一麸拄、什么是服務(wù)器端渲染(SSR)派昧?
大致就是在服務(wù)端拼接好用戶請(qǐng)求的靜態(tài)頁(yè)面,直接返回給客戶端拢切,客戶端激活這些靜態(tài)頁(yè)面蒂萎,讓他們變成動(dòng)態(tài)的,并且能夠響應(yīng)后續(xù)的數(shù)據(jù)變化淮椰。
二五慈、為什么使用服務(wù)器端渲染(SSR)?
1主穗、更好的 SEO泻拦,由于搜索引擎爬蟲(chóng)抓取工具可以直接查看完全渲染的頁(yè)面。
2忽媒、產(chǎn)生更好的用戶體驗(yàn)争拐,更快的內(nèi)容到達(dá)時(shí)間(time-to-content),特別是對(duì)于緩慢的網(wǎng)絡(luò)情況或運(yùn)行緩慢的設(shè)備猾浦。無(wú)需等待所有的 JavaScript 都完成下載并執(zhí)行陆错,才顯示服務(wù)器渲染的標(biāo)記,所以你的用戶將會(huì)更快速地看到完整渲染的頁(yè)面金赦。
三音瓷、基本用法
首先vue-ssr 需要一個(gè)及其重要的插件,所以我們需要安裝一下
npm install vue vue-server-renderer --save
3.1 渲染一個(gè)簡(jiǎn)單的實(shí)例(官網(wǎng))
// 第 1 步:創(chuàng)建一個(gè) Vue 實(shí)例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// 第 2 步:創(chuàng)建一個(gè) renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:將 Vue 實(shí)例渲染為 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
// 在 2.5.0+夹抗,如果沒(méi)有傳入回調(diào)函數(shù)绳慎,則會(huì)返回 Promise:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
3.2 與服務(wù)器集成的案例(官網(wǎng))
先安裝npm install express --save
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>訪問(wèn)的 URL 是: {{ 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)
3.3 使用一個(gè)頁(yè)面模板
// index.html
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
注意 注釋 -- 這里將是應(yīng)用程序 HTML 標(biāo)記注入的地方。
const renderer = createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
console.log(html) // html 將是注入應(yīng)用程序內(nèi)容的完整頁(yè)面
})
當(dāng)然,以上內(nèi)容只是基礎(chǔ)中的基礎(chǔ)杏愤,最終我們使用靡砌,都是需要集成vue-cli,并結(jié)合node珊楼,express通殃,webpack等的配置,來(lái)生成一個(gè)更加靈活的vue-ssr應(yīng)用厕宗,花了將近4天時(shí)間画舌,一遍看api,一遍查資料已慢,終于搭建出了一個(gè)靜態(tài)的單頁(yè)應(yīng)用vue-ssr曲聂。趕緊趁熱乎分享一下,畢竟年紀(jì)大了佑惠,記性不好朋腋。
四、vue2.0+node+express+webpack實(shí)現(xiàn)vue-ssr單頁(yè)應(yīng)用
首先膜楷,我們需要安裝vue-cli旭咽,具體安裝,可以參考筆者的vue系列 第一篇文章把将,如果之前已經(jīng)全局安裝過(guò)轻专,那么只需要init一下即可。
剛開(kāi)始看不懂官網(wǎng)上的圖察蹲,后來(lái)搭建出來(lái)以后,發(fā)現(xiàn)官方的就是官方的催训,就跟北京地鐵圖一樣清晰洽议,借鑒一下別人的話就是,ssr 有兩個(gè)入口文件漫拭,client.js 和 server.js亚兄, 都包含了應(yīng)用代碼,webpack 通過(guò)兩個(gè)入口文件分別打包成給服務(wù)端用的 server bundle 和給客戶端用的 client bundle. 當(dāng)服務(wù)器接收到了來(lái)自客戶端的請(qǐng)求之后采驻,會(huì)創(chuàng)建一個(gè)渲染器 bundleRenderer审胚,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼礼旅, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器膳叨,等到客戶端加載了 client bundle 之后,會(huì)和服務(wù)端生成的DOM 進(jìn)行 Hydration(判斷這個(gè)DOM 和自己即將生成的DOM 是否相同痘系,如果相同就將客戶端的vue實(shí)例掛載到這個(gè)DOM上菲嘴, 否則會(huì)提示警告)。
根據(jù)以上內(nèi)容,我們先來(lái)搭建一個(gè)簡(jiǎn)單的暫時(shí)沒(méi)有數(shù)據(jù)請(qǐng)求的vue-ssr龄坪。
如何搭建昭雌?
1、創(chuàng)建一個(gè)vue實(shí)例
2健田、配置路由烛卧,以及相應(yīng)的視圖組件
3、創(chuàng)建客戶端入口文件
4妓局、創(chuàng)建服務(wù)端入口文件
5总放、配置 webpack,分服務(wù)端打包配置和客戶端打包配置
6跟磨、創(chuàng)建服務(wù)器端的渲染器间聊,將vue實(shí)例渲染成html
創(chuàng)建vue實(shí)例:為每個(gè)請(qǐng)求創(chuàng)建一個(gè)新的根 Vue 實(shí)例。這與每個(gè)用戶在自己的瀏覽器中使用新應(yīng)用程序的實(shí)例類似抵拘。如果我們?cè)诙鄠€(gè)請(qǐng)求之間使用一個(gè)共享的實(shí)例哎榴,很容易導(dǎo)致交叉請(qǐng)求狀態(tài)污染,因此僵蛛,我們應(yīng)該暴露一個(gè)可以重復(fù)執(zhí)行的工廠函數(shù)尚蝌,為每個(gè)請(qǐng)求創(chuàng)建新的應(yīng)用程序?qū)嵗?/p>
// app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
Vue.config.productionTip = false
export function createApp () {
const router = createRouter()
const app = new Vue({
// el: '#app',
router,
render: h => h(App)
})
return { app, router }
}
配置路由,以及相應(yīng)的視圖組件: 注意充尉,類似于 createApp飘言,我們也需要給每個(gè)請(qǐng)求一個(gè)新的 router 實(shí)例,所以文件導(dǎo)出一個(gè) createRouter 函數(shù).
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import home from '@/components/home'
import about from '@/components/about'
Vue.use(Router)
export default function createRouter() {
return new Router({
mode:'history',
routes: [
{
path: '/',
name: 'home',
component: home
},
{
path: '/about',
name: 'about',
component: about
}
]
})
}
創(chuàng)建客戶端入口文件:entry-client.js 客戶端 entry 只需創(chuàng)建應(yīng)用程序驼侠,并且將其掛載到 DOM 中
import { createApp } from './app'
// 客戶端特定引導(dǎo)邏輯……
const { app, router } = createApp()
// 這里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
app.$mount('#app')
})
創(chuàng)建服務(wù)端入口文件:entry-server.js 中實(shí)現(xiàn)服務(wù)器端路由邏輯
import { createApp } from './app'
export default (context) => {
// 因?yàn)橛锌赡軙?huì)是異步路由鉤子函數(shù)或組件姿鸿,所以我們將返回一個(gè) Promise,
// 以便服務(wù)器能夠等待所有的內(nèi)容在渲染前倒源,
// 就已經(jīng)準(zhǔn)備就緒苛预。
return new Promise((resolve, reject) => {
const { app, router } = createApp(context)
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// 設(shè)置服務(wù)器端 router 的位置
router.push(context.url)
// 等到 router 將可能的異步組件和鉤子函數(shù)解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 應(yīng)該 resolve 應(yīng)用程序?qū)嵗员闼梢凿秩? resolve(app)
}, reject)
})
}
創(chuàng)建好以后笋熬,我們的項(xiàng)目還不能啟動(dòng)热某,我們需要配置webpack,來(lái)生成服務(wù)端用的 server bundle 和給客戶端用的 client bundle胳螟。
客戶端的client bundle比較好創(chuàng)建昔馋,我們?cè)趘ue-cli下的build文件夾下的webpack.dev.conf.js中,引入const vueSSRClient = require('vue-server-renderer/client-plugin')
糖耸,然后秘遏,在下方配置plugins的地方new vueSSRClient()
創(chuàng)建一個(gè) vueSSRClient()就好了,此時(shí)我們客戶端的bundle就已經(jīng)悄然生成了蔬捷。
服務(wù)端的server bundle垄提,就需要我們?cè)赽uild文件夾下新建一個(gè)webpack.server.conf.js榔袋,配置如下
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')
module.exports = merge(baseWebpackConfig,{
entry: './src/entry-server.js',
devtool:'source-map',
target:'node',
output:{
filename:'server-bundle.js',
libraryTarget:'commonjs2'
},
externals: [nodeExternals({
// do not externalize CSS files in case we need to import it from a dep
whitelist: /\.css$/
})],
plugins:[
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
現(xiàn)在我們已經(jīng)能通過(guò)webpack,生成了服務(wù)端用的 server bundle 和給客戶端用的 client bundle铡俐,但是我們需要?jiǎng)?chuàng)建一個(gè)渲染器 bundleRenderer凰兑,這個(gè) bundleRenderer 會(huì)讀取上面生成的 server bundle 文件,并且執(zhí)行它的代碼审丘, 然后發(fā)送一個(gè)生成好的 html 到瀏覽器吏够,所以我們?cè)赽uild文件夾下 ,新建一個(gè)dev-server.js 文件滩报,內(nèi)容如下(注意看注釋)
// dev-server.js
const webpack = require('webpack')
const baseWebpackConfig = require('./webpack.server.conf')
const clientConfig = require('./webpack.base.conf')
const fs = require('fs')
const path = require('path')
// 讀取內(nèi)存的文件
const Mfs = require('memory-fs')
const axios = require('axios')
module.exports = (cb) => {
// 用來(lái)讀取內(nèi)存文件
var mfs = new Mfs()
const webpackComplier = webpack(baseWebpackConfig)
webpackComplier.outputFileSystem = mfs
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
} catch (e) {}
}
webpackComplier.watch({},async (err,stats) => {
if(err) {
return console.log(err)
}
stats = stats.toJson();
stats.errors.forEach(err => {console.log(err)});
stats.warnings.forEach(err => {console.log(err)});
// 獲取vue-server-renderer/server-plugin生成的服務(wù)端bundle的json文件 默認(rèn)名字為vue-ssr-server-bundle.json
let serverBundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
// 獲取vue-server-renderer/client-plugin生成的客戶端bundle的json文件锅知,默認(rèn)名字vue-ssr-client-manifest.json
let clientBundle = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
// 獲取模板文件 注意模板文件里面加上<!--vue-ssr-outlet-->
let template = fs.readFileSync(path.join(__dirname,'..','index.html'), 'utf-8')
cb(serverBundle, clientBundle, template)
})
}
現(xiàn)在就差臨門(mén)一腳了,就是調(diào)用我們的dev-server脓钾,用bundleRenderer生成我們的html售睹,并返回到頁(yè)面上、那我們?cè)诟夸浵滦陆ㄒ粋€(gè)server.js ,整合所有資源可训,使其成為我們客戶端的入口
// server.js
const devServer = require('./build/dev-server.js')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// 在服務(wù)器處理函數(shù)中……
server.get('*', (req, res) => {
const context = { url: req.url }
res.status(200)
devServer((serverBundle, clientBundle, template) => {
let renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推薦
template, // (可選)頁(yè)面模板
clientManifest:clientBundle.data // (可選)客戶端構(gòu)建 manifest
})
renderer.renderToString(context,(err,html) => {
res.send(html)
})
})
})
const port = process.env.PORT || 5001;
server.listen(port, () => {
console.log(`server started at localhost:${port}`)
console.log('啟動(dòng)成功')
})
最終我們的項(xiàng)目目錄如下:
為了能在服務(wù)端啟動(dòng)我們的項(xiàng)目昌妹,在package.json的scripts中,加一行代碼"server": "node server.js"
現(xiàn)在握截,我們運(yùn)行npm run dev
啟動(dòng)的就是客戶端渲染飞崖,運(yùn)行npm run server
啟動(dòng)的就是服務(wù)端渲染,前端頁(yè)面呈現(xiàn)是一樣的谨胞,但是翻看頁(yè)面源碼的時(shí)候就會(huì)發(fā)現(xiàn)區(qū)別
npm run dev
默認(rèn)啟動(dòng)8080端口
查看客戶端源碼
npm run server
默認(rèn)啟動(dòng)5001端口
查看服務(wù)端源碼固歪,會(huì)有一個(gè)明顯的 data-server-rendered="true"
現(xiàn)在,我們的簡(jiǎn)單的案例就完成了胯努,那么最終我們還要集成vuex來(lái)動(dòng)態(tài)顯示數(shù)據(jù)牢裳,就放下集吧。