该文章阅读需要7分钟,更多文章请点击本人博客halu886
目前所在团队主要负责秀场项目,中间层是基于Nodejs并且集成了Egg企业级框架,主要处理路由分发以及一级缓存,同时还负责了首页的渲染以及前端代码的脚手架。
但是由于遗留代码过于老旧,且集成了比较多和杂的框架,不论是编译以及打包和维护都需要耗费大量的精力和时间。
基于以上痛点,我们决定尝试研习出一套更加客制化以及功能完善的项目架构.
对于To C端的产品还是非常依赖SEO(Search Engine Optimization)带来的流量,并且基于服务端渲染的页面的TTC(time-to-content)也是非常有吸引力的。
技术选型则是企业级框架Egg,对于前端MVVM框架则选择国内比较热门VUE,配上Webpack打包工具。
最基本的需求主要有以下几点
- Vue组件服务端渲染
- 模版缓存
- 依赖解析
- 多入口打包
- 支持同构
项目初始化 Pc-4.0-demo
我们将业务代码以Egg标准骨架进行承载。然后将我们的VUE渲染引擎和Webpack打包工具分别封装成Egg-Plugin进行解耦。
标准的目录结构如下

具体的目录分类可以参考Egg doc
Vue模版引擎 egg-view-vue-tuji
首先实现view-plugin下标准对外开放接口lib/view.js的render和renderString。
这里主要是集成Vue官方推荐的vue-server-renderer的渲染工具。
通过createBundleRenderer实例化BunlderRenderer。
将接收到第一次请求时,获取egg-webpack-tuji编译打包生成的JSON格式bundler传入BunlderRenderer中进行模版加载。
最后在标准开放接口中获取cxt挂载的相关参数,将参数注入加载好的模版,然后在回调中获得渲染后好的HTML字符串。
将回调转换成promise后render接口功能就算实现了。
render(filename, locals) {
try {
filename = path.relative(this.options.root[0], filename);
const renderer = getRender(filename, this);
return new Promise((resolve, reject) => {
renderer.renderToString(locals, (err, html) => {
if (err) {
reject(err);
}
resolve(html);
});
});
} catch (error) {
this.ctx.logger.error(error);
}
}
Webpack打包插件 egg-webpack-tuji
webpack的默认配置我们维护在插件下标准的config/config.defualt.js目录下,业务上需要客制化的配置则可以挂载在config.webpack进行webpack-merge进行merge。
对于服务端Vue组件渲染,我们需要默认集成Vue-loader,babel-loader,以及less-loader等官方推荐的loader工具链。
由于不支持多入口打包,在这里我们放弃了Vue SSR官网上推荐的vue-server-renderer/server-plugin,
This is the plugin that turns the entire output of the server build into a single JSON file. The default file name will be
vue-ssr-server-bundle.json
而选择通过更加灵活的手动加载多入口下打包生成的Bundler Object。
我们在该插件下的app/lib/tujiWebpack.js封装了大部分Webpack相关操作。
初始化时将相关参数挂载在该实例上,通过build()进行构造编译器compile。
通过哨兵变量isBuilde监听构建状态。
通过getBundle接口将生成的Bundler暴露给Egg-view-vue-tuji。
module.exports = class TujiWebpack {
constructor(app) {
this.app = app;
this.options = app.config.webpack;
this.isBuild = false;
this.build();
}
build() {
this.compile = webpack(this.options, err => {
if (err) {
this.app.logger.error(err);
return;
}
this.isBuild = true;
});
this.compile.outputFileSystem = fs;
}
getBulder(filePath) {
if (!this.isBuild) {
this.logger.warm('waiting...build ing~');
return {};
}
const appRoot = this.app.baseDir;
const contentString = this.compile.outputFileSystem.readFileSync(path.join(appRoot, 'dist', filePath), 'utf-8');
return contentString;
}
};
模版缓存
当每次请求访问时,每次都要重复生成一个新的模版这是非常浪费CPU和影响性能。
所以我们尝试利用NodeJS的文件依赖的缓存机制将模版缓存在内存中,以便于重复利用。
同时集成LRU双向链表的缓存策略进行优化。
const getRender = (() => {
const renderers = new LRU(20);
let template;
return (filename, viewInit) => {
if (!template) {
viewInit.ctx.logger.info('template start init');
template = require('fs').readFileSync(viewInit.options.template, 'utf-8');
}
if (!renderers.get(filename)) {
renderers.set(filename, createBundleRenderer(viewInit.ctx.app.webpack.getBulder(filename), {
template,
}));
}
return renderers.get(filename);
};
})();
考虑到NodeJS在单节点下的1.7G的内存限制,将模版节点限制20个。
小结
以上便是基础版的服务端渲染的Demo,但是应用到生产以及推广到项目中还需要进行非常多的加工,后续进展也会陆陆续续整理出来,欢迎持续关注~