Vue CLI构建Vue SSR
Vue SSR官方的指导教程只有简单的Webpack配置,没有指导如果使用Vue CLI来构建SSR。Vue CLI构建CSR的SPA比较方便,默认不支持构建SSR。好在Vue CLI的可定制性比较强,经过一番的摸索,终于成功使用Vue CLI构建Vue SSR。
在构建SSR之前,需要按照官方的指导完成代码的改造,让SSR与CSR代码复用。
1. 代码改造
本节均按照Vue SSR官方指导来完成代码改造: main.ts、entry-client.ts、entry-server.ts。比较特殊的是:main.ts 中去除import './registerServiceWorker';
,NodeJS中没有service worker,避免SSR执行时异常。
// main.ts
/* eslint-disable import/prefer-default-export */
import Vue from 'vue';
import { sync } from 'vuex-router-sync';
import App from './App.vue';
// import './registerServiceWorker'; // 不能直接导入 service worker 相关的内容!!!
import { createRouter } from './router';
import { createStore } from './store';
Vue.config.productionTip = false;
export function createApp() {
const router = createRouter();
const store = createStore();
sync(store, router);
const app = new Vue({
router,
store,
render: (h) => h(App),
});
return { app, router, store };
}
// entry-client.ts
import { createApp } from './main';
const { app, router } = createApp();
router.onReady(() => {
app.$mount('#app');
});
// entry-server.ts
import { createApp } from './main';
import { AsyncDataComponent } from './views/aysncParam';
export interface Context {
url: string,
title: 'Vue App',
metas: `<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue srr demo">`,
state: {
[key: string]: any,
},
}
export interface RespStatusCodeError extends Error {
code: number;
message: string;
}
export class PageNotFoundError extends Error implements RespStatusCodeError {
code: number;
message: string;
constructor(...args: []) {
super(...args);
this.code = 404;
this.message = 'Page not found';
}
}
export default (context: Context) => new Promise((resolve, reject) => {
const { app, router, store } = createApp();
// 设置服务器端 router 的位置
router.push(context.url);
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents() as AsyncDataComponent[];
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
reject(new PageNotFoundError());
return;
}
Promise.all(matchedComponents.map((Component) => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
return undefined;
})).then(() => {
// 在所有预取钩子(preFetch hook) resolve 后,
// 我们的 store 现在已经填充入渲染应用程序所需的状态。
// 当我们将状态附加到上下文,
// 并且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state;
resolve(app);
}).catch(reject);
}, reject);
});
2. 定义构建命令
在 package.json 中添加 build:ssr
命令:
{
"scripts": {
"serve": "cross-env VUE_SSR=false vue-cli-service serve --open",
"build": "cross-env VUE_SSR=false vue-cli-service build",
"build:ssr": "cross-env VUE_SSR=true vue-cli-service build --dest dist-ssr --formats commonjs --skip-plugins pwa,workbox"
},
}
主要是添加 VUE_SSR
环境变量,让后面的 vue.config.js
能够识别构建目标。
3. 配置Vue CLI脚本
在项目根目录添加 vue.config.js
:
// vue.config.js
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const isClient = process.env.VUE_SSR !== 'true';
const isProd = process.env.NODE_ENV === 'production';
console.log('process.env.VUE_SSR', process.env.VUE_SSR);
console.log('isClient', isClient);
const getClientWebpackConfig = () => ({
plugins: [
new VueSSRClientPlugin(),
],
});
const getServerWebpackConfig = () => ({
target: 'node',
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2',
},
plugins: [
new VueSSRServerPlugin(),
],
optimization: {
minimize: false,
runtimeChunk: false,
splitChunks: false,
},
});
module.exports = {
pages: {
index: {
entry: isClient ? ['src/entry-client', 'src/registerServiceWorker'] : 'src/entry-server',
},
},
css: {
extract: isClient && isProd,
},
configureWebpack: isClient ? getClientWebpackConfig() : getServerWebpackConfig(),
};
在构建SSR时,css.extract
必需设为false
。否则,动态导入的模块包含CSS时,NodeJS因无document而异常。
4. SSR服务脚本
// server/index.js
/* eslint-disable @typescript-eslint/no-var-requires, import/no-extraneous-dependencies */
const { createBundleRenderer } = require('vue-server-renderer');
const server = require('express')();
const serverBundle = require('../dist-ssr/vue-ssr-server-bundle.json');
const clientManifest = require('../dist/vue-ssr-client-manifest.json');
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推荐
// template, // (可选)页面模板
clientManifest, // (可选)客户端构建 manifest
});
server.get('*', (req, res) => {
const context = { url: req.url };
// 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。
// 现在我们的服务器与应用程序已经解耦!
renderer.renderToString(context, (err, html) => {
// 处理异常……
res.end(html);
});
});
server.listen(8080);