原文鏈接:因卓誒-簡(jiǎn)單聊聊前端渲染模式以及Nuxt3.js
前言
最近的工作有涉及到ssr琉朽,所以這篇文章算是一個(gè)總結(jié)博其,并且對(duì)還在beta階段的nuxt3做一個(gè)淺析枫耳。前段時(shí)間有一個(gè)蠻火的視頻原朝,關(guān)于rollup作者rich的一段演講桥胞,在演講里面rich梳理了ssr和csr瞻坝,并且講述了痛點(diǎn)句各,和提出新的概念“transition app”航厚,如果你有興趣可以看看這個(gè)視頻
在文章開始前,我來(lái)簡(jiǎn)單介紹一下"spa", "mpa", "ssr", "csr"......這些個(gè)名詞的意義气忠。如果你是做web前端開發(fā)的,這幾個(gè)詞可能伴隨著你的工作生涯很久很久了赋咽,相關(guān)文章互聯(lián)網(wǎng)上多如牛毛旧噪,如果你對(duì)這些概念比較模糊甚至壓根不知道,那么別關(guān)閉網(wǎng)頁(yè)脓匿,我希望這篇文章能夠拯救你淘钟。
SPA與MPA
MPA稱之為“多頁(yè)應(yīng)用”, 那么什么是多頁(yè)應(yīng)用呢?字面意思其實(shí)就是有多個(gè)頁(yè)面的應(yīng)用就是多頁(yè)應(yīng)用陪毡。從技術(shù)手段上來(lái)講米母,你可以這么粗略地理解。SPA,MPA不同點(diǎn)太多了毡琉,而且各有利弊铁瞒。
MPA應(yīng)用你需要單獨(dú)維護(hù)多個(gè)html頁(yè)面,而且我們每加載/切換一次頁(yè)面桅滋,都需要加載一整個(gè)頁(yè)面慧耍。但是它對(duì)于seo特別友好,因?yàn)槲覀兛梢越o每一個(gè)html頁(yè)面設(shè)置不同的meta等信息丐谋,從而達(dá)到更好的收錄效果芍碧;所以MPA多出現(xiàn)在大型的電商/新聞網(wǎng)站等。
不同于MPA号俐,SPA可以使得我們通過(guò)ajax或者其他技術(shù)動(dòng)態(tài)的更改某一個(gè)區(qū)域的內(nèi)容而不需要重新加載頁(yè)面泌豆,包括切換頁(yè)面也不會(huì)重新加載整個(gè)html,它對(duì)狀態(tài)的留存做的很好吏饿,而且在移動(dòng)端表現(xiàn)特別優(yōu)異(因?yàn)樵谝郧傲髁渴呛苷滟F的踪危,可以以最小的損失切換頁(yè)面蔬浙,無(wú)論是用戶體驗(yàn)還是成本相較于MPA都是極大的改善)
SSR
在我們web較早的時(shí)候,開發(fā)者喜歡使用jsp或者其他模板渲染引擎來(lái)構(gòu)造一個(gè)應(yīng)用陨倡。我們一般稱之為SSR(服務(wù)端渲染) 它的大致架構(gòu)是如下這個(gè)樣子
用戶發(fā)起一個(gè)請(qǐng)求抵達(dá)后端服務(wù)器后:
- 后端會(huì)將用戶所需要的內(nèi)容通過(guò)數(shù)據(jù)層進(jìn)行查詢
- 處理業(yè)務(wù)
- 通過(guò)模板來(lái)拼接頁(yè)面
- 返回一個(gè)html字符串給客戶端
- 前端渲染然后加載js腳本完成剩余交互
你可能也發(fā)現(xiàn)了敛滋,在SSR服務(wù)端渲染中,前端負(fù)責(zé)的東西太過(guò)單薄兴革,說(shuō)得好聽叫交互绎晃,難聽點(diǎn)就是“點(diǎn)擊事件工程師”。所以老一輩的后端基本人人都會(huì)前端杂曲,js的水平高的一抓一大把庶艾。隨著使用SSR渲染頁(yè)面的應(yīng)用越來(lái)越多,弊端也出現(xiàn)了:
- 后端做了太多事情了擎勘,再牛逼的人也吃不消
- 前后端耦合咱揍,維護(hù)難度升級(jí)
- 內(nèi)容更新/跳轉(zhuǎn),都需要重新加載一次頁(yè)面
- 服務(wù)端渲染成本很高
- ...
CSR
CSR(客戶端渲染)大致是以下的架構(gòu):
CSR架構(gòu)更貼近我們的現(xiàn)代前端開發(fā)棚饵,我們一般使用VUE, REACT這一類的前端視圖框架時(shí)煤裙,都是默認(rèn)CSR體系的。大致的流程是下面這樣子的:
- 瀏覽器向前端服務(wù)器請(qǐng)求html和js噪漾,html頁(yè)面是空html硼砰,并且同時(shí)執(zhí)行js
- js渲染頁(yè)面
- 通過(guò)后端暴露的api進(jìn)行交互
SSR和CSR的區(qū)別
可以發(fā)現(xiàn),使用CSR進(jìn)行開發(fā)欣硼,會(huì)有幾個(gè)明顯的缺點(diǎn)
SEO
因?yàn)閺那岸朔?wù)器獲取的html最開始是空html题翰,這非常不利于seo,很多搜索引擎的老版本蜘蛛會(huì)直接爬頁(yè)面诈胜,不會(huì)等待js加載完豹障,所以會(huì)直接爬出來(lái)一個(gè)空頁(yè)面。盡管現(xiàn)在的百度焦匈,谷歌等搜索引擎的爬蟲能力很強(qiáng)血公,能夠部分支持CSR SPA頁(yè)面,SEO效果雖然可以其他方式彌補(bǔ) (比如加入meta標(biāo)簽等等); 但是我們使用SSR完全不用擔(dān)心缓熟,因?yàn)楂@得的html頁(yè)面是一個(gè)完整的坞笙,可以直接渲染的。
用戶體驗(yàn)(白屏)
關(guān)于白屏荚虚,由于CSR從HTML構(gòu)建完成到JS渲染頁(yè)面完成(但還沒(méi)呈現(xiàn)頁(yè)面)這一段過(guò)程中薛夜,是處于一個(gè)白屏的時(shí)間,用戶體驗(yàn)很不好版述,反之使用SSR獲得HTML之后只需要直接構(gòu)建DOM就可以了梯澜。
同樣的,我們使用SSR還有不一樣的缺點(diǎn):
- 成本問(wèn)題(相比CSR多了構(gòu)建HTML以及獲取數(shù)據(jù),需要更多的服務(wù)器負(fù)載均衡)
- 部署問(wèn)題(與CSR部署環(huán)境不同晚伙,不是僅僅需要一個(gè)靜態(tài)文件托管服務(wù)器那么簡(jiǎn)單了)
- 代碼難度問(wèn)題
- ...
使用Vite快速構(gòu)建一個(gè)SSR(實(shí)踐SSR)
Vite SSR雖然現(xiàn)在是一個(gè)實(shí)驗(yàn)性質(zhì)吮龄,不能用于生產(chǎn)環(huán)境。但是我們可以使用Vite做一個(gè)ssr的demo咆疗,幫助我們理解SSR的構(gòu)建漓帚,理解之后我們?cè)賮?lái)引入"Nuxt", "同構(gòu)"等概念。Vite里面為SSR提供了很多支持午磁,所以我們要開發(fā)一個(gè)demo尝抖,會(huì)非常非常簡(jiǎn)單,你也可以參考這篇官網(wǎng)文檔
我們首先需要更改index.html的內(nèi)容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/entry-client"></script>
</body>
</html>
可以看到我們?cè)赼pp的div里寫了一段注釋迅皇,到時(shí)候我們渲染完之后的html將會(huì)replace這個(gè)注釋昧辽。
然后需要在根目錄新建一個(gè)server.mjs,作為我們的服務(wù)入口登颓,用express作為一個(gè)例子:
import { readFileSync } from 'fs'
import { resolve } from 'path'
import express from 'express'
import { createServer as createViteServer } from 'vite'
const createServer = async () => {
const app = express()
const vite = await createViteServer({
server: { middlewareMode: 'ssr' }
})
app.use(vite.middlewares)
app.use('*', async (req, res) => {
try {
const url = req.originalUrl
let template = readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const { render } = await vite.ssrLoadModule('./src/entry-server.js')
const appHtml = await render(url)
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (error) {
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
app.listen(3000)
}
createServer()
我們的main.js也需要更改
import App from './App.vue'
import Router from './router'
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(Router)
return { app, router: Router }
}
我們?cè)趍ain.js中却妨,從vue導(dǎo)出createSSRApp函數(shù)栋荸,并且使用router躺彬,并且返回一個(gè)對(duì)象簿姨,這個(gè)對(duì)象之后將會(huì)被entry-server引用。
那么router也和我們傳統(tǒng)的csr應(yīng)用不太一樣喇嘱,我們根據(jù)env判斷茉贡,傳入了不同的路由類型:
import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router'
const Router = createRouter({
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
routes: [
{
name: 'index',
path: '/index',
component: () => import('../pages/index.vue')
}
]
})
export default Router
然后我們需要在src中新建 entry-client.js(會(huì)被index.html引入) 以及 entry-server.js
import { createApp } from './main'
const { app, router } = createApp()
router.isReady().then(() => {
app.mount('#app')
})
import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'
export const render = async (url) => {
try {
const { app, router } = createApp()
router.push(url)
await router.isReady()
const ctx = {}
const html = await renderToString(app, ctx)
return html
} catch (error) {
}
}
到此為止我們可以在本地啟動(dòng)一個(gè)服務(wù)器,并且可以將我們的頁(yè)面以ssr的形式渲染到瀏覽器中了婉称,由于我們的demo代碼都是esm块仆,所以我們使用node執(zhí)行构蹬,必須要寫成mjs的后綴王暗。
啟動(dòng)服務(wù)器之后,訪問(wèn)/index這個(gè)路由庄敛,你就能看到我們的頁(yè)面了
如果你的node版本不支持mjs俗壹,請(qǐng)先升級(jí)...
ssr示例項(xiàng)目:
喝水,脫水藻烤,注水(SSR)
讀到這里绷雏,你或許已經(jīng)對(duì)ssr的流程有一個(gè)粗略的了解了;那么這一part的三個(gè)例子會(huì)加深你對(duì)ssr的理解怖亭,就是ssr常常說(shuō)的喝水涎显,脫水,注水
兴猩。
我們ssr在服務(wù)端構(gòu)造頁(yè)面時(shí)期吓,數(shù)據(jù)是從數(shù)據(jù)源流下
,使得我們頁(yè)面數(shù)據(jù)得到填充倾芝,這個(gè)過(guò)程就叫做喝水
(render & beforeRender)喝水的過(guò)程就是在服務(wù)端渲染頁(yè)面做的事情讨勤,就好比下面這個(gè)圖:
飽滿的水氣球代表了一個(gè)健壯的網(wǎng)頁(yè)
我們實(shí)現(xiàn)ssr需要直出html箭跳,所以需要把結(jié)構(gòu)以及數(shù)據(jù)進(jìn)行脫水
(如圖)
然后到了客戶端,我們需要ssr應(yīng)用重新煥活潭千,就要讓原本脫水了的state,prop等等數(shù)據(jù)恢復(fù)到原來(lái)的生機(jī)谱姓,并且重新render組件,這個(gè)過(guò)程就叫做注水
SSG
SSG這種渲染模式采取了CSR和SSR的共同優(yōu)點(diǎn)刨晴,它不需要開發(fā)者介入服務(wù)器操作屉来,開發(fā)者只需要準(zhǔn)備cdn或者其他靜態(tài)網(wǎng)頁(yè)托管服務(wù)器,prerender出靜態(tài)資源這一步將在構(gòu)建時(shí)就已經(jīng)做了割捅,呈現(xiàn)在用戶眼前的雖然不是實(shí)時(shí)變更的奶躯,但是也保留了CSR和SSR的精髓,一定程度上有了平衡亿驾。但是因?yàn)閜rerender的緣故嘹黔,它和SSR的大致工作方式會(huì)相似一點(diǎn)。
也是有缺點(diǎn)的
- 隨著業(yè)務(wù)的復(fù)雜莫瞬,需要生成的頁(yè)面可能不單單只有1儡蔓,2個(gè),所以這對(duì)于構(gòu)建的要求很高
- 時(shí)效性問(wèn)題疼邀,用戶可能看到的頁(yè)面是上一次生成的喂江,所以這一部分仍需要其他模式來(lái)補(bǔ)充...
同構(gòu)SSR和CSR(共享data)
同構(gòu)說(shuō)白了,就是將我們的前端代碼旁振,既能在客戶端運(yùn)行获询,也能在服務(wù)端運(yùn)行,而且還能保持上下文的狀態(tài)拐袜,我們?cè)谏厦娴母脑炖右呀?jīng)實(shí)現(xiàn)了同一份代碼在2個(gè)端的運(yùn)行吉嚣,但是并沒(méi)有實(shí)現(xiàn)狀態(tài)的同步,比如我們?cè)趎uxt中蹬铺,使用asyncData
這類鉤子一樣尝哆,能在服務(wù)端運(yùn)行而且返回的data可以和客戶端共享。
async asyncData({ store, $axios, $oss }) {
return {
hello: "world"
}
}
我們現(xiàn)在需要改造我們的demo:
asyncData() {
return {
hello: 'message'
}
}
其次在server端將asyncData返回的對(duì)象和其他頁(yè)面html一起進(jìn)行脫水:
import { createApp } from './main'
import { renderToString } from 'vue/server-renderer'
export const render = async (url) => {
try {
const { app, router } = createApp()
router.push(url)
await router.isReady()
let data = {}
if (router.currentRoute.value.matched[0].components.default.asyncData) {
const asyncFunc = router.currentRoute.value.matched[0].components.default.asyncData
data = asyncFunc.call()
}
const html = await renderToString(app)
return { html, data }
} catch (error) {
}
}
// 我們的server.mjs也需要變更一下
app.use('*', async (req, res) => {
try {
const url = req.originalUrl
let template = readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const { render } = await vite.ssrLoadModule('./src/entry-server.js')
const { html: appHtml, data } = await render(url)
const html = template.replace(`<!--ssr-outlet-->`, `${appHtml}<script>window.__data__=${JSON.stringify(data)}</script>`)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (error) {
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
可以看到我們將data序列化到了window對(duì)象中了甜攀,接下來(lái)我們需要在client端注水的時(shí)候秋泄,把新data進(jìn)行替換
router.isReady().then(() => {
const component = router.currentRoute.value.matched[0].components.default
let _data = {}
if (typeof component.data === 'function') {
_data = component.data.call()
}
if (window.__data__) {
_data = {
..._data,
...window.__data__
}
}
component.data = () => _data
app.mount('#app')
})
這個(gè)時(shí)候我們已經(jīng)成功的看到index.vue中能夠正確的在template中打印hello
這個(gè)字段了
到這里,你就可以舉一反三规阀,使用vuex也可以進(jìn)行同步數(shù)據(jù)恒序,都是把data序列化到window中保存,然后在client掛載前重新commit到store里面就可以了谁撼。
Nuxt3
是時(shí)候引入nuxt了歧胁,我們?nèi)绻褂胣uxt將會(huì)更容易的完成ssr需求,這一部分不會(huì)教大家怎么寫nuxt,畢竟都是框架与帆,都很簡(jiǎn)單了赌。我會(huì)和大家梳理一下nuxt2和nuxt3的變化,如果你用過(guò)nuxt2玄糟,那么這一部分內(nèi)容你可能會(huì)非常感興趣勿她。寫這篇文章的時(shí)候,nuxt3并沒(méi)有release阵翎,所以到時(shí)候release后會(huì)考慮再出一篇總結(jié)逢并。
值得關(guān)注的更新內(nèi)容
- 更好的性能
- esm的支持
- vue3更好的集成,說(shuō)明我們可以使用composition api了
- vite開發(fā)服務(wù)器加持
- webpack5 支持(盡管我不用)
Nitro Engine
簡(jiǎn)單翻閱了一下文檔郭卫,和大家分享一下砍聊,在nuxt3中的新服務(wù)端引擎 Nitro Engine
, nuxt2中服務(wù)端核心使用的是connect.js,而nuxt3使用的是nuxt團(tuán)隊(duì)自研的h3框架贰军,特點(diǎn)就是具有很強(qiáng)的可移植性玻蝌,而且非常輕量級(jí),并且還支持connect編寫的中間件词疼。也就是說(shuō)nuxt3基于h3編寫的server端俯树,可以無(wú)縫地移植到支持js運(yùn)行環(huán)境的地方,比如說(shuō)woker贰盗,serverless...
我們先試試许饿,開發(fā)一個(gè)在nuxt3中使用的api
export default (req, res) => {
return 'Hello World'
}
同樣,支持異步舵盈,也支持nodejs風(fēng)格的調(diào)用
export default async (req, res) => {
res.statusCode = 200
res.end('hello world')
}
nuxt3也支持在同一個(gè)server文件夾中編寫middleware陋率,而且是自動(dòng)導(dǎo)入的。nuxt3這次的更新秽晚,屬于是把文件系統(tǒng)玩出花了瓦糟,不光plugins不需要重復(fù)聲明了(nuxt2要在config重復(fù)聲明),而且components爆惧,composables(nuxt3新增的文件夾狸页,可以存放公共hook)... 都可以支持自動(dòng)導(dǎo)入锨能。
試想一下扯再,如今寫nuxt3應(yīng)用,搭配vue3 composition api址遇,將會(huì)使開發(fā)體驗(yàn)上升好幾個(gè)臺(tái)階熄阻。
文末,我們可以試試打包一個(gè)nuxt應(yīng)用到cloudflare 作為woker運(yùn)行是什么效果倔约?我們?cè)赽uild之后會(huì)發(fā)現(xiàn)output文件夾很簡(jiǎn)潔(不像nuxt2遷移部署都很令人頭疼)
我們不僅可以在最后的demo中看到頁(yè)面秃殉,也可以訪問(wèn) api/hello 這個(gè)路由查看剛剛我們?cè)趎uxt中定義的api
點(diǎn)擊訪問(wèn)
部署到cloudflare-文檔
demo地址
結(jié)語(yǔ)
又是水文一篇,希望以后可以出一些高質(zhì)量的總結(jié)文章,希望這篇文章所講述的前端常見的渲染模式钾军,你能夠知道鳄袍,并且知道原理,這也就是本文最終的目標(biāo)吏恭∞中。框架會(huì)不會(huì)都沒(méi)關(guān)系,我們要洞悉一切技術(shù)背后的真相樱哼,再去研究框架不是手到擒來(lái)么哀九?
本文使用 文章同步助手 同步