Vue 3 / Nuxt 开发笔记
这页适合已经会写基础前端、准备把 Vue 3 / Nuxt 3 用顺手的人。阅读时可以把它当成“从组件开发到全栈页面交付”的中间层笔记,而不是单纯 API 速查。
推荐学习顺序
如果你是第一次系统接触 Vue 3 / Nuxt,建议按这个顺序走:
- 先理解
ref、reactive、computed、watch - 再掌握组件通信、Composable 和页面级状态
- 然后理解
useFetch/useAsyncData/ 服务端 API 的边界 - 最后补 SEO、路由中间件、部署和环境变量
先把“响应式”和“数据流”理顺,后面的 SSR、缓存、SEO 才不容易踩坑。
Vue 3 Composition API
响应式
<script setup lang="ts">
const count = ref(0);
const doubled = computed(() => count.value * 2);
const user = reactive({
name: "Domi",
age: 20,
});
// 监听
watch(count, (newVal, oldVal) => {
console.log(`${oldVal} -> ${newVal}`);
});
// 监听多个
watch([count, () => user.name], ([c, n]) => {
console.log(c, n);
});
// 立即执行的副作用
watchEffect(() => {
console.log(`count is ${count.value}`);
});
</script>
生命周期
<script setup lang="ts">
onMounted(() => {
/* DOM 已挂载 */
});
onUnmounted(() => {
/* 清理 */
});
onBeforeUpdate(() => {
/* 更新前 */
});
</script>
组合式函数(Composable)
// composables/useMouse.ts
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e: MouseEvent) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
return { x, y };
}
Props 与 Emits
<script setup lang="ts">
const props = defineProps<{
title: string;
count?: number;
}>();
const emit = defineEmits<{
update: [value: string];
close: [];
}>();
// 带默认值
const props = withDefaults(
defineProps<{
size?: "sm" | "md" | "lg";
}>(),
{
size: "md",
},
);
</script>
Nuxt 常用模式
数据获取
<script setup lang="ts">
// SSR 友好的数据获取
const { data, pending, error, refresh } = await useFetch("/api/users");
// 带缓存键
const { data } = await useAsyncData("users", () => $fetch("/api/users"));
// 仅客户端
const { data } = await useFetch("/api/data", { lazy: true });
</script>
路由与中间件
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useUserStore();
if (!user.isLoggedIn) {
return navigateTo("/login");
}
});
页面中使用:
<script setup lang="ts">
definePageMeta({
middleware: "auth",
layout: "admin",
});
</script>
SEO
<script setup lang="ts">
useHead({
title: "页面标题",
meta: [{ name: "description", content: "页面描述" }],
});
// 或使用组件
useSeoMeta({
title: "页面标题",
ogTitle: "页面标题",
description: "页面描述",
ogDescription: "页面描述",
});
</script>
服务端 API
// server/api/hello.get.ts
export default defineEventHandler((event) => {
return { message: "Hello World" };
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return { created: true };
});
常见目录约定
Nuxt 的目录约定本身就是开发效率的一部分,推荐至少记住下面这些高频目录:
app/
components/ # 通用组件
composables/ # 复用逻辑
layouts/ # 页面框架
middleware/ # 路由守卫
pages/ # 文件路由
server/api/ # 服务端接口
server/utils/ # 服务端工具函数
content/ # 内容型站点数据
经验上:
- 组件负责展示,Composable 负责复用逻辑
- 页面负责拼装数据,不要把所有请求都塞进组件
- 服务端接口负责保护密钥、聚合第三方请求、做输入校验
数据获取怎么选
几个常见 API 的使用边界可以这样理解:
useFetch:页面 / 组件里最常用,支持 SSR,适合直接请求接口useAsyncData:更灵活,适合把请求逻辑封装成函数并复用缓存键$fetch:更像底层请求方法,适合在事件、Composable、服务端逻辑中手动调用
简单判断:
- 首屏要展示的数据:优先
useFetch/useAsyncData - 点击按钮后才请求:优先
$fetch - 依赖私密环境变量或第三方密钥:优先在
server/api里中转
常见坑
Hydration mismatch
服务端和客户端渲染结果不一致时,经常会出现 hydration 警告。高频原因包括:
- 模板里直接使用
window、document - 服务端渲染时取不到浏览器 API
- 使用了带随机值、当前时间、屏幕尺寸的初始渲染内容
遇到这类问题时,可以把仅浏览器执行的逻辑放到 onMounted 中,或者明确使用客户端组件策略。
Composable 里副作用过多
Composable 最容易被写成“隐形全局状态”。建议尽量避免:
- 在导入时立即发请求
- 在函数外层创建会跨页面污染的可变状态
- 把页面级业务规则和通用逻辑硬绑在一起
环境变量边界混乱
Nuxt 项目里,优先把公开配置放进 runtimeConfig.public,私密配置放进 runtimeConfig。不要把服务端密钥直接暴露给前端页面。
部署前检查
- 路由命名是否清晰,动态参数页是否能正确处理空值
- 页面首屏数据是否支持 SSR
useSeoMeta是否覆盖标题、描述、开放图基础字段- 环境变量是否区分本地、预览、生产
- 第三方接口是否经过服务端转发与兜底处理
延伸阅读
常用库
| 库 | 说明 |
|---|---|
| VueUse | Vue 组合式工具集 |
| Pinia | 状态管理 |
| UnoCSS | 原子化 CSS |
| Nuxt Content | 内容管理 |
| Nuxt Image | 图片优化 |