工程化下的SSR初探-1

该文章阅读需要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进行解耦。

标准的目录结构如下

1

具体的目录分类可以参考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接口功能就算实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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双向链表的缓存策略进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,但是应用到生产以及推广到项目中还需要进行非常多的加工,后续进展也会陆陆续续整理出来,欢迎持续关注~

谢谢老板,老板必发大财!💰💰💰💰💰💰