Vue.js 単一ファイルコンポーネントを使って開発環境を強化する

最終更新日
2018.03.05

Vue.js はコンパイルせずにそのままブラウザで利用できるのも特徴だけどやっぱり .vue ファイルを使いたい!

単一ファイルコンポーネント

単一ファイルコンポーネントはコンポーネントのテンプレート、JavaScript、CSS を1つのファイルでまとめて管理できるコンポーネントの定義方法です。 独自のフォーマットで構成されているため、Webpack などを使ってプリコンパイルする必要があります。 いくつかのバンドルツールを利用できますが、この記事では Webpack を使った方法を説明しています。

MyComponent.vue

<template>
<div id="example">
  <span>{{ text }}</span>
</div>
</template>

<script>
export default {
  data() {
    return {
      text: 'example'
    }
  }
}
</script>

<style scoped>
span { color: #ffbb00; }
</style>

このファイルを importrequire で読み込んで、親コンポーネントの components オプションに登録します。

main.js

import MyComponent from 'MyComponent.vue'
new Vue({
  el: '#app',
  components: {
    MyComponent
  }
})
// グローバルもOK
Vue.component(MyComponent)

これは Vue の開発もだいぶ捗りそう(•ө•)ノ

scoped CSS

スタイルシート定義に scoped を設定しておくと、そのコンポーネントで使っているテンプレートの要素と CSS の定義が紐付けられます。 他のスタイルと競合することが無くなるので基本的には scoped で書くといいです。 ある程度クラス名が付いていたほうがパフォーマンスが良いのと、何を意味するスタイルか分かりやすいため併用するといいようです。 上の例ではコンポーネント内の span タグだけ文字色が変わります。 ちなみに、コンポーネントのルートタグには親と子の両方のスコープが付くのでどちらに CSS を書いても大丈夫。

外部ファイルを読み込むこともできる

長くなる場合は .vue ファイルから外部ファイルを読み込む事ができます。 スタイルシートファイルは複数読み込み可能。

child-comp.vue

<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style1.scss" lang="scss"></style>
<style src="./style2.scss" lang="scss" scoped></style>

Vue CLIを使う場合

Vue CLI は Vue プロジェクトの開発環境を自動的にセットアップするコマンドラインインターフェースです。 あらかじめ Node.js のインストールと npm コマンドの基礎知識が必要になります。

ややこしい Webpack や ホットリロードの設定ファイルを作成してくれてコードを書き始める前から悩まずに済みます。 ペライチ構成のサイトや SPA サイトを最初から作るならこれを使うとサクッと始められます。

npm install -g vue-cli
vue init (template-name) (project-name)
cd (project-name)
npm install

さまざまな開発環境を想定したテンプレートが用意されていますが、個人的には「webpack-simple」か「webpack」テンプレートをよく使っています。 プロダクト向けの開発には「webpack」テンプレートが推奨されてますが、学習やプロトタイプ開発には「webpack-simple」テンプレートで十分な場合が多く、シンプルで設定構成を追いやすいです。

手軽に試す webpack-simple の init

npm install -g vue-cli
vue init webpack-simple vue-test
cd vue-test
npm install

次のような手順でインストールします。

  1. Vue CLI をインストール
  2. Vue プロジェクトを「webpack-simple」テンプレートを使ってセットアップ
  3. プロジェクトのディレクトリに移動
  4. package.json に記述されている必要なモジュール群をインストール

インストールが終わったら以下のコマンドで開発サーバーを起動できます。

npm run dev

デフォルトでは「localhost:8080」でアクセスできます。 このサーバーが立ち上がっている間はホットリロードが有効なのでファイルを編集するとブラウザリロードしなくても編集した部分だけが更新されます。このホットリロード機能が物凄い便利!

ちなみに「webpack」テンプレートではデフォルトで CSS の抽出と JavaScript をチャンク化してビルドしてくれます。 インストール時に ESLint や テストモジュールを使うかなどいくつか質問され、ESLint は便利ですがエディタで既に使用している場合もあると思うので好みで選択します。

開発時のAPIのパス統一やクロスドメイン対策

開発サーバーの API のパスは /local/api だけど 本番は /main/api なんだよね~というケースがあります。

開発サーバーのホスト名はデフォルトでは localhost:8080 になりますが、パスを変えたり別のドメインにある API からデータを取得するにはプロキシを設定するといいです。

config/index.js (webpack-simple の場合は webpack.config.js)

proxyTable: {
  '/main/api': {
    target: 'http://dev.example.com',
    changeOrigin: true,
    pathRewrite: {
      '^/main/api': '/local/api'
    }
  }
}

これで http://dev.example.com/main/api というパスで /local/api にアクセスできるようになります。

開発モードでCSSソースマップを使用する

ソースマップは JavaScript や CSS の圧縮前どこに書いてあったか確認できるコードを添付する機能です。 開発中はほぼ必須なのだけど、CSS のソースマップはデフォルトではオフになっています。

https://github.com/webpack-contrib/css-loader#sourcemap

webpack

「config/index.js」一番下のほう。

cssSourceMap: true

webpack-simple

webpack.config.js

{
  test: /\.vue$/,
  loader: 'vue-loader',
  options: {
    loaders: {
      'scss': process.env.NODE_ENV === 'production' ? 
        'vue-style-loader!css-loader!sass-loader' : 
        'vue-style-loader!css-loader?sourceMap!sass-loader'
    }
    // other vue-loader options go here
  }
}

webpack-simple で開発モードだけソースマップをオンにする

「webpack-simple」テンプレートでは、プロダクトモードでソースマップがオンになっています。 ビルド後は基本不要なので開発モードだけオンにします。

if (process.env.NODE_ENV === 'production') {
  // コメントアウトする
  // module.exports.devtool = '#source-map'
  // http://vue-loader.vuejs.org/en/workflow/production.html
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: false,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    })
  ])
} else {
  // else で開発モードの設定を追加
  module.exports.devtool = '#eval-source-map'
  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"development"'
      }
    })
  ])
}

package.json インストールする場所

package.json にはプロジェクトの情報や使用するモジュールとバージョンなどが書かれています。

--save (-S) でインストールすると package.json の dependencies--save-dev (-D) でインストールすると devDependencies に追加されます。

基本は全部 devDependencies でも大丈夫なんですけど、開発環境として使うものは -D で、実際にアプリで使うもの同梱するものは -S でインストールするのがセオリーみたいです。モジュールはコマンドラインからインストール・アンインストールして package.json を直接編集するのはなるべく避けるといいです。

非SPAサイトでホットリロード機能を使う

Vue CLI のホットリロード最高かよ! Vue-cli を使ったサーバーでの開発では XAMPP などで作っているファイルは表示できませんがそんな非 SPA サイトでもホットリロードを使う記事も書きました。 こちらから

Gulp+Babel+Webpack を使う場合

SPA じゃない場合は Gulp でやりたい場合もあります。 モジュール化させたファイルを Webpack にまとめてもらい Babel でブラウザ用に変換して Gulp に編集をウォッチしてもらう事にします。 自分で Webpack の設定とかしないといけないので Vue CLI を使う場合よりも初期設定がやや面倒くさい。

必要なモジュール

とりあえず Webpack と vue-loader というモジュールを使います。

  • vue-html-loader
  • vue-template-compiler(vueと同じバージョン)
  • css-loader
  • sass-loader(SASS/SCSS を使う場合)
  • node-sass(同上)
  • babel-loader
  • babel-preset-es2015

上記の開発に必要なモジュールを --save-dev を付けてインストールします。実際にアプリで使うライブラリの場合は --save でインストールします。

もし何か足りなければそんな感じのエラーが出ます。 英語ですが何が原因なのかちゃんと書いてあるのでコピペしてGoogle翻訳するといいです。

gulpfile.js を書く

var gulp = require('gulp');
var webpack = require('webpack');

Webpack 自体凄い沢山設定があって覚えるの大変そうですね(´•ω•`)

var webpackBuild = function (opt) {
    var plugins = [
        new webpack.optimize.UglifyJsPlugin({
            sourceMap: opt.map ? true : false,
            compress: {
                warnings: false
            },
        }),
    ];
    webpack({
        entry: './src/js/main.js',
        output: {
            filename: 'bundle.js',
            path: "./dist/js/",
        },
        plugins: opt.min ? plugins : [],
        module: {
            rules: [{
                    test: /\.vue$/,
                    exclude: /node_modules/,
                    loader: 'vue-loader',
                    options: {
                        loaders: {
                            scss: 'vue-style-loader!css-loader!sass-loader?includePaths[]=./src/sass/'
                        }
                    }
                },
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    loader: 'babel-loader',
                }
            ]
        },
        devtool: opt.map ? 'source-map' : '',
        resolve: {
            extensions: ['.js', '.vue']
        }
    }, function (error, stats) {});
}
gulp.task('webpack.watch', function() {
    webpackBuild({ map: false, min: false });
});

`.vue` ファイルの中で SCSS の使用と変数定義用のファイルを `@import` をできるようにしました。

watch タスクに登録。

gulp.task('watch', function() {
  gulp.watch(['./src/**/*.js', './src/**/*.vue', './src/**/*.scss'], ['webpack.watch']);
});

CSSをファイルに抽出する

Webpack テンプレートの設定を参考にして、.vue の中の CSS コードを抽出してファイルに吐くようにしてみました。vue-loader のページにも載っています

var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

var webpackBuild = function (env) {
  var webpackBaseConfig = {
    cache: true,
    devtool: 'source-map',
    entry: {
      app: './src/main.js',
      /* 自分のコード以外をまとめる */
      vendor: ['vue', 'axios', 'es6-promise']
    },
    output: {
      path: './dist',
      filename: '[name].js',
      publicPath: 'dist'
    },
    resolve: {
      extensions: ['.js', '.vue'],
      alias: {
        '@': path.resolve('src'),
        'vue$': 'vue/dist/vue.common.js'
      }
    },
    module: {
      rules: [
        {
          test: /\.vue$/,
          loader: 'vue-loader',
          include: [path.resolve('src')],
          options: {
            /* vue-loader のオプション CSSを抽出する */
            loaders: {
              'css': ExtractTextPlugin.extract({
                use: ['css-loader'],
                fallback: 'vue-style-loader',
              }),
              'scss': ExtractTextPlugin.extract({
                use: [{
                  loader: 'css-loader'
                },
                {
                  loader: 'sass-loader',
                  options: {
                    includePaths: [path.resolve('src/sass')]
                  }
                }],
                fallback: 'vue-style-loader',
              })
            },
          }
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          include: [path.resolve('src')],
        },
      ]
    },
    plugins: [
      new webpack.DefinePlugin({
        'process.env': {
          'NODE_ENV': JSON.stringify(env)
        }
      }),
      /* ライブラリを共有ファイルにする */
      new webpack.optimize.CommonsChunkPlugin({
        name: 'vendor',
        filename: 'vendor.bundle.js',
        minChunks: Infinity,
      }),
      /* JavaScriptを圧縮する */
      new webpack.optimize.UglifyJsPlugin({
        /* ソースマップがある場合はここでも指定 */
        sourceMap: true,
        compress: {
          warnings: false
        },
      }),
      /* CSSを抽出する */
      new ExtractTextPlugin({
        filename: './css/app.bundle.css'
      }),
      /* ローダー用オプション ロードファイルの改行スペース省くだけ? ↓かどっちか */
      /*new webpack.LoaderOptionsPlugin({
        minimize: true,
      }),*/
      /* CSSの最適化 */
      /*new OptimizeCSSPlugin({
        cssProcessorOptions: {
          safe: true,
        },
      }),*/
    ],
  }
  webpack(webpackBaseConfig, function (error, stats) {});
};

gulp.task('webpack.build', function () {
  webpackBuild('production');
});
gulp.task('webpack.dev', function () {
  webpackBuild('development');
});

ちょっと遅いのでやはりホットリロードのありがたみを感じる。

おわりに

正直 Vue CLI がとてもお手軽なので、特に事情がないなら Vue CLI を使うのがおすすめ!

とりあえず今まで何も考えずに jQuery で作っていた所はプレーンな JavaScript や Vue コンポーネントにしました。Vue は導入する場合全部作りなおさないとダメというわけじゃなくて、すこしづつ移行できるっていうのもいいですね(๑'ᴗ'๑)

Vue を使い始めると DOM に直接アクセスする事は大分減りますが、それでも body タグとか Vue アプリの範囲外を操作したい時があります。 標準関数だとセレクタ操作は結構面倒くさいので、jQuery よりコンパクトな Selector というセレクタ処理に特化したライブラリを Vue と併用するのも良さそう。 (ちょっとした DOM 操作のためだけに読み込むには jQuery は少々サイズが大きいので)

さて、前回親子間のコンポーネントのやりとりをやったので、次回は非親子間のコンポーネントのやりとりについてやります。