Nuxt 3 入门指北 — 从零搭建一个现代博客
记录我使用 Nuxt 3 + Tailwind CSS + Nuxt Content 搭建个人博客的完整过程,涵盖项目初始化、文件路由、内容管理和暗色模式配置。
Vue 2 时代,我们写组件依赖 Options API——把 data、methods、computed、watch 分别塞进不同选项里。小型组件还好,但当组件逻辑变复杂时,同一个功能的代码被拆散到不同选项中,阅读和维护都很痛苦。Vue 3 引入的 Composition API 彻底改变了这一点:按逻辑关注点组织代码,而非按选项类型。
打个比方:Options API 像是把所有忍术卷轴分别放在「火遁架」「水遁架」「雷遁架」上——你要修改一个完整的忍术,就得翻好几个架子。Composition API 则把每个忍术的教学内容收在一卷里,翻阅起来一气呵成。
ref 和 reactive 是 Composition API 中最常用的两个响应式 API,但很多初学者搞不清什么时候用哪个。
import { ref } from 'vue'
const count = ref(0)
const name = ref('Naruto')
const isVisible = ref(true)
// 在 script 中访问需要 .value
console.log(count.value) // 0
// 在 template 中自动解包
// <span>{{ count }}</span>
ref 适合包装基本类型(string、number、boolean),但也可以包装对象。内部原理是用一个带有 .value 属性的响应式对象包装值。
import { reactive } from 'vue'
const state = reactive({
user: { name: 'Naruto', level: 99 },
missions: ['S级任务', 'A级任务']
})
// 不需要 .value,直接访问
console.log(state.user.name) // 'Naruto'
reactive 通过 Proxy 代理整个对象,适合管理一组相关的状态。但它有几个限制:
⚠️ 注意:
reactive不能包装基本类型;解构会丢失响应式;不能整体替换(state = newState不会触发更新)。
┌─────────────────────────────────────┐
│ 简单值(string, number, bool) → ref │
│ 单条数据(article, user) → ref │
│ 一组紧密耦合的状态 → reactive │
│ 不确定的时候 → ref │ ← 最安全的选择
└─────────────────────────────────────┘
import { ref, computed } from 'vue'
const missions = ref([
{ rank: 'S', reward: 10000 },
{ rank: 'A', reward: 5000 },
{ rank: 'B', reward: 1000 }
])
// 自动追踪依赖,只在 missions 变化时重新计算
const totalReward = computed(() =>
missions.value.reduce((sum, m) => sum + m.reward, 0)
)
const sRankMissions = computed(() =>
missions.value.filter(m => m.rank === 'S')
)
computed 有缓存机制——依赖不变时直接返回缓存值,不会重复计算。别把它和普通函数搞混:模板里多次引用 totalReward,只会执行一次计算函数。
import { ref, watch } from 'vue'
const searchQuery = ref('')
const results = ref([])
// 基础用法:监听 ref 变化
watch(searchQuery, async (newQuery, oldQuery) => {
if (newQuery.trim()) {
results.value = await fetchSearchResults(newQuery)
}
})
// 监听多个源
watch([searchQuery, filterOptions], ([newQ, newFilter]) => {
console.log('搜索条件变动')
})
// 深度监听 reactive 对象
watch(state, (newVal) => {
console.log('状态发生了深层变化')
}, { deep: true })
// 立即执行 + 防抖
watch(searchQuery, () => {
debouncedSearch()
}, { immediate: true })
import { watchEffect } from 'vue'
// 自动追踪内部用到的所有响应式依赖
watchEffect(() => {
// 这里面用到什么 ref / reactive,就自动监听什么
document.title = `${pageTitle.value} - 木葉忍村`
})
选择原则:知道要监听什么用 watch,只关心副作用结果用 watchEffect。
这可能是 Composition API 最强大的特性。把逻辑提取成可复用的函数,无需 mixin、无需高阶组件。
useMouse// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
在任何组件中直接用:
<script setup>
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置:{{ x }}, {{ y }}</p>
</template>
useFetchimport { ref } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const res = await fetch(url)
data.value = await res.json()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
execute()
return { data, error, loading, refetch: execute }
}
💡 命名约定: 可组合函数必须以
use开头(useMouse、useFetch、useDark),这是 Vue 社区的铁律,IDE 和 ESLint 插件都依赖这个约定。
<script setup>:省去样板代码的糖<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// ✅ 不需要 defineComponent
// ✅ 不需要 return { count, double }
// ✅ 顶层绑定自动暴露给模板
const count = ref(0)
const double = computed(() => count.value * 2)
onMounted(() => {
console.log('组件已挂载')
})
// Props 和 Emits 用编译器宏
const props = defineProps<{ title: string }>()
const emit = defineEmits<{ update: [value: number] }>()
</script>
<template>
<h1>{{ title }}</h1>
<p>{{ count }} × 2 = {{ double }}</p>
<button @click="count++">+1</button>
</template>
不需要手动 import { defineProps },它是编译器宏,构建时自动被处理。
| Options API | Composition API (<script setup>) |
|---|---|
data() { return { x: 1 } } | const x = ref(1) |
computed: { d() { return ... } } | const d = computed(() => ...) |
watch: { x() { ... } } | watch(x, (val) => { ... }) |
methods: { fn() {} } | function fn() {} |
mounted() {} | onMounted(() => {}) |
props: ['title'] | const props = defineProps<{ title: string }>() |
this.$emit('e', v) | const emit = defineEmits<{ e: [v: number] }>() → emit('e', v) |
provide: { key: val } | provide('key', val) |
inject: ['key'] | const val = inject('key') |
🎯 核心心得: 别试图一次性把整个项目从 Options API 重构成 Composition API。新组件用新方式写,旧组件在需要改逻辑时顺手迁移,这才是务实之道。
Composition API 不是 Vue 3 的"新写法",而是更好的逻辑组织方式。它让代码更容易拆分和复用——你可以把关联逻辑打包成 composable,在组件间像积木一样拼装。这也是为什么 Vue 3 社区生态能在短时间内涌现出 VueUse 这样的高质量工具库。
掌握了这些基础,你已经可以写出一手干净的 Vue 3 代码了。接下来,去实战吧,忍者!
记录我使用 Nuxt 3 + Tailwind CSS + Nuxt Content 搭建个人博客的完整过程,涵盖项目初始化、文件路由、内容管理和暗色模式配置。
告别 XML Layout,拥抱 Kotlin 声明式 UI —— Compose 的核心概念、状态管理、布局系统和导航一网打尽。
掌握 SwiftUI 的 View 协议、@State/@Binding 状态体系、布局三件套和 MVVM 架构——适合有 iOS 基础想转型的开发者。