Nuxt モジュールパーフェクトガイド 🍜 Nuxt.js 2.15 🍜 TypeScriptでの開発対応
Nuxt自体の動きを外部から変えたいと思ったことはありませんか? もちろん、ベタに nuxt.conf.js
を弄るのもよいのですが、共通分に関しては切り分けて個別の部品として管理したいですよね。そんなときに、 モジュール が役に立ちます。
Nuxt モジュールができること
モジュール は nuxt.conf.js
で記述するようなビルドの動きを拡張することができます。例えば、ミドルウェアを追加したり、事前に特定のファイルを作成したいときなどに便利です。
一方で、Nuxt は混乱しやすい用語として プラグイン という仕組みが存在します。
プラグインは、Vue インスタンスと、サーバコンテキストから特定のオブジェクトを利用できるようにするために利用されるものです。
Nuxt モジュールからは、Nuxt プラグインを追加することができるので、使い分けとしては以下のようになります。
種類 | 用途 |
---|---|
プラグイン |
$root やcontext から呼び出すことができる何かを提供する |
モジュール | ビルドの動きを変えたい場合。設定により動的な挙動を持つプラグインとして提供したい場合 |
参考例として、 gtm-module では、やりたいことはプラグインとしてGoogle Tag Manager 操作オブジェクトとして $gtm
を使えるようにすることなので、一見プラグイン単体で良さそうです。ただし、以下のような状況があるため、モジュールからプラグインを動的に生成する必要があります。
- IDのスクリプトへの埋め込み
- head へのスクリプト読み込みタグの直接追加
- 開発環境での mock の読み出し (読み込むプラグインを環境によって分ける)
モジュールの骨格と読み込み
モジュールは単純な関数によって提供されます。
試しに、modulesというフォルダをNuxtプロジェクトのルート下に作り、その中に、example.js を作成します。
export default function myModule (option) {
}
設定ファイルに、モジュールを読み込む設定を追加します。
export default {
// 中略..
// modules 利用
modules: [
'~/modules/example'
]
}
これにより、Nuxtは、実行時に modules/example.js
に記載されているexportされたdefault関数を実行しようとします。
この関数は、Promiseを返すことにより、非同期処理が必要な作業も行うことができます。nuxt.config.js
の modules
に記載された順序どおりに、1つづつ順次実行する流れになっているので、仮に別のモジュールが先に読み込まれている必要がある場合などには注意が必要です。
今回は Nuxtプロジェクトフォルダにモジュールを作成しましたが、実際には、npmパッケージを作成して複数のプロジェクトで共有できるようにしたり、単体でテストできるようにしたほうが良いでしょう。npmパッケージの場合は通常、文字列に modules
に @nuxtjs/gtm
のような文字列を渡すことで対応できます。
npmパッケージ名を指定した場合は、node.js上で、require
した場合と同じく、package.json
の main
に記載されたファイルを読み込みに行きます。(デフォルトはnpmの標準挙動で、npmパッケージ直下の index.js を読み込みに行こうとします。)
モジュールは実行時にインポートされ読み込まれることになるので、トランスパイルが完了されていて、実行環境 (node.js) が解釈できるものである必要があります。
この文章上は、最終的にTypeScriptで記載するつもりですが、簡単のため最初はJavaScript(node.js 14が解釈できるもの)で記載していきます。
nuxt.config.js
からモジュールへは、オプションを渡すことができます。先程から引用している、@nuxtjs/gtm
の場合は、 GTMのIDを渡すときなどに利用されます。
export default {
// 中略..
// modules 利用
modules: [
['~/modules/example', {id: 'sample'}]
]
}
オプションは関数の引数として渡され、利用することができます。特に何もオプションを渡していない場合については空のオブジェクト {}
が option として渡されます。
export default function myModule (option) {
console.log(option.id) // sampleがモジュールを読み込んだタイミングで出力される
}
モジュールはファイルを作らずに、nuxt.config.js
に直接インラインで記載することもできます。プロジェクト固有で、再利用不要・量が少ない場合においては、この方法を使う考え方もあります。
export default {
// 中略..
// modules 利用
modules: [
function () {
// モジュールの処理
}
]
}
もし、モジュールが本番環境には不要な場合は、設定 buildModules
でモジュールをロードします。開発限定の機能拡張 (例えば、dotenv-module) は、こちらで読み込みます。
export default {
// 中略..
// buildModules 利用
buildModules: [
'~/modules/example'
]
}
Hook
モジュールでできることをそれぞれ考えていきます。1つめは Hook です。Nuxtで用意されている特定のタイミングで何かを行うことができます。
export default function myModule (option) {
this.nuxt.hook('ready', async nuxt => {
console.log('Nuxt.js準備完了!!')
})
}
module 中で使える this
は、 ModuleContainer と呼ばれるオブジェクトになります。このオブジェクトは、Nuxtを拡張するために必要な便利な機能を提供しています。
this.nuxt
は、Nuxtの根幹をなす Nuxt オブジェクトです。このオブジェクトは、hookable を継承したものとなっており、Nuxtのビルド等で生じるさまざまなイベントにフックを行うことができます。
フックする関数についてはPromiseを返すことにより、非同期にすることができます。
通常は、フックした処理が完了するまで待って次の処理に進む動きになります。
以下のようなHookが確認されています。(現在廃止されているものを除く, 2.15時点)
hook名はバージョン間で変更がなされたり、渡されれる値が変わったりするので要注意です。
一応はドキュメントはあるのですが、ここではドキュメントに記載されていないものも含め2.15時点で確認されている最新のものを記載します。
Nuxt全体の動き
hook名 | args | タイミング |
---|---|---|
ready | nuxt:Nuxt |
Nuxtが動作する準備が完了した段階。 |
close | nuxt:Nuxt |
Nuxtのインスタンスが正常に終了する手前。 |
listen |
server:any , `{host: string, port: number |
string}` |
error | error:any |
フックを呼び出したタイミングでエラーが発生した場合。errorには、例外としてキャッチされたものが渡される。これは、Nuxt上の定義というよりは、hookableで定義されたもの。 |
ドキュメント:
モジュールのロード
準備フェーズで行われます
hook名 | args | タイミング |
---|---|---|
modules:before |
moduleContainer:ModuleContainer , options:NuxtOptionsModule[]
|
すべてのモジュールのロードの前。つまり、モジュール内でこちらにフックしようとしても機能しません。後述しますが、フックで呼び出される関数は nuxt.config.js からも定義できるため、そちらにモジュール関係で必要な前処理を記載するために使うのが良さそうです。 |
modules:done | moduleContainer:ModuleContainer |
すべてのモジュールがロードされた後 |
ドキュメント:
Renderer
サーバ関連のための処理について。つまり、 npm run build
や、 npm run generate
を行う際にはこのイベントは呼び出されません。
準備段階
hook名 | args | タイミング |
---|---|---|
render:before |
renderer:Server (NuxtのServerインスタンス), options:NuxtOptionsRender
|
Rendererクラスのミドルウェアとリソースを設定する前 |
render:setupMiddleware |
app:Server (connectのインスタンス) |
Nuxtがミドルウェアをセットアップする前 |
render:errorMiddleware |
app:Server (connectのインスタンス) |
Nuxtがエラー用のミドルウェアをセットアップする前。具体的には、Sentryモジュール のようなエラー収集ツールで使うのが良さそうです。 |
render:resourcesLoaded | resources:any |
リソースのロード後。リソースとは、サーバを動かすために必要なファイル等を指します。(server.manifest.json 等) |
render:done |
renderer:Server (NuxtのServerインスタンス) |
Rendererの準備が完了した段階 |
レンダリング段階
特にSSR周りの挙動
hook名 | args | タイミング |
---|---|---|
vue-renderer:context | renderContext:any |
SPA/SSR問わず、サーバがHTMLレンダリングに関して必要な処理をしようとする前 |
vue-renderer:ssr:prepareContext | renderContext:any |
SSRでアプリケーションをレンダリングする前。Vueを通してHTMLへの変換を行っておらず、asyncData等はまだ呼ばれていない。 |
vue-renderer:spa:prepareContext | renderContext:any |
SPAでアプリケーションをレンダリングする前 |
vue-renderer:ssr:context | renderContext:any |
SSRでアプリケーションをレンダリングした後。asyncData等は呼ばれているため、renderContext.nuxt には window.__NUXT__ (asyncDataの結果等はこちらに書き込まれる) にシリアライズする前の値があり、参照したり、書き換えたりすることもできる。 |
vue-renderer:ssr:csp | cspScriptSrcHashes:string[] |
SSRで、コンテンツセキュリティポリシー 用のメタ情報生成前 |
vue-renderer:ssr:templateParams |
templateParams:Record<string, string> , renderContext:any
|
SSRで、HTMLをレンダリングする前。templaraParamsには、Vueを通して生成したHTMLなどが入っており、それを組み立ててレスポンスのbodyを作る前の状態になっている。templateParams.HTML_ATTRS , templateParams.HEAD_ATTRS , templateParams.BODY_ATTRS , templateParams.HEAD , templateParams.APP , templateParams.ENV を参照・書き換えを行うことができる。これらの値は、app.htmlに渡されHTMLが完成する。 |
vue-renderer:spa:templateParams | templateParams:Record<string, string> |
SPAで、HTMLをレンダリングする前。SSR用のrenderContextがない以外は、vue-renderer:ssr:templateParams と同じ |
render:route |
url:string , result:any , context:{req, res}
|
routeがレンダリングされるとき。レスポンスを送る前 |
render:routeDone |
url:string , result:any , context:{req, res}
|
routeがレンダリングされるとき。レスポンスを送った後 |
server:nuxt:renderLoading |
req , res
|
routeの結果が空だった場合。開発環境でビルド中の場合にローディング画面を出すために利用されるようです |
ドキュメント:
Build
ビルド時に発生する挙動です。npm run dev
実行時は、Nuxt が ready
になった後にこれらのフックが呼び出されます。
hook名 | args | タイミング |
---|---|---|
build:before |
builder:Builder , buildOption:NuxtOptionsBuild
|
Nuxtビルドの開始前 |
builder:prepared |
builder:Builder , buildOption:NuxtOptionsBuild
|
ビルド用のディレクトリが作成されたとき |
builder:extendPlugins | plugins:NuxtOptionsPlugin[] |
プラグイン作成時 |
build:templates | { templatesFiles, templateVars, resolve } |
.nuxt テンプレートファイル生成時 |
build:extendRoutes |
routes , resolve
|
ルーティング生成時 |
webpack:config |
webpackConfigs:Configuration[] (WebPack の Configuration オブジェクト。name key に client (SPA用), server (SSR用), modern (モダンモード) がそれぞれ設定されている) |
コンパイラの設定前 |
build:compile | {name: string, compiler} |
webpackコンパイル前。univasalモードの場合は、client と server の名前で2度呼び出される |
build:compiled | {name: string, compiler, stats} |
webpackのビルド終了時 |
build:done | builder:Builder |
Nuxtビルドの終了時 |
ドキュメント:
Generator
静的ファイル生成ジェネレーター利用時に発生する挙動です。サーバとして動作させている場合は発生しません。
hook名 | args | タイミング |
---|---|---|
generator:before |
generator:Generator , generateOptions:NuxtOptionsGenerate
|
生成前 |
generate:distRemoved | generator:Generator |
ビルド先のフォルダが削除されるとき |
generate:distCopied | generator:Generator |
静的ファイルとビルドされたファイルがコピーされたとき |
generate:extendRoutes | routes:NuxtRouteConfig[] |
route初期設定の最後。ジェネレーターのためにroute情報を書き換えたりなどに利用できる。 |
generate:route |
route:NuxtRouteConfig , setPayload:(payload: object) => void
|
ページ生成前。動的にペイロードを渡すときに利用する。ルートごとに呼ばれる |
generate:page | {route:NuxtRouteConfig , path:string , html:string , exclude: boolean , errors: {type: string, error: Error}[] } |
生成後のパスとHTMLを更新する時 |
generate:routeCreated | {route:NuxtRouteConfig , path:string , errors: {type: string, error: Error}[] } |
生成されたページの保存に成功した時 |
generate:routeFailed | {route:NuxtRouteConfig , errors: {type: string, error: Error}[] } |
生成されたページの保存に失敗した時。失敗ごとに呼ばれる |
generate:manifest |
manifest , generator:Generator
|
manifest.js 作成前。このファイルは、Nuxtがアセットなどのパス情報を知るために使われるもの。PWAの manifest.json とは違う |
generate:done |
generator:Generator , errors
|
生成完了時 |
ドキュメント:
Command / CLI
hook名 | args | タイミング |
---|---|---|
run:before | {argv: any , cmd: {name: string, usage: string, description: string} , rootDir: string } |
CLIコマンド実行前 |
config | options:NuxtConfig |
configロード時 |
cli:buildError | error:any |
yarn run dev 時にビルドエラー発生時。catchしたエラーをハンドリングしたい時に利用できる |
Hook を設定から定義する
modules:before
のようなモジュールのロード以前に呼ばれるHookについては、モジュールから利用することができません。
これを利用する場合は、nuxt.confg.js
設定に直接記載する必要があります。
export default {
hooks: {
modules: {
before(moduleContainer, options) {
// モジュールのロード前に行う必要のある処理を記載できます
}
}
}
}
Template
モジュールからは動的なファイルを生成してPluginとして読み込ませたりといった使い方ができます。
この機能は lodash.template を利用しているので、元となるファイルはこの形式で記載することになります。
Plugin
ModuleContainerのaddPlugin()
を利用することで、プラグイン用のファイルの生成と、Pluginのロードを行うことができます。
const id = '<%= options.id %>'
export default function (ctx, inject) {
const example = {
logId: () => {
console.log(id)
}
}
inject('example', example)
}
const path = require('path')
export default function myModule (option) {
const { id } = option
this.addPlugin({
src: path.resolve(__dirname, 'plugin.js'),
fileName: 'example.js', // ここのKeyはfilename でも良い
options: { // テンプレートに渡したい値は options で指定する
id
}
})
}
export default {
// 中略..
// modules 利用
modules: [
['~/modules/example', {id: 'sample'}]
]
}
これで、Vue
の $root
や、サーバコンテキストから、$example
を使えるようになります。
addPlugin()
では、テンプレートからのファイル作成に必要な値のほかに、pluginsプロパティで利用する値を渡すこともできます。
クライアントのみに利用したいプラグインの場合は、 mode: 'client'
を指定することで、サーバサイドで読み込まれることを抑止することができます。
ファイルのみ生成
順番が前後しますが、ModuleContainerのaddPlugin()
の中では、addTemplate()
という関数が呼ばれ、ファイルを生成した後に、設定の pluginsプロパティ に、生成したプラグインファイルを読み込むように挿入します。
単純にファイルだけを生成しておき、そのファイルをプラグインから読み込むといった使い方ができます。
先程から説明に使っている、gtm-moduleでは、以下のようにプラグインが利用するファイルを作成しています。コードを読みながら流れを理解してみます。
テンプレートの追加:plugin.utils.js
を読み取り、gtm.utils.js
を配置する。
テンプレート側:plugin.utils.js
はデバッグモード (options.debug
) が true
であれば、console.log
を行うようなコードを生成する。
プラグイン側:gtm.utils.js
を読み、その中で定義されている log()
を利用する。モジュールのデバッグオプション (options.debug
) が true
であれば、console.log
によりログが出力される。
Layout
ModuleContainerのaddLayout()
を利用することで、モジュールから新たなLayoutを追加することもできちゃいます。流れとしては、addPlugin()
の時と同じで、内部的に addTemplate()
が呼ばれて、第2引数として指定した名前で呼び出せるようにします。
デザイン系のモジュールとして、ガワごと提供する場合に利用できそうな機能です。
const path = require('path')
export default function myModule (option) {
const { id } = option
this.addLayout({
src: path.resolve(__dirname, 'exampleLayout.vue'),
fileName: 'exampleLayout.vue',
options: {
id
}
}, 'example')
}
利用するときは、利用側のpageコンポーネントで以下のように呼び出します。
ドキュメント: https://ja.nuxtjs.org/docs/2.x/components-glossary/pages-layout/
<template>
<div>Test</div>
</template>
<script>
export default {
layout: 'example'
}
</script>
なお、第2引数に 'error'
を指定すると、唯一のエラーページ用のレイアウトが差し替わります。Nuxtは、エラーページを1つしか指定できないので、この処理を行うモジュールが2つある場合はコンフリクトを起こします。(特にエラー等は出ないようです。)
ServerMiddleware の拡張
Server用のミドルウェアの拡張を行うときは、ModuleContainerのaddServerMiddleware()
を利用します。
実体は、設定の serverMiddleware
に渡した値を追加するだけです。
よって指定方法は、ドキュメント に記載されているとおりです。
ビルドの拡張
ModuleContainerのextendBuild()
を利用することで、モジュールからビルド設定の拡張を行うことができます。挙動は、設定の build.extend()
と同じです。
webpackの挙動を変える必要がある場合などに使います。
export default function myModule (option) {
this.extendBuild((config, ctx) => {
// config は、WebpackConfiguration が入っている。
// このオブジェクトを書き換えることで、webpackビルドの挙動を変更することができる。
// ctxには、isClient, isDev, isModern, isServer, loaders などが入っている
})
}
ルートの拡張
ModuleContainerのextendRoutes()
を利用することで、モジュールからルートを弄ったり、追加することができます。挙動は、設定の router.extendRoutes()
と同じです。
addLayout()
等と組み合わせて、管理用モジュール等を提供する (切り離し可能) といった使い方が想定できるでしょう。
export default function myModule (option) {
this.extendRoutes((routes, resolve) => {
routes.push({
name: 'sample',
path: '/sample',
chunkName: 'sample',
component: resolve(__dirname, 'sample.vue')
})
})
}
モジュールから別のモジュールを読み込む
ModuleContainerのrequireModule()
を利用することで、モジュールからさらにサブモジュールを読んだり、別の依存のあるモジュールを呼ぶこともできます。
export default function myModule (option) {
this.requireModule(['~/modules/example-sub', option])
}
モジュールから設定にアクセスする
nuxt.config.js
で定義した設定を直接使いたい場合は、ModuleContainerのoptions
を使うことができます。
export default function myModule (option) {
console.log(this.options) // 設定された情報がそのまま出てくる
}
この値は書き換えることもできるので、APIとして用意されていないCSSの追加や、アセットの追加については直接このオブジェクトを操作することになります。
meta情報の定義
モジュールには、モジュール名・バージョンといったメタ情報を追加することができます。これは、現状はモジュールの一意性確保のためにされます。
基本的には、フォーマットは、package.json
と同じなため、それをそのまま利用しているケースも見受けられます。
const myModule function(option) {
this.requireModule(['~/modules/example-sub', option])
}
myModule.meta = {
"name": "@mymodule/example"
}
export default myModule
TypeScript によるモジュールの記述
さて、ここまでモジュールができることを述べてきました。実際に開発する際は、TypeScriptを利用してIDEの入力補完などを大いに活用して、安全な拡張ライフを送りたいものです。
先日、ModuleContainerに関して、typesを追加するPull Request を送りましたが、hookに関してはまだまだ不完全な状況ではあります。
また、モジュールに関しては、トランスパイル済みのものを提供する必要があるため、開発時には何らかのビルドツールを導入する必要が出てきます。
現状リリースされているいくつかのモジュールの実装を確認すると、siroc を利用しているケースが多いようです。
こちらは、Nuxtの開発等のために、一般的なJavaScriptでも使える製品として切り出されたものの1つで、TypeScriptからJavaScriptのコードへ、設定無しでトランスパイルできるようにすることを目指しているようです。
中身的には、Rollupを含んでおり、少なくとも Nuxt 用のモジュール開発のためには、本当に設定無しで対応できるようです。
実際に、空のプロジェクトからTypeScriptでモジュールを作成していきます。(利用するNuxtプロジェクトは用意されているものとします。)
プロジェクト作成
$ mkdir sample-nuxt-module
$ yarn init
yarn init v1.22.4
question name (sample-nuxt-module):
question version (1.0.0): 0.0.1
question description: sample
question entry point (index.js): dist/index.js
question repository url:
question author:
question license (MIT):
question private:
success Saved package.json
$ yarn add -D siroc @nuxt/types
コミットするべきでないファイルを記載します。
node_modules
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
tsconfig.json
には以下のように記述します。
{
"compilerOptions": {
"target": "ESNext",
"moduleResolution": "Node",
"strict": true,
"types": [
"@nuxt/types"
]
}
}
少し、dependencies を書き換えます。現在まだリリースされていない Nuxt の types を利用したいがためです。
リポジトリ中のフォルダにあるnpmパッケージを直接ロードするには、GitPkg を利用するのが便利です。
また、ビルド用のスクリプト追加や、types があることについても記載します。(これを書かない場合 index.d.ts
が生成されません。)
{
"name": "sample-nuxt-module",
"version": "0.0.1",
"description": "sample",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "siroc build"
},
"devDependencies": {
"@nuxt/types": "https://gitpkg.now.sh/nuxt/nuxt.js/packages/types?dev",
"siroc": "^0.9.2"
}
}
次に、src/main.ts
を作成し、適当なモジュールを作ります。
IDEなどで開発しているのであれば、thisで利用できる関数がわかるはずです。
import type { Module } from '@nuxt/types'
interface Option {
id?: string
}
const exampleModule: Module<Option> = function(options: Option) {
this.nuxt.hook('ready', () => {
console.log('Nuxt準備完了', options.id)
})
}
// @ts-ignore
exampleModule.meta = require('../package.json')
export default exampleModule
ビルドを行います。
$ yarn run build
yarn run v1.22.4
$ siroc build
ℹ Beginning build sample-nuxt-module 23:41:33
✔ Built sample-nuxt-module index.d.ts 153 B sample-nuxt-module 23:41:35
✔ Built sample-nuxt-module index.js 168 B sample-nuxt-module 23:41:35
✔ Finished building in 1.7s sample-nuxt-module 23:41:35
✨ Done in 2.37s.
目的のブツができているかを確認します。
'use strict';
const exampleModule = function(options) {
this.nuxt.hook("ready", () => {
console.log("Nuxt\u6E96\u5099\u5B8C\u4E86", options.id);
});
};
exampleModule.meta = require("../package.json");
module.exports = exampleModule;
yarn link を利用してローカルで、パッケージが利用できるか試してみましょう。
$ yarn link
yarn link v1.22.4
success Registered "sample-nuxt-module".
info You can now run `yarn link "sample-nuxt-module"` in the projects where you want to use this package and it will be used instead.
✨ Done in 0.03s.
$ yarn link sample-nuxt-module
yarn link v1.22.4
success Using linked package for "sample-nuxt-module".
nuxt.config.js
で今回作ったモジュールをロードするように設定して、yarn run dev
を実行するとどうなるか試してみましょう。
export default {
modules: [
['sample-nuxt-module', {id: 'sample'}]
]
}
$ yarn run dev
...(中略)...
Nuxt準備完了 sample
...
おわりに
これで、Nuxtモジュールの作り方の全容について説明しました。思ったよりはシンプルな仕組みです。
モジュールについて公開できるものであれば、nuxt/modules リポジトリにPull Requestを送ることで、Explore Nuxt Modulesに記載してもらえるようです。
もし実装方法などに迷った場合は上記のディレクトリに掲載されているモジュールのソースコードを読んで見ることと、Nuxt自体のソースコードを読んでみることで理解が進みます。
元々は、TypeScriptでNuxtモジュールを作成しようと思いメモ程度に文章を作りましたが、Nuxt自体へPull Requestを送ったり、文章がかなりボリューミーになるなど、想像より話が広がってしまいました。
NuxtのTypeScriptサポートについては、まだまだなところもあるので、必要に応じて修正を送っていこうと思います。
Discussion