教程:生成模块 TS 接口

编辑页面

一篇关于使用 expo-type-information 包为 Expo 模块创建 TypeScript 接口的教程。


For the complete documentation index, see llms.txt. Use this file to discover all available pages.

本教程适用于 macOS 用户,因为 expo-type-information 包仅能在 macOS 上运行。

编写 Expo Modules 往往意味着需要多次编写模块接口:在 Swift 中、在 Kotlin 中以及在 TypeScript 中。expo-type-information 包通过直接从你的 Swift 代码中提取类型定义来生成 TypeScript 接口,从而自动化这一过程。

在本教程中,你将学习如何使用 expo-type-information 包为 inline modulesregular Expo modules 生成 TypeScript 接口。

设置你的项目

首先安装 expo-type-information 包。

Terminal
npm install expo-type-information

要使用此包,你还需要安装 sourcekitten。

Terminal
brew install sourcekitten

生成 inline modules 接口

我们将基于 inline modules 示例 继续。在该教程中,我们构建了一个包含 inline module 和 inline view 的示例应用。在你的项目中,你应该有:

app
FirstInlineModule.kt
FirstInlineModule.swift
FirstInlineView.swift
FirstInlineView.kt
index.ts

请记住,我们曾经有一个 index 文件,在其中我们直接使用 requireNativeModulerequireNativeView 引用 inline modules,但它们返回的是 any 类型,既没有类型安全,也不支持自动补全。

app/index.tsx
import { requireNativeModule, requireNativeView } from 'expo'; import { StyleSheet, Text, View } from 'react-native'; const FirstInlineModule = requireNativeModule('FirstInlineModule'); const FirstInlineView = requireNativeView('FirstInlineView'); export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {FirstInlineModule.Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });

现在我们将使用 expo-type-information 包为这些模块生成 TS 接口。在项目根目录运行以下命令:

Terminal
npx expo-type-information inline-modules-interface --app-json ./app.json --watcher

--app-json(简写 -a)用于指定应用配置文件的路径,其中定义了 inline modules 的 watchedDirectories。使用 --watcher(简写 -w)选项时,应用配置文件以及所有 watchedDirectories 都会被监听,当它们发生变化时,TS 接口将被重新生成。

运行该命令后,你应该会在项目中看到 4 个新文件:

app
FirstInlineModule.generated.ts
FirstInlineModule.tsx
FirstInlineView.generated.ts
FirstInlineView.tsx

我们先看看 FirstInlineModule.swift。对于你项目中的每个 inline module,都会创建两个文件,在这个例子中是 FirstInlineModule.generated.tsFirstInlineModule.tsx

app/FirstInlineModule.generated.ts
/*由 expo-type-information 自动生成。*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class FirstInlineModuleNativeModuleType extends NativeModule { readonly Hello: string; }

这个“generated”文件包含了所有已解析的类型,在我们的例子中,它只包含 FirstInlineModule 的定义,其中只声明了一个名为 Hello、类型为 string 的常量。再次运行该命令,或者在使用 --watcher 时,该文件都会被重新生成,所以不要修改它,除非你不再打算使用这个命令。

接下来看看为 FirstInlineModule 生成的另一个文件

app/FirstInlineModule.tsx
// 文件哈希:c7729100cc23e11d5d39fcb99fe861f7b03502986ee7becb85731cb631f37000 import { FirstInlineModuleNativeModuleType } from './FirstInlineModule.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineModule: FirstInlineModuleNativeModuleType = requireNativeModule<FirstInlineModuleNativeModuleType>('FirstInlineModule'); export const Hello: string = FirstInlineModule.Hello;

这个文件应该是你的模块的“stable”接口。CLI 使用文件哈希来检测手动修改。如果你自定义了这个文件,CLI 将停止覆盖它,从而允许你添加自定义逻辑或辅助函数,同时让你的原生类型在“generated”文件中保持同步。

在我们的例子中,我们只是将原生模块中的 Hello 常量重新导出。

现在让我们看看为 FirstInlineView.swift 生成的文件

app/FirstInlineView.generated.ts
/*由 expo-type-information 自动生成。*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; // 以下类型未在提供的文件中定义。 export type URL = unknown; export interface ExpoWebViewProps extends ViewProps { url: URL; onLoad?: (event: any) => void; } export declare class FirstInlineViewNativeModuleType extends NativeModule {}

查看“generated”文件,我们可以看到并非 FirstInlineView.swift 中的所有类型都能被解析,URL 类型被设为 unknown。当某个类型不是基础类型(这些类型由工具手动映射),并且没有在提供的文件中定义时,就可能出现这种情况(在这里它没有在 FirstInlineView.swift 中定义),或者工具未能解析其定义时也会出现。

我们还可以看到生成了一个 ExpoWebViewProps 接口,它包含了 FirstInlineView 的属性和事件。

app/FirstInlineView.tsx
// 文件哈希:6eb6c583bee1f61cbb9f6557faadc9d6b7fb51313c05027076431304668f7ac5 import React from 'react'; import { URL, FirstInlineViewNativeModuleType, ExpoWebViewProps, } from './FirstInlineView.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineView: FirstInlineViewNativeModuleType = requireNativeModule<FirstInlineViewNativeModuleType>('FirstInlineView'); const ExpoWebView = requireNativeView<ExpoWebViewProps>('FirstInlineView', 'ExpoWebView'); export default function ExpoWebViewComponent(props: ExpoWebViewProps) { return <ExpoWebView {...props} />; }

“stable” 文件在这里也与前一个例子略有不同。它现在有一个默认导出,包含一个包裹原生 FirstInlineView 视图的 ExpoWebViewComponent。不过请注意,由于这是一个默认导出,为了让这个“stable”文件正常工作,inline-module 中只能定义一个视图。

有了这些生成文件后,我们现在就可以轻松地在 TypeScript 中使用 inline modules 和 inline views 了。app/index.tsx 之前是这样的

app/index.tsx
import { requireNativeModule, requireNativeView } from 'expo'; import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; const FirstInlineModule = requireNativeModule('FirstInlineModule'); const FirstInlineView = requireNativeView('FirstInlineView'); export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {FirstInlineModule.Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });

现在我们可以去掉 requireNativeModule,并从“stable”文件中导入模块和视图。

app/index.tsx
import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; import { Hello } from './FirstInlineModule'; import FirstInlineView from './FirstInlineView'; export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {Hello} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });

监听器

要查看监听器的实际效果,请确保前面的命令仍在运行

Terminal
npx expo-type-information inline-modules-interface --app-json ./app.json --watcher

然后我们给 FirstInlineModule 添加一个新函数:

app/FirstInlineModule.swift
internal import ExpoModulesCore class FirstInlineModule: Module { public func definition() -> ModuleDefinition { Constant("Hello") { return "Hello iOS inline modules!" } Function("ConcatStrings") { (str1: String, strings: [String]) -> String in return strings.reduce(str1) { $0 + $1 } } } }

在 Swift 模块文件中添加新的 ConcatStrings 函数后,你应该会看到“generated”和“stable”文件已更新,并且现在也包含了 ConcatStrings 函数。

app/FirstInlineModule.generated.ts
/*由 expo-type-information 自动生成。*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export declare class FirstInlineModuleNativeModuleType extends NativeModule { readonly Hello: string; ConcatStrings(str1: string, strings: string[]): string; }
app/FirstInlineModule.tsx
// 文件哈希:2311de57c3a0c2135c45f49be6d2fdccd57a2b65c0ad10285c248bdf5276b7b6 import { FirstInlineModuleNativeModuleType } from './FirstInlineModule.generated'; import { requireNativeModule, requireNativeView } from 'expo'; const FirstInlineModule: FirstInlineModuleNativeModuleType = requireNativeModule<FirstInlineModuleNativeModuleType>('FirstInlineModule'); export const Hello: string = FirstInlineModule.Hello; export function ConcatStrings(str1: string, strings: string[]) { return FirstInlineModule.ConcatStrings(str1, strings); }

如果这个文件没有更新,那么你大概率是手动修改过它!如果你希望它被重新生成,需要先删除它,然后再修改 Swift 模块以触发监听器。

现在你可以在应用中使用这个新函数了

app/index.tsx
import { StyleSheet, Text, View } from 'react-native'; import * as React from 'react'; import { Hello, ConcatStrings } from './FirstInlineModule'; import FirstInlineView from './FirstInlineView'; export default function InlineModulesDemoComponent() { return ( <> <View style={styles.textBox}> <Text style={styles.text}> {Hello} </Text> <Text style={styles.text}> {ConcatStrings('Nicely ', ['typed ', 'function ', 'which ', 'concatenates ', 'strings!'])} </Text> </View> <FirstInlineView style={styles.inlineView} url="https://docs.expo.dev/modules/" /> </> ); } const styles = StyleSheet.create({ textBox: { height: 100, justifyContent: 'flex-end', alignItems: 'center' }, text: { fontSize: 26 }, inlineView: { flex: 1 }, });

以上就是关于如何为 inline modules 生成并使用 TypeScript 接口的教程。

Expo 模块接口

让我们在 原生模块教程示例 的基础上继续。在那个示例中,你创建了一个 expo-settings 模块,其中定义了一个简单的 Swift 模块。

expo-settings/ios/SettingsModule.swift
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: Theme) -> Void in UserDefaults.standard.set(theme.rawValue, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme.rawValue ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue } } enum Theme: String, Enumerable { case light case dark case system } }

你还为这个模块创建了一个 TypeScript 接口,它由以下文件组成:

  • expo-settings/src/ExpoSettings.types.ts
  • expo-settings/src/ExpoSettingsModule.ts
  • expo-settings/src/index.ts

现在我们来使用 expo-type-information CLI 自动生成这个接口,而不是手动编写它!

首先删除上面的文件,并确保你位于项目根目录。

然后运行一条命令来生成接口

Terminal
npx expo-type-information module-interface --module ./expo-settings

如果你打算修改文件,并想看看接口会如何变化,请在命令中添加 --watcher(简写 -w)标志。

Terminal
npx expo-type-information module-interface --module ./expo-settings -w

--module 选项(简写 -m)是模块根目录的路径。运行该命令后,你应该会在模块包中看到 3 个新生成的文件。

  • expo-settings/src/ExpoSettings.types.ts
  • expo-settings/src/ExpoSettingsModule.ts
  • expo-settings/src/index.ts

现在让我们看看生成了什么。

expo-settings/src/ExpoSettings.types.ts
// 文件哈希: 455b035995710b95054ffc0fa6ee888d3be158c5145e64ce4b8e0a3a92c5c510 /*由 expo-type-information 自动生成。*/ import { ViewProps } from 'react-native'; import { NativeModule } from 'expo'; export enum Theme { light, dark, system, }

*.types.ts 文件包含模块中定义的所有类型。在我们的例子里,我们只声明了一个 Theme 枚举,它已经被正确放入该文件。不过需要注意的是,与教程中的 ExpoSettings.types.ts 不同,事件类型并没有被生成。expo-type-information 工具是新的,而且很强大,但并不是每个选项都已经实现了,模块事件就是其中之一。

expo-settings/src/ExpoSettingsModule.ts
// 文件哈希: 21a1653e3cadc31ac359d32209987615e0feb20925c494864a9038013a3416b6 /*由 expo-type-information 自动生成。*/ import { requireNativeModule, NativeModule } from 'expo'; import { Theme } from './ExpoSettings.types'; export declare class ExpoSettings extends NativeModule { setTheme(theme: Theme): void; getTheme(): string; } const _default: ExpoSettings = requireNativeModule<ExpoSettings>('ExpoSettings'); export default _default;

*Module.ts 文件包含原生模块类的声明,并导出该模块实例。请注意,与前一个文件类似,这里也没有定义事件。

expo-settings/src/index.ts
/*由 expo-type-information 自动生成。*/ export type * from './ExpoSettings.types'; export { default as ExpoSettings } from './ExpoSettingsModule';

这个 index 文件与示例相比差异更大。我们没有把每个模块方法包装成单独的函数,而是选择直接重新导出模块对象。