如何让 vite 完美接入 qiankun
前言
因为公司有使用微前端技术,技术栈为 qiankun,为了保证对 qiankun 的支持,使用了开发模式走 vite,构建走 webpack:如何在 webpack 项目中使用 vite 加速。
但是 vite 的开发体验实在是太爽了,于是寻找了一波 qiankun 的 vite 插件,都没有对生产环境 js,css 沙箱的集成,于是只能自己动手写了一个:vite-plugin-legacy-qiankun
实现过程
首先我们得清楚 vite 为何不兼容 qiankun
Qiankun 执行子应用 script
如下是 qiankun 执行子应用代码的地方,可以看到我们子应用的 script 是用 eval 执行的。
// import-html-entry
export function evalCode(scriptSrc, code) {
const key = scriptSrc;
if (!evalCache[key]) {
const functionWrappedCode = `window.__TEMP_EVAL_FUNC__ = function(){${code}}`;
(0, eval)(functionWrappedCode);
evalCache[key] = window.__TEMP_EVAL_FUNC__;
delete window.__TEMP_EVAL_FUNC__;
}
const evalFunc = evalCache[key];
evalFunc.call(window);
} | }
Vite
vite 中的入口文件是以 ES Module 的形式引入的,而 qiankun 加载子应用时会获取 main.js 的代码。
<script type="module" src="/src/main.js"></script>
我们 main.js 代码会被获取且包装到一个函数里面,然后被 eval 执行。
function () {
// main.js 中的代码
import xx from 'xx'
import xx1 from 'xx1'
// 省略...
}
当我们用 eval 来执行包含 main.js 的代码,当然会报错喽!
Uncaught SyntaxError: Cannot use import statement outside a module
解决 ES Module 脚本在 qiankun 中执行
硬的不行就来软的,我们略微改变一下 vite 入口文件的使用方式。
from
<script type="module" src="/src/main.js"></script>
to
<script>
import('/src/main.js')
</script>
这样 eval 执行脚本的时候就不会报错了
Qiankun 沙箱的实现
如上所示,以 import() 引入的脚本,执行是不会有问题的,但是沙箱这个功能就没了
// import-html-entry
function getExecutableScript(scriptSrc, scriptText, opts = {}) {
// 省略...
return strictGlobal
? (
scopedGlobalVariableFnParameters
? `;(function(){with(window.proxy){(function(${scopedGlobalVariableFnParameters}){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(${scopedGlobalVariableFnParameters})}})();`
: `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
)
: `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}
如上代码,大家只用关注 scriptText,这是我们子应用的脚本,可以看到 qiankun 使用 with 对代码做了劫持,所以子应用中给 window 挂载属性才可以被监听到。但是我们 import('xx') 这段代码里面的内容是没有被 qiankun 劫持的,所以我们 js 没有被劫持,自然 js 中动态加载 css 也不会被劫持。
如果 css 没被劫持,就会泄漏,从而影响其他子应用样式,js 没被劫持,也会影响到其他子应用。
@vitejs/plugin-legacy
😎😎😎,我们使用 vite 提供的这个包来打包应用。
// dist/index.html
<script type="module" crossorigin src="/assets/index-e0b01283.js"></script>
<script type="module">try{import.meta.url;import("_").catch(()=>1);}catch(e){}window.__vite_is_modern_browser=true;</script>
<script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy build because dynamic import or import.meta.url is unsupported, syntax error above should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
<script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
<script nomodule crossorigin id="vite-legacy-polyfill" src="/assets/polyfills-legacy-45a716f9.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="/assets/index-legacy-92d5c701.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
大家可以看到有 module,还有 nomodule 的 script,就是如果浏览器支持 es module,就会走前三个脚本,如果浏览器不支持 es module,就会走 polyfill 的代码。
SystemJS
我们打开 vite-legacy-polyfill 这个文件,发现 vite polyfill 之后的代码是被 SystemJS 加载的,咱们 import('xx') 会被转换成 System.import('xx')。
System.import 简而言之就是使用 document.createElement('script') 创建脚本,然后 append 到 head 中,加载完再删除。
因为 polyfill 代码被 qiankun 劫持了,所以其中 System.import 中使用的 document 也会被劫持,这样一来我们 js,css 沙箱就不会失效了。
systemJSPrototype.createScript = function (url) {
var script = document.createElement('script');
script.async = true;
// Only add cross origin for actual cross origin
// this is because Safari triggers for all
// - https://bugs.webkit.org/show_bug.cgi?id=171566
if (url.indexOf(baseOrigin + '/'))
script.crossOrigin = 'anonymous';
var integrity = importMap.integrity[url];
if (integrity)
script.integrity = integrity;
script.src = url;
return script;
};
systemJSPrototype.instantiate = function (url, firstParentUrl) {
var autoImportRegistration = autoImportCandidates[url];
if (autoImportRegistration) {
delete autoImportCandidates[url];
return autoImportRegistration;
}
var loader = this;
return Promise.resolve(systemJSPrototype.createScript(url)).then(function (script) {
return new Promise(function (resolve, reject) {
script.addEventListener('error', function () {
reject(Error(errMsg(3, process.env.SYSTEM_PRODUCTION ? [url, firstParentUrl].join(', ') : 'Error loading ' + url + (firstParentUrl ? ' from ' + firstParentUrl : ''))));
});
script.addEventListener('load', function () {
document.head.removeChild(script);
// Note that if an error occurs that isn't caught by this if statement,
// that getRegister will return null and a "did not instantiate" error will be thrown.
if (lastWindowErrorUrl === url) {
reject(lastWindowError);
}
else {
var register = loader.getRegister(url);
// Clear any auto import registration for dynamic import scripts during load
if (register && register[0] === lastAutoImportDeps)
clearTimeout(lastAutoImportTimeout);
resolve(register);
}
});
document.head.appendChild(script);
});
});
};
vite-plugin-legacy-qiankun
我主要做了这么几件事情。
- 保留 vite 开发优势
- 生产环境 js 沙箱
- 生产环境 css 沙箱
- 开发环境 css 沙箱
- 开发环境 js 沙箱(开发中。。。)
最后
实现比较简单,而且已经在公司的平台中使用一段时间了,欢迎大家的使用和交流!😊😊😊
转载自:https://juejin.cn/post/7189117358697349178