Vue SSR
浏览器渲染和服务端渲染
一、基本用法
1.安装插件:
npm install vue npm install vuex npm install vue-router npm install vue-server-renderer2.逻辑代码
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统
const Vue = require('vue')
const vm = new Vue({
//服务端没有el
data(){
return {
name: 'zf',
age:10
}
},
//渲染模板
template:`
<div>
<p>{{name}}</p>
<span>{{age}}</span>
</div>
`
})
const VueServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const render = VueServerRender.createRenderer();//创建一个渲染器
router.get('/',async (ctx) =>{
// ctx.body = 'hello2225555'
ctx.body = await render.renderToString(vm) //渲染成一个字符串
})
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听3000端口
查看网页源码:
<div data-server-rendered="true"><p>zf</p> <span>10</span></div> |
可以看到是一串字符串
但我们前端网页都是有head标签的,可以把渲染好的字符串插入到一个单独的html文件中
创建template.html 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
<!--vue-ssr-outlet--> 这句话表示模板占位,特别重要,一定要加,不加会报错
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统
const Vue = require('vue')
const vm = new Vue({
//服务端没有el
data(){
return {
name: 'zf',
age:10
}
},
//渲染模板
template:`
<div>
<p>{{name}}</p>
<span>{{age}}</span>
</div>
`
})
const fs = require('fs')
const path = require('path')
//使用html
const template = fs.readFileSync(path.resolve(__dirname,'template.html'),'utf8') //同步读取html模板
console.log(template)
const VueServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const render = VueServerRender.createRenderer({
template
});//创建一个渲染器
router.get('/',async (ctx) =>{
// ctx.body = 'hello2225555'
ctx.body = await render.renderToString(vm) //渲染成一个字符串
})
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口
渲染的结果:
二、纯浏览器渲染
1.目录结构:
2.mian.js
import Vue from 'vue'
import App from './App.vue'
let vm = new Vue({
el:'#app',
render:h=>h(App)
})
3.App.vue
<template>
<div>
<bar></bar>
<foo></foo>
</div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
components:{
Bar,
Foo
}
}
</script>
4.index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器渲染</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
5.bar.vue
<template>
<div>
bar
</div>
</template>
<script>
export default {
name:'Bar'
}
</script>
<style scoped>
div {
background: red;
}
</style>
6.foo.vue
<template>
<div>
foo <button @click="show()">点击</button>
</div>
</template>
<script>
export default {
name:'Foo',
methods:{
show(){
alert(1)
}
}
}
</script>
<style scoped>
</style>
7.webpack.config.js
//打包默认文件
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
entry:path.resolve(__dirname,'src/main.js'), //打包入口文件
output:{
filename:'bundle.js', //打包后的文件名
path:path.resolve(__dirname,'./dist') //打包后的文件存放地址
},
module:{
rules:[
{
test:/\.js/, //匹配js 使用babel-loader进行转义
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
}
},
{
test:/\.vue/, //匹配.vue 文件使用vue-loader进行转义
use:'vue-loader'
},
{
test:/\.css/, //匹配css 使用style-loader进行转义
use:['vue-style-loader','css-loader']
}
]
},
plugins:[
new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
template:path.resolve(__dirname,'public/index.html')
}),
new VueLoaderPlugin()
]
}
8.package.json
{
"name": "vue_ssr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev":"webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"babel-loader": "^8.1.0",
"css-loader": "^3.5.3",
"html-webpack-plugin": "^4.3.0",
"koa": "^2.12.0",
"koa-router": "^9.0.1",
"koa-static": "^5.0.0",
"nodemon": "^2.0.4",
"vue": "^2.6.11",
"vue-loader": "^15.9.2",
"vue-router": "^3.3.2",
"vue-server-renderer": "^2.6.11",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"vuex": "^3.4.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0"
}
}
9.渲染结果
查看网页源代码:
可以看到,浏览器渲染的结果就是html文件中只有一个div,一个js文件,默认会通过这个js文件来渲染展示页面,其他的html元素并不会出现在在网页中的,所以浏览器渲染不利于SEO优化。
三、服务端渲染
1、Vue SSR构建流程:
app.js入口文件
app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去做了。
两个entry
接下里我们来看Client entry和Server entry,这两者分别是客户端的入口和服务端的入口。Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上;Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的渲染中重复的调用。
webpack打包构建
然后我们的服务端代码和客户端代码通过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。
weboack.client.config.js
,一个用于服务端渲染webpack.server.config.js
,将它们的公有部分抽出来作为webpack.base.cofig.js
,后续通过webpack-merge
进行合并。同时,也要有一个server
来提供http
服务,我这里用的是koa
。
在浏览器端渲染中,每个客户都会在他们的客户端得到一个新的应用程序,浏览器端渲染也是,我没希望每次返回给用户的是一个新的app实例,以免交叉请求造成状态污染。所以要将app.js封装成一个工厂函数,每次调用都会产生一个新的实例。
app.js
import Vue from 'vue'
import App from './App.vue'
export default function () {
let app = new Vue({
// el:'#app', //客户端需要 服务端不需要
render:h=>h(App)
})
return {app}
}
浏览器端的根组件,将其挂载:
client-entry.js
import createApp from './app'
const {app} = createApp()
//客户端挂载
app.$mount('#app')
clinet.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器渲染</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
服务器端要返回一个新的实例,该实例可以接收一个context参数,同时每次都会返回一个新的根组件,
server-entry.js
//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'
//服务端渲染要求打包后的结果返回一个函数
export default (context) =>{
const {app} = createApp()
return app
}
server.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务端渲染</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
App.vue
<template>
<div id='app'>
<bar></bar>
<foo></foo>
</div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
components:{
Bar,
Foo
}
}
</script>
webpack.base.js
//打包默认文件
const path = require('path')
// const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
// entry:path.resolve(__dirname,'src/main.js'), //打包入口文件
output:{
filename:'[name].bundle.js', //打包后的文件名
path:path.resolve(__dirname,'../dist') //打包后的文件存放地址
},
module:{
rules:[
{
test:/\.js/, //匹配js 使用babel-loader进行转义
use:{
loader:'babel-loader',
options:{
presets:['@babel/preset-env']
}
}
},
{
test:/\.vue/, //匹配.vue 文件使用vue-loader进行转义
use:'vue-loader'
},
{
test:/\.css/, //匹配css 使用style-loader进行转义
use:['vue-style-loader','css-loader']
}
]
},
plugins:[
// new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
// template:path.resolve(__dirname,'public/index.html')
// }),
new VueLoaderPlugin()
]
}
入口文件不是同一个了,所以要抽离出去,打包后的html文件也不是同一个了,所以,HtmlWebapckPlugin也要分离出去。
webpack.client.js
const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = merge(base,{
entry:{
client:path.resolve(__dirname,'../src/client-entry.js')
},
plugins:[
new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
filename:'client.html',
template:path.resolve(__dirname,'../public/client.html')
}),
]
})
这里定义了客户端渲染的入口文件,并且规定将打包后的client.bundle.js引入到client.html文件
webpack.server.js
const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = merge(base,{
entry:{
server:path.resolve(__dirname,'../src/server-entry.js')
},
target:'node', //输出的文件是给node用的,不需要打包node自带的模块。
output:{
libraryTarget:'commonjs2' //表示以node 的 形式打包输出 module.exports = 导出入口函数
},
plugins:[
new HtmlWebpackPlugin ({
filename:'server.html',
template:path.resolve(__dirname,'../public/server.html'),
excludeChunks:['server'] //表示 服务端不需要把 打包后的 server.bundle.js 引入到 html中
}),
]
})
这里定义了服务端渲染的入口文件,并且规定不需要将打包后的server.bundle.js引入到server.html文件中,而是要引入client.buldle.js,同时要指定打包输出的文件是给node服务端渲染用的,output 指定打包输出的时候是以module.exports形式导出
打包结果如下:
package.json
{
"name": "vue_ssr",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"client:dev": "webpack-dev-server --config build/webpack.client.js",
"client:build": "webpack --config build/webpack.client.js",
"server:build":"webpack --config build/webpack.server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.10.2",
"@babel/preset-env": "^7.10.2",
"babel-loader": "^8.1.0",
"css-loader": "^3.5.3",
"html-webpack-plugin": "^4.3.0",
"koa": "^2.12.0",
"koa-router": "^9.0.1",
"koa-static": "^5.0.0",
"nodemon": "^2.0.4",
"vue": "^2.6.11",
"vue-loader": "^15.9.2",
"vue-router": "^3.3.2",
"vue-server-renderer": "^2.6.11",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"vuex": "^3.4.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2"
}
}
打包命令:
"client:dev": "webpack-dev-server --config build/webpack.client.js",
"client:build": "webpack --config build/webpack.client.js",
"server:build":"webpack --config build/webpack.server.js"
服务端渲染相关代码
const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')
const ServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统
// const Vue = require('vue')
// const vm = new Vue({
// //服务端没有el
// data(){
// return {
// name: 'zf',
// age:10
// }
// },
// //渲染模板
// template:`
// <div>
// <p>{{name}}</p>
// <span>{{age}}</span>
// </div>
// `
// })
//使用html
const template = fs.readFileSync(path.resolve(__dirname,'dist/server.html'),'utf8') //同步读取html模板
const ServerBundle = fs.readFileSync(path.resolve(__dirname,'dist/server.bundle.js'),'utf8') //同步读取打包后的js文件
console.log(template)
let render = ServerRender.createBundleRenderer(ServerBundle,{ //将这个打包后的js文件 渲染成一个字符串
template
});//创建一个渲染器
router.get('/',async (ctx) =>{
// ctx.body = 'hello2225555'
// ctx.body = await render.renderToString() //这样写css不生效
ctx.body = await new Promise ((resolve,reject) =>{ //必须写成回调的方式 css才能解析成功
render.renderToString((err,data) =>{
if(err){
console.log(err)
reject(err)
}else {
resolve(data)
}
})
})
})
app.use(static(path.resolve(__dirname,'dist'))) //静态资源访问目录
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口
ServerBundle 是打包后的服务端代码,表示要将这个js文件渲染成一个字符串,插入到template模板中。
vue-server-renderer插件,有两个方法可以做渲染,一个是createRenderer,一个是createBundleRenderer。createRenderer
无法接收为服务端打包出的server.bundle.js
文件,所以这里只能用createBundleRenderer
。
使用createRenderer
和createBundleRenderer
返回的renderer
函数包含两个方法renderToString
和renderToStream
,我们这里用的是renderToString
成功后直接返回一个完整的字符串,renderToStream
返回的是一个Node
流。renderToString
支持Promise。
至此配置完成
打包:
npm run client:build
npm run server:build
启动客户端:
npm run client:dev
启动服务端:
nodemon server.js
查看结果
浏览器端渲染:
查看源代码:
客户端渲染:
查看网页源代码:
data-server-rendered="true"属性表示是服务端渲染,但是这时候点击按钮并不生效,因为后端渲染出的字符串只是可以在页面展示的静态html,但是并没有js动态功能,如果要点击页面某个元素,并不会生效,所以要手动引入客户端打包后的js文件client.bundle.js ,并且要自行添加 id=“app”
App.vue
<template>
<div id='app'>
<bar></bar>
<foo></foo>
</div>
</template>
<script>
import Bar from './components/bar.vue'
import Foo from './components/foo.vue'
export default {
components:{
Bar,
Foo
}
}
</script>
server.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>浏览器渲染</title>
</head>
<body>
<!--vue-ssr-outlet-->
<!-- 后端渲染出的字符串只是可以在页面展示的静态html,但是并没有js动态功能,如果要点击页面某个元素,并不会生效,所以要手动引入这个客户端打包后的js文件 -->
<script src="client.bundle.js"></script>
</body>
</html>
此时再次点击便可生效,这个功能在官网叫客户端激活。
所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。参考官网链接:https://ssr.vuejs.org/zh/guide/hydration.html
继续完善------------------------------------------------------------------------------------------
修改配置文件,让前端代码修改之后可以自动打包自动更新,并且服务端渲染自动引入客户端的打包文件,而不是手动引入,官网叫Bundle Renderer 指引,接下来我们实现一个这个功能
2、Bundle Renderer 指引
webpack.client.js
const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const VueServerRenderer = require('vue-server-renderer/client-plugin')
module.exports = merge(base,{
entry:{
client:path.resolve(__dirname,'../src/client-entry.js')
},
plugins:[
new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
filename:'client.html',
template:path.resolve(__dirname,'../public/client.html')
}),
new VueServerRenderer() //会生成一个文件叫vue-ssr-client-manifest.json
]
})
webpack.server.js
const base = require('./webpack.base.js')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const VueServerRenderer = require('vue-server-renderer/server-plugin')
module.exports = merge(base,{
entry:{
server:path.resolve(__dirname,'../src/server-entry.js')
},
target:'node', //输出的文件是给node用的,不需要打包node自带的模块。
output:{
libraryTarget:'commonjs2' //表示以node 的 形式打包输出 module.exports = 导出入口函数
},
plugins:[
new VueServerRenderer(), //会生成一个文件叫vue-ssr-server-bundle.json
new HtmlWebpackPlugin ({ //将打包后的js文件通过script标签引入这个html文件中
filename:'server.html',
template:path.resolve(__dirname,'../public/server.html'),
excludeChunks:['server'] //表示 服务端不需要把 打包后的 server.bundle.js 引入到 html中
}),
]
})
打包后生成两个文件
在服务端渲染代码中引入这个文件
server.js
const Koa = require('koa')
const Router = require('koa-router')
const fs = require('fs')
const path = require('path')
const static = require('koa-static')
const ServerRender = require('vue-server-renderer') //vue 的服务端渲染包
const app = new Koa() //创建一个应用
const router = new Router() //产生一个路由系统
//使用html
const template = fs.readFileSync(path.resolve(__dirname,'dist/server.html'),'utf8') //同步读取html模板
// const ServerBundle = fs.readFileSync(path.resolve(__dirname,'dist/server.bundle.js'),'utf8') //同步读取打包后的js文件
const ServerBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
console.log(template)
let render = ServerRender.createBundleRenderer(ServerBundle,{ //将这个打包后的js文件 渲染成一个字符串
template,
clientManifest //表示自动引入客户端打包结果,不需要手动引入客户端打包文件了
});//创建一个渲染器
router.get('/',async (ctx) =>{
// ctx.body = 'hello2225555'
// ctx.body = await render.renderToString() //这样写css不生效
ctx.body = await new Promise ((resolve,reject) =>{ //必须携程回调的方式 css才能解析成功
render.renderToString((err,data) =>{
if(err){
console.log(err)
reject(err)
}else {
resolve(data)
}
})
})
})
app.use(static(path.resolve(__dirname,'dist'))) //静态资源访问目录
app.use(router.routes()) //使用路由系统
app.listen(5000) //监听5000端口
在服务端渲染的时候会自动引入打包后的客户端文件,而不用再手动引入
路由配置:
新建router.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
//一函数的形式导出 防止函数被共用
export default () =>{
return new VueRouter({
mode:'history',
routes:[
{
path:'/',
component:() => import('./components/foo.vue')
},
{
path:'/bar',
component:() => import('./components/bar.vue')
}
]
})
}
路由实例要以函数的形式导出,这样每次请求都可以返回一个新的实例,防止被共用。
修改app.js文件:
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
export default function () {
let router = createRouter() //每次调用创建一个新的router
let app = new Vue({
router, //前端渲染直接注入即可
// el:'#app', //客户端需要 服务端不需要
render:h=>h(App)
})
return {app,router} //服务端渲染返回
}
每次渲染都会创建一个新的router实例,前端渲染直接注入即可,后端渲染的时候返回这个router实例
服务端渲染代码server.js
//当访问其他路径的时候 需要把路径回传到打包入口文件server-entry.js,让它跳转到那个页面再返回整个实例
router.get('*',async (ctx) =>{
try{
ctx.body = await new Promise ((resolve,reject) =>{
render.renderToString({url:ctx.path},(err,data) =>{
if(err){
console.log(err)
reject(err)
}else {
resolve(data)
}
})
})
}catch(e){
console.log(e)
ctx.body = 'page not found'
}
})
我们的服务器代码使用了一个 *
处理程序,它接受任意 URL。这允许我们将访问的 URL 传递到我们的 Vue 应用程序中,然后对客户端和服务器复用相同的路由配置!
也就是说,当访问除了 ‘/’ 以外的其他路径时,比如:loacolhost:5000/bar , 首先拿到这个路径 /bar,然后将这个路径回传到server-entry.js文件,对应代码 {url:ctx.path},在这里进行跳转,然后返回跳转后的整个实例。
修改server-entry.js:
//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'
//服务端渲染要求打包后的结果返回一个函数
export default (context) =>{
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve,reject) => {
const {app,router} = createApp()
router.push(context.url)
router.onReady(() => {
//获取匹配路由
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if(!matchedComponents.length){
return reject({code:404})
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
},reject)
})
}
router.push表示路由的跳转,如果匹配不到的路由,执行 reject 函数,并返回 404,然后返回整个app进行渲染
四、Vuex+Vue-router-SSR
在上面的例子中我们都是同步渲染,那如果SSR需要获取一些异步数据该怎么处理?
服务端渲染和浏览器端渲染经过的生命周期不同,在服务器端,只会经历beforeCreate
和created
两个生命周期,因为服务器端渲染是在后端拼接好了字符串直接输出,不会有dom结构的渲染,就不会有beforeMount
和mounted。
我们先来想一下,在纯浏览器渲染的Vue
项目中,我们是怎么获取异步数据并渲染到组件中的?一般是在created
或者mounted
生命周期里发起异步请求,然后在成功回调里执行this.data = xxx
,Vue
监听到数据发生改变,走后面的Dom Diff
,打patch
,做DOM
更新。
那么服务端渲染可不可以也这么做呢?答案是不行的。
- 在
mounted
里肯定不行,因为SSR
都没有mounted
生命周期,所以在这里肯定不行。 - 在
beforeCreate
里发起异步请求是否可以呢,也是不行的。因为请求是异步的,可能还没有等接口返回,服务端就已经把html
字符串拼接出来了。
所以,参考一下官方文档,我们可以得到以下思路:
- 在渲染前,要预先获取所有需要的异步数据,然后存到
Vuex
的store
中。 - 在后端渲染时,通过
Vuex
将获取到的数据注入到相应组件中。 - 把
store
中的数据设置到window.__INITIAL_STATE__
属性中。 - 在浏览器环境中,通过
Vuex
将window.__INITIAL_STATE__
里面的数据注入到相应组件中。
正常情况下,通过这几个步骤,服务端吐出来的html
字符串相应组件的数据都是最新的,所以第4步并不会引起DOM
更新,但如果出了某些问题,吐出来的html
字符串没有相应数据,Vue
也可以在浏览器端通过`Vuex
注入数据,进行DOM
更新。
1.创建store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default () => {
let store = new Vuex.Store({
state:{
name:''
},
mutations:{
changeName(state){
state.name = 'leah'
}
},
actions:{
changeName({commit}){ //模拟数据请求
return new Promise ((resolve,reject)=>{
setTimeout(() => {
commit('changeName')
resolve()
},3000)
})
}
}
})
//表示浏览器渲染才会走这个逻辑
if(typeof window !== 'undefined'){ // 服务端环境没有window属性,只有前端渲染才有window属性
if(window.__INITIAL_STATE){
store.replaceState(window.__INITIAL_STATE) //用服务端渲染结果替换当前state
}
}
return store
}
在actions中模拟数据请求,最重要的是对当前执行环境的判断,因为服务端渲染会默认将获取的结果保存在 window.__INITIAL_STATE这个属性上,这个时候就要对store中的状态做一个替换,保证统一。
2.app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
import createStore from './store'
export default function () {
let router = createRouter() //每次调用创建一个新的router
let app = new Vue({
store,
router, //前端渲染直接注入即可
// el:'#app', //客户端需要 服务端不需要
render:h=>h(App)
})
return {app,router,store} //服务端渲染返回
}
同样的前端渲染直接注入store即可,服务端渲染需要导出store
3.bar.vue
<template>
<div>
bar {{$store.state.name}}
</div>
</template>
<script>
export default {
name:'Bar',
//服务端渲染没有这个钩子
mounted(){
this.$store.dispatch('changeName')
},
//规定这个方法只可以在服务端渲染中使用,并且只能在页面级组件中使用
asyncData(){ //这个请求是在服务端发的
return store.dispatch('changeName') //返回一个promise
}
}
</script>
<style scoped>
div {
background: red;
}
</style>
如果是前端渲染可以通过mounted生命周期拿到异步数据,如果是服务端渲染就需要走asyncData()这个方法,他会返回一个promise,
4.server-entry.js
//服务端渲染需要一个vm实例
// 每一个客户端访问都要有一个新的实例返回
import createApp from './app'
//服务端渲染要求打包后的结果返回一个函数
export default (context) =>{
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve,reject) => {
const {app,router} = createApp()
router.push(context.url)
router.onReady(() => {
//获取匹配路由
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if(!matchedComponents.length){
return reject({code:404})
}
Promise.all(matchedComponents.map(comp => {
return comp.asyncData && comp.asyncData(store) //返回页面级组件
})).then(()=>{
context.state = store.state //将服务端调用Vuex渲染的结果放到当前的上下文中
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
},err =>{
reject(err)
})
},reject)
})
}
在所有的组件中找到具有asyncData方法的组件并返回,并且将服务端调用Vuex的结果保存在上下文中,context.state = store.state
作用是,当使用createBundleRenderer
时,如果设置了template
选项,那么会把context.state
的值作为window.__INITIAL_STATE__
自动插入到模板html
中。
至此,Vue SSR就完结了
好文推荐:https://segmentfault.com/a/1190000016637877