☰
Vue前端小例子实战:列表渲染、表单绑定与组件通信核心机制解析
2026/10/18 10:12:38 网站建设 项目流程

简介:这是一份面向Vue.js初学者与前端进阶者的实战型小例子资源,围绕一个资产管理应用展开,帮助读者在动手实践中理解Vue的核心概念与工程化配置。内容涵盖Vue实例与数据绑定、计算属性与侦听器、组件化拆分、模板语法中的条件与循环渲染,以及路由管理、状态管理、响应式布局和错误处理等常见知识点,适合用来对照学习或作为课程练习参考。资源包共39个文件,以15个js脚本、9个vue单文件组件为主,另含7张png示意图、2个json配置、1个html入口及若干工程配置文件,压缩包约45KB,体量轻巧、结构完整。目前已有4211人学习下载,读者可从中获得可直接运行的示例代码、清晰的目录组织方式与组件划分思路,便于快速上手Vue项目开发并迁移到更复杂的应用场景中。

1. 从一个真实场景说起:为什么我又回头翻出了这个 VUE 前端小例子

前阵子帮一个做后台管理系统的朋友救火,需求很简单——把一张表格里的数据按状态筛选、分页、点行展开详情。他用了某个组件库的 ProTable,配置项写了三百多行,结果筛选条件一改就整页重渲染,控制台里 watch 和 computed 打成一团。我让他把业务逻辑抽出来,用最朴素的 VUE 前端小例子重写一遍,两个文件、不到一百五十行,跑通了。这件事让我重新意识到:VUE 例子的价值不在于炫技,而在于它把响应式、组件通信、生命周期这些核心机制压缩到最小可运行单元里。这份VUE 前端小例子资源就是干这个的——它不教你从零搭脚手架,而是给你一批能直接npm run serve跑起来的最小实现,覆盖列表渲染、表单双向绑定、父子传参、条件渲染这些日常开发里绕不开的场景。适合谁?刚学完模板语法但不知道怎么写完整页面的新手,以及想快速验证某个交互逻辑、不想被工程配置拖住的老手。VUE 实现资源里,这种"小而全"的样本反而最难找。

2. 项目结构与环境搭建:从零把第一个例子跑起来

2.1 目录结构长什么样

拿到这份资源,解压后你会看到一个典型的 VUE CLI 风格目录。它没有用 Vite,也没有上 TypeScript,就是最朴素的 webpack 版本,好处是依赖少、报错信息直白,新手不会被一堆插件配置劝退。

vue-examples/ ├── public/ │ └── index.html # 挂载入口,只有一个 #app 节点 ├── src/ │ ├── main.js # 全局入口,挂载 Vue 实例 │ ├── App.vue # 根组件,负责路由或示例切换 │ ├── components/ # 通用子组件 │ │ ├── TodoItem.vue │ │ └── UserCard.vue │ └── views/ # 每个例子一个页面级组件 │ ├── ListDemo.vue │ ├── FormDemo.vue │ └── CommunicateDemo.vue ├── package.json └── vue.config.js # 可选,改端口或配代理用

这个结构的关键在于views和components的分离:views里放的是"一个完整页面",components里放的是"可复用零件"。很多新手把两者混在一起写,结果组件越写越大,最后变成一坨。我一般会强制自己:只要一个.vue文件超过 200 行,就拆出至少一个子组件。

2.2 依赖安装与启动命令

环境要求很宽松,Node.js 12 以上就能跑。进入项目根目录后执行:

# 安装依赖,国内网络建议加 registry 参数 npm install # 启动开发服务器,默认 8080 端口 npm run serve # 打包生产版本,产物在 dist 目录 npm run build

npm run serve背后是vue-cli-service serve,它会启动 webpack-dev-server 并开启热更新。改完代码保存,浏览器自动刷新,不用手动重启。如果 8080 被占用,在vue.config.js里加一段:

// vue.config.js module.exports = { devServer: { port: 8081, // 换成空闲端口 open: true // 启动后自动打开浏览器 } }

这里有个血泪经验:open: true在 CI 环境或远程服务器上会报错,因为找不到图形界面。本地开发开着方便,提交代码前记得关掉。

2.3 入口文件做了什么

main.js是整个应用的起点,它只干三件事:引入 Vue、引入根组件、挂载到 DOM。

// src/main.js import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false // 关掉生产环境提示 new Vue({ render: h => h(App) // 用 render 函数替代 template,体积更小 }).$mount('#app') // 挂载到 public/index.html 里的 #app

render: h => h(App)这种写法等价于template: '<App/>',但前者不需要运行时编译器,打包体积能小一圈。$mount('#app')把 Vue 实例和页面上那个空 div 绑定,之后所有组件都渲染在这个 div 内部。理解这三行,你就理解了 VUE 应用的启动链路——后面所有例子都是在这个根实例上长出来的。

3. 核心功能拆解:列表、表单、组件通信三个例子逐段讲

3.1 列表渲染与条件筛选

ListDemo.vue是最基础也最常用的例子:一个数组渲染成列表,支持按关键字过滤,空结果时显示提示。

<template> <div class="list-demo"> <!-- 搜索框,v-model 双向绑定 keyword --> <input v-model="keyword" placeholder="输入关键字筛选" /> <!-- 有数据时渲染列表 --> <ul v-if="filteredList.length"> <li v-for="item in filteredList" :key="item.id"> {{ item.name }} - {{ item.status }} </li> </ul> <!-- 无数据时显示空状态 --> <p v-else class="empty">没有匹配的结果</p> </div> </template> <script> export default { data() { return { keyword: '', list: [ { id: 1, name: '任务A', status: '进行中' }, { id: 2, name: '任务B', status: '已完成' }, { id: 3, name: '任务C', status: '进行中' } ] } }, computed: { // 依赖 keyword 和 list,任一变化自动重算 filteredList() { const kw = this.keyword.trim().toLowerCase() if (!kw) return this.list return this.list.filter(item => item.name.toLowerCase().includes(kw) ) } } } </script>

这段代码的核心是computed而不是methods。filteredList依赖keyword和list,VUE 会缓存计算结果,只有依赖变化时才重新执行。如果你把它写成methods里的函数,每次重渲染都会重新过滤一遍,数据量大时就是性能杀手。:key="item.id"也不能省——用 index 做 key 在列表顺序变化时会导致 DOM 复用错乱,这是新手最常见的翻车点之一。

3.2 表单双向绑定与校验

FormDemo.vue演示了v-model在输入框、下拉框、复选框上的不同表现,以及一个轻量的提交前校验。

<template> <form @submit.prevent="handleSubmit"> <!-- 文本输入 --> <label>用户名:</label> <input v-model.trim="form.username" /> <!-- 下拉选择 --> <label>角色:</label> <select v-model="form.role"> <option value="admin">管理员</option> <option value="user">普通用户</option> </select> <!-- 复选框绑定布尔值 --> <label> <input type="checkbox" v-model="form.agree" /> 同意条款 </label> <button type="submit">提交</button> <p v-if="errorMsg" class="error">{{ errorMsg }}</p> </form> </template> <script> export default { data() { return { form: { username: '', role: 'user', agree: false }, errorMsg: '' } }, methods: { handleSubmit() { // 提交前做最小校验 if (!this.form.username) { this.errorMsg = '用户名不能为空' return } if (!this.form.agree) { this.errorMsg = '请先同意条款' return } this.errorMsg = '' console.log('提交数据:', { ...this.form }) } } } </script>

v-model.trim是修饰符,自动去掉首尾空格,比在提交时手动trim()更省事。@submit.prevent阻止表单默认提交行为,否则页面会刷新,Vue 的状态全丢。注意form对象里的字段必须先在data里声明,VUE 2 无法检测对象新增属性的变化——如果你在提交时动态加一个form.extra,视图不会更新。这是响应式系统的边界,不是 bug。

3.3 父子组件通信:props 下行、emit 上行

CommunicateDemo.vue和UserCard.vue配合,演示了最经典的父子通信模式。

<!-- 父组件 CommunicateDemo.vue --> <template> <div> <UserCard v-for="user in users" :key="user.id" :user-info="user" @delete="handleDelete" /> </div> </template> <script> import UserCard from '@/components/UserCard.vue' export default { components: { UserCard }, data() { return { users: [ { id: 1, name: '张三' }, { id: 2, name: '李四' } ] } }, methods: { handleDelete(id) { // 子组件传来 id,父组件负责真正删除 this.users = this.users.filter(u => u.id !== id) } } } </script>
<!-- 子组件 UserCard.vue --> <template> <div class="user-card"> <span>{{ userInfo.name }}</span> <button @click="$emit('delete', userInfo.id)">删除</button> </div> </template> <script> export default { // props 声明类型和必填,便于排查传参错误 props: { userInfo: { type: Object, required: true } } } </script>

父组件通过:user-info="user"把数据传下去,子组件用props接收;子组件通过$emit('delete', id)把事件抛上去,父组件用@delete="handleDelete"监听。数据单向流动,谁的数据谁修改,这样出问题时排查路径非常清晰。我见过有人直接在子组件里改this.userInfo.name = 'xxx',短期能跑,但父组件和其他兄弟组件的数据源就乱了,后期加个撤销功能都无从下手。

4. 避坑与排查:这几个错误我几乎每次都能遇到

4.1 数据改了但页面没更新

现象:this.list[0] = newItem或this.obj.newKey = value之后,视图纹丝不动。原因:VUE 2 的响应式基于Object.defineProperty,无法检测数组索引赋值和对象新增属性。解决:数组用this.$set(this.list, 0, newItem)或splice;对象用this.$set(this.obj, 'newKey', value)。更彻底的做法是初始化时就把字段声明全。

4.2 组件样式互相污染

现象:A 组件里写的.title { color: red }把 B 组件的标题也染红了。原因:<style>默认是全局作用域,所有组件共享。解决:给每个组件的 style 加scoped,即<style scoped>。如果还需要改子组件内部样式,用::v-deep(VUE 2)穿透,但别滥用,穿透多了说明组件边界没设计好。

4.3 路由跳转后组件状态没重置

现象:从详情页返回列表页,搜索框里还留着上次的关键字。原因:keep-alive缓存了组件实例,或者路由复用了同一个组件。解决:在activated钩子里重置数据,或者给<router-view>加:key="$route.fullPath"强制重建。前者性能好,后者省心,看场景选。

4.4 npm install 卡住或报错

现象:npm install停在某个包上不动,或者报node-sass编译失败。原因:网络问题或 Node 版本与依赖不匹配。解决:先换 registry,再检查 Node 版本。node-sass这类包对 Node 版本极其敏感,Node 16 以上建议换成sass(dart-sass)。如果还不行,删掉node_modules和package-lock.json重来,这是最有效的后悔药。

4.5 控制台报 "Cannot read property of undefined"

现象:页面白屏,控制台指向模板里某个{{ user.name }}。原因:异步数据还没回来,user是undefined,模板已经渲染了。解决:用v-if="user"包一层,或者初始化时给user一个空对象{}。更规范的做法是用可选链{{ user?.name }},但需要模板编译器支持,老项目里还是v-if最稳。

5. 二次开发与改造:把这批例子变成你自己的项目底座

这批例子的定位是"最小可运行单元",所以二次开发的第一步不是加功能,而是定规范。我一般会先做三件事:统一组件命名(大驼峰文件名、模板里用短横线)、统一事件命名(动词开头,如handleSubmit、onDelete)、统一目录职责(views只做页面组装,components只做展示和局部交互)。这三条定下来,后面加多少页面都不会乱。

第二步是抽公共逻辑。比如列表筛选、分页、表单校验,几乎每个页面都要写一遍。可以抽成 mixin:

// src/mixins/listMixin.js export default { data() { return { keyword: '', page: 1, pageSize: 10 } }, computed: { pagedList() { const start = (this.page - 1) * this.pageSize return this.filteredList.slice(start, start + this.pageSize) } }, methods: { handlePageChange(newPage) { this.page = newPage } } }

组件里mixins: [listMixin]引入,再补一个filteredList计算属性即可。mixin 的缺点是来源不清晰,变量冲突时不好排查,所以只抽真正通用的逻辑,业务相关的别往里塞。

第三步是加一层 API 封装。例子里的数据都是写死的,真实项目要调接口。建议在src/api/下按模块建文件,统一处理请求拦截、错误提示和 loading 状态:

// src/api/request.js import axios from 'axios' const request = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 从环境变量读,别硬编码 timeout: 10000 }) request.interceptors.response.use( res => res.data, err => { console.error('请求失败:', err.message) return Promise.reject(err) } ) export default request

baseURL走环境变量,本地开发、测试、生产各一份.env文件,切换环境不用改代码。这是我在多个项目里踩过坑之后固定下来的习惯——硬编码的接口地址,上线前一定会有人忘记改。

改造完怎么验证?我的做法是先跑通一个完整闭环:列表页调接口拿数据、筛选、分页、点进详情、返回列表保持筛选状态。这一个闭环跑通,说明路由、状态、请求、组件通信都没问题,剩下的页面就是复制粘贴加微调。如果闭环里某一步卡住,问题一定出在那一步对应的模块,排查范围小很多。

从那以后我每次拿到一批示例代码,都强制自己先跑通一个最小闭环再动手改,而不是一上来就加功能。这个习惯帮我省下了大量"改到一半发现底层不通"的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询