什么是 Node.js
JavaScript
诞生于 1995 年,几乎是和互联网同时出现;Node.js
诞生于 2009 年,比 JavaScript
晚了 15 年左右。
Node.js
不是一门新的编程语言,也不是一个 JavaScript
框架,它是一套 JavaScript
运行环境,用来支持 JavaScript
代码的执行。用编程术语来讲,Node.js
是一个 JavaScript
运行时(Runtime,可以类比于Java的JRE,Java Runtime Environment)。
在 Node.js
之前,JavaScript
只能运行在浏览器中,作为网页脚本使用,为网页添加一些特效,或者和服务器进行通信。有了 Node.js
以后,JavaScript 就可以脱离浏览器,像其它编程语言一样直接在计算机上使用,再也不受浏览器的限制了。换句话说,Node.js 几乎完全抛弃了浏览器,自己从头构建了一套全新的 JavaScript
运行时。
Node.js
运行时主要由 V8 引擎 、标准库 和本地模块组成。
- v8引擎:是
JavaScript
解释器,它负责解析和执行JavaScript
代码(类比于Java的JVM)。 - 标准库:提供了一套优雅的
JavaScript
接口给开发人员,并且要保持接口在不同平台(操作系统)上的一致性。 - 本地模块:Node.js 集成了众多高性能的开源库,它们使用 C/C++ 语言实现。比如
libuv
、nmp
和OpenSSL
等
注:谷歌公司在 Chrome 浏览器中集成了一种名为"V8"的
JavaScript
引擎(也即JavaScript
解释器),它能够非常快速地解析和执行JavaScript
代码。V8 引擎使用 C++ 语言编写,可以独立运行,也可以嵌入到任何其它 C++ 程序中。谷歌公司将 V8 引擎甚至整个 Chrome 浏览器都开源了,任何人都可以免费地将 V8 应用到自己的项目中。
什么是 TypeScript
TypeScript
,简称TS,是指添加了类型系统的 JavaScript。TS一种由微软开发的自由和开源的编程语言,它是 JavaScript
的一个超集,扩展了 JavaScript
的语法,是一种面向对象的语言,更适合开发大型复杂应用程序。
TS代码经过编译,最终会产物是.js
代码。TS 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错 。而在运行时,与普通的 JS 文件一样,不会对类型进行检查。
什么是 ArkTS
ArkTS是HarmonyOS优选的主力应用开发语言。它在TypeScript的基础上,匹配ArkUI框架,扩展了声明式UI、状态管理等相应的能力,是TypeScript的超集,底层对接ArkTS引擎。
什么是 Node-API
Node-API
(以前称为 N-API)是用于封装JavaScript能力为Native插件的API,独立于底层JavaScript,并作为Node.js的一部分。Node-API
可以去除底层的JavaScript引擎(如V8引擎等)的差异,提供一套稳定的接口。
HarmonyOS的Native API组件对Node-API
的接口进行了重新实现,底层对接了ArkJS等引擎。当前支持Node-API
标准库中的部分接口。
Node-API
基本使用
线程约束
ark引擎会对js对象线程使用进行保护,使用不当会引起应用crash,因此需要遵循如下原则:
- napi接口只能在js线程使用。
- env与线程绑定,不能跨线程使用。
- native侧js对象与线程所持有的env绑定,即native侧js对象只能在创建时的线程使用
注册模块
- 步骤1:编写特定函数,在 so加载时触发"注册"动作
C
// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
.nm_version =1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
// 打开so时,该函数将自动被调用,使用上述demoModule模块信息,进行模块注册相关动作。
extern "C" __attribute__((constructor)) void RegisterHelloModule(void)
{
napi_module_register(&demoModule);
}
注意:
- 建议:
nm_modname
的值应该与so命名一致,且大小写也一致。如,so的名称为libentry.so,那么nm_modname
的值应为entry
- 规则:
nm_register_func
对应的函数需要加上static,防止与其他so里的符号冲突 - 规则:
__attribute__((constructor))
修饰的函数的函数名需要确保不与其他模块的重复
- 步骤2:编辑注册信息 在
nm_register_func
中指定的函数中补充注册信息,建立 TS Call Native 的入口
C
EXTERN_C_START
// Init将在exports上挂上Add/NativeCallArkTS这些native方法,此处的exports就是开发者import之后获取到的ArkTS对象。
static napi_value Init(napi_env env, napi_value exports)
{
// 函数描述结构体
// 以Add为例
// 第一个参数"add"为暴露给TS层的方法名称
// 第三个参数"Add"为Native方法
// 注册多个 Native 方法,继续追加数组即可
napi_property_descriptor desc[] = {
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "nativeCallArkTS", nullptr, NativeCallArkTS, nullptr, nullptr, nullptr, napi_default, nullptr },
// 略...
};
// 在exports这个ArkTS对象上,挂载native方法。
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
- 步骤3:编辑声明文件,方便 TS层对 Native 层的调用 在项目中存在
index.d.ts
声明文件。在该文件中按照如下规则将Native层暴露的接口进行声明
ts
// 声明规则:
// export 表示导出,add 是 函数名
// a b 是 参数,类型是 number
// =>number 表示返回值类型是 number
export const add: (a: number, b: number) => number;
注:d.ts 文件的作用仅仅是用来声明。如果没有在该文件中声明,TS层强制调用,也是允许的。只是在没有声明的情况下,IDE不会弹出提示
详见官方demo
TS Call Native
- 步骤1:在TS的模块中添加依赖
编辑TS 模块下的 oh-package.json5
文件
json5
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {
// 指向 entry Native 模块的配置文件的文件夹,并将其命名为 libentry.so
"libentry.so": "file:./src/main/cpp/types/libentry"
}
}
- 步骤2:在 TS 代码中引入 entry Native 模块
TS
// 此处的 libentry.so 即为 配置文件中引入的依赖
// 并将其命名为 testNapi
import testNapi from 'libentry.so';
- 步骤3:调用
TS
// 使用 testNapi 调用 Native 方法
testNapi.add(2, 3);
注意:由于 entry Native 模块配置文件夹中存在 d.ts 文件,所以编写代码时 IDE 可以弹出调用 add()
方法的提示
- 步骤4:Native 响应
根据前一个大步骤中补充的注册信息,TS 层调用 add()
方法,会调用到Native 层的 Add()
当中
C
// env 为当前运行上下文,与线程绑定
// TS 层 传递的参数都在 info 中
static napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2] = {nullptr};
// 从 info 中获取 传递的参数
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
napi_valuetype valuetype0;
napi_typeof(env, args[0], &valuetype0);
napi_valuetype valuetype1;
napi_typeof(env, args[1], &valuetype1);
// 将 TS 参数 number 类型转换为 C/C++ 的 double 类型
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
napi_value sum;
int32_t ret = test2(1, 2);
// 构造返回值,并将结果保存到的 napi_value 中
napi_create_double(env, ret, &sum);
// 返回 napi_value ,传递给 TS 层
return sum;
}
Native Call TS
Native 调用 TS 的Function 是项目中十分常见的行为。但是调用过程又存在着诸多限制。 下面主要介绍两种情况下,Native 是如何调用 TS Function的
首先,我们需要定义一个 TS Function。那么,我们的目标就是能够从Native的逻辑中成功触发 testFunction()
方法。
TS
// 定义一个名为 testFunction() 的TS 层方法
function testFunction(a: number): number {
// TODO print something
// return value
return 1024;
}
在主线程发起调用
- 步骤1:准备env
可以将主线程 env保存起来。比如,在上述的 注册模块 的步骤中,将主线程的env保存至全局变量
C
EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
// 略...
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
// 保存主线程 env
g_env = env;
return exports;
}
EXTERN_C_END
注:env 与线程绑定,所以该 env 只能在主线程中使用。请勿跨线程使用
- 步骤2:准备 TS 层的方法
Node-API
无法像 JNI 一样 通过某些 find 方法查找TS层类或者方法(也许是没有找到,欢迎大家补充)。 因此需要设计一个接口,主动将 TS 层的方法传递到Native层,然后保存起来使用。
C
// 定义一个TS 层Function的全局引用
napi_ref js_fun_ref;
// 设计一个Native接口
void *Napi_Register(void *env, void *info) {
// 1. 获取参数信息
// 参数个数与 TS 层 传入的要严格对应
int32 const argc = 1;
napi_value argv[argc] = {NULL};
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
// napi_value 类型的 argv[0] 即为 TS 层Function的引用
// 保存 TS 层Function 的引用
napi_create_reference(env, argv[0], 1, &js_fun_ref);
// 不再使用时,调用 napi_delete_reference() 销毁引用...略
// 不再使用时,调用 napi_delete_reference() 销毁引用...略
// 不再使用时,调用 napi_delete_reference() 销毁引用...略
napi_value ret;
napi_create_int32(env, 0, &ret);
return ret;
}
TS层主动调用一下 Napi_Register()
方法,将 待调用的TS方法传递给Native层
TS
// 将 testFunction 方法注册到native层
testNapi.Napi_Register(testFunction);
- 步骤3:在Native层 Call TS层的方法 准备好 env 和 TS层方法的引用以后,就可以Call了
C
// 取出 TS 层Function的值
napi_value js_fun = NULL;
napi_get_reference_value(g_env, js_fun_ref, &js_fun);
// 准备 入参
size_t argc = 1;
napi_value argv[1];
napi_create_int32(env, 48000, &argv[0]);
// 关键: Call
// 最后一个参数表示 TS Function 的返回值。此处略...
napi_status callRet = napi_call_function(env, NULL, js_callback, argc, argv, NULL);
if (callRet != napi_ok) {
// LOGE("NAPI real callback failed: %d", callRet);
}
在Native子线程发起调用
ark引擎会对js对象线程使用进行保护,napi接口只能在js线程使用,使用不当会引起应用crash。但是在实际开发中,更多的场景是创建几个子线程去执行任务。那么此时在子线程中该如何调用TS层的方法呢?
- 步骤1:准备一个Native层的 threadsafe 方法 Native 子线程对TS Function发起请求,会调度到这个方法中执行。该方法所在的线程是 主线程。只有在主线程中才能发起对 TS 的调用
C
static void Napi_ThreadSafe_TestFunction(napi_env env, napi_value js_callback, void *context, void *data)
{
// 参考前面的思路,通过 napi_call_function() 方法对 TS Function 直接发起调用
// 此处不再赘述...
}
- 步骤2:准备 TS 层的方法 同样地,需要将TS Function 注册到Native层。但不同的是,Native层拿到Function以后,需要为其创建一个可以多线程调用的持久引用。 该引用可以将Native层子线程的请求异步调度到主线程,然后在主线程中发起对TS Function 的调用。这意味着:子线程发起的请求永远是异步的。
C
// 定义一个TS 线程安全Function的全局引用
napi_threadsafe_function funHandle;
// 设计一个Native接口
void *Napi_Register(void *env, void *info) {
// 1. 获取参数信息
// 参数个数与 TS 层 传入的要严格对应
int32 const argc = 1;
napi_value argv[argc] = {NULL};
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
// napi_value 类型的 argv[0] 即为 TS 层Function的引用
status = napi_create_threadsafe_function(env, argv[0], NULL, 'testFunction', 0, 1, NULL, NULL, NULL, Napi_ThreadSafe_TestFunction, funHandle);
if (status != napi_ok) {
LOGE("Failed to create threadsafe_function");
}
// 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略
// 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略
// 不再使用时,调用 napi_release_threadsafe_function() 销毁 funHandle 引用...略
napi_value ret;
napi_create_int32(env, 0, &ret);
return ret;
}
- 步骤3:发起调用 在子线程中,根据业务需要,在合适的实际可以发起对TS Function 的调用。
C
// funHandle:上一个步骤生成的 线程安全Function的全局引用
// param:传递到步骤1指定函数中的参数指针
// napi_tsfn_nonblocking :如果队列已满,调用应该阻塞
napi_status status = napi_call_threadsafe_function(funHandle, param, napi_tsfn_nonblocking);
if (status != napi_ok) {
LOGE("napi_call_threadsafe_function failed. status = %d", status);
}
通过以上3个步骤,可以实现Native 子线程对TS Function的调用。 在此过程中,涉及到线程切换。所以十分有必要对每一个步骤执行时所在的线程进行梳理:
- 首先,请求发起是发生在步骤3。所以执行
napi_call_threadsafe_function()
方法是在Native的子线程。在一般情况下,调用该方法后,子线程不会阻塞(即,不会等待主线程TS Function执行的结果),继续执行自己的任务。 - 然后,主线程根据自己的任务队列,调度执行到步骤1中声明的Native方法。在该方法中,会通过
napi_call_function()
方法同步执行 TS Function,并且可以拿到TS Function的返回值。注意,此时是同步调用 - 最后,步骤2中的 注册 过程,是由TS层调度过来的。一般发生在主线程。注意,env与线程绑定,不能跨线程使用