こんにちは。 「マネーフォワード クラウド勤怠」エンジニアの 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 build
と nuxt 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
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』