VUE学习
引入VUE
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
{{}}语法
-
不会自动侵入html
-
需要对html设置id,引入vue以及配合
{{}}
才会被vue解析,并按照JS执行<div id="box"> {{myname}} </div> <script> //将节点的数据全挂到全局变量ve中 var ve=new Vue({ el:"#box", data:{ myname:"zsl" } }) </script>
Vue拦截原理
-
Object.definedProperty方法可以在一个对象上直接定义一个新的属性、或修改一个对象已经存在的属性,最终返回这个对象
缺点:
- 无法监听es6的Set与Map变化
- 无法监听cllass类型的数据
- 无法监听新加或删除的属性
- 无法监听新加或删除的数组元素
但在VUE3中都能很好地解决了,但存在兼容问题,不兼容会自动降级为Object.definedProperty的数据监听系统
-
原理:每次更改的时候,通过我们的set拦截通知watcher进行触发更新
模板语法
事件绑定:@事件类型
- 改变属性:
:
//切换内容
<div is="box">
{{myname}}-{{myage}}
<button @click="handlerChange">change</button>
</div>
<script>
var ve=new Vue({
el:"#box",
data:{
myname:"zsl",
myage:19
},
methods:{
handlerChange(){
ve.myname="haha",
myage=60
}
}
})
<?script>
//切换属性
<div is="box">
{{myname}}-{{myage}}
<div:class="whichcolor">切换背景颜色</div>
<button @click="handlerChange">change</button>
</div>
<script>
var ve=new Vue({
el:"#box",
data:{
myname:"zsl",
myage:19,
whichcolor:'red'
},
methods:{
handlerChange(){
ve.myname="haha",
myage=60,whichcolor:'yellow'
}
}
})
<?script>
当内容或属性可以被vue解析时,里面的内容可以写成表达式
如改变颜色
<div is="box">
{{myname}}-{{myage}}
<div:class="whichcolor?'red':'yellow'">切换背景颜色</div>
<button @click="handlerChange">change</button>
</div>
<script>
var ve=new Vue({
el:"#box",
data:{
myname"zsl",
myage=19,
whichcolor=true
},
methods:{
handlerChange(){
ve.myname="haha",
myage=60,
whichcolor=!this.whichcolor
}
}
})
<?script>
指令
-
v-show
:动态隐藏与显示 -
v-if
动态创建与删除 -
v-for
动态遍历
<div v-show="isShow"> </div>
<div v-if="isCreated"> </div>
<div v-for="item in list">{{item}} </div>
-
v-bind:src
简写成:src
:就是改变属性的方法 -
v-once
:添加后,组件模块不再更新 -
v-on:click
简写成@click
-
v-model
:双向绑定表单 -
v-html
: 把一块html片段解析后直接渲染在页面中//没加粗,直接渲染mytext {{}} //解析后渲染加粗后的字体 <div v-html="mytext"></div> <script> new Vue({ el:'#box', data:{ mytext:'<b>加粗的</b>' } }) </script>
动态表格案例
<div id="box">
//model双向绑定一个输入框的value
<input type="text"/ v-model="mytext">{{mytext}}
<button @click="handlerAdd()">sdd</button>
<ul v-show="datalist.length">
<li v-for="(data,index) in datalist">
{{data}}
<button @click="handlerDel(index)">del</button>
</li>
</ul>
<div v-show="!datalist.length">无待办事项</div>
</div>
<script>
var ve=new Vue({
el:"#box",
data:{
datalist:["111"."222","333"],
mytext:"aaaa"
},
methods:{handlerAdd(){
this.datalist.push(this.mytext)
this.mytext=""
},
handlerDel(index){
this.datalist.splice(index,1)
}
})
</script>
案例:点击选项变色
<style>
*{margin:0;
padding:0;}
li{
flex:1;
height:50px;
line-height:50px;
text-align:center;
}
.active{
background:green;
}
</style>
<div id="box">
<ul>
<li v-for="da ta in datalist" :class="current ===index?'active':''" @click="handlerClick(index)">
{{data}}
</li>
</ul></div>
<script>
new Vue({
el:"#box",
data:{
datalist:["首页","列表","我的"],
current:0
},
methods:{
handlerClick(index){
this.current=index
}
}
})
</script>
Vue2中动态类名&属性
(以类名为例子)
<div id="box">
<div :class="classobj"></div>
</box>
<script>
var ve=new Vue({
el:"#box",
data:{
classobj:{
aa:true,
bb:true,
cc:false
}
}
})
</script>
有不能后期临时添加类名的缺陷,Vue3可以
解决方法:
-
Vue.set(对象,属性,true)
删除就false -
数组方法可以后期添加删除,push,slpce
<div id="box"> <div :class="classarr"></div> </box> <script> var ve=new Vue({ el:"#box", data:{ classarr:["aa","bb"] } }) </script>
Vue3中动态类名&属性
<div id="box">
{{myname}}
<input type="text" v-model="mytext">
<button @click="handlerAdd">add</button>
</box>
<script>
//函数式的写法
var obj={
data(){
return{
myname:"zsl"
},
classobj:{
aa:true,
bb:false
}
},
methods:{
handlerAdd(){
this.classobj.add=true
}
}
}
Vue.createApp(obj).mount("#box")
</script>
条件渲染
-
只有v-if可用
<div id="box"> <h2>所有订单</h2> <ul> <li v-for='item in datalist'> {{item.title}}--- <span v-if="item.state===0">未付款 </span> <span v-if="item.state===1">代发货 </span> <span v-if="item.state===2">已发货 </span> <span v-else>已签收 </span> </li> </ul> </div> <script> var ve=new Vue({ el:"#box", data:{ //isCreated:true, datalist:[ { title:"111", state:"0" }, { title:"111", state:"1" }, { title:"111", state:"2" }, { title:"111", state:"3" } ] } }) </script>
- template标签是Vue提供的包装元素,会真实创建在页面中
列表渲染
v-for
-
v-for:遍历数组内容(常用)
in 也可以用 of 代替
<body> <div id="div1"> <li v-for='(p,i) in persons' :key=i> {{p.name}}--{{p.age}} <!-- 张--18 李--19 刘--17 --> </li> </div> </body> <script type="text/javascript"> new Vue({ el:"#div1", data:{ persons:[ {id:'001',name:"张",age:18}, {id:'002',name:"李",+age:19}, {id:'002',name:"刘",age:17}, ] } }) </script>
-
v-for:遍历对象内容(不常用)
index相当于key
<body> <div id="div1"> <li v-for='(item,key) in persons' > {{item}}--{{key}} <!-- 张--name 18--age --> </li> </div> </body> <script type="text/javascript"> Vue.config.produnctionTps=false new Vue({ el:"#div1", data:{ persons:{ name:"张", age:18 } } })
-
遍历指定次数(不常用)
<body> <div id="div1"> <li v-for='(item,key) in 5' :key=i> {{item}}--{{key}} <!-- 1--0 2--1 3--2 4--3 5--4 --> </li> </div> </body>
-
key值
key值特殊的属性主要使用在Vue的虚拟DOM算法,在新旧node对比,数据修改的时候,他会根据key值去判断某一个值是否修改,如果修改则重新渲染该项,否则复用之前的元素。在v-for中key值一般为id,也就是唯一的值,但是一般不要使用index作为key(没有删除或添加Li的时候可以用)。
-
虚拟dom
Vue的虚拟DOM算法,在新旧node对比时辨识VNodes,如果不使用key,Vue会使用一种最大限度减少动态元素,并且尽可能的尝试就地复用,相同类型元素的算法,而使用key值时,它会就key的变化重新排列元素顺序,并且会移出key不存在的元素
数组更新检测
对原数组产生影响的方法会被拦截到
-
push(),pop(),shift(),unshift(),splice(),sort(),reverse()
以上方法原数组会发生变动
-
filter(),concat(),slice(),map()
以上方法原数组不会发生变动,只能用新数组替换旧数组方案
解决方案:
vm.items[indexOfitem]=newValue //(1) Vue.set(example.itemsindexOfitem,newValue) //(2) splice
模糊查询案例
方案1:
<div id="box">
<input type="text" @input="handlerInput" v-model="mytext" />
<ul>
<li v-for="data in newlist" :key="data">
{{data}}
</li>
</ul>
</div>
<script>
//用户输入后获得数据,过滤,输出
var vm = new Vue({
el: "#box",
data: {
datalist: ["aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg"],
newlist: ["aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg"],
mytext: ""
},
methods: {
//input事件可以实现对input输入框值的实时监控,只要input输入框值发生改变就会触发
handlerInput() {
//创建于原数组一样的新数据,永远用旧的过滤给新的
this.newlist = this.datalist.filter(item => item.includes(this.mytext))
//console.log(newlist)
}
}
})
</script>
方案2:函数表达式
<div id="box">
<input type="text" v-model="mytext" />
<ul>
<!-- 在属性中放表达式执行函数 -->
<!-- 利用函数返回值 -->
<li v-for="data in test()" :key="data">
{{data}}
</li>
</ul>
</div>
<script>
//用户输入后获得数据,过滤,输出
var vm = new Vue({
el: "#box",
data: {
datalist: ["aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg"],
mytext: ""
},
methods: {
test() {
return this.datalist.filter(item => item.includes(this.mytext))
}
}
})
</script>
事件处理器
是否加小括号的区别
- @click=“handleClick”
- 不加小括号的事件处理方式,vue会自动传递事件对象(事件源),可以在事件处理函数中获取事件对 象,但是无法传递自定义的参数
- @click=“handleClick()”
- 可以传递自定义的参数
- @click=“handleClick($event,参数,参数…)”
- 既能获取事件对象也能传参,$event是固定的,自定义的参数传递从第二个参数开始
事件修饰符
-
.stop:阻止事件冒泡(加给子元素事件)
在vue中子父级都绑定了同一事件,如原生一样会有事件冒泡行为
原生js使用e.stopPropagation()阻止事件冒泡
-
.self:阻止事件冒泡(加给父元素事件)
-
.prevent:阻止默认事件的发生
-
.once:设置事件只能触发一次
-
案例:模态框(悬浮输入框,点击狂周围的位置,输入框会小时,点击输入框内不会)
<style> #overlay { background: rgba(0, 0, 0, 0.6); width: 100%; margin: auto; position: fixed; top: 0; right: 0; left: 0; bottom: 0; } #center { background: #ffff; border-radius: 5px; padding-top: 15px; padding-left: 30px; padding-bottom: 15px; width: 200px; height: 160px; position: fixed; margin: auto; left: 0; right: 0; top: 0; bottom: 0; } </style> <div id="box"> <button @click="isShow=true">show</button> <div id="overlay" v-show="isShow" @click="isShow=false"> <div id="center" @click.stop> <div>用户名<input type="text"></div> <div>密码<input type="password"></div> <div><button>登录</button></div> </div> </div> </div> <script> //用户输入后获得数据,过滤,输出 var vm = new Vue({ el: "#box", data: { isShow: "false" } }) </script>
按键修饰符
与键盘中按键事件绑定在一起
- .enter
- .tab
- .delete (捕获“删除”和“退格”键)
- .esc
- .space
- .up
- .down
- .left
- .right
- .keycode值
表单控件绑定
<body>
<div id="box">
<!-- 记住用户名 -->
<div>
<div>用户名:<input v-model="mytext" /></div>
<input type="checkbox" v-model="isRemember" />记住用户名
<button @click="handlerLogin">登录</button>
</div>
<!-- 多选框记忆 -->
<div>
<h2>注册页面-兴趣爱好 </h2>
<input type="checkbox" v-model="checklist" value="vue">vue
<input type="checkbox" v-model="checklist" value="react">react
<input type="checkbox" v-model="checklist" value="weixin">小程序
</div>
<!-- 单选 -->
<div>
<h2>
性别选择
</h2>
<input type="radio" v-model="select" value="a">男
<input type="radio" v-model="select" value="b">女
</div>
</div>
<script>
var vm = new Vue({
el: "#box",
data: {
mytext: localStorage.getItem("username"),
isRemember: true,
checklist: [],
select: "a"
},
methods: {
handlerLogin() {
if (this.isRemember) {
localStorage.setItem("username", this.mytext)
}
}
}
})
</script>
购物车案例
表单修饰符
-
number:将表单的值转为数字类型
与text输入框配合使用,输入框内有数字与字母时,会自动取出字母
-
trim:将表单值左右两侧的空格去除
-
lazy:将表单的input事件改为change事件,当失去焦点或按下enter键时,绑定的数据会发生改变
<div><input type="number" placeholder="请输入内容" v-model.number="age"></div>
<div><input type="text" placeholder="请输入文本" v-model.trim="info"></div>
<div><input type="text" placeholder="请输入文本" v-model.lazy="msg"></div>
计算属性
为了计算某段计算逻辑得到结果,是写在computed对象中的属性
修改英文串首字母大小写为例:
三种方法
- 模板语法:虽然模板语法使用非常便利,但是它是被设计成用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护
- method方法:因为计算属性是基于缓存实现的,只在计算属性所依赖的数据发生改变时它们才会重新求值,否则访问 计算属性会立即返回之前的计算结果,而不必再次执行函数。 相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
- 计算属性:任何复杂逻辑其计算结果需要被缓存的都应当使用计算属性
<div id="box">
<!-- 模板语法: -->
{{my.name.substring(0,1).toUpperCase()+myname.substring(1)}}
<!-- method方法: -->
{{myMethodsName()}}
<!-- 计算属性: -->
{{myComputedName}}
</div>
<script>
var ve=new({
el:"#box",
data:{
myname:"zsl"
},
methods:{
myMethodsName(){
return this.my.name.substring(0,1).toUpperCase()+myname.substring(1)
}
},
computed:{
myComputedName(){
return this.my.name.substring(0,1).toUpperCase()+myname.substring(1)
}
}
})
</script>
监听watch
监听的属性发生变化时,会自动调用回调函数,执行相关操作
-
语法:
watch:{ mytext(){} }
区分data,methods,computed与watch
- data:状态;被拦截的数据
- 方法:事件绑定,逻辑计算,可以不用return,没有缓存
- 计算属性:重视结果,必须有return,有缓存,同步
- watch:重视过程,监听值的变化,不用返回值,异步同步都可以
fetch
Fetch 是一个现代的概念, 等同于XMLHttpRequest。它提供了许多与XMLHttpRequest相同的功能。
但兼容性不太好,可以通过引入https://github.com/camsong/fetch-ie8
解决
-
get
语法
methods:{ handlerFetch(){ fetch("...") .then(res=>res.json()) .then(res=>{ console.log(res) }) } }
-
post
key=value&key=value格式
fetch("...", {
method: "post",
headers: {//请求头,请求数据格式
"Content-Type": "application/x-www-form-urlencoded"
},
body: "name=key$age=2"//请求体
}).then(res => res.json())
.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
axios
Axios是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装
-
get
axios.get("...") .then(res => { console.log(res.data); //这里data是axios要求的,不是指状态的data })
-
post
axios会自动根据你传入的是字符串还是对象判断添加相应的请求头。
axios.post("...", "name=wer&age=1") .then(res => { console.log(res.data); })
过滤器
Vue3不支持过滤器
<div id="box">
<ul>
<li v-for="item in datalist" :key="item.id">
<img :src="item.img|imgFilter1">
{{item.nm}}
</li>
</ul>
</div>
<script>
Vue.filter("imgFilter1",(url)=>{
return url.replace('','')
})
var ve = new({
......
组件
定义
将html,css,js封装在一个容器内,优势是能复用编码,简化项目代码,提高运行效率
- 组件名字不能与系统标签重名
- 组件名js用驼峰,html用-
- data状态必须是函数的写法
- dom部分无高亮显示
- css只能写行内样式
- 每个组件是孤岛,无法访问外界的状态或方法
- template只能有一个跟组件
- new Vue 也是一个组件
- 组件注册要在 new Vue完成
语法
<div id="box">
根组件标签
</div>
<script>
Vue.component("标签名",{
template:"dom结构",
data(){return {}}
methods: {},
computed: {},
watch:{},
})
</script>
组件注册
-
全局组件
在script中
Vue.component
的是全局组件,可以插入到其他组件的dom结构,也可以单独自己成为根节点 -
局部组件
在component中
components:{"组件名":{},...}
的是全局组件,可以插入到其他组件的dom结构,也可以单独自己成为根节点
传值
利用属性
-
父传子-用属性
- 在根组件中
<navbar myname="父组件传的信息"></navvar>
,要是动态的就需要设置动态属性:
- 在component中
props:["接收的父组件属性"]
- 接下来在component的dom结构中的子组件就可以调用信息
例子:
<div id="box"> <navbar myname="电影" :myright="true" :myparent="parent"></navbar> <navbar myname="电影" :myright="false" :myparent="parent"></navbar> </div> <script> Vue.component("navbar",{ props:["myname","myright","myparent"], template:"<div> <span>{{myname}}--{{myparent}}</span> <button v-show="myright"></button> </div>" </script>
- 在根组件中
-
属性验证
不符合数据类型会报错
props;{ myname:String, myright:Boolean }
-
默认属性
例子理解
props:{ myname:{ type:String, defalut:"" }, myname:{ type:Boolean, defalut:"true" } }
-
子传父-用事件
- 子组件中定义数据
- 子组件中定义一个方法,用于触发传值事件
- 子组件中通过$emit(“自定义方法名”,需要传的值)传值
- 在父组件中定义自定义事件接收子组件传递的事件
- 在父组件data中定义一个值存储接受的值
抽屉例子:将isshow取反
<div id="box"> //在navbar中监听一个事件handleEvent,此事件是在父组件box中提前定义好的 <navbar @myevent="handleEvent"></navbar> <sidebar v-show="isShow"></isdebar> </div> <script> Vue.component("navbar",{ template:'<div style="background-color:red;"> //一点击按钮,触发监听事件 <button @click="handlerClick()">点击</button></div>', methods:{ handlerClick(){ this.$emit("myevent") } } }) Vue.component("sidebar",{ template:'<div style="background-color:yellow;"> <ul> <li>11</li> <li>22</li> <li>33</li> </div>' }) new Vue({ el:"#box", data:{ isShow:true }, methods:{ handleEvent(data) { this.isShow=!this.isShow } } }) </script>
-
兄弟相传
子传父后再父传另一个子
例子理解
<div id="box"> <button @click="handleAjax">ajax</button> <!-- 2.监听事件 --> <film-item v-for="item in datalist" :key="item.filmId" :mydata="item" @event="handleEvent"></film-item> <film-detail :film-data="filmData"></film-detail> </div> <script> Vue.component("filmItem", { //1.获取父组件的datalist里的item props: ["mydata"], template: '<div class="item"><img :src="mydata.paster"/>{{mydata.name}}<div><button @click="hanleClick">详情</button></div></div>', methods: { //3.自定义处理器,传数据 handleClick() { this.$emit("event", this.mydata.synopsis) } } }) Vue.component("filmDetail", { //5.接收父组件的数据 props: ["filmDate"], template: '<div class="filminfo">{{filmData}}</div>' }) new Vue({ el: "#box", data: { datalist: [], filmData: "" }, methods: { handleAjax() { fetch("...") .then(res => res.json()) .then(res => { console.log(res.data.films) this.datalist = res.data.films }) }, //4.接收数据 handleEvent(data) { //临时变量不能渲染到页面,将data临时变量赋值给父组件 this.filmData = data } } }) </script>
-
中央数据总线bus
各个组件内部要传输的数据或者要执行的命令信息,靠bus来通信。
靠订阅发布模式实现,创建Bus总线
var bus=new Vue
,有一方是订阅者bus.$on(""*.()=>{})
,一方是发布者bus.$emit("")
代码理解
<div id="box"> <button @click="handleAjax">ajax</button> <film-item v-for="item in datalist" :key="item.filmId" :mydata="item"></film-item> <film-detail></film-detail> </div> <script> //1.定义一个全局的总线 var bus = new Vue() Vue.component("filmItem", { props: ["mydata"], template: '<div class="item"><img :src="mydata.paster"/>{{mydata.name}}<div><button @click="hanleClick">详情</button></div></div>', methods: { handleClick() { //发布者传数据 bus.$emit("zsl", this.mydata.synopsis) } } }) Vue.component("filmDetail", { data() { return { info: "" } }, //生命周期,组件挂好就触发 mounted() { //订阅者接收数据 bus.$on("zsl", (data) => { this.info = data }) }, template: '<div class="filminfo">{{info}}}</div>' }) new Vue({ el: "#box", data: { datalist: [] }, methods: { handleAjax() { fetch("...") .then(res => res.json()) .then(res => { console.log(res.data.films) this.datalist = res.data.films }) } } }) </script>
②单独创建bus.js文件
-
组建通信ref
绑定节点可以得到节点对象,绑定组件得到组件对象
在子组件绑定
ref=""
,在父组件中$ref.
可以获取子组件<div id="box"> <input type="text" ref="mytext"/> <div ref="mydiv">111</div> <button @click="handleAdd"></button> <child ref="mychild"></child> </div> <script> vue.component("child",{ data(){ return{ myname:"1111" } }, template:'<div>{{name}}}}</div>' }) new Vue({ el:"#box", methods:{ handleAdd(){ console.log(this.$refs.mytext,this.$refs.mydiv,this.$refs.mychild) console.log(this.$refs.mychild.myname) this.$refs.mychild.myname=2222 } } }) </script>
组件注意
组件属性:父组件传给子组件的属性,只有父组件可以重新传,但不允许子组件随意修改
组件状态:组件内部状态,可以随便修改
Vue3的组件语法
//dom结构无改变
<div id="box">
<navbar myname="**">
<div>111</div>
</navbar>
</div>
<script>
//用对象封装好vue内的
var obj = {
data() {
return {
myname: "aa"
}
},
methods: {},
computed: {}
}
//语法1:
Vue.creatApp(obj).component("navbar", {
props: ["myname"],
template: '...'
}).mount("#box")
//语法2:好看些
var app = Vue.creatApp(obj)
app.component("navbar", {
props: ["myname"],
template: '...'
})
app.component("...", {})
//相当于el:"#box"
app.mount("#box")
</script>
动态组件
选项卡:方案1
每一个选项组件设置一个组建文件中,通过修改状态控制组件显示与隐藏
<div id="box">
<home v-show="which==='home'"></home>
<list v-show="which==='list'"></list>
<shopcar v-show="which==='shopcar'"></shopcar>
<footer>
<ul>
<li @click="which='home'">首页</li>
<li @click="which='list'">列表</li>
<li @click="which='shopcar'">购物车</li>
</ul>
</footer>
</div>
<script>
vue.component("home",{
template:'<div>home</div>'
})
vue.component("list",{
template:'<div>list</div>'
})
vue.component("shopcar",{
template:'<div>shopcar</div>'
})
var vm=new Vue({
el:"#box",
data:{
which:"home"
}
})
</script>
方案2:
Vue内置了一个动态组件<component></componnet>
,可以设置有一个is
属性,控制创建的是哪个组件
-
动态组件有一个特点:每次修改is属性时,之前创建的组件会删除再创建新的组件,所以每次转变选项卡,用户在前面选项卡留下的数据不会被保存下来
解决方案:
可以在动态组件套一个
<keep-alive></keep-alive>
<div id="box">
<component :is="which"></component>
<footer>
<ul>
<li @click="which='home'">首页</li>
<li @click="which='list'">列表</li>
<li @click="which='shopcar'">购物车</li>
</ul>
</footer>
</div>
<script>
vue.component("home",{
template:'<div>home</div>'
})
vue.component("list",{
template:'<div>list</div>'
})
vue.component("shopcar",{
template:'<div>shopcar</div>'
})
var vm=new Vue({
el:"#box",
data:{
which:"home"
}
})
插槽slot
扩展组件能力,提高组件的复用性
可以混合父组件内容子组件自己的模板
分类
-
单个插槽
-
语法:
HTML中:
<组件标签>想插入html结构<组件标签>
组件的template的dom结构中:
<slot></solt>
结果想插入的html结构会插入到slot标签中
-
-
具名插槽
-
语法:
-
HTML中:
<组件标签>想插入html结构加上slot属性<组件标签>
组件的template的dom结构中:
<slot name-""></solt>
结果想插入的html结构会插入到对应name的slot标签中
-
Vue3新语法
HTML改:
<组件标签>
<template>
想插入html结构,想要要创建具名插槽就加上v-slot=“...”指令或#...特殊符号
</template>
<组件标签>
slot改进抽屉
<div id="box">
<navbar></navbar>
<!-- box组件可以直接访问插槽的isShow -->
<button @click="isShow=!isShow"></button>
<slidebar v-show="isShow"></slidebar>
</div>
<script>
vue.component("navbar", {
template: '<div><slot></slot></div>'
})
Vue.component("slidebar",{
template:'<ul style="background-color:yellow;width:200px;height:500px;"><li>首页</li><li>钱包</li><li>设置</li></ul>'
})
new Vue({
el:"#box",
data:{
isShow:false
}
})
</script>
过渡效果
动画过渡
检测到transition标签内节点的改变而更换class值
appear属性:一开始页面加载完默认无动画,此属性可以加载有动画
mode=“out-in”:实现transition内两标签同行进出
-
语法一:
<style> //设置css动画 .zsl-enter-active{ animation: aa 1.5s; } .zsl-leave-active{ animation:aa 1.5s reverse } @keyframes aa{} </style> <div id="box"> //class自动切换控制动画进出 <button @click="isShow=!isShow">change</button> <transition enter-active-class="zsl-enter-active" leave-active-class="zsl-leave-active"> <div v-show="isShow">11111111111</div> </transition> </div> <script> new Vue({ el:"#box", data:{ isShow:false } }) </script>
-
语法二:
CSS:
.***-enter-active
&.***-leave-active
HTML:
<transition name="***">...</transition>
过渡中的diff算法
引:transition标签内的div互斥,两者只能出现一个,如果想加两个div,那么必须为两个div加上key值
vue更新原理,通过拦截到数据发生改变后,Vue中有watcher观察者通知所有与此节点相关的dom与组件重新渲染,渲染时,会渲染新的虚拟dom节点,其本质是对象,与老的虚拟dom节点对比。对比过程中Vue内置了diff算法,此算法会保证最优的算法对比,以最小的代价更新dom
组件模板更新时
-
同层级对比:适用于一层有1个标签
-
同key值对比
key一样会对比,key不一样会直接删除再创建新的
检测到两个div一样,会实施替换行为(删除节点);但两标签有key值或不同标签,检测到两者大为不同,实施动画过渡行为
-
同组件对比
列表过渡
<transition-group>
默认实例化为一个span标签呈现,通过tag
属性更换实例化为其他元素
语法:为列表外层添加<transition-group name="***"></transition-group>
包裹的列表必须提供唯一的key属性值(可以是状态的item)
可复用过度
将动画封在组件中实现可复用
封装多套动画时,可以通过html设置自定义属性传值给组件实现不同动画的切换。语法如下
<!-- 动画封装-->
<style>
.right-enter-active{}
.right-leave-active{}
@keyframes aa{}
.left-enter-active{}
.left-leave-active{}
@keyframes bb{}
...
</style>
<body>
<组件标签 mode="动画"></组件标签>
<script>
Vue.component("组件名",{
props:["mode"],
template:`<transition name="mode"></transition>`
})
</script>
生命周期
创建阶段
创建阶段的以下函数只会执行一次。
-
beforeCreate
在组件实例初始化完成之后立即调用。
此时无法访问到实例的状态,methods,computed等。
-
created
在组件实例处理完与状态相关的选项后调用。
此时可以访问到实例的状态,methods,computed等。
可以在这个函数中进行初始化状态和挂载一些属性到当前实例等操作;
因为拦截不到状态以外的,这里挂载的属性不会被拦截。就是页面上如果显示了这个属性。属性值改变了,页面上不会更新。
-
beforeMount
组件被挂载之前调用。调用这个函数的时候,已经完成了其响应状态的设置,但还没有创建DOM节点。还没有上树。能拿到解析之前的DOM节点。
-
mounted
组件被挂载之后(上树)调用。
能达到真实的dom节点
在mounted函数里可以进行以下操作:
1.依赖DOM创建之后才做初始化工作的插件(swiper)
2.订阅:bus.$on
3.加载页面时的Ajax请求
更新阶段
有任何状态发生改变就会调用
-
beforeUpdate
在组件的状态改变,或DOM结构发生改变时调用
状态立即更新,dom异步更新
-
updated
状态改变之后,更新完DOM树之后调用。在updated里访问节点才是最安全的
销毁阶段
用于组件删除前的清除定时器,事件解绑
-
beforeDestroy (Vue3:beforeUnmount)
组件销毁之前调用。调用这个函数是组件实例任然保留着之前的所有功能
-
destroyed (Vue3:unmounted)
组件销毁完之后调用。所有的子组件已经被销毁,所有的相关响应式作用都已经停止
swiper
具有滑动特效插件,能实现触屏焦点图、触屏Tab切换、触屏轮播图切换等常用效。
基于dom,在Vue中不管用
官网:https://www.swiper.com.cn/
使用:可以在基础演示中查看特效源代码复制粘贴
dom使用
静态使用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../allin/swiper-min-css.css">
<script src="../allin/swiper-min-js.js"></script>
<style>
.zsl {
height: 500px;
}
</style>
</head>
<body>
<header>导航</header>
<div class="swiper-container zsl">
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- 如果需要滚动条 -->
<div class="swiper-scrollbar"></div>
</div>
<footer>底部内容</footer>
<script>
new Swiper(".zsl", {
//垂直方向
// direction: "vertical",
// 如果需要分页器
//循环返回第一张
loop: true,
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
//自动播放
autoplay: {
delay: 2500,
disableOnInteraction: false,
},
})
</script>
</body>
</html>
动态使用:
swiper有初始化过早的问题,swiper更新完成后,AJAX才想后端请求数据,此时swiper中无动态节点,没办法实现特效。
解决方案:把swiper放在ajax请求后执行
Vue动态使用
将new swiper放在updated()里面,等dom加载更新完成后再调用
但有缺点:
- 无复用性
- 当页面其他状态更新,update重新运行,new swiper会执行多次,出现bug
swiper组件使用
-
同时解决动态问题与初始化问题
<div id="box"> 初始化前key值为0,初始化后值为3;新老dom对比时后删除老的,创建新的。创建新的时,数据已经完善,就不会出现wisper初始化过早的问题 <swiper key="datalist.length"> <swiper-item v-for="data in datalist" :key="data"> <img :src="data"> </swiper-item> </swiper> </div> <script> Vue.component("swiperItem", { template: `<div class="swiper-slide"><slot></slot></div>` }) Vue.component("swiper", { template: `<div class="swiper-container zsl"><div class="swiper-wrapper"><slot></slot></div><div class="swiper-pagination"></div></div>`, mounted() { new Swiper(".zsl", { pagination: { el: '.swiper-pagination' }, loop: true, autoplay: { delay: 2500, disableOnInteraction: false, } }) }, }) new Vue({ el: "#box", data: { datalist: [] }, mounted() { //模拟AJAX请求数据,并映射成div渲染页面 setTimeout(() => { this.datalist = ["...", "...", "..."] }, 2000) } })
-
在dom传属性控制特效
HTML: //传属性 <swiper :loop="***"></swiper> JS: Vue.component("swiper", { //接并判断,设置默认值 props: { loop: { type: Boolean, default: true } }, template: ``, mounted() { new Swiper("", { //赋值控制特效 loop: true }) }
指令
为了操作底层dom
通过指令可以知道dom什么时候创建完成,从而进行初始化工作
指令的生命周期
- bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个绑定时执行一次的初始化动作。
- inserted:第一次插入父节点时调用(父节点存在即可调用,不必存在于document中)。
- update:模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
- unbind:只调用一次,指令与元素解绑时调用。
Vue3指令的生命周期
- created:在绑定元素的 attribute 或事件监听器被应用之前调用;
- beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
- mounted:在绑定元素的父组件被挂载后调用;
- beforeUpdate:在更新包含组件的 VNode 之前调用;
- updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
- beforeUnmount:在卸载绑定元素的父组件之前调用;
- unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
语法
HTML中<div v-指令名><div>
JS中Vue.directive('指令名',{})
可以传值
HTML中<div v-指令名="'**'"><div>
JS中
Vue.directive('指令名',{
inserted(el,binding){
el.style.background=binding.value
}
})
指令应用
利用指令可以知道dom什么时候创建完成
轮播图swiper
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="../allin/swiper-min-css.css">
<script src="../allin/swiper-min-js.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="box">
<header>导航-{{myname}}</header>
<div class="swiper-container zsl">
<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"></div>
</div>
<div class="swiper-pagination"></div>
</div>
<footer>底部</footer>
</div>
<script>
Vue.directive("swiper", {
//每一个节点插入都执行一次
inserted(el, binding) {
let {
index,
length
} = binding.value
if (index == length - 1) {
new Swiper(".zsl", {
pagination: {
el: '.swiper-pagination',
},
loop: true,
autoplay: {
delay: 2500,
disableOnInteraction: false,
}
})
}
}
})
var vm = new Vue({
el: "#box",
data: {
datalist: [],
myname: "zsl"
},
mounted() {
setTimeout(() => {
this.datalist = ["https://gw.alicdn.com/bao/uploaded/O1CN010GiV8T1sGVnjOuwVK_!!6000000005739-0-yinhe.jpg_210x210q75.jpg_.webp", "https://gw.alicdn.com/bao/uploaded/O1CN01KViP3y1LIuXLEwfeU_!!6000000001277-0-yinhe.jpg_210x210q75.jpg_.webp", "https://gw.alicdn.com/bao/uploaded/i1/6000000002039/O1CN01tuS3H41QvuO8gMTMz_!!6000000002039-0-picassoopen.jpg_210x210q75.jpg_.webp"]
}, 2000)
},
/* updated(){
new Swiper(".zsl",{
pagination:{
el:'.swiper-pagination',
},
loop:true,
autoplay:{
delay:2500,
disableOnInteraction:false,
}
})
} */
})
</script>
</body>
</html>
nextTick
Vue在更新 DOM时是异步执行的。当数据发生变化,Vue将开启一个异步更新。
nexttick是Vue 内部的异步队列的调用方法,它比updated执行晚并只执行依次,但无任何复用性
语法
this.$nextTick(() => {
//swiper写在这
})
单文件组件
在很多 Vue 项目中,我们使用 Vue.component
来定义全局组件,紧接着用 new Vue({ el: '#container '})
在每个页面内指定一个容器元素。
这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:
- 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
- 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的
\
- 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器,如 Pug (formerly Jade) 和 Babel
文件扩展名为 .vue
的 单文件组件为以上所有问题提供了解决方法
文件内部结构与语法
-
template 组件的模板区域
template内部只能有唯一根节点
-
script 业务逻辑区域
<script> //ES6导出规范,bable会将es6转化为es5 export default{ data(){ return{} } } </script>
-
style 样式区域
可以用scss写法(让css支持简单的逻辑计算) <style lang="scss"> $width:300px; ul{ li{ backgground:yellow; width:$width; } } </style>
Vue-cli脚手架
组件注册
单文件组件中插入子单文件组件时,要在父文件中注册子组件
-
全局注册
import ** from "子组件路径" import Vue from "vue" Vue.component("子组件", **)
-
局部注册
import ** from "子组件路径" export default{ components:{ ** } }
通讯
通讯,插槽与生命周期可正常使用
但父组件的css样式会影响子组件的样式
解决方案:在父组件的css加上scoped
属性,为样式加上唯一的属性选择器,<style lang="scss" scoped></style>
axios获取数据(72集)
默认情况下,我们的项目中并没有对axios包的支持,所以我们需要下载安装。
-
在项目根目录中使用 npm安装包
npm install axios
-
main.js文件中,导入axios并把axios对象 挂载到vue属性中作为一个子对象,这样我们才能在组件中使用
import axios from 'axios' // 把对象挂载vue中 Vue.prototype.$axios = axios; // 把对象挂载vue中 new Vue({ el: '#app', components: { App }, template: '<App/>' })
-
在组建中利用axios获取数据
<script> export default{ methods:{ // 使用axios请求数据 get_w() { this.$axios.get("http://wthrcdn.etouch.cn/weather_mini?city=青岛").then((response) => { console.log(response.data); }) } }, </script>
反向代理
在前后端分离开发的场景,前端有个服务器(提供静态页面),后端有个服务器(提供接口);此时,前端需要连接后端的接口,就会出现跨域问题
反向代理原理:
让proxy通过vue产生一个代理服务器,然后通过这个代理服务器去请求数据,最后把请求的数据返回
注意:一般都需要重新运行
-
首先在项目根目录下创建 vue.config.js
module.exports = { lintOnSave:false, devServer:{ //设置反向代理 proxy:{ '/ajax':{ target:'...',//要跳转的位置 changeOrigin:true } //反向代理执行过程: axios访问地址中的 /zsl 转换为 target + /zsl: 创建虚拟服务器 去掉/api '/zsl': { target: '...', changeOrigin: true, pathRewrite: { '^/zsl': "" }, } } }
别名
@
是webpack配置的别名,代表指定到src这个文件夹的路径
使用例子:
import navbar from '@/mycomponent/Navbar'
单页面应用spa
单页应用:只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。单页应用跳转,就是使用路由跳转切换相关组件,仅刷新局部资源。
多页应用:多个独立的页面的应用,每个页面必须重复加载js、css等相关资源,多页应用跳转,需要整页资源刷新
单页应用(SPA) | 多页应用(MPA) | |
---|---|---|
结构 | 一个主页面 + 许多模块的组件 | 许多完整的页面 |
资源文件(css,js) | 组件公用的资源只需要加载一次 | 每个页面都要自己加载公用的资源 |
刷新方式 | 页面局部刷新 | 整页刷新 |
url模式 | a.com/#/page1 | a.com/page1.html |
a.com/#/page2 | a.com/page1.html | |
用户体验 | 页面切换快,体验佳;当初次加载文件过多时,需要做相关的调优。 | 页面切换慢,网速慢的时候,体验尤其不好 |
搜索引擎优化(SEO) | 对体验度和流畅度有较高要求的应用,不利于 SEO(可借助 SSR 优化 SEO),适用于经常切换页面的场景和数据传递较多,多表单的场景 | 适用于对 SEO 要求较高的应用 |
过渡动画 | Vue 提供了 transition 的封装组件,容易实现 | 很难实现 |
内容更新 | 相关组件的切换,即局部更新 | 整体 HTML 的切换,费钱(重复 HTTP 请求) |
路由模式 | 可以使用 hash ,也可以使用 history | 普通链接跳转 |
数据传递 | 因为单页面,使用全局变量就好(Vuex) | cookie 、localStorage 等缓存方案,URL 参数,调用接口保存等 |
相关成本 | 前期开发成本较高,后期维护较为容易 | 前期开发成本低,后期维护就比较麻烦,因为可能一个功能需要改很多地方 |
html文件请求 | 第一次进入页面的时候会请求一个html文件,刷新清除一下。切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了 | 每一次页面跳转的时候,后台服务器都会给返回一个新的html文档 |
首屏时间 | 首屏时间慢,首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来 | 首屏时间快,访问页面的时候,服务器返回一个 html,页面就会展示出来,这个过程只经历了一个HTTP请求 |
路由
路由route是一组映射关系,多个路由需要路由器router进行管理。
设定访问路径,并将路径和组件映射起来,用于局部刷新页面
配置
复习77集
重定向
就是通过各种方法将各种网络请求重新定个方向转到其他位置
解读重定向:当路径是‘/’时跳到/center
{
path: '/',
redirect: '/center'
}
声明式导航
难点:刷新页面后,页面如何得知当前页面的选中的选项
解决:
window.onhashchange可以监听路径改变, location.hash
可以获取路径
App.vue中:
<template>
<div>
<!-- 声明式导航:a链接跳转 -->
<ul>
<!-- vue-router 声明式导航 -->
<li>
<!-- 点击后会自己加上router-link-active的class,可以利用sctive-class属性修改其自动加上的class名 --> <router-link to="/#/movies">电影</router-link>
</li>
<li>
<router-link to="/#/cinemas">影院</router-link>
</li>
<li>
<router-link to="/#/center">我的</router-link>
</li>
</ul>
<!-- 路由容器 -->
<router-view></router-view>
</div>
</template>
<style scoped>
/* 点击后会自己加上router-link-active的class,借此设置样式 */
.router-link-active{
color: red;
}
</style>
嵌套路由
app.vue
<template>
<div id="box">
<ul>
<li> <router-link to="/films">电影</router-link>
</li>
<li>
<router-link to="/cinemas">影院</router-link>
</li>
<li>
<router-link to="/center">我的</router-link>
</li>
</ul>
<router-view></router-view>
</div>
</template>
<script>
</script>
<style scoped>
.router-link-active{
color: red;
}
</style>
index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Films from '@/views/Films';
import Nowplaying from '@/views/films/Nowplaying';
import Comingsoon from '@/views/films/Comingsoon';
import Cinemas from '@/views/Cinemas';
import Center from '@/views/Center';
Vue.use(VueRouter); //注册路由插件
//配置表
const routes = [{
path: '/films',
component: Films,
children: [{
path: '/films/nowplaying',
component: Nowplaying,
},
{
path: '/films/comingsoon',
component: Comingsoon,
}
]
},
{
path: '/cinemas',
component: Cinemas,
},
{
path: '/center',
component: Center,
},
/* 重定向:当路径是/时跳到 */
{
path: '/',
redirect: '/films'
}
];
const router = new VueRouter({
routes,
});
export default router;
films.vue
<template>
<div>
<div style="height:200px;background:yellow;">轮播图</div>
<div>二级声明式导航</div>
<router-view></router-view>
</div>
</template>
<script>
</script>
center.vue、cinemas.vue
<template>
<div>
center
</div>
</template>
编程式导航
不使用router-link组件标签 而是使用路哟对象上的push方法实现组件之间的跳转
例子:导航栏下的选项页面中的列表的详情页
Nowplaying .vue
<template>
<div>
nowplaying
<ul>
<!-- 声明式导航写法 -->
<!-- <li v-for="data in datalist" :key="data">
<router-link to="/detail">
{{data}}</router-link>
</li> -->
<!-- 编程式写法 -->
<li v-for="data in datalist" :key="data" @click="handleChangePage">{{data}}</li>
</ul>
</div>
</template>
<script>
export default {
data(){
return{
datalist:["111","222","333"]
}
},
methods:{
handleChangePage(){
this.$router.push('/detail')
}
}
}
</script>
index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Films from '@/views/Films';
import Nowplaying from '@/views/films/Nowplaying';
import Comingsoon from '@/views/films/Comingsoon';
import Cinemas from '@/views/Cinemas';
import Center from '@/views/Center';
import Detail from '@/views/Detail';
Vue.use(VueRouter); //注册路由插件
//配置表
const routes = [{
path: '/films',
component: Films,
children: [{
path: '/films/nowplaying',
component: Nowplaying,
},
{
path: '/films/comingsoon',
component: Comingsoon,
},
{
path: '/films',
redirect: '/films/Nowplaying'
}
]
},
{
path: '/detail',
component: Detail,
},
{
path: '/cinemas',
component: Cinemas,
},
{
path: '/center',
component: Center,
},
/* 重定向:当路径是/时跳到 */
{
path: '/',
redirect: '/films'
}
];
const router = new VueRouter({
routes,
});
export default router;
动态路由
过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Tzvntk6-1676170901165)(C:\Users\ABC\AppData\Roaming\Typora\typora-user-images\image-20230209183151787.png)]
后端给两个接口,一个列表接口,一个详情信息接口,
-
朝后端列表接口请求回来列表数据然后布局;
-
点完列表绑定事件,进行动态路由跳转到详情页面;(id是直接被放在了地址上)
-
在详情页面获取这个id;
这个id指的是每个商品唯一的id,然后通过这个id,浏览器才知道我们点击的是哪个商品,才往这个商品详情页面跳转,加载详情页面信息也是通过这个id向后端要的商品信息数据
-
利用获取到的id发请求给后端,后端有相应的接口返回真实的详情数据,
-
布局页面
用:
代表动态路径
nowplaying.vue
<template>
<div>
nowplaying
<ul>
<!-- 声明式导航写法 -->
<!-- <li v-for="data in datalist" :key="data">
<router-link to="/detail">
{{data}}</router-link>
</li> -->
<!-- 编程式写法 -->
<li v-for="data in datalist" :key="data" @click="handleChangePage(data)">{{data}}</li>
</ul>
</div>
</template>
<script>
export default {
data(){
return{
datalist:["111","222","333"]
}
},
methods:{
handleChangePage(id){
this.$router.push(`/detail/${id}`)
}
}
}
</script>
index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Films from '@/views/Films';
import Nowplaying from '@/views/films/Nowplaying';
import Comingsoon from '@/views/films/Comingsoon';
import Cinemas from '@/views/Cinemas';
import Center from '@/views/Center';
import Detail from '@/views/Detail';
Vue.use(VueRouter); //注册路由插件
//配置表
const routes = [{
path: '/films',
component: Films,
children: [{
path: '/films/nowplaying',
component: Nowplaying,
},
{
path: '/films/comingsoon',
component: Comingsoon,
},
{
path: '/films',
redirect: '/films/Nowplaying'
}
]
},
{
//动态二级路由
path: '/detail/:id',
component: Detail,
},
{
path: '/cinemas',
component: Cinemas,
},
{
path: '/center',
component: Center,
},
/* 重定向:当路径是/时跳到 */
{
path: '/',
redirect: '/films'
}
];
const router = new VueRouter({
routes,
});
export default router;
detail.vue
<template>
<div>
detail
</div>
</template>
<script>
export default {
created(){
//拿到当前匹配的路由id
//params传值页面数据消失,传递不显示参数,params相对于query来说较安全一点
console.log('created',this.$route.params.id)
//axios利用id发请求到详情接口,获取数据,布局页面
}
}
</script>
路由命名
通过命名路由来进行从列表页面往详情页面跳转
{
name: '**',
path: '...',
component:...
}
handleClick (id) {
// 1-通过路径跳转
// this.$router.push(`...`)
// 2-通过命名路由跳转
this.$router.push({
name: '**',
params: {
id: myid
}
})
}
路由模式
网页链接中是否包含#
,带#
一定是前端路由,但不带的不一定
网浏览器也不能分清前后端接口,他可能会向前端路由发后端请求
庆幸的是,开发服务器已经做好了处理,凡是它不接受的接口,会重新把index页面渲染到浏览器端,让前端路由接管
路由拦截
现象:点击一个功能时,会跳转到登录页面让我们先登录,登录之后再进行功能操作;但是如果我们登录了,它就不会跳转。
解释:在路由跳转之前,进行路由拦截检查是否登录
全局路由拦截
index.js
//to,from是从哪来,到哪去;next是函数,对某些不拦截的路由执行的函数
router.beforeEach((to, from, next) => {
if (需要拦截验证的路由) {
if (验证成功) {
next()
} else {
// 登录页面跳转
path:'/login'
}
} else {
next()
}
})
export default router;
-
fullpath:
-
fullPath是路由全地址,包括连接携带的参数,如:192.168.0.1/index?page=1,fullPath为/index?page=1
-
path是路径,不带参数,如:192.168.0.1/index?page=1,path为/index
-
-
token
当我们登录时填写账号和密码点击登录的时候,会向服务器发送一个认证,如果认证成功服务器会返回一个token值,这个token值是就相当于身份证一样,如果再次向服务器请求时,由于已经带上了身份证,这样就可以免去再次输入账号和密码的麻烦
-
query
页面跳转的时候,可以在地址栏看到请求参数,即可以看到从哪里跳转
router.beforeEach((to, from) => { console.log(to); if (to.meta.isZslRequired) { // 在本地存储中查看是否有token if (false) {} else { return { // 登录页面跳转 path: '/login', query: { redirect: to.fullPath }, //next('/login'); }; } } else {} }); export default router;
// 跳回上一步 this.$router.push(this.$route.query.redirect);
-
拦截方法:router.beforeEach(回调函数),每个路由跳之前都会执行这个函数
-
回调函数有三个参数:
- to:跳转后路由
- from:跳转前路由
- next:函数
-
不需要拦截的路由通行;
并不需要对所有路由进行拦截,不需要拦截的就next()
局部路由拦截
在需要拦截的路由内部写代码
{
path: '/order',
component: Order,
meta: {
isKerwinRequired: true
},
beforeEnter: (to, from, next) => {}
}
路由懒加载
不在路由文件index.js开始就用import导组件,而是当点到某个路径时,才会加载
语法
取消import导入法,换成回调函数导入法:
{
path: '/center',
component: () => import('../views/CenterView.vue'),
meta: {
isKerwinRequired: true
}
},
组件的rem
在index.html的头部中
<script> document.documentElement.style.fontSize = document.documentElement.clientWidth / 设备宽 * 100 + 'px' </script>
在app.vue中用通用选择器统一设置字体fontsize:16px
swiper单文件组件
轮播实现例子
根目录下新开终端npm i --save swiper
filmswiper.vue
<template>
<div class="swiper-cotainer zsl">
<div class="swiper-wrapper">
<slot></slot>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- 如果需要滚动条 -->
<div class="swiper-scrollbar"></div>
</div>
</template>
<script>
//如果不想到分页器其他的东西,就仅导入模块
import Swiper from 'swiper'
//css样式
import 'swiper/swiper.min.css'
//如果需要分液器或者其他的将上面的css替换为
/* import 'swiper/swiper-bundle.css' */
export default {
mounted(){
new Swiper('.zsl',{
pagination: {
el: '.swiper-pagination',
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// 如果需要滚动条
scrollbar: {
el: '.swiper-scrollbar',
},
loop:this.loop,
autoplay:{
delay:2500,
disableOnInteraction:false
}
})
}
}
</script>
films.vue
<template>
<div>
<film-swiper :key="datalist.length">
<film-swiper-item v-for="data in datalist" :key="data.id" class="filmswiperitem"><img :src="data.imgUrl"/></film-swiper-item>
</film-swiper>
<div>二级声明导航</div>
<router-view></router-view>
</div>
</template>
<script>
//导入模块
import filmSwiper from '@/mycomponents/films/FilmSwiper'
import filmSwiperItem from '@/mycomponents/films/FilmSwiperItem'
import axios from 'axios'
export default {
data(){
return{
datalist:[]
}
},
mounted(){
axios.get("/banner.json").then(res=>{
this.datalist=res.data.banner
})
},
components:{
filmSwiper,
filmSwiperItem
}
}
</script>
<style lang="scss" scoped>
.filmswiperitem{
img{
width: 100%;
}
}
</style>
图标引入方式
-
在index.html中用最原始的方式引入,图标文件放在public文件夹下的iconfont文件夹中
<link rel="stylesheet" href="/iconfont/iconfont.css">
-
在组件文件中用import引入,图标文件放在src文件下的assets文件夹下
import'../assets/iconfont/iconfont.css'
导航栏与二级选项卡
未解决的问题:正在热映不能默认选中
app.vue
<template>
<div >
<!-- 路由容器 -->
<router-view></router-view>
<tabbar></tabbar>
</div>
</template>
<script>
import tabbar from '@/mycomponents/Tabbar'
export default {
data(){
return{}
},
components:{
tabbar
}
}
</script>
<style lang="scss">
*{margin: 0;
padding: 0;
}
html,body{
width: 100%;
height: 100%;
overflow: hidden;
}
ul{
list-style: none;
}
</style>
film.vue
<template>
<div>
<film-swiper :key="datalist.length">
<film-swiper-item v-for="data in datalist" :key="data.id" class="filmswiperitem"><img :src="data.imgUrl"/></film-swiper-item>
</film-swiper>
<film-header></film-header>
<router-view></router-view>
</div>
</template>
<script>
//导入模块
import filmSwiper from '@/mycomponents/films/FilmSwiper'
import filmSwiperItem from '@/mycomponents/films/FilmSwiperItem'
import filmHeader from '@/mycomponents/films/FilmHeader'
import axios from 'axios'
export default {
data(){
return{
datalist:[]
}
},
mounted(){
axios.get("/banner.json").then(res=>{
this.datalist=res.data.banner
})
},
components:{
filmSwiper,
filmSwiperItem,
filmHeader
}
}
</script>
<style lang="scss" scoped>
.filmswiperitem{
img{
width: 100%;
}
}
</style>
filmheader.vue
<template>
<ul> <router-link to="/films/nowplaying" custom v-slot="{navigate,isActive}">
<li @click="navigate" >
<span :class="isActive?'zslactive':''" >正在热映</span>
</li>
</router-link>
<router-link to="/films/comingsoon" custom v-slot="{navigate,isActive}">
<li @click="navigate">
<span :class="isActive?'zslactive':''">即将上映</span>
</li>
</router-link>
</ul>
</template>
<script>
export default {
data(){
return{
}
}
}
</script>
<style lang="scss" scoped>
ul{
display: flex;
height: 50px;
line-height: 50px;
li{
flex: 1;
text-align: center;
}
}
.zslactive{
color: red;
border-bottom: 2px solid red;
}
</style>
取数据
获取网页请求数据时可能出现的问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LImAUsgd-1676170901167)(C:\Users\ABC\AppData\Roaming\Typora\typora-user-images\image-20230210173047863.png)]
解决方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vpo84eve-1676170901168)(C:\Users\ABC\AppData\Roaming\Typora\typora-user-images\image-20230210173113099.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHtxLPvD-1676170901169)(C:\Users\ABC\AppData\Roaming\Typora\typora-user-images\image-20230210173129634.png)]
解决跨域三种方案
- 反向代理
- jsonp
- 后端设置Access-Control-Allow-Origin
渲染页面
nowplaying.vue
<template>
<div>
<ul>
<!-- 声明式导航写法 -->
<!-- <li v-for="data in datalist" :key="data">
<router-link to="/detail">
{{data}}</router-link>
</li> -->
<!-- 编程式写法 -->
<li v-for="data in datalist" :key="data.filmId" @click="handleChangePage(data.filmId)">
<img :src="data.poster"/>
<div><div class="title">{{data.name}}</div></div>
<div class="content">
<!-- 无评分信息就隐藏 -->
<div :class="data.grade?'':'hidden'">观众评分:<span style="color:red; ">{{data.grade}}</span></div>
<!-- 将主角对象过滤为字符串 -->
<div class="actor">主角: {{data.actors | actorsFilter}}</div>
<div>{{data.nation}} | {{data.runtime}} 分钟
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import axios from "axios";
import Vue from 'vue'
Vue.filter('actorsFilter',(data)=>{
/* 防止后端数据无主演,造成过滤undefined报错 */
if(data===undefined){
return '暂无主演'
}
else{
return data.map(item=>item.name).join(' ')
}
})
export default {
data() {
return {
datalist: [],
};
},
mounted() {
axios({
url:
"https://m.maizuo.com/gateway?cityId=440700&pageNum=1&pageSize=10&type=1&k=3101199",
headers: {
"X-Client-Info":
'{"a":"3000","ch":"1002","v":"5.2.1","e":"16759303085854860763201537"}',
"X-Host": "mall.film-ticket.film.list",
},
}).then((res) => {
this.datalist = res.data.data.films;
});
},
methods: {
handleChangePage(id) {
this.$router.push(`/detail/${id}`);
},
},
};
</script>
<style lang=
"scss" scoped>
ul {
li {
overflow: hidden;
padding: 15px;
img {
width: 66px;
float: left;
margin-right: 10px;
}
.title {
font-size: 16px;
}
.content {
font-size: 12px;
color: grey;
.actor{
overflow: hidden
;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
}
}
}
.hidden{
/* 不占行的隐藏 */
visibility: hidden;
}
</style>
吸顶功能
-
方法一:定位
.sticky{ position: sticky; top: 0; background: white; }
封装axios
在src中新建文件夹util,可以将算法和一些小工具封装在此
函数份封装
http.js中封装的方法
//1-对于数据请求的封装
import axios from 'axios'
function httpForList() {
return axios({
url: "https://m.maizuo.com/gateway?cityId=440700&pageNum=1&pageSize=10&type=1&k=3101199",
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"16759303085854860763201537"}',
"X-Host": "mall.film-ticket.film.list",
}
})
}
function httpForDetail(params) {
return axios({
url: `https://m.maizuo.com/gateway?filmId=${params}&k=8333488`,
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"16759303085854860763201537"}',
"X-Host": "mall.film-ticket.film.info",
}
})
}
export default {
httpForList,
httpForDetail
}
引用
//在文件中引入
import http from '@/util/http'
//nowplaying.vue
http.httpForList().then((res) => {
this.datalist = res.data.data.films;
});
//detail.vue
http.httpForDetail(this.$route.params.id).then(res=>{
console.log(res.data)
})
axios封装
http.js中封装的方法
import axios from 'axios'
const http = axios.create({
baseURL: 'https://m.maizuo.com',
timeout: 10000,
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.2.1","e":"16759303085854860763201537"}'
}
})
export default http
引用
//在文件中引入
import http from '@/util/http'
//nowplaying.vue
http({
url: "/gateway?cityId=440700&pageNum=1&pageSize=10&type=1&k=3101199",
headers: {
"X-Host": "mall.film-ticket.film.list",
}
}).then((res) => {
this.datalist = res.data.data.films;
});
//detail.vue
http({
url: `/gateway?filmId=${(this.$route.params.id)}&k=8333488`,
headers: {
"X-Host": "mall.film-ticket.film.info",
}
}).then(res=>{
console.log(res.data.data.film)
this.filmInfo=res.data.data.film
})
详情页渲染
- 电影回去时间是时间戳,需要过滤转化
- 新开终端npm i --save moment
- import moment from ‘moment’
- 过滤器过滤
nowplaying.vue
<template>
<div>
<ul>
<!-- 声明式导航写法 -->
<!-- <li v-for="data in datalist" :key="data">
<router-link to="/detail">
{{data}}</router-link>
</li> -->
<!-- 编程式写法 -->
<li v-for="data in datalist" :key="data.filmId" @click="handleChangePage(data.filmId)">
<img :src="data.poster"/>
<div><div class="title">{{data.name}}</div></div>
<div class="content">
<!-- 无评分信息就隐藏 -->
<div :class="data.grade?'':'hidden'">观众评分:<span style="color:red; ">{{data.grade}}</span></div>
<!-- 将主角对象过滤为字符串 -->
<div class="actor">主角: {{data.actors | actorsFilter}}</div>
<div>{{data.nation}} | {{data.runtime}} 分钟
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import axios from "axios";
import Vue from 'vue'
import http from '@/util/http'
Vue.filter('actorsFilter',(data)=>{
/* 防止后端数据无主演,造成过滤undefined报错 */
if(data===undefined){
return '暂无主演'
}
else{
return data.map(item=>item.name).join(' ')
}
})
export default {
data() {
return {
datalist: [],
};
},
mounted() {
http({
url: "/gateway?cityId=440700&pageNum=1&pageSize=10&type=1&k=3101199",
headers: {
"X-Host": "mall.film-ticket.film.list",
}
}).then((res) => {
this.datalist = res.data.data.films;
});
},
methods: {
handleChangePage(id) {
this.$router.push(`/detail/${id}`);
},
},
};
</script>
<style lang=
"scss" scoped>
ul {
li {
overflow: hidden;
padding: 15px;
img {
width: 66px;
float: left;
margin-right: 10px;
}
.title {
font-size: 16px;
}
.content {
font-size: 12px;
color: grey;
.actor{
overflow: hidden
;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
}
}
}
.hidden{
/* 不占行的隐藏 */
visibility: hidden;
}
</style>
detail.vue
<template>
<div v-if="filmInfo">
<!-- 冒号后面可以加对象,动态绑定图片 -->
<div :style="{backgroundImage:'url('+filmInfo.poster+')'}" class="poster"></div>
<div class="content">
<div>{{filmInfo.name}}</div>
<div >
<div class="detail-text">{{filmInfo.category}}</div>
<!-- 获取的是时间戳,以s为单位,过滤器获取后要*1000 -->
<div class="detail-text">{{filmInfo.premiereAt | dateFilter}}上映</div>
<div class="detail-text">{{filmInfo.nation}} | {{filmInfo.runtime}}分钟</div>
<!-- 正常只显示两行,隐藏时设置高度使其隐藏溢出部分,不隐藏时不设置高度,防止使上箭头固定住位置;
点击后出现全部,设置动态class控制overflow:hidden;-->
<div class="detail-text" style="line-height:13px;" :class="isHidden?'':'hidden'">{{filmInfo.synopsis}} </div>
<div style="text-align:center"><i @click="isHidden=!isHidden">︿</i></div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import http from '@/util/http'
import moment from 'moment'
import Vue from 'vue'
//设置成中文
moment.locale('zh-cn')
Vue.filter('dateFilter',(date)=>{
//日期格式化
return moment(date*1000).format('YYYY-MM-DD')
})
export default {
data(){
return{
//一定要设置成Null
//因为渲染第一次时,还没请求,数据是null,所以会报错
//解决方案是设置v-if,当null时是假,就不走
filmInfo:null,
isHidden:false
}
},
created() {
// 拿到当前匹配的路由id
// params传值页面数据消失,传递不显示参数,params相对于query来说较安全一点
console.log('created', this.$route.params.id);
// axios利用id发请求到详情接口,获取数据,布局页面
http({
url: `/gateway?filmId=${(this.$route.params.id)}&k=8333488`,
headers: {
"X-Host": "mall.film-ticket.film.info",
}
}).then(res=>{
console.log(res.data.data.film)
this.filmInfo=res.data.data.film
})
},
}
</script>
<style lang="scss" scoped>
.poster{
width:100%;
height:210px;
background-position:center;
background-repeat:cover;
}
.content{
padding:15px;
.detail-text{
color:#797d82;
font-size:13px;
margin-top:4px
}
}
.hidden{
overflow: hidden;
height: 26px;
}
</style>
详情轮播
- 在同一个页面new多个swiper会出现bug,如perview后面会覆盖前面的状况,因为两者拥有同样的class
- 设置另外的属性Name
- 在组件中接收属性,并设置为动态类名
- 根据class设置不同的perview或其他
- 为什么后面的详情轮播不会出现初始化过早的问题
- 在detail.vue的一开始就设置了
v-if="filmInfo"
- 在detail.vue的一开始就设置了
detail.vue
<template>
<div v-if="filmInfo">
<!-- 冒号后面可以加对象,动态绑定图片 -->
<div :style="{backgroundImage:'url('+filmInfo.poster+')'}" class="poster"></div>
<div class="content">
<div>{{filmInfo.name}}</div>
<div >
<div class="detail-text">{{filmInfo.category}}</div>
<!-- 获取的是时间戳,以s为单位,过滤器获取后要*1000 -->
<div class="detail-text">{{filmInfo.premiereAt | dateFilter}}上映</div>
<div class="detail-text">{{filmInfo.nation}} | {{filmInfo.runtime}}分钟</div>
<!-- 正常只显示两行,隐藏时设置高度使其隐藏溢出部分,不隐藏时不设置高度,防止使上箭头固定住位置;
点击后出现全部,设置动态class控制overflow:hidden;-->
<div class="detail-text" style="line-height:13px;" :class="isHidden?'':'hidden'">{{filmInfo.synopsis}} </div>
<div style="text-align:center"><i @click="isHidden=!isHidden">︿</i></div>
</div>
<!-- 演员 -->
<div>
<div>演职人员</div>
<!-- 详情轮播样式差不多,用组件封装起来 -->
<detail-swiper :perview="3.5" name="actors">
<detail-swiper-item v-for="(data,index) in filmInfo.actors" :key="index">
<div :style="{backgroundImage:'url('+data.avatarAddress+')'}" class="avatar"></div>
<div style="text-align:center;font-size:12px;">{{data.name}}</div>
<div style="text-align:center;font-size:13px;">{{data.role}}</div>
</detail-swiper-item>
</detail-swiper>
</div>
<!-- 剧照 -->
<div>
<div>剧照</div>
<!-- 详情轮播样式差不多,用组件封装起来 -->
<detail-swiper :perview="2" name="photos">
<detail-swiper-item v-for="(data,index) in filmInfo.photos" :key="index">
<div :style="{backgroundImage:'url('+data+')'}" class="avatar"></div>
</detail-swiper-item>
</detail-swiper>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import http from '@/util/http'
import moment from 'moment'
import Vue from 'vue'
import detailSwiper from '@/mycomponents/detail/DetailSwiper'
import detailSwiperItem from '@/mycomponents/detail/DetailSwiperItem'
//设置成中文
moment.locale('zh-cn')
Vue.filter('dateFilter',(date)=>{
//日期格式化
return moment(date*1000).format('YYYY-MM-DD')
})
export default {
data(){
return{
//一定要设置成Null
//因为渲染第一次时,还没请求,数据是null,所以会报错
//解决方案是设置v-if,当null时是假,就不走
filmInfo:null,
isHidden:false
}
},
components:{
detailSwiper,
detailSwiperItem
},
created() {
// 拿到当前匹配的路由id
// params传值页面数据消失,传递不显示参数,params相对于query来说较安全一点
console.log('created', this.$route.params.id);
// axios利用id发请求到详情接口,获取数据,布局页面
http({
url: `/gateway?filmId=${(this.$route.params.id)}&k=8333488`,
headers: {
"X-Host": "mall.film-ticket.film.info",
}
}).then(res=>{
console.log(res.data.data.film)
this.filmInfo=res.data.data.film
})
},
}
</script>
<style lang="scss" scoped>
.poster{
width:100%;
height:210px;
background-position:center;
background-repeat:cover;
}
.content{
padding:15px;
.detail-text{
color:#797d82;
font-size:13px;
margin-top:4px
}
}
.hidden{
overflow: hidden;
height: 26px;
}
.avatar{
width:100%;
height:85px;
background-position: center;
background-size: cover;
}
</style>
detailswiper.vue
<template>
<div class="swiper-cotainer" :class="name">
<div class="swiper-wrapper">
<slot></slot>
</div>
</div>
</template>
<script>
import Swiper from 'swiper'
import 'swiper/swiper.min.css'
export default {
props:{
perview:{
type:Number,
default:1
},
name:{
type:String,
default:'zsl'
}
},
mounted(){
new Swiper('.'+this.name,{
slidesPerView:this.perview,
spaceBetween:30,
freeMode:true
})
}
}
</script>
detailswiperitem.vue
<template>
<div class="swiper-slide">
<slot></slot>
</div>
</template>
详情头部
在详情页监听一个scroll,滚动到一定距离就触发,删除组件时销毁
detail.vue
<template>
<div v-if="filmInfo" >
<!-- 指令传参 -->
<detail-header v-scroll="50">
{{filmInfo.name}}
</detail-header>
<!-- 冒号后面可以加对象,动态绑定图片 -->
<div :style="{backgroundImage:'url('+filmInfo.poster+')'}" class="poster"></div>
<div class="content">
<div>{{filmInfo.name}}</div>
<div >
<div class="detail-text">{{filmInfo.category}}</div>
<!-- 获取的是时间戳,以s为单位,过滤器获取后要*1000 -->
<div class="detail-text">{{filmInfo.premiereAt | dateFilter}}上映</div>
<div class="detail-text">{{filmInfo.nation}} | {{filmInfo.runtime}}分钟</div>
<!-- 正常只显示两行,隐藏时设置高度使其隐藏溢出部分,不隐藏时不设置高度,防止使上箭头固定住位置;
点击后出现全部,设置动态class控制overflow:hidden;-->
<div class="detail-text" style="line-height:15px;" :class="isHidden?'':'hidden'">{{filmInfo.synopsis}} </div>
<div style="text-align:center"><i @click="isHidden=!isHidden">︿</i></div>
</div>
<!-- 演员 -->
<div>
<div>演职人员</div>
<!-- 详情轮播样式差不多,用组件封装起来 -->
<detail-swiper :perview="3.5" name="actors">
<detail-swiper-item v-for="(data,index) in filmInfo.actors" :key="index">
<div :style="{backgroundImage:'url('+data.avatarAddress+')'}" class="avatar"></div>
<div style="text-align:center;font-size:12px;">{{data.name}}</div>
<div style="text-align:center;font-size:13px;">{{data.role}}</div>
</detail-swiper-item>
</detail-swiper>
</div>
<!-- 剧照 -->
<div>
<div>剧照</div>
<!-- 详情轮播样式差不多,用组件封装起来 -->
<detail-swiper :perview="2" name="photos">
<detail-swiper-item v-for="(data,index) in filmInfo.photos" :key="index">
<div :style="{backgroundImage:'url('+data+')'}" class="avatar"></div>
</detail-swiper-item>
</detail-swiper>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import http from '@/util/http'
import moment from 'moment'
import Vue from 'vue'
import detailSwiper from '@/mycomponents/detail/DetailSwiper'
import detailSwiperItem from '@/mycomponents/detail/DetailSwiperItem'
import detailHeader from '@/mycomponents/detail/DetailHeader'
//设置成中文
moment.locale('zh-cn')
Vue.filter('dateFilter',(date)=>{
//日期格式化
return moment(date*1000).format('YYYY-MM-DD')
})
//定义指令
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
}
})
export default {
data(){
return{
//一定要设置成Null
//因为渲染第一次时,还没请求,数据是null,所以会报错
//解决方案是设置v-if,当null时是假,就不走
filmInfo:null,
isHidden:false
}
},
components:{
detailSwiper,
detailSwiperItem,
detailHeader
},
created() {
// 拿到当前匹配的路由id
// params传值页面数据消失,传递不显示参数,params相对于query来说较安全一点
console.log('created', this.$route.params.id);
// axios利用id发请求到详情接口,获取数据,布局页面
http({
url: `/gateway?filmId=${(this.$route.params.id)}&k=8333488`,
headers: {
"X-Host": "mall.film-ticket.film.info",
}
}).then(res=>{
console.log(res.data.data.film)
this.filmInfo=res.data.data.film
})
},
}
</script>
<style lang="scss" scoped>
.poster{
width:100%;
height:210px;
background-position:center;
background-repeat:cover;
}
.content{
padding:15px;
.detail-text{
color:#797d82;
font-size:13px;
margin-top:4px
}
}
.hidden{
overflow: hidden;
height:30px;}
.avatar{
width:100%;
height:85px;
background-position: center;
background-size: cover;
}
</style>
detail.vue
<template>
<div class="header">
<!-- 返回按钮 -->
<i @click="handleBack"><</i>
<slot></slot>
</div>
</template>
<script>
export default {
methods:{
handleBack(){
//返回上一页面
this.$router.back()
}
}
}
</script>
<style lang="scss" scoped>
.header{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 44px;
line-height: 44px;
background-color: white;
text-align: center;
}
i{
font-size: 30px;
position: fixed;
left: 10px;
top: 10px;
line-height: 30px;
color: grey;
}
</style>
影院组件
betterscroll
影院中渲染的数据会撑开文档,文档实际高度很大,会造成滚动效果卡顿等。我们可以设置固定高度,隐藏溢出部分,再使用betterscroll,其作用是不让浏览器撑开轮动条
-
导入
import BetterScroll from 'better-scroll'
-
新开终端下载模块:
npm i --save better-scroll
-
mounted生命周期直接调用,不需要异步结束,动态计算高度
this.height = document.documentElement.clientHeight‐x+"px"
-
初始化better scroll
this.$nextTick(()=>{var myscroll = new BScroll('.kerwin', { pullDownRefresh: { threshold: 50, stop: 20 }
cinemas
<template>
<div>
<!-- 将列表数据报在一个div中,传给betterscroll -->
<div class="box" :style="{
height:height
} ">
<ul>
<li v-for="data in cinemaList" :key="data.cinemaId">
<div class="left">
<div class="cinema_name">
{{data.name}}
</div>
<div class="cinema_text">
{{data.address}}
</div>
</div>
<div class="right">
<div style="color:red;">
¥{{data.lowPrice/100}}起
</div>
</div>
</li>
</ul>
</div>
</div>
</template>
<script>
import http from '@/util/http'
import BetterScroll from 'better-scroll'
export default {
data(){
return{
cinemaList:[],
height:'0px'
}
},
mounted(){
//动态计算高度
this.height=document.documentElement.clientHeight-document.querySelector("footer").offsetHeight+'px'
http({
url:"/gateway?cityId=440700&ticketFlag=1&k=8515193",
headers:{
"X-Host": "mall.film-ticket.cinema.list"
}
}).then(res=>{
this.cinemaList=res.data.data.cinemas
//保证dom上树
//第二值是滚动条,默认无滚动条,需要传第二个值scrollbar
//滚动条会以浏览器窗口为标准,会滚出box的范围
//解决方案是加相对定位
this.$nextTick(()=>{
new BetterScroll('.box',{
scrollbar:{
fade:true
}
})
})
})
}
}
</script>
<style lang="scss" scoped>
ul{
li{
padding: 20px;
display: flex;
justify-content: space-between;
.left{
width: 210px;
}
.cinema_name{
font-size: 15px;
}
.cinema_text{
color: grey;
font-size: 12px;
margin-top: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.box{
overflow: hidden;
position: relative;
}
</style>
组件库
Element
适用于pc端
链接:组件 | Element
-
新开终端
npm i --save element-ui
-
引入
import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import Vue from 'vue' Vue.use(ElementUI)
-
看文档复制代码,可以赋属性调节样式
vant4
适用于移动端,vue3
链接:Vant 4 - 轻量、可定制的移动端组件库 (vant-ui.github.io)
-
新开终端
npm i vant@latest-v2
-
引入:
import Vue from 'vue' import Vant from 'vant' import 'vant/lib/index.css' //全局定义 Vue.use(Vant)
-
看文档使用
图片预览
<template>
<div v-if="filmInfo">
<!-- 剧照 -->
<div>
<div>剧照</div>
<!-- 详情轮播样式差不多,用组件封装起来 -->
<detail-swiper :perview="2" name="photos">
<detail-swiper-item
v-for="(data, index) in filmInfo.photos"
:key="index"
>
<div
:style="{ backgroundImage: 'url(' + data + ')' }"
class="avatar"
@click="handlePreview(index)"
></div>
</detail-swiper-item>
</detail-swiper>
</div>
</div>
</div>
</template>
<script>
//引入vant图片预览函数
import { ImagePreview } from "vant";
export default {
methods: {
handlePreview(index) {
//默认从第一张打开,可以用index控制打开哪张
// ImagePreview(this.filmInfo.photos)
ImagePreview({
images: this.filmInfo.photos,
startPosition: index,
//显示关闭按钮
closeable: true,
//关闭按钮位置
closeIconPosition: "top-left",
});
},
},
</script>
数据懒加载
-
数据还没回来已经开始检测,会认为已经到底就会触发onload函数,我们可以用
:immediate-check
取下页面开始时的检测 -
当我们点进详情页滚动至底部再退出详情页,页面不再懒加载数据列表,会直接显示到底了
这是因为,onload事件发生在mounted之前,ajax还没拿到数据,所以数据列表长度为0,等于一开始的total自定出发禁用懒加载的机制
所以我们if条件必须加上total不等于0
noeplaying.vue
<template>
<div>
<!-- 所有数据取完finished值weitrue会显示text -->
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
:immediate-check="false"
>
<!-- 声明式导航写法 -->
<!-- <li v-for="data in datalist" :key="data">
<router-link to="/detail">
{{data}}</router-link>
</li> -->
<!-- 编程式写法 -->
<van-cell
v-for="data in datalist"
:key="data.filmId"
@click="handleChangePage(data.filmId)"
>
<img :src="data.poster" />
<div>
<div class="title">{{ data.name }}</div>
</div>
<div class="content">
<!-- 无评分信息就隐藏 -->
<div :class="data.grade ? '' : 'hidden'">
观众评分:<span style="color: red">{{ data.grade }}</span>
</div>
<!-- 将主角对象过滤为字符串 -->
<div class="actor">主角: {{ data.actors | actorsFilter }}</div>
<div>{{ data.nation }} | {{ data.runtime }} 分钟</div>
</div>
</van-cell>
</van-list>
</div>
</template>
<script>
import axios from "axios";
import Vue from "vue";
import http from "@/util/http";
Vue.filter("actorsFilter", (data) => {
/* 防止后端数据无主演,造成过滤undefined报错 */
if (data === undefined) {
return "暂无主演";
} else {
return data.map((item) => item.name).join(" ");
}
});
export default {
data() {
return {
datalist: [],
loading: false,
finished: false,
current: 1,
total: 0,
};
},
mounted() {
http({
url: "/gateway?cityId=440700&pageNum=1&pageSize=10&type=1&k=3101199",
headers: {
"X-Host": "mall.film-ticket.film.list",
},
}).then((res) => {
this.datalist = res.data.data.films;
this.total = res.data.data.total;
});
},
methods: {
handleChangePage(id) {
this.$router.push(`/detail/${id}`);
},
onLoad() {
console.log("到底");
//总长度匹配,禁用懒加载
if (this.datalist.length === this.total && this.total != 0) {
this.finished = true;
return;
} else {
this.current++;
http({
url: `/gateway?cityId=440700&pageNum=${this.current}&pageSize=10&type=1&k=3101199`,
headers: {
"X-Host": "mall.film-ticket.film.list",
},
}).then((res) => {
this.datalist = [...this.datalist, ...res.data.data.films];
//将loading设置成false
this.loading = false;
});
}
},
},
};
</script>
<style lang=
"scss" scoped>
.van-list {
.van-cell {
overflow: hidden;
padding: 15px;
}
img {
width: 66px;
float: left;
margin-right: 10px;
}
.title {
font-size: 16px;
}
.content {
font-size: 12px;
color: grey;
.actor {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
}
}
.hidden {
/* 不占行的隐藏 */
visibility: hidden;
}
</style>
loading&axios拦截器
数据加载时的加载提示
https.js中封装
//引入toast函数
import { Toast } from "vant";
//发请求之前拦截
http.interceptors.request.use(function(config) {
//可以数据前做点什么
Toast.loading({
message: "加载中...",
forbidClick: true,
//设置固定时间消失
duration: 0
});
return config;
}, function(error) {
return Promise.reject(error);
});
// 获取成功后响应之前拦截
http.interceptors.response.use(function(response) {
//隐藏加载,获取完数据就消失,实现加载提示根据实际情况消失
Toast.clear()
return response;
}, function(error) {
//请求失败也需要
Toast.clear()
return Promise.reject(error);
});
export default http
城市数据组件
-
渲染数据
- 获取数据
- 数据分组
- 利用转化后的数组,结合组件库渲染页面
city.vue
<template> <div> <!-- index-list控制右边列表 --> <van-index-bar :index-list="computedList" @select="handleChange"> <div v-for="data in cityList" :key="data.type"> <van-index-anchor :index="data.type"/> <van-cell :title="item.name" v-for="item in data.list" :key="item.cityId" /> </div> </van-index-bar> </div> </template> <script> import http from "@/util/http"; import { computed } from "vue"; import {Toast} from 'vant' export default { data() { return { cityList: [], }; }, computed: { computedList() { return this.cityList.map((item) => item.type); }, }, mounted() { 2; http({ url: "/gateway?k=7173736", headers: { "X-Host": "mall.film-ticket.city.list", }, }).then((res) => { this.cityList=this.renderCity(res.data.data.cities); }); }, methods: { //城市首拼音改变是触发,data自动是所在首拼音 handleChange(data){ //弹出提示文字 Toast(data); }, renderCity(list) { var cityList = []; //定义字母数组 var letterList = []; //便利出26个字母 for (var i = 65; i < 91; i++) { letterList.push(String.fromCharCode(i)); } //过滤出个字母开头的分组 letterList.forEach((letter) => { var newList = list.filter( //后端给了pinyin (item) => item.pinyin.substring(0, 1).toUpperCase() === letter ); //前面为假后面压根不会执行,避免有些没有o等字母开头的城市分组 newList.length > 0 && cityList.push({ type: letter, list: newList, }); }); return cityList }, }, }; </script> <style lang="scss" > //在页面获取class,覆盖它的css,因为他是临时创建的,所以要删去scoped .van-toast--html, .van-toast--text{ min-width: 20px; } </style>
-
点击对应城市退出后左上角显示对应城市
-
传统的多页方案
location.href=‘#/cinemas?cityname=’+item.name
cookie,localStorage
-
单页面模式
中间人模式
bus事件总线 o n , on, on,emit
-
Vuex状态管理模式
-
状态管理模式vuex
管理保存公共资源(分散在各个角落的状态,统一管理)
注意:默认是管理在内存,刷新页面会丢失公共状态
应用:
非父子的通信;
后端数据的缓存快照,减少重复数据请求,减轻服务器压力
vue全家桶:vue-cli(脚手架)、vue-router(路由管理器)、vuex(状态管理模式)
架构Vuex
store下的index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
});
main.js中导入和实例化vuex
在其他文件中可以通过this.$store.state访问,HTML在双括号中不用加 this,在js中要加
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import store from './store'
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWT7mnmJ-1676170901170)(C:\Users\ABC\AppData\Roaming\Typora\typora-user-images\image-20230211225114455.png)]
- vuex在vue组件外面进行组件状态的管理;
- 管理的这些状态会被拦截;
- 组件都可以去访问这些状态;
- 因为状态会被拦截,所以当一个组件改了这个状态,其他的组件在使用这个状态时给被改变了,使用的是最新的状态;
状态修改Mutations(同步)
通过Mutations来修改状态,并配合devtools调试工具会记录这个状态何时被修改过
只支持同步
-
交给Mutations去修改
this.$store.commit('changeCityName', item.name)
-
在vuex文件Mutations中定义changeCityName方法
参数解释:
第一个:state是固定的,就是传过来state对象,里面存放着的都是状态,把state传过来这样就可以在这个方面里面直接 ‘’state.状态‘’ 来访问和修改state里的状态了
第二个参数:是 this.$store.commit(),传过来的item.name:要修改的参数
export default new Vuex.Store({ // state存放全组件都能访问到的公共状态 state: { cityId: '310100', cityName: '上海' }, mutations: { changeCityName (state, cityName) { // 修改状态 state.cityName = cityName } } })
Devtools插件
可以监控状态的改变
BUGFIX Vue.js devtools_5.0.0.4_chrome扩展插件下载安装 - 插件小屋 (chajianxw.com)
状态修改Actions(同异步)
- 在index.js设置action
- 在组建文件中分发action
- 再在action中发出AJAX请求数据回来并存储在vuex中
vuex新写法
-
在使用vuex的组件中先导入 mapState
import {mapState} from 'vuex'
-
写计算属性computed
computed: { ...mapState(['cinemaList','cityName']) },
-
使用状态时,就直接 ‘this.状态’ 就可以了,去掉 $store.state
$store.state.cinemaList => cinemaList $store.state.cityName => cityName
actions、Mutations等也能简写,
Vuex持久化
Vuex是基于内存,存在内存里面的,刷新网页之后就没有了,不会持久化储存
解决方案:每次往vuex中缓存新请求来的数据时候,这个小插件都会往Local Storage中存一份;当我们把vuex持久化需要的东西引入,再来切换一下地区,刷新一下页面,定位数据就会停留在当前的定位了
vuex-persistedstate
-
下载: vuex-persistedstate
npm install --save vuex-persistedstate
-
使用:在放vuex的js代码中引入
import createPersistedState from 'vuex-persistedstate' export default new Vuex.Store({ //... plugins: [createPersistedState()] }
//reducer里
plugins: [createPersistedState({
reducer: (state) => {
return {
cityId: state.cityId,
cityName: state.cityName
}
}
})]
GIT
一个分布式版本控制软件,可以使我们回退到任何修改过的版本
-
集中式
版本库集中存放在中央服务器,使用时需要先从中央服务器取得最新版本,最后需要将修改后的版本提交至中央服务器。缺点是需要联网,可能速度比较慢
-
分布式
没有中央服务器,每个人的电脑上有一个版本库。这样就不需要联网了,当需要多人修改同一个文件时,只需要将各自的修改推送给对方。其实,分布式版本控制系统通常也有一台充当中央服务器的电脑,只是方便交换
git本地仓库
- 开发人员在完成部分代码的编写之后,可以将这一部分的代码做一个提交。
- 这个提交完全就是一个新的版本提交。
- 当然这个提交动作是在开发者的电脑上进行操作的,而且也没有与外界进行联系,此次代码的提交即提交到了本地的版本库中。
- 这个本地的版本库就称为本地仓库。
- 本地仓库就是:对本地代码进行管理的仓库。
git远程仓库
- 区别于本地仓库,远程仓库不是在开发者的电脑上的。
- 远程仓库一般会有代码托管中心帮助进行维护。
- 代码托管中心可以分为两种
- 远程仓库可以理解为:在开发者可以访问的网络内的某个服务器上有一个包含所有版本的仓库。
本地与远程关系
- 开发者可以把本地的新版本推送到远程仓库上
- 开发者也可以把远程仓库上的新版本拉取到本地仓库上
- 由此可见,远程仓库为不同开发者之间的协作提供了一个渠道。
vue3
函数定义与状态
有两种写法:类写法(vue一样)、函数写法
- 定义状态
<script>
import { reactive } from '@vue/reactivity'
export default {
//vue3老写法/vue写法中beforeCreate,created生命周期 == setup
//自动执行,立即执行
setup(){
//定义状态
const obj=reactive({
myname:"yiyi",
myage:100
})
return {
myname,
myage
}
}
- 定义函数
setup(){
//定义函数
const handleClick=()=>{
}
return {
handleClick
}
}
-
访问状*
{{obj.myname}}
ref
点击按钮状态被改变
<template>
<div>
<!-- 这里其实是{{myname.value}},不过直接写myname,它也给你当成myname.value -->
{{myname}}
<button @click="handleClick()">change</button>
</div>
</template>
<script>
import { ref } from '@vue/reactivity'
export default {
setup(){
const myname = ref("yiyi")//拦截的是ref对象里的value属性
const handleClick=()=>{
console.log(myname) //=>ref对象
console.log(myname.value)//=>yiyi
myname.value = 'linlin'
}
return{
myname,
handleClick
}
}
}
</script>
ref与reactive
ref能直接拦截到字符串,拦截到字符串的原理其实是拦截的是ref对象里的value属性
reactive 就不能拦截到字符串,只能拦截到对象和数组
两者可利用toRefs(obj)进行转换
<template>
<div>
//使用ref方式
{{myname}}--{{myage}}
</div>
</template>
<script>
//引入toRefs
import { reactive,toRefs } from '@vue/reactivity'
export default {
setup(){
//使用reactive方式
const obj=reactive({
myname:"yiyi",
myage:100
})
return {
//ref和reactive进行转换
...toRefs(obj),
}
}
}
</script>
父传子属性props
父组件
<template>
<div>
通信
<--使用<navbar>组件-->
<navbar myname="home" myid="111"></navbar>
</div>
</template>
<script>
import navbar from './components/navbar.vue'
export default {
components:{
navbar
}
}
</script>
子组件
<template>
<div>
<button>left</button>
{{myname}}
<button>right</button>
</div>
</template>
<script>
export default {
props:["myname","myid"],
setup(data){
console.log(data)//=>结果返回一个包含属性的对象
console.log(data.myname,data.myid)
}
}
</script>
子组件接收父组件传过来的属性,vue3中一样还是用props接收
子传父事件emit
子组件绑事件
<navbar @event="change"></navbar>
父组件定义change函数
const change=()=>{
}
子组件控制函数执行
setup(data,{emit}){
emit('event')
}
生命周期
类方式
类方式的生命周期用法和vue一样;
函数方式
-
生命周期要写在setup中
-
要导入生命周期
-
生命周期参数是一个回调函数
<script> import {onMounted} from 'vue' export default { setup(){ ... onMounted(()=>{ console.log('onMounted') }) ... } } </script>
计算属性
-
导入计算属性:
import { computed } from '@vue/reactivity'
-
定义计算属性
const computedList = computed(()=>{ return obj.datalist.filter(item=>item.includes(obj.mytext)) })
-
使用计算函数
<ul> <li v-for="data in computedList" :key="data"> {{data}} </li> </ul>
函数用法
<template>
<div>
<input type="text" v-model="obj.mytext">
<ul>
<li v-for="data in filterlist()" :key="data">
{{data}}
</li>
</ul>
{{filterlist()}}
</div>
</template>
<script>
import { reactive } from '@vue/reactivity'
export default {
setup(){
const obj=reactive({
mytext:'',
datalist:["aaa","abb","abc","bcc","add","bcd"]
})
//函数用法
const filterlist=()=>{
return obj.datalist.filter(item=>item.includes('obj.mytext'))
}
return{
obj,
filterlist
}
}
}
</script>
计算属性用法
<template>
<div>
<input type="text" v-model="obj.mytext">
<ul>
<li v-for="data in computedList" :key="data">
{{data}}
</li>
</ul>
{{computedList}}
</div>
</template>
<script>
//导入计算属性
import { reactive,computed } from '@vue/reactivity'
export default {
setup(){
const obj=reactive({
mytext:'',
datalist:["aaa","abb","abc","bcc","add","bcd"]
})
//计算属性
const computedList = computed(()=>{
return obj.datalist.filter(item=>item.includes(obj.mytext))
})
return{
obj,
computedList
}
}
}
</script>
watch
watch的用法:
<template>
<div>
<input type="text" v-model="obj.mytext">
<ul>
<li v-for="data in obj.datalist" :key="data">
{{data}}
</li>
</ul>
</div>
</template>
<script>
import { reactive,watch } from '@vue/reactivity'
export default {
setup(){
const obj=reactive({
mytext:'',
datalist:["aaa","abb","abc","bcc","add","bcd"],
objlist:["aaa","abb","abc","bcc","add","bcd"]
})
//watch监听mytext值的变化
watch(()=>obj.mytext,()=>{
obj.datalist=obj.objlist.filter(item=>item.includes(obj.mytext))
})
return{
obj
}
}
}
</script>
绑定input事件的用法:效果和watch一样,只是两种不同的方法
<template>
<div>
<input type="text" v-model="obj.mytext" @input="handleInput">
<ul>
<li v-for="data in obj.datalist" :key="data">
{{data}}
</li>
</ul>
</div>
</template>
<script>
import { reactive } from '@vue/reactivity'
export default {
setup(){
const obj=reactive({
mytext:'',
datalist:["aaa","abb","abc","bcc","add","bcd"],
objlist:["aaa","abb","abc","bcc","add","bcd"]
})
const handleInput=()=>{
obj.datalist=obj.objlist.filter(item=>item.includes(obj.mytext))
}
return{
obj,
handleInput
}
}
}
</script>
自定义hooks
在setup里写的定义函数和状态,封装在外部的js文件里,主文件只负责调用这些函数即可,虽然composition api比之前vue的写法好像更麻烦了,但是用上自定义hooks就可以实现函数编程的复用,更加的简洁高效
主文件
setup(){
const{list1}=getData1()
const{list2}=getData2()
return{
list1,
list2
}
}
js文件
import { reactive } from "vue";
function getData1(){
const obj = reactive({
myname:"yiyi",
myage:100
})
return{
obj
}
}
function getData2(){
const handleClick=()=>{
console.log("click")
}
return{
handleClick
}
}
export default {getData1,getData2}