Skip to content

生命周期与工程化开发(二) ​

本页对应 生命周期与工程化开发,共 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/[email protected]/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/[email protected]/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的”,阅读时要以代码拼写为准。

运行流程:从模板到根组件 ​

Vue 项目运行流程图

为什么需要理解运行流程?因为调试时要知道“代码什么时候执行、页面什么时候渲染”。如果不知道先后,就容易在页面还没渲染时去找元素,或者在数据还没准备好时发请求。怎么想?按本节图和转写稿的生命周期讲解,可以把运行流程理解为:浏览器先加载 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 作为根组件,负责包裹所有普通小组件,是理解整个应用结构的起点。

课件图示 ​

课件第20页:组件化开发& 根组件 yarn serve main.js index.html

课件第22页:组件化开发& 根组件 ①组件化:一个页面可以拆分成一个个组件,每个组件有着自己独

课件第18页:脚手架目录文件介绍& 项目运行流程 VUE-DEMO │─node_module

课件第24页:总结 总结 (1) 组件化: 页面可拆分成一个个组件,每个组件有着独立的结构、样

课件第25页:普通组件的注册使用 组件注册的两种方式: 1. 局部注册:只能在注册的组件内使用

课件第12页:小黑记账清单 功能需求: 1. 基本渲染 2. 添加功能 3. 删除功能 4.

课件第19页:脚手架目录文件介绍& 项目运行流程 yarn serve main.js imp

课件第23页:App.vue 文件(单文件组件)的三个组成部分 1. 语法高亮插件: 2. 三

代码示例 ​

示例一:小黑记账清单(渲染 + 添加 + 删除 + 饼图) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>小黑记账清单</title>
  <!-- Bootstrap 样式 -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/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/[email protected]/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 拿到的是未解析的 &#123;&#123; title &#125;&#125;,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。

常见错误 ​

  1. 钩子函数根本不执行

    • 现象:在 created / mounted 里写了 console.log 或发请求,控制台毫无反应。
    • 原因:把钩子写进了 data、methods 或 computed 内部,成了普通属性/方法,Vue 不会识别,也不会自动调用。
    • 修法:钩子必须与 data、methods 平级,直接写在 new Vue({...}) 的顶层配置对象里。
  2. 在 created 里操作 DOM 或初始化 echarts,报 null / 图表空白

    • 现象:document.querySelector('#main') 返回 null;echarts.init(...) 报“Initialize failed: invalid dom”。
    • 原因:created 阶段数据已就绪但页面尚未渲染,模板中的元素还不存在。
    • 修法:DOM 相关逻辑(找元素、echarts.init、focus())一律移到 mounted;只有发请求、拿数据可以放在 created。
  3. 在 beforeCreate 里发请求并用 this.xxx 作参数或接收结果

    • 现象:请求参数是 undefined,或 this.list = res.data 赋值后页面仍为空、控制台报错。
    • 原因:beforeCreate 时 data 和 methods 都还没初始化,this.list 不存在,赋值也不具备响应式。
    • 修法:请求放到 created(此时 data/methods 可用,且早于渲染),结果赋值给 data 中预先声明的字段。
  4. 请求成功、数据也有了,但页面不渲染

    • 现象:Network 里能看到返回数据,console.log 也打印了,表格/列表依旧是空的。
    • 原因:把结果赋给了局部变量或普通变量,没有写进 data;或写进了 data 里未声明的字段(Vue2 无法劫持新增属性)。
    • 修法:先在 data 中声明 list: [],再 this.list = res.data;需要新增响应式字段时用 this.$set。
  5. 添加/删除成功,但列表和饼图不刷新

    • 现象:接口返回 message: ok,页面数据纹丝不动,刷新浏览器后才变。
    • 原因:只发了写请求,没有重新拉取列表;或拉取代码被复制成多份、漏调;饼图也没有重新 setOption。
    • 修法:把“发请求取列表 + 存 data”封装成 getList(),在 created、添加成功回调、删除成功回调中统一调用;饼图在 getList 拿到数据后再 setOption 更新。
  6. echarts 图表容器有 DOM 但一片空白

    • 现象:#main 元素存在,init 不报错,但什么也不显示。
    • 原因:容器没有宽高(div 默认高度为 0);或实例被声明成 mounted 内的局部变量 const myChart,后续方法里取不到,无法 setOption。
    • 修法:给容器写死 width/height(或用 CSS 类);实例挂到 this.myChart 上,其他方法统一用 this.myChart.setOption(...)。
  7. 饼图数据传进去不显示扇区

    • 现象:setOption 执行了,图例空白或只有一个灰圈。
    • 原因:直接把接口返回的 { name, price } 数组丢给 series.data,而 echarts 要求 { name, value }。
    • 修法:用 map 做一次结构转换:list.map(item => ({ name: item.name, value: item.price }))。
  8. 点击“添加账单”整页刷新、表单数据丢失

    • 现象:一点按钮页面闪一下,输入框被清空,控制台请求也没发出。
    • 原因:button 在 form 内默认 type="submit",触发了原生表单提交。
    • 修法:写 <button type="button">,或在事件处理中 e.preventDefault()。

练习 ​

  1. 钩子顺序验证:在同一个组件中按顺序打印 8 个生命周期钩子的名字,刷新页面记录执行顺序;再点击按钮修改 count、调用 vm.$destroy(),分别记录哪些钩子被触发。用一句话说明“为什么刷新后只会执行前四个”。

  2. 请求位置改造:把小黑记账清单里 created 中的请求代码原样移到 beforeCreate,运行并截图报错信息;再移回 created。写 2 条结论,说明为什么请求最适合放在 created 而不是更早。

  3. 添加功能补全:为添加表单补齐三项——非空校验(名称和价格都不能为空)、价格使用 .number 修饰符、提交成功后重置表单对象。提交后确认新数据出现在列表中。

  4. 删除 + 图表联动:完成删除功能(传 id 发请求 → 调 getList 重新渲染),并确认删除后右侧饼图的扇区数量同步减少。若不同步,定位问题出在哪一步。

  5. 脚手架起步:用 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

小结 ​

  1. 生命周期钩子必须与 data/methods 平级书写,写在配置对象内部的其他位置等于普通函数,永远不会自动执行。
  2. 记忆锚点只有两个:created 发请求,mounted 操作 DOM;其余钩子了解顺序即可。
  3. 请求放 created 而非 beforeCreate,是因为此时 data 与 methods 已可用——既能用数据做参数,也能把结果存回响应式数据。
  4. 数据要驱动视图,前提是先声明、再赋值;赋值给局部变量或未声明字段,页面不会有任何变化。
  5. 增删改之后页面不更新,本质是“没有重新拉取数据”;把拉取逻辑封装成 getList() 复用,比复制粘贴可靠得多。
  6. echarts 三板斧:有宽高的容器 → mounted 中 init 并挂到 this → 拿到数据后 setOption,且数据结构必须是 { name, value }。
  7. 工程化解决的是“配置雷同、标准缺失”的问题,Vue CLI 提供零配置的标准化架子;上手只需关注 main.js(入口)、App.vue(根组件)、index.html(模板)。
  8. 组件化把页面拆成结构、样式、行为独立的单元,App.vue 是最上层的根组件,其 template 有且只能有一个根元素。