Nuxt.js核心原理深度解析
一句话概括
Nuxt.js 是基于 Vue.js 的全栈元框架,它通过约定优于配置的理念,将 Vue 应用的开发体验提升到一个全新的高度。Nuxt.js 的三大核心能力——自动导入(Auto-imports)、多渲染模式(Universal/SPA/Static)、模块系统——分别解决了 Vue 开发中的样板代码、部署灵活性和生态复用问题。与 Next.js 不同,Nuxt.js 的设计哲学更强调”开箱即用”的开发体验:无需手动配置路由、无需显式导入组件、无需关心构建工具。在 2026 年的 Vue 生态中,Nuxt.js(特别是 Nuxt 3+)已经成为构建生产级应用的标准选择。
背景与意义
Nuxt.js 的定位与演变
2016 年,当 Next.js 凭借 React 生态崭露头角时,Vue 社区缺少一个与之对等的全栈框架。Sebastien Chopin 创建了 Nuxt.js,初始目标是提供一个类似 Next.js 但基于 Vue 的 SSR 框架。
Nuxt.js 的演进轨迹清晰可见:
- Nuxt 1:简单的 Vue SSR 框架,模仿 Next.js 的文件路由。
- Nuxt 2:引入模块系统,生态爆发,支持多种渲染模式。
- Nuxt 3(2022+):全面拥抱 Vue 3、Vite、Nitro Server、组合式 API,完全重写。
到 2026 年,Nuxt 3 已经成为 Vue 生态中最成熟的全栈框架,被广泛用于电商、企业后台、内容平台等各类场景。
开发者痛点与 Nuxt 的解法
| 开发者痛点 | Nuxt.js 的解决方案 |
|---|---|
| 手动配置 Vue Router | 基于文件系统的自动路由 |
| 重复导入组件 | 自动导入 components/ 目录 |
| 引入/注册 composables | 自动导入 composables/ 目录 |
| 构建工具配置 | Vite 内置,零配置 |
| SSR/SSG 部署 | 三种渲染模式一键切换 |
| 服务端逻辑 | Nitro Server 内置 |
| 生态扩展 | 模块系统,安装即用 |
在面试场景中,Nuxt.js 是 Vue 开发者面试的高频话题。面试官通常从”说说 Nuxt.js 和 Vue CLI/Webpack 的区别”切入,然后深入到自动导入的实现原理、Nitro 引擎的工作方式、以及如何编写一个 Nuxt 模块。
概念与定义
什么是 Nuxt.js?
Nuxt.js 是一个基于 Vue.js 的全栈元框架,提供了:
- 文件系统路由:
pages/目录中的.vue文件自动映射为路由。 - 自动导入:
components/、composables/、utils/等目录中的文件自动注册。 - 多渲染模式:Universal(SSR)、SPA、Static(SSG)三种模式。
- Nitro Server:内置服务端引擎,支持 API 路由和中间件。
- 模块系统:通过安装模块扩展框架功能(如 auth、sitemap、pwa)。
核心目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
my-nuxt-app/
├── app.vue # 根组件(Nuxt 3,替代 layouts/default.vue)
├── pages/ # 页面路由文件
│ ├── index.vue # /
│ ├── about.vue # /about
│ └── blog/
│ └── [slug].vue # /blog/:slug
├── components/ # 自动导入的组件
│ ├── AppHeader.vue
│ └── blog/
│ ├── PostCard.vue
│ └── PostList.vue
├── composables/ # 自动导入的组合式函数
│ ├── useAuth.ts
│ └── useCounter.ts
├── layouts/ # 布局文件(可选)
│ ├── default.vue
│ └── custom.vue
├── middleware/ # 路由中间件
├── server/ # Nitro 服务端代码
│ ├── api/
│ │ └── hello.ts
│ └── middleware/
├── plugins/ # Vue 插件
├── nuxt.config.ts # Nuxt 配置文件
└── public/ # 静态资源(直接可访问)
Nuxt 3 的关键技术栈
- Vue 3:以 Composition API 为核心的响应式框架。
- Vite:基于 ES Module 的构建工具,开发服务器秒级启动。
- Nitro:轻量级服务端引擎,支持 Node.js、Serverless、Edge 等运行环境。
- h3:Nuxt 团队开发的轻量级 HTTP 框架,Nitro 的核心依赖。
核心知识点拆解
1. 自动导入机制(Auto-imports)的原理
自动导入是 Nuxt.js 最令人印象深刻的特性之一。当你创建 components/AppButton.vue 后,即可在任意页面或组件中使用 <AppButton />,无需 import。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- pages/index.vue -->
<!-- ❌ 不需要 import AppButton from '~/components/AppButton.vue' -->
<template>
<div>
<AppButton @click="handleClick">
点击我
</AppButton>
<BlogPostList />
</div>
</template>
<script setup lang="ts">
// ❌ 不需要 import { useCounter } from '~/composables/useCounter'
const counter = useCounter();
function handleClick() {
counter.increment();
}
</script>
自动导入的底层实现——Nuxt 在构建阶段扫描目录并生成自动导入声明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// nuxt 内部自动导入的实现(简化版)
import * as fs from 'fs';
import * as path from 'path';
import { generateImportStatement } from './import-utils';
interface AutoImportInfo {
name: string; // 导出名称,如 'AppButton'
as: string; // 导入后的名称,与 name 相同
from: string; // 源文件路径
}
class AutoImportScanner {
private scannedImports: AutoImportInfo[] = [];
// 扫描目录并收集自动导入信息
scanDirectories(rootDir: string) {
const scanTargets = [
{ dir: 'components', importType: 'component' },
{ dir: 'composables', importType: 'composable' },
{ dir: 'utils', importType: 'util' },
];
for (const { dir, importType } of scanTargets) {
const fullPath = path.join(rootDir, dir);
if (fs.existsSync(fullPath)) {
this.scanDirectory(fullPath, '', importType);
}
}
// 生成自动导入的虚拟模块
this.generateAutoImportModule();
}
// 递归扫描目录
private scanDirectory(
dirPath: string,
relativePath: string,
importType: string
) {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const newRelativePath = relativePath
? `${relativePath}/${entry.name}`
: entry.name;
if (entry.isDirectory()) {
// 递归扫描子目录
this.scanDirectory(fullPath, newRelativePath, importType);
} else if (/\.(vue|ts|js|tsx|jsx)$/.test(entry.name)) {
// 生成导入名称
const importName = this.generateImportName(newRelativePath, importType);
this.scannedImports.push({
name: importName,
as: importName,
from: fullPath
});
}
}
}
// 生成导入名称
// components/blog/PostCard.vue → BlogPostCard
// composables/useCounter.ts → useCounter
private generateImportName(filePath: string, importType: string): string {
// 去除扩展名
let name = filePath.replace(/\.\w+$/, '');
// 对于组件,首字母大写
if (importType === 'component') {
name = name
.split(/[/-]/)
.map(segment => {
// 目录名转为 PascalCase
return segment.charAt(0).toUpperCase() + segment.slice(1);
})
.join('');
// 移除目录层次(Nuxt 3 组件命名不再包含目录层次)
// 但如果存在命名冲突,保留层次
}
return name;
}
// 生成自动导入模块(虚拟文件)
private generateAutoImportModule() {
// 生成类似以下的代码:
// export { default as AppButton } from '~/components/AppButton.vue'
// export { useCounter } from '~/composables/useCounter'
const importStatements = this.scannedImports.map(info => {
// 对于 .vue 文件,使用 export default
if (info.from.endsWith('.vue')) {
return `export { default as ${info.name} } from '${info.from}'`;
}
// 对于 .ts/.js 文件,重新导出
return `export { ${info.name} } from '${info.from}'`;
});
// 这个虚拟模块会被注入到全局
// Vite 的 virtual module 机制实现
const virtualModuleCode = importStatements.join('\n');
// 注册到 Vite 的虚拟模块系统
// vite.config.ts 中:
// plugins: [{
// name: 'nuxt:auto-imports',
// resolveId(id) {
// if (id === '#nuxt-auto-imports') return '\0nuxt-auto-imports'
// },
// load(id) {
// if (id === '\0nuxt-auto-imports') return virtualModuleCode
// }
// }]
console.log(`[Nuxt AutoImport] 发现 ${this.scannedImports.length} 个自动导入项`);
}
}
// 类型声明(nuxt 自动生成的 .nuxt/types/auto-imports.d.ts)
// 这个文件确保 TypeScript 知道哪些变量无需导入
declare global {
// 组件
const AppButton: typeof import('~/components/AppButton.vue')['default'];
const BlogPostList: typeof import('~/components/blog/PostList.vue')['default'];
// Composables
const useCounter: typeof import('~/composables/useCounter')['useCounter'];
const useAuth: typeof import('~/composables/useAuth')['useAuth'];
// Utils
const formatDate: typeof import('~/utils/formatDate')['formatDate'];
}
2. 三种渲染模式的选择与切换
Nuxt 3 提供了三种渲染模式,可以通过 nuxt.config.ts 轻松切换。
1
2
3
4
5
6
7
// nuxt.config.ts
export default defineNuxtConfig({
// 渲染模式配置
ssr: true, // Universal 模式(SSR)
// ssr: false, // SPA 模式(CSR)
// 对于 SSG 模式,在 package.json 中使用 nuxt generate 命令
})
Universal 模式(SSR)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!-- pages/products/[id].vue -->
<script setup lang="ts">
// 1. useAsyncData: 组件挂载前获取数据
// 在服务端执行,数据自动嵌入 HTML
const { data: product, pending, error } = await useAsyncData(
'product',
() => $fetch(`/api/products/${useRoute().params.id}`)
);
// 2. useFetch: useAsyncData 的简化版本
const { data: reviews } = await useFetch(
`/api/products/${useRoute().params.id}/reviews`,
{
// 缓存策略
key: `reviews-${useRoute().params.id}`,
// 服务端获取 + 客户端也可再次获取
server: true,
lazy: true
}
);
// 3. 处理 loading 状态
if (pending.value) {
console.log('数据加载中...');
}
</script>
<template>
<div v-if="product">
<h1>{{ product.title }}</h1>
<p>{{ product.description }}</p>
<div v-if="reviews">
<ReviewList :reviews="reviews" />
</div>
<div v-else>
加载评论中...
</div>
</div>
<div v-else-if="error">
加载失败: {{ error.message }}
</div>
<div v-else>
加载中...
</div>
</template>
SPA 模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// nuxt.config.ts - SPA 模式
export default defineNuxtConfig({
ssr: false, // 关闭服务端渲染
// SPA 模式下,Nuxt 只生成一个空壳 HTML
// 所有渲染工作都在浏览器完成
// 可以为 SPA 模式单独配置路由
router: {
options: {
hashMode: false, // 使用 history 模式
scrollBehaviorType: 'smooth'
}
}
})
Static 模式(SSG)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// nuxt.config.ts - 静态生成
export default defineNuxtConfig({
ssr: true,
// 静态生成配置
generate: {
// 自动发现动态路由(尝试预生成)
routes: async () => {
// 从 API 获取所有需要预生成的动态路径
const posts = await $fetch('https://api.example.com/posts');
return posts.map((post: { slug: string }) => `/blog/${post.slug}`);
},
// 排除特定路由
exclude: ['/admin/**']
},
// 使用 nitro static 预设
nitro: {
preset: 'static'
}
})
3. Nitro Server 引擎
Nuxt 3 最大的创新之一是自建了 Nitro 引擎——一个轻量级、跨平台的服务端运行时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// server/api/products/[id].ts - API 路由
// 自动注册为 /api/products/:id
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const db = useDatabase(); // 自动注入的数据库连接
try {
const product = await db.products.findUnique({
where: { id: parseInt(id) },
include: { reviews: true }
});
if (!product) {
throw createError({
statusCode: 404,
statusMessage: 'Product not found'
});
}
return product;
} catch (error) {
// 统一错误处理
throw createError({
statusCode: 500,
statusMessage: 'Failed to fetch product'
});
}
});
// server/api/contact.post.ts - 只接受 POST 请求的方法路由
export default defineEventHandler(async (event) => {
const body = await readBody(event);
// 验证输入
if (!body.email || !body.message) {
throw createError({
statusCode: 400,
statusMessage: 'Email and message are required'
});
}
// 发送邮件(模拟)
console.log(`[Contact] From: ${body.email}, Message: ${body.message}`);
return {
success: true,
message: '感谢您的留言,我们会尽快回复'
};
});
// server/middleware/auth.ts - 服务端中间件
export default defineEventHandler(async (event) => {
// 跳过登录页的认证检查
const url = getRequestURL(event);
if (url.pathname === '/api/login' || url.pathname === '/api/register') {
return;
}
// 检查认证 token
const token = getHeader(event, 'authorization');
if (!token) {
throw createError({
statusCode: 401,
statusMessage: '未授权访问'
});
}
// 将用户信息注入事件上下文
try {
const user = await verifyToken(token);
event.context.user = user;
} catch {
throw createError({
statusCode: 401,
statusMessage: 'Token 无效或已过期'
});
}
});
Nitro 的跨平台能力——Nitro 最强大的特性是可以根据部署目标生成不同的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// nuxt.config.ts - 不同的部署配置
export default defineNuxtConfig({
nitro: {
// Node.js 服务器部署
preset: 'node-server',
// preset: 'node-cluster', // 多进程模式
// Serverless 部署
// preset: 'vercel', // Vercel
// preset: 'cloudflare', // Cloudflare Workers
// preset: 'aws-lambda', // AWS Lambda
// preset: 'netlify', // Netlify
// 静态部署
// preset: 'static',
// ES Module 模式
// preset: 'service-worker',
// 自定义输出目录
output: {
dir: 'dist',
serverDir: 'dist/server',
publicDir: 'dist/public'
},
// 存储配置(文件上传等)
storage: {
uploads: {
driver: 'fs',
base: './uploads'
}
}
}
});
4. 模块系统
Nuxt.js 的模块系统是其生态繁荣的关键。模块可以扩展框架的任何部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// modules/analytics.ts - 自定义模块
import { defineNuxtModule, createResolver, addComponent, addPlugin } from '@nuxt/kit';
// 模块元数据
export default defineNuxtModule({
meta: {
name: 'my-analytics',
version: '1.0.0',
configKey: 'analytics', // 模块配置键
compatibility: {
nuxt: '^3.0.0'
}
},
// 模块默认配置
defaults: {
id: '',
debug: false,
trackPageView: true
},
// 模块安装逻辑
setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
console.log(`[Analytics Module] Initializing with ID: ${options.id}`);
// 1. 添加自动导入的组合式函数
addImports({
name: 'useAnalytics',
as: 'useAnalytics',
from: resolver.resolve('./runtime/useAnalytics')
});
// 2. 注册插件
if (options.trackPageView) {
addPlugin(resolver.resolve('./runtime/plugin'));
}
// 3. 在构建完成后输出信息
nuxt.hook('build:done', () => {
console.log('[Analytics Module] Build complete');
});
// 4. 添加 nitro 插件
if (options.debug) {
nuxt.hook('nitro:config', (nitroConfig) => {
nitroConfig.plugins = nitroConfig.plugins || [];
nitroConfig.plugins.push(resolver.resolve('./runtime/nitro-plugin'));
});
}
}
});
// 在 nuxt.config.ts 中使用模块
export default defineNuxtConfig({
modules: [
// 官方模块
'@nuxtjs/tailwindcss',
'@nuxt/image',
'@vueuse/nuxt',
// 自定义模块
'~/modules/analytics',
],
// 模块配置
analytics: {
id: 'UA-XXXXX-Y',
debug: true,
trackPageView: true
}
});
实战案例
构建一个完整的博客系统
使用 Nuxt 3 构建一个包含服务端 API、SSR 渲染和交互功能的博客系统。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true,
modules: [
'@nuxtjs/tailwindcss',
'@nuxt/image',
'@vueuse/nuxt',
],
// 运行时配置(可以在运行时通过环境变量覆盖)
runtimeConfig: {
// 服务端私有配置
dbUrl: '',
jwtSecret: '',
// 客户端可访问的公开配置
public: {
siteName: 'Nuxt Blog',
siteUrl: 'https://blog.example.com'
}
},
app: {
head: {
title: 'Nuxt Blog',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
]
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// server/api/posts/index.get.ts - 获取文章列表
import { z } from 'zod';
// 查询参数验证
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(50).default(10),
tag: z.string().optional(),
search: z.string().optional()
});
export default defineEventHandler(async (event) => {
// 验证和解析查询参数
const query = await getValidatedQuery(event, querySchema.parse);
// 模拟数据库查询
const allPosts = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
title: `Nuxt 教程 - 第 ${i + 1} 篇`,
slug: `nuxt-tutorial-${i + 1}`,
excerpt: `这是 Nuxt.js 系列教程的第 ${i + 1} 篇文章,深入探讨...`,
content: `<p>这是第 ${i + 1} 篇文章的详细内容。</p>`,
author: '作者',
createdAt: new Date(2026, 7, (i % 28) + 1).toISOString(),
tags: ['Nuxt', 'Vue', '前端'].filter(() => Math.random() > 0.5),
readingTime: Math.floor(Math.random() * 15) + 3
}));
// 过滤
let filtered = allPosts;
if (query.tag) {
filtered = filtered.filter(post => post.tags.includes(query.tag!));
}
if (query.search) {
const s = query.search.toLowerCase();
filtered = filtered.filter(post =>
post.title.toLowerCase().includes(s) ||
post.excerpt.toLowerCase().includes(s)
);
}
// 分页
const total = filtered.length;
const totalPages = Math.ceil(total / query.limit);
const start = (query.page - 1) * query.limit;
const posts = filtered.slice(start, start + query.limit);
return {
posts,
pagination: {
page: query.page,
limit: query.limit,
total,
totalPages,
hasMore: query.page < totalPages
}
};
});
// server/api/posts/[slug].get.ts - 获取单篇文章
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug');
// 模拟查询数据库
const post = {
id: 1,
title: `文章: ${slug}`,
slug,
content: `<article>
<h2>引言</h2>
<p>这是一篇关于 "${slug}" 的技术文章。</p>
<p>Nuxt.js 通过其卓越的自动导入和模块系统...</p>
<h2>自动导入机制</h2>
<p>Nuxt 自动导入的底层实现建立在 Vite 的虚拟模块系统之上...</p>
</article>`,
author: { name: '张三', avatar: '/avatars/default.png' },
createdAt: '2026-08-15T10:00:00Z',
tags: ['Nuxt', '前端'],
relatedPosts: [
{ id: 2, title: '理解 Vue 3 Composition API', slug: 'vue3-composition-api' },
{ id: 3, title: 'Nitro 引擎深度解析', slug: 'nitro-engine-deep-dive' }
]
};
if (!post) {
throw createError({ statusCode: 404, statusMessage: '文章不存在' });
}
return post;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- app.vue - 根组件 -->
<template>
<div>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: opacity 0.3s ease;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!-- layouts/default.vue - 默认布局 -->
<script setup lang="ts">
const route = useRoute();
const { data: siteInfo } = await useFetch('/api/site-info');
// 动态设置页面标题
useHead({
titleTemplate: (titleChunk) => {
return titleChunk
? `${titleChunk} - ${siteInfo.value?.name || 'Nuxt Blog'}`
: 'Nuxt Blog';
}
});
</script>
<template>
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow-sm">
<nav class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<NuxtLink to="/" class="text-xl font-bold text-gray-800">
Nuxt Blog
</NuxtLink>
<div class="space-x-4">
<NuxtLink to="/" class="text-gray-600 hover:text-gray-800">
首页
</NuxtLink>
<NuxtLink to="/about" class="text-gray-600 hover:text-gray-800">
关于
</NuxtLink>
</div>
</nav>
</header>
<main class="max-w-4xl mx-auto px-4 py-8">
<slot />
</main>
<footer class="bg-white border-t mt-8">
<div class="max-w-4xl mx-auto px-4 py-6 text-center text-gray-500 text-sm">
© 2026 Nuxt Blog. Powered by Nuxt 3.
</div>
</footer>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<!-- pages/index.vue - 首页 -->
<script setup lang="ts">
// 服务端获取文章列表
const { data, pending, error, refresh } = await useFetch('/api/posts', {
query: { page: 1, limit: 9 },
// 缓存 60 秒(ISR 效果)
key: 'home-posts'
});
const posts = computed(() => data.value?.posts ?? []);
const pagination = computed(() => data.value?.pagination);
// 搜索功能
const searchQuery = ref('');
const showSearchResults = ref(false);
async function handleSearch() {
if (!searchQuery.value.trim()) return;
showSearchResults.value = true;
// 重新获取数据
await refresh();
}
// 更改页面
const currentPage = ref(1);
async function changePage(page: number) {
currentPage.value = page;
await refresh();
}
</script>
<template>
<div>
<!-- 搜索区域 -->
<div class="mb-8">
<input
v-model="searchQuery"
type="text"
placeholder="搜索文章..."
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
@keyup.enter="handleSearch"
/>
</div>
<!-- 文章列表 -->
<div v-if="pending" class="text-center py-12">
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto"></div>
<p class="mt-4 text-gray-500">加载中...</p>
</div>
<div v-else-if="error" class="text-center py-12 text-red-500">
加载失败: {{ error.message }}
</div>
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="post in posts"
:key="post.id"
class="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow p-6"
>
<NuxtLink :to="`/blog/${post.slug}`" class="block">
<h2 class="text-lg font-semibold text-gray-800 hover:text-blue-600 mb-2">
{{ post.title }}
</h2>
<p class="text-gray-600 text-sm mb-4 line-clamp-3">
{{ post.excerpt }}
</p>
<div class="flex items-center justify-between text-xs text-gray-400">
<span>{{ post.author }}</span>
<span>{{ formatDate(post.createdAt) }}</span>
</div>
<div class="mt-3 flex gap-2">
<span
v-for="tag in post.tags"
:key="tag"
class="px-2 py-1 bg-blue-50 text-blue-600 text-xs rounded"
>
{{ tag }}
</span>
</div>
</NuxtLink>
</article>
</div>
<!-- 分页 -->
<div v-if="pagination && pagination.totalPages > 1" class="mt-8 flex justify-center gap-2">
<button
:disabled="currentPage <= 1"
class="px-3 py-1 border rounded disabled:opacity-50"
@click="changePage(currentPage - 1)"
>
上一页
</button>
<span
v-for="p in pagination.totalPages"
:key="p"
class="px-3 py-1 border rounded cursor-pointer"
:class="{ 'bg-blue-500 text-white': p === currentPage }"
@click="changePage(p)"
>
{{ p }}
</span>
<button
:disabled="currentPage >= pagination.totalPages"
class="px-3 py-1 border rounded disabled:opacity-50"
@click="changePage(currentPage + 1)"
>
下一页
</button>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!-- pages/blog/[slug].vue - 文章详情页 -->
<script setup lang="ts">
const route = useRoute();
const slug = computed(() => route.params.slug as string);
// 服务端获取文章详情
const { data: post, pending, error } = await useAsyncData(
`post-${slug.value}`,
() => $fetch(`/api/posts/${slug.value}`)
);
// 设置页面头信息
useHead({
title: post.value?.title,
meta: [
{
name: 'description',
content: post.value?.excerpt
},
{
property: 'og:title',
content: post.value?.title
},
{
property: 'og:description',
content: post.value?.excerpt
}
]
});
// 格式化学时
function formatReadingTime(minutes: number): string {
if (minutes < 1) return '不到 1 分钟';
return `${minutes} 分钟`;
}
</script>
<template>
<div>
<div v-if="pending" class="text-center py-12">
<div class="animate-spin w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto"></div>
<p class="mt-4">加载文章中...</p>
</div>
<article v-else-if="post" class="bg-white rounded-lg shadow-sm p-8">
<!-- 文章头部 -->
<header class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ post.title }}</h1>
<div class="flex items-center gap-4 text-sm text-gray-500">
<div class="flex items-center gap-2">
<img
:src="post.author.avatar"
:alt="post.author.name"
class="w-8 h-8 rounded-full"
/>
<span>{{ post.author.name }}</span>
</div>
<span>{{ formatDate(post.createdAt) }}</span>
<span>{{ formatReadingTime(post.readingTime) }} 阅读</span>
</div>
<div class="mt-4 flex gap-2">
<NuxtLink
v-for="tag in post.tags"
:key="tag"
:to="`/?tag=${tag}`"
class="px-3 py-1 bg-blue-50 text-blue-600 text-sm rounded hover:bg-blue-100"
>
{{ tag }}
</NuxtLink>
</div>
</header>
<!-- 文章内容 -->
<div
class="prose prose-lg max-w-none"
v-html="post.content"
/>
<!-- 相关文章 -->
<section v-if="post.relatedPosts?.length" class="mt-12 pt-8 border-t">
<h2 class="text-xl font-semibold mb-4">推荐阅读</h2>
<div class="grid md:grid-cols-2 gap-4">
<NuxtLink
v-for="related in post.relatedPosts"
:key="related.id"
:to="`/blog/${related.slug}`"
class="p-4 bg-gray-50 rounded hover:bg-gray-100 transition-colors"
>
<h3 class="font-medium text-gray-800">{{ related.title }}</h3>
</NuxtLink>
</div>
</section>
<!-- 评论区域(Client Component) -->
<ClientOnly>
<CommentSection :post-id="post.id" />
</ClientOnly>
</article>
<div v-else-if="error" class="text-center py-12">
<h2 class="text-2xl font-bold text-red-500 mb-2">文章加载失败</h2>
<p class="text-gray-500">{{ error.message }}</p>
<NuxtLink to="/" class="mt-4 inline-block text-blue-500 hover:underline">
返回首页
</NuxtLink>
</div>
</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!-- components/CommentSection.vue - 评论组件(客户端交互) -->
<script setup lang="ts">
interface Comment {
id: number;
author: string;
content: string;
createdAt: string;
likes: number;
}
const props = defineProps<{
postId: number;
}>();
const comments = ref<Comment[]>([]);
const loading = ref(true);
const newComment = ref('');
const submitting = ref(false);
// 仅在客户端加载评论
onMounted(async () => {
try {
comments.value = await $fetch(`/api/posts/${props.postId}/comments`);
} catch (e) {
console.error('Failed to load comments:', e);
} finally {
loading.value = false;
}
});
async function submitComment() {
if (!newComment.value.trim() || submitting.value) return;
submitting.value = true;
try {
const result = await $fetch(`/api/posts/${props.postId}/comments`, {
method: 'POST',
body: { content: newComment.value }
});
comments.value.unshift(result);
newComment.value = '';
} catch (e) {
console.error('Failed to submit comment:', e);
} finally {
submitting.value = false;
}
}
async function likeComment(commentId: number) {
try {
await $fetch(`/api/comments/${commentId}/like`, { method: 'POST' });
const comment = comments.value.find(c => c.id === commentId);
if (comment) comment.likes++;
} catch (e) {
console.error('Failed to like comment:', e);
}
}
</script>
<template>
<div class="comments-section mt-8">
<h3 class="text-xl font-semibold mb-4">评论 ({{ comments.length }})</h3>
<!-- 评论输入 -->
<div class="mb-6">
<textarea
v-model="newComment"
placeholder="写下你的评论..."
class="w-full px-4 py-3 border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
rows="3"
/>
<button
:disabled="submitting || !newComment.trim()"
class="mt-2 px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50 hover:bg-blue-600"
@click="submitComment"
>
{{ submitting ? '提交中...' : '发表评论' }}
</button>
</div>
<!-- 评论列表 -->
<div v-if="loading" class="text-center py-4 text-gray-500">
加载评论中...
</div>
<div v-else-if="comments.length === 0" class="text-center py-4 text-gray-500">
暂无评论,快来抢沙发吧!
</div>
<div v-else class="space-y-4">
<div v-for="comment in comments" :key="comment.id" class="p-4 bg-gray-50 rounded-lg">
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-gray-800">{{ comment.author }}</span>
<span class="text-xs text-gray-400">{{ formatDate(comment.createdAt) }}</span>
</div>
<p class="text-gray-700">{{ comment.content }}</p>
<button
class="mt-2 text-sm text-gray-500 hover:text-red-500"
@click="likeComment(comment.id)"
>
👍 {{ comment.likes }}
</button>
</div>
</div>
</div>
</template>
底层原理
Nitro 引擎的跨平台输出机制
Nitro 是 Nuxt 3 最核心的底层创新。理解 Nitro 如何实现”一份代码,多处运行”的机制,是理解 Nuxt 3 架构的关键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Nitro 的核心构建流程(简化版)
class NitroBuilder {
private options: {
preset: string;
inputDir: string;
outputDir: string;
};
constructor(options: any) {
this.options = options;
}
async build() {
// 1. 收集服务端代码
const serverFiles = await this.collectServerFiles();
// 2. 将文件打包成 Rollup bundle
const bundle = await this.bundle(serverFiles);
// 3. 根据目标预设生成输出
const preset = this.loadPreset(this.options.preset);
const output = await preset.transform(bundle);
// 4. 写入输出目录
await this.writeOutput(output);
}
private async collectServerFiles(): Promise<ServerFile[]> {
const files: ServerFile[] = [];
// 扫描 server/ 目录
const scanDir = (dir: string, basePath: string = '') => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const routePath = path.join(basePath, entry.name);
if (entry.isDirectory()) {
scanDir(fullPath, routePath);
} else {
// 解析文件为路由处理器
files.push({
path: fullPath,
route: this.parseRoute(routePath),
handler: fullPath
});
}
}
};
scanDir(this.options.inputDir);
return files;
}
// 解析文件路径为路由
// server/api/users/[id].get.ts → GET /api/users/:id
private parseRoute(filePath: string): RouteDefinition {
// 去除扩展名
let route = filePath.replace(/\.\w+$/, '');
// 提取 HTTP 方法(如果有)
let method: string | undefined;
const methodMatch = route.match(/\.(get|post|put|delete|patch)$/i);
if (methodMatch) {
method = methodMatch[1].toUpperCase();
route = route.replace(/\.(get|post|put|delete|patch)$/i, '');
}
// 转换动态路径
route = route.replace(/\[(\w+)\]/g, ':$1');
return { path: route, method };
}
// 加载预设
private loadPreset(name: string): BuildPreset {
const presets: Record<string, BuildPreset> = {
'node-server': {
// 输出为 Node.js http.createServer handler
async transform(bundle: any) {
return {
entry: bundle.entry,
type: 'node',
handler: bundle.entry
};
}
},
'vercel': {
// 输出为 Vercel Serverless Functions 格式
async transform(bundle: any) {
return {
entry: bundle.entry,
type: 'vercel',
config: {
routes: [
{ src: '/api/(.*)', dest: '/api/$1' }
]
}
};
}
},
'cloudflare': {
// 输出为 Cloudflare Workers 格式
async transform(bundle: any) {
return {
entry: bundle.entry,
type: 'cloudflare-module',
exportName: 'default'
};
}
}
};
return presets[name];
}
}
// Nitro 的运行时抽象层
// 不同平台的适配器实现了统一的接口
interface RuntimeAdapter {
// 处理请求
handleRequest(request: Request): Promise<Response>;
// 运行时配置
runtimeConfig: Record<string, any>;
// 存储系统
storage: {
get(key: string): Promise<any>;
set(key: string, value: any): Promise<void>;
delete(key: string): Promise<void>;
};
}
// Node.js 适配器
class NodeAdapter implements RuntimeAdapter {
async handleRequest(request: Request): Promise<Response> {
// 将 Node.js 的 req/res 转换为 Web 标准 Request/Response
// 使用 h3 库进行转换
return new Response('Hello from Node.js');
}
runtimeConfig = {};
storage = new Map();
}
// Cloudflare Workers 适配器
class CloudflareAdapter implements RuntimeAdapter {
async handleRequest(request: Request): Promise<Response> {
// 使用 Cloudflare Workers API
return new Response('Hello from Cloudflare');
}
runtimeConfig = {};
storage = {
async get(key: string) {
// 使用 KV 存储
return null;
},
async set(key: string, value: any) {},
async delete(key: string) {}
};
}
useAsyncData 和 useFetch 的数据获取机制
Nuxt 3 的数据获取函数是 SSR 的关键桥梁。理解它们的内部机制,有助于在复杂场景中做出正确的选择。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// useAsyncData 的简化实现
function useAsyncData<T>(
key: string, // 唯一标识
handler: () => Promise<T>, // 数据获取函数
options?: {
server?: boolean; // 是否在服务端执行
lazy?: boolean; // 是否延迟加载
default?: () => T; // 默认值
transform?: (data: T) => any; // 数据转换
pick?: string[]; // 只选取部分字段
watch?: any[]; // 响应式监听
immediate?: boolean; // 是否立即执行
}
) {
// 1. 创建响应式状态
const data = ref(options?.default?.() ?? null) as Ref<T | null>;
const pending = ref(true);
const error = ref<Error | null>(null);
// 2. 检查服务端注水的数据
if (import.meta.server) {
// 服务端:执行 handler 并注水
const nuxtApp = useNuxtApp();
// 注册到服务端 payload 中
if (!nuxtApp._dataCache) {
nuxtApp._dataCache = {};
}
// 如果该 key 已经缓存,跳过
if (!nuxtApp._dataCache[key]) {
// 设置响应标记,让 Nuxt 知道需要等待此数据
nuxtApp.ssrContext?.onBeforeRender(() => {
nuxtApp._dataCache[key] = true;
});
// 执行数据获取
handler().then(result => {
data.value = options?.transform
? options.transform(result)
: result;
pending.value = false;
// 将数据注入 SSR 上下文
nuxtApp.payload.data[key] = data.value;
}).catch(err => {
error.value = err;
pending.value = false;
});
} else {
// 已有缓存数据
data.value = nuxtApp.payload.data[key];
pending.value = false;
}
} else {
// 客户端:检查服务端是否已经注水
const nuxtApp = useNuxtApp();
const serverData = nuxtApp.payload.data[key];
if (serverData !== undefined && !options?.lazy) {
// 服务端已经获取过数据,复用
data.value = serverData;
pending.value = false;
} else if (!options?.lazy) {
// 服务端没有数据(可能是 SPA 模式),客户端获取
handler().then(result => {
data.value = options?.transform
? options.transform(result)
: result;
pending.value = false;
}).catch(err => {
error.value = err;
pending.value = false;
});
}
// 如果是 lazy 模式,暂停在 pending 状态
// 直到组件在客户端首次访问
}
return {
data: readonly(data),
pending: readonly(pending),
error: readonly(error),
refresh: () => {
// 重新获取数据
pending.value = true;
error.value = null;
return handler().then(result => {
data.value = options?.transform
? options.transform(result)
: result;
pending.value = false;
}).catch(err => {
error.value = err;
pending.value = false;
});
},
execute: () => {
// lazy 模式下手动触发
}
};
}
// 服务端注水 payload 的结构
// 在 HTML 中嵌入:
// <script>
// window.__NUXT__ = {
// data: {
// 'post-123': { title: '...', content: '...' },
// 'home-posts': { posts: [...], pagination: {...} }
// }
// }
// </script>
高频面试题解析
面试题 1:Nuxt.js 的自动导入是如何实现的?为什么不需要 import 语句就能使用组件和组合式函数?
答案要点:
Nuxt 自动导入基于以下机制:
扫描阶段(Build Time):Nuxt 在构建时扫描
components/、composables/、utils/等目录,收集所有文件信息。命名转换:根据文件路径生成全局唯一名称。例如
components/blog/PostCard.vue→BlogPostCard,composables/useAuth.ts→useAuth。虚拟模块:Nuxt 生成一个 Vite 虚拟模块(
#nuxt-auto-imports),包含所有自动导入项的 export 语句。TypeScript 声明生成:自动生成
.nuxt/types/auto-imports.d.ts文件,TypeScript 知道这些变量是全局可用的,不会报错。Unplugin 插件:Nuxt 使用 unplugin-auto-import 库,通过 Vite/Rollup 插件拦截未声明的全局变量引用,自动插入 import 语句。
核心价值:自动导入不是”魔法”,而是构建时的代码生成。它省去了手动导入的样板代码,减少了重构时的导入路径维护成本。
面试题 2:Nuxt 3 中的渲染模式有哪些?如何在它们之间切换?
答案要点:
Nuxt 3 支持三种渲染模式:
- Universal(SSR)模式(
ssr: true):- 服务端在请求时渲染完整 HTML。
- 客户端接收 HTML 后激活(hydration)。
- 适合需要 SEO 和首屏速度的场景。
- 使用
nuxt dev/nuxt build+ Node.js 服务器运行。
- SPA 模式(
ssr: false):- 服务端只返回一个空壳 HTML 和 JavaScript 链接。
- 所有渲染工作在浏览器完成。
- 适合管理后台等不需要 SEO 的应用。
- 使用
nuxt dev/nuxt build+ 静态文件服务运行。
- Static(SSG)模式:
- 构建时预生成所有页面的静态 HTML。
- 使用
nuxt generate命令生成。 - 适合内容不频繁变化的网站。
- 可以直接部署到任何静态托管服务。
切换方式:
1
2
3
4
5
6
7
8
9
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // Universal 模式
// ssr: false, // SPA 模式
// 如果要生成静态站点,运行:
// npx nuxi generate
// 并在 generate.routes 中配置动态路由
})
面试题 3:useAsyncData 和 useFetch 有什么区别?分别在什么场景下使用?
答案要点:
useFetch 是 useAsyncData 的封装,提供了更便捷的 API:
useFetch自动处理 URL 和选项,直接使用$fetch进行网络请求。useAsyncData更通用,可以执行任意异步操作(不仅仅是 HTTP 请求)。
使用场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 场景 1:调用内部 API(推荐 useFetch)
const { data: posts } = await useFetch('/api/posts');
// 等同于:
const { data: posts } = await useAsyncData('posts', () => $fetch('/api/posts'));
// 场景 2:调用外部 API
const { data: weather } = await useFetch('https://api.weather.com/current', {
baseURL: '' // 外部 API 需要空 baseURL
});
// 场景 3:非 HTTP 操作(使用 useAsyncData)
const { data: processedData } = await useAsyncData('process', async () => {
const raw = await $fetch('/api/raw-data');
return complexProcessing(raw); // 对数据进行服务端处理
});
// 场景 4:数据库查询(在 server/ 中定义 API)
// server/api/stats.ts
export default defineEventHandler(async () => {
const db = useDatabase();
return db.query('SELECT COUNT(*) as total FROM users');
});
// pages/index.vue
const { data: stats } = await useFetch('/api/stats');
面试题 4:如何编写一个 Nuxt 模块?请描述模块的核心 API 和生命周期。
答案要点:
编写一个 Nuxt 模块的核心步骤:
- 创建模块文件:使用
defineNuxtModule定义模块。 - 配置模块元数据:name、version、configKey、compatibility 等。
- 实现 setup 函数:在 setup 中扩展 Nuxt 的功能。
核心 API(来自 @nuxt/kit):
| API | 作用 |
|---|---|
addComponent | 注册全局组件 |
addImports / addImportsDir | 注册自动导入 |
addPlugin | 注册 Vue 插件 |
addServerHandler | 添加 API 路由 |
addServerPlugin | 注册 Nitro 插件 |
extendPages | 扩展页面路由 |
addRouteMiddleware | 添加路由中间件 |
useLogger | 日志记录 |
模块生命周期:
1
2
nuxt:ready → modules:done → pages:extend →
components:extend → build:before → build:done
示例——一个简单的 i18n 模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// modules/i18n/index.ts
import { defineNuxtModule, createResolver, addPlugin, addImportsDir } from '@nuxt/kit';
export default defineNuxtModule({
meta: { name: 'nuxt-i18n' },
defaults: { locales: ['zh', 'en'], defaultLocale: 'zh' },
setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
// 注入运行时配置
nuxt.options.runtimeConfig.public.i18n = options;
// 注册插件
addPlugin(resolver.resolve('./runtime/plugin'));
// 注册 auto-imports
addImportsDir(resolver.resolve('./runtime/composables'));
}
});
面试题 5:Nuxt 3 中的 Nitro 引擎是什么?它解决了什么问题?
答案要点:
Nitro 的定义:Nitro 是 Nuxt 3 自建的服务端引擎,负责处理 API 路由、中间件、服务端渲染和数据获取。
解决的问题:
跨平台部署:同一套服务端代码可以部署到 Node.js、Vercel、Cloudflare Workers、AWS Lambda、Netlify Edge 等不同平台,Nitro 会自动适配。
自动路由:
server/api/目录中的文件自动注册为 API 路由,无需手动配置路由表。类型安全:自动生成 API 的类型定义,前端调用时获得完整的 TypeScript 类型提示。
前后端 API 共享:
useFetch('/api/posts')在服务端和客户端都能工作——服务端直接调用 handler,客户端发起 HTTP 请求。独立打包:服务端代码与 Vue 应用解耦,可以被作为独立的 Node.js 服务器运行。
Nitro 的技术基础:基于 h3 HTTP 框架(Nuxt 团队开发)和 Rollup 构建,输出格式根据预设目标自动适配。
总结与扩展
知识体系
Nuxt.js 的知识图谱:
- 基础层:页面定义、组件自动导入、布局系统、路由中间件。
- 数据层:useAsyncData、useFetch、useState、服务端 API。
- 运行时层:Nitro 引擎、渲染模式切换、服务端中间件。
- 生态层:模块系统、常用模块(Auth、Sitemap、PWA、Image)。
- 部署层:Node.js 服务器、Vercel/Netlify、Cloudflare Workers、Docker。
- 进阶层:自定义模块开发、Nitro 预设编写、性能优化、边缘渲染。
Nuxt vs Next:核心差异
| 维度 | Nuxt.js | Next.js |
|---|---|---|
| 前端框架 | Vue 3 | React |
| 自动导入 | 组件 + composables + utils | 无(需要手动 import) |
| 服务端引擎 | Nitro(自研) | Node.js / Edge Runtime |
| 模块生态 | 官方模块 + 社区模块 | Vercel 生态 + 社区 |
| 构建工具 | Vite | Webpack / Turbopack |
| 数据获取 | useAsyncData / useFetch | getStaticProps / getServerSideProps |
| 渲染模式 | Universal / SPA / Static | SSG / SSR / ISR |
延伸阅读
- Nuxt 3 官方文档 — 最权威的参考资料
- Nuxt 3 GitHub — 源码阅读
- Nitro 引擎文档 — 独立于 Nuxt 的 Nitro 项目
- h3 HTTP 框架 — Nitro 的底层 HTTP 库
- Nuxt 模块教程 — 官方模块开发指南
- Vue.js 官方文档 — Nuxt 的基础框架
Nuxt.js 的设计哲学是”让开发者关注业务逻辑而非配置”。理解其自动导入、渲染模式和 Nitro 引擎的原理,不仅能在面试中展现 Vue 架构的深度理解,更能在实际项目中做出正确的技术选型和架构决策。