2024年前端技术探索 - 你不知道的JS 沙箱隔离,互联网大厂面试题库

后话

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

对于面试,说几句个人观点。

面试,说到底是一种考试。正如我们一直批判应试教育脱离教育的本质,为了面试学习技术也脱离了技术的初心。但考试对于人才选拔的有效性是毋庸置疑的,几千年来一直如此。除非你有实力向公司证明你足够优秀,否则,还是得乖乖准备面试。这也并不妨碍你在通过面试之后按自己的方式学习。
其实在面试准备阶段,个人的收获是很大的,我也认为这是一种不错的学习方式。首先,面试问题大部分基础而且深入,这些是平时工作的基础。就好像我们之前一直不明白学习语文的意义,但它的意义就在每天的谈话间。

所谓面试造火箭,工作拧螺丝。面试往往有更高的要求,也迫使我们更专心更深入地去学习一些知识,也何尝不是一种好事。

  1. 执行从不受信的源获取到的第三方 JavaScript 代码时(比如引入插件、处理 jsonp 请求回来的数据等)。

  2. 在线代码编辑器场景(比如著名的 codesandbox)。

  3. 使用服务端渲染方案。

  4. 模板字符串中的表达式的计算。

  5. … …

这里我们先回到开头,先将前提假设在我正在面对的微前端架构设计下。在微前端架构(推荐文章 Thinking in Microfrontend 、拥抱云时代的前端开发架构——微前端 等)中,其最关键的一个设计便是各个子应用间的调度实现以及其运行态的维护,而运行时各子应用使用全局事件监听、使全局 CSS 样式生效等常见的需求在多个子应用切换时便会成为一种污染性的副作用,为了解决这些副作用,后来出现的很多微前端架构(如 乾坤)有着各种各样的实现。譬如 CSS 隔离中常见的命名空间前缀、Shadow DOM、 乾坤 sandbox css 的运行时动态增删等,都有着确实行之有效的具体实践,而这里最麻烦棘手的,还是微应用间的 JavaScript 的沙箱隔离。

在微前端架构中,JavaScript 沙箱隔离需要解决如下几个问题:

  1. 挂在 window 上的全局方法/变量(如 setTimeout、滚动等全局事件监听等)在子应用切换时的清理和还原。

  2. Cookie、LocalStorage 等的读写安全策略限制。

  3. 各子应用独立路由的实现。

  4. 多个微应用共存时相互独立的实现。

在 乾坤 架构设计中,关于沙箱有两个入口文件需要关注,一个是 proxySandbox.ts,另一个是 snapshotSandbox.ts,他们分别基于 Proxy 实现代理了 window 上常用的常量和方法以及不支持 Proxy 时降级通过快照实现备份还原。结合其相关开源文章分享,简单总结下其实现思路:起初版本使用了快照沙箱的概念,模拟 ES6 的 Proxy API,通过代理劫持 window ,当子应用修改或使用 window 上的属性或方法时,把对应的操作记录下来,每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复,而后来为了兼容多个子应用共存的情况,又基于 Proxy 实现了代理所有全局性的常量和方法接口,为每个子应用构造了独立的运行环境。

另外一种值得借鉴的思路是阿里云开发平台的 Browser VM,其核心入口逻辑在 Context.js 文件中。它的具体实现思路是这样的:

  1. 借鉴 with 的实现效果,在 webpack 编译打包阶段为每个子应用代码包裹一层代码(见其插件包 breezr-plugin-os 下相关文件),创建一个闭包,传入自己模拟的 window、document、location、history 等全局对象(见 根目录下 相关文件)。

  2. 在模拟的 Context 中,new 一个 iframe 对象,提供一个和宿主应用空的(about:blank) 同域 URL 来作为这个 iframe 初始加载的 URL(空的 URL 不会发生资源加载,但是会产生和这个 iframe 中关联的 history 不能被操作的问题,这时路由的变换只支持 hash 模式),然后将其下的原生浏览器对象通过 contentWindow 取出来(因为 iframe 对象天然隔离,这里省去了自己 Mock 实现所有 API 的成本)。

  3. 取出对应的 iframe 中原生的对象之后,继续对特定需要隔离的对象生成对应的 Proxy,然后对一些属性获取和属性设置,做一些特定的实现(比如 window.document 需要返回特定的沙箱 document 而不是当前浏览器的document 等)。

  4. 为了文档内容能够被加载在同一个 DOM 树上,对于 document,大部分的 DOM 操作的属性和方法仍旧直接使用宿主浏览器中的 document 的属性和方法处理等。

总的来说,在 Browser VM 的实现中, 可以看出其实现部分还是借鉴了 乾坤 或者说其他微前端架构的思路,比如常见全局对象的代理和拦截。并且借助 Proxy 特性,针对 Cookie、LocalStorage 的读写同样能做一些安全策略的实现等。但其最大的亮点还是借助 iframe 做了一些取巧的实现,当这个为每个子应用创建的 iframe 被移除时,写在其下 window 上的变量和 setTimeout、全局事件监听等也会一并被移除;另外基于 Proxy,DOM 事件在沙箱中做记录,然后在宿主中生命周期中实现移除,能够以较小的开发成本实现整个 JavaScript 沙箱隔离的机制。

除了以上社区中现在比较火的方案,最近我也在 大型 Web 应用插件化架构探索 一文中了解到了 UI 设计领域的 Figma 产品也基于其插件系统产出了一种隔离方案。起初 Figma 同样是将插件代码放入 iframe 中执行并通过 postMessage 与主线程通信,但由于易用性以及 postMessage 序列化带来的性能等问题,Figma 选择还是将插件放入主线程去执行。Figma 采用的方案是基于目前还在草案阶段 Realm API,并将 JavaScript 解释器的一种 C++ 实现 Duktape 编译到了 WebAssembly,然后将其嵌入到 Realm 上下文中,实现了其产品下的三方插件的独立运行。这种方案和探索的基于 Web Worker 的实现可能能够结合得更好,持续关注中。

Web Worker 与 DOM 渲染


在了解了 JavaScript 沙箱的「前世今生」之后,我们将目光投回本文的主角——Web Worker 身上。

正如本文开头所说,Web Worker 子线程的形式也是一种天然的沙箱隔离,理想的方式,是借鉴 Browser VM 的前段思路,在编译阶段通过 Webpack 插件为每个子应用包裹一层创建 Worker 对象的代码,让子应用运行在其对应的单个 Worker 实例中,比如:

WRAP_WORKER(/* 打包代码 */ });

function WRAP_WORKER(appCode) {

var blob = new Blob([appCode]);

var appWorker = new Worker(window.URL.createObjectURL(blob));

}

但在了解过微前端下 JavaScript 沙箱的实现过程后,我们不难发现几个在 Web Worker 下去实现微前端场景的 JavaScript 沙箱必然会遇到的几个难题:

  1. 出于线程安全设计考虑,Web Worker 不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现。

  2. Web Worker 无法访问 window、document 之类的浏览器全局对象。

其他诸如 Web Worker 无法访问页面全局变量和函数、无法调用 alert、confirm 等 BOM API 等问题,相对于无法访问 window、document 全局对象已经是小问题了。不过可喜的是,Web Worker 中可以正常使用 setTimeout、setInterval 等定时器函数,也仍能发送 ajax 请求。

所以,当先要解决问题,便是在单个 Web Worker 实例中执行 DOM 操作的问题了。首先我们有一个大前提:Web Worker 中无法渲染 DOM,所以,我们需要基于实际的应用场景,将 DOM 操作进行拆分。

React Worker DOM

因为我们微前端架构中的子应用局限在 React 技术栈下,我先将目光放在了基于 React 框架的解决方案上。

在 React 中,我们知道其将渲染阶段分为对 DOM 树的改变进行 Diff 和实际渲染改变页面 DOM 两个阶段这一基本事实,那能不能将 Diff 过程置于 Web Worker 中,再将渲染阶段通过 postMessage 与主线程进行通信后放在主线程进行呢?简单一搜,颇为汗颜,已经有大佬在 5、6 年前就有尝试了。这里我们可以参考下 react-worker-dom 的开源代码。

react-worker-dom 中的实现思路很清晰。其在 common/channel.js 中统一封装了子线程和主线程互相通信的接口和序列化通信数据的接口,然后我们可以看到其在 Worker 下实现 DOM 逻辑处理的总入口文件在 worker 目录下,从该入口文件顺藤摸瓜,可以看到其实现了计算 DOM 后通过 postMessage 通知主线程进行渲染的入口文件 WorkerBridge.js 以及其他基于 React 库实现的 DOM 构造、Diff 操作、生命周期 Mock 接口等相关代码,而接受渲染事件通信的入口文件在 page 目录下,该入口文件接受 node 操作事件后再结合 WorkerDomNodeImpl.js 中的接口代码实现了 DOM 在主线程的实际渲染更新。

简单做下总结。基于 React 技术栈,通过在 Web Worker 下实现 Diff 与渲染阶段的进行分离,可以做到一定程度的 DOM 沙箱,但这不是我们想要的微前端架构下的 JavaScript 沙箱。先不谈拆分 Diff 阶段与渲染阶段的成本与收益比,首先,基于技术栈框架的特殊性所做的这诸多努力,会随着这个框架本身版本的升级存在着维护升级难以掌控的问题;其次,假如各个子应用使用的技术栈框架不同,要为这些不同的框架分别封装适配的接口,扩展性和普适性弱;最后,最为重要的一点,这种方法暂时还是没有解决 window 下资源共享的问题,或者说,只是启动了解决这个问题的第一步。

接下来,我们先继续探讨 Worker 下实现 DOM 操作的另外一种方案。window 下资源共享的问题我们放在其后再作讨论。

AMP WorkerDOM

在我开始纠结于如 react-worker-dom 这种思路实际落地开发的诸多「天堑」问题的同时,浏览过其他 DOM 框架因为同样具备插件机制偶然迸进了我的脑海,它是 Google 的 AMP。

AMP 开源项目 中除了如 amphtml 这种通用的 Web 组件框架,还有很多其他工程采用了 Shadow DOM、Web Component 等新技术,在项目下简单刷了一眼后,我欣喜地看到了工程 worker-dom。

粗略翻看下 worker-dom 源码,我们在 src 根目录下可以看到 main-thread 和 worker-thread 两个目录,分别打开看了下后,可以发现其实现拆分 DOM 相关逻辑和 DOM 渲染的思路和上面的 react-worker-dom 基本类似,但 worker-dom 因为和上层框架无关,其下的实现更为贴近 DOM 底层。

先看 worker-thread DOM 逻辑层的相关代码,可以看到其下的 dom 目录 下实现了基于 DOM 标准的所有相关的节点元素、属性接口、document 对象等代码,上一层目录中也实现了 Canvas、CSS、事件、Storage 等全局属性和方法。

接着看 main-thread,其关键功能一方面是提供加载 worker 文件从主线程渲染页面的接口,另一方面可以从 worker.ts 和 nodes.ts 两个文件的代码来理解。

在 worker.ts 中像我最初所设想的那样包裹了一层代码,用于自动生成 Worker 对象,并将代码中的所有 DOM 操作都代理到模拟的 WorkerDOM 对象上:

const code = `

‘use strict’;

(function(){

${workerDOMScript}

self[‘window’] = self;

var workerDOM = WorkerThread.workerDOM;

WorkerThread.hydrate(

workerDOM.document,

${JSON.stringify(strings)},

${JSON.stringify(skeleton)},

框架相关

原生JS虽能实现绝大部分功能,但要么就是过于繁琐,要么就是存在缺陷,故绝大多数开发者都会首选框架开发方案。现阶段较热门是React、Vue两大框架,两者工作原理上存在共通点,也存在一些不同点,对于校招来说,不需要两个框架都学得特别熟,一般面试官会针对你简历中写的框架进行提问。

在框架方面,生命周期、钩子函数、虚拟DOM这些基本知识是必须要掌握的,在学习的过程可以结合框架的官方文档

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

Vue框架

知识要点:
1. vue-cli工程
2. vue核心知识点
3. vue-router
4. vuex
5. http请求
6. UI样式
7. 常用功能
8. MVVM设计模式

React框架

知识要点:
1. 基本知识
2. React 组件
3. React Redux
4. React 路由

vuex**

5. http请求
6. UI样式
7. 常用功能
8. MVVM设计模式

[外链图片转存中…(img-1eSh3rcw-1715484781413)]

React框架

知识要点:
1. 基本知识
2. React 组件
3. React Redux
4. React 路由

[外链图片转存中…(img-ZUS1XgR2-1715484781414)]

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值