npm package: @mifind/mf-plugin-cli
Description: RN 插件命令行工具，提供 iOS、Android 工程创建、代码和文档生成等能力
Author: luobei
Latest version: 1.2.3

## 背景

React-Native 插件平台与其说平台不如说是一套 RN 自定义模块开发的流程规范。

>  下文中若无特殊说明，**RN 插件** 即指 **RN 自定义模块**

与以往开发习惯相比，使用平台开发有以下几个优点：

- 单一职责：每个插件单独仓库管理，方便各业务方并行开发，也方便功能裁剪
- 两端对齐：使用 DSL 描述接口，天然保证 iOS 与 Android 的接口一致性
- 规范文档：通过平台构建 RN 插件自动生成接口文档，保证文档更新的及时性
- 自由裁剪：插件功能自由组装，方便定制输出

## 简介


RN 插件平台由 `管理后台` 、 `CLI & DSL`  和 `文档服务` 三部分组成，它们串联了一个 RN 插件开发的全流程

一个标准的 RN 插件开发流程如下：

```mermaid
sequenceDiagram
		participant FE as RN 开发
    participant N as 原生开发
		participant G as Gitlab
		participant C as 后台
		
		FE->>C: 创建插件
		C-->>FE: Clone 仓库到本地
		FE->>FE: 编辑 DSL 实现接口描述
		FE->>FE: 使用 CLI 生成原生接口模板代码
		FE->>G: 推送远程仓库
		G->>N: 拉取远程仓库
		N->>N: 根据接口实现业务逻辑
		N->>G: 推送远程仓库
		N->>C: 构建插件版本
		C-->>G: 插件版本更新
		N->>C: 在 API 中更新插件版本并构建
		C-->>G: API 版本更新，5.x
		FE->>FE: 基于 5.x 版本 RN 接口开发面板
```

### 管理后台

后台地址：[https://appci.tuya-inc.top:7799/rn-plugin/myPlugin](https://appci.tuya-inc.top:7799/rn-plugin/myPlugin)

后台包含 `插件` 与 `API` 两个概念

- 插件即为一个 RN 自定义模块或 UI 组件，平台提供新建、编辑、删除、构建等操作，并提供 Git 仓库跳转与文档跳转
- API 是一组插件的集合，作为一个普通 CI 组件被依赖入公版项目。平台提供 API 中的插件新增、删除、更新以及 API 本身的版本构建等操作

> 大家熟悉的 5.17、5.18 之类的 RN 接口版本，实际就是 API 的构建版本，通过 API 管理插件，可以方便的生成完整的 RN 接口文档提供给前端同学，避免手工维护出现的各种问题

![插件管理](http://lbcdn.pany.fun/20191105152406.png)

![API 管理](http://lbcdn.pany.fun/20191105152637.png)

### DSL - TypeScript

DSL 其实是 Domain Specific Language 的缩写，中文翻译为*领域特定语言*（下简称 DSL）

插件平台中，为了做到接口一致性（iOS、Android 的接口对齐），我们使用 `TypeScript` 作为嵌入式 DSL 为 RN 插件提供了一套描述规范

下面我们看一个例子，一个典型的 RN 插件如下定义：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'


/**
 * 场景专用
 */
@d.Plugin("TYRCTScenePanelManager", "1.0.3", t.PluginType.Module, ["shizq@tuya.com"])
export class RNPlugin {

    /**
    * 设置自动化开启或关闭
    * @param sceneId 自动化id
    * @param enable 是否开启
    * @param success 成功回调
    */
    @d.Method('1.0.0')
    enableScene(sceneId: string, enable: boolean, success: t.SuccessCb) { }

    /**
     * 创建自动化
     * @param body true / false
     */
    @d.Event("1.0.0")
    createAuto(body: Object) { }
}

```

一个名为 `RNPlugin` 类承担了描述 RN 接口的责任。

> DSL 中使用注解特性标注各种元信息
>
> - 对于 TypeScript 类，提供插件名、插件版本、插件类型、负责人和是否弃用等信息
> - 对于 TypeScript 方法和属性，提供 RN 方法、RN 事件、RN 控件属性和 RN 控件事件等类型约定
>
> DSL 中的类、方法和属性都必须添加注释，注释会在插件构建时自动生成文档

命令行工具 CLI 可以根据 TypeScript DSL 生成对应的 iOS、Android 接口模板代码已经接口文档，继而由客户端同学分别实现各端的业务逻辑，最终推送代码至远程，并通过后台构建版本。

### CLI - 命令行工具

有了 DSL 描述语言，当然需要配套的 CLI 工具来实现 DSL 到 iOS、Android 模板代码的转换。为了实现这些功能，我们开发了 [@tuya-native/rct-plugin-cli](https://tnpm.tuya-inc.top/package/@tuya-native/rct-plugin-cli) ，使用插件平台首先要在本地环境全局安装它

#### 安装

确认本地是否已安装 node 环境。若无，可以从 node 官网下载 Mac 安装包 [下载地址](https://nodejs.org/zh-cn/download/)。安装成功后执行以下命令会返回当前 node 版本

```shell
node --version
v8.15.0
```

在 `~/.npmrc` 中添加私有源:

```shell
@tuya-rn:registry=http://registry.npm.tuya-inc.top:7001/
@tuya-native:registry=https://registry-npm.tuya-inc.top/
```

添加淘宝镜像

```shell
npm config set registry https://registry.npm.taobao.org
```

全局安装 [@tuya-native/rct-plugin-cli](https://tnpm.tuya-inc.top/package/@tuya-native/rct-plugin-cli)  命令行工具:

```shell
sudo npm i -g @tuya-native/rct-plugin-cli
```

若执行失败，可尝试：

```shell
npm install -g --registry=https://registry-npm.tuya-inc.top/ --scope=tuya-native @tuya-native/rct-plugin-cli
```

大功告成！`rct-plugin-cli` 的使用会在使用指南中详细介绍，这里仅说明安装步骤

### 文档服务

文档服务是 RN 插件平台的重要组成部分，它承载平台生成的文档，为 RN 开发同学提供了详细的文档服务

![](http://lbcdn.pany.fun/20191109152943.png)

![](http://lbcdn.pany.fun/20191109153059.png)

## 平台接入指南

### 创建插件

在 RN 插件平台创建插件，Gitlab 仓库创建、模板代码提交等工作平台都会帮你完成

插件创建需要经历 Gitlab 仓库创建、模板代码生成等过程，所以稍等片刻，收到创建成功提示后就可以开始开发了

![](http://lbcdn.pany.fun/20191111145405.png)

### 插件声明

#### 检出 Git 仓库

在平台「我的插件」中，找到创建的插件，点击「仓库」跳转到 Gitlab 仓库，先将代码 clone 到本地

![](http://lbcdn.pany.fun/20191111155350.png)

进入本地仓库，可以看到如下目录结构:

```shell
.
├── package.json
├── plugin.ts
└── tsconfig.json
```

#### 安装 Node 依赖

现在你应该已经安装了 node 环境以及 [@tuya-native/rct-plugin-cli](https://tnpm.tuya-inc.top/package/@tuya-native/rct-plugin-cli) 命令行 CLI，如果没有请回到平台介绍中查看安装方法。

首先在项目根目录执行 `npm install` 安装依赖，完成后请使用顺手的 IDE (如 VSCode) 打开项目，此时目录结构如下:

```shell
.
├── node_modules
│   ├── @tuya-native
│   ├── @types
│   ├── reflect-metadata
│   └── typescript
├── package-lock.json
├── package.json
├── plugin.ts
└── tsconfig.json
```

现在，将我们的焦点关注在 `plugin.ts` 文件上，它即为前文提到过的 **TypeScript DSL**，在 RN 插件平台中，我们使用这个文件来描述接口。

#### DSL - 插件声明

DSL 中的插件对应 RN 中的 [原生模块](https://reactnative.cn/docs/0.51/native-modules-ios/)

下面我们以 APM 埋点模块为例介绍 **TypeScript DSL** 的写法

> **需要注意 DSL 中注释是必须的**，缺少方法说明或参数说明都会导致后续平台构建失败

项目创建初始，`plugin.ts` 的内容如下：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'

/**
 * RN 通用埋点组件
 */
@d.Plugin("TYRCTAPMEventManager", "1.0.0", t.PluginType.Module, ["weifeng@tuya.com"])
export class RNPlugin {

}
```

`class RNPlugin` 声明了一个 RN 插件，它的方法、属性通过注解语法的装饰，会分别对应到 RN 原生模块中的方法、事件、UI 属性和 UI 事件，这些下面会一一展开说明。

> 对 TypeScript 注解 (装饰器) 语法不了解的同学，可以在这里查看 [官方文档](https://www.tslang.cn/docs/handbook/decorators.html)
>
> RN 插件平台的 TypeScript DSL 中大量使用注解 (装饰器) 模式以达到 **接口版本控制**、**Owners 标注与平台权限** 和 **弃用标记** 等功能

首先，我们先把目光放在 `class RNPlugin` 顶部的这行注解上:

```typescript
@d.Plugin("TYRCTAPMEventManager", "1.0.0", t.PluginType.Module, ["weifeng@tuya.com"])
```

参数列表如下所示：

| 参数名     | 类型         | 必填 | 备注                                                         |
| ---------- | ------------ | ---- | ------------------------------------------------------------ |
| name       | string       | 是   | RN 插件名                                                    |
| version    | string       | 是   | RN 插件版本                                                  |
| type       | t.PluginType | 是   | RN 插件类型 (模块 Or UI组件) <br>t.PluginType.Module<br>t.PluginType.Component |
| owners     | Array        | 是   | 插件负责人                                                   |
| platform   | t.Platform   | 否   | 可用平台<br>t.Platform.All<br>t.Platform.iOS<br>t.Platform.Android |
| deprecated | boolean      | 否   | 是否弃用                                                     |

`name`、`version` 、`type` 和 `owners` 会在平台创建组件时根据填入的信息自动生成，之后每次构建版本这些信息也会自动更新，所以 **🙅不要尝试修改**

- platform
  - 不填默认全平台 t.Platform.All，后续构建将生成 iOS、Android 双平台接口代码
  - t.Platform.iOS，仅 iOS 平台，后续构建仅生成 iOS 接口代码
  - t.Platform.Android，仅 Android 平台，后续构建仅生成 Android 接口代码
- deprecated
  - 标记插件是否弃用，默认否。若标记弃用则生成的原生代码中改 RN 模块将被标记为废弃（标记为废弃的插件原则上不再允许做任何改动）

> 后续方法、属性、事件等声明中均会包含 `platform` 和 `deprecated` 字段，含义与默认值与这里一致

#### DSL - 方法声明

```mermaid
graph LR;
Js ==>|Call| Native
Native -.->|Callback| Js
```

RN 模块中的导出方法 (iOS `RCT_EXPORT_METHOD()`; Android `@ReactMethod`) 在 DSL 中我们使用 TS 实例方法表示，并使用 `@d.Method` 注解装饰，标明它是一个导出方法。

例如 `TYRCTAPMEventManager` 需要给 RN 导出一个埋点方法，在 `plugin.ts` 中我们可以这样写：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'

/**
 * RN打点
 */
@d.Plugin("TYRCTAPMEventManager", "1.0.1", t.PluginType.Module, ["weifeng@tuya.com"])
export class RNPlugin {
    /**
     * RN打点
     * @param eventType 打点类型
     * @param attributes 打点参数
     */
    @d.Method('1.0.0')
    event(eventType: string, attributes: object) { }
}
```

> DSL 中所有 TypeScript 实例方法均不需要写实现，对于接口描述，我们仅关心方法声明。

这样我们就为 `TYRCTAPMEventManager` 声明了一个 `event` 方法，并通过 `@d.Method` 注解标记该方法从插件的 `1.0.0` 版本开始提供。埋点方法有两个入参，分别是字符串类型的 `eventType` 和对象类型 (iOS `NSDictionary`; Android `ReadableMap`) 的 `attributes`。

是不是既清晰又简洁，完美~

DSL 方法声明`@d.Method` 注解支持一下参数:

| 参数名     | 类型       | 必填 | 备注                                                         |
| ---------- | ---------- | ---- | ------------------------------------------------------------ |
| available  | string     | 是   | 起始版本<br>必须小于等于当前插件版本                         |
| platform   | t.Platform | 否   | 可用平台<br/>t.Platform.All<br/>t.Platform.iOS<br/>t.Platform.Android |
| deprecated | boolean    | 否   | 是否弃用                               |

为了满足 iOS、Android 两端的接口一致性，DSL 方法声明时参数类型仅支持以下有限几种:

| DSL 参数类型 | js 类型  | iOS 类型     | Android 类型                                                 | 说明                                                         |
| ------------ | -------- | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Boolean      | Boolean  | BOOL         | Bool                                                         | -                                                            |
| t.Integer    | Number  | NSInteger    | Integer                                                      | -                                                            |
| t.Double     | Number   | double       | Double                                                       | -                                                            |
| t.Float      | Number   | float        | Float                                                        | -                                                            |
| String       | String   | NSString     | String                                                       | -                                                            |
| Object       | Object   | NSDictionary | ReadableMap                                                  | -                                                            |
| Array        | Array    | NSArray      | ReadableArray                                                | -                                                            |
| t.SuccessCb  | function | Block        | Callback                                                     | -                                                            |
| t.FailureCb  | function | Block        | Callback                                                     | -                                                            |
| t.PromiseCb  | Promise  | Block, Block | [Promise](https://reactnative.cn/docs/0.51/native-modules-android/#promises) | iOS：<br>一对 Block (resolve, reject) 回调<br>Android：<br>RN 提供的 [Promise](https://reactnative.cn/docs/0.51/native-modules-android/#promises) 对象<br> |

在前端 js 代码中，`event` 方法将会这样被使用:

```javascript
import { NativeModules } from 'react-native';
var TYRCTAPMEventManager = NativeModules.TYRCTAPMEventManager;
TYRCTAPMEventManager.event('ButtonClicked', {
  someKey: "a",
  anotherKey: "xxx"
});
```

#### DSL - 事件声明

```mermaid
graph RL;
Native ==>|Call| Js
```

[iOS]([https://reactnative.cn/docs/0.51/native-modules-ios/#%E7%BB%99javascript%E5%8F%91%E9%80%81%E4%BA%8B%E4%BB%B6](https://reactnative.cn/docs/0.51/native-modules-ios/#给javascript发送事件))、[Android]([https://reactnative.cn/docs/0.51/native-modules-android/#%E5%8F%91%E9%80%81%E4%BA%8B%E4%BB%B6%E5%88%B0javascript](https://reactnative.cn/docs/0.51/native-modules-android/#发送事件到javascript)) 原生向 RN 环境发送事件，在 DSL 中我们也通过 TS 实例方法表示，**但仅支持一个 `Object` 类型的参数**，同时使用 `@d.Event` 注解装饰，标明它是一个 Native -> Js 的事件

假设在 `TYRCTAPMEventManager` 埋点服务中当一次埋点上报完成需要通知 Js 端，我们可以这样声明一个事件:

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'

/**
 * RN打点
 */
@d.Plugin("TYRCTAPMEventManager", "1.0.1", t.PluginType.Module, ["weifeng@tuya.com"])
export class RNPlugin {
  
  	/**
     * APM 完成一次批量埋点上报
     * @param data 批量上报事件回执信息
     */
  	@d.Event("1.0.0")
  	batchEventUploaded(data: object) { }
}
```

`@d.Event` 注解的参数列表与 `@d.Method` 一致

在 js 代码中，该事件将会这样被使用：

```javascript
import { NativeEventEmitter, NativeModules } from 'react-native';
const { TYRCTAPMEventManager } = NativeModules;

const TYRCTAPMEventManagerEmitter = new NativeEventEmitter(TYRCTAPMEventManager);

const subscription = TYRCTAPMEventManagerEmitter.addListener(
  'batchEventUploaded',
  (data) => console.log(data)
);
```

#### DSL - UI 属性声明

> 仅 UI Component 类型的插件 (t.PluginType.Component) 支持

```mermaid
graph LR;
js[RN UI Component] ==>|set property| Native[Plugin]
Native ==>| update | NUI[Native UI]
```

在 RN 原生视图组件不满足需求时，往往需要开发自定义视图组件提供给 RN 使用，这时提供丰富的原生属性 ([iOS]([https://reactnative.cn/docs/0.51/native-component-ios/#%E5%B1%9E%E6%80%A7](https://reactnative.cn/docs/0.51/native-component-ios/#属性)); [Android]([https://reactnative.cn/docs/0.51/native-component-android/#3-%E9%80%9A%E8%BF%87-reactprop-%E6%88%96-reactpropgroup-%E6%B3%A8%E8%A7%A3%E6%9D%A5%E5%AF%BC%E5%87%BA%E5%B1%9E%E6%80%A7%E7%9A%84%E8%AE%BE%E7%BD%AE%E6%96%B9%E6%B3%95](https://reactnative.cn/docs/0.51/native-component-android/#3-通过-reactprop-或-reactpropgroup-注解来导出属性的设置方法))) 就是必须的了。

属性声明使用 TS 类中的字段表示，并使用 `@d.Prop` 注解装饰，标记其为一个 UI 属性

这里我们用 `TYRCTEncryptImageManager` 插件举例，它提供了一个可以展示加密图片的视图，含有两个属性需要设置，分别是 `encryptKey` 密钥和 `encryptPath` 加密图片地址，它们均为字符串类型。

在 `plugin.ts` 中我们可以这样写：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'


/**
 * 展示加密图片
 */
@d.Plugin("TYRCTEncryptImageManager", "1.0.0", t.PluginType.Component, ["shizq@tuya.com"])
export class RNPlugin {
    /**
    * 设置密钥
    */
    @d.Prop('1.0.0')
    encryptKey: string
    /**
     * 设置图片url
    */
    @d.Prop('1.0.0')
    encryptPath: string
}

```

`@d.Prop` 注解的参数列表与 `@d.Method` 和 `@d.Event` 一致

UI 属性类型仅支持除 `function` 之外的基本数据类型，具体列表如下：

| DSL 参数类型 | js 类型  | iOS 类型     | Android 类型                                                 | 说明                                                         |
| ------------ | -------- | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Boolean      | Boolean  | BOOL         | Bool                                                         | -                                                            |
| t.Integer    | Boolean  | NSInteger    | Integer                                                      | -                                                            |
| t.Double     | Number   | double       | Double                                                       | -                                                            |
| t.Float      | Number   | float        | Float                                                        | -                                                            |
| String       | String   | NSString     | String                                                       | -                                                            |
| Object       | Object   | NSDictionary | ReadableMap                                                  | -                                                            |
| Array        | Array    | NSArray      | ReadableArray                                                | -                                                            |

> **为了保持 iOS、Android 两端的接口一致性，DSL 中的 UI 属性类型仅支持以上 7 中基本类型**
>
> iOS RN 中默认支持 `UIColor` 、`UIImage` 等类型转换，在我们的 DSL 中并不支持，接口代码入参会传入 js 提供的原始数据，开发者可以使用 `RCTConvert` 自行转换。

有了以上两个属性的定义，在 js 代码中就可以这样使用自定义 UI 控件加载加密图片了：

```react
<TYRCTEncryptImage encryptKey={"xxxx"} encryptPath={"xx/xx/xx"} />
```

#### DSL - UI 事件声明

> 仅 UI Component 类型的插件 (t.PluginType.Component) 支持

```mermaid
graph RL;
N[Native Logic Or UI] ==>| trigger | P[Plugin]
P ==>|emit ui event| RN[RN UI Component]
```

如何让 JS 处理原生视图的用户事件，例如点击、缩放等？这就需要在 RN 插件中声明 UI 事件了 ([iOS]([https://reactnative.cn/docs/0.51/native-component-ios/#%E4%BA%8B%E4%BB%B6](https://reactnative.cn/docs/0.51/native-component-ios/#事件)); [Android]([https://reactnative.cn/docs/0.51/native-component-android/#%E4%BA%8B%E4%BB%B6](https://reactnative.cn/docs/0.51/native-component-android/#事件)))

**DSL UI 事件声明与 DSL 事件的声明方式一致，区别在于使用 `@d.UIEvent` 注解装饰**

还以 `TYRCTEncryptImageManager` 模块举例，假设我们要在加密图片加载失败时通知 RN，可以添加一个 `onLoadFailed` UI 事件：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'

/**
 * 展示加密图片
 */
@d.Plugin("TYRCTEncryptImageManager", "1.0.0", t.PluginType.Component, ["shizq@tuya.com"])
export class RNPlugin {
    /**
     * 设置密钥
     */
    @d.Prop('1.0.0')
    encryptKey: string
    /**
     * 设置图片url
     */
    @d.Prop('1.0.0')
    encryptPath: string
  
  	/**
     * 图片加载失败时的通知事件
     */
  	@d.UIEvent(t.UIEventType.Direct, '1.0.0')
  	onLoadFailed(data: object) {}
}
```

**需要注意的是，UI 事件的命名必须以 `on` 开头**

`@d.UIEvent` 注解首个参数为 UI 事件类型 `t.UIEventType` ，剩余参数与 DSL 事件注解一致

`UIEventType` 有两个可选值

- `t.UIEventType.Bubbling`  冒泡事件，会根据组件层级向上传递
- `t.UIEventType.Direct` 直接事件，仅当前视图可处理

> 定义 UI 事件类型时，与用户操作相关的建议使用冒泡事件，与视图本身逻辑相关的推荐使用直接事件

#### CLI - 生成代码&文档

完成 DSL 接口声明后，我们就可以利用 CLI 来生成 iOS、Android 两端的接口代码了，激动人心的一步终于要来了，调整好呼吸我们马上开始

保存 `plugin.ts` 文件，在命令行进入项目根目录 (确保已全局安装 [@tuya-native/rct-plugin-cli](https://tnpm.tuya-inc.top/package/@tuya-native/rct-plugin-cli)  工具)，键入以下命令：

```shell
# 根据 DSL 生成 iOS、Android 两端接口代码
rct-plugin-cli generate all

# 根据 DSL 生成 iOS 端接口代码
rct-plugin-cli generate ios

# 根据 DSL 生成 Android 端接口代码
rct-plugin-cli generate android
```

> 对于单平台的 RN 插件，可以选择仅生成一端接口代码

若命令执行失败，请检查 `plugin.ts` 中书写是否规范

命令执行成功后，项目中会多出 iOS、Android 两端源码，目录结构如下：

```shell
.
├── README.md
├── TYRCTAPMEventManager.podspec
├── android
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src
├── build.gradle
├── doc.md
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── ios
│   ├── Example
│   ├── LICENSE
│   ├── README.md
│   └── TYRCTAPMEventManager
├── node_modules
├── package-lock.json
├── package.json
├── plugin.ts
├── settings.gradle
└── tsconfig.json
```

- `doc.md` 为插件文档，根据 DSL 中的注释和注解信息自动生成
- `ios/` 目录为 iOS 项目源码目录
- `android/` 目录为 Android 项目源码目录

版本迭代时，每次更新过 `plugin.ts` 文件都要执行下 `rct-plugin-cli generate all` 命令，在 iOS、Android 目录已存在时，它仅会更新必要的接口声明文件，可以放心使用。

---

iOS、Android 接口代码生成/更新后，先提交代码至远程仓库，再同步对应的 iOS、Android 同学拉取代码，之后就可以按照新的接口变更实现具体业务逻辑了。

### 插件实现

通过 DSL 生成的仅是 iOS 与 Android 的接口声明，目的是保证 RN 接口的一致性与文档的自动生成。接口实现还是需要两端同学各自实现。

关于 iOS 与 Android 插件实现的方法参考各端接入指南

### 构建版本

确保两端 (iOS、Android) 代码实现都完成并提交远程仓库后，就可以在 RN 插件平台上尝试构建版本了。构建过程主要做以下几个事情：

1. 根据 DSL 生成两端接口，重置源码接口声明（保持接口一致性）
2. 打 tag 更新插件版本，并推送文档至文档托管服务中
3. 编译验证 iOS、Android 源码

![](http://lbcdn.pany.fun/20191113203333.png)

构建成功后稍等片刻，待文档服务同步后，就可以在平台上点击 `文档` 链接查看最新构建版本的接口文档了

这里是一份文档样例 [TYRCTAPMTrackManager (v1.0.4)](http://172.16.246.13:3232/#/rndocs/plugins/TYRCTAPMTrackManager/1.0.4/)

![](http://lbcdn.pany.fun/20191113211455.png)

### 更新 API

RN 插件构建并测试好后，最终还是要加入公版项目中使用的。如何管理有效的组织管理公版所依赖的 RN 插件并作为整体输出递增的 RN 接口版本，这就是 API 的意义所在了

API 就像一个容器，它可以添加、删除、更新所包含的插件，通过 API 的构建我们可以将一组插件整体打出版本，并将这个 API 版本输出至 RN 环境，作为面板 JS 判断可用性的接口版本依据

![](http://lbcdn.pany.fun/20191113210943.png)

![](http://lbcdn.pany.fun/20191113211035.png)

## iOS 接入指南

### 项目结构

#### 组件依赖

RN 插件项目默认依赖两个组件：

```ruby
s.dependency 'TYReactNative'
s.dependency 'TYPanelContext'
```

> 业务方可根据需要增加组件依赖

`TYReactNative` 组件是 RN 源码。别问，依赖就好了 🙂

`TYPanelContext` 是面板上下文组件，它提供当前 RN 面板页面与设备的必要信息，帮助我们开发插件。具体内容见头文件：

```objective-c
@interface TYRCTPanelContext : NSObject

@property(nonatomic, strong) NSString *gwId;
@property(nonatomic, strong) NSString *devId;
@property(nonatomic, strong) NSString *groupId;
@property(nonatomic, assign) BOOL isGroup;
@property(nonatomic, assign) BOOL isVDevice;
@property(nonatomic, assign) BOOL isMeshDevice;
@property(nonatomic, assign) BOOL isInHybrid;
@property(nonatomic, assign) BOOL isActiveDelete; // 用户主动删除设备
@property(nonatomic, assign) BOOL isSupportAutorotate;
@property(nonatomic, strong) NSDictionary *extraInfo; // 外部调用传入的自定义上下文参数 'contextProps'
@property(nonatomic, strong) NSDictionary *extraInitialProps; // 外部调用传入的自定义初始化参数 'initialProps'
@property(nonatomic, weak) RCTRootView *rootView;
@property(nonatomic, weak) __kindof UIViewController *panelVc;

@end
```

我们通过 `RCTBridge+TYPanelContext.h` 为 `RCTBridge` 增加了 `ty_panelContext` 属性，通过它我们可以在接口实现时方便的获取到当前面板的设备上下文信息。

```objective-c
@interface RCTBridge (TYPanelContext)
@property (nonatomic, strong) TYRCTPanelContext *ty_panelContext;
@end
```

> `RCTBridge` 是 React-Native 的核心类，它是沟通 JS 与原生环境的桥梁
>
> 每个 RN 插件 (即 React-Native Module) 都有一个弱引用属性 `bridge` 以获取持有此插件的 `RCTBridge` 实例

#### 自动生成的接口文件

上一章提到使用 `rct-plugin-cli generate all` 命令生成了 iOS、Android 两端的接口声明文件

这里我们以插件 `TYRCTReviewManager` 为例，它的 `plugin.ts` DSL 文件内容如下：

```typescript
import * as d from '@tuya-native/rct-plugin-meta/lib/decorators'
import * as t from '@tuya-native/rct-plugin-meta/lib/types'

/**
 * RN 审核模板支持模块，提供审核模板与原生的接口通讯
 */
@d.Plugin("TYRCTReviewManager", "1.0.8", t.PluginType.Module, ["weifeng@tuya.com","misaka@tuya.com"], t.Platforms.iOS)
export class RNPlugin {

    /**
     * JS -> Native，RN 调用原生接口
     * @param action 调用消息
     * {
     *  target: "xxx",  //目标模块
     *  action: "xxx",  //目标操作
     *  payload: {}     //参数，由具体模块与操作定义
     * }
     * @param callback promise 类型回调
     */
    @d.Method("1.0.0")
    sendAction(action: object, callback: t.PromiseCb) {}

    /**
     * Native -> JS，RN 接收原生事件
     * @param event 事件消息
     * {
     *  target: "xxx",  //来源模块
     *  event: "xxx",   //来源事件
     *  payload: {}     //数据内容，由具体某块与事件定义
     * }
     */
    @d.Event("1.0.0")
    receiveEvent(event: object) {}
}

```

经过 CLI 处理，生成的 iOS 目录的文件结构是这样的：

```shell
.
├── Example
├── LICENSE
├── README.md
└── TYRCTReviewManager
    ├── Assets
    └── Classes
        ├── TYRCTReviewManager+Ext.h
        ├── TYRCTReviewManager+Impl.h (locked)
        ├── TYRCTReviewManager+Impl.m
        ├── TYRCTReviewManager.h (locked)
        └── TYRCTReviewManager.m (locked)
```

CLI 工具帮我们生成的只有一个类，与插件同名的 `TYRCTReviewManager` ，它以类、类 Category 和 类 Extension 的形式提供出来

TYRCTReviewManager.h/m 和 TYRCTReviewManager+Impl.h 三个文件每次通过 DSL 生成原生接口代码时都会被替换，**不要🙅在里面开发业务代码**

### 接口实现

`TYRCTReviewManager` 是一个实现 [RCTBridgeModule](https://github.com/facebook/react-native/blob/v0.51.0/React/Base/RCTBridgeModule.h) 协议的类

`.h/m` `锁定` 按照 DSL 生成了与 RN 的桥接代码，但并不实现具体逻辑，而是转发到 Category 分类中的实现方法

`+Impl.h` `锁定` 按照 DSL 声明了 Category 分类中需要实现的转发方法

`+Impl.m`  `可编辑` 由开发者实现头文件中声明的转发方法、实现业务逻辑

`+Ext.h` `可编辑` 是一个类扩展 (Extension)，由于类文件被锁定，声明属性和方法在这里进行

#### 关于 PanelContext 上下文

上文提到，我们通过为 ` ty_panelContext` 属性为 `RCTBridge` 增加了获取当前面板上下文信息的能力，但是它并不是一开始就设置上去的，所以在插件初始化 `init` 方法执行时并不能获取到它。

为了解决这个问题，我们提供了 `TYRCTPanelContextModule` 协议:

```objective-c
@protocol TYRCTPanelContextModule <NSObject>

/// 与面板相关的上下文信息已设置 (self.bridge.ty_panelContext)
/// 注意：对于延迟加载的模块，改方法可能不在主线程执行
- (void)panelContextDidSetup;

@end
```

在 `+Ext.h` 中，我们让插件实现该协议，并在 `+Impl.m` 中实现该协议方法 `panelContextDidSetup`
插件系统会在上下文准备好时调用该方法，你只需要在该方法中获取 `self.bridge.ty_panelContext` 来初始化你的相关属性即可


#### Module 方法

DSL 声明了一个 `sendAction` 方法

```typescript
/**
 * JS -> Native，RN 调用原生接口
 * @param action 调用消息
 * {
 *  target: "xxx",  //目标模块
 *  action: "xxx",  //目标操作
 *  payload: {}     //参数，由具体模块与操作定义
 * }
 * @param callback promise 类型回调
 */
@d.Method("1.0.0")
sendAction(action: object, callback: t.PromiseCb) {}
```

iOS 实现方法会在 `+Impl.h` 中声明 (`TYRCTReviewManager+Impl.h`)：

```objective-c
/**
JS -> Native，RN 调用原生接口

@available 1.0.0
@param action 调用消息
{
target: "xxx",  //目标模块
action: "xxx",  //目标操作
payload: {}     //参数，由具体模块与操作定义
}
@param callback promise 类型回调
*/
- (void)impl_sendAction:(NSDictionary*)action resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject ;
```

> DSL 中的 PromiseCb 类型在 iOS 中对应两个 Block 回调
>
> resolve 为成功回调；reject 为失败回调

在 `+Impl.m` 中实现该方法，加入自己的业务逻辑，一个 RN 插件方法就开发完成了 

#### Module 事件

DSL 中有一个 `receiveEvent` 事件

```typescript
/**
 * Native -> JS，RN 接收原生事件
 * @param event 事件消息
 * {
 *  target: "xxx",  //来源模块
 *  event: "xxx",   //来源事件
 *  payload: {}     //数据内容，由具体某块与事件定义
 * }
 */
@d.Event("1.0.0")
receiveEvent(event: object) {}
```

iOS 中向 RN 发送事件的方法生命在 `TYRCTReviewManager.h` 中

```objective-c
/**
Native -> JS，RN 接收原生事件

@available 1.0.0
@param event 事件消息
{
target: "xxx",  //来源模块
event: "xxx",   //来源事件
payload: {}     //数据内容，由具体某块与事件定义
}
*/
- (void)sendEvent_receiveEvent:(NSDictionary *)body;
```

需要向 RN 发送事件时，调用方法并传入数据参数即可

#### UI Component 属性

DSL 中 UI 属性会像这样声明

```typescript
/**
 * 图片填充模式，整型枚举:
 * UIViewContentModeScaleToFill: 0 (拉伸填充，默认)
 * UIViewContentModeScaleAspectFit: 1 (类似 contain)
 * UIViewContentModeScaleAspectFill: 2 (类似 cover，但上下左右会均匀裁剪)
 */
@d.Prop("1.0.0")
contentMode: t.Integer = 0
```

在 iOS 中，`+Impl.h` 文件会声明业务方需要实现的属性设置方法：

```objective-c
/**
图片填充模式，整型枚举:
UIViewContentModeScaleToFill: 0 (拉伸填充，默认)
UIViewContentModeScaleAspectFit: 1 (类似 contain)
UIViewContentModeScaleAspectFill: 2 (类似 cover，但上下左右会均匀裁剪)

@available 1.0.0
*/
- (void)impl_set_contentMode:(NSInteger)value forView:(__kindof UIView*)view withDefaultView:(__kindof UIView*)defaultView;
```

- `value` 为 RN 设置的属性值
- `view` 为当前 RN 组件对应的原生视图实例，属性值就要设置到该 view 上
- `defaultView` 是方便开发者获取视图属性默认值的实例

#### UI Component 事件

UI 事件的开发方式与 UI 属性类似，不同的是当 DSL 中声明了 UI 事件时，模块头文件中会多出一个 Protocol 协议，这个协议中声明了与事件对应的 Block 类型属性

以 `TYRCTNewTopBarManager` 举例，DSL 中添加了冒泡事件 `onChange`

```typescript
/**
 * 更新UI
 * @param param 参数
 */
@d.UIEvent(t.UIEventType.Bubbling, '1.0.0')
onChange(param: object) {}
```

在 `TYRCTNewTopBarManager.h` 中将会多出一个 `TYRCTNewTopBarManagerViewProtocol` 协议：

```objective-c
@protocol TYRCTNewTopBarManagerViewProtocol <NSObject>
/**
更新UI

@available 1.0.0
*/
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@end
```

你所创建的原生视图需要符合该协议，并合成 `onChange` 属性

当 JS 端设置了该事件回调函数时，RN 会向该属性设置对应的 Block 回调。在恰当时机执行该 Block 并传入数据，JS 端的回调函数就会触发，达到事件通知的目的。

## Android 接入指南

### Modlue 
以`TYRCTAPMEventManager`为例
使用 rct-plugin-cli 生成目录后,会在`android/src`下生成

```java
ITYRCTAPMEventManagerSpec.java //对应plugin.ts 生成的接口,不能改动
TYRCTAPMEventManager.java //对应 ITYRCTAPMEventManagerSpec 的实现类,此处写具体业务逻辑
TYRCTAPMEventManagerPackage.java //对应的 ReactPackage,一般情况下不用改动
```

`ITYRCTAPMEventManagerSpec`每次会在 CI 端重新生成,所以不可以手动改动,涉及签名、参数及注释改动必须修改`plugin.ts`文件,然后使用`rct-plugin-cli generate <对应的平台>`命令重新生成接口

```java
/**
 * @version: 1.0.0
 * @owner: shizq@tuya.com yueshi@tuya.com weifeng@tuya.com
 * @platform: all
 * RN打点
 */
public class TYRCTAPMEventManager extends ReactContextBaseJavaModule implements ITYRCTAPMEventManagerSpec {

    private StatService mStatService;

    /**
     * RN打点
     *
     * @param eventType  打点类型
     * @param attributes 打点参数
     * @available: 1.0.0
     * @platform: all
     */

    @Override
    @ReactMethod
    public void event(String eventType, ReadableMap attributes) {
        if (mStatService != null) {
            mStatService.eventObjectMap(eventType, parseToMap(attributes));
        }
    }


    @Override
    public String getName() {
        return "TYRCTAPMEventManager";
    }

    public TYRCTAPMEventManager(ReactApplicationContext reactContext) {
        super(reactContext);
        mStatService = MicroContext.getServiceManager().findServiceByInterface(StatService.class.getName());
    }
```

`TYRCTAPMEventManager`中的`getName`方法返回值不可更改,`@ReactMethod`方法注解不可删除,否则ReactNative调用不生效.

对于需要`ReadableMap 转 HashMap`或者`ReadableArray 转 ArrayList`等数据类型转化需求,项目可以依赖`implementation 'com.tuya.smart:tuyasmart-react-natvie-utils:lastVersion'`模块.

### Component

以`TYRCTEncryptImageManager`为例
使用 rct-plugin-cli 生成目录后,会在`android/src`下生成

```java
ITYRCTEncryptImageManagerSpec.java //对应plugin.ts 生成的接口,不能改动
TYRCTEncryptImageManager.java //对应 ITYRCTEncryptImageManagerSpec 的实现类,此处写具体业务逻辑
TYRCTEncryptImageManagerPackage.java //对应的 ReactPackage,一般情况下不用改动
```

`ITYRCTEncryptImageManagerSpec`每次会在 CI 端重新生成,所以不可以手动改动,涉及签名、参数及注释改动必须修改`plugin.ts`文件,然后使用`rct-plugin-cli generate <对应的平台>`命令重新生成接口

``` java
/**
 * @version: 1.0.0
 * @owner: shizq@tuya.com yueshi@tuya.com weifeng@tuya.com
 * @platform: android
 * 加密图片
 */

public class TYRCTEncryptImageManager extends SimpleViewManager<DecryptImageView> implements ITYRCTEncryptImageManagerSpec<DecryptImageView> { //替换对应的泛形T

    private String path;
    private String key;

    /**
     * 设置密钥
     *
     * @available: 1.0.0
     * @platform: android
     */

    @Override
    @ReactProp(name = "encryptKey")
    public void setEncryptKey(DecryptImageView view, String encryptKey) {
        this.key = encryptKey;
        setImageUri(view);
    }

    /**
     * 设置图片url
     *
     * @available: 1.0.0
     * @platform: android
     */

    @Override
    @ReactProp(name = "encryptPath")
    public void setEncryptPath(DecryptImageView view, String encryptPath) {
        this.path = path;
        setImageUri(view);
    }

    private void setImageUri(DecryptImageView imageView) {
        if (!TextUtils.isEmpty(path) && !TextUtils.isEmpty(key)) {
            imageView.setImageURI(path, key.getBytes());
        } else {
            imageView.setImageURI(path);
        }
    }

    @Override
    public String getName() {
        return "TYRCTEncryptImageManager";
    }

    @Override
    protected DecryptImageView createViewInstance(ThemedReactContext reactContext) {
        return new DecryptImageView(reactContext);
    }

}  
```

`TYRCTEncryptImageManager`类中将`T`替换为自己所需的`View`,此例子中`DecryptImageView`为根`View`.在`createViewInstance`方法中返回此`View`的实例.`getName`方法的返回值不可能更改.`@ReactProp(name = "encryptPath")`注解不可修改,对于方法签名和注解内容的改动都必须通过对`plugin.ts`文件修改,然后使用`rct-plugin-cli generate <对应的平台>`命令重新生成接口,删除实现类里原来的旧方法.

## 技术细节

### TypeScript Compiler API

[链接](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)

### TypeScript Decorators

[链接](https://www.tslang.cn/docs/handbook/decorators.html)

### EJS 模板

[链接](https://ejs.bootcss.com/)