likes
comments
collection
share

如何让 vite 完美接入 qiankun

作者站长头像
站长
· 阅读数 15

前言

因为公司有使用微前端技术,技术栈为 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
评论
请登录