准备工作
首先创建一个vue项目
vue create ssr
既然是服务端渲染,我们需要一个服务器,这里我们选择express;此外还需要一个库vue-server-renderer
npm install vue-server-renderer express -D
开始干活
服务器启动脚本
const express = require('express')
const Vue = require('vue')
const app = express()
// 创建渲染器
// createRenderer()是一个工厂函数,会染回一个渲染器
const renderer = require('vue-server-renderer').createRenderer()
const page = new Vue({
template: `<div>hello ssr</div>`
})
// 设置路由
app.get('/', async (req, res) => {
try {
const html = await renderer.renderToString(page)
res.send(html)
} catch (error) {
res.status(500).send('服务器错错误')
}
})
app.listen(3000, () => {
console.log('server listening port:3000');
})
通过编译器可以看见renderToString方法接收一个Vue示例作为参数,也就是上面编写的page
你可能会发现一个问题,在服务端编写路由并不是非常灵活,而且也不方便我们后续的spa,这时候我们需要将路由的控制权交给vue-router
配置vue-router
import Vue from 'vue'
import Router from 'vue-router'
import About from '../pages/About'
import Index from '../pages/Index'
Vue.use(Router)
// 使用工厂函数,返回一个路由的构造器,防止污染不同访问者的路由
export default function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: Index },
{ path: '/about', component: About },
],
})
}
这里注意向外暴露的不再是一个VueRouter实例,而是一个工厂函数,因为不同的用户会发送不同的请求,如果都返回同一个实例,则会对其他用户造成影响
这里我们再来看一下构建流程
通过server-bundle构建之后的html不再只有一个根div,而是已经被渲染的html,当第一次请求结束时,又会变成一个spa,不在跟服务端有联系,而是通过ajax直接访问api服务器
编写入口
总入口app.js
import Vue from 'vue'
import App from './App'
import createRouter from './router/index'
export default function createApp() {
const router = createRouter()
const app = new Vue({
router,
render: h => h(App)
}) // 这里不需要挂载到根节点,因为没有根节点
return {app, router}
}
服务端入口server-entry.js
import createApp from './app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// 进入首屏
router.push(context.url)
router.onReady(() => {
resolve(app)
}, reject)
})
}
客户端入口client-entry.js
// 挂载激活app
import createApp from './app'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
进行打包配置
安装依赖
npm install webpack-node-externals lodash.merge cross-env -D
根目录添加vue.config.js
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
// 决定入口是服务端还是客户端
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? "server" : "client"
module.exports = {
css: {
extract: false
},
outputDir: './dist' + target,
configureWebpack: () => ({
// 指向entry文件
entry: `./src/${target}-entry.js`,
// 对bundle提供source-map支持
devtool: 'source-map',
target: TARGET_NODE ? 'node' : 'web',
node: TARGET_NODE ? undefined : false,
output: {
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
externals: TARGET_NODE ? nodeExternals({
allowlist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined,
},
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
config.module
.rule("vue")
.use("vue-loader")
.tap(options => {
merge(options, {
optimizeSSR: false
})
})
}
}
然后在package.json中添加脚本
"scripts": {
"serve": "vue-cli-service serve",
"lint": "vue-cli-service lint",
"build:server": "vue-cli-service build",
"build:client": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
"build": "npm run build:server && npm run build:client"
},
打包之后再来修改服务器启动脚本
const express = require('express')
const fs = require('fs')
// const Vue = require('vue')
const app = express()
const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
// 创建渲染器
// createRenderer()是一个工厂函数,会染回一个渲染器
const { createBundleRenderer } = require('vue-server-renderer')
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
// 记得新建一个模板
template: fs.readFileSync('../public/index.temp.html', 'utf-8'),
clientManifest
})
app.use(express.static('../dist/client', {index: false}))
// const page = new Vue({
// template: `<div>hello ssr</div>`
// })
// 设置路由
app.get('*', async (req, res) => {
try {
const context = {
url: req.url,
title: 'ssr test'
}
const html = await renderer.renderToString(context)
res.send(html)
} catch (error) {
res.status(500).send('服务器错错误')
}
})
app.listen(3000, () => {
console.log('server listening port:3000');
})
大功告成
此时我们再启动服务器,就可以看到成果了
可以看到html文档中不再是一个app根元素了
总结
SSR(server side render)解决了SEO和首屏渲染慢的问题,首屏在服务器端进渲染,等全部文件加载完之后在浏览器再激活mount实现spa
源码已经上传码云仓库