react 用 vite 构建 & 用 qiankun 做微前端

工程不经常改动,就不建仓库了,弄个压缩包完事。
代码压缩包:micro-qiankun.tar.gz

背景

文章不是为了讲怎么用 react & vite & qiankun 结合搭建。搭建也不复杂,直接看示例代码就可以了。主要是这其中遇到的一个问题:

qiankun.js:17 [import-html-entry]: error occurs while executing normal script <script type=”module”>

详情如图:

记录下这个问题的产生及解决。

介绍

qiankun.js 是一个微前端框架。对于微前端,之前也尝试过《用 webpack ModuleFederationPlugin 搭建微前端》。

两者差别的话:

  1. 如果用 vite,那 webpack5 MF 肯定用不了(废话~)。
  2. 对于 qiankun,是不论框架的,基座和各个子应用可以完全不同框架,因为用的沙箱机制,可以可以云云(详看官网介绍)。
  3. 而 webpack MF 其实也不是为了微前端出来,而是共享一部分功能出来,恰好能用作微前端。最终形式是暴露出一份 js 文件给主应用引用。那就最好基座和子应用都是同框架。

个人理解:

  • 如果是成熟的几个不同项目,要揉在一起,但是彼此关联又不是很大,可以用 qiankun。
  • 如果是本身就是同一个大项目,要拆成不同模块分工维护。有很多共享的数据,可以用 webpack ModuleFederationPlugin。

搭建

qiankun 的使用也挺简单的,根据官网指示就好了。用 vite 构建 react,可以参考《react 基础工程 Ⅱ(@reduxjs/toolkit & @craco/craco & vite)》,也不复杂。但是要混合使用,还差一点。

主要是没法直接用,因为 vite 调试的时候,就是用原生 esm 模块引入。网上有大佬先一步产出插件了:vite-plugin-qiankun
根据大佬的指示,很快就搭建起来了,也成功运行起来,就是会报上述那个错误。如果是基座也是 vite 那还好,只会在控制台打印出来。如果基座是 webpack,就会变成错误遮罩一直挡着。

那么:

上述错误说的是在不声明 type="module" 的情况下使用了 esm 模块。我们知道 vite 的入口 js 就是一句:<script type="module" src="/src/index.js"></script>。所以 vite-plugin-qiankun 插件就是把它变成动态 import() 引入。

这个没问题,重点是用 vite 构建 react 官方出了一个支持的插件:@vitejs/plugin-react。这个帮助调试时候快速更新,以及主动注入 react jsx runtime。这也是可以解决一个缺少依赖的错误,这个错误也是老面孔了:Uncaught ReferenceError: React is not defined。以前用 webpack + babel 也会出现。
如果打包工具不主动注入 react jsx runtime,那么就要自己在代码层面,在使用到 jsx 组件地方主动引入:import React from react;

这是其中一个功能,为了支持快速更新,也在页面注入了一段代码:

import RefreshRuntime from 'http://localhost:5173/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true

也就是和报错相关联的那一段代码。这点在官网的 后端集成第二点 也有提到。

然后 qiankun 在引入子应用时,是使用类似 eval 的方法触发子应用 js。所以对于这段直接使用 import 的代码就报错了。

解决

既然知道了产生原因,就可以对症下药:

  1. 眼不见为净。不理它。上面说了,如果是基座是 vite 那就只会出现在控制台打印,不影响运行。而且这段代码是为了辅助调试使用的,打成生产包就没了。子应用都用 vite 难道基座不用?(狗头…)
  2. 不要用 @vitejs/plugin-react。有点因噎废食。且不说每份用到 jsx 的文件都要主动引入 react 很麻烦,万一后面更新了啥功能岂不是用不到?不如第一条不理这个错误。
  3. 自己写个 vite 插件处理一下:
// 插件写法
const htmlRemoveFreshPlugin = () => {
  return {
    name: 'html-transform',
    transformIndexHtml(html) {
      const $ = require('cheerio').load(html);
      $('script').eq(0).remove();
      // $('head').prepend(
      // `<script>
      // import((window.proxy ? (window.proxy.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ + '..') : '') + '/@react-refresh').then(({ default: RefreshRuntime }) => {    
      //   RefreshRuntime.injectIntoGlobalHook(window)
      //   window.$RefreshReg$ = () => {}
      //   window.$RefreshSig$ = () => (type) => type
      //   window.__vite_plugin_react_preamble_installed__ = true
      // })
      // </script>`);
      html = $.html();
      return html;
    },
  };
};
// 引入部分
{
  plugins: [
    react(),
    vitePluginQiankun(appName, { useDevMode: isDev }),
    ...(isDev ? 
      [htmlRemoveFreshPlugin()]
      : []),
  ],
}

解释一下:

  • vite 是基于 rollup 开发的,插件写法也和 rollup 插件一致。transformIndexHtml 是 vite 独有的钩子,用于处理返回的 html。
  • cheerio 是 vite-plugin-qiankun 带的依赖。可以像 jQuery (jQuery 就不介绍了)一样操作 dom,也可以用来做爬虫相关使用。
  • 做法就是把那段 esm 代码移除。如果还想要有注入的效果,可以把注释那段打开,变成动态 import() 引用。

我的建议做法是:
不理它。只是作用于调试,不阻塞的情况下没必要专门弄个插件去处理。主要是我把这段代码去掉后(包括基座对于这段的代码引入),发现还是可以热更新。是因为这段代码不是支持热更新的还是说别的地方起作用了?这个就不细究了。

另外:
开发子应用的时候。其实只看子应用的页面就可以了。如果硬要连同基座一起启动,在基座页面调试查看子应用,则使用 vite-plugin-qiankun 的时候一定要传入 { useDevMode: true }。子应用引用入口 js 的时候,能带上完整的路径:import((window.proxy ? (window.proxy.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ + '..') : '') + '/src/index.js')。不然子应用在基座页面调试,会变成引入基座的入口 js。