大家好,我是 ssh,前几天在推上冲浪的时候,看到 Francois Valdy 宣布他制作了 browser-vite[1],成功把 Vite 成功在浏览器中运行起来了。这引起了我的兴趣,如何把重度依赖 node 的一个 Vite 跑在浏览器上?接下来,就和我一起探索揭秘吧。
简而言之的原理
- Service Worker[2]:用来取代 Vite 的 HTTP 服务器。
- Web Worker[3]:运行 browser-vite 来处理主线程。
- 文件系统被一个 in-memory 的模拟文件系统替代。
- 转换特殊扩展名 (.ts, .tsx, .scss…) 的导入。
遇到的挑战
没有真正的文件系统
Vite[4] 用文件系统完成了很多工作。读取项目的文件、监听文件改变、globs 的处理等等……在浏览器的模拟实现的内存文件系统中,这些就很难实现了,所以 browser-vite 删除了监听、globs 和配置文件来把复杂性降低。
项目文件被保存在内存文件系统中,所以 broswer-vite 和 vite plugins 可以正常处理它们。
没有 “node_modules”
Vite 依赖 node_modules
的存在来解析依赖。在启动时会把他们预打包(Dependencing Pre-Bundling)[5]来优化。
同样为了降低复杂度,所以 broswer-vite 非常小心的从 Vite 中删除了 node_modules
解析和依赖预打包。
所以使用 browser-vite 的用户需要创建一个 Vite plugin[6] 来解析裸模块导入。
正则表达式“后行断言”
Vite 中的一些代码用了后行断言[7]。在 Node.js 里没问题,但是 Safari 不支持。
所以作者重写了这些正则。
热更新(HMR)
Vite 用了 WebSockets[8] 来在服务端(node)和客户端(browser)之间同步代码变更。
在 browser-vite 中,服务端是 ServiceWorker + Vite worker,客户端是 iframe。所以作者把 WebSockets 切换成了对 iframe 使用 post message。
如何使用
截止本文撰写时间为止,这个工具还没有做到开箱即用,如果想使用的话,需要阅读很多 Vite 内部的处理细节。
如果感兴趣的话,可以保持关注 browser-vite’s README[9] 来获取最新的使用方式。
安装
安装 browser-vite npm 包。
$ npm install --save browser-vite
或者
$ npm install --save vite@npm:browser-vite
来将 "vite" 的 import 改写到 "browser-vite"
iframe - browser-vite 的窗口
需要一个 iframe 来显示由 browser-vite 提供的内部页面。
Service Worker - 浏览器内的 Web 服务器
Service Worker 会捕获到来自 iframe 的特定 url 请求。
一个使用 workbox[10] 的例子:
workbox.routing.registerRoute( /^https?:\/\/HOST/BASE_URL\/(\/.*)$/, async ({ request, params, url, }: import('workbox-routing/types/RouteHandler').RouteHandlerCallbackContext): Promise<Response> => { const req = request?.url || url.toString(); const [pathname] = params as string[]; // send the request to vite worker const response = await postToViteWorker(pathname) return response; } );
大多数情况下,对 "Vite Worker" 发送消息用的是 postMessage[11] 和 broadcast-channel[12]。
Vite Worker - 处理请求
Vite Worker是一个 Web Worker,它会处理 Service Worker 捕获的请求。
创建 Vite 服务器的示例:
import { transformWithEsbuild, ModuleGraph, transformRequest, createPluginContainer, createDevHtmlTransformFn, resolveConfig, generateCodeFrame, ssrTransform, ssrLoadModule, ViteDevServer, PluginOption } from 'vite'; export async function createServer = async () => { const config = await resolveConfig( { plugins: [ // virtual plugin to provide vite client/env special entries (see below) viteClientPlugin, // virtual plugin to resolve NPM dependencies, e.g. using unpkg, skypack or another provider (browser-vite only handles project files) nodeResolvePlugin, // add vite plugins you need here (e.g. vue, react, astro ...) ] base: BASE_URL, // as hooked in service worker // not really used, but needs to be defined to enable dep optimizations cacheDir: 'browser', root: VFS_ROOT, // any other configuration (e.g. resolve alias) }, 'serve' ); const plugins = config.plugins; const pluginContainer = await createPluginContainer(config); const moduleGraph = new ModuleGraph((url) => pluginContainer.resolveId(url)); const watcher: any = { on(what: string, cb: any) { return watcher; }, add() {}, }; const server: ViteDevServer = { config, pluginContainer, moduleGraph, transformWithEsbuild, transformRequest(url, options) { return transformRequest(url, server, options); }, ssrTransform, printUrls() {}, _globImporters: {}, ws: { send(data) { // send HMR data to vite client in iframe however you want (post/broadcast-channel ...) }, async close() {}, on() {}, off() {}, }, watcher, async ssrLoadModule(url) { return ssrLoadModule(url, server, loadModule); }, ssrFixStacktrace() {}, async close() {}, async restart() {}, _optimizeDepsMetadata: null, _isRunningOptimizer: false, _ssrExternals: [], _restartPromise: null, _forceOptimizeOnRestart: false, _pendingRequests: new Map(), }; server.transformIndexHtml = createDevHtmlTransformFn(server); // apply server configuration hooks from plugins const postHooks: ((() => void) | void)[] = []; for (const plugin of plugins) { if (plugin.configureServer) { postHooks.push(await plugin.configureServer(server)); } } // run post config hooks // This is applied before the html middleware so that user middleware can // serve custom content instead of index.html. postHooks.forEach((fn) => fn && fn()); await pluginContainer.buildStart({}); await runOptimize(server); return server; }
通过 browser-vite 处理请求的伪代码:
import { transformRequest, isCSSRequest, isDirectCSSRequest, injectQuery, removeImportQuery, unwrapId, handleFileAddUnlink, handleHMRUpdate, } from 'vite/dist/browser'; ... async (req) => { let { url, accept } = req const html = accept?.includes('text/html'); // strip ?import url = removeImportQuery(url); // Strip valid id prefix. This is prepended to resolved Ids that are // not valid browser import specifiers by the importAnalysis plugin. url = unwrapId(url); // for CSS, we need to differentiate between normal CSS requests and // imports if (isCSSRequest(url) && accept?.includes('text/css')) { url = injectQuery(url, 'direct'); } let path: string | undefined = url; try { let code; path = url.slice(1); if (html) { code = await server.transformIndexHtml(`/${path}`, fs.readFileSync(path,'utf8')); } else { const ret = await transformRequest(url, server, { html }); code = ret?.code; } // Return code reponse } catch (err: any) { // Return error response } }
查看 Vite 内部中间件源码[13] 获取更多细节。
和 Stackblitz WebContainers 相比如何
["WebContainers"](https://blog.stackblitz.com/posts/introducing-webcontainers/ ""WebContainers""):在浏览器中运行 Node.js
Stackblitz 的 WebContainers 也可以在浏览器中运行Vite。你可以去优雅的去 vite.new 拥有一个工作环境。
作者表示自己不是 WebContainers 方面的专家,但简而言之,browser-vite 在 Vite 级别上模拟了 FS 和 HTTPS 服务器,WebContainers 在 Node.js 级别上模拟了 FS 和其他很多东西,而 Vite 只需做一些额外的修改就可在上面运行。
它可以将 node_modules 存储在浏览器的 WebContainer 中。但它不会直接运行 npm 或 yarn,可能是因为会占用太多空间。他们将这些命令链接到 Turbo[14] ———— 他们的包管理器。
WebContainers 也可以运行其他框架,如 Remix[15]、SvelteKit[16] 或 Astro[17]。
这很神奇✨这是令人兴奋的🤯 作者对 WebContainer 的团队表示巨大的尊重,Stackblitz 团队牛逼!
WebContainers 的一个缺点是,它目前只能在 Chrome 上运行[18],但可能很快就会在 Firefox 上运行[19]。browser-vite 目前适用于 Chrome、Firefox和Safari浏览器。
简而言之,WebContainers在较低的抽象级别上运行Vite。browser-vite在更高的抽象层次上运行,非常接近Vite本身。
打个比方,对于那些复古游戏玩家来说,browser-vite 有点像 UltraHLE(任天堂 N64 模拟器)🕹️😊
(*) gametechwiki.com: 高/低层级模拟器[20]
作者接下来的计划
browser-vite 是作者计划的解决方案中的核心。打算逐步推广到他们的全系列产品中:
- Backlight.dev
- Components.studio
- WebComponents.dev
- Replic.dev (即将发布的新应用)
展望未来,作者将继续在 browser-vite 中投入,并向上游报告。上个月他们还宣布向 Evan You 和 Patak赞助来支持 Vite[21],以支持这个超赞的项目。
想知道更多?
- GitHub库:browser-vite[22]
- 加入 Discord[23], 有一个 #browser-vite 的频道。🤗
参考资料
- https://divriots.com/blog/vite-in-the-browser
- https://github.com/divriots/browser-vite
- https://blog.stackblitz.com/posts/introducing-webcontainers/
参考资料
[1]
browser-vite: https://github.com/divriots/browser-vite
[2]
Service Worker: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
[3]
Web Worker: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
[4]
Vite: https://vitejs.dev/
[5]
预打包(Dependencing Pre-Bundling): https://vitejs.dev/guide/dep-pre-bundling.html
[6]
Vite plugin: https://vitejs.dev/guide/api-plugin.html
[7]
后行断言: https://www.regular-expressions.info/lookaround.html
[8]
WebSockets: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API
[9]
browser-vite’s README: https://github.com/divriots/browser-vite/blob/browser-vite/README.md#usage
[10]
workbox: https://developers.google.com/web/tools/workbox
[11]
postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
[12]
broadcast-channel: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
[13]
Vite 内部中间件源码: https://github.com/vitejs/vite/tree/main/packages/vite/src/node/server/middlewares
[14]
Turbo: https://developer.stackblitz.com/docs/platform/turbo/
[15]
Remix: https://blog.stackblitz.com/posts/remix-runs-on-webcontainers/
[16]
SvelteKit: https://blog.stackblitz.com/posts/sveltekit-supported-in-webcontainers/
[17]
Astro: https://blog.stackblitz.com/posts/astro-support/
[18]
只能在 Chrome 上运行: https://developer.stackblitz.com/docs/platform/browser-support
[19]
在 Firefox 上运行: https://developer.stackblitz.com/docs/platform/browser-support/#testing-on-firefox
[20]
gametechwiki.com: 高/低层级模拟器: https://emulation.gametechwiki.com/index.php/High/Low_level_emulation
[21]
向 Evan You 和 Patak赞助来支持 Vite: https://divriots.com/blog/supporting-vitejs
[22]
browser-vite: https://github.com/divriots/browser-vite
[23]
Discord: https://discord.gg/XkQxSU9