Expo 模块开发基础设施指南:基于expo-module-scripts的统一工程化实践
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
本指南面向希望在 Expo 开源仓库中贡献或移植"通用模块"(universal module)的开发者,系统讲解从
src源码到build产物的标准开发流程:模块脚手架、统一构建/测试工具链、TypeScript 配置、Jest 预设与package.json元数据约定。读完本文,你将掌握 Expo 多模块 monorepo 中"一份配置、处处复用"的工程化心法,并能独立创建一个符合官方质量标准的原生模块。
本文依据仓库文档 guides/Expo Module Infrastructure.md 展开,并结合当前仓库中的实际实现(packages/expo-module-scripts、packages/expo-sms 等)进行源码级佐证与更新。该文档在仓库中已被标注Warning: This doc is outdated and will be updated soon,因此本文在保留其核心骨架的同时,会以当前仓库代码为准说明演进后的实际用法。
统一模块基础设施:为什么要标准化
Expo 仓库(本仓库即其镜像)维护着大量跨平台原生模块(Android / iOS / Web),例如短信、相机、文件系统等。这些模块如果各自为政,会带来三方面问题:
- SDK 一致性:不同模块在 API 形态、目录组织、构建产物上差异过大,会破坏整体 SDK 的连贯质量;
- 知识复用:开发者在一个模块上积累的经验难以迁移到其他模块;
- 维护负担:工具链版本漂移(Babel、TypeScript、Jest 各用各的版本)会让 monorepo 的整体升级变得极其痛苦。
因此该仓库的目标非常明确:让所有模块共享同一套标准配置与工具,降低模块间差异,让"在 Expo 上开发模块"这件事保持简单。
这一目标的技术落点是名为expo-module-scripts的私有包——它被定位为"模块配置的唯一事实来源(source of truth)"。在 pnpm workspaces 布局下,所有模块都引用仓库内版本(workspace:*),从结构上保证了各模块使用完全一致的 Babel、TypeScript、Jest 版本。
仓库根目录存在 pnpm-workspace.yaml,确认了 pnpm workspace 的多包管理方式;各模块package.json中形如"expo-module-scripts": "workspace:*"的声明(参见 expo-sms/package.json)正是这一机制的体现。
生成一个新模块:从expo-cli到create-expo-module
原文档记录的是expo-cli时代的脚手架命令:
expo generate-module [new module directory]- 可选参数
[new module directory]用于指定模块名,例如expo generate-module expo-test-module会创建名为expo-test-module的模块;省略时会以交互方式询问模块名; - 可选参数
--template <template directory>用于指定创建模块时使用的模板目录。
需要说明的是,原文档特别标注为 outdated,从当前仓库源码结构看,模块脚手架已经演进为独立的包与模板目录:
- packages/create-expo-module:负责模块创建的编排逻辑(特征探测、平台支持、示例应用生成、模板处理、包管理器选择、遥测等均可从 packages/create-expo-module/src 的源码结构推断出来);
- packages/expo-module-template:沉淀了标准模块骨架的模板文件(以
.ejs模板为主,包含src、平台原生代码、Jest 配置、package.json、tsconfig.json等模板源)。
若你计划基于当前仓库创建模块,应以这两个包为准;文档中expo-cli的用法描述可作为历史背景理解。
expo-module-scripts:模块开发的"标准件集"
把它声明为开发依赖
每个模块要在package.json中把expo-module-scripts声明为开发依赖:
{ "devDependencies": { "expo-module-scripts": "^<latest version>" } }当前仓库中 expo-module-scripts/package.json 表明其最新版本为56.0.3,自述定位为 "A private package for various tasks for Expo module packages like compiling and testing"(为 Expo 模块包提供编译与测试等任务的私有包)。注意:
- 该包是私有(private)基础设施包,一般不被模块对外发布所依赖,仅作为开发期工具链存在;
- 它暴露两个可执行入口(
bin字段):expo-build与expo-module(见 package.json)。
标准 npm scripts
expo-module-scripts定义了一系列开发期或 npm 生命周期期应执行的脚本。按原文档约定,各模块在package.json中声明如下公共脚本:
{ "scripts": { "build": "expo-module build", "clean": "expo-module clean", "lint": "expo-module lint", "test": "expo-module test", "postinstall": "expo-module postinstall", "prepare": "expo-module prepare", "prepublishOnly": "expo-module prepublishOnly", "expo-module": "expo-module" } }expo-module程序由expo-module-scripts提供,可通过pnpm expo-module --help查看全部命令。这些脚本中,相当一部分是交互式的(会启动文件监听器,面向人类开发者而非 CI)。若要以非交互模式运行,需设置环境变量EXPO_NONINTERACTIVE=1。
对照当前仓库 bin/expo-module.js 的源码,实际注册的子命令如下:
| 分类 | 命令 | 说明 |
|---|---|---|
| 常用脚本 | configure | 生成公共配置文件(如 tsconfig 等) |
| 常用脚本 | readme | 生成 README |
| 常用脚本 | typecheck | 只做类型检查、不产出 JS,并监听文件变化 |
| 常用脚本 | build | 编译 src 中的 JS/TS 并监听文件变化 |
| 常用脚本 | depscheck | 校验源码 import 是否全部声明在 dependencies 中 |
| 常用脚本 | format | 格式化源码 |
| 常用脚本 | test | 以交互式 watcher 运行单元测试 |
| 常用脚本 | clean | 删除编译产物 |
| 生命周期脚本 | prepare | 在 npmprepare阶段执行 |
| 生命周期脚本 | prepublishOnly | 在 npmprepublishOnly阶段执行 |
| 透传脚本 | jest | 以给定参数运行 Jest |
| 透传脚本 | tsc | 以给定参数运行 tsc |
原文档所列的postinstall子命令在现版 CLI 中已被configure取代——这一点从 bin/expo-module.js 的命令清单可以确认。各子命令实际委托给bin/下对应的可执行脚本(如expo-module-clean、expo-module-configure、expo-module-test等),这些文件在 publishConfig.executableFiles 中被逐一登记以保证发布后具有可执行权限。
以真实模块为例,expo-sms/package.json 中即采用了与上表一致的生命周期接线:clean/format/prepublishOnly/depscheck直接调用expo-module子命令,build使用expo-build src,typecheck使用tsc -p tsconfig.json。
自动生成的配置文件与"提交进 Git"策略
configure(原文档描述中为postinstall)会在必要时于模块包内自动生成配置文件——例如 Babel 会在包目录内查找自己的配置文件。
关键约定是:这些自动生成的配置文件要提交进 Git。理由有二:
- 可以在版本历史中跟踪这些文件的变更;
- 必要的情况下,允许开发者手动编辑并提交这些文件。
如果你看过真实模块目录(例如 expo-sms),会发现tsconfig.json、oxlint.config.mjs、expo-module.config.json等生成文件确实被提交在仓库中,而非仅存在于本机。
目录结构与构建产物约定
expo-module-scripts对模块目录有严格约定:
- 模块源码必须以 TypeScript 书写,放在名为
src的目录下; - 编译产物输出到名为
build的目录; build目录不提交到 Git(在.gitignore中)。
之所以敢把产物排除出版本控制,是因为 Turborepo 即承担这一任务编排职责。
以 expo-sms/src 为例,其源码组织为:
SMS.ts:公开 API 入口SMS.types.ts:类型定义ExpoSMS.ts/ExpoSMS.native.ts:平台桥接实现__tests__/:随附单元测试
在package.json中,包的主入口要指向build下的编译产物:
{ "main": "build/ExampleModule.js" }真实示例见 expo-sms/package.json:"main": "build/SMS.js",同时"types": "build/SMS.d.ts"。
运行pnpm clean会删除整个build目录(如 expo-sms/package.json 的"clean": "expo-module clean")。
编译 TypeScript 与类型检查
- 运行
pnpm build:把源码从src编译到build。在模块内,该命令通常为expo-build src(见 expo-sms/package.json)。 - 运行
pnpm typecheck:用tsc做纯类型检查,不产出任何 JS 文件。
在 monorepo 中跨包工作时,优先从仓库根目录通过 Turborepo 运行pnpm build、pnpm typecheck——这样只有受影响的包会被重建,且结果会被缓存复用。
模块的tsconfig.json由configure自动生成,并extendsexpo-module-scripts内部的主配置。该主配置即 tsconfig.base.json。从其中可以读到模块级 TypeScript 的几组关键默认值:
- 模块与解析:
module: "esnext"、moduleResolution: "bundler"、moduleDetection: "force"、verbatimModuleSyntax: true、isolatedModules: true; - React JSX:
jsx: "react-jsx"; - 严格性:
strict: true,外加noImplicitReturns、noUncheckedIndexedAccess、noFallthroughCasesInSwitch、forceConsistentCasingInFileNames等; - 产物:
declaration: true、declarationMap: true、sourceMap: true、inlineSources: true——这意味着类型声明与源码映射在编译期就一并产出,方便 IDE 跳转与调试; - 类型检查加速:
incremental: true,并把增量缓存写入${configDir}/.tsbuildinfo; - 类型来源:
lib同时包含dom、DOM.Iterable、esnext,types预置jest,且通过customConditions把 monorepo 内包的类型重定向到src。
快速且可预期的单元测试
expo-module-scripts同时提供一个 Jest 预设。模块只需在package.json中加入:
{ "jest": { "preset": "expo-module-scripts" } }该预设为测试拉起专门的 tsconfig,并让测试代码像在真实 App 中那样完成转译。
原文档提到该预设基于ts-jest,这一点已随仓库演进有所变化:从 expo-module-scripts/package.json 的依赖清单可以看到@swc/jest、@swc/core、@swc-contrib/mut-cjs-exports以及同目录下的 jest-swc-transform.cjs,说明当前实现走的是 SWC 与 jest-expo 组合的转译路径,还预置了@testing-library/react-native供组件测试使用。文档中关于ts-jest的描述应按过期处理。
阅读预设核心 jest-preset.cjs 还能看到几个值得注意的工程决策:
passWithNoTests: true——允许尚无测试文件的包不因"没有测试"而失败;- 通过
jest-expo/config/maxWorkers限制并行 worker 数,避免在开发机上跑满 CPU; - 采用multi-project runner,把 iOS / Android / Web / Node 四个平台的 jest-expo 预设分别包进
createJestPreset(...)并作为独立 project 运行——这意味着一个模块的单元测试会同时在这些目标环境中验证; - 接入
jest-watch-typeahead与jest-snapshot-prettier(对快照做 prettier 排版),提升交互式 watch 体验。
运行pnpm test会以 watcher 模式启动 Jest:默认只跑受改动影响的测试,并在文件保存后自动重跑。由于每次文件变化都会触发测试,这些单元测试必须保持快速与确定性——这也是上面maxWorkers限制与分层预设设计的初衷。真实模块的接线可参考 expo-sms/package.json 的"jest": { "preset": "expo-module-scripts" }。
package.json元数据字段约定
为了让每个模块在 npm 与 GitHub 上可追溯、可归属,原文档要求各模块在package.json中填写统一元数据:repository与bugs指向 Expo 仓库,homepage指向模块源码位置,并声明author与license。
原文档给出的示例形态如下:
{ "repository": { "type": "git", "url": "<Expo 仓库的 git 地址>" }, "author": "Expo", "license": "MIT", "bugs": { "url": "<Expo 仓库的 issue 地址>" }, "homepage": "<该模块的源码或文档地址>" }参考当前仓库中真实模块的落地写法(见 expo-sms/package.json),expo-sms在 monorepo 内的成熟实践还包含repository.directory指向本包子目录packages/expo-sms,homepage指向其 SDK 文档页,keywords罗列expo、react-native、sms等检索词,sideEffects: false帮助打包器做 tree-shaking。
此外,一个功能完整的模块通常还会声明types(指向build下的.d.ts)、peerDependencies(如对expo的依赖)、devDependencies(如expo-module-scripts: workspace:*)——全部细节都可在 expo-sms/package.json 中对照查看。
从零到一:模块开发速查清单
把上述约定收敛为一份可执行的清单:
- 初始化:在
packages/下放置模块目录,命名遵循expo-*;如用脚手架,参考 create-expo-module 与 expo-module-template(原文档中expo-cli generate-module为过期描述)。 - 接入
expo-module-scripts:在devDependencies中声明expo-module-scripts(当前版本56.0.3),利用 pnpm workspace 统一其来源。 - 声明 npm scripts:按上文表格接入
build/clean/lint/test/prepare/prepublishOnly/typecheck;交互式命令在 CI 中记得设置EXPO_NONINTERACTIVE=1。 - 生成并提交配置:运行
expo-module configure生成tsconfig.json等公共配置,并将其提交进 Git。 - 布局源码与产物:源码放
src/,产物输出build/(不入库),main指向build/<Entry>.js。 - 配置测试:在
package.json中声明"jest": { "preset": "expo-module-scripts" },保持单测快速、确定性,以支撑 watcher 模式的每次保存重跑。 - 填写元数据:
repository/bugs指向 Expo 仓库并在directory中标注模块子路径,homepage指向模块文档或源码位置,声明author、license: MIT、types与合适的keywords。 - 编译与发布:开发期用
pnpm build/pnpm typecheck(优先从 monorepo 根通过 Turborepo 执行以复用缓存),发布走prepublishOnly阶段校验。
遵循这套基础设施,模块开发者的日常循环就收敛为"写src→ watcher 自动构建与测试 → 提交配置与源码",而把工具链的差异与版本漂移彻底隔绝在expo-module-scripts这一个事实来源中——这正是 Expo 能在数十个跨平台模块间长期维持 API 连贯性与工程质量的关键所在。
【免费下载链接】expoAn open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.项目地址: https://gitcode.com/GitHub_Trending/ex/expo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考