読者です 読者をやめる 読者になる 読者になる

Tech memo

日々学んだ技術のびぼうろく

Lumen 5.3 と Vue.js 2.1 で簡易掲示板を作る 〜フロントエンド編〜

完成目標

このような簡易掲示板を作る。 f:id:yjhm214:20170105214532p:plain

バックエンド編はこちら。 www.yjhm214.com

スポンサーリンク

前提

Lumen はインストール済み

www.yjhm214.com

  • Lumen 5.3.3
  • Vue.js 2.1.4

ルーティングの作成

routes/api.phpに以下のルーティングを追加する。

$app->get('/', function() use ($app) {
    return view('index');
});

View の作成

resources/views/index.blade.phpを作成する。

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta http-equiv="Cache-Control" content="no-cache">
  <meta http-equiv="Pragma" content="no-cache">
  <meta http-equiv="cache-control" content="no-cache">
  <meta http-equiv="expires" content="0">
  <meta name="description" content="">
  <meta property="og:type" content="website">
  <meta property="fb:app_id" content="">
  <meta property="og:title" content="">
  <meta property="og:description" content="">
  <meta property="og:image" content="">

  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <style>
    body { padding-top: 20px; padding-bottom: 60px; }
    form { padding: 20px 0 0 0; }
    .title { 
      text-align: center;
      background-color: #5E83A2;
      color: #FFFFFF;
      padding: 20px;
      margin: 0px 0 40px 0;
      border-radius: 5px;
    }
    .title p { color: #D2E2EF; font-size: 23px; }
    .comment { margin: 0 0 20px 0; word-wrap: break-word; }
  </style>

  <title>BBS</title>
</head>

<body>
  <div class="container" id="bbs">
    <div class="title">
      <h1>Sample BBS SPA</h1>
      <p>using Lumen 5.3 and Vue.js 2.1</p>
    </div>

    <div class="comment" v-for="comment in comments">
      <h2>Comment #(% comment.id %) <small>by (% comment.name %)</small></h2>
      <p>(% comment.comment %)</p>
    </div>

    <form v-on:submit="postComments">
      <div class="form-group">
        <label for="form-name">Name</label>
        <input v-model="name" type="text" class="form-control" id="from-name" aria-describedby="name-help" placeholder="Enter your name" required>
        <small id="name-help" class="form-text text-muted">We'll never share your name with anyone else.</small>
      </div>
      <div class="form-group">
        <label for="form-comment">Comment</label>
        <textarea v-model="comment" class="form-control" id="form-comment" placeholder="Enter comment" required></textarea>
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>


  <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  <script src="//cdn.jsdelivr.net/vue/2.1.8/vue.min.js"></script>
  {{-- <script src="/js/vue.js"></script> --}}
  <script>
    new Vue ({
        delimiters: ['(%', '%)'],

        el: '#bbs',

        data: {
          comments: [],
          comment: '',
          name: '',
        },

        created: function() {
          console.log('created');
          this.getComments();
        },

        methods: {
          getComments: function() {
            var $vue = this;
            $.get('/api/comment')
              .done(function(comments){
                $vue.comments = comments;
                console.log('GET:', $vue.comments);
              })
              .fail(function(){
                console.log('error');
              })
          },
          postComments: function(e) {
            e.preventDefault();
            var $vue = this;
            $.post('/api/comment', {name: $vue.name, comment: $vue.comment})
              .done(function(res) {
                $vue.comments.push(res);
                $vue.name = '';
                $vue.comment = '';
                console.log('POST:', res);
              })
              .fail(function(){
                console.log('error');
              })
          }
        }
      })
  </script>

</body>

</html>

下のように表示されれば完成!

f:id:yjhm214:20170105214532p:plain

View の解説 〜 html編 〜

Vue.js のデリミタを変更する

html 内に Vue.js の変数を埋め込むには通常、

<div id="app">
  {{ message }}
</div>

のように {{ }} で変数を囲む。

これをデリミタという。

今回 Lumen で使っているサーバーサイドのテンプレートエンジン bladeのデリミタも {{ }} なので、Vue.js とコンフリクトをおこしてしまう。

そのため、ここではデリミタを (% %) に変更している。

その変更部分は、

new Vue ({
    delimiters: ['(%', '%)'],

の部分。

以降、ここでは変数を

<div id="app">
  (% message %)
</div>

のように記述する。

次にbodyタグから順にコードを見ていく。

宣言的レンダリング <div class="container" id="bbs">

<body> タグ直下の <div class="container" id="bbs"> には id="bbs"を設定している。

Vue.js では Vue.js の機能が有効になる範囲を指定する必要がある。

ここで設定した id は、スクリプト内の elプロパティで設定している。

new Vue ({
    delimiters: ['(%', '%)'],

    el: '#bbs',

これにより、<div class="container" id="bbs"></div> 内で Vue.js が使えるようになる。

ループ 『v-forディレクティブ』

<div class="comment" v-for="comment in comments">
    <h2>Comment #(% comment.id %) <small>by (% comment.name %)</small></h2>
    <p>(% comment.comment %)</p>
</div>

Vue.js ではループ処理には v-forディレクティブを使う。

※ v1.0より前までは v-repeatディレクティブを使っていたようだが、v1.0で廃止され v-forディレクティブを使うように変更された。 ネット上の記事ではまだ v-repeatディレクティブを使っているものも多いので注意が必要!

イベントリスナ 『v-onディレクティブ』

フォームには <form v-on:submit="postComments"> として、v-onディレクティブ によってイベントリスナを設定している。

v-onディレクティブ は、ここでは単純に submit されたら postComments() を呼び出すよ、という動作をしている。

双方向バインディング 『v-modelディレクティブ』

Vue.js では、入力値とアプリケーションの状態の双方向バインディングを行うための v-modelディレクティブ がある。

コードでは

<input v-model="name" type="text" class="form-control" id="from-name" aria-describedby="name-help" placeholder="Enter your name" required>

v-model="name" の部分。

例えば下記のように記述すると、<input v-model="message"> に入力した値がリアルタイムで <p>{{ message }}</p> に反映される。

<div id="app">
  <p>{{ message }}</p>
  <input v-model="message">
</div>
new Vue({
  el: '#app',
  data: {
    message: 'Hello!'
  }
})

View の解説 〜 Vue.js 編 〜

ここからは Vue.js 部分、

<script>
    new Vue ({
    })
</script>

内の解説を進める。

dataプロパティ

data: {
  comments: [],
  comment: '',
  name: '',
},

dataプロパティは、DOM との同期性を保つためのプロパティで、

それぞれのキーは v-modelディレクティブにバインディングされる。

ライフサイクルフック created

created: function() {
  console.log('created');
  this.getComments();
},

Vue.js では、インスタンス生成時に一連の初期化を行う。

その過程でいろいろなカスタムロジックを実行できるのだが、

それを可能にするフックをライフサイクルフックと呼ぶ。

createdフックもその一つで、インスタンスが生成された後に呼び出される。

ちなみに、ライフサイクルフック内のthisは、Vueインスタンス自身を指す。

その他のライフサイクルフックや呼び出されるタイミングは、以下の公式から拝借してきた図がわかりやすい。

f:id:yjhm214:20170115124404p:plain

methodプロパティ

methods: {
  getComments: function() {
    var $vue = this;
    $.get('/api/comment')
      .done(function(comments){
        $vue.comments = comments;
        console.log('GET:', $vue.comments);
      })
      .fail(function(){
        console.log('error');
      })
  },
  postComments: function(e) {
    e.preventDefault();
    var $vue = this;
    $.post('/api/comment', {name: $vue.name, comment: $vue.comment})
      .done(function(res) {
        $vue.comments.push(res);
        $vue.name = '';
        $vue.comment = '';
        console.log('POST:', res);
      })
      .fail(function(){
        console.log('error');
      })
  }
}

methodプロパティでは、このコンポーネント内(id=“bbs"内)で使うメソッドを定義する。

  • getComments : 全てのコメントを取得する
  • postComments : コメントを保存する

各メソッド内で使っている ajaxメソッドは jquery のものなので解説は省略する。

まとめ

ものすごくシンプルな掲示板を Lumen5.3 と Vue.js2.1 で作ってみた。

バックエンドの Lumen はシンプルで扱いやすく、資料も豊富だったので特に困らず実装することができたが、

フロントエンドの Vue.js はまだ開発が活発であるということもあり、

ネット上の資料が古いバージョンのものだったりして結構ハマりどころが多かった。

ただ、実装がシンプルなため新しく学ぶことはあまりなく、初期学習コストは低いなと感じた。

また、公式が充実しているため公式を一通り読めば十分理解できる点もいい。

React とか Angular とかも触ったけど、その中では今のところ Vue.js が使いやすさの点では群を抜いている気がする。

おすすめの本

Laravel リファレンス[Ver.5.1 LTS 対応] Web職人好みの新世代PHPフレームワーク

Laravel リファレンス[Ver.5.1 LTS 対応] Web職人好みの新世代PHPフレームワーク

  • 作者: 新原雅司,竹澤有貴,川瀬裕久,大村創太郎,松尾大,丸山弘詩
  • 出版社/メーカー: インプレス
  • 発売日: 2015/12/04
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで

改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで

関連記事

www.yjhm214.com

www.yjhm214.com