在 Next.js 项目中运行 Vite Dev Server,业务代码零修改。
Next.js dev server 在大型项目中有两个痛点:
- 内存占用高 — 开发时还要同时跑 AI 工具(Copilot、Claude Code),两者叠加经常 OOM
- HMR 慢 — 改一行代码等几秒才刷新,打断心流
Nextvi 让你用 pnpm vite 启动一个轻量的 Vite dev server,直接消费 src/app/ 下的 Next.js 页面,不需要修改任何业务代码。
pnpm dev → Next.js(原有,不变,生产构建 & CI/CD)
pnpm vite → Vite(新增,~200ms 启动,即时 HMR,低内存)
- 零侵入 — 不改业务代码,不改 Next.js 配置,不影响
next build - 自动路由 — 扫描
src/app/目录,自动生成 react-router 路由配置(含 layout 嵌套) - 透明兼容 —
next/link、next/image、next/navigation等 API 通过 shim 层自动替换 - Async 组件自动处理 — server component 的
async function在浏览器端自动转为 sync 组件 - Wrapper 覆盖 — 有 server fetch 的复杂页面,写一个 wrapper 文件即可覆盖自动行为
pnpm add next-vite-dev
# 或 monorepo workspace
# "next-vite-dev": "workspace:*"Peer Dependencies:
react>= 18,react-dom>= 18,react-router-dom>= 6vite>= 5@tailwindcss/vite>= 4(可选,安装后自动启用)
@vitejs/plugin-react 已内置,不需要单独安装。
// vite.config.ts
import { defineConfig } from 'vite'
import nextvi from 'next-vite-dev'
export default defineConfig({
plugins: [nextvi()],
})就这样。React 插件(含装饰器支持)、Tailwind CSS、依赖预构建全部自动处理。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8" /><title>My App</title></head>
<body>
<div id="root"></div>
<script type="module" src="/vite/entry.tsx"></script>
</body>
</html>// vite/entry.tsx
import '../src/app/globals.css'
import ReactDOM from 'react-dom/client'
import ViteApp from './ViteApp'
ReactDOM.createRoot(document.getElementById('root')!).render(<ViteApp />)// vite/ViteApp.tsx
import { Suspense } from 'react'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { routes } from 'virtual:auto-routes' // 自动生成的路由
const router = createBrowserRouter(routes)
export default function ViteApp() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} />
</Suspense>
)
}{
"scripts": {
"dev": "next dev",
"vite": "vite"
}
}pnpm vite # Vite 模式:~200ms 启动,即时 HMR
pnpm dev # Next.js 模式:完整 SSR,不受影响Nextvi 扫描 src/app/ 目录,自动生成 react-router 路由配置:
src/app/
├── layout.tsx → 根 layout(自动嵌套为父路由 + <Outlet />)
├── page.tsx → /
├── [locale]/
│ ├── layout.tsx → locale layout(自动嵌套)
│ ├── page.tsx → /:locale
│ ├── dashboard/page.tsx → /:locale/dashboard
│ ├── posts/
│ │ ├── page.tsx → /:locale/posts
│ │ └── [id]/page.tsx → /:locale/posts/:id
│ └── [...catchAll]/page.tsx → /:locale/*
layout.tsx 自动嵌套 — 不需要手动包装,插件自动发现 layout 文件并生成 react-router 嵌套结构。
async 组件自动处理 — Next.js 的 async function Layout/Page 在浏览器端自动转为 sync 组件(__wrapAsync),传递 params 和 searchParams。
文件监听 — 新增/删除 page.tsx 或 layout.tsx,路由自动更新,无需重启。
90% 的页面通过自动路由 + __wrapAsync 直接能跑。但有些页面有 server-side fetch,在浏览器会失败:
// src/app/[locale]/trade/[pair]/page.tsx — async server component
export default async function TradePage({ params }) {
const { pair } = await params
const data = await serverFetch(`/api/token/${pair}`) // 浏览器跑不了
return <TradeView data={data} />
}解决:在 vite/wrappers/ 下写一个镜像文件,用 client-side fetch 替代:
// vite/wrappers/[locale]/trade/[pair].tsx — 客户端 wrapper
'use client'
import { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { TradeView } from '@/views/trade'
export default function TradePage() {
const { pair } = useParams()
const [data, setData] = useState(null)
useEffect(() => {
fetch(`/api/token/${pair}`).then(r => r.json()).then(setData)
}, [pair])
if (!data) return <div>Loading...</div>
return <TradeView data={data} />
}规则:
- Wrapper 存在 → 自动覆盖原始 page.tsx
- 删除 wrapper → 自动回退到 page.tsx +
__wrapAsync - Wrapper 目录也被监听,增删即时生效
在接入前,扫描你的 Next.js 项目看兼容程度:
npx next-vite-dev check ./my-next-app
npx next-vite-dev check ./my-next-app --json # JSON 格式,便于 CI 集成输出三级兼容性报告:
- ✓ 支持 — nextvi 已有 shim,直接运行
- **~ 部分支持** — 需要写 wrapper 或有功能降级
- ✗ 不支持 — 服务端特性(API Routes、middleware、SSR 数据获取),开发时用
next dev
扫描范围:
import/require中的next/*模块引用package.json中与 Next.js 深度耦合的第三方库(next-auth、next-intl 等)- 服务端 API(
getServerSideProps、getStaticProps、generateMetadata、API Routes) next.config.js配置项(webpack、i18n、headers 等)- middleware 文件检测
如果你使用 Claude Code,项目中内置了 /check skill,提供比 CLI 更智能的分析:
/check ./my-next-app
除了 CLI 的所有扫描能力外,skill 还会:
- 逐页分析 async 组件,给出具体的 wrapper 代码建议(包含完整的
vite/wrappers/示例代码) - 检测 monorepo 结构,建议是否开启
clientDedup: true - 检测 barrel 依赖(antd、lodash、@mui 等),建议是否开启
importOptimization: true - 检测 Tailwind 版本,提示 v3 需保留 postcss.config 还是 v4 自动检测
- 生成推荐的 vite.config.ts,基于扫描结果给出完整的配置示例
- 评估 wrapper 工作量,统计需要手动处理的页面数量和原因
Skill 文件位于 .claude/skills/check/SKILL.md,克隆项目后自动可用。
nextvi 遵循零配置优先原则。绝大多数场景只需 nextvi(),需要定制时传入选项:
// vite.config.ts
import { defineConfig } from 'vite'
import nextvi from 'next-vite-dev'
export default defineConfig({
plugins: [
nextvi({
// React 插件:默认 true(自动注册 + 装饰器),传对象可追加 babel 插件
react: { babel: { plugins: ['styled-jsx/babel'] } },
// Tailwind CSS:默认 true(自动检测),传 false 关闭
tailwind: true,
// 自动路由
autoRouting: {
appDir: 'src/app',
wrappersDir: 'vite/wrappers',
metadataExtraction: true,
},
// 路径别名(追加到内置 shim alias 之后)
aliases: [{ find: /^@\//, replacement: './src/' }],
// Import 优化
importOptimization: true,
// Monorepo 重复包检测
clientDedup: true,
// 额外空模块 alias
emptyModuleAliases: ['@sentry/nextjs'],
}),
],
// 标准 Vite 配置照常写
server: {
port: 3000,
proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true } },
},
})| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
react |
boolean | ReactOptions |
true |
React 插件配置。true = 自动注册(含 legacy decorators);对象 = 合并 babel 插件;false = 自己管 |
tailwind |
boolean |
true |
自动检测 @tailwindcss/vite。安装了就启用,没装静默跳过 |
autoOptimizeDeps |
boolean |
true |
自动读取 package.json 的 dependencies 加入预构建 |
aliases |
AliasEntry[] |
[] |
路径别名(追加到内置 shim alias 之后) |
envPrefixes |
string[] |
[] |
额外环境变量前缀(内置 NEXT_PUBLIC_、NEXT_API_、BUILD_ENV) |
optimizeDepsInclude |
string[] |
[] |
额外预构建依赖(追加到自动扫描结果之上) |
optimizeDepsExclude |
string[] |
[] |
排除预构建(alias 指向源码的包) |
optimizeDepsEsbuildPlugins |
any[] |
[] |
esbuild 预构建阶段的插件 |
extraDefine |
Record<string, string> |
{} |
额外 define 注入的环境变量 |
emptyModuleAliases |
string[] |
[] |
额外 alias 到空模块的包名 |
autoRouting |
AutoRoutingOptions |
- | 自动路由配置(详见下方) |
importOptimization |
boolean | ImportOptimizationOptions |
- | barrel import → 直接子模块导入 |
clientDedup |
boolean | ClientDedupOptions |
- | monorepo 重复包检测 + 自动 dedupe |
以下能力默认开启,无需配置:
| 能力 | 说明 |
|---|---|
| React 插件 | 自动注册 @vitejs/plugin-react,内置 @babel/plugin-proposal-decorators(legacy 模式,MobX 等需要) |
| Tailwind CSS | 自动检测 @tailwindcss/vite 是否安装,安装了就启用(Tailwind v4+) |
| 依赖预构建 | 自动读取 package.json 的 dependencies 加入 optimizeDeps.include,告别手动维护 300 行列表 |
| Next.js shim | 17 个 next/* 模块自动 alias 到 shim(详见 Shim 清单) |
| Buffer polyfill | Web3 库需要的 Buffer 全局变量自动注入 |
| CJS → ESM | require() 调用自动转换为 ESM import |
| public/ JSON | 支持 Next.js 风格的 import data from '../public/xxx.json' |
| 重复插件检测 | 消费方手动加了 react() 或 tailwindcss() 时直接报错,避免冲突 |
| catch-all 警告 | dev 模式下未知 next/* import 落入 catch-all 时打印警告 |
这些都是在实际开发中被运行时错误逼出来的修复:
| # | Workaround | 解决的问题 |
|---|---|---|
| W1 | history.replaceState/pushState monkey-patch | query params 变更后组件不重渲染 |
| W2 | Buffer polyfill 自动注入 | Buffer is not defined(Web3) |
| W3 | CJS require() → ESM 转换 | require is not defined |
| W4 | public/ JSON 导入插件 | public/*.json 模块解析失败 |
| W5 | Buffer npm 包显式 alias | Vite 外部化 Node buffer |
| W6 | resolve.dedupe 单例保证 | Context mismatch / Hook 报错 |
| W7 | crypto → crypto-js 桥接 | crypto module not available |
| W8 | dynamic memo/forwardRef 处理 | React.lazy() only accepts functions |
| W9 | catch-all next/* regex | 未知 next/* import 失败 |
| W10 | .js 后缀 regex alias | next/navigation.js 无法匹配 |
| W11 | 空模块 stub | 可选依赖初始化崩溃 |
| W12 | CJS .default fallback | ESM namespace default 取值 |
| W13 | RouterContext try/catch | next 未安装时降级 |
| W14 | useSyncExternalStore | history API 后 searchParams 不更新 |
| W15 | React 插件自动注册 + 装饰器 | 消费方不需要关心 babel 配置 |
| W16 | Tailwind CSS 自动检测 | 安装了 @tailwindcss/vite 就自动启用 |
| W17 | package.json 依赖自动预构建 | 告别手动维护 300 行 optimizeDepsInclude |
| Next.js 模块 | Shim 文件 | 映射策略 |
|---|---|---|
next/navigation |
next-navigation.tsx |
react-router-dom + history 拦截 |
next/router |
next-router.tsx |
react-router-dom + RouterContext |
next/dynamic |
next-dynamic.tsx |
React.lazy + Suspense |
next/link |
next-link.tsx |
<a> + react-router navigate |
next/image |
next-image.tsx |
<img> + srcSet 生成 + blur placeholder |
next/script |
next-script.tsx |
DOM script 注入 |
next/head |
next-head.tsx |
null 组件 |
next/headers |
next-headers.ts |
document.cookie 解析 |
next/server |
next-server.ts |
NextRequest/NextResponse stub |
next/types |
next-types.ts |
Metadata/Viewport 类型 |
@ant-design/nextjs-registry |
antd-registry.tsx |
透传 children |
crypto |
crypto.ts |
crypto-js 桥接 |
buffer |
buffer-polyfill.ts |
npm buffer 包 |
next-i18next |
empty-module.ts |
i18next shim |
next/font/google |
next-font-google.ts |
运行时 Google Fonts <link> 注入 |
支持 Next.js 的 Intercepting Routes 模式(@modal/(.)products/[slug]),在 Vite 中实现和 Next.js 一致的行为:站内点击显示弹窗,直接访问/刷新显示完整页面。
解决这个问题需要绕过 3 个核心障碍:
1. 阻止 react-router 导航(DOM capture 拦截)
react-router 的 <Link> 导航会切换路由,导致底层页面消失。解决方案:在 document 上注册 capture 阶段的 click 事件监听器,在 React 事件系统之前拦截匹配 intercept pattern 的 <a> 点击,调用 e.preventDefault() + e.stopImmediatePropagation() 阻止所有后续处理。
2. 更新 URL 但不触发页面重渲染(suppress W1 事件)
用 history.pushState 更新 URL(不触发 react-router),但 W1 monkey-patch 会派发 __vite_shim_search_change__ 事件导致 useSearchParams 订阅者重渲染。解决方案:pushState 前通过 Symbol.for('__nextvi_suppress_history_event') 设置 suppress flag,W1 monkey-patch 检查此 flag 跳过事件派发。
3. 弹窗渲染不影响主应用(独立 React root)
弹窗组件在独立的 createRoot 容器中渲染(<div id="__nextvi-intercept">),完全脱离 RouterProvider 树。弹窗内的 useRouter() 等 hook 通过包裹 <BrowserRouter> 提供 router context。
4. 关闭弹窗不触发页面重渲染(stopImmediatePropagation)
弹窗内的 router.back() 触发 popstate 事件。如果 react-router 收到这个事件会重新匹配路由导致页面重渲染(骨架屏)。解决方案:在模块加载时(早于 react-router)注册 popstate 监听器,如果弹窗打开就关闭弹窗并 e.stopImmediatePropagation() 阻止 react-router 处理。
vinext app-router.ts 扫描 @modal/(.)products/[slug]
→ 生成 InterceptingRoute { targetPattern, pagePath, params }
→ generateInterceptRuntime() 将 pattern 编译为正则
→ document click capture 监听器匹配 <a> href
→ pushState + 独立 createRoot 渲染弹窗
→ popstate 关闭弹窗
| 改动 | 影响范围 | 风险等级 | 说明 |
|---|---|---|---|
W1 flag check (next-navigation.tsx) |
所有项目的每次 pushState/replaceState | 极低 | 仅多一次 Symbol.for() 属性读取,默认 undefined 走原逻辑 |
| DOM capture click listener | 仅有 @modal 目录的项目 |
低 | 不匹配 intercept pattern 立即 return,不影响其他 click 事件 |
| popstate stopImmediatePropagation | 仅弹窗打开时 | 中 | 弹窗打开期间会阻断 react-router 的 popstate 处理;弹窗关闭后(innerHTML 为空)恢复正常 |
| createRoot/BrowserRouter import | 仅有 @modal 目录的项目 |
无 | 无 @modal 的项目不生成这些 import |
无 @modal 目录的项目:唯一的影响是 next-navigation.tsx 中 W1 monkey-patch 多了一个 flag 检查(极低开销)。其他代码完全不会被加载或执行。
- 弹窗内的
router.push()在独立 BrowserRouter 内执行,不影响主应用路由(和 Next.js 行为一致) - 弹窗组件的 Tailwind 样式依赖全局 CSS(需确保
globals.css在index.html或main.tsx中被引入) - 仅支持
(.)同级拦截模式,(..)和(...)的 pattern 生成依赖 vinext 扫描器正确计算 targetPattern
| # | Workaround | 解决的问题 |
|---|---|---|
| W15 | Tailwind v3 postcss 保留 | 仅在使用 @tailwindcss/vite 时清空 postcss,否则保留项目的 postcss.config.mjs |
| W16 | next/font/google 运行时注入 | 通过 <link> 标签运行时加载 Google Fonts + CSS 变量注入 |
| W17 | redirect() NEXT_REDIRECT digest | redirect() 使用 window.location.replace + 特殊 error digest,避免 ErrorBoundary 误捕获 |
| W18 | searchParams 注入 | __wrapAsync 和 __wrapPage 自动注入 searchParams(从 window.location.search)给所有页面组件 |
| W19 | __resolveAsync 递归解析 | 递归解析 JSX 树中的嵌套 async server component,使 Suspense 内的 async 子组件正常工作 |
| W20 | __unwrapHtmlBody 标签剥离 | 自动剥离 layout 返回的 <html>/<body>/<head> 标签,避免浏览器拒绝渲染嵌套 <html> |
test-app/ 是一个完整的 Next.js + Vite 双模式 demo 项目:
cd test-app
pnpm install
pnpm dev # Next.js 模式 → http://localhost:3000
pnpm vite # Vite 模式 → http://localhost:3099同一套 src/app/ 页面代码,两种模式都能跑:
- 首页、Dashboard、Posts(列表+详情)、Settings(嵌套 layout)
- Demo 子目录:Navigation (W1+W14)、Dynamic (W8)、Crypto (W2+W5+W7)、Cookies、I18n (W11) 等 shim 测试页
vite/wrappers/[locale]/posts/[id].tsx— wrapper 覆盖示例
本项目在设计过程中参考了 cloudflare/vinext(MIT 协议)的部分设计思路。
vinext 的定位是用 Vite 完整替代 Next.js 构建系统(框架级迁移,包含 SSR/RSC/ISR/部署),与 nextvi "0 侵入 dev server" 的定位不同。以下记录每项借鉴的来源、原始设计、nextvi 的改造方式以及两者的差异。
| # | 借鉴项 | vinext 来源 | nextvi 实现 |
|---|---|---|---|
| 1 | 兼容性检查 CLI | src/check.ts |
cli/check.ts |
| 2 | Trie 路由匹配 | routing/route-trie.ts |
plugins/auto-routing/route-trie.ts |
| 3 | 文件系统路由发现 | routing/file-matcher.ts + routing/app-router.ts |
plugins/auto-routing/app-router.ts + file-matcher.ts |
| 4 | Import 优化 | plugins/optimize-imports.ts |
plugins/import-optimization.ts |
| 5 | Client 去重 | plugins/client-reference-dedup.ts |
plugins/client-dedup.ts |
| 6 | next/image 增强 | shims/image.tsx |
shims/next-image.tsx |
| 7 | Metadata 提取 | server/app-page-metadata.ts + shims/metadata/ |
shims/metadata-injector.tsx + auto-routing 集成 |
vinext 原始设计: vinext check 命令扫描项目源码,检测 40+ Next.js import、next.config 选项、30+ 第三方库、项目约定(middleware、CJS globals 等),输出三级兼容性报告(✓ 支持 / ~ 部分 / ✗ 不支持)和兼容度百分比。扫描机制为正则匹配(非 AST),同时解析 package.json 依赖。
nextvi 改造: npx next-vite-dev check [dir],保留三级报告 + 兼容度评分的输出格式。扫描范围针对 nextvi 的 17 个 shim 调整:
- import 扫描:匹配 nextvi 已有 shim 的
next/*模块(supported)和未覆盖的(partial/unsupported) - 服务端 API 扫描:检测
getServerSideProps、getStaticProps、generateMetadata、revalidate、runtime = 'edge'等 export - 第三方库扫描:检测
next-auth、next-intl、@next/mdx等与 Next.js 深度耦合的库 - next.config 扫描:检测
webpack、i18n、headers、middleware等配置项 - 文件检测:middleware 文件、API Routes 目录
- 新增
--json格式输出,便于 CI 集成
差异: vinext check 面向 vinext 的 94% API 覆盖;nextvi check 面向纯 CSR 场景,会将所有服务端特性标记为不支持,并给出迁移建议(如"改用 useEffect + fetch")。
vinext 原始设计: routing/route-trie.ts 构建前缀树,实现 O(depth) 路由查找。遍历顺序强制优先级:静态段 > 动态 :id > 捕获 :slug+ > 可选 :slug*。每个路由通过优先级评分(precedence scoring)预排序后插入 Trie。
nextvi 改造: plugins/auto-routing/route-trie.ts 使用相同的 Trie 数据结构和优先级评分算法。评分规则:
- 静态前缀:-50/段
- 动态段:+100/位
- 捕获段
[...slug]:+1000/位 - 可选捕获
[[...slug]]:+2000/位 - 中缀静态段:-500/段
差异: vinext 的 Trie 用于服务端请求路由匹配(HTTP 请求 → handler);nextvi 的 Trie 用于代码生成时确定 react-router 路由顺序(生成时排序,运行时由 react-router 匹配)。
vinext 原始设计: routing/app-router.ts + routing/file-matcher.ts 扫描 app/ 目录,识别 Next.js App Router 约定:
page.tsx/route.ts— 页面/API 路由layout.tsx— 布局嵌套loading.tsx/error.tsx/not-found.tsx— 边界组件[id]/[...slug]/[[...slug]]— 动态/捕获/可选捕获段(group)— 路由组(不生成 URL 段)@slot— 并行路由(.)pattern— 拦截路由
使用 ValidFileMatcher 创建正则匹配文件,scanWithExtensions() 基于 Node glob API 发现文件,支持配置扩展名。
nextvi 改造: plugins/auto-routing/app-router.ts + file-matcher.ts 实现相同的扫描逻辑,但生成目标不同:
- vinext:生成服务端路由 handler(RSC entry + SSR entry)
- nextvi:生成 react-router-dom 路由配置(
lazy: () => import(...)懒加载) - nextvi 额外实现了 wrapper 优先级覆盖(
vite/wrappers/目录下的手动 wrapper 替代自动__wrapAsync) - nextvi 将 async server component 通过
__wrapAsync转换为 sync 组件(useState + useEffect),vinext 直接在服务端执行 async 组件
差异: vinext 的路由发现服务于完整的 SSR/RSC 渲染管线;nextvi 的路由发现仅用于生成客户端路由配置,所有服务端逻辑被转换或跳过。
vinext 原始设计: plugins/optimize-imports.ts 在 RSC/SSR 环境中转换 barrel import 为直接子模块导入。解析 barrel 入口的 AST 构建 export map,解析 react-server export condition,防止 React.createContext() 在不可用环境下被提前执行。
nextvi 改造: plugins/import-optimization.ts 简化版,面向纯 CSR dev 环境:
-
使用正则匹配(非 AST)提取
import { X, Y } from 'pkg'格式的 barrel import -
内置 6 个常用包映射:
包名 转换模板 命名转换 antdantd/es/{name}preserve lodash/lodash-eslodash-es/{name}camelCase @mui/material@mui/material/{name}preserve @mui/icons-material@mui/icons-material/{name}preserve lucide-reactlucide-react/dist/esm/icons/{name}kebabCase @ant-design/icons@ant-design/icons/es/icons/{name}preserve -
正确处理
import { type X }— type-only import 保留在原包 -
正确处理
import { X as Y }— 重命名导入 -
用户可通过
packages选项自定义映射,exclude选项排除内置包
配置方式:
nextvi({
// 使用内置映射
importOptimization: true,
// 或自定义
importOptimization: {
packages: {
'my-ui-lib': { transform: 'my-ui-lib/es/{name}', caseTransform: 'kebabCase' },
},
exclude: ['lodash'], // 排除内置 lodash 映射
},
})差异: vinext 使用 AST 解析 barrel 入口的 export map,支持 react-server condition 和 Context 提前执行防护;nextvi 使用正则 + 预定义映射表,更轻量但不分析 barrel 入口的实际导出结构。对于 dev 阶段的场景已足够。
vinext 原始设计: plugins/client-reference-dedup.ts 拦截 client-in-server-package-proxy 模块的 import,重定向到虚拟模块(通过 bare specifier 重新导出),确保浏览器端使用 .vite/deps/ 预打包版本而非原始 ESM,避免同一模块被多次实例化。
nextvi 改造: plugins/client-dedup.ts 面向 dev 阶段的诊断 + 自动修复:
- 诊断功能: dev server 启动时自动检测 monorepo 中是否存在关键包的多版本
- 支持 pnpm workspace(
.pnpm目录扫描) - 支持 npm/yarn workspaces(
package.jsonworkspaces 字段) - 通过
fs.realpathSync解析 symlink 检测真实路径差异 - 发现重复时输出彩色警告 +
pnpm dedupe修复建议
- 支持 pnpm workspace(
- 自动修复: 通过 Vite 的
confighook 返回扩展的resolve.dedupe列表 - 默认检测 11 个 Context 敏感包:react, react-dom, react-router-dom, mobx, mobx-react-lite, i18next, react-i18next, zustand, @tanstack/react-query, jotai, recoil
配置方式:
nextvi({
clientDedup: true,
// 或自定义
clientDedup: {
extraPackages: ['my-state-lib'],
silent: false, // 设为 true 不输出警告(仍然 dedupe)
},
})差异: vinext 在模块解析层面做 import 重定向(运行时拦截);nextvi 在配置层面做 resolve.dedupe 扩展 + 启动时诊断(利用 Vite 已有的去重机制)。nextvi 的方式更轻量,依赖 Vite 内置能力而非自定义虚拟模块。
vinext 原始设计: shims/image.tsx 基于 @unpic/react 提供完整的 image 组件,支持 Cloudflare Images CDN 优化、自动 srcset、responsive/fixed 布局、blur placeholder(服务端生成 blurDataURL)、优先级提示(fetchPriority)。
nextvi 改造: shims/next-image.tsx 在纯 CSR 环境下实现尽可能接近 Next.js 的体验:
| 特性 | vinext | nextvi |
|---|---|---|
| srcSet 生成 | 基于 CDN loader + deviceSizes | 基于 loader prop + Next.js 默认断点 (640~3840) |
| 默认 loader | Cloudflare Images | ({ src }) => src(不优化,结构正确) |
| blur placeholder | 服务端生成 blurDataURL | 客户端 CSS background-image + filter:blur(20px) + 0.3s 过渡 |
| priority 提示 | fetchPriority + preload link | fetchPriority + loading="eager" + decoding="sync" |
| fill 模式 | position:absolute + objectFit | 相同 |
| sizes | 自动计算 | fill 模式默认 "100vw",其余透传 prop |
| StaticImageData | 支持(含 blurDataURL) | 支持(自动读取 src.blurDataURL) |
| 图片优化 | Cloudflare Images 实时优化 | 无(纯 CSR 不依赖服务端) |
差异: vinext 依赖 Cloudflare Images CDN 做实时图片优化(resize、format 转换);nextvi 的 srcSet 默认返回原图 URL(开发阶段不需要真正优化),但保留了正确的 HTML 结构和 loader 接口,用户可传入自定义 CDN loader 获得真实优化。
vinext 原始设计: server/app-page-metadata.ts + shims/metadata/ 在服务端完整解析 Next.js Metadata API:
- 支持
export const metadata = { ... }静态对象 - 支持
export async function generateMetadata(props)动态生成 - 支持
metadata.title.template模板继承(父 layout → 子页面) - 服务端渲染时注入到 HTML
<head>中 - 支持 Open Graph、Twitter Cards、robots、icons、manifest 等全部 Metadata 字段
nextvi 改造: shims/metadata-injector.tsx + plugins/auto-routing/index.ts 代码生成集成:
提取阶段(构建时,auto-routing 插件中):
- 读取每个
page.tsx的源码 - 正则匹配
export const metadata = { ... }(支持类型注解export const metadata: Metadata = { ... }) - 使用平衡括号算法提取对象字面量(正确处理字符串中的
{}、转义字符、模板字符串) - 安全检查:去除字符串后如果包含
函数调用()则跳过(不支持动态内容) generateMetadata()函数形式跳过不提取
注入阶段(运行时,MetadataInjector 组件):
- 在路由代码生成中,有 metadata 的页面使用
__wrapPageWithMetadata(m, { title: '...', ... })替代__wrapPage(m) - MetadataInjector 组件通过
useEffect将 metadata 注入到document.head - 支持字段:title(含
{ default, template, absolute }格式)、description、keywords、openGraph(title/description/type/url/images)、twitter(card/title/description/images)、robots(含{ index, follow }对象格式) - 路由切换时自动清理旧 meta 标签(
data-vite-metadata属性标记 + cleanup function) - 默认开启,可通过
autoRouting.metadataExtraction: false关闭
配置方式:
nextvi({
autoRouting: {
appDir: 'src/app',
metadataExtraction: true, // 默认 true
},
})差异:
| 维度 | vinext | nextvi |
|---|---|---|
| 提取方式 | 服务端运行时 import + 执行 | 构建时正则提取静态字面量 |
| 动态 metadata | 支持 generateMetadata() |
不支持(CSR 中无法执行服务端函数) |
| 模板继承 | 支持 title.template 跨 layout 继承 |
不支持(仅页面级 metadata) |
| 注入位置 | SSR HTML <head> |
客户端 useEffect → document.head |
| 适用范围 | 仅纯字面量 export const metadata |
同左 |
已知限制:
- 变量引用(
metadata = { title: myTitle })、函数调用(metadata = { title: getTitle() })会导致提取跳过 - 不支持 layout 级别的 metadata(仅提取
page.tsx中的) - SEO 爬虫无法看到客户端注入的 meta 标签(dev 阶段可接受)
| 维度 | vinext | nextvi |
|---|---|---|
| 定位 | 用 Vite 替代 Next.js(框架级迁移) | 0 侵入的 Vite dev server(开发工具) |
| 渲染 | SSR + RSC + SSG + ISR + CSR | 仅 CSR |
| 侵入性 | 需要替换整个构建系统和 CLI | 业务代码零修改,只加一个 vite.config.ts |
| 依赖版本 | Vite 7+ / React 19.2.5+ | Vite 5+ / React 18+ |
| 部署 | Cloudflare Workers 优先,支持 Vercel/Netlify | 不涉及部署(纯开发环境) |
| API 兼容度 | ~94% Next.js 16 API | 17 个 shim + 20 个 Workaround(客户端 API) |
| 代码量 | 50+ 服务端文件、52 shim、9 子插件 | 4 插件、17 shim、1 CLI |
| 核心路由 | 独立实现(RSC stream + SSR HTML) | 基于 react-router-dom |
核心差异一句话: vinext 是 "用 Vite 重建 Next.js",nextvi 是 "让 Vite 兼容 Next.js"。前者做框架迁移,后者做开发提速。
| nextvi | vinext | |
|---|---|---|
| 一句话 | 和 Next.js 共存的 Vite dev server | 用 Vite 完整替代 Next.js |
| 目标 | 只加速开发,生产仍用 Next.js | 框架级迁移,包含 SSR/RSC/部署 |
| 依赖 | Vite 5+、React 18+、无 Node 版本限制 | Vite 8 (Rolldown)、React 19、Node 22+ |
| RSC | 不支持(async 组件用 __wrapAsync 降级) |
支持(@vitejs/plugin-rsc) |
| SSR | 不支持(纯 CSR) | 支持 |
| 成熟度 | 已在多个生产项目跑通 | 0.0.41,Cloudflare 刚发布 |
| 侵入性 | 零侵入,不改业务代码 | 需替换整个构建系统 |
| Monorepo | clientDedup、嵌套依赖预构建、workspace 检测 |
未专门处理 |
| 用法 | plugins: [nextvi()] |
plugins: [vinext()] |
选择建议:
- 想在现有 Next.js 项目上加速开发,不动生产 → nextvi
- 想彻底从 Next.js 迁移到 Vite → vinext
MIT