Vue

Vue

简介

vue 渐进式JavaScript框架

课程介绍

  • 1 Vue基础知识
  • 2 Vue全家桶(vue/vue-router/vuex/axios)
  • 3 组件化开发
  • 4 webpack - 前端模块化打包构建工具
  • 5 ES6 - ECMAScript 6 入门
  • 6 Vue项目

介绍

  • vue 中文网
  • vue github
  • Vue.js (读音 /vjuː/,类似于 view) 是一套构建用户界面(UI)的渐进式JavaScript框架
  • 要求:通读一遍Vue官网教程中的基础内容

库和框架的区别

框架和库的区别

Library

库,本质上是一些函数的集合。每次调用函数,实现一个特定的功能,接着把控制权交给使用者

  • 代表:jQuery
  • jQuery这个库的核心:DOM操作,即:封装DOM操作,简化DOM操作

Framework

框架,是一套完整的解决方案,使用框架的时候,需要把你的代码放到框架合适的地方,框架会在合适的时机调用你的代码

  • 框架规定了自己的编程方式,是一套完整的解决方案
  • 使用框架的时候,由框架控制一切,我们只需要按照规则写代码

主要区别

  • You call Library, Framework calls you
  • 核心点:控制反转(谁起到主导作用)
    • 框架中控制整个流程的是框架
    • 使用库,由开发人员决定如何调用库中提供的方法(辅助)
  • 好莱坞原则:Don’t call us, we’ll call you.

MVVM

MVC

  • MVC是一种软件架构模式,也有人叫做设计模式
  • M: Model 数据模型(专门用来操作数据,数据的CRUD)
  • V:View 视图(对于前端来说,就是页面)
  • C:Controller 控制器(是视图和数据模型沟通的桥梁,用于处理业务逻辑)

MVVM

  • MVVM ===> M / V / VM
  • M:model数据模型
  • V:view视图
  • VM:ViewModel 视图模型

优势

  • MVC模式,将应用程序划分为三大部分,实现了职责分离
  • 但是,在前端中经常要通过JS代码来进行一些逻辑操作,最终还要把这些逻辑操作的结果展示在页面中。也就是需要频繁的操作DOM
  • MVVM通过数据双向绑定让数据自动地双向同步
    • V(修改视图) -> M
    • M(修改数据) -> V
  • 数据驱动视图的思想,数据是核心

Vue中的MVVM

  • 注意:不推荐直接手动操作DOM!!!

虽然没有完全遵循 MVVM 模型,Vue 的设计无疑受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的简称) 这个变量名表示 Vue 实例

学习Vue要转化思想

  • 数据驱动视图:不要在想着怎么操作DOM,而是想着如何操作数据!!!

vue起步

安装:

1
npm install vue

开发的时候使用未压缩版,有提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">{{ msg }}</div>

<!-- 引入 vue.js -->
<script src="vue.js"></script>

<!-- 使用 vue -->
<script>
var vm = new Vue({
// el:提供一个在页面上已存在的 DOM 元素作为Vue实例的挂载目标
el: '#app',
// Vue 实例的数据对象,用于给 View 提供数据
data: {
msg: 'Hello Vue'
}
})
</script>

构造函数new出来的实例,需要传递参数,参数是一个对象:

  • el :指定页面上的vue实例能够作用的边界,值是一个选择器;
  • $el :是vue管理的边界对应的DOM对象;
  • data :提供数据的对象;

vue实例

Vue是一个构造函数,所以需要new一个实例,构造函数的首字母大写。

需要的数据在实例的参数中,data对象提供数据;

1
2
3
4
5
var vm = new Vue({
data: {
msg: '大家好,...'
}
});

Vue 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<body>
<div id="app">
<htmllabel></htmllabel>
</div>
</body>

<script>
//创建组件
Vue.component("htmllabel", {
template: `
<div class="father">
<p>{{ msg }}</p>
<erzi :skill="liaomei" :money="money"></erzi>
</div>`,
data() {
return {
msg: "测试数据",
}
}
})

// 在Vue中通过 过滤器(filter) 来对数据格式化处理,全局过滤器器
Vue.filter('date', function (input) {
// 回调函数的参数(input):表示要格式化的数据
return input.getFullYear() + '-' + (input.getMonth() + 1)
})

// 自定义按键修饰符别名
Vue.config.keyCodes.f2 = 113

//new Vue 实例
const vm = new Vue({
// Vue 实例控制边界
el: "#app",
// 数据
data: {},
// methods配置项,用来指定vue中用到的方法
methods: {
btn () {}
},
// 局部过滤器
filters: {
date: function (input, value = 'YYYY-MM-DD HH:mm:ss') {
return moment(input).format(value)
}
},
// 重点钩子函数 - 渲染页面之前 使用场景:可以发送ajax请求获取数据
created () {},
// 重点钩子函数 - 渲染页面之后 可以操作DOM,也可以发送ajax请求
mounted () {},
// watch配置项用来监视 Vue 中数据的变化
watch: {},
// 通过该配置项来创建计算属性
computed: {}
});
</script>

数据

data数据使用注意

  • 先在data中声明,然后在使用数据;
  • 没有先声明的数据,动态添加的数据是非响应式的;
    • 在data中声明的数据,会被vue处理,遍历这个data数据对象,内部使用Object.defineProperty()方法处理之后,返回给new Vue的实例对象,所以实例对象可以直接使用this来调用data数据对象的属性;
    • 所以动态添加的新属性数据,没有被处理过,所以是非响应式的数据;

在某些特殊的时候,需要给vm实例动态添加新的属性和数据,并且希望是响应式的;

需要使用Vue提供的$set()方法来设置;

vm.$set(obj.user, 'name', 'jack')

  • 第一个参数:给哪一个对象添加属性
  • 第二个参数:添加的属性的名称,字符串
  • 第三个参数:添加的新的属性的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
msg: 'shuju',
user: {
age: 19
}
}

var vm = new Vue({
el: '#app',
data: obj
})

// 动态添加响应式数据
// 第一个参数表示:给哪个对象添加属性
// 第二个参数表示:添加属性的名称,字符串
// 第三个参数表示:默认值
vm.$set(obj.user, 'name', 'jack')

数据绑定

语法:&#123&#123 msg &#125&#125

Mustache语法,插值表达式;– 小胡子语法;

  • 解释:使用&#123&#123&#125&#125(Mustache)从data中获取数据,并展示在模板中;
  • 说明:数据对象的属性值发生了改变,插值处的内容都会更新;
  • 说明:&#123&#123&#125&#125中只能出现JavaScript表达式,不能出现语句,例如if语句for语句;
  • 注意:Mustache 语法不能作用在 HTML 元素的属性上;
1
2
3
4
5
6
<h1>Hello, {{ msg }}.</h1>
<p>{{ isOk ? 'yes': 'no' }}</p>
<p>{{ 1 + 2 }}</p>

<!-- !!!错误示范!!! -->
<h1 title="{{ err }}"></h1>

vue数据双向绑定

  • 双向数据绑定:将DOM与Vue实例的data数据绑定到一起,彼此之间相互影响
    • 数据的改变会引起DOM的改变
    • DOM的改变也会引起数据的变化
  • 原理:数据劫持,Object.defineProperty中的getset方法
    • gettersetter:访问器
    • 作用:指定读取或设置对象属性值的时候,执行的操作
  • 注意:Object.defineProperty方法是ES5中提供的,IE8浏览器不支持这个方法。因此,Vue支持IE8及其以下版本浏览器;
  • Vue - 深入响应式原理
  • MDN - Object.defineProperty()

补充说明:

  • Vue双向数据绑定的原理:Object.defineProperty();
  • 只有经过 Object.defineProperty() 处理后的数据,才会有get/set
  • Vue内部遍历了 data 中的数据,将其使用 Object.defineProperty() 来修改为 get/set 的形式
  • Object.defineProperty(vm, 'msg', {})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
语法
*/

var obj = {}
/*
参数1:监听的对象
参数2:需要给对象的那个属性设置值,每当给这个对象的属性设置值得时候会被监听和劫持
参数3:一个对象:`getter`和`setter`:访问器
*/
Object.defineProperty(obj, 'msg', {
// 设置 obj.msg 执行的操作
set: function () {},
// 读取 obj.msg 执行的操作
get: function () {}
})

Vue双向绑定的极简实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div id="app">
<input type="text" id="text">
<p id="p1"></p>
</div>

<script>
var text = document.getElementById('text')
var p1 = document.getElementById('p1')

var data = {}
var temp
Object.defineProperty(data, 'txt', {
// 1 修改数据的值,让文本框的值改变(Model ====> View)
set: function (value) {
text.value = value
p1.innerText = value
// 每次设置的值,由第三个变量来接收
temp = value
},

get: function () {
// 每次需要获取值得时候通过第三个变量接收到的值来传递
return temp
}
})

// 2 修改文本框的值,让数据也改变
// 监听文本框的输入事件
text.oninput = function () {
// console.log('文本框的值变了,当前文本框的值:', this.value)
data.txt = this.value
}
</script>

实现原理就是,给obj.txt设置值得时候会被Object.defineProperty()监听劫持,不会被设置到obj.txt属性;通过劫持到的数据设置给data,用来实现双向绑定;由于obj.txt不会被设置成功,所以一般用第三个变量来接收;

Vue中异步DOM更新的说明

在数据改变之后,vue并不是立即更新DOM,而是将更新延后了,在vue确认当前阶段数据不在变化之后,才会去更新DOM;(例如:点击事件改变了数据);

为了提高Vue的渲染性能,减少不必要的DOM更新;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const vm = new Vue({
el: '#app',

data: {
name: 'jack'
},

methods: {
fn () {
this.name = 'rose'

// $el 是Vue管理的边界对应的DOM对象
// console.log(this === vm)
console.log('获取h1中的文本内容:', this.$el.children[0].innerText)
// 打印的是数据修改之前的值,说明DOM更新是异步的;
}
}
});

需要获取DOM更新后的内容:

使用vm.$nextTick(() => {});在这个回调函数内部可以获取到DOM更新后的数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const vm = new Vue({
el: '#app',
data: {
msg: 'jack'
},

methods: {
fn () {
this.msg = 'rose'

// console.log('数据修改后立即获取DOM:', this.$el.children[0].innerText)
// 可以通过 $nextTick 来获取到更新后的DOM内容
// 说明:这个回调函数是在DOM更新后立即执行的,因此,可以获取到更新后的DOM内容
this.$nextTick(function () {
console.log('在nextTick中获取DOM:', this.$el.children[0].innerText)
})
}
}

vue数据更新特点

当动态数据发生改变的时候,页面内的所有指令和表达式都会重新计算一次;

  • 前提:必须是页面中存在的;
  • 事件除外

如果数据是复杂类型的,只是修改了复杂类型数据的属性,而不是修改了引用地址,是不会触发 DOM 更新的;

指令

指令:就是一种特殊的标记,用来给 HTML 元素添加一个特殊的行为(功能);

vue中的指令都是以 v-开头的;

v-model

v-model指令:

  1. 只能在表单元素中使用;
  2. 用来实现数据双向绑定;给表单元素添加了 v-model ,就能实现表单元素的值和数据双向绑定的功能;
    • 改变文本框的值,对应的数据txt会自动改变
    • 改变数据txt的值,对应的文本框的值也会自动改变
  3. v-momdel 在不同的表单元素对应的值也不一样;
1
<input type="text" v-model="txt">

v-model的第二种用法

  • v-model用于表单数据的双向绑定,其实它就是一个语法糖,这个背后就做了两个操作:
  1. 父组件向子组件绑定v-model属性;
  2. 子组件使用 props: ['value'] 接收, value 固定写法,用 value 来接收父组件传递过来的数据;
  3. 子组件内部通过调用 this.$emit("input", "在子组件中修改了绑定的数据") input 方法是固定的,通过调用这个方法来修改父组件的数据,达到同步效果;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 父组件的数据 -->
<template>
<div id="app">
<input type="text" v-model="msg">
<comp v-model="msg"></comp>
</div>
</template>

<!-- 子组件的数据 -->
<template>
<div> 组件 {{value}} <button @click="change">改变绑定的数据</button></div>
</template>

<script>
export default {
methods: {
change(){
this.$emit("input", "在子组件中修改了绑定的数据")
}
},
props: ['value']
}
</script>

v-text 和 v-html

v-text指令:

  • 作用相当于操作DOM的innerText,会覆盖原来的文本内容

v-html指令:

  • 作用相当于操作DOM的innerHtml,会覆盖原来的所有子元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="app">
<p v-text="msg">内容</p>
<div v-html="msg1">
<p>内容</p>
</div>
</div>

<script src="./vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: '确认过眼神',
msg1: '<h1>我遇上对的人</h1>'
}
})

v-on

v-on指令:

  • 作用:绑定事件
  • 语法:v-on:click="say" or v-on:click="say('参数', $event)"
  • 说明:绑定的事件从methods中获取
1
2
3
4
<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>

推荐使用简写方式;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<div id="app">
<h1>{{ msg }}</h1>
<!-- <button v-on:click="fn">点我</button> -->
<button @click="fn" @mouseenter="foo">点我</button>
</div>

<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: '聪聪'
},
methods: {
fn: function () {
this.msg = '玺哥'
},
foo: function () {
console.log('鼠标过来了')
}
}
});
</script>
</body>

methods

methods对象是专门用来设置v-on事件的执行函数;

注意:在Vue构造函数new出来的实例中,methods对象内的事件执行函数内部可以直接使用this,this指向当前new出来的实例对象,可以通过this直接调用实例对象内部的参数;

事件修饰符

.prevent 阻止默认行为,调用 event.preventDefault()

1
<button type="submit" class="btn btn-default" @click.prevent="add">添加</button>

v-bind

v-bind指令:

  • 作用:专门用来给标签设置属性的指令,用来给标签属性绑定动态数据;
  • 作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM;
  • 语法:v-bind:title="msg"
  • 简写::title="msg"
1
2
3
4
<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>

样式操作

v-bind:class="{}" or v-bind:class="{}"

  • 作用:专门用来操作样式的指令,添加类名或者行内样式;主要操作类名;
  • 样式生效和css一致;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 1 -->
<!-- 重点 -->
<!-- 类名的值为true时添加这个类,为false时移除这个类 -->
<div v-bind:class="{ active: true }"></div> ===> <div class="active"></div>

<!-- 2 -->
<!-- 可以是一个数组,多个类 -->
<div :class="['active', 'text-danger']"></div> ===> <div class="active text-danger"></div>

<!-- 3 -->
<!-- 一个数组,可以混写,errorClass表示存在Vue实例对象的data数据内的动态数据 -->
<div v-bind:class="[{ active: true }, errorClass]"></div> ===> <div class="active text-danger"></div>


--- style ---
<!-- 1 -->
<div v-bind:style="{ color: activeColor, 'font-size': fontSize + 'px' }"></div>
<!-- 2 将多个 样式对象 应用到一个元素上-->
<!-- baseStyles 和 overridingStyles 都是对象 -->
<div v-bind:style="[baseStyles, overridingStyles]"></div>

v-for

v-for指令:

  • 根据数组数据多次渲染页面元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<div id="app">
<!--
将 list 中的每项数据,生产一个p元素
item in list 中:
item 表示list数组中的每一项
list 表示 data 中list数据
index 表示下标,可以直接使用,需要加上小括号
-->
<!-- <p v-for="item in list">ID:{{ item.id }} , name:{{ item.name }}</p> -->

// 加上下标的写法
<p v-for="(item, index) in list">
索引号为:{{ index }},
ID:{{ item.id }} ,
name:{{ item.name }}
</p>
</div>
<script src="./vue.js"></script>
<script>
// 需求:将列表数据展示在页面中
const vm = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: '吃饭' },
{ id: 2, name: '睡觉' },
{ id: 3, name: '打豆豆' }
]
}
})
</script>
</body>

key属性

vue中为了提高DOM渲染的性能,默认采用了“就地复用”策略;

数据改变的时候,vue会对比数据修改前后的结构,发现元素结构相同时,优先复用元素,

在没有key属性的时候,按照新的数据复用元素,会按照新的数据依次修改元素的内容(元素机构相同,只修改内容);如果新的数据顺序被改变,则会依次修改;

如果有key属性,vue通过辨识key属性来确定带有key属性的元素直接使用,根据新的数据来确认修改、新建还是删除元素;所以会极大减少对DOM的操作,以提升性能;不管数据顺序有没有改变,保留带有key属性的元素,对照新的数据来确定是否更改;

下标是数组自动按照内容产生的,所以是会变动的;

在某些用到临时状态(复选框、文本框输入的内容)的表单元素中,如果当前勾选了复选框,在原数据前插入了新的数据,自动更新了页面后,选中的状态给加到新增加的数据上,此时需要一个key属性来确定原来的状态;

key属性是唯一的,和数据是一一对应的,不能变化的,比如id,不能选择下标来作为key属性,因为下标会在数组中自动重排,所以下标和数据不能对应了;会把新增的属性当成原来的属性;

所以在使用时最佳建议:添加key属性,并且用id来对应数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<body>
<div id="app">
<button @click="insertData">插入数据</button>

<!--
给 v-for 指令,添加一个 key 属性,key属性的之为:每一项的id
注意:key的值是不能重复的!!!

注意:如果使用 index 作为key值,也会有bug
如果没有 临时状态(比如:复选中选中,文本框中输入值) 是可以使用 index 作为key

总结:有id,就是用id作为key值;如果没有,就是用 index 作为key值
-->
<p v-for="(item, index) in list" :key="item.id">
<!-- <p v-for="(item, index) in list" :key="index"> -->
<input type="checkbox">
<span>{{ index }}</span>
<span>{{ item.name }}</span>
</p>
</div>
<script src="./vue.js"></script>
<script>
// 最佳实践:在使用 v-for 指令的时候,添加 key 属性!!!

// 需求:点击按钮,往 list 数据的第一条前面插入一条新的数据

const vm = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: '吃饭' },
{ id: 2, name: '睡觉' },
{ id: 3, name: '打豆豆' }
]
},

methods: {
insertData () {
this.list.unshift({
id: 4,
name: '聪聪老司机'
})
}
}
})
</script>
</body>

v-cloak

v-cloak指令:

  • 作用:用来解决&#123&#123&#125&#125==> 编译后的数据 页面闪烁的问题;
  • 语法:在标签内存在这个指令,没有值;<h1 v-cloak>&#123&#123 msg &#125&#125</h1>
  • 在vue还没有渲染页面的时候,默认为这个元素增加行内样式display: none,在页面上隐藏;vue处理数据后渲染页面时候移除这个行内样式,重新显示在页面上;

vue处理数据渲染页面的时候有一定的延时,JavaScript在页面渲染后才执行vue语句,所以&#123&#123 txt &#125&#125会被原文输出;vue处理完数据渲染页面后显示处理后的内容,页面会闪烁一下;

解决方法:

  1. 在标签内使用v-text代替标签内&#123&#123 &#125&#125
  2. 使用v-cloak

如果只是几个标签的数据,使用v-text

如果有大量的标签需要使用vue来渲染页面,直接给父元素添加v-cloak

v-if 和 v-show

v-if指令:

  • 写在标签上:值为true时显示此标签,值为false时隐藏此标签;
  • 如果隐藏时,这个标签会从页面上删除;
  • 切换显示/隐藏时,创建/删除这个标签;浪费性能;
  • 只需要切换一次状态,例如:许愿墙删除某个留言;

v-show指令:

  • 写在标签上:值为true时显示此标签,值为false时隐藏此标签;
  • 如果隐藏时,这个标签会添加行内样式 display: none;,在页面上隐藏;
  • 切换显示/隐藏时,通过css来操作这个标签;性能高,不需要创建和删除;
  • 需要频繁的切换元素显示/隐藏时,使用v-show
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<h1 v-if="isShow">您能看到我</h1>
<h1 v-show="isShow">您能看到我</h1>
<script>
/*
v-if 和 v-show 都用来控制元素的展示和隐藏

如果值为true:表示展示元素
如果值为false:表示隐藏元素

区别:
v-if 如果隐藏元素,那么该标签会从HTML中移除
切换展示和隐藏的时候,会创建或移除元素
v-show 通过 display: none; 来隐藏元素
通过 CSS 控制元素展示和隐藏,性能高

如何选择使用哪个指令来控制展示和隐藏?
如果需要频繁的展示和隐藏元素,此时,应该使用 v-show 来控制
如果一个元素要么展示要么隐藏,此时,应该使用 v-if 来控制
*/

const vm = new Vue({
el: '#app',
data: {
isShow: false
}
})
</script>

v-else-if 和 v-else

分支语句,用来根据条件判断来显示哪一个标签;

v-else限制:前一兄弟元素必须有 v-ifv-else-if

v-else-if限制:前一兄弟元素必须有 v-ifv-else-if

1
2
3
4
5
6
7
8
9
10
11
12
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>

v-once 和 v-pre

v-once指令:

  • vue只会解析处理带有v-once指令的标签一次,后续数据更新也不会再次来解析处理这个标签的内容;

v-pre指令:

  • vue不会解析带有v-pre指令的元素,一次都不会,直接忽略掉;
  • 一般应用于没有表达式和指令的元素,例如:内容是大量的文章内容,会提升Vue渲染整个页面的性能;

过滤器

过滤器的作用:数据格式化(数据按照某个格式输出展示)

在Vue中使用filter()方法来格式化数据输出格式;

  • 第一个参数:表示过滤器的名称
  • 第二个参数:是一个回调函数,当使用该过滤器的时候,这个回调函数就会执行。格式化数据的代码逻辑就放在这个回调函数中;
  • 回调函数的第一个参数:需要格式化的数据,在标签内使用过滤器时,自动把数据传递到回调函数中;
  • 通过回调函数的返回值来决定格式化后要展示的内容;

|也被称为:管道符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<h1>{{ time | date }}</h1>
<script>
Vue.filter('date', function (input) {
// 回调函数的参数(input):表示要格式化的数据
return input.getFullYear() + '-' + (input.getMonth() + 1)
})

const vm = new Vue({
el: '#app',
data: {
time: new Date()
}
})
</script>

回调函数内可以传递多个参数;

  • 在元素内第一个参数不需要写,默认自动传递,只需要在回调函数内使用形参来接收就可以;
  • 在ES6中形参默认值可以直接在小括号内设置,val1 = 'YYYY-MM-DD HH:mm:ss':如果传递了就使用传递的参数,如果没有传递就是用”=”后面的默认参数;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<p>{{ time | date('YYYY-MM-DD', 123) }}</p>

<script>
Vue.filter('date', function (input, val1 = 'YYYY-MM-DD HH:mm:ss', val2) {
console.log(val1)
return moment(input).format(val1)
})

const vm = new Vue({
el: '#app',
data: {
time: new Date()
}
})
</script>

局部过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = new Vue({
el: '#app',
data: {
time: new Date()
},

// 创建局部过滤器:
filters: {
date: function (input, value = 'YYYY-MM-DD HH:mm:ss') {
return moment(input).format(value)
}
}
})

过滤器可以直接在new Vue()实例对象的参数内部,表示是这个实例对象的过滤器;如果有多个实例对象,建议在全局使用;

一定要在vm实例对象之前使用,注意代码的执行顺序;

日期格式化 - 第三方包

安装:

1
npm i moment

使用:

  • 通过<script src="./moment.js">来引入;
  • 直接使用moment().format()就可以了;
    • moment() 函数的参数为:要格式化的日期对象;
    • format() 对日期进行格式化,参数表示:格式
1
2
3
4
Vue.filter('date', function (input, val1 = 'YYYY-MM-DD HH:mm:ss', val2) {
console.log(val1)
return moment(input).format(val1)
})

Vue生命周期

  • 所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象即可 (一些根实例特有的选项除外)。
  • 实例生命周期也叫做:组件生命周期
  1. 挂载阶段
  2. 更新阶段
  3. 卸载极端

生命周期介绍

  • vue生命周期钩子函数
  • 简单说:一个组件从开始到最后消亡所经历的各种状态,就是一个组件的生命周期

生命周期钩子函数的定义:从组件被创建,到组件挂载到页面上运行,再到页面关闭组件被卸载,这三个阶段总是伴随着组件各种各样的事件,这些事件,统称为组件的生命周期函数!

  • 注意:Vue在执行过程中会自动调用生命周期钩子函数,我们只需要提供这些钩子函数即可
  • 注意:钩子函数的名称都是Vue中规定好的!

框架和库的区别

钩子函数

钩子函数 - beforeCreate()

  • 说明:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用
  • 注意:此时,无法获取 data中的数据、methods中的方法

钩子函数 - created()

钩子函数 - beforeMounted()

  • 说明:在挂载开始之前被调用

钩子函数 - mounted()

  • 说明:此时,vue实例已经挂载到页面中,可以获取到el中的DOM元素,进行DOM操作

钩子函数 - beforeUpdated()

  • 说明:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • 注意:此处获取的数据是更新后的数据,但是获取页面中的DOM元素是更新之前的

钩子函数 - updated()

  • 说明:组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。

钩子函数 - beforeDestroy()

  • 说明:实例销毁之前调用。在这一步,实例仍然完全可用。
  • 使用场景:实例销毁之前,执行清理任务,比如:清除定时器等

钩子函数 - destroyed()

  • 说明:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

axios

  • 以Promise为基础的HTTP客户端,适用于:浏览器和node.js
  • 封装ajax,用来发送请求,异步获取数据;

安装:

1
npm install axios

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
// 发送请求:
axios
.get('请求接口地址')
// 通过 then 方法来获取到接口返回的结果
.then(function (res) {
// 参数res:表示服务器返回的数据,这个数据是经过axios处理后的数据
console.log(res)
})

// 发送post
axios
.post('接口地址')
.then((res) => {
console.log(res)
})
</script>

通过.then((res) => {})中的回调函数来获取返回的数据,res来接收返回的数据,这个数据是axios已经处理好的;

json-server

json-server 包的作用:

  • 能够根据一个json文件,快速生成针对于这个json文件的 增删改查 接口
  • 使用这个工具,可以很方便的作为接口来测试功能

场景:我们写了一个demo,但是,不会写服务端代码,这个时候,只需要通过json-server就可以生成所有 CRUD 的接口了

使用:

  1. 全局安装 npm i -g json-server
  2. 创建一个json文件
  3. 在json文件所在目录中打开终端(命令工具)
  4. 指定命令: json-server ./brand.json

并且会设置好跨域的问题;

获取数据:axios.get(url).then((res) => {})url:地址

添加数据:axios.post(url, data).then((res) => {})data:添加的数据

删除数据:axios.delete(url + id).then((res) => {})id:需要删除的数据对应id

按键修饰符

键盘事件:keydown keypress keyup

在监听键盘事件时,我们经常需要检查常见的键值。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:

1
2
<!-- 只有在 `keyCode` 是 13 时调用 `vm.submit()` -->
<input v-on:keyup.13="submit">

记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:

全部的按键别名:

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
1
2
3
4
5
<!-- 同上 -->
<input v-on:keyup.enter="submit">

<!-- 缩写语法 -->
<input @keyup.enter="submit">

可以通过全局 config.keyCodes 对象自定义按键修饰符别名:

1
2
// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112

在某些情况下,按键修饰符不会生效,需要使用 .native 使事件生效;

例如

1
<input v-on:keyup.enter.native="submit">

监视数据

在 Vue 中提供了监听数据变化的方法,可以通过 watch:{} 对象来设置对那些数据进行变化监听;

为需要监听的数据设置方法,每当数据发生变化时就会触发这个方法(函数);

对简单类型数据和复杂类型数据监听;

  • 简单类型数据监听可以看成是复杂类型数据监听方法的简写;
  • 简单数据类型也可以使用复杂类型数据监听方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<div id="app">
<input type="text" v-model="msg">
<input type="text" v-model="user.name">
</div>
<script>
// 监视数据变化:
// watch 当数据变化的时候,通过watch就可以监视到,然后,就可以进行响应的处理

const vm = new Vue({
el: '#app',
data: {
msg: '',
user: {
name: ''
}
},
watch: {
/*
监视基本类型的变化:
参数1:新的数据
参数2:旧的数据
msg (curVal, oldVal) {},
*/
msg: {
// 数据变化后,就可以执行这个方法中的代码
handler (curVal) {
console.log('监视到数据变化了', curVal)
},
// 立即监视数据变化,页面一加载就会执行一次监听;
immediate: true
},
// 监视引用类型的变化:
user: {
// 深度监听,复杂数据类型,需要设置这个属性
deep: true,
// 数据变化后,就可以执行这个方法中的代码
handler (curVal, oldVal) {
// 第一次执行:oldVal还没有数据,因此 第一次 两个参数的值不相同
// 以后的每一次,curVal 和 oldVal 指向了同一个对象,因此是相同的
console.log('监视user对象变化', curVal, curVal === oldVal)
},
// 立即监视数据变化
immediate: true
}
}
})
</script>

计算属性

  • Vue 处理页面,当动态数据发生改变的时候,页面内的所有指令和表达式都会重新计算一次;在某些情况下,当其中一部分数据发生改变时,也会引起另一部分数据变化,所以某些使用动态数据渲染的页面会频繁更新,会带来极大的性能问题;

使用计算属性 computed:{} 来解决这一问题:

在模板中不直接使用a数据,使用计算属性内c数据来渲染模板,c数据依赖a数据,只有当a数据发生改变c数据才会重新计算;

计算属性采用的是缓存机制,计算后的数据存在缓存中,可以直接多次调用,而不用重复计算;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<div id="app">
<h1>计算属性C:{{ c }}</h1>
<h1>计算属性C:{{ c }}</h1>
<h1>计算属性C:{{ c }}</h1>
<h1>计算属性C:{{ c }}</h1>
<h1>计算属性C:{{ c }}</h1>

<div>
A:<input type="text" v-model="a">
</div>
<div>
B:<input type="text" v-model="b">
</div>
</div>
<script src="./vue.js"></script>
<script>
// 计算属性:
// 特点:当计算属性的依赖的数据改变的时候,计算属性才会被重新计算一次
// 如果不是计算属性的依赖数据改变了,那么 计算属性 不会重新计算

// 特点:使用缓存(cache)机制来提升计算属性的性能
// 如果在页面中使用计算属性多次,计算属性的函数也只会被调用一次,以后直接从缓存中取出来使用

// 注意点:计算属性不能与data中的属性名重名!否则,会报错!!!

// 比如:有 A 和 B 两个数据,有一个计算属性C,C依赖于A
// 当A改变,C会被重新计算
// 当B改变,C不会重新计算

const vm = new Vue({
el: '#app',
data: {
a: '',
b: '',

// c: ''
},

// 通过该配置项来创建计算属性:
computed: {
// 在 计算属性 中使用 this.a ,就表示 该计算属性 依赖于 数据a
c: function () {
console.log('计算属性C的代码执行了')
// 可以根据条件对 this.a 进行过滤,返回需要的数据
return '---- ' + this.a + ' ----'
}
}
})
</script>
  • 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。
1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都应当使用计算属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
<script>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
</script>

组件化开发

因为组件是可复用的 Vue 实例,所以它们与 new Vue 接收相同的选项,例如 data、computed、watch、methods 以及生命周期钩子等。仅有的例外是像 el 这样根实例特有的选项。

你可以将组件进行任意次数的复用;并且维护方便;

全局组件

全局组件,可以任意使用,嵌套关系之间没有限制;

使用 Vue.component() 来创建组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div id="app">
<!-- 使用组件,可以多次复用 -->
<hello></hello>
<hello></hello>
<hello></hello>
<hello></hello>
</div>
<script>
// 创建组件:
// 第一个参数:表示组件名称
// 第二个参数:是一个对象,用来配置组件
Vue.component('hello', {
// `` 反引号(键盘ESC下面的键),是ES6提供的一个新的字符串语法:字符串模板
template: `
<div>
<h1>这是我的第一个组件 -- 通过字符串模板来创建的</h1>
<p>这是一段文本</p>
</div>
`
})

// 需要一个 Vue 实例对象才会处理数据渲染页面
const vm = new Vue({
el: '#app',
data: {}
})
</script>

注意:template 配置模板,HTML结构只能有一个根元素;不能有多个根元素否则会报错;

组件的数据和其他配置项

Vue 数据

定义一个组件,内部的数据 data 不是一个对象而是一个函数,通过这个函数内部 return 一个对象来使用数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component('hello', {
// `` 反引号(键盘ESC下面的键),是ES6提供的一个新的字符串语法:字符串模板
template: `
<div>
<h1>这是我的第一个组件 -- 通过字符串模板来创建的</h1>
<p>这是一段文本</p>
</div>
`,
data () {
return {
msg : "测试数据"
}
}
})

组件的通讯

每一个组件都是一个封闭的个体,不能直接使用外部的数据;需要使用 组件通讯 策略;

注意:HTML标签内部不支持大写,在使用驼峰命名法的饿时候一定要注意;

父级组件向子组件传递数据

  • 父级组件内准备需要向子组件传递的数据,data () &#123return &#123parentMsg: '撩妹',money: 100000000000000&#125&#125
  • 在子组件的标签上设置属性 <child v-bind:skill="parentMsg" :money="money"></child>
  • 子组件中通过 props: ['skill', 'money'] 来接收父级组件传递来的数据
  • 子组件中就可以使用这些数据了;
  • props 接收的数据是直读属性,不支持修改,修改会报错;动态传递的,父级数据修改后,会自动同步;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<div id="app">
<parent></parent>
</div>
<script src="./vue.js"></script>
<script>
// 在父组件的模板中,使用子组件,这样,就会形成父子组件之间的嵌套关系了

// 创建父组件:
Vue.component('parent', {
template: `
<div class="parent">
<h1>这是父组件</h1>

<child :skill="parentMsg" :money="money"></child>
</div>
`,
data () {
return {
parentMsg: '撩妹',
money: 100000000000000
}
}
})

// 创建子组件:
Vue.component('child', {
template: `
<div class="child">
<h2>这是子组件,接受到父亲传授技能:{{ skill }}</h2>
<p>接受到父亲的财产:{{ money }}</p>
</div>
`,
// 接受父组件中传递过来的数据
props: ['skill', 'money']
})

const vm = new Vue({
el: '#app',
data: {}
})
</script>

子组件向父级组件传递数据

  • 父级组件提供一个方法
  • 在子组件标签上设置自定义事件
  • 子组件调用这个方法,将需要的传递的数据作为参数传递
  • 父级组件通过这个方法接收到子组件传递过来的数据,设置给动态数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<div id="app">
<parent></parent>
</div>
<script src="./vue.js"></script>
<script>
// 创建父组件:
Vue.component('parent', {
template: `
<div class="parent">
<h1>这是父组件 -- 技能:{{ skill }}</h1>

<child @fn="getSkill"></child>
</div>
`,

data () {
return {
skill: ''
}
},

methods: {
// a 父组件中提供一个方法
getSkill (skill) {
// 参数skill就是子组件中传递过来的数据
console.log('父亲接受到儿子的技能:', skill)
// 设置给动态数据
this.skill = skill
}
}
})

// 创建子组件:
Vue.component('child', {
template: `
<div class="child">
<h2>这是子组件</h2>
<button @click="teachSkill">教父亲技能</button>
</div>
`,

// 如果想要进入页面就传递数据,就可以在 created 钩子函数中来触发 fn
created () {
this.$emit('fn', '进入页面自动传递:撩汉子')
},

methods: {
teachSkill () {
// 调用父组件中传递过来的方法,将数据作为参数传递
// 第一个参数:表示要调用的方法名称
// 第二个参数:表示传递父组件的数据
this.$emit('fn', '撩汉子')
}
}
})

const vm = new Vue({
el: '#app',
data: {

}
})
</script>

非父子组件数据传输

一种通用的组件通讯,用于组件之间的数据传递,可以是父子组件(嵌套),也可以是兄弟,也可以是多层级之间的组件通讯;

  • 创建一个空的 Vue 实例,const bus = new Vue() 专业名称:事件总线;
  • 需要传递数据的两个组件通过在 bus(事件总线) 上注册方法 / 调用方法来传递数据;
  • 需要接收数据的A组件在 bus(事件总线)注册一个方法;
  • 传递数据的B组件通过调用A组件在 bus(事件总线) 注册的方法,将数据作为参数传递过去;
  • A组件就可以通过B组件调用这个方法获得数据了;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<body>
<div id="app">
<jack></jack>
<rose></rose>
</div>

</body>
<script>
const bus = new Vue()

Vue.component("jack", {
template: `
<div class="jack">
<h2>这是jack组件:</h2>
<input type="text" v-model="info">
<button @click="fn">告诉rose</button>
</div>
`,
data() {
return {
info: ""
}
},
methods: {
fn() {
bus.$emit("love", this.info)
}
}
})

Vue.component("rose", {
template: `
<div class="rose">
<h2>这是rose组件:</h2>
<p>接受到jack的表白:{{ msg }}</p>
</div>
`,
data() {
return {
msg: ""
}
},
created() {
bus.$on("love", (data) => {
this.msg = data
})
}
})

const vm = new Vue({
el: "#app",
})
</script>

局部组件

局部组件是在一个 Vue 实例内部的组件,只有父子级关系才能使用嵌套关系,否则会报错;只有父子级关系才能是嵌套(不能跨代,不能同级)。

Vue 实例内部和组件内部都可以创建局部组件,使用限制:局部组件只能在内部被使用;

组件是独立的个体,不能是外部数据,嵌套关系也不能使用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<div id="app">
<node1></node1>
<node12></node12>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: "测试数据"
},
components: {
node1: {
template: `
<div>
<h1>son</h1>
<node2></node2>
</div>
`,
components: {
node2: {
template: `
<h2>sun</h2>
`
}
}
},
node12: {
template: `
<div>
<h1>son2</h1>
</div>
`
}
}
})
</script>

slot 内容分发插槽

<slot></slot> 元素作为组件模板之中的内容分发插槽。<slot></slot> 元素自身将被替换。

在页面中使用组件时,组件标签内部的内容不会被识别(元素或者字符串);HTML标签内的内容替换掉这个标签 <slot></slot>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<div id="app">
<node1>
这里的字符串可以替换
<h1>标签也能生效</h1>
</node1>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
Vue.component("node1", {
template: `
<div>
<p>{{ msg }}</p>
<slot></slot>
<p>{{ info }}</p>
<p>{{ data }}</p>
</div>
`,
data() {
return {
msg: "测试数据",
info: "测试数据2",
data: "测试数据3"
}
}
})

const vm = new Vue({
el: '#app',
data: {}
})
</script>

具名插槽

  • 模板中根据name属性对应slot标签,进行内容替换;
  • 不具名的slot对应没有name属性的节点;没有slot则被忽略掉,没有节点内容则不解析;
  • 与HTML标签内的顺序无关,只和组件中模板的顺序有关;
  • 不要混写具名和不具名的内容节点;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<body>
<div id="app">
<node1>
<p slot="p1">这里对应的具名slot,name="p1"</p>
<div>
这里对应不具名的的slot
<h1>标签也能生效</h1>
</div>
<p slot="p2">这里对应的具名slot,name="p2"</p>
</node1>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script>
Vue.component("node1", {
template: `
<div>
<slot name="p1"></slot>
<slot></slot>
<slot name="p2"></slot>
</div>
`
})

const vm = new Vue({
el: '#app',
data: {}
})
</script>

SPA -单页应用程序

  • 单页Web应用(Single Page Application,SPA),就是只有一个Web页面的应用,是加载单个HTML页面,并在用户与应用程序交互时动态更新该页面的Web应用程序。
  • 对于传统的多页面应用程序来说, 每次请求服务器返回的都是一个完整的页面;
  • 对于单页应用程序来说, 只有第一次会加载页面, 以后的每次请求, 仅仅是获取必要的数据.然后, 由页面中js解析获取的数据, 展示在页面中

优势:

  1. 减少了请求体积,加快页面响应速度,降低了对服务器的压力
  2. 更好的用户体验,让用户在web app感受native app的流畅

主要技术点

  1. ajax 请求数据
  2. 哈希值(锚点)的使用(window.location.hash #)
    • 哈希值 URL中#后的数据值;通过 location.hash 可以获取到带#的哈希值;
  3. hashchange 事件
    • 当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号)

实现思路

  • a标签的锚点跳转
  • 监听锚点值变化的事件,根据不同的锚点值,请求相应的数据
  • 1 锚点(#)原本用作页面内部进行跳转,定位并展示相应的内容
  • 2 SPA中,锚点被用作请求不同资源的标识,请求数据并展示内容
1
2
3
http-server 包的使用:
1 全局安装 npm i -g http-server
2 需要在哪个文件夹中开启服务器,就在哪个目录中执行: http-server 即可

路由

路由即:浏览器URL中的哈希值(# hash)与展示视图内容(template)之间的对应规则

vue中的路由是:hash 和 component的对应关系,一个哈希值对应一个组件

  • 在 Web app 中,通过一个页面来展示和管理整个应用的功能。
  • SPA往往是功能复杂的应用,为了有效管理所有视图内容,前端路由 应运而生!
  • 简单来说,路由就是一套映射规则(一对一的对应规则),由开发人员制定规则。
  • 当URL中的哈希值(# hash)发生改变后,路由会根据制定好的规则,展示对应的视图内容

Vue Router 基本使用

Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。

包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

使用步骤:

  1. 引入 Vue.js 和 vue-router.js 文件
  2. 创建路由规则(Vue 内部根据 URL 中哈希值来处理)
    • routes: [] 用来配置路由规则
    • path 是路由规则,用来与浏览器地址栏中的哈希值进行匹配
    • component 用来值该规则匹配后,要展示到页面中的组件内容
    • 默认的路由为 /,所以,进入页面就匹配该路由规则
    • redirect 跳转到对应的哈希值,相当于重定向
    • 创建路由规则对应的组件
  3. 将 路由 关联到 Vue 的实例
  4. 指定路由出口,在HTML页面中的位置
    • 在HTML页面中使用 <router-view></router-view> 指定路由出口的位置
  5. 指定路由入口,相当于导航菜单
    • 在HTML页面中使用 <router-link to="home">黄河</router-link> 指定路由入口
    • to="home" 会自动为在当前URL后再次添加此哈希值,并跳转到锚点;不删除原来的值,如果当前本页面哈希值末尾部分与本次哈希值相同则不再添加本次哈希值
    • to="/home" 会把当前URL地址#后的哈希值都删除掉,重新添加此哈希值,并跳转到锚点;会删除原来的值
    • 内部处理了slot,页面中可以识别标签内部的节点内容

注意:添加哈希值时带 / (会删除掉原来#后面的值)和不带 / (不会删除原来#后面的值,在原来的值后面添加这个哈希值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<body>
<div id="app">
<!-- 5. 指定路由入口,相当于导航菜单 -->
<router-link to="home">黄河</router-link>
<router-link to="find">长江</router-link>

<!-- 4. 指定路由出口,在HTML页面中的位置 -->
<router-view></router-view>
</div>
<!-- 1. 引入 Vue.js 和 vue-router.js 文件 -->
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vue-router/dist/vue-router.js"></script>
<script>
// 创建路由规则对应的组件
const home = {
template: `
<h1>长江长江这里是黄河,首页</h1>
`
}

const find = {
template: `
<h1>黄河黄河这里是长江,寻找</h1>
`
}

// 2. 创建路由
const router = new VueRouter({
// 用来配置路由规则
// path 是路由规则,用来与浏览器地址栏中的哈希值进行匹配
// component 用来值该规则匹配后,要展示到页面中的组件内容
routes: [
{
path: "/",
redirect: "/home"
},
{
path: "/home",
component: home
},
{
path: "/find",
component: find
}
]
})

const vm = new Vue({
el: '#app',
data: {

},
// 3. 将 路由 关联到 Vue 的实例
router: router
})
</script>
</body>

Vue 路由菜单的两个类

Vue路由的入口,也就是在页面中显示的导航菜单,Vue 内部添加了两个类,这两个类会在点击了对应的菜单的导航上添加,其余的菜单会被移除;

可以通过这两个类在css中添加被选中后的样式;

1
2
3
4
/* 精确匹配的类名: */
.router-link-exact-active {}
/* 模糊匹配的类名: */
.router-link-active {}

如果路由规则中设置的是 path: "/" 将会被 .router-link-active 模糊匹配到,也会被添加这个类,如果这个类设置了样式,这个导航就会和选中的效果一样;

可以在这个路由入口上添加 exact 属性,就会被精确匹配;

1
<router-link to="/" exact>首页</router-link>

Vue 嵌套路由

可以在路由中嵌套子路由,当我们需要在多层路由的时候,可以将路由添加在子组件的模板中;

子级路由配置,要写在父级路由的配置对象中;

嵌套路由中,如果子路由规则匹配项没有带 / 则表示相对路径会在匹配项中会默认把父路由哈希值自动拼在前面,带 / 则表示绝对路径,就匹配以子路由的绝对路径哈希值;在路由入口中,设置跳转锚点哈希值时一定要注意与匹配规则对应;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<body>
<div id="app">
<router-link to="/user">用户管理</router-link>
<!-- 路由出口 -->
<router-view></router-view>
</div>
<script src="./vue.js"></script>
<script src="./node_modules/vue-router/dist/vue-router.js"></script>
<script>
// 父组件
const User = {
template: `
<div class="user">
<h2>用户中心</h2>
<router-link to="/user/profile">个人资料</router-link>
<router-link to="/posts">岗位</router-link>

<!-- 子路由展示在此处 -->
<router-view></router-view>
</div>
`
}

// 子组件:
const UserProfile = {
template: '<h3>个人资料:张三</h3>'
}
const UserPosts = {
template: '<h3>岗位:FE(frontend)</h3>'
}

// 配置路由规则:
const router = new VueRouter({
routes: [
{
path: '/user',
component: User,

// 该配置项表示子路由:
children: [
// path中没有以/开头,相对路径,拼上父级哈希值,那么匹配的哈希值为:/user/profile
{ path: 'profile', component: UserProfile },
// path中以/开头,绝对路径,与父级哈希值无关,那么匹配的哈希值为:/posts
{ path: '/posts', component: UserPosts },
]
},
]
})

const vm = new Vue({
el: '#app',
router
})
</script>
</body>

Vue 路由参数

如果类似新闻页面,多个新闻内容不同,页面结构相同,可以通过路由参数来设置路由规则;

设置了路由参数的规则,是动态路由,可以匹配所有符合这个规则但是参数不同的哈希值;

1
2
3
4
5
6
const router = new VueRouter({
routes: [
{ path: '/newslist', component: News },
{ path: '/newsinfo/:page', component: NewsInfo }
]
})
  • /newsinfo/:page 路由参数 page 可以自定义;
  • 能够匹配的哈希值为:
    • /newsinfo/01 /newsinfo/02 /newsinfo/03
    • /newsinfo/a /newsinfo/xiaoming
  • 但是无法匹配以下形式的哈希值:
    • /newsinfo/ /newsinfo
    • /newsinfo/01/abc /newsinfo1

/newsinfo/:page? ? 表示路由参数可选,可以有路由参数也可以没有路由参数;

  • 能够匹配的哈希值为:
    • /newsinfo/ /newsinfo
    • /newsinfo/01 /newsinfo/02 /newsinfo/03
    • /newsinfo/a /newsinfo/xiaoming

this.$route 可以获取到路由参数

可以通过 this.$route.params.page 来获取到这个动态的路由参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在钩子函数内获取这个值的时候,如果在本页面改变了动态路由参数,Vue 会复用结构,这个钩子函数不会被执行
created () {
console.log('获取到路由参数为:', this.$route.params.page)
}

// 可以使用 watch 来监视这个动态路由参数
watch: {
$route: function (to, from) {
// from:从哪个路由跳转过来
// to: 要跳转到哪个路由去
console.log('监听到路由变化了:', to.params.page)
// 只要路由参数发生改变,就可以在这拿到当前的路由参数,做出相应的处理
}
}

Webpack

webpack 官网

bundle [ˈbʌndl] 捆绑,收集,归拢,把…塞入

  1. webpack 将带有依赖项的各个模块打包处理后,变成了独立的浏览器能够识别的文件
  2. webpack 合并以及解析带有依赖项的模块

概述

  • webpack的两个特点:1 模块化 2 打包

webpack 是一个现代 JavaScript 应用程序的模块打包器(module bundler)
webpack 是一个模块化方案(预编译)
webpack 获取具有依赖关系的模块,并生成表示这些模块的静态资源

  • 四个核心概念:入口(entry)输出(output)加载器loader插件(plugins)
  • webpack 与 模块化
1
2
3
4
5
6
模块化方案: webpack 和 requirejs(通过编写代码的方式将前端的功能,划分成独立的模块)

browserify 是与 webpack 相似的模块化打包工具

webpack 预编译 (在开发阶段通过webpack进行模块化处理, 最终项目上线, 就不在依赖于 webpack)
requirejs 线上的编译( 代码运行是需要依赖与 requirejs 的 )
  • 一般项目上线的时候,会对项目进行打包
  • 打包做什么??? js压缩、css压缩、less转css、HTML压缩 等
  • 比如 .vue(Vue中的单文件组件) 需要经过webpack打包处理后,才能在浏览器中运行
  • 比如 使用的ES6语法,浏览器不支持,但是,经过webpack打包处理后,浏览器就认识了
  • 比如 修改代码后,浏览器自动刷新,也是webpack提供的功能+
  • webpack 是基于 Node 的
  • 但是,node.js中的代码几乎没有,用到: node模块化

webpack起源

  • webpack解决了现存模块打包器的两个痛点:
    • 1 Code Spliting - 代码分离
    • 2 静态资源的模块化处理方案

webpack与模块

  • 前端模块系统的演进
  • 在webpack看来:所有的静态资源都是模块
  • webpack 模块能够识别以下等形式的模块之间的依赖:
  • JS的模块化规范:
    • ES2015 import export
    • CommonJS require() module.exports
    • AMD definerequire
  • 非JS等静态资源:
    • css/sass/less 文件中的 @import
    • 图片连接,比如:样式 url(...) 或 HTML <img src=...>
    • 字体 等

webpack文档和资源


安装webpack

安装:webpack模块 和 webpack命令行

1
npm install -D webpack webpack-cli

使用:

  • 在 package.json 文件的 scripts 配置中配置:"dev": "webpack ./src/main.js" 开发阶段使用默认 dev 命令
  • 在项目的根目录中,打开终端,并且运行: npm run dev 命令;webpack 打包生成的内容被自动生成到:dist目录中

main.js 打包入口文件,导入模块

1
2
// 导入jquery,安装了 jQuery 包后 通过 import {  } from "module" 导入
import $ from 'jquery'

注意:浏览器不能识别 import 这个语法,因此,无法直接在浏览器中使用该js文件

解决方式:使用 webpack 对该文件进行打包处理,在页面中使用打包后的js文件

配置文件 package.json 文件的 scripts 配置:"dev": "webpack ./src/main.js --mode development --watch"

  • ./src/main.js 指定了webpack打包的入口文件
  • --mode development 表示:指定webpack的模式为开发模式(代码不压缩)
  • --watch 参数来监视文件变化,当js文件内容改变后,webpack 会自动对其进行打包处理

webpack中有两种运行模式;

  • development(开发模式) 代码未压缩
  • production(生产模式) 代码压缩

webpack 的配置文件 webpack.config.js 使用说明

项目的根目录中创建 webpack 的配置文件;名称约定为 webpack.config.js

配置文件 package.json 文件的 scripts 配置:"dev": "webpack"

webpack 的配置文件是运行在 Node 环境中的,因此,需要按照 node 的方式来写配置

注意:在 webpack.config.js 配置文件中不要使用 import !!!因为 node 也不能处理 import 关键字!!!

只要使用 webpack,不管是 命令行 还是 配置文件,都要:在index.html页面中,手动引入 <script src="../dist/main.js"></script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 导入path模块(内置模块)
const path = require('path')
// 导出配置对象
// 说明:导出的就是 webpack 要读取的配置对象
module.exports = {
// 入口
entry: path.join(__dirname, './src/main.js'),
// 出口
output: {
// 指定输出文件所在目录
path: path.join(__dirname, '/dist'),
// 指定输出文件的名称
filename: 'main.js'
},
// 指定webpack的模式使用 开发模式
mode: 'development',
// 开启监视模式,监视文件内容的变化
watch: true,
}

webpack-dev-server

在开发期间会使用webpack的一个辅助包: webpack-dev-server;

  • 自动搭建开发环境
  • 自动开启HTTP服务
  • 自动打开浏览器(以HTTP协议的方式)
  • 自动监视文件变化

安装:

1
npm i -D webpack-dev-server

修改 scripts 中的dev命令为: webpack-dev-server

ebpack-dev-server 不会帮我们生成一个 dist 目录

对比 webpack 和 webpack-dev-server 两个命令:

  • webpack 会在磁盘中生成一个 dist 目录
  • webpack-dev-server 不会往磁盘中生成dist目录,而是放在内存中了
  • 往内存中存内容,要比往磁盘中存内容,快得多

配置文件 package.json 文件的 scripts 配置:"dev": "webpack-dev-server ./src/main.js --mode development --open --contentBase ./src"

  • ./src/main.js 指定了webpack打包的入口文件
  • --mode development 表示:指定webpack的模式为开发模式(代码不压缩)
  • --open 自动打开浏览器
  • --contentBase ./src 表示:默认打开哪个文件中的 index.html 页面
  • --port 9999 修改默认端口号为 9999
  • --hot 热更新,修改了哪个地方将来就只会更新修改的内容

webpack-dev-server 的配置文件 webpack.config.js 使用说明

配置文件 package.json 文件的 scripts 配置:"dev": "webpack-dev-server"

命令行可以和配置文件一起来使用;比如:webpack-dev-server --hot,表示:通过命令行的方式开启热更新,而其他的配置项从配置文件中读取

注意:只要配置文件的内容修改了,就需要重启 npm run dev 命令,那么,配置才会生效

html-webpack-plugin插件

作用:能够自动帮我们引入 js/css 等文件,在内存中根据模板生成一个页面的页面

该插件根据我们指定的模板路径,在内存中根据这个模板生成了一个页面;在生成的页面中自动引入了js文件。并且,在浏览器中打开的就是这个自动生成并且自动引入js文件的页面

安装:

1
npm i -D html-webpack-plugin

在 webpack.config.js 文件中配置该插件

  • 导入 html-webpack-plugin 包
  • 在 plugins 中 new HtmlWebpackPlugin()
  • 指定模板路径 template: path.join(__dirname, './src/index.html')

处理 css / less 文件

非 JavaScript 文件处理:webpack 自身只能处理js文件,所以,对于 非JS 文件来说,需要专门的 loader 来进行响应的处理!!!

处理 css 文件

安装:

1
npm i -D style-loader css-loader

在 webpack.config.js 文件中配置 loader 来处理css文件

  • 通过 module 配置 loader,处理 非JS 文件
  • 通过 rules 来配置处理 非JS 文件的规则
  • use 中配置的loader是有顺序的!先配置 style-loader 再配置 css-loader
  • use 执行的顺序是:从右往左,也就是:先调用 css-loader,在调用 style-loader
  • css-loader 读取css文件的内容,并且根据这个css内容,来创建一个模块(node)
  • style-loader 根据模块内容,创建一个style标签,然后,插入页面中

处理 less 文件

安装:

1
npm i -D less less-loader

less-loader 需要 style-loader css-loader

less-loader 依赖于 less 包;less 包就是用来将 less 语法转化为 css 语法的

1
2
// 引入less文件
import './less/index.less'
  • 通过 less-loader 用来将 less 转化为css文件,之后再按照处理 css 文件的方式处理
  • use 中配置的loader是有顺序的!先配置 style-loader 再配置 css-loader 再配置 less-loader
  • use 执行的顺序是:从右往左,也就是:先调用 less-loader,再调用 css-loader,再调用 style-loader

处理图片和字体文件

安装:

1
npm i -D file-loader url-loader

在 webpack.config.js 的 module 中配置一个 rule 规则

这两个包可以单独使用;如果使用 url-loader 那么,需要依赖于 file-loader

file-loader

  • 会文件名称进行重命名操作,这样可以避免文件重复加载的问题
  • 文件名是经过 MD5(特征码提取) 处理后, 得到一个32位的字符串: d5e6826816c62eb9aeb62dd4fb36d525
  • 相同的内容, 不管经过MD5处理多少次, 得到字符串都是相同的(复制的图片,名字改了内容一样)

在程序中, 经常会使用 MD5 对密码进行加密:1 ===> MD5 ( 32位 ) + ‘随机字符串’ ===> MD5;先对密码进行 MD5加密处理, 然后, 将该结果拼接一个随机字符串, 再进行一次MD5加密, 将这个结果存储到数据库中

url-loader

  • 会对图片文件进行 base64 编码处理,处理后,就变成了一种 base64 格式的图片字符串
  • 浏览器能够解析这个字符串,所以,在浏览器中能够看到图片
    • 精灵图:解决了小图片发送多次请求,有了精灵图以后,只需要发送一次请求
    • 字体图标(font-awesome):将一些小图标集成到字体文件中,减少了网络请求
    • base64 直接将小图标内嵌在 css 样式文件中,减少了网络请求
    • 只有很小的图标,才应该使用 base64 的方式。
    • 如果图片很大,反而会影响 网站加载 速度

base64 格式的图片字符串,相当于把图片转码成字符,可以内嵌在网页中;

字体图标的处理和图片一样;

babel的使用说明

babel是一个 JS 的编译器,能够将下一代的JS语法编译为浏览器能够识别的 ES5 的语法;通过babel,就可以在今天使用下一代的JS语法

如何使用:

  1. 安装:npm i -D babel-core babel-loader babel-preset-env
  2. 在 webpack.config.js 中添加一个 rule 规则
  3. 在项目根目录中创建 babel 的配置文件叫: .babelrc
  4. 在配置文件中,配置babel
1
2
3
4
5
{
"presets": [
"env", "stage-2"
]
}

模块的作用

  • babel-core 是babel的核心包
  • babel-loader 用来加载js文件
  • babel-preset-env 表示要让babel解析什么版本的JS,
  • env: ES2015/ES2016/ES2017 的结合体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ES6 ---> ES2015
ES7 ---> ES2016
ES8 ---> ES2017
ES9 ---> ES2018
...

ES6 一般表示 最新的 JS 语法,而不单纯是 第6个版本的JS

JS语法提案需要进行5个步骤:stage-0 ~ 4
其中,stage-0~3 这几个阶段还没有被采纳为标准
tage-4 就是下一代的JS,也就被采纳为标准的语法

如果一个语法没有被采纳为JS的标准,但是,你还想用,一般我们使用 babel-preset-stage-2 包来处理这些语法
npm i -D babel-preset-stage-2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 1 使用配置文件的方式来演示 webpack 和 webpack-dev-server 的使用

// 导入path模块(内置模块)
const path = require('path')
// 导入 webpack
// const webpack = require('webpack')

// 导入 html-webpack-plugin 包
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 导出配置对象
// 说明:导出的就是 webpack 要读取的配置对象
module.exports = {
// 入口
entry: path.join(__dirname, './src/main.js'),

// 出口
output: {
// 指定输出文件所在目录
path: path.join(__dirname, '/dist'),
// 指定输出文件的名称
filename: 'main.js'
},

// 指定webpack的模式使用 开发模式
mode: 'development',

// 开启监视模式,监视文件内容的变化
watch: true,

// 配置webpack-dev-server
devServer: {
// 自动打开浏览器
open: true,
// 指定默认打开那个目录中的index.html页面
// contentBase: path.join(__dirname, './src'), // 如果使用了html-webpack-plugin插件,开发模式就不需要这个路径了
// 指定端口号
port: 3000
// 开启热更新
// 第一步:配置 hot true
// hot: true
},

// 配置loader,处理 非JS 文件
module: {
// 通过 rules 来配置处理 非JS 文件的规则
rules: [
// 注意:use 中配置的loader是有顺序的! 先配置 style-loader 再配置 css-loader
// use 执行的顺序是: 从右往左,也就是:先调用css-loader,在调用style-loader

// css-loader 读取css文件的内容,并且根据这个css内容,来创建一个模块(node)
// style-loader 根据模块内容,创建一个style标签,然后,插入页面中
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },

// less-loader 用来将 less 转化为css文件
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },

// { test: /\.(jpg|jpeg|png|gif)$/, use: 'file-loader' },
{
test: /\.(jpg|jpeg|png|gif)$/,
// use: 'url-loader'
use: {
loader: 'url-loader',
options: {
// limit的单位是:字节
// 1024 表示:1024字节,也就是: 1kb

// 配置该项后,只有图片尺寸小于 limit 值的,才会被解析为 base64 编码的格式
// 否则,url-loader会自动调用 file-loader 以 url 的方式来加载图片
limit: 1024 * 60
}
}
},

// 处理字体图标文件
// { test: /\.(eot|svg|ttf|woff|woff2|otf)$/, use: 'file-loader' }
{ test: /\.(eot|svg|ttf|woff|woff2|otf)$/, use: 'url-loader' },

// 使用babel解析新的JS语法:
// 通过 exclude 配置项,告诉 babel 不要处理 node_modules 目录中的js文件
// 这样,可以加快 babel 处理的速度
{ test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ }
]
},

plugins: [
// 第二步:启用该插件
// new webpack.HotModuleReplacementPlugin(),

new HtmlWebpackPlugin({
// 指定模板路径
template: path.join(__dirname, './src/index.html')
})
]
}

脚手架 vue-cli

在工作中开发 Vue 都是基于 webpack 来开发的,但是 webpack 很难用,配置项太多,导致用户放弃使用 Vue,Vue 的作者提供了一个脚手架命令 vue-cli,通过这个命令行工具可以非常方便的生成一个配置好的完整 Vue 项目目录;

全局安装:

1
npm i -g vue-cli

使用:

1
vue init webpack 项目名称

在需要使用的目录上级运行命令;可以生成这个 项目名称 文件夹,里面包含配置好的 webpack;

  • 目录不要带有中文路径,项目名称不能大写,项目名称不要使用关键字 vue / vue-cli;

Vue 单文件组件

  • vue-loader
  • single-file components(单文件组件)
  • 后缀名:.vue,该文件需要被预编译后才能在浏览器中使用
  • 注意:单文件组件依赖于两个包 vue-loader / vue-template-compiler

安装:

1
npm i -D vue-loader vue-template-compiler

Vue 单文件组件使用

  • 1 安装:npm i -D vue-loader vue-template-compiler
  • 2 在 webpack.config.js 中配置 .vue 文件的loader
    • { test: /\.vue$/, use: 'vue-loader' }
  • 3 在 webpack.config.js 中添加 plugins
    • const VueLoaderPlugin = require('vue-loader/lib/plugin')
    • new VueLoaderPlugin()
  • 4 创建 App.vue 单文件组件,注意:App可以是任意名称
  • 5 在 main.js 入口文件中,导入 vueApp.vue 组件,通过 render 将组件与实例挂到一起

App.vue 组件已经通过 render 将组件与实例挂到一起,相当于根组件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- App.vue 示例代码: -->
<template>
<div>
<h1>VUE 单文件组件示例 -- App.vue</h1>
<p>这是 模板内容</p>
</div>
</template>

<script>
// 组件中的逻辑代码
export default {}
</script>

<style>
/* 组件样式 */
h1 {
color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js 配置:
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},

plugins: [
new VueLoaderPlugin()
]
1
2
3
4
5
6
7
8
9
10
11
/* main.js */

import Vue from 'vue'
// 导入 App 组件
import App from './App.vue'

const vm = new Vue({
el: '#app',
// 通过 render 方法,渲染App组件
render: c => c(App)
})

Vuex

  • 前提:只有在必要的情况下才使用Vuex工具,如果不需要Vuex,那就别用它
    • 使用 Vuex 之后,会附加更多框架的概念进来,会增加项目的复杂度!!!

Vuex 是什么

  • Vuex 是一个Vue的状态管理工具
  • 状态,即:数据(组件、实例中的data)
  • 状态管理,就是管理Vue项目中的数据

Vuex 能做什么

  • 用来解决:组件通讯问题
  • 优势:采用集中式,管理Vue项目中用到的所有数据

为什么需要使用 Vuex

  • 问题:为什么需要使用 Vuex 来管理Vue项目中的数据?
1
2
在大型项目中,组件之间的通讯会变得混乱,为了更好的管理组件之间的通讯,也为了更容易的管理和
调试项目,就需要一个工具来管理整个项目中的数据。这个工具就是:Vuex

状态管理的介绍

  • 最早的状态管理概念是在 React 中给提出的,Flux

Vuex 的使用

  • 1 安装:npm install vuex --save
  • 2 先引入 Vue,再引入 vuex
    • 因为 Vuex 是依赖于 Vue 的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<div id="app">
<h1>这里可以直接使用 Vuex 的数据{{ $store.state.count }}</h1>
<button @click="add1">加1</button>
<button @click="add5">加5</button>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vuex/dist/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
countAdd1 (state) {
state.count += 1
},
countAdd5 (state, Payload) {
state.count += Payload.num
}
}
})

const vm = new Vue({
el: '#app',
store: store,
methods: {
add1 () {
this.$store.commit('countAdd1')
},
add5 () {
this.$store.commit('countAdd5', {
num : 5
})
}
}
})
</script>

Vuex 中的核心概念

  • store:数据仓库
    • 1 提供了 state
    • 2 提供了操作 state 的方法(mutations)
  • state:状态,也就是 数据(相当于data)
  • mutations:操作数据的方法
    • 只要修改state,就要通过 mutations 来修改!!!
  • actions:异步操作数据
    • 1 包含异步操作
    • 2 最终,还是通过 mutations 来修改state
  • getters:Vuex的计算属性
    • 如果需要从现有的state中派生出一些状态,那就使用 getters
    • 比如: 未完成任务数量 状态就可以从 任务列表(todoList) 中派生出来

store

store 是 Vuex 提供的数据仓库,所有的需要共享通讯的数据都放在这个数据仓库中;

1
2
3
4
5
const store = new Vuex.Store({
state: {
count: 0
}
})

state

state 就是状态,实例或者是组件中的数据(data);

在实例或者组件中通过 this.$store.state 来获取到这个数据对象;

实例或者组件可以通过 mapState 辅助函数来映射数据到自己的计算属性中;

mapState(['count']) 是一个对象可以通过对象解构语法展开;

mapState(['count', 'data1', 'data2']) 也可以同时映射多个数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const store = new Vuex.Store({
state: {
count: 0,
data1: 1,
data2: 2
}
})

// 在组件中导入映射
import { mapState } from 'vuex'
// 将数据映射到子组件的计算属性中
// 1 简写映射
computed: mapState(['count'])
// 2 箭头函数映射
computed: mapState(count: state => state.count)
// 3 对象自定义名映射
computed:mapState({
myCount: 'count'
})
// 4 同时映射多个数据,并解构这个对象
computed:{
...mapState(['count', 'data1', 'data2'])
}

就可以直接使用这个数据;

1
2
3
4
5
6
7
<!-- 1 简写映射 2 箭头函数映射 -->
<div>{{ count }}</div>
<!-- 3 对象自定义名映射 -->
<div>{{ myCount }}</div>
<!-- 4 同时映射多个数据,并解构这个对象 -->
<div>{{ data1 }}</div>
<div>{{ data2 }}</div>

mutations

实例或者组件通过 this.$store.state 可以读取到数据仓库中的数据,但是如果需要修改数据,就需要使用 mutations 操作数据;

如果数据是复杂类型的,可以直接修改复杂类型的属性,前提是不能修改引用地址;简单类型修改数据就是重新赋值了;

mutations 对象中提供需要操作数据的方法,实例或者组件通过 this.$store.commit(handler, Payload) 来调用这个方法来修改数据;

  • mutations 对象声明的方法,有两个参数;
    • 参数:state 表示 Vuex 中的数据,通过这个参数可以直接操作 $store.state 这个数据对象;
    • 参数:Payload 表示传递的修改数据的参数对象;
  • this.$store.commit(handler, Payload)
    • 参数:handler 表示在 mutations 对象中声明的方法,是一个字符串方法名;
    • 参数:Payload 表示需要修改数据的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<div id="app">
<h1>这里可以直接使用 Vuex 的数据{{ $store.state.count }}</h1>
<button @click="add1">加1</button>
<button @click="add5">加5</button>
</div>
</body>
<script src="../../node_modules/vue/dist/vue.js"></script>
<script src="../../node_modules/vuex/dist/vuex.js"></script>
<script>
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
countAdd1 (state) {
state.count += 1
},
countAdd5 (state, Payload) {
state.count += Payload.num
}
}
})

const vm = new Vue({
el: '#app',
store: store,
methods: {
add1 () {
this.$store.commit('countAdd1')
},
add5 () {
this.$store.commit('countAdd5', {
num : 5
})
}
}
})
</script>

Vue 电商管理后台项目

项目前言

Vue的两种构建版本:

  • 完整版:编译器+运行时
  • 运行时版本:文件体积更小,运行速度更快

脚手架中使用的:完整版

默认通过 import Vue from 'vue' 导入的是运行时版本
脚手架中,通过一个配置,让默认导入的版本为:完整版

1
2
3
4
5
alias: {
// 给 vue 起别名,那么,在使用 import Vue from 'vue' 的时候
// 这个别名就会生效,此时,就会加载 vue 的完整版本
'vue$': 'vue/dist/vue.esm.js'
}

在导入完整版Vue后,需要使用以下方式来处理组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import App from './App'

new Vue({
el: '#app',
router,

// 注册一个局部组件
// 也就是将单文件组件 App,注册为Vue实例的一个局部组件
// 注册为局部组件了,那么,在模板中才可以使用这个组件
components: { App },

// 只要有template配置项,那么就会以该配置的内容,作为Vue编译的模板
template: '<App></App>'
})

初始化项目

  • src 目录中原来默认生成的内容,全部删除
1
2
3
4
5
6
7
源码全部放在 src 目录,只要修改 src 目录中的内容即可:

/assets 资源文件夹,放:图片、样式等等
/components 组件文件夹,所有的组件,都放到该目录中,并且每个组件都使用文件夹包裹
/router 路由
App.vue 根组件,就包含一个路由出口 <router-view></router-view>
main.js 整个项目的入口,也是webpack打包的入口

关闭ESLint

  • /config/index.jsuseEslint:false

如何开一个新功能

  • 1 在 components 目录中创建组件
  • 2 在 router/index.js 中配置路由

Element-UI的使用

  • 1 安装:npm i element-ui -S
  • 2 在 main.js 文件中导入element-ui的js和样式,并且安装称为插件
1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
// 1 导入EelmentUI
import ElementUI from 'element-ui'
// 2 导入 ElementUI的样式
import 'element-ui/lib/theme-chalk/index.css'
import App from './App.vue'
// 3 安装插件
Vue.use(ElementUI)

new Vue({
el: '#app',
render: h => h(App)
})
  • 3 打开element-ui的文档,从左侧菜单中找到对应的组件,参考示例代码,复制到自己页面中使用即可

axios发送请求

  • 1 安装:npm i -S axios
  • 2 导入:import axios from 'axios'
  • 3 使用:axios.post(接口地址, 参数).then(成功).catch(失败)

vue-router 编程式导航

  • 通过JS代码来实现跳转:this.$router.push('/home')

session说明

HTTP协议是:无状态

对于服务器来说,服务器是不会记忆两次请求是不是同一个浏览器发送过来的

但是,服务器不知道这两次请求是同一个人发送过来的,因此,登录功能就无法实现了;

状态保持:为了让服务器知道两次请求是同一个人发送过来的;

对于 状态保持 来说,第一次登录成功了,服务器会给我们分配一个唯一不会重复的 sessionid,并且,以后的每次请求,都会携带这个 sessionid ,这样,服务器就可以根据这个 sessionid 来判断你是否登录过了;

注意:两个不同的人登录,会得到不同的 sessionid

session 机制,是服务器默认就支持的一种状态保持机制,seesion 一般都会配合 cookie 使用

对于使用 token 机制的服务器来说:

  • 在登录成功后,获取到 token
  • 并且,以后的每次请求中,都需要手动将 token 传递给服务器,那么,服务器才知道你登录了
  • 并且,才能根据token值来区分是哪个用户

简单来看,sessionid+cookie 和 token 机制,就是实现的相同的功能

token 验证机制

样式写在哪

  • 说明:全局样式写在 index.css 中,组件的样式写在自己组件的style中

在组件中使用预编译CSS

  • 直接安装loader的包:npm i -D less-loader less 就可以使用 Less 了
  • <style lang="less"></style> 标签内可以以直接写 less 语法

抽离单文件组件的内容

  • 说明:如果将所有的template、script、style都放在 .vue 文件中,那么,这个文件会变的非常臃肿。可以将 不同的内容,抽离到单独的文件中
1
2
3
4
5
6
<!-- 将 模板 抽离到,当前目录下的:template.html文件中 -->
<template src="./template.html"></template>
<!-- 将 js 抽离到,当前目录下的:script.js文件中 -->
<script src="./script.js"></script>
<!-- 将 style 抽离到,当前目录下的:style.css文件中 -->
<style src="./style.css"></style>

如果需要使用 less 在 <style src="./style.less" lang="less"></style> 标签中加入 lang="less",并且引入的是less文件即可;

带token请求接口

  • 说明:除了登录接口以外,其他接口都需要 token 才能够成功获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
axios
// 通过第二个参数的 params 来给 get 请求传递参数
.get('http://localhost:8888/api/private/v1/users', {
params: {
query: '',
pagenum: 1,
pagesize: 3
},
// 因为这个接口是需要登录后才能够访问的,所以,需要将 token 作为
// 请求头中的一个属性来传递给服务器,这样,服务器才会知道我们已经登录成功
headers: {
Authorization: localStorage.getItem('token')
}
})
.then(res => {
console.log('用户列表获取成功:', res) if (res.data.meta.status === 200) {
// 获取数据成功
this.userList = res.data.data.users
}
})

Vue 中使用 .sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

通过使用 .sync 修饰符来修改数据,其实现原理是:父组件传递数据给子组件,子组件使用 prop 接收,不支持子组件直接修改 prop 中的数据,如果需要修同步数据,则使用子组件向父组件传递数据,通过父组件中提前声明的方法来修改父组件传递给子组件的数据,以达到同步数据的问题;

通过父组件提供方法,由子组件调用,并传递新的数据给父组件,然后父组件来修改数据;可以通过 .sync 修饰符来简写这一过程,Vue 提供了这个方法。

.sync 对应的方法就是 update:title;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 创建父组件:
Vue.component('parent', {
template: `
<div class="parent">
<!-- <child :title="doc.title" v-on:fn="getData"></child> -->
<!-- 事件中可以直接写函数体,就不用在 methods 声明方法了 -->
<!-- <child :title="doc.title" v-on:fn="doc.title = newData"></child> -->
<child :title.sync="doc.title"></child>
</div>
`,
data () {
return {
doc: {
title: '修改父组件的数据,以达到同步数据'
}
}
},
methods: {
getData (newData) {
// 接收数据直接修改传递给子组件的数据
this.doc.title = newData
}
}
})

// 创建子组件:
Vue.component('child', {
template: `
<div class="child">
<button @click="update_title">{{title}}</button>
</div>
`,
props: ['title'],
methods: {
update_title () {
// this.$emit('fn', '子组件传递数据给父组件,由父组件来修改数据,达到同步的目的')
// .sync 对应的方法就是 update:title
this.$emit('update:title', '子组件传递数据给父组件,由父组件来修改数据,达到同步的目的')
}
}
})

const vm = new Vue({
el: '#app'
})

配置 axios

在入口文件中引入 axios;把 axios 添加到 Vue 的原型中;

组件可以看成是 Vue 的实例,组件可以通过使用构造函数的原型的方法来使用 axios;

1
2
3
import axios from 'axios'
// 可以自定义一个属性名来接收,例如:$http
Vue.prototype.$http = axios

axios 公共的接口地址

只要配置该基础地址后,axios 会在每次发送请求的时候,将 baseUrl 和 当前请求的接口 合并到一起

比如:当前请求接口为:/login,那么 axios 会将 baseUrl + login 得到:http://localhost:8888/api/private/v1/login 最终的完整接口地址了

会自动解析接口开头是否带有 /

1
2
3
axios.defaults.baseURL = 'http://localhost:8888/api/private/v1/'

$http.post('/login')

axios 拦截器

axios 提供请求和响应拦截器,在发送请求之前,和接收响应数据之后,可已通过拦截器对这些数据操作;

回调函数的接收这些请求或者相应数据参数就是这些数据,操作完数据之后需要返回新的数据,return config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// 例如为每次请求添加请求头,token数据
if (config.url.indexOf('login') <= -1) {
// 给请求头中添加 Authorization 请求头:
config.headers.Authorization = localStorage.getItem('token')
}
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
return Promise.reject(error);
});

数据修改和DOM更新的问题

  • 问题描述:
1
2
3
4
因为 权限对话框 一开始隐藏的, 所以 Vue 是不会渲染该对话框的DOM内容的
接下来, 我们将控制权限对话框显示和隐藏的数据(assignRightsDialog), 修改为: true( assignRightsDialog = true )
但是, Vue 是异步更新DOM的, 也就是说: 更新数据后, DOM还没有立即更新(也就是: 对话框在DOM中还没有出现)
所以, 此时, 直接通过 this.$refs.rightsTree 无法获取到该组件(DOM内容)
  • 解决问题:
1
2
3
4
5
6
7
8
// 在修改数据后, 可以在 $nextTick() 的回调函数中, 获取到更新后的DOM内容
this.$nextTick(() => {
// 回调函数会在DOM更新后立即执行,此时,权限对话框已经出现在DOM中了
// 所以,此处,就可以获取到 树形控件了

// 在此处获取 this.$refs.rightsTree 就可以获取到了
this.$refs.rightsTree
})

element-tree-grid

  • 1 安装:npm i -S element-tree-grid
  • 2 在 main.js 中,注册全局组件:
1
2
import ElTreeGrid from 'element-tree-grid'
Vue.component(ElTreeGrid.name, ElTreeGrid)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--
label :设置列名称
prop :提供列内容的属性名
tree-key :区分其他菜单,不添加该key会导致所有菜单同时展开,添加该key只展开该菜单
level-key :设置菜单级别,以缩进形式表示子菜单
child-key :指定子菜单的属性名称
parent-key :父级菜单id,不添加该key,则无法收起子菜单
-->

<el-table-tree-column
label="分类名称"
prop="cat_name"
tree-key="cat_id"
level-key="cat_level"
child-key="children"
parent-key="cat_pid"
width="320"
:indent-size="20">
<template slot-scope="scope">
<span>{{ scope.row.cat_name }}</span>
</template>
</el-table-tree-column>

用户管理 - 分配角色

  • 1 点击分配角色按钮,展示对话框
  • 2 展示用户名以及所有的角色列表
    • Select 选择器 组件
  • 3 选中当前用户的角色
  • 4 获取到当前选中的角色,给当前用户分配角色
    • 查看接口,需要什么样的数据,就获取什么数据,然后,发送请求

左侧菜单的展示

  • 权限 和 菜单 是关联在一起的,也就是说:具有这个权限才会有对应的菜单
  • 不同的用户具有不同的角色,而不同的角色具有不同的权限,也就是:具有不同菜单
  • 问题:在获取左侧菜单的时候,没有传递 userId 这样能区分不同用户的信息,但是每个用户登录后,左侧菜单还是不同的,内部是如何区分不同用户的呢?
    • 答案:服务端通过 token 来区分不同用户,说明 token 中包含了 userId 这样区分用户id的数据

quill-editor

1
2
3
4
<quill-editor
v-model="goodsFormAdd.goods_introduce"
class="goods-editor">
</quill-editor>

按需加载

  • 1 修改 router/index.js 中导入组件的语法
1
2
3
4
5
6
7
8
9
10
11
// 使用:
const Home = () => import('@/components/home/Home')
// 替换:
// import Home from '@/components/home/Home'

// 给打包生产的JS文件起名字
const Home = () => import(/* webpackChunkName: 'home' */ '@/components/home/Home')

// chunkName相同,将 goods 和 goods-add 两个组件,打包到一起
const Goods = () => import(/* webpackChunkName: 'goods' */'@/components/goods')
const GoodsAdd = () => import(/* webpackChunkName: 'goods' */'@/components/goods-add')
  • 2 (该步可省略)修改 /build/webpack.prod.conf.js 中的chunkFilename
1
2
3
4
{
// [name] 代替 [id]
chunkFilename: utils.assetsPath('js/[name].[chunkhash].js')
}

使用CDN

  • 开源项目 CDN 加速服务
  • 1 在 index.html 中引入CDN提供的JS文件
  • 2 在 /build/webpack.base.conf.js 中(resolve前面)添加配置 externals
  • 注意:通过CDN引入 element-ui 的样式文件后,就不需要在 main.js 中导入 element-ui 的CSS文件了。所以,直接注释掉 main.js 中的导入 element-ui 样式即可
  • externals配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
externals: {
// 键:表示 导入包语法 from 后面跟着的名称
// 值:表示 script 引入JS文件时,在全局环境中的变量名称
vue: 'Vue',
axios: 'axios',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT',

BMap: 'BMap',
echarts: 'echarts',
}

import ElementUI from 'element-ui'

常用包CDN

缓存和保留组件状态

  • keep-alive
  • 解决方式:使用 keep-alive ,步骤如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1 在需要被缓存组件的路由中添加 meta 属性
meta 属性用来给路由添加一些元信息(其实,就是一些附加信息)
{
path: '/',
name: 'home',
component: Home,
// 需要被缓存
meta: {
keepAlive: true
}
}

2 修改路由出口,替换为以下形式:
根据 meta 是否有 keepAlive 属性,决定该路由是否被缓存
<keep-alive>
<!-- 这里是会被缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive">
</router-view>
</keep-alive>

<!-- 这里是不被缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive">
</router-view>

启用路由的 History 模式

  • 通过在路由中添加 mode: 'history' 可以去掉 浏览器地址栏中的 #
  • 在开发期间,只需要添加这个配置即可
  • 但是,在项目打包以后,就会出现问题
1
2
3
4
5
6
// 去掉 # 后,地址变为:

http://localhost:8080/goods

那么,服务器需要正确处理 /goods 才能正确的响应内容,
但是,/goods 不是服务端的接口,而是 用来在浏览器中实现 VueRouter 路由功能的

后端的处理方式

  • 1 优先处理静态资源
  • 2 对于非静态资源的请求,全部统一处理返回 index.html
  • 3 当浏览器打开 index.html 就会加载 路由的js 文件,那么路由就会解析 URL 中的 /login 这种去掉#的路径了