Electron学习笔记(四)
一方面electron给开发者提供了不少API,
另一方面, 也可以使用node.js的API.
但是, 有时候开发者还是想用自己实现的API.
下面, 我将介绍如何在Electron通过Addon添加接口.
addon是node.js扩展api的方式, 同时electron也可以以同样的方式扩展.
addon有三种编写方式, n-api, nan, addon-api.
其中官方推荐使用n-api, 因为N-API编译的lib兼容性最好.
从JS传入一个字符串, 在C++创建一个JS可访问的Object对象, 其中属性包含前面的字符串, 返回.
从C++创建一个JS可调用的函数. 调用后, 可以获得该函数的返回值.
创建一个对象, 该对象的todo函数返回promise. 这个函数有点绕, 为什么不直接返回promise呢? 因为官方示例直接返回的promise对象用第一次还可以, 用第二次就在C++层冲突了. 原因是内部实现用同一个对象在记录. 如果发生多次调用就会冲突, 因此这里我实现了一个对象, 每次创建一个对象来管理上面提到的对象, 从而避免多次调用引起的冲突.
这里考虑到native接口的添加对js开发者来说有些繁琐, 因此, 这里想出了一种实现方式, 采用json命令的模式来完成调用和回调. 只要使用这一套接口, 后面addon(n-api)代码的接口层就可以不用再改动.
方法是这样的, js app启动时, 先通过RegSvcRsp接口监听消息. app退出时, 通过UnregSvcRsp接口注销监听.
当需要向C++发起命令时, 通过SendSvcReq发出一个json字符串, 示例如下.
其中type: req表示当前是请求, cmd可以扩展很多命令, params是该命令的参数, 参数又是一个json, 可以任意扩展.
var reqStr = JSON.stringify({
"type":"req",
"cmd":"get_user_info",
"params":{ "user_id": 100001 }
})
然后命令执行的结果可以直接通过返回值同步返回.
也可以通过前面RegSvcRsp注册的回调异步返回, 返回的也是json, 格式上跟上面保持一致.
其中, type: rsp表示是回应, cmd跟前面的请求保持一致, params也是json.
char* retStr = "{\"type\":\"rsp\",\"cmd\":\"get_user_info\",\"params\":{ \"user_name\": \"miller\" }}";
所以有了这一套接口, 足以应付几乎所有的情况, 除非开发者对性能要求到了极致, 不能接受json转换过程中的性能消耗.
这里也像前面的promise一样, 对官方示例做了优化, 从官方的示例如果设置一个回调, 从另一个函数去触发该回调, 会异常. 因为底层new一个对象使用napi_create_threadsafe_function, napi_release_threadsafe_function, napi_call_threadsafe_function来管理和访问这个回调. 所以这里我相应的实现了UnregSvcRsp来做回收工作, 否则无法退出和泄露资源.
1. 首先创建一个electron的项目.
npm install create-electron-app -g
create-electron-app my-app
2. 在my-app目录添加一个目录addon
3. 在addon添加文件binding.gyp, 内容如下, 其中的napi是我设置的导出接口对象名.
{
"targets": [
{
"target_name": "napi",
"sources": [ "napi.cc" ]
}
]
}
在addon目录添加napi.js, 用于直接在node.js调试或命令运行. 内容如下.
var napi = require('bindings')('napi');
console.log("RetStr: " + napi.RetStr());
console.log("Add: " + napi.Add(1, 2));
napi.RunCallback((msg) => {
console.log("RunCallback: " + msg);
});
var obj1 = napi.CreateObject("MyObj");
console.log("CreateObject: " + obj1.msg);
var func = napi.CreateFunction();
console.log("CreateFunction: " + func());
var doer1 = napi.CreateDoer();
let promise1 = doer1.todo();
promise1.then(data => {
console.log("Promise1.0.then data param: " + data);
});
promise1.then((data)=>{
console.log("Promise1.then data param: " + data);
return data + 1;
}).then((data)=>{
console.log("Promise1.2.then data param: " + data);
return data + 1;
});
var doer2 = napi.CreateDoer();
doer2.todo().then((data)=>{
console.log("Promise2.then data param: " + data);
return data + 1;
});
var doer3 = napi.CreateDoer();
doer3.todo().then((data)=>{
console.log("Promise3.then data param: " + data);
return data + 1;
});
function onSvcRsp(resp) {
console.log("RegSvcRsp: resp:" + resp);
}
global.onSvcRsp = onSvcRsp;
var ret = napi.RegSvcRsp(global.onSvcRsp);
var reqStr = JSON.stringify({
"type":"req",
"cmd":"get_user_info",
"params":{ "user_id": 100001 }
});
console.log("SendSvcReq: " + reqStr);
ret = napi.SendSvcReq(reqStr);
console.log("SendSvcReq ret: " + ret);
setTimeout(() => {
napi.UnregSvcRsp();
console.log("UnregSvcRsp");
console.log("done");
}, 4000);
4. 在addon目录添加package.json, 也可以自己敲命令生成, 主要是安装bindings.
{
"name": "napi",
"version": "0.0.0",
"description": "Node.js Addons Example #1",
"main": "napi.js",
"private": true,
"dependencies": {
"bindings": "^1.5.0"
},
"scripts": {
"test": "node napi.js"
},
"gypfile": true
}
运行npm install, 这里会自动配置本地的node-gyp编译环境.
期间如果依赖的环境不对, 就会报错, 可以百度了解一下, 需要配哪些依赖.
5. 在addon目录添加napi.cc, 主要用于编译出lib文件.
#include <windows.h>
#include <string>
#include <assert.h>
#include <stdlib.h>
#include <node_api.h>
#include <stdio.h>
#include <ctime>
#define NAPI_DESC(name, func) \
napi_property_descriptor{ name, 0, func, 0, 0, 0, napi_default, 0 }
#define NAPI_DESC_Data(name, func, data) \
napi_property_descriptor{ name, 0, func, 0, 0, 0, napi_default, data }
#define CHECK(expr) \
{ \
if ((expr == napi_ok) == 0) { \
fprintf(stderr, "[Err] %s:%d: %s\n", __FILE__, __LINE__, #expr); \
fflush(stderr); \
abort(); \
} \
}
#define Logout(str) { \
fprintf(stderr, "%s:%d: %s \n", __FILE__, __LINE__, str); \
fflush(stderr); \
}
#define LogoutInt(nValue) { \
fprintf(stderr, "%s:%d: %ld \n", __FILE__, __LINE__, nValue); \
fflush(stderr); \
}
///
// 1. Return a string.
napi_value RetStr(napi_env env, napi_callback_info info) {
napi_value world;
CHECK(napi_create_string_utf8(env, "world", 5, &world));
return world;
}
///
// 2. Return sum of two params.
napi_value Add(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
CHECK(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
double value0, value1;
CHECK(napi_get_value_double(env, args[0], &value0));
CHECK(napi_get_value_double(env, args[1], &value1));
napi_value sum;
CHECK(napi_create_double(env, value0 + value1, &sum));
return sum;
}
///
// 3. Call the callback from params, and get the result.
napi_value RunCallback(napi_env env, const napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
CHECK(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
napi_value cb = args[0];
napi_value argv[1];
CHECK(napi_create_string_utf8(env, "test RunCallback", NAPI_AUTO_LENGTH, argv));
napi_value global;
CHECK(napi_get_global(env, &global));
napi_value result;
CHECK(napi_call_function(env, global, cb, 1, argv, &result));
return nullptr;
}
///
// 4. Return a object, with a property from js.
napi_value CreateObject(napi_env env, const napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
CHECK(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
napi_value obj;
CHECK(napi_create_object(env, &obj));
CHECK(napi_set_named_property(env, obj, "msg", args[0]));
return obj;
}
///
// 5. Return a function, for js call.
napi_value MyFunction(napi_env env, napi_callback_info info) {
napi_value str;
CHECK(napi_create_string_utf8(env, "do Something", NAPI_AUTO_LENGTH, &str));
return str;
}
napi_value CreateFunction(napi_env env, napi_callback_info info) {
napi_value fn;
CHECK(napi_create_function(
env, "theFunction", NAPI_AUTO_LENGTH, MyFunction, nullptr, &fn));
return fn;
}
///
// 7. Fetcher
struct AddonData {
AddonData(): work(nullptr), deferred(nullptr), dataValue(-1) {};
napi_async_work work;
napi_deferred deferred;
int dataValue;
};
static void ExecuteWork(napi_env env, void* data) {
static int s_work_count = 0;
if (data != nullptr) {
((AddonData*)data)->dataValue = ++ s_work_count;
}
}
static void WorkComplete(napi_env env, napi_status status, void* data) {
if (status != napi_ok) {
return;
}
if (data != nullptr) {
AddonData* addon_data = (AddonData*)data;
napi_value jsValue;
CHECK(napi_create_int32(env, addon_data->dataValue, &jsValue));
CHECK(napi_resolve_deferred(env, addon_data->deferred, jsValue));
CHECK(napi_delete_async_work(env, addon_data->work));
addon_data->work = NULL;
addon_data->deferred = NULL;
delete addon_data;
}
}
napi_value todo(napi_env env, napi_callback_info info) {
napi_value work_name, promise;
AddonData* addon_data;
CHECK(napi_get_cb_info(env, info, NULL, NULL, NULL, (void**)(&addon_data)));
CHECK(napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &work_name));
CHECK(napi_create_promise(env, &(addon_data->deferred), &promise));
CHECK(napi_create_async_work(env, NULL, work_name, ExecuteWork, WorkComplete,
addon_data, &(addon_data->work)));
CHECK(napi_queue_async_work(env, addon_data->work));
return promise;
}
napi_value CreateDoer(napi_env env, const napi_callback_info info) {
napi_value doer;
CHECK(napi_create_object(env, &doer));
AddonData* addon_data = new AddonData();
napi_property_descriptor desc = NAPI_DESC_Data("todo", todo, addon_data);
CHECK(napi_define_properties(env, doer, 1, &desc));
return doer;
}
///
// RegSvcRsp, CallSvc
struct CallbackData{
CallbackData(): work(nullptr), tsfn(nullptr) {}
napi_async_work work;
napi_threadsafe_function tsfn;
};
CallbackData* g_cb_data;
napi_value g_js_cb;
static void PostSvcRsp(napi_env env, napi_value js_cb, void* context, void* pData) {
Logout((char*)pData);
napi_value argv[1];
CHECK(napi_create_string_utf8(env, (char*)pData, NAPI_AUTO_LENGTH, argv));
napi_value undefined;
CHECK(napi_get_undefined(env, &undefined));
napi_value result;
CHECK(napi_call_function(env, undefined, js_cb, 1, argv, &result));
}
napi_value RegSvcRsp(napi_env env, const napi_callback_info info) {
napi_value work_name;
CallbackData* cb_data;
size_t argc = 1;
CHECK(napi_get_cb_info(env, info, &argc, &g_js_cb, NULL, (void**)(&cb_data)));
CHECK(napi_create_string_utf8(env, "CallAsyncWork", NAPI_AUTO_LENGTH, &work_name));
CHECK(napi_create_threadsafe_function(env, g_js_cb, NULL, work_name,
0, 1, NULL, NULL, NULL, PostSvcRsp, &(cb_data->tsfn)));
return nullptr;
}
napi_value UnregSvcRsp(napi_env env, const napi_callback_info info) {
g_js_cb = nullptr;
CHECK(napi_release_threadsafe_function(g_cb_data->tsfn, napi_tsfn_release));
// 这里没启动work, 所以不需要删除work
// CHECK(napi_delete_async_work(env, g_cb_data->work));
g_cb_data->work = nullptr;
g_cb_data->tsfn = nullptr;
delete g_cb_data;
return nullptr;
}
napi_value SendSvcReq(napi_env env, const napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
CHECK(napi_get_cb_info(env, info, &argc, args, nullptr, nullptr));
size_t realSize = 0;
CHECK(napi_get_value_string_utf8(env, args[0], nullptr, realSize, &realSize)); // get length
char* buf = new char[realSize];
CHECK(napi_get_value_string_utf8(env, args[0], buf, realSize, &realSize));
Logout(buf);
delete buf;
// send resp with another thread.
char* retStr = "{\"type\":\"rsp\",\"cmd\":\"get_user_info\",\"params\":{ \"user_name\": \"miller\" }}";
CHECK(napi_call_threadsafe_function(g_cb_data->tsfn, (void*)retStr, napi_tsfn_blocking));
// or return data directly
napi_value retData;
CHECK(napi_create_string_utf8(env, retStr, NAPI_AUTO_LENGTH, &retData));
return retData;
}
///
// Init, exports functions.
napi_value Init(napi_env env, napi_value exports) {
g_cb_data = new CallbackData();
Logout("napi init");
napi_property_descriptor desc;
desc = NAPI_DESC("RetStr", RetStr);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("Add", Add);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("RunCallback", RunCallback);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("CreateObject", CreateObject);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("CreateFunction", CreateFunction);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("CreateDoer", CreateDoer);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC_Data("RegSvcRsp", RegSvcRsp, g_cb_data);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("UnregSvcRsp", UnregSvcRsp);
CHECK(napi_define_properties(env, exports, 1, &desc));
desc = NAPI_DESC("SendSvcReq", SendSvcReq);
CHECK(napi_define_properties(env, exports, 1, &desc));
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
以上addon目录就可以单独调试和运行了. 使用node-gyp命令编译出lib.
使用node napi.js运行起来.
接下来, 将在electron工程中引用addon.
1. 修改index.html
<body>
<h1>show napi call and callback, take look at console!</h1>
<script>
require("./app.js");
</script>
</body>
演示接口为主, 因此这里的调用过程和结果返回都直接通过devtools中的console来查看. 并没有实现网页.
2. 创建app.js
var napi = require('bindings')('napi');
var app = {
run: function() {
console.log("RetStr: " + napi.RetStr());
console.log("Add: " + napi.Add(1, 2));
napi.RunCallback((msg) => {
console.log("RunCallback: " + msg);
});
var obj1 = napi.CreateObject("MyObj");
console.log("CreateObject: " + obj1.msg);
var func = napi.CreateFunction();
console.log("CreateFunction: " + func());
var doer1 = napi.CreateDoer();
let promise1 = doer1.todo();
promise1.then(data => {
console.log("Promise1.0.then data param: " + data);
});
promise1.then((data)=>{
console.log("Promise1.then data param: " + data);
return data + 1;
}).then((data)=>{
console.log("Promise1.2.then data param: " + data);
return data + 1;
});
var doer2 = napi.CreateDoer();
doer2.todo().then((data)=>{
console.log("Promise2.then data param: " + data);
return data + 1;
});
var doer3 = napi.CreateDoer();
doer3.todo().then((data)=>{
console.log("Promise3.then data param: " + data);
return data + 1;
});
function onSvcRsp(resp) {
console.log("RegSvcRsp: resp:" + resp);
}
global.onSvcRsp = onSvcRsp;
var ret = napi.RegSvcRsp(global.onSvcRsp);
var reqStr = JSON.stringify({
"type":"req",
"cmd":"get_user_info",
"params":{ "user_id": 100001 }
});
console.log("SendSvcReq: " + reqStr);
ret = napi.SendSvcReq(reqStr);
console.log("SendSvcReq ret: " + ret);
setTimeout(() => {
napi.UnregSvcRsp();
console.log("UnregSvcRsp");
console.log("done");
}, 4000);
}
}
window.onload = function () {
app.run();
}
module.exports = exports = app;
3. 这里有一步比较关键.
在electron项目中npm install bindings
然后进入node_modules修改js, 从而可以让bindings访问到addon目录.
".\node_modules\bindings\bindings.js"
在"defaults"下面添加一行.
['module_root', 'addon', 'build', 'Release', 'bindings'],
接下来就可以运行electron了.
https://github.com/gzx-miller/electron-n-api
请用star来鼓励我的努力和分享, 谢谢~!
https://nodejs.org/api/n-api.html#n_api_n_api
Electron学习笔记(六)