Help us understand the problem. What is going on with this article?

Nuxt.js で TypeScript を使って一通り試したら色々ハマった件

はじめに

これは Nuxt.js で TypeScript を使って一通り試そうとしたら、
色々なところでつまずいてしまったのでメモに残すお話です。

また、この記事は Nuxt.js Advent Calendar 2019 21日目の記事です。

今までに Vue.js の経験はありますが Nuxt.js は初めて触ります。

nuxt-community/starter-templatecreate-nuxt-app の違いもよくわからない状態でした。
まぁ、、、公式を見ると create-nuxt-app を使う手順になっており、nuxt-community/starter-template は、deprecated になっていたので、今は create-nuxt-app を使うんだろう。と思っている程度です。

作業環境の情報

ターミナル
$ npm -v
6.13.2

$ node -v
v12.13.1

$ yarn -v
1.21.0

$ create-nuxt-app -v
create-nuxt-app/2.12.0 darwin-x64 node-v12.13.1

Nuxt(SPA) プロジェクト作成

この辺はささーっと。

ターミナル
$ create-nuxt-app tutorial

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in tutorial
? Project name tutorial
? Project description My super-duper Nuxt.js project
? Author name chibi929
? Choose the package manager Yarn
? Choose UI framework Buefy
? Choose custom server framework Express
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint, Prettier
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
yarn run v1.21.0
$ eslint --ext .js,.vue --ignore-path .gitignore . --fix
✨  Done in 3.89s.

🎉  Successfully created project tutorial

  To get started:

    cd tutorial
    yarn dev

  To build & start for production:

    cd tutorial
    yarn build
    yarn start

  To test:

    cd tutorial
    yarn test
  • UI framework: なんとなく Buefy 選択
  • Server framework: SPA だけどなんとなく Express を選択
  • linting tool: Lint、Formatter が欲しいので ESLint と Prettier 両方を選択

起動してみる

ターミナル
$ cd tutorial
$ yarn dev
yarn run v1.21.0
$ cross-env NODE_ENV=development nodemon server/index.js --watch server
[nodemon] 1.19.4
[nodemon] to restart at any time, enter `rs`
[nodemon] watching dir(s): server/**/*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server/index.js`
ℹ Preparing project for development                                                                                                                                                                                                                                         17:33:31
ℹ Initial build may take a while                                                                                                                                                                                                                                            17:33:31
✔ Builder initialized                                                                                                                                                                                                                                                       17:33:31
✔ Nuxt files generated                                                                                                                                                                                                                                                      17:33:31

✔ Client
  Compiled successfully in 7.14s

ℹ Waiting for file changes                                                                                                                                                                                                                                                  17:33:39

 READY  Server listening on http://localhost:3000     

ここまでは大丈夫。
Vue CLI 3 以降と違って、CLI で TypeScript 用のプロジェクトが作れないのが辛いだけ。

TypeScript 対応してみよう

https://typescript.nuxtjs.org/ に記載されている通りに進めるだけ。
しかし、所々で自分はハマってしまったので合わせてメモ。

Introduction

Introduction ページ を見てみると以下のパッケージが必要そう。

  • @nuxt/types
    • これは下2つに含まれているっぽいので敢えてインストールする必要はなさそうだ。
  • @nuxt/typescript-build
  • @nuxt/typescript-runtime

Introduction

Setup

Setup ページ では @nuxt/typescript-build のインストール・設定を行う。
やっぱり Introduction に記載されていた @nuxt/types は気にしなくて良さそう。
手順通りに以下を行う。

  • Installation
  • Configuration
    • nuxt.config.js
    • tsconfig.json
    • vue-shim.d.ts

メモ: Configuration - nuxt.config.js

// nuxt.config.js
export default {
buildModules: ['@nuxt/typescript-build']
}

と、記載されていたので buildModules を上記のように変更するのかと思ったが、
上書きではなくて追加するように修正したので、一応メモ。

nuxt.config.js
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
+   '@nuxt/typescript-build'
  ],

動作確認のためもう一度 yarn dev してみる。
特に問題は出ないはず...

Runtime (optional)

TypeScript runtime is needed for files not compiled by Webpack, such as nuxt.config file, local modules and serverMiddlewares.

ということなので nuxt.config.js を TS 化するときに必要そう。
Introduction に記載されていた @nuxt/typescript-runtime は Optional だったということか...
このページは、Optional になっていたので一旦無視する。 (後でハマることになるとは...)

Lint

Lint ページ では、
Introduction では未登場のモジュール( @nuxtjs/eslint-config-typescript )のインストールと設定を行う。
手順は Setup と大体同じで以下を行う。

  • Installation
  • Configuration
    • .eslintrc.js
    • package.json

メモ: Configuration - .eslintrc.js

WARNING

As it will make ESlint use a TypeScript parser ( @typescript-eslint/parser ), please ensure parserOptions.parser option is not overriden either by you or by another configuration you're extending.

If you were using babel-eslint as parser, just remove it from your .eslintrc.js and your dependencies.

という WARNING が記載されていた。
parserOptions.parser を見てみると babel-eslint が記載されていた。
消さないとダメらしい。一応以下のように修正する。

.eslintrc.js
- parserOptions: {
-   parser: 'babel-eslint'
- },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
+   '@nuxtjs/eslint-config-typescript'
  ],

メモ: Configuration - package.json

"lint": "eslint --ext .ts,.js,.vue ."

と、記載されていたが、
.ts だけを追加記載する形にした。

package.json
  "scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate",
-   "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
+   "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
    "test": "jest"
  },

とりあえず Lint を実行しておこう。
その結果がこれである。

ターミナル
$ yarn lint
yarn run v1.21.0
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore .

/tmp/tutorial/layouts/default.vue
  10:11  warning  Disallow self-closing on HTML void elements (<img/>)  vue/html-self-closing
  44:7   error    Missing space before function parentheses             space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error  Missing space before function parentheses                                 space-before-function-paren
  60:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 6 problems (5 errors, 1 warning)
  3 errors and 1 warning potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

5 errors, 1 warning...

激闘の ESLint と Prettier

これからがほんとうの地獄だ...

--fix をとりあえず付けてやってみれば少しは改善しそう...的なことが書いてあるからやってみる。

--fix 1回目

ターミナル
$ yarn lint --fix
yarn run v1.21.0
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix

/tmp/tutorial/layouts/default.vue
  10:63  warning  Expected no space before '>', but found    vue/html-closing-bracket-spacing
  10:64  error    Insert `/`                                 prettier/prettier
  44:7   error    Missing space before function parentheses  space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error  Missing space before function parentheses                                 space-before-function-paren
  60:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 7 problems (6 errors, 1 warning)
  4 errors and 1 warning potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

6 errors, 1 warning...
増えたやん...

--fix 2回目

ターミナル
$ yarn lint --fix
yarn run v1.21.0
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix

/tmp/tutorial/layouts/default.vue
  10:63  error  Insert `·/`                                prettier/prettier
  44:7   error  Missing space before function parentheses  space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error  Missing space before function parentheses                                 space-before-function-paren
  60:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 6 problems (6 errors, 0 warnings)
  4 errors and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

6 errors, 0 warnings...
改善する気配がない...

--fix 3回目

ターミナル
$ yarn lint --fix
yarn run v1.21.0
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore . --fix

/tmp/tutorial/layouts/default.vue
  10:11  warning  Disallow self-closing on HTML void elements (<img/>)  vue/html-self-closing
  44:7   error    Missing space before function parentheses             space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error  Missing space before function parentheses                                 space-before-function-paren
  60:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 6 problems (5 errors, 1 warning)
  3 errors and 1 warning potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Lint Error の内容まで振り出しに戻る...

何も考えずに --fix しただけなのでちゃんと見てみる

初めは ESLint と Prettier がバッティングしていることに気づかず苦しみ、
--fix した時の diff などを見ながら何度も実行して苦しみ...

一旦冷静になってエラーの1つだけに着目してみると...

ターミナル
# 初回
# ESLint さんは self-closing を許していないっぽい
10:11  warning  Disallow self-closing on HTML void elements (<img/>)  vue/html-self-closing

# 1回目の --fix 後
## --fix 効果で self-closing の / が消滅したけどスペースが残ったっぽくて怒ってる ESLint さん
10:63  warning  Expected no space before '>', but found    vue/html-closing-bracket-spacing
## 一方、Prettier さんは self-closing の / を入れろよ!と怒っていらっしゃる
10:64  error    Insert `/`                                 prettier/prettier

# 2回目の --fix 後
## --fix 効果で残ってたスペースが消えたっぽくて ESLint さんの怒りが静まる
## 一方、'スペース' と / を入れろよ!と激怒する Prettier さん
10:63  error  Insert `·/`                                prettier/prettier

# 3回目の --fix 後
## Prettier さんの言い分が通ったのかスペースと / が復活し、振り出しに戻る
10:11  warning  Disallow self-closing on HTML void elements (<img/>)  vue/html-self-closing

とりあえず、自分のスタイルにルールを設定すれば良いんじゃね?
と、考え直す。

vue/html-self-closing

Enforce self-closing style
(self-closing style を強制する)

https://eslint.vuejs.org/rules/html-self-closing.html
self-closing は許容したいので、ESLint さんの設定を変えよう。

html.void ("never" by default) ... The style of well-known HTML void elements.

この設定が良くなさそうだな。

.eslintrc.js
  // add your custom rules here
  rules: {
-   'nuxt/no-cjs-in-config': 'off'
+   'nuxt/no-cjs-in-config': 'off',
+   'vue/html-self-closing': ['error', {
+     'html': {
+       'void': 'always'
+     }
+   }]
  }


折りたたみ (Lint 実行ログ)
ターミナル
$ yarn lint
yarn run v1.19.2
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore .

/tmp/tutorial/layouts/default.vue
  44:7  error  Missing space before function parentheses  space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error  Missing space before function parentheses                                 space-before-function-paren
  60:12  error  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  error  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 5 problems (5 errors, 0 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.


5 errors, 0 warnings
1個改善!

@typescript-eslint/no-unused-vars

Disallow unused variables
(未使用の変数を許可しない)

https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md
変数が未使用なだけで怒るのは、業務でもない限りちょっと怒りすぎだろう。
Warning くらいに変えておこう。

.eslintrc.js
  rules: {
    'nuxt/no-cjs-in-config': 'off',
    'vue/html-self-closing': ['error', {
      'html': {
        'void': 'always'
      }
-   }]
+   }],
+   'no-unused-vars': 'off',
+   '@typescript-eslint/no-unused-vars': 'warn'
  }


折りたたみ (Lint 実行ログ)
ターミナル
$ yarn lint
yarn run v1.19.2
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore .

/tmp/tutorial/layouts/default.vue
  44:7  error  Missing space before function parentheses  space-before-function-paren

/tmp/tutorial/nuxt.config.js
  60:11  error    Missing space before function parentheses                                 space-before-function-paren
  60:12  warning  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  warning  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

/tmp/tutorial/server/index.js
  10:21  error  Missing space before function parentheses  space-before-function-paren

✖ 5 problems (3 errors, 2 warnings)
  3 errors and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.


3 errors, 2 warnings
よしよし。2つ warning 化。

space-before-function-paren

Require or disallow a space before function parenthesis
(関数の括弧の前にスペースを必要とするか、許可しない)

https://eslint.org/docs/rules/space-before-function-paren

  • anonymous is for anonymous function expressions (e.g. function () {}).
  • named is for named function expressions (e.g. function foo () {}).
  • asyncArrow is for async arrow function expressions (e.g. async () => {}).

自分のスタイルでは以下のようなルールにすれば良いかな。。。

.eslintrc.js
    // note you must disable the base rule as it can report incorrect errors
    'no-unused-vars': 'off',
-   '@typescript-eslint/no-unused-vars': 'warn'
+   '@typescript-eslint/no-unused-vars': 'warn',
+   'space-before-function-paren': ['error', {
+     'anonymous': 'never',
+     'named': 'never',
+     'asyncArrow': 'always'
+   }]
  }


折りたたみ (Lint 実行ログ)
ターミナル
$ yarn lint
yarn run v1.19.2
$ eslint --ext .ts,.js,.vue --ignore-path .gitignore .

/tmp/tutorial/nuxt.config.js
  60:12  warning  'config' is defined but never used. Allowed unused args must match /^_/u  @typescript-eslint/no-unused-vars
  60:20  warning  'ctx' is defined but never used. Allowed unused args must match /^_/u     @typescript-eslint/no-unused-vars

✖ 2 problems (0 errors, 2 warnings)

Done in 3.74s.


自分で設定した warning (no-unused-vars) 以外、消えた!

TypeScript で記述してみよう

いよいよ TypeScript 編突入である。
型がないと怖くて生きていけない!!

https://typescript.nuxtjs.org/cookbook/components/ を参考に...
元々 Class API スタイルを使おうとしてたが、全部やってみた!

Options API スタイル

components/Options.vue
<template>
  <div>
    Name: {{ fullName }}
    Message: {{ message }}
  </div>
</template>

<script lang="ts">
import Vue, { PropOptions } from 'vue'

interface User {
  firstName: string
  lastName: number
}

export default Vue.extend({
  name: 'OptionsAPIComponent',

  props: {
    user: {
      type: Object,
      required: true
    } as PropOptions<User>
  },

  data() {
    return {
      message: 'This is a message'
    }
  },

  computed: {
    fullName(): string {
      return `${this.user.firstName} ${this.user.lastName}`
    }
  }
})
</script>
pages/index.vue
  <template>
      ~略~
+     <options :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
    </section>
  </template>

  <script>
  import Card from '~/components/Card'
+ import Options from '~/components/Options'

  export default {
    name: 'HomePage',

    components: {
      Card,
+     Options
    }
  }
  </script>

(こちらはあえてそのまま JavaScript スタイルで...)

メモ: ESLint/Prettier 連合との再戦

yarn dev を実行したところエラーが...
Prettier め...

ターミナル
/tmp/tutorial/components/Options.vue
  2:8  error  Replace `⏎····Name:·{{·fullName·}}⏎····Message:·{{·message·}}⏎··` with `Name:·{{·fullName·}}·Message:·{{·message·}}`  prettier/prettier

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

面倒なのでまたもやとりあえず --fix

components/Options.vue
  <template>
-   <div>
-     Name: {{ fullName }}
-     Message: {{ message }}
-   </div>
+   <div>Name: {{ fullName }} Message: {{ message }}</div>
  </template>

これでエラーなくなるならとりあえずはいいか。
そのうちフォーマッターのスタイルは直したいけど。

Options API スタイル

反映された!!

Class API スタイル

Using vue-class-component through vue-property-decorator

Composition API は見覚えがないが、
Class API 見覚えがあるので、先にこっちから...
Vue CLI3 で作った TypeScript でも Class-Component Style で触ったデコレーター!

ターミナル
$ yarn add -D vue-property-decorator
components/Class.vue
<template>
  <div>Name: {{ fullName }} Message: {{ message }}</div>
</template>

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

interface User {
  firstName: string
  lastName: number
}

@Component
export default class ClassAPIComponent extends Vue {
  @Prop({ type: Object, required: true }) readonly user!: User

  message: string = 'This is a message'

  get fullName(): string {
    return `${this.user.firstName} ${this.user.lastName}`
  }
}
</script>
pages/index.vue
  <template>
      ~略~
      <options :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
+     <ClassAPIComponent :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
    </section>
  </template>

  <script>
  import Card from '~/components/Card'
  import Options from '~/components/Options'
+ import ClassAPIComponent from '~/components/Class'

  export default {
    name: 'HomePage',

    components: {
      Card,
      Options,
+     ClassAPIComponent
    }
  }
  </script>

Class API スタイル

2行目の文字列がそれだ。上手くいった!

メモ: experimentalDecorators

但し、以下のエラーが出ているので tsconfig.json に追加しておこう

ターミナル
 ERROR  ERROR in /tmp/tutorial/components/Class.vue(14,22):                                                                                                                                                  nuxt:typescript 06:59:04
14:22 Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning.
tsconfig.json
  {
    "compilerOptions": {
      ~略~
+     "experimentalDecorators": true,
      ~略~
  }

Composition API スタイル

Using @vue/composition-api plugin

これは、知らない記法だった。
普段 Vue では前述の Class API スタイルを使っているし使う機会はなさそう...

ターミナル
yarn add @vue/composition-api
plugin/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)
nuxt.config.js
  /*
   ** Plugins to load before mounting the App
   */
- plugins: [],
+ plugins: ['@/plugins/composition-api'],
components/Composition.vue
<template>
  <div>Name: {{ fullName }} Message: {{ message }}</div>
</template>

<script lang="ts">
import { createComponent, computed, ref } from '@vue/composition-api'

interface User {
  firstName: string
  lastName: number
}

export default createComponent({
  props: {
    user: {
      type: Object as () => User,
      required: true
    }
  },

  setup({ user }) {
    const fullName = computed(() => `${user.firstName} ${user.lastName}`)
    const message = ref('This is a message')

    return {
      fullName,
      message
    }
  }
})
</script>
pages/index.vue
      <options :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
      <ClassAPIComponent :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
+     <Composition :user="{ firstName: 'Chibi', lastName: 'Kinoko' }" />
    </section>
  </template>

  <script>
  import Card from '~/components/Card'
  import Options from '~/components/Options'
  import ClassAPIComponent from '~/components/Class'
+ import Composition from '~/components/Composition'

  export default {
    name: 'HomePage',

    components: {
      Card,
      Options,
      ClassAPIComponent,
+     Composition
    }
  }
  </script>

Composition スタイル

SPA だけど Express を使ってみよう

server/index.js
 const express = require('express')
 const consola = require('consola')
 const { Nuxt, Builder } = require('nuxt')
 const app = express()

 // Import and Set Nuxt.js options
 const config = require('../nuxt.config.js')
 config.dev = process.env.NODE_ENV !== 'production'

 async function start() {
+  app.get('/test', (req, res, next) => {
+    const param = { test: 'Hello World!' }
+    res.send(param)
+  })

   // Init Nuxt.js
   const nuxt = new Nuxt(config)
   ~略~
 }
 start()

yarn dev 後に動作確認

ターミナル
$ curl http://localhost:3000/test
{"test":"Hello World!"}

メモ: 記述位置は大事らしい

以下の位置に GET API を記述したところ上手くいかなかった

server/index.js
 ~略~

 async function start() {
   ~略~

   // Give nuxt middleware to express
   app.use(nuxt.render)
+  app.get('/test', (req, res, next) => {
+    const param = { test: 'Hello World!' }
+    res.send(param)
+  })

   ~略~
 }
 start()
ターミナル
# 失敗バージョン
##############################

$ curl http://localhost:3000/test
<!doctype html>
<html >
~略~

メモ: GET / もダメらしい

GET / を登録してしまうと、ページへのアクセスができなくなる。
そりゃそうか。

メモ: this.$axios が使えない

component/Class.vue
    ~略~
+   mounted() {
+     this.$axios
+       .get('/test')
+       .then((res) => {
+         console.log(res)
+       })
+       .catch((err) => {
+         console.log(err)
+       })
+   }
  }
  </script>
ターミナル
 ERROR  ERROR in /tmp/tutorial/components/Class.vue(25,10):                                                                                                                                                  nuxt:typescript 20:58:26
25:10 Property '$axios' does not exist on type 'ClassAPIComponent'.
    23 |   mounted() {
    24 |     console.log('mounted')
  > 25 |     this.$axios
       |          ^
    26 |       .get('/test')
    27 |       .then((res) => {
    28 |         console.log(res)

tsconfig.json に追記が必要

tsconfig.json
  {
    "compilerOptions": {
      ~略~
      "types": [
        "@types/node", 
        "@nuxt/types", 
+       "@nuxtjs/axios"
      ]
    },

Store を TypeScript で書いてみよう

https://typescript.nuxtjs.org/cookbook/store.html

とりあえず Vanilla JS の方で良いや...と...
ちなみにここも自分の中では激ハマりポイントである。
上記のページを参考に store/index.ts を準備できたら、
任意の Component の mounted() でログを出してみる。

mounted() {
  console.log(this.$store.state.counter)
}
ターミナル
client.js?06a0:76 TypeError: Cannot read property 'state' of undefined
    at VueComponent.mounted (Class.vue?e831:34)

しかし、どうも this.$store が undefined みたい...

次に、Nuxt.js のページを参考に...
普通の Vuex... というわけではないが Nuxt Typescript よりは普通に近い形で、型に怒られたら any でにげる。

store/index.ts
export const state = () => ({
  counter: 0
})

export const mutations = {
  increment(state: any) {
    state.counter++
  }
}
ターミナル
client.js?06a0:76 TypeError: Cannot read property 'state' of undefined
    at VueComponent.mounted (Class.vue?e831:34)

しかし、やっぱり 'state' of undefined
どうにも this.$store が未定義状態だなー...

試しに JS ファイルにしてみると...

store/index.js
export const state = () => ({
  counter: 0
})

export const mutations = {
  increment(state) {
    state.counter++
  }
}

動くやん...?

Runtime 編突入

でもやっぱり TypeScript で書きたい!
TypeScript のセットアップの中で、ひとつだけ js 系を ts 系に置き換えるためのものなのに
やっていないものを思い出す。そう、オプショナルだったRuntime...

TypeScript runtime is needed for files not compiled by Webpack, such as nuxt.config file, local modules and serverMiddlewares.

と書いてあったものなので、
Store 系にも影響するかはわからない。けどやってみよう。そう思った。

そして、もう一度Runtimeページでセットアップ方法を確認する。
内容は Setup とほとんど同で以下の通りである。

  • Installation
  • Usage
    • package.json

モジュールのインストールは特に問題ない。ひとつだけ注意するとしたら

TIP

Note that this package is installed as dependency and not devDependency like @nuxt/typescript-build, cause @nuxt/typescript-runtime is needed for production

devDependencies には入れちゃいけない。

ターミナル
yarn add @nuxt/typescript-runtime

package.json の書き換えは結構困った。
ドキュメントによると nuxt-ts コマンドに書きかえろよ。と...

package.json
{
  ~略~
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "generate": "nuxt-ts generate",
    "start": "nuxt-ts start"
  },
  ~略~
}

しかし、自分の package.json は buildgenerate は良いが...
devstart には cross-env なるものが使われている...
果たして以下のように全部書き換えて良いものなのか?

package.json
  {
    ~略~
    "scripts": {
-     "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+     "dev": "nuxt-ts",
-     "build": "nuxt build",
+     "build": "nuxt-ts build",
-     "start": "cross-env NODE_ENV=production node server/index.js",
+     "start": "nuxt-ts start"
-     "generate": "nuxt generate"
+     "generate": "nuxt-ts generate"
    },
    ~略~
  }

とりあえず、なんか怖いと思ったので devstart は変えないことにした。
cross-env を使ったまま server/index.js を起動する形のまま置いておく。

package.json
  {
    ~略~
    "scripts": {
      "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
-     "build": "nuxt build",
+     "build": "nuxt-ts build",
      "start": "cross-env NODE_ENV=production node server/index.js",
-     "generate": "nuxt generate"
+     "generate": "nuxt-ts generate"
  },
  ~略~
}

とりあえず yarn dev を実行しても問題なさそう。
いよいよ store/index.js を TypeScript に置き換えてみる
と、言っても any の型を付けるだけである

store/index.ts
 export const state = () => ({
   counter: 0
 })

 export const mutations = {
-  increment(state) {
+  increment(state: any) {
     state.counter++
   }
 }

yarn dev を実行してみると、どうやら this.$store はまだ読み込めていない。
しかたなく nuxt-ts を実行してみると...
this.$store が読み込め、 counter の値が表示できた。

さて、ここで問題なのは
nuxt-ts で動かした時に Express で作成した API はどうなるのか?
server/index.jsnuxt コマンドは何をやっているのかを追ってみることにした。

server/index.js が何をしているのか?

server/index.js は短いコードなのですぐ読める。

Nuxt インスタンスを生成して Builder.build を実行する。
だったり Nuxt.ready を実行する。
ついでに、Express の API を使う感じだ。

nuxt コマンドが何をしているのか?

@nuxt/cli を追ってみると色々汎用的にガチャガチャやってるが、
結局は cli-devBuilder.build したり Nuxt.ready を実行しているだけである。

つまり答えは、 server/index.js を動かしてないから、
単純に Express が立っていない状態になるのである。たぶん!!

ターミナル
$ curl http://localhost:3000/test
<!doctype html>
<html >

安心して nuxt-ts に書き換えよう

package.json
 {
   ~略~
   "scripts": {
-    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
+    "dev": "nuxt-ts",
     "build": "nuxt-ts build",
-    "start": "cross-env NODE_ENV=production node server/index.js",
+    "start": "nuxt-ts start"
     "generate": "nuxt-ts generate"
   },
   ~略~
 }

こうして、Express を犠牲にして、
Store を TypeScript で書くことができた。

ServerMiddleware を使ってみよう

さて、そしたら次に気になってしまうのは
Nuxt.js で TypeScript を使ったとき、Express はどうやって使うのか。

Nuxt.js v2.9にTypeScriptとExpress.jsを対応してみた
API: serverMiddleware プロパティ

を参考にした。

nuxt.config.jsserverMiddleware の項目を追加すれば良いらしい。
まずは server/index.js を書き換えよう。

server/index.ts
import express from 'express'
import bodyParser from 'body-parser'
const app = express()

app.use(bodyParser.json())
app.get('/test', (req, res, next) => {
  const param = { test: 'Hello World!' }
  res.send(param)
})

export default app

Nuxt 系の処理が全部不要(nuxt-ts コマンドに任せた)なので、
ごっそり消して Express の処理だけ書きます。
Express のインスタンスは export しておきます。

次に nuxt.config.jsserverMiddleware の項目を追加します。

nuxt.config.js
 {
   ~略~
+  serverMiddleware: [
+    { path: '/api', handler: '@/server/index.ts' }
+  ]
 }

ts ファイルが直接設定できるんだなー。
きっと Runtime 編を突破したおかげですね!

ターミナル
$ curl http://localhost:3000/api/test
{"test":"Hello World!"}

実行してみると普通に叩けました。

Middleware を使ってみよう

API: serverMiddleware プロパティ を参考してたら Middleware も目に入ってしまった。

API: middleware プロパティ
ルーティング - ミドルウェア

どうやら、ルーティング時に呼び出すことができるものらしい。
sereverMiddleware 同様、 nuxt.config.jsソースコード をちょちょっと触るだけみたい。

ルートが変更された時に呼ばれる Middleware

middleware/hoge.ts
export default async function() {
  console.log('Middleware: hoge')
}
nuxt.config.js
   serverMiddleware: [
     { path: '/api', handler: '@/server/index.ts' }
   ],
+  router: {
+    middleware: 'hoge'
+  }
 }

これだけで、ルーティング変更時に毎度 console.log('Middleware: hoge') が実行されるのである。

特定のルートに変更された時に呼ばれる Middleware

前述の nuxt.config.js に書く Middleware は、
全てのルートにおいて、呼び出し時に動く Middleware である。
特定のページに行った時のみ実行したい場合はどうするか。

以下の通りである。
とりあえず hoge.ts を使うとわかりづらくなってしまうので foo.ts を用意する。

middleware/foo.ts
export default async function() {
  console.log('Middleware: foo')
}

次にルーティング対象の pages 以下に手を入れる

pages/inspire.vue
 ~略~
 </template>
+ <script lang="ts">
+ import { Vue, Component, Prop } from 'vue-property-decorator'
+ 
+ @Component({
+   middleware: 'foo'
+ })
+ export default class Inspire extends Vue {}
+ </script>

これで、inspire.vue ルーティングを変更したときに foo が呼び出される。

これで stats ミドルウェアはすべてのルート変更時に呼び出されるようになります。
同様に、特定のレイアウトもしくはページ内にもミドルウェアを追加することができます:

注意点としてはルーティング - ミドルウェアに記載されている通り、
layouts もしくは pages のものしか使えなそう。
ここで試している Class API スタイルの Component に middleware を追加してみたけど、
foo は実行されなかった。

Store にデコレーターを使ってみよう

さて Store は最初 Vanilla JS を使ってしまいましたが、
vuex-module-decorator を使った記述が推されているっぽい。

まずはモジュールをインストールしておく

ターミナル
yarn add -D vuex-module-decorators

そして Store | Nuxt TypeScriptModule re-use, use with NuxtJS を参考に Store を書き換えてみることにする

store/example.ts
import { Module, VuexModule, Mutation } from 'vuex-module-decorators'

@Module({ stateFactory: true })
class MyModule extends VuexModule {
  counter = 0

  @Mutation
  increment() {
    this.counter++
  }
}
utils/store-accessor.ts
import { Store } from 'vuex'
import { getModule } from 'vuex-module-decorators'
import example from '~/store/example'

let exampleStore: example

function initialiseStores(store: Store<any>): void {
  exampleStore = getModule(example, store)
}

export {
  initialiseStores,
  exampleStore,
}
store/index.ts
import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'

メモ: example が見つからない

ターミナル
ERROR in /tmp/tutorial/utils/store-accessor.ts(3,8):                                                                                                                                                 nuxt:typescript 23:40:46
3:8 Module '"/tmp/tutorial/store/example"' has no default export.
    1 | import { Store } from 'vuex'
    2 | import { getModule } from 'vuex-module-decorators'
  > 3 | import example from '~/store/example'

ドキュメント通りにやるとエラーになる。
store/example.ts は export してないから当然か。

store/example.ts
  @Module({ stateFactory: true })
- class MyModule extends VuexModule {
+ export default class MyModule extends VuexModule {
    counter = 0

メモ: module name が設定されていない

意気揚々と yarn dev !!
http://localhost:3000 をブラウザで開いてみると
なんと真っ白!

Developer Tool を開いて確認すると

Error: ERR_GET_MODULE_NAME : Could not get module accessor.
      Make sure your module has name, we can't make accessors for unnamed modules
      i.e. @Module({ name: 'something' })
    at getModuleName (index.js?6fc5:24)
    at getModule (index.js?6fc5:41)
    at initialiseStores (store-accessor.ts?8dac:8)
    at initializer (index.ts?598a:3)
    at eval (vuex.esm.js?2f62:353)
    at Array.forEach (<anonymous>)
    at new Store (vuex.esm.js?2f62:353)
    at createStore (store.js?6c6b:42)
    at createApp$ (index.js?f26e:51)
    at tryCatch (runtime.js?96cf:45)

@Module({ name: 'something' }) のように設定してくれ。とのこと。
vuex-module-decorator 使うと名前空間付きモジュールしか設定できないのかなー?

store/example.ts
- @Module({ stateFactory: true })
+ @Module({
+   stateFactory: true,
+   namespaced: true,
+   name: 'example'
+ })
  export default class MyModule extends VuexModule {
    counter = 0

のように書き換えて再度実行!
任意の Component では以下のようなログ出力を行なっています。

import { exampleStore } from '~/store'

mounted() {
  console.log(this.$store.state.counter)
  console.log(this.$store.state.example.counter)
  console.log(exampleStore.counter)
}

// => undefined
// => 0
// => 0

やっぱり名前空間付きモジュールになっている。
ちなみに store/example.tsnamespaced: truenamespaced: false に書き換えてもダメでした。
vuex-module-decorator を使うとルートモジュールは作れないのかな?

ひとまず store/index.ts を以下のように書き換えたら
ルートモジュールの store/index.ts 及び、
名前空間付きモジュールの store/example.ts の両方が使えるようになりました。

store/index.ts
  import { Store } from 'vuex'
  import { initialiseStores } from '~/utils/store-accessor'
  const initializer = (store: Store<any>) => initialiseStores(store)
  export const plugins = [initializer]
  export * from '~/utils/store-accessor'
+ 
+ export const state = () => ({
+   counter: 0
+ })
+ 
+ export const mutations = {
+   increment(state: any) {
+     state.counter++
+   }
+ }

まとめ

これだけ一通りやって、たくさん躓き、
後輩の雑用を軽減するアプリを作成しました。

ちなみに、この Nuxt プロジェクトは、最初に作成している通り、
SPA なので、最終的には nuxt-ts generate を実行して静的ホスティングをしております。
つまり、無駄に使った serverMiddleware は動いていません。
まぁ、動いても本記事に書いたレベルの {"test":"Hello World!"} 程度の戻り値が出るだけなんですけどね。

しかし、実験コードが入れっぱなしなので、
Developer Tool を開くと以下のエラーが出ているオマケ付きです。
まぁ、後輩が使うだけだし、悪影響ないし、ヨシ

http://example.com/api/test net::ERR_CONNECTION_REFUSED

ということで、

  • 一番ハマるのは Linter + Prettier
  • Runtime は Optional だけど TypeScript を使うなら入れておけ
  • $axios もそうだが、時々型定義でハマる (本記事には書いてないけど $buefy も型定義エラーになった)
  • 何かやろうとすると多少なりとも動かない!

[やってないこと]

  • sass
mashandroom
会社外活動・コミュニティとして、ハッカソンや各種イベントのボランティアなどに参加。ただただ自分たちが楽しみ、結果周りの人が喜んでくれるようなアウトプットを目指しています。創造せよ!頭にキノコが生えるまで
https://mashandroom.org
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした