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
2
3
4
5
const DEFAULT_IFRAME_SOURCE = 'https://stackblitz.com/headless';
// ......
function getIframeUrl() {
return new URL(window.WEBCONTAINER_API_IFRAME_URL ?? DEFAULT_IFRAME_SOURCE);
}

这个网址就是首个发送到 staticblitz 的请求,它被设置到一个 iframe 的 src 中,这个请求返回了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<title>StackBlitz</title>
<script src="https://w-corp-staticblitz.com/webcontainer.9e2d28a3.js"></script>
<link rel="preload" as="script" href="https://w-corp-staticblitz.com/webcontainer.9e2d28a3.js" fetchpriority="low">
<link rel="preload" as="script" href="https://w-corp-staticblitz.com/fetch.worker.9e2d28a3.js" fetchpriority="low">

<script src="https://c.staticblitz.com/assets/vite/headless-BNfRU0p-.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="https://c.staticblitz.com/assets/vite/semver-Zyv2pDaP.js" as="script" crossorigin="anonymous">
</head>
<body>
<script type="application/json" id="webcontainer-context">{"options":{"baseUrl":"https://w-corp-staticblitz.com","initOptions":{"server":"https://local-corp.webcontainer-api.io","isolationPolicy":"require-corp","version":"9e2d28a3","flattenedServer":false},"systemBinaries":{},"git":{"proxy":"https://p.stackblitz.com"},"turboBaseUrl":"https://t.staticblitz.com","registryProxy":"https://nr.staticblitz.com","registryMaxConcurrency":""},"embedder":"https://vslite.dev/","shortAppId":false}</script>

</body>
</html>

本地化部署的核心在于把请求公网的资源,全部在本地 mock 一遍,也就是把上面看到的所有网址全部指向本地。

我们先把这个文件保存到 VSLite 项目的 public/wc/headless 中(这里的 wc 是我随便取的,取自 WebContainers 的首字母)。

刚才注意到容器启动会先判断有无 window.WEBCONTAINER_API_IFRAME_URL,所以只需要在容器启动前修改这个值就可以了。

修改 src/vite-env.d.ts

1
2
3
4
declare module globalThis {
// ......
var WEBCONTAINER_API_IFRAME_URL: string;
}

阅读 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
2
import { loader } from '@monaco-editor/react';
loader.config({ paths: { vs: window.location.origin + '/monaco-editor-0.36.1/min/vs' } });

把这行放在 src/hooks/useStartup.tsuseMonaco() 之前,monaco 的本地化就完成了。

最后 public 目录中一共放了这么些文件。

至此,VSLite 的本地化工作已经初步完成,断网访问 localhost:5101,容器正常加载,创建一个 js 文件用 node 执行试试,也可以正常执行。

以上所有的改动都可以在 我的 GitHub 找到。

你可能会意识到,headless 里面还有 p.stackblitz.comt.staticblitz.comnr.staticblitz.com,这三个地址没有本地化,他们的作用是反向代理 Git 请求和 npm 请求(可能是为了跨域?),这部分请求无法简单通过静态 mock,经过实测,这不妨碍 WebContainers 加载,但会影响 git clonenpm install,有兴趣的朋友可以试试自己搭建 proxy 来解决这个问题!

VSLite 原理解析与本地化部署

https://www.imaegoo.com/2024/vslite-offline/

作者

iMaeGoo

发布于

2024-06-19

更新于

2024-06-20

许可协议

CC BY 4.0

评论

微信二维码