大型Web应用插件化架构探索
由于篇幅过长,本文会拆分成系列文章,借助Web应用的插件架构进行介绍。
一前言随着Web技术的逐渐成熟,越来越多的应用架构趋向于复杂,例如阿里云等巨型控制台项目,每个产品下都有各自的团队来负责维护和迭代。不论是维护还是发布以及管控成本都随着业务体量的增长而逐渐不可控。在这个背景下微前端应用而生,微前端在阿里内部已经有许多成熟的实践,这里不再赘述。本文以微前端为引子(蹭热度),探讨一些另类的Web应用所面临的类似问题。二现代文本编辑器沉浮年微软GitHub后,Atom便经常被拿来调侃,所谓一山不容二虎。在VSCode已经成为一众前端工程师编辑器首选的当下,Atom的地位显得很尴尬,论性能被同为Electron的VSCode秒杀,论插件,VSCode去年插件总数就已经突破1w大关,而早发布一年多的Atom至今还停留在8k+。再加上微软官方主导的LSP/DAP等重量级协议的普及,时至今日Atom作为曾经Web/Electron技术标杆应用的地位早已被VSCode斩落马下。网上关于Atom的日渐衰落的讨论,始终离不开性能。Atom的确太慢了,究其原因很大程度上是被其插件架构所拖累的。尤其是Atom在UI层面开放过多的权限给插件开发者定制,插件质量良莠不济以及UI完全开放给插件后带来的安全隐患都成为Atom的阿喀琉斯之踵。甚至其主界面的FileTree、Tab栏、SettingViews等重要组件都是通过插件实现的。相比之下VSCode则封闭很多,VSCode插件完全运行在Node.js端,对于UI的定制性只有极个别被封装为纯方法调用的API。但另一方面,VSCode这种相对封闭的插件UI方案,一些需要更强定制性的功能便无法满足,更多插件开发者开始魔改VSCode底层甚至源码来实现定制。例如社区很火的VSCodeBackground,这款插件通过强行修改VSCode安装文件中的CSS来实现编辑器区域的背景图。而另一款VSCNeteaseMusic则更激进,因为VSCode捆绑包中的Electron剔除了FFmpeg导致在Webview视图下无法播放音视频,使用此插件需要自行替换FFmpeg的动态链接库。而这些插件不免会对VSCode安装包造成一定程度的破坏,导致用户需要卸载重装。三不止编辑器-飞个马Figma是一个在线协作式UI设计工具,相比Sketch它具有跨平台、实时协作等优点,近年来逐渐受到UI设计师们的青睐。而近期Figma也正式上线了其插件系统。作为一个Web应用,Figma的插件系统自然也是基于JavaScript构建的,这一定程度上降低了开发门槛。自去年6月份Figma官方宣布开放插件系统测试以来,已经有越来越多的Designner/Developer开发了00+插件,其中包括图形资源、文件归档、甚至是导入D模型等。四Figma的插件系统是如何工作的?这是一个基于TypeScript+React技术栈,使用Webpack构建的Figma插件目录结构:.├──README.md├──figma.d.ts├──manifest.json├──package-lock.json├──package.json├──src│├──code.ts│├──logo.svg│├──ui.css│├──ui.html│└──ui.tsx├──tsconfig.json└──webpack.config.js在其manifest.json文件中包含了一些简单的信息:
{"name":"ReactSample","id":"","api":"1.0.0","main":"dist/code.js","ui":"dist/ui.html"}可以看出Figma将插件入口分为了main与ui两部分,main中包含了插件实际运行时的逻辑,而ui则是一个插件的HTML片段。即UI与逻辑分离。安装一个ColorSearch插件后观察页面结构可以发现main中的js文件被包裹在一个iframe里加载到页面上,关于main入口的沙箱机制后文中有详细的阐述。而ui中的HTML最终也被包裹在一个iframe里渲染出来,这将有效的避免插件UI层CSS代码导致全局样式污染。FigmaDevelopers文档中有一章节HowPluginsRun对其插件系统运行机制进行了简单的介绍,简单来说Figma为插件中逻辑层的main入口创建了一个最小的JavaScript执行环境,它运行在浏览器主线程上,在这个执行环境中插件代码无法访问到一些浏览器全局的API,从而也就无法在代码层面对Figma本身运行造成影响。而UI层有且仅有一份HTML代码片段,在插件被激活后被渲染到一个弹窗中。Figma官方博客中对其插件的沙箱机制做了详细的阐述。起初他们尝试的方案是iframe,一个浏览器自带的沙箱环境。将插件代码由iframe包裹起来,由于iframe天然的限制,这将确保插件代码无法操作Figma主界面上下文,同时也可以只开放一份白名单API供插件调用。乍一看似乎解决了问题,但由于iframe中的插件脚本只能通过postMessage与主线程通信,这导致插件中的任何API调用都必须被包装为一个异步async/await的方法,这无疑对Figma的目标用户非专业前端开发者的设计师不够友好。其次对于较大的文档,postMessage通信序列化的性能成本过高,甚至会导致内存泄漏。Figma团队选择回到浏览器主线程,但直接将第三方代码运行在主线程,由此引发的安全问题是不可避免的。最终他们发现了一个尚在stage阶段的草案RealmAPI。Realm旨在创建一个领域对象,用于隔离第三方JavaScript作用域的API。
letg=window;//outergloballetr=newRealm();//rootrealmletf=r.evaluate("(function(){return17})");f()===17//trueReflect.getPrototypeOf(f)===g.Function.prototype//falseReflect.getPrototypeOf(f)===r.globalThis.Function.prototype//true值得注意的是,Realm同样可以使用JavaScript目前已有的特性来实现,即with与Proxy。这也是目前社区比较流行的沙箱方案。
constwhitelist={windiw:undefined,document:undefined,console:window.console,};constscopeProxy=newProxy(whitelist,{get(target,prop){if(propintarget){returntarget[prop]}returnundefined}});with(scopeProxy){eval("console.log(document.write)")//Cannotreadpropertywriteofundefined!eval("console.log(hello)")//hello}前文中Figma插件被iframe所包裹的插件main入口即包含了一个被Realm接管的作用域,你可以认为是类似这段示例代码中的一份白名单API,毕竟维护一份白名单比屏蔽黑名单实现起来更简洁。但事实上由于JavaScript的原型式继承,插件仍然可以通过console.log方法的原型链访问到外部对象,理想的解决方案是将这些白名单API在Realm上下文中包装一次,从而彻底隔离原型链。
constsafeLogFactory=realm.evaluate(`(functionsafeLogFactory(unsafeLog){returnfunctionsafeLog(...args){unsafeLog(...args);}})`);constsafeLog=safeLogFactory(console.log);constouterIntrinsics=safeLoginstanceOfFunction;constinnerIntrinsics=realm.evaluate(`loginstanceOfFunction`,{log:safeLog});if(outerIntrinsics
!innerIntrinsics)thrownewTypeError();realm.evaluate(`log("Hellooutsideworld!")`,{log:safeLog});显然为每一个白名单中的API做这样操作的工作是非常繁杂且容易出错的。那么如何构建一个安全且易于添加API的沙箱环境呢?Duktape是一个由C++实现的用于嵌入式设备的JavaScript解释器,它不支持任何浏览器API,自然地它可以被编译到WebAssembly,Figma团队将Duktape嵌入到Realm上下文中,插件最终通过Duktape解释执行。这样可以安全的实现插件所需API,且不用担心插件会通过原型链访问到沙箱外部。这是一种被称为MembranePattern的防御性的编程模式,用于在程序中与子组件(广义上)实现一层中介。简单来说就是代理(Proxy),为一个对象创建一个可控的访问边界,使得它可以保留一部分特性给第三方嵌入脚本,而屏蔽一部分不希望被访问到的特性。关于Membrane的详细论述可以查看Isolatingapplicationsub-
转载请注明:http://www.sinoeverlife.com/glscf/11835.html