在瀏覽器中,把 Vite 跑起來了!
首發於公眾號前端從進階到入院,歡迎關注。
大家好,我是 ssh,前幾天在推上衝浪的時候,看到 Francois Valdy 宣佈他製作了 browser-vite,成功把 Vite 成功在瀏覽器中執行起來了。這引起了我的興趣,如何把重度依賴 node 的一個 Vite 跑在瀏覽器上?接下來,就和我一起探索揭祕吧。
簡而言之的原理
-
Service Worker:用來取代 Vite 的 HTTP 伺服器。
-
Web Worker:執行 browser-vite 來處理主執行緒。
-
檔案系統被一個 in-memory 的模擬檔案系統替代。
-
轉換特殊副檔名 (.ts, .tsx, .scss…) 的匯入。
遇到的挑戰
沒有真正的檔案系統
Vite 用檔案系統完成了很多工作。讀取專案的檔案、監聽檔案改變、globs 的處理等等……在瀏覽器的模擬實現的記憶體檔案系統中,這些就很難實現了,所以 browser-vite 刪除了監聽、globs 和配置檔案來把複雜性降低。
專案檔案被儲存在記憶體檔案系統中,所以 broswer-vite 和 vite plugins 可以正常處理它們。
沒有 “node_modules”
Vite 依賴 node_modules
的存在來解析依賴。在啟動時會把他們預打包(Dependencing Pre-Bundling)來優化。
同樣為了降低複雜度,所以 broswer-vite 非常小心的從 Vite 中刪除了 node_modules
解析和依賴預打包。
所以使用 browser-vite 的使用者需要建立一個 Vite plugin 來解析裸模組匯入。
正則表示式“後行斷言”
Vite 中的一些程式碼用了後行斷言。在 Node.js 裡沒問題,但是 Safari 不支援。
所以作者重寫了這些正則。
熱更新(HMR)
Vite 用了 WebSockets 來在服務端(node)和客戶端(browser)之間同步程式碼變更。
在 browser-vite 中,服務端是 ServiceWorker + Vite worker,客戶端是 iframe。所以作者把 WebSockets 切換成了對 iframe 使用 post message。
如何使用
截止本文撰寫時間為止,這個工具還沒有做到開箱即用,如果想使用的話,需要閱讀很多 Vite 內部的處理細節。
如果感興趣的話,可以保持關注 browser-vite’s README 來獲取最新的使用方式。
安裝
安裝 browser-vite npm 包。
$ npm install --save browser-vite
或者
$ npm install --save [email protected]:browser-vite
來將 "vite" 的 import 改寫到 "browser-vite"
iframe - browser-vite 的視窗
需要一個 iframe 來顯示由 browser-vite 提供的內部頁面。
Service Worker - 瀏覽器內的 Web 伺服器
Service Worker 會捕獲到來自 iframe 的特定 url 請求。
一個使用 workbox 的例子:
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 和 broadcast-channel。
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 內部中介軟體原始碼 獲取更多細節。
和 Stackblitz 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 ———— 他們的包管理器。
WebContainers 也可以執行其他框架,如 Remix、SvelteKit 或 Astro。
這很神奇✨這是令人興奮的🤯 作者對 WebContainer 的團隊表示巨大的尊重,Stackblitz 團隊牛逼!
WebContainers 的一個缺點是,它目前只能在 Chrome 上執行,但可能很快就會在 Firefox 上執行。browser-vite 目前適用於 Chrome、Firefox和Safari瀏覽器。
簡而言之,WebContainers在較低的抽象級別上執行Vite。browser-vite在更高的抽象層次上執行,非常接近Vite本身。
打個比方,對於那些復古遊戲玩家來說,browser-vite 有點像 UltraHLE(任天堂 N64 模擬器)🕹️😊
(*) gametechwiki.com: 高/低層級模擬器
作者接下來的計劃
browser-vite 是作者計劃的解決方案中的核心。打算逐步推廣到他們的全系列產品中:
- Backlight.dev
- Components.studio
- WebComponents.dev
- Replic.dev (即將釋出的新應用)
展望未來,作者將繼續在 browser-vite 中投入,並向上遊報告。上個月他們還宣佈向 Evan You 和 Patak贊助來支援 Vite,以支援這個超讚的專案。
想知道更多?
- GitHub庫: browser-vite
- 加入 Discord, 有一個 #browser-vite 的頻道。🤗
參考資料
- https://divriots.com/blog/vite-in-the-browser
- https://github.com/divriots/browser-vite
- https://blog.stackblitz.com/posts/introducing-webcontainers/
- TypeScript 官方:JavaScript 中直接支援型別!
- 在瀏覽器中,把 Vite 跑起來了!
- 在瀏覽器中,把 Vite 跑起來了!
- Dan Abramov 接受油管 UP 主的面試挑戰,結果差點沒寫出來居中……?
- Nuxt 3 來了!
- 應用效能前端監控,位元組跳動這些年經驗都在這了
- 聽完玉伯的直播,我學到了這些。
- 從 umi 作者的視角再看 Babel,慢是它的大問題?
- ReScript 是什麼梗,更好的 TypeScript?
- 位元組跳動現代 Web 開發者問卷調查報告
- 前端設計稿轉程式碼現狀,會不會失業?
- 我的 CSS 不可能這麼可愛!
- 當下 React 專案該放棄的以及更好用的技術推薦
- 徹底搞懂觀察者模式
- 尤雨溪是如何做 Vue.js 自動化釋出的?
- 我的學習方法是每天看 10 個 NPM 模組?
- Vite 太快了,煩死了,是時候該小睡一會了。
- 如何實現比 setTimeout 快 80 倍的定時器?
- 尤雨溪:Vue3 考慮徹底放棄 IE 瀏覽器
- Vite 2.0 React Ant Design 4.0 搭建開發環境