Vue SSR
SSR概念:
https://ssr.vuejs.org/zh/
server side render
服务端渲染
服务端渲染解释:
将一个Vue组件在服务器渲染为HTML字符串并发送到浏览器,最后再将这些静态标记“激活”为可交互应用程序的过程称为服务端渲染。
百度蜘蛛爬虫的机制
百度蜘蛛是百度搜索引擎的一个自动化程序,它会不断的访问收集互联网上的网页、文章、视频等,通过抓取链接来收录网站,计算网站的权重和排名。纯html等静态化网站对百度蜘蛛比较友好,且百度蜘蛛几乎不会爬取js动态的网站,如vue/react构建的且经webpack/gulp等构建工具压缩处理过的网站。百度蜘蛛爬取网站是从主站开始爬,一次根据网站暴露的内链依次往深层次爬取。meta的设置,以及网站TDK的优化,网站结构优化,外链,文章原创等同样对SEO有很大作用,但本文主要是从技术层面入手,则主要是针对网站内链的处理以及基于vue等现在技术流做ssr处理。
传统服务端渲染
asp.net php jsp浏览器拿到的是全部的dom结构。
优缺点:
- 前后端职责不清
- 前后端代码杂揉在一起
- 项目难以管理和维护
尽管如此,这种渲染方式还是有一些好处:
- 客户端能够快速呈现服务器端渲染好的页面,减少白屏时间,这能够提供很好的用户体验
- SEO 友好,服务端渲染从服务器发出的html带有页面内容,可提高搜索排名。
CSR
单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染(Client Side Rendering)。
客户端发起页面请求,服务端把页面(响应的字符串)发送给客户端,客户端从上到下依次解析,解析过程中,发现网络请求,再次向服务器发送网络请求,客服端拿到响应的结果,模板引擎渲染到HTML页面。
优点:灵活,真正的前后端分离,方便于前后台各自更新维护。
缺点: 对SEO不友好,增加了HTTP请求的次数,减缓了页面加载速度。
SSR
在后端看来,页面文件其实就是一个“字符串”,所以服务端完全可以在获取到HTML文件的内容之后经过一些处理再返回给客户端,也就说,服务端可以将数据插入到HTML 字符串中之后再返回给客户端。
优点:对 SEO搜索引擎友好,减少了HTTP请求的次数,加速了页面初次渲染的速度。
缺点:不灵活,前后端耦合太深。
在 SPA 模式下,所有的数据请求和 Dom 渲染都在浏览器端完成,所以当我们第一次访问页面的时候很可能会存在“白屏”等待,而服务端渲染所有数据请求和 html内容已在服务端处理完成,浏览器收到的是完整的 html 内容,可以更快的看到渲染内容,在服务端完成数据请求肯定是要比v在浏览器端效率要高的多。
服务端渲染和客户端渲染结合
最好的方案就是服务端渲染和客户端渲染的结合,第一次访问页面是服务端渲染,基于第一次访问后续的交互就是客户端渲染的效果和体验,同时还不影响SEO效果
实现Vue SSR具体过程
创建工程
vue-cli创建工程即可
安装依赖
vue-server-renderer express
要确保vue、vue-server-renderer版本一致
启动脚本
创建一个express服务器,将vue ssr集成进来。
./server/index.js
/*
* SSR
* 思考:vue在nodejs中如何展示?
*/
const express = require('express')
const vue = require('vue')
const app = express()
const page = new vue({
data: { title: '沐沐沐 --- 服务端渲染' },
template: `<div>
<h1>{{title}}</h1>
<div>hello moira !</div>
</div>`
})
// 将vue实例渲染成html
// 使用vue官方渲染器 vue-server-renderer
// createRenderer()工厂函数,返回一个渲染器实例
// 渲染器实例作用:把vue实例作为参数传给它,可以将vue当前页面的内容生成html
const renderer = require('vue-server-renderer').createRenderer()
// 页面可能写得有问题,在render的过程中可能会发生异常
// 考虑健壮性,使用try catch
app.get('/', async (req, res) => {
try {
// renderToString 异步生成一个html,参数是一个vue实例。
// 异步方法,返回一个Promise
const html = await renderer.renderToString(page)
res.send(html)
}
catch (error) {
res.status(500).send('服务器内部错误!')
}
})
app.listen(5200, () => {
console.log('服务器启动成功!')
})
路由
路由支持仍然使用vue-router
创建路由实例
每次请求的url委托给vue-router处理
./server/example_simple_ssr.js
// 创建一个express实例
const express = require('express')
const app = express()
// 导入vue
const Vue = require('vue')
// 创建渲染器
const { createRenderer } = require('vue-server-renderer')
const renderer = createRenderer()
// 导入路由
const Router = require('vue-router')
Vue.use(Router)
app.get('*', async (req, res) => {
// 创建一个路由器实例
const router = new Router({
routes: [
{ path: '/', component: { template: '<div>Index</div>' } },
{ path: '/detail', component: { template: '<div>detail</div>' } },
]
})
// 构建渲染页面内容
const vm = new Vue({
router,
data() {
return {
name: '沐沐'
}
},
template: `
<div>
<router-link to="/">index</router-link>
<router-link to="/detail">detail</router-link>
<div>{{name}}</div>
<router-view></router-view>
</div>
`
})
try {
// 路由跳转
router.push(req.url)
// 渲染: 得到html字符串
const html = await renderer.renderToString(vm)
// 发送回前端
res.send(html)
} catch (error) {
res.status(500).send('服务器内部错误')
}
})
// 监听端口
app.listen(5200, () => {
console.log('服务器启动成功!')
})
问题
- 同构开发
同构开发SSR应用
对于同构开发,我们依然使用webpack打包,我们要解决两个问题:
服务端首屏渲染和客户端激活。
目标是生成一个「服务器 bundle」用于服务端首屏渲染,和一个「客户端bundle」用于客户端激活。
server bundle:
处理前端请求渲染哪个页面,把首屏渲染成htmlclient bundle:
打包页面相关代码,作用:将静态html激活成前端spa
代码结构
除了两个不同入口之外,其他结构和之前vue应用完全相同。
app.js 通用入口,用于创建vue实例
entry-client.js # 客户端入口,用于静态内容“激活”
entry-server.js # 服务端入口,用于首屏内容渲染
app.js
// 服务端入口
// 核心作用:创建vue实例
import Vue from "vue";
// 根页面
import App from "./App.vue"
// router工厂函数
import createRouter from "./router";
// 每一次用户请求都要全新Vue实例
export default function createApp() {
const router = createRouter()
const app = new Vue({
router,
// 渲染函数
// 问题: 是否需要 .$mount()挂载
render: h => h(App)
});
// 要返回app和router实例 router实例之后有用
return { app, router }
}
entry-client.js
// 客户端入口只需创建vue实例并执行挂载,这一步称为激活。
import createApp from "./app";
const { app, router } = createApp()
// 准备就绪后挂载
router.onReady(() => {
// 挂载
app.$mount('#app')
})
entry-server.js
// 服务端入口
// 核心作用:渲染首屏
// 处理前端请求渲染哪个页面,把首屏渲染成html
// 得到具体vue实例
import createApp from "./app";
// 导出
// 还是返回一个工厂函数 将来会被服务器调用 因为只有服务器知道用户请求的是什么 在这里处理首屏并且返回对应实例
export default context => {
// 这里返回一个Promise,确保路由或组件准备就绪
return new Promise((resolve, reject) => {
const { app, router } = createApp();
// 跳转到首屏的地址
router.push(context.url)
// 路由就绪,返回结果
router.onReady(()=>{
// 返回当前实例 app
resolve(app);
}, reject)
})
}
webpack 打包
安装依赖
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");
// *** 根据传入环境变量决定入口文件和相应配置项
// 环境变量 WEBPACK_TARGET :决定入口是客户端还是服务端
// node ? server : client
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false
},
outputDir: './dist/' + target,
configureWebpack: () => ({
// *** 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// *** target设置为node使webpack以Node适用的方式处理动态导入,
// 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
target: TARGET_NODE ? "node" : "web",
// 是否模拟node全局变量
node: TARGET_NODE ? undefined : false,
output: {
// 此处使用Node风格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
// 优化代码
// externals: TARGET_NODE
// ? nodeExternals({
// // 不要外置化webpack需要处理的依赖模块。
// // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// // 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
// whitelist: [/\.css$/]
// })
// : undefined,
// optimization: {
// splitChunks: undefined
// },
// *** 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
// 客户端默认文件名为 `vue-ssr-client-manifest.json`。
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
// 优化代码 不太重要
chainWebpack: config => {
// cli4项目添加
if (TARGET_NODE) {
config.optimization.delete('splitChunks')
}
config.module
.rule("vue")
.use("vue-loader")
.tap(options => {
merge(options, {
optimizeSSR: false
});
});
}
};
打包脚本
"build": "npm run build:server & npm run build:client",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
宿主文件
./public/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>vue ssr moira</title> </head> <body> <!--vue-ssr-outlet--> </body></html>
服务器启动文件
修改服务器启动文件,现在需要处理所有路由,./server/index_new.js
const express = require('express')const Vue = require('vue')const fs = require('fs') //文件系统const path = require('path')// 创建express实例和vue实例const app = express()// const renderer = require('vue-server-renderer').createRenderer()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')// 参数1:服务端的包// 将来生成的页面要把清单解析出来附加到页面中// 最后生成的html页面会有一些js需要附加,客户端清单告诉了js附加到哪里const renderer = createBundleRenderer(serverBundle, { // 上下文 runInNewContext: false, // 这里需要用path转换为绝对地址 template: fs.readFileSync(path.join(__dirname, '../public/index.template.html'), 'utf-8'), //宿主模板文件,使用文件系统的方式导入,使用fs clientManifest})// 中间件处理静态文件请求// 需要把dist client js暴露给客户端,前端是可以访问的// express.static() 静态文件私服app.use(express.static('../dist/client', { index: false }))// 路由请求// * 将来前端请求什么我都不关心,所有路由都让renderer接管// 路由的权限由vue接管// 路由的处理交给vueapp.get('*', async (req, res) => { try { // 创建上下文 const context = { url: req.url, title: 'ssr moira title' } // renderer此时是BundleRenderer,接收上下文,而不是vue实例 约定 const html = await renderer.renderToString(context); res.send(html) } catch (error) { // 返回一个状态码,500,服务器内部错误 res.status(500).send('服务器内部错误') }});app.listen(5200, () => { console.log('渲染服务器启动成功!')})
打包后的server端:
json文件,能够告诉渲染器必要的渲染信息。打包信息。
打包后的client端:
完整。index.html 宿主文件。js文件 动态从前端加载。
client目录需要成为服务器的静态目录。
vue-ssr-client-manifest.json 前端挂载
defer 不能阻塞程序 延迟加载
SSR优缺点
优点
与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。
更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。
缺点
使用服务器端渲染 (SSR) 时还需要有一些权衡之处:
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
- 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
- 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间 (time-to-content) 对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染 (SSR) 将是一个小题大作之举。然而,内容到达时间 (time-to-content) 要求是绝对关键的指标,在这种情况下,服务器端渲染 (SSR) 可以帮助你实现最佳的初始加载性能。
整合Vuex
数据预取{ 组件中的数据预取. 服务端数据预取. 客户端数据预取处理}
优化:
- 页面缓存
- pm2守护node进程配置,用它来做自动重启、性能监控以及负载均衡。