夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 微前端

    • 微前端
      • 什么是微前端
      • 关于 Iframe
      • 各大框架
        • QianKun
        • wujie
        • Micro App
      • 其他
        • 联邦模块
        • 微前端实践
  • 权限控制

  • monorepo

  • 《前端架构》
  • 微前端
夜猫子
2024-06-18
目录

微前端

# 什么是微前端

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构特点:

  • 无关技术栈
  • 开发、仓库、部署独立
  • 允许增量升级、渐进式重构
  • 微应用之间状态隔离,运行时状态不共享

# 关于 Iframe

如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步,浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. dom 结构不共享,想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

更多关于微前端的相关介绍,推荐大家可以去看这几篇文章:

  • Micro Frontends (opens new window)
  • Micro Frontends from martinfowler.com (opens new window)

# 各大框架

# QianKun

QianKun (opens new window) 基于 single-spa ,阿里系开源的微前端框架,应该也是大家接触最多的了,社区比较活跃,这点比较重要。

# 实现方案

  • single-spa是基于js-entry方案,而qiankun 是基于html-entry 及沙箱设计,使得微应用的接入像使用 iframe 一样简单。

  • 主应用监听路由,加载对应子应用的html,挂载到主应用的元素内,然后解析子应用的html,从中分析出css、js再去沙盒化后加载执行,最终将子应用的内容渲染出来。

  • qiankun实现样式隔离有两种模式可供开发者选择:

    • trictStyleIsolation:这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响
    • experimentalStyleIsolation: 当 experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子应用所添加的样式,会为所有样式规则增加一个特殊的选择器规则,来限定其影响范围
  • qiankun 实现js 隔离,采用了两种沙箱,分别为基于Proxy实现的沙箱和快照沙箱,当浏览器不支持Proxy会降级为快照沙箱

流程

1.主应用注册微应用

2.URL 发生变化时匹配激活规则,然后加载对应的微应用

3.获取微应用的入口 HTML 内容(通过 fetch 请求)

4.解析 HTML,获取脚本、样式等资源

5.创建沙箱环境(JS 沙箱和样式沙箱)

6.沙箱环境中执行微应用的 JavaScript 代码,并调用微应用暴露的生命周期函数(bootstrap, mount, unmount)

7.通信通过全局状态 / 自定义事件实现

qiankun 通过 fetch 请求获取子应用资源,通过 import-html-entry 库提取出 html、css 和 js,在 默认直接创建容器通过 scoped 管理样式或选择在 shadow dom 中应用 html 和 css ,通过快照或者 Proxy 沙箱隔离 js 操作

Html Entry 机制

async function fetchSubAppHTML(entry) {
  const html = await fetch(entry).then(res => res.text());
  return html;
}

// 2. 解析 HTML,执行 JS,应用 CSS
function processHTML(html, app) {
  const template = document.createElement('div');
  template.innerHTML = html;
  
  // 提取和处理所有资源
  extractScripts(template, app);
  extractStyles(template, app);
  extractHTMLContent(template, app);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Proxy沙箱机制:

  1. 创建一个假的全局对象(fakeWindow)作为代理的目标对象。
  2. 用 Proxy 拦截 fakeWindow 上的操作,包括 get、set、has 等。
  3. 替换全局执行环境,将子应用的代码包裹在一个函数中,然后通过call方法将 fakeWindow 作为this传入,同时将 fakeWindow 作为参数传入。这样,在子应用代码中,如果使用this或者window(参数名)来访问全局对象,就会访问到 fakeWindow
  4. 当子应用修改全局变量时,实际上修改的是 fakeWindow,不会影响主应用的全局变量。
  5. 当子应用读取全局变量时,优先从 fakeWindow 上读取,如果 fakeWindow 上没有,则从主应用的全局对象(window)上读取。
// 伪代码
class ProxySandbox {
    constructor() {
        const rawWindow = window;
        const fakeWindow = {}
        const proxy = new Proxy(fakeWindow, {
            set(target, p, value) {
                target[p] = value;
                return true
            },
            get(target, p) {
                return target[p] || rawWindow[p];
            }
        });
        this.proxy = proxy
    }
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
// 伪代码
((window) => {
    window.a = 'hello';
    console.log(window.a) // hello
})(sandbox1.proxy);
((window) => {
    window.a = 'world';
    console.log(window.a) // world
})(sandbox2.proxy);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

快照沙箱:

// 伪代码
class SnapshotSandbox {
    constructor() {
        this.proxy = window; 
        this.modifyPropsMap = {}; // 修改了那些属性
        this.active(); // 调用active保存主应用window快照
    }

    /**1. 初始化时,在子应用即将mount前,先调用active,保存当前主应用的window快照*/
    active() {
        this.windowSnapshot = {}; // window对象的快照
        for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
                // 将window上的属性进行拍照
                this.windowSnapshot[prop] = window[prop];
            }
        }
        Object.keys(this.modifyPropsMap).forEach(p => {
            window[p] = this.modifyPropsMap[p];
        });
    }

    /**
    * 子应用卸载时,遍历当前子应用的window属性,和主应用的window快照做对比
    * 如果不一致,做两步操作 
    *     1. 保存 不一致的window属性,
    *     2. 还原window
    */
    inactive() {
        for (const prop in window) { // diff 差异
            if (window.hasOwnProperty(prop)) {
                // 将上次拍照的结果和本次window属性做对比
                if (window[prop] !== this.windowSnapshot[prop]) {
                    // 保存修改后的结果
                    this.modifyPropsMap[prop] = window[prop]; 
                    // 还原window
                    window[prop] = this.windowSnapshot[prop]; 
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
如和改变子应用全局作用域
// qiankun 内部大致实现思路
function execScript(scriptText, sandbox) {
  const { fakeWindow } = sandbox;
  
  // 使用 Function 构造器,将 window 替换为 fakeWindow
  const scriptRunner = new Function('window', 'self', scriptText);
  scriptRunner.call(fakeWindow, fakeWindow, fakeWindow);
}
1
2
3
4
5
6
7
8

使用 Function 构造器动态创建一个函数:

  • 第一个参数 'window' 是新函数的第一个形参名
  • 第二个参数 'self' 是新函数的第二个形参名
  • scriptText 是函数体(要执行的子应用代码)
  • 这相当于创建了一个这样的函数:
function scriptRunner(window, self) {
  // scriptText 的内容
}
1
2
3

调用刚创建的函数:

  • call() 方法的第一个参数 fakeWindow 是 this 绑定值
  • 后面的两个 fakeWindow 参数分别传递给函数的第一个和第二个形参(即 window 和 self)

工作原理详解

  1. JavaScript 作用域欺骗

当子应用中有如下代码时:

window.myVariable = 'hello';
console.log(window.myVariable);
1
2

由于我们通过 new Function('window', scriptText) 创建函数,上面的代码实际上变成了:

function scriptRunner(window) {
  window.myVariable = 'hello';
  console.log(window.myVariable);
}
1
2
3
4

当我们调用 scriptRunner.call(null, fakeWindow) 时,就相当于:

function scriptRunner(fakeWindow) {
  fakeWindow.myVariable = 'hello';
  console.log(fakeWindow.myVariable);
}
1
2
3
4
  1. this 指向控制

通过 call(fakeWindow, ...) 确保了在子应用代码中直接使用 this 时也指向 fakeWindow:

// 子应用中的代码
console.log(this === window); // true,在沙箱环境中都指向 fakeWindow
1
2
  1. 完整的转换示例

假设子应用有这样一段代码:

// 子应用代码 (scriptText)
var appName = 'sub-app';
window.version = '1.0';
self.description = 'This is sub app';
console.log(this.appName);
1
2
3
4
5

经过 execScript 处理后,实际执行效果类似于:

// 转换后的执行环境
(function(window, self) {
  var appName = 'sub-app';
  window.version = '1.0';
  self.description = 'This is sub app';
  console.log(this.appName);
}).call(fakeWindow, fakeWindow, fakeWindow);
1
2
3
4
5
6
7

这样,子应用中所有的:

  • window.xxx 操作都变成对 fakeWindow 的操作
  • self.xxx 操作也都变成对 fakeWindow 的操作
  • 直接的 this 引用也指向 fakeWindow

# 优点

  • html entry的接入方式,不需要自己写load方法,而是直接写子应用的访问链接就可以。

  • 提供js沙箱

  • 提供样式隔离,两种方式可选 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。

  • 社区活跃 umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统,除了最后一点拓展以外,微前端想要达到的效果都已经达到。

  • 应用间通信简单,全局注入路由保持,浏览器刷新、前进、后退,都可以作用到子应用

  • 路由保持,浏览器刷新、前进、后退,都可以作用到子应用

# 缺点

  • 改造成本较大,从 webpack、代码、路由等等都要做一系列的适配

  • 对 eval 的争议,eval函数的安全和性能是有一些争议的:MDN的eval介绍;

  • 无法同时激活多个子应用,也不支持子应用保活

  • 无法支持 vite 等 ESM 脚本运行,3.0 版本预计支持

# wujie

无界是腾讯推出的一款微前端解决方式。它是一种基于 Web Components + iframe 的全新微前端方案,继承iframe的优点,补足 iframe 的缺点,让 iframe 焕发新生。

# 实现方案

wujie跟qiankun一样,都是基于html entry加载的,但他们解析html的过程是不一样的。 qiankun是直接解析并执行js、css、html的,而wujie则是先解析html,提取出script脚本放入空的iframe中,提取出css、html放入到web components中,具体来说:

  1. 解析入口 HTML ,分别得到script、css、模版html
  2. 创建一个纯净的 iframe,为了实现应用间(iframe 间)通讯,无界子应用 iframe 的 url 会设置为主应用的域名(同域),因此 iframe 的 location.href 并不是子应用的 url。创建好后停止加载iframe
  3. iframe内插入js,将抽离出来的script脚本,插到iframe中去,在iframe中执行子应用的js
  4. 创建web component,id为子应用id,将抽离出来的html插入
  5. 由于iframe内的js有可能操作dom,但是iframe内没有dom,随意wujie框架内对iframe拦截document对象,统一将dom指向shadowRoot,此时比如新建元素、弹窗或者冒泡组件就可以正常约束在shadowRoot内部

wujie 会为每一个子应用创建一个同源的 iframe 以方便主应用修改其内容,也就是说一个子应用对应一个iframe,但这个iframe是隐藏的,会把子应用地址获取到的资源提取的样式和元素(处理过)插入这个 iframe 中(也就是说 iframe 并不是直接渲染子应用的地址)并执行提取的 js,然后把元素和样式挂载到 Shadow DOM 上,创建一个代理对象,替换掉iframe中的 document 以代理 iframe 中的 dom 操作,执行 dom 操作时变为操作 Shadow DOM,然后通过 postMessage 进行跨域通信和数据同步。

简化的代理操作
// 在主应用中
const shadowRoot = document.querySelector('wujie-app').shadowRoot;

// 在子应用的 iframe 中
const originalDocument = iframe.contentWindow.document;

// 重写 createElement
originalDocument.createElement = function(tagName) {
  // 在 Shadow DOM 中创建真实元素
  const element = shadowRoot.createElement(tagName);

  // 返回一个代理对象,拦截对元素的操作
  return new Proxy(element, {
    set(target, key, value) {
      // 如果设置的是样式,需要映射到真实元素上
      if (key === 'style') {
        for (let styleKey in value) {
          target.style[styleKey] = value[styleKey];
        }
      } else if (key.startsWith('on')) {
        // 事件处理,例如 onclick、onchange 等
        const eventName = key.slice(2);
        target.addEventListener(eventName, value);
      } else {
        target[key] = value;
      }
      return true;
    },
    get(target, key) {
      // 如果访问的是 appendChild 等方法,需要重写这些方法以在 Shadow DOM 中操作
      if (key === 'appendChild') {
        return function(child) {
          // 如果 child 是一个代理,那么需要获取其真实元素
          const realChild = child instanceof Proxy ? child : child;
          return shadowRoot.appendChild(realChild);
        };
      }
      // 其他属性直接返回
      return target[key];
    }
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 优点

  • 接入简单,可以以组件的方式引入子应用

  • 纯净无污染

    • 无界利用iframe和webcomponent来搭建天然的js隔离沙箱和css隔离沙箱
    • 利用iframe的history和主应用的history在同一个top-level browsing context来搭建天然的路由同步机制
    • 副作用局限在沙箱内部,子应用切换无需任何清理工作,没有额外的切换成本
  • 支持vite esmoudle加载,由于js是独立在iframe中加载的,所以支持esmodule加载

  • 支持预加载

  • 支持应用保活,子应用状态保留,由于是独立在iframe中的,而切换应用时不会移除iframe,所以子应用的状态会被保留在原来的iframe中,当主应用再次渲染子应用dom时,会显示之前的状态。

  • 多应用同时激活在线

# 缺点

iframe沙箱的src设置了主应用的host,初始化iframe的时候需要等待iframe的location.orign从’about:blank’初始化为主应用的host,这个采用的计时器去等待的不是很优雅

# Micro App

Micro App (opens new window) 是京东出的一款基于 Web Component 原生组件进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。

采用iframe沙箱模式,这点和wujie的方案一样了,都是webComponent + iframe

# 实现方案

首先micro-app实现了一个基于WebComponent的组件,并实现了类Shadow Dom 的效果,开发者只需要用来加载子应用,整个对子应用的加载、js隔离、css隔离的逻辑都封装在了web component组件中,具体来说:

  1. 当调用microApp.start()后,会注册一个名为micro-app 的自定义 webComponent 标签。我们可以从 中拿到子应用的线上入口地址。
  2. 组件内部,当匹配到路由后,跟qiankun一样加载html,得到html字符串模版
  3. 分析html字符串,提取头和,并替换为框架自定义标签和
  4. 在<micro-app-head>内,会对script标签和link标签的内容进行加载并执行
  5. 将 <micro-app-head> 和 <micro-app-body> 插入到 <micro-app> 标签内
  6. <micro-app> 内提供了js沙箱方法(v1.0以前跟qiankun沙箱一样), <micro-app-head>挂载到 <micro-app> 后,内部会逐一对 <micro-app-head> 内的 script 标签的 js 绑定作用域,实现 js 隔离

# 优点

  • 接入简单,组件式引入子应用

  • 团队持续更新维护

  • js隔离、css隔离、路由同步

  • 支持子应用保活, 需要开启keep-alive模式 支持fiber模式,提升主应用的渲染性能。

# 缺点

  • 1.0之前不支持vite,1.0之后支持了

  • 默认css隔离方式,主应用的样式还是会污染到子应用。

  • 子应用和主应用必须相同的路由模式,要么同时hash模式,要么同时history模式

  • 依赖于CustomElements和Proxy两个较新的API。Proxy暂时没有做兼容,所以对于不支持Proxy的浏览器无法运行micro-app。

个人是比较看好 WebComponent 化的,侵入较低,接入更流畅。

它是由google推出的浏览器的原生组件,具体内容查看 链接 (opens new window) 了解

  • 复用性:不需要对外抛出加载和卸载的全局 API,可复用能力更强
  • 标准化:W3C 的标准,未来能力会得到持续升级
  • 插拔性:可以非常便捷的进行移植和组件替换

笔记

切记不要以为微前端而去微前端,老旧项目带来的迁移成本,不同项目技术栈的兼容与边界问题处理,因为没有迫切的需求而接入微前端,只会带来额外的负担,很多时候,iframe 其实就很够用了。

# 其他

# 联邦模块 (opens new window)

# 微前端实践 (opens new window)

编辑 (opens new window)
上次更新: 2025/10/27 10:53:52
前端实现权限控制

前端实现权限控制→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
IoC 解决了什么痛点问题?
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式