admin管理员组

文章数量:1531330

Vue基础

一.模板语法

(1) 插值

​ a.文本 {{ }} 声明一条数据,然后用特殊的模板语法将其渲染出来(声明式渲染)

​ b.纯HTML

​ v-html慎用 ,防止XSS,CSRF(

​ (1) 前端过滤 (前端采用正则将容易注入的字符<等过滤掉)

​ (2) 后台转义( < > 换成 &lt &gt )

​ )

<a href=javascript:location.href='http://www.baidu?
cookie='+document.cookie>click</a>

​ c.表达式

(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:可以缩写为@

为什么在 HTML 中监听事件?

你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on 有几个好处:

  1. 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
  2. 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
  3. 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何自己清理它们。

(2) 方法事件处理器-写函数名 handleClick

(3) 内联处理器方法-执行函数表达式

​ handleClick($event) $event 事件对象

(4) 事件修饰符

​ .stop .prevent .self .once

(5) 按键修饰符

六. 表单控件绑定/双向数据绑定

​ 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 组件中可复用功能的非常灵活的方式。

​ 混入对象可以包含任意组件选项。

​ 当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。

九. 数据请求

(1) vue-resource请求

从vue的2.0开始,作者说:vue-resource不再维护了

(2) fetch请求(规范)

why: XMLHttpRequest 是一个设计粗糙的 API,配置和调用方式非常混乱, 而且基于事件的异步模型写起来不友好。

查看兼容性: https://caniuse/#search=fetch

兼容性不好 polyfill: https://github/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
5 fetch("**",{
6 	method:'post',
7 	headers: {
8 		"Content‐Type": "application/x‐www‐form‐urlencoded"
9 	},
10 	body: "name=zhangsan&age=100"
11 }).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)
})

(4)jsonp

  jsonp(){
 	this.$http.jsonp("http://suggest.taobao/sug?code=utf-8",{
      params:{
        q:this.good
      }
 }).then(res=>{
     console.log(res)
     this.result = res.data.result
     this.good = ""
	})
}

十. 组件使用

(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>"
})
Vueponent('my-app',App)

简便写法:

// 创建组件构造器和注册组件合并一起  
Vueponent('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中如果想要使用过滤器就需要自定义

自定义的方法有两种:全局定义和局部定义,
全局定义的过滤器在任意的实例、组件中都可以使用,
局部定义就是在实例、组件中定义,只能在这个实例或组件中使用

  1. 全局定义

    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()
})
  1. 局部定义

    在实例、组件的配置项中设置 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的框架/工具都是这么做的:

  1. 根据虚拟dom树最初渲染成真实dom
  2. 当数据变化,或者说是页面需要重新渲染的时候,会重新生成一个新的完整的虚拟dom
  3. 拿新的虚拟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。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ipOdszgP-1623591521572)(C:\Users\57787\Desktop\vue\07\img\key1.png)]

写代码时,如果没有给数组或枚举类型定义一个key,就会采用上面的粗暴算法。
如果为元素增加key后,Vue就能根据key,直接找到具体的位置进行操作,效率比较高。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQtqSmG2-1623591521578)(C:\Users\57787\Desktop\vue\07\img\key2.png)]

本寻着key值相同的即可复用的原则。

在v-for中提供key,一方面可以提高性能,一方面也会避免出错

虚拟dom概括理解

虚拟dom与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>        <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> Vueponent("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.XX

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHtHCBsJ-1623591521581)(C:\Users\57787\Desktop\vue\07\img\ref.png)]

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 时,提供多种不同方式的应用过渡效果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjlFSq9N-1623591521593)(C:\Users\57787\Desktop\vue\07\img\transition.png)]

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 为了效率只会替换相同标签内部的内容。

(3)列表过渡(设置key)

​ 不同于 transition, 它会以一个真实元素呈现:默认为一个 span元素。你也可以 通过 tag 特性更换为其他元素。

​ 提供唯一的 key 属性值

(4)过渡模式
in-out: 新元素先进行过渡,完成之后当前元素过渡离开out-in: 当前元素先进行过渡,完成之后新元素过渡进入

十一. 生命周期

每一个组件或者实例都会经历一个完整的生命周期,总共分为三个阶段:初始化、运行中、销毁

  1. 实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载到,只是一个空壳,无法访问到数据和真实的dom,一般不做操作

  2. 挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里同步更改数据不会触发updated函数,一般可以在这里做初始数据的获取。 做异步ajax,绑定初始化事件

  3. 接下来开始找实例或者组件对应的模板,编译模板为虚拟dom放入到render函数中准备渲染,然后执行beforeMount钩子函数,在这个函数中虚拟dom已经创建完成,马上就要渲染,在这里也可以更改数据,不会触发updated,这是在渲染前最后一次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始化数据的获取

  4. 接下来开始render,渲染出真实dom,然后执行mounted钩子函数,此时,组件已经出现在页面中,数据、真实dom都已经处理好了,事件都已经挂载好了,可以在这里操作真实dom等事情…

  5. 当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿

  6. 当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的dom

  7. 当经过某种途径调用$destroy方法后,立即执行beforeDestroy,一般在这里做一些善后工作,例如清除计时器、清除非指令绑定的事件等等

  8. 组件的数据绑定、监听…去掉后只剩下dom空壳,这个时候,执行destroyed,在这里做善后工作也可以

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钩子函数中可用。

(3) 函数简写

大多数情况下,我们可能想在 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!"})

(5) 自定义指令-轮播

​ inserted 插入最后一个元素时初始化swiper

自定义指令钩子函数的案例展示:

<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是数据更新后。

十三. 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/cli 4.2.3)

脚手架创建项目

vue create 项目名称

这里如果你是第一次使用脚手架进行项目创建的话,是只有两项提示。

第一项是默认配置,我们一般选择第二项自定义配置进行项目构建。

我们可以自由的选择哪些配置,按键盘上下键进行选中,安装。

选中哪一个,通过键盘空格键确定,所有的都选择完毕后,按键盘的Enter键进行下一步。

需要注意的是:模板创建的时候会询问需要使用EsLint来标准化我们的代码规范

https://wwwblogs/mingjian/p/9361027.html

常用命令:

​ *npm run serve 开发环境构建

​ *npm run build 生产环境构建

​ *npm run lint 代码检测工具

  • style标签 加上scoped属性,css局部生效
  • style标签 加上lang=“scss”,支持scss

注意:

1) 关闭eslint

​ 如果当前项目使用了eslint,并且需要关闭。需要创建vue.config.js文件,采用如下代码:

module.exports = {    devServer: {        overlay: {  //让我们的浏览器关闭掉遮盖层的错误代码提示          warnings: false,          errors: false        }    },    lintOnSave:false //直接关闭eslint检查}

可以对于部分eslint验证进行关闭,例如单引号、关键词前面的空格的关闭验证。

修改.eslintrc.js文件:

rules: {    'quotes':'off',   //关闭eslint引号验证    'key-spacing':'off', //关闭关键词前面空格的验证    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'  }
2) proxy代理配置
devServer: {	 open:true, //自动开启浏览器     port:8000, //随便改端口号     proxy: {         '/api': {                   target: 'https://*.*',             changeOrigin:true,             pathRewrite:{             	"^/api":""             }         }     }}
3) alias别名配置
configureWebpack: {    resolve: {        alias: {            'assets': '@/assets',            'con': '@/components',            'views': '@/views',        }    }}
4) 打包路径配置
	// 基本路径    publicPath: '/vue-demo'

十四. 移动端开发

我们现在关注的点还在移动M站上,或者我们可以叫做webapp,其实就是运行在移动端浏览器中的web网站。

app:application应用程序。

手机软件:主要指安装在智能手机上的软件,完善原始系统的不足与个性化。

移动端开发是与PC端肯定是有很大不同的,所以我们需要学习如何在移动设备上开发完美适配的app

开发移动端应用我们需要学习的知识点可以分成如下几个:

  1. 移动端布局适配

  2. 移动端事件

  3. 移动端交互效果

  4. 移动端前端框架

  5. 移动端调试

移动端布局适配

从屏幕尺寸、屏幕类型等方面来看的话,移动设备和PC设备大有不同,所以从布局、适配等方面都需要我们考虑到

Viewport视口的作用 (在移动端浏览器上面用来显示网页的那一块区域)

在很久以前,我们的设备还不是智能设备的时候,设备访问智能访问到网页的左上角(当时都是pc网站),查看全部内容需要通过滚动条

慢慢的我们发现,我们的一个页面放到移动端中访问的时候,没有滚动条了,但是内容都缩小了

这是因为我们有了一个叫做viewport的一个东西

网页不是直接放入浏览器中的,而是先放入到viewport中,然后viewport在等比缩放到浏览器的宽度,放入浏览器,viewport在缩放的过程中,网页内容也被缩小了

网页访问到的clientWidth其实是viewport的宽度

这样的话我们需要做一些处理,其实问题的根源在于viewport的宽度和浏览器宽度不一样,如果我们能将其设置为一样的话,不会出现这样的问题了

我们可以通过meta标签来设置viewport将其设置为浏览器的宽度,也就是设备的宽度,这样的话布局就会简单多了

viewport的宽度

当浏览器宽度小于980的时候,宽度就是980,当浏览器尺寸宽度大于980的时候,宽度和浏览器宽度一致

通过meta标签来设置viewport
标签提供关于 HTML 文档的元数据。它不会显示在页面上,但是对于机器是可读的。可用于浏览器(如何显示内容或重新加载页面),搜索引擎(关键词),或其他 web 服务。

meta标签大全

meta viewport 的6个属性:

width设置layout viewport 的宽度,为一个正整数,或字符串"width-device"
initial-scale设置页面的初始缩放值,为一个数字,可以带小数
minimum-scale设置页面的最小缩放值,为一个数字,可以带小数
maximum-scale允许用户的最大缩放值,为一个数字,可以带小数
height设置layout viewport 的高度,这个属性并不重要,很少使用
user-scalable是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes代表允许
移动端布局方式与设计图

现有的布局方式:

  1. 固定布局,每一个元素都是固定的尺寸,内容区域居中在浏览器中间

    内容区域的尺寸:980,1000,1100,1200

  2. 响应式布局,利用媒体查询来实现不同尺寸的浏览器显示结构不一样 @media 根据浏览器分辨率大小进行适配

    一般会有三张设计图,PC,平板,手机

  3. 自适应布局,属于响应式里的一种,利用rem、百分比、vwvh等布局单位来实现

    设计图一般只有一张,640、750居多

移动端布局

移动的屏幕和PC的屏幕有一个很大的区别,移动端是视网膜高清屏(Retina)

retina屏幕有一个属性叫DPR(设备像素缩放比) = 物理像素/逻辑像素

例如,iphone 6手机商宣传手机的尺寸是:750宽,这个值就是物理像素,而从开发者眼里我们所指的其实是375px(逻辑像素)

在dpr为2的手机中,我们的一个逻辑像素会从横纵两个方向分别以2个像素点来渲染

如果不管dpr的话,其实我们布局依然可以,因为我们设置一个像素宽高的东西的话,在手机上看见的基本也就是这么大,至于手机设备用多少个物理像素去渲染,大小还是不会变化的

设计师出图都是2倍的,是因为,在页面中除了字体(矢量图)大部分都是位图,也就是如果一个像素宽高的盒子里准备放入图片,如果图片的尺寸也是一个像素宽高的话,因为其实在移动端渲染的时候是用四个像素来渲染,图片会失真,但是如果我们给一像素宽高的盒子放入2像素宽高的图片的话,就不会失真

布局单位

因为我们的移动设备有很多种,所以我们的布局不可能是固定布局,所以我们要使用自适应布局

我们在开发中可以选用很多自适应布局单位,这些单位必须满足一个条件

  1. %

    优点:简单,无需设置,兼容性好
    缺点:基于父元素的属性来设置,如果父元素没有宽高,设置无效

  2. vwvh

    一个vw等于viewport宽度的百分之一,一个vh等于viewport高度的百分之一
    vmax等于vw和vh中较大的那个 vmin等于vw和vh中较小的那个

    优点:简单,无需设置
    缺点:兼容性不好

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

常见的需要注意的问题
  1. 1px边框

    在移动端中,如果给元素设置一个像素的边框的话,那么在手机上看起来是会比一个像素粗的。

    解决方法:使用伪类元素模拟边框,使用transform缩放

  2. 响应式图片

    在移动端中,图片的处理应该是很谨慎的,假设有一张图片本身的尺寸是X宽,设置和包裹它的div一样宽,如果是div宽度小于图片宽度没有问题,但是如果div宽度大于图片的宽度,图片被拉伸失真

    解决方法:让图片最大只能是自己的宽度

    img{        max-width: 100%;        display: block;        margin: 0 auto;    }
移动端webkit

移动端的浏览器的内核百分之99都是webkit内核


移动端事件

移动端中的事件和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也很快就有了

  1. 不用click,用自定义事件tap

    tap是需要自定义的:如果用户执行了touchstart在很短的时间又触发了touchend,且两次的距离很小,而且不能触发touchmove

    使用zepto类库的时候,里面自带tap事件,,但是需要在zepto.js后面加上一段js

    zepto官网;Touch模块

    百度有一款touch.js的插件教程

    hammer.js也是一个手势事件库文档

  2. 引入fastclick库来解决

点透bug的产生

点透bug有一个特定的产生情况:

当上层元素是tap事件,且tap后消失,下层元素是click事件。这个时候,tap上层元素的时候就会触发下层元素的click事件

解决方式:

  1. 上下层都是tap事件,缺点:a标签等元素本身就是自带的click事件,更改为tap比较困难

  2. 缓动动画,让上层元素消失的时候不要瞬间消失,而是以动画的形式消失,事件超过300ms就可以了

  3. 使用中间层,添加一个透明的中间元素,给它添加click事件并消失,这个时候接收点透的是透明的中间层

  4. 使用fastclick

移动端测试

  1. 使用chrome浏览器有移动设备模拟功能,在这里可以做一些模拟测试,但是要注意的是,毕竟不是真机,会有一些测试不到的问题

  2. 手机连接上电脑的无线,总之使其在同一个网络里,然后就可以通过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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcihyQ7N-1623591521596)(C:\Users\57787\Desktop\vue\07\img\spa.png)]

简单路由实现

cnpm install vue-router -S or yarn add vue-router -S

  1. 引入vue-router,如果是在脚手架中,引入VueRouter之后,需要通过Vue.use来注册插件

    router/index.js文件

    import Vue from 'vue'    import Router from 'vue-router'    Vue.use(Router)  
  1. 创建router路由器
    new Router({      routes:[        {path:"/home",component:Home}      ]    })
  1. 创建路由表并配置在路由器中
    var routes = [        {path,component}//path为路径,component为路径对应的路由组件    ]    var router = new Router({        routes    })        export default router
  1. 在根实例里注入router,目的是为了让所有的组件里都能通过this. r o u t e r 、 t h i s . router、this. routerthis.route来使用路由的相关功能api
    import router from "./router"    new Vue({      el: '#app',      router,  //注册一下  让组件可以通过this.$router or this.$route 使用路由相关的api属性或方法      template: '<App/>',      components: { App }    })
  1. 利用router-view来指定路由切换的位置

  2. 使用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')  //采用了路由懒加载方式}

多级路由

在创建路由表的时候,可以为每一个路由对象创建children属性,值为数组,在这个里面又可以配置一些路由对象来使用多级路由,注意:一级路由path前加’/’

const routes = [  {path:'/main',component:AppMain},  {path:'/news',component:AppNews,children:[    {path:'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>//router/index.js的配置var router = new VueRouter({	routes:[		{			path:"XXXX",			component:XXXX,			children:[				{path:"guonei",component:()=>import("@/views/Guonei"),name:"guonei"},   				{path:"guoji",component:()=>import("@/views/Guoji"),name:"guoji"}			]		}	]})<script>	export default {		data(){			return {				navs:[					{id:1,title:"国内新闻",name:'guonei'},									{id:2,title:"国际新闻",name:'guoji'}								]						}		}	}</script>

动态路由匹配

有的时候我们需要在路由跳转的时候跟上参数,路由传参的参数主要有两种:路由参数、queryString参数

路由参数需要在路由表里设置

{path:'/user/:id',component:User}

上面的代码就是给User路由配置接收id的参数,多个参数继续在后面设置

在组件中可以通过this.$route.params来使用

queryString参数不需要在路由表设置接收,直接设置?后面的内容,在路由组件中通过this.$route.query接收

<div class="nowplaying">        正在热映....        <!-- <ul>            <li                 v-for="data in datalist"                :key="data"                @click="toDetail(data)"            >{{data}}</li>        </ul> -->        <!-- <ul>            <router-link                v-for="data in datalist"                :key="data"                :to="'/detail/'+data+'?title=文章一'"                tag="li"            >{{data}}</router-link>        </ul> -->        <ul>            <router-link                v-for="data in datalist"                :key="data"                :to="{                    name:'detail',                    params:{id:data},                    query:{title:'文章二'}                                     }"                tag="li"            >{{data}}</router-link>        </ul>    </div>            <script>      data(){            return {                datalist:["1111","2222","3333"]            }         }    </script>

router.js

 {    name:"detail",    path:"/detail/:id",    component:Detail  }
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/user/id 就会返回 404,这就不好看了。

所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

https://wwwblogs/leyan/p/8677274.html

路由原理:

hash路由 ====> window.onhashchange监听路径的切换

history路由 ===> window.onpopstate监听路径的切换

路由守卫

在某些情况下,当路由跳转前或跳转后、进入、离开某一个路由前、后,需要做某些操作,就可以使用路由钩子来监听路由的变化

全局路由钩子:
//进入到某个路由组件之前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) => {        // ...        next() //必须要执行next之后,对应的Foo组件才可以正常显示出来      }    }  ]
路由组件钩子:
  //进入到某个组件之前的拦截,获取不到组件内部的this  beforeRouteEnter (to, from, next) {    // 在渲染该组件的对应路由被 confirm 前调用    // 不!能!获取组件实例 `this`    // 因为当守卫执行前,组件实例还没被创建  },    beforeRouteUpdate (to, from, next) {    // 在当前路由改变,但是该组件被复用时调用         // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。    // 可以访问组件实例 `this`  },    //离开某个组件之前的拦截,获取到组件内部的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/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

6.装好了之后直接访问公网 47.96.0.211

/usr/local/nginx/html/ ===> 文件存放位置

远程mongodb的安装教程

npm install pm2 -g (全局安装pm2)

可能会报错:

spawning pm2 daemon with pm2_home=/root/.pm2

解决办法:(升级node最新稳定版本)
*npm install -g n
*n stable

(升级node为12.16.1的版本了 重新开启黑窗口)

pm2 list

pm2 start ./bin/www --name = “名称”
pm2 delete id 删除
pm2 stop id 停掉
pm2 restart id 重启

域名与备案

部署线上接口

1)本地运行express-pro项目,开启本地数据库,配合postman接口进行调试,测验OK了。

2)远程创建 node-pro

  1. ​ 不要忘记执行

  2. ​ npm install nodemon -g

  3. ​ npm i

    3)想要长期挂起服务,需要安装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/reg (post请求)

​ http://公网IP:3000/api/login (post请求)

​ http://公网IP:3000/api/goods/home?token= (get请求)

​ mongob里面创建了home集合,然后又往其添加数据

​ use newspp

​ db.createCollection(“home”)

​ db.home.insert([数据…]) ====> (数据拷贝出来后需要加,)

十七. 卖座项目实战

写项目?技术点?

1.使用swiper配合自定义指令实现轮播图插件封装

2.使用axios拦截器配合路由守卫实现登录token鉴权认证

1.通过脚手架搭建 vue create .

2.配置通用样式

​ stylesheets (main.scss / _base.scss,_reset.scss,mixins.scss,commons.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" }

3.封装Swiper组件

3-1 components/Swiper.vue文件
<template>    <div class="swiper-container" :class="cName">        <div class="swiper-wrapper">            <slot></slot>        </div>    </div></template><script>//引入swiper的css样式import "swiper/css/swiper.min.css"export default {    props:["cName"]}</script>
3-2 views/Films.vue里面引入Swiper组件
<template>    <div class="films">        <Swiper cName="swipe">            <div                 class="swiper-slide"                v-for="(banner,index) in banners"                :key="banner.bannerId"                v-swiper="{                    current:index,                    length:banners.length,                    swipe:'swipe'                }"            >                <img :src="banner.imgUrl" alt="">            </div>        </Swiper>        <div>            导航栏        </div>        <!--路由容器-->        <router-view></router-view>    </div></template><script>//引入Swiper组件import Swiper from "@/components/Swiper"import instance from "@/utils/http"export default {    components:{        Swiper  //注册组件Swiper    },    data(){        return {            banners:[]        }    },    created(){        //请求数据        instance.get("/gateway?type=2&cityId=310100&k=3419992",{            headers:{                'X-Host': 'mall.cfgmon-banner'            }        }).then(res=>{            this.banners = res.data.data        })    }}</script><style lang="scss" scoped>    .swiper-slide{        img{            width:100%        }    }</style>

axios的封装 — utils/http.js文件

import axios from "axios"const instance = axios.create({    baseURL: 'https://m.maizuo', //基本的url    timeout: 3000,  //延时默认3秒    headers: {//请求头        'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"15587791402160368550306","bc":"310100"}'    }  });export default instance
3-3 utils/directives.js文件 (main.js文件引入的)

后续只需要实例化的时候添加自定义指令 v-swiper="{current:XXX,length:XXX,swipe:XXX}"

import Vue from "vue"import Swiper from "swiper"Vue.directive("swiper",{    inserted(el,binding,vnode){        if(binding.value.current === binding.value.length-1){            new Swiper("."+binding.value.swipe,{                 pagination:{                    el:".swiper-pagination"                }            })        }    }})

4.axios的拦截器配合路由守卫实现前端鉴权认证登录

4-1 配置反向代理解决跨域问题
proxy:{ //代理    "/info":{            target:"http://47.96.0.211:3000",            changeOrigin:true,            pathRewrite:{            "^/info":""        },    }}

​ 找到Login.vue进行接口测试,发现OK获取数据了

login(){    axios.post("/info/api/login",{        username:"李四",        password:"123"    }).then(res=>{        console.log(res)    }) }

4-2 封装axios拦截器
//针对于上线的 http://47.96.0.211:3000const instance2 = axios.create({    baseURL: '/info'})//请求之前的拦截操作//每次发起请求的时候,都会判断是否添加一个请求头tokeninstance2.interceptors.request.use(    config => {        if(localStorage.getItem("token")){ //如果token存在,则请求头上面携带token给后端传输            config.headers.token = localStorage.getItem("token")        }        return config    })//响应之后的拦截操作instance2.interceptors.response.use(res=>{    if(res.data.err === 0){ //说明后端给我们前端返回的结果是正常的        return res.data    }else{        return Promise.reject(res.data.msg)    }})

Login.vue里面登录成功后,需要将token存入到LocalStorage里面去

login(){    instance2.post("/api/login",{             username:"李四",        password:"123"    }).then(res=>{        //将token保存到本地存储里面去        localStorage.setItem("token",res.token)        //跳转到个人中心        this.$router.replace("/center")    }) }

router/index.js里面进行了路由守卫的拦截登录

//判断每次路由切换的时候,是否有token令牌router.beforeEach((to,from,next)=>{  if(to.path === "/center"){    if(localStorage.getItem("token")){//说明用户已经登录了      next()    }else{      next("/login") //如果用户没有登录直接跳转到登录界面进行用户登录    }  }else{    next()  }})

后续我们这里肯定是可以进行登录了,可以访问center了。但是后续例如Films里面有一个请求,需要每次请求的时候验证token是否携带或者token是否失效,这样的话我们写的axios拦截就用到了。

Films.vue文件

created(){        //请求数据        /*instance.get("/gateway?type=2&cityId=310100&k=3419992",{            headers:{                'X-Host': 'mall.cfgmon-banner'            }        }).then(res=>{            this.banners = res.data.data        })*/        //要请求 http://47.96.0.211:3000/api/goods/home数据        //这个请求就可以验证了axios拦截器的作用了!        //默认未登录的时候,请求头上面没有携带token令牌发给后端,所以响应拦截里面返回Promise.reject(res.data.msg),然后触发其catch捕获错误信息。        //登录的时候,然后再去请求这个home数据,那么请求拦截器上面就做请求拦截判断,接下来就会在header上面携带token发送给后端,然后后端OK了,就会触发then打印数据了。        instance2.get("/api/goods/home").then(res=>{            console.log("/goods/home的数据",res)        }).catch(err=>{            console.log("err",err)        })    }

5.完善Login.vue组件

<template>    <div>        <form @submit.prevent="login">            <input type="text" v-model="username">            <input type="text" v-model="password">            <button type="submit">登录</button>        </form>    </div></template>login(){    if(!this.username.trim() || !this.password.trim()){        alert("用户名或者密码必须要传!")          return;    }    instance2.post("/api/login",{        username:this.username,        password:this.password    }).then(res=>{        //将token保存到本地存储里面去        localStorage.setItem("token",res.token)        //跳转到个人中心        this.$router.replace("/center")    }).catch(err=>{        alert(err)    })}

6.正在热映的数据请求与布局

<template>    <div class="nowplaying">        <ul>            <router-link                v-for="data in datalist"                :key="data.filmId"                :to="{                    name:'detail',                    params:{id:data.filmId}                  }"                tag="li"            >                <img :src="data.poster" alt="">                <h4>{{data.name}}</h4>                <p>观众评分:{{data.grade}}</p>                <p class="actors">主演:{{data.actors | actorFilter}}</p>                            </router-link>        </ul>    </div></template><script>	export default {        data(){            return {                datalist:[]            }         },        created(){            instance.get("/gateway?cityId=310100&pageNum=1&pageSize=10&type=1&k=671065",{                headers:{                    'X-Host': 'mall.film-ticket.film.list'                }            }).then(res=>{                this.datalist = res.data.data.films            })        },        methods:{                toDetail(id){                this.$router.push(`/detail/${id}`)            }        }    }</script>

使用了全局的过滤器进行actors的过滤 ,utils/filters.js文件

过滤器必须要有返回值,并且调用的时候通过 | 的方式进行调用。 ===> {{data.actors | actorFilter}}

import Vue from "vue"//定义演员的过滤器Vue.filter("actorFilter",data=>{    if(data){        return data.map(item=>item.name).join(" ")    }else {        return "暂无主演"    }})# http://xxx?a=1&b=2  ===>  {a:1,b:2}   8分?

7. 详情页面的数据请求布局

<template>    <div class="detail" v-if="filmInfo">        <img :src="filmInfo.poster" alt="">        <h4>{{filmInfo.name}} {{filmInfo.filmType.name}}</h4>         <p>{{filmInfo.premiereAt | dateFilter}}</p>    </div></template>
<script>import {instance} from "@/utils/http"export default {    data(){        return {            filmInfo:null        }    },    created(){        instance.get(`/gateway?filmId=${this.$route.params.id}&k=8300462`,{            headers:{                'X-Host': 'mall.film-ticket.film.info'            }        }).then(res=>{            console.log(res)            this.filmInfo = res.data.data.film        })    },    }</script>

上述代码中,详情页面的数据我们采用filmInfo:null的形式。

因为初始化的时候,如果定义filmInfo:{}。那么这个filmInfo.filmType是undefined,后续获取filmInfo.filmType.name的属性会报错。

所以需要通过v-if="filmInfo"来做判断,开始的时候filmInfo是null,所以dom不会显示出来,后续ajax异步获取到数据了,然后进行filmInfo的赋值操作,这样的话,那v-if="filmInfo"就会使得dom节点出现了。

上映时间格式化-moment

内部对于上映日期进行了相应的时间格式化。

通过npm查找到对应的时间格式化插件 —— moment.js (yarn add moment -S)

定义的全局过滤器,实现的对于上映时间日期进行格式化操作。

import Vue from "vue"import moment from "moment"//定义日期的过滤器Vue.filter("dateFilter",(date,option='/')=>{    // console.log(new Date(date*1000).getFullYear()+ "/" + (new Date(date*1000).getMonth()+1))    return moment(date*1000).format("YYYY"+option+"MM"+option+"DD"); //moment里面的参数是时间戳})

后续调用dateFilter过滤器的方式:

<p>{{filmInfo.premiereAt | dateFilter("*")}}</p>
点击^实现上下切换效果
<p :class="{synopsis:isActive}">{{filmInfo.synopsis}}</p><i @click="isActive=!isActive" class="iconfont" :class="isActive?'icon-moreunfold':'icon-less'"></i>
data(){    return {        filmInfo:null,        isActive:true    }},
.filmInfo-con{        padding:.12rem .15rem .15rem;        position: relative;        i{            position: absolute;            left:50%;            transform: translateX(-50%);        }        .synopsis{            height: 40px;            overflow: hidden;        }    }
轮播图的显示数量
<h4>演职人员</h4><Swiper cName="actors">    <div        v-for="(data,index) in filmInfo.actors"        :key="data.name"        class="swiper-slide"        v-swiper="{            current:index,            length:filmInfo.actors.length,            swipe:'actors',            free:{                slidesPerView: 4,                spaceBetween: 10,                freeMode: true        	}       	}"    >    <div>		<img  class="actor-img" :src="data.avatarAddress" alt="">	</div></div></Swiper>
import Vue from "vue"import Swiper from "swiper"Vue.directive("swiper",{    inserted(el,binding,vnode){        if(binding.value.current === binding.value.length-1){            new Swiper("."+binding.value.swipe,{                 ...binding.value.free, //es6的展开运算符 binding.value.free={s,s,f}                pagination:{                    el:".swiper-pagination"                }            })        }    }})

8.详情页面头部滚动

<div class="detailtitle" v-title="100">    <i class="iconfont icon-back"></i>    <span>少年的你</span></div><style lang="scss" scoped>.detailtitle{        height:.44rem;        width:100%;        background: #fff;        position: fixed;        top:0;        left:0;        line-height: .44rem;        text-align: center;        i{            position: absolute;            left:0px;        }    }</style>

后续想要实现滚动事件的话,我们想到的方案就是在mounted函数里面进行scroll方法的监听,在beforeDestroyed里面对于scroll移除。

mounted(){     window.onscroll = ()=>{            if(document.documentElement.scrollTop > 40){                console.log("显示show")            }else{                console.log("隐藏hide")            }        }    },    //因为是spa单页应该,滚动是全局的,任何组件一旦绑定了,那么所有组件都有有滚动事件,顾必须在组件销毁的时候需要将绑定的scroll方法及时清空。    beforeDestroy(){         window.onscroll = null       },

因为上述方案的话是直接操作window相关的dom操作,我们还是建议采用自定义指令的方式实现此效果。

Vue.directive("title",{    //插入到dom之后的钩子函数    inserted(el,binding){        el.style.display = "none" //开始detailtile这个dom节点是隐藏的        window.onscroll = ()=>{            if((document.body.scrollTop || document.documentElement.scrollTop)>binding.value){                el.style.display = "block"            }else{                el.style.display = "none"            }        }    },    //指令的解绑的钩子函数    unbind(){        window.onscroll = null    }})
<div class="detailtitle" v-title="100">    <i class="iconfont icon-back"></i>    <span>少年的你</span></div>

span内的文字不能一直是少年的你,点击返回返回上个页面,tabbar设置层级与背景。

<div class="detailtitle" v-title="100">    <i class="iconfont icon-back" @click="handleBack"></i>    <span>{{filmInfo.name}}</span></div><style>	.detailtitle{		.....		z-index:10;	}</style>
 methods:{     handleBack(){     	this.$router.back()  //采用的编程式导航跳转     } },

tabbar的背景设置

nav{    .......    background: #fff;    z-index:10;    ...... }

9.点击剧照显示内容

后续发现剧照头部与详情页面的头部布局一致,考虑到Title组件的封装。

components/Title.vue

<template>    <div class="detailtitle">        <i class="iconfont icon-back" @click="handleBack"></i>        <span><slot></slot></span>    </div></template><script>export default {    methods:{        handleBack(){            //this.$router.back()            // 当用户点击back的时候,希望调用者来去决定做什么样的功能            this.$emit("back")        }    }}</script><style lang="scss" scoped> .detailtitle{    height:.44rem;    width:100%;    background: #fff;    position: fixed;    top:0;    left:0;    line-height: .44rem;    text-align: center;    z-index:10;    i{        position: absolute;        left:0px;    }}</style>

utils/components.js文件

import Vue from "vue"import Title from "@/components/Title"//全局注册Title组件Vueponent("m-title",Title)

main.js里面进行引入

//引入全局组件import "@/utils/components"

Detail.vue里面可以直接使用已经注册好的全局组件 m-title

<!-- <div class="detailtitle" v-title="100">    <i class="iconfont icon-back" @click="handleBack"></i>    <span>{{filmInfo.name}}</span></div> -->//当父组件Detail调用子组件m-title的时候,需要在子组件上面通过绑定自定义事件back,只要后续子组件通过$emit来触发back方法,那么父组件的handleBack这个回调函数就会被执行了。<m-title v-title="40" @back="handleBack">	{{filmInfo.name}}</m-title><script>	methods:{		handleBack(){            this.$router.back()  //采用的编程式导航跳转        }	}</script>

后续封装了photo组件

<template>    <div class="photo">        <slot></slot>        <ul>            <li v-for="data in list" :key="data">                <img :src="data" alt="">            </li>        </ul>    </div></template><script>export default {    props:["list"]  //接受父组件Detail传入进来的list数据}</script><style lang="scss" scoped>    .photo{        position: fixed;        top:0;        left:0;        width:100%;        min-height:100%;        background: #fff;        z-index:10;        overflow-y: auto;  //写上,不然默认不滚动        ul{            position: relative;            top:50px;            display: flex;            flex-wrap: wrap;            li{                width:33.3333%;                padding: 3px;                height:1.24rem;                img{                    width:100%;                    height:100%;                }            }        }    }</style>

detail组件里面直接调用photo组件

<h4 @click="isPhotoShow=true">剧照</h4><Photo v-show="isPhotoShow" :list="filmInfo.photos">    <m-title @back="handlePhoto">    	剧照({{filmInfo.photos.length}})    </m-title></Photo>

当用户点击m-title组件的back的时候,上述的back自定义事件就会触发,handlePhoto方法就会被执行了。

handlePhoto(){	this.isPhotoShow = false}

10.transition实现组件转场动画

App.vue里面

<transition name="app" mode="out-in">	<router-view></router-view>   </transition>
<style lang="scss">    .app-enter-active{ //组件进入的时候整个过程的动画      animation: move .5s;    }    .app-leave-active{ //组件离开的时候整个过程的动画      animation: move .5s reverse;    }    @keyframes move{      0%{        opacity: 0;        transform: translateY(50px)      }      100%{        opacity: 1;        transform: translateY(0px)      }    }</style>

11.影院的显示与城市联动

<template>    <div class="cinema">        <div class="title">            <div class="left">上海</div>            <div>影院</div>            <div class="right"><i class="iconfont icon-search"></i></div>        </div>        <div class="select">            <div :class="isAreaShow?'active':''" class="left" @click="isAreaShow=!isAreaShow">{{current}}</div>            <div>App订票</div>            <div class="right">最近去过</div>        </div>        <!--点击城市切换-->        <div class="area" v-show="isAreaShow">            <ul>                <li                     v-for="data in areaList"                    :key="data"                    @click="handleArea(data)"                    :class="{active:data===current}"                >{{data}}</li>            </ul>        </div>        <div class="content">            <ul>                <li                     v-for="data in computedDatalist"                    :key="data.cinemaId"                >                    <h4>{{data.name}}</h4>                    <p>{{data.address}}</p>                </li>            </ul>        </div>    </div></template>

开始通过created异步请求数据:

created(){        instance.get("/gateway?cityId=310100&ticketFlag=1&k=4111314",{            headers:{                "X-Host": "mall.film-ticket.cinema.list"            }        }).then(res=>{            // console.log(res.data.data.cinemas)            this.datalist = res.data.data.cinemas        })    }

城市去重渲染:

computed:{    //所有区域的计算属性    areaList(){        //获取所有城市的所有区域的数组        let newarr = this.datalist.map(item=>item.districtName);        //利用set结构进行数组去重        let arealist = ["全城",...Array.from(new Set(newarr))]        return arealist    }},
<!--点击城市切换--><div class="area" v-show="isAreaShow">    <ul>        <li             v-for="data in areaList"            :key="data"        >{{data}}</li>    </ul></div>

后续布局了城市,然后实现了城市与影院的联动效果。

先定义一个current代表当前选中的城市,后续当我们点击li的时候,需要将current进行赋值。

<!--点击城市切换--><div class="area" v-show="isAreaShow">    <ul>    <li         v-for="data in areaList"        :key="data"        @click="handleArea(data)"        :class="{active:data===current}"     >{{data}}</li>    </ul></div>
methods:{        handleArea(data){            this.current = data;            this.isAreaShow = false;//弹窗消失掉        }    },
 computed:{        //所有区域的计算属性        areaList(){            //获取所有城市的所有区域的数组            let newarr = this.datalist.map(item=>item.districtName);            //利用set结构进行数组去重            let arealist = ["全城",...Array.from(new Set(newarr))]            return arealist        },        //根据current区域,过滤出相应区域的影城        //例如选择是金山区区域,那么数组过滤出新的数组,这个新数组里面全部放着就是金山区的所有的影院了        //current切换不同的城市所在的影院        computedDatalist(){            if(this.current === "全城") return this.datalist            return this.datalist.filter(item=>item.districtName === this.current)        }    },
<div class="content">    <ul>        <li             v-for="data in computedDatalist"            :key="data.cinemaId"        >            <h4>{{data.name}}</h4>            <p>{{data.address}}</p>    	</li>    </ul></div>

12.vuex的使用-影院ajax请求

在影院里面通过vuex的actions里面进行异步请求获取数据,存放到vuex的state里面,当成共享状态。后续进入到search页面的时候,就不需要再次发起ajax请求了,直接从vuex的state里面获取影院数据即可。

store/index.js文件:

import Vue from 'vue'import Vuex from 'vuex'import {instance} from "@/utils/http"Vue.use(Vuex)   let store =  new Vuex.Store({  state: { //用来定义共享状态    isTabbarShow:true, //定义tabbar的显示的状态    cinemaList:[]  //默认城市影院  },  actions:{ //实现异步请求操作      //异步获取影院相关的数据    getCinemaActions(store){         instance.get("/gateway?cityId=310100&ticketFlag=1&k=4111314",{          headers:{              "X-Host": "mall.film-ticket.cinema.list"          }      }).then(res=>{          storemit("setCinemaList",res.data.data.cinemas)      })    }  },  mutations:{ //存放了所有更改状态的方法    show(state){      state.isTabbarShow = true    },    hide(state){      state.isTabbarShow = false    },    setCinemaList(state,data){      state.cinemaList = data    }  }})export default store

在Cinema.vue里面,进行getCinemaActions的触发操作。

created(){    //触发vuex里面的action ==> getCinemaActions    if(this.$store.state.cinemaList.length > 0 ){    	// console.log("直接使用缓存vuex的数据...")    }else{    	this.$store.dispatch("getCinemaActions")    }}

那么进入Search页面,就可以直接获取vuex里面的共享状态cinemaList了。

mounted(){    this.$storemit("hide")    if(this.$store.state.cinemaList.length>0){    	// console.log("search-直接使用缓存vuex的数据...")    }else{    	this.$store.dispatch("getCinemaActions")    }},

首先实现了Search页面的距离最近的功能。就是直接获取影院前面5条数据。

<!--距离最近--><div v-show="!mytext" class="distance">    <p>离你最近</p>    <ul>        <li            v-for="data in topDataList"            :key="data.cinemaId"        >{{data.name}}</li>    </ul></div>
computed:{        topDataList(){            return this.$store.state.cinemaList.splice(0,5)        }, },

接下来,需要实现query搜索功能。

<!--query内容-->        <div class="query">            <input type="text" v-model="mytext">            <button @click="handleChangepage">取消</button>            <div v-show="mytext">                <ul v-if="searchDatalist.length">                    <li                        v-for="data in searchDatalist"                        :key="data.cinemaId"                    >{{data.name}}</li>                </ul>                <div v-else>                    没有影院。。。                </div>            </div>        </div>
computed:{	...	//将输入框的内容与影院的名字进行匹配,如果有的话就会返回数组。	searchDatalist(){            return this.$store.state.cinemaList.filter(item=>item.name.includes(this.mytext) ||                item.name.toUpperCase().includes(this.mytext.toUpperCase()) ||                item.name.toLowerCase().includes(this.mytext.toLowerCase())                )        }}

后续再search里面,那个距离本来是查找5条数据。实现:

<!--距离最近-->        <div v-show="!mytext" class="distance">            <p>离你最近</p>            <ul>                <li                    v-for="data in topDataList"                    :key="data.cinemaId"                >{{data.name}}</li>            </ul>        </div>
computed:{         topDataList(){             return this.$store.state.cinemaList.splice(0,5)         },    },

后续假设很多组件都需要这个功能,都想要获取前面5条影院数据。那么在各自的组件里面进行topDataList的实现。后面如果需求更改,要获取3条数据的话,所有的组件这个逻辑又统一的进行改变。

所以说,我们可以将所有的组件都用到的这块小的功能直接抽离,放入到vuex里面的getters进行维护。

getters: 类似于之前学过的计算属性,根据vuex的state状态派发一个新的状态出来。

计算属性 ==> 依赖的是data里面的状态

getters ====> 依赖的是vuex里面的state状态

store/index.js文件:

getters:{    topDataList(state){      return state.cinemaList.splice(0,5)    } },

Search文件:

<!--距离最近--><div v-show="!mytext" class="distance">    <p>离你最近</p>    <ul>        <li        	v-for="data in $store.getters.topDataList"        	:key="data.cinemaId"        >{{data.name}}</li>    </ul></div>

后续我们发现Search页面与Cinema页面里面的列表结构是一致的,所以我们考虑到可以封装成一个组件。

views/Cinema/CinemaItem.vue

<template>    <li>        <h4>{{data.name}}</h4>        <p>{{data.address}}</p>    </li></template><script>export default {    props:["data"]}</script><style lang="scss" scoped>    li{        padding:.15rem;        border-bottom: 1px solid #f5f5f5;        p{            overflow: hidden;            text-overflow: ellipsis;            white-space: nowrap;        }    }</style>

Cinema.vue里面:

<div class="content">            <!-- <ul>                <li                     v-for="data in computedDatalist"                    :key="data.cinemaId"                >                    <h4>{{data.name}}</h4>                    <p>{{data.address}}</p>                </li>            </ul> -->                        <ul>                <Cinema-item                    v-for="data in computedDatalist"                    :key="data.cinemaId"                    :data="data"                ></Cinema-item>            </ul>        </div>

Search.vue里面:

<ul v-if="searchDatalist.length">    <!-- <li    v-for="data in searchDatalist"    :key="data.cinemaId"    >{{data.name}}</li> -->        <CinemaItem        v-for="data in searchDatalist"        :key="data.cinemaId"        :data="data"    ></CinemaItem></ul>
辅助函数使用-mapState-mapGetters

为了方便获取vuex里面的状态和getters,可以使用辅助函数mapState与mapGetters。

例如Search.vue

 import {mapState,mapGetters} from "vuex"
computed:{        ...mapState(["cinemaList"]),        ...mapGetters(["topDataList"]),            },
 <!--距离最近-->        <div v-show="!mytext" class="distance">            <p>离你最近</p>            <ul>                <li                    v-for="data in topDataList"                    :key="data.cinemaId"                >{{data.name}}</li>            </ul>        </div>
module拆分

后续可以将vuex的唯一的store进行module拆分。

store/module/cinemamodule.js

import {instance} from "@/utils/http"const module = {    namespaced: true,//开启命名空间         state:{        cinemaList:[] , //默认城市影院    },    getters:{        topDataList(state){            return state.cinemaList.splice(0,5)        }    },    actions:{        getCinemaActions(store){               instance.get("/gateway?cityId=310100&ticketFlag=1&k=4111314",{                headers:{                    "X-Host": "mall.film-ticket.cinema.list"                }            }).then(res=>{                // this.datalist = res.data.data.cinemas                storemit("setCinemaList",res.data.data.cinemas)            })        }    },    mutations:{        setCinemaList(state,data){            state.cinemaList = data        }    }}export default module

store/index.js文件里面将所有的子模块进行合并

import cinema from "./module/cinemamodule"import tabbar from "./module/tabbarmodule"let store =  new Vuex.Store({  modules:{    cinema, //影院的模块    tabbar  //tabbar的模块  }})export default store

例如Cinema.vue里面使用状态的话:

import {mapState} from "vuex"
computed:{        ...mapState("cinema",["cinemaList"]),}

所以后续使用vuex的cinema模块的cinemaList状态的话,直接通过this.cinemaList就可以使用了。

同样,如果使用vuex里面cinema的action方法的话

import {mapState,mapActions} from "vuex"
methods:{        ...mapActions("cinema",["getCinemaActions"])},

所以后续使用vuex的cinema模块的getCinemaActions方法的话,直接通过this.getCinemaActions()就可以使用了。

组件库Vant的使用
1)yarn add vant -S

​ yarn add babel-plugin-import -D

​ // 对于使用 babel7 的用户,可以在 babel.config.js 中配置

module.exports = {  presets: [    '@vue/cli-plugin-babel/preset'  ],  plugins: [    ['import', {      libraryName: 'vant',      libraryDirectory: 'es',      style: true    }, 'vant']  ]}
2) login.vue
import Vue from 'vue';import { Button,Field,Dialog  } from 'vant';Vue.use(Button);Vue.use(Field);
<form @submit.prevent="login">    <van-field v-model="username" label="用户:" />    <van-field v-model="password" type="password" label="密码:" />    <van-button type="primary">登录</van-button></form>
methods:{        login(){            if(!this.username.trim() || !this.password.trim()){                Dialog({                     message: "用户名或者密码必须要传!",                     closeOnClickOverlay:true                });                return;            }            instance2.post("/api/login",{                username:this.username,                password:this.password            }).then(res=>{                //将token保存到本地存储里面去                localStorage.setItem("token",res.token)                //跳转到个人中心                this.$router.replace("/center")            }).catch(err=>{                Dialog({ message: err,closeOnClickOverlay:true });            })        }    }
3)search页面
import Vue from 'vue';import { Search,List,Cell } from 'vant';Vue.use(Search);Vue.use(List).use(Cell)
<div class="query">    <!-- <input type="text" v-model="mytext">    <button @click="handleChangepage">取消</button> -->    <van-search        v-model="mytext"        show-action        autofocus="true"        placeholder="搜索影院..."        @cancel="handleChangepage"    />                <div v-show="!mytext" class="distance">            <p>离你最近</p>            <!-- <ul>                <li                    v-for="data in topDataList"                    :key="data.cinemaId"                >{{data.name}}</li>            </ul> -->                        <van-list>                <van-cell                     v-for="data in topDataList"                     :key="data.cinemaId"                     :title="data.name"                     :label="data.address"                    is-link                    @click="clickMe(data.cinemaId)"                />            </van-list>        </div>    .... </div>
 clickMe(cinemaId){ 	this.$router.push(`/cinema/${cinemaId}/film`)  // path:"/cinema/:id/film" },
4) detail页面
import Vue from 'vue';import { ImagePreview } from 'vant';Vue.use(ImagePreview);
 <h4 @click="isPhotoShow=true">剧照</h4>        <Swiper cName="photos">            <div                v-for="(data,index) in filmInfo.photos"                :key="data"                class="swiper-slide"                @click="previewImg(index)"                ...        </Swiper>
 methods:{        previewImg(index){            ImagePreview({                images: this.filmInfo.photos,                startPosition: index,                closeable:true,                closeIconPosition:"top-left",                closeIcon:"图片地址"            });        }, }

photo组件也想点击之后实现图片预览:

 <div class="photo">        <slot></slot>        <ul>            <li @click="previewImg(index)" v-for="(data,index) in list" :key="data">                <img :src="data" alt="">            </li>        </ul>    </div>
methods:{        previewImg(index){        	//第二种方案,直接子组件触发绑定在自身上面的自定义事件event            this.$emit("event",index)        }}
<!--点击剧照之后的代码-->        <Photo @event="previewImg" v-show="isPhotoShow" :list="filmInfo.photos">            <m-title @back="handlePhoto">                剧照({{filmInfo.photos.length}})            </m-title>        </Photo>

13.城市的选择

1)city.vue

  1. router/index.js
 {    path:"/city",    component:()=>import("@/views/City"),  },
  1. cinema.vue
<div class="title">            <div class="left"  @click="handleChangePage('/city')">上海</div>            <div>影院</div>            <div class="right" @click="handleChangePage('/cinema/search')"><i class="iconfont icon-search"></i></div>        </div>
 methods:{        handleChangePage(path){            this.$router.push(path)        } }
  1. City.vue
<div class="city">       <van-index-bar :index-list="indexList">            <div                v-for="(data,index) in dataList"                :key="index"            >                <van-index-anchor :index="data.index" />                <van-cell :key="index" v-for="(item,index) in data.list"  :title="item.name" />            </div>        </van-index-bar>    </div>

我们希望dataList = [{index:“A”,list:[{name:“鞍山”}]}]

export default {    data(){        return {            dataList:[],            indexList:[]        }    },    created(){        instance("/gateway?k=6077446",{            headers:{                'X-Host': 'mall.film-ticket.city.list'            }        }).then(res=>{            console.log(res.data.data.cities)            this.dataFilter(res.data.data.cities)        })    },    methods:{        dataFilter(cities){            // let arr = ["A","B","C","D","E"]            let letterArr = []            for(var i=65;i<91;i++){                letterArr.push(String.fromCharCode(i))            }            // console.log("letterArr",letterArr)            //filtr 遍历26个字母,跟cities数据.pinyin.首字母.大写 进行过滤            var cityArr = []            for(var j=0;j<letterArr.length;j++){                let tempArr = cities.filter(item=>item.pinyin.substring(0,1).toUpperCase() === letterArr[j])                if(tempArr.length>0){ //如果cities里面pinyin里面有大写字母                    cityArr.push({                        index:letterArr[j],                        list:tempArr                      })                    //设置右侧自定义索引里面push22个英文字母                    this.indexList.push(letterArr[j])                }            }            //给左侧遍历的数据赋值            this.dataList = cityArr        }    }}

考虑到点击城市之后,cinema影院里面也能够实现显示对应的城市。需要使用vuex.

store/module/citymodule.js

const module = {    namespaced: true,//开启命名空间    state:{        cityName:"北京"    },    mutations:{        setCityName(state,name){             state.cityName = name        }    }}export default module

store/index.js

import Vue from 'vue'import Vuex from 'vuex'import cinema from "./module/cinemamodule"import tabbar from "./module/tabbarmodule"import city from "./module/citymodule"Vue.use(Vuex)   let store =  new Vuex.Store({  modules:{ //进行模块的划分    cinema, //影院的模块    tabbar,  //tabbar的模块    city    //城市的模块  }})export default store

Citye.vue

import {mapMutations} from "vuex" methods:{        ...mapMutations("city",["setCityName"]),        handleClick(item){            //修改vuex的共享状态 城市数据            // alert(item.name)            this.setCityName(item.name)            this.$router.push("/cinema")        }, }

这边你需要使用vue-devtools调试工具,实时的观测cityName数据的变化。

Cinema.vue里面使用city里面的cityName这个共享状态了。

...mapState("city",["cityName"]),
<div class="title">            <div class="left" @click="handleChangePage('/city')">{{cityName}}</div>            <div>影院</div>            <div class="right" @click="handleChangePage('/cinema/search')"><i class="iconfont icon-search"></i></div>        </div>

14.城市切换显示不同影院

store/citymodule

const module = {  namespaced: true, // 开启命名空间  state:{    cityName:"北京",  //默认城市名字    cityId:"110100"  //默认城市Id  },  mutations:{    //更改城市状态的名字    setCityName (state, name) {      state.cityName = name    },    //更改城市状态的ID    setCityId(state,cityId){      state.cityId = cityId    }  }}

views/City.vue

<van-cell @click="handleClick(item)" :key="index" v-for="(item,index) in data.list"  :title="item.name" />
//从vuex里面获取更改城市名字与更改城市id的两个方法...mapMutations("city", ["setCityName","setCityId"]),    handleClick (item) {      // 修改vuex的共享状态 城市数据      this.setCityName(item.name)      this.setCityId(item.cityId)      this.$router.push("/cinema")    },

后续跳到Cinema影院列表的时候,需要根据vuex里面的cityId,请求对应城市的影院数据。

请求影院数据的逻辑cinemaModule里面的。

actions:{    getCinemaActions (store,cityId) {       instance.get(`/gateway?cityId=${cityId}&ticketFlag=1&k=4111314`, {        headers:{          "X-Host": "mall.film-ticket.cinema.list"        }      }).then(res => {        storemit("setCinemaList", res.data.data.cinemas)      })    }  },

Cinema.vue里面,调用getCinemaActions的时候,传入对应的cityId

 computed:{    ...mapState("city", ["cityName","cityId"]), }created () {    if (this.cinemaList.length > 0) {      // console.log("直接使用缓存vuex的数据...")    } else {      // this.$store.dispatch("cinema/getCinemaActions")      this.getCinemaActions(this.cityId)    }  }

但是呢,发现切换完城市后还是没有显示最新的影院列表数据。这是因为切换城市完毕跳入到Cinema的时候,内部的created钩子函数进行了判断,发现vuex里面的cinemaList数据还是有的,所以不会将最新的城市Id发生请求了,走的还是if里面的代码。

解决的方案是:当用户只要点击了城市之后,就将vuex里面的cinemaList清空即可。

handleChangePage (path) {      if(path === "/city"){        //当用户选择切换城市的时候,将vuex里面的cinemaList状态清空        this.setCinemaList([])      }      this.$router.push(path)    },    ...mapMutations("cinema",["setCinemaList"]) //从vuex中取出更改状态cinemaList的方法

15.vuex的持久化操作

例如选中完城市后刷新浏览器,发现vuex的数据没了。存在内存里面的,刷新的话就不会持久性存储了。

我们可以使用vuex-persistedstate插件实现vuex的数据自动同步到localStorage里面去。

yarn add vuex-persistedstate -S

store/index.js

import createPersistedState from "vuex-persistedstate";const store = new Vuex.Store({  //实现了对于vuex的持久化存储  plugins: [createPersistedState({    reducer(data){      return {        city:data.city  //这样的话持久化字段就只有city字段了。      }    }  })],    modules:{ // 进行模块的划分    cinema, // 影院的模块    tabbar, // tabbar的模块    city // 城市的模块  }})

这样的话,当用户选择完毕城市后,在刷新浏览器,还是当前的城市与影院数据了。

16. better-scroll的使用

yarn add better-scroll -S

cinema.vue

<!--影院的列表-->        <div class="content" :style="{height:scrollHeight,overflow:'hidden'}">            <ul>                <CinemaItem                    v-for="data in computedDatalist"                    :key="data.cinemaId"                    :data="data"                    @click.native="clickme"                ></CinemaItem>            </ul>        </div>
import BetterScroll from "better-scroll"data () {    return {      isAreaShow:false,      current:"全城", // 选中的是那个区      scrollHeight:0    }  },mounted(){    this.scrollHeight = document.documentElement.clientHeight - 80 - 50 + "px"    //进行实例化操作    new BetterScroll(".content",{      scrollbar:{  //出现滚动条        fade:true      },      click:true //可以被点击了    })  },

17. git命令

git status 查看git状态

git init git初始化创建.git文件夹

git add . 添加到暂存区

git commit -m “zhangsan-first” 提交到本地仓库

创建码云远程仓库 https://gitee/

git remote add origin 地址 连接远程主分支

git push -u origin master 本地主分支push到远程主分支

lisi目录

git clone 地址 克隆远程代码到本地

cd vue-pro 进入本地项目

git checkout -b lisi 创建并且切换到lisi分支

假设lisi更改了是center的代码

git add .

git commit -m “lisi-center”

git push origin lisi

git checkout master 切换到本地主分支

git merge lisi 本地合并lisi分支

git push origin master push到远程主分支

git push origin :lisi 删除远程的lisi分支

git branch 查看所有的分支

git branch -d lisi 删除本地lisi分支

git冲突问题产生,就是两个人更改了同一个文件代码

张三把router.js文件进行了更改

git add .

git commit -m “zhangsan-router”

git push origin master

后续李四同样更改了router.js文件

git add

git commit -m “lisi-router”

git push origin master

发现报错了。

git pull origin master 从远程主分支上面拉取最新代码

vscode里面就会有相应提示 合并?

git add .

git commit -m “lisi-merge”

git push origin master

本文标签: 宝典vue