Expo Universal Modules 迁移 TypeScript 的完整实践指南:导入改造、构建管线与工程化落地

发布时间:2026/9/8 18:08:41
Expo Universal Modules 迁移 TypeScript 的完整实践指南:导入改造、构建管线与工程化落地 Expo Universal Modules 迁移 TypeScript 的完整实践指南导入改造、构建管线与工程化落地【免费下载链接】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/expoUniversal Modules如今统称为 Expo Modules是 Expo SDK 生态中承载设备能力文件系统、传感器、相机、通知等的模块体系。本指南以仓库内历史迁移文档 Migrating Universal Modules to TypeScript.md 为核心骨架逐步骤讲解如何把一个旧的 JavaScript/Flow 编写的 Universal Module 迁移为 TypeScript 工程从为死代码消除重构导入方式、用getter兼容旧语法并提示弃用到建立src/__tests__测试、接入 Jest/CI、调整依赖与构建脚本、生成标准tsconfig.json的完整过程。阅读完本文你将掌握一套可直接照搬到任意 Expo 模块仓库的 TypeScript 迁移清单并理解该实践在今天的 expo 仓库如 packages/expo-sms中沉淀出的最终工程形态。迁移文档的目标读者是维护 SDK 仓库本身的工程师——当时仓库中仍存在一批以 JavaScript Flow 编写、缺乏统一测试与构建约定的模块。与之一脉相承的模块工程化总纲生成新模块、目录结构、脚本约定可进一步参考 Expo Module Infrastructure.md。下文沿用原文档的七个步骤骨架并在每一节结合当前仓库源码说明这些规范最终是如何被expo-module-scripts固化成默认工程结构的。一、为什么要把模块迁移到 TypeScript原文档记录的是 Expo 团队内部的一次「全模块工程化统一」行动其动机可概括为三点原文在 guides/Expo Module Infrastructure.md 中有更完整的表述类型安全与一致性TypeScript 让 SDK 各模块拥有可被tsc校验、可被下游应用引用的.d.ts类型产物API 跨模块风格统一可维护性与可测试性配合 Jest 建立模块级单元测试让每次改动都有快速、确定性的反馈为打包优化铺路通过把模块入口从「一个汇总的index.js」调整为「与模块同名的build/MODULE.js」并配合具名/命名空间导入方式让打包器能做更彻底的死代码消除dead code elimination避免把整个模块的体积白白带进用户应用。原文将这些目标浓缩为七步操作改造 import 入口、补测试并接入 CI、原生依赖下沉到peerDependencies、接入expo-module脚本、生成标准tsconfig.json以及若干杂项收尾。下面按此顺序展开。二、第一步改造模块导入方式为死代码消除做准备1. 从具名导出改为命名空间导入为了优化库的「死代码消除」效果原文档要求把模块的导出方式迁移为「命名空间导入」- import { FileSystem } from expo-file-system; import * as FileSystem from expo-file-system/legacy;原因很实际import { FileSystem } from ...会让打包器认为需要解析并保留整个模块的所有副作用导出而当模块把 API 收敛为「单一命名空间对象 各具名导出」时未使用的部分更容易被摇树工具移除。这在今天的模块代码中已是约定俗成的写法——例如 packages/expo-file-system/src/legacy/FileSystem.ts 中类型注释与文档字符串里就明确写着使用import * as FileSystem from expo-file-system/legacy的推荐姿势。2. 主入口指向与模块同名的构建产物理想的入口设计是让模块的主入口成为与模块同名的文件例如build/FileSystem.js而不是笼统的build/index.js。对应地在package.json中声明package.json- main: index.js, main: build/MODULE NAME.js, types: build/MODULE NAME.d.ts,mainNode/Metro 等解析包时加载的 JS 入口typesTypeScript 解析该包类型时读取的声明文件直接指向同名.d.ts可让编译器获得精确到单文件的类型解析避免「类型都堆在一个巨型 index 里」导致的构建负担。这一点在今天已完全落地为仓库的默认规范。以 packages/expo-sms/package.json 为例main: build/SMS.js, types: build/SMS.d.ts,即迁移后的模块把自己的入口命名为与模块同名SMSbuild/由src/编译产出并且不提交到 Git由.gitignore排除靠 Turborepo 按需构建与缓存。3. 全仓同步替换 import文档特别强调当时有大量库只是import ... from expo从 expo 总包导入。这类情况同样要替换并且要把packages/、apps/、docs/下所有 import 一并改掉否则会留下新旧两套导入长期并存。这一步没有捷径属于迁移时的机械性重活需要配合全局搜索逐个模块确认文档中原话要求 Ensure you change all imports acrosspackages/,apps/, anddocs/.三、第二步兼容旧导入语法为老 API 添加弃用警告直接改 import 会让所有存量调用方编译失败。原文档给出的过渡方案是在模块的src/index.ts入口保留一个「兼容别名」通过Object.defineProperty定义带get的遗留导出在首次被访问时打印一次弃用警告src/index.tsimport * as FileSystem from ./FileSystem; export * from ./FileSystem; let wasImportWarningShown false; // ts-ignore: Temporarily define an export named FileSystem for legacy compatibility Object.defineProperty(exports, FileSystem, { get() { if (!wasImportWarningShown) { console.warn( The syntax import { FileSystem } from expo-file-system is deprecated. Use import * as FileSystem from expo-file-system or import named exports instead. Support for the old syntax will be removed in SDK 34. ); wasImportWarningShown true; } return FileSystem; }, });几个值得注意的实现细节wasImportWarningShown保证警告只弹一次避免污染用户控制台SDK 34是当时设定的移除期限是本文档时间线的重要锚点——该版本线早已过去ts-ignore是因为该导出名并非静态声明需要临时绕过类型检查警告信息里明确给出迁移方向改为命名空间导入或改用具名导出让使用者有路可循。文档同时给出长远目标最终删除src/index.ts这个汇总入口只保留与模块同名的命名文件src/FileSystem.ts——过渡期入口只是「临时脚手架」不是目的地。从「运行时 getter 警告」到今天的演进这套「旧语法兼容 渐进移除」的思路在今天的仓库中以更精细的形态继续存在。仍以 expo-file-system 为例包通过 packages/expo-file-system/package.json 中的exports字段暴露了.、./legacy、./next三条子路径其中./legacy指向src/legacy的构建产物./next则与主入口同源——即「新 API 走新路径老 API 走 legacy 路径」的子路径隔离方案比当年的单入口 getter 兼容更进一步同一模块的 packages/expo-file-system/src/legacyWarnings.ts 把「弃用」写成两类信号一类是deprecatedJSDoc 标注编译期对调用方的提示另一类是运行时抛错/提示——例如其中的提示文本已经写明import the legacy API from expo-file-system/legacy并指向 v54 文档说明该迁移策略在后续多个 SDK 大版本中一直在延续。四、第三步建立src/__tests__并接入 CI迁移必须附带测试原文档要求为每个模块新增src/__tests__目录测试可在包的根目录用yarn test执行。1. 测试工具迁移到jest-expo如果包内还是旧的test/工具结构要迁移到使用jest-expoExpo 官方 Jest preset负责注入 React Native / Expo 平台的 mock 环境src/__tests__/MODULE NAME-test.ts- import { mockPlatformWeb } from ../../test/mocking; import { mockPlatformWeb } from jest-expo;mockPlatformWeb用于模拟 Web 平台让同一套测试可以在 mock 出的不同平台上运行——这正是 Expo 多端 SDK 模块测试的关键基础设施。当前仓库中jest-expo作为工作区依赖统一提供给模块构建工具链见 packages/expo-module-scripts/package.json 中声明的jest-expo: workspace:~56.0.4。2. 在package.json中声明 Jest presetpackage.jsonjest: { preset: expo-module-scripts },expo-module-scripts提供的 preset 会为模块装配好 TypeScript 编译当前实现基于swc/jest与 React Native 的 jest preset 栈见其 package.json 依赖列表、平台 mock 与快照格式化等能力让「模块级测试」与「App 内实际运行行为」尽量贴近。真实模块如 packages/expo-sms/package.json 和 packages/expo-file-system/package.json 至今仍保留着完全相同的这段配置。3. 运行测试在包的根目录执行yarn test在如今以 pnpm 为包管理器的 monorepo 中等价命令是pnpm test例如 packages/expo-sms 的test脚本直接就是jest。以 packages/expo-sms/src/tests/SMS-test.ts 为样板可以看到迁移后的模块测试文件就放在约定目录内能被 preset 直接识别与运行。4. 把测试挂进 CI文档要求在当时的仓库根级.circleci/config.yaml中向名为expo_sdk的 job 增加一个 step且与其他测试 step 保持字母序排列这是为了维持 CI 配置的可读性.circleci/config.yaml- yarn: command: test --maxWorkers 1 working_directory: ~/expo/packages/expo-sms两点说明--maxWorkers 1让 CI 单 worker 串行跑测试避免在资源受限的 CI 容器里 OOMworking_directory: ~/expo/packages/expo-sms把测试限定在单个模块目录内执行。需要说明的是.circleci/config.yaml是该文档写作时期仓库 CI 的载体。Expo 仓库的 CI 编排此后一直在演进如今 monorepo 的构建/测试已交由根目录的 turbo.json 调度包管理也统一为根目录 pnpm-workspace.yaml 描述的工作区。落地到自己的仓库时应按你当前 CI 的 job/步骤机制套用同样的「单模块、限定工作目录、串行执行」模式。五、第四步把含原生代码的dependencies挪到peerDependencies原文档给出了一个关键的原生依赖原则In order to prevent overlapping native code innode_modules, we should move anydependenciescontaining native code topeerDependencies.即任何携带原生代码Android Kotlin/Java、iOS Objective-C/Swift或依赖它们的桥接库的依赖都不能放进dependencies否则不同模块会各自安装一份原生实现导致node_modules中出现重叠的原生代码重复类名、重复链接、版本分裂。迁移时把这些依赖声明为peerDependencies由宿主 App 统一安装与解析一份。这一规范沿用至今例如 packages/expo-sms/package.json 中dependencies为空原生宿主能力依赖expo等全部收敛在peerDependenciesdependencies: {}, peerDependencies: { expo: * }从工程语义上理解JS 库代码无原生部分仍可放进dependencies只要涉及原生代码就必须上移到peerDependencies让 AppExpo Go / 独立原生工程来决定最终链接哪一份。六、第五步接入expo-module脚本统一构建/清理/测试/发布为了让所有模块用同一套命令约定工作文档要求在package.json中加入统一的expo-module系列脚本package.jsonscripts: { build: expo-module build, clean: expo-module clean, test: expo-module test, prepare: expo-module prepare, prepublishOnly: expo-module prepublishOnly, expo-module: expo-module }各脚本职责结合 Expo Module Infrastructure.md 的现行说明expo-module暴露 CLI 本身可用expo-module --help查看全部子命令build把src/编译到build/clean删除build/产物目录test以 watcher 模式跑 Jest面向开发者改动即重跑prepare生成/校验包内自动生成的配置文件prepublishOnly发布前的检查与构建保证发布产物是最新编译结果。expo-module可执行文件正是由expo-module-scripts提供的见 packages/expo-module-scripts/package.json 中bin: { expo-module: bin/expo-module.js, ... }。该文档也提醒其中多个子命令面向「人类开发者」会启动文件监听等交互行为要在 CI 等非交互环境运行请设置环境变量EXPO_NONINTERACTIVE1。今天的模块脚本集在其基础上又补充了lintoxlint、typechecktsc类型检查、depscheck、format等真实的expo-sms脚本见 packages/expo-sms/package.json可作为迁移后的完整模板。七、第六步用expo-module-scripts生成标准tsconfig.json在模块根目录执行expo-module prepare或已定义脚本时直接yarn prepare/pnpm prepare即可生成仓库所有模块统一采用的 tsconfig/tsconfig.json// generated by expo-module-scripts { extends: expo-module-scripts/tsconfig.base, compilerOptions: { outDir: ./build }, include: [./src], exclude: [**/__mocks__/*, **/__tests__/*] }要点解读文件首行标注// generated by expo-module-scripts表示该文件由工具生成Expo 的约定是把它提交进 Git以便跟踪变更、必要时人工微调extends: expo-module-scripts/tsconfig.base让所有模块共享同一份编译基线避免各模块各自漂移出不同的编译选项outDir: ./build把产物输出到约定的构建目录include: [./src]限定只编译源码exclude排除__mocks__Jest mock 目录与__tests__测试代码保证发布产物里不混入测试与 mock。今天的基线配置与模板差异expo-module-scripts的基线配置 packages/expo-module-scripts/tsconfig.base.json 目前已是一份相当严格的生产级配置值得逐项了解例如模块解析采用moduleResolution: bundlermodule: esnextjsx使用react-jsx适配 RN/Metro 生态与 React 19开启strict全量严格模式以及noImplicitReturns、noUncheckedIndexedAccess、noFallthroughCasesInSwitch等进阶严格项declaration/declarationMap/sourceMap全开保证产物带.d.ts与源码映射方便下游调试customConditions注入expo-source条件把 monorepo 内对包的类型解析重定向到src/源码这也与 expo-file-system 包exports字段里的expo-source条件一一对应支撑「改源码即改类型」的本地开发体验。而由工具生成的模板 packages/expo-module-scripts/templates/tsconfig.json 在演进中变成了「类型检查专用」形态// generated by expo-module-scripts { extends: expo-module-scripts/tsconfig.base, compilerOptions: { rootDir: ./src, noEmit: true }, include: [./src] }对比可见真正的 JS 产物改由专门的构建命令如expo-build src产出而 tsconfig 里以noEmit: truerootDir承担纯类型检查职责。真实模块 packages/expo-sms/tsconfig.json 与 templates/tsconfig.json 完全一致就是这套演进的直接证据。八、第七步各类收尾杂项原文档最后罗列了几项容易被遗漏的迁移清理工作移除babel-preset-expo模块自身的 Babel 配置不再需要手写由expo-module-scripts的 preset 统一接管Jest 一侧也通过 preset 完成与 App 一致的转译移除 Flow删掉所有 Flow 类型标注与// flow头改为 TypeScript 类型。相关代码风格约定可参考 Expo JavaScript Style Guide.md类型文件命名规范.types.ts当一套类型既要在 Web 实现的「原生层」使用、又要在 API 层复用时把类型抽到带.types.ts后缀的命名文件里避免循环依赖与职责混杂。原文档还指出某些大模块当时举的例子是expo-av类型复杂时应进一步把类型拆分到更小的文件中——「单个文件类型汇聚点」并非终态规模上来后要主动拆分如今仓库中的音频/视频能力已由expo-audio、expo-video承接原expo-av目录已不在 packages 中但其「类型独立成文件、必要时拆小」的原则仍在沿用。九、结语一份可复用的迁移检查清单把原文档的七步浓缩成可直接对照执行的检查清单步骤检查项验收标准1. 导入改造模块提供命名空间导入主入口为build/MODULE.jstypesimport * as X from pkg全仓生效无用导出可被摇树2. 兼容与弃用旧语法保留一次性运行时警告存量调用方平滑过渡警告文案给出新写法3. 测试与 CI新增src/__tests__、jest-expo、jest.presetyarn/pnpm test通过CI 单 worker 限定模块目录运行4. 依赖治理含原生代码的依赖迁至peerDependenciesnode_modules不出现重复原生实现5. 构建脚本加入expo-module系列 scripts各模块命令一致build/可一键重建6. tsconfigexpo-module prepare生成generatedtsconfig所有模块extends同一基线类型检查通过7. 收尾清理移除babel-preset-expo、Flow抽.types.ts源码无 Flow 残留类型可跨层复用这套迁移方法论的价值在于「沉淀」今天 packages/expo-module-scripts 这一私有包把构建expo-build、Jest preset、tsconfig 基线与模板、模块 CLIexpo-module全部固化为默认设施新模块开箱即用老模块则通过本次迁移逐步并入同一套规范。无论你是要迁移 Expo 仓库内的一个存量模块还是为自己的多模块 TypeScript 工程建立统一工程化基线都可以把本文的检查清单当作迁移执行的路线图。【免费下载链接】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),仅供参考