附上Demo的仓库地址:https://gitee.com/jfengz003/vue-ssr-demo.git
希望能与大家一起进步~
一、SSR的概念
传统web开发
单页应用SPA
服务端渲染SSR
什么是服务器端渲染 (SSR)?
官方文档:Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。
我们从上门解释得到以下结论:
1、Vue SSR是一个在SPA上进行改良的服务端渲染;
2、通过Vue SSR渲染的页面,需要在客户端激活才能实现交互;
3、Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA;
为什么使用服务器端渲染 (SSR)?
- 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
- 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。
使用服务器端渲染 (SSR) 时还需要有一些权衡之处:
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
- 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
- 更多的服务器端负载。每个请求都是n个实例的创建,不然会污染,消耗会变得很大
如果你调研服务器端渲染 (SSR) 只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
二、如何实现SSR
对于同构开发,我们依然使用webpack打包,我们要解决两个问题:服务端首屏渲染和客户端激活;
这里需要生成一个服务器bundle文件用于服务端首屏渲染和一个客户端bundle文件用于客户端激活
新增:app.js、entry-client.js、entry-server.js、server.js
src
├── router
├────── index.js # 路由声明
├── store
├────── index.js # 全局状态
├── server
├────── server.js # node服务
├── app.js # 通用入口,⽤于创建vue实例
├── entry-client.js # 客户端⼊⼝,⽤于静态内容“激活”
└── entry-server.js # 服务端⼊⼝,⽤于⾸屏内容渲染
修改路由配置
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Login',
component: () => import('@/views/login.vue')
},
{
path: '/layout',
name: 'Layout',
component: () => import('@/views/layout.vue'),
redirect: '/layout/pageOne',
children: [
{
path: 'pageOne',
name: 'pageOne',
component: () => import('@/views/page1.vue')
},
{
path: 'pageTwo',
name: 'pageTwo',
component: () => import('@/views/page2.vue')
}
]
}
]
// 使用工厂函数创建路由
function createRouter() {
return new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
});
}
export default createRouter
通用入口
// src/app.js
import Vue from 'vue';
import App from './App.vue';
import createRouter from './router';
// 使用工厂函数,导出Vue实例和Router实例
export default function createApp() {
const router = createRouter();
const app = new Vue({
router,
render: h => h(App)
});
return { app, router };
}
服务端入口
// src/entry-server.js
import createApp from "./app";
// 返回⼀个函数,接收请求上下⽂,返回创建的vue实例
export default context => {
// 这⾥返回⼀个Promise,确保路由或组件准备就绪
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 进入首屏
router.push(context.url);
// 路由就绪
router.onReady(() => {
resolve(app);
}, reject);
});
}
客户端入口
// entry-client.js
import createApp from "./app";
// 创建vue、router实例
const { app, router } = createApp();
// 路由就绪 执行挂载
router.onReady(() => {
app.$mount('#app');
});
webpack配置(vue.config.js)
安装依赖:
npm install webpack-node-externals lodash.merge -D
// vue.config.js
// webpack插件
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 path = require('path');
// 环境变量:决定入口是客户端还是服务端
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false
},
outputDir: path.resolve(__dirname, `./dist/${target}`) ,
configureWebpack: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: path.resolve(__dirname, `./src/entry-${target}.js`),
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 这允许 webpack 以 Node 适用方式处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: TARGET_NODE ? "node" : "web",
node: TARGET_NODE ? undefined : false,
output: {
// 此处告知 server bundle 使用 Node 风格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的 bundle 文件。
externals: TARGET_NODE
? nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
allowlist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined
},
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
config.module
.rule("vue")
.use("vue-loader")
.tap(options => {
merge(options, {
optimizeSSR: false
});
});
}
};
对脚本进行配置,安装依赖
npm install cross-env -D
"scripts": {
"serve": "vue-cli-service serve",
"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",
"lint": "vue-cli-service lint"
},
编写服务端启动脚本
安装依赖:
npm install vue-server-renderer express -S
// server/server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
// 创建express实例和Vue实例
const app = express();
// 创建渲染器
const { createBundleRenderer } = require('vue-server-renderer');
// 服务端bundle文件
const serverBundle = require(path.resolve(__dirname, '..', 'dist/server/vue-ssr-server-bundle.json'));
// 客户端清单
const clientManifest = require(path.resolve(__dirname, '..', 'dist/client/vue-ssr-client-manifest.json'));
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: fs.readFileSync(path.resolve(__dirname, '..', 'public/index.temp.html'), 'utf-8'), // 宿主模板文件
clientManifest
});
// 中间件处理静态文件请求
app.use(express.static(path.resolve(__dirname, '..', 'dist/client'), { index: false }));
app.get('*', async (req, res) => {
try {
// 上下文
const content = {
url: req.url,
title: 'ssr test'
}
const html = await renderer.renderToString(content);
console.log('object', html);
res.send(html);
} catch(err) {
res.status(500).send('服务器内部错误');
}
});
app.listen(3002, () => {
console.log('渲染服务器启动成功');
});
新增宿主文件:/public/index.temp.html
<!DOCTYPE html>
<html lang="">
<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>ssr</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
注意:<!--vue-ssr-outlet-->是服务端渲染出口位置,不能有空格
执行打包:
npm run build
cd进入server文件夹,在终端执行:
node server.js
打开http://localhost:3002,查看页面源代码如下图的话,恭喜你,初步实现vue服务端渲染ssr。
参考视频:手把手教你打造Vue SSR
参考文章:如何在vue中实现SSR服务端渲染?