服务端渲染 SSR
目标
- ssr 概念
- vue ssr 原生实现
- nuxt.js
理解 ssr
传统服务端渲染 SSR VS 单页面应用 SPA VS 服务端渲染 SSR
传统 web 开发
传统 web 开发,网页内容在服务端渲染完成,一次性传输到浏览器
单页面应用 Single Page App
单页应用优秀的用户体验,使其逐渐成为主流,页面内容由 JS 渲染出来,这种方式称为客户端渲染
Spa 缺点:
- seo
- 首屏内容到达时间
服务端渲染 Server Side Render
SSR 解决方案,后端渲染出完整的首屏的 dom 结构返回,前端拿到的内容包括首屏及完整 spa 结构,应用激活后依照 spa 方式运行,这种页面渲染方式被称为服务端渲染(server side render)
Vue SSR 实战
新建工程
vue-cli 创建工程即可
vue create ssr
安装依赖
npm install vue-server-renderer express -D
确保 vue、vue-server-render、 vue-template-compiler 版本一致
启动脚本
创建一个 express 服务器,将 vue ssr 集成进来
//nodejs代码
//express是我们的web服务器
const express = require("express");
const path = require("path");
const fs = require("fs");
//获取express实例
const server = express();
const vue = require("vue");
//获取绝对路由函数
function resolve(dir) {
// 把当前执行js文件绝对地址和传入dir做拼接
return path.resolve(__dirname, dir);
}
//2.获取渲染器实例
const { createRenderer } = require("vue-server-renderer");
const renderer = createRenderer();
//处理favicon
const favicon = require("serve-favicon");
server.use(favicon(path.join(__dirname, "../public", "favicon.ico")));
//静态资源处理
server.use(express.static(resolve("../dist/client"), { index: false }));
//编写路由处理不同url请求
server.get("*", (req, res) => {
//console.log(req.url)
//解析模板名称 /user
const template = req.url.substr(1) || "index";
//加载模板
const buffer = fs.readeFileSync(path.join(__dirname, `${template}.html`));
//res.send('<strong>hello world</strong>')
//1.创建vue实例
const app = new Vue({
template: buffer.toString(), //转换为模板字符串
data() {
return { msg: "vue ssr" };
},
methods: {
onClick() {
console.log("do something");
},
},
});
//3.用渲染器渲染vue实例
renderer
.renderToString(app)
.then((html) => {
res.send(html);
})
.catch((err) => {
res.status(500);
res.send("Internal Server Error, 500!");
});
});
//监听端口
server.listen(3000, () => {
console.log("server running!");
});
//解决express前端请求过来的/favicon请求
npm i serve-favicon -s
路由
路由支持仍然使用 vue-router
安装
若未引入 vue-router 则需要安装
npm i vue-router -s
配置
创建@/router/index.js
import Vue from "vue";
import Router from "vue-router";
import Home from "@/views/Home";
import About from "@/views/About";
Vue.use(Router);
//导出工厂函数
//修改1:路由这里是工厂函数
export function createRouter() {
return new Router({
routes: [
{ path: "/", component: Home },
{ path: "/about", component: About },
],
});
}
构建
对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包-服务器需要[服务器 bundle]然后用于服务器端渲染(SSR),而[客户端 bundel]会发送给浏览器,用于混合静态标记
//main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
//返回工厂函数,每次请求创建一个Vue实例
export function createApp(context) {
//1.创建路由器实例
const router = createRouter();
//2.创建Vue实例
const app = new Vue({
router,
context, //上下文用于给渲染器传递参数
render: (h) => h(App),
});
return { app, router };
}
//entry-server.js
//调用刚才main里面的工厂函数创建实例
import { createApp } from "./main";
//该函数会被express路由处理函数调用,用于创建vue实例
export default (context) => {
// 返回promise,确保异步的操作都结束
return new Promise((resolve, reject) => {
// 获取vue/router实例
const { app, router, store } = createApp(context);
//显示首屏处理/跳转至首屏
//路由模式: hash/history/abstract
router.push(context.url);
//检测路由就绪事件
router.onReady(() => {
//获取匹配的路由组件数组
const matchedComponent = router.getMatchedComponents();
//若无匹配则抛出异常
if(!matchedComponents.length){
return reject({code:404})
}
//对所有匹配的路由组件调用可能存在的'asyncData()'
Promise.all(
//遍历匹配组件
mathedComponents.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);
});
};
//客户端也需要创建vue实例
//entry-client.js
import { createApp } from "./main";
const { app, router,store } = createApp();
//当使用template时,context.state将作为window.__INITIAL_STATE__状态自动嵌入到最终
if(window._INITIAL_STATE__){
//还原store状态
store.replaceState(window.__INITIAL_STATE__);
}
//路由就绪,执行挂载(激活过程)
router.onReady(() => {
//挂载激活
app.$mount("#app");
});
webpack 配置
安装依赖
npm install webpack-node-externals lodash.merge -D
具体配置,vue.config.js
脚本配置
安装依赖
npm i cross-env -D
定义创建脚本,package.json
"scripts": {
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build", "build": "npm run build:server && npm run build:client"
}
执行打包:npm run build
宿主文件
最后需要定义宿主文件,修改./public/index.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>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
服务器启动文件
修改服务器启动文件,现在需要处理所有路由,
//nodejs代码
//express是我们的web服务器
const express = require('express')
const path = require('path')
const fs = require('fs')
//获取express实例
const server = express()
const vue = require('vue')
//获取绝对路由函数
function resolve(dir) {
// 把当前执行js文件绝对地址和传入dir做拼接
return path.resolve(__dirname,dir )
}
//2.获取渲染器实例 第 2 步:获得一个createBundleRenderer
const { createBundleRenderer } = require('vue-server-renderer')
//参数1服务端brundle,第3步:导入服务端打包文件
const bundle = require(ewsolve('../dist/server/vue-ssr-server-bundle.json'))
// 第 4 步:创建渲染器
const template = fs.readFileSync(resolve("../public/index.html"), "utf-8");
const clientManifest = require(resolve("../dist/client/vue-ssr-client- manifest.json"));
const renderer = createBundleRenderer(bundle, {
runInNewContext: false, // https://ssr.vuejs.org/zh/api/#
runinnewcontext template, // 宿主文件
clientManifest // 客户端清单
})
//处理favicon
const favicon = require('serve-favicon')
server.use(favicon(path.join(__dirname,'../public','favicon.ico')))
//静态资源处理 第1步:开放dist/client目录,关闭默认下在index页的选项,不然到不了后面路由
// /index.html
server.use(express.static(resolve('../dist/client'),{index: false}))
//编写路由处理不同url请求
server.get('*', async (req,res) => {
//构造上下文
const context = {
title: 'ssr test',
url: req.url //首屏地址
}
try{
//渲染输出
const html = await renderer.renderToString(context)
//响应给前端
res.send(html)
} catch(error) {
res.status(500).send(''服务器渲染出错)
}
})
//监听端口
server.listen(3000, () => {
console.log('server running!')
})
整合VueX
安装vuex
npm install -s vuex
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
VUe.use(Vuex)
//工厂函数
export function createStore () {
return new Vuex.Store({
state: {
count:108
}
mutations: {
add(state){
state.count += 1;
}
//加一个初始化
init(state, count){
state.count = count;
}
},
actions: {
//加一个异步请求count的action
getCount({commit}){
//加一个异步请求count的action
return new Promise(resolve => {
})
}
}
})
}
挂载store,main.js
import {createStore} from './store'
export function createApp (context) {
//创建实例
const store = createStore()
const app = new Vue({
store,//挂载
render: h => h(App)
})
return { app, router, store}
}
使用,.src/components/index.vue
<h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
//main.js
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from './router';
import { createStore } from './store';
//确保客户端每个组件如果有asyncData,要执行之
Vue.mixin({
beforeMount() {
const { asyncData } = this.$options;
if(asyncData) {
//将获取数据操作分配给promise
//以便在组件中,我们可以在数据准备就绪后
//通过运行`this.dataPromisr.then(...) ` 来执行 其他任务
this.dataPromise = asyncData({
store:this.$store,
route:this.$route,
})
}
}
})
Vue.config.productionTip = false;
//返回工厂函数,每次请求创建一个Vue实例
export function createApp(context) {
//1.创建路由器实例
const router = createRouter();
const store = createStore();
//2.创建Vue实例
const app = new Vue({
router,
context, //上下文用于给渲染器传递参数
store
render: (h) => h(App),
});
return { app, router,store };
}
数据预取
服务器端渲染的时应用程序的”快照”,如果应用依赖于一些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据
异步数据获取,store/index.js
export function createStore() {
return new Vuex.Store({
mutations: {
//加一个初始化
init(state, count){
state.count = count;
}
},
actions:{
//加一个异步请求count的action
getCount({commit}){
return new Promise(resolve => {
setTimeput(() => {
commit("init", Math.random() * 100);
resolve();
}, 1000)
})
}
}
})
}
总结
SSR优缺点都很明显
优点:
seo
首屏显示时间
缺点:
开发逻辑复杂
开发条件限制:比如一些生命周期不能用,一些第三方库会不能用
服务器负载大
已经存在spa
需要seo页面是否只是少数几个营销页面预渲染是否可以考虑
确实需要做ssr改造,利用服务器端爬虫技术puppeteer
最后选择重构
全新项目建议nuxt.js
Nuxt.js实战
Nuxt.js是一个基于vue。js的通用应用框架。
通过对客户端/服务端基础架构的抽象组织,Nuxt.js主要关注的是应用的UI渲染。
nuxt.js特性
- 代码分层
- 服务端渲染
- 强大的路由功能
- 静态文件服务
- 。。。
nuxt渲染流程
一个完整的服务器请求到渲染的流程
nuxt安装
运行create-nuxt-app
npx create-nuxt-app <项目名>
选项
运行项目: npm run dev
案例
实现如下功能点
- 服务端渲染
- 权限控制
- 全局状态管理
- 数据接口调用
nuxt路由配置
配置嵌套路由
![](nuxt-children .png)
路由传参
路由跳转
路由配置
视图
下图展示了Nuxt.js如何为指定的路由配置数据和视图
默认布局
查看layouts/default.vue
<template>
<nuxt/>
</template>
自定义布局
创建空白布局页面layouts/blank.vue,用于login.vue
<template>
<div>
<nuxt/>
</div>
</template>
页面 pages/login.vue 使用自定义布局:
export default { layout: 'blank' }
自定义错误页面
创建layouts/error.vue
<template>
<div class="container">
<h1 v-if="error.statusCode === 404">页面不存在</h1>
<h1 v-else>应用发生错误异常</h1>
<p>{{error}}</p>
<nuxt-link to="/">首 页</nuxt-link>
</div>
</template>
<script>
export default {
props: ['error'], layout:'blank'
}
</script>