SSR
是 Server-Side Rendering
的缩写,意为服务端渲染,它是指前端页面是由服务端提供并渲染到浏览器;
SSR
的优缺点:
SEO
:服务端渲染会返回完整的 DOM 结构,客户端渲染只有根节点和 js 文件;XSS
攻击的风险:服务端拼接字符串返回浏览器会直接执行,容易遭受 XSS
攻击;Vue SSR 需要服务端的支持,我们可以用 express
简单搭建一个服务器,首先通过 vue-cli 简单创建一个项目 vue-ssr
,然后在项目下新建一个 server
文件夹,server
文件下新建一个 app.js
,app.js
的内容如下:
const express = require("express")
const fs = require("fs")
const Vue = require("vue")
// 读取模板
const template = fs.readFileSync("./index.template.html", "utf-8")
// 根据传进去的模板获取 renderer
const renderer = require("vue-server-renderer").createRenderer({ template })
const server = express()
server.get("/", async (req, res) => {
// 创建一个实例
const app = new Vue({
data: {
title: "Hello SSR!"
},
template: "{{ title }}
",
methods: {
changeTitle() {
this.title = 'after change title'
}
},
})
renderer.renderToString(app)
.then(html => {
res.send(html)
})
})
server.listen(3000, () => {
console.log("server is running at 3000")
})
在启动服务器之前,我们需要在终端执行命令 npm i express vue-server-render vue -S
安装一下依赖,安装完成之后,在server
目录下执行命令 node app.js
,服务器启动之后在浏览器输入 localhost:3000
就可以看到页面;
index.template.html
的代码如下:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>titletitle>
head>
<body>
body>
html>
实际上,我们在代码里面设置了一个点击事件,但是在页面点击的时候,是没有触发点击事件的,因为 vue-server-renderer
没有帮我们做这方面的处理,为了解决这个问题,我们需要客户端再渲染一遍,这一过程可以称为同构;
上面讲到我们需要客户端重新渲染一遍页面,因此我们需要 webpack
帮我们打包两份 bundle
文件,一份用于客户端,一份用于服务端,具体实现接着往下看:
1、安装依赖包
npm install vue-router vue-loader cross-env lodash.merge webpack-node-externals -S
2、在 App.vue 文件中使用路由标签
首页
关于我
click me
3、新建两个页面 Home.vue 和 About.vue,然后对路由的入口文件进行修改
客户端渲染的路由入口文件,是直接通过一个 new VueRouter
生成路由实例,但是在服务端渲染的时候,我们需要把它包装成一个工厂函数,每个用户请求都会生成一个路由实例,因为在服务端渲染,如果没有重新生成实例,而是每个用户共享一个实例的话,会造成很严重的后果,根实例也是一样;
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const Home = () => import("../components/Home.vue")
const About = () => import("../components/About.vue")
// 每个用户请求都要创建一个路由实例和根实例
export default function createRouter() {
return new Router({
// 必须是 history,因为 hash 模式的"#"不会发送给服务器
mode: 'history',
routes: [
{
path: '/',
component: Home,
name: 'home'
},
{
path: '/about',
component: About,
name: 'about'
}
]
})
}
4、对 main.js 进行修改
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router/index'
Vue.config.productionTip = false
// 每个用户请求都要创建一个路由实例和根实例
export function createApp() {
const router = new createRouter()
const app = new Vue({
router,
render: h => h(App)
})
// router 后面会用到,跟 app 一起返回方便后面使用
return { app, router }
}
5、新建 entry-client.js 和 entry-server.js
// entry-client.js 挂载激活 app
import { createApp } from './main.js'
const { app } = createApp()
app.$mount("#app")
// entry-server.js
import {createApp} from "./main.js";
export default context => {
return new Promise((resolve, reject) => {
const { app, router } = createApp();
router.push(context.url) ;
router.onReady(() => {
resolve(app);
}, reject);
})
}
6、配置 webpack 文件
// 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");
// 决定入口是客户端还是服务端,执行 webpack 命令时确认
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
outputDir: `./dist/${target}`,
configureWebpack: () => ({
// 将 entry 指向客户端或服务端文件
entry: `./src/entry-${target}.js`,
devtool: "source-map",
// 这允许 webpack 以 Node 使用方式处理动态导入
// 在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码
target: TARGET_NODE ? "node" : "web",
node: TARGET_NODE ? undefined : false,
output: {
// 配置服务器端使用 node 的风格构建
libraryTarget: TARGET_NODE ? "commonjs2" : 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 });
});
}
}
7、修改 package.json
{
"scripts": {
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
"build": "npm run build:server && npm run build:client",
"serve": "cd server && nodemon app.js"
}
}
8、对 server / app.js 进行修改
const express = require("express")
const fs = require('fs')
const server = express()
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require("../dist/server/vue-ssr-server-bundle.json");
const clientManifest = require("../dist/client/vue-ssr-client-manifest.json");
const template = fs.readFileSync("./index.template.html", "utf-8");
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template,
clientManifest,
});
// index: false 是为了忽略 dist/client/index.html
server.use(express.static("../dist/client", { index: false }))
server.get("*", async (req, res) => {
try {
// 这里的 context 就是之前 createApp 传入的 context
const context = {
url: req.url,
title: 'vue ssr'
}
// 这里不用 renderToString 是因为防止文件大的情况下出现卡顿现象
const stream = renderer.renderToStream(context)
let buffer = []
stream.on("data", chunk => {
buffer.push(chunk)
})
stream.on("end", () => {
res.end(Buffer.concat(buffer))
})
} catch (err) {
res.status(500).send("服务器内部错误")
}
})
server.listen(3000, () => {
console.log("server is running at 3000")
})
9、终端执行命令
npm run build
进行打包,生成客户端和服务端相应的 json 文件;
npm run serve
启动服务器;
注意:vue-server-renderer 的版本需要跟 vue 的版本一致,否则可能会报错,还有 vue-router 对应版本也不能太高
"vue": "^2.6.14",
"vue-router": "^3.5.3",
"vue-server-renderer": "^2.6.14"