微前端方案初探-qiankun

时间:2021-2-20 作者:admin

前言

    天地混沌,洪荒初开。应业务之需,初探乾坤(qiankun)。

我要做什么

公司项目采用react、angular框架,功能复杂繁多,导致项目结构复杂、代码量巨大。为了进一步验证qiankun方案是否可行,确定一下调研目标:

  • 兼容react、angular不同技术栈项目
  • 子应用嵌套
  • 子应用并行
  • localStrage、cookie是否共享
  • qiankun的props、initGlobalState通信机制可用性

项目搭建

react项目:

主基座搭建sub-main,子项目搭建react-sub、react-sub2,使用官网create-react-app进行,结合@rescripts/cli快速配置脚手架,进行相关配置。

主项目sub-main:

* 子项目react-sub:

子项目react-sub2:

angular项目:

使用@angular/cli脚手架构建子项目angular-sub

qiankun框架使用

主项目安装依赖

yarn add qiankun # 或者 npm i qiankun -S

主应用中注册微应用

import { registerMicroApps, start } from "qiankun";
registerMicroApps([
  {
    name: "react-sub", // 应用名称
    entry: "//localhost:3000", // 应用地址
    container: "#subContainer", // 嵌入主应用位置
    activeRule: "/sub1",    // 匹配规则
  },
  {
    name: "react-sub2", 
    entry: "//localhost:3002",
    container: "#subContainer",
    activeRule: "/sub2",
  },
  {
    name: "angular-sub",
    entry: "//localhost:4200",
    container: "#subContainer",
    activeRule: "/sub3",
  },
]);
start();

子应用配置

子应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出bootstrapmountunmount三个生命周期钩子,以供主应用在适当的时机调用。

react项目:

// 项目挂载
function render(props) {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById("sub2root")
  );
  console.log(props);
}

// 子应用独立运行判断
if (!window.__POWERED_BY_QIANKUN_PARENT__) {
  render();
}

// 只会在子应用初始化的时候调用一次
export async function bootstrap() {
  console.log("react-sub2 bootstraped");
}

// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
  console.log("react-sub2 props from main framework", props);
  render(props);
}

// 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(
    container
      ? container.querySelector("#sub2root")
      : document.querySelector("#sub2root")
  );
}

添加public-path.js文件,并引入子应用index.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

angular项目:

Angular 与 qiankun 目前的兼容问题,需要特殊处理。

使用 single-spa-angular 生成一套配置

ng add single-spa-angular 

在生成 single-spa 配置后,我们需要进行一些 qiankun 的接入配置。我们在 Angular 微应用的入口文件 main.single-spa.ts 中,导出 qiankun 主应用所需要的三个生命周期钩子函数,代码实现如下:

import "zone.js/dist/zone"; // 需额外引入,保证子项目独立运行
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';

if (environment.production) {
  enableProdMode();
}

// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch((err) => console.error(err));
}

const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
  },
  template: '<app-root />',
  Router,
  NgZone,
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

需在独立安装zone.js, 并引入上述文件

npm i zone.js -S

处理angular项目兼容问题,安装zone.js,并在主应用中的index.js文件引入

npm i zone.js -S

注:兼容原因

  1. qiankun在加载子应用前,会使用es6的Proxy方法,在window上创建一份proxy(也就是所谓的jsSandbox),让子应用实际运行在proxy中,以此来监听子应用做出的对window修改并能在卸载应用时还原。简单来说,就是当前的proxy的与Angular所依赖的zone.js不兼容,导致在加载Angular子应用时直接报错。
  2. 当前qiankun进行子应用的模块导入时,会查找最后一个添加到window上的全局变量作为umd导出的变量。而Angular 6.x以上版本会在全局创建ngDevMode等全局变量,导致qiankun寻找到的的export变量为ngDevMode。最后导致qiankun进行模块查找时出现错误。

打包命令修改,支持指定路径应用

ng serve --disable-host-check --port 2000 --base-href /sub3 --live-reload false

构建工具配置

react应用webpack:

const { name } = require('./package');

module.exports = {
  webpack: config => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';

    return config;
  },

  devServer: _ => {
    const config = _;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;

    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

ng 配置文件 extra-webpack.config.js

const singleSpaAngularWebpack = require("single-spa-angular/lib/webpack")
  .default;
const webpackMerge = require("webpack-merge");
const { name } = require("./package");

module.exports = (angularWebpackConfig, options) => {
  const singleSpaWebpackConfig = singleSpaAngularWebpack(
    angularWebpackConfig,
    options
  );

  const singleSpaConfig = {
    output: {
      library: `${name}-[name]`,
      libraryTarget: "umd",
    },
    externals: {
      "zone.js": "Zone",
    },
  };
  const mergedConfig = webpackMerge.smart(
    singleSpaWebpackConfig,
    singleSpaConfig
  );
  return mergedConfig;
};

注:主应用加载子应用时,子应用必须支持跨域加载

启用各应用独立运行

由实际效果可得,qiankun可兼容react、angular子应用,angular项目存在一些兼容问题。

加载子应用react-sub

加载子应用react-sub2

加载子应用angular-sub

子应用嵌套

子项目自己运行一个 qiankun

存在的问题:

1、子项目无法根据已有信息判断是独立运行还是被集成

if (!window.__POWERED_BY_QIANKUN__) {
 render();
}

由于子项目本身也是一个qiankun 项目,所以独立运行时 window.POWERED_BY_QIANKUN 为 true,被集成时,还是 true。

解决办法:在主项目的入口文件另外定义一个全局变量window.POWERED_BY_QIANKUN_PARENT = true;,用这个变量来区分是被集成还是独立运行

2、子项目入口文件的修改

主要有以下几点注意的地方:
切换子项目时,避免重复注册孙子项目,
由于子项目会被注入一个前缀,那么孙子项目的路由也要加上这个前缀
注意容器的冲突,子项目和孙子项目使用不同的容器

3、打包配置的修改

以上操作完成后,在主项目中可以把这个 qiankun 子项目加载出来,但是点击其孙子项目,报错,生命周期找不到。

修改一下孙子项目的打包配置:
library: `${name}`,

原因:qiankun 取子项目的生命周期,优先取子项目运行时最后一个挂载到 window 上的变量,如果这个不是生命周期函数,再根据 appName 取。让 webpack 的 library 值对应 appName 即可。

运行结果:

子应用并行

start开启多实例,路由匹配规则activeRule配置上多个应用,则会同事激活,注意子应用并行需要挂在主应用不同id位置。

registerMicroApps([
  {
    name: "react-sub", // 应用名称
    entry: "//localhost:3000", // 应用地址
    container: "#subContainer", // 嵌入主应用位置
    activeRule: "/sub1", // 匹配规则
  },
  {
    name: "react-sub2",
    entry: "//localhost:3002",
    container: "#subContainer2",
    activeRule: "/sub1",
  },
]);

start({singular: false});

通信

浏览器 api

由于qiankunshi采用HTML Entry,localStrage、cookie可共享。

qiankun api

props

主应用通过props,传递给子应用

{
    name: "react-sub", // 应用名称
    entry: "//localhost:3000", // 应用地址
    container: "#subContainer", // 嵌入主应用位置
    activeRule: "/sub1",    // 匹配规则
    props: {
      aa: 11,
    },
  },

子应用在生命周期中获取

export async function bootstrap(props) {
  
}
export async function mount(props) {
 
}
export async function unmount(props) {
}

initGlobalState

主应用通过initGlobalState传递,并监听

// 初始化 state
const actions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log("main-onGlobalStateChange", state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

子应用在生命周期中接收

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

注意坑点

  1. 子应用必须支持跨域
  2. react 需要设置public-path的文件,保证子应用的正常加载
  3. angluar子应用注意zone.js与乾坤框架的兼容问题
  4. anluar子应用安装single-spa-angular后,需要独立安装zone.js依赖,并引入
  5. 子应用嵌套、低版本chrome浏览器需要注意library配置修改为packageNmae
  6. 主、子应用根元素id保持唯一,建议以各自项目名称设置

github项目地址

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。