Nuxt.jsのbuild&startが何をやってるのかをコードから追ってみる

以前、Herokuで簡易的なNuxt.jsのアプリケーションのSSRを確認するエントリーを書いた。

mugi1.hateblo.jp

書いたはいいけど、で...

なんでこれ動いてんの?

デプロイできたやった〜と言いたいところだけど、あまりにも動作がブラックボックスすぎるので、一体何がどうなっているのかを調べてみることにする。

Herokuへのデプロイ

node.jsアプリケーションをHerokuにデプロイする際は、デフォルトではpacakge.jsonに記載のstart scriptが実行される。

(参考: Deploying Node.js Apps on Heroku | Heroku Dev Center)

そしてさらに、デプロイ時には以下の設定を追加する必要がある。

 "heroku-postbuild": "npm run build"

つまりデプロイ時には以下の流れでビルドが実行されていることがわかる。

  1. nuxt build
  2. nuxt start

オフィシャルのガイドによると

  • nuxt build - アプリケーションを Webpack でビルドし、JS と CSS をプロダクション向けにミニファイします
  • nuxt start - プロダクションモードでサーバーを起動します(nuxt build 後に実行してください)

とのこと。

調べること

というわけで、以下を調べてみることにする。

  • next build では何をビルドして何を作っているのか
  • nuxt start では何を起動していて、どこでSSRをやっているのか。

nuxt スクリプト

そもそも nuxt というスクリプトのコードを確認してみる。

nuxt.js/nuxt at dev · nuxt/nuxt.js · GitHub

色々やってるけど、とりあえず

const bin = join(__dirname, 'nuxt-' + cmd)

という箇所があり、cmd の部分に指定したオプションが渡る。

つまり、nuxt-buildnuxt-start といったスクリプトが実行される。

nuxt-build

nuxt.js/nuxt-build at dev · nuxt/nuxt.js · GitHub

オプションのロードなどの後に、以下のようなコードがある。

...

if (options.mode !== 'spa') {
  // Build for SSR app
  builder.build()
    .then(() => debug('Building done'))
    .catch((err) => {
      console.error(err)
      process.exit(1)
    })
} else {
  ...

options.mode !== 'spa' という記述があるが、Nuxt.jsではSSRを使わないアプリケーションの構築もサポートしているので、その場合にビルド方法が変わるものと思われる。

https://ja.nuxtjs.org/guide#シングルページアプリケーション-spa-

デフォルトではSSRは有効なので、どうやら builder.build() がビルドの本体のようだ。

実態は Builderというクラス。コード的にはこのあたり。

https://github.com/nuxt/nuxt.js/blob/dev/lib/builder/builder.js#L107

処理の中で特に重要そうなものだけピックアップすると

  • this.nuxt.ready()
  • this.generateRoutesAndFiles()
  • this.webpackBuild()

かな〜と思われる。

this.nuxt.ready()

実体は Nuxt#ready が該当する。

https://github.com/nuxt/nuxt.js/blob/dev/lib/core/nuxt.js#L51

内部ではさらにModuleContainer#readyRenderer#readyを呼び出しており、デフォルトのものに加えて、任意に追加されたミドルウェアやモジュールをロードしている。

this.generateRoutesAndFiles()

webpackでビルドする前のファイルをテンプレートをもとに生成している。

例えば、pages/ の配下にコンポーネントを突っ込むだけで自動的にルーティングが生成されるのは、このあたりで動的にファイルを取得した上でテンプレートファイルからvue-router向けのjsファイルが出力されている。

pages/ に限らず、Nuxt.jsがいい感じにに解決してくれているものの多くがlib/app 配下にテンプレートとして用意されているっぽいので、生成されるファイルの元となるものが見たい場合はここを参照するとよさそう。

https://github.com/nuxt/nuxt.js/tree/dev/lib/app

this.webpackBuild()

みんな大好きwebpackビルド。

lib/builder/webpackの配下にwebpackビルドで必要なコンフィグファイルがまとまっている。

クライアント側とサーバ側でエントリーファイルが異なるので、client.config.jsserver.config.js が存在しており、それぞれについてビルドが実行される。

最終的な成果物はデフォルトだとプロジェクトルートから見て .nuxt/dist 配下に出力される。

nuxt-start

nuxt.js/nuxt-start at dev · nuxt/nuxt.js · GitHub

Nuxt#listen を実行している。

実行前に .nuxt/dist や、SSRを行う場合には .nuxt/dist/server-bundle.json の存在をチェックしているので、あらかじめ nuxt build が実行されていないと落ちる。

Nuxt#listen

https://github.com/nuxt/nuxt.js/blob/dev/lib/core/nuxt.js#L124

this.renderer.app.listen を実行しており、この app の実体は connectインスタンス。これで実際にNuxt.jsアプリケーションが待ち受け状態になる。

github.com

サーバサイドでのルーティングについては、初期化の過程でRenderer#readyがコールされており、そこからさらに辿ると以下のコードが見つけられる。

// Finally use nuxtMiddleware
this.useMiddleware(this.nuxtMiddleware.bind(this))

useMiddlewareは、connectインスタンスに対してMiddlewareを登録している。

// Use middleware
this.app.use(path, handler)

nuxtMiddlewareについては、内部で renderRoute をコールしており、この中で実際にレンダリングしたHTMLを返却している。

// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
let APP = await this.bundleRenderer.renderToString(context)

bundleRendererにはvue-server-renderer#createBundleRenderer で生成されたバンドルレンダラがセットされており、.nuxt/dist/server-bundle.json が元になっている。細かいところまで追えてないが、URLとのマッピングはこのへんで解決しているものと思われる。

というわけで

コードを追ってみることで、事前のビルドで何をしていて、どのあたりでサーバサイドでHTMLを生成しているのかはなんとなくわかった。(ような気がする。)

深く読み込むというよりかは、大体の流れを追うようにコードを見ていったので、もしかすると一部で間違いとかもあるかもしれない。そうだったらスイマセン。

とりあえず、自分の中で完全に闇だった部分が少しわかったのは良かった。

感想としては、コードを見ていると「よくこんなの作ったな...」という気持ちになってきた。OSSコントリビュータに感謝しよう。