Vue.js

Page content

Vue.js 勉強してるのでメモっていく。個人用なので伝わらなくてもごめんなさい。

Vue.js

選んだ理由 (2019/08)

  • プロトタイプなので気軽でもいいかも…と言いつつ本番でも適用できればラッキー
  • 学習コストの低さ
  • すぐ試せる
  • 必要量のドキュメントを持つこと
  • 静的 site だと嬉しい気がするけどホントかわからない。選定には関係有無不明
  • すぐオワコンにならなさそう
  • 目的に合うこと(xhr call しやすい, 派手さは不要, ブラウザを選ばない)
  • モバイル対応は不要
  • 周りでも使っていそうな雰囲気だった

Vue.js 全般乱雑メモ

まずはとにかくメモ。後で整理。

Vue インスタンス

これのことね

new Vue({
    hogehoge
})

ディレクティブという用語

v-for とか v-if とか v-else とか v-bind とか、 v- で始まる、 html タグの中で使われる属性のこと。

ディレクティブの省略記法

省略記法って、何でもそうだけど探しづらいよね。なのでメモ

  • v-bind 省略記法
    <!-- 完全な構文 -->
    <a v-bind:href="url"> ... </a>
    
    <!-- 省略記法 -->
    <a :href="url"> ... </a>
    
    <!-- 動的引数の省略記法 (2.6.0 以降) -->
    <a :[key]="url"> ... </a>
    
  • v-on 省略記法
    <!-- 完全な構文 -->
    <a v-on:click="doSomething"> ... </a>
    
    <!-- 省略記法 -->
    <a @click="doSomething"> ... </a>
    
    <!-- 動的引数の省略記法 (2.6.0 以降) -->
    <a @[event]="doSomething"> ... </a>
    

特別な属性

key は特別な属性です。 v-for と一緒に使うのが一般的らしいがまだよくわからない。

プラグインの使用

  • Vue.use() を使う。 new Vue() より先に定義する必要がある
  • プラグインの例: テーブル表示するための VueTables
    Vue.use(VueTables.ClientTables);
    
    new Vue({
        // ...
    })
    

コンポーネントとプロパティ

コンポーネントは名前付きの再利用可能な Vue インスタンスです。プロパティは、コンポーネントに登録できるカスタム属性です。親コンポーネントから子コンポーネントに値を受け渡せるなどの特徴があります。

  • まず Vue インスタンスで groceryList を作っていて、それを id に app5 を持つ html に渡します。

    var app5 = new Vue(
        {
            el: '#app5',
            data: {
                groceryList: [
                    {id: 0, text: 'Vegetables'},
                    {id: 1, text: 'Cheese'},
                    {id: 2, text: 'Whatever else humans are supposed to eat'}
                ]
            }
        }
    )
    
  • html では、与えられた groceryList を for 文で 1 つずつの item にちぎり、繰り返し todo 属性に与えています。

      <div class="container" id="app5">
        <ol>
          <todo-item
            v-for="item in groceryList"
            v-bind:todo="item"
            v-bind:key="item.id">
          </todo-item>
        </ol>
      </div>
    
  • そこにこの Vue コンポーネントでは, プロパティとして todo 属性が指定されているので、 todo オブジェクトが 1 つ 1 つの gloceryList のアイテムになります。それを template で活用しています。

    Vue.component('todo-item', {
        props: ['todo'],
        template: '<li>{{ todo.text }}</li>'
    })
    

親 component から子 component への値の受け渡し

親子関係でない component 間での値の受け渡し

イベントバスという概念を使います。 ここでは drawer (サイドバー的なもの) の表示・非表示を切り替えるために AppBar コンポーネントから Drawer コンポーネントに値を渡す方法を書く。本当は値を渡す必要はない (event-bus がキックされれば良い) が、他のケースでも使えるようにあえて値を渡すようにする。

(参考: 【Vue.js】 コンポーネント間の通信について解説

  • イベントバスの定義: main.js などで以下を定義

    Vue.prototype.drawerBus = new Vue()
    // これで this.$drawerBus として各コンポーネントから呼び出せる
    
  • 送信側のコンポーネント: AppBar.vue

    <template>
        <v-app-bar app>
            <!-- この 1行 と script 1行 だけで drawer の show/hide をトグルできる -->
            <v-app-bar-nav-icon @click="toggleDrawer" />
            ...
        </v-app-bar>
    </template>
    
    <script>
        export default {
            data: () => ({
                dummyValue1: 'I am dummy not in use',
            }),
            methods: {
                toggleDrawer () {
                    this.drawerBus.$emit('bus-event', this.dummyValue1) // イベントバスに dummyValue1 を送って発火
                },
            },
        }
    </script>
    
  • 受信側のコンポーネント: Drawer.vue

    <template>
        <!-- サイドバー作るには v-navigation-drawer -->
        <v-navigation-drawer
            v-model="drawer"
            app
            color="blue-grey lighten-5"
        >
        ...
        </v-navigation-drawer>
    </template>
    
    <script>
        export default {
            data: () => ({
                drawer: null,
            }),
            mounted () {
                this.drawerBus.$on('bus-event', this.changeMethod)
            },
            methods: {
                changeMethod (dummyValue2) { // イベントバス経由で値を受け取るならここで引数を定義
                    console.log(dummyValue2)
                    this.drawer = !this.drawer // このように toggle するだけなので、この事例では AppBar から値を受け取る必要がない
                },
            },
        }
    </script>
    

slot がわからん

これが丁寧そう: 1分でわかるVue.jsのslotの使い方(サンプル付き)

XHR, Ajax はどうやる

  • Axios と言う子がよく使われていそう。
  • もともとは vue-resource というのが公式の一部だったがどれでも良いので公式から外された。代わりに Axios が紹介されている (それってどれでもいいといいつつ vue-resource < Axios ってことだよね、うわ、政治) その記事は こちら

axios と async/await の組み合わせ

.then とかやっぱりまだ慣れなくて…とネストが深くなるので…

Vue-router によるルーティング!

公式

  • ルーターインスタンスを作成
  • new Vue して root となる Vue インスタンスを作成
  • Vue インスタンスに router を inject
  • これで components の中で router を扱える (this.$route で現在のルート, this.$router でルーターインスタンス自体にアクセス可能)
    // Home.vue
    export default {
      computed: {
        username () {
          // `params` が表示される
          return this.$route.params.username
        }
      },
      methods: {
        goBack () {
          window.history.length > 1
          ? this.$router.go(-1)
          : this.$router.push('/')
        }
      }
    }
    

CRUD をもっときれいに

どこかでイメージ付のチュートリアルをやっていきたい。

i18n = Vue I18n

面白いとこ

  • single file component ( .vue 拡張子のファイル) で <i18n> というカスタムブロックを作ってそのコンポーネント専用の localization を定義できる。

    • 使い方: GitHub の example を見るのが最短
    • このカスタムブロックを有効化するには vue-i18n-loader を npm install する必要があるのと、他の共通コンフィグにも修正が必要
  • global の定義ファイルを用意することもできる。

  • ロケールを dropdown list などから選択して、画面遷移なくリアクティブに言語を変更することも可能。転がっていたサンプルの LocaleChanger.vue を使いまわして簡単に実現!

    • ページ再読込やページ移動がなくて良さそうに感じるが、 言語 A に合わせて表幅とかレイアウトされたあと、 言語 B に切り替えると崩れるとか…ないかな?
  • lazy loading translations は表示の軽量化に必要そう。要チェケ

  • 定義ファイルと表示箇所をリンクさせるためにも、 translation を表管理 & 番号管理して番号振って、言語種別「debug」みたいなものを用意しておけば、翻訳に定義番号を書くことができて検証が早そう?

vue-cli お作法系

SPA を vue-cli, vue-router で組むことになりそうなので、その枠組に則った中でのお作法をメモしていく。

vue-cli 利用開始

  • vue-cli 公式の説明 を参照。

    npm i -g @vue/cli
    vue create vue-cli(===プロジェクト名)
    cd vue-cli
    npm run serve
    npm run build
    
  • 中身を理解するのに素晴らしい記事に従ってチュートリアル。

  • Hello world アプリの構造を読み解き、 index.html <- (build) <- main.js <- App.vue <- HelloWorld.vue なんだなぁと理解しておく。 index.html は特に何もしていない。 main.js が App.vue を import している。 App.vue が index.html に挿入する html テンプレートを定義していて、 script 部分で挿入するものの詳細を HelloWorld.vue に任せていて、最後に style を定義している。 HelloWorld.vue では index.html に挿入する大枠のコンポーネントにさらに挿入するものを決めている。

グローバル変数の定義方法

  • 状況
    • 例えば backend の basepath を書きたいとする。 /src/main.js に書く、というのが一番トップレベルのファイルなのでまず思い浮かぶが、流石に汚れすぎる。
    • main.js <- plugins/index.js <- plugins/variables.js てな感じの import
    • /src/plugins/variables.js :
      import Vue from 'vue'
      
      Vue.prototype.basePath = 'http://localhost:3000/v1/xxx'
      

Vue Router

Vue Roouter 公式サイト に大体載っているが、経験したところをメモしていく。

名前をつけたルートにリンクするには

router-link コンポーネントの to プロパティにオブジェクトを渡す。 html <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

Hash mode vs. History mode

ルーティングには2種類のモードが有る。 Vue Router 公式が比較をしているわかりやすいページはこちら→ HTML5 History モード

しかしもう少しちゃんとそれぞれのモードを理解したい。

  • hash mode: あの # (ハッシュ) が URL というかパスに入るやつ。 Vue.js では今の所デフォルトでこのモード。「 # が入ることに気持ち悪さを覚える」というコメントが散見されるが、真面目な話でメリットってなんなのだろう? (1) 検索エンジンがハッシュ以下を除去してクローリングやインデックス化の対象にするため、コンテンツを変化させるハッシュは SEO 対策的に不利, (2) 他にも機械的に識別する時に検索エンジン同様に同一ページ扱いされて支障が出うる, (3) やっぱり気持ち悪い, ってところか?
  • history mode: ハッシュがない自然なパスになる。よく見かける注意点は、 (1) ブラウザが古いと history.pushState API に対応していなくてうまく動かない(ページ遷移履歴が追えないということだろう)というのと, (2) SPAをホストするWebサーバが全部のパスをデフォルトで index.html に向けるようにしないと、クライアントがパス指定でアクセスした時に(有効なパスにも関わらず) 404 になる、という2点。どちらも十分利用に堪える話なので良いと言えば良い。

そもそも # (ハッシュ)URL フラグメント というもので、 定義は MDN で勉強しましょう ね。

Vue.js + TypeScript

tutorial

vue-cli v3.8.4 だと create した TypeScript プロジェクトがちょっとピュアな vue.js プロジェクトと コードが違う。 なのでカスタマイズしながら、参考リンクのとおりに勉強していく。

  • 参考: vue.js + typescript = vue.ts ことはじめ

    vue create ts-practice
    # ts, router, eslint を選択
    # あとは全部 yes or 最初の選択肢, 最後の解答だけ No
    
    cd ts-practice
    code .
    npm run serve
    
  • App.vue を修正するとちゃんとルーティングが動くようになるよ。具体的には template の router-view がキモ。

    <template>
    <div id="app">
        <div id="nav">
            <router-link to="/">Home</router-link> |
            <router-link to="/about">About</router-link>
        </div>
        <router-view/>
    </div>
    </template>
    
    <script lang="ts">
    import { Component, Vue } from 'vue-property-decorator';
    export default class App extends Vue {}
    </script>
    
    <style>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
    }
    
    #nav {
        padding: 30px;
    }
    
    #nav a {
        font-weight: bold;
        color: #2c3e50;
    }
    
    #nav a.router-link-exact-active {
        color: #42b983;
    }
    </style>
    
  • 続き

    • やってみたけど理解が追いついていなくて、ちょっと普通に typescript 慣れてからやろうかなと思った。 vue ももう少し慣れていないとリファクタリング or full-scratch がしんどそう

見た目関係

Grid と Flexbox が対決している感覚。と思ったら 比較してくれているサイトが多数!(例: これからのレイアウトはGrid Layoutで決まり?特徴で使い分けたいCSSレイアウト手法

Flexbox

  • リンク
  • 特徴
    • Grid のように 12 column が 1 行に収まるが、 Grid と違って余っている横幅は食い放題
    • 縦に並べていくこともできる
    • 幅の変化に合わせて自動で折返ししてくれたりもする (flex-wrap を指定. 逆順に折り返すのは flex-wrap-reverse を指定)
      <v-card
      class="d-flex flex-row flex-wrap my-6"
      tile
      >
      <v-card
          v-for="resource in resources"
          :key="resource.name"
          class="mx-auto justify-space-around ma-6"
          max-width="300"
          max-height="300"
          shaped
          dark
          elevation="12"
      />
      </v-card>
      

Grid System

開発環境

  • 公式ガイドのツール以降を読むべき:
    • 単一ファイルコンポーネント. 結局ここだけまずはやった
    • ユニットテスト: mocha で書けるっぽい
    • TypeScript のサポート: 別にまだ求めてない
    • プロダクション環境への配信: webpack か browserify をビルドツールとして使う勉強をすることになるかも
    • ルーティング: vue-router を使うんだろうなぁ
    • 状態管理: まだピンときていない。
    • サーバーサイドレンダリング (SSR): 必要にならないといいなぁ… Nuxt.js とか登場するらしいから気にはなるけど

どうやら本格的な大規模開発には html, js だけじゃダメそうだぞ

Vue.jsを使った大規模開発に必要なもの

lint が2重に働く

vuetify.js dashboard template で勉強しているときに遭遇。 ファイル保存時に auto-fix が 2 回動いた。編集が 2 回走るから嫌だ。 Ctrl + Z するときに 2 回戻らないといけない。 多分 babel-eslintVetur (vscode plugin) がどっちも異なるルールで auto-fix しようとしているからだ。

ここではテンプレートの lint rule に乗っ取りたかったので、 vetur の auto-fix を無効化することにした。 設定箇所は vscode extension から veture の設定に行って、 Vetur > Format: Enable のチェックボックスを外せば良い。

どこかでちゃんと理解しておきたいなぁ。

Vue 独自のテスト

  • vue-test-utils: Vue コンポーネントのユニットテストのための公式ライブラリ

css template 探し - Vue Argon Dashboard

Vue Argon Dashboard

Vue Argon Dashboard 使ってみよう。 と思っていた

Argon Design System について調べていた

Argon Design System はたくさんのコンポーネントを備えたオープンソースのBootstrap 4テンプレートです。開発スピードをサポートするのが目的との事でデザインは概ね完成されている印象です。そのまま使えるように設計されているようで、基本的なWebページの他、LPやログインページ、プロフィールページやユーザー登録ページなども同梱されています。開発元は当ブログでもよくご紹介させて頂いているCreative Timによるもので、ライセンスはMITとなっています。

これ入れたらもう vue-cli, vue-router, babel, etc etc が入ったデモアプリが手に入ってしまった。 ちょっと一足飛びすぎ感あるけど、これで勉強していこう。

構造理解

main.js
|-- App.vue
|-- router.js  # routing
    |-- / => /dashboard
    |-- /dashboard => ./views/Dashboard.vue
    |-- /tables => ./views/Tables.vue
        |-- ./Tables/ProjectsTable.vue
            |-- components/baseTable.vue

axios 入れる

  • まず入れる
    npm i -S axios
    
  • src/main.js に追加
    import axios from 'axios'
    Vue.prototype.$axios = axios
    
  • コンポーネント内では、”this.$axios”という書き方で axios を呼び出せるようになります。 CircuitsTable.vue で:
<script>
  export default {
    name: 'circuits-table',
    props: {
      type: {
        type: String
      },
      title: String
    },
    data () {
      return {
        tableData: []
      }
    },
    beforeCreate () {
      this.$axios.get('http://localhost:3000/circuits')
      .then((response) => {
        this.tableData = response.data
      })
      .catch( () => {
      })
    }
  }
</script>

Notification

  • 何度も popup するには このjsfiddle を参考にすると良い
  • いや、 vue-notification プラグインを使うほうが楽
  • いやいや、 vue-argon-dashboard ではそもそも vue ファイルで以下のように呼び出すと動いたぞ?
    this.$notify({
      type: 'success',
      title: 'Important message',
      text: 'Hello user! This is a notification!'
    })
    

css template 鞍替え - Vuetify.js

Material Design に手を出したくなったのと色合い的に転がっているダッシュボードテンプレートが良かったので。

グラフ描画系ライブラリ - vue-chartjs

Web Professional JP の Vue.jsでカッコいいグラフを手軽に作るChart.jsのラッパー3つ などを眺めつつ、調べた。 Chart.js というライブラリが一番人気の様子(Chart.js リンク)。 それを Vue.js 用に実装しているライブラリがいくつかあるが、一番 GitHub star が多い vue-chartjs を採用した(vue-chartjs リンク

vue-chartjs 使い方, 時系列データで line chart

基本的な line-chart, bar-chart は割愛する。時系列データのチャートについて書く。

  • インストール

    $ npm i -S chart.js vue-chartjs
    
  • srv/App.vue で読み込むとする。

    <template>
       <div>
           <v-card class="pa-5">
               <h2>Line Chart test</h2>
               <line-chart />
           </v-card>
       </div>
    </template>
    
    <script>
       import LineChart from '@/components/LineChart'
       export default {
           components: {
               LineChart,
           },
       }
    </script>
    
  • src/components/Linechart.vue はこんな感じ:

    <script>
        // Importing Line class from the vue-chartjs wrapper
        import { Line } from 'vue-chartjs'
        // Exporting this so it can be used in other components
        export default {
            extends: Line,
            props: {
                datacollection: {
                    type: Object,
                    default: () => ({
                        // Data to be represented on x-axis
                        // labels: null,
                        datasets: [
                            {
                                label: 'Data One',
                                backgroundColor: '#f87979',
                                pointBackgroundColor: 'white',
                                borderWidth: 1,
                                pointBorderColor: '#f87979',
                                // Data to be represented on y-axis
                                data: null,
                            },
                        ],
                    }),
                },
            },
    
            data () {
                return {
                    // Chart.js options that controls the appearance of the chart
                    options: {
                        scales: {
                            yAxes: [{
                                ticks: {
                                    beginAtZero: true,
                                },
                                gridLines: {
                                    display: true,
                                },
                            }],
                            xAxes: [{
                                type: 'time',
                                distribution: 'linear',
                                time: {
                                    unit: 'minute',
                                    displayFormats: {
                                        month: 'YYYY/MM',
                                        day: 'YYYY/MM/DD hh:mm',
                                        minute: 'MM/DD hh:mm',
                                    },
                                },
                                gridLines: {
                                    display: false,
                                },
                            }],
                        },
                        legend: {
                            display: true,
                        },
                        responsive: true,
                        maintainAspectRatio: false,
                    },
                }
            },
    
            created () {
            this.datacollection.datasets[0].data = [
                { t: '2019-11-11T15:11:00.000Z', y: 10 },
                { t: '2019-11-11T15:12:00.000Z', y: 12 },
                { t: '2019-11-11T15:13:00.000Z', y: 13 },
                { t: '2019-11-11T15:15:00.000Z', y: 11 },
                { t: '2019-11-11T15:16:00.000Z', y: 10 },
            ]
            },
    
            mounted () {
            // renderChart function renders the chart with the datacollection and options object.
            this.renderChart(this.datacollection, this.options)
            },
        }
    </script>
    
  • 解説

    • データセット (datasets) にタイムスタンプも y 軸の値も入れたオブジェクトを並べる感じ。
    • 時刻の表現は Moment.js が読める形式ならよく、 Date オブジェクトの new Date('2019-11-18 21:45:59') だったり、 ISO 形式の文字列 '2019-11-18T13:34:45.000Z' など。もう少しゆるい文字列でも動く。
    • プロットが等間隔でない(時間が飛び飛び)ものはちゃんとその通りプロットしてくれる。これは distribution'linear' にしているから。
    • ラベルの表示単位を設定するのは time.unit で、その時の表記ルールをカスタマイズするのが time.displayFormats の中の奴ら。

未整理 tips

TypeScript 導入可否

  • 可能! 公式にも解説がある。