字节跳动是如何落地微前端的

字节技术年货之实践篇:微服务架构演进、全链路压测、埋点数据治理……(2) 在技术领域内,没有任何一门课程可以让你学完后一劳永逸,再好的课程也只能是“师傅领进门,修行靠个人”。“学无止境”这句话,在任何技术领域,都不只是良好的习惯,更是程序员和工程师们不被时代淘汰、获得更好机会和发展的必要前提。如果你觉得自己学习效率低,缺乏正确的指导,可以一起学习交流!加入我们吧!群内有许多来自一线的技术大牛,也有在小厂或外包公司奋斗的码农,我们致力打造一个平等,高质量的Android交流圈子,不一定能短期就让每个人的技术突飞猛进,但从长远来说,眼光,格局,长远发展的方向才是最重要的。 阅读详情

在这里插入图片描述

作者:字节跳动终端技术——周晓

本文内提及的 Garfish 微前端解决方案已开源:https://github.com/modern-js-dev/garfish(目前的 Garfish 作为字节跳动各部门应用最广泛的微前端解决方案已经服务超过 100+ 前端团队,400+ 项目),另外字节跳动的现代 Web 工程体系即将开源(Modern.js),深度集成 Garfish 提供了对微前端的原生支持,提供更开箱即用的能力,敬请期待!

微前端的出现的背景和意义

微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。

微前端诞生在两个大的背景下,在提倡拥抱变化的前端社区可以看到新的框架、技术、概念层出不穷,并且随着 Web 标准的演进,前端应用已经具备更好的性能、更快的开发效率。但随着而来的是应用的复杂程度更高、涉及的团队规模更广、更高的性能要求,应用复杂度已经成为阻塞业务发展的重要瓶颈。

微前端就是诞生于 Web 应用日益复杂化的场景中,因为随着网络速度、计算机硬件水平的提升和 Web 标准的演进,过去 Web 应用用户体验远不如传统的应用软件时代已逐渐远去,两者之间在用户体验上的差距不断缩减,并且由于 Web 应用开发速度快、用完即走等特性,导致的一个最终结果就是「能用 Web 技术实现的应用,最终都会通过 Web 来实现」。在近几年涌现了一大批之前只能在传统 PC 软件中才能看到的优秀产品,例如:Photoshop、Web Office、Web IDE。尽管随着 Web 标准的演进,前端工程化也在不断演变,从模块化到组件化在到现在的工程化,但在面对跨团队大规模开发、跨团队企业级应用协作,现有的分治设计模式仍然显得有心无力。

在这里插入图片描述

大规模 Web 应用的困局

尽管 Web 应用的复杂度和参与人数以爆炸式的增长速度,但却没有一种新的架构模式来解决现有的困境,并同时兼顾 DX(developer experience)和 UX(user experience)。

以字节跳动内「研发中台」举例,在研发日常工作中需要使用非常多的研发系统,例如:代码管理、代码构建、域名管理、应用发布、CDN 资源管理、对象存储等。站在整个公司研发的角度考虑,最好的产品形态就是将所有的研发系统都放置同一个产品内,用户是无法感知他在使用不同的产品,对于用户而言就是单个产品不存割裂感,也不需要去学习多个平台,仅仅需要学习和了解字节跳动内的「研发中台」即可。

在字节跳动内这一类应用随处可见,由于字节跳动内存在大量业务线,每一条业务线都会诞生大量的中台系统,并且还在指数增长,以字节跳动内电商业务举例,对于电商运营的日常工作来说,其实与研发日常工作一样,围绕在:商品、商家、品牌、风控、营销等工作上,那么对于电商运营来说怎么样才最高效的电商运营系统呢,由于整个系统涉及范围较广,在实际的研发过程中必然会以功能或业务需求垂直的切分成更小的子系统,切分成各种小系统后尽管由于分治的设计理念提升了开发者体验,但是一定程度上降低了用户体验。那能否以一种新的架构模式,既保开发者体验,又能提升用户体验呢。

传统 Web 应用的利与弊

这里简单分析一下传统 Web 应用在开发大规模应用和涉及多研发团队协作时遇到的一些困境,以上面案例中的「电商运营平台」举例,对于电商运营而言商品、商家、品牌等都是电商运营平台能力的一部分,而不是独立之间的孤岛。若以传统的前端研发模式进行开发,那么此时有两种项目设计策略:

  1. 将平台内多个系统放置同一个代码仓库维护 ,采用 SPA(Single-page Application) 单页应用模式
  2. 将系统分为多个仓库维护,在首页聚合所有平台的入口,采用 MPA(Multi-page Application)多页应用模式

若采用多个系统放置同一个项目内维护:

  • 优势:
    • 更好的性能
    • 具备局部更新,无缝的用户体验
    • 提前预加载用户下一页的内容
    • 统一的权限管控、统一的 Open API 开发能力
    • 更好的代码复用,基础库复用
    • 统一的运营管理能力
    • 不同系统可以很好的通信
    • SPA 应用特有优势:
  • 劣势:
    • 代码权限管控问题
    • 项目构建时间长
    • 需求发布相互阻塞
    • 代码 commit 混乱、分支混乱
    • 技术体系要求统一
    • 无法同时灰度多条产品功能
    • 代码回滚相互影响
    • 错误监控无法细粒度拆分

采用方案一的劣势非常明显,在日常开发中研发:代码构建半小时以上、发布需求时被需求阻塞、无法局部灰度局部升级、项目遇到问题时回滚影响其他业务、无法快速引进新的技术体系提高生产力,项目的迭代和维护对于研发同学而言无疑是噩梦。

尽管降低了开发体验,如果对项目整体的代码拆分,懒加载控制得当,其实对于使用平台的用户而言体验却是提升的,这一切都归咎于 SPA 应用带来的优势,SPA 应用跳转页面时无需刷新整个页面,路由变化时仅更新局部,不用让用户产生在 MPA 应用切换时整个页面刷新带来的抖动感而降低体验,并且由于页面不刷新的特性可以极大程度的复用页面间的资源,降低切换页面时带来的性能损耗,用户也不会感知他在使用不同平台。

若采用拆分成多个仓库维护

  • 优势
    • 可以以项目维度拆分代码,解决权限管控问题
    • 仅构建本项目代码,构建速度快
    • 可以使用不同的技术体系
    • 不存在同一个仓库维护时的 commit 混乱和分支混乱等问题
    • 功能灰度互不影响
  • 劣势
    • 用户在使用时体验割裂,会在不同平台间跳转,无法达到 SPA 应用带来的用户体验
    • 只能以页面维度拆分,无法拆分至区块部分,只能以业务为维度划分
    • 多系统同灰度策略困难
    • 公共包基础库重复加载
    • 不同系统间不可以直接通信
    • 公共部分只能每个系统独立实现,同一运维通知困难
    • 产品权限无法进行统一收敛

采用方案二在一定程度上提升了开发体验,但却降低了用户体验,研发在日常开发工作中需要使用大量的平台,但是却需要跳转到不同的平台上进行日常的研发工作,整体使用体验较差。体验较差的原因在于将由于通过项目维度拆分了整体「研发中台」这样的一个产品,使各个产品之间是独立的孤岛,系统间相互跳转都是传统意义上的 MPA,跳转需要重新加载整个页面的资源,除了性能是远不如 SPA 应用的并且应用间是没法直接通信,这就进一步增强了用户在使用产品时的割裂感。

背景和意义总结

通过以上两个场景案例,其实可以发现由于 Web 应用在逐步取代传统的 PC 软件时,大规模 Web 应用在面对高复杂度和涉及团队成员广下无法同时保证 DX 和 UX 的困境。传统的分而治之的策略已经无法应对现代 Web 应用的复杂性,因此衍生出了微前端这样一种新的架构模式,与后端微服务相同,它同样是延续了分而治之的设计模式,不过却以全新的方法来实现。

微前端解决方案

上一节总结了微前端出现的背景和意义,并且了解了两种传统 Web 应用的研发模式:SPA(Single-page Application)、MPA(Multi-page Application)在涉及人员广和项目复杂度高的场景下带来的劣势,那么期望能有一种新的架构能同时具备 SPA 和 MPA 两种架构优势,并同时提升 DX(developer experience)和 UX(user experience)呢?

那在理想的情况下,期望能达到,将一个复杂的单体应用以功能或业务需求垂直的切分成更小的子系统,并且能够达到以下能力:

  • 子系统间的开发、发布从空间上完成隔离
  • 子系统可以使用不同的技术体系
  • 子系统间可以完成基础库的代码复用
  • 子系统间可以快速完成通信
  • 子系统间需求迭代互不阻塞
  • 子应用可以增量升级
  • 子系统可以走向同一个灰度版本控制
  • 提供集中子系统权限管控
  • 用户使用体验整个系统是一个单一的产品,而不是彼此的孤岛
  • 项目的监控可以细化到到子系统

那么基于上面理想情况,如何从零设计一套全新的架构用于解决现代 Web 应用在面对企业级系统遇到的困境呢。

微前端的整体架构

那么如何提供一套既具备 SPA 的用户体验,又具备 MPA 应用带来的灵活性,并且可以实现应用间同灰度,监控也可以细化到子系统的解决方案呢?目前在字节跳动内应用的微前端解决方案「Garfish」就是这样的一套方案 ,该解决方案主要分为三层:部署侧、框架运行时、调试工具,采用的是 SPA 的架构。

解决方案整体架构
在这里插入图片描述
在这里插入图片描述

微前端部署平台

部署平台作为微前端研发流程中重要的一环,主要提供了:微前端的服务发现、服务注册、子应用版本控制、多个子应用间同灰度、增量升级子应用、下发子应用信息列表,分析子应用依赖信息提取公共基础库降低不同应用的依赖重复加载。

用于解决微前端中子应用的独立部署、版本控制和子应用信息管理,通过 Serverless 平台提供的接口或在渲染服务中下发主应用的 HTML 内容中包含子应用列表信息,列表中包括了子应用的详细信息例如:应用 id、激活路径、依赖信息、入口资源等信息,并通过对于子应用的公共依赖进行分析,下发子应用的公共依赖,在运行时获取到子应用的信息后注册给框架,然后在主应用上控制子应用进行渲染和销毁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-11fy5aEI-1633760422344)(https://tech-proxy.bytedance.net/tos/images/1633747964449_29257b870a384ac4f92441e877a45d33)]
在这里插入图片描述

微前端运行时

Why not iframe

谈到微前端绕不开的话题就是为什么不适用 iframe 作为承载微前端子应用的容器,其实从浏览器原生的方案来说,iframe 不从体验角度上来看几乎是最可靠的微前端方案了,主应用通过iframe 来加载子应用,iframe 自带的样式、环境隔离机制使得它具备天然的沙盒机制,但也是由于它的隔离性导致其并不适合作为加载子应用的加载器,iframe 的特性不仅会导致用户体验的下降,也会在研发在日常工作中造成较多困扰,以下总结了 iframe 作为子应用的一些劣势:

  • 使用iframe 会大幅增加内存和计算资源,因为 iframe 内所承载的页面需要一个全新并且完整的文档环境
  • iframe 与上层应用并非同一个文档上下文导致
    • 主应用劫持快捷键操作
    • 事件无法冒泡顶层,针对整个应用统一处理时效
    • 事件冒泡不穿透到主文档树上,焦点在子应用时,事件无法传递上一个文档流
    • 跳转路径无法与上层文档同步,刷新丢失路由状态
    • iframe 内元素会被限制在文档树中,视窗宽高限制问题
    • iframe 登录态无法共享,子应用需要重新登录
    • iframe 在禁用三方 cookie 时,iframe 平台服务不可用
    • iframe 应用加载失败,内容发生错误主应用无法感知
    • 难以计算出 iframe 作为页面一部分时的性能情况
  • 无法预加载缓存 iframe 内容
  • 无法共享基础库进一步减少包体积
  • 事件通信繁琐且限制多

基于 SPA 的微前端架构

尽管难以将 iframe 作为微前端应用的加载器,但是却可以参考其设计思想,一个传统的 iframe 加载文档的能力可以分为四层:文档的加载能力、HTML 的渲染、执行 JavaScript、隔离样式和 JavaScript 运行环境。那么微前端库的基础能力也可以参考其设计思想。

从设计层面采取的是基座+子应用分治的概念,部署平台负责进行服务发现和服务注册,将注册的应用列表信息下发至基座,通过基座来动态控制子系统的渲染和销毁,并提供集中式的模式来完成应用间的通信和应用的公共依赖管理,因此 Garfish 在 Runtime 层面主要提供了以下四个核心能力:

  • 加载器(Loader)
    • HTML 入口类型,拆解 HTML Dom、Script、Style
    • JS 入口类型,提供基础 Dom 容器
    • 负责注册平台侧提供的应用列表
    • 负责加载和解析子应用入口资源
    • 预加载能力
    • 解析子应用导出内容
  • 沙箱隔离(Sandbox)
    • 提供代码执行能力,收集执行代码时存在的副作用
    • 提供销毁收集副作用的能力
    • 支持沙箱多实例,收集不同实例的副作用
  • 路由托管(Router)
    • 解决不同应用间的路由不同步问题
    • 提供路由劫持能力,在主应用上管控子应用路由
    • 提供路由驱动能力来拼装完整的平台的能力
  • 子应用通信(Store)
    • 建立通信桥梁
    • 提供共享机制
      在这里插入图片描述

应用生命周期

整个微前端子应用的生命周期基本可以总结为:

  • 渲染阶段
    • 若入口类型为 HTML 类型,将开始解析和拆解子应用资源
    • 若入口类型为 JS,创建子应用的挂点 DOM
    • 主应用通过路由驱动或手动挂载的方式触发子应用渲染
    • 开始加载应用的资源内容,并初始化子应用的沙箱运行时环境
    • 判断入口类型
    • 将子应用存在”副作用“(对当前页面可能产生影响的内容)交由沙箱处理
    • 开始渲染子应用的 DOM 树
    • 触发子应用的渲染 Hook
  • 销毁阶段
    • 若路由变化离开子应用的激活范围或主动触发销毁函数,触发应用的销毁
    • 清除应用在渲染时和运行时产生的副作用
    • 移除子应用的 DOM 元素
      在这里插入图片描述

加载器的设计

加载器的整体设计理念其实与 React-loadable 非常类似,具备以下能力:

  • 异步加载组件资源
  • 可以预加载资源
  • 可以缓存组件资源
  • 缓存组件实例

与组件不同的是微前端作为一种能够将单体应用拆解成多个子应用的架构模式,不同于组件,这些被拆分出去的子应用最好的研发模式是在开发、测试、部署都与宿主环境分离,子应用本身应具备自治能力,那么此时就与 iframe 提供的能力非常类似,iframe 通过加载 HTML 文档的形式加载整个子应用的资源,那么子应用本身就可作为一个独立站点,天然具备独立开发、测试的能力。因此 Garfish 的加载器提供了两种应用入口类型:HTML 类型和 JS 入口类型,但需要注意的是 Garfish 并非像 iframe 一样将其分为了另一个文档流,而是将其与主应用作为同一个文档流处理,用以规避其不再同一个文档流带来的体验感割裂问题。

由于 HTML 入口类型天然具备独立、开发、测试的特性,在微前端整体架构设计中,对于跨团队协作而言,最好的研发模式是能降低其沟通成本,而降低沟通成本的最好方式是不沟通,所以一般项目类型都尽可能的推荐用户使用 HTML 的入口类型。

那么针对 HTML 入口类型的加载器需要做一些什么呢,下面是一张浏览器的渲染过程图:
在这里插入图片描述

针对浏览器的渲染过程也可将其分为:HTML 文本下载、 HTML 拆解为语法树、拆解语法树中具备”副作用的内容“(对当前页面可能产生影响的内容)如 Script、Style、Link 并交由沙箱处理进行后渲染,与一般的子应用不同的是需要子应用提供 provider,provider 中包含了子应用渲染和销毁的生命周期,这两个 Hook 可以应用缓存模式中进一步增强应用的渲染速度和性能。
在这里插入图片描述

沙箱的设计

为什么需要沙箱

其实在过去的 Web 应用中是很少提及到沙箱这一概念的,因为组件的开发一般都会由研发通过研发规范来尽可能的去避免组件对当前应用环境造成副作用,诸如:组件渲染后添加了定时器、全局变量、滚动事件、全局样式并且在组件销毁后会及时的清除子应用对当前环境产生的副作用。

与组件完全不同的是微前端是由多个独立运行的应用组成的架构风格,这些系统可能分别来自不同的技术体系。项目的开发、测试从空间和时间上都是分离的,由于没有 iframe 一样原生能力的隔离很难应用间不发生冲突,这些冲突可能会导致应用发生异常、报错、甚至不可用等状态。

以 Webpack4 JsonpFunction 为例

在 Webpack5 中提供了一个重要的功能就是 Module Federation,随着 Webpack 5 推出 Module Federation ,与 Webpack 4 发生变化的一个重要配置就是 JsonpFunction 属性变为了 chunkLoadingGlobal,并且由原来的默认值 webpackJsonp 变成了默认使用 output.library 名称或者上下文中的 package.json 的 包名称(package name)作为唯一值(webpack.js.org/issues/3940)。

为什么会发生这个转变呢,如果了解过 Webpack 构建产物的一定会知道 Webpack 通过全局变量存储了分 chunk 后的产物,用于解决分包 chunk 的加载问题。由于 Webpack 5 引入 Module Federation 页面中可能会同时存在两个以上的 Webpack 构建产物,如果还是通过是通过同一个变量存储要加载的 chunk ,必然会造成产物之间的互相影响。

通过 Webpack 4 到 Webpack 5 支持 Module Federation 之后可以发现,在一个基础库尚未考虑默认兼容多实例的场景下,贸然将其作为多实例使用很可能会造成应用无法按照预期运行,更为严重的是你以为其正常运行了其实应用已经发生了严重的内存泄漏或不可预知的情况,倘若将 Webpack 构建产物的应用多次动态的在页面中运行,将会发现已经造成严重的内存泄漏,因为 Webpack 会增量的向全局存储 chunk 的变量上挂载模块以及依赖信息,简单来说就是每次执行 Webpack 构建的子应用代码都会向 webpackJsonp 数组 push 大量的数据,最终造成内存泄漏,直至页面崩溃。

沙箱的核心能力

为了保证应用能够稳定的运行且互不影响,需要提供安全的运行环境,能够有效地隔离、收集、清除应用在运行期间所产生的副作用,那应用运行期间主要会产生哪些副作用呢,可以将其分为以下几类:全局变量、全局事件、定时器、网络请求、localStorage、Style 样式、DOM 元素。

在 Garfish Runtime 中的沙箱主要能力也是围绕在这一块的能力建设上,针对子应用可能产生的副作用类型主要分为两类,一类是:静态副作用、另一类则是:动态副作用。这里静态副作用和动态副作用分别指的是什么呢,静态副作用指的是 HTML 中静态标签内容例如:Script 标签、Style 标签、Link 标签,这些内容属于在 HTML 文档流中就包含的,另外一部分副作用属于动态副作用,动态副作用指的是由 JavaScript 动态创建出来的,例如 JavaScript 可以动态创建 Style、动态创建 Script、动态创建 Link、动态执行代码、动态添加 DOM 元素、添加全局变量、添加定时器、网络请求、localStorage 等对当前页面产生副作用的内容。

针对子应用的静态副作用的收集比较简单,Loader 核心模块上已经提供了子应用入口资源类型的分析和拆解,可以从子应用 DOM 树中轻松拆解获取副作用内容,那么对于静态副作用已经可以完成有效的收集、清除,但是尚未具备隔离的能力。动态创建的副作用都是通过 JavaScript 来动态创建的,需要收集到 JavaScript 运行时产生的副作用,并提供副作用的隔离和销毁能力。

沙箱设计的两种思路

在 Garfish 微前端中,如何有效收集、隔离、清除应用的副作用是保障应用能够平稳运行的核心能力之一。沙箱的主要能力也在于能够捕获动态创建的副作用,对应用的副作用进行隔离和清除。

那么如何能够有效的捕获到动态创建的副作用、收集、并隔离呢?目前 Garfish 提供了两种设计思路,一种是快照模式,另外一种是 VM 模式。

快照沙箱

顾名思义,在应用运行前通过快照的模式来保存当前执行环境,在应用销毁后恢复回应用之前的执行环境,用于实现应用间副作用的隔离和清除。类似于 “SL 大法”,通过 save 存储环境,通过 load 加载环境的模式。

代码实现思路
在这里插入图片描述

核心设计思想简述:

  1. 针对每一种副作用提供一个 Patch 类,这个类需要提供 save 和 load 两个方法
  2. Save 对应着该副作用的环境快照存储,Load 对应着销毁该副作用的销毁恢复环境
  3. 并且针对每一种 Patch 还可以存储其在运行期间发生的变化,在优化场景时并不用所有代码,仅恢复执行环境即可
VM 沙箱

通过快照沙箱的最简化的核心实现后可以发现,它的设计理念依赖于整个代码的执行属于线性的过程,即:存储执行环境=>执行具备副作用的代码=>恢复执行环境,但在实际的场景中对于应用的划分并以页面为维度划分,同一个页面可能存在多个应用,所以它的执行顺序并非线性,可能同时存在多个快照沙箱的实例环境,也就是快照沙箱多实例,以下面代码举例:
在这里插入图片描述
通过上面的代码可以发现,在同时运行多个快照沙箱实例时,在代码执行顺序非线性的场景下,并不能有效的收集和处理应用的副作用,也基于此快照沙箱无法使用在非线性呢多实例的场景中,因此也进一步推出了 VM(virtual machine) 沙箱。

维基百科关于 VM 的解释:在计算机科学中的体系结构里,是指一种特殊的软件,可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于虚拟机这个软件所创建的环境来操作其它软件。虚拟机(VM)是计算机系统的仿真器,通过软件模拟具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统,能提供物理计算机的功能。

在 Node 中也提供了 VM 模块,不过不过不同于传统的 VM,它并不具备虚拟机那么强的隔离性,并没有从模拟完整的硬件系统,仅仅将指定代码放置了特定的上下文中编译并执行代码,所以它无法用于不可信来源的代码。

参考 Node 中 VM 模块的设计,以及 JavaScript 词法作用域 的特性,可以设计出 VM 沙箱,不过与传统的 VM 差异也同样存在,它并能执行不可信的代码,因为它的隔离能力仅限于将其运行在一个指定的上下文环境中。

从而得出以下设计
在这里插入图片描述
在这里插入图片描述

隔离环境需要哪些上下文

针对副作用的类型:全局变量、全局事件、定时器、网络请求、localStorage、Style 样式、DOM 元素,分别提供了全新的执行上下文:

  • Window
    • 用于隔离全局环境
  • document
    • 收集 DOM 副作用
    • 收集 Style 副作用,进行处理
    • 收集 Script,继续放置沙箱处理
    • 用于捕获动态创建的 DOM 节点、Style、Script
  • timeout、interval
    • 处理定时器
  • localstorage
    • 隔离 localStorage
  • listener
    • 收集全局事件

新的执行上下文哪里来

新的执行上下文有两个来源,

  • 来源于当前环境
  • 来源于 iframe 的执行环境

由于 iframe 创建后需要需要较多的内存资源和计算资源,而微前端中的 VM 沙箱并不需要一个完全的执行上下文,所以可以基于当前环境。
在这里插入图片描述

快照沙箱和 VM 沙箱能力对比在这里插入图片描述

路由系统的设计

在于现代 MVC 的设计思想,前端框架的设计思想也一直在发生变更,现代 Web 前端框架提供的最经典的能力莫过于将 MVC 中的 Constroller 变为了 Router,目前几乎主流的前端框架都支持路由驱动视图,仅提供一个 Router Map 路由表,无需关注控制任何路由状态即可完成跳转后的路由更新。

通过微前端出现的背景和意义,可以了解到微前端主要是用于解决:应用增量升级、多技术体系并存、构建大规模企业级 Web 应用而诞生的。那么在基于 SPA 的微前端架构中也可以了解到,目前微前端主要是采用应用分而治之 + 动态加载 + SPA 应用的模式来解决大规模应用带来的一系列问题。在以组件为颗粒度的 SPA 应用中组件内部是不需要关心路由的,但是在微前端中主要通过应用维度来拆分,那么拆分的应用也可能是一个独立的 SPA 应用,那么此时主应用与子应用的关系如何编排呢?

微前端应用中理想的路由调度

假设存在一个 Garfish 站点,这个站点它是由主应用+三个子应用构成,主应用的 basename 为 /demo,并存在三个 Tab 分别指向跳转至不同的应用,理想的路由效果:

  1. 在点击 vue-app Tab,跳转至 /demo/vue-app 路由后,分别激活 vue-app 下,为 Vue 类型的 A 应用和 B 应用,并激活 A 应用和 B 应用中的 Home 组件
  2. 点击 React-app Tab 进入到 /demo/react-app 路由后,分别激活 react-app 下,为 React 类型的 C 应用,并激活 C 应用的 Home 组件
  3. 在激活 C 应用的基础上,点击 Detail 按钮,跳转至 /demo/react-app/detail,并激活 C 应用的 detail 组件。
  4. 点击浏览器返回按钮展示,跳转 /demo/react-app/detail,并激活 C 应用的 Home 组件,至此完成浏览器的基本路由跳转能力。
    在这里插入图片描述
不考虑任何路由处理的场景

假设存在一个 Garfish 站点,这个站点它是由主应用+一个子应用构成。由于 Garfish 采用的是 SPA 架构,子应用与主应用所处于同一个执行上下文,子应用的路由原样反应在主应用上。
在这里插入图片描述

那么此时分别跳转到:/home、/detail路由会发现哪些问题呢?

  • 假定跳转的方法可以同时触发主子应用路由更新,主应用路由和子应用路由会同时发生抢占情况,后渲染的组件会覆盖先渲染的路由组件
  • 在触发路由跳转方后,只有主应用视图触发刷新、只有子应用视图刷新、或都不刷新
    • 「视图的路由状态维护在框架内部」,通过原生跳转无法触发视图更新

此时当分别跳转到:/home、/detail、/test 路由时分别触发对应的组件视图,但是倘若子应用路由中也存在 /detail视图呢,由于应用的开发采用分治的模式,应用的开发从空间和时间上都是分离的,无法保证应用间的路由不发生路由抢占的情况。

「通过 history 路由跳转无法保证应用能够触发视图更新」,在通过 history api 进行路由跳转时,是无法触发应用视图更新,假设存在一个 React 应用 A,存在一个组件视图 Test,分别通过 React 提供的路由方法跳转和原生的路由跳转进行观察:
在这里插入图片描述

Hash 和 History 路由模式

目前主流的 SPA 前端应用基本上都支持两种路由模式,一种是:hash 模式、另一种则是 History 路由模式,两者的优劣和使用并不在本文的讨论范围之内,这里仅做在微前端这种分离式开发模式下的介绍,在微前端这种分离式 SPA 应用开发的模式下该选择哪种路由模式,以及多 SPA 应用下他们的路由应该如何编排:

假设站点地址为:http://garfish.bytedance.net

正常路由情况

  • 主应用 history 模式、子应用 history 模式

    • 主应用(basename: /example):
      • 主应用所有路由基于:http://garfish.bytedance.net/example例
      • 例如跳转到:/appA,http://garfish.bytedance.net/example/appA/子
    • 子应用(basename: /example/appA):
      • 子应用所有路由基于:http://garfish.bytedance.net/example/appA
      • 跳转到子应用的 /detail 页,http://garfish.bytedance.net/example/appA/detail
    • 特点:
      • 当主子应用分别为 history 模式时,子应用的路由基于主应用基础路由并带上自己的业务路由
      • 路由同步到主应用路由上,通过 子应用 scope 命名空间隔离(子应用 A,提供 appA 的 scope)主应用和其他应用的路由冲突,并将子应用
      • 路径符合用户和开发者认知和理解
      • 支持嵌套层级使用,并继续通过 scope 的命名空间保证路由可读
    • \
  • 主应用 history 模式、子应用 hash 模式

    • 主应用(basename: /example):
      • 主应用所有路由基于:http://garfish.bytedance.net/example
      • 例如跳转到:/appA,http://garfish.bytedance.net/example/appA/
    • 子应用(basename: /example/appA):
      • 子应用所有路由基于:http://garfish.bytedance.net/example/appA
      • 从主应用:http://garfish.bytedance.net/example/appA,跳转到子应用的 /detail 页,http://garfish.bytedance.net/example/appA#/detail特
    • 特点:
      • 在一定程度上具备主子应用都为 history 模式的优势,不支持嵌套层级使用
      • 目前多数框架都不支持以 hash 值作为 basename
      • 可读性尚可

异常路由情况

  • 主应用 hash 模式、子应用 history 模式

    • 主应用(basename: /example):
      • 主应用所有路由基于:http://garfish.bytedance.net/example
      • 例如跳转到:/detail,http://garfish.bytedance.net/example#/appA
    • 子应用(basename: /example#/appA):
      • 子应用所有路由基于:http://garfish.bytedance.net/example#/appA
      • 跳转到子应用的 /detail 页,http://garfish.bytedance.net/example/detail#/appA
    • 特点:
      • 「路由混乱」,不符合用户和开发者直觉
      • 目前多数框架都不支持以 hash 值作为 basename
  • \

  • 主应用 hash 模式、子应用 hash 模式

    • 主应用(basename: /example):
      • 主应用所有路由基于:http://garfish.bytedance.net/example
      • 例如跳转到:/detail,http://garfish.bytedance.net/example#/appA
    • 子应用(basename: /example#/appA):
      • 子应用所有路由基于:http://garfish.bytedance.net/example#/appA
      • 跳转到子应用的 /detail 页,http://garfish.bytedance.net/example#/detail
    • 特点:
      • 「路由混乱」,不符合用户和开发者直觉
      • 目前多数框架都不支持以 hash 值作为 basename
      • 可能与主应用或其他子应用发生路由冲突
Garfish Router 如何处理路由

通过上面理想的路由模式案例发现,微前端应用拆分成子应用后,子应用路由应具备自治能力,可以充分的利用应用解耦后的开发优势,但与之对应的是应用间的路由可能会发生冲突、两种路由模式下可能产生用户难以理解的路由状态、无法激活不同前端框架的下带来的视图无法更新等问题。

目前 Garfish 主要提供了以下四条策略

  • 提供 Router Map,减少典型中台应用下的开发者理解成本
  • 为不同子应用提供不同的 basename 用于隔离应用间的路由抢占问题
  • 路由发生变化时能准确激活并触发应用视图更新
Router Map 降低开发者理解成本

在典型的中台应用中,通常可以将应用的结构分为两块,一块是菜单另一块则是内容区域,依托于现代前端 Web 应用的设计理念的启发,通过提供路由表来自动化完成子应用的调度,将公共部分作为拆离后的子应用渲染区域。
在这里插入图片描述
在这里插入图片描述

自动计算出子应用所需的 basename

当应用处于激活状态时,根据应用的激活条件自动计算出应用所需的基础路径,并在渲染时告诉框架,以便于应用间路由不发生冲突。
在这里插入图片描述

如何有效的触发不同应用间的视图更新

目前主流框架实现路由的方式并不是监听路由变化触发组件更新,让开发者通过框架包装后的 API 进行跳转,并内部维护路由状态,在使用框架提供 API 方法发生路由更新时,内部状态发生变更触发组件更新。

由于框架的路由状态分别维护在各自的内部,那么如何保证在路由发生变化时能及时有效的触发应用的视图更新呢,答案是可以的,目前主要有两种实现策略:

  1. 收集框架监听的 popstate 事件
  2. 主动触发 popstate 事件

因为目前支持 SPA 应用的前端框架都会监听浏览器后退事件,在浏览器后退时根据路由状态触发应用视图的更新,那么其实也可以利用这种能力主动触发应用视图的更新,可以通过收集框架的监听事件,也可以触发 popstate 来响应应用的 popstate 事件

基于「现代 Web 框架」的微前端最佳实践

微前端作为一种全新的 Web 应用类型,不同于以往传统的 Web 应用开发,微前端需要采用主子应用分治的开发模式后带来了一系列新的挑战,这些挑战包括但不限于:主子应用开发调试、普通 Web 应用如何快速变为微前端应用、如何支持微前端应用 SSR、主子应用数据通信触发视图更新。Modern.js 作为 Garfish 上层的现代 Web 框架,能够很好的解决这些问题,并提供开箱即用的开发体验。

微前端应用的调试开发

由于微前端应用采用分治的开发策略,应用间的维护和开发可能在时间和空间上都是分离的,那么在开发环境时启动整个微前端项目的所有主子应用,是一个并不明智的策略,不仅需要 clone 其他仓库并完成应用的运行,还要保证其代码的时效性。Modern.js 提供了更优的的策略:

  • 某些子应用需要更新时
    • 主应用线上环境
    • 需要开发的子应用线下环境
    • 不需要开发的子应用上线
  • 主应用需要更新时
    • 主应用线下环境
    • 所有子应用线上环境

通过以上更优的调试策略,可以保证开发者仅运行自己的关注的应用即可。那么如何达到这种更优的,可以采用应用列表的下发模式,框架运行时加载下发的应用列表,在开发主应用时拉取线上的应用列表,在开发某个子应用时代理代理列表中的资源为子应用的列表。

传统 Web 应用支持微前端模式

通过微前端运行时章节可以发现传统 Web 应用与微前端应用间进行切换成本并不高,但需要研发关注应用的路由的调度、应用的生命周期导出、额外的构建配置、应用通信数据触发视图更新,微前端模式应用和传统 Web 应用间如何进行切换都存在一定的学习和理解成本。

在 Modern.js 中作为上层框架集成了 Garfish,原生支持微前端应用,可以通过简单配置即可完成微前端应用类型的转换,帮助用户快速搭建应用基础结构,以降低其学习成本,快速生成微前端应用。

微前端应用如何支持 SSR

微前端作为一种全新的架构模式,其分治设计模式除了带来的诸多优点外,但与之对应的是引入了新的问题,如何支持传统 Web 应用提供的 SSR 能力,由于微前端采用了分治的开发模式,应用拆分成了多个子应用,那么需要实现整体应用的 SSR 能力,则需要与具体的 Web 框架相结合,通过制定微前端应用的加载规则,达到微前端应用也能有效的实现 SSR 能力。

Modern.js 作为 Garfish 的上层框架,提供更开箱即用的上层能力 ,并解决了以上微前端不同于传统 Web 应用开发后带来的弊端,文末有关于 Modern.js 的发布预告,可以了解并关注。

微前端的优点

  • 适用于大规模 Web 应用的开发
  • 更快的开发速度
  • 支持迭代可开发和增强升级
  • 拆解后的部分降低了开发者的理解成本
  • 同时具备 UX 和 DX 的开发模式

微前端的缺点

  • 复杂度从代码转向基础设施
  • 整个应用的稳定性和安全性变得更加不可控
  • 具备一定的学习和了解成本
  • 需要建立全面的微前端周边设施,才能充分发挥其架构的优势
    • 调试工具
    • 监控系统
    • 上层 Web 框架
    • 部署平台

何时使用微前端

  • 大规模企业级 Web 应用开发
  • 跨团队及企业级应用协作开发
  • 长期收益高于短期收益
  • 不同技术选型的项目
  • 内聚的单个产品中部分需要独立发布、灰度等能力
  • 微前端的目标并非用于取代 iframe
    • 应用的来源必须可信
    • 用户体验要求更高

总结

微前端概念的出现是前端发展的必然阶段,PC 互联网转向移动互联网时代时,PC 的场景并未完全被消灭,反而转向了衍生出了更多沉浸感更高、体验感更强的应用,与之对应的应该是出现新的架构模式来应对这些应用规模的增长。

微前端也并非银弹,采用微前端后复杂度并未凭空消失,而是由代码转向了基础设施,对架构设计带来了更大的挑战,并且在新的架构下需要设计并提供更多的周边工具和生态来助力这一新的研发模式。

本文更多的是从背景和设计层面讲清楚微前端解决方案应具备哪些能力,以及核心模块的设计。每一部分并未包含过于详细的细节,如果想要了解「微前端运行时」详细设计,可以通过 https://github.com/modern-js-dev/garfish 仓库了解细节。

参考

  • 如何设计微前端中的主子路由调度:https://mp.weixin.qq.com/s/TAXP7ipDdtb2Jb-L3QHszA
  • 如何取巧实现一个沙箱:https://mp.weixin.qq.com/s/Mg3fU0WvZUQnlWHdxc-b5A
  • 微服务架构及其最重要的 10 个设计模式:https://www.infoq.cn/article/kdw69bdimlx6fsgz1bg3
  • single-spa:https://github.com/single-spa/single-spa

Modern.js 开源预告

Modern.js 和 Garfish 都是字节跳动 Web Infra 发起的「现代 Web 工程体系」开源项目,Modern.js 原生支持微前端,在 Garfish 基础上提供了完整的微前端最佳实践。

Modern.js 计划在 10 月 14 号发布 1.0.0 版和上线文档站,欢迎关注和参与。

字节技术年货之实践篇:微服务架构演进、全链路压测、埋点数据治理…… 动手点关注干货不迷路????祝全天下的开发者朋友,新年职业发展虎啸风生,新年工资增速如虎添翼,新年爱情运势虎跃龙骧,新年代码事故下虎口逃生,虎!虎!虎!社区里缺少的不是架构图,而是可供参考复... 阅读详情

相关推荐

字节跳动前端面试全流程与高频考点解析

前端开发作为现代Web应用的核心技术栈,其知识体系涵盖从基础语言特性到工程化实践的完整链路。理解JavaScript闭包、事件循环等核心原理是构建复杂应用的基础,而React Fiber架构、Hooks机制等框架特性则体现了前端技术的演进方向。在工程化层面,Webpack优化与性能监控体系的搭建能显著提升应用质量,这些技术通过字节跳动等头部企业的面试实践得到验证。本文以字节跳动前端面试流程为样本,详解包括作用域链、微前端架构、虚拟列表优化等高频考点,为开发者提供从基础知识到系统设计的完整备战方案。

weixin_29001655的博客 314

你怎么会懂?字节跳动是如何落地微前端的?

微前端的出现的背景和意义 微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。 微前端诞生在两个大的背景下,在提倡拥抱变化的前端社区可以看到新的框架、技术、概念层出不穷,并且随着 Web 标准的演进,前端应用已经具备更好的性能、更快的开发效率。但随着而来的是应用的复杂程度更高、涉及的团队规模更广、更高的性能要求,应用复杂度已经成为阻塞业务发展的重要瓶颈。 微前

m0_68850571的博客 874

字节跳动商业化前端社招面试全流程复盘与准备指南

前端面试不仅考察基础概念的背诵,更看重对原理的深入理解与工程实践能力。从JavaScript闭包的内存泄漏机制,到浏览器缓存策略(Cache-Control/ETag)的优先级,再到前端工程化中的CI流程设计,这些核心知识点共同构成了大厂面试的高频考察维度。在实际的社招场景中,候选人需要将技术原理与业务价值结合,例如通过GraphQL优化数据看板首屏性能、基于qiankun实现微前端方案落地。一份完整的面试复盘能帮助求职者系统梳理项目经验与算法能力。本文记录了字节跳动商业化前端团队的完整面试流程,涵盖技术面

weixin_29016315的博客 292

字节跳动的前端工程化实践

前言首先分析了当前前端开发领域的趋势和所面临的新挑战,包括涉及平台的增多、业务复杂度的增加以及前端团队规模的增大等。接着,分享了字节跳动针对这些挑战采取的新实践,包括 Monorepo 工具的使用、自研的 Bundler 和 Build System 工具的建设以及微前端的工程化实践。最后,介绍了字节跳动自研的诊断工具,用于有效防止应用劣化,并总结了各个工具在实践中的落地情况。整篇文章展示了字节跳...

React_Community的博客 509

字节跳动15个前端开源项目全景:构建、组件、可视化与跨端选型指南

前端工程化的核心命题,是在构建效率、研发规范与多端交付之间找到平衡。以 Rspack 为代表的 Rust 打包器,通过多线程并行与 webpack 配置兼容,显著压缩冷启动和热更新时间;Rstack 体系(Rsbuild、Rslib、Rspress)进一步把应用、库、文档站统一到同一构建内核,降低工具链切换成本。组件库选型则围绕 Arco Design、Semi Design 等设计系统展开,需权衡中后台数据密度、主题换肤与 React/Vue 技术栈;数据可视化中 VChart、VTable 面向复杂图表

weixin_33812433的博客 334

字节商业化前端面经:基础考点与场景题全复盘

前端面试不仅考察JavaScript闭包、事件循环等基础原理,更看重技术在实际业务场景中的落地能力。在商业化前端领域,广告投放、数据看板等场景对渲染性能、数据上报链路、多端兼容性提出了更高要求。虚拟滚动通过只渲染可视区域DOM来优化长列表性能,微前端架构则解决多团队技术栈协同问题,而埋点方案设计直接关系到业务数据的精确采集。字节跳动商业化前端岗位的面试正是围绕这些核心点展开,通常涵盖JS基础、场景设计、算法手写与项目深挖等环节。本文基于真实面试过程,逐题复盘考察意图与追问套路,帮助求职者构建从知识到业务的前

weixin_29708043的博客 342

前端微服务在字节跳动的打磨与应用

本文讨论了微前端在字节跳动的应用情况,内容主要分析了微前端具体落地的步骤和两年来的使用情况。其中分析的部分主要讲到一些实际问题和我们的应对,落地情况强调了实现的过程。特别讲到很多在我们观...

字节跳动技术团队官方博客 1万+

Verilog除法器(32位无符号+带符号)

32位无符号除法器设计 恢复余数法: 恢复余数除法的基本思路是从“部分余数”中减去除数,如果结果为负(不够减),则恢复原来的部分余数,商0. 寄存器使用: reg_r 存放被除数a reg_b 存放除数b reg_r 存放余数(初始清零) 具体做法: 做减法时,减数是reg_b中的除数,被减数是reg_r中的余数左移一位,最低位由reg_q(被除数)的最高位补充。为了能够判断相减结果的正...

qq_38600065的博客 1万+

微前端解决方案

用于解决微前端中子应用的独立部署、版本控制和子应用信息管理,通过 Serverless 平台提供的接口或在渲染服务中下发主应用的 HTML 内容中包含子应用列表信息,列表中包括了子应用的详细信息例如:应用 id、激活路径、依赖信息、入口资源等信息,并通过对于子应用的公共依赖进行分析,下发子应用的公共依赖,在运行时获取到子应用的信息后注册给框架,然后在主应用上控制子应用进行渲染和销毁。

zz130428的博客 902

OpenCV视频防抖算法实现与优化指南

本文详细解析了如何使用OpenCV实现视频防抖算法。通过特征点检测与光流跟踪估算帧间运动,利用滑动平均平滑运动轨迹,并应用仿射变换重塑视频帧。文章提供了完整的代码实现、参数调优指南及黑边处理等优化技巧,帮助开发者高效处理手持拍摄等场景下的视频抖动问题。

weixin_29062823的博客 339

用VSCode搭建Vue.js开发环境及Vue.js第一个应用

用VSCode搭建Vue.js开发环境及Vue.js第一个应用

m0_61961937的博客 1万+

微前端调研及简析SPA实现原理

最近对微前端讨论很多,梳理下自己对微前端的理解以及业内的一些微前端尝试反馈。第零部分:自己对微前端理解第一部分:基于Single-SPA微前端的一些demo第二部分:Single-SPA微前端实现原理简析第三部分:微前端业内一些总结第零部分:自己对微前端理解在判断自己项目是否需要使用微前端前,只要记住一句话即可:杀鸡焉用牛刀。如果项目很简单,请不要没有困难创造困难;如果项目太大,受够了iframe...

didi_cloud的博客 413

解决Word中英文混排的5个常见排版问题(含单词换行、间距调整等)

本文详细解析Word中英文混排的5大常见排版问题,包括单词换行、间距调整、标点混用等,提供专业解决方案。通过设置段落格式、调整兼容性选项和使用字体组合等技巧,帮助用户轻松解决文档排版难题,提升专业性和阅读体验。

vim8coder的博客 1336

ajax请求是宏任务还是微任务_前端微服务在字节跳动的打磨与应用

本文讨论了微前端在字节跳动的应用情况,内容主要分析了微前端具体落地的步骤和两年来的使用情况。其中分析的部分主要讲到一些实际问题和我们的应对,落地情况强调了实现的过程。特别讲到很多在我们观念里面务必要提供的微前端基石,这些方面作为基础设施几乎是使用微前端的必要和前提条件。传统前端业务通常会根据业务线集成在一个站点上,随着业务复杂度上升,包体积会迅速变的过大。为了适应这个变化往往需要更多的开...

weixin_39609650的博客 433

【RT-DETR改进涨点】ResNet18、34、50、101等多个版本移植到ultralytics仓库(RT-DETR官方一比一移植)

本文是本专栏的第一篇改进,我将RT-DETR官方版本中的ResNet18、ResNet34、ResNet50、ResNet101移植到ultralytics仓库,网上很多改进机制是将基础版本的也就是2015年发布的ResNet移植到ultralytics仓库中,但是其实RT-DETR实验的ResNet和其是有很大区别的在其中有很多细节上的不同(文章中我也会列出举例)

Snu77的博客 7221

PCL RANSAC拟合分割圆柱体模型【2026最新版】

使用RANSAC拟合分割圆柱的原理介绍、官网示例代码实现以及一个可视化拟合出来的圆柱的具体应用案例。博客长期更新,最近一次更新时间为:2026年1月28日。

点云侠的博客 40万+

React+TS开发者必看:揭秘字节跳动广告前端团队的技术栈与飞鱼CRM架构设计

本文深度解析字节跳动广告前端团队采用React+TypeScript技术栈的实践哲学,重点介绍飞鱼CRM的架构设计。从类型安全与性能优化到微前端架构演进,再到数据可视化与Serverless落地,展现了大型商业项目的工程化解决方案。特别探讨了Webpack 5 Module Federation在微前端中的应用,为React+TS开发者提供宝贵参考。

opencv7vision的博客 652

uniapp - 微信支付报错 {“errMsg“:“requestPayment:fail:[payment微信:-1]General errors“} 详细解决办法,uniApp使用微信支付时报错

uniapp,微信支付报错,uniapp微信小程序 H5网页 安卓苹果ios真机调试时报错,uniapp真机测试微信支付报错如何解决,调用微信支付出现报错{"errMsg":"requestPayment:fail:[payment微信:-1]General errors"},微信支付提示错误,uniapp进行微信支付时报错怎么解决,如何解决微信支付报错,uniapp微信支付报错fail,uni-app真机测试ios上报错,uniapp真机测试安卓手机第一次支付成功第二次就报错了,uniapp微信支付调用第

✅ 精选推荐文章 4133

字节前端面试全解析:从基础到系统设计

前端开发作为现代Web应用的核心技术,其核心机制如事件循环、闭包、异步编程等是构建复杂应用的基础。理解浏览器工作原理和渲染性能优化技术,能够显著提升页面加载速度和用户体验。在实际工程实践中,缓存策略设计和前端监控系统是实现高性能应用的关键技术。本文通过字节跳动前端面试的深度复盘,详细解析了从JavaScript核心原理到复杂系统设计的全流程,涵盖了事件循环、React Fiber架构、微前端设计等高频考点,为前端开发者提供了全面的技术提升路径。

weixin_34198762的博客 423

DeiT详解:知识蒸馏的Transformer

DeiT模型(8600万参数)仅用一台 GPU 服务器在 53 hours train,20 hours finetune,仅使用 ImageNet 就达到了 84.2 top-1 准确性,而无需使用任何外部数据进行训练,性能与最先进的卷积神经网络(CNN)可以抗衡。其核心是提出了针对 ViT 的教师-学生蒸馏训练策略,并提出了方法,使得在视觉领域训练得又快又好。

走在深度学习前沿的小宋 2636

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

社招上岸字节:一个Vue工程师如何用AI思维搞定三轮技术面(附完整复盘录音技巧)

本文分享了一位Vue工程师如何通过AI思维成功通过字节跳动三轮技术面试的完整经验。从技术栈盘点、AI能力速成到面试实战策略,详细介绍了如何将传统前端技能与AI应用结合,最终斩获offer。文章包含实用的面试技巧和AI工具使用心得,适合准备社招的前端开发者参考。

weixin_29008691的博客 125

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

字节是如何落地微前端的

???? 加个关注,后续上新不错过~本文内提及的 Garfish 微前端解决方案已开源:https://github.com/modern-js-dev/garfish(目前的 Garfish ...

frontend_frank的博客 572

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

上一篇: Android性能优化之Android 10+ dex2oat实践
下一篇: 火山引擎MARS-APM Plus x 飞书 |降低线上OOM,提高App性能稳定性
字节跳动终端技术
博客等级 码龄7年 206粉丝 52原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值