day04 1. 学习目标 1.1 组件的三大组成部分(结构/样式/逻辑) scoped解决样式冲突/data是一个函数
1.2 组件通信
组件通信语法
父传子
子传父
非父子通信(扩展)
1.3 综合案例:小黑记事本(组件版)
拆分组件
列表渲染
数据添加
数据删除
列表统计
清空
持久化
1.4 进阶语法
v-model原理
v-model应用于组件
sync修饰符
ref和$refs
$nextTick
2. scoped解决样式冲突 2.1 默认情况: 写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
全局样式 : 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
局部样式 : 可以给组件加上scoped 属性,可以让样式只作用于当前组件
2.2 代码演示 BaseOne.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <div class="base-one"> BaseOne </div> </template> <script> export default { } </script> <style scoped> </style>
BaseTwo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div class="base-one"> BaseTwo </div> </template> <script> export default { } </script> <style scoped> </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div id="app"> <BaseOne></BaseOne> <BaseTwo></BaseTwo> </div> </template> <script> import BaseOne from './components/BaseOne' import BaseTwo from './components/BaseTwo' export default { name: 'App', components: { BaseOne, BaseTwo } } </script>
2.3 scoped原理
当前组件内标签都被添加data-v-hash值 的属性
css选择器都被添加 [data-v-hash值 ] 的属性选择器
最终效果: 必须是当前组件的元素 , 才会有这个自定义属性, 才会被这个样式作用到
2.4 总结
style的默认样式是作用到哪里的?
scoped的作用是什么?
style中推不推荐加scoped?
3. data必须是一个函数 3.1 data为什么要写成函数 一个组件的 data 选项必须是一个函数 。目的是为了:保证每个组件实例,维护独立 的一份数据 对象。
每次创建新的组件实例,都会新执行一次data 函数 ,得到一个新对象。
3.2 代码演示 BaseCount.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="base-count"> <button @click="count--">-</button> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template> <script> export default { data: function () { return { count: 100, } }, } </script> <style> .base-count { margin: 20px; } </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="app"> <BaseCount></BaseCount> </div> </template> <script> import BaseCount from './components/BaseCount' export default { components: { BaseCount, }, } </script> <style> </style>
3.3 总结 data写成函数的目的是什么?
4. 组件通信 4.1 什么是组件通信? 组件通信,就是指组件与组件 之间的数据传递
组件的数据是独立的,无法直接访问其他组件的数据。
想使用其他组件的数据,就需要组件通信
4.2 组件之间如何通信
思考:
组件之间有哪些关系?
对应的组件通信方案有哪几类?
4.3 组件关系分类
父子关系
非父子关系
4.4 通信解决方案
4.5 父子通信流程
父组件通过 props 将数据传递给子组件
子组件利用 $emit 通知父组件修改更新
4.6 父向子通信代码示例 父组件通过props 将数据传递给子组件
父组件App.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 <template> <div class="app" style="border: 3px solid #000; margin: 10px"> 我是APP组件 <Son></Son> </div> </template> <script> import Son from './components/Son.vue' export default { name: 'App', data() { return { myTitle: '学前端,就来黑马程序员', } }, components: { Son, }, } </script> <style> </style>
子组件Son.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div class="son" style="border:3px solid #000;margin:10px"> 我是Son组件 </div> </template> <script> export default { name: 'Son-Child', } </script> <style> </style>
父向子传值步骤
给子组件以添加属性的方式传值
子组件内部通过props接收
模板中直接使用 props接收的值
4.7 子向父通信代码示例 子组件利用 $emit 通知父组件,进行修改更新
子向父传值步骤
$emit触发事件,给父组件发送消息通知
父组件监听$emit触发的事件
提供处理函数,在函数的性参中获取传过来的参数
4.8 总结
组件关系分类有哪两种
父子组件通信的流程是什么?
父向子
子向父
5. Props 5.1 什么是props
Props 定义
组件上 注册的一些 自定义属性
Props 作用
向子组件传递数据
特点
可以 传递 任意数量 的prop
可以 传递 任意类型 的prop
4.代码演示
父组件App.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 33 34 <template> <div class="app"> <UserInfo :username="username" :age="age" :isSingle="isSingle" :car="car" :hobby="hobby" ></UserInfo> </div> </template> <script> import UserInfo from './components/UserInfo.vue' export default { data() { return { username: '小帅', age: 28, isSingle: true, car: { brand: '宝马', }, hobby: ['篮球', '足球', '羽毛球'], } }, components: { UserInfo, }, } </script> <style> </style>
子组件UserInfo.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 <template> <div class="userinfo"> <h3>我是个人信息组件</h3> <div>姓名:</div> <div>年龄:</div> <div>是否单身:</div> <div>座驾:</div> <div>兴趣爱好:</div> </div> </template> <script> export default { } </script> <style> .userinfo { width: 300px; border: 3px solid #000; padding: 20px; } .userinfo > div { margin: 20px 10px; } </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="userinfo"> <h3>我是个人信息组件</h3> <div>姓名:{{username}} </div> <div>年龄:{{age}} </div> <div>是否单身: {{isSingle ? '是' : '否'}} </div> <div>座驾:{{car.brand}} </div> <div>兴趣爱好:{{hobby.join('、')}} </div> </div> </template> <script> export default { props: ['username', 'age', 'isSingle', 'car', 'hobby'] } </script>
5.2 props校验 1️⃣思考
组件的props可以乱传吗
2️⃣作用
为组件的 prop 指定验证要求 ,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
3️⃣语法
4️⃣代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="app"> <BaseProgress :w="width"></BaseProgress> </div> </template> <script> import BaseProgress from './components/BaseProgress.vue' export default { data() { return { width: 30, } }, components: { BaseProgress, }, } </script> <style> </style>
BaseProgress.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 33 34 35 36 37 38 39 <template> <div class="base-progress"> <div class="inner" :style="{ width: w + '%' }"> <span>{{ w }}%</span> </div> </div> </template> <script> export default { props: ['w'], } </script> <style scoped> .base-progress { height: 26px; width: 400px; border-radius: 15px; background-color: #272425; border: 3px solid #272425; box-sizing: border-box; margin-bottom: 30px; } .inner { position: relative; background: #379bff; border-radius: 15px; height: 25px; box-sizing: border-box; left: -3px; top: -2px; } .inner span { position: absolute; right: 0; top: 26px; } </style>
1 2 3 4 5 6 export default { props : { w : Number } }
5.3 props校验完整写法 1️⃣语法
1 2 3 4 5 6 7 8 9 10 11 props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
2️⃣代码实例
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: { w: { type: Number, //required: true, default: 0, validator(val) { // console.log(val) if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, }, } </script>
3️⃣注意
default和required一般不同时写(因为当时必填项时,肯定是有值的)
default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
5.4 props&data、单向数据流 1️⃣共同点
都可以给组件提供数据
2️⃣区别
data 的数据是自己 的 → 随便改
prop 的数据是外部 的 → 不能直接改,要遵循 单向数据流
3️⃣单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
4️⃣代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div class="app"> <BaseCount></BaseCount> </div> </template> <script> import BaseCount from './components/BaseCount.vue' export default { components:{ BaseCount }, data(){ }, } </script> <style> </style>
BaseCount.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 <template> <div class="base-count"> <button @click="count--">-</button> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template> <script> export default { // 1.自己的数据随便修改 (谁的数据 谁负责) data () { return { count: 100, } }, // 2.外部传过来的数据 不能随便修改 //props: { // count: { // type: Number, // }, //} } </script> <style> .base-count { margin: 20px; } </style>
5️⃣口诀
谁的数据谁负责
6. 综合案例 6.1 综合案例-组件拆分 1️⃣需求说明
拆分基础组件
渲染待办任务
添加任务
删除任务
底部合计 和 清空功能
持久化存储
2️⃣拆分基础组件
咱们可以把小黑记事本原有的结构拆成三部分内容:头部(TodoHeader)、列表(TodoMain)、底部(TodoFooter)
6.2 综合案例-列表渲染 思路分析:
提供数据:提供在公共的父组件 App.vue
通过父传子,将数据传递给TodoMain
利用v-for进行渲染
6.3 综合案例-添加功能 思路分析:
收集表单数据 v-model
监听时间 (回车+点击 都要进行添加)
子传父,将任务名称传递给父组件App.vue
父组件接受到数据后 进行添加 unshift (自己的数据自己负责)
6.4 综合案例-删除功能 思路分析:
监听时间(监听删除的点击)携带id
子传父,将删除的id传递给父组件App.vue
进行删除 filter (自己的数据自己负责)
6.5 综合案例-底部功能及持久化存储 思路分析:
底部合计:父组件传递list到底部组件 —>展示合计
清空功能:监听事件 —> 子组件 通知父组件 —>父组件清空
持久化存储:watch监听数据变化,持久化到本地
6.6 完整代码 App.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 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 <template> <!-- 主体区域 --> <section id="app"> <TodoHeader @add="handleAdd"></TodoHeader> <TodoMain @del="handleDel" :list="list"></TodoMain> <TodoFooter @clear="clear" :list="list"></TodoFooter> </section> </template> <script> import TodoHeader from './components/TodoHeader.vue' import TodoMain from './components/TodoMain.vue' import TodoFooter from './components/TodoFooter.vue' // 渲染功能 // 1. 提供数据 -> 提供在公共的父组件 App.vue // 2. 通过父传子,将数据传递给 TodoMain // 3. 利用v-for 渲染 // 添加功能 // 1. 收集表单数据 -> v-model // 2. 监听事件(回车 + 点击 都要进行添加) // 3. 子传父,将任务名称传递给父组件App.vue // 4. 进行添加 unshift (自己的数据自己负责) // 删除功能 // 1. 监听事件(监听删除的点击) 携带id // 2. 子传父,将删除的id传递给父组件App.vue // 3. 进行删除 filter(自己的数据自己负责) // 底部合计 // 1. 父传子传list -> 渲染 // 清空功能:子传父 通知到父组件 -> 父组件进行清空 // 持久化存储:watch 深度监视list的变化 -> 往本地存储 -> 一进入页面优先读取本地 export default { data () { return { list: JSON.parse(localStorage.getItem('list')) || [ { id: 1, name: '打篮球' }, { id: 2, name: '看电影' }, { id: 3, name: '逛街' } ] } }, methods: { handleAdd(todoName) { this.list.unshift({ id: +new Date(), name: todoName }) }, handleDel(id) { this.list = this.list.filter(item => item.id !== id) }, clear() { this.list = [] } }, watch: { list: { deep: true, handler(newValue) { localStorage.setItem('list', JSON.stringify(newValue)) } } }, components: { TodoHeader, TodoMain, TodoFooter } } </script> <style> </style>
TodoHeader.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 33 <template> <!-- 输入框 --> <header class="header"> <h1>小黑记事本</h1> <input @keyup.enter="handleAdd" v-model="todoName" placeholder="请输入任务" class="new-todo" /> <button @click="handleAdd" class="add">添加任务</button> </header> </template> <script> export default { data () { return { todoName: '' } }, methods: { handleAdd() { if(this.todoName.trim() === '') { alert('任务名称不能为空') return } this.$emit('add', this.todoName) this.todoName = '' } } }; </script> <style> </style>
TodoMain.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 <template> <!-- 列表区域 --> <section class="main"> <ul class="todo-list"> <li v-for="(item, index) in list" :key="item.id" class="todo"> <div class="view"> <span class="index"> {{index + 1}}.</span> <label>{{item.name}}</label> <button @click="handleDel(item.id)" class="destroy"></button> </div> </li> </ul> </section> </template> <script> export default { props: { list: Array }, methods: { handleDel(id) { this.$emit('del', id) } } }; </script> <style> </style>
TodoFooter.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 <template> <!-- 统计和清空 --> <footer class="footer"> <!-- 统计 --> <span class="todo-count">合 计:<strong> {{list.length}} </strong></span> <!-- 清空 --> <button @click="clear" class="clear-completed">清空任务</button> </footer> </template> <script> export default { props: { list: Array }, methods: { clear() { this.$emit('clear') } } }; </script> <style> </style>
7. 非父子通信 7.1 非父子通信-event bus 事件总线 1️⃣作用
非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
2️⃣步骤
创建一个都能访问的事件总线 (空Vue实例)
1 2 3 import Vue from 'vue' const Bus = new Vue ()export default Bus
A组件(接受方),监听Bus的 $on事件
1 2 3 4 5 created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) }
B组件(发送方),触发Bus的$emit事件
1 Bus.$emit('sendMsg', '这是一个消息')
3️⃣代码示例
EventBus.js
1 2 3 import Vue from 'vue' const Bus = new Vue ()export default Bus
BaseA.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 33 <template> <div class="base-a"> 我是A组件(接收方) <p>{{msg}}</p> </div> </template> <script> import Bus from '../utils/EventBus' export default { data() { return { msg: '', } }, created () { // 2. 在 A 组件(接收方),进行监听Bus的事件(订阅消息) Bus.$on('sendMsg', (msg) => { this.msg = msg }) } } </script> <style scoped> .base-a { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
BaseB.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 <template> <div class="base-b"> <div>我是B组件(发布方)</div> <button @click="clickSend" >发送消息</button> </div> </template> <script> import Bus from '../utils/EventBus' export default { methods: { clickSend() { // 3. B 组件(发送方)触发事件的方式传递参数(发布消息) Bus.$emit('sendMsg', '今日下雨,不宜出行') } } } </script> <style scoped> .base-b { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div class="app"> <BaseA></BaseA> <BaseB></BaseB> </div> </template> <script> import BaseA from './components/BaseA.vue' import BaseB from './components/BaseB.vue' export default { components:{ BaseA, BaseB } } </script> <style> </style>
4️⃣总结
非父子组件传值借助什么?
什么是事件总线
发送方应该调用事件总线的哪个方法
接收方应该调用事件总线的哪个方法
一个组件发送数据,可不可以被多个组件接收
7.2 非父子通信-provide&inject 1️⃣作用
跨层级共享数据
2️⃣场景
3️⃣语法
父组件 provide提供数据
1 2 3 4 5 6 7 8 9 10 export default { provide () { return { color : this .color , userInfo : this .userInfo , } } }
子/孙组件 inject获取数据
1 2 3 4 5 6 export default { inject : ['color' ,'userInfo' ], created () { console .log (this .color , this .userInfo ) } }
4️⃣注意
provide提供的==简单类型的数据不是响应式==的,==复杂类型数据是响应式==。(推荐提供复杂类型数据)
子/孙组件通过inject获取的数据,不能在自身组件内修改
8. v-model 原理及简化代码 8.1 v-model 原理 1️⃣原理:
v-model本质上是一个语法糖 。例如应用在输入框上,就是==value属性 和 input事件==的合写
1 2 3 4 5 6 7 8 <template> <div id="app" > <input v-model="msg" type="text"> <input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
2️⃣作用:
提供数据的双向绑定
数据变,视图跟着变 :value
视图变,数据跟着变 @input
3️⃣注意
$event
用于在模板中,获取事件的形参
4️⃣代码示例
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 <template> <div class="app"> <BaseSelect :cityId = 'selectId' @changeId="selectId = $event" ></BaseSelect> </div> </template> <script> import BaseSelect from './components/BaseSelect.vue' export default { data() { return { selectId: '102' } }, components: { BaseSelect, }, } </script> <style> </style>
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 <template> <div> <select :value="cityId" @change="handleChange"> <option value="101">北京</option> <option value="102">上海</option> <option value="103">武汉</option> <option value="104">广州</option> <option value="105">深圳</option> </select> </div> </template> <script> export default { props: { cityId: String }, methods: { handleChange(e) { this.$emit('changeId', e.target.value) } } } </script> <style> </style>
5️⃣v-model使用在其他表单元素上的原理
不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model
底层处理的是 checked属性和change事件。
不过咱们只需要掌握应用在文本框上的原理即可
8.2 v-model简化代码 1️⃣目标:
父组件通过v-model 简化代码 ,实现子组件和父组件数据 双向绑定
2️⃣如何简化:
v-model其实就是 :value和@input事件的简写
子组件:props通过value接收数据,事件触发 input
父组件:v-model直接绑定数据
3️⃣代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 <select :value="value" @change="handleChange">...</select> <script> export default { props: { value: String, }, methods: { handleChange(e) { this.$emit("input", e.target.value); } } } </script>
1 <BaseSelect v-model="selectId"></BaseSelect>
8.3 总结 1️⃣表单类基础组件封装思路
① 父传子:父组件动态传递 prop 数据,拆解v-model,绑定数据
② 子传父:监听输入,子传父传值给父组件修改
本质:实现了实现 子组件 和 父组件数据 的双向绑定
2️⃣v-model 简化代码的核心步骤
① 子组件中:props 通过 value 接收,事件触发 input
② 父组件中: v-model 给组件直接绑数据
3️⃣小作业:封装输入框组件,利用v-model简化代码
9. 表单类组件封装 9.1 需求目标 实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
9.2 代码演示 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="app"> <BaseSelect></BaseSelect> </div> </template> <script> import BaseSelect from './components/BaseSelect.vue' export default { data() { return { selectId: '102', } }, components: { BaseSelect, }, } </script> <style> </style>
BaseSelect.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template> <div> <select> <option value="101">北京</option> <option value="102">上海</option> <option value="103">武汉</option> <option value="104">广州</option> <option value="105">深圳</option> </select> </div> </template> <script> export default { } </script> <style> </style>
10. sync修饰符 10.1 作用 可以实现 子组件 与 父组件数据 的 双向绑定 ,简化代码
简单理解:子组件可以修改父组件传过来的props值
10.2 场景 封装弹框类的基础组件, visible属性 true显示 false隐藏
10.3 本质 .sync修饰符 就是 :属性名 和 @update:属性名 合写
10.4 语法 父组件
1 2 3 4 5 6 7 8 //.sync写法 <BaseDialog :visible.sync="isShow" /> -------------------------------------- //完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
子组件
1 2 3 4 5 props: { visible: Boolean }, this.$emit('update:visible', false)
10.5 代码示例 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 <template> <div class="app"> <button @click="openDialog">退出按钮</button> <BaseDialog :isShow.sync="isShow"></BaseDialog> </div> </template> <script> import BaseDialog from './components/BaseDialog.vue' export default { data() { return { isShow: false, } }, components: { BaseDialog, }, methods: { openDialog() { this.isShow = true } } } </script> <style> </style>
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 <template> <div class="base-dialog-wrap" v-show="isShow"> <div class="base-dialog"> <div class="title"> <h3>温馨提示:</h3> <button @click="close" class="close">x</button> </div> <div class="content"> <p>你确认要退出本系统么?</p> </div> <div class="footer"> <button @click="close">确认</button> <button @click="close">取消</button> </div> </div> </div> </template> <script> export default { props: { isShow: Boolean, }, methods: { close() { this.$emit('update:isShow', false) } } } </script> <style scoped> .base-dialog-wrap { width: 300px; height: 200px; box-shadow: 2px 2px 2px 2px #ccc; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 0 10px; } .base-dialog .title { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #000; } .base-dialog .content { margin-top: 38px; } .base-dialog .title .close { width: 20px; height: 20px; cursor: pointer; line-height: 10px; } .footer { display: flex; justify-content: flex-end; margin-top: 26px; } .footer button { width: 80px; height: 40px; } .footer button:nth-child(1) { margin-right: 10px; cursor: pointer; } </style>
10.6 总结
父组件如果想让子组件修改传过去的值 必须加什么修饰符?
子组件要修改父组件的props值 必须使用什么语法?
11. ref和$refs 11.1 作用 利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
11.2 特点: 查找范围 → 当前组件内(更精确稳定)
11.3 语法 1.给要获取的盒子添加ref属性
1 <div ref ="chartRef" > 我是渲染图表的容器</div >
2.获取时通过 $refs获取 this.$refs.chartRef 获取
1 2 3 mounted () { console.log(this.$refs.chartRef) }
11.4 注意 之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子
11.5 代码示例 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="app"> <BaseChart></BaseChart> </div> </template> <script> import BaseChart from './components/BaseChart.vue' export default { components:{ BaseChart } } </script> <style> </style>
BaseChart.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 33 34 35 36 37 38 39 40 41 42 <template> <div class="base-chart-box" ref="baseChartBox">子组件</div> </template> <script> // yarn add echarts 或者 npm i echarts import * as echarts from 'echarts' export default { mounted() { // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.querySelect('.base-chart-box')) // 绘制图表 myChart.setOption({ title: { text: 'ECharts 入门示例', }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'], }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20], }, ], }) }, } </script> <style scoped> .base-chart-box { width: 400px; height: 300px; border: 3px solid #000; border-radius: 6px; } </style>
12. 异步更新 & $nextTick 12.1 需求 编辑标题, 编辑框自动聚焦
点击编辑,显示编辑框
让编辑框,立刻获取焦点
12.2 代码实现 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 <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="editFn">编辑</button> </div> </div> </template> <script> export default { data() { return { title: '大标题', isShowEdit: false, editValue: '', } }, methods: { editFn() { // 显示输入框 this.isShowEdit = true // 获取焦点 this.$refs.inp.focus() } }, } </script>
12.3 问题 “显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
12.4 解决方案 $nextTick:等 DOM更新后 ,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
1 2 3 this .$nextTick(() => { this .$refs .inp .focus () })
注意: $nextTick 内的函数体 一定是箭头函数 ,这样才能让函数内部的this指向Vue实例
day05 1. 学习目标 1.1 自定义指令
基本语法(全局、局部注册)
指令的值
v-loading的指令封装
1.2 插槽
1.3 综合案例:商品列表
1.4 路由入门
2. 自定义指令 2.1 自定义指令基本介绍 1️⃣指令介绍
2️⃣自定义指令
概念:自己定义的指令,可以封装一些DOM操作 ,扩展额外的功能
3️⃣自定义指令语法
4️⃣指令中的配置项介绍
5️⃣代码示例
需求:当页面加载时,让元素获取焦点(autofocus在safari浏览器有兼容性 )
App.vue
1 2 3 4 <div> <h1>自定义指令</h1> <input v-focus ref="inp" type="text"> </div>
6️⃣总结
自定义指令的作用是什么?
使用自定义指令的步骤是哪两步?
2.2 自定义指令-指令的值 1️⃣需求
实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
2️⃣语法
在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值
1 <div v-color ="color" > 我是内容</div >
通过 binding.value
可以拿到指令值,指令值修改会 触发 update 函数
1 2 3 4 5 6 7 8 9 10 directives : { color : { inserted (el, binding) { el.style .color = binding.value }, update (el, binding) { el.style .color = binding.value } } }
3️⃣代码示例
App.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 <template> <div> <!--显示红色--> <h2 v-color="color1">指令的值1测试</h2> <!--显示蓝色--> <h2 v-color="color2">指令的值2测试</h2> <button> 改变第一个h1的颜色 </button> </div> </template> <script> export default { data () { return { color1: 'red', color2: 'blue' } } } </script> <style> </style>
2.3 自定义指令-v-loading指令的封装 1️⃣场景
实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好
2️⃣需求
封装一个 v-loading 指令,实现加载中的效果
3️⃣分析
本质 loading效果就是一个蒙层,盖在了盒子上
数据请求中,开启loading状态,添加蒙层
数据请求完毕,关闭loading状态,移除蒙层
4️⃣实现
准备一个 loading类,通过伪元素定位,设置宽高,实现蒙层
开启关闭 loading状态(添加移除蒙层),本质只需要添加移除类即可
结合自定义指令的语法进行封装复用
1 2 3 4 5 6 7 8 9 .loading :before { content : "" ; position : absolute; left : 0 ; top : 0 ; width : 100% ; height : 100% ; background : #fff url ("./loading.gif" ) no-repeat center; }
5️⃣准备代码
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 <template > <div class ="box" v-loading ="isLoading" > <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 > </template > <script > import axios from 'axios' export default { data () { return { list : [], isLoading : true } }, directives : { loading : { inserted (el, binding) { binding.value ? el.classList .add ('loading' ) : el.classList .remove ('loading' ) }, update (el, binding) { binding.value ? el.classList .add ('loading' ) : el.classList .remove ('loading' ) } } }, async created () { const res = await axios.get ('http://hmajax.itheima.net/api/news' ) setTimeout (() => { this .list = res.data .data this .isLoading = false }, 2000 ) } } </script > <style > .loading :before { content : '' ; position : absolute; left : 0 ; top : 0 ; width : 100% ; height : 100% ; background : #fff url ('./loading.gif' ) no-repeat center; } .box { width : 800px ; min-height : 500px ; border : 3px solid orange; border-radius : 5px ; position : relative; } .news { display : flex; height : 120px ; width : 600px ; margin : 0 auto; padding : 20px 0 ; cursor : pointer; } .news .left { flex : 1 ; display : flex; flex-direction : column; justify-content : space-between; padding-right : 10px ; } .news .left .title { font-size : 20px ; } .news .left .info { color : #999999 ; } .news .left .info span { margin-right : 20px ; } .news .right { width : 160px ; height : 120px ; } .news .right img { width : 100% ; height : 100% ; object-fit : cover; } </style >
3. 插槽 3.1 默认插槽 1️⃣作用
2️⃣需求
将需要多次显示的对话框,封装成一个组件
3️⃣问题
组件的内容部分,不希望写死 ,希望能使用的时候自定义 。怎么办
4️⃣插槽的基本语法
组件内需要定制的结构部分,改用<slot></slot>
占位
使用组件时, <MyDialog></MyDialog>
标签内部, 传入结构替换slot
给插槽传入内容时,可以传入纯文本、html标签、组件
5️⃣代码示例
MyDialog.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 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 <template> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div> <div class="dialog-content"> 您确定要进行删除操作吗? </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div> </template> <script> export default { data () { return { } } } </script> <style scoped> * { margin: 0; padding: 0; } .dialog { width: 470px; height: 230px; padding: 0 25px; background-color: #ffffff; margin: 40px auto; border-radius: 5px; } .dialog-header { height: 70px; line-height: 70px; font-size: 20px; border-bottom: 1px solid #ccc; position: relative; } .dialog-header .close { position: absolute; right: 0px; top: 0px; cursor: pointer; } .dialog-content { height: 80px; font-size: 18px; padding: 15px 0; } .dialog-footer { display: flex; justify-content: flex-end; } .dialog-footer button { width: 65px; height: 35px; background-color: #ffffff; border: 1px solid #e1e3e9; cursor: pointer; outline: none; margin-left: 10px; border-radius: 3px; } .dialog-footer button:last-child { background-color: #007acc; color: #fff; } </style>
App.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 <template> <div> <MyDialog> </MyDialog> </div> </template> <script> import MyDialog from './components/MyDialog.vue' export default { data () { return { } }, components: { MyDialog } } </script> <style> body { background-color: #b3b3b3; } </style>
6️⃣总结
场景:组件内某一部分结构不确定,想要自定义怎么办
使用:插槽的步骤分为哪几步?
2.2 插槽-后备内容(默认值) 1️⃣问题
通过插槽完成了内容的定制,传什么显示什么, 但是如果不传,则是空白
能否给插槽设置 默认显示内容 呢?
2️⃣插槽的后备内容
封装组件时,可以为预留的 <slot>
插槽提供后备内容(默认内容)。
3️⃣语法
在 <slot>
标签内,放置内容, 作为默认显示内容
4️⃣效果
5️⃣代码示例
App.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 <template> <div> <MyDialog></MyDialog> <MyDialog> 你确认要退出么 </MyDialog> </div> </template> <script> import MyDialog from './components/MyDialog.vue' export default { data () { return { } }, components: { MyDialog } } </script> <style> body { background-color: #b3b3b3; } </style>
3.3 插槽-具名插槽 1️⃣需求
一个组件内有多处结构,需要外部传入标签,进行定制
上面的弹框中有三处不同 ,但是默认插槽 只能定制一个位置 ,这时候怎么办呢?
2️⃣具名插槽语法
3️⃣v-slot的简写
v-slot写起来太长,vue给我们提供一个简单写法 v-slot —> #
4️⃣总结
组件内 有多处不确定的结构 怎么办?
具名插槽的语法是什么?
v-slot:插槽名可以简化成什么?
3.4 作用域插槽 1️⃣插槽分类
默认插槽
具名插槽
插槽只有两种,作用域插槽不属于插槽的一种分类
2️⃣作用
定义slot 插槽的同时, 是可以传值 的。给 插槽 上可以 绑定数据 ,将来 使用组件时可以用
3️⃣场景
4️⃣使用步骤
给 slot 标签, 以 添加属性的方式传值
1 <slot :id="item.id" msg="测试文本"></slot>
所有添加的属性, 都会被收集到一个对象中
在template中, 通过 #插槽名= "obj"
接收,默认插槽名为 default
1 2 3 4 5 <MyTable :list="list"> <template #default="obj"> <button @click="del(obj.id)">删除</button> </template> </MyTable>
5️⃣代码示例
MyTable.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 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 <template> <table class="my-table"> <thead> <tr> <th>序号</th> <th>姓名</th> <th>年纪</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id" > <td>{{index + 1}}</td> <td>{{item.name}}</td> <td>{{item.age}}</td> <td> <slot :row="item" msg="测试文本" ></slot> </td> </tr> </tbody> </table> </template> <script> export default { props: { data: Array } } </script> <style scoped> .my-table { width: 450px; text-align: center; border: 1px solid #ccc; font-size: 24px; margin: 30px auto; } .my-table thead { background-color: #1f74ff; color: #fff; } .my-table thead th { font-weight: normal; } .my-table thead tr { line-height: 40px; } .my-table th, .my-table td { border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; } .my-table td:last-child { border-right: none; } .my-table tr:last-child td { border-bottom: none; } .my-table button { width: 65px; height: 35px; font-size: 18px; border: 1px solid #ccc; outline: none; border-radius: 3px; cursor: pointer; background-color: #ffffff; margin-left: 5px; } </style>
App.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <template> <div> <MyTable :data="list"> <template #default="obj" > <button @click="del(obj.row.id)"> 删除 </button> </template> </MyTable> <MyTable :data="list2"> <template #default="{row}" > <button @click="show(row)">查看</button> </template> </MyTable> </div> </template> <script> import MyTable from './components/MyTable.vue' export default { data () { return { list: [ { id: 1, name: '张小花', age: 18 }, { id: 2, name: '孙大明', age: 19 }, { id: 3, name: '刘德忠', age: 17 }, ], list2: [ { id: 1, name: '赵小云', age: 18 }, { id: 2, name: '刘蓓蓓', age: 19 }, { id: 3, name: '姜肖泰', age: 17 }, ] } }, methods: { del(id) { this.list = this.list.filter(item => item.id !== id ) }, show(row) { alert(row) console.log(row) } }, components: { MyTable } } </script>
6️⃣总结
作用域插槽的作用是什么?
作用域插槽的使用步骤是什么?
4. 综合案例 4.1 综合案例 - 商品列表-MyTag组件抽离
1️⃣需求说明
my-tag 标签组件封装
(1) 双击显示输入框,输入框获取焦点
(2) 失去焦点,隐藏输入框
(3) 回显标签信息
(4) 内容修改,回车 → 修改标签信息
my-table 表格组件封装
(1) 动态传递表格数据渲染
(2) 表头支持用户自定义
(3) 主体支持用户自定义
2️⃣代码准备
App.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 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 147 148 149 150 151 152 153 154 155 156 157 <template> <div class="table-case"> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg" /> </td> <td> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </td> </tr> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg" /> </td> <td> <div class="my-tag"> <!-- <input ref="inp" class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 男靴 </div> </div> </td> </tr> </tbody> </table> </div> </template> <script> export default { name: 'TableCase', components: {}, data() { return { goods: [ { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具', }, { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋', }, { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰', }, { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰', }, ], } }, } </script> <style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } .my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all 0.5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } } </style>
3️⃣my-tag组件封装-创建组件
MyTag.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 33 34 35 36 37 38 <template> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </template> <script> export default { } </script> <style lang="less" scoped> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> ... <tbody> <tr> .... <td> <MyTag></MyTag> </td> </tr> </tbody> ... </template> <script> import MyTag from './components/MyTag.vue' export default { name: 'TableCase', components: { MyTag, }, .... </script>
4.2 综合案例-MyTag组件控制显示隐藏 MyTag.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 33 34 <template> <div class="my-tag"> <input v-if="isEdit" v-focus ref="inp" class="input" type="text" placeholder="输入标签" @blur="isEdit = false" /> <div v-else @dblclick="handleClick" class="text"> 茶具 </div> </div> </template> <script> export default { data () { return { isEdit: false } }, methods: { handleClick () { this.isEdit = true } } } </script>
main.js
1 2 3 4 5 6 7 Vue .directive ('focus' , { inserted (el) { el.focus () } })
4.3 综合案例-MyTag组件进行v-model绑定 App.vue
1 2 3 4 5 6 7 8 <MyTag v-model="tempText"></MyTag> <script> export default { data(){ tempText:'水杯' } } </script>
MyTag.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <template > <div class ="my-tag" > <input v-if ="isEdit" v-focus ref ="inp" class ="input" type ="text" placeholder ="输入标签" :value ="value" @blur ="isEdit = false" @keyup.enter ="handleEnter" /> <div v-else @dblclick ="handleClick" class ="text" > {{ value }} </div > </div > </template > <script > export default { props : { value : String }, data () { return { isEdit : false } }, methods : { handleClick () { this .isEdit = true }, handleEnter (e) { if (e.target .value .trim () === '' ) return alert ('标签内容不能为空' ) this .$emit('input' , e.target .value ) this .isEdit = false } } } </script >
4.4 综合案例-封装MyTable组件-动态渲染数据 App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template> <div class="table-case"> <MyTable :data="goods"></MyTable> </div> </template> <script> import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTable }, data(){ return { .... } }, } </script>
MyTable.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 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 <template> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> 标签内容 <!-- <MyTag v-model="item.tag"></MyTag> --> </td> </tr> </tbody> </table> </template> <script> export default { props: { data: { type: Array, required: true } } }; </script> <style lang="less" scoped> .my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all .5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } </style>
4.4 综合案例-封装MyTable组件-自定义结构 App.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 33 34 35 36 37 38 39 40 41 42 <template> <div class="table-case"> <MyTable :data="goods"> <template #head> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </template> <template #body="{ item, index }"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> <MyTag v-model="item.tag"></MyTag> </td> </template> </MyTable> </div> </template> <script> import MyTag from './components/MyTag.vue' import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTag, MyTable }, data () { return { .... } } </script>
MyTable.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 <template> <table class="my-table"> <thead> <tr> <slot name="head"></slot> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <slot name="body" :item="item" :index="index" ></slot> </tr> </tbody> </table> </template> <script> export default { props: { data: { type: Array, required: true } } }; </script>
5. 单页应用程序介绍 5.1 概念 单页应用程序:SPA【Single Page Application】是指所有的功能都在一个html页面 上实现
5.2 具体示例 单页应用网站: 网易云音乐 https://music.163.com/
多页应用网站:京东 https://jd.com/
5.3 单页应用 VS 多页面应用
单页应用类网站:系统类网站 / 内部网站 / 文档类网站 / 移动端站点
多页应用类网站:公司官网 / 电商类网站
5.4 总结
什么是单页面应用程序?
单页面应用优缺点?
单页应用场景?
6. 路由 6.1 路由介绍 6.1.1 思考 单页面应用程序,之所以开发效率高,性能好,用户体验好
最大的原因就是:页面按需更新
比如当点击【发现音乐】和【关注】时,只是更新下面部分内容 ,对于头部是不更新的
要按需更新,首先就需要明确:访问路径 和 组件 的对应关系!
访问路径 和 组件的对应关系如何确定呢? 路由
6.1.2 路由的介绍 生活中的路由:设备和ip的映射关系
Vue中的路由:路径和组件 的映射 关系
6.1.3 总结
什么是路由
Vue中的路由是什么
路径和组件的映射关系
根据路由就能知道不同路径的,应该匹配渲染哪个组件
6.2 路由的基本使用(VueRouter) 1️⃣目标
认识插件 VueRouter,掌握 VueRouter 的基本使用步骤
2️⃣作用
3️⃣说明
4️⃣官网
5️⃣VueRouter的使用(5+2)
固定5个固定的步骤(不用死背,熟能生巧)
下载 VueRouter 模块到当前工程,版本3.6.5
1 yarn add vue-router@3.6.5
main.js中引入VueRouter
1 import VueRouter from 'vue-router'
安装注册
创建路由对象
1 const router = new VueRouter()
注入,将路由对象注入到new Vue实例中,建立关联
1 2 3 4 5 new Vue({ render: h => h(App), router:router }).$mount('#app')
当我们配置完以上5步之后 就可以看到浏览器地址栏中的路由 变成了 /#/的形式。表示项目的路由已经被Vue-Router管理了
6️⃣代码示例
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 路由的使用步骤 5 + 2 // 5个基础步骤 // 1. 下载 v3.6.5 // yarn add vue-router@3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象 // 5. 注入到new Vue中,建立关联 import VueRouter from 'vue-router' Vue.use(VueRouter) // VueRouter插件初始化 const router = new VueRouter() new Vue({ render: h => h(App), router }).$mount('#app')
7️⃣两个核心步骤
创建需要的组件 (views目录),配置路由规则
配置导航,配置路由出口(路径匹配的组件显示的位置)
App.vue
1 2 3 4 5 6 7 8 <div class="footer_wrap"> <a href="#/find">发现音乐</a> <a href="#/my">我的音乐</a> <a href="#/friend">朋友</a> </div> <div class="top"> <router-view></router-view> </div>
8️⃣总结
如何实现 路径改变,对应组件 切换,应该使用哪个插件?
Vue-Router的使用步骤是什么(5+2)?
// 5个基础步骤
// 1. 下载 3.6.5
// 2. 引入
// 3. 安装注册 Vue.use
// 4. 创建路由对象
// 5. 注入到new Vue 中, 建立关联
// 2个核心步骤
// 1. 建组件(views目录),配规则
// 2. 准备导航链接,配置路由出口(匹配的组件展示的位置)
7. 组件的存放目录问题 注意: .vue文件 本质无区别
1.组件分类 .vue文件分为2类,都是 .vue文件(本质无区别)
页面组件 (配置路由规则时使用的组件)
复用组件(多个组件中都使用到的组件)
2.存放目录 分类开来的目的就是为了 更易维护
src/views文件夹
==页面组件== - 页面展示 - 配合路由用
src/components文件夹
==复用组件== - 展示数据 - 常用于复用
3.总结
8. 路由的封装抽离 问题:所有的路由配置都在main.js中合适吗?
目标:将路由模块抽离出来。 好处:拆分模块,利于维护
路径简写:
脚手架环境下 @指代src目录,可以用于快速引入组件
总结:
路由模块的封装抽离的好处是什么?
以后如何快速引入组件?
day06 1. 声明式导航 1.1 导航链接 1.1.1 需求 实现导航高亮效果
如果使用a标签进行跳转的话,需要给当前跳转的导航加样式,同时要移除上一个a标签的样式,太麻烦!!!
1.1.2 解决方案 vue-router 提供了一个全局组件 router-link (取代 a 标签)
能跳转 ,配置 to 属性指定路径(必须 ) 。本质还是 a 标签 ,to 无需 #
能高亮 ,默认就会提供高亮类名 ,可以直接设置高亮样式
语法: <router-link to="path的值">
发现音乐</router-link>
1 2 3 4 5 6 7 8 9 10 11 <div> <div class="footer_wrap"> <router-link to="/find">发现音乐</router-link> <router-link to="/my">我的音乐</router-link> <router-link to="/friend">朋友</router-link> </div> <div class="top"> <!-- 路由出口 → 匹配的组件所展示的位置 --> <router-view></router-view> </div> </div>
1.1.3 通过router-link自带的两个样式进行高亮 使用router-link跳转后,我们发现。当前点击的链接默认加了两个class的值 router-link-exact-active
和router-link-active
我们可以给任意一个class属性添加高亮样式即可实现功能
1.1.4 总结
router-link是什么?
vue-router提供的全局组件,用于替换a标签
router-link怎么用?
<router-link to = "/路径值"> </router-link>
router-link的好处是什么?
1.2 声明式导航-两个类名 当我们使用<router-link></router-link>
跳转时,自动给当前导航加了两个类名
1️⃣router-link-active 模糊匹配(用的多)
2️⃣router-link-exact-active 精确匹配
3️⃣在地址栏中输入二级路由查看类名的添加
4️⃣总结
router-link 会自动给当前导航添加两个类名,有什么区别呢?
1.3 声明式导航-自定义类名(了解) 1️⃣问题
router-link的两个高亮类名 太长了 ,我们希望能定制怎么办
2️⃣解决方案
我们可以在创建路由对象时,额外配置两个配置项即可。 linkActiveClass
和linkExactActiveClass
1 2 3 4 5 const router = new VueRouter ({ routes : [...], linkActiveClass : "类名1" , linkExactActiveClass : "类名2" })
3️⃣代码演示
1 2 3 4 5 6 7 8 const router = new VueRouter ({ routes : [ ... ], linkActiveClass : 'active' , linkExactActiveClass : 'exact-active' })
4️⃣总结
如何自定义router-link的两个高亮类名
1.4 声明式导航-查询参数传参 1️⃣目标
在跳转路由时,进行传参
比如:现在我们在搜索页点击了热门搜索链接,跳转到详情页,需要把点击的内容带到详情页 ,改怎么办呢?
2️⃣跳转传参
我们可以通过两种方式,在跳转的时候把所需要的参数传到其他页面中
3️⃣查询参数传参
4️⃣代码演示
App.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 33 34 35 <template> <div id="app"> <div class="link"> <router-link to="/home">首页</router-link> <router-link to="/search">搜索页</router-link> </div> <router-view></router-view> </div> </template> <script> export default {}; </script> <style scoped> .link { height: 50px; line-height: 50px; background-color: #495150; display: flex; margin: -8px -8px 0 -8px; margin-bottom: 50px; } .link a { display: block; text-decoration: none; background-color: #ad2a26; width: 100px; text-align: center; margin-right: 5px; color: #fff; border-radius: 5px; } </style>
Home.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 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 <template> <div class="home"> <div class="logo-box"></div> <div class="search-box"> <input type="text"> <button>搜索一下</button> </div> <div class="hot-link"> 热门搜索: <router-link to="">黑马程序员</router-link> <router-link to="">前端培训</router-link> <router-link to="">如何成为前端大牛</router-link> </div> </div> </template> <script> export default { name: 'FindMusic' } </script> <style> .logo-box { height: 150px; background: url('@/assets/logo.jpeg') no-repeat center; } .search-box { display: flex; justify-content: center; } .search-box input { width: 400px; height: 30px; line-height: 30px; border: 2px solid #c4c7ce; border-radius: 4px 0 0 4px; outline: none; } .search-box input:focus { border: 2px solid #ad2a26; } .search-box button { width: 100px; height: 36px; border: none; background-color: #ad2a26; color: #fff; position: relative; left: -2px; border-radius: 0 4px 4px 0; } .hot-link { width: 508px; height: 60px; line-height: 60px; margin: 0 auto; } .hot-link a { margin: 0 5px; } </style>
Search.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="search"> <p>搜索关键字: 黑马程序员</p> <p>搜索结果: </p> <ul> <li>.............</li> <li>.............</li> <li>.............</li> <li>.............</li> </ul> </div> </template> <script> export default { name: 'MyFriend', created () { // 在created中,获取路由参数 } } </script> <style> .search { width: 400px; height: 240px; padding: 0 20px; margin: 0 auto; border: 2px solid #c4c7ce; border-radius: 5px; } </style>
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import Home from '@/views/Home' import Search from '@/views/Search' import Vue from 'vue' import VueRouter from 'vue-router' Vue .use (VueRouter ) const router = new VueRouter ({ routes : [ { path : '/home' , component : Home }, { path : '/search' , component : Search } ] }) export default router
main.js
1 2 3 4 5 6 7 ... import router from './router/index' ... new Vue({ render: h => h(App), router }).$mount('#app' )
1.5 声明式导航-动态路由传参 1.5.1 动态路由传参方式
配置动态路由
动态路由后面的参数可以随便起名,但要有语义
1 2 3 4 5 6 7 8 9 const router = new VueRouter ({ routes : [ ..., { path : '/search/:words' , component : Search } ] })
配置导航链接
to=”/path/参数值”
对应页面组件接受参数
$route.params.参数名
params后面的参数名要和动态路由配置的参数保持一致
1.5.2 查询参数传参 VS 动态路由传参
查询参数传参 (比较适合传多个参数 )
跳转:to=”/path?参数名=值&参数名2=值”
获取:$route.query.参数名
动态路由传参 (优雅简洁 ,传单个参数比较方便)
配置动态路由:path: “/path/:参数名”
跳转:to=”/path/参数值”
获取:$route.params.参数名
注意:动态路由也可以传多个参数,但一般只传一个
1.5.3 总结 声明式导航跳转时, 有几种方式传值给路由页面?
查询参数传参(多个参数)
动态路由传参(一个参数,优雅简洁)
2. 动态路由参数的可选符(了解) 2.1 问题 配了路由 path:”/search/:words” 为什么按下面步骤操作,会未匹配到组件,显示空白?
2.2 原因 /search/:words 表示,必须要传参数 。如果不传参数,也希望匹配,可以加个==可选符”?”==
1 2 3 4 5 6 const router = new VueRouter ({ routes : [ ... { path : '/search/:words?' , component : Search } ] })
3. Vue路由 3.1 Vue路由 - 重定向 1️⃣问题
网页打开时, url 默认是 / 路径,未匹配到组件时,会出现空白
2️⃣解决方案
重定向 → 匹配 / 后, 强制跳转 /home 路径
3️⃣语法
1 2 3 { path : 匹配路径, redirect : 重定向到的路径 }, 比如: { path :'/' ,redirect :'/home' }
4️⃣代码演示
1 2 3 4 5 6 const router = new VueRouter({ routes: [ { path: '/' , redirect: '/home' }, ... ] })
3.2 Vue路由-404 1️⃣作用
当路径找不到匹配时,给个提示页面
2️⃣位置
404的路由,虽然配置在任何一个位置都可以,但一般都配置在其他路由规则的最后面
3️⃣语法
path: “*” (任意路径) – 前面不匹配就命中最后这个
1 2 3 4 5 6 7 8 import NotFind from '@/views/NotFind' const router = new VueRouter ({ routes : [ ... { path : '*' , component : NotFind } ] })
4️⃣代码示例
NotFound.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <div> <h1>404 Not Found</h1> </div> </template> <script> export default { } </script> <style> </style>
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 ... import NotFound from '@/views/NotFound' ... const router = new VueRouter ({ routes : [ ... { path : '*' , component : NotFound } ] }) export default router
3.3 Vue路由-模式设置 1️⃣问题
路由的路径看起来不自然, 有#,能否切成真正路径形式?
2️⃣语法
1 2 3 4 const router = new VueRouter ({ mode :'histroy' , routes :[] })
4. 编程式导航 4.1 编程式导航-两种路由跳转方式 1️⃣问题
点击按钮跳转如何实现?
2️⃣方案
编程式导航:用JS代码来进行跳转
3️⃣语法
两种语法:
path 路径跳转 (简易方便)
name 命名路由跳转 (适合 path 路径长的场景)
4️⃣path路径跳转语法
特点:简易方便
1 2 3 4 5 6 7 this .$router .push ('路由路径' )this .$router .push ({ path : '路由路径' })
5️⃣代码演示 path跳转方式
1 2 3 4 5 6 7 8 methods: { goSearch() { // this.$router.push('/search') this.$router.push({ path: '/search' }) } }
6️⃣name命名路由跳转
特点:适合 path 路径长的场景
语法:
路由规则,必须配置name配置项
1 { name : '路由名' , path : '/path/xxx' , component : XXX },
通过name来进行跳转
1 2 3 this .$router .push ({ name : '路由名' })
7️⃣代码演示通过name命名路由跳转
8️⃣总结
编程式导航有几种跳转方式?
4.2编程式导航-path路径跳转传参 1️⃣问题
点击搜索按钮,跳转需要把文本框中输入的内容传到下一个页面如何实现?
2️⃣两种传参方式
查询参数
动态路由传参
3️⃣传参
两种跳转方式,对于两种传参方式都支持:
① path 路径跳转传参
② name 命名路由跳转传参
4️⃣path路径跳转传参(query传参)
1 2 3 4 5 6 7 8 9 10 this .$router .push ('/路径?参数名1=参数值1&参数2=参数值2' )this .$router .push ({ path : '/路径' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
接受参数的方式依然是:$route.query.参数名
5️⃣path路径跳转传参(动态路由传参)
1 2 3 4 5 6 this.$router .push ('/路径/参数值' ) this.$router .push ({ path : '/路径/参数值' })
接受参数的方式依然是:$route.params.参数值
注意: path不能配合params使用
4.3 编程式导航-name命名路由传参 4.3.1 name 命名路由跳转传参 (query传参) 1 2 3 4 5 6 7 this .$router .push ({ name : '路由名字' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
4.3.2 name 命名路由跳转传参 (动态路由传参) 1 2 3 4 5 6 this .$router .push ({ name : '路由名字' , params : { 参数名: '参数值' , } })
4.3.3 总结 编程式导航,如何跳转传参?
1️⃣path路径跳转
query传参
1 2 3 4 5 6 7 8 this .$router .push ('/路径?参数名1=参数值1&参数2=参数值2' )this .$router .push ({ path : '/路径' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
动态路由传参
1 2 3 4 this .$router .push ('/路径/参数值' )this .$router .push ({ path : '/路径/参数值' })
2️⃣name命名路由跳转
query传参
1 2 3 4 5 6 7 this .$router .push ({ name : '路由名字' , query : { 参数名1 : '参数值1' , 参数名2 : '参数值2' } })
动态路由传参 (需要配动态路由)
1 2 3 4 5 6 this .$router .push ({ name : '路由名字' , params : { 参数名: '参数值' , } })
5. 面经基础版 5.1 面经基础版 - 案例效果分析 1️⃣面经效果演示
2️⃣功能分析
通过演示效果发现,主要的功能页面有两个,一个是列表页 ,一个是详情页 ,并且在列表页点击时可以跳转到详情页
底部导航可以来回切换,并且切换时,只有上面的主题内容在动态渲染
3️⃣实现思路分析:配置路由+功能实现
配置路由
首页和面经详情页,两个一级路由
首页内嵌套4个可切换的页面(嵌套二级路由)
实现功能
首页请求渲染
跳转传参 到 详情页,详情页动态渲染
组件缓存,性能优化
5.2 面经基础版-一级路由配置
把文档中准备的素材拷贝到项目中
针对router/index.js文件 进行一级路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ... import Layout from '@/views/Layout.vue' import ArticleDetail from '@/views/ArticleDetail.vue' ... const router = new VueRouter ({ routes : [ { path : '/' , component : Layout }, { path : '/detail' , component : ArticleDetail } ] })
5.3 面经基础版-二级路由配置 二级路由也叫嵌套路由,当然也可以嵌套三级、四级…
1️⃣使用场景
当在页面中点击链接跳转,只是部分内容切换时,我们可以使用嵌套路由
2️⃣语法
在一级路由下,配置children属性即可
配置二级路由的出口
在一级路由下,配置children属性
注意 :一级的路由path 需要加 /
二级路由的path不需要加 /
1 2 3 4 5 6 7 8 9 10 11 12 13 const router = new VueRouter ({ routes : [ { path : '/' , component : Layout , children :[ {path :'xxxx' ,component :xxxx.vue }, {path :'xxxx' ,component :xxxx.vue }, ] } ] })
技巧:二级路由应该配置到哪个一级路由下呢?
这些二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边
配置二级路由的出口 <router-view></router-view>
注意: 配置了嵌套路由,一定配置对应的路由出口,否则不会渲染出对应的组件
Layout.vue
1 2 3 4 5 6 7 8 <template> <div class="h5-wrapper"> <div class="content"> <router-view></router-view> </div> .... </div> </template>
3️⃣代码实现
router/index.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 31 32 33 34 35 36 ... import Article from '@/views/Article.vue' import Collect from '@/views/Collect.vue' import Like from '@/views/Like.vue' import User from '@/views/User.vue' ... const router = new VueRouter ({ routes : [ { path : '/' , component : Layout , redirect : '/article' , children :[ { path :'/article' , component :Article }, { path :'/collect' , component :Collect }, { path :'/like' , component :Like }, { path :'/user' , component :User } ] }, .... ] })
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template> <div class="h5-wrapper"> <div class="content"> <!-- 内容部分 --> <router-view></router-view> </div> <nav class="tabbar"> <a href="#/article">面经</a> <a href="#/collect">收藏</a> <a href="#/like">喜欢</a> <a href="#/user">我的</a> </nav> </div> </template>
5.4 面经基础版-二级导航高亮 1️⃣实现思路
将a标签替换成 <router-link></router-link>
组件,配置to属性,不用加 #
结合高亮类名实现高亮效果 (推荐模糊匹配:router-link-active)
2️⃣代码实现
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 .... <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> <style> a.router-link-active { color: orange; } </style>
5.5 面经基础版-首页请求渲染 1️⃣步骤分析
安装axios
看接口文档,确认请求方式,请求地址,请求参数
created中发送请求,获取数据,存储到data中
页面动态渲染
2️⃣代码实现
安装axios
yarn add axios
npm i axios
接口文档
1 2 请求地址: https://mock.boxuegu.com/mock/3083/articles 请求方式: get
created中发送请求,获取数据,存储到data中
1 2 3 4 5 6 7 8 9 data() { return { articelList: [], } }, async created() { const { data: { result: { rows } }} = await axios.get('https://mock.boxuegu.com/mock/3083/articles') this.articelList = rows },
页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id"> <div class="head"> <img :src="item.creatorAvatar" 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>
5.6 面经基础版-查询参数传参 1️⃣说明
跳转详情页需要把当前点击的文章id传给详情页,获取数据
查询参数传参 this.$router.push('/detail?参数1=参数值&参数2=参数值')
动态路由传参 先改造路由 在传参 this.$router.push('/detail/参数值')
2️⃣查询参数传参实现
Article.vue
1 2 3 4 5 6 7 8 9 <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail?id=${item.id}`)"> ... </div> </div> </template>
ArticleDetail.vue
1 2 3 created(){ console.log(this.$route.query.id) }
5.7 面经基础版-动态路由传参 1️⃣实现步骤
2️⃣代码实现
改造路由
router/index.js
1 2 3 4 5 ... { path : '/detail/:id' , component : ArticleDetail }
Article.vue
1 2 3 4 5 <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail/${item.id}`)"> .... </div>
ArticleDetail.vue
1 2 3 created(){ console.log(this.$route.params.id) }
3️⃣额外优化功能点-点击回退跳转到上一页
ArticleDetail.vue
1 2 3 4 5 6 <template> <div class="article-detail-page"> <nav class="nav"><span class="back" @click="$router.back()"><</span> 面经详情</nav> .... </div> </template>
5.8 面经基础版-详情页渲染 1️⃣实现步骤分析
导入axios
查看接口文档
在created中发送请求
页面动态渲染
2️⃣代码实现
接口文档
1 2 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id 请求方式: get
在created中发送请求
1 2 3 4 5 6 7 8 9 10 11 12 data() { return { articleDetail:{} } }, async created() { const id = this.$route.params.id const {data:{result}} = await axios.get( `https://mock.boxuegu.com/mock/3083/articles/${id}` ) this.articleDetail = result },
页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div class="article-detail-page"> <nav class="nav"> <span class="back" @click="$router.back()"><</span> 面经详情 </nav> <header class="header"> <h1>{{articleDetail.stem}}</h1> <p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p> <p> <img :src="articleDetail.creatorAvatar" alt="" /> <span>{{articleDetail.creatorName}}</span> </p> </header> <main class="body"> {{articleDetail.content}} </main> </div> </template>
5.9 面经基础版-缓存组件 5.9.1 问题 从面经列表 点到 详情页,又点返回,数据重新加载了 → 希望回到原来的位置
5.9.2 原因 当路由被跳转 后,原来所看到的组件就被销毁 了(会执行组件内的beforeDestroy和destroyed生命周期钩子),重新返回 后组件又被重新创建 了(会执行组件内的beforeCreate,created,beforeMount,Mounted生命周期钩子),所以数据被加载了
5.9.3 解决方案 利用keep-alive把原来的组件给缓存下来
5.9.4 什么是keep-alive
优点:
App.vue
1 2 3 4 5 6 7 <template> <div class="h5-wrapper"> <keep-alive> <router-view></router-view> </keep-alive> </div> </template>
问题:
5.9.5 keep-alive的三个属性 ① include : 组件名数组,只有匹配的组件会被缓存
② exclude : 组件名数组,任何匹配的组件都不会被缓存
③ max : 最多可以缓存多少 组件实例
App.vue
1 2 3 4 5 6 7 <template> <div class="h5-wrapper"> <keep-alive :include="['LayoutPage']"> <router-view></router-view> </keep-alive> </div> </template>
5.9.6 额外的两个生命周期钩子 keep-alive的使用会触发两个生命周期函数
组件缓存后 就不会执行 组件的created, mounted, destroyed 等钩子了
所以其提供了actived 和deactived 钩子,帮我们实现业务需求。
5.9.7 总结 1️⃣keep-alive是什么
2️⃣keep-alive的优点
组件切换过程中,把切换出去的组件保留在内存中(提升性能)
3️⃣keep-alive的三个属性 (了解)
4️⃣keep-alive的使用会触发两个生命周期函数(了解)
5.10 总结 1️⃣ 项目案例实现的基本步骤分哪两大步?
① 配路由 ② 实现页面功能
2️⃣ 嵌套路由的关键配置项是什么?
3️⃣路由传参两种方式?
4️⃣缓存组件可以用哪个内置组件?
keep-alive
6. VueCli 自定义创建项目 1.安装脚手架 (已安装)
2.创建项目
1 vue create hm-exp -mobile
1 2 3 4 5 Vue CLI v5.0 .8 ? Please pick a preset : Default ([Vue 3 ] babel, eslint) Default ([Vue 2 ] babel, eslint) > Manually select features 选自定义
选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子
是否保存预设,下次直接使用? => 不保存,输入 N
7. ESlint代码规范及手动修复 代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…
没有规矩不成方圆
ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。
7.1 JavaScript Standard Style 规范说明 建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。
下面是这份规则中的一小部分:
字符串使用单引号 – 需要转义的地方除外
无分号 – 这 没什么不好。 不骗你!
关键字后加空格 if (condition) { ... }
函数名后加空格 function name (arg) { ... }
坚持使用全等 ===
摒弃 ==
一但在需要检查 null || undefined
时可以使用 obj == null
……
7.2 代码规范错误 如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。
下面我们在main.js中随意做一些改动:添加一些空行,空格。
1 2 3 4 5 6 7 8 9 10 11 12 13 import Vue from 'vue' import App from './App.vue' import './styles/index.less' import router from './router' Vue .config .productionTip = false new Vue ( { render : h => h (App ), router }).$mount('#app' )
按下保存代码之后:
你将会看在控制台中输出如下错误:
eslint 是来帮助你的。心态要好,有错,就改。
7.3 手动修正 根据错误提示来一项一项手动修正。
如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…..)去 ESLint 规则列表中查找其具体含义。
打开 ESLint 规则表 ,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。
8. 通过eslint插件来实现自动修正
eslint会自动高亮错误显示
通过配置,eslint会自动帮助我们修复错误
1 2 3 4 5 6 "editor.codeActionsOnSave" : { "source.fixAll" : true }, "editor.formatOnSave" : false
注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目
注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify
settings.json 参考
1 2 3 4 5 6 7 8 9 10 11 12 { "window.zoomLevel" : 2 , "workbench.iconTheme" : "vscode-icons" , "editor.tabSize" : 2 , "emmet.triggerExpansionOnTab" : true , "editor.codeActionsOnSave" : { "source.fixAll" : true }, "editor.formatOnSave" : false }