微前端二: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响应时,匹配子应用路由并加载渲染子应用。

基座配置

//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标签的方式挂载子应用

子应用配置

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打包方式

//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、资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

基座配置

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('第一个微应用已挂在,可以后续操作');
});

子应用配置

// 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 库格式

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 加载微应用的方式除了上面那种外,如果微应用不是直接跟路由关联的时候,也可以选择手动加载微应用的方式

import { loadMicroApp } from 'qiankun';

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

生命周期钩子注释说明

/**
 * 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 可以触发所有的 观察者 函数,从而达到组件间通信的效果。

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

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

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 注册文件

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 传给了子应用。
子应用:

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 实例大概是这样:

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;

同样的传入方式

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();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值