vue-cli3 SSR 服务端渲染从零剖析构建
关注公众号: 微信搜索 前端工具人
; 收货更多的干货
一、开篇
- 上篇 vue2.x SSR 服务端渲染从零剖析构建,优缺点解读 , 介绍了
SSR
的优缺点及从零构建配置 - 接下来介绍
vue-cli3
搭建SSR
以及热重载
(开发过程中无需每次打包运行,实时看到更改效果)提升开发效率
; - 热重载个人觉得是开发
SSR
项目 必备姿势; - 这里就不在介绍 SSR 原理,优缺点之类的了; 详情请移步上篇文章 vue2.x SSR 服务端渲染从零剖析构建,优缺点解读
坑也不少
,具体的还是忘了,大多数都是 插件依赖版本问题,可参考我的目录,或者留言;看到后及时回复- 源码地址:
https://github.com/laijinxian/vue2.x-ssr-template/tree/master/vue-cli3.x-ssr
来个star
呗
二、SSR 代码构建
1. vue-router
导出一个工厂函数,用于创建新的
import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home'
Vue.use(VueRouter);
/**
* TODO
* 注意: 默认路由不要使用懒加载 component: () => import('../views/About'), 后续路由可以
* 不然会报错 repalce ... router ... 错什么的;
*/
const routes = [{
path: '/',
name: 'Home',
component: Home
},{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: 'about' */ '../views/About')
}];
export function createRouter() {
return new VueRouter({
mode: 'history', // 必须是history 模式
routes
})
}
2. main.js
导出一个工厂函数,用于创建新的
// 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.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
import '@/mixins'
// 导出一个工厂函数,用于创建新的
export function createApp () {
// 创建 router 和 store 实例
const router = createRouter()
const store = createStore()
// 同步路由状态(route state)到 store
sync(store, router)
// 创建应用程序实例,将 router 和 store 注入
const app = new Vue({
router,
store,
render: h => h(App)
})
// 暴露 app, router 和 store。
return { app, router, store }
}
3. src
下新增 entry-client.js
及 entry-server.js
// entry-client.js
// 写法 1
// import { createApp } from './main'
// const { app, router, store } = createApp()
// if (window.__INITIAL_STATE__) {
// store.replaceState(window.__INITIAL_STATE__)
// }
// router.onReady(() => {
// app.$mount('#app')
// })
// 写法 2
import { createApp } from './main'
const { app, router, store } = createApp()
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
// 添加路由钩子函数,用于处理 asyncData.
// 在初始路由 resolve 后执行,
// 以便我们不会二次预取(double-fetch)已有的数据。
// 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
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()
// 这里如果有加载指示器 (loading indicator),就触发
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
// 停止加载指示器(loading indicator)
next()
}).catch(next)
})
app.$mount('#app', true)
})
// entry-server.js
import { createApp } from './main'
// 导出函数将由 bundlerender 调用
// 数据预取是异步的,所以应该返回一个Promise
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// 匹配不到的路由,执行 reject 函数,返回错误
if (!matchedComponents.length) {
// 做点什么
// vue-cli3 直接reject 会导致引入文件错误
// return reject(new Error({ code: 404 }))
}
// 对所有匹配的路由组件调用 `asyncData()`
Promise.all(matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
})
}
})).then(() => {
// 在所有预取钩子(preFetch hook) resolve 后,
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,
// 并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.rendered = () => {
context.state = store.state
}
resolve(app)
}).catch(reject)
resolve(app)
}, reject)
})
}
4. 增加 vue.config.js
这是 vue-cli3
的 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');
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const target = TARGET_NODE ? 'server' : 'client';
module.exports = {
configureWebpack: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
target: TARGET_NODE ? 'node' : 'web',
node: TARGET_NODE ? undefined : false,
output: {
libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: TARGET_NODE
? nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
allowlist: [/\.css$/]
})
: undefined,
optimization: { splitChunks: TARGET_NODE ? false : undefined },
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
merge(options, {
optimizeSSR: false
});
});
// fix ssr hot update bug
if (TARGET_NODE) {
config.plugins.delete("hmr");
}
}
}
5. src
下新增 template/index.ssr.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>index.template.html</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
6. src
下新增 server/index.js
const fs = require("fs");
const Koa = require("koa");
const express = require('express')
const path = require("path");
const koaStatic = require("koa-static");
const app = new Koa();
// const app = express()
const resolve = file => path.resolve(__dirname, file);
// 1. 开放dist目录
app.use(koaStatic(resolve("./dist")));
// 2. 获取一个 createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require(resolve('../dist/vue-ssr-server-bundle.json'))
const clientManifest = require(resolve('../dist/vue-ssr-client-manifest.json'))
const template = fs.readFileSync(resolve('../src/template/index.ssr.html'), 'utf-8')
const renderer = createBundleRenderer(serverBundle, {
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 => {
const context = {
title: "ssr test",
url: ctx.url
};
// 将 context 数据渲染为 HTML
const html = await renderToString(context);
ctx.body = html;
});
const port = process.env.PORT || 8090
app.listen(port, () => {
console.log(`server started at localhost:${port}`)
})
7. package.json 增加打包命令
...
"start": "node server/index.js",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
"build": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json"
...
8. 执行命令构建
yarn run build
ornpm run build
yarn run start
ornpm run start
- 浏览器输入
http://localhost:8090/
即可看到页面 - 怎么辨别是否服务端渲染成功 请看
上篇文章
有介绍
三、 热重载 --> 开发利器
1. 原理 参考 锅巴哥热重载文章
- 通过
compiler
编译webpack
配置文件,监听文件修改,获取最新的vue-ssr-server-bundle.json
实时编译 - 通过
webpack dev server
获取最新的vue-ssr-client-manifest.json
- 结合
vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
渲染html
页面返回给浏览器
2. 安装需要的插件
yarn add -D webpack memory-fs concurrently
orcnpm install webpack memory-fs concurrently -D
yarn add koa-router axios
orcnpm install koa-router axios -S
3. src/serve
下 新增 setup-dev-server.js
const webpack = require("webpack");
const axios = require("axios");
const MemoryFS = require("memory-fs");
const fs = require("fs");
const path = require("path");
const Router = require("koa-router");
// 1. webpack的配置文件在 /node_modules/@vue/cli-service/webpack.config.js 中
const webpackConfig = require("@vue/cli-service/webpack.config");
const { createBundleRenderer } = require("vue-server-renderer");
// 2. 编译 webpack 配置文件
const serverCompiler = webpack(webpackConfig);
const mfs = new MemoryFS();
// 指定输出文件到内存流中
serverCompiler.outputFileSystem = mfs;
// 3. 监听文件修改,实时编译获取最新的 vue-ssr-server-bundle.json
let bundle;
serverCompiler.watch({}, (err, stats) => {
if (err) {
throw err;
}
stats = stats.toJson();
stats.errors.forEach(error => console.error(error));
stats.warnings.forEach(warn => console.warn(warn));
const bundlePath = path.join(
webpackConfig.output.path,
"vue-ssr-server-bundle.json"
);
bundle = JSON.parse(mfs.readFileSync(bundlePath, "utf-8"));
console.log("new bundle generated");
});
// 处理请求
const handleRequest = async ctx => {
console.log("path", ctx.path);
if (!bundle) {
ctx.body = "等待webpack打包完成后在访问";
return;
}
// 4. 获取最新的 vue-ssr-client-manifest.json
const clientManifestResp = await axios.get(
"http://localhost:8080/vue-ssr-client-manifest.json"
);
const clientManifest = clientManifestResp.data;
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync(
path.resolve(__dirname, "../src/index.temp.html"),
"utf-8"
),
clientManifest: clientManifest
});
const html = await renderToString(ctx, renderer);
ctx.body = html;
};
function renderToString(context, renderer) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
const router = new Router();
router.get("*", handleRequest);
module.exports = router;
4. src/serve
下 新增 ssr.js
开发热重载入口文件
// server/ssr.js
const Koa = require("koa");
const KoaStatis = require("koa-static");
const path = require("path");
const resolve = file => path.resolve(__dirname, file);
const app = new Koa();
const isDev = process.env.NODE_ENV !== "production";
const router = isDev ? require("./setup-dev-server.js") : require("./index.js");
app.use(router.routes()).use(router.allowedMethods());
// 开放目录
app.use(KoaStatis(resolve("../dist")));
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`server started at localhost: ${port}`);
});
module.exports = app;
5. package.json
增加构建命令
...
"dev": "concurrently \"npm run serve\" \"npm run dev:serve\" ",
"dev:serve": "cross-env WEBPACK_TARGET=node node ./server/ssr.js",
...
6. 执行命令构建
yarn run dev
ornpm run dev
- 浏览器输入
http://localhost:8081/
即可看到效果 - 测试 随便一个
vue
文件更改点东西 浏览器便自动重载效果
7. 提示
热重载原理可参考 锅巴哥热重载文章 有详细介绍
四、 结语
vue
不同版本的ssr
服务端到这就结束了- 两个版本遇到的坑都蛮多的,大多数都是依赖版本问题, 百度难的那种 ...
- 后续应该还会加上
vue3.0
的ssr
版本吧, 不过时间不确定了。 - 坑多但真不记得具体的了, 有问题可以留言 或者
github
提Issues