好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

在浏览器中,把 Vite 跑起来了!

大家好,我是 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

原文链接:https://mp.weixin.qq.com/s/MOR4zCtEoSg3jhc9igHgNQ

查看更多关于在浏览器中,把 Vite 跑起来了!的详细内容...

  阅读:42次