Vue SSR 源碼結構

2021-01-07 15:26 更新

避免狀態(tài)單例

當編寫純客戶端 (client-only) 代碼時,我們習慣于每次在新的上下文中對代碼進行取值。但是,Node.js 服務器是一個長期運行的進程。當我們的代碼進入該進程時,它將進行一次取值并留存在內存中。這意味著如果創(chuàng)建一個單例對象,它將在每個傳入的請求之間共享。

如基本示例所示,我們為每個請求創(chuàng)建一個新的根 Vue 實例。這與每個用戶在自己的瀏覽器中使用新應用程序的實例類似。如果我們在多個請求之間使用一個共享的實例,很容易導致交叉請求狀態(tài)污染 (cross-request state pollution)。

因此,我們不應該直接創(chuàng)建一個應用程序實例,而是應該暴露一個可以重復執(zhí)行的工廠函數,為每個請求創(chuàng)建新的應用程序實例:

  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. }

并且我們的服務器代碼現在變?yōu)椋?/p>

  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. })

同樣的規(guī)則也適用于 router、store 和 event bus 實例。你不應該直接從模塊導出并將其導入到應用程序中,而是需要在 createApp 中創(chuàng)建一個新的實例,并從根 Vue 實例注入。

在使用帶有 { runInNewContext: true } 的 bundle renderer 時,可以消除此約束,但是由于需要為每個請求創(chuàng)建一個新的 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」會發(fā)送給瀏覽器,用于混合靜態(tài)標記。

架構

我們將在后面的章節(jié)討論規(guī)劃結構的細節(jié) - 現在,先假設我們已經將構建過程的規(guī)劃都弄清楚了,我們可以在啟用 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」。在純客戶端應用程序中,我們將在此文件中創(chuàng)建根 Vue 實例,并直接掛載到 DOM。但是,對于服務器端渲染(SSR),責任轉移到純客戶端 entry 文件。app.js 簡單地使用 export 導出一個 createApp 函數:

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. // 導出一個工廠函數,用于創(chuàng)建新的
  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 只需創(chuàng)建應用程序,并且將其掛載到 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 導出函數,并在每次渲染中重復調用此函數。此時,除了創(chuàng)建和返回應用程序實例之外,它不會做太多事情 - 但是稍后我們將在此執(zhí)行服務器端路由匹配 (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. }
以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號