引言
ReactNative的很多第三方模块都不怎么维护了,现在质量有保障的是Expo官方提供的一系列库,比如expo-image可以替换很久没维护的react-native-fast-image,所以感觉RN开发的趋势就是Expo框架了。
我以前还挺担心Expo框架的一些第三方原生模块的集成和打包,其实当捋清楚它的功能和文档以后 还挺好用的,不用担心打包和第三方库的问题。
集成第三方原生库(比如支付宝、微信、友盟、极光推送)等,打包可以prebuild出ios和安卓目录,然后用Xcode或Android打包即可。
第三方库的集成原生代码可以使用AI生成。
1、生成Expo项目
查看npx create-expo-app@latest --help命令,初始化Expo项目如下,选一个就好了:
# 使用npm安装
npx create-expo-app 项目名
# 使用yarn安装
yarn create expo-app 项目名
# 使用pnpm安装
pnpm create expo-app 项目名
# 使用bun安装
bun create expo-app 项目名
初始化好以后,可以删除不要的web文件,比如截图所示:
2、集成 PullRefresh 功能
这里演示 Ios使用MJRefresh库,Android使用SmartRefreshLayout库。
首先创建一个Expo的原生模块,比如pull-refresh,跟着下面命令去创建就好。
npx create-expo-module@latest --local
2.1 ios集成 MJRefresh
修改modules/pull-refresh/ios/PullRefresh.podspec文件,加入MJRefresh
接着expo prebuild出安卓和ios目录
# prebuild出安卓和ios目录
npx expo prebuild --clean
编写ios原生代码,AI开发modules/pull-refresh/ios/PullRefreshModule.swift
import ExpoModulesCore
import MJRefresh
public class PullRefreshModule: Module {
public func definition() -> ModuleDefinition {
Name("PullRefresh")
// 定义视图组件
View(PullRefreshView.self) {
// 事件定义
Events("onRefresh", "onLoadMore")
// Props定义 - 功能控制
Prop("refreshEnabled") { (view: PullRefreshView, enabled: Bool) in
view.setRefreshEnabled(enabled)
}
Prop("loadMoreEnabled") { (view: PullRefreshView, enabled: Bool) in
view.setLoadMoreEnabled(enabled)
}
// Props定义 - 文字设置
Prop("refreshingText") { (view: PullRefreshView, text: String) in
view.setRefreshingText(text)
}
Prop("loadMoreText") { (view: PullRefreshView, text: String) in
view.setLoadMoreText(text)
}
// Props定义 - 状态控制
Prop("refreshingKey") { (view: PullRefreshView, key: String) in
view.setRefreshingKey(key)
}
Prop("loadingKey") { (view: PullRefreshView, key: String) in
view.setLoadingKey(key)
}
Prop("noMoreData") { (view: PullRefreshView, flag: Bool) in
view.setNoMoreDataFlag(flag)
}
// 注意:不需要暴露 endRefresh 等方法给 JS
// 已通过 Props 的 key 机制(refreshingKey、loadingKey)来控制刷新状态
// 这种方式更符合 React 的声明式编程理念,且避免了复杂的方法调用
}
}
}
编写ios原生代码,AI开发modules/pull-refresh/ios/PullRefreshView.swift
import ExpoModulesCore
import UIKit
import MJRefresh
// PullRefresh视图组件,集成MJRefresh功能
class PullRefreshView: ExpoView {
// 事件分发器
let onRefresh = EventDispatcher()
let onLoadMore = EventDispatcher()
// 刷新状态标记
private var isRefreshing = false
private var isLoadingMore = false
// 用于追踪状态变化的key
private var currentRefreshingKey: String = ""
private var currentLoadingKey: String = ""
private var currentNoMoreData: Bool = false
// 存储配置,等待找到ScrollView后应用
private var refreshEnabled = true
private var loadMoreEnabled = true
private var refreshingText = "正在刷新..."
private var loadMoreText = "加载更多..."
private var idlePullingText = "下拉刷新"
private var releasePullingText = "释放立即刷新"
private var idleLoadingText = "点击或上拉加载更多"
private var releaseLoadingText = "释放立即加载"
private var noMoreDataText = "没有更多数据了"
// 找到的ScrollView引用
private weak var targetScrollView: UIScrollView?
// 是否已经设置过刷新控件
private var hasSetupRefreshControls = false
// 同步队列,保证线程安全
private let syncQueue = DispatchQueue(label: "com.pullrefresh.sync")
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
// 不创建自己的ScrollView,等待子视图添加
}
// 递归查找第一个UIScrollView(优化:添加深度限制)
private func findScrollView(in view: UIView, depth: Int = 0, maxDepth: Int = 10) -> UIScrollView? {
// 防止过深的递归
guard depth < maxDepth else { return nil }
// 优先检查当前视图
if let scrollView = view as? UIScrollView {
return scrollView
}
// 广度优先搜索,先检查直接子视图
for subview in view.subviews {
if let scrollView = subview as? UIScrollView {
return scrollView
}
}
// 如果直接子视图中没有,再递归搜索
for subview in view.subviews {
if let scrollView = findScrollView(in: subview, depth: depth + 1, maxDepth: maxDepth) {
return scrollView
}
}
return nil
}
// 在找到的ScrollView上设置MJRefresh
private func setupRefreshControls(on scrollView: UIScrollView) {
// 防止重复设置
guard !hasSetupRefreshControls else {
print("⚠️ PullRefresh: 刷新控件已设置,跳过重复设置")
return
}
// 移除可能存在的旧控件
scrollView.mj_header = nil
scrollView.mj_footer = nil
// 设置下拉刷新
let header = MJRefreshNormalHeader { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.syncQueue.async {
self.isRefreshing = true
}
self.onRefresh([:])
}
}
header.lastUpdatedTimeLabel?.isHidden = true
header.stateLabel?.textColor = .gray
header.stateLabel?.font = .systemFont(ofSize: 14)
// 设置不同状态的文字(使用配置的文字)
header.setTitle(idlePullingText, for: .idle) // 默认状态
header.setTitle(releasePullingText, for: .pulling) // 下拉中(超过触发距离)
header.setTitle(refreshingText, for: .refreshing) // 正在刷新
header.setTitle("", for: .willRefresh) // 即将刷新
header.setTitle("", for: .noMoreData) // 没有更多数据
header.isHidden = !refreshEnabled
scrollView.mj_header = header
// 设置上拉加载
let footer = MJRefreshAutoNormalFooter { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.syncQueue.async {
self.isLoadingMore = true
}
self.onLoadMore([:])
}
}
footer.stateLabel?.textColor = .gray
footer.stateLabel?.font = .systemFont(ofSize: 14)
// 设置不同状态的文字(使用配置的文字)
footer.setTitle(idleLoadingText, for: .idle) // 默认状态
footer.setTitle(releaseLoadingText, for: .pulling) // 上拉中
footer.setTitle(loadMoreText, for: .refreshing) // 正在加载
footer.setTitle("", for: .willRefresh) // 即将加载
footer.setTitle(noMoreDataText, for: .noMoreData) // 没有更多数据
footer.isHidden = !loadMoreEnabled
scrollView.mj_footer = footer
hasSetupRefreshControls = true
print("✅ PullRefresh: 刷新控件设置成功")
}
// 结束刷新(线程安全)
func endRefresh() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.syncQueue.async {
guard self.isRefreshing else { return }
self.isRefreshing = false
}
self.targetScrollView?.mj_header?.endRefreshing()
}
}
// 结束加载更多(线程安全)
func endLoadMore() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.syncQueue.async {
guard self.isLoadingMore else { return }
self.isLoadingMore = false
}
self.targetScrollView?.mj_footer?.endRefreshing()
}
}
// 没有更多数据(线程安全)
func noMoreData() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.syncQueue.async {
self.isLoadingMore = false
}
self.targetScrollView?.mj_footer?.endRefreshingWithNoMoreData()
}
}
// 重置没有更多数据状态(线程安全)
func resetNoMoreData() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.targetScrollView?.mj_footer?.resetNoMoreData()
}
}
// 启用/禁用下拉刷新
func setRefreshEnabled(_ enabled: Bool) {
refreshEnabled = enabled
DispatchQueue.main.async { [weak self] in
self?.targetScrollView?.mj_header?.isHidden = !enabled
}
}
// 启用/禁用上拉加载
func setLoadMoreEnabled(_ enabled: Bool) {
loadMoreEnabled = enabled
DispatchQueue.main.async { [weak self] in
self?.targetScrollView?.mj_footer?.isHidden = !enabled
}
}
// 设置刷新文字
func setRefreshingText(_ text: String) {
refreshingText = text
DispatchQueue.main.async { [weak self] in
if let header = self?.targetScrollView?.mj_header as? MJRefreshNormalHeader {
// 只更新正在刷新状态的文字
header.setTitle(text, for: .refreshing)
}
}
}
// 设置加载更多文字
func setLoadMoreText(_ text: String) {
loadMoreText = text
DispatchQueue.main.async { [weak self] in
if let footer = self?.targetScrollView?.mj_footer as? MJRefreshAutoNormalFooter {
// 只更新正在加载状态的文字
footer.setTitle(text, for: .refreshing)
}
}
}
// 通过key控制刷新状态
func setRefreshingKey(_ key: String) {
if key != currentRefreshingKey && !key.isEmpty {
currentRefreshingKey = key
endRefresh()
}
}
// 通过key控制加载状态
func setLoadingKey(_ key: String) {
if key != currentLoadingKey && !key.isEmpty {
currentLoadingKey = key
endLoadMore()
}
}
// 设置没有更多数据状态
func setNoMoreDataFlag(_ flag: Bool) {
if flag != currentNoMoreData {
currentNoMoreData = flag
if flag {
noMoreData()
} else {
resetNoMoreData()
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// 优化:只在需要时调整子视图大小
if bounds.size != .zero {
for subview in subviews where subview.frame.size != bounds.size {
subview.frame = bounds
}
}
}
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
super.insertReactSubview(subview, at: atIndex)
addSubview(subview)
// 设置子视图frame
subview.frame = bounds
subview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// 优化:使用 DispatchQueue.main.async 而非延迟,更可靠
DispatchQueue.main.async { [weak self] in
self?.trySetupScrollView()
}
}
override func removeReactSubview(_ subview: UIView!) {
super.removeReactSubview(subview)
// 修复:检查 targetScrollView 是否是被移除视图的子孙
if let scrollView = targetScrollView {
// 递归检查 scrollView 是否在 subview 的子树中
if isView(scrollView, descendantOf: subview) {
targetScrollView = nil
hasSetupRefreshControls = false
print("⚠️ PullRefresh: ScrollView 已被移除,清除引用")
}
}
subview.removeFromSuperview()
}
// 辅助方法:检查一个视图是否是另一个视图的子孙
private func isView(_ view: UIView, descendantOf parentView: UIView) -> Bool {
var currentView: UIView? = view
while let current = currentView {
if current == parentView {
return true
}
currentView = current.superview
}
return false
}
// 尝试查找并设置ScrollView(优化版本)
private func trySetupScrollView() {
// 如果已经找到并设置过了,检查是否还有效
if let existingScrollView = targetScrollView {
// 检查 ScrollView 是否还在视图树中
if existingScrollView.superview == nil {
print("⚠️ PullRefresh: 检测到 ScrollView 已从视图树移除")
targetScrollView = nil
hasSetupRefreshControls = false
} else {
// ScrollView 仍然有效,不需要重新设置
return
}
}
// 在子视图中查找ScrollView
for subview in subviews {
if let scrollView = findScrollView(in: subview) {
targetScrollView = scrollView
setupRefreshControls(on: scrollView)
print("✅ PullRefresh: 找到 ScrollView 类型: \(type(of: scrollView))")
return
}
}
// 如果还是没找到,延迟重试一次(React Native 视图树可能还在构建中)
if targetScrollView == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
guard let self = self, self.targetScrollView == nil else { return }
for subview in self.subviews {
if let scrollView = self.findScrollView(in: subview) {
self.targetScrollView = scrollView
self.setupRefreshControls(on: scrollView)
print("✅ PullRefresh: 延迟查找成功,找到 ScrollView")
return
}
}
print("⚠️ PullRefresh: 未找到 ScrollView,请确保子组件包含可滚动视图(ScrollView/FlatList等)")
}
}
}
// 当视图添加到窗口时,再次尝试查找ScrollView
override func didMoveToWindow() {
super.didMoveToWindow()
if window != nil {
trySetupScrollView()
}
}
// 清理资源
deinit {
// 移除 MJRefresh 控件
targetScrollView?.mj_header = nil
targetScrollView?.mj_footer = nil
print("🔄 PullRefresh: 视图已销毁,清理资源")
}
}
编写src代码,AI开发modules/pull-refresh/src/PullRefresh.types.ts
import type { ReactNode } from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
// 下拉刷新事件回调
export type OnRefreshEventPayload = {
// 可以添加额外的事件数据
};
// 上拉加载事件回调
export type OnLoadMoreEventPayload = {
// 可以添加额外的事件数据
};
// PullRefresh视图组件的Props
export type PullRefreshViewProps = {
// 子组件
children?: ReactNode;
// 样式
style?: StyleProp<ViewStyle>;
// 是否启用下拉刷新,默认true
refreshEnabled?: boolean;
// 是否启用上拉加载,默认true
loadMoreEnabled?: boolean;
// 下拉刷新的文字提示
refreshingText?: string;
// 上拉加载的文字提示
loadMoreText?: string;
// 下拉刷新回调
onRefresh?: () => void;
// 上拉加载回调
onLoadMore?: () => void;
// 控制刷新状态的ID,每次改变会触发刷新结束
refreshingKey?: string | number;
// 控制加载状态的ID,每次改变会触发加载结束
loadingKey?: string | number;
// 是否显示"没有更多数据"
noMoreData?: boolean;
};
编写src代码,AI开发modules/pull-refresh/src/PullRefreshComponent.tsx
import * as React from 'react';
import { PullRefreshViewProps } from './PullRefresh.types';
import PullRefreshView from './PullRefreshView';
// Ref控制方法接口
export interface PullRefreshRef {
// 结束下拉刷新
endRefresh: () => void;
// 结束上拉加载
endLoadMore: () => void;
// 设置没有更多数据
noMoreData: () => void;
// 重置没有更多数据状态
resetNoMoreData: () => void;
}
/**
* 带Ref控制的PullRefresh组件
*
* 使用forwardRef支持通过ref调用控制方法
*
* @example
* ```tsx
* const pullRefreshRef = useRef<PullRefreshRef>(null);
*
* const handleRefresh = async () => {
* await fetchData();
* pullRefreshRef.current?.endRefresh();
* };
*
* <PullRefresh
* ref={pullRefreshRef}
* onRefresh={handleRefresh}
* >
* <YourContent />
* </PullRefresh>
* ```
*/
const PullRefresh = React.forwardRef<PullRefreshRef, PullRefreshViewProps>(
(props, ref) => {
// 使用状态来触发刷新结束
const [refreshingKey, setRefreshingKey] = React.useState('');
const [loadingKey, setLoadingKey] = React.useState('');
const [hasNoMoreData, setHasNoMoreData] = React.useState(false);
// 暴露控制方法给外部
React.useImperativeHandle(ref, () => ({
endRefresh: () => {
// 改变key来触发刷新结束
setRefreshingKey(Date.now().toString());
},
endLoadMore: () => {
// 改变key来触发加载结束
setLoadingKey(Date.now().toString());
},
noMoreData: () => {
setHasNoMoreData(true);
setLoadingKey(Date.now().toString());
},
resetNoMoreData: () => {
setHasNoMoreData(false);
},
}));
return (
<PullRefreshView
{...props}
refreshingKey={refreshingKey}
loadingKey={loadingKey}
noMoreData={hasNoMoreData}
/>
);
}
);
PullRefresh.displayName = 'PullRefresh';
export default PullRefresh;
编写src代码,AI开发modules/pull-refresh/src/PullRefreshModule.ts
import { NativeModule, requireNativeModule } from 'expo';
// 定义原生模块接口
declare class PullRefreshModule extends NativeModule {
// 这里可以添加模块级别的方法(如果需要)
}
// 加载原生模块
const nativeModule = requireNativeModule<PullRefreshModule>('PullRefresh');
export default nativeModule;
编写src代码,AI开发modules/pull-refresh/src/PullRefreshView.tsx
import { requireNativeView } from 'expo';
import * as React from 'react';
import { View } from 'react-native';
import { PullRefreshViewProps } from './PullRefresh.types';
// 导入原生视图 - 使用any类型避免类型问题
const NativeView: any = requireNativeView('PullRefresh');
/**
* PullRefresh 下拉刷新组件
*
* 跨平台原生下拉刷新和上拉加载体验:
* - iOS: 使用 MJRefresh 库
* - Android: 使用 SmartRefreshLayout 库
*
* @example
* ```tsx
* <PullRefreshView
* refreshEnabled={true}
* loadMoreEnabled={true}
* onRefresh={handleRefresh}
* onLoadMore={handleLoadMore}
* >
* <FlatList data={data} ... />
* </PullRefreshView>
* ```
*/
const PullRefreshView = React.forwardRef<View, PullRefreshViewProps>(
(props, ref) => {
const {
children,
style,
refreshEnabled = true,
loadMoreEnabled = true,
refreshingText = '正在刷新...',
loadMoreText = '加载更多...',
onRefresh,
onLoadMore,
...otherProps
} = props;
// 处理下拉刷新事件
const handleRefresh = React.useCallback(() => {
if (onRefresh) {
onRefresh();
}
}, [onRefresh]);
// 处理上拉加载事件
const handleLoadMore = React.useCallback(() => {
if (onLoadMore) {
onLoadMore();
}
}, [onLoadMore]);
return (
<NativeView
ref={ref}
style={style}
refreshEnabled={refreshEnabled}
loadMoreEnabled={loadMoreEnabled}
refreshingText={refreshingText}
loadMoreText={loadMoreText}
onRefresh={handleRefresh}
onLoadMore={handleLoadMore}
{...otherProps}
>
{children}
</NativeView>
);
}
);
PullRefreshView.displayName = 'PullRefreshView';
export default PullRefreshView;
编写src代码,AI开发modules/pull-refresh/index.ts
// 导出主要的PullRefresh组件(带ref支持)
export { default, PullRefreshRef } from './src/PullRefreshComponent';
// 导出基础视图组件
export { default as PullRefreshView } from './src/PullRefreshView';
// 导出原生模块(如果需要直接访问)
export { default as PullRefreshModule } from './src/PullRefreshModule';
// 导出类型定义
export * from './src/PullRefresh.types';
2.2 ios演示
2.3 android集成 SmartRefreshLayout
后面再更新...