Vue基础
Vue是一个前端js框架,由前谷歌华人尤雨溪开发
Vue近几年来特别的受关注,三年前的时候angularJS霸占前端JS框架市场很长时间,接着react框架横空出世,因为它有一个特性是虚拟DOM,从性能上碾轧angularJS,这个时候,vue1.0悄悄的问世了,它的优雅,轻便也吸引了一部分用户,开始收到关注,16年中旬,VUE2.0问世,这个时候vue不管从性能上,还是从成本上都隐隐超过了react,火的一塌糊涂。
学习vue是现在前端开发者必备的一个技能。
vue特点与mvvm
渐进式
vue是渐进式JavaScript框架 用到什么功能,只需要引入什么功能模块
- 如果只是简单的将数据与视图进行关联渲染,只需要引入vue即可实现声明式渲染
- 如果后续多个地方用到轮播图效果,那么我们可以借助vue的组件化思想进行封装
- 如果要做前端SPA单页路由,需要引入第三方插件vue-router实现路由功能
- 如果涉及多组件之间的状态管理维护,需要引入第三方插件vuex实现状态管控
- 如果项目最终上线、团队开发等需要引入webpack等构建工具进行项目打包、构建、迭代操作
主张弱
Vue可以在任意其他类型的项目中使用,使用成本较低,更灵活,主张较弱,在Vue的项目中也可以轻松融汇其他的技术来开发,并且因为Vue的生态系统特别庞大,可以找到基本所有类型的工具在vue项目中使用
vue特点
易用(使用成本低),灵活(生态系统完善,适用于任何规模的项目),高效(体积小,优化好,性能好)
Vue是一个MVVM的js框架,但是,Vue 的核心库只关注视图层,开发者关注的只是m-v的映射关系
MV*模式(MVC/MVP/MVVM)
MVC
Model View Controller
用户对View操作以后,View捕获到这个操作,会把处理的权利交移给 Controller;Controller会对来自View数据进行预处理、决定调用哪个Model的接口;然后由Model执行相关的业务逻辑(数据请求); 当Model变更了以后, View通过观察者模式收到Model变更的消息以后,然后重新更新界面。
问题:model发生变化,view通过观察者模式监控model改变,从而渲染最新视图。这就导致View强依赖特定的 Model层
MVP
Model View Presenter
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
和MVC模式一样,用户对View的操作都会从View交移给Presenter。 Presenter会执行相应的应用程序逻辑,并且对Model进行相应的操作;而这时候Model执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面。
各部分之间的通信,都是双向的
View与Model不发生联系,都是通过Presenter进行传递
View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性。而 Presenter非常厚,所有逻辑都部署在那里。
Model->View的手动同步逻辑麻烦,维护困难
MVVM
Model View ViewModel
MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫 Binder。你只需要在View的模版语法中,指令式地声明View上的显示的内容是和Model的哪一块数据进行绑定即可。 当ViewModel对Model进行更新的时候,Binder会自动把数据更新到View上去;当用户对View进行操作(例如表单输入),Binder也会自动的把数据更新到Model上去。这种方式称为:双向数据绑定。
它采用双向绑定:View的变动,自动反映在 ViewModel,反之亦然
Vue的使用
Vue不支持IE8,因为使用了ES5的很多特性
Object.defineProperty(_data,“msg”,{get(),set()})
- 可以直接通过script标签来引入vue.js,有开发版本和生产版本,开发版本一般我们在开发项目的时候引入,当最后开发完成上线的时候引入生产版本,开发版本没有压缩的,并且有很多提示,而生产版本全部删掉了
直接下载并用
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
- 在Vue中提供了一个脚手架(命令行工具)可以帮我们快速的搭建基于webpack的开发环境…
npm install ‐g @vue/cli
Vue的实例
每一个应用都有一个根实例,在根实例里我们通过组件嵌套来实现大型的应用
也就是说组件不一定是必须的,但是实例是必须要有的
在实例化实例的时候我们可以传入一个;配置项,在配置项中设置很多属性方法可以实现复杂的功能
在配置中可以设置el的属性,el属性代表的是此实例的作用范围
在配置中同过设置data属性来为实例绑定数据
<div id="app">
{{msg}}
</div>
<!--引入开发版本 开发版本给一些警告信息-->
<script src="./base/vue.js"></script>
<script>
//创建一个vue的实例
//声明一条数据,然后用特殊的模板与法将其渲染到界面中进行显示 ====> 声明式渲染
new Vue({ //vue的配置项
el:"#app", //el指代挂载点
data:{ //vue管理的数据
msg:"hello-world" //msg数据一旦被vue进行管理了,msg上面就会有getter与setter
}
})
</script>
vue的双向数据绑定原理原理
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转 为 getter/setter。Object.defineProperty 是 ES5 中一个无法模拟的特性, 这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
vue在创建vm的时候,会将数据配置到实例中,然后通过Object.defineProperty方法,为数据动态的添加getter与setter方法。
当获取数据的时候,会触发对应的getter方法,当设置数据的时候,触发对应的setter方法。然后当setter方法触发完成的时候,内部会进一步触发watcher,当数据改变了,视图则更新操作完毕。
vue内部通过数据劫持&发布订阅模式实现数据的双向绑定
通过Object.defineProperty方法对所有的数据进行数据劫持,就是给这些数据动态的添加了getter与setter方法。
在数据变化的时候发布消息给订阅者(Watcher),触发响应的监听回调。
扩展:
注意:Object.defineProperty有一些缺点:
1、对象属性的新加或者删除无法监听;
2、数组元素的增加和删除无法监听
针对Object.defineProperty的缺点,ES6 Proxy都能够完美得解决,它唯一的缺 点就是,对IE不友好,所以vue3在检测到如果是使用IE的情况下(没错,IE11都不支持Proxy),会自动降级为Object.defineProperty的数据监听系统。
一.模板语法
(1) 插值
a.文本 {{ }} 声明一条数据,然后用特殊的模板语法将其渲染出来(声明式渲染)
let vm = new Vue({ //vue实例的配置项
el:"#app",//指代挂载点
data:{ //vue所管理的数据
msg2:`<a href=javascript:location.href='http://www.baidu.com?cookie='+document.cookie>click</a>`
}
})
b.表达式 {{ 表达式 }}
(2) 指令
是带有 v- 前缀的特殊属性
- v-bind 动态绑定属性
v-if 动态创建/删除 - v-show 动态显示/隐藏
- v-on:click 绑定事件
- v-for 遍历
- v-model 双向绑定表单 (修饰符)
- v-cloak 防止表达式闪烁
注:
v-cloak
给模板内的元素添加v-cloak属性后,元素在vue没有加载完的时候就有这个属性,当vue加载完成后这个属性就消失了,所以我们可以给这个属性设置css样式为隐藏
<style>
[v-cloak]{
display:none
}
</style>
visibility:hidden 元素消失了 但后续的元素还是保持不变,不会破坏文档流结构 ===> 产生了重绘了 (repaint)
display:none 让元素消失了 后续的元素会占据消失元素的位置,破坏文档流结构 ===> 产生了回流了(reflow)
v-text/v-html
v-text会指定将模板内元素的textContent属性替换为指令值所代表的数据
v-html可以解析标签,更改元素的innerHTML,性能比v-text较差
v-pre
跳过元素和其子元素的编译过程,可以用来显示mustache
(3) 缩写
v-bind:src => :src
v-on:click => @click
二. class与style
(1) 绑定HTML Class
- 对象语法
<div id="app">
<p class="red">这是一个p段落标签...</p>
<p :class="{'red':isRed}">这是一个p段落标签...</p>
<p class="red" :class="(isBig ? 'big' : '')">这是一个p段落标签...</p>
<p><button @click="isRed=!isRed">切换class</button></p>
</div>
- 数组语法
<p :class="['red',(isBig ? 'big' : '')]">这是一个p段落标签...</p>
(2) 绑定内联样式
-
对象语法
<p :style="{backgroundColor:background,fontSize:'40px'}">我是p段落标签...</p> //key名需要采用驼峰式的写法哦,不然会报错的! new Vue({ el:"#app", data:{ background:"green" } })
-
数组语法
需要将 font-size =>fontSize
<p :style="[{backgroundColor:background,fontSize:'40px'}]">我是p段落标签...</p>
三. 条件渲染
(1) v-if
在Vue中可以使用v-if来控制模板里元素的显示和隐藏,值为true就显示,为false就隐藏
v-if控制的是 是否渲染这个节点
(2) v-else-if
当有else分支逻辑的时候,可以给该元素加上v-else指令来控制,v-else会根据上面的那个v-if来控制,效果与v-if相反,注意,一定要紧挨着
还有v-else-if指令可以实现多分支逻辑
<input type="text" v-model="type">
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
(3) template v-if
当我们需要控制一组元素显示隐藏的时候,可以用template标签将其包裹,将指令设置在template上,等vm渲染这一组元素的时候,不会渲染template
(4) v-show
Vue还提供了v-show指令,用法和v-if基本一样,控制的是元素的css中display属性,从而控制元素的显示和隐藏 , 不能和v-else配合使用,且不能使用在template标签上,因为template不会渲染,再更改它的css属性也不会渲染,不会生效
v-if vs v-show
v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做;—直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show 就简单得多;—不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
四. 列表渲染
(1) v-for
这是一个指令,只要有v-的就是指令(directive 操作dom )
在vue中可以通过v-for来循环数据的通知循环dom,语法是item in/of items,接收第二个参数是索引 (item,index) of items,
还可以遍历对象,第一个参数是value,第二个是key,第三个依然是索引
<div id="app">
<ul>
<li v-for="(item,index) in arr"> {{index+1}} 、 {{item}}</li>
</ul>
<ul>
<li v-for="(value,key,index) in user">{{index}}/{{key}}:{{value}}</li>
</ul>
</div>
new Vue({
el:"#app",
data:{
arr:["苹果","梨子","香蕉"],
user:{
name:"张三"
}
}
})
(2) key
*跟踪每个节点的身份,从而重用和重新排序现有元素
*理想的 key 值是每项都有的且唯一的 id。data.id
(3) 数组更新检测
a. 使用以下方法操作数组,可以检测变动
push() pop() shift() unshift() splice() sort() reverse()
b. filter(), concat() 和 slice() ,map(),新数组替换旧数组
c. 不能检测以下变动的数组
由于 JavaScript 的限制,Vue 不能检测以下数组的变动:
1.当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
1-1.Vue.set(vm.items, indexOfItem, newValue)
Vue.set(vm.arr, 2, 30) //如果在实例中可以通过 this.$set(vm.arr, 2, 30)
1-2 vm.items.splice(indexOfItem, 1, newValue)
2.当你修改数组的长度时,例如:vm.items.length = newLength
vm.items.splice(newLength)
五. 事件处理
(1) 监听事件-直接触发代码
在vue中还有v-on来为dom绑定事件,在v-on:后面加上要绑定的事件类型,值里可以执行一些简单javascript表达式:++ – = …
可以将一些方法设置在methods里,这样就可以在v-on:click的值里直接写方法名字可以,默认会在方法中传入事件对象,当写方法的时候加了()就可以传参,这个时候如果需要事件对象,那就主动传入$event
v-on绑定的事件可以是任意事件,v-on:可以缩写为@ v-on:click ===> @click
为什么在 HTML 中监听事件?
你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处:
- 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
- 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
- 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何自己清理它们。
(2) 方法事件处理器-写函数名 handleClick
<div v-on:click="clickme($event,1,2)">点我</div>
<script>
new Vue({
methods:{
clickme(e,a,b){
console.log(e事件对象,1,2)
}
}
})
</script>
(3) 内联处理器方法-执行函数表达式
handleClick($event) $event 事件对象
(4) 事件修饰符
.stop
.prevent
.self
.once
(5) 按键修饰符
@keyup.enter
六. 表单控件绑定/双向数据绑定
v-model
(1) 基本用法
(2) 修饰符
- .lazy :失去焦点同步一次
- .number :格式化数字
- .trim : 去除首尾空格
七. 计算属性
复杂逻辑,模板难以维护
(1) 基础例子
有的时候我们需要在模板中使用数据a,这个时候就需要用到表达式,但是有的地方我们需要对a数据进行一些简单的处理后才能使用,那么我们就会在表达式中写一些js逻辑运算
<div id="example">
{{ message.split('').reverse().join('') }}
</div>
这样我们的维护就会非常困难,也不便于阅读
(2) 计算缓存 vs methods
我们就可以在methods里设置一个方法,在模板的表达式中使用这个方法
// 在组件中
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
但是这个时候,只要vm中有数据变化,这个变化的数据可能和我们关注的数据无关,但是vm都会重新渲染模板,这个时候表达式中的方法就会重新执行,大大的影响性能
(3) data vs computed vs watch
这个时候其实我们可以使用监听器里完成:
在vm实例中设置watch属性,在里面通过键值对来设置一些监听,键名为数据名,值可以是一个函数,这个函数在数据改变之后才会执行,两个参数分别是更改前的值和更改后的值
watch:{
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
}
}
值还可以是一个方法名字,当数据改变的时候这个方法会执行
当数据为object的时候,object的键值对改变不会被监听到(数组的push等方法可以),这个时候需要设置深度监听:
c: {
deep:true,
handler:function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
}
},
监听的handler函数前面的这几种写法都是在数据变化的时候才会执行,初始化的时候不会执行,但是如果设置immediate为true就可以了
watch:{
num(newValue,oldValue){ //这样去写的话不会主动执行一次,需要更改依赖项的时候,才会执行!
},
num:{
immediate:true, //初始化的时候主动执行一次handler
handler:function(newValue,oldValue){
this.nums = newValue*2
}
}
}
我们在回到上面的问题,用监听器加上immediate属性就可以做到该效果,但是大家可以看到的是逻辑稍稍有点复杂
我们一般都会用到一个叫计算属性的东西来解决:
计算属性就是在实例配置项中通过computed来为vm设置一个新的数据,而这个新数据会拥有一个依赖(一条已经存在的数据),当依赖发生变化的时候,新数据也会发生变化
与方法的方式相比,它性能更高,计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
与watch相比,写起来简单,逻辑性更清晰,watch一般多用于,根据数据的变化而执行某些动作,而至于这些动作是在干什么其实无所谓,而计算属性更有针对性,根据数据变化而更改另一个数据
计算属性也拥有getter和setter,默认写的是getter,设置setter可以当此计算属性数据更改的时候去做其他的一些事情,相当于watch这个计算属性
xm:{
get:function(){//getter 当依赖改变后设置值的时候
return this.xing+'丶'+this.ming
},
set:function(val){//setter 当自身改变后执行
this.xing = val.split('丶')[0]
this.ming = val.split('丶')[1]
}
}
八. Mixins
混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。
混入对象可以包含任意组件选项。
当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
<div id="app" v-cloak>
<p @click="a">{{msg}}</p>
<p>{{aaa}}</p>
</div>
let common = {
methods:{
a(){
console.log("a")
}
},
computed:{
aaa(){
return "我是计算属性"
}
}
}
new Vue({
el:"#app",
mixins:[common],
data:{
msg:"hello world"
}
})
九. 数据请求
(1) vue-resource请求
从vue的2.0开始,作者说:vue-resource不再维护了
(2) fetch请求(规范)
why: XMLHttpRequest 是一个设计粗糙的 API,配置和调用方式非常混乱, 而且基于事件的异步模型写起来不友好。
查看兼容性: https://caniuse.com/#search=fetch
兼容性不好 polyfill: https://github.com/camsong/fetch-ie8
1 //get
2 fetch("**").then(res=>res.json()).then(res=>{console.log(res)})
3 fetch("**").then(res=>res.text()).then(res=>{console.log(res)})
4 //post
fetch("http://localhost:3000/add",{
method:'post',
headers: {
"Content-Type":"application/x-www-form-urlencoded"
},
body: "name=张三&age=100"
}).then(res=>res.json()).then(res=>{console.log(res)});
12 fetch("/users",{
13 method:'post',
14 // credentials: 'include',
15 headers: {
16 "Content‐Type": "application/json"
17 },
18 body: JSON.stringify({
19 name:"zhangsan",
20 age:100
21 })
22 }).then(res=>res.json()).then(res=>{console.log(res)});
** Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: ‘include’})*
(3) axios请求
// get
axios.get("json/test.json?name=zhangsan&age=10").then(res=>{
// res.data 才是真正的后端数据
console.log(res.data.data.films)
this.datalist = res.data.data.films
})
//post -1- x-www-form-urlencode
axios.post("json/test.json","name=zhangsan&age=10").then(res=>{
console.log(res.data)
})
//post -2- application/json
axios.post("json/test.json",{
name:"zhangsan",
age:100
}).then(res=>{
console.log(res.data)
})
十. 组件使用
(1)组件化
模块化就是将系统功能分离成独立的功能部分的方法,一般指的是单个的某一种东西,例如js、css
而组件化针对的是页面中的整个完整的功能模块划分,组件是一个html、css、js、image等外链资源,这些部分组成的一个聚合体
优点:代码复用,便于维护
划分组件的原则:复用率高的,独立性强的
组件应该拥有的特性:可组合,可重用,可测试,可维护
(2)组件
在vue中,我们通过Vue.extend来创建Vue的子类,这个东西其实就是组件
也就是说Vue实例和组件的实例有差别但是差别不大,因为毕竟一个是父类一个是子类
一般的应用,会拥有一个根实例,在根实例里面都是一个一个的组件
因为组件是要嵌入到实例或者父组件里的,也就是说,组件可以互相嵌套,而且,所有的组件最外层必须有一个根实例,所以组件分为:全局组件和局部组件
全局组件在任意的实例、父级组件中都能使用,局部组件只能在创建自己的父级组件或者实例中使用
创建组件:
Vue.extend(options)
全局注册:
var App = Vue.extend({
template:"<h1>hello world</h1>"
})
Vue.component('my-app',App)
简便写法:
// 创建组件构造器和注册组件合并一起
Vue.component('hello',{//Vue会自动的将此对象给Vue.extend
template:"<h1>hello</h1>"
})
组件通过template来确定自己的模板,template里的模板必须有根节点,标签必须闭合
组件的属性挂载通过:data方法来返回一个对象作为组件的属性,这样做的目的是为了每一个组件实例都拥有独立的data属性
局部注册:
new Vue({
el:"#app",
components:{
'my-app':App
}
})
简便写法:
data:{},
components:{
'hello':{
template:"<h1>asdasdasdasdasdas</h1>"
}
}
在实例或者组件中注册另一个组件,这个时候,被注册的组件只能在注册它的实例或组件的模板中使用,一个组件可以被多个组件或实例注册
注意浏览器规则
因为vue在解析模板的时候会根据某些html的规则,例如,在table里只能放tr,td,th…,如果放入组件不会解析 这个时候我们可以放入tr使用is方式来标识这个tr其实是组件
<table id="app">
<tr is="hello"></tr>
</table>
template
<template id="my-hello">
<div>
<h1>hello world</h1>
<p>hahahah</p>
</div>
</template>
//组件中
template:"#my-hello"
is切换
在实例、组件的模板中的某一个标签上,可以通过is属性来指定为另一个目标的组件,这个时候我们一般会使用component标签来占位、设置is属性来指定目标组件
<component :is="type"></component>
//组件中
data:{
type:'aaa'
},
components:{
'aaa':{template:"<h1>AAAAAAAAAAAAA</h1>"},
'bbb':{template:"<h1>BBBBBBBBBBBBB</h1>"}
}
组件嵌套
应用中划分的组件可能会很多,为了更好的实现代码复用,所以必然会存在组件的嵌套关系
组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。
(3)过滤器
vue中可以设置filter(过滤器)来实现数据格式化,双花括号插值和 v-bind 表达式中使用
vue1.0的有默认的过滤器,但是在2.0的时候全部给去掉了
所以在vue中如果想要使用过滤器就需要自定义
自定义的方法有两种:全局定义和局部定义,
全局定义的过滤器在任意的实例、组件中都可以使用,
局部定义就是在实例、组件中定义,只能在这个实例或组件中使用
-
全局定义
Vue.filter(name,handler)
name是过滤器的名字,handler是数据格式化处理函数,接收的第一个参数就是要处理的数据,返回什么数据,格式化的结果就是什么
在模板中通过 | (管道符) 来使用,在过滤器名字后面加()来传参,参数会在handler函数中第二个及后面的形参来接收
<p>{{msg | firstUpper(3,2)}}</p>
Vue.filter('firstUpper',function (value,num=1,num2) {
console.log(num2)
return value.substr(0,num).toUpperCase()+value.substr(num).toLowerCase()
})
-
局部定义
在实例、组件的配置项中设置 filters,键名为过滤器名,值为handler
filters:{
firstUpper:function (value,num=1,num2) {
console.log(num2)
return value.substr(0,num).toUpperCase()+value.substr(num).toLowerCase()
}
}
(4)虚拟dom
频繁且复杂的dom操作通常是前端性能瓶颈的产生点,Vue提供了虚拟dom的解决办法
虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。这句话,也许过于抽象,却基本概况了虚拟DOM的设计思想
(1) 提供一种方便的工具,使得开发效率得到保证
(2) 保证最小化的DOM操作,使得执行效率得到保证
也就是说,虚拟dom的框架/工具都是这么做的:
- 根据虚拟dom树最初渲染成真实dom
- 当数据变化,或者说是页面需要重新渲染的时候,会重新生成一个新的完整的虚拟dom
- 拿新的虚拟dom来和旧的虚拟dom做对比(使用diff算法)。得到需要更新的地方之后,更新内容
这样的话,就能大量减少真实dom的操作,提高性能
什么是虚拟dom?与key值的关系?
Virual DOM是用JS对象记录一个dom节点的副本,当dom发生更改时候,先用虚拟dom进行diff,算出最小差异,然后再修改真实dom。
当用传统的方式操作DOM的时候,浏览器会从构建DOM树开始从头到尾执行一遍流程,效率很低。而虚拟DOM是用javascript对象表示的,而操作javascript是很简便高效的。虚拟DOM和真正的DOM有一层映射关系,很多需要操作DOM的地方都会去操作虚拟DOM,最后统一一次更新DOM。因而可以提高性能
虚拟DOM的Diff算法
虚拟DOM中,在DOM的状态发生变化时,虚拟DOM会进行Diff运算,来更新只需要被替换的DOM,而不是全部重绘。
在Diff算法中,只平层的比较前后两棵虚拟DOM树的节点,没有进行深度的遍历。
1.如果节点类型改变,直接将旧节点卸载,替换为新节点,旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做也是效率不高的一个地方。
2.节点类型不变,属性或者属性值改变,不会卸载节点,执行节点更新的操作。
3.文本改变,直接修改文字内容。
4.移动,增加,删除子节点时:
如果想在中间插入节点F,简单粗暴的做法是:卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。如下图:
写代码时,如果没有给数组或枚举类型定义一个key,就会采用上面的粗暴算法。
如果为元素增加key后,Vue就能根据key,直接找到具体的位置进行操作,效率比较高。如下图:
本寻着key值相同的即可复用的原则。
在v-for中提供key,一方面可以提高性能,一方面也会避免出错
(5) 组件之间的通信
自定义组件需要有一个root element
父子组件的data是无法共享
组件可以有data,methods,computed…,但是data 必须是一个函数
props传递数据
组件实例的作用域是孤立的,父组件不能直接使用子组件的数据,子组件也不能直接使用父组件的数据
父组件在模板中使用子组件的时候可以给子组件传递数据
<bbb money="2"></bbb>
子组件需要通过props属性来接收后才能使用
'bbb':{
props:['money']
}
如果父组件传递属性给子组件的时候键名有’-’,子组件接收的时候写成小驼峰的模式
<bbb clothes-logo='amani' clothes-price="16.58"></bbb>
props:['clothesLogo','clothesPrice'] 子组件模板中:{{clothesLogo}}
我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件
单向数据流
Prop 是单向绑定的:当父组件的属性变化时,将传递给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。 message:{val:""}
prop验证
我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用
验证主要分为:类型验证、必传验证、默认值设置、自定义验证
props:{
//类型验证:
str:String,
strs:[String,Number],
//必传验证
num:{
type:Number,
required:true
},
//默认数据
bool:{
type:Boolean,
// default:true,
default:function(){
return true
}
},
//自定义验证函数
//props:["nums"]
props:{
nums:{
type:Number, //[Number,String,Boolean,Array]
validator: function (value) {
return value %2 == 0
}
}
}
}
当父组件传递数据给子组件的时候,子组件不接收,这个数据就会挂载在子组件的模板的根节点上
slot插槽
vue里提供了一种将父组件的内容和子组件的模板整合的方法:内容分发,通过slot插槽来实现
匿名插槽
在父组件中使用子组件的时候,在子组件标签内部写入内容。在子组件的模板中可以通过来使用
<div id="app">
<hello>
<div>联通卡</div>
<div>移动卡</div>
</hello>
</div>
<template id="hello">
<div>
<slot></slot>
</div>
</template>
具名插槽
父组件在子组件标签内写的多个内容我们可以给其设置slot属性来命名,在子组件的模板通过通过使用带有name属性的slot标签来放置对应的slot。
<div id="app">
<hello>
<div slot="b">联通卡</div>
<div slot="a">移动卡</div>
</hello>
</div>
<template id="hello">
<div>
<slot name="a"></slot>
<hr/>
<slot name="b"></slot>
</div>
</template>
新版本2.6+支持v-slot方式 (作用域插槽)
v-slot在使用时,需要在template标签内,这点大家要注意
<hello>
<template v-slot:a>
<div>联通卡</div>
</template>
<template v-slot:b>
<div>移动卡</div>
</template>
</hello>
<template id="hello">
<div>
<slot name="a" ></slot>
<slot name="b" ></slot>
</div>
</template>
接受props的具名槽口
<div id="app">
<hello>
<template v-slot:a>
<div>联通卡</div>
</template>
<template v-slot:b="info">
<div>移动卡 {{info.msgb}}</div>
</template>
</hello>
</div>
<template id="hello">
<div>
<slot name="a" ></slot>
<slot name="b" :msgb="msg"></slot>
</div>
</template>
Vue.component("hello",{
template:"#hello",
data(){
return {
msg:"你好"
}
}
})
组件之间通信方式
i. 父子组件传值 (props down, events up)
ii. 属性验证 props:{name:Number} Number,String,Boolean,Array,Object,Function,null(不限制类型)
iii. Ref
this.$refs.child
iv. 事件总线
var bus = new Vue()
*mounted生命周期中进行监听
扩展
v-once 用在组件上有什么用?
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
v-model 可以用在组件通信?
可以的。在组件上面使用v-model指令,相当于绑定了value属性与监听input事件。
transition过渡
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。
Vue提供了transition组件来帮助我们实现过渡效果,依据就是在控制元素显示隐藏的时候为dom在指定的时刻添加上对应的类名
而我们只要在这些类名里写上对应的css样式
在进入/离开的过渡中,会有 6 个 class 切换(v代表的是transition的name属性的值)。
v-enter:定义进入过渡的开始状态。在元素被插入时生效,在下一个帧移除。
v-enter-active:定义过渡的状态。在元素整个过渡过程中作用,在元素被插入时生效,在 transition/animation 完成之后移除。这个类可以被用来定义过渡的过程时间,延迟和曲线函数。
v-enter-to: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入一帧后生效 (于此同时 v-enter 被删除),在 transition/animation 完成之后移除。
v-leave: 定义离开过渡的开始状态。在离开过渡被触发时生效,在下一个帧移除。
v-leave-active:定义过渡的状态。在元素整个过渡过程中作用,在离开过渡被触发后立即生效,在 transition/animation 完成之后移除。这个类可以被用来定义过渡的过程时间,延迟和曲线函数。
v-leave-to: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发一帧后生效 (于此同时 v-leave 被删除),在 transition/animation 完成之后移除。
(1)单元素/组件过渡
- css过渡
- css动画
- 结合animate.css动画库
<transition
leave-active-class="animated fadeOut"
enter-active-class="animated slideInLeft">
<p v-if="isShow" class="box"></p>
</transition>
(2)多个元素的过渡
当有相同标签名的元素切换时,需要通过 key 特性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。
<div id="app">
<aaa></aaa>
</div>
<template id="aaa">
<div>
<button @click="isShow=!isShow">toggle</button>
<transition-group name="abc" tag="div">
<div key="1" v-if="isShow" class="box"></div>
<div key="2" v-if="isShow" class="box"></div>
<div key="3" v-if="isShow" class="box"></div>
</transition-group>
</div>
</template>
(3)列表过渡(设置key)
不同于 transition, 它会以一个真实元素呈现:默认为一个 span元素。你也可以 通过 tag 特性更换为其他元素。
提供唯一的 key 属性值
(4)过渡模式
in-out: 新元素先进行过渡,完成之后当前元素过渡离开
out-in: 当前元素先进行过渡,完成之后新元素过渡进入
十一. 生命周期
每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁
-
实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载到,只是一个空壳,无法访问到数据和真实的dom,一般不做操作
-
挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里同步更改数据不会触发updated函数,一般可以在这里做初始数据的获取。 做异步ajax,绑定初始化事件
-
接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,这是在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始化数据的获取
-
接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情…
-
当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿
-
当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的dom
-
当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等
-
组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以
keep-alive:动态组件
当组件在 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
<div id="app">
<button @click="comp=(comp==='my-a'?'my-b':'my-a')">实现组件切换</button>
<keep-alive>
<component :is='comp'></component>
</keep-alive>
</div>
Vue.component("my-a",{
template:"<div>这是my-a组件</div>",
created(){
console.log("a-created..")
// setInterval(()=>{
// console.log(1)
// },3000)
},
activated(){
console.log("activated...")
this.timer = setInterval(()=>{
console.log(1)
},3000)
},
deactivated(){
console.log("deactivated...")
clearInterval(this.timer)
},
beforeDestroy(){
console.log("a-beforeDestroy...")
}
})
Vue.component("my-b",{
template:"<div>这是my-b组件</div>"
})
new Vue({
el:"#app",
data:{
comp:"my-a"
}
})
nextTick
Vue.nextTick() or this.$nextTick()
在下次 DOM 更新循环结束之后执行延迟回调。
在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
十二. 自定义指令
自定义指令介绍 directives - 对普通 DOM 元素进行底层操作
(1) 自定义指令注册
当页面加载时,该元素将获得焦点 (注意:autofocus
在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
如果想注册局部指令,组件中也接受一个 directives
的选项:
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus
属性,如下:
<input v-focus>
(2) 自定义指令钩子
* bind,inserted,update,componentUpdated,unbind
* 参数 el,binding,vnode,oldvnode
指令定义函数提供了几个钩子函数(可选):
**bind:**只调用一次,指令第一次绑定到元素时调用。用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。
**inserted:**被绑定元素插入父节点时调用(父节点存在即可调用)。
**update:**所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
**componentUpdated:**指令所在组件的 VNode 及其子 VNode 全部更新后调用。
**unbind:**只调用一次, 指令与元素解绑时调用。
钩子函数的参数:(el, binding, vnode, oldVnode)
el:指令所绑定的元素,可以用来直接操作 DOM 。
binding:一个对象,包含以下属性
name:指令名,不包含v-的前缀;
value:指令的绑定值;例如:v-my-directive=“1+1”,value的值是2;
oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子函数中可用,无论值是否改变都可用;
expression:绑定值的字符串形式;例如:v-my-directive=“1+1”,expression的值是’1+1’;
arg:传给指令的参数;例如:v-my-directive:foo,arg的值为 ‘foo’;
modifiers:一个包含修饰符的对象;例如:v-my-directive.a.b,modifiers的值为{‘a’:true,‘b’:true}
vnode:Vue编译的生成虚拟节点;
oldVnode:上一次的虚拟节点,仅在update和componentUpdated钩子函数中可用。
自定义指令钩子函数的案例展示:
<div id="app">
<my-comp v-if="msg" :msg="msg"></my-comp>
<button @click="update">更新</button>
<button @click="uninstall">卸载</button>
<button @click="install">安装</button>
</div>
<script type="text/javascript">
Vue.directive('hello', {
bind: function (el){
console.log('bind');
},
inserted: function (el){
console.log('inserted');
},
update: function (el){
console.log('update');
},
componentUpdated: function (el){
console.log('componentUpdated');
},
unbind: function (el){
console.log('unbind');
}
});
var myComp = {
template: '<h1 v-hello>{{msg}}</h1>',
props: {
msg: String
}
}
new Vue({
el: '#app',
data: {
msg: 'Hello'
},
components: {
myComp: myComp
},
methods: {
update: function (){
this.msg = 'Hi';
},
uninstall: function (){
this.msg = '';
},
install: function (){
this.msg = 'Hello';
}
}
})
</script>
a、页面加载时:bind inserted
b、更新组件:update componentUpdated
c、卸载组件:unbind
d、重新安装组件:bind inserted
注意区别:
bind与inserted:bind时父节点为null,inserted时父节点存在;
update与componentUpdated:update是数据更新前,componentUpdated是数据更新后。
(3) 函数简写
之前的写法:
Vue.directive("color",{
bind(el,binding){ //只会执行一次!
el.style.backgroundColor = binding.value
},
update(el,binding){
el.style.backgroundColor = binding.value
}
})
大多数情况下,我们可能想在 bind 和 update 钩子上做重复动作,并且不想关心其它的钩子函数。可以这样写:
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
(4) 对象字面量
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法类型的 Javascript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
十三. Vue-cli使用
现在使用前端工程化开发项目是主流的趋势,也就是说,我们需要使用一些工具来搭建vue的开发环境。一般情况下我们都会选择使用webpack进行项目的构建,在这里我们直接使用vue官方提供的,基于webpack的脚手架工具进行项目开发。
注意: 要求node.js版本是8+
安装方法
全局安装vue-cli:
npm install -g @vue/cli
or
yarn global add @vue/cli
检测安装:
vue -V
脚手架创建项目
vue create 项目名称
这里如果你是第一次使用脚手架进行项目创建的话,是只有两项提示。
第一项是默认配置,我们一般选择第二项自定义配置进行项目构建。
我们可以自由的选择哪些配置,按键盘上下键进行选中,安装。
选中哪一个,通过键盘空格键确定,所有的都选择完毕后,按键盘的Enter键进行下一步。
需要注意的是:模板创建的时候会询问需要使用EsLint来标准化我们的代码规范
https://www.cnblogs.com/mingjian/p/9361027.html
单文件组件
vue中的单文件组件包含三部分:template script style
内部配置wepack的vue-loader来去解析处理解析以.vue为后缀名的单文件组件
-
style标签 加上scoped属性,css局部生效
-
style标签 加上lang=“scss”,支持scss
注意:
1) 关闭eslint
如果当前项目使用了eslint,并且需要关闭。需要创建vue.config.js文件,采用如下代码:
module.exports = {
devServer: {
overlay: {
warnings: false,
errors: false
}
},
lintOnSave:false //直接关闭eslint检查
}
2) proxy代理配置
devServer: {
open:true, //自动开启浏览器
port:8000, //随便改端口号
proxy: {
'/api': {
target: 'https://*.*.com',
host: '*.*.com',
changeOrigin:true
}
}
}
3) alias别名配置
configureWebpack: {
resolve: {
alias: {
'assets': '@/assets',
'con': '@/components',
'views': '@/views',
}
}
}
4) 打包路径配置
// 基本路径
publicPath: '/vue-demo'
十四. 移动端开发
我们现在关注的点还在移动M站上,或者我们可以叫做webapp,其实就是运行在移动端浏览器中的web网站。
app:application应用程序。
手机软件:主要指安装在智能手机上的软件,完善原始系统的不足与个性化。
移动端开发是与PC端肯定是有很大不同的,所以我们需要学习如何在移动设备上开发完美适配的app
开发移动端应用我们需要学习的知识点可以分成如下几个:
-
移动端布局适配
-
移动端事件
-
移动端交互效果
-
移动端前端框架
-
移动端调试
移动端布局适配
从屏幕尺寸、屏幕类型等方面来看的话,移动设备和PC设备大有不同,所以从布局、适配等方面都需要我们考虑到
Viewport视口的作用 (在移动端浏览器上面用来显示网页的那一块区域)
在很久以前,我们的设备还不是智能设备的时候,设备访问智能访问到网页的左上角(当时都是pc网站),查看全部内容需要通过滚动条
慢慢的我们发现,我们的一个页面放到移动端中访问的时候,没有滚动条了,但是内容都缩小了
这是因为我们有了一个叫做viewport的一个东西
网页不是直接放入浏览器中的,而是先放入到viewport中,然后viewport在等比缩放到浏览器的宽度,放入浏览器,viewport在缩放的过程中,网页内容也被缩小了
网页访问到的clientWidth其实是viewport的宽度
这样的话我们需要做一些处理,其实问题的根源在于viewport的宽度和浏览器宽度不一样,如果我们能将其设置为一样的话,不会出现这样的问题了
我们可以通过meta标签来设置viewport将其设置为浏览器的宽度,也就是设备的宽度,这样的话布局就会简单多了
viewport的宽度
当浏览器宽度小于980的时候,宽度就是980,当浏览器尺寸宽度大于980的时候,宽度和浏览器宽度一致
通过meta标签来设置viewport
标签提供关于 HTML 文档的元数据。它不会显示在页面上,但是对于机器是可读的。可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。meta viewport 的6个属性:
width | 设置layout viewport 的宽度,为一个正整数,或字符串"width-device" |
---|---|
initial-scale | 设置页面的初始缩放值,为一个数字,可以带小数 |
minimum-scale | 设置页面的最小缩放值,为一个数字,可以带小数 |
maximum-scale | 允许用户的最大缩放值,为一个数字,可以带小数 |
height | 设置layout viewport 的高度,这个属性并不重要,很少使用 |
user-scalable | 是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes代表允许 |
移动端布局方式与设计图
现有的布局方式:
-
固定布局,每一个元素都是固定的尺寸,内容区域居中在浏览器中间
内容区域的尺寸:980,1000,1100,1200
-
响应式布局,利用媒体查询来实现不同尺寸的浏览器显示结构不一样 @media 根据浏览器分辨率大小进行适配
一般会有三张设计图,PC,平板,手机
-
自适应布局,属于响应式里的一种,利用rem、百分比、vwvh等布局单位来实现
设计图一般只有一张,640、750居多
移动端布局
移动的屏幕和PC的屏幕有一个很大的区别,移动端是视网膜高清屏(Retina)
retina屏幕有一个属性叫DPR(设备像素缩放比) = 物理像素/逻辑像素
例如,iphone 6手机商宣传手机的尺寸是:750宽,这个值就是物理像素,而从开发者眼里我们所指的其实是375px(逻辑像素)
在dpr为2的手机中,我们的一个逻辑像素会从横纵两个方向分别以2个像素点来渲染
如果不管dpr的话,其实我们布局依然可以,因为我们设置一个像素宽高的东西的话,在手机上看见的基本也就是这么大,至于手机设备用多少个物理像素去渲染,大小还是不会变化的
设计师出图都是2倍的,是因为,在页面中除了字体(矢量图)大部分都是位图,也就是如果一个像素宽高的盒子里准备放入图片,如果图片的尺寸也是一个像素宽高的话,因为其实在移动端渲染的时候是用四个像素来渲染,图片会失真,但是如果我们给一像素宽高的盒子放入2像素宽高的图片的话,就不会失真
布局单位
因为我们的移动设备有很多种,所以我们的布局不可能是固定布局,所以我们要使用自适应布局
我们在开发中可以选用很多自适应布局单位,这些单位必须满足一个条件
-
%
优点:简单,无需设置,兼容性好
缺点:基于父元素的属性来设置,如果父元素没有宽高,设置无效 -
vwvh
一个vw等于viewport宽度的百分之一,一个vh等于viewport高度的百分之一
vmax等于vw和vh中较大的那个 vmin等于vw和vh中较小的那个优点:简单,无需设置
缺点:兼容性不好 -
rem
一个rem等于根元素(html)的字体大小,兼容性很好
优点:兼容好,使用简单
缺点:需要设置
rem与适配
当我们想使用一个自适应单位的时候,发现%有缺陷,vwvh兼容性差,弹性盒所针对的是元素排列的问题,只适用于某种情况,所以我们就想,能给我一个没啥上面的缺陷的单位,想到了rem
rem的兼容性好一点,它也确实是一个布局单位,不受父子元素的影响,设置了rem之后,也不会对px、em等单位造成影响,它是一个理想的单位
rem也有一个致命的问题,就是它不是一个自适应的单位,不会跟着设备尺寸不同而不同,但是没有关系,我们有万能的js,可以去动态的设置它
方法1:
我们可以将1rem设置成屏幕的某一个比例,比如将1rem设置成屏幕的十分之一
假设我们的设计图是640宽的,我们拿到之后量了一下a的宽度为480px,得到比例a所占屏幕3/4,根据rem与屏幕的关系,最后设置成7.5rem
就是说在设置元素的宽度是时候,会根据设定好的比例关系去进行换算
方法2:
如果设计图是640的图,这个时候我们知道它是照着i5来的,我们现在假设世界上所有的手机都是320的,也就是每一个人用的都是i5,在这个理想的情况下,因为手机都一样,尺寸都一样,和pc端的固定布局也就一样了
假设有一个在640的图上我们量得的宽度是320,因为是二倍图,所以我们知道,它的实际宽度是160px,这样的话,我们直接给这个设置设置width:160px就可以了,这个时候,我们玩个花子,不要单纯的使用px来设置,用rem来设置,例如,我可以将rem设置为100px,这样的,刚才的盒子设置为width:1.6rem,算法就是 量的宽度/(dpr*100) = 要设置的rem值
这样我们就可以开心的开发,量一个尺寸,除个2,再小数点推两位,设置就行了,但是我们也知道,手机的尺寸并不可能都是320,这样的话,没有关系,我们可以根据一个比例来算rem到底设置为多少
在手机宽度为320的时候,我们设置的1rem=100px,所以有一个比例 b = 100/320
那么在W宽度的手机上,1rem应该是多少呢?设为x 那么x/w = b
得到x = w/3.2
那么就不要写死html的fontsize为100了。而是用js去设置:
document.documentElement.style.fontSize = document.documentElement.clientWidth/3.2 + 'px’
这样,我们就可以得到一个自适应的rem
常见的需要注意的问题
-
在移动端中,如果给元素设置一个像素的边框的话,那么在手机上看起来是会比一个像素粗的。
解决方法:使用伪类元素模拟边框,使用transform缩放
-
响应式图片
在移动端中,图片的处理应该是很谨慎的,假设有一张图片本身的尺寸是X宽,设置和包裹它的div一样宽,如果是div宽度小于图片宽度没有问题,但是如果div宽度大于图片的宽度,图片被拉伸失真
解决方法:让图片最大只能是自己的宽度
img{
max-width: 100%;
display: block;
margin: 0 auto;
}
移动端事件
移动端中的事件和PC的事件有一些是不同的,例如,mouse部分事件在移动端里没有了
取而代之的是touch事件:
touchstart/touchmove/touchend/touchcancel(玩游戏 忽然来电话)
添加事件的时候可以用ontouchstart,但是有的时候很可能失效,建议使用addEventListener的方式
touchcancel比较少见,在系统取消触摸的时候触发
touch事件对象里面的属性和mouse的略有不同,例如在mouse事件里可以直接从事件对象里取出pageX,clientX,screenX
touch事件对象里有touches,changedTouches,targetTouches三个属性,上面保存着关键的位置信息
它们里面保存的是触发事件的手指的信息,但是要注意,虽然三个里面保存的信息看似都一样,但是在touchend事件里,只能使用changedTouches
click的300ms延迟问题
在移动端中,click事件是生效的,但是它有一个问题,点击之后会有300ms的延迟响应
原因:safari是最早做出这个机制的,因为在移动端里,浏览器需要等待一段事件来判断此次用户操作是单击还是双击,所以就有click300ms的延迟机制,Android也很快就有了
-
不用click,用自定义事件tap
tap是需要自定义的:如果用户执行了touchstart在很短的时间又触发了touchend,且两次的距离很小,而且不能触发touchmove
使用zepto类库的时候,里面自带tap事件,,但是需要在zepto.js后面加上一段js
百度有一款touch.js的插件教程
hammer.js也是一个手势事件库文档
-
引入fastclick库来解决
点透bug的产生
点透bug有一个特定的产生情况:
当上层元素是tap事件,且tap后消失,下层元素是click事件。这个时候,tap上层元素的时候就会触发下层元素的click事件
解决方式:
-
上下层都是tap事件,缺点:a标签等元素本身就是自带的click事件,更改为tap比较困难
-
缓动动画,让上层元素消失的时候不要瞬间消失,而是以动画的形式消失,事件超过300ms就可以了
-
使用中间层,添加一个透明的中间元素,给它添加click事件并消失,这个时候接收点透的是透明的中间层
-
使用fastclick
移动端测试
-
使用chrome浏览器有移动设备模拟功能,在这里可以做一些模拟测试,但是要注意的是,毕竟不是真机,会有一些测试不到的问题
-
手机连接上电脑的无线,总之使其在同一个网络里,然后就可以通过ip访问
需要测试的浏览器:
chrome,firefox,UC,百度,QQ,微信,Android,safari
移动端交互
动画效果全部使用css3
JQ生成二维码
可以使用jquery.qrcode.js插件,可以快速的生成基于canvas绘制的二维码
兼容查阅网站
can i use ,在这里可以查看很多属性、api的兼容性
十五. vue-router
现在的应用都流行SPA应用(single page application)
传统的项目大多使用多页面结构,需要切换内容的时候我们往往会进行单个html文件的跳转,这个时候受网络、性能影响,浏览器会出现不定时间的空白界面,用户体验不好
单页面应用就是用户通过某些操作更改地址栏url之后,动态的进行不同模板内容的无刷新切换,用户体验好。
Vue中会使用官方提供的vue-router插件来使用单页面,原理就是通过检测地址栏变化后将对应的路由组件进行切换(卸载和安装)
SPA vs MPA
简单路由实现
(1)cnpm install vue-router -S
(2)yarn add vue-router
-
引入vue-router,如果是在脚手架中,引入VueRouter之后,需要通过Vue.use来注册插件
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
- 创建router路由器
new Router({
routes:[
{path:"/home",component:Home}
]
})
- 创建路由表并配置在路由器中
var routes = [
{path,component}//path为路径,component为路径对应的路由组件
]
new Router({
routes
})
-
根实例里(main.js)里注入route
import Vue from 'vue' import App from './App.vue' //需要引入创建好的router的实例 import router from './router' Vue.config.productionTip = false new Vue({ render: h => h(App), router //目的? 让我们组件上面可以访问到的路由相关的api($route/$router) }).$mount('#app')
-
在根实例里注入router,目的是为了让所有的组件里都能通过this. r o u t e r 、 t h i s . router、this. router、this.route来使用路由的相关功能api
import router from "./router/index.js"
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
6.利用router-view来指定路由切换的位置
7.使用router-link来创建切换的工具,会渲染成a标签,添加to属性来设置要更改的path信息,且会根据当前路由的变化为a标签添加对应的router-link-active/router-link-exact-active(完全匹配成功)类名
<router-link to="main">main</router-link>
<router-link to="news">news</router-link>
.router-link-active{
color:red;
}
路由的懒加载
懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,延时过长,不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。
非按需加载则会把所有的路由组件块的js包打在一起。当业务包很大的时候建议用路由的按需加载(懒加载)。
按需加载会在页面第一次请求的时候,把相关路由组件块的js添加上;
{
path: '/about',
name: 'about',
component: () => import('@/views/About') //采用了路由懒加载方式(异步组件+webpack代码分割)
}
多级路由
在创建路由表的时候,可以为每一个路由对象创建children属性,值为数组,在这个里面又可以配置一些路由对象来使用多级路由,注意:一级路由path前加’/’
const routes = [
{path:'/main',component:AppMain},
{path:'/news',component:AppNews,children:[
{path:'/news/inside',component:AppNewsInside},
{path:'outside',component:AppNewsOutside}
]},
]
二级路由组件的切换位置依然由router-view来指定(指定在父级路由组件的模板中)
<router-link to='inside'>inside</router-link>
<router-link to='outside'>outside</router-link>
<router-view></router-view>
默认路由和重定向
当我们进入应用,默认像显示某一个路由组件,或者当我们进入某一级路由组件的时候想默认显示其某一个子路由组件,我们可以配置默认路由:
{path:'',component:Main}
当我们需要进入之后进行重定向到其他路由的时候,或者当url与路由表不匹配的时候:
{path:'/',redirect:'/main'}
///...放在最下面
{path:'*',redirect:'/main'},
命名路由
我们可以给路由对象配置name属性,这样的话,我们在跳转的时候直接写name:main就会快速的找到此name属性对应的路由,不需要写大量的urlpath路径了
<router-link
v-for="nav in navs"
:key="nav.id"
:to="{name:nav.name}"
>
{{nav.title}}</router-link>
{path:"guonei",component:Guonei,name:"guonei"},
{path:"guoji",component:()=>import("@/views/Guoji"),name:"guoji"}
动态路由匹配
有的时候我们需要在路由跳转的时候跟上参数,路由传参的参数主要有两种:路由参数、queryString参数
路由参数需要在路由表里设置
{path:'/detail/:id',component:Detail}
上面的代码就是给User路由配置接收id的参数,多个参数继续在后面设置
在组件中可以通过this.$route.params来使用
queryString参数不需要在路由表设置接收,直接设置?后面的内容,在路由组件中通过this.$route.query接收
prop将路由与组件解耦
在组件中接收路由参数需要this.$route.params.id,代码冗余,现在可以在路由表里配置props:true
{path:'detail/:id',component:AppNewsDetail,name:'detail',props:true}
在路由自己中可以通过props接收id参数去使用了
props:[‘id’]
声明式导航 router-link
组件支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签.。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。
router-link的to属性,默认写的是path(路由的路径),可以通过设置一个对象,来匹配更多
:to='{name:"detail",params:{id:_new.id},query:{content:_new.content}}'
name是要跳转的路由的名字,也可以写path来指定路径,但是用path的时候就不能使用params传参,params是传路由参数,query传queryString参数
replace属性可以控制router-link的跳转不被记录
active-class属性可以控制路径切换的时候对应的router-link渲染的dom添加的类名
编程式导航
有的时候需要在跳转前进行一些动作,router-link直接跳转,需要在方法里使用$router的方法
this.$router.push()
路由模式
为了构建SPA(单页面应用),需要引入前端路由系统,这也就是Vue-router存在的意义。前端路由的核心,就在于 ——— 改变视图的同时不会向后端发出请求。
路由有两种模式:hash、history,默认会使用hash模式,但是如果url里不想出现丑陋hash值,在new VueRouter的时候配置mode值为history来改变路由模式,本质使用H5的histroy.pushState方法来更改url,不会引起刷新.
history模式,会出现404 的情况,需要后台配置。
因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id 就会返回 404,这就不好看了。
所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
https://www.cnblogs.com/leyan/p/8677274.html
https://blog.csdn.net/ygh5123687/article/details/89473578
hash模式背后原理: 其实就是调用了window.onhashchange方法 hash值的切换
history模式的原理: 本质使用H5的histroy.pushState方法来更改url
hash模式和history模式的区别
- hash模式较丑,history模式较优雅
- hash兼容IE8以上,history兼容IE10以上
- history模式需要后端配合将所有访问都指向index.html,否则用户刷新页面,会导致404错误
路由守卫
在某些情况下,当路由跳转前或跳转后、进入、离开某一个路由前、后,需要做某些操作,就可以使用路由钩子来监听路由的变化
全局路由钩子:
router.beforeEach((to, from, next) => {
//会在任意路由跳转前执行,next一定要记着执行,不然路由不能跳转了
console.log('beforeEach')
console.log(to,from)
next()
})
router.afterEach((to, from) => {
//会在任意路由跳转后执行
console.log('afterEach')
})
单个路由钩子:
只有beforeEnter,在进入前执行,to参数就是当前路由
routes: [
{
path: '/foo',
component: Foo,
//进入到Foo组件之前调用!
beforeEnter: (to, from, next) => {
// ...
}
}
]
路由组件钩子:
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
十六. 阿里云服务器
Centos 64位 7.6 用户名都叫做root!
1.改密码 重启实例 233243 (改密码后需要重启实例才会有效!!)
2.远程控制 记录一个密码! ( git黑窗口 ssh root@公网IP 47.96.0.211)
3.配置安全组 1/60000 0.0.0.0/0 (ls cd / )
4.安装node.js
现在可以使用yum命令安装Node.js了。
sudo yum install nodejs
5.安装nginx服务器(静态服务器)
https://www.linuxidc.com/Linux/2016-09/134907.htm
/software/niginx-1.10.3.tar.gz
cd software
tar -zxvf nginx-1.10.3.tar.gz
cd nginx-1.10.3
./configure make make install
6.装好了之后直接访问公网 47.96.0.211
/usr/local/nginx/html/ ===> 文件存放位置
npm install pm2 -g (全局安装pm2)
可能会报错:
spawning pm2 daemon with pm2_home=/root/.pm2
解决办法:(升级node最新稳定版本)
*npm install -g n
*n stable
(升级node为12.18的版本了 重新开启黑窗口)
pm2 list
pm2 start ./bin/www --name = “名称”
pm2 delete id 删除
pm2 stop id 停掉
pm2 restart id 重启
部署线上接口
1)本地运行express-pro项目,开启本地数据库,配合postman接口进行调试,测验OK了。
2)远程创建 node-pro
-
不要忘记执行
. npm install nodemon -g
. npm i3)想要长期挂起服务,需要安装pm2
npm instal pm2 -g
pm2 start ./bin/www --name “express接口”
后续 pm2的命令:
pm2 start 启动服务id
pm2 delete 删除服务id
pm2 restart 重启服务id
4)直接用postman进行接口测试
http://公网IP:3000/api/user/loginin (post请求 username/password)
十七. 卖座项目
一. Films轮播图的实现
安装swiper轮播图插件:
npm view swiper versions
yarn add swiper@5.2.0
src/components/SwiperCom.vue
<template>
<div class="swiper-container" :class="cName">
<div class="swiper-wrapper">
<slot></slot>
</div>
<div class="swiper-pagination"></div>
</div>
</template>
<script>
export default {
props:["cName"]
}
</script>
src/views/Films.vue
<SwiperCom cName="films-banner">
<div
class="swiper-slide"
v-for="banner in banners"
:key="banner.bannerId"
>
<img :src="banner.imgUrl" alt="">
</div>
</SwiperCom>
<script>
import "swiper/css/swiper.min.css"
import axios from "axios"
import SwiperCom from "@/components/SwiperCom"
import Swiper from "swiper"
export default {
components:{
SwiperCom
},
data(){
return {
banners:[]
}
},
created(){
axios.get("https://m.maizuo.com/gateway?type=2&cityId=310100&k=9728409",{
headers:{
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"159097808175630079115266"}',
'X-Host': 'mall.cfg.common-banner'
}
}).then(res=>{
this.banners = [...res.data.data,{bannerId:234,imgUrl:""}];
//数据改变了,内部进行虚拟dom对比,对比成功才会生成真实dom结构。
this.$nextTick(()=>{
new Swiper(".films-banner",{
loop:true
})
})
})
}
}
</script>
后续会用到卖座的很多接口,不妨将其封装一下,后续调用的时候就会比较方便。
src/utils/http.js
import axios from "axios"
const instance = axios.create({
baseURL: 'https://m.maizuo.com',
timeout: 5000,
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"159097808175630079115266"}'
}
})
export default instance;
created(){
http.get("/gateway?type=2&cityId=310100&k=9728409",{
headers:{
'X-Host': 'mall.cfg.common-banner'
}
}).then(res=>{
this.banners = [...res.data.data,{bannerId:234,imgUrl:"https://pic.maizuo.com/usr/movie/463392bf3a2dd33e5f6e2b2fbbe32665.jpg?x-oss-process=image/quality,Q_70"}];
//数据改变了,内部进行虚拟dom对比,对比成功才会生成真实dom结构。
this.$nextTick(()=>{
new Swiper(".films-banner",{
loop:true,
pagination:{
el:".swiper-pagination"
}
})
})
})
}
二. 样式与rem相关设置
stylesheets/main.scss
@import "_base.scss";
@import "_reset.scss";
@import "_commons.scss";
在入口文件里面引入main.scss文件:
import “./stylesheets/main.scss”
utils/rem.js
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 3.75 + "px"
window.onresize = function(){
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 3.75 + "px"
}
在入口文件里面引入rem.js文件:
import “./utils/rem.js”
更改了components/Tabbar.vue的样式文件:
<style lang="scss" scoped>
ul{
position: fixed;
bottom:0;
left:0;
width:100%;
height:0.5rem; //iphone6 的375下,1rem=100px 0.5rem=50px
display: flex;
li{
flex:1;
text-align: center;
line-height: 0.5rem;
}
}
.active{
color:orange;
}
</style>
引入了iconfont字体图标库:
<!--引入iconfont的css样式文件-->
<link rel="stylesheet" href="<%= BASE_URL %>iconfont/iconfont.css">
Tabbar.vue文件:
<template>
<ul class='border-1px'>
<router-link
v-for="nav in navList"
:key="nav.id"
:to="nav.path"
tag="li"
active-class="active"
>
<i class="iconfont" :class="nav.icon"></i>
{{nav.title}}
</router-link>
</ul>
</template>
<script>
export default {
data(){
return {
navList:[
{id:1,title:"电影",path:"/films",icon:"icon-all"},
{id:2,title:"影院",path:"/cinema",icon:"icon-electronics"},
{id:3,title:"个人中心",path:"/center",icon:"icon-account"}
]
}
}
}
</script>
<style lang="scss" scoped>
ul{
position: fixed;
bottom:0;
left:0;
width:100%;
height:0.5rem;
display: flex;
padding-top:2px;
li{
flex:1; //让每个li均分
text-align: center;
line-height: 0.25rem;
display: flex; //设置弹性盒子
flex-direction: column; //设置主轴方向
}
}
.active{
color:orange;
}
</style>
又考虑到1像素边框问题?https://www.jianshu.com/p/5ff121936666
stylesheets/border-1.scss
Tabbar.vue
<style lang="scss" scoped>
//引入border-1.scss文件
@import "@/stylesheets/border-1.scss";
ul{
@include border-top(1px, #ccc); //通过@include就可以调用@mixins语法
position: fixed;
bottom:0;
left:0;
width:100%;
height:0.5rem;
display: flex;
padding-top:2px;
li{
flex:1; //让每个li均分
text-align: center;
line-height: 0.25rem;
display: flex; //设置弹性盒子
flex-direction: column; //设置主轴方向
}
}
.active{
color:orange;
}
</style>
<ul class='border-1px'>
三. 登录功能实现
3-1 Login页面获取token令牌
methods:{
login(){
axios.post("/info/api/user/loginin",{
username:"admin",
password:123
}).then(res=>{
console.log(res)
})
}
}
vue.config.js文件配置正向代理:(重启服务)
devServer: {
overlay: {
warnings: false,
errors: false
},
open:true, //自动开启浏览器
port:8000, //随便改端口号
proxy: { //配置代理 解决前端浏览器的跨域问题
'/info': {
target: 'http://47.96.0.211:3000', //目标请求的域名地址
changeOrigin:true, //是否改变
pathRewrite:{ //重写地址
"^/info":""
}
}
}
},
3-2 axios封装
utils/http.js
import axios from "axios"
//关联的是卖座相关的接口
const instance = axios.create({
baseURL: 'https://m.maizuo.com',
timeout: 5000,
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"159097808175630079115266"}'
}
})
//关联的是http://47.96.0.211:3000
const instance2 = axios.create({
baseURL: '/info',
})
export {
instance,
instance2
};
记得需要更改Films里面代码。
import {instance} from "@/utils/http"
Login.vue
<script>
import {instance2} from "@/utils/http"
export default {
methods:{
login(){
instance2.post("/api/user/loginin",{
username:"admin",
password:123
}).then(res=>{
console.log(res)
})
}
}
}
</script>
在http.js文件里面进行了axios的拦截器封装操作:
//在发生请求之前做一个拦截操作
instance2.interceptors.request.use(config=>{
console.log("config1====>",config)
return config
})
//获取数据后,后端给我们进行响应,做一个拦截
//真实业务场景中,一般后台给前端返回很多的状态码,这里根据返回的状态码进行业务逻辑实现,从而给前端返回不同数据内容。
instance2.interceptors.response.use(res=>{
if(res.data.flag){ //代表后端成功响应结果了
return res.data.data
}else{
return Promise.reject("出错了...")
}
})
Login.vue
methods:{
login(){
instance2.post("/api/user/loginin",{
username:"admin",
password:123
}).then(res=>{
//登录成功后,需要将后端返回的token令牌放入到本地存储里面
localStorage.setItem("token",res.token)
//跳转到个人中心
//个人中心内部beforeRouteEnter进行组件内部的路由守卫的拦截,发现如果本地存储里面有了token令牌了,那么就next() , 否则 next("/login")
this.$router.push("/center")
}).catch(err=>{
console.log(err)
})
}
}
后续考虑到token令牌可能过段时间就会失效了,刚刚写的代码无论何时都可以一直登陆状态。
Center.vue
created(){
//进行接口请求,为了检测token是否处于失效状态
instance2({
method:"post",
url:"/api/user/isloginin",
headers:{
'X-Access-Token': localStorage.getItem("token")
}
}).then(res=>{
console.log("centerres-->",res)
}).catch(err=>{ //token已经失效了,需要跳转到登录界面进行重新登录
this.$router.push("/login")
})
}
如果很多地方的接口都需要携带token令牌发送后端,上述写法就比较麻烦。麻烦之处在于每次请求的时候都需要携带token给后端,可不可以在一个地方写上token之后,其他调用接口的地方不需要携带了呢?
//在发生请求之前做一个拦截操作
instance2.interceptors.request.use(config=>{
//例如你在每次发送请求给后端的时候,都可以在他的请求头上面携带一些东西给后端。
if(localStorage.getItem("token")){
config.headers['X-Access-Token'] = localStorage.getItem("token")
}
return config
})
created(){
//进行接口请求,为了检测token是否处于失效状态
instance2({
method:"post",
url:"/api/user/isloginin",
}).then(res=>{
console.log("centerres-->",res)
}).catch(err=>{ //token已经失效了,需要跳转到登录界面进行重新登录
this.$router.push("/login")
})
}
四. NowPlaying页面编写
<div class="nav">
<router-link active-class="active" :to="{
name:'now'
}">正在热映</router-link>
<router-link to="/films/comingsoon" active-class="active">即将上映</router-link>
</div>
.nav{
height:40px;
line-height: 40px;
display: flex;
justify-content: space-around;
.active{
color:orange;
}
}
Nowplaying.vue
created(){
instance.get("/gateway?cityId=310100&pageNum=1&pageSize=10&type=1&k=7998846",{
headers:{
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res=>{
this.dataList = res.data.data.films
})
}
<div>
<ul>
<router-link
tag="li"
v-for="data in dataList"
:key="data.filmId"
to="/detail/1"
>
<img :src="data.poster" alt="">
<div class="content">
<h4>{{data.name}}</h4>
<p>观众评分 <span>{{data.grade}}</span></p>
<p class="actors">主演:<span>{{data.actors | actorsFilter}}</span></p>
</div>
</router-link>
</ul>
</div>
actorsFilter 封装成了过滤器,并且全局注册了。
utils/filters.js (Vue上面全局的方法有哪些? Vue.filter Vue.component Vue.directive Vue.nextTick Vue.set)
import Vue from "vue"
Vue.filter("actorsFilter",(actors)=>{
return actors.map(item=>item.name).join(" ")
})
main.js
//引入 filters.js文件
import "./utils/filters"
即将上映页面:
<script>
import {instance} from "@/utils/http"
export default {
data(){
return {
dataList:[]
}
},
created(){
instance.get("/gateway?cityId=310100&pageNum=1&pageSize=10&type=2&k=1161682",{
headers:{
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res=>{
console.log(res)
this.dataList = res.data.data.films
})
}
}
</script>
<template>
<div>
<ul>
<router-link
tag="li"
v-for="data in dataList"
:key="data.filmId"
to="/detail/1"
>
<img :src="data.poster" alt="">
<div class="content">
<h4>{{data.name}}</h4>
<p class="actors">主演:<span>{{data.actors | actorsFilter}}</span></p>
<p>上映日期:</p>
</div>
</router-link>
</ul>
</div>
</template>
<style lang="scss" scoped>
li{
padding: 10px;
display: flex;
img{
width:0.66rem;
height:0.9rem;
}
.content{
display: flex;
flex-direction: column;
margin-left: 10px;
}
.actors{
width:2.2rem;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
后续封装了组件FilmItem,因为发现正在热映与即将上映的数据item几乎是一样的。
<template>
<div class="item">
<img :src="data.poster" alt="">
<div class="content">
<h4>{{data.name}}</h4>
<p v-if="type==='nowplaying'">观众评分 <span>{{data.grade}}</span></p>
<p class="actors">主演:<span>{{data.actors | actorsFilter}}</span></p>
<p v-if="type==='nowplaying'">{{data.nation}} | {{data.runtime}}分钟</p>
<p v-if="type==='comingsoon'">上映日期:</p>
</div>
</div>
</template>
<script>
export default {
props:["data","type"]
}
</script>
<FilmItem :data="data" type='nowplaying'></FilmItem>
五. 跳转到详情页面
<router-link
tag="li"
v-for="data in dataList"
:key="data.filmId"
:to="'/detail/'+data.filmId"
>
</router-link>
Detail.vue
export default {
data(){
return {
filmInfo:null
}
},
created(){
instance.get(`/gateway?filmId=${this.$route.params.id}&k=7699964`,{
headers:{
'X-Host': 'mall.film-ticket.film.info'
}
}).then(res=>{
console.log(res)
this.filmInfo = res.data.data.film
})
}
}
<template>
<div class="detail" v-if="filmInfo">
<img width='100%' :src="filmInfo.poster" alt="">
<h4>{{filmInfo.name}} {{filmInfo.filmType.name}}</h4>
<p>{{filmInfo.category}}</p>
<p>{{filmInfo.premiereAt | time}}上映</p>
</div>
</template>
然后又进行了日期时间的格式化,采用了moment插件进行日期格式化
https://momentjs.com/docs/
https://www.cnblogs.com/ckmouse/p/11754454.html
//过滤时间
import moment from "moment"
moment.locale('zh-cn');
Vue.filter("time",time=>{
let date = new Date(time*1000) //将秒数转成毫秒数
return moment(date).format("YYYY年MM月DD日 dddd")
})
六. 详情界面的布局
6-1 详情的动画效果
<div class="detail" v-if="filmInfo">
<img width='100%' :src="filmInfo.poster" alt="">
<div class="filmInfo-con">
<h4>{{filmInfo.name}} {{filmInfo.filmType.name}}</h4>
<p>{{filmInfo.category}}</p>
<p>{{filmInfo.premiereAt | time}}上映</p>
<p class="grade">{{filmInfo.grade}} 分</p>
<p style="height:120px;overflow: hidden;transition:height .5s ease" :class="{synopsis:synopsis}">{{filmInfo.synopsis}}</p>
<i @click="synopsis=!synopsis" class="iconfont" :class="synopsis?'icon-moreunfold':'icon-less'"></i>
</div>
</div>
data(){
return {
filmInfo:null,
synopsis:true
}
},
.synopsis{
height:38px!important;
}
6-2 轮播图实现
<h4>演职人员</h4>
<SwiperCom cName='actors'>
<div
class="swiper-slide"
v-for="data in filmInfo.actors"
:key="data.name"
>
<img class="actor-img" :src="data.avatarAddress" alt="">
<p style="text-align:center;">{{data.name}}</p>
<p style="text-align:center;">{{data.role}}</p>
</div>
</SwiperCom>
<h4>剧照</h4>
<SwiperCom cName='photos'>
<div
class="swiper-slide"
v-for="(data,index) in filmInfo.photos"
:key="index"
>
<img class="photo-img" :src="data" alt="">
</div>
</SwiperCom>
//进行演员轮播图实例化
this.$nextTick(()=>{
new Swiper(".actors",{
slidesPerView: 4, //一行显示4个
spaceBetween: 30, //每一个的间距
})
})
//进行剧照轮播图实例化
this.$nextTick(()=>{
new Swiper(".photos",{
slidesPerView: 2.2,
spaceBetween: 5
})
})
.filmInfo-con{
position: relative;
padding: 15px;
.actor-img{
width:.85rem;
height:1rem;
}
.photo-img{
width:1.5rem;
// height:0.95rem;
}
.grade{
position: absolute;
top:10px;
right:10px;
color: #5c564c;
font-style: italic;
font-size: 16px;
}
.synopsis{
height:38px!important;
}
i{
position: absolute;
left:50%;
transform: translateX(-50%);
}
}
6-3 详情头部实现
<div class="detail-title">
<i class="iconfont icon-back"></i>
<span>八佰</span>
</div>
.detail-title{
height: .44rem;
line-height: .44rem;
position: fixed;
background: #fff;
width: 100%;
text-align: center;
i{
position: absolute;
left:10px
}
}
如果想要实现滚动事件,需要在mounted进行初始化事件绑定,在beforeDestroy里面将初始化事件及时清空。
mounted(){
window.onscroll = function(){
console.log(document.documentElement.scrollTop)
}
},
beforeDestroy(){
window.onscroll = null
},
发现上述实现比较麻烦,因为时刻谨记着写销毁的钩子函数。
采用自定义指令方式,来去操作真实dom元素
import Vue from "vue"
//自定义指令做什么的? 用来操作真实dom元素的
//如何定义自定义指令? 两种方案? 全局Vue.directive() 局部directives
//自定义指令的钩子函数? bind inserted update componentUpdated unbind
// v-title
Vue.directive("title",{
//插入到dom的时候执行
inserted(el,binding){
el.style.display = "none"
window.onscroll = function(){
let sTop = document.documentElement.scrollTop || document.body.scrollTop;
if(sTop > 100){
el.style.display = "block"
}else{
el.style.display = "none"
}
}
},
unbind(){
window.onscroll = null
}
})
通过自定义指令可以给他传一些额外的参数,实现不同的功能。
并且通过切换opacity,来去实现显示与隐藏的效果。 记住:给它添加样式 transition:opacity .3s linear;
import Vue from "vue"
Vue.directive("title",{
//插入到dom的时候执行
inserted(el,binding){
el.style.opacity = 0
window.onscroll = function(){
let sTop = document.documentElement.scrollTop || document.body.scrollTop;
let value = binding.value || 100
if(sTop > value){
el.style.opacity = 1
}else{
el.style.opacity = 0
}
}
},
unbind(){
window.onscroll = null
}
})
6-4 剧照的实现
views/detail/photos.vue
<style lang="scss" scoped>
.photos{
width:100%;
height:100%;
position: fixed;
top:0;
left:0;
background: #fff;
z-index: 10;
}
</style>
Detail.vue
<!--显示剧照页面-->
<Photos v-if="isShow"></Photos>
封装了全局组件mz-title组件。需要在main.js里面将其引入,然后进行全局注册。
全局注册了之后,任何组件只要用到这个头部了,直接调用mz-title即可。
//引入全局组件Title
import Title from "@/components/Title"
Vue.component("mz-title",Title)
通过插槽的方式,来进行动态内容实现
<template>
<div class="detail-title">
<i class="iconfont icon-back"></i>
<!--在标题模板里面提供了插槽,等待外部调用此组件的时候,可以往其插入内容-->
<slot></slot>
</div>
</template>
Detail.vue
<!--调用全局组件-->
<mz-title>
<span>{{filmInfo.name}}</span>
</mz-title>
点击左箭头,可以返回之前的页面。
Title.vue
<i class="iconfont icon-back" @click="clickme"></i>
<script>
export default {
methods:{
goBack(){
this.$router.back()
}
}
}
</script>
在详情页面的头部点击确实可以实现返回到正在热映的界面,但是在photos里面点击仍然返回到正在热映,这个就不符合目前的需求了。
<template>
<div class="detail-title">
<i class="iconfont icon-back" @click="goback"></i>
<slot></slot>
</div>
</template>
<script>
export default {
methods:{
goback(){
//希望点击的时候,可以触发自定义事件 <mz-title @change="back">
this.$emit("change")
}
}
}
</script>
Detail.vue
<!--调用全局组件-->
<mz-title @change="back">
<span>{{filmInfo.name}}</span>
</mz-title>
methods:{
back(){
this.$router.back()
}
},
Photos.vue
<!--调用title组件-->
<mz-title @change="back">
<span>剧照</span>
</mz-title>
<script>
export default {
methods:{
back(){
//希望父组件可以将isShow变成false 通过viewModel关系链来去实现通信
//就是通过关系链找到父组件,从而让父组件的isShow变成false
this.$parent.isShow = false
}
}
}
</script>
同时也可以采用下面方式实现:
<!--显示剧照页面-->
<Photos v-if="isShow">
<mz-title @change="back2">
<span>剧照</span>
</mz-title>
</Photos>
back2(){
this.isShow = false
},
<template>
<div class="photos">
<slot></slot>
</div>
</template>
在Photos将数据进行渲染,显示数据
<!--显示剧照页面-->
<Photos v-if="isShow" :photos="filmInfo.photos">
<mz-title @change="back2">
<span>剧照 ({{filmInfo.photos.length}})</span>
</mz-title>
</Photos>
export default {
props:["photos"]
}
<template>
<div class="photos">
<slot></slot>
<ul>
<li
v-for="item in photos"
:key="item"
>
<img :src="item" alt="">
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.photos{
width:100%;
height:100%;
position: fixed;
top:0;
left:0;
background: #fff;
z-index: 10;
ul{
position: relative;
top:50px;
display: flex;
flex-wrap: wrap;
li{
width:33.33333%;
height:1.24rem;
padding:4px;
img{
width:100%;
height:100%;
}
}
}
}
</style>
6-5 切换路由实现转场动画
发现卖座切换路由的时候,有一些转场动画,所以可以通过transition标签实现。
App.vue
<!--router-view来指定路由切换的位置-->
<transition name="app" mode="out-in">
<router-view></router-view>
</transition>
<style lang="scss" scoped>
.app-enter-active{
animation: move .66s; //调用关键帧
}
.app-leave-active{
animation: move .66s reverse;
}
@keyframes move{ //关键帧
0%{
transform:translateY(40px);
opacity: 0;
}
100%{
transform: translateY(0px);
opacity: 1;
}
}
</style>
七. 影院布局渲染
7-1 影院数据渲染
<script>
import {instance} from "@/utils/http"
export default {
data(){
return {
cinemaList:[] //影院列表
}
},
created(){
instance.get("/gateway?cityId=310100&ticketFlag=1&k=9249373",{
headers:{
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res=>{
console.log(res.data)
this.cinemaList = res.data.data.cinemas
})
}
}
</script>
<template>
<div class="cinema">
<div class="title">
<div class="left">上海</div>
<div class="center">影院</div>
<div class="right"><i class="iconfont icon-search"></i></div>
</div>
<div class="select">
<div>全城</div>
<div>App订票</div>
<div>最近去过</div>
</div>
<!--遍历影院的数据-->
<div class="cinemaList">
<ul>
<li
v-for="data in cinemaList"
:key="data.cinemaId"
>
<h4>{{data.name}}</h4>
<p>{{data.address}}</p>
</li>
</ul>
</div>
</div>
</template>
7-2 实现选择区域
computed:{
//获取所有的区域
cinemaArea(){
let newArr = this.cinemaList.map(item=>item.districtName)
//如何实现数组去重? 3种 最高效的一种(set) set数据结构只能是唯一
//https://segmentfault.com/a/1190000016418021
// console.log("newArr===>",Array.from(new Set(newArr)))
// 第一种方案 在数组前面追加全城
// let arr = Array.from(new Set(newArr))
// arr.unshift("全城")
// 第二种方案 数组的展开运算符
return ["全城",...Array.from(new Set(newArr))]
}
}
区域有了之后,点击每一个区域,显示不同区域下面的影院列表。所以需要有一个变量标识,记住当前所点击的区域的名字 currentArea.
data(){
return {
cinemaList:[], //影院列表
isAreaShow:false, //默认区域是不显示
currentArea:"全城"
}
},
<!--显示区域-->
<div class="area" v-show="isAreaShow">
<ul>
<li
v-for="data in cinemaArea"
:key="data"
@click="currentArea=data;isAreaShow=false"
:class="{active:data===currentArea}"
>{{data}}</li>
</ul>
</div>
computed:{
//获取所有的区域
cinemaArea(){
.......
},
//点击不同的区域,显示不同区域的影院
filterCinemaArea(){
//如果用户点击的是全城,则直接返回所有的影院列表数据
if(this.currentArea === "全城") return this.cinemaList;
//数组的过滤筛选
return this.cinemaList.filter(item=>{
if(item.districtName === this.currentArea){
return true
}
return false
})
}
},
<!--遍历影院的数据-->
<div class="cinemaList">
<ul>
<li
v-for="data in filterCinemaArea"
:key="data.cinemaId"
>
<h4>{{data.name}}</h4>
<p>{{data.address}}</p>
</li>
</ul>
</div>
7-3 通过vuex控制tabbar显示
yarn add vuex
src/store/index.js
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)
let store = new Vuex.Store({
})
export default store
src/main.js
import store from "./store"
new Vue({
router, //目的 就是让组件可以访问this.$route / this.$router api.
store, //目的 就是让组件可以访问this.$store
render: h => h(App)
}).$mount('#app')
在组件中,this.$store如果存在,则vuex就已经可以在项目里面使用了。
需要在vuex中创建一个isTabbarShow这个state.
let store = new Vuex.Store({
state:{ //用来定义vuex所维护的状态
isTabbarShow:true
},
mutations:{ //只能通过定义mutations的一些方法,来去同步的更改vuex中的状态。
show(state){
state.isTabbarShow = true
},
hide(state){
state.isTabbarShow = false
}
}
})
<!--显示tabbar-->
<Tabbar v-if="$store.state.isTabbarShow"></Tabbar>
Search.vue组件里面:
created(){
//需要触发mutations里面的hide方法
this.$store.commit("hide")
},
beforeDestroy(){
this.$store.commit("show")
}
点击cinema里面的search图标,通过编程式方式进行路由跳转
<div class="right" @click="toSearch"><i class="iconfont icon-search"></i></div>
methods:{
toSearch(){
this.$router.push("/cinema/search")
}
},
7-4 将影院数据由vuex管理
本来数据是在cinema的created中进行异步请求,后续发现Search组件里面也会用到影院的数据,也得请求一次。如果很多地方都会用到一些同样的数据,所以将这些数据直接放到vuex中管理,多组件就可以共享这些数据了。
store
state:{ //用来定义vuex所维护的状态
isTabbarShow:true,
cinemaList:[]
},
actions:{ //进行异步请求的方法,做异步操作获取数据
getCinemaListAction(context){
instance.get("/gateway?cityId=310100&ticketFlag=1&k=9249373",{
headers:{
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res=>{
// console.log(res.data)
// 需要调用mutations里面的setCinemaList方法
context.commit("setCinemaList",res.data.data.cinemas)
})
}
},
mutations:{
setCinemaList(state,data){
state.cinemaList = data
}
}
Cinema.vue
computed:{
//获取所有的区域
cinemaArea(){
let newArr = this.$store.state.cinemaList.map(item=>item.districtName)
return ["全城",...new Set(newArr)]
},
//点击不同的区域,显示不同区域的影院
filterCinemaArea(){
if(this.currentArea === "全城") return this.$store.state.cinemaList;
return this.$store.state.cinemaList.filter(item=>{
if(item.districtName === this.currentArea){
return true
}
return false
})
}
},
Search.vue
computed:{
getCinemaListFive(){
return this.$store.state.cinemaList.slice(0,5)
}
},
<ul>
<li
v-for="data in getCinemaListFive"
:key="data.cinemaId"
>{{data.name}}</li>
</ul>
发现如果直接刷新浏览器,search里面的数据就不存在了。原因是vuex中的数据是放入到内存里面的,所以浏览器一刷新,他就没有数据了,就直接变成初始值了。
created(){
this.$store.commit("hide")
//判断一下如果vuex中数据的长度为0,需要重新进行异步请求
if(this.$store.state.cinemaList.length === 0){
this.$store.dispatch("getCinemaListAction")
}
},
八. 搜索界面
8-1 搜索界面数据搜索
<template>
<div class="search">
<div class="query">
<input type="text" v-model="searchVal">
<span>取消</span>
<div v-show="searchVal">
<ul>
<li>11111</li>
<li>222222</li>
<li>3333333</li>
</ul>
</div>
</div>
<div v-show="!searchVal">
<ul>
<li
v-for="data in getCinemaListFive"
:key="data.cinemaId"
>{{data.name}}</li>
</ul>
</div>
</div>
</template>
data(){
return {
searchVal:""
}
},
computed:{
searchCinemaData(){
return this.$store.state.cinemaList.filter(item=>item.name.toLowerCase().includes(this.searchVal.toLowerCase()))
},
getCinemaListFive(){
return this.$store.state.cinemaList.slice(0,5)
}
},
8-2 vuex中getters实现获取5条数据
本来在Search.vue组件中通过getCinemaListFive 可以获取5条数据,但是如果后续很多组件都想要获取5条影院数据,那么需要分别在他们各自的组件里面写getCinemaListFive。
我们可以采用vuex中核心模块getters去实现。它类似于computed,依赖于vuex中state
getters:{
getCinemaListFive(state){
return state.cinemaList.slice(0,5)
}
},
8-3 vuex中的辅助函数使用
mapState
mapState辅助函数就是用来方便获取vuex中的状态。本来想要获取vuex中状态this.$store.state.cinemaList.
这样获取状态太麻烦了,后续采用mapState即可。
import {mapState} from "vuex"
computed:{
...mapState(["cinemaList"]),
//获取所有的区域
cinemaArea(){
let newArr = this.cinemaList.map(item=>item.districtName)
return ["全城",...new Set(newArr)]
},
//点击不同的区域,显示不同区域的影院
filterCinemaArea(){
if(this.currentArea === "全城") return this.cinemaList;
return this.cinemaList.filter(item=>{
if(item.districtName === this.currentArea){
return true
}
return false
})
}
},
mapGetters
辅助函数,本来获取getters里面的内容,this.$store.getters.xxx 比较麻烦。
import {mapState,mapGetters} from "vuex"
computed:{
...mapState(["cinemaList"]),
...mapGetters(["getCinemaListFive"])
}
mapActions
辅助函数,用法与前面两个类似。但是需要将其写入到methods方法
import {mapState,mapActions} from "vuex"
created(){
if(this.cinemaList.length>0){
}else{
// this.$store.dispatch("cinema/getCinemaListAction")
this.getCinemaListAction()
}
}
mapMutations
辅助函数,跟mapActions一模一样用法
Search.vue
import {mapState,mapGetters,mapMutations,mapActions} from "vuex"
methods:{
...mapMutations("tabbar",["hide","show"]),
...mapActions("cinema",["getCinemaListAction"])
},
created(){
//需要触发mutations里面的hide方法
this.hide() //this.$store.commit("tabbar/hide")
if(this.cinemaList.length === 0){
this.getCinemaListAction() //this.$store.dispatch("cinema/getCinemaLIstxxx")
}
},
beforeDestroy(){
this.show()
}
modules划分模块
首先store是唯一的,不能拆分store. 只能对齐内部的模块进行划分。
store/module/cinema.js
import {instance} from "@/utils/http"
const cinema = {
namespaced: true,
state:{
cinemaList:[]
},
mutations:{
setCinemaList(state,data){
state.cinemaList = data
}
},
actions:{
getCinemaListAction(context){
instance.get("/gateway?cityId=310100&ticketFlag=1&k=9249373",{
headers:{
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res=>{
// console.log(res.data)
// this.cinemaList = res.data.data.cinemas
// 需要调用mutations里面的setCinemaList方法
context.commit("setCinemaList",res.data.data.cinemas)
})
}
},
getters:{
getCinemaListFive(state){
return state.cinemaList.slice(0,5)
}
}
}
export default cinema
store/tabbar.js
const tabbar = {
namespaced: true,
state:{
isTabbarShow:true
},
mutations:{
show(state){
state.isTabbarShow = true
},
hide(state){
state.isTabbarShow = false
}
}
}
export default tabbar
store/index.js 里面将cinema/tabbar进行modules合并
import Vue from "vue"
import Vuex from "vuex"
import cinema from "./module/cinema"
import tabbar from "./module/tabbar"
Vue.use(Vuex)
let store = new Vuex.Store({
modules:{
cinema:cinema,
tabbar:tabbar
}
})
export default store
Cinema.vue
使用的时候,必须要开启命名空间! namespaced: true
computed:{
...mapState("cinema",["cinemaList"]),
}
this.$store.dispatch("cinema/getCinemaListAction")
Search.vue
computed:{
...mapState("cinema",["cinemaList"]), //用来获取vuex中state
...mapGetters("cinema",["getCinemaListFive"]), //用来获取vuex中getters
}
created(){
this.$store.commit("tabbar/hide")
if(this.cinemaList.length === 0){
this.$store.dispatch("cinema/getCinemaListAction")
}
},
beforeDestroy(){
this.$store.commit("tabbar/show")
}
App.vue
computed:mapState("tabbar",["isTabbarShow"]),
后续获取vuex相关的一些状态、getters、actions、mutations的话,需要根据指明的模块名字才可以获取到。
vuex中的项目结构:
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
九. Vant组件库使用
9-1 项目中使用vant组件库
yarn add vant
yarn add babel-plugin-import -D
babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
重启完毕后,在Login.vue中使用了vant里面的组件。
import Vue from "vue"
import {Button,Field,Dialog} from "vant"
Vue.use(Button).use(Field)
<form @submit.prevent="login">
<van-field v-model="username" label="用户:" placeholder="请输入用户名" />
<van-field v-model="password" type="password" label="密码:" placeholder="请输入密码" />
<van-button type="primary">登录</van-button>
</form>
methods:{
login(){
if(!this.username.trim() || !this.password.trim()){
Dialog({ message: '请输入用户名或者密码...' });
return false;
}
instance2.post("/api/user/loginin",{
username:this.username,
password:this.password
}).then(res=>{
localStorage.setItem("token",res.token)
//跳转到个人中心
this.$router.push("/center")
}).catch(err=>{
Dialog.alert({
theme: 'round-button',
confirmButtonText:"哈哈",
message: err
});
})
}
}
9-2 search页面中使用vant组件库
import Vue from 'vue';
import { Search } from 'vant';
Vue.use(Search);
<van-search
v-model="searchVal"
show-action
placeholder="请输入搜索关键词"
@cancel="onCancel"
/>
methods:{
...mapMutations("tabbar",["hide","show"]),
...mapActions("cinema",["getCinemaListAction"]),
onCancel(){
this.$router.back()
}
},
import { Search,List,Cell } from 'vant';
Vue.use(Search).use(List).use(Cell);
<van-list>
<van-cell
v-for="data in getCinemaListFive"
:key="data.cinemaId"
:title="data.name"
:label="data.address"
icon="location-o"
/>
</van-list>
9-3 nowplaying的上拉加载
import Vue from 'vue';
import { List,Cell } from 'vant';
Vue.use(List).use(Cell)
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
offset="50"
>
<van-cell v-for="data in datalist" :key="data.filmId" :title="data.name" />
</van-list>
data(){
return {
datalist:[],
loading:false,
finished:false,
pageNum:1
}
},
methods:{
onLoad(){ //初始化的时候执行 & 滚动到距离底部50px会执行
instance.get("/gateway?cityId=430100&pageNum="+this.pageNum+"&pageSize=10&type=1&k=1414009",{
headers:{
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res=>{
//这里要使用数组的concat方法!
this.datalist = this.datalist.concat(res.data.data.films)
// 加载状态结束
this.loading = false;
// 数据全部加载完成
if(this.pageNum * 10 >= res.data.data.total){
this.finished = true;
}
// 让页码加一
this.pageNum++
})
}
},
9-4 search搜索内容的组件复用
views/cinema/Item.vue
<template>
<li>
<h4>{{data.name}}</h4>
<p>{{data.address}}</p>
</li>
</template>
<script>
export default {
props:["data"] //接受外部传入的data属性
}
</script>
<style lang="scss" scoped>
li{
padding: 0.15rem;
p{
width:65%;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
Cinema
import Item from "./cinema/Item"
components:{
Item
},
<!--遍历影院的数据-->
<div class="cinemaList">
<ul>
<!-- <li
v-for="data in filterCinemaArea"
:key="data.cinemaId"
>
<h4>{{data.name}}</h4>
<p>{{data.address}}</p>
</li> -->
<Item
v-for="data in filterCinemaArea"
:key="data.cinemaId"
:data="data"
/>
</ul>
</div>
Search
import Item from "./Item"
components:{
Item
},
<div v-show="searchVal">
<ul>
<!-- <li
v-for="data in searchCinemaData"
:key="data.cinemaId"
>{{data.name}}</li> -->
<Item
v-for="data in searchCinemaData"
:key="data.cinemaId"
:data="data"
/>
</ul>
</div>
9-5 选择城市功能
1.创建city组件
2.router/index.js里面进行路由映射
{
path:"/city",
component:()=>import("@/views/City")
},
3. 需要在Cinema组件中,点击城市的时候,跳转到city组件
<div class="title">
<div class="left" @click="toPage('/city')">上海</div>
<div class="center">影院</div>
<div class="right" @click="toPage('/cinema/search')"><i class="iconfont icon-search"></i></div>
</div>
methods:{
...mapActions("cinema",["getCinemaListAction"]),
toPage(path){
//通过编程式导航的跳转到目标路由地址
this.$router.push(path)
}
},
4. 需要布局City页面了
import Vue from 'vue';
import { IndexBar, IndexAnchor, Cell } from 'vant';
import {instance} from "@/utils/http"
Vue.use(IndexBar);
Vue.use(IndexAnchor);
Vue.use(Cell);
created(){
instance.get("/gateway?k=1641570",{
headers:{
'X-Host': 'mall.film-ticket.city.list'
}
}).then(res=>{
console.log(res)
})
}
异步请求获取到了307个城市后,想到如何将这些城市渲染到界面中。cities如果改装成下述这个结构即可。
data(){
return {
cities:[
{
index:'A',
list:["鞍山","安顺","安泰","安阳"]
},
{
index:"B",
list:["保定","北京","保安","宝鸡"]
}
]
}
},
<van-index-bar>
<div
v-for="(data,index) in cities"
:key="index"
>
<van-index-anchor :index="data.index" />
<van-cell
v-for="(item,index) in data.list"
:key="index"
:title="item"
/>
</div>
</van-index-bar>
methods:{
filterCity(cities){
//生成26个大写字母
let letterArr = []
let citiesList = []
for(var i=65;i<91;i++){
letterArr.push(String.fromCharCode(i))
}
//遍历所有的城市,获取每个对象的pinyin的第一个字母,然后跟所需要的26个字母匹配
for(var i=0;i<letterArr.length;i++){
let tempArr = cities.filter(item=>item.pinyin.substring(0,1).toUpperCase() === letterArr[i])
if(tempArr.length>0){ //如果对应大写字母有数据的话,才往数组push内部
citiesList.push({
index: letterArr[i],
list:tempArr
})
//需要自定义索引列表
this.indexList.push(letterArr[i])
}
}
this.cities = citiesList
}
}
后续更改了左侧的自定义索引列表。
<van-index-bar :index-list="indexList">
9-6 切换城市
src/store/module/city.js
const city = {
namespaced: true,
state:{
cityName:"上海"
},
mutations:{
setCityName(state,name){
state.cityName = name
}
}
}
export default city
import Vuex from "vuex"
import cinema from "./module/cinema"
import tabbar from "./module/tabbar"
import city from "./module/city"
Vue.use(Vuex)
//vuex中只能有一个store实例,所以后续只能进行store的模块拆分
let store = new Vuex.Store({
modules:{
cinema,
tabbar,
city
}
})
export default store
City
...mapMutations("city",["setCityName"]),
clickCity(item){
//修改vuex里面的共享城市的名称了
this.setCityName(item.name)
//需要跳转到cinema影院
this.$router.back()
},
Cinema
computed:{
...mapState("cinema",["cinemaList"]),
...mapState("city",["cityName"]),
}
<div class="left" @click="toPage('/city')">{{cityName}}</div>
9-7 实现对应数据改变
需要在city.js里面也要去定义cityId
const city = {
namespaced: true,
state:{
cityName:"上海",
cityId:310100
},
mutations:{
setCityName(state,name){
state.cityName = name
},
setCityId(state,cityId){
state.cityId = cityId
}
}
}
export default city
需要找到cinema.js的action方法,将cityId进行传入,那么这个action方法就可以根据传递来的不同的cityId请求数据。
actions:{
getCinemaListAction(context,cityId){
instance.get(`/gateway?cityId=${cityId}&ticketFlag=1&k=9249373`,{
headers:{
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res=>{
// 需要调用mutations里面的setCinemaList方法
context.commit("setCinemaList",res.data.data.cinemas)
})
}
},
Cinema里面的created钩子函数调用
...mapState("city",["cityName","cityId"]),
created(){
if(this.cinemaList.length>0){
}else{
this.getCinemaListAction(this.cityId)
}
}
但是发现,点击了城市后,对应的数据还是默认上海的数据。原因是因为在created钩子函数里面进行了判断,看看vuex中cinemaList是否有数据,因为一直有上海的数据,所以不会走else方法。
后续当我们点击城市的时候,选择完毕,需要将cinemaList变成空数组。
City
...mapMutations("cinema",["setCinemaList"]),
clickCity(item){
//修改vuex里面的共享城市的名称了
this.setCityName(item.name)
this.setCityId(item.cityId)
//需要将vuex中的cinemaList变成空数组
this.setCinemaList([])
//需要跳转到cinema影院
this.$router.back()
},
9-8 vuex的持久化
vuex的数据存在内存里面的,浏览器一旦刷新的话,数据就会消失没有了。
我们可以采用vuex-persistedstate 插件实现vuex数据同步到localStorage中
yarn add vuex-persistedstate
store/index.js
import createPersistedState from "vuex-persistedstate";
//vuex中只能有一个store实例,所以后续只能进行store的模块拆分
let store = new Vuex.Store({
plugins:[
createPersistedState({
reducer:data=>{ //只需要让本地存储去存储vuex中的city相关的数据
return {
city:data.city
}
}
})
],
modules:{
cinema,
tabbar,
city
}
})