web系统的插件架构之—— web-worker-rpc

需求,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)设施

  1. 首先构建协议
// 请求,当SN为-1时,我们认为是不需要返回的纯指令
req(SN, command, ...args) // 序列化
 
// 返回序列化
rsp(SN, res) // 序列化
  1. 怎么处理语法,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
  1. 异步调度
    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<number>
        }, 
        object_:{
            methodA():number
            methodB():promise<number>
        }
    }
}

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的语言里则可实现得即完备又简单。

如果有(高性能+跨语言) 的要求,则建议选择第一种方式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335