# 源代码结构

# 避免有状态的单例模式

在编写只有客户端的代码的时候,我们会假设代码每次都会允许在一个干净的上下文中。然而 Node.js 服务器是长期运行的进程。当代码第一次被导入进程时,它会被执行一次然后保留在内存里。也就是说你创建了一个单例对象,它共享于每次发来的请求之间,并带有跨请求的状态污染风险。

因此,我们需要为每个请求创建一个新的 Vue 根实例。为了做到这一点,我们需要编写一个工厂函数来重复地执行,并为每个请求创建干净的应用实例:

// app.js
const { createSSRApp } = require('vue')

function createApp() {
  return createSSRApp({
    data() {
      return {
        user: 'John Doe'
      }
    },
    template: `<div>Current user is: {{ user }}</div>`
  })
}

module.exports = {
  createApp,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同时我们的服务端代码现在变成了:

// server.js
const { renderToString } = require('@vue/server-renderer')
const server = require('express')()
const { createApp } = require('src/app.js')

server.get('*', async (req, res) => {
  const app = createApp()

  const appContent = await renderToString(app)
  const html = `
  <html>
    <body>
      <h1>My First Heading</h1>
      <div id="app">${appContent}</div>
    </body>
  </html>
  `

  res.end(html)
})

server.listen(8080)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

同理其它实例 (诸如路由器或 store) 也是一样的。取代从一个模块直接导出路由器或 store 并将它们导入应用的,是在 createApp 创建一个干净的实例并从这个 Vue 根实例注入它们。

# 介绍构建步骤

截至目前,我们尚未讨论如何向客户端传递相同的 Vue 应用。为了做到这一点,我们需要使用 webpack 打包 Vue 应用。

  • 我们需要用 webpack 处理服务端代码。例如 .vue 文件需要被 vue-loader 处理,很多 webpack 特有的功能,诸如通过 vue-loader 导入文件或通过 css-loader 导入 CSS 在 Node.js 中都不会直接工作。

  • 类似地,我们需要分隔客户端构建,因为尽管最新版本的 Node.js 完全支持 ES2015 特性,但旧浏览器仍然需要对代码进行转译。

因此,基本的想法是,使用 webpack 同时打包客户端和服务端应用。服务端的包会被引入到服务端用来渲染 HTML,同时客户端的包会被送到浏览器用于 hydrate 静态标记。

架构

我们会在稍后的章节讨论设置的细节——现在,让我们先假设我们已经完成了构建的设置,且我们可以基于 webpack 编写 Vue 应用。

# 使用 webpack 的目录结构

现在我们使用 webpack 同时处理服务端和客户端应用,源代码的主体可以以通用的方式编写,支持所有的 webpack 特性。同时,当编写通用的代码时你需要注意一些事情。

一个简单的项目形如:

src
├── components
│   ├── MyUser.vue
│   └── MyTable.vue
├── App.vue
├── app.js # 通用入口
├── entry-client.js # 只在浏览器中运行
└── entry-server.js # 只在服务器运行
1
2
3
4
5
6
7
8

# app.js

app.js 是应用的通用入口。在只有客户端的应用里,我们会在此创建 Vue 应用实例并直接挂载到 DOM。然而,对于 SSR 来说该职责被转移到了只在客户端里运行的入口文件。app.js 的职责则变为了创建一个应用实例并导出它:

import { createSSRApp } from 'vue'
import App from './App.vue'

// 导出一个创建根组件的工厂函数
export default function(args) {
  const app = createSSRApp(App)

  return {
    app
  }
}
1
2
3
4
5
6
7
8
9
10
11

# entry-client.js

此客户端入口会使用根组件创建应用并挂载到 DOM:

import createApp from './app'

// client-specific bootstrapping logic...

const { app } = createApp({
  // here we can pass additional arguments to app factory
})

// this assumes App.vue template root element has `id="app"`
app.mount('#app')
1
2
3
4
5
6
7
8
9
10

# entry-server.js

服务端入口使用了一个默认导出,它是一个可以为每次渲染重复调用的函数。目前它除了返回应用实例并不会做其它事情——但稍后我们会在这里处理服务端路由匹配和数据预获取逻辑。

import createApp from './app'

export default function() {
  const { app } = createApp({
    /*...*/
  })

  return {
    app
  }
}
1
2
3
4
5
6
7
8
9
10
11