工程化下的SSR初探-1

268 阅读4分钟

该文章阅读需要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接口功能就算实现了。

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,但是应用到生产以及推广到项目中还需要进行非常多的加工,后续进展也会陆陆续续整理出来,欢迎持续关注~