微前端乾坤方案

微前端乾坤方案


了解乾坤

官方文档

介绍

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

qiankun 的核心设计理念

  • 🥄 简单

    由于主应用微应用都能做到技术栈无关,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造。同时由于 qiankun 的 HTML entry 及沙箱的设计,使得微应用的接入像使用 iframe 一样简单。

  • 🍡 解耦/技术栈无关

    微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用,而 qiankun 的诸多设计均是秉持这一原则,如 HTML entry、沙箱、应用间通信等。这样才能确保微应用真正具备 独立开发、独立运行 的能力。

特性

  • 📦 基于 single-spa 封装,提供了更加开箱即用的 API。
  • 📱 技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。
  • 💪 HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。
  • 🛡​ 样式隔离,确保微应用之间样式互相不干扰。
  • 🧳 JS 沙箱,确保微应用之间 全局变量/事件 不冲突。
  • ⚡️ 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
  • 🔌 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统。

qiankun 在 single-spa 的基础上都增加了啥?

以下是 qiankun 提供的特性:

  • 实现了子应用的加载,在原有 single-spa 的 JS Entry 基础上再提供了 HTML Entry
  • 样式和 JS 隔离
  • 更多的生命周期:beforeMount, afterMount, beforeUnmount, afterUnmount
  • 子应用预加载
  • umi 插件
  • 全局状态管理
  • 全局错误处理



qiankun 的使用

主应用

0、安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
1、引入
import {
    registerMicroApps, start } from 'qiankun';
2-1、注册子应用,并启动
registerMicroApps([
  {
   
    name: 'react app', // app name registered
    entry: '//localhost:7100',
    container: '#yourContainer',
    activeRule: '/yourActiveRule',
  },
  {
   
    name: 'vue app',
    entry: {
    scripts: ['//localhost:7100/main.js'] },
    container: '#yourContainer2',
    activeRule: '/yourActiveRule2',
  },
]);

// 启动 qiankun
start();

备注

自动注册模式:当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可。

2-2、手动加载子应用

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

import {
    loadMicroApp } from 'qiankun';

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



子应用改造

微应用不需要额外安装任何其他依赖即可接入到 qiankun 主应用。

1、前提

子应用的资源和接口的请求都在主域名发起,所以会有跨域问题,子应用必须做cors 设置

// webpack

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

module.exports = {
   
  devServer: (config) => {
   
    // 因为内部请求都是 fetch 来请求资源,所以子应用必须允许跨域
    config.headers = {
   
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Credentials': true,
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
      'Content-Type': 'application/json; charset=utf-8',
    };
    return config;
  },
};

2、生命周期改造

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

具体操作可以参考下面示例
步骤一:在 src 目录新增 public-path.js,用于修改运行时的 publicPath
if (window.__POWERED_BY_QIANKUN__) {
   
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

备注

什么是运行时的 publicPath ?
运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。

步骤二:修改入口文件

主要改动:

  1. 引入 public-path.js
  2. export 生命周期函数
    • 将子应用路由的创建、实例的创建渲染挂载到mount函数上
    • 将实例的销毁挂载到unmount
// vue 2

// 入口文件 `main.js` 修改,为了避免根 id `#app` 与其他的 DOM 冲突,需要限制查找范围。

import './public-path';
import Vue from 'vue';
import App from './App.vue';
import VueRouter from 'vue-router';
import routes from './router';
import store from './store';

Vue.config.productionTip = false;

let router = null;
let instance = null;

function render(props = {
    }) {
   
  const {
    container } = props;
  router = new VueRouter({
   
    // histroy模式的路由需要设置base。app-vue 根据项目名称来定
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
    mode: 'history',
    // hash模式则不需要设置base
    routes,
  });

  instance = new Vue({
   
    router,
    store,
    render: (h) => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
   
  render();
}

export async function bootstrap() {
   
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
   
  console.log('[vue] props from main framework', props);
  render(props);
}

export async function unmount() {
   
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}
// react 16

// 以 `create react app` 生成的 `react 16` 项目为例,搭配 `react-router-dom` 5.x。
// 入口文件 `index.js` 修改,为了避免根 id `#root` 与其他的 DOM 冲突,需要限制查找范围。
// 这里需要特别注意的是,通过 ReactDOM.render 挂载子应用时,需要保证每次子应用加载都应使用一个新的路由实例。

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

function render(props) {
   
  const {
    container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}

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

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

export async function mount(props) {
   
  console.log('[react16] props from main framework', props);
  render(props);
}

export async function unmount(props) {
   
  const {
    container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}

备注

  • qiankun 基于 single-spa,所以你可以在这里找到更多关于微应用生命周期相关的文档说明。
  • 无 webpack 等构建工具的应用接入方式请见这里

注意点

  • 容器名

    • 修改 index.html 中项目初始化容器的根 id,不要使用 #app#root,避免与其他的项目冲突,建议换成 项目name 的驼峰写法
  • 微应用建议使用 history 路由模式。

  • 路由模式为 history 时,同时需要设置路由 base,值和它的 activeRule 是一样的:

    • vue:

      router = new VueRouter({
              
        mode: 'history',
        base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
      });
      
    • react:

      <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
      

3、构建工具配置

微应用分为有 webpack 构建和无 webpack 构建项目。

注意
qiankun v2 及以下版本只支持 webpack,并不支持 vite!!!

下面以 webpack 为构建工具的微应用为例(主要是指 Vue、React)的配置说明

配置点说明

构建工具的配置主要有两个:

  1. 允许跨域
  2. 打包成 umd 格式

为什么要打包成 umd 格式呢?是为了让 qiankun 拿到其 export 的生命周期函数。我们可以看下其打包后的 app.js 就知道了:
在这里插入图片描述

配置示例

微应用的打包工具需要增加如下配置:

react

修改 webpack 配置

安装插件 @rescripts/cli,当然也可以选择其他的插件,例如 react-app-rewired

npm i -D @rescripts/cli

根目录新增 .rescriptsrc.js

// webpack

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

module.exports = {
   
  webpack: (config) => {
   
    config.output.library = `${
     name}-[name]`;
    config.output.libraryTarget = 'umd';
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    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;
  },
};

修改 package.json

-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",
-   "test": "react-scripts test",
+   "test": "rescripts test",
-   "eject": "react-scripts eject"
vue
// vue.config.js

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

module.exports = {
   
  devServer: {
   
    headers: {
   
      'Access-Control-Allow-Credentials': true,
      'Access-Control-Allow-Origin': req.headers.origin || '*',
      'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
      'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
      'Content-Type': 'application/json; charset=utf-8',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
   
    output: {
   
      library: `${
     name}-[name]`, // 微应用的包名,这里与主应用中注册的微应用名称一致
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式,可以在 AMD 或 CommonJS 的 require 访问。
      // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
      jsonpFunction: `webpackJsonp_${
     name}`, // webpack 用来异步加载 chunk 的 JSONP 函数。
    },
  },
};

注意

这个 name 默认从 package.json 获取,可以自定义,只要和父项目注册时的 name 保持一致即可。qiankun 拿这三个生命周期,是根据注册应用时,你给的 name 值,name 不一致则会导致拿不到生命周期函数。

webpack 不同版本的配置

webpack v5:

const packageName = require('./package.json').name;

module.exports = {
   
  output: {
   
    library: `${
     packageName}-[name]`,
    libraryTarget: 'umd',
    chunkLoadingGlobal: `webpackJsonp_${
     packageName}`,
  },
};

webpack v4:

const packageName = require('./package.json').name;

module.exports = {
   
  output: {
   
    library: `${
     packageName}-[name]`,
    libraryTarget: 'umd',
    // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
    jsonpFunction: `webpackJsonp_${
     packageName}`,
  },
};

备注

更多相关配置介绍可以查看 webpack 相关文档





子项目开发的一些注意事项

js 相关
给 body 、 document 等绑定的事件,请在 unmount 周期清除

js 沙箱只劫持了 window.addEventListener,而使用了 document.body.addEventListener 或者 document.body.onClick 添加的事件并不会被沙箱移除,会对其他的页面产生影响,因此请在 unmount 生命周期清除绑定的事件。


css 相关
避免 css 污染

组件内样式的 css-scoped 是必须的。

对于一些插入到 body 的弹窗,无法使用 scoped,请不要直接使用原 class 修改样式,请添加自己的 class,来修改样式。

.el-dialog {
   
  /* 不推荐使用组件原有的class */
}

.my-el-dialog {
   
  /* 推荐使用自定义组件的class */
}
谨慎使用 position:fixed

在父项目中,浮动定位未必准确,应尽量避免使用,确有相对于浏览器窗口定位需求,可以用 position: sticky,但是会有兼容性问题(IE 不支持)。如果定位使用的是 bottom 和 right,则问题不大。

还有个办法,位置可以写成动态绑定 style 的形式:

<div :style="{ top: isQiankun ? '10px' : '0'}"></div>

静态资源 相关
所有的资源(图片/音视频等)都应该放到 src 目录下,不要放在 public 或者 static

资源放 src 目录,会经过 webpack 处理,能统一注入 publicPath。否则在主项目中会 404。

参考:vue-cli3 的官方文档介绍:何时使用-public-文件夹


第三方库 相关
请给 axios 实例添加拦截器,而不是 axios 对象

后续会考虑子项目共享公共插件,这时就需要避免公共插件的污染

// 正确做法:给 axios 实例添加拦截器
const instance = axios.create();
instance.interceptors.request.use(function () {
   
  // ...
});

// 错误用法:直接给 axios 对象添加拦截器
axios.interceptors.request.use(function () {
   
  // ...
});




qiankun 功能介绍

运行模式

乾坤只有一种运行模式:单例模式

在微前端框架中,子应用会随着主应用页面的打开和关闭反复的激活和销毁(单例模式:生命周期模式)。

单例模式

子应用页面如果切走,会调用 unmount 销毁子应用当前实例,子应用页面如果切换回来,会调用 mount 渲染子应用新的实例。

在单例式下,改变 url 子应用的路由会发生跳转到对应路由。



生命周期

微应用需要在应用的入口文件导出 bootstrapmountunmountupdate 四个生命周期钩子,以供主应用在适当的时机调用。

注意

💡 qiankun 生命周期函数都必须要 export 暴露
💡 qiankun 生命周期函数都必须是 Promise函数,使用 async 会返回一个 Promise 对象

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
   
  console.log('app bootstraped');
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
   
  // ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
  console.log('app mount');
}

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

/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
   
  console.log('update props', props);
}


通讯方式

项目之间的不要有太多的数据依赖,毕竟项目还是要独立运行的。通信操作需要判断是否 qiankun 模式,做兼容处理。

props 通信

如果父子应用都是vue项目,通过 props 传递父项目的 Vuex store,可以实现响应式;但假如子项目是不同的技术栈(jQuery/react/angular),这是就不能很好的监听到数据的变化了。

注意

vue 项目之间数据传递还是使用共享父组件的 Vuex 比较方便,与其他技术栈的项目之间的通信使用 qiankun 提供的 initGlobalState

通过 vuex/pinia 实现通信

第一步:在主应用中创建一个 store

// src/store/index.ts

const personModule = {
   
  state: {
   
    id: 0,
    name: '',
    age: 0,
  },
  mutations: {
   
    setId(state: Object, id: number) {
   
      state.id = id;
    },
    setName(state: Object, name: string) {
   
      state.name = name;
    },
    setAge(state: Object, age: number) {
   
      state.age = age;
    },
  },
  actions: {
   
    setId(context: Object, id: number) {
   
      context.commit('setId', id);
    },
    setName(context: Object, name: string) {
   
      context.commit('setName', name);
    },
    setAge(context: Object, 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值