又名:React Native 从0.61.5升级到0.64之旅
再名:为了解决手势冲突,我升级了React native
事情是这样的,我是一个前端,主要工作是nocode开发平台,之前在慕课网上录制了一期关于React Native的视频,《React Native + TypeScript仿喜马拉雅开发APP》的课程。
这一天,阳光正好,我外面看着太阳,晒着书,哦,不是,晒着太阳,看着书。课程群里有一个小伙伴就问我,使用手势切换标签组件的页签时,如果滑动的地方有点击事件的时候,会在切换标签之后触发点击事件,这导致切换标签之后,会跳转到了第二页导航页面中了。
小伙伴觉得是事件冒泡了导致的,嗯,这种说法其实不太对。因为React Native最终是渲染成了原生组件的,在Android里叫事件分发更恰当,不管怎么说,这明显是手势冲突导致的。
得,太阳没法晒了,小黄书也没法看了。赶紧回家,一番操作,很简单啊。我就在我的模拟器上重现了这个问题。
看源码分析问题
在项目中,我使用的是react-navigation的material-top-tabs导航器。而material-top-tabs又是依赖的react-native-tab-view。于是,我下载了react-native-tab-view源码,看到了关于手势切换标签的代码。
就在src/Pager.tsx文件里啊。很轻松啊,就找到了。
// src/Pager.tsx
...
const canMoveScreen = (
event: GestureResponderEvent,
gestureState: PanResponderGestureState
) => {
if (swipeEnabled === false) {
return false;
}
return (
isMovingHorizontally(event, gestureState) &&
((gestureState.dx >= DEAD_ZONE && currentIndexRef.current > 0) ||
(gestureState.dx <= -DEAD_ZONE &&
currentIndexRef.current < routes.length - 1))
);
};
...
// 使用了React Native 原生的手势操作
const panResponder = PanResponder.create({
onMoveShouldSetPanResponder: canMoveScreen,
// 这个方法如果返回true,可以阻止子视图响应触摸事件
onMoveShouldSetPanResponderCapture: canMoveScreen,
...
});
小技巧: 关于如何在源码中找到自己想要知道的东西,可以从关键参数入手。比如说我想知道react-native-tab-view关于手势操作相关的逻辑在哪,而这个组件有一个属性叫swipeEnabled,它可以控制是否启动手势切换标签的功能。那么就可以搜索看看那些地方用到了这个属性。
可以看到,源代码中,是有设置onMoveShouldSetPanResponderCapture函数的,且逻辑很清晰,调用了isMovingHorizontally函数,从名字就可以看出是用于判断是否正在水平移动的函数。基本可以猜测,如果是水平移动,这个方法就会返回true,用于阻止子视图响应事件。
但是我的项目明明是水平移动之后,依然触发了Touchable组件的onPress事件。
莫非是isMovingHorizontally写的有问题。
我决定使用比较low的办法,进入到我自己的项目,进入node_modules里面找到react-native-tab-view,直接打印一下,看看结果是什么。
但是,我大意了啊。朋友们!当我进入我的项目,并且找到node_modules/react-native-tab-view时,没想到,我看到的是一个800多行的Pager.tsx。
我再仔细一看,原来是版本不同,我的项目中使用的react-native-tab-view是2.13.0。而我看到的源代码版本是3.0.0。啥时候更新的啊?哦,5天前。嗯!!!
这说明了什么,说明了什么?小伙伴们,看源代码的时候,一定要注意版本啊!注意版本啊!
艰难的升级之旅
这就很简单了,我只要在我的项目中更新react-native-tab-view版本到3.0.0就好了。到时候再看看还有没有什么冲突。
等等,我使用的是react-navigation里的material-top-tabs啊,打开node_modules直接看一下material-top-tabs,看到package.json里面并没有强依赖react-native-tab-view,只规定了react-native-tab-view版本>=2.0.0就可以了。
吁,还好,我可以自己直接添加react-native-tab-view最新的库。
马上开始吧。于是我开始了升级react-native-tab-view之旅。
但是,我大意了啊。朋友们,接下来的旅程并不顺利,这也是这篇文章出现的原因。
简单的看了一下react-native-tab-view的文档。
发现现在的tab-view还依赖react-native-pager-view。这个库的作用是使用原生组件实现了左右切换的功能。
This component allows the user to swipe left and right through pages of data. Under the hood it is using the native Android ViewPager and the iOS UIPageViewController implementations. See it in action!
这个组件允许用户在一个页面中左右滑动。 在Android端使用Android ViewPager原生库实现,在iOS端使用iOS UIPageViewController库实现
第一次
继续我们的未完成的事业。
我开心的执行了yarn android,重新安装应用。很快就被打脸了。
报错了。
提示很简单,说react-native-pager-view最低sdk是17,而我的项目中最低sdk是16。我渐渐有了不详的预感。但是,没关系,修改一下我的项目中的minSdk版本也是很简单事情。
// android/build.gralde
buildscript {
ext {
...
- minSdkVersion = 16
+ minSdkVersion = 17
...
}
...
}
第二次
改完之后,我继续执行了一次yarn android。我先去喝了口水。回来之后,又一次报错了。
居然是语法错误。import type是什么鬼,它为啥要报错,哪里错了?
好吧,去查一下文档吧。发现import type 和export type是typescript 3.8.0的新特性。我的typescript版本是3.7.3
import type仅导入要用于类型注释和声明的声明。它总是会被完全擦除,因此在运行时不会有任何残留。同样,
export type仅提供可用于类型上下文的导出,并且还会从TypeScript的输出中删除该导出。
也有的帖子说,删掉yarn.lock,重新执行yarn安装依赖包,可以解决问题,我试过了,确实可以,而且我的typescript只是被升级到了3.7.5.
第三次
到此结束吧。我再再一次的执行了yarn android,这一次,我哪也没去。但是。。。
我的渣渣英语告诉我,React Native它居然说我注册了两个名字同样叫RNCViewPager的组件,它欺负我!
但是我不能放弃。直觉告诉我,这应该是我刚刚安装的react-native-pager-view,毕竟它们的名字都有pager。
好吧,我再去看看react-native-pager-view的文档。这一次,我看的很仔细。果然,让我发现了些什么
在文档下面还有一个提示
"@react-native-community/viewpager"library has been changed toreact-native-pager-view. Here you can find more information, how to migrate pager view to the latest version
@react-native-community/viewpager库居然移动到这里来了。我直接安装了一下,发现@react-native-community/viewpager还是可以安装的,但是内部代码和react-native-pager-view是一样的,只是包名不一样。而我项目中确实安装过@react-native-community/viewpager,版本是4.0.0 。
这两个包虽然包名不同,但是注册组件名称是一样,导致了RN提示说RNCViewPager重名了。
再一次说明了,看文档要仔细啊,小伙伴们,血泪一般教训!
那我直接删掉@react-native-community/viewpager就好了呀。我在项目搜索一下,看看哪里用到了@react-native-community/viewpager,尴尬的发现,居然没有任何用到这个组价的地方。别慌,当时添加这个组件肯定是有原因的。
好好想想,最终,我发现是因为我当时为了优化react-native-tab-view左右切换的性能,添加了react-native-tab-view-viewpager-adapter组件,而这个组件又依赖了@react-native-community/viewpager组件。
而现在,react-native-tab-view内置就使用了原生组件进行了优化,这一步优化操作就是多余的了。
我再次详细地看了react-native-tab-view源码,发现了一个叫Pager.native.tsx的文件。
小知识,
如何制定特定平台代码 :使用Platform.OS,判断是android/ios,判断。
特定平台扩展名:xxx.native.js xxx.ios.js xxx.android.js xxx.js
React Native 会根据扩展名自动加载对应平台的代码,如果你还希望在 web 端复用 React Native 的代码,那么还可以使用.native.js的扩展名。此时 iOS 和 Android 会使用xxx.native.js文件,而 web 端会使用xxx.js
而在Pager.native.tsx文件中,确实有用到react-native-pager-view,而且触摸事件直接由原生组件处理了,之前的分析都是没用的,不过Pager.tsx中,官方既然已经处理过了禁止子视图响应触摸事件的逻辑,原生组件应该也是有同样的逻辑的。
抱着忐忑不安的心情,我继续填坑之旅。
首先,删除以前用到react-native-tab-view-viewpager-adapter组件的地方。
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter';
...
function HomeTabs() {
...
return (
<Tab.Navigator
...
pager={props => <ViewPagerAdapter {...props} />}
...
>
...
</Tab.Navigator>
);
...
}
第四次
绝对的最后一次了。我执行了yarn android。但是魔鬼并没有放过我!
啥东东?我有一个组件返回的是undefined?我确定我没有,你又在骗我?老师,编译器出问题了。
不要放弃,你看,今天的阳光真好!哦,天已经黑了呀!
看下方,它让我去检查PlatformPressable,我再次去看了react-native-tab-view源码。找到了PlatformPressable文件,噢噢,糟糕了!
import { Platform, Pressable, PressableProps } from 'react-native';
它居然使用Pressable组件,这个组件是React Native版本0.62.0提供的新组件。
Pressable是一个核心组件的封装,它可以检测到任意子组件的不同阶段的按压交互情况。
而我的React Native版本是0.61.5
我整个人瞬间都不好了,就在前几天,React Native邮件通知我它更新到了0.64.0 我都假装没看到。
但是现在,我必须要看到了。
经过一番激烈的思想挣扎,我做出了一个违背祖训的决定,我要升级React Native。
没有困难的工作,只有勇敢的干饭人。
正好可以试试React Native在0.61版本提供的upgrade功能,能让我们无痛升级React Native。呵呵,生活真是太美好了。
我执行了
npx react-native upgrade
并且我做到了fq,此时此刻,我的心胸是宽广的,我的世界是无限的,但是我的心是卑微的。
此时此刻,我的内心毫无波动!
也许我应该重启一下电脑!
也许我不应该fq,我的世界可能太大了!
也许是因为0.64版本太高,它没有适应,我还可以指定升级版本。
npx react-native upgrade 0.63.4
但是依然不行,我的浏览器确实可以访问到网站,但是它依然执着的提示我,我没资格。我是个loser。
当两个人相处时,太过执着不是一件好事,最终总有一个人要离开!我要离开了。朋友,当我离开时,请不要还念!
此时此刻,我不仅没有意志消沉,反而充满了斗志,今天,你对我爱答不理,明天我让你高攀不起,三十年河东,三十年河西,莫欺少年穷!
我决定,我要重学React Native,哦,不是,我要重新安装一个最新的React Native应用。好好处理一下升级的过程。
努力吧,干饭人!
重新创建项目
因为我的项目使用了typescript,所以我指定了创建模板
npx react-native init ximalaya --template react-native-template-typescript
呦,启动web服务时,换了个图标啊,以前是显示Welcome to React Native。现在是Welcome to Metro。莫非要改名了吗?
我喜欢,咱前端就是喜欢新鲜玩意!
当项目创建完成时,我看到package.json中使用的react-native版本并不是0.64,而是0.63.4
要不,就用这个版本吧?
这个念头只在我脑海中停留了一秒钟,身为一个前端,怎么能退缩,怎么能说自己学不动了,要用,咱就用最新的,用next版本的。
那我就不指定版本,安装一个最新的应用,再按照typescript依赖和配置吧!
我继续执行了
npx react-native init ximalaya
在2020年7月份的时候,React Native更新过一次官方文档。现在在文档中有一篇关于使用typescript以及使用别名的教程,写的也是很清楚的。我就不重复了。
一切准备妥当了,很简单的,我就重新安装了Android端的APP,但是在安装iOS端的时候,报错了。
它问了三个人生终极问题,我是谁?我在哪?我要干什么?
好吧,我们来看看,React Native 0.64到底更新了哪些东西?为啥不行了呢?
- Hermes引擎可以作为iOS端的可选项了,好处就是以后iOS和Android统一js引擎,能减少一些系统性的差异导致的bug;
- 默认启动内联引用,不是很懂,后续在分析;
- View Hermes traces with Chrome,不懂,大概是Hermes可以在Chrome浏览器上调试?
- Hermes支持了Proxy,以前居然不支持?还好我以前没用mobx;
- React升级到了17,React 17对开发者是无感的,唯一的区别大概是不需要再引用React也可以使用jsx语法了。
最后就是版本依赖:
-
不再支持Android sdk 16-20了,最低版本21,SDK 21版本对应的Android版本是5.0,目前市面上基本已经不存在5.0一下的Android手机了吧!
buildscript { ext { buildToolsVersion = "29.0.3" minSdkVersion = 21 compileSdkVersion = 29 targetSdkVersion = 29 ndkVersion = "20.1.5948944" }
dependencies { classpath("com.android.tools.build:gradle:4.1.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }}
2.需要xcode升级到12以及cocoaPods升级到1.10;
看上图,我的xcode版本是11.3.1,cocoaPods版本是1.9.1,那么问题找到了!
3.nodejs版本最低要求12,上个星期,我正好从10升级到了12,没问题;
4.Flipper 升级到 0.75.1
好吧,接下来,我需要先升级xcode,很简单,在APP Store上更新就可以了,11G,我可以先去睡个觉了
cocoaPods,直接安装一次
sudo gem install cocoapods
当我一觉醒来,xcode终于更新好了。
安装导航库 react-navigation
在我的项目中,我使用到了react-navigation导航库,重新安装最新文档安装一次。
yarn add @react-navigation/native
react-navigation本身是一个纯js写的库,但是它依赖了一些原生手势库、动画库等,所以它的性能很好,不用担心。只是有一点麻烦的事,我们需要一个个去安装这些依赖库
yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
其中,大部分的原生库,目前在Android端都可以做到自动链接,iOS端需要先cd ios && pod install ,也不是特别的麻烦。
但是有一些原生库,是需要修改一些原生代码的。
比如说react-native-gesture-handler在Android端有些麻烦。需要修改一些Android代码。
// 文件路径 ximalaya/android/app/src/main/java/com/ximalaya
package com.ximalaya;
import com.facebook.react.ReactActivity;
+ import com.facebook.react.ReactActivityDelegate;
+ import com.facebook.react.ReactRootView;
+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class MainActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "ximalaya";
}
+ @Override
+ protected ReactActivityDelegate createReactActivityDelegate() {
+ return new ReactActivityDelegate(this, getMainComponentName()) {
+ @Override
+ protected ReactRootView createRootView() {
+ return new RNGestureHandlerEnabledRootView(MainActivity.this);
+ }
+ };
+ }
}
然后在index.js文件中,导入react-native-gesture-handler库
import 'react-native-gesture-handler';
比如说 react-native-screens,也需要修改一下原生代码,iOS端不需要做任何操作,Android端需要添加一段代码
// 文件路径 ximalaya/android/app/src/main/java/com/ximalaya
package com.ximalaya;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
+ import android.os.Bundle;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "ximalaya";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(null);
+ }
}
大概意思是为了防止MainActivity在restart状态时发生崩溃,有兴趣的小伙伴可以去了解一下Android的Activity的生命周期。
然后在index.js文件中添加
import { enableScreens } from 'react-native-screens';
enableScreens();
只有react-native-gesture-handler 和react-native-screens是需要我们手动修改源代码的,其他的库自动链接就行了,但是iOS端还是要pod install。
小知识,如果安装的依赖库有android或ios文件夹,说明它们有原生代码,那就需要重新安装APP才行
如果以上都没有问题,那么就可以继续了!
刚刚安装的都是react-navigation的核心库和依赖库,还需要安装其他的一些导航库
堆栈导航、底部标签导航、顶部标签导航
yarn add @react-navigation/stack @react-navigation/bottom-tabs @react-navigation/material-top-tabs
然后就安装今天我们的主角 react-native-tab-view
yarn add react-native-tab-view
报错了:Connect to dl.google.com:443
其他的组件都可以,在我睡觉之前,也可以,为什么到你了,就不可以了。
还有这样的错。
我确定,我的心是自由的,我的世界是广阔的,但是依然不行,也许是我的信仰欠费了。
于是,我尝试使用国内的淘宝源、关闭sock、打开sock、升级Android studio到4.0,删除build文件。
但是统统不行,折腾了很久。
唯一欣慰的是,我在Android studio中得到了一些信息。
它说我设置了代理服务器的地址,但是,我找遍了电脑所有的角落,依然没有找到任何我配置过代理的信息。
也许我需要一台更智能的电脑。
最终,我在一个帖子中,看到了答案:
遇到这种错误,大部分是因为ip冲突导致的,你可以试试重启电脑。
也许是因为我家里的网不太稳定,电脑的ip经常会变,导致了ip冲突。
一瞬间,我觉得我看到了世界的真理。看到了曙光。这个世界的规则就是如此简单,我重新燃起了对世界的爱。
当我重启了电脑之后,这个问题果然消失了,但是生活依然没有放过我这个小喵咪。
我什么都不懂!生活的苦只有品尝过的人才知道,但是生活依然要继续下去!
我熟练的打开了GitHub,进入了react-native-pager-view的仓库,发现它更新了,从5.1.1更新到了5.1.2。是的,就在我写这篇文章的时候,它更新了。
我满怀希望的更新到了最新的版本,重新执行了yarn android。
这一次,我成功了!
BUILD SUCCESSFUL in 26s
150 actionable tasks: 138 executed, 12 up-to-date
info Connecting to the development server...
8081
info Starting the app on "emulator-5554"...
Starting: Intent { cmp=com.ximalaya/.MainActivity }
✨ Done in 29.32s.
幸福来得太突然,我好想还没做好准备!
IOS端也尝试运行一下,看看报不报错!
cd ios && pod install
cd ..
yarn ios
依然没有报错
success Successfully built the app
info Installing "/Users/chen/Library/Developer/Xcode/DerivedData/ximalaya/Build/Products/Debug-iphonesimulator/ximalaya.app"
info Launching "org.reactjs.native.example.ximalaya"
success Successfully launched the app on the simulator
我果然太幸福了!
接下来,还有一个需要特殊处理的库 react-native-config,需要修改一些配置
这个库用于在原生代码中读取配置信息,简化不了解Android、iOS的前端工程师方便的修改配置
比如说:微信支付等的信息等等。
便于后期的维护
// 路径 android/app/build.gradle
// 添加到第二行
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
然后将之前的.env配置信息复制到新项目中
接下来,是react-native-splash-screen,需要修改一下原生代码
...
import org.devio.rn.splashscreen.SplashScreenReactPackage;
public class MainActivity extends ReactActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
+ SplashScreen.show(this); // here
+ super.onCreate(savedInstanceState);
}
}
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "RNSplashScreen.h" // here
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// ...other code
[RNSplashScreen show]; // here
return YES;
}
@end
把旧项目的其他的一些配置文件、布局文件等都复制到新项目中。
最后一个库,realm,React Native的本地数据库,也是涉及到原生代码的库。这一次Android端很容易就安装成功了,iOS端在执行pod install时一直卡在Installing RealmJS ,这一步上,这主要是因为它需要下载一个比较大的核心包,而且服务器还在国外。你懂得。
为啥我会知道呢?不管做什么时候,时间长了总会积累一些经验的
解决方案,我就不在这里详细解释了。
我之前使用的版本是6.0.1,目前最新版本是10,不能浪,大版本依然保持不变。
剩下的就是将之前的js代码以及一些配置项之类复制到新项目中,基本无法启动的问题了。
在文章开头遇到的手势冲突问题,现在已经解决了,
但是react-native-gesture-handler和react-native-tab-view结合的时候,导致又有冲突了。原因还是因为react-native-tab-view修改底层页面切换的实现导致的。
真是成也萧何,败也萧何。
严肃总结
这是一个很正经的总结,真的很正经。
我一定会回来的!
下一篇文章预告,关于如何解决react-native-gesture-handler和react-native-tab-view冲突的问题!突然好想哭,呜呜呜!