ソースコードの構造

ステートフルなシングルトンの回避

クライアントのみのコードを書くとき、私たちはコードが毎回新しいコンテキストで評価されるという事実に慣れています。しかし、 Node.js サーバーは長時間実行されるプロセスです。私たちのコードがプロセスに要求されるとき、それは一度評価されメモリにとどまります。つまりシングルトンのオブジェクトを作成したとき、それは全ての受信リクエスト間でシェアされると言うことです。

以下の基本的な例を見て分かるように、私たちは リクエストごとに新しいルート Vue インスタンスを作成します。それは各ユーザがそれぞれのブラウザでアプリケーションの新しいインスタンスを使用することに似ています。もし私たちが複数のリクエストをまたいでインスタンスを共有すると、それは容易にクロスリクエスト状態の汚染につながるでしょう。

そのため、直接アプリケーションのインスタンスを作成するのではなく、各リクエストで繰り返し実行される新しいアプリケーションのインスタンスを作成するファクトリ関数を公開する必要があります:

// app.js
const Vue = require('vue')

module.exports = function createApp (context) {
  return new Vue({
    data: {
      url: context.url
    },
    template: `<div>The visited URL is: {{ url }}</div>`
  })
}

そして私たちのサーバーコードはこうなります:

// server.js
const createApp = require('./app')

server.get('*', (req, res) => {
  const context = { url: req.url }
  const app = createApp(context)

  renderer.renderToString(app, (err, html) => {
    // エラーを処理 ...
    res.end(html)
  })
})

同じルールがルータ、ストア、イベントバスのインスタンスに適用されます。モジュールから直接エクスポートしアプリケーションにインポートするのでは無く、 createApp で新しいインスタンスを作成し、ルート (root) Vue インスタンスから注入する必要があります。

{ runInNewContext: true } でバンドルレンダラを使用するとき、その制約を取り除くことが可能です。しかし各リクエストに対して新しい VM コンテキストを作成する必要があるため、いくらか重大なパフォーマンスコストがかかります。

ビルドステップの紹介

これまでは、同じ Vue アプリケーションをクライアントへ配信する方法を論じてはいませんでした。これを行うには、webpack を使用して Vue アプリケーションをバンドルする必要があります。実際、webpack を使用して Vue アプリケーションをサーバーにバンドルしたいと思っているのはおそらく次の理由によるものです。

  • 典型的な Vue アプリケーションは webpack と vue-loader によってビルドされ、 file-loader 経由でのファイルのインポートやcss-loader 経由でCSSをインポートなどの多くの webpack 固有の機能は Node.jsで直接動作しません。

  • Node.jsの最新バージョンはES2015の機能を完全にサポートしていますが、古いブラウザに対応するためにクライアントサイドのコードをトランスパイルする必要があります。これはビルドステップにも再び関係します。

従って基本的な考え方は webpack を使用してクライアントとサーバー両方をバンドルすることです。サーバーバンドルはサーバーによって SSR のために要求され、クライアントバンドルは静的なマークアップのためにブラウザに送信されます。

architecture

セットアップの詳細については次のセクションで議論されます。今のところ、ビルドのセットアップが分かっていると仮定すると、webpack を有効にして Vue アプリケーションコードを書くことが可能になっています。

webpackによるコード構造

webpack を使用してサーバーとクライアントのアプリケーションを処理しているので、ソースコードの大部分はユニバーサルに書かれており、すべての webpack の機能にアクセスできます。 同時に、ユニバーサルなコードを書くときに留意すべきいくつかあります。

シンプルなプロジェクトは以下のようになります:

src
├── components
│   ├── Foo.vue
│   ├── Bar.vue
│   └── Baz.vue
├── App.vue
├── app.js # 共通のエントリ
├── entry-client.js # ブラウザでのみ実行されます
└── entry-server.js # サーバでのみ実行されます

app.js

app.js はアプリケーションのユニバーサルエントリーです。クライアントアプリケーションでは、このファイルにルート Vue インスタンスを作成し、DOM に直接マウントします。しかし、SSRの場合は責務はクライアント専用のエントリファイルに映されます。app.js はシンプルに createApp 関数をエクスポートします:

import Vue from 'vue'
import App from './App.vue'

// 新しいアプリケーション、ルータ、ストアを作成するためのファクトリ関数をエクスポートします
// インスタンス
export function createApp () {
  const app = new Vue({
    // ルートインスタンスは単に App コンポーネントを描画します
    render: h => h(App)
  })
  return { app }
}

entry-client.js:

クライアントエントリは単にアプリケーションを作成しそれをDOMにマウントします:

import { createApp } from './app'

// クライアント固有の初期化ロジック

const { app } = createApp()
// これは App.vue テンプレートのルート要素が id="app" だからです。
app.$mount('#app')

entry-server.js:

サーバーエントリは描画ごとに繰り返し呼び出すことができる関数をデフォルトでエクスポートします。現時点では、アプリケーションインスタンスを作成して返す以外のことはほとんど行いませんが、後でサーバーサイドのルートマッチングとデータプリフェッチロジックを実行します。

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}