需求,web环境下的插件系统
我希望在web环境(browser, app web view)里实现资源式的插件系统,考虑以下设计因素,
沙盒环境,
能够完整加载,卸载,且插件与框架之间,插件之间需要有充分的隔离性,仅通过框架暴露的API,实现扩展,以及功能调用密集计算阻塞
插件由用户,我们不能预测用户实现的方式,因为web的环境是单线程,任何密集计算会对主框架的响应造成直接影响
web worker已经是现代浏览器标准,我们能够且只能够使用web worker解决以上问题。
平凡框架下的web worker(或 actor)使用方式如下,
// main.js
worker.onmessage
worker.postMessage
// ext1.js
self.onmessage
self.postMessage
但这显然不能满足我们希望用户使用简单的要求,理想中我们希望,作为插件与框架之间通过接口定义进行通信,如下以API直接调用的形式
// main.js
main_lib = {
func_a() ...
func_b() {
// ...
// 远程调用 (RPC)
this.current_ext.method_a ()
}
}
// ext1.js
ext_methods = {
method_a(...args) {
// ...
// 远程调用 (RPC)
const r = remote.func_a()
// ...
}
}
因此,需要RPC(remote processure call)设施
- 首先构建协议
// 请求,当SN为-1时,我们认为是不需要返回的纯指令
req(SN, command, ...args) // 序列化
// 返回序列化
rsp(SN, res) // 序列化
- 怎么处理语法,
remote.method(...args)
javascript es6有proxy特性,利用此特性可将remote.xxx
, 转成统一函数调用的方式,
(Tips: proxy特性在很多语言中存在,ruby里method_missing
(ruby), kotlin叫做getter delegate
,也被叫做面向对象模型的元方法 )
// remote对象 =>
remote = new Proxy({
// 拦截methodName的访问
get(methodName) {
// 构造了异步方法
return (...args) => Promise((res, rej) => {
const now = time.now()
remoteTaskManager.req(serial_NO, methodName, [...args], now, now + timeout)
return remoteTaskManager.subscribeRsp(serial_NO, res, rej);
})
}
})
// SO, remote.method(arg1, arg2) 构造了一个等待 结果/超时 的Promise
- 异步调度
remoteTaskManager,是异步任务的调度器,负责 任务请求/yield,返回/resume
remoteTaskManager里维护一个任务状态队列,如下
SN state time_stamp timeout
...
10086 timeout 1433 1435
10087 pending 1434 1436
10088 succ 1435 1437
10089 fail 1436 1438
...
它的两个方法将作为构造异步函数的基础,
// 将异步任务的上下文放入队列
remoteTaskManager.request(serial_NO, methodName, [...args], now, now+timeout)
// 注册一个对流水号任务的回调
remoteTaskManager.subscribeRsp(serial_NO, onResult, onException)
需求,异步.invork (异步.invork (...
除了简单函数的调用,我们还想支持自身是异步函数的调用,(这样可以remote调用remote,完成远程连续调用的完备性),
方法是通过server 判断调用如果返回了promise对象,等待本地异步完成后,再将结果传回client
需求,远程对象/模块,链式调用
但我们的远程API,不仅仅是函数调用,而且还存在 远端模块,远端对象,比如远端API如下
remote_API.exports = {
lib:{
module_:{
funA():number
funB():promise
},
object_:{
methodA():number
methodB():promise
}
}
}
javascript 作为动态类型语言, 我们不知道比如说,remote.object_or_function 成员是模块对象还是函数,以下是一个相当trick的方式,它依赖于 javascript函数也是对象 这个特性,而且以递归过程实现了连续调用
function makeProxy(remote_object_or_function) {
return new Proxy({
// 拦截methodName的访问
get(object_or_function) {
// 构造了异步方法,如果是函数,调会在这里调用
const async_object_or_function = (...args)=>Promise((res, rej)=>{
const now = time.now()
remoteTaskManager.req(serial_NO, methodName, [...args], now, now+timeout)
return remoteTaskManager.subscribeRsp(serial_NO, res, rej);
})
// 递归,如果是对象,将会在这里调用
return makeProxy(async_object_or_function);
}
})
}
当我使用webpack发布时,babel, ES6→ES5时发生了如下意想不到的转换,此时需要对 apply,eval做特殊处理
// object_.methodA(...args)
// =>
// apply(object_, "methodA", ...args)
// eval(object_, "methodA", ...args)
需求,注册回调API的处理
通常 RPC不适合处理包括 参数和返回中有复杂对象 (因为并非所有对象可以序列化,并且考虑传递到远端的性能问题),更不用闭包(本地上下文信息问题),但现实世界中的JS API很多都有闭包参数和返回,尤其处理起来颇为棘手,
- 问题1, 函数参数如何传递?
函数并不传递,而是在本地保存一个 callback map, 在远端 构造一个回调函数代理,这个代理函数作用是,透传参数回本地,调用本地callback,然后在callback map里释放这个 callback ID
if (typeof (arg) == 'function') {
// 注册本地callback 到 callbackMap
const argProxy = __regCb(callbackMap, reqId, arg);
argProxys.push(proxyArg)
// ...
}
// ...
if (arg.hasOwnProperty('reqId') && arg.hasOwnProperty('cbId')) {
let reqId = arg.reqId;
let cbId = arg.cbId;
// 传入回调参数,做远程透转
args.push((...args) => {
sender({
type: 'cb', reqId, cbId, args
})
})
// ...
}
- 问题2. 返回函数如何传递?
这里有一个(唯一)约定,只允许注册类型的API使用回调,且返回 canceller : ()=>void 类型,作为注册取消方法,这所以这样的限定完全是因为javascript动态类型所致,调用期间我无法得知返回值类型。
而canceller函数的远程调用,处理办法也类似于问题1,只是调用传递过程反过来,client 调用proxy canceller, 透传到server中直正的 canceller,当然,在server中也需要建表保存,
chrome 特殊跨域限制
谷歌浏览器建立web worker出现cannot be accessed from origin 'null'错误,即是说,如果client和server处于不同域,即使服务器处理了允许域外访问 header,chrome依然创建不能由外域脚本的worker,
参考 爆栈 解法如下
// 将远端 javascript文件作为资源对象加载,(普通跨域问题由服务器解除限定即可)
const blob = new Blob([workerHeader, workerText], { type: 'application/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
总结:
RPC实现有两种路线,一种是中间API描述的代码生成(如GRPC),另一种是利用反射/动态特性,运行时构造远端的调用,在这里使用的是第二种,javascript的动态特性,对于实现RPC具有优点(实现非常简短),也带来了一些限制(不可解),
如果在一个静态类型+反射特性+DSL的语言里则可实现得即完备又简单。
如果有(高性能+跨语言) 的要求,则建议选择第一种方式。