今天正式写Vue。目标是早报站首页的第一个真实组件:文章卡片------标题、摘要、链接、点赞按钮,全站复用。
🎯 本篇学会四件事 :单文件组件(SFC)怎么写、ref响应式怎么用、props怎么传参、v-for怎么渲染列表。含组件代码约60行。
📌 太长不看版(给想快速上手的你)
| 项目信息 | 一句话说明 |
|---|---|
| 本篇目标 | 写出早报站第一个Vue组件:ArticleCard |
| 代码行数 | ~60行(含注释) |
| 依赖 | 无新增(脚手架已就位) |
| 核心功能 | 组件 + props传参 + ref响应式 + v-for列表 |
| 跑起来的命令 | npm run dev → http://localhost:5173 |
| 核心知识点 | SFC、ref、defineProps、computed、v-for |
| 做完你能得到 | 一个可复用、可交互、有隔离样式的文章卡片组件 |
一、概念复习:三句话把Vue装进脑子
在写代码前,把前端世界观翻译成Vue的三个具体机制:
| # | 机制 | 说明 |
|---|---|---|
| ① | 组件 = 自包含的积木 | 一个.vue文件 = HTML(长得怎样)+ JS(怎么反应)+ CSS(怎么打扮)住一个房间------拆组件就是在做你之前做过的事:函数拆分,只不过拆的是 "界面函数" |
| ② | 模板 = 视图的声明 | {``{ title }}就是 "这里显示title",v-for就是 "这个列表重复渲染多少次"------你声明,Vue执行 |
| ③ | ref = 会通知视图的变量 | 普通let count = 0改了视图不知道;ref(0)包装后,count.value一变,用到它的视图自动更新------这就是 "数据是账本,视图是投影"的落地 |
二、第0步:清理脚手架
在上篇的daily-front项目里:
bash
# 删掉默认的HelloWorld组件
rm src/components/HelloWorld.vue
# 清空App.vue
💡 从零开始写早报站的壳------脚手架给的默认内容都是演示,真正写项目时第一步永远是清空它们。
三、第1步:第一个组件ArticleCard
src/components/ArticleCard.vue:
vue
<script setup>
import { computed, ref } from 'vue'
// ① props:父组件传入的参数(≈ 函数的参数,只读)
const props = defineProps({
title: String,
summary: String,
url: String,
})
// ② ref:响应式状态------liked / likeCount 一变,视图自动更新
const liked = ref(false)
const likeCount = ref(0)
// ③ 事件处理:模板里的 @click 会调用它
function toggleLike() {
liked.value = !liked.value
likeCount.value += liked.value ? 1 : -1
}
// ④ computed:由已有数据 "派生"的新值,依赖变化自动重算
const preview = computed(() =>
(props.summary || '').slice(0, 40) + '...')
</script>
<template>
<article class="card">
<h2><a :href="url" target="_blank">{{ title }}</a></h2>
<p>{{ preview }}</p>
<button @click="toggleLike">
{{ liked ? '❤️ 已赞' : '🤍 点赞' }} · {{ likeCount }}
</button>
</article>
</template>
<style scoped>
.card { border: 1px solid #d8dee4; border-radius: 8px;
padding: 14px 18px; margin: 12px 0; }
.card h2 { margin: 0 0 6px; font-size: 17px; }
button { cursor: pointer; border: 1px solid #1d6f9e;
background: none; color: #1d6f9e;
padding: 4px 12px; border-radius: 999px; }
</style>
📖 逐段拆解这个60行的文件------它同时演示了Vue的全部基础
| # | 概念 | 说明 |
|---|---|---|
| ① | <script setup> |
组合式API的语法糖------这里写的变量和函数,模板里直接用(setup = "准备"),不用再写一坨return |
| ② | defineProps |
声明这个组件接收哪些参数。title: String意思是 "调用方必须按字符串传title"。props ≈ 函数参数 ,而且是只读的------组件不能改自己的参数 |
| ③ | ref(false) |
把布尔值包装成 "响应式变量"。JS里读写要.value,模板里不用------Vue自动解包 |
| ④ | computed |
基于props派生的摘要预览------数据变了它自动重算,不用手动维护 |
| ⑤ | @click |
事件绑定(v-on的简写),按钮点了调用toggleLike |
| ⑥ | <style scoped> |
scoped意味着这套样式只作用于本组件------组件的自我隔离,外面同名类名不影响你 |
四、第2步:在App.vue里使用组件
src/App.vue:
vue
<script setup>
import ArticleCard from './components/ArticleCard.vue'
// 模拟数据:第13篇联调时换成从API拉
const articles = [
{ id: 1, title: '重构教会我的事', summary: '把歪代码拆开重装,功能不变结构变好......', url: '#' },
{ id: 2, title: '第一个爬虫抓到了什么', summary: '250部电影入库之后,我看到了榜单背后的规律......', url: '#' },
{ id: 3, title: '给项目装上AI', summary: '一次HTTP调用 + 一段好Prompt......', url: '#' },
]
</script>
<template>
<h1>Python 早报</h1>
<p class="subtitle">每天5分钟,掌握Python社区动态</p>
<!-- v-for:列表渲染;:key是每个条目的身份证 -->
<ArticleCard
v-for="a in articles"
:key="a.id"
:title="a.title"
:summary="a.summary"
:url="a.url"
/>
</template>
📌 三个语法点
| # | 语法点 | 说明 |
|---|---|---|
| ① | v-for="a in articles" |
循环渲染------articles有几条,就生成几个ArticleCard |
| ② | :key="a.id" |
每个列表项的身份证,Vue用它追踪 "谁是谁"------永远用稳定的id,别用下标index(第六节③) |
| ③ | :title是v-bind:title的简写 |
把变量值绑给prop。:title="a.title"与title="a.title"完全不同 ------后者传的是字符串字面量"a.title" |
✅ 验收时刻
浏览器打开localhost:5173:

🎉 三张卡片渲染,点点赞按钮,数字+1,❤️/🤍切换------没有整页刷新,没有任何后端请求,这就是声明式渲染。
五、验收清单
bash
1. 三张文章卡片渲染成功,样式隔离生效(scoped无污染)
2. 点击点赞 → 数字+1、图标切换,无页面刷新
3. 连点两次 → 数字回到原值(toggle逻辑正确)
4. 在articles里加第4条 → 保存即出现第四张卡(热重载 + v-for生效)
5. 故意把title拼错 → 浏览器控制台出现清晰警告(练读报错)
6. 全程无报错后提交Git
bash
git add .
git commit -m "Vue3起步:ArticleCard组件 + 响应式点赞"
六、常见报错:这6个,Vue新手的标配(重点!)
① 页面空白,什么都没有
🔍 原因 :八成是组件没挂上------App.vue忘了import,或template里没写<ArticleCard />。
✅ 解法:三步自查------
- import了吗?
- 模板里用了吗?
- 组件文件路径对吗?
📌 组件不会自己出现在页面上。
② [Vue warn]: Property "articles" was accessed during render but is not defined
🔍 原因 :模板里用了articles,但script setup里没定义(或拼写不一致)。
✅ 解法 :模板引用的每个名字,都必须在script setup里存在。
📌 模板与脚本是同一份变量的两面。
③ v-for控制台警告缺key
🔍 原因 :列表渲染没写:key,或用了index当下标。
✅ 解法 :用数据的稳定id 。index当key在 "列表中间插入/删除"时会张冠李戴------后端读者可以理解为 "索引变化导致对错行"。
④ Cannot read properties of undefined (reading 'value') 或点击无反应
🔍 原因 :JS里忘了.value------liked = !liked改的是包装对象本身,不是里面的值。
✅ 解法 :script里凡是ref变量,读写一律.value;模板里不需要。
📌 记住这条分界线:JS里.value,模板里光着。
⑤ 样式写了一大堆,页面纹丝不动
🔍 原因 :<style scoped>只作用于当前组件------你写在了A组件,但类名用在B组件。
✅ 解法:
- 全局样式(字体、背景)放App.vue的非scoped style块;
- 组件样式各管各家。
⑥ 改完代码,页面上的点赞数归零了
🔍 原因 :不是bug------热重载/刷新后内存状态自然重置,ref是内存变量不是数据库。
✅ 解法:现阶段接受 "刷新即失忆";登录态和数据的持久化之后的(Pinia + localStorage)和(联调API)处理。
七、课后练习
| # | 练习 | 难度 | 提示 |
|---|---|---|---|
| 1 | 标签属性 :给ArticleCard加一个tag prop(如"AI"/"爬虫"),卡片左上角渲染一个小标签 |
⭐ | 复习props全流程 |
| 2 | 卡片跳转:把整张卡片包进链接,点击卡片跳原文 | ⭐⭐ | 卡片h2的a已就位,扩展到整卡 |
| 3 | 空摘要兜底:summary为空时显示 "(暂无摘要)" | ⭐⭐ | v-if / 或computed返回兜底文案------两种思路都试 |
| 4(选做) | 拆SiteHeader:把标题+副标题拆成独立组件,在App里用一次 | ⭐⭐ | 体会 "组件即函数"的复用感 |
📦 配套代码
完整组件已上传Git(python_daily/,node_modules不入仓):【gitee仓库地址】