因爲前端渲染SEO的問題,因此首先博客優化點先把服務端渲染(Server-Side Rendering
)放在首位,折騰了段時間將博客前臺部分以及服務端koa2部分改版,成功實現服務端渲染,這篇文章旨在記錄下本次博客的升級以及實現vue2
與koa2
配合服務端渲染的相關經驗和小結。
javascript
Talk is cheap. Show me the codehtml
你們能夠打開network看下渲染出的html是否實現了SSR前端
這個博客項目會持續更新,追求更完美的博客體驗,歡迎star、fork,提出你寶貴的意見啊~😸vue
上一篇文章:基於vue二、koa二、mongodb的我的博客java
這是原來的
webpack
/admin
(這部分後面會講到),而後前臺部分則是使用vue-server-renderer
的方法經過讀取template和vue-ssr-server-bundle.json來渲染出html返回實現服務端渲染咱們都在說SSR,其實就是Server-Side Rendering(服務端渲染)的縮寫,它能夠解決前端渲染的兩個痛點SEO(搜索引擎優化)
以及首屏渲染性能
由於前端渲染每每初始頁面基本上幾個div,而後其餘什麼數據之類都是須要用js渲染到dom上,SEO方面有些爬蟲就只會到html(雖然如今有些爬蟲已經能識別到js加載出的,不過仍是按照它的url), 首屏渲染方面更加顯而易見,不少前端渲染的應用每每js相對較大,就得等到js加載解析完成才能加載到首屏,影響體驗。git
這篇文章寫得不錯 實測Vue SSR的渲染性能:避開20倍耗時github
文章中講到分爲兩種web
前一種是咱們以前很常見到的,經過ejs
或者pug
等等這些引擎經過它們一些規則實現一些數據的填充
第二種則是每每和前端渲染相配合的服務端同構渲染(isomorphic
),同構即先後端共用一套代碼,後端經過編寫一些規則將前端代碼轉成virtual-dom
對象,調用render再取數據渲染出HTML出來mongodb
具體也不深刻介紹這部分,上面那篇文章有比較詳細的分析,有興趣的同窗能夠前往觀看
Thanks for the suggestion. We are obviously aware of the fact that the virtual-dom-based SSR is slower than string-based ones; but one important reason for Vue's SSR to be virtual-dom-based is so that it fully supports manually written render functions as well. This is critical for advanced components such as
<transition>
,<keep-alive>
,<router-view>
etc. to work properly - a lot of these features are simply impossible with plain string templates.It may be possible to use a hybrid strategy where we render simple components using string concatenation, but advanced ones using the current vdom-based algorithm. If the user's app contains large amount of template-only components this should still result in significant perf win.
這裏開始步入本文的重點
有可能有些朋友還不怎麼了解這部分的流程,這裏我簡單介紹一下
這裏講述是生產環境下的
vue-server-renderer
的createRenderer
,讀取兩個文件,一個客戶端相關的html模板(如今也可使用生成的json),一個服務端相關的json, (這兩個文件都是webpack生成,這裏看不懂沒關係,後面會接着講),而後經過createRenderer
構造出一個渲染器renderToString
或者renderToStream
在上下文中傳入req.url
,剛剛不是傳入個服務端相關的json(其實它是由咱們自定義的一個entry-server.js生成,這裏面寫着如何去提早取數據),而後調用後就進入構造初始化store,初始化router,初始化App,進入提早取數據的邏輯,經過匹配路由的組件,而後調用咱們在組件事先寫好的preFetch
方法去取數據,最後將app resolve
出來,這樣提早請求數據的步驟完成context.state = store.state
,這部分上下文拿到取好的數據後,會在html裏嵌入一段window.__INITIAL_STATE__={//...}
,而後前端部分咱們能夠這樣store.replaceState(window.__INITIAL_STATE__)
,而後咱們就初始化的store數據就是服務端已經請求渲染好的數據,就達到了匹配,關於前端還要不要拿數據,若是服務端渲染的數據已經知足了其實就不用拿了,不過有些時候由於咱們須要更快的響應速度,可讓服務端取一部分數據,前端取大數據來提高速度,不過這裏也要注意到頁面的那些元素的匹配問題,假如渲染出的跟前端部分不匹配的話,vue部分會報出warning[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
warn複製代碼
上部分其實就是整個SSR流程,固然上面也略寫了不少背後的渲染深層原理以及部分細節,想看細節讀者能夠繼續啦~😸
這裏有兩個很關鍵的文件,一個是entry-client.js和entry-server.js
// entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
// store替換使client rendering和server rendering匹配
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
// 掛載#app
router.onReady(() => {
app.$mount('#app')
})複製代碼
entry-client.js主要起到的做用是替換store來跟服務端匹配,能夠經過閱讀上一節的流程看到
// entry-server.js
import { createApp } from './app'
const isDev = process.env.NODE_ENV !== 'production'
export default context => {
console.log(context)
const s = isDev && Date.now()
// 注意下面這句話要寫在export函數裏供服務端渲染調用,從新初始化那store、router
const { app, router, store } = createApp()
return new Promise((resolve, reject) => {
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
reject({ code: 404 })
}
Promise.all(matchedComponents.map(component => {
if(component.preFetch) {
// 調用組件上的preFetch(這部分只能拿到router第一級別組件,子組件的preFetch拿不到)
return component.preFetch(store)
}
})).then(() => {
isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
// 暴露數據到HTMl,使得客戶端渲染拿到數據,跟服務端渲染匹配
context.state = store.state
context.state.posts.forEach((element, index) => {
context.state.posts[index].content = '';
})
resolve(app)
}).catch(reject)
})
})
}複製代碼
entry-server.js經過webpack中的vue-server-renderer/server-plugin
打包成一個json供服務端vue-server-renderer
的createRenderer
讀取,主要起到每一次SSR服務端請求從新createApp
以及匹配路由提早取數據渲染的做用
構造渲染器部分
const bundle = require('../client/dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync(resolve('../client/dist/front.html'), 'utf-8')
renderer = createRenderer(bundle, template)複製代碼
router匹配路由部分renderToString或者renderToStream
router.get('*', async(ctx, next) => {
let res = ctx.res;
let req = ctx.req;
// 因爲koa內有處理type,此處須要額外修改content-type
ctx.type = 'html';
const s = Date.now();
let context = { url: req.url };
// let r = renderer.renderToStream(context)
// .on('end', () => console.log(`whole request: ${Date.now() - s}ms`))
// ctx.body = r
function renderToStringPromise() {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
if (err) {
console.log(err);
}
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`)
}
resolve(html);
})
})
}
ctx.body = await renderToStringPromise();
})複製代碼
vue方面咱們得提早定義好preFetch邏輯, entry-server.js會傳入store而後調用action等就能夠提早取數據
export default {
name: 'list',
//...
preFetch(store) {
store.dispatch('getAllTags')
return store.dispatch('getAllPosts',{page:store.state.route.params.page}).then(()=>{
})
}
//...
}複製代碼
網上不少例子都是圍繞着express
來的,雖然koa
的異步處理很優秀,但不得不認可express
的生態比koa
好太多,不少中間件都有express
版本,可是沒有koa
版本。
不過改寫那些中間件並非很複雜,咱們只要搞清楚express和koa中的req、res、ctx、next這些相關概念以及瞭解koa對req與res的封裝,就能去改寫
好比對connect-history-api-fallback
function historyApiFallback (options) {
const expressMiddleware = require('connect-history-api-fallback')(options)
const url = require('url')
return (ctx, next) => {
let parseUrl = url.parse(ctx.req.url)
// 添加path match,讓不匹配的路由能夠直接穿過中間件
if(!parseUrl.pathname.match(options.path)) {
return next()
}
// 修改content-type
ctx.type = 'html'
return expressMiddleware(ctx.req, ctx.res, next)
}
}
module.exports = historyApiFallback複製代碼
好比對webpack-dev-middleware
const devMiddleware = require('webpack-dev-middleware');
module.exports = (compiler, opts) => {
const expressMiddleware = devMiddleware(compiler, opts)
let nextFlag = false;
function nextFn() {
nextFlag = true;
}
function devFn(ctx, next) {
expressMiddleware(ctx.req, {
end: (content) => {
ctx.body = content
},
setHeader: (name, value) => {
ctx.headers[name] = value
}
}, nextFn)
if(nextFlag) {
nextFlag = false;
return next();
}
}
devFn.fileSystem = expressMiddleware.fileSystem
return devFn;
}複製代碼
經驗之談其實就是返回一個(ctx, next)=>{}
相似的函數,而後咱們取看express中間件的源碼,看它對req和res有什麼相關操做,而後咱們根據這些操做傳入koa的處理方式,好比下面
expressMiddleware(ctx.req, {
end: (content) => {
ctx.body = content
},
setHeader: (name, value) => {
ctx.headers[name] = value
}
}, nextFn)複製代碼
而後看一下它調用next的邏輯,選擇咱們手動調用或者直接將koa的next傳入
這種改寫問題具體狀況具體分析~上面也只是寫了個大概思路
其實就是使用咱們改寫好的webpack-dev-middleware
與webpack-hot-middleware
而後在內存中拿文件,而後hot監聽文件修改reload頁面
// 開發環境下使用hot/dev middleware拿到bundle與template
require('../client/build/setup-dev-server')(app, (bundle, template) => {
renderer = createRenderer(bundle, template)
})複製代碼
其實上面也已經說到了,這裏已經提早生成好template
和json
讀取到而後調用渲染器render方法便可
服務端和客戶端同構,可是服務端並無window
和document
這些方法怎麼辦
能夠經過全局window的存在與否去判斷
// 解決移動端300ms延遲問題
if (typeof window !== "undefined") {
const Fastclick = require('fastclick')
Fastclick.attach(document.body)
}複製代碼
其實服務端渲染vue-server-renderer
並無全部鉤子都調用,因此這部分咱們就能夠利用這個,將一些須要操做window以及dom相關的放入相似beforeMount
等等這些鉤子裏,具體能夠看vue文檔,都有介紹是否支持服務端渲染
not match
問題怎麼辦[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
warn複製代碼
這兩個我都試過,多是因爲個人應用複雜程度較低,二者差別不大,有興趣的讀者也能夠把個人源碼clone下來本地跑一下試試,目前使用的是renderToString,註釋部分有renderToStream
因爲差別不大,考慮到可擴展性,相對string可能可擴展的程度較高一點,而且SSR文檔寫的以下,
大體意思就是雖然流式響應獲取到第一塊數據能第一時間返回,可是那時子組件尚未實例化,就沒辦法在它們的生命週期鉤子裏拿到數據渲染,還有由於前面的head頭部信息以及內嵌style有可能不少的緣故,因此最後它表述的是不建議當你的組件生命週期鉤子依賴於上下文數據的時候使用stream模式
In stream rendering mode, data is emitted as soon as possible when the renderer traverses the Virtual DOM tree. This means we can get an earlier "first chunk" and start sending it to the client faster.
However, when the first data chunk is emitted, the child components may not even be instantiated yet, neither will their lifecycle hooks get called. This means if the child components need to attach data to the render context in their lifecycle hooks, these data will not be available when the stream starts. Since a lot of the context information (like head information or inlined critical CSS) needs to be appear before the application markup, we essentially have to wait until the stream to complete before we can start making use of these context data.
It is therefore NOT recommended to use streaming mode if you rely on context data populated by component lifecycle hooks.
如今你們能夠閱讀vue ssr服務端指南 ssr.vuejs.org/en/
謝謝閱讀~
歡迎follow我哈哈github.com/BUPT-HJM
看到這裏,不star不行了😋
github.com/BUPT-HJM/vu…
歡迎繼續觀光個人博客~
歡迎關注