普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月7日首页

详解 Nuxt 4 ,快速上手使用!

作者 GentlyBeing
2026年4月7日 07:11

一、Nuxt 4 适用前提

Nuxt 4 适合的,不只是“想写 Vue 项目”的场景,而是“希望在 Vue 之上直接获得一整套成熟应用能力”的场景。它解决的核心问题不是单纯把页面跑起来,而是把路由、数据获取、服务端渲染、服务端接口、部署形态和工程组织一起收进同一个框架里。

以下场景通常很适合选择 Nuxt 4:

  • 你要做的是内容站、官网、博客、文档站、电商前台、中后台、SaaS 前端这类真正的网站或 Web 应用,而不是只做几个纯前端页面。
  • 你希望默认就具备 SSR、SEO、文件路由、布局系统、数据获取、服务端接口等能力,而不是自己从 Vue + Vite 一项项拼出来。
  • 你希望前后端边界更顺滑,例如前端页面和服务端 API 放在同一个仓库中协作。
  • 你需要根据不同页面选择 SSR、预渲染、缓存、重定向等渲染策略。
  • 你更看重“约定优于配置”的开发效率,希望团队新成员进入项目后能更快读懂结构。

以下场景则建议先评估:

  • 项目只是一个非常轻量的纯前端单页应用,没有 SEO、SSR、服务端逻辑需求,用 Vue + Vite 往往更直接。
  • 团队已经有成熟的纯前端架构和配套基础设施,Nuxt 带来的约定反而可能束缚既有体系。
  • 项目需要极度特殊的路由、渲染或服务端组织方式,而你不想遵循 Nuxt 的目录约定。

一句话概括:Nuxt 4 不是“Vue 的脚手架”,而是 Vue 生态里的全栈应用框架。

二、Nuxt 4 简介

Nuxt 4 是构建在 Vue 3 之上的全栈框架。它把现代 Web 应用里常见但又重复的能力预先组织好了,例如:

  • 文件系统路由
  • 布局系统
  • 自动导入
  • SSR 与 CSR
  • 服务端 API
  • 预渲染与混合渲染
  • 模块化扩展

Nuxt 4 的核心价值,在于它把这些能力整合成一个统一的开发体验。你写页面、写组件、写接口、写配置、写部署策略,都不再是彼此割裂的几套工具链,而是在同一个框架中完成。

从官方文档当前 4.x 版本可以明确确认几件很重要的事:

  • 新项目要求 Node.js 20.x 或更高版本
  • Nuxt 4 默认将应用源码放在 app/ 目录下。
  • 服务端能力由 Nitro 提供。
  • 数据获取、状态共享、自动导入、路由生成等能力是框架级设计,不是后期再补的插件习惯。

Nuxt 4 的核心能力

mindmap
  root((Nuxt 4))
    Vue 应用层
      页面路由
      布局系统
      组件自动导入
      中间件
    全栈能力
      SSR
      Server API
      数据获取
      useState
    服务端引擎
      Nitro
      routeRules
      prerender
      cache
    工程体验
      零碎配置更少
      目录约定清晰
      模块生态
      多种部署形态

三、Nuxt 4 与 Vue 的关系

Nuxt 经常被拿来和 Vue + Vite 一起讨论,但它们不是同一层次的工具。

方案 定位 优势 适合场景
Vue + Vite 前端应用基础组合 轻量、自由、上手快 纯前端 SPA、小型项目、已有成熟工程体系
Nuxt 4 Vue 全栈应用框架 路由、SSR、服务端、数据获取、部署策略一体化 官网、内容站、SaaS、需要 SEO 或 SSR 的 Vue 应用

如果你已经确定技术栈是 Vue,那么思考:

  • 你是否只需要 Vue + Vite 这样的基础前端组合。
  • 还是你已经进入“需要框架级约定和全栈能力”的阶段。

当项目出现下面这些需求时,Nuxt 的优势会非常明显:

  • 需要 SEO。
  • 需要 SSR 或预渲染。
  • 需要文件路由。
  • 需要服务端 API。
  • 需要更细粒度的页面级渲染控制。

四、Nuxt 4 快速上手

这一部分先解决“怎么把项目跑起来”,同时把几个最常见的理解误区顺手讲清楚。

1. 前置准备

根据 Nuxt 4 官方文档,建议准备:

  • Node.js 20.x 或更高版本,优先使用当前 LTS。
  • 一个具备 Vue 语言服务支持的编辑器。
  • 包管理器保持团队一致,本文以 pnpm 为例。
# 查看 Node.js 版本
node -v

# 查看 pnpm 版本
pnpm -v

如果你在 Windows 环境下感觉本地开发响应偏慢,官方文档也特别提醒了两点:

  • 可考虑使用 WSL 改善 HMR 和文件监听体验。
  • 浏览器访问本地开发服务时,使用 127.0.0.1:3000 往往会比 localhost:3000 更快。

2. 创建项目

# 创建 Nuxt 4 项目
pnpm create nuxt@latest my-nuxt-app

创建完成后进入目录:

cd my-nuxt-app

Nuxt 会根据模板生成基础项目结构。和很多旧教程不同,Nuxt 4 默认不是把页面代码全放在根目录,而是默认使用 app/ 目录作为应用源码目录。

3. 启动开发服务

pnpm dev -o

默认开发地址通常是:

http://localhost:3000

启动后你会立刻感受到 Nuxt 的几个默认体验:

  • 页面路由会根据目录自动生成。
  • 组件、组合式函数、工具函数有不少可以自动导入。
  • 页面切换和数据获取已经考虑了 SSR/CSR 之间的衔接。

4. 构建与预览

# 生产构建
pnpm build

# 本地预览构建结果
pnpm preview

和纯前端项目相比,Nuxt 的“构建结果”不只是静态资源这么简单。根据渲染模式不同,它可能包含:

  • 服务器运行所需产物
  • 预渲染页面
  • 客户端资源
  • payload 数据

因此,pnpm preview 比“只是看看页面能不能打开”更重要,它能帮助你提前发现渲染模式、资源路径和运行时配置相关的问题。

五、Nuxt 4 项目结构与目录认知

Nuxt 4 最大的学习成本,不在 API 本身,而在于你要先接受它的目录约定。目录一旦理解顺了,后面的很多能力都会自然变得清晰。

1. 一个典型的 Nuxt 4 目录结构

my-nuxt-app/
├── app/
│   ├── assets/              # 会进入构建流程的资源
│   ├── components/          # 组件
│   ├── composables/         # 组合式函数
│   ├── layouts/             # 布局
│   ├── middleware/          # 路由中间件
│   ├── pages/               # 页面路由
│   ├── plugins/             # Nuxt 插件
│   ├── utils/               # 工具函数
│   ├── app.config.ts        # 应用级公开配置
│   └── app.vue              # 应用根组件
├── public/                  # 原样公开的静态资源
├── server/
│   ├── api/                 # /api/* 接口
│   ├── middleware/          # 服务端中间件
│   ├── plugins/             # Nitro 插件
│   └── routes/              # 非 /api 前缀服务端路由
├── nuxt.config.ts           # Nuxt 核心配置
├── .env                     # Nuxt 读取的环境变量
├── package.json
└── tsconfig.json

这里最容易理解错的,是 app/public/server/ 三者的边界:

  • app/ 放的是 Vue 应用层代码。
  • public/ 放的是原样对外提供的静态资源。
  • server/ 放的是 Nitro 服务端逻辑。

如果把这三个目录的职责混在一起,后面几乎所有问题都会开始变得难排查。

2. app/ 是 Nuxt 4 的前台应用层

Nuxt 4 默认的 srcDirapp/。这意味着页面、组件、布局、组合式函数等前台应用代码,默认都应该往这里放。

可以简单理解为:

  • app/pages/ 决定页面路由。
  • app/layouts/ 决定页面外壳。
  • app/components/ 决定可复用视图单元。
  • app/composables/ 决定通用逻辑复用。
  • app/plugins/ 决定应用级注入与初始化。

3. server/ 是 Nuxt 的服务端能力入口

Nuxt 的服务端能力不是“顺手加了个 API 目录”,而是由 Nitro 提供的正式能力。

例如:

  • server/api/hello.ts 会生成 /api/hello
  • server/routes/health.ts 会生成 /health
  • server/middleware/log.ts 会在请求进入时执行

这意味着 Nuxt 项目天然就可以既写页面,又写服务端接口,不需要额外再搭一个独立的 Node 服务才能开始工作。

4. public/ 与 app/assets/ 的区别

这两个目录在所有前端框架里都容易让人混淆,在 Nuxt 中也一样:

  • public/ 中的资源不会经过构建转换,适合 favicon、robots.txt、静态下载文件这类稳定资源。
  • app/assets/ 中的资源会进入构建流程,更适合业务图片、样式资源、字体等。

如果一个资源你希望它保持稳定 URL,优先考虑 public/;如果你希望它参与构建优化、哈希命名、依赖分析,优先考虑 app/assets/

六、Nuxt 4 核心配置文件介绍

Nuxt 4 的配置理解难点,不在于“配置项很多”,而在于它有几层配置分别面向不同用途。

graph LR
    A[nuxt.config.ts] --> B[框架级配置]
    C[app/app.config.ts] --> D[应用公开配置]
    E[.env] --> F[runtimeConfig 环境变量注入]
    G[tsconfig.json] --> H[类型系统与编辑器体验]
    I[app/app.vue] --> J[应用根结构]

1. 文件 .env:环境变量

.env 文件本质上就是一个“给项目提供变量值”的配置文件。

mindmap
  root((环境变量))
    最常见的例子
      接口基础地址
      第三方服务密钥
      站点标题
      功能开关
    最适合放
      不能写死在代码里的值
      随环境变化的值
      多环境可切换的值

先看一个最简单的例子:

NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=https://api.example.com
NUXT_PUBLIC_SITE_NAME=我的 Nuxt 网站

你可以先把 .env 理解成“变量值仓库”。它只负责提供值,本身不负责告诉 Nuxt“这些值该怎么安全地在项目里使用”。这也是为什么后面还需要 runtimeConfig

值会怎么进入项目?

最常见的链路是这样的:

flowchart LR
    A[".env 文件"] -->|Nuxt 启动时把变量放到| B["process.env"]
    B -->|变量配置分类| D["nuxt.config.ts<br><b>runtimeConfig</b>"]
    D -->|业务代码读取| E["useRuntimeConfig"]

下面这句代码意思就是:

process.env.NUXT_API_SECRET
  • 去当前运行环境里读取名为 NUXT_API_SECRET 的环境变量
  • 这个值通常来自 .env,也可能来自系统环境变量或部署平台配置

为什么变量名前面经常有 NUXT_PUBLIC_

这是 Nuxt 用来区分“这个值能不能给前端看到”的重要约定。

你现在先记住最实用的一层就够了:

  • NUXT_PUBLIC_ 开头的,通常是准备给前端也能访问的值
  • 不带 NUXT_PUBLIC_ 的,通常更适合服务端私有使用

2. 文件 nuxt.config.ts:Nuxt 的总开关

这是 Nuxt 项目的核心配置入口。绝大部分全局能力,都应该优先从这里理解。

先看一个比前文更完整、也更接近真实项目的示例:

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ['@pinia/nuxt', '@nuxtjs/tailwindcss'],
  css: ['~/assets/styles/main.scss'],
  app: {
    head: {
      title: 'Nuxt 4 Demo',
      meta: [
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'Nuxt 4 快速上手示例项目' }
      ]
    }
  },
  runtimeConfig: {
    apiSecret: process.env.NUXT_API_SECRET,
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  },
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { prerender: true },
    '/admin/**': { ssr: false },
    '/api/**': { cache: { maxAge: 60 * 5 } }
  },
  vite: {
    server: {
      port: 3000
    }
  },
  nitro: {
    compressPublicAssets: true
  }
})
mindmap
  root((Config))
    modules  接入生态能力
    css      全局样式引入
    runtimeConfig  运行时参数管理
    routeRules     路由渲染与缓存
    vite           前端构建配置
    nitro          服务端引擎配置

(1)modules:接入 Nuxt 模块生态

它的作用很直接:给 Nuxt 安装并启用框架级能力扩展。

可以把它理解成“Nuxt 官方推荐的扩展入口”

  • 作用:一键集成第三方扩展包 / 插件,自动为项目注入功能、简化配置、拓展 Nuxt 核心能力,不用你手动写繁琐的初始化、注册、兼容代码。
  • 举例说明:
    1. @pinia/nuxt 自动在 Nuxt 中注册 Pinia,无需手动写 createPinia() + app.use();直接在项目任意组件 / 页面中使用 useStore,无需重复引入。
    2. @nuxtjs/tailwindcss 自动配置 Tailwind 依赖、PostCSS、样式注入;自动识别项目中的 Tailwind 类名,无需手动创建 tailwind.config.js 基础配置;

(2)css:注册全局样式

这里注册的是整个应用都会生效的全局样式文件。

如果只是某个组件自己的样式,依然优先放回组件内部;css 更适合“全项目共享”的样式入口。

(4)app:全局基础配置

它管的是整个网站通用、所有页面都生效的设置,不是某一个页面,是全站统一的规则。

这里只写了head,对应网页源码里的 <head> 标签,全站统一配置网页头部,不用每个页面单独写。

(3)runtimeConfig:管理运行时配置与环境变量

这项配置是 Nuxt 里“怎么安全、统一地读取环境配置”的标准入口;public 里的给前后端共用,外面的只留给服务端。专门存放不能写死在代码里、会随环境变化、敏感保密的配置(比如接口密钥、接口地址),运行时自动加载,不用改代码。

很多新手困惑:不是已经有 .env 了吗,为什么还要多这一层?

这里最容易踩的坑是:把敏感信息放进 public,或者误以为 .env 本身就是配置系统。

最简单的理解是:.env:负责“提供原始变量值”,runtimeConfig:负责“把变量按 Nuxt 的规则组织起来,供项目读取”;

也就是说,你可以把 .env 里的值先交给 runtimeConfig,然后在项目里统一通过 useRuntimeConfig() 去读取,而不是到处直接写 process.env.xxx

先看一个最常见、也最实用的例子:

NUXT_API_SECRET=super-secret
NUXT_PUBLIC_API_BASE=https://api.example.com
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: process.env.NUXT_API_SECRET,
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
    }
  }
})

这段配置的意思其实很简单:

  • apiSecret 是服务端私有配置,例如第三方服务密钥。
  • public.apiBase 是公开配置,例如前端请求接口时要用的基础地址。

在代码里这样读取:

const config = useRuntimeConfig()

console.log(config.public.apiBase)

如果是在服务端代码里,还可以读取私有配置:

export default defineEventHandler(() => {
  const config = useRuntimeConfig()

  return {
    apiBase: config.public.apiBase,
    hasSecret: Boolean(config.apiSecret)
  }
})
graph LR
    A[".env"] --> C["nuxt.config.ts<br><b>runtimeConfig</b>"]
    
    C --> D["public"]
    C --> E["private"]
    
    D --> F["前端可读"]
    D --> G["服务端也可读"]
    D --> H["例子:api基础网址 / 网站标题"]
    
    E --> I["仅服务端可读"]
    E --> J["浏览器不可见"]
    E --> K["例子:api密码 / token / 数据库配置"]

(4)routeRules:控制页面级渲染与缓存策略

这项配置让你可以按路由粒度决定页面接口的行为,给网站里不同的网址路径,单独设置「怎么渲染、要不要缓存、能不能访问」的规则,不用全站统一设置,精准优化每个页面。

上面这个例子表达的意思分别是:

  • 首页构建时预渲染
  • 博客文章页也走预渲染
  • 后台页面关闭 SSR,走客户端渲染
  • 接口增加短时缓存

如果你在学 Nuxt 时只记住一个“和部署形态高度相关”的配置,那通常就是它。

(5)vite:向底层 Vite 传递配置

Nuxt 底层使用 Vite 作为开发和构建能力的一部分,因此当你确实需要改 Vite 行为时,可以从这里传配置。

常见场景包括:

  • 调整开发端口
  • 配置样式预处理器
  • 传递一部分 Vite 构建或开发参数

更稳妥的原则是:

  • 先找 Nuxt 自己有没有对应配置。
  • 只有 Nuxt 层没有、而底层 Vite 层确实需要改时,再用 vite

(6)nitro:向服务端引擎传递配置

nitro 是面向服务端引擎这一层的配置入口,常见用途包括:

  • 调整服务端输出行为
  • 配置压缩、缓存、部署相关细节
  • 对 Nitro 运行时做更底层的控制

它和 vite 的区别可以直接这样记:

  • vite 偏前端构建与开发链路
  • nitro 偏服务端运行与输出链路

对于刚接触 Nuxt 的读者来说,不需要一开始就深入 nitro 的所有细节,但至少要知道:Nuxt 不是只有 Vue 应用层配置,它还有服务端这一层。

如果想把这一节收束成最实用的判断原则,可以记住:

  • 和 Nuxt 整体行为相关的,先看 nuxt.config.ts
  • 和应用运行中要读到的公开配置相关的,再看 app.config.tsruntimeConfig

3. app/app.config.ts:公开且偏静态的应用配置

Nuxt 官方文档明确区分了 runtimeConfigapp.config。如果配置是:

  • 公开的,不敏感的
  • 更偏应用展示层
  • 更适合在构建期确定

那么它通常更适合放在 app/app.config.ts 里。

示例:

export default defineAppConfig({
  siteName: 'Nuxt 4 Demo',
  theme: {
    primaryColor: '#0ea5e9'
  }
})

在代码中读取:

const appConfig = useAppConfig()

如果你拿不准该放哪,可以这样判断:

  • 和密钥、环境变量、部署环境相关的,先想 runtimeConfig
  • 和站点标题、主题、公开开关相关的,先想 app.config

七、Nuxt 4 核心能力实操

这一部分不追求列全,而是优先讲 Nuxt 最有代表性的能力。

1. 文件系统路由

Nuxt 的页面路由来自 app/pages/ 目录。例如:

app/pages/
├── index.vue
├── about.vue
└── posts/
    └── [id].vue

它大致会生成这样的路由:

graph TD
    A["app/pages/index.vue"] --> B["/"]
    C["app/pages/about.vue"] --> D["/about"]
    E["app/pages/posts/[id].vue"] --> F["/posts/:id"]

这套规则的价值不只是“少写路由表”,而是让页面结构和 URL 结构天然对齐,项目越大越能体会到这种可读性。

2. 布局系统

布局放在 app/layouts/ 中,适合承载:

  • 顶部导航
  • 侧边栏
  • 页脚
  • 页面公共壳层

然后在 app.vue 中配合 <NuxtLayout> 使用。

如果某个页面需要特殊布局,也可以在页面中通过 definePageMeta 指定。这样做比“在每个页面里重复写头尾结构”更清晰,也更符合 Nuxt 的组织方式。

3. 自动导入

Nuxt 的自动导入是它最能提升开发手感的能力之一。根据官方文档,以下目录默认就有明显的自动导入能力:

  • app/components/
  • app/composables/
  • app/utils/

这意味着很多时候你不需要手动 import

<script setup lang="ts">
const count = useState('count', () => 0)
const doubled = computed(() => count.value * 2)
</script>

如果你更希望显式导入,Nuxt 也提供了 #imports 别名。

自动导入的好处很明显,但也要保持清醒:

  • 它提升了开发效率。
  • 但也会让“这个函数到底来自哪里”变得没那么直观。

所以团队协作里,通常建议对公共逻辑命名保持克制,不要让自动导入把语义搞得太散。

4. 数据获取

这一节是 Nuxt 最容易“看起来会用、实际上没用明白”的部分。因为在普通 Vue 项目里,大家很容易形成一种习惯:哪里要数据,就直接 fetch 一下。
但在 Nuxt 里,这样想往往不够,因为页面首屏数据获取要同时考虑:

  • 服务端渲染
  • payload 传递
  • hydration 复用
  • 避免客户端再次请求同一份数据
  1. $fetch 是基础能力,本质上是 Nuxt 中一个很强的同构请求工具,底层来自 ofetch。它能在服务端和客户端两边工作。你可以把它理解成:

    • 适合在服务端路由、插件、事件处理函数里直接使用

      • 但它本身不负责把 SSR 数据自动安全地传给客户端复用
  2. useAsyncData 是核心的 SSR 友好数据获取,是 Nuxt 数据获取体系里更底层、也更核心的组合式函数。它做的事情不是“帮你请求”,而是:

    • 包裹一个异步函数,在服务端执行它

    • 把结果放进 Nuxt 的数据传递链路

    • 在客户端 hydration 时复用这份结果,防止“二次获取”

    它有一个非常重要的点:需要一个唯一的key 来去重。

  3. useFetch 是最常用的便利层,可以理解成:useAsyncData + $fetch 的常用封装。当在页面或组件里“从某个 API 地址拿数据”时,它通常就是最自然的首选。

    它之所以常用,是因为:

    • 写法短

    • 自带 pendingerrorrefresh

    • 会根据 URL 和选项自动生成 key

    如果你只是想在页面里拿 /api/products 这种接口数据,先用 useFetch,通常就是最对路的选择。

SSR 友好的数据获取:Nuxt 怎么防止二次获取

Nuxt 通过“水合” (hydration) 过程来解决二次获取问题:数据在服务器上获取,页面被渲染成 HTML,获取到的数据被序列化并嵌入到 HTML 载荷(payload)中。在客户端,Nuxt 读取这个载荷并“水合”应用状态,从而避免了重新请求相同的数据。

flowchart LR
    A["服务端执行<br>useFetch/useAsyncData<br>获取数据"] --> C["渲染 HTML"]
    A --> D["写入 payload"]
    C --> E["返回浏览器"]
    D --> E
    E --> F["浏览器 hydration"]
    F --> G["复用 payload"]

这就是为什么 Nuxt 官方文档一直强调:页面初始化数据,不要在 <script setup> 里直接裸用 $fetch,否则很可能服务端请求一次,客户端 hydration 时又请求一次。

所以这一句一定要记住:$fetch 能请求数据,但不自动帮你解决 SSR 首屏复用;useFetchuseAsyncData 才会把数据接入 Nuxt 的 SSR 链路。

如果你想快速做判断,可以直接按这个规则记:

  • 页面首屏数据、希望 SSR 参与并防止二次获取:优先 useFetch
  • 页面首屏需要执行更灵活的异步逻辑:优先 useAsyncData
  • 点击按钮、提交表单、手动触发请求:直接用 $fetch
  • 页面初始化阶段,不要直接裸用 $fetch
  • useAsyncDatakey 一定要唯一

代码片段:最常见的 useFetch

<script setup lang="ts">
const { data: products, pending, error, refresh } = await useFetch('/api/products')
</script>

<template>
  <div v-if="pending">加载中...</div>
  <div v-else-if="error">错误: {{ error.message }}</div>
  <ul v-else>
    <li v-for="product in products" :key="product.id">{{ product.name }}</li>
  </ul>
  <button @click="refresh">刷新数据</button>
</template>

这个例子很典型,因为它基本覆盖了页面首屏取数最常见的需求:

  • data:取到的数据
  • pending:加载状态
  • error:错误状态
  • refresh:主动刷新

代码片段:useAsyncData 与唯一 key

<script setup lang="ts">
const route = useRoute()

const { data: post } = await useAsyncData(
  `post-${route.params.slug}`,
  () => $fetch(`/api/posts/${route.params.slug}`)
)
</script>

这段代码里最重要的不是 $fetch,而是前面的这个 key:

`post-${route.params.slug}`

虽然 useFetch 往往会自动生成 key,但对 useAsyncData,或者对 useFetch 那些 URL 本身不够独特的场景,手动提供清晰且唯一的 key 非常重要。

5. 状态共享

useState 本质上也是响应式状态,但它比普通 ref 多了一层 Nuxt 的 SSR 友好能力。可以把它理解成:

  • 它像 ref,会在使用相同 key 的地方共享状态
  • 但它会参与 Nuxt 的服务端到客户端状态传递
  • 它能在 hydration 时复用服务端已经生成好的状态,解决“水合不匹配”

所以它解决的根本问题,不只是“共享”,而是:让客户端以和服务端渲染时完全一致的初始状态启动。

如果没有这层机制,就很容易出现“服务端渲染出的是 A,客户端接管时算出来的是 B”,最终引发 hydration 不匹配。

最佳实践:把 useState 包装成组合式函数

而是把它包进一个组合式函数里。这样做的好处是:key 集中不易乱、类型更稳定、更容易复用

// app/composables/useCounter.ts
export const useCounter = () => useState<number>('counter', () => 0)
// 创建一个名为 `counter` 的共享状态,用 `0` 作为初始值
// 之后其他地方只要使用同样的 key,就会拿到同一份状态

然后在组件里这样使用:

<!-- app/components/TheCounter.vue -->
<script setup lang="ts">
const counter = useCounter()
</script>

<template>
  <div>
    <p>计数器: {{ counter }}</p>
    <button @click="counter++">+</button>
  </div>
</template>

代码注意事项

  • 不要在文件顶层直接用 ref 做全局状态

    这一点非常重要,尤其是对刚从普通 Vue 项目切过来的同学。下面这种写法在 Nuxt 的通用渲染应用里是有风险的:

    const counter = ref(0)
    

    如果它出现在文件顶层作用域,就可能变成服务端进程里的单例状态。这样一来,不同用户请求之间就有机会共享同一份状态,严重时甚至可能导致数据泄露。

  • useState 里的值要可序列化

    因为 useState 的值需要从服务端传到客户端,所以它本质上也会进入序列化流程。

    这意味着你放进去的数据最好是可序列化的,例如:字符串、数字、布尔值、数组、普通对象

    不推荐直接放进去的有:函数、类实例、带复杂原型链的对象

    可以简单理解成:能安全“打包后再还原”的数据,更适合放进 useState

扩展:何时以及为何集成 Pinia

useState 非常适合处理简单到中等复杂度的状态。对于复杂的全局状态管理,特别是当需要 actions、getters 以及 Vue DevTools 的时间旅行调试等高级功能时,那么官方推荐的方向就是 Pinia。Nuxt 也提供了专门的 Pinia 模块来帮你处理 SSR 集成。

你可以直接把两者理解成:

  • useState:轻量、直接、SSR 友好,适合大多数简单共享状态
  • Pinia:更完整的状态管理方案,适合复杂全局状态

6. 服务端 API

Nuxt 项目可以直接在 server/ 目录中写接口,通过文件命名约定来处理不同的 HTTP 方法。

  • 基本的 GET 端点:

    // server/api/hello.get.ts
    export default defineEventHandler(() => {
      return {message: 'hello from server'}
    })
    

    页面中直接调用:

    <script setup lang="ts">
    const { data } = await useFetch('/api/hello')
    </script>
    
  • 基本的 POST 端点:

    // server/api/users.post.ts
    export default defineEventHandler(async (event) => {
      const body = await readBody(event);
      // 创建新用户的逻辑...
      console.log('新用户:', body);
      setResponseStatus(event, 201); // 设置 HTTP 状态码
      return { success: true, user: body };
    });
    

这套体验的价值在于:

  • 前端页面和接口距离更近。
  • 本地联调成本更低。
  • 对中小型项目尤其友好。

很多人会说 “Nitro 就是 Nuxt 的服务端引擎”,这句话没错,但还不够完整。更准确地说,Nitro 是 Nuxt 的服务端运行时,负责接收请求、执行服务端逻辑、处理接口、参与 SSR 渲染、应用 routeRules,并生成可部署的服务端运行结果。

它主要承担这些核心能力:server/api/server/routes/ 路由处理、server/middleware/ 中间件、server/plugins/ 插件、SSR 服务端渲染执行、缓存、预渲染、重定向等路由规则,以及最终 .output/server 运行时产物的构建。

可以简单理解为:Nuxt 负责应用框架层面,Nitro 负责服务端执行层面,Nuxt 将 Vue 应用层与 Nitro 服务端层整合在了同一套工作流中。

Nitro 其一大核心优势便是同构 fetch 优化。在 SSR 渲染阶段调用内部 API 路由(如 useFetch('/api/hello'))时,Nuxt 并不会发起真实的 HTTP 网络请求,而是直接在当前进程内调用对应的事件处理函数。

这种机制彻底消除了网络开销与延迟,带来了显著的性能提升,这是传统前后端分离部署难以实现的。因此,在 Nuxt 中将 API 与前端代码同构存放,不只是开发上的便捷性,更是为了在服务端渲染阶段获得实打实的性能增益,也是 Nuxt 全栈方案的核心竞争力之一。

7. 服务端中间件

如果你需要在所有请求进入前做日志、鉴权上下文注入等处理,还可以使用 server/middleware/

// server/middleware/logger.ts
export default defineEventHandler((event) => {
  console.log(`[${event.method}] 新请求: ${getRequestURL(event).pathname}`);
});

代码注意事项:服务器中间件不应返回值或结束响应,其职责是修改 event 上下文或执行副作用操作。若返回值会导致请求短路,使其无法到达目标处理器。

8. 客户端路由中间件

对新手而言,服务器中间件(server/middleware/)和路由中间件(app/middleware/)的区别很容易混淆。

服务器中间件运行在 Nitro 服务端层面,处理原始 HTTP 请求,对所有请求生效,包括 API 与静态资源请求。

路由中间件则基于 Vue 和 vue-router 运行,在客户端或服务端页面导航时执行,不会作用于直接的 API 调用。

例如,路由中间件一个简单的登录保护:

// app/middleware/auth.ts
export default defineNuxtRouteMiddleware(() => {
  const user = useState('user')
  if (!user.value) {
    return navigateTo('/login')
  }
})

页面中使用:

<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

9. Head 管理基础

在 Nuxt 里,页面 <head> 相关内容不是后期随便拼接的,而是框架级能力。
从使用角度看,可以先分成两层:

  • 全局层:例如 nuxt.config.ts 里的 app.head,适合放全站通用的标题模板、基础 meta、favicon。
  • 页面层:例如在页面或组件里使用 useHead / useSeoMeta,适合根据当前页面数据动态设置标题和描述。

可以简单理解成:

  • 全局层解决“整个站点默认长什么样”
  • 页面层解决“当前这个页面要展示什么头部信息”

如果只写全局 app.head,当然能让站点具备基础头部信息;但如果你要真正做好 SEO,尤其是商品页、文章页、详情页这种“每页内容都不同”的页面,就必须进入页面级动态管理。

10. SEO 与元数据管理

Nuxt 的 SSR 基础天然对 SEO 友好,因为搜索引擎拿到的不是一个空壳 HTML,而是已经包含页面内容的首屏结构。
但 SSR 只是基础,真正把 SEO 做细,还需要把标题、描述、Open Graph、Twitter Card 这些元数据管起来。

这里最值得先掌握的三个工具是:

  • useHead
  • useSeoMeta
  • useHeadSafe

useHead:通用的 Head 管理工具

useHead 是最基础、也最通用的组合式函数。只要是合法的 head 标签内容,它基本都能管理,例如:titlemetalinkscripthtmlAttrsbodyAttrs

比如:

<script setup lang="ts">
useHead({
  title: '商品详情页',
  meta: [
    { name: 'description', content: '这是商品详情页' }
  ]
})
</script>

你可以把它理解成:

  • 能力最全
  • 适合需要自己精细控制 head 结构时使用
  • 但写法也相对更底层、更自由

useSeoMeta:更推荐的 SEO 写法

如果你的目标主要是 SEO,而不是任意 head 标签管理,那么更推荐优先使用 useSeoMeta

它的特点是:

  • 更贴近 SEO 场景
  • 类型更清晰
  • 能减少常见拼写错误

例如你不用自己纠结:

  • 这里应该写 name
  • 还是应该写 property

而是直接写更语义化的字段:titledescriptionogTitleogDescriptionogImagetwitterCard

所以从教程角度,可以直接给一个很实用的结论:

  • 想通用控制 head:用 useHead
  • 想专门做 SEO 元数据:优先 useSeoMeta

动态 SEO:产品页是最典型的场景

Nuxt 真正体现优势的地方,不是“能写一个静态 title”,而是可以结合页面数据,动态生成每个页面自己的 SEO 信息。

例如商品详情页:

<script setup lang="ts">
const { data: product } = await useFetch('/api/products/some-product')

useSeoMeta({
  title: () => `${product.value?.name} - 我的商店`,
  description: () => product.value?.description,
  ogTitle: () => `${product.value?.name} - 我的商店`,
  ogDescription: () => product.value?.description,
  ogImage: () => product.value?.imageUrl,
  twitterCard: 'summary_large_image'
})
</script>

这段代码最重要的不是 API 写法,而是它体现出的思路:

  • 页面数据先通过 useFetch 获取
  • SEO 标签再根据这份数据动态生成
  • 因为 Nuxt 支持 SSR,所以搜索引擎拿到的首屏里就已经包含这些元数据

这正是 Nuxt 对 SEO 友好的关键原因之一。

什么时候要用 useHeadSafe

如果你处理的是用户生成内容,或者来源不完全可控的数据,例如:

  • 用户输入的标题
  • CMS 后台可编辑的描述
  • 外部系统返回的富文本摘要

那么在把这些内容放进 head 时,要特别注意安全问题。

这时更适合使用 useHeadSafe,因为它会对输入内容做更安全的处理,避免把危险属性或值直接渲染进页面头部,从而降低 XSS 风险。

可以简单理解成:

  • 普通可控内容:useHead / useSeoMeta
  • 用户生成或不完全可信内容:优先考虑 useHeadSafe

实战里的最实用建议

如果你希望这部分先能落地,而不是一下子记一堆 API,可以先记住下面几条:

  • 全站默认头部放 app.head
  • 页面级动态 SEO 优先用 useSeoMeta
  • 需要更细粒度 head 控制时再用 useHead
  • 页面 SEO 最好绑定真实页面数据,而不是写死模板文本
  • 用户生成内容进入 head 时,优先考虑 useHeadSafe

八、Nuxt 4 渲染模式与部署思路

Nuxt 真正拉开和普通前端项目差距的地方,就在这里。你不只是“构建一个站点”,而是在决定每条路由应该怎样被渲染。

1. 默认并不只是 SPA

Nuxt 的默认优势之一,就是它天然适合 SSR。你不用先把 Vue 项目搭起来,再额外拼接 SSR 方案。

但 Nuxt 也不只支持 SSR,它可以在同一个项目里组合多种策略:

  • SSR(Server-Side Rendering,服务端渲染)
  • CSR(Client-Side Rendering,客户端渲染)
  • 预渲染
  • 混合渲染
  • Edge 部署

2. routeRules 是理解 Nuxt 渲染能力的关键

Nuxt 官方文档明确指出,Nitro 的 routeRules 可以对不同路径设置不同规则。

示例:

export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { prerender: true },
    '/api/**': { cache: { maxAge: 60 * 60 } },
    '/old-page': {
      redirect: {
        to: '/new-page',
        statusCode: 302
      }
    }
  }
})

这意味着同一个 Nuxt 项目里,你完全可以让:

  • 首页在构建时预渲染
  • 博客页按规则预渲染
  • 某些接口带缓存
  • 某些旧地址自动重定向

这就是 Nuxt 的“混合渲染”思路,它比“整个站点只有 SSR 或只有 SPA”灵活得多。

3. 预渲染不只是“导出静态 HTML”

根据官方文档,Nuxt 在预渲染时还会生成 _payload.json,其中包含 useAsyncDatauseFetch 产生的序列化数据。客户端导航时可以直接读取这些 payload,而不是重复请求。

这也是为什么前面一直强调:

  • 数据获取方式和渲染模式是连在一起的。
  • 不能把 Nuxt 中的数据获取简单当作普通前端里的异步请求。

九、Nuxt 4 核心架构与工作流程

这一章不再按“功能清单”来讲,而是按 Nuxt 真正的工作方式来拆。重点是把 Nuxt 的生成层、运行层、服务端层、客户端接管层,以及这些层之间怎么衔接讲清楚,尤其补清楚两个经常被讲虚的点:Nuxt 到底扫描了什么,以及应用真正的入口链路是什么。

1. Nuxt 到底是什么架构,不只是“Vue + SSR”

一个比较完整、能拿分的回答应该是:

Nuxt 不是简单把 Vue 套上 SSR,而是把 Vue 应用层、服务端引擎、构建生成层和部署产物层组织成统一工作流的全栈框架。

它至少可以拆成 4 层:

  • app/:Vue 应用层,放页面、布局、组件、composables、插件
  • server/:Nitro 服务端层,放 API、server middleware、server plugins、routes
  • .nuxt/:生成层,把约定式源码整理成可运行的应用骨架
  • .output/:部署层,生产环境真正运行的产物
graph TD
    A["源码"] --> B["app/"]
    A --> C["server/"]
    A --> D["nuxt.config.ts"]
    B --> E["Vue 应用层"]
    C --> F["Nitro 服务端层"]
    D --> G["全局配置层"]
    E --> H["Nuxt 生成层 .nuxt"]
    F --> H
    G --> H
    H --> I["开发运行"]
    H --> J["生产构建"]
    J --> K["部署产物 .output"]

2. Nuxt 到底扫描了什么:扫描对象、规则和结果

很多教程会说“Nuxt 会扫描目录”,但如果不继续说清楚“扫描什么、按什么规则扫描、扫描后拿这些结果做什么”,这一句其实帮助不大。

Nuxt 扫描的不是整个项目里所有文件,而是被框架约定为有特殊语义的目录和文件。最重要的几类包括:

  • app/pages/:扫描后生成页面路由
  • app/layouts/:扫描后生成布局映射
  • app/middleware/:扫描后生成客户端路由中间件映射
  • app/plugins/:扫描并自动注册 Nuxt 插件
  • app/components/:扫描后生成组件自动导入能力
  • app/composables/app/utils/:扫描后生成自动导入声明
  • server/api/:扫描后生成 /api/* 服务端路由
  • server/routes/:扫描后生成普通服务端路由
  • server/middleware/:扫描后挂载到 Nitro 请求链路
  • server/plugins/:扫描后在 Nitro 启动时执行

这里最关键的细节是:不同目录的扫描规则并不完全一样。

例如 app/plugins/ 并不是“递归扫描一切文件并全部注册”。官方文档明确说明:

  • 默认自动注册的是顶层插件文件
  • 子目录下的 index 文件目前也会被扫描,但这种形式已经不推荐长期依赖
  • 也就是说,自动注册是有边界和规则的,不是无差别递归

所以更准确地说,Nuxt 的扫描是:

  • 以目录约定为边界
  • 以文件位置和类型为规则
  • 以生成 .nuxt 中间结果为目的
flowchart LR
    A["app/ + server/ + nuxt.config.ts"] --> B["Nuxt 扫描器"]
    B --> C["路由记录"]
    B --> D["布局与中间件映射"]
    B --> E["插件注册表"]
    B --> F["自动导入声明"]
    B --> G["Nitro 路由与处理器"]
    C --> H["写入 .nuxt"]
    D --> H
    E --> H
    F --> H
    G --> H

3. 应用真正的入口是什么

“Nuxt 的入口是什么”这个问题特别容易回答得似是而非。

从应用视图树的角度看,app/app.vue 是 Nuxt 应用的根组件入口。官方文档也明确把 app.vue 定义为 Nuxt application 的 main component。

但这里一定要分清两个层次:

视图根入口:app.vue

app.vue 决定的是:

  • 整个应用最外层长什么样
  • 布局体系如何包裹页面
  • 当前页面最终渲染到哪里

最常见的写法是:

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

这里三者职责分别是:

  • app.vue:应用根组件
  • <NuxtLayout>:当前页面外面套哪层布局
  • <NuxtPage>:当前路由对应的页面组件渲染到哪里

这也解释了一个很容易忽略的点:

  • app/pages/ 只是被扫描并生成路由记录
  • 真正把当前页面显示出来的,是 app.vue 里的 <NuxtPage />

框架运行入口:.nuxt 里的生成入口

Nuxt 框架当然不是“从你手写的 app.vue 文件直接启动”的。更准确的说法是:

  • Nuxt 先生成自己的运行入口
  • 这个入口会把 app.vue 接成根组件
  • 然后再由 Nuxt / Vue 创建应用实例

所以要把这两层区分开:

  • app.vue 是应用视图树入口
  • .nuxt 里的生成入口是框架真正的运行入口

4. 执行 pnpm dev 后,Nuxt 内部到底发生了什么

这题不要回答成“就启动开发服务器”。更完整的回答是:

  1. 读取 nuxt.config.ts
  2. 扫描 app/server/
  3. 生成 .nuxt 与类型
  4. 启动开发服务器
  5. 等待请求进入
  6. 请求进入后,由 Nitro 和 Nuxt 一起处理 SSR/CSR 链路

更直观的流程可以看这张图:

flowchart TD
    subgraph 启动阶段
        direction LR
        A["执行 pnpm dev"] --> B["读取<br>nuxt.config.ts"]
        B --> C["扫描 app/ 和 server/"]
        C --> D["生成 .nuxt 与类型"]
        D --> E["启动 Nuxt Dev Server"]
    end

    subgraph 请求阶段
        direction LR
        F["浏览器请求页面"] --> G["Nitro<br>接收请求"]
        G --> H["创建<br>Nuxt实例<br>Vue 实例"]
        H --> I["执行<br>app<br>plugins<br>服务端相关逻辑"]
        I --> J["执行<br>页面校验<br>app middleware"]
        J --> K["渲染页面组件"]
        K --> L["执行<br>useFetch<br>useAsyncData"]
        L --> M["生成<br>HTML<br>payload"]
    end

    启动阶段 --> 请求阶段
    请求阶段 --> N["返回内容到浏览器"]
    N--> O["浏览器 hydration"]

这里最值得抓住的两个点通常是:

  • Nuxt 开发时不是直接跑源码,而是先生成 .nuxt
  • 请求进来不是直接进 Vue 组件,而是先过 Nitro,再进入 Nuxt 应用渲染链路

5. 一次页面请求进来以后,Nuxt 内部的执行顺序

按官方 Lifecycle 文档,可以整理成更适合理解框架执行链路的顺序:

服务启动时执行的部分

  • Nitro 启动
  • 执行 server/plugins/
  • 注册服务端钩子和运行时扩展

这里容易被问到一个细节:

  • server/plugins/ 更接近服务端启动初始化
  • 不是每个页面请求都重新执行一次的页面逻辑

每个请求都会走的部分

  1. 请求进入 Nitro
  2. 执行 server/middleware/
  3. 创建 Nuxt 与 Vue 应用实例
  4. 执行 app/plugins/
  5. 执行页面 validate
  6. 执行 app/middleware/
  7. 匹配布局、页面与组件树
  8. 执行 useFetch / useAsyncData
  9. 生成 HTML
  10. 把 HTML、payload、资源信息返回浏览器

这套顺序非常值得记,因为它能帮你回答很多追问:

  • server/middleware/app/middleware/ 有什么区别
  • 为什么插件比页面先执行
  • 为什么 useFetch 能参与 SSR

6. .nuxt 是什么:先生成再运行

官方文档明确说明,Nuxt 会生成 .nuxt/ 目录,而 nuxt prepare 也会专门创建 .nuxt 并生成类型。
这说明 .nuxt 不是无意义缓存,而是 Nuxt 的中间生成层

你可以这样答:

.nuxt 是 Nuxt 根据 app/server/nuxt.config.ts 等约定式源码,自动生成出来的可运行应用骨架。

它通常承载这些东西:

  • 路由生成结果
  • 自动导入声明
  • 类型文件
  • 插件注册结果
  • 应用运行入口
flowchart LR
    A["app/<br>server/<br>nuxt.config.ts"] --> B["Nuxt 扫描"]
    B --> C["生成 .nuxt"]
    C --> D["路由定义"]
    C --> E["自动导入声明"]
    C --> F["类型文件"]
    C --> G["插件注册和运行入口"]

换成更底层的理解方式,可以直接这样记:

  • 因为 Nuxt 先扫描目录约定
  • 再把结果生成进 .nuxt
  • 最后运行的其实是生成后的应用结构

7. .nuxt 和 .output 有什么区别

这题很经典,因为它能测出你有没有真正理解生成层和部署层。

标准区分方式是:

  • .nuxt:开发期 / 生成期的中间结果
  • .output:生产构建后真正部署和运行的最终结果
flowchart LR
    A["源码与配置"] --> B["nuxt build"]
    B --> C["生成客户端资源"]
    B --> D["生成 Nitro 服务端产物"]
    B --> E["应用 routeRules / prerender"]
    C --> F[".output"]
    D --> F
    E --> F

所以:

  • .nuxt 更像“运行前整理好的应用骨架”
  • .output 更像“真正拿去上线部署的产物”

官方部署文档也直接给出运行方式,例如:

node .output/server/index.mjs

这说明生产环境真正跑的不是源码目录,而是 .output

昨天以前首页

一文了解 pnpm,并快速上手操作!

作者 GentlyBeing
2026年4月1日 20:51

PNPM 简介

npm(Node Package Manager)中文名为 Node 包管理器,是 Node.js 官方自带、全球最大的 JavaScript 软件包管理工具,也是前端 / Node.js 开发最基础、最常用的工具之一。

pnpm(Performant NPM),翻译过来就是高性能的 npm,旨在解决传统包管理器(如 npm 和 Yarn Classic)在性能、磁盘空间使用和依赖管理结构上的不足。

目前,pnpm 已成为前端生态最受欢迎的包管理器之一,被 Vue、Vite、Nuxt、Next.js、Svelte 等顶级开源项目,以及字节、阿里、腾讯等大厂的前端团队广泛采用,是公认的「当前最先进的包管理工具」。

pnpm 比传统方案安装包的速度快了两倍,以下是官方给出的benchmarks(对比了npm, pnpm, Yarn Classic, and Yarn PnP),在多种常见情况下,执行install的速度比较

Graph of the alotta-files results

pnpm 的核心优势,源于它从底层架构上彻底重构了依赖管理方式,精准解决了传统工具的多个顽疾:

1. 解决“磁盘空间浪费”与“安装速度慢”

使用 npm 时,若你有 100 个项目都依赖同一个包,硬盘中就会重复存储 100 份该依赖包的完整副本,造成大量磁盘空间浪费。

pnpm 则通过硬链接搭配符号链接(软链接) 的机制,从根源上避免依赖重复拷贝,同时大幅提升安装效率:

  1. 增量存储:针对同一依赖包的不同版本,pnpm 仅会存储版本间存在差异的文件。例如新版本仅修改了单个文件,pnpm update 只需新增这一个文件至存储仓库,无需完整保存整个依赖包。
  2. 全局共享:pnpm 会在本地维护一个全局内容寻址存储库(默认路径为 ~/.pnpm-store),所有项目用到的依赖包,在全局仓库仅留存一份真实物理文件。
  3. 零拷贝:项目安装依赖时,pnpm 不会复制文件,而是通过硬链接(项目里 /node_modules/.pnpm/)指向全局仓库中的源文件;
  4. 兼容结构:最后通过软链接搭建符合 Node.js 规范的 node_modules 根目录结构,让项目和构建工具能正常识别依赖,同时为依赖隔离打下基础。

2. 解决“幽灵依赖” (Phantom Dependencies)

幽灵依赖是 npm 体系中一个极具隐患的问题。npm 会通过依赖扁平化(Dependency Hoisting) 机制,将间接依赖提升至 node_modules 根目录,这就导致项目可以直接引入并使用那些并未在自身 package.json 中声明的依赖包。这类依赖之所以能被访问,只是因为它们是其他直接依赖的子依赖,并在依赖提升后暴露在了根目录下。

幽灵依赖看似能简化开发,实则暗藏风险:

  1. 依赖版本不可控,易造成构建结果不稳定,排查问题时需要逐层追溯依赖树,调试与维护成本极高;
  2. 不同环境下依赖结构可能存在差异,极易出现本地正常、线上构建失败的情况;
  3. 间接依赖若存在安全漏洞,开发者往往难以感知,会带来潜在的安全风险。

pnpm 则通过虚拟存储(Virtual Store) 机制,从底层解决这一痛点:

它在项目内模拟传统 node_modules 的嵌套结构,同时在 .pnpm 目录下以包名 + 版本号的形式为每个版本创建独立文件夹,通过硬链接从全局仓库精准关联对应版本的真实文件;

再通过根目录node_modules 的符号链接,构建出严格的依赖隔离层级。

最终实现不同版本的依赖独立存放、精准引用、互不干扰,从根源上杜绝版本冲突,完美支持 Monorepo 场景下多子包的不同版本依赖,保障复杂依赖关系下项目的稳定运行。

3. 解决“多版本依赖冲突”

在实际项目中,常会出现不同第三方库依赖同一依赖包不同版本的情况(如部分组件库依赖 React 17,另一部分依赖 React 18)。

npm 针对该问题采用嵌套 node_modules 的基础隔离方案,将不兼容的版本安装在对应第三方包的内部目录中。但此方案存在明显缺陷:依赖目录结构变得杂乱无章,Windows 系统下易因路径过长导致报错,大量重复嵌套的依赖会造成磁盘空间浪费;加之 npm 保留的扁平化逻辑无法实现严格隔离,版本冲突与项目运行异常的风险始终存在。

pnpm 使用了一种叫做虚拟存储(Virtual Store) 机制。它在项目内模拟传统 node_modules 的嵌套结构,同时在 .pnpm 目录下以包名 + 版本号的形式为每个版本创建独立文件夹,通过硬链接从全局仓库精准关联对应版本的真实文件;再通过根目录 node_modules 的符号链接,构建出严格的依赖隔离层级。最终实现不同版本的依赖独立存放、精准引用、互不干扰,从根源上杜绝版本冲突,保障复杂依赖关系下项目的稳定运行。

总结:三层架构

层级 位置 内容性质 作用
L1: 全局仓库 ~/.pnpm-store 真实文件 节省磁盘空间,所有项目共享。
L2: 项目仓库 node_modules/.pnpm 硬链接 管理项目内复杂的依赖版本和嵌套关系。
L3: 暴露接口 node_modules/ 符号链接 方便构建工具寻找依赖。

pnpm.png

PNPM 安装

必须先安装 Node.js(npm 自带),先去官网装:nodejs.org/(选 LTS 版本)

# 安装 pnpm
npm install -g pnpm

# 检查是否安装成功
pnpm -v

# 初始化 pnpm
pnpm setup

# 尝试升级
pnpm self-update

# 国内换源
pnpm config set registry https://registry.npmmirror.com/

PNPM 快速上手

以下是列出常用的命令,具体可以参考官网管理依赖

安装依赖包

pnpm add <pkg>
命令 说明
pnpm add sax 安装并保存到 dependencies(生产依赖)
pnpm add -D sax 安装并保存到 devDependencies(开发依赖)
pnpm add -O sax 安装并保存到 optionalDependencies(可选依赖)
pnpm add -g sax 全局安装
pnpm add sax@next 安装 next 标签对应的版本
pnpm add sax@3.0.0 安装指定版本 3.0.0

安装项目全部依赖

pnpm install
# 或简写 pnpm i

更新依赖

pnpm update
# 或简写 pnpm up
命令 说明
pnpm up package.json 约定的版本范围内,更新所有依赖
pnpm up --latest 忽略版本范围约束,更新所有依赖到最新版
pnpm up foo@2 foo 更新到 v2 系列的最新版本
pnpm up "@babel/*" 更新 @babel scope 下的所有依赖

删除依赖

pnpm remove # 或简写 pnpm rm
pnpm uninstall # 或简写 pnpm un
# remove 和 uninstall 作用上完全等价

运行脚本

  • 执行 package.json 中定义的脚本:

    pnpm run <script>
    
  • 运行测试脚本:

    pnpm test
    
  • 运行启动脚本:

    pnpm start
    # 或
    pnpm run start
    

初始化 / 创建项目

create-*@foo/create-* 模板快速创建项目:

pnpm create <starter> [项目名]

示例:

pnpm create react-app my-app

常用进阶小技巧

  1. 清理无用依赖

    pnpm prune
    
  2. 查看依赖来源(排查冲突用)

    pnpm why react
    
  3. 删除 node_modules(比手动删更快)

    pnpm store prune
    
❌
❌