news 2026/9/13 21:26:17

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/next(README 中称之为 devframe 托管方案);
  • 全程不涉及 Vite。

README 明确强调这一点:

Next.js app using UnoCSS through@unocss/postcss, hosting the inspector with@devframes/next— no Vite involved.

2. 快速上手

仓库给出的运行方式非常简单(README):

pnpm 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')与btnshortcut
next.config.ts通过withDevframe包装 Next 配置,并将 devframe 相关包设为外部依赖
tsconfig.json配置@/*路径别名(@/uno.config因此可被路由直接 import)
app/%5F_unocss/[[...path]]/route.tsInspector 的 catch-all 路由(目录名%5F_的 URL 编码写法)
app/page.tsx演示页,使用原子类与btnshortcut
app/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-wind3unocss,以及 devframe 宿主包@devframes/next

3.1 样式生成侧:PostCSS 配置

postcss.config.mjs 完整内容:

const config = { plugins: { '@unocss/postcss': { content: ['./app/**/*.{html,js,ts,jsx,tsx}'], }, }, } export default config

content字段限定工具类提取范围为app目录下的源码文件;app/globals.css 中的@unocss all;指令则告诉插件在此处输出全部生成的原子类。页面 app/page.tsx 中实际使用的类如h-screen flex flex-col items-centerop-60hover: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 doesn't 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 devframe's 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 doesn't 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) }

逐段拆解这段代码的设计意图:

  1. runtime = 'nodejs'dynamic = 'force-dynamic':路由必须运行在 Node.js runtime 且禁用静态缓存——因为 Inspector 需要在服务端持有内存中的 UnoCSS 上下文,并处理 WebSocket/RPC 动态请求,不能被边缘 runtime 或静态预渲染接管。

  2. handlerPromise是模块级单例createStandaloneInspectorDevframe(...)的 Promise 在模块加载时就创建,所有GET请求共享同一个 handler。这保证了「启动时扫描一次」只发生一次,后续的 SPA 静态资源、RPC 连接都复用同一个 devframe handler。

  3. 内联配置而非文件路径config: { ...config, configFile: false }直接把uno.config.ts以对象形式传入,并置configFile: false跳过文件加载器。源码注释解释了原因:Next 的打包器对运行时读取配置文件这条路径不友好(bundling 后import.meta.url等路径语义会被改写),因此改为在编译期把配置对象内联进 bundle,再由@unocss/inspector的上下文创建逻辑消费。

  4. patterns收窄扫描范围['app/**/*.{ts,tsx,html}']只覆盖 Next.js 的app目录(对比 5 节中createStandaloneInspectorDevframe的默认 patterns 会扫全部常见源码扩展名)。

  5. as any强制断言的原因:源码注释写明「pnpm may instantiate devframe twice across the linked monorepo」——在 monorepo link 场景下,devframe包可能被实例化两份,导致类型不兼容,故对definition做断言。

  6. 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 = {}): Promise<UnocssInspectorDevframe> { 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) }

从源码结构看,它的工作流程是:

  1. 创建独立上下文createContext(config, defaults)依据传入的(内联)配置构建一个不与任何 bundler 绑定的 UnoCSS 插件上下文,updateRoot定位项目根目录(示例中即process.cwd(),与 postcss 的扫描根一致)。
  2. 一次性全量扫描:用tinyglobbypatterns做 glob,逐文件readFileSync+ctx.extract(code, file)提取工具类,最后ctx.flushTasks()等待提取任务完成。这里没有任何 watch/fs 监听逻辑——这正解释了 README 中「新增工具类后需重启 dev server 重新扫描」的说明:standalone 模式是快照式的。
  3. 包装为 devframecreateInspectorDevframe(ctx)把上下文包装成可挂载的 devframe 定义(id: 'unocss'basePath: '/__unocss/'),供 Next 宿主通过createDevframeNextHandler转为 Next.js 路由 handler。

StandaloneInspectorOptions的完整选项(devframe.ts):

选项说明
root要扫描的项目根目录,默认process.cwd()
configUnoCSS 配置对象或配置文件路径;省略时自动从 root 加载
defaults传给上下文的配置默认值(UserConfigDefaults
patterns提取工具类的 glob 模式,相对于 root

未显式指定patterns时,默认的DEFAULT_PATTERNS(devframe.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/postcsscontent范围保持同一语义(app目录),使 Inspector 看到的工具集合与页面实际生成的 CSS 一致。

5.1 变更信号如何到达 Inspector 客户端

同一文件中,createInspectorDevframe还展示了 devframe 的通信设计(devframe.ts):devframe 定义声明了@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 SPA's relative assets // resolve under /__unocss/ export default withDevframe({ ...nextConfig })

两处配置都带有明确注释,对应两个真实的坑:

  1. serverExternalPackagesdevframe@devframes/next@unocss/inspector三个包被声明为服务端外部依赖,不参与 bundle——devframe 宿主的可选 MCP adapter 会懒加载 peer 包,若被打进 bundle 会在构建/运行时出问题。
  2. 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.ts),createDevframeNextHandler+withDevframe负责把 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 21:25:46

NPU架构原理与嵌入式部署实战:边缘AI的算力引擎

边缘AI项目里摸爬滚打久了&#xff0c;你会发现一个现象&#xff1a;大家都在比算力&#xff0c;但真正决定产品能不能落地的&#xff0c;往往是单位功耗下的有效算力。今天这篇是边缘AI系列的第6篇&#xff0c;主角就是边缘设备里最讲效率的计算芯片——NPU&#xff08;Neural…

作者头像 李华
网站建设 2026/9/13 21:24:14

Widlar电流源设计原理与实战:低功耗芯片的稳定电流基准

1. 项目概述&#xff1a;为什么一个“老古董”电路至今仍是芯片设计的基石&#xff1f;Widlar 电流源——这个名字听起来像半导体教科书里泛黄一页上的铅印字&#xff0c;但它不是历史遗迹&#xff0c;而是每天在你手机SoC、汽车ECU、工业传感器芯片内部默默工作的“隐形心脏”…

作者头像 李华
网站建设 2026/9/13 21:20:36

Golang Memberlist库:分布式节点管理与Gossip协议实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 21:18:46

激光位移传感器破解3C零件厚度测量难题:MLD25系列适配全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华