文章来自
带你五步学会Vue-SSR
Vue服务器端渲染
构建一个SSR应用程序
SSR热更新
github项目
Vue-SSR优缺点
- 请求到的首屏页面是服务器渲染好的了,SEO很好
- 但是对服务器的压力很大
安装插件
# 安装 vue-server-renderer
# 安装 lodash.merge
# 安装 webpack-node-externals
# 安装 cross-env
npm install vue-server-renderer lodash.merge webpack-node-externals cross-env --save-dev
# 安装 koa
# 安装 koa-static
npm install koa koa-static --save
改造vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore() {
return new Vuex.Store({
state: {
test: ''
},
mutations: {
SET_TEST(state, data) {
state.test = data;
}
},
actions: {
test({ commit },opt) {
// 异步查询
commit('SET_TEST', data);
}
}
});
}
改造vue-route
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export function createRouter() {
return new Router({
mode: 'history', // 注意这里要使用history模式,因为hash不会发送到服务端
routes: []
});
}
改造main.js
import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';
import './assets/css/style.scss';
import './assets/iconfont/iconfont.css';
Vue.config.productionTip = false;
export function createApp() {
const router = createRouter();
const store = createStore();
const app = new Vue({
router,
store,
render: h => h(App)
});
return { app, router, store };
}
改造所有的vue文件
<template>
<div>{{ test }}</div>
</template>
<script>
export default {
// 这个方法需要主动调用,等于是自定义生命周期函数,而且必须是这个名字
asyncData ({ store, route }) {
return store.dispatch('test', route.params.id)
},
computed: {
// 当asyncData被执行,vuex数据改变,导致computed发生改变
test() {
return this.$store.state.test
}
}
}
</script>
创建entry-client.js
import { createApp } from './main';
// 客户端特定引导逻辑……
const { app, router, store } = createApp();
// 如果有__INITIAL_STATE__变量,则将store的状态用它替换
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData方法
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to);
const prevMatched = router.getMatchedComponents(from);
// 我们只关心非预渲染的组件
// 所以我们对比它们,找出两个匹配列表的差异组件
let diffed = false;
const activated = matched.filter((c, i) => {
return diffed || (diffed = prevMatched[i] !== c);
});
if (!activated.length) {
return next();
}
Promise.all( activated.map(c => {
// 把所有的vue里的asyncData方法一起执行了
if (c.asyncData) {
return c.asyncData({ store, route: to });
}
})
).then(() => {
next();
}).catch(next);
});
// 将Vue实例挂载到dom中,完成浏览器端应用启动
app.$mount('#app');
});
创建entry-server.js
import { createApp } from './main';
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise
// 以便服务器能够等待所有的内容在渲染前,就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, store, router } = createApp();
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 });
}
Promise.all( matchedComponents.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: router.currentRoute});
}
})
).then(() => {
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中
context.state = store.state;
// 返回根组件
resolve(app);
})
.catch(reject);
}, reject);
});
};
修改vue.config.js
有些教程是把这个分成三个配置文件,效果也一样
const path = require('path');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const nodeExternals = require('webpack-node-externals'); // 忽略node_modules文件夹中的所有模块
// const merge = require('lodash.merge');
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const target = TARGET_NODE ? 'server' : 'client'; //根据环境变量来指向入口
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
//基本路径
publicPath: process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8080' : './',
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
// productionSourceMap: false,
// 输出文件目录
outputDir: 'dist',
css: {
extract: process.env.NODE_ENV === 'production',
sourceMap: true
//向 CSS 相关的 loader 传递选项(支持 css-loader postcss-loader sass-loader less-loader stylus-loader)
},
configureWebpack: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 需要开启source-map文件映射,因为服务器端在渲染时,
// 会通过Bundle中的map文件映射关系进行文件的查询
devtool: 'source-map',
// 服务器端在Node环境中运行,需要打包为类Node.js环境可用包(使用Node.js require加载chunk)
// 客户端在浏览器中运行,需要打包为类浏览器环境里可用包
target: TARGET_NODE ? 'node' : 'web',
// 关闭对node变量、模块的polyfill
node: TARGET_NODE ? undefined : false,
output: {
// 配置模块的暴露方式,服务器端采用module.exports的方式,客户端采用默认的var变量方式
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
// 外置化应用程序依赖模块。可以使服务器构建速度更快
externals: TARGET_NODE
? nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: [/.css$/]
})
: undefined,
optimization: {
splitChunks: TARGET_NODE ? false : undefined
},
// 根据之前配置的环境变量判断打包为客户端/服务器端Bundle
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
// 关闭vue-loader中默认的服务器端渲染函数
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
// merge(options, {
// optimizeSSR: false
// });
options.optimizeSSR = false;
return options;
});
config.resolve.alias
.set('@src', resolve('src'))
.set('@api', resolve('src/api'))
.set('@assets', resolve('src/assets'))
.set('@comp', resolve('src/components'))
.set('@views', resolve('src/views'));
},
devServer: {
historyApiFallback: true,
headers: { 'Access-Control-Allow-Origin': '*' }
// port: 8088
// proxy: { ... }
// }
},
lintOnSave: false
};
创建index.temp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }}</title>
</head>
<body>
<div id="app">
<!--vue-ssr-outlet-->
</div>
</body>
</html>
修改package.json
// 在原本的dev和build基础上加上
"build:server": "cross-env NODE_ENV=production WEBPACK_TARGET=node vue-cli-service build",
运行npm run build:server
会生成两个json
创建service.js
const fs = require("fs");
const Koa = require("koa");
const path = require("path");
const koaStatic = require('koa-static')
const app = new Koa();
const resolve = file => path.resolve(__dirname, file);
// 开放dist目录
app.use(koaStatic(resolve('./dist')))
// 第 2 步:获得一个createBundleRenderer
const template = fs.readFileSync(resolve("./public/index.temp.html"), "utf-8");
const { createBundleRenderer } = require("vue-server-renderer");
const bundle = require("./dist/vue-ssr-server-bundle.json");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: template,
clientManifest: clientManifest
});
function renderToString(context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
// 第 3 步:添加一个中间件来处理所有请求
app.use(async (ctx, next) => {
const context = {
title: "ssr-test",
};
// 将 context 数据渲染为 HTML
const html = await renderToString(context);
ctx.body = html;
});
const port = 3000;
app.listen(port, function() {
console.log(`server started at localhost:${port}`);
});
修改package.json
// 再加上
"dev:serve": "node server.js",
全部配置完成,先执行打包再启动服务
其他配置
- 服务器路由缓存,加快编译