95
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Nuxt.jsAdvent Calendar 2019

Day 6

Nuxt.js with TypeScript and Composition API

Last updated at Posted at 2019-12-05

はじめに

この記事は Nuxt.js Advent Calendar 2019 6日目の記事です。

皆さん、Composition API 使ってますか? Composition API は現在開発中で、2020年1Qにもリリース予定の Vue.js 3.0 からデフォルトで搭載されることになっている新しいコンポーネントの実装方法です。Composition API を使うことで、散らかりがちだった実装を一箇所に固めることができたり、今まで組み合わせにくかった TypeScript との連携が比較的やりやすくなったりします。Vue.js 2.x にもプラグインとして導入すれば今すぐ使い始めることができるようになっています。

この記事では、その Composition API を Nuxt.js に導入し、さらに TypeScript を使ってなるべく1型の力を借りて型安全なフロントエンドを作成する方法を紹介します。

環境構築

諸注意

今回はnpmではなくyarnを使っていきます。npm派の方は適宜コマンドを読み替えてください。また、コマンドは全てmacOS Catalinaでの実行例となっています。Linuxなどをお使いの方はパッケージマネージャーなどが出てきた際に、適切なコマンドに読み替えてください。

準備

とりあえずnodeyarnを入れておきます。エディタはVSCodeやWebStormなどお好みで。

$ brew install node yarn

プロジェクトの初期化

yarn create コマンドでざっくり作っていきます。create-nuxt-appではまだTypeScriptを使ったプロジェクトの生成には対応していないので、いったんJavaScript用のプロジェクトを作ります。各種選択肢はお好みでOKです。一応どれを選んだかはここに書いておきます。

% yarn create nuxt-app composition-sample
yarn create v1.19.2
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Installed "create-nuxt-app@2.12.0" with binaries:
      - create-nuxt-app

create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in composition-sample
? Project name composition-sample
? Project description My primo Nuxt.js project
? Author name Aruneko
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)

TypeScript対応

基本的に公式サイトのやり方に従えばOKですが、簡単に手順を記しておきます。

まずはビルド時にTypeScriptを有効化する設定です。@nuxt/typescript-buildを入れてから設定ファイルを書き換え、tsconfig.jsonを生成するところまでやっておきます。これはあくまでビルド時にしか使わないので、開発用依存パッケージとしてインストールします。

$ yarn add --dev @nuxt/typescript-build

インストールが終わったらnuxt.config.jsを書き換えます。まずbuildModules'@nuxt/typescript-build'を書き加えましょう。

nuxt.config.js
export default {
  // 前略
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxt/typescript-build',  // ここを追加
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify'
  ],
  // 後略
}

そしてこの際なのでnuxt.config.jsnuxt.config.tsにリネームして、型を付けます。default exportしているあたりをいったんnuxtConfig変数に入れて型を付け、後からエクスポートするようにします。

$ mv nuxt.config.js nuxt.config.ts
nuxt.config.ts
import { Configuration } from '@nuxt/types'
import colors from 'vuetify/es5/util/colors'

const nuxtConfig: Configuration = {
  // 中略
}

module.exports = nuxtConfig

最後に次のような内容でtsconfig.jsonをプロジェクトのトップ階層に設置すれば完了です。

tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

続いてランタイム時にもTypeScriptが有効になるようにしていきます。これは@nuxt/typescript-runtimeを導入して、package.jsonを書き換えるだけのお手軽作業です。こちらはランタイムなので、開発用依存としないように注意しましょう。

$ yarn add @nuxt/typescript-runtime

package.jsonの変更ですが、scriptsセクションを探してその中にある4箇所のnuxtコマンドを全てnuxt-tsコマンドに置き換えるだけです。

package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
}

最後にlintの設定もしましょう。せっかくESLintとPrettierをプロジェクト初期化時に有効にしてありますからね。まずは専用のESLint用定義を入れていきます。

$ yarn add -D @nuxtjs/eslint-config-typescript

インストールが終わったら.eslintrc.jsでその定義を有効化しておきます。parser を TypeScript のものに変更し、extends セクションで導入した定義を有効化してください。

.eslintrc.js
module.exports = {
  // 前略
  parserOptions: {
    // ここを変更
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
    '@nuxtjs/eslint-config-typescript'. // ここを追加
  ],
  // 後略
}

最後にpackage.jsonscriptsセクションにあるlintコマンドで.tsファイルに対してもlintが走るように対象となる拡張子を追加すれば完了です。

package.json
"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "start": "nuxt-ts start",
  "generate": "nuxt-ts generate",
  "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
}

Composition API の導入

まずは yarn でインストールしてしまいましょう。

$ yarn add @vue/composition-api

インストールできたら plugins ディレクトリの下に composition-api.ts を作って、Composition API を有効化します。

plugins/composition-api.ts
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

nuxt.config.ts でプラグインを有効化することもお忘れなく。

nuxt.config.ts
const nuxtConfig: Configuration = {
  // 前略
  plugins: ['@/plugins/composition-api'],
  // 後略
}

これで導入は完了です。

Composition API による実装の例

コンポーネントの作成

ではVuetifyを選択することによってデフォルトで生成されたコンポーネントを改造する形で、Composition API をどうやって Nuxt.js に組み込んでいくか説明していきます。

まずは layouts/default.vue を確認してみましょう。デフォルトの実装は以下のようになっています。なお下記コードでは<script>内を抜粋しています。

layouts/default.vue
export default {
  data() {
    return {
      clipped: false,
      drawer: false,
      fixed: false,
      items: [
        {
          icon: 'mdi-apps',
          title: 'Welcome',
          to: '/'
        },
        {
          icon: 'mdi-chart-bubble',
          title: 'Inspire',
          to: '/inspire'
        }
      ],
      miniVariant: false,
      right: true,
      rightDrawer: false,
      title: 'Vuetify.js'
    }
  }
}

これを Composition API + TypeScript で書き換えていきます。まず <script> タグに lang="ts" 属性を追加して <script lang="ts"> としてこの中に書かれたコードがTypeScriptであるということを明示してから、コードを書き換えていきます。まずは書き換えた後のコード全体を掲載しますが、おおよそは Composition API のサンプルコード通りとなります。

layouts/default.vue
<script lang="ts">
// (1) 必要なものをインポート
import { createComponent, ref } from '@vue/composition-api'

type Item = {
  icon: string
  title: string
  to: string
}

// (2) createComponentによるコンポーネントの作成
export default createComponent({
  setup() {
    // (3) refを使ったリアクティブ値の生成
    const clipped = ref(false)
    const drawer = ref(false)
    const fixed = ref(false)
    // (4) 型の明示
    const items = ref<Item[]>([
      {
        icon: 'mdi-apps',
        title: 'Welcome',
        to: '/'
      },
      {
        icon: 'mdi-chart-bubble',
        title: 'Inspire',
        to: '/inspire'
      }
    ])
    const miniVariant = ref(false)
    const right = ref(false)
    const rightDrawer = ref(false)
    const title = ref('Vuetify.js')

    // (5) Template内で使うものだけまとめて返す
    return {
      clipped,
      drawer,
      fixed,
      items,
      miniVariant,
      right,
      rightDrawer,
      title
    }
  }
})
</script>

コンポーネントの作成

Composition API では、(2) で行っているように createComponent 関数によってコンポーネントを作成します。これは @vue/composition-api から提供されるので、(1) の箇所で事前にインポートしておきます。さらに setup を使ってリアクティブな値の定義を行っていきます。今までは data() を使ってこれらの値を定義していましたが、Composition API では変更されていますので注意してください。

リアクティブ値

単体のリアクティブ値は ref を使って作成します。投入した初期値によって型推論が行われるため、(3) のように通常は型を明示する必要はありません。ただし、明示的に書きたい場合は (4) でやっているように <> を使えばOKです。

一方、型の明示が必須な場合もあります。例えば初期値では空配列だけれども後から値が変化するような場合や、null が入る可能性があるリアクティブ値を定義する場合には型を明示しなければなりません2。例えば以下のようにすると良いでしょう。

const userList = ref<User[]>([])
const nullableValue = ref<User | null>(null)

setup 関数の最後で <template> 内で使う値だけまとめて返してあげれば完了です。これで少なくとも <script> 内では TypeScript による型チェックが働くようになります3

Nuxt固有の拡張機能

Nuxt.js には asyncData など便利機能が色々と備わっています。ですが、Composition API と組み合わせた場合これらの機能を使うことは現時点では非常に難しいです。そこで Composition API にある代替機能を使ってこれらの機能を実装していきましょう。ある程度はそれで機能の代わりを果たすことができます。

非同期値の取得

API からデータを引っ張ってくるなど非同期な値を持ってきたいことはしばしばあると思います。ただ先にも述べたように asyncData は封印されていますので、別の手段を用います。 Composition API には watch という関数が用意されており、これを使って async をラップしてあげることでページ遷移してきたときに1回だけ呼んであげることができます。本来 watch は名前の通りリアクティブ値を監視して、変更があったときに指定した関数を動かすための関数ですが、なぜか4こういった使い方もできるようになっています。

const users = ref<User[]>([])

watch(async () => {
    users.value = await fetch('http://api.example.com/users/')
})

レイアウトやプロパティの指定

これは従来のやり方がそのまま使えます。props もOKです。createComponent の中でそれぞれに対応した Key-Value を設定してあげましょう。

import { createComponent } from '@vue/composition-api'

export default createComponent({
  layout: 'empty',
  props: {
    user: {
      type: Object,
      default: null
    }
  },
  setup() {
    // 略
  }
})

Nuxt Axios / Auth Moduleとの連携

これも問題なくできますが、少し準備が必要です。まず型付けを正しく行うために、nuxt.config.ts を編集します。なお、Auth Module を利用する際は事前にパッケージと型定義を導入しておいてください。あとは型定義をインポートして、declare module でどのプロパティにどの型を適用するか定義してあげましょう。

$ yarn add @nuxtjs/auth
$ yarn add -D @types/nuxtjs__auth
nuxt.config.ts
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import { Auth } from 'nuxtjs__auth'

const nuxtConfig: Configuration = {
  // 省略
}

// ここのひとかたまりを追加
declare module 'vue/types/vue' {
  interface Vue {
    $auth: Auth
    $axios: NuxtAxiosInstance
  }
}

使う側では setup メソッドの第2引数に渡されてくる context 引数に実装が詰め込まれているので、そこから読むようにします。root プロパティの中に先ほど interface Vue で指定したプロパティが生えているので、呼んであげるだけです。補完もバッチリ効きますので、便利に使うことができます。

export default createComponent({
  setup(_props, context) {
    const users = ref<User[]>([])
    watch(async () => {
      // Axios Module を呼ぶ例
      users.value = await context.root.$axios.$get('/users')
    })

    const login = async () => {
      // Auth Module を呼ぶ例
      await context.root.$auth.loginWith(/* ユーザー名とパスワードを送信 */)
      context.root.$router.push('/')
    }

    return { users, login }
  }
})

context.root.$router.push なんかをしれっと使ってますが、だいたい欲しいもの($el$store など)は context.root に生えているので、困ったらまずここを探してみると良いでしょう。何が入っているかは補完機能が全部教えてくれます。ちなみに emitcontext.emitcontext 直下にぶら下がっています。

おわりに

ここまで Nuxt.js に Composition API を導入し、TypeScript で型を付けながら実装する方法を紹介してきました。Composition API 自体がまだちょっと洗練されておらず、特に Nuxt.js が用意している専用機能に関するサポートに関しては手つかずの状況ではあります。しかし、あんまり凝ったことをしないのであれば十分使えるかなといった手応えです。来年Q1にも予定されている Vue.js 3.0 とそれをベースにした新しい Nuxt.js を楽しみにしつつ、年を越したいと思います。

  1. たまにanyを書かざるを得ないのはご愛敬

  2. 例えば初期値に null を入れたら Ref<null> に推論されるので、後から何かしらの型を持つ値を代入しようとしたときに型エラーになります

  3. <template> 内で型チェックできる方法があれば教えてください......

  4. 初回に計算されて、それ以降変更を検知する箇所がないため、そういう動作になるものと思われます

95
76
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?