微前端是2019年很火的一个话题,不少公司都分享了他们的微前端解决方案,我的以为“微前端”这个名字仍是比较贴切的,由于它的目标主要是对标后端的“微服务”,但愿前端的巨石工程也可以拆分红小工程来更好地进行维护。笔者近期也在作微前端的工做,参考了业界的不少方案,有了本身的一些体会,但愿经过这篇文章对微前端的一个核心功能——“JS 模块的动态加载”作一些总结。html
目前正经的微前端方案主要是两种类型:前端
对于技术栈无关型来讲,动态加载子工程主要是将子工程的代码跑起来便可,可能还涉及到挂载一些生命周期钩子,而技术栈统一型的目标要大得多,是须要拿到子工程的组件代码,将其动态嵌入到主工程内完成解析。react
先来看一下字节跳动的实现,从他们文章中的介绍能够看出来他们也应该属于技术栈无关型,他们的方案是: “子模块(Modules)就是一个个的 CMD 包,我用 new Function 来包起来。” 简单的两句话,我猜想这是说用 fetch 或者其余请求库直接拿到做为 cmd 包的子工程模块,而后用 new Function 传入自定义的 define 等参数,将子模块做为 function 的函数体来执行。可是这里本能够跟 requirejs 同样全局定义好 define 等全局变量,而后用 script 标签直接引用子工程天然加载执行,为何要用 fetch + new Function 呢? 多是由于全局的 define 不方便在组件方法内部动态使用吧。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 的拆包和动态加载时的模块加载也是经过 jsonp 实现的,每个拆出来的包,也叫 chunk,都是被包在一个全局 jsonp 方法中的,模块被加载时 jsonp 方法就会被执行,这个 jsonp 方法会去注册这个 chunk 和它所依赖的 chunk,在它所依赖的全部 chunk 都加载好以后时候会去触发该 chunk 的入口模块(entry module)的执行。熟悉这个过程对咱们排查生产环境中拆包产物的部署问题有很大帮助,图解以下:
这个 jsonp 函数就是咱们常见的 chunk 顶端的 push 方法:
咱们能够看到,因为目前前端工程的主要打包方案是 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 这类方法来进行解析。虽然原理并不复杂,可是你们能够看到,为了达到环境隔离或者直接使用输出值等不一样效果,具体的实现细节变化仍是不少的。
参考: