likes
comments
collection
share

深入解析:如何将 Vite + React 子应用无缝接入 Qiankun 基座本文详细介绍了如何将基于 Vite 和

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

摘要

本文详细介绍了如何将基于 Vite 和 React 的子应用接入 Qiankun 微前端框架。首先阐述了微前端的三种主流接入方式,并深入分析了 Qiankun 的原理,特别是其如何通过 JavaScript 动态加载实现微前端架构。

接着,文章提供了具体的接入步骤,包括依赖安装、配置文件修改、路由设置及部署注意事项。

最后,针对接入过程中可能遇到的问题,提供了多种解决方案。读者通过本文将掌握 Qiankun 的基本使用方法以及如何在不同环境中灵活配置子应用。

原理浅析

微前端的接入主要有三种主流的方式:

  1. Iframe 方式

    • 原理:通过在主应用中嵌入 iframe 来加载微前端应用。每个微前端应用在自己的沙箱中运行,互不干扰。
    • 优点:隔离性强,避免了样式和脚本的冲突。
    • 缺点:通信较为复杂,性能开销较大,SEO 不友好。
  2. JavaScript 动态加载

    • 原理:主应用通过动态加载 JavaScript 文件来引入微前端应用,通常使用模块化的方式(如 ES6 模块或 CommonJS)。
    • 优点:集成较为简单,支持代码分割和懒加载。
    • 缺点:微前端应用之间可能会存在全局变量冲突,需做好命名空间管理。
  3. Web Components

    • 原理:使用 Web Components 技术(如 Custom Elements 和 Shadow DOM)来封装微前端应用,主应用通过自定义标签引入。
    • 优点:组件化程度高,样式和脚本隔离性好。
    • 缺点:浏览器兼容性问题,学习曲线较陡。

qiankun 的原理浅析

而下面我们使用的 qiankun 就是采用的第二种方式,将 DOM 挂载到基座的某个 dom 上。然后样式隔离使用 Shadow DOM 来实现,JS 的隔离使用沙箱机制。

JS 沙箱隔离

// 沙箱实现
function createSandbox() {
  const sandbox = {};

  // 使用 Proxy 拦截全局对象的访问
  const proxy = new Proxy(window, {
    get(target, prop) {
      // 返回 sandbox 中的值,或者返回 undefined
      return prop in sandbox ? sandbox[prop] : undefined;
    },
    set(target, prop, value) {
      // 只允许设置 sandbox 中的值
      sandbox[prop] = value;
      return true;
    },
    // 其他拦截器可以根据需要实现
  });

  return { sandbox, proxy };
}

// 使用沙箱
const { sandbox, proxy } = createSandbox();

// 在沙箱中设置变量
proxy.myVar = 'Hello, Sandbox!';
console.log(proxy.myVar); // 输出: Hello, Sandbox!

// 尝试访问全局变量
console.log(window.myVar); // 输出: undefined

react+vite 接入微前端 qiankun 基座

基座是使用的 qiankun, 子应用是 vite + react,需要接入基座。首先需要在基座注册一个新的子应用,参考:qiankun.umijs.org/zh/guide/ge…

接入过程中,开发模式和生产模式要分开考虑。如果子应用和基座是不用的域名,要考虑到前端静态资源+接口支持跨域。

安装依赖

pnpm install vite-plugin-qiankun -D

配置 vite.config.ts

import qiankun from 'vite-plugin-qiankun';
export default ({ mode }) => {
    // 放入环境变量控制
    const env = loadEnv(mode, process.cwd(), '')
    // 使用微前端开发模式
    const useMicroAppDevMode = env.VITE_OPEN_MICRO_APP_MODE === 'true'
    const config = {
        plugins: [
          react(),
          qiankun('[子应用code]', { // 这里一定要和基座注册时的 code 一致!
              useDevMode: useMicroAppDevMode
          })
          // ... 其他
        ],
        server: {
            // vite-plugin-qiankun 开发模式和 vite 热更新冲突
            hmr: !useMicroAppDevMode,
            // 前端资资源支持跨域
            cors: useMicroAppDevMode ? {
                "origin": "http://[基座域名].com",
                "credentials": true,
            }: {},
            proxy: {
                '/api': {
                    target: "https://[子应用域名].com",
                    changeOrigin: true,
                    // 后端接口支持跨域
                    configure: useMicroAppDevMode ? (proxy) => {
                        proxy.on('proxyRes', (proxyRes, req) => {
                        proxyRes.headers['Access-Control-Allow-Origin'] = 'http://[基座域名].com"';
                          proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
                        });
                    } : () => {}
                }
            }
        }
    }
    return defineConfig(config)
})
  • 因为放弃了热更新。修改代码后需要手动刷新页面。

  • qiankun 的 props 信息需要在 mouted 生命周期后才能取到,所以获取基座的 props 信息一定是在 mouted 后获取。

修改 main.ts

import ReactDOM, { Root } from "react-dom/client";
import {
  renderWithQiankun,
  qiankunWindow,
} from "vite-plugin-qiankun/dist/helper";

let root: Root;
function render(props: any) {
  root = ReactDOM.createRoot(
    props?.container
      ? props.container.querySelector("#root")
      : document.getElementById("root"),
  );
  // 将基座信息挂载在 window 上
  window.qiankunProps = props;
  root.render(
    <RecoilRoot>
          <App />
    </RecoilRoot>
  );
}

// qiankun 生命周期
renderWithQiankun({
  mount(props) {
    console.log("mount", props);
    render(props);
  },
  bootstrap() {
    console.log("bootstrap");
  },
  unmount(props: any) {
    console.log("unmount");
    root.unmount();
  },
  update(props: any) {},
});

if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render(null);
}

修改 router.ts

export function createRouters(basename: string = "/base") {
  return createBrowserRouter([
    {
        path: "/",
        element: <Navigate replace to="/application" />,
    },
    // .......
  ], {
    basename,
  })
}

修改 App.tsx


// 创建路由,支持动态传入 basename
const router = createRouters(
    qiankunInfos.inQiankun ? qiankunInfos.qiankunProps?.base : "/base",
);

function App() {
    return <RouterProvider router={router} />
}
  • 为什么要动态传入 basename ?因为子应用是 histroy 路由模式,例如子应用 page1 页面链接是: http://[基座域名].com/app1/page1, 那么,basename 是 /app1, 而不是 /

部署

如果子应用和基座不是同一个域名,前端静态资源要支持跨域,后端接口要支持跨域。

遇到问题&解决方案

1. vite 动态构建 base 路径

因为子应用和基座不是部署在同一个域名下,那么在 vite.config.ts 里需要将 base 改成子应用的域名(默认是 /), 这样才不会访问到基座下的域名。但是这样就会有一个问题,部署到不同的环境,子应用就需要打包多次,因为每个环境的域名不一样,而 base 是打包的时候写死的。

方式一:尝试使用 vite experimental.renderBuiltUrl 配置

注意,这是 vite 实验性支持。构建生产版本 | Vite 官方中文文档

原理类似 webpack 中的 __webpack_public_path__

vite.config.ts

experimental: {
  renderBuiltUrl(filename, { hostId, hostType, type }) {
    if (type === 'public') {
      //代码里引用的 public 资源,不能配置 runtime 模式
      return 'https://www.domain.com/' + filename
    } else if (path.extname(hostId) === '.js') {
      return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
    } else {
      return 'https://cdn.domain.com/assets/' + filename
    }
  },
},

index.html

<script>
    window.__assetsPath = function (filename) {
        return window.baseurl + filename
    }
</script>

通过 runtime 方式,我们可以动态指定生成的 index.html 中 scriptlink src 前缀路径。这个前缀 window.baseurl 我们可以是SSR注入到 window 变量里,也可以是基座应用写到 window 变量里。

问题: 不过 index.html 里的静态资源是可以动态从 window 里拿,代码引用的 public assets 代码,却不支持 runtime 方式!只能返回 string。

到此,此方式放弃,做不到真正的动态替换 base 参数。

方式二:配置网关(强烈推荐!)

这个方式是最简单也是最稳妥的方式。

  • 打包是修改 base 为一个绝对路径。

vite.config.ts

{
    base: '/app1'
}
  • 配置网关

假设:座的域名是: www.base.com, 子应用的域名是: www.app1.com

那么可以在基座配置网关,通过 www.base.com/app1/ 转发到 www.app1.com/app1/,这样配置以后,基座访问 /app1 就能请求到子应用的资源。这样就能保证打包一次,放到任意环境都能用。

方式三:脚本动态替换代码

这种方式比较暴力,前提我们是用 docker 部署应用。

  • base 配置一个特殊的字符串。

vite.config.ts

{
    base: '[__VITE_BASE_PLACEHODER__]'
}

这并不是一个真实的路径,只是方便我们在后续替换。

  • docker 启动替换

Dockerfile

ENTRYPOINT [replace.sh]

在 docker 启动时,会运行 ENTRYPOINT 里的脚本,我们可以在里面写一个脚本replace.sh,把 dist 目录下的所有文件遍历一下,然后把每个文件里含有 [VITE_BASE_PLACEHODER] 的字符串,替换成真实的域名。这个域名可以存在 docker 容器的启动命令里,或者放在 docker 的环境变量里, 然后脚本里动态去读。

2.乾坤(qiankun)下不支持 @vitejs/plugin-legacy 插件

配置了 @vitejs/plugin-legacy 插件,index.html 里会额外生成一些 type="module" 的 script。接入到 qiankun 后报错:

qiankun.js?v=f2d8e798:3712 Uncaught SyntaxError: Cannot use 'import.meta' outside a module

原因:import-html-entry 解析 html 后,用 eval去执行里面的 script,如果 script 里使用了type="module" 或者 静态的 import 语法,就会报错。

// 报错,eval 不支持静态 import 语法。
<script>
    import aa from './test.js'
</script>
// 报错,eval type="module"。
<script type="module" src="/aaa.js"></script>

// 兼容写法, eval 可解析动态 import 语法。
<script>
    import('./test.js').finally(() => {})
</script>

解决方案:

我目前临时的方案是将 index.html 里的 type="module" 相关代码通过 vite 插件直接删掉的。

const htmlRemoveModulePlugin = () => {
  return {
    name: "html-remove-module",
    enforce: "post",
    apply: "build",
    transformIndexHtml(html, options) {
      return {
        html: html.replace(/<script type="module"(.*?)<\/script>/g, ""),
        tags: [],
      };
    },
    generateBundle(options, bundle) {
      const template = bundle["index.html"] ? bundle["index.html"].source : "";
      if (template) {
        bundle["index.html"].source = template.replace(
          /<script type="module"(.*?)<\/script>/g,
          "",
        );
      }
    },
  };
};

export default htmlRemoveModulePlugin;

当然,这不是好的方法,后续再研究下怎么把 type="module" 代码转成 eval 支持的写法。

这里吐槽一下 qiankun , 2024 年了,还不支持 vite。当然,我也理解社区靠爱发电的无力,组织了 qiankun v3 版本,其中我最看重的特色就是支持vite, 可最终也没多少人参与 v3 开发,现在也不了了之了。唉~

3.子应用 antd 弹窗无法覆盖基座 header.

原因:基座的 header 是 fixed 定位,切 z-index 为 1000,子应用弹窗同样也是 fixed 定位,且基础 z-index 1000.

解决:基座用的是 antd 组件库,需要自定义主题,把基础 zIndex 设置成 2000。详情参考: antd 主题定制

关键代码如下:

main.tsx

<ConfigProvider
      theme={{
        token: {
          zIndexBase: 2000,
          zIndexPopupBase: 2000,
        },
      }}
    >
</ConfigProvider>

总结

本文系统地讲解了如何将 Vite + React 子应用接入 Qiankun 基座,包括详细的实现步骤和配置建议。文章创新之处在于针对不同的部署环境提出了多种解决方案,尤其是在处理跨域问题和动态构建 base 路径方面。然而,本文也存在不足之处,例如对某些复杂问题的解决方案没有深入展开,可能会对新手造成一定困惑。未来可以增加更多实例和实用技巧,以帮助开发者更好地理解和应用 Qiankun 微前端架构。

转载自:https://juejin.cn/post/7418815782489047052
评论
请登录