Ikkyu's Tech Blog

技術系に関するブログです。

Nuxt.js(TypeScript) × Vuetify × Storybookを使用して開発環境を再構築してみる

以前、Nuxtの開発環境構築の記事を書きました。

ikkyu.hateblo.jp

その後、Nuxt.jsやStorybookのバージョンが上がり設定方法が変わったので、改めて開発環境を記事にしてみようと思いました。

動作環境 - maxOS 10.15.4 - VSCode(Veturをインストール済み) - yarnインストール済み

構成は以下の通りです。(変更はありません)

  • Nuxt.js
  • TypeScript
  • Vuetify
  • ESLint
  • Prettier
  • Jest
  • Storybook

今回はnpxでなくyarnを使います。

プロジェクトを作成

yarn create nuxt-app <アプリケーション名>

対話形式で答えていきます。 このようにしました。

$ yarn create nuxt-app nuxt-typesciprt-starter
yarn create v1.21.1
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-nuxt-app@2.15.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in .
? Project name nuxt-typesciprt-starter
? Project description My world-class Nuxt.js project
? Author name ikkyu-3
? Choose programming language TypeScript
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose the runtime for TypeScript @nuxt/typescript-runtime
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)ESLint, Prettier, Lint staged files, St
yleLint
? Choose test framework Jest
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)

プロジェクトが作成されたら一度起動してみます。

$ cd nuxt-typescript-starter
$ yarn dev

http://localhost:3000/にアクセスしてページが表示されるか確認します。

f:id:Ikkyu:20200403003859p:plain
アクセスしたページ

簡単ですね🎉

TypeScript導入

プロジェクト作成時に使用言語をTypeScriptにしましたが、開発に必要なライブラリや設定を行います。

vue-property-decoratorを使用します。

$ yarn add vue-property-decorator

TypeScriptでvueファイルを読み込めるように設定

プロジェクトのrootディレクトリにshims-vue.d.tsを作成します。

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

vueファイルを変更

components/Logo.vueにscriptタグを追加します。

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Logo extends Vue {}
</script>

次にcomponents/VuetifyLogo.vueにscriptタグを追加します。

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class VuetifyLogo extends Vue {}
</script>

次にpages/index.vueのscriptタグを変更します。

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Logo from '~/components/Logo.vue'
import VuetifyLogo from '~/components/VuetifyLogo.vue'

@Component({
  components: {
    Logo,
    VuetifyLogo
  }
})
export default class Index extends Vue {}
</script>

package.jsonを変更

scriptsのlintとlint-stagedを変更します。

{
  "scripts": {
    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .", // tsファイルを対象に追加します
    ...
  },
  ...
  "lint-staged": {
    "*.{ts,js,vue}": "yarn lint", // tsファイルを対象に追加します
    ...
  }
}

JestにTypeScriptを導入

yarn add -D @types/jest

次にtsconfig.jsonを変更します。

{
  ...,
  "allowJs": true, // 削除
  ...,
  "types": [
    ...,
    "@types/jest"
  ]
}

allowJs1がtrueだとCannot write file ... because it would overwrite input file.とエラーとなりテストが失敗するため削除します。

test/Logo.spec.jstest/Logo.spec.tsに変更し、テストを実行してみます。

$ yarn test
yarn run v1.21.1
$ jest
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with 
ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.
0 <26.0.0). Please do not report issues in ts-jest if you are using unsupported versions.
 PASS  test/Logo.spec.ts
  Logo
    ✓ is a Vue instance (8ms)

------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    23.08 |      100 |       25 |       25 |                   |
 components       |       50 |      100 |       50 |       50 |                   |
  Logo.vue        |      100 |      100 |      100 |      100 |                   |
  VuetifyLogo.vue |        0 |      100 |        0 |        0 |             1,6,9 |
 pages            |        0 |      100 |        0 |        0 |                   |
  index.vue       |        0 |      100 |        0 |        0 |     1,66,67,68,76 |
  inspire.vue     |        0 |      100 |        0 |        0 |                 1 |
------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.034s
Ran all test suites.
✨  Done in 3.98s.

テストが通りました✌️

Vuetifyのコンポーネントを使用したvueファイルをテストする場合の設定

jestでTypeScriptを使用できるようになりましたが、vuetifyのコンポーネントをjestで使用できるようにユニットテスト — Vuetify.jsをみながら設定します。

test/setup.jsを作成します。

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

次に、jest.config.jsに設定を追加します。

module.exports = {
  setupFilesAfterEnv: ['./test/setup.js'], // 追加
  ...
}

試しにtest/index.spec.tsを作成します。

import Vuetify from 'vuetify'
import { mount, createLocalVue, RouterLinkStub } from '@vue/test-utils'
import Index from '@/pages/index.vue'

const localVue = createLocalVue()

describe('Index', () => {
  let vuetify: typeof Vuetify

  beforeEach(() => {
    vuetify = new Vuetify()
  })

  test('is a Vue instance', () => {
    const wrapper = mount(Index, {
      vuetify,
      localVue,
      stubs: {
        NuxtLink: RouterLinkStub
      }
    })
    expect(wrapper.isVueInstance()).toBeTruthy()
  })
})

テストを実行してみます。

$ yarn test
yarn run v1.21.1
$ jest
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with 
ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.
0 <26.0.0). Please do not report issues in ts-jest if you are using unsupported ver
sions.
ts-jest[versions] (WARN) Version 24.9.0 of jest installed has not been tested with 
ts-jest. If you're experiencing issues, consider using a supported version (>=25.0.
0 <26.0.0). Please do not report issues in ts-jest if you are using unsupported ver
sions.
 PASS  test/Logo.spec.ts
 PASS  test/index.spec.ts
------------------|----------|----------|----------|----------|-------------------|
File              |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
------------------|----------|----------|----------|----------|-------------------|
All files         |    92.31 |      100 |       75 |    91.67 |                   |
 components       |      100 |      100 |      100 |      100 |                   |
  Logo.vue        |      100 |      100 |      100 |      100 |                   |
  VuetifyLogo.vue |      100 |      100 |      100 |      100 |                   |
 pages            |    85.71 |      100 |       50 |    83.33 |                   |
  index.vue       |      100 |      100 |      100 |      100 |                   |
  inspire.vue     |        0 |      100 |        0 |        0 |                 1 |
------------------|----------|----------|----------|----------|-------------------|

Test Suites: 2 passed, 2 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        3.503s
Ran all test suites.
✨  Done in 4.47s.

できました👍

Storybookを導入

Storybook for Vueを見ながらStorybookを導入していきます。今回も自動セットアップで行います。

$ npx -p @storybook/cli sb init --type vue

実行すると必要なパッケージがインストールされ、.storybookとstoriesというディレクトリが作成されて、その中にいくつかのファイルが作成されます。

また、package.jsonもstorybook, build-storybookというscriptsが新たに追加されています。
staticディレクトリのファイルを使用できるように、scriptsを以下のように変更します。2

{
  ...
  "scripts": {
    ...
    "storybook": "start-storybook -p 6006 -s ./static",
    ...
  },
  ...
}

一度Storybookを表示してみます。

$ yarn storybook

サーバが起動したらhttp://localhost:6006/にアクセスします。

f:id:Ikkyu:20200403155454p:plain
Storybook

表示できました👍

では次に、StorybookでVuetifyを使用できるようにします。

Vuetify対応

StorybookでVuetifyを使用するため、.storybook/preview.jsを追加します。

import { addDecorator } from '@storybook/vue'
import 'vuetify/dist/vuetify.css'

import Vue from 'vue'
import Vuetify from 'vuetify'

Vue.use(Vuetify)

Vue.component('nuxt-link', {
  props: ['to'],
  methods: {
    log() {
      action('link target')(this.to)
    }
  },
  template: '<a href="#" @click.prevent="log()"><slot>NuxtLink</slot></a>'
})

const vuetifyConfig = new Vuetify({
  icons: { iconfont: 'fa' },
  theme: { dark: false }
})

addDecorator(() => {
  return {
    vuetify: vuetifyConfig,
    template: '<v-app><div><story/></div></v-app>'
  }
})

では次に、JavaScriptで書かれたStorybookをTypeScriptに置き換えます。

TypeScript対応

Storybook用のwebpackをセットアップ

TypeScriptで書かれたstoriesファイルを読み込むために、
.storybook/main.jsを以下のように変更します。3

const path = require('path');

module.exports = {
  stories: ['../**/*.stories.ts'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
  webpackFinal: async config => {
    config.module.rules.push({
      test: /\.ts$/,
      exclude: /node_modules/,
      use: [
        {
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/],
            transpileOnly: true
          }
        }
      ]
    })
    config.resolve.extensions.push('.ts')

    const rootPath = path.resolve(__dirname, '..')
    config.resolve.alias['@'] = rootPath
    config.resolve.alias['~'] = rootPath

    return config
  }
};

これで設定は終わりです。

TypeScriptでStoryを書いてみる

components配下のLogo.vueとVuetifyLogo.vueのStoryをCSF4で書いてみます。

components/index.stories.tsを作成します。
(表示したいコンポーネントと同じディレクトリにStroyを作成するとメンテナンスが簡単になります。5)

※ storiesディレクトリは使用しないので、削除します。

import Logo from './Logo.vue'
import VuetifyLogo from './VuetifyLogo.vue'

export default {
  title: 'Components'
}

export const logo = () => ({
  components: { Logo },
  template: '<logo />'
})

export const vuetifyLogo = () => ({
  components: { VuetifyLogo },
  template: '<vuetify-logo />'
})

yarn storybookでstorybookを起動します。

f:id:Ikkyu:20200403180410p:plain
TypeScriptで書いたStory

できました!

今回作成したプロジェクトファイルはこちらで確認できます。

以上です。