Nuxt(v2)にMaterial Components for the Webを導入する

# はじめに

当記事では、Google社製のマテリアルデザイン用のオープンソースライブラリであるMaterial Components for the Web（以下、「MDC Web」）をNuxt.js(v2)に導入するための手順を記載します。


Material Components for the Web 関連リンク
- [公式ドキュメント](https://material.io/develop/web/)
- [GitHubリポジトリ](https://github.com/material-components/material-components-web/)

# Nuxtプロジェクトの作成

まずはNuxtのプロジェクトを作成して行きます。

## 準備
必要なものを準備しましょう。Node.jsとnpmをインストールします。

【必要なもの】※バージョンは筆者の動作環境のものを記載
- Node.js 8.11.3
- npm 5.6.0

## プロジェクト作成

vue-cliをインストールします。今回はyarnも使ったので一緒にインストールします。（npmだけでも問題ないです）

```
npm install -g vue-cli yarn
```

vueコマンドでNuxtのテンプレートプロジェクトを作成します。（nuxtpfというディレクトリとします）
実行するといくつか質問されるので、任意のものを入力してください。

```
vue init nuxt-community/starter-template nuxtpj
```

作成されたディレクトリに移動します。

```
cd nuxtpj
```

次に、必要なモジュールをインストールします。

```
yarn
```

http://localhost:3000
をブラウザで開くと、Nuxt.jsのウェルカムページが表示されます。

## MDC Webのインストール

ここから本題のMDC Webのインストールについて記載して行きます。

## package.json

package.jsonの中身は下記の通りです。追加したのは以下のモジュールです。
手動で個別に入れても構いませんし、package.jsonを書き換えて一括で入れても構いません。

- material-components-web
- node-sass
- postcss-hexrgba
- postcss-loader
- postcss-nested
- postcss-responsive-type
- sass-loader
- style-loader
- vue-loader
- vue-style-loader

```json
{
  "name": "nuxtpj",
  "version": "1.0.0",
  "description": "Nuxt.js project",
  "author": "",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint"
  },
  "dependencies": {
    "@nuxtjs/axios": "^5.3.6",
    "material-components-web": "^0.43.1",
    "node-sass": "^4.11.0",
    "nuxt": "^2.0.0",
    "postcss-hexrgba": "^1.0.1",
    "postcss-loader": "^3.0.0",
    "postcss-nested": "^4.1.1",
    "postcss-responsive-type": "^1.0.0",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "vue-loader": "^15.6.1",
    "vue-style-loader": "^4.1.2"
  },
  "devDependencies": {
    "babel-eslint": "^10.0.1",
    "eslint": "^4.19.1",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-loader": "^2.1.1",
    "eslint-plugin-vue": "^4.0.0"
  }
}
```

## nuxt.config.js
次に、nuxt.config.jsにwebpackの設定を行います。

```js
module.exports = {
  /*
  ** Headers of the page
  */
  head: {
    title: 'nuxtpj',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient, loaders: { scss } }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }

      scss.includePaths = ['node_modules',] // 追加（※後述）
    },

    // 追加（※後述）
    postcss: [
      require('postcss-nested')(),
      require('postcss-responsive-type')(),
      require('postcss-hexrgba')(),
    ],
  },
  modules: [
    '@nuxtjs/axios',
  ]
}

```

設定は以上です。

動作確認のためにdefault.vueファイルを編集します。

```vue:layouts/default.vue
<template>
  <div>
    <button class="mdc-button" ref="button"><span class="mdc-button__label">テスト</span></button>
    <nuxt/>
  </div>
</template>

<style lang="scss">
@import "~material-components-web/material-components-web";

</style>

<script>
import {MDCRipple} from '@material/ripple'

export default {
    mounted() {
      this.ripple = new MDCRipple(this.$refs.button)
    },
    destroyed() {
      this.ripple.destroy()
    }
}
</script>

```

サーバを起動します。

```
yarn dev
```

http://localhost:3000
にアクセスしてみてください。

表示されたボタンを押下するとマテリアルなRippleエフェクトが付いた動きが確認できると思います。

# トラブルシューティング

NuxtにMDC Webを導入するにあたり、情報が少なかったり、エラーが良くわからんかったりと、だいぶ苦労しました。
以下、構築中に発生した問題と解決策を記載しておきます。

## SCSSのincludePahts設定

MDC Webを入れて起動した直後のエラーです。

```
yarn add material-components-web
```

```vue:layouts/default.vue
<template>
  <div>
    <nuxt/>
  </div>
</template>

<style lang="scss">
@import "~material-components-web/material-components-web";

</style>
```


```bash:エラー内容
 ERROR  Failed to compile with 1 errors                                                      friendly-errors 17:09:59


 ERROR  in ./layouts/default.vue?vue&type=style&index=0&lang=scss&                           friendly-errors 17:09:59

Module build failed (from ./node_modules/sass-loader/lib/loader.js):                         friendly-errors 17:09:59

undefined
^
      File to import not found or unreadable: @material/button/mdc-button.
      in c:\develop\pota\client\node_modules\material-components-web\material-components-web.scss (line 23, column 1)
                                                                                             friendly-errors 17:09:59
 @ ./node_modules/vue-style-loader!./node_modules/css-loader??ref--6-oneOf-1-1!./node_modules/@nuxt/webpack/node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--6-oneOf-1-2!./node_modules/sass-loader/lib/loader.js??ref--6-oneOf-1-3!./node_modules/vue-loader/lib??vue-loader-options!./layouts/default.vue?vue&type=style&index=0&lang=scss& 4:14-395 14:3-18:5 15:22-403
 @ ./layouts/default.vue?vue&type=style&index=0&lang=scss&
 @ ./layouts/default.vue
 @ ./.nuxt/App.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=/__webpack_hmr/client ./.nuxt/client.js
                                                                                             friendly-errors 17:09:59
i Waiting for file changes
```

どうやら、MDC Web自体はnode_modulesのmaterial-components-webの中にあるのですが、実際はnode_modules/@material配下にあるコンポーネントの各ディレクトリを参照する構造になっているようです。

Getting Startedに書いてある通りです。

https://material.io/develop/web/docs/getting-started/
> Note: Configuring includePaths should suffice for most cases where all MDC Web packages are kept up-to-date together. If you encounter problems compiling Sass due to nested node_modules directories, see the Appendix below on how to configure a custom importer instead.

従って、scssにパスの解決をしてあげる必要があるとのこと。includePathsを設定します。

```js:nuxt.config.js
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient, loaders: { scss } }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }

      scss.includePaths = ['node_modules'] // パスを追加
    },
```

ここまででとりあえずマテリアルデザインの基本的なものは使えるようです。

## PostCSS設定

次に、こんなエラーが出ました。

```bash:エラー内容
 WARN  Compiled with 2 warnings                                                              friendly-errors 17:17:57


 WARN  in ./layouts/default.vue?vue&type=style&index=0&lang=scss&                            friendly-errors 17:17:57

Module Warning (from ./node_modules/postcss-loader/src/index.js):                            friendly-errors 17:17:57
Warning

(6603:5) start value has mixed support, consider using flex-start instead
                                                                                             friendly-errors 17:17:57
 @ ./node_modules/vue-style-loader!./node_modules/css-loader??ref--6-oneOf-1-1!./node_modules/@nuxt/webpack/node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--6-oneOf-1-2!./node_modules/sass-loader/lib/loader.js??ref--6-oneOf-1-3!./node_modules/vue-loader/lib??vue-loader-options!./layouts/default.vue?vue&type=style&index=0&lang=scss& 4:14-395 14:3-18:5 15:22-403
 @ ./layouts/default.vue?vue&type=style&index=0&lang=scss&
 @ ./layouts/default.vue
 @ ./.nuxt/App.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=/__webpack_hmr/client ./.nuxt/client.js
                                                                                             friendly-errors 17:17:57

 WARN  in ./layouts/default.vue?vue&type=style&index=0&lang=scss&                            friendly-errors 17:17:57

Module Warning (from ./node_modules/postcss-loader/src/index.js):                            friendly-errors 17:17:57
Warning

(6721:5) start value has mixed support, consider using flex-start instead
                                                                                             friendly-errors 17:17:57
 @ ./node_modules/vue-style-loader!./node_modules/css-loader??ref--6-oneOf-1-1!./node_modules/@nuxt/webpack/node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/src??ref--6-oneOf-1-2!./node_modules/sass-loader/lib/loader.js??ref--6-oneOf-1-3!./node_modules/vue-loader/lib??vue-loader-options!./layouts/default.vue?vue&type=style&index=0&lang=scss& 4:14-395 14:3-18:5 15:22-403
 @ ./layouts/default.vue?vue&type=style&index=0&lang=scss&
 @ ./layouts/default.vue
 @ ./.nuxt/App.js
 @ ./.nuxt/index.js
 @ ./.nuxt/client.js
 @ multi webpack-hot-middleware/client?name=client&reload=true&timeout=30000&path=/__webpack_hmr/client ./.nuxt/client.js
                                                                                             friendly-errors 17:17:57
i Waiting for file changes
```

これもGetting Statrtedに書いてある通りで、PostCSSにautoprefixerなどの設定をしてあげないといけないそうです。

> In order to add vendor-specific styles to the Sass files, we need to configure autoprefixer through PostCSS.

```json
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient, loaders: { scss } }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }

      scss.includePaths = ['node_modules',]
    },

    // 追加
    postcss: [
      require('postcss-nested')(),
      require('postcss-responsive-type')(),
      require('postcss-hexrgba')(),
    ],
  },
```

## ちなみに...

MDCRippleをVueで使う際はmountedのところでDOMの参照をしてあげる必要があります。そのため、button要素などにref属性を付けて参照できるようにしてあげる必要があります。

```html
<button class="mdc-button" ref="button">
```
# おわりに

まだMDCButton+MDCRippleの確認程度までしか出来てませんが、一旦はこれで動作するかと思います。
何かあればコメント等頂ければアップデートして行きたいと思いますので、
よろしくお願いいたします。

尚、筆者はやっと実装に入れるー！と大喜びです。

以上