主题
生命周期与工程化开发(二)
本页对应 生命周期与工程化开发,共 6 段视频,约 1 小时 25 分。
学习目标
- 能够准确说明 Vue 实例生命周期的四个阶段及 8 个钩子函数的执行顺序,并通过代码验证
beforeCreate/created阶段 data 与 methods 的可用性、beforeMount/mounted阶段 DOM 的可用性。 - 能够在
created钩子中发送异步请求获取列表数据并保存到 data,在mounted钩子中完成自动聚焦、初始化图表等 DOM 操作,并能解释两类操作放置位置的依据。 - 能够基于接口完成小黑记账清单的列表渲染,使用
v-for渲染账单名称与价格,并按价格条件动态添加高亮样式。 - 能够完成小黑记账清单的添加与删除功能:使用表单双向绑定收集数据、阻止表单默认提交、调用添加/删除接口,并在操作成功后重新获取列表、清空表单和更新页面。
- 能够使用 ECharts 完成饼图渲染:创建具备宽高的容器、封装图表创建方法、在
mounted中初始化图表、在获取账单数据后通过setOption更新图表,并将接口数据转换为{ name, value }格式。 - 能够说明工程化开发与脚手架的基本流程,识别 Vue 项目目录及运行入口,并在根组件
App.vue中注册和使用拆分后的组件。
知识点
需求图示与目标

本节做一个“小黑记账清单”:左侧展示消费列表、支持添加和删除,底部显示消费总计;右侧用 ECharts 饼图展示各项消费占比。和之前案例最大的不同是:数据不再写死在 data 里,而是通过接口获取、添加、删除,所以要把生命周期钩子和 axios 串起来。
需求分析
功能分四块:基本渲染、添加功能、删除功能、饼图渲染。基本渲染要解决“数据从服务器来,页面怎么显示”;添加/删除要解决“操作服务器后页面怎么更新”;饼图要解决“什么时候能操作 DOM、数据变化后怎么更新图表”。
基本渲染:created 发请求,list 存数据
为什么需要:页面一打开就要看到账单,数据在服务器,必须尽早请求。
怎么想:created 时 data 和 methods 已可用,适合发请求;拿到结果存到响应式 list,再用 v-for 渲染;总计用计算属性。
怎么做:在 created 中调用 getList,请求参数按接口文档填 creator,成功后 this.list = res.data;表格行用 v-for="(item, index) in list",价格列按阈值加 red 类;computed 中 total 用 reduce 累加 Number(item.price)。
易错点:created 必须和 data 平级,不能写进 data 里;请求地址、creator 要按接口文档复制;价格参与计算前确认是数字;总计不要手写死。
转写稿里的调试过程是:刷新页面后 created 自动执行,Network 看到请求成功,list 从空数组变成接口数据,页面才渲染出来。若 list 没赋值,页面仍是空表。
添加功能:form 绑定,成功后重新获取
为什么需要:用户要新增一笔消费。
怎么想:把接口要的 creator/name/price 准备成一个 form 对象,名称和价格用 v-model 双向绑定;点击添加发请求,成功后重新调用 getList,让列表和饼图都基于服务器最新数据更新。
怎么做:form: { creator: '你的测试账号', name: '', price: '' };名称 v-model="form.name",价格 v-model.number="form.price";按钮用 type="button" 或表单 @submit.prevent 防止跳转;提交前做非空判断,再 axios.post;成功后 this.getList(),并把 this.form 重置为新对象以清空表单。
易错点:按钮默认提交会刷新页面;价格不加 .number 可能变字符串;添加成功后只往本地 list.push 不够,因为数据源在服务器;清空表单要重置整个对象或逐项清空。
转写稿中点击添加后控制台返回成功,但页面没变,原因是还没重新获取数据;把获取逻辑封装成 getList 后再调用,列表才刷新。
删除功能:拿 id 发请求,再重新渲染
为什么需要:删除错误账单。
怎么想:点击删除时拿到当前行 id,按接口要求把 id 拼到 URL 后发送删除请求,成功后同样调用 getList。
怎么做:@click="del(item.id)";方法里 axios.delete('接口地址/' + id)(具体拼接以接口文档为准),成功后 this.getList()。
易错点:URL 里不是字面写 {id},要拼真实参数;删除后不要只在本地 filter,因为服务器数据未变;注意方法里的 this 指向。
饼图渲染:mounted 初始化,数据到了 setOption
为什么需要:用图表直观看出消费占比。
怎么想:echarts.init 需要找到有宽高的 DOM,必须等页面渲染完,所以放 mounted;初始可先创建空图,等 getList 拿到数据后再 setOption 更新。
怎么做:封装 createChart,里面 this.myChart = echarts.init(document.getElementById('main')) 并设置初始配置;mounted 中调用 this.createChart();getList 成功后把 list 转成 { name: item.name, value: item.price },再 this.myChart.setOption({ series: [{ data: ... }] })。
易错点:created 里不能操作 DOM;myChart 要存到 this 上,否则其他方法拿不到;接口字段是 name/price,饼图要 name/value;setOption 只改 series.data 时注意层级不要写错。
转写稿中先看到灰色空图,点击/获取数据后才出现饼图,说明“先 init,后 setOption”的顺序是对的。
代码准备
html
<!-- CSS only -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
/>
<style>
.red {
color: red!important;
}
.search {
width: 300px;
margin: 20px 0;
}
.my-form {
display: flex;
margin: 20px 0;
}
.my-form input {
flex: 1;
margin-right: 20px;
}
.table > :not(:first-child) {
border-top: none;
}
.contain {
display: flex;
padding: 10px;
}
.list-box {
flex: 1;
padding: 0 30px;
}
.list-box a {
text-decoration: none;
}
.echarts-box {
width: 600px;
height: 400px;
padding: 30px;
margin: 0 auto;
border: 1px solid #ccc;
}
tfoot {
font-weight: bold;
}
@media screen and (max-width: 1000px) {
.contain {
flex-wrap: wrap;
}
.list-box {
width: 100%;
}
.echarts-box {
margin-top: 30px;
}
}
</style>
<div id="app">
<div class="contain">
<!-- 左侧列表 -->
<div class="list-box">
<!-- 添加资产 -->
<form class="my-form">
<input type="text" class="form-control" placeholder="消费名称" />
<input type="text" class="form-control" placeholder="消费价格" />
<button type="button" class="btn btn-primary">添加账单</button>
</form>
<table class="table table-hover">
<thead>
<tr>
<th>编号</th>
<th>消费名称</th>
<th>消费价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>帽子</td>
<td>99.00</td>
<td><a href="javascript:;">删除</a></td>
</tr>
<tr>
<td>2</td>
<td>大衣</td>
<td class="red">199.00</td>
<td><a href="javascript:;">删除</a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">消费总计: 298.00</td>
</tr>
</tfoot>
</table>
</div>
<!-- 右侧图表 -->
<div class="echarts-box" id="main"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 接口文档地址:
* https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
*
* 功能需求:
* 1. 基本渲染
* 2. 添加功能
* 3. 删除功能
* 4. 饼图渲染
*/
const app = new Vue({
el: '#app',
data: {
},
})
</script>先分清传统开发与工程化开发
为什么需要:学习 Vue 时,如果只写一个很小的页面,直接引入 Vue 核心包就能跑;但真实项目往往有很多组件、样式、图片和第三方库,不能只靠几个 html/css/js 文件硬拼。先分清两种方式,才知道后面为什么要学脚手架。
怎么想:传统方式是“基于 html / css / js 文件,直接引入核心包,开发 Vue”;工程化方式是“基于构建工具(例如 webpack)的环境中开发 Vue”。前者适合快速验证和小页面,后者适合多人协作和持续维护的项目。
怎么做:传统开发时,在页面中引入 Vue 核心包后直接写 Vue 代码;工程化开发时,把代码放进构建工具管理的项目中,由工具完成编译、打包和开发服务。

易错点:不要把“能跑一个小 demo”误认为“能直接做项目”。传统方式不是错,而是规模和协作能力有限;工程化也不是为了炫技,而是为了解决项目变大后的编译与规范问题。
工程化开发的能力与代价
为什么需要:工程化能提高编码效率。比如使用 JS 新语法、Less/Sass、Typescript 等,通过 webpack 都可以编译成浏览器识别的 ES3/ES5/CSS 等。也就是说,开发者可以写更现代、更好维护的代码,再由工具转换成浏览器能运行的结果。
怎么想:工程化的价值在“编译能力”和“统一配置”,但代价是 webpack 配置不简单,容易出现雷同的基础配置,而且缺乏统一的标准。每个项目都从零配一遍,既慢又容易配错。
怎么做:理解工程化的目标不是让配置越复杂越好,而是把常见能力沉淀成标准配置:能编译新语法、能处理样式、能启动开发服务、能打包项目。
易错点:不要以为用了工程化就不需要理解基础 HTML/CSS/JS;工具只是编译和打包,代码逻辑仍要自己写。也不要每个项目复制一份“祖传 webpack 配置”,那正是笔记里说的“雷同的基础配置”和“缺乏统一标准”。
为什么需要 Vue CLI 脚手架
为什么需要:webpack 配置不简单,雷同配置多,又缺乏统一标准,所以需要一个工具生成标准化的配置。Vue CLI 就是为解决这个问题出现的:它是 Vue 官方提供的一个全局命令工具,可以帮助我们快速创建一个开发 Vue 项目的标准化基础架子,并且集成了 webpack 配置。
怎么想:脚手架不是业务代码,而是“项目底座”。它把常见的 webpack、babel 等配置准备好,让开发者把精力放在 Vue 组件和业务逻辑上。笔记总结的好处是:开箱即用,零配置;内置 babel 等工具;标准化的 webpack 配置。
怎么做:把 Vue CLI 理解为“创建项目架子的命令工具”。先全局安装,再检查版本,再创建项目,最后启动项目。
易错点:Vue CLI 是全局命令工具,不是每个项目里随便写的某个函数;它的作用是创建标准化基础架子,不是替代 Vue 本身。不要把它和项目内依赖混为一谈。
安装 Vue CLI 并检查版本
为什么需要:只有全局安装后,终端里才有 vue 命令可用;检查版本可以确认安装是否成功,避免后面创建项目时命令不存在。
怎么想:全局安装只需安装一次即可。安装完成后,先不要急着创建项目,先用版本命令验证环境。
怎么做:在终端执行全局安装命令:
bash
yarn global add @vue/cli或者:
bash
npm i @vue/cli -g然后查看 vue/cli 版本:
bash
vue --version易错点:-g 或 global 表示全局安装,漏掉后可能在终端找不到 vue 命令。安装和检查版本要在终端里做,不是在 Vue 文件里写。若命令不可用,应先排查安装是否成功,而不是直接怀疑项目代码。
创建项目架子
为什么需要:手工搭项目要处理目录、构建配置、依赖等重复工作;用脚手架可以快速生成标准化基础架子,减少从零配置的成本。
怎么想:创建项目时,命令后面跟的是项目名。笔记明确提醒:项目名不能使用中文。创建完成后,项目里会有标准化配置,后续再按项目需要写业务。
怎么做:执行:
bash
vue create project-name其中 project-name 换成自己的英文项目名。
易错点:项目名不要用中文;不要在没有安装成功的情况下反复执行创建命令;创建的是“项目架子”,不是直接生成完整业务页面,后续仍要自己开发组件和功能。
启动项目与找对命令
为什么需要:项目创建好后需要启动开发服务,才能在浏览器中预览和调试。不同项目的启动命令可能不完全一样,所以不能死记一个命令到处套。
怎么想:笔记给出的启动方式是 yarn serve 或者 npm run serve,同时强调“命令不固定,找 package.json”。也就是说,最终要以项目里的 package.json 为准。
怎么做:先尝试:
bash
yarn serve或者:
bash
npm run serve如果命令不对,就打开项目的 package.json,查看 scripts 中实际配置的启动命令。
易错点:不要认为所有 Vue 项目都一定是同一个启动命令;不要离开项目目录启动;启动命令要看 package.json,这是笔记里明确给出的排查依据。
关于转写稿的说明
本节笔记主题是“工程化开发和脚手架”,但提供的转写稿主要讲生命周期、钩子函数以及记账清单案例,和本节主题不匹配。转写稿中出现的“构子函数”“多字函数”“盗墓操作”等,按上下文应分别理解为“钩子函数”“生命周期函数”“DOM 操作”,但这些属于生命周期章节,不能硬塞进本节当作工程化推导。为避免编造,本节只依据笔记扩写工程化与 Vue CLI 的内容。
先认清三个核心文件

脚手架生成的文件很多,初学阶段最怕“每个文件都想看懂”,结果抓不住主线。笔记里明确说:目前只需先认识三个文件——main.js、App.vue、index.html。为什么先认这三个?因为它们是项目跑起来的最小闭环:谁先被浏览器加载、谁负责创建 Vue 应用、谁作为根组件被渲染,必须先有地图。
怎么想?可以把它们类比成一次页面启动:index.html 是浏览器最先拿到的模板;main.js 是入口文件,负责把 Vue 应用接起来;App.vue 是 App 根组件,是后面所有页面内容的根。怎么做?打开项目时先定位这三个文件:看到 main.js 就知道入口在这里;看到 App.vue 就知道根组件在这里;看到 index.html 就知道模板和挂载点在这里。易错点是:不要把 App.vue 当成普通 HTML 页面直接双击运行;不要一上来改一堆配置文件;也不要把入口文件和根组件混为一谈。转写稿里常把 Vue 说成“VOE/VUB”,把 DOM 说成“盗墓/到M”,把 created 说成“可瑞的”、mounted 说成“mon的”,阅读时要以代码拼写为准。
运行流程:从模板到根组件

为什么需要理解运行流程?因为调试时要知道“代码什么时候执行、页面什么时候渲染”。如果不知道先后,就容易在页面还没渲染时去找元素,或者在数据还没准备好时发请求。怎么想?按本节图和转写稿的生命周期讲解,可以把运行流程理解为:浏览器先加载 index.html 模板,入口 main.js 执行,创建 Vue 实例/应用,引入并挂载 App.vue 根组件,组件模板被渲染到页面挂载点,页面显示;之后数据变化会触发更新,实例销毁时进入销毁阶段。
怎么做?先看文件关系:index.html 提供模板容器,main.js 作为入口把根组件挂到模板上,App.vue 提供根组件内容。再看执行顺序:创建阶段数据和方法逐步可用,但页面还没渲染;挂载阶段把数据渲染到页面;挂载完成后才能稳定进行 DOM 操作。转写稿中的调试很关键:beforeCreate 阶段还不能使用 data 和 methods;created 阶段数据和方法已经可用,适合发请求;mounted 阶段页面已经渲染,适合找元素、操作 DOM。易错点是:在 created 里操作 DOM 往往拿不到渲染后的内容;在 beforeCreate 里发依赖 data 参数的请求不合理;生命周期钩子要与 data、methods 平级,不能嵌到 data 或 methods 里面;钩子函数会自动执行,不需要手动调用。
用调试验证流程
为什么还要调试?因为只看图容易背概念,一刷新页面就不知道谁先执行。怎么想?用“刷新页面看顺序”的办法验证:页面刷新后,创建和挂载相关钩子会依次执行,页面渲染完成后 mounted 才执行;点击按钮修改数据时才进入更新阶段;调用销毁或关闭页面时才进入销毁阶段。怎么做?可以在 created 中发请求,把返回数据存到 data 中;在 mounted 中查找渲染后的元素并操作;观察数据更新时“数据先变、页面后变”的顺序。易错点是:不要以为所有钩子每次刷新都会全部执行,更新和销毁钩子要满足条件才触发;也不要把“入口文件执行”误解成“所有组件都已渲染完成”。把这三个文件和运行流程图连起来看,再结合 created、mounted 的调试现象,就能建立从项目目录到页面运行的完整主线。
组件化是什么
为什么需要: 一个页面如果把所有代码都写在一起,结构、样式、行为会混在一起,代码显得混乱,后期维护困难。组件化就是为了解决“页面越写越大、越改越乱”的问题。
怎么想: 把页面看成一棵由多个模块拼起来的树,而不是一整块代码。每个模块只负责自己的结构、样式和行为,模块之间通过清晰的关系组合成页面。
怎么做: 按页面功能或视觉区域拆分组件。一个组件拥有自己独立的结构、样式、行为;页面由多个组件组合而成。比如下面这个页面,可以把所有代码写在一个页面中,但那样会显得代码比较混乱、难以维护,所以可以按模块进行组件划分。

易错点: 不要为了“组件化”而把页面拆得过碎。拆得太细会增加沟通和组合成本;拆得太粗又起不到维护和复用的作用。判断标准是:这个模块是否有相对独立的职责。
组件化的好处
为什么需要: 页面需要长期维护,也需要反复使用相似模块。如果每次都复制粘贴,修改时要多处同步,容易漏改。
怎么想: 组件化的核心收益是“便于维护、利于复用,从而提升开发效率”。维护时只改对应组件;复用时直接组合已有组件。
怎么做: 把重复出现的区域抽成普通组件;把页面入口保留为根组件;修改某个模块时只进入对应组件处理,避免在整页代码里到处查找。
易错点: 不要把“复用”理解成所有东西都必须复用。一次性的页面结构强行抽组件,反而增加理解成本。复用应建立在真实重复或独立职责上。
组件分类:普通组件与根组件
为什么需要: 组件拆分后需要明确层级关系,否则不知道谁是页面入口、谁是被组合的模块。
怎么想: 根组件是页面最外层入口,负责组织页面;普通组件是页面中拆出来的功能模块或区域模块。先确定根组件,再往下拆普通组件。
怎么做: 先保留一个根组件承载页面整体结构,再把列表、表单、图表等区域做成普通组件。普通组件关注自身结构与行为,根组件负责组合。
易错点: 不要把大量业务逻辑都塞进根组件,否则根组件会变成新的“大页面”。普通组件也不应反向依赖根组件的内部细节,否则耦合会变重。
从转写稿看组件内部书写与调试
为什么需要: 组件化后,每个组件都可以看成一个独立配置对象,选项写错位置会导致代码不按预期执行。转写稿中反复调试生命周期钩子函数的位置,正好说明“选项要放对位置”。
怎么想: 转写稿里强调:生命周期钩子函数要和 data 平级,不要嵌套到 data 里,也不要嵌到 methods 里。这个调试经验可以迁移到组件选项书写:结构要平级、职责要清楚。
怎么做: 写组件配置时,先确认每个选项的位置;调试时通过控制台输出、刷新页面、观察执行顺序来验证。转写稿中测试 beforeCreate 时输出 count 发现数据还不可用,到 created 再输出发现数据可用、方法也能调用;测试 mounted 时对比 DOM 是否已经渲染完成。这个过程说明:位置和阶段不对,代码表现就会不同。
易错点: 不要把钩子函数或方法嵌套错位置;不要把“生命周期”当成“组件化”本身。转写稿大量内容讲的是生命周期、Ajax、ECharts 和小黑记账清单案例,并不属于本节“组件化开发”的定义;本节只借用其中“选项平级、调试验证”的经验,不能把无关内容硬说成组件化。
本节总结
组件化的好处是:便于维护、利于复用,提升开发效率。组件的分类是:普通组件、根组件。落地时先按模块拆分页面,再明确根组件与普通组件的职责,最后在组件内部把结构、样式、行为组织清楚。
根组件 App.vue 是什么
App.vue 是整个应用最上层的组件,它包裹所有普通小组件。可以把它理解成页面总壳:其他组件最终都挂在它下面,由它组织整体结构。为什么需要先认识它?因为后续写页面时,我们不是在散落的一堆文件里乱写,而是先有一个根组件作为入口,再往里组合普通组件。怎么想?先看整体,再看局部:App.vue 是“最外层”,普通组件是“里面的一小块”。怎么做?打开项目时先找到 App.vue,确认它是否承担了应用最上层包裹作用。

易错点:不要把 App.vue 当成普通小组件随意嵌套到别的组件里;它的定位是根组件,负责包裹和组织,而不是被普通组件反向包裹。
组件三部分总览
一个 Vue 组件通常由三部分构成:template 负责结构,script 负责 JS 逻辑,style 负责样式。为什么需要拆成三部分?因为结构、逻辑、样式职责不同,混在一起不利于维护。怎么想?写组件时先问自己:页面长什么样——写 template;数据和行为是什么——写 script;外观怎么呈现——写 style。怎么做?在 App.vue 中按这三块组织内容,并借助语法高亮插件看清结构。

易错点:三部分不是随便堆叠,尤其 template 有硬性要求:有且只能有一个根元素。转写稿里把 Vue 说成“VOE/VUB”、把 DOM 说成“盗墓/盗M”、把 Ajax 说成“阿杰克斯”,这里统一纠正为 Vue、DOM、Ajax;理解概念时不要被同音字带偏。
template:结构层
template 写结构,也就是页面上有哪些标签、元素如何嵌套。为什么需要它?因为 Vue 最终要把数据渲染到页面上,必须先有结构承载。怎么想?把 template 看成“页面骨架”,先搭骨架,再让 script 提供数据。怎么做?在 template 中写合法结构,并保证只有一个根元素。易错点:多个并列根元素会报错;转写稿中调试 mounted 时先找页面元素,发现“页面还没渲染好就找 DOM 会过早”,这提醒我们:template 的结构真正渲染完成需要时机,不能想当然。
script:逻辑层
script 写 JS 逻辑,包括数据、方法、生命周期钩子等。为什么需要它?因为页面不能只有静态骨架,还要有数据驱动和交互。怎么想?把 script 看成组件的大脑:数据从哪来、点击后做什么、什么时候发请求、什么时候操作 DOM,都在这里组织。怎么做?在 script 中写逻辑时注意选项的层级关系。转写稿反复调试 created 和 mounted:把钩子函数写在 data、methods 外面,与它们平级,不要嵌套进 data 或 methods 里面;否则页面刷新后不会按预期自动执行。转写稿还强调:created 阶段数据和方法已可用,适合发 Ajax 请求;mounted 阶段页面渲染完成,适合做 DOM 操作。这个调试经验放到 App.vue 的 script 中同样适用:先检查“写的位置对不对”,再检查“执行时机对不对”。易错点:把钩子写进 data 或 methods 内部;在页面还没渲染时做 DOM 操作;发请求后忘记把结果存到组件数据中。
style:样式层与 less
style 写样式。为什么需要它?因为结构有了、逻辑有了,还要控制外观。怎么想?普通 CSS 能写就用 CSS;如果团队用 less,再开启 less 支持。怎么做?第一步,在 style 标签上写 lang="less" 开启 less 功能;第二步,安装依赖:yarn add less less-loader -D 或 npm i less less-loader -D。易错点:只写 lang="less" 但没装包,编译会失败;包名和 -D 不要漏写。转写稿里“装包、开功能”的顺序也说明:先确认工具链,再写语法。
易错点与自检
自检顺序可以这样:先看 App.vue 是否是应用最上层组件;再看三部分是否齐全;再看 template 是否只有一个根元素;再看 script 中逻辑选项是否平级;最后看 style 是否需要 less,若需要是否同时完成 lang="less" 和装包。转写稿中“钩子函数会自动执行”的调试过程提醒我们:写对位置比写多代码更重要;页面刷新后没反应,先查层级和时机,不要盲目加代码。
本节总结
App 组件包含三部分:template 结构(有且只能一个根元素)、script JS 逻辑、style 样式(可支持 less,需要装包并写 lang="less")。App.vue 作为根组件,负责包裹所有普通小组件,是理解整个应用结构的起点。
课件图示








代码示例
示例一:小黑记账清单(渲染 + 添加 + 删除 + 饼图)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>小黑记账清单</title>
<!-- Bootstrap 样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
<style>
.red { color: red !important; }
.my-form { display: flex; margin: 20px 0; }
.my-form input { flex: 1; margin-right: 20px; }
.table > :not(:first-child) { border-top: none; }
.contain { display: flex; padding: 10px; }
.list-box { flex: 1; padding: 0 30px; }
.list-box a { text-decoration: none; }
.echarts-box { width: 600px; height: 400px; padding: 30px; margin: 0 auto; border: 1px solid #ccc; }
tfoot { font-weight: bold; }
@media screen and (max-width: 1000px) {
.contain { flex-wrap: wrap; }
.list-box { width: 100%; }
.echarts-box { margin-top: 30px; }
}
</style>
</head>
<body>
<div id="app">
<div class="contain">
<!-- 左侧列表 -->
<div class="list-box">
<!-- 添加资产 -->
<form class="my-form">
<input type="text" class="form-control" placeholder="消费名称" v-model.trim="form.name" />
<input type="text" class="form-control" placeholder="消费价格" v-model.number="form.price" />
<!-- type="button" 防止表单默认提交导致页面刷新跳转 -->
<button type="button" class="btn btn-primary" @click="add">添加账单</button>
</form>
<table class="table table-hover">
<thead>
<tr>
<th>编号</th>
<th>消费名称</th>
<th>消费价格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="item.id">
<td>{{ index + 1 }}</td>
<td>{{ item.name }}</td>
<td :class="{ red: item.price > 300 }">{{ item.price.toFixed(2) }}</td>
<td><a href="javascript:;" @click="del(item.id)">删除</a></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">消费总计: {{ total.toFixed(2) }}</td>
</tr>
</tfoot>
</table>
</div>
<!-- 右侧图表:必须有宽高 -->
<div class="echarts-box" id="main"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 接口地址按课件接口文档复制,这里用常量统一管理
const CREATOR = '小黑'
const BASE_URL = 'http://你的接口地址'
const app = new Vue({
el: '#app',
data: {
list: [], // 存储接口返回的账单数据
form: { creator: CREATOR, name: '', price: '' }, // 正好是添加接口要的三个参数
myChart: null // 图表实例存到 data,其他方法才能用
},
computed: {
// 消费总计 —— 由 list 自动计算
total() {
return this.list.reduce((sum, item) => sum + item.price, 0)
}
},
// 创建阶段:数据和方法已就绪,可以发请求
created() {
this.getList()
},
// 挂载阶段:页面 DOM 已渲染,可以 echarts.init
mounted() {
this.createChart()
},
methods: {
// 获取列表(封装成方法,便于多次调用)
async getList() {
const { data: res } = await axios.get(BASE_URL + '/bill', {
params: { creator: CREATOR }
})
this.list = res.data
// 数据变了,同步更新饼图
if (this.myChart) {
this.myChart.setOption({
series: [{ data: this.list.map(item => ({ name: item.name, value: item.price })) }]
})
}
},
// 添加账单
async add() {
if (!this.form.name || this.form.price === '') {
alert('消费名称和消费价格不能为空')
return
}
await axios.post(BASE_URL + '/bill', this.form)
this.getList() // 重新渲染列表
this.form = { creator: CREATOR, name: '', price: '' } // 重置表单,相当于清空
},
// 删除账单
async del(id) {
await axios.delete(BASE_URL + '/bill/' + id) // id 拼在 URL 后面
this.getList() // 重新渲染列表
},
// 创建饼图(初始化)
createChart() {
this.myChart = echarts.init(document.querySelector('#main'))
this.myChart.setOption({
title: { text: '消费清单', left: 'center' },
tooltip: { trigger: 'item' },
legend: { top: 'bottom' },
series: [
{
name: '消费清单',
type: 'pie',
radius: '60%',
data: [], // 初始无数据,就是一块灰色区域
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
})
}
}
})
</script>
</body>
</html>关键行与易错点
created()里发请求:此时data、methods都已准备好,请求参数和结果才能存进data;放beforeCreate里this.list是拿不到的。mounted()里echarts.init:只有挂载完成,页面上才真正存在#main这个有宽高的 DOM。this.list = res.data:请求回来的数据必须存进响应式数据,页面才会自动渲染,v-for才有数据可遍历。created中的getList可能先于mounted返回,所以更新饼图前加了if (this.myChart),否则setOption of null报错。v-model.number让price是数字而不是字符串,否则item.price.toFixed(2)和reduce求和都会出问题。<button type="button">:不写type会触发表单默认提交,页面跳转,账单列表就“丢了”。- 添加/删除后都调
this.getList()重新拉数据,而不是手动push/splice数组,保证与服务器一致。 - 图表实例
this.myChart必须挂到data上,否则createChart里的局部变量在getList中访问不到。
示例二:生命周期钩子函数完整演示
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>生命周期钩子函数</title>
</head>
<body>
<div id="app">
<h2>{{ title }}</h2>
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
title: '生命周期演示',
count: 100
},
/* ===== 创建阶段 ===== */
beforeCreate() {
// data 还没初始化,拿不到数据,也不能调 methods
console.log('beforeCreate —— this.count:', this.count) // undefined
},
created() {
// 数据、方法都已就绪,页面还没渲染
console.log('created —— this.count:', this.count) // 100
// 实际项目里 axios 请求写在这里:this.getList()
},
/* ===== 挂载阶段 ===== */
beforeMount() {
// 页面还没渲染,h2 里还是原始的 {{ title }}
console.log('beforeMount —— h2:', document.querySelector('h2').innerHTML)
},
mounted() {
// 页面已渲染完成,可以做 DOM 操作、echarts.init
console.log('mounted —— h2:', document.querySelector('h2').innerHTML)
},
/* ===== 更新阶段(循环往复) ===== */
beforeUpdate() {
// 数据已更新,页面还没更新
console.log('beforeUpdate —— 数据:', this.count,
' 页面:', document.querySelector('span').innerHTML)
},
updated() {
// 数据和页面都更新了
console.log('updated —— 数据:', this.count,
' 页面:', document.querySelector('span').innerHTML)
},
/* ===== 销毁阶段 ===== */
beforeDestroy() {
// 实例即将销毁,一般在这里清理定时器、解绑事件
console.log('beforeDestroy —— 清理定时器')
},
destroyed() {
console.log('destroyed —— 实例已销毁')
}
})
// 在控制台执行 vm.$destroy() 可手动触发销毁钩子
</script>
</body>
</html>关键行与易错点
- 8 个钩子函数写在
new Vue({...})里,与data、methods平级,不要嵌套进data或methods内部。 - 钩子函数由 Vue 自动执行,不需要手动调用;页面刷新后依次执行前四个(beforeCreate → created → beforeMount → mounted)。
- 更新阶段的钩子只在数据变化时触发,是循环往复的;销毁阶段的钩子需要
vm.$destroy()或关闭页面才触发。 beforeCreate中this.count是undefined,说明此时还不能用数据,所以发请求不能写在这里。beforeMount中h2.innerHTML拿到的是未解析的{{ title }},mounted中才是真实文本,这就是“DOM 操作必须在 mounted 之后”的原因。- 最需要记住的两个:
created发 ajax 请求,mounted做 DOM 操作(如 echarts 初始化)。
示例三:根组件 App.vue 三部分构成
vue
<template>
<!-- 结构:有且只能有一个根元素 -->
<div class="app">
<h2>{{ msg }}</h2>
<p class="tip">组件由 template / script / style 三部分组成</p>
</div>
</template>
<script>
// js 逻辑
export default {
name: 'App',
data() {
return {
msg: 'Hello Vue'
}
}
}
</script>
<style lang="less">
/* 样式:lang="less" 需先执行 npm i less less-loader -D 或 yarn add less less-loader -D */
.app {
h2 {
color: red;
}
.tip {
font-size: 14px;
}
}
</style>js
// main.js 入口文件:把根组件 App.vue 挂载到 index.html 的 #app 上
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')关键行与易错点
template中有且只能有一个根元素,多个并列标签会直接编译报错。script里的data必须是函数并return一个对象,组件中写成对象会报错。style上写lang="less"前必须先装包npm i less less-loader -D,否则编译报错。- 脚手架里只需关注三个文件:
main.js(入口)、App.vue(根组件)、index.html(模板文件)。 - 启动命令以
package.json中的scripts为准,通常是npm run serve或yarn serve。
常见错误
钩子函数根本不执行
- 现象:在
created/mounted里写了console.log或发请求,控制台毫无反应。 - 原因:把钩子写进了
data、methods或computed内部,成了普通属性/方法,Vue 不会识别,也不会自动调用。 - 修法:钩子必须与
data、methods平级,直接写在new Vue({...})的顶层配置对象里。
- 现象:在
在
created里操作 DOM 或初始化 echarts,报null/ 图表空白- 现象:
document.querySelector('#main')返回null;echarts.init(...)报“Initialize failed: invalid dom”。 - 原因:
created阶段数据已就绪但页面尚未渲染,模板中的元素还不存在。 - 修法:DOM 相关逻辑(找元素、
echarts.init、focus())一律移到mounted;只有发请求、拿数据可以放在created。
- 现象:
在
beforeCreate里发请求并用this.xxx作参数或接收结果- 现象:请求参数是
undefined,或this.list = res.data赋值后页面仍为空、控制台报错。 - 原因:
beforeCreate时data和methods都还没初始化,this.list不存在,赋值也不具备响应式。 - 修法:请求放到
created(此时 data/methods 可用,且早于渲染),结果赋值给data中预先声明的字段。
- 现象:请求参数是
请求成功、数据也有了,但页面不渲染
- 现象:Network 里能看到返回数据,
console.log也打印了,表格/列表依旧是空的。 - 原因:把结果赋给了局部变量或普通变量,没有写进
data;或写进了data里未声明的字段(Vue2 无法劫持新增属性)。 - 修法:先在
data中声明list: [],再this.list = res.data;需要新增响应式字段时用this.$set。
- 现象:Network 里能看到返回数据,
添加/删除成功,但列表和饼图不刷新
- 现象:接口返回
message: ok,页面数据纹丝不动,刷新浏览器后才变。 - 原因:只发了写请求,没有重新拉取列表;或拉取代码被复制成多份、漏调;饼图也没有重新
setOption。 - 修法:把“发请求取列表 + 存 data”封装成
getList(),在created、添加成功回调、删除成功回调中统一调用;饼图在getList拿到数据后再setOption更新。
- 现象:接口返回
echarts 图表容器有 DOM 但一片空白
- 现象:
#main元素存在,init不报错,但什么也不显示。 - 原因:容器没有宽高(div 默认高度为 0);或实例被声明成
mounted内的局部变量const myChart,后续方法里取不到,无法setOption。 - 修法:给容器写死
width/height(或用 CSS 类);实例挂到this.myChart上,其他方法统一用this.myChart.setOption(...)。
- 现象:
饼图数据传进去不显示扇区
- 现象:
setOption执行了,图例空白或只有一个灰圈。 - 原因:直接把接口返回的
{ name, price }数组丢给series.data,而 echarts 要求{ name, value }。 - 修法:用
map做一次结构转换:list.map(item => ({ name: item.name, value: item.price }))。
- 现象:
点击“添加账单”整页刷新、表单数据丢失
- 现象:一点按钮页面闪一下,输入框被清空,控制台请求也没发出。
- 原因:
button在form内默认type="submit",触发了原生表单提交。 - 修法:写
<button type="button">,或在事件处理中e.preventDefault()。
练习
钩子顺序验证:在同一个组件中按顺序打印 8 个生命周期钩子的名字,刷新页面记录执行顺序;再点击按钮修改
count、调用vm.$destroy(),分别记录哪些钩子被触发。用一句话说明“为什么刷新后只会执行前四个”。请求位置改造:把小黑记账清单里
created中的请求代码原样移到beforeCreate,运行并截图报错信息;再移回created。写 2 条结论,说明为什么请求最适合放在created而不是更早。添加功能补全:为添加表单补齐三项——非空校验(名称和价格都不能为空)、价格使用
.number修饰符、提交成功后重置表单对象。提交后确认新数据出现在列表中。删除 + 图表联动:完成删除功能(传 id 发请求 → 调
getList重新渲染),并确认删除后右侧饼图的扇区数量同步减少。若不同步,定位问题出在哪一步。脚手架起步:用
vue create创建一个项目,在App.vue中写一个h1标题和一段样式,把style改为lang="less"并安装依赖,运行npm run serve查看效果。记录main.js、App.vue、index.html三者各自的作用。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 生命周期及其钩子函数(6-9) | 25:21 |
| 2 | 练习-使用created和mounted的例子(10) | 10:38 |
| 3 | 小黑记账清单-渲染功能(11-13) | 09:26 |
| 4 | 小黑记账清单-添加功能(11-13) | 09:19 |
| 5 | 小黑记账清单-删除功能(11-13) | 06:59 |
| 6 | 小黑记账清单-饼图渲染(11-13) | 23:36 |
小结
- 生命周期钩子必须与
data/methods平级书写,写在配置对象内部的其他位置等于普通函数,永远不会自动执行。 - 记忆锚点只有两个:
created发请求,mounted操作 DOM;其余钩子了解顺序即可。 - 请求放
created而非beforeCreate,是因为此时data与methods已可用——既能用数据做参数,也能把结果存回响应式数据。 - 数据要驱动视图,前提是先声明、再赋值;赋值给局部变量或未声明字段,页面不会有任何变化。
- 增删改之后页面不更新,本质是“没有重新拉取数据”;把拉取逻辑封装成
getList()复用,比复制粘贴可靠得多。 - echarts 三板斧:有宽高的容器 →
mounted中init并挂到this→ 拿到数据后setOption,且数据结构必须是{ name, value }。 - 工程化解决的是“配置雷同、标准缺失”的问题,Vue CLI 提供零配置的标准化架子;上手只需关注
main.js(入口)、App.vue(根组件)、index.html(模板)。 - 组件化把页面拆成结构、样式、行为独立的单元,
App.vue是最上层的根组件,其template有且只能有一个根元素。