Taro 框架 React Native 项目开发全流程记录
Taro 框架 React Native 项目开发全流程记录
本文记录用一个 Taro 4.3 + React Native 版「计算器」项目走通 Taro RN 工程的完整流程。项目业务代码很简单,主要目标是熟悉 Taro 框架 Android 项目的从脚手架到出包的全过程。
目录
- 0. 环境要求
- 1. 安装 Taro 脚手架
- 2. 修复脚手架模板的兼容性问题
- 3. 用 Taro Play 预览
- 4. 打包 release APK
- 5. 常见报错与解决方案
- 6. 构建结果自检
- 7. 注意事项
- 8. 本次涉及的文件改动
0. 环境要求
| 工具 | 版本 | 说明 |
|---|---|---|
| Node.js | ≥ 18 | 建议用 LTS |
| pnpm | v11 | 注意 v11 的配置方式变了,见 5.1 |
| JDK | 17 | Gradle / AGP 要求 11+,本机 JAVA_HOME 默认指向 jdk1.8,必须处理 |
| Android SDK | compileSdk 34 / NDK 27.1.12297006 | 模板默认的 NDK 25.1.8937393 本机没装 |
1. 安装 Taro 脚手架
官方文档:https://nervjs.github.io/taro-docs/docs/GETTING-STARTED
pnpm install -g @tarojs/cli
taro init my_taro_calc # my_taro_calc 换成你自己的项目名
创建过程一路 next 即可,模板选择 TypeScript + Sass + React。
pnpm install 的构建脚本被忽略
如果安装时出现:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: @parcel/watcher@2.6.0, @swc/core@1.3.96, @tarojs/binding@4.3.0, @tarojs/cli@4.3.0, core-js-pure@3.51.0, core-js@3.51.0, esbuild@0.21.5, esbuild@0.28.2
直接放行:
pnpm approve-builds --all
pnpm v11 会把这些白名单写进 pnpm-workspace.yaml 的 allowBuilds。
2. 修复脚手架模板的兼容性问题
用 pnpm dev:rn --qr 预览时大概率一片报红。根因是脚手架模板的依赖版本已经和 Taro 4.3 / RN 0.73 不兼容,需要手动对齐。
本项目最终使用的 package.json 关键部分:
{
"scripts": {
"prepare": "husky",
"new": "taro new",
"build:rn": "taro build --type rn && node scripts/build-android.js", // 顺带打出 release APK
"build:apk": "node scripts/build-android.js", // 只打 APK
"dev:rn": "taro build --type rn --watch"
},
"dependencies": {
"@react-native/metro-config": "^0.73.5",
"@tarojs/components": "4.3.0",
"@tarojs/components-rn": "^4.3.1",
"@tarojs/helper": "4.3.0",
"@tarojs/plugin-framework-react": "4.3.0",
"@tarojs/react": "4.3.0",
"@tarojs/rn-runner": "^4.3.1",
"@tarojs/rn-supporter": "^4.3.1",
"@tarojs/runtime": "4.3.0",
"@tarojs/runtime-rn": "^4.3.1",
"@tarojs/shared": "4.3.0",
"@tarojs/taro": "4.3.0",
"@tarojs/taro-rn": "^4.3.1",
"expo": "~50.0.21",
"react": "^18.3.1",
"react-native": "^0.73.11",
"react-native-gesture-handler": "2.14.1",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.27.0"
},
"devDependencies": {
"@react-native-community/cli-platform-android": "12.3.7",
"@react-native/babel-preset": "0.73.21",
"@react-native/gradle-plugin": "0.73.5",
"babel-preset-taro": "4.3.0",
"stylelint": "16.10.0",
"stylelint-config-standard": "^36.0.1"
}
}
必须固定的几个版本
| 依赖 | 版本 | 为什么必须固定 |
|---|---|---|
@react-native/babel-preset | 0.73.21(精确) | @rnx-kit/babel-preset-metro-react-native 的 loadPreset() 会读根 package.json 文本,只有出现这个字面量才会去解析它;否则回落到未安装的 metro-react-native-babel-preset → Cannot find module 'metro-react-native-babel-preset' |
react-native-gesture-handler / react-native-safe-area-context / react-native-screens | 2.14.1 / 4.8.2 / 3.27.0 | 生成的 RN 入口会 import gesture-handler;pnpm 严格布局下它们必须是根项目的直接依赖,否则运行时报红 |
stylelint | 16.10.0(精确) | stylelint-taro-rn@4.3.1 声明 stylelint >=16.4.0 <=16.10.0,并 import 了 stylelint 16.11.0 移除的 stylelint/lib/utils/declarationValueIndex.cjs |
expo | ~50.0.21 | 原因见 5.3 |
这三个 pin 在 Taro 官方文档里查不到,而且失败得很隐蔽:CLI 输出一切正常,只在手机端显示红屏。
3. 用 Taro Play 预览
Taro RN 项目需要安装 Taro 官方的 playground APK 来扫码预览。
- 仓库:https://github.com/wuba/taro-playground
- 安装地址:https://github.com/wuba/taro-playground/releases#release-v1.11.0-beta.18
版本一定要选 v1.11.0-beta.18。更新的版本里 hermes 的 removeListener 方法被移除了,会直接报红——这个和你的 Taro 版本相关,4.x 系列用这个版本没问题。
预览命令:
pnpm dev:rn --qr
4. 打包 release APK
有两种做法,本项目最终采用方案 A。
方案 A:项目内置 android 壳(当前采用)
思路:不下载外部壳工程,直接把 node_modules/react-native/template/android 拷进项目,改成自己的 applicationId,再让 pnpm build:rn 顺带调 Gradle 出包。
好处是原生壳和业务代码在同一个仓库里,改版本号、换签名、CI 都不用跨目录同步。
4.1 拷贝并改造原生壳
cp -r node_modules/react-native/template/android ./android
需要改动的地方:
| 文件 | 改动 |
|---|---|
android/app/build.gradle | namespace / applicationId 改成 com.mytarocalc;signingConfigs.release 用模板自带的 debug.keystore(见 7.1) |
android/app/src/main/java/com/mytarocalc/MainActivity.kt | 包名改为 com.mytarocalc;getMainComponentName() 返回 "taroDemo",必须和 config/index.ts 里的 rn.appName 一致 |
android/app/src/main/java/com/mytarocalc/MainApplication.kt | 包名改为 com.mytarocalc |
android/app/src/main/res/values/strings.xml | app_name 改成「计算器」 |
android/gradle.properties | 写死 JDK 路径:org.gradle.java.home=C:/Program Files/Java/jdk-17 |
android/local.properties | sdk.dir=D:/Android/Sdk(本机路径,不要提交) |
android/build.gradle | 仓库加阿里云镜像、NDK 版本改成本机已装的版本 |
android/build.gradle 的关键部分:
buildscript {
ext {
buildToolsVersion = "34.0.0"
minSdkVersion = 21
compileSdkVersion = 34
targetSdkVersion = 34
// 本机 Android SDK 只装了 NDK 27.1.12297006,模板默认的 25.1.8937393 没装
ndkVersion = "27.1.12297006"
kotlinVersion = "1.8.0"
}
repositories {
// 阿里云镜像放在最前面:Maven Central 上的 react-android 会被 301 重定向到
// repo.reactnative.dev,这个域名在当前网络下解析不通,会导致依赖下载失败
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
google()
mavenCentral()
}
// ...
}
// 这个块要写在 apply plugin 之前,RN 插件会在之后往 allprojects 里追加
// mavenCentral / google 等仓库,我们加的镜像排在前面才会先被用到。
allprojects {
repositories {
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
}
}
apply plugin: "com.facebook.react.rootproject"
4.2 pnpm 下的两个额外依赖
Gradle 脚本里用的是相对路径 ../node_modules/@react-native-community/cli-platform-android/native_modules.gradle 和 ../node_modules/@react-native/gradle-plugin。
pnpm 的顶层 node_modules 只有直接依赖,传递依赖都在 .pnpm/ 下;而 Java 的 getCanonicalPath() 在 Windows 上不解析符号链接,Gradle 按相对路径找不到文件。所以这两个包必须是根项目的直接依赖:
pnpm add -D @react-native-community/cli-platform-android@12.3.7 @react-native/gradle-plugin@0.73.5
4.3 打包脚本 scripts/build-android.js
Gradle 自己会用 metro 现场打 JS bundle(产物进 assets/index.android.bundle),这个过程有两个 pnpm/环境相关的坑,脚本里统一处理:
// 1. Java:Gradle / AGP 需要 JDK 11 以上。
// 本机 JAVA_HOME 指向 jdk1.8,所以要自己挑一个可用的 JDK。
const JDK_CANDIDATES = [
'C:/Program Files/Java/jdk-17',
'C:/Program Files/Java/jdk-21',
'C:/Program Files/Eclipse Adoptium/jdk-17',
]
const jdk = [env.JAVA_HOME, ...JDK_CANDIDATES].find((p) => p && javaMajor(p) >= 11)
// 2. Android SDK:读 android/local.properties 里的 sdk.dir
const sdkDir = env.ANDROID_HOME || env.ANDROID_SDK_ROOT || readSdkDir()
// 3. NODE_PATH:pnpm 是扁平化依赖结构,babel 解析
// babel-plugin-transform-react-jsx-to-rn-stylesheet 这类插件只会存在于
// node_modules/.pnpm 下,metro worker 里必须能从这里找到。
const pnpmHoist = path.join(ROOT, 'node_modules/.pnpm', 'node_modules')
env.NODE_PATH = pnpmHoist
// --no-daemon:daemon 会复用启动时的环境变量,NODE_PATH 传不进去
spawnSync('cmd.exe', ['/c', gradlew, ':app:assembleRelease', '--no-daemon'], { env })
打包完成后把 APK 复制一份到 dist/app-release.apk。
4.4 使用
pnpm build:rn # taro build --type rn && 打 APK
pnpm build:apk # 只打 APK
产物:
android/app/build/outputs/apk/release/app-release.apk
dist/app-release.apk
方案 B:官方 taro-native-shell 壳工程
Taro 官方提供了现成的壳工程:https://github.com/NervJS/taro-native-shell.git
git clone 下来放在和你的项目同级的目录下,然后配置 config/index.ts:
rn: {
appName: 'taroDemo', // 需要和原生壳里的 getMainComponentName() 一致
output: {
// 用 pnpm build:rn --platform android 时,Taro 会把打好的产物写到这里
android: '../taro-native-shell/android/app/src/main/assets/index.android.bundle',
androidAssetsDest: '../taro-native-shell/android/app/src/main/res',
},
postcss: {
cssModules: {
enable: false, // 如需使用 css modules 则设为 true
}
}
}
然后进壳工程里构建:
cd taro-native-shell/android
gradlew clean
gradlew assembleRelease
产物:
taro-native-shell/android/app/build/outputs/apk/release/app-release.apk
这个方案编译时可能遇到的坑见 第 5 节,其中 5.2(expo 模块 CMake 路径超长)和 5.3(expo 版本漂移)是这个方案独有的——方案 A 没有引入 expo 原生模块,不会触发它们。
5. 常见报错与解决方案
5.1 pnpm v11 不读项目里的 .npmrc
症状:pnpm config get <key> 返回 undefined,写在 .npmrc 里的设置全部不生效。
原因:pnpm v11 只认 pnpm-workspace.yaml,且配置项用驼峰命名(nodeLinker 而不是 node-linker)。
解决:所有 pnpm 专属设置都写进 pnpm-workspace.yaml:
allowBuilds:
'@parcel/watcher': true
'@swc/core': true
'@tarojs/binding': true
'@tarojs/cli': true
core-js: true
core-js-pure: true
esbuild: true
另外 pnpm 不会迁移已存在的 node_modules。改了 virtual-store-dir / nodeLinker 之后 pnpm install 仍然输出 Already up to date,必须删掉重装:
rm -rf node_modules # Windows: rmdir /s /q node_modules
pnpm install
5.2 ninja: error: manifest 'build.ninja' still dirty after 100 tries
仅方案 B(taro-native-shell)会遇到。
症状
CMake Warning in CMakeLists.txt:
The object file directory
D:/.../node_modules/.pnpm/expo-av@13.10.6_expo@50.0.2_b269dcd.../node_modules/expo-av/android/.cxx/RelWithDebInfo/36ld3n11/arm64-v8a/CMakeFiles/expo-av.dir/./
has 224 characters. The maximum full path to an object file is 250
characters (see CMAKE_OBJECT_PATH_MAX).
ninja: error: manifest 'build.ninja' still dirty after 100 tries
根因
Windows 路径长度超限,与 JDK 无关。
expo-av / expo-camera / expo-location 等 expo 原生模块要在 node_modules/<pkg>/android/.cxx/ 下跑 CMake/NDK 编译。pnpm 默认的隔离布局会把路径撑得很长:
<项目>/node_modules/.pnpm/expo-av@13.10.6_expo@50.0.2_<32位hash>/node_modules/expo-av/android/.cxx/RelWithDebInfo/<hash>/arm64-v8a/CMakeFiles/expo-av.dir/./src/main/cpp/JAVManager.cpp.o
└─ 合计 257 字符 > CMake 的 CMAKE_OBJECT_PATH_MAX(250),也逼近 Windows MAX_PATH(260)
ninja 反复重新生成 build.ninja 都对不齐,于是报 “still dirty”。
这个错误不只出现在
assembleRelease:gradlew clean也会触发 expo 模块的 CMake configure,同样炸。
解决:改用扁平 node_modules
在 pnpm-workspace.yaml 中设置:
# 使用扁平 node_modules:缩短 expo-* 原生模块 CMake 编译路径,避免超过 MAX_PATH(260)
nodeLinker: hoisted
改完后路径变为:
<项目>/node_modules/expo-av/android/.cxx/.../src/main/cpp/JAVManager.cpp.o
└─ 合计 173 字符 < 250 ✅
然后必须删掉 node_modules 重装(见 5.1)。
备选方案
| 方案 | 效果 | 代价 |
|---|---|---|
nodeLinker: hoisted(当前采用) | 路径 173 字符 | 依赖树从 pnpm 隔离布局变为扁平,可能有 peer 依赖告警;不含机器绝对路径,可安全提交 |
virtualStoreDir: 'D:/.pnpm-vstore' | 路径约 200 字符 | 需删除 node_modules 重装;带机器绝对路径,不适合提交 |
把仓库移到更短路径(如 D:\tns) | 路径约 214 字符 | 余量较小;移动目录 + 重装依赖 |
| 改用 npm / yarn | 同 hoisted | 与仓库的 pnpm 流程冲突 |
5.3 Unresolved reference: raw / Too many arguments for BarCodeScannerResult
仅方案 B 会遇到。
症状
e: .../expo-barcode-scanner/android/src/main/java/expo/modules/barcodescanner/BarCodeScannerView.kt:72:23 Unresolved reference: raw
e: .../expo-camera/android/src/main/java/expo/modules/camera/next/analyzers/BarcodeAnalyzer.kt:54:81 Type mismatch: inferred type is String? but (Mutable)List<Int!>! was expected
e: ... Too many arguments for public constructor BarCodeScannerResult(p0: Int, p1: String!, p2: (Mutable)List<Int!>!, p3: Int, p4: Int)
根因
仓库自身的依赖版本漂移,与 pnpm 布局无关。
BarCodeScannerResult定义在expo-modules-core里(android/src/main/java/expo/modules/interfaces/barcodescanner/BarCodeScannerResult.java)。expo-modules-core@1.11.7(由expo@50.0.2精确锁定)的构造函数是 5 个参数,没有raw:public BarCodeScannerResult(int type, String value, List<Integer> cornerPoints, int height, int width)- 而
expo-camera@14.1.3和expo-barcode-scanner@12.9.3的源码都要求带raw的 6 参数版本。 expo-modules-core从 1.11.9 起才加入了raw:public BarCodeScannerResult(int type, String value, String raw, List<Integer> cornerPoints, int height, int width)
即:expo 锁在 50.0.2(→ core 1.11.7),但 expo-camera / expo-barcode-scanner 的源码要求 core ≥ 1.11.9。
解决
升级 expo 到同一 SDK 内的最新 patch,让它带上新版 core:
pnpm update expo
结果:expo 50.0.2 → 50.0.21,expo-modules-core 1.11.7 → 1.11.14。
pnpm v11 会同步把
package.json里的范围改成"expo": "~50.0.21",lockfile 一并更新。
验证:
node -e "console.log(require('./node_modules/expo-modules-core/package.json').version)" # >= 1.11.9
5.4 Cannot find module 'babel-plugin-transform-react-jsx-to-rn-stylesheet'
方案 A 打包时必踩。
症状
> Task :app:createBundleReleaseJsAndAssets FAILED
error index.js: Cannot find module 'babel-plugin-transform-react-jsx-to-rn-stylesheet'
Require stack:
- ...\@babel\core\lib\config\files\plugins.js
根因
pnpm 的隔离布局下,babel-plugin-transform-react-jsx-to-rn-stylesheet 只是 @tarojs/rn-supporter 的传递依赖,只存在于 node_modules/.pnpm/ 里;而 babel 解析「以字符串书写的插件名」时是按 cwd / projectRoot 找的,从项目根根本找不到。
npx react-native bundle 之所以能跑通,是因为 npx 会往 NODE_PATH 里注入 pnpm 的隐藏提升目录(node_modules/.pnpm/node_modules),metro worker 恰好能解析到。Gradle 拉起的 bundle 进程没有这个 NODE_PATH,于是失败。
这个坑最阴的地方在于它会"假成功":如果只是让打包跑过去而没解决解析,产出的 bundle 里会残留原始 className 字符串、完全没有样式,界面看起来像"布局全乱"。
解决
给 Gradle 进程显式设置 NODE_PATH(scripts/build-android.js 里已完成):
const pnpmHoist = path.join(ROOT, 'node_modules/.pnpm', 'node_modules')
env.NODE_PATH = env.NODE_PATH
? path.delimiter + [env.NODE_PATH, pnpmHoist].join(path.delimiter)
: pnpmHoist
注意必须用 --no-daemon:Gradle daemon 会复用启动时的环境变量,NODE_PATH 传不进去。
验证打包结果确实是带样式的:
grep -c 'className' android/app/build/generated/assets/createBundleReleaseJsAndAssets/index.android.bundle
# 期望输出 0(>0 说明样式表 transform 没跑)
5.5 Failed to create MD5 hash for file ... as it does not exist
症状
* What went wrong:
Execution failed for task ':expo-sensors:compileReleaseKotlin'.
> Cannot access output property 'destinationDirectory' of task ...
> Failed to create MD5 hash for file '...\AccelerometerModule$definition$1$1.class' as it does not exist.
或
Execution failed for task ':app:mergeReleaseNativeLibs'.
> Failed to create MD5 hash for file '...\lib\barhopper_v3.so' as it does not exist.
根因
Gradle 给任务做输出快照(MD5)与文件实际落盘之间的竞态,文件随后又能正常找到。Windows 上更常见(杀软实时扫描、文件索引、Kotlin daemon 多会话叠加)。属于偶发问题。
解决
先停掉 daemon 再重跑即可(大部分任务会命中 up-to-date,重跑很快):
cd android
./gradlew --stop
./gradlew assembleRelease
若频繁复现,可尝试:
- 删除对应中间产物目录后重跑,例如
rm -rf android/app/build/intermediates/merged_native_libs - 关闭 Gradle 文件监听:
android/gradle.properties加org.gradle.vfs.watch=false - 关闭构建缓存:
./gradlew assembleRelease --no-build-cache
5.6 Could not download react-android-0.73.11-release.aar
症状
Could not download react-android-0.73.11-release.aar
> Could not GET 'https://repo.reactnative.dev/...'
> 未知的主机
根因
Maven Central 上的 RN 构件会 301 重定向到 repo.reactnative.dev,这个域名在当前网络下解析不通(注意:命令行 curl 能通,Gradle 的 JVM 不行)。
解决:在 android/build.gradle 里把阿里云镜像排到最前,详见 4.1。加完之后 RN 相关构件都能正常下载(react-android、hermes-android、flipper-integration、fbjni、soloader、fresco 均实测 HTTP 200)。
5.7 Could not read script '...native_modules.gradle' as it does not exist
根因
pnpm 布局 + Windows 符号链接不被 Java 的 getCanonicalPath() 解析(已实测:Java 拿到的规范化路径仍是 …\node_modules\react-native,而 node 的 realpathSync 返回的是 .pnpm/... 路径)。
解决:把 @react-native-community/cli-platform-android 和 @react-native/gradle-plugin 提成根项目的直接依赖,见 4.2。
6. 构建结果自检
cd android/app/build/outputs/apk/release
unzip -l app-release.apk | grep -E "index.android.bundle|libhermes.so" # JS bundle + Hermes 引擎
unzip -l app-release.apk | grep -o "lib/[a-z0-9_-]*/" | sort -u # 4 个 ABI
方案 A 的实测结果:
| 项 | 值 |
|---|---|
| APK | app-release.apk,约 23 MB |
| applicationId | com.mytarocalc |
| versionCode / versionName | 1 / 1.0 |
| ABI | arm64-v8a、armeabi-v7a、x86、x86_64 |
| JS bundle | assets/index.android.bundle(约 1.4 MB) |
| 签名 | ⚠️ debug keystore(见 7.1) |
7. 注意事项
7.1 release 目前用的是 debug 签名
android/app/build.gradle 里 signingConfigs.release 默认指向模板自带的 debug.keystore(android / androiddebugkey),这样本地随时能出包。
正式发布前换成自己的 keystore,不用改 build.gradle,在 android/gradle.properties 里配好这四项即可:
TARO_UPLOAD_STORE_FILE=my-release.keystore
TARO_UPLOAD_STORE_PASSWORD=***
TARO_UPLOAD_KEY_ALIAS=my-alias
TARO_UPLOAD_KEY_PASSWORD=***
对应 RN 官方文档:https://reactnative.dev/docs/signed-apk-android
7.2 android/gradle.properties 里的 org.gradle.java.home 是机器相关路径
提交前确认其他同事的 JDK 路径,或改成用脚本探测(scripts/build-android.js 已内置 JDK 11+ 的自动探测,优先用 JAVA_HOME,探测不到再回落到常见安装路径)。
7.3 pnpm-workspace.yaml 的 nodeLinker: hoisted 需要重装才生效
其他同事拉取后如果不生效,就是他们的 node_modules 还是旧的隔离布局 —— 让他们 rm -rf node_modules && pnpm install。
7.4 expo 升级跨度较大(50.0.2 → 50.0.21,跨 19 个 patch)
同属 SDK 50,理论上兼容,但建议重点回归 expo-camera / expo-barcode-scanner / expo-location 相关功能。
7.5 config/index.ts 的 rn.output 指向 taro-native-shell
方案 A 下 Gradle 会在构建时自己打 bundle,理论上这个配置可以不用;但保留它不影响构建(taro build --type rn 会额外往 taro-native-shell 里写一份),也方便随时切回方案 B。
8. 本次涉及的文件改动
| 文件 | 改动 | 是否建议提交 |
|---|---|---|
android/ | 新增,从 node_modules/react-native/template/android 拷贝并改造 | ✅ 建议(local.properties / build/ 已在 .gitignore) |
scripts/build-android.js | 新增,打包脚本(JDK 探测、SDK 路径、NODE_PATH、调 Gradle、复制 APK) | ✅ 建议 |
package.json | 新增 build:apk;build:rn 串联打包脚本;dev:rn 改为 taro build --type rn --watch | ✅ 建议 |
package.json | devDeps 增加 @react-native-community/cli-platform-android、@react-native/gradle-plugin | ✅ 建议 |
pnpm-lock.yaml | 随上面更新 | ✅ 建议 |
config/index.ts | rn.output 指向 ../taro-native-shell/... | ✅ 建议 |
.gitignore | 忽略 android/.gradle/、android/app/build/、android/local.properties 等 | ✅ 建议 |
android/local.properties | 本机 SDK 路径 | ❌ 不提交 |
android/gradle.properties | org.gradle.java.home 是机器相关路径 | ⚠️ 按需决定 |
更多推荐

所有评论(0)