Skip to content

467469274/next-vite-dev

Repository files navigation

Nextvi

在 Next.js 项目中运行 Vite Dev Server,业务代码零修改。

为什么

Next.js dev server 在大型项目中有两个痛点:

  1. 内存占用高 — 开发时还要同时跑 AI 工具(Copilot、Claude Code),两者叠加经常 OOM
  2. HMR 慢 — 改一行代码等几秒才刷新,打断心流

Nextvi 让你用 pnpm vite 启动一个轻量的 Vite dev server,直接消费 src/app/ 下的 Next.js 页面,不需要修改任何业务代码。

pnpm dev   → Next.js(原有,不变,生产构建 & CI/CD)
pnpm vite  → Vite(新增,~200ms 启动,即时 HMR,低内存)

核心理念

  1. 零侵入 — 不改业务代码,不改 Next.js 配置,不影响 next build
  2. 自动路由 — 扫描 src/app/ 目录,自动生成 react-router 路由配置(含 layout 嵌套)
  3. 透明兼容next/linknext/imagenext/navigation 等 API 通过 shim 层自动替换
  4. Async 组件自动处理 — server component 的 async function 在浏览器端自动转为 sync 组件
  5. 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 >= 6
  • vite >= 5
  • @tailwindcss/vite >= 4(可选,安装后自动启用)

@vitejs/plugin-react 已内置,不需要单独安装。

快速开始

1. 添加 Vite 配置

// vite.config.ts
import { defineConfig } from 'vite'
import nextvi from 'next-vite-dev'

export default defineConfig({
  plugins: [nextvi()],
})

就这样。React 插件(含装饰器支持)、Tailwind CSS、依赖预构建全部自动处理。

2. 添加 HTML 入口

<!-- 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>

3. 添加 Vite 入口

// 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>
  )
}

4. 添加 script

{
  "scripts": {
    "dev": "next dev",
    "vite": "vite"
  }
}

5. 运行

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),传递 paramssearchParams

文件监听 — 新增/删除 page.tsxlayout.tsx,路由自动更新,无需重启。

Wrapper 覆盖机制

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 项目看兼容程度:

CLI 方式

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

扫描范围:

  1. import/require 中的 next/* 模块引用
  2. package.json 中与 Next.js 深度耦合的第三方库(next-auth、next-intl 等)
  3. 服务端 API(getServerSidePropsgetStaticPropsgenerateMetadata、API Routes)
  4. next.config.js 配置项(webpack、i18n、headers 等)
  5. middleware 文件检测

Claude Code Skill 方式

如果你使用 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 } },
  },
})

nextvi() 选项

选项 类型 默认值 说明
react boolean | ReactOptions true React 插件配置。true = 自动注册(含 legacy decorators);对象 = 合并 babel 插件;false = 自己管
tailwind boolean true 自动检测 @tailwindcss/vite。安装了就启用,没装静默跳过
autoOptimizeDeps boolean true 自动读取 package.jsondependencies 加入预构建
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.jsondependencies 加入 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

这些都是在实际开发中被运行时错误逼出来的修复:

# 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

Shim 清单

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> 注入

Intercepting Routes(弹窗路由拦截)

支持 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.cssindex.htmlmain.tsx 中被引入)
  • 仅支持 (.) 同级拦截模式,(..)(...) 的 pattern 生成依赖 vinext 扫描器正确计算 targetPattern

额外 Workaround

# 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 集成

1. 兼容性检查 CLI

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 扫描:检测 getServerSidePropsgetStaticPropsgenerateMetadatarevalidateruntime = 'edge' 等 export
  • 第三方库扫描:检测 next-authnext-intl@next/mdx 等与 Next.js 深度耦合的库
  • next.config 扫描:检测 webpacki18nheadersmiddleware 等配置项
  • 文件检测:middleware 文件、API Routes 目录
  • 新增 --json 格式输出,便于 CI 集成

差异: vinext check 面向 vinext 的 94% API 覆盖;nextvi check 面向纯 CSR 场景,会将所有服务端特性标记为不支持,并给出迁移建议(如"改用 useEffect + fetch")。


2. Trie 路由匹配

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 匹配)。


3. 文件系统路由发现

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 的路由发现仅用于生成客户端路由配置,所有服务端逻辑被转换或跳过。


4. Import 优化插件

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 个常用包映射:

    包名 转换模板 命名转换
    antd antd/es/{name} preserve
    lodash / lodash-es lodash-es/{name} camelCase
    @mui/material @mui/material/{name} preserve
    @mui/icons-material @mui/icons-material/{name} preserve
    lucide-react lucide-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 阶段的场景已足够。


5. Client Reference 去重

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.json workspaces 字段)
    • 通过 fs.realpathSync 解析 symlink 检测真实路径差异
    • 发现重复时输出彩色警告 + pnpm dedupe 修复建议
  • 自动修复: 通过 Vite 的 config hook 返回扩展的 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 内置能力而非自定义虚拟模块。


6. next/image 增强

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 获得真实优化。


7. Metadata 提取

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> 客户端 useEffectdocument.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 vs vinext

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

License

MIT

About

Run Vite Dev Server alongside Next.js — zero-config Vite plugin with Next.js API shims

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors