qiankun
是一个很流行的微前端解决方案。之前我也详细的分析过qiankun的原理,感兴趣的可以看看。
Vite是当下比较流行的构建工具,它对标的是webpack,并作为Vue3脚手架的默认工具替代了老版vue-cli中的webpack。当然,Vite不仅仅能使用在Vue中,React+Vite也是很好用的。它的特点就是开发模式下运行特别“快”,这个得益于浏览器对ESM的原生支持。
既然两个都是流行的解决方案,那么是否可以把它们结合在一起使用呢?比如React+Vite+qiankun
架构的项目,把它作为一个微应用接入到主应用中,同时微应用还保留ESM的特性?很可惜,直接接入是不行的。
存在哪些问题?
Vite构建的应用,开发环境下使用ESM的方式加载脚本。而到目前为止,qiankun
还没有增加对ESM的官方支持。如果我们在加载基于Vite的微应用时,会出现一系列问题:
-
直接报错:
Cannot use import statement outside a module
原因:ESM的脚本内部直接使用
import xxx from 'xxx'
语法,Vite开发模式下并不会对其转码。而这样的语法是无法在qiankun
里直接fetch
然后eval
的,它只能在ESM中使用。即通过<sciprt type="module>"
声明的入口脚本。 -
ESM无法导出qiankun生命周期函数
原因:
qiankun
需要微应用打包为umd格式。这种情况下,qiankun
对微应用的脚本进行eval
操作后,可以在js沙箱*(window.proxy[appName])*下,获取到导出的所有生命周期函数。而对于ESM格式的脚本,qiankun
无法直接获取到生命周期。 -
应用内相对路径的静态资源出现404
原因:微应用独立运行时,相对路径从自己的根开始。但当微应用被加载到主应用中时,相对路径从主应用的根开始。此时静态资源会出现404。webpack支持运行时publicPath技术,即
__webpack_public_path__
。所以需要进行如下设置:// public-path.js if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
这代表如果微应用被加载到主应用中,则publicPath会设置为
qiankun
注入的变量,这个变量就是微应用的地址。这样所有的静态资源都会从微应用的地址获取,不会404。而独立运行时也不会404。但是Vite是不支持运行时publicPath技术的。我们得去手动设置静态的publicPath。
-
不支持多环境部署
原因同上,由于Vite不支持在运行时,动态修改publicPath。所以只能手动设置静态的publicPath。但是这样的话多环境就得配置环境变量
.env
,分开构建并部署。不能实现一次构建,多环境运行。
对于这些问题,qiankun
没有解决,我们只能自己写个插件,看看在插件的帮助下,能否让Vite微应用,既能保留ESM特性,又能接入微前端的能力。
实现解决方案
针对于上面的这几个问题,最好的肯定是qiankun官方去支持,对ESM和普通script分开去处理。但是它没有做,那只能我们去做了。我们也不可能大量的改项目源码去支持。那最好的就是开发一个Vite插件,让Vite插件去modify我们的代码。
初始化一个插件项目,然后一步一步的解决问题。
import导入ESM并运行
首要的问题就是ESM的脚本无法直接被fetch
然后eval
。但是通过测试发现,动态的import()
语法,可以做到不报错正常执行。即如下代码:
// <==静态import语句会报错:`Cannot use import statement outside a module`
const scriptText = `
import "./main.js";
`;
evalSandox(scriptText);
// ==>动态import()语句不报错,正常执行
const scriptText = `
import("./main.js");
`;
evalSandox(scriptText);
那么,我们的插件就需要对index.html
下,所有<script type="module" src="xxx">
的ESM入口脚本进行转换。转换为内联脚本:
<script>
import(window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ + "xxx")
</script>
由于我们改为手动控制ESM脚本的导入,所以需要自己追加base路径。否则它会从主应用下找脚本,导致404.
ESM导出应用生命周期
qiankun需要应用导出生命周期函数。我们需要插件做到:把生命周期函数绑定到window.proxy[appName]
下。
在上一步中,我们使用动态import语法导入了模块,正好我们可以从模块中获取到生命周期。这样模块中仍然保持qiankun官方的设置方法:使用export
导出生命周期。
// 插件中
import("./main.js").then((mod) => {
mod.bootstrap
mod.mount
mod.unmount
})
// main.js模块中
// ...
export function bootstrap() {
/* */ }
export function mount(props) {
/* */ }
export function unmount() {
/* */ }
在模块导入后再设置到window上肯定不行的,因为动态import语法是异步的。而qiankun
需要在所有入口脚本执行完毕后,同步获取生命周期函数。类似于下面:
// 执行所有脚本
execScripts(entry, global);
// 获取生命周期
const scriptExports = global[app.name];
const {
bootstrap, mount, unmount } = scriptExports;
所以如果在导入后再设置到window上,直接就undefined报错了。那怎么办?看到qiankun
的文档中,生命周期函数可以是异步的。这就有操作空间了。我们可以先返回一个虚拟的生命周期对象,生命周期函数内,如果模块已加载那调用真实的生命周期,否则返回Promise,这个Promise会在模块加载后被resolve。简化代码如下:
// 插件内
let realModule = null