最近研究了一下 React Native(简称RN),并用它作为毕设项目(一个仿小红书的校园社交应用)。经过一段时间的折腾,对 RN 生态有了一些了解,是时候可以分享一些心得了。
代码仓库: https://github.com/kuizuo/youni
为什么是 RN 而不是 Flutter?
很简单,就是技术栈问题。从开发角度而言,尤其还是对于前端开发人员,会 JS 且搞过 React ,那 RN 上手就十分友好,最起码有关 React 社区的逻辑库或状态库是可以使用的。
虽说 Flutter 的性能是会比 RN 好上不少,但抛开需求不谈,与其比性能不如比开发速度。很显然开发 RN 的效率比 开发 Flutter 高上不少。况且真在意性能的话,那多半就不会考虑跨平台技术了,而是直接考虑原生开发了。
再从需求考量,我所编写的应用更偏向于内容展示的 app,而不是编写一个手机电池监控或者内存监控的app,如果是后者,那这时选择任何跨平台开发都没有意义,像这些系统级别的API在跨平台开发基本不太可能实现的。
对于这两个跨平台技术的选择,应该考虑自身需求、开发成本、技术选型,没有最好的只有最适合的。如果有的选择,谁不会选择原生开发是吧。
但凭我自己接触 RN 以来,国内的 RN 资源甚少,反倒是 Flutter 资源很多,并且从这些相关资料来看,确实 Flutter 优于 RN,但还是那句话,这里就不再过多赘述了。
是否有必要学 react-native?
先说一个结论:RN ≠ 原生,别指望会个 react 就能写出靠谱的原生应用。
就从我的开发经历来说,坑是真的多,但好在RN拥有庞大的线上社区,可以找到的几乎所有问题的答案。但国内的社区好像并不是很好,很多问题我都是在国外论坛中解决的。
如果你学习它是为了扩展其他平台的开发能力,那么还是可以学习一番的,会有另一番的收获。但如果学 RN 只是为了避免不用学 android 和 iOS 等原生技术就能写 app,那便不建议学习。抱着这心态的话前期开发可能不明显,但到了后面会踩很多坑,而且两眼一黑,因为你不懂 native 开发。
我的个人评价是 RN 只能作为 H5 手机页面运行在原生移动设备的一种展示形态。虽然本质不是,但其所展示的效果如同。RN 不仅仅只是 Web,但也止步于 Web。
顺带吐槽一番,React-Native 项目发布4年多了,还没有 1.0 版本么(¬_¬)
如果你想再继续了解 RN,那么就请往下看。
Expo
Expo 是基于 React Native 并整合大量常用的 native module(Expo SDK),像原生的功能如相册,相机,蓝牙等功能,在 expo 都是直接集成的,相当于封装原生的api,暴露给js调用。因此你不用去了解原生开发的许多知识和坑点,上手即用便可。本地配置好应用所需的环境,就直接直接运行 RN 项目,开发十分方便。
此外 Expo 还提供了 Expo Go App,只需要在你的移动端设备中安装它,启动开发服务器并生成 QR 码。在浏览器打开 snack.expo.dev ,点击 MyDevice,扫码并在 Expo app 中查看。
会自动将该程序实时运行在你的移动端设备,意味着你更改代码也将会同步到Expo go 中。极大程度上提升 RN 的开发体验,尤其是在真机测试阶段。
Expo 官方还贴心的提供了云服务 Expo Application Services (EAS),意为这你可以你可以将你的 RN 项目在托管在云服务上, 来执行构建与发布等流程。
总之如今开发 RN 请毫不犹豫的使用上 Expo。
开发中遇到的一些坑点
实际开发中所遇到的坑点远不止下述所说,这里只列举几个相对有代表,坑比较深的点。甚至有很多坑都不是前端方面的知识了。
在 pnpm 下无法启动 Android
错误提示:Error: Unable to resolve module ./nxode_modules/expo/AppEntry
解决方案:在项目根目录创建 .npmrc
,内容如下
shamefully-hoist=true
node-linker=hoisted
删除 node_modules 与 .expo 文件夹,重新安装依赖即可。
相关链接:https://github.com/expo/expo/issues/9591#issuecomment-1485871356
样式问题
在样式方面与传统的 Web 开发存在一定的区别。在 RN 中有两个主要组件,View 与 Text,可以理解为 Web 的 div 与 span。基本所有的 View 都是 flex 布局,想要让 View 组件占满通常不会使用 width: ’100%’ 或 height: ‘100%’,而是使用 flex: 1,例如一般都会带上这么一个样式。
<View style={{ felx: 1 }}>
如果样式问题就只是这样就好了,同一套样式在不同平台上所展示的效果都可能不大一样,尤其使用原生 Web 的样式,哪怕你用 style 编写,在 Web 网页也能成功显示效果,但是在 IOS 与 Android 中绝大多数情况下是不显示的。这会在后面介绍 Tailwindcss 相关库的时候会额外在提到一点。
文本必须要用 Text 包裹
如果不怎么做的话,会报错,如果只是这样倒还没什么。重点是错误提示并没有堆栈信息!就如下图所示
这点对于开发体验而言并不友好。
模拟器无法请求本地 api
由于一开始是在 Web 端进行调试开发的,所以没留意到这个问题,直到切换到安卓模拟器之后发现模拟器无法请求本地后端服务,在IOS 端暂无这问题。因此需要做如下配置:
1、首先将模拟器内网切换到本地。
假设后端 api 地址为 [http://localhost:6001](http://localhost:6001)
,正常情况下,开发环境下的调试主机可以通过如下方式获取
import Constants from 'expo-constants'
const debuggerHost = Constants.expoConfig?.hostUri
// 192.168.123.233:8081
接着所要做的就是将 192.168.123.233:8081 替换成我们的目标端口 192.168.123.233:6001
这里以 axios 为例, 先为环境变量添加 EXPO_PUBLIC_API_URL=http://localhost:6001
,具体替换的代码如下所示
export const client = axios.create({
baseURL: getApiUrl(),
timeout: 5000,
})
export function getApiUrl() {
const apiUrl = process.env.EXPO_PUBLIC_API_URL
return replaceLocalhost(apiUrl)
}
export function getLocalhost() {
if (localhost !== undefined) return localhost
const debuggerHost = Constants.expoConfig?.hostUri
// 192.168.123.233:8081
localhost = debuggerHost?.split(':')[0] ?? 'localhost'
return localhost
}
export function replaceLocalhost(address: string) {
const PROTOCOL = 'http:'
const localhostRegex = new RegExp(`${PROTOCOL}\/\/localhost:`, 'g')
return address.replace(localhostRegex, () => `${PROTOCOL}//${getLocalhost()}:`)
}
2、端口转发
此外还需要执行以下命令转发端口。
adb reverse tcp:6001 tcp:6001
此时安卓模拟器便可正常请求本地后端服务的资源,IOS 端并未有该问题。
组件库的选择
如今在 UI 的选择上,我是毫不犹豫选择 Tailwindcss,在 RN 使用 Tailwindcss 有两个库可以作为选择 nativewind 和 twrnc。
nativewind
nativewind 采用 Web 的 className 属性,其用法如同 Web 开发使用 Tailwindcss 的写法,这里便不过多展示了。
twrnc
twrnc 的写法则有些不同,需要通过 tw 包装,然后填写到 style 中,就如下图所示
import { View, Text } from 'react-native'
import tw from 'twrnc'
const MyComponent = () => (
<View style={tw`p-4 android:pt-2 bg-white dark:bg-black`}>
<Text style={tw`text-md text-black dark:text-white`}>Hello World</Text>
</View>
)
但要值得注意的是,由于 RN 的组件样式中并不是完全兼容 Web 端,就比如说你想实现毛玻璃效果,通过 backdrop-blur 原子类就可以轻松实现,但是在原生移动端并不能生效,其原因就是原生组件的 View 并没有毛玻璃效果,想要实现则需要使用 expo-blur 这个库。
事实上有很多 Web 端支持的类,在移动端并不能生效,通常来说只适合用 Tailwindcss 来编写基本的宽高,内外边距等样式。
这两个库的区别
从 Web 开发使用的角度,nativewind 会更好用一些, npm 实际使用量也确实比 twrnc 来的多,但要在一些情况下,比如给第三方组件更改 props 的样式情况下就会没有 twrnc 那么直观了,例如一些第三方组件有 xxxStyle 属性,例如 contentContainerStyle,这时 twrnc 就方便很多。
<FlatList style={tw`flex-1`} contentContainerStyle={tw`p-4`} />
而 nativewind 则繁琐许多,下图例子。
// This component has two 'style' props
function ThirdPartyComponent({ style, contentContainerStyle, ...props }) {
return <FlatList style={style} contentContainerStyle={contentContainerStyle} {...props} />
}
// Call this once at the entry point of your app
remapProps(ThirdPartyComponent, {
className: 'style',
contentContainerClassName: 'contentContainerStyle',
})
// Now you can use the component with NativeWind
<ThirdPartyComponent className="p-5" contentContainerClassName="p-2" />
再者,twrnc 可以使用动态变量,例如在 RN 中经常需要处理安全区域,如下写法在 twrnc 就支持,但 nativewind 则不生效。
const { top } = useSafeAreaInsets();
<View style={tw`pt-[${top}]`}> // twrnc 支持
<View className={`pt-[${top}]`}> // nativewind 不支持