vue ssr+koa2構(gòu)建服務(wù)端渲染的示例代碼
之前做了活動(dòng)投放頁(yè)面在百度、360等渠道投放,采用 koa2 + 模版引擎的方式。發(fā)現(xiàn)幾個(gè)問(wèn)題
- 相較于框架開(kāi)發(fā)頁(yè)面效率較低,維護(hù)性差
- 兼容性問(wèn)題,在頁(yè)面中添加埋點(diǎn)后發(fā)現(xiàn)有些用戶的數(shù)據(jù)拿不到,排查后發(fā)現(xiàn)通過(guò)各個(gè)渠道過(guò)來(lái)的用戶的設(shè)備中仍然包含大量低版本的瀏覽器。
服務(wù)端渲染
服務(wù)端渲染和單頁(yè)面渲染區(qū)別
查看下面兩張圖,可以看到如果是服務(wù)端渲染,那么在瀏覽器中拿到的直接是完整的 html 結(jié)構(gòu)。而單頁(yè)面是一些 script 標(biāo)簽引入的js文件,最終將虛擬dom去掛在到 #app 容器上。
@vue/cli 4 來(lái)構(gòu)建項(xiàng)目結(jié)構(gòu)
下面代碼使用最精簡(jiǎn)的實(shí)例完整代碼會(huì)放到 github 上
step1 安裝最新的腳手架初始化項(xiàng)目
yarn global add @vue/cli
step2 添加服務(wù)端文件
啟動(dòng)一個(gè) web 服務(wù)下方代碼中 http://localhost:9000 就是我們最終要訪問(wèn)到地址
const Koa = require('koa')
const path = require('path')
const resolve = file => path.resolve(__dirname, file)
const app = new Koa()
const router = require('./router')
const port = 9000
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
module.exports = app
這里只是啟動(dòng)了服務(wù),我們需要在去讀取服務(wù)端和客戶端到文件,下面代碼就是服務(wù)端渲染的關(guān)鍵步驟
const fs = require('fs')
const path = require('path')
const send = require('koa-send')
const Router = require('koa-router')
const router = new Router()
// 獲取當(dāng)前文件的絕對(duì)路徑
const resolve = file => path.resolve(__dirname, file)
const { createBundleRenderer } = require('vue-server-renderer')
const bundle = require('../dist/vue-ssr-server-bundle.json')
const clientManifest = require('../dist/vue-ssr-client-manifest.json')
// 創(chuàng)建一個(gè) BunleRender 實(shí)例用于 renderer.renderToString 將 bundle 渲染為字符串
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync(resolve('../src/index.temp.html'), 'utf-8'),
clientManifest: clientManifest
})
const handleRequest = async ctx => {
ctx.res.setHeader('Content-Type', 'text/html')
// 在 2.5.0+ 版本中,此 callback 回調(diào)函數(shù)是可選項(xiàng)。在不傳遞 callback 時(shí),此方法返回一個(gè) Promise 對(duì)象,在其 resolve 后返回最終渲染的 HTML。
ctx.body = await renderer.renderToString(Object.assign({}, ctx.state.deliver, { url }))
}
router.get('/home',handleRequest)
module.exports = router
vue-server-render 提供一個(gè)名為 createBundleRenderer 的 API 使用方法如下
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推薦
template, // (可選)頁(yè)面模板
clientManifest // (可選)客戶端構(gòu)建 manifest
})
通過(guò)上面的 createBundleRenderer 方法生產(chǎn) render 對(duì)象最終將 bunlde 渲染為字符串,將最終的 html 返回給客戶端。
bundleRenderer.renderToString([context, callback]): ?Promise<string>
step3 添加 entry-client.js,entry-server.js 入口文件
在 src 中除了這兩個(gè)入口文件,其他的文件都是在客戶端和服務(wù)端公用的。來(lái)看下這兩個(gè)入口文件中分別干了什么。
大體的流程就是:服務(wù)端創(chuàng)建 vue 實(shí)例,將頁(yè)面中的異步請(qǐng)求的數(shù)據(jù)拿到存儲(chǔ)在容器中 --> 客戶端接收到服務(wù)端發(fā)送的 html 以激活模式進(jìn)行掛載,自動(dòng)給根元素 #app 上添加 data-server-rendered="true" 特殊屬性
main.js
import Vue from 'vue'
import App from './App.vue'
...
export function createApp() {
// ...
const app = new Vue({
router,
store,
render: h => h(App)
})
return { app, router, store }
}
entry-server.js
import { createApp } from './main.js'
export default context => {
// 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件,所以我們將返回一個(gè) Promise,
// 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前,
// 就已經(jīng)準(zhǔn)備就緒。
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
// 設(shè)置服務(wù)器端 router 的位置
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數(shù)解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,執(zhí)行 reject 函數(shù),并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
Promise.all(
matchedComponents.map(component => {
if (component.asyncData) {
return component.asyncData({
store,
context,
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。
// 否則會(huì)導(dǎo)致客戶端和服務(wù)端數(shù)據(jù)不統(tǒng)一造成渲染錯(cuò)誤
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
entry-client.js
import { createApp } from './main'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = prevMatched[i] !== c)
})
if (!activated.length) {
return next()
}
Promise.all(
activated.map(component => {
if (component.asyncData) {
component.asyncData({
store,
route: to
})
}
})
)
.then(() => {
next()
})
.catch(next)
})
app.$mount('#app')
})
最后
完整代碼參考 github地址
順便貼上這張圖

到此這篇關(guān)于vue ssr+koa2構(gòu)建服務(wù)端渲染的示例代碼的文章就介紹到這了,更多相關(guān)vue ssr koa2 服務(wù)端渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Egg Vue SSR 服務(wù)端渲染數(shù)據(jù)請(qǐng)求與asyncData
- 15分鐘學(xué)會(huì)vue項(xiàng)目改造成SSR(小白教程)
- Vue SSR 即時(shí)編譯技術(shù)的實(shí)現(xiàn)
- Vue使用預(yù)渲染代替SSR的方法
- 如何構(gòu)建 vue-ssr 項(xiàng)目的方法步驟
- vuecli項(xiàng)目構(gòu)建SSR服務(wù)端渲染的實(shí)現(xiàn)
- vue的ssr服務(wù)端渲染示例詳解
- vue中vue-router的使用說(shuō)明(包括在ssr中的使用)
- 關(guān)于VueSSR的一些理解和詳細(xì)配置
- Vue.js?狀態(tài)管理及?SSR解析
相關(guān)文章
Vue中在新窗口打開(kāi)頁(yè)面及Vue-router的使用
這篇文章主要介紹了Vue中在新窗口打開(kāi)頁(yè)面 及 Vue-router的使用,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
vue數(shù)據(jù)更新了但在頁(yè)面上沒(méi)有顯示出來(lái)的解決方法
有時(shí)候 vue 無(wú)法監(jiān)聽(tīng)到數(shù)據(jù)的變化,導(dǎo)致數(shù)據(jù)變化但是視圖沒(méi)有變化,也就是數(shù)據(jù)更新了,但在頁(yè)面上沒(méi)有顯示出來(lái),所以本文給出了三種解決方法,通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12
Vue入門(mén)實(shí)戰(zhàn)之天氣預(yù)報(bào)
這篇文章主要為大家詳細(xì)介紹了Vue入門(mén)實(shí)戰(zhàn)之天氣預(yù)報(bào),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
Vue與.net?Core?接收List<T>泛型參數(shù)
這篇文章主要介紹了Vue與.net?Core?接收List<T>泛型參數(shù),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-04-04

