在瀏覽器中,把 Vite 跑起來了!
大家好,我是 ssh,前幾天在推上沖浪的時候,看到 Francois Valdy 宣布他制作了 browser-vite[1],成功把 Vite 成功在瀏覽器中運行起來了。這引起了我的興趣,如何把重度依賴 node 的一個 Vite 跑在瀏覽器上?接下來,就和我一起探索揭秘吧。
簡而言之的原理
- Service Worker[2]:用來取代 Vite 的 HTTP 服務器。
- Web Worker[3]:運行 browser-vite 來處理主線程。
- 文件系統(tǒng)被一個 in-memory 的模擬文件系統(tǒng)替代。
- 轉(zhuǎn)換特殊擴展名 (.ts, .tsx, .scss…) 的導入。
遇到的挑戰(zhàn)
沒有真正的文件系統(tǒng)
Vite[4] 用文件系統(tǒng)完成了很多工作。讀取項目的文件、監(jiān)聽文件改變、globs 的處理等等……在瀏覽器的模擬實現(xiàn)的內(nèi)存文件系統(tǒng)中,這些就很難實現(xiàn)了,所以 browser-vite 刪除了監(jiān)聽、globs 和配置文件來把復雜性降低。
項目文件被保存在內(nèi)存文件系統(tǒng)中,所以 broswer-vite 和 vite plugins 可以正常處理它們。
沒有 “node_modules”
Vite 依賴 node_modules 的存在來解析依賴。在啟動時會把他們預打包(Dependencing Pre-Bundling)[5]來優(yōu)化。
同樣為了降低復雜度,所以 broswer-vite 非常小心的從 Vite 中刪除了 node_modules 解析和依賴預打包。
所以使用 browser-vite 的用戶需要創(chuàng)建一個 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 內(nèi)部的處理細節(jié)。
如果感興趣的話,可以保持關注 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 提供的內(nèi)部頁面。
Service Worker - 瀏覽器內(nèi)的 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;
- }
- );
大多數(shù)情況下,對 "Vite Worker" 發(fā)送消息用的是 postMessage[11] 和 broadcast-channel[12]。
Vite Worker - 處理請求
Vite Worker是一個 Web Worker,它會處理 Service Worker 捕獲的請求。
創(chuàng)建 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 內(nèi)部中間件源碼[13] 獲取更多細節(jié)。
和 Stackblitz WebContainers 相比如何
["WebContainers"](https://blog.stackblitz.com/posts/introducing-webcontainers/ ""WebContainers""):在瀏覽器中運行 Node.js
Stackblitz 的 WebContainers 也可以在瀏覽器中運行Vite。你可以去優(yōu)雅的去 vite.new 擁有一個工作環(huán)境。
作者表示自己不是 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 是作者計劃的解決方案中的核心。打算逐步推廣到他們的全系列產(chǎn)品中:
- Backlight.dev
- Components.studio
- WebComponents.dev
- Replic.dev (即將發(fā)布的新應用)
展望未來,作者將繼續(xù)在 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 內(nèi)部中間件源碼: 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
本文轉(zhuǎn)載自微信公眾號「前端從進階到入院」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系前端從進階到入院公眾號。