一、vue介绍
简单了解vue
vue是一个前端渐进式框架。
Vue.js是一套构建用户界面的框架,只关注视图层,它不仅易于上手,还便于与第三方库或既有项目进行整合。
前端工作主要负责MVC中的V这一层,主要与用户的界面打交道。
在Vue中,一个核心的概念,就是让程序员不再操作DOM元素,解放了程序员的双手,让程序员可以有更多的时间去关注业务逻辑。
最新vue版本已经到3.x。3.x版本向下支持,支持2.x版本的语法。课程主要学习2.x,后期再讲解3.x版本新增的语法功能。Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架, Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。渐进式:随着功能的增多,能满足不同阶段的需求
vue框架
框架和库的区别
- 框架: 是一套完整的技术解决方案;对项目的侵入性比较大,项目如果需要更换框架,则需要重新构架整个项目。
- 库(插件): 提供某一个小功能,对项目的侵入小较小,如果某个库无法完成某些需求,可以很容易切换到其他库实现需求
Vue框架的特点
-
模板渲染:基于 html 的模板语法,学习成本低。
-
响应式的更新机制:数据改变之后,视图会自动刷新。【重要】
-
渐进式框架
-
组件化/模块化
-
轻量:开启 gzip压缩后,可以达到 20kb 大小。(React 达到 35kb,AngularJS 达到60kb)。
jQuery 和vue对比
jQuery:选择器( ) 选 取 D O M 对 象 , 对 其 进 行 赋 值 、 取 值 、 事 件 绑 定 等 操 作 。 数 据 和 界 面 是 在 一 起 的 。 比 如 需 要 获 取 l a b e l 标 签 的 内 容 : )选取DOM对象,对其进行赋值、取值、事件绑定等操作。数据和界面是在一起的。比如需要获取label标签的内容: )选取DOM对象,对其进行赋值、取值、事件绑定等操作。数据和界面是在一起的。比如需要获取label标签的内容:(“lable”).val();它还是依赖DOM元素的值
Vue:则是通过Vue对象将数据和View完全分离开来了。对数据进行操作不再需要引用相应的DOM对象,可以说数据和View是分离的,他们通过Vue对象这个vm实现相互的绑定。这就是传说中的MVVM
Vue环境搭建
引用 Vue.js 文件
1、方式一:(CDN的方式进行引用)
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
2、方式二:(下载 vue.js 文件)
去网站 https://cdn.jsdelivr.net/npm/vue/ 下载 vue.js 文件,直接放到工程文件里,然后引用。
<script src="lib/vue.js"></script>
3、方式三:(NPM的方式安装vue)
# 最新稳定版
$ npm install vue
如果网络不稳定,可以采用下面的方式安装:
$ cnpm i vue --save
然后在代码中通过下面这种方式进行引用:
import Vue from 'vue'
常见的插件
-
Webpack:代码模块化构建打包工具。
-
Gulp:基于流的自动化构建工具。
-
Babel:使用最新的 规范来编写 js。
-
Vue:构建数据驱动的Web界面的渐进式框架
-
Express:基于 Node.js 平台,快速、开放、极简的 Web 开发框架。
以上这些包,都可以通过 NPM 这个包管理工具来安装。
二、原理介绍
vue 是一套用于构建用户界面的渐进式框架。它的核心库只关注视图,采用的mvvm设计模式。vue的中心思想就是:数据驱动视图
mvvm
“MVVM”: model view viewmodelMVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。
可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1vxHJbc-1623771327465)(C:\Users\xujing\AppData\Roaming\Typora\typora-user-images\image-20210327110959874.png)]
Vue是一个 MVVM框架,其各层的对应关系如下
-
Model:负责数据存储 ; Model层在Vue中是data、computed、methods等中的数据
-
View:负责页面展示 ; View层在Vue中是绑定dom对象的HTML
-
View Model:负责业务逻辑处理(比如Ajax请求等),对数据进行加工后交给视图展示 ; View Model层在Vue中是实例的vm对
在 Model 层的数据变化时,View层会在ViewModel的作用下,实现自动更新
-
面试:对于 MVVM 的理解?
MVVM 是 Model-View-ViewModel 的缩写。
Model 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑。
View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View。
在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 View 和Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
数据绑定原理
vue拦截原理:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的属性(property),并使用Object.defineProperty
把这些属性全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知watcher,从而使它关联的组件重新渲染。
每个组件实例都对应一个 watcher 实例,它会在组件初始化的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知watcher,从而使它关联的组件重新渲染。
// new Vue({})这也是一个组件,被称为根组件
// 1. 当组件渲染的时候,vue会使用Object.defineProperty方法将data选项下的myname属性全部转为 getter/setter
// 2. 当myname属性值发生变化的时候就会通知当前组件对应的watcher,从而使它关联的组件重新渲染。初始的时候给赋值了'kervin',所以也会通知对应的watcher。
var vm = new Vue({
el:"#app1", // element
data:{
myname:"kerwin"
},// 状态
methods:{
},
watch:{
}
})
//js实现双向绑定
var obox = document.getElementById("box")
Object.defineProperty(obj,"myname",{
get(){
console.log('被访问了')
return obox.innerHTML
},
set(value){
console.log("修改",value)
obox.innerHTML = value
}
})
//Object.defineProperty(参数1,参数2,参数3) 返回值为该对象obj
//参数1为该对象(obj),参数2为要定义或修改的对象的属性名,参数3为属性描述符,属性描述符是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合使用。而get和set属于存取描述符对象的属性。
//这个方法会直接在一个对象上定义一个新属性或者修改对象上的现有属性,并返回该对象
通过console.log(vm)就能观察到vm实例下自动创建了set/get 方法。如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44Nno7T8-1623771327468)(C:\Users\xujing\AppData\Roaming\Typora\typora-user-images\image-20210327114043497.png)]
Object.defineProperty的缺点。
1、无法监听es6的Set、Map 变化;
2、无法监听Class类型的数据;
3、属性的新加或者删除也无法监听;
4、数组元素的增加和删除也无法监听 (解决:Vue.set)
针对Object.defineProperty的缺点,vue3 下的 ES6 Proxy都能够完美得解决,它唯一的缺点就是,对IE不友好, 所以vue3在检测到如果是使用IE的情况下(没错,IE11都不支持Proxy),会自动降级为Object.defineProperty的数据监听系统。
- 面试:Vue 实现数据双向绑定的原理
vue 实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 **Object.defineProperty()**来劫持各个属性的 setter/getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
vue 的数据双向绑定 将 MVVM 作为数据绑定的入口,整合 Observer,Compile 和Watcher 三者,通过 Observer 来监听自己的 model 的数据变化,通过 Compile 来解析编译模板指令(vue 中是用来解析 {{}}),最终利用 watcher 搭起 observer 和Compile 之间的通信桥梁,达到数据变化 ==>视图更新;视图交互变化(input) ==> 数据 model 变更双向绑定效果。
虚拟dom
- 面试题:
1. 虚拟dom 的底层原理
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的DOM 树上,视图就更新了。
例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。
其实虚拟DOM在Vue.js主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图
2. 为什么使用key?
Key是用来跟踪每个节点的身份,从而重用和重新排序现有元素;理想的 key 值是每项都有的且唯一的 id
- 需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。作用主要是为了高效的更新虚拟DOM。
- 当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key 的作用主要是为了高效的更新虚拟 DOM
diff算法
diff算法三大原则(对象对比算法):
- 同层级对比
- 同标签, 组件 对比
- 同key值对比
- 面试:
diff算法是什么?怎么用? 虚拟dom怎么优化性能?
新老虚拟dom创建完之后,新的虚拟dom会跟老的虚拟dom去对比,虚拟dom本质上是一个对象,实质两个对象在对比,vue中内置的一套diff算法,它能保证最优的对比效率。diff算法有以下几个原则,第一:把树按照层级分解进行对比,同层级对比,不同层级不会对比,防止无效的比较。同层级如果有多个可以按照key对比 ,如果没key可以对比标签/组件 ,对比结果一样,复用,对比出不同,就删除重建。
vue底层更新原理:
vue2 中obejct.defineproperty 通过getter/setter拦截;vue3中通过propx方案拦截,拦截到数据发生改变时候,通过watcher通知所有跟节点相关的dom 和相关的组件进行渲染,在渲染的时候,会创建一份新的虚拟dom节点,虚拟dom本质上是对象,对比老的虚拟dom ,对比的过程中,vue内置了一套diff算法,diff算法会保证最优的效率去对比 ,最小的代价去更新dom。diff算法,会按照树的结果层级相同才进行对比,层级不同就不对比;如果是在列表中,是通过key来进行对比,key一样就复用,key不一样就删了重建;还有标签、组件进行对比,在新老dom中对比发现标签或者组件名字不一样,会直接删了重新创建
生命周期
vue生命周期是指vue实例对象从创建之初到销毁的过程,vue所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数实现组件数据管理和DOM渲染两大重要功能。
vue生命周期分为八个阶段:
beforeCreate(创建前) 在数据观测和初始化事件还未开始,此时的数据和数据观察和事件机制都不由形成,所以不能操作
created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
<div id="box">
<div id="myname">{{myname}}</div>
<input type="text" v-model="mytext"/>
<!-- 记住用户名 -->
<button @click="handleLogin">登录</button>
<!-- <child v-if="isCreated"></child> -->
</div>
//创建根组件(每个组件都有8个生命周期,如果需要,自己调用)
var vm = new Vue({
el: "#box",
data:{
mytext:"",
myname:"kerwin"
},
methods:{
handleLogin(){
//localStorage.setItem("username",this.mytext)
this.myname="xiaoming" //状态立即改变,但是dom是异步更新
}
},
beforeCreate(){//实例被完全创建出来前执行
//在该周期,data和method都还未被初始化
console.log("beforeCreate",this.mytext)
},
created(){ //初始化状态或者挂载到当前实例的一些属性
//此时,data和method都已初始化
//也可以发ajax(因为是异步) 但更习惯于在mounted发ajax
console.log("created","初始化工作",this.mytext)
this.mytext = this.mytext + 1111 //被拦截的状态
this.mytext = localStorage.getItem("username") //this下面的属性
},
beforeMount(){//模板解析之前最后一次修改模板节点
//模板已编译于内存中,但未被页面渲染
//即{{}} 还未有效
//dom挂载之前的,无法访问dom,自己在服务端渲染,这个函数会在客户端和服务端各自触发一次
},
mounted(){ //dom挂载完成了拿到真正的dom节点
//当执行该函数时,实例已被完全创建好了。若没有后续操作,则一直放置于内存中
// 依赖于dom创建之后, 才进行初始化工作的插件 (轮播插件)
// 订阅 bus.$on
// 发ajax
// setInterval()
// window.onscoll
},
beforeUpdate(){
//数据发生改变时,this.data已发生变化,但html的value未改变
//更新之前,记录老的dom某些状态,比如滚动条记录位置
},
updated(){
//数据的变化,页面也已渲染完成
//更新完成,获取更新后的dom,依赖与dom操作的库(例如swiper),需要知道状态更新完,什么时候dom更新
},
beforeDestroy(){
//此时,实例的相关内容仍然有效
//清除定时器,事件解绑...
},
destroyed(){
//实例已被销毁,相关内容已无效
//清除定时器,事件解绑...
}
})
- 面试:
- 什么是 vue 生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载 Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
- vue 生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个 Vue 实例的过程时更容易形成好的逻辑。
- vue 生命周期总共有几个阶段?
答:它可以总共分为 8 个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。
- 第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个 beforeCreate, created, beforeMount, mounted 。
- DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。
三、vue实例
当创建一个 Vue 实例时,你可以传入一个选项对象,这些选项对象后面一一介绍,这里只做了解
常见选项对象有:
-
(选项/数据)data , props , propsData , computed , methods,watch
-
(选项/DOM)el , template , render , renderError
-
(选项/生命周期钩子)beforeCreate,created, beforeUpdate,updated, beforeMount,mounted, activated,deactivated, destoryed,beforeDestoryed, errorCaptured
-
(选项/资源)**directives,filters,components
-
(选项/组合)parent,mixins,extends,provide/inject
-
(选项/其他)name,delimiters,functional,model,inheritAttrs,comments
var vm = new Vue({
// 选项
})
-
el用于提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例
-
data 用于定义属性,实例中有三个属性分别为:site、url、alexa。
-
methods 用于定义的函数,可以通过 return 来返回函数值。
-
{{ }} 用于输出对象属性和函数返回值。
当一个 Vue 实例被创建时,它向 Vue 的响应式系统中加入了其 data 对象中能找到的所有的属性。当这些属性的值发生改变时,html 视图将也会产生相应的变化
<div id="app1">
{{ 10 + 20 }}
<!-- {{}}里面是js的地盘,支持js的语法规则。只要这些代码片段在js里被new Vue过就可以。一旦js里有new Vue() ,相应的html里部分代码就会被vue所解析渲染。 -->
{{ 10>20 ? 'aaaa':'bbb' }}
{{ myname }}
<button @click="handleMyClick()">change</button>
</div>
<script>
//使用vue创建 vm对象
var vm = new Vue({
el:"#app1", // element
data:{
myname:"kerwin"
},// 状态
methods:{
handleMyClick(){
console.log("click")
this.myname = "xiaoming"
}
}
})
// el: 定义vue 实例作用区域
//data : 定义变量的地方 (实例所用到的数据)
</script>
绑定事件:html里写法用@,@后面跟的事件名字,如@click,@mouseover等等。js里定义事件,放到methods属性下。
<div id="box">
<button @click="handleClick()">点击</button>
</div>
<script>
new Vue({
el: '#app',
data: { //所有的状态
message: 'Hello Vue!',
myname:'kerein'
},
methods:{ //事件处理函数
handleClick:function(){
this.myname="xiaoming" //this ->指向vue创建的实例对象this.myname=vm.myname
console.log('click')
}
}
})
</script>
四、vue 模板语法
插值
{{…}} 文本插值:
{{}} 是最基本的文本插值方法,里面可以填写变量,js表达式等等
<div id="app1">
{{ myname }}
</div>
<script>
var vm = new Vue({
el:"#app1", // element
data:{
myname:"kerwin" //状态
}
})
</script>
v-html 输出 html 代码
注意:使用v-html渲染数据可能会非常危险,因为它很容易导致 XSS(跨站脚本) 攻击,使用的时候请谨慎,能够使用{{}}或者v-text实现的不要使用v-html
只在可信内容上使用 v-html
,永不用在用户提交的内容上。
<!-- v-html指令 负责dom解析 -->
<div v-html="myhtml"></div>
{{ 默认不解析 }}
注:下面这种情况需要注意,不能使用v-html
new Vue({
el: "#box",
data: {
dangerhtml: `<a href=javascript:location.href='http://www.baidu.com?cookie='+document.cookie>男人看了沉默,,,,</a>`
}
})
因为使用v-html后会进行dom解析。所以应该使用{{ dangerhtml }}的方式
<a href=javascript:location.href='http://www.baidu.com?cookie='+document.cookie>男人看了沉默,,,,</a>
表达式
<div id="app1"> <!-- {{}}这里支持js表示式语法-->
{{ 10 + 20 }}
{{ ok ? 'YES' :'NO'}}
</div>
指令
v-bind 动态绑定属性
HTML 属性中的值应使用 v-bind 指令。
写法:v-bind : 属性名=”{‘属性值’:vue绑定的数据属性名}” 或者 v-bind:属性名=”{vue绑定的数据属性名}”
缩写:v-bind:属性名 => :属性名
例:v-bind:src =>:src
<!-- src ,style, class, value ,title, type..... -->
<img v-bind:src="imgpath" />
var vm= new Vue({
el:"#box",
data:{
imgpath:"https://static.maizuo.com/pc/v5/usr/movie/c3a32bd973128a58d969836ea8e0120e.jpg"
}
})
v-if 动态创建/删除
<!-- v-if 指令 动态创建和删除 -->
<!--if条件渲染,达到条件创建出来,没有达到条件删除 -->
<div v-if="isCreated">我是动态创建和删除</div>
var vm= new Vue({
el:"#box",
data:{
isCreated:true //当isCreated值为true的时候,页面对应的元素创建,isCreated值为false的时候,页面对应的元素删除
}
})
v-show 动态显示/隐藏
<!-- v-show指令, 不会删除元素,元素会被设置成display:none-->
<div v-show="isShow">我是动态显示和隐藏</div>
var vm= new Vue({
el:"#box",
data:{
isShow:true //当isShow值为true的时候,页面对应的元素显示,isShow值为false的时候,页面对应的元素隐藏
}
})
v-on: 绑定事件
<!-- 添加绑定事件 写法:v-on:事件名字="方法名字" -->
<!-- 事件支持原生js下所有的事件,比如:click,input,change,blur,fouse 等等 -->
<button v-on:click="handleClick()">隐藏div</button>
var vm= new Vue({
el:"#box",
data:{
isShow:true
},
methods:{ //事件定义写在methods选项下面,这样页面才可以进行引用
handleClick(){
this.isShow = false;
}
}
})
v-for 遍历
<!-- v-for指令 for循环 -->
<!-- v-for加在标签A上,数组每循环一次,页面就会增加一个A标签元素 -->
<ul>
<li>11111111</li>
<li v-for="(item,index) in list">
{{item}}------{{index}}
</li>
<li>22222222</li>
</ul>
var vm= new Vue({
el:"#box",
data:{
list:["aaa","bbb","ccc","ddd","eee","fff"]
}
})
v-model 双向绑定表单
<!-- v-model 双向表单绑定指令,用于表单元素 -->
<input type="text" v-model="mytext"/>
<!--双向绑定:输入框的值改变会直接赋值到mytext的变量 ,同样mytext的值也会给到输入框的值 -->
var vm= new Vue({
el:"#box",
data:{
mytext:'wuhongtao'
}
})
补充下:用v-model 指令进行绑定数据,checkbox返回的checkbox标签上value属性对应的值,没有value属性默认返回true/false;text类型返回的是输入框里的值;radio是radio标签上value属性对应的值;select对应选中的那个option标签上的value属性值。
v-bind和v-model区别
- v-bind是数据绑定,没有双向绑定效果,但不一定在表单元素上使用,任何有效元素上都可以使用;
- v-model是双向绑定,基本上只用在表单元素上;
- 当v-bind和v-model同时用在一个元素上时,它们各自的作用没变,但v-model优先级更高,而且需区分这个元素是单个的还是一组出现的。
缩写
<!-- v-on指令支持缩写 -->
<button v-on:click="handleClick()">click-完整</button>
<button @click="handleClick()">click-简写</button>
<!-- v-bind指令支持缩写 -->
<img v-bind:src="imgpath" />完整语法
<img :src="imgpath" />缩写
todolist案例(待办事项/留言板)
<div id="box">
<!-- v-model 表单绑定指令 双向表单绑定指令,输入框有任何改变,就直接把输入框的value值赋值给mytext-->
<input type="text" v-model="mytext">
<button @click="handleAdd()">add</button>
<ul>
<li v-for="(item , index) in datalist">
{{item}}----------{{index}}
<button @click="handleDelClick(index)">x</button>
</li>
</ul>
<div v-show="datalist.length === 0">暂无待办事项</div>
</div>
var vm= 0;
new Vue({
el : "#box", //el固定的
data :{
mytext : "",
datalist:["1111","2222","3333"]
}, //data固定的
methods:{ //methods固定的
handleAdd(){
console.log(this.mytext);
this.datalist.push(this.mytext);
this.mytext = "";//清空value
},
handleDelClick(index){
console.log("del" , index);
this.datalist.splice(index , 1);
//把索引传过来,删除的位置就是索引的位置(第几个li) , 第二个参数 :就是删除的个数
}
}
})
样式绑定
Class绑定
-
对象语法
<style> .aa{background-color: red;} .bb{background-color: red;} .cc{background-color: red;} <style> <div id="box"> <div :class="classstr">动态切换class -变量写法 - 1 </div> <div :class="classobj">动态切换class -对象写法 - 2 </div> <div :class="classarr">动态切换class -数组写法 - 3 </div> </div>
var vm = new Vue({ el:"#box", data:{ classstr:"aa", classobj:{ aa:true,//只要是true的都会加到class中 bb:false, cc:true }, classarr:["aa","bb"], } }) vm.classobj.dd = true // 不好用,因为没有被拦截 Vue.set(vm.classobj,"dd",true) //动态拦截某个属性 vm.classarr.push("dd") // 好用,是因为数组被改变了,并没有被动态拦截
data下的属性 后期增加或删除的属性都无法监听到
例 :vm.classobj.dd = true // 不好用,因为没有被拦截
vue2解决方案:利用 Vue.set(对象,属性,true) 动态拦截某个属性
Vue.set(vm.classobj,"dd",true)
页面渲染后,代码会变成
<div class="aa">动态切换class -变量写法 - 1 </div> <div class="aa cc dd">动态切换class -对象写法 - 2 </div> <div class="aa bb dd">动态切换class -对象写法 - 3 </div> <!--注:由于设置的多个样式,样式可能会重叠。后面会覆盖前面的样式,即cc覆盖aa的样式,跟css里相同。-->
-
数组语法
<style> .aa{background-color: red;} .bb{background-color: red;} <style> <div :class="classarr">动态切换class -数组写法 - 3 </div>
var vm= new Vue({ el:"#box", data:{ classarr:["aa","bb"]//数组中的样式都会加到class中 } })
vm.classarr.push("dd") // 好用,因为数组被改变了,并不是vue2可以拦截
如何切换样式?可采取三元运算符的方式。(三元运算只能两个来回切换,不能切换多个,如果是多个的话还得用数组或对象)
样例:
<div v-bind:class="[errorClass ,isActive ? activeClass : '']"></div> <!--errorClass 是始终存在的,isActive 为 true 时添加 activeClass 类-->
绑定内联样式
-
对象语法
<div :style="styleobj">动态切换style-对象写法-1</div>
var vm= new Vue({ el:"#box", data:{ styleobj:{ backgroundColor:"yellow", fontSize:"20px" } } })
-
数组语法
<div :style="stylearr">动态切换style-数组写法-2</div>
var vm= new Vue({ el:"#box", data:{ stylearr:[ {backgroundColor:"yellow"} ] } })
页面渲染后,代码会变成
<div style="color:red, fontSize :18px"></div> <div style=" backgroundColor:yellow, fontSize :20px">动态切换style-对象写法-1</div> <div style=" backgroundColor:yellow">动态切换style-数组写法-2</div>
-
数据选项data下定义对象注意事项
由于data下定义的所有数据都会被vue拦截,并形成set,get方法。所以当data下定义一个obj,需要注意:当后面对这个对象添加新的属性的时候,不可以用vm.styleobj.dd=true;这种方式。因为这种方式设置obj不会被拦截。应该采用==Vue.set(vm.styleobj,“dd”,true)==这种方式。
<div id="box"> <div :style="styleobj">动态切换style-对象写法</div> <button @click="changeColor">点击变红</button> </div> <script> var vm = new Vue({ el:"#box", data:{ styleobj:{ fontSize:"20px" } }, methods:{ changeColor(){ // this.styleobj.backgroundColor = "red";//不好用 Vue.set(this.styleobj,"backgroundColor","red");//好用 } } }) </script>
vue3
//vue2初始化,面向对象的思想,new 一个构造函数 /* new Vue({ el:"#box", data:{myname:"kerwin"} }) */ //vue3中 vue不再是构造函数,不能使用new Vue var obj = { data(){ //vue3 状态函数式,data是独立的空间,只负责自己维护的数据,不会造成混乱 return {} } }, methods:{} } var vm = Vue.createApp(obj).mount("#box")
总结:vue3初始化做了两个改变。
- Vue.createApp(obj).mount("#box")
- data必须是一个函数,不再是对象
条件语句
v-if
条件判断使用 v-if 指令, 在元素 和 template 中使用 v-if 指令。
<div v-if="isCreated">我是动态创建和删除</div>
v-else 、v-else-if
<div v-if=" score>85 "> 优秀 </div>
<div v-else-if=" score>=60 && score<85 "> 及格 </div>
<div v-else> 不及格 </div>
new Vue({
el:"#box",
data:{
isCreated:true,
score:50
}
})
template
template 就是一个包装元素, 不会真正创建在页面上。
<div id="box">
<!-- 包装元素template不会被创建 -->
<template v-if="isCreated">
<div >111111</div>
</template>
</div>
var vm = new Vue({
el:"#box",
data:{
isCreated:true
}
})
v-show
使用 v-show 指令根据条件展示元素: v-show 的元素会始终渲染并保持在 DOM 中。v-show 相当于给元素设置 CSS 属性 display,v-show 不支持 用法
<div v-show="isShow">我是动态显示和隐藏</div>
循环语句
v-for迭代数组
v-for 指令需要以 site in sites / site of sites 形式的特殊语法, sites 是源数据数组, site 是数组元素迭代的别名。v-for 可以绑定数据到数组来渲染一个列表。这里有一点需要注意:v-for循环取数据的同时,需要指定:key。
<div id="box">
<ul>
<li v-for="(data,index) of datalist" :key="index">
<!--key: 跟踪每个节点的身份,从而重用和重新排序现有元素 -->
<!-- 理想的 key 值是每项都有的且唯一的 id。data.id -->
{{data}}--{{index}}
</li>
</ul>
<ul>
<li v-for="(item,index) in 10" :key="index">
{{item}}
</li>
</ul>
</div>
<script>
new Vue({
el:"#box",
data:{
datalist:["1111","222","3333"]
}
})
</script>
例:模糊查询(过滤应用)
<div id="box">
<input type="text" v-model="mytext" @input="handlechange()">
<ul>
<li v-for="data in datalist" :key="data">
{{data}}
</li>
</ul>
</div>
<script>
var vm = new Vue({
el:"#box",
data:{
mytext:"",
datalist:["aaa","add","bbb","bbc","ccc","ddd","eee","ade"],
oldlist:["aaa","add","bbb","bbc","ccc","ddd","eee","ade"]
},
methods:{
handlechange(){
this.datalist = this.oldlist.filter( item => item.includes(this.mytext) )
//item 数组里的每一项
//(item)=>{return item.includes(this.mytext)}
//filter过滤数组,把符合条件的过滤出来
//includes 包含 查看字符是否存在,存在返回true
//找一个旧的数组进行过滤,过滤完再赋给新的数组,这样不会对原数组产生影响
}
}
})
</script>
输入框事件:
@change : 失去焦点,并value改变, 才会触发
@input : 每次value改变
@blur: 失去焦点触发
@keyup.enter : pc端点击回车键触发,手机端点击确定键触发
v-for迭代对象
<div id="app">
<ul>
<li v-for=" (value, key) in object">
{{ key }} : {{ value }}
<!--value:JavaScript中文网、http://www.javascriptcn.com、学的不仅是技术,更是梦想! -->
<!--key: name、url、slogan-->
</li>
</ul>
</div>
<script>
new Vue({
el: '#app',
data: {
object: {
name: 'JavaScript中文网',
url: 'http://www.javascriptcn.com',
slogan: '学的不仅是技术,更是梦想!'
}
}
})
</script>
当然上面v-for里也可以加三个参数,如下所示。index为索引
<li v-for="(value, key, index) in object">
{{ index }}. {{ key }} : {{ value }}
</li>
v-for 迭代整数
<li v-for="n in 10">
{{ n }}
<!-- 遍历1到10 -->
</li>
数组检测变动
- 使用以下方法操作数组,可以检测变动 push() 、pop() 、shift() 、unshift() 、splice() 、sort() 、reverse()
- filter(), concat() 和 slice() ,map(),新数组替换旧数组
- 通过数组索引下标方式改变数组内元素的值,并不能触发数组检测变动,例如:vm.items[indexOfItem] = newValue
解决办法: Vue.set(example1.items, indexOfItem, newValue) ;或者采取数组的splice方法 vm.items.splice(indexOfItem, indexOfItem, newValue);
<ul>
<li v-for="(item,index) of datalist" :key="item">
{{item}}--{{index}}
</li>
</ul>
<script>
var vm = new Vue({
el:"#box",
data:{
datalist:["1111","2222","3333"],
}
})
vm.datalist[0]="kerwin" //×
//对于改索引值,vue拦截不到,vue2解决方案:
vm.datalist.splice(0,1,"kerwin") //√
Vue.set(vm.datalist, 0, "kerwin") //√
//vue3 都可以拦截,就不需要上面两个方法
</script>
函数表达式
vue函数表达式可以在{{}}里调用,也可以在属性里调用
<div id="box">
{{sum()}}
<div v-for="data in mydatalist()">
{{mydatalist()}}
</div>
</div>
<script>
var vm = new Vue({
el:"#box",
data:{
datalist:["1111","22222","3333"]
},
methods:{
sum(){
console.log("sum");
},
mydatalist(){
return ["a","b","c"]
}
}
})
</script>
案例:模糊查询(函数版)
<div id="box">
<input type="text" v-model="mytext">
<ul>
<li v-for="item in test()" :key="item">
<!--注意test要加小括号,加小括号才是函数调用,函数返回的是一个过滤后的数组-->
{{item}}
</li>
</ul>
</div>
<script>
var vm = new Vue({
el:"#box",
data:{
mytext:"",
datalist:["aaa","add","bbb","bbc","ccc","ddd","eee","ade"]
},
methods:{
test(){
return this.datalist.filter(item => item.includes(this.mytext))
//this.datalist = ["aaa","add","bbb","bbc","ccc","ddd","eee","ade"]
//filter:过滤datalist数组,把满足条件的过滤出来返回一个新数组,不会改变原数组
//(item)=>{return item.includes(this.mytext)}
//item数组里的每一项
//includes 包含 查看字符是否存在,存在返回true
}
}
})
</script>
事件处理
事件绑定三种方式
- 监听事件-直接触发代码
<button @click="count++">add3表达式</button> <!-- 引号里也可以写表达式。只支持简单的表达式 ++、--、!-->
- 方法事件处理器-写函数名 handleAdd2
<button @click="handleAdd2">add2函数名</button> <!--不传参可以直接写函数名调用-->
<!--不加小括号默认得到的事件对象-->
- 内联处理器方法-执行函数表达式 handlechange( e v e n t ) ∗ ∗ event) ** event)∗∗event 事件对象**
<button @click="handleAdd1(‘abc’,20)">add1函数表达式</button><!--handleAdd1方法带参数-->
<input type="text" @input="handlechange($event)"/>
<!-- 没有参数可以传$event 固定写法, 传其他会被当成变量,如果没有定义变量会报错 -->
后期可以通过 evt.target.value 拿到input输入框的值
事件修饰符
Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节,Vue.js通过由点(.)表示的指令后缀来调用修饰符。
常用事件修饰符如下:
.stop
阻止冒泡。本质是调用 event.stopPropagation()。.prevent
阻止默认事件(默认行为)。本质是调用 event.preventDefault()。.self
只有当事件在该元素本身(比如不是子元素)触发时,才会触发回调。.once
事件只触发一次。.capture
添加事件监听器时,使用捕获的方式(也就是说,事件采用捕获的方式,而不是采用冒泡的方式)。.{keyCode | keyAlias}
只当事件是从侦听器绑定的元素本身触发时,才触发回调。.native
监听组件根元素的原生事件。
<!-- 阻止单击事件冒泡 -->
<a @click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 添加事件侦听器时使用事件捕获模式 -->
<div @click.capture="doThis">...</div>
<!-- 只当事件在该元素本身(而不是子元素)触发时触发回调 -->
<div @click.self="doThat">...</div>
<!-- click 事件只能点击一次,2.1.4版本新增 -->
<a @click.once="doThis"></a>
<!-- 2.3版本新增, passive会告诉浏览器你不想阻止事件的默认行为-->
<a @scroll.passive="doThis"></a>
修饰符可以串联
<a v-on:click.stop.prevent="doThat"></a>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。
用 @click.prevent.self 会阻止所有的点击,而 @click.self.prevent 只会阻止对元素自身的点击。
不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。.passive一般用在滚动监听,@scroll,@touchmove
案例:登录弹框
#overlay{ background:rgba(0,0,0,.6); width:100%; position:fixed; top:0;left:0;right:0;bottom:0; }
#center { background: #ffff; border-radius: 5px;padding-top: 15px; padding-left: 30px; padding-bottom: 15px;width: 290px;height: 160px; position: fixed;margin: auto;left: 0; right: 0;top: 0; bottom: 0; }
<div id="box">
<!-- 1.刚开始不显示,点击登录显示遮罩层overlay -->
<!-- 2.再点击overlay,取消显示 isShow=false -->
<button @click="isShow=true">登录</button>
<div id="overlay" v-show="isShow" @click.self="isShow=false" >
<div id="center">
<div>用户名:<input type="text"></div>
<div>密码:<input type="password"></div>
<div><button>登录</button></div>
</div>
</div>
</div>
<script>
new Vue({
el:"#box",
data:{
isShow:false
}
})
</script>
按键修饰符
Vue 允许为 v-on 在监听键盘事件时添加按键修饰符, 记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名。
全部的按键别名:
.enter,.tab,.delete,.esc,.space,.up,.down,.left,.right,.ctrl,.alt,.shift,
.meta
<input type="text" @keyup.enter="handleKeyup"/>
<input type="text" @keyup.13="handleKeyup"/>
组合键
<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>
表单控件绑定
采取v-model指定实现双向数据绑定
输入框
<input v-model="message" placeholder="编辑我……">
<p>消息是: {{ message }}</p>
<script>
new Vue({
el: '#app',
data: {
message: 'JavaScript'
}
})
</script>
复选框
复选框如果是一个为逻辑值,如果是多个则绑定到同一个数组
单选按钮
<div>
<input type="radio" v-model="select" value="man">男
<input type="radio" v-model="select" value="woman">女
</div>
<script>
new Vue({
el: '#box',
data: {
select : 'man'
}
})
</script>
下拉列表
<select v-model="selected" name="fruit">
<option value="">选择一个网站</option>
<option value="www.javascriptcn.com">Runoob</option>
<option value="www.google.com">Google</option>
</select>
<div id="output">
选择的网站是: {{selected}}
</div>
<script>
new Vue({
el: '#app',
data: {
selected: ''
}
})
</script>
checkbox 选中
是否选中 => v-model 绑定一个布尔值 ;true选中,false不选中
<!-- 记住用户名 -->
<div>
用户名:<input type="text" v-model="mytext">
<input type="checkbox" v-model="isRemember">记住用户名
<button @click="handleLogin">登录</button>
</div>
<script>
var vm = new Vue({
el: '#box',
data: {
mytext:localStorage.getItem("username"),
isRemember: true,
},
methods:{
handleLogin(){ // 点击 添加本地存储
if(this.isRemember){ //this.isRemember本身是一个布尔值,当它是true的时候走进来
localStorage.setItem("username",this.mytext)
}
}
}
})
</script>
多选
多选v-model绑定一个空数组,并设置value值,这样每次选中的时候可以把value值添加到数组里
<div id="box">
<h3>你喜欢的女明星</h3>
<input type="checkbox" v-model="checkgroup" value="lyf">刘亦菲
<input type="checkbox" v-model="checkgroup" value="baby">baby
<input type="checkbox" v-model="checkgroup" value="egon">egon
<br>
{{checkgroup}}
</div>
<script>
var vm = new Vue({
el: '#box',
data: {
checkgroup:[],
},
})
</script>
表单修饰符
v-model双向绑定,所以只要表单对应的value值发生变化,相应数据对应的元素都会进行同步,通过.lazy则是失去焦点的时候同步一次。
<!-- 失去焦点的时候 ,同步一次 -->
<p>用户名:<input type="text" v-model.lazy="username"/></p>
{{username}}
<p>密码:<input type="password" v-model="password"/></p>
{{password}}
<!-- 配合 input number类型,得到number类型的值 -->
<p>年龄:<input type="number" v-model.number="age"/></p>
<!-- 去掉首尾空格 -->
<p>格言:<input type="text" v-model.trim="text"/></p>
.lazy
等失去焦点的时候再同步更新.number
得到number类型的值.trim
去首尾空格
@change和@click区别
click事件的触发是在点击的那一刻,而change事件是在状态改变之后触发。例如:
<input type="checkbox" @click="handleAllChange()" v-model="isAllChecked">
<!--当点击checkbox后,isAllChecked值并不会发生改变。把click事件换成change事件的话,点击checkbox后,isAllChecked值发生了改变-->
案例:购物车
li{ display: flex;justify-content: space-around;padding: 10px;}
li img{width: 100px;}
<div id="box">
<input type="checkbox" v-model="isAllChecked" @change="handleAllChange"> 全选
<ul>
<li v-for="(item,index) in datalist" :key="item.id">
<input type="checkbox" v-model="checkGroup" :value="item" @change="handleItemChange">
<img :src="item.pic" alt="">
<div>
<div>商品:{{item.name}}</div>
<div style="color: red;">价格:{{item.price}}</div>
</div>
<div>
<button @click="item.number--" :disabled="item.number===1">-</button>
<span>{{item.number}}</span>
<button @click="item.number++" :disabled="item.number===item.limit">+</button>
</div>
<div><button @click="handleDelClick(index)">删除</button></div>
</li>
</ul>
<div>总金额:{{sum()}}</div>
</div>
new Vue({
el:"#box",
data:{
checkGroup:[],
isAllChecked:false,
datalist: [{
name: "商品1",
price: 10,
number: 1,
id: 1,
limit: 5, //限购
pic: "https://static.maizuo.com/pc/v5/usr/movie/44dc08914d508fc47c8267c6ca73f2d8.jpg"
},
{
name: "商品2",
price: 20,
number: 2,
id: 2,
limit: 10, //限购
pic: "https://static.maizuo.com/pc/v5/usr/movie/44dc08914d508fc47c8267c6ca73f2d8.jpg"
},
{
name: "商品3",
price: 30,
number: 3,
id: 3,
limit: 15, //限购
pic: "https://static.maizuo.com/pc/v5/usr/movie/44dc08914d508fc47c8267c6ca73f2d8.jpg"
}]
},
methods:{
//计算总金额
sum(){
var total = 0;
//累加计算 checkList 数组的每一项的 价格*数量
/* for( i in this.checkGroup){
total += this.checkGroup[i].price * this.checkGroup[i].number
} */
this.checkGroup.forEach(item=>{
total += item.price*item.number
})
return total
},
//删除
handleDelClick(index){
var deleteid = this.datalist[index].id
this.datalist.splice(index,1)
this.checkGroup.filter(item=>item.id !== deleteid)
this.handleAllChange()
},
//全选
handleAllChange(){ //全选change事件 inpu发生改变,change触发
//console.log(this.isAllChecked);
if(this.isAllChecked){//全选选中
this.checkGroup = this.datalist
}else{//全不选
this.checkGroup = []
}
},
handleItemChange(){
if(this.checkGroup.length === this.datalist.length){
this.isAllChecked = true
}
else{
this.isAllChecked = false
}
}
}
})
计算属性
计算属性是为了解决模板过重,难以维护的一个方案
计算属性关键词: computed。计算属性的特点: 定义的像一个方法,必须返回值,使用的时候像 属性data(状态)
<div id="box">
<!-- 模板过重,最好别用 -->
{{ myname.substring(0,1).toUpperCase()+myname.substring(1) }}
<!-- 计算属性,推荐使用 -->
<h3>计算属性</h3>
{{ mycomputedName }}
{{ mycomputedName }}
{{ mycomputedName }}
<!-- 计算属性有缓存,基于依赖的缓存,调用三次会直接从缓存里复用 -->
<!-- 函数 -->
<h3>函数</h3>
{{ mymethods() }}
{{ mymethods() }}
{{ mymethods() }}
<!-- 函数调用没有缓存,调用三次,执行三次 -->
</div>
//计算属性,负责逻辑放在计算属性中来写。
var vm = new Vue({
el:"#box",
data:{
myname:"kerwin"
},
methods:{
mymethods(){
console.log("方法执行")
return this.myname.substring(0,1).toUpperCase()+this.myname.substring(1)
}
},
computed:{
//计算属性的特点, 定义的像一个方法,必须返回值。
//使用的时候像 属性(状态)
mycomputedName(){
console.log("计算属性执行")
return this.myname.substring(0,1).toUpperCase()+this.myname.substring(1)
}
}
})
我们可以使用 methods 来替代 computed,效果上两个都是一样的,但是 computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值。而使用 methods ,在重新渲染的时候,函数总会重新调用执行。computed 性能会更好,但是如果你不希望缓存,你可以使用 methods 属性
- 数据data => 状态,被拦截。
- 方法methods==> 事件绑定, 逻辑计算。可以不用return,没有缓存
- 计算computed(重视结果)=> 解决模板过重问题,必须有return ,只求结果 ,有缓存,同步√ 异步×
- 监听watch (重视过程), 监听一个值的改变。 不要有返回值 ,异步同步都可以
computed 和 methods的区别:职责不同
methods :负责事件处理,调用要加小括号,可以传参
computed :负责复杂的逻辑计算而得到的结果 ,调用不能加小括号,不能传参
- 面试:什么是 vue 的计算属性?
在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。
好处: ①使得数据处理结构清晰;
②依赖于数据,数据更新,处理结果自动更新;
③计算属性内部 this 指向 vm 实例;
④在 template 调用时,直接写计算属性名即可;
⑤常用的是 getter 方法,获取数据,也可以使用 set 方法改变数据;
⑥相较于 methods,不管依赖的数据变不变,methods 都会重新计算,但是依赖数据不变的时候 computed 从缓存中获取, 不会重新计算。
监听属性
我们可以通过 watch 来响应数据的变化。监听是按照先后顺序执行的。
<div id="box">
千米:<input type="text" v-model="kilometers">
米:<input type="text" v-model="meters">
</div>
<p id="info"></p>
var vm = new Vue({
el:"#box",
data:{
kilometers:0,
meters:0
},
watch:{
//监听kilometers和meters改变
kilometers: function (val) {
// 这个回调将在kilometers 改变后调用
this.kilometers = val
this.meters = val * 1000
},
meters:function (val) {
// 这个回调将在meters 改变后调用
this.kilometers = val /1000
this.meters = val
}
}
})
//watch是一个实例方法
vm.$watch("kilometers",function(newValue,oldValue){
document.getElementById("info").innerHTML = "修改前值为" + oldValue + "修改后值为" + newValue
})
效果:
computed 和 watch 的区别:
computed :重视结果 ,必须有return ,只能是同步立即执行。如果是异步回来的不行
watch : 重视过程 ,不要有return ,异步同步都可以
Mixins
<div id="box">
<button @click="a">click-a</button>
<button @click="b">click-b</button>
{{sum}}
</div>
const obj1 = {
methods:{
a(){
console.log("a")
},
b(){
console.log("b")
}
}
}
const obj2 = {
computed:{
sum(){
return 100
}
}
}
new Vue({
el:"#box",
data:{},
methods:{},
mixins:[obj1,obj2] //mixins混入,[]里放要混入的对象
})
fetch & axios
fetch
fetch的诞生是因为XMLHttpRequest 是一个设计粗糙的 API,配置和调用方式非常混乱 ,而且基于事件的异步模型写起来不友好。
fetch有兼容性问题,兼容性不好
查浏览器兼容性:https://caniuse.com/
polyfill: https://github.com/camsong/fetch-ie8 (fetch兼容库)
fetch是基于promise设计的。同时他不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。所以写法较为复杂,需要自己进行封装
get请求:
fetch("**").then(res=>res.json()).then(res=>{console.log(res)})
//res.json()拿到的是json对象
fetch("**").then(res=>res.text()).then(res=>{console.log(res)})
//res.text()拿到的是json字符串 需要再进行JSON.parse()解析才能得到json对象
例:
//get请求
fetch("json/maoyan.json?name=kerwin&age=100") //()里是要获取数据的路径,?后是要携带的数据
.then(res=>res.json()) //{retrun res.json()} 简写:res.json()
.then(res=>{ console.log(res.data.films)
this.datalist = res.data.films})
.catch(err=>{console.log("err")})
//fetch要进行两次.then的调用
//第一个.then拿到请求数据,要对请求的数据要进行转换,例如请求头信息,状态码
//第二个.then拿到最终的真实数据
res.json
调用完返回的是一个json格式的promise对象
post请求:
写法一:
//post请求 post-1
fetch("**",{ //第一个参数:请求的url地址 第二个参数:请求信息
method:'post',
headers: {//设置请求头信息(必须的)
"Content‐Type": "application/x‐www‐form‐urlencoded" //请求数据类型 :名称/值对
},
body: "name=kerwin&age=100", //请求主体,要携带的数据写在body里
}).then(res=>res.json())
.then(res=>{console.log(res)});
//x‐www‐form‐urlencoded 表单编码模式 对应的请求数据是 name=kerwin&age=100 这种格式
写法二:
//post请求 post-2
fetch("https://m.vip.com/server.html?rpc&method=pageCache&f=www&_=1563946036520",{
method:'post',
headers: { //设置请求头信息(必须的)
"Content‐Type": "application/json" //请求数据类型 必须是json格式
},
body: JSON.stringify({ //请求数据,得到的是个对象,需要转成字符串
name:"kerin",
age:100
})
}).then(res=>res.json())
.then(res=>{console.log(res)});
//拿不到数据,因为直接写地址,跨域
总结:fetch必须有两次**.then**的调用
注意:
A. fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials:‘include’})
B. fetch 请求得到数据后,需要自行处理。res.json() =>json数据;res.blob()=> 格式化二进制;res.text()=>文本数据
axios
第三方的库,可以在GitHub里找到源码
两种写法:1、模块化下载 ;2、script标签引入
本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范
它本身具有以下特征:
1.从浏览器中创建 XMLHttpRequest
2.支持 Promise API
3.客户端支持防止CSRF
4.提供了一些并发请求的接口(重要,方便了很多的操作)
5.从 node.js 创建 http 请求
6.拦截请求和响应
7.转换请求和响应数据
8.取消请求
9.自动转换JSON数据
axios.put("")
axios.delete("")
axios.get("")
//get请求
axios.get("json/maoyan.json").then(res=>{//成功走then
console.log(res.data.data.films)
this.datalist = res.data.data.films
//注意:这里第一个res.data是请求的数据(头信息、状态码等),第二个.data 是真正的数据
}).catch(err=>{console.log(err)})//失败走catch
axios.post("")
//post请求 post-1
axios.post("json/maoyan.json","name=kerwin&age=100").then(res=>{
console.log(res.data.data.films)
this.datalist = res.data.data.films
}).catch(err=>{console.log(err)})
//post请求 post-2
axios.post("json/maoyan.json",{name:"kerwin",age:100}).then(res=>{
console.log(res.data.data.films)
this.datalist = res.data.data.films
}).catch(err=>{console.log(err)})
axiosd({}) 也可以直接传个对象
axios({
url: 'https://m.maizuo.com/gateway',
method: 'GET', // 不配method 默认get请求
headers: { // 头部信息,许可访问数据 后端提供
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res=>{
console.log(res.data)
})
/* get url路径?name=kerwin&age=100
post body请求体 ,
(1)x-www-formurlencoded , name=kerwin&age=100
(2) json , `{"name":"kerwin",age:100}`
axios.post("****","name=kerwin&age=100") // (1)
axios.post("****",{name:"kerwin",age:100}) //(2)
*/
总结:一个.then 两个.data
过滤器
Vue.js 允许你自定义过滤器,被用作一些常见的文本格式化。由"管道符"指示, 格式如下。过滤器函数接受表达式的值作为第一个参数。
过滤器:需要把原始数据处理成想要的数据
<!-- 在两个大括号中 -->
{{ message | capitalize }}
<!-- 把原始数据message通过管道符送到过滤器capitalize中 -->
<!-- 在 v-bind 指令中 -->
<div v-bind:id="rawId | formatId"></div>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3qCqAe5U-1623771327471)(file:///C:\Users\xujing\AppData\Local\Temp\ksohtml11024\wps3.jpg)]
过滤器可以进行串联,这里message作为filterA过滤器的参数,filterA执行完得到的结果作为filterB的参数。此时需要在filters{}里定义filterA,filterB两个方法:
{{ message | filterA | filterB }}
过滤器是 JavaScript 函数,因此可以接受参数,因此这里的filterA函数里有两个参数,分别是message,arg1
{{ message | filterA(‘arg1’) }}
注:vue3中不支持过滤器
后端返回:http://p0.meituan.net/w.h/movie/e2ecb7beb8dadc9f07f2fad9820459f92275588.jpg
真是渲染:https://p0.meituan.net/movie/e2ecb7beb8dadc9f07f2fad9820459f92275588.jpg@1l_1e_1c_128w_180h
<div id="box">
<button @click="handleAjax">click-ajax</button>
<ul>
<li v-for="item in datalist" :key="item.id">
<img :src="item.img | imgFilter1 | imgFilter2"/>
{{item.nm}}
</li>
</ul>
</div>
Vue.filter("imgFilter1",(url)=>{
return url.replace('w.h/','')
})
Vue.filter("imgFilter2",(url)=>{
return url+'@1l_1e_1c_128w_180h'
})
new Vue({
el:"#box",
data:{
datalist:[]
},
methods:{
handleAjax(){
axios.get("./json/maoyan.json").then(res=>{
console.log(res.data.movieList)
this.datalist = res.data.movieList
})
}
}
})
五、vue 组件
为什么要使用组件?
- 复用代码结构
- 清晰化项目结构
拓展HTML元素,封装可重用的代码
组件编写过程中需要注意的地方
起名字 :不要起跟现有html标签冲突的名字。例如:main;
命名方法有两种:驼峰命名法 / 连接符命名法
js代码定义组件:驼峰/连接符, html代码引用组件:连接符-
例:js用 kerwinHeader && 页面用 kerwin-header
编写dom片段时候,vscode没有代码提示,没有高亮显示。 ----- vue单文件组件解决(后面会讲)
css 只能写成 行内。----- vue单文件组件解决(后面会讲)
template 包含一个根节点
组件是孤岛,无法【直接】访问外面的组件的状态或者方法。----- 间接的组件通信来交流。
自定义的组件 data 必须是一个函数
所有的组件都在一起, 太乱了 ------ vue单文件组件解决(后面会讲)
<div id="box">
<kerwin-header></kerwin-header>
<kerwin-tabbar></kerwin-tabbar>
</div>
//全局组件
Vue.component("kerwinHeader",{
template:`
<section style="background:yellow;">
<button @click="handleLeft">left</button>
页面的导航栏部分-{{myname}}
</section>
`,
//data必须是一个函数
data(){
return {
myname:"导航栏组件"
}
},
methods:{
handleLeft(){
console.log("left",this.myname)
}
}
})
//根组件
new Vue({
el:"#box",
data:{ //data是一个对象
myname:"box组件"
}
})
组件定义
全局组件
- 全局组件定义使用==Vue.component()==方法。在js全局作用域下进行定义,即在script标签下/某个js文件下
- 全局组件可以定义多个,在页面任何地方都可以使用该组件。例如下面的child组件,kerwinHeader组件
- 页面引用组件是通过组件名字,确保名字书写没问题。否则页面报错。例如 kerwinHeader && kerwin-header
<div id="box">
<kerwin-header></kerwin-header>
</div>
Vue.component("kerwinHeader",{
template:`
<section style="background:yellow;">
页面的导航栏部分
<button>right</button>
<child></child>
</section>
`,
methods:{}
})
Vue.component("child",{
template:`<div>child</div>`
})
局部组件
- 局部组件定义在vue组件里,使用components属性
- html页面上使用局部组件时,也必须放到定义的那个组件下。例如:kerwin-tabbar-a组件,kerwin-tabbar-b组件
<div id="box">
<kerwin-tabbar></kerwin-tabbar>
</div>
Vue.component("kerwin-tabbar",{
template:`
<div style="background:red;">
tabbar
<kerwin-tabbar-a></kerwin-tabbar-a>
<kerwin-tabbar-b></kerwin-tabbar-b>
<child></child>
</div>
`,
//局部组件
components:{
"kerwin-tabbar-a":{
template:`<div>kerwin-tabbar-a</div>`
},
"kerwin-tabbar-b":{
template:`<div>kerwin-tabbar-b</div>`
}
}
})
组件传值(通信)
组件父传子
父传子通过属性 :为了更好的复用组件,例:选项卡、 导航栏
使用 props 属性动态传递参数
<div id="box">
<navbar mytitle="电影" :myleft="false"></navbar> <!--1.传一个mytitle属性(父)-->
<navbar mytitle="我的" :myright="false"></navbar>
</div>
子组件通过props选项接受需要父组件传递的属性。有三种写法
Vue.component("navbar",{
//2-1.接收mytitle myleft myright属性(子)
//props:["mytitle","myleft","myright"],
//在接收属性的同时最好验证属性值
//2-2 接收属性,验证 属性值
/* props:{
mytitle:String,
myleft:Boolean,
myright:Boolean
},*/
//2-3 接收mytitle 属性,验证 属性值,默认值
props:{
mytitle:{
type:Boolean,
default:true
},
mytitle:{
type:String,
default:"11111"
}
},
template:`
<div style="background:red;color:white">
<button>返回</button>
<span>{{mytitle}}</span>
<button>首页</button>
</div>
`, //3.使用mytitle属性(对组件进行复用)
})
new Vue({
el:"#box"
})
props之数据验证
验证的类型可以是:String,Number,Boolean,Object,Array,Function
props:{
props1:Number, //必须是数值类型
props2:[String,Number], // 数字类型 || 字符串
props3:{
type:Number, // 数值型 true为必传
required: true
},
props4:{
type:Boolean, // 布尔值 default默认为true
default: true
},
props5:{
type: Array, // 如果是数组 || 对象 默认值必须是一个函数返回
default:function{
return [];
}
}
}
总结:
- 子组件在props中创建一个属性,用以接收父组件传过来的值
- 父组件中注册子组件
- 在子组件标签中添加子组件props中创建的属性
- 把需要传给子组件的值赋给该属性
注:属性 – 父组件传给你的属性,只有父组件可以重新传,但不允许子组件随意修改.
状态 --组件内部的状态,可以随意修改
组件子传父
子传父通过自定义事件:
父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,就需要使用自定义事件!
我们可以使用 @ 绑定自定义事件, 每个 Vue 实例都实现了事件接口(Events interface),即:
- 使用
$on(eventName)
监听事件- 使用
$emit(eventName)
触发事件
例:实现抽屉效果存在子传父
-
父组件在html结构上自定义事件
<!-- @后面的名字是自定义的,是为了区分js原生的事件。 handleEvent需定义在父组件的methods里--> <navbar @myevent="handleEvent"></navbar><!--@myevent自定义事件--> <sidebar v-show="isShow"></sidebar>
new Vue({ el:"#box", data:{ isShow:true }, methods:{ handleEvent(data){ console.log(data) // 此处打印11111111. 为子组件传递过来的数据 this.isShow= !this.isShow } } })
-
在子组件中创建一个按钮,给按钮绑定一个点击事件
在响应该点击事件的函数中使用**$emit**来触发一个自定义事件
子组件通过$emit触发父组件里自定义的事件(事件名字得对应上)
Vue.component("navbar",{ template:` <div> <button @click="handleClick()">点击显示列表</button> </div> `, methods:{ handleClick(){ // console.log("navbar孩子,希望父组件能够将isShow 取反") //触发 navbar身上的监听事件。$emit方法两个参数。第一个参数:事件名称;第二个参数:数据 this.$emit("myevent","11111111") } } }) Vue.component("sidebar",{ template:` <div style="background:yellow;width:200px;"> <ul> <li>我的</li> <li>设置</li> <li>收藏</li> </ul> </div> ` })
总结:
- 子组件中需要以某种方式例如点击事件的方法来触发一个自定义事件
- 将需要传的值作为$emit的第二个参数,该值将作为实参传给响应自定义事件的方法
- 在父组件中注册子组件并在子组件标签上绑定对自定义事件的监听
非父子通信
中间人模式
两个同级的组件进行通信,可以设置一个父组件,两个子组件同时在这个父组件下,可以通过父组件去获取数据和方法
(子组件1<——>父组件<——>子组件2)也叫中间人模式
案例:
film-item组件先通过子传父,将数据传到父组件this.$emit(“callback”,this.item.synopsis)
父组件接受数据,并传递给film-detail子组件handleCallBack(data){ this.detailInfo = data }
<div id="box">
<button @click="handleAjax">ajax</button>
<film-item v-for="item in datalist" :key="item.filmId" :item="item" @callback="handleCallBack"></film-item>
<film-detail :myinfo="detailInfo"></film-detail>
</div>
Vue.component("film-detail",{
props:["myinfo"],/// this.myinfo myinof
template:`
<div class="filminfo">
{{myinfo}}
</div>
`
})
Vue.component("film-item",{
props:["item"],//接受属性, this.item
template:`
<div class="item">
<img :src="item.poster"/>
<div>{{item.name}}</div>
<button @click="handleDetail">详情</button>
</div>
`,
methods:{
handleDetail(){
// console.log(this.item.synopsis)
this.$emit("callback",this.item.synopsis)
}
}
})
new Vue({
el:"#box",
data:{
datalist:[],
detailInfo:""
},
methods:{
handleAjax(){
fetch("json/test.json").then(res=>res.json())
.then(res=>{
// console.log(res)
this.datalist = res.data.films
})
},
handleCallBack(data){
// console.log("父组件触发",data)
this.detailInfo = data
}
}
})
bus中央总线
bus其实就是一个发布订阅模式,利用vue的自定义事件机制,在触发的地方通过 e m i t 向 外 发 布 一 个 事 件 , 在 需 要 监 听 的 页 面 , 通 过 emit向外发布一个事件,在需要监听的页面,通过 emit向外发布一个事件,在需要监听的页面,通过on监听事件。
一个组件负责监听bus. o n , 一 个 组 件 负 责 触 发 b u s . on,一个组件负责触发bus. on,一个组件负责触发bus.emit
mounted生命周期中进行监听只适合少量非父子通信
<div id="box">
<button @click="handleAjax">ajax</button>
<film-item v-for="item in datalist" :key="item.filmId" :item="item"></film-item>
<film-detail></film-detail>
</div>
var bus = new Vue() //中央事件总线
Vue.component("film-detail",{
template:`
<div class="filminfo">
{{info}}
</div>
`,
data(){
return {
info:""
}
},
// 8个生命周期 ---mounted
mounted(){
console.log("mounted","当前挂载到页面上就会执行")
bus.$on("kerwin",(data)=>{
// console.log("kerwin事件",data)
this.info = data
})
}
})
Vue.component("film-item",{
props:["item"],//接受属性, this.item
template:`
<div class="item">
<img :src="item.poster"/>
<div>{{item.name}}</div>
<button @click="handleDetail">详情</button>
</div>
`,
methods:{
handleDetail(){
bus.$emit("kerwin", this.item.synopsis)
}
}
})
new Vue({
el:"#box",
data:{
datalist:[],
},
methods:{
handleAjax(){
fetch("json/test.json").then(res=>res.json())
.then(res=>{
this.datalist = res.data.films
})
}
}
})
vuex-状态管理
共享的状态,监控(方便调试)
ref
ref
的英文单词是reference,表示引用。我们平时可以经常看到控制台会报错referenceError的错误,就和引用类型的数据有关。
- $ref用来获取组件实例,不通过父子通信,可以获取组件的所有方法及data的变量
- $ref需要在dom挂载后才能获取到,否则undefined
- 获取原生dom节点
<div id="box">
<h2>ref- 获取原生dom节点</h2>
<input type="text" ref="myinput"/>
<div ref="mydiv">111111111111111111111111</div>
<button @click="handleGet()">获取</button>
</div>
new Vue({
el:"#box",
methods:{
handleGet(){
console.log(this.$refs.myinput.value) // 页面input框内输入的值
console.log(this.$refs.mydiv)
// <div ref="mydiv">111111111111111111111111</div>
}
}
})
总结:在Vue中,通过 ref 属性获取DOM元素的步骤
- 在标签中给 DOM 元素设置 ref 属性。
- 通过 this.$refs.xxx 获取 DOM 元素
- 获取组件实例
<div id="box">
<h2>ref-登录页-获取组件实例,不通过父子通信,直接拿状态和赋值状态</h2>
<!-- mylabel, mytype是用来父传子-->
<!-- mychange是用来子传父-->
<!-- ref是用来获取子组件实例,跳过父子通信-->
<vant-field mylabel="用户名" mytype="text" @mychange="handleChange"></vant-field>
<vant-field mylabel="密码" mytype="password" ref="mypassword"></vant-field>
<vant-field mylabel="年龄" mytype="number" ref="myage"></vant-field>
<button @click="handleLogin">登录</button>
<button>重置</button>
</div>
// 全局组件vant-field
Vue.component("vant-field",{
props:["mylabel","mytype"], // 用来接收父组件传递过来的mylabel,mytype
data(){
return {
mytext:""
}
},
// 这里:type="mytype" 一定要用:type。因为这里的mytype是一个变量,从父组件传递过来的
// type="mytype" 这么写的话,组件到时会解析成 <input type="mytype" />
// :type="mytype",假设传递过来的数据是text,到时会解析成 <input type="text" />
template:`
<div>
<label>{{mylabel}}</label>:
<input :type="mytype" v-model="mytext" @input="handleInput"/>
</div>
`,
methods:{
handleInput(){
// console.log(this.mytext)
this.$emit("mychange",this.mytext) //触发子传父
}
}
})
new Vue({
el:"#box",
data:{
myusername:""
},
methods:{
handleChange(data){
// console.log("父组件定义的",data)
this.myusername = data;
},
handleLogin(){
console.log("表单的用户名",this.myusername)
console.log("表单的密码",this.$refs.mypassword.mytext)
console.log("表单的年龄",this.$refs.myage.mytext)
this.$refs.myage.mytext = 2000
}
}
})
总结:绑在组件上,拿到的就是组件对象,绑在dom上,拿到的就是dom对象
props修改
- 问题(1)*属性可不可以修改?
严格来说,Vue子组件不能随便更改父组件传递过来的属性,但是可以通过 props 配合 $emit 改变父组件传入的值
//父组件
<div id="box">
{{title}}
<child :mytitle="title"></child>
</div>
<script>
Vue.component("child",{
props:["mytitle"],
template:`<div>
child ----- {{mytitle}}
<button @click="handleChange">change</button>
</div>`,
data(){
return {
mystate:""
}
},
methods:{
handleChange(){
// this.mystate = this.mytitle
this.mytitle = "222222222222222222"
}
}
})
var vm = new Vue({
el:"#box",
data:{
title:"11111111111111"
}
})
//属性 -- 父组件传给你的属性,只有父组件可以重新传,但不允许子组件随意修改.
//状态 --组件内部的状态,可以随意修改
v-once : 执行一次性地插值,当数据改变时,插值处的内容不会更新,v-once所定义的元素或组件只会渲染一次,首次渲染后,不再随着数据的改变而重新渲染。也就是说使用v-once,该块都将被视为静态内容。
- 问题(2)*v-once 用在组件上有什么用?
在根元素
上添加v-once这个attribute以确保这些内容只计算一次,然后缓存起来。组件不会更新
动态组件
component是vue的一个内置组件,作用是:配合is动态渲染组件。: is=“自定义的组件名”
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
案例:切换选项卡
<div id="box">
<!-- 动态组件 -->
<!-- *<component> 元素,动态地绑定多个组件到它的 is 属性 -->
<!-- *<keep-alive> 保留状态,避免重新渲染 -->
<keep-alive>
<component :is="which"></component>
</keep-alive>
<footer>
<ul>
<li @click=" which='home' ">
首页
</li>
<li @click=" which='list' ">
列表
</li>
<li @click=" which='shopcar' ">
购物车
</li>
</ul>
</footer>
</div>
Vue.component("home",{
template:`
<div>
home
<input type="text"/>
</div>
`
})
Vue.component("list",{
template:`
<div>
list
</div>
`
})
Vue.component("shopcar",{
template:`
<div>
shopcar
</div>
`
})
var vm = new Vue({
el:"#box",
data:{
which:"home"
}
})
slot插槽(内容分发)
插槽(Slot)用于决定将所携带的内容,插入到指定的某个位置,从而使模板分块,具有模块化的特质和更大的重用性。插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示就由子组件来进行控制
匿名插槽
<div id="box">
<child>
<div>11111</div>
<div>222222</div>
</child>
</div>
<script>
Vue.component("child",{
template:`
<div>
child
<slot></slot>
</div>
`
})
new Vue({
el:"#box"
})
</script>
没有使用 slot, 无论添加什么内容,浏览器什么都不会识别,向子组件中添加slot插槽,添加在父组件里的内容就能显示
具名插槽
具名插槽的作用是,一个萝卜一个坑,可以将父组件中的内容插入指定的子组件位置中
具名插槽的使用语法
1 . 子组件定义slot时,在标签上加上name='xxx’属性
2 . 父组件将想插入的部分最外部的div上加上slot="xxx"属性
<div id="box">
<child>
<div slot="header">父组件传递过来的头部内容</div>
<div slot="body">父身体内容</div>
<div slot="footer">父组件传递过来的页脚内容</div>
</child>
</div>
<script>
Vue.component("child",{
template:`
<div>
子组件
<div>----------页头-----------</div>
<slot name='header'></slot>
<div>----------页中-----------</div>
<slot name='body'></slot>
<div>----------页脚-----------</div>
<slot name='footer'></slot>
</div>
`
})
new Vue({
el:"#box"
})
</script>
新slot
<div id="box">
<!-- 当前组件或者节点 在哪个模板中,就能访问哪个模板状态 -->
<navbar>
<template #left>
<!-- v-slot:left简写 #left -->
<button>aaaa</button>
</template>
<!-- 注意 v-slot 只能添加在 <template> 上 -->
<!-- 注:文本节点也可以当具名插槽内容插入 -->
<template #right>
bbbbbbbb
</template>
</navbar>
</div>
<script>
Vue.component("navbar",{
template:`
<div>
<slot name="left"></slot>
<span>title</span>
<slot name="right"></slot>
</div>
`
})
new Vue({
el:"#box"
})
</script>
总结:
slot是Vue中的插槽,它是使用在子组件中的 。slot简单理解就是,在子组件内占坑,在父组件里填坑。
slot一般都是子组件定义了主体部分,父组件引入,然后父组件根据需求不同需要向里面添加不同的内容
插槽的意义 : 扩展组件能力, 提高组件的复用性 , 可以按照开发者是心愿小传什么就自己传
父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。
transition过度
Vue中有一个自带的transition组件,用来实现过渡效果的,使用步骤:
-
用transition组件,把要做过渡效果的元素包起来
-
写上相应的过渡效果的css
过渡的类名
enter-active-class:定义进入过渡生效时的状态。
leave-active-class:定义离开过渡生效时的状态。
<style>
.kerwin-enter-active{animation: aaa 1.5s;}
.kerwin-leave-active{animation: aaa 1.5s reverse;}
@keyframes aaa {
0%{
opacity: 0;
transform: translateX(100px);
}
100%{
opacity: 1;
transform: translateX(0px);
}
}
</style>
<body>
<div id="box">
<button @click="isShow = !isShow">show/hide</button>
<transition enter-active-class="kerwin-enter-active" leave-active-class="kerwin-leave-active">
<div v-if="isShow">111111111</div>
</transition>
<transition name="kerwin">
<div v-if="isShow">2222222222</div>
</transition>
</div>
</body>
new Vue({
el:"#box",
data:{
isShow: true
}
})
注:transition只能有一个根节点
初始元素过度
关于appear的用法和enter的用法相似,它只是在第一次渲染的时候才会起作用。
/* 进场动画 */
.kerwin-enter-active{animation: aaa 1.5s;}
/* 出场动画 */
.kerwin-leave-active{animation: aaa 1.5s reverse;}
/* 定义动画的名字和开始到结束的样式 */
@keyframes aaa {
0%{
opacity: 0;
transform: translateX(100px);
}
100%{
opacity: 1;
transform: translateX(0px);
}
}
<div id="box">
<button @click="isShow=!isShow">show/hide</button>
<!-- kerwin-enter-active,kerwin-leave-active是自定义的样式 -->
<transition enter-active-class="kerwin-enter-active" leave-active-class="kerwin-leave-active">
<div v-if="isShow">1111111111111111111</div>
</transition>
</div>
new Vue({
el:"#box",
data:{
isShow: true
}
})
如果进出场动画对应的样式定义为:kerwin-enter-active,kerwin-leave-active。那么页面除了上面那种写法外,还有种简单的写法
<transition name="kerwin" appear>
<div v-if="isShow">2222222222222222222</div>
</transition>
多个元素过度(设置key值)
在html代码中使用v-if v-else用来控制多个元素的过渡。mode表示进出动画的顺序
mode属性值:out -in(先出再进) in-out(先进再出)
<div id="box">
<button @click="isShow=!isShow">点击切换显示隐藏</button>
<transition enter-active-class="kerwin-enter-active" leave-active-class="kerwin-leave-active" mode="out-in">
<div v-if="isShow" key="1">1111</div>
<!--<main v-else key="2">2222</main>--> <!--标签不一样也可以实现多元素动画-->
<div v-else key="2">2222</div>
<!--因为标签一样,层级一样,通过设置不同的key值 diff算法对比后,会删除重新创建,diff对比如果都一样的化,会进行复用-->
</transition>
</div>
当有相同标签名的元素切换时,需要通过 key 特性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。
多个组件过渡
使用component标签实现动态组件效果。对每个动态组件包上一个transition,从而实现多个组件过渡
<keep-alive>
<transition name="kerwin" mode="out-in">
<component :is="which"></component>
</transition>
</keep-alive>
<footer>
<ul>
<li @click=" which='home' ">
首页
</li>
<li @click=" which='list' ">
列表
</li>
<li @click=" which='shopcar' ">
购物车
</li>
</ul>
</footer>
Vue.component("home",{
template:`
<div>
home
<input type="text"/>
</div>
`
})
Vue.component("list",{
template:`
<div>
list
</div>
`
})
Vue.component("shopcar",{
template:`
<div>
shopcar
</div>
`
})
多个列表过渡
使用transition-group, 因为transition里面只能有一个根节点,多个节点 用transition-group
- 子元素通常使用v-for进行循环。
- 子元素必须要有唯一的key属性,且key不能为索引值。
<ul>
<transition-group name="kerwin" tag="ul" v-show="datalist.length">
<li v-for="(item,index) in datalist" :key="item">
{{item}}--{{index}}
<button @click="handleDelClick(index)">del</button>
</li>
</transition-group>
<!-- transition-group会实例化成一个span标签 -->
</ul>
不同于 transition, 它会以一个真实元素呈现:默认为一个 。你也可以通过 tag 特性更换为其他元素。
var vm = new Vue({
el:"#box",
data:{
mytext:"",
datalist:["1111","222","3333"]
},
methods:{
handleDelClick(index){
this.datalist.splice(index,1)
}
}
})
可复用过度
<div id="box">
<navbar @myevent="handleEvent"></navbar>
<sidebar v-show="isShow" mode="right"></sidebar>
</div>
<script>
Vue.component("navbar",{
template:`
<div>
nabbar-<button @click="handleClick">click</button>
</div>
`,
methods:{
handleClick(){
// 通知父组件 取反 isShow - 子传父 依靠 事件
this.$emit("myevent")
}
}
})
Vue.component("sidebar",{
props:["mode"],
template:`
<transition :name="mode">
<ul style="background-color: yellow;width: 200px;height: 500px;" :class="mode">
<li>首页</li>
<li>钱包</li>
<li>设置</li>
</ul>
</transition>
`
})
new Vue({
el:"#box",
data:{
isShow:false
},
methods:{
handleEvent(){
console.log("父组件","1111111")
this.isShow = !this.isShow
}
}
})
mixin
六、swiper
官网:https://www.swiper.com.cn/
- 引用相关css,js文件
<script src="lib/swiper/js/swiper.js"></script>
<link rel="stylesheet" href="lib/swiper/css/swiper.css">
- html结构
完整的轮播插件包括:
.swiper-container容器:用来放轮播内容,分页器,导航按钮
.swiper-wrapper容器:用来放轮播的内容
.swiper-slide容器:用来放每个轮播元素,这里也可以放置一个img标签(图片轮播)
.swiper-pagination容器:用来放分页器
.swiper-button-prev .swiper-button-next:用来放轮播左右按钮
<style>
/**设置轮播插件外层容器大小**/
.kerwin {
height: 500px;
}
</style>
<div class="swiper-container kerwin">
<div class="swiper-wrapper">
<div class="swiper-slide">Slide aaaa</div>
<div class="swiper-slide">Slide bbbb,</div>
<div class="swiper-slide">Slide cccc</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<!-- <div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div> -->
</div>
- swiper插件初始化
//初始化 swiper
new Swiper(".kerwin", {
loop: true, //开启循环
// direction:"vertical"//垂直轮播
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
},
slidesPerView: 3,
spaceBetween: 30,
// 如果需要前进后退按钮
// navigation: {
// nextEl: '.swiper-button-next',
// prevEl: '.swiper-button-prev',
// },
})
当轮播图上的数据来自后台返回,需要注意new Swiper()初始化过早导致轮播图不好用的问题。注意轮播数据加载和轮播插件初始化两者之间的先后顺序
下面是错误样例
先初始化swipper插件,后加载轮播数据
setTimeout(() => {
//ajax成功,加载轮播数据
}, 2000)
// 初始化swipper
new Swiper(".kerwin", {
loop: true, //开启循环
pagination: {
el: '.swiper-pagination',
}
})
正确写法
先加载轮播数据,等轮播数据渲染完页面dom后,再初始化swipper插件
setTimeout(() => {
//ajax成功,加载轮播数据
// 初始化swipper
new Swiper(".kerwin", {
loop: true, //开启循环
pagination: {
el: '.swiper-pagination',
}
})
}, 2000)
注:此方法在vue中不适用,因为mounted初始化完后,数据回来和初始化swiper是异步的, new Swiper 要先于数据加载 。 ( vue的设计思想是数据驱动dom,得到轮播数据后赋值给一个状态,页面dom并不会立马更新,状态改变会通知监听,只有在组件生命周期的updated里面才是页面渲染完毕。)
vue下的swiper插件
swiper插件里的轮播数据为动态数据的时候,为避免出现Swiper初始化过早从而导致轮播图插件失效情况的发生,可采取以下几种方式进行处理。
1. 使用updated生命周期(不推荐)
将swiper插件初始化代码放到updated生命周期下。解决问题的同时带来其他两个问题:
A. 如果其他状态更新 ,也会导致updated重复执行, 就会导致new Swiper多次初始化。容易出bug
B. 没有复用性
<div class="swiper-container kerwin">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="data in datalist" :key="data">
<img :src="data"/>
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>
new Vue({
el: "#box",
data:{
datalist:[]
},
mounted() {
setTimeout(() => {
this.datalist = ["https://static.maizuo.com/pc/v5/usr/movie/e856bdc65507b99800f22e47801fa781.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/47aa5a3ad2ebff403d073288e4365694.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/8b0755547313706883acc771bda7709d.jpg"
]
// 初始化放在这会出现swipper初始化过早的问题。因为此时页面初始渲染完毕,页面上轮播的数据是为空的
// 上面 设置了this.datalist 。页面的再次重新渲染完毕是在updated生命周期下。所以在mounted里不会再次重新渲染
}, 2000)
},
updated(){
console.log("updated","获取到更新后得dom,依赖于dom操作的库,需要知道状态更新完,什么时候dom更新。")
new Swiper(".kerwin", {
loop: true, //开启循环
pagination: {
el: '.swiper-pagination',
}
})
}
})
2. slot+props方式自定义组件
封装swiper组件,页面html引用swiper组件,这里loop为传入swiper组件的属性
<!-- 动态数据加key值,第一次key=0 ;第二次key=数组长度; key不一样了,会删了之前的重新创建一个轮播,diff算法-->
<swiper :key="datalist.length" :loop="true">
<div class="swiper-slide" v-for="item in datalist" :key="item">
<img :src="item"/>
</div>
</swiper>
<!--v-if ==> v-if第一次 长度为0 ,不会创建swiper节点 ,第二次不为0,才会创建节点 -->
<swiper v-if="datalist.length" :loop="true">
<div class="swiper-slide" v-for="item in datalist" :key="item">
<img :src="item"/>
</div>
</swiper>
swiper组件,slot用来占坑,是个匿名插槽。props用来接收父组件传递过来的参数
Vue.component("swiper",{
props:{
loop:{
type:Boolean,
default:true
}
//好多其他属性.....
},
template:`
<div class="swiper-container kerwin" style="height:300px">
<div class="swiper-wrapper">
<slot></slot>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
</div>`,
mounted(){
console.log("mounted")
new Swiper(".kerwin", {
loop: this.loop, //开启循环
pagination: {
el: '.swiper-pagination',
}
})
},
destroyed(){
console.log("destroyed")
}
})
根组件里初始化轮播图数据
new Vue({
el: "#box",
data:{
datalist:[]
},
mounted() {
setTimeout(() => {
this.datalist = ["https://static.maizuo.com/pc/v5/usr/movie/e856bdc65507b99800f22e47801fa781.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/47aa5a3ad2ebff403d073288e4365694.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/8b0755547313706883acc771bda7709d.jpg"
]
}, 2000)
}
})
3. 使用$nextTick()
有了$nextTick()之后,我们就可以在mounted()钩子函数下进行初始化swiper插件。Vue.nextTick()方法会延迟一段时间执行,而且只监听一次。
nextTick() 在当前的状态更新完到dom后,就会触发,而且只触发一次
mounted() {
setTimeout(() => {
this.datalist = [
"https://static.maizuo.com/pc/v5/usr/movie/e856bdc65507b99800f22e47801fa781.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/47aa5a3ad2ebff403d073288e4365694.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/8b0755547313706883acc771bda7709d.jpg"
]
this.$nextTick(() => {
console.log("我比updated还要晚,而且只执行一次")
new Swiper(".kerwin", {
loop: true, //开启循环
pagination: {
el: '.swiper-pagination',
}
})
})
}, 2000)
}
4. 指令轮播
<body>
<div id="box">
<div class="swiper-container kerwin" style="height:300px">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(data,index) in datalist" :key="data"
v-swiper="{index:index,length:datalist.length}">
<img :src="data" />
</div>
</div>
</div>
</div>
Vue.directive("swiper", {
inserted(el, binding) {
console.log(binding.value)
//如果最后一个节点都插入到父节点中,那就是所有的节点都插入完了
if (binding.value.index === binding.value.length - 1) {
new Swiper(".kerwin", {
loop: true
})
}
}
})
new Vue({
el: "#box",
data: {
datalist: []
},
mounted() {
setTimeout(() => {
//改变状态,异步更新dom
this.datalist = [
"https://static.maizuo.com/pc/v5/usr/movie/e856bdc65507b99800f22e47801fa781.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/47aa5a3ad2ebff403d073288e4365694.jpg",
"https://static.maizuo.com/pc/v5/usr/movie/8b0755547313706883acc771bda7709d.jpg"
]
}, 2000)
},
})
七、自定义指令
自定义指令作用:为了操作底层dom
把dom操作包装在指令里,以后还可以进行复用
vue设计理念就是数据驱动dom,不会针对dom进行操作。自定义指令是作者给预留的方案
实际应用-- 可以通过指令知道什么时候dom创建完成, 从而进行 依赖dom的库的初始化工作
添加一个自定义指令,有两种方式:
- 通过 Vue.directive() 函数注册一个全局的指令。
- 通过组件的 directives 属性,对该组件添加一个局部的指令。
指令用法
首先定义一个指令,其次在页面上引用该指令即可。
html上 使用 v-指令名 引用指定的自定义指令。
定义指令时通常需要配合使用指令生命周期,常用的有inserted,update
<div id="box">
<div v-hello=" 'red' " >1111111111111111</div>
<div v-hello=" 'yellow' " >222222222222222</div>
<div v-hello=" mycolor " >333333333333333</div>
</div>
<script>
Vue.directive("hello",{
inserted(el,binding){ //创建的时候走
// 当前指令绑定的节点插入到页面中,就回被执行--指令的生命周期--类似mounted
// console.log(el) 这里el得到的是使用该指令的dom元素,即“div”
// console.log(binding.value) 这里的binding是页面传递过来的值,即“red”
el.style.background=binding.value
},
update(el,binding){ //指令更新生命周期
//组件:updated ; 指令:update
el.style.background=binding.value
}
})
var vm = new Vue({
el:"#box",
data:{
mycolor :"blue"
}
})
</script>
指令注册
自定义指令类似于组件,他也有相应的生命周期,注册方式。指令注册分为两种:局部注册和全局注册
全局注册
// 使用Vue.directive 进行全局指令注册。跟组件component类似
// 这里hello是指令名
Vue.directive("hello",{
inserted(el,binding){
el.style.background=binding.value
}
})
局部注册
//在组件中使用directives 进行局部指令注册。跟定义局部组件components类似
directives: {
// 这里hello是指令名
"hello": {
inserted(el,binding){
el.style.background=binding.value
}
}
}
指令函数简写
<div id="box">
<div v-hello=" 'red' " >1111111111111111</div>
<div v-hello=" 'yellow' " >222222222222222</div>
<div v-hello=" mycolor " >333333333333333</div>
</div>
<script>
Vue.directive("hello",(el,binding)=>{ // 创建和更新都走这
el.style.background=binding.value
})
var vm = new Vue({
el:"#box",
data:{
mycolor:"blue"
},
mounted() {
},
})
</script>
指令定义函数提供了几个钩子函数(可选):
-
bind
: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。 -
inserted
: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。 -
update
: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。 -
unbind
: 只调用一次, 指令与元素解绑时调用。 -
componentUpdated
:指令所在组件的 VNode 及其子 VNode 全部更新后调用inserted
和update
可以简写在一个回调函数里,初始化和更新都会走进这个回调函数
钩子函数的参数有:
- el: 指令所绑定的元素,可以用来直接操作 DOM 。
- binding: 一个对象,包含以下属性:
- name: 指令名,不包括
v-
前缀。 - value: 指令的绑定值, 例如:
v-my-directive="1 + 1"
, value 的值是2
。 - arg: 传给指令的参数。例如
v-my-directive:foo
, arg 的值是"foo"
。
- name: 指令名,不包括
Vue3 指令
<div id="box">
<div v-hello=" 'red' ">1111111111111111</div>
<div v-hello=" 'yellow' ">222222222222222</div>
<div v-hello=" mycolor ">333333333333333</div>
</div>
<script>
var obj = {
data() {
return {
mycolor: "blue"
}
}
}
var app = Vue.createApp(obj)
app.directive("hello", {
mounted(el, binding) {
el.style.background = binding.value
},
updated(el, binding) { //指令更新生命周期
el.style.background = binding.value
}
})
app.mount("#box")
</script>
拖拽
<style type="text/css">
.one,.two{
height:100px;
width:100px;
border:1px solid #000;
position: absolute;
-webkit-user-select: none;
-ms-user-select: none;
-moz-user-select: -moz-none;
cursor: pointer;
}
.two{
left:200px;
}
</style>
<div id="app">
<div class="one" v-drag>拖拽one</div>
<div class="two" v-drag>拖拽two</div>
</div>
Vue.directive('drag', {
inserted:function(el){
el.οnmοusedοwn=function(e){
let l=e.clientX-el.offsetLeft;
let t=e.clientY-el.offsetTop;
document.οnmοusemοve=function(e){
el.style.left=e.clientX-l+'px';
el.style.top=e.clientY-t+'px';
};
el.οnmοuseup=function(){
document.οnmοusemοve=null;
el.οnmοuseup=null;
}
}
}
})
new Vue({
el:'#app'
});
八、vue 单文件组件
传统Vue组件的缺陷:
- 全局定义的组件不能重名,字符串模板缺乏语法高亮,不支持css(当html和js组件化时,css没有参与其中)
- 没有构建步骤限制,只能使用H5和ES5,不能使用预处理器(babel)
单文件组件:
使用Vue单文件组件,每个单文件组件的后缀名都是.vue。每一个Vue单文件组件都由三部分组成
- template组件组成的模板区域
- script组成的业务逻辑区域
- style样式区域(scoped为只在此组件生效)
<template>
<!-- 组件代码区域 这里只能有一个根节点 -->
<div>
hello
</div>
</template>
<script>
js代码区域
</script>
<style scoped>
/* style标签 加上scoped属性,css局部生效
style标签 加上lang="scss",支持scss */
样式代码区域
</style>
注:vscode 安装 vetur 插件, 可以使得.vue文件中的代码高亮 和 代码提示
单文件组件中, 必须也只能有一个根节点
脚手架创建项目
Vue-cli是一个专门为单页面应用快速搭建脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。
利用vue-cli脚手架来构建Vue项目需要先安装Node.js和NPM环境。
vue-cli 4.5的使用 :
-
npm install -g @vue/cli (一次安装)
-
vue --version 查看版本
-
vue create myapp 创建一个项目 (myapp为自定义的项目名)
- 快速创建一个项目:快速生成一个基于webpack模板构建的项目,项目名为vue-project。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QakInMbX-1623771327474)(C:\Users\xujing\AppData\Roaming\Typora\typora-user-images\image-20210416102602217.png)]
- 配置完成后,可以看到目录下多出了一个myapp 的项目文件夹,里面就是 vue-cli 创建的一个基于 webpack 的 vue.js 项目。
启动流程与入口文件
启动服务:
package.json 项目启动方式
"scripts": {
"start": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
scripts : 定义了一组可以运行的 node 脚本
npm start 定义start 直接启动服务 ,可以省略run
脚本的名字是:start(启动)、 stop(停止) 、restart(重启)、 test(测试) 是可以直接npm 操作的 。除了这四个以外的脚本,需要 npm rum xxx
*npm run serve 开发环境构建
*npm run build 生产环境构建
*npm run lint 代码检测工具 (检查代码错误并修复)
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.6.5",
"swiper": "^6.5.6",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"vuex": "^3.4.0"
},
dependencies : npm 软件包的列表(开发阶段和生产阶段) 列出来的都是:整个环境用的第三方软件包的名字
启动方式:
通过文件目录输入cmd直接进入
选择指定文件目录 => 右键 + Powershell
vscode => 指定文件目录 => 右键 => 集成终端打开
项目目录文件
项目的目录结构 (对于开发者更多操作的是src目录):
|--public: //静态资源文件夹,访问的时候可以不用写public
|-- index.html // 入口页面 创建项目时自动生成的,一旦创建完,基本不会去改里面的代码
|-- src // 源码目录
| |-- assets //存放资产文件
| |-- components // vue公共组件(这个文件夹下放的是根组件下的子组件)
| |-- router //存放路由js文件,用于页面的跳转
| |-- views // 业务页面中的组件
| |-- App.vue // 页面入口文件 根组件 通过main.js来把App根组件挂载到index.html上
| |-- main.js // 核心模块入口(唯一不可以改名字)
建议:组件名字首字母大写,文件夹小写
main.js
// Es6导入方式
import Vue from 'vue' // node_module 不加相对路径
import App from './App.vue' // 导入根组件app,自己定义的要加相对路径
import router from './router' // 路由配置文件 ./router 可以省略index
// import store from './store'
Vue.config.productionTip = false // 生产环境会不会出现log
new Vue({
router,
// store,
render: h => h(App) //vue支持的新写法, render渲染 App作为形参实例化挂载到#app节点上
}).$mount('#app')
//模块化开发中把app组件渲染完之后挂到app节点上
eslint修复
- 安装eslint插件 , 并启用
[文件] => [首选项] => [设置] => 用户 ,找到 setting.json , 加上以下配置
"editor.codeActionsOnSave": {
"source.fixAll": true
}
- 关闭eslint
- 根目录下创建 vue.config.js 并进行以下配置 ,配置完后不会再出现代码格式报错,如要修复,可每次写完代码后再 npm run lint
// vue项目的配置文件 覆盖,
module.exports = {
lintOnSave: false // 暂时关闭代码格式检测
}
- .eslintrc 删除 ‘@vue/standard’ (对于某个规则关闭, no-new:“off” )
- npm run lint 每次执行会进行代码检测并修复
Vue.config.js的配置
vue.config.js 是根目录下手动创建 ,vue.config.js 是对vue项目的配置文件进行覆盖,配置完后必须重启服务才能生效
关闭eslint
// vue项目的配置文件 覆盖,
module.exports = {
lintOnSave: false // 暂时关闭代码格式检测
}
配置完后不会再出现代码格式报错,如要修复,可每次写完代码后再 npm run lint
- .eslintrc 删除 ‘@vue/standard’ (对于某个规则关闭, no-new:“off” )
proxy反向代理
前后端分离的网站下
同源策略:域名 、协议、 端口 必须一致才同源 。 不同源的网站不予许发送ajax请求
只有浏览器获取服务器数据才存在跨域,服务器获取服务器数据不存在跨域
跨域解决办法:
jsonp
–原理:不用ajax,使用script标签,script标签不受同源策略的影响,缺点是script只能是get请求,大小为4kb
cors
–原理:ajax2.0允许跨域资源共享,cors必须需要服务器的允许,并且如果是IE浏览器不能低于IE10。优点是:前端不需要做任何事情,只需要添加一个请求头(它会自动在HTTP头部中添加附加信息(例如domain),关键在于服务器是否实现了CORS接口)
反向代理
– 原理,避免跨域 服务器给服务器发请求 利用的是 proxy 属性
自己的前端向自己的后端发请求,后端向猫眼发请求,请求回来的数据返回给前端
devServer.proxy 手动去配置反向代理
module.exports = {
// devServer的配置
devServer: {
port: 8888, // 自定义端口
open: true, // 自动打开浏览器
proxy: { // 用于配置反向代理
'/ajax': { // 代理请求, 匹配所有以/ajax开头的请求
target: 'http://touxxxxxx.itheima.net/', // 目标服务器,所有以/api开头的请求接口代理到目标服务器
changeOrigin:true,
pathRewrite: {
'^/api': '' // 重写路径,此时用于匹配反向代理的/api可以替换为空
},
secure: true // 如果代理到HTTPS服务器需要设置secure为true,默认为false
}
}
}
}
注:配置完vue.config.js文件需要重启服务
注意:开发阶段才能使用webpack的反向代理配置,如果项目上线,那webpack的反向代理就失效了,也就是说以上代码只是开发阶段的反向代理
为了解决跨域问题,可在vue项目中设置反向代理,从而保证前端能顺利请求后端数据。
反向代理配置如下:
- 在package.json文件同级目录下新建一个vue.config.js,并将如下代码放入vue.config.js中。
// vue的配置文件, 修改重启服务器
module.exports = {
// 对于当前开发服务器配置反向代理
devServer: {
proxy: { // 一个proxy下可以配置多个代理,下面就配置了两个代理。代理目录分别为/kervin,/ajax
'/kerwin': { // /kerwin为代理目录
target: 'https://m.maoyan.com', // https://m.maoyan.com 为后台服务器域名或ip地址
changeOrigin: true,
//pathRewrite对请求路径进行重定向以匹配到正确的请求地址
// 假设页面路由配置 /kervin/movieOnInfoList?token=&aaa
// 解析后的地址为 https://m.maoyan.com/ajax/movieOnInfoList?token=&aaa
pathRewrite: {
'^/kerwin': '/ajax' //路由里的地址匹配 : 替换后的地址
}
},
// 解析后的请求地址:target后配置的地址/代理目录
// 假设页面路由配置 ajax/movieOnInfoList?token=&aaa
// 解析后的地址为 https://m.maoyan.com/ajax/movieOnInfoList?token=&aaa
'/ajax': {
target: 'https://m.maoyan.com',
changeOrigin: true
}
}
}
}
- js里使用反向代理
mounted () {
axios.get('/kerwin/movieOnInfoList?token=&optimus_uuid=840641A09D2711EBBAEFDF7172CF0F5FC3DD4F7550754A5D83394257B26BFECF&optimus_risk_level=71&optimus_code=10').then(res => {
console.log(res.data)
})
}
// 或者
mounted () {
axios.get('/ajax/movieOnInfoList?token=&optimus_uuid=840641A09D2711EBBAEFDF7172CF0F5FC3DD4F7550754A5D83394257B26BFECF&optimus_risk_level=71&optimus_code=10').then(res => {
console.log(res.data)
})
}
alias别名配置
默认 @ ==> src的绝对路径
如需要单独配置,可在vue.config.js中配置
@ is an alias to /src
module.exports = {
configureWebpack: {
resolve: {
alias: {
'assets': '@/assets',
'components': '@/components',
'views': '@/views',
}
}
},
}
注册组件
在根组件App.vue的script标签头部使用 import 引入组件,随后在components中定义组件
<template>
<div>
<input type="text" v-model="mytext">
<button @click="handleClick">add</button>
<ul>
<li v-for="(data,index) in datalist" :key="data">
{{data}}
<button @click="handleDelClick(index)">del</button>
</li>
</ul>
<navbar title="首页" :right="false" @event="handleEvent"></navbar>
<sidebar></sidebar>
</div>
</template>
<script>
/* import 自定义变量名 from '引用的文件路径' */
import navbar from './components/Navbar'
import sidebar from './components/Sidebar'
// import Vue from 'vue' // 在哪儿用,在哪里引入
// 全局组件
// Vue.component('navbar', navbar)
// navbar通过import导入进来的是个对象,所以还得注册,第一个参数是注册组件的名字
// es6 导出
export default {
data () {
return {
mytext: '',
datalist: []
}
},
mounted () {},
components: {
// 局部定义navbar组件
navbar, // navbar : navbar 属性等于属性值可以简写navbar
sidebar
},
methods: {
handleClick () {
this.datalist.push(this.mytext)
},
handleDelClick (index) {
console.log(index)
this.datalist.splice(index, 1)
},
handleEvent (data) {
console.log('父组件定义', data)
}
}
}
</script>
<style scoped lang="scss">
$width:300px;
ul{
li{
width: $width;
background-color: yellow;
}
}
</style>
(注:定义组件中也可使用 “组件名”: 组件 的形式)
在components文件中创建一个文件类型为vue的文件,如 Navbar.vue(components 文件夹下放的是公共组件)
Navbar.vue
<template>
<div>
<button @click="handleClick">left</button>
<span>{{title}}</span>
<button v-show="right">right</button>
</div>
</template>
<script>
export default { // Es6导出
props:{
title:{
type:String,
default: 'navbar'
},
right:{
type:Boolean,
default:true
}
},
methods:{
handleClick(){
this.$emit('event',1000)
}
},
data () { //data 必须是函数写法
}
}
</script>
组件传值
1. 父子传值
在父组件( app.vue )里给子组件标签添加属性。属性名自行定义,需要注意的是属性名和子组件接收props里对应上就可以。
子组件标签属性前面如果有:,那么对应的属性值是一个变量,需要在父组件的data下定义
<template>
<div>
<navbar :title="首页" :right="false" ></navbar>
</div>
</template>
<script>
import navbar from './components/Navbar'
export default {
components: {
// 局部定义navbar组件
navbar, // navbar : navbar 属性等于属性值可以简写navbar
},
methods: {
}
}
</script>
在子组件( Navbar.vue )中使用props来接收这个数据即可
<template>
<div>
<button>left</button>
<span>{{title}}</span>
<button v-show="right">right</button>
</div>
</template>
<script>
export default {
props: {
//对象定义模式,定义类型和默认值
title: {
type: String,
default: 'navbar'
},
right: {
type: Boolean,
default: true
}
}
}
</script>
2. 子父传值
-
在子组件中定义一个事件触发按钮
-
在method定义方法,方法中,$emit定义了一个事件,括号中前者为事件的名称,后者为事件的参数
<template>
<div>
<button @click="handleClick">left</button>
<span>{{title}}</span>
<button v-show="right">right</button>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: 'navbar'
},
right: {
type: Boolean,
default: true
}
},
data () {
return {}
},
methods: {
handleClick () {
//$emit定义了一个事件,括号中前者为事件的名称,后者为事件的参数
this.$emit('event', 1000000)
}
}
}
</script>
- 来到父组件,在调用子组件时使用v-on(简写@)来绑定子组件中定义的事件
- 在父组件(app.vue)的methods中实现handleEvent方法,用来接收子组件中传来的事件,获得数据
<template>
<div>
<navbar title="首页" :right="false" @event="handleEvent"></navbar>
</div>
</template>
<script>
import navbar from './components/Navbar'
export default {
components: {
// 局部定义navbar组件
navbar
},
methods: {
handleEvent (data) {
console.log('父组件定义', data)
}
}
}
</script>
插槽
app.vue
<template>
<div>
<navbar :title="首页" :right="false">
<div>我想插入内容</div>
</navbar>
</div>
</template>
Navbat.vue
<template>
<div>
<slot></slot>
</div>
</template>
九、vue 全家桶
Vue全家桶有哪些技术?
我们可以使用 Vue 框架来开发 SPA,开发时使用的技术:
1、使用 Vue Cli 脚手架工具快速构建项目目录结构、开发环境、部署
2、使用 Vue-Router 实现路由,实现组件之间的切换
3、使用 Vuex 实现状态数据的管理
4、使用 axios 发送 AJAX 和服务器通信
SPA
什么是SPA?
SPA
SPA(单页面应用程序),整个网站只有一个页面,在这个页面中会加载很多个不同的组件,当我们点击按钮时,并不会打开一个新的页面,而是还在当前的页面中切换显示不同的组件。
传统的网站
传统的网站是由 多个独立的页面 组成的,当我们点击页面中的 a 标签时,就会向服务器发送一个新的请求,然后下载另一个页面显示,跳转时是页面之间的跳转。
SPA 的优、缺点
优点
1、减轻服务器的压力:一个网站只有一个页面,只需要从服务器加载一次,并且把大量操作都放到了浏览器中去完成
2、前、后端完成分离,使服务器只需要提供同一套 JSON 数据接口,就可以同时满足WEB端、桌面端、手机端等不同的前端设备,而且前端只关注前端、后端只操作数据,各司其职
缺点
1、首屏加载速度可能会很长
2、SEO(搜索引擎优化)不友好,爬虫数据时什么也抓不到
3、页面复杂度提高、开发难度加大
- 面试:vue 等单页面应用及其优缺点
优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
缺点:不支持低版本的浏览器,最低只支持到 IE9;不利于 SEO 的优化(如果要支持 SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。
vue 文件–SPA中的组件
在使用 Vue 开发 SPA 时,SPA 是由很多个 Vue 的组件组成的,每个组件就是一个 .vue 文件。
每个 .vue 文件中都由三部分组件:HTML、CSS、JS。
SPA 中的 Vue
-
在 spa 中,在组件中
data 必须是一个函数
,而且不能使用 new Vue 创建新的 Vue 对象,因为整个 SPA 中只有一个 Vue 对象 -
在 spa 中需要使用 es6 提供的 import 来引入组件:
import Pagination from "Pagination.vue"
Vue-Router
在 SPA 中,网站内容的变换实际上的组件的切换,为了方便的实现组件间的切换,Vue 框架引入了 vue-router
的工具来实现多个组件之间的切换。
官方文档:https://router.vuejs.org/zh/
路由原理:
(1) hash路由 ==> location.hash 切换
window.onhashchange 监听路径的切换
(2) history路由==> history.pushState 切换
window.onpopstate 监听路径的切换
配置路由
在使用 vue-router 之前,我们需要先配置访问的路径与要显示的组件的对应关系。
我们需要在 router/index.js
文件中进行配置。
默认配置了两个路由:
1、当访问 / 时,显示 Films 组件
2、当访问 /cinemas时,显示 Cinemas组件
import Vue from 'vue'
// 1. 导入官方提供的路由插件
import VueRouter from 'vue-router'
import Films from '@/views/Films' // ..可以换成@, @指向src
import Cinemas from '@/views/Cinemas'
import Center from '@/views/Center'
// 2. 注册使用路由插件
Vue.use(VueRouter) // 在使用的时候内部会定义两个全局组件 router-view router-link
// 3. 路由配置表 配置路由信息,有一个页面,在此处就需要配置一个路由
const routes = [
{
path: '/',
name: 'films', //路由名称
component: Films //对应的组件
},
{
path: '/cinemas', // 就是url,就是未来访问的路径
name: 'cinemas', // 名字随意定义,未来访问的时候可以通过名字来找到对应的路径
component: () => import( '../views/Cinemas.vue')
},
]
// 4. 初始化路由插件并导出
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
说明:
1、about 组件的写法是延迟加载:在访问 /about 路径时才会加载该组件,这样可以提高首屏显示速度
2、 的意思是将这个组件添加到 about 这个组中,当访问 about 这个组件时就会添加所有 about 这个组中的组件
一级路由配置
{
path: '/center',
component: Center // 单文件
}
二级路由
二级路由根据写法不同,可分为并列关系和嵌套关系(嵌套路由)
嵌套路由通常用来做选项卡的切换。类似于下图
{
path: '/films',
component: Film, // 单文件
// 嵌套路由
children: [
{
path: '/films/nowplaying',
component: Nowplaying // 单文件
},
{
path: '/films/comingsoon',
component: Comingsoon // 单文件
}
]
}
路由跳转方式
当我们定义好路由之后,我们就可以在页面中添加按钮跳转到相应的路由,有两种跳转方法:
1、在 HTML 中使用 router-link
标签(相当于a标签)
2、在 JS 中使用 router-push
实现跳转(相当于 location.href )
声明式跳转( router-link)
声明式跳转一般用于html中,使用router-link
组件。
这里有两种写法
写法一(旧版):
vue3 之前,支持这种写法,vue4会移出tag属性
<router-link to="/cinemas" active-class="kerwinactive" tag="li">cinemas</router-link>
to
: 用来指定点击按钮时要切换到的路由,相当于a标签中的"herf"属性,后面跟跳转链接所用
active-class
:是router-link组件中的属性,用来做选中样式的切换;
tag
: router-link 要渲染成的标签
写法二(最新):
tag属性改为插槽用法 v-slot
<router-link to="/films/nowplaying" custom v-slot="{ navigate,isActive }">
<li @click="navigate" >
<span :class="isActive?'kerwinactive':''">正在热映</span>
</li>
</router-link>
to 属性里配置跳转的路由地址
custom 表示支持自定义模板
v-slot 属性里配置固定值"{ navigate,isActive }",navigate表示单击绑定的事件function,由vue提供;isActive 表示是否访问跳转后的路由,默认返回true,这个也是有vue提供。
router-link 标签里的为插槽内容(自定义模板的具体html结构),即router-link组件最终渲染后的html结构。需要注意,绑定事件为固定"navigate",高亮显示通过isActive值进行设置。
总结:
vue提供:router-link, to,custom,v-solt,navigate,isActive。
我们提供:跳转的路由地址,自定义html结构,高亮显示效果
编程式跳转(router.push)
编程式跳转一般用于在js中,使用this.$router.push()
方法来进行
this.$router.push('/detail')
this.$router.back() // 回退到上一个页面
路由容器
我们在使用 vue-router 时,除了要配置路由之后,最重要的是,我们还需要在页面中使用 router-view
标签来指定组件显示的位置:
<template>
<div>
<tabbar></tabbar>
<!-- 留个地方 - 插槽- 路由容器-->
<router-view></router-view>
</div>
</template>
这时所有的路由组件都会显示在 router-view
标签所在的位置上:
1、默认显示路由配置中的第一个路由,也就是 / 路由
2、当切换路由时,也是在这个标签的位置上切换路由
因为我们在 router/index.js
文件的路由是这样配置的:
const routes = [ // 配置路由的数组
{
path: '/films', // 访问路径
component: Films // 对应的组件
},
{
path: '/cinemas',
component: Cinemas
},
{
path: '/center',
component: Center
},
// 重定向
{
path: '*',
redirect: '/films'
}
]
第一个是 home 路由,所以默认显示的就是 Home 组件的内容:
路由重定向
路由重定向通常用来过滤路由,使用redirect
关键字配置。
以下面代码为例,当页面输入/films/123,在films的二级目录下找不到/films/123该路由,但是一级路由是/films。那么就会找到路由3,这个时候路由重定向,路由会跳转到/films/nowplaying。也就是说/films/123 - > /films/nowplaying
当输入任何没有配置的路由,会重定向到films
{
path: '/films',
component: Film, // 单文件
children: [
{
path: '/films/nowplaying', //路由1
component: Nowplaying // 单文件
},
{
path: '/films/comingsoon', //路由2
component: Comingsoon // 单文件
},
{
path: '/films', //路由3
redirect: '/films/nowplaying'
}
],
}
{
path: '/film',
name: 'Film',
component: Film
},
{
path: '/center',
name: 'Center',
component: Center
},
{
path: '/cinema',
name: 'Cinema',
component: Cinema
},
{
path: '*', // * 通配符 优先级最低
redirect: '/films'
}
动态路由
动态路由通常用来路由跳转的过程中传递参数,例如点击列表页某条数据,跳转到详细页。
定义动态路由:
{
path: '/detail/:myid', //动态的二级路由
component: Detail // 单文件
}
跳转到指定动态路由:
// 1- 跳转路径 携带参数
methods: {
handleClick (id) {
// 1-通过路径跳转
this.$router.push(`/detail/${id}`)
}
}
动态路由参数:this.$route.params
获取当前匹配到的detail路由
export default {
created () {
// console.log(location.hash) // 原生获取id
//'利用次id去后端获取详情数据。vue渲染页面'
console.log(this.$route.params.myid) // 当前匹配的路由对象
}
}
- 面试: r o u t e 和 route 和 route和router 的区别 :
r o u t e 是 “ 路 由 信 息 对 象 ” , 包 括 p a t h , p a r a m s , h a s h , q u e r y , f u l l P a t h , m a t c h e d , n a m e 等 路 由 信 息 参 数 。 而 route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。而 route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
axios利用id发请求到详情接口,获取详情数据,布局页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYBKHRc3-1623771327477)(C:\Users\xujing\AppData\Roaming\Typora\typora-user-images\image-20210419151057810.png)]
命名路由
命名路由是用一个名称来标识一个路由,当路由的另一种path方式有时候会名字很长的时候,用命名路由就显得很简单了。跳转路由的时候根据路由的名字name进行跳转
{
name: 'kerwindetail',
path: '/detail/:myid',
component: Detail // 单文件
}
methods: {
handleClick (id) {
// 2-通过命名路由跳转
this.$router.push({
name:'kerwinDetail',
params:{
myid:id
}
})
}
}
路由模式
const router = new VueRouter({
mode: 'history',
routes: [...]
})
hash
vue-router 默认 hash 模式—使用 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载。
(微信有时候转发会自动加上一些#)
history
(好看,分享的时候不会出现问题,不会加上#,但有风险,上线的时候容易出现404错误,由后端配置资源,如果匹配不到资源,重新render到index页面)
这种模式需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 http://oursite.com/user/id
就会返回 404,这就不好看了
如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app 依赖的页面。
路由拦截(守卫)
我们主要是利用vue-router提供的钩子函数beforeEach()对路由进行判断,从而达到路由拦截的效果。
to: Route: 即将要进入的目标路由对象
from: Route: 当前导航正要离开的路由
next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
确保要调用 next 方法,否则钩子就不会被 resolved
全局前置守卫
写法一:利用素组,把需要拦截的路径加在数组里,再进行判断
// 全局,在router/index.js里定义
// router.beforeEach -- router进来之前
router.beforeEach((to, from, next) => { //to:跳到哪 from:从哪来 next:是否放行
console.log(to.fullPath)
const persmisson = ['/order', '/money', '/card', '/center'] // 授权路径
if (persmisson.includes(to.fullPath)) { // 包含授权的路径进行拦截,例:center
// console.log('拦截')
// 登录成功过后, 后端返回加密后的token ,前端可以判断加密token是否有效,
if (localStorage.getItem('token')) {
next('/login') // 跳转到login 页面
} else { //放行
next()
}
} else {
next()
}
})
写法二:在需要拦截的路由里加一个meta属性
{
path: '/center',
component: Center,
meta: {
isRequired: true // isRequired自定义
}
}
// 全局拦截
router.beforeEach((to, from, next) => { // router进来之前
console.log(to)
if (to.meta.isRequired) { // isRequired为true的时候走进来,需要验证
// 判断 本地存储中是否token
if (localStorage.getItem('token')) { // 有token 放行
next()
} else { // 没有token 走进登录页
next('/login')
}
} else {
next()
}
})
路由独享守卫
在路由配置上直接定义 beforeEnter
守卫:
const router = new VueRouter({
routes: [
{
path: '/center',
component: Center,
beforeEnter: (to, from, next) => { // 只拦截当前这一个路由
if(localStorage.getItem('token')){
next()
}else{
next('/login')
}
}
}
]
})
组件内守卫
例:center.vue
export default {
// 路由的钩子函数 当进入组件之前,执行 beforRouteEnter 路由钩子函数。也就是说先执行beforRouteEnter,再执行beforeCreate
beforeRouteEnter (to, from, next) {
// console.log('beforeRouterEnter')
if (localStorage.getItem('token')) { //有token next()放行,没有token 跳到login页面
next()
} else {
next({
path: '/login',
query: { redirect: to.fullPath }
})
}
},
beforeRouteLeave(to,from,next){//离开组件的时候触发
//什么都不写的时候,不会离开(走下一步)
next()
}
}
注:这种方法不能获取组件实例this!!!
路由元信息
定义路由的时候可以配置 meta
字段:
通过 meta
定义要验证的路由
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
全局导航守卫中检查元字段:
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 此路由需要认证,检查是否登录
// 如果不是,重定向到登录页面。
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
总结:无论哪种方案,路由拦截最终都是判断是否有token,有token就next(), 没有就拦截 ;为了记录是从哪来的,可以传一个query字段,有了query ,可以记录从哪来,跳到哪里去。
- 面试:vue 路由的钩子函数
首页可以控制导航跳转,beforeEach,afterEach 等,一般用于页面 title 的修改。一些需要登录才能调整页面的重定向功能。
beforeEach 主要有 3 个参数 to,from,next:
to:route 即将进入的目标路由对象,
from:route 当前导航正要离开的路由
next:function 一定要调用该方法 resolve 这个钩子。执行效果依赖 next 方法的调用参数。可以控制网页的跳转。
路由懒加载
当打包构建应用时,JS包会变得非常大,影响页面加载。把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
-
面试:有一个项目组件特别多的情况下,打开特别慢,有什么解决办法?
可以使用懒加载,把之前的导入写法改为函数式的写法。
component改为回调函数的写法
等路径匹配后,再加载当前页面对应的js文件。主要用于解决首屏加载过慢问题。
// import Cinema from '../views/Cinema.vue' // 不需要引入
{
path: '/cinemas',
component: () => import('../views/Cinema.vue') //懒加载
// 路径匹配后自动指向回调函数,再引入(达到临时的引入的效果)一开始先不引入,等点到路径的时候,再加载。返回值就是一个导入功能
}
只有第一次会懒加载
axios封装
实际项目中不会在组件对axios进行引用,直接暴露在组件中,而是对axios进行进一步的封装。
对于数据请求的封装有两种方案,自己写一个函数,或把公共部分提出来赋值给一个变量
例: 在src目录文件下 创建 —util|http.js
// 方案一:自己函数封装
import axios from 'axios'
function httpForList () {
return axios({
url: 'https://m.maizuo.com/gateway?cityId=440100&pageNum=1&pageSize=10&type=1&k=5196770',
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"1606697250632532718583809","bc":"440100"}',
'X-Host': 'mall.film-ticket.film.list'
}
})
}
function httpForDetail (params) {
return axios({
url: 'https://m.maizuo.com/gateway?filmId=${params}&k=5501344',
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"1606697250632532718583809","bc":"440100"}',
'X-Host': 'mall.film-ticket.film.info'
}
})
}
export default {
httpForList,
httpForDetail
}
// 方案二
import axios from 'axios'
const http = axios.create({
baseURL: 'https://m.maizuo.com/',
timeout: 10000, // 请求超时
headers: { 'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.0.4","e":"1615724226596841540354049","bc":"110100"}' }
})
// http 进行拦截(发请求之前,收到相应之后可以提前拦截???)
export default http
axios拦截器(interceptors)
https://github.com/axios/axios#interceptors
在axios的实例对象(http)里添加拦截器,
import { Toast } from 'vant'
// 用到了vant组件库里的Toast,所以要先引用
// 添加请求拦截器 // 在发请求之前拦截 -- showLoading
http.interceptors.request.use(function (config) {
//在这可以写 请求发送之前做什么
// 显示loading
Toast.loading({
message: '加载中...',
forbidClick: true,
duration: 0 // 展示时长(ms),值为 0 时,toast 不会消失
})
return config
}, function (error) {
// 请求错误时要做什么
return Promise.reject(error)
})
// 添加响应拦截器 // 在成功后拦截 -- hideLoading
http.interceptors.response.use(function (response) {
// 任何处于2xx范围内的状态代码都会触发此函数
// 对响应数据做些什么
// 隐藏loading
Toast.clear()
return { // 在响应成功之前可以添加想要添加的字段
...response,
name:'kerwin'
}
}, function (error) { // 响应失败
// 任何超出2xx范围的状态码都会触发此函数
// 在响应错误时做些什么
// 隐藏loading
Toast.clear()
return Promise.reject(error)
})
Vuex
什么是Vuex
Vuex就是一个管理公共状态的模式,Vuex包含了一套对state的操作规范,集中管理应用的所有组件的状态。
状态管理
-
简单来说就是管理各个组件共享的数据,类似session
-
session可以存数据,存的过程就是管理,数据的每一次赋值就是当次状态。
-
Vuex在Vue实例顶层中。
什么状态需要Vuex去管理?
- 比如用户的登录的状态(token)、用户的信息(头像、名称、地理位置信息)等等
- 比如商品的收藏,购物车的商品等等
- 这些状态应该是响应式的,用户昵称、头像修改了需要响应
Vuex简单模型
-
state,驱动应用的数据源;
-
view,以声明方式将 state 映射到视图;
-
actions,响应在 view 上的用户输入导致的状态变化。
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
- 所以我们需要vuex的规范操作来管理状态。
vuex管理公共状态,store={ cityID :’’ } 其他组件可以使用公共状态,
Vuex工作流
state : Vuex 使用单一状态树, 每个应用仅包含一个 store 实例,单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations : mutations 定义的方法动态修改 Vuex 的 store 中的状态或数据。
getters : 类似 vue 的计算属性,主要用来过滤一些数据。
action : actions 可以理解为通过将 mutations 里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。
使用 Vuex
定义状态数据与方法
使用 Vuex 时,最重要的两部分是:state 和 mutation。
-
state(公共的状态):保存所有组件公共的数据。(数据部分)
-
mutation:保存操作公共数据的方法。(函数部分) 唯一改变状态的地方,只支持同步
在最开始构建项目时,如果在安装时勾选了 vuex 组件,那么就已经安装好了 vuex。
在 --store|index.js 文件中定义 state 和 mutation :
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// state 公共状态
state: {},
// 统一管理状态,被devtools 记录状态的修改
mutations: {}
}
})
使用状态数据
在 --store|index.js 文件中定义好状态数据之后,我们就可以在页面中使用 $store.state.状态数据名称
来读取状态数据的值了:
views/City.vue
<van-cell v-for="item in data.list" :key="item.cityId" :title="item.name"
@click="handleCity(item)" />
handleCity (item) {
this.$store.state.cityName = item.name
// 不能直接修改,直接改无法跟踪状态,后期容易混乱,组件多的情况下会不知道是谁改的
this.$router.back()
},
调用mutation
当我们要修改状态数据时,我们可以使用 this.$store.commit('方法名')
来调用状态函数来修改状态数据:
views/City.vue
handleCity (item) {
this.$store.commit('changeCityName', item.name)
this.$router.back()
},
export default new Vuex.Store({
state: {
cityId: '310100',
cityName: '上海'
},
mutations: {
changeCityName (state, cityname) {
console.log(cityname)
}
}
})
持久化存储(persist)
vuex 默认存储在内存中,所以页面刷新时,数据会丢失
import Vuex from "vuex";
import createPersistedState from "vuex-persistedstate";
const store = new Vuex.Store({
// ...
plugins: [createPersistedState()],
});
-
面试:vuex 是什么?怎么使用?哪种功能场景使用它?
只用来读取的状态集中放在 store 中; 改变状态的方式是提交 mutations, 这是个同步的事物; 异步逻辑应该封装在 action 中。在 main.js 引入 store,注入。新建了一个目录 store,…… export 。场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
Vuex新写法
this.$store.state.状态名字 === …mapState([“title”])
*this.$store.getters.计算属性名字 === …mapGetters([“getFilms”])
字面意思就是把store中state 的值遍历出来,任你取用,就不用写this.$store.getters.getState.openId等这么长的取值了,同理,mapMutations mapGetters也是把store中对应的mutations getters方法遍历出来
// 导入 mapState 函数,只有一个也要加{}
import { mapState, mapActions, mapMutations } from 'vuex'
export default {
data () {
return {
height: '0px'
}
},
computed: {// 把store里的state(状态)映射到自己的计算属性
...mapState(['cinemaList', 'cityId', 'cityName'])
},
methods: { // 把store里的(方法)映射到自己的方法
...mapActions(['getCinemaData']),
...mapMutations(['clearCinema']),
handleLeft () {
this.$router.push('/city')
this.clearCinema()
},
handleRight () {
this.$router.push('/cinemas/search')
}
},
mounted () {
if (this.cinemaList.length === 0) {
this.getCinemaData(this.cityId).then(res => {
this.$nextTick(() => {
new BetterScroll('.box', {
scrollbar: {
fade: true
}
})
})
})
} else {
console.log('缓存')
this.$nextTick(() => {
new BetterScroll('.box', {
scrollbar: {
fade: true
}
})
})
}
}
}
总结:
(1)应用层级的状态应该集中到单个 store 对象中。
(2)提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
(3)异步逻辑都应该封装到 action 里面。
十、betterScroll
主要完成的功能需要包含Better-Scroll实现页面中拖动滚动、拉动属性等功能 。是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。
https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/
1. 初始化
//html
<div class="kerwin" >
<ul >
<li v‐for="item in datalist">{{item}}</li>
</ul>
<div class="loading‐wrapper"></div>
</div>
import BScroll from 'better‐scroll'
//mounted生命周期直接调用,不需要异步结束
this.height = document.documentElement.clientHeight‐x+"px" //动态计算高度
this.$nextTick(()=>{
//以上状态更新完后,再初始化better scroll
var myscroll = new BScroll('.kerwin', {
pullDownRefresh: {
threshold: 50,
stop: 20
},
scrollbar: {
fade: true,
interactive: false // 1.8.0 新增
},
pullUpLoad: {
threshold: 50
},
click:true //事件生效
})
})
.kerwin{
height: 300px; //设置高度
overflow:hidden; //溢出隐藏
position: relative; //修正滚动条位置
}
2. 下拉刷新
myscroll.on('pullingDown',()=>{
console.log("下拉了")
setTimeout(() => {
myscroll.finishPullDown() // 自动调用 .refresh()
}, 1000)
})
3. 上拉加载
myscroll.on('pullingUp',()=>{
console.log("到底了")
setTimeout(() => {
myscroll.finishPullUp() // 自动调用 .refresh()
}, 1000)
})
十一 UI组件库
为了提高开发效率,我们会选择引入组件库
-
散落在角落里组件,github 或 百度 搜的, 例:swiper,betterscroll……
-
UI组件库
PC端: element: https://element.eleme.cn/#/zh-CN (饿了么团队) ……
mobile: vant:https://vant-contrib.gitee.io/vant/#/zh-CN/ (有赞团队) ……
vant
使用
- 下载
- 引入
- 复制组件
- 看不懂组件上的属性是干嘛用的,,,
- 查文档
- 增减属性,实现自己的效果
- 让别人看不懂
- 查文档
在入口文件引入vant所有组件库
//方式一:引入vant所有组件库 (不推荐,因为会引太多,浪费)
import Vant from 'vant'
import 'vant/lib/index.css' // 导入css
Vue.use(Vant)
//方式二: 按需引入(推荐)
// 1. 安装插件 npm i babel-plugin-import -D
// 2. 在 babel.config.js 中配置
module.exports = {
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
};
// 3. 单独引入要用的组件,在哪用在哪引 例:引用button
import Vue from 'vue';
import { Button } from 'vant';
Vue.use(Button);
懒加载
利用vant组件库实现电影列表懒加载,当列表即将滚动到底部时,会触发事件并加载更多列表项。
1.引入
import Vue from 'vue'
import { List, Cell } from 'vant'
Vue.use(List).use(Cell)
2.基础用法
List 组件通过 loading
和 finished
两个变量控制加载状态,当组件滚动到底部时,会触发 load
事件并将 loading
设置成 true
。此时可以发起异步操作并更新数据,数据更新完毕后,将 loading
设置成 false
即可。若数据已全部加载完毕,则直接将 finished
设置成 true
即可。
<van-list v-model="loading"
:finished="finished"
finished-text="没有更多了"
:immediate-check="false"
@load="onLoad">
<van-cell v-for="data in datalist"
:key="data.filmId"
@click="handleClick(data.filmId)"
class="myli">
<img :src="data.poster" />
<div class="content">
<div class="title">
{{ data.name }} <span>{{ data.filmType.name }}</span>
</div>
<div class="grade"
:class="data.grade ? '' : 'hidden'">
观众评分:<span style="color: #ffb232; font-size: 14px">{{
data.grade
}}</span>
</div>
<div class="actors">主演:{{ data.actors | actorFilter }}</div>
<div class="">{{ data.nation }} |{{ data.runtime }}分钟</div>
</div>
<div class="nowplaybtn">购票</div>
</van-cell>
</van-list>
export default {
data () {
return {
datalist: [],
loading: false, // true表示正在加载中,防止频繁触发
finished: false, // true表示没有更多数据
pageNum: 1,
total: 0
}
},
mounted () {
http({
url: '/gateway?cityId=310100&pageNum=1&pageSize=10&type=1&k=2063761',
headers: {
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res => {
this.datalist = res.data.data.films
this.total = res.data.data.total
})
},
methods: {
onLoad () { // 每次到底触发onLoad
if (this.datalist.length === this.total && this.total !== 0) {
this.finished = true
return
}
this.pageNum++
http({
url: `/gateway?cityId=310100&pageNum=${this.pageNum}&pageSize=10&type=1&k=2063761`,
headers: {
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res => {
// 取出来的新数据需要和原来的数据进行合并,否则会覆盖, es6合并数组 [...arr]
this.datalist = [...this.datalist, ...res.data.data.films]
this.loading = false // 为了下一次触发
})
},
handleClick (id) {
this.$router.push(`/detail/${id}`)
}
}
}
懒加载中遇到的两个问题:
1.没有数据后会不断的请求数据,没有数据取回来的每次都是个空数组, datalist没有变化,继续执行loading为false,数据加载完毕,触发下一次加载,再次发起ajax , this.pageNum++, 后面没有数据,每次都是空数组,每次合并相当于没合,数据长度没有将列表撑开,列表还是在底部,出现无限的超后端发起请求,解决办法,判断datalist的长度等于后端数据的总长度是时候,数据已经取完,返回return,不再执行
2.点击其他页面,当其他页面出现滚动条的时候,再返回原来的页面时,懒加载已经判断完到底了,onload是立即触发,先走的mounded,ajax异步回来,此时taotal初始是0,datalist空数组, 0=0 ===> finished=true,直接到底了,解决办法:判断条件total或datalist.length不等于0
十二、Vue3
介绍
Vue3.0设计目标 :
-
更小
- 全局 API 和内置组件 / 功能支持 tree-shaking
- 常驻的代码尺寸控制在 10kb gzipped 上下
-
更快
-
基于 Proxy 的变动侦测,性能整体优于 getter / setter
-
Virtual DOM 重构
-
编译器架构重构,更多的编译时优化
-
-
加强API设计一致性
-
加强TypeScript支持
-
提高自身可维护性
-
代码采用 monorepo 结构,内部分层更清晰
-
TypeScript 使得外部贡献者更有信心做改动
-
-
开放更多底层功能。
vue3(main.js)写法:
import {createApp} from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
createApp(App)
.use(router)
.use(store)
.use(Vant)
.mount('#app');
vue3 (router)写法:
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../views/Home.vue';
const routes = [
{
path:'/',
name:'Home',
component:Home
},
{
path:'/:kerwin',
redirect:{
name:'Home' // 命名路由写法
}
}
];
export default createRouter({
// history:createWebHashHistory(), // history 模式
history:createWebHashHistory(), // hash模式
routes
});
注:vue3中重定向 path: * 换成了动态路由写法,重定向换成命名路由
vue3x(store)写法:
import { createStore } from 'vuex';
export default createStore({
state:{},
mutation:{},
action:{},
modules:{}
});
vue3中 router-link 不支持 tag 属性 ,只支持以下写法
<router-link to="/films/nowplaying" custom v-slot="{ navigate,isActive }">
<li @click="navigate" >
<span :class="isActive?'kerwinactive':''">正在热映</span>
</li>
</router-link>
注:vue3 热更新有问题,需要及时手动刷新页面
Composition API
起初定义的是Vue-Function-API,后经过社区意见收集,更名为Vue-Composition-API.
reactive
作用:创建响应式对象,非包装对象,可以认为是模板中的状态。
{{obj.myname}} -- <button @click="handleClick()">change</button>
import {reactive} from 'vue'
export default {
// vue3老写法或者vue 写法 中 beforeCreeate,created 生命周期=== setup
setup(){
//定义状态
const obj = reactive({
myname:"kerwin",
myage:100
})
const handleClick=()=>{
obj.myname = 'xiaoming'
}
return {
obj,
handleClick
}
}
}
注:
- template 可以放兄弟节点
- reactive 类似useState,如果参数是字符串,数字,会报警告,value cannot be made reactive,所以应该设置对象,这样可以数据驱动页面
例:
import {reactive} from 'vue'
export default {
setup(){
const obj = reactive({
mylist:[]
})
const datalist = reactive([])
return {
obj,
datalist
}
}
}
案例:todo
<input type="text" v-model="obj.mytext"/>
<button @click='handleAdd'>add</button>
<ul>
<li v-for='data in obj2.datalist' :key="data">
{{data}}
</li>
</ul>
import {reactive,ref} from 'vue'
export default {
data(){
return {
myname:"kerwin"
}
},
setup(){
const obj = reactive({
mytext:'',
})
const obj2 = reactive({
datalist:[]
})
const handleAdd = ()=>{
obj2.datalist.push(obj.mytext)
obj.mytext = ''
}
return {
obj,
obj2,
handleAdd
}
}
}
ref
作用:创建一个包装式对象,含有一个响应式属性value。它和reactive的差别,就是前者没有包装属性value
{{myname}} -- <button @click="handleClick()">change</button>
import {ref} from 'vue'
export default {
setup(){
const myname = ref("kerwin") // .value 属性
// console.log(myname)
const handleClick = ()=>{
myname.value = "xiaoming"
}
return {
handleClick,
myname
}
}
}
案例:todo
<input type="text" ref="mytext">
<button @click="handleAdd">add</button>
<ul>
<li v-for="data in datalist" :key="data">
{{data}}
</li>
</ul>
import {ref} from 'vue'
export default {
setup(){
const mytext= ref()
const datalist = ref(["111","222"])
const handleAdd = ()=>{
datalist.value.push(mytext.value.value)
}
return {
mytext,
handleAdd,
datalist
}
}
}
toRefs
默认直接展开state ,那么此时reactive 数据变成普通数据,通过toRefs,可以把reactive里的每个属性,转化为ref对象,这样展开后,就会变成多个ref对象,依然具有响应式特性。
{{myname}}-{{myage}}
<button @click="handleClick()">chnage</button>
import {reactive,toRefs} from 'vue'
export default {
// vue3老写法或者vue 写法 中 beforeCreeate,created 生命周期=== setup
setup(){
//定义状态
const obj = reactive({
myname:"kerwin",
myage:100
})
const handleClick=()=>{
obj.myname = 'xiaoming'
}
return {
...toRefs(obj),
handleClick
}
}
}
props
<navbar myname="home" myid="111" @event= "change"></navbar>
<sidebar v-show="obj.isShow"></sidebar>
import navbar from './components/navbar'
import sidebar from './components/sidebar'
import {reactive} from 'vue'
export default {
components:{
navbar,
sidebar
},
setup(){
const obj = reactive({
isShow:true
})
const change = ()=>{
obj.isShow = !obj.isShow
}
return {
obj,
change
}
}
}
生命周期
<ul>
<li v-for="data in obj.list" :key="data">
{{data}}
</li>
</ul>
import { onBeforeMount, onMounted, reactive } from 'vue'
export default {
setup(){
const obj = reactive({
list:[]
})
onBeforeMount(()=>{
console.log("onBeforeMount")
})
onMounted(()=>{
console.log("dom上树",`axios,事件监听, setInterval,,,,,,`)
setTimeout(()=>{
obj.list = ["aaa","vvvv","cccc"]
},2000)
})
return {
obj
}
}
}
计算属性
<input type="text" v-model="obj.mytext"/>
<ul>
<li v-for="data in computedList" :key="data">
{{data}}
</li>
</ul>
{{filterlist()}}
{{filterlist()}}
{{computedList}}
{{computedList}}
import { reactive,computed } from 'vue'
export default {
setup(){
const obj = reactive({
mytext:'',
datalist:["aaa","abb","abc","bbb","bcc","add","bcd"]
})
const filterlist = ()=>{
console.log("filterlist")
return obj.datalist.filter(item=>item.includes(obj.mytext))
}
const computedList = computed(()=>{
console.log("computedList")
return obj.datalist.filter(item=>item.includes(obj.mytext))
})
return {
obj,
filterlist,
computedList
}
},
computed:{
aaa(){
return "111111"
}
}
}
监听属性
<input type="text" v-model="obj.mytext" />
<ul>
<li v-for="data in obj.datalist" :key="data">
{{ data }}
</li>
</ul>
import { reactive, watch } from "vue";
export default {
setup() {
const obj = reactive({
mytext: "",
datalist: ["aaa", "abb", "abc", "bbb", "bcc", "add", "bcd"],
oldlist: ["aaa", "abb", "abc", "bbb", "bcc", "add", "bcd"],
});
watch(
() => obj.mytext,
() => {
obj.datalist = obj.oldlist.filter((item) => item.includes(obj.mytext));
}
);
const handleInput = () => {};
return {
obj,
handleInput,
};
},
watch:{
mytext(){
}
}
};
自定义hook
虽然composition api 比之前写法更麻烦了,但是用上自定义hooks就可以实现函数编程的复用了。
import { ref } from 'vue';
function useCount () {
const count = ref(1);
const addCount = (num = 1) => count.value += num;
return {
count,
addCount
}
}
export { useCount }
路由
配置
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(''), // hash模式
routes
})
const router = createRouter({
history: createWebHistory('/v5'),//history模式
routes
})
// 重定向
{
path: '/:kerwin',
redirect: {
name: 'film' //命名路由写法
}
}
获取$router
import { getCurrentInstance } from 'vue-router'
setup () {
// const router = useRouter() vue-router中的useRouter直接获取router对象
const { ctx } = getCurrentInstance() // 必须setup中定义
// ctx.$router == this.$router(之前写法)
// 编程式导航
ctx.$router.push('/about')
}
// 获取动态路由参数
setup(){
const router = useRouter()
console.log(router.currentRoute.value.params.id);
}
// 第二种方案
import {useRouter} from 'vue-router'
const router = useRouter() // 被proxy拦截代理的proxy对象,可以直接访问属性
console.log(router.params.id);
vuex
入口配置 createApp(App).use(router).use(store).mount(#app)
{{storeCount}}
setup(){
// const store = useStore() vuex中的useStore直接获取store对象
// store.commit
// store.dispath
// store.state
const { ctx } = getCurrentInstance()
const storeCount = computed(()=>ctx.$store.state.count)
add(){
ctx.$store.commit("addMutation")
}
return{
storeCount
}
}
// store/index.js
export default Vuex.createStore({
state:{
count:1
},
mutations:{
addMutation(state){
state.count++
}
},
actions:{
},
modules:{
}
})
不能使用mapMutations, mapState…, 因为依赖于this.$store
vuex替代方案
provide、inject是 vue-composition-api 的一个新功能:依赖注入功能
import { provide,inject} from 'vue'
// 根组件 共享自己的状态
const kerwinshow = ref(true)
provide('kerwinshow',kerwinshow)
// detail组件
onMounted(()=>{
const kerwinshow = inject('kerwinshow')
kerwinshow.value = false
})
app
<ul>
<li v-for="data in obj1.list" :key="data.name">
{{data.name}}
</li>
</ul>
<ul>
<li v-for="data in obj2.list" :key="data.title">
{{data.title}}
</li>
</ul>
import { getData1,getData2 } from './module/app'
export default {
setup(){
const obj1 = getData1()
const obj2 = getData2()
return {
obj1,
obj2
}
}
}
注:
vue3中没有过滤器写法,换回函数式写法
vue3中指令生命周期约等于组件生命周期
vue3中的destroyed生命周期换成 unmounted , beforeDestroy 换成 beforeUnmounted
vue3中vant组件的使用,参考vant官方文档 ,需安装 Vant 3 : npm i vant@next -S