生命周期与数据请求
本章你将学会 Vue3 的生命周期钩子,并在合适的时机用 fetch 从 JSON 文件加载博客数据。
什么是生命周期?
每个 Vue 组件从创建到销毁,会经历一系列阶段,这些阶段称为「生命周期」。
Vue 在每个阶段提供对应的 钩子函数,让你在特定的时间点执行代码。
比如:你想在组件加载完成后去请求数据,就需要用到 onMounted 这个钩子。
Vue3 生命周期钩子总览
| 钩子函数 | 执行时机 | 常用场景 |
|---|---|---|
| setup() | 组件初始化,最早执行 | 定义响应式数据、计算属性 |
| onBeforeMount | 组件挂载到 DOM 之前 | 较少使用 |
| onMounted | 组件挂载到 DOM 之后 | 发起网络请求、初始化第三方库、获取 DOM 引用 |
| onBeforeUpdate | 数据变化、DOM 更新之前 | 较少使用 |
| onUpdated | 数据变化、DOM 更新之后 | DOM 更新后的操作 |
| onBeforeUnmount | 组件销毁之前 | 清除定时器、取消订阅、移除事件监听 |
| onUnmounted | 组件销毁之后 | 清理工作确认 |
对于博客项目,我们最常用的是 onMounted——在页面加载后去获取文章数据。
生命周期钩子必须写在
<script setup>中,且不能放在异步函数或条件语句内部。Vue 通过调用顺序来判断它们属于哪个组件。
onMounted 的基本使用
实例
<script setup>
import { ref, onMounted } from 'vue'
const message = ref('加载中...')
// onMounted 在组件挂载到页面后执行
onMounted(() => {
// 此时 DOM 已经渲染完成,可以安全地操作 DOM 或发请求
console.log('组件已挂载,可以开始加载数据了')
message.value = '数据加载完成!'
})
</script>
<template>
<p>{{ message }}</p>
</template>
import { ref, onMounted } from 'vue'
const message = ref('加载中...')
// onMounted 在组件挂载到页面后执行
onMounted(() => {
// 此时 DOM 已经渲染完成,可以安全地操作 DOM 或发请求
console.log('组件已挂载,可以开始加载数据了')
message.value = '数据加载完成!'
})
</script>
<template>
<p>{{ message }}</p>
</template>
一个组件可以调用多次 onMounted,它们会按注册顺序依次执行。
用 fetch 加载 JSON 数据
我们准备把文章数据放到一个 JSON 文件中,然后在 onMounted 中用 fetch 加载它。
第一步:创建数据文件
实例
// 文件路径:public/posts.json
[
{
"id": 1,
"title": "Vue3 入门完全指南",
"summary": "从零开始学习 Vue3 组合式 API,涵盖 ref、reactive、computed 等核心概念。",
"content": "<h2>为什么学 Vue3?</h2><p>Vue3 是目前最流行的前端框架之一...</p>",
"category": "Vue",
"date": "2024-05-10"
},
{
"id": 2,
"title": "JavaScript 异步编程详解",
"summary": "一文搞懂 Promise、async/await、事件循环与微任务队列。",
"content": "<h2>什么是异步?</h2><p>JS 是单线程的...</p>",
"category": "JavaScript",
"date": "2024-05-08"
},
{
"id": 3,
"title": "CSS Grid 布局实战",
"summary": "用 CSS Grid 轻松实现复杂的响应式布局。",
"content": "<h2>Grid 入门</h2><p>Grid 是二维布局系统...</p>",
"category": "CSS",
"date": "2024-05-05"
}
]
[
{
"id": 1,
"title": "Vue3 入门完全指南",
"summary": "从零开始学习 Vue3 组合式 API,涵盖 ref、reactive、computed 等核心概念。",
"content": "<h2>为什么学 Vue3?</h2><p>Vue3 是目前最流行的前端框架之一...</p>",
"category": "Vue",
"date": "2024-05-10"
},
{
"id": 2,
"title": "JavaScript 异步编程详解",
"summary": "一文搞懂 Promise、async/await、事件循环与微任务队列。",
"content": "<h2>什么是异步?</h2><p>JS 是单线程的...</p>",
"category": "JavaScript",
"date": "2024-05-08"
},
{
"id": 3,
"title": "CSS Grid 布局实战",
"summary": "用 CSS Grid 轻松实现复杂的响应式布局。",
"content": "<h2>Grid 入门</h2><p>Grid 是二维布局系统...</p>",
"category": "CSS",
"date": "2024-05-05"
}
]
JSON 文件放在
public/目录下,Vite 会直接以静态文件形式提供服务。在浏览器中可以通过/posts.json直接访问。放在 public 中的文件不会被编译,原样复制到构建产物中。
第二步:在组件中 fetch 数据
实例
<!-- 文件路径:src/views/HomeView.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const articles = ref([]) // 初始空数组
const isLoading = ref(true) // 加载状态
const error = ref(null) // 错误信息
// 异步加载文章数据
async function fetchPosts() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/posts.json')
// 检查响应是否成功(404 或 500 时抛出错误)
if (!response.ok) {
throw new Error(`加载失败:HTTP ${response.status}`)
}
const data = await response.json()
articles.value = data
} catch (err) {
error.value = err.message
console.error('文章数据加载失败:', err)
} finally {
isLoading.value = false
}
}
// 组件挂载后立即加载数据
onMounted(() => {
fetchPosts()
})
</script>
<template>
<div class="home">
<h2>最新文章</h2>
<!-- 加载中状态 -->
<p v-if="isLoading" class="status-msg">加载中,请稍候...</p>
<!-- 错误状态 -->
<p v-else-if="error" class="status-msg error">
加载失败:{{ error }}
<button @click="fetchPosts">重试</button>
</p>
<!-- 空数据状态 -->
<p v-else-if="articles.length === 0" class="status-msg">
还没有文章,敬请期待。
</p>
<!-- 正常数据展示 -->
<template v-else>
<div class="article-grid">
<BlogCard
v-for="article in articles"
:key="article.id"
:id="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</template>
</div>
</template>
<style scoped>
.status-msg {
text-align: center;
padding: 60px 0;
font-size: 16px;
color: #999;
}
.error { color: #e74c3c; }
.error button {
margin-left: 10px;
padding: 4px 12px;
cursor: pointer;
}
</style>
<script setup>
import { ref, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const articles = ref([]) // 初始空数组
const isLoading = ref(true) // 加载状态
const error = ref(null) // 错误信息
// 异步加载文章数据
async function fetchPosts() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/posts.json')
// 检查响应是否成功(404 或 500 时抛出错误)
if (!response.ok) {
throw new Error(`加载失败:HTTP ${response.status}`)
}
const data = await response.json()
articles.value = data
} catch (err) {
error.value = err.message
console.error('文章数据加载失败:', err)
} finally {
isLoading.value = false
}
}
// 组件挂载后立即加载数据
onMounted(() => {
fetchPosts()
})
</script>
<template>
<div class="home">
<h2>最新文章</h2>
<!-- 加载中状态 -->
<p v-if="isLoading" class="status-msg">加载中,请稍候...</p>
<!-- 错误状态 -->
<p v-else-if="error" class="status-msg error">
加载失败:{{ error }}
<button @click="fetchPosts">重试</button>
</p>
<!-- 空数据状态 -->
<p v-else-if="articles.length === 0" class="status-msg">
还没有文章,敬请期待。
</p>
<!-- 正常数据展示 -->
<template v-else>
<div class="article-grid">
<BlogCard
v-for="article in articles"
:key="article.id"
:id="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</template>
</div>
</template>
<style scoped>
.status-msg {
text-align: center;
padding: 60px 0;
font-size: 16px;
color: #999;
}
.error { color: #e74c3c; }
.error button {
margin-left: 10px;
padding: 4px 12px;
cursor: pointer;
}
</style>
处理异步请求的三种状态
任何数据请求都应覆盖以下三种 UI 状态,否则用户体验会很差:
| 状态 | 何时出现 | UI 表现 |
|---|---|---|
| 加载中 | 请求发出后、响应返回前 | loading 动画、骨架屏 |
| 成功 | 数据正确返回 | 正常渲染内容 |
| 失败 | 网络错误、服务器异常 | 错误提示 + 重试按钮 |
你还可以额外处理第四种状态——空数据(请求成功但返回空数组),给用户明确提示而非空白页面。
动手:详情页也改用 fetch 加载
详情页需要根据 URL 中的 ID,从 JSON 中找到对应的文章。
可以把数据加载逻辑复用——在详情页也调用 fetch,然后过滤。
实例
<!-- 文件路径:src/views/PostView.vue -->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
const route = useRoute()
const id = Number(route.params.id)
const posts = ref([]) // 所有文章
const isLoading = ref(true)
const error = ref(null)
// 根据 ID 查找当前文章
const article = computed(() => {
return posts.value.find(p => p.id === id)
})
// 加载所有文章数据
async function fetchPosts() {
isLoading.value = true
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error('加载失败')
posts.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchPosts()
})
</script>
<template>
<div class="post-view">
<p v-if="isLoading">加载中...</p>
<p v-else-if="error">加载失败:{{ error }}</p>
<div v-else-if="!article">
<h2>文章不存在</h2>
<RouterLink to="/">返回首页</RouterLink>
</div>
<article v-else>
<span class="category-tag">{{ article.category }}</span>
<h1>{{ article.title }}</h1>
<time>{{ article.date }}</time>
<div class="content" v-html="article.content"></div>
<RouterLink to="/">← 返回首页</RouterLink>
</article>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
const route = useRoute()
const id = Number(route.params.id)
const posts = ref([]) // 所有文章
const isLoading = ref(true)
const error = ref(null)
// 根据 ID 查找当前文章
const article = computed(() => {
return posts.value.find(p => p.id === id)
})
// 加载所有文章数据
async function fetchPosts() {
isLoading.value = true
try {
const res = await fetch('/posts.json')
if (!res.ok) throw new Error('加载失败')
posts.value = await res.json()
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
onMounted(() => {
fetchPosts()
})
</script>
<template>
<div class="post-view">
<p v-if="isLoading">加载中...</p>
<p v-else-if="error">加载失败:{{ error }}</p>
<div v-else-if="!article">
<h2>文章不存在</h2>
<RouterLink to="/">返回首页</RouterLink>
</div>
<article v-else>
<span class="category-tag">{{ article.category }}</span>
<h1>{{ article.title }}</h1>
<time>{{ article.date }}</time>
<div class="content" v-html="article.content"></div>
<RouterLink to="/">← 返回首页</RouterLink>
</article>
</div>
</template>
本章小结
本章你掌握了 Vue3 生命周期钩子中最核心的 onMounted、用 fetch 从 public 目录加载 JSON 数据、以及如何处理请求的四种 UI 状态(加载/成功/失败/空)。
现在博客数据从外部 JSON 文件异步加载,更接近真实项目的工作方式。
