• 源码结构
    • 避免状态单例
    • 介绍构建步骤
    • 使用 webpack 的源码结构
      • app.js
      • entry-client.js:
      • entry-server.js:

    源码结构

    避免状态单例

    当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。

    如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。

    因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:

    1. // app.js
    2. const Vue = require('vue')
    3. module.exports = function createApp (context) {
    4. return new Vue({
    5. data: {
    6. url: context.url
    7. },
    8. template: `<div>访问的 URL 是: {{ url }}</div>`
    9. })
    10. }

    并且我们的服务器代码现在变为:

    1. // server.js
    2. const createApp = require('./app')
    3. server.get('*', (req, res) => {
    4. const context = { url: req.url }
    5. const app = createApp(context)
    6. renderer.renderToString(app, (err, html) => {
    7. // 处理错误……
    8. res.end(html)
    9. })
    10. })

    同样的规则也适用于 router、store 和 event bus 实例。你不应该直接从模块导出并将其导入到应用程序中,而是需要在 createApp 中创建一个新的实例,并从根 Vue 实例注入。

    在使用带有 { runInNewContext: true } 的 bundle renderer 时,可以消除此约束,但是由于需要为每个请求创建一个新的 vm 上下文,因此伴随有一些显著性能开销。

    介绍构建步骤

    到目前为止,我们还没有讨论过如何将相同的 Vue 应用程序提供给客户端。为了做到这一点,我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:

    • 通常 Vue 应用程序是由 webpack 和 vue-loader 构建,并且许多 webpack 特定功能不能直接在 Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS)。

    • 尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。

    所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。

    架构

    我们将在后面的章节讨论规划结构的细节 - 现在,先假设我们已经将构建过程的规划都弄清楚了,我们可以在启用 webpack 的情况下编写我们的 Vue 应用程序代码。

    使用 webpack 的源码结构

    现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些事项要牢记在心。

    一个基本项目可能像是这样:

    1. src
    2. ├── components
    3. ├── Foo.vue
    4. ├── Bar.vue
    5. └── Baz.vue
    6. ├── App.vue
    7. ├── app.js # 通用 entry(universal entry)
    8. ├── entry-client.js # 仅运行于浏览器
    9. └── entry-server.js # 仅运行于服务器

    app.js

    app.js 是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简单地使用 export 导出一个 createApp 函数:

    1. import Vue from 'vue'
    2. import App from './App.vue'
    3. // 导出一个工厂函数,用于创建新的
    4. // 应用程序、router 和 store 实例
    5. export function createApp () {
    6. const app = new Vue({
    7. // 根实例简单的渲染应用程序组件。
    8. render: h => h(App)
    9. })
    10. return { app }
    11. }

    entry-client.js:

    客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:

    1. import { createApp } from './app'
    2. // 客户端特定引导逻辑……
    3. const { app } = createApp()
    4. // 这里假定 App.vue 模板中根元素具有 `id="app"`
    5. app.$mount('#app')

    entry-server.js:

    服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。

    1. import { createApp } from './app'
    2. export default context => {
    3. const { app } = createApp()
    4. return app
    5. }