浅尝了一下qiankun,感觉不如Nx框架好用

一 为什么要使用微前端框架?

我们不能为了用而用某种技术,先要了解一下这种技术有什么优势,解决了什么问题。为什么要使用微前端架构,与单体架构相比,它有以下优势:

  1. 降低项目复杂度:随着应用规模的增大,前端代码变得庞大和复杂,单体架构逐渐发展成巨石应用,复杂度与日俱增。微前端将大型应用拆分为多个独立的子应用,每个子应用可以单独开发、测试和部署,降低了子应用之间的耦合度,使得子应用代码更容易管理和维护。降低了项目整体复杂度。
  2. 更自由的技术栈选择:单体应用通常需要统一技术栈,这限制了技术的自由灵活度。微前端框架允许不同子应用使用不同的技术栈,各团队可以根据需要选择最适合的技术工具,提升了技术栈选择的自由度。
  3. 更快的部署和发布:单体应用发展成巨石应用之后,发布和部署耗时较长(笔者参与的一个单体应用项目,打包出来的文件有30多M,打包部署比较耗时,一般至少需要十来分钟,不可谓慢),因为任何小的改动都需要重新构建和发布整个应用。微前端架构下,每个子应用可以独立部署和更新,显著的缩短了打包和部署时间,包括本地开发环境修改代码之后的热更新时间。
  4. 更好的用户体验:如果涉及到技术栈升级改造,微前端框架可以实现渐进式迁移,将老旧系统逐步替换为新的系统,用户体验不会受到大的干扰,且可以持续优化。

如果你的项目存在以上应用场景,那么使用微前端框架让你更好地应对规模增长带来的复杂性挑战,降低项目的复杂度,提升技术栈选择的自由度、部署灵活性和维护便利性, 带来更好的用户体验 。

二 qiankun使用方法

知道了微前端框架的好处,现在我们来看看qiankun微前端框架的用法。qiankun 是一个基于 single-spa 的微前端框架,它允许在一个页面中运行多个独立开发、独立部署的前端应用。qiankun 框架的几个组成要素是:

2.1. 子应用的注册和加载

qiankun 允许你注册多个子应用,并根据不同的路由规则加载这些子应用。每个子应用配置参数中的name是子应用的名称,必须唯一(使用场景之一是:当配置样式隔离模式为experimentalStyleIsolation,给所有样式添加一个限定范围的样式类名时会用到) ;entry是子应用项目本地运行地址,container是子应用的容器(子应用嵌入到主项目id为xx的地方),activeRule是应用激活时的路由规则(子应用路由),这四个参数是必填参数,还有其它参数,这里就不一一介绍了。

javascript 复制代码
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'vueApp',
    entry: '//localhost:9000',
    container: '#subapp-container',
    activeRule: '/app-vue',
  },
  // 可以注册更多的子应用
]);

start();

2.2. 路由劫持

qiankun 使用路由劫持技术来决定何时加载和卸载子应用。它通过监听浏览器的 URL 变化,判断当前的 URL 是否匹配某个子应用的 activeRule,从而触发对应的加载或卸载操作。猜测实现原理应该和单页应用的路由原理差不多。

2.3. JS沙箱机制和样式隔离

JS沙箱机制

为了隔离各个子应用,防止它们互相干扰,qiankun 提供了沙箱机制。沙箱机制确保每个子应用运行在一个独立的上下文中,避免全局变量和样式污染。qiankun 提供了两种沙箱(默认的sandbox配置是快照沙箱):

  • 快照沙箱:基于快照的机制,每次子应用卸载时,保存当前全局状态,再次加载时恢复。
  • Proxy 沙箱:基于 ES6 的 Proxy 特性,对全局对象进行代理,确保每个子应用拥有独立的全局环境。

两种沙箱的应用场景:

  • 快照沙箱:适用于多实例、频繁切换和需要状态恢复的场景,尽管有性能开销。
  • 单实例沙箱:适用于单实例、生命周期较长和性能敏感的场景,虽然状态恢复能力较弱。

样式隔离

  • 严格模式(strictStyleIsolation) 默认情况下的沙箱设置无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。 开启严格样式隔离模式。qiankun 会为每个微应用的容器包裹上一个shadow dom节点,确保微应用的样式不会对全局造成影响。

  • 实验模式(experimentalStyleIsolation) qiankun 还提供了另一个实验性的样式隔离特性, 当设置为 true 时,qiankun 会改写子应用所添加的样式,给所有样式规则增加一个样式前缀限定其作用范围。

配置方法如下:

js 复制代码
import {  start } from 'qiankun';
// 启动子应用
start(
  {
    // 详细配置沙箱
    sandbox: {
      // 会为每个微应用的容器包裹上一个shadow dom 节点
      strictStyleIsolation: true, // 启用严格样式隔离
      // 如果为true,会为所有样式规则增加一个前缀选择器规则来限定其影响范围
      experimentalStyleIsolation: false, 
    },
    // 其它配置...
  });

2.4. 生命周期管理

在 Qiankun 框架中,每个子应用需要按照规范暴露一定的生命周期函数,以便主应用能够在不同阶段正确加载和卸载子应用。这些生命周期函数主要包括 bootstrapmountunmount,有时还包括 update(可选)。这些函数是子应用与主应用通信的关键点,确保子应用能够在正确的时机执行相应的逻辑。

子应用必须暴露的生命周期函数

  1. bootstrap:子应用的初始化函数。在子应用加载时,只执行一次。这个函数通常用于初始化全局变量、设置全局状态等操作。
  2. mount:子应用的挂载函数。在每次进入子应用时执行。这个函数通常用于渲染子应用的界面,将子应用的内容挂载到 DOM 中。
  3. unmount:子应用的卸载函数。在每次离开子应用时执行。这个函数通常用于销毁子应用的实例,清理全局状态和事件监听器等操作。
  4. update(可选):子应用的更新函数。这个函数在子应用需要更新时被调用,通常用于处理子应用的动态属性或状态变化。
js 复制代码
export async function bootstrap() {
  console.log('vue app bootstraped');
}

export async function mount(props) {
  console.log('vue app mount', props);
}

export async function unmount() {
  console.log('vue app unmount');
}

// update 函数(可选)
export async function update(props) {
  console.log('更新子应用', props);
  // 在这里处理动态属性或状态变化
}

2.5. 主应用与子应用通信

qiankun 提供了一套机制,使主应用和子应用之间能够进行通信。这包括通过 props 传递数据,以及使用全局状态管理工具(如 Redux 或 Pina/Vuex)进行状态共享。

2.6 qiankun的工作流程

  1. 注册子应用:在主应用中注册所有子应用,并指定每个子应用的入口、挂载容器和激活规则。
  2. 路由劫持:qiankun 劫持浏览器的路由变化,判断当前 URL 是否匹配某个子应用的激活规则。
  3. 加载子应用:如果 URL 匹配某个子应用,qiankun 加载该子应用的资源(HTML、CSS、JS),并在指定的容器中挂载子应用。
  4. 运行子应用:调用子应用的生命周期钩子函数,执行子应用的初始化和渲染逻辑。
  5. 卸载子应用:当 URL 变化时,如果不再匹配当前子应用,qiankun 卸载该子应用,调用其卸载钩子函数,清理子应用的资源。

三 动手实践

光说不练假把式。让我们动手用qiankun创建一个vue3和react子应用,并能让两个子应用进行通信,证明我们确实会用qiankun微前端框架了。

3.1 创建主应用

create-vite脚手架创建一个vue3主应用,在主应用中安装qiankun

sql 复制代码
npx create-vite mian-app --template vue 
cd main-app 
pnpm add qiankun

在主应用的入口文件src/main.js中,注册和启动两个子应用, 实现子应用之间通信的方式是:在主应用中创建全局共享状态,因为qiankun会给每个子应用自动添加setGlobalState方法,每个子应用可以通过此方法修改全局状态。现在问题是,怎么让子应用获取到全局状态最新值。有人说可以在子应用中使用onGlobalStateChange方法监听全局状态的变化。可以是可以,但效率太低。我们只是想在子应用每次加载的时候,同步一下最新的全局状态数据。经过思考,发现可以定义一个getGlobalState方法,获取最新的全局状态值,在注册子应用时,通过props属性传递给每个子应用,每个子应用在mount事件里调用getGlobalState方法就能获取到全局状态最新值。

js 复制代码
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

import { registerMicroApps, start, initGlobalState } from "qiankun";

const app = createApp(App);
app.use(router);
app.mount("#main-app");

// 初始化 state
let initState = { count: 0 };
const actions = initGlobalState(initState);
// 在主应用中监听贡献数据的变化
actions.onGlobalStateChange((state) => {
  initState = state;
}, true);

const getGlobalState = () => initState;

// 注册子应用
registerMicroApps([
  {
    // 子应用的名称,必须唯一。
    name: "react-app",
    // 子应用项目本地运行地址
    entry: "//localhost:8100",
    // 子应用的容器(子应用嵌入到主项目id为container的地方)
    container: "#container",
    activeRule: "/react",
    //子应用激活时的路由规则(子应用路由)
    props: { getGlobalState },
  },
  {
    name: "vue-app",
    entry: "//localhost:8200",
    container: "#container",
    activeRule: "/vue",
    props: { getGlobalState },
  },
]);

start({ sandbox: { experimentalStyleIsolation: true } });

在App.vue中添加子应用导航和挂载节点

html 复制代码
<template>
  <li><router-link to="/react">react微应用</router-link></li>
  <li><router-link to="/vue">vue微应用</router-link></li>
  <!-- 挂载子应用,id要与注册子应用时的容器id相对应 -->
  <div id="container"></div>
</template>

<script>
export default {
  name: "App",
};
</script>

<style></style>

创建主应用的路由文件main-app\src\router\index.js, 内容如下: 因为前面已经在App.vue文件中添加了子应用dom挂载节点, 所以这里/react/vue这两个路由的页面都设置成空壳,无需真正的创建文件,只是为了遵循vue-router定义路由的规则而已,不报错而已。

js 复制代码
import { createRouter, createWebHistory } from "vue-router";
import App from "../App.vue";

const routes = [
  {
    path: "/",
    component: App,
  },
  {
    path: "/react",
    // 仅用来占位,未实际用到
    component: { template: "<template></template>" },
  },
  {
    path: "/vue",
    component: { template: "<template></template>" },
  },
];

const router = createRouter({
  history: createWebHistory("/"),
  routes,
});
export default router;

修改package.json中的脚本命令,在每条指令后面加个:main, 用于区分应用,防止应用多了,不好辨识,容易搞混。

json 复制代码
{
  // ...
  "scripts": {
    "start:main": "vite",
    "build:main": "vite build",
    "preview:main": "vite preview"
  },
}

3.2 创建React子应用

create-vite脚手架创建一个React子应用,并安装必要的依赖(要添加一个依赖包vite-plugin-qiankun)。为什么这里选择用create-vite而非create-react-app脚手架创建React应用? 因为create-react-app默认使用的编译工具webpack,没有create-vite脚手架默认的编译工具vite快。天下武功,唯快不破。

bash 复制代码
npx create-vite react-app --template react 
cd react-app 
pnpm add vite-plugin-qiankun

src/main.jsx 入口函数中导出 qiankun 生命周期函数,子应用有两种打开方式,一个通过qiankun主应用加载,而是独立运行,两种方式的初始化内容不一样,要通过qiankunWindow.__POWERED_BY_QIANKUN__判断一下是以哪种方式打开的,执行对应的初始化流程。

js 复制代码
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/dist/helper";

const initQianKun = () => {
  renderWithQiankun({
    // 把要用的写在前面
    mount(props) {
      render(props);
    },
    bootstrap() {},
    unmount() {},
  });
};

const render = (props = {}) => {
  // 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
  const { container,  setGlobalState, getGlobalState } = props;
  const appDom = container ? container : document.getElementById("root");
  ReactDOM.createRoot(appDom).render(
    <React.StrictMode>
      <App  setGlobalState={setGlobalState} getGlobalState={getGlobalState} />
    </React.StrictMode>
  );
};

// 判断当前应用是否在主应用中
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render();

修改vite.config.js, 修改子应用的端口号(须与注册时的端口保持一致), 另外要加一项server.origin的配置,解决本地开发,主应用加载子应用时,图片显示不出来的问题。

js 复制代码
import path from "path";
import { defineConfig } from "vite";
import qiankun from "vite-plugin-qiankun";

export default defineConfig({
  // react-app' 是子应用名,与主应用注册时保持一致
  // useDevMode 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响
  plugins: [vue(), qiankun("react-app", { useDevMode: true })],
  server: {
    port: 8100,
    open: true,
    // 解决本地开发环境,主应用访问子应用时,子应用图片加载不出来的问题
    origin: "http://localhost:8100",
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  // ...
});

在package.json中,修改脚本命令命令,区分应用:

json 复制代码
{
  // ...
  "scripts": {
    "start:react": "vite",
    "build:react": "vite build",
    "preview:react": "vite preview"
  },
}

3.3 创建Vue3子应用

create-vite脚手架创建一个Vue3子应用

bash 复制代码
npx create-vite vue3-app --template vue
cd vue3-app
pnpm add vite-plugin-qiankun

修改 Vue3 子应用代码, 在 src/main.js 中导出 qiankun 生命周期函数,逻辑与创建React子应用十分相似, 这里有一点要说明一下,就是要修改一下项目根目录下的index.html中的根容器的id, 因为用create-vite创建的vue项目,页面根容器的id都是app,为了使各个项目的id不产生冲突,要修改一下根容器的id,与app.mount(container ? container.querySelector("#vue3-app") : "#vue3-app");这句的id保持一致。

js 复制代码
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { store } from "./store";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/es/helper";

let app = null;
const render = (props = {}) => {
  const { container, setGlobalState, getGlobalState } = props;

  if (getGlobalState) {
    store.count = getGlobalState().count;
  }
  app = createApp(App, { setGlobalState });
  app.use(router);
  app.mount(container ? container.querySelector("#vue3-app") : "#vue3-app");
};

const initQianKun = () => {
  renderWithQiankun({
    mount(props) {
      render(props);
    },
    bootstrap() {
      console.log("vue3 app bootstraped");
    },
    unmount() {
      app.unmount();
      app._container.innerHTML = "";
      app = null;
    },
  });
};

// 如果不是通过qiankun启动的,则单独渲染
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render();

修改vite.config.js,这里只列出与React子应用配置有差别的部分

js 复制代码
export default defineConfig({
  // vue-app' 是子应用名,与主应用注册时保持一致
  // useDevMode 如果是在主应用中加载子应用vite,必须打开这个,否则vite加载不成功, 单独运行没影响
  plugins: [vue(), qiankun("vue-app", { useDevMode: true })],
  server: {
    port: 8200,
    origin: "http://localhost:8200", // 子应用引入到主应用之后,子应用中的图片在主应用下加载不出来、找不到,需要将origin设置成子应用本地运行地址
  },
});

在package.json中添加如下命令:

json 复制代码
{
  // ...
  "scripts": {
    "start:vue3": "vite",
    "build:vue3": "vite build",
    "preview:vue3": "vite preview"
  },
}

3.4 启动应用

在根目录下,打开三个终端,依次执行下面的命令,启动主应用与react和vue两个子应用

bash 复制代码
cd main-app && yarn start:main
cd react-app && yarn start:react
cd vue3-app && yarn start:vue3

运行效果如下:可以看到,两个子应用切换正常,子应用之间可以进行通信,React子应用的图片也加载正常,至此,我们已经掌握了qiankun框架的用法,是不是感觉很有收获。

3.5 线上部署

本地开发跑通之后,还要考虑微前端的部署问题。这样才算打通了全流程。如果你使用 Nginx网页服务器,可以参考以下 Nginx 配置示例:

bash 复制代码
server {
    listen 80;
    server_name yourdomain.com;

    location / {
        root /path/to/your/main-app/dist;
        # 防止使用history路由时,刷新页面报404
        try_files $uri /index.html;
    }

    location /react/ {
        alias /path/to/your/react-app/dist/;
        try_files $uri /index.html;
    }

    location /vue/ {
        alias /path/to/your/vue-app/dist/;
        try_files $uri /index.html;
    }
}

这一部分我没有亲测,可能会有点小坑,需要自己填一下。

最后

笔者还用过Nx微前端框架,qiankunNx微前端框架相比,有一点明显不如Nx框架设计理念好,Nx微前端框架,每个子应用的npm依赖包,tsprettier, eslint, stylelint, commitlint, postcss, jest, vite等的基本配置都是共享的,每个子应用可以基础配置的基础上定制本应用的一些特有属性,而qiankun微前端框架,所用应用的依赖都是独立的,灵活是灵活,但我觉得,开发者更倾向把每个项目公共的配置提取出到子应用之外进行复用,这样能减少一些重复文件和配置,使项目更好维护。当然你也可能有不同的意见,只要是基于理性和事实,我也赞同你的意见。技术就是用来讨论和对比的,谁说的言之有理,我就会按照正确的道理去做。另外,本文的qiankun模版项目已上传到码云,如果你也对qiankun微前端框架感兴趣,欢迎下载学习交流。

相关推荐
Мартин.3 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。4 小时前
案例-表白墙简单实现
前端·javascript·css
数云界4 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd4 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常4 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer4 小时前
Vite:为什么选 Vite
前端
小御姐@stella4 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing4 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd4 小时前
前端知识汇总(持续更新)
前端
万叶学编程7 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js