はじめに
この記事は 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 を楽しみにしつつ、年を越したいと思います。