36 升新不升旧:自定义组件如何升级到新架构?
你好,我是蒋宏伟。
有同学后台留言说:“我把公司应用升级到了 0.71.8 版本,开启新架构后,一些组件都乱了,折腾了三天最终无奈退回到老架构了。”
这个问题还挺典型的,今天我们就来聊聊到底应该怎么升级?分享一些经验。
在上节课中,我把我们的教学项目 React-Native-Classroom 项目,从 0.68 升级到了 0.72 的新架构,迁移过程十分顺利。这是因为 React-Native-Classroom 项目没有使用自定义模块或组件,它使用的都是官方的或成熟的社区方案,这些方案都对新老架构进行了兼容。所以,无需额外的 Native 代码升级工作。
但是,新旧架构的自定义模块或组件研发方式完全不同。原来那些自己维护的自定义模块或组件,要完全升级成 Turbo Modules 和 Fabric Components 是需要大量的改造工作的。
对于业务而言,研发效率是第一位的。那如何升级新架构,才能降低升级成本呢?
先说结论:升级新架构时不动旧的业务代码,是一个好策略。
老代码是否兼容?
那首先,我们就来测试,老代码不做任何修改,能否直接在新架构上运行?
我参考了官方文档中的老架构部分,分别创建了一个老模块和一个老组件。
老模块是自定义的简化版存储模块 StorageModule。它有两个方法,一个是利用键值对的形式存储字符串,另一个是使用键去读取存储在 Native 端的值。如果存储(SET ITEM)和读取(GET ITEM)成功,那么就说明这类老模块代码能在新架构中直接使用。
其 JS 的部分代码如下:
import {NativeModules} from 'react-native';
const StorageTest = () => {
const handleSetItem = async () => {
await NativeModules.StorageModule.setItem('testKey', 'Hello, World!');
};
const handleGetItem = async () => {
const result = await NativeModules.StorageModule.getItem('testKey');
console.log('Received value: ', result);
};
return <jsx.../>
};
老组件是一个自定义视图 CustomView,它只有简单的设置背景颜色的功能。如果颜色设置成功,那么就说明这类老组件代码在新架构中能够直接使用。
其 JS 部分的代码如下:
import {requireNativeComponent} from 'react-native';
const CustomView = requireNativeComponent('CustomView');
const App: () => JSX.Element = () => {
return (
<CustomView
style={{width: 100,height: 100 }}
color="red"
/>
);
};
export default App;
这些原本运行在老架构上的代码,在开启新架构后,运行的结果截图如下:
可以看到,老的自定义模块 StorageModule 正常运行。它能够从 Native 侧正确地存储和读取 “Hello, World!” 这段文字。
然而,老的自定义组件报错了。报错内容以文字形式显示在界面上,提示我 CustomView 老组件不兼容 Fabric,无法在新架构中运行。
从上述简单的演示中,可以推测官方默认对老架构的自定义模块代码做了兼容,但并未对老架构的自定义组件代码进行直接的兼容。
老组件兼容方案
为了兼容老架构的自定义组件,官方提供了一种手动开启兼容模式的方案——New Renderer Interop Layer。
显然,如果历史代码众多,底层架构升级需要大规模改动业务研发代码,这对业务来说无疑是一个巨大的负担。在 Facebook 内部,许多业务都使用 React Native,因此基建和业务研发团队肯定也经过了几轮讨论。兼容方案显然能够降低业务团队的升级成本。
所谓的 New Renderer Interop Layer 就是通俗理解的组件兼容层,它起源于 Facebook 内部,在内部使用了一段时间后,于 0.72 版本进行了开源。
开启组件兼容层的方法有三步。
首先,在根目录创建 react-native.config.js 文件。
然后,分别列举 iOS 和 Android 需要兼容的组件,将这些组件填写在 unstable_reactLegacyComponentNames
字段中。在我的代码中,旧架构的组件名字是 CustomView。因此,文件内容如下:
module.exports = {
project: {
android: {
// list of components that needs to be wrapped by the interop layer
unstable_reactLegacyComponentNames: ['CustomView'],
},
ios: {
// list of components that needs to be wrapped by the interop layer
unstable_reactLegacyComponentNames: ['CustomView'],
},
},
};
最后一步,重新构建和启动 App。
完成上述三步后,在重新启动的 App 中,你就能看到旧架构自定义组件 CustomView 能够正常展示 color=“red” 的红色了。
但是,这个兼容方案并不完美。
因为,官方并不打算 100% 支持所有的旧组件代码。官方给出的理由是,完全兼容可能会阻碍新架构性能的最大化展现。因此,官方希望社区的所有老架构的自定义组件都能迁移到新架构。
实际上,绝大部分功能都能得到兼容,包括:
- Props
- Events
- Native View Commands(即使用 UIManager.dispatchViewManager)
- Native Methods(如 setNativeProps 和 measure* 函数)
- 向 JS 导出的常量
已知不能兼容的包括:
- Concurrent Features(如 startTransition)
从现有的兼容方案来看,组件中常用的功能兼容层都已经支持,而 Concurrent Features 在老架构中就不支持,迁移后仍然不支持也是可以理解的。
由此可见,绝大部分老架构的组件代码都可以通过兼容层来适配新架构。
完全迁移到新架构
然而,如果我们继续在新架构上运行老代码,就无法享受到新架构带来的性能提升。在第34讲中,我们已经讨论过,新架构的组件渲染在 Android 上可以提高 0%~8% 的性能,在 iOS 上则可以提升 13%~39% 的性能。
因此,对于有时间和资源进行代码迁移的团队,或者正在开发新组件的团队,我仍然建议采用新架构的开发方案。
在第 22 讲中,我们介绍过新架构的自定义组件,但那时候还没有 CodeGen 自动化升级工具,所有的 Turbo Modules 和 Fabric Component 都是我们手动创建的。现在,我们可以利用 CodeGen 工具帮助我们自动化生成很多模板代码。
在此,我将以在 Android 上开发 CustomView 组件为例,来介绍 Fabric 自定义组件的研发流程。这个自定义组件只有一个功能,那就是改变其背景颜色。我在 GitHub 上发布了这个组件完整的新架构代码和老架构代码。
整体开发流程包含三个步骤:
- 定义组件(JS):你需要把对组件的设计思想编写成 TypeScript 的 Interface,并将其定义成一个 npm 包,以便后续使用。
- 实现组件(Native):利用 CodeGen 自动化工具,将 TypeScript 的 Interface 生成为 Native 的 Interface,并通过一些模板代码将 JS 和 Native 连接起来。
- 使用组件(JS):在 JS 中使用由 Native 实现的代码。
在开始开发之前,你需要创建一个用于开发 CustomView 的文件夹。
在这里,我直接在项目的根目录下创建了一个名为 RTNCustomView 的文件夹。文件夹的名称以 RTN 开头是为了提高其辨识度,因为 RTN 是 React Native 的缩写。
这个文件夹包含三个部分,具体如下:
其中,android 和 ios 文件夹用于存放该组件的客户端代码,而 js 文件夹用于存放 JS/TS 代码。
定义组件
定义组件的步骤主要包括两个部分:
- RTNCustomViewNativeComponent.tsx:这是自定义组件的 TypeScript Interface。
- package.json:这是自定义组件 npm 包的配置文件。
首先,在 js 目录中,我创建了一个名为 RTNCustomViewNativeComponent.tsx 的文件。这个文件以 NativeComponent 结尾,这是官方约定的规则,因为后续 CodeGen 需要根据 NativeComponent 结尾的标识找到 TypeScript 的规范文件,然后进行代码生成(code generate)。
RTNCustomViewNativeComponent.tsx 文件的内容如下:
import type {ViewProps, HostComponent} from 'react-native';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
export interface NativeProps extends ViewProps {
background?: string;
}
export default codegenNativeComponent<NativeProps>(
'RTNCustomView',
) as HostComponent<NativeProps>;
在这里,我们约定这个组件只能接受一个自定义参数 background,这个参数的类型是 string。
此外,在 RTNCustomView 目录下,我创建了一个名为 package.json 的文件。这个文件的内容如下:
{
"name": "rtn-custom-view",
"version": "0.0.1",
"description": "Showcase a Fabric Native Component with a custom background view",
"react-native": "js/index",
"source": "js/index",
"devDependencies": {},
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "RTNCustomViewSpecs",
"type": "components",
"jsSrcsDir": "js"
}
}
在这里,需要特别关注的是 codegenConfig 的配置,包括包名 name、类型 type 以及规范的目录 jsSrcsDir。
有一些约定俗成的规则:包名必须以 Specs 结尾;组件类型应填写 component,模块类型应填写 modules,这里填写的是 component;规范的目录就是我们之前创建的 js 目录。
实现组件
Native 这部分的内容相对较多,以 Android 为例,包括五个部分:
- build.gradle:这是 Android 模块的构建配置。
- CodeGen:这是用于生成链接 JS/Java/C++ 的模板代码。
- CustomView.java:这是新的原生组件。
- CustomViewManager.java:这是原生组件的管理器。
- CustomViewPackage.java:这个部分负责将原生组件暴露给 React Native。
整个结构如下:
RTNCenteredText
├── android
│ ├── build.gradle
│ └── src/main/java/com/rtncenteredtext
│ ├── CustomView.java
│ ├── CustomViewManager.java
│ └── CustomViewPackage.java
├── ios
├── js
└── package.json
整个流程开始于配置 Android 的构建脚本,然后使用 CodeGen 自动化工具生成胶水代码,把 JS/Java/C++ 连接起来,接着定义新的 Java 自定义原生组件,再创建一个管理这个组件的类,最后在 React Native 组件列表中注册这个组件,使其能够被 JavaScript 代码访问。
下面我们详细讲解下流程。
第一步:配置 build.gradle
build.gradle 是 Android 构建系统用来定义构建规则的文件。在这个配置文件中,绝大部分都是模板代码,你只需要复制和粘贴就可以了,需要修改的只是自定义组件的名字。在这里,我使用的是 namespace "com.rtncustomview"
。完整的配置如下:
buildscript {
ext.safeExtGet = {prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
}
}
apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)
namespace "com.rtncustomview"
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
targetSdkVersion safeExtGet('targetSdkVersion', 33)
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
}
}
repositories {
mavenCentral()
google()
}
dependencies {
implementation 'com.facebook.react:react-native'
}
第二步:运行 CodeGen
CodeGen 是一个自动代码生成工具,它可以根据之前创建的 RTNCustomViewNativeComponent.tsx 规范文件,自动生成连接 JS、Java 和 C++ 的模板代码。这样,你就可以在 JS 中方便地调用原生方法,同时也避免了手动编写这些模板代码的繁琐工作。
在运行 CodeGen 之前,你需要先将 RTNCustomView 组件添加到 package.json 文件中,然后通过 android
命令来生成模板代码。命令如下:
// 在 RN 项目根目录运行
yarn add ./RTNCustomView
// CD 到根目录下的 android
cd android
./gradlew generateCodegenArtifactsFromSchema
第三步:创建 CustomView.java
接下来,我们要创建一个新的原生组件,名为 CustomView.java。这个 Java 类将扩展一个 Android 原生视图类(例如 TextView),并定义所需的属性和行为。
在这个类中,你可以定义你需要的方法和属性,这样就可以在 JS 中直接访问这些方法和属性。同时,你还需要定义一些用来设置和获取这些属性的方法,这样,React Native 就能通过这些方法与原生组件进行交互。
完整代码如下:
package com.rtncustomview;
import androidx.annotation.Nullable;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
public class CustomView extends View {
private int color;
public CustomView(Context context) {
super(context);
this.configureComponent();
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.configureComponent();
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.configureComponent();
}
private void configureComponent() {
// 默认背景色为红色
this.color = Color.RED;
this.setBackgroundColor(this.color);
}
public void setBackground(String color) {
// 颜色值由React Native传入,格式为#RRGGBB,如红色为#FF0000
this.color = Color.parseColor(color);
this.setBackgroundColor(this.color);
}
}
其核心是 public void setBackground(String color) 方法,它对应着 RTNCustomViewNativeComponent.tsx 定义的 background 属性,它是真正使 android.view.View 视图背景色设置生效的代码。
第四步:创建 CustomViewManager.java
每个原生组件都需要一个管理器类,CustomViewManager.java 就是 CustomView.java 的管理类。管理器类通常需要扩展 SimpleViewManager,并实现必要的方法,例如 createViewInstance() 方法用于创建原生组件的实例。
对于 React Native 自定义组件,我们还需要使用 @ReactProp 注解来标记可以从 JS 端设置的属性,这样 React Native 就可以通过这些方法来更新原生组件的属性了。
其完整代码如下:
package com.rtncustomview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.graphics.Color; // 这里添加导入语句
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.RTNCustomViewManagerInterface;
import com.facebook.react.viewmanagers.RTNCustomViewManagerDelegate;
@ReactModule(name = CustomViewManager.NAME)
public class CustomViewManager extends SimpleViewManager<CustomView>
implements RTNCustomViewManagerInterface<CustomView> {
private final ViewManagerDelegate<CustomView> mDelegate;
static final String NAME = "RTNCustomView";
public CustomViewManager(ReactApplicationContext context) {
mDelegate = new RTNCustomViewManagerDelegate<>(this);
}
@Nullable
@Override
protected ViewManagerDelegate<CustomView> getDelegate() {
return mDelegate;
}
@NonNull
@Override
public String getName() {
return CustomViewManager.NAME;
}
@NonNull
@Override
protected CustomView createViewInstance(@NonNull ThemedReactContext context) {
return new CustomView(context);
}
// Here we are exposing a background color prop.
@ReactProp(name = "background", customType = "Color")
public void setBackground(CustomView view, @Nullable String color) {
if (color != null) {
view.setBackground(color);
}
}
}
第五步:升级 CustomViewPackage.java
在创建了 CustomViewManager.java 之后,我们需要创建一个 CustomViewPackage.java 来包含我们的 ViewManager。在 createViewManagers 方法中,我们将添加我们刚刚创建的 CustomViewManager。
完整代码如下:
package com.rtncustomview;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CustomViewPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
// return a list that contains our CustomViewManager
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new CustomViewManager(reactContext));
return viewManagers;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
// return an empty list because we do not have any native modules to export
return Collections.emptyList();
}
}
使用组件
在组件使用方面,回到 JS 代码中,使用在客户端实现的组件。
首先运行 yarn add ./RTNCustomView
命令,重新添加 RTNCustomView,这样可以刷新我们实现组件的代码。
然后编写一段测试代码,代码如下:
import React from 'react';
import RTNCustomView from 'rtn-custom-view/js/RTNCustomViewNativeComponent';
import {View, Text, Button} from 'react-native';
const App: () => JSX.Element = () => {
return (
<View style={{borderColor: '#0ac', borderWidth: 1, padding: 5}}>
<Text>New Component:自定义视图组件</Text>
<RTNCustomView
background="blue"
style={{
width: 100,
height: 100,
}}
/>
</View>
);
};
export default App;
RTNCustomView 的使用方式非常类似于其他原生组件。你可以向它传递各种属性,例如 background 和 style。在这个例子中,我们设置了 background 为 "blue"
,并且设置了它的宽度和高度。
这里的 style 属性设置了视图的宽度和高度。请注意,尽管在 CustomView.java 文件中我们只提供了 setBackground 方法用于修改颜色,但 React Native 的视图管理器为所有的原生视图组件默认提供了对 style 属性的支持。
完整代码,我放在 Github 上了。
最后,重新构建 Android 应用,你就能看到代码生效了。
总结
这节课我介绍了两种升级新架构的策略。
一种策略是老代码兼容方案,该方案需要开启组件兼容层才能使用,而且可能会有部分性能损耗。另一种策略是完全迁移到新架构的方案,课程中我以新建 Android 组件为例,介绍了迁移新架构的策略。
可以看到,在新架构模式下,自定义组件的创建方式与老架构有很大的不同,新架构是以包为单元将各端统一在一起来进行开发的,老架构代码都在自己的 JS/Android/iOS 仓库中。这意味着,将老代码改为新架构的写法可能会有巨大的迁移成本。
因此,我的建议是,新的组件或模块使用新架构,而旧的代码保持不变,使用兼容模式。这种升级策略成本会低很多。
那么到这节课,我们的动态更新专栏也要和大家说再见了,感谢你过去一年的陪伴。因为有你,所以一同成长。我们有缘,再会!
- 听说昵称太长了躲在树后面会被别人看见的 👍(5) 💬(4)
我就是那位折腾了三天退回去的同学,没想到被点名了😂。经过了一年 RN 的折腾我也总结了一些看法,这个折腾过程估计也是很多初尝跨段方案的团队要走的老路。首先不能把 RN 当做客户端最终解决方案,我们当时调研后下的结论:“跨端是未来,原生是辅助”,因为项目初期各部门同事一致不负责的说:怎么快怎么来,能用就行!这给了我们拥抱跨端的勇气。可当加班加点忙完项目上线跑起来终于松一口气时大家口风就变了,因为能用和用到极致是两码事,当开始盈利时跟竞品一对比那必须要用到极致,起初那些不痛不痒的问题在这个阶段就变成了棘手问题,兜兜转转最后又回到了原生,因为不是所有团队都有像宏伟老师这样的团队去做深度优化,所以我们花了很多时间去优化却没折腾出效果,公司没法再忍受我们去研究而不产出了,然后只能用原生把大部分核心功能重写了,只留下少量边边角角的业务用 RN,回过头一看,原本想借助跨端提高生产力,结果反而是降低了生产力,可以说是羊肉没吃到弄了一身骚,原因很简单,一但过了 0 到 1 这个阶段摆在面前只有两条路,要么深入甚至魔改 RN(这没个一年半载门都摸不着),要么用原生开始重构(原生开发组的同事还整天做优化呢,纯 RN 咋个做到极致),如果没有做这到选择题那么大概率我猜你们宣告项目失败甚至到了裁员的边缘,市场机会总是稍纵即逝,项目的失败与技术选项不无关系,这个问题值得深思。总结,得结合自身项目借助 RN 辅助完成一些工作,最好不要一梭哈的把鸡蛋全放 RN 的篮子里,除非你做的是需求明确并且生命周期可预测的项目。另外,这两天在研究原生承载页加载 RN 过慢的问题,安卓低端机得两三秒白屏后才能加载完渲染出来,不知道老师能否加个餐讲一讲怎么优化。
2023-08-10 - 长林啊 👍(1) 💬(0)
老师后面能扩展react-native服务端渲染方向的相关内容吗?
2023-11-30 - 大大小小 👍(1) 💬(0)
哈哈,我居然是第一个毕业的!
2023-08-08 - Geek_64953b 👍(0) 💬(0)
大佬从58毕业了嘛
2025-01-09