CSR(传统的浏览器端渲染)
Client Side Render
通常,我们的Vue项目是在npm run build打包之后,直接放到服务器端。浏览器去请求相应的html,加载对应的js文件,生成DOM。
路由改变,局部刷新,浏览器不会刷新
缺点
需要js全部加载完,页面才能出来,加载较慢(懒加载)
js改变dom,生成页面,不利于SEO
SSR(服务器端渲染是什么)
Server Side Render
SSR的原理是将打包后的文件,先在服务器端处理,生成一个个的HTML字符串,当浏览器请求的时候直接发过去。
每一个路由请求到的都是一个html串,路由改变,浏览器刷新
优点
利于SEO,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
更快的内容到达时间 (time-to-content),更好的用户体验,特别是对于缓慢的网络情况或运行缓慢的设备。(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
缺点
- 复杂度
- 库的支持性,代码兼容
- 性能问题,每个请求都是n个实例的创建,不然会污染,消耗会变得很大
缓存 node serve 、 nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。
降级:监控cpu、内存占用过多,就spa,返回单个的壳 - 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用(比如一整套图表页面,相对于服务端渲染,可能用户不会在乎初始加载的前几秒,可以交由客户端使用类似于骨架屏,或者懒加载之类的提升用户体验)
预渲染
只是用来改善少数营销页面的 SEO,可以使用预渲染(例如:pre-renderer),在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件,而无需使用 web 服务器实时动态编译 HTML。
也可以考虑用爬虫工具,比如puppeteer,让它直接从spa项目中爬出结果
nuxt配置SSR
nuxt中文网
全新项目,可以考虑用nuxt来做
vue项目中手动加入SSR
1、改造router、store和main文件,将其中的new Router new Store和new Vue转化为方法,便于后面使用
router:
import Vue from 'vue'
import Router from 'vue-router'
import page1 from '@/components/page1'
import page2 from '@/components/page2'
Vue.use(Router)
export function createRouter(){
return new Router({
mode: "history",
routes: [
{
path: '/',
component: page1
},
{
path: '/page2',
component: page2
}
]
})
}
store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore(){
return new Vuex.Store({
state:{
count:0
},
mutations:{
//初始化
init(state,count){
state.count = count
},
add(state){
state.count++
}
},
actions:{
//异步请求count
getCount({commit}){
return new Promise(resolve=>{
setTimeout(()=>{
commit("init",Math.random()*100)
resolve()
},1000)
})
}
}
})
}
main:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import {createRouter} from './router'
import {createStore} from './store'
Vue.config.productionTip = false
//客户端数据预取处理
Vue.mixin({
beforeMount(){
const {asyncData} = this.$options
if(asyncData){
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 this.dataPromise.then(...)来执行其他任务
this.dataPromise = asyncData({
store: this.$store,
route: this.$route
})
}
}
})
/* eslint-disable no-new */
//这儿采用工厂函数导出vue实例,确保每次请求都为独立实例
export function createApp(context){
let router = createRouter();
let store = createStore();
//context用于给上下文传递参数
const app = new Vue({
router,
store,
context,
render:h => h(App)
})
return {app,router,store};
}
2、创建两个js文件,server.js和client.js
将之前的一次打包,转化为两次打包,一个用于服务端打包入口js文件,一个用于客户端打包入口js文件。
server.js
//服务器端打包入口文件
import {createApp} from './main'
//返回一个函数,接收请求上下文,返回创建的vue实例
//根据返回的内容,拿到指定的路由节点
export default context => {
//这里返回一个Promise,确保路由会组件准备就绪
return new Promise((resolve, reject)=>{
const {app, router, store} = createApp(context);
//跳转到首屏地址
router.push(context.url);
//路由就绪,返回结果
router.onReady(()=>{
//获取匹配的路由组件数组
const mathComponents = router.getMatchedComponents();
//若无匹配则抛出异常
if(!mathComponents){
return reject({code: 404});
}
//对所有匹配的路由组件调用可能存在的asyncData()
Promise.all(
mathComponents.map(Component=>{
if(Component.asyncData){
return Component.asyncData({
store,
route: router.currentRoute
})
}
})
).then(()=>{
// 所有预取钩子 resolve 后
// store 已经填充入渲染应用所需状态
// 将状态附加到上下文,且 template 选项用于 renderer 时
// 状态将自动序列化为 window.__INITIAL_STATE__,并注入 HTML
context.state = store.state;
resolve(app)
}).catch(reject)
},reject);
})
}
client.js
//客户端打包入口文件
import {createApp} from './main'
const {app,router,store} = createApp();
//当使用template时,context.state将作为window.__INITIAL_STATE__状态自动嵌入到最终的HTML
//在客户端挂载到应用程序之前,store就应该获取到状态
if(window.__INITIAL_STATE__){
store.replaceState(window.__INITIAL_STATE__)
}
//路由完成之后,再去进行挂载,以防有异步路由的情况
router.onReady(()=>{
app.$mount("#app");
})
3、新建存放服务器端生成的html模板
index.ssr.html用于保存服务器端生成的html(宿主文件),其中<!–vue-ssr-outlet–>为注入标记,即在这儿进行添加服务端返回内容
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ssr-test</title>
</head>
<body>
<!--vue-ssr-outlet-->
<script type="text/javascript" src="<%=htmlWebpackPlugin.options.files.js%>"></script>
</body>
</html>
vue文件
<template>
<div>
<h2>其他页</h2>
<p @click="$store.commit('add')">{{$store.state.count}}</p>
</div>
</template>
<script>
export default {
asyncData({store,route}){//约定预取逻辑编写在预取钩子asyncData中
//触发action后,返回Promise以便确定请求结果
return store.dispatch("getCount")
}
}
</script>
<style scoped>
</style>
4、创建客户端和服务器端打包配置文件
安装:
npm install vue vue-server-renderer --save
webpack.build.server.js拷贝webpack.prod.conf.js生产打包配置文件进行修改
添加入口文件
打包后的服务端文件需要在node下面执行,所以将target指定为node
nodejs的规范为commonjs,所以需要将output中的libraryTarget指定为commonjs。新版本node使用commonjs2
修改HtmlWebpackPlugin下面的模板文件和目标文件为我们创建的index.ssr.html
删除HtmlWebpackPlugin下面minify中的removeComments(删除注释),因为我们上面说的服务器端html模板中的注释有特定作用,不能被删除
引入vue-server-renderer下面的server-plugin,并进行注册
const VueSSRServePlugin = require('vue-server-renderer/server-plugin')
entry:{
app:'./src/server.js'
},
target: 'node',
output:{
libraryTarget:'commonjs2'
},
plugins: [
new VueSSRServePlugin(),
new HtmlWebpackPlugin({
filename: 'index.ssr.html',
template: 'index.ssr.html',
inject: true,
files:{
js:'app.js'
},
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
})
]
webpack.build.client.js拷贝webpack.prod.conf.js生产打包配置文件进行修改
添加入口文件
引入vue-server-renderer下面的client-plugin,并进行注册
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
entry:{
app:'./src/ client.js'
},
plugins: [
new VueSSRClientPlugin(),
]
5、package.json中新加两条打包命令
"build:client": "webpack --config build/webpack.build.client.js",
"build:server": "webpack --config build/webpack.build.server.js"
"build": "npm run build:server && npm run build:client",
npm run build
打包完成之后的目录
6、配置node服务
根目录创建server.js
const express = require('express');
const server = express();
const { createBundleRenderer } = require('vue-server-renderer');
const path = require('path');
const fs = require('fs');
const { target } = require('./config/dev.env');
//服务端打包文件地址
const serverBuild = require(path.resolve(__dirname, "./dist/vue-ssr-server-bundle.json"));
//客户端清单
const clientManifest = require(path.resolve(__dirname, "./dist/vue-ssr-client-manifest.json"));
//宿主文件
const template = fs.readFileSync(path.resolve(__dirname, "./dist/index.ssr.html"), 'utf-8');
//创建渲染器
const renderer = createBundleRenderer(serverBuild, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
server.get("*", (req, res) => {
if(req.url != "/favicon.ico"){
const context = { url: req.url };
//以流的形式分片读取大文件
const ssrStream = renderer.renderToStream(context);
ssrStream.on('error',(err)=>{
res.status(500).end('Internal Server Error')
console.log(err);
return;
});
let buffers = [];
ssrStream.on('data',(data) => buffers.push(data));
ssrStream.on('end', () => {res.end(Buffer.concat(buffers))});
}
})
server.listen(3000);
7、启动服务,查看效果
node server.js