普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月10日首页

歼20居然是个框架-基于 Signals 信号的前端框架设计

作者 anuoua
2025年12月9日 09:31

logo

大家好,我是 anuoua,今天我们来讲讲基于 Signal 如何构建一个前端框架。

以 Vue 为响应式前端框架的代表,以 React 则是非响应式前端框架的代表,算是目前前端框架的稳定格局。

响应式的优点不言而喻,是高性能前端框架的选择。

而响应式也有不同的设计理念,区别于 Vue 的 reactivity,preact 的作者提出了 Signal 这种响应式的理念,和深度劫持的 reactivity 不同,Signal 更简单直观,其理念传播广泛,目前 Signal 作为 js 语言特性被提出成为 proposal。

响应式前端框架的现状

目前一些具有代表性的前端框架,基本都走向了响应式 API + 真实 DOM,例如:svelte、solid、vue,这几个前端框架在性能上有了大幅提升,但是仍然存在一些问题。

Vue 3

Vue 作为响应式框架的开创者,Vue3 仍然是虚拟 DOM,而 Vue 3 vapor 转向真实 DOM。Vue 3 版本中遇到最严重的问题是自动解包、**解构以及类型,**为了解决这些问题作者试验过很多语法,最终在数个的迭代后,还是上了编译手段,在SFC中使用宏用来解决开发体验以及 Typescript 类型问题。

<script setup>
const props = defineProps({
  foo: String
})
</script>

除此之外,Vue 的问题就在于官方没有引导用户到理想的开发模式上去,组件写法太多,导致社区力量分散,发力不在一处。如果统一使用 SFC 开发,统一使用 composition api,那么社区就不会陷入使用 jsx 还是 SFC,使用 options 还是 composition api 的纠结,那么社区的生态会好很多。

Svelte

Svelte 借助编译手段将视图转换成真实DOM实现,在 Svelte 5 中转向了和 Vue 类似的深度劫持的响应式API。它设计了一种叫 runes 的概念,通过编译技术追踪由特殊函数名创建的变量,将其编译成响应式代码,基本解决了类似 Vue 的困扰,无需手动解包,开发体验不错。

let message = $state('hello');

我认为 Svelte 的 runes 已经很接近完美了,开发体验很不错。

但 Svelte 本身仍然有以下几点问题:

第一:它有自己的 DSL .svelte,我认为 JSX 更佳,Typescript 对 JSX 的支持非常好,DSL 支持 TS 总是需要付出更多的代价,而且需要支付更多的学习成本。

第二:它的响应式仍然是和 Vue 一样的默认深度劫持,如果是复杂嵌套对象,劫持内部对象会被包装带来会有隐晦的debug负担和理解成本。我认为 Signal 信号的浅劫持理念更加简单和直观。

第三:runes 还不够完美,若在大型应用中使用其创建的变量,会导致和普通变量混淆,编译器可以追踪变量,但是在多文件代码复杂组合的时候,很难区分是普通变量还是响应式变量,给debug带来困难。

Solid

Solidjs,它则是视图部分采取编译手段,API部分保持原生,让用户裸使用原生 Signal API,Solidjs 的 API 是符合 Signal 理念的,没有深度劫持。但是原生的 Signal API 看起来使用较为繁琐。

import { createSignal } from "solid-js";
const [count, setCount] = createSignal(0);

Solid 性能不错,JSX + TS 的开发体验基本拉满,唯一的问题是裸 Signal API 的使用不够优雅,略显繁琐。

例如它也不能直接解构 props,需要借助帮助函数才能维持响应性。

通病

它们在支持 Web Component 这点上,都没有做好无缝的开发体验,有额外的使用成本。

总结

以上三个框架都抛弃了虚拟DOM,配合响应式API,性能表现都非常好,但它们都或多或少都有令人在意的问题,很难找到理想中的前端框架。

框架 真实 DOM Signal JSX Signal API 编译
Vue 支持(Vapor Mode) 兼容(shallowRef) 兼容 混合
Svelte 支持 不支持 不支持 支持
Solid 支持 支持 支持 不支持

理想的前端框架

如果我们需要一个新的前端框架,那么应该怎么设计?

根据上述总结,我认为 真实 DOM + JSX + Signal API 编译策略 + Web Component 一等支持 才是最接近完美的方案。

而 Solid 已经接近我们想要的了,给它加上剩下两个特性基本上就满足我们需要了。

所以怎么实现一个“完美”的框架呢?

从细粒度绑定到组件

signal 如何细粒度绑定 DOM 更新呢?又是怎么从基本的绑定演化为框架组件呢?

我们先从 Signal 的用法说起。

Signal 的基本用方法

// 声明信号
const name = signal("hello");
// 派生信号
const displayName = computed(() => name.value + " world");
// 副作用绑定
effect(() => {
  // 当 name.value = "hello2";
  // console => 1. "hello world" 2. "hello2 world"
  console.log(displayName);
});

绑定DOM元素

// 声明信号
const name = signal("hello");
// 派生信号
const displayName = computed(() => name.value + " world");

const text = document.createTextNode("");

// 副作用绑定
effect(() => {
  text.nodeValue= displayName.value;
});

演化成组件

一个只有 text 节点的组件:

const App = () => {
  const name = signal("hello");
  const displayName = computed(() => name.value + " world");
  return (() => {
    const text = document.createTextNode("");
    effect(() => {
      text.nodeValue= displayName.value;
    });
    return text;
  })();
}

更复杂的组件

在 div 中添加 text 节点:

const App = () => {
  const name = signal("hello");
  const displayName = computed(() => name.value + " world");
  return (() => {
    const el1 = (() => {
      const text = document.createTextNode("");
      effect(() => {
        text.nodeValue= displayName.value;
      });
      return text;
    })();
    const div = document.createElement("div");
    div.append(el1);
    return div;
  })();
}

演化成 JSX

Solid 的编译策略和上述是类似的,视图的编译是有规律的,创建 - 绑定 - 挂载,只要是有规律的,那就可以通过 DSL 来描述,JSX 正好可以表达这个过程。

const App = () => {
  const name = signal("hello");
  const displayName = computed(() => name.value + " world");
  return <div>{displayName.value}</div>;
}

可以看到复杂的视图创建流程通过 DSL 的使用配合编译手段,开发体验可以大幅提升。

同时需要指出 Solid 的编译方式未必是最好的,编译后的代码量挺大,还有各种闭包嵌套,可以稍微改进一下,编译成:

import { jsx, template } from "some/jsx-runtime"

const temp1 = template("<div>");

const App = () => {
  const name = signal("hello");
  const displayName = computed(() => name.value + " world");
  return jsx(temp1(), {
    get children() {
      return displayName.value;
    }
  });
}

Solid 把一部分 DOM 操作过程也编译出来了,事实上创建真实 DOM 的过程很大一部分是通用的,我们把创建元素的方法抽出来 jsx,用于创建和组装元素,这样编译出来的代码也会相对直观。

同时需要注意到 template 方法,它做了一件事,内部使用 cloneNode 去创建静态节点,这样可以提升性能。

总结

这套编译策略,从演化中总结编译策略,然后完成 JSX AST 转换实现,确实是有创新思维和难度的,属于框架的创新点核心。

最先搞视图转真实 DOM 编译的是 Svelte,而 Solid 完成了更高效的实现,又最终促进了 Svelte 5 的诞生,使 Web 框架在性能得到了上大幅升级。

完整的框架要考虑的更多

只靠上面的编译策略显然是不够的,需要考虑很多细节问题。

组件的创建,事实上挺复杂的,组件是有实例的,初始化实例的过程中需要做很多工作。

比如:利用插桩来定位组件组件的边界,假设组件直接返回 <><span>1</span><span>2</span></> ,如果没有插桩框架将无法识别边界,在做列表 diff 的时候,组件内元素集合的移除、添加、移动等操作将错乱。

const App = () => {
  const fragment = document.createDocumentFragment();
  const instance = {
    range: [
      document.createTextNode(""),
      document.createTextNode(""),
    ]
  }
  const span1 = document.createElement("span");
  const span2 = document.createElement("span");
  fragment.append(instance.range[0]);
  fragment.append(span1);
  fragment.append(span2);
  fragment.append(instance.range[1]);
  return fragment;
}

界面突变和 diff 算法

和 React 和 Vue 一样,这类编译型的前端框架仍然有 diff 过程。

界面突变的根本逻辑就是列表渲染,而列表渲染一定会涉及 diff,而 Vue 高效的 diff 算法也是可以使用的,算法和实现分离,不同的框架有不同的实现。

为什么说界面突变的根本逻辑是列表渲染?

条件渲染本质也是列表渲染,我们来看一个三目逻辑 :

// React
const List = () => {
  const [toggle, setToggle] = 0;
  
  useEffect(() => {
    setToggle((toggle[0] + 1) % 2);
  });
  
  return [toggle].map(i => (<Fragment key={i}>{i}</Fragment>))
}

实际上就是列表 [0][1] 之间相互切换。

Switch Case 逻辑也类似:

// React
const List = ({ value }) => {
  const [list, setList] = [1,2,3,4];
  
  const deriveList = list.filter(i => i === value).slice(0, 1);
  
  return [deriveList].map(i => (<Fragment key={i}>{i}</Fragment>));
}

根据 value 的值过滤列表,即可以实现 Switch Case 逻辑。

虚拟 DOM 和 真实 DOM 的 diff 实现差异

虚拟 DOM 的 diff 是从的组件节点(Vue)或者根节点(React)开始,遍历一遍,抽离出 DOM 指令以更新视图。

但是真实 DOM 的框架,列表是细粒度绑定的,当列表变化后,更新视图是在副作用内执行的,所以它需要一个特定的组件或者函数来封装这个副作用的逻辑,在 Solid 中就是 <For> 组件, Vue Vapor 和 Svelte 是在编译的时候编译成了一个特定的函数。

svelte:

$.each(node, 16, () => expression, $.index, ($$anchor, name, index, $$array) => {
    $.next();
    var text_2 = $.text('...');
    $.append($$anchor, text_2);
});

diff 算法可以借鉴,但是虚拟 DOM 和 真实 DOM 框架在 diff 算法中进行的操作并不一样,理论上 Solid 也可以用 Vue 3 的算法。

开发体验升级

上面指出 Solid 体验已经很好的,但是仍有不足,裸 Signal API 的使用不够优雅,getter setter 满屏幕跑,Vue Svelte 为了解决体验问题都通过对应的编译策略来解决这个问题,而 Solid 没有,有点遗憾。

事实上开发体验这块,React 除了需要手动管理依赖这块过于逆天之外,它的开发体验真的不错。

React 的组件状态写法已经很简洁了,不用像 Vue,Solid 那样套 computed。

const App = () => {
  const [name, setName] = useState("");
  
  const displayName = "Info: " + name
  
  return <div onClick={() => setName(name + "world")}>{displayName}</div>
}

也就是说,如果我们能改进 Solid,给它加上一组编译手段,改进 Signal 的使用体验,是不是会提升开发体验呢?

让我们尝试推演一下。

理想的组件形态

我们先提出一个理想中的组件形态,要求足够简洁,开发体验足够好:

const App = () => {
  let name = "hello";
  
  return (
    <div onClick={() => {name = name + "world"}}>{name}</div>
  )
}

我们希望改变 name 的时候,视图就会更新,但是这样是做不到的,改变一个变量没有任何作用。

但是如果是信号就不一样了:

const App = () => {
  const name = signal("");
  
  return (
    <div onClick={() => name.value = name.value + "xxx"}>{name.value}</div>
  )
}

我们根据上文所说的 JSX 编译手段,创建元素可以绑定副作用,name.value是可以被副作用收集到,并在name.value 更新的时候顺便更新视图。

import { jsx, template } from "some/jsx-runtime"

const temp1 = template("<div>");

const App = () => {
  const name = signal("");
  return jsx(temp1(), {
    get onClick() {
      return () => {
        name.value = name.value + "xxx";
      }
    },
    get children() {
      return name.value;
    }
  });
}

这时候就需要编译来完成我们的代码转换,在这里我们把信号变量使用 **$** 标记。然后就代码如下:

const App = () => {
  let $name = "hello";
  
  return (
    <div onClick={() => {$name = $name + "world"}}>{$name}</div>
  )
}

这个代码和我们理想中的组件代码非常接近了,要是真的能这样写代码,那么开发体验就能得到大幅提升。

Signal 信号编译策略

前面提到使用 $ 标记信号,就是一种创新的编译策略,通过特殊命名标记变量,将变量编译成响应式信号代码。

编译策略说明

这里我们按照 preact/signals 库的 api 做示例。

编译策略一:let 搭配 $ 开头的变量,即为声明信号。

let $name = "hello"
// 编译成
import { signal } from "@preact/signal";
let $name = signal("hello");

编译策略二:读取 $ 开头的变量会默认解包

let $name = "hello";
console.log($name);
// 编译成
let $name = signal("hello");
console.log($name.value);

编译策略三:const 搭配 $ 开头的变量,为声明派生信号。

let $name = "hello";
const $display = $name + "world";
// 编译成
import { signal, computed } from "@preact/signal";
let $name = signal("hello");
const $display = computed(() => $name.value + "world");

编译策略四:$use 开头的为自定义 hooks 。

const $useName = () => {
  let $name = "hello";
  
  return {
    name: $name
  }
}

// 编译成
const $useName = () => {
  let $name = signal("hello");
  
  return computed(() => ({
    name: $name.value
  }))
}

编译策略五:解构 + 变量传递。

函数入参,入参的响应传递,解构变量需要设置$前缀

const App = ({ name: $name, ...$rest }) => {
  console.log($rest);
  return <div>{$name}</div>
}

// 编译为
const App = ($__0) => {
  const $name = computed(() => $__0.value.name);
  const $rest = computed(() => {
    const { name, ...rest } = $__0.value;
    return rest;
  });
  console.log($rest.value);
  return <div>{$name.value}</div>
}

自定义 hook 返回,解构的时候为了不丢失响应,同样也要解构变量设置$前缀,这样就能触发编译。

const $useName = () => {
  let $name = "hello";
  
  return {
    name: $name
  }
}

// 解构后的变量也必须是 $ 开头,否则丢失响应,退化为普通变量(原始信号对象,可手动.value使用)
const { name: $name } = $useName();
// 自定义 hook 返回赋值的变量也必须是 $ 开头,否则丢失响应,退化为普通变量(原始信号对象,可手动.value使用)
const $nameSignal = $useName();

// 编译成
const $useName= () => {
  let $name = signal("hello");
  
  return computed(() => ({
    name: $name.value
  }))
}

const $__0 = computed(() => $useName().value);
const $name = computed(() => $__0.value.name);
const $nameSignal = $useName();

此编译策略的优点

  1. 无需手动导入 API,像普通变量一样使用 Signal
  2. 和 TS 类型的结合非常好,特别和 JSX 的类型结合非常完美
  3. 不怕解构
  4. 标记变量和常规变量一起用不会有混淆

一个简单的鼠标位置hook

const $usePosition = () => {
  let $x = 0;
  let $y = 0;
  
  const $pos = {
    x: $x,
    y: $y
  };
  
  mounted(() => {
    document.addEventListener('mousemove', (e) => {
      $x = e.pageX;
      $y = e.pageY;
    });
  })
  
  return {
    $pos,
  }
}

const App = () => {
  const { $pos } = $usePosition();
  
  return <div>x: {$pos.x}; y:{$pos.y}</div>
}

是不是清爽很多,简单应用的代码量差距不是很明显,但是如果代码量增加,那么代码量的差距还是非常可观的。

同时这样的设计,甚至不需要手动导入 API ,它在编译期间自动导入,让人无需关心 Signal 本身,真正做到了无感,开发体验得到了提升。

Web Component 支持

Vue Solid Svelte 都支持封装 Web Component,但是在开发体验上并没有多好,需要额外操作才能集成到框架中使用,做不到在框架内无缝使用,这样也限制了 Web Component 的推广和使用。

所以我们希望框架能够做好以下几点来支持 Web Component:

  • 和框架本身可以无缝集成,像普通组件一样方便使用
  • 组件 TS 类型易用且完善
  • 可以按照常规 Web Component 一样可以独立使用
  • 可以供给原生 HTML 或者其他框架使用

有这样的框架吗?

有啊 J20 框架 J20

logo

点个 Star 吧。

说在最后

这大概是我最后一个前端框架了,也算是完成了之前对前端框架的想法(中间隔了很久才想起来还有个东西没完成)。

歼20框架大量代码都是AI写的,我负责设计,它负责实现,同时帮我写测试,速度大幅提升。

AI 时代,也许框架不再重要了吧。哈哈

谢谢大家!

微前端:从“大前端”到“积木式开发”的架构演进

作者 扑棱蛾子
2025年12月10日 10:52

记得那些年我们维护的“巨石应用”吗?一个package.json里塞满了几百个依赖,每次npm install都像是一场赌博;团队协作时,git merge冲突解决到怀疑人生;技术栈升级?那意味着“全盘推翻重来”……

随着前端复杂度的爆炸式增长,传统单体架构已不堪重负。而微前端,正是为了解决这些痛点而生的一种架构范式。本文将以qiankun为切入点,学习一下微前端的模式。

基础概念

微前端是什么?

微前端不是框架,而是一种架构理念 ——将大型前端应用拆分为多个独立开发、独立部署、技术栈无关的小型应用,再将其组合为一个完整的应用。

一句话,它让前端开发从“造大楼”变成了 “搭乐高”

为什么需要微前端?

痛点真实存在:

  • 🐌 开发效率低下:几百人维护一个仓库,每次上线都需全量回归
  • 🔒 技术栈锁定:三年前选的框架,现在想升级?代价巨大
  • 👥 团队协作困难:功能边界模糊,代码相互渗透
  • 🚢 部署风险高:一个小改动,可能导致整个系统崩溃

微前端带来的改变:

  • ✅ 独立自治:每个团队负责自己的“微应用”,从开发到部署全流程自主
  • ✅ 技术栈自由:React、Vue、Angular、甚至jQuery,和平共处
  • ✅ 增量升级:老系统可以一点点替换,而不是“一夜重构”
  • ✅ 容错隔离:一个子应用崩溃,不影响其他功能

微前端的核心思想:

  • 拆分:将大型前端应用拆分为多个独立的小型应用。
  • 集成:通过某种方式将这些小型应用集成在一起,形成一个整体。
  • 自治:每个小型应用都可以独立开发、测试、部署。
// 微前端架构
├── container/      // 主应用(基座)
├── app-react/      // React子应用(团队A)
├── app-vue/        // Vue子应用(团队B)
├── app-angular/    // Angular子应用(团队C)
└── app-legacy/     // 老系统(jQuery)

// 优势:
// 1. ✅ 技术栈无关
// 2. ✅ 独立开发、独立部署
// 3. ✅ 增量更新
// 4. ✅ 容错性高(一个子应用挂了不影响其他)

应用场景

渐进式重构:对于一个老项目一点点进行架构的升级

老系统(jQuery + PHP) → 逐步替换为现代框架
   ↓
保留核心业务模块 + 逐步添加React/Vue新模块

多团队协作:不同部门人员之间技术栈存在差异,需要单独开发

团队A(React专家) → 负责电商商品模块
团队B(Vue专家)   → 负责购物车模块
团队C(Angular专家)→ 负责用户中心
主应用协调所有模块

中后台系统:复杂系统的功能拆分

一个后台管理系统包含:
- 权限管理(React)
- 数据报表(Vue + ECharts)
- 工作流(Angular)
- 监控面板(React + Three.js)

四种架构模式

基座模式(也称为中心化路由模式)

  • 基座模式是最常见的微前端架构。它有一个主应用(通常称为基座或容器),负责整个应用的布局、路由和公共逻辑。子应用根据路由被动态加载和卸载。
  ┌─────────────────────────────────────────┐
  │            主应用(Container)           │
  │ 负责:路由、鉴权、布局、共享状态、公共依赖   │
  ├─────────────────────────────────────────┤
  │  ┌──────────┐  ┌──────────┐  ┌──────────┐ 
  │  │ 子应用A  │  │ 子应用B  │  │ 子应用C  │ │
  │  │ (React)  │  │  (Vue)   │  │(Angular) │ 
  │  └──────────┘  └──────────┘  └──────────┘ 
  └─────────────────────────────────────────┘

工作流程

graph TD
用户访问主应用-->主应用根据当前URL匹配子应用--> A["加载对应子应用的资源(JS、CSS)"]-->将子应用渲染到指定容器中-->子应用运行并处理自己的内部路由和逻辑

优点

  • 集中控制,易于管理
  • 路由逻辑清晰
  • 公共依赖容易处理(基座可提供共享库)
  • 子应用间隔离性好

缺点

  • 主应用成为单点故障
  • 基座和子应用耦合(通过协议通信)
  • 基座需要知道所有子应用的信息

适用场景

  • 企业级中后台系统
  • 需要统一导航和布局的应用
  • 子应用技术栈差异大

自组织模式(也称为去中心化模式)

  • 在自组织模式中,没有中心化的基座。每个微前端应用都是独立的,它们通过某种通信机制(如自定义事件、消息总线)来协调。通常,每个应用都可以动态发现和加载其他应用。
┌──────────┐    ┌──────────┐    ┌──────────┐
│  应用A   │     │  应用B   │    │  应用C    │
│ (React)  │    │  (Vue)   │    │(Angular) │
└────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │
     └───────────────┼───────────────┘
                     │
            ┌────────┴─────────┐
            │  运行时协调器     │
            │  (Runtime Bus)   │
            └──────────────────┘
graph TD
1["应用A启动,并注册到消息总线"]
-->2["应用B启动,并注册到消息总线"]
-->用户操作触发应用A需要应用B的某个功能
-->应用A通过消息总线请求应用B的资源
-->3["应用B响应请求,提供资源(或直接渲染)"]

优点

  • 去中心化,避免单点故障
  • 应用之间完全解耦
  • 更灵活的通信方式

缺点

  • 通信复杂,容易混乱
  • 难以统一管理(如路由、权限)
  • 依赖公共协议,版本更新可能破坏通信

适用场景

  • 高度自治的团队
  • 应用间功能相对独立
  • 需要动态组合的页面

微件模式(也称为组合式模式)

  • 微件模式类似于传统门户网站,页面由多个独立的微件(Widget)组成。每个微件都是一个独立的微前端应用,可以独立开发、部署,然后动态组合到页面中。
┌───────────────────────────────────┐
│          Dashboard页面            │
│  ┌────────┬────────┬─────────┐    │
│  │ 天气    │ 新闻   │ 股票    │     │
│  │ Widget │ Widget │ Widget  │    │
│  ├────────┼────────┼─────────┤    │
│  │ 待办    │ 日历   │ 邮件    │     │
│  │ Widget │ Widget │ Widget  │    │
│  └────────┴────────┴─────────┘    │
└───────────────────────────────────┘
graph TD
用户访问页面
    -->
页面布局引擎根据配置加载微件
    -->
每个微件独立加载资源并渲染
    -->
微件之间通过预定义的接口通信

优点

  • 组件可以复用
  • 用户可以自定义布局
  • 所有widget在同一个页面
  • 可以按需加载widget

缺点

  • 样式管理复杂,需要处理widget间样式冲突
  • 通信限制,widget间通信需要经过主应用
  • 版本管理,大量widget的版本管理困难
  • 性能问题,太多widget可能影响性能

适用场景

  1. 数据可视化大屏
  2. 门户网站首页
  3. 个人工作台
  4. 可配置的管理后台

混合模式(实战中最常见)

  • 在实际项目中,我们常常根据需求混合使用以上模式。例如,在基座模式中,某个子应用内部使用微件模式来组合多个微前端模块。
  • 比如一个电商系统的架构
主应用(基座模式)
    ├── 商品管理(React子应用)
    ├── 订单管理(Vue子应用)
    └── 用户管理(Angular子应用)
        在用户管理内部,使用微件模式:
            ├── 用户统计(微件A)
            ├── 用户列表(微件B)
            └── 用户权限(微件C)
┌─────────────────────────────────────────────────┐
│                主应用(基座模式)                 │
│   统一路由、权限、用户中心、消息中心、全局状态       │
└─────────────────┬───────────────────────────────┘
                  │
    ┌─────────────┼─────────────┐
    │             │             │
┌───▼───┐   ┌────▼────┐   ┌────▼────┐
│订单中心│   │商品管理 │   │用户管理   │
│(React)│   │ (Vue)   │   │(Angular)│
└───┬───┘   └────┬────┘   └────┬────┘
    │            │             │
    └────────────┼─────────────┘
                 │
          ┌──────▼──────┐
          │ 数据分析模块 │
          │ (微件模式)   │
          │┌───┬───┬───┐│
          ││图表│地图│报表│
          │└───┴───┴───┘│

快速上手

  • 新建三个项目,分别为main-app,sub-app1,sub-app2,项目结构一目了然:
   ├── main-app/      // 主应用(基座)
   ├── sub-app1/      // vue3子应用(团队A)
   ├── app-vue/        // vue3子应用(团队B)

安装qiankun

yarn add qiankun # 或者 npm i qiankun -S

主项目中注册微应用

// 主应用main-app/main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { registerMicroApps, start } from 'qiankun'

createApp(App).mount('#app1')

registerMicroApps(
  [
    {
      name: 'sub-app1', // app name registered
      entry: 'http://localhost:5175',
      container: '#micro-app-container',
      activeRule: (location) => location.hash.startsWith('#/app-a'),
      props: {
        name: 'kuitos'
      }
    }
  ],
  {
    beforeLoad: (app) => console.log('before load', app.name),
    beforeMount: [(app) => console.log('before mount', app.name)]
  }
)
// start()

// 启动 qiankun,配置沙箱模式
start({
  sandbox: {
    strictStyleIsolation: true,
  },
})

微应用导出钩子

  • 由于qiankun不支持module,所以对于vue3项目,需要使用vite-plugin-qiankun来集成
  • renderWithQiankun用来对外暴露钩子
  • qiankunWindow替代window变量
// 子应用 sub-app1/mian.js
import { createApp } from 'vue'
import {
  renderWithQiankun,
  qiankunWindow
} from 'vite-plugin-qiankun/dist/helper'

import './style.css'
import App from './App.vue'
let instance = null

function render(props = {}) {
  const container = props.container || '#app'
  console.log('子应用挂载容器:', container)

  instance = createApp(App)
  instance.mount(container)
}
console.log('qiankunWindow',qiankunWindow);
console.log('window.__POWERED_BY_QIANKUN__',window.__POWERED_BY_QIANKUN__);

// 独立运行时,直接渲染
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  console.log('独立运行时,直接渲染')
  render()
}

renderWithQiankun({
  mount(props) {
    console.log(props)
    render(props)
  },
  bootstrap() {
    console.log('bootstrap')
  },
  unmount(props) {
    console.log('unmount', props)
  },
  update(props) {
    console.log('update', props)
  }
})

在子应用的vite.config.js中注册插件

// 子应用 sub-app1/vite.config.js
plugins: [
    vue(),
    qiankun('sub-app1', {
      useDevMode: true,
    })
],

进阶场景

应用通信

应用拆分后,不可避免的会涉及到通信问题,那么如何让它们“愉快地对话”?

props

  • 最简单的方式,正如目前的主流框架,qiankun也提供了一个props属性,可以实现父->子之间的数据通信,当主应用注册registerMicroApps子应用的时候,利用props传递
// 主应用 main-app/main.js
registerMicroApps(
  [
    {
      name: 'sub-app1', // app name registered
      entry: 'http://localhost:5175',
      container: '#micro-app-container',
      activeRule: (location) => location.hash.startsWith('#/app-a'),
      props: {
        // name: 'kuitos' //该属性会被覆盖?
        count: 100,
        time: new Date().getTime()
      }
    }
  ],
  {
    beforeLoad: (app) => console.log('before load', app.name),
    beforeMount: [(app) => console.log('before mount', app.name)]
  }
)

// 子应用 sub-app1/main.js
renderWithQiankun({
  mount(props) {
    render(props)
  },
})
// 子应用 sub-app1/main.js
function render(props = {}) {
  const container = props.container || '#app'
  console.log('子应用挂载容器:', container)

  instance = createApp(App)
  instance.config.globalProperties.__SUBAPP__ = props //vue3的globalProperties全局挂载
  window.__SUBAPP__ = props //挂载到子应用的window对象

  instance.mount(container)
}

子应用的其他组件使用时

//子应用 sub-app1/src/components/HelloWord.vue
<script setup>
    import { ref,getCurrentInstance } from 'vue'

    defineProps({
      msg: String,
    })

    const count = ref(0)
    console.log('window方式获取数据',window.__SUBAPP__)
    console.log('getCurrentInstance方式获取数据', getCurrentInstance().proxy.__SUBAPP__)
</script>

image-20251208221830086

initGlobalState

父->子传递

首先在父应用中创建一个globalState.js初始化一下state

//main-app/globalState.js
import { initGlobalState } from 'qiankun';

// 定义初始状态
export const initialState = {
  user: { id: null, name: '', token: '' },
  globalConfig: { theme: 'light', language: 'zh-CN' },
  sharedData: {},
  currentRoute: {}
};

// 当前全局状态
export let currentGlobalState = { ...initialState };

// 全局状态管理器实例
export let globalActions = null;

// 初始化全局状态管理
export const initGlobalStateManager = () => {
  // 初始化 state
  const actions = initGlobalState(initialState);
  
  // 监听状态变更
  actions.onGlobalStateChange((state, prev) => {
    currentGlobalState = { ...state };
    console.log('主应用:全局状态变更', { newState: state, prevState: prev });
  });
  
  // 设置初始状态
  actions.setGlobalState(initialState);
  
  globalActions = actions;
  return actions;
};

// 更新全局状态
export const updateGlobalState = (newState) => {
  if (!globalActions) {
    globalActions = initGlobalStateManager();
  }
  globalActions.setGlobalState(newState);
};


其中关键方法:

// 定义初始状态
export const initialState = {
  user: { id: null, name: '', token: '' },
  globalConfig: { theme: 'light', language: 'zh-CN' },
  sharedData: {},
  currentRoute: {}
};
//初始化 state
const actions = initGlobalState(initialState);
// 监听状态变更
actions.onGlobalStateChange((state, prev) => {
    currentGlobalState = { ...state };
    console.log('主应用:全局状态变更', { newState: state, prevState: prev });
});
// 更新全局状态
actions.setGlobalState(newState);
// 取消监听
actions.offGlobalStateChange();

// main-app/login.vue
import { updateGlobalState } from './globalState'
const handleLogin =()=>{
  // 。。。主应用的业务逻辑
  // 更新state  
  updateGlobalState({
      isLoggedIn: true,
    });
}

在子应用中监听

// sub-app1/main.js
function render(props = {}) {
  const container = props.container || '#app'

  instance = createApp(App)
  
  // 监听全局状态变化
  //props 里面有setGlobalState和onGlobalStateChange 方法,可用于监听和修改状态 
  if (props.onGlobalStateChange) {
    props.onGlobalStateChange((state, prev) => {
      console.log('子变更后的状态', state, '子变更前的状态', prev);
    });
  }
  // 挂载一下props,以便于在其他组件中使用setGlobalState和onGlobalStateChange
  // 挂载的方式有很多, pinia等,总之其他地方能获取到props对象就行  
  window.__SUBAPP__ = props
  pinia = createPinia()
  instance.use(pinia)
  instance.mount(container)
}
image-20251209221654305
子->父传递

在子应用创建的时候,已经将props保存了window.__SUBAPP__ = props,在子应用的任何组件中都可以使用

所以只需要在某个组件中调用setGlobalState方法就可

// sub-app1/HelloWord.vue
// 获取全局状态管理方法
const { setGlobalState } = window.__SUBAPP__ || {}
if (setGlobalState) {
// 更新全局状态
    setGlobalState({
      sharedData: {
        count: newValue
      }
    })
}

image-20251209222325435

微前端选型指南:何时用?用哪个?

适合场景 ✅

  • 大型企业级应用(100+页面)
  • 多团队协作开发(3+前端团队)
  • 老系统渐进式重构
  • 需要支持多技术栈
  • 独立部署需求强烈

不适合场景 ❌

  • 小型项目(页面<20)
  • 单人/小团队开发
  • 对性能要求极致(首屏加载时间<1s)
  • 无技术栈异构需求

结语

千万不要手里攥着锤子看啥都像钉子。 微前端不是银弹,而是一种架构选择。它用复杂度换来了灵活性、独立性和可维护性。就像乐高积木,单个模块简单,但组合起来却能构建出无限可能的世界。

后续有时间将继续深入学习一下微前端的生命周期、样式隔离、部署发布这几个部分。

最后,觉得有用的话三连一下~

❌
❌