UnoCSS Inspector 在 Next.js 中的落地:用 @unocss/postcss + Devframe 在纯 Next.js 应用中托管 UnoCSS 检查器

UnoCSS Inspector 在 Next.js 中的落地:用 @unocss/postcss + Devframe 在纯 Next.js 应用中托管 UnoCSS 检查器 UnoCSS Inspector 在 Next.js 中的落地用 unocss/postcss Devframe 在纯 Next.js 应用中托管 UnoCSS 检查器【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss本篇以仓库示例 examples/inspector-next 及其 README 为主体讲解如何在不引入 Vite 的前提下通过unocss/postcss让 UnoCSS 在 Next.js 中生成样式再用devframes/next把 UnoCSS Inspector 作为开发期工具面板devframe挂载到 Next.js 应用自身的路由上。读完本文你将掌握这条「PostCSS Devframe」集成链路的工作原理catch-all 路由如何构建独立 UnoCSS 上下文、一次性扫描如何工作、以及一次性解锁码等安全机制从何而来。1. 这个示例解决什么问题UnoCSS 生态中最常见的 Inspector 宿主是 Vite通过unocss插件挂载。但在纯 Next.js 项目中UnoCSS 通常通过 PostCSS 集成 接入应用本身并不存在一个可被插件直接驱动的 UnoCSS 插件上下文。examples/inspector-next演示的正是这条替代路径样式生成走unocss/postcss由 Next.js 内置的 CSS 处理管线触发Inspector 的 SPA 与 RPC 服务由 Next.js 应用的一个 catch-all 路由托管宿主是第三方框架层devframes/nextREADME 中称之为 devframe 托管方案全程不涉及 Vite。README 明确强调这一点Next.js app using UnoCSS throughunocss/postcss, hosting the inspector withdevframes/next— no Vite involved.2. 快速上手仓库给出的运行方式非常简单READMEpnpm install pnpm dev然后打开http://localhost:3000/__unocss/并输入Next dev 终端中打印的一次性解锁码才能进入 Inspector。README 还给出了一个关键限制说明Note: the standalone context scans the project once at startup — restart the dev server (or re-save the route file) to re-scan after adding new utilities.即独立上下文只在启动时扫描项目一次新增工具类后需要重启 dev server或重新保存路由文件触发重建才会重新扫描。这一点在 5 节的源码分析中能看到具体成因。3. 项目结构与关键文件示例项目结构紧凑核心文件各司其职文件职责postcss.config.mjs注册unocss/postcss插件并指定content扫描范围uno.config.tsUnoCSS 配置presetWind3dark: media与btnshortcutnext.config.ts通过withDevframe包装 Next 配置并将 devframe 相关包设为外部依赖tsconfig.json配置/*路径别名/uno.config因此可被路由直接 importapp/%5F_unocss/[[...path]]/route.tsInspector 的 catch-all 路由目录名%5F是_的 URL 编码写法app/page.tsx演示页使用原子类与btnshortcutapp/globals.css仅一行unocss all;作为 UnoCSS 的 CSS 入口依赖方面package.json生产依赖是next 16.1.2/react 19.2.3开发依赖中通过 monorepolink:协议引用了unocss/core、unocss/postcss、unocss/inspector、unocss/preset-wind3与unocss以及 devframe 宿主包devframes/next。3.1 样式生成侧PostCSS 配置postcss.config.mjs 完整内容const config { plugins: { unocss/postcss: { content: [./app/**/*.{html,js,ts,jsx,tsx}], }, }, } export default configcontent字段限定工具类提取范围为app目录下的源码文件app/globals.css 中的unocss all;指令则告诉插件在此处输出全部生成的原子类。页面 app/page.tsx 中实际使用的类如h-screen flex flex-col items-center、op-60、hover:op-50以及btn这个 shortcut展开为px-4 py-2 rounded bg-sky-600 text-white hover:bg-sky-700 cursor-pointer定义于 uno.config.ts都会进入 Inspector 可检查的范围。uno.config.ts中还有一个值得注意的工程细节——注释明确写道// Import presets from their own packages (instead of the unocss root // export) so the Next bundler doesnt pull in Vite-only transformers const config: UserConfig { presets: [ presetWind3({ dark: media, }), ], shortcuts: { btn: px-4 py-2 rounded bg-sky-600 text-white hover:bg-sky-700 cursor-pointer, }, } export default config即从unocss/preset-wind3而不是unocss根包导入 preset避免 Next 打包器把仅适用于 Vite 的 transformer 一并打进 bundle。4. 核心机制catch-all 路由托管 Inspector整个方案的枢纽是路由文件app/%5F_unocss/[[...path]]/route.ts完整源码import process from node:process import { createDevframeNextHandler } from devframes/next/single import { createStandaloneInspectorDevframe } from unocss/inspector/devframe import config from /uno.config export const runtime nodejs export const dynamic force-dynamic // Build a standalone UnoCSS context by scanning the project files once at // startup, then host the inspector devframe from this catch-all route. // The RPC WebSocket runs on a side-car port advertised through // /__unocss/__connection.json, gated by devframes one-time-code auth // (the code is printed in the Next dev terminal). const handlerPromise createStandaloneInspectorDevframe({ root: process.cwd(), // Pass the config inline (configFile: false skips the file loader, // which doesnt play well with the Next bundler) config: { ...config, configFile: false }, patterns: [app/**/*.{ts,tsx,html}], // (cast: pnpm may instantiate devframe twice across the linked monorepo) }).then(({ definition }) createDevframeNextHandler(definition as any)) export async function GET(request: Request) { return (await handlerPromise).fetch(request) }逐段拆解这段代码的设计意图runtime nodejs与dynamic force-dynamic路由必须运行在 Node.js runtime 且禁用静态缓存——因为 Inspector 需要在服务端持有内存中的 UnoCSS 上下文并处理 WebSocket/RPC 动态请求不能被边缘 runtime 或静态预渲染接管。handlerPromise是模块级单例createStandaloneInspectorDevframe(...)的 Promise 在模块加载时就创建所有GET请求共享同一个 handler。这保证了「启动时扫描一次」只发生一次后续的 SPA 静态资源、RPC 连接都复用同一个 devframe handler。内联配置而非文件路径config: { ...config, configFile: false }直接把uno.config.ts以对象形式传入并置configFile: false跳过文件加载器。源码注释解释了原因Next 的打包器对运行时读取配置文件这条路径不友好bundling 后import.meta.url等路径语义会被改写因此改为在编译期把配置对象内联进 bundle再由unocss/inspector的上下文创建逻辑消费。patterns收窄扫描范围[app/**/*.{ts,tsx,html}]只覆盖 Next.js 的app目录对比 5 节中createStandaloneInspectorDevframe的默认 patterns 会扫全部常见源码扩展名。as any强制断言的原因源码注释写明「pnpm may instantiate devframe twice across the linked monorepo」——在 monorepo link 场景下devframe包可能被实例化两份导致类型不兼容故对definition做断言。README 中提到的 side-car 架构RPC WebSocket 运行在一个独立端口side-car客户端通过/__unocss/__connection.json获取连接信息且受 devframe 的「一次性码」鉴权保护——这就是打开http://localhost:3000/__unocss/后需要输入终端打印的解锁码的原因。5. 源码纵深createStandaloneInspectorDevframe做了什么路由调用的createStandaloneInspectorDevframe实现在 packages-integrations/inspector/src/devframe.ts这是理解整个方案行为边界的关键export async function createStandaloneInspectorDevframe(options: StandaloneInspectorOptions {}): PromiseUnocssInspectorDevframe { const { root process.cwd(), config, defaults {}, patterns DEFAULT_PATTERNS, } options const ctx createContext(config, defaults) await ctx.updateRoot(root) await ctx.ready const files await glob(patterns, { cwd: root, absolute: true }) await Promise.all(files.map(async (file) { try { const code readFileSync(file, utf-8) await ctx.extract(code, file) } catch {} })) await ctx.flushTasks() return createInspectorDevframe(ctx) }从源码结构看它的工作流程是创建独立上下文createContext(config, defaults)依据传入的内联配置构建一个不与任何 bundler 绑定的 UnoCSS 插件上下文updateRoot定位项目根目录示例中即process.cwd()与 postcss 的扫描根一致。一次性全量扫描用tinyglobby对patterns做 glob逐文件readFileSyncctx.extract(code, file)提取工具类最后ctx.flushTasks()等待提取任务完成。这里没有任何 watch/fs 监听逻辑——这正解释了 README 中「新增工具类后需重启 dev server 重新扫描」的说明standalone 模式是快照式的。包装为 devframecreateInspectorDevframe(ctx)把上下文包装成可挂载的 devframe 定义id: unocss、basePath: /__unocss/供 Next 宿主通过createDevframeNextHandler转为 Next.js 路由 handler。StandaloneInspectorOptions的完整选项devframe.ts选项说明root要扫描的项目根目录默认process.cwd()configUnoCSS 配置对象或配置文件路径省略时自动从 root 加载defaults传给上下文的配置默认值UserConfigDefaultspatterns提取工具类的 glob 模式相对于 root未显式指定patterns时默认的DEFAULT_PATTERNSdevframe.ts覆盖了绝大多数前端源码扩展名并排除构建产物const DEFAULT_PATTERNS [ **/*.{html,vue,svelte,astro,jsx,tsx,js,ts,mdx,md,marko,pug,elm,php,phtml}, !**/node_modules/**, !**/dist/**, !**/.next/**, !**/.nuxt/**, !**/.output/**, ]示例项目主动把patterns收窄为app/**/*.{ts,tsx,html}与unocss/postcss的content范围保持同一语义app目录使 Inspector 看到的工具集合与页面实际生成的 CSS 一致。5.1 变更信号如何到达 Inspector 客户端同一文件中createInspectorDevframe还展示了 devframe 的通信设计devframe.tsdevframe 定义声明了devframes/service-shiki服务用于服务端语法高亮覆盖 css/html/js/ts/vue/jsx/tsx并在setup阶段注册 Inspector 的 RPC 函数与一个名为changes的 devframe shared state。源码注释解释了选择 shared state 而非自定义广播的理由Change signals ride a devframe shared state (changes) rather than custom broadcasts: every host holds one, mutating it bumps a revision the client watches, and a reconnecting client gets the latest snapshot for free.即任何宿主变更都会 bumprevision客户端监视该值刷新断线重连的客户端则免费获得最新快照。返回值中的notifyModuleUpdated/notifyConfigChanged/notifyInvalidated三个信号函数供宿主在模块热更、配置重载、CSS 失效时通知已连接的 Inspector——在 standalone 模式下这些信号虽少被触发没有 bundler 热更事件但机制本身保持一致。此外resolveClientDist()devframe.ts处理了 Next/Turbopack 改写import.meta.url的问题优先使用模块相对路径定位 Inspector SPA 的构建产物失败时回退到经由消费项目node_modules中unocss/inspector/package.json反查dist/client。6.next.config.ts让 devframe 宿主正确工作next.config.ts 完整内容import type { NextConfig } from next import { withDevframe } from devframes/next/single const nextConfig: NextConfig { // Keep the devframe host out of the bundle — its optional MCP adapter // imports peer packages that are lazily loaded at runtime only serverExternalPackages: [devframe, devframes/next, unocss/inspector], } // Sets skipTrailingSlashRedirect so the inspector SPAs relative assets // resolve under /__unocss/ export default withDevframe({ ...nextConfig })两处配置都带有明确注释对应两个真实的坑serverExternalPackagesdevframe、devframes/next、unocss/inspector三个包被声明为服务端外部依赖不参与 bundle——devframe 宿主的可选 MCP adapter 会懒加载 peer 包若被打进 bundle 会在构建/运行时出问题。withDevframe包装它设置了skipTrailingSlashRedirect保证 Inspector SPA 的相对资源路径能正确解析在/__unocss/前缀下与 devframe 定义中的basePath: /__unocss/对应否则 Next 的尾斜杠重定向会破坏 SPA 静态资源的相对引用。7. 与 Vite 版 Inspector 示例的对照仓库同时提供 examples/inspector-vite 作为对照Vite 集成下 Inspector 由unocss插件直接挂载享有文件系统热更信号而本文的 Next.js 方案以「一次性快照扫描 devframe 路由托管」换取了对非 Vite 构建栈的支持。二者的能力取舍可以概括为Vite 集成上下文由 bundler 插件持续驱动模块热更与工具类变化实时反映本文的 standalone 集成上下文启动时扫描一次无热更信号代价是修改工具类后需重启 dev server 重新扫描README 已明确说明此限制换取的是任何能跑 Node runtime 路由的 Next.js 应用都能原生托管 Inspector。8. 小结examples/inspector-next展示了一条完整的非 Vite 链路unocss/postcss负责样式生成postcss.config.mjs unocss all;createStandaloneInspectorDevframe负责构建独立 UnoCSS 上下文并一次性扫描devframe.tscreateDevframeNextHandlerwithDevframe负责把 devframe 挂进 Next 路由next.config.ts而 catch-all 路由app/%5F_unocss/[[...path]]/route.ts以模块级单例 Promise 承载全部请求。理解了这套机制你可以在任何纯 Next.js 项目中复刻「PostCSS 出样式、应用自身路由出 Inspector」的组合并正确应对内联配置、serverExternalPackages、尾斜杠解析与「启动时扫描一次」这几个关键约束。【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考