Taro 框架 React Native 项目开发全流程记录

本文记录用一个 Taro 4.3 + React Native 版「计算器」项目走通 Taro RN 工程的完整流程。项目业务代码很简单,主要目标是熟悉 Taro 框架 Android 项目的从脚手架到出包的全过程。

目录


0. 环境要求

工具版本说明
Node.js≥ 18建议用 LTS
pnpmv11注意 v11 的配置方式变了,见 5.1
JDK17Gradle / AGP 要求 11+,本机 JAVA_HOME 默认指向 jdk1.8,必须处理
Android SDKcompileSdk 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-preset0.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-screens2.14.1 / 4.8.2 / 3.27.0生成的 RN 入口会 import gesture-handler;pnpm 严格布局下它们必须是根项目的直接依赖,否则运行时报红
stylelint16.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 来扫码预览。

版本一定要选 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.gradlenamespace / 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.xmlapp_name 改成「计算器」
android/gradle.properties写死 JDK 路径:org.gradle.java.home=C:/Program Files/Java/jdk-17
android/local.propertiessdk.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 的实测结果:

项值
APKapp-release.apk,约 23 MB
applicationIdcom.mytarocalc
versionCode / versionName1 / 1.0
ABIarm64-v8a、armeabi-v7a、x86、x86_64
JS bundleassets/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.jsondevDeps 增加 @react-native-community/cli-platform-android、@react-native/gradle-plugin✅ 建议
pnpm-lock.yaml随上面更新✅ 建议
config/index.tsrn.output 指向 ../taro-native-shell/...✅ 建议
.gitignore忽略 android/.gradle/、android/app/build/、android/local.properties 等✅ 建议
android/local.properties本机 SDK 路径❌ 不提交
android/gradle.propertiesorg.gradle.java.home 是机器相关路径⚠️ 按需决定
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐