服务端渲染的本质是后端根据路由的情况在后端渲染这个组件的html代码,然后发送到前端,前端通过传来的 data-server-rendered=”true”标识符来得知这个html是由服务器渲染的,然后进行加载到自身进行管理。
在服务端渲染的时候,beforeCreate和created生命周期都是存在于服务端的,没有任何浏览器对象,如果在里面访问document或者window这种对象会在渲染的时候node抛出异常,其他周期则运行在客户端中。
这里使用vue的脚手架来初始化项目
通过 vue-cli 脚手架生成一个基础结构
vue init webpack ssrdemo
cnpm i
安装ssr需要的插件
cnpm i webpack-node-externals vue-server-renderer -D
- webpack-node-externals:忽略打包工具,内联忽略node_modules文件件,可以为其指定白名单 whitelist让某类文件打包到包中
- vue-server-renderer 为文件生成对应json的ssr插件
安装node服务器,官方示例中使用的是express 这里使用的是koa
cnpm i koa2 koa-router koa-static koa-bodyparser --save
因为服务器渲染需要针对客户端和服务器端分别打包,所以需要两个入口文件,并把入口main.js改造成一个工厂函数,如果包含vuex或者vue-router还需要对这个两个进行改变成工厂函数并通过改造后的main函数引用,之后进行同步
在src下新建 client.js server.js 两个入口js
改造router.js,因为在服务端每一次独立请求都应该返回一个全新的router对象,所以需要这个工厂函数,vuex也是一样
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld.vue'
export function createRouter(){
return new Router({
mode:'history',
routes:[
{
path:"/",
component:HelloWorld
}
]
})
}
改造main.js
import Vue from 'vue'
import {createRouter} from '@/router/router.js'
//引入Router的工厂函数
import App from '@/App.vue'
export function createApp(){
const router=createRouter()
//生产router,如果需要vuex一样
const app = new Vue{
render:h=>h(App),
router:router
}
return {app,router}
//返回app和router
}
编写server.js
import {createApp} from '@/main.js'
//引用工厂函数
export default (context)=>{//获取上下文,上下文由服务器传入进来 @1
return new Promise( (resolve,rejct)=>{
//返回一个Promise
const {url} = context;
//通过上下文对象获取现在的url地址
const {app,router}= createApp();
//通过工厂函数拿到app和router
router.push(url) //把地址传入router
router.onReady(()=>{ //等待router跳转完成
let matchedCs=router.getMatchedComponents; //匹配到的Components
if(!mathchedCs.length){
reject({code:404})
}
resolve(app) //成功返回app
},reject)
} )
}
编写client.js
import {createApp} from '@/main.js'
const {app,router} = createApp()
router.onReady(()=>{ //等待路由同步
app.$mount("#app")//这里的#app应该是服务端通过App组件的第一个id="app"指定的
})
改造build里的base配置
把入口点改为client.js
entry:"./src/client.js"
改造prod配置,改造为客户端打包的配置文件,让他等同于 webpack.client.conf.js
//只显示更改
const vueSSRPlugin = require('vue-server-renderer/client-plugin')
module.exports=merge(baseConfig,{
pluins:[
//...其他插件
//但是注释掉通过html模版生产html插入标签的plugin:HtmlWebpackPlugin
//在webpack.DefinePlugin插件中新增加一个VUE_ENV
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"'
}),
new vueSSRPlugin()//增加SSR客户端插件
]
})
新建一个webpack.server.conf.js文件
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.conf')
const nodeExternals = require('webpack-node-externals')
const VueSSRPlugin = require('vue-server-renderer/server-plugin') //服务端打包插件
const utils = require('./utils') //之所以要引入这个和下面那个插件,是因为如果服务端需要用到css渲染的代码的时候需要依赖这些因为编译
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports=merge(baseConfig,{
entry:'./src/server.js', //入口点为server.js
target:'node',
devtool:'source-map',
output:{
libraryTarget:'commonjs2'
},
externals:nodeExternals({ //选择性打包
whitelist:/\.css$/
}),
plugins:[
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
new VueSSRPlugin()
]
})
至此构建工作完成了,最后在package里添加打包命令:
"build:client": "cross-env NODE_ENV=production node build/build.js", //用原有的打包命令打包客户端
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
"build": "rimraf dist && npm run build:client && npm run build:server",
//rimraf和cross-env是前者一个清空指定目录的包后者是一个指定运行参数的工具,需要npm install
配置服务器之前先建立一个服务端渲染所需要的template的html文件
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title> //这里可以使用插值来定义模板,数据来源于下面服务器中传入的一个context上下文
</head>
<body>
<!--vue-ssr-outlet--> //不需要任何标签,只需要在这里加入这样一行注释
</body>
</html>
因为服务端根据bundle推算出html内容后插入到这个注释上面,然后客户端根据data-server-rendered=”true”和id来进行挂载的时候进行数据本地化,但是在dev的时候,如果使用了同样的模版会因为挂载$mount(‘#app’)但是找不到app标签导致为空
解决方法可以使用两套html模板,一个dev,一个服务端。或者参考官方例子来进行dev也使用服务端渲染的方式
进行服务器编写
const PORT=3000
const Koa=require('koa2')
const Router =require('koa-router')()
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const fs =require('fs')
const path = require('path')
const {createBundleRenderer} = require('vue-server-renderer')
const serverBundle=require('./dist/vue-ssr-server-bundle.json') //加载webpack打包出来的服务端映射文件
const clientManifest = require('./dist/vue-ssr-client-manifest.json')//加载webpack打包出来的客户端映射文件
const render=createBundleRenderer(serverBundle,{
template: fs.readFileSync('index.ssr.template.html','utf-8'), //上面定义的服务器渲染模版
clientManifest:clientManifest,
runInNewContext:false
})
const App = new Koa()
App.use(static(path.resolve(__dirname,'./dist'))) //匹配静态路径
App.use(bodyParser())
App.use(async (ctx,next)=>{
console.log(ctx.req.url);//如果插入自己的中间件
await next(); //一定要是async形式的异步不然会导致ctx.body先给页面返回了404,你才收到render好的数据
})
Router.get("*",async (ctx,next)=>{
const context={url:ctx.req.url,title:"自定义title"} //使用模版插值的title @1
try{
let value=await render.renderToString(context) 这个context上下文会由server.js接收
ctx.body=value
}catch(e){
console.log(e);
next()
}
})
App.use(Router.routes())
App.listen(PORT,()=>{
console.log('listen:'+PORT);
})
服务端渲染的构建工作宣告完成。
node ./app.js