script怎么写
-
开头必须写:import{ref)from 'vue'
-
变量用const名字=ref(初始值)
-
修改变量用:名字.value=新值
v-一系列
v-for=循环
v-if=条件显示
v-show=显示隐藏(不删标签)
v-model=输入框同步变量
:XXX=v-bind属性绑定
@XXX= v-on点击事件
快速上手
-
index.js = 路线表(必须先写)
-
@click = 触发动作
-
router.push = 执行跳转
这三个加一起 = 你项目里所有页面跳转的逻辑
创建npm create vue@latest
一、Vue 快速上手
**1.**Vue 是一个用于构建用户界面的渐进式框架

**2.**创建Vue 实例

**3.**插值表达式
javascript
<body>
<div id="app">
<!-- 这里将来会编写一些用于渲染的代码逻辑 -->
<a href="">{{ count }}</a>
<h1>{{ msg }}</h1>
</div>
<!-- 引入的是开发版本包 - 包含完整的注释和警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
// 一旦引入 VueJS核心包,在全局环境,就有了 Vue 构造函数
const app = new Vue({
// 通过 el 配置选择器, 指定 Vue 管理的是哪个盒子
el:'#app',
// 通过 data 提供数据
data: {
msg: 'hello 灰太狼',
count: 666
}
})
</script>
</body>
(1)作用:利用表达式进行插值,渲染到页面中
(2)语法:
(3)注意点:① 使用的数据要存在 (data)
② 支持的是表达式,而非语句 if ... for
③ 不能在标签属性里面使用

**4.**响应式特性
定义:数据改变,视图自动更新
(1)访问数据: "实例.属性名"
(2)修改数据: "实例.属性名" = "值"
二、Vue 指令
指令:带有 v- 前缀的特殊标签属性
**1.**动态解析标签
v-html = "表达式 " → 动态设置元素 innerHTML
**2.**v-show 和 v-if
(1)v-show

(2)v-if

javascript
<body>
<!--
v-show 底层原理:切换 css 的 dispaly: none 来控制显示隐藏
v-if 底层原理:根据 判断条件 控制元素的 创建 和 移除
-->
<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.16/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
flag: false
}
})
</script>
</body>
**3.**v-else 和 v-else-if

javascript
<body>
<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.16/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
gender: 2,
score: 100
}
})
</script>
</body>
**4.**v-on

(1)普通
javascript
<body>
<div id="app">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button v-on:click="count++">+</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
count: 100
}
})
</script>
</body>
(2)调用传参
javascript
<body>
<div id="app">
<div class="box">
<h3>灰太狼自动售货机</h3>
<button @click="buy(5)">可乐5r</button>
<button @click="buy(10)">咖啡10r</button>
</div>
<p>银行卡余额:{{ money }}r</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
money: 100,
},
methods: {
buy (price) {
console.log(price)
this.money -= price
}
}
})
</script>
</body>
**5.**v-bind

javascript
<body>
<div id="app">
<!-- v-bind:src => :src -->
<img :src="imgUrl" v-bind:title="msg" alt="">
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data: {
imgUrl: '../考核JS/img/course1.png',
msg: '哈喽,灰太狼'
}
})
</script>
</body>
**6.**v-for

javascript
<body>
<div id="app">
<h3>青青草原</h3>
<ul>
<li v-for="(item, index) in list">
{{ item }} - {{ index }}
</li>
</ul>
<ul>
<li v-for="(item) in list">
{{ item }}
</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
list: ['灰太狼','蕉太狼','红太狼']
}
})
</script>
</body>
v-for 中的 key

7.v-model

javascript
<div id="app">
<!--
v-model 可以让数据和视图,形成双向数据绑定
(1) 数据变化, 视图自动更新
(2) 视图变化, 数据自动更新
-->
账户:<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.16/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.**指令修饰符

(1)按键修饰符
@keyup.enter → 键盘回车监听
javascript
<body>
<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/dist/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: ''
},
methods: {
fn (e) {
if(e.key === 'enter') {
console.log('键盘回车的时候触发', this.username)
}
}
}
})
</script>
</body>
(2)v-model 修饰符

(3)事件修饰符

javascript
<div id="app">
<h3>v-model修饰符 .trim .number</h3>
姓名:<input v-model.trim="username" type="text"><br>
年纪:<input v-model.number="age" type="text"><br>
<h3>@事件名.stop → 阻止冒泡</h3>
<div @click="fatherFn" class="father">
<div @click.stop="sonFn" class="son">儿子</div>
</div>
<h3>@事件名.prevent → 阻止默认行为</h3>
<a @click.prevent href="http://www.baidu.com">阻止默认行为</a>
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
age: '',
},
methods: {
fatherFn () {
alert('老父亲被点击了')
},
sonFn () {
alert('儿子被点击了')
}
}
})
</script>
**2.**v-bind 对于样式操作的增强
(1)操作 class
语法 :class = "对象/数组"

javascript
<style>
.box {
width: 200px;
height: 200px;
border: 3px solid #000;
font-size: 30px;
margin-top: 10px;
}
.pink {
background-color: pink;
}
.big {
width: 300px;
height: 300px;
}
</style>
<body>
<div id="app">
<div class="box" :class="{ pink: false, big: true}">黑马程序员</div>
<div class="box" :class="['pink', 'big']">黑马程序员</div>
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>
(2)操作 style
语法 :style = "样式对象"
适用场景:某个具体属性的动态设置
javascript
<style>
.box {
width: 200px;
height: 200px;
background-color: rgb(187, 150, 156);
}
</style>
<body>
<div id="app">
<div class="box" :style="{ width: '400px', height: '400px', backgroundColor: 'skyblue'}"></div>
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
}
})
</script>
</body>
**3.**v-model 应用于其他表单元素

javascript
<style>
textarea {
display: block;
width: 240px;
height: 100px;
margin: 10px 0;
}
</style>
<body>
<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>
自我描述:
<textarea v-model="desc"></textarea>
<button>立即注册</button>
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
username: '',
isSingle: true,
gender: "2",
cityId: '102',
desc: ""
}
})
</script>
</body>
二、computed 计算属性
**1.**计算属性
(1)定义:基于现有的数据,计算出来的新属性。依赖的数据变化,自动重新计算。
(2)语法:

javascript
<style>
table {
border: 1px solid #000;
text-align: center;
width: 240px;
}
th,td {
border: 1px solid #000;
}
h3 {
position: relative;
}
</style>
<body>
<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="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
// 现有的数据
list: [
{ id: 1, name: '篮球', num: 1 },
{ id: 2, name: '玩具', num: 2 },
{ id: 3, name: '铅笔', num: 5 },
]
},
computed: {
totalCount () {
// 基于现有的数据,编写求值逻辑
// 计算属性函数内部,可以直接通过 this 访问到 app 实例
// console.log(this.list)
// 需求:对 this.list 数组里面的 num 进行求和 => reduce
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
}
})
</script>
</body>
**2.**computed 计算属性 与 methods 方法
(1)计算属性

(2)方法

javascript
<style>
table {
border: 1px solid #000;
text-align: center;
width: 300px;
}
th,td {
border: 1px solid #000;
}
h3 {
position: relative;
}
span {
position: absolute;
left: 145px;
top: -4px;
width: 16px;
height: 16px;
color: white;
font-size: 12px;
text-align: center;
border-radius: 50%;
background-color: #e63f32;
}
</style>
<body>
<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 },
]
},
computed: {
// 计算属性:有缓存的,一旦计算出来啊结果,就会立刻缓存
// 下次读取 => 直接读缓存执行 => 性能特别高
totalCount () {
console.log('计算属性执行了')
let total = this.list.reduce((sum, item) => sum + item.num, 0)
return total
}
}
})
</script>
</body>

**3.**计算属性完整写法
语法:

javascript
<div id="app">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
<p>{{ fullName}}</p>
<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: '太狼',
},
computed: {
// 简写 => 获取
// fullName () {
// return this.firstName + this.lastName
// }
// 完整写法 => 获取 + 设置
fullName: {
// (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)
}
}
},
methods: {
changeName () {
this.fullName = '红太狼'
}
}
})
</script>
4.综合案例

三、watch 侦听器
1.作用:监视数据变化,执行一些 业务逻辑或异步操作。
2.语法:

(1)简单写法
javascript
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 18px;
}
#app {
padding: 10px 20px;
}
.query {
margin: 10px 0;
}
.box {
display: flex;
}
textarea {
width: 300px;
height: 160px;
font-size: 18px;
border: 1px solid #dedede;
outline: none;
resize: none;
padding: 10px;
}
textarea:hover {
border: 1px solid #1589f5;
}
.transbox {
width: 300px;
height: 160px;
background-color: #f0f0f0;
padding: 10px;
border: none;
}
.tip-box {
width: 300px;
height: 25px;
line-height: 25px;
display: flex;
}
.tip-box span {
flex: 1;
text-align: center;
}
.query span {
font-size: 18px;
}
.input-wrap {
position: relative;
}
.input-wrap span {
position: absolute;
right: 15px;
bottom: 15px;
font-size: 12px;
}
.input-wrap i {
font-size: 20px;
font-style: normal;
}
</style>
<body>
<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="./vue.js"></script>
<script src="./axios.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: '', // 翻译结果
// timer: null // 延时器id
},
// 具体讲解:(1) watch语法 (2) 具体业务实现
watch: {
// 该方法会在我数据变化时调用执行
// newValue新值,oldValue老值(一般不用)
// words (newValue) {
// console.log('变化了', newValue)
// }
'obj.words' (newValue) {
// console.log('变化了', 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
}
}, 300)
this.result = res.data.data
})
}
}
})
</script>
</body>
(2)完整写法 → 添加额外配置项

javascript
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 18px;
}
#app {
padding: 10px 20px;
}
.query {
margin: 10px 0;
}
.box {
display: flex;
}
textarea {
width: 300px;
height: 160px;
font-size: 18px;
border: 1px solid #dedede;
outline: none;
resize: none;
padding: 10px;
}
textarea:hover {
border: 1px solid #1589f5;
}
.transbox {
width: 300px;
height: 160px;
background-color: #f0f0f0;
padding: 10px;
border: none;
}
.tip-box {
width: 300px;
height: 25px;
line-height: 25px;
display: flex;
}
.tip-box span {
flex: 1;
text-align: center;
}
.query span {
font-size: 18px;
}
.input-wrap {
position: relative;
}
.input-wrap span {
position: absolute;
right: 15px;
bottom: 15px;
font-size: 12px;
}
.input-wrap i {
font-size: 20px;
font-style: normal;
}
</style>
<body>
<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="./vue.js"></script>
<script src="./axios.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: '', // 翻译结果
},
// 具体讲解:(1) watch语法 (2) 具体业务实现
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
}, 300)
this.result = res.data.data
})
}
}
// '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
// }
// }, 300)
// this.result = res.data.data
// })
// }
}
})
</script>
</body>
四、综合案例

五、生命周期
**1.**生命周期 & 生命周期四个阶段

**2.**生命周期钩子

**3.**综合案例
javascript
<body>
<div id="app">
<h3>{{ title }}</h3>
<div>
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</div>
<script src="./vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
count: 100,
title: '计数器'
},
// 1. 创建阶段(准备数据)
beforeCreate () {
console.log('beforecreate 响应式数据准备好之前', this.count)
},
created () {
console.log('created 响应式数据准备好之后', this.count)
// this.数据名 = 请求回来的数据
// 可以开始发送初始化渲染的请求了
},
// 2. 挂载阶段(渲染模板)
beforeMount () {
console.log('beforemount 模板渲染之前', document.querySelector('h3').innerHTML)
},
mounted () {
console.log('mounted 模板渲染之后', document.querySelector('h3').innerHTML)
// 可以开始操作dom了
},
// 3.更新阶段(修改数据 => 更新视图)
beforeUpdate () {
console.log('beforeUpdate 数据修改了,视图还没更新', document.querySelector('span').innerHTML)
},
updated () {
console.log('updated 数据修改了,视图已经更新', document.querySelector('span').innerHTML)
},
// 4.卸载阶段
beforeDestroy () {
console.log('beforeDestroy 卸载前')
console.log('清除掉一些Vue以外的资源占用,定时器,延时器···')
},
destroyed () {
console.log('destroyed 卸载后')
}
})
</script>
</body>
六、综合案例

一、工程化开发入门
**1.**工程化开发和脚手架


我采用的是npm.cmd

**2.**脚手架目录文件介绍和项目运行流程


一、组件通信
**1.**什么是prop
定义:组件上注册的一些自定义属性
作用:向子组件传递数据


javascript
/* 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>
javascript
/* UserInfo.vue */
<template>
<div class="userinfo">
<h3>我是个人信息组件</h3>
<div>姓名:{{ username }}</div>
<div>年龄:{{ age }}</div>
<div>是否单身:{{ isSingle ? '是' : '否' }}</div>
<div>座驾:{{ car.brand }}</div>
<div>兴趣爱好 {{ hobby.join(' , ') }}</div>
</div>
</template>
<script>
export default {
props: ['username', 'age', 'isSingle', 'car', 'hobby']
}
</script>
<style>
.userinfo {
width: 300px;
border: 3px solid #000;
padding: 20px;
}
.userinfo > div {
margin: 20px 10px;
}
</style>
**2.**prorps校验
语法:


javascript
/* 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>
javascript
/* BaseProgress.vue */
<template>
<div class="base-progress">
<div class="inner" :style="{ width: w + '%' }">
<span>{{ w }}%</span>
</div>
</div>
</template>
<script>
export default {
// props: ["w"],
// 1.基础写法(类型校验)
// props: {
// w: Number // Number String Boolean Array Object function
// }
// 2.完整写法(类型、是否必填、默认值、自定义校验)
props: {
w: {
type: Number,
// required: true
default: 0, // 默认值
validator (value) {
// console.log(value)
if (value >= 0 && value <= 100) {
return true
} else {
console.error('传入的 prop w. 必须是0 ~ 100')
return false
}
}
}
}
}
</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>
**3.**prop 和 data、单向数据流


javascript
/* App.vue */
<template>
<div class="app">
<BaseCount
@changeCount="handleChange"
:count="count">
</BaseCount>
</div>
</template>
<script>
import BaseCount from './components/BaseCount.vue'
export default {
components:{
BaseCount
},
data(){
return {
count:100
}
},
methods:{
handleChange (newCount) {
// console.log(newCount)
this.count = newCount
}
}
}
</script>
<style>
</style>
javascript
/* BasCount.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: Number
},
methods: {
handleAdd () {
// 子传父 this.$emit(事件名, 参数)
this.$emit('changeCount', this.count + 1)
},
handleSub () {
this.$emit('changeCount', this.count - 1)
}
}
}
// 单向数据流,父组件的prop更新,会单向向下流动,影响到子组件
</script>
<style>
.base-count {
margin: 20px;
}
</style>
二、综合案例
**1.**总结

**2.**非父子通信 (拓展) - event bus 事件总线
作用:非父子组件之间,进行简易消息传递。(复杂场景 → Vuex)


**3.**非父子通信 (拓展) - provide & inject



javascript
<template>
<div class="app">
我是APP组件
<button @click="change">修改数据</button>
<SonA></SonA>
<SonB></SonB>
</div>
</template>
<script>
import SonA from './components/SonA.vue'
import SonB from './components/SonB.vue'
export default {
provide () {
return {
color: this.color, // 简单类型(非响应式)
userInfo: this.userInfo // 复杂类型(想要式) - 推荐
}
},
data() {
return {
color: 'pink',
userInfo: {
name: 'jtl',
age: 18,
},
}
},
methods: {
change () {
// this.color = 'green'
this.userInfo.name = 'htl'
}
},
components: {
SonA,
SonB,
},
}
</script>
<style>
.app {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
}
</style>
javascript
<template>
<div class="grandSon">
我是GrandSon
{{ color }} - {{ userInfo.name }} - {{ userInfo.age}}
</div>
</template>
<script>
export default {
inject: ['color', 'userInfo']
}
</script>
<style>
.grandSon {
border: 3px solid #000;
border-radius: 6px;
margin: 10px;
height: 100px;
}
</style>
三、进阶语法
**1.**v-model 原理
(1)原理:v-model本质上是一个语法糖。如应用在输入框上,就是 value属性和input事件的合写
(2)作用:提供数据的双向绑定

(3)注意:$event 用于在模板中,获取事件的形参

javascript
<template>
<div class="app">
<input v-model="msg1" type="text" />
<br />
<!-- 模板中获取事件的形参 => $event 获取 -->
<input :value="msg2" @input="msg2 = $event.target.value" type="text" >
</div>
</template>
<script>
export default {
data() {
return {
msg1: '',
msg2: '',
}
},
}
</script>
<style>
</style>
**2.**表单类组件封装 & v-model 简化代码
(1)表单类组件封装 => 实现 子组件 和 父组件数据 的双向绑定



javascript
<template>
<div class="app">
<BaseSelect
:cityId="selectId"
@changeId="selectId = $event"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102'
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
javascript
<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) {
// console.log(e.target.value)
this.$emit('changeId', e.target.value)
}
}
}
</script>
<style>
</style>
(2)父组件 v-model 简化代码,实现 子组件 和 父组件数据 双向绑定



javascript
<template>
<div class="app">
<!-- v-model => :value + @input -->
<BaseSelect
v-model="selectId"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
javascript
<template>
<div>
<select :value="value" @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: {
value: String
},
methods: {
handleChange (e) {
this.$emit('input', e.target.value)
}
}
}
</script>
<style>
</style>
3..sync 修饰符
(1)作用:可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
(2)特点:prop属性名,可以自定义,非固定为 value
(3)场景:封装弹框类的基础组件, visible属性 true显示 false隐藏
(4)本质:就是 :属性名 和 @update:属性名 合写


4..ref 和 $refs
作用:利用 ref 和 $refs 可以用于 获取 dom 元素, 或 组件实例


5. Vue异步更新、$nextTick


javascript
<template>
<!-- 编辑状态 -->
<div class="app">
<div v-if="isShowEdit">
<input type="text" v-model="editValue" ref="inp" />
<button>确认</button>
</div>
<!-- 默认状态 -->
<div v-else>
<span>{{ title }}</span>
<button @click="handleEdit">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
isShowEdit: false,
editValue: '',
}
},
methods: {
handleEdit () {
// 1. 显示输入框 (异步 dom 更新)
this.isShowEdit = true
// 2. 让输入框获取焦点 ($nextTick等 dom 更新完,立刻执行准备的函数体)
this.$nextTick(() => {
// console.log(this.$refs.inp)
this.$refs.inp.focus()
})
// setTimeout(() => {
// this.$refs.inp.focus()
// }, 1000)
}
},
}
</script>
<style>
</style>
一、自定义指令
1.基础语法
自定义指令:自己定义的指令, 可以封装一些 dom 操作, 扩展额外功能
(1)全局注册

(2)局部注册


(3)总结:

javascript
<template>
<div>
<h1>自定义指令</h1>
<input v-focus ref="inp" type="text">
</div>
</template>
<script>
export default {
// mounted () {
// this.$refs.inp.focus()
// }
// 2. 局部注册指令
directives: {
// 指令名:指令的配置项
focus: {
inserted (el) {
el.focus()
}
}
}
}
</script>
<style>
</style>
2.指令的值
(1)语法:在绑定指令时,可以通过"等号"的形式为指令 绑定 具体的参数值

(2)通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数。

(3)总结:

javascript
<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) {
// console.log(el,binding.value)
// binding.value 就是指令的值
el.style.color = binding.value
},
// 2. update 指令的值修改的时候触发,提供值变化后,dom更新的逻辑
update (el, binding) {
console.log('指令的值修改了')
el.style.color = binding.value
}
}
}
}
</script>
<style>
</style>
3.v-loading 指令封装
(1)场景:实际开发过程中,发送请求需要时间,在请求的数据未回来时,
页面会处于空白状态 => 用户体验不好
(2)


(3)总结: 
javascript
<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 class="box2"></div>
</div>
</template>
<script>
// 安装axios => yarn add 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 中,用于页面渲染 v-for
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.默认插槽
(1)作用:让组件内部的一些结构支持自定义
(2)需求: 将需要多次显示的对话框, 封装成一个组件
(3)语法:

(4)总结


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


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


总结:


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



三、综合案例


一、路由入门
**1.**单页应用程序


**2.**路由概念
Vue中的路由是路径和组件的映射关系
**3.**VueRouter的介绍
(1)作用:修改地址栏路径时,切换显示匹配的组件
(2)说明:Vue 官方的一个路由插件,是一个第三方包
(3)官网:https://v3.router.vuejs.org/zh/
(4)5个基础步骤:


(5)2 个核心步骤:


**4.**组件目录存放问题
组件分类: .vue文件分2类; 页面组件 和 复用组件
注意:都是 .vue文件 (本质无区别)
分类:页面组件 - views文件夹 => 配合路由,页面展示
复用组件 - components文件夹 => 封装复用
二、路由进阶
**1.**路由模块封装
目标:将路由模块抽离出来。 好处:拆分模块,利于维护
绝对路径:@指代src目录,可以用于快速引入组件
**2.**声明式导航和导航高亮
router-link:能跳转,能高亮 (自带激活时的类名)
<router-link to="/路径值" ></router-link>
<a href="#/路径值" ></router-link>


**3.**精确匹配和模糊匹配

**4.**自定义高亮类名

javascript
// router index.js
import Find from '@/views/Find'
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 },
],
// link自定义高亮类名
linkActiveClass: 'active', // 配置模糊匹配的类名
linkExactActiveClass: 'exact-active' // 配置精确匹配的类名
})
export default router
**5.**声明式导航传参 ( 查询参数传参和动态路由传参 )
(1)查询参数传参(比较适合传多个参数)
① 语法格式如下 to="/path?参数名=值"
② 对应页面组件接收传递过来的值 $route.query.参数名

(2)动态路由传参(优雅简洁,传单个参数比较方便)
① 配置动态路由

② 配置导航链接 to="/path/参数值"
③ 对应页面组件接收传递过来的值 $route.params.参数名

(3)动态路由参数可选符
/search/:words 表示,必须要传参数。如果不传参数,也希望匹配,可以加个可选符 "?"

**6.**路由重定向
说明:重定向 → 匹配path后, 强制跳转path路径
语法: { path: 匹配路径, redirect: 重定向到的路径 }

**7.**路由404
作用:当路径找不到匹配时,给个提示页面
位置:配在路由最后
语法:path: "*" (任意路径) -- 前面不匹配就命中最后这个

**8.**路由模式

**9.**编程式导航 - 基本跳转
(1)path 路径跳转 (简易方便)

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

javascript
<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 () {
// 1. 通过路径的方式跳转
// (1) this.$router.push('路由路径') [简写]
// this.$router.push('路由路径?参数名=参数值')
// this.$router.push('/search')
// this.$router.push(`/search?key=${this.inpValue}`)
// this.$router.push(`?search/${this.inpValue}`)
// (2) this.$router.push({ [完整写法] 更适合传参
// path: '路由路径'
// query: {
// 参数名:参数值,
// 参数名:参数值
// }
// })
// this.$router.push({
// path: '/search',
// query: {
// key: this.inpValue
// }
// })
this.$router.push({
path: `/search/${this.inpValue}`
})
// 2. 通过命名路由的方式跳转(需要给路由取名字) 适合长路径
// this.$router.push({
// name: '路由名',
// query: {参数名:参数值},
// params: { 参数名:参数值}
// })
this.$router.push({
name: 'search',
// query: {
// key: this.inpValue
// }
params: {
words: this.inpValue
}
})
}
}
}
</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>
10.编程式导航传参 ( 查询参数传参和动态路由传参)
(1)①path 路径跳转传参 (query传参)

②path 路径跳转传参 (动态路由传参)

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

②name 命名路由跳转传参 (动态路由传参)

javascript
<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 () {
// 1. 通过路径的方式跳转
// (1) this.$router.push('路由路径') [简写]
// this.$router.push('路由路径?参数名=参数值')
// this.$router.push('/search')
// this.$router.push(`/search?key=${this.inpValue}`)
// this.$router.push(`/search/${this.inpValue}`)
// (2) this.$router.push({ [完整写法] 更适合传参
// path: '路由路径'
// query: {
// 参数名: 参数值,
// 参数名: 参数值
// }
// })
// this.$router.push({
// path: '/search',
// query: {
// key: this.inpValue
// }
// })
// this.$router.push({
// path: `/search/${this.inpValue}`
// })
// 2. 通过命名路由的方式跳转 (需要给路由起名字) 适合长路径
// this.$router.push({
// name: '路由名'
// query: { 参数名: 参数值 },
// params: { 参数名: 参数值 }
// })
this.$router.push({
name: 'search',
// query: {
// key: this.inpValue
// }
params: {
words: this.inpValue
}
})
}
}
}
</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>
一、综合案例
**1.**分析

**2.**组件缓存 keep-alive
问题:从面经点到详情页,又点返回,数据重新加载了 → 希望回到原来的位置
原因:路由跳转后,组件被销毁了,返回回来组件又被重建了,所以数据重新被加载了
解决:利用 keep-alive 将组件缓存下来

(1)keep-alive定义
keep-alive是Vue的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中
(2)keep-alive的优点
在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM,
减少加载时间及性能消耗,提高用户体验性。
(3)语法

(4)keep-alive的三个属性
① include : 组件名数组,只有匹配的组件会被缓存
② exclude : 组件名数组,任何匹配的组件都不会被缓存
③ max : 最多可以缓存多少组件实例

(5)keep-alive的使用会触发两个生命周期函数
activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发
deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发
组件缓存后就不会执行组件的created, mounted, destroyed 等钩子了
所以其提供了actived 和 deactived钩子,帮我们实现业务需求。

**3.**总结

**4.**自定义创建项目

**5.**ESlint 代码规范
规范说明:https://standardjs.com/rules-zhcn.html

自动修正:基于 vscode 插件 ESLint 高亮错误,并通过配置 自动 帮助我们修复错误。

二、Vuex的基本使用
1.vuex概述

**2.**构建 vuex [多组件数据共享] 环境
目标:基于脚手架创建项目,构建 vuex 多组件数据共享环境
**3.**创建一个空仓库

javascript
// 这里面存放的就是 vuex 相关的核心代码
import Vue from 'vue'
import Vuex from 'vuex'
// 插件安装
Vue.use(Vuex)
// 创建仓库(空仓库)
const store = new Vuex.Store()
// 导出给main.js使用
export default store
一、核心概念 - state 状态
目标:明确如何给仓库提供数据,如何使用仓库的数据
1.提供数据
State提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。 在state
对象中可以添加我们要共享的数据。

2.使用数据
① 通过 store 直接访问

② 通过辅助函数


**二、**核心概念 - mutations
**1.**目标:明确 vuex 同样遵循单向数据流,组件中不能直接修改仓库的数据
通过 strict: true 可以开启严格模式

(1)定义 mutations 对象,对象中存放修改 state 的方法

(2)组件中提交调用 mutations

**2.**目标:掌握 mutations 传参语法
提交 mutation 是可以传递参数的 `this.$store.commit( 'xxx', 参数 )`
(1)提供 mutation 函数 (带参数 - 提交载荷 payload )

(2)页面中提交调用 mutation

Tips: 提交参数只能一个,如果有多个参数,包装成一个对象传递

**3.**核心概念 - mutations - 练习
(1)目标:减法功能,巩固 mutations 传参语法

(2)目标:实时输入,实时更新,巩固 mutations 传参语法

三、辅助函数 - mapMutations
目标:掌握辅助函数 mapMutations,映射方法
mapMutations和mapState很像,它是把位于mutations中的方法提取了出来,映射到组件methods

四、核心概念 - actions
目标:明确 actions 的基本语法,处理异步操作。
需求: 一秒钟之后, 修改 state 的 count 成 666。
说明:mutations 必须是同步的 (便于监测数据变化,记录调试)

五、辅助函数 - mapActions
目标:掌握辅助函数 mapActions,映射方法
mapActions 是把位于 actions中的方法提取了出来,映射到组件methods中

六、核心概念 - getters
目标:掌握核心概念 getters 的基本语法 (类似于计算属性)
说明:除了state之外,有时我们还需要从state中派生出一些状态,这些状态是依赖state的,此时
会用到getters
例如:state中定义了list,为 1-10 的数组,组件中,需要显示所有大于5的数据

**1.**定义 getters

**2.**访问getters
(1)通过 store 访问 getters

(2)通过辅助函数 mapGetters 映射

七、核心概念 - 模块 module (进阶语法)
1.目标:掌握核心概念 module 模块的创建
(1)由于vuex使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复
杂时,store对象就有可能变得相当臃肿。(当项目变得越来越大的时候,Vuex会变得越来越
难以维护)

(2)模块拆分:user模块: store/modules/user.js

2.目标:掌握模块中 state 的访问语法
尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的state中,属性名就是模块名使用
模块中的数据:
(1)直接通过模块名访问 $store.state.模块名.xxx
(2)通过 mapState 映射
默认根级别的映射 mapState([ 'xxx' ])
子模块的映射 mapState('模块名', ['xxx']) - 需要开启命名空间

3.目标:掌握模块中 getters 的访问语法
使用模块中 getters 中的数据:
(1)直接通过模块名访问 $store.getters['模块名/xxx ']
(2)通过 mapGetters 映射
默认根级别的映射 mapGetters([ 'xxx' ])
子模块的映射 mapGetters('模块名', ['xxx']) - 需要开启命名空间

4.目标:掌握模块中 mutation 的调用语法
注意:默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模
块。 调用子模块中 mutation:
(1)直接通过 store 调用 $store.commit('模块名/xxx ', 额外参数)
(2)通过 mapMutations 映射
默认根级别的映射 mapMutations([ 'xxx' ])
子模块的映射 mapMutations('模块名', ['xxx']) - 需要开启命名空间

5.目标:掌握模块中 action 的调用语法 (同理 - 直接类比 mutation 即可)
注意:默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模
块。 调用子模块中 action :
(1)直接通过 store 调用 $store.dispatch('模块名/xxx ', 额外参数)
(2)通过 mapActions 映射
默认根级别的映射 mapActions([ 'xxx' ])
子模块的映射 mapActions('模块名', ['xxx']) - 需要开启命名空间

javascript
// ./store/index.js
// 这里面存放的就是 vuex 相关的核心代码
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import setting from './modules/setting'
// 插件安装
Vue.use(Vuex)
// 创建仓库(空仓库)
const store = new Vuex.Store({
// 严格模式(有利于初学者,检测不规范的代码 => 上线时需要移除)
strict: true,
// 1. 通过 state 可以提供数据(所以组件共享的数据)
state: {
title: '仓库大标题',
count: 100,
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
// 2. 通过 mutation 可以提供修改数据的方法
mutations: {
// 所有 mutation 函数,第一个参数,都是 state
// 注意点:mutation 参数有且只能有一个,如果需要多个参数,包装成一个对象
addCount (state, n) {
// 修改数据
state.count += n
},
subCount (state, n) {
state.count -= n
},
changeCount (state, newCount) {
state.count = newCount
},
changeTitle (state, newTitle) {
state.title = newTitle
}
},
// 3. actions 处理异步,
// 注意:不能直接操作 state. 操作state. 还是需要 commit mutation
actions: {
// context 上下文(此处未分模块,可以当成store仓库)
// context.commit('mutation名字',额外参数)
changeCountAction (context, num) {
// 这里是setTimeout模拟异步,以后大部分场景是发请求
setTimeout(() => {
context.commit('changeCount', num)
}, 1000)
}
},
// 4.getters 类似于计算属性
getters: {
// 注意点:
// 1. 形参第一个参数,就是state
// 2. 必须有返回值,返回值就是getters的值
filterList (state) {
return state.list.filter(item => item > 5)
}
},
// 5.modules 模块
modules: {
user,
setting
}
})
// 导出给main.js使用
export default store
javascript
// ./App.vue
<template>
<div id="app">
<h1>
根组件
- {{ title }}
- {{ count }}
</h1>
<input :value="count" @input="handleInput" type="text">
<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',
created () {
// console.log(this.$router) // 没配
console.log(this.$store.state.count)
},
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>
<style>
#app {
width: 600px;
margin: 20px auto;
border: 3px solid #ccc;
border-radius: 3px;
padding: 10px;
}
</style>
javascript
// ./components/Son1.vue
<template>
<div class="box">
<h2>Son1 子组件</h2>
从vuex中获取的值: <label>{{ $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="handleChange">1秒后修改成666</button>
<button @click="changeFn">改标题</button>
<hr>
<!-- 计算属性getters -->
<div>{{ $store.state.list }}</div>
<div>{{ $store.getters.filterList }}</div>
<hr>
<!-- 测试访问模块中的state - 原生 -->
<div>{{ $store.state.user.userInfo.name }}</div>
<button @click="updateUser">更新个人信息</button>
<button @click="updateUser2">1秒后更新个人信息</button>
<div>{{ $store.state.setting.theme}}</div>
<button @click="updateTheme">更新主题色</button>
<hr>
<!-- 测试访问模块中的getters - 原生 -->
<div>{{ $store.getters['user/UpperCaseName'] }}</div>
</div>
</template>
<script>
export default {
name: 'Son1Com',
created () {
console.log(this.$store.getters)
},
methods: {
updateUser () {
// $store.commit('模块名/mutation名', 额外传参)
this.$store.commit('user/setUser', {
name: 'ytl',
age: 20
})
},
updateUser2 () {
// 如何调用action dispatch
this.$store.dispatch('user/setUserSecond', {
name: 'qjl',
age: 28
})
},
updateTheme () {
this.$store.commit('setting/setTheme', 'blue')
},
handleAdd (n) {
// 错误代码(vue 默认不会检测,需要监测成本,开启严格模式即可)
// this.$store.state.count++
// console.log(this.$store.state.count)
// 应该通过 mutation 核心概念,进行修改数据
// 需要提供调用 mutation
// this.$store.commit('addCount')
// console.log(n)
// 调用带参数的 mutation 函数
this.$store.commit('addCount', n)
},
changeFn () {
this.$store.commit('changeTitle', '灰太狼')
},
handleChange () {
// 调用action
// this.$store.dispatch('action名字', 额外参数)
this.$store.dispatch('changeCountAction', 666)
}
}
}
</script>
<style lang="css" scoped>
.box{
border: 3px solid #ccc;
width: 400px;
padding: 10px;
margin: 20px;
}
h2 {
margin-top: 10px;
}
</style>
javascript
// ./components/Son2.vue
<template>
<div class="box">
<h2>Son2 子组件</h2>
从vuex中获取的值:<label>{{ count }}</label>
<br />
<button @click="subCount(1)">值 - 1</button>
<button @click="subCount(5)">值 - 5</button>
<button @click="subCount(10)">值 - 10</button>
<button @click="chanageCountAction(888)">1秒后修改成888</button>
<button @click="changeTitle('我是灰太狼')">改标题</button>
<hr>
<div>{{ filterList }}</div>
<hr>
<!-- 访问模块中的state -->
<div>{{ user.userInfo.name }}</div>
<div>{{ setting.theme }}</div>
<hr>
<!-- 访问模块中的state -->
<div>user模块数据:{{ userInfo}}</div>
<button @click="setUser({ name: 'hettl', age: 40})">更新个人信息</button>
<button @click="setUserSecond({ name: 'hettl', age: 40})">1秒后更新个人信息</button>
<div>setting模块数据:{{ theme }} - {{ desc }}</div>
<button @click="setTheme('blue')">更新主题色</button>
<hr>
<!-- 访问模块中的getters -->
<div>{{ UpperCaseName }}</div>
<div></div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
export default {
name: 'Son2Com',
computed: {
// mapState 和 mapActions 都是映射属性
...mapState(['count', 'user', 'setting']),
...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc']),
...mapGetters(['filterList']),
...mapGetters('user', ['UpperCaseName'])
},
methods: {
// mapState 和 mapActions 都是映射方法
// 全局级别的映射
...mapMutations(['subCount', 'changeTitle']),
...mapActions(['chanageCountAction']),
// 分模块的映射
...mapMutations('setting', ['setTheme']),
...mapMutations('user', ['setUser']),
...mapActions('user', ['setUserSecond'])
// handleSub (n) {
// this.subCount(n)
// }
}
}
</script>
<style lang="css" scoped>
.box {
border: 3px solid #ccc;
width: 400px;
padding: 10px;
margin: 20px;
}
h2 {
margin-top: 10px;
}
</style>
javascript
// ./store/modules/user.js
// user模块
const state = {
userInfo: {
name: 'htl',
age: 18
},
score: 100
}
const mutations = {
setUser (state, newUserInfo) {
state.userInfo = newUserInfo
}
}
const actions = {
setUserSecond (context, newUserInfo) {
// 将异步在action中进行封装
setTimeout(() => {
// 调用mutation context上下文,默认提交的就是直接模块的action和mutation
context.commit('setUser', newUserInfo)
}, 1000)
}
}
const getters = {
// 分模块后,state指代子模块的state
UpperCaseName (state) {
return state.userInfo.name.toUpperCase()
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
javascript
// ./store/modules/setting.js
// setting模块
const state = {
theme: 'light', // 主题色
desc: '测试demo'
}
const mutations = {
setTheme (state, newTheme) {
state.theme = newTheme
}
}
const actions = {}
const getters = {}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
**1.**项目演示
目标:查看项目效果,明确功能模块 → 完整的电商购物流程

**2.**创建项目
在cmd(win + R)中输入 vue create project-name
选中自定义(第三个)

**3.**调整初始化目录
目标:将目录调整成符合企业规范的目录

**4.**vant 组件库
目标:认识第三方 Vue组件库 vant-ui
组件库:第三方 封装 好了很多很多的 组件,整合到一起就是一个组件库。
链接:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/
**5.**其他 Vue 组件库
目标:了解其他 Vue 组件库
Vue的组件库并不是唯一的,vant-ui 也仅仅只是组件库的一种。
一般会按照不同平台进行分类:
(1)PC端: element-ui (element-plus) ant-design-vue
(2)移动端:vant-ui Mint UI(饿了么) Cube UI(滴滴)
**6.**vant 全部导入(方便) 和 按需导入(性能)【推荐】
(1)目标:阅读文档,掌握 全部导入 的基本使用
全部导入:

(2)目标:阅读文档,掌握 按需导入 的基本使用
按需导入:

**7.**项目中的 vw 适配
目标:基于 postcss 插件 实现项目 vw 适配
px => vx

**8.**路由设计配置
(1)目标:分析项目页面,设计路由,配置一级路由
但凡是单个页面,独立展示的,都是一级路由

(2)目标:阅读vant组件库文档,实现底部导航 tabbar
tabbar标签页:

(3)目标:基于底部导航,完成二级路由配置

**9.**登录页静态布局


(1)request模块 - axios 封装
目标:将 axios 请求方法,封装到 request 模块
使用 axios 来请求后端接口, 一般都会对 axios 进行 一些配置 (比如: 配置基础地址,请求响应
拦截器等) 所以项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个 request 模块
中, 便于维护使用
接口文档地址:
https://apifox.com/apidoc/shared-12ab6b18-adc2-444c-ad11-0e60f5693f66/doc-2221080
基地址:http://cba.itlike.com/public/index.php?s=/api/
(2)图形验证码功能完成
目标:基于请求回来的 base64 图片,实现图形验证码功能
说明:

需求:


(3)api 接口模块 -封装图片验证码接口
目标:将请求封装成方法,统一存放到 api 模块,与页面分离


(4)Toast 轻提示
目标:阅读文档,掌握 toast 轻提示

(5)短信验证倒计时
目标:实现短信验证倒计时功能
步骤分析:


(6)登录功能
目标:封装api登录接口,实现登录功能
步骤分析:

(7)响应拦截器统一处理错误提示
目标:通过响应拦截器,统一处理接口的错误提示


(8)登录权证信息存储
目标:vuex 构建 user 模块存储登录权证 (token & userId)
补充说明:


(9)storage存储模块 - vuex 持久化处理
目标:封装 storage 存储模块,利用本地存储,进行 vuex 持久化处理

(10)添加请求 loading 效果
目标:统一在每次请求后台时,添加 loading 效果
背景:有时候因为网络原因,一次请求的结果可能需要一段时间后才能回来, 此时,需要
给用户 添加 loading 提示。
添加 loading 提示的好处:

实操步骤:

**10.**页面访问拦截
目标:基于全局前置守卫,进行页面访问拦截处理
说明:智慧商城项目,大部分页面,游客都可以直接访问, 如遇到需要登录才能进行的操作,提示并跳转到登录 但是:对于支付页,订单页等,必须是登录的用户才能访问的,游客不能进入该页面,需要做拦截处理




**11.**首页 - 静态结构准备 & 动态渲染
目标:实现首页静态结构,封装接口,完成首页动态渲染

**12.**搜索 - 历史记录管理
目标:构建搜索页的静态布局,完成历史记录的管理

**13.**搜索列表 - 静态布局 & 动态渲染
目标:实现搜索列表页静态结构,封装接口,完成搜索列表页的渲染

14.商品详情- 静态布局 & 渲染
目标:实现商品详情静态结构,封装接口,完成商品详情页渲染

**15.**加入购物车 - 唤起弹层
目标:点击加入购物车,唤起弹层效果

16.加入购物车 - 封装数字框组件
目标:封装弹层中的数字框组件

17.加入购物车 - 判断 token 添加登录提示
目标:给未登录的用户,添加登录提示

**18.**加入购物车 - 封装接口进行请求
目标:封装接口,进行加入购物车的请求

**19.**购物车模块
说明:购物车 数据联动关系 较多,且通常会封装一些 小组件, 所以为了便于维护,一般都会将购物车的数据基于 vuex 分模块管理

**20.**订单结算台
说明:所有的结算,本质上就是 跳转到 "订单结算台",并且,跳转的同时,需要 携带上对应的订单相关参数, 具体需要哪些参数,基于 "订单结算台" 的需求来定。
(1)确认订单信息
目标:封装通用的订单信息确认接口

(2)购物车结算
目标:购物车结算跳转,传递参数,调用接口渲染订单结算台

(3)立即购买结算
目标:购物车结算跳转,传递参数,调用接口渲染订单结算台

21.提交订单并支付
目标:封装 API 请求方法,提交订单并支付

**22.**订单管理 & 个人中心 (快速实现)
目标:基于笔记,快速实现 订单管理 和 个人中心 跑通流程
**23.**打包发布
(1)目标:明确打包的作用
说明:vue脚手架只是开发过程中,协助开发的工具,当真正开发完了 => 脚手架不参与上线
打包的作用: ① 将多个文件压缩合并成一个文件 ② 语法降级 ③ less sass ts 语法解析
(2)目标:打包的命令 和 配置
说明:vue脚手架工具已经提供了打包命令,直接使用即可。
命令:npm run build
结果:在项目的根目录会自动创建一个文件夹`dist`, dist中的文件就是打包后的文件,只需要
放到服务器中即可。
配置:默认情况下,需要放到服务器根目录打开,如果希望双击运行,需要配置publicPath
配成相对路径

24.打包优化:路由懒加载
目标:配置路由懒加载,实现打包优化
说明:当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同 的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

一、Vue3的优势

二、create-vue搭建Vue3项目
**1.**认识 create-vue
create-vue是Vue官方新的脚手架工具,底层切换到了vite(下一代构建工具),为开发提供极速响应
**2.**使用create-vue创建项目
(1)前提环境条件
已安装 16.0 或更高版本的 Node.js
使用:node -v 查询
(2)创建一个Vue应用
npm init vue@latest
这一指令将会安装并执行 create-vue这一指令将会安装并执行 create-vue

三、熟悉项目目录和关键文件
项目目录和关键文件

四、组合式API - setup选项
**1.**setup选项的写法和执行时机

**2.**setup选项中写代码的特点

3.<script setup>语法糖

**4.**总结

javascript
<!-- <script>
export default {
// setup
// 1. 执行时机,比beforeCreate要早
// 2. setup函数中,获取不到this (this是undefined)
// 3. 数据 和 函数,需要在 setup 最后 return 才能在模板中应用
// 问题:每次都要return太麻烦了
setup () {
// console.log('set函数')
// 数据
const message = 'hello Vue3'
// 函数
const logMessage = () => {
console.log(message)
}
return {
message,
logMessage
}
},
beforeCreate () {
console.log('beforeCreate函数')
}
}
</script> -->
<script setup>
const message = 'this is message'
const logMessage = () => {
console.log(message)
}
</script>
<template>
<div>{{ message }}</div>
<button @click="logMessage">按钮</button>
</template>
五、组合式API - reactive和ref函数
**1.**reactive()
(1)作用:接受对象类型数据的参数传入并返回一个响应式的对象
(2)核心步骤:
从 vue 包中导入 reactive 函数
在 <script setup> 中执行 reactive 函数并传入类型为对象的初始值,并使用变量接收返回值

**2.**ref()
(1)作用:接收简单类型或者对象类型的数据传入并返回一个响应式的对象
(2)核心步骤:
从 vue 包中导入 ref 函数
在 <script setup> 中执行 ref 函数并传入初始值,使用变量接收 ref 函数的返回值

**3.**总结

javascript
<script setup>
// 1. reactive: 接收一个对象型的数据,返回一个响应式的对象
// 问题:如果是简单类型,怎么办呢?
// import { reactive } from 'vue'
// const state = reactive({
// count: 100
// })
// const setCount = () => {
// state.count++
// }
// 2. ref: 接收简单类型 或 复杂类型,返回一个响应式的对象
// 本质:是在原有传入数据的基础上,外层包了一层对象,包成了复杂类型
// 底层,包成复杂类型之后,再借助 reactive 实现的响应式
// 注意点:
// 1. 访问数据,需要通过 .value
// 2. 在 template 中, .value不需要加(帮我们扒了一层)
// 推荐: 以后声明数据,统一用 ref => 统一了编码规范
import { ref } from 'vue'
const count = ref(0)
const setCount = () => {
count.value++
}
</script>
<template>
<div>{{ count }}</div>
<button @click="setCount">+1</button>
</template>
六、组合式API - computed
**1.**computed计算属性函数
(1)计算属性基本思想和Vue2的完全一致,组合式API下的计算属性只是修改了写法
(2)核心步骤:
导入computed函数
执行函数 在回调参数中return基于响应式数据做计算的值,用变量接收

**2.**总结

javascript
<script setup>
// const 计算属性 = computed(() => {
// return 计算返回结果
// })
import { computed, ref } from 'vue'
// 声明数据
const list =ref([1, 2, 3, 4, 5, 6, 7, 8])
console.log(list) // 对象
console.log(list.value) // 数组
// 基于list派生一个计算属性,从list中过滤出 > 2
const computedList = computed(() => {
return list.value.filter(item => item > 2)
})
// 定义一个修改数组的方法
const addFn = () => {
list.value.push(666)
}
</script>
<template>
<div>
<div>原始数据: {{ list }}</div>
<div>计算后的数据:{{ computedList }}</div>
<button @click="addFn" type="button">修改</button>
</div>
</template>
七、组合式API - watch
**1.**watch函数
作用: 侦听一个或者多个数据的变化,数据变化时执行回调函数
俩个额外参数:1. immediate(立即执行) 2. deep(深度侦听)
**2.**基础使用 - 侦听单个数据
(1)导入watch函数
(2)执行watch函数传入要侦听的响应式数据(ref对象)和回调函数

**3.**基础使用 - 侦听多个数据
说明:同时侦听多个响应式数据的变化,不管哪个数据变化都需要执行回调

**4.**immediate
说明:在侦听器创建时立即触发回调, 响应式数据变化之后继续执行回调

**5.**deep
默认机制:通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执
行,需要开启deep 选项

**6.**精确侦听对象的某个属性
需求:在不开启deep的前提下,侦听age的变化,只有age变化时才执行回调

**7.**总结

javascript
<script setup>
import { ref, watch } from "vue";
const count = ref(0)
const nickname = ref('灰太狼')
const changeCount = () => {
count.value++
}
const changeNickname = () => {
nickname.value = '红太狼'
}
// 1. 监视单个数据的变化
// watch(ref对象, (newValue, oldValue) => { ... })
// watch(count, (newValue, oldValue) => {
// console.log(newValue, oldValue)
// })
// 2. 监视多个数据的变化
// watch([ref对象1, ref对象2],(newArr, oldArr) => { ... })
// watch([count, nickname], (newArr, oldArr) => {
// console.log(newArr, oldArr)
// })
// 3. immediate 立刻执行
// watch(count, (newValue, oldValue) => {
// console.log(newValue, oldValue)
// }), {
// immediate: true,
// }
// 4. deep 深度监视,默认 watch 进行的是 浅层监视
// const ref1 = ref(简单类型) 可以直接监视
// const ref2 = ref(复杂类型) 监视不到复杂类型内部数据的变化
const userInfo = ref({
name: 'htl',
age: 18
})
const setUserInfo = () => {
// 修改了 userInfo.value 修改了对象的地址,才能监视到
// userInfo.value = { name: 'hettl', age: 20 }
userInfo.value.age++
}
// deep 深度监视
// watch(userInfo, (newValue) => {
// console.log(newValue)
// }, {
// deep: true
// })
// 5. 对于对象中的属性,进行监视
watch(() => userInfo.value.age, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
</script>
<template>
<div>{{ count }}</div>
<button @click="changeCount">改数字</button>
<div>{{ nickname }}</div>
<button @click="changeNickname">改昵称</button>
<div>-----------------------</div>
<div>{{ userInfo }}</div>
<butto @click="setUserInfo">修改userInfo</button>
</template>
八、组合式API - 生命周期函数
**1.**Vue3的生命周期API (选项式 VS 组合式)

**2.**生命周期函数基本使用
(1)导入生命周期函数
(2)执行生命周期函数 传入回调

**3.**执行多次
生命周期函数是可以执行多次的,多次执行时传入的回调会在时机成熟时依次执行

**4.**总结

javascript
<script setup>
// beforeCreate 和 created 的相关代码
// 一律放在 setup 中执行
import { onMounted } from "vue"
const getList = () => {
setTimeout(() => {
console.log('发送请求,获取数据')
}, 2000)
}
// 一进入页面的请求
getList()
// 如果某些代码需要在mounted生命周期中执行
onMounted (() => {
console.log('mounted生命周期函数 -逻辑1')
})
// 写成函数的调用方式,可以调用多次,并不会冲突,而是按照顺序依次输出
onMounted (() => {
console.log('mounted生命周期函数 -逻辑2')
})
</script>
<template>
</template>
九、组合式API - 父子通信
**1.**组合式API下的父传子
(1)基本思想
父组件中给子组件绑定属性
子组件内部通过props选项接收

(2)defineProps 原理:就是编译阶段的一个标识,实际编译器解析时,遇到后会进行编译转换

**2.**组合式API下的子传父
基本思想
(1)父组件中给子组件标签通过@绑定事件
(2)子组件内部通过 emit 方法触发事件

**3.**总结

javascript
<!-- 父组件 -->
<script setup>
// 父传子
// 1. 给子组件,添加属性的方式传值
// 2. 在子组件,通过props接收
// 子传父
// 1. 在子组件内部,emit触发事件(编译器宏获取)
// 2. 在父组件,通过 @ 监听
// 局部组件(导入进来就能用)
import { ref } from 'vue'
import SonCom from '@/components/son-com.vue'
const money = ref(100)
const getMoney = () => {
money.value += 10
}
const changeFn = (newMoney) => {
money.value = newMoney
}
</script>
<template>
<div>
<h3>
父组件 - {{ money }}
<button @click="getMoney">挣钱</button>
</h3>
<!-- 给子组件,添加属性方式传值 -->
<SonCom
@changeMoney="changeFn"
car="宝马车"
:money="money">
</SonCom>
</div>
</template>
javascript
<script setup>
// 子组件
// 注意:由于写了 setup,所以无法直接配置 props 选项
// 所以:此处需要借助于"编译器宏"函数来接收子组件传递的数据
const props = defineProps({
car: String,
money: Number
})
const emit = defineEmits(['changeMoney'])
console.log(props.car)
console.log(props.money)
const buy = () => {
// 需要 emit 触发事件
emit('changeMoney', 5)
}
</script>
<template>
<!-- 对于props传递过来的数据,模板中可以直接使用 -->
<div class="son">
我是子组件 - {{ car }} - {{ money }}
<button @click="buy">花钱</button>
</div>
</template>
<style scoped>
.son {
border: 1px solid #000;
padding: 30px;
}
</style>
十、组合式API - 模版引用
**1.**模板引用的概念
通过ref标识获取真实的dom对象或者组件实例对象

**2.**如何使用(以获取dom为例 组件同理)
(1)调用ref函数生成一个ref对象
(2)通过ref标识绑定ref对象到标签

**3.**defineExpose()
默认情况下在<script setup> 语法糖下组件内部的属性和方法是不开放给父组件访问的
可以通过defineExpose编译宏指定哪些属性和方法允许访问

**4.**总结

javascript
<script setup>
import TestCom from '@/components/test-com.vue'
import { onMounted, ref } from 'vue'
// 模板引用(可以获取dom,也可以获取组件)
// 1. 调用ref函数,生成一个ref对象
// 2. 通过ref标识,进行绑定
// 3.通过ref对象.value即可访问到绑定的元素(必须渲染完成后,才能拿到)
const inp = ref(null)
// 生命周期钩子 onMounted
onMounted(() => {
// console.log(inp.value)
// inp.value.focus()
})
const clickFn = () => {
inp.value.focus()
}
// ------------------------------
const testRef = ref(null)
const getCom = () => {
console.log(testRef.value.count)
testRef.value.sayHi()
}
</script>
<template>
<div>
<input ref="inp" type="text">
<button @click="clickFn">点击让输入框聚焦</button>
</div>
<TestCom ref="testRef"></TestCom>
<button @click="getCom">获取组件</button>
</template>
十一、组合式API - provide和inject
**1.**作用和场景
顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信

**2.**跨层传递普通数据
(1)顶层组件通过provide函数提供数据

(2)底层组件通过inject函数获取数据

**3.**跨层传递响应式数据
在调用provide函数时,第二个参数设置为ref对象
(1)顶层组件

(2)底层组件

**4.**跨层传递方法
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据
(1)顶层组件

(2)底层组件

**5.**需求解决思考

**6.**总结

javascript
<script setup>
import CenterCom from '@/components/center-com.vue'
import { provide } from 'vue'
// 1. 跨层传递普通数据
provide('theme-color', 'blue')
// 2. 跨层传递响应式数据
const count = ref(100)
provide('count', count)
setTimeout(() => {
count.value = 500
}, 2000)
// 3. 跨层传递函数 => 给子孙后代传递可以修改数据的方法
provide('changeCount', (newCount) => {
count.value = newCount
})
</script>
<template>
<div>
<h1>我是顶层组件</h1>
<CenterCom></CenterCom>
</div>
</template>
javascript
<script setup>
import { inject } from 'vue'
const themColor = inject('theme-color')
const count = inject('count')
const changeCount = inject('changeCount')
const clickFn = () => {
changeCount(1000)
}
</script>
<template>
<div>
<h1>我是底层组件 - {{ themColor }} - {{ count }}</h1>
<button @click="clickFn">更新count</button>
</div>
</template>
十二、Vue3.3新特性-defineOptions


十三、Vue3.3新特性-defineModel
Vue3 中的 v-model 和 defineModel

javascript
<script setup>
import MyInput from '@/components/my-input.vue'
import { ref } from 'vue'
const txt = ('123456')
</script>
<template>
<div>
<MyInput v-model="txt"></MyInput>
{{ txt }}
</div>
</template>
一、Pinia 快速入门
**1.**什么是Pinia
Pinia 是 Vue 的最新 状态管理工具 ,是 Vuex 的 替代品
(1)提供更加简单的API (去掉了 mutation )
(2)提供符合,组合式风格的API (和 Vue3 新语法统一)
(3)去掉了 modules 的概念,每一个 store 都是一个独立的模块
(4)配合 TypeScript 更加友好,提供可靠的类型推断
**2.**手动添加Pinia到Vue项目
(1)使用 Vite 创建一个空的 Vue3 项目
npm create vue@latest
(2)按照官方文档 安装 pinia 到项目中
**3.**Pinia基础使用 - 计数器案例
(1)定义store

(2)组件使用store

**4.**getters实现
Pinia中的 getters 直接使用 computed函数 进行模拟, 组件中需要使用需要把 getters return出去

**5.**action异步实现
(1)编写方式:异步action函数的写法和组件中获取异步数据的写法完全一致
(2)接口地址:http://geek.itheima.net/v1_0/channels
(3)需求:在Pinia中获取频道列表数据并把数据渲染App组件的模板中

**6.**storeToRefs工具函数
使用storeToRefs函数可以辅助保持数据(state + getter)的响应式解构


**7.**Pinia的调试
Vue官方的 dev-tools 调试工具 对 Pinia直接支持,可以直接进行调试

**8.**Pinia持久化插件
(1)官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
(2)安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
(3)main.js 使用
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
(4)store仓库中,persist: true 开启
**9.**总结

二、Vue3 大事件管理系统
**1.**大事件项目介绍 和 创建
(1)Vue3 大事件管理系统
①在线演示:https://fe-bigevent-web.itheima.net/login
②接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
③基地址:http://big-event-vue-api-t.itheima.net
(2)pnpm 包管理器 - 创建项目
一些优势:比同类工具快2倍左右、节省磁盘空间... https://www.pnpm.cn/
安装方式:npm install -g pnpm
创建项目:pnpm create vue

(3)创建项目
① 进入项目目录 ② 安装依赖 ③ 启动项目
=>

**2.**Eslint 配置代码风格
配置文件 .eslintrc.cjs
(1)prettier 风格配置 https://prettier.io
① 单引号 ② 不使用分号 ③ 宽度80字符
④ 不加对象|数组最后逗号 ⑤ 换行符号不限制(win mac 不一致)
(2)vue组件名称多单词组成(忽略index.vue)
(3)props解构(关闭)
提示:安装Eslint且配置保存修复,不 要开启默认的自动保存格式化

**3.**配置代码检查工作流
(1)提交前做代码检查
① 初始化 git 仓库,执行 git init 即可
② 初始化 husky 工具配置,执行pnpm dlx husky-init && pnpm install即可
https://typicode.github.io/husky/
③ 修改 .husky/pre-commit 文件

问题:pnpm lint 是全量检查,耗时问题,历史问题
(2)暂存区 eslint 校验
① 安装 lint-staged 包pnpm i lint-staged -D
② package.json 配置 lint-staged 命令
③ .husky/pre-commit 文件修改
=>

(3)总结

**4.**目录调整

**5.**vue-router4 路由代码解析
(1)路由初始化

=>

-
创建路由实例由 createRouter 实现
-
路由模式
① history 模式使用 createWebHistory()
② hash 模式使用 createWebHashHistory()
③参数是基础路径,默认/
(2)总结


创建一个路由实例,路由模式是history模式,路由的基础地址是 vite.config.js中的 base 配置的值,默认是 / , 环境变量地址:https://cn.vitejs.dev/guide/env-and-mode.html
**6.**引入 Element Plus 组件库
(1)按需引入 Element Plus
① 安装: pnpm add element-plus
② 配置按需导入:
官方文档:https://element-plus.org/zh-CN/guide/quickstart.html
③ 直接使用组件
④ 彩蛋:默认 components 下的文件也会被 自动注册~

**7.**Pinia 构建仓库 和 持久化
**8.**Pinia 仓库统一管理

(1)pinia 独立维护
现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一
优化:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
(2)仓库 统一导出
现在:使用一个仓库 import { useUserStore } from `./stores/user.js` 不同仓库路径不一致
优化:stores/index.js统一导出,导入路径统一`./stores`,而且仓库维护在stores/modules 中
**9.**数据交互 - 请求工具设计

**10.**整体路由设计

1.登录注册页面 [element-plus 表单 & 表单校验]
功能需求说明:
(1)注册登录 静态结构 & 基本切换
(2)注册功能 (校验 + 注册)
(3)登录功能 (校验 + 登录 + 存token)

2.首页 layout 架子 [element-plus 菜单组件]
功能需求说明:
(1)基本架子拆解 (菜单组件的使用)
(2)登录访问拦截
(3)用户基本信息获取&渲染
(4)退出功能 [element-plus 确认框]

3.文章分类页面 - [element-plus 表格]
功能需求说明:
(1)基本架子 - PageContainer 封装
(2)文章分类渲染 & loading 处理
(3)文章分类添加编辑 [element-plus 弹层]
(4)文章分类删除

源代码见C:\Users\lin\day13-big-event-admin
javascript
<template>
<div class="carousel-container">
<!-- 轮播图区域 -->
<div class="carousel-slides" @mouseenter="stopAutoPlay" @mouseleave="startAutoPlay">
<!-- 根据 currentIndex 显示当前图片 -->
<div
v-for="(item, index) in data"
:key="index"
class="slide"
v-show="currentIndex === index"
:style="{ backgroundColor: item.color }" <!-- 动态背景色 -->
>
<img :src="item.url" :alt="item.title" />
<div class="title">{{ item.title }}</div>
</div>
<!-- 左右按钮 -->
<button class="carousel-btn prev" @click="prevSlide">❮</button>
<button class="carousel-btn next" @click="nextSlide">❯</button>
<!-- 指示器小圆点(显示标题) -->
<div class="indicators">
<span
v-for="(item, index) in data"
:key="index"
class="indicator"
:class="{ active: currentIndex === index }"
:style="{ backgroundColor: currentIndex === index ? item.color : '#ccc' }"
@click="goToSlide(index)"
>
{{ item.title }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
// 你提供的数据(已修正最后一项的括号)
const data = [
{ url: './image/1.jpg', title: '111', color: 'green' },
{ url: './image/2.jpg', title: '222', color: 'yellow' },
{ url: './image/3.jpg', title: '333', color: 'blue' },
{ url: './image/4.jpg', title: '444', color: 'pink' },
{ url: './image/5.jpg', title: '555', color: 'red' },
{ url: './image/6.jpg', title: '666', color: 'brown' },
{ url: './image/7.jpg', title: '777', color: 'grey' },
{ url: './image/8.jpg', title: '888', color: 'skyblue' }
];
// 当前显示第几张(从0开始)
const currentIndex = ref(0);
let timer = null;
// 下一张
const nextSlide = () => {
if (currentIndex.value === data.length - 1) {
currentIndex.value = 0;
} else {
currentIndex.value++;
}
};
// 上一张
const prevSlide = () => {
if (currentIndex.value === 0) {
currentIndex.value = data.length - 1;
} else {
currentIndex.value--;
}
};
// 跳转到指定图片
const goToSlide = (index) => {
currentIndex.value = index;
};
// 自动播放
const startAutoPlay = () => {
if (timer) return;
timer = setInterval(() => {
nextSlide();
}, 3000);
};
const stopAutoPlay = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
// 组件挂载后启动自动播放,卸载时清除定时器
onMounted(() => {
startAutoPlay();
});
onUnmounted(() => {
stopAutoPlay();
});
</script>
<style scoped>
.carousel-container {
width: 800px;
margin: 0 auto;
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.carousel-slides {
position: relative;
width: 100%;
background-color: #f0f0f0;
}
.slide {
width: 100%;
transition: opacity 0.5s ease-in-out;
position: relative;
text-align: center;
}
.slide img {
width: 100%;
height: auto;
display: block;
}
.title {
position: absolute;
bottom: 20px;
left: 20px;
background-color: rgba(0,0,0,0.6);
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 18px;
}
/* 左右按钮 */
.carousel-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
font-size: 24px;
padding: 10px 16px;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.3s;
}
.carousel-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.prev {
left: 20px;
}
.next {
right: 20px;
}
/* 指示器(带标题的小圆点) */
.indicators {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
}
.indicator {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
color: white;
cursor: pointer;
transition: all 0.3s;
background-color: #ccc;
}
.indicator.active {
transform: scale(1.1);
box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
</style>