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

简介: 大家好,我是 ssh,前几天在推上冲浪的时候,看到 Francois Valdy 宣布他制作了 browser-vite[1],成功把 Vite 成功在浏览器中运行起来了。这引起了我的兴趣,如何把重度依赖 node 的一个 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 的频道。🤗

参考资料


参考资料

[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

相关文章
|
5天前
|
Go
cypress里浏览器里的相关操作有哪些?
cypress里浏览器里的相关操作有哪些?
|
5月前
|
Web App开发 JavaScript 前端开发
从浏览器原理出发聊聊Chrome插件
本文从浏览器架构演进、插件运行机制、插件基本介绍和一些常见的插件实现思路几个方向聊聊Chrome插件。
792 0
|
8月前
|
数据采集 Web App开发 JavaScript
Puppeteer无头浏览器:开启自动化之门,掌握浏览器世界的无限可能
大概还是入门期,我曾用Puppeteer做爬虫工具以此来绕过某网站的防爬机制。近期有需求要做任意链接网页截图,像这种场景非常适合用Puppeteer完成。无头浏览器我已知的还有Selenium。
184 2
Puppeteer无头浏览器:开启自动化之门,掌握浏览器世界的无限可能
|
9月前
Vite 如何兼容老版本浏览器,解决浏览器无报错但打开空白
最近一个医院的项目,遇到有些电脑能访问web页面,有些电脑无法访问。最后发现是浏览器版本不一样,老的浏览器版本不能正常访问,而新的浏览器可以访问。因为医院是内网环境,电脑数量又比较多,所以没办法一一去升级浏览器版本。
493 0
|
10月前
|
Web App开发 JSON Unix
浏览器:好用的浏览器插件,亲测好用
浏览器:好用的浏览器插件,亲测好用
130 0
|
11月前
|
Web App开发 开发者
谈一谈|脚本—丰富你的浏览器
谈一谈|脚本—丰富你的浏览器
73 0
|
JavaScript 前端开发
electron 10行核心代码定制自己的浏览器
electron 10行核心代码定制自己的浏览器
527 0
electron 10行核心代码定制自己的浏览器
|
Web App开发
【浏览器】chrome 打包分享自己安装的插件
【浏览器】chrome 打包分享自己安装的插件
214 1
【浏览器】chrome 打包分享自己安装的插件
|
设计模式 前端开发 测试技术
web自动化多次打开浏览器嫌烦?打开一次浏览器,pytest有个招
web自动化多次打开浏览器嫌烦?打开一次浏览器,pytest有个招
web自动化多次打开浏览器嫌烦?打开一次浏览器,pytest有个招
|
JavaScript 前端开发 开发者
介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)
介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)
315 0
介绍一下主流的浏览器的开发者工具(js调试和查看网络请求)