# 微前端演进(二)

2020年11月,进阶改造

  • 微前端对我们当前的业务的实际价值是什么?
  • 微前端从构建到运行,过程中还有哪些可以优化的点?

上文微前端演进(一)中,讲解了微前端第一阶段落地的演进,验证了微前端的可行性。微前端也在产品的开发中稳定运行。不过在业务的实践过程中,我们也发现了许多可以优化的点。

# 对微前端的思考

微前端对我们实际产生的价值是什么?

为什么会有这样的疑问,因为微前端的实际应用场景更适合中后台系统。中后台系统的子系统特别多且系统独立,能够最大限度发挥微前端应用隔离和独立发布的优势,但是对于有格来说,我们发现有格是一个业务形态统一的产品,应用之间并不是完全独立,有格的需求是模块之间高内聚低耦合。

有格的子应用更接近于子模块,模块之间保持一定的边界,但又开放一定程度的跨模块沟通的能力;代码在业务上保证独立,但是在底层上保证统一且复用。于是我们开始对qiankun框架进行更深入的研究。经过研究讨论之后,我们明确了对微前端的一些优化方向。

首先,HTML Entry会首先解析html内容,再拉取资源,在请求上会浪费一次请求的时间。这样我们对有格子应用的定位,从“应用”转化为了“资源”,类似于webpack的异步chunk。所以,优化方法就是提前获取资源路径,在浏览器运行时减少一次请求的时间,从而提升子应用的加载速度

html-entry优化.png

其次,是对微前端子应用的代码复用方案,一些公共库可以最大程度的复用。

# 子应用信息的采集

上文提到的减少html请求时间, 直接加载资源,就是通过qiankun的api,entry字段支持以{ scripts?: string[]; styles?: string[]; html?: string }的格式配置。

于是子应用的配置就变成了类似这样的形式:

{
  name: 'foo',
  // 旧的格式
  // entry: '//localhost:8080',
  entry: {
    scripts: [
      '//localhost:8080/foo/vendor.js',
      '//localhost:8080/foo/app.js',
    ],
    styles: [
      '//localhost:8080/foo/vendor.css',
      '//localhost:8080/foo/app.css',
    ],
  },
  container: '#yourContainer',
  activeRule: '/foo',
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 通过webpack-manifest-plugin采集子应用构建信息

那么如何获取上文提到的scritps 和 styles信息呢?

最合适的解决办法是在构建时获取,使用webpack-manifest-plugin在构建完成后生成一个manifest.json文件。

子应用经过webpack-manifest-plugin构建之后生成的subapp_manifest.json文件格式如下:

{
  "app.css": "/tableview/v966577ec-2103310646-1384/css/app.5ba0472c.css",
  "app.js": "/tableview/v966577ec-2103310646-1384/js/app.b6ffbbe2.js",
  "vendors.css": "/tableview/v966577ec-2103310646-1384/css/vendors.e62a84ef.css",
  "vendors.js": "/tableview/v966577ec-2103310646-1384/js/vendors.1e9fedd2.js",
  "index.html": "/tableview/index.html"
}
1
2
3
4
5
6
7

在构建时,将生成的dist目录下的资源和subapp_manifest.json文件,上传到OSS资源服务器中,以版本号作为标识。

请看jenkins的构建执行脚本:

jenkins-build.png

再结合s2i中的镜像构建脚本一起看:

TODO

此处可优化,将oss上传的脚本命令从镜像中抽离出来。理论上,子应用构建可以只生产资源,不生产镜像。

s2ishell.png

# nodejs中间层

nodejs中间层,是指在网关和浏览器资源之间添加的一层服务端,就是我们用来实现服务端渲染的中间层。

我们可以在这一层来实现上述的优化。

参考这篇文章了解有格的服务端渲染。在nodejs启动时,会根据子应用列表,去对应的子应用服务中采集subapp_manifest.json文件信息。

获取到manifest之后,再通过服务端渲染挂载到window上。可以通过window.__H3_SUBAPPS_MANIFEST__来获取。

manifestjson.png

# 聚合服务

微前端聚合服务系统(开发环境)

微前端将产品划分为了多个子应用,如果每个子应用在发版时都独立发版的话,就一定需要考虑服务之间的依赖问题和版本兼容问题,这样会导致前端的发版会非常复杂。

聚合发版系统就是解决这样的问题,保证前端可以一次性发版,而不需要考虑发版顺序;保证子应用的独立和解耦,又兼顾整个产品的完整性。

聚合发版流程.drawio

# 公共依赖的抽离

得力于我们对HTMLEntry的改造,我们对子应用的entry配置将更灵活。我们甚至可以在代码中自定义加载的脚本。我们可以通过这个能力来对沙箱做修改。

JS 沙箱的相关知识了解:解密qiankun沙箱 - CSDN

首先是对第三方的依赖的抽离,将第三方依赖包打包好放到资源服务器中。

其次是在子应用的初始化脚本中,引入'<script>window[Symbol.for("_SANDBOX_INJECT_")](window);</script>', 这样一段脚本可以修改沙箱。





 
 










{
  name: 'foo',
  entry: {
    scripts: [
      '<script>window[Symbol.for("_SANDBOX_INJECT_")](window);</script>', // 沙箱初始化脚本
      '//assets.devoa.h3yun.net/common-library/0.1.0/vue-bundle/v1.0.0/vue-bundle.js', // 第三方静态资源
      '//localhost:8080/foo/app.js',
    ],
    styles: [
      '//localhost:8080/foo/app.css',
    ],
  },
  container: '#yourContainer',
  activeRule: '/foo',
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

提前在主应用中,声明好沙箱的初始化函数。可以将一些库,从主框架获取到,注入到沙箱中。之后,只需要在子应用中配置webpack的external属性即可。如下图:

export default () => {
  const _SANDBOX_INJECT_ = Symbol.for('_SANDBOX_INJECT_');
  window[_SANDBOX_INJECT_] = function (global: any) {
    try {
      // 从主框架中复用的第三方库
      global._H3Icons_ = H3Icons;
      global._H3IconsBiz_ = H3IconsBiz;
      global._H3YunOpensdk_ = H3YunOpensdk;
      global._H3YunRequest_ = H3YunRequest;
      global._H3YunGlobalProvider_ = H3YunGlobalProvider;
      global._UmiRequest_ = UmiRequest;
      global._Vuedraggable_ = Vuedraggable;
      // global.Vue = Vue;
      // global.Vuex = Vuex;
      // global.VueRouter = VueRouter;
      // global.VuePropertyDecorator = VuePropertyDecorator;
      // global.VueCompositionAPI = VueCompositionAPI;
      // const extendedVue = Vue.extend();
      // extendedVue.version = Vue.version;
      // extendedVue.config = Vue.config;
      // (extendedVue as any).util = (Vue as any).util;
      // extendedVue.observable = Vue.observable;
      // extendedVue.set = Vue.set;
      // (extendedVue as any).__SUB_VUE__ = true;
      // global.Vue = extendedVue;
      // global.VuePropertyDecorator = Object.assign({}, VuePropertyDecorator, {
      //   Vue: extendedVue,
      // });
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error(err);
    }
  };
};

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
30
31
32
33
34
35

在子应用的webpack配置中, 使用externals将依赖排除:

{
  externals: {
    '@h3-icons/basic': '_H3Icons_',
    '@h3-icons/basic-biz': '_H3IconsBiz_',
    '@h3yun/opensdk': '_H3YunOpensdk_',
    '@h3yun/request': '_H3YunRequest_',
    '@h3yun/global-provider': '_H3YunGlobalProvider_',
    'umi-request': '_UmiRequest_',
    lodash: '_',
    moment: 'moment',
    '@h3/antd-vue': 'antd',
    '@ant-design/icons/lib/dist': 'AntDesignIcons',
    vue: 'Vue',
    'vue-router': 'VueRouter',
    vuex: 'Vuex',
    'vue-class-component': 'VueClassComponent',
    'vue-property-decorator': 'VuePropertyDecorator',
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19