Vue基础 (黑马程序员讲解)

Vue2.x技术精讲

目录:

1. Vue 快速上手
    vue概念/创建实例/插值表达式/响应式特性/开发者工具
2. Vue 指令
    v-html/v-show/v-if/v-else/v-on/v-bind/v-for/v-model
3. 综合案例-小黑记事本
    列表渲染/删除功能/添加功能/底部统计/清空

一、vue是什么

概念:Vue 是一个用于构建用户界面的渐进式框架

1. 构建用户界面基于数据渲染 出用户看到的页面。


2. 渐进式: 循序渐进 (学一点用一点)


Vue的两种使用方式:

  • Vue 核心包开发

    • 场景:局部模块改造

  • Vue 核心包 & Vue插件 工程化开发

    • 场景:整体开发


3. 框架:一套完整的项目解决方案

优点:大大提升开发效率 (70%)

缺点:需要理解记忆规则 -> 官网


二、创建一个vue实例

核心步骤:

1. 准备容器
2. 引包 (官网) - 开发版本 / 生产版本
3. 创建 Vue 实例 new Vue()
4. 指定配置项 -> 渲染数据(哪个容器,哪个数据)
    + el 指定容器
    + data 提供数据

引包:

Vue2 网址:v2.cn.vuejs.org


<!-- 创建Vue实例,初始化渲染
    1. 准备容器 (Vue所管理的范围)
    2. 引包
    3. 创建实例
    4. 添加配置项 => 完成渲染
-->
<div id="app">
    {{msg}}
    <h3>{{count}}</h3>
</div>
​
<!-- 引入的是开发版本的包 - 包含完整的注释和警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
​
<script>
    // 一旦引入 Vuejs核心包,在全局环境下,就有了Vue构造函数
    const app = new Vue({
        // 通过 el配置选择器,指定 Vue 管理的是哪个盒子
        el: '#app',
        // 通过 data 提供数据
        data: {
            msg: 'hello 黑马',
            count: 666
        }
    });
</script>

三、插值表达式

插值表达式是一种 Vue 的模板语法

<div id="app">
    {{ msg }}
</div>

data: {
    msg: 'Hello 黑马'
}

1. 作用:利用表达式进行插值,渲染到页面中

  • 表达式:是可以被求值的代码,JS 引擎会将其计算出一个结果。

money + 100
money - 100
money * 10
money / 10
price >= 100 ? '真贵' : '还行'
obj.name
arr.name
arr[0]
fn()
obj.fn()

2. 语法: {{ 表达式 }}

<h3> {{ title }} </h3>
<p> {{ nickname.toUpperCase }}</p>
<p> {{ age >= 18? '成年' : '未成年' }} </p>

3. 注意点:

  • 使用的数据必须存在

  • 支持的是表达式,而非语句,比如:if、for...

  • 不能在标签属性中使用 {{ }} 插值


<div id="app">
    <p>{{msg}}</p>
    <p>{{msg.toUpperCase()}}</p>
    <p>{{msg + 'Nice to meet you'}}</p>
    <p>{{ age >= 18 ? '成年' : '未成年' }}</p>
</div>
​
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
​
<script>
    const app = new Vue({
        el: '#app',
        data: {
            msg: 'hello 黑马',
            age: 18
        }
    });
</script>

四、响应式特性

我们已经掌握了基础的模板渲染,其实除了基本的模板渲染,Vue背后还做了大量工作。

比如:数据的响应式处理

  • 响应式:数据变化,视图自动更新


<div id="app">
    {{ msg }}
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script>
    // data中的数据,是会被添加到实例上
    // 1.访问数据,实例.属性名
    // 2. 修改数据,实例.属性名 = 新值
    
    const app = new Vue({
        el: '#app',
        // 响应式数据
        data: {
            msg: '你好,黑马'
        }
    })
</script>

Vue核心特征:响应式

数据改变,试图会自动更新


五、开发者工具安装

安装 Vue开发者工具:装插件调试 Vue 应用

(1) 通过谷歌应用商店安装 (国外网站)

(2) 极简插件:搜索 vue (选择第一个) ->下载 -> 开发者模式 -> 拖拽安装 -> 插件详情允许访问文件

极简插件_Chrome扩展插件商店_优质crx应用下载

(3) 打开 Vue 运行的页面,调试工具中 Vue 栏,即可查看修改数据,进行调试。

开发者模式


插件详情允许访问文件


六、Vue指令

Vue 会根据不同的 [ 指令 ],针对标签实现不同的 [功能]

指令:带有 v- 前缀 的特殊 标签属性

<!-- Vue 指令: v- 前缀的标签属性 -->
<div v-html = "str"></div>

<!-- 普通标签属性 -->
<div class="box">...</div>
<div title="小张">...</div>

(一) 指令初始 v-html

作用:设置元素的 innerHTML

语法:v-html = "表达式"

div id="app">
    <div v-html="msg"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            msg:
              `<a href="http://itheima.com">黑马程序员</a>`
        }
    })
</script>

(二) 指令 v-show 和 v-if

v-show

1. 作用:控制元素显示隐藏

2. 语法:

v-show = "表示式"  表达式值 true 显示,false 隐藏

3. 底层原理:切换 CSS 的 display: none 来控制显示隐藏

4. 场景:频繁切换显示隐藏的场景


v-if

1. 作用:控制元素显示隐藏 (条件渲染)

2. 语法:

3. 底层原理: 根据判断条件 控制元素的创建和移除

4. 场景: 要么显示,要么隐藏,不频繁切换的场景

v-if = "表达式"	表达式值 true 显示,false 隐藏

<div id="app">
    <div v-show="flag" class="box">我是v-show控制的盒子</div>
    <div v-if="flag" class="box">我是v-if控制的盒子</div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
           flag: false;
        }
    })
</script>

(三) 指令v-else和v-else-if

1. 作用:辅助 v-if 进行判断渲染

2. 语法:v-else v-else-if = "表达式"

3. 注意: 需要紧挨着 v-if 一起使用


<
div id="app">
    <p v-if="gender === 1">性别:男</p>
    <p v-else>性别:女</p>
    <hr>
    <P v-if="score >= 90">成绩评定为A: 奖励电脑一台</p>
    <P v-else-if="score >= 70">成绩评定为B: 奖励周末郊游</p>
    <P v-else-if="score >= 60">成绩评定为C: 奖励零食礼包</p>
    <P v-else>成绩评定为D: 惩罚一周不能玩手机</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            gender: 1,
            score: 80
        }
    })
</script>

(四) 指令 v-on

1. 作用: 注册事件 = 添加监听 + 提供处理逻辑

2. 语法:

方式1. v-on: 事件名 = "内联语句"		// 内联语句:可执行代码
方式2. v-on: 事件名 = "methods中的函数名"

3. 简写:@事件名

button v-on:click="count++"> 按钮 </button>
button @click="count++"> 按钮 </button>

(1) 第一种事件注册方式

<div id="app">
    <!-- <button v-on:click="count--">-</button> -->
    // v-on: 可以用 @ 替换
    <button @click="count = count - 2">+</button>
    
    <span> {{ count }} </span>
    
    <!-- <button v-on:click="count++">+</button> -->
    // v-on: 可以用 @ 替换
    <button @click="count = count + 2">+</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            count: 100
        }
    })
</script>

(2) 第二种注册事件方式

button @click="fn">-</button>

const app = new Vue({
        el: '#app',
        data: {
        	// 提供数据
            count: 100
        },
        methods: {
        	// 提供处理逻辑函数
        	fn() {
        		console.log('提供逻辑代码')
        	}
        }
    })

<div id="app">
    <button @click="fn">切换显示隐藏</button>
    <h1 v-show="isShow">黑马程序员</h1>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            isShow: true
        },
        methods: {
            fn() {
            	// 让提供的所有methods中的函数,this都指向当前实例 (this === app)
            	// app.isShow = !app.isShow
                this.isShow = !this.isShow
            }
        }
    })
</script>

4. 注意:methods 函数内的 this 执行Vue 实例


(3) Vue指令v-on调用传参


<div id="app">
    <div class="box">
        <h3>小黑自动售货机</h3>
        <button @click="buy(5)">可乐5元</button>
        <button @click="buy(10)">咖啡10元</button>
    </div>
    <p>银行卡余额: {{ money }}</p>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            money: 100
        },
        methods: {
            buy(price) {
                this.money -= price
            }
        }
    })
</script>

(五) 指令 v-bind

1. 作用: 动态设置 html的 标签属性 -> src、url、title ...

2. 语法: v-bind:属性名= "表达式"

3. 注意:简写形式::属性名="表达式"

<div id="app">
    <!-- v-bind:src  =>  :src -->
    <!-- <img v-bind:src="imgUrl" v-bind:title="msg" alt=""> -->
    <img :src="imgUrl" :title="msg" alt="">
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            imgUrl: 'images/01.jpg',
            msg: 'hello'
        }
    })
</script>

案例-波仔的学习之旅

核心思路分析:

  1. 数组存储图片路径 -> [图片1,图片2,图片3,...]

  2. 准备下标 index,数组 [下标] -> v-bind设置 src展示图片 -> 修改下标切换图片

<div id="app">
    <button v-show="index > 0" @click="index--">上一页</button>
    <div>
        <img v-bind:src="list[index]" alt="">
    </div>
    <button v-show="index < list.length -1" @click="index++">下一页</button>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            index: 0,
            list: [
                `images/bo1.jpg`,
                `images/bo2.jpg`,
                `images/bo3.jpg`,
                `images/bo4.jpg`,
            ]
        }
    })
</script>

(六) 指令 v-for

1. 作用:基于数据循环,多次渲染整个元素 -> 数组、对象、数字 ...


2. 遍历数组语法:

v-for = "(item,index) in 数组"

  • item 每一项,index 下标

  • 省略 index,: v-for = "item in 数组"

<div id="app">
    <ul>
        <li v-for="item in list">{{ item }}</li>
    </ul>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            list: ['西瓜', '苹果', '鸭梨', '榴莲']
        }
    })
</script>

案例-小黑的书架

明确需求:

  1. 基本渲染 -> for

  2. 删除功能



<div id="app">
    <h3>小黑的书架</h3>
    <ul>
        <li v-for="item in booksList" :key="item.id">
            <span>{{ item.name }}</span>
            <span>{{item.author}}</span>
            <!-- 注册点击事件 -> 通过 id 进行删除数组中的对应项 -->
            <button @click="del(item.id)">删除</button>
        </li>
    </ul>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            booksList: [
                { id: 1, name: '《红楼梦》', author: '曹雪芹' },
                { id: 2, name: '《西游记》', author: '吴承恩' },
                { id: 3, name: '《水浒传》', author: '施耐庵' },
                { id: 4, name: '《三国演义》', author: '罗贯中' },

            ]
        },
        methods: {
            del(id) {
                //通过 id 进行删除数组中的对应项    -> filter(不会改变原数组)
                // filter: 根据条件,保留满足条件的对应项,得到一个新数组
                this.booksList = this.booksList.filter(item => item.id != id)
            }
        }
    })
</script>

(七) 指令 v-for的key

1. 语法: key属性 = "唯一标识"

2. 作用: 给列表项添加的唯一标识。便于Vue进行列表项的正确排序复用

v-for 的默认行为会尝试 原地修改数据 (就地复用)

<li v-for="(item,index) in xxx" :key="唯一值">

3. 注意点:

  • key的值只能是字符串或数字类型

  • key的值必须具有唯一性

  • 推荐使用 id作为 key (唯一),不推荐使用index作为 key (会变化,不对应)


(八) 指令 v-model

1. 作用:给表单元素使用,双向数据绑定 -> 可以快速获取 或 设置 表单元素内容。

1. 数据变化 -> 视图自动更新
2. 视图变化 -> 数据自动更新

2. 语法: v-model = '变量'

<div id="app">
    <!-- 
        v-model 可以快速[获取]或[设置]表单元素的内容 
    -->
    账户: <input type="text" v-model="username"><br><br>
    密码: <input type="password" v-model="password"><br><br>
    <button @click="login">登录</button>
    <button @click="reset">重置</button>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            username: '',
            password: ''
        },
        methods: {
            login() {
                console.log(this.username, this.password);
            },
            reset() {
                this.username = ''
                this.password = ''
            }
        }
    })
</script>

综合案例 - 小黑记事本

功能需求:

  1. 列表渲染

  2. 删除功能

  3. 添加功能

  4. 底部统计和清空


Vue代码

<!-- 主体区域 -->
<section id="app">
    <!-- 输入框 -->
    <header class="header">
        <h1>小黑记事本</h1>
        <input v-model="todoName" placeholder="请输入任务" class="new-todo" />
        <button @click="add" class="add">添加任务</button>
    </header>
    <!-- 列表区域 -->
    <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 @click="del(item.id)" class="destroy"></button>
                </div>
            </li>
        </ul>
    </section>
    <!-- 统计和清空 -> 如果没有任务了,底部隐藏 -> v-show-->
    <footer class="footer" v-show="list.length > 0">
        <!-- 统计 -->
        <span class="todo-count">合计: <strong> {{ list.length }} </strong></span>
        <!-- 清空 -->
        <button @click="clear" class="clear-completed">
            清空任务
        </button>
    </footer>
</section>

<!-- 底部 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    // 添加功能
    // 1.通过 v-model 绑定 输入框 -> 实时获取表单元素的内容
    // 2.点击按钮,进行新增,往数组最前面加 unshift
    const app = new Vue({
        el: '#app',
        data: {
            todoName: '',
            list: [
                { id: 1, name: '跑步一公里' },
                { id: 2, name: '跳绳200次' },
                { id: 3, name: '游泳100米' },
            ]
        },
        methods: {
            del(id) {
                this.list = this.list.filter(item => item.id !== id)
            },
            add() {
                if (this.todoName.trim() === '') {
                    alert('请输入任务名称')
                    return
                }
                this.list.unshift({
                    id: +new Date(),
                    name: this.todoName
                })
                this.todoName = ''
            },
            clear() {
                this.list = []
            }
        }
    })

</script>

七、指令补充

(一) 指令修饰符

通过 "."指明一些指令 后缀,不同后缀封装了不同的处理操作 -> 为了简化代码

1. 按键修饰
	@keyup.enter  -> 键盘回车监听
2. v-model修饰符
	v-model.trim   -> 去除首尾空格
	v-model.number   ->转数字
3.事件修饰符
	@事件名.stop	->   组织冒泡
	@事件名.prevent   -> 阻止默认行为

<div id="app">
    <h3>@keyup.enter - > 监听键盘回车事件 </h3>
    <input @keyup.enter="fn" v-model="username" type="text">
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            username: '',
        },
        methods: {
            fn() {
                console.log('键盘回车的时候触发', this.username)
            }
        }

    })
</script>

(二) v-bind操作class

v-bind对于样式控制的增强

为了方便开发者进行样式控制,Vue 扩展了 v-bind的语法,可以针对 class类名和 style行内样式进行控制。

语法:

:class="对象/数组"

1. 对象 -> 键就是类名,值是布尔值。如果值为 true,有这个类,否则没有这个类

<div class="box" :class="{ 类名1: 布尔值,类名2: 布尔值 }"></div>

适合场景:一个类名,来回切换


2. 数组 -> 数组中所有的类,都会添加到盒子上,本质就是一个 class 列表

<div class="box" :class="[ 类名1,类名2,类名3 ]"></div>

适合场景:批量添加或删除类


<div id="app">
    <div class="box" :class="{ pink: true,big: true }">黑马程序员</div>
    <div class="box" :class="['pink','pig']">黑马程序员</div>
</div>
<script src=" https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {

        }
    })
</script>

案例:京东秒杀 tab 导航高亮


核心思路:

  1. 基于数据动态渲染 tab -> v-for

  2. 准备下标记录高亮的是哪一个 tab -> activeIndex

style.css

<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>

<div id="app">
    <ul>
        <li v-for="(item,index) in list" :key="item.id" @click="activeIndex = index">
        	<a :class="{ active:index === activeIndex}" 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>

(三) v-bind操作style

语法: :style = "样式对象"

<div class="box" :style="{ CSS属性名1: CSS属性值,CSS属性名2:CSS属性值2 }"></div>

<div id="app">
    <div class="box" :style="{ width: '400px',height: '400px',backgroundColor: 'red'}"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {

        }
    })
</script>

使用场景:某个具体属性的动态设置


(四) v-model应用于其他表单元素

常见的表单元素都可以用 v-model 绑定关联 -> 快速获取 或 设置表单元素的值。

它会根据控件类型自动选取正确的方法来更新元素。


<div id="app">
    <h3>小黑学习网</h3>
    性名:
    <input type="text" v-model="username">
    <br><br>
    是否单身:
    <input type="checkbox" v-model="isSingle ">
    <br><br>
    <!-- 
        前置理解:
            1.name: 给单选框加上 name属性,可以分组 ->同一组互相会排斥
            2. value: 给单选框加上 value 属性, 用于提交给投胎的数据 
        结合 Vue -> v-model
        -->
    性别:
    <input v-model="gender" type="radio" name="gender" value="1">男
    <input v-model="gender" type="radio" name="gender" value="2">女
    <br><br>
    <!-- 
        前置理解:
            1. option 需要设置 value 值,提交给后台
            2. select 的value值,关联了选中的 option 的 value值
        结合 vue 使用 v-model
        -->
    所在城市:
    <select v-model="cityId">
        <option value="101">北京</option>
        <option value="102">上海</option>
        <option value="103">广州</option>
        <option value="104">深圳</option>
    </select>
    <br><br>

    自我描述:
    <br>
    <textarea v-model="desc"></textarea>
    <br><br>
    <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",
            cityId: '102',
            desc: ''
        }
    })
</script>

八、computed计算属性

概念:基于现在的数据,计算出来的新属性。依赖的数据变化,自动重新计算。


(一) 计算属性用法

语法:

  1. 声明在 computed 配置项中,一个计算属性对应一个函数

  2. 使用起来和普通属性一样使用 {{ 计算属性名 }}


<div id="app">
    <h3>小黑的礼物清单</h3>
    <table>
        <tr>
            <th>名字</th>
            <th>数量</th>
        </tr>
        <tr v-for="(item, index) in list" :key="item.id">
            <td>{{ item.name }}</td>
            <td>{{ item.num }}个</td>
        </tr>
    </table>

    <!-- 目标:统计求和,求得礼物总数 -->
    <p>礼物总数:{{ totalCount }} 个</p>
</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: 2 },
                { id: 3, name: '铅笔', num: 5 },
            ]
        },
        computed: {
            totalCount() {      // totalCount是一个属性
                // 基于现有的数据,编写求值逻辑  
                // 计算属性函数内部,可以直接通过 this 访问到 app 实例
                // 需求: 对 this.list 数组里面的num进行求和 -> reduce
                let total = this.list.reduce((sum, item) => sum + item.num, 0)
                return total
            }
        }
    })
</script>

(二) 计算属性与methods方法

1. computed 计算属性:

作用:封装了一段对于数据的处理,求得一个结果

语法:

1. 写在 computed配置项中

2. 作为属性,直接使用 -> this.计算属性 {{计算属性}}


2. methods 方法:

作用:给实例提供一个方法,调用以处理业务逻辑

语法:

1. 写在 methods配置项中

2. 作为方法,需要调用 -> this.方法名() {{方法名()}} @事件名 = "方法名"


3. computed 优势

在 methods 中也可以封装 computed属性,求得一个结果,不过,computed存在缓存特性.

4. 缓存特性 (提升性能):

计算属性会对计算出来的结果缓存,再次使用直接读取缓存,依赖项变化了,会自动重新计算 -> 并再次缓存

<div id="app">
    <h3>小黑的礼物清单🛒<span>{{ totalCount() }}</span></h3>
    <table>
        <tr>
            <th>名字</th>
            <th>数量</th>
        </tr>
        <tr v-for="(item, index) in list" :key="item.id">
            <td>{{ item.name }}</td>
            <td>{{ item.num }}个</td>
        </tr>
    </table>

    <p>礼物总数:{{ totalCount() }} 个</p>
</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: 2 },
                { id: 3, name: '铅笔', num: 5 },
            ]
        },
        methods: {
            totalCount() {
                console.log('计算属性执行了');  // 使用多少次就调用多少次
                let total = this.list.reduce((sum, item) => sum + item.num, 0)
                return total
            }
        }
        // computed: {
        //     // 计算属性: 有缓存的,一旦计算出来结果,就会立即缓存
        //     // 下一次读取 -> 直接读缓存就行
        //     totalCount() {
        //         console.log('计算属性执行了');  // 只会读取一次
        //         let total = this.list.reduce((sum, item) => sum + item.num, 0)
        //         return total
        //     }
        // }
    })
</script>

(三) 计算属性完整写法

计算属性默认的简写,只能读取访问,不能 "修改"

如果要"修改" -> 需要写计算属性的完整写法。


<div id="app">
    姓:<input type="text" v-model="firstName"> +
    名:<input type="text" v-model="lastName"> =
    <span> {{ funllName }} </span><br><br>
    <button @click="changeName">改名卡</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            firstName: '刘',
            lastName: '备',
        },
        methods: {
            changeName() {
                this.funllName = '吕小布'
            }
        },
        computed: {
            // 简写 -> 获取,没有配置设置的逻辑
            // funllName() {
            //     return this.firstName + this.lastName
            // }

            // 完整写法 -> 获取 + 设置
            funllName: {
                // (1) 当 fullName 计算属性,被获取求值时,执行get (有缓存,优先读取缓存)
                // 会将返回值作为求值的结果
                get() {
                    return this.firstName + this.lastName
                },
                // (2) 当fullName计算属性,被修改赋值时,执行set
                // 修改的值,传递给set方法的形参
                set(value) {
                    this.firstName = value.slice(0, 1)
                    this.lastName = value.slice(1)
                }
            }
        }
    })
</script>

综合案例 - 成绩案例



<div id="app" class="score-case">
    <div class="table">
        <table>
            <thead>
                <tr>
                    <th>编号</th>
                    <th>科目</th>
                    <th>成绩</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody v-if="list.length > 0">
                <tr v-for="(item,index) in list" :key="item.id">
                    <td> {{ index+1 }} </td>
                    <td> {{ item.subject }} </td>
                    <!-- 需求: 不及格的标红, <60 分,加上red类 -->
                    <td :class="{ red: item.score < 60 }"> {{ item.score }} </td>
                    <td><a @click.prevent="del(item.id)" href="#">删除</a></td>
                </tr>
            </tbody>
            <tbody v-else>
                <tr>
                    <td colspan="5">
                        <span class="none">暂无数据</span>
                    </td>
                </tr>
            </tbody>

            <tfoot>
                <tr>
                    <td colspan="5">
                        <span>总分:{{ totalScore }}</span>
                        <span style="margin-left: 50px">平均分:{{ averageScore }} </span>
                    </td>
                </tr>
            </tfoot>
        </table>
    </div>
    <div class="form">
        <div class="form-item">
            <div class="label">科目:</div>
            <div class="input">
                <input type="text" placeholder="请输入科目" v-model.trim="subject" />
            </div>
        </div>
        <div class="form-item">
            <div class="label">分数:</div>
            <div class="input">
                <input type="text" placeholder="请输入分数" v-model.number="score" />
            </div>
        </div>
        <div class="form-item">
            <div class="label"></div>
            <div class="input">
                <button @click="add" class="submit">添加</button>
            </div>
        </div>
    </div>
</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, subject: '语文', score: 100 },
                { id: 7, subject: '数学', score: 100 },
                { id: 12, subject: '英语', score: 100 },
            ],
            subject: '',
            score: ''
        },
        computed: {
            totalScore() {
                return this.list.reduce((sum, item) => sum + item.score, 0)
            },
            averageScore() {
                if (this.list.length === 0) {
                    return 0
                }
                return (this.totalScore / this.list.length).toFixed(2)
            }
        },
        methods: {
            del(id) {
                // console.log(id);
                this.list = this.list.filter(item => item.id != id)
            },
            add() {
                if (!this.subject) {
                    alert('请输入科目')
                    return
                }
                if (typeof this.score !== 'number') {
                    alert('请输入正确的成绩')
                    return
                }
                this.list.unshift({
                    id: +new Date(), subject: this.subject, score: this.score
                })

                this.subject = ''
                this.score = ''
            }
        }
    })
</script>

九、watch 侦听器 (监视器)

(一) watch 监视器用法

作用:监视数据变化,执行一些 业务逻辑异步操作。


语法:

  1. 简单写法 -> 简单类型数据,直接监视

  2. 完整写法 -> 添加额外配置项


<div id="app">
        <!-- 条件选择框 -->
        <div class="query">
            <span>翻译成的语言:</span>
            <select>
                <option value="italy">意大利</option>
                <option value="english">英语</option>
                <option value="german">德语</option>
            </select>
        </div>

        <!-- 翻译框 -->
        <div class="box">
            <div class="input-wrap">
                <textarea v-model="obj.words"></textarea>
                <span><i>⌨️</i>文档翻译</span>
            </div>
            <div class="output-wrap">
                <div class="transbox">{{result}}</div>
            </div>
        </div>
    </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>
        // 接口地址:https://applet-base-api-t.itheima.net/api/translate
        // 请求方式:get
        // 请求参数:
        // (1)words:需要被翻译的文本(必传)
        // (2)lang: 需要被翻译成的语言(可选)默认值-意大利
        // -----------------------------------------------
        const app = new Vue({
            el: '#app',
            data: {
                // words: ''
                obj: {
                    words: '',
                },
                result: ''
            },
            watch: {
                // js当中方法名不能出现特殊字符,必须加引号
                // newValue新值,oldValue老值 (一般不用)
                'obj.words'(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
                        console.log(res.data.data);
                    }, 300)
                }
            }
        })
    </script>

(二) watch 监视器完整写法

完整写法 -> 添加额外==配置项

  1. deep:true 对复杂类型深度监视

  2. immediate: true 初始化立即执行一次handler 方法


语法:

data: {
	obj: {
		words: '苹果',
		lang: 'italy'
	},
},

watch: {
	// watch完整写法
	数据属性名: {
		deep: true,	// 深度监视
		handler(newValue) {
			console.log(newValue)
		}
	}
}


<div id="app">
    <!-- 条件选择框 -->
    <div class="query">
        <span>翻译成的语言:</span>
        <select v-model="obj.lang">
            <option value="italy">意大利</option>
            <option value="english">英语</option>
            <option value="german">德语</option>
        </select>
    </div>

    <!-- 翻译框 -->
    <div class="box">
        <div class="input-wrap">
            <textarea v-model="obj.words"></textarea>
            <span><i>⌨️</i>文档翻译</span>
        </div>
        <div class="output-wrap">
            <div class="transbox">{{result}}</div>
        </div>
    </div>
</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>
    // 接口地址:https://applet-base-api-t.itheima.net/api/translate
    // 请求方式:get
    // 请求参数:
    // (1)words:需要被翻译的文本(必传)
    // (2)lang: 需要被翻译成的语言(可选)默认值-意大利
    // -----------------------------------------------
    const app = new Vue({
        el: '#app',
        data: {
            // words: ''
            obj: {
                words: '小黑',
                lang: 'italy'
            },
            result: ''
        },
        watch: {
            obj: {
                deep: true,     // 深度监视
                immediate: true,    // 立即执行,一进入页面 handler就立即执行一次
                handler(newValue) {
                    clearTimeout(this.timer)
                    this.timer = setTimeout(async () => {
                        const res = await axios({
                            url: 'https://applet-base-api-t.itheima.net/api/translate',
                            params: newValue
                        })
                        this.result = res.data.data
                        console.log(res.data.data);
                    }, 300)
                }
            }
        }
    })
</script>

Vue2 核心技术与实战

目录:

1. 生命周期
	生命周期 & 生命周期四个阶段 / 生命周期钩子 / 生命周期案例
2.综合案例: 小黑记账清单
	列表渲染(请求) /添加 /删除 /饼图渲染
3.工程化开发入门
	工程化开发和脚手架 /项目运行流程 /组件化 /组件注册
4.综合案例: 小兔鲜首页
	拆分模块-局部注册 /结构样式完善 /拆分组件-全局注册

一、Vue生命周期和生命周期四个阶段

思考:

1. 什么时候可以发送初始化渲染请求?  (越早越好)
2. 什么时候可以操作dom?  (至少dom得渲染出来)

Vue生命周期:一个Vue实例从创建到销毁的整个过程。

生命周期四个阶段:

1 创建    2 挂载    3 更新    4 销毁


二、Vue生命周期函数 (钩子函数)

Vue生命周期过程中,会自动运行一些函数,被称为 [生命周期钩子] —> 让开发者可以在 [特定阶段] 运行自己的代码。



<div id="app">
    <h3> {{ title }} </h3>
    <div>
        <button @click="count--">-</button>
        <span> {{ count }} </span>
        <button @click="count++">+</button>
    </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            count: 100,
            title: '计数器'
        },
        // 1.创建阶段 (准备数据)
        beforeCreate() {
            console.log('beforeCreate 响应式数据准备好之前', this.count);
        },
        created() {
            console.log('beforeCreate 响应式数据准备好之后', this.count);
        },
        // 2.挂载阶段 (渲染模板)
        beforeMount() {
            console.log('beforeMount 模块渲染之前', document.querySelector('h3').innerHTML);
        },
        mounted() {
            console.log('beforeMount 模块渲染之后', document.querySelector('h3').innerHTML);
        },
        // 3.更新阶段 (修改数据 -> 更新视图)
        beforeUpdate() {
            console.log('beforeUpdate 数据修改了,视图还没更新', document.querySelector('span').innerHTML)
        },
        updated() {
            console.log('Updated 数据修改了,视图已经更新', document.querySelector('span').innerHTML);
        },
        // 4.卸载阶段
        beforeDestroy() {
            console.log('beforeDestory');
        },
        destroyed() {
            console.log('destoryed,卸载前');
            console.log('清除掉一些Vue以外的资源占用,定时器,延时器...');
        }
    })
</script>

(一) created 应用

created:响应式数据准备好了,可以开始发送初始化渲染请求。


<div id="app">
    <ul>
        <li v-for="(item,index) 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>
    // 接口地址:http://hmajax.itheima.net/api/news
    // 请求方式:get
    const app = new Vue({
        el: '#app',
        data: {
            list: []
        },
        async created() {
            // 1. 发送请求,获取数据
            const res = await axios.get('http://hmajax.itheima.net/api/news')
            // 2.将数据更新给 data 中的 list
            this.list = res.data.data
        }
    })
</script>

(二) mounted 应用

mounted:模块渲染完成,可以开始操作DOM了

要求:获取焦点


<div class="container" id="app">
    <div class="search-container">
        <img src="https://www.itheima.com/images/logo.png" alt="">
        <div class="search-box">
            <input type="text" v-model="words" id="inp">
            <button>搜索一下</button>
        </div>
    </div>
</div>

<script src=" https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            words: ''
        },
        // 核心思路:
        // 1.等输入框渲染出来
        // 2.让输入框获取焦点
        mounted() {
            // console.log(document.querySelector('#inp'));
            document.querySelector('#inp').focus()
        }
    })
</script>

案例 - 小黑记账清单

<div id="app">
    <div class="contain">
        <!-- 左侧列表 -->
        <div class="list-box">

            <!-- 添加资产 -->
            <form class="my-form">
                <input v-model.trim="name" type="text" class="form-control" placeholder="消费名称" />
                <input v-model.number="price" type="text" class="form-control" placeholder="消费价格" />
                <button @click="add" type="button" class="btn btn-primary">添加账单</button>
            </form>

            <table class="table table-hover">
                <thead>
                    <tr>
                        <th>编号</th>
                        <th>消费名称</th>
                        <th>消费价格</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    <tr 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 @click="del(item.id)" href="javascript:;">删除</a></td>
                    </tr>
                </tbody>
                <tfoot>
                    <tr>
                        <td colspan="4">消费总计: {{ totalPrice.toFixed(2) }}</td>
                    </tr>
                </tfoot>
            </table>
        </div>

        <!-- 右侧图表 -->
        <div class="echarts-box" id="main"></div>
    </div>
</div>
<script src="echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
    /**
     * 接口文档地址:
     * https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
     * 
     * 功能需求:
     * 1. 基本渲染
     *  (1) 立刻发送请求获取数据 created
     *  (2) 拿到数据,存到data的响应式数据中
     *  (3) 结合数据,进行渲染 v-for
     *  (4) 消费统计 => 计算属性
     * 2. 添加功能
     *  (1) 收集表单数据 v-model
     *  (2) 给添加按钮注册点击事件,发送添加请求
     *  (3) 需不需要重新渲染?  需要。
     * 3. 删除功能
     *  (1) 注册点击事件,传参(传id)
     *  (2) 根据 id 发送删除请求
     * 4. 饼图渲染
     *  (1) 初始化一个饼图
     *  (2) 根据数据实时更新饼图    echarts.setOption({ ... })
     *  
     */
    const app = new Vue({
        el: '#app',
        data: {
            list: [],
            name: '',
            price: ''
        },
        computed: {
            totalPrice() {
                return this.list.reduce((sum, item) => sum + item.price, 0)
            }
        },
        created() {
            this.getList()
        },
        mounted() {
            this.myChart = echarts.init(document.querySelector('#main'))
            this.myChart.setOption({
                // 大标题
                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)'
                            }
                        }
                    }
                ]
            })
        },
        methods: {
            async getList() {
                const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
                    params: {
                        creator: '小黑'
                    }
                })
                this.list = res.data.data

                // 更新图表
                this.myChart.setOption({
                    // 数据项
                    series: [
                        {
                            data: this.list.map(item => ({ value: item.price, name: item.name }))
                        }
                    ]
                })
            },
            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: '小黑',
                    name: this.name,
                    price: this.price
                })
                // 重新渲染一次
                this.getList()
                this.name = ''
                this.price = ''
            },
            async del(id) {
                // 根据 id 发送删除请求
                const res = await axios.delete(`https://applet-base-api-t.itheima.net/bill/${id}`)
                // 重新渲染
                this.getList()
            }
        }
    })
</script>

工程化开发入门

开发 Vue 的两种方式:

1. 核心包传统开发模式: 基于 html/css/js 文件,直接引入核心包,开发 Vue。
2. 工程化开发模式: 基于构建工具 (例如:webpack) 的环境中开发 Vue。


问题:

1. webpack 配置不简单
2. 雷同的基础配置
3. 缺乏统一标准

因此需要一个工具,生成标准化的配置 (Vue CLI)


一、脚手架Vue CLI

基本介绍:

Vue CLI 是Vue官方提供的一个全局命令工具

可以帮助我们快速创建一个开发Vue项目的标准化基础架子。【集成了webpack配置】


好处:

  1. 开箱即用,零配置

  2. 内置babel等工具

  3. 标准化的webpack配置

使用步骤:

  1. 按 win+r ,输入 cmd,按回车

  2. 输入:npm config set registry https://registry.npm.taobao.org,按回车

  3. 全局安装(只需安装一次即可) yarn global add @vue/cli 或者 npm i @vue/cli -g

  4. 查看vue/cli版本:vue --version

  5. 创建项目架子:vue create project-name(项目名不能使用中文)

  6. 启动项目:yarn serve 或者 npm run serve(命令不固定,找package.json)


二、项目目录介绍和运行流程

1.项目目录介绍

虽然脚手架中的文件有很多,目前咱们只需认识三个文件即可

  1. main.js 入口文件

// 作用:导入App.vue,基于App.vue创建结构渲染index.html 
// 1.导入 Vue 核心包
import Vue from 'vue'
// 2. 导入App.vue根组件
import App from './App.vue'
// 提示:当前处于什么环境 (生产环境 / 开发环境)
Vue.config.productionTip = false	// false 无提示

// 3. Vue 实例化,提供render方法 -> 基于 App.vue创建结构渲染index.html
new Vue({
 // el: '#app',作用:和 $mount('选择器')作用一致,用于指定Vue所管理容器
 //简化写法: render: h => h(App),
 // 完整写法:
 // 基于App创建元素结构,将App.vue渲染到 index.html容器中
 render:(createElement) => {
 	return createElement(App)
 }
}).$mount('#app')		

   2.  App.vue App根组件

    3. index.html 模板文件

<body>
  <!-- 兼容:给不支持 js的浏览器一个提示 -->
  <noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
        Please enable it to continue.</strong>
  </noscript>
 
   <!-- Vue所管理的容器,将来创建结构动态渲染这个容器 -->
  <div id="app">
  <!-- 工程化开发模式中:这里不再直接编写模板语法,通过App.vue提供结构渲染 -->
  </div>
  <!-- built files will be auto injected -->
</body>

2.运行流程

三、组件化开发

组件化: 一个页面 可以拆分成一个个组件,每个组件有着自己独立的结构、样式、行为。

好处:便于维护,利于复用 → 提升开发效率。

组件分类:普通组件、根组件。

比如:下面这个页面,可以把所有的代码都写在一个页面中,但是这样显得代码比较混乱,难易维护。咱们可以按模块进行组件划分


四、根组件 App.vue

1.根组件介绍

整个应用最上层的组件,包裹所有普通小组件


2.组件是由三部分构成


  • 语法高亮插件

  • 三部分构成

    • template:结构 (有且只能一个根元素)

    • script: js逻辑

    • style: 样式 (可支持less,需要装包)

  • 让组件支持less

    (1) style标签,lang="less" 开启less功能

    (2) 装包: yarn add less less-loader -D 或者npm i less less-loader -D


五、普通组件的注册使用


组件注册的两种方式:

1. 局部注册:只能在注册的组件内使用
   + 创建 .vue 文件 (三个组成部分)
   + 在使用的组件内导入并注册
2. 全局注册:所有组件内都能使用

(一) 局部注册

特点

只能在注册的组件内使用

步骤

  1. 创建.vue文件(三个组成部分)

  2. 在使用的组件内先导入再注册,最后使用

使用方式

当成html标签使用即可 <组件名></组件名>

注意

组件名规范 —> 大驼峰命名法, 如 HmHeader

语法


// 导入需要注册的组件
import 组件对象 from '.vue文件路径'
import HmHeader from './components/HmHeader'

export default {  // 局部注册
  components: {
   '组件名': 组件对象,
    HmHeader:HmHeaer,
    HmHeader
  }
}

6.练习

在App组件中,完成以下练习。在App.vue中使用组件的方式完成下面布局


<template>
  <div class="app">
    <!-- 头部组件 -->
    <HmHeader></HmHeader>
    <!-- 主体组件 -->
    <HmMain></HmMain>
    <!-- 底部组件 -->
    <HmFooter></HmFooter>
  </div>
</template>

<script>
import HmHeader from './components/HmHeader.vue';
import HmMain from './components/HmMain.vue';
import HmFooter from './components/HmFooter.vue';
export default {
  components: {
    HmHeader: HmHeader,
    HmMain: HmMain,
    HmFooter:HmFooter
  }
}
</script>

将下方组件写入到components文件夹内


<template>
  <div class="hm-header">
    我是hm-header
  </div>
</template>

<script>
export default {

}
</script>

<style>
.hm-header {
  height: 100px;
  line-height: 100px;
  text-align: center;
  font-size: 30px;
  background-color: #8064a2;
  color: white;
}
</style>

<template>
  <div class="hm-main">
    我是hm-main
  </div>
</template>

<script>
export default {

}
</script>

<style>
.hm-main {
  height: 400px;
  line-height: 400px;
  text-align: center;
  font-size: 30px;
  background-color: #f79646;
  color: white;
  margin: 20px 0;
}
</style>

<template>
  <div class="hm-footer">
    我是hm-footer
  </div>
</template>

<script>
export default {

}
</script>

<style>
.hm-footer {
  height: 100px;
  line-height: 100px;
  text-align: center;
  font-size: 30px;
  background-color: #4f81bd;
  color: white;
}
</style>

如果 HmFooter + tab 出不来 (< HmFooter>< /HmFoooter>) -> 需要配置 vscode ,在设置中搜索 trigger on tab -> 勾选第一个


(二) 全局注册

特点:

全局注册的组件,在项目的 任何组件 中都能使用

步骤

  • 创建.vue组件(三个组成部分)


  • main.js中进行全局注册


使用方式

当成HTML标签直接使用

<组件名></组件名>

​​​​​​​


注意

组件名规范 —> 大驼峰命名法, 如 HmHeader

5.语法

Vue.component('组件名', 组件对象)

例:

// 导入需要全局注册的组件,往代码的顶部编写(规范)
import HmButton from './components/HmButton'
// 进入全局注册 -> 在所有的组件范围内都能直接使用
Vue.component('HmButton', HmButton)

6.练习

在以下3个局部组件中是展示一个通用按钮

HmFooter.vue


<template>
  <button class="hm-button">通用按钮</button>
</template>
​
<script>
export default {
​
}
</script>

main.js

import Vue from 'vue'
import App from './App.vue'
​// 导入全局注册的组件
import HmButton from './components/HmButton'

vue.config.productionTip = false
​
// 进行全局注册
Vue.component('HmButton',HmButton)
​
new Vue({
    render: (createElement) => {
        // 基于App创建元素结构
        return createElement(App)
    }
})

HmHeader.vue

<template>
    <div class="hm-header">
       我是hm-header
    <HmButton></HmButton>
</template>
​
<script>
   export default {
    
}
</script>

HmMain.vue

<template>
    <div class="hm-main">
       我是hm-main
    <HmButton></HmButton>
</template>
​
<script>
   export default {
    
}
</script>

目录

1. 组件的三大组成部分 (结构 /样式 /逻辑)
	scoped样式冲突 / data是一个函数
2. 组件通信
	组件通信语法 /父传子 /子传父 /非父子(扩展)
3. 综合案例:小黑记事本(组件版)
	拆分组件 /渲染 /添加 /删除 /统计 /清空 /持久化
4. 进阶语法
	v-model原理	/v-model应用于组件 /sync修饰符 /ref 和 $refs /$nextTick

六、组件注意点说明

组件的三大组成部分


(一) scoped样式冲突

默认情况

写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。

  1. 全局样式: 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响

  1. 局部样式: 可以给组件加上scoped 属性,可以让样式只作用于当前组件


代码演示

BaseOne.vue

<template>
  <div class="base-one">
    BaseOne
  </div>
</template>

<script>
export default {

}
</script>
<style scoped>
</style>

BaseTwo.vue

<template>
  <div class="base-one">
    BaseTwo
  </div>
</template>

<script>
export default {

}
</script>

<style scoped>
</style>

App.vue

<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>

scoped原理

  1. 当前组件内标签都被添加data-v-hash值 的属性

  2. css选择器都被添加 [data-v-hash值] 的属性选择器

最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到


(二) data是一个函数

data为什么要写成函数

一个组件的 data 选项必须是一个函数。目的是为了:保证每个组件实例,维护独立的一份数据对象

每次创建新的组件实例,都会重新执行一次data 函数,得到一个新对象。


代码演示

BaseCount.vue

<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

<template>
  <div class="app">
    <BaseCount></BaseCount>
  </div>
</template>

<script>
import BaseCount from './components/BaseCount'
export default {
  components: {
    BaseCount,
  },
}
</script>

<style>
</style>

组件通信

一、什么是组件通信?

组件通信,就是指 组件与组件 之间的 数据传递

  • 组件的数据是独立的,无法直接访问其他组件的数据。

  • 想使用其他组件的数据,就需要组件通信


组件之间如何通信


思考:

  1. 组件之间有哪些关系?

  2. 对应的组件通信方案有哪几类?


二、组件关系分类

  1. 父子关系

  2. 非父子关系



通信解决方案


三、父子通信流程

  1. 父组件通过 props 将数据传递给子组件

  2. 子组件利用 $emit 通知父组件修改更新


父向子传值步骤

  1. 给子组件以添加属性的方式传值

  2. 子组件内部通过props接收

  3. 模板中直接使用 props接收的值


父向子通信代码示例

父组件通过 props 将数据传递给子组件

父组件App.vue

<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

<template>
  <div class="son" style="border:3px solid #000;margin:10px">
    我是Son组件 {{ title }}
  </div>
</template>

<script>
export default {
 // 2.通过props进行接收
    props: ['title']
}
</script>

<style>

</style>

子向父传值步骤

  1. $emit触发事件,给父组件发送消息通知

  2. 父组件监听$emit触发的事件

  3. 提供处理函数,在函数的性参中获取传过来的参数


子向父通信代码示例

子组件利用 $emit 通知父组件,进行修改更新


父组件App.vue

<template>
  <div class="app" style="border: 3px solid #000; margin: 10px">
    我是APP组件
    <!-- 2.父组件监听事件 -->
    <Son :title="myTitle" @changeTitle="changeFn"></Son>
  </div>
</template>

<script>
import Son from './components/Son.vue'
export default {
  name: 'App',
  data() {
    return {
      myTitle: '学前端,就来黑马程序员',
    }
  },
  methods: {
  	// 3. 提供处理函数,形参中获取参数
  	changeFn(newTitle) {
  		this.myTitle = newTitle
  	}
  }
</script>

<style>
</style>

子组件 Son.vue:

<template>
  <div class="son" style="border:3px solid #000;margin:10px">
    我是Son组件 {{ title }}
  </div>
</template>

<script>
export default {
    props: ['title']
    methods: {
    	handleClick(){
    		// 1. 通过$emit触发事件,给父组件发送消息通知
    		this.$emit('changeTitle','传智教育')	// 传智教育(实参)传给父组件
    	}
    }
}
</script>

<style>

</style>

四、props详解

1. Props 定义

组件上 注册的一些 自定义属性


2. Props 作用

向子组件传递数据


3. 特点

  1. 可以 传递 任意数量 的prop

  2. 可以 传递 任意类型 的prop


4. 代码演示

父组件App.vue

<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

<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>

props校验

1. 思考

组件的props可以乱传吗

2. 作用

为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误

3. 语法

  • 类型校验

  • 非空校验

  • 默认值

  • 自定义校验


4. 代码演示

App.vue

<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

<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>

props校验完整写法

1.语法

props: {
  校验的属性名: {
    type: 类型,  // Number String Boolean ...
    required: true, // 是否必填
    default: 默认值, // 默认值
    validator (value) {
      // 自定义校验逻辑
      return 是否通过校验
    }
  }
},

2. 代码实例

<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.注意

1.default 和 required 一般不同时写(因为当时必填项时,肯定是有值的)

2.default 后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值


四、props&data、单向数据流

1.共同点

都可以给组件提供数据


2.区别

  • data 的数据是 自己 的 → 随便改

  • prop 的数据是 外部 的 → 不能直接改,要遵循 单向数据流


3.单向数据流:

父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的


4.代码演示

App.vue

<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

<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>


五、非父子通信-event bus 事件总线

1.作用

非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)

2.步骤

  1. 创建一个都能访问的事件总线 (空Vue实例)—> utils/EventBus.js

    import Vue from 'vue'
    const Bus = new Vue()
    export default Bus

  2. A组件(接受方),监听Bus的 $on事件

    created () {
      Bus.$on('sendMsg', (msg) => {
        this.msg = msg
      })
    }

  3. B组件(发送方),触发Bus的$emit事件

    Bus.$emit('sendMsg', '这是一个消息')

3.代码示例

EventBus.js

import Vue from 'vue'
const Bus  =  new Vue()
export default Bus

BaseA.vue(接受方)

<template>
  <div class="base-a">
    我是A组件(接收方)
    <p>{{msg}}</p>  
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
}
</script>

<style scoped>
.base-a {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

BaseB.vue(发送方)

<template>
  <div class="base-b">
    <div>我是B组件(发布方)</div>
    <button>发送消息</button>
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
}
</script>

<style scoped>
.base-b {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

App.vue

<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>

六、非父子通信-provide&inject

1.作用

跨层级共享数据


2.场景


3.语法

  1. 父组件 provide提供数据

export default {
  provide () {
    return {
       // 普通类型【非响应式】
       color: this.color, 
       // 复杂类型【响应式】
       userInfo: this.userInfo, 
    }
  },
    data () {
        return {
            color: 'pink',	// 简单类型
            userInfo: {		// 复杂类型
                name: 'zs',
                age: 19
            }
        }
    }
}

2.子/孙组件 inject获取数据

export default {
  inject: ['color','userInfo'],
  created () {
    console.log(this.color, this.userInfo)
  }
}

4.注意

  • provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)

  • 子/孙组件通过inject获取的数据,不能在自身组件内修改


v-model原理

1.原理:

v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写


2.作用:

提供数据的双向绑定

  • 数据变,视图跟着变 :value

  • 视图变,数据跟着变 @input


3.注意

$event 用于在模板中,获取事件的形参


4.代码示例

<template>
  <div class="app">
    <input v-model="msg1" type="text"  />
    <br /> 
      <!-- 模块中获取事件的形参, —> $event 获取 -->
    <input :value="msg2" @click="msg2 = $event.target.value" type="text" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg1: '',
      msg2: '',
    }
  },
}
</script> 
<style>
</style>

5.v-model使用在其他表单元素上的原理

不同的表单元素, v-model 在底层的处理机制是不一样的。比如给checkbox 使用 v-model

底层处理的是 checked属性和change事件。

不过咱们只需要掌握应用在文本框上的原理即可


一、表单类组件封装

父传子: 数据 应该是父组件 props 传递 过来的,v-model 拆解 绑定数据


子传父: 监听输入,子传父传值给父组件修改


需求目标

实现 子组件父组件 数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)


代码演示

App.vue

<template>
  <div class="app">
      :cityId="selectId"
      @changeId = "selectId = $event"
    <BaseSelect></BaseSelect>
  </div>
</template>

<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
  data() {
    return {
      selectId: '102',
    }
  },
  components: {
    BaseSelect,
  },
}
</script>

<style>
</style>

BaseSelect.vue

<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>

二、v-model简化代码

1.目标:

父组件通过v-model 简化代码,实现子组件和父组件数据 双向绑定


2.如何简化:

v-model其实就是 :value和@input事件的简写

  • 子组件:props通过value接收数据,事件触发 input

  • 父组件:v-model直接绑定数据


3.代码示例

子组件

<select :value="value" @change="handleChange">...</select>
props: {
  value: String
},
methods: {
  handleChange (e) {
    this.$emit('input', e.target.value)
  }
}

父组件

<BaseSelect v-model="selectId"></BaseSelect>

.sync修饰符

1.作用

可以实现 子组件父组件数据双向绑定,简化代码

简单理解:子组件可以修改父组件传过来的props值


2.特点

prop属性名,可以自定义,非固定为 value


3.场景

封装弹框类的基础组件, visible属性 true显示 false隐藏


4.本质

.sync修饰符 就是 :属性名 和 @update:属性名 合写

5.语法

父组件

//.sync写法
<BaseDialog :visible.sync="isShow" />
--------------------------------------
//完整写法
<BaseDialog 
  :visible="isShow" 
  @update:visible="isShow = $event" 
/>

子组件

props: {
  visible: Boolean
},

this.$emit('update:visible', false)

6.代码示例

App.vue

<template>
  <div class="app">
    <button @click="openDialog">退出按钮</button>
    <BaseDialog :isShow="isShow"></BaseDialog>
  </div>
</template>

<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
  data() {
    return {
      isShow: false,
    }
  },
  components: {
    BaseDialog,
  },
}
</script>

<style>
</style>

BaseDialog.vue

<template>
  <div class="base-dialog-wrap" v-show="isShow">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button class="close">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button>确认</button>
        <button>取消</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    isShow: Boolean,
  }
}
</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>

ref和$refs

1.作用

利用 ref$refs 可以用于 获取 dom 元素组件实例


2.特点:

查找范围 → 当前组件内(更精确稳定)

获取 dom:

  • 目标标签 - 添加 ref 属性

<div ref="chartRef">我是渲染图表的容器</div>
  • 恰当时机,通过 this.$refs.xxx(ref名),获取目标标签

mounted(){
	console.log(this.$refs.chartRef)
},

代码示例

App.vue

<template>
  <div class="app">
    <BaseChart></BaseChart>
  </div>
</template>

<script>
import BaseChart from './components/BaseChart.vue'
export default {
  components:{
    BaseChart
  }
}
</script>

<style>
</style>

BaseChart.vue

<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>

获取组件:

  • 目标组件 - 添加 ref 属性

<BaseForm ref="baseForm"></BaseForm>
  • 恰当时机,通过 this.$refs.xxx,获取目标组件,就可以调用组件对象里面的方法

this.$refs.baseForm.组件方法()

代码实例:

App.vue

<template>
  <div class="app">
    <BaseForm></BaseForm>
    <button @click="handleGet">获取数据</button>
    <button @click="handleSet>重置数据</button>
  </div>
</template>

<script>
import BaseForm from './components/BaseForm.vue';
export default {
  data () {
    return {

    }
  },
  methods: {
    handleGet (){
      console.log(this.$refs.baseForm);
    },
    handleReset () {
      this.$refs.baseForm.resetValues()
    }
  },
  components: {
      BaseForm
  }
}
</script>

BaseForm.vue

<template>
    <form action="">
        账户:<input type="text" v-model="account">
        密码: <input type="password" v-model="password">
    </form>
</template>

<script>
export default {
    data (){
        return {
            account: '',
            password: ''
        }
    },
    methods: {
        // 方法1: 收集表单数据,返回一个对象
        getValues () {
            return {
                account: this.account,
                password: this.password 
            }
        },
        // 方法2: 重置表单
        resetValues () {
            this.account = ''
            this.password = ''
        }
    }
}
</script>

异步更新 & $nextTick

1.需求

编辑标题, 编辑框自动聚焦

  1. 点击编辑,显示编辑框

  2. 让编辑框,立刻获取焦点

this.isShowEdit = true  // 显示输入框
this.$refs.inp.focus()	// 获取焦点


2.代码实现

<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() {
        // 1.显示输入框 (异步dom更新)
        this.isShowEdit = true  
        // 获取焦点
        // this.$refs.inp.focus() 
        // $nextTick等dom更新完,立刻执行准备的函数体
        this.$nextTick(() => {
  			this.$refs.inp.focus()
		})
    }  
  },
}
</script> 

3.问题

"显示之后",立刻获取焦点是不能成功的!

原因:Vue 是异步更新DOM (提升性能)


4.解决方案

$nextTick:等 DOM更新后,才会触发执行此方法里的函数体

语法: this.$nextTick(函数体)

this.$nextTick(() => {
  this.$refs.inp.focus()
})

注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例


目录:

自定义指令
	+ 基本语法(全局&局部注册) / 指令的值 / v-loading指令封装
插槽
	+ 默认插槽 / 后备内容 / 具名插槽 / 作用域插槽
综合案例: 商品列表
	MyTag 组件封装 / MyTable组件封装
路由入门
	单页应用程序 / 路由概念 / VueRouter的基本使用 / 组件目录存放问题

自定义指令


自定义指令: 自己定义的指令,可以封装一些 dom 操作,扩展额外功能。

需求:当页面加载时,让元素将获得焦点(autofocus 在safari浏览器有兼容性)

操作 dom: dom元素.focus()

mounted() {
	this.$refs.inp.focus()
}

一、全局注册 - 语法

Vue.directive('指令名',{
	"inserted" (el) {
		// 可以对 el标签,扩展额外功能
		el.focus()
	}
})

二、局部注册 - 语法

directives: {
	"指令名": {
		inserted () {
			// 可以对 el 标签,扩展额外功能
			el.focus()
		}
	}
}


使用:

<input v-指令名 type="text">

全局注册

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 全局注册指令
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
})

new Vue({
  render: h => h(App),
}).$mount('#app')

<template>
  <div>
    <h1>自定义指令</h1>
    <input v-focus type="text">
  </div>
</template>

<script>
export default {
	
}
</script>

<style>

</style>

局部注册

<template>
  <div>
    <h1>自定义指令</h1>
    <input v-focus type="text">
  </div>
</template>

<script>
export default {
  directives: {
    focus: {
     inserted (el) {
      el.focus()
     }
    }
  }
}
</script>

<style>

</style>

三、自定义指令 - 指令的值

需求:实现一个 color 指令 - 传入不同的颜色,给标签设置文字颜色

  • 语法:在绑定指令时,可以通过 "等号" 的形式为指令绑定 具体的参数值

<div v-color="color">我是内容</div>
  • 通过 binding.value可以拿到指令值,指令值修改会 触发 updata 函数

directives: {
	color: {
		inserted (el,binding) {
			el.style.color = binding.value
		},
		updata (el,binding) {
			el.style.color = binding.value
		}
	}
}

<template>
  <div>
    <h1 v-color="color1">指令的值1测试</h1>
    <h1 v-color="color2">指令的值2测试</h1>
  </div>
</template>

<script>
export default {
  data () {
    return {
      color1: 'red',
      color2: 'green'
    }
  },
  directives: {
    color: {
      // 1. inserted 提供的是元素被添加到页面中时触发
      inserted (el,binding) {
        el.style.color = binding.value
      },
      // 2. update 指令的值被修改的时候触发
      update () {
          el.style.color = binding.value
      }
    }
  }
}
</script>

<style>

</style>

四、自定义指令 v-loading

场景:实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好

需求:封装一个 v-loading 指令,实现加载中的效果

分析:

  1. 本质 loading 效果就是一个蒙层,盖在了盒子上

  2. 数据请求中,开启 loading状态,添加蒙层

  3. 数据请求完毕,关闭 loading状态,移除蒙层


实现:

  1. 准备一个 loading 类,通过伪元素定位,设置宽高,实现蒙层

  2. 开启关闭 loading 状态 (添加移除蒙层),本质只需要添加移除类即可

  3. 结合自定义指令的语法进行封装复用

.loading:before {
	content: "",
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	background: #fff url("./loading.gif") no-repeat center;
}

App.vue

<template>
  <div class="main">
    <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>
  </div>
</template>

<script>
// 安装axios =>  yarn install axios
import axios from 'axios'

// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
  data() {
    return {
      list: [],
      isLoading: true
    }
  },
  async created() {
    // 1. 发送请求获取数据
    const res = await axios.get('http://hmajax.itheima.net/api/news')

    setTimeout(() => {
      // 2. 更新到 list 中
      this.list = res.data.data
      this.isLoading = false
    }, 2000)
  },
  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')
      }
    }

  }
}
</script>

<style>
/* 伪类 - 蒙层效果 */
.loading:before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url('./loading.gif') no-repeat center;
}

/* .box2 {
  width: 400px;
  height: 400px;
  border: 2px solid #000;
  position: relative;
} */

.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>

插槽

插槽分类:

  1. 默认插槽:组件内定制一处结构

  2. 具名插槽:组件内定制多处结构


一、默认插槽

作用:让组件内部的一些结构支持自定义

需求:要在页面中显示一个对话框,封装成一个组件

问题:组件的内部部分,不希望写死,希望能使用的时候自定义。怎么办?


插槽基本语法:

  1. 组件内需要定制的结构部分,改用<slot></slot>占位

  2. 使用组件时,<MyDialog></MyDialog>标签内部,传入结构替换 slot


App.vue

<template>
  <div>
    <!-- 2.在使用组件时,组件标签内填入内容 -->
    <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>

MyDialog.vue

<template>
    <div class="dialog">
        <div class="dialog-header">
            <h3>友情提示</h3>
            <span class="close">✖️</span>
        </div>

        <div class="dialog-content">
            <!-- 1. 在需要定制的位置,使用 slot 占位 -->
            <slot></slot>
        </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>

二、后备内容 (默认内容)

通过插槽完成了内容的定制,传什么显示什么,但是如果不传,则是空白

能否给插槽设置 默认显示内容呢?

插槽后备内容:封装组件时,可以为预留的 < slot >插槽提供后备内容 (默认内容)。

  • 语法:在< slot >标签内,放置内容,作为默认显示内容

  • 效果:

    • 外部使用组件时,不传东西,则 slot 会显示后备内容

      <MyDialog></MyDialog>

    • 外部使用组件时,传东西了,则 slot整体会被换掉

      <MyDialog>我是内容</MyDialog>


APP.vue

<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>

MyDialog.vue

<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>

<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>

三、具名插槽

需求:一个组件内有多处结构,需要外部传入标签,进行定制

默认插槽:一个的定制位置

具名插槽语法:

  1. 多个 slot 使用 name属性 区分名字

<div class="dialog-header">
	<slot name="head"></slot>
</div>
<div class="dialog-center">
	<slot name="content"></slot>
</div>
<div class="dialog-footer">
	<slot name="footer"></slot>
</div>
  1. template 配合 v-slot: 名字来分发对应标签

<MyDialog>
      <template v-slot:head>
        大标题
      </template>
      <template v-slot:content>
          内容文本
      </template>
      <template v-slot:footer>
          <button>按钮</button>
      </template>
    </MyDialog>


  1. v-slot:插槽名可以简化成 #插槽名


App.vue

<template>
  <div>
    <MyDialog>
        <template #head>
          <div>大标题</div>
        </template>
        <template #content>
           <div>内容文本</div>
        </template>
        <template #footer>
            <button>按钮</button>
        </template>
      </MyDialog>
  </div>
</template>

<script>
import MyDialog from './components/MyDialog.vue'
export default {
  data() {
    return {

    }
  },
  components: {
    MyDialog
  }
}
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

MyDialog.vue

<template>
    <div class="dialog">
        <div class="dialog-header">
            <slot name="head"></slot>
        </div>

        <div class="dialog-content">
            <slot name="content"></slot>
        </div>
        <div class="dialog-footer">
            <slot name="footer"></slot>
        </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>

四、作用域插槽

作用域插槽:定义 slot插槽 的同时,是可以传值的。给插槽上可以绑定数据,将来使用组件时可以用。

场景:封装表格组件


  1. 父传子,动态渲染表格内容

  2. 利用默认插槽,定制操作列

  3. 删除或查看都需要用到当前项的 id,属于组件内部的数据

    通过作用域插槽传值绑定,进而使用


基本使用步骤:

  1. 给 slot 标签,以添加属性的方式传值

<slot :id="item.id" msg="测试文本"></slot>
  1. 所有添加的属性,都会被收集到一个对象中

{ id: 3, msg: '测试文本'}
  1. 在 template中,通过 #插槽名 = "obj"接收,默认插槽名为 default

<MyTable :list="list">
      <template #default="obj">
        <button @click="del(obj.id)">删除</button>
      </template>
</MyTable>

App.vue

<template>
  <div>
    <MyTable :data="list">
      <!-- 3. 通过template #插槽名="命令名"接收 -->
      <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.name};年龄: ${row.age}`)
    }
  },
  components: {
    MyTable
  }
}
</script>

MyTable.vue

<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>
                    <!-- 1. 给slot标签,添加属性的方式传值 -->
                    <slot :row="item" msg="测试文本"></slot>
                    <!-- 2. 将所有的属性,添加到一个对象中 -->
                    <!-- 
                        {
                            row: { id: 2, name: '孙大明', age: 19},
                            msg: '测试文本'
                        }
                     -->
                </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>

单页应用程序 SPA

  • 单页面应用 (SPA): 所有功能在 一个html页面 上实现

  • 具体示例:网易云音乐 网易云音乐



单页面与多页面的区别

开发分类实现方式页面性能开发效率用户体验学习成本首屏加载SEO
单页一个html页面按需更新性能高非常好
多页多个html页面整页更新性能低一般中等

推荐:

单页面应用:系统类网站 / 文档类网站 / 移动端站点

多页面应用: 公司官网 / 电商类网站


思考:
	单页面应用程序,之所以开发效率高,性能高,用户体验好
	最大的原因就是:页面按需更新
	要按需更新,首先就需要明确:访问路径 和 组件 的对应关系!
	访问路径 和 组件的对应关系如何确定呢? 路由

路由

一、路由的介绍

生活中的路由:设备和ip的映射关系


Vue中的路由:路径和组件的映射关系


二、VueRouter的介绍

目标:认识插件 VueRouter,掌握 VueRouter的基本使用步骤

作用:修改地址栏路径时,切换显示匹配的组件

说明:Vue官方的一个路由插件,是一个第三方包

官网:Vue Router


三、VueRouter的使用 (5 + 2)

Vue2和vue3版本区别:


5个基本步骤 (固定)

  1. 下载:下载 VueRouter 模块到当前工程,版本 3.6.5

npm install vue-router@3.6.5
  1. 引入

import VueRouter from 'vue-router'
  1. 安装注册

Vue.use(VueRouter)
  1. 创建路由对象

const router = new VueRouter()
  1. 注入,将路由对象注入到 new Vue 实例中,建立关联

new Vue({
	render: h => h(App),
	router
}).$mount('#app')


2个核心步骤

  1. 创建需要的组件 (views目录),配置路由规则

Find.vue	My.vue		Friend.vue


import Find from './views/Find.vue'
import My from './views/My.vue'
import Friend from './views/Friend.vue'

const router = new VueRouter({
	routes: [
	  { path: '/find',component: Find },
	  { path: '/my', component: My},
	  { path: '/friend', component: Friend},
	]
})

  1. 配置导航,配置路由出口 (路径匹配的组件显示的位置)


<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>

APP.vue

<template>
  <div>
     
    <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>
  </div>
</template>

<script>
export default {};
</script>

<style>
body {
  margin: 0;
  padding: 0;
}

.footer_wrap {
  position: relative;
  left: 0;
  top: 0;
  display: flex;
  width: 100%;
  text-align: center;
  background-color: #333;
  color: #ccc;
}

.footer_wrap a {
  flex: 1;
  text-decoration: none;
  padding: 20px 0;
  line-height: 20px;
  background-color: #333;
  color: #ccc;
  border: 1px solid black;
}

.footer_wrap a:hover {
  background-color: #555;
}</style>

main.js

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'

import Find from './views/Find'
import My from './views/My'
import Friend from './views/Friend'
Vue.use(VueRouter)

const router = new VueRouter({
  // routes: 路由规则集合
  // route: 一条路由规则 { path: 路径, component: 组件}
  routes: [
    { path: '/find', component: Find },
    { path: '/my', component: My },
    { path: '/friend', component: Friend },
  ]
})
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 全称: router: router
  router
}).$mount('#app')

Find.vue

<template>
    <div>
        <p>发现音乐</p>
        <p>发现音乐</p>
        <p>发现音乐</p>
        <p>发现音乐</p>
        <p>发现音乐</p>
    </div>
</template>

<script>
export default {
    name: 'FindMusic'
}
</script>

<style>

</style>

另外两个相同。


四、组件存放目录问题

注意:.vue文件本质无区别

路由相关的组件,为什么放在 views 目录呢?


组件分类:.vue 文件分 2类,页面组件 & 复用组件


分类开来更易维护

  • src/views文件夹

    • 页面组件 - 页面展示 - 配合路由用

  • src/components文件夹

    • 复用组件 - 展示数据 - 常用于复用


目录

路由进阶
	1.路由模块封装
	2.声明式导航 & 导航高亮 /精确匹配&模糊匹配 /自定义高亮类目
		声明式导航传参 (查询参数传参 & 动态路由传参)
	3.路由重定向 /路由404 /路由模式
	4.编程式导航 / 编程式导航传参 (查询参数传参 & 动态路由传参)
综合案例: 面经基础版
	一级路由 / 二级路由 / 导航高亮 / 请求渲染 / 路由传参 / 缓存组件

路由进阶

一、路由的封装抽离

问题:所有的路由配置都堆在 main.js 中合适么?

目标:将路由模块抽离出来。好处:拆分模块,利于维护



  1. 新建 router文件夹,然后新建 index.js

import Find from '@/views/Find'    // @ 代表当前的src目录 (脚手架情况下)
import My from '@/views/My'
import Friend from '@/views/Friend'

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)	// VueRouter插件初始化
// 创建了一个路由对象
const router = new VueRouter({
    // routes: 路由规则集合
    // route: 一条路由规则 { path: 路径, component: 组件}
    routes: [
        { path: '/find', component: Find },
        { path: '/my', component: My },
        { path: '/friend', component: Friend },
    ]
})
export default router

  1. 在 mian.js中导入

import router from './router/index.js'

二、声明式导航 - 导航链接

需求:实现导航高亮效果


vue-router 提供了一个全局组件 router-link (取代 a 标签)

  1. 能跳转,配置 to 属性指定路径 (必须)。本质还是 a 标签,to 无需 #

  2. 能高亮,默认就会提供高亮类名,可以直接设置高亮样式

<template>
  <div> 
    <div class="footer_wrap">
          <router-link to="/find">发现音乐</rout-link>
          <router-link to="/my">我的音乐</rout-link>
          <router-link to="/part">朋友</rout-link>
      </div>
   <div class="top">
          <router-view></router-view>
    </div>
  </div>
</template>


三、声明式导航 - 两个类名

说明:我们发现 router-link 自动给当前导航添加了两个高亮类名


  • router-link-active 模糊匹配 (用的多)

    • to="/my" 可以匹配 /my /my/a /my/b ...

  • router-link-exact-active 精确匹配

    • to="/my" 仅可以匹配 /my


说明:router-link 的两个高亮类名太长了,我们希望能定制怎么办?

const router = new VueRouter({
	linkActiveClass: "类名1",
	linkExactActiveClass: "类名2"
})


四、声明式导航 - 跳转传参

目标:在跳转路由时,进行传值


跳转传参的两种方式:

  1. 查询参数传参

  2. 动态路由传参


(一) 查询参数传参

  1. 语法格式如下:

    • to="/path?参数名=值"

  2. 对应页面组件接收传递过来的值

    • $route.query.参数名


App.vue

<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>

index.js

import Home from '@/views/Home'
import Search from '@/views/Search'
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter) // VueRouter插件初始化

// 创建了一个路由对象
const router = new VueRouter({
  routes: [
    { path: '/home', component: Home },
    { path: '/search', component: Search }
  ]
})

export default router

Home.vue

<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="/search?key=黑马程序员">黑马程序员</router-link>
            <router-link to="/search?key=前端培训">前端培训</router-link>
            <router-link to="/search?key=如何成为前端大牛">如何成为前端大牛</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

<template>
    <div class="search">
        <p>搜索关键字: {{ $route.query.key }} </p>
        <p>搜索结果: </p>
        <ul>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'MyFriend',
    created () {
        // 在created中,获取路由参数,this.$route.query.参数名 
        console.log(this.$route.query.key);
    }
}
</script>

<style>
.search {
    width: 400px;
    height: 240px;
    padding: 0 20px;
    margin: 0 auto;
    border: 2px solid #c4c7ce;
    border-radius: 5px;
}
</style>

(二) 动态路由传参

  1. 配置动态路由

const router = new VueRouter({
  routes: [
    { path: '/search/:words', component: Search }
  ],
})
  1. 配置导航链接

    • to="/path/参数值"

  2. 对应页面组件接收传递过来的值

    • $route.params.参数名


// Home.vue

<div class="hot-link">
	热门搜索:
	<router-link to="/search/黑马程序员">黑马程序员</router-link>
	<router-link to="/search/前端培训">前端培训</router-link>
	<router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
</div>

// Search.vue

 <p>搜索关键字: {{ $route.params.words }} 

(三) 动态路由参数可选符

问题:配了路由 path: "/search/:words" 为什么按下面步骤操作,会未匹配到组件,显示空白?


原因: /search/:words 表示必须要传参数。如果不传参数,也希望匹配,可以加个可选符 "?"

const router = new VueRouter({
  routes: [
    { path: '/search/:words?', component: Search }
  ],
})

五、Vue路由 - 重定向

问题:网页打开,url 默认是 / 路径,未匹配到组件时,会出现空白


说明:重定向 -> 匹配 path 后,强制跳转 path 路径

语法: { path: 匹配路径,redirect: 重定向到的路径}

const router = new VueRouter({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/search/:words?', component: Search }
  ],
})

六、Vue路由 - 404

作用:当路径找不到匹配时,给个提示页面

位置:配在路由最后

语法:path: "*" (任意路径) - 前面不匹配就命中最后这个


import NotFind from '@views/NotFind'

const router = new VueRouter({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/search/:words?', component: Search },
    //  前面不匹配就命中最后这个
    { path: '*', component: NotFind}
  ],
})

// NotFind.vue
<template>
  <div>
    <h1> 404 Page Not Found</h1>
  </div>
</template>

<script>
export default {

}
</script>

<style>

</style>

七、Vue路由 - 模式设置

问题: 路由的路径看起来不自然,有 #,能否切成真正路径形式?

const router = new VueRouter({
// 一旦采用 history模式,地址栏就没有#,需要后台配置访问规则
  mode: "history"
})


编程式导航

一、基本跳转

问题:点击按钮跳转如何实现?

编程式导航:用 JS 代码来进行跳转

两种语法:

  1. path 路径跳转

  2. name 命名路由跳转


Path 路径跳转 (简单方便)

this.$router.push({
    path: '路由路径'
})

this.$router.push({
    path: '路由路径'
})

Home.vue

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>
​
<script>
export default {
    name: 'FindMusic',
    methods: {
        // 通过路径的方式跳转:
        // (1) this.$router.push('路由路径') [简写]
        // (2) this.$router.push({ path: '路由路径'}) [完整写法]
        goSearch () {
            this.$router.push('/search')
        }
    }
}
</script>

name 命名路由跳转 (适合 path 路径长的场景)

this.$router.push({
    name: '路由名'
})

{ name: '路由名', path: '/path/xxx', component: xxx },

index.js

// 创建了一个路由对象
const router = new VueRouter({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { name: 'search', path: '/search/:words?', component: Search },
    { path: '*', component: NotFind }
  ],
  mode: "history"
})

Home.vue

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>
​
<script>
export default {
    name: 'FindMusic',
    methods: {
        // 通过命名路由的方式跳转:(需要给路由起名字) 适合路径名字长的场景
        goSearch () {
            this.$router.push({
                name: 'search'
            })
        }
    }
}
</script>

二、路由传参

问题:点击搜索按钮,跳转需要传参如何实现?

两种传参方式: 查询参数 + 动态路由传参

两种跳转方式,对于两种传参方式都支持:

  1. path 路径跳转传参

  2. name 命名路由跳转传参


path 路径跳转传参:

this.$router.push('/路径/参数值')
this.$router.push({
	path: '/路径/参数值'
})

查询参数传参

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input v-model="inpValue" type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>
​
<script>
export default {
    name: 'FindMusic',
    data () {
        return {
            inpValue: ''
        }
    },
    methods: {
         goSearch() {
            // this.$router.push(`/search?key=${this.inpValue}`)
            this.$router.push({
                path: '/search',
                query: {
                    key: this.inpValue
                } 
            })
        }
}
</script>

Search.vue

<template>
    <div class="search">
        <p>搜索关键字: {{ $route.query.key }} </p>
        <p>搜索结果: </p>
        <ul>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
        </ul>
    </div>
</template>
​
<script>
export default {
    name: 'MyFriend',
    created () {
        // 在created中,获取路由参数,this.$route.query.参数名 
        console.log(this.$route.params.words);
    }
}
</script>

动态路由传参

// 创建了一个路由对象
const router = new VueRouter({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { name: 'search', path: '/search/:words?', component: Search },
    { path: '*', component: NotFind }
  ],
  mode: "history"
})

Home.vue

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input v-model="inpValue" type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>
​
<script>
export default {
    name: 'FindMusic',
    data () {
        return {
            inpValue: ''
        }
    },
    methods: {
        goSearch() {
          this.$router.push(`/search/${this.inpValue}`)
            
    }
}
}
</script>

Search.vue

<template>
    <div class="search">
        <p>搜索关键字: {{ $route.params.words }} </p>
        <p>搜索结果: </p>
        <ul>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'MyFriend',
    created () {
        // 在created中,获取路由参数,this.$route.query.参数名 
        console.log(this.$route.params.words);
    }
}
</script>

name 命名路由跳转传参 (query传参)

this.$router.push({
	name: '路由名',
	query: {
		参数名1: '参数值1',
		参数名2: '参数值2',
	}
})

Home.vue

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input v-model="inpValue" type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>
​
<script>
export default {
    name: 'FindMusic',
    data () {
        return {
            inpValue: ''
        }
    },
    methods: {
        goSearch() {
          this.$router.push({
            name: 'search',
            query: {
                key: this.inpValue
            }
          })
            
    }
}
}
</script>
​

Search.vue

<template>
    <div class="search">
        <p>搜索关键字: {{ $route.query.key }} </p>
        <p>搜索结果: </p>
        <ul>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'MyFriend',
    created () {
        // 在created中,获取路由参数,this.$route.query.参数名 
        console.log(this.$route.params.words);
    }
}
</script>

第二种方式 (param传参):

index.js

// 创建了一个路由对象
const router = new VueRouter({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { name: 'search', path: '/search/:words?', component: Search },
    { path: '*', component: NotFind }
  ],
  mode: "history"
})

<template>
    <div class="home">
        <div class="logo-box"></div>
        <div class="search-box">
            <input v-model="inpValue" type="text">
            <button @click="goSearch">搜索一下</button>
        </div>
        <div class="hot-link">
            热门搜索:
            <router-link to="/search/黑马程序员">黑马程序员</router-link>
            <router-link to="/search/前端培训">前端培训</router-link>
            <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
        </div>
    </div>
</template>

<script>
export default {
    name: 'FindMusic',
    data () {
        return {
            inpValue: ''
        }
    },
    methods: {
        goSearch() {
          this.$router.push({
            name: 'search',
            params: {
                words: this.inpValue
            }
          })
            
    }
}
}
</script>

search.vue

<template>
    <div class="search">
        <p>搜索关键字: {{ $route.params.words }} </p>
        <p>搜索结果: </p>
        <ul>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
            <li>.............</li>
        </ul>
    </div>
</template>
​
<script>
export default {
    name: 'MyFriend',
    created () {
        // 在created中,获取路由参数,this.$route.query.参数名 
        console.log(this.$route.params.words);
    }
}
</script>

小结:



基于 VueCli 自定义创建项目

一、自定义创建项目

目标:基于 VueCli 自定义创建项目架子


---










二、ESlint代码规范及手动修复

代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?...

老话说:"没有规矩不成方圆" —> 正规的团队 需要 统一的编码风格

JavaScript Standard Style 规范说明 JavaScript Standard Style


下面是这份规则中的一小部分:

  • 字符串使用单引号 'abc'

  • 无分号 const name = 'zs'

  • 关键字后加空格 if (condition) { ... }

  • 函数名后加空格 function name (arg) { ... }

  • 坚持使用全等 === (摒弃 ==)

  • ......


代码规范错误

如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。

下面我们在main.js中随意做一些改动:添加一些空行,空格。

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 是来帮助你的。心态要好,有错,就改。


手动修正

根据错误提示来一项一项手动修正。

如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码去 [ ESLint 规则表] 中查找其具体含义。

打开 ESLint 规则表,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。



三、通过eslint插件来实现自动修正

1. eslint会自动高亮错误显示
2. 通过配置,eslint会自动帮助我们修复错误

  • 如何安装 (VSCode插件)

  • 如何配置

// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
    "source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false


  • 注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目

  • 注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify


settings.json 参考

{
    "window.zoomLevel": 2,
    "workbench.iconTheme": "vscode-icons",
    "editor.tabSize": 2,
    "emmet.triggerExpansionOnTab": true,
    // 当保存的时候,eslint自动帮我们修复错误
    "editor.codeActionsOnSave": {
        "source.fixAll": true
    },
    // 保存代码,不自动格式化
    "editor.formatOnSave": false
}

vuex

一、vuex概述

目标:明确 vuex 是什么,应用场景,优势。


是什么

vuex  是一个 vue 的状态管理工具,状态就是数据

大白话:vuex 是一个插件,可以帮我们管理 vue 通用的数据 (多组件共享的数据),例如:购物车数据 个人信息数据


场景

  1. 某个状态在很多个组件来使用 (个人信息)

  2. 多个组件共同维护一份数据 (购物车)


优势

  1. 共同维护一份数据,数据集中化管理

  2. 响应式变化

  3. 操作简洁 (vuex 提供了一些辅助函数)


注意:

官方原文:

  • 不是所有的场景都适用于vuex,只有在必要的时候才使用vuex

  • 使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度 (数据的操作更便捷,数据的流动更清晰)

Vuex就像《近视眼镜》, 你自然会知道什么时候需要用它~


二、需求: 多组件共享数据

目标:基于脚手架创建项目,构建 vuex 多组件数据共享环境


效果是三个组件共享一份数据:

  • 任意一个组件都可以修改数据

  • 三个组件的数据是同步的


效果是三个组件共享一份数据:

  • 任意一个组件都可以修改数据

  • 三个组件的数据是同步的


1.创建项目

vue create vuex-demo

目前只选这个,剩余的照常。


2.创建三个组件, 目录如下

|-components
|--Son1.vue
|--Son2.vue
|-App.vue

3.源代码如下

App.vue在入口组件中引入 Son1 和 Son2 这两个子组件

<template>
  <div id="app">
    <h1>根组件</h1>
    <input type="text">
    <Son1></Son1>
    <hr>
    <Son2></Son2>
  </div>
</template>
​
<script>
import Son1 from './components/Son1.vue'
import Son2 from './components/Son2.vue'
​
export default {
  name: 'app',
  data: function () {
    return {
​
    }
  },
  components: {
    Son1,
    Son2
  }
}
</script>
​
<style>
#app {
  width: 600px;
  margin: 20px auto;
  border: 3px solid #ccc;
  border-radius: 3px;
  padding: 10px;
}
</style>

main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

Son1.vue

<template>
  <div class="box">
    <h2>Son1 子组件</h2>
    从vuex中获取的值: <label></label>
    <br>
    <button>值 + 1</button>
  </div>
</template>

<script>
export default {
  name: 'Son1Com'
}
</script>

<style lang="css" scoped>
.box{
  border: 3px solid #ccc;
  width: 400px;
  padding: 10px;
  margin: 20px;
}
h2 {
  margin-top: 10px;
}
</style>

Son2.vue

<template>
  <div class="box">
    <h2>Son2 子组件</h2>
    从vuex中获取的值:<label></label>
    <br />
    <button>值 - 1</button>
  </div>
</template>
​
<script>
export default {
  name: 'Son2Com'
}
</script>
​
<style lang="css" scoped>
.box {
  border: 3px solid #ccc;
  width: 400px;
  padding: 10px;
  margin: 20px;
}
h2 {
  margin-top: 10px;
}
</style>

三、vuex 的使用 - 创建仓库


1.安装 vuex

安装vuex与vue-router类似,vuex是一个独立存在的插件,如果脚手架初始化没有选 vuex,就需要额外安装

yarn add vuex@3  或者 npm install vuex@3 --legacy-peer-deps

2.新建 store/index.js 专门存放 vuex

为了维护项目目录的整洁,在src目录下新建一个store目录其下放置一个index.js文件。 (和 router/index.js 类似)


3.创建仓库 store/index.js

// 导入 vue
import Vue from 'vue'
// 导入 vuex
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
​
// 创建仓库 store
const store = new Vuex.Store()
​
// 导出仓库,给 main.js使用
export default store

4. 在 main.js 中导入挂载到 Vue 实例上

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')

此刻起, 就成功创建了一个 空仓库!!


5.测试打印Vuex

App.vue

export default {
  name: 'app',
   // 检验: this.$store
  created () {
    console.log(this.$store)
  },
  data: function () {
    return {
​
    }
  },

四、核心概念 - state 状态

1.目标

明确如何给仓库提供数据,如何使用仓库的数据


2.提供数据

State 提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。

打开项目中的 store.js 文件,在state对象中可以添加我们要共享的数据。

// 创建仓库 store
const store = new Vuex.Store({
  // state 状态, 即数据, 类似于vue组件中的data,
  // 区别:
  // 1.data 是组件自己的数据, 
  // 2.state 中的数据整个vue项目的组件都能访问到
  state: {
    count: 101
  }
})

3.访问Vuex中的数据

问题: 如何在组件中获取count?

  • 通过$store直接访问 —> {{ $store.state.count }}

获取 store:
 1.Vue模板中获取 this.$store
 2.js文件中获取 import 导入 store
 

​
模板中:     {{ $store.state.xxx }}
组件逻辑中:  this.$store.state.xxx
JS模块中:   store.state.xxx

  • 通过辅助函数 (简化)

mapState 是辅助函数,帮助我们把 store 中的数据 自动映射到组件的计算属性中


核心步骤:


5.代码实现

模板中使用

组件中可以使用 $store 获取到vuex中的store对象实例,可通过state属性属性获取count, 如下

<h1>state的数据 - {{ $store.state.count }}</h1>

组件逻辑中使用

将state属性定义在计算属性中 State | Vuex

<h1>state的数据 - {{ count }}</h1>

// 把state中数据,定义在组件内的计算属性中
  computed: {
    count () {
      return this.$store.state.count
    }
  }

5.3 js文件中使用

 
//main.js
import store from "@/store"
​
console.log(store.state.count)

每次都像这样一个个的提供计算属性, 太麻烦了,我们有没有简单的语法帮我们获取state中的值呢?


五、通过辅助函数 - mapState获取 state中的数据

mapState是辅助函数,帮助我们把store中的数据映射到 组件的计算属性中, 它属于一种方便的用法

用法 :

1.第一步:导入mapState (mapState是vuex中的一个函数)

import { mapState } from 'vuex'

2.第二步:采用数组形式引入state属性

mapState(['count']) 

上面代码的最终得到的是 类似于

count () {
    return this.$store.state.count
}

3.第三步:利用展开运算符将导出的状态映射给计算属性

  computed: {
    ...mapState(['count'])
  }

 <div> state的数据:{{ count }}</div>

六、开启严格模式及Vuex的单项数据流

1.目标

明确 vuex 同样遵循单向数据流,组件中不能直接修改仓库的数据


2.直接在组件中修改Vuex中state的值


Son1.vue

button @click="handleAdd">值 + 1</button>
​
methods:{
     handleAdd (n) {
      // 错误代码(vue默认不会监测,监测需要成本)
      // 严格模式 (有利于初学者,检测不规范的代码 => 上线时需要关闭)
       this.$store.state.count++
      // console.log(this.$store.state.count) 
    },
}

3.开启严格模式

通过 strict: true 可以开启严格模式,开启严格模式后,直接修改state中的值会报错

state数据的修改只能通过mutations,并且mutations必须是同步的


七、核心概念-mutations

目标:掌握 mutations 的操作流程,来修改 state 数据。(state 数据的修改只能通过 mutations)


1.定义mutations

const store  = new Vuex.Store({
  state: {
    count: 0
  },
  // 定义mutations
  mutations: {
     
  }
})

2.格式说明

mutations是一个对象,对象中存放修改state的方法

mutations: {
    // 方法里参数 第一个参数是当前store的state属性
    // payload 载荷 运输参数 调用mutaiions的时候 可以传递参数 传递载荷
    addCount (state) {
      state.count += 1
    }
  },

3.组件中提交 mutations

this.$store.commit('addCount')

index.js

// 导入 vue
import Vue from 'vue'
// 导入 vuex
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)

// 创建仓库 store
const store = new Vuex.Store({
  // 通过 state 可以提供数据 (所有组件共享的数据)
  // 开启严格模式
  strict: true,
  // 1. 通过 state 可以提供数据 (所有组件共享的数据)
  state: {
    title: '大标题',
    count: 100
  },
  // 2. 通过 mutations 可以通过修改数据的方法
  mutations: {
    // 所有 mutation函数,第一个函数都是 state
    addCount (state) {
      state.count += 1
    },
    addFive (state) {
      state.count += 5
    },
    ChangeTitle (state) {
      state.title = '小标题'
    }
  }
})

// 导出仓库,给 main.js使用
export default store

Son1.vue

<template>
    <div class="box">
        <h2>Son1 子组件</h2>
        从vuex中获取的值: <label>{{ this.$store.state.count}}</label>
        <br>
        <button @click="handleAdd">值 + 1</button>
        <button @click="addFive">值 + 5</button>
           <button @click="changeFn">改标题</button>
    </div>
</template>
​
<script>
export default {
  name: 'Son1Com',
  methods: {
    // 应该通过 musation 核心概念,进行修改数据
    handleAdd () {
      this.$store.commit('addCount')
    },
    addFive () {
      this.$store.commit('addFive')
    },
    changeFn () {
      this.$store.commit('ChangeTitle')
    }
  }
}
</script>
​
<style lang="css" scoped>
.box {
    border: 3px solid #ccc;
    width: 400px;
    padding: 10px;
    margin: 20px;
}
​
h2 {
    margin-top: 10px;
}
</style>
 

八、带参数的 mutations

1.目标:

掌握 mutations 传参语法

2.语法

看下面这个案例,每次点击不同的按钮,加的值都不同,每次都要定义不同的mutations处理吗?


提交 mutation 是可以传递参数的

this.$store.commit('xxx',  参数)

2.1 提供mutation函数(带参数 - 提交载荷 payload)

mutations: {
  ...
  addCount (state, n) {
    state.count += n
  }
},

2.2 页面中提交mutation

handle ( ) {
  this.$store.commit('addCount', 10)
}

小tips: 提交的参数只能是一个, 如果有多个参数要传, 可以传递一个对象

this.$store.commit('addCount', {
  count: 10
})

九、练习-mutations的减法功能


1.步骤


2.代码实现


Son1.vue

<template>
    <div class="box">
        <h2>Son1 子组件</h2>
        从vuex中获取的值: <label>{{ this.$store.state.count}}</label>
        <br>
        <button @click="handleAdd(1)">值 + 1</button>
        <button @click="handleAdd(5)">值 + 5</button>
        <button @click="handleAdd(10)">值 + 10</button>
           <button @click="changeFn">改标题</button>
    </div>
</template>
​
<script>
export default {
  name: 'Son1Com',
  methods: {
    // 应该通过 musation 核心概念,进行修改数据
    handleAdd (n) {
       // 注意点: mutation参数有且只能有一个,如果需要多个参数,包装成一个对象或数组
      // this.$store.commit('addCount', n)
        this.$store.commit('addCount', n
        }     
       )
    },
    changeFn () {
      this.$store.commit('ChangeTitle','黑马程序员')
    }
  }
}
</script>
 

store/index.js

// 导入 vue
import Vue from 'vue'
// 导入 vuex
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
​
// 创建仓库 store
const store = new Vuex.Store({
  // 通过 state 可以提供数据 (所有组件共享的数据)
  // 开启严格模式
  strict: true,
  // 1. 通过 state 可以提供数据 (所有组件共享的数据)
  state: {
    title: '大标题',
    count: 100
  },
  // 2. 通过 mutations 可以通过修改数据的方法
  mutations: {
    // 所有 mutation函数,第一个函数都是 state
    // 注意点: mutation参数有且只能有一个,如果需要多个参数,包装成一个对象或数组
    addCount (state, n) {
      state.count += n
    },
   // addCount (state, n) {
   //   state.count += n
  //  },
    ChangeTitle (state, newTitle) {
      state.title = newTitle
    }
  }
})
​
// 导出仓库,给 main.js使用
export default store

十、练习-Vuex中的值和组件中的input双向绑定

1.目标

实时输入,实时更新,巩固 mutations 传参语法



2.实现步骤


3.代码实现

App.vue


<template>
  <div id="app">
    <h1>根组件 - {{ title }} </h1>
    <input type="text" :value="count" @input="handleInput">
    <Son1></Son1>
    <hr>
    <Son2></Son2>
  </div>
</template>

<script>
import Son1 from './components/Son1.vue'
import Son2 from './components/Son2.vue'
import { mapState } from 'vuex'
console.log(mapState(['count', 'title']))
export default {
  name: 'app',
  computed: {
    ...mapState(['count', 'title'])
  },
  data: function () {
    return {

    }
  },
  methods: {
    handleInput (e) {
      // 1. 实时获取输入框的值
      const num = +e.target.value
      // 2. 提交 mutation,调用mutation函数
      this.$store.commit('changeCount', num)
    }
  },
  components: {
    Son1,
    Son2
  }
}
</script>

store/index.js

mutations: { 
   changeCount (state, newCount) {
      state.count = newCount
   }
},

十一、辅助函数- mapMutations

mapMutations和mapState很像,它把位于mutations中的方法提取了出来。

mutations: {
	subCount (state, n) {
		state.count -= n
	},
}

import  { mapMutations } from 'vuex'
methods: {
    ...mapMutations(['subCount','changeTitle'])
}

上面代码的含义是将mutations的方法映射到组件methods中,等价于

methods: {
      // commit(方法名, 载荷参数)
      subCount (n) {
          this.$store.commit('subCount', n)
      },
 }

此时,就可以直接通过this.subCount调用了

this.subCount(10) 调用

但是请注意: Vuex中mutations中要求不能写异步代码,如果有异步的ajax请求,应该放置在actions中


十二、核心概念 - actions

state是存放数据的,mutations是同步更新数据 (便于监测数据的变化, 更新视图等, 方便于调试工具查看变化),

actions 则负责进行 异步操作

说明:mutations必须是同步的 (便于监测数据变化,记录调试)


需求: 一秒钟之后, 要给一个数 去修改state


1.定义actions

mutations: {
  changeCount (state, newCount) {
    state.count = newCount
  }
}
​
​
actions: {
  setAsyncCount (context, num) {
    // 一秒后, 给一个数, 去修改 num
    setTimeout(() => {
      context.commit('changeCount', num)
    }, 1000)
  }
},

2.组件中通过dispatch调用

setAsyncCount () {
  this.$store.dispatch('setAsyncCount', 666)
}


Index.js

// 导入 vue
import Vue from 'vue'
// 导入 vuex
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)
​
// 创建仓库 store
const store = new Vuex.Store({
  // 通过 state 可以提供数据 (所有组件共享的数据)
  // 开启严格模式
  strict: true,
  // 1. 通过 state 可以提供数据 (所有组件共享的数据)
  state: {
    title: '大标题',
    count: 100
  },
  // 2. 通过 mutations 可以通过修改数据的方法
  mutations: {
    // 所有 mutation函数,第一个函数都是 state
    addCount (state, n) {
      state.count += n
    },
    ChangeTitle (state, obj) {
      state.title = obj.title
    },
    changeCount (state, newCount) {
      state.count = newCount
    }
  },
  // 3.actions 处理异步,注意:不能直接操作  state,需要commit mutation
  actions: {
    // context 上下文 (此处未分模块,可以当成 store仓库)
    // context.commit('mutation名字',额外参数)
    changeCountAction (context, num) {
      // 这里是setTimeout 模拟异步,以后大部分场景是发请求
      setTimeout(() => {
        context.commit('changeCount', num)
      }, 1000)
    }
  }
})
​
// 导出仓库,给 main.js使用
export default store

Son1.vue

<template>
    <div class="box">
        <h2>Son1 子组件</h2>
        从vuex中获取的值: <label>{{ this.$store.state.count}}</label>
        <br>
        <button @click="addCount(1)">值 + 1</button>
        <button @click="addCount(1)">值 + 1</button>
        <button @click="addCount(5)">值 + 5</button>
        <button @click="addCount(10)">值 + 10</button>
        <button @click="handleChange">一秒后改成666</button>
        <button @click="changeFn">改标题</button>
    </div>
</template>
​
<script>
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'Son1Com',
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapMutations(['addCount']),
    // 应该通过 musation 核心概念,进行修改数据
    // handleAdd (n) {
    //   this.addCount(n)
    // },
    changeFn () {
      this.$store.commit('ChangeTitle', {
        title: '哈哈'
      })
    },
    handleChange () {
      // 调用 actions
      this.$store.dispatch('changeCountAction', 666)
    }
  }
}
</script>

十三、辅助函数 -mapActions

1.目标:掌握辅助函数 mapActions,映射方法

mapActions 是把位于 actions中的方法提取了出来,映射到组件methods中

actions: {
    changeCountAction (context, num) {
        setTimeout(() => {
            context.commit('changeCount', num)
        },1000)
    }
}

import { mapActions } from 'vuex'
methods: {
    ...mapActions(['changeCountAction'])
}

Son2.vue

import { mapActions } from 'vuex'
methods: {
   ...mapActions(['changeCountAction'])
}
​
//mapActions映射的代码 本质上是以下代码的写法
//methods: {
//  changeCountAction (n) {
//    this.$store.dispatch('changeCountAction', n)
//  },
//}


直接通过 this.方法 就可以调用

this.changeCountAction(666) 调用

十四、核心概念 - getters

除了state之外,有时我们还需要从state中 筛选出符合条件的一些数据,这些数据是依赖state的,此时会用到getters

例如,state中定义了list,为1-10的数组,

state: {
    list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}

组件中,需要 显示所有大于5的数据,正常的方式,是需要list在组件中进行再一步的处理,但是getters可以帮助我们实现它


1.定义getters

  getters: {
    // 注意:
    // (1) getters函数的第一个参数是 state
    // (2) getters函数必须要有返回值
     filterList: (state) {
         return state.list.filter(item => item > 5)
     }
  }

2.访问getters

2.1 通过store 访问 getters

{{ $store.getters.filterList }}

2.2 通过辅助函数 - mapGetters

computed: {
    ...mapGetters(['filterList'])
}

 <div>{{ filterList }}</div>

十五、使用小结


模块 module(进阶语法)

1.目标

掌握核心概念 module 模块的创建


2.问题

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。(当项目变得越来越大的时候,Vuex会变得越来越难以维护)

由此,又有了Vuex的模块化

state: {
    userInfo: {
        name: 'zs',
        age: 18
    },
    theme: 'dark',
    desc: '测试项目'
},



3.模块定义 - 准备 state

定义两个模块 usersetting

user中管理用户的信息状态 userInfo modules/user.js

const state = {
  userInfo: {
    name: 'zs',
    age: 18
  }
}
​
const mutations = {}
​
const actions = {}
​
const getters = {}
​
export default {
  state,
  mutations,
  actions,
  getters
}

setting中管理项目应用的 主题色 theme,描述 desc, modules/setting.js

const state = {
  theme: 'dark'
  desc: '描述真呀真不错'
}
​
const mutations = {}
​
const actions = {}
​
const getters = {}
​
export default {
  state,
  mutations,
  actions,
  getters
}

store/index.js文件中的modules配置项中,注册这两个模块

import user from './modules/user'
import setting from './modules/setting'
​
const store = new Vuex.Store({
    modules:{
        user,
        setting
    }
})

使用模块中的数据, 可以直接通过模块名访问

$store.state.模块名.xxx  =>  $store.state.setting.desc

也可以通过 mapState 映射


一、获取模块内的state数据

1.目标:

掌握模块中 state 的访问语法

尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的 state 中,属性名就是模块名

2.使用模块中的数据

  1. 直接通过模块名访问 $store.state.模块名.xxx

  2. 通过 mapState 映射:

    1. 默认根级别的映射 mapState([ 'xxx' ])

    2. 子模块的映射 :mapState('模块名', ['xxx']) - 需要开启命名空间 namespaced:true

modules/user.js

const state = {
  userInfo: {
    name: 'zs',
    age: 18
  },
  myMsg: '我的数据'
}
​
const mutations = {
  updateMsg (state, msg) {
    state.myMsg = msg
  }
}
​
const actions = {}
​
const getters = {}
​
export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
}

3.代码示例

$store直接访问

...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc']),

mapState辅助函数访问

...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc']),

二、获取模块内的getters数据

1.目标:

掌握模块中 getters 的访问语

2.语法:

使用模块中 getters 中的数据:

  1. 直接通过模块名访问$store.getters['模块名/xxx ']

  2. 通过 mapGetters 映射

    1. 默认根级别的映射 mapGetters([ 'xxx' ])

    2. 子模块的映射 mapGetters('模块名', ['xxx']) - 需要开启命名空间

3.代码演示

modules/user.js

<!-- 测试访问模块中的getters - 原生 -->
<div>{{ $store.getters['user/UpperCaseName'] }}</div>

Son1.vue 直接访问getters

<!-- 测试访问模块中的getters - 原生 -->
<div>{{ $store.getters['user/UpperCaseName'] }}</div>

Son2.vue 通过命名空间访问

computed:{
  ...mapGetters('user', ['UpperCaseName'])
}

三、获取模块内的mutations方法

1.目标:

掌握模块中 mutation 的调用语法

2.注意:

默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

3.调用方式:

  1. 直接通过 store 调用 $store.commit('模块名/xxx ', 额外参数)

  2. 通过 mapMutations 映射

    1. 默认根级别的映射 mapMutations([ 'xxx' ])

    2. 子模块的映射 mapMutations('模块名', ['xxx']) - 需要开启命名空间

4.代码实现

modules/user.js

const mutations = {
  setUser (state, newUserInfo) {
    state.userInfo = newUserInfo
  }
}

modules/setting.js

const mutations = {
  setTheme (state, newTheme) {
    state.theme = newTheme
  }
}

Son1.vue

<button @click="updateUser">更新个人信息</button> 
<button @click="updateTheme">更新主题色</button>
​
​
export default {
  methods: {
    updateUser () {
      // $store.commit('模块名/mutation名', 额外传参)
      this.$store.commit('user/setUser', {
        name: 'xiaowang',
        age: 25
      })
    }, 
    updateTheme () {
      this.$store.commit('setting/setTheme', 'pink')
    }
  }
}

Son2.vue

<button @click="setUser({ name: 'xiaoli', age: 80 })">更新个人信息</button>
<button @click="setTheme('skyblue')">更新主题</button>
​
methods:{
// 分模块的映射
...mapMutations('setting', ['setTheme']),
...mapMutations('user', ['setUser']),
}

四、获取模块内的actions方法

1.目标:

掌握模块中 action 的调用语法 (同理 - 直接类比 mutation 即可)

2.注意:

默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

3.调用语法:

  1. 直接通过 store 调用 $store.dispatch('模块名/xxx ', 额外参数)

  2. 通过 mapActions 映射

    1. 默认根级别的映射 mapActions([ 'xxx' ])

    2. 子模块的映射 mapActions('模块名', ['xxx']) - 需要开启命名空间

4.代码实现

需求:


modules/user.js

const actions = {
  setUserSecond (context, newUserInfo) {
    // 将异步在action中进行封装
    setTimeout(() => {
      // 调用mutation   context上下文,默认提交的就是自己模块的action和mutation
      context.commit('setUser', newUserInfo)
    }, 1000)
  }
}

Son1.vue 直接通过store调用

<button @click="updateUser2">一秒后更新信息</button>
​
methods:{
    updateUser2 () {
      // 调用action dispatch
      this.$store.dispatch('user/setUserSecond', {
        name: 'xiaohong',
        age: 28
      })
    },
}

Son2.vue mapActions映射

<button @click="setUserSecond({ name: 'xiaoli', age: 80 })">一秒后更新信息</button>
​
methods:{
  ...mapActions('user', ['setUserSecond'])
}

五、Vuex模块化的使用小结

1.直接使用

  1. state --> $store.state.模块名.数据项名

  2. getters --> $store.getters['模块名/属性名']

  3. mutations --> $store.commit('模块名/方法名', 其他参数)

  4. actions --> $store.dispatch('模块名/方法名', 其他参数)

2.借助辅助方法使用

1.import { mapXxxx, mapXxx } from 'vuex'


computed、methods: {

// ...mapState、...mapGetters放computed中;

// ...mapMutations、...mapActions放methods中;

...mapXxxx('模块名', ['数据项|方法']),

...mapXxxx('模块名', { 新的名字: 原来的名字 }),

}

2.组件中直接使用 属性 {{ age }} 或 方法 @click="updateAge(2)"

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要使用Vue框架模仿黑马程序员官网,你可以按照以下步骤进行操作: 1. 创建Vue实例:首先,在HTML文件中引入Vue.js库,并创建一个Vue实例。在实例中,你可以定义数据、方法和计算属性等。 2. 绑定数据和视图:使用Vue的数据绑定语法,将数据和视图进行绑定。你可以在HTML中使用双花括号{{}}来显示数据,也可以使用v-bind指令来绑定属性。 3. 创建组件:根据官网的结构,将页面拆分为多个组件。每个组件都有自己的模板、数据和方法。你可以使用Vue的组件系统来创建和注册组件。 4. 路由配置:使用Vue Router来配置页面的路由。你可以定义不同的路由路径和对应的组件,以实现页面之间的切换。 5. 状态管理:使用Vuex来管理应用的状态。你可以在Vuex中定义状态、mutations和actions等,以实现数据的共享和管理。 6. 样式设计:根据官网的样式,使用CSS来设计页面的样式。你可以使用Vue的样式绑定语法,将样式与数据进行绑定。 7. 响应式交互:根据官网的交互效果,使用Vue的指令和事件处理机制,实现页面的响应式交互。你可以使用v-on指令来监听事件,也可以使用v-if和v-for等指令来控制元素的显示和循环。 8. 发布部署:最后,将你的代码打包并发布到服务器上,以实现在浏览器中访问你的模仿官网。 下面是一个简单的示例代码,演示了如何使用Vue框架模仿黑马程序员官网: ```html <!DOCTYPE html> <html> <head> <title>模仿黑马程序员官网</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> <header> <h1>{{ title }}</h1> </header> <nav> <ul> <li v-for="item in menu" :key="item.id">{{ item.name }}</li> </ul> </nav> <main> <router-view></router-view> </main> <footer> <p>{{ copyright }}</p> </footer> </div> <script> // 创建Vue实例 const app = new Vue({ el: '#app', data: { title: '模仿黑马程序员官网', menu: [ { id: 1, name: '首页' }, { id: 2, name: '课程' }, { id: 3, name: '讲师' }, { id: 4, name: '社区' }, { id: 5, name: '关于我们' } ], copyright: '版权所有 © 2021 黑马程序员' } }); </script> </body> </html> ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值