本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-28
将app在服务端渲染成一个完整的 HTML 字符串并发送到浏览器,最后再将这些静态标记“激活”为可交互应用程序的过程称为服务端渲染。更多详情
启用一个node中间层服务端,对于页面资源的请求,在node中间层进行SSR渲染,把渲染完的完整HTML发送给浏览器,就可以减少首屏渲染时间。而对于其他的数据接口API就向web服务器去请求,因为返回的HTML页面通过加载客户端相关js,就会“恢复”成SPA应用,之后就只与web服务端做ajax交互。
这样的弊端有:
服务器负载会变大,每一次用户请求都会创建一个 vue 实例 (或者需要 Vuex、VueRouter插件的时候,也需要每次请求创建对应的实例),所以你需要确保有效地缓存页面。
开发变得更加困难
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用入口
├── entry-client.js # 客户端入口,仅运行于浏览器 用于将应用激活成SPA
└── entry-server.js # 服务端入口,仅运行于服务器
安装依赖: npm i vue-server-renderer express -D
最简易的SSR代码:
// server.js
const express = require('express');
const Vue = require('vue'); // 在node环境引入Vue
// 创建express实例和Vue实例
const app = express();
// 创建渲染器
const renderer = require('vue-server-renderer').createRenderer();
// 将来用渲染器渲染Page
const page = new Vue({
data: { title: '我的app' }
template: `
<div>
<h1>{{title}}</h1>
<p>hello</p>
</div>`
})
// template 一样要遵循vue的只有一个根标签
// 监听服务
app.get('/', async (req, res) => {
try {
const html = await renderer.renderToString(page);
res.send(html);
} catch (error) {
res.status(500).send('服务器内部错误');
}
})
app.listen(8080);
安装: npm i vue-router -S
// router/index.js
import Vue from 'vue';
import Router from 'vue-router';
import Index from '@/components/Index';
import Detail from '@/components/Detail';
Vue.use(Router);
// 这里为什么不导出一个 router 实例 ?
// 因为避免污染,不然整个应用都只有一个 Vue 实例 和 一个 VueRouter 实例
// 但多个用户请求的时候就都是同一个了 就会弄混淆 导致异常
// 所以每次请求都是返回一个Vue实例和VueRouter实例 (上面的server.js只是简版代码)
export default function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: Index },
{ path: '/detial', component: Detail },
]
})
}
使用工厂函数来创建Vue实例,给服务端和客户端引用,相当于 main.js
// app.js
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
export default function createApp() {
const router = createRouter();
const app = new Vue({
router,
render: h => h(App),
});
// 不需要挂载 因为最终直接访问就是 HTML文档字符串了
// SPA应用的时候 返回空div 所以需要 $mount 来完成客户端渲染
return { app, router };
}
// entry-server.js
import createApp from './app';
export default (context) => {
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 进入首屏 context 是在 server.js 渲染 HTML 模板的时候传入的
// 当用户访问 localhost:8080/about
// server.js 中间层应用程序会将 req.url 即 /about 传入到 context
router.push(context.url);
router.onReady(() => {
// 路由可能是异步的 整个函数返回一个Promise
// 这里在router准备就绪的情况下 resolve 中 vue 实例 app
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
resolve(app);
}, reject)
})
}
// entry-client.js
// 只有一个作用:挂载、激活app
import createApp from './app';
const {app, router } = createApp();
// 需要注意的是,你仍然需要在挂载 app 之前调用 router.onReady
// 因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子
router.onReady(() => {
app.$mount('#app');
})
// server/index.js
// nodejs服务器
const express = require("express");
const Vue = require("vue");
const fs = require('fs')
// 创建express实例和vue实例
const app = express();
// 创建渲染器,这里使用createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
// 通过webpack加对应插件生产的对应的客户端打包代码和服务端打包代码
const serverBundle = require('../dist/server/vue-ssr-server-bundle.json');
const clientManifest = require('../dist/client/vue-ssr-client-manifest.json');
// 创建用于ssr的renderer
// 该renderer能够生成对应的完整html
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: fs.readFileSync('../public/index.temp.html', 'utf-8'), // 宿主模板文件
clientManifest,
// 这个完整的html需要用到的一些客户端js,css等文件,会通过script标签defer延迟引用
// 因为首次返回的是纯html文件,只是首页的内容,而之后的交互需要激活以及对应的静态文件
})
// 中间件处理静态文件请求
// index:false,是因为当我们访问项目根路径的时候,默认都是首页,而此时我们希望程序进入到我们的ssr服务端代码中
// 由于客户端静态文件也是会生产一个 index.html 文件用于客户端渲染
// 如果不关闭,则会访问 ../dist/client/index.html 而不会进入到我们的ssr程序中
app.use(express.static('../dist/client', {index: false}))
// app.use(express.static('../dist/client'))
// 路由处理交给vue
app.get("*", async (req, res) => {
try {
const context = {
url: req.url,
title: 'ssr test'
}
const html = await renderer.renderToString(context);
console.log(html);
res.send(html);
} catch (error) {
res.status(500).send("服务器内部错误");
}
});
app.listen(3000, () => {
console.log("渲染服务器启动成功");
});
示例演示:
1、安装依赖 npm i --registry https://registry.npm.taobao.org
2、执行 npm run build
3、执行 node ./server/index2.js