Vue笔记
- Vue2
- 01_Vue
- 02_响应式渲染
- 03_fetch & axios
- 04_过滤器
- 05_组件
- P36_组件定义
- P37_全局&局部组件
- P38_父传子
- P39_属性验证&默认属性
- P40-P41_子传父
- P42_中间人模式-亲兄弟通信
- P43_中央事件总线(bus)
- P44_ref 组件通信
- P45
- P46_动态组件
- P47_旧版 slot
- P48_新版 slot
- P49_插槽版抽屉
- P50_过渡效果
- P51_过渡中的 diff 算法(多个元素过渡)
- P52_列表过渡
- P53_可复用过渡
- P54_生命周期(组件)-创建阶段
- P55_生命周期-更新阶段
- P56_生命周期-销毁阶段
- P57_swiper 使用
- P58_swiper-vue 写法(没封装 全写根组件里了)
- P59-P60_swiper-组件
- P61_vue3 组件写法
- P62_vue3 生命周期&轮播改造
- 06_指令
- 07_单文件组件
- P66_使用 vue-cli 创建 vue 项目
- P67_启动流程&入口文件
- P68_eslint 修复
- P69-P72_单文件组件
- P73_反向代理&别名
- P74_SPA&路由引入
- P75_一级路由
- P76_重定向
- P77_声明式导航
- P78_嵌套路由
- P79_编程式导航
- P80_动态路由
- P81_命名路由
- P82_路由模式
- P83_全局路由拦截(全局路由守卫)
- P84_局部路由拦截(路由独享拦截方式)
- P85_路由懒加载
- P86-P87_rem
- P88-P89_swiper 组件
- P90_底部选项卡封装
- P91_二级声明导航封装
- P92-P93_正在热映页面
- P94-P101_详情页面
- P102-P103_影院组件
- P104_Element UI 组件库
- P105-P106_Vant 组件库
- P107-P108 正在热映页面-数据懒加载(组件库:List 列表)
- P109_loading 加载(组件库:Toast 轻提示)& axios 拦截器(Interceptors)
- P110-P112_city 组件
- P113_vuex 引入
- P114_vuex 同步工作流
- P115-P118_vuex 异步工作流
- P119_vuex 新写法
- P120_vuex 控制底部选项卡
- P121_vuex 持久化
- Vue 3
对应b站视频:【千锋Vue2.0+Vue3.0全套教程,vue.js零基础入门到vue项目实战,前端必学框架教程】 https://www.bilibili.com/video/BV1GL4y1v79M/?share_source=copy_web&vd_source=99b0080c20437dec6ac4e3437f81d47a
Vue2
01_Vue
P4_Vue 引入
<script src="地址">
// 设置 id
<div id="box">
// 双大括号
{{ 10+20 }}
{{ myname }}
</div>
<script>
var vm = new Vue({
// 将 box 下的内容交给 vue 管理
el: '#box',
data: {
// 状态
myname: 'syp'
}
})
</script>
vm.myname = 'tiechui'
- 无需操作 DOM,直接修改状态即可
P5_Vue 拦截原理
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。
类似于封装了以下代码:
<div id="box">
</div>
<script>
var obj = { }
var box = document.getElementById('box')
Object.defineProperty(obj, 'myname', {
// 访问时
get() {
console.log('get')
return box.innerHTML
},
// 修改时
set(value) {
console.log('set')
box.innerHTML = value
}
})
</script>
Vue2 的 Object.defineProperty 缺陷:
- 无法监听 ES6 的 Set、Map 变化
- 无法监听 Class 类型的数据
- 无法监听属性的新加或者删除
- 无法监听数组元素的增加和删除
- Vue3 Proxy 能够完美解决上述问题,唯一缺点是兼容性不好
02_响应式渲染
P6-P7_Vue 模板语法
<style>
.red {
background-color: red;
}
.yellow {
background-color: yellow;
}
</style>
<div id="box">
{{ myname }}-{{ myage }}
{{ 10>20?'aaa':'bbb' }}
// 冒号 动态绑定(v-bind 的简写)
<div :class="whichcolor">切换背景色1</div>
<div :class="isColor?'red':'yellow'">切换背景色2</div>
<img :src="imgpath">
// @ 绑定事件(v-on 的简写)
<button @click="handleChange()">change</button>
// v-show v-if 指令
<div v-show="isShow">我是动态显示和隐藏</div>
<div v-if="isCreated">我是动态创建和删除</div>
// v-for 列表渲染的指令
<ul>
<li v-for="(data, index) in list">{{ data }}-{{ index }}</li>
</ul>
</div>
<script>
new Vue({
el: '#box',
data: {
myname: 'syp',
myage: 18,
whichcolor: red,
isColor: true,
imgpath: '',
isShow: false,
isCreated: false
list: ['aaa', 'bbb', 'ccc', 'ddd']
},
// 方法
methods: {
handleChange() {
this.myname = 'tiechui'
this.myage = 20
this.whichcolor = 'yellow'
this.isColor = !this.isColor
this.imgpath = '地址'
this.isShow = !this.isShow
this.isCreated = !this.isCreated
}
}
})
</script>
:class="whichcolor"
不动态绑定的话传的只是一个字符串- 动态绑定后 引号之内就是 JS 代码
P8_todolist
思路:
- v-for 遍历数组显示列表
- 通过向数组中增添元素和删除元素实现列表的增添和删除
- v-model 双向绑定;利用其绑定输入框的 value
<input type="text" v-model="mytext">
P9_v-html 指令
- 把一段 html 页面直接渲染在页面上
{{ }} 默认不解析:
- 为什么不支持直接解析:防止XSS、CSRF攻击等
- 前端过滤
- 后台转义
- 给 cookie 加上属性 http
- 为什么要解析:后端发来的有用的标签需要解析
<div v-html="myhtml"></div>
P10_点击变色
- 当前点击的 li 索引与 index 相同时,则将其加上高亮属性
<ul>
<li v-for="(data, index) in datalist" :class=" current===index?'active':'' " @click="handleClick(index)">
{{ data }}
</li>
</ul>
P11_class&style(动态绑定时)- vue2
动态切换 class
<style>
.aa {
}
……
</style>
<div :class="classobj">动态切换class-对象</div>
<div :class="classarr">动态切换class-数组</div>
var vm = new Vue({
……
data: {
clssobj: {
aa: true,
bb: true,
cc: true
},
clssarr: ["aa", "bb", "cc"]
}
})
对象写法:
切换:vm.classobj.cc = false
vue2 不支持动态添加属性的拦截
解决方案:Vue.set(vm.classobj, "dd", true)
数组写法:
切换:数组操作
添加:数组.push("新属性")
动态切换 style
<div :style="styleobj">动态切换style-对象</div>
<div :style="stylearr">动态切换style-数组</div>
var vm = new Vue({
……
data: {
styleobj: {
backgroundColor: 'red'
},
stylearr: [{backgroundColor: 'red'}]
}
})
对象写法:
切换:vm.styleobj.backgroundColor = 'bule'
vue2 不支持动态添加方法的拦截
解决方案:Vue.set(对象, "属性", "值")
数组写法:
切换:数组操作
添加:数组.push({属性: "值"})
- 其实 vue2 同样拦截不到数组的改变,只是重写了数组方法;vue3 则从底层解决了这个 bug
P12_class&style(动态绑定时)-vue3
- Vue3 改为函数式写法是为了在组件化开发时避免不同组件之间的命名冲突
动态切换 class
对象.属性 = bool值
动态切换 style
对象.属性 = "值"
P13_条件渲染
<div v-if=""></div>
<div v-else-if=""></div>
<div v-else></div>
<template> // 包装作用,不破坏原 DOM 结构
<div></div>
<div></div>
<div></div>
</template>
P14_列表渲染
<li v-for="(item, index) in/of datalist"></li>
// 遍历对象
<li v-for="(key, value) in/of obj"></li>
P15_key 设置
- vue 在每一次创建数组后,先会创建一份虚拟 DOM,然后再根据其创建出真实的 DOM 节点
- 虚拟 DOM:用 JS 对象描述的真实 DOM 节点
- 直接创建真实 DOM 节点所需的代价太大
- 更新数组后根据新的数据创建一份新的虚拟 DOM,以最小的代价更新真实 DOM 节点
// key 值的作用是跟踪每个节点的身份,从而重用和重新排序现有元素
// 理想的 key 值是每一项都有且唯一的 id (后端提供)
<li v-for="(item, index) in datalist" :key="item.id"></li>
P16_检测数组变动
-
可以检测到变动的数组操作(底层检测不到,其实是 vue2 对数组方法进行了重写):
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
-
检测不到变动,用新数组替换旧数组
- filter()
- concat()
- slice()
- map()
-
检测不到变动的数组操作
- vm.数组[索引] = “值”
- 解决:
- splice
- Vue.set(数组, 索引, 值)
-
vue3 直接在底层支持对数组的拦截
P17-P18_模糊查询
@change
失去焦点且 value 改变触发
@input
value 改变即触发
v-model
双向数据绑定- 用
@input
检测输入框状态改变时触发事件 - 用
filter
方法筛选符合条件的数组项 - 用一个新数组保存过滤后的数组项
函数表达式写法
v-model
双向数据绑定v-for
遍历一个test()
方法的返回值test()
方法返回一个filter
过滤后的数组v-model
双向数据绑定后只要拦截到数据发生变化,与其相关的方法都会重新执行
P19_事件处理器
// 传参 ($event 为事件对象)
<button @click="handleAdd1($event, 参数1, ……)">函数表达式写法</button>
// 不传参
<button @click="handleAdd2">函数名写法</button>
<button @click="表达式">表达式写法</button>
handleAdd1(evt, a, ……) { }
// 可得到事件对象 evt
handleAdd2(evt) {
// .target 事件源
// .target.value 值
console.log(evt.target)
}
P20_事件修饰符
@click.stop
事件触发后阻止冒泡
@click.self
只有事件源是自身会触发事件,点击孩子不会
@click.once
只触发一次
@click.prevent
阻止默认行为
……
P21_按键修饰符
@keyup.enter
按下回车键后触发
- .esc .up .down .left .right .space .ctrl .shift .delete……
- 可以组合使用
- .keyCode
P22_表单控件绑定
- 获取一个 checkbox 单选框的状态的方法:双向数据绑定一个 bool 值
<input type="checkbox" v-model="isChecked"> 记住用户名
- checkbox 多选框:双向数据绑定一个数组
<input type="checkbox" v-model="checkList" value="a"> a
<input type="checkbox" v-model="checkList" value="b"> b
<input type="checkbox" v-model="checkList" value="c"> c
// 用 “数组” 存储每个选项的勾选情况
// 必须加 “value” 属性才能将勾选情况存入 “checkList”
- radio:双向数据绑定一个字符串
<input type="radio" v-model="select" value="a"> a
<input type="radio" v-model="select" value="b"> b
// 双向数据绑定一个字符串
P23-25 购物车
P26_表单修饰符
v-model.lazy
失去焦点之后再生效
v-model.number
改成数字类型
v-model.trim
去首尾空格
P27_计算属性
computed: {
// 防止模板过重难以维护
// 负责逻辑放在计算属性中来写
return
}
method 和 computed 对比:
- 一个方法/计算属性多次使用时,计算属性会缓存(只调用一次)
P29_watch
- 计算属性和函数表达式只能用于同步请求(立即执行)
- 处理异步请求时,可以用 “过滤应用” 的 “@input”
- 处理异步请求时,可以用 “watch” 监听 “mytext” 改变
watch: {
mytext(newval) {
console.log("改变了", newval)
}
}
- data => 是状态,可以被拦截
- method => 事件绑定、逻辑计算,可以不用 return,没有缓存
- computed => 解决模板过重问题,只重视结果,必须有 return,有缓存,同步
- watch => 监听一个值的改变,可以不用 return,可处理异步
03_fetch & axios
P30_fetch-get
-
ajax 是一种异步请求数据、局部更新页面的技术
-
xhr 是原生 JS 实现 ajax 的一种方法
-
fetch 是一种新的实现 ajax 的方法
-
xhr 与 fetch 都是标准,自带的
-
解决fetch 低版本浏览器兼容性问题 https://github.com/camsong/fetch-ie8
handleFetch() {
fetch(请求地址)
.then(res => {
// res 中是状态码、响应头
return res.json()
})
.then(res => {
// 第二个 .then 中是 json 数据
console.log(res)
})
.catch(err => {
console.log(err)
})
}
P31_fetch-post
get 传参:url路径 ?name=syp&age=100
post 传参:body请求体
(1)x-www-formurlencoded, "name=syp&age=100"
(2)json, {name:"syp", age:100}
两种 post 请求的方法(根据后端要求)
fetch(请求地址, {
method: "post",
// 请求头 要求传 form 格式编码数据(key=value)
header: {
"Content-Type": "application/x-www-formurlencoded"
},
// 参数
body: "name=syp&age=100"
})
.then(res => res.json())
.then(res => console.log(res))
fetch(请求地址, {
method: "post",
// 请求头 要求传 json 格式编码数据
header: {
"Content-Type": "application/json"
},
// 参数
body: JSON.stringfy({
name: "syp",
age: 18
})
})
.then(res => res.json())
.then(res => console.log(res))
P33_axios
<script src="axios.js位置">
// get 请求
axios.get(请求地址).then(res => {
// 数据在 res.data 中
console.log(res.data)
})
// post 请求
// axios 会自动根据传的数据是判断什么类型
axios.post("请求地址", "name=syp&age=100").then(res => {})
axios.post("请求地址", {name:"syp", age:100}).then(res => {})
04_过滤器
P35_过滤器
// 将原始数据送入过滤器
<img :src="item.img | imgFilter">
Vue.filter("imgFilter", (url) => {
return url.replace('w.h/', '')+'@……'
})
- 可以写多个
<img :src="item.img | imgFilter1 | imgFilter2 ……">
- vue3 不支持
05_组件
P36_组件定义
- 扩展HTML元素,封装可重用代码
<navbar></navbar>
// 定义一个全局组件
Vue.component("navbar", {
// dom, js, css
template: `
<div style="background:red">
<button @click="handleLeft">left</button>
猫眼电影
<button @click="handleRight">right</button>
</div>
`,
methods: {
handleLeft() {
},
handleRight() {
}
},
computed: {},
watch() {},
// data 必须是函数写法
data() {
return {
key: value,
}
}
})
- 组件名:HTML 采用连接符命名,JS 采用驼峰命名法
- template 只能包含一个根节点
- 自定义的组件中的 data 必须是一个函数
- dom 片段没有代码提示和高亮显示 — vue 单文件组件解决
- css 只能写成行内 — vue 单文件组件解决
- 所有组件都写在一起 — vue 单文件组件解决
- 组件无法【直接】访问其他组件的状态或方法 — 间接的组件通信来交流
P37_全局&局部组件
<div id="box">
<father-navbar></father-navbar>
// 全局定义的组件可以在全局使用
<child-navbar1></child-navbar1>
</div>
Vue.component("fatherNavbar", {
template: `
<div>
……
<child-navbar1></child-navbar1>
// 局部定义的组件只能在当前组件内部使用
<child-navbar2></child-navbar2>
</div>
`,
……
// 局部定义组件
components: {
"childNavbar2": { …… }
}
})
// 全局定义组件
Vue.component("childNavbar1", {
……
})
- 父子关系看谁放在谁内
<father-navbar>
放在box
内,就是box
的子组件<child-navbar1>
和<child-navbar2>
放在<father-navbar>
内,就是<father-navbar>
的子组件
P38_父传子
- 字符串、布尔值、状态等等都能传
P39_属性验证&默认属性
<div id="box">
<navbar myname="电影" :myright="true" :myfather="father"></navbar>
<navbar myname="影院" :myright="false" :myfather="father"></navbar>
</div>
Vue.component("navbar", {
// props: ["myname"],
// 属性验证+默认属性
props: {
myname: {
type: String,
default: ""
},
myright: {
type: Boolean,
default: true
},
myfather: {
type: String,
default: ""
}
},
template: `
<div>
<button>left</button>
<span>{{myname}}--{{myfather}}</span>
<button v-show="myright">right</button>
</div>
`,
})
new Vue({
el: "box",
data: {
father: "11111"
}
})
P40-P41_子传父
应用场景举例:实现抽屉功能
<div>
// 在父组件中写一个监听事件 "myevent"
// myevent 事件被触发后执行 handleEvent 方法
<navbar @myevent="handleEvent"></navbar>
<sidebar v-show="isShow"></sidebar>
</div>
Vue.component("navbar", {
template: `
<div>
<button @click="handleClick()">点击</button>-导航栏
</div>
`,
methods: {
// 在孩子组件中写 一点按钮就触发 "myevent" 事件
// 还能顺带传值
handleClick() {
this.$emit("myevent", 1000)
}
}
})
Vue.component("sidebar", {
template: `
<div>
<ul>
<li>111</li>
<li>111</li>
<li>111</li>
<li>111</li>
<li>111</li>
<li>111</li>
</ul>
</div>
`
})
new Vue({
el: "box",
data: {
isShow: true
},
methods: {
// 不写 小括号 时,自带一个形参 data
handleEvent(data) {
console.log(data)
this.isShow = !this.isShow
}
}
})
P42_中间人模式-亲兄弟通信
- 父组件先把接收到的数据传给子组件
- 子组件点击事件触发父组件监听事件,并把数据传给父组件
- 父组件把得到的数据传给另一个子组件
P43_中央事件总线(bus)
- 采用 订阅发布模式
Var bus = new Vue()
// 发布者 触发事件
bus.$emit("事件名称", 数据)
// 订阅者 监听事件
// 为了让组件刚创建好就可以监听 放在组件生命周期的 mounted 中
bus.$on("事件名称", (data) => {})
P44_ref 组件通信
<div id="box">
<input type="text" ref="mytext">
</div>
methods: {
handleAdd() {
// 拿到的是节点
console.log(this.$refs.mytext)
}
}
- ref 绑定 dom 节点,拿到的就是 dom 对象
- ref 绑定组件,拿到的就是组件对象
- ref 绑定组件时,父组件可以拿到和修改子组件的状态,可以作为父传子通信的一种方法,但是容易造成数据流的紊乱
P45
-
状态:组件内部的,可以随意修改
-
属性:父组件赋予的,只有父组件可以重新传,不允许随意修改
- 子组件随意修改父组件传的属性会造成 父子组件中的值不同,数据流紊乱
-
v-once
让内容只计算一次然后缓存起来,再改也不更新了
P46_动态组件
<component is="你说我是啥组件我就是啥组件"></component>
- 弊端:切换组件后,上一个组件就被销毁了
- 解决
<keep-alive>
<component is="你说我是啥组件我就是啥组件"></component>
</keep-alive>
// 其实是先暂存到内存中了
// 但是自定义组件中写组件 需要插槽来实现
P47_旧版 slot
<child>
<div>111111111</div>
<div>222222222</div>
<div slot="a">333333333</div>
</child>
Vue.component("child", {
template: `
<div>
……
// 单个插槽 有啥全放进去
<slot></slot>
<slot></slot>
// 具名插槽
<slot name="a"></slot>
</div>
`
})
// 1 2 1 2 3
- 插槽的意义:扩展组件能力,提高组件复用性
P48_新版 slot
<child>
// 老写法 不是指令也不是属性 不伦不类
<div slot="a">111111111</div>
// 新写法 指令写法
<template v-slot:b>
<div>222222222</div>
</template>
// 简写
<template #c>
<div>333333333</div>
</template>
</child>
Vue.component("child", {
template: `
<div>
……
<slot name="a"></slot>
<slot name="b"></slot>
<slot name="c"></slot>
</div>
`
})
P49_插槽版抽屉
<navbar>
// 留了一个插槽 把 @click 写在了父组件中
<button @click="isShow=!isShow">click</button>
</navbar>
<sidebar v-show="isShow"></sidebar>
Vue.component('navbar', {
template: `
<div>
navbar
<slot></slot>
</div>
`
})
插槽的意义:
- 扩展组件能力,提高组件复用性
- 简化子父通信
- 父组件模板中的内容在父组件作用域内编译;子组件模板中的内容在子组件作用域内编译
P50_过渡效果
.syp-enter-active {}
.syp-leave-active {}
<div id="box">
// transition 可监测 isShow 改变
// 只能监测节点改变,监测不到内容改变
<transition enter-active-class="syp-enter-active" leave-active-class="syp-leave-active">
<div v-show="isShow">111111111</div>
</transition>
// 简写
// appear 初始就带效果
<transition name="syp" appear>
<div v-show="isShow">111111111</div>
</transition>
<button @click="isShow = !isShow">click</button>
</div>
P51_过渡中的 diff 算法(多个元素过渡)
- transition 包裹多个相同的节点时,当节点之间的关系是互斥时会失效
- 原因:虚拟 dom 在对比时,会先对比标签,如果标签相同则只会替换内容
- 解决方法:设置 key
<transition name="syp" mode="out-in"> // mode 先走再来
<div v-if="isShow" key="1">11111</div>
<div v-else key="2">22222</div>
</transition>
- Vue 底层更新原理:
- 通过 vue2 中 Object.defineProperty 的 get/set 拦截,vue3 中通过 Proxy 进行拦截,拦截到数据发生改变后,通过 watcher 通知所有与该数据相关的组件重新进行渲染。
- 渲染时会创建一份新的虚拟 dom 节点(本质是对象),对比老的虚拟 dom。
- 对比时,vue 内置了一套 diff 算法来保证最优的效率,以最小的代价更新 dom。
- 按树的结构,层级相同才对比
- 在列表中,按 key 值对比
- 标签、组件名字一样时会对比,否则直接删除重新创建
P52_列表过渡
- 多个组件的切换:动态组件+过渡
<transition name="syp" >
<component :is="which"></component>
</transition>
- 多个列表的过渡
// 实现列表插入、删除的动画效果
<ul>
<transition-group> // 可以放多个节点
<li v-for="" :key=""><li>
</transition-group>
</ul>
- transition-group 会默认实例化为一个 span 标签
- 属性:
tag="要实例化的标签"
P53_可复用过渡
- 动画写在组件里
// 传一个自定义的 mode 属性控制动画效果
<sidebar v-show="isShow" mode="left"></sidebar>
template: `
// 接收 mode,选择对应的 css 样式
<transition :name="mode" tag="ul">
<li>……</li>
<li>……</li>
<li>……</li>
</transition>
`
P54_生命周期(组件)-创建阶段
-
beforeCreate 没啥用
-
created 可初始化状态或者挂载到当前实例的一些属性
-
beforeMount 可在模板解析之前最后一次修改模板节点
-
mounted 可拿到真实的 dom 节点
- 一些依赖于 dom 创建之后才进行初始化工作的插件
- 轮播
- 订阅 bus.$on
- 发 Ajax
- 一些依赖于 dom 创建之后才进行初始化工作的插件
-
属于创建阶段,只在初始化时执行一次
P55_生命周期-更新阶段
-
beforeUpdate 可记录老的 dom 的某些状态
-
updated 可获取更新后的 dom
- Ajax 更新取到的数据在这里进行操作
- 因为更新数据后还需要创建虚拟 dom,diff对比 — 状态立即更新,dom 异步更新
- Ajax 更新取到的数据在这里进行操作
-
属于更新阶段,每次数据更新时都会执行
P56_生命周期-销毁阶段
- beforeDestroy 可清除定时器、事件解绑……
- destroyed 可清除定时器、事件解绑……
P57_swiper 使用
-
静态: https://www.swiper.com.cn/usage/index.html
-
动态(操作 dom 写法)
<div class="swiper myswiper">
<div class="swiper-wrapper"></div>
……
</div>
setTimeout(() => {
// 模拟 Ajax
var list = ["aaa", "bbb", "ccc"]
var newlist = list.map(item => `<div clsaa="swiper-slide">${item}</div>`)
var owrapper = document.querySelector(".swiper-wrapper")
owrapper.innerHTML = newlist.join('')
// 取完数据,创建完 dom 后
// 再初始化 swiper
init()
}, 2000)
function init() {
// 直接写外面会导致初始化过早
// 初始化类名是 “myswiper” 的 swiper 组件
new Swiper('.myswiper', { …… })
}
P58_swiper-vue 写法(没封装 全写根组件里了)
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="data in datalist" :key="data">
<img :src="data" alt="">
</div>
</div>
……
</div>
mounted() {
// 异步数据
setTimeout(() => {
this.datalist = ["", "", ……]
// new Swiper 在这不行
// 因为 dom 异步更新
}, 2000)
// new Swiper 在这不行
// 因为数据还没取到
}
updated() {
new Swiper()
}
当前问题:
- 无法复用
- 如果当前页面还有其他数据更新,
new Swiper()
会被执行多次
P59-P60_swiper-组件
// 方法一:
// 给 swiper 加上 :key="datalist.length" 初始为0
// 当 key 值改变,原始的 swiper 被删除
// 重新执行 mounted 生命周期,创建一个新的 swiper
<swiper :key="datalist.length" :loop="false">
// 方法二:
<swiper v-if="datalist.length" :loop="false">
<swiper-item v-for="data in datalist" :key="data">
<img :src="data">
</swiper-item>
</swiper>
Vue.component("swiperItem", {
template: `
<div class="swiper-slide">
<slot></slot>
</div>
`
})
Vue.component("swiper", {
prop: {
loop: {
type: Boolean,
default: true
}
},
template: `
<div class="swiper">
<div class="swiper-wrapper">
<slot></slot>
</div>
……
</div>
`,
mounted() {
new Swiper(".swiper", {
// 分页器
pagination: {
el: '.swiper-pagination'
},
loop: this.loop,
autoplay: {
delay: 2500,
disableOnInteraction: false
}
})
}
})
new Vue({
……
mounted() {
setTimeout(() = {
……
}, 2000)
}
})
tips:
父组件(Vue)更新,子组件(swiper)一定会重新渲染
最终效果:
- 能传值(父子通信)、能控制组件特性、有默认值、能对所传值进行验证
- 可轮播任意内容:图片、文字……(插槽)
- 能处理异步数据(生命周期)
P61_vue3 组件写法
var obj = {
data() {
return: {
}
},
……
}
Vue.createApp(obj)
// 定义组件
.component("navbar" {
……
})
// 最后上树
.mount("#box")
P62_vue3 生命周期&轮播改造
06_指令
P63_指令写法
- 作用:把 DOM 操作放在指令中封装,用自定义指令实现业务需求
<div v-hello="参数">1111</div>
Vue.directive("hello", {
// 指令生命周期 inserted 只在第一次会触发
inserted(el, binding) {
// el 是 dom 节点;bindig 是对象,binding.value 是传的参数
dom 操作……
},
// 更新触发
update() {
}
})
P64_指令应用
- 指令绑在哪里就能够知道 dom 什么时候创建完成,从而进行依赖 dom 的初始化工作
swiper 指令写法
<div class="swiper">
<div class="swiper-wrapper">
// 指令传参只能传一个值,但是可以传对象
<div class="swiper-slide" v-for="(data, index) in datalist" :key="data"
v-swiper="{index: index, length: datalist.length}">
<img :src="data" alt="">
</div>
</div>
……
</div>
Vue.dircetive("swiper", {
inserted(el, binding) {
let {index, length} = binding.value
// 还是当数据全部插入,dom 创建完毕后再 new Swiper
if(index === length-1) {
new Swiper(……)
}
}
})
new Vue({
……
mounted() {
setTimeout(() = {
……
}, 2000)
}
})
P65_指令补充 & nextTick
指令函数简写
Vue.directive("名称", (el, binding) => {
// 这样写 在创建和更新时都会执行
})
指令生命周期(vue2)
- bind(类似 created):只在指令第一次绑定到元素时调用一次。可在此进行一次性的初始化设置
- inserted(类似 mounted):被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入到 dom 中)
- update (类似 beforeUpdate):所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生改变也可能没有,可以通过比较更新前后的值来忽略不必要的模板更新。
- componentUpdated(类似 updated):指令所在组件的 VNode 及其子 VNode 全部更新后调用
- unbind (类似 destroyed):只在指令与元素解绑时调用一次
Vue3 写法
var app = Vue.createApp(obj)
app.directive()
app.mount('#box')
vue3 指令生命周期(新写法)
- created
- beforeMount
- mounted
- beforeUpdate
- uodated
- BeforeUnmount
- unmounted
nextTick
new Vue({
……
mounted() {
setTimeout(() => {
this.datalist = []
// nextTick 比 updated 执行的都晚且只执行一次
// 在 datalist 更新到 dom 之后触发一次
// 是一种捷径 但是无法复用
this.$nextTick(() => {
new Swiper(……)
})
}, 2000)
}
})
07_单文件组件
P66_使用 vue-cli 创建 vue 项目
-
单文件组件 .vue
-
浏览器只认识 .html .css .js 文件,需要配置环境将 .vue 文件转化为上述
-
需配置:webpack、babel、sass、postcss……
-
使用 vue-cli 脚手架 帮助配置
-
Vue CLI 官方文档 安装……
项目创建
- 打开 cmd
vue create 文件夹名称
- Please pick a preset: Manually select features(手动配置)
- Check the features needed for your project: Choose Vue version、Babel、Router、Vuex、CSS Pre-processors、Linter/Fprmattor
- Choose a version of Vue.js that you want to start the project with: 2.x
- Use history mode for router? YES
- Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
- Pick a linter/formatter confing: ESLint + Standard config(ESLint 标准配置)
- Pick addition lint features: Lint on save、Lint and fix on commit(保存自动 Lint、提交自动 Lint 且 自动修复)
- Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files(选择将这些配置配置到 package.json 中,还是每个有自己的配置文件)
- Save this as a preset for future projects? NO
- Babel 可将 ES6 代码转换为 ES5 代码
- PWA 谷歌开发使用
- Router 路由 + Vuex 多路由开发,做动态管理
- Linter/Fprmattor 用于代码规范
P67_启动流程&入口文件
-
package.json
记录了项目下安装的所有模块 -
npm run serve
启动 开发阶段 /npm start
-
npm run build
编译 提交阶段 -
npm run lint
修复代码格式错误 -
–save(-S):安装包信息将加入到 dependencies(生产阶段的依赖,也就是项目运行时的依赖,程序上线后仍需要使用)
-
–save-dev(-D):安装包信息将加入到 devDependencies(开发阶段的依赖,只在开发阶段使用到)
-
不再需要 live service,直接在服务器中运行
-
入口界面 index.html
-
入口 main.js
import Vue from 'vue' // ES6 导入方式
import App from './App.vue' // 导入根组件 APP
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router, // 把导入的 router 对象挂到 this.$router 这个属性上了(this.$router === router)
store, // this.$store === store
render: h => h(App) // 将 App.vue 中的根组件渲染实例化后
}).$mount('#app') // 挂载到 index.html 的 app 节点上
P68_eslint 修复
解决 eslint 代码格式报错问题
npm run lint
- ESLint 插件(不好配置)
- 暂时关闭 eslint,提交时再打开
- 根目录下创建 vue.config.js(vue 项目的配置文件,在其中的代码会覆盖掉原始的设置),在其中添加代码
lintOnSave: false
- 根目录下创建 vue.config.js(vue 项目的配置文件,在其中的代码会覆盖掉原始的设置),在其中添加代码
P69-P72_单文件组件
// dom
<template>
<> // 里面只能包裹一个标签
<>
</template>
// js
<script>
// ES6 导出规范
export default {
}
</script>
// css
<style lang="scss" scoped>
// scss 写法
// scoped 局部作用域(会为组件添加一个独一无的属性),不会被父组件影响
</style>
- 插件 Vetur 代码提示、高亮显示
- public 文件夹下存放的是静态资源
组件引入&注册
- src 下创建 mycomponents 文件夹,存公共组件
- 文件夹名小写
- 组件名首字母大写
import navbar from './mycomponents/Navbar'
引入单文件组件 navbarimport Vue from 'vue'
模块化开发 在哪用在哪引Vue.component('navbar', navbar)
引入后还要注册(全局注册(放 main.js 中),将原来 {} 内的内容替换成 navbar)- 局部注册(在哪用在哪注册)(不需要步骤 5 6)
export default {
……
compontent: {
// 将 {} 内的内容替换成 navbar
navbar: navbar
}
}
组件通信
父传子、传值验证、插槽、子传父……都没变化
生命周期
- 生命周期正常运行
- fetch、axios 正常使用
- axios 下载
cnpm i --save axios
后 import
指令
import Vue from 'vue'
- 没变化
- 过滤器也没变化
P73_反向代理&别名
本地 A 向远程 B 发送请求 跨域
解决:反向代理,本地 A 向本地服务器发请求,本地服务器再向远程 B 发请求
- 在 vue.config.js 中配置
module.exports = {
devServer: {
proxy: {
// 凡是向 syp 发请求的
'/syp': {
// 都转向 target
target: 'https://m.maoyan.com',
changeOrigin: true,
pathRewrite: {
// 执行时把 '/syp' 替换成 ''
'^/syp': ''
}
}
}
}
}
补充:
“@” 符号是别名,永远指向 src 的绝对路径,也可自己配置
- 官方文档:Vue Router
P74_SPA&路由引入
单页面应用(SPA) | 多页面应用(MPA) | |
---|---|---|
组成 | 由一个外壳页面和多个页面片段组成 | 由多个完整页面组成 |
资源共用 | 共用 只需要在外壳部分加载 | 不共用 每个页面都需要加载 |
刷新方式 | 页面局部刷新或更改 | 整页刷新 |
url模式 | a.com/#/pageone a.com/#/pagetwo | a.com/pageone.html a.com/pagetwo.html |
用户体验 | 页面片段间的切换快,用户体验良好 | 页面切换加载缓慢 |
转场动画 | 容易实现 | 无法实现 |
数据传递 | 容易 | 依赖url传参或者cookie、localStorage等 |
搜索引擎优化(SEO) | 需单独方案,实现较为困难,不利于SEO搜索。 可利用服务器端渲染(SSR)优化 | 实现方法容易 |
适用范围 | 适用于体验度要求高、追求页面流畅的应用 | 适用于追求高度支持搜索引擎的应用 |
开发成本 | 较高,常需要借助专业的框架 | 较低,但是页面重复代码多 |
维护成本 | 相对容易 | 相对复杂 |
- 单页面应用开发实现方法:Vue路由
- 路由(router):一张映射表,基于不同的路径,加载不同的组件
P75_一级路由
- 在 main.js 中引入 router 文件、实例化
- 在 APP.vue 根组件中留一个路由容器
<router-view></router-view>
- 在 src 下的 views 文件夹内写组件
- 在 router 文件夹下的 index.js 文件内写路由配置
import Vue from 'vue'
import VueRouter from 'vue-router'
import Films from '@/views/Films.vue'
import Films from '@/views/Cinemas.vue'
import Films from '@/views/Center.vue'
// 注册路由插件
// 其实就在全局定义了俩标签 router-view、router-link
Vue.use(VueRouter)
// 配置表
const routes = [
{
path: '/films', // localhost8080/#/films
component: Films
},
{
path: '/cinemas',
component: Cinemas
},
{
path: 'center',
component: Center
},
]
const router = new VueRouter({
routes
})
P76_重定向
{
path: '*'
redirect: '/films'
}
// 运行步骤是先找其它的,找不到之后再走重定向
P77_声明式导航
路由的底层原理:
- BOM 方法
window.onhashchange
能检测到哈希值的改变,每当路径发生改变就触发 location.hash
能拿到哈希值
<ul>
<li>
// 会自动判断路由模式
// 自动判断路径加不加 “#”
<router-link to="/films" active-class="syp-ctive">电影</router-link>
</li>
<li>
<router-link to="/cinemas" active-class="syp-active">影院</router-link>
</li>
// 在每次被选中后会自动添加一个 ‘router-link-active’ 的类名
// 也可以重命名类名 active-class="新类名"
// 自定义一下这个类名的样式 就可以实现选中之后高亮且刷新之后不消失
</ul>
<style lang='scs'>
.syp-active {
}
<style>
- router-link 有 tag 属性,但在 vue4 中被移除
vue3 写法(新写法):
// navigate 跳转函数
// isActive 记录是否选中
<router-link to="/films" custom v-slot="{navigate, isActive}">
// tag 变成了想绑啥直接写,可以套随意层
// class 变成了要自己写
<li @click="navigate" :class="isActive?'sypactive':''">电影</li>
</router-link>
tips:
- 每个原生属性或者组件的自定义属性都是支持动态绑定的
- 如:可以
:to=""
用v-for
遍历一个数组
P78_嵌套路由
- views 文件夹下新建一个 films 文件夹
- 写二级路由
{
path: '/films',
component: Films,
// 这样 nowplaying 不会覆盖 films
// 而是会在 films 中给它预留好的容器(router-view)显示
cildren [
{
path: '/films/nowplaying',
component: Nowplaying
},
{
path: '/films/comingsoon',
component: Comingsoon
},
// 在 films 中再次重定向
// 会先走 films
// 进入 films 后再重定向
{
path: '/films',
redirect: '/films/nowplaying'
}
]
},
// 这样 search 会替换掉 cinemas
{
path: '/cinemas/search',
component: Search
}
- 在 films 中预留好一个路由容器
<router-view></router-view>
P79_编程式导航
// 例子:列表跳详情
<ul>
<li v-for="data in datalist" :key="data" @click="handleChangePage()"></li>
</ul>
methods: {
handleChangePage () {
// 老的编程式导航
location.href = '#/detail'
// vue 封装后的编程式导航
// 好处:自动判断路由模式(加不加 “#”)
this.$router.push('/detail')
}
}
P80_动态路由
列表跳详情步骤:
- 从后端(获取列表的接口)请求到列表数据,布局到页面
- 点击一条数据后跳转到详情页面
- 获取 id(每次跳转的详情数据不同,都布局到同一个详情页面,所以要根据 id 向后端请求对应的数据)
- 利用获取到的 id 发请求给后端(获取详情的接口)
- 后端传来对应数据,布局到页面
第3、第4步的实现方法——动态路由:
- 在路由表中
{
// 动态路由格式
path: '/detail/:myid', // ":" 代表可以随意写
component: Detail
}
- 列表的编程式导航中
methods: {
handleChangePage (id) {
// 跳转页面
this.$router.push(`/detail/${id}`)
}
}
- 在 detail 页面中
export default {
created () {
// router 中拿到的是整个路由
// route 中拿到的是当前匹配的路由,params 中是占位的参数的实际值
console.log(this.$route.params.myid)
// 然后 axios 利用 id 发送请求到后端的详情接口
}
}
P81_命名路由
第3、第4步的实现方法——动态路由之命名路由:
- 在路由表中
{
name: 'sypDetail'
path: '/detail/:myid',
component: Detail
}
- 列表的编程式导航中
methods: {
handleChangePage (id) {
// 跳转页面
this.$router.push({
name: 'sypDetail', // 向名为 “sypDetail” 的命名路由跳转
params: {
myid: id
}
})
}
}
- ……
P82_路由模式
(1)hash 路由
location.hash
切换window.onhashchange
监听路径切换
(2)history 路由
-
history.pushState
切换 -
window.onpopstate
-
vue-router 给封装成 “router” 了
-
默认 hash 模式(带 “#”)
-
可改为 history 模式
const router = new VueRouter({
mode: 'history',
routes
})
history 模式:
优点:更优美、更安全
缺点:访问一个 url 时浏览器也无法区分出是前端路由还是后端路由(不知道是向前端发送请求还是向后端发送请求)
解决:如果 url 匹配不到任何静态资源,则应该返回同一个 index.html 页面(交给前端接管)
方法:npm run build
后生成一个 dist 文件夹,把这个文件夹给后端
tips:
- 带 “#” 的路由 一定是前后端分离开发的
- 不带的不一定
P83_全局路由拦截(全局路由守卫)
- 在路由跳转之前做拦截(判断是否登录、是否有权限等)
- 在路由表中
{
path: '/center',
component: Center,
// 把需要授权的路由加一个 meta 标签(路由元信息)
meta: {
isSypRequired: true
}
}
router.beforeEach((to, from, next) => {
if (to.meta.isSypRequired) { // 是需要授权的路由
if (localStorage.getItem('token')) { // 授权通过(方法:判断本地存储中是否有 token 字段)
next()
} else {
next({
path: '/login',
// 记录原先是向哪里跳转的
query: { myredirect: to.fullPath }
})
}
} else {
next()
}
})
- login 页面
<button @click="handleLogin">登录</button>
methods: {
handleLogin () {
setTimeout(() = {
localStorage.setItem('token', '后端返回的 token 字段')
// 跳回原先要去的页面
this.$router.push(this.$route.qyery.myredirect)
}, 0)
}
}
P84_局部路由拦截(路由独享拦截方式)
- 形式一:把全局的 “beforeEach” 那段代码改成 “beforeEnter” 写在路由内部
{
path: '/center',
component: Center,
beforeEnter: (to, from, next) => { …… }
}
- 形式二:直接写在组件里面
在 center 组件内
export default {
// 称为 “路由的生命周期(钩子函数)”
beforeRouteEnter (to, from, next) {
……
}
}
P85_路由懒加载
tips:
build 后生成的 dist 文件夹下的 js 文件夹内
- app.js 自己开发的所有代码的压缩
- chunk_vendors.js 非自己开发的第三方文件的压缩
- map 是源代码目录映射文件
问题:首屏加载过慢
原因:单页面开发时,用户打开首页时要加载整个 app.js
解决:路由懒加载(把不同路由对应的组件分割成不同的代码块,当相关路由被访问时再加载这段代码对应的 js 逻辑)
实现方法:
{
path: '\films',
// 把直接导入改成懒加载
component: () => import('@/views/Films.vue')
}
底层原理:Vue 的异步组件和 Webpack 的代码分割功能
- 用于优化组件过多,页面加载慢的问题
P86-P87_rem
- 写完路由逻辑之后开始开发页面
- 在 index.html 中写
<script>
document.documentElement.style.fontSize =
document.documentElement.clientWidth/设计稿宽度 * 100 + 'px'
// 100 好计算
// 或者设置成 "16",使用工具 "px to rem" 辅助计算
</script>
- 在 APP 根组件内写
body {
font-size: 16px;
}
P88-P89_swiper 组件
tips:
在 eslintrc.js 的 rules 中添加 'no-new': 'off'
可关闭 “no-new” 的代码格式错误检查
cnpm i --save swiper
全局下载 “Swiper”import Swiper from 'swiper/bundle'
在 “FilmSwiper” 组件内导入全部的 “Swiper” 模块(不同版本引入方式不同)import 'swiper/swiper-bundle.css'
在 “FilmSwiper” 组件内导入全部的 “Swiper” 的样式(不同版本引入方式不同)- 在 “FilmSwiper” 组件内封装自己的 “FilmSwipe”
- 在 “FilmSwiperItem” 组件内封装自己的 “FilmSwipeItem”
- 在 Films 内使用自己封装的 “FilmSwipe“ 和 ”FilmSwipeItem“
tips:
- 新版 swiper 的每个模块需要单独引入
import Swiper from 'swiper'
只引入了核心模块,其它功能没有引入- 单独引入方法(英文官方文档)
// core version + navigation, pagination modules:
import Swiper, { Navigation, Pagination } from 'swiper';
// import Swiper and modules styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
- 整体引入方法(英文官方文档)
// import Swiper bundle with all modules installed
import Swiper from 'swiper/bundle';
// import styles bundle
import 'swiper/css/bundle';
P90_底部选项卡封装
- myconponents 下写单独的 “tabbar” 组件
- 在 App.vue 中使用
tips:
-
public 文件夹下的内容可以通过 “/” 找到
-
src 文件夹下的内容只能通过模块化的方式引入(import)
-
iconfont 文件放 public 下引入:在 index.html 中
<link rel="stylesheet" href="/iconfont/iconfont.css">
-
iconfont 文件放在 src/assets 下引入:在 Tabbar.vue 中
import '@/assets/iconfont/iconfont.css'
P91_二级声明导航封装
- mycomponents/films 下写单独的 “film-header” 组件
- 在 Film.vue 中使用
P92-P93_正在热映页面
tips:
请求后端数据的三种设置方法:
- 后端设置允许任何跨域请求:Access-Control-Allow-Origin: *
- 配置反向代理
- 无需反向代理但需传一个 “headers”
axios({
url: '……',
headers: {
'参数名': '示例值',
}
}).then(res => {
……
})
-
问题一:主演名存在一个 actors 数组中的每个元素的 name 属性上
解决:过滤器 + map 映射 + 拼接成字符串 + 溢出省略号 -
问题二:主演为空时后端没给 actors 数组
解决:if(data === undefined) -
问题三:最后一条被遮挡
原因:底部选项卡脱离了文档流
解决:给页面加一个距离底部的边距 -
问题四:没有评分要求不显示且占位
解决:设置一个 class,没有评分时占位隐藏
P94-P101_详情页面
film-header 吸顶(粘性定位写法)
tips:
vue 为封装好的组件标签添加 class,该 class 会渗透到组件内部最外层的标签上,react 做不到
取详情数据
tips:
加 key 的作用:防止缓存
axios 封装(1.0)
- 在 src 文件夹下新建文件夹 util(工具库)
- 在 util 下新建文件 http.js
封装方法1(就是把原先分散在各个组件内的请求拿到了一个专门的文件里写了)
- 在 http.js 内
import axios from 'axios'
function httpForList () {
return axios({
url: '……',
headers: {
……
}
})
}
function httpForDetail (params) {
return axios({
url: '……',
headers: {
……
}
})
}
export default {
httpForList,
httpForDetail
}
- 在组件内使用
import http from '@/util/http'
http.httpForList().then(res => { …… })
封装方法2(用 axios 封装一个实例,做了一些自定义配置)
- 在 http.js 内
import axios from 'axios'
const http = axios.create({
baseURL: '请求地址的公共部分',
timeout: 10000, // 10s 后超时
headers: {
// 公共的请求头
}
})
export default http
- 在组件内使用
import http from '@/util/http'
http({
url: '除去公共部分后的地址',
headers: {
// 除去公共请求头后的其余请求头
}
}).then(res => { …… })
渲染详情页面
tips:
-
数据取回来是个数组的话,初始化
datalist: []
-
数据取回来是个对象的话,初始化
data: null
-
问题一:数据是异步请求直接渲染会报错
解决:v-if 判断请求到了再创建 dom
tiips:
-
空对象.属性 会报错
-
空数组.属性 会 undefined
-
问题二:背景图片不固定
解决::style="{ backgroundImage: 'url(' + filmInfo.poster + ')' }"
-
问题三:时间戳改年月日写法
解决:cnpm i --save moment
+ 过滤器
Moment.js
tips:
动态绑定的 class 和 style 与静态绑定的是共存的
- 问题四:内容介绍部分折叠-显示
解决:动态绑定 class + 三目运算;不设置行高高度就会被内容自动撑开
详情轮播
-
封装一个轮播组件,可以一页显示 “preview” 张图片
-
swiper 组件库提供了方法
-
问题五:一个页面用两个 swiper 会有轮播冲突
原因:第一个 swiper 会被多次 new
解决:1. 为每个 swiper 传一个 “name” 属性
2. 在 Swiper 组件中接收动态的 “name” 属性作为动态 class
3. 初始化时 new Swiper( ‘.’ + this.name, { …… }) -
问题五:详情轮播不加 key 值也不会出现初始化过早的问题
原因:在最外层已经 v-if 判断过了
详情 Header
- 写 detail-header 组件
- 用指令封装 DOM 操作控制显示隐藏
<detail-header v-scroll="80">{{ filmInfo.name }}</detail-header>
Vue.directive('scroll', {
inserted (el, binding) {
el.style.display = 'none'
window.onscroll = () => {
if((document.documentElement.scrollTop || document.body.scrollTop) > binding.value) {
el.style.display = 'block'
}
else {
el.style.display = 'none'
}
}
},
unbind () {
window.onscroll = null
}
})
P102-P103_影院组件
- 取数据……渲染……
问题一:影院数据太多,滚动卡顿(超长列表的滚动行为)
解决:better-scroll
<div class="box" :style="{ height:height }">
列表……
</div>
import BetterScroll from 'better-scroll'
mounted () {
this.height = document.documentElement.clientHeight - document.querySelector('footer').offsetHeight + 'px' // footer 是底部选项卡最外层的标签名
http( …… ).then(res => {
……
this.$nextTick(() => { // dom 上完树之后
new BetterScroll('.box', {
// 加滚动条
scrollbar: {
// 划动时显示,不划时隐藏
fade: true
}
})
})
})
}
.box {
height: 不能在这设置; // rem 布局高度缩放会出现问题(愿因:rem 是按照 weight 计算比例的)
overflow: hidden;
// 加定位后 better-scroll 的滚动条才知道盒子高度(修正滚动条的位置)
position: relative;
}
原理:不让浏览器撑开滚动条
问题二:rem 布局高度缩放出现问题
解决:(方法二)
<tabbar ref="mytabbar"></tabbar>
this.$refs.mytabbar.$el.offsetHeight
vuex 管理……
P104_Element UI 组件库
- PC 端 :Element UI
- 移动端:Vant
P105-P106_Vant 组件库
tips:
- Vue.use() 相当于全局注册了
影院导航栏(组件库:navbar 导航栏)
- 看文档
问题:滚动条高度
解决:this.height = document.documentElement.clientHeight - this.$refs.navbar.$el.offsetHeight - document.querySelector('footer').offsetHeight + 'px'
全屏预览(组件库:imagePreview 图片预览)
- 看文档
tips:
- Vant 提供的组件在 APP.vue 内全局引入后就可以在各个组件内直接使用
- Vant 提供的函数在各个组件内调用时还是要引入
P107-P108 正在热映页面-数据懒加载(组件库:List 列表)
-
原理:判定 “滚动过的距离+视口高度 = 内容区的高度” 时触发 “onload” 事件
-
看文档
注意:
- 数据全部取完后要禁用,否则会一直触发 “onload” 取回空数组
- 新取到的数据要和老列表合并
- 取完数据 Loding 要设置回 false
问题:进到详情页面后,如果撑开滚动条再返回, onload 事件会立刻触发且被禁用
愿因:onload 事件在 ajax 异步取数据之前触发,此时数组长度为 0 且 total 初始值为 0
解决:if 判断 this.total !== 0
P109_loading 加载(组件库:Toast 轻提示)& axios 拦截器(Interceptors)
- Toast 轻提示:看文档
- axios 拦截器:看文档 axios
- axios 拦截器与 http 绑定封装 Toast 轻提示
import { Toast } from 'vant'
const http = axios.create({ …… })
// 在发送请求之前拦截 -- showLoading
http.interceptors.request.use(
function (config) {
// Do something before request is sent
Toast.loading({
message: '加载中……',
fprbidClick: true,
duration: 0
})
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// 在请求成功之后拦截 -- hideLoading
http.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
Toast,clear()
return response;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
Toast.clear()
return Promise.reject(error);
}
);
export default http
P110-P112_city 组件
- 取 city 数据
- 将取到的数据按以A、B、C…开头进行分组
- 利用转换后的数组,结合组件库进行页面渲染
数据转换
期望的结构:
cityList: [
{
type: 'A',
list: ['A1', 'A2', ……]
},
{
type: 'B',
list: ['B1', 'B2', ……]
},
……
]
实现方法:
renderCity (list) {
var cityList = []
// 创建一个 26 英文字母数组
var letterList = []
for (var i=65; i<+91; i++) {
letterList.push(String.fromCharCode(i))
}
// 分类出以每个字母开头的城市
letterList.forEach(letter => {
var newList = list.filter(item => item.pinyin.substring(0, 1).toUpperCase() === letter)
// 剔除掉空数组
newliat.length>0 && cityList.push({
type: letter,
list: newList
})
})
return cityList
}
组件渲染(组件库:IndexBar 索引栏)
- 两层循环嵌套(外层循环 index,内层循环以 “index” 开头的城市)
- 索引字符列表与数据对应(剔除不含城市的首字母)
计算属性写法
:index-list="computedList"
computed: {
computedList () {
return this.cityList.map(item => item.type)
}
}
- 索引字符弹出提示:“IndexBar” 组件 “select” 事件 + “Toast” 函数
P113_vuex 引入
引入
切换城市功能的实现:
(不好)
- 传统的多页面开发中的实现方案
- location.href = ‘#/cinemas?cityname=’ + item.name
- cookie、localStorage
- 单页面开发中的实现方案
- 中间人模式
- bus 事件总线
(好)
- vuex - 状态管理模式
P114_vuex 同步工作流
- 在 main.js 中引入 store 文件、实例化
- 在 store 文件夹下的 index.js 内进行公共状态管理
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex)
export default new Vuex.Store({
// 公共状态
state: {
cityId: '',
cityName: ''
},
// mutations 监控状态改变,管理状态更改
mutations: {
changeCityName (state, cityName) {
state.cityName = cityName
}
}
})
- 在组件(cinemas、nowplaying)中访问公共状态
// 双大括号内不写 “this”
{{ $store.state.cityId }}
- 在组件(city)中修改公共状态
// this.$store.state.cityName = item.name 不好
this.store.commit('changeCityName', item.name)
- mutations 的好处
- 可以统一管理状态的修改
- 可以使用 devtools 工具来记录状态何时被修改、被谁修改、时光回溯
P115-P118_vuex 异步工作流
tips:
- vuex 默认是管理在内存,一刷新页面公共状态就丢了
- vuex 持久化 - todo
问题:“影院” 页面和 “搜索” 页面取同样的数据取了两次
解决:vuex
vuex 在项目中的应用
- 非父子的通信
- 后端数据的缓存快照(减少重复数据请求,减轻服务器压力,提高用户体验)
- mutations 中只支持同步(因为是被监控的)
- actions 支持异步和同步
- 在 store 文件夹的 index.js 中
state: {
// 把影院数据缓存到这里
cinemaData: []
},
mutations: {
// 缓存
changeCinemaData (state, data) {
state.cinemaList = data
},
clearCinemaData (state) {
state.cinemaList = []
}
},
actions: {
getCinemaData (store, cityId) {
// 返回 getCinemaData 执行结果
return http({
url: `/gateway?cityId=${cityId}&ticketFlag=1&k=7617567`,
headers: {
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res => {
// 依旧需要把请求到的数据交给 mutations 去管理
store.commit('changeCinemaData', res.data.data.cinemas)
})
}
}
- 在组件(cinemas)中
mounted () {
if(this.$store.state.cinemaList.length === 0) { // 如果缓存里没有数据,取数据
this.$store.dispatch('getCinemaData', this.$store.state.cityId)
// 在 getCinemaData 执行完毕,异步数据取到之后
.then(res => {
初始化 BetterScroll
})
} else { // 缓存里有数据,直接用
初始化 BetterScroll
}
},
methods: {
handleLeft () {
this.$router.push('/cinemas/city')
// 每次选择城市时先清空 cinemaList 的缓存
this.$store.commit('clearCinemaData')
}
}
- 在组件(search)中
mounted () {
if(this.$store.state.cinemaList.length === 0) { // 如果缓存里没有数据,取数据
this.$store.dispatch('getCinemaData', this.$store.state.cityId)
// 在 getCinemaData 执行完毕,异步数据取到之后
.then(res => {
初始化 BetterScroll
})
}
}
search 组件(组件库:Search 搜索)
- 看文档 + 模糊查询
P119_vuex 新写法
- this.$store.state.cinemaList 写着太麻烦
- 新写法
import { mapState, mapAction, mapMutations } from 'vuex'
{{ cinemaList }}
computed: {
...mapState(['cinemaList', 'cityId'])
/*
// mapState 写法的返回值就是一个 key: value 的对象
// 相当于返回了大括号包裹的如下
cinemaList: function () {
return this.$store.state.cinemaList
}
*/
}
methods: {
...mapAction(['getCinemaData'])
...mapMutations(['clearCinemaDate'])
}
this.cinemaList
this.getCinemaData(this.cityId)
this.clearCinemaData()
P120_vuex 控制底部选项卡
- 在 store 文件夹下的 index.js 内
state: {
isTabbarShow: true
},
mutations: {
show (state) {
state.isTabbarShow = true
},
hide (state) {
state.isTabbarShow = false
}
}
- 在App.vue 下
<tabbar v-show="$store.state.isTabbarShow"></tabbar>
- 在需要隐藏 “tabbar” 的组件下
mounted () {
this.$store.commit.show()
},
destroyed () {
this.$store.commit.hide()
}
问题:要多次写 “show” 和 “hide”
解决:
方法一:在路由中拦截
方法二:混入(mixin)
- 在 util 文件夹下写一个公共对象 mixinObj.js
const obj = {
created () {
this.$store.commit.show()
},
destroyed () {
this.$store.commit.hide()
}
}
export default obj
- 在需要隐藏 “tabbar” 的组件下
import obj from '@/util/mixinObj'
export default {
mixin: [obj],
}
tips:
- “…” 同 key 值会覆盖
- “mixin” 同 key 值(key: value)会保留后深度混入
P121_vuex 持久化
问题:刷新页面切换的 “城市” 公共状态消失
解决:vuex-persistedstate
- 全局安装 vuex-persistedstate
- 在 store 文件夹下的 index.js 内
plugins: [createPersistedState({
// 只持久化需要缓存的
reducer: (state) => {
return {
cityId: state.cityId,
cityName: state.cityName
}
}
})],
Vue 3
01_Vue2 到 Vue3
P131_项目创建
脚手架配置
- 打开 powershell
vue create 文件夹名称
- Please pick a preset: Manually select features
- Check the features needed for your project: Choose Vue version、Babel、Router、Vuex、CSS Pre-processors
- Choose a version of Vue.js that you want to start the project with: 3.x
- Use history mode for router? YES
- Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
- Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
- Save this as a preset for future projects? NO
Vue2 到 Vue3
全局定义组件和指令方法改变(局部定义不变)
createApp(App)
.component ("组件名", {
……
})
.directive("指令名", {
……
})
P132_项目改造(只强调有改变的地方)
- main.js
import {{ createApp }} from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App)
.use(router)
.use(store)
.mount('#app')
- router/index.js
// createWebHistory -- history 模式
// createWebHashHistory -- hash 模式
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
const routes = [
……
// 重定向改动
{
path: '/',
redirect: '/films'
},
{
path:'/:any',
redirect: {
// 用命名路由不报警告
name: 'film'
}
}
// 两个一起相当于 vue2 的 "/*"
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
- store/index.js
import { createStore } from 'vuex'
export default createStore({
……
})
- App.vue
// router-link 新写法
<ul v-show="$store.state.isTabbarShow">
<router-link to="/films" custom v-slot="{navigate, isActive}">
<li @click="navigate" :class="isActive?'active':''">
<i class="iconfont icon-video"></i>
<span>电影</span>
</li>
</router-link>
<router-link to="/cinemas" custom v-slot="{navigate, isActive}">
<li @click="navigate" :class="isActive?'active':''">
<i class="iconfont icon-video"></i>
<span>影院</span>
</li>
</router-link>
<router-link to="/center" custom v-slot="{navigate, isActive}">
<li @click="navigate" :class="isActive?'active':''">
<i class="iconfont icon-video"></i>
<span>我的</span>
</li>
</router-link>
</ul>
- NowPlaying.vue
// vue3 没了过滤器,只能改成函数式写法
{{ actorFilter(data.actors) }}
methods: {
actorFilter (data) {
……
}
}
- 生命周期改变了
- Vant 组件库下载、使用方法稍有变动
tips:
- 上述写法其实是披着 vue3 外皮的 vue2
02_Vue3 函数式写法
类写法:
- Vue2
- this.
函数写法:
- Vue3
- composition Api(抄的 react 的 hooks)
P133_reactive
tips:
setup 生命周期相当于 Vue3 老写法和 Vue2 的 beforeCreate 和 created
{{ obj.myname }}
<button @click="handleClick()">change</button>
<script>
import { reactive } from 'vue'
export default {
setup () {
// 定义状态(函数式写法)
const obj = reactive({
myname = 'syp'
})
const handleClick = () => {
obj.myname = 'xiaoming'
}
return {
obj,
handleClick
}
}
}
</script>
tips:
- reactive 可以调用多次
- reactive 参数不能是字符串、数字,需要设置成对象才能驱动页面更新
- Vue3 的 template 内可以放兄弟节点
痛点:要 obj.
P134_ref
在原来的基础上增加了新功能
- 创建一个包装式对象,含有一个响应式属性 value
- 和 reactive 的区别:ref 没有包装属性 value,可以接收普通数据类型
老功能(拿节点)-新写法:
<input type="text" ref="mytextref">
import { ref } from 'vue'
const mytextref = ref()
// 使用时: mytextref.value 拿到的是 dom 节点
// mytextref.value.value 拿到的是文本框的内容
新功能(解决 reactive 的痛点):
{{ myname }} // 双大括号内省略 ".value",所以间接达成了目的
setup () {
const myname = ref("syp") // 参数可以是一个字符串这种普通数据类型
// 看似拦截一个字符串,其实是对 myname 的 value 属性进行拦截
// myname.value 中是 "syp"
const handleClick = () =>
// 修改时还是必须 .value
myname.value = "xiaoming"
}
return {
myname
}
}
新痛点:必须 .value
P135_toRefs
可一次性解决两个痛点
- html 中不需要 obj.
- js 中不需要 .value
{{ myname }} --- {{ myage }}
import { reactive, toRefs } from 'vue'
const obj = reactive({
myname: 'syp',
myage: '18'
})
const handleClick = () => {
obj.myname = 'xiaoming'
}
return {
// 在 return 时转换为 ref 对象然后展开
...toRefs(obj),
handleClick
}
P136_props & emit(父子通信)
- 父组件中
<navbar myname="home" myid="111" @event="change"></navbar>
<sidebar v-show="obj.isShow"></sidebar>
components: {
navbar,
sidebar
},
setup() {
const obj = reactive({
isShow: true
})
const change = () => {
obj.isShow = !obj.isShow
}
return {
……
}
}
- 子组件中
export default {
props: ["myname", "myid"]
setup (props, {emit}) { // 结解构赋值,emit === this.$emit
// props.myname
// props.myid
emit("event")
}
}
P137_生命周期(Vue3 的第二套写法)
- setup
- setup
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
发 Ajax:
import { onMounted } from 'vue'
setup () {
onMounted(() => {
setTimeout(() => {
obj.list = ['aaa', 'bbb', ……]
}, 2000)
})
}
P138_计算属性
import { computed } from 'vue'
const computedList = computed(() => {
……
return
})
return {
computedList
}
P139_watch
import { watch } from 'vue'
watch(() => obj.mytext, // 每次 "obj.mytext" 的值发生改变,watch 的回调函数就触发一次
() =>{
……
})
P140_自定义 hooks
tips:
- 类写法中的 this 指向的不明确性导致我们没办法把一些逻辑代码单独分出去一个文件写
- 函数式写法只需要把一个大函数拆成几个小函数就可以轻松地复用
- 新建一个 module 文件夹,内新建一个 app.js 写 “请求逻辑”
function getData1 () {
const obj1 = reactive({
list: []
})
onMounted(() => {
axios.get().then(res => {
obj1.list = res.data.list
})
})
}
function getData2 () {
const obj2 = reactive({
list: []
})
onMounted(() => {
axios.get().then(res => {
obj2.list = res.data.list
})
})
}
- 在 App.vue 中写 “视图逻辑”
import { getData1, getData2 } from './module/app'
setup () {
const obj1 = getData1()
const obj2 = getData2()
return {
obj1,
obj2
}
}
P141_router & store
在组件中用 router
import { useRouter, useRoute } from 'vue-router'
setup () {
const router = useRouter() // router === this.$router
const route = useRoute() // route === this.$route
// 使用:router.push()
// 使用:route.
}
在组件中用 store
import { useStore } from 'vuex'
setup () {
const store = useStore() // store === this.$store
// 使用:store.
}
依赖注入(vuex 的替代方案)
-
provide
-
inject
-
用于少量通信时代替 vuex
例:控制底部选项卡 Tabbar 显示隐藏
- 在 App.vue
<tabar v-show="isShow"></tabbar>
import { provide } from 'vue'
const isShow = ref(true)
provide("sypShow", isShow)
- 在需要隐藏 Tabbar 的组件中
import { inject } from 'vue'
const isShow = inject("sypShow")
// 使用:isShow.value