微前端二:qiankun

qiankun是基于Single-spa开发的框架,所以我们先来看下Single-spa是怎么做的:

Single-spa 是最早的微前端框架,兼容多种前端技术栈,是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架;

优点:

1、敏捷性 - 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;

2、技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权;

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

4、更快交付客户价值,有助于持续集成、持续部署以及持续交付;

5、维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域;

缺点:

1、无通信机制

2、不支持 Javascript 沙箱

3、样式冲突

4、无法预加载

Single-spa实现原理

首先在基座应用中注册所有App的路由,single-spa保存各子应用的路由映射关系,充当微前端控制器Controler。URL响应时,匹配子应用路由并加载渲染子应用。

基座配置

csharp 复制代码
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

const mountApp = (url) => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url

    script.onload = resolve
    script.onerror = reject

    // 通过插入script标签的方式挂载子应用
    const firstScript = document.getElementsByTagName('script')[]
    // 挂载子应用
    firstScript.parentNode.insertBefore(script, firstScript)
  })
}

const loadApp = (appRouter, appName) => {

  // 远程加载子应用
  return async () => {
    //手动挂载子应用
    await mountApp(appRouter + '/js/chunk-vendors.js')
    await mountApp(appRouter + '/js/app.js')
    // 获取子应用生命周期函数
    return window[appName]
  }
}

// 子应用列表
const appList = [
  {
    // 子应用名称
    name: 'app1',
    // 挂载子应用
    app: loadApp('http://localhost:8083', 'app1'),
    // 匹配该子路由的条件
    activeWhen: location => location.pathname.startsWith('/app1'),
    // 传递给子应用的对象
    customProps: {}
  },
  {
    name: 'app2',
    app: loadApp('http://localhost:8082', 'app2'),
    activeWhen: location => location.pathname.startsWith('/app2'),
    customProps: {}
  }
]

// 注册子应用
appList.map(item => {
  registerApplication(item)
})
 
// 注册路由并启动基座
new Vue({
  router,
  mounted() {
    start()
  },
  render: h => h(App)
}).$mount('#app')

构建基座的核心是:配置子应用信息,通过registerApplication注册子应用,在基座工程挂载阶段start启动基座。

我们通过代码也发现 Single-spa 是通过插入script标签的方式挂载子应用

子应用配置

csharp 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = {
  el: '#microApp',
  router,
  render: h => h(App)
}

// 支持应用独立运行、部署,不依赖于基座应用
// 如果不是微应用环境,即启动自身挂载的方式
if (!process.env.isMicro) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}
// 基于基座应用,导出生命周期函数
const appLifecycle = singleSpaVue({
  Vue,
  appOptions
})

// 抛出子应用生命周期
// 启动生命周期函数
export const bootstrap = (props)  => {
  console.log('app2 bootstrap')
  return appLifecycle.bootstrap(() => { })
}
// 挂载生命周期函数
export const mount = (props) => {
  console.log('app2 mount')
  return appLifecycle.mount(() => { })
}
// 卸载生命周期函数
export const unmount = (props) => {
  console.log('app2 unmount')
  return appLifecycle.unmount(() => { })
}

配置子应用为umd打包方式

csharp 复制代码
//vue.config.js
const package = require('./package.json')
module.exports = {
  // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
  publicPath: '//localhost:8082',
  // 开发服务器
  devServer: {
    port: 8082
  },
  configureWebpack: {
    // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个
    // 全局对象获取一些信息,比如子应用导出的生命周期函数
    output: {
      // library的值在所有子应用中需要
      library: package.name,
      libraryTarget: 'umd'
    }
  }

子应用配置的核心是用singleSpaVue生成子路由配置后,必须要抛出其生命周期函数。

用以上方式便可轻松实现一个简单的微前端应用了。

那么我们有single-spa这种微前端解决方案,qiankun 又在此基础上做了什么呢

相比于single-spa,qiankun他解决了JS沙盒环境,不需要我们自己去进行处理。在single-spa的开发过程中,我们需要自己手动的去写调用子应用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你传入响应的apps的配置即可,会帮助我们去加载。

Qiankun

1、基于 single-spa 封装,提供了更加开箱即用的 API。

2、技术栈无关,任意技术栈的应用均可 使用/接入,不论是 React/Vue/Angular/JQuery 还是其他等框架。

3、HTML Entry 接入方式,让你接入微应用像使用 iframe 一样简单。

4、样式隔离,确保微应用之间样式互相不干扰。

5、JS 沙箱,确保微应用之间 全局变量/事件 不冲突。

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

基座配置

csharp 复制代码
import { registerMicroApps, start, runAfterFirstMounted, setDefaultMountApp } from 'qiankun';

const microApps = [
	{
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
    loader: (loading: boolean) => void - 可选,loading 状态发生变化时会调用的方法。
	props: {} - 可选,主应用需要传递给微应用的数据。
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  }
]
/**
	beforeLoad - Lifecycle | Array<Lifecycle> - 可选
	beforeMount - Lifecycle | Array<Lifecycle> - 可选
	afterMount - Lifecycle | Array<Lifecycle> - 可选
	beforeUnmount - Lifecycle | Array<Lifecycle> - 可选
	afterUnmount - Lifecycle | Array<Lifecycle> - 可选
*/
registerMicroApps(
  microApps, 
  {
     beforeLoad: (app) => console.log('before load', app.name),
     beforeMount: [(app) => console.log('before mount', app.name)],
  }
);
// 启动 qiankun
start();
// 设置主应用启动后默认进入的微应用。
setDefaultMountApp('/app-react');
// 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
runAfterFirstMounted(() => {
  console.log('第一个微应用已挂在,可以后续操作');
});

子应用配置

csharp 复制代码
// main.ts
function render(props: any = {}) {
  if (Object.keys(props).length === 0) return;
  const { userInfo } = props;
  store.commit('SetGlobalObj', props);
  store.commit('SetUserInfo', userInfo);
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

let win:any = window;
// 判断是否是qiankun环境,兼容非微前端环境
if (!win.__POWERED_BY_QIANKUN__) {
  const router = getRoute('/dashboard/detail/information');
  new Vue({
    el: "#app",
    router,
    store,
    render: h => h(App)
  });
}

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

export async function mount(props: any) {
  render(props);
}

export async function unmount() {
}

修改 webpack 配置

这个跟Single-spa 差不多,主要是 把微应用打包成 umd 库格式

csharp 复制代码
const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      // 把微应用打包成 umd 库格式
      libraryTarget: 'umd', 
      // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
      jsonpFunction: `webpackJsonp_${name}`
    },
  },
};

这里特别说明下 qiankun 加载微应用的方式除了上面那种外,如果微应用不是直接跟路由关联的时候,也可以选择手动加载微应用的方式

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

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

生命周期钩子注释说明

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

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

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

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

qiankun的通信方式

1、localStorage/sessionStorage

2、通过路由参数共享

3、官方提供的 props

4、官方提供的 actions

5、使用vuex或redux管理状态,通过shared分享

1、localStorage/sessionStorage

有人说这个方案必须主应用和子应用是同一个域名下。其实不是的,子应用使用不同的域名也是可以的,因为在 qiankun 中,主应用是通过 fetch 来拉取子应用的模板,然后渲染在主应用的 dom 上的,说白了还是运行在主应用上,所以还是运行在同一个域名上,也就是主应用的域名。

父传子

主应用 main.js

localStorage.setItem('token', '123')

console.log('在main中设置了token')

子应用app1 main.js

const token = localStorage.getItem('token')

console.log('app1中打印token:', token)

子传父

同理app1修改token,main也可以看到,这里就不再赘述

2、通过路由参数共享

这个也很好理解,因为只有一个 url,不管子应用还是主应用给 url 上拼接一些参数,那么父子应用都可以通过 route 来获取到。

3、官方提供的 props

这个在前面已经说过了就是registerMicroApps注册微应用时,通过props传送信息

4、官方提供的 actions

就一个 API initGlobalState

qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
setGlobalState :设置 globalState - 设置新的值时,内部将执行 浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的 观察者 函数。
onGlobalStateChange :注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
offGlobalStateChange:取消 观察者 函数 - 该实例不再响应 globalState 变化

我们从上图可以看出,我们可以先注册 观察者 到观察者池中,然后通过修改 globalState 可以触发所有的 观察者 函数,从而达到组件间通信的效果。

下面就是注册了一个观察者

csharp 复制代码
const actions: MicroAppStateActions = initGlobalState({});
actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
});
setTimeout(() => {
  actions.setGlobalState(Object.assign({ username: 'Lee', obj: { token: 222 } }));
}, 1000);
// actions.offGlobalStateChange();

我们来看具体应用:

父传子

主应用:

actions.js

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

const state = {
  num: 1
};

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

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log('主应用检测到state变更:', state, prev);
});

// 你还可以定义一个获取state的方法下发到子应用
actions.getGlobalState = function () {
  return state
}

export default actions;

index.js 注册文件

csharp 复制代码
import {
  registerMicroApps,
  start,
} from "qiankun";
import actions from './actions.js'
const apps = [
  {
    name: "App1MicroApp",
    entry: '//localhost:9001',
    container: "#app1",
    activeRule: "/app1",
    props: {
      parentActions: actions
    }
  }
];

registerMicroApps(apps);
start();

这样就把这个 actions 传给了子应用。

子应用:

csharp 复制代码
let instance = null
let router = null

function render (props) {
  console.log(props.parentActions);
  // 在子应用中使用就可以访问到这个parentActions了
  props.parentActions.setGlobalState({ num: 2 })
  // 调用挂载在 actions 上的自定义方法,获取当前的全局 state
  props.parentActions.getGlobalState();
  router = new VueRouter({
    base: '',
    mode: 'history',
    routes: routes
  })
  new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount('#app')
}

export async function mount(props) {
  render(props);
}

5、shared 方案

就是父应用通过 vuex 或者 redux 正常使用维护一个 state,然后创建一个 shared 实例,这个实例提供对 state 的增删改查,然后通过 props 把这个 shared 实例传给子应用,子应用使用就行。其实和上面挺相似的。

不过可以看出上面4中方案,比较适合各个应用通信比较少的情况,实时上这也是微应用的使用原则-尽量减少他们之间的通信。

但如果我们通过 vuex 或者 redux 正常使用维护一个 state,那可扩展的就多了,也比较适合较多通信的情况。

主应用:

这个 shared 实例大概是这样:

csharp 复制代码
import store from "./store";

class Shared {
  /**
   * 获取 Username
   */
  public getUsername(): string {
    const state = store.getState();
    return state.username || "";
  }

  /**
   * 设置 Username
   */
  public setUsername(token: string): void {
    // 将 token 的值记录在 store 中
    store.dispatch({
      type: "set_username",
      payload: username
    });
  }
}
const shared = new Shared();
export default shared;

同样的传入方式

csharp 复制代码
import {
  registerMicroApps,
  start,
} from "qiankun";
import shared from './shared'
const apps = [
  {
    name: "App1",
    entry: '//localhost:9001',
    container: "#app1",
    activeRule: "/app1",
    props: {
      parentShared: shared
    }
  }
];

registerMicroApps(apps);
start();
相关推荐
Apifox5 分钟前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
-代号952710 分钟前
【JavaScript】十四、轮播图
javascript·css·css3
树上有只程序猿33 分钟前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼1 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
QTX187301 小时前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
黄毛火烧雪下1 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox1 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞1 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行1 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758101 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox