最后
面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
let f = r.evaluate(“(function() { return 17 })”);
f() === 17 // true
Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.globalThis.Function.prototype // true
值得注意的是,Realm 同样可以使用 JavaScript 目前已有的特性来实现,即 with 与 Proxy。这也是目前社区比较流行的沙箱方案。
const whitelist = {
windiw: undefined,
document: undefined,
console: window.console,
};
const scopeProxy = new Proxy(whitelist, {
get(target, prop) {
if (prop in target) {
return target[prop]
}
return undefined
}
});
with (scopeProxy) {
eval(“console.log(document.write)”) // Cannot read property ‘write’ of undefined!
eval(“console.log(‘hello’)”) // hello
}
前文中 Figma 插件被 iframe 所包裹的插件 main 入口即包含了一个被 Realm 接管的作用域,你可以认为是类似这段示例代码中的一份 白名单 API,毕竟维护一份白名单比屏蔽黑名单实现起来更简洁。但事实上由于 JavaScript 的原型式继承,插件仍然可以通过 console.log 方法的原型链访问到外部对象,理想的解决方案是将这些白名单 API 在 Realm 上下文中包装一次,从而彻底隔离原型链。
const safeLogFactory = realm.evaluate(`
(function safeLogFactory(unsafeLog) {
return function safeLog(…args) {
unsafeLog(…args);
}
})
`);
const safeLog = safeLogFactory(console.log);
const outerIntrinsics = safeLog instanceOf Function;
const innerIntrinsics = realm.evaluate(log instanceOf Function
, { log: safeLog });
if (outerIntrinsics || !innerIntrinsics) throw new TypeError();
realm.evaluate(log("Hello outside world!")
, { log: safeLog });
显然为每一个白名单中的 API 做这样操作的工作是非常繁杂且容易出错的。那么如何构建一个安全且易于添加 API 的沙箱环境呢?
Duktape 是一个由 C++ 实现的用于嵌入式设备的 JavaScript 解释器,它不支持任何浏览器 API,自然地它可以被编译到 WebAssembly,Figma 团队将 Duktape 嵌入到 Realm 上下文中,插件最终通过 Duktape 解释执行。这样可以安全的实现插件所需 API,且不用担心插件会通过原型链访问到沙箱外部。
这是一种被称为 Membrane Pattern 的防御性的编程模式,用于在程序中与子组件(广义上)实现一层中介。简单来说就是代理(Proxy),为一个对象创建一个可控的访问边界,使得它可以保留一部分特性给第三方嵌入脚本,而屏蔽一部分不希望被访问到的特性。关于 Membrane 的详细论述可以查看 Isolating application sub-components with membranes 与 Membranes in JavaScript 这两篇文章。
这是最终 Figma 的插件方案,它运行在主线程,不需要担心 postMessage 通信带来的传输损耗。多了一次 Duktape 解释执行的消耗,但得益于 WebAssembly 出色的性能,这部分消耗并不是很大。
另外 Figma 还保留了最初的 iframe ,允许插件可以自行创建 iframe ,并在其中插入任意 JavaScript ,同时它可以与沙箱中的 JavaScript 脚本通过 postMessage 相互通信。
▐ 鱼和熊掌如何兼得?
我们把这类插件的需求总结为在 Web 应用中运行第三方代码及其自定义控件,它有与开头提到的微前端架构非常相似的一些问题。
-
一定程度上的 JavaScript 代码沙箱隔离机制,应用主体对第三方代码(或子应用)有一定的管控能力
-
样式强隔离,第三方代码样式不对应用主体产生 CSS 污染
JavaScript 沙箱
JavaScript 沙箱隔离在社区是个经久不衰的话题,最简单的 iframe 标签 Sandbox 属性就已经能做到 JavaScript 运行时的隔离,社区较为流行的是利用一些语言特性(with、realm、Proxy 等 API )屏蔽(或代理) Window、Document 等全局对象,建立白名单机制,对可能潜在危险操作的 API 重写(如阿里云 Console OS - Browser VM)。另外还有 Figma 这种尝试嵌入平台无关的 JavaScript 解释器,所有第三方代码都通过嵌入的解释器来执行。以及利用 Web Worker 做 DOM Diff 计算,并将计算结果发送回 UI 线程来进行渲染,这个方案早在 2013 年就已经有人进行了实践,这篇论文中作者将 JSDOM 这一 Node.js 平台广泛流行的测试库运行在 Web Worker。而近些年来也有 preact-worker-demo 、react-worker-dom 等项目基于 Web Worker 的 DOM Renderer 尝试将 DOM API 代理到 Worker 线程。而 Google AMP Project 在JSCONF 2018 US 对外公布的 worker-dom 则将 DOM API 在 Web Worker 端实现了 DOM API,虽然实践下来还存在一些问题(例如同步方法无法模拟),但 WorkerDOM 在性能和隔离性上都取得了一定成果。
以上这些解决方案被广泛的应用在各种插件化架构的 Web 应用中,但大多都是 Case By Case,每种解决方案都有各自的成本与取舍。
CSS 作用域
CSS 样式隔离方案中,如上文中 Figma 使用 iframe 渲染插件界面,牺牲一部分性能换来了相对完美的样式隔离。而在现代前端工程化体系下,可以通过 CSS Module 在转译时对 class 添加 hash 或 namespace 等方式实现,这类方案较为依赖插件代码编译过程。而更新潮的是利用 Web Component 的 Shadow DOM,将插件元素用 Web Component 包裹起来,Shadow Root 外部样式无法作用于内部,同样 Shadow Root 内部的样式也无法影响到外部。
最后
本文列举了目前编辑器、设计工具这类大型 Web 应用插件化架构下所面临的的一些问题,以及社区实践的解决方案。不论是让人又爱又恨的 iframe ,还是 Realm、Web Worker 、 Shadow DOM 等,目前来说每种方案都有各自的优势与不足。但随着 Web 应用的复杂度增长,插件化这一需求也逐渐被各大标准化组织所重视起来。下一篇将着重介绍 KAITIAN IDE 中插件架构的探索与实践,包括 JavaScript 沙箱、CSS 隔离、Web Worker 等。
淘宝技术部-前端工程&商家业务-招贤纳士
我们是淘宝技术部前端工程&商家业务团队,负责阿里巴巴集团统一前端工程平台的规划和建设,负责为商家开放&自运营体系提供基础服务,也负责的阿里手机中台致力于将手机使用服务化来降低规模化手机使用的成本,真正做到技术+业务双引擎,为我们的客户端保驾续航,期待有技术追求和技术深度的你加入!
招聘岗位:前端技术专家(Web方向)/前端技术专家(Nodejs方向)/前端技术专家(IDE方向)
如果您有兴趣可以将简历发至 xubing.bxb@alibaba-inc.com
✿ 拓展阅读
最后
全网独播-价值千万金融项目前端架构实战
从两道网易面试题-分析JavaScript底层机制
RESTful架构在Nodejs下的最佳实践
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
一线互联网企业如何初始化项目-做一个自己的vue-cli
思维无价,看我用Nodejs实现MVC
代码优雅的秘诀-用观察者模式深度解耦模块
前端高级实战,如何封装属于自己的JS库
VUE组件库级组件封装-高复用弹窗组件
https://i-blog.csdnimg.cn/blog_migrate/1f7358690bba8963935831b372d672c4.png)
VUE组件库级组件封装-高复用弹窗组件