vue_t_v14
2022/3/22 6:29:11
本文主要是介绍vue_t_v14,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
VUE
vue框架的两大核心:
数据驱动和组件化。
第一周:大家需要转变编程思维的习惯:数据驱动(数据的变化会驱动页面发生变化,不用操作DOM)
第二周:大家需要转变编程思维的习惯:组件化(封装的是页面)
一、前端开发历史
1994年可以看做前端历史的起点(但是没有前端的叫法)
1995年网景推出了JavaScript
1996年微软推出了iframe标签, 实现了异步的局部加载
1999年W3C发布第四代HTML标准,微软推出用于异步数据传输的 ActiveX(ActiveXObject),各大浏览器厂商模仿实现了 XMLHttpRequest(这是前端的真正的起始)
2006年,XMLHttpRequest被W3C正式纳入标准(这是前端正式的起始,叫作富(胖)客户端)
2006年, 发布了jQuery
2008年问世的谷歌V8引擎,发布H5的草案
2009年发布了第五代JavaScript
2009年 AngularJS 诞生
2010年 backbone.js 诞生
2011年React和Ember诞生
2014年Vue.js诞生
2014年,第五代HTML标准发布
2014年左右,4G时代到来,混合开发(js, android, ios混合开发)
2016年 微信小程序诞生
2017年 微信小程序正式投入运营
2017年底年 微信小游戏
以前的三大框架: angular, react, vue,现在: react, vue, 小程序(微信、支付宝、百度、头条)
以后: js ----- ts (typescript)
二、MV*模式
库 VS 框架
把一小部分通用的业务逻辑进行封装(函数),多个封装形成一个模块或者文件,多个模块或者文件就发展成为库或者框架
库:函数库,不会改变编程的思想,如:jQuery。
框架:框架改变了编码思想,代码的整体结构,如:vue,react,小程序等等。
MVC架构模式
MVC的出现是用在后端(全栈时代)
M:model,模型,主要完成业务功能,在数据库相关的项目中,数据库的增删改查属于模型(重点)。(nodeJS,不含html的php文件),没有页面,是纯粹的逻辑
V:view,视图,主要负责数据的显示(HTML+CSS,动态网页(jsp,含有html的php文件))页面的展示和用户的交互。
C:controller,控制器,主要负责每个业务的核心流程,在项目中体现在路由以及中间件上(nodeJS)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGNUZoYm-1647875628960)(.\img\01mvc.png)]
优点:耦合度低、复用性高、生命周期成本低、部署快、可维护性高、有利软件工程化管理
缺点:由于模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难。
MVP架构模式
MVP是单词Model View Presenter的首字母的缩写,分别表示数据层、视图层、发布层,它是MVC架构的一种演变。作为一种新的模式,
M:model,模型,主要完成业务功能,在数据库相关的项目中,数据库的增删改查属于模型(重点)。
V:view,视图,主要负责数据的显示
P:Presenter负责业务流程的逻辑的处理,Presenter是从Model中获取数据并提供给View的层,Presenter还负责处理后端任务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-APL4OT2p-1647875628962)(.\img\02mvp.png)]
MVP模式与MVC模式的区别:
在MVP中View并不直接使用Model,而在MVC中View可以绕过Controller从直接Model中读取数据。
MVVM架构模式
MVVM是Model-View-ViewModel的缩写,MVVM模式把Presenter改名为ViewModel,基本与MVP模式相似。
唯一区别是:MVVM采用数据双向绑定的方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cJGABu2e-1647875628964)(.\img\03mvvm.png)]
vue是MVVM
MVC衍生出很多变体,MVP,MVVM,MV*, VUE是MVVM,M----V----VM,M数据,V视图, VM是负责M与V相互通信
总结:
架构只是一种思维方式,不管是MVC,MVP,还是MVVM,都只是一种思考问题解决问题的思维,其目的是要解决编程过程中,模块内部高内聚,模块与模块之间低耦合,可维护性,易测试等问题。
架构在于,做好代码的分工,配合。
三、开发工具
vscode,webstorm,HbuilderX等等
四、vue框架的初识
vue官网](https://cn.vuejs.org/)
作者:尤雨溪 ( 华人 ) , 前Google员工
vue的介绍
-
构建数据驱动的web应用开发框架
-
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架
-
Vue 被设计为可以自底向上逐层应用
-
Vue 的核心库只关注视图层
-
vue框架的核心:数据驱动和组件化
-
便于与第三方库或既有项目整合,另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,
Vue 也完全能够为复杂的单页应用程序提供驱动。 -
Vue 不支持 IE8 及以下版本
vue的示例代码
<body> V <div id="app"> 要被控制的html{{key}} </div> </body> <script src="vue.js"></script> <script> let vm = new Vue({ el:'#app' //el:element。要控制的那片html代码 data:{ key:value }//数据 M }) </script>
vue框架的理解
1、首先:一个页面包括:结构(HTML模板),表现(css),行为(js)
2、其次:原生JS中的做法:写好HTML的模板和css样式,用js产生数据(逻辑),通过dom的方式控制数据显示在HTML中的那个位置,包括如何显示(DOM的方式改变样式)
3、vue框架:vue中,写好HTML模板,声明式地告知vuejs库,数据显示在何处,在vue对象中处理数据,不用做DOM操作(vuejs框架负责)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuVJMagr-1647875628965)(\img\04vuejs.png)]
简单理解:new出来一个Vue的实例,传一堆配置参数,控制一片html
记住一点:有了vue,不用程序员操作dom了,因为,vue把dom操作都做好了。
数据驱动和声明式渲染
1、数据驱动
数据驱动,就是通过控制数据的变化来改变(驱动)DOM。背后使用了观察者模式,靠数据的变化来控制页面的变化。这是vue框架的核心,第一周,一定要把数据驱动的思路和编程习惯养成。
2、声明式渲染
声明的意思就是告知,广而告之,即告知程序(Vue框架),在何处渲染什么数据
Vue 实现数据绑定(响应式)的原理(面试题)
vue数据绑定是通过 数据劫持和观察者模式 的方式来实现的
1、数据劫持: vue2.× 使用Object.defineProperty(); Vue3使用的是proxy。
当你把一个普通的 JavaScript 对象(json)传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
目的是:感知属性的变化。当给属性赋值时,程序是能够感知的(知道的)。如果知道的话,就可以控制属性值的有效范围,也可以改变其它属性的值等,在Vue中也可以去改变模板上的显示。
Object.defineProperty()函数:https://blog.csdn.net/jiang7701037/article/details/102785223
2、观察者模式(发布订阅模式):
目的:当属性发生变化时,所有使用该数据地方(订阅)跟着变化
数据绑定:涵盖了:数据绑定,响应式。
五、vue语法(view层)
data选项(model层)
功能:
data选项中存储的是页面中需要显示的数据(当然数据可以进行一定的加工后再显示)
是初始化数据的位置,是元数据,是vue实例的一个实例属性,可以接受的数据类型: number/string/boolean/array/json/undefined/null/function
数据绑定
把data中的数据,显示在html中的某处。
https://cn.vuejs.org/v2/guide/syntax.html#%E6%8F%92%E5%80%BC
插值表达式
功能:可以让html标签里的内容变成动态的(从data中获取), 使用 mustache语法
格式:{{变量|属性名|表达式|函数调用等等}};
示例:
<div id="app"> <span>Message: {{ msg }}</span> </div> new Vue({ el:"#app" data:{ msg:"hello 哥们!" } });
这种写法叫作:声明式渲染,即:只需要告知vue,数据在何处显示。
内容指令
内容指令:让标签的内容变成动态的。
指令名:v-text 和 v-html
指令是什么? 指令就是html标签的自定义属性
1、v-text="数据名"
转义输出
功能:可以让html标签里的内容变成动态的(从data中获取),相当于innerText。
示例:
<p v-text="str">内容:</p>
对比v-text和插值({{}})表达式:
1)、当网络速度慢的时候(vueJs还没有处理自己的语法(v-text和{{}}),插值表达式会在页面上出现 {{}} 的显示,但是指令v-text不会,因为,v-text是自定义属性,最多不做解读。当,标签中的内容全部来自于属性,那么,可以使用v-text。
2)、插值表达式更加灵活,可以在标签里面的某个地方显示,但是v-text会让整个标签的内容全部变化。
2、v-html="数据"
非转义输出(原封不动进行替换)
功能:可以让html标签里的内容变成动态的(从data中获取),但是不会对内容中的html标签进行转义。相当于innerHTML。
示例:
输出真正的 HTML <p v-html="str">内容:</p>
注
双花括号和 v-text(相当于innerText)会将数据解释为普通文本。
v-html相当于innerHTML
属性指令
属性指令:让标签的属性部分变成动态的。
指令名:v-bind
功能:可以让html标签里的**属性(名和值)**变成动态的(从data中获取)
1、 属性值动态绑定:v-bind:html属性="数据"
简写 :html属性=“数据”`
2、 属性名动态绑定: v-bind:[属性名]="数据"
示例:
<div v-bind:id="dynamicId"></div>
注意: 属性值是布尔值 的情况
<button v-bind:disabled="isButtonDisabled">Button</button> 如果 isButtonDisabled 的值是 null、undefined 或 false,则 disabled 属性甚至不会被包含在渲染出来的 元素中
javascript表达式
在dom里面插入数据,除了可以写原始的数据,还可以使用javascript表达式
格式:{{数据+表达式}}
、 v-指令="数据+表达式"
示例:
{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ 'i love you'.split('').reverse().join('') }} <div v-bind:id="'list-' + id"></div>
注意:
不能使用语句 ,条件语句可以使用三元表达式代替
条件渲染(指令)
https://cn.vuejs.org/v2/guide/conditional.html
指令名: v-if 和 v-show
功能:一段dom可以根据条件进行渲染
<div v-if="false">box2</div> <div v-show="true">box1</div>
v-show
VS v-if
v-show=“布尔” | v-if=“布尔” | |
---|---|---|
(原理上)区别 | 操作css的display属性 | 操作dom(添加和删除dom完成的) |
使用场景 | 适合频繁切换 | 适合不频繁切换 |
性能消耗 | 初始渲染消耗(就算元素不显示,也得创建元素) | 频繁切换消耗 |
面试题: v-if和 v-show的区别?
相同点:
v-show和 v-if都是 控制 dom元素 的 显示和隐藏 的。
不同点:
1、原理:
v-show是通过控制元素的样式属性display的值,来完成显示和隐藏;
v-if是通过对dom元素的添加和删除,完成显示和隐藏
2、使用场景:由原理(做法)得出使用场景的区别
v-show:使用在dom元素频繁切换的场景
v-if:当dom元素的切换不频繁,可以使用。特别是,首次元素处于隐藏的情况下。
补充: dom的操作(如:创建和删除)是非常耗性能的。为什么?
请看:https://blog.csdn.net/jiang7701037/article/details/98516468
另外,v-if指令还可以结合v-else-if , v-else一起使用。
示例:
<p v-if="age<10"> 宝宝 </p> <p v-else-if="age<18">大宝宝</p> <p v-else>非宝宝</p>
列表渲染(循环指令)
https://cn.vuejs.org/v2/guide/list.html
指令名: v-for
功能:把数据进行循环显示在html里(渲染)。推荐操作的数据类型:数组、对象、字符串、数字
格式:
用in或者用of都可以: <li v-for="值 in 数据" v-bind:属性="值">{{值}}</li> <li v-for="值 of 数据">{{值}}</li>
各种情况:
<li v-for="(值,索引) in 数组">{{值}}/{{索引}}</li> <li v-for="(对象,索引) in 数组">{{对象.key}}/{{索引}}</li> <li v-for="(值,键) in 对象"> <li v-for="(数,索引) in 数字"> <li v-for="(单字符,索引) in 字符串">
注意:
1、空数组,null,undefined不循环
2、也可以进行循环嵌套
3、v-for和v-if使用在同一个标签里时,v-for 的优先级比 v-if 更高,即:v-for套着v-if,先循环再判断
面试题:为什么不建议把v-for和v-if连用?
https://blog.csdn.net/jiang7701037/article/details/114954542
列表渲染时的key:
在标签里使用属性key,可以唯一标识一个元素。
1、当 Vue.js 用 v-for **更新**已渲染过的元素列表时,它默认用“就地复用”策略。即:**尽量不进行dom元素的操作,只替换文本**。
2、如果你希望进行dom操作,就用key(key不要使用下标),因为key的目的是为了唯一标识一个元素。
有了key后,可以跟踪每个节点的身份,从而重用和重新排序现有元素
建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为(就地复用)以获取性能上的提升。
注意:
key不要使用(数组)下标,并不是会报错,而是失去了唯一标识的意义
事件绑定(指令)
https://cn.vuejs.org/v2/guide/events.html
指令名:v-on
v-on指令可以简写为:@
功能:绑定事件,vue通过v-on
指令把事件和处理函数进行绑定。
事件处理函数需要放在methods
选项当中去,事件名 不带on,函数可以按照ES5的写法,也可以按照ES6的写法。
格式:
<input type="button" v-on:事件名="方法" > <input type="button" v-on:事件名="方法(参数)" > <input type="button" v-on:事件名="方法($event,参数)">
new Vue({ el:, data:{} methods:{ 方法1:function(ev,参数){ 业务 这里面的this是vue对象本身 } 方法2(ev,参数){ 业务 } } })
获取事件对象:
1、 不传参,默认第一个参数,就是事件对象
如:
<button type="button" @click="fn02()">修改数据</button> ……………… fn02(ev){ console.log('事件对象',ev); },
2、 传参,事件对象需要手动传入(使用vue框架官方提供的 $event)
如:
<button type="button" @click="fn03('qq',$event)">修改数据</button> ……………… fn03(str,ev){ console.log('事件对象',ev); console.log('参数',str); },
事件处理函数的this:
1、methods选项里的函数里的this都是vue对象本身,所以,事件处理函数里的this也是vue对象本身
2、vue提供的选项的值如果是函数时,不可用箭头函数 , 会造成this丢失
事件修饰符
<div @click.修饰符="函数"></div> .stop 阻止单击事件继续传播(阻止事件流) .prevent 阻止默认行为 .capture 使用事件捕获模式 .self 点到自己时才触发,不是从其它地方(事件流的流)流过来的 .once 只会触发一次 .passive onScroll事件 结束时触发一次,不会频繁触发,移动端使用
注意:
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用
v-on:click.stop.self
会阻止所有的点击,而 `v-on:click.self.stop 阻止自身的点击,stop就不起作用了
<div class="greenbox" @click="fnGreen"> <!-- <div class="redbox" @click.self.stop="fnRed"> --> <div class="redbox" @click.stop.self="fnRed"> <div class="bluebox" @click="fnBlue"></div> </div> </div>
按键修饰符
<!--普通键--> <div @keyup.修饰符="函数"></div> .left 左 .enter 回车 .13 可以使用键码 <!--系统(组合)键--> <div @keyup.修饰符="函数"></div> .ctrl .alt .shift .exact 精准控制,@键盘事件.修饰符1.修饰符2.exact 只有1+2才可触发 1+2+3不可以 <!--鼠标--> <div @mousedown.修饰符="函数"></div> .left .right .middle 鼠标中键
双向绑定(指令)_表单控件绑定
https://cn.vuejs.org/v2/guide/forms.html
指令名:v-model
功能:视图控制数据,数据也可控制视图,这就是双向绑定,可通过属性+事件来实现双向绑定。而vue中直接提供了一个指令v-model直接完成(v-model 本质上不过是语法糖)。v-model指令只能使用在表单元素上。
不使用v-model完成双向绑定 <input type="text" :value="data数据" v-on:input="changeIptValue"> 使用v-model完成双向绑定 <input type="text" v-model="data数据">
其它表单元素的双向绑定
https://cn.vuejs.org/v2/guide/forms.html
表单修饰符
<input v-model.修饰符="数据" /> .number 把标签的值转成数字赋给vue的数据 .trim 删除前后空格 .lazy 确认时才修改model数据
示例:
//需求: //在下拉框里选择 房间类型(一个房子住多少人,即:几人间) //动态产生 相应数量的文本框 <select v-model.number="num" > <option value="1">1人间</option> <option value="2">2人间</option> <option value="3">3人间</option> </select> <hr> <input v-for="i in num" type="text" :value="i" >
指令总结
指令 (Directives) 是带有 v- 前缀的特殊属性。其实就是html标签的里的自定义属性。指令属性的值预期是单个 JavaScript 表达式 (v-for 是例外情况)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM,以下是官方的指令,程序员也可以自定义指令。
常见指令:
- v-text: 更新元素的 textContent。如果要更新部分的 textContent ,需要使用 {{ Mustache }} 插值。
- v-html:更新元素的 innerHTML
- v-bind:动态地绑定一个或多个属性(特性),或一个组件 prop 到表达式。
- v-on:绑定事件监听器。事件类型由参数指定。
- v-model:在表单控件或者组件上创建双向绑定v-show:根据表达式值的真假,切换元素的 display (CSS 属性)。
- v-if:根据表达式的值的真假条件渲染元素(与编程语言中的if是同样的意思)
- v-else:表示否则(与编程语言中的else是同样的意思)
- v-else-if:(与编程语言中的else if是同样的意思)
- v-for:可以循环数组,对象,字符串,数字,
- v-pre:跳过这个元素和它的子元素的编译过程(vue处理的过程)。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
- v-cloak:防闪烁,模板没编译完,电脑配置差,网速慢等等,有可能会看到{{}},体验不佳,不如用css先隐藏,之后再显示,包括被包裹的子元素。这个指令保持在元素上直到关联实例结束编译。和 CSS 规则,如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。
- v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
指令缩写:
v-bind 缩写 是冒号
v-on 缩写是 @
样式操作
https://cn.vuejs.org/v2/guide/class-and-style.html
操作样式,就是属性绑定,只不过绑定的属性是class和style,vue在class和style属性上做了加强,给样式属性赋的值还可以是对象,数组。
绑定姿势(格式)
<div v-bind:class="数据|属性|变量|表达式"></div> <div v-bind:style="数据|属性|变量|表达式"></div>
属性值的类型支持
字符串/对象 / 数组
<div :class="{active:true,t1:false}"></div> <div :style="[{css属性名:值},{css属性名小驼峰:值}]"></div>
class的属性值(对象的方式)示例:
// <div class='big-box' :class="{active:isActive,borderbox:isBorder}"></div> let vm = new Vue({ el: "#app", data: { isActive:true, isBorder:true } });
注:vue绑定的class属性和普通的class属性可以共存
class的属性值(数组的方式)示例:
<div v-bind:class="[activeClass, errorClass]"></div> data: { activeClass: 'active', errorClass: 'text-danger' }
style的属性值(对象的方式)示例:
<div id="app"> <p v-bind:style="str">我是个p</p> <p v-bind:style="{ width:widthVal+'px',height:'100px',backgroundColor:'blue'}">我是p</p> <input type="button" value="测试" @click="fn"> </div> let vm = new Vue({ el: "#app", data: { // style属性的值是个对象 str: { width:"200px",height:"100px",backgroundColor:"blue"}, widthVal:200 }, methods:{ fn(){ this.str.width="250px"; this.widthVal ++; } } });
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> data: { activeColor: 'red', fontSize: 30 }
style的属性值(数组的方式)示例:
<div :style="[obj1,obj2]"> 我是div</div> data: { obj1:{ width:"200px", height:"150px" }, obj2:{ "background-color":"red" } }
示例:
todoList 练习的实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6myV0IKo-1647875628967)(img\1594365849928.png)]
为了让大家体会vue是数据驱动,根据班级情况尝试扩展如下:
二阶段的运动
全选反选(购物车)
非响应式情况
什么是响应式?数据a发生变化了,那么界面使用a的所有地方也会变化(依赖a的所有的数据和界面都会发生变化),背后使用了观察者模式。
什么是非响应式?数据发生变化了,而依赖该数据的地方没有任何变化。
在vue中,使用某些方式改变数据(model层)时,vue不会把结果呈现在页面上(view层),这就是vue出现的非响应式的情况:
-
对数组使用了 非变异 (non-mutating method) 方法(方法的调用不会改变原始数组的内容,如:concat,filter,map)。因为,没有改变原始数组的内容。
-
使用数组的索引(根索引)的方式改变数组的元素时
-
修改数组的长度时
-
给对象添加新的属性时。
所以,强烈建议:不要用下标的方式修改数组的元素,不要修改数组的长度,可以把未来需要的数据都声明在data选项内部,不要对数组使用非变异的api(数组的变异方法:https://cn.vuejs.org/v2/guide/list.html#%E5%8F%98%E6%9B%B4%E6%96%B9%E6%B3%95)
vue也提供了非响应式问题 解决方案(尽量不要使用)
Vue.set | this.$set(数组, index, value)
Vue.set | this.$set(对象, key, value)
this.$forceUpdate() 强制刷新
六、vue语法(model层)vue的配置项
data属性:
不再说了
methods:
也暂时不说了,这里面放的是函数的定义,就是普通函数的定义。这些函数可以作为事件处理函数,也可以在其它地方调用。
计算属性
在模板(HTML)中放入太多的逻辑会让模板过重且难以维护,而且不好阅读。计算属性computed来帮忙。
计算属性是一个函数,是经过元数据(data里)进一步运算后的数据,计算属性的优点是:当元数据不发生变化时,不会再做计算(即:缓存),只有元数据发生变化时,才做计算。是响应式的,需要在模板中渲染才可调用(计算属性只能在模板上使用)
语法
//定义 computed:{ 计算属性: function(){return 返回值} } //使用 使用: {{计算属性}} | v-指令="计算属性"
面试题:
computed VS methods
methods | computed |
---|---|
每次调用都会执行函数里的代码 | 基于它们的响应式依赖进行缓存的,如果依赖没有变化,就不再调用 |
性能一般 | 性能高 |
{{methodname()}} | {{computedname}} |
适合强制执行和渲染 | 适合做筛选 |
属性检测(侦听属性)
https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7-vs-%E4%BE%A6%E5%90%AC%E5%B1%9E%E6%80%A7
需要在数据变化时执行异步或开销较大的操作时,这个时候需要属性检测watch。而不要使用计算属性,因为计算属性是同步的(需要立即返回结果)
定义一个选项
watch:{ 被侦听的属性名:'methods的里函数名' //数据名==data的key 被侦听的属性名:函数体(new,old){} 被侦听的属性名:{ handler:function(new,old){}, deep: true //面试题:深度检测,当侦听的属性是个对象,修改对象的某个键时,就能检测到 immediate: true //首次运行,要不要监听 } }
示例:
请输入您的问题:<input type="text" v-model.lazy="question" /><br/> 答案:<input type="text" v-model="answer" /><br/> let vm = new Vue({ el:"#app", data:{ question:"", answer:"" }, watch:{ question:function(){ setTimeout(()=>{ this.answer = "吃的是草" },2000); } } });
深度检测:
<div id="app"> <input type="button" value="测试" @click="fn" /> </div> let vm = new Vue({ el:"#app", data:{ person:{ name:"张三疯", wife:{ name:"梅超风" } }, }, methods:{ fn(){ this.person.name="李思峰"; } }, watch:{ person:{ deep: true, //深度检测,当侦听的属性是个对象,修改对象的某个键时,就能检测到 handler:function(){ console.log("person变了"); }, immediate: true } } });
面试题:
计算属性 VS 函数 VS 属性检测
计算属性(computed) | 函数(methods) | 属性检测(侦听)(watch) | |
---|---|---|---|
为了显示而用 | 只是处理逻辑,跟普通的函数一样 | 属性变化的检测(相当于事件),当属性的值发生变化时,可以调用函数 | |
依赖模板调用 | √(只能在模板上调用的) | √(可以在模板使用) | ×(不能在模板上使用) |
是否缓存 | √ | × | √ |
异步 | ×(必须是同步) | √(可以有异步) | √(可以有异步) |
自定义指令
https://cn.vuejs.org/v2/guide/custom-directive.html
系统(官方)指令在不够用的情况下,考虑自定义,指令是个函数|对象,用来操作dom的, 里面的this 返回window
全局定义
Vue.directive('指令名',{ bind:function(el,binding){ binding.arg v-bind:class }//指令第一次绑定到元素时调用,此时DOM元素还没有显示在页面上 inserted:function(el,binding){} //绑定指令的DOM元素插入到父节点时调用。DOM已经渲染(显示) update:function(el,binding){} //指令所在的元素的model层的数据,view有更新请求时 componentUpdated:function(el,binding){} //更新完成时,不讲 })
-
**指令名:**定义指令时,不用加 v- , 使用指令时,加 v-
-
钩子函数的参数:
钩子函数的参数 (即 el、binding、vnode 和 oldVnode)。
https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0
name: 指令名(不带v-)
arg:写在指令名后面的参数,如:v-myfocus:id , arg就是id 。 v-bind:value ,arg就是value
expression: 等号后面的表达式,如:v-myfocus:id=“msg+‘1’” ,expression就是msg+‘1’。
value:绑定数据的值,如:v-myfocus:id=“msg+‘1’” , value就是msg的值和1拼接的结果
示例:
获得焦点
//定义指令: Vue.directive('myfocus', { inserted: function (el) { // 当被绑定的元素插入到 DOM 中时…… // 聚焦元素 el.focus() } }) 使用指令 <div> <input type="text" v-myfocus /> </div>
模拟v-bind指令的功能:
Vue.directive("mybind",{ bind:function(el,binding){ el[binding.arg] = binding.value; }, update:function(el,binding){ el[binding.arg] = binding.value; } });
全局定义格式(简写)
Vue.directive('指令名', function(el,binding){ //等价于:bind + update })
模拟v-bind指令的功能:
Vue.directive("mybind",function(el,binding){ el[binding.arg] = binding.value; });
钩子函数的详解:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
局部定义
new Vue({ directives:{ 指令名 : function(el,binding){},//简写方式: bind + update 指令名(el,binding){}, 指令名:{ bind:fn(el,binding){} //指令第一次绑定到元素时调用 v-drag inserted:fn(el,binding){} //绑定指令的元素插入到父节点时调用 v-focus update:fn(el,binding){} //指令所在的元素的model层的数据,view有更新请求时 componentUpdated:fn(el,binding){} //更新完成时 } } })
示例:
回到顶部
//自定义指令的代码 // 注册一个全局自定义指令 `v-pagetop` Vue.directive('pagetop', { inserted: function (el) { el.onclick = function(){ document.body.scrollTop = 0; document.documentElement.scrollTop = 0; } } }) //使用指令 <div id="app" > <div v-pagetop style="position:fixed;bottom:10px;right:10px;width: 100px;height: 100px;background-color:red;"> 回到顶部 </div> </div>
拖拽:
./js/mydirections.js Vue.directive("drag", { inserted: function (el, binding) { el.style.position = "absolute"; let offsetX; let offsetY; let parentLeft = el.offsetParent.offsetLeft; let parentTop = el.offsetParent.offsetTop; let maxLeft = el.offsetParent.offsetWidth - el.offsetWidth; let maxTop = el.offsetParent.offsetHeight - el.offsetHeight; // 鼠标按下的函数 function mousedownFn(event) { let e = event || window.event; offsetX = e.offsetX offsetY = e.offsetY // onm ousemove // offsetParent: 找到最近的有定位属性的那个元素 el.offsetParent.addEventListener("mousemove", mousemoveFn); }; // 鼠标移动的函数 function mousemoveFn(event) { let e = event || window.event; // 一、数据处理 let left1 = e.pageX - parentLeft - offsetX; let top1 = e.pageY - parentTop - offsetY; if (left1 < 0) { left1 = 0; } if (left1 > maxLeft) { left1 = maxLeft; } if (top1 < 0) { top1 = 0; } if (top1 > maxTop) { top1 = maxTop; } // 二、外观呈现 el.style.left = left1 + "px"; el.style.top = top1 + "px"; } // onm ousedown el.addEventListener("mousedown", mousedownFn); // onm ouseup document.addEventListener("mouseup", function () { el.offsetParent.removeEventListener("mousemove", mousemoveFn); }); } } ); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style> .redbox { width: 200px; height: 200px; border-radius: 10px; background-color: red; } #box{ position: relative; border: 1px solid black; width: 500px; height: 400px; } </style> </head> <body style="height: 1200px"> <div id="box"> <div class="redbox" v-drag>我可以被拖拽</div> </div> </body> </html> <script src="./js/vue.js"></script> <script src="./js/mydirections.js"></script> <script> let vm = new Vue({ el: "#box", data: {} }); </script>
过滤器
https://cn.vuejs.org/v2/guide/filters.html
对数据在模板中的表现过滤,符合预期,比如:数据是0和1,想要表现成对错、成功失败、数据需要过滤器来格式化,vue1.x版本有系统自带过滤器,vue2.x之后完全需要自定义,没有自带过滤器
全局定义
Vue.filter('过滤器名称',函数(要过滤的元数据,参数1,参数n){ 过滤器的功能 return 过滤的结果 })
使用
{{数据名 | 过滤器名(参数1,参数2)}} v-bind:属性="数据| 过滤器名(参数1,参数2)"
局部定义
new Vue({ …………………… filters:{ 过滤器名称:函数(要过滤的元数据,参数){ 过滤器的功能 return 过滤的结果 } //函数必须要有返回值 } })
示例:
阿拉伯数字的金额转为大写的金额: 12.56
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <style> </style> </head> <body> <div id="box"> <input type="number" v-model.number="money"><br/> <p>¥:{{money}}</p> <p>大写:{{money | toChina}}</p> </div> </body> </html> <script src="./js/vue.js"></script> <script> Vue.filter("toChina",function(value){ let money = value.toFixed(2);//保留两位小数 //汉字的数字 var cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; //基本单位 var cnIntRadice = ['', '拾', '佰', '仟']; //对应整数部分扩展单位 var cnIntUnits = ['', '万', '亿', '兆']; //对应小数部分单位 var cnDecUnits = ['角', '分', '毫', '厘']; //整数金额时后面跟的字符 var cnInteger = '整'; //整型完以后的单位 var cnIntLast = '元'; //最大处理的数字 var maxNum = 999999999999999.9999; //金额整数部分 var integerNum; //金额小数部分 var decimalNum; //输出的中文金额字符串 var chineseStr = ''; //分离金额后用的数组,预定义 var parts; if (money == '') { return ''; } money = parseFloat(money); if (money >= maxNum) { //超出最大处理数字 return ''; } if (money == 0) { chineseStr = cnNums[0] + cnIntLast + cnInteger; return chineseStr; } //转换为字符串 money = money.toString(); if (money.indexOf('.') == -1) { integerNum = money; decimalNum = ''; } else { parts = money.split('.'); integerNum = parts[0]; decimalNum = parts[1].substr(0, 4); } //获取整型部分转换 if (parseInt(integerNum, 10) > 0) { var zeroCount = 0; var IntLen = integerNum.length; for (var i = 0; i < IntLen; i++) { var n = integerNum.substr(i, 1); var p = IntLen - i - 1; var q = p / 4; var m = p % 4; if (n == '0') { zeroCount++; } else { if (zeroCount > 0) { chineseStr += cnNums[0]; } //归零 zeroCount = 0; chineseStr += cnNums[parseInt(n)] + cnIntRadice[m]; } if (m == 0 && zeroCount < 4) { chineseStr += cnIntUnits[q]; } } chineseStr += cnIntLast; } //小数部分 if (decimalNum != '') { var decLen = decimalNum.length; for (var i = 0; i < decLen; i++) { var n = decimalNum.substr(i, 1); if (n != '0') { chineseStr += cnNums[Number(n)] + cnDecUnits[i]; } } } if (chineseStr == '') { chineseStr += cnNums[0] + cnIntLast + cnInteger; } else if (decimalNum == '') { chineseStr += cnInteger; } return chineseStr; } ); let vm = new Vue({ el: "#box", data: { money:12.56 } }); </script>
混入
意义在于分发 Vue 组件(对象)中的可复用功能,混入对象就是一个json对象,json对象的属性就是 Vue对象的配置项(data,methods等等,但是没有el)
用法
1、定义格式
let mixin1 = { data: ... methods: ... } let mixin2 = { data: ... methods: ... }
2、局部混入(组件内部混入 组件选项内)
mixins: [mixin1,mixin2] //当混入的键与引入键冲突时以组件内的键为主 new Vue({ el:"#app", data:{ msg:"hello" } mixins: [mixin1,mixin2] })
3、全局混入:
Vue.mixin(mixin1)//绝对不推荐的
混入普通选项与组件(vue对象)选项合并,遇冲突,以组件(Vue对象)为主,即:就近原则。
如果是生命周期钩子函数,那么都会调用(混入的钩子先调用)
预购阿里云服务器(ecs云服务器)
链接 -》个人云 -》突发性能型 t5(74/年)-》系统(centOs系统,认准ecs云服务器-》控制台做一下实名认证
域名购买
链接
虚拟DOM和diff算法(原理)(面试题)
什么是虚拟DOM(virtual DOM):
所谓的虚拟 dom,也就是我们常说的虚拟节点,它是通过JS的Object对象模拟DOM中的节点,然后再通过特定的render(渲染)方法将其渲染成真实的DOM的节点。
为什么使用虚拟DOM:
使用js操作DOM时(增删改查等等),那么DOM元素的变化自然会引起页面的回流(重排)或者重绘,页面的DOM回流(重排)或者重绘自然会导致页面性能下降,那么如何尽可能的去减少DOM的操作是框架需要考虑的一个重要问题!
https://blog.csdn.net/jiang7701037/article/details/98516468。
vue中,使用虚拟DOM 来提高性能。
真实DOM和虚拟DOM的区别:
虚拟DOM不会进行排版与重绘操作
真实DOM频繁排版与重绘的效率是相当低
虚拟DOM进行频繁修改,然后一次性比较(使用diff算法)并修改真实DOM中需要改的部分,最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗
虚拟DOM有效降低了重绘与排版的次数,因为,最终把虚拟dom与真实DOM比较差异,可以只渲染局部
diff算法:
虚拟DOM,是一种为了尽可能减少页面频繁操作DOM的方式,那么在虚拟DOM中,通过什么方式才能做到呢? 就是Diff算法进行对比
diff算法的原理:
逐步解析newVdom的节点,找到它在oldVdom中的位置,如果找到了就移动对应的DOM元素,如果没找到说明是新增节点,则新建一个节点插入。遍历完成之后如果oldVdom中还 有 没处理过的节点,则说明这些节点在newVdom中被删除了,删除它们即可。
总结:
1、产生两个虚拟DOM树:newVDom,oldVDom。
2、oldVDom和真实DOM保持一致
3、数据变化时,影响的是(操作的是)newVDom
4、操作后newVDom后,通过diff算法对比newVDom和oldVDom的差异,并在oldVDom标注哪些节点要删除,哪些节点要增加,修改
5、根据oldVDom操作真实的DOM,让真实Dom和oldVDom保持一致
总结:
虚拟DOM: 用JSON对象模拟的真实dom
diff算法:用来比较两个虚拟dom的不同之处。
vue-dev-tools安装
方案1: 登录谷歌应用商城->索引vue-dev-tools->安装->重启浏览器
方案2: https://blog.csdn.net/jiang7701037/article/details/99708017
七、组件
组件封装的是完整的页面功能(包括:HTML、CSS、JS),而函数只封装JS(逻辑)
组件的概念:
组件是自定义标签,vueJS提供的组件可以让程序员自定义标签,对页面进行模块化。每个标签里包含HTML,CSS,JS。
vue的组件就是一个vue对象。(vue的两大核心:数据驱动,组件化) 。vue对象的配置项,在vue组件里也可以使用。
组件的配置项如下:
没有el属性。
template:html模板代码,只能有一个根标签
data:必须是个函数
methods:
………………………………
一个完整的标签格式是: <标签名 属性名=“属性值" 事件=”函数“>内容</标签名>
vue组件的基本使用(标签名):
1、定义组件:
第一种:
let 组件变量名= Vue.extend({ template:'<div class="header">{{msg}},我是header组件</div>' data:function(){ return { msg:”hi” } }, });
第二种(简化写法):
let 组件变量名={ template:'<div class="header">{{msg}},我是header组件</div>' data:function(){ return { msg:”hi” } }, };
2、注册组件:
全局注册:
Vue.component('标签名',组件变量名);
全局注册的组件,在任何vue对象里都可以使用
局部注册:
//在vue对象的components选项里进行注册 new Vue({ el: components:{ 标签名:组件变量名 } });
局部注册的组件,只能在当前vue对象(组件)里使用。
3、使用组件:
组件就是自定义标签,所以,使用组件,就跟使用标签是同样的。
<组件名></组件名>
示例代码:
<body > <div id="box"> <!--使用组件(组件就是自定义标签,所以,就是使用标签)--> <chat></chat> </div> </body> </html> <script src="./js/vue.js"></script> <script> // 定义组件 let chatObj = { template:` <div> <div style="width:200px;height:300px;border:1px solid black" v-html="msg"></div><br/> <input type="text" v-model="str" /> <input type="button" value="发送" @click="send" /> </div> `, data:function(){ return{ str:"", msg:"" } }, methods:{ send(){ this.msg += this.str+"<br/>" this.str =""; } } }; // 2、全局注册 Vue.component("chat",chatObj); let vm = new Vue({ el: "#box", data: { }, //局部注册: components:{ "chat":chatObj } }); </script>
4、组件嵌套:
把一个组件的标签写在另外一个组件的template里,就是组件嵌套。
如:
//子组件 let myComSon = { template:"<div>我是son里的div:{{msg}}</div>", data:function(){ return { msg:"hi:son" } } }; //父组件 let myComParent = { template:`<div> <p>我是p:{{msg}}</p> <my-com-son></my-com-son> </div>`, data:function(){ return { msg:"hi" } }, components:{ // 局部注册了另外一个组件 "my-com-son":myComSon } };
5、组件编写方式与 Vue 实例的区别:
1、组件名(标签名)不可和html官方的标签名同名,组件名如果小驼峰,那么使用时,用短横线(羊肉串的写法),或者组件名首字母大写(这个规则是在未来的单文件组件,模块化的写法里使用)。
2、组件没有el选项,只有根实例存在el,组件里使用template定义模板
3、组件模板(html代码)只能有一个根标签
4、data是个函数(面试题)
一个组件的 data 选项必须是一个函数,且要有返回object,只有这样,每个实例(vue组件对象)就可以维护一份被返回对象的独立的拷贝,否则组件复用时,数据相互影响,也就是说,组件的作用域是独立的。
简单回答:如果不是函数,那么,复用的组件的data共享同一块内存空间。
组件的属性(标签的属性)
使用props(property的简写)来完成组件属性的声明。 props是外部给组件传入的数据。data是组件内部的数据。
使用 Prop 传递静态数据
1)、在组件内部增加配置项 props来声明组件里的属性。props里面可以声明多个属性,所以,是个数组。
let myComParent = { props:["name","sex"], //声明了两个自定义属性 template:`<div> <p>我是p:{{msg}}</p> <p>人的信息:</p> <p>姓名:{{name}}</p> <p>性别:{{sex}}</p> </div>`, data:function(){ return { msg:"hi" } } };
2)、使用组件时,给属性传入数据:
<!-- 使用组件时,给属性传入值,传入的值,就可以在组件内部使用 --> <my-com-parent name="张三疯他哥" sex="男"></my-com-parent>
总结提升认识:
把封装组件和封装函数进行类比:
在组件内部用props声明的属性,相当于封装函数时声明的形参。
使用组件时,相当于调用函数,传递实参。
props是外部给组件传入的数据(相当于函数中的参数)。data是组件内部的数据(相当于函数里的局部变量)
动态Prop
组件属性和官方标签的属性是同样的道理,所以,给组件的属性也可以v-bind 数据。即:绑定动态的数据
<my-com-parent v-bind:name="name" sex="男"></my-com-parent>
如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:
todo: { text: 'Learn Vue', isComplete: false } <todo-item v-bind="todo"></todo-item> 等价于: <todo-item v-bind:text="todo.text" v-bind:is-complete="todo.isComplete" ></todo-item>
奇葩情况(组件的属性是引用类型)
在 JavaScript 中对象和数组是通过引用传入的(传的是地址),所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态(data),相当于函数的形参是引用类型
Prop 验证
vueJS还提供了对属性类型的验证、属性默认值,是否必须等等。这时候,props不能使用数组,而需要使用对象。
如:
props:{ "name":{ type:String, //限制name属性的类型是字符串 required:true //限制name属性是必须传入值的。 }, "sex":[String,Number], //限制sex属性的值可以为字符串,也可以为数字 "age":{ type:Number, //限制age属性的类型是数字型 default:10 // age属性的默认值是10 。如果没有给age传值,那么age就是10。 }, "isadult":Boolean },
单向数据流
Prop 是单向绑定的:当父组件的属性(数据)变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。
另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。
总结:
组件就是标签,prop是标签的属性。
prop是外部传入的数据,data是组件内部的数据。
组件的事件
1、绑定事件(父组件里写):
HTML(标签)里的绑定方式:v-on
JS(Vue)里绑定方式: vue对象.$on(“事件名”,事件处理函数)
2、触发事件(子组件里写) : vue对象.$emit(“事件名”,参数);
示例:
//子组件: Vue.component('button-counter',{ template: "<input type='button' v-on:click='incrementCounter' v-model='counter' />", data:function(){ return { counter:0 } }, methods:{ incrementCounter:function(){ this.counter += 1 this.$emit('increment') } } }); //父组件: <div id="app1"> <p>{{ total }}</p> <button-counter v-on:increment="incrementTotal"></button-counter> </div> var app1 = new Vue({ el: '#app1', data:{ total:0 }, methods:{ incrementTotal:function(){ this.total += 1; } } });
组件的内容(插槽)
组件的内容就是标签的innerHTML。vueJS里使用**Slot(插槽)**分发内容。
props用来处理标签的属性 ,slot用来处理标签的内容。
将父组件的内容(DOM)放到子组件指定的位置叫作内容分发。
单个插槽
//子组件 let person = { template:`<div> <hr/> <p>我是上p</p> <slot></slot> <p>我时下p</p> <hr/> </div>` }; //父组件: <div id="app"> <person> <div>我是div</div> </person> </div> new Vue({ el:"#app", components:{ person } });
具名插槽(多个插槽需要使用名字)
如果父级给子级传来了好多DOM(HTML元素),而且需要把不同的DOM放在子组件不同位置时,就需要给slot起名字,这就是具名插槽。slot元素可以用一个特殊的属性name 来配置如何分发内容。
格式:
<slot name="插槽名"></slot>
示例:
//子组件 let person = { template:`<div> <hr/> <p>我是上p</p> <slot name="s1"></slot> <p>我是中p</p> <slot name="s2"></slot> <p>我是下p</p> <hr/> </div>` }; //父组件 <div id="app"> <person> <div slot="s1">我是div1</div> <div slot="s2">我是div2</div> </person> </div> new Vue({ el:"#app", components:{ person } });
编译作用域
父组件模板的内容在父组件作用域内(父组件对应的对象的作用域)编译;子组件模板的内容在子组件作用域内编译。
示例:
//子组件 let person = { template:`<div> <hr/> <p>我是上p:{{t}}</p> <p>我是下p</p> <hr/> </div>`, data(){ return{ t:"我是子组件的数据" } } }; //父组件: <div id="app"> <input :value="msg" /> <person v-show="s"> <p>{{msg}}</p> <div>我是div1:{{msg}}</div> </person> </div> new Vue({ el:"#app", data:{ msg:"hi", s:true }, components:{ person } });
自行研究:作用域插槽
作用域插槽
组件(间)的通信(面试中经常问的)
vue组件之间的通信(传递数据)是必然的,依据vue组件之间的关系(父子,兄弟,或者无关组件)会有不同的做法:
1)、父子组件:
父—> 子: props, 子—> 父:事件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t2pALMIK-1647875628969)(\1599837423830.png)]
2)、父给子组件:refs
3)、兄弟组件,或者无关组件:事件总线,集中管理,vueX等
refs
首先,需要知道:用标签的方式使用组件(如:),实际就是创建了组件对象。只要拿到组件对象,那么组件对象里的methods就可以使用。refs是vue中获取dom的一种方式,dom也就是标签,标签就是组件对象。即就是:拿到了dom,就相当于拿到了组件对象(这段话需要慢慢品……)
如果某个元素使用ref属性,那么,在vue对象里,就能用this.$refs 访问。可以认为是给元素增加了个id属性,在vue里就可以操作该dom了
格式:
如: <p ref = "pId"> {{msg}}</p> <p ref = "pId02"> {{msg}}</p> methods:{ testf:function(){ this.$refs.pId.innerHTML = "hi"; } }
示例:
let myCom = { template:` <div> </div> `, data(){ return { } }, methods:{ focusFn(str){ console.log(str); } } } Vue.component("my-com",myCom); new Vue({ el:"#app", data:{ msg:"hi" }, methods:{ fn(){ this.$refs.comId.focusFn(this.msg);//focusFn()是子组件里的函数。 } } });
事件总线(event-bus)
event-bus实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件(其实,也可以实现父子)通信的一种解决方案。
步骤:
1、单独new一个Vue空对象: let bus= new Vue(); 2、在组件A里,绑定一个自定义事件(相当于定义了一个自定义事件): bus.$on('eclick',target => { console.log(target) }) 3、在组件B里,触发事件 bus.$emit('eclick',"b传给a的");
示例:
// 定义一个vue对象(作为事件总线的对象) let bus = new Vue(); let myCom1 = { template:` <div> <hr> 组件com1 <hr> </div> `, created(){ // 注册了一个事件 bus.$on("eclick",(target)=>{ console.log(target); }); } } let myCom2 = { template:` <div> <input type="button" value=" 传 " @click="fn" /> </div> `, methods:{ fn(){ //触发事件 bus.$emit("eclick","hello"); } } } Vue.component("my-com1",myCom1); Vue.component("my-com2",myCom2); new Vue({ el:"#app" });
集中管理($root)
把数据存到根实例的data选项,其他组件直接修改或者使用
定义
new Vue({ data:{ a:1 } })
使用
//子组件内部 this // 子组件本身 this.$root // vm 根实例 this.xx //组件内部数据 this.$root.a //根实例数据
动态组件
有的时候,在不同组件之间进行动态切换是非常有用的。即页面的某个位置要显示的组件是不确定的,是会变化的。
vue中使用的方式实现。
示例:
<div id="app"> <span @click="show(0)">娱乐</span>|<span @click="show(1)">八卦</span>|<span @click="show(2)">体育</span> <div> <component :is="currCom"></component> </div> </div> let yuLe = { template:"<div>娱乐新闻</div>" } let eightGua = { template:"<div>八卦新闻</div>" } let sports = { template:"<div>体育新闻</div>" } new Vue({ el:"#app", data:{ currCom:"yuLe", coms:["yuLe","eightGua","sports"] }, methods:{ show(index){ this.currCom = this.coms[index]; } }, components:{ yuLe,eightGua,sports } });
根据班级情况尝试扩展如下:
增删改查(或者留言板,或者员工信息)
八、vue对象
类和实例API
https://cn.vuejs.org/v2/api/#%E5%85%A8%E5%B1%80-API
https://cn.vuejs.org/v2/api/#%E5%AE%9E%E4%BE%8B-property
Vue类的属性和方法
Vue 是个类,所以Vue.属性名,是类属性|静态属性,Vue.方法名() 是类方法(也叫静态方法)
如:Vue.extend(),Vue. mixin(),Vue.component()……………………
Vue实例(对象)属性和方法
let vm = new Vue() 返回的是实例,所以vm. 属 性 名 是 实 例 属 性 , v m . 属性名 是实例属性,vm. 属性名是实例属性,vm.方法名()是实例方法,同时vue类内部的this指向的是实例vm。 实例 vm.$属性名对等 vue选项的 根key。
如:vm. d a t a , v m . data, vm. data,vm.el ……………………。
生命周期(面试题)
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会
链接
https://blog.csdn.net/jiang7701037/article/details/83118665
面试题:请问您对vue的生命周期是怎么理解的?
一、vue生命周期是什么
就是vue对象从创建,到使用,到销毁的过程。
二、vue对象生命周期经历了四个阶段,同时有八个钩子函数:
1)、数据挂载 阶段 :把传入的data属性的内容(data配置项),赋给vue对象。即:把形参中data的属性赋给vue对象。
前后分别的钩子函数是:beforeCreate、created
2)、模板渲染阶段:把vue对象中data渲染到dom对象上(模板上,视图上)。
前后分别的钩子函数是:beforeMount、mounted
3)、组件(模板)更新阶段:当数据(必须是在模板上使用的数据)发生变化时,会触发组件的更新,组件会重新渲染。
前后分别的钩子函数是:beforeUpdate、updated
4)、组件销毁阶段:
前后分别的钩子函数是:beforeDestroy、destroyed
如果组件在缓存的话,那么,组件切换时,会调用的钩子函数是:
activated 和 deactivated
三、(当页面初始的数据来自后端)发送请求在哪个钩子函数,created
四、在beforeDestroy钩子函数里,会做什么事情?
清除定时器。因为,定时器是属于window对象的,启动的定时器也属于window对象,只要网页不关闭,window对象就不会销毁。所以,组件销毁时,并不会销毁定时器:
<body > <div id="box"> <span @click="show(0)">娱乐</span>|<span @click="show(1)">八卦</span> <div> <component :is="currCom"></component> </div> </div> </body> </html> <script src="./js/vue.js"></script> <script> let yuLe = { template:"<div>娱乐新闻</div>", data(){ return { timer:null, msg:"hi" } }, created(){ console.log("娱乐组件创建"); var ord=0; this.timer = setInterval(function(){ console.log("定时器:"+ord++); },1000); }, beforeDestroy(){ window.clearInterval(this.timer); console.log("娱乐组件销毁前"); } } let eightGua = { template:"<div>八卦新闻</div>", beforeCreate(){ console.log("八卦组件创建"); } } let vm = new Vue({ el: "#box", data: { currCom:yuLe, coms:[yuLe,eightGua] }, components:{ yuLe, eightGua }, methods:{ show(idx){ this.currCom = this.coms[idx]; } } }); </script>
总结:
1、vue组件(对象)从创建到(初次)显示在用户眼前:经历了 beforeCreate,created,beforeMount,mounted
2、数据(必须在模板中使用的数据)更新:调用beforeUpdate和updated函数
3、为什么叫钩子函数:和回调函数是同样的道理,只不过钩子函数更多会强调(函数调用的)时机。
思考题:
1、如果一打开组件,就需要显示数据,那么请求,应该在哪个钩子函数里写?为什么?
created,
因为,一般来说,后端返回来的数据需要赋给vue对象的属性(this.属性名),在created里是最早能够拿到vue对象属性的。如果在beforeMount和mounted里就有点晚了。
2、如果组件在keep-alive里,而且有定时器(比如:轮播图),如果说deactivated时,可以停止定时器。
项目环境(脚手架)
单文件组件
xx.vue,内部组成(template + script +style)
概念: 把一个组件的所有代码(HTML模板,样式,js(vue对象))写在一个文件里,扩展名是.vue。一个文件里就只有一个组件。这就是单文件组件。
基本结构:
<template> HTML代码 </template> <script> vue对象 (使用模块化导出vue对象):可以使用es6的写法 </script> <style scoped> //scoped: 表示局部作用域,表示当前style标签的样式只会应用在当前vue文件里的模板里 样式 </style>
示例:
<template> <p>{{msg}}</p> </template> <script> export default { data(){ return { msg:”hello” } } } </script> <style scoped> p{ color:red; font-size:12px } </style>
脚手架(vue-cli)环境搭建
cli: command line interface
- 通过官方脚手架,(命令行的方式)搭建模块化,工程化,自动化开发环境
//1、查看版本(这是查看脚手架的版本) vue -V //2、如果版本是低于3.X,那么卸载脚手架,安装最新的。 npm uninstall vue-cli -g //3、安装(脚手架工具) //1)、装脚手架工具的3.x/4.x npm install -g @vue/cli //2)、桥接2.X(兼容脚手架2.X) npm install -g @vue/cli-init //4、创建项目:使用脚手架搭建项目(文件夹) //1)、如果想搭建版本 v3.x/4.x vue create 项目目录 //2)、如果想搭建版本 2.X vue init webpack 项目目录
注意:项目目录,不要和官方的或者第三方的模块重名,也不要使用汉字(每一级文件夹尽量不要使用汉字)
- HbuildX自动搭建模块化,工程化,自动化开发环境
工作区-》右键-》创建项目-》普通项目-》vue-cli项目
- 直接copy他人生成的项目环境也可以。
去掉eslint提示
在vue.config.js里增加如下代码:
module.exports = {
lintOnSave:false,
}
脚手架搭建好的项目的讲解
1)、vue-cli2.X
build文件夹(暂时不需要关注): webpack的相关配置
config文件夹(暂时不需要关注):项目环境的配置。
src文件夹:源代码的文件夹,程序员就在此目录下写代码
src/assets文件夹: 存放静态资源(css,图片,字体文件,视频,音频等等),这些静态资源会做(webpack)打包处理(编译)
src/components文件夹: 所有的组件
src/App.vue:main.js所引入的App.vue是模板(main.js里的Vue对象的模板)
src/main.js: 就是唯一的html文件所对应的唯一的Vue对象(根实例)。入口文件。
static文件夹:存放静态资源(但是该文件夹下的文件不做(webpack)打包处理)。
index.html:唯一的HTML文件。
2)、vue-cli3+
区别:
隐藏了build和config文件夹
把static文件夹变成了public文件夹:放置静态资源,把唯一的index.html也放在了public下。
main.js
Vue.config.productionTip = false//设置为 false 以阻止 vue 在启动时生成生产提示。 render: h => h(App) //↓ render: function(createElement){ // return createElement('h3','内容') // return createElement(组件) return createElement(App) }
es6模块化复习
https://blog.csdn.net/jiang7701037/article/details/101215999
1)、export
输出
//定义一个文件: a.js //export 可以在一个模块里面多次使用 //export可以对外导出任意类型的数据 //export 导出的数据要有名字(变量名,函数名) export const a = 100 export var b = 200 export var person ={ }
输入(方式一)
//引入:需要解构 import {a,b,person} from './a.js' import {a,person} from './a.js'
输入(方式二)
可以把模块输出的所有变量包装成一个对象 import * as obj from './a.js' 即:obj就是有三个属性的对象 { a, b, person }
2)、export default
输出
//定义一个文件: a.js //export default 在一个模块里面只能出现一次 //export default可以对外导出任意类型的数据 //export default导出的数据是匿名的(不能写变量名),即就是,只导出值。 格式: export default 任何类型的变量的值(大部分时候是对象) export default 100; export default { };
输入
格式: import 变量名 from "模块所在的路径及其文件名"; (如果是自定义模块就需要写路径,如果是官方的或第三方的模块,不需要写路径) import a from "./a.js"; a就是export default导出的数据。 a其实就是个变量名。 这种写法其实就是,把变量名和赋的值分在了两个模块里。
css 规则
style-loader 插入到style标签,style标签多了选择器冲突问题就出来了,解决方案如下
/* 方案1: 命名空间 不太推荐BEM命名风格*/ /*B__E--M block 区域块 element 元素 midiler 描述|状态 */ .search{ } .search__input{} .search__input--focus{} .search__input--change{} .search__button{} // B__E--M // b-b-b__e-e-e--m-m-m
<!--方案2 css模块化 --> <template> <div :class="$style.box"> ... </div> </template> <script> export default { mounted(){ this.$style //返回一个对象 {选择器:'混淆后的选择器',xx:oo} } } </script> <style module> /* 注意: 选择器名(支持 id,类,或者id、类开头其他选择器,不支持标签名(标签名开头的)) */ .box{} #box2{} .box3 div{} #box3 div{} </style> 模块化的好处:使用实例属性$style可以访问到样式。
<!--方案3 独立样式作用域--> <template></template> <script></script> <style scoped></style>
注意:在vue脚手架里,图片(静态资源)的路径问题:
1、如果img标签src使用的是静态的写法(就是纯粹html代码),那么,文件来自于 assets 下,按照正常路径写。
2、如果img标签的src使用的动态的写法(vue去给它赋值,使用v-bind),那么,文件来自于 public下,但是,src后面的值,不能带“public”
原因(原理):
1、开发目录和发布目录(二阶段用的gulp,三阶段用的webpack(vue脚手架里用的webpack))
2、在浏览器运行的是发布目录的代码
所以说,最终的路径应该以发布目录的路径为准。
二阶段能够直接看到发布目录的代码,webpack默认是在内存中放的。如果想看那就用 npm run build打包
webpack只做打包,并不执行js代码。
明确一些名词(叫法):
1、vue.js :是一个js框架。现在讲的版本是 2.×.× ,最新的版本是3.×.×
2、vue-cli:vue脚手架,脚手架是个工具,帮程序员自动搭建项目环境(创建了一些文件夹和文件,并写了最基础的代码)。 现在市面上使用的版本有 2.×.×和 4. ×.×
坑:
1、不要在单文件组件里使用
单页面应用(SPA)
单页面应用的概念
SPA:single page application,单页面应用。
就是整个项目就只有一个html页面(文件),首次加载时,把所有的html,css,js全部加载下来。通过操作dom的删除和创建(添加)来完成页面的切换
单页面应用优缺点
优点:
1,局部刷新,所以,用户体验好
2,前后端分离
3,页面效果会比较炫酷(比如切换页面内容时的转场动画)
缺点:
1,不利于seo
2,导航不可用,如果一定要导航需要自行实现前进、后退。
3,初次加载时耗时多
4,页面复杂度提高很多
SPA和MPA的区别
SPA:single page application。只有一个主页面的应用,浏览器一开始要加载所有必须的 html, js, css。所有的页面内容都包含在这个所谓的主页面中。但在写的时候,还是会分开写(页面片段),然后在交互的时候由路由程序动态载入,单页面的页面跳转,仅刷新局部资源。vue后来做了改进,有些组件按需加载
MPA:multiple page application 。就是指一个应用中有多个页面,页面跳转时是整页刷新。
插槽页面模式 | 多页面模式(MPA Multi-page Application) | 单页面模式(SPA Single-page Application) |
---|---|---|
页面组成 | 多个完整页面, 例如page1.html、page2.html等 | 由一个初始页面和多个页面模块组成, 例如:index.html |
公共文件加载 | 跳转页面前后,js/css/img等公用文件重新加载 | js/css/img等公用文件只在加载初始页面时加载,更换页面内容前后无需重新加载 |
页面跳转/内容更新 | 页面通过window.location.href = "./page2.html"跳转 | 通过使用js方法,append/remove或者show/hide等方式来进行页面内容的更换 |
数据的传递 | 可以使用路径携带数据传递的方式,例如:http://index.html?account=“123”&password=123456"",或者localstorage、cookie等存储方式 | 直接通过参数传递,或者全局变量的方式进行,因为都是在一个页面的脚本环境下 |
用户体验 | 如果页面加载的文件相对较大(多),页面切换加载会很慢 | 页面片段间切换较快,用户体验好,因为初次已经加载好相关文件。但是初次加载页面时需要调整优化,因为加载文件较多 |
场景 | 适用于高度追求高度支持搜索引擎的应用 | 高要求的体验度,追求界面流畅的应用 |
转场动画 | 不容易实现 | 容易实现 |
单页面模式:相对比较有优势,无论在用户体验还是页面切换的数据传递、页面切换动画,都可以有比较大的操作空间 多页面模式:比较适用于页面跳转较少,数据传递较少的项目中开发,否则使用cookie,localstorage进行数据传递,是一件很可怕而又不稳定的无奈选择
mock数据
mock这词本意是虚拟,模拟的意思。mock server工具,通俗来说,就是模拟服务端接口数据,一般用在前后端分离后,前端人员可以不依赖API开发,而在本地搭建一个JSON服务,自己产生测试数据。即:今天要讲的json-server就是个存储json数据的server
json-server 支持CORS和JSONP跨域请求。
json-server
使用步骤:
1、初始化项目:
npm init -y
2、安装json-server
npm i json-server -D
3、打开项目编写数据
在项目根目录下创建db.json,并写上合法的json数据,如下:
{ "inc": { "count": 3 }, "vips": [ { "username": "李茂军", "userpass": "123666", "id": 2 }, { "username": "李茂军02", "userpass": "123666", "id": 3 }, { "username": "李茂军03", "userpass": "123666", "id": 4 }, { "username": "李茂军05", "userpass": "123666", "id": 5 }, { "username": "王翠霞", "userpass": "123888", "id": 6 }, { "username": "李家恒", "userpass": "123999", "id": 7 } ], "bannerImgs": [ "/imgs/1.jpg", "/imgs/2.jpg", "/imgs/3.jpg", "/imgs/4.jpg" ], "comments": [ { "bookId": "878911", "id": "001", "username": "李茂军", "time": "2021-10-27 11:40:15", "content": "三国演义很有内涵" }, { "bookId": "878911", "id": "002", "username": "李茂军02", "time": "2021-10-27 11:41:15", "content": "三国演义很有内涵,女朋友很喜欢" } ], "books": [ { "id": "878911", "name": "三国", "author": "罗贯中", "price": 51.2, "img": "/imgs/1.jpg", "type": "hot" }, { "id": "878912", "name": "水浒", "author": "施耐庵", "price": 51.5, "img": "/imgs/2.jpg", "type": "hot" }, { "id": "878913", "name": "红楼梦", "author": "曹雪芹", "price": 51.8, "img": "/imgs/3.jpg", "type": "hot" }, { "id": "878914", "name": "西游记", "author": "吴承恩", "price": 51.8, "img": "/imgs/4.jpg", "type": "hot" }, { "id": "878915", "name": "大学", "author": "李茂军", "price": 52.8, "img": "/imgs/img1.jpg", "type": "new" }, { "id": "878916", "name": "中庸", "author": "王翠霞", "price": 52.9, "img": "/imgs/img2.jpg", "type": "new" }, { "id": "878917", "name": "论语", "author": "王锐", "price": 53.8, "img": "/imgs/img3.jpg", "type": "new" }, { "id": "878918", "name": "孟子", "author": "李家恒", "price": 54.8, "img": "/imgs/img4.jpg", "type": "new" }, { "id": "878919", "name": "孟子2", "author": "李家恒", "price": 54.8, "img": "/images/img4.jpg", "type": "new" }, { "id": "878920", "name": "孟子3", "author": "李家恒", "price": 54.8, "img": "/images/img4.jpg", "type": "new" }, { "id": "878921", "name": "孟子4", "author": "李家恒", "price": 54.8, "img": "/images/img4.jpg", "type": "new" }, { "id": "878922", "name": "孟子5", "author": "李家恒", "price": 54.8, "img": "/images/img4.jpg", "type": "new" }, { "id": "878923", "name": "孟子6", "author": "李家恒", "price": 54.8, "img": "/images/img4.jpg", "type": "new" } ], "readers": [ { "id": "007", "name": "张三疯", "age": 35 }, { "id": "008", "name": "张四疯", "age": 32 } ] }
注意:每个键后面的值,只能是对象或者数组。
4、启动配置
在package.json下增加如下代码:
"scripts": { + "server":"json-server db.json" },
5、运行
在命令行运行: npm run server
JSON-SERVER的各种请求:
可以使用postman等工具测试以下请求。
-
GET 请求数据列表
获取所有的书籍
localhost:3000/bookS
-
GET 请求指定ID的数据
localhost:3000/bookS/878911
-
GET 请求指定字段值的数据
localhost:3000/users?name=李四&age=15
-
GET 数据分页
localhost:3000/bookS?_page=1&_limit=2
_page表示页码
_limit表示每页的条数
-
GET 数据排序
localhost:3000/bookS?_sort=price&_order=asc - asc 升序 desc 降序
-
GET 区间查询
搜索 age属性的值大于等于30 而 小于等于40的记录
localhost:3000/users?age_gte=30&age_lte=40
搜索 price 属性的值大于等于51.2而 小于等于51.8的记录
http://localhost:3000/bookS?price_gte=51.2&price_lte=51.8
-
GET 搜索
搜索所有属性值里包括“三”的记录,模糊查询。在所有属性中进行查询。
localhost:3000/bookS?q=三
-
GET 关联查询
http://localhost:3000/books/878911?_embed=comments
查询books中id为878911,并把comments中 bookId为878911的数据关联出来,结果是:
{ "id": "878911", "name": "三国", "author": "罗贯中", "price": 51.2, "img": "/imgs/1.jpg", "type": "hot", "comments": [ { "bookId": "878911", "id": "001", "username": "李茂军", "time": "2021-10-27 11:40:15", "content": "三国演义很有内涵" }, { "bookId": "878911", "id": "002", "username": "李茂军02", "time": "2021-10-27 11:41:15", "content": "三国演义很有内涵,女朋友很喜欢" } ] }
-
POST 添加数据
请求方式为:POST
- localhost:3000/users
- Headers:{ Content-Type:‘application/json’ }
- body -> raw
{ "name": "赵六", "age": 50, "companyId": 3 }
-
delete 删除数据
请求方式为:DELETE
localhost:3000/users/1
-
patch 更新数据
请求方式为:PATCH
- localhost:3000/users/3
- Headers:{ Content-Type:‘application/json’ }
- body -> raw
{ "age": 100 }
json-server的路由
需求场景:当使用mock数据,并且需要反向代理时:
1、在json-server的文件夹下新建route.json文件,写上如下代码:
{
“/api/*”: “/$1” // /api/posts => /posts
}
上面route.json的意思就是,当请求/api/posts时,重定向到/posts。
2、命令行中输入如下命令( 路由文件通过–routes 参数来指定):
json-server --routes route.json db.json
mock.js(自行研究)
数据交互
向服务器发送ajax请求,抓取数据
解决方案
- 自行通过XMLHttpRequest对象封装一个ajax(已经讲过)
- 使用第三方自带的ajax库,如:jquery(已经讲过)
- ES6新增的 fetch
- 使用第三方ajax封装成promise习惯的库,如:vue-resource、axios
fetch
ES6新增的前后端交互方案。
格式:Promise fetch(String url,配置);
Promise fetch(url,{ method: headers: body: })
功能:发送请求
返回值:一个 Promise,resolve 时回传 Response 对象
参数:
URL: 请求地址。
配置:(包括请求方式,请求参数等等)
method: 请求使用的方法,如 GET、POST。默认是GET
headers: 请求的头信息,形式为 Headers 对象或 ByteString。
body: 请求的 body 信息:可能是一个 Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象。注意 GET 或 HEAD 方法的请求不能包含 body 信息。
GET方式: fetch(url?id=001,{method:'GET',}) .then(response => response.json()) .then(data => console.log(data)) .catch(e => console.log("Oops, error", e)) POST方式: fetch(url,{ method:"POST", headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' // 指定提交方式为表单提交 }), body: "id=001&name=张三疯" // post请求的参数 }) .then(response => response.json()) .then(data => console.log(data)) .catch(e => console.log("Oops, error", e)) async和await的写法: try { let response = await fetch(url); let data = response.json(); console.log(data); } catch(e) { console.log("Oops, error", e); }
特点:
-
语法简洁,更加语义化
-
基于标准 Promise 实现,支持 async/await
-
同构方便,使用 isomorphic-fetch
-
Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: ‘include’})
-
服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
axios
概念:
一个基于 promise 的 第三方 库,可以用在浏览器(前端)和 node.js(后端) 中
格式:
Promise axios(配置)
Promise axios({ url : “地址“ method: “ 提交方式” params:{} 地址栏携带的数据(get方式) data:{} 非地址栏携带数据(如:post,put等等), baseURL:如果url不是绝对地址,那么将会加在其前面。当axios使用相对地址时这个设置非常方便 }).then(res=>{ console.log(res.data); })
axios的完整格式,和axios的别名(get和post)
//axios的完整的格式 axios({配置}).then(成功回调(res)).catch(失败回调(res)) //axios的别名: axios.get(url,{配置}).then(成功回调(res)).catch(失败回调(res)) axios.post(url,data,{配置}).then(成功回调(res)).catch(失败回调(res))
res: 响应体 数据是在res.data里
示例:
get请求:
axios({ url:'getGoodsListNew.php', // method:'get', 默认是get请求 params:{ count:3 } }) .then(res=>this.goodslist=res.data);
post请求示例:
1)、data是字符串
当data是字符串时,请求头里的content-type是 application/x-www-form-urlencoded,network中看到的数据类型是:formData
data 是字符串类型 axios( { method:'post', url:'regSave.php', data:'username=jzmdd&userpass=123' }) .then(res=>{ …………………… });
2)、data是URLSearchParams对象
当data是URLSearchParams对象时(跟data是字符串是一样的),请求头里的content-type是 application/x-www-form-urlencoded,network中看到的数据类型是:formData
var params = new URLSearchParams(); params.append('username', 张三疯); params.append('userpass', '123'); axios( { method:'post', url:'regSave.php', data:params }) .then(res=>{ …………………… });
3)、data是json对象
当data是json对象时,请求头里的content-type是 application/json,network中看到的数据类型是:request payload
axios({ url:"/vips", method:"post", data:{ name:this.name, pass:this.pass, sex:this.sex }, baseURL:"http://localhost:3000" }) .then(res=>console.log(res.data))
在google浏览器的network里能够看到传输的数据格式
注意:
使用post方式,在和后端联调时,如果后端接不到数据,需要看network中的
Content-type和数据的格式;
1)、如果data是字符串或者是URLSearchParams
content-type:application/x-www-form-urlencoded
数据格式:form data
2)、如果data是json对象(现在:大部分情况会用这个)
content-type:application/json
数据格式:request payload
读取php接口get 示例:
axios({ url: 'http://localhost:80/php7/get.php', params: {//地址栏数据 a: 11, b: 22 } }).then( res => this.list = res.data )
读取php接口post示例:
let params = new URLSearchParams(); //创建一个url传输的实例 params.append("a", "1"); //添加一个key/value params.append("b", "22"); //添加一个key/value // params.delete("a") 删除 // params.set("a","11");//修改 // params.has("a") 返回 true/false axios({ url: 'http://localhost:80/php7/post.php', method: 'POST', // data: {//非地址栏数据 *** // a: 11, // b: 22 // }, // data: "a=11&b=22", data: params,//URLSearchParams类型 headers: { 'Content-Type': 'application/x-www-form-urlencoded' } //php 默认要求传递请求头 }).then( res => this.list = res.data )
处理并发
axios.all(iterable)//all函数执行所有的请求
axios.spread(callback)//处理响应回来的回调函数
function getbooks(){ return axios.get('api/books'); } function getreaders(){ return axios.get('api/readers'); } axios.all([axios.get('api/books'),axios.get('api/readers')]).then( axios.spread(function (books, readers) { //所有请求完毕后,调用该函数,books是第一个请求的结果,readers是第二个请求的结果 console.log(books.data); console.log(readers.data); }) ); 对比一下,发送一个请求和发送多个请求: //1)、发送一个请求 axios.get('api/books').then(books=>books.data); //2)、发送两个请求 axios.all([axios.get('api/books'),axios.get('api/readers')]).then( axios.spread(function(books,readers){ books.data readers.data }); );
全局配置
所有axios请求的基础地址: axios.defaults.baseURL = 'https://api.example.com'; 所有post请求的默认content-type的值 axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
拦截器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChCNxLzD-1647875628973)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1594522141849.png)]
请求拦截器
请求拦截器是在请求到达后端之前,可以进行“拦截”(所有请求的全局处理),可以对配置进行修改,如:url,检查并在请求头携带token
axios.interceptors.request.use(function(config){ //config是请求时的配置信息。 //一、可以修改baseURL // config.baseURL = "http://localhost:3000"; //二、可以增加token的携带 // token:是验证身份的。 // token的逻辑: //1、token什么时候存储的?当登录成功时,后端会产生一个token,发给前端,前端拿到token, // 保存在cookie或者localStorage。 //2、在请求拦截器里,就需要携带token给服务器端。 // 1)、(假定存储在localStorage中)从localStorage中获取token let token = localStorage.getItem("token"); if(token){ config.headers.authorization = token; } //三、显示loading窗口 // Loading = true; return config; },function(){ 出错 });
响应拦截器
给返回的数据增加公共信息,或者,根据后端返回的状态码,来决定让用户进行再次登录,也可以改变loading的状态为false
axios.interceptors.response.use( function (response) {//response参数是响应对象 response.data.unshift({“goodsid”:“商品编号”,“goodsname”:“商品名称”,“goodsprice”:“商品价格”});//给响应的数据增加一个对象 //隐藏loading窗口 return response; }, function (error) { console.log('响应出错') return Promise.reject(error) })
特点:
从浏览器中创建 XMLHttpRequest/从 node.js 创建 http 请求
支持 Promise API
拦截请求和响应
转换请求数据和响应数据
取消请求 abort
自动转换 JSON 数据
客户端支持防御 CSRF
面试题:
Ajax、jQuery ajax、axios和fetch的区别
1、Ajax:
ajax:最早出现的前后端交互技术,是原生js,核心使用XMLHttpRequest对象,多个请求之间如果有先后关系的话,就会出现回调地狱。
2、Jquery Ajax:
jquery Ajax是原生ajax的封装
3、Fetch:
fetch是ES6新增的,Fetch是基于promise设计的。fetch不是ajax的进一步封装,而是原生js。Fetch函数就是原生js,没有使用XMLHttpRequest对象。
4、axios:
axios是原生ajax的封装,基于promise对象的。Axios也可以在请求和响应阶段进行拦截。它不但可以在客户端使用,也可以在nodejs端使用。
swiper
$nextTick();
什么时候使用$nextTick()
答:当需要操作 ”数据更新影响的(新的)dom时“,就使用$nextTick()。
换种说法:当需要操作dom,而且这个dom是由于数据更新(修改)后引起重新渲染的dom。
https://cn.vuejs.org/v2/guide/reactivity.html 异步更新队列
https://blog.csdn.net/jiang7701037/article/details/95887439 事件循环
特别注意:
n e x t T i c k 跟 是 否 发 送 请 求 没 有 关 系 。 只 要 你 需 要 操 作 d o m 是 由 于 数 据 更 新 的 。 那 就 需 要 使 用 nextTick跟是否发送请求没有关系。只要你需要操作dom是由于数据更新的。那就需要使用 nextTick跟是否发送请求没有关系。只要你需要操作dom是由于数据更新的。那就需要使用nextTick();
<template> <div> <p>{{msg}}</p> <input type="button" value="测试" @click="fn"> </div> </template> <script> export default { name: "Banner", data: function () { return { msg:"hi" }; }, methods:{ fn(){ this.msg = "hello"; this.$nextTick(()=>{ console.log(p标签的innerHTML); }); } } }; </script>
路由
一、路由的作用
vue的路由使用在SPA应用中的组件跳转,相当于多页面的 a标签。官网
二、路由的基本使用
1、引入js文件的方式
1)、引入vue-router.js文件
<script src="js/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
2)、定义若干个组件(为了跳转用)
let goodlist = { template:"<div>商品列表</div>" } let goodsdetail = { template:"<div>商品详情</div>" }
3)、定义路由对象
3.1)路由配置(json数组)
let routes = [ {path:'/goodslist',component:goodlist}, {path:'/goodsdetail',component:goodsdetail} ];
3.2)、实例化一个vueRouter对象
let router = new VueRouter({ routes:routes });
4)、挂载vueRouter对象
实例化vue对象,(把vueRouter对象,挂载(注册)到vue对象里)
let vm = new Vue({ el:"#app", router });
5)、跳转代码(声明式)
<h1>路由跳转</h1> <hr/> <router-link to='/goodslist'>商品列表</router-link> <router-link to='/goodsdetail'>商品详情</router-link> <hr/> <router-view></router-view>
解释:
: 超链, 相当于标签a 。
: 组件显示的位置。
2、模块化的方式(脚手架里)
脚手架安装时,会默认安装vue-router。
1)、安装
npm i vue-router -S (--save的缩写)
2)、定义组件(单文件组件)
如:HelloWorld.vue 、 Home.vue
3)、创建vueRouter对象,并做路由配置和引入
3.1)、创建vueRouter对象(定义路由对象,配置路由)
// src/router/index.js import Vue from 'vue' //1. 引入路由包 import Router from 'vue-router' //2. 安装插件包到Vue上, Vue.use(Router) //3. 路由配置 let routes = [ { path: '/', component: HelloWorld }, { path: '/home', component: Home }, //route 一条路由的配置 ] //4.路由实例 let router = new Router({ //插件路由对象 // routes:routes routes, }); //5.导出路由实例,让它去控制vue根 export default router
3.2)、在main.js中引入vueRouter对象,并植入到根属性
// src/main.js import router from './router/index'; new Vue({ el: '#app', router, //植入根属性,在组件里就可以使用 this.$router ……………… })
4)、跳转
4.1)、声明式跳转
//1、路径使用字符串 <router-link to="/home">声明式跳转</router-link> <router-link to="/home" tag='li' active-class='类名' >声明式跳转</router-link> //2、路径使用对象 <router-link :to="{path:'/home'}">声明式跳转</router-link>
router-link 组件属性:
to:跳转的路径
tag=‘li’ 指定编译后的标签,默认是 a 标签。
active-class=‘类名’ 指定激活后的样式 模糊匹配
exact-active-class=‘类名’ 指定激活后的样式 严格匹配
4.2)编程式跳转(编程式导航)
**1)、this.$router.push(字符串/对象):**添加一个路由 (记录到历史记录)
// 字符串 $router.push('/home') // 对象 $router.push({ path: '/home' })
** r o u t e r : ∗ ∗ 表 示 v u e R o u t e r 对 象 , 由 于 我 们 在 v u e 对 象 里 把 v u e R o u t e r 对 象 植 入 到 根 属 性 里 , 所 以 , 在 组 件 内 部 是 可 以 使 用 router:**表示vueRouter对象,由于我们在vue对象里把vueRouter对象植入到根属性里,所以,在组件内部是可以使用 router:∗∗表示vueRouter对象,由于我们在vue对象里把vueRouter对象植入到根属性里,所以,在组件内部是可以使用router拿到该对象的。
2)、this.$router.replace({name:’…’}) //替换一个路由 (不记录到历史记录)
3)、this.$router.go(-1|1)|back()|forward() //回退/前进
5)、展示
<router-view>展示区</router-view>
三、动态路由匹配
1、路由传参
路由配置
const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头,相当于在path里声明了一个变量 id { path: '/user/:id', component: User } ] })
跳转
//匹配上 '/user/:id' 路径,01001的值会赋给id <router-link to="/user/01001">用户01001的信息</router-link> <router-link to="/user/01002">用户01002的信息</router-link>
组件中获取id的值
模板里的写法: $route.params.id 脚本里的写法: this.$route.params.id
r o u t e r 和 router和 router和route
** r o u t e r : ∗ ∗ 表 示 v u e R o u t e r 对 象 , 由 于 我 们 在 v u e 对 象 里 把 v u e R o u t e r 对 象 植 入 到 根 属 性 里 , 所 以 , 在 组 件 内 部 是 可 以 使 用 router:**表示vueRouter对象,由于我们在vue对象里把vueRouter对象植入到根属性里,所以,在组件内部是可以使用 router:∗∗表示vueRouter对象,由于我们在vue对象里把vueRouter对象植入到根属性里,所以,在组件内部是可以使用router拿到该对象的。
**$route:**表示匹配到的当前路由对象,可以简单粗暴的理解为,路由配置中的某个json对象。当然,这个对象里的信息比路由配置的更多。
2、捕获所有路由或 404 Not found 路由
1)、通配符 *
{ path:'*' 会匹配所有路径,即:所有的路径都会跳到当前对应组件 component: } { path:'/user-*' 会匹配以 `/user-` 开头的任意路径 component: }
注意:路由匹配的顺序是按照路由配置的顺序进行的,所以,你肯定不能把 * 的路径放在最前面,否则,后面的路由配置都不起作用了。
当使用一个通配符时,$route.params
内会自动添加一个名为 pathMatch
参数。它包含了 URL 通过通配符被匹配的部分。
如:
路由配置:
{ // 会匹配以 `/user-` 开头的任意路径 path: '/user-*' }
路由跳转:
this.$router.push('/user-admin')
组件里:
this.$route.params.pathMatch // 'admin'
404
{ path: '*', component: NoPage组件 }
四、命名路由
给路由起个名字,就可以使用名字来指定路由
1、路由配置
const router = new VueRouter({ routes: [ { path: '/user/:userId', name: 'user', component: User } ] })
2、跳转
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
五、重定向
redirect:
{ path: '/', //默认页 redirect: '/home' //配置型跳转 },
路由传参:
一、params
1、传:
1)、动态路由匹配(路由配置里写)
{ name:"user", path: '/user/:id', component: User } //id:相当于声明了变量
2)、跳转时传参
1)、跳转时,使用字符串 //声明式 <router-link to="/user/01001">用户01001的信息</router-link> //编程式 this.$router.push("/user/01001"); 2)、跳转时,使用对象 //声明式: 命名的路由,同时传参 <router-link :to="{ name: 'user', params: { id: '01001' }}">User</router-link> //编程式: this.$router.push({ name: 'user', params: { id: '01001' }})
2、接:
//模板里的写法: $route.params.参数名 //脚本里的写法: this.$route.params.参数名
二、query
1、传:
1)、路由配置不用改(不用动态路由匹配)
{ path: '/user', component: User }
2)、跳转时,使用 path
//1)、跳转时,使用字符串 //声明式: <router-link to="/Reg?userid=007&username=mjl">User</router-link> //编程式: this.$router.push("/Reg?userid=007&username=mjl"); //跳转时,使用对象: //声明式: <router-link :to="{ path: '/user', query: { id: '01001' }}">User</router-link> //编程式: $router.push({ path: '/user', query: { id: '01001' }}) 注意:如果提供了 path,那么params 会被忽略 // 带查询参数,变成 /user?id=01001
params和query的对象写法的区别:
name 对应params
path对应query
2、接
//模板里的写法: $route.query.参数名 //脚本里的写法: this.$route.query.参数名
使用场景的区别:
query:传递多个参数时,query方式对应的地址栏上好阅读。
params:传递一个参数时,比较方便
六、路由传参和props
一个组件在项目中,有两种使用方式,或者说,组件要在浏览器中显示,有两种方式:
1、标签的方式,外部给组件传数据,用props
2、使用路由跳转的方式,外部给组件传数据,用params或者query。
如果, 一个组件需要从外部传入数据, 并且在项目中,这两种方式的使用都会出现,那么,在组件内部就需要适应这两种情况。
如何使用 props
属性将 组件和路由解耦:
props
被设置为 true
,route.params
将会被设置为组件属性。
路由配置:
{ path: '/user/:id', component: User, props: true },
组件:
const User = { props: ['id'], template: '<div>User {{ id }}</div>' }
七、嵌套路由
子路由嵌套
1、路由配置
// src/router/index.js const router = new VueRouter({ routes: [ { path: '/user/:id', component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: 'profile', component: UserProfile //子组件UserProfile显示在父组件User里的<router-view> }, { // 当 /user/:id/posts 匹配成功 // UserPosts 会被渲染在 User 的 <router-view> 中 path: 'posts', component: UserPosts//子组件UserPosts显示在父组件User里的<router-view> } ] } ] })
后台管理系统里,经常要使用子路由嵌套
2、组件的展示:
子组件会展示在父组件里的 的位置。
八、路由模式
路由模式分为两种:hash和history。
区别(面试题):
1)、外观上
hash模式时,路径上有#。
history模式时,路径上没有#。
2)、跟后端有关的区别
hash模式不会给后端发请求
history模式会给后端发请求,需要后端配合。要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html
页面,这个页面就是你 app 依赖的页面。否则,就会返回404。如果后端有同名的url,那么就会找后端的url。
// src/router/index.js let router = new VueRouter({ //插件路由对象 routes, // mode:'hash'//哈希模式 location.href mode:'history'//历史记录 history.pushState });
九、扩展
路由守卫
全局守卫
// src/router/index.js //前置钩子 router.beforeEach((to, from, next) => { // to: 目标路由 // from: 当前路由 // next() 跳转 一定要调用 next(false);//不让走 next(true);//继续前行 next('/login')//走哪 next({path:'/detail/2',params:{},query:{}})//带点货 // 守卫业务 if(to.path=='/login' || to.path=='/reg' ){ next(true) }else{ //是否登录的判断 if(没有登录过){ next('/login'); }else{ next(true); } } }) //后置 router.afterEach((to,from)=>{ //全局后置守卫业务 }) //过程: 1、请求一个路径:如:/Index 2、经历前置守卫(路由配置前) 决定了能去哪个路径 3、根据去的路径,找对应component(路由配置) 4、经过后置守卫(路由配置后) 5、创建组件
路由独享守卫(只有前置)
// src/router/index.js { path: '/user', component: User, //路由独享守卫 beforeEnter: (to,from,next)=>{ //路由独享守卫 前置 console.log('路由独享守卫'); if(Math.random()<.5){ next() }else{ next('/login') } } }
独享,没有后置
组件内部守卫
//在组件内部写: //组件内部钩子 beforeRouteEnter (to, from, next) {//前置 // 不!能!获取组件实例 `this` // 因为当守卫执行前,组件实例还没被创建 // 虽然,这个函数的代码,在组件的内部写着,但是,它编译后,并不隶属于当前组件。 }, beforeRouteUpdate (to, from, next) { // 在当前路径改变,但是该组件被复用时调用 // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 `this` }, beforeRouteLeave (to, from, next) {//后置 // 导航离开该组件的对应路由时调用 // 可以访问组件实例 `this` }
注意:
路由独享守卫,守的是path
组件内部守卫,守的是component
路由元信息
定义路由的时候配置 meta
字段
//src/plugins/router.js { path: '/home', component: Home, meta: { requiresAuth: true } }
访问 meta
字段
this.$route.meta to.meta
路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面(特别是首次)加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
https://blog.csdn.net/jiang7701037/article/details/106794996
转场效果:transition
https://cn.vuejs.org/v2/guide/transitions.html
第三方动画库( V3.5.2——CSS3动画库 )的地址:
官网: https://www.jq22.com/yanshi819
CDN: https://cdn.jsdelivr.net/npm/animate.css@3.5.1
状态(数据)管理(VueX)
官网
大型项目中使用,不是必须的,但是,咱们在学习阶段,在项目中一定要用。
抛出一个问题
vue单页面应用中,每个组件内部的数据在data中存放,供vue组件内部使用,但是,vue组件之间的数据共享怎么办?即A组件要使用B组件里的数据怎么办?
传统的处理方案:父子组件传值、平行组件在跳转时,利用url,路由里的传值等等。
传统处理方案的问题:
1、兄弟组件传值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wu76JQF-1647875628975)(img\08vuex01.png)]
缺点:
1、数据传递复杂,容易出错
2、浪费内存
2)、平行组件,无关组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iouI0b4x-1647875628977)(img\09vuex02.png)]
缺点:
1、数据传递复杂,容易出错
2、浪费内存
解决方案(vueX)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwbeiI44-1647875628978)(img\10vuex03.png)]
vueX的作用
1、vuex能够保存全局数据,供整个应用使用
2、vuex保存的数据是响应式的
3、vuex保存的数据可以跟踪状态的变化
vueX的核心概念(创建vueX.store对象里的配置项):
state : 数据仓库 ,存储所有的 共享数据 ,相当于vue组件里的data
Getter : 在state的基础上 派生的数据, 相当于vue组件里 computed
Mutation:修改state的数据时,用mutation,这与跟踪状态 有关系,只能有同步代码
Action:解决mutation里只能有同步代码的问题,action里可以有异步代码
modules:模块化
vueX的数据流转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wwARrQl3-1647875628979)(img\image-20211102111905111.png)]
vueX的基本使用步骤
1)、安装
npm安装vueX: npm install vuex –save
2)、创建 vueX.store 对象
./src/store/index.js import Vue from 'vue' //引入vueX import Vuex from 'vuex' //把vueX安装到Vue里 Vue.use(Vuex) export default new Vuex.Store({ state:{ id: '01' }, getters:{}, mutations:{}, actions:{} })
3)、把vueX.store对象植入到vue的根属性
./src/main.js import store from './store' new Vue({ el: '#app', store,//把store对象植入到vue的根属性,在vue组件里就可以使用 this.$store拿到vueX.store对象 router …………………… })
4)、组件里获取数据:
//模板里: $store.state.id //脚本里 this.$store.state.id
5)、组件里保存数据
this.$store.state.id = '02' //这个方式虽然可以,但是不推荐,因为,它不能跟踪状态。推荐,强烈推荐(必须)使用mutation来修改数据。
vueX的核心概念详解:
state:
数据仓库 ,存储所有的 共享数据 ,相当于vue组件里的data,是一个单一状态树
使用:
在组件中使用:this.$store.state.属性名。
export default new VueX.Store({ state:{ age:12, isAdult:"未成年", isAllowMarry:false } ………………………… });
组件里获取:
{{$store.state.age}} {{$store.state.isAdult}} {{$store.state.isAllowMarry?"可以结婚了":"不要着急,再等等"}}
Getter : 在state的基础上 派生的数据, 相当于vue组件里 computed
export default new VueX.Store({ state:{ age:12, isAdult:"未成年", isAllowMarry:false }, getters:{ // state:就是仓库的state,不用程序员处理,vuex已经处理好了 ageChina:function(state){ let shi = parseInt(state.age/10); //1 let ge = state.age%10;//2 let str = ""; switch(shi){ case 1:str='一';break; case 2:str='二';break; } str+='十' switch(ge){ case 1:str+='一';break; case 2:str+='二';break; case 3:str+='三';break; case 4:str+='四';break; case 5:str+='五';break; case 6:str+='六';break; case 7:str+='七';break; case 8:str+='八';break; case 9:str+='九';break; } return str+'岁'; } }, ………………………… });
组件里获取
{{$store.getters.ageChina}}
Mutation:
修改state的数据时,用mutation,这与跟踪状态 有关系。换句话说,在vuex中,对mutation的定义(定位)是:修改状态的,即:在mutation提交的前后,状态应该是不一样的。当然了,也会在vue的dev-tools工具中看到跟踪状态的效果。
在vuex中,强烈建议(必须)使用mutation改变state中的值。可以在vuex对象中使用严格模式来检测:
const store = new Vuex.Store({ strict: true })
export default new VueX.Store({ state:{ age:12, isAdult:"未成年", isAllowMarry:false }, getters:{ ageChina:function(state){ let shi = parseInt(state.age/10); //1 let ge = state.age%10;//2 let str = ""; switch(shi){ case 1:str='一';break; case 2:str='二';break; } str+='十' switch(ge){ case 1:str+='一';break; case 2:str+='二';break; case 3:str+='三';break; case 4:str+='四';break; case 5:str+='五';break; case 6:str+='六';break; case 7:str+='七';break; case 8:str+='八';break; case 9:str+='九';break; } return str+'岁'; } }, // mutations:是跟踪状态。这里面只能有同步代码,这是必须的。 mutations:{ // incAge(state,num){ // state.age+=num; //state:就是store对象里的state,不用程序员传入 //payload:就是形参,叫作载荷 incAge(state,payload){ state.age+=payload.num; if(state.age>=18){ state.isAdult = "已成年"; }else{ state.isAdult = "未成年"; } if(state.age>=22){ state.isAllowMarry = true; }else{ state.isAllowMarry = false; } } }, ………………………………………… });
提交mutation
//组件里提交 this.$store.commit('incAge',num); //action提交 incAge(context,num){ context.commit('incAge',num); }
Action:
解决mutation里只能有同步代码的问题,action里可以有异步代码
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
export default new VueX.Store({ state:{ age:12, isAdult:"未成年", isAllowMarry:false }, getters:{ ageChina:state=>{ let shi = parseInt(state.age/10); //1 let ge = state.age%10;//2 let str = ""; switch(shi){ case 1:str='一';break; case 2:str='二';break; } str+='十' switch(ge){ case 1:str+='一';break; case 2:str+='二';break; case 3:str+='三';break; case 4:str+='四';break; case 5:str+='五';break; } return str+'岁'; } }, // mutations:是跟踪状态。这里面只能有同步代码,这是必须的。 mutations:{ // incAge(state,num){ // state.age+=num; incAge(state,payload){ state.age+=payload.num; if(state.age>=18){ state.isAdult = "已成年"; }else{ state.isAdult = "未成年"; } if(state.age>=22){ state.isAllowMarry = true; }else{ state.isAllowMarry = false; } } }, // 如果出现异步操作,就需要使用action,action不是必须的。 actions:{ // incAge(context,num){ // context.commit('incAge',num); // } // incAge(context,payload){ // context.commit('incAge',payload); // } //context:是store对象 incAge(context,payload){ // context.commit('incAge',payload); context.commit(payload); } } });
Action和mutation的区别
在代码的角度上,action是来提交mutation的
在用途(意义)上:区分 actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。 vuex 真正限制你的只有 mutation 必须是同步的这一点。
当然actions可以包含任何异步操作,如果程序员自己想处理异步操作,也可以不使用actions。
补充:
vuex推荐:在派发action和 提交mutation时,参数使用对象的方式
//一、组件里派发action的代码 this.$store.dispatch({ type:"incAge", //incAge 是action的名字 username:"haha", userpass:"123" }); //二、vuex里的代码: //1、action的定义 actions:{ incAge(context,payload){ console.log("payload",payload); axios({ url:"/inc", params:payload }) .then(res=>{ context.commit({ type:"incAge1", //mutation的名字 count:res.data.count }); }); } } //2、mutation的定义: mutations:{ incAge1(state,payload){ state.age+= payload.count; if(state.age>=18){ state.isAdult = "已成年" }else{ state.isAdult = "未成年" } } },
示例:loading
1、vuex中:
在vuex中定义一个全局的属性:isLoading,来控制loading图片的显示和隐藏
state:{ isLoading:false, } mutations:{ changeLoading(state,payload){ state.isLoading = payload.isLoading }, }
2、在App.vue
<van-loading class="loading" type="spinner" color="#1989fa" v-show="isLoading" /> <script> import {mapState} from "vuex"; export default { name: 'App', computed:{ ...mapState(["isLoading"]), } } </script> <style scoped> .van-loading__spinner{ position: fixed; left:0; top:0; right: 0; bottom: 0; margin:auto; } </style>
3、在请求拦截器和响应拦截器里
import store from "../store"; // 请求拦截器:请求的全局工作,发生在到达后端之前 axios.interceptors.request.use(function(config){ // 处理loading (把loading显示) store.commit({ type:"changeLoading", isLoading:true }); return config; }); // 响应拦截器:响应全局工作,发生,后端响应完毕。在axios请求进入到then的回调函数之前 axios.interceptors.response.use(function(res){ // 处理loading(把loading隐藏) store.commit({ type:"changeLoading", isLoading:false }); return res; });
Modules
当项目比较大时,所有的全局数据存放在state里,会非常混乱,怎么办?使用module,把数据分门别类的进行处理,即:模块化。
每个模块是一个独立的store。然后由总体的store引入所有的分模块store。
如下:
两个模块分别管理不同类型的数据:
./src/store/moduleA.js export default { state: {count:1}, mutations: { ... }, actions: { incCount(context){ console.log("moduleA的action"); setTimeout(()=>{ context.commit("incCount"); },2000); } }, getters: { ... } } ./src/store/moduleB.js export default { state: {count:2}, mutations: { ... }, actions: { incCount(context){ console.log("moduleB的action"); setTimeout(()=>{ context.commit("incCount"); },2000); } }, getters: { ... } }
在总的store里包含所有的模块:
./src/store/index.js import Vue from "vue"; import vueX from "vuex"; import moduleA from "./moduleA"; import moduleB from "./moduleB"; Vue.use(vueX); export default new vueX.Store({ modules:{ moduleA:moduleA, moduleB:moduleB } //简写: modules:{ moduleA,moduleB } });
组件里使用数据时,多加个模块名即可,如:
$store.state.moduleA.count // -> moduleA 的状态 $store.state.moduleB.count // -> moduleB 的状态
组件里派发action(或者提交mutation)时,如果,直接写action(mutation)的名字,那么就会找到所有同名的action(mutation)。
//如: //在组件内容,派发action this.$store.dispatch({ type:"incCount" }); 那么就会,派发给moduleA和moduleB里的incCount。即:moduleA和moduleB里的incCount都被执行了。如果不希望这样,那就用命名空间
模块(Module)里的命名空间(namespaced:true)。
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
1)、模块定义时,加上namespaced:true
export default { namespaced:true, state:{ count:1 }, mutations:{ …………………… }, actions:{ incCount(context){ console.log("moduleA的action"); setTimeout(()=>{ context.commit("incCount"); },2000); } } }
2)、组件里派发action时,加上模块名
this.$store.dispatch('moduleA/incCount');
**酌情扩展 **
mapState, mapGetters,mapMutations, mapActions
https://vuex.vuejs.org/zh/guide/state.html
1、mapState:
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键:
//1、vueX export default new vueX.Store({ state:{ count:10 } }) //2、组件里 import { mapState } from "vuex"; export default { name: "Store01", data() { return {}; }, //1)、把vueX中的state用计算属性,很麻烦 // computed:{ // count(){ // return this.$store.state.count // } // }, // 2)、使用mapState(不能再写其它计算属性) // computed: mapState({ // // 箭头函数可使代码更简练 // count: state => state.count, // }), // 3)、使用mapState继续简写(不能再写其它计算属性) // computed: mapState(['count']), // 4)、使用mapState,这样写 computed:{ ...mapState(['count']), a:function(){ return 250; }, } };
2、mapGetters
在computed里映射
computed: { ...mapState(["count", "age", "isAdult"]), ...mapGetters(["ageChina"]), },
3、mapMutations和mapActions
在methods里映射
methods: { ...mapMutations(["incAge1"]), ...mapActions(["incAge"]), }
反向代理
在前后端分离开发的场景,前端有个服务器(提供页面)。后端也有个服务器(提供接口)。
1、开发环境,前端要连后端的接口,就会出现跨域问题。
2、生产(发布)环境:
1)、如果还是前后端分离(在不同的服务器上)。依然有跨域问题(nginx)
2)、如果前后端代码在一起(一个服务器),不存在跨域问题
跨域的解决方案:
1)、jsonp
2)、CORS(后端配合) :cross origin resource sharing 后端解决跨域的方案
php 中,这么写:header(“Access-Control-Allow-Origin:*”);
3)、反向代理(前端webpack的devServer)
反向代理
一、为啥要叫反向代理
正向代理隐藏真实客户端,反向代理隐藏真实服务端,让浏览器依然是同源。
二、反向代理解决的问题
就是跨域,在前后端联调的情况下,前端有服务器,后端也有服务器,那么联调时,就存在跨域问题
三、反向代理的原理
通过伪造请求使得http请求为同源的,然后将同源的请求发送到反向代理服务器上,由反向代理服务器去请求真正的url,这样就绕过直接请求真正的url导致跨域问题。
四、vue-cli2.x里反向代理使用步骤和配置
1、安装axios与http-proxy-middleware(vue-cli中默认已经安装)
2、在config/index.js中的dev中配置
3、在dev配置对象中找到proxyTable:{ }
4、添加如下配置(把对“/api”开头的访问转换成对 http://www.itzhihu.cn的访问):
proxyTable: {
‘/api’: { // 要替换的地址,在本地新建一个/api的访问路径 。
target: ‘http://www.itzhihu.cn’, // 替换成啥,要访问的接口域名 。
changeOrigin: true, // 是否跨域
pathRewrite: {
‘^/api’: ‘’ //重写接口,去掉/api, 在代理过程中是否替换掉/api/路径
}
}
}
这样以后访问以“/api”开头的地址都会转向http://www.itzhihu.cn的访问
五、图示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ogk0jqAd-1647875628982)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600184095886.png)]
六、vue-cli3+配置反向代理
1.打开(新建)项目根/vue.config.js
写上如下代码:
module.exports = { devServer:{ //设置代理 proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换 '/api': { //axios访问 /api == target + /api target: 'http://localhost:3001', changeOrigin: true, //创建虚拟服务器 pathRewrite: { '^/api': '' //重写接口,去掉/api, 在代理过程中是否替换掉/api/路径 } } } } }
注意:
1、修改完 vue.config.js文件后,必须要重启服务器
2、如果你复制了笔记上的代码,而且出现问题,那么,删掉看不见的中文空格和注释。
postman
跟后端联调项目
一、联调前:保证前后端各自没有问题
1.后端用postman测试一下(跟接口文档一致)
2.前端连接的jsonserver,(跟接口文档一致)
目的:说明前后端的程序逻辑没有问题。
二、前后端的计算机连在同一个局域网(能够连上)
可以在cmd 里,用命令 ping 对方的ip地址,来测试是否在同一个局域网里。
ping 后端的ip地址
目的:保证前后端能够连通
三、前端通过postman测试接口(这一步很重要)
切记,切记:前端先用postman测试接口,如果响应的数据没有问题(能够返回数据,并且返回的数据格式跟接口文档一致),说明,和后端的链接没有问题,也能看出后端给的数据格式是否正确,前后端是否都根据接口进行了开发。
注意:前后端的content-type是否一致。
目的:验证后端的程序有没有问题。
四、跨域
1)、前端解决跨域(反向代理)
Vue-cli2.X:
把config/index.js中的反向代理改一下:
把target改成后端的ip地址和端口号
proxyTable: {
‘/api’: { //所有“/api”开头的请求都会进行代理(转)
target: ‘http://10.35.167.126:8080’, //所有“/api”开头的请求都会转到 http://10.35.167.126:8080
changeOrigin: true,
pathRewrite: {
‘^/api’: ‘’ // 去掉"/api"
}
}
}
Vue-cli3/4
1.打开(新建)项目根/vue.config.js
写上如下代码:
module.exports = { devServer:{ //设置代理 proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换 '/api': { //axios访问 /api == target + /api target: 'http://localhost:3001', changeOrigin: true, //创建虚拟服务器 pathRewrite: { '^/api': '' //重写接口,去掉/api, 在代理过程中是否替换掉/api/路径 } } } } }
注意:一旦修改了配置(vue.config.js),必须重启服务器(npm run serve)
2)、后端解决跨域(cors)
四、发送请求,获取数据
测试功能
五、如果跟后端的交互还有问题,需要查看network:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rniKoUbU-1647875628983)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1607395011467.png)]
移动端事件
1、click事件
Click:单击事件
类似于PC端的click,移动端也有click,但在移动端中,连续click的触发有200ms ~ 300ms的延迟
为什么移动端不用click
移动端的click有300ms延迟的问题,在移动端浏览器中,连续两次点击是缩放操作,所以在用户点击屏幕后,浏览器会检查在300ms内是否有另一次点击,如果没有才触发click事件。因为有延迟,所以不使用。
2、touch类事件
触摸事件,有touchstart、touchmove、touchend、touchcancel 四种
touchstart:手指触摸到屏幕会触发
touchmove:当手指在屏幕上移动时,会触发
touchend:当手指离开屏幕时,会触发
touchcancel:可由系统进行的触发,比如手指触摸屏幕的时候,突然alert了一下,或者系统中其他打断了touch的行为,则可以触发该事件
onorientationchange: 屏幕旋转事件
示例:
<div id="box"> </div> window.onload = function () { //setInterval(()=>{alert("弹出东西了");},2000); document.getElementById("box").onclick = function(){ //click有延迟300毫秒的情况,所以,先触发的是ontouchstart. //如果在300毫秒内不放开,则不触发click事件,只触发touchend事件 //如果在300毫秒内放开了,就会触发click事件. this.innerHTML +="onclick:点击了<br/>"; } document.getElementById("box").ontouchstart = function(){ this.innerHTML +="ontouchstart<br/>";//手指触摸到屏幕 } document.getElementById("box").ontouchend = function(){ this.innerHTML +="ontouchend<br/>";//手指离开屏幕 } document.getElementById("box").ontouchmove = function(){ this.innerHTML +="ontouchmove<br/>";//手指在屏幕上移动 } document.getElementById("box").ontouchcancel = function(){ console.log("touchcancel");//当两秒钟后弹出alert 后,touchcancel就被触发了。这个不常用 }}
3、touch的优先级高于click
1)、如果touch的持续时间在300ms以上,就不会触发click事件
当你在红盒子里,按下300ms以上,再放开时,不触发click事件
触发顺序: touchstart,touchend
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9g2El4yu-1647875628984)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600248195772.png)]
2)、当你在红盒子里,按下不超过300ms,再放开时,触发顺序是:
触发顺序: touchstart,touchend,click。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t9Ry9tqI-1647875628993)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600248201082.png)]
3)、如果手指在屏幕上移动了,就不会触发click事件。
当你在红盒子里,按下,并移动,再放开,就算不超过300ms的时间,
也不会触发click事件,因为,移动时,就不触发click事件了
(移动事件的优先级高于click)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FtsGstMM-1647875628994)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600248226674.png)]
4、 阻止click事件的触发
我们在开发移动端时,肯定只希望触发touch类型的事件,所以,如何阻止click事件的触发。
由于:当用户在点击屏幕的时候,系统会触发touch事件和click事件,touch事件优先级高,touch事件触发完毕后, 才会去触发click事件.
所以:在touch事件里使用event.preventDefault()就可以阻止click事件的触发
示例:
document.getElementById("box").ontouchstart = function(event){ this.innerHTML +="ontouchstart<br/>";//手指触摸到屏幕 event.preventDefault(); } document.getElementById("box").onclick = function(){ this.innerHTML +="onclick:点击了<br/>"; }
5、点透(面试题)
什么是点透
1、A 和 B两个html元素不是后代和继承关系(如果是后代继承关系的话,就直接是冒泡了), A的z-index大于B,把A显示在B之上
2、A元素绑定touch事件, 在touch事件处理函数里,让A立即消失,
3、B元素绑定click事件
示例:
<style type="text/css"> #boxparent{ position: relative; } #box1 { position: absolute; z-index: 1; background: red; width: 100px; height: 100px; } #box2{ background: green; width: 200px; height: 200px; } </style> <div id="boxparent"> <div id="box1"> </div> <div id="box2"> </div> </div> <script type="text/javascript"> var box1 = document.getElementById("box1"); var box2 = document.getElementById("box2"); box1.ontouchstart=function(e) { box1.style.display = 'none'; }; box2.onclick = function() { console.log('box2被点了'); } </script>
为什么会这样?
当用户在box1元素上触摸屏幕时,先触发box1元素的touch事件,然后隐藏了box1,当click(延迟200-300ms)触发时,发现没有box1了,只有box2,所以,就触发了box2元素的click事件。
点透和冒泡的区别:
冒泡:是元素之间存在父子关系。
点透:元素之间没有父子关系(叠放关系),只是由于click在移动端里有200-300ms的延时造成的。
6、第三方的移动端事件库
1)、zepto.js
Zepto是一个轻量级的针对现代高级浏览器的JavaScript库, 它与jquery有着类似的api。 如果你会用jquery,那么你也会用zepto。针对移动端开发的JavaScript库(不仅仅只是事件库)
-
tap
— 元素 tap 的时候触发。(叩击) -
singleTap
anddoubleTap
— 这一对事件可以用来检测元素上的单击和双击。(如果你不需要检测单击、双击,使用tap
代替)。 -
longTap
— 当一个元素被按住超过750ms触发。 -
swipe
,swipeLeft
,swipeRight
,swipeUp
,swipeDown
— 当元素被划过时触发。(可选择给定的方向)
示例:
<div id="box"> </div> $("#box").on("tap", function () { $('#box').append('<li>tap:单击屏幕</li>'); }); $("#box").on("singleTap", function () { $('#box').append('<li>singleTap:单击屏幕</li>'); }); $("#box").on("doubleTap", function () { $('#box').append('<li>双击屏幕</li>'); }); $("#box").on("longTap", function () { $('#box').append('<li>长安屏幕750ms</li>'); }); $("#box").on("swipe", function () { $('#box').append('<li>划过屏幕</li>'); }); $("#box").on("swipeLeft", function () { $('#box').append('<li>划过屏幕(左)</li>'); }); $("#box").on("swipeRight", function () { $('#box').append('<li>划过屏幕(右)</li>'); }); $("#box").on("swipeUp", function () { $('#box').append('<li>划过屏幕(上)</li>'); }); $("#box").on("swipeDown", function () { $('#box').append('<li>划过屏幕(下)</li>'); }); </script>
2)、touch.js
介绍:
touch.js 是百度开发的一款插件,针对移动端应用的事件插件
绑定事件
touch.on( element, types, callback );
功能描述:事件绑定方法,根据参数区分事件绑定和事件代理。
解除事件绑定
touch.off( element, types, callback )
参数 | 类型 | 描述 |
---|---|---|
element | element或string | 元素对象、选择器 |
types | string | 事件的类型(多为手势事件),多个事件用空格分开 |
callback | function | 事件处理函数,移除函数与绑定函数必须为同一引用; |
事件类型
缩放:
事件名 | 描述 | |
---|---|---|
pinchstart | 缩放手势起点 | |
pinchend | 缩放手势终点 | |
pinch | 缩放手势 | |
pinchin | 收缩 | |
pinchout | 放大 |
旋转
描述 | ||
---|---|---|
rotateleft | 向左旋转 | |
rotateright | 向右旋转 | |
rotate | 旋转 |
滑动
参数 | 描述 |
---|---|
swipestart | 滑动手势起点 |
swiping | 滑动中 |
swipeend | 滑动手势终点 |
swipeleft | 向左滑动 |
swiperight | 向右滑动 |
swipeup | 向上滑动 |
swipedown | 向下滑动 |
swipe | 滑动 |
拖动
dragstart | 拖动屏幕起始 |
---|---|
drag | 拖动屏幕中 |
dragend | 拖动屏幕结束 |
按键
hold | 长按屏幕 |
---|---|
tap | 单击屏幕 |
doubletap | 双击屏幕 |
示例:
<div id="box"> </div> <script type="text/javascript" src="js/zepto.min.js"></script> <script type="text/javascript" src="https://cdn.bootcss.com/touchjs/0.2.14/touch.js"></script> <script type="text/javascript"> touch.on("#box", 'tap', function () { console.log("tap"); }); touch.on("#box", 'swipestart', function () { console.log("swipestart"); }); </script>
touch的配置
touch.config(config)
config为一个对象
{
tap: true, //tap类事件开关, 默认为true
doubleTap: true, //doubleTap事件开关, 默认为true
hold: true, //hold事件开关, 默认为true
holdTime: 650, //hold时间长度
swipe: true, //swipe事件开关
swipeTime: 300, //触发swipe事件的最大时长
swipeMinDistance: 18, //swipe移动最小距离
swipeFactor: 5, //加速因子, 值越大变化速率越快
drag: true, //drag事件开关
pinch: true, //pinch类事件开关
}
事件代理格式
1)、事件代理绑定
touch.on( delegateElement, types, selector, callback );
2)、解除事件代理绑定
touch.off( delegateElement, types, selector, callback )
参数 | 类型 | 描述 |
---|---|---|
delegateElement | element或string | 事件代理元素或选择器 |
types | string | 手势事件的类型,可接受多个事件以空格分开; |
selector | string | 代理子元素选择器 |
callback | function | 事件处理函数 |
示例:
<ul id="touchs"> <li> 第一个</li> <li> 第二个 </li> <li> 第三个</li> </ul> touch.on("#touchs","tap","li",function(event){ console.log("li被点击了"); let e = event || window.event; console.log(e.target.tagName); });
事件处理对象(event)
事件处理函数的第一个参数为事件对象,除了原生属性之外,百度手势库还提供了部分新属性
属性 | 描述 |
---|---|
originEvent | 触发某事件的原生对象 |
type | 事件的名称 |
rotation | 旋转角度 |
scale | 缩放比例 |
direction | 操作的方向属性 |
fingersCount | 操作的手势数量 |
position | 相关位置信息,不同的操作产生不同的位置信息 |
distance | swipe类两点之间的位移 |
distanceX, x | 手势事件x方向的位移值,向左移动时为负数 |
distanceY, y | 手势事件y方向的位移值,向上移动时为负数 |
angle | rotate事件触发时旋转的角度 |
duration | touchstart与touchend之间的时间戳 |
factor | swipe事件加速度因子 |
startRotate() | 启动单指旋转方法,在某个元素的touchstart触发时调用 |
第三方(UI)组件
使用一些别人开发好的组件、组件库、插件、ui库,来提高项目开发进度,组件库通常是某家公司开发并开源出来,获取方式可以通过npm、github查询,或者百度vue组件库,这里有一些推荐1,、推荐2、推荐3
使用方式
//安装 npm i vue-swipe --save 安装 //引入 import './node_modules/vue-swipe/dist/vue-swipe.css'; 引入样式 import { Swipe, SwipeItem } from 'vue-swipe'; 引入组件 Vue.component('swipe', Swipe); //注册安装到全局 Vue.component('swipe-item', SwipeItem); //注册到选项components 私有使用
组件类别
pc端、后台管理
- element-ui 饿了么 √
- iview 个人
- ant design 蚂蚁金服
移动端、客户端(前台系统)
- mint-ui 饿了么
- vant 有赞 电商 √
- vue-material
- muse-ui
- VUX
- cube-ui
- vonic
- Vue-Carbon
- YDUI
通用
- bootstrap4/3
- ameizi
elementUI
官网
安装
npm i element-ui -S
整体引入全局使用
import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI);
按需引入
npm install babel-plugin-component -D //修改babel配置 baberc/babel.config.js //添加: "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] //全局使用: 所有应用内部组件直接使用 <el-button></..> import { Button } from 'element-ui'; Vue.component(Button.name, Button); | Vue.use(Button) //局部使用: 只有当前组件可使用 import { Select, Option } from 'element-ui'; components:{ 'bulala':Select, [Option.name]:Option, },
mintUI
官网
安装
npm i mint-ui -S
整体引入
import Mint from 'mint-ui'; import 'mint-ui/lib/style.css' Vue.use(Mint);
按需引入
//全局使用: npm install babel-plugin-component -D import { Button } from 'mint-ui'; Vue.component(Button.name, Button); //babel.config.js配置: 添加 "plugins": [ [ "component", { "libraryName": "mint-ui", "style": true } ] ] //组件内部引入,只有当前组件可使用 import { Button } from 'mint-ui'; components:{ //'bulala':Button, [Button.name]:Button, },
vant
官网
整体引入
import Vant from 'vant'; import 'vant/lib/index.css'; Vue.use(Vant); //全局使用: <van-xxx></van-xxx>
按需引入
npm i babel-plugin-import -S // 对于使用 babel7 的用户,可以在 babel.config.js 中配置 module.exports = { plugins: [ ['import', { libraryName: 'vant', libraryDirectory: 'es', style: true }, 'vant'] ] }; //局部使用: import { Button, Cell } from 'vant'; components:{Button,Cell} //<Button></Button> //全局使用 import { Button } from 'vant'; Vue.use(Button); //<van-xxx></van-xxx>
组件样式顺序
- 修改主题
- 使用props
- 添加 class/style
- 查看元素,查询相关样式名,修改编译后的样式
- scope 影响
css解决: .a >>> .b { /* … */ } 深度选择器
Sass解决: .a{ /deep/ .b{} }
举例
app组件
Tabbar 标签栏
home组件
Search 搜索/轮播/Tab 标签页
category组件
Tab 标签页
follow组件
pull-refresh 下拉刷新 / 单元格 cell
column组件
Popup 弹出层
detail组件
NavBar 导航栏/栅格布局
ShopCart组件
NavBar 导航栏/SwipeCell 滑动单元格>Card 卡片/SubmitBar 提交订单栏/stepper步进器
user组件
flex布局/van-tag标记/Panel 面板/cell-group 单元格组/ icon图标
login组件
NavBar 导航栏/field输入框
reg组件
van-uploader 文件上传
下拉刷新上拉加载(Better-scroll)
介绍
better-scroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化,不仅可以做普通的滚动列表,还可以做轮播图、picker 等等。better-scroll 是基于原生 JS 实现的,不依赖任何框架。
http://ustbhuangyi.github.io/better-scroll/doc/api.html
安装和使用
1、安装:npm install better-scroll --save
2、引入:import BScroll from ‘better-scroll’
3、使用:
let scroll = new BScroll(’.wrapper’,{
scrollY: true,
click: true
})
注意:
better-scroll 的初始化时机很重要,因为它在初始化的时候,会计算父元素和子元素的高度和宽度,来决定是否可以纵向和横向滚动。因此,我们在初始化它的时候,必须确保父元素和子元素的内容已经正确渲染了。如果子元素或者父元素 DOM 结构发生改变的时候,必须重新调用 scroll.refresh() 方法重新计算来确保滚动效果的正常
在vue脚手架里使用(数据写死)
假定组件文件为:./src/components/Books.vue //1、引入: import BScroll from 'better-scroll' //2、使用 <template> <div class="wrapper"> <ul> <div v-show="downShow">加载中…………………………</div> <li v-for="(book,index) in books" :key="index"> <p>编号:{{book.id}}</p> <img src="../assets/img/img1.jpg" /> <p>书名:{{book.name}}</p> <p>价格:¥{{book.price}}</p> </li> <div v-show="upShow">加载中…………………………</div> </ul> </div> </template> <script> import BScroll from "better-scroll"; export default { name: "Books", data: function() { return { downShow: false, //下拉时显示 upShow: false,//上拉时显示 books: [ { id: "878913", name: "红楼梦", author: "曹雪芹", price: 52, type: "hot" }, { id: "01001", name: "西游记", author: "曹雪芹", price: 52, type: "recommend" }, { id: "01002", name: "霸道总裁", author: "王馨", price: 50, type: "recommend" }, { id: "01004", name: "侠客行", author: "金庸", img: "img/img7.jpg", price: 53, type: "武侠" }, { id: "01005", name: "绝代双骄", author: "古龙", img: "img/img8.jpg", price: 58, type: "武侠" }, { id: "01006", name: "三体", author: "王馨", img: "img/img9.jpg", price: 59, type: "科幻" }, { id: "01007", name: "流浪地球", author: "魏瑞峰", img: "img/img10.jpg", price: 68, type: "科幻" } ], scroll: null }; }, mounted() { //1、实例化 Better-scroll对象 this.scroll = new BScroll(".wrapper", { scrollY: true, //开启纵向滚动。 //click: true, pullDownRefresh: { //下拉刷新的配置 threshold: 30 // 当下拉到超过顶部 30px 时,触发 pullingDown 事件 }, pullUpLoad: { //上拉加载的配置 threshold: -50 // 在上拉到超过底部 50px 时,触发 pullingUp 事件 } }); //2、绑定事件 pullingDown this.scroll.on("pullingDown", () => { this.downShow = true; //显示加载中……的文字或者图片 this.getDataUnshift(); //发送请求获取数据 }); //3、绑定事件 pullingUp this.scroll.on("pullingUp", () => { this.upShow = true; //显示加载中……的文字或者图片 this.getDataPush(); //发送请求获取数据 }); }, methods: { getDataUnshift() { setTimeout(() => { let arr = []; for (let i = 0; i < 5; i++) { arr.push({ id: parseInt(Math.random() * 1000) + "", name: "三国", author: "罗贯中", price: (Math.random() * 100).toFixed(2) }); } this.books = [...arr, ...this.books]; this.downShow = false; //隐藏加载中……的文字或者图片 this.$nextTick(() => { this.refresh(); //渲染后要重新计算父子元素的高度 }); }, 1000); }, getDataPush(cb) { setTimeout(() => { let arr = []; for (let i = 0; i < 5; i++) { arr.push({ id: parseInt(Math.random() * 1000) + "", name: "三国", author: "罗贯中", price: (Math.random() * 100).toFixed(2) }); } this.books = [...this.books, ...arr]; this.upShow = false;//隐藏加载中……的文字或者图片 this.$nextTick(() => { this.refresh(); //渲染后要重新计算父子元素的高度 }); }, 1000); }, refresh() { this.scroll.finishPullDown(); this.scroll.finishPullUp(); this.scroll.refresh(); //重新计算元素高度 } } }; </script> <style scoped> .wrapper { width: 100%; height: 800px; } img { width: 100%; } </style>
在vue脚手架里使用(数据来自后端)
需要使用$nextTick()
<template> <div class="box"> <ul> <div v-show="downShow">加载中…………………………</div> <li v-for="book in books" :key="book.id"> <p>书号:{{ book.id }}</p> <p>书名:{{ book.name }}</p> <img :src="book.img" /> <p>作者:{{ book.author }}</p> <p>价格:{{ book.price }}</p> </li> <div v-show="upShow">加载中…………………………</div> </ul> </div> </template> <script> import axios from "axios"; import BScroll from "better-scroll"; export default { name: "BookJia", data() { return { downShow: false, //下拉时显示 upShow: false,//上拉时显示 books: [], pageIndex:0, scroll:null }; }, created() { this.pageIndex++; axios({ url: "/booklist?_page="+this.pageIndex+"&_limit=4", }).then((res) => { this.books = res.data; this.$nextTick(() => { this.scroll = new BScroll(".box", { scrollY: true, pullDownRefresh: { //下拉刷新的配置 threshold: 30 // 当下拉到超过顶部 30px 时,触发 pullingDown 事件 }, pullUpLoad: { //上拉加载的配置 threshold: -50 // 在上拉到超过底部 50px 时,触发 pullingUp 事件 } }); this.scroll.on("pullingDown",()=>{ this.downShow = true; }) this.scroll.on("pullingUp",()=>{ this.upShow = true; this.pageIndex++; axios({ url: "/booklist?_page="+this.pageIndex+"&_limit=4", }).then((res) => { this.books = this.books.concat(res.data); // this.books = [...this.books,...res.data]; this.upShow = false; this.$nextTick(()=>this.refresh()); }) }) }); }); }, methods:{ refresh() { this.scroll.finishPullDown(); this.scroll.finishPullUp(); this.scroll.refresh(); //重新计算元素高度 } } }; </script> <style scoped> .box { width: 100%; height: 612px; overflow: auto; border:2px solid red; } img { width: 100%; height: 200px; } </style>
在vue脚手架里使用(封装了)
<template> <div class="box"> <ul> <div v-show="downShow">加载中…………………………</div> <li v-for="book in books" :key="book.id"> <p>书号:{{ book.id }}</p> <p>书名:{{ book.name }}</p> <img :src="book.img" /> <p>作者:{{ book.author }}</p> <p>价格:{{ book.price }}</p> </li> <div v-show="upShow">{{upMsg}}</div> </ul> </div> </template> <script> import axios from "axios"; import BScroll from "better-scroll"; export default { name: "BookJia", data() { return { downShow: false, //下拉时显示 upShow: false, //上拉时显示 books: [], pageIndex: 0, maxPageIndex:4, //最大页码数 scroll: null, upMsg:"加载中…………………………" }; }, created() { //1、获取最大的页码数。赋给 maxPageIndex this.maxPageIndex = 4; //2、获取数据,并且初始化better-scroll this.getBooks(this.initBScroll); }, methods: { // 获取数据 getBooks(cb) { console.log("发送请求"); this.pageIndex++; axios({ url: "/booklist?_page=" + this.pageIndex + "&_limit=4", }).then((res) => { cb(res.data); }); }, //初始化better-scroll; initBScroll(data) { this.books = data; this.$nextTick(() => { //1、 创建better-sroll; this.createBScroll(); //2、 给better-scroll添加事件 this.addEvent(); }); }, // 创建better-sroll; createBScroll(){ this.scroll = new BScroll(".box", { scrollY: true, pullDownRefresh: { //下拉刷新的配置 threshold: 30, // 当下拉到超过顶部 30px 时,触发 pullingDown 事件 }, pullUpLoad: { //上拉加载的配置 threshold: -50, // 在上拉到超过底部 50px 时,触发 pullingUp 事件 }, }); }, // 给better-scroll添加上拉和下拉的事件 addEvent() { this.scroll.on("pullingDown", () => { this.downShow = true; }); this.scroll.on("pullingUp", () => { console.log(this.pageIndex); this.upShow = true; if(this.pageIndex>=this.maxPageIndex){ this.upMsg = "------我是有底线的----------"; return; } // 再次获取数据,更新better-scroll。 this.getBooks(this.updateBScroll); }); }, //更新better-scroll; updateBScroll(data){ this.books = this.books.concat(data); this.upShow = false; this.$nextTick(() => this.refresh()); }, refresh() { this.scroll.finishPullDown(); this.scroll.finishPullUp(); this.scroll.refresh(); //重新计算元素高度 }, }, }; </script> <style scoped> .box { width: 100%; height: 612px; overflow: auto; border: 2px solid red; } img { width: 100%; height: 200px; } </style>
构造函数的选项
scrollX
- 类型:Boolean
- 默认值: false
- 作用:当设置为 true 的时候,可以开启横向滚动。
- 备注:当设置 eventPassthrough 为 ‘horizontal’ 的时候,该配置无效。
scrollY
- 类型:Boolean
- 默认值:true
- 作用:当设置为 true 的时候,可以开启纵向滚动。
- 备注:当设置 eventPassthrough 为 ‘vertical’ 的时候,该配置无效。
click
- 类型:Boolean
- 默认值:false
- 作用:better-scroll 默认会阻止浏览器的原生 click 事件。当设置为 true,better-scroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 _constructed,值为 true。但是自定义的 click 事件会阻止一些原生组件的行为,如 checkbox 的选中等,所以一旦滚动列表中有一些原生表单组件,推荐的做法是监听 tap 事件
probeType
- 类型:Number
- 默认值:0
- 可选值:1、2、3
- 作用:有时候我们需要知道滚动的位置。当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件, 函数节流 ; 当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。
对象的属性:
maxScrollY
- 类型:Number
- 作用:scroll 最大纵向滚动位置。
- 备注:scroll 纵向滚动的位置区间是 0 - maxScrollY,并且 maxScrollY 是负值。
对象的方法:
refresh()
- 参数:无
- 返回值:无
- 作用:重新计算 better-scroll,当 DOM 结构发生变化的时候务必要调用确保滚动的效果正常。
对象的事件
scroll
- 参数:{Object} {x, y} 滚动的实时坐标
- 触发时机:滚动过程中,具体时机取决于选项中的 [probeType](
vue项目打包上线
1、打包前的修改
在开发vue项目时,使用了反向代理的配置(在webpack中的配置),并使用了Axios的BaseUrl:
Axios.defaults.baseURL = ‘/api/’;
1)、如果上线时,项目还是前后端分离(如:在两个服务器上)
Axios.defaults.baseURL = ‘/api/’; //不用改。互联网服务器上的反向代理可以使用nginx。
2)、如果上线时,项目不是前后端分离。就不存在跨域问题。
Axios.defaults.baseURL = ‘’; //去掉baseURL的配置。
把前端和后端的打包结果放在一起,就ok了(把前端代码发给后端,后端一起打包)
2、打包命令
npm run build
命令行(项目目录下)运行此命令,就会在项目里产生dist目录。
dist目录就是打包后的结果,把该目录下的文件拷贝到服务器的根目录,记住一定是根目录(引入webpack打包时,会把很多文件路径设置为根路径)
补充:vue脚手架项目打包前后的对比
???????
3、远程服务器上线(nginx)
1)、如果上线时,项目还是前后端分离(如:在两个服务器上),那么webpack中的反向代理怎么办?没事,nginx服务器可以。nginx是一个服务器(跟apache)
nginx的反向代理配置:
location ~ ^/api/ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; rewrite ^/api/(.*)$ /$1 break; proxy_pass http://localhost:3000; }
2)、如果上线时,项目不是前后端分离。就不存在跨域问题。
vue脚手架项目从开发到打包完整的流程(从零开始做一个项目)
一、git版本管理
1、建立远程仓库,邀请成员,分配权限
2、每个程序员 克隆项目(就会有本地仓库)
二、脚手架搭建和开发
1、项目负责人搭建脚手架,并且把项目上传到远端
1)、vue create 项目名 | vue init webpack 项目名
2)、项目负责人把脚手架的代码拷贝到本地仓库所在目录下。
3)、项目负责人把脚手架的代码上传到远端版本库。
git add .
git commit -m “”
git push 远端地址 master
4、各程序员下载远端的脚手架空项目
5、各程序员进行开发
1)、建立分支(小仓库)
2)、写代码
3)、上传:git add git commit git push
4)、 循环以上三步,直到当前功能开发完毕
5)、合并分支的代码到主分支
6)、上传主分支的代码到远端。
7)、每天重复以上六步,这就是程序员的一生。
6、注意点:
每次push的时候,应该先pull。
三、打包上线
1、项目负责人(或指派人员)打包
npm run build
结果在dist目录下。
1)、第一种情况:前后端不分离(在一台服务器上)
不存在跨域问题,所以不需要反向代理。那么,打包前需要看看baseURL要不要改。 /api
2)、第二种情况:前端后端分离
有跨域问题,但是,打包时,不需要做修改。
2、上线:
1)、第一种情况:前后端不分离(在一台服务器上)
把dist的目录的代码发给后端,后端统一传到互联网(www服务器)服务器上
2)、第二种情况:前端后端分离
跨域问题(反向代理),使用nginx解决。
前端www服务上使用nginx。把曾经在webpack中的反向代理配置,写在nginx里。
后端会把自己的代码放在另外一台(www)服务器上
查漏补缺:
1、vue-cli3/4的配置
在项目根目录创建文件vue.config.js
1)、反向代理配置
module.exports = { devServer:{ //设置代理 proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换 '/api': { //axios访问 /api == target + /api target: 'http://localhost:3001', changeOrigin: true, //创建虚拟服务器 ws: true, //websocket代理 }, …………………… } } }
2)、别名配置:
npm install path --save-dev vue.config.js配置 const path = require("path"); function resolve(dir) { return path.join(__dirname, dir); } module.exports = { chainWebpack: config => { config.resolve.alias .set("@", resolve("src")) .set("assets", resolve("src/assets")) .set("components", resolve("src/components")) } }
2、axios的封装
//在公司里,都会对axios进行封装。如果说,我们不学这个,不执行,那么到公司中就看不懂别人的代码
1)、创建目录和文件:src/utils/service.js,封装axios的请求拦截器和全局的配置
import axios from "axios" // 创建axios 赋值给常量service const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 100000 headers: { "Content-Type": "application/json;charset=UTF-8", // "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", } }); // 添加请求拦截器(Interceptors) service.interceptors.request.use(function (config) { // 发送请求之前做写什么 let token = localStorage.getItem("token"); // 如果有 if(token){ // 放在请求头(token跟后端沟通,他需要什么该成什么就可以了) config.headers.authorization = token; } return config; }, function (error) { // 请求错误的时候做些什么 return Promise.reject(error); }); // 添加响应拦截器 service.interceptors.response.use(function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); export default service
2)、创建api文件夹,
2.1)、index.js封装get和post请求
import service from "../utils/service"; export function POST(url, params) { return new Promise((resolve, reject) => { service.post(url, params) .then(response => { if (!response.data) { resolve(response); } resolve(response.data); }, err => { reject(err); }) .catch((err) => { reject(err) }) }) } // get方法 export function GET(url, params) { return new Promise((resolve, reject) => { service.get(url, { params }) .then(response => { resolve(response.data); }, err => { reject(err); }) .catch((err) => { reject(err) }) }) }
2.2)、根据实际情况进行分模块:
如:home.js存放首页的所有请求
//home.js import * as axiosBase from '@/api/index.js'; export let getBooks = function (params) { params = params || {} return axiosBase.GET('/api/books', params) } export let getBannerImgs = function (params) { params = params || {} return axiosBase.GET('/api/imgs', params) }
如:users.js 存放登录注册相关的请求
//user.js import * as axiosBase from '@/api/index.js'; export let checkUser = function (params) { params = params || {} return axiosBase.GET('/api/checkUser', params) } export let reg = function (params) { params = params || {} return axiosBase.POST('/api/users', params) } export let loginCheck = function (params) { params = params || {} return axiosBase.POST('/api/loginCheck', params) }
还有这么干的:
import { get, post } from "./utils/index"; Vue.prototype.$http = { get, post }; 发送请求时,直接使用:this.$http.get(); this.$http.post();
3、脚手架里使用sass
1、安装
npm install node-sass --save-dev //安装node-sass
npm install sass-loader@6.0.6 --save-dev //安装sass-loader
npm install style-loader --save-dev //安装style-loader 或者 vue-style-loader !
2.在需要用到sass的地方添加lang=scss
3、可能出现的问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pqBMty4Z-1647875628997)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600185128727.png)]
说明版本太高,安装指定版本即可:
npm install sass-loader@6.0.6 --save-dev
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Zwp0qcZ-1647875628998)(C:\Users\31759\AppData\Roaming\Typora\typora-user-images\1600185132782.png)]
安装node-sass就行。
npm install node-sass --save-dev
- 参数:{Object} {x, y} 滚动的实时坐标
- 触发时机:滚动过程中,具体时机取决于选项中的 [probeType](
vue项目打包上线
1、打包前的修改
在开发vue项目时,使用了反向代理的配置(在webpack中的配置),并使用了Axios的BaseUrl:
Axios.defaults.baseURL = ‘/api/’;
1)、如果上线时,项目还是前后端分离(如:在两个服务器上)
Axios.defaults.baseURL = ‘/api/’; //不用改。互联网服务器上的反向代理可以使用nginx。
2)、如果上线时,项目不是前后端分离。就不存在跨域问题。
Axios.defaults.baseURL = ‘’; //去掉baseURL的配置。
把前端和后端的打包结果放在一起,就ok了(把前端代码发给后端,后端一起打包)
2、打包命令
npm run build
命令行(项目目录下)运行此命令,就会在项目里产生dist目录。
dist目录就是打包后的结果,把该目录下的文件拷贝到服务器的根目录,记住一定是根目录(引入webpack打包时,会把很多文件路径设置为根路径)
补充:vue脚手架项目打包前后的对比
???????
3、远程服务器上线(nginx)
1)、如果上线时,项目还是前后端分离(如:在两个服务器上),那么webpack中的反向代理怎么办?没事,nginx服务器可以。nginx是一个服务器(跟apache)
nginx的反向代理配置:
location ~ ^/api/ { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header REMOTE-HOST $remote_addr; rewrite ^/api/(.*)$ /$1 break; proxy_pass http://localhost:3000; }
2)、如果上线时,项目不是前后端分离。就不存在跨域问题。
vue脚手架项目从开发到打包完整的流程(从零开始做一个项目)
一、git版本管理
1、建立远程仓库,邀请成员,分配权限
2、每个程序员 克隆项目(就会有本地仓库)
二、脚手架搭建和开发
1、项目负责人搭建脚手架,并且把项目上传到远端
1)、vue create 项目名 | vue init webpack 项目名
2)、项目负责人把脚手架的代码拷贝到本地仓库所在目录下。
3)、项目负责人把脚手架的代码上传到远端版本库。
git add .
git commit -m “”
git push 远端地址 master
4、各程序员下载远端的脚手架空项目
5、各程序员进行开发
1)、建立分支(小仓库)
2)、写代码
3)、上传:git add git commit git push
4)、 循环以上三步,直到当前功能开发完毕
5)、合并分支的代码到主分支
6)、上传主分支的代码到远端。
7)、每天重复以上六步,这就是程序员的一生。
6、注意点:
每次push的时候,应该先pull。
三、打包上线
1、项目负责人(或指派人员)打包
npm run build
结果在dist目录下。
1)、第一种情况:前后端不分离(在一台服务器上)
不存在跨域问题,所以不需要反向代理。那么,打包前需要看看baseURL要不要改。 /api
2)、第二种情况:前端后端分离
有跨域问题,但是,打包时,不需要做修改。
2、上线:
1)、第一种情况:前后端不分离(在一台服务器上)
把dist的目录的代码发给后端,后端统一传到互联网(www服务器)服务器上
2)、第二种情况:前端后端分离
跨域问题(反向代理),使用nginx解决。
前端www服务上使用nginx。把曾经在webpack中的反向代理配置,写在nginx里。
后端会把自己的代码放在另外一台(www)服务器上
查漏补缺:
1、vue-cli3/4的配置
在项目根目录创建文件vue.config.js
1)、反向代理配置
module.exports = { devServer:{ //设置代理 proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换 '/api': { //axios访问 /api == target + /api target: 'http://localhost:3001', changeOrigin: true, //创建虚拟服务器 ws: true, //websocket代理 }, …………………… } } }
2)、别名配置:
npm install path --save-dev vue.config.js配置 const path = require("path"); function resolve(dir) { return path.join(__dirname, dir); } module.exports = { chainWebpack: config => { config.resolve.alias .set("@", resolve("src")) .set("assets", resolve("src/assets")) .set("components", resolve("src/components")) } }
2、axios的封装
//在公司里,都会对axios进行封装。如果说,我们不学这个,不执行,那么到公司中就看不懂别人的代码
1)、创建目录和文件:src/utils/service.js,封装axios的请求拦截器和全局的配置
import axios from "axios" // 创建axios 赋值给常量service const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 100000 headers: { "Content-Type": "application/json;charset=UTF-8", // "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", } }); // 添加请求拦截器(Interceptors) service.interceptors.request.use(function (config) { // 发送请求之前做写什么 let token = localStorage.getItem("token"); // 如果有 if(token){ // 放在请求头(token跟后端沟通,他需要什么该成什么就可以了) config.headers.authorization = token; } return config; }, function (error) { // 请求错误的时候做些什么 return Promise.reject(error); }); // 添加响应拦截器 service.interceptors.response.use(function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); }); export default service
2)、创建api文件夹,
2.1)、index.js封装get和post请求
import service from "../utils/service"; export function POST(url, params) { return new Promise((resolve, reject) => { service.post(url, params) .then(response => { if (!response.data) { resolve(response); } resolve(response.data); }, err => { reject(err); }) .catch((err) => { reject(err) }) }) } // get方法 export function GET(url, params) { return new Promise((resolve, reject) => { service.get(url, { params }) .then(response => { resolve(response.data); }, err => { reject(err); }) .catch((err) => { reject(err) }) }) }
2.2)、根据实际情况进行分模块:
如:home.js存放首页的所有请求
//home.js import * as axiosBase from '@/api/index.js'; export let getBooks = function (params) { params = params || {} return axiosBase.GET('/api/books', params) } export let getBannerImgs = function (params) { params = params || {} return axiosBase.GET('/api/imgs', params) }
如:users.js 存放登录注册相关的请求
//user.js import * as axiosBase from '@/api/index.js'; export let checkUser = function (params) { params = params || {} return axiosBase.GET('/api/checkUser', params) } export let reg = function (params) { params = params || {} return axiosBase.POST('/api/users', params) } export let loginCheck = function (params) { params = params || {} return axiosBase.POST('/api/loginCheck', params) }
还有这么干的:
import { get, post } from "./utils/index"; Vue.prototype.$http = { get, post }; 发送请求时,直接使用:this.$http.get(); this.$http.post();
3、脚手架里使用sass
1、安装
npm install node-sass --save-dev //安装node-sass
npm install sass-loader@6.0.6 --save-dev //安装sass-loader
npm install style-loader --save-dev //安装style-loader 或者 vue-style-loader !
2.在需要用到sass的地方添加lang=scss
3、可能出现的问题
[外链图片转存中…(img-pqBMty4Z-1647875628997)]
说明版本太高,安装指定版本即可:
npm install sass-loader@6.0.6 --save-dev
[外链图片转存中…(img-8Zwp0qcZ-1647875628998)]
安装node-sass就行。
npm install node-sass --save-dev
这篇关于vue_t_v14的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-16Vue3资料:新手入门必读教程
- 2024-11-16Vue3资料:新手入门全面指南
- 2024-11-16Vue资料:新手入门完全指南
- 2024-11-16Vue项目实战:新手入门指南
- 2024-11-16React Hooks之useEffect案例详解
- 2024-11-16useRef案例详解:React中的useRef使用教程
- 2024-11-16React Hooks之useState案例详解
- 2024-11-16Vue入门指南:从零开始搭建第一个Vue项目
- 2024-11-16Vue3学习:新手入门教程与实践指南
- 2024-11-16Vue3学习:从入门到初级实战教程