使用 Devframe 构建跨框架开发者工具
使用Devframe构建跨框架开发工具:只需定义一次工具,即可将标准请求处理器和WebSocket接入Vite、Hono、Next.js等环境。
Devframe 是一个采用 MIT 许可证、与框架无关的开发者工具基础层:你只需用 defineDevframe() 描述一次工具,initDevframe() 便会将该定义转换为一个符合 Web 标准的请求处理器(request handler),任何兼容的宿主环境都可以挂载它。
如果你维护着一个库,很可能想过为它做一个检查面板,类似 React Developer Tools,只不过面向的是你自己库的内部机制。然而,兼容性矩阵随之而来:Vite 插件、Next.js 路由、Hono 中间件……每一种都需要编写各自的服务端胶水代码。
本文将构建一个小型 Devframe 工具,将其转换为处理器,在 Vite 和 Hono 中挂载同一份定义,然后介绍其余的宿主环境和打包好的适配器。最后,我们会讨论在哪些场景下浏览器扩展是更简单的选择。
核心要点
devframe中的defineDevframe()用于一次性描述工具。devframe/initiate中的initDevframe(def, { base })将其转换为一个运行中的实例,并暴露handler和nodeMiddleware。- Hono、Nitro、Next.js 和 SvelteKit 挂载
devtools.handler,它接收一个 Web 标准的Request并返回一个Response。Vite 和 Rsbuild 挂载devtools.nodeMiddleware。 - 运行中的 devframe 不会自行打开端口。由宿主决定 RPC WebSocket 的位置:共享服务器、
attach()或ws: { sidecar: true }。 - Devframe 默认启用访问控制。
auth: false仅适用于 localhost 上的单用户场景。 - Next.js 和 SvelteKit 通过名为
%5F_my-tool的路由文件夹来提供/__my-tool/路径,并且两者都需要 side-car WebSocket。
Devframe 解决了什么问题?
Devframe 解决的是”每个框架一个插件”的问题:如果没有共享层,每个开发者工具都必须为每个宿主框架单独提供一套集成。Devframe 在工具与其运行环境之间放置了一个统一的标准处理器,从而消除了这种重复。v1.0 发布公告将该处理器视为工具的边界。UI、连接信息、实时 RPC、鉴权以及可选的 MCP 端点都位于它之后,统一挂在同一个路径前缀下。
由于处理器只处理 Request 和 Response,工具便不再依赖任何特定开发服务器的 API。v1.0 版本稳定了公共接口,源代码托管在 devframes/devframe 仓库中,采用 MIT 许可证。
如何定义一个 Devframe 工具?
Devframe 定义是由 defineDevframe() 返回的一个普通对象。它为工具命名,并在 setup(ctx) 中注册 RPC 函数。示例工具只列出环境变量的名称,从不列出它们的值。
安装说明请参阅 Devframe 文档。我们从定义开始:
// my-tool.ts
import { defineDevframe } from 'devframe'
import { listEnvNames } from './rpc'
export default defineDevframe({
id: 'my-tool',
name: 'Env Names',
// Required metadata and the browser entry are omitted here.
// See the Devframe docs for the full definition.
setup(ctx) {
ctx.scope('my-tool').rpc.register(listEnvNames)
},
})
在 rpc.ts 中,将 listEnvNames 导出为一个 RPC 函数,其处理函数返回 Object.keys(process.env).sort()。包装器的结构及其选项在 Devframe 定义指南中有详细说明。该指南还列出了完整定义所需的字段:id、name、version、packageName、homepage、description 和 setup 均为必填项,此外还应传入 importMetaUrl: import.meta.url。指南同时说明了作用域上下文(scoped context)会以 devframe 的 id 作为 RPC id 的命名空间。
只返回名称是一个有意为之的数据暴露策略。鉴权机制保护的是面板本身,但一个从不读取变量值的函数,无论最终是谁打开了面板,都不可能泄露这些值。
将定义转换为处理器
initDevframe() 将定义转换为一个运行中的实例,其 handler 在单一基础路径下提供整个工具。传入定义和 base 即可:
// devtools.ts
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'
export const devtools = initDevframe(myDevframe, { base: '/__my-tool/' })
标准处理器文档说明,一个挂载基础路径即可覆盖整个工具:处理器负责提供 SPA、__connection.json 发现文件、RPC socket、鉴权网关以及 MCP 路由。base 选项为必填项。handler 和 nodeMiddleware 都会在内部等待实例就绪,因此可以立即挂载。该实例还暴露了 attach、handleUpgrade 和 close(),用于控制 socket 及其生命周期。
在 Vite 中挂载
要在 Vite 中挂载 Devframe 工具,需在 configureServer 钩子中使用 devtools.nodeMiddleware,因为 Vite 使用的是 Connect 风格的中间件。在钩子内部创建实例,以便它可以共享 Vite 的 HTTP 服务器:
// vite.config.ts
import { defineConfig } from 'vite'
import { initDevframe } from 'devframe/initiate'
import myDevframe from './my-tool'
export default defineConfig({
plugins: [
{
name: 'my-tool',
apply: 'serve',
configureServer(server) {
const devtools = initDevframe(myDevframe, {
base: '/__my-tool/',
server: server.httpServer ?? undefined,
})
server.middlewares.use(devtools.nodeMiddleware)
},
},
],
})
apply: 'serve' 确保面板不会进入生产构建。server 选项将 RPC WebSocket 放在 Vite 自身的 HTTP 服务器上,因此无需额外开放端口。Rsbuild 同样接受 nodeMiddleware,但它的处理器永远不会收到 upgrade 请求,因此需要使用 ws: { sidecar: true } 来提供 socket。
在 Hono 中挂载同一个工具
Hono 原生处理 Web 标准请求,因此只需将原始请求传给 devtools.handler,并将 WebSocket 附加到 Node 服务器上。这里复用了前面的 devtools.ts 模块,只有胶水代码发生了变化:
// server.ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { devtools } from './devtools'
const app = new Hono()
app.all('/__my-tool/*', c => devtools.handler(c.req.raw))
devtools.attach(serve({ fetch: app.fetch, port: 3000 }))
app.all 通配路由只是一个普通路由,其作用域划分思路与编写自定义 Hono 中间件时相同。c.req.raw 是底层的 Web 标准 Request。fetch 处理器只能看到单个请求,而运行中的 devframe 不绑定任何端口,因此 attach() 会将 Node 服务器的 upgrade 事件转交给 devframe 的 socket。如果不调用它,UI 可以正常加载,但实时 RPC 将无法建立连接。
其他宿主环境与打包适配器
所有 Devframe 宿主都遵循同一条规则:Web 标准宿主挂载 handler,Connect 风格的开发服务器挂载 nodeMiddleware,而路由处理器无法接收 upgrade 请求的宿主则使用 side-car WebSocket。
| 宿主 | 挂载方式 | 路由 / 文件 | WebSocket |
|---|---|---|---|
| Vite | 通过 server.middlewares.use 挂载 nodeMiddleware | configureServer 插件 | server: server.httpServer |
| Hono | handler(c.req.raw) | app.all('/__my-tool/*', …) | devtools.attach(serve(...)) |
| Nitro | handler(event.req) | routes/__my-tool/[...path].ts 加上 routes/__my-tool/index.ts | ws: { sidecar: true } |
| Next.js | export const GET = devtools.handler | app/%5F_my-tool/[[...path]]/route.ts | ws: { sidecar: true } |
| SvelteKit | GET = ({ request }) => devtools.handler(request) | src/routes/%5F_my-tool/[...path]/+server.ts | ws: { sidecar: true } |
Next.js 会将以下划线开头的文件夹视为私有文件夹。因此,通过 /__my-tool/ 提供服务的路由需要放在名为 %5F_my-tool 的文件夹中,该名称解码后即为 __my-tool。在开发期间会重新加载模块的宿主(如 Next.js、Nitro 和 SvelteKit)应将实例缓存在 globalThis 上,避免每次重载都创建新的 devframe。initiate 文档页面展示了这种缓存的写法,其类型为 ReturnType<typeof initDevframe>。
同一份定义也可以脱离宿主单独打包。以下方式均列在适配器索引中:
- 独立 CLI: 来自
devframe/adapters/cac的createCac()。需要安装可选的cacpeer 依赖。 - 开发服务器: 来自
devframe/adapters/dev的createDevServer()。 - 静态构建: 来自
devframe/adapters/build的createBuild(),用于生成快照报告。 - MCP 服务器: 来自
devframe/adapters/mcp的createMcpServer(),供编程智能体(coding agent)使用。除非显式启用,RPC 函数对智能体不可见。
仓库的 examples/ 文件夹中提供了多个宿主的可运行参考项目。
保持鉴权网关开启
Devframe 默认启用访问控制。当未经授权的浏览器请求访问时,终端会打印一个验证码和一个魔法链接(magic link)。Devframe 从不根据原始 Host 请求头或转发请求头推断魔法链接的 origin,因此任何非回环地址的部署都必须显式设置 origin。虽然存在 auth: false 选项,但它仅适用于 localhost 上的单用户场景。
何时值得使用 Devframe,何时浏览器扩展更简单?
当工具需要服务端状态并且必须在多个宿主中运行时,Devframe 的价值最为明显。如果工具只是在某一个框架中检查页面,那么浏览器扩展的实现成本更低。
以下情况选择 Devframe:
- 工具需要读取 Node 端状态,例如构建图、配置、进程环境变量或文件系统;
- 库被多个框架或开发服务器使用;
- 希望同一个工具能以 CLI、静态 CI 报告或 MCP 服务器的形式发布。
以下情况选择浏览器扩展:
- 工具只检查页面中已有的内容,例如 DOM 或客户端 store;
- 只针对单一框架的运行时;
- 不需要获取服务端数据。
总结
借助 Devframe,跨框架支持归结为每个宿主几行挂载代码。你只需将工具编写为一份定义,initDevframe() 将其转换为标准处理器,各个宿主再将该处理器接入各自的路由和 WebSocket。接下来,请参照 Devframe 定义指南补全必需的元数据和浏览器入口,然后先在用户最常用的宿主中挂载,再根据上表逐步添加其他宿主。
常见问题
如何将 Devframe RPC 函数暴露给编程智能体?
在 defineRpcFunction 中添加带有 description 的 agent 字段,例如 agent: { description: 'Returns a summary' }。agent 字段会开启严格的 JSON 序列化,因为 MCP 只接受 JSON 结构的数据;若将其与 jsonSerializable: false 同时使用,会抛出 DF0019 错误。使用 devframe/adapters/mcp 中的 createMcpServer(devframe, { transport: 'stdio' }) 来提供该定义,这需要安装可选的 @devframes/agentic peer 依赖。没有 agent 字段的函数仅作为内部 RPC 使用。
可以在不使用 Vite 的情况下或在 CommonJS 项目中使用 Devframe 吗?
可以。Devframe 不依赖 Vite,因此可以运行在任何 Web 标准宿主(如 Hono)中,也可以通过 createDevServer 或 createCac 作为独立工具运行。它仅以 ESM 格式发布,因此 CommonJS 代码库需要通过 ESM import 或动态 import() 调用来加载它。如果在使用 CLI 适配器时未安装可选的 cac peer 依赖,错误会在导入时出现。
除了 static 之外,Devframe 还支持哪些 RPC 函数类型?
Devframe RPC 函数使用以下四种类型之一:query、static、action 和 event。只有 static 和 query 函数可以携带用于存放预计算结果的 dump 字段。为 action 或 event 函数添加 dump 会抛出校验错误。在 args 中声明的参数会在处理函数运行前,通过任意 Standard Schema 校验器(如 Valibot)进行校验。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k