sunilwang

V1

2022/07/26阅读:22主题:绿意

qiankun微前端入门

作者简介

谢宇航:Self-introduction is not defined !

qiankun微前端入门

什么是微前端?

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。

  • 从下图可以看出,微前端的页面布局主要分为三部分header头部(他也是主应用的基座),菜单(切换不同子应用),以及子应用展示区域。我们通过切换切换不同菜单来渲染不同的应用。
未命名文件 (1)
未命名文件 (1)
(页面布局)

微前端具备以下几个特点

  1. 技术栈无关(主框架不限制接入应用的技术栈,微应用具备完全自主权)
  2. 独立开发,独立部署(微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新)
  3. 增量升级(面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略)
  4. 独立运行时状态隔离(每个微应用之间状态隔离,运行时状态不共享)

why not iframe?

​ 提到微前端的解决方案,为什么不用 iframe?这几乎是所有微前端方案第一个会被提到的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的...

​ 虽然iframe 无疑是最简单的方式,还天然支持样式隔离以及全局变量隔离,但是iframe也有让人诟病的几个问题:

  1. url 不同步。浏览器刷新 后iframe url 状态会丢失、后退前进按钮无法在iframe里使用。但是产品又要求你子应用之间跳转返回到上一个子应用里某个页面的url的时候,你是不是心里会凉了一截。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中,这时候你是不是已经感到无比的崩溃。
  3. 全局上下文完全隔离,内存变量不共享iframe 内外系统的通信、数据同步等需求,主应用的 cookietoken 要透传到根域名的不同的子应用中实现免登效果。在之前需求中,子应用接口数据还需要到主应用中解密才可以,导致需要利用post message + 监听做父子之间。虽然可以解决,但是你是不是感到十分的恶心?
  4. 由于iframe还存在同源策略,极大地增加了子应用之间通信的难度。
  5. 。每次子应用切换都是一次浏览器上下文重建、资源重新加载的过程。

qiankun微前端优势

​ 我们开始切入主题,既然说了iframe这么多缺点,那么一定是有很好的方案去解决iframe的痛点。下面我们用一张表格来对比一下qiankuniframe的区别:

qiankun Iframe
数据共享 window hash
事件机制 私有通信机制 PostMessage
访问历史 应用之间统一 应用之间独立
全局作用域 共享 完全隔离
CSS作用域 可共享可独立 完全独立
资源加载 可预加载 快
  • 数据共享方面,qiankun是通过**共享window**来实现,而iframe则很局限,只能在通过hashquery等方式在url上添加数据。
  • 事件机制方面,qiankun封装了一套私有通信机制,主应用和子应用可以通过提供封装好的API来进行双向的数据交互,而iframe 子应用到主应用通信则需要postMessage方式进行。
  • 访问历史方面,qiankun应用之间前进后退的访问栈是统一的,iframe应用之间独立。
  • qiankun可预加载子应用资源,iframe很慢,每次仍然需要加载全部资源。

qiankun实现原理

qiankun是基于single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。

  • qiankun主要作用在主应用层(基座)。应用层基座作为微前端主框架的核心成员,充当调度者的角色,由它来决定在不同的条件下激活不同的子应用。因此主框架的定位则仅仅是:导航路由 + 资源加载框架。它保证应用具备独立开发权的同时,又有将它们整合到一起保证产品完整的流程体验的能力。(下图为应用架构示意图)
未命名文件 (1)
未命名文件 (1)
(应用架构)

他的实现原理主要分为以下几个方面,这里通过梳理原理顺便讲一下对应需要的配置项。这样在应用期间可以很清晰的明白为什么这样配置,并且也可以帮助快速定位问题。

路由系统

​ 例如下面的链路图,当点击导航中的某个子应用链接,这时候主应用通过劫持url change 事件,匹配子路由并加载命中的子应用资源,待加载完之后,子路由接管url change事件。子应用的路由表需要设置base url来保证访问某一子应用期间是一直可以命中当前子应用的。

未命名文件 (1)
(链路图)

APP Entry

​ 主应用与子应用的集成方式为运行时组合。子应用自己构建打包,主应用运行时动态加载子应用资源。这样的方式也更加灵活,并没有采用构建时的组合方式。这样子应用每次发布更新不依赖父应用重新打包发布。达到父子应用解耦的效果。

HTML Entry

​ 刚才讲到路由系统时说道匹配完子路由并加载子应用,那么qiankun加载过程实际上是主框架通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中(如下图所示)。由于子节点是实实在在插入到主应用的,这样实际上实现了共享window的效果。

​ qiankun一大特点就是将html做为入口文件,规避了JavaScript为了支持缓存而根据文件内容动态生成文件名,造成入口文件无法锁定的问题。qiankun将html做为入口,所依赖的 import-html-entry 库。

原理:import-html-entry模块

  1. 传入需要解析的html模板路径(和封装的fetch方法) ->
  2. 解析html文档 ->
  3. 返回被处理后的HTML模板字符串,script, link, style标签及内容提取 ->
  4. 外联样式被替换为内联样式 ->
  5. 通过修改脚本字符串,改变脚本执行时候的window/self/this 的指向 ->
  6. 执行所有的script代码
未命名文件 (1)
未命名文件 (1)
(HTML Entry示意图)

需要注意的几点:

  1. 使用fetch涉及跨域,需要在子应用中设置CORS跨域。

  2. 需要配置public path解决微应用动态载入的 脚本、样式、图片 等地址不正确的问题。

JS隔离

​ qiankun的JS沙箱是基于 Proxy 实现代理了 window 上常用的常量和方法以及不支持 Proxy 时降级通过快照实现备份还原。

原理:

  1. 模拟 ES6 的 Proxy API,通过代理劫持 window ->
  2. 当子应用修改或使用 window 上的属性或方法时,把对应的操作记录下来 ->
  3. 每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复
  4. 通过qiankun提供的生命周期钩子,在bootstrap 及 mount钩子下记录快照;unmount回滚,remount恢复。

CSS隔离

​ 由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。解决方案有以下几种:

1. Shadow DOM

基于 Web Components 的 Shadow DOM 能力,子应用的样式作用域仅在 shadow 元素下,我们可以将每个子应用包裹到一个 Shadow DOM 中,保证其运行时的样式的绝对隔离。

​ 但是shadow DOM的缺点也很明显。比如 sub-app 里调用了 antd modal 组件,由于 modal 是动态挂载到 document.body 的,而由于 Shadow DOM 的特性 antd 的样式只会在 shadow 这个作用域下生效,结果就是弹出框无法应用到 antd 的样式。解决的办法是把 antd 样式上浮一层,丢到主文档里,但这么做意味着子应用的样式直接泄露到主文档了。

2. CSS Module

通过约定 css 前缀的方式来避免样式冲突,即各个子应用使用特定的前缀来命名 class,或者直接基于 css module 方案写样式。

​ 通常可以给主应用样式都加前缀,因为主应用内容相对较少,加起来很方便。这也适用于全新的项目。但是对于子应用中使用了三方的组件库,三方库在写入了大量的全局样式的同时又不支持定制化前缀的时候又很难去解决。

3. Dynamic Stylesheet

配合HTML Entry,只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样就可以保证,在一个时间点里,只有一个应用的样式表是生效的。这也是qiankun默认选择的css 隔离方式。

qiankun使用教程

1. 安装qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
2. 在主应用中注册微应用
import { registerMicroApps, loadMicroApp, start } from 'qiankun';

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',
  },
]);

// 如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

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

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

/**
 * 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);
}
4. 配置微应用的打包工具

webpack:

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

module.exports = {
  output: {
    library`${packageName}-[name]`,
    libraryTarget'umd',
    jsonpFunction`webpackJsonp_${packageName}`,
  },
};
5. 父子应用通信
// 父应用
import { initGlobalState } from 'qiankun';

// 定义全局状态,并返回通信方法
const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user'qiankun',
});

// 监听全局状态
onGlobalStateChange((value, prev) => 
  // // value: 变更后的状态; prev 变更前的状态
  console.log('[onGlobalStateChange - master]:', value, prev
));

// 修改全局状态(触发监听事件)
setGlobalState({
  ignore'master',
  user: {
    name'master',
  },
});
// 子应用
// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props{
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log(state, prev);
  });

  props.setGlobalState(state);
}

demo 如下视频

image
image

qiankun应用场景

  1. 需要对大型工程进行拆分解耦

  2. 较低成本的应用改造

  3. 在老项目中使用新的前端技术栈

  4. 对产品体验有高要求

  5. 开发无感知


LBG开源项目推广:

还在手写 HTML 和 CSS 吗?
还在写布局吗?
快用 Picasso 吧,Picasso 一键生成高可用的前端代码,让你有更多的时间去沉淀和成长,欢迎Star

开源项目地址:https://github.com/wuba/Picasso
官网地址:https://picassoui.58.com

分类:

前端

标签:

前端

作者介绍

sunilwang
V1