vuejs 頁面渲染_Vue SSR服務(wù)端渲染改造踩坑指南

本文將對專欄《從0到1 實(shí)戰(zhàn)朋友圈移動(dòng)Web App開發(fā)》涉及的實(shí)戰(zhàn)項(xiàng)目進(jìn)行Vue的SSR改造,在線體驗(yàn)地址,建議使用手機(jī)瀏覽器體驗(yàn):

客戶端渲染地址:https://app.nihaoshijie.com.cn/ SSR渲染地址:https://app.nihaoshijie.com.cn/index_ssr

本文是對改造中踩的坑進(jìn)行總結(jié)和梳理,至于改造的步驟,強(qiáng)烈建議直接參考官方文檔指南:https://ssr.vuejs.org/zh/ 如果在改造中遇到任何問題并難以找到解決辦法,不妨試試在文中找到答案。

版本匹配
Vue的SSR渲染,可以當(dāng)作一個(gè)全新的項(xiàng)目,需要安裝依賴的模塊(node_modules),可以將原先使用vue cli 3創(chuàng)建的項(xiàng)目的package.json拷貝過來,確保不缺少相關(guān)模塊,然后在此基礎(chǔ)上添加SSR需要的模塊。

主要是vue-server-renderer:

npm install vue vue-server-renderer --save
vue-server-renderer是SSR渲染的核心,提供bundle renderer來調(diào)用renderToString()方法將Vue組件渲染成HTML字符串,需要注意的是vue-server-renderer 和 vue 必須匹配版本,例如@2.6.11版本的vue必須對應(yīng)@2.6.11版本的vue-server-renderer 。

路由模式history
采用了vue-router的Vue的SSR渲染,必須使用history作為路由模式,因?yàn)閔ash模式的路由提交不到服務(wù)器上,如果之前使用的是hash模式,需要進(jìn)行修改:

const router = new Router({
mode: 'history',
...
})
兩個(gè)入口
Vue的SSR渲染,一般都會(huì)是同構(gòu)的,也就是業(yè)務(wù)代碼是一套,通過不同的構(gòu)建配置,來分別構(gòu)建客戶端client和服務(wù)端server,對webpack構(gòu)建而言,這就需要有兩個(gè)入口,修改vue.config.js來支持,代碼如下:

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'

const target = TARGET_NODE ? 'server' : 'client'
...
configureWebpack: {
// 將 entry 指向應(yīng)用程序的 server / client 文件
entry: ./src/entry-${target}.js,
// 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動(dòng)態(tài)導(dǎo)入(dynamic import),
// 并且還會(huì)在編譯 Vue 組件時(shí),
// 告知 vue-loader 輸送面向服務(wù)器代碼(server-oriented code)。
target: TARGET_NODE ? 'node' : 'web',
// node: TARGET_NODE? undefined : false,
// 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports)
output: {
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
// devtool: 'source-map',
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應(yīng)用程序依賴模塊??梢允狗?wù)器構(gòu)建速度更快,
// 并生成較小的 bundle 文件。
externals: TARGET_NODE ? nodeExternals({
// 不要外置化 webpack 需要處理的依賴模塊。
// 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應(yīng)該將修改 global(例如 polyfill)的依賴模塊列入白名單
whitelist: /.css$/
}) : undefined,
optimization: {
splitChunks: false
},
},
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return merge(options, {
optimizeSSR: false //https://vue-loader-v14.vuejs.org/zh-cn/options.html#optimizeSSR
})
})
},
...
其中,分別使用entry-client.js和entry-server.js作為entry入口即可,如果需要一次命令同時(shí)執(zhí)行兩個(gè)構(gòu)建,可以修改package.json如下:

"scripts": {
"build": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build && vue-cli-service build"
},
對于開發(fā)模式的SSR構(gòu)建,由于使用不多,這里只提供生產(chǎn)模式的SSR構(gòu)建,執(zhí)行如下命令:

npm run build:server
bundle文件
Vue的SSR渲染服務(wù)端啟動(dòng)時(shí),bundle renderer主要解析兩個(gè)bundle文件,分別是:vue-ssr-server-bundle.json和vue-ssr-client-manifest.json文件,這兩個(gè)文件分別由webpack構(gòu)建生成,可以修改之前的vue.config.js配置文件:

configureWebpack: {
...
plugins: [
TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()
],
...
},
其中vue-ssr-server-bundle.json主要存放的是資源的映射信息,是bundle renderer必須的,而vue-ssr-client-manifest.json是對象clientManifest配置項(xiàng),此對象包含了 webpack 整個(gè)構(gòu)建過程的信息,從而可以讓 bundle renderer自動(dòng)推導(dǎo)需要在HTML模板中注入的內(nèi)容,從而實(shí)現(xiàn)最佳的預(yù)加載(preload)和預(yù)取(prefetch)資源,如下圖所示:

模擬window對象
Vue的SSR渲然Vue組件(包括根組件App.vue和其若干個(gè)子組件)時(shí),由于沒有動(dòng)態(tài)更新,所有的組件生命周期鉤子函數(shù)中,只有 beforeCreate 和 created 會(huì)在SSR渲染過程中被調(diào)用。這就是說任何其他生命周期鉤子函數(shù)中的代碼(例如 beforeMount 或 mounted),只會(huì)在客戶端執(zhí)行。 此外還需要注意的是,應(yīng)該避免在 beforeCreate 和 created 生命周期時(shí)產(chǎn)生全局副作用的代碼,例如在其中使用 setInterval 設(shè)置 timer。在純客戶端 (client-side only) 的代碼中,我們可以設(shè)置一個(gè) timer,然后在 beforeDestroy 或 destroyed 生命周期時(shí)將其銷毀。但是,由于在 SSR 期間并不會(huì)調(diào)用銷毀鉤子函數(shù),所以 timer 將永遠(yuǎn)保留下來。為了避免這種情況,請將副作用代碼移動(dòng)到 beforeMount 或 mounted 生命周期中。 對于SSR渲染,由于采用的Node.js環(huán)境,所以需要對于window對象做兼容處理,這里推薦使用jsdom:

npm install jsdom --save
然后,在SSR的入口文件service.js中(非entry-server.js),添加如下代碼:

const jsdom = require('jsdom')
const { JSDOM } = jsdom

/* 模擬window對象邏輯 */
const resourceLoader = new jsdom.ResourceLoader({
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
});// 設(shè)置UA
const dom = new JSDOM('', {
url:'https://app.nihaoshijie.com.cn/index.html',
resources: resourceLoader
});

global.window = dom.window
global.document = window.document
global.navigator = window.navigator
window.nodeis = true //給window標(biāo)識出node環(huán)境的標(biāo)志位
/* 模擬window對象邏輯 */
normalizeFile get undefined file錯(cuò)誤
當(dāng)運(yùn)營SSR渲染,遇到如下錯(cuò)誤時(shí):

該錯(cuò)誤通常會(huì)導(dǎo)致vue-ssr-client-manifest.json文件中的映射信息異常,導(dǎo)致無法正確找到對應(yīng)的資源文件,可能屬于VueSSRClientPlugin()的bug,但是可以通過webpack配置來進(jìn)行規(guī)避,修改vue.config.js,添加代碼如下:

css: {
sourceMap: true
}
相關(guān)的issue地址。

在服務(wù)端請求SSR首屏數(shù)據(jù)
對于一些首屏非靜態(tài)頁面的場景,這些頁面的渲染依賴后端數(shù)據(jù),所以在SSR端進(jìn)行數(shù)據(jù)的拉取,并且在SSR渲染完成之后,將數(shù)據(jù)直接帶給客戶端進(jìn)行二次渲染,減少請求的次數(shù),所以對于數(shù)據(jù)共享的方案,最合適的方式是通過Vuex的Store完成,所以這里推薦項(xiàng)目使用Vuex,首先修改entry-server.js代碼如下:

// 對所有匹配的路由組件調(diào)用 asyncData()
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {

      return Component.asyncData({
        store,
        route: router.currentRoute
      })
    }
  })).then(() => {
    // 在所有預(yù)取鉤子(preFetch hook) resolve 后,
    // 我們的 store 現(xiàn)在已經(jīng)填充入渲染應(yīng)用程序所需的狀態(tài)。
    // 當(dāng)我們將狀態(tài)附加到上下文,
    // 并且 `template` 選項(xiàng)用于 renderer 時(shí),
    // 狀態(tài)將自動(dòng)序列化為 `window.__INITIAL_STATE__`,并注入 HTML。
    context.state = store.state

    resolve(app)
  }).catch((err)=>{
    console.error(err)
    reject(err)
  })

同時(shí)給首屏的第一個(gè)路由組件添加asyncData方法來請求數(shù)據(jù),注意是組件的靜態(tài)方法,而非在methods中定義的方法,代碼如下:

export default {
name: 'wecircle',
...
asyncData ({ store }) {
// 觸發(fā) action 后,會(huì)返回 Promise
return store.dispatch('setWecircleDataListSSR')
},
...
}
后面的action和mutation按照正常邏輯寫即可,最后,當(dāng)SSR數(shù)據(jù)渲染完成后,會(huì)在生成的HTML中添加一個(gè)window.INITIAL_STATE對象,修改entry-client.js可以將數(shù)據(jù)直接賦值給客戶端渲染,代碼如下:

const { app, router, store } = createApp()

if (window.INITIAL_STATE) {
store.replaceState(window.INITIAL_STATE)
}
最后一點(diǎn)需要注意是由于客戶端和服務(wù)端代碼是同構(gòu)的,但是服務(wù)端請求數(shù)據(jù)的地址和客戶端是完全不一樣的,所以針對這種場景,需要利用之前設(shè)置的window.nodejs標(biāo)志位來判斷在不同場景下的接口地址,從而區(qū)分對待。

cookie透傳
當(dāng)在SSR端請求數(shù)據(jù)時(shí),可能會(huì)需要帶上瀏覽器的cookie,在客戶端到SSR服務(wù)器的請求中,客戶端是攜帶有cookie數(shù)據(jù)的。但是在SSR服務(wù)器請求后端接口的過程中,卻是沒有相應(yīng)的cookie數(shù)據(jù)的。因此在SSR服務(wù)器進(jìn)行接口請求的時(shí)候,我們需要手動(dòng)拿到客戶端的cookie傳給后端服務(wù)器。這里如果使用是axios,就可以手動(dòng)設(shè)置axios請求的headers字段,代碼如下: 在server.js中獲取瀏覽器cookie,并利用window對象存儲(chǔ):

app.use('*', (req, res) => {
...
window.ssr_cookie = req.cookie
...
})
在axios中,添加header將cookie塞進(jìn)去:

axios.create({
...
headers: window.ssr_cookie || {}
...
})
這樣就可以將瀏覽器的cookie帶給SSR服務(wù)器了。

No stacktrace on NavigationDuplicated error錯(cuò)誤
該錯(cuò)誤本身是由于重復(fù)的點(diǎn)擊相同的導(dǎo)航組件,會(huì)報(bào)錯(cuò)NavigationDuplicated,由于SSR渲染會(huì)使用hostory模式,所以在第一次進(jìn)入路由時(shí),會(huì)經(jīng)過多次導(dǎo)航(可以通過給router添加beforeEach鉤子可以看到),所以也會(huì)報(bào)NavigationDuplicated錯(cuò)誤,本身這個(gè)錯(cuò)誤不影響使用,但是如果需要規(guī)避,可以采用如下代碼,在router.js中添加:

const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
相關(guān)的issue地址。

同時(shí)支持客戶端渲染和服務(wù)端渲染
既然使用了Vue的SSR渲染,那么首先需要考慮的就是SSR服務(wù)的穩(wěn)定性,所以為了最大程度的保證服務(wù)可用,當(dāng)服務(wù)端渲染掛掉時(shí),需要有容錯(cuò)邏輯保證頁面可用,所以,原先的客戶端渲染相關(guān)的構(gòu)建要保留,即通過直接訪問index.html的方式能夠正常使用頁面,這里直接通過nginx配置路徑轉(zhuǎn)發(fā),代碼如下:

location /index.html {
return 301 https://$server_name/;
}
即將原先的通過http://www.abc.com/index.html訪問的地址轉(zhuǎn)發(fā)到http://www.abc.com/,這樣就能夠觸發(fā)采用history模式的vue-router的path="/"的路由,對于客戶端訪問和服務(wù)的訪問,分別配置不同的轉(zhuǎn)發(fā),如下:

客戶端渲染服務(wù)

location / {
# 給靜態(tài)文件添加緩存
location ~ ..(js|css|png|jpeg)(.) {
valid_referers *.nihaoshijie.com.cn;
if ($invalid_referer) {
return 404;
}
proxy_pass http://localhost:8080;
expires 3d;# 3天
}
proxy_pass http://localhost:8080; # 靜態(tài)資源走8080端口
}

ssr服務(wù)

location = /index_ssr {
proxy_pass http://localhost:8888; # ssr服務(wù)使用8888端口
}
只保留/index_ssr作為SSR渲染的入口,然后在server.js中,將/index_ssr處理成首頁的路徑,并添加對SSR渲染的容錯(cuò)邏輯,代碼如下:

if (req.originalUrl === '/index_ssr' || req.originalUrl === '/index_ssr/') {
context.url = '/'
}
...
renderer(bundle, manifest).renderToString(context, (err, html) => {
...
if (err) {
// 發(fā)現(xiàn)報(bào)錯(cuò),直接走客戶端渲染
res.redirect('/')
// 記錄錯(cuò)誤信息 這部分內(nèi)容可以上傳到日志平臺 便于統(tǒng)計(jì)
console.error(error during render : ${req.url})
console.error(err)
}
...
})
針對服務(wù)端渲染的容錯(cuò)機(jī)制,不限于使用上面介紹的方案,也可以自行根據(jù)實(shí)際場景來解決。

PWA和SSR的集成
由于本項(xiàng)目使用到了PWA技術(shù),這樣在集成時(shí)需要注意PWA相關(guān)的代碼和插件只需要在entry-client.js入口的邏輯中添加即可,SSR服務(wù)端是不需要配置PWA相關(guān)的邏輯,例如之前的OfflinePlugin插件,在vue.config.js做如下配置:

if (TARGET_NODE) {
plugins.push(new VueSSRServerPlugin())
} else {
plugins.push(new VueSSRClientPlugin())

plugins.push(new OfflinePlugin({
// 要求觸發(fā)ServiceWorker事件回調(diào)
ServiceWorker: {
events: true,
// push事件邏輯寫在另外一個(gè)文件里面
entry: './public/sw-push.js'
},
// 更更新策略選擇全部更新
updateStrategy: 'all',
// 除去一些不需要緩存的文件
excludes: ['/.map', '/.svg', '/.png', '/.jpg', '/sw-push.js', '/sw-my.js','*/.json'],

  // 添加index.html的更新
  rewrites (asset) {
    if (asset.indexOf('index.html') > -1) {
      return './index.html'
    }

    return asset
  }
}))

}
總結(jié)
對于Vue的SSR渲染,本身邏輯是相對復(fù)雜的,需要同時(shí)了解客戶端和Node端的技術(shù),并且使用起來對后端服務(wù)也是有一定要求的,所以需要具體的使用場景來判斷是否使用,作為初學(xué)者,筆者建議跟著官方文檔來一步一步進(jìn)行改造,這樣能夠更加理解其中的含義,如果感興趣的話也可以了解一下Vue的SSR框架:nuxt.js

上述改造完整項(xiàng)目Github地址。
————————————————
版權(quán)聲明:本文為CSDN博主「Li小飛」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_42498206/article/details/112357085

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容