VSLite 原理解析与本地化部署
VSLite 是一个开源的、完全运行在浏览器中的开发环境,它的亮点在于,不需要联网也可以直接在浏览器中使用 Node.js、Python、Git 等工具。你可以在 vslite.dev 体验到在线 demo。
传统的基于云的开发环境,例如 GitHub Codespaces,依赖于后端服务,前端所执行的命令,实际上是通过网络请求发送到后端对应的云虚拟机(或容器)中执行的,这就需要消耗后端的计算资源;而 VSLite 的思路完全不同,它在浏览器中启动了一个容器,做到了不再依赖后端。
想要在浏览器中跑起来一个容器,第一次听说这样的想法时,我说:不可能!绝对不可能!就浏览器这也受限那也受限的环境,还能跑起来一个操作系统?然而 StackBlitz 真的做到了,他们开发出了 WebContainers,WebContainers 是一个基于浏览器的运行时,用于执行 Node.js 程序和操作系统命令,完全在浏览器选项卡中。以前需要云虚拟机才能执行的应用,现在可以完全在浏览器端运行。
经过两天的研究,我开始对 WebContainers 的原理有了初步的了解。
真假离线环境?
一开始我对浏览器能运行容器持怀疑态度,为了证明真是浏览器在运行容器,而不是某台远程的虚拟机,我拉取了 VSLite 项目,本地启动,然后断开网络访问 localhost:5101
,果然根本无法加载,哈哈!露馅了吧?
重新连接网络,打开浏览器开发者工具能发现,容器启动过程需要向 staticblitz.com
发送数百个请求,难道它其实还是在线运行的?
改进一下步骤,先在联网状态下打开本地环境,等待加载完成之后断网,然后在终端中执行一段命令。诶?执行成功了?再瞅瞅操作系统,也能正常打印,居然还是 Ubuntu。
这下破案了,容器在启动过程需要从 staticblitz 的域名获取一系列资源,在启动完成后断网则不影响使用。浏览器运行容器,还真让他给实现了。
但我既然选择 VSLite,是因为它不需要连接云服务,这下还是要连接 staticblitz,没满足我的需求啊,我想本地部署。
查阅文档,根据 WebContainers 文档上的许可协议,本地化部署是需要商用授权的,所以文档上也无法找到任何有关本地部署的指导。
本地部署!
没有文档,那就读源码,自己研究原理,开搞!
等等,WebContainers 好像没开源……
等等,我可以看到 @webcontainer/api
这个包的代码,而且好在他们没给代码做混淆。
打开 node_modules/@webcontainer/api/dist/index.js
,发现了这一段代码:
1 | const DEFAULT_IFRAME_SOURCE = 'https://stackblitz.com/headless'; |
这个网址就是首个发送到 staticblitz 的请求,它被设置到一个 iframe 的 src 中,这个请求返回了什么呢?
1 |
|
本地化部署的核心在于把请求公网的资源,全部在本地 mock 一遍,也就是把上面看到的所有网址全部指向本地。
我们先把这个文件保存到 VSLite 项目的 public/wc/headless
中(这里的 wc 是我随便取的,取自 WebContainers 的首字母)。
刚才注意到容器启动会先判断有无 window.WEBCONTAINER_API_IFRAME_URL
,所以只需要在容器启动前修改这个值就可以了。
修改 src/vite-env.d.ts
1 | declare module globalThis { |
阅读 WebContainers 文档得知,启动容器的命令是 WebContainer.boot
,全局搜一下,只有一处,在 src/hooks/useShell.ts
,在这行代码前面加一行
1 | window.WEBCONTAINER_API_IFRAME_URL = window.location.origin + '/wc/headless'; |
这样,第一个请求就本地化成功了。
我们继续看 headless 文件,发现里面还涉及到多个 js 文件和后端地址,然而我无法确定具体发往这些地址的请求,咋办?
有一个偷懒的办法,浏览器先打开一个标签,打开开发者工具切到 Network 标签,然后访问 vslite.dev
,加载完成之后,右键任意一个请求,点击 Copy - Copy all URLs,嘿嘿,所有用到的请求就都复制出来了。
根据这些请求的文件,我也对 WebContainers 的启动过程有了一个认识,full_bin_index.9e2d28a3
和几个 wasm 文件组成了一个精简版的操作系统镜像,这个镜像在 WebAssembly 环境中运行,并通过 Service Worker 和前端页面通讯。
找个批量下载工具(例如迅雷、aria2…)依次下载所有涉及到的资源,全部放到 public 里面。然后修改 headless 文件,把所有请求地址都改成本地地址。
改完后发现还有一部分 monaco-editor 的资源请求,这部分就比较简单了。
先用 npm info 查一下 monaco-editor 的下载地址,看输出的 tarball 就是下载地址
1 | npm info monaco-editor@0.36.1 |
下载下来
1 | curl -LO https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.36.1.tgz |
再解压
1 | tar -xzf monaco-editor-0.36.1.tgz |
解压 monaco-editor 后,只需把里面的 min
目录移动到 public/monaco-editor-0.36.1
,其他的都不需要,删除。
然后看一下项目怎么引入 monaco 的,发现用的是 @monaco-editor/react,看一下这个包的文档,里面说可以通过 loader-config 来自定义 monaco 文件地址。
1 | import { loader } from '@monaco-editor/react'; |
把这行放在 src/hooks/useStartup.ts
的 useMonaco()
之前,monaco 的本地化就完成了。
最后 public 目录中一共放了这么些文件。
至此,VSLite 的本地化工作已经初步完成,断网访问 localhost:5101
,容器正常加载,创建一个 js 文件用 node 执行试试,也可以正常执行。
以上所有的改动都可以在 我的 GitHub 找到。
你可能会意识到,headless 里面还有 p.stackblitz.com
、t.staticblitz.com
、nr.staticblitz.com
,这三个地址没有本地化,他们的作用是反向代理 Git 请求和 npm 请求(可能是为了跨域?),这部分请求无法简单通过静态 mock,经过实测,这不妨碍 WebContainers 加载,但会影响 git clone
和 npm install
,有兴趣的朋友可以试试自己搭建 proxy 来解决这个问题!
VSLite 原理解析与本地化部署