20200803のvue.jsに関する記事は18件です。

Vue.js理解のためのテンプレート作成

Vue.js理解のためのテンプレート作成

作業手順

  1. Vue.jsの読み込み
  2. Vueインスタンスの作成
  3. 空のルートテンプレートの作成
  4. ルートテンプレートをマウント
  5. dataオプションにsampleTextプロパティを追加
  6. sampletextプロパティの内容をマスタッシュ構文で画面に表示
  7. urlプロパティを作成
  8. v-bindを使ってurlプロパティのリンクを設置
  9. v-showでリンクを非表示にする
  10. v-forで配列をリスト表示
  11. onclickイベントでsampleTextプロパティの値を置き換える
  12. v-modelでsampleTextプロパティの内容を変更

1. Vue.jsの読み込み

CDNで読み込む
・body終端タグの上に記述。
・真下でjsファイルを読み込む。

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>


2. Vueインスタンスの作成

.js
var app = new Vue({

})


3. 空のルートテンプレートの作成

.html
<body>
    <div id="app">

    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>


4. ルートテンプレートをマウント

.js
var app = new Vue({
    el: '#app',
})


5. dataオプションにsampleTextプロパティを追加

.js
var app = new Vue({
    el:'#app',
    data:{
        sampleText:'hello world!'
    }
}


6. sampleTextプロパティの内容をマスタッシュ構文で画面に表示

.html
<body>
    <div id="app">
        <p>{{sampleText}}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>


7. urlプロパティを作成

.js
var app = new Vue({
    el:'#app',
    data:{
        sampleText:'hello world!',
        url:'https://jp.vuejs.org/'
    }
})


v-bindを使ってurlプロパティのリンクを設置

.html
<body>
    <div id="app">
        <p>{{sampleText}}</p>
        <a v-bind:href="url">vue.js公式サイト</a>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>


9. v-showでリンクを非表示にする

.js
var app = new Vue({
    el:'#app',
    data:{
        sampleText:'hello world!',
        url:'https://jp.vuejs.org/',
    // ▼追加
        toggle: false
    }
})
.html
<body>
    <div id="app">
        <p>{{sampleText}}</p>
        <!-- ▼追加 -->
        <p v-show="toggle">
            <a v-bind:href="url">vue.js公式サイト</a>
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>


10. v-forで配列をリスト表示

.js
var app = new Vue({
    el:'#app',
    // ▼追加
    data:{
        sampleText:'hello world!',
        url:'https://jp.vuejs.org/',
        toggle: false,
        prefs: ['tokyo',
                'kanagawa',
                'chiba',
                'saitama']
    }
})
.html
<body>
    <div id="app">
        <p>{{sampleText}}</p>
        <p v-show="toggle">
            <a v-bind:href="url">vue.js公式サイト</a>
        </p>
        <!-- ▼追加 -->
        <ol>
            <li v-for="pref in prefs">{{pref}}</li>
        </ol>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>


11. onclickイベントでsampleTextプロパティの値を置き換える

.html
<body>
    <div id="app">
        <p>{{sampleText}}</p>
        <p v-show="toggle">
            <a v-bind:href="url">vue.js公式サイト</a>
        </p>
        <ol>
            <li v-for="pref in prefs">{{pref}}</li>
        </ol>
        <button v-on:click="clickHandler">クリック</button>
        <!-- ▼追加 -->
        <p>
            <input type="text" v-model="sampleText">
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>



▼ブラウザの表示
image.png


▼ボタンクリック後
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.jsでurl('@/assets/xxx.png')がエラーになる時の解決策

Nuxt.jsでurl('@/assets/xxx.png')がエラーになる時の解決策

nuxt 2.13.0で確認。

背景画像を設定したい時など、以下のように書くことがある。

index.vue(抜粋)
<style>
.test-class {
  background-image: url('@/assets/xxx.png');
}
</style>

しかし、これでは以下のようなエラーが出てしまう。

Module not found: Error: Can't resolve './@/assets/xxx.png' in ...

見ての通り、./が追加されて相対パスとみなされてしまっている。

正しくは以下のように書く。

index.vue(抜粋)
<style>
.test-class {
  background-image: url('~assets/xxx.png');
}
</style>

参考:アセット - NuxtJS

Warning: Nuxt 2.0 からは ~/ エイリアスは CSS ファイルで正しく解決されないでしょう。 CSS の参照には、~assets(スラッシュなし)か、@ のエイリアスを使わなければなりません。 例:background: url("~assets/banner.svg")

ただし、@assets/xxx.png@/assets/xxx.png~/assets/xxx.pngも試した限りはNG。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡易SPARQLエンドポイント検索アプリをjQueryからVue.jsに書き直しました

RDFデータベースをSPARQLと呼ばれるクエリ言語で検索できるWeb API「SPARQLエンドポイント」を公開するオープンデータサイトが行政を中心に増えています。例えば、

これらのサイトの多くは、Webブラウザ上でSPARQLクエリを実行して、表形式で検索結果を見られるようになっています。
しかし、Web APIなので本来は仕様に基づいた形式でエスケープ処理をしたSPARQLクエリをGETもしくはPOSTしなけれならないので、上記のような気の利いたページを公開しないSPARQLエンドポイントもあります。

そこでだいぶ前にWebブラウザ上でSPARQLエンドポイントへ簡単にSPARQLクエリを実行できて、その結果を見やすい表形式で表示するWebアプリをjQueryで作りました。

JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)
https://github.com/uedayou/simple-sparqlsearch-js

JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)

かなり古いアプリですが、今でも問題なく動きます。ただ、今見るとコードの可読性が悪いなとおもいます。とても短いコードでもあるので、jQueryを使わずに書き直すことにしました。

Vue.js で書き直し

今回は Vue.js を使うことにしました。React, AngularでもWebアプリを書いたことがありますが、今回はできるだけコード量を減らして、Webpack等でのビルドを行わない形にしたかったので、Vue.jsを使いました。

Vue.jsによるSPARQLエンドポイント検索アプリ
https://github.com/uedayou/simple-sparqlsearch-vue

Vue.jsによるSPARQLエンドポイント検索アプリ

機能としては全く同じですが、検索した結果をJSONファイルでダウンロードできるようにしました。

ダウンロード

検索結果が表示されているときに、ダウンロードボタンを押すとJSONファイルがダウンロードできます。

Vue.js vs jQuery

Vue.js で書き直してみて、jQueryのコードよりも個人的には格段に可読性が上がったと思います。
jQueryは、DOM操作をコード内で行わないといけなかったのが、Vue.jsだとDOMにデータをバインディングできるので書き直すのも楽にできました。

Vue.jsのコード
Vue.use(VueLoading);
Vue.component('loading', VueLoading);

var app = new Vue({
  el: '#app',
  data: {
    query: 'select * where {?s ?p ?o} LIMIT 10',
    results: {
      data: null,
      head: [],
      body: [],
    },
  },
  methods: {
    doSearch: function() {
      var loader = this.$loading.show();
      var that = this;
      axios.get(
        endpoint+"?query="+encodeURIComponent(this.query),
        { headers: {'Accept': 'application/sparql-results+json'} })
      .then(function(res) {
        that.results.data = res.data;
        that.results.head = res.data.head.vars;
        that.results.body = res.data.results.bindings;
      })
      .catch(function(error) {
        console.log(error);
        alert("Error!");
      })
      .then(function() {
        loader.hide();
      });
    },
    downloadData: function() {
      var filename = "results.json";
      var a = document.createElement('a');
      var uriContent = 'data:application/octet-stream,'+encodeURIComponent(JSON.stringify(this.results.data));
      a.setAttribute('href', uriContent);
      a.setAttribute('download', filename);
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    }
  }
})

jQueryのコード
(function(){
  $.fn.modal.defaults.spinner = $.fn.modalmanager.defaults.spinner = '<div class="loading-spinner" style="width: 200px; margin-left: -100px;"><div class="progress progress-striped active"><div class="progress-bar" style="width: 100%;"></div></div></div>';
  $('#find_query').click(function(){
    $('body').modalmanager('loading').find('.modal-scrollable').off('click.modalmanager');
    qr = sendQuery(endpoint,encodeURIComponent($('#query_area').val().replace(/[\n\r]/g,"")));
    qr.fail(
      function (xhr, textStatus, thrownError) {
        $('body').modalmanager('removeLoading');
        alert("Error: A '" + textStatus+ "' occurred.");
      }
    );
    qr.done(
      function (d) {
        $('body').modalmanager('removeLoading');
        $('body').removeClass('modal-open');
        result_table(d.results.bindings);
      }
    );
  });
  $('#result_div').hide();
}());

var result_table = function(data){
  var result_div = $('#result_div');
  var table = $('#result_list')[0];
  if (table == undefined) {
    result_div.append($('<table></table>').attr({
      'id' : 'result_list',
      'class' : 'table'
    }));
    table = $('#result_list')[0];
  }
  while (table.rows.length > 0) { table.deleteRow(0); }
  if (data instanceof Array) {
    result_div.show();
    var header = table.createTHead();
    var headerRow = header.insertRow(0);
    id = 1;
    for (var d = 0; d < data.length; d++) {
      var row1 = table.insertRow(d + 1);
      if (d == 0) {
        for ( var key in data[0]) {
          var th = document.createElement('th');
          var label = key;
          th.innerHTML = key;
          headerRow.appendChild(th);
        }
      }
      var i = 0;
      for ( var key in data[d]) {
        var cell = row1.insertCell(i++);
        var value = data[d][key];
        if (value.value != undefined){value = value.value;}
        if (value == null) {value = '';}
        var link = true;
        if (link) {
          if (value != null && value.indexOf("http://") == 0) {
            value = '<a href="'+value+'" target="_blank">'+value+'</a>';
          }
        }
        cell.innerHTML = value;
      }
    }
  }
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SPARQLエンドポイント簡易検索アプリをjQueryからVue.jsに書き直しました

RDFデータベースをSPARQLと呼ばれるクエリ言語で検索できるWeb API「SPARQLエンドポイント」を公開するオープンデータサイトが行政を中心に増えています。例えば、

これらのサイトの多くは、Webブラウザ上でSPARQLクエリを実行して、表形式で検索結果を見られるようになっています。
しかし、Web APIなので本来は仕様に基づいた形式でエスケープ処理をしたSPARQLクエリをGETもしくはPOSTしなけれならないので、上記のような気の利いたページを公開しないSPARQLエンドポイントもあります。

そこでだいぶ前にWebブラウザ上でSPARQLエンドポイントへ簡単にSPARQLクエリを実行できて、その結果を見やすい表形式で表示するWebアプリをjQueryで作りました。

JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)
https://github.com/uedayou/simple-sparqlsearch-js

JavaScriptによるSPARQL利用サンプル(クエリ検索アプリ)

かなり古いアプリですが、今でも問題なく動きます。ただ、今見るとコードの可読性が悪いなとおもいます。とても短いコードでもあるので、jQueryを使わずに書き直すことにしました。

Vue.js で書き直し

今回は Vue.js を使うことにしました。React, AngularでもWebアプリを書いたことがありますが、今回はできるだけコード量を減らして、Webpack等でのビルドを行わない形にしたかったので、Vue.jsを使いました。

Vue.jsによるSPARQLエンドポイント検索アプリ
https://github.com/uedayou/simple-sparqlsearch-vue

Vue.jsによるSPARQLエンドポイント検索アプリ

機能としては全く同じですが、検索した結果をJSONファイルでダウンロードできるようにしました。

ダウンロード

検索結果が表示されているときに、ダウンロードボタンを押すとJSONファイルがダウンロードできます。

Vue.js vs jQuery

Vue.js で書き直してみて、jQueryのコードよりも個人的には格段に可読性が上がったと思います。
jQueryは、DOM操作をコード内で行わないといけなかったのが、Vue.jsだとDOMにデータをバインディングできるので書き直すのも楽にできました。

Vue.jsのコード
Vue.use(VueLoading);
Vue.component('loading', VueLoading);

var app = new Vue({
  el: '#app',
  data: {
    query: 'select * where {?s ?p ?o} LIMIT 10',
    results: {
      data: null,
      head: [],
      body: [],
    },
  },
  methods: {
    doSearch: function() {
      var loader = this.$loading.show();
      var that = this;
      axios.get(
        endpoint+"?query="+encodeURIComponent(this.query),
        { headers: {'Accept': 'application/sparql-results+json'} })
      .then(function(res) {
        that.results.data = res.data;
        that.results.head = res.data.head.vars;
        that.results.body = res.data.results.bindings;
      })
      .catch(function(error) {
        console.log(error);
        alert("Error!");
      })
      .then(function() {
        loader.hide();
      });
    },
    downloadData: function() {
      var filename = "results.json";
      var a = document.createElement('a');
      var uriContent = 'data:application/octet-stream,'+encodeURIComponent(JSON.stringify(this.results.data));
      a.setAttribute('href', uriContent);
      a.setAttribute('download', filename);
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    }
  }
})

jQueryのコード
(function(){
  $.fn.modal.defaults.spinner = $.fn.modalmanager.defaults.spinner = '<div class="loading-spinner" style="width: 200px; margin-left: -100px;"><div class="progress progress-striped active"><div class="progress-bar" style="width: 100%;"></div></div></div>';
  $('#find_query').click(function(){
    $('body').modalmanager('loading').find('.modal-scrollable').off('click.modalmanager');
    qr = sendQuery(endpoint,encodeURIComponent($('#query_area').val().replace(/[\n\r]/g,"")));
    qr.fail(
      function (xhr, textStatus, thrownError) {
        $('body').modalmanager('removeLoading');
        alert("Error: A '" + textStatus+ "' occurred.");
      }
    );
    qr.done(
      function (d) {
        $('body').modalmanager('removeLoading');
        $('body').removeClass('modal-open');
        result_table(d.results.bindings);
      }
    );
  });
  $('#result_div').hide();
}());

var result_table = function(data){
  var result_div = $('#result_div');
  var table = $('#result_list')[0];
  if (table == undefined) {
    result_div.append($('<table></table>').attr({
      'id' : 'result_list',
      'class' : 'table'
    }));
    table = $('#result_list')[0];
  }
  while (table.rows.length > 0) { table.deleteRow(0); }
  if (data instanceof Array) {
    result_div.show();
    var header = table.createTHead();
    var headerRow = header.insertRow(0);
    id = 1;
    for (var d = 0; d < data.length; d++) {
      var row1 = table.insertRow(d + 1);
      if (d == 0) {
        for ( var key in data[0]) {
          var th = document.createElement('th');
          var label = key;
          th.innerHTML = key;
          headerRow.appendChild(th);
        }
      }
      var i = 0;
      for ( var key in data[d]) {
        var cell = row1.insertCell(i++);
        var value = data[d][key];
        if (value.value != undefined){value = value.value;}
        if (value == null) {value = '';}
        var link = true;
        if (link) {
          if (value != null && value.indexOf("http://") == 0) {
            value = '<a href="'+value+'" target="_blank">'+value+'</a>';
          }
        }
        cell.innerHTML = value;
      }
    }
  }
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】vue.jsの基礎

【Vue.js】vue.jsの基礎

jQueryの進化版。jQueryよりも簡単に記述できる。

目次
1. Vue.jsを読み込む方法(3種類)
2. ルートテンプレート
3. データバインディング
4. エラーの確認
5. ディレクティブ
6. v-bind
7. v-if
8. v-show
9. v-for
10. v-on:click
11. v-model
12. コンポーネント

Vue.jsを読み込む方法(3種類)

  1. CDN
    https://jp.vuejs.org/v2/guide/installation.html

− Vue.jsの読み込みはソースコードにCDNのURLを貼り付け
 ┗ body末尾の前に挿入
 ┗ 下でjsファイルを読み込む(※逆だと動作しない可能性あり)

html
<body>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="js/main.js"></script>
</body>

※versionを指定する。

  1. 直接読み込み
  2. NPM

▶︎基本的にCDNを使用。


veuインスタンスの生成(jsファイル)

js
var app= new Vue({})

*変数名は任意。
 ┗ app, vm(view model)を使うことが多い


ルートテンプレート

vue.jsの記述を描画するエリア。htmlソースコード上に記述。
elオブジェクトを使用。

▼.js側の記述(Vueインスタンス内)
el='#id名'
┗ elはelementの略

▼.html側の記述
<div id='id名'>

.js
new Vue({
    el: '#app',
})


紐づく

.html
<div id ="app">
</div>

▼表示結果
image.png


データバインディング

データバインディングとは?

データ反映
 ┗ 文字のリアルタイム反映
 ┗ jsの場合、keyupイベントハンドラを使用など

Vueインスタンスのdataオプションと、HTMLソースコードのマスタッシュ構文が紐づく。

種類

  • 単方向バインディング
  • 双方向バインディング
  • ワンタイムバインディング

記述

▼.jsの記述
data:{message:'出力するテキスト'}

▼.htmlの記述
<p>{{message}}</p>
 ┗ {{}}:マスタッシュ構文(ヒゲのように見えるため)

.js
var app = new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue.js'
    }
})
.html
<body>
    <div id ="app">
        <p>
            {{message}}
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

Dataオブジェクトの設定

プロパティは任意で設定できる。
設定した値はマスタッシュ構文で呼び出す。

  • message: 'テキスト'
  • number: 0
  • user: ユーザー名
    • lastname: '苗字'
    • firstName: '名前'
    • prefecture: '都道府県'
  • 配列名: ['AAA', 'BBB', 'CCC']

データの呼び出し

・通常:オブジェクト名
・入れ子:オブジェクト名.プロパティ
・配列:配列名[番号]

.js
var app = new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue.js',
        count: 100,
        user: {
            lastName: 'Todoroki',
            firstName: 'Takashi',
            prefecture: 'Tokyo'
        },
        colors:['red','blue','yellow']
    }
})
.html
<body>
    <div id ="app">
        <p>
            {{message}}
        </p>
        <p>
            {{count}}
        </p>
        <p>
            {{user.lastname}}
            {{user.prefecture}}
        </p>
        <p>
            {{colors[0]}}
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼出力結果
image.png


エラーの確認

・ChromeのDeveloperツールを使用(F12)
・consoleで読み込めていないデータが確認できる。

image.png

→ messageが読み込まれていない。
 {{message}}のデータバインディングを確認。


ディレクティブ

ディレクティブ(directive)とは?

HTMLタグの属性でvue.jsを適用する。-vで始まる。
htmlタグの属性でデータバインディング(vue.jsの内容を反映)を行うなど。

主なディレクティブの種類

  • v-bind
  • v-if
  • v-show
  • v-for
  • v-on
  • v-model

v-bind

nameなどの属性でデータバインディングを行う。
v-bind属性名="プロパティ名"

▼inputタグで使用する例
<input type="text" v-bind:value="message">

.html
<body>
    <div id ="app">
        <input type="text" v-bind:value="message">
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

image.png

※値にマスタッシュ構文は使えない※
×: <input type="text" value="{{message}}">
×: <input type="text" value="message">

v-if

要素をDOMごと表示/削除を切り替える。
※頻繁に表示を切り替える時は、v-showディレクティブを使う。
 − true:表示
 − false:非表示
 

▼htmlタグの記述
< v-if="プロパティ名"></>

▼vue.jsの記述
data:{プロパティ名:真偽値}

.js
var app = new Vue({
    el: '#app',
    data: {
        aaa:false
    }
})
.html
<body>
    <div id ="app">
        <p v-if="aaa">
            hello
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

→ ブラウザに「hello」が表示されない。(falseのため)

image.png

v-show

要素の表示・非表示を切り替える。
 ┗ cssのdisplayプロパティと同じ
 ┗ v-ifより軽い(DOM← →要素)

▼htmlタグの記述
< v-show="プロパティ名"></>

▼vue.jsの記述
data:{プロパティ名:真偽値}

.js
var app = new Vue({
    el: '#app',
    data: {
        aaa:false
    }
})
.html
<body>
    <div id ="app">
        <p v-show="aaa">
            hello
        </p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼v-ifと違いdispaly:noneとなっている
image.png

v-for

配列オブジェクトの繰り返し表示。
 ・マスタッシュ構文を使う。
 ・配列と、複数の値をもつプロパティのどちらでも使用可
 ・キーを抜き出すことも可能

<要素名 v-for="変数 in 配列オブジェクト名">{{変数}}</要素名>

①値の繰り返し

配列の場合

.js
var app = new Vue({
    el: '#app',
    data: {
     colors:['red','blue','yellow']
    }
})
.html
<body>
    <div id ="app">
        <ol>
            <li v-for="color in colors">{{color}}</li>
        </ol>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

複数のプロパティの場合

.js
var app = new Vue({
    el: '#app',
    data: {
     user: {
            lastName: 'Todoroki',
            firstName: 'Takashi',
            prefecture: 'Tokyo',
            age:'34'
    }
})
.html
<body>
    <div id ="app">
        <ul>
            <li v-for=" value in user">{{value}}</li>
        </ul>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

②キーとプロパティの繰り返し

オブジェクトの「キー:プロパティ」の両方を抜き出す。
 ※第1引数がvalue, 第2引数がkey

<要素名 v-for="(値の変数,キーの変数) in 配列オブジェクト名">{{変数}}</要素名>`

.js
var app = new Vue({
    el: '#app',
    data: {
     user: {
            lastName: 'Todoroki',
            firstName: 'Takashi',
            prefecture: 'Tokyo',
            age:'34'
    }
})
.html
<body>
    <div id ="app">
        <ul>
            <li v-for=" (value, key) in user">{{key}}: {{value}}</li>
        </ul>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

v-on:click

onclickイベントの設定。

・Vueインスタンスの中にmethodsを記述。
・methodsの中に、onclickイベントを記述。

▼htmlタグの記述
<要素 v-on:click="onclick"></>

▼vue.jsの記述
methods:{onclick: function(){処理}}

.js
var app = new Vue({
    el: '#app',
    data: {},
    methods:{
        onclick: function(){
            alert('クリックイベント発動')
        }
    }
})
.html
<body>
    <div id ="app">
      <button v-on:click="onclick">クリック</button>
   </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

ボタンをクリックすると現在時刻を表示するプログラム

▼現在時刻の表示(javascript)
new Date().toLocaleString()
 ┗ Date(): 国コードまで含んだフルの時刻を表示
 ┗ toLocaleString()とすることで、 yyyy/m/d hh: mm:ss

▼データバインディング
オブジェクトを設定し、this.で呼び出して代入する。

.js
var app = new Vue({
   el: '#app', 
   data: {
        now: '',
    },
    methods:{
        onclick: function(){
            this.now =   new Date().toLocaleString();
        }
    }
})
.html
<body>
    <div id ="app">
      <button v-on:click="onclick">クリック</button>
    <p>
         {{now}}
      </p>
   </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

v-model

双方向データバインディング

▼単方向と双方向の違い

・単方向データバインディング
 ┗ マスタッシュ構文で、テンプレートにdataオブジェクトのプロパティを反映する。(v-bind)

・双方向データバインディング
 ┗ jsのdata側とテンプレート側の両方で変更可能。
 ┗ テンプレートの値を変更すると、dataオブジェクトのプロパティも変更される。

★双方向データバインディングを簡単に実装できるのは、vue.jsの利点。

v-model="プロパティ名"
 ┗ タグ要素とvue.jsのプロパティが結びつく

.js
var app = new Vue({
 data: {
    el: '#app',
    data: {
        message: '双方向バインディング'
    }
})
.html
<body>
    <span>セルのテキスト変更で、dataプロパティの値も変更</span>
    <div id ="app">
        <p>
            <input type="text" v-model="message">
        </p>
        <pre>{{$data}}</pre>
    </div>    
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

dataプロパティの中身をテンプレートに表示する

<pre>{{$data}}</pre>

・「\$el」や「\$methods」は表示できない。


コンポーネント

UIパーツ。定義したコンポーネントを要素として呼び出せる。

▼vue.componentメソッドの構文
Vue.component(コンポーネント名, {コード})

※注:vueインスタンスよりも前に記述する。

▼コンポーネントの呼び出し
<コンポーネント名></コンポーネント名>

.js
Vue.component('test-component', {
    template: '<p>Vue-componentのテスト</p>'
})


var app = new Vue({
    el: '#app',
    data:{

    }
})
.html
<body>
    <div id ="app">
        <test-component></test-component>
        <test-component></test-component>
    </div>    
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="js/main.js"></script>
</body>

▼ブラウザの表示
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel+Vue.jsで[Vue warn]: Error compiling templateを解消する方法

LaravelからVue.jsを使おうとすると、Chromeの開発者ツール上で表題のワーニングが表示される。
具体的には以下。

[Vue warn]: Error compiling template:

Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as <script>, as they will not be parsed.

45 |          <example-component></example-component>
46 |      </div>
47 |      <script src="http://localhost/js/app.js"></script></div></main></div>
   |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

(found in <Root>)

原因は、Vue.jsのコンポーネントを使うためのapp.jsが、bodyの一番最後ではなく中途半端な位置のためと思われる。

基盤のBladeであるapp.blade.phpに以下を埋め込んでおく。

...
    @yield('end_of_body') →これを入れる
</body>
</html>

そして、Vue.jsのコンポーネントを呼び出す側のBladeで、以下のように書いておくとワーニングが消えた。

@section('end_of_body')
    <script src="{{ asset('/js/app.js') }}"></script>
@endsection
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクトのKeyの値を変更する

個人メモ

array: [
  { id: 1, text: 'test' },
  { id: 2, text: 'test' },
  { id: 3, text: 'test' },
  { id: 4, text: 'test' },
  { id: 5, text: 'test' }
]

この配列のtesttestTextというKey名に変える。

スクリーンショット 2020-08-03 午後21.22.44 午後.png

changeKey () {
  const array = this.array.map(value => {
    return {id: value.id, testText: value.text}
  })
  return array
}

スクリーンショット 2020-08-03 午後21.24.42 午後.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueのOptionsAPI導入して「Property 'hoge' does not exist on type 'CombinedVueInstance<Vue…」みたいなエラーが出る

問題提起

Vue.jsのTypeScriptを簡単に導入したくてOptionsAPIを導入してみた!
今までのVue.jsの書き方に export default Vue.extend({})とするだけで簡単だったけど何故かエラーを吐く

Property 'hoge' does not exist on type 'CombinedVueInstance<Vue …省略

また、Vuexを使っている場合はmapGettersmapActionsなどで呼び出した関数のエラーが消せない。

結論: optionsの型を明示的に指定しようぜ!!

この形を基本形にして使いたい所の型を指定するだけ!!

<script lang="ts">
import Vue from 'vue'
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options'

interface Data {}
interface Methods {}
interface Computed {}
interface Props {}

const options:  ThisTypedComponentOptionsWithRecordProps<
  Vue,
  Data,
  Methods,
  Computed,
  Props
> = {
  data () {},
  computed () {},
  methods: {},
}

export default Vue.extend(options)
</script>

dataの型指定

特に何も考えずに指定するだけです。

<script lang="ts">
import Vue from 'vue'
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options'

interface Data {
  reutrn {
    form: {
      name: string|null;
      age: number|null;
      email: string|null;
    }
  }
}
interface Methods {}
interface Computed {}
interface Props {}

const options:  ThisTypedComponentOptionsWithRecordProps<
  Vue,
  Data,
  Methods,
  Computed,
  Props
> = {
  data () {
    form: {
      name: null,
      age: null,
      email: null
    }
  },
  computed () {},
  methods: {},
}

export default Vue.extend(options)
</script>

computedの型指定

今回はisLogin: boolean;のように型指定してみました。
関数で指定する場合はisLogin: () => boolean;みたいに指定できます!
もちろんVuexのmapGettersの関数の型も指定できるので便利です。

<script lang="ts">
import Vue from 'vue'
import { mapGetters } from 'vuex'
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options'
import { UserTypes } form '@/types/user'

interface Data {}
interface Methods {}
interface Computed {
  isLogin: boolean;
  loginUser: UserTypes;
  loginUserEmail: string;
}
interface Props {}

const options:  ThisTypedComponentOptionsWithRecordProps<
  Vue,
  Data,
  Methods,
  Computed,
  Props
> = {
  data () {},
  computed () {
    ...mapGetters(['isLogin'])
    ...mapGetters('modules/user', ['loginUser']),
    loginUserEmail () {
       return this.loginUser.email
    }
  },
  methods: {},
}

export default Vue.extend(options)
</script>

methodsの型指定

もう慣れてきたと思うのでテキトーに書きました。
また、返り値の無い関数ならvoid指定してあげるだけでOKです。

<script lang="ts">
import Vue from 'vue'
import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options'
import { UserTypes } form '@/types/user'

interface Data {}
interface Methods {
  getLoginUser: () => Promise<UserTypes>;
  createUser: (user: UserTypes) => Promise<boolean>;
  isAdule: (age: number) => boolean;
}
interface Computed {}
interface Props {}

const options:  ThisTypedComponentOptionsWithRecordProps<
  Vue,
  Data,
  Methods,
  Computed,
  Props
> = {
  data () {},
  computed () {},
  methods: {
    ...mapActions(['getLoginUser', 'createUser'])
    isAdule (age) {
      return age >= 20
    }
  },
}

export default Vue.extend(options)
</script>

propsの型指定

ちょっと割愛します。
もし知りたければ東京都のCOVID19対策サイトのOSSのソースコードが上がっているので参考にしてみてください!

https://github.com/tokyo-metropolitan-gov/covid19/blob/development/components/MonitoringConsultationDeskReportChart.vue#L126

参考

tokyo-metropolitan-gov/covid19 - GitHub

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【プラグイン導入】SyntaxError Cannot use import statement outside a module【Nuxt.js】

参考対象者

  • 外部ライブラリをNuxt.jsに導入したい方
  • npmでプラグイン等を管理している方

環境

package.json
{
  "name": "nuxt-proj",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "nuxt": "^2.14.0"
  },
  "devDependencies": {}
}

状況

外部ライブラリのvue-flag-iconを導入したいが、SyntaxErrorになってしまう。

SyntaxError
Cannot use import statement outside a module

エラーにならない、プラグイン導入方法(node_modulesで管理)

  1. プラグインをnpmで、インストール
  2. pluginsディレクトリに、プラグインをimportするファイルを作成
  3. nuxt.config.jsファイルに、importファイルを読み込む設定をする
  4. サーバーの再起動

プラグインをnpmで、インストール

まずは、今回のプラグインであるvue-flag-iconをインストールしていきます。

$ npm i vue-flag-icon@latest

すると自動でpackage.jsonにも記述されますね。

package.json
"dependencies": {
    "nuxt": "^2.14.0",
    "vue-flag-icon": "^1.0.6"
  },

pluginsディレクトリに、プラグインをimportするファイルを作成

プラグイン公式(https://github.com/vikkio88/vue-flag-icon)
にもあるように、必要な記述を設定していきます。

そしてその設定は、どのファイルからでもライブラリを読み込めるように、pluginディレクトリというVue.jsをインスタンス化する前に読み込んでくれるJSファイルの置き場所に、新規ファイルでしていく。

plugins/flag-icon.js
import Vue from 'vue';                  //Vueを定義するために、Vueをimport
import FlagIcon from 'vue-flag-icon';
Vue.use(FlagIcon);

nuxt.config.jsファイルに、importファイルを読み込む設定をする

あとは、先ほど設定したimportファイルを、Nuxt.js自体の設定ファイルに読み込む記述をする。

nuxt.config.js
export default {
  //省略
  plugins: [
    '@/plugins/flag-icon',
  ],
  //省略
}

ただ、これだけだとエラーになってしまう。
原因は、今回のライブラリの管理方法がnpmなので、さらに専用の設定が必要なため。

とは言っても、あと1行だけ追記すればいいのだが、

nuxt.config.js
export default {
  //省略
  build: {
    transpile: ['vue-flag-icon']
  }
}

これにて、ライブラリが使えるようになる。

サーバーの再起動

と、よく忘れますが、nuxt.config.jsファイルに変更を加えた場合、サーバーの再起動をする必要があるので、

$ npm run dev

して実際に使ってみましょう!!

実際に、使ってみる

どのVueファイルでも良いので、<flag iso="jp" />を追加してみると、

pages/index.vue
<template>
  <div>
    //省略
    <flag iso="jp" />    
  </div>
</template>

我らが日本の国旗が、表示されるであろう!

参考

Nuxt.js公式様
https://ja.nuxtjs.org/guide/plugins/

@fj_yohei様
https://qiita.com/fj_yohei/items/cddf267a94fa30ecb0b8

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アップロードしたCSVファイルをオブジェクト形式に変換するモジュール(Vue)をつくりました

概要

目的

CSVアップロードしてVueの変数に格納して使いたい!
よく使うし、外部モジュール化しておいて必要な時にファイル読み込んできて使いたい!
ということで、外部からインポートして使えるCSV読込コンポーネントを作成しました。

JavaScript、非同期覚えたてなところがあるので、おかしいところあればご指摘いただけると嬉しいです。

こちらを参考にさせていただきました。
- Vue.jsでCSVをインポート
- JavaScript, Python, Ruby で多次元配列を zip で作ってループする場合のメモ
- 【JavaScript】FileAPIからCSVを読み込んでみる

コード

Form.vue
<template>
  <div class="container">
    <form action="">
      <input @change="csvUpload" type="file">
    </form>
  </div>
</template>

<script>
import csvUpload from '../lib/csvUpload'; //libフォルダに格納してます

export default {
  data(){
    return {
      csvJson: {}
    }
  },
  methods:{
    async csvUpload(event){
      const res = await csvUpload.csvUpload(event)
      if(res[0]){
        this.csvJson = res[1]
      } else{
        alert(res[1]);
      }
    }
  }
}
</script>

<style>
省略
</style>
csvUpload.js
function checkFileReader(){
    return window.File && window.FileReader && window.FileList && window.Blob
}

export default {
  csvUpload(event){
    return new Promise(function(resolve,reject){
      // ブラウザチェック
      if(!checkFileReader()){
        reject([ false, 'FileAPI非対応のブラウザです。'])
      }

      const file = event.target.files[0]
      const reader = new FileReader()

      // ファイル読込完了
      const mailJson = []
      const loadFunc = () => {
        const lines = reader.result.split('\n')
        const header = lines[0].split(',')
        lines.shift()

        // 一行ずつ読込
        lines.forEach(eachLine => {
          const elements = eachLine.split(',')
          if(elements.length != header.length){
            return
          }

          // 一列ずつ読込
          const line = {}
          const zip = (array1, array2) => array1.map((_, i) => [array1[i], array2[i]])
          zip(header, elements).forEach(eachElement => {
            line[eachElement[0]] = eachElement[1]
          })

          mailJson.push(line)
        })
        resolve([ true, mailJson])
      }
      reader.onload = loadFunc
      reader.onerror = () => {
        reject([ false,  'ファイルの読込に失敗しました'])
      }
      reader.readAsText(file)
    })
  }
}

詳細

外部コンポーネントの読込

Form.vue
import csvUpload from '../lib/csvUpload'; //libディレクトリに格納してます

libディレクトリに格納しているcsvUpload.jsを読み込んでいます。

メソッドを定義する

Form.vue
  methods:{
    async csvUpload(event){
      const res = await csvUpload.csvUpload(event)
      if(res[0]){
        this.csvJson = res[1]
      } else{
        alert(res[1]);
      }
    }
  }

今回は、csvUploadというメソッドを準備し、そのメソッドの中で外部モジュールを実行しています。
csvUpload.csvUploadはPromiseを返す関数になっています。

awaitをつけることで、Promiseが実行された結果をresで受け取ることができます。
無事にファイル読み込みできたら[ true, ファイルオブジェクト]、エラーが出たら[ false, エラーメッセージ ]が返ってくるようにしています。
ここでは、vueのdataに定義したcsvJsonという変数に格納されるようにしています。

フォーム部品をつくる

Form.vue
    <form action="">
      <input @change="csvUpload" type="file">
    </form>

@changeでメソッドを指定すると、ファイル選択ボタンが押されたときに先ほど定義したcsvUploadメソッドが実行されます。

CSVの変換処理

ここから、呼び出してきたcsvUploadモジュールで何をしているか説明していきます。

戻り値はPromise

csvUpload.js
export default {
  csvUpload(event){
    return new Promise(function(resolve,reject){
      処理内容
    }
  }
}

Promiseオブジェクトを戻してやることによって、呼び出し元でasync,awaitが使えるようになります。
Promise内では、resolve、rejectが関数にとってのreturnみたいな形で働きます。
Promiseを呼び出すときにawaitをつけてやることで、resolve,rejectでの戻り値をちゃんと受け取れるんですね。

ファイル形式のチェック

MIMEタイプで判定しようと思いましたが、単純に「text/csv」とかだけじゃないみたいなので、あきらめました。

ファイル容量のチェック

簡単にできそうなので、あとで付け足したいと思います。

FileAPI・FileReaderによるファイルの読込

csvUpload.js
      reader.onload = loadFunc
      reader.onerror = () => {
        reject([ false,  'ファイルの読込に失敗しました'])
      }
      reader.readAsText(file)

FileReader.readAsText() メソッドは非同期処理です。ファイルの読込が完了しないまま、次の処理に進んでしまいます。
ファイルの読込が完了したか判断するには、onloadイベントを使い、ファイルの読み込みにエラーが起きたか判断するには、onerrorイベントを使います。
onload,onerrorは、そのイベントが起きた時に何を実行するか、関数を指定しておく形になります。

ここで詰まりました。
onloadで関数指定しますけど、returnつかったら戻り値受け取れないじゃん!!ってなりました。
結果的に、Promiseで囲ってから、onloadに指定した関数からresolveでPromiseを解決するという手段をとりました。

終わりに

せっかくresolveとrejectを使っているのに、実行完了のときは[true,obj],失敗の時は[false,err]を返すのって、スマートじゃないですよね。resolveとrejectの判定について勉強不足なので、これからどうやって書いていったら良いか考えてみたいと思います。

あと、csvUpload.csvUploadの部分、モジュール名と関数名が同じで、続けて書くという書き方気に入ってないんですけど、どういう書き方するとスマートになるか教えてください・・・

また、forEachの外に定義した配列を、forEachで変えていくのも良くなさそうです。この辺もまだやり方わかりませんが、勉強したいと思います。

このへんちょっとずつ直していって安心して使っていただけるようなモジュールを作りたいです。
他にもおかしいところあったらぜひご指摘くださいm(__)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLIのactive-classを使ってCSSを適用させる

やりたいこと

スクリーンショット 2020-08-03 16.00.18.png
スクリーンショット 2020-08-03 16.00.25.png

現在表示しているページのアイコンだけ色を変える。
Vuetifyを使います。

やり方

active-class属性をつけ、cssを適用する。

<router-link active-class="link-active" :to="{name:'Fav'}"> //ハート
 <v-btn icon>
  <v-icon>mdi-heart</v-icon>
 </v-btn>
</router-link>

<router-link active-class="link-active" :to="{name: 'Search'}"> //検索 
 <v-btn icon>
  <v-icon>mdi-magnify</v-icon>
 </v-btn>
</router-link>
<style scoped>
a{
  text-decoration: none;
}
.link-active .v-btn--icon .v-icon{
  color: rgb(202, 96, 114);
}
</style>

.v-btn--icon .v-iconをつけることを忘れない。

問題点

選択されていないlinkにもcssが適用されてしまう。

スクリーンショット 2020-08-03 15.54.48.png

検索のページでは、ハートのアイコンは適用してほしくないが
URLが一致しているとそれ以降は全てactiveになってしまうため、ハートと検索のページのアイコンにcolorが適用されてしまう。

解決策

exactを追加

active-class属性にexactを追加することによってURLが完全に一致する時でないとactiveにならない。

最終的なコード

<router-link active-class="link-active" exact  :to="{name:'Fav'}">
 <v-btn icon>
  <v-icon>mdi-heart</v-icon>
 </v-btn>
</router-link>

<router-link active-class="link-active" exact  :to="{name: 'Search'}">
 <v-btn icon>
  <v-icon>mdi-magnify</v-icon>
 </v-btn>
</router-link>
<style scoped>
a{
  text-decoration: none;
}
.link-active .v-btn--icon .v-icon{
  color: rgb(202, 96, 114);
}
</style>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLIでbuildしたサイトを公開する方法

この記事で行うこと

この記事ではVue CLIで作成したプロジェクト(Webサイト)をデプロイして公開するまでの手順を解説しています。

背景

今回初めてvue-cliを使ったWebサイトを作成して「いざ公開」という時につまづいてしまった為、対処方法をまとめました。

参考サイト

下記のサイトを参考にさせていただきました。
・Vue-CLI3でbuildすると画面が真っ白になる

環境

Mac
-vue/cli v4.4.6
-npm v6.14.6
-node v12.16.3

つまづいた原因

作成したデータをbuildすることはできましたが、buildされたdistフォルダをサーバーにアップロードされても何も表示されませんでした。

原因を調べてみるとbuildされたindex.htmlが参照するcssやjsのパスが違うことによるものということが判明
Vue-CLI3でbuildすると画面が真っ白になるを参照)

build準備からアップロード(デプロイ)まで

buildする準備

プロジェクトのディレクトリ直下にvue.config.jsを作成して下記を記述する。

vue.config.js
module.exports = {
    publicPath: './'
}

以前はpublicPathではなくbaseUrlで実行することができたようですが、現在baseUrlでbuildを行うと
ERROR Invalid options in vue.config.js: "baseUrl" is not allowed
と表示され正しくbuildすることができません。

build

ターミナルを開いて下記のコマンドを実行していきます。

buildしたいディレクトリに移動
cd ディレクトリ名前

buildする
npm run build

実行後に
DONE Build complete. The dist directory is ready to be deployed.
と表示されればbuildは完了です。

サーバーにアップロードする

あとはサーバーの任意のディレクトリに/distフォルダ内にあるファイルやフォルダをアップロードすれば完了です!

最後に

以上がVue CLIで作成したプロジェクト(Webサイト)をデプロイして公開するまでの手順です。
この記事が参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue CLIでbuildしたWebサイトを公開する方法

この記事で行うこと

この記事ではVue CLIで作成したプロジェクト(Webサイト)をデプロイして公開するまでの手順を解説しています。

背景

今回初めてvue-cliを使ったWebサイトを作成して「いざ公開」という時につまづいてしまった為、対処方法をまとめました。

参考サイト

下記のサイトを参考にさせていただきました。
・Vue-CLI3でbuildすると画面が真っ白になる

環境

Mac
-vue/cli v4.4.6
-npm v6.14.6
-node v12.16.3

つまづいた原因

作成したデータをbuildすることはできましたが、buildされたdistフォルダをサーバーにアップロードされても何も表示されませんでした。

原因を調べてみるとbuildされたindex.htmlが参照するcssやjsのパスが違うことによるものということが判明
Vue-CLI3でbuildすると画面が真っ白になるを参照)

build準備からアップロード(デプロイ)まで

buildする準備

プロジェクトのディレクトリ直下にvue.config.jsを作成して下記を記述する。

vue.config.js
module.exports = {
    publicPath: './'
}

以前はpublicPathではなくbaseUrlで実行することができたようですが、現在baseUrlでbuildを行うと
ERROR Invalid options in vue.config.js: "baseUrl" is not allowed
と表示され正しくbuildすることができません。

build

ターミナルを開いて下記のコマンドを実行していきます。

buildしたいディレクトリに移動
cd ディレクトリ名前

buildする
npm run build

実行後に
DONE Build complete. The dist directory is ready to be deployed.
と表示されればbuildは完了です。

サーバーにアップロードする

あとはサーバーの任意のディレクトリに/distフォルダ内にあるファイルやフォルダをアップロードすれば完了です!

最後に

以上がVue CLIで作成したプロジェクト(Webサイト)をデプロイして公開するまでの手順です。
この記事が参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Web API を介したバリデーションエラーを VeeValidate にマニュアル追加する

Vue のバリデーションライブラリの一つである VeeValidate を使うと動的なバリデーションチェックが行われるフォームを低コストで実装することができます。通常はクライアント側で持っているデータやロジックに基づいたバリデーションを行いますが、 Web API を介してサーバー側のデータを用いた動的なバリデーションチェックを実装することもできるので、その方法をメモします。

なお API client の実装自体は本題から逸れるので割愛します。

TL;DR

環境

  • Vue: 2.x
  • VeeValidate: 3.2.5
  • Axios: 0.19.2
  • TypeScript: 3.7.4

実装

シチュエーションとしてはホテルや旅客機などの予約日を入力するフォームを想定しています。
以下のコードでは日付のフォーマットなどをフロントエンドのみでバリデーションチェックし、指定した日付が予約可能かのチェックでサーバへの問い合わせを行います。

components/price.vue
<template>
  <ValidationProvider
    v-slot="{ errors }"
    ref="provider"
    name="予約日"
    rules="required|'date_format:dd/MM/yyyy'"
  >
    <input
      v-model="value"
      type="text"
      @blur="checkRegistrable"
    />
    <span>{{ errors[0] }}</span>
  </ValidationProvider>
</template>

<script lang="ts">
import { createComponent, ref, watch } from '@vue/composition-api'
import { ValidationResult } from 'vee-validate/dist/types/types'
import { AxiosInstance } from 'axios'

import { useReservationDate } from '~/compositions/reservation-date'

export default createComponent({
  setup() {
    const { checkIfRegistrable } = useReservationDate(axios)
    const value = ref<string | null>(null)
    const checkRegistrable = async () => {
      const result: ValidationResult = await provider.value.validate()
      if (!result.valid) {
        return
      }

      const canRegister: boolean = await checkIfRegistrable(val.value)
      if (!canRegister) {
        provider.value.applyResult({
          errors: ['この日付は指定できません。'],
          valid: false,
          failedRules: {}
        })
      }
    }
    return {
      value,
      provider
    }
  }
})
</script>

<input> タグで blur イベントが発生した際に checkRegistrable というメソッドが実行されます。

メソッド内部ではまず通常のバリデーションチェックを行い、結果が valid だった場合は予約日に関するロジックを集約した composition の useReservationDate から checkIfRegistrable を呼び出します。ここで checkIfRegistrable は API の呼び出し結果を boolean で返すようなメソッドとなる想定です。

この結果が false の場合は ValidateProvider の applyResult というメソッドを利用してバリデーションの状態を invalid に変更します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

非同期的なバリデーションエラーを VeeValidate にマニュアルで追加する

Vue のバリデーションライブラリの一つである VeeValidate を使うと動的なバリデーションチェックが行われるフォームを低コストで実装することができます。通常はクライアント側で持っているデータやロジックに基づいたバリデーションを行いますが、 Web API など非同期的な処理を挟んだ動的なバリデーションチェックを実装する機会があったので、その方法をメモします。

なお API client の実装自体は本題から逸れるので割愛します。

TL;DR

環境

実装

シチュエーションとしてはホテルや旅客機などの予約日を入力するフォームを想定しています。
以下のコードでは日付のフォーマットなどをフロントエンドのみでバリデーションチェックし、指定した日付が予約可能かのチェックでサーバへの問い合わせを行います。

components/price.vue
<template>
  <ValidationProvider
    v-slot="{ errors }"
    ref="provider"
    name="予約日"
    rules="required|'date_format:dd/MM/yyyy'"
  >
    <input
      v-model="value"
      type="text"
      @blur="checkRegistrable"
    />
    <span>{{ errors[0] }}</span>
  </ValidationProvider>
</template>

<script lang="ts">
import { createComponent, ref, watch } from '@vue/composition-api'
import { ValidationResult } from 'vee-validate/dist/types/types'

import { useReservationDate } from '~/compositions/reservation-date'

export default createComponent({
  setup() {
    const { checkIfRegistrable } = useReservationDate()
    const value = ref<string | null>(null)
    const checkRegistrable = async () => {
      const result: ValidationResult = await provider.value.validate()
      if (!result.valid) {
        return
      }

      const canRegister: boolean = await checkIfRegistrable(val.value)
      if (!canRegister) {
        provider.value.applyResult({
          errors: ['この日付は指定できません。'],
          valid: false,
          failedRules: {}
        })
      }
    }
    return {
      value,
      provider
    }
  }
})
</script>

<input> タグで blur イベントが発生した際に checkRegistrable というメソッドが実行されます。

メソッド内部ではまず通常のバリデーションチェックを行い、結果が valid だった場合は予約日に関するロジックを集約した composition の useReservationDate から checkIfRegistrable を呼び出します。ここで checkIfRegistrable は API の呼び出し結果を boolean で返すようなメソッドとなる想定です。

この結果が false の場合は ValidateProvider の applyResult というメソッドを利用してバリデーションの状態を invalid に変更します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsとIdentity Platform(IDプラットフォーム)でAPIの認証機能を作る

はじめに

今、ちょっとしたWebアプリを作ってるのですが、認証システムを自作するのはセキュリティの意味でも生産性の意味でもナンセンスです。
なので何かしらのCIAM(Customer identity access management)を入れようと思ったのですが、ちょうどGCPを使っていたのでIdentity Platform(IDプラットフォーム)を使ってみることにしました。これはFireabase認証をベースにGoogleがエンタープライズグレードとしてGCPに統合したIDaaSです。

というわけで備忘録としてIdentity PlatformとVue.jsおよびJWT(+Quarkus)を使ったサーバサイドでAPI認証のやり方を解説します。

システム構成

今回作るシステム構成はクライアント(Vue.js)、API(Quarkus)、認証サーバ(Identity Platform)、IdP(Google/Twitterなどのソーシャルログイン)で構築します。イメージ図は以下の通り。

image.png

JWTなのでAPI側は基本的にトークンのみを見れば良く、実際のIdPと通信をする必要はありません。今回は公開鍵をIdentity Platformから取得するのをリアルタイムでやっていますが負荷などの観点で必要であればオフラインで事前に取得しておくこともできます。

そもそもIdentity Platformって?

日本語訳が「IDプラットフォーム」で英語名が「Identity Platform」なのと「Cloud Identity」というまったく別のサービスをGoogleは持っているので非常に分かりづらいですが、これはFirebase認証をベースにGCP側に取り込んで作られたIDaaSです。Firebase Authenticationとの主な違いは下記の通り。

Functional Features

Feature Identity Platform Firebase Authentication
Sign in with email Yes Yes
Sign in with OAuth (ソーシャル認証) Yes Yes
Sign in with phone Yes Yes
Custom authentication Yes Yes
Sign in with OIDC Yes No
Sign in with SAML Yes No
Multi-tenancy Yes No
IAP integration Yes No

Non Functional Features

Feature Identity Platform Firebase Authentication
ISO 27001 Yes Yes
SSAE 18 SOC1 Yes Yes
SSAE 18 SOC2 Yes Yes
SSAE 18 SOC3 Yes Yes
TISAX Yes No
BAA coverage Yes No
PCI-DDS in scope Yes No
Enterprise SLA 99.95% uptime No

Price

下記は独自のパスワード認証やGoogle/Twitter/Facebook/GitHubなどを使ったソーシャル認証の価格テーブルです。SAMLやOpenID Connectを使う場合は価格テーブルが異なります。1 人の MAU を 1 か月で複数回認証しても追加料金はかからないようです。

月間アクティブ ユーザー(MAU) Identity Platform Firebase Authentication
0~49,999 $0 $0
50,000~99,999 $0.0055 $0
100,000~999,999 $0.0046 $0
1,000,000~9,999,999 $0.0032 $0
10,000,000 以上 $0.0025 $0

Identity Platform まとめ

Identity Platformの主な違いはOIDC/SAMLへの対応、コンプライアンスとSLA準拠、有料か無償かの違いです。
個人的には月間5万ユーザを超えるまでは無料ですし、それ以降も十分安いのでエンタープライズグレードのIdentity PlatformがGCPにも統合されていて便利だと思います。

なお、現時点ではFirebase AuthenticationとIdentity Platformは別のプロダクトですが、将来的にはFireaStoreの時のように同じ製品の別モードという形になるのでは無いかと想像しています。

注意点としては、Identity Platform単体でのドキュメントは非常に少ないです。SDKも含めてFireabaseのものをそのまま利用しているので、基本的にはFirebase側のドキュメントを読むことになります。この辺は成熟度が全然足りて無いところですね。。。

GCPでIdentity Platformのセットアップ

こちらのページを参考にIdentity Platformのセットアップをします。まずはIdentity PlatformのマーケットプレイスからAPIを有効にします。

続いて、Identity Platformのページに遷移してプロバイダの追加を行います。
今回は独自のアカウントは不要なのでGoogleとTwitterを追加しておきます。
image.png
後から追加や削除は出来るので必要最小限で作ればよいと思います。

次に、「アプリケーション設定の詳細」リンクから「アプリケーションの構成」を開き、apiKeyとauthDomainを確認します。これは後程、Vue.jsで設定します。

image.png

以上で、とりあえずのIdentity Platformのセットアップは完了です。

Vue.jsでのIdentity Platformによる認証の実装

セットアップ

まずはVue.jsにIdentity Platformでの認証を組み込みます。Identity Platformは既存のFirebase SDKをそのまま利用します。

yarn add firebase

ライブラリと設定をmain.jsで読み込みます。API Keyは先ほどメモしたものを利用します。誤ってGitHubなどに上げてしまわないように環境変数を取得するようにし.env.localあたりに記載しておくのが無難でしょう。

src/main.js
import firebase from 'firebase';
var config = {
  apiKey: process.env.VUE_APP_AUTH_API_KEY,
  authDomain: process.env.VUE_APP_AUTH_API_DOMAIN,
};
env.local
VUE_APP_AUTH_API_KEY={YOUR_API_KEY}
VUE_APP_AUTH_API_DOMAIN={YOUR_API_DOMAIN}

サインインページの作成

サインインページの作成をします。今回はソーシャルログインを使いかつ追加のユーザ情報を入力しないので、ユーザ登録の画面等は不要です。

src/views/Signin.vue
<template>
  <div class="signin">
    <h1>サインイン</h1>
    <button @click="signIn">Googleで認証</button>
    <p>Token: {{token}}</p>
  </div>
</template>

<script>
import firebase from "firebase";

export default {
  name: "Signin",
  data() {
    return { token: "" };
  },
  methods: {
    signIn: function () {
      const self = this;
      firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider())
        .then((res) => {
          res.user.getIdToken()
            .then((token) => (self.token = token))
            .catch((error) => {
              console.log(error);
              this.errorMessage = error.message;
              this.showError = true;
            });
        });
    },
  },
};
</script>

firebase.auth().signInWithPopup(new firebase.auth.GoogleAuthProvider())でGoogleの認証画面を呼び出して認証情報を取得し、Identity PlatformのIDトークン(アクセストークン)をJWTで取得します。プロバイダの部分を変更する事でそれ以降のロジックは特に変えずに認証が可能になります。

1点注意したいのは例えばサンプルコードには以下のようにも書いてあります。

firebase.auth().signInWithPopup(provider).then(function(result) {
  // This gives you a Google Access Token. You can use it to access the Google API.
  var token = result.credential.accessToken;
}

ここでcredential.accessTokenで取得できるものがFirebase/Identity Platformのアクセストークンだと勘違いしてしまいますが、これはあくまでも 「認証プロバイダのアクセストークンでありIdentity Platformのアクセストークンでは無い」ということです。なので、ここで取得出来ているのは「Googleのアクセストークン」です。Twitterで認証してればTwitterのアクセストークン、GitHubならGitHubのアクセストークンが返ります。そのためこちらのトークンを使ってをAPIの検証等は出来ません。
「Firebase/Identity Platformのアクセストークンの取得はgetIdToken() という事に注意してください。

トークンを利用したAPIの認証

つづいてFireabaseから取得したトークンを使ってサーバサイドでAPIの認証をする仕組みを作ります。今回もQuarkusを使用しています。

MicroProfile JWT RBACの実装

MicroProfile にはMicroProfile JWT RBAC (Json Web Token Role-Based Access Control)の仕様があるので、こちらを使います。Firebase SDKを使う事も可能ですがJWTとして処理する方が汎用性があって良い気がします。

まずはライブラリを追加します。

./mvnw quarkus:add-extension -Dextensions="smallrye-jwt"

JAX-RSのコードは以下のように書きます。

TokenSecuredResource.java
@Path("/secured")
@RequestScoped
public class TokenSecuredResource {
    @Inject
    JsonWebToken jwt;

    @GET
    @PermitAll
    @Produces(MediaType.APPLICATION_JSON)
    public Response get(@Context SecurityContext ctx) throws JsonProcessingException {
        var caller = ctx.getUserPrincipal();
        var result = Map.of(
                "id", caller == null ? "anonymous" : caller.getName(),
                "name", jwt.getClaim("name"),
                "picture", jwt.getClaim("picture"),
                "expiration_time", jwt.getExpirationTime(),
                "has_jwt", jwt.getClaimNames() != null,
                "payload", jwt.getClaimNames().stream()
                        .map(k -> List.of(k, jwt.getClaim(k).toString()))
                        .collect(Collectors.toMap(x -> x.get(0), x -> x.get(1)))
        );

        return Response.ok(new ObjectMapper().writeValueAsString(result))
                .build();
    }
}

@PermitAllなど「JSR 250に登録されたアノテーション」を使用するとこでアクセスコントロールを行えます。
Authorization: Bearer ...からのJWTの取得は自動でされJsonWebTokenに格納されています。ペイロードはJsonWebToken#getClaimで取得できます。

続いて、JWTの複合および検証をするためのPublic Keyの場所等の指定をします。これはFireabase/Identity Platformに対して検証が行われるのでGoogleやTwitterにフェデレーションをしている時でも記述するlocationやissuerは同一です。

application.properties
# JWT with Firebase/Identity Platform
mp.jwt.verify.publickey.location=https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com
mp.jwt.verify.issuer=https://securetoken.google.com/{YOUR_GCP_PEOJCT_ID}
quarkus.smallrye-jwt.auth-mechanism=MP-JWT
quarkus.smallrye-jwt.enabled=true

1点注意が必要なのはmp.jwt.verify.publickey.locationです。「Firebaseのドキュメント」にはサードパーティでの検証に使う公開鍵はhttps://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.comを使えと記載があります。
しかし、x509は「Quarkus」でサポートされるJWT向けの公開鍵の範囲ではありません。なので、サポートに入っているJWKSに対応するURLであるhttps://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.comを使う必要があります。

これでサーバサイド側の準備は完了です。

Vue.jsとサーバサイドの連携

Signin.vueを拡張して認証情報の取得後にAPIにトークンを付与してリクエストを投げるコードを作成します。

src/views/Signin.vue
<template>
  <div class="signin">
    <h1>サインイン</h1>
    <button @click="signInWithGoogle">Googleで認証</button>
    <button @click="signInWithTwitter">Twitterで認証</button>
    <div v-show="user.id">
      <p>ID: {{user.id}}</p>
      <p>
        名前:
        <img :src="user.pic" width="24px" height="24px" />
        {{user.name}}
      </p>
      <p>有効期限: {{user.expiration_time}}</p>
    </div>
  </div>
</template>

<script>
import firebase from "firebase";

export default {
  name: "Signin",
  data() {
    return {
      user: {
        id: "",
        name: "",
        expiration_time: "",
        pic: "",
      },
    };
  },
  methods: {
    signInWithGoogle: function () {
      this.signIn(new firebase.auth.GoogleAuthProvider());
    },
    signInWithTwitter: function () {
      this.signIn(new firebase.auth.TwitterAuthProvider());
    },
    signIn: function (provider) {
      const callApi = (token) => {
        const url = "http://localhost:8080/secured";
        const config = {
          headers: {
            Authorization: "Bearer " + token,
          },
        };
        this.axios.get(url, config).then((response) => {
          this.user.id = response.data.id;
          this.user.name = response.data.name;
          this.user.expiration_time = new Date(
            response.data.expiration_time * 1000
          );
          this.user.pic = response.data.picture;
        });
      };

      firebase
        .auth().signInWithPopup(provider).then((res) => {
          res.user
            .getIdToken().then(callApi).catch((error) => {
              console.log(error);
              this.errorMessage = error.message;
              this.showError = true;
            });
        });
    },
  },
};
</script>

認証後にaxiosを使ってサーバサイドにリクエストを投げています。その際にトークンをAuthorizationヘッダーにBearerキーワードと共に投げています。
image.png
これによって認証したトークンをサーバサイドの認証に使えることが分かりました。

まとめ

今回はGoogleのCIAMであるIdentity Platform(IDプラットフォーム)とVue.jsを組合わせた認証の仕組みを実装しました。
SPAの場合は古き良きWebアプリケーションと違いクライアント側の認証とAPI側の認証が両方必要です。自前で作る事はもちろん可能ですが、CIAMを使う事で独自のアカウントでもソーシャルログインでも簡単に作る事が可能です。

まあ、独自ロジックをベースにする場合はFIDO認証に対応したCIAMを使う方が今後は良いでしょうけど。。。

今回は省きましたが実際にアプリとして作るときにはクライアント側の認証画面の制御とかも必要になるのでVue.jsでの実装方法はこちらの記事をご覧ください。

Identity Platformでアクセストークンの寿命の設定やリフレッシュトークンを無効にする方法はまだ調べきってないので、その辺も今後調査しておきたいと思います。

それではHappy Hacking!

参考:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【超簡単】iframeを使ってYouTubeを埋め込んだ場合に簡単にレイアウトを整える方法

こんにちは、Yuiです。

今回はiframeを使ってYouTube動画を埋め込んだ際にものすごく簡単にレイアウトを整える方法をお伝えします。(私はVueで書いてますが、要はCSSを工夫するだけなので、HTML/CSSを使ってwebページを作成しているときにも使えます。)

先日Vue.jsでVuetifyを使ってページを作成してたら変に上部のスペースが空いて困った件という記事を書いたのですが、その最後の完成形として上げていたこちらの写真、確かにpaddingは消えていますが、「え、Youtube画像細長すぎない???バランス悪すぎない???」ということで、今回はレイアウトを整えることにしました。
スクリーンショット 2020-08-01 3.29.28.png

まずはYouTubeをiframeで埋め込みます

まずは埋め込みたいYouTube動画のページにいきます。

スクリーンショット 2020-08-03 7.59.00.png
共有をクリック

スクリーンショット 2020-08-03 7.59.19.png
埋め込むを押します

スクリーンショット 2020-08-03 8.00.03.png
そうすると上記のような埋め込みコードが出てくるので、それをそのままコピー&ペースト。

簡単にこのiframeのコードを説明すると、width="560"、height="315"という部分がその名の通り幅と高さを表しています。
ただ、注意が必要なのがこれはpxではなく、縦横の比率を表しているので基本的に変更してはいけません。

今回私が失敗した原因としてはこのiframeをブロックで囲んで、それをそれぞれグリッドシステムを利用してレイアウトを整えてから、その中いっぱいにこの動画が広がれば良いなと思って、width="100%"、height="100%"としたら、上記の写真のように横に細長いブサイクなレイアウトが出来上がってしまいました・・・。

iframeを一つのボックスで囲んでクラス名をつけてrelativeからの中身をabsoluteに

じゃあどうやるのかというと、iframeをまずはボックスで囲みます。私はVueで書いていたので、v-containerで囲みました。そしてそれを親クラスとしてrelativeを指定してからその中(つまりiframe部分)をabsoluteで移動させます。

実際のコードを見てみましょう。

YouTube1.vue
<template>
    <v-container class="responsive-style">
        <iframe
        width="560"
        height="315"
        src="https://www.youtube.com/embed/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        frameborder="0"
        allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
        ></iframe>
    </v-container>
</template>

<style lang="scss" scoped>
.responsive-style{
    position:relative;
    width:100%;
    height:0;
    padding-top:50%;
}
.responsive-style iframe{
    position:absolute;
    top:0;
    left:0;
    width:100%;
    height:100%;
}
</style>

まず親要素でボックスの幅を最大にします。
注意が必要なのが、ここでは高さは0にするということです。
そのかわり、paddingを入れてあげます。
このpaddingに高さにしたい数値を入れます。
ここでポイントはpxで指定するのではなく、%で指定するということです。
そうすることで、PCからでもスマホからでも同じ割合で表示することができます。(後述しますが、PCでは2カラム、スマホでは1カラムとしたい場合は、グリッドシステムを利用して表示します。)

そして、その親要素のpaddingが中身(iframe)の入る部分になるので、余分な隙間をゼロにして(top:0;、left:0;の部分)縦横いっぱいに広げます。

これで簡単にレイアウトが整うのでぜひお試しください!

おまけ

上記の方法だと、デバイスごとでカラム数を分けることはできません。
というわけで、PCでは2カラム、スマホでは1カラムとしたい場合は、更に上記の親要素を大きな枠でくくって、グリッドシステムで表現します。

※今回、諸事情で左と右のYouTube部分をそれぞれ別のコンポーネントとしています。下記の感じ。

Home.vue
<template>
  <v-app>
    <v-container class="container-style">
      <v-row class="youtube-class">
        <v-col cols=12 sm=12 md=6>
          <YouTube1/>
        </v-col>
        <v-col cols=12 sm=12 md=6>
          <YouTube2/>
        </v-col>
      </v-row>
      <!--省略-->
    </v-container>
  </v-app>
</template>

こうすることで、無事PC画面では2カラムで、スマホでは1カラムでYouTubeを埋め込むことができました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring+Vue.js でAPIの連携をする

はじめに

今回SpringフレームワークでRestAPIを作成し、
Vue.jsでデータを表示させることをゴールに記載していきます。
(Javaのインストール等の初期設定となる環境構築は省きます。)

プロジェクト作成

◇Vue.js

VueCliを用いて作成していきます。

①VueCliをnpmよりインストールする

コンソール
npm install -g @vue-cli

②プロジェクト作成

コンソール
vue create practice

実行すると、アプリケーション作成に必要な設定ファイルやサンプルソースが自動で作成されます。
image.png
③サンプル画面確認
作成したプロジェクト配下にて以下実行

コンソール
npm run serve

image.png

◇Spring

「Spring Tools for Eclipse」を使用して、進めていきます。

①プロジェクト作成
「Spring Starter Project」を選択肢プロジェクトを作成する

image.png

プロジェクトを作成すると以下のようなフォルダ、ファイルが作成されます。
(今回は「gradle」を使用しております。)
image.png

RestAPIの作成

H2データベースからデータを取得するAPIを作成します。
①H2データベース準備
H2データベースとは・・・・・
 JAVAプラットフォーム上でオープンソースのRDB
「インメモリデータベース」として使用が可能でSpring bootではデフォルトで付属されているため複雑な設定不要
以下のJDBCドライバがすでに登録されている
image.png

今回は「インメモリデータベース」として使用するため、初期化するテーブル、データを作成していきます。
「src/main/resources」配下に「data.sql」、「schema.sql」を配置する。
アプリ起動時、インメモリデータベースのため毎回初期化されます。
初期化時には2つのSQLが自動で実行されます。
image.png

data.sql
data.sql
INSERT INTO person(code, name, belong_nm)
VALUES('001', 'テスト1', '総務部');
INSERT INTO person(code, name, belong_nm)
VALUES('002', 'テスト2', '人事部');


schema.sql
schema.sql
CREATE TABLE person
(
   id INT NOT NULL AUTO_INCREMENT,
   code VARCHAR(100) NOT NULL,
   name VARCHAR(100) NOT NULL,
   belong_nm VARCHAR(500) NOT NULL,
   PRIMARY KEY(id)
);

プロジェクト上で右クリックし、「Spring Boot App」にて起動する
image.png
起動後「http://localhost:8080/h2-console」
にアクセスし、テーブルが作成されていることを確認

Connectを選択
image.png
テーブルもデータも作成されている。
image.png

②Controller、Serviceを作成する
ControllerからServiceを呼び出します。
一旦DBの参照は行わず、固定の値を戻すようにする。

固定値をListに詰めて返却します。

PracticeServiceImp.java
PracticeServiceImp.java
package com.example.demo.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

@Service
public class PracticeServiceImp implements PracticeService {

    @Override
    public List<String> getAll() {
        List<String> list = new ArrayList<>();

        list.add("1");
        list.add("2");
        list.add("3");

        return list;
    }
}

Service呼び、取得した値をListに詰めて、返却します。

PracticeController.java
PracticeController.java
package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.service.PracticeService;
import com.example.demo.service.PracticeServiceImp;

@RestController
@RequestMapping("api/practice")
public class PracticeController {

    private final PracticeService practiceService;

    @Autowired
    public PracticeController(PracticeServiceImp practiceService){
        this.practiceService = practiceService;
    }

    @GetMapping
    public List<String> getAll() {
        List<String> list = practiceService.getAll();

        return list;
    }
}


実際に実行し確認していきます。
image.png

http://localhost:8080/api/practice」
にアクセスし、固定値が表示されました。

③DBから取得した値をFormクラスに格納し、returnするよう修正
DBとの接続、SQLの発行はDAOクラスに任せます。
「Controller(リクエスト、レスポンスのハンドリング)」「Service(ロジック)」「Dao(DB操作)」という役割です。

PracticeServiceImp.java
PracticeServiceImp.java
package com.example.demo.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.dao.PracticeDao;
import com.example.demo.form.PracticeForm;

@Service
public class PracticeServiceImp implements PracticeService {

    private final PracticeDao dao;

    @Autowired
    public PracticeServiceImp(PracticeDao dao) {
        this.dao = dao;
    }
    @Override
    public List<PracticeForm> getAll() {
//      List<PracticeForm> list = new ArrayList<>();
//       
//        list.add("1");
//        list.add("2");
//        list.add("3");
        return dao.getAll();
    }
}

PracticeController.java
PracticeController.java
package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.form.PracticeForm;
import com.example.demo.service.PracticeService;
import com.example.demo.service.PracticeServiceImp;

@RestController
@RequestMapping("api/practice")
@CrossOrigin(origins = {"http://localhost:8081"})
public class PracticeController {

    private final PracticeService practiceService;

    @Autowired
    public PracticeController(PracticeServiceImp practiceService){
        this.practiceService = practiceService;
    }

    @GetMapping
    public List<PracticeForm> getAll() {
        List<PracticeForm> list = practiceService.getAll();

        return list;
    }
}

データを格納するための新規ファイル

PracticeForm.java
PracticeForm.java
package com.example.demo.form;

import javax.validation.constraints.NotNull;

public class PracticeForm {
    public PracticeForm() {};

    public PracticeForm(int id, String code, String name, String belong_nm) {
        super();
        this.id = id;
        this.code = code;
        this.name = name;
        this.belong_nm = belong_nm;
    }

    @NotNull
    private int id;

    @NotNull
    private String code;

    @NotNull
    private String name;

    @NotNull
    private String belong_nm;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBelong_nm() {
        return belong_nm;
    }

    public void setBelong_nm(String belong_nm) {
        this.belong_nm = belong_nm;
    }

}

DB操作するための新規ファイル

PracticeDaoImp.java
PracticeDaoImp.java
package com.example.demo.dao;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.example.demo.form.PracticeForm;

@Repository
public class PracticeDaoImp implements PracticeDao {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public PracticeDaoImp(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public List<PracticeForm> getAll() {
        // TODO Auto-generated method stub
        String sql = "select id, code, name, belong_nm from person";
        List<Map<String, Object>> resultList =  jdbcTemplate.queryForList(sql);
        List<PracticeForm> list = new ArrayList<PracticeForm>();
        for(Map<String, Object> result : resultList) {
            PracticeForm practiceForm = new PracticeForm();
            practiceForm.setId((int)result.get("id"));
            practiceForm.setCode((String)result.get("code"));
            practiceForm.setName((String)result.get("name"));
            practiceForm.setBelong_nm((String)result.get("belong_nm"));
            list.add(practiceForm);
        }
        return list;
    }

}

上記修正後、サイドアクセスしてみます。
image.png

DBの値が取得できております。

Vue.jsからAPIを実行

フロントエンド側からAPIを呼び出しデータを表示させます。
①axiosをインストール
APIを実行するために「axios」を使用します。
axios:HTTP通信が可能なJavaScriptのライブラリ

コンソール
npm install --save axios

②axiosでAPIを呼び出す。
※VueもSpringもサーバーを起動させておく。
axiosにて、APIのURLを指定しデータを取得する。

Home.vue
Home.vue
// Home.vue
<template>
  <div>
    {{ people }}
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data () {
    return {
      people: []
    }
  },
  methods: {
    getPerson () {
      const path = 'http://localhost:8080/api/practice'
      axios.get(path)
        .then(response => {
          this.people = response.data
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  created () {
    this.getPerson()
  }
}
</script>

画面を確認する。
image.png

データが取得されました。

③テーブルに表示させる
UIフレームワーク「vuetify」のデータテーブルを用いて、画面をそれっぽく加工してみます

Person.vue
Person.vue
// Person.vue
<template>
  <div>
    <h1>社員一覧</h1>
    <v-data-table
          :headers="headers"
          :items="people">
    </v-data-table>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  components:{

  },
  data () {
    return {
      people: [],
      singleSelect: false,
        selected: [],
        headers: [
          {
            align: 'start',
            sortable: false,
          },
          { text: 'ID', value: 'id' },
          { text: '氏名', value: 'name' },
          { text: '社員コード', value: 'code' },
          { text: '所属名', value: 'belong_nm' },
        ],
    }
  },
  methods: {
    getNews () {
      const path = 'http://localhost:8080/api/practice'
      axios.get(path)
        .then(response => {
          this.people = response.data
        })
        .catch(error => {
          console.log(error)
        })
    }
  },
  created () {
    this.getNews()
  }
}
</script>

データテーブルを使って表示させることで、簡単にそれっぽいものが表示されました。
image.png

まとめ

簡単なAPIを作成しそれをフロントエンドと連携させることができました。
いまどきのフレームワークのおかげでどこにどういうロジックを書くかがだいたい決まっております。
そのため、役割もはっきりし便利ですね。

参考文献

参考にさせていただきました。
https://b1tblog.com/2020/03/17/spring-rest-2/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む