東京ラビットハウスのerukitiです。ごきげんよう。
Nuxt大人気ですね。ですが皆さん、Nuxtを使っていてもその中身がどうなってるかまではちゃんと調べている方は少ないのではないでしょうか?エンジニアたるもの中身をちゃんと知らずに使うなどとはもってのほかです!
そこで今回の記事はではNuxtの中身を読んで仕組みを解説します。
2018/10/08(月曜祝日)に開催される技術書典5では、Nuxtの中身を主な題材として、JavaScriptフレームワークの技術と仕組みについて解説する本と、V8を崇める本の2冊を出す予定です。ご興味があればサークルページから、チェックリストに登録してみてください。
この記事は、本を書く過程で調べ上げたことを元にブログバージョンとしてまとめたものです。
誤りがある、ここがわかりづらい、もっと深く説明がほしいなどの要望があれば、お気軽にコメントや、@erukiti宛てにご意見・ご要望をお願いします。
Nuxt v2
本記事で参考としているバージョンは、9/20公開のcommit 7a68e1dde17e0ed7eb7598d9c63ca1ac2383b76e で、tag: v2.0.0です。
Nuxt.js のソースコードはnuxt/nuxt.jsで公開されています。
ディレクトリ構成
まずはnuxt.jsのソースコードでのディレクトリ構成です。
| ディレクトリ名 | 内容 |
|---|---|
| bin | CLI実行ファイル |
| build | Nuxt自体をビルドするためのファイル群 |
| examples | サンプル |
| dist | ビルドされたNuxt |
| lib | ビルド前のソース |
| lib/app | クライアント向けの lodash templates |
| lib/builder | Nuxtのbuilder |
| lib/common | builer, coreなどから参照される共通コード |
| lib/core | Nuxtのcoreとなるコード群 |
| lib/core/middleware | http server向けミドルウェアのコード |
Nuxt.jsなどにおいてミドルウェアというのは、HTTP server向けに特定のインターフェースを持つ関数のことを指します。
これとは別に、.nuxtというディレクトリがアプリケーションのディレクトリ下に作成されます。もちろんnuxt.jsとは違うディレクトリの下にあるはずなのでご注意ください。
| ディレクトリ名 | 内容 |
|---|---|
| .nuxt/ | 中間生成物やビルド成果物などが配置される |
| .nuxt/components | 中間生成物で、Vue component |
| .nuxt/views | 中間生成物で、テンプレートとなるHTMLファイル |
| .nuxt/layouts | 中間生成物でレイアウト用Vueファイル |
| .nuxt/dist | ビルド成果物 |
| .nuxt/dist/client | クライアントに配信する方のビルド成果物 |
| .nuxt/dist/server | SSR用のビルド成果物 |
.nuxtとあれば、アプリケーションのディレクトリ下にある点にご注意ください。
今回説明に使うサンプル
CONTRIBUTINGを参考にしつつ、環境をセットアップしてみましょう。
$ git clone nuxt....
$ cd nuxt.js
$ yarn
今回はアプリケーションディレクトリを _/ とします。
$ mkdir _
$ cd _
$ mkdir pages
$ cat > pages/index.vue
<template>
<div>ほげほげ</div>
</template>
catでファイルを作る場合、</template>の次の行で、CTRL+Dを押してファイル入力を終了します。
$ yarn build ; bin/nuxt _
INFO Building project
✔ success Builder initialized
✔ success Nuxt files generated
READY Listening on http://localhost:3000
このコマンドで _/ をアプリケーションディレクトリとして、nuxtが起動します。デフォルトだと、localhostの3000番ポートで起動するので、ウェブブラウザでアクセスしてみましょう。
たった1ファイルindex.vueを用意するだけでウェブページが表示されました。Nuxtはほんと素晴らしいですね!
大まかなNuxtの動きを見てみよう
Nuxtをdevelopment modeで起動し、ウェブブラウザでトップページにアクセスした時の中の挙動を見てみましょう。
起点はbin/nuxt および bin/nuxt-devです。bin/下のファイルは、CLIでnuxtコマンドを叩いた時に呼ばれるコードです。
まずNuxtとBuilderのオブジェクトを初期化します。
const { Nuxt, Builder } = require('..')
let nuxt, builder
nuxt = new Nuxt(config())
builder = new Builder(nuxt)
-
Nuxtクラスはlib/core/nuxt.jsで定義されています。 -
Builderクラスはlib/builder/builder.jsで定義されています。 -
Nuxtと一緒にRendererなども一緒に初期化されています。
次にBuilderのbuildメソッドを呼び出してビルド処理します。
.then(() => builder.build())
あとは
.then(() => nuxt.listen(port, host, socket))
あとはNuxtのウェブサーバーを起動します。
nuxt.hook('watch:fileChanged', (builder, fname) => {
consola.debug(`[${fname}] changed, Rebuilding the app...`)
startDev({ nuxt: builder.nuxt, builder })
})
ここらへんのコードではファイル変更を検知して、再起動する処理が入っていたりしますが、そこらへんは今回は省略します。
build
Builderのbuildメソッドでは次の2つの処理を行います。
-
generateRoutesAndFilesメソッドでテンプレートからバンドル前のファイルを生成する -
webpackBuildメソッドでwebpackを走らせてバンドル処理を行う
generateRoutesAndFiles
generateRoutesAndFilesではlib/app下にあるファイルを、loadshのtemplate機能を使ってテンプレート展開し、プロジェクトディレクトリの.nuxt/以下に書き出します。
| 元ファイル | 書き出し先 |
|---|---|
| lib/app/App.js | .nuxt/App.js |
| lib/app/client.js | .nuxt/client.js |
| lib/app/empty.js | .nuxt/empty.js |
| lib/app/index.js | .nuxt/index.js |
| lib/app/middleware.js | .nuxt/middleware.js |
| lib/app/router.js | .nuxt/router.js |
| lib/app/server.js | .nuxt/server.js |
| lib/app/utils.js | .nuxt/utils.js |
| lib/app/components/nuxt-error.vue | .nuxt/components/nuxt-error.vue |
| lib/app/components/nuxt-loading.vue | .nuxt/components/nuxt-loading.vue |
| lib/app/components/nuxt-child.js | .nuxt/components/nuxt-child.js |
| lib/app/components/nuxt.js | .nuxt/components/nuxt.js |
| lib/app/components/no-ssr.js | .nuxt/components/no-ssr.js |
| lib/app/components/nuxt-link.js | .nuxt/components/nuxt-link.js |
| lib/app/layouts/default.vue | .nuxt/layouts/default.vue |
| lib/app/views/loading/default.html | .nuxt/loading.html |
| lib/app/views/app.template.html | .nuxt/views/app.template.html |
| lib/app/views/error.html | .nuxt/views/error.html |
これらのうち、client.jsとserver.jsは次のwebpackBuild行程でバンドル処理のエントリポイントとして使われます。componentsは、Vue componentとして登録され、views/はHTMLのテンプレート、layoutsは、デフォルトのレイアウトとして利用されます。
webpackBuild
webpackBuildではclientとserver向けにそれぞれバンドル処理をします。
client向けではwebpack-hot-middleware/clientと先ほどテンプレート展開された.nuxt/client.jsをソースとして、いくつかのファイルをはき出します。
バンドル処理では普通ならはき出すファイルは1つになるのですが、webpackの機能・プラグインを使っているため複数のファイルがはき出されます。
- メインファイルである
app.js - webpack optimizationのsplitChunkを使った
commons.app.js -
html-webpack-pluginで
index.spa.htmlとindex.ssr.html - webpack optimizationのruntimeChunkを使った
runtime.js(webpack用ランタイム) - pages/ 以下のファイルをコンパイルしたもの(たとえば、
pages/index.js) -
Vue.jsのSSRで使われる
vue-ssr-client-manifest.json
色々はき出していますが、基本的にはWebpack4における最適化のためのものです。
Nuxtでこれらの面倒を見ているので、ユーザーは特に何も設定しなくてもwebpack最適化の恩恵を受けられます。素晴らしいですね!
くわしいことは、lib/builder/webpack以下を読み解く必要がありwebpackという大いなる闇の深淵をのぞき込むことになりますので、今回の記事ではおいておきます。
server向けでは、nuxt/server.jsをソースとしてserver-bundle.jsonというファイルをはき出していてSSRに使います。
client向けにせよserver向けにせよ、webpackのバンドル処理を行った場合、副次的にRendererのloadResourcesが呼び出されます。loadResourcesではRendererの処理の為に、vue-ssr-client-manifest.json server-bundler.json index.ssr.html index.spa.htmlのそれぞれのファイルが読み出されレンダラーの初期化が行われます。
ちなみにこれらのファイルはdevだと、webpack-dev-middlewareのせいで、実際のファイルシステムには吐き出されません。実際にファイルを確認したい場合は、webpack-dev-middlewareの設定を変更して、builder.jsのオプションに、writeToFile: trueなどを指定すると良いでしょう。
// lib/builder/builder.js
// Create webpack dev middleware
this.webpackDevMiddleware = pify(
webpackDevMiddleware(
compiler,
Object.assign(
{
publicPath: this.options.build.publicPath,
stats: false,
logLevel: 'silent',
watchOptions: this.options.watchers.webpack
},
this.options.build.devMiddleware
)
)
)
nuxtの設定ファイルをいじるべきではありますが、破壊的コードリーディング(実際にコードを変更したり、consola.logなどを差し込む)とコードリーディングがはかどるので、筆者は面倒くさがってそのままコードを書き換えてしまいました。
Nuxtのウェブサーバー起動
Nuxtはlib/core/nuxt.jsで定義されています。listenメソッドにより、ウェブサーバーを起動し、いくつかのミドルウェアを登録します。
ここまでで、一通りの初期化が完了したことになります。
レンダリング
ウェブサーバーにアクセスが来た場合、lib/core/middleware/nuxt.jsで定義されてるミドルウェア経由で、RendererのrenderRouteメソッドが呼び出されます。
renderRouteメソッドでは、SSRモードかSPAモードかで挙動が変わります。
if (this.noSSR || spa)
このif文の中身はSPAモードとしての挙動で、if文を抜けたあとの処理がSSRモードの処理になります。
nuxt devで何もいじらずに立ち上げた場合はSSRモードなので、そちらの説明をします。
let APP = await this.bundleRenderer.renderToString(context)
vue-server-rendererパッケージのBundleRendererを使いHTMLにレンダリングしてAPP変数に納めます。このthis.bundleRendererは、loadResourcesメソッドからcreateBundleRendererにより初期化されています。
APPの次は、微妙に涙ぐましいコードでヘッダー情報をHEAD変数に納めます。
const m = context.meta.inject()
let HEAD = m.title.text() + m.meta.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text()
if (this.options._routerBaseSpecified) {
HEAD += `<base href="${this.options.router.base}">`
}
if (this.options.render.resourceHints) {
HEAD += context.renderResourceHints()
}
あとは、同じRendererクラスのrenderTemplateメソッドを呼び出し、index.ssr.htmlに、APPとHEADを当てはめてHTMLファイルが完成します。
HTMLファイルからはいくつかのJSファイルが読み込まれることにはなります。
- /_nuxt/runtime.js
- /_nuxt/pages/index.js
- /_nuxt/commons.app.js
- /_nuxt/vendor.app.js
- /_nuxt/app.js
これらのファイル名に見覚えがあることでしょう。Builderがwebpackで生成したファイルです。
ここまでで、Nuxtのdevelopmentモードでの基本的な処理の流れを説明しました。
まとめ
- Builderで、lodashのtemplate展開とwebpackのbundle処理を行う
- Nuxt middlewareなどを登録したウェブサーバーを起動する
- ウェブへアクセスが来たら、RendererによりSSRして結果となるHTMLを返す
Nuxtがやっていることを大雑把に説明するとこんな感じです。簡単ですね!
技術書典5で、ここらへんをまとめた本を出します!
ご興味があればサークルページから、チェックリストに登録してみてください。
