基于 react-native-contacts 的 OpenHarmony 适配版,读写设备通讯录联系人
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 年前 | ||
| 7 年前 | ||
| 9 年前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 3 个月前 | ||
| 11 个月前 | ||
| 1 个月前 |
模板版本:v0.4.2
react-native-contacts
本项目基于 react-native-contacts 开发。
该第三方库的仓库已迁移至 Gitcode,并支持直接从 npm 下载。新的包名为:@react-native-ohos/react-native-contacts。版本对应关系如下:
| 第三方库名称 | 第三方库版本(npm 地址) | 发布信息 | 支持的 RN 版本 | Autolink | 编译 API 版本 | 社区基线版本 | 源码地址 |
|---|---|---|---|---|---|---|---|
| @react-native-ohos/react-native-contacts | ~8.1.0(开发中) | Gitcode 发布 | 0.82.* / 0.84.* | 是 | API12+ | 8.0.10 | master |
| @react-native-ohos/react-native-contacts | ~8.0.7 | Gitcode 发布 | 0.77.* | 否 | API12+ | 8.0.6 | br_rnoh0.77 |
| @react-native-ohos/react-native-contacts | ~7.0.8 | Gitcode 发布 | 0.72.* | 是 | API12+ | 7.0.7 | br_rnoh0.72 |
| @react-native-oh-tpl/react-native-contacts | <= 7.0.7-0.0.3@deprecated | Github 发布 | 0.72.* | 否 | API12+ | 7.0.7 | sig |
简介
react-native-contacts 是面向 React Native 的联系人管理库,提供设备通讯录的读取与写入能力。
下载安装
进入工程目录,并执行以下命令:
npm
npm install @react-native-ohos/react-native-contacts
yarn
yarn add @react-native-ohos/react-native-contacts
快速上手:
使用时,import 的库名保持不变。
import React, { useState } from "react";
import { ScrollView, Button, Alert } from "react-native";
import Contacts from "react-native-contacts";
export const ContactsDemo = () => {
let emailAddress: Contacts.EmailAddress = {
label: "emailAddress",
email: "test@163.com",
};
let phoneNumber: Contacts.PhoneNumber = {
label: "phoneNumber",
number: "13142536789",
};
let postalAddress: Contacts.PostalAddress = {
label: "label",
formattedAddress: "formattedAddress",
street: "street",
pobox: "pobox",
neighborhood: "neighborhood",
city: "city",
region: "region",
state: "state",
postCode: "postCode",
country: "country",
};
let birthday: Contacts.Birthday = {
day: 1,
month: 5,
year: 2024,
};
let instantMessageAddress: Contacts.InstantMessageAddress = {
username: "username",
service: "service",
};
let urlAddress: Contacts.UrlAddress = {
url: "url",
label: "label",
};
let contact: Contacts.Contact = {
company: "addcompany",
emailAddresses: [emailAddress],
displayName: "adddisplayName",
familyName: "addfamilyName",
givenName: "addgivenName",
middleName: "addmiddleName",
jobTitle: "addjobTitle",
phoneNumbers: [phoneNumber],
hasThumbnail: false,
thumbnailPath: "addthumbnailPath",
isStarred: false,
postalAddresses: [postalAddress],
prefix: "addprefix",
suffix: "addsuffix",
department: "adddepartment",
birthday: birthday,
imAddresses: [instantMessageAddress],
urlAddresses: [urlAddress],
note: "addnote",
};
return (
<ScrollView>
<Button
title="requestPermission"
onPress={() => {
Contacts.requestPermission().then((data) => {
console.log(`requestPermission:${JSON.stringify(data)}`);
Alert.alert(`requestPermission:${JSON.stringify(data)}`);
});
}}
/>
<Button
title="checkPermission"
onPress={() => {
Contacts.checkPermission().then((data) => {
console.log(`checkPermission:${JSON.stringify(data)}`);
Alert.alert(`checkPermission:${JSON.stringify(data)}`);
});
}}
/>
<Button
title="getAll"
onPress={() => {
Contacts.getAll().then((contacts: Contacts.Contact[]) => {
console.log(`getAll:${JSON.stringify(contacts)}`);
});
}}
/>
<Button
title="getAllWithoutPhotos"
onPress={() => {
Contacts.getAllWithoutPhotos().then(
(contacts: Contacts.Contact[]) => {
console.log(`getAllWithoutPhotos:${JSON.stringify(contacts)}`);
}
);
}}
/>
<Button
title="getContactById"
onPress={() => {
Contacts.getContactById("1").then(
(contact: Contacts.Contact | null) => {
console.log(`getContactById:${JSON.stringify(contact)}`);
}
);
}}
/>
<Button
title="getCount"
onPress={() => {
Contacts.getCount().then((count: number) => {
console.log(`getCount:${count}`);
});
}}
/>
<Button
title="getPhotoForId"
onPress={() => {
Contacts.getPhotoForId("1").then((photoUrl: string) => {
console.log(`getPhotoForId:${photoUrl}`);
});
}}
/>
<Button
title="addContact"
onPress={() => {
Contacts.addContact(contact).then((contact: Contacts.Contact) => {
console.log(`addContact:${JSON.stringify(contact)}`);
});
}}
/>
<Button
title="openContactForm"
onPress={() => {
Contacts.openContactForm(contact).then(
(contact: Contacts.Contact) => {
console.log(`openContactForm:${JSON.stringify(contact)}`);
Alert.alert(`openContactForm success`);
}
);
}}
/>
<Button
title="openExistingContact"
onPress={() => {
Contacts.openExistingContact({
recordID: "1",
phoneNumbers: [
{
label: "phoneNumber2",
number: "13521456721",
},
],
}).then((contact: Contacts.Contact) => {
console.log(`openExistingContact:${JSON.stringify(contact)}`);
Alert.alert(`openExistingContact success`);
});
}}
/>
<Button
title="viewExistingContact"
onPress={() => {
Contacts.viewExistingContact({
recordID: "1",
phoneNumbers: [
{
label: "phoneNumber2",
number: "13521456721",
},
],
}).then((contact: Contacts.Contact) => {
console.log(`viewExistingContact:${JSON.stringify(contact)}`);
Alert.alert(`viewExistingContact success`);
});
}}
/>
<Button
title="editExistingContact"
onPress={() => {
Contacts.editExistingContact({
recordID: "1",
phoneNumbers: [
{
label: "phoneNumber2",
number: "13521456721",
},
],
}).then((contact: Contacts.Contact) => {
console.log(`editExistingContact:${JSON.stringify(contact)}`);
Alert.alert(`editExistingContact success`);
});
}}
/>
<Button
title="updateContact"
onPress={() => {
Contacts.updateContact({
recordID: "1",
familyName: "updateContact",
givenName: "updateContact",
phoneNumbers: [
{
label: "phoneNumber2",
number: "13521456721",
},
{
label: "phoneNumber3",
number: "13521456222",
},
],
}).then(() => {
Alert.alert(`updateContact success`);
});
}}
/>
<Button
title="deleteContact"
onPress={() => {
Contacts.deleteContact({
recordID: "3",
}).then(() => {
Alert.alert(`deleteContact success`);
});
}}
/>
<Button
title="getContactsMatchingString"
onPress={() => {
Contacts.getContactsMatchingString("addfamilyName").then(
(contacts: Contacts.Contact[]) => {
console.log(
`getContactsMatchingString:${JSON.stringify(contacts)}`
);
}
);
}}
/>
<Button
title="getContactsByPhoneNumber"
onPress={() => {
Contacts.getContactsByPhoneNumber("789").then(
(contacts: Contacts.Contact[]) => {
console.log(
`getContactsByPhoneNumber:${JSON.stringify(contacts)}`
);
}
);
}}
/>
<Button
title="getContactsByEmailAddress"
onPress={() => {
Contacts.getContactsByEmailAddress("test@163.com").then(
(contacts: Contacts.Contact[]) => {
console.log(
`getContactsByEmailAddress:${JSON.stringify(contacts)}`
);
}
);
}}
/>
<Button
title="writePhotoToPath"
onPress={() => {
Contacts.writePhotoToPath("1", "file").then((data) => {
console.log(`writePhotoToPath:${JSON.stringify(data)}`);
});
}}
/>
<Button
title="iosEnableNotesUsage"
onPress={() => {
Contacts.iosEnableNotesUsage(true);
console.log(`iosEnableNotesUsage:true`);
}}
/>
</ScrollView>
);
};
链接
| 是否支持 autolink | RN 框架版本 | |
|---|---|---|
| ~8.1.0 | 是 | 0.82/0.84 |
使用 AutoLink 的工程请根据该文档进行配置,Autolink 框架指导文档:https://gitcode.com/CPF-RN/ohos_react_native/blob/master/docs/zh-cn/Autolinking.md
如果您使用的版本支持 Autolink,并且工程已经接入 Autolink,则可以跳过 ManualLink 配置。
ManualLink:此步骤用于指导手动配置原生依赖项
首先,请使用 DevEco Studio 打开项目中的 HarmonyOS 工程 harmony
1. 在工程根目录的 oh-package.json5 中添加 overrides 字段
{
...
"overrides": {
"@rnoh/react-native-openharmony" : "./react_native_openharmony"
}
}
2.引入原生端代码
目前有两种引入方式:
- 通过 har 包引入(待 IDE 完善相关功能后,该方式将被弃用,目前优先采用此方式);
- 直接引入源码。
方法一:通过 har 包引入
har 包位于三方库安装路径的 `harmony` 文件夹下。
打开 entry/oh-package.json5,添加以下依赖
"dependencies": {
"@rnoh/react-native-openharmony" : "file:../react_native_openharmony",
"@react-native-ohos/react-native-contacts": "file:../../node_modules/@react-native-ohos/react-native-contacts/harmony/contacts.har"
}
点击右上角的 sync 按钮
或者在终端执行:
cd entry
ohpm install
方法二:直接链接源码
如需使用直接链接源码,请参考[直接链接源码说明](https://gitcode.com/CPF-RN/usage-docs/blob/master/zh-cn/link-source-code.md)
3.在 ArkTs 侧引入 ContactsPackage
打开 entry/src/main/ets/RNPackagesFactory.ts,添加:
...
+ import {ContactsPackage} from '@react-native-ohos/react-native-contacts/ts';
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
return [new SamplePackage(ctx),
+ new ContactsPackage(ctx)];
}
4.配置 CMakeLists 和引入 ContactsPackage
若使用的是 <= 7.0.7-0.0.3 版本,请跳过本章。
打开 entry/src/main/cpp/CMakeLists.txt,添加:
project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")
set(LOG_VERBOSITY_LEVEL 1)
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use
add_compile_definitions(WITH_HITRACE_SYSTRACE)
add_subdirectory("${RNOH_CPP_DIR}" ./rn)
# RNOH_BEGIN: manual_package_linking_1
add_subdirectory("../../../../sample_package/src/main/cpp" ./sample-package)
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-contacts/src/main/cpp" ./
contacts)
# RNOH_END: manual_package_linking_1
file(GLOB GENERATED_CPP_FILES "./generated/*.cpp")
add_library(rnoh_app SHARED
${GENERATED_CPP_FILES}
"./PackageProvider.cpp"
"${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)
target_link_libraries(rnoh_app PUBLIC rnoh)
# RNOH_BEGIN: manual_package_linking_2
target_link_libraries(rnoh_app PUBLIC rnoh_sample_package)
+ target_link_libraries(rnoh_app PUBLIC rnoh_contacts)
# RNOH_END: manual_package_linking_2
打开 entry/src/main/cpp/PackageProvider.cpp,并添加:
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
#include "SamplePackage.h"
+ #include "ContactsPackage.h"
using namespace rnoh;
std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
return {
std::make_shared<RNOHGeneratedPackage>(ctx),
std::make_shared<SamplePackage>(ctx),
+ std::make_shared<ContactsPackage>(ctx)
};
}
运行
点击右上角的 sync 按钮
或在终端执行:
cd entry
ohpm install
随后编译并运行即可。
约束与限制
兼容性
本文档内容已在以下版本中验证通过:
- RNOH: 0.82.1; SDK: HarmonyOS API Version 21 Release SDK; IDE: DevEco Studio 6.0.1.260; ROM: 6.0.0.130;
- RNOH: 0.84.2; SDK: HarmonyOS 6.0.1 Release SDK; IDE: DevEco Studio 26.0.0 Beta1; ROM: 6.0.0.120 SP7;
权限要求
[!TIP] "ohos.permission.READ_CONTACTS"、"ohos.permission.WRITE_CONTACTS" 的权限等级为system_basic,授权方式为user_grant,使用 ACL 签名的配置指导
打开entry/src/main/module.json5,添加:
"requestPermissions": [
...
{
"name": "ohos.permission.READ_CONTACTS",
"reason": "$string:read_contacts_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_CONTACTS",
"reason": "$string:write_contacts_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
}
]
API
“平台”列表示该属性在原始第三方库中支持的平台。
“HarmonyOS 支持”列为“是”表示 HarmonyOS 平台支持该属性;“否”表示不支持;“部分支持”表示部分支持。跨平台调用方式一致,效果对标 iOS 或 Android 平台的效果。
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| getAll: Promise<Contact[]> | 以对象数组形式返回所有联系人 | function | 否 | Android,iOS | 是 |
| getAllWithoutPhotos:Promise<Contact[]> | 与 getAll 相同,但 iOS 平台不返回联系人照片的 URI,因为生成图片会造成明显性能开销 |
function | 否 | Android,iOS | 是 |
| getContactById(contactId): Promise | 返回指定 contactId 对应的联系人;若不存在,则返回 null |
function | 是 | Android,iOS | 是 |
| getCount(): Promise | 返回联系人总数 | function | 否 | Android,iOS | 是 |
| getPhotoForId(contactId: string): Promise | 返回指定联系人的照片 URI;若不存在,则返回 null |
function | 是 | Android,iOS | 是 |
| addContact(contact: Partial): Promise | 向通讯录添加一个联系人 | function | 是 | Android,iOS | 是 |
| openContactForm(contact: Partial): Promise<Contact | null> | 创建新联系人,并在系统联系人界面中显示 | function | 是 | Android,iOS | 部分支持 |
| openExistingContact(contact: Contact): Promise | 以编辑模式打开已有联系人,contact 为包含有效 recordID 的对象 |
function | 是 | Android,iOS | 部分支持 |
| viewExistingContact(contact: { recordID: string }) | 以查看模式打开已有联系人,contact 为包含有效 recordID 的对象 |
function | 是 | Android,iOS | 部分支持 |
| editExistingContact(contact: Contact): Promise | 为联系人新增电话号码,contact 为包含有效 recordID 和 phoneNumbers 数组的对象 |
function | 是 | Android,iOS | 否 |
| updateContact(contact: Partial & {recordID: string}): Promise | 更新联系人,contact 为包含有效 recordID 的对象 |
function | 是 | Android,iOS | 是 |
| deleteContact(contact: Contact): Promise | 删除联系人,contact 为包含有效 recordID 的对象 |
function | 是 | Android,iOS | 是 |
| getContactsMatchingString(str: string): Promise<Contact[]> | 根据字符串匹配联系人姓名(名字、中间名、姓氏) | function | 是 | Android,iOS | 是 |
| getContactsByPhoneNumber(phoneNumber: string): Promise<Contact[]> | 根据电话号码检索联系人 | function | 是 | Android,iOS | 是 |
| getContactsByEmailAddress(emailAddress: string): Promise<Contact[]> | 根据电子邮件地址检索联系人 | function | 是 | Android,iOS | 是 |
| checkPermission(): Promise<'authorized' | 'denied' | 'undefined' | 'limited'> | 检查联系人访问权限(仅 iOS) | function | 否 | iOS | 部分支持(authorized、denied) |
| requestPermission(): Promise<'authorized' | 'denied' | 'undefined' | 'limited'> | 请求联系人访问权限(仅 iOS) | function | 否 | iOS | 部分支持(authorized、denied) |
| writePhotoToPath(contactId: string, file: string): Promise | 将联系人照片写入指定路径(仅 Android) | function | 是 | Android | 否 |
| getGroups(): Promise<Group[]> | 获取全部群组列表 | function | 否 | Android,iOS | 是 |
| getGroup(identifier: string): Promise<Group | null> | 根据标识符获取单个群组;若不存在,则返回 null |
function | 是 | Android,iOS | 是 |
| deleteGroup(identifier: string): Promise | 删除指定群组 | function | 否 | Android,iOS | 否 |
| updateGroup(identifier: string, groupData: Object): Promise | 更新群组名称 | function | 否 | Android,iOS | 否 |
| addGroup(group: Object): Promise | 创建新群组,仅需提供 name 字段 |
function | 否 | Android,iOS | 否 |
| contactsInGroup(identifier: string): Promise<Contact[]> | 获取指定群组内的所有联系人 | function | 否 | Android,iOS | 否 |
| addContactsToGroup(groupIdentifier: string, contactIdentifiers: string[]): Promise | 将多个联系人添加到指定群组,contactIdentifiers 为 recordID 数组 |
function | 否 | Android,iOS | 否 |
| removeContactsFromGroup(groupIdentifier: string, contactIdentifiers: string[]): Promise | 将多个联系人从指定群组中移除 | function | 否 | Android,iOS | 否 |
| iosEnableNotesUsage(enabled: boolean): void | 启用或禁用联系人备注(notes)的读写能力(仅 iOS,需特殊授权)。 | function | 是 | iOS | 否 |
Contacts
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| recordID | 联系人标识符 | string | 否 | Android,iOS | 是 |
| backTitle | 返回键标题 | string | 否 | Android,iOS | 否 |
| company | 公司 | string | 否 | Android,iOS | 是 |
| emailAddresses | 电子邮件地址列表 | EmailAddress[] | 否 | Android,iOS | 是 |
| displayName | 显示名称 | string | 否 | Android,iOS | 是 |
| familyName | 姓氏 | string | 否 | Android,iOS | 是 |
| givenName | 名字 | string | 否 | Android,iOS | 是 |
| middleName | 中间名 | string | 否 | Android,iOS | 是 |
| jobTitle | 职位 | string | 否 | Android,iOS | 是 |
| phoneNumbers | 电话号码列表 | PhoneNumber[] | 否 | Android,iOS | 是 |
| hasThumbnail | 是否包含头像 | boolean | 否 | Android,iOS | 否 |
| thumbnailPath | 头像路径 | string | 否 | Android,iOS | 否 |
| isStarred | 是否星标 | boolean | 否 | Android,iOS | 部分支持(false) |
| postalAddresses | 邮政地址列表 | PostalAddress[] | 否 | Android,iOS | 是 |
| prefix | 前缀 | string | 否 | Android,iOS | 是 |
| suffix | 后缀 | string | 否 | Android,iOS | 是 |
| department | 部门 | string | 否 | Android,iOS | 是 |
| birthday | 生日 | Birthday | 否 | Android,iOS | 是 |
| imAddresses | 即时消息地址列表 | InstantMessageAddress[] | 否 | Android,iOS | 是 |
| urlAddresses | 网址地址列表 | UrlAddress[] | 否 | Android,iOS | 否 |
| note | 备注 | string | 否 | Android,iOS | 是 |
EmailAddress
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| label | 标签 | string | 否 | Android,iOS | 是 |
| 地址 | string | 否 | Android,iOS | 是 |
PhoneNumber
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| label | 标签 | string | 否 | Android,iOS | 是 |
| number | 号码 | string | 否 | Android,iOS | 是 |
PostalAddress
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| label | 标签 | string | 否 | Android,iOS | 是 |
| formattedAddress | 格式化地址 | string | 否 | Android,iOS | 是 |
| street | 街道 | string | 否 | Android,iOS | 是 |
| pobox | 信箱 | string | 否 | Android,iOS | 是 |
| neighborhood | 街区 | string | 否 | Android,iOS | 是 |
| city | 城市 | string | 否 | Android,iOS | 是 |
| region | 区域 | string | 否 | Android,iOS | 是 |
| state | 州 | string | 否 | Android,iOS | 是 |
| postCode | 邮政编码 | string | 否 | Android,iOS | 是 |
| country | 国家/地区 | string | 否 | Android,iOS | 是 |
InstantMessageAddress
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| username | 用户名 | string | 否 | Android,iOS | 是 |
| service | 服务 | string | 否 | Android,iOS | 是 |
Birthday
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| day | 日 | number | 否 | Android,iOS | 是 |
| month | 月 | number | 否 | Android,iOS | 是 |
| year | 年 | number | 否 | Android,iOS | 是 |
UrlAddress
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| url | 网址 | string | 否 | Android,iOS | 是 |
| label | 标签 | string | 否 | Android,iOS | 是 |
Group
| 名称 | 描述 | 类型 | 是否必填 | 平台 | HarmonyOS 支持 |
|---|---|---|---|---|---|
| identifier | 群组唯一标识符 | string | 否 | Android,iOS | 是 |
| name | 群组名称 | string | 否 | Android,iOS | 是 |
遗留问题
其他
目录结构
/rntpc_react-native-contacts # 项目根目录
├── harmony # 鸿蒙适配代码
│ ├── contacts.har # har包
│ └── contacts # 鸿蒙适配核心代码
│ ├── Index.ets # 鸿蒙适配代码入口
│ └── src/main
│ ├── ets
│ │ ├── ContactsPackage.ets # 鸿蒙侧Package注册
│ │ └── tm # TurboModule实现
│ │ └── ContactsTurboModule.ts # 联系人TurboModule
│ └── cpp # C++ TurboModule 桥接层
│ ├── ContactsPackage.h # C++ 侧Package注册
│ └── generated/RNOH # 自动生成RNOH绑定代码
├── src # RN代码
│ └── NativeContacts.ts # 原生模块类型定义
├── index.js # JS入口文件
├── index.d.ts # TypeScript类型声明入口
├── README.md # 中文安装使用方法
└── README_en.md # 英文安装使用方法
贡献代码
使用过程中发现任何问题,都可以提交 Issue,当然,也非常欢迎提交 PR。
开源协议
本项目基于 The MIT License (MIT),欢迎自由使用并参与开源。