在出行赛道上做App,很多团队第一反应是“做一套类似Uber的系统”,可真落地时会发现,光是一个“司机怎么接单”的交互逻辑就够你折腾半个月。今年我帮朋友团队评估过一个项目,他们想用React Native加Node.js快速复刻一个InDriver风格的本地出行应用,挑战点在于:InDriver和Uber表面都是叫车,但计价方式和撮合逻辑完全不同。这篇内容就基于这个实际项目,把React Native做客户端、Node.js做服务端,从环境搭建到核心业务开发、再到启动白屏排查的完整链路写出来。适合准备切入出行领域、或者想用这套技术栈做实时撮合类App的朋友参考。
1. 先确定项目边界:InDriver模式和Uber模式对系统设计的影响
很多教程上来就写代码,但做打车类应用,最核心的坑往往在业务模式没有理清楚。同样的订单表、同样的司机池,因为撮合规则不同,整个服务端的架构设计会差出去一大截。
1.1 两种计价模式背后的逻辑差异
Uber这类平台,乘客看到的价格是平台用算法算好的,司机和乘客都没有议价空间。乘客下单后,系统根据供需关系、距离、时段做动态定价,然后把订单推送给附近的司机,司机只能选择“接受”或“拒绝”。
InDriver的思路反过来了。乘客提交行程时,自己输入一个愿意支付的价格,系统把这个订单广播给附近司机,司机端看到的是乘客的出价、行程起终点和大致距离。司机觉得划算就抢单,觉得不划算可以忽略,也可以在心里算一笔账——这个单子有没有跑头。乘客如果长时间没人接单,可以主动加价,直到有司机接单。
这个差异带来的技术影响非常直接:
- Uber模式需要一套比较完整的计价引擎,服务端要根据实时供需算出价格,而且价格是强约束。
- InDriver模式不需要复杂计价引擎,但需要一套可靠的“广播+抢单”机制,还要处理“乘客多次加价”“多个司机同时抢同一个单”的并发冲突。
我当时的建议是:项目起步阶段不要把两套逻辑全做进去,先选定一个模式打通闭环。如果你要做的产品形态更接近InDriver,服务端核心就应该是“订单广播”和“抢单锁单”;如果更接近Uber,核心则是“调度派单”和“计价计算”。两种模式混在一起做,前期会非常痛苦,因为很多页面和接口设计是冲突的。
1.2 功能清单与服务端模块划分
不管选哪种模式,一个打车应用的骨架都是差不多的。我把这个项目的功能拆成五个模块:
- 用户模块:乘客注册登录、司机注册审核、实名认证信息。这个最简单,用手机号加验证码就能跑通。
- 订单模块:发布行程、生成订单、状态流转、订单历史。这是整个系统的核心状态机。
- 撮合模块:订单广播到附近司机、司机抢单、订单锁定。这个模块最考验服务端的实时性。
- 计费模块:InDriver模式里,乘客出价和最终支付金额挂钩,服务端要记录加价记录;Uber模式则需要一套计价规则。
- 消息模块:订单推送、司机与乘客的匿名通话或站内消息。这里最常用的是WebSocket,而不是普通HTTP请求。
在这个项目里,我把订单模块和撮合模块放在同一个服务里先开发,计费模块简化成“乘客出价即最终价格”,只保留加价记录。这样MVP阶段能最快跑通。
1.3 为什么后端选Node.js而不是Java或Go
服务端我选了Node.js,主要原因有三个:
第一,实时推送和Node.js配合得非常自然。打车应用最典型的场景是“乘客下单后,订单要实时出现在附近司机的手机屏幕上”。Node.js对WebSocket和长连接的天然亲和力,让这个功能不需要额外引入复杂的消息中间件。初期用Socket.IO就能撑住几千个司机同时在线,等量大了再迁到专业的推送服务也不迟。
第二,整个团队只用一门语言。客户端是React Native,用的是JavaScript/TypeScript;服务端用Node.js还是同一门语言。一个小团队不需要同时维护两套技术栈,光这一条就能省掉大量沟通成本。
第三,I/O密集型场景很合适。打车业务绝大部分请求是短平快的读写操作,比如查附近司机、更新位置、拉订单列表。Node.js基于事件循环的非阻塞模型,处理这类I/O密集型请求的表现非常稳定。当然,等之后用户量大了,可以把计算量大的部分(比如路径规划)单独拆出去用更合适的语言写,但这是后话。
提示:如果你一开始就认定订单量的天花板很高(比如百万级日活),那就不要犹豫,直接上Go或Java。Node.js的优势在于快速迭代和中小规模场景,怕的不是Node.js性能不行,而是团队对异步模型的掌控力不够。
2. Node.js环境搭建:Ubuntu下从零配好20+版本运行环境
很多人在项目第一章就卡住了——不是不会写代码,而是开发环境装不对。尤其是Ubuntu服务器上装Node.js这件事,网上的教程乱七八糟,照着复制命令装出来的版本要么太老,要么干脆装不上。这里把我实际踩过的路数完整写一遍。
2.1 为什么Ubuntu自带源安装的Node版本极其尴尬
在Ubuntu 20.04上,如果你直接执行sudo apt install nodejs,装出来的Node.js版本是10.x或者12.x,具体看系统版本。这个版本跑老项目没问题,但用来开发新项目就很尴尬了:
- Node.js 20+版本里有很多新特性,比如更稳定的WebSocket客户端、更好的ESM支持,老版本根本用不上。
- 很多新版的npm包,尤其是React Native相关工具链,对Node版本有最低要求。我遇到过安装某个依赖时直接报错,提示Node版本必须大于等于18,当时服务器上装的是12,只能重来。
npm install在旧版本上偶尔会出一些诡异的问题,比如依赖树解析出错、锁文件版本冲突,排查起来很浪费时间。
所以,不要在Ubuntu上用apt默认源装Node.js做新项目。不是说apt不能装,而是默认源的版本更新太滞后,不适合做现代前端或Node服务开发。
2.2 两条可落地的安装路径
我推荐两条路径,任选其一。
路径一:用nvm做版本管理。这个方式最适合开发机,因为它可以随时切换Node版本,同一个机器上可以共存多个版本,给不同的项目用。安装步骤如下:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 安装完成后重开终端,或者执行 source ~/.bashrc nvm install 20 nvm use 20 node -v我自己用的就是这个方式。好处很明显:如果某个老项目需要切回Node 16,一条nvm use 16就搞定,不需要卸载重装。
路径二:用NodeSource的APT源直接装。这个方式适合服务器生产环境,因为服务器上不需要频繁切换版本,装一个稳定版本常驻就行。
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs node -v这里注意一点:NodeSource的setup脚本会帮你把NodeSource仓库加进apt源列表,安装完的Node版本就有保证,同时也把npm一起装好了。如果你需要装的是最新的LTS版本,把脚本里的setup_20.x改成setup_22.x即可,不过按我的经验,20.x是目前生态兼容性最稳的一条线。
2.3 安装完要做的三件检查
装完Node.js别急着写代码,先跑三个命令确认环境没毛病:
node -v # 查看Node版本,确认是v20.x npm -v # 查看npm版本,确认是10.x npx -v # 查看npx版本,方便后续初始化脚手架如果npm -v输出异常,或者执行npm install时报"EACCES permission denied",说明之前的安装有权限问题。开发机上最简单粗暴的解决方式是给npm全局目录加权限,但更优雅的做法是配置npm的prefix到用户目录,这里不展开,记住一点:不要用sudo去运行npm install解决依赖问题,这会引入更多麻烦。
2.4 从npm下载到初始化Express项目的完整过程
Node环境准备好后,初始化打车应用的服务端项目。我习惯用Express框架,它是Node.js生态里最成熟的Web框架,文档多、社区大、出海项目里也遍地都是。
mkdir taxi-server cd taxi-server npm init -y npm install express npm install socket.io npm install mysql2 # 如果用MySQL npm install redis # 如果用Redis做缓存和在线状态 npm install jsonwebtoken # 用于登录态token这里解释一下各组件的用途:
- express:HTTP接口框架,处理RESTful API。
- socket.io:WebSocket库,用于订单广播、司机位置实时推送。
- mysql2:数据库驱动,我用的是MySQL,如果团队偏好PostgreSQL,换成pg包即可。
- jsonwebtoken:签发和校验登录token,所有需要鉴权的接口都用它。
初始化完的package.json里,记得把start脚本配好:
"scripts": { "start": "node server.js", "dev": "nodemon server.js" }项目初期用nodemon做热重启,改完代码不用手动重启服务,开发效率高很多。这个阶段不要追求微服务架构,一个单体服务跑通核心流程是性价比最高的选择。
3. 服务端核心实现:发单、抢单与实时推送
服务端的核心不在CRUD,而在订单状态的正确流转和实时消息的可靠推送。打车应用的订单数据虽然不复杂,但并发场景下很容易出逻辑漏洞。我当时踩过最深的坑就是“多个司机同时抢同一个订单”,这两个问题必须在一开始就设计好。
3.1 订单状态机:不设计好这个,后面全是雷
订单状态我用一个状态机统一管理,每个字段都有明确含义:
| 状态 | 含义 | 可跳转状态 |
|---|---|---|
| created | 乘客创建订单 | published, cancelled |
| published | 订单已广播给附近司机 | accepted, cancelled |
| accepted | 司机已抢单 | arrived, cancelled |
| arrived | 司机已到达上车点 | in_progress, cancelled |
| in_progress | 行程进行中 | completed |
| completed | 行程结束 | 无 |
| cancelled | 订单取消 | 无 |
状态机的核心价值是防止非法跳转。比如一个订单正在行程中,乘客想取消,服务端要判断是否允许、是否有取消费。我的实现方式是:在服务端维护一份状态流转表,每次更新订单状态时先校验合法性,而不是让客户端随便传一个状态过来就更新。
3.2 乘客发布订单API与计价规则简化方案
我先做了InDriver简化模式:乘客输入起点终点,系统预估距离,乘客自己填价格,订单进入published状态。这个模式下服务端不需要计算结果价格,只需要存下来。
核心API是POST/api/orders,请求体大概是这样的:
{ "passengerId": 123, "pickup": { "lat": 31.2304, "lng": 121.4737 }, "dropoff": { "lat": 31.2401, "lng": 121.4832 }, "price": 28.5, "isInDriverMode": true }服务端收到后,先验登录态,再插入订单表并生成订单号。订单号不建议用自增ID,因为用户会拿着订单号找客服,自增ID既不好看又容易暴露系统规模,我用的是时间戳加随机数的组合。
价格校验这里有一个细节:要设一个合理的价格上下限区间,不然乘客填个1块钱的单子挂在广播里,不仅自己叫不到车,还会污染司机的抢单大厅。我当时的做法是根据预估距离算出一个建议价格区间(比如每公里2.5元到5元),超出区间的价格直接拦截。
3.3 地理距离计算与附近司机筛选
广播订单的前提是“找到附近的司机”,这里有一个绕不开的计算问题:怎么判断司机离乘客有多远?
最粗浅的做法是用经纬度差值做简单加减,但在地球球面上,经纬度一度的实际距离在不同纬度差异很大,必须用球面距离公式。Haversine公式是业界应用最广的方案,直接用代码实现:
function haversineDistance(lat1, lon1, lat2, lon2) { const R = 6371; // 地球半径,单位公里 const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // 公里 }实际应用中不用对全量司机算距离,那样性能太差。更常见的做法是先用“经纬度范围框”粗筛一遍,比如限定在乘客坐标的某个经纬度差值范围内(比如0.1度,大约10公里),再对框内司机算精确距离。这个思路叫“地理围栏粗筛+精确距离精算”,能省掉大量无意义的计算。
3.4 Socket.IO实时抢单:广播、抢单、锁单三步走
订单进入published状态后,服务端要通过Socket.IO把它广播给所有在线且在订单范围内的司机。我用的是Socket.IO的room机制,每个司机上线时,根据Ta的当前位置加入对应的范围房间,订单创建时向相关房间广播。
广播逻辑伪代码如下:
// 订单创建后,广播给3公里内的司机 const nearbyDrivers = await findNearbyDrivers(order.pickup, 3); nearbyDrivers.forEach(driver => { io.to(`driver_${driver.id}`).emit('new_order', { orderId: order.id, pickup: order.pickup, dropoff: order.dropoff, price: order.price }); });真正考验人的是“多个司机同时抢单”的处理。在没有锁的情况下,两个司机同时请求抢同一个订单,服务端查订单状态都是published,然后都更新成accepted,就出问题了。
我当时用的方案是:数据库条件更新加原子判断,只有订单状态是published时才允许更新为accepted,并且立即写入抢单司机ID。具体SQL是:
UPDATE orders SET status = 'accepted', driver_id = :driverId, accepted_at = NOW() WHERE id = :orderId AND status = 'published';然后检查affectedRows,如果等于0,说明订单已经被别人抢走,返回“手慢了”;如果等于1,说明抢单成功,再通过Socket.IO通知乘客和该司机。
这个方案的核心价值是“把并发冲突的判断交给数据库去处理”,而不是在应用层用锁或事务,逻辑简单且可靠。如果你的数据库支持行级锁,用SELECT FOR UPDATE也能达到类似效果,但影响行数的条件更新方案是最直接易懂的。
4. React Native双端开发:乘客端与司机端的页面和地图集成
客户端用React Native开发,最大的优势是一套代码同时跑iOS和Android,但在出行场景里有几个绕不开的痛点:地图组件、定位权限、后台持续定位。这三个点如果不在项目开始就规划好,后面每次改版都痛不欲生。
4.1 react-native-maps集成:iOS和Android都要兼容的地图方案
地图是打车应用的脸面。React Native生态里最主流的方案是react-native-maps,但集成过程有几个坑,我一个个说。
安装命令:
npm install react-native-mapsiOS上跑pod install后基本就能用。Android上需要确认build.gradle里配置没有问题,尤其是如果团队计划在国内使用高德或百度地图,那就要去改react-native-maps的底层依赖,公开版用的是Google Maps。我的建议是:如果你的用户群体在海外,直接用Google Maps;如果目标市场在国内,要么用WebView方案嵌入Web地图,要么做好底层替换的预期管理。
针对本地出行场景,地图上需要展示三类内容:
- 乘客端的地图:显示当前位置、上车点。
- 司机端的地图:显示乘客位置、去接乘客的导航路线。
- 订单派发下的地图:显示司机和乘客的相对位置。
实现上我用<MapView>和<Marker>组合,自定义了Marker样式让乘客端和司机端角色一目了然。地图性能是最容易忽视的环节,如果地图上有大量Marker频繁更新,一定要用trackViewChanges={false}关闭iOS上的实时标注跟踪,不然地图会变卡。
4.2 乘客端叫单流程页面:从发单到等待响应
乘客端的核心页面是“发单页”,交互流程是:
- 选择上车点和目的地。
- 系统根据预估距离给建议价格区间。
- 乘客输入自定义价格,或者选择“一口价”。
- 点击发布订单,进入等待页。
- 等待页显示当前状态:有人接单、正在上车、行程中。
这个页面看起来简单,但有一件事必须在开发前想清楚:等待接单的过程中,乘客如果切到后台,怎么保证能收到接单通知?这里要区分两种状态:
- App在前台:通过WebSocket消息直接弹窗。
- App在后台:靠远程推送。iOS走APNs,Android走FCM,如果国内生态则要看具体厂商推送通道。
RN的推送方案我一律推荐统一的推送封装,比如react-native-notifications或@notifee/react-native,同时从业务设计上做一个兜底:乘客切到后台后,如果长时间没有收到推送,回到App时根据订单轮询一次最新状态。这个兜底逻辑听起来微不足道,但在弱网环境下能救回大量“乘客莫名其妙取消了订单”的体验问题。
4.3 司机端抢单大厅:用列表还是用地图
司机端的核心页面是“抢单大厅”。在这个页面上,附近的订单以列表或地图标注形式展示。我最终做的是“地图+列表”双模式,但MVP阶段只做了列表,因为数据量少的时候地图模式反而增加复杂度。
订单列表的每一条展示:上车点、目的地、乘客出价、预估距离。司机点击某个订单后进入详情页,看地图上的起终点位置,再决定抢不抢。
这里有一个业务细节需要注意:抢单页展示的“距离”是直线距离,还是驾车距离?我见过很多项目直接用Haversine算的直线距离显示给司机,结果司机一看2公里觉得很近,接单后实际开了5公里,体验极差。正确做法是接单详情页用地图SDK的路线规划接口计算实际驾车距离,列表页可以先用直线距离,但要在界面上标注清楚。
4.4 定位与权限处理:不小心就白屏崩溃的坑
出行App绕不开定位。React Native里用react-native-geolocation-service或@react-native-community/geolocation,同时要在原生配置里声明定位权限。
iOS的Info.plist里必须加上NSLocationWhenInUseUsageDescription和NSLocationAlwaysAndWhenInUseUsageDescription,Android则需要在AndroidManifest.xml里声明定位权限。这里最坑的地方是:Android 12(API 31)开始,定位权限弹窗逻辑变了,需要在运行时同时申请前台和后台定位权限,如果漏了其中一个,用户开启后台导航时App就静默挂掉。排查这类问题特别耗时间,当时调了很久才发现是权限字段没配全。
处理定位还涉及一个技术选型:要不要做后台持续定位?我的建议是MVP阶段不要做。后台持续定位在高版本的iOS和Android上限制很多,要申请特殊权限,而且测试真机上的表现不可控。初期方案是司机端上车后,每次到关键节点(到达上车点、行程开始、行程结束)调用一次定位更新,服务端记录下来即可。这个方案实现简单、出问题少,等业务量上来了再考虑真正的前台服务模式定位。
5. 启动白屏问题的完整排查链路:React Native冷启动优化实战
说到React Native,几乎每个开发者都遇到过启动白屏。这个热搜词的出现频率非常高,说明这不是个例。我在这也踩过不少坑,把这套排查链路完整写出来。
5.1 先搞清楚白屏出现在哪个阶段
React Native应用启动到显示首帧内容,会经历大致四个阶段:
- 原生启动阶段:从点击App图标到原生入口代码执行。
- 加载JS Bundle阶段:从原生代码开始加载打包好的JS文件,到JS引擎执行完毕。
- 渲染首帧阶段:React组件树渲染,最终显示到屏幕上。
- 稳定阶段:在首帧之后,如果有大数据请求或复杂运算,可能出现白屏或卡顿。
白屏问题最常出现在第二和第三阶段,尤其是JS Bundle的加载和解析耗时太长导致用户长时间盯着一个空白屏幕。注意,这里说的白屏不是透明或闪一下,而是整个屏幕长时间没有内容,这是一种非常糟糕的体验。如果你把白屏当成“偶发抖动”忽略掉,用户很容易在启动的3秒内卸载App。
5.2 JS Bundle体积和加载耗时:白屏的头号元凶
React Native启动时,原生端要先把JS代码加载进JavaScript引擎。在Debug模式下,这一过程通过Metro服务器实时编译,首次加载的耗时可能非常夸张;Release模式下则是加载打进App包里的bundle文件。
我复盘过这个项目,启动阶段最大的性能瓶颈就是bundle体积过大。一个功能完整的打车App,React Native的bundle很容易就到5MB以上。手机在加载5MB JS代码时,解析和执行的耗时少说几百毫秒,中低端机型可能要一两秒。
优化的基础思路从两个方向入手:
第一,启用字节码或更快的JS引擎。RN 0.70以上,Hermes引擎是默认选项,它把JS代码预编译成Hermes字节码,加载和执行速度远超传统的JavaScriptCore。看你的android/app/build.gradle里是否配置了:
def jscFlavor = 'org.webkit:android-jsc:+' def enableHermes = project.ext.react.get("enableHermes", true)第二,拆bundle或者压缩代码体积。React Native提供了ram bundle机制,核心思想是只加载启动需要的模块,其他模块按需加载。再加上压缩bundle体积的手段,比如减少不必要的第三方库、避免在入口文件里一次性import大量页面,都是有效做法。
我给这个项目定的目标简单粗暴:Release包首屏时间控制在2秒内,Debug模式白屏是正常现象,但不允许超过5秒。
5.3 原生启动阶段的白屏:SplashScreen和Theme的配合
有时候白屏的根源不在JS,而在原生层。React Native应用在点击图标的瞬间,原生窗口已经创建了,但首帧内容还没准备好,这段窗口期如果没做处理,就是一个纯白屏。
解决这个问题最直观的方案是加一个启动页(SplashScreen)。原生启动页在线程创建和JS加载的过程中是一个静态的Logo或品牌色界面,用户看起来就不觉得是“白屏”。等JS引擎加载完毕,React组件渲染出首帧后,再让启动页消失。
实现上不要用复杂动画的启动页,因为启动页的展示逻辑是“原生View盖在RN根View上方”,你的动画做得越复杂,两者切换时越容易出现一闪而过的割裂感。我自己用的是react-native-bootsplash这个库,配置简单,支持的平台也全,按文档走下来即可。
5.3 实测记录:这个项目从白屏到首屏的调整记录
我把当时的排查过程记录如下,你可以照着来。第一步,先抓播放日志。在原生端打开logcat或Xcode console,看ReactNativeJS相关的日志输出。如果bundle加载前没有任何React日志,说明瓶颈在原生层。如果日志显示JS已经执行了,但界面迟迟没渲染,那瓶颈就在React组件的挂载过程。
第二步,检查入口文件。我当时发现入口文件里一次性初始化了Redux store、Socket.IO连接、地图SDK初始化、版本更新检查五六个任务,这些任务全部是同步串行的。地图SDK和Socket.IO连接在弱网环境下会长时间阻塞后续代码执行,白屏时间直接翻倍。
我的修复方式是:把非首屏必须的操作全部放到InteractionManager.runAfterInteractions里延迟执行。
import { InteractionManager } from 'react-native'; // 首屏渲染完成后再执行地图SDK初始化 InteractionManager.runAfterInteractions(() => { initMapSDK(); initSocket(); checkAppUpdate(); });这个改动带来的效果非常明显,首屏时间从3.5秒降到了1.8秒。很多时候性能问题不是某个单点造成,而是“所有事情都要在启动时做完”的贪心心态导致的。启动路径上只保留“进入主界面必须做的事”,其他都延后。
第三步,检查Metro配置文件。Debug模式下的白屏经常是Metro的缓存问题。团队里几个人同时开发时,Metro缓存偶尔会失效,导致每次reload都要等很久。清理缓存的方法:
npx react-native start --reset-cache这个命令对启动白屏问题有奇效,尤其是那种“之前还好好的,今天突然白屏”的情况,十有八九是缓存问题而不是代码问题。
注意:React Native启动白屏和初始化白屏是两个概念。前者是应用从零启动时的等待,后者是页面切换时的JS线程被阻塞导致的白屏。前者重点排查bundle加载和原生启动页,后者重点排查是否有大量同步计算阻塞JS线程。
6. 打包上线前的经验清单:从开发环境到真机部署
很多项目功能都做完了,卡在打包和上线环节。这里把最容易踩的坑集中列一下,避免你在同样的问题上浪费时间。
6.1 环境分离与调试地址配置
React Native开发过程中,模拟器访问本机的服务端地址是有讲究的:
- Android模拟器访问宿主机要使用
10.0.2.2,而不是localhost或127.0.0.1。 - iOS模拟器可以直接用
localhost。 - 真机调试时,要用电脑在局域网内的IP地址。
我建议在项目里做一个环境配置文件,把API地址和Socket地址集中管理,不要再代码里写死某个IP。举例:
const ENV = { dev: { apiBaseUrl: Platform.OS === 'android' ? 'http://10.0.2.2:3000' : 'http://localhost:3000', socketUrl: Platform.OS === 'android' ? 'http://10.0.2.2:3000' : 'http://localhost:3000' }, prod: { apiBaseUrl: 'https://api.yourdomain.com', socketUrl: 'https://api.yourdomain.com' } };这个配置看似简单,但新手经常因为忘记切换环境,拿着开发配置打出来的包放到生产环境,请求全部打到了本地上。另外要注意:Android 9以上默认禁止明文HTTP请求,开发版还好,打Release包时如果API是HTTP协议,要在AndroidManifest.xml里配置usesCleartextTraffic或用HTTPS域名。iOS同样有ATS限制,需要配置NSAppTransportSecurity的例外域名,但坦白说,生产环境直接用HTTPS是最省心的。
6.2 Android打包与签名:一堆证书和Gradle的问题
Android的Release包打包步骤如下:
- 生成签名文件:
keytool -genkey -v -keystore taxi-release.keystore -alias taxi-key -keyalg RSA -keysize 2048 -validity 10000- 把
taxi-release.keystore放到android/app目录下。 - 在
android/gradle.properties里配置签名信息。 - 在
android/app/build.gradle里配置signingConfigs和buildTypes。 - 执行打包命令:
cd android ./gradlew assembleRelease这一步最容易出的问题有两个。第一个是Gradle下载依赖太慢,中低网络环境下经常超时,建议提前把Gradle distributionUrl和阿里云镜像或者公司内网镜像配好。第二个是打包时如果RN版本和Android Gradle Plugin版本不匹配,会出现各种莫名其妙的编译报错,这种问题没有统一答案,最快的办法是去对应的升级迁移文档里查版本对照表,不要盲目升级或降级。
还有一点,很多团队把taxi-release.keystore和签名密码明文写在build.gradle里,而且把包含密码的文件提交到了Git仓库。这是极大的安全风险,一旦仓库泄露,任何人都可以用你的签名重打包应用并盗用品牌。正确的做法是签名信息从环境变量或本地配置文件中读取,并且千万不要把keystore文件提交到Git,哪怕仓库是私有的也不要。
6.3 真机联调与不同机型的兼容坑
打车App对设备兼容性要求很高,因为司机端要长时间挂在后台,乘客端要高频调用地图和定位。我测试时遇到过这么几个问题:
- 低端Android手机上,地图拖动卡顿、页面切换白屏闪烁。
- iOS 16刘海屏机型上,自定义的底部栏被Home指示条遮挡。
- 部分国产Android机型的后台限制策略非常激进,App切后台几秒就被系统杀掉,导致司机接单后收不到推送。
针对第一类问题,主要是减少地图Marker的实时更新频率,把乘客位置的轮询间隔从每2秒改成每5秒一次,同时使用removeClippedSubviews来裁剪屏幕外的子视图。这个改动需要实际测试,不同机型不能一刀切。
针对第二类问题,用SafeAreaView或react-native-safe-area-context统一处理。
针对第三类问题,只能走厂商通道的推送SDK,纯FCM在国内部分机型上不可靠,这里不做过度展开,但你要明确一点:MVP阶段一定要在主流中端Android机和旧款iPhone上各测一台,不然上线后会接到大量“收不到订单通知”的投诉。
6.4 省略不了的合规与资质问题
最后说一个很多技术类博客不爱提、但实际项目绕不开的事情。做打车应用,不管叫车形式是InDriver还是Uber,本质上是提供客运服务或为客运服务提供信息撮合。这涉及到当地的道路运输经营许可、网约车平台资质、司机和车辆的入网要求等一系列法规问题。
技术文章里我不做法律解读,但作为开发者,在项目开工之前一定要先确认:你在这个地区运营类似服务,需要什么样的牌照和合规流程,不能技术都做完了才发现无法上线。如果只是作为技术学习和个人项目跑通流程,那无所谓;但如果打算商业化运营,建议先把商务侧和法务侧的准备工作做在前面,否则后期会非常被动。
收尾:这个项目做到现在的真实体会
这个项目从环境搭建到跑通核心抢单流程,前后用了大概六周时间,还是在两个开发者的配置下完成的。实际开发过程中,最耗时间的部分其实是React Native的地图组件调优和Socket.IO的并发逻辑测试,而不是功能实现本身。
最后分享两个小技巧。服务端联调时,我会同时开着好几个司机端的模拟器,故意用程序模拟同一秒内多个司机同时抢单,反复验证数据库条件更新是否可靠,这个测试帮我提前发现了不少问题。客户端排查白屏时,别光盯着JS代码,先用原生日志确认一下白屏到底发生在原生层还是JS层,这个定位步骤能帮你节省至少一整天的排查时间。整个技术路线走下来,React Native加Node.js做打车类应用是完全可行的,尤其适合中小团队快速试错。希望这篇记录能帮到正在做或准备做类似项目的朋友。