标签:提高 多个 为我 contex 统计分析 tree 扩展 cto blog
在实现 egg + vue 服务端渲染工程化实现之前,我们先来看看前面两篇关于Webpack构建和Egg的文章:
从 Vue 的官方支持我们知道,Vue 是支持服务端渲染的,而且还提供了官方渲染插件 vue-server-renderer 提供了基于 JSBundle 或 JSON 文件渲染模式和流渲染模式。这里我们主要讲基于 JSBundle 的服务端渲染实现,流渲染模式目前在 Egg 框架里面与 Egg 部分插件有冲突(Header写入时机问题), 后续作为单独的研究课题。另外基于 Vue JSON 文件字符串构建渲染请移步 VueSSRPlugin 这种方案目前基于 Vue 官方的Plugin在构建上面只能构建单页面(生成一个json manfiest,多个会有冲突),完善的解决方案需要继续研究。
首先,我们来看看 vue-server-renderer 提供的 createBundleRenderer 和 renderToString 怎么把 JSBundle 编译成 HTML。
基于 vue-server-renderer 实现 JSBundle 主要代码如下:
const renderer = require(‘vue-server-renderer‘); // filepath 为 Webpack 构建的服务端代码 const bundleRenderer = renderer.createBundleRenderer(filepath, renderOptions); // data 为 Node端获取到的数据 const context = { state: data }; return new Promise((resolve, reject) => { bundleRenderer.renderToString(context, (err, html) => { if (err) { reject(err); } else { resolve(html); } });
这里面仅仅简单考虑了编译,对于缓存,资源依赖都没有考虑。其实在做 Vue 服务端渲染时,关键的地方就在于这里,如何保证 Vue 渲染的速度,同时也要满足实际的项目需要。
缓存
基于以上两点, 我们实现了 egg-view-vue 插件, 提供了 Vue 渲染引擎。在 Egg 项目里面,我们可以通过 this.app.vue 拿到 Vue 渲染引擎的实例,然后就可以根据提供的方法进行 Vue 编译成 HTML。
const Engine = require(‘../../lib/engine‘); const VUE_ENGINE = Symbol(‘Application#vue‘); module.exports = { get vue() { if (!this[VUE_ENGINE]) { this[VUE_ENGINE] = new Engine(this); } return this[VUE_ENGINE]; }, };
‘use strict‘; const Vue = require(‘vue‘); const LRU = require(‘lru-cache‘); const vueServerRenderer = require(‘vue-server-renderer‘); class Engine { constructor(app) { this.app = app; this.config = app.config.vue; this.vueServerRenderer = vueServerRenderer; this.renderer = this.vueServerRenderer.createRenderer(); this.renderOptions = this.config.renderOptions; if (this.config.cache === true) { this.bundleCache = LRU({ max: 1000, maxAge: 1000 * 3600 * 24 * 7, }); } else if (typeof this.config.cache === ‘object‘) { if (this.config.cache.set && this.config.cache.get) { this.bundleCache = this.config.cache; } else { this.bundleCache = LRU(this.config.cache); } } } createBundleRenderer(name, renderOptions) { if (this.bundleCache) { const bundleRenderer = this.bundleCache.get(name); if (bundleRenderer) { return bundleRenderer; } } const bundleRenderer = this.vueServerRenderer.createBundleRenderer(name, Object.assign({}, this.renderOptions, renderOptions)); if (this.bundleCache) { this.bundleCache.set(name, bundleRenderer); } return bundleRenderer; } renderBundle(name, context, options) { context = context || /* istanbul ignore next */ {}; options = options || /* istanbul ignore next */ {}; return new Promise((resolve, reject) => { this.createBundleRenderer(name, options.renderOptions).renderToString(context, (err, html) => { if (err) { reject(err); } else { resolve(html); } }); }); } renderString(tpl, locals, options) { const vConfig = Object.assign({ template: tpl, data: locals }, options); const vm = new Vue(vConfig); return new Promise((resolve, reject) => { this.renderer.renderToString(vm, (err, html) => { if (err) { reject(err); } else { resolve(html); } }); }); } } module.exports = Engine;
资源依赖
基于以上两点, 我们实现了 egg-view-vue-ssr 插件, 解决资源依赖和数据问题。该插件是基于 egg-view-vue 扩展而来, 会覆盖 render 方法。 目前的实现方式会产生一个问题,具体请看 多引擎问题 。
inject(html, context, name, config, options) { const fileKey = name; const fileManifest = this.resourceDeps[fileKey]; if (fileManifest) { const headInject = []; const bodyInject = []; const publicPath = this.buildConfig.publicPath; if (config.injectCss && (options.injectCss === undefined || options.injectCss)) { fileManifest.css.forEach(item => { headInject.push(this.createCssLinkTag(publicPath + item)); }); } else { headInject.push(context.styles); } if (config.injectJs) { fileManifest.script.forEach(item => { bodyInject.push(this.createScriptSrcTag(publicPath + item)); }); if (!/window.__INITIAL_STATE__/.test(html)) { bodyInject.unshift(`<script> window.__INITIAL_STATE__= ${serialize(context.state, { isJSON: true })};</script>`); } } this.injectHead(headInject); html = html.replace(this.headRegExp, match => { return headInject.join(‘‘) + match; }); this.injectBody(bodyInject); html = html.replace(this.bodyRegExp, match => { return bodyInject.join(‘‘) + match; }); } return config.afterRender(html, context); }
在开头我们提到了 easywebpack-vue 构建方案,我们可以通过该解决方案完成 Webpack + Vue 的构建方案。具体实现请看 Webpack工程化解决方案easywebpack 和 easywebpack-vue插件。 这里我们直接提供 webpack.config.js 配置,根据该配置即可完成 Vue 前端渲染构建和 Node 层构建。
‘use strict‘; const path = require(‘path‘); module.exports = { egg: true, framework: ‘vue‘, entry: { include: [‘app/web/page‘, { ‘app/app‘: ‘app/web/page/app/app.js?loader=false‘ }], exclude: [‘app/web/page/[a-z]+/component‘, ‘app/web/page/test‘, ‘app/web/page/html‘, ‘app/web/page/app‘], loader: { client: ‘app/web/framework/vue/entry/client-loader.js‘, server: ‘app/web/framework/vue/entry/server-loader.js‘, } }, alias: { server: ‘app/web/framework/vue/entry/server.js‘, client: ‘app/web/framework/vue/entry/client.js‘, app: ‘app/web/framework/vue/app.js‘, asset: ‘app/web/asset‘, component: ‘app/web/component‘, framework: ‘app/web/framework‘, store: ‘app/web/store‘ } };
我们知道,在本地开发时,大家都会用 Webpack 热更新功能. 而 Webpack 热更新实现是基于内存编译实现的。
在线上运行时,我们可以直接读取构建好的JSBundle文件,那么在本地开发时,在 Egg 服务端渲染时,如何获取到 JSBundle文件 内容时, 同时又不耦合线上代码。
这里我们结合 Egg + Webpack 热更新实现 里面提到插件 egg-webpack ,该插件在 egg app上下文提供了 app.webpack.fileSystem 实例,我们可以根据文件名获取到 Webpack编译的内存文件内容。有了这一步,为我们本地开发从 Webpack 内存里面实时读取文件内容提供了支持。至于不耦合线上代码线上代码的问题我们可以单独编写一下插件,覆盖 egg-view-vue 暴露的 engine renderBundle 方法。具体实现请看如下实现。
if (app.vue) {
const renderBundle = app.vue.renderBundle;
app.vue.renderBundle = (name, context, options) => {
const filePath = path.isAbsolute(name) ? name : path.join(app.config.view.root[0], name);
const promise = app.webpack.fileSystem.readWebpackMemoryFile(filePath, name);
return co(function* () {
const content = yield promise;
if (!content) {
throw new Error(`read webpack memory file[${filePath}] content is empty, please check if the file exists`);
}
return renderBundle.bind(app.vue)(content, context, options);
});
};
}
基于以上实现,我们封装了 egg-webpack-vue 插件,用于 Egg + Webpack + Vue 本地开发模式。
有了上面的 3 个渲染相关的 Egg 插件和 easywepback-vue 构建插件, 该如何搭建一个基于 Egg + Webpack + Vue 的服务端渲染工程项目呢?
项目你可以通过 easywebpack-cli 直接初始化即可完成或者clone egg-vue-webpack-boilerplate。下面说明一下从零如何搭建一个Egg + Webpack + Vue 的服务端渲染工程项目。
egg-init egg-vue-ssr
// choose Simple egg app
npm i easywebpack-vue --save-dev
npm i egg-webpack --save-dev
npm i egg-view-vue --save
npm i egg-view-vue-ssr --save
exports.webpack = {
enable: true,
package: ‘egg-webpack‘
};
exports.webpackvue = {
enable: true,
package: ‘egg-webpack-vue‘
};
2. 在 ${app_root}/config/config.local.js 添加如下配置
const EasyWebpack = require(‘easywebpack-vue‘);
// 用于本地开发时,读取 Webpack 配置,然后构建
exports.webpack = {
webpackConfigList: EasyWebpack.getWebpackConfig()
};
‘use strict‘;
const path = require(‘path‘);
module.exports = {
egg: true,
framework: ‘vue‘,
entry: {
include: [‘app/web/page‘, { ‘app/app‘: ‘app/web/page/app/app.js?loader=false‘ }],
exclude: [‘app/web/page/[a-z]+/component‘, ‘app/web/page/test‘, ‘app/web/page/html‘, ‘app/web/page/app‘],
loader: {
client: ‘app/web/framework/vue/entry/client-loader.js‘,
server: ‘app/web/framework/vue/entry/server-loader.js‘,
}
},
alias: {
server: ‘app/web/framework/vue/entry/server.js‘,
client: ‘app/web/framework/vue/entry/client.js‘,
app: ‘app/web/framework/vue/app.js‘,
asset: ‘app/web/asset‘,
component: ‘app/web/component‘,
framework: ‘app/web/framework‘,
store: ‘app/web/store‘
},
loaders: {
eslint: false,
less: false, // 没有使用, 禁用可以减少npm install安装时间
stylus: false // 没有使用, 禁用可以减少npm install安装时间
},
plugins: {
provide: false,
define: {
args() { // 支持函数, 这里仅做演示测试,isNode无实际作用
return {
isNode: this.ssr
};
}
},
commonsChunk: {
args: {
minChunks: 5
}
},
uglifyJs: {
args: {
compress: {
warnings: false
}
}
}
}
};
node index.js 或 npm start
// 首先安装 easywebpack-cli 命令行工具
npm i easywebpack-cli -g
// Webpack 编译文件到磁盘
easywebpck build dev/test/prod
服务端渲染
在app/web/page 目录下面创建 home 目录, home.vue 文件, Webpack自动根据 .vue 文件创建entry入口, 具体实现请见 webpack.config.js
<template>
<layout title="基于egg-vue-webpack-dev和egg-view-vue插件的工程示例项目" description="vue server side render" keywords="egg, vue, webpack, server side render">
{{message}}
</layout>
</template>
<style>
@import "home.css";
</style>
<script type="text/babel">
export default {
components: {
},
computed: {
},
methods: {
},
mounted() {
}
}
</script>
exports.index = function* (ctx) {
yield ctx.render(‘home/home.js‘, { message: ‘vue server side render!‘ });
};
app.get(‘/home‘, app.controller.home.home.index);
前端渲染
exports.client = function* (ctx) {
yield ctx.renderClient(‘home/home.js‘, { message: ‘vue server side render!‘ });
};
app.get(‘/client‘, app.controller.home.home.client);
更多实践请参考骨架项目:egg-vue-webpack-boilerplate
本地运行模式
本地开发服务端渲染页面访问
页面可以直接使用 /public/client/js/vendor.js 相对路径, /public/client/js/vendor.js 由后端框架代理转发到webpack编译服务, 然后返回内容给后端框架, 这里涉及两个应用通信. 如下:
<link rel="stylesheet" href="/public/client/css/home/android/home.css">
<script type="text/javascript" src="/public/client/js/vendor.js"></script>
<script type="text/javascript" src="/public/client/js/home.js"></script>
页面必须使用 http://127.0.0.1:9001/public/client/js/vendor.js 绝对路径
<link rel="stylesheet" href="http://127.0.0.1:9001/public/client/css/home/android/home.css">
<script type="text/javascript" src="http://127.0.0.1:9001/public/client/js/vendor.js"></script>
<script type="text/javascript" src="http://127.0.0.1:9001/public/client/js/home.js"></script>
其中 http://127.0.0.1:9001 是 Agent里面启动的Webpack编译服务地址, 与Egg应用地址是两回事
发布模式构建流程和运行模式
标签:提高 多个 为我 contex 统计分析 tree 扩展 cto blog
原文地址:http://www.cnblogs.com/hubcarl/p/7623325.html