VUE-let
let声明,只是在代码块中有效。如果上个循环中的var变成let声明,结果就大不一样了。
VUE-const
const声明一个只读的常量。所谓常量,即一旦声明,值就不能改变。
const声明的变量不能改变值,这说明,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const的作用域与let相同:只在声明所在的块级作用域内有效。
const app = new Vue({
el: '#app',
data: {
movies: ['星际穿越', '大话西游', '盗梦空间']
}
})
el:
类型:string | HTMLElement
作用:决定之后Vue实例会管理哪一个DOM。
data:
类型:Object | Function (组件当中data必须是一个函数)
作用:Vue实例对应的数据对象。
methods:
类型:{ [key: string]: Function }
作用:定义属于Vue的一些方法,可以在其他地方调用,也可以在指令中使用。
MVVM有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或GUI代码实现的。MVVM的视图模型是一个值转换器, 这意味着视图模型负责从模型中暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型比视图做得更多,并且处理大部分视图的显示逻辑。 视图模型可以实现中介者模式,组织对视图所支持的用例集的后端逻辑的访问。
MVVM模式的组成部分
模型
模型是指代表真实状态内容的领域模型(面向对象),或指代表内容的数据访问层(以数据为中心)。
视图
就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。
视图模型
视图模型是暴露公共属性和命令的视图的抽象。MVVM没有MVC模式的控制器,也没有MVP模式的presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通信。
绑定器
声明性数据和命令绑定隐含在MVVM模式中。在Microsoft解决方案堆中,绑定器是一种名为XAML的标记语言。 绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。
vue 中的MVVM
View层:
视图层
在我们前端开发中,通常就是DOM层。
主要的作用是给用户展示各种信息。
Model层:
数据层
数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据。
在我们计数器的案例中,就是后面抽取出来的obj,当然,里面的数据可能没有这么简单。
VueModel层:
视图模型层
视图模型层是View和Model沟通的桥梁。
一方面它实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中
另一方面它实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情况下改变对应的Data。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
</head>
<body>
<div id="app">
</div>
<script>
// 生命周期函数就是 Vue实例在某一个时间点自动执行的函数
var vm = new Vue({
el : "#app",
template : "{{message}}",
data : {
message : "hello world"
},
// vue实例基础初始化之后,就会触发
beforeCreate : function(){
console.log("beforeCreate");
},
created : function(){
console.log("created");
},
// 如果没有template模板,就会将el内的当成模板
// 如果有template,就会用template里的东西
// beforeMount:当和模板结合,在渲染页面之前的一瞬间,执行beforeMount函数(还没有渲染到页面上————用模板的情况下)
beforeMount : function(){
console.log(this.$el);
console.log("beforeMount");
},
// 这时候hello World就会渲染在页面上了
// mounted就会自动执行(已经渲染了)
mounted : function(){
console.log(this.$el);
console.log("mounted");
},
// 实例还没有销毁,在销毁的前一刻
// 销毁实例:vm.$destroy()
beforeDestroy : function(){
console.log("beforeDestory");
},
// 实例被销毁了
destroyed : function(){
console.log("destoryed");
},
// 数据发生改变,还没渲染之前
beforeUpdate : function(){
console.log("beforeUpdate");
},
// 数据改变,渲染之后
updated : function(){
console.log("updated");
}
})
</script>
</body>
</html>
1.Mustache语法
我们已经学习过了,可以通过Mustache语法(也就是双大括号)。
Mustache: 胡子/胡须.
数据是响应式的
<!--mustache语法中,不仅仅可以直接写变量,也可以写简单的表达式-->
<h2>{{firstName + lastName}}</h2>
<h2>{{firstName + ' ' + lastName}}</h2>
<h2>{{firstName}} {{lastName}}</h2>
<h2>{{counter * 2}}</h2>
2.v-once:
该指令后面不需要跟任何表达式(比如之前的v-for后面是由跟表达式的)
该指令表示元素和组件(组件后面才会学习)只渲染一次,不会随着数据的改变而改变。
<div id="app">
<h2>{{message}}</h2>
<h2 v-once>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
3.v-html
某些情况下,我们从服务器请求到的数据本身就是一个HTML代码
如果我们直接通过{{}}来输出,会将HTML代码也一起输出。
但是我们可能希望的是按照HTML格式进行解析,并且显示对应的内容。
4.v-text
v-text作用和Mustache比较相似:都是用于将数据显示在界面中
但是它不够灵活,运用得少
<h2 v-text="message">, 李银河!</h2>
//message的内容会被李银河覆盖
5.v-pre
v-pre用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。
6.v-cloak
在某些情况下,浏览器卡住时可能会直接显示出未编译的Mustache标签。
<div id="app" v-cloak> //v-cloak作为一个属性添加在div中
<h2>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
// 在vue解析之前, div中有一个属性v-cloak
// 在vue解析之后, div中没有一个属性v-cloak(删掉了)
setTimeout(function () {
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
}, 1000)
原理:
<style>
[v-cloak] {
display: none;
}
</style>
//让它不可见
1.基本使用
作用:动态绑定属性
缩写::
预期:any (with argument) | Object (without argument)
参数:attrOrProp (optional)
v-bind用于绑定一个或多个属性值,或者向另一个组件传递props值(这个学到组件时再介绍)
在开发中,有哪些属性需要动态进行绑定呢?
还是有很多的,比如图片的链接src、网站的链接href、动态绑定一些类、样式等等
<div id="app">
<!-- 错误的做法: 这里不可以使用mustache语法-->
<!--<img src="{{imgURL}}" alt="">-->
<!-- 正确的做法: 使用v-bind指令 -->
<img v-bind:src="imgURL" alt="">//把imgURL当做变量来解析
<a v-bind:href="aHref">百度一下</a>//冒号后面是要绑定的属性
<!--<h2>{{}}</h2>-->
<!--语法糖的写法-->
<img :src="imgURL" alt="">
<a :href="aHref">百度一下</a>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
imgURL: 'https://img11.360buyimg.com/mobilecms/s350x250_jfs/t1/20559/1/1424/73138/5c125595E3cbaa3c8/74fc2f84e53a9c23.jpg!q90!cc_350x250.webp',
aHref: 'http://www.baidu.com'
}
})
</script>
2.v-bind动态绑定class
①对象语法:
<!--<h2 v-bind:class="{key1: value1, key2: value2}">{{message}}</h2>-->
<!--<h2 v-bind:class="{类名1: true, 类名2: boolean}">{{message}}</h2>-->
//一个{}是一个对象,key和value分别对应类名和布尔值,为true时将类添加到元素中,为false是将类从元素中抹去
<h2 class="title" v-bind:class="{active: isActive, line: isLine}">{{message}}</h2>
<h2 class="title" v-bind:class="getClasses()">{{message}}</h2>
//用v-bind绑定类的同时也还可以用老方法添加类,会整合两种方式
<button v-on:click="btnClick">按钮</button>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isActive: true,
isLine: true
},
methods: {
btnClick: function () {
this.isActive = !this.isActive
},
getClasses: function () {
return {active: this.isActive, line: this.isLine}
}
}
})
</script>
②数组语法(很少用):
<div id="app">
<h2 class="title" :class="[active, line]">{{message}}</h2>
//用数组来装类名,实际上也是写死的,不灵活
<h2 class="title" :class="getClasses()">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
active: 'aaaaaa',
line: 'bbbbbbb'
},
methods: {
getClasses: function () {
return [this.active, this.line]
}
}
})
</script>
3.v-bind动态绑定style
①对象语法
:style = “{key(属性名): value(属性值)”
属性名不加引号 ,属性值一定要加引号否则会当做变量解析(及时value是一个变量,在date中赋值变量时也需要加引号)
<div id="app">
<!--<h2 :style="{key(属性名): value(属性值)}">{{message}}</h2>-->
<!--'50px'必须加上单引号, 否则是当做一个变量去解析-->
<!--<h2 :style="{fontSize: '50px'}">{{message}}</h2>-->
<!--finalSize当成一个变量使用-->
<!--<h2 :style="{fontSize: finalSize}">{{message}}</h2>-->
<h2 :style="{fontSize: finalSize + 'px', backgroundColor: finalColor}">{{message}}</h2>
<h2 :style="getStyles()">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
finalSize: 100,
finalColor: 'red',
},
methods: {
getStyles: function () {
return {fontSize: this.finalSize + 'px', backgroundColor: this.finalColor}
}
}
})
</script>
②数组语法(用的很少)
<div id="app">
<h2 :style="[baseStyle, baseStyle1]">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
baseStyle: {backgroundColor: 'red'},
baseStyle1: {fontSize: '100px'},
}
})
</script>
1、v-for循环普通数组
①创建vue对象
② 循环数据
item 是数组中的每一项,i 是每次遍历的索引值
结果:
2、v-for循环对象数组
① 创建vue实例对象
② 循环对象数组
user 是每次遍历就从list中取出的对象 i 是当前对象在list中的索引
结果:
3、v-for循环对象
①创建vue对象实例
②循环对象
val 是当前遍历对象中的value
key 是是当前遍历对象中的key
i 是当前遍历对象中的属性的index 如果是第一个属性索引就是0
结果:
4、v-for循环数字
①创建vue对象实例
②循环数字
结果:
也是定义函数,但是起名字尽量取得像属性,不要加动词
计算属性使用时不需要加括号
<div id="app">
<h2>{{firstName + ' ' + lastName}}</h2>
<h2>{{firstName}} {{lastName}}</h2>
//上面的方法复杂不易阅读
//方法
<h2>{{getFullName()}}</h2>
//计算属性
<h2>{{fullName}}</h2>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
firstName: 'Lebron',
lastName: 'James'
},
// computed: 计算属性()
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
},
methods: {
getFullName() {
return this.firstName + ' ' + this.lastName
}
}
})
</script>
<div id="app">
//计算属性
<h2>总价格: {{totalPrice}}</h2>
//方法
<h2>总价格: {{getTotalPrice()}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
books: [
{id: 110, name: 'Unix编程艺术', price: 119},
{id: 111, name: '代码大全', price: 105},
{id: 112, name: '深入理解计算机原理', price: 98},
{id: 113, name: '现代操作系统', price: 87},
]
},
methods: {
getTotalPrice: function () {
let result = 0
for (let i=0; i < this.books.length; i++) {
result += this.books[i].price
}
return result
}
},
computed: {
totalPrice: function () {
let result = 0
for (let i=0; i < this.books.length; i++) {
result += this.books[i].price
}
return result
}
}
})
</script>
方法每次调用都会重新计算一次,没有缓存
而计算属性只执行一次,保持缓存结果,效率更高
//计算属性完整写法
computed: {
// 计算属性一般是没有set方法, 只读属性.
fullName: {
set: function(newValue) {
},
get: function () {
}
},
}
由于计算属性一般没有set方法,因为我们不希望数据被改变;
简写属性如下
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
<div id="app">
<h2>{{fullName}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
firstName: 'Kobe',
lastName: 'Bryant'
},
computed: {
// fullName: function () {
// return this.firstName + ' ' + this.lastName
// }
// name: 'coderwhy'
// 计算属性一般是没有set方法, 只读属性.
fullName: {
set: function(newValue) {
// 由于要改变数据,所以需要传值
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[1];
},
get: function () {
return this.firstName + ' ' + this.lastName
}
},
// fullName: function () {
// return this.firstName + ' ' + this.lastName
// }
}
})
</script>
主要是计算属性如果数据没有变化,只会执行一次,将结果缓存。之后每次执行只需要直接使用结果缓存而不会多次执行计算。
而methods调用一次就要执行一次,耗费内存和资源
事实上var的设计可以看成JavaScript语言设计上的错误. 但是这种错误多半不能修复和移除, 以为需要向后兼容.
大概十年前, Brendan Eich就决定修复这个问题, 于是他添加了一个新的关键字: let.
我们可以将let看成更完美的var
块级作用域
在ES5中使用var来声明变量
但是var声明的变量只有在函数中有作用域,在其他块中没有作用域比如if/for。但是我们希望变量有块级作用域,不希望它在一些情况下被改变,从而引起问题。
在ES5中我们解决这个问题的办法是使用闭包,因为闭包是函数,函数有块级作用域,但是明显将问题复杂化了。
if没有块级作用域
//设置5个按钮
<body>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>
</body>
// 2.没有块级作用域引起的问题: if的块级
var func;
if (true) {
var name = 'why';
func = function () {
console.log(name);
}
// func()
}
name = 'kobe' //作用域内的变量被意外改变了
func() //输出kobe 这不是我们想要的
函数是有块级作用域的,作用域外的变量改变不会影响函数内部
function abc() {
var name = 'why'
console.log(name);
}
name = 'kobe'
abc(name) //输出why
使用闭包解决块级作用域的问题
//3.没有块级作用域引起的问题: for的块级
//为什么闭包可以解决问题: 函数是一个作用域.
var btns = document.getElementsByTagName('button');
for (var i=0; i<btns.length; i++) {
(function (num) { // 0
btns[i].addEventListener('click', function () {
console.log('第' + num + '个按钮被点击');
})
})(i) //每次自调用函数得到的i传给内部函数num,
}
使用var绑定响应函数时,不管点哪一个按钮都是显示第五个被点击,就是因为for没有块级作用域
var btns = document.getElementsByTagName('button')
var i = 5
for (var i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
使用let,变量有块级作用域,就不会存在不管点哪一个都是显示第5个被点击的情况了。
const btns = document.getElementsByTagName('button')
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + i + '个按钮被点击');
}
}
情况一: ES5中没有使用闭包(错误的方式)
因为没有var没有块级作用域,每次循环,每对花括号内部都是使用的同一个i ,i 写在花括号里面或是外面没有区别。然后点击事件函数并不是立即执行函数,当他执行时,共同的 i 已经循环到最后一个,所以无论点哪一个都会显示最后一个 按钮被点击。
var btns = document.getElementsByTagName('button');
for (var i=0; i<btns.length; i++) {
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
// 一个花括号代表一次循环
i = 2
{
// i = 0
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
{
i = 1
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
{
// i = 2
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
情况二: ES5中使用闭包
函数有作用域,所以每次执行函数,本次执行的函数内部都有一个属于自己的 i ,外部for循环的 i 变化不会对每次执行的函数内部的 i 造成影响,它仅仅是把 i 传到函数中。
// 2.
var btns = document.getElementsByTagName('button');
for (var i=0; i<btns.length; i++) {
(function (i) { // 单次执行,这里面的 i 与外面的 i 隔绝
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}) (i)
}
//执行第一次
function (i) { // i = 0
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}(0)
//执行第二次
function (i) { // i = 1
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}(1)
//执行第三次
function (i) { // i = 2
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}(2)
情况三:ES6中的let
let有块级作用域,效果就和函数一样,括号内的 i 和外面的 i 相互隔绝,外面 i 的变化不会影响括号内的代码。
//
const btns = document.getElementsByTagName('button')
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
i = 10000000
{ i = 0
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
{ i = 1
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
{ i = 2
btns[i].addEventListener('click', function () {
console.log('第' + i + '个按钮被点击');
})
}
const关键字
在很多语言中已经存在, 比如C/C++中, 主要的作用是将某个变量修饰为常量.
在JavaScript中也是如此, 使用const修饰的标识符为常量, 不可以再次赋值.
什么时候使用const呢?
当我们修饰的标识符不会被再次赋值时, 就可以使用const来保证数据的安全性.
建议: 在ES6开发中,优先使用const, 只有需要改变某一个标识符的时候才使用let.
1.注意一: 一旦给const修饰的标识符被赋值之后, 不能修改
const name = ‘why’;
name = ‘abc’; 报错!!!
2.注意二: 在使用const定义标识符,必须进行赋值
const name; 报错!!!
3.注意三: 常量的含义是指向的对象不能修改, 但是可以改变对象内部的属性.
1.属性的增强写法
//一些全局变量
const name = 'why';
const age = 18;
const height = 1.88
//将这些变量在对象中使用
ES5的写法
const obj = {
name: name,
age: age,
height: height
}
//ES6的写法,
//它将变量名作为key 值作为value,内部自动完成,和ES5在浏览器中是完全一样的
const obj = {
name,
age,
height,
}
console.log(obj);
2.函数的增强写法
//ES5的写法
const obj = {
run: function () {},
eat: function () {}
}
//ES6的写法
//更简单更方便
const obj = {
run() {},
eat() {}
}
<div id="app">
<h2>{{counter}}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>//decrement省略了括号
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
counter: 0
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
})
语法糖:
当通过methods中定义方法,以供@click调用时,需要注意参数问题:
情况一:如果该方法不需要额外参数,那么方法后的()可以省略。
但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。
<body>
<div id="app">
<!--1.事件调用的方法没有参数-->
// 如果函数需要参数,但是没有传入, 那么函数的形参为undefined
<button @click="btn1Click()">按钮1</button> //打印undefinde
<button @click="btn1Click">按钮2</button> //event事件赋值给形参
<button @click="btn2Click">按钮3</button> //会把event事件赋值给第一个参数
<button @click="btn2Click(3,$event)">按钮3</button>
//如果参数传的一个没有引号的字符串(符合变量命名规则),会当做变量去data中找,找不到就是undefined。如果传的一个数字就会当做一个参数。
</div>
//手动获取event事件通过$event,必须一模一样,$e这种自定义的参数不行,必须是 $event(大小写也要一致)
</body>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
abc: 123
},
methods: {
btn1Click(nf) {
console.log(nf); //如果调用btn1Click不加括号也不传参数,会将event事件传递给 nf 形参。
},
btn2Click(abc,event) {
console.log('--------',abc, event);
},
}
})
</script>
①.stop - 调用 event.stopPropagation()。
阻止事件冒泡
②.prevent - 调用 event.preventDefault()。
阻止默认事件
③.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
④.native - 监听组件根元素的原生事件。
<div id="app">
<!--1. .stop修饰符的使用-->
<div @click="divClick">
aaaaaaa
<button @click.stop="btnClick">按钮</button>
</div>
<!--2. .prevent修饰符的使用-->
<br>
<form action="baidu">
<input type="submit" value="提交" @click.prevent="submitClick">
</form>
<!--3. .监听某个键盘的键帽-->
<input type="text" @keyup.enter="keyUp">
<!--4. .once修饰符的使用-->
<button @click.once="btn2Click">按钮2</button>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
methods: {
btnClick() {
console.log("btnClick");
},
divClick() {
console.log("divClick");
},
submitClick() {
console.log('submitClick');
},
keyUp() {
console.log('keyUp');
},
btn2Click() {
console.log('btn2Click');
}
}
})
v-if 和 v-else
<div id="app">
<h2 v-if="isShow">
<div>abc</div>
{{message}} <!-- isShow为true时,渲染这两个元素。isShow为false时,这两个元素不会被渲染(显示) -->
</h2>
<h1 v-else>isShow为false时, 显示我</h1><!--isShow为false时,渲染这个元素 -->
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isShow: true
}
})
</script>
<div id="app">
<h2 v-if="score>=90">优秀</h2>
<h2 v-else-if="score>=80">良好</h2>
<h2 v-else-if="score>=60">及格</h2>
<h2 v-else>不及格</h2>
<h1>{{result}}</h1>
</div>
v-show的用法和v-if非常相似,也用于决定一个元素是否渲染:
v-if和v-show对比
v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?
v-if当条件为false时,压根不会有对应的元素在DOM中。
v-show当条件为false时,是将元素的display属性设置为none。
开发中如何选择呢?
当需要在显示与隐藏之间切片很频繁时,使用v-show
当只有一次切换时,通过使用v-if
<div id="app">
<ul>
<li v-for="item in letters" :key="item">{{item}}</li>
<!-- 给每个item绑定一个key属性,为了使用虚拟dom时提升性能 -->
</ul>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
letters: ['A', 'B', 'C', 'D', 'E']
}
})
</script>
1.push方法
this.letters.push('aaa')
this.letters.push('aaaa', 'bbbb', 'cccc')
2.pop(): 删除数组中的最后一个元素
this.letters.pop();
3.shift(): 删除数组中的第一个元素
this.letters.shift();
4.unshift(): 在数组最前面添加元素
this.letters.unshift()
this.letters.unshift('aaa', 'bbb', 'ccc')
5.splice作用: 删除元素/插入元素/替换元素
删除元素: 第二个参数传入你要删除几个元素(如果没有传,就删除后面所有的元素)
替换元素: 第二个参数, 表示我们要替换几个元素, 后面是用于替换前面的元素
插入元素: 第二个参数, 传入0, 并且后面跟上要插入的元素
splice(start)
splice(start):
this.letters.splice(1, 3, 'm', 'n', 'l', 'x')
this.letters.splice(1, 0, 'x', 'y', 'z')
6.sort()
this.letters.sort()
7.reverse()
this.letters.reverse()
注意: 通过索引值修改数组中的元素,页面不会更新数据,不是响应式的
this.letters[0] = 'bbbbbb';
但是可以通过下面的方式替换数据
this.letters.splice(0, 1, 'bbbbbb')
set(要修改的对象, 索引值, 修改后的值) 是Vue的方法
Vue.set(this.letters, 0, 'bbbbbb')
在购物车案例中
1.普通的for循环
let totalPrice = 0
for (let i = 0; i < this.books.length; i++) {
totalPrice += this.books[i].price * this.books[i].count
}
return totalPrice
//这种for循环 i 是books数组的索引
2.for (let i in this.books)
let totalPrice = 0
for (let i in this.books) {
const book = this.books[i]
totalPrice += book.price * book.count
}
return totalPrice
//二这种for循环 i 拿到的是books对象数组中的每个对象
3.for (let i of this.books)
let totalPrice = 0
for (let item of this.books) {
totalPrice += item.price * item.count
}
return totalPrice
编程范式: 命令式编程/声明式编程
编程范式: 面向对象编程(第一公民:对象)/函数式编程(第一公民:函数)
/map/reduce
1.filter函数
filter中的回调函数有一个要求: 必须返回一个boolean值
true: 当返回true时, 函数内部会自动将这次回调的n加入到新的数组中
false: 当返回false时, 函数内部会过滤掉这次的n
将数组每个元素作为回调函数的参数执行一遍回调函数,将返回结果为true的元素加入新的数组,为false的过滤掉(元素的值不会改变但个数会改变)
//filter遍历nums数组,会把数组每个元素传入回调函数的形参 n
//filter会将新的数组作为返回值返回,用total接收就行了
let total = nums.filter(function (n) {
return n < 100
})
2.map函数
map()方法定义在JavaScript的Array中,它返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
数组中每个元素都作为回调函数的参数,执行一次回调函数,并返回一个新的数组(数组的个数不会改变)
// 2.map函数的使用
// 20, 40, 80, 100
const nums = [10, 20, 111, 222, 444, 40, 50]
let new2Nums = nums.map(function (n) { // 20
return n * 2
})
console.log(new2Nums);
3.map 和filter的区别
原生js中数组可以直接通过map(),filter()函数来进行一次操作,他们分别是做一次统一映射,和一次过滤。说的更通俗一点,就是map函数之后,数组元素个数不变,但是按照一定的条件转换,数组元素发生了变化。filter函数之后,数组元素个数可能发生了改变,但是数组元素不会发生改变。
4.reduce函数
reduce作用:对数组中所有的内容进行汇总
遍历数组num(有多少个元素就遍历多少次),第一个参数是一个函数,函数有两个参数。第一个参数第一次循环的值就是初始化参数,第二个参数 n 是每次从数组中取出的值。然后每次return的值又会赋给preValue。
第二个参数是初始化参数,一般设为 0.
// 3.reduce函数的使用
num = [20, 40, 80, 100]
let total = num.reduce(function (preValue, n) {
return preValue + n
}, 0)
console.log(total);
第一次: preValue:0 n:20
第二次: preValue:20 n:40
第二次: preValue:60 n:80
第二次: preValue:140 n:100
返回值 :240
函数式编程,链式编程
const nums = [10, 20, 111, 222, 444, 40, 50]
let total = nums.filter(function (n) {
return n < 100
}).map(function (n) {
return n * 2
}).reduce(function (prevValue, n) {
return prevValue + n
}, 0)
console.log(total);
v-model 主要是用于表单上数据的双向绑定
一:基本
1:主要用于 input,select,textarea,component
2:修饰符:
.lazy- 取代input监听change事件
.number- 输入字符串转为数字
.trim- 输入首尾空格过滤
二:语法糖
此时mes值就与input的值进行双向绑定
实际上上面的代码是下面代码的语法糖。
要理解这行代码,首先你要知道 input 元素本身有个 oninput 事件,这是 HTML5 新增加的,类似 onchange ,每当输入框内容发生变化,就会触发 oninput ,把最新的value传递给 mes。从而实现了v-model。
表单控件在实际开发中是非常常见的。特别是对于用户信息的提交,需要大量的表单。
Vue中使用v-model指令来实现表单元素和数据的双向绑定。
案例的解析:
当我们在输入框输入内容时
因为input中的v-model绑定了message,所以会实时将输入的内容传递给message,message发生改变。
当message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。
所以,通过v-model实现了双向的绑定。
当然,我们也可以将v-model用于textarea元素
<div id="app">
<input type="text" v-model="message">
{{message}}
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
双向绑定就是,改变message的值后,input框内的内容会随着一起改变。而当改变input框里的内容时,message的值也会改变。
v-model的原理
其实就是value绑定message,然后监听input,一旦有输入行为,就通过event事件的target.value获取刚刚输入的数据并赋值给message。因为value绑定了messgae,所以message改变,input的value也会改变。
<div id="app">
<!--<input type="text" v-model="message">-->
<!--<input type="text" :value="message" @input="valueChange">-->
<input type="text" :value="message" @input="message = $event.target.value">
<h2>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
methods: {
valueChange(event) {
this.message = event.target.value;
}
}
})
</script>
v-model其实是一个语法糖,它的背后本质上是包含两个操作:
1.v-bind绑定一个value属性
2.v-on指令给当前元素绑定input事件
向服务器提交请求时是通过name属性作为key,所以如果两个input需要互斥,就添加值相同的name属性。但是如果绑定了相同的v-model就不需要name了
<div id="app">
<label for="male">
<input type="radio" id="male" value="男" v-model="sex">男
</label>
<label for="female">
<input type="radio" id="female" value="女" v-model="sex">女
</label>
<h2>您选择的性别是: {{sex}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
sex: '女'
}
})
</script>
input外面包裹一个label的好处。当label和input绑定之后。点击到 label的区域也会触发input(点在label上和点在input上一样)。改善了用户体验
<div id="app">
<!--1.checkbox单选框-->
<!-- <label for="agree">-->
<!--<input type="checkbox" id="agree" v-model="isAgree">同意协议-->
<!--</label> -->
<!--<h2>您选择的是: {{isAgree}}</h2>-->
<!--<button :disabled="!isAgree">下一步</button>-->
<!--2.checkbox多选框-->
<input type="checkbox" value="篮球" v-model="hobbies">篮球
<input type="checkbox" value="足球" v-model="hobbies">足球
<input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
<input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
<h2>您的爱好是: {{hobbies}}</h2>
<label v-for="item in originHobbies" :for="item">
<input type="checkbox" :value="item" :id="item" v-model="hobbies">{{item}}
</label>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isAgree: false, // 单选框
hobbies: [], // 多选框,
//['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
}
})
</script>
<div id="app">
<!--1.选择一个-->
<select name="abc" v-model="fruit">
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="葡萄">葡萄</option>
</select>
<h2>您选择的水果是: {{fruit}}</h2>
<!--2.选择多个-->
<select name="abc" v-model="fruits" multiple>
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="葡萄">葡萄</option>
</select>
<h2>您选择的水果是: {{fruits}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
fruit: '香蕉', //单选是字符串
fruits: [] //多选是数组
}
})
</script>
就是动态的给value赋值而已
所以我们可以通过v-bind:value动态的给value绑定值。
<div>
<label v-for="item in originHobbies" :for="item">
<input type="checkbox" :value="item" :id="item" v-model="hobbies">{{item}}
</label>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isAgree: false, // 单选框
hobbies: [], // 多选框,
originHobbies: ['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
}
})
</script>
①lazy修饰符:
默认情况下,v-model默认是在input事件中实时同步输入框的数据的。
也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。
lazy修饰符可以让数据在失去焦点或者回车时才会更新。
②number修饰符:
默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。
但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。
number修饰符可以让在输入框中输入的内容自动转成数字类型:
③trim修饰符:
如果输入的内容首尾有很多空格,通常我们希望将其去除
trim修饰符可以过滤内容左右两边的空格
ES6补充语法
//以前拼接字符串
var str = 'wyp' +'ssss';
//ES6语法
let str1 = 'dsfagfd
asdfasdg
dfgafd'
// ‘’单引号内的字符都会自动拼接,被认为是一个字符串
<div id="app">
<!--3.使用组件-->
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<div>
<div>
<my-cpn></my-cpn>
</div>
</div>
</div>
<my-cpn></my-cpn>
//无效的,因为没有在vue实例里面,在vue管理的div之外了
<script src="../js/vue.js"></script>
<script>
// 1.创建组件构造器对象
const cpnC = Vue.extend({
template: `
我是标题
我是内容, 哈哈哈哈
我是内容, 呵呵呵呵
`
})
// 2.注册组件
Vue.component('my-cpn', cpnC)
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
1.Vue.extend():
调用Vue.extend()创建的是一个组件构造器。
通常在创建组件构造器时,传入template代表我们自定义组件的模板。
该模板就是在使用到组件的地方,要显示的HTML代码。
事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。
2.Vue.component():
调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
所以需要传递两个参数:1、注册组件的标签名 2、组件构造器
3.组件必须挂载在某个Vue实例下,否则它不会生效。
我们来看下面我使用了三次
而第三次其实并没有生效:
全局组件可以在不同vue实例中使用
局部组件只能在当前注册的实例中使用(开发中一般只有一个vue实例)
<script>
// 1.创建组件构造器
const cpnC = Vue.extend({
template: `
我是标题
我是内容,哈哈哈哈啊
`
})
// 2.注册组件(全局组件, 意味着可以在多个Vue的实例下面使用)
//(使用的标签名,创建的组件名)
Vue.component('cpn', cpnC)
// 局部组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
// cpn使用组件时的标签名,cpnC创建的组件名
cpn: cpnC
}
})
const app2 = new Vue({
el: '#app2'
})
</script>
在父组件的 components属性中注册子组件,在vue实例饿 components属性中注册父组件。然后子组件就可以在父组件中使用,但是不能直接在父组件外面使用。
<div id="app">
<cpn2></cpn2>
<!-- <cpn1></cpn1> -->
//直接使用cpn1会报错
</div>
<script src="./VUE/vue.js"></script>
<script>
// 1.创建第一个组件构造器(子组件)
const cpnC1 = Vue.extend({
template: `
我是标题1
我是内容, 哈哈哈哈
`
})
// 2.创建第二个组件构造器(父组件)
const cpnC2 = Vue.extend({
template: `
我是标题2
我是内容, 呵呵呵呵
`,
components: {
cpn1: cpnC1
}
})
// root组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn2: cpnC2
}
})
</script>
以前注册组件的写法
//1.构建组件
const cpnC2 = Vue.extend({
template: `
我是标题2
我是内容, 呵呵呵呵
`
//2.注册组件
//注册全局组件
Vue.component('cpn2',cpnC2 )
//注册局部组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn2: cpnC2
}
})
语法糖写法
用模板内容将变量替换掉
主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
// 2.注册全局组件
Vue.component('cpn1', {
template: `
我是标题1
我是内容, 哈哈哈哈
`
})
// 2.注册局部组件的语法糖
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
'cpn2': {
template: `
我是标题2
我是内容, 呵呵呵
`
}
}
})
①用script标签设置类型为‘text/x-template’ 设置id属性
1.script标签, 注意:类型必须是text/x-template-->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈</p>
</div>
</script>
②直接使用template标签 设置id属性
<!--2.template标签-->
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容,呵呵呵</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
将模板分离之后,注册时就只需要将id属性写在template属性里。
分离模板既方便写html代码也可以是代码更加整洁。
// 1.注册一个全局组件
Vue.component('cpn', {
template: '#cpn'
})
组件中不能直接访问Vue实例中的data
组件自己的数据存放在哪里呢?
组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
只是这个data属性必须是一个函数
而且这个函数返回一个对象,对象内部保存着数据
组件中也是无法访问vue实例的方法的,所以它有自己的data,自己的method。
为什么data在组件中必须是一个函数呢?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
function abc(){
return {
name:'wyp'
}
}
var a = abc() //每次调用函数都返回一个对象
var b = abc() //每次调用函数都返回一个对象
var c = abc() //每次调用函数都返回一个对象
console.log(a)
console.log(b)
console.log(c)
将对象放在函数中,每次执行函数都会产生一个新的对象,与其他对象数据相互独立。改变a.name的值不会影响b和c中的内容。
而作为组件时,因为组件是需要复用的。每次复用就会执行一次data函数,为当前组件储存数据。我们不希望一个组件中数据更改影响所有该类型组件。所以data需要是一个函数。
如果data是一个对象,她保存的是一个地址值,每次复用组件的data都指向同一个对象,也就是说,他们的数据是共享的,我们并不希望这样。
var abc ={
name:www
}
var a = abc //这里仅仅是做了地址值的赋值,对象内的数据是共享的
var b = abc //这里仅仅是做了地址值的赋值,对象内的数据是共享的
var c = abc //这里仅仅是做了地址值的赋值,对象内的数据是共享的
console.log(a)
console.log(b)
console.log(c)
在开发中,往往一些数据确实需要从上层传递到下层:
比如在一个页面中,我们从服务器请求到了很多的数据。
其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
如何进行父子组件间的相互通信呢?Vue官方提到
通过props向子组件传递数据
通过事件向父组件发送消息
本案例 vue实例是父组件,cpn是子组件。
①在子组件的props属性中创建数据变量[‘cmovies’, ‘cmessage’](需要装在一个数组里)。
②在页面结构中使用cpn组件时,使用v-bind将:cmessage=“message” 父属性的数据赋值给子属性。
(props的数组写法)
<div id="app">
<!-- cpn组件的props属性 props: ['cmovies', 'cmessage'],绑定在cpn标签上 -->
<!-- 这一步相当于是父组件数据赋值给子组件数据 -->
<cpn :cmessage="message" :cmovies="movies"></cpn>
//因为cpn在父组件内部,所以可以访问父组件的数据,然后通过cmessage来传递数据给子组件
</div>
<!-- 模板 -->
<template id="cpn">
<div>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 父传子: props
// 创建子组件
const cpn = {
template: '#cpn',
//props属性专门用来传递数据
props: ['cmovies', 'cmessage'],
},
data() {
return {}
},
methods: {
}
}
//vue实例也是cpn组件的父组件
// 要将父组件里的数据message 和 movies传给cpn组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
movies: ['海王', '海贼王', '海尔兄弟']
},
//注册子组件
components: {
//简写属性cpn:'cpn'
cpn
}
})
</script>
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称。(上面那种)
方式二:对象,对象可以设置传递时的类型,也可以设置默认值等
<div id="app">
<!-- cpn组件的props属性 props: ['cmovies', 'cmessage'],绑定在cpn标签上 -->
<!-- 这一步相当于是父组件数据赋值给子组件数据 -->
<!--<cpn v-bind:cmovies="movies"></cpn>-->
<!--<cpn cmovies="movies" cmessage="message"></cpn>-->
<cpn :cmessage="message" :cmovies="movies"></cpn>
</div>
<!-- 模板 -->
<template id="cpn">
<div>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 父传子: props
// 注册子组件
const cpn = {
template: '#cpn',
props: {
// 1.类型限制(传入的数据类型必须与这里的一致)
cmovies: Array,
cmessage: String,
// 2.提供一些默认值, 以及必传值
cmessage: {
type: String,
default: 'aaaaaaaa',//当没有传值(绑定 :cmovies="movies")时,显示默认值
required: true //如果写了required属性,就必须绑定 :cmovies="movies"否则会报错
},
// 类型是对象或者数组时, 默认值必须是一个函数,然后通过一个返回值来返回数据
cmovies: {
type: Array,
default() {
return []
}
}
},
data() {
return {}
},
methods: {
}
}
//vue实例也是cpn组件的父组件
// 要将父组件里的数据message 和 movies传给cpn组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
cpn
}
})
</script>
当需要对props进行类型等验证时,需要使用对象写法。
验证都支持哪些数据类型呢?
String
Number
Boolean
Array
Object
Date
Function
Symbol
当我们有自定义构造函数时,验证也支持自定义的类型
在父组件中使用子组件cpn时,v-bind绑定名称是不支持驼峰命名的。cInfo需要改成c-info形式。cMassageInfo → c-massage-info
组件中是可以正常使用的,只有在父组件中使用子组件时,需要改变形式。
<div id="app">
<cpn :c-info="info" :child-my-message="message" v-bind:class></cpn>
</div>
<template id="cpn">
<div>
<h2>{{cInfo}}</h2>
<h2>{{childMyMessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
props: {
cInfo: {
type: Object,
default() {
return {}
}
},
childMyMessage: {
type: String,
default: ''
}
}
}
const app = new Vue({
el: '#app',
data: {
info: {
name: 'why',
age: 18,
height: 1.88
},
message: 'aaaaaa'
},
components: {
cpn
}
})
</script>
props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。
什么时候需要自定义事件呢?
当子组件需要向父组件传递数据时,就要用到自定义事件了。
我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
自定义事件的流程:
在子组件中,通过$emit()来触发事件。
在父组件中,通过v-on来监听子组件事件。
<!--父组件模板-->
<div id="app">
<cpn @item-click="cpnClick"></cpn>
//因为conClick在父组件模板内,所以只有父组件才能访问,应该设置在父组件内部。item-click则是父组件与子组件通信的桥梁
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<!-- 在子组件模板中设置一个监听事件,该事件函数内包含一个发射事件 -->
<!-- 因为需要知道是点击的哪个按钮所以item也要传进去 -->
<button v-for="item in categories"
@click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 1.子组件
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{id: 'aaa', name: '热门推荐'},
{id: 'bbb', name: '手机数码'},
{id: 'ccc', name: '家用家电'},
{id: 'ddd', name: '电脑办公'},
]
}
},
methods: {
btnClick(item) {
// 发射事件: 自定义事件
this.$emit('item-click', item) //第一个参数是自定义事件的名字,要在父组件中使用
}
}
}
// 2.父组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn
},
methods: {
cpnClick(item) {
console.log('cpnClick', item);
}
}
})
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- number1是子组件的 -->
<!-- v-bind 是父传子 -->
<cpn :number1="num1"
:number2="num2"
@num1change="num1change"
@num2change="num2change"/>
<!-- v-on是子传父 -->
<!-- 自定义事件num1change也是子组件的 -->
</div>
<template id="cpn">
<div>
<h2>props:{{number1}}</h2>
<h2>data:{{dnumber1}}</h2>
<!--<input type="text" v-model="dnumber1">-->
<input type="text" :value="dnumber1" @input="num1Input">
<h2>props:{{number2}}</h2>
<h2>data:{{dnumber2}}</h2>
<!--<input type="text" v-model="dnumber2">-->
<input type="text" :value="dnumber2" @input="num2Input">
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
num1: 1,
num2: 0
},
methods: {
num1change(value) {
this.num1 = parseFloat(value)
},
num2change(value) {
this.num2 = parseFloat(value)
}
},
components: {
//子组件
cpn: {
template: '#cpn',
props: {
number1: Number,
number2: Number
},
data() {
return {
dnumber1: this.number1,
dnumber2: this.number2
}
},
methods: {
num1Input(event) {
// 1.将input中的value赋值到dnumber中
this.dnumber1 = event.target.value;
// 2.为了让父组件可以修改值, 发出一个事件
this.$emit('num1change', this.dnumber1)
// 3.同时修饰dnumber2的值
this.dnumber2 = this.dnumber1 * 100;
this.$emit('num2change', this.dnumber2);
},
num2Input(event) {
this.dnumber2 = event.target.value;
this.$emit('num2change', this.dnumber2)
// 同时修饰dnumber2的值
this.dnumber1 = this.dnumber2 / 100;
this.$emit('num1change', this.dnumber1);
}
}
}
}
})
</script>
</body>
</html>
1.children 在父组件中使用children会返回一个数组,里面包含当前组件所有的子组件,然后通过索引访问子组件,再‘.’访问属性或方法。
2.$refs 在父组件中使用。默认是一个空对象。当在子组件上打下标识
<div id="app">
<cpn></cpn>
<cpn></cpn>
<my-cpn></my-cpn>
<y-cpn></y-cpn>
<cpn ref="aaa"></cpn>
<button @click="btnClick">按钮</button>
</div>
<template id="cpn">
<div>我是子组件</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
methods: {
btnClick() {
// 1.$children
// console.log(this.$children);
// for (let c of this.$children) {
// console.log(c.name);
// c.showMessage();
// }
// console.log(this.$children[3].name);
// 2.$refs => 对象类型, 默认是一个空的对象 ref='bbb'
console.log(this.$refs.aaa.name);
}
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是子组件的name'
}
},
methods: {
showMessage() {
console.log('showMessage');
}
}
},
}
})
</script>
1.$parent 用来访问父组件用得很少,因为会增加耦合度,降低组件之间的独立性
2.$root 访问根组件,也用得少
<div id="app">
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>我是cpn组件</h2>
<ccpn></ccpn>
</div>
</template>
<template id="ccpn">
<div>
<h2>我是子组件</h2>
<button @click="btnClick">按钮</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是cpn组件的name'
}
},
components: {
ccpn: {
template: '#ccpn',
methods: {
btnClick() {
// 1.访问父组件$parent
// console.log(this.$parent);
// console.log(this.$parent.name);
// 2.访问根组件$root
console.log(this.$root);
console.log(this.$root.message);
}
}
}
}
}
}
})
</script>
组件的插槽:
组件的插槽也是为了让我们封装的组件更加具有扩展性。
让使用者可以决定组件内部的一些内容到底展示什么。
栗子:移动网站中的导航栏。
移动开发中,几乎每个页面都有导航栏。
导航栏我们必然会封装成一个插件,比如nav-bar组件。
一旦有了这个组件,我们就可以在多个页面中复用了。
但是每个导航条所需要的元素都不完全相同,所以我们可以利用插槽,设置当前组件所需要的元素。
如何封装合适呢?抽取共性,保留不同。
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
是搜索框,还是文字,还是菜单。由调用者自己来决定。
有了这个插槽后,父组件如何使用呢?
<div id="app">
<cpn></cpn>
<cpn><span>哈哈哈</span></cpn> //使用时会自动把span标签替换插槽
</div>
<template id="cpn">
<div>
<slot></slot> //这里设置一个插槽
</div>
</template>
<div id="app">
<cpn></cpn> //每个组件都会有一个button
<cpn></cpn>
<cpn> //每个组件都会包含这些元素
<i>呵呵呵</i>
<div>我是div元素</div>
<p>我是p元素</p>
</cpn>
</div>
<template id="cpn">
<div>
<slot><button>按钮</button></slot> //这里设置一个插槽
</div>
</template>
在模板里定义插槽时就给插槽一个name属性,用来标识插槽。在父组件里使用子组件时,将要替换插槽内容的元素添加slot属性,并且属性值应对应要替换的插槽的name属性值。
<div id="app">
<cpn><span slot="center">标题</span></cpn>
<cpn><button slot="left">返回</button></cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="center"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
</div>
</template>
官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
其实就是父组件只能访问自己的数据不能访问子组件的,子组件也是只能访问自己的数据不能访问父组件的
<div id="app">
<cpn v-show="isShow"></cpn>
//父组件只能访问到父组件作用域内的数据,这里的isShow是父组件的 为true
<cpn v-for="item in names"></cpn>
</div>
<template id="cpn">
<div>
<h2>我是子组件</h2>
<p>我是内容, 哈哈哈</p>
<button v-show="isShow">按钮</button>
//子组件只能访问子组件作用域内的数据所以这里的isShow是子组件的为false
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isShow: true
},
components: {
cpn: {
template: '#cpn',
data() {
return {
isShow: false
}
}
},
}
})
</script>
当父组件要获取子组件的数据时
1.先在子组件的模板中使用插槽并定义一个变量用于绑定需要的子组件的数据
2.然后再使用子组件时,在其内部使用一个template标签(2.5.x以上版本也可以使用别的标签把内容套起来) ,标签中使用\v-slot:default=“slotProps”。slotProps包含所有的插槽 prop 。然就就可以拿到已经绑定需要数据的data,slotProps.date里装着我们需要的子组件的数据。然后在这个cpn里就可以通过data使用子组件的数据了。
<div id="app">
<cpn>
<!--目的是获取子组件中的pLanguages-->
<template v-slot:default="slotProps">//以前版本的slot-scope="slot"被替换为v-slot:default="slotProps"
<span>{{slotProps.data.join(' - ')}}</span>
</template>
</cpn>
</div>
<template id="cpn">
<div>
<slot :data="pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template: '#cpn',
data() {
return {
pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
}
}
}
}
})
</script>
!!!注意:v-slot只能用于组件或模板,其他标签会报错
导入:import
导出:export
export指令用于导出变量,也可以导出对象和函数,比如下面的代码
// 1.导出方式一:
export {
flag, sum //在其他地方定义了的变量
}
// 2.导出方式二:
export var num1 = 1000;
export var height = 1.88
// 3.导出函数/类
export function mul(num1, num2) {
return num1 * num2
}
export class Person {
run() {
console.log('在奔跑');
}
}
export default
某些情况下,一个模块中包含某个的功能,我们并不希望给这个功能命名,而且让导入者可以自己来命名
export default在同一个模块中,不允许同时存在多个。(是唯一的)
const address = '北京市'
export default address
// 4.导入 export default中的内容
import addr from "./aaa.js"; //不管叫什么名字,没有花括号就会赋默认值‘北京市’
import aaaa from "./aaa.js";
在主页中引入时,表明使用模块化思想
导入:
// 1.导入的{}中定义的变量
import {flag, sum} from "./aaa.js";
if (flag) {
console.log('小明是天才, 哈哈哈');
console.log(sum(20, 30));
}
// 2.直接导入export定义的变量
import {num1, height} from "./aaa.js";
console.log(num1);
console.log(height);
// 3.导入 export的function/class
import {mul, Person} from "./aaa.js";
console.log(mul(30, 50));
const p = new Person();
p.run()
// 4.导入 export default中的内容
import addr from "./aaa.js";
addr('你好啊');
// 5.统一全部导入
// import {flag, num, num1, height, Person, mul, sum} from "./aaa.js";
import * as o from './aaa.js'
//这里是将aaa.js的所有变量加入对象o中。需要在当前文件使用时 o.名称 就可以了。而且避免了引入变量与当前文件变量冲突问题。
console.log(o.flag);
console.log(o.height);
文件和文件夹解析:
dist文件夹:用于存放之后打包的文件
src文件夹:用于存放我们写的源文件
main.js:项目的入口文件。具体内容查看下面详情。
mathUtils.js:定义了一些数学工具函数,可以在其他地方引用,并且使用。具体内容查看下面的详情。
index.html:浏览器打开展示的首页html
package.json:通过npm init生成的,npm包管理的文件(暂时没有用上,后面才会用上)
main.js文件中的代码:
mathUtils.js文件中的代码:
首先使用模块化的思想(不管是哪一种模块化规范都可以,webpack会自动帮我们处理)在src文件夹中进行开发。然后利用webpack将src中的所有文件打包到dist文件中。再然后在index.html主页中引入dist中打包完成的文件。
打包指令:
src/main.js中包含各种依赖,是整个项目的入口文件。上述指令指出将打包好的文件以bundle.js文件命名并放入dist文件。
配置webpack.config.js 文件之后就可以直接执行webpack指令而不用每次都把入口和出口作为参数。‘ webpack.config.js ’这个名字是固定的不能更改。
webpack.config.js文件:
const path = require('path')
//这里依赖node的包,需要执行命令 npm init 对当前项目做初始化。会生成一个文件‘package.json’,里面包含此项目的相关信息。
module.exports = {
entry: './src/main.js', //入口
output: { //出口
path: path.resolve(__dirname, 'dist'),
//resolve函数是对两个路径进行一个拼接
//resolve是node里的函数,__dirname是一个全局变量,里面保存了当前文件路径而且是绝对路径。后面的‘dist’参数会在__dirname后拼接一个dist。也就变成了dist的绝对路径。
filename: 'bundle.js'
},
}
package.json文件:
{
"name": "meetwebpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"author": "",
"license": "ISC",
//开发时依赖
"devDependencies": {
"webpack": "^3.6.0"
}
}
scripts属性里的代码表示:
当在终端运行npm run xxx时就会来package.json文件的scripts属性里面查找对应的命令名,并执行后面的代码。
比如:
执行命令npm run test 。 然后会到scripts里面找到test,并执行"echo “Error: no test specified” && exit 1"。
所以我们可以自定义一个命令和调用它时将要执行的代码。
scripts里面定义的命令会优先在本地找,也就是会使用我们安装的本地的webpack,找不到才去全局里面找。而终端里面直接输入的webpack命令会使用全局安装的webpack。
①npm install --save-dev: 安装我们项目开发时的依赖,比如一些插件,对我们的less/sass/js/img进行一些处理,用于开发环境。
②npm install --save: 安装项目上线运行时的依赖,比如我们项目依赖的框架vue,插件jQuery等,用于生产环境。
③npm install -g: 全局安装,通常用于安装脚手架等工具。
④npm install: 不带参数,也会进行安装,但是安装的依赖不会被添加进项目的package.json中。
总结
哪些是我们项目开发时依赖的呢?哪些又是上线后依赖的呢?
开发时的依赖,就是为我们上线做准备工作的,而生产环境中的依赖,会被打包进我们最终的项目代码中,作为项目的一部分。
如果从公司服务器克隆项目到自己的电脑,而自己的电脑webpack和公司开发时使用打包的webpack版本不一致,可能会报错,所以需要安装本地webpack。
npm install [email protected] --sava-dav 安装本地webpack
loader的作用:
在开发中我们不仅仅有基本的js代码处理,我们也需要加载css、图片,也包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等。这些工作由loader完成。
loader使用过程:
步骤一:通过npm安装需要使用的loader
步骤二:在webpack.config.js中的modules关键字下进行配置
大部分loader我们都可以在webpack的官网中找到,并且学习对应的用法。
步骤三:在主函数中依赖对应文件
// 依赖css文件
require('./css/normal.css')
步骤四:使用命令 npm run build(build是scripts中自定义的命令) 打包项目相关文件
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
// css-loader只负责将css文件进行加载
// style-loader负责将样式添加到DOM中
// 使用多个loader时, 是从右向左
use: [ 'style-loader', 'css-loader' ]
}
]
}
}
首先,还是需要安装对应的loader
注意:我们这里还安装了less,因为webpack会使用less对less文件进行编译
其次,修改对应的配置文件
添加一个rules选项,用于处理.less文件
图片处理,我们使用url-loader来处理,依然先安装url-loader
修改webpack.config.js配置文件:
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
// css-loader只负责将css文件进行加载
// style-loader负责将样式添加到DOM中
// 使用多个loader时, 是从右向左
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.less$/,
use: [{
loader: "style-loader", // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader", // compiles Less to CSS
}]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 13000,
name: 'img/[name].[hash:8].[ext]'
},
}
]
},
]
}
}
如果图片大于limit会报错,我们需要安装file-loader模块
npm install --save-dev file-loader
再次打包,运行index.html,就会发现我们的背景图片选出了出来。
而仔细观察,你会发现背景图是通过base64显示出来的
OK,这也是limit属性的作用,当图片小于8kb(limit)时,对图片进行base64编码,当图片小于8kb(limit)时使用file-loader模块。
再次打包,就会发现dist文件夹下多了一个图片文件
我们发现webpack自动帮助我们生成一个非常长的名字
这是一个32位hash值,目的是防止名字重复
但是,真实开发中,我们可能对打包的图片名字有一定的要求
比如,将所有的图片放在一个文件夹中,跟上图片原来的名称,同时也要防止重复。
所以,我们可以在options中添加上如下选项:
img:文件要打包到的文件夹
name:获取图片原来的名字,放在该位置
hash:8:为了防止图片名称冲突,依然使用hash,但是我们只保留8位
ext:使用图片原来的扩展名
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 13000,
name: 'img/[name].[hash:8].[ext]'
//img/的意思是放在img文件夹下。如果不加[]就会被当成变量。重新打包后悔自动创建一个img文件
},
但是,我们发现图片并没有显示出来,这是因为图片使用的路径不正确
默认情况下,webpack会将生成的路径(index路径)直接返回给使用者
但是,我们整个程序是打包在dist文件夹下的,所以这里我们需要在路径下再添加一个dist/
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: 'dist/'
//这句代码是配置url的路径,在每个url前面加上dist/。但是当最终要index放到dist文件中去时。就要删除这个配置,因为index的路径变了。
},
在前面我们说过,如果希望将ES6的语法转成ES5,那么就需要使用babel。
而在webpack中,我们直接使用babel对应的loader就可以了。
npm install --save-dev babel-loader@7 babel-core babel-preset-es2015
babel-core 是 babel的核心用来编译代码。
配置webpack.config.js文件
{
test: /\.js$/,
// exclude: 排除
// include: 包含
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
},
后续项目中,我们会使用Vuejs进行开发,而且会以特殊的文件来组织vue的组件。
所以,下面我们来学习一下如何在我们的webpack环境中集成Vuejs
现在,我们希望在项目中使用Vuejs,那么必然需要对其有依赖,所以需要先进行安装
注:因为我们后续是在实际项目中也会使用vue的,所以并不是开发时依赖
那么,接下来就可以按照我们之前学习的方式来使用Vue了
main.js:
// 5.使用Vue进行开发
import Vue from 'vue'
// import App from './vue/app'
import App from './vue/App.vue'
new Vue({
el: '#app',
data:{
message:'hello world'
}
})
使用vue
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
{{message}}
</div>
<script src="./dist/bundle.js"></script>
</body>
</html>
修改完成后,重新打包,运行程序:
打包过程没有任何错误(因为只是多打包了一个vue的js文件而已)
但是运行程序,没有出现想要的效果,而且浏览器中有报错
这个错误说的是我们使用的是runtime-only版本的Vue。
runtime-only和runtime-compiler
runtime-only 不包含编译template的模块,无法编译template所以会报错。
runtime-compiler 包含编译template的模块,可以编译template,所以应该使用这个版本。
所以我们修改webpack.config.js的配置,添加如下内容即可:
resolve: {
// alias: 别名
extensions: ['.js', '.css', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
//就是vue这个名字对应的是这个目录下的文件vue/dist/vue.esm.js。也就是可以编译template的vue版本。
}
}
但是html模板在之后的开发中,我并不希望手动的来频繁修改。
定义template属性:
在前面的Vue实例中,我们定义了el属性,用于和index.html中的#app进行绑定,让Vue实例之后可以管理它其中的内容
这里,我们可以将div元素中的{{message}}内容删掉,只保留一个基本的id为div的元素
但是如果我依然希望在其中显示{{message}}的内容,应该怎么处理呢?
我们可以再定义一个template属性,代码如下:
new Vue({
el: '#app',
template: '{{message}}',
data:{
message:'xiaowang'
}
})
重新打包,运行程序,显示一样的结果和HTML代码结构
那么,el和template模板的关系是什么呢?
在我们之前的学习中,我们知道el用于指定Vue要管理的DOM,可以帮助解析其中的指令、事件监听等等。
而如果Vue实例中同时指定了template和el,那么template模板的内容会替换掉挂载的对应el的模板。template内容替换掉了el所管理的内容。
这样做有什么好处呢?
这样做之后我们就不需要在以后的开发中再次操作index.html,只需要在template中写入对应的标签即可。但是,书写template模块非常麻烦怎么办呢?
没有关系,稍后我们会将template模板中的内容进行抽离。
会分成三部分书写:template、script、style,结构变得非常清晰。
在上面的案例中vue非常臃肿,看起来非常混乱,现在来整理代码。
①把template从vue里抽离出来(使用组件的方式)
App = {
template: `
{{message}}
{{name}}
`,
data() {
return {
message: 'Hello Webpack',
name: 'coderwhy'
}
},
methods: {
btnClick() {
}
}
}
new Vue({
el: '#app',
template: ' ',
components: {
App
}
})
创建一个App组件并在vue中注册。数据和函数等相关的东西都放到组件中去。
②但是即使抽离了数据,组件仍然在main.js文件里。使得文件看起来臃肿。将组件放到另一文件中,命名app.js
app.JS:
export default {
template: `
{{message}}
{{name}}
`,
data() {
return {
message: 'Hello Webpack',
name: 'coderwhy'
}
},
methods: {
btnClick() {
}
}
}
设置默认导出。
③然后在main.js文件中就只需要
import App from './vue/app'
//defult导入所以不需要{}
④但是在app.js文件里模板和js代码依然没有分开。所以我们需要把他们分开。使得结构更加清晰。
创建一个App.vue文件,将app.js中的template标签里的内容复制到App.vue文件的template标签中。其余的script代码都复制到App.vue的script标签里。style标签可以写css样式。
App.vue
<template>
<div>
<h2 class="title">{{message}}</h2>
<button @click="btnClick">按钮</button>
<h2>{{name}}</h2>
<Cpn/>
</div>
</template>
<script>
import Cpn from './Cpn'
// 另外以App.vue的形式重新创建一个Cpn.vue组件,然后在App.vue文件中使用,先引入,然后注册
export default {
name: "App",
//App是组件名,和.vue文件同名,在创建.vue文件时自动生成
components: {
//在当前组件中注册子组件Cpn
Cpn
},
data() {
return {
message: 'Hello Webpack',
name: 'coderwhy'
}
},
methods: {
btnClick() {
}
}
}
</script>
<style scoped>
.title {
color: green;
}
</style>
设置完App.vue之后在main.js中导入并使用。现在整个App.vue文件是一个组件。
import App from './vue/App.vue'
new Vue({
el: '#app',
template: ' ',
components: {
App
}
})
①安装vue-loader和vue-template-compiler
npm install vue-loader vue-template-compiler --save-dev
③ 打包时可能会报错,缺少一个什么文件。这是因为vue-loader版本的原因在14.0.0之后的版本使用vue-loader时还需要配置一个文件,如果不想配置这个文件就修改package.json文件
"vue-loader": "^13.0.0", // ^符号会匹配一个13.0.0到14.0.0的版本
再运行 npm install 命令。
④ 安装完成后执行npm run bulid 命令打包
自此,就可以以App.vue为根节点,创建子组件(如:Cpn.vue)并在App.vue文件中使用。形成一套vue树结构。
App.vue文件参见 第6点,vue的使用方案。
Cpn.vue:
<template>
<div>
<h2>我是cpn组件的标题</h2>
<p>我是cpn组件的内容, 哈哈哈</p>
<h2>{{name}}</h2>
</div>
</template>
<script>
export default {
name: "Cpn",
data() {
return {
name: 'CPN组件的name'
}
}
}
</script>
<style scoped>
//在Vue组件中,为了使样式私有化(模块化),不对全局造成污染,可以在style标签上添加scoped属性以表示它的只属于当下的模块,
//局部有效。如果一个项目中的所有vue组件style标签全部加上了scoped,相当于实现了样式的私有化。
</style>
如果希望在import时更简洁,省略文件后缀名可以在webpack.config.js里配置
resolve: {
extensions: ['.js', '.css', '.vue'],
//数组里写上希望省略的文件后缀名
// alias: 别名
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
loader和plugin区别
loader主要用于转换某些类型的模块,它是一个转换器。
plugin是插件,它是对webpack本身的扩展,是一个扩展器。
plugin的使用过程:
步骤一:通过npm安装需要使用的plugins(某些webpack已经内置的插件不需要安装)
步骤二:在webpack.config.js中的plugins中配置插件。
我们先来使用一个最简单的插件,为打包的文件添加版权声明
该插件名字叫BannerPlugin,属于webpack自带的插件。
按照下面的方式来修改webpack.config.js的文件:
const path = require('path')
const webpack = require('webpack')
module.exports = {
...
plugins: [
// BannerPlugin是webpack自带的插件所以通过webpack来使用,使用webpack对象之前需要先引入const webpack = require('webpack')
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
}),
new UglifyjsWebpackPlugin()
],
}
重新打包程序:查看bundle.js文件的头部,看到如下信息
目前,我们的index.html文件是存放在项目的根目录下的。
我们知道,在真实发布项目时,发布的是dist文件夹中的内容,但是dist文件夹中如果没有index.html文件,那么打包的js等文件也就没有意义了。
所以,我们需要将index.html文件打包到dist文件夹中,这个时候就可以使用HtmlWebpackPlugin插件
HtmlWebpackPlugin插件可以为我们做这些事情:
自动生成一个index.html文件(可以指定模板来生成)
将打包的js文件,自动通过script标签插入到body中
安装HtmlWebpackPlugin插件
npm install html-webpack-plugin --save-dev
使用插件,修改webpack.config.js文件中plugins部分的内容如下:
plugins: [
// BannerPlugin是webpack自带的插件所以通过webpack来使用,使用webpack对象之前需要先引入const webpack = require('webpack')
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
//在当前webpack.config.js所在文件目录下,寻找index.html,并以他为模板,生成dist文件夹下面的index.html文件
}),
],
这里的template表示根据什么模板来生成index.html
另外,我们需要删除之前在output中添加的publicPath属性(打包的时候,webpack会在静态文件路径前面添加publicPath的值,然而我们现在index.html和bundle.js在同一路径下,所以不需要配置publicPath了)
否则插入的script标签中的src可能会有问题
在项目发布之前,我们必然需要对js等文件进行压缩处理
这里,我们就对打包的js文件进行压缩
我们使用一个第三方的插件uglifyjs-webpack-plugin,并且版本号指定1.1.1,和CLI2保持一致
npm install [email protected] --save-dev
搭建本地服务器的好处:
我们现在每更改一次代码,就要重新打包运行,在开发过程中耗时耗力。使用我们可以搭建一个本地服务器,然后我们修改文件中的代码之后,浏览器会实时刷新,不用改一点打包一次改一点打包一次。等到开发完成统一打包即可。
webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,可以实现我们想要的让浏览器自动刷新显示我们修改后的结果。
不过它是一个单独的模块,在webpack中使用之前需要先安装它.这个版本对应脚手架2的版本,webpack3.6.0也是对应的脚手架2.
npm install --save-dev [email protected]
devserver也是作为webpack中的一个选项,选项本身可以设置如下属性:
contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
port:端口号
inline:页面实时刷新
historyApiFallback:在SPA页面中,依赖HTML5的history模式
webpack.config.js文件配置修改如下:
devServer: {
contentBase: './dist',
inline: true
}
我们可以再配置package.json中的scripts属性:
(package.json的scripts属性里定义的命令会现在本地找,然后再去全局找)
"dev": "webpack-dev-server --open"
–open参数表示直接打开浏览器
现在执行npm run dev就可以启动本地服务器
我们注意到在webpack.config.js文件中有一些配置是开发时需要的比如(devServer搭建本地服务器时需要配置),但在将代码部署到真正的服务器上的时候时这些配置又不需要了。所以现在把webpack.config.js文件中的开发时配置和生产时配置做一个分离,以便我们更好的管理项目。
devServer: {
contentBase: './dist',
inline: true
}
现在创建3个文件:
①base.config.js 用来装基础的配置。开发时和运行时都需要的配置
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'bundle.js',
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
// css-loader只负责将css文件进行加载
// style-loader负责将样式添加到DOM中
// 使用多个loader时, 是从右向左
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.less$/,
use: [{
loader: "style-loader", // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader", // compiles Less to CSS
}]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 13000,
name: 'img/[name].[hash:8].[ext]'
},
}
]
},
{
test: /\.js$/,
// exclude: 排除
// include: 包含
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
},
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
resolve: {
// alias: 别名
extensions: ['.js', '.css', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
plugins: [
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
②prod.config.js 用来装生产时(即将部署到服务器上时)相关配置
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')
module.exports = webpackMerge(baseConfig, {
plugins: [
new UglifyjsWebpackPlugin()
]
})
③dev.config.js 用来装开发时配置
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')
module.exports = webpackMerge(baseConfig, {
devServer: {
contentBase: './dist',
inline: true
}
})
文件分离之后需要合并,这里用到一个工具webpack-merge,先安装这个工具。
npm install webpack-merge --save-dev
使用这个工具需要先请求
const webpackMerge = require('webpack-merge')
然后在文件导出中使用
module.exports = webpackMerge(baseConfig, { //这里填写各种相关配置
})
第一个参数:为基础配置文件名,就是将基础配置文件融入到本文件中。
第二个参数:是一个对象,里面包含各种配置
具体使用参见上面两个代码片段。
当将webpack.config.js文件分离成3个文件之后,webpack.config.js文件就不再需要了,可以删除。删除之后我们在进行打包 npm run build 会报错,因为找不到webpack.config.js,接下来手动配置package.json文件
原来:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server --open"
},
现在:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/prod.config.js",
"dev": "webpack-dev-server --open --config ./build/dev.config.js"
},
可以看见我们更改的内容就是,将build 和dev命令手动设置了配置文件目录。这样它将会去我们指定的目录找配置文件,而不是去webpack.config.js找相关配置。
CLI是什么意思?
●CLI是Command-Line Interface, 翻译为命令行界面, 但是俗称脚手架.
●Vue CLI是一个官方发布 vue.js 项目脚手架
●使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置.
Vue.js官方脚手架工具就使用了webpack模板
●对所有的资源会压缩等优化操作
●它在开发过程中提供了一套完整的功能,能够使得我们开发过程中变得高效。
安装Vue脚手架
npm install -g @vue/cli
注意:上面安装的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目时不可以的。
Vue CLI2初始化项目
vue init webpack my-project
Vue CLI3初始化项目
vue create my-project
当我们选择runtime-only方式构建的话,需要用到render函数
render返回的内容会替换el挂载的对象,作为模版。因此它省略了template→ast这一步,使得程序更快,代码更少。
创建一个组件:
render函数返回一个组件:
和上面的图片完全相同,只是不同的写法
import App from './App'
//App是被抽离成一个单独文件的组件
new Vue({
el: '#app',
// h代表createElement函数,叫什么名字都可以
render: function (h) {
return h(App)
//用App组件替换挂载的app
}
})
createElement函数
createElement('标签', {标签的属性}, [''])
//后面两个参数可省
createElement('button', ['按钮'])
//相当于创建一个这个元素
//可嵌套
createElement('h2',
{class: 'box'},
['Hello World', createElement('button', ['按钮'])])
//Hello World
但是App.vue文件中依然有template标签,怎么省略template→ast这一步呢?
那么.vue文件中的template是由谁处理的了?
是由vue-template-compiler,它就是将.vue文件中的template转化为render函数。在main.js中import App时就已经将App转化为一个对象,对象中没有template,只有render。
render -> vdom -> UI
Vue CLI3初始化项目
vue create my-project
vue-cli 3 与 2 版本有很大区别
vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,build和config等目录
vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
移除了static文件夹,新增了public文件夹,并且index.html移动到public中
执行 命令
vue ui
会进入vue的图形配置界面,可以在这里面进行一些相关的配置。
以前的像cli2里面的很多配置文件被隐藏起来了
自定义配置:大部分的配置都被cli3帮我们隐藏起来了,如果我们希望自己配置一些东西可以在src目录下创建vue.config.js文件,只能叫这个名字不能修改。
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
// 箭头函数: 也是一种定义函数的方式
// 1.定义函数的方式: function
const aaa = function () {
}
// 2.对象字面量中定义函数
const obj = {
bbb() {
}
}
// 3.ES6中的箭头函数
// const ccc = (参数列表) => {
//
// }
const ccc = () => {
}
1.参数问题:
// 1.参数问题:
// 1.1.放入两个参数
const sum = (num1, num2) => {
return num1 + num2
}
// 1.2.放入一个参数
const power = num => {
return num * num
}
//一个参数时,()可省略
2.函数中
函数代码块中只有一行代码时,可以使用简写形式,会执行代码并将结果作为函数返回值。
// 2.1.函数代码块中有多行代码时
const test = () => {
// 1.打印Hello World
console.log('Hello World');
// 2.打印Hello Vuejs
console.log('Hello Vuejs');
}
// 2.2.函数代码块中只有一行代码
// const mul = (num1, num2) => {
// return num1 + num2
// }
const mul = (num1, num2) => num1 * num2
//使用函数
console.log(mul(20, 30));
// const demo = () => {
// console.log('Hello Demo');
// }
const demo = () => console.log('Hello Demo')
//使用函数
console.log(demo());
☀箭头函数中的this
匿名函数的this统一都是window
但是箭头函数中不一样,它向外层作用域中, 一层层查找this, 直到有this的定义.
const obj = {
aaa() {
setTimeout(function () {
setTimeout(function () {
console.log(this); // window
})
setTimeout(() => {
console.log(this); // window
})
})
setTimeout(() => {
setTimeout(function () {
console.log(this); // window
})
//this
setTimeout(() => {
console.log(this); // obj
})
})
}
}
obj.aaa()
后端路由:
早期的网页是通过html css 和一些特殊技术如:php ,jsp在服务器渲染好网页再推送,也称服务器端渲染。页面请求一个url,后端根据正则表达式识别这个url并返回相应页面给前端显示。后端处理页面和url之间的映射关系
当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户顿.
这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
后端路由的缺点:
一种情况是整个页面的模块由后端人员来编写和维护的.
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码.
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情.
前后端分离阶段:
◆ 随着Ajax的出现, 有了前后端分离的开发模式.
◆ 后端只提供API来返回数据, 前端通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中.
◆ 这样做最大的优点就是前后端责任的清晰, 后端专注于数据上, 前端专注于交互和可视化上.
◆ 并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可.
◆ 目前很多的网站依然采用这种模式开发.
单页面富应用阶段:
◆ 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
◆ 也就是前端来维护一套路由规则.
当前端路由接收到url请求时,去找到对应页面的html+css+js把它们抽离出来,渲染成网页。以前的操作方式都是一个页面一套html+css+js,通过将url和页面映射来管理页面和请求。前端路由只有一套html+css+js,它通过前端路由来管理url和对应的html+css+js资源的映射
前端路由的核心
◆ 改变URL,但是页面不进行整体的刷新。
url的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性.
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新
◆ HTML5的history模式:pushState
history接口是HTML5新增的, 它有五种模式改变URL而不刷新页面.
history.pushState() 三个参数 {对象},‘title’,‘url’
pushState方法就是入栈
back()方法是出栈
url永远只显示栈顶元素
◆ HTML5的history模式:replaceState
用当前url替换之前的url
补充说明:
上面只演示了三个方法
因为 history.back() 等价于 history.go(-1)
history.forward() 则等价于 history.go(1)
这三个接口等同于浏览器界面的前进后退。
使用vue-router的步骤:
第一步: 创建路由组件
第二步: 配置路由映射: 组件和路径映射关系
第三步: 使用路由: 通过
创建路由组件About.vue
<template>
<div>
<h2>我是关于</h2>
<p>我是关于内容, 呵呵呵</p>
</div>
</template>
<script>
export default {
name: "About",
created() {
console.log('About created');
},
destroyed() {
console.log('About destroyed');
}
}
</script>
<style scoped>
</style>
配置路由映射: 组件和路径映射关系 index.js
// 配置路由相关的信息
import VueRouter from 'vue-router'
import Vue from 'vue'
import Home from '../components/Home'
import About from '../components/About'
// 1.通过Vue.use(插件), 安装插件
Vue.use(VueRouter)
//插件都需要用这个方法来安装
// 2.创建VueRouter对象
const routes = [
{
path: '/home',
component: Home,
},
{
path: '/about',
component: About,
]
// ↑将routes 提取出来以免结构太复杂不利于阅读
const router = new VueRouter({
// 配置路由和组件之间的应用关系
routes,
})
// 3.将router对象传入到Vue实例
export default router
使用路由: 通过
App.vue
<template>
<div id="app">
<h2>我是APP组件</h2>
<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
//router-link 是router特定的标签,用于渲染组件
<router-view></router-view>
//router-view是用来占位,表示组件内容渲染在哪个位置,一旦点击组件,就会把组件内容替换到router-view的位置
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
使用重定向显示首页
index.js
//2.定义路由
const routes = [
{
path: '',
//空白就是缺省值
// redirect重定向
redirect: '/home'
},
{
path:'/home',
component:home
},
{
path:'/about',
component:about
}
]
有时候, 页面的跳转可能需要执行对应的JavaScript代码, 这个时候, 就可以使用第二种跳转方式了。
1.this.$router.push()
描述:跳转到不同的url,但这个方法会向history栈添加一个记录,点击后退会返回到上一个页面。
用法:
2.this.$router.replace()
描述:同样是跳转到指定的url,但是这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。
3.this.$router.go(n)
相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面
1.首先 创建uer.vue
<template>
<div>
<h2>用户标题</h2>
<p>用户内容</p>
<h2>{{userId}}</h2>
<h2>{{$route.params.userId}}</h2>
<!-- 获取url中的数据,$route表示当前活动路由,$router表示我们创建的整个路由 -->
</div>
</template>
<script>
export default {
name:'user',
computed: {
userId() {
return this.$route.params.userId
}
},
}
</script>
</script>
this.$route.params.userId //是获取url中的userId=
route是当前活跃的路由,router是整个我们创建和配置的路由。
params是route的属性,用来储存数据的键值对(对象形式,{key:value}),储存很多属性(键值对,属性,属性值)在里面.
2.将user.vue导入App.vue
<template>
<div id="app">
<p>app</p>
<p>ddd</p>
<router-link to='/home' tag="button" replace> 首页 </router-link>
<router-link to='/about' tag="button" replace> 关于 </router-link>
<router-link v-bind:to= "'/user/'+userId" tag="button" replace> 用户 </router-link>
//动态绑定数据用v-bind
<router-view></router-view>
</div>
</template>
<script>
import home from './components/Home.vue'
import about from './components/About.vue'
import user from './components/User.vue'
export default {
name: 'App',
components: {
home,
about,
user,
},
data() {
return {
userId:'xiaowang',
}
},
}
</script>
<style>
</style>
index.js里配置路由映射关系
import Vue from 'vue'
import VueRouter from 'vue-router'
import home from '../components/Home.vue'
import about from '../components/About.vue'
import user from '../components/User.vue'
//1.注入插件
Vue.use(VueRouter)
//2.定义路由
const routes = [
{
//后面要拼接的语法
path:'/user/:userId',
component:user
}
]
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
首先, 我们知道路由中通常会定义很多不同的页面.
这个页面最后被打包在哪里呢? 一般情况下, 是放在一个js文件中.
但是, 页面这么多放在一个js文件中, 必然会造成这个页面非常的大.
如果我们一次性从服务器请求下来这个页面, 可能需要花费一定的时间, 甚至用户的电脑上还出现了短暂空白的情况.
如何避免这种情况呢? 使用路由懒加载就可以了.
路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块.
只有在这个路由被访问到的时候, 才加载对应的组件
嵌套路由是一个很常见的功能
比如在home页面中, 我们希望通过/home/news和/home/message访问一些内容.
一个路径映射一个组件, 访问这两个路径也会分别渲染两个组件.
News和Message都是不同的模板
先写News和Message组件
<template>
<div>
<ul>
<li>信息</li>
<li>信息</li>
<li>信息</li>
<li>信息</li>
<li>信息</li>
</ul>
</div>
</template>
<script>
export default {
name : 'message'//一定要加引号!!!
}
</script>
然后在他们的父组件home的template中使用router-link使用这两个组件
<template>
<div>
<h2>首页标题</h2>
<p>首页内容</p>
<router-link to='/home/news'>新闻</router-link>
<router-link to='/home/message'>信息</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name:'home',
}
</script>
之后,在index.js中设置路由
import Vue from 'vue'
import VueRouter from 'vue-router'
// import home from '../components/Home.vue'
// import about from '../components/About.vue'
// import user from '../components/User.vue'
const home = () => import('../components/Home.vue')
const about = () => import('../components/About.vue')
const user = () => import('../components/User.vue')
const message = () => import('../components/Message.vue')
const news = () => import('../components/News.vue')
//路由懒加载的形式
//1.注入插件
Vue.use(VueRouter)
//2.定义路由
const routes = [
{
path: '',
// redirect重定向
redirect: '/home'
},
{
path:'/home',
component:home,
children :[
{
path:'/home/news',
component:news
},
{
path:'message',
component:message
}
]
},
{
path:'/about',
component:about
},
{
//后面要拼接的语法
path:'/user/:userId',
component:user
}
]
//3.创建路由实例
const router = new VueRouter({
routes,
mode: 'history',
//应用history模式
})
//导出router实例
export default router
传递参数主要有两种类型: params和query
params的类型:
配置路由格式: /router/:id
传递的方式: 在path后面跟上对应的值
传递后形成的路径: /router/123, /router/abc
$route.params.userId 利用route的params属性取出数据
query的类型:
配置路由格式: /router, 也就是普通配置
传递的方式: 对象中使用query的key作为传递方式
传递后形成的路径: /router?id=123, /router?id=abc
http://localhost:8080/profile?name=xiaowang&age=22
?之后就是query
$route.query.name 取值的方式
App.vue
<router-link :to= "{path:'/profile',query:{name:'xiaowang',age:22}}" > 档案 </router-link>
profile.vue
<template>
<div>
<p>我是profile组件</p>
<p>{{$route.query.name}}</p>
<p>{{$route.query.age}}</p>
</div>
</template>
<script>
export default {
name : 'profile'
}
</script>
index.js
{
path:'/profile',
component:profile
}
◆$route和$router是有区别的
$router为VueRouter实例,想要导航到不同URL,则使用、$router.push方法
$route为当前router跳转对象里面可以获取name、path、query、params等
什么是导航守卫?
vue-router提供的导航守卫主要用来监听监听路由的进入和离开的.
vue-router提供了beforeEach和afterEach的钩子函数, 它们会在路由即将改变前和改变后触发.
导航钩子的三个参数解析:
to: 即将要进入的目标的路由对象.
from: 当前导航即将要离开的路由对象.
next: 调用该方法后, 才能进入下一个钩子.
我们可以利用beforeEach来完成标题的修改.
首先, 我们可以在钩子当中定义一些标题, 可以利用meta来定义
其次, 利用导航守卫,修改我们的标题.
钩子函数中一定要有next()否则程序不会进行下一步
补充一:如果是后置钩子, 也就是afterEach, 不需要主动调用next()函数.
补充二: 上面我们使用的导航守卫, 被称之为全局守卫.
◆路由独享的守卫.
◆组件内的守卫.
更多内容, 可以查看官网进行学习:
https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E8%B7%AF%E7%94%B1%E7%8B%AC%E4%BA%AB%E7%9A%84%E5%AE%88%E5%8D%AB
跳转不同的url时,组件会不停的被渲染被销毁。
它们有两个非常重要的属性:
◆include - 字符串或正则表达,只有匹配的组件会被缓存
◆exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
<keep-alive exclude="Profile,User">//多个组件名用,连接
<router-view/>
</keep-alive>
router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存:
<script>
export default {
name: "Home",
data() {
return {
message: '你好啊',
path: '/home/news'
}
},
created() {
console.log('home created');
},
destroyed() {
console.log('home destroyed');
},
// 这两个函数, 只有该组件被保持了状态使用了keep-alive时, 才是有效的
//activated是当当前页面处于活动状态时触发的回调函数
activated() {
this.$router.push(this.path);
console.log('activated');
},
//deactivated是当当前页面要跳转到其它页面之前一瞬间执行的回调函数
deactivated() {
console.log('deactivated');
},
beforeRouteLeave (to, from, next) {
console.log(this.$route.path);
this.path = this.$route.path;
next()
}
}
</script>
bulid —> webpack.base.conf.js —> resolve里配置别名
正常情况下,不会有什么问题,可以正常运行并且获取我们想要的结果。
但是,这样额代码难看而且不容易维护。
我们更加期望的是一种更加优雅的方式来进行这种异步操作。
如何做呢?就是使用Promise。
Promise可以以一种非常优雅的方式来解决这个问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
// 1.使用setTimeout
// setTimeout(() => {
// console.log('Hello World');
// }, 1000)
// 什么情况下会用到Promise?
// 一般情况下是有异步操作时,使用Promise对这个异步操作进行封装
// new -> 构造函数(1.保存了一些状态信息 2.执行传入的函数)
// 在执行传入的回调函数时, 会传入两个参数, resolve, reject.本身又是函数
new Promise((resolve, reject) => {
//primise的参数本身是一个函数。而这个箭头函数包含2个参数resolve和reject,这两个参数又分别是两个函数
setTimeout(() => {
// 成功的时候调用resolve
// resolve('Hello World')
//resolve执行结束后会执行then(),resolve的参数也会传给then作为then回调函数的参数
// 失败的时候调用reject
//调用reject之后执行catch,reject的参数传给catch()
reject('error message')
}, 1000)
}).then((data) => {
// 1.100行的处理代码
console.log(data);
console.log(data);
console.log(data);
console.log(data);
console.log(data);
}).catch((err) => {
console.log(err);
})
</script>
</body>
</html>
异步操作之后会有三种状态
◆pending:等待状态,比如正在进行网络请求,或者定时器没有到时间。
◆fulfill:满足状态,当我们主动回调了resolve时,就处于该状态,并且会回调.then()
◆reject:拒绝状态,当我们主动回调了reject时,就处于该状态,并且会回调.catch()
promise另外的处理形式
<script>
new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('Hello Vuejs')
reject('error message')
}, 1000)
}).then((data)=> {
console.log(data);
}, (err) => {
console.log(err);
})
//在then传入两个参数(两个箭头函数),第一个在调用resolve时执行,第二个在调用reject时执行
</script>
<script>
// 参数 -> 函数(resolve, reject)
// resolve, reject本身它们又是函数
// 链式编程
new Promise((resolve, reject) => {
// 第一次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
// 第一次拿到结果的处理代码
console.log('Hello World');
console.log('Hello World');
console.log('Hello World');
console.log('Hello World');
console.log('Hello World');
console.log('Hello World');
return new Promise((resolve, reject) => {
// 第二次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
})
}).then(() => {
// 第二次处理的代码
console.log('Hello Vuejs');
console.log('Hello Vuejs');
console.log('Hello Vuejs');
console.log('Hello Vuejs');
console.log('Hello Vuejs');
console.log('Hello Vuejs');
return new Promise((resolve, reject) => {
// 第三次网络请求的代码
setTimeout(() => {
resolve()
})
})
}).then(() => {
// 第三处理的代码
console.log('Hello Python');
console.log('Hello Python');
console.log('Hello Python');
console.log('Hello Python');
console.log('Hello Python');
})
</script>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
// 1.自己处理10行代码
console.log(res, '第一层的10行处理代码');
// 2.对结果进行第一次处理
return Promise.resolve(res + '111')
}).then(res => {
console.log(res, '第二层的10行处理代码');
return Promise.resolve(res + '222')
}).then(res => {
console.log(res, '第三层的10行处理代码');
})
// 省略掉Promise.resolve
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
// 1.自己处理10行代码
console.log(res, '第一层的10行处理代码');
// 2.对结果进行第一次处理
return res + '111'
}).then(res => {
console.log(res, '第二层的10行处理代码');
return res + '222'
}).then(res => {
console.log(res, '第三层的10行处理代码');
})
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
// 1.自己处理10行代码
console.log(res, '第一层的10行处理代码');
// 2.对结果进行第一次处理
// return Promise.reject('error message')
throw 'error message'
}).then(res => {
console.log(res, '第二层的10行处理代码');
return Promise.resolve(res + '222')
}).then(res => {
console.log(res, '第三层的10行处理代码');
}).catch(err => {
console.log(err);
})
官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
vuex相关的代码放在store文件夹内
创建一个index.js文件
import Vue from 'vue'
import Vuex from 'vuex'
// 1.安装插件
Vue.use(Vuex)
// 2.创建对象
const state = {
counter: 1000,
students: [
],
}
const mutations = {
}
const actions = {
}
const getters = {
}
const store = new Vuex.Store({
state,
mutations,
actions,
getters,
modules: {
a: moduleA
}
})
// 3.导出store独享
export default store
Actions管理异步操作。
mutations储存方法,可以被devtools跟踪管理。
state通过mutations改变数据,数据是响应式的。
main.js
一定要import store!!!
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
创建组件HelloVuex.vue
<template>
<div>
<h2>{{$store.state.counter}}</h2>
</div>
</template>
<script>
export default {
name: "HelloVuex"
}
</script>
<style scoped>
</style>
在src创建store文件夹,文件夹下新建index.js
index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 1.安装插件
Vue.use(Vuex)
const actions = {
}
const getters = {
}
// 3.导出store独享
export default new Vuex.Store({
state:{
counter: 1080
},
mutations:{
//方法
// 要传入state参数,就是上面那个state。否则会报错
increment(state){
state.counter++
},
decrement(state){
state.counter--
}
},
actions,
getters,
modules: {
}
})
App.vue
<template>
<div id="app">
<h1>------------App内容</h1>
<h2>{{$store.state.counter}}</h2>
<button @click="addition">+</button>
<button @click="subtraction">-</button>
<h1>------------App内容</h1>
<h1>----------helloVueX内容-----------------</h1>
<HelloVuex></HelloVuex>
<h1>----------helloVueX内容-----------------</h1>
</div>
</template>
<script>
import HelloVuex from './components/HelloVuex'
export default {
name: 'App',
components:{
HelloVuex,
},
data(){
return{
messgae:'我是APP组件',
}
},
methods: {
addition(){
this.$store.commit('increment')
},
subtraction(){
this.$store.commit('decrement')
}
},
}
</script>
<style>
</style>
英文名称是Single Source of Truth,也可以翻译成单一数据源。
vuex让我们在一个项目里只创建一个store,如果创建了多个store不利于后期维护和管理。
new Vuex.Store({})new的整个store永远对应使用数据时的$store
◆State
用于存放数据,是响应式的
◆Getters
计算属性
export default new Vuex.Store({
state:{
counter: 1080
},
mutations:{
//方法
// 要传入state参数,就是上面那个state。否则会报错
increment(state){
state.counter++
},
decrement(state){
state.counter--
}
},
actions:{
},
getters:{
powerCounter(state){
return state.counter*state.counter;
},
},
modules: {
}
})
◆Mutation
◆Action
◆Module
有时候,我们需要从store中获取一些state改变后的状态(操作后的数据),比如下面的Store中:
◆获取学生年龄大于20的个数。
◆我们可以在Store中定义getters
在组件内使用↑
Getters作为参数和传递参数
如果我们已经有了一个获取所有年龄大于20岁学生列表的students, 那么代码可以这样来写
getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数.getters return一个箭头函数,这个箭头函数可以传参数。
比如上面的案例中,我们希望根据ID获取用户的信息
Vuex的store状态的更新唯一方式:提交Mutation(要改变store的数据通过mutation改变,vuex推荐,因为可以跟踪改变过程)
Mutation主要包括两部分:
◆字符串的事件类型(type)
◆一个回调函数(handler),该回调函数的第一个参数就是state。
Mutation传递参数
在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数
参数被称为是mutation的载荷(Payload),payload可以是一个对象.
通过commit进行提交是一种普通的方式
subtraction(){
this.$store.commit('changeCount',count)
}
Vue还提供了另外一种风格, 它是一个包含type属性的对象
App.js
type属性值就是提交的函数名
store/index.js
payload是整个提交的对象相当于payload={type:‘changeCount’ , count:100}
Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.
这就要求我们必须遵守一些Vuex对应的规则:
◆提前在store中初始化好所需的属性.
◆当给state中的对象添加新属性时, 使用下面的方式:
方式一: 使用Vue.set(obj, ‘newProp’, 123)
方式二: 用新对象给旧对象重新赋值
info对象中本没有height属性,在Mutation中直接添加是不会变成响应式的,但是info对象里已经添加了height属性,只是界面上不会显示。
Vue.set(state.info,'height',1.98)
//(要更改属性的对象,key,value)
delete方法也不是响应式,删掉之后虽然对象里的数据被删除了,但页面还在。
delete(state.info,age)
Vue.delete方法是响应式的,不仅删除了数据,页面也会刷新。
Vue.delete(state.info,age)
问题:
在mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.
解决:
在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.
在一个文件中定义所有的事件。
store/index.js
App.js
异步操作在mutation中不是响应式的,把异步操作放在actions中
由图的Vue Components开始。
App.vue上定义一个按钮
App.vue 里定义这个点击事件
methods: {
updataInfo(){
// this.$store.commit('aUpdataInfo')
// commit提交的是mutations,dispatch发送的是actions
this.$store.dispatch('aUpdataInfo')
}
},
组件通过dispatch发送aUpdataInfo事件到actions,在actions中定义aUpdataInfo事件
actions:{
// context
aUpdataInfo(context){
setTimeout(() => {
context.commit("updataInfo")
},1000)
},
},
在actions中又沿着图的方向commit updataInfo事件给mutations
在mutations中修改数据,这样才能被devtools跟踪到。
mutations:{
updataInfo(state){
state.info.name = 'xiaowang'
}
},
actions也是支持传递payload
在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.
当应用变得非常复杂时,store对象就有可能变得相当臃肿.
为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutations、actions、getters等
创建好moudle之后用键值对的方式引入store
App.vue中定义一个按钮
<h2>{{$store.state.a.name}}</h2>
<!-- 在app中访问模块里的state数据要加上注册模块的key值,否则不能访问 -->
<button @click="upDataName">修改名字</button>
定义按钮的点击事件,提交到mutations
upDataName(){
this.$store.commit('upDataName','lisi')
}
在mutations中修改数据
const moudleA = {
state:{
name:'xiaoxiao'
},
mutations:{
upDataName(state,payload){
state.name = payload
}
},
}
store/index.js
!!!注意:moudle里的actions会commit给moudle自己的mutations,不会给
const moudleA = {
state:{
name:'xiaoxiao'
},
mutations:{
upDataName(state,payload){
state.name = payload
},
changeName(state,payload){
state.name = payload
}
},
actions:{
changeName(context){
setTimeout(() => {
context.commit('changeName','wwww')
},1000)
}
},
}
app.vue
绑定一个点击事件,发送给store的actions
changeName(){
this.$store.dispatch('changeName')
},
局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
打印context(有root的是根节点的数据)
当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰.
JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。由于同源策略,一般来说位于 server1.example.com 的网页无法与不是 server1.example.com的服务器沟通,而 HTML 的
params的参数通过“?”连接在url后面(专门针对get请求的参数拼接)
1.axios的基本使用
import Vue from 'vue'
import App from './App'
import axios from 'axios'
Vue.config.productionTip = false
new Vue({
el: '#app',
render: h => h(App)
})
1.axios的基本使用
axios({
url: 'http://123.207.32.32:8000/home/multidata',
// method: 'post'
}).then(res => {
console.log(res);
})
axios({
url: 'http://123.207.32.32:8000/home/data',
// 专门针对get请求的参数拼接
params: {
type: 'pop',
page: 1
}
}).then(res => {
console.log(res);
})
2.axios发送并发请求
//2.axios发送并发请求
axios.all([axios({
url: 'http://123.207.32.32:8000/home/multidata'
}), axios({
url: 'http://123.207.32.32:8000/home/data',
params: {
type: 'sell',
page: 5
}
})]).then(results => {
console.log(results);
console.log(results[0]);
console.log(results[1]);
})
3.使用全局的axios和对应的配置在进行网络请求
// 3.使用全局的axios和对应的配置在进行网络请求
axios.defaults.baseURL = 'http://123.207.32.32:8000'
axios.defaults.timeout = 5000
axios.all([axios({
url: '/home/multidata'
}), axios({
url: '/home/data',
params: {
type: 'sell',
page: 5
}
})]).then(axios.spread((res1, res2) => {
console.log(res1);
console.log(res2);
}))
// axios.defaults.baseURL = 'http://222.111.33.33:8000'
// axios.defaults.timeout = 10000
axios({
url: 'http://123.207.32.32:8000/category'
})
4.创建对应的axios的实例
// 4.创建对应的axios的实例
const instance1 = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})
instance1({
url: '/home/multidata'
}).then(res => {
console.log(res);
})
instance1({
url: '/home/data',
params: {
type: 'pop',
page: 1
}
}).then(res => {
console.log(res);
})
const instance2 = axios.create({
baseURL: 'http://222.111.33.33:8000',
timeout: 10000,
// headers: {}
})
5.封装request模块
// 5.封装request模块
import {request} from "./network/request";
request({
url: '/home/multidata'
}, res => {
console.log(res);
}, err => {
console.log(err);
})
request({
baseConfig: {
},
success: function (res) {
},
failure: function (err) {
}
})
request({
url: '/home/multidata'
}).then(res => {
console.log(res);
}).catch(err => {
// console.log(err);
})
request.js
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})
理解:
request.js
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})
return instance(config)
//返回的是一个promise
}
main.js
request({
url: '/home/multidata'
//由于request返回的是一个promise所以可以继续.then .catch
}).then(res => {
console.log(res);
}).catch(err => {
// console.log(err);
})
// 2.axios的拦截器
// 2.1.请求拦截的作用
instance.interceptors.request.use(config => {
// console.log(config);
// 1.比如config中的一些信息不符合服务器的要求
// 2.比如每次发送网络请求时, 都希望在界面中显示一个请求的图标
// 3.某些网络请求(比如登录(token)), 必须携带一些特殊的信息
return config
}, err => {
// console.log(err);
})
// 2.2.响应拦截
instance.interceptors.response.use(res => {
// console.log(res);
return res.data
}, err => {
console.log(err);
})
使用拦截器之后记得要return,否则没有数据返回