Vue SSR 源碼結(jié)構(gòu)

2021-01-07 15:26 更新

避免狀態(tài)單例

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

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

因此,我們不應(yīng)該直接創(chuàng)建一個應(yīng)用程序?qū)嵗?,而是?yīng)該暴露一個可以重復(fù)執(zhí)行的工廠函數(shù),為每個請求創(chuàng)建新的應(yīng)用程序?qū)嵗?/p>

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


module.exports = function createApp (context) {
  return new Vue({
    data: {
      url: context.url
    },
    template: `<div>訪問的 URL 是: {{ url }}</div>`
  })
}

并且我們的服務(wù)器代碼現(xiàn)在變?yōu)椋?/p>

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

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

在使用帶有 { runInNewContext: true } 的 bundle renderer 時,可以消除此約束,但是由于需要為每個請求創(chuàng)建一個新的 vm 上下文,因此伴隨有一些顯著性能開銷。

介紹構(gòu)建步驟

到目前為止,我們還沒有討論過如何將相同的 Vue 應(yīng)用程序提供給客戶端。為了做到這一點,我們需要使用 webpack 來打包我們的 Vue 應(yīng)用程序。事實上,我們可能需要在服務(wù)器上使用 webpack 打包 Vue 應(yīng)用程序,因為:

  • 通常 Vue 應(yīng)用程序是由 webpack 和 vue-loader 構(gòu)建,并且許多 webpack 特定功能不能直接在 Node.js 中運行(例如通過 file-loader 導(dǎo)入文件,通過 css-loader 導(dǎo)入 CSS)。

  • 盡管 Node.js 最新版本能夠完全支持 ES2015 特性,我們還是需要轉(zhuǎn)譯客戶端代碼以適應(yīng)老版瀏覽器。這也會涉及到構(gòu)建步驟。

所以基本看法是,對于客戶端應(yīng)用程序和服務(wù)器應(yīng)用程序,我們都要使用 webpack 打包 - 服務(wù)器需要「服務(wù)器 bundle」然后用于服務(wù)器端渲染(SSR),而「客戶端 bundle」會發(fā)送給瀏覽器,用于混合靜態(tài)標(biāo)記。

架構(gòu)

我們將在后面的章節(jié)討論規(guī)劃結(jié)構(gòu)的細(xì)節(jié) - 現(xiàn)在,先假設(shè)我們已經(jīng)將構(gòu)建過程的規(guī)劃都弄清楚了,我們可以在啟用 webpack 的情況下編寫我們的 Vue 應(yīng)用程序代碼。

使用 webpack 的源碼結(jié)構(gòu)

現(xiàn)在我們正在使用 webpack 來處理服務(wù)器和客戶端的應(yīng)用程序,大部分源碼可以使用通用方式編寫,可以使用 webpack 支持的所有功能。同時,在編寫通用代碼時,有一些事項要牢記在心。

一個基本項目可能像是這樣:

src
├── components
│?? ├── Foo.vue
│?? ├── Bar.vue
│?? └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 僅運行于瀏覽器
└── entry-server.js # 僅運行于服務(wù)器

app.js

app.js 是我們應(yīng)用程序的「通用 entry」。在純客戶端應(yīng)用程序中,我們將在此文件中創(chuàng)建根 Vue 實例,并直接掛載到 DOM。但是,對于服務(wù)器端渲染(SSR),責(zé)任轉(zhuǎn)移到純客戶端 entry 文件。app.js 簡單地使用 export 導(dǎo)出一個 createApp 函數(shù):

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


// 導(dǎo)出一個工廠函數(shù),用于創(chuàng)建新的
// 應(yīng)用程序、router 和 store 實例
export function createApp () {
  const app = new Vue({
    // 根實例簡單的渲染應(yīng)用程序組件。
    render: h => h(App)
  })
  return { app }
}

entry-client.js:

客戶端 entry 只需創(chuàng)建應(yīng)用程序,并且將其掛載到 DOM 中:

import { createApp } from './app'


// 客戶端特定引導(dǎo)邏輯……


const { app } = createApp()


// 這里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

entry-server.js:

服務(wù)器 entry 使用 default export 導(dǎo)出函數(shù),并在每次渲染中重復(fù)調(diào)用此函數(shù)。此時,除了創(chuàng)建和返回應(yīng)用程序?qū)嵗?,它不會做太多事?- 但是稍后我們將在此執(zhí)行服務(wù)器端路由匹配 (server-side route matching) 和數(shù)據(jù)預(yù)取邏輯 (data pre-fetching logic)。

import { createApp } from './app'


export default context => {
  const { app } = createApp()
  return app
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號