はじめに
この記事は 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などをお使いの方はパッケージマネージャーなどが出てきた際に、適切なコマンドに読み替えてください。
準備
とりあえずnodeとyarnを入れておきます。エディタは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'を書き加えましょう。
export default {
  // 前略
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxt/typescript-build',  // ここを追加
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify'
  ],
  // 後略
}
そしてこの際なのでnuxt.config.jsをnuxt.config.tsにリネームして、型を付けます。default exportしているあたりをいったんnuxtConfig変数に入れて型を付け、後からエクスポートするようにします。
$ mv nuxt.config.js nuxt.config.ts
import { Configuration } from '@nuxt/types'
import colors from 'vuetify/es5/util/colors'
const nuxtConfig: Configuration = {
  // 中略
}
module.exports = nuxtConfig
最後に次のような内容で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コマンドに置き換えるだけです。
"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 セクションで導入した定義を有効化してください。
module.exports = {
  // 前略
  parserOptions: {
    // ここを変更
    parser: '@typescript-eslint/parser'
  },
  extends: [
    '@nuxtjs',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended',
    '@nuxtjs/eslint-config-typescript'. // ここを追加
  ],
  // 後略
}
最後にpackage.jsonのscriptsセクションにあるlintコマンドで.tsファイルに対してもlintが走るように対象となる拡張子を追加すれば完了です。
"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 を有効化します。
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
nuxt.config.ts でプラグインを有効化することもお忘れなく。
const nuxtConfig: Configuration = {
  // 前略
  plugins: ['@/plugins/composition-api'],
  // 後略
}
これで導入は完了です。
Composition API による実装の例
コンポーネントの作成
ではVuetifyを選択することによってデフォルトで生成されたコンポーネントを改造する形で、Composition API をどうやって Nuxt.js に組み込んでいくか説明していきます。
まずは layouts/default.vue を確認してみましょう。デフォルトの実装は以下のようになっています。なお下記コードでは<script>内を抜粋しています。
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 のサンプルコード通りとなります。
<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
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 に生えているので、困ったらまずここを探してみると良いでしょう。何が入っているかは補完機能が全部教えてくれます。ちなみに emit は context.emit と context 直下にぶら下がっています。
おわりに
ここまで Nuxt.js に Composition API を導入し、TypeScript で型を付けながら実装する方法を紹介してきました。Composition API 自体がまだちょっと洗練されておらず、特に Nuxt.js が用意している専用機能に関するサポートに関しては手つかずの状況ではあります。しかし、あんまり凝ったことをしないのであれば十分使えるかなといった手応えです。来年Q1にも予定されている Vue.js 3.0 とそれをベースにした新しい Nuxt.js を楽しみにしつつ、年を越したいと思います。