当前位置: 首页 > news >正文

前端微服务架构解析:qiankun 运行原理详解

前端微服务在现在的前端项目中也不陌生,经常在一些大型的项目中有看到过它的身影,而如今最火也最常用的,当初qiankun了,今天我们来聊一聊qiankun是如何实现微服务的

一、前端微服务架构的价值与应用场景

首先,为什么需要有微服务这么一种技术架构呢,它的出现解决了什么问题呢?我们先来看几个场景。

  • 假设你正在维护迭代一个大型项目,但是你每次迭代只是在修改项目的某一个模块,但你每次的代码开发,提交,部署都需要拉上整个项目。耗费的资源更多,速度更慢。这时候你是不是就会想,要是可以只部署这一个模块就好了
  • 假设你公司有一个很陈旧的老项目,用的很老旧的技术架构,你们需要在这个的基础之上去迭代新的需求,想用新的技术架构去进行迭代,你是不是就会想,要是新增部分,能独立开发,独立部署就好了

是的,此时,微服务它就诞生了
解决的问题与优势:

  1. 巨石应用拆分:解决单体前端工程臃肿、维护困难的问题,支持多团队并行开发
  2. 技术栈无关:主应用与子应用可使用不同框架(React/Vue/Angular),实现渐进式升级
  3. 独立部署,可跨团队协作:子应用可独立构建部署,无需整体发布,降低发布风险
  4. 资源按需加载:仅加载当前路由匹配的子应用,优化首屏性能

典型应用场景:

  • 大型后台管理系统(如阿里云控制台)
  • 多产品线聚合平台(如电商导购门户)
  • 遗留系统现代化改造
  • 跨团队协作项目

**二、qiankun 核心实现原理详解

微服务的工作本质,是能够把多个独立项目能够拼接组装成一个大型项目,那既然是拼接的,我们肯定就会疑惑,它是如何把几个毫无联系的独立应用串起来的,它中间做了些什么呢?又是如何进行数据通信的呢?又是如何保证几个应用相互不冲突呢?

诶,下面我们就来看看,qiankun是怎么做的

2.1 子应用注册机制

首先,会有一个主应用的概念,用来管理协调各个子应用,而其他子应用,都需要在主应用中进行注册
原理:通过配置对象声明子应用信息,qiankun 创建应用管理器进行统一调度

// 主应用注册子应用
import { registerMicroApps, start } from 'qiankun';// registerMicroApps内部最终还是通过调用single-spa的registerApplication方法去注册应用
registerMicroApps([{name: 'react-app',  // 应用唯一标识entry: '//localhost:3001',  // 子应用入口地址container: '#subappReact',  // 挂载容器activeRule: '/react',  // 路由匹配规则props: { user: 'admin' }  // 透传数据}{name: 'vue-app',  // 应用唯一标识entry: '//localhost:3002',  // 子应用入口地址container: '#subappVue',  // 挂载容器activeRule: '/vue',  // 路由匹配规则props: { user: 'admin' }  // 透传数据}
]);start();  // 启动 qiankun

关键注解

  • activeRule 支持函数/字符串/正则,实现动态路由匹配
  • props 实现主应用向子应用的数据注入
  • 应用实例存储在全局 apps 数组中统一管理

子应用注册,并配置了匹配规则,当项目运行时,浏览器访问路由页面或页面路由发生变化时,qiankun会用当前的路由去和注册的规则表进行一个匹配,当匹配到某个规则时,会开始去加载当前规则的子应用(根据配置的entry地址加载子应用),加载完成后,将加载到的内容挂载到配置的container元素上,这个container元素,一定是主应用中的某个元素。

2.2 路由规则匹配

实现流程

  1. 监听 hashchangepopstate 事件
  2. 遍历所有注册应用的 activeRule
  3. 执行匹配算法确定需加载的子应用
// 路由匹配核心逻辑(简化版)
function matchApp(currentPath) {return apps.filter(app => {if (typeof app.activeRule === 'function') {return app.activeRule(currentPath);}// pathPrefix是个工具函数,里面是一系列的匹配规则return pathPrefix(app.activeRule, currentPath);});
}

通过匹配确定了需要加载某个应用后,会去取到该app的entry属性,通过这个地址去加载子应用,加载完成后,去执行子应用。那么我们之前有说到,每个子应用,它都是相互独立的,是不会相互影响的,那当多个子应用代码集中在一起执行时,必然要通过隔离机制,将各个子应用以及主应用相互隔离,这必然要用到沙箱机制

2.3 沙箱机制(核心安全隔离)

qiankun主要用到3种沙箱类型
沙箱类型

类型原理适用场景
LegacySandbox单例代理兼容性要求高
ProxySandbox多例代理多实例并行
SnapshotSandbox快照恢复IE 等老旧浏览器

现在最普遍的就是ProxySandbox,我们以ProxySandbox来做说明

代理沙箱
代理沙箱的本质是为每个微应用创建一个 window 的代理对象,微应用对全局变量的操作都发生在代理上,不会影响真实的 window。可以理解为,每个子应用代码中的顶级对象不再是window,而是一个集成了window部分属性的代理对象,因此,所有子应用的全局变量定义,函数定义等,本质都会挂在这个代理对象下,然后子应用代码都执行在一个自调用的函数中。从而实现js互不干扰

// 代理沙箱实现(核心代码)
// 简化的 ProxySandbox 实现
class ProxySandbox {constructor(name) {this.name = name;// 创建一个空的 fakeWindow 作为初始全局对象副本const rawWindow = window; // 保存真实的 windowconst fakeWindow = Object.create(null); // 用于记录在沙箱内新增或修改的全局变量this.updatedPropsMap = new Map(); // 创建 Proxy 代理 fakeWindowthis.proxy = new Proxy(fakeWindow, {set: (target, prop, value) => {// 将修改记录在 updatedPropsMap 中,而不是直接设置到 rawWindowthis.updatedPropsMap.set(prop, value);// 这里也可以选择将修改同步到 fakeWindow 上,供内部读取target[prop] = value;return true;},get: (target, prop) => {// 优先从修改记录中取if (this.updatedPropsMap.has(prop)) {return this.updatedPropsMap.get(prop);}// 否则,从真实的 window 上读取(如一些原生 API)const value = rawWindow[prop];if (typeof value === 'function') {return value.bind(rawWindow); // 保持函数执行上下文正确}return value;},has: (target, prop) => {return prop in rawWindow || this.updatedPropsMap.has(prop);}});}
}

css隔离
而css隔离主要是通过给每个子应用的所有样式添加一个特殊的前缀(data-qiankun=xxx)来实现

// 样式隔离的简化思路(通常在处理 HTML 模板时进行)
function processHTML(html, appName) {
// 使用一个唯一的属性选择器为所有样式规则添加作用域
const scopedHTML = html.replace(/<style>([\s\S]*?)<\/style>/g, (match, styleContent) => {const scopedStyle = styleContent.replace(/([^{]+\{)([^}]+)(\})/g, (fullMatch, selectorPart, contentPart, endPart) => {// 简化处理:为所有简单选择器添加 [data-qiankun="appName"] 前缀// 实际实现会更复杂,需要处理各种复杂选择器const scopedSelector = selectorPart.split(',').map(selector => {return `[data-qiankun="${appName}"] ${selector.trim()}`;}).join(', ');return `${scopedSelector} { ${contentPart} ${endPart}`;});return `<style>${scopedStyle}</style>`;
});
return scopedHTML;
}

2.4 子应用加载执行流程

生命周期时序
在这里插入图片描述

加载逻辑
当通过路由匹配到需要加载的app后,会通过importEntry方法去加载子应用,方法内部会利用import-html-entry库去解析子应用,解析出模板,样式以及需要执行的script等,执行script时,会放置在沙箱中执行

// 简化的 loadApp 函数核心逻辑
export async function loadApp(appConfig) {// 1. 获取并解析 HTML 入口const { template, getExternalScripts, getExternalStyleSheets } = await importHTML(appConfig.entry);// 2. 创建沙箱环境const sandbox = createSandbox(appConfig.name);sandbox.active(); // 激活沙箱const fakeWindow = sandbox.proxy;// 3. 获取并执行样式const styles = await getExternalStyleSheets();styles.forEach(styleContent => {const style = document.createElement('style');style.textContent = styleContent;document.head.appendChild(style);});// 4. 关键逻辑:获取脚本并放在沙箱中执行const scripts = await getExternalScripts();scripts.forEach(scriptCode => {// 使用 with 语句和 Proxy 将脚本中的全局变量访问指向伪造的 window (fakeWindow)const wrappedCode = `with(fakeWindow) {${scriptCode}}`;// 在沙箱上下文(如 iframe 或 Proxy 创建的隔离环境)中执行代码(function(fakeWindow) { eval(wrappedCode); }).bind(sandbox.proxy)(sandbox.proxy); });// 5. 返回一个包含生命周期钩子的对象return {// 通常,子应用的 JS 入口文件会将其生命周期函数挂载到全局,这里需要获取它们bootstrap: window[`${appConfig.name}_bootstrap`],mount: window[`${appConfig.name}_mount`],unmount: window[`${appConfig.name}_unmount`],};
}

​​with语句​​:改变 JavaScript 的作用域链查找顺序。代码中所有未明确作用域的变量(如 window)会优先从 sandbox.proxy(也就是fakeWindow)中查找。

2.5 生命周期调度机制

而子应用中,需要暴露对应的生命周期接口,供主应用调用,主应用会在子应用挂载时,调用对应的生命周期函数
标准化生命周期

// 子应用暴露的接口(React 示例)
export const bootstrap = async () => {console.log('初始化资源');
};export const mount = async (props) => {ReactDOM.render(<App/>, props.container);
};export const unmount = async (props) => {ReactDOM.unmountComponentAtNode(props.container);
};

qiankun 调度引擎

// 生命周期执行器(核心)
async function execHooks(app, hookName) {const hooks = app[hookName] || [];for (const hook of hooks) {await hook(app.props); // 透传容器等参数}
}// 挂载流程
async function mountApp(app) {await execHooks(app, 'beforeMount');await execHooks(app, 'mount');  // 执行子应用的 mountawait execHooks(app, 'afterMount');
}

2.6 通信原理

通信机制特点

  • 发布订阅模式:状态变更通知所有关联应用
  • 单向数据流:主应用作为数据源
  • 防循环触发:通过事务ID避免无限循环

qiankun 提供了一个简单的全局状态管理工具,基于发布-订阅模式

// 简化的 initGlobalState 实现
class GlobalState {constructor(initialState = {}) {this.state = initialState;this.listeners = [];}setGlobalState(newState) {this.state = { ...this.state, ...newState };// 通知所有订阅者this.listeners.forEach(listener => listener(this.state));}onGlobalStateChange(callback) {this.listeners.push(callback);// 返回一个取消订阅的函数return () => {const index = this.listeners.indexOf(callback);if (index > -1) {this.listeners.splice(index, 1);}};}
}let globalStateInstance = null;export function initGlobalState(state) {if (globalStateInstance === null) {globalStateInstance = new GlobalState(state);}return globalStateInstance;
}

子应用可以通过onGlobalStateChange监听state的变化,并通过setGlobalState去改变全局状态,全局状态一旦发生改变,会立刻通知所有的其他监听者

export async function mount(props) {// 监听数据变化props.onGlobalStateChange((state) => {// state: 变更后的状态;if (!state.routes.length) {const _state = {routes: routes}// 设置数据props.setGlobalState(_state);}}, true); // 第二个参数设置为 true 表示一上来就执行一次render(props)
}

start初始化
主应用注册完子应用后,会调用start方法进行初始化

// 基于 src/apis.ts 和 src/start.ts 简化
export function start(opts: FrameworkConfiguration = {}) {// 1. 设置全局变量,标记 qiankun 启动状态window.__POWERED_BY_QIANKUN__ = true; // :cite[2]// 2. 初始化全局配置,使用默认值合并用户配置frameworkConfiguration = {prefetch: true,singular: true, // 默认单例模式sandbox: true,  // 默认开启沙箱...opts,};const {prefetch,singular = true,sandbox = true,...importEntryOpts} = frameworkConfiguration;// 3. 配置预加载策略if (prefetch) {// 在第一个应用挂载后预加载其他应用资源 :cite[2]listenAfterFirstMount(() => prefetchAfterFirstMounted(apps, prefetch));}// 4. 设置沙箱模式if (sandbox) {if (window.Proxy) {// 支持 Proxy 的浏览器使用先进的沙箱if (singular) {legacySandbox = true;} else {proxySandbox = true;}} else {// 不支持 Proxy 的浏览器使用降级方案 SnapshotSandbox :cite[1]snapshotSandbox = true;}}// 5. 启动 single-spastartSingleSpa({ urlRerouteOnly: true }); // :cite[7]frameworkStartedDefer.resolve(); // 标记框架已启动
}
三、模块联邦

有时当我们有些功能模块需要在各个组件之间复用时,比如A应用的一个模块,希望其他应用也能够调用它。这是一个很常见的需求,之前,webpack5提供了一个模块联邦的能力,很好的解决了这个问题。

模块联邦在构建阶段就明确了模块的“提供方”和“消费方”。在运行时,消费方应用会通过一个全局的模块映射表,动态拉取并提供方应用暴露的代码并执行。这更像是“代码复用”而非传统“通信”,但能达到共享逻辑和状态的效果。

实现原理:

远程应用:暴露一些模块给其他应用使用。这些模块可以是组件、页面、工具函数等。

主机应用:消费(使用)远程应用暴露的模块。

它不是在“加载一个应用”,而是在“加载一个模块”,而这个模块恰好可以是一个完整的 React/Vue 应用。


配置详情:
远程应用配置 (app1/webpack.config.js)

module.exports = {plugins: [new ModuleFederationPlugin({name: ‘app1’,filename: ‘remoteEntry.js’, // 入口文件exposes: {./Button’:./src/Button’, // 暴露一个按钮组件./App’:./src/App’, // 暴露整个App组件},shared: { react: { singleton: true }, ‘react-dom’: { singleton: true } }, // 共享依赖}),],
};

主机应用配置 (host/webpack.config.js):

module.exports = {plugins: [new ModuleFederationPlugin({name: ‘host’,remotes: {app1: ‘app1@http://localhost:3001/remoteEntry.js’, // 引用远程应用},shared: { react: { singleton: true }, ‘react-dom’: { singleton: true } },}),],
};

使用:

// 像导入本地模块一样导入远程模块!
import RemoteButton from ‘app1/Button’;
import RemoteApp from ‘app1/App’;function HostApp() {return (<div><h1>我是主应用</h1><RemoteButton /><RemoteApp /></div>);
}
四、总结

加载流程复盘:
用户访问 https://portal.com。

1、主应用加载,初始化路由系统。

2、用户点击导航跳转到 /app1。

3、主应用的路由器捕获到变化,查询映射表,发现 /app1 对应 //app1.com/bundle.js。

4、主应用通过 动态脚本加载(如 System.import() 或 自己创建

5、主应用创建沙箱环境,在沙箱中运行微应用代码

6、微应用的脚本执行后,会向全局(如 window)暴露其生命周期函数(window.app1.bootstrap, window.app1.mount 等)。

7、主应用先调用 app1.bootstrap() 进行初始化。

8、主应用准备好一个 DOM 容器(如

),然后调用 app1.mount({ container: ‘#micro-app-container’, props: {…} })。

9、微应用 app1 在自己的 mount 方法中,将整个应用渲染到主应用提供的容器中。

10、当用户从 /app1 导航到 /app2 时,主应用会先调用 app1.unmount() 进行清理,然后重复步骤 4-9 来加载和挂载 app2。

本文剖析了 qiankun 的大致的架构设计与实现,其目标是让读者能够以一个更简单的角度来更简单的理解qiankun以及微服务的大致原理。

http://www.dtcms.com/a/486063.html

相关文章:

  • linux ssh config详解
  • 内网攻防实战图谱:从红队视角构建安全对抗体系
  • 鲲鹏ARM服务器配置YUM源
  • 网站分类标准沈阳网站制作招聘网
  • 建设一个网站需要几个角色建筑工程网课心得体会
  • 基于Robosuite和Robomimic采集mujoco平台的机械臂数据微调预训练PI0模型,实现快速训练机械臂任务
  • 深度学习目标检测项目
  • SQL 窗口函数
  • 盟接之桥浅谈目标落地的底层逻辑:实践、分解与认知跃迁
  • 【Qt】4.项目文件解析
  • Redis-布隆过滤器BloomFilter
  • 网站建设找至尚网络深圳制作企业网站
  • 网页是网站吗苏州刚刚发生的大事
  • WPF中RelayCommand的实现与使用详解
  • 百度天气:空气质量WebGIS可视化的创新实践 —— 以湖南省为例
  • Flutter---GridView+自定义控件
  • OJ竞赛平台----C端题目列表
  • 【完整源码+数据集+部署教程】行人和斑马线检测系统源码和数据集:改进yolo11-RFCBAMConv
  • 做海淘的网站做海淘的网站网站建设案例步骤
  • [Zer0pts2020]Can you guess it?
  • Go 通道非阻塞发送:优雅地处理“通道已满”的场景
  • 设计模式【工厂模式和策略模式】
  • 【Go】P6 Golang 基础:流程控制
  • Perl 基础语法
  • 酒店网站模板网站开发好的语言
  • C++入门——多态
  • 用数据绘图(1):用 Highcharts 打造你的数据艺术世界
  • Hadoop面试题及详细答案 110题 (96-105)-- Hadoop性能优化
  • 监控系统理论与实践:从认知到Zabbix入门
  • ROS 传感器模块的通用架构设计与跨中间件扩展实践