对比微前端方案看 JS 模块的动态加载

微前端是2019年很火的一个话题,不少公司都分享了他们的微前端解决方案,我的以为“微前端”这个名字仍是比较贴切的,由于它的目标主要是对标后端的“微服务”,但愿前端的巨石工程也可以拆分红小工程来更好地进行维护。笔者近期也在作微前端的工做,参考了业界的不少方案,有了本身的一些体会,但愿经过这篇文章对微前端的一个核心功能——“JS 模块的动态加载”作一些总结。html

微前端方案分类

目前正经的微前端方案主要是两种类型:前端

  • 一种是以蚂蚁金服 qiankun 为表明的工程之间技术栈无关型。
  • 另外一种是以美团外卖为表明的工程之间技术栈统一型。

对于技术栈无关型来讲,动态加载子工程主要是将子工程的代码跑起来便可,可能还涉及到挂载一些生命周期钩子,而技术栈统一型的目标要大得多,是须要拿到子工程的组件代码,将其动态嵌入到主工程内完成解析。react

下面咱们来具体看一下几个方案的实现:

字节跳动

先来看一下字节跳动的实现,从他们文章中的介绍能够看出来他们也应该属于技术栈无关型,他们的方案是: “子模块(Modules)就是一个个的 CMD 包,我用 new Function 来包起来。” 简单的两句话,我猜想这是说用 fetch 或者其余请求库直接拿到做为 cmd 包的子工程模块,而后用 new Function 传入自定义的 define 等参数,将子模块做为 function 的函数体来执行。可是这里本能够跟 requirejs 同样全局定义好 define 等全局变量,而后用 script 标签直接引用子工程天然加载执行,为何要用 fetch + new Function 呢? 多是由于全局的 define 不方便在组件方法内部动态使用吧。jquery

蚂蚁金服

qiankun 动态加载
再来看一下典型的技术栈无关型方案蚂蚁金服 qiankun 的实现方式,相关的代码都在它使用的 import-html-entry 仓库中,一样是经过 fetch 等请求库,但拿到的是做为 umd 包的子工程模块内容,而后 eval 执行。eval 执行时更改了绑定的 window 对象,这样作主要是为了避免把子工程导出值都绑定到 window 上,而是绑定到自定义的 window.proxy 上,作自定义的隔离处理。这里还用到了相似 Systemjs global loading 的自动寻找新增全局变量的方式来找到子模块暴露的全局入口,这也是这个方案的一大特色,即不须要对子应用的代码作特殊改造或者约定,只要webpack 配置成 umd 输出便可,正如 systemjs global 原本就是给加载 jquery 等传统库准备的。这比较适用于子应用还须要独立部署的情形。

qiankun 推荐的子应用 webpack 配置:webpack

const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};
复制代码

美团外卖

接下来咱们看一下技术栈统一型的美团外卖方案中模块的动态加载方法,他们的方案介绍中对于模块的加载方式没有细讲,可是贴出的代码里能够看到 loadAsyncSubapp 和 subappRoutes,也提到了触发 jsonp 钩子window.wmadSubapp,种种迹象显示他们的方案是经过 jsonp 实现的。能够经过 webpack 设置 libraryTarget 为 jsonp,这样配置的的打包产物在加载时会执行全局的 jsonp 方法,传入主模块的 export 值做为参数。参考 webpack 文档 other-targets。亲测可行:git

子工程配置 webpack configgithub

output: {
  library: `registerSubApp`,
  libraryTarget: 'jsonp',
}
复制代码

主模块web

export default App
复制代码

父工程json

window.registerSubApp = function (lib) {
  ReactDOM.render(
    React.createElement(lib.default),
    document.getElementById('root')
  )
}
// lib = {default: App}
复制代码

这样的配置使他们能够直接拿到子工程的组件,进而能够将组件动态整合到主工程中。能够参考他们文章中介绍的结合 react-router 作的动态路由解析。segmentfault

Webpack 拆包和动态加载

咱们知道 webpack 的拆包和动态加载时的模块加载也是经过 jsonp 实现的,每个拆出来的包,也叫 chunk,都是被包在一个全局 jsonp 方法中的,模块被加载时 jsonp 方法就会被执行,这个 jsonp 方法会去注册这个 chunk 和它所依赖的 chunk,在它所依赖的全部 chunk 都加载好以后时候会去触发该 chunk 的入口模块(entry module)的执行。熟悉这个过程对咱们排查生产环境中拆包产物的部署问题有很大帮助,图解以下:

webpack chunk 加载过程

这个 jsonp 函数就是咱们常见的 chunk 顶端的 push 方法:

webpack jsonp
webpack jsonp

Webpack module federation

咱们能够看到,因为目前前端工程的主要打包方案是 webpack,微前端的不少动态加载方案都须要借助 webpack 的能力,后来天然就有人想到让 webpack 更好更方便地支持不一样工程之间构建产物的互相加载,这就是 webpack module federation,使用方式多是:

new ModuleFederationPlugin({
    name: 'app_two',
    library: { type: 'global', name: 'app_a' },
    remotes: {
      app_one: 'app_one',
      app_three: 'app_three'
    },
    exposes: {
       AppContainer: './src/App'
    },
    shared: ['react', 'react-dom', 'relay-runtime']
})
----
import('app_one/AppContainer')
复制代码

目前这项工做还在进行当中,能够在这里看到。

小结

总的来讲,JS 模块动态加载的原理主要有两种,jsonp 方式是用动态 script 标签直接加载并解析的,而 fetch 请求方式是拿到模块内容,以后须要用 eval 或者 new Function 这类方法来进行解析。虽然原理并不复杂,可是你们能够看到,为了达到环境隔离或者直接使用输出值等不一样效果,具体的实现细节变化仍是不少的。

参考:

相关文章
相关标签/搜索