跳到主要内容

199 篇博文 含有标签「iCoding」

个人简介

查看所有标签

电脑智能化

· 阅读需 1 分钟
Quany
软件工程师

ubuntu

网络配置:安装网卡驱动

sudo apt update
sudo apt install bcmwl-kernel-source

AI不间断运行

邮件往来审批 AI 执行

微信公众号

微信公众号

软件开发需求分类

· 阅读需 5 分钟
Quany
软件工程师

在中国软件开发实践中,需求分类已形成与国际接轨且符合本土管理规范的完整体系。以下是基于国内主流方法论(如软考、ASPICE标准、GB/T标准)及行业实践整理的需求分类框架:


一、按抽象级别和来源划分(三级需求体系)

这是中国软件工程领域最标准的分层方式,广泛应用于系统集成、政企项目及大型产品开发:

1. 业务需求(Business Requirements)

  • 定义:组织或业务部门的高层次目标,关注"为什么要做"
  • 来源:客户高层、业务战略、市场分析
  • 示例"提升订单处理效率30%""实现跨部门数据打通"
  • 文档形式:项目愿景与范围文档、商业论证

2. 用户需求(User Requirements)

  • 定义:最终用户对系统的期望,描述"用户想要什么"
  • 获取方式:访谈、问卷、用户故事、场景分析
  • 示例"用户能通过手机号快速注册""支持批量导入Excel数据"
  • 文档形式:用户故事(敏捷)、用例规约(传统)

3. 系统需求(System Requirements)

  • 定义:系统必须实现的具体技术特性和约束,说明"系统必须做什么"
  • 子类型
    • 功能需求:具体功能点(如"登录验证需支持短信验证码")
    • 非功能需求:性能、安全性、可靠性等质量属性(如"响应时间≤3秒")
  • 文档形式:软件需求规格说明书(SRS)

追溯关系:业务需求 → 用户需求 → 系统需求(双向可追溯性)


二、按性质划分(功能 vs 非功能)

这是开发团队执行层面的核心分类,直接影响技术方案:

功能需求(Functional Requirements)

  • 定义:系统必须完成的业务功能或服务
  • 占比:通常占需求总数的60-70%
  • 示例
    • 用户登录、权限管理
    • 订单创建、支付处理
    • 数据导入导出
  • 验证方法:功能测试、接口测试

非功能需求(Non-functional Requirements,NFR)

  • 定义:系统应满足的质量属性或约束条件
  • 关键类别
    类别说明示例
    性能需求响应时间、吞吐量"支持1000并发用户"
    安全性需求数据保护、访问控制"符合等保2.0三级要求"
    可靠性需求可用性、容错性"系统可用性≥99.9%"
    兼容性需求软硬件环境适配"支持Android 8.0+"
    可维护性代码规范、扩展性"采用微服务架构"
    合规性需求法律、行业标准"符合GDPR数据保护条例"

国内实践要点:非功能需求在合同验收中占比越来越高,尤其是政府项目对安全和性能有强制性要求。


三、按优先级划分(MoSCoW与Kano模型)

中国项目管理中常用的优先级划分方法:

1. MoSCoW方法

  • Must Have(必须有):核心功能,不满足则项目失败
  • Should Have(应有):重要功能,但可延期实现
  • Could Have(可以有):增值功能,资源允许时实现
  • Won't Have(暂不需要):明确排除在本次范围外

2. Kano模型

将需求分为三类:

  • 基本需求:必须满足,否则用户极度不满(如系统稳定性)
  • 期望需求:满足程度与用户满意度成正比(如界面美观度)
  • 兴奋需求:超出预期,带来惊喜(如AI智能推荐)

四、其他重要分类维度

1. 产品与过程需求

  • 产品需求:对软件本身的要求(如"验证用户输入合法性")
  • 过程需求:对开发过程的约束(如"必须使用Java开发"、"需通过CMMI三级评审")

2. 接口需求

  • 用户接口:UI/UX设计规范
  • 硬件接口:设备、传感器连接要求
  • 软件接口:第三方API、中间件集成
  • 通信接口:协议、数据格式(如RESTful API)

3. 派生需求 vs 基本需求

  • 基本需求:用户直接提出的原始需求
  • 派生需求:从技术实现或系统约束推导出的间接需求(如"因安全要求需增加日志审计功能")

五、中国本地化实践特点

1. 政策与合规驱动分类

  • 监管需求:如《数据安全法》《个人信息保护法》要求的数据存储、跨境传输限制
  • 信创适配需求:国产CPU(鲲鹏、飞腾)、操作系统(统信UOS、麒麟)、数据库适配
  • 等保/分保需求:明确的安全等级保护要求

2. 招投标导向分类

在政企项目中,需求常被分为:

  • 实质性需求:必须响应,否则废标(功能、性能指标)
  • 非实质性需求:评分项(界面美观、培训服务等)

3. 敏捷开发中的需求分层

国内互联网大厂普遍采用:

  • Epic(史诗):跨多个迭代的业务目标
  • Feature(特性):可交付的用户价值模块
  • User Story(用户故事):可在一个迭代内完成的细粒度需求
  • Task(任务):开发任务

六、需求分类的最佳实践

  1. 尽早建立可追溯性:使用需求管理工具(如Jira、PingCode、Ones)建立业务需求到代码的双向追溯
  2. 量化非功能需求:避免"快速响应"等模糊描述,必须量化(如"95%请求响应小于1秒")
  3. 分类评审机制:每类需求由不同角色评审(业务需求-客户签字,系统需求-架构师评审)
  4. 动态调整:采用敏捷模式,每迭代重新评估优先级,适应市场变化

通过清晰的需求分类,中国软件团队能有效应对需求蔓延评审效率低验收标准模糊等常见问题,也是通过软考高级系统分析师CMMI/ASPICE认证的关键能力要求。

微信公众号

微信公众号

TODO List

· 阅读需 1 分钟
Quany
软件工程师

下拉选择 [ ] 返回头

微信公众号

微信公众号

React Native 中实现流畅的列表下拉刷新和上拉加载更多功能

· 阅读需 13 分钟
Quany
软件工程师

在 React Native 中实现流畅的列表下拉刷新和上拉加载更多功能,关键在于理解核心组件并合理管理状态。下面我将为你梳理最佳实践方案。

🔄 核心实现方案

实现下拉刷新和上拉加载主要有两种路径:使用 React Native 官方提供的组件,或者借助功能丰富的第三方库。

方案核心组件/库优点适用场景
官方方案FlatList + RefreshControl稳定性高,无需额外依赖,官方维护需要标准、轻量级的刷新加载功能,项目限制外部依赖
第三方库react-native-SmartRefreshLayout功能丰富,UI 定制性强,可能跨平台一致性更好需要更精美的动画效果或更复杂交互

使用官方 FlatList 方案

这是最直接、最常用的方法。FlatList 组件自身就提供了实现这些功能的关键属性:

  • 下拉刷新 (onRefresh & refreshing): 通过 refreshing 属性控制刷新状态指示器的显示,onRefresh 属性指定下拉时触发的函数。
  • 上拉加载更多 (onEndReached): 当列表滚动到底部阈值时触发 onEndReached 回调函数。

一个结合 Hooks 的基本示例代码如下:

import React, { useState, useCallback } from 'react';
import { FlatList, Text, View, ActivityIndicator, RefreshControl } from 'react-native';

const AdvancedList = () => {
const [data, setData] = useState(initialData);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);

// 下拉刷新
const onRefresh = useCallback(async () => {
setRefreshing(true);
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 2000));
// 通常这里会获取最新数据并重置页码
setData(initialData);
setHasMore(true);
setRefreshing(false);
}, []);

// 上拉加载更多
const onEndReached = useCallback(async () => {
// 防止重复请求、无更多数据时不再请求
if (loadingMore || !hasMore) return;

setLoadingMore(true);
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 2000));
// 模拟追加数据
const newData = [...data, ...moreData];
setData(newData);
setLoadingMore(false);
// 模拟数据加载完毕的情况
if (newData.length >= 30) {
setHasMore(false);
}
}, [loadingMore, hasMore, data]);

const renderFooter = () => {
if (!hasMore) {
return (
<View style={{ padding: 10, alignItems: 'center' }}>
<Text>没有更多数据了</Text>
</View>
);
}
return loadingMore ? <ActivityIndicator style={{ padding: 10 }} /> : null;
};

return (
<FlatList
data={data}
renderItem={({ item }) => <Text style={{ padding: 20 }}>{item.title}</Text>}
keyExtractor={item => item.id}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
// 可自定义样式,例如 tintColor (iOS), colors (Android) 等
/>
}
onEndReached={onEndReached}
onEndReachedThreshold={0.2} // 调整触发灵敏度,表示距离底部 20% 时触发
ListFooterComponent={renderFooter}
/>
);
};

// 模拟数据
const initialData = [{id: '1', title: '初始项目1'}, {id: '2', title: '初始项目2'}];
const moreData = [{id: '3', title: '更多项目3'}, {id: '4', title: '更多项目4'}];

export default AdvancedList;

探索第三方库

如果你需要更强大的下拉动画或更精细的控制,可以考虑像 react-native-SmartRefreshLayout 这样的第三方库。它们通常提供更丰富的内置样式和开箱即用的高级功能。使用时请参考其特定文档进行安装和配置。

⚠️ 关键细节与优化

实现基本功能后,以下几点能显著提升稳定性和用户体验:

  1. 状态管理是核心:务必妥善管理 refreshingloadingMorehasMore 等状态。确保在请求开始和结束时正确更新状态,避免重复请求或状态不一致。
  2. 防止 onEndReached 重复触发:这是常见问题。可以通过设置标志位(如 loadingMore)在请求期间阻止再次触发。注意 onEndReached 可能在滚动时多次调用,需要合理设计逻辑。
  3. 优化网络请求:在实际项目中,将数据获取逻辑封装成独立的函数或服务。使用 async/await 处理异步操作,并务必添加错误处理,在请求失败时给予用户反馈并提供重试机制。
  4. 自定义视觉反馈:除了默认的 ActivityIndicator,你可以通过 ListFooterComponentListHeaderComponent 完全自定义加载中和数据加载完的界面,使其更符合应用整体设计。

💎 方案选择建议

对于大多数应用,从官方 FlatList 方案入手是最佳选择。它稳定、可靠,能满足基本需求,且社区支持丰富。

如果你的项目对下拉刷新的动画效果有特殊要求,或者你希望减少自行处理细节状态的工作量,那么探索一款维护良好的第三方库(如 react-native-SmartRefreshLayout)会是更高效的选择。

希望这些实践建议能帮助你顺利实现功能!如果你在实现过程中遇到更具体的问题,比如如何与特定状态管理库结合,欢迎随时提出。

在 React Native 应用开发中,优化 FlatListonEndReached 回调以防止其重复触发,是确保列表分页加载性能和使用体验的关键。下面这张表格汇总了核心的解决方案,帮助你快速抓住要点。

优化维度具体方案说明
触发时机控制合理设置 onEndReachedThreshold将其设置为一个较小的值(如 0.10.01),表示滚动到距离列表底部 10%1% 时触发,避免过早或意外触发。
滚动动量管理使用 onMomentumScrollBegin 标志位利用滚动动量事件,确保一次完整的滚动动作只触发一次加载。
函数执行频率控制实现防抖(Debounce)在连续快速触发时,只执行最后一次调用,有效避免重复请求。

💡 核心方案详解与代码实现

1. 标志位控制法

这种方法通过一个标志变量来记录是否已经开始加载,从而避免在一次滚动中重复触发。

import React, { useRef, useState, useCallback } from 'react';
import { FlatList, View, Text } from 'react-native';

const OptimizedList = () => {
const [data, setData] = useState([...]); // 初始数据
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
// 使用 useRef 创建在渲染间保持不变的标志位
const isOnEndReachedCalled = useRef(false);

const loadMoreData = useCallback(async () => {
// 如果正在加载或已无更多数据,则直接返回
if (isLoadingMore || !hasMore) return;

setIsLoadingMore(true);
// 模拟网络请求
const newData = await fetchMoreData();
setData(prevData => [...prevData, ...newData]);
setIsLoadingMore(false);
// 根据返回数据判断是否还有更多
setHasMore(newData.length > 0);
// 加载完成后,重置标志位,允许下一次触发
isOnEndReachedCalled.current = false;
}, [isLoadingMore, hasMore]);

const handleOnEndReached = useCallback(() => {
// 关键:如果本次滚动还未触发加载,则执行
if (!isOnEndReachedCalled.current) {
loadMoreData();
isOnEndReachedCalled.current = true; // 设置标志位,阻止重复触发
}
}, [loadMoreData]);

return (
<FlatList
data={data}
renderItem={({ item }) => <Text>{item.title}</Text>}
onEndReached={handleOnEndReached}
onEndReachedThreshold={0.1} // 设置为一个较小的阈值
onMomentumScrollBegin={() => {
// 当用户开始滚动时,重置标志位,为下一次触发做准备
isOnEndReachedCalled.current = false;
}}
ListFooterComponent={() => (
isLoadingMore ? <Text>加载中...</Text> : null
)}
// 其他优化属性
maxToRenderPerBatch={5}
windowSize={5}
/>
);
};

原理:通过 onMomentumScrollBeginisOnEndReachedCalled 标志位协同工作,确保一次“滚动手势”只触发一次 onEndReached

2. 防抖函数优化

防抖技术可以确保在频繁触发事件时,只执行最后一次操作。

import { debounce } from 'lodash'; // 或者自己实现一个简单的防抖函数

// 自定义一个简单的防抖Hook
function useDebounce(callback, delay) {
const debouncedFn = useRef(debounce(callback, delay)).current;
return debouncedFn;
}

// 在组件中使用
const MyListComponent = () => {
// ... 状态定义

const loadMoreData = useCallback(async () => {
// 加载数据的逻辑
}, [/* 依赖 */]);

// 创建一个防抖版本的加载函数,300毫秒内连续触发只会执行一次
const debouncedLoadMore = useDebounce(loadMoreData, 300);

const handleOnEndReached = useCallback(() => {
if (!isLoadingMore && hasMore) {
debouncedLoadMore();
}
}, [isLoadingMore, hasMore, debouncedLoadMore]);

return (
<FlatList
// ... 其他属性
onEndReached={handleOnEndReached}
onEndReachedThreshold={0.2}
/>
);
};

原理:当 onEndReached 被连续快速调用时,防抖函数会延迟执行 loadMoreData。如果在延迟时间内再次被调用,会取消之前的计时并重新计时,从而确保只有最后一次滚动操作会真正加载数据。

⚙️ 其他重要优化配置

除了防止重复触发,合理配置 FlatList 的其他属性对整体性能也至关重要。

  • 优化渲染性能:通过 maxToRenderPerBatch(控制每批渲染的项目数)和 windowSize(控制渲染窗口大小)等属性,可以减少JS线程的负担,使滚动更加流畅。例如,可以设置 maxToRenderPerBatch={5}windowSize={5}
  • 确保外层容器高度:有时 FlatList 的父容器高度计算异常会导致 onEndReached 在初始化时就被触发。确保父容器有明确的高度(例如设置 style={{ flex: 1 }}{height: '100%'})可以解决此问题。
  • 使用稳定的 Key:为 keyExtractor 提供一个唯一且稳定的键(如数据项的 id),而不是数组的 index,这有助于 FlatList 更高效地识别项目变化,减少不必要的重渲染。
  • 考虑使用性能更好的列表组件:对于超长列表或极其复杂的列表项,可以考虑使用社区提供的性能更佳的组件,如 Shopify 的 FlashList,它在底层做了大量优化。

💎 总结与建议

你可以根据项目复杂度和需求,选择最适合的方案。对于大多数场景,“标志位控制法” 因其简单有效通常是首选。如果列表滚动非常频繁,可以结合防抖函数进一步优化。同时,别忘了配置好 FlatList 的其他优化属性,并从整体上保证组件设计的合理性。

希望这些具体的方案能帮助你有效解决 onEndReached 重复触发的问题!如果你在实现过程中遇到其他难题,欢迎随时提出。

在复杂列表场景下,单纯的基础虚拟化可能仍会遇到性能瓶颈。结合虚拟化技术进行深度优化,需要像指挥一场交响乐一样,让数据加载、渲染和内存管理各个环节精准协同。下面我将从核心策略、关键技术到实践方案,为你系统梳理提升长列表刷新加载性能的方法。

📊 复杂列表的额外挑战与优化方向

首先,我们来看看复杂列表相比简单列表带来了哪些特定的性能挑战,这决定了我们的优化方向。

挑战维度对性能的影响基础虚拟化的不足
数据量极大网络传输、解析耗时过长,阻塞渲染仅优化了渲染,未优化数据到达速度
列表项结构复杂单个项渲染成本高,即使数量不多也会卡顿仍会尝试渲染视口内所有复杂项
交互频繁与数据实时性频繁的状态更新导致大量组件重渲染缺乏对更新粒度的控制
内存占用高尤其是图片、多媒体内容易引发内存泄漏虚拟化主要减少DOM节点,需额外内存管理

🚀 提升性能的核心策略

针对上述挑战,可以采取以下关键策略,它们共同构成了高性能虚拟列表的支柱。

  1. 数据层面的异步与分片加载 虚拟化解决了渲染量,但数据本身的加载速度也需要优化。

    • 增量加载:不要等待所有数据都获取完毕再渲染。采用“无限滚动”模式,滚动到底部时自动加载下一页数据。
    • 数据分片与优先级:对超大规模数据(如10万条),可在首屏优先加载前几百条,同时利用 Web Worker 在后台线程静默加载和预处理剩余数据,避免阻塞主线程。
    • 智能预加载:根据滚动速度预测用户行为。快速滚动时,预加载更多数据;慢速浏览时,减少预加载以节省资源。
  2. 渲染层面的精细控制与复用 这是降低每个列表项渲染开销的关键。

    • 极致复用与防重渲染:对列表项组件使用 React.memoPureComponent,并结合自定义的 arePropsEqual 函数,确保只有当前项依赖的数据真正变化时才会重渲染。
    • 降低组件复杂度:简化列表项组件结构,避免深层次的嵌套。对于复杂项,可考虑将交互状态提升到父组件或使用状态管理库,减少内部状态导致的更新。
    • 占位符与骨架屏:数据加载过程中,先使用骨架屏占位,保持布局稳定,提升用户体验。
  3. 内存与资源管理 尤其在移动端或图片、视频多的场景,内存管理至关重要。

    • 图片优化与卸载:使用专门的图片库(如 react-native-fast-image),它们能更好地处理缓存和内存。确保图片滚出视口时,其内存能被正确释放。
    • 主动清理资源:在组件卸载或数据更新时,清理所有副作用,如事件监听器、定时器等,防止内存泄漏。

🛠️ 关键技术方案与代码实践

以下是一些可以直接应用的技术方案和代码思路。

  1. 虚拟化库的高级用法

    • 动态高度处理:使用支持动态高度的虚拟化库(如 react-windowVariableSizeListreact-virtuoso)。关键在于提前测量或预估项高度,并缓存结果以避免重复计算。
    • ** Overscan(缓冲区)配置**:合理设置 overscanCount 或类似参数。在可视区域外多渲染几行,快速滚动时避免出现空白,但设置过大会增加渲染负担。
  2. 数据获取与状态更新优化

    • 防抖与节流:对搜索过滤等操作进行防抖,避免频繁发起请求和重新渲染虚拟列表。
    • 不可变数据与结构共享:更新列表数据时,使用不可变数据更新方式。这样可以快速通过引用比较判断数据是否变化,优化 React.memo 的效果。
  3. 针对React Native的特定优化 如果使用 React Native 的 FlatList,以下配置尤为关键:

    <FlatList
    data={data}
    keyExtractor={(item) => item.id} // 必须提供稳定的key
    initialNumToRender={10} // 初始渲染数量,不宜过大
    maxToRenderPerBatch={5} // 每批增量渲染数量,控制卡顿
    windowSize={5} // 渲染区域高度是可视区域的倍数,调小可节省内存
    removeClippedSubviews={true} // 卸载屏幕外视图(Android尤其需要)
    getItemLayout={(data, index) => ( // 提前告知项尺寸,避免测量开销
    {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
    )}
    // 结合你的异步加载逻辑
    onEndReached={loadMoreData}
    onEndReachedThreshold={0.5} // 触发加载的阈值
    />

💎 总结与进阶建议

将虚拟化技术与上述策略结合,能显著提升复杂长列表的性能。关键在于理解一个核心思想:虚拟化不是一颗银弹,而是一个性能基础。在此之上,需要数据、渲染、内存多方位的协同优化。

对于追求极致性能的场景,还可以探索:

  • 更先进的虚拟化库:如 react-virtuoso,它内置了对分组列表、自动高度测量等复杂场景的良好支持。
  • 并发渲染:在支持 React 18+ 的环境中,考虑使用 useTransitionuseDeferredValue 将列表渲染标记为可中断的非紧急更新,避免交互卡顿。

希望这些策略和方案能为你优化复杂列表提供清晰的路径。如果你能分享更多关于你遇到的具体场景(例如,列表项的主要复杂度来源是图片、计算还是复杂的交互?),我可以提供更有针对性的建议。

微信公众号

微信公众号

想通过命令行安装特定版本的 Android NDK

· 阅读需 3 分钟
Quany
软件工程师

您想通过命令行安装特定版本的 Android NDK,但需要先澄清一个关键点:adb 命令主要用于与已连接的 Android 设备通信,而安装 NDK 是在您的本地开发电脑上进行的操作。正确的工具是 Android SDK 的 sdkmanager

下面为您详细介绍两种方案来满足您的需求。

🔧 方案一:使用 sdkmanager 安装指定版本(推荐)

这是最直接、由 Android 官方推荐的方法。sdkmanager 是一个专门用于管理 Android SDK 包(包括 NDK)的命令行工具。

  1. 定位 sdkmanager:首先,您需要找到您电脑上 Android SDK 目录下的 sdkmanager 工具。它通常位于 android-sdk/tools/bin/ 目录下。如果您设置了 ANDROID_SDK_ROOTANDROID_HOME 环境变量,会更容易定位。

  2. 执行安装命令:打开终端(或命令提示符),使用以下命令格式进行安装。请注意,sdkmanager 期望的版本字符串格式可能与您提供的略有不同,它通常遵循 major.minor.revision 的模式。

    sdkmanager "ndk;21.0.6113669"

    在命令执行过程中,系统会显示许可协议,您需要接受才能继续安装。您也可以在命令中直接加上 --accept_licenses 参数来自动接受。

  3. 验证安装:安装完成后,您可以在 $ANDROID_SDK_ROOT/ndk/ 目录下找到一个名为 21.0.6113669 的文件夹,这就是安装好的 NDK。

⚙️ 方案二:配置项目使用已安装的 NDK

如果您的开发环境中已经安装了多个版本的 NDK(您的错误信息显示本地有众多版本),另一个高效的策略是直接修改项目配置,使其使用一个已有的、兼容的版本,而不是重新安装。

  • 修改项目配置:在您项目模块(通常是 app)级的 build.gradle 文件中,指定一个您本地已有的 NDK 版本。从您的错误日志看,21.3.6528147 是一个很接近的选择。

    android {
    ndkVersion "21.3.6528147" // 指定一个本地已有的兼容版本
    }

💎 方案对比与建议

为了让您更清晰地选择,这里有一个简单的对比表格:

方案核心命令/操作优点适用场景
方案一sdkmanager "ndk;21.0.6113669"官方推荐,直接满足版本要求需要精确匹配特定 NDK 版本时
方案二build.gradle 中设置 ndkVersion快速,无需下载,利用现有资源项目有一定灵活性,允许使用兼容版本

综合建议

  • 如果您确定项目必须使用 21.0.6113669 这个特定版本,请使用方案一
  • 如果希望快速解决问题,并且经过测试后确认使用一个已有的高版本(如 21.3.6528147)也能成功编译,那么方案二是更高效的选择。

⚠️ 重要注意事项

  • 确保 sdkmanager 可用:在执行方案一的命令前,请确认 sdkmanager 在您的系统环境变量 PATH 中,或者您需要切换到其所在目录执行。
  • 版本可用性:极老的或非常规的 NDK 版本可能已从官方仓库中移除。如果 sdkmanager 提示找不到该版本,您可能需要从其他渠道手动下载并配置。
  • AGP 版本:您遇到这个问题的原因可能是项目使用的 Android Gradle Plugin (AGP) 版本较老,其默认的 NDK 版本恰好是 21.0.6113669。长远考虑,升级 AGP 版本是一个更好的选择,因为它会自动管理并使用更新的、兼容性更好的 NDK。

希望这些方案能帮助您解决问题!如果您在操作中遇到任何其他情况,欢迎随时提出。

微信公众号

微信公众号

React Native 0.83

· 阅读需 4 分钟
Quany
软件工程师

React 19.2 的新特性确实为 React Native 开发带来了更优雅的解决方案,特别是 useEffectEvent,它能显著改善副作用管理和状态处理。下面的表格总结了这些特性的核心用途和兼容性,方便你快速了解。

特性 核心用途 React Native 兼容性

useEffectEvent 从 Effect 中分离“事件逻辑”,解决闭包陷阱,简化依赖数组 需 RN 0.83+(已集成 React 19.2)

Activity 组件 条件渲染时隐藏而非卸载组件,以保留状态(如表单输入、滚动位置) 需 RN 0.83+(已集成 React 19.2)

React Compiler 1.0 构建时自动优化重渲染,减少手动useMemo, useCallback使用 需项目配置启用(目前仍为实验性)

性能工具增强 新的 DevTools 提供更强大的性能追踪和网络请求调试能力 需 RN 0.83+

💡 核心特性详解与应用

  1. useEffectEvent:告别闭包陷阱

useEffectEvent 是 React 19.2 中一个解决常见副作用的 Hook,它允许你将一个包含“非响应式”逻辑(即那些不应该导致副作用重新执行的逻辑)的函数标记为 “Effect 事件”。

• 解决的问题:在以往的 React 开发中,如果一个 Effect 内部使用的函数引用了组件的 props 或 state,你必须将该函数放入 Effect 的依赖数组中。但这常常会导致 Effect 过度重复执行。使用 useRef 手动管理最新值虽然可行,但增加了代码的复杂度和出错风险。

• React Native 使用示例:一个典型的场景是处理 WebSocket 消息。你希望 WebSocket 连接只在 roomId 改变时重建,但消息处理函数又需要能访问到最新的 messages 状态。

    // 注意:此示例基于 React 19.2 的 Canary 版本,使用时请确认你的 React 版本
import { useState, useEffect, useEffectEvent } from 'react';
import { View, Text } from 'react-native';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);

// 使用 useEffectEvent 包裹消息处理逻辑
const handleNewMessage = useEffectEvent((newMessage) => {
// 此函数总能访问到最新的 messages 状态
setMessages((currentMessages) => [...currentMessages, newMessage]);
});

useEffect(() => {
const socket = new WebSocket(`wss://myapi.com/chat/${roomId}`);

socket.onmessage = (event) => {
const messageData = JSON.parse(event.data);
handleNewMessage(messageData); // 在 Effect 内部调用
};

return () => socket.close();
}, [roomId]); // 依赖数组只需包含 roomId,handleNewMessage 不需要加入

return (
<View>
{messages.map(msg => <Text key={msg.id}>{msg.text}</Text>)}
</View>
);
}

• 重要限制:useEffectEvent 返回的函数必须仅在 Effect 内部调用,而不能直接作为如 onPress 这类事件处理函数使用。

  1. Activity 组件:优雅的状态保持

Activity 组件提供了一种更优雅的方式来条件性地渲染组件,其核心优势在于当组件被隐藏时,它不会被卸载,从而完美保留所有状态。

• 解决的问题:在 React Native 中,我们常用条件渲染 。但当 isVisible 变为 false 时,组件会被卸载,其内部状态(如输入框的内容、滚动位置)会完全丢失。再次显示时,组件会重新挂载,状态被重置。

• React Native 使用示例:在具有多个标签页的界面中,使用 Activity 可以确保切换标签时各个标签页的状态得以保留。

    // 注意:此组件在 React 19.2 中引入
import { Activity } from 'react';
import { View, Text, TextInput } from 'react-native';

function TabScreen({ isActive }) {
const [inputValue, setInputValue] = useState(''); // 这个状态在标签隐藏时不会丢失

return (
<Activity mode={isActive ? 'visible' : 'hidden'}>
<View>
<Text>这个标签页的状态会被保留</Text>
<TextInput
value={inputValue}
onChangeText={setInputValue}
placeholder="尝试输入后切换标签..."
/>
</View>
</Activity>
);
}

🔐 使用前的重要检查

在将 React 19.2 的新特性用于 React Native 项目前,请务必确认以下两点:

  1. React Native 版本:确保你的项目使用的是 React Native 0.83 或更高版本,因为该版本才开始集成 React 19.2。你可以通过查看 package.json 文件中的 react-native 依赖版本来确认。
  2. React 版本:确认项目的 react 和 react-native 包版本兼容。React Native 0.83 内置了兼容的 React 版本。如果你想在更早的 RN 版本中尝试,需要手动升级 React 到 19.2,但这可能会带来风险,强烈建议通过升级 RN 版本来获取稳定支持。

希望这些具体的示例能帮助你在 React Native 项目中更好地运用 React 19.2 的新特性。如果你在升级版本或使用特定特性时遇到具体问题,我很乐意提供进一步的探讨。

微信公众号

微信公众号

App 系统架构设计

· 阅读需 14 分钟
Quany
软件工程师

1. 概述

本项目是一个基于 React Native 的移动端销售管理系统,模仿纷享销客 App 的功能和界面设计,主要服务于企业销售团队的日常管理工作。

1.1 技术栈

类别技术选型版本
框架React Native0.82.1
语言TypeScript5.8.x
状态管理Zustand5.x
导航React Navigation7.x
UI 组件库@rneui/themed4.x
样式方案NativeWind (Tailwind)4.x
HTTP 客户端Axios1.7.x
表单处理Formik2.x
国际化react-i18next16.x
存储AsyncStorage + Keychain-
热更新CodePush-

2. 整体架构图

┌─────────────────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Navigation (React Navigation) ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ HomeTabs │ │ Login │ │ WebView │ │ Setting │ ││
│ │ │ (企信/工作台│ │ Register │ │ Browser │ │ Profile │ ││
│ │ │ /CRM/我) │ │ │ │ │ │ │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ UI Components ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ Common │ │ Form │ │ Message │ │ Layout │ │ Business │ ││
│ │ │Components│ │Components│ │Components│ │Components│ │Components│ ││
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ Business Layer │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ State Management ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ AuthStore │ │ ChatStore │ │ UserStore │ │ BusinessStore│ ││
│ │ │ (认证状态) │ │ (聊天状态) │ │ (用户状态) │ │ (业务状态) │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Services ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││
│ │ │ AuthService │ │ UserService │ │OrderService │ │CacheService │ ││
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ Data Layer │
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ API Client ││
│ │ ┌───────────────────────────────────────────────────────────────────┐ ││
│ │ │ Axios Instance (拦截器、Token管理、错误处理、Loading状态) │ ││
│ │ └───────────────────────────────────────────────────────────────────┘ ││
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││
│ │ │OrderAPI │ │PartnerAPI││CustomerAPI││ApprovalAPI││EmployeeAPI│ ││
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────────────────┐│
│ │ Local Storage ││
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ││
│ │ │ AsyncStorage │ │ Keychain │ │ react-native- │ ││
│ │ │ (用户偏好/缓存) │ │ (安全凭证存储) │ │ storage │ ││
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ││
│ └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Theme │ │ i18n │ │ Config │ │ Utils │ │ Types │ │
│ │ (主题) │ │ (国际化) │ │ (配置) │ │ (工具) │ │ (类型) │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘

3. 目录结构设计

src/
├── api/ # API 层
│ ├── client.ts # Axios 客户端配置
│ ├── index.ts # API 统一导出
│ ├── sales.ts # 销售相关 API
│ ├── customer.ts # 客户相关 API (建议拆分)
│ ├── order.ts # 订单相关 API (建议拆分)
│ └── types/ # API 请求/响应类型定义
│ ├── request.ts
│ └── response.ts

├── components/ # 通用组件
│ ├── common/ # 基础通用组件
│ │ ├── Button/
│ │ ├── Card/
│ │ ├── Empty/
│ │ ├── ErrorBoundary/
│ │ └── index.ts
│ │
│ ├── form/ # 表单组件
│ │ ├── FormInput.tsx
│ │ ├── PasswordInput.tsx
│ │ ├── PhoneInput.tsx
│ │ ├── AgreementCheckbox.tsx
│ │ └── index.ts
│ │
│ ├── layout/ # 布局组件
│ │ ├── Header/
│ │ ├── TabBar/
│ │ ├── SafeContainer/
│ │ └── index.ts
│ │
│ ├── business/ # 业务组件
│ │ ├── CustomerCard/
│ │ ├── OrderItem/
│ │ ├── ApprovalCard/
│ │ └── index.ts
│ │
│ └── message/ # 消息相关组件
│ ├── MessageItem/
│ ├── ChatBubble/
│ └── index.ts

├── screens/ # 页面/屏幕
│ ├── user/ # 用户相关页面
│ │ ├── Login.tsx
│ │ ├── Register.tsx
│ │ └── Profile.tsx
│ │
│ ├── home/ # 主页 Tab 页面
│ │ ├── index.tsx # Tab 导航配置
│ │ └── tabs/
│ │ ├── CorporateMessage.tsx # 企信
│ │ ├── Workbench.tsx # 工作台
│ │ ├── Crm.tsx # CRM
│ │ └── My.tsx # 我
│ │
│ ├── crm/ # CRM 模块页面
│ │ ├── CustomerList/
│ │ ├── CustomerDetail/
│ │ ├── OrderList/
│ │ └── OrderDetail/
│ │
│ ├── approval/ # 审批模块页面
│ │ ├── ApprovalList/
│ │ └── ApprovalDetail/
│ │
│ └── common/ # 通用页面
│ ├── Setting.tsx
│ ├── SplashScreen.tsx
│ ├── PDFViewer.tsx
│ └── web/
│ └── index.tsx

├── navigation/ # 导航配置
│ ├── index.tsx # 根导航器
│ ├── types.ts # 导航类型定义
│ ├── linking.ts # 深度链接配置
│ └── helpers.ts # 导航辅助函数

├── store/ # 状态管理 (Zustand)
│ ├── index.ts # Store 工具函数
│ ├── authStore.ts # 认证状态
│ ├── userStore.ts # 用户状态
│ ├── chatStore.ts # 聊天状态
│ └── types.ts # Store 类型定义

├── services/ # 业务服务层
│ ├── authService.ts # 认证服务
│ ├── userService.ts # 用户服务
│ ├── storageService.ts # 存储服务
│ └── pushService.ts # 推送服务

├── hooks/ # 自定义 Hooks
│ ├── useAuth.ts
│ ├── useUser.ts
│ ├── useToast.ts
│ ├── useRefresh.ts
│ └── index.ts

├── contexts/ # React Context
│ ├── AuthContext.tsx
│ ├── UserContext.tsx
│ ├── ChatContext.tsx
│ └── index.ts

├── theme/ # 主题配置
│ ├── index.ts # 主题导出
│ ├── colors.ts # 颜色定义
│ ├── typography.ts # 字体定义
│ ├── spacing.ts # 间距定义
│ ├── shadows.ts # 阴影定义
│ └── ThemeContext.tsx # 主题 Context

├── constants/ # 常量定义
│ ├── config.ts # 应用配置
│ ├── sizes.ts # 尺寸常量
│ ├── api.ts # API 相关常量
│ └── index.ts

├── types/ # 全局类型定义
│ ├── api.ts # API 类型
│ ├── navigation.ts # 导航类型
│ ├── models/ # 数据模型类型
│ │ ├── user.ts
│ │ ├── customer.ts
│ │ ├── order.ts
│ │ └── index.ts
│ └── index.ts

├── utils/ # 工具函数
│ ├── env.ts # 环境变量工具
│ ├── storage.ts # 存储工具
│ ├── timezone.ts # 时区工具
│ ├── toast.ts # Toast 工具
│ ├── format.ts # 格式化工具
│ ├── validation.ts # 验证工具
│ └── index.ts

├── locales/ # 国际化资源
│ ├── en.json
│ ├── zh.json
│ └── index.ts

└── index.tsx # 应用入口

4. 分层架构详解

4.1 表现层 (Presentation Layer)

负责 UI 渲染和用户交互。

// screens/crm/CustomerList/index.tsx
import { useCustomerList } from '@/hooks/useCustomer';
import { CustomerCard } from '@/components/business';
import { SafeContainer, EmptyState } from '@/components/layout';

export default function CustomerList() {
const { data, loading, refresh } = useCustomerList();

if (loading) return <Loading />;
if (!data?.length) return <EmptyState type="customer" />;

return (
<SafeContainer>
<FlatList
data={data}
renderItem={({ item }) => <CustomerCard customer={item} />}
onRefresh={refresh}
/>
</SafeContainer>
);
}

4.2 业务逻辑层 (Business Layer)

4.2.1 状态管理 (Zustand Store)

// store/authStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface AuthState {
isAuthenticated: boolean;
token: string | null;
user: UserInfo | null;
}

interface AuthActions {
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
setUser: (user: UserInfo) => void;
}

export const useAuthStore = create<AuthState & AuthActions>()(
persist(
immer((set, get) => ({
// State
isAuthenticated: false,
token: null,
user: null,

// Actions
async login(email: string, password: string) {
const { token, user } = await authService.login(email, password);
set(state => {
state.isAuthenticated = true;
state.token = token;
state.user = user;
});
},

async logout() {
await authService.logout();
set(state => {
state.isAuthenticated = false;
state.token = null;
state.user = null;
});
},

async refreshToken() {
const newToken = await authService.refreshToken(get().token);
set(state => {
state.token = newToken;
});
},

setUser(user: UserInfo) {
set(state => {
state.user = user;
});
},
})),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
partialize: (state) => ({ token: state.token, user: state.user }),
}
)
);

4.2.2 服务层 (Services)

// services/authService.ts
import * as Keychain from 'react-native-keychain';
import client from '@/api/client';
import Storage from '@/utils/storage';

export interface AuthService {
login(email: string, password: string): Promise<LoginResult>;
logout(): Promise<void>;
refreshToken(token: string | null): Promise<string>;
validateToken(): Promise<boolean>;
}

export const authService: AuthService = {
async login(email: string, password: string) {
// 1. 验证凭证
const validateRes = await client.post('/login/validate', { email, password });

// 2. 获取 Token
const loginRes = await client.post('/login/email', {
...validateRes.data,
email,
});

// 3. 安全存储 Token
await Keychain.setGenericPassword('auth', loginRes.token);

// 4. 存储用户信息
await Storage.save({
key: 'userInfo',
data: loginRes.user,
});

return loginRes;
},

async logout() {
await Keychain.resetGenericPassword();
await Storage.remove({ key: 'userInfo' });
},

async refreshToken(token) {
const res = await client.post('/auth/refresh', { token });
await Keychain.setGenericPassword('auth', res.token);
return res.token;
},

async validateToken() {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) return false;

const res = await client.post('/auth/validate');
return res.valid === true;
} catch {
return false;
}
},
};

4.3 数据层 (Data Layer)

4.3.1 API 客户端

// api/client.ts
import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import Config from 'react-native-config';
import * as Keychain from 'react-native-keychain';
import { Loader } from '@/components/loading';
import { showToast } from '@/utils/toast';
import { reset } from '@/navigation';

// 创建实例
const client: AxiosInstance = axios.create({
baseURL: Config.SALES_SERVER,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});

// 请求拦截器
client.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
// 1. 处理 Loading
if (!config.hideLoading) {
Loader.current?.show();
}

// 2. 注入 Token
const credentials = await Keychain.getGenericPassword();
if (credentials?.password) {
config.headers['vulcan-token'] = credentials.password;
}

// 3. 注入通用请求头
config.headers['accept-timezone'] = await getTimezone();
config.headers['language-code'] = 'zh-CN';

return config;
},
(error) => Promise.reject(error)
);

// 响应拦截器
client.interceptors.response.use(
(response) => {
Loader.current?.hide();

const { code, data, msg } = response.data;

if (code === 200) {
return data;
}

// 业务错误处理
if (code === 401) {
showToast('登录已过期');
reset({ index: 0, routes: [{ name: 'Login' }] });
} else {
showToast(msg || '请求失败');
}

return Promise.reject(response.data);
},
(error) => {
Loader.current?.hide();

if (error.response?.status === 401) {
reset({ index: 0, routes: [{ name: 'Login' }] });
}

showToast(error.message || '网络错误');
return Promise.reject(error);
}
);

export default client;

4.3.2 API 模块化

// api/customer.ts
import client from './client';
import type { Customer, CustomerListParams, CustomerListResponse } from '@/types/models';

export const customerApi = {
/**
* 获取客户列表
*/
list(params: CustomerListParams): Promise<CustomerListResponse> {
return client.post('/web/customer/list', params);
},

/**
* 获取客户详情
*/
detail(customerId: string): Promise<Customer> {
return client.post('/web/customer/info', { customerId });
},

/**
* 创建/更新客户
*/
save(data: Partial<Customer>): Promise<Customer> {
return client.post('/web/customerManagement/createOrModifyCustomer', data);
},

/**
* 今日统计
*/
todayStatistics(): Promise<CustomerStatistics> {
return client.get('/web/customer/todayStatistics');
},
};

5. 导航架构

5.1 导航结构图

RootNavigator (Native Stack)
├── SplashScreen # 启动屏
├── AuthNavigator (Stack) # 认证流程
│ ├── Login # 登录
│ └── Register # 注册
├── MainNavigator (Stack) # 主应用
│ ├── HomeTabs (Bottom Tab) # 主页 Tabs
│ │ ├── CorporateMessage # 企信
│ │ ├── Workbench # 工作台
│ │ ├── CRM # CRM
│ │ └── My # 我
│ ├── CRMNavigator (Stack) # CRM 模块
│ │ ├── CustomerList
│ │ ├── CustomerDetail
│ │ ├── OrderList
│ │ └── OrderDetail
│ ├── ApprovalNavigator (Stack) # 审批模块
│ │ ├── ApprovalList
│ │ └── ApprovalDetail
│ └── CommonScreens # 通用页面
│ ├── Setting
│ ├── WebView
│ └── PDFViewer
└── Modal Screens # 模态页面
├── ImageViewer
└── ActionSheet

5.2 导航类型定义

// navigation/types.ts
import type { NavigatorScreenParams } from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';

// Root Navigator
export type RootStackParamList = {
SplashScreen: undefined;
Login: undefined;
Register: undefined;
HomeTabs: NavigatorScreenParams<HomeTabParamList>;
CustomerDetail: { customerId: string };
OrderDetail: { orderId: string };
Setting: undefined;
WebView: { url: string; title?: string };
};

// Home Tab Navigator
export type HomeTabParamList = {
CorporateMessage: undefined;
Workbench: undefined;
CRM: undefined;
My: undefined;
};

// Screen Props 类型
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;

// 扩展全局导航类型
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}

6. 数据流设计

6.1 单向数据流

┌─────────────────────────────────────────────────────────────────┐
│ 用户交互 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ UI Component │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Props │ ← │ Store │ ← │ Service │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Action Dispatch │ │
│ │ onClick → store.action() → API Call → Update State │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ State Update │ │
│ │ Zustand: set(state => { state.xxx = newValue }) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Re-render │ │
│ │ UI Component 自动订阅状态变化,触发重新渲染 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

6.2 异步数据流示例

// hooks/useCustomerList.ts
import { useState, useCallback } from 'react';
import { customerApi } from '@/api/customer';
import { useToast } from './useToast';

export function useCustomerList(initialParams?: CustomerListParams) {
const [data, setData] = useState<Customer[]>([]);
const [loading, setLoading] = useState(false);
const [params, setParams] = useState(initialParams || { pageNum: 1, pageSize: 20 });
const { showError } = useToast();

const fetch = useCallback(async (newParams?: Partial<CustomerListParams>) => {
const mergedParams = { ...params, ...newParams };
setLoading(true);

try {
const res = await customerApi.list(mergedParams);

if (mergedParams.pageNum === 1) {
setData(res.list);
} else {
setData(prev => [...prev, ...res.list]);
}

setParams(mergedParams);
return res;
} catch (error) {
showError('加载失败');
throw error;
} finally {
setLoading(false);
}
}, [params]);

const refresh = useCallback(() => fetch({ pageNum: 1 }), [fetch]);

const loadMore = useCallback(() => {
return fetch({ pageNum: params.pageNum + 1 });
}, [fetch, params.pageNum]);

return { data, loading, fetch, refresh, loadMore };
}

7. 核心模块设计

7.1 认证模块

┌─────────────────────────────────────────────────────────────┐
│ Authentication Flow │
│ │
│ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ App │ → │ Splash │ → │ Check │ → │ Navigate │ │
│ │ 启动 │ │ Screen │ │ Token │ │ to... │ │
│ └──────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ │ │ │
│ Token 有效 Token 无效 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ HomeTabs │ │ Login │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Validate │ │
│ │ Email & │ │
│ │ Password │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Login │ │
│ │ with │ │
│ │ VerifyCode│ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Save │ │
│ │ Token & │ │
│ │ UserInfo │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────┘

7.2 CRM 模块

┌─────────────────────────────────────────────────────────────┐
│ CRM Module │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 客户管理 ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ 客户列表 │ │ 客户详情 │ │ 客户编辑 │ ││
│ │ │ - 分页 │ │ - 基本信息│ │ - 表单 │ ││
│ │ │ - 搜索 │ │ - 品牌 │ │ - 验证 │ ││
│ │ │ - 筛选 │ │ - 项目 │ │ - 提交 │ ││
│ │ └──────────┘ │ - 合同 │ └──────────┘ ││
│ │ │ - 订单 │ ││
│ │ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 订单管理 ││
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
│ │ │ 订单列表 │ │ 订单详情 │ │ 创建订单 │ ││
│ │ │ - 状态筛选│ │ - 产品 │ │ - 选客户 │ ││
│ │ │ - 时间筛选│ │ - 金额 │ │ - 选产品 │ ││
│ │ │ - 导出 │ │ - 审批流程│ │ - 提交 │ ││
│ │ └──────────┘ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 审批管理 ││
│ │ ┌──────────┐ ┌──────────┐ ││
│ │ │ 待办审批 │ │ 审批详情 │ ││
│ │ │ - 我发起的│ │ - 同意 │ ││
│ │ │ - 待我审批│ │ - 拒绝 │ ││
│ │ │ - 我审批的│ │ - 转交 │ ││
│ │ └──────────┘ └──────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

7.3 消息模块

┌─────────────────────────────────────────────────────────────┐
│ Message Module │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 企业消息 (企信) ││
│ │ ││
│ │ 消息类型: ││
│ │ - 待办事项通知 ││
│ │ - CRM 消息通知 ││
│ │ - 审批消息 ││
│ │ - 日程提醒 ││
│ │ - 系统通知 ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 实时通信 ││
│ │ ││
│ │ 技术方案: SSE (Server-Sent Events) ││
│ │ ││
│ │ ┌────────────────────────────────────────────────────┐││
│ │ │ Client Server │││
│ │ │ │ │ │││
│ │ │ │ ── POST /chat/msg ──────────> │ │││
│ │ │ │ │ │││
│ │ │ │ <── GET /chat/sse?msgId=xxx ─ │ │││
│ │ │ │ │ │││
│ │ │ │ <── [Event: message] ──────── │ │││
│ │ │ │ <── [Event: message] ──────── │ │││
│ │ │ │ <── [Event: done] ─────────── │ │││
│ │ │ │ │ │││
│ │ └────────────────────────────────────────────────────┘││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

8. 主题与样式系统

8.1 设计 Token

// theme/colors.ts
export const colors = {
// 品牌色 - 纷享销客橙色系
primary: '#FF6B35',
primaryLight: '#FF8C5A',
primaryDark: '#E55A2B',

// 功能色
success: '#1CC860',
warning: '#FE7E04',
error: '#FC5A5A',
info: '#2196F3',

// 中性色
text: '#192A3E',
textSecondary: '#666666',
textLight: '#999999',
textDisabled: '#D9D9D9',

// 背景色
background: '#F7F8FA',
backgroundSecondary: '#FFFFFF',
white: '#FFFFFF',

// 边框色
border: '#E5E5E5',
divider: '#E0E0E0',

// 渐变色
backgroundGradient: ['#d6edfb', '#f6f5fa', '#f8e6da'],
};

// theme/spacing.ts
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
};

// theme/typography.ts
export const typography = {
h1: { fontSize: 24, fontWeight: '700' as const },
h2: { fontSize: 20, fontWeight: '600' as const },
h3: { fontSize: 18, fontWeight: '600' as const },
body: { fontSize: 16, fontWeight: '400' as const },
bodySmall: { fontSize: 14, fontWeight: '400' as const },
caption: { fontSize: 12, fontWeight: '400' as const },
};

8.2 样式方案

项目采用 NativeWind (Tailwind CSS) + StyleSheet 混合方案:

// 推荐方式:Tailwind 类名
function Card() {
return (
<View className="bg-white rounded-lg p-4 shadow-md">
<Text className="text-lg font-semibold text-gray-800">
标题
</Text>
</View>
);
}

// 复杂样式:StyleSheet
const styles = StyleSheet.create({
container: {
...shadow.md,
backgroundColor: colors.white,
borderRadius: borderRadius.lg,
},
});

9. 错误处理策略

9.1 全局错误边界

// components/common/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { View, Text, Button } from 'react-native';

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// 上报错误到 Sentry
console.error('Error caught by boundary:', error, errorInfo);
}

handleRetry = () => {
this.setState({ hasError: false, error: null });
};

render() {
if (this.state.hasError) {
return this.props.fallback || (
<View className="flex-1 items-center justify-center p-4">
<Text className="text-lg font-semibold mb-2">出错了</Text>
<Text className="text-gray-500 mb-4">
{this.state.error?.message || '未知错误'}
</Text>
<Button title="重试" onPress={this.handleRetry} />
</View>
);
}

return this.props.children;
}
}

9.2 API 错误处理

// utils/errorHandler.ts
export enum ErrorCode {
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
SERVER_ERROR = 500,
NETWORK_ERROR = -1,
}

export function handleApiError(error: any): string {
const code = error.response?.status || error.code;

switch (code) {
case ErrorCode.UNAUTHORIZED:
// 跳转登录页
reset({ index: 0, routes: [{ name: 'Login' }] });
return '登录已过期,请重新登录';

case ErrorCode.FORBIDDEN:
return '您没有权限执行此操作';

case ErrorCode.NOT_FOUND:
return '请求的资源不存在';

case ErrorCode.SERVER_ERROR:
return '服务器错误,请稍后重试';

case ErrorCode.NETWORK_ERROR:
return '网络连接失败,请检查网络';

default:
return error.message || '请求失败,请稍后重试';
}
}

10. 性能优化策略

10.1 列表性能优化

// 使用 React.memo 避免不必要的重渲染
const CustomerItem = React.memo<{ customer: Customer }>(({ customer }) => {
return (
<View className="p-4 bg-white border-b border-gray-100">
<Text className="font-semibold">{customer.name}</Text>
</View>
);
});

// FlatList 优化配置
<FlatList
data={customers}
renderItem={({ item }) => <CustomerItem customer={item} />}
keyExtractor={(item) => item.id}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
windowSize={5}
initialNumToRender={10}
/>

10.2 图片优化

// 使用 FastImage 替代 Image
import FastImage from '@d11/react-native-fast-image';

<FastImage
source={{
uri: imageUrl,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
style={{ width: 100, height: 100 }}
/>

10.3 状态更新优化

// 使用 useMemo 缓存计算结果
const filteredCustomers = useMemo(() => {
return customers.filter(c =>
c.name.includes(searchText) || c.phone?.includes(searchText)
);
}, [customers, searchText]);

// 使用 useCallback 缓存回调函数
const handlePress = useCallback(() => {
navigation.navigate('CustomerDetail', { customerId: customer.id });
}, [customer.id]);

11. 安全策略

11.1 敏感数据存储

// 使用 Keychain 存储敏感信息
import * as Keychain from 'react-native-keychain';

// 存储 Token
await Keychain.setGenericPassword('auth', token, {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED,
});

// 读取 Token
const credentials = await Keychain.getGenericPassword();

// 删除 Token
await Keychain.resetGenericPassword();

11.2 输入验证

// 使用 Zod 进行输入验证
import { z } from 'zod';

const loginSchema = z.object({
email: z.string().email('请输入有效的邮箱地址'),
password: z.string().min(6, '密码至少6位'),
});

function validateLogin(data: unknown) {
const result = loginSchema.safeParse(data);
if (!result.success) {
throw new Error(result.error.errors[0].message);
}
return result.data;
}

12. 测试策略

12.1 单元测试

// __tests__/services/authService.test.ts
import { authService } from '@/services/authService';

describe('AuthService', () => {
it('should login successfully with valid credentials', async () => {
const result = await authService.login('test@example.com', 'password123');
expect(result.token).toBeDefined();
expect(result.user).toBeDefined();
});

it('should throw error with invalid credentials', async () => {
await expect(
authService.login('invalid@example.com', 'wrong')
).rejects.toThrow();
});
});

12.2 组件测试

// __tests__/components/CustomerCard.test.tsx
import { render, fireEvent } from '@testing-library/react-native';
import { CustomerCard } from '@/components/business';

const mockCustomer = {
id: '1',
name: '测试客户',
phone: '13800138000',
};

describe('CustomerCard', () => {
it('should render customer info correctly', () => {
const { getByText } = render(<CustomerCard customer={mockCustomer} />);
expect(getByText('测试客户')).toBeTruthy();
expect(getByText('13800138000')).toBeTruthy();
});

it('should call onPress when pressed', () => {
const onPress = jest.fn();
const { getByTestId } = render(
<CustomerCard customer={mockCustomer} onPress={onPress} />
);
fireEvent.press(getByTestId('customer-card'));
expect(onPress).toHaveBeenCalledWith(mockCustomer);
});
});

13. 部署与发布

13.1 多环境配置

# .env.dev
SALES_SERVER=https://dev-api.example.com
LOGIN_SERVER=https://dev-login.example.com
BUNDLE_ENV=dev

# .env.staging
SALES_SERVER=https://staging-api.example.com
LOGIN_SERVER=https://staging-login.example.com
BUNDLE_ENV=staging

# .env.prod
SALES_SERVER=https://api.example.com
LOGIN_SERVER=https://login.example.com
BUNDLE_ENV=prod

13.2 热更新 (CodePush)

# 发布到测试环境
appcenter codepush release-react -a {app-name} -d Staging \
--description "功能更新..." \
-t "1.0.0 - 1.0.5"

# 发布到生产环境
appcenter codepush release-react -a {app-name} -d Production \
--description "功能更新..." \
-t "1.0.0"

14. 后续优化建议

14.1 短期优化 (1-2周)

  1. API 模块拆分: 将 sales.js 拆分为独立的 API 模块 (customer.ts, order.ts, approval.ts)
  2. 类型完善: 为所有 API 请求/响应添加 TypeScript 类型定义
  3. 错误边界: 为关键页面添加 ErrorBoundary 组件
  4. 加载状态: 添加骨架屏 (Skeleton) 组件改善加载体验

14.2 中期优化 (1-2月)

  1. 离线支持: 添加数据缓存和离线访问能力
  2. 性能监控: 接入 Sentry 或 Firebase Performance
  3. 自动化测试: 完善单元测试和集成测试覆盖率
  4. 深度链接: 实现 Universal Links 和 Deep Links

14.3 长期规划

  1. 微前端架构: 考虑按业务模块拆分独立子应用
  2. 跨平台复用: 探索 React Native Web 支持
  3. CI/CD 完善: 自动化构建、测试、发布流程
  4. 设计系统: 建立完整的组件设计系统文档

附录

A. 命名规范

类型规范示例
文件夹kebab-casecustomer-list/
组件文件PascalCaseCustomerCard.tsx
工具文件camelCaseformatDate.ts
常量UPPER_SNAKE_CASEAPI_TIMEOUT
类型/接口PascalCaseCustomerInfo
变量camelCasecustomerList
Boolean 变量is/has/can 前缀isLoading, hasError

B. Git 提交规范

feat: 新功能
fix: 修复 bug
docs: 文档更新
style: 代码格式
refactor: 重构
perf: 性能优化
test: 测试
chore: 构建/工具

示例:
feat(customer): 添加客户搜索功能
fix(login): 修复登录失败无提示问题

C. 参考资源

微信公众号

微信公众号

国际化

· 阅读需 1 分钟
Quany
软件工程师

语言设置

  1. 跟随系统;
  2. 记录用户设置;

微信公众号

微信公众号

编程模型

· 阅读需 5 分钟
Quany
软件工程师

这张图中展示的模型(Opus 4.5, GPT-5.2, Sonnet 4.5, Gemini 3 Pro, Composer 1)是目前在编程辅助领域备受关注的几款顶级AI模型。它们的设计理念、技术背景和擅长的编程任务各有侧重,了解其特点能帮助你更好地利用多模型功能。

下图清晰地展示了这五款主流AI编程模型的核心定位与技术背景,以及它们在代码生成、逻辑推理、代码解释和成本效率这四个关键编程辅助维度上的能力差异: quadrantChart title AI编程模型能力象限分析 x-axis “代码生成/补全” --> “逻辑推理/调试” y-axis “代码解释/教学” --> “成本/效率” “GPT-5.2 (OpenAI)”: [0.85, 0.2] “Opus 4.5 (Anthropic)”: [0.7, 0.6] “Sonnet 4.5 (Anthropic)”: [0.5, 0.8] “Gemini 3 Pro (Google)”: [0.6, 0.7] “Composer 1 (Cohere)”: [0.3, 0.9]

以下是这五个模型在编程领域的具体特点,你可以根据项目需求和具体任务场景来选择和组合它们:

🧠 各模型深度解析

  1. GPT-5.2 (OpenAI)

• 核心优势:全能型代码生成与迭代。在快速生成、补全和重构代码方面表现尤为出色。它通常能生成非常符合人类习惯、结构清晰的代码,并且能很好地理解你的意图,进行多次迭代修改。

• 编程场景:

◦ 快速原型开发:当你需要从一个想法快速生成可运行的基础代码时。

◦ 代码重构与优化:为现有代码段提供改进建议、优化性能或提升可读性。

◦ 跨语言转换:将一种编程语言的代码转换为另一种。

  1. Opus 4.5 (Anthropic)

• 核心优势:深度逻辑理解与复杂系统设计。它的长上下文处理能力极强,擅长处理极其复杂、需要深度推理的编程问题,能更好地理解整个代码库的上下文和架构。

• 编程场景:

◦ 系统架构设计:设计复杂的软件架构、数据库模型或微服务交互。

◦ 调试疑难杂症:分析晦涩难懂的Bug日志,定位深层逻辑错误。

◦ 审查复杂代码:对大型、复杂的函数或模块进行深入分析和安全审查。

  1. Sonnet 4.5 (Anthropic)

• 核心优势:高性价比的“思考者”。可以看作是Opus的“经济版”,它在逻辑推理和代码质量上保持了很高水准,但响应速度更快、成本更低,是处理日常中等复杂度编程任务的绝佳平衡选择。

• 编程场景:

◦ 日常代码编写与解释:大部分日常的函数实现、API调用、数据处理脚本。

◦ 学习与教学:请求它解释一段算法或设计模式,通常会得到清晰、易于理解的回答。

  1. Gemini 3 Pro (Google)

• 核心优势:强大的多模态与推理能力。在多模态理解和复杂逻辑推理上表现突出。在编程任务中,它能很好地结合代码、图表描述和错误信息进行综合推理。

• 编程场景:

◦ 处理多模态任务:根据UI草图生成前端代码框架,或解释图表中的数据逻辑。

◦ 算法与数据结构:在解决需要严密逻辑推理的算法题时表现出色。

◦ 集成谷歌生态:在处理与Google服务(如Firebase, BigQuery)相关的代码时可能有额外优势。

  1. Composer 1 (Cohere)

• 核心优势:专业级的对话与指令跟随。它被设计为顶级的对话和协作模型,擅长理解自然语言指令并进行多轮、上下文连贯的对话,在解释和讨论代码时体验非常自然。

• 编程场景:

◦ 代码审查与解释:像一位耐心的同事一样,逐行为你解释代码逻辑、潜在风险和改进建议。

◦ 结对编程:适合进行开放式的、探索性的编程讨论,在你思路不清晰时帮你梳理逻辑。

◦ 编写技术文档:根据代码生成清晰、结构化的注释、API文档或使用说明。

💡 如何搭配使用(多模型策略)

你开启的“多模型”功能非常强大,可以根据不同场景组合使用,发挥各自长处:

  1. “生成 + 审查”组合: ◦ 用 GPT-5.2 快速生成代码草稿。

    ◦ 用 Composer 1 或 Opus 4.5 对生成的代码进行深度审查、解释和提出改进建议。

  2. “实现 + 设计”组合: ◦ 面对一个复杂功能时,先用 Opus 4.5 进行高层级的设计和模块划分。

    ◦ 再用 GPT-5.2 或 Sonnet 4.5 去实现具体的模块代码。

  3. “日常 + 专家”组合: ◦ 将 Sonnet 4.5 或 Gemini 3 Pro 作为日常主力,处理大部分效率型任务,兼顾质量与成本。

    ◦ 当遇到棘手难题时,切换到 Opus 4.5 进行深度攻坚。

简单总结: • 想快速得到可用代码:首选 GPT-5.2。

• 解决复杂、需要深度思考的难题:首选 Opus 4.5。

• 追求最佳性价比的日常开发:首选 Sonnet 4.5。

• 需要结合图表或进行强逻辑推理:试试 Gemini 3 Pro。

• 想深入讨论、审查和理解代码逻辑:首选 Composer 1。

你目前同时启用了 GPT-5.2 和 Composer 1,这是一个非常经典的“实干家 + 评论家”组合,一个负责高效产出,一个负责深度分析和优化,能形成很好的互补。建议你可以尝试用GPT-5.2写代码,然后立即用Composer 1来审查,看看效果如何。

微信公众号

微信公众号

UI组件库

· 阅读需 5 分钟
Quany
软件工程师

在 React Native 应用中处理时区问题确实是个挑战,特别是当你的应用需要服务全球用户时。下面我为你梳理了核心的解决思路和具体的实践方案。

🧠 理解问题根源

首先,React Native 应用(尤其是使用 Hermes 引擎时)在获取设备时区信息时可能不会如预期般工作。例如,关键的 Intl.DateTimeFormat().resolvedOptions().timeZone API 可能无法稳定地返回用户的实际时区,而是统一返回 'UTC',这会导致日期显示和计算出现偏差 。

🛠️ 解决方案与实战代码

下面的表格概述了解决此问题的几个关键方向。

解决方向核心目标关键技术/库
准确获取时区可靠地获取用户所在时区。expo-localization, @formatjs/intl-datetimeformat
正确处理时间在代码中进行时区转换和计算。date-fns-tz, moment-timezone
正确显示时间将时间以符合用户地区习惯的格式呈现。Intl.DateTimeFormat

1. 可靠地获取用户时区

不要依赖默认的 Intl API,推荐使用专为 React Native 设计的库。

  • 使用 expo-localization:这是最直接可靠的方式。

    npx expo install expo-localization
    import * as Localization from 'expo-localization';

    // 获取设备设置的时区
    const userTimeZone = Localization.timezone;
    // 或者获取日历设置中的时区(可能更准确)
    const calendarTimeZone = Localization.getCalendars()[0].timezone;
    console.log('用户时区:', userTimeZone);
  • 使用 @formatjs 补丁(如不使用 Expo):这个方法可以增强 Intl API 的时区支持 。

    npm install @formatjs/intl-datetimeformat
    import '@formatjs/intl-datetimeformat/polyfill';
    import '@formatjs/intl-datetimeformat/add-all-tz.js'; // 引入全时区数据

    // 之后,Intl API 可能可以正确工作
    const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

2. 进行时区转换和格式化

获取时区后,需要使用专门的库来处理复杂的时区转换。推荐使用 date-fnsdate-fns-tz

npm install date-fns date-fns-tz
  • 场景一:将服务器返回的 UTC 时间转换为用户本地时间

    import { format, utcToZonedTime } from 'date-fns-tz';

    // 假设从后端API收到一个UTC时间字符串
    const utcTimeStringFromServer = "2025-12-11T08:00:00Z";
    const userTimezone = 'Asia/Shanghai'; // 此值来自上面的expo-localization

    // 1. 将UTC时间转换为用户时区下的Date对象
    const zonedDate = utcToZonedTime(utcTimeStringFromServer, userTimezone);

    // 2. 格式化显示
    const pattern = "yyyy-MM-dd HH:mm:ss (OOOO)";
    const output = format(zonedDate, pattern, { timeZone: userTimezone });

    console.log(output); // 输出:2025-12-11 16:00:00 (GMT+08:00)
  • 场景二:将用户选择的本地时间转换为 UTC 时间再传给后端 这在提交表单(如日历事件)时至关重要 。

    import { zonedTimeToUtc } from 'date-fns-tz';

    // 用户在其本地时区选择了日期时间
    const userLocalDate = new Date(2025, 11, 11, 20, 0); // 用户选择 2025-12-11 20:00
    const userTimezone = 'Asia/Shanghai';

    // 转换为UTC时间
    const utcDate = zonedTimeToUtc(userLocalDate, userTimezone);

    console.log(utcDate.toISOString()); // 输出:2025-12-11T12:00:00.000Z (UTC时间)
    // 将这个 UTC 时间发送给后端服务器

3. 处理特定 UI 组件的时区问题

某些第三方组件(如日期选择器)可能有自己的时区逻辑。例如,react-native-paper-dates 的星期错位问题,可以通过显式设置时区来解决 。

// 在应用入口或使用组件前,设置全局默认时区
import { Platform } from 'react-native';
import * as Localization from 'expo-localization';

if (Platform.OS !== 'web' && '__setDefaultTimeZone' in Intl.DateTimeFormat) {
// 使用从expo-localization获取的正确时区
Intl.DateTimeFormat.__setDefaultTimeZone(Localization.timezone);
}

4. 应对极端情况:“时间偏移”法

对于某些特殊场景,比如日期选择组件强制返回 UTC 时间,而后端接口又无法修改时,可以采用一种“时间偏移”策略。这不是最“标准”的做法,但在特定约束下是有效的解决方案 。

// 示例:用户选择了一个日期,但组件返回的是该日期在UTC时区的零点
const getOffsetHours = (timeZone) => {
// 一个简单示例,动态计算指定时区与UTC的偏移小时数
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timeZone,
timeZoneName: 'short'
});
const parts = formatter.formatToParts(new Date());
const tzName = parts.find(part => part.type === 'timeZoneName').value;
// 可根据tzName(如"GMT+8")解析出偏移量,这里简化为固定值
if (timeZone === 'Pacific/Auckland') return 12; // 新西兰时间可能偏移12小时
return 0;
};

const userSelectedDate = new Date('2025-04-30T00:00:00.000Z'); // 组件返回的UTC时间
const offset = getOffsetHours('Pacific/Auckland') * 60 * 60 * 1000; // 偏移毫秒数

// 创建一个“调整”后的日期对象
const adjustedDate = new Date(userSelectedDate.getTime() + offset);

// 将adjustedDate发给后端,它表示的物理时刻是正确的

💡 最佳实践与总结

  1. 前后端约定使用 UTC:这是黄金法则。前后端传输数据时,永远使用 UTC 时间(ISO 8601 格式),只在最终显示给用户的那一刻才转换为本地时间 。
  2. 在客户端处理显示层:时区转换是一个表示层问题,理想情况下应在客户端解决,避免给后端服务增加不必要的复杂性。
  3. 早期并经常测试:在开发早期就使用模拟器切换不同时区进行测试,确保日期逻辑在所有场景下都正确 。
  4. 谨慎选择第三方库:评估日期处理库的体积、功能和兼容性。对于新项目,date-fns-tz 是不错的选择。

希望这些具体的方案和代码示例能帮助你彻底解决 React Native 中的时区切换难题!如果你在实现过程中遇到更具体的情况,欢迎随时提出。

微信公众号

微信公众号