edge.js的一些使用心得

最近尝试了一下Edge.js,用来给Electron的应用扩展一些功能,后续也可以用在普通的node程序上。

Edge.js本身的功能还是很强大的,它提供了多个语言互操作、混合编程的能力,不过主要是在.Net这一块上。除了C#它还支持F#、Python、PwerShell、Lisp、T-SQL等,能力还是很强的。

而且它不只提供一个JS和C#混合编程、在JS里面操作C#的能力,在C#内操作JS也是可以的。对于Electron来说,最主要的能力是C#可以通过.Net暴露一些Electron不能直接用的系统接口给Electron,对于一个纯粹针对Windows开发的应用来说还是很有用的。

使用

Edge.js在Electron里面需要引入它的特别实现,API和Edge.js都保持一致,只是引入的依赖需要更换,从edge-js换成electron-edge-js。

安装的话还是npm一行搞定:

1
npm install electron-edge-js

引入之后,项目内可以直接用Edge.js跑一个C#片段,当然也可以用C#编译的DLL。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
const NetMeterDLLStatic = {
getAdapterNames: edge.func({
assemblyFile: DLL_PATH,
typeName: TYPE_NAME,
methodName: 'GetAdapterNames',
}),
getActiveAdapter: edge.func({
assemblyFile: DLL_PATH,
typeName: TYPE_NAME,
methodName: 'GetActivatedAdapter',
})
};

直接用edge.func函数就可以把一个JS内的函数定位到一个C# DLL某个类的某个函数里面,其中TypeName填入命名空间(到类名位置),methodName指向方法。

DLL会被运行到一个单独的进程里,这个进程是Edge.js管理的,不需要我们去操心。在开发的过程中我们一样可以在VS里面给DLL的代码下断点,只要输出DLL的时候有同时输出调试符号,我们就可以通过附加到Electron这个子进程的方式进行调试。

如果你运行的是一个C#片段,那么Edge.js会先把这个片段存成一个临时的C#文件,然后把它编译运行。

这里需要注意的是,由于通信方面的原因,所有的edge.func都是异步返回结果的,而且Edge.js本身并没有使用Promise或者async/await开发,取值还是用基本回调。

为了简化调用,Edge.js对于C#暴露出来的方法是有一个要求的,一定只能是async Task<object&rt; (object o) {},意味着如果需要用这种方法来实现混合编程,暴露的方法是一定需要包装的。

在JS这边,如果你期望它以一个同步的方法来走,也需要用Promise进行一个包装,例如上面两个方法,包装完会是下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const NetMeterDLLStatic = {
getAdapterNames: edge.func({
assemblyFile: DLL_PATH,
typeName: TYPE_NAME,
methodName: 'GetAdapterNames',
}),
getActiveAdapter: edge.func({
assemblyFile: DLL_PATH,
typeName: TYPE_NAME,
methodName: 'GetActivatedAdapter',
}),
getAdapterNamesPromise() {
return new Promise((resolve, reject) => {
this.getAdapterNames(null, (err, res) => {
if (err) {
console.error(err);
resolve(null);
return;
}
try {
resolve(JSON.parse(res));
} catch (e) {
resolve(null);
}
});
});
},
getActiveAdapterPromise() {
return new Promise((resolve, reject) => {
this.getActiveAdapter(null, (err, res) => {
if (err) {
console.error(err);
resolve(null);
return;
}
resolve(res);
});
});
},
};

需要注意的是res返回的这个值默认会转化成JS里有的类型,但是对于一个C#里复杂的object,如果是JSON安全的,那么要在C#中预先用Newtonsoft.JSON等转化成JSON,返回一个string给JS,JS的逻辑取到这个值后再做一个

Promise里我没有用reject,严格来说应该要用reject,并且在后续用async/await把异步任务同步化的时候做try/catch,但是我应用在逻辑上没这个需求,就没有这么做。

到这一步事情基本上是搞定了,但是还要注意Edge.js存在得坑,这个坑是不可避免的,和Edge.js的执行方法有关系。

异常捕获

Edge.js的代码在执行中如果出现了异常,会把异常放到回调函数的err里。

原则上靠这个err就能够排错了,当然,返回的Error是C#那边的,所以我们得去.cs源码里找错误。异常信息是包括调用栈信息的,还不错,但是我个人还是推荐用VS的“附加到进程...”去除错。

默认来说Edge.js的C#出现了问题是不会影响执行的,但是如果你在主进程里面调用C#方法,如果有错误可能会触发Electron本身的异常处理出现弹窗。

会引起崩溃的问题我目前还没见到过,但是考虑到它是一个多进程模型,应该是不会引起崩溃的,除非C#这边的代码引发了什么系统这边的问题。

避坑

JS和C#在一个底层的原理上有本质区别,JS没有“类”这个说法,只有使用对象和原型链去模拟类操作的东西,不存在定义一个抽象的类然后去实例化这种方式。

即使是ES6的class关键字,它提供的也是一种基于[[Prototype]]的类似类的实现,只是这是一种语法糖,让它看起来更像是一个真正的“类”。

根据笔者的测试,即使用ES6的class去包装C#导出的一个类中函数,在JS里使用class去包装它,然后new,得到的行为其实并不和C#中的一致,因为实际上JS里面是不存在这样一个做法的。

但对于Edge.js来说,如果它要调用DLL里面某个类的某个函数,如果类不是静态的,那么它必然还是要实例化,根据笔者的观察,Edge.js在和DLL操作的过程中每一次调用,对于被调用到的类来说,运行的不是同一个实例。

但是调用之中构造出来的实例,是可以在内存里保留的,所以或许我们可以简单理解为Edge.js做了这样一个事情:

1
2
ClassA a = new ClassA();
return a.func();

所以对于C#暴露出来的类,类里面一定不能放非静态的变量,否则结果肯定是未引用。你可以考虑把它放到其他的类里面去,这个是可以的,相关的东西也会在内存中保持。

我个人的做法是如果要在暴露类里面维护多个实例,那么就用一个静态的Dictionary<>去维护。

如果你类里面包含的变量只是临时一用,那Edge.js的这个特性并不影响最后的结果,但是如果你需要后面再用到它,那么非静态变量已经会抛出一个未引用到对象的错误。

参考

关于Edge.js,最好的参考还是他们的文档:
Edge.js

关于本文提到的一些JavaScript相关的一些行为上的问题,可参考《你不知道的JavaScript》,里面有比较详尽的解释。