Vue.js
Vue 简介
Vue 简单认识
Vue(读音 /vjuː/,类似于 view)是一个渐进式的框架,什么是渐进式?
- 渐进式意味着你可以将 Vue 作为应用的一部分嵌入其中,带来更丰富的交互体验
- 或者如果你希望将更多的业务逻辑使用Vue实现,那么Vue的核心库以及其生态系统(比如:Core+Vue-router+Vuex)也可以满足你各种各样的需求
- Vue 有很多特点和Web开发中常见的高级功能
- 解耦视图和数据
- 可复用的组件
- 前端路由技术
- 状态管理
- 虚拟DOM
Vue 安装
安装Vue的方式有很多:
方式一:直接CDN引入
你可以选择引入开发环境版本还是生产环境版本
1 | <!-- 开发环境版本,包含了有帮助的命令行警告 --> |
方式二:下载和引入
方式三:NPM 安装
1 | npm install vue |
Vue 初体验
Hello World
1 | <div class="div">{{message}}</div> |
展示列表
1 | <div class="div"> |
计数器
1 | <div class="div"> |
Vue 中的 MVVM
什么是MVVM

- View 层:视图层,在前端开发中,通常就是 DOM 层,主要的作用是给用户展示各种信息
- Model 层:数据层,数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据
- VueModel 层:视图模型层,视图模型层是 View 和 Model 沟通的桥梁,一方面实现了 Data Binding(数据绑定),将 Model 的改变实时的反应到 View 中,另一方面它实现了 DOM Listener(DOM监听),当 DOM 发生一些事件(点击、滚动、touch 等)时,可以监听到,并在需要的情况下改变对应的 Data
MVC 和 MVVM 的区别
- MVC

- MVVM

Vue 的生命周期


Vue 基础语法
插值语法
插值:将值插入到模板的内容中
Mustache
1 | <div class="div"> |
v-once
在某些情况下,我们可能不希望界面中 Mustach 中的值随意的跟随改变,就可以使用一个 Vue 的指令:v-once
- 该指令后面不需要跟任何表达式
- 该指令表示元素和组件只渲染一次,不会随着数据的改变而改变
1 | <div class="app"> |

v-html
某些情况下,我们从服务器请求到的数据本身就是一个 HTML 代码,如果我们直接通过 {{}} 来输出,会将 HTML 代码也一起输出,但如果希望按照 HTML 格式进行解析,并且显示对应的内容,可以使用 v-html 指令
v-html 指令后面往往会跟上一个 string 类型,会将 string 的 html 解析出来并且进行渲染
v-text
- v-text 作用和 Mustache 比较相似,都是用于将数据显示在界面中
- v-text 通常情况下,接受一个 string 类型

v-pre
v-pre 用于跳过这个元素和它子元素的编译过程,显示原本的 Mustache 语法,将代码原封不动的解析出来

v-cloak
将未解析出来的代码块进行隐藏,但基本不会用到

绑定属性 v-bind
v-bind 基本使用
Mustache 指令主要作用是将值插入到我们模板的内容当中,但是,除了内容需要动态来决定外,某些属性我们也希望动态来绑定,比如:
- 动态绑定 a 元素的 href 属性
- 动态绑定 img 元素的 src 属性
这时,可以使用 v-bind 指令来动态绑定属性,v-bind 用于绑定一个或多个属性值,或者向另一个组件传递 props值

v-bind 语法糖
v-bind 有一个对应的语法糖(简写方式),在开发中,通常会使用语法糖的形式,因为这样更加简洁
简写方式如下:
1 | <div class="app"> |
即省略 v-bind,直接写 :
v-bind 动态绑定 class
很多时候,我们希望动态的来切换 class,比如:
- 当数据为某个状态时,字体显示红色
- 当数据另一个状态时,字体显示黑色
绑定 class 有两种方式:
- 对象语法
- 数组语法
- 对象语法
对象语法的含义是:class 后面跟的是一个对象
对象语法有下面这些用法:
1 | 用法一:直接通过{}绑定一个类 |
点击按钮,修改文字颜色:

有时候,如果 v-bind:class="{active:isActive,line:isLine}" 中 class 的项太多,可以定义一个方法,将其放到 methods 中:
1 | <div class="app"> |
- 数组语法
数组语法的含义是:class 后面跟的是一个数组
数组语法有下面这些用法:
1 | 用法一:直接通过{}绑定一个类 |
1 | <div class="app"> |
案例:点击当前项变色

v-bind 动态绑定 style
我们可以利用 v-bind:style 来绑定一些 CSS 内联样式
在写CSS属性名的时候,比如 font-size,可以使用驼峰式 (camelCase) fontSize,或短横线分隔 (kebab-case,记得用单引号括起来) 'font-size'
绑定class有两种方式:
- 对象语法
- 数组语法
- 对象语法
style 后面跟的是一个对象类型
- 对象的 key 是 CSS 属性名称
- 对象的 value 是具体赋的值,值可以来自于 data 中的属性
1 | <div class="app"> |
- 数组语法
style 后面跟的是一个数组类型
1 | <div class="app"> |
计算属性 computed
在模板中可以直接通过插值语法显示一些 data 中的数据,但是在某些情况下,可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示,这时可以使用计算属性 computed
计算属性 computed 的基本使用
比如:现在有 firstName 和 lastName 两个变量,需要显示完整的名称,可能直接使用空格将其隔开:
1 | <h2>{{firstName}} {{lastName}}</h2> |
或者使用加号拼接:
1 | <h2>{{firstName + '' +lastName}}</h2> |
但是如果多个地方都需要显示完整的名称,我们就需要写多个 {{firstName}} {{lastName}} 或 {{firstName + '' +lastName}} ,使代码看上去很不优雅
这时,可能想到,将{{firstName + '' +lastName}} 封装为一个函数,通过函数的方式调用,但其实最佳方案是使用计算属性:
1 | <div class="app"> |
计算属性中也可以进行一些更加复杂的操作,比如下面计算图书价格的例子:
1 | <div class="app"> |
计算属性的 setter 和 getter
每个计算属性都包含一个 getter 和一个 setter,getter 用来读取值,setter 用来设置值(但 setter 不常用)

couputed 与 methods 的区别
我们可能会考虑这样的一个问题:
- methods 和 computed 看起来都可以实现我们的功能,那么为什么还要多一个计算属性这个东西呢?
- 原因:计算属性会进行缓存,如果多次使用时,计算属性只会调用一次
computed 区别于 methods 的核心
在官方文档中,强调了computed 区别于 methods 最重要的两点
- computed 是属性调用,而 methods 是函数调用
- computed 带有缓存功能,而 methods 没有
- computed 定义的方法,我们是以属性访问的形式调用的,
{{computedTest}},但是 methods 定义的方法,我们必须要加上()来调用,如{{methodTest()}} - 我们可以将同一函数定义为一个方法而不是一个计算属性,两种方式的最终结果确实是完全相同的然而,不同的是计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值,这就意味着只要 text 还没有发生改变,多次访问 getText 计算属性会立即返回之前的计算结果,而不必再次执行函数,而方法只要页面中的属性发生改变就会重新执行
- 对于任何复杂逻辑,都应当使用计算属性
- computed 依赖于 data 中的数据,只有在它的相关依赖数据发生改变时才会重新求值
事件监听 v-on
v-on 基本使用
在前端开发中,我们需要经常和用户进行交互,这个时候,就必须监听用户发生的事件,比如点击、拖拽、键盘事件等,在 Vue 中监听事件使用 v-on 指令
1 | <div class="app"> |
当通过 methods 中定义方法,以 @click 调用时,需要注意参数问题:
- 如果该方法不需要额外参数,那么方法后的 () 可以不添加,但如果方法本身中有一个参数,那么会默认将原生事件 event 参数传递进去
- 如果需要同时传入某个参数,同时需要 event 时,可以通过
$event传入事件
1 | <div class="app"> |
v-on 修饰符
在某些情况下,我们拿到 event 的目的可能是进行一些事件处理,vue 提供了修饰符来帮助我们方便的处理一些事件:
.stop:调用event.stopPropagation().prevent:调用event.preventDefault().{keyCode | keyAlias}:只当事件是从特定键触发时才触发回调.native:监听组件根元素的原生事件.once:只触发一次回调
1 | <div class="app"> |
条件和循环
条件判断
v-if、v-else-if、v-else 这三个指令与 JavaScript 的条件语句 if、else、else if 类似
vue 的条件指令可以根据表达式的值在 DOM 中渲染或销毁元素或组件

*v-if *
v-if 后面的条件为 false 时,对应的元素以及其子元素不会渲染,也就是不会有对应的标签出现在 DOM 中
v-show
v-show 的用法和 v-if 非常相似,也用于决定一个元素是否渲染
v-show 和 v-if 的区别
- v-if 是真正的条件渲染,会确保在切换过程中,条件块内的事件和子组件被销毁和重建(组件被重建将会调用created)
- v-show 不论如何,都会被渲染在 DOM 中,当条件为真值时,将会修改条件的 css 样式
- v-if 有更高的切换开销,v-show 有更高的初始渲染开销
- v-if 是动态的向 DOM 树内添加或者删除 DOM 元素,v-show 是通过设置 DOM 元素的 display 样式属性控制显隐
- v-if 切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件,v-show 只是简单的基于 css 切换
- v-if 是惰性的,如果初始条件为假,则什么也不做,只有在条件第一次变为真时才开始局部编译,v-show 是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且 DOM 元素保留
- v-if 有更高的切换消耗,v-show 有更高的初始渲染消耗
- v-if 适合运营条件不大可能改变,v-show适合频繁切换
v-if 和 v-show 都可以决定一个元素是否渲染,那么开发中我们如何选择呢?
v-if 当条件为 false 时,压根不会有对应的元素在 DOM 中
v-show 当条件为 false 时,仅仅是将元素的 display 属性设置为 none 而已
结论:
- 当需要在显示与隐藏之间切换很频繁时,使用 v-show
- 当只有一次切换时,通过使用 v-if
案例:切换用户账号
1 | <div class="app"> |
循环 v-for
当我们有一组数据需要进行渲染时,我们就可以使用 v-for 来完成
v-for 的语法类似于 JavaScript 中的 for 循环:item in items
遍历数组
1 | <div class="app"> |
遍历对象
1 | <div class="app"> |
组件的 key 属性
官方推荐我们在使用 v-for 时,给对应的元素或组件添加上一个 :key 属性
为什么需要这个 key 属性呢,其实和 Vue 的虚拟 DOM 的 Diff 算法有关系,这里借用 React’s diff algorithm 中的一张图来简单说明一下:

当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点

我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:
即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E:

是不是很没有效率?
所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以,key 的作用主要是为了高效的更新虚拟 DOM


1 | <li v-for="item in info" :key="item">{{item}}</li> |
案例:图书购物车

index.html:
1 |
|
main.js:
1 | const app = new Vue({ |
表单绑定 v-model
v-model 基本使用
表单控件在实际开发中是非常常见的,特别是对于用户信息的提交,需要大量的表单,vue 中使用 v-model 指令来实现表单元素和数据的双向绑定

案例解析:当我们在输入框输入内容时,因为 input 中的 v-model 绑定了 message,所以会实时将输入的内容传递给 message,message 发生改变,当 message 发生改变时,因为使用了 Mustache 语法,所以将 message 的值插入到 DOM 中,所以 DOM 会发生响应的改变,所以通过 v-model 实现了双向绑定
v-model 原理
v-model 其实是一个语法糖,它的背后本质上是包含两个操作:
- v-bind 绑定一个 value 属性
- v-on 指令给当前元素绑定 input 事件
也就是说:
1 | <input type="text" v-model="message"> |
等同于:
1 | <input type="text" :value="message" @input="message = $event.target.value"> |
v-model 结合 radio 使用

1 | <div class="app"> |
v-model 结合 checkbox 使用
- checkbox 单选框

1 | <div class="app"> |
- checkbox 多选框

1 | <div class="app"> |
v-model 结合 select 使用

1 | <div class="app"> |
v-model 修饰符
lazy 修饰符
- 默认情况下,v-model 是在 input 事件中同步输入框的数据,也就是说,一旦有数据发生改变,对应的 data 中的数据就会自动发生改变
- lazy 修饰符可以让数据在失去焦点或者回车时才会更新
number 修饰符
- 默认情况下,在输入框中无论输入的是字母还是数字,都会被当做字符串类型进行处理,但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理
- number 修饰符可以让在输入框中输入的内容自动转成数字类型
trim 修饰符
- 如果输入的内容首尾有很多空格,通常我们希望将其去除
- trim 修饰符可以过滤内容左右两边的空格
1 | <div class="app"> |

组件化开发
认识组件化
- 人面对复杂问题的处理方式:任何一个人处理信息的逻辑能力都是有限的,但人有一种天生的能力,就是将问题进行拆解,如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,会发现大的问题也会迎刃而解
- 组件化也是类似的思想:如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展,但如果将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
Vue 组件化思想
组件化是 Vue.js 中的重要思想
,它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
任何的应用都会被抽象成一颗组件树:

有了组件化的思想,我们在之后的开发中就要充分的利用它,尽可能的将页面拆分成一个个小的、可复用的组件,这样让我们的代码更加方便组织和管理,并且扩展性也更强
注册组件
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件

注意:以上代码方式创建的组件是全局组件,即可以在多个 vue 的实例下使用,要改为局部组件,需要将注册方法写到具体的某个实例中
全局组件和局部组件
当我们通过调用 Vue.component() 注册组件时,组件的注册是全局的
,这意味着该组件可以在任意 Vue 实例下使用;如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件

父组件和子组件
组件和组件之间存在层级关系,而其中一种非常重要的关系就是父子组件的关系

注册组件语法糖
在上面注册组件的方式,可能会有些繁琐,Vue为了简化这个过程,提供了注册的语法糖,主要是省去了调用Vue.extend() 的步骤,直接使用一个对象来代替
语法糖注册全局组件和局部组件:
1 | <div class="app"> |
注册组件模板分离
即使用 script 标签或 template 标签将模板内容从注册时的 template 中抽离出来
使用script标签

使用template标签
这种方式更为简单常用

组件数据存放
组件是一个单独功能模块的封装,这个模块有属于自己的HTML模板,也应该有属性自己的数据 data,那么组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
先来测试一下,组件中能不能直接访问 Vue 实例中的 data:

通过以上代码发现,组件不能直接访问 Vue 实例中的 data,而且即使可以访问,如果将所有的数据都放在 Vue 实例中,Vue 实例就会变的非常臃肿
结论:Vue 组件应该有自己保存数据的地方
那么,组件自己的数据存放在哪里呢?
其实,组件对象也有一个 data 属性,只是这个 data 属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据

为什么 data 在组件中必须是一个函数呢?
- 首先,如果不是一个函数,Vue直接就会报错
- 其次,Vue 让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响

父子组件通信
- 父传子:props
- 子传父:自定义事件

父传子 props
在组件中,使用选项 props 来声明需要从父级接收到的数据
props 的值有两种方式:
- 字符串数组,数组中的字符串就是传递时的名称

- 对象,对象可以设置传递时的类型,也可以设置默认值和类型等

验证支持的数据类型:

子传父 自定义事件
自定义事件流程:
- 在子组件中,通过
$emit()来触发事件 - 在父组件中,通过
v-on来监听子组件事件

双向绑定
1 | <!-- 父组件模板 --> |
图解关系:
父子组件访问
父组件访问子组件
父组件访问子组件有两种方式:
- $children(不常用)
- $refs(常用)
$children
this.$children 是一个数组类型,它包含所有子组件对象,可以通过遍历,取出所有子组件的信息

$children 的缺陷:
通过 $children 访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值,但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化
有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用 $refs
$refs
$refs 和 ref 指令通常是一起使用的:
首先,我们通过 ref 给某一个子组件绑定一个特定的 ID
1
2<cpn ref="child1"></cpn>
<cpn ref="child2"></cpn>其次,通过
this.$refs.ID就可以访问到该组件了
1 | console.log(this.$refs.child1.name); |
子组件访问父组件(不常用)
子组件访问父组件通过:$parent
如果是向访问根组件,通过:$root
注意:
- 尽管在 Vue 开发中,我们允许通过 $parent 来访问父组件,但是在真实开发中尽量不要这样做,因为这样耦合度太高了
- 子组件应该尽量避免直接访问父组件的数据,如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题
- 另外,通过 $parent 直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于调试和维护
- 也不常用 $root 来访问根组件(即 vue 实例),因为根组件中一般只存放路由等重要数据,不存放其他信息
组件化高级
插槽 slot(Vue 2.6之前用法)
为什么使用 slot
slot 翻译为插槽,插槽的目的是让我们原来的设备具备更多的扩展性
组件的插槽,也是为了让我们封装的组件更加具有扩展性,让使用者可以决定组件内部的一些内容到底展示什么
slot 基本使用
在子组件中,使用特殊的元素 <slot> 就可以为子组件开启一个插槽,该插槽插入什么内容取决于父组件如何使用

具名插槽 slot
当子组件的功能复杂时,子组件的插槽可能并非是一个,比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边
那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?这个时候,我们就需要给插槽起一个名字,这就是具名插槽
具名插槽的使用很简单,只要给 slot 元素一个 name 属性即可:<slot name='myslot'></slot>

编译作用域
- 父组件模板的所有东西,都会在父级作用域内编译
- 子组件模板的所有东西,都会在子级作用域内编译

作用域插槽
作用域插槽是 slot 一个比较难理解的点,一句话总结就是:
父组件替换插槽的标签,但是内容由子组件来提供

插槽slot(Vue 2.6之后用法)
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即
v-slot指令)。它取代了slot和slot-scope这两个目前已被废弃但未被移除且仍在文档中的特性。新语法的由来可查阅这份 RFC。
slot 有三种类型
- 默认插槽 default
- 具名插槽 name
- 作用域插槽 v-slot
在子组件中:
- 插槽用
<slot>标签来确定渲染的位置,里面放的是父组件没传内容时的后备内容,一个不带 name 的<slot>出口会带有隐含的名字 default - 具名插槽用 name 属性来表示插槽的名字
- 作用域插槽在作用域上绑定属性来将子组件的信息传给父组件使用
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:
1 | <div class="container"> |
对于这样的情况,<slot> 元素有一个特殊的特性:name,这个特性可以用来定义额外的插槽:
1 | <div class="container"> |
一个不带 name 的
v-slot
- 具名插槽通过指令参数
v-slot:插槽名的形式传入,可以简化为#插槽名 - 作用域插槽通过
v-slot:xxx="slotProps"的 slotProps 来获取子组件传出的属性
1 | //具名插槽的缩写 |
在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
1 | <base-layout> |
现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot的 <template> 中的内容都会被视为默认插槽的内容
然而,如果你希望更明确一些,仍然可以在一个 <template> 中包裹默认插槽的内容:
1 | <base-layout> |
任何一种写法都会渲染出:
1 | <div class="container"> |
注意 v-slot 只能添加在 上 (只有一种例外情况),这一点和已经废弃的 slot 特性)不同。
v-slot 作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的,例如,设想一个带有如下模板的 <current-user> 组件:
1 | <span> |
我们可能想换掉备用内容,用名而非姓来显示,如下:
1 | <current-user> |
然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user 而我们提供的内容是在父级渲染的
为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个特性绑定上去:
1 | <span> |
绑定在 <slot> 元素上的特性被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
1 | <current-user> |
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,这样我们就可以把 v-slot 直接用在组件上:
1 | <current-user v-slot:default="slotProps"> |
这种写法还可以更简单,就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
1 | <current-user v-slot="slotProps"> |
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
1 | <!-- 无效,会导致警告 --> |
只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法:
1 | <current-user> |
解构插槽 Prop
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
1 | function (slotProps) { |
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式
所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
1 | <current-user v-slot="{ user }"> |
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:
1 | <current-user v-slot="{ user: person }"> |
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
1 | <current-user v-slot="{ user = { firstName: 'Guest' } }"> |
注意:
- 默认插槽名为default,可以省略default直接写v-slot。缩写为#时不能不写参数,写成#default(这点所有指令都一样,v-bind、v-on)
- 多个插槽混用时,v-slot不能省略default
- 只要出现多个插槽,请始终为所有的插槽使用完整的基于 的语法
动态插槽名( 2.6.0 新增)
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
1 | <base-layout> |
模块化开发
为什么需要模块化
JavaScript 原始功能
在网页开发的早期,js 制作作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码很少,直接将代码写在 <script> 标签中即可:
1 | <script> |
随着 ajax 异步请求的出现,慢慢形成了前后端的分离,客户端需要完成的事情越来越多,代码量也是与日俱增,为了应对代码量的剧增,我们通常会将代码组织在多个js文件中,进行维护,但是这种维护方式,依然不能避免一些灾难性的问题
比如全局变量同名问题:
1 | // a.js 文件中,小明定义了一个变量 flag , 值为 true |
另外,这种代码的编写方式对 js 文件的依赖顺序几乎是强制性的,当 js 文件过多,比如有几十个的时候,弄清楚它们的顺序是一件比较麻烦的事情,而且即使你弄清楚顺序了,也不能避免上面出现的这种尴尬问题的发生
这时,我们可以使用匿名函数来解决方面的重名问题
匿名函数的解决方案
在 a.js 文件中,我们使用匿名函数:
1 | (function(){ |
使用匿名函数虽然解决了重名问题,但是如果我们希望在 main.js 文件中,用到 flag,应该如何处理呢?
显然,另外一个文件中不方便使用,因为 flag 是一个局部变量
这时,我们可以将需要暴露到外面的变量,使用一个模块作为出口
使用模块作为出口
来看下对应的代码:
1 | // 使用模块作为出口 |
以上代码在匿名函数内部,定义一个对象,给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可),最后将这个对象返回,并且在外面使用了一个 MoudleA 接受。
接下来,在 main.js 中使用只需要使用属于自己模块的属性和方法即可:
1 | // 使用模块 |
这就是模块最基础的封装,事实上模块的封装还有很多高级的话题,幸运的是,前端模块化开发已经有了很多既有的规范,以及对应的实现方案
常见的模块化规范:
- CommonJS
- AMD
- CMD
- ES6 Modules
CommonJS(了解)
模块化有两个核心:导出和导入
1 | // CommonJS 导出 |
ES6 的 export 和 import
*export 基本使用 *
export 指令用于导出变量,比如下面的代码:
1 | // info.js |
上面的代码还有另外一种写法:
1 | // info.js |
导出函数或类
上面我们主要是输出变量,也可以输出函数或者输出类
上面的代码也可以写成这种形式:

export default
某些情况下,一个模块中包含某个的功能,我们并不希望给这个功能命名,而且让导入者可以自己来命名
这个时候就可以使用 export default
1 | // info.js |
来到 main.js 中,这样使用就可以了:
1 | import myFunc from './info.js' |
这里的 myFunc 是自己命名的,可以根据需要命名它对应的名字
另外,需要注意:
export default 在同一个模块中,不允许同时存在多个
import使用
我们使用 export 指令导出了模块对外提供的接口,就可以通过 import 命令来加载对应的这个模块了
首先,需要在HTML代码中引入两个 js 文件,并且类型需要设置为 module:
1 | <script src="info.js" type="module"></script> |
import 指令用于导入模块中的内容,比如 main.js 的代码:
1 | import {name,age,height} from "./info.js" |
如果我们希望某个模块中所有的信息都导入,一个个导入显然有些麻烦,则可以导入模块中所有的 export 变量,但是通常情况下我们需要给起一个别名,方便后续的使用
1 | import * as info from "./info.js" |
webpack
认识 webpack
什么是Webpack?官方的解释:
At its core, webpack is a static module bundler for modern JavaScript applications.
从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具。

从两个关键词模块和打包来理解 webpack
前端模块化
前面已经解释了为什么前端需要模块化,也提到了目前使用前端模块化的一些方案:AMD、CMD、CommonJS、ES6
在ES6之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发,并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,并且将其进行整合打包,而 webpack 其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系,而且不仅仅是 JavaScript 文件,我们的 CSS、图片、json 文件等,在 webpack 中都可以被当做模块来使用,这就是 webpack 中模块化的概念
打包
webpack 可以帮助我们进行模块化和处理模块间的各种复杂关系,而打包则是将 webpack 中的各种资源模块进行打包合并成一个或多个包(Bundle),并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将 scss 转成 css,将 ES6 语法转成 ES5 语法,将 TypeScript 转成 JavaScript 等操作
但是打包的操作似乎 grunt/gulp 也可以帮助我们完成,它们有什么不同呢?
webpack 和 grunt/gulp 对比
grunt/gulp 的核心是 Task:我们可以配置一系列的 task,并且定义 task 要处理的事务(例如 ES6、ts 转化,图片压缩,scss 转成 css),之后让 grunt/gulp 来依次执行这些 task,而且让整个流程自动化,所以 grunt/gulp 也被称为前端自动化任务管理工具
来看一个 gulp 的 task,下面的 task 就是将 src 下面的所有 js 文件转成 ES5 的语法,并且最终输出到 dist 文件夹中:

什么时候用 grunt/gulp
- 如果你的工程模块依赖非常简单,甚至是没有用到模块化的概念,只需要进行简单的合并、压缩,就使用 grunt/gulp 即可
- 但是如果整个项目使用了模块化管理,而且相互依赖非常强,就可以使用更加强大的 webpack
grunt/gulp 和 webpack 有什么不同
- grunt/gulp:更加强调前端流程的自动化,模块化不是它的核心
- webpack:更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能
webpack 的安装
安装 webpack 首先需要安装 Node.js,Node.js 自带了软件包管理工具 npm
查看自己的 node 版本:node -v<br />
全局安装 webpack
- 先指定版本号3.6.0,因为vue cli2依赖该版本
- -g:global,全局
1 | npm i webpack@3.6.0 -g |
局部安装webpack
–save-dev是开发时依赖,项目打包后不需要继续使用的
1 | npm i webpack@3.6.0 -save-dev |
为什么全局安装后,还需要局部安装
- 在终端直接执行 webpack 命令,使用的全局安装的 webpack
- 当在 package.json 中定义了 scripts 时,其中包含了 webpack 命令,那么使用的是局部 webpack
webpack 的起步
准备工作
创建如下文件和文件夹:

文件和文件夹解析:
- dist文件夹:用于存放之后打包的文件
- src文件夹:用于存放我们写的源文件
- main.js:项目的入口文件
- mathUtils.js:定义了一些数学工具函数,可以在其他地方引用
- index.html:浏览器打开展示的首页html
- package.json:npm 包管理的文件,通过
npm init生成
mathUtils.js 文件中的代码:
1 | function add(num1, num2) { |
main.js 文件中的代码:
1 | // 使用 CommonJS 规范导入 |
js 文件的打包
现在的 js 文件中使用了模块化的方式进行开发,不能直接使用,原因是:
- 如果直接在 index.html 引入这两个 js 文件,浏览器并不识别其中的模块化代码
- 另外,在真实项目中当有许多这样的 js 文件时,一个个引用非常麻烦,并且后期非常不方便对它们进行管理
这时,可以使用 webpack 工具,对多个 js 文件进行打包:
1 | webpack src/main.js dist/bundle.js |
使用打包后的文件
打包后会在 dist 文件下,生成一个 bundle.js 文件
,是 webpack 处理了项目直接文件依赖后生成的一个 js 文件,只需要将这个 js 文件在 index.html 中引入即可
1 | <script src="dist/bundle.js"></script> |
webpack 的配置
入口和出口
上面使用 webpack 对多个 js 文件进行打包的命令是:webpack src/main.js dist/bundle.js,其中“src/main.js”是出口,“dist/bundle.js”是入口
试想,如果每次使用 webpack 的命令都需要写上入口和出口作为参数,显得非常麻烦,有没有一种方法可以将这两个参数写到配置中,在运行时,直接读取呢?
这时,我们可以创建一个 webpack.config.js 文件:
1 | const path = require('path') |
配置完后,可以通过执行简单的命令:webpack 来对多个 js 文件进行打包了
但此时,我们使用的 webpack 是全局的 webpack(只要是在终端直接执行 webpack 命令执行的都是全局的 webpack),而一个项目往往依赖特定的 webpack 版本,全局的版本可能很这个项目的 webpack 版本不一致,导出打包出现问题,所以通常一个项目,都安装有自己局部的 webpack
安装局部 webpack
为了能够使用局部的 webpack,我们需要先安装局部 webpack:
1 | npm i webpack@3.6.0 -save-dev |
安装之后,通过命令:node_modules/.bin/webpack 来启动 webpack 进行打包
但是,每次执行用这么长的命令,感觉很不方便,我们可以在 package.json 的 scripts 中定义自己的执行脚本,来简化命令
package.json 中定义启动
package.json 中的 scripts 的脚本在执行时,会按照一定的顺序寻找命令对应的位置:
- 首先,会寻找本地的 node_modules/.bin 路径中对应的命令
- 如果没有找到,会去全局的环境变量中寻找

配置完成以后,只需要执行 npm run build 命令即可调用 webpack 进行打包
loader 的使用
在之前的实例中,主要是用 webpack 来处理 js 代码,并且 webpack 会自动处理 js 之间相关的依赖
但在开发中往往不仅仅有基本的 js 代码处理,也需要加载 css、图片,也包括一些高级的将 ES6、TypeScript 转成 ES5 代码,将scss、less 转成 css,将 .jsx、.vue 文件转成 js 文件等
对于webpack本身的能力来说,对于这些转化是不支持的,我们需要给 webpack 扩展对应的 loader
loader 是 webpack 中一个非常核心的概念,webpack 可以使用 loader 来预处理文件
css-loader
- 通过 npm 安装需要使用的 loader
1 | npm install style-loader css-loader --save-dev |
style-loader将模块的导出作为样式添加到 DOM 中css-loader解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
- 在 webpack.config.js 中的 module关键字下进行配置
1 | module.exports = { |
*css 文件处理 *
项目开发过程中,我们必然需要添加很多的样式,而样式我们往往写到一个单独的文件中
- 重新组织文件的目录结构,将零散的 js 文件放在一个 js 文件夹中,此时引用 mathUtils.js 的文件路径也需修改:
1 | const math = require('./js/mathUtils') |
- 在 src 目录中,创建一个 css 文件,其中创建一个 normal.css 文件

- 在 normal.css 中,将 body 设置为 red:
1 | body { |
这时,normal.css 中的样式是不会生效的,因为我们还没有引用它,所以 webpack 不能找到该文件
在入口文件 main.js 中引用 css 文件:
1 | // 引用 css 文件 |
配置完成以后,可以使用 npm run build 命令将 css 文件代码也一并打包到 bundle.js 文件中了,此时,css 中的代码会生效:

less-loader
less-loader 加载和转译 LESS 文件
- 安装
1 | npm install --save-dev less-loader less |
- 在 css 文件夹中新建 less 文件 special.less:
1 | @fontSize:50px; |
- 配置 less 文件
1 | module.exports = { |

- 在 main.js 中引入 less 文件
1 | // 引用 less 文件 |
使用 npm run build 重新打包后,查看效果:

url-loader 和 file-loader
- 资源准备
在 src 文件夹下新建 img 文件夹,加入两张图片:
- 一张较小的图片 test01.jpg (小于8kb)
- 一张较大的图片 test02.jpg (大于8kb)

- 在 css 样式中引用图片,修改 normal.css 中的样式:
1 | body { |
- 安装配置 url-loader
url-loader 像 file loader 一样工作,但如果文件小于限制,可以返回 data URL
1 | npm install --save-dev url-loader |
- 修改 webpack.config.js 配置文件,配置 url-loader
1
2
3
4
5
6
7
8
9{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
再次打包,运行 index.html,就会发现背景图片渲染了出来:

而且,此时背景图是通过 base64 显示出来的:

其实,这是 limit 属性的作用,当图片小于 8kb 时,对图片进行 base64 编码

那么问题来了,如果图片大于 8kb 呢?
将 background 的图片改成 test02.jpg:
1 | body { |
再次打包时,会报一下错误:

这是因为大于 8kb 的图片,会通过 file-loader 进行处理,但项目中并没有 file-loader,所以需要安装一下 file-loader
- 安装 file-loader
file-loader 将文件发送到输出文件夹,并返回(相对)URL
1 | npm install --save-dev file-loader |
再次打包运行,会发现 dist 文件夹下多了一个图片文件:

但是,我们发现图片并没有显示出来,这是因为图片使用的路径不正确
默认情况下,webpack 会将生成的路径直接返回给使用者,但我们整个程序是打包在 dist 文件夹下的,所以这里需要在路径下再添加一个 dist/ :

再次打包运行,发现 test02.jpg 的背景能够显示出来了:

- 修改图片文件名称
webpack 自动帮助我们为图片生成了一个非常长的名字,这是一个32位 hash 值,目的是防止名字重复
但在真实开发中,我们可能对打包的图片名字有一定的要求,比如:将所有的图片放在一个文件夹中,跟上图片原来的名称,同时也要防止重复
所以,我们可以在 options 中添加如下选项:
- img:文件要打包到的文件夹
- name:获取图片原来的名字,放在该位置
- hash:8:为了防止图片名称冲突,依然使用 hash,但是我们只保留8位
- ext:使用图片原来的扩展名

再次打包,发现在 dist/img 文件夹中的图片是按照我们预先设定的名字命名:

babel-loader
webpack 打包的 js 文件中,ES6 语法并没有转成 ES5,那么就意味着可能一些对 ES6 还不支持的浏览器没有办法很好的运行我们的代码:

如果希望将 ES6 的语法转成 ES5,那么就需要使用 babel
在webpack中,直接使用 babel 对应的 loader 就可以了
- 安装 babel-loader
1 | npm install --save-dev babel-loader@7 babel-core babel-preset-es2015 |
- 配置 webpack.config.js 文件
1 | { |
重新打包,查看 bundle.js 文件,发现其中的内容变成了 ES5 的语法:

webpack 中配置 vue
webpack 中基本配置 vue
- 安装 vue
如果希望在项目中使用 Vue.js,那么必然需要对其有依赖,所以需要先进行安装:
1 | npm i vue -S |
注:因为我们后续是在实际项目中也会使用 vue 的,所以并不是开发时依赖
- 在 main.js 中引入 vue,创建 vue 实例
1 | // 引用 vue |
- 在 index.html 中挂载 vue 实例
1 | <div id="app"> |
修改完成后,重新打包,运行程序,
发现打包过程没有任何错误,但是运行程序,没有出现想要的效果
这是因为目前我们使用的 Vue 版本为:runtime-only,这里需要修改为:runtime-compiler
修改方法就是在 webpack.config.js 中添加如下代码:
1 | resolve: { |

修改之后,再次打包运行,发现挂载的内容生效:

el 和 template 的区别
在之前的 vue 代码中,不管是想让 data 中的数据显示在界面中还是想修改自定义组件,都必须修改 index.html ,但有时候,我们并不希望手动的来频繁修改,这时可以定义 template 属性
- 先将 index.html 中挂载的 vue 代码进行修改(注释掉要显示的数据,只保留一个基本的 id 为 div 的元素)

- 在 vue 实例中定义一个 template 属性

- 重新打包,运行程序

发现显示效果与之前一样
那么,el 和 template 模板的关系是什么呢?
在之前的学习中,我们知道 el 用于指定 Vue 要管理的 DOM,可以帮助解析其中的指令、事件监听等,而如果 Vue实例中同时指定了 template,那么 template 模板的内容会替换掉挂载的对应 el 的模板
这样的好处是在以后的开发中,当我们再次操作 index.html 时,只需要在 template 中写入对应的标签即可
还可以将 template 模板中的内容进行抽离,分成 template、script、style 三部分书写,使结构变得更加清晰
Vue 组件化开发
为了采用组件化的形式进行开发,可以将 vue 实例中的 template 和 data 抽离出来:
1 | const App = { |
此时,打包运行的结果与之前一样:

但着组件的增加,main.js 文件会变得越来越臃肿,于是我们可以将这段代码单独抽取出来,放到一个 js 文件中
在 src 文件夹中新建一个文件夹 vue,再在 vue 文件夹中新建一个 app.js 文件:

将上面抽离出来的代码放入 app.js 中,再将其导出:
1 | export default { |
然后只需要在 main.js 中引用一下就行了:

此时,再打包运行,也能实现之前的效果
但是一个组件以一个 js 对象的形式进行组织和使用的时候是非常不方便的:
- 编写template模块非常的麻烦
- 如果有样式的话,不知道写在哪里比较合适
于是,我们可以以一种全新的方式来组织一个 vue 的组件:.vue 文件
.vue 文件封装处理
安装 vue-loader 和 vue-template-compiler
1
npm install vue-loader@14.2.2 vue-template-compiler --save-dev
修改 webpack.config.js 的配置文件
1 | { |
- 在 vue 文件夹中新建 App.vue 文件
1 | <template> |
- 在 main.js 中引用 App.vue 文件
1 | import App from './vue/App.vue' |
打包运行:

plugin 的使用
什么是 plugin
- plugin 是插件的意思,通常是用于对某个现有的架构进行扩展
- webpack 中的插件,就是对 webpack 现有功能的各种扩展,比如打包优化,文件压缩等
loader 和 plugin 区别
- loader 主要用于转换某些类型的模块,它是一个转换器
- plugin 是插件,它是对 webpack 本身的扩展,是一个扩展器
plugin 的使用过程
- 通过 npm 安装需要使用的 plugins (某些 webpack 已经内置的插件不需要安装)
- 在 webpack.config.js 中的 plugins 中配置插件
添加版权的 plugin:BannerPlugin
BannerPlugin 属于 webpack 自带的插件,可以为打包的文件添加版权声明
- 按照下面的方式来修改 webpack.config.js 的文件:

- 重新打包程序,查看 bundle.js 文件的头部,可以看到如下信息:

打包 html 的 plugin:HtmlWebpackPlugin
目前,我们的 index.html 文件是存放在项目的根目录下的,但在真实发布项目时,发布的是 dist 文件夹中的内容,而 dist 文件夹中如果没有 index.html 文件,那么打包的 js 等文件也就没有意义了
所以,我们需要将 index.html 文件打包到 dist 文件夹中,这个时候可以使用 HtmlWebpackPlugin 插件
HtmlWebpackPlugin 插件可以为我们做这些事情:
- 自动生成一个 index.html 文件(可以指定模板来生成)
- 将打包的 js 文件,自动通过 script 标签插入到 body 中
- 安装 HtmlWebpackPlugin 插件
1 | npm install html-webpack-plugin --save-dev |
- 修改 webpack.config.js 文件中 plugins

- 修改模板 index.html
上面的 template 表示根据什么模板来生成 index.html,这里表示的是 src 目录中的 index.html,由于 HtmlWebpackPlugin 插件会将打包的 js 文件,自动通过 script 标签插入到 body 中,所以我们要去掉模板 index.html 中的多余部分:
- 需要删除之前在 output 中添加的 publicPath 属性
,否则插入的 script 标签中的 src 可能会有问题

- 重新打包执行,会发现 dist 目录中多了一个 index.html 文件


js 压缩的 Plugin:uglifyjs-webpack-plugin
在项目发布之前,我们必然需要对 js 等文件进行压缩处理
对打包的js文件进行压缩,可以使用一个第三方的插件 uglifyjs-webpack-plugin,并且版本号指定 1.1.1,和 CLI2 保持一致
- 安装 uglifyjs-webpack-plugin
1 | npm install uglifyjs-webpack-plugin@1.1.1 --save-dev |
- 修改 webpack.config.js 文件,使用插件

- 查看打包后的 bunlde.js 文件,发现已经被压缩过了

搭建本地服务器
webpack 提供了一个可选的本地开发服务器,这个本地服务器基于 node.js 搭建,内部使用 express 框架,可以实现让浏览器自动刷新显示我们修改后的结果,不过它是一个单独的模块,在webpack中使用之前需要先安装它
- 安装 webpack-dev-server
1 | npm install --save-dev webpack-dev-server@2.9.1 |
- webpack.config.js 文件配置修改

devserver 也是作为 webpack 中的一个选项,选项本身可以设置如下属性:
- contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
- port:端口号
- inline:页面实时刷新
- historyApiFallback:在SPA页面中,依赖HTML5的history模式
此时,可以通过命令:webpack-dev-server 启动服务器了
- 配置 package.json 中的 scripts,简化启动服务器的命令

– open 参数表示直接用默认浏览器打开
- 执行命令
nmp run dev启动服务器,打开网页

webpack 配置分离
由于我们目前所有的生产环境代码和发布环境代码都写在 webpack.config.json 中,这样当我们在编译的时候,会将生产环境代码和发布环境代码一起编译,但有时候我们希望将二者分开编译,比如:

这时候,将生产环境代码和发布环境代码抽离出来显得很有必要
- 新建一个文件夹 build,将所有编译环境代码放到里面,在 build 中新建三个文件:

- base.config.js:公共部分
1 | const path = require('path') |
- dev.config.js:开发部分
1 | module.exports = { |
- prod.config.js:生产部分
1 | const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin') |
- 安装对配置文件合并的插件:webpack-merge
1 | npm i webpack-merge --save-dev |
- 修改配置文件
- dev.config.js

- prod.config.js

- 删除原来的 webpack.config.js 文件
此时执行 npm run build 命令会报如下错误:

需要修改 package.json 文件中的 scripts 选项
- 修改 package.json 文件

此时可以打包成功了,但是发现打包的 bundle.js 的位置是在 built/dist 文件夹中,而不是在原来的 dist 中

这时因为在 base.config.js 中的 output 中的文件拼接路径是 'dist',表示的是从当前路径下拼接

需要将其修改为从上一级目录开始拼接:

Vue CLI
什么是 Vue CLI
如果你只是简单写几个 Vue 的 Demo 程序, 那么你不需要 Vue CLI
如果你在开发大型项目, 那么你需要, 并且必然需要使用 Vue CLI
使用 Vue.js 开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情,如果每个项目都要手动完成这些工作,效率比较低,所以通常我们会使用一些脚手架工具来帮助完成这些事情
CLI 是什么意思
CLI 是 Command-Line Interface , 翻译为命令行界面, 俗称脚手架
Vue CLI 是一个官方发布的 vue.js 项目脚手架
使用 vue-cli 可以快速搭建 Vue 开发环境以及对应的 webpack 配置
Vue CLI 使用前提:
- Node
- webpack
Vue CLI 的使用
安装 Vue 脚手架
1 | npm install -g @vue/cli |
注意:上面安装的是 Vue CLI3 的版本,如果需要想按照 Vue CLI2 的方式初始化项目,还需要拉取 2.x 的模板
拉取 2.x 模板(旧版本)
1 | npm i @vue/cli-init -g |
初始化项目
Vue CLI2 初始化项目
1 | vue init webpack vuecli2test |
vue cli 2 项目详解

vue cli 2 目录详解

Vue CLI3 初始化项目
1 | vue create vuecli3test |
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 cli 3 项目详解

vue cli 3 目录详解

CLI 相关配置
Runtime-Compiler 和 Runtime-only 的区别
构建项目时:

官方解释:

简单总结:
- 如果在之后的开发中,你依然使用 template,就需要选择 Runtime-Compiler
- 如果你之后的开发中,使用的是 .vue 文件开发,那么可以选择 Runtime-only
render 和 template

Vue 程序运行过程

render 函数的使用
npm run build

npm run dev

配置文件去向
在 Vue CLI 3 中,webpack 等相关配置文件被隐藏起来了,如果想查看或修改相关配置,可以通过以下3种方式:
- 从 UI 界面上修改
- 启动配置服务器:
vue ui - 进入 Vue 项目管理器,导入我们的项目:

- 然后可以在配置中查看或修改我们的 webpack 等配置:

- 在 node_modules 中寻找

- 在项目中新建一个 vue.config.js 文件,将需要修改的配置代码写入其中

最终编译时会自动将我们添加的代码与隐藏的代码进行合并
Vue-router
认识路由
什么是路由
路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动 — 维基百科
路由器提供了两种机制, 路由和转发
- 路由是决定数据包从来源到目的地的路径
- 转发将输入端的数据转移到合适的输出端
网站发展的几个阶段
后端路由阶段
什么是后端路由
早期的网站开发,整个 HTML 页面都是是由服务器来渲染的,服务器直接生产渲染好对应的 HTML 页面, 返回给客户端进行展示
但是, 服务器如何处理一个网站的诸多页面呢?
首先,一个页面会有自己对应的网址, 也就是 URL,客户端发生请求时,URL 会发送到服务器,服务器通过正则对该URL 进行匹配且最后交给 Controller 进行处理,Controller 进行各种处理后,最终生成 HTML 或者数据,返回给前端,这就完成了一个IO操作,这种操作, 就是后端路由
后端路由的优点
当页面中需要请求不同的路径内容时,交给服务器来进行处理, 服务器渲染好整个页面,并且将页面返回给客户端,
这种情况下渲染好的页面,不需要单独加载任何的 js 和 css,可以直接交给浏览器展示,这样也有利于 SEO 的优化
后端路由的缺点
**
- 整个页面的模块都要由后端人员来编写和维护,工作量太大
- 前端开发人员如果要开发页面,需要通过 PHP 和 Java 等语言来编写页面代码,增加了额外的学习成本
- HTML 代码和数据以及对应的逻辑混在一起,,不利于编写和维护
前端路由阶段
前端路由的核心:改变URL,但是页面不进行整体的刷新
前后端分离阶段
随着 Ajax 的出现,有了前后端分离的开发模式:后端只提供 API 来返回数据,前端通过 Ajax 获取数据,并且可以通过 JavaScript 将数据渲染到页面中
优点:
- 前后端责任变得很清晰,后端专注于数据上, 前端专注于交互和可视化上
- 当移动端(iOS/Android)出现后,后端不需要进行任何处理, 依然使用之前的一套API即可
单页面富应用阶段
单页面富应用,即单页Web应用(single page web application,SPA),就是只有一张 Web 页面的应用,是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序
简单理解:就是在前后端分离的基础上加了一层前端路由
SPA的特点
**
- 速度:更好的用户体验,让用户在 web app 感受 native app 的速度和流畅
- ·MVVM:经典 MVVM 开发模式,前后端各负其责
- ·ajax:重前端,业务逻辑全部在本地操作,数据都需要通过AJAX同步、提交
- ·路由:在 URL 中采用 # 号来作为当前视图的地址,改变 # 号后的参数,页面并不会重载
SPA 缺点
**
- 首屏渲染等待时长: 必须得加载完毕,才能渲染出首屏
- seo不友好:爬虫只能拿到一个 div,认为页面是空的,不利于 seo
- 初次加载耗时多:为实现单页Web应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面可以在需要的时候加载,所以必须对 JavaScript 及 CSS 代码进行合并压缩处理,如果使用第三方库,建议使用一些大公司的 CDN,因此带宽的消耗是必然的
SPA 优点
- 良好的交互体验
:用户不需要重新刷新页面,获取数据也是通过 Ajax 异步获取,页面显示流畅 - 良好的前后端工作分离模式:单页 Web 应用可以和 RESTful 规约一起使用,通过 REST API 提供接口数据,并使用 Ajax 异步获取,这样有助于分离客户端和服务器端工作,更进一步,可以在客户端也可以分解为静态页面和页面交互两个部分
- 减轻服务器压力:服务器只用出数据就可以,不用管展示逻辑和页面合成,吞吐能力会提高几倍
- 共用一套后端程序代码
:不用修改后端程序代码就可以同时用于 Web 界面、手机、平板等多种客户端
改变 URL,页面不刷新
URL 的 hash
URL 的 hash 也就是锚点(#),本质上是改变 window.location 的 href 属性,可以通过直接赋值 location.hash 来改变 href,但是页面不发生刷新

HTML5 的 history 模式
history 接口是 HTML5 新增的,它有五种模式改变 URL 而不刷新页面
- history.pushState()

- history.replaceState()
- history.forward()
- history.back()
- history.go()

Vue-router 基本使用
目前前端流行的三大框架,都有自己的路由实现:
- Angular:ngRouter
- React:ReactRouter
- Vue:vue-router
vue-router 是 Vue.js 官方的路由插件,它和 vue.js 是深度集成的,适合用于构建单页面应用
vue-router 是基于路由和组件的,路由用于设定访问路径, 将路径和组件映射起来,在 vue-router 的单页面应用中, 页面路径的改变就是组件的切换
安装 vue-router
直接使用 npm 来安装路由即可
- 安装 vue-router
1 | npm install vue-router --save |
- 在模块化工程中使用
因为 vue-router 是一个插件,所以可以通过 Vue.use() 来安装路由功能
- 导入路由对象,并且调用 Vue.use(VueRouter)
- 创建路由实例,并且传入路由映射配置
- 在 Vue 实例中挂载创建的路由实例
使用 vue-router
- 创建路由组件
- 创建 router 实例

- 挂载到 Vue 实例中

- 配置路由映射: 组件和路径映射关系
- 新建两个组件

- 为组件配置路由映射关系

- 使用路由: 通过
<router-link>和<router-view>
<router-link>: 该标签是一个 vue-router 中已经内置的组件, 它默认会被渲染成一个<a>标签<router-view>: 该标签会根据当前的路径,动态渲染出不同的组件

最终效果如下:

细节处理
路由的默认路径
默认情况下, 进入网站的首页,我们希望 <router-view> 渲染首页的内容,但是在上面的实现中,默认没有显示首页组件,必须让用户点击才可以
如何可以让路径默认跳到到首页,并且<router-view>渲染首页组件呢?
只需要多配置一个映射就可以了:

我们在 routes 中又配置了一个映射:
- path:根路径
/ - redirect:重定向,也就是将根路径重定向到
/home的路径下
这样,打开页面时,就会默认显示首页的内容了
HTML5 的 History 模式
前面说过改变路径的方式有两种:
- URL 的 hash
- HTML5 的 history
默认情况下,Vue 路径的改变使用的是 URL 的 hash,这样显示出的页面的地址中有一个 # 号,不太美观:

可以使用 HTML5 的 history 模式来进行改变,进行如下配置即可:


router-link 补充
在前面的 <router-link> 中,我们只是使用了一个属性:to,用于指定跳转的路径
<router-link> 还有一些其他属性:
- tag:tag 可以指定
<router-link>之后渲染成什么组件,默认是渲染为<a></a>标签


- replace: replace 不会留下 history 记录,所以指定 replace 的情况下, 后退键返回不能返回到上一个页面中
1 | <router-link to="/home" tag="button" replace>首页</router-link> |
- active-class: 当
<router-link>对应的路由匹配成功时,会自动给当前元素设置一个router-link-active的class

设置 active-class 可以修改默认的名称:
1 | <router-link to="/home" tag="button" replace active-class="active">首页</router-link> |

在进行高亮显示的导航菜单或者底部 tabbar 时,会使用到该类,比如想设置按钮点击时变为红色:


该 class 具体的名称也可以通过 router 实例的属性进行修改:

但是通常不会修改类的属性, 会直接使用默认的 router-link-active 即可
路由代码跳转
**
有时候,页面的跳转可能需要执行对应的 JavaScript 代码,这个时候,就可以使用第二种跳转方式了
比如,我们将代码修改如下:

动态路由
在某些情况下,一个页面的 path 路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径:/user/aaaa 或 /user/bbbb,除了有前面的 /user之外,后面还跟上了用户的 ID
这种 path 和 Component 的匹配关系,称之为动态路由(也是路由传递数据的一种方式)
- 在
vue-router的路由路径中使用“动态路径参数”(dynamic segment) :

- 在组件中手动绑定一个用户 ID:

- 更新
User的模板,输出当前用户的 ID:

- 查看显示效果:

路由的懒加载
认识路由的懒加载
当打包构建应用时,Javascript 包会变得非常大,影响页面加载,如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
为了实现这种效果,我们可以使用路由的懒加载
路由懒加载的主要作用就是将路由对应的组件打包成一个个的 js 代码块,只有在这个路由被访问到的时候,才加载对应的组件
懒加载的三种方式
- 结合 Vue 的异步组件和 Webpack 的代码分析
1 | const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) })}; |
- AMD 写法
1 | const About = resolve => require(['../components/About.vue'], resolve); |
- 在ES6中,可以用更加简单的写法来组织 Vue 异步组件和 Webpack 的代码分割
1 | const Home = () => import('../components/Home.vue') |
路由懒加载的效果
**
Vue-router 嵌套路由
嵌套路由是一个很常见的功能
,比如在 home 页面中,我们希望通过 /home/news 和 /home/message 访问一些内容,一个路径映射一个组件,访问这两个路径也会分别渲染两个组件
路径和组件的关系如下:

实现嵌套路由有两个步骤:
- 创建对应的子组件,并且在路由映射中配置对应的子路由
- 定义两个子组件

- 配置子组件的路由

- 配置嵌套路由的默认路径

- 在父组件内部显示子组件

- 查看显示效果
Vue-router 参数传递
准备工作
为了演示传递参数,再创建一个组件,并且将其配置好
- 创建新的组件 Profile.vue

- 配置路由映射

- 添加跳转的
<router-link>

传递参数的方式
传递参数主要有两种类型: params 和 query
params
- 配置路由格式:
/router/:id - 传递的方式:在 path 后面跟上对应的值
- 传递后形成的路径:
/router/123,/router/abc
- 配置路由格式:
query
- 配置路由格式:
/router - 传递的方式:对象中使用 query 的 key 作为传递方式
- 传递后形成的路径:
/router?id=123,/router?id=abc
- 配置路由格式:



获取参数
**
获取参数是通过 route 对象获取的,在使用了 vue_−_router 的应用中,路由对象会被注入每个组件中,赋值为_ `this_.route` ,并且当路由切换时,路由对象会被更新
route 和 router 的区别
_
- router 为_ VueRouter 实例,想要导航到不同 URL_,则使用
router.push方法 - $route 为当前 router 跳转对象,里面可以获取 name、path、query、params 等
Vue-router 导航守卫
在一个 SPA 应用中,如何改变网页的标题呢?
普通的修改方式:在每一个路由对应的组件 .vue 文件中,通过 mounted 声明周期函数,执行对应的代码进行修改
但是当页面比较多时,需要在多个页面执行类似的代码,所以这种方式不容易维护
更好的办法是使用导航守卫
什么是导航守卫
导航守卫主要用来监听路由的进入和离开,vue-router 提供了 beforeEach 和 afterEach 的钩子函数,它们会在路由即将改变前和改变后触发
导航守卫使用
我们可以利用 beforeEach 来完成标题的修改:
- 在钩子当中利用 meta 来定义标题

- 利用导航守卫,修改标题

- 查看修改效果

导航钩子的三个参数解析
- to: 即将要进入的目标的路由对象
- from: 当前导航即将要离开的路由对象
- next: 调用该方法后,才能进入下一个钩子
导航守卫补充
- 如果是后置钩子,也就是afterEach,不需要主动调用 next() 函数
- 上面使用的导航守卫,被称之为全局守卫,除此之外,还有路由独享的守卫、组件内的守卫
keep-alive
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染
router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存:
1 | <keep-alive> |
现在有这样一个需求:首页正正在显示的是被嵌套的消息路由,当我们点击其他页面,又重新点击首页时,让首页仍然显示被嵌套的消息路由

- 先取消嵌套路由的默认路径

- 在 App.vue 中使用
<keep-alive></keep-alive>包裹<router-view/>

- 在 Home.vue 中记录离开之前的路径

keep-alive 还有两个非常重要的属性:
- include - 字符串或正则表达,只有匹配的组件会被缓存
- exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
让部分组件不缓存:
1 | <keep-alive exclude="Profile,User"><router-view/></keep-alive> |
Vuex
认识 Vuex
什么是 Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
- Vuex 采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
- Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能
状态管理
什么是状态管理
简单理解:把需要多个组件共享的变量全部存储在一个对象里面,然后将这个对象放在顶层的 Vue 实例中,让其他组件可以使用,共享这个对象中的所有变量属性,并且是响应式的
管理什么状态
- 用户的登录状态、用户名称、头像、地理位置信息等
- 商品的收藏、购物车中的物品等
单界面的状态管理
在单个组件中进行状态管理是一件非常简单的事情,如图:

- State:状态(姑且可以当做是 data 中的属性)
- View:视图层,可以针对 State 的变化,显示不同的信息
- Actions:用户的各种操作:点击、输入等,会导致状态的改变
单界面状态管理的实现

在这个案例中:
- counter 需要某种方式被记录下来,也就是 State
- counter 目前的值需要被显示在界面中,也就是我们的 View
- 界面发生某些操作时(这里是用户的点击,也可以是用户的 input),需要去更新状态,也就是 Actions
多界面状态管理
Vue 已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?
多个视图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
全局单例模式(大管家)
现在要做的就是将共享的状态抽取出来,交给大管家统一进行管理,之后每个试图按照规定进行访问和修改等操作,这就是 Vuex 的基本思想
Vuex 状态管理图例

Vuex 的基本使用
简单的案例
**
还是实现一下之前简单的案例:

首先,我们需要在某个地方存放我们的 Vuex 代码:
- 安装 Vuex:
npm i vuex --save - 创建一个文件夹 store,并且在其中创建一个 index.js 文件

- 在 index.js 文件中写入如下代码
1 | import Vue from 'vue' |
挂载到 Vue 实例中
**
其次,我们让所有的 Vue 组件都可以使用这个 store 对象
来到 main.js 文件,导入 store 对象,并且放在 new Vue 中:
1 | import Vue from 'vue' |
此后,在其他 Vue 组件中,可以通过 this.$store 的方式,获取到 store 对象
使用 Vuex 的 count
**
1 | <template> |
**
这就是使用 Vuex 最简单的方式了:
- 提取出一个公共的 store 对象,用于保存在多个组件中共享的状态
- 将 store 对象放置在 new Vue 对象中,这样可以保证在所有的组件中都可以使用到
- 在其他组件中使用 store 对象中保存的状态即可
- 通过
this.$store.state.属性的方式来访问状态 - 通过
this.$store.commit(‘Mutations中方法’)来修改状态
- 通过
注意事项:
我们通过提交 Mutations 的方式,而非直接改变 store.state.count ,这是因为 Vuex 可以更明确的追踪状态的变化,所以不要直接改变 store.state.count 的值。
**
Vuex 核心概念
State 单一状态树
Vuex 提出使用单一状态树(Single Source of Truth),也可以翻译成单一数据源
如果状态信息是保存到多个 Store 对象中的,那么之后的管理和维护等等都会变得特别困难,所以 Vuex 使用了单一状态树来管理应用层级的全部状态
单一状态树能够让我们以最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护
Getters
有时候,我们需要从 store 中获取一些 state 变异后的状态,则可以使用 getters
Getters 基本使用
比如,现在 Store 中有这样的学生信息:
1 | const store = new Vuex.Store({ |
现要求获取年龄小于20的学生个数,可以在 Store 中定义 getters:
1 | getters: { |
**
之后,在各个组件中想要使用这个 getters,只需要:
1 | <template> |
Getters 作为参数
**如果我们已经有了一个获取所有年龄小于20岁学生列表的 getters,那么可以这样来写:
1 | getters: { |
Getters 传递参数
**
getters 默认是不能传递参数的,如果希望传递参数,那么只能让 getters 本身返回另一个函数
比如上面的案例中,我们希望根据 ID 获取用户的信息:
1 | getters: { |
**
在组件中使用:
1 | <template> |
Mutations 状态更新
Vuex 的 store 状态的更新唯一方式:提交 Mutations
Mutations 组成
**
- 字符串的事件类型(type)
- 一个回调函数(handler),该回调函数的第一个参数就是 state
Mutations 的定义方式
1 | mutations: { |
通过 Mutations 更新
1 | methods:{ |
Mutations 传递参数
在通过 Mutations 更新数据的时候,有时候希望携带一些额外的参数,这种参数被称为 Mutations 的载荷(Payload)
比如之前的计数器案例,每次都是加减 1 ,现在想让加减的数字作为一个参数,传到 Mutations 的方法中:


如果有很多参数需要传递,通常会以对象的形式传递,也就是 payload 是一个对象,再从对象中取出相关的信息

Mutations 提交风格
上面的通过 commit 进行提交是一种普通的方式
Vue 还提供了另外一种风格, 它是一个包含 type 属性的对象

Mutations 中的处理方式是将整个 commit 的对象作为 payload 使用, 所以代码没有改变, 依然如下:

Mutations 响应规则
**
Vuex 的 store 中的 state 是响应式的, 当 state 中的数据发生改变时,Vue 组件会自动更新
这就要求我们必须遵守一些Vuex对应的规则:
- 提前在store中初始化好所需的属性
- 当给 state 中的对象添加新属性时,使用下面的方式:
- 使用
Vue.set(obj, 'newProp', 123) - 用新对象给旧对象重新赋值
- 使用
我们来看一个例子:
当我们点击更新信息时,界面并没有发生对应改变:

以上代码给 state 中的对象添加新属性时,由于不是响应式添加,所以界面不会更新,要想让界面更新,可以使用一下方式:
**
1 | updateInfo(state, payload) { |
**
此时,点击修改信息按钮后,界面和 info 里面的信息都会更新:
Mutations 常量类型
在 Mutations 中, 我们定义了很多事件类型(也就是其中的方法名称),当我们的项目不断增大时,会出现:
- Vuex 管理的状态越来越多,需要更新状态的情况越来越多
- Mutations 中的方法越来越多,使用者需要花费大量的经历去记住这些方法,甚至是多个文件间来回切换,查看方法名称,甚至出现写错的情况
如何避免上述的问题呢?
在各种 Flux 实现中,一种很常见的方案是:
- 使用常量替代 Mutations 事件的类型
- 将这些常量放在一个单独的文件中,方便管理以及让整个 app 所有的事件类型一目了然
具体操作:
- 创建一个文件: mutations-types.js,并且在其中定义我们的常量

定义常量时,我们可以使用 ES2015 中的风格,使用一个常量来作为函数的名称
- 使用定义的常量
Actions
通常情况下,Vuex 要求 Mutations 中的方法必须是同步方法,因为当我们使用 devtools 时,devtools 可以帮助我们捕捉 Mutations 的快照,但如果是异步操作, 那么 devtools 将不能很好的追踪这个操作什么时候会被完成
比如之前的代码,当执行修改信息操作时,devtools 中会有如下信息:

但是,如果 Vuex 中的代码使用了异步函数:
1 | mutations: { |
这时会发现 state 中的 info 数据一直没有被改变:

这是因为 devtools 无法追踪到异步操作
虽然强调不要再 Mutations 中进行异步操作,但某些情况,,又确实希望在 Vuex 中进行一些异步操作,比如网络请求,这时可以使用 Action
Action 类似于 Mutations,是用来代替 Mutations 进行异步操作的
在 Action 中,可以将异步操作放在一个 Promise 中,并且在成功或者失败后,调用对应的 resolve 或 reject:
Modules
Modules 基本使用
Modules 是模块的意思,为什么在Vuex中我们要使用模块呢?
Vue 使用单一状态树,意味着很多状态都会交给 Vuex 来管理,当应用变得非常复杂时,store 对象就有可能变得相当臃肿,为了解决这个问题,Vuex 允许我们将 store 分割成模块(Module), 每个模块拥有自己的 state、mutations、actions、getters 等
我们按照什么样的方式来组织模块呢?
**
1 | const ModuleA = { |
Modules 中的 state
要想知道如何调用 Modules 中的 state,需要先了解一下 Modules 中的 module 真实位置
其实,在 store 实例的 modules 中定义的 a 和 b 会被放到 store 的 state 中:
在 devtools 中也能看到:
**
既然 Module 被放到了 store 的 state 中,那么在其他组件中就可以使用 this.$store.state 来调用了:

Modules 中的 mutations
虽然 mutations 是定义在模块中的,但是在组件中提交时还是使用:this.$store.commit

**
Modules 中的 getters

Modules 中的 actions

项目结构
当 Vuex 帮助我们管理过多的内容时,好的项目结构可以让我们的代码更加清晰:

网络模块封装
网络模块的选择
传统的 Ajax
- 传统的 Ajax 基于 XMLHttpRequest(XHR)
- 之所以不用它,是因为它配置和调用方式等非常混乱,编码起来看起来令人十分头疼
- 真实开发中很少直接使用传统的 Ajax , 而是使用 jQuery-Ajax
jQuery-Ajax
- jQuery-Ajax 相对于传统的 Ajax 非常好用
- 之所以不用它,是因为 jQuery 是一个重量级的框架,代码有 1w+ 行,而 Vue 的代码才 1w+ 行,所以没必要为了网络请求,特意引用一个 jQuery
Vue-resource
- 官方在 Vue1.x 的时候, 推出了 Vue-resource
- Vue-resource 的体积相对于 jQuery 小很多
- 之所以不用它,是因为在 Vue2.0 推出后,Vue 的作者就在 GitHub 的 Issues 中说明了去掉 vue-resource,并且以后也不会再更新,意味着以后 vue-reource 不再支持新的 Vue 版本,也不会再继续更新和维护,对以后的项目开发和维护都存在很大的隐患
axios
- 在说明不再继续更新和维护 vue-resource 的同时,作者还推荐了一个框架:axios
- axios有非常多的优点, 并且用起来也非常方便
axios
axios 功能特点
- 在浏览器中发送 XMLHttpRequests 请求
- 在 node.js 中发送 http请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
axios 请求方式
axios(config)axios.request(config)axios.get(url[, config])axios.delete(url[, config])axios.head(url[, config])axios.post(url[, data[, config]])axios.put(url[, data[, config]])axios.patch(url[, data[, config]])
发送 get 请求

发送并发请求
**
使用 axios.all,可以放入多个请求的数组
axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2
1 | axios.all([axios.get('http://123.207.32.32:8000/home/multidata'), |
全局配置
在开发中,可能很多参数都是固定的,这个时候,我们可以进行一些抽取,也可以利用 axios 的全局配置
1 | axios.defaults.baseURL = '123.207.32.32:8000' |
axios 实例
为什么要创建 axios 的实例呢?
当我们从 axios 模块中导入对象时,使用的实例是默认的实例,当给该实例设置一些默认配置时,这些配置就被固定下来了,但是后续开发中,某些配置可能会不太一样,比如某些请求需要使用特定的 baseURL 或者 timeout 或者content-Type 等,这时我们就可以创建新的实例,并且传入属于该实例的配置信息
1 | // 创建新的实例 |
axios 封装
**
- 封装 axios

- 请求 axios
拦截器
axios 提供了拦截器,用于我们在发送每次请求或者得到相应后,进行对应的处理
如何使用拦截器呢?
拦截器中都做什么呢?
请求拦截可以做到的事情:
**请求拦截中错误拦截较少,通常都是配置相关的拦截
比如请求超时,可以将页面跳转到一个错误页面中
响应拦截中完成的事情:
响应的成功拦截中,主要是对数据进行过滤

响应的失败拦截中,可以根据 status 判断报错的错误码,跳转到不同的错误提示页面:
ES6 补充
JS 高阶函数:filter、map、reduce
1 | <script> |
Promise
Promise 是 ES6 中一个非常重要和好用的特性,是异步编程的一种解决方案
通常在网络请求时,我们会处理异步事件,因为不能立即拿到结果,所以往往会传入另外一个函数,在数据请求成功时,将数据通过传入的函数回调出去
如果只是一个简单的网络请求,那么这种方案不会给我们带来很大的麻烦,但当网络请求非常复杂时,就会出现回调地狱
网络请求的回调地狱
我们来考虑下面的场景:
- 通过一个 url1 从服务器加载一个数据 data1,data1 中包含了下一个请求的 url2
- 再通过 data1 取出 url2,从服务器加载数据 data2,data2 中包含了下一个请求的 url3
- 再通过 data2 取出 url3,从服务器加载数据 data3,data3 中包含了下一个请求的 url4
- 最后发送网络请求 url4,获取最终的数据 data4

正常情况下,上面的代码不会有什么问题,可以正常运行,并且获取我们想要的结果
但是,这样的代码难看而且不容易维护
我们期望的是以一种更加优雅的方式来进行这种异步操作:使用 Promise
定时器的异步事件
用一个定时器来模拟异步事件:

假设下面的 data 是从网络上 1 秒后请求的数据,console.log 就是我们的处理方式
这是过去的处理方式,我们将它换成 Promise 代码:

这个例子会让我们感觉使用 Promise 有些多此一举,因为下面的 Promise 代码明显比上面的代码看起来还要复杂
定时器异步事件解析
我们先来认认真真的读一读这个程序到底做了什么?
new Promise创建了一个 Promise 对象- 小括号中
((resolve, reject) => {})是一个箭头函数- 在创建 Promise 时,传入的这个箭头函数是固定的(一般我们都会这样写)
- resolve 和 reject 也是函数,通常情况下,会根据请求数据的成功和失败来决定调用哪一个
- 如果是成功的,通常会调用
resolve(messsage),这时,后续的 then 会被回调 - 如果是失败的,通常会调用
reject(error),这时,后续的 catch 会被回调
- 如果是成功的,通常会调用
Promise 三种状态
当开发中有异步操作时,可以给异步操作包装一个 Promise,异步操作之后会有三种状态:
- pending:等待状态,比如正在进行网络请求,或者定时器没有到时间
- fulfill:满足状态,当我们主动回调了 resolve 时,就处于该状态,并且会回调
.then() - reject:拒绝状态,当我们主动回调了 reject 时,就处于该状态,并且会回调
.catch()

以上代码的另一种写法:

Primose 的链式调用
1 | <script> |
链式调用简写
将数据直接包装成 Promise.resolve,在 then 中直接返回:
1 | <script> |
promise 的 all 方法的使用
1 | <script> |














