快速在你的Vue/React應(yīng)用中實(shí)現(xiàn)Ssr(服務(wù)端渲染)
前言
我們都知道, Vue和React是構(gòu)建客戶端應(yīng)用程序的框架。默認(rèn)情況下,可以在瀏覽器中輸出自定義組件,進(jìn)行生成 DOM 和操作 DOM, 也就是我們常說(shuō)的客戶端渲染, 并且我們大部分主流的場(chǎng)景都是SPA(單頁(yè)面)應(yīng)用, 而隨著 SPA尤其是 React、Vue、Angular 為代表的前端框架的流行,越來(lái)越多的 Web App 使用的是客戶端渲染。
使用客戶端渲染的優(yōu)勢(shì)在于節(jié)省后端資源、局部刷新、前后端分離等,但隨著應(yīng)用的日益復(fù)雜, 首屏渲染時(shí)間不斷變長(zhǎng), 并且存在嚴(yán)重的SEO問(wèn)題。
所以為了解決SPA應(yīng)用遇到的這些問(wèn)題, 我們必須考慮SSR:
服務(wù)端渲染(ssr),是指由服務(wù)器端完成頁(yè)面的HTML 結(jié)構(gòu)拼接,并且直接將拼接好的HTML發(fā)送到瀏覽器,然后為其綁定狀態(tài)與事件,成為完全可交互頁(yè)面的處理技術(shù)。
對(duì)于服務(wù)端渲染的頁(yè)面,服務(wù)端可以直接將帶數(shù)據(jù)的內(nèi)容通過(guò) HTML 文本的形式返回,搜索引擎爬蟲(chóng)可以輕易的獲取頁(yè)面內(nèi)容,而對(duì)于客戶端渲染的應(yīng)用,客戶端必須執(zhí)行服務(wù)器返回的 Javascript 才能得到正確的網(wǎng)頁(yè)內(nèi)容。目前,除 Google、Bing 支持 Javascript 外(也會(huì)有一些限制),其他的大部分搜索引擎都不支持 Javascript,也就無(wú)法獲取正確的網(wǎng)頁(yè)內(nèi)容。而本文要講的技術(shù)方案,正是為了解決SPA下的SSR技術(shù)困境.接下來(lái)我們看看常用的ssr技術(shù)實(shí)現(xiàn)方案。
ssr(服務(wù)端渲染)技術(shù)實(shí)現(xiàn)方案
接下來(lái)筆者將列舉幾個(gè)常用的基于vue/react的服務(wù)端渲染方案,如下:
- 使用next.js/nuxt.js的服務(wù)端渲染方案
- 使用node+vue-server-renderer實(shí)現(xiàn)vue項(xiàng)目的服務(wù)端渲染
- 使用node+React renderToStaticMarkup實(shí)現(xiàn)react項(xiàng)目的服務(wù)端渲染
- 傳統(tǒng)網(wǎng)站通過(guò)模板引擎來(lái)實(shí)現(xiàn)ssr(比如ejs, jade, pug等)
- 使用rendertron實(shí)現(xiàn)SPA項(xiàng)目的服務(wù)端渲染
以上是筆者之前實(shí)踐過(guò)的方案, 最后一種方案筆者將在下面一節(jié)詳細(xì)介紹, 因?yàn)閚ext/nuxt是已有的服務(wù)端渲染解決方案,文檔寫(xiě)的比較詳細(xì),這里筆者就不再做過(guò)多介紹了,這里我們簡(jiǎn)單介紹一下第二種和第三種方案。
1.使用node+vue-server-renderer實(shí)現(xiàn)vue項(xiàng)目的服務(wù)端渲染
首先vue-server-renderer依賴(lài)node的api,所以只能運(yùn)行在node環(huán)境, 我們需要先安裝它:
npm install vue vue-server-renderer --save
在node中使用,代碼如下:
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `<div>趣談前端:{{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
當(dāng)然實(shí)際情況比上面的案例要復(fù)雜很多, 我們可以專(zhuān)門(mén)寫(xiě)一個(gè)template.html,然后通過(guò)模板差值的方式導(dǎo)入后端數(shù)據(jù),進(jìn)而實(shí)現(xiàn)服務(wù)端渲染. 在使用這種方式的時(shí)候我們?nèi)匀灰S護(hù)兩套代碼。
2.使用node+React renderToStaticMarkup實(shí)現(xiàn)react項(xiàng)目的服務(wù)端渲染
使用這種方案和vue的方案類(lèi)似, 只不過(guò)這里我們用了react自帶的api來(lái)實(shí)現(xiàn)ssr,簡(jiǎn)單的實(shí)現(xiàn)代碼如下:
var express = require('express');
var app = express();
var React = require('react'),
ReactDOMServer = require('react-dom/server');
var App = React.createFactory(require('./App'));
app.get('/', function(req, res) {
var html = ReactDOMServer.renderToStaticMarkup(
React.DOM.body(
null,
React.DOM.div({id: 'root',
dangerouslySetInnerHTML: {
__html: ReactDOMServer.renderToStaticMarkup(App())
}
})
)
);
res.end(html);
});
app.listen(80, function() {
console.log('running on port ' + 80);
});
以上使用了renderToStaticMarkup, 我們都知道react-dom提供了兩種服務(wù)端渲染函數(shù),如下:
renderToString:將 React Component 轉(zhuǎn)化為 HTML 字符串,生成的 HTML 的 DOM 會(huì)帶有額外屬性:各個(gè) DOM 會(huì)有data-react-id屬性,第一個(gè) DOM 會(huì)有data-checksum屬性。
renderToStaticMarkup:將 React Component 轉(zhuǎn)化為 HTML 字符串,但是生成 HTML 的 DOM 不會(huì)有額外屬性,從而節(jié)省 HTML 字符串的大小。
所以這里我們一般使用renderToStaticMarkup函數(shù). 同理在實(shí)際業(yè)務(wù)場(chǎng)景中我們也會(huì)寫(xiě)2套代碼來(lái)實(shí)現(xiàn)ssr。
使用谷歌rendertron實(shí)現(xiàn)服務(wù)端渲染
Google 推出的 Rendertron 使得 SPA 也能夠被不支持執(zhí)行 Javascript 的搜索引擎爬取渲染后的內(nèi)容。其原理主要是通過(guò)使用 Headless Chrome 在內(nèi)存中執(zhí)行 Javascript,并在得到完整內(nèi)容后,將內(nèi)容返回給客戶端。
我們通常會(huì)將 Rendertron 部署為一個(gè)獨(dú)立的 HTTP 服務(wù),然后為 Web 應(yīng)用框架配置 Google 官方提供的中間件或者在反向代理上添加相應(yīng)路由規(guī)則,使得能夠在檢測(cè)到搜索引擎爬蟲(chóng)的 UA 時(shí),可以將請(qǐng)求代理給 Rendertron 服務(wù)。筆者總結(jié)了一下其基本實(shí)現(xiàn)原理圖,方便大家理解:
Rendertron 提供了兩個(gè)主要 API:
- Render 用于渲染網(wǎng)站內(nèi)容
- Screenshot 用于將網(wǎng)站內(nèi)容截圖
在 SEO 場(chǎng)景下我們使用的是 Render 接口。
比如當(dāng)客戶端請(qǐng)求我們的網(wǎng)站時(shí),我們服務(wù)端可以根據(jù)請(qǐng)求頭 User Agent 發(fā)現(xiàn)是否包含了 Baiduspider/2.0 關(guān)鍵字,如果是, 那么可以認(rèn)定為當(dāng)前的客戶端是一個(gè)百度爬蟲(chóng)此時(shí)可以將這個(gè)請(qǐng)求代理 Rendertron 服務(wù)的 /render/客戶端請(qǐng)求地址 路由,讓 Rendertron 幫助執(zhí)行網(wǎng)頁(yè)內(nèi)的 Javascript,并將最終內(nèi)容返回給搜索引擎爬蟲(chóng)。
使用Rendertron的好處在于我們可以不用考慮服務(wù)端渲染的部分,完全按照SPA的模式開(kāi)發(fā)項(xiàng)目,也不用為了兼容服務(wù)端渲染而寫(xiě)多余的兼容代碼。
具體實(shí)現(xiàn)
首先我們需要安裝Rendertron, 可以在github中找到其安裝和使用方法,在安裝前最好先安裝docker, 目前docker的最新版本以支持傻瓜式安裝,所以安裝啟動(dòng)都非常方便。
1.本地運(yùn)行
在安裝好docker之后, 我們先全局安裝rendertron:
npm install -g rendertron
然后我們需要安裝谷歌瀏覽器(作為合格的開(kāi)發(fā)都應(yīng)該有谷歌瀏覽器~),然后就可以用它的cli來(lái)啟動(dòng)服務(wù)了,我們只需要在命令行執(zhí)行如下命令:
rendertron
之后控制臺(tái)會(huì)打印本地服務(wù)啟動(dòng)的地址,比如localhost:3000 這個(gè)時(shí)候我們只需要在地址后面輸入我們想渲染的網(wǎng)站即可:localhost:3000:render/你的網(wǎng)站地址, 如下圖所示:
此時(shí)我們的rendertron服務(wù)已經(jīng)搭建完成, 接下來(lái)我們可以在服務(wù)端來(lái)實(shí)現(xiàn)ssr了,代碼如下:
const koa = require('koa');
const app = new koa();
app.use(async (ctx, next) => {
ctx.type = "html";
if(/Baiduspider\/2\.0/g.ctx.header['user-agent']) {
// 是百度爬蟲(chóng),則轉(zhuǎn)發(fā)到rendertron服務(wù)中
ctx.redirect(`http://localhost:3000/render/${ctx.url}`)
}else {
// 渲染正常的路由頁(yè)面
}
await next();
})
app.listen('80');
當(dāng)然如果我們后端技術(shù)棧采用的是express, rendertron有專(zhuān)門(mén)的中間件可以使用, 不僅僅可以攔截百度的爬蟲(chóng),具體用法如下:
const express = require('express');
const rendertron = require('rendertron-middleware');
const app = express();
app.use(rendertron.makeMiddleware({
proxyUrl: 'http://your-rendertron-instance/render',
}));
// 正常的路由和頁(yè)面渲染邏輯
app.use(...);
app.listen(81);
所以為了降低開(kāi)發(fā)成本筆者建議可以采用rendertron的方案, 單獨(dú)部署一套服務(wù)器用來(lái)實(shí)現(xiàn)ssr. 但是我們需要考慮當(dāng)網(wǎng)站流量增加時(shí)的擴(kuò)容問(wèn)題,以及配置搭建反向代理或負(fù)載均衡等配套服務(wù)。
后期展望
后期筆者將會(huì)繼續(xù)帶大家探索大前端相關(guān)內(nèi)容, 基本框架如下: