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

#初めに

正直JS界隈のエコシステムは難しいです。わからないことが目白押しです。

今までVue.jsをブラウザ上でなんのエコシステムも使わずに使ってきましたが、
規模が大きくなるとしんどくなるのは明らかなので
[Vue.jsガイド：大規模アプリケーションの構築](http://v1-jp.vuejs.org/guide/application.html)を参考にwebpackを使ったりnpmを使って、
それっぽい環境を作って一からコードを書いてみました。
（編注：上記リンクはv1.0当時のものです。v2.0のガイドは[単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)、[プロダクション環境への配信のヒント](https://jp.vuejs.org/v2/guide/deployment.html)、[ルーティング](https://jp.vuejs.org/v2/guide/routing.html)、[状態管理](https://jp.vuejs.org/v2/guide/state-management.html)、[単体テスト](https://jp.vuejs.org/v2/guide/unit-testing.html)、[サーバサイドレンダリング](https://jp.vuejs.org/v2/guide/ssr.html)などをご覧ください。）



#前提
[Vue.jsガイド：大規模アプリケーションの構築](http://v1-jp.vuejs.org/guide/application.html)の指示に従い構築するだけですが、この時点で以下の様な選択肢がある模様です。

* プリプロセッサ？の違い
Webpack(こっちだけ触ります) 
Browserify

* ルーティング
オフィシャルでvue-router推奨なのでそれに従います。

* サーバとの通信
vue-resourceを使うといいみたいですが、今回はjqueryを使います。

* 単体テスト
Karma


上記以外の技術は使わない方向でやってみます。

#はじめよう

まずはnpmを使えるようにします。

```
$ brew install npm
```

##環境構築

###npmのパッケージ導入

プロジェクトの為のディレクトリを作りそのディレクトリに移動します。
そして、以下のコマンドでpackage.jsonを作成します。

```
$ npm init
```

対話プロンプトに適当に答えると以下の様なファイルが作成されます。

```json:package.json
{
  "name": "vuejssample",
  "version": "0.0.1",
  "description": "vue.js + webpack+ vue-loader mysample",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "m0a",
  "license": "MIT"
}
```


webpack,vue-loaderの導入(local)

```bash:インストール
#ローカルインストール
npm install webpack vue-loader  --save-dev
npm install vue --save

#グローバルインストール
npm install webpack -g 

```

``--save-dev`` オプションはインストール時に開発用の依存関係としてpackage.jsonに登録するってことです。
``--save``は実際に必要なライブラリをpackage.jsonに登録します。

ローカルインストールなので``node_modules``にインストールされますが、
リモートリポジトリに登録する際に上記を除外します。


除外するために``.gitignore``を作っておきます。自分の場合は[環境](http://qiita.com/m0a/items/5577aa170cb4dfc32b7e)を作っているので
``gi >.gitignore ``で一発です。

以降必要なライブラリはnpmで管理できるようになりました。


###webpackの設定

webpack用のconfigは`webpack.config.js`となります。
webpackがビルドプロセスでvue-loaderを動かして.vueファイルを解釈してくれます。

以下の様なconfigを作ります。

```js:webpack.cinfig.js

// webpack.config.js
module.exports = {
  entry: './src/main.js',
  output: {
    path: "./build",
    filename: 'build.js'
  },
  module: {
    loaders: [
      { test: /\.vue$/, loader: 'vue' },
    ]
  }
}
```

`src/main.js`にコードを書いていき最終的な結果を得るのは`build/build.js`というわけですね。

##開発します。(最初はシンプルに)

まずはディレクトリを作ってみます。
webpack.config.jsに書いたようにsrcディレクトリが開発用
buildディレクトリが出力用です。

以下のように基本的なファイルを作成します。

```js:./src/main.js
var Vue = require('vue')
var appOptions = require('./app.vue')
var app = new Vue(appOptions).$mount('#app')

```
```:./src/app.vue
<style>
  .red {
    color: #f00;
  }
</style>

<template>
  <h1 class="red">{{msg}}</h1>
</template>

<script>
  module.exports = {
    data: function () {
      return {
        msg: 'Hello world!'
      }
    }
  }
</script>
```

```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue simple sample </title>
  </head>
  <body>
    <div id="app"></div>
    <script src="build/build.js"></script>
  </body>
</html>


```

editorにatomを使っているなら``Language Vue Component``を導入するとシンタックスハイライトが効きます。

作り終わったら

```
$ webpack --watch
```
でファイル修正時に自動でビルドが効くようになります。




参考:
https://github.com/vuejs/vue-loader-example


ここまでの作業を以下に記録します
https://github.com/m0a-mystudy/vuejssample/tree/v1.0


##vue-router導入

Vue.jsはシンプルで素敵ですがrouterがないのが辛いです。
公式推奨のvue-routerを導入します。

以下のコマンドを実行するだけで導入できます。

```bash:インストール
$ npm install vue-router --save
```
おお、簡単に導入できた。環境を作るメリットがやっとわかりました。

main.jsをいじってモジュールをロードしルーティングの設定コードを記述します。

```diff:main.js

var Vue = require('vue')
+ var VueRouter = require('vue-router')
+ Vue.use(VueRouter)
- var appOptions = require('./app.vue')
- var app = new Vue(appOptions).$mount('#app')
+ var PageA = Vue.extend(require('./components/pageA.vue'));
+ var PageB = Vue.extend(require('./components/pageB.vue'));
+ var App = Vue.extend({});
+ var router = new VueRouter();

+ router.map({
+     '/pageA': {
+         component: PageA
+     },
+     '/pageB': {
+         component: PageB
+     }
+ })
+ 
+ router.start(App, '#app')

```

app.vueは削除して以下のファイルを追加します。

```src/components/pageA.vue
<style>
  .yellow {
    color: #f0f;
  }
</style>

<template>
  <h1 class="yellow">apge A</h1>
  <p>
    this page is A!!!
  </p>
  <p>
    msg = {{msg}}
  </p>

</template>

<script>
  module.exports = {
    data: function () {
      return {
        msg: 'yes page A'
      }
    }
  }
</script>
```

```src/components/pageB.vue
<style>
  .blue {
    color: #00f;
  }
</style>

<template>
  <h1 class="blue">apge B</h1>
  <p>
    this page is B!!!
  </p>
  <p>
    msg = {{msg}}
  </p>

</template>

<script>
  module.exports = {
    data: function () {
      return {
        msg: 'yes page B!!!!!'
      }
    }
  }
</script>
```

index.htmlを以下のように修正します。

```
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>vue simple sample </title>
  </head>
  <body>
    <div id="app">
      <h1>Hello App!</h1>
      <p>
        <!-- ナビゲーション向けに v-link ディレクティブを使用 -->
        <a v-link="{ path: '/pageA' }">Go to PageA</a>
        <a v-link="{ path: '/pageB' }">Go to PageB</a>
      </p>
      <!-- route outlet -->
      <router-view></router-view>
    </div>
    <script src="build/build.js"></script>
  </body>
</html>

```

ページ遷移ができました。

以下に記録を残します。
https://github.com/m0a-mystudy/vuejssample/tree/v2.0/


## jQueryをglobalに設定する

さてjQueryをインストールします。

```bash:インストール
npm install jquery --save
```

以下の設定でglobalにします。

```diff:webpack.config.js
+var webpack = require('webpack');
+
 module.exports = {
   entry: './src/main.js',
   output: {
     path: './build',
     filename: 'build.js'
   },
   module: {
     loaders: [
       { test: /\.vue$/, loader: 'vue' },
     ]
-  }
+  },
+  plugins: [
+    new webpack.ProvidePlugin({
+      $: "jquery",
+    })
+  ]
 }
```

試しに使ってみます。

以下のようにコードを変更

```diff:src/components/pageA.vue
<template>
   <h1 class="yellow">apge A</h1>
   <p>
    this page is A!!!
   </p>
   <p>
     msg = {{msg}}
   </p>
+  <pre>
+    {{items | json 2}}
+  </pre>
+

 </template>

 <script>
+  // webpack.ProvidePluginがないと以下を追加しないとjqueryが使えません
+  // var $ = require('jquery');
   module.exports = {
     data: function () {
       return {
-        msg: 'yes page A'
+        msg: 'yes page A',
+        items:''
+      }
+    },
+    ready:function(){
+      this.getJson();
+    },
+    methods: {
+      getJson:function(){
+        var that = this;
+        $.ajax({
+          type: 'GET',
+          crossDomain: true,
+          url: 'https://qiita.com/api/v2/items?page=1&per_page=5',
+          dataType: 'json',
+          success: function(json) {
+            that.$data.items = json;
+          },
+          data: null
+        });
+
       }
     }
   }
```

こうして書いてみるとテンプレートとコードが一つにまとめられるのは便利っぽいですね。

以下にコードを残します。
https://github.com/m0a-mystudy/vuejssample/tree/V3.0

#テスト

テスト環境を作ります。

##karmaの導入

karma自体はテストランナー（テストを自動実行するツール）で実際のテスト用のフレームワークはjasmineを使います。
jsのエコシステムはこんな感じでいろんな名前が付いてるツールを組み合わせるし、デファクトスタンダードもよくわからないので、本当に途方にくれます。

<!-- golangはパッケージ管理もテストツールも全部言語の中に収まっているので、（しかも言語仕様もシンプルだし）本当に素晴らしいです。 -->

まずはシンプルな環境から入れていきます。
`karma-chrome-launcher`を入れていることからわかるようにテスト自体はchromeで行う想定です。
別なブラウザを使う方は逐次入れ替えてください。


```bash:インストール
# Install Karma:
$ npm install karma --save-dev

# Install plugins that your project needs:
$ npm install karma-jasmine karma-chrome-launcher --save-dev

$ npm install -g karma-cli
```

参考:http://karma-runner.github.io/0.13/intro/installation.html


さてここでさらに今回の環境のプラグインを導入します。


```bash:インストール
npm install --save-dev karma-webpack
npm install --save-dev karma-sourcemap-loader
```

`karma-webpack`を使うことでテストを実行するファイルをプリプロセスでワンファイル化します。今回`.vue`ファイルにまとまっていますが、
karma自体にこのファイルを解釈する機能がないのでwebpackでjsに変換後にテストを走らせる想定です。

ただしそのままではエラー時に行番号が分からなくなるので、`karma-sourcemap-loader`を入れることで行番号を出すようにします。


###karma用のconfig作成

以下のコマンドにてkarma.conf.jsを作成します。

```bash
krama init
```

対話インターフェースには全部イエスと答えておきます。
作成後以下のように修正します。

```javascript:karma.conf.js
var webpack = require('webpack');

// Karma configuration
// Generated on Sat Oct 03 2015 17:25:04 GMT+0900 (JST)
module.exports = function(config) {
  config.set({

    basePath: '',
    frameworks: ['jasmine'],
    files: [
      'test/*_test.js', //<-- testディレクトリ内にtestを書く想定です。
    ],
    exclude: [
    ],

    preprocessors: {
      'test/*_test.js': ['webpack','sourcemap'],  //<-- 追加
    },

    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
	// <-- 追加（start）
    webpack: {
            // webpack configuration
          module: {
            loaders: [
              { test: /\.vue$/, loader: 'vue' },
            ]
          },
          plugins: [
            new webpack.ProvidePlugin({
              $: "jquery",
            })
          ],
          devtool: 'inline-source-map' //<-- 基本的にほとんどwebpack.config.jsと同じですが、ここの追加を忘れないようにします。
    },
    webpackMiddleware: {
            // webpack-dev-middleware configuration
            // i. e.
        // inoInfo: true
    },
    plugins: [
      require("karma-jasmine"),
      require("karma-webpack"),
      require("karma-chrome-launcher"),
      require("karma-sourcemap-loader"),
     ]
	// <-- 追加（end）
  })
}
```

##テストの実行

プロジェクト直下から以下のコマンドで実行します。

```
karma start
```

専用のブラウザが立ち上がりファイルが変更されるたびに自動実行されます。
ログはターミナルに出るので、別途ターミナルを立ち上げて実行した方がいいでしょう。


##テストコードを書いていく

testディレクトリを作成し以下のようにテストコードを記載していきます。

```javascript:test/pageA_test.js

describe('pageA', function () {
  // require source module
  var pageA = require('../src/components/pageA.vue');
  it('should have a ready hook', function () {
    expect(typeof pageA.ready).toBe('function')
  })
  it('should set correct default data', function () {
    expect(typeof pageA.data).toBe('function')
    var defaultData = pageA.data()
    expect(defaultData.msg).toBe('yes page A')
  })
  it('set method cheeck', function() {
    expect(typeof pageA.methods.getJson).toBe('function')
  })
})

//非同期実行のテスト
describe('pageA.methods.getJson', function () {
  // require source module
  var pageA = require('../src/components/pageA.vue');
  var pageAThis = {$data:{}}; //pageA.methods.getJsonはthisにアクセスするのでここで作成しておく
  beforeEach(function(done){
     pageAThis.$data = pageA.data(); //$data手動初期化
    //  console.log(pageAThis);
    pageA.methods.getJson.call(pageAThis,done); //thisを指定して呼び出し。callバックにてdoneを実行されないと次に進まない
  })
  
  it('pageA.methods.getJson result', function() { //done実行後に呼び出される
    // console.log(pageAThis);
    expect(typeof pageAThis.$data.items).toBe('object')
  })
})

```

テストが自動実行されるなら以下のようなエラーが出るかと思います。

```
Chrome 45.0.2454 (Mac OS X 10.11.0) pageA.methods.getJson pageA.methods.getJson result FAILED
	Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Chrome 45.0.2454 (Mac OS X 10.11.0): Executed 4 of 4 (1 FAILED) (5.006 secs / 5.003 secs)

```

pageAのgetJsonはcallbackを受け取らない実装になっていたので修正します

```diff:src/components/pageA.vue

     methods: {
-      getJson:function(){
+      getJson:function(callback){
         var that = this;
         $.ajax({
           type: 'GET',
@@ -42,6 +42,9 @@
           dataType: 'json',
           success: function(json) {
             that.$data.items = json;
+             if(typeof callback === 'function') {
+                 callback();
+             }
           },
           data: null
         });
```

保存すると自動実行されて今度は成功しているかと思います。

ここまでの作業は以下に記録しています。
https://github.com/m0a-mystudy/vuejssample/tree/V4.0

次やるとしたらプロダクション環境の構築になります。