乐趣区

关于android:Shopee在React-Native-架构方面的探索

1. 背景

React Native(下文简称 RN)是混合应用领域风行的跨端开发框架。RN 非常适合灵便多变的电商畛域业务,因为 RN 是基于客户端渲染的技术,所以相较于 H5 页面,它在用户体验方面有肯定劣势。

随同着 Shopee 业务的飞速发展,咱们 App 中的 RN 代码量增长得十分快,呈现了构建产物体积过大、部署工夫太长、不同团队依赖抵触等问题。为了应答这些痛点,咱们摸索了去中心化的 RN 架构,并联合该模型自研了零碎(Code Push Platform,简称 CPP)和客户端 SDK,笼罩了多团队的开发、构建、公布、运行等一系列 RN 研发周期。通过近三年的迭代,现已接入多款公司级外围 App。

Shopee 商家服务前端团队打造了多款商家端利用,大部分用户是商家服务人员,他们对业务零碎高可用和问题及时反馈有着很高的要求,从而也推动咱们对 React Native 的架构有了更高的要求。

本文会从倒退历史、架构模型、零碎设计、迁徙计划四个方向逐个介绍咱们如何一步步地满足多团队在简单业务中的开发需要。

2. 倒退历程

随着业务高速倒退,咱们的 RN bundle 个数飞速减少,App 个数也达到近十个。整个 RN 我的项目在开发模型、部署模型和架构模型三个维度都产生了变动,从单团队倒退成多团队,从一个 bundle 倒退成多个 bundle,从中心化架构倒退成为去中心化,最终倒退成为每个团队的业务代码能够独立地开发、部署、运行。

整个倒退历史分为 4 个阶段,别离是单 bundle 集中开发模式、单 bundle 多业务组开发模式、多 bundle 中心化公布模式、多 bundle 去中心化公布模式。

2.1 第一阶段:单 bundle 集中开发模式

最后的 RN 整体技术架构绝对简略。因为过后业务状态不算简单,为了满足独立团队在同一个代码仓库当中的开发流程,整个公布流程是基于 CDN 的更新公布,并且应用配置文件记录 RN bundle 文件的版本以及下载地址,以此进行资源管理。整个公布的产物有两个,一个是 RN 资源包,一个是用于资源版本治理的 JSON 配置文件。

每次 RN 资源在实现构建后,这两种构建产物会被搁置在动态资源目录下。App 在特定的工夫节点(例如 App 重启等)会主动拉取配置文件查看资源更新状态,而后再从 CDN 拉取 RN 动态资源。在下一次关上页面的时候,App 会加载最新的页面内容。

随着业务倒退,越来越多业务团队冀望应用 RN 技术栈开发业务,这种状况让已有架构产生扭转,咱们天然地产生了“多个业务组多个代码仓库”的想法。

2.2 第二阶段:单 bundle 多业务组开发模式

针对上述问题,多业务组的研发解决方案是 host-plugin 这种模式。

host 用于治理公共依赖和通用逻辑,它将 React、React Native、Shopee RN SDK 等通过一个独立的仓库治理起来,保障了非凡 RN 依赖的“singleton”(单例模式)条件,防止了局部客户端组件的重叠依赖,而这种重叠依赖是 RN 官网不容许的。

一个 host 对应着多个 plugin 仓库,业务代码仓库则是被看作为一个插件(plugin),以插件的模式接入主利用当中。业务团队能够按本人的编码标准来治理这个仓库。每个插件仓库会被视为 host 我的项目的 npm 依赖,它的构建是一个集中公布的流程。所有代码都会集成在 host 我的项目当中执行构建脚本。这种模式满足超级 App 的要求。

与此同时,host-plugin 的模式也带来了一个“难题”,业务倒退使得 RN 产物体积逐步变大,过大的产物会影响客户端的解压效率和 RN 容器加载 JS 时长。

2.3 第三阶段:多 bundle 中心化架构模式

针对 RN 产物体积过大的问题,咱们利用构建工具将打包产物细分成多个 bundle,这一优化是十分有必要的,咱们称它为“分包”。host 我的项目对应的是公共包,plugin 我的项目对应的是业务包。

整个构建产生在 host 我的项目,我的项目的模式还是“集中构建”和“集中公布”。多 bundle 产物将会公布到零碎当中,客户端将拉取热更新的内容。客户端会按需加载对应的 bundle,RN 容器单次加载耗费的资源大大减少,解决了效率问题。

然而它的毛病也很显著。随着业务团队的变大和业务内容的扩张,多 bundle 中心化公布模式同样也具备四个弊病:

  • 针对 RN 的运行时 ,即便分包技术使得产物拆散,然而它们还是运行在同一个 JSContext 当中,这种状况可能会导致依赖抵触和环境变量净化;
  • 在开发调试的过程中 ,我的项目重依赖于 host 我的项目,每次存在着代码变更,须要从新加载很多内容,让开发调试不太敌对;
  • 在我的项目构建的过程中 ,打包速度受到 plugin 个数的影响,特大型利用甚至须要 50 分钟执行一次构建,过长的构建耗时重大影响了公布效率;
  • 在部署公布的过程中 ,host 我的项目维护者负责整个 App,每个业务组不能独立公布,公布工夫会绑定在一起。当呈现 live issue 的状况,开发者须要破费大量的沟通老本,且只能整体回滚。

2.4 第四阶段:多 bundle 去中心化架构模式

去中心化 React Native 架构模式与网页的“微前端”或者客户端的“微利用”的概念相似,满足了多业务团队独立开发部署,可能在同一个 App 各模块独立运行。它涵盖了开发、构建、公布、运行等多个方面。该模型解决了下面所说的四个弊病,并针对整个研发体系有了全面的降级,长处有:RN 运行时的互不烦扰,开发调试的高效,构建公布的独立性。

下文会重点介绍我的项目的去中心化 RN 架构和零碎设计,以及咱们是怎么做到灵活性和稳定性的均衡的。

3. 去核心的 RN 架构模型

简略来说,去中心化的 RN 公布模型波及到四个局部:独立的 JS 运行时;独立的开发流程;独立的构建流程;独立的公布流程。在这四个关键环节的帮忙下,每个团队按本人的节奏掌控 RN 的研发流程。

3.1 独立 JS 运行时

独立运行时(多 JSContext,执行上下文环境)的呈现是去中心化架构的最大特色。独立运行时是对独立公布的完满保障,将 RN 运行代码依照 plugin 维度进行隔离,它能够无效防止不同业务之间的变量抵触以及依赖抵触问题,即“plugin A”的公布相对不会影响到“plugin B”。

它的设计次要蕴含以下三点:

  • 提前创立 JSContext 且预加载公共包;
  • 进入 plugin 的页面,SDK 会查看对应的 JSContext 是否已被实例化。如果曾经被实例化,就间接应用,否则从 JSContext Pool 选取一个独立的上下文,加载执行业务包,各个 plugin 之间运行时是隔离的;
  • 退出业务页面时,该 JSContext 不会立刻销毁,而是放入一个缓存池,使得反复进入该业务能够取得极致体验。

安装 JSContext 的容器能够是线程或者过程。为了防止它频繁创立和回收,咱们要保护缓存池且尽可能地复用现有的 JSContext。

这里咱们采纳 Least Frequently Recently Used(简称 LFRU)的策略。当刚退出的利用被从新关上,该 JSContext 会被从新启用。这样,咱们可能节俭 85% 的首屏渲染时长。缓存个数治理是可配置的,业务方能够依据利用的规模作为正当的预估。当该 RN 页面还在应用中,即便超出预估数,该上下文也不会立刻被回收,该设计无效地保障页面的可用性。

3.2 开发流程

上文提及 RN 我的项目的调试效率问题,它会随着业务代码的体量增多,代码调试效力也会随之降落。每个开发者的效率问题间接影响到大家的“幸福感”。相比之下,RN 去中心化公布则是针对开发流程做了特定的优化。

随着独立运行时环境的呈现,RN 进入调试的时候,客户端能够做到只加载一个 plugin 到对应的 JSContext 中,其余 plugin 则采纳内置 cache。

这样做有两个益处:一是保障了服务启动范畴的最小化,保障了代码热加载的效率;二是确保开发和构建两种流程的一致性,这样会让一些问题在开发阶段提前裸露进去,比方 babel 插件缺失导致的编译问题。这样的“去中心化”的开发流程进步了 RN 调试效率。

3.3 构建流程

随着业务倒退,某 App 的 RN plugin 数有 4 个,旧构建流程受到 plugin 个数的影响,集中构建时长超过 20 分钟。而采纳去中心化 RN 架构,构建时长不再随 plugin 个数增长,只和该 plugin 代码量无关,稳固在 5 分钟左右。

新架构也是同样基于 host-plugin 模型,独立仓库的隔离让每个团队有自在的倒退空间。思考到在利用内的根底 Native 依赖是对立的,host 我的项目仅用来治理对立的公共依赖。我的项目须要优先将 common bundle 构建实现,零碎会记录公共包中的依赖信息。当每个 plugin 我的项目进行构建的时候,构建工具会剔除掉公共包依赖信息,并实现业务包的构建。每个业务包的构建产物都是独立地寄存于零碎当中。零碎具备独立回滚、独立公布、独立灰度的能力。

这样的益处在于构建工作的最小粒度化,每个 plugin 的构建不会引起整个我的项目的从新构建,做到真正意义的“按需打包”。

3.4 公布流程

RN 的构建和公布是两个独立的流程。这也意味着 bundle 的构建环节和公布环节齐全解耦,公布工夫点也能够由每个业务团队公布负责人灵便安顿。每个业务组对本人的代码品质负责,灵便地把控本人的发版本节奏,不会影响其余团队线上业务。公布流程外面蕴含了全量公布、联结公布、灰度公布、回滚等操作,后续章节会具体介绍如何保障公布的稳定性。

4. 零碎设计

对于简单的大型项目来说,简略的热更新流程已无奈满足多业务组协同单干,咱们须要一个功能完善、性能优越、操作敌对的热更新零碎来满足简单业务的倒退。Code Push Platform 由 Node.js 编写,搭配零碎从属的命令行工具和客户端 SDK。

为了满足该零碎在多业务团队的运作,整个零碎从性能角度能够划分为三大部分,别离是:

  • 多团队权限管控;
  • bundle 生命周期治理;
  • 零碎效力晋升。

其中,零碎效力晋升性能又细分为:

  • 增量差分;
  • 多场景入口体积优化;
  • 一站式多环境整合。

4.1 多团队权限管控

零碎除了记录每次构建操作,更重要的是工作流程的去中心化,每个 plugin 的权限是隔离的。每个负责人只能在零碎外部操作,plugin 1 的负责人只能触发相干的构建和公布,没法看到 plugin 2 的操作状况。零碎通过严格的权限管控来标准所有公布流程,保障了我的项目的可控性。

React Native 去中心化公布的设计指标是节俭不同团队之间的沟通老本。零碎会限度他们的构建和公布的动作,各自的公布不会相互烦扰。

权限的治理呈树状构造,一个 App 对应着一个我的项目,我的项目负责人默认是 App 团队的我的项目负责人。创立一个全新的插件等零碎操作须要我的项目负责人审批。一个 App 蕴含有多个 plugin,每个 plugin 负责人默认是相应的业务团队负责人,他有权限调配公布和构建的权限。

4.2 bundle 生命周期治理

4.2.1 客户端版本控制

RN 有别于网页利用,它对客户端有着严密的依赖关系。在客户端底层依赖没有变动的状况下,个别状况下开发者能够通过热更新进行 RN 代码的更新。然而遇到重大的更新,例如 React Native 的版本从 59 降级到 63,不仅仅须要 JavaScript 侧改变,客户端也要降级版本且没法持续向下兼容。从技术层面看,它是难以避免的。这种客户端无奈向下兼容的状况,被称为“断层”。

零碎会提供客户端版本控制的能力。当重大变更呈现时,App 负责人应该在零碎上新建一个“断层信息”,版本号的范畴是从最低 App 兼容版本到最高 App 版本。在这个区间客户端能力拉取到该断层的最新 RN 资源。

如下表所示,大于等于 2.5.0 版本的 App 拉取的是 105 版本 RN 包;在 2.0.0 至 2.5.0 版本拉取到 103 版本 RN 包;在 1.0.0 至 2.0.0 版本拉取到 100 版本 RN 包。

这种措施可能无效防止潜在危险。而最新的需要只会在最新断层上线,旧的断层只做线上问题修复。毕竟是两套代码,代码的保护有老本,随着用户更新至最新版本,该当逐步淘汰掉旧断层。

4.2.2 灰度和回滚

公布流程外面蕴含了全量公布、灰度公布、回滚等操作。对于大型需要,全量上线会带来潜在危险。一般来说,优先针对局部用户投放新版本,公布负责人能够依据指定用户和特定范畴进行灰度公布,逐渐扩充灰度公布范畴,直至转到全量。当发现重大 bug 的时候,发布者能够采纳“零构建”的形式进行“秒级”回滚。

去中心化 RN 架构反对每个 plugin 独立公布、独立灰度、独立回滚,以最小颗粒度的操作来保证质量躲避危险。plugin 维度级别的灰度和回滚可能为不同的业务团队带了灵活性,每个业务团队能够自行公布版本,管制灰度节奏,解决线上问题。

4.3 零碎效力晋升

4.3.1 差分增量

App 频繁更新 RN 资源包会造成对用户流量的耗费,最无效的形式是利用增量更新来节俭流量。RN 资源包涵盖了编译后的 JavaScript 产物、图片、翻译文件等动态资源。它们的前后版本差别即是该版本变更的代码或者其余资源文件。为了让差分粒度深刻到资源包外部,零碎专门提供独立的“差分服务”,采纳二进制差分的形式对构建产物进行差分。

RN 资源包的 diff(差分)操作在服务端实现,patch(整合)操作在 App 端实现。在去中心化 RN 架构中,每个 plugin 的差分都是独立的。plugin 的发布会主动触发差分的执行,零碎会以 plugin 为维度拉取最近五个版本,Diff Server 则会顺次将它们和以后版本进行差分计算。如果计算胜利,会将差分后果上传到 CDN 并反馈给零碎,否则持续重试。整个差分操作是一个异步的过程,即便呈现“差分服务”下线等极其状况,零碎会主动降级为全量包,保证系统的可用性。

4.3.2 多场景入口体积优化

因为 React Native 的构建官网依赖于 metro.js,而它并没有具备无用代码剔除(tree-shaking)的能力。随着业务代码的收缩,包体积的优化是一个很重要的问题。

例如,ShopeePay 为公司多款外围 App 提供领取业务。ShopeePay plugin 在不同地区、不同 App 之间存在一些页面级别差别。同一个仓库含了所有代码和资源,然而构建脚本会将它们都会打包成为一个产物。很显著,这导致 ShopeePay 的公布产物蕴含大量冗余资源,并非最优,节约下载流量,同时也影响代码的执行效率。

咱们采纳自研的多场景插件(babel-plugin-scene),该插件通过注入的环境变量设置一个场景值,babel 能够依据场景值的差异化加载不同的文件,并且以默认文件作为降级兜底。不同场景对应不同的入口文件,利用这种模式能够无效管制包体积。

4.3.3 一站式多环境交融

一个失常的研发流程是从 test 环境,到 uat 环境,再到 live 环境。Code Push Platform 对接了 App 的 test/uat/live 环境,所以 RN 开发者只须要在该零碎就能够进行“一站式”的操作,方可满足一个需要的整个研发周期。

不同环境的包资源流转,是多环境交融的一大亮点。如果某 RN bundle 在 uat 环境构建,它也不须要从新构建,将 bundle 无缝转换到 线上 环境进行公布。它带来的劣势在于“零构建时长”以及资源包的稳定性,因为 bundle 没有从新进行构建,所以它的内容曾经在 uat 失去了充沛的验证,公布危险更小。

5. 旧业务的迁徙计划

如何迁徙现有业务的 App 是一个十分庄重的问题,特地是历史背景较重的业务,它们可能存在“逻辑耦合”或者“组件耦合”的场景。与此同时,很多相干业务都在需要迭代当中,零碎的迁徙是不能妨碍需要迭代,所以旧业务“渐进式迁徙”计划是十分必要的。

5.1 逻辑耦合

如果两个以上 plugin 存在逻辑依赖关系,用户必须同时加载到最新的 plugin。思考到热更新失败的可能性,逻辑耦合就是多个 plugin 暗藏着一种束缚关系。例如,订单业务和购买业务存在肯定的逻辑耦合关系,公布负责人针对流量极大的超级 App,不可能一一公布 plugin。在极其的状态下,用户可能会先加载到 plugin A,新版本的 plugin A 和旧版本的 plugin B 是不兼容的,这样会带来严重后果。遇到这种状况,有两种解决方案:

  • 计划一 :plugin 间逻辑解耦,保障每个 plugin 的独立性。
  • 计划二 :零碎提供了联结公布,在 Native 侧保障多个 plugin 可能同时加载到最新。

计划一是最理想化的状态,然而在业务场景细分的状况下,我的项目构造很难做到相对独立。

针对老业务能够思考计划二,零碎提供了 module 的概念,一个 module 对应着两个以上的 plugin。它们存在着一个绑定的关系。在同一个下载工作外面,客户端 SDK 以“事务”模式,保障多个 plugin 可能同时下载实现并投入使用。联结公布这个能力在零碎层面,无效躲避这种谬误的可能性。

5.2 组件耦合

如果说联结公布是针对在 plugin 维度的“逻辑耦合”兼容计划,“组件耦合”则是更细粒度的组件级别的耦合关系。也就是说,一个页面中存在多个组件来自不同的团队,例如商品详情页等页面有评估性能组件。这种“一个页面存在着 JSContext 互相嵌套”的情景存在于电商业务当中。

针对这种“组件耦合”状况,有两种解决方案:

  • 计划一 :嵌套组件抽离成为一个独立仓库,供第三方 plugin 应用。
  • 计划二 :应用“同屏渲染”的能力实现“多 Context 嵌套”。

计划一是最现实的解决方案。然而思考到迁徙老本,咱们也提供了计划二(一种“同屏渲染”嵌套组件)来反对这种场景,它相似一种 Native 组件。在多个 JSContext 的状况下,通过 plugin 名和页面名将所须要的内容嵌套到另一个页面当中。

如下图所示,plugin A 会嵌套 plugin B 的内容,A 和 B 也能够实现在同一个屏幕进行渲染。从 Web 的方向了解,这种状况有点像“iframe”的场景,反对多个页面的嵌套。它十分易于 RN 开发者的了解,客户端 SDK 可能动静加载指标 bundle 并将它渲染在适合的地位。

5.3 渐进式迁徙

对于现有的 App,因为业务没法暂停迭代,咱们难以一次性实现整体迁徙。因而,咱们提供了“渐进式迁徙”计划。思考到历史背景,该计划不会一次性把所有 plugin 都迁徙,而是逐渐拆分,再迁徙接入到新公布零碎。

迁徙的步骤如下图所示:

  • 优先将独立的业务迁徙到 Code Push Platform,它们享受一个独立的 JSContext;
  • 所有“待拆分代码”共用一个独立的 JSContext;
  • 将“待拆分代码”持续拆分成几个独立 plugin,独立应用 JSContext,其余内容则放弃步骤二的状态。

随着版本迭代,反复第二和第三步骤,直至历史业务全副拆分结束。这样咱们能够达到一个最优的指标,即是真正意义的“独立构建”和“独立公布”。

6. 总结

该零碎的指标在于满足所有 App 的多团队研发合作效率问题,去中心化 RN 公布模型思考到“独立运行时”、“独立开发”、“独立构建”、“独立公布”四大方面,保障了每个 plugin 运行的独立性。最终目标在于撑持 Shopee 的多个 RN 团队在不同 App 平台依据本人节奏自在公布且高效运作。

零碎设计波及到“多团队权限管控”、“客户端版本控制”、“灰度和回滚”、“增量差分”、“多入口包体积优化”、“一站式多环境交融”,减速了整个研发流程,真正做到了“灵活性”和“稳定性”的兼得。

退出移动版