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