前端 Vue vue Vue2 学习笔记 Kaede 2025-04-13 2026-01-03 Vue 2 核心技术 Vue 快速上手 Vue 是什么 Vue 是一个用于构建用户界面的渐进式框架. 构建用户界面, 就是基于数据构建用户的界面; 渐进式就是循序渐进的一个学习过程, 并不是一上来就什么都会的, 学一些用一些就可以完成很多的需求了.
Vue 有两种使用方式:
核心包开发: 局部的模块改造
核心包+插件工程化开发: 整站开发
创建 Vue 示例 如果我们需要使用一个数据, 使用 Vue 来进行渲染, 第一步就是创建 Vue 示例. 另外, 我们需要引入这个 Vue 代码才能进行使用.
构建用户界面其实就是分为四个步骤:
准备容器, 放需要渲染的东西
引入包, 从官网引入开发版本的就可以了
创建 Vue 实例: new Vue()
指定配置项, 渲染数据
el: 指定挂载点
data: 提供数据
有思路了, 我们不妨直接按照上面的步骤, 创建一个页面试一试.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > {{ msg }} </div > <script src ="./js/vue.js" > </script > <script > const app = new Vue ({ el : "#app" , data : { msg : "Hello Lc" } }) </script > </body > </html >
现在打开页面, 已经成功地将数据渲染到页面上了!
这里的双花括号中才可以拿到传入的数据, 否则不会进行渲染. 这里的双花括号其实就是下面的 [[#插值表达式]] 了.
插值表达式 本质就是一种 Vue 的模板语法, 利用表达式进行插值, 将数据渲染到页面中去.
[!note] 表达式 表达式, 就是可以被求值的代码, JS可以得到返回值的东西
只要是表达式, 就可以渲染, 写法就是 {{ 表达式 }}.
不过这里有一些注意点:
使用的数据必须在 data 中, 不能不存在
支持的是表达式, 不是语句, 所以 if, for 是不可以的
不可以在标签的属性中使用 {{}} 插值语法
代码中, 还是一样的, 先准备刚才的内容. 随后, 我们提供一些数据, 使用插值表达式进行渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <div id ="app" > {{ name1 }} {{ name2 }} <p > {{ num1 + num2 }}</p > <p > num1 = {{ num1 }}</p > <p > num2 = {{ num2 }}</p > <p > num1 > num2: {{ num1 > num2 ? "Yes" : "No" }}</p > </div > <script src ="./js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { name1 : 'lc' , name2 : 'yyt' , num1 : 10 , num2 : 20 , } }) </script >
页面正常渲染啦!
响应式特性 响应式, 其实指的就是只要数据变了, 那么视图都会进行更新, 纯自动的. 如何进行修改呢? 我们 data 中的数据都会自动的添加到实例上面, 所以直接通过 object.name 就可以获取数据; 同时也可以通过 object.name = xxx 来进行赋值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > <div id ="app" > {{ msg }} </div > <button id ="btn" > 修改msg</button > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { msg : "Hello Yyt from lc" } }) document .getElementById ('btn' ).addEventListener ('click' , () => { app.msg = "Good Job Lc!" ; }) </script > </body > </html >
点击按钮后, 发现数据直接更新了!
[!success] 优点 我们只需要专注于数据逻辑就好, 渲染的事情我们不需要再过度担心.
这就是 Vue 的核心特性之一: 响应式 . 如果需要修改数据, 直接通过 实例.属性名 就可以进行修改以及访问了.
开发者工具 我们如果需要进行 Vue 开发, 可以直接安装一个开发者插件, 更好的进行开发.
直接在微软商店搜索 Vue 安装开发者工具即可, 随后配置一下允许访问 Url :
开发者工具就算安装完毕. 打开刚才的 Vue 页面, 在开发者工具中就能看到 Vue 的细节了:
现在如果需要修改数据, 直接在下面进行修改即可, 这样就方便很多了.
Vue 指令 什么是指令 简单说, Vue 会根据不同的指令, 针对标签实现不同的功能. 指令都是带有 v- 前缀的特殊标签属性 . 例如下面这样子, v-html 就是一个指令:
1 2 <div v-html ="str" > </div >
对应的, 普通的 class 标签属性是不属于指令的.
v-html
[!note] 作用 动态的将 HTML 渲染到对应标签的 innerHTML 中
就直接拿上面的东西举例. 我们来到代码中, 使用一下这个指令标签. 假如我现在有一些 HTML 标签的字符串, 我希望能够渲染到页面上, 我们可能会这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" > {{ msg }} </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { msg : ` <h1>Hello World</h1> ` } }) </script >
然而效果却是这样的:
这个时候, 我们就可以使用 v-html 指令了.
1 2 3 4 <div id ="app" > <div v-html ="msg" > </div > </div >
现在该 HTML 标签已经成功地渲染到页面上了.
v-show 和 v-if
[!note] 作用 v-show 与 v-if 都是控制元素显示隐藏的指令, 但是 v-if 存在条件渲染.
v-show 的语法就是跟上一个 boolean 值, 真则显示, 假则隐藏; v-if 的语法差不多, 几乎是一摸一样的. 不妨还是在代码中试一试.
1 2 3 4 5 6 7 8 9 10 11 12 13 <div id ="app" > <div v-show ="flag" > 我是 v-show 控制的</div > <div v-if ="flag" > 我是 v-if 控制的</div > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { flag : true } }) </script >
显示起来其实没有区别, 但是这两个不可能一模一样. 查看一下结构层面的区别.
显示的时候, 结构层级如下:
但如果改成隐藏, 结构层级则会变成如下:
由此可见, 两个东西的本质不一样. v-show 本质上控制的是 css 代码, v-if 则是在控制元素的创建以及移除. 因此, 我们一般把第一个叫做简单的显示隐藏, 第二个叫做条件渲染.
由此特性, 我们可以得到如下使用场景:
v-show 可以适用于频繁切换显示隐藏的场景, 防止节点不断创建和销毁, 导致卡顿
v-if 可以适用于不频繁切换显示隐藏的场景, 比如询问是否登陆这种
v-else v-else-if
[!note] 作用 辅助 v-if 进行条件判断
就和 JS 中的 else, else if 一样, 比如判断成绩, 判断年龄之类的地方都可以用到. 语法上, v-else 后面什么都不需要跟, 直接写就好; 但是 v-else-if 后面需要跟上条件, 和 v-if 一样.
唯一需要注意的是: 使用的时候需要紧挨着 v-if . 使用示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <div id ="app" > <div > 性别为: <div v-if ="gender === 1" > 男♂</div > <div v-else > 女♀</div > </div > <div > 成绩等级为: <div v-if ="score >= 90" > A</div > <div v-else-if ="score >= 80" > B</div > <div v-else-if ="score >= 70" > C</div > <div v-else > D</div > </div > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { gender : 1 , score : 80 } }) </script >
页面正常进行显示.
v-on
[!note] 作用 注册事件
一般来说, 我们注册一个事件, 通常包含两个步骤:
添加事件的监听
提供逻辑处理的部分
所以 v-on 的语法也是差不多的, 基本语法为:
1 v-on :事件名称="内联语句 or methods中的函数名"
内联语句基本使用 内联语句, 就是可执行代码, 比如一个按钮点击后, 执行一段 JS 代码. 这里直接实现一个简单的计数器出来. (使用的是点击事件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <h1 > Number is {{ counter }}</h1 > <button v-on:click ="counter--" > -</button > <button v-on:click ="counter++" > +</button > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { counter : 0 } }) </script >
通过内联语句就可以实现效果了.
别的事件都是完全可以 的!
简写形式 因为事件其实是写的非常多的, 如果每次都写会非常的麻烦. 所以 Vue 提供了一种语法糖:
1 2 3 v-on :click="xxx" @click="xxx"
我们通常来说都会使用 @事件名 的方式进行事件的注册.
methods 中的函数名 内联语句虽然简单, 但是逻辑一旦复杂, 就变得难以维护. 所以我们需要使用函数; 函数需要定义在 methods 中! methods 其实是和 data 配置项的并列配置项, 专门用来提供方法.
弄一个更简单的例子: 切换文本的显示隐藏, 可以先实现点击按钮的绑定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <div id ="app" > <button @click ="toggleIsShow" > 切换显示隐藏</button > <h1 v-show ="isShow" > Yyt And Lc</h1 > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { isShow : false }, methods : { toggleIsShow ( ) { console .log ("点击了" ) } } }) </script >
现在我们需要实现切换的效果, 但是这里需要注意: 模板语法中可以直接使用 isShow, 但是 JS 中不行! 我们要访问的是上面的数据, 数据挂载到了 app 上面, 所以可以从 app 来获取!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script > const app = new Vue ({ el : '#app' , data : { isShow : false }, methods : { toggleIsShow ( ) { app.isShow = !app.isShow ; } } }) </script >
现在效果就成功实现了!
优化 methods 考虑到我们的 app 是定义出来的, 如果我们换了一个变量名, 比如 app2, 那么所有的东西都需要更改, 非常的麻烦. Vue 中, methods 中的方法的 this, 都会直接指向自己的 app .
因此, 刚才的 app 可以直接改成 this, 完全不会出现问题!
1 2 3 4 toggleIsShow ( ) { this .isShow = !this .isShow ; }
调用传参 在 Vue 中, 我们也是可以通过传参来调用同一个函数的. 使用的时候直接传递参数就好, 不会出现直接调用一次函数的情况!
[!tip] 和 React 对比 React 中如果直接这么写, 函数会被直接调用, 但是 Vue 中改善了这一点, 所以 Vue 中就不需要函数柯里化了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <div id ="app" > <h2 > Counter is {{ counter }}</h2 > <button @click ="changeCounter(10)" > +10</button > <button @click ="changeCounter(-20)" > -20</button > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { counter : 0 }, methods : { changeCounter (deltaCounter ) { this .counter += deltaCounter; } } }) </script >
v-bind
[!note] 作用 动态的设置 html 的标签属性, 比如 src, url, title …
语法和 v-on 差不多, 这也解决了一开始的插值表达式不能写在标签属性中的问题.
基础用法 表达式的结果会作为属性名, 交给标签. 使用案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div id ="app" > <div v-bind:tag ="tag" > Hello See My Tags</div > <button @click ="tag += 'Cat'" > Add Tags</button > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { tag : "YytCat" }, methods : {} }) </script >
当然, 如果需要使用, 那么下面也是需要定义的, 否则就无法使用, 报错啦.
简写形式 这个和 v-on 差不多, 使用的频率还是非常高的, 所以 Vue 官方也提供了简写的方式:
1 2 3 v-bind :tag="xxx" :tag="xxx"
v-for
[!note] 作用 基于数据进行循环, 多次渲染元素
可以遍历很多数据, 比如数组以及对象. 语法如下:
1 v-for = "(item, index) in 数组"
其实类似 python 的 for 循环, 或者 forEach. 其中的 item 就是数组的元素, index 就是数组的下标, index 可以省略. 直接使用案例会更加直观:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <div id ="app" > <ul > <li v-for ="(item, index) in fruits" > {{ index }} -> {{ item }}</li > </ul > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { fruits : ['苹果' , '梨子' , '桃子' , '西瓜' , '菠萝' ] }, }) </script >
[!check] 注意一下 我们的 li 出现了多次, 所以 v-for 是写在 li 上面的!
v-bind:key 如果我们进行一个列表的循环渲染, 那么每个列表项其实都是单独的东西. Key 就是 Vue 用来区分列表项的一个东西, 为了代码完整性以及避免莫名其表的 Bug, 必须 写上.
[!info] 原因 v-for 的默认行为就是尝试原地修改元素, 如果我原本第一个存在一个样式, 那么样式将会轮换给接下来的对应元素.
每个 key 都是唯一的, 最简单的就是直接使用某个固定的, 不会改变的字段作为 Key; 不推荐使用 index 作为 key.
1 2 3 <li v-for ="(item) in arr" :key ="item.id" > ... </li >
小案例 我有一个列表, 我希望能够通过按钮删除其中的数据. 具体细节见代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <div id ="app" > <ul > <li v-for ="(item) in students" :key ="item.id" > 姓名: {{ item.name }}, 年龄: {{ item.age }} <button @click ="deleteStudent(item.id)" > 删除</button > </li > </ul > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { students : [ {id : "01" , name : "张三" , age : 18 }, {id : "02" , name : "李四" , age : 19 }, {id : "03" , name : "王五" , age : 20 }, ] }, methods : { deleteStudent (id ) { let newStudents = JSON .parse (JSON .stringify (this .students )); newStudents = newStudents.filter ((item ) => { return item.id !== id; }) this .students = JSON .parse (JSON .stringify (newStudents)); } } }) </script >
很简单的, 我们的效果就实现出来了!
v-model
[!note] 作用 给表单元素使用, 实现数据的双向绑定, 快速的获取和设置表单内容
双向数据绑定, 就是数据和视图进行绑定, 修改数据视图也变化, 修改视图数据也变化!
语法也非常简单:
直接做一个模拟登陆的界面进行演示.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id ="app" > 账户: <input type ="text" v-model ="username" /> <br /> 密码: <input type ="text" v-model ="password" /> <br /> <button > 登陆</button > <button > 重置</button > </div > <script src ="../js/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { username : "" , password : "" }, methods : {} }) </script >
写到这里, 数据已经实现双向绑定了. 可以在开发者工具中试试看:
登陆的时候可以通过 methods 中的函数来获取数据; 重置只需要重置变量的值就可以实现效果了. 这里用到了 双向绑定可以快速的获取已经设置表单的内容 的特性.
这就是 v-model 的双向数据绑定.
指令补充 指令的修饰符 通过 . 指明一些指令的后缀, 不同的指令封装了不同的处理操作, 进而实现简化代码的效果. 例如下面这些, 语法是不变的:
按键修饰符 @keyup.enter 监听回车, 如果没有后面的 ., 则需要我们手动的进行判断按键是什么, 逻辑较为复杂
v-model
v-model.trim 去除首尾空格
v-model.number 转换为数字
事件修饰符
@事件.stop 阻止事件冒泡
@事件.prevent 阻止默认行为
例如我下面有一个输入框, 输入内容后, 回车将内容添加到下面的列表中进行渲染. 就可以使用上面介绍的第一个按键修饰符.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <div id ="app" > <input @keyup.enter ="addTask" placeholder ="按下回车添加任务" v-model ="inputText" /> <ul > <li v-for ="(item, index) in notes" > {{ item }}</li > </ul > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { inputText : '' , notes : [ "唱" , "跳" , "Rap" , "篮球" ] }, methods : { addTask ( ) { if (this .inputText !== "" ){ this .notes .push (this .inputText ); this .inputText = "" ; } } } }) </script >
不需要判断按下的东西就可以监听 enter 了
冒泡其实就是, 如果一个大的可点击内容包裹了一个小的可点击内容, 点击小的就会触发大的事件. 我们只需要在小的点击部分添加一个 .prevent, 就可以阻止这种情况的发生了.
另外, 也可以阻止默认的 a 标签的链接跳转.
v-bind 操作样式 基本语法 为了方便进行开发, Vue 增强了 v-bind 的语法, 可以针对 class 类名和 style 行内样式进行控制. 语法如下:
1 2 3 4 5 :class ="{类名1: 布尔}" :class ="[类名1, 类名2, 类名3 ...]"
实际代码中, 可以实现更改一个东西样式的效果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <div id ="app" > <div :class ="styleObject" > {{ name }} <button @click ="()=>{this.styleObject.text1 = !this.styleObject.text1}" > 切换状态1</button > <button @click ="()=>{this.styleObject.text2 = !this.styleObject.text2}" > 切换状态2</button > </div > <hr /> <div :class ="['text1', 'text2']" > yyt and lc </div > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { name : "Kaede" , styleObject : { text1 : true , text2 : true } } }) </script >
京东导航高亮切换 借用刚才学习的内容, 尝试实现一下京东的的导航切换效果. 其实就是点谁谁亮, 别的不亮. 这里使用动态渲染一个导航出来.
完整代码如下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > <style > * { margin : 0 ; padding : 0 ; } ul { display : flex; border-bottom : 2px solid #e01222 ; padding : 0 10px ; } li { width : 100px ; height : 50px ; line-height : 50px ; list-style : none; text-align : center; } li a { display : block; text-decoration : none; font-weight : bold; color : #333333 ; } li a .active { background-color : #e01222 ; color : #fff ; } </style > </head > <body > <div id ="app" > <ul > <li v-for ="(item, index) in list" @click ="()=>{activeIndex=index}" > <a :class ="{active: activeIndex === index}" href ="#" > {{ item.name }}</a > </li > </ul > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { activeIndex : 0 , list : [ {id : 1 , name : '京东秒杀' }, {id : 2 , name : '每日特价' }, {id : 3 , name : '品类秒杀' } ] } }) </script > </body >
实现效果如下:
绑定行内样式 其实写法差不多, 只不过这里的属性名和值直接写在标签当中了而已. 这个情况我们一般只会用于某一些小的, 特定的样式部分. 例如下面这样子设置一个 div 的样式.
1 2 3 4 <div id ="app" > <div :style ="{width: '100px', height: '200px', backgroundColor: 'blue'}" > </div > </div >
v-model 应用于其他表单元素 表单并不是只有输入框, 还有很多的各种各样的表单元素. 比如下拉菜单, 单选框, 复选框, 以及文本域等等. 好就好在, v-model 会自动的根据需求, 更新对应的值.
直接看下面的代码就好, 不多介绍了, 不如直接实战.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <div id ="app" > <h3 > 小黑学习网</h3 > 姓名: <input type ="text" v-model ="username" > <br > <br > 是否单身: <input type ="checkbox" v-model ="isSingle" > <br > <br > 性别: <input v-model ="gender" name ="gender" type ="radio" value ="1" > 男 <input v-model ="gender" name ="gender" type ="radio" value ="2" > 女 <br > <br > 所在城市: <select v-model ="city" > <option value ="1" > 北京</option > <option value ="2" > 上海</option > <option value ="3" > 成都</option > <option value ="4" > 南京</option > </select > <br > <br > 自我描述: <textarea v-model ="desc" > </textarea > <button > 立即注册</button > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { username : "" , isSingle : false , gender : "1" , city : '2' , desc : "" } }) </script >
在 Vue 开发工具中可以看到对应的内容实现了双向绑定!
[!important] 结论 其实我们不需要特别的去记忆 v-model 绑定的是什么东西, 其实都是符合直觉的.
computed 计算属性 什么是计算属性 计算属性, 就是基于现有数据, 计算出来的新的属性. 依赖的数据一旦变化, 自动重新计算.
计算属性语法 计算属性声明在 computed 配置项 中, 一个计算属性对应一个函数. 使用的时候和普通的属性使用是一样的, 直接 {{属性名}} 即可.
例如下面这个示例, 我希望能够显示当前物品的总个数, 就可以使用计算属性.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <div id ="app" > <ul > <li v-for ="(item) in list" > {{ item.name }}有 {{ item.num }} 个</li > </ul > 总共有 {{ totalCount }} 个物品 </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { list : [ {id : 1 , name : '篮球' , num : 3 }, {id : 2 , name : '音乐' , num : 5 }, ] }, computed : { totalCount ( ) { let res = 0 ; this .list .forEach ((item ) => { res += item.num ; }) return res; } } }) </script >
数据成功进行渲染了!
computed 和 methods 对比 computed 更加侧重于属性的值, 比如对某一个特征的一些数据进行统计; 而方法更加侧重于逻辑运行, 比如点击一个按钮后执行哪些操作, 跳转页面, etc.
另外, 两者的语法不一样:
computed 属性:
写在 computed 配置项中
作为属性直接使用: this.计算属性, {{计算属性}}
methods 方法:
写在 methods 配置项中
作为方法需要调用: this.方法名(), {{方法名()}}, @事件="方法名"
[!abstract] 缓存特性 计算属性会对结果进行缓存, 如果需要重复读取, 就会读取缓存, 进而提升效率; 如果依赖的属性变化了, 才会进行重新计算, 并且再次进行缓存 .
计算属性的完整写法 作为属性, 我们应该是可以获取也可以进行设置的. 无论如何, 我们应该知道: 默认的计算属性只能访问, 不能修改 . 因为它毕竟是一个方法返回的内容.
但是我们还是有办法的, 对象属性写成一个对象, 并且有两个方法. 完整的写法如下:
1 2 3 4 5 6 7 8 9 10 11 computed : { 计算属性名: { get ( ){ 获取属性的代码逻辑; return 结果; }, set (修改的值 ){ 修改属性的代码逻辑; } } }
我接下来有一个需求, 计算两个数字的和, 并输出.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <div id ="app" > <h3 > num1 is {{ num1 }}</h3 > <h3 > num2 is {{ num2 }}</h3 > <h3 > num1 + num2 is {{ total }}</h3 > </div > <script src ="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js" > </script > <script > const app = new Vue ({ el : '#app' , data : { num1 : 10 , num2 : 20 }, computed : { total ( ) { return this .num1 + this .num2 ; } }, methods : {} }) </script >
这样的代码非常简单; 但是我现在希望能够输入和, 并且平均分给两个数字! 这里我就需要使用到计算属性的完整写法了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <div id ="app" > <h3 > num1 is {{ num1 }}</h3 > <h3 > num2 is {{ num2 }}</h3 > <h3 > num1 + num2 is {{ total }}</h3 > <input type ="number" v-model.number ="new_sum" /> <button @click ="()=>{total = new_sum}" > 更改</button > </div > <script > const app = new Vue ({ el : '#app' , data : { num1 : 10 , num2 : 20 , new_sum : 0 }, computed : { total : { get ( ) { return this .num1 + this .num2 ; }, set (newSum ) { this .num1 = newSum / 2 ; this .num2 = newSum - this .num1 ; } } }, methods : {} }) </script >
至此, 计算属性的修改已经正常实现!
watch 侦听器 侦听器介绍 watch 就是一个用来监视数据变化的东西. 只要数据发生了变化, 就会执行一些业务逻辑, 或者异步操作. 基本语法很简单, 对于简单的数据类型可以直接进行监视; 对于复杂数据类型则需要做一些额外的配置.
简写语法 我们先对简单数据类型进行监视. 基本的代码语法如下:
1 2 3 4 5 6 7 8 9 10 data : { words : "lc" }, waich : { words (newValue, oldValue ){ } }
业务实现 我们可以写一个模拟翻译的效果出来, 这里先提供代码框架:
我们开始代码的编写部分. 下面的代码有如下注意点:
翻译后的结果需要使用一个变量来储存
需要做防抖处理
async 函数以及 await 方法的使用需要注意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <script > const app = new Vue ({ el : '#app' , data : { words : '' , result : "" , timer : null }, watch : { words (newValue, oldValue ) { clearTimeout (this .timer ); this .timer = setTimeout (async () => { const res = await axios ({ url : 'https://applet-base-api-t.itheima.net/api/translate' , params : { words : newValue } }) this .result = res.data .data ; }, 1000 ) } } }) </script >
现在, 一个模拟翻译的效果就实现出来了!
完整写法 还是上面的小案例, 我们其实并没有实现功能. 我希望在语言的部分能够选择语言后就重新进行翻译. 一般来说, 我们会把属性的内容以及需要翻译的内容放在一个对象中, 方便进行传输数据.
我们如果需要监视一个对象中的所有属性, 则可以使用点的方法, 但是一个对象如果有太多的属性, 这种方法就显然不合理了. 于是我们需要用到 watch 侦听器的完整写法了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 data : { obj : { words : "" , lang : "" } }, watch : { obj : { deep : true , handler (newValue) { } } },
更改刚才写的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <div id ="app" > <div class ="query" > <span > 翻译成的语言:</span > <select v-model ="transObj.lang" > <option value ="italy" > 意大利</option > <option value ="english" > 英语</option > <option value ="german" > 德语</option > </select > </div > </div > <script > const app = new Vue ({ el : '#app' , data : { transObj : { words : '' , lang : 'english' }, result : "" , timer : null }, watch : { transObj : { deep : true , handler (newValue ) { clearTimeout (this .timer ); this .timer = setTimeout (async () => { const res = await axios ({ url : 'https://applet-base-api-t.itheima.net/api/translate' , params : { words : newValue } }) this .result = res.data .data ; }, 300 ) } }, } }) </script >
现在, 就算修改选择框, 也会自动的进行更新了!
生命周期 生命周期及其四个阶段 简单说, 就是一个 Vue 实例, 从创建到销毁的过程. 数据大致有下面这些过程:
创建阶段. 数据被创建出来了!
挂载阶段. 我们的数据通过模板语法渲染在页面上了.
更新阶段. 我们想要修改数据, 修改视图, 都是这个部分. 这个阶段会进入循环, 不断更新
销毁阶段. 浏览器关闭了, 实例销毁.
我们可以同时进行思考, 我们需要什么时候发送请求? 又应该什么时候操作 DOM? 发送请求自然是创建阶段的最后; 操作 DOM 也自然是页面挂在完毕后进行操作.
生命周期函数 (钩子函数) 初识生命周期钩子 Vue 已经提供了一些函数, 会在四个阶段进行调用. 四个阶段对应八个函数, 每个阶段都会调用其中两个函数. 有了这些钩子函数, 我们就可以在需要的时候调用需要的代码了.
八个生命周期函数如下, 其实就是四对钩子函数:
before Create
created ==可以用来发送初始化的数据请求, 进而渲染数据==
before Mount
mounted ==可以用来根据数据渲染 DOM==
before Update
updated
before Destroy ==可以释放一下 Vue 以外的资源, 比如定时器==
destroyed
观察钩子调用时机 钩子函数是直接写在 data 并列的配置项中的. 下面直接在代码中调用对应的钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <div id ="app" > <h3 id ="test" > {{ counter }}</h3 > <button @click ="counter--" > -</button > <button @click ="counter++" > +</button > </div > <script > const app = new Vue ({ el : '#app' , data : { counter : 1 }, beforeCreate ( ) { console .log ("beforeCreate 数据准备好之前" ); console .log ("数据准备之前" , this .counter ); }, created ( ) { console .log ("created 数据准备好之后" ); console .log ("数据准备之后" , this .counter ); }, beforeMount ( ) { console .log ("beforeMount 模板渲染之前" ); console .log ('挂载前' , document .getElementById ('test' )); }, mounted ( ) { console .log ("mounted 模板渲染之后" ); console .log ('挂载后' , document .getElementById ('test' )); }, beforeUpdate ( ) { console .log ('beforeUpdate' ) }, updated ( ) { console .log ('updated' ) }, beforeDestroy ( ) { console .log ('beforeDestroy' ) }, destroyed ( ) { console .log ('destroyed' ) } }) </script >
直接运行代码, 可以看到控制台如下输出:
效果
解释
由此, 我们就已经知道了: 数据在准备之前是不能正常访问的, 准备后才可以; 页面上的模板语法一开始就是 {{}}, 只有组件挂载完成才会变成真的内容. 另外, 由于下面的钩子一个是更新视图, 所以只有在点击按钮, 更新数据进而更新视图的时候才会调用; 最后的销毁只有在关闭页面的时候才会调用.
对于更新数据, 以及销毁数据, 都可以直接在控制台中进行演示:
[!warning] 为什么卸载了内容还在? 因为卸载的是逻辑, 其实所有 Vue 相关的东西都点不聊了.
created 应用 一般来说, 这个时候的响应式数据已经就位. 我们可以在这个时候发送初始化渲染请求, 也就是请求一些数据回来.
假如我想要实现一个新闻列表的案例. 这里先准备如下静态代码:
这里我们先实现在初始化的时候请求到新闻数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script > const app = new Vue ({ el : '#app' , data : { list : [] }, async created ( ) { const res = await axios.get ('http://hmajax.itheima.net/api/news' ); console .log (res) } }) </script >
请求后得到数据如下:
随后, 我们需要将数据更新给 data 中的 list 数据. 这里直接设置数据; 随后使用 v-for 进行数据的渲染就好. 写完后, 得到代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <div id ="app" > <ul > <li v-for ="(item) in list" :key ="item.id" class ="news" > <div class ="left" > <div class ="title" > {{ item.title }}</div > <div class ="info" > <span > {{ item.source }}</span > <span > {{ item.time }}</span > </div > </div > <div class ="right" > <img :src ="item.img" alt ="" > </div > </li > </ul > </div > <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 app = new Vue ({ el : '#app' , data : { list : [] }, async created ( ) { const res = await axios.get ('http://hmajax.itheima.net/api/news' ); this .list = res.data .data ; } }) </script >
页面中已经可以渲染出来一个新闻列表了!
mounted 应用 我有一个需求, 页面中有一个输入框, 进入页面后, 我希望立马获取焦点. 显然, 我们的模板渲染完毕才可以操作 DOM. 同理, 这里使用一个准备好的素材文件进行编写.
代码中, 我们首先需要写一下生命函数钩子, 调用一下就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 <script > const app = new Vue ({ el : '#app' , data : { words : '' }, mounted ( ) { document .querySelector ("#inp" ).focus (); } }) </script >
案例: 记账清单 功能需求:
基本渲染
已进入页面就立马发送请求请求数据
将数据存入 data 中
结合数据进行渲染
消费统计, 使用计算属性实现
添加功能
删除功能
饼图渲染
提前准备的静态代码以及几个 JS 文件如下:
数据基本渲染 按照需求一步一步走就好. 首先我们通过阅读 API 文档, 使用 axios 从 API 获取数据.
[!tip] 关于API API传入一个 creator 的 params 参数, 你可以随意更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script > const app = new Vue ({ el : '#app' , data : {}, async created ( ) { const res = await axios.get ('https://applet-base-api-t.itheima.net/bill' , { params : { creator : 'lc' } }) console .log (res); } }) </script >
通过请求, 获取到了如下数据:
保存一下就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <script > const app = new Vue ({ el : '#app' , data : { list : [] }, async created ( ) { ... this .list = res.data .data ; } }) </script >
随后, 我们修改渲染数据部分的逻辑:
1 2 3 4 5 6 7 8 <tr v-for ="(item, index) in list" :key ="item.id" > <td > {{ index + 1 }}</td > <td > {{ item.name }}</td > <td :class ="{red: item.price > 500}" > {{ item.price.toFixed(2) }}</td > <td > <a href ="javascript:;" > 删除</a > </td > </tr >
下面的消费总计, 直接创建一个新的计算属性即可.
1 2 3 4 5 computed : { totalPrice ( ) { return this .list .reduce ((sum, item ) => sum + item.price , 0 ) } }
上面直接使用就好.
1 <td colspan ="4" > 消费总计:{{ totalPrice }}</td >
至此, 基本的渲染已经实现.
添加功能 简单说, 就是点击添加后, 往下面这个列表新增数据. 这里也是需要发送请求进行添加的! 发送请求, 需要收集表单数据, 就是消费名称以及上面的消费价格.
数据绑定, 使用 v-model 就好. 随后给按钮添加点击事件, 发送添加账单的请求, 这里还是同样根据 API 文档走.
1 2 3 4 5 6 7 8 9 10 11 12 13 methods : { async add ( ) { const res = await axios.post ('https://applet-base-api-t.itheima.net/bill' , { creator : 'lc' , name : this .name , price : this .price }) } },
这里发现, 我们需要更新本地的数据, 而上面的 created 中的逻辑就是我们想要的逻辑. 我们直接将上面的代码进行封装, 方便我们进行二次调用即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 created ( ) { this .getList (); }, methods : { async getList ( ) { const res = await axios.get ('https://applet-base-api-t.itheima.net/bill' , { params : { creator : 'lc' } }) this .list = res.data .data ; }, async add ( ) { const res = await axios.post ('https://applet-base-api-t.itheima.net/bill' , { creator : 'lc' , name : this .name , price : this .price }) this .getList (); } },
另外, 我们可以做一下边界判断, 输入的名字是否存在, 存在了再进行请求的提交. 完整的 add 方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 async add ( ) { if (!this .name ) { alert ("请输入消费名称" ); return ; } if (typeof this .price !== 'number' ) { alert ("请输入正确的价格" ); return ; } const res = await axios.post ('https://applet-base-api-t.itheima.net/bill' , { creator : 'lc' , name : this .name , price : this .price }) this .getList (); this .name = "" ; this .price = "" ; }
现在的基本效果已经实现了!
删除功能 点击删除, 就调用请求 API, 删除对应的内容; 这里删除内容其实改的还是后端, 所以我们还是需要重新发送请求并且重新进行渲染.
[!danger] 注意 这里必须使用 async 和 await, 否则第一次点击删除是没反应的
1 2 3 4 5 6 7 8 async del (id ) { await axios.delete (`https://applet-base-api-t.itheima.net/bill/${id} ` ); this .getList (); }
删除效果已经实现了!
饼图渲染 这里的饼图使用 echarts 进行渲染. 首先需要初始化饼图, 随后根据数据进行更新即可. 不过这里需要注意, 初始化 echarts 实例, 需要基于已经准备好的 DOM 元素. 所以这个步骤应该写在 mounted 中进行.
[!note] 为什么使用 this 因为这个东西我们还需要在外面用到, 不如直接挂载在对象上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 mounted ( ) { this .myChart = echarts.init (document .querySelector ('#main' )); var option = { title : { text : '账单列表' , left : 'center' }, tooltip : { trigger : 'item' }, legend : { orient : 'vertical' , left : 'left' }, series : [ { name : '消费账单' , type : 'pie' , radius : '50%' , data : [], emphasis : { itemStyle : { shadowBlur : 10 , shadowOffsetX : 0 , shadowColor : 'rgba(0, 0, 0, 0.5)' } } } ] }; this .myChart .setOption (option); },
我们在更新数据的部分, 进行图标的更新即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 async getList ( ) { const res = await axios.get ('https://applet-base-api-t.itheima.net/bill' , { params : { creator : 'lc' } }) this .list = res.data .data ; this .myChart .setOption ({ series : [ { data : this .list .map ((item ) => ({value : item.price , name : item.name })) } ] }) },
至此, 功能已经实现.
工程化开发入门 什么是工程化 开发 Vue 有两种方式:
核心包传统开发模式, 就是基于 HTML+CSS+JS, 之前的那种开发模式
工程化开发模式: 基于构建工具的环境中开发 Vue (可以使用 TS, ES6 语法等等)
使用脚手架 创建项目 然而, 这些东西配置起来并不简单, 缺乏一个统一的配置标准. 这里我们希望能够快速的, 标准的构建一个开发环境. 对于 Vue, 我们可以使用官方提供的一个脚手架: Vue CLI.
开箱即用, 零配置, 并且内置了各种标准工具, 标准化! 使用方式其实就是四个步骤:
全局安装脚手架 (只需要执行一次): yarn global add @vue/cli 或者 npm i @vue/cli -g
直接打开控制台, 输入上面的命令之一进行安装即可.
安装后, 只要在控制台输入 vue --version, 能够出来版本号就算安装完成.
创建项目架子: vue create project-name, 项目名称不能有中文, 最好全部小写字母加 -.
在想要创建项目的地方执行命令, 就可以创建一个项目了
这里会提示你选择版本以及包管理器, 我们目前使用 Vue 2 +yarn 就好. 随后就会开始创建项目了!
启动项目: yarn serve 或者 npm run serve.
根据上面的说明, 直接进入目录并且运行命令即可!
根据提示, 打开浏览器并且进行访问, 就可以看到效果了.
查看 package 中的命令 启动的命令我们是可以自由更改的. 查看一下新建的项目的 package.json 文件, 可以找到如下命令:
这些命令就是 npm 和 yarn 运行的东西了. 比如上面的 yarn serve, 就是运行的第一个命令, 可以根据需要自行修改.
目录文件介绍 我们可以观察一下项目自动创建的一些文件:
目录
介绍
index.html 就是最简单的模板文件, 决定了页面.App.vue 文件就是 App 的根组件文件, 项目运行后看到的内容都会写在这个文件中.main.js 文件, 是入口文件, 也是项目打包或者运行的时候, 第一个执行的文件.node_modules 目录就是依赖目录, 我们不需要手动的修改它.vue.config.js 是 vue-cli 的配置文件. 剩下的文件都是一些基本的文件, 具体作用不做进一步的介绍了.
项目运行流程 就像刚才说的, main.js 是我们的入口文件. 不妨直接分析一下这个入口文件.
main.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Vue from 'vue' import App from './App.vue' Vue .config .productionTip = false new Vue ({ render : h => h (App ), }).$mount('#app' )
对于 .vue 文件, 就是一个组件了, 这就是我们 Vue 开发主要写的地方.
组件化开发 初识组件 什么是组件化 一个页面可以拆分为多个组件, 每个组件都有自己独立的结构, 样式, 行为. 好处就是便于维护, 同时利于复用, 可以极大的提升开发效率.
另外, 一个组件也可以继续拆分为多个组件, 进而实现零件化, 组件化 . 最后我们搭建页面其实就是搭积木了, 会非常的方便!
组件可以分类为: 根组件和普通组件, 普通组件就是所有的其他小组件, 根组件就是整个应用最上层的组件. 比如一个根组件可以包含头部组件, 中间主体组件以及最后的底部组件.
vue 的组件 在 vue 中, 一个组件分为三个大块:
结构部分
行为部分
样式部分
结构部分只能存在一个根元素, 行为部分就是 js 代码部分, 可以实现一些事件效果; 样式部分则可以给结构部分提供样式, less 和 sass 都是支持的!
在 App 组件中, 我们仅保留如下内容:
1 2 3 4 5 6 7 8 9 10 11 <template> <div> 我是结构 </div> </template> <script> </script> <style> </style>
可以看到页面中已经正常进行渲染了.
如果需要提供样式, 直接在下面的 style 中进行书写即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="app"> 我是结构 </div> </template> <script> </script> <style> .app { width: 400px; height: 400px; background-color: pink; } </style>
[!tip] 支持 less 如果需要写 less 语法, 只需要设置 lang=’less’, 并且安装对应的包即可.
如果还需要提供逻辑, 直接写在 script 中就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="app" @click="handleClick"> 我是结构 </div> </template> <script> /*这里可以导出当前组件的配置项 所有的生命周期钩子都是可以直接写的 不过data比较特殊就对了*/ export default { methods: { /*可以写一些方法*/ handleClick(){ console.log("你好") } } } </script>
点击事件正常被触发了!
普通组件的注册 上面已经介绍过根组件了, 我们还想要将根组件进行拆分, 拆分为多个小组件. 组件的注册有两种方式: 局部注册和全局注册, 局部的就是只能在组件内使用; 全局就是在所有的组件中都可以进行使用.
对于局部注册, 我们可以直接按照两步走:
在组件目录 components 下创建 .vue 文件, 三个部分组成
在使用的组件内导入并且注册
[!important] 注意点 组件名称必须由两个单词, 并且按照大驼峰命名法进行定义
接下来, 可以注册三个组件, 作为一个页面的 Header, Main 以及 Footer. Header 组件基本的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <script> export default { name: "MyHeader" } </script> <template> <div class="header"> 我是Header </div> </template> <style scoped> .header { height: 100px; line-height: 100px; text-align: center; font-size: 30px; background-color: mediumpurple; color: white; } </style>
定义好了组件, 就可以去 App 中使用了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div class="app"> <!--使用注册好的组件--> <MyHeader/> </div> </template> <script> // 要使用组件 就需要先导入组件 import MyHeader from "@/components/MyHeader.vue"; // 需要使用配置项 说明我要使用这个组件 export default { components: { MyHeader } } </script> <style> .app { width: 600px; height: 700px; background-color: #87ceeb; } </style>
现在, 组件已经成功地使用, 并且显示出来了!
别的组件同理进行实现就好.
[!success] 注册时为什么直接写组件名 因为组件名和导入的组件名称一样, 可以简写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <template> <div class="app"> <!--使用注册好的组件--> <MyHeader/> <MyMain/> <MyFooter/> </div> </template> <script> // 要使用组件 就需要先导入组件 import MyHeader from "@/components/MyHeader.vue"; import MyMain from "@/components/MyMain.vue"; import MyFooter from "@/components/MyFooter.vue"; // 需要使用配置项 说明我要使用这个组件 export default { components: { MyHeader, MyMain, MyFooter } } </script> <style> .app { width: 600px; height: 700px; background-color: #87ceeb; padding: 10px 20px; } </style>
这样 App 就成功进行渲染了!
全局组件的注册 其实就是将组件注册在 main.js 中, 这样子所有的组件就都可以读取到这个组件了. 例如我有一个按钮组件, 就可以在 main.js 中使用如下语法 进行注册: (就是调用一下 Vue 的一个方法)
main.js 1 2 3 4 import MyButton from "@/components/MyButton.vue" ;Vue .component ("MyButton" , MyButton );
现在, 在所有的组件中, 不需要引入就可以直接使用这个注册的组件了!
1 2 3 4 5 6 <template> <div class="header"> 我是Header <MyButton/> </div> </template>
不需要引入, 就可以直接使用, 并且 WebStorm 会提供代码提示!
[!help] 什么时候使用全局组件 一般来说我们都是使用局部注册的, 只有我们发现这确实是一个通用组件, 才会进行组件的全局注册.
组件分类 后面我们会学到路由相关的东西, 干脆这里直接说明不同组件的位置.
对于页面性质的组件, 比如登陆页面, 或者是其他页面, 我们都是放在 views 目录下的; 但是对于复用类的组件, 也就是普通的组件, 我们都是放在 components 目录里面的.
无论如何, 其实本质都是 .vue 文件, 都是组件. 我们进行分类存放, 就是单纯的方便进一步的维护 .
组件的三大组成部分 组件的三大部分, 其实就是: 结构, 样式和逻辑部分. 这里有一些注意点需要注意.
结构中, 只能有一个根元素.
样式中, 如果希望自己的样式只作用于自己, 需要写一个 scope.
逻辑中, el 是根组件独有的, data 是一个函数, 其他的配置项都是一致的.
我们在代码中进行演示其中的 2 和 3, 剩下的我们已经有所了解了.
组件样式冲突 scoped 一般来说, 组件中的样式其实都是全局生效的. 因此我们写的样式容易让多个组件之间出现冲突. 例如我有两个 ComA 和 ComB 组件, 其中只有文本和样式不一样.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script> export default { name: "ComA" } </script> <template> <div class="my-div"> 我是A组件的Div标签 </div> </template> <style> .my-div { background-color: dodgerblue; /*B中为 background-color: palevioletred;*/ } </style>
在 App 中引入渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div class="app"> <!--使用组件--> <ComA/> <ComB/> </div> </template> <script> import ComA from "@/components/ComA.vue"; import ComB from "@/components/ComB.vue"; export default { components: { ComA, ComB } } </script>
显示效果如下了
这里显然存在问题, 我们的蓝色效果消失了! 为了解决这个问题, 我们需要在 style 标签后面添加一个 scoped, 表示这个样式仅仅适用于自己.
1 2 3 4 5 <style scoped> .my-div { background-color: palevioletred; } </style>
现在再次查看效果, 已经不会出现冲突的情况了.
查看一下 HTML 元素, 发现其实 Vue 帮我们在原本样式的类名后面加了点料:
就是前面的这个自动添加的一个属性选择器, 让我们的效果就完全不一样了!
[!success] Scoped 原理
当前组件内标签都加上了一个 data-v-hash 值的属性.
css选择器都被添加了一个 [data-v-hash值] 的属性选择器
最后的效果就是只有当前组件能够使用对应的样式.
data 是一个函数 对于组件来说, 一个组件的 data 必须是一个函数. 这是为了保证每个组件实例都维护独立 的一份数据对象. 通过函数返回的是一个新的对象, 就不会造成之间的影响了.
最直观的, 我们实现一个简单的计数器组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <script> export default { /*data是一个函数*/ data() { return { count: 100 } } } </script> <template> <div class="base-counter"> <button @click="count--">-</button> <!--使用还是一样的 插值语法--> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template> <style scoped> .base-counter * { margin: 5px; } </style>
挂载到页面上, 我们可以看到计数器的效果已经实现了!
接下来, 我们多次使用这个组件, 就可以发现每个组件的数据都是独立运行的, 而不是统一的了.
1 2 3 4 5 6 7 8 <template> <div class="app"> <!--使用组件--> <KCounter/> <KCounter/> <KCounter/> </div> </template>
这就是为什么 data 需要写成一个函数, 以及 data 在组件中的使用方法.
组件通信 引入组件通信 我们知道, 每个组件的数据都是独立的, 无法直接访问其他组件的数据. 如果我们想要访问到的组件的数据, 就需要使用组件通信了.
组件关系 组件其实就两种关系:
父子关系, 一个是包裹, 一个是被包裹
非父子关系, 就是分开的, 并列的, 或者没有直接的关联.
不同的关系我们使用的组件通信方案是不一样的! 对于父子关系, 我们使用的是 props 以及 $emit; 对于非父子关系, 我们使用 provide & inject 以及 eventbus.
如果使用的场景非常非常的复杂, 我们有一个终极的解决方案: Vuex . 我们根据不同的需求, 使用不同的方案即可.
父子组件传参 这里也需要进行区分, 父->子 以及 子->父 . 这里按照顺序进行记录.
父 -> 子传参 父组件给子组件传参, 需要使用 props 进行传参. props 其实就是子组件身上的标签, 我们通过标签进行传参. 标签中的值就是传递的值, 使用 props 进行接收即可.
父组件中, 正常进行定义数据, 不过使用标签的方式进行数据的传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template> <div class="app"> 我是父组件 <!--使用组件标签 添加属性的方式传递参数--> <!--这里是动态的属性 所以只用:title 来进行传递--> <KSon :title="myTitle"/> </div> </template> <script> import KSon from "@/components/KSon.vue"; export default { data() { /*父组件直接准备一些数据进行传递*/ return { myTitle: "Kaede 221" } }, components: { KSon } } </script>
随后, 我们在子组件中, 就可以通过 props 进行接收了.
1 2 3 4 5 6 7 8 9 10 11 12 13 <script> export default { /*接收参数 也是在这个导出的对象当中的*/ /*props是一个数组, 里面的字符串就是传入的数据的名称*/ /*传入的是title, 则必须是title*/ props: ['title'] } </script> <template> <div>我是Son组件, 传入的title为: {{ title }}</div> <!--拿到数据了, 直接渲染使用即可--> </template>
子组件中只需要进行接收, 使用的时候直接双花括号就可以获取对应的值了!
[!info] 如果修改父组件的数据呢? 子组件传入的数据发生了变化, 同样会触发页面的重新渲染
子 -> 父传参 这里需要使用到一个 $emit 来通知父组件进行数据的修改更新. 父组件中需要准备一个修改的函数, 传递给子组件; 子组件中通过传入的函数名称作为信号, 传递对应的数据, 就可以实现传参了.
首先, 我们在子组件中实现点击按钮后, 发送一个修改 title 的请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <script> export default { methods: { changeFn() { /*我们不能直接修改数据, 需要通知父亲修改*/ this.$emit('changeTitle', 'yyt love lc!') } } } </script> <template> <div> 我是Son组件 <!--提供一个按钮 点击后传递数据给父组件--> <button @click="changeFn">修改title</button> </div> </template>
之后, 在父组件中, 我们接收这个信号, 并且绑定对应的函数进行传参即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template> <div class="app"> 我是App组件 目前标题为: {{ title }} <KSon @changeTitle="handleChange"/> </div> </template> <script> import KSon from "@/components/KSon.vue"; export default { data() { return {title: "Hello World"} }, components: { KSon }, methods: { /*提供处理函数*/ handleChange(newTitle) { /*实现修改逻辑*/ this.title = newTitle; } } } </script>
至此, 数据修改成功!
props 详解 组件的 props 是不能随便传递的, 我们肯定需要对类型做一些限制. 刚才我们使用的语法是 props: ["aaa", "bbb", "ccc"], 现在我们有一个新的写法了:
1 2 3 4 5 6 7 8 9 10 11 props : { 校验的属性名: { type : 类型, required : true , default : 默认值, validator (value ){ return 是否通过校验 } } }
该配置项写在子组件中进行校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script> export default { /*在子组件中配置props的属性*/ props: { myTitle: { type: String, required: true, default: 'yyt and lc', } } } </script> <template> <div> 我是Son组件, Title 为 {{ myTitle }} </div> </template> <style scoped> </style>
父组件中正常进行传参即可.
1 2 3 4 5 <template> <div class="app"> <KSon :my-title="title"/> </div> </template>
如果没有写必要的内容, 编辑器和浏览器中就会有一些提示:
编辑器
浏览器
另外, props 的数据是外部的, 不能够直接进行修改, 需要遵循单向数据流 .
非父子组件传参 事件总线 EventBus 我们现在有两个关系不是那么大的组件, 我想要这两个组件之间能够传参, 怎么办呢? 这里就需要使用到事件总线, 也就是 event bus 了. 事件总线可以实现非父子组件之间的简单消息传递. (复杂场景用 Vuex)
首先, 我们需要创建一个全局可以访问的事件总线, 比如空的 Vue 实例; 随后, 我们监听实例的变化, 并且触发需要的回调事件即可.
例如我现在有两个独立的组件:
现在他们两个不是父子关系, 就可以尝试使用事件总线了. 首先, 我们需要创建一个大家都可以访问到的事件总线. 作为一个工具 , 我们可以创建在 utils 目录中.
1 2 3 4 5 6 7 8 9 10 import Vue from "vue" ;const Bus = new Vue ()export default Bus
随后, 我们可以在 A 组件中导入这个事件总线进行使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <script > import Bus from "@/utils/EventBus" ;export default { created ( ) { Bus .$on('sendMsg' , (msg ) => { console .log (msg) }) } } </script >
这里就实现了接收消息的部分, 随后我们需要在 B 组件中发送消息.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <script> import Bus from "@/utils/EventBus"; export default { name: "BaseB", methods: { clickSend() { // 调用事件总线 发布消息 Bus.$emit('sendMsg', '我是B组件的消息!') } } } </script> <template> <div> B组件 <button @click="clickSend">发送通知</button> </div> </template> <style scoped> </style>
至此, 事件的沟通已经实现了! 这就是事件总线.
[!question] 注意 这里的事件总线的原理就是订阅一个信号, 只要有人触发了这个信号, 就会调用对应的回调. 所以, 一个信号如果有多个接收者, 则会同时触发多个事件!
provide & inject 这种语法可以实现跨层级的共享数据. 比如说, 我的上层组件中, 存在一个 A 数据, 现在我的下层组件, 以及下层组件的下层组件等等, 都可以使用到这个 A 数据, 就可以使用这种方法, 实现跨层级传数据.
只需要在最上面提供的时候, 使用 provide 提供数据, 就可以在子组件中使用 inject 来接收数据了.
假如现在我有一个多层级的结构, 我在 App 组件中准备数据, 准备传递给最底层的 C 组件, 就可以首先提供数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 data ( ) { return { color : 'pink' , userinfo : { name : 'yyt' , age : 18 } } }, provide ( ) { return { color : this .color , userInfo : this .userinfo } }
现在就实现了数据的共享了, 可以在子孙组件中尝试进行接收:
1 2 3 4 5 6 7 8 9 10 11 <script> export default { name: "ComC", // 接收provided的数据 可以直接使用的. inject: ['color', 'userinfo'], } </script> <template> <div>ComC: Color is {{ color }} and userInfo is {{ JSON.stringify(userinfo) }}</div> </template>
现在已经接收到数据了.
我们尝试在 App 中修改一下这两种数据:
1 2 3 4 5 6 7 8 <template> <div> 我是顶层组件: <button @click="()=>{color='blue'}">修改color</button> <button @click="()=>{userinfo.age = 19}">修改age</button> <ComA/> </div> </template>
可以看到, 简单数据没有响应式的出现; 但是对象, 复杂数据类型是响应式的! 这就是说, 如果我们需要做多层数据的传递, 我们一般都会用一个复杂类型进行传递.
v-model 用于组件通信 v-model 原理 为了更好的使用它进行通信, 我们更需要了解它的原理. 其实 v-model 就是一种语法糖:
1 2 3 <input v-model ="msg" type ="text" > <input :value ="msg" @input ="msg = $event.target.value" type ="text" >
简单说, 就是数据变了, 视图中的 value 就变化了; 另外, 如果输入东西, 就会触发 @input, 将输入框的值更新在 msg 中. 这就是 v-model 的基本原理.
表单类组件封装 数据是在父组件的, 但是子组件中的数据需要拿到. 我们都知道, 子组件不能直接修改父组件的值, 这里我们就需要做一个步骤: v-model 的拆解.
父传子: 父组件 props 传递, v-model 拆解绑定数据
子传父: 监听输入, 传值给父组件进行修改
这里先实现第一步. 在父组件中传递数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div> <BaseSelect :cityId="selectID" /> </div> </template> <script> import BaseSelect from "@/components/BaseSelect.vue"; export default { components: {BaseSelect}, data() { return { // 数据是由父组件提供的 所以写在这里 selectID: '104' } } } </script>
随后, 在子组件中使用 props 进行接收, 并且下面不能使用 v-model, 而是使用 :value 直接设置值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> export default { name: "BaseSelect", // 使用props进行接收 props: { cityId: String } } </script> <template> <!--这里注意, prop不能直接v-model 所以需要拆分!--> <select :value="cityId"> <option value="101">北京</option> <option value="102">上海</option> <option value="103">广州</option> <option value="104">深圳</option> <option value="105">武汉</option> </select> </template>
接下来是第二步, 如果修改了下拉菜单的值, 就通过子传父的形式, 调用函数来发送数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> export default { methods: { handleChange(e) { // 通过事件, 拿到新的选中项目 this.$emit('changeID', e.target.value) } } } </script> <template> <!--这里注意, prop不能直接v-model 所以需要拆分!--> <select :value="cityId" @change="handleChange"> </select> </template>
随后在父组件中接收事件, 并且设置当前的 ID 即可:
1 2 3 4 5 6 7 8 9 10 <template> <div> 当前城市ID: {{ selectID }} <BaseSelect :cityId="selectID" @changeID="selectID = $event" /> <!--这里通过$event来获取函数的形式参数.--> </div> </template>
至此, 就实现了双向数据绑定了!
v-model 简化代码 在父组件中, 数据其实都是自己的, 那么为什么不直接写一个 v-model 呢? 确实可以, 不过这里有限制:
子组件中, 需要 props 只能通过 value 接收, 事件触发就是 @input.
父组件中, v-model 需要直接绑定数据
父组件中, 代码更改后如下:
1 <BaseSelect v-model="selectID"/>
子组件中, 如上进行修改即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script> export default { // props必须是value props: { value: String }, methods: { handleChange(e) { // 事件必须是input this.$emit('input', e.target.value) } } } </script> <template> <!--这里注意, prop不能直接v-model 所以需要拆分!--> <select :value="value" @change="handleChange"> </select> </template>
现在的效果和刚才一摸一样, 代码还得到了简化+提升了规范性!
.sync 修饰符 其实和上面的 v-model 差不多, 不过它的 prop 属性名是可以进行修改的. 本质就是 :属性名 配合 @update:属性名 的合写.
[!note] 感觉用的不是很多 所以这里就不写啦
案例: 记事本组件版 导入代码模板 相当于对之前的记事本进行重构, 使用组件化开发的模式来实现记事本的效果. 使用到的基本项目框架如下: 直接下载, 覆盖 src 目录中的内容就好.
这是一个基本的代码框架, 导入了样式文件, 进而实现样式.
我们要做的就是拆分为单个组件, 并且实现对应功能.
拆分组件 拆分一下结构, 这个程序由 Header 部分, 以及中间的部分, 以及下面的三个部分组成. 所以我们在 components 目录下创建对应的组件.
其中, Header 的部分就是 HTML 中的 Header 部分, 直接复制出来就好:
1 2 3 4 5 6 7 <template> <header class="header"> <h1>小黑记事本</h1> <input placeholder="请输入任务" class="new-todo"/> <button class="add">添加任务</button> </header> </template>
列表区域, 就是主体区域, 同理, 直接拿走!
1 2 3 4 5 6 7 8 9 10 11 12 <template> <section class="main"> <ul class="todo-list"> <li class="todo"> <div class="view"> <span class="index">1.</span> <label>吃饭</label> <button class="destroy"></button> </div> </li> </ul> </section> </template>
最后, 也是底部部分, 一样直接拿走拿走~
1 2 3 4 5 6 7 8 9 10 <template> <footer class="footer"> <!-- 统计 --> <span class="todo-count">合 计:<strong> 1 </strong></span> <!-- 清空 --> <button class="clear-completed"> 清空任务 </button> </footer> </template>
最后, 我们回到 App 中, 引入, 注册并且使用三个组件, 实现代码框架 (否则是看不到东西的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <section id="app"> <TodoHeader/> <TodoMain/> <TodoFooter/> </section> </template> <script> // 导入组件 import TodoHeader from "@/components/TodoHeader.vue"; import TodoMain from "@/components/TodoMain.vue"; import TodoFooter from "@/components/TodoFooter.vue"; export default { /*注册组件*/ components: { TodoHeader, TodoMain, TodoFooter } } </script>
回到页面, 没有问题, 至此组件拆分完毕.
渲染待办任务 首先我们需要提供数据, 其次我们再进行渲染就好. 不过这里我们需要注意一下, 数据应该提供在哪里呢? 显然, 这个数据在中间会用到, 在底部也会用到, 顶部也会用到. 所以数据 应当提供在父组件中 .
需要的时候, 直接通过 props 进行传参就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <template> <section id="app"> <TodoHeader/> <TodoMain :list="todoList"/> <TodoFooter/> </section> </template> <script> // 导入组件 import TodoHeader from "@/components/TodoHeader.vue"; import TodoMain from "@/components/TodoMain.vue"; import TodoFooter from "@/components/TodoFooter.vue"; export default { /*注册组件*/ components: { TodoHeader, TodoMain, TodoFooter }, data() { return { todoList: [ {id: 1, name: "唱"}, {id: 2, name: "跳"}, {id: 3, name: "rap"} ] } } } </script>
随后来到子组件中, 接收传入的数据就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <script> export default { /*接收参数*/ props: { list: { type: Array } } } </script> <template> <section class="main"> <ul class="todo-list"> <li class="todo" v-for="(item, index) in list" :key="item.id"> <div class="view"> <span class="index">{{ index + 1 }}.</span> <label>{{ item.name }}</label> <button class="destroy"></button> </div> </li> </ul> </section> </template>
至此, 列表已经成功渲染.
添加任务 本质就是在 Header 中输入任务, 然后往任务列表的最前面添加一个任务. 这里涉及到了 子->父 组件传参, 也就是调用 $emit 进行传参.
先在子组件中, 实现表单数据的绑定.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <script> export default { data() { return { todoName: "" } }, methods: { handleAddTodo() { // 传递给父组件 进行添加 this.$emit('addTodo', this.todoName); // 清空输入 this.todoName = ""; } } } </script> <template> <header class="header"> <h1>小黑记事本</h1> <!--绑定输入的内容 并且监听回车--> <input v-model="todoName" @keyup.enter="handleAddTodo" placeholder="请输入任务" class="new-todo"/> <button class="add" @click="handleAddTodo">添加任务</button> </header> </template>
随后在父组件中进行绑定.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <section id="app"> <!--绑定添加任务的方法--> <TodoHeader @addTodo="addTodo"/> </section> </template> <script> export default { methods: { addTodo(newTodo) { // 根据新的 添加新任务 this.todoList.unshift({ id: new Date(), name: newTodo }) } } } </script>
至此, 添加任务的功能已经实现!
删除任务 其实差不多, 就是通过 id 进行删除. 还是在子组件中添加回调方法, 传递 ID 给父组件, 然后父组件调用对应的方法删除列表中的元素即可.
这个实现起来比较简单, 直接写在行内了. 首先在子组件中发送请求:
1 <button class="destroy" @click="$emit('deleteTodo', item.id)"></button>
随后在父组件中接收请求, 并且调用对应的方法:
1 <TodoMain :list="todoList" @deleteTodo="deleteTodo"/>
1 2 3 4 5 6 7 deleteTodo (todoId ) { this .todoList = this .todoList .filter ((item ) => { return item.id !== todoId; }) }
删除功能就这样实现了!
底部统计功能 考虑到这直接就是一个数字, 同时是一个基于数据的统计结果, 我们不妨使用计算属性来直接实现, 然后传递计算属性给子组件即可.
1 2 3 4 5 computed : { totalTasks ( ) { return this .todoList .length ; } }
1 <TodoFooter :totalTasks ="totalTasks" />
随后在子组件中接收参数, 并且渲染到页面上.
1 2 3 4 5 export default { props : { totalTasks : Number } }
1 <span class ="todo-count" > 合 计:<strong > {{ totalTasks }}</strong > </span >
现在效果就实现完成!
清空功能 其实实现起来, 就是直接让列表为空就好, 最简单的一个功能.
1 2 3 <button class ="clear-completed" @click ="$emit('clearTodos')" > 清空任务 </button >
父组件中直接清空就好.
1 <TodoFooter :totalTasks ="totalTasks" @clearTodos ="()=>{this.todoList = []}" />
持久化存储 我们现在只要刷新页面, 原本的数据就没了, 这并不是我们想要的结果. 所以我们可以让数据持久化的保存在浏览器的本地, 刷新的话, 可以拿到之前的数据!
我们什么时候储存数据呢? 其实就是当数据变化的时候. 自然, 我们可以使用侦听器来实现功能.
1 2 3 4 5 6 7 8 9 10 watch : { todoList : { deep : true , handler (newValue ) { localStorage .setItem ('list' , JSON .stringify (newValue)); } } }
随后, 我们在读取的时候, 肯定是优先读取本地数据的. 改一下上面初始化数据的部分:
1 2 3 4 5 6 7 8 9 data ( ) { return { todoList : JSON .parse (localStorage .getItem ('list' )) || [ {id : 1 , name : "唱" }, {id : 2 , name : "跳" }, {id : 3 , name : "rap" } ] } },
现在, 就算刷新页面, 数据也将会储存了.
至此, 案例完结!
ref & $refs 介绍 我们可以使用 ref 以及 $refs 来获取 DOM 元素, 或者组件实例. 查找的范围是当前的组件内, 更加的精确以及稳定. 比如我们获取图表, 就可以使用 ref 来指定图表是什么, 然后直接获取这个图表进行配置.
基础语法, 其实就是给目标标签添加一个 ref 属性, 随后在恰当的时机, 使用 this.$refs.xxx 获取就好; 这里的恰当时机指的是至少等待 DOM 渲染完毕后.
获取 DOM 例如我有一个 echart 表格组件, 现在我想要挂载在一个盒子中. 为了防止类名冲突, 我们就可以直接使用 ref 进行数据的绑定了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div ref="myChart" class="base-chart-box">子组件</div> </template> <script> import * as echarts from 'echarts' export default { mounted() { // 基于准备好的dom,初始化echarts实例 const myChart = echarts.init(this.$refs.myChart) // 绘制图表 myChart.setOption({...}) }, } </script>
这样我们就不需要使用什么 document.xxx 来获取 DOM 元素了.
获取组件 我们有的时候, 组件中会存在一些方法, 我们可以直接使用 ref 来获取这个组件, 随后使用 $refs 来调用组件身上的一些方法, 进而实现一些功能.
比如我有一个输入框的表单, 我希望直接在外部获取输入后的整合好的信息. 子组件中只需要提供这样一个获取数据的方法就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <script> export default { name: "BaseCom", data() { return { name: "", age: 0, } }, methods: { getInfo() { return { name: this.name, age: this.age } } } } </script> <template> <div> <div>姓名: <input v-model="name" type="text"/></div> <div>年龄: <input v-model.number="age" type="number"/></div> </div> </template>
随后, 父组件中, 直接使用 ref 指向这个组件, 就可以快速的通过 this.$refs.xxx 来调用组件身上的方法, 进入获取数据了!
vue 异步更新 在 Vue 中, 其实数据和视图是异步更新的. 因为如果只要有页面的东西发生变化就立马更新, 页面一旦复杂, 就会变得非常低效了. 所以 Vue 其实是等待一个时刻进行统一的更新.
可以想象这样一个场景, 点击按钮后, 隐藏按钮, 下面出现一个输入框并且自动聚焦在输入框中. 如果我们直接写简单的代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <template> <div class="app"> <div v-if="isShowEdit"> <input type="text" v-model="editValue" ref="inp"/> <button>确认</button> </div> <div v-else> <span>{{ title }}</span> <button @click="handleEdit">编辑</button> </div> </div> </template> <script> export default { data() { return { title: '大标题', isShowEdit: false, editValue: '', } }, methods: { handleEdit() { // 首先 显示输入框 this.isShowEdit = !this.isShowEdit; // 然后获取焦点 this.$refs.inp.focus(); } }, } </script>
网页中查看, 尝试点击按钮:
可以看到, 点击后并没有成功地进行自动聚焦, 并且控制台是有报错的, 找不到对象.
为了解决这个问题, 我们可以使用一个东西: $nextTick. 作用就是等待 DOM 更新后, 再触发对应的内容. 语法其实就是在调用聚焦 focus 的时候, 传入一个回调函数, 回调函数中写 DOM 更新后的逻辑即可.
1 2 3 4 5 6 7 8 handleEdit ( ) { this .isShowEdit = !this .isShowEdit ; this .$nextTick(() => { this .$refs .inp .focus (); }) }
现在回到网页, 已经可以成功地自动聚焦了!
[!info] setTimeout 可以吗 可以, 但是我们不知道 DOM 需要渲染多久, 时间是不精准的
自定义指令 引言 之前我们学到过 v-for, v-if, 以及 v-model 等等. 这些都是 vue 内置好的, 叫做内置指令; 不过我们也可以自己封装一些指令, 比如 v-focus 实现自动聚焦, 或者 v-loading 实现加载效果等等.
我们可以在指令中封装一些 DOM 操作, 进而快速的实现我们的需求!
基础语法 例如, 我们现在存在一个需求, 页面加载的时候, 让元素获得焦点. 这里可以使用 autofocus, 但是在 safari 浏览器存在兼容性问题; 所以我们需要操作 DOM, 实现聚焦.
我们根据之前的经验, 其实就是下面这一段话:
1 2 3 mounted () { this .$refs .inp .focus () }
虽然很简单, 但是如果写太多次就没什么必要了. 所以我们需要将这种东西直接封装为指令. 自定义指令存在两种语法, 一种是全局注册的语法, 可以参考下面的定义方式:
1 2 3 4 5 6 7 Vue .directive ('指令名' , { inserted (el ) { el.focus (); } })
局部注册其实就是写在组件里面, 按照配置项进行配置就好, key 是指令名, value 就是 inserted 之类的.
[!important] inserted 是什么 这个函数会在指令所在元素挂在完成后调用
只要指令定义完毕, 我们只需要使用 v-指令名 就可以直接使用对应的指令了!
全局注册 比如我现在有一个输入框, 我希望进入页面后直接聚焦. 如果使用全局注册, 则可以直接使用指令. 首先在 main.js 中进行定义:
[!tip] 位置 注册指令应当在创建 Vue 实例前, 否则报错
1 2 3 4 5 6 7 Vue .directive ('focus' , { inserted (el ) { el.focus (); } })
随后, 只需要在需要的组件身上使用即可
1 <input type ="text" v-focus />
现在只需要打开浏览器, 就是聚焦的状态了.
局部注册 就是在局部的配置项中注册. 比如有一个组件, 就可以在组件中进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div class="app"> <!--使用局部的指令--> <input type="text" v-focus/> </div> </template> <script> /*在局部注册这个指令*/ export default { directives: { // 指令名:指令配置项 focus: { inserted(el) { el.focus(); } } } } </script>
局部注册, 就是只有当前组件可以使用了, 效果是一样的.
指令的值 比如, 我希望实现一个 color 指令, 传入不同的颜色, 设置标签文本的颜色是不一样的. 这里的指令就需要传递参数了. 语法上, 我们只需要使用 = 就可以给指令传值, 绑定具体的参数值; 使用 binding.value 就可以拿到传入的参数值了. 这个 binding 就是 el 之后的第二个参数.
另外, 如果指令的值发生了修改, 会调用与 inserted 同级的一个函数: update, 参数还是 el 和 binding, 这里就可以实现组件的更新了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div class="app"> <h1 v-color="'red'">指令的值 1</h1> <h1 v-color="color2">指令的值 2</h1> </div> </template> <script> export default { data() { return { color2: '#0670ea' } }, directives: { color: { inserted(el, binding) { /*使用binding获取value*/ el.style.color = binding.value; }, update(el, binding) { // 实现响应式 如果颜色变化 也能正常显示 // 提供值变化后的更新逻辑 el.style.color = binding.value; } } }, } </script>
以上就是比较完善的一个指令了, 下面是显示效果.
插槽 默认插槽 作用就是让组件内部的一些结构支持自定义. 比如我封装了一个对话框组件, 我希望里面的内容是可以自定义直接写在里面的, 不写死了, 这里就可以使用到插槽了.
插槽的使用语法分为两步:
在组件内需要定制结构的部分, 使用 <slot></slot> 进行占位
使用组件的时候, 组件需要使用双标签, 中间写插槽的结构即可.
在组件中, 我们使用 slot 占位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div> <div class="dialog-content"> <!--这里我是希望可以自定义的, 使用占位--> <slot></slot> </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div> </template>
随后在使用组件的时候, 直接填入东西就好.
1 2 3 4 5 6 7 8 9 10 11 12 <template> <div> <MyDialog> 你确认要离开吗? </MyDialog> <MyDialog> <h1>这是一个标题</h1> <h2>这是第二个标题</h2> <div>可以说 啥都行</div> </MyDialog> </div> </template>
我们查看效果, 所有我们需要的东西都正常显示了!
插槽默认值 如果我们啥都不传入, 是啥都没有的, 这样不好. 所以我们可以使用插槽的默认值! 语法其实就是在 slot 中写默认内容即可.
1 2 3 <slot > 我是默认内容, 看来你没有写内容呢 </slot >
使用的时候, 如果没有写东西:
1 2 3 4 5 6 7 8 <template > <div > <MyDialog > 你确认要离开吗? </MyDialog > <MyDialog > </MyDialog > </div > </template >
则效果会变成这样子:
这就是插槽的后备内容, 也就是默认值.
具名插槽 我们经常会遇到一个组件中, 有多个内容需要定制. 默认插槽, 只能定制一个地方的内容, 但是我们可能需要定制标题, 也需要定制内容, 甚至下面的按钮.
具名, 其实就是给 slot 起名字. 每个 slot 代表不同地方就好; 使用的时候, 使用 <template> 包裹内容, 并且设置 v-slot:name 来配置是哪个插槽的内容.
[!success] v-slot:xxx 简写v-slot:xxx 可以等效于 #xxx, 算是一种简写形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="dialog"> <div class="dialog-header"> <h3> <!--我希望标题是可以自定义的--> <slot name="head"></slot> </h3> <span class="close">✖️</span> </div> <div class="dialog-content"> <slot name="content"></slot> </div> <div class="dialog-footer"> <slot name="footer"></slot> </div> </div> </template>
现在, 在使用的时候, 包裹我们需要分发的结果, 作为一个整体进行传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div> <MyDialog> <template #head>这是测试标题</template> <template #content>这是内容</template> <template #footer> <button>确认</button> <button>取消</button> </template> </MyDialog> <MyDialog> <template #head>Lc And yyt</template> <template #content>Love Forever!</template> <template #footer> <button>OK!</button> <button>Nono~</button> </template> </MyDialog> </div> </template>
现在效果已经顺利实现了!
作用域插槽
[!question] 注意 作用域插槽是插槽的传参语法, 不属于插槽的分类. 分类就是上面那两种.
定义 slot 插槽的时候, 其实是可以传值的! 也就是说可以给插槽绑定数据, 在使用组件的时候使用. 使用步骤还是两步: 给 slot 标签按照 添加属性 的方式传值; 所有的数据都会被收集到对象, 使用 #插槽名="obj" 进行接收就好. 如果是默认插槽, 就是 #default="xxx"
子组件中, 或者说有插槽的组件中, 需要在插槽中传递数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <table class="my-table"> ... <!--根据数据进行循环全然--> <tr v-for="(item, index) in data" :key="item.id"> ... <td> <!--我希望这里的东西是可以定制的--> <!--另外 需要传递ID, 所以传值--> <slot :id="item.id"></slot> </td> </tr> ... </table> </template>
随后, 使用的时候, 首先套一个 template 来获取传递的参数, 然后就可以直接使用了
1 2 3 4 5 6 7 <MyTable :data ="list" > <template #default ="id" > <button @click ="del(id)" > 删除</button > </template > </MyTable >
这里的 del 就是一个输出函数, 测试看看:
已经拿到数据了, 那么修改数据就很简单了.
路由 介绍 我们使用 Vue 开发的其实是 SPA 应用, 和 React 差不多. 单页面应用和多页面应用的实现方式不同, 单页面就是切换需要的地方, 按需更新, 开发效率和性能就更高了; 但是多页面应用的学习成本低, 首页加载速度更快, SEO 也更加友好.
通常来说, 系统类网站, 内部网站, 文档类网站就可以使用 SPA, 以及移动端的站点; 对于 SEO 要求高的程序, 比如官网, 或者什么, 则多页面更好.
那么说回路由, SPA 应用中, 页面是按需更新的, 所以我们需要明确访问路径和组件之间的对应关系. 这种关系其实就是 路由 . 路由, 就是路径和组件的映射关系.
路由的基本使用 我们在 Vue 中, 使用的路由是 VueRouter, 这是 Vue 官方提供的插件. 它的作用就是修改地址栏路径的时候, 切换显示的匹配的组件.
VueRouter 的使用分为 5+2 个步骤 ! 前 5 步是固定的.
下载 VueRouter 模块 (3.6.5 因为当前是 Vue2)
yarn add vue-router@3.6.5
引入模块
import VueRouter from 'vue-router'
安装注册
Vue.use(VueRouter) 可以理解为 Vue 的初始化
创建路由对象
const router = new VueRouter()
注入, 将路由对象注入到 Vue 实例中, 建立关联.
直接在 new Vue 的时候, 传入 router 即可.
下面是完整的代码实现.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import VueRouter from "vue-router" ;Vue .use (VueRouter );const router = new VueRouter ()new Vue ({ render : h => h (App ), router }).$mount('#app' )
写完后, 直接查看页面, 虽然什么都没有, 但是路径已经发生变化了!
自动多了一个 #, 这就代表当前已经被路由所管理了.
随后, 就是核心的两个步骤了. 这两个步骤在不同的项目中是不一样的!
创建需要的组件 (views 目录), 配置路由规则
配置导航, 配置路由出口 (匹配路由后显示组件的位置)
假设当前我的页面如下:
首先创建一下 views, 视图目录, 并且建立对应的组件.
随后, 就是在 main.js 中配置对应的规则.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const router = new VueRouter ({ routes : [ { path : '/find' , component : VFind }, { path : '/my' , component : VMy }, { path : '/friend' , component : VFriend } ] })
现在是没有效果的, 我们还需要告诉 Vue, 对于规则的东西应该显示在哪里, 并且设置点击后跳转的 href 路径
1 2 3 4 5 6 7 8 9 10 11 <template> <div> <div class="header"> <a class="item" href="#/find">发现音乐</a> <a class="item" href="#/my">我的音乐</a> <a class="item" href="#/friend">朋友</a> </div> <!--例如出口就在这里了--> <router-view></router-view> </div> </template>
现在点击不同的链接, 已经可以看到对应的路由已经实现了~ 这就是路由的基础使用.
路由模块封装 我们所有的路由配置, 目前都是写在 main.js 中的, 其实这样子并不合适. 因为后面项目大了, 这样的规则就很麻烦了. 所以我们需要将路由模块进行抽离, 提取到一个 router 目录下的 index.js 中进行维护.
首先, 在 src 下新建文件:
随后, 将路由相关的代码放进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import Vue from 'vue' import VueRouter from "vue-router" ;import VFind from "@/views/VFind.vue" ;import VMy from "@/views/VMy.vue" ;import VFriend from "@/views/VFriend.vue" ;Vue .use (VueRouter );const router = new VueRouter ({ routes : [ { path : '/find' , component : VFind }, { path : '/my' , component : VMy }, { path : '/friend' , component : VFriend } ] }) export default router
随后, 在 main.js 中引入我们导出的这个路由对象并且进行使用即可.
1 2 3 4 5 6 7 8 9 10 11 12 import Vue from 'vue' import App from './App.vue' import router from "@/router" ;Vue .config .productionTip = false new Vue ({ render : h => h (App ), router }).$mount('#app' )
现在的效果还是不变的, 能够正常使用路由.
[!tip] 注意 移动了位置, 组件的目录就变了. 最推荐的就是使用 @ 表示 src 目录, 进而使用对应的组件.
router-link 高亮 我们的路由可能点击后, 是需要有一个颜色变化的, 但是需要更多的 JS 代码来实现. 我们不如直接使用 VueRouter 提供好的一个东西: router-link, 这是另外一个提供好的全局组件.
基本使用 使用的时候, 正常按照标签的方式写, 不过 href 变成了 to, 其他的一摸一样.
1 2 3 4 5 6 7 8 9 10 11 <template> <div> <div class="header"> <!--使用的时候 to必须要写 否则报错--> <router-link to="/find">发现音乐</router-link> <router-link to="/my">我的</router-link> <router-link to="/friend">朋友</router-link> </div> <router-view></router-view> </div> </template>
点击后, 可以看到自动的添加了一些类名:
我们不妨直接实现其中的类名:
1 2 3 .router-link-active { color : cornflowerblue; }
现在查看页面, 点击后已经出现高亮效果了!
这就是导航链接 (声明式导航).
两个类名 刚才可以看到, 其实它加了两个类名. 其中一个有 exact 字样, 这就代表这是精准匹配用的. 比如说, 如果现在我的路由是: /home/a 或 /home/b 之类的, 那么普通的高亮都会亮; 但是如果设置的是 exact, 则只有完全匹配才会高亮, 否则无效.
一般来说, 我们使用的都是模糊匹配的, 这里就不做演示了.
自定义匹配类名 毕竟官方提供的类名很长, 我们有的时候可能就是希望使用一个 active 来代表. 这个时候, 我们就可以在配置路由对象的时候, 与 routes 并列的配置两个类名了.
1 2 3 4 5 6 7 8 const router = new VueRouter ({ routes : [], linkActiveClass : 'active' , linkExactActiveClass : 'active-exact' })
现在, 我们就可以使用简单的类名来实现效果了.
使用的时候自然, 只需要设置 .active 类名或者 .active-exact 即可.
跳转传参 我们有的时候会希望, 点击链接后, 将我们的数据发送出去, 进而请求对应的内容. 这里就需要进行传参了! 路由传参有两种: 查询参数传参以及动态路由传参.
查询参数传参 查询参数, 其实就是浏览器路径后面的那一串 ?xxx=yyy 之类的东西. 我们只需要在路径的后面加上就可以进行传参了. 传递参数后, 我们可以使用 $route.query.参数名 来获取.
可以先提供一个搜索的路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <template> <div> <div id="app"> <!--提供两个查询链接--> <router-link to="/search/?name=yyt">yyt</router-link> <router-link to="/search/?name=lc">lc</router-link> </div> <!--显示路由组件--> <router-view></router-view> </div> </template> <style scoped> #app { width: 100%; display: flex; justify-content: space-around; } a { color: white; font-size: larger; font-weight: bold; background-color: cornflowerblue; text-decoration: none; } </style> <script setup lang="ts"> </script>
随后, 在路由组件 Search 中, 接收参数, 并且显示出来.
1 2 3 <template> <div>我是Search组件. Name为: {{ $route.query.name }}</div> </template>
现在, 数据已经可以拿到了!
如果想在 JS 中获取, 加一个 this 就可以直接获取并且进行访问了.
动态路由传参 简单说, 就是路由规则中, 有一些东西是动态的. 我们在配置动态路由的时候, 语法如下:
1 2 3 4 5 6 routes : [ { path : '/search/:words' . component : Search } ]
多了一个 :, 这个冒号就是动态路由了. 这里可以匹配多个路径, 只要 words 存在就可以顺利匹配. 随后, 跳转的时候, 就可以直接按照路由的方式传递参数值了, 效率高一些.
可以使用 $route.params.参数名 来获取传入的参数内容.
代码中, 还是上面的案例, 我们修改一下路由的配置:
1 2 3 4 5 const router = new VueRouter ({ routes : [ {path : '/search/:words' , component : VSearch } ] })
现在, 在跳转的时候, 修改跳转的规则:
1 2 3 4 5 6 7 8 9 <template> <div> <div id="app"> <router-link to="/search/yyt">yyt</router-link> <router-link to="/search/lc">lc</router-link> </div> <router-view></router-view> </div> </template>
最后, 就可以接收传入的参数了.
1 <div > 我是Search组件. Name为: {{ $route.params.words }}</div >
效果实现了!
[!example] 什么时候使用对应的传参方法? 如果参数较多, 可以考虑查询参数; 如果参数少, 可以使用动态路由传参
动态路由参数可选符 我们如果在刚才的案例中, 直接访问 search, 可以看到页面中是啥都没有的.
这个时候, 我们就需要配置一下路由是可选的 了. 只需要修改路由部分, 加一个问号就好:
1 2 3 4 5 const router = new VueRouter ({ routes : [ {path : '/search/:words?' , component : VSearch } ] })
就算没有传递参数, 页面的基本框架也已经顺利展示.
重定向 有的时候, 网页打开的时候访问的是 / 路径, 但是我们的 / 是没东西的, 我们可能需要自动找到 /home 路径下面才有东西. 这就需要配置网页的重定向了, 也就是匹配某个路径后, 强制跳转到另外一个路径.
1 2 3 4 5 6 7 const router = new VueRouter ({ routes : [ {path : '/' , redirect : '/search' }, {path : '/search/:words?' , component : VSearch } ] })
现在, 直接访问网页的时候, 就会跳转到希望查看的页面了.
404 路由 如果路径找不到了, 我们肯定不能空在那里. 所以, 我们可以配置一个 404 页面. 这个配置是写在所有路由最后面的, 因为路由的匹配是从上到下的; 如果从上到下都没有找到, 则可以使用一个 404 的路由视图.
首先, 配置一下 404 的路由 (其实就是 *, 表示所有其他的未知路由)
1 2 3 4 5 6 7 8 const router = new VueRouter ({ routes : [ {path : '/' , redirect : '/search' }, {path : '/search/:words?' , component : VSearch }, {path : '*' , component : NotFound } ] })
我们现在尝试访问非法路径, 就会跳转到我们自定义的 404 页面了!
[!important] NotFound 是什么 是一个自己写的组件哦, 这里省略了, 不多说了
路由模式 默认的, 我们的路由是一个 # 后面再加上一些内容, 其实这样子是有一些不太自然的; 我们可以非常方便的, 给路由添加一个 mode 配置项, 来指定是使用默认的 Hash 类型, 还是 history 类型.
1 2 3 4 5 6 const router = new VueRouter ({ routes, mode : 'history' , })
下面是 history 的类型:
[!tip] 注意 我们唯一修改的就是 mode, 别的代码其实压根没变过. 唯一需要注意的就是记得告诉后端, 我们的更改
编程式路由 其实就是点击一个按钮进行跳转, 或者是几秒后自动跳转, 比如 404 页面在两秒后自动跳转走. 这里我们就可以使用编程式路由了.
path 跳转 编程式路由有两种跳转的方式, 一种是 path 路径跳转, 非常简单, 直接使用如下代码就可以跳转了:
1 2 3 4 5 this .$router .push ('路由路径' )this .$router .push ({ path : '路由路径' })
这里直接实现一个访问 404 页面, 3 秒后进行跳转的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <script> export default { name: "NotFound", data() { return { timer: 3, mainTimer: null } }, mounted() { // 在页面挂载后后进行跳转 this.mainTimer = setInterval(() => { this.timer--; // 判断时间是否到达 if (this.timer === 0) { // 进行路由跳转 this.$router.push('/search') } }, 1000) }, beforeDestroy() { clearInterval(this.mainTimer); } } </script> <template> <div> <h1>Kaede is Sorry But...</h1> <h4>There is nothing to see...</h4> <h5>Times: {{ timer }}</h5> </div> </template>
效果如下, 还是很正常的.
这就是编程式路由的第一种写法, 也是简单的写法.
name 跳转 第二种, 也就是基于 name, 命名路由的方式进行跳转. 路由其实是可以给名字的, 方便长路由的跳转. 首先来到路由规则的地方, 给一个名字:
1 2 3 4 5 6 7 8 9 const router = new VueRouter ({ routes : [ {path : '/' , redirect : '/search' }, {name : 'main' , path : '/search/:words?' , component : VSearch }, {path : '*' , component : NotFound } ], mode : 'history' })
随后, 在跳转的地方, 直接使用名字就好:
1 2 3 4 5 this .$router .push ({ name : 'main' })
路由传参 我们可能希望跳转的时候, 也携带一些参数过去, 进而跳转到我们想要的页面上面. 对于 path 跳转, 我觉得就不用多说了, 直接使用 push('/路径?参数=值&参数=值') 即可. 或者为了看起来更加舒服, 可以使用第二种传参方式, 不过带一个 query. 这里的 query 是一个对象, 其中的 key-value 对应的就是我们需要的参数名以及参数值.
例如下面这两种, 都可以实现传参:
1 2 3 4 5 6 7 8 9 this .$router .push ('/search?name=yyt' );this .$router .push ({ name : 'main' , query : { name : 'yyt' } })
这样就实现了跳转.
另外, 对于 name 命名路由的动态路由传参, 使用起来直接在路径直接写就行.
1 2 this .$router .push (`/search/${'yyt' } ` )
如果想写成对象的样子, 则需要使用 params 配置项即可实现效果.
1 2 3 4 5 6 7 8 9 this .$router .push ({ name : 'main' , params : { words : 'yyt' } })
[!note] 总结 动态路由 params, 查询路由 query
案例: 面经基础版 引入 这是一个类似于论坛的案例, 存在四个标签页, 并且其中的数据都是动态渲染的. 我们为了实现这种大一些的案例, 一般都是按照两个大的步骤走:
配置路由
实现功能
针对于这个案例, 我们可以看到, 整体上其实是两个, 一个是首页, 另外一个是面经详情. 这两个叫做一级路由; 其次, 下面的四个 Tag, 点击后可以切换到其他的页面, 这就是二级路由.
其中的功能实现, 首先我们需要在首页请求进行渲染, 随后跳转传参到详情页面, 详情页渲染, 最后再做组件的缓存, 进而优化性能.
这里我们直接使用已经准备好的素材库进行开发:
配置路由 查看页面, 我们可以看到这些页面:
一级路由 高亮的两个部分, 就是一级路由了. Layout 中还有四个子页面, 也就是二级路由. 我们开始进行路由的配置. 来到 router 的 index.js 中, 进行一级路由的配置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const router = new VueRouter ({ routes : [ { path : '/' , component : Layout }, { path : '/detail' , component : ArticleDetail } ] })
现在, 主页和详情页面直接访问都是可以查看的了.
首页
详情页
二级路由 接下来, 我们的首页中还有一些基础的导航, 其实就是首页的二级路由了. 我们在首页中, 添加一个配置项: children.
这里的 children 也是一个数组+对象, 直接写就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const router = new VueRouter ({ routes : [ { path : '/' , component : Layout , children : [ { path : '/article' , component : Article }, { path : '/collect' , component : Collect , }, { path : '/like' , component : Like , }, { path : '/user' , component : User } ] }, { path : '/detail' , component : ArticleDetail } ] })
另外, 配置好了路由, 还需要路由出口, 否则页面不会正常显示. 来到首页组件中, 我们把写死的内容部分, 改成一个路由出口:
1 2 3 <div class ="content" > <router-view > </router-view > </div >
现在, 匹配到的二级路由就会在这个路由出口当中访问了.
路由高亮 现在我们的路由点击以后是没有高亮效果的, 我们希望能够有高亮效果, 这里就徐娅使用到 router-link 组件了. 来到 Layout 组件, 将 a 标签进行修改: (href 改为 to 否则用不了)
1 2 3 4 5 6 <nav class="tabbar"> <router-link to="/article">面经</router-link> <router-link to="/collect">收藏</router-link> <router-link to="/like">喜欢</router-link> <router-link to="/user">我的</router-link> </nav>
随后, 我们可以添加一个高亮效果.
1 2 3 4 a .router-link-active { color : orange; }
现在页面中, 点击页面后已经有高亮效果了.
首页请求渲染 首页的东西目前是写死的, 我们当然希望是可以动态渲染的, 所以我们来到 Article 中进行修改. 我们需要进行请求, 自然需要用到 axios, 所以我们需要进行安装: yarn add axios, 随后阅读接口文档, 确认如何请求.
[!tip] 接口失效了 下面我使用静态的伪造数据即可, 不然太麻烦了, 因为数据接口可能随时爆掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 data ( ) { return { articles : [ { "id" : 1 , "content" : "这是一个测试内容1" , "createdAt" : "2022-01-20" , "creatorName" : "Kaede" , "difficulty" : 1 , "likeCount" : 45 , "likeFlag" : 0 , "stem" : "百度前端" , "views" : 315 , "subject" : "前端与移动开发" , "questionBankType" : 9 , "questionNo" : "mj27483" }, { "id" : 2 , "content" : "Vue3响应式原理深度解析" , "createdAt" : "2023-05-12" , "creatorName" : "Alice_Wang" , "difficulty" : 2 , "likeCount" : 189 , "likeFlag" : 1 , "stem" : "Vue框架" , "views" : 1280 , "subject" : "前端与移动开发" , "questionBankType" : 7 , "questionNo" : "fe20456" }, { "id" : 3 , "content" : "React性能优化实战案例" , "createdAt" : "2023-09-28" , "creatorName" : "Bob_Li" , "difficulty" : 3 , "likeCount" : 325 , "likeFlag" : 1 , "stem" : "React性能优化" , "views" : 2240 , "subject" : "前端与移动开发" , "questionBankType" : 5 , "questionNo" : "rc19873" }, { "id" : 4 , "content" : "移动端适配方案对比(REM vs Viewport)" , "createdAt" : "2022-11-05" , "creatorName" : "Luna_Zhang" , "difficulty" : 2 , "likeCount" : 98 , "likeFlag" : 0 , "stem" : "移动端适配" , "views" : 840 , "subject" : "前端与移动开发" , "questionBankType" : 8 , "questionNo" : "md34721" }, { "id" : 5 , "content" : "Node.js中间件开发入门示例" , "createdAt" : "2024-02-18" , "creatorName" : "Eve_Chen" , "difficulty" : 2 , "likeCount" : 156 , "likeFlag" : 1 , "stem" : "Node.js中间件" , "views" : 1020 , "subject" : "前端与移动开发" , "questionBankType" : 6 , "questionNo" : "nj45239" }, { "id" : 6 , "content" : "CSS动画性能优化技巧总结" , "createdAt" : "2022-07-25" , "creatorName" : "Frank_Wu" , "difficulty" : 1 , "likeCount" : 62 , "likeFlag" : 0 , "stem" : "CSS动画优化" , "views" : 480 , "subject" : "前端与移动开发" , "questionBankType" : 9 , "questionNo" : "cs87612" }, { "id" : 7 , "content" : "微信小程序路由传参最佳实践" , "createdAt" : "2023-03-09" , "creatorName" : "Grace_Liu" , "difficulty" : 2 , "likeCount" : 210 , "likeFlag" : 1 , "stem" : "微信小程序路由" , "views" : 1450 , "subject" : "前端与移动开发" , "questionBankType" : 7 , "questionNo" : "xc56328" }, { "id" : 8 , "content" : "TypeScript泛型使用场景解析" , "createdAt" : "2024-04-06" , "creatorName" : "Henry_Zhou" , "difficulty" : 3 , "likeCount" : 288 , "likeFlag" : 1 , "stem" : "TypeScript泛型" , "views" : 1920 , "subject" : "前端与移动开发" , "questionBankType" : 5 , "questionNo" : "ts78901" }, { "id" : 9 , "content" : "跨浏览器兼容性问题解决方案" , "createdAt" : "2022-04-15" , "creatorName" : "Ivy_Sun" , "difficulty" : 1 , "likeCount" : 35 , "likeFlag" : 0 , "stem" : "浏览器兼容性" , "views" : 270 , "subject" : "前端与移动开发" , "questionBankType" : 9 , "questionNo" : "cb56743" }, { "id" : 10 , "content" : "Webpack5配置优化实战指南" , "createdAt" : "2023-12-20" , "creatorName" : "Jack_Yang" , "difficulty" : 3 , "likeCount" : 360 , "likeFlag" : 1 , "stem" : "Webpack配置优化" , "views" : 2400 , "subject" : "前端与移动开发" , "questionBankType" : 5 , "questionNo" : "wp32145" } ] } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="article-page"> <!--需要循环渲染--> <div class="article-item" v-for="item in articles" :key="item.id"> <div class="head"> <img src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png" alt=""/> <div class="con"> <p class="title">{{ item.stem }}</p> <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p> </div> </div> <div class="body">{{ item.content }}</div> <div class="foot">点赞 {{ item.likeCount }} | 浏览 {{ item.views }}</div> </div> </div> </template>
现在页面中已经可以正常的渲染模拟数据了!
详情页面跳转 点击不同的面经的时候, 是需要进行传参的, 我们有两种传参方式: 查询参数以及动态路由. 我们给文章注册一下点击事件:
1 2 3 4 5 6 7 <div class ="article-item" v-for ="item in articles" :key ="item.id" @click ="$router.push(`/detail?id=${item.id}`)" > </div >
这里传递一个 id 就好, 我们在详情页接收 id 参数, 并且读取数据, 根据 ID 进行渲染即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <template> <div class="article-detail-page"> <nav class="nav"><span class="back"><</span> 面经详情</nav> <header class="header"> <h1>{{ article.stem }}</h1> <p>{{ article.createdAt }} | {{ article.views }} 浏览量 | {{ article.likeCount }} 点赞数</p> <p> <img src="http://teachoss.itheima.net/heimaQuestionMiniapp/%E5%AE%98%E6%96%B9%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F%402x.png" alt="" /> <span>{{ article.creatorName }}</span> </p> </header> <main class="body">{{ article.content }}</main> </div> </template> <script> // 引入虚拟数据 import articles from "@/mock/articles"; export default { name: "ArticleDetailPage", data() { return { article: null } }, created() { const id = this.$route.query.id; // 遍历 找到目标文章 for (let i = 0; i < articles.length; i++) { if (articles[i].id === id) { this.article = articles[i]; break; } } console.log(this.article); } } </script>
现在点击不同的文章, 已经可以看到不同的页面了.
返回按钮 我们的返回按钮就是一个简单的 span, 不妨写一下, 回到主页的逻辑:
1 <nav class="nav"><span @click="$router.back()" class="back"><</span> 面经详情</nav>
这里使用到了一个路由的方法: $router.back(), 这样就可以实现返回的效果了.
Vuex 引入 Vuex 是一个 Vue 的状态管理工具 (插件). Vuex 可以帮助我们管理 Vue 的通用数据 , 多组件共享的数据. 比如一个购物车, 商品的数据, 在顶部, 提交, 以及其他的页面都可能使用到, 这个时候就可以使用 Vuex 了.
Vuex 的使用场景如下:
某个状态在很多个组件中来使用的时候 (个人信息)
多个组件共同维护一份数据 (购物车)
如果没有 Vuex, 我们需要用的话, 就需要不断地进行传递才能使用, 效率太低; 不如直接使用一个 store 进行状态管理, 把数据放在仓库中, 别的地方直接从仓库拿数据就行. 这就实现了 数据集中化 .
另外, 这些数据都是响应式的! 只要数据变化了, 其他用到这个数据的地方也会同步更新!
准备基础环境 我们如果需要构建 Vuex 的共享数据环境, 可以直接使用脚手架进行搭建. 使用 vue create vuex-demo, 创建一个新的项目就好, 这里选择自定义.
虽然是学习 Vuex, 不过这里不需要安装. 剩下的正常选择即可. 随后清理文件, 我们创建两个新的组件: KSon1 和 KSon2, 页面如下:
创建空仓库 既然是多组件共享数据, 数据必然要一个地方进行存放, 这个地方就叫做仓库. 所以我们需要先初始化一个仓库出来. 另外需要使用 Vuex, 所以需要安装 Vuex.
[!note] 注意版本 Vue2 的路由以及 Vuex 的版本都是 3
随后, 我们需要创建 vuex 的模块文件. 在 src 目录下创建一个新的 store 目录, 以及 index.js.
接下来, 我们需要创建仓库. 直接在 store 的 index.js 中进行配置即可:
1 2 3 4 5 6 7 8 9 10 11 12 import Vue from 'vue' import Vuex from 'vuex' Vue .use (Vuex )const store = new Vuex .Store ()export default store
接下来, 在 main.js 导入并且挂载这个仓库就好.
1 2 3 4 5 6 7 8 9 10 import Vue from 'vue' import App from './App.vue' import store from '@/store' Vue .config .productionTip = false new Vue ({ render : h => h (App ), store }).$mount('#app' )
至此, 就完成空仓库的创建了.
我们尝试在组件中输出一下这个仓库试试:
1 console .log (this .$store )
可以看到, 仓库已经创建成功了! (如果没有成功则无法访问这个东西)
☆ state 状态 定义 state 我们现在想要给这个仓库提供数据, 并且使用数据. 这里就需要提到一个东西了: state 对象, 我们在这个对象里面写需要共享的数据即可.
1 2 3 4 5 6 const store = new Vuex .Store ({ state : { count : 0 } })
获取数据 如果需要获取数据, 还是需要先导入仓库; 随后可以通过 $store.state.xxx 来访问数据. 例如在组件 Son 1 中, 就可以使用如下语法获取 state 中的值.
1 2 3 4 5 6 7 8 <template> <div> <h2>Son 1组件</h2> 从Vuex中获取的值: <label>{{ $store.state.count }}</label> <br> <button>值 + 1</button> </div> </template>
不需要导入任何东西, 直接使用即可.
mapState 例如刚才的 count, 我们如果需要进行渲染, 则需要使用 $store.state.count, 这样写还是过于麻烦了, 为什么不想办法让它直接变成 count 呢? 这里就可以使用这个辅助函数来实现了.
1 2 3 4 5 6 7 8 9 10 <script> import { mapState } from 'vuex' export default { computed: mapState({ // 使用箭头函数 更快速 count: state => state.count }) } </script>
现在就可以直接使用 count 这个属性了.
1 2 3 <h2 > Son 1组件</h2 > 从Vuex中获取的值: <label > {{ count }}</label > <br >
[!tip] 总结 mapState 可以将 store.state 中的内容映射到 this 身上, 进而方便使用
☆ mutations 修改数据 什么是 mutations 为了修改数据, 这里需要使用到一个新的东西: mutations. 因为 Vuex 也是单向数据流, 我们不能直接修改仓库中的数据. mutations 其实是一个对象, 定义在 store 中, 与 state 平级. 对象里面可以提供一些方法, 这些方法都是用来修改 state 的方法.
基础使用 直接定义在 state 平级的地方, 按照配置项进行传递即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const store = new Vuex .Store ({ state : { count : 0 }, mutations : { addCount (state) { state.count ++ } } })
随后, 在组件中使用提交的方式, 调用这个方法即可: this.$store.commit('方法名称')
1 2 3 4 5 6 7 8 9 <template > <div > <h2 > Son 1组件</h2 > 从Vuex中获取的值: <label > {{ $store.state.count }}</label > <br > <button @click ="$store.commit('addCount')" > 值 + 1</button > </div > </template >
现在的加法已经可以正常运行了.
同理, 减法也实现, 并且调用对应的函数即可.
mutations 传参 刚才的数字变化都是写死的, 加一或者减一, 但是这样的效率不够高, 我们可以通过参数来判断需要加多少, 或者是减多少. 传参语法, 就是在 commit 的时候, 传递第二个参数作为调用函数的参数; 随后在 mutations 中, 第二个参数就是接收传递的内容.
比如我修改一下刚才的修改 count 的函数, 实现一个统一的方法: setCount, 模仿 React 的语法.
1 2 3 4 5 6 mutations : { setCount (state, newValue) { state.count = newValue } }
接下来, 增加就可以写成这样子:
1 <button @click ="$store.commit('setCount', $store.state.count + 1)" > 值 + 1</button >
至此, 效果顺利实现, 和之前没有任何区别, 并且使用起来更加方便, 不需要定义更多的函数了.
另外, 上面的输入框也可以调用这个方法, 进而修改数据:
1 <input type ="number" :value ="$store.state.count" @change ="handleChange" />
方法中, 获取改变的值, 并且再次进行提交即可.
1 2 3 4 5 6 7 methods : { handleChange (e) { this .$store .commit ('setCount' , Number (e.target .value )) } }
至此, 效果顺利实现.
mapMutations 它和 mapState 很像, 但是它可以将 mutations 中的方法提取出来, 映射在 methods 中. 我们直接使用展开运算符即可:
1 2 3 4 methods : { ...mapMutations (['setCount' ]) }
下面使用的时候, 直接当作一个函数进行调用即可, 非常方便.
1 <button @click ="setCount(count + 1)" > 值 + 1</button >
☆ actions 操作 什么是 actions 我们如果需要发送请求, 或者是执行一些异步操作, 则需要使用 actions. 因为 mutations 是不能使用异步的, 只能同步. 这里就需要使用到 actions 了.
另外, 如果需要调用 actions, 则和 React 一样, 需要使用 dispatch 方法来调用. 例如还是刚才的案例, 但是我希望点击按钮后 1 秒再增加.
基础使用 首先, 提供 actions 方法:
1 2 3 4 5 6 7 8 9 10 11 actions : { changeCountAction (context, num) { setTimeout (() => { context.commit ('setCount' , num) }, 1000 ) } }
接下来使用的时候, 使用 dispatch 进行函数的调用.
1 <button @click ="$store.dispatch('changeCountAction', count + 5)" > 1秒后增加5</button >
查看页面, 一秒后增加已经实现,.
mapActions 同理, 既然前面的都有对应的 map 辅助函数, 那么 actions 也是有的, 这里的映射会将 actions 中的方法映射在 methods 中, 和 mapMutations 是一样的.
1 2 3 4 5 6 7 methods : { ...mapMutations (['setCount' ]), ...mapActions (['changeCountAction' ]) }
调用的时候直接调用函数就好, 不需要再 dispatch 了.
1 <button @click ="changeCountAction(count + 10)" > 调用解构的actions +10</button >
使用效率得到了显著提升.
☆ getters getters 是什么 除了 state 之外, 我们还需要从 state 中派生出一些状态, 这些状态是依赖 state 的, 这个时候就会用到 getters 了. 例如, 我现在有一个数组, 但是我想要得到大于等于 10 的数据数组.
getters 的定义也是和 state 并列的, 其中存放很多的函数, 函数名就是数据名, 传入一个 state 参数. 另外这个函数必须有返回值, 和计算属性很像.
使用的时候, 可以通过辅助函数访问, 或者是使用 store 直接访问.
基础使用 我们提供给一个数组, 随后再 getters 配置项中, 按照想法来处理数据:
1 2 3 4 5 6 7 8 9 10 getters : { filterList (state) { return state.list .filter (item => { return item >= 5 }) } }
随后, 可以直接在组件中进行使用:
1 <div > 数组数据为: {{ $store.getters.filterList }}</div >
渲染出来的就是处理后的数据了:
mapGetters 也是一样的, 可以将数据进行解构, 方便使用. 在该组件的 computed 中, 进行解构:
1 2 3 4 5 6 7 8 9 10 11 <script> // 引入 import { mapGetters } from 'vuex' export default { computed: { // 解构 ...mapGetters(['filterList']) } } </script>
随后, 使用的时候直接使用这个值就可以了.
1 <div > 数组数据为: {{ filterList }}</div >
效果正常显示.
Vuex 分模块 为什么要分模块 因为 Vuex 使用的是单一状态树, 如果所有的状态都写在一个文件中, 会形成一个非常大的对象. 当应用程序变得非常复杂的时候, store 对象就会变得相当臃肿, 难以维护.
[!tip] 单一状态树 就是所有东西都写在一个对象中
创建模块 我们的用户模块, 可以单独写在 user 模块中, 其他的同理. 这里我们就可以将刚才的 count 部分拆分到一个单独的模块中. 为了拆分模块, 我们需要创建一个新的文件夹: modules/xxx.js. 其中, 我们创建对应的 state, mutations, actions 以及 getters 部分, 最后默认导出所有即可.
这里创建一个 count.js, 作为一个模块, 其中可以写一些基础代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const state = {}const mutations = {}const actions = {}const getters = {}export default { state, mutations, actions, getters }
随后, 我们将原来 main.js 中的内容放进来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const state = { count : 0 } const mutations = { setCount (state, newValue) { state.count = newValue } } const actions = { changeCountAction (context, num) { setTimeout (() => { context.commit ('setCount' , num) }, 1000 ) } } const getters = {} export default { state, mutations, actions, getters }
使用模块中的数据 模块化后, 其实我们的数据使用就多了一层壳, 防止出现数据的混乱. 我们还是有两种方式来获取, 第一种就是 $store.state.模块名.数据, 以及对应的 getters 以及 mutations. 例如下面这样子:
1 2 3 <h2 > Son 1组件</h2 > 从Vuex中获取的值: <label > {{ $store.state.count.count }}</label > <br >
如果需要使用 mapXXX 的方式, 则需要声明命名空间. 首先需要在导出的时候, 加上一个 namespaced:
1 2 3 4 5 6 7 export default { namespaced : true , state, mutations, actions, getters }
随后, 就可以使用子模块的映射了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script> import { mapState, mapMutations, mapActions } from 'vuex' export default { computed: { // 进行子模块的映射 ...mapState('count', ['count']) }, methods: { // 使用解构 解析在methods中进而使用 ...mapMutations('count', ['setCount']), // 解构actions ...mapActions('count', ['changeCountAction']) } } </script>
随后, 就可以正常进行使用了.