qiankun保姆级教程(React)

一、介绍

1、什么是qiankun

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。(官方说的)

正所谓乾坤无偏奉一体,万象森罗尽在其中。在我看来qiankun就是一个乾坤百宝袋(主应用),可以把很多应用(微应用)都装进来,每个应用都可以独立开发,独立部署,技术栈自主。 有了qiankun企业的业务就可以实现真正的增量升级,将一个业务量庞大的系统拆分成多个自主独立的模块,每个微应用独立运行,解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。(再也不用担心程序猿们在屎山修改业务)

2、qiankun的特性

技术栈无关:微应用可以使用任何技术栈,只需和主应用连接皆可实现开箱即用。

独立开发,独立部署:统一系统的不同应用可由不同团队独立开发,大大节省项目开发周期。

增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略。

独立运行:每个微应用之间状态隔离,运行时状态不共享。

资源预加载:在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

3、qiankun vs iframe

为什么不使用iframe这种原生的更为简便的方案而要另辟蹊径的使用qiankun呢?

原因在于iframe最大的的问题在于他的隔离性太强了,很难对其进行突破,导致应用上下文很难被共享,这带来了很多开发体验和产品体验的问题。这也是为什么不用iframe而去选择使用qiankun的原因。

二、qiankun搭建

1、安装qiankun

主应用和微应用都要进行安装哟~

$ yarn add qiankun # 或者 npm i qiankun -S

2、通用配置

在所有应用中安装插件 react-app-rewired

npm install react-app-rewired --save-dev 或 yarn add react-app-rewired --dev

在package.json文件中修改启动脚本

json 复制代码
"scripts": {
    "start": "react-app-rewired start",
  },

在根目录新增文件config-overrides.js

javascript 复制代码
const { name } = require('./package');

module.exports = {
  webpack: (config) => {
    //将微应用打包成 umd 模块时,设置输出的全局变量名。
    config.output.library = `${name}-[name]`; 
    //设置打包输出的模块格式为 umd。
    config.output.libraryTarget = 'umd';
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    // config.output.jsonpFunction = `webpackJsonp_${name}`; 
    // 指定在浏览器环境中使用全局对象
    config.output.globalObject = 'window';

    return config;
  },

  devServer: (_) => {
    const config = _;
//为开发服务器的响应头添加 Access-Control-Allow-Origin,允许跨域请求(注意仅适用于开发环境)。
    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    // 当路由请求404时,返回根目录index.html,常用于单页应用的路由配置。
    config.historyApiFallback = true;
    //禁用热模块替换(HMR),即禁止在开发服务器中热更新模块。
    config.hot = false;
    //禁用开发服务器的内容监视功能,这样开发服务器不会监听文件的变化。
    config.watchContentBase = false;
    //禁用开发服务器的实时重新加载功能。
    config.liveReload = false;

    return config;
  },
};

qiankun 在微应用中使用 react-app-rewired 是为了能够定制 Create React App 的 webpack 配置,以适应微应用的需求。这样可以确保微应用在 qiankun 的环境中能够正常运行并集成到宿主应用中。

3、在主应用中注册微应用

js 复制代码
/*主应用的index.tsx(不同项目架构有不同的注册方法,可以在任何地方注册子应用。
一般来说,我们会在主应用的入口文件或根组件中注册子应用。)*/
import { registerMicroApps, start } from 'qiankun';
const micors = [
  {
    name: 'react app', // 微应用的名字,自拟最好能一眼看出这个应用的功能
    entry: '//localhost:7100', // 微应用的入口地址
    container: '#container', // 微应用要挂载的容器id
    activeRule: '/app1',// 微应用对应的路由匹配规则
  },
  {
    name: 'vue app',
    entry: { scripts: ['//localhost:7100/main.js'] },
    container: '#container2',
    activeRule: '/app2',
  },
];
registerMicroApps(micors,{
    beforeLoad: (app) => {
        return Promise.resolve();
    },
    beforeMount: (app) => {
        return Promise.resolve();
    },
});

start();// 最后别忘了启动哟

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式如下:

js 复制代码
import { loadMicroApp } from 'qiankun';

loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

配置微应用挂载点(这个挂载点名字要和上面的container对应)

js 复制代码
// App.tsx
import { Route, BrowserRouter, Routes, Link } from "react-router-dom";
import "./App.css";

function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Link to="/app1">app1</Link>
        <Link to="/app2">app2</Link>
        <div id="container"></div>
        <Routes>
          <Route path="/app1">app1</Route>
          <Route path="/app2">app2</Route>
        </Routes>
      </div>
    </BrowserRouter>
  );
}

export default App;

以上是我主应用实现路由跳转的一些方式。

4、配置微应用

在所有微应用src 目录新增 public-path.js文件

js 复制代码
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

这个文件是用于根据qiankun注入的全局变量来动态配置webpack的publicPath属性,以确保子应用的静态资源能够正确加载。(指定静态资源路径的,不然图片那些静态资源就加载不出来。)

修改微应用入口文件index.tsx

js 复制代码
// index.tsx
import "./public-path.js";//导入上一步配置的文件,用于正确加载静态资源文件
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";

export async function bootstrap() {
  console.log("[react16] react app bootstraped");
}

export async function mount(props: any) {
  console.log("[react16] props from main framework", props);
  ReactDOM.render(
    <BrowserRouter
      basename={window.__POWERED_BY_QIANKUN__ ? "app1" : undefined}
    >
      <App />
    </BrowserRouter>,
    props.container
      ? props.container.querySelector("#root")
      : document.getElementById("root")
  );
}

export async function unmount(props: any) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(
    container
      ? container.querySelector("#root")
      : document.querySelector("#root")
  );
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById("root")
  );
}
reportWebVitals();

三、主应用与子应用间的通信

在完成以上步骤之后你就能得到一个由微前端搭建起来的应用啦,当然如果仅仅只是这样这还远远不够,我们还需要实现主应用和子应用的通信。

1、qiankun自带的全局状态API

首先在主应用的入口文件index.tsx写入以下代码

js 复制代码
import { initGlobalState, MicroAppStateActions } from "qiankun";

const state = {
  nickname: "crazy",
};
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log("base", state, prev);
});

在注册微应用时将props注册进微应用

js 复制代码
registerMicroApps([
  {
    name: "app1",
    entry: "//localhost:3011",
    container: "#container",
    activeRule: "/app1",
    props: { state },
  },
  {
    name: "app2",
    entry: "//localhost:3012",
    container: "#container",
    activeRule: "/app2",
    props: { state },
  },
]);

在主应用的入口文件中可以使用actions.setGlobalState()对全局变量进行修改

js 复制代码
actions.setGlobalState({ ...state, nickname: "123", age: 20 });

在微应用中可以使用props.setGlobalState()对已有的state对象中的属性进行修改,但是没有权限向state对象中进行属性的添加,如果你使用props.setGlobalState({ ...state, nickname: "123" ,age:19});那么nickname中的数据会被更改,但是age这一项会被忽略无法添加到全局变量中,而主应用则可以正常添加。

js 复制代码
props.setGlobalState({ ...state, nickname: "123" });

1.1局限性

如上所说qiankun自带的通信方式,子应用对全局状态中的属性只有修改的权限,但是无法向全局状态中新增或者删除属性。

2、localStorage通信

使用localStorage进行主应用与微应用间的通信是目前我觉得最直接的通信方式,不管是主应用还是微应用,使用以下两行代码都可以很快的进行数据的传递,但是不能滥用localStorage,我们将各应用公用的数据放到localStorage就足够了,否则会出现影响性能,增加跨站点脚本攻击风险等一系列的问题。

js 复制代码
localStorage.setItem("username", "crazy");
localStorage.getItem("username");

四、API

1、registerMicroApps(apps, lifeCycles?)

用于注册子应用

  • 参数

    • apps - Array<RegistrableApp> - 必选,微应用的一些注册信息
    • lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子
  • 类型

    • RegistrableApp

      • name - string - 必选,微应用的名称,微应用之间必须确保唯一。

      • entry - string | { scripts?: string[]; styles?: string[]; html?: string } - 必选,微应用的入口。

        • 配置为字符串时,表示微应用的访问地址,例如 https://qiankun.umijs.org/guide/
        • 配置为对象时,html 的值是微应用的 html 内容字符串,而不是微应用的访问地址。微应用的 publicPath 将会被设置为 /
      • container - string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'container: document.querySelector('#root')

      • activeRule - string | (location: Location) => boolean | Array<string | (location: Location) => boolean> - 必选,微应用的激活规则。

        • 支持直接配置字符串或字符串数组,如 activeRule: '/app1'activeRule: ['/app1', '/app2'],当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。
        • 支持配置一个 active function 函数或一组 active function。函数会传入当前 location 作为参数,函数返回 true 时表明当前微应用会被激活。如 location => location.pathname.startsWith('/app1')
      • loader - (loading: boolean) => void - 可选,loading 状态发生变化时会调用的方法。

      • props - object - 可选,主应用需要传递给微应用的数据。

    • LifeCycles

      type Lifecycle = (app: RegistrableApp) => Promise;

      • beforeLoad - Lifecycle | Array<Lifecycle> - 可选
      • beforeMount - Lifecycle | Array<Lifecycle> - 可选
      • afterMount - Lifecycle | Array<Lifecycle> - 可选
      • beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
      • afterUnmount - Lifecycle | Array<Lifecycle> - 可选
js 复制代码
registerMicroApps([
  {
    name: 'react app', // 微应用的名字,自拟最好能一眼看出这个应用的功能
    entry: '//localhost:7100', // 微应用的入口地址
    container: '#container', // 微应用要挂载的容器id
    activeRule: '/app1',// 微应用对应的路由匹配规则
    loader:(loading)=>{},// loading 状态发生变化时会调用的方法。
    props: {},// 传递给子应用的props
  },
],
{
    // 各生命周期调用的方法
    beforeLoad: async (app) => {},
    beforeMount: async (app) => {},
    afterMount: async (app) => {},
    afterUnmount: async (app) => {},
    beforeUnmount: async (app) => {},
});

2、start(opts?)

用于启动qiankun

  • prefetch - boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] }) - 可选,是否开启预加载,默认为 true

    • 配置为 true 则会在第一个微应用 mount 完成后开始预加载其他微应用的静态资源

      配置为 'all' 则主应用 start 后即开始预加载所有微应用静态资源

      配置为 string[] 则会在第一个微应用 mounted 后开始加载数组内的微应用资源

      配置为 function 则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)

    • sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true

    • singular - boolean | ((app: RegistrableApp<any>) => Promise<boolean>); - 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true

    • fetch - Function - 可选,自定义的 fetch 方法。

    • getPublicPath - (entry: Entry) => string - 可选,参数是微应用的 entry 值。

    • getTemplate - (tpl: string) => string - 可选。

    • excludeAssetFilter - (assetUrl: string) => boolean - 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理。

    js 复制代码
    start();

3、setDefaultMountApp(appLink)

用于设置主应用启动后默认进入的微应用。

  • 参数

    • appLink - string - 必选
js 复制代码
import { setDefaultMountApp } from 'qiankun';

setDefaultMountApp('/app1');

4、runAfterFirstMounted(effect)

用于第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。

  • 参数

    • effect - () => void - 必选
js 复制代码
import { runAfterFirstMounted } from 'qiankun';

runAfterFirstMounted(() => startMonitor());

5、loadMicroApp(app, configuration?)

用于手动加载微应用

  • 参数

    • app - LoadableApp - 必选,微应用的基础信息

      • name - string - 必选,微应用的名称,微应用之间必须确保唯一。
      • entry - string | { scripts?: string[]; styles?: string[]; html?: string } - 必选,微应用的入口(详细说明同上)。
      • container - string | HTMLElement - 必选,微应用的容器节点的选择器或者 Element 实例。如container: '#root'container: document.querySelector('#root')
      • props - object - 可选,初始化时需要传递给微应用的数据。
    • configuration - Configuration - 可选,微应用的配置信息

      • sandbox - boolean | { strictStyleIsolation?: boolean, experimentalStyleIsolation?: boolean } - 可选,是否开启沙箱,默认为 true
      • singular - boolean | ((app: RegistrableApp<any>) => Promise<boolean>); - 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 false
      • fetch - Function - 可选,自定义的 fetch 方法。
      • getPublicPath - (entry: Entry) => string - 可选,参数是微应用的 entry 值。
      • getTemplate - (tpl: string) => string - 可选
      • excludeAssetFilter - (assetUrl: string) => boolean - 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理
  • 返回值 - MicroApp - 微应用实例

    • mount(): Promise;
    • unmount(): Promise;
    • update(customProps: object): Promise;
    • getStatus(): | "NOT_LOADED" | "LOADING_SOURCE_CODE" | "NOT_BOOTSTRAPPED" | "BOOTSTRAPPING" | "NOT_MOUNTED" | "MOUNTING" | "MOUNTED" | "UPDATING" | "UNMOUNTING" | "UNLOADING" | "SKIP_BECAUSE_BROKEN" | "LOAD_ERROR";
    • loadPromise: Promise;
    • bootstrapPromise: Promise;
    • mountPromise: Promise;
    • unmountPromise: Promise;
js 复制代码
import { loadMicroApp } from 'qiankun';
import React from 'react';

class App extends React.Component {
  containerRef = React.createRef();
  microApp = null;

  componentDidMount() {
    this.microApp = loadMicroApp({
      name: 'app1',
      entry: '//localhost:1234',
      container: this.containerRef.current,
      props: { brand: 'qiankun' },
    });
  }

  componentWillUnmount() {
    this.microApp.unmount();
  }

  componentDidUpdate() {
    this.microApp.update({ name: 'kuitos' });
  }

  render() {
    return <div ref={this.containerRef}></div>;
  }
}

6、prefetchApps(apps, importEntryOpts?)

用于手动预加载指定的微应用静态资源。

  • 参数

    • apps - AppMetadata[] - 必选 - 预加载的应用列表
    • importEntryOpts - 可选 - 加载配置
  • 类型

    • AppMetadata

      • name - string - 必选 - 应用名
      • entry - string | { scripts?: string[]; styles?: string[]; html?: string } - 必选,微应用的 entry 地址
js 复制代码
import { prefetchApps } from 'qiankun';

prefetchApps([
  { name: 'app1', entry: '//localhost:7001' },
  { name: 'app2', entry: '//localhost:7002' },
]);

7、addGlobalUncaughtErrorHandler(handler)

用于添加全局的未捕获异常处理器。

  • handler - (...args: any[]) => void - 必选
js 复制代码
import { addGlobalUncaughtErrorHandler } from 'qiankun';

addGlobalUncaughtErrorHandler((event) => console.log(event));

8、removeGlobalUncaughtErrorHandler(handler)

用于移除全局的未捕获异常处理器。

  • 参数

    • handler - (...args: any[]) => void - 必选
js 复制代码
import { removeGlobalUncaughtErrorHandler } from 'qiankun';

removeGlobalUncaughtErrorHandler(handler);

9、initGlobalState(state)

用于定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。

  • 参数

    • state - Record<string, any> - 必选
  • 返回

    • MicroAppStateActions

      • onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
      • setGlobalState: (state: Record<string, any>) => boolean, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
      • offGlobalStateChange: () => boolean,移除当前应用的状态监听,微应用 umount 时会默认调用

主应用

js 复制代码
import { initGlobalState, MicroAppStateActions } from 'qiankun';

// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

微应用

js 复制代码
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

相关推荐
y先森5 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy5 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189115 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿6 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡7 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇7 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒7 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员8 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐8 小时前
前端图像处理(一)
前端