Nuxt v2 のSPAモードが起動するまでの流れを追ってみた

こんにちは。
マネーフォワード クラウド勤怠」エンジニアの ktmouk です。

最近、社内でも個人的にも Nuxt.js の名前をきく機会が増えたのですが、内部でどのように動いているのか気になったのでコードを読んで流れを追ってみました。

なお、Nuxtのバージョンは v2.15.4 時点で、SPAモードで実行した場合の流れを見ていきます。

今回見ていく題材

コードを追う前に、とりあえずNuxtを起動してみます。

Nuxtのリポジトリには簡単なサンプルコードも含まれているので、その中からSPAのサンプルコードを動かしてみます。

# Nuxtをクローンして v2.15.4 にチェックアウト
$ git clone git@github.com:nuxt/nuxt.js.git 
$ cd nuxt.js 
$ git checkout v2.15.4

# SPAのサンプルコードに移動
$ cd examples/spa

# Nuxtをビルドして起動する
$ yarn
$ yarn run nuxt build
$ yarn run nuxt start

上記のコマンド実行後、ブラウザで確認します。

いい感じに起動していますね。

nuxt buildnuxt start では何をしているか見てみます。

nuxt build.nuxt ディレクトリを作るコマンド

nuxt build を実行すると、プロジェクトのルートに .nuxt というディレクトリが生成されます。

.nuxt ディレクトリにはWebpackビルド後の最終成果物と、Webpackビルド前の中間成果物が含まれています。
最終成果物は、nuxt start でNuxtを起動する際に必要になります。

中間成果物は lodash.template で作られる

では、Webpackビルド前の中間成果物はどこから生成されたのでしょうか。
実は中間成果物は、@nuxt/vue-app で管理されているファイルを元に生成されています。

これらのファイルは単純なJavaScriptファイルではなく、
lodash.template でコンパイル可能なテンプレート形式で管理されています。
テンプレートで管理している理由は、nuxt build を実行したタイミングでないと分からない値が存在するためです。

たとえば、Nuxtのルーティングは、pages ディレクトリにある Vue ファイルに基づいて vue-router の設定を自動的に生成 します。
これには template/router.js のテンプレートが使用されています。

nuxt build を実行したタイミングで 、pages ディレクトリにある Vue ファイル名の一覧を変数としてテンプレートへ渡して、そのコンパイル結果が中間生成物として出力されます。

ルーティングの自動生成のコードを覗いてみる

実際にテンプレートからJavaScriptファイルを出力しているコードを見てみます。
Builder クラスの generateRoutesAndFiles メソッドで、各テンプレートをJavaScriptファイルにコンパイルしています。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/builder/src/builder.js

実際にコンパイルを実施しているのは compileTemplates メソッドですが、
その前に templateContext というオブジェクトを用意して、テンプレートへ渡す変数の一覧や、
テンプレートが格納されているパスの一覧を templateContext に格納していきます。

resolve から始まっているメソッドの中で、それぞれレイアウト用の変数、ルーティングの変数、Vuexの変数を templateContext に格納しているイメージです。

最終的にtemplateContext が完成したら、compileTemplates メソッドに渡して実際にテンプレートのコンパイルを行います。

async generateRoutesAndFiles () {
  consola.debug('Generating nuxt files')

  this.plugins = Array.from(await this.normalizePlugins())

  const templateContext = this.createTemplateContext()

  await Promise.all([
    this.resolveLayouts(templateContext),
    this.resolveRoutes(templateContext),
    this.resolveStore(templateContext),
    this.resolveMiddleware(templateContext)
  ])

  this.addOptionalTemplates(templateContext)

  await this.resolveCustomTemplates(templateContext)

  await this.resolveLoadingIndicator(templateContext)

  await this.compileTemplates(templateContext)

  consola.success('Nuxt files generated')
}

今回はルーティングの部分だけ注目して見てみます。

実際にルーティング用の変数を準備している resolveRoutes メソッドのコードを見ます。
pages 配下のvueファイルのパス一覧を、templateVars.router.routes に代入しています。
templateVars がテンプレートに渡す変数になります。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/builder/src/builder.js#L353-L370

async resolveRoutes ({ templateVars }) {
  ...

  // Use nuxt createRoutes bases on pages/
  const files = {}
  const ext = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`)
  for (const page of await this.resolveFiles(this.options.dir.pages)) {
    const key = page.replace(ext, '')
    // .vue file takes precedence over other extensions
    if (/\.vue$/.test(page) || !files[key]) {
      files[key] = page.replace(/(['"])/g, '\\$1')
    }
  }
  templateVars.router.routes = createRoutes({
    files: Object.values(files),
    srcDir: this.options.srcDir,
    pagesDir: this.options.dir.pages,
    routeNameSplitter,
    supportedExtensions: this.supportedExtensions,
    trailingSlash
  })
  ...
}

次に @nuxt/vue-app にあるルーティングのテンプレートも見てみます。
下記の行で、 router.routes の値を recursiveRoutes 関数の引数に渡して、vue-router の設定を生成しています。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/vue-app/template/router.js#L72

<% function recursiveRoutes(routes, tab, components, indentCount) {
  ... vue-router の設定を生成するロジック (省略) ....
}
const _components = []
const _routes = recursiveRoutes(router.routes, '  ', _components, 1)
%>

テンプレート内のコードが中々複雑ですが、
確かにテンプレートを元にJavaScriptファイルを動的に生成しているのが分かります。

nuxt start はNuxtのサーバを起動するコマンド

次に起動時の挙動を見ていきます。

nuxt start はNuxtのサーバを起動するコマンドです。
Nuxtのサーバは、リクエスト元のブラウザに nuxt build でビルドした最終成果物とHTMLを返します。

サーバを起動するロジックは packages/server/src/server.js に書かれています。
サーバのフレームワークは senchalabs/connect というライブラリが使われています。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/server/src/server.js#L35

export default class Server {
  constructor (nuxt) {
    ...

    // Create new connect instance
    this.app = connect()

    ...
  }
}

senchalabs/connect はミドルウェアという機能を持っていて、このミドルウェアを使ってリクエストが来た際のロジックを記述していきます。
ちなみにミドルウェアは、Nuxtの serverMiddleware の設定でユーザ独自のミドルウェアを追加すること可能です。

どんなミドルウェアを使っているか見てみる

どんなミドルウェアを使っているか覗いてみます。
setupMiddleware のメソッド内で、 senchalabs/connect に渡すミドルウェアのセットアップを行っています。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/server/src/server.js#L70

nuxt build で生成した最終成果物 (.nuxt/dist/client) や静的ファイルの配信は、
serve-static というミドルウェアが使われているようです。

また最後には、 nuxtMiddleware というミドルウェアが追加されています。

import path from 'path'
import consola from 'consola'
import launchMiddleware from 'launch-editor-middleware'
import serveStatic from 'serve-static'
...

async setupMiddleware () {
  ...

  // For serving static/ files to /
  const staticMiddleware = serveStatic(
    path.resolve(this.options.srcDir, this.options.dir.static),
    this.options.render.static
  )
  staticMiddleware.prefix = this.options.render.static.prefix
  this.useMiddleware(staticMiddleware)

  // Serve .nuxt/dist/client files only for production
  // For dev they will be served with devMiddleware
  if (!this.options.dev) {
    const distDir = path.resolve(this.options.buildDir, 'dist', 'client')
    this.useMiddleware({
      path: this.publicPath,
      handler: serveStatic(
        distDir,
        this.options.render.dist
      )
    })
  }
  ...

  // Add user provided middleware
  for (const m of this.options.serverMiddleware) {
    this.useMiddleware(m)
  }
  ...

  // Finally use nuxtMiddleware
  this.useMiddleware(nuxtMiddleware({
    options: this.options,
    nuxt: this.nuxt,
    renderRoute: this.renderRoute.bind(this),
    resources: this.resources
  }))
}

最後に追加されている nuxtMiddleware は、CSPヘッダや Content-Type ヘッダを設定したうえで、
アプリテンプレートを元に生成したHTMLをブラウザに返すミドルウェアになります。

ちなみに、アプリテンプレートからHTMLを作るロジックも lodash.template が使われています。
こちらは、nuxt build のタイミングで事前にコンパイルしている訳ではなく、
レスポンスが来る度にアプリテンプレートをコンパイルしています。
https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/vue-renderer/src/renderer.js#L373-L378

parseTemplate (templateStr) {
  return template(templateStr, {
    interpolate: /{{([\s\S]+?)}}/g,
    evaluate: /{%([\s\S]+?)%}/g
  })
}

所感

Nuxtのコードを読んでみると、意外と外部ライブラリを積極的に使っているのが分かりました。
今回はSPAの起動の流れを追ってみたのですが、SSRやSSGではまた挙動が違うかもしれません。
時間があったらそちらも追ってみたいと思います。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket