使用vue技术栈,作为一个前端架构师是必须掌握这些知识点的

4,473 阅读33分钟

Vue

一.对于mvvm的理解 1.MVVM 是 Model-View-ViewModel 的缩写 2.Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑 3.View代表UI 组件,它负责将数据模型转化成UI 展现出来 4.ViewModel监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。

二.vue的生命周期(11个,重点8个) 1.beforeCreate(创建前) 在数据观测和初始化事件还未开始

2.created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来

3.beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上

4.mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。

5.beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。

6.updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

7.beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。

8.destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

三.Vue实现数据双向绑定的原理:Object.defineProperty()

Vue实现数据双向绑定的三大对象Observer(Object.defineProperty中的getter,每当数据发生变化,就会触发setter,这时候Observer就要通知订阅者,订阅者就是Watcher)、Watcher(Watcher订阅者作为Observer和Compile之间通信的桥梁)、Compile

Watcher: 1.在自身实例化时往属性订阅器(dep)里面添加自己 2.自身必须有一个update()方法 3.待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调

Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

history模式和hash模式 页面404的情况,我们前端需要自己处理 routes: [ { path: '*', component: NotFoundComponent } ]

何如访问vue实例 beforeRouteEnter(to, from, next) { next(vm => { console.log('vm', vm) }) }

activated:keep-alive 组件激活时调用,该钩子在服务器端渲染期间不被调用

deactivated:keep-alive 组件停用时调用,该钩子在服务器端渲染期间不被调用

beforeCreate 实例创建前 created 实例创建完成 beforeMount 挂载前 mounted 挂载完成 beforeUpdate 更新前 updated 更新完成 beforeDestory 销毁前 destoryed 销毁完成

1.beforeCreate 这个钩子是new Vue()之后触发的第一个钩子,在当前阶段中data、methods、computed以及watch上的数据和方法均不能被访问。

2.created 这个钩子在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,注意请求数据不易过多,会造成白屏时间过长。在当前阶段无法与Dom进行交互,如果你非要想,可以通过vm.$nextTick来访问Dom。

3.beforeMounted 这个钩子发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。

4.mounted 这个钩子在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$ref属性对Dom进行操作。也可以向后台发送请求,拿到返回数据。

5.beforeUpdate 这个钩子发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

6.updated 这个钩子发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

7.beforeDestroy 这个钩子发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

8.destroyed 这个钩子发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

最近利用空闲时间又翻看了一遍Vue的源码,只不过这次不同的是看了Flow版本的源码。说来惭愧,最早看的第一遍时对Flow不了解,因此阅读的是打包之后的vue文件,大家可以想象这过程的痛苦,没有类型的支持,看代码时摸索了很长时间,所以我们这次对Vue源码的剖析是Flow版本的源码,也就是从Github上下载下来的源码中src目录下的代码。不过,在分析之前,我想先说说阅读Vue源码所需要的一些知识点,掌握这些知识点之后,相信再阅读源码会较为轻松。

1. 前置知识点

我个人认为要想深入理解Vue的源码,至少需要以下知识点:

vue源码前置知识点.png

下面咱们一一介绍

1.1 Flow基本语法

相信大家都知道,javascript是弱类型的语言,在写代码灰常爽的同时也十分容易犯错误,所以Facebook搞了这么一个类型检查工具,可以加入类型的限制,提高代码质量,举个例子:

function sum(a, b) {
  return a + b;
}

可是这样,我们如果这么调用这个函数sum('a', 1) 甚至sum(1, [1,2,3])这么调用,执行时会得到一些你想不到的结果,这样编程未免太不稳定了。那我们看看用了Flow之后的结果:

function sum(a: number, b:number) {
  return a + b;
}

我们可以看到多了一个number的限制,标明对a和b只能传递数字类型的,否则的话用Flow工具检测会报错。其实这里大家可能有疑问,这么写还是js吗? 浏览器还能认识执行吗?当然不认识了,所以需要翻译或者说编译。其实现在前端技术发展太快了,各种插件层出不穷--Babel、Typescript等等,其实都是将一种更好的写法编译成浏览器认识的javascript代码(我们以前都是写浏览器认识的javascript代码的)。我们继续说Flow的事情,在Vue源码中其实出现的Flow语法都比较好懂,比如下面这个函数的定义:

export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode>{
...
}

val是any代表可以传入的类型是任何类型, keyOrIndex是string|number类型,代表要不是string类型,要不是number,不能是别的;index?:number这个我们想想正则表达式中?的含义---0个或者1个,这里其实意义也是一致的,但是要注意?的位置是在冒号之前还是冒号之后--因为这两种可能性都有,上面代码中问号是跟在冒号前面,代表index可以不传,但是传的话一定要传入数字类型;如果问号是在冒号后面的话,则代表这个参数必须要传递,但是可以是数字类型也可以是空。这样是不是顿时感觉严谨多了?同时,代码意义更明确了。为啥这么说呢? 之前看打包后的vue源码,其中看到观察者模式实现时由于没有类型十分难看懂,但是看了这个Flow版本的源码,感觉容易懂。 当然,如果想学习Flow更多的细节, 可以看看下面这个学习文档: Flow学习资料

1.2 原型与原型继承

Vue中的组件相信大家都使用过,并且组件之中可以有子组件,那么这里就涉及到父子组件了。组件其实初始化过程都是一样的,显然有些方法是可以继承的。Vue代码中是使用原型继承的方式实现父子组件共享初始化代码的。所以,要看懂这里,需要了解js中原型的概念;这里不多谈,只是提供几个学习资料供大家参考: 廖雪峰js教程 js原型理解 1.3 Object.defineProperty 这个方法在js中十分强大,Vue正是使用了它实现了响应式数据功能。我们先瞄一眼Vue中定义响应式数据的代码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  .....
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

其中我们看到Object.defineProperty这个函数的运用,其中第一个参数代表要设置的对象,第二个参数代表要设置的对象的键值,第三个参数是一个配置对象,对象里面可以设置参数如下: value: 对应key的值,无需多言 configurable:是否可以删除该key或者重新配置该key enumerable:是否可以遍历该key writable:是否可以修改该key get: 获取该key值时调用的函数 set: 设置该key值时调用的函数 我们通过一个例子来了解一下这些属性:

 let x = {}
 x['name'] = 'vue'
 console.log(Object.getOwnPropertyDescriptor(x,'name'))

Object.getOwnPropertyDescriptor可以获取对象某个key的描述对象,打印结果如下:
{
    value: "vue",
    writable: true,
    enumerable: true,
    configurable: true
}

从上可知,该key对应的属性我们可以改写(writable:true),可以重新设置或者删除(configurable: true),同时可以遍历(enumerable:true)。那么让我们修改一下这些属性,比如configurable,代码如下:

Object.defineProperty(x, 'name', {
      configurable: false
})

执行成功之后,如果你再想删除该属性,比如delete x['name'],你会发现返回为false,即无法删除了。 那enumerable是什么意思呢?来个例子就明白了,代码如下:

let x = {}
x[1] = 2
x[2] = 4
Object.defineProperty(x, 2, {
     enumerable: false
})
for(let key in x){
    console.log("key:" + key + "|value:" +  x[key])
}

结果如下: key:1|value:2 为什么呢? 因为我们把2设置为不可遍历了,那么我们的for循环就取不到了,当然我们还是可以用x[2]去取到2对应的值得,只是for循环中取不到而已。这个有什么用呢?Vue源码中Observer类中有下面一行代码: def(value, 'ob', this);

这里def是个工具函数,目的是想给value添加一个key为__ob__,值为this,但是为什么不直接 value.ob = this 反而要大费周章呢? 因为程序下面要遍历value对其子内容进行递归设置,如果直接用value.__ob__这种方式,在遍历时又会取到造成,这显然不是本意,所以def函数是利用Object.defineProperty给value添加的属性,同时enumerable设置为false。 至于get和set嘛?这个就更强大了,类似于在获取对象值和设置对象值时加了一个代理,在这个代理函数中可以做的东西你就可以想象了,比如设置值时再通知一下View视图做更新。也来个例子体会一下吧: let x = {} Object.defineProperty(x, 1, { get: function(){ console.log("getter called!") }, set: function(newVal){ console.log("setter called! newVal is:" + newVal) } })

当我们访问x[1]时便会打印getter called,当我们设置x[1] = 2时,打印setter called。Vue源码正是通过这种方式实现了访问属性时收集依赖,设置属性时源码有一句dep.notify,里面便是通知视图更新的相关操作。

1.4 Vnode概念

Vnode,顾名思义,Virtual node,虚拟节点,首先声明,这不是Vue自己首创的概念,其实Github上早就有一个类似的项目:Snabbdom。我个人认为,Vue应该也参考过这个库的实现,因为这个库包含了完整的Vnode以及dom diff算法,甚至实现的具体代码上感觉Vue和这个库也是有点相像的。为啥要用Vnode呢?其实原因主要是原生的dom节点对象太大了,我们运行一下代码:

let dom = document.createElement('div');
for(let key in dom){
      console.log(key)
}

打印的结果灰常长!!!说明这个dom对象节点有点重量级,而我们的html网页经常数以百计个这种dom节点,如果采用之前的Jquery这种方式直接操作dom,性能上确实稍微low一点。所以snabbdom或者Vue中应用了Vnode,Vnode对象啥样呢? 看看Vue源码对Vnode的定义:

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string;
....
}

相比之下, Vnode对象的属性确实少了很多;其实光属性少也不见得性能就能高到哪儿去,另一个方面便是针对新旧Vnode的diff算法了。这里其实有一个现象:其实大多数场景下即便有很多修改,但是如果从宏观角度观看,其实修改的点不多。举个例子: 比如有以下三个dom节点A B C 我们的操作中依次会改成 B C D 如果采用Jquery的改法,当碰到第一次A改为B时,修改了一次,再碰到B改为C,又修改了一次,再次碰到C改为D,又又修改了一次,显然其实从宏观上看,只需要删除A,然后末尾加上D即可,修改次数得到减少;但是这种优化是有前提的,也就是说能够从宏观角度看才行。以前Jquery的修改方法在碰到第一次修改的时候,需要把A改为B,这时代码还没有执行到后面,它是不可能知道后面的修改的,也就是无法以全局视角看问题。所以从全局看问题的方式就是异步,先把修改放到队列中,然后整成一批去修改,做diff,这个时候从统计学意义上来讲确实可以优化性能。这也是为啥Vue源码中出现下述代码的原因: queueWatcher(this);

1.5 函数柯里化

函数柯里化是什么鬼呢?其实就是将多参数的函数化作多个部分函数去调用。举个例子:

function getSum(a,b){
      return a+b;
}

这是个两个参数的函数,可以直接getSum(1,2)调用拿到结果;然而,有时候并不会两个参数都能确定,只想先传一个值,另外一个在其他时间点再传入,那我们把函数改为:

function getSum(a){
      return function(b){
            return a+b;
      }
}

那我们如何调用这个柯里化之后的函数呢?

let f = getSum(2)
console.log(f(3))
console.log(getSum(2)(3)) //结果同上

可见,柯里化的效果便是之前必须同时传入两个参数才能调用成功而现在两个参数可以在不同时间点传入。那为毛要这么做嘛?Vue源码是这么应用这个特性的,Vue源码中有一个platform目录,专门存放和平台相关的源码(Vue可以在多平台上运行 比如Weex)。那这些源码中肯定有些操作是和平台相关的,比如会有些以下伪代码所表示的逻辑: if(平台A){ .... }else if(平台B){ .... }

可是如果这么写会有个小不舒服的地方,那就是其实代码运行时第一次走到这里根据当前平台就已经知道走哪一个分支了,而现在这么写必当导致代码再次运行到这里的时候还会进行平台判断,这样总感觉会多一些无聊的多余判断,因此Vue解决此问题的方式就是应用了函数柯里化技巧,类似声明了以下一个函数: function ...(平台相关参数){ return function(平台不相关参数){ 处理逻辑 } }

在Vue的patch以及编译环节都应用了这种方式,讲到那部分代码时我们再细致的看,读者提前先了解一下可以帮助理解Vue的设计。

1.6 Macrotask与Microtask

可能有的读者第一次听到这两个词,实际上这个和js的事件循环机制息息相关。在上面我们也提到,Vue更新不是数据一改马上同步更新视图的,这样肯定会有性能问题,比如在一个事件处理函数里先this.data = A 然后再this.data=B,如果要渲染两次,想想都感觉很low。Vue源码实际上是将更改都放入到队列中,同一个watcher不会重复(不理解这些概念不要紧,后面源码会重点介绍),然后异步处理更新逻辑。在实现异步的方式时,js实际提供了两种task--Macrotask与Microtask。两种task有什么区别呢?先从一个例子讲起:

console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
    Promise.resolve().then(function() {
        console.log('promise3');
    }).then(function() {
        console.log('promise4');
    });
}, 0);
Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});
console.log('script end');

以上代码运行结果是什么呢?读者可以思考一下,答案应该是:

script start
script end
promise1
promise2
setTimeout
promise3
promise4

简单可以这么理解,js事件循环中有两个队列,一个叫MacroTask,一个MircroTask,看名字就知道Macro是大的,Micro是小的(想想宏观经济学和微观经济学的翻译)。那么大任务队列跑大任务--比如主流程程序了、事件处理函数了、setTimeout了等等,小任务队列跑小任务,目前读者记住一个就可以--Promise。js总是先从大任务队列拿一个执行,然后再把所有小任务队列全部执行再循环往复。以上面示例程序,首先整体上个这个程序是一个大任务先执行,执行完毕后要执行所有小任务,Promise就是小任务,所以又打印出promise1和promise2,而setTimeout是大任务,所以执行完所有小任务之后,再取一个大任务执行,就是setTimeout,这里面又往小任务队列扔了一个Promise,所以等setTimeout执行完毕之后,又去执行所有小任务队列,所以最后是promise3和promise4。说的有点绕,把上面示例程序拷贝到浏览器执行一下多思考一下就明白了,关键是要知道上面程序本身也是一个大任务。一定要理解了之后再去看Vue源码,否则不会理解Vue中的nextTick函数。 推荐几篇文章吧(我都认真读完了,受益匪浅) Macrotask Vs Microtask 理解js中Macrotask和Microtask 阮一峰 Eventloop理解

1.7 递归编程算法

很多程序员比较害怕递归,但是递归真的是一种灰常灰常强大的算法。Vue源码中大量使用了递归算法--比如dom diff算法、ast的优化、目标代码的生成等等....很多很多。而且这些递归不仅仅是A->A这么简单,大多数源码中的递归是A->B->C...->A等等这种复杂递归调用。比如Vue中经典的dom diff算法:

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        } else {
          vnodeToMove = oldCh[idxInOld];
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined;
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }

上面代码是比较新旧Vnode节点更新孩子节点的部分源码,调用者是patchVnode函数,我们发现这部分函数中又会调用会patchVnode,调用链条为:patchVnode->updateChildren->patchVnode。同时,即便没有直接应用递归,在将模板编译成AST(抽象语法树)的过程中,其使用了栈去模拟了递归的思想,由此可见递归算法的重要性。这也难怪,毕竟不管是真实dom还是vnode,其实本质都是树状结构,本来就是递归定义的东西。我们也会单独拿出一篇文章讲讲递归,比如用递归实现一下JSON串的解析。希望读者注意查看。

1.8 编译原理基础知识

这恐怕比递归更让某些程序员蛋疼,但是我相信只要读者认真把Vue这部分代码看懂,绝对比看N遍编译原理的课本更能管用。我们看看Vue源码这里的实现:

  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }

上述代码首先通过parse函数将template编译为抽象语法树ast,然后对ast进行代码优化,最后生成render函数。其实这个过程就是翻译,比如gcc把c语言翻译为汇编、又比如Babel把ES6翻译为ES5等等,这里面的流程十分都是十分地相似。Vue也玩了这么一把,把模板html编译为render函数,什么意思呢?

   <li v-for="record in commits">
       <span class="date">{{record.commit.author.date}}</span>
   </li>

比如上面的html,你觉得浏览器会认识嘛?显然v-for不是html原生的属性,上述代码如果直接在浏览器运行,你会发现{{record.commit.author.date}}就直接展示出来了,v-for也没有起作用,当然还是会出现html里面(毕竟html容错性很高的);但是经过Vue的编译系统一编译生成一些函数,这些函数一执行就是浏览器认识的html元素了,神奇吧? 其实仅仅是应用了编译原理课本的部分知识罢了,这部分我们后面会灰常灰常详细的介绍源码,只要跟着看下来,必定会对编译过程有所理解。现在可以这么简单理解一下AST(抽象语法树),比如java可以写一个if判断,C语言也可以写,js、python等等也可以(如下所示): java: if(x > 5){ .... }

python: if x>5: ....

注意点 在使用生命周期时有几点注意事项需要我们牢记。 1.第一点就是上文曾提到的created阶段的ajax请求与mounted请求的区别:前者页面视图未出现,如果请求信息过多,页面会长时间处于白屏状态。 2.除了beforeCreate和created钩子之外,其他钩子均在服务器端渲染期间不被调用。 3.上文曾提到过,在updated的时候千万不要去修改data里面赋值的数据,否则会导致死循环。 4.Vue的所有生命周期函数都是自动绑定到this的上下文上。所以,你这里使用箭头函数的话,就会出现this指向的父级作用域,就会报错。原因下面源码部分会讲解。

Webpack

一.webpack的基本功能的介绍 Webpack是一款用户打包前端模块的工具。主要是用来打包在浏览器端使用的javascript的。 同时也能转换、捆绑、打包其他的静态资源,包括css、image、font file、template等。 个人认为它的优点就是易用,而且常用功能基本都有, 另外可以通过自己开发loader和plugin来满足自己的需求。 这里就尽量详细的来介绍下一些基本功能的使用。

二.运行webpack webpack需要编写一个config文件,然后根据这个文件来执行需要的打包功能。 我们现在来编写一个最简单的config。新建一个文件,命名为webpack-config.js。 config文件实际上就是一个Commonjs的模块。内容如下:

var webpack = require('webpack'); var path = require('path'); var buildPath = path.resolve(__dirname,"build"); var nodemodulesPath = path.resolve(__dirname,'node_modules');

var config = { //入口文件配置 entry:path.resolve(__dirname,'src/main.js'), resolve:{ extentions:["","js"]//当requrie的模块找不到时,添加这些后缀 }, //文件导出的配置 output:{ path:buildPath, filename:"app.js" } }

module.exports = config;

1.entry:配置要打包的文件的入口;可以配置多个入口文件

2.output:output参数是个对象,用于定义构建后的文件的输出。其中包含path和filename

3.module(loaders): 配置要使用的loader。对文件进行一些相应的处理。比如babel-loader可以把es6的文件转换成es5。 大部分的对文件的处理的功能都是通过loader实现的。loader就相当于gulp里的task。 loader可以用来处理在入口文件中require的和其他方式引用进来的文件。loader一般是一个独立的node模块,要单独安装。 loader项的配置: test: /.(js|jsx)$/,//注意是正则表达式,不要加引号,匹配要处理的文件 loader: 'eslint-loader',//要使用的loader,"-loader"可以省略 include: [path.resolve(__dirname, "src/app")],//把要处理的目录包括进来 exclude: [nodeModulesPath]//排除不处理的目录

module的配置: module: { preLoaders: [ { test: /.(js|jsx)/,
        loader: 'eslint-loader',
        include: [path.resolve(__dirname, "src/app")],
        exclude: [nodeModulesPath]
      },
    ],
    loaders: [
      {
        test: /\.(js|jsx)/, //正则表达式匹配 .js 和 .jsx 文件 loader: 'babel-loader?optional=runtime&stage=0',//对匹配的文件进行处理的loader exclude: [nodeModulesPath]//排除node module中的文件 } ] }

process.env,获取基本的全局环境

通过选择 development 或 production 之中的一个,来设置 mode 参数, 你可以启用相应模式下的 webpack 内置的优化

核心概念

entry output loader plugins 模式

webpack.config.js const config = { entry: './file.js' } module.exports = config

常见场景: 分离 应用程序和第三方库入口 但页面应用和多页面应用

高级进阶(使用CDN和资源的hash)

loader的定义 loader 用于对模块的源代码进行转换。

loader 可以使你在 import 或"加载"模块时预处理文件。

因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。

loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。

loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

plugins

插件目的在于解决 loader 无法实现的其他事

vuex

vuex是什么

Vuex 是一个专为 Vue.js 应用程序开发的** 状态管理模式** 。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

什么是“状态管理模式”?

首先,我们先看一下下面这个例子

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>{{ count }}</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的简单示意:

vuex.vuejs.org/flow.png

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

vuex.png

什么情况下我应该使用 Vuex?

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择

最简单的Store

具体的代码实现

const store = new Vuex.Store({
  state: {
    token: 'xxx12345'
  },
  mutations: {
    changeToken(state, token) {
      state.token = token
    }
  }
})
// 通过mutation去改变token的状态值
store.commit('changeToken', 'xxxx12345555')

再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。

由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。

vuex核心概念

State

** 单一状态树 ** Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突——在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中

** 在 Vue 组件中获得 Vuex 状态 **

那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

// 创建一个 tree 组件
const trees = {
  template: `<div>{{ tree }}</div>`,
  computed: {
    tree () {
      return store.state.tree
    }
  }
}

每当 store.state.tree 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。

然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):

const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { trees },
  template: `
    <div class="app">
      <trees></trees>
    </div>
  `
})

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。让我们更新下 trees 的实现:

const trees = {
  template: `<div>{{ tree }}</div>`,
  computed: {
    count () {
      return this.$store.state.tree
    }
  }
}

Getter

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

** 通过属性访问 ** Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

** 通过方法访问 ** 你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')

** 提交载荷(Payload)**

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):

mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
store.commit('increment', {
  amount: 10
})

** 对象风格的提交方式 **

store.commit({
  type: 'increment',
  amount: 10
})

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

** Mutation 需遵守 Vue 的响应规则 ** 既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

最好提前在你的 store 中初始化好所有所需属性。

当需要在对象上添加新属性时,你应该

使用 Vue.set(obj, 'newProp', 123), 或者

以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:

state.obj = { ...state.obj, newProp: 123 } ** 使用常量替代 Mutation 事件类型 ** 使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。

** Mutation 必须是同步函数 ** 一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

** 在组件中提交 Mutation ** 你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

Action

在 scrimba 上尝试这节课 Action 类似于 mutation,不同在于:

Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。 让我们来注册一个简单的 action:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

** 分发 Action ** Action 通过 store.dispatch 方法触发:

store.dispatch('increment') 乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:

actions: {
  checkout ({ commit, state }, products) {
    // 把当前购物车的物品备份起来
    const savedCartItems = [...state.cart.added]
    // 发出结账请求,然后乐观地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接受一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。

** 在组件中分发 Action ** 你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

** 组合 Action ** Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在你可以:

store.dispatch('actionA').then(() => {
  // ...
})
在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果我们利用 async / await,我们可以如下组合 action:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

** 模块的局部状态 **

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

** 命名空间 ** 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: { ... },
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: { ... },
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

** 在带命名空间的模块内访问全局内容(Global Assets)** 如果你希望使用全局 state 和 getter,rootState 和 rootGetter 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

** 在带命名空间的模块注册全局 action ** 若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

** 带命名空间的绑定函数 ** 当使用 mapState, mapGetters, mapActions 和 mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

** 给插件开发者的注意事项 ** 如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:

// 通过插件的参数对象得到空间名称 // 然后返回 Vuex 插件函数

export function createPlugin (options = {}) {
  return function (store) {
    // 把空间名字添加到插件模块的类型(type)中去
    const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')
  }
}

** 模块动态注册 ** 在 store 创建之后,你可以使用 store.registerModule 方法注册模块:

// 注册模块 myModule store.registerModule('myModule', { // ... }) // 注册嵌套模块 nested/myModule store.registerModule(['nested', 'myModule'], { // ... }) 之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。

模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync 插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。

你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

** 保留 state ** 在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('a', module, { preserveState: true })。

当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。

** 模块重用 ** 有时我们可能需要创建一个模块的多个实例,例如:

创建多个 store,他们公用同一个模块 (例如当 runInNewContext 选项是 false 或 'once' 时,为了在服务端渲染中避免有状态的单例) 在一个 store 中多次注册同一个模块 如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。

实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):

const MyReusableModule = {
  state () {
    return {
      foo: 'bar'
    }
  },
  // mutation, action 和 getter 等等...
}

vue-router

一、前言 要学习vue-router就要先知道这里的路由是什么?为什么我们不能像原来一样直接用标签编写链接哪?vue-router如何使用?常见路由操作有哪些?等等这些问题,就是本篇要探讨的主要问题。

想阅读更多优质文章请猛戳GitHub博客

二、vue-router是什么 这里的路由并不是指我们平时所说的硬件路由器,这里的路由就是SPA(单页应用)的路径管理器。再通俗的说,vue-router就是WebApp的链接路径管理系统。 vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系。

至于我们为啥不能用a标签,这是因为用Vue做的都是单页应用(当你的项目准备打包时,运行npm run build时,就会生成dist文件夹,这里面只有静态资源和一个index.html页面),所以你写的标签是不起作用的,你必须使用vue-router来进行管理。

三、vue-router实现原理 SPA(single page application):单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。单页面应用(SPA)的核心之一是: 更新视图而不重新请求页面;vue-router在实现单页面前端路由时,提供了两种方式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。

1、Hash模式: vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。

2、History模式: 由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。

//main.js文件中 const router = new VueRouter({ mode: 'history', routes: [...] }) 当你使用 history 模式时,URL 就像正常的 url,例如 yoursite.com/user/id,比较好… 不过这种模式要玩好,还需要后台配置支持。因为我们的应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问 oursite.com/user/id 就会返回 404,这就不好看了。 所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。

export const routes = [ {path: "/", name: "homeLink", component:Home} {path: "/register", name: "registerLink", component: Register}, {path: "/login", name: "loginLink", component: Login}, {path: "*", redirect: "/"}] 此处就设置如果URL输入错误或者是URL 匹配不到任何静态资源,就自动跳到到Home页面

3、使用路由模块来实现页面跳转的方式 方式1:直接修改地址栏

方式2:this.$router.push(‘路由地址’)

方式3:

四、vue-router使用方式 1:下载 npm i vue-router -S 2:在main.js中引入 import VueRouter from 'vue-router'; 3:安装插件Vue.use(VueRouter); 4:创建路由对象并配置路由规则 let router = new VueRouter({routes:[{path:'/home',component:Home}]}); 5:将其路由对象传递给Vue的实例,options中加入 router:router 6:在app.vue中留坑

具体实现请看如下代码:

//main.js文件中引入 import Vue from 'vue'; import VueRouter from 'vue-router'; //主体 import App from './components/app.vue'; import Home from './components/home.vue' //安装插件 Vue.use(VueRouter); //挂载属性 //创建路由对象并配置路由规则 let router = new VueRouter({ routes: [ //一个个对象 { path: '/home', component: Home } ] }); //new Vue 启动 new Vue({ el: '#app', //让vue知道我们的路由规则 router: router, //可以简写router render: c => c(App), }) 最后记得在在app.vue中“留坑”

//app.vue中

五、 vue-router参数传递 声明式的导航和编程式的导航router.push(...)都可以传参,本文主要介绍前者的传参方法,同样的规则也适用于编程式的导航。

1.用name传递参数 在路由文件src/router/index.js里配置name属性

routes: [ { path: '/', name: 'Hello', component: Hello } ] 模板里(src/App.vue)用route.name来接收 比如:<p>{{route.name}}

2 通过 标签中的to传参 这种传参方法的基本语法:

valueString 比如先在src/App.vue文件中

Hi页面1 然后把src/router/index.js文件里给hi1配置的路由起个name,就叫hi1.

{path:'/hi1',name:'hi1',component:Hi1} 最后在模板里(src/components/Hi1.vue)用$route.params.username进行接收.

{{route.params.username}}-{{route.params.id}} 3 利用url传递参数----在配置文件里以冒号的形式设置参数。 我们在/src/router/index.js文件里配置路由

{ path:'/params/:newsId/:newsTitle', component:Params } 我们需要传递参数是新闻ID(newsId)和新闻标题(newsTitle).所以我们在路由配置文件里制定了这两个值。

在src/components目录下建立我们params.vue组件,也可以说是页面。我们在页面里输出了url传递的的新闻ID和新闻标题。

<template>
    <div>
        <h2>{{ msg }}</h2>
        <p>新闻ID:{{ $route.params.newsId}}</p>
        <p>新闻标题:{{ $route.params.newsTitle}}</p>
    </div>
</template>
<script>
export default {
  name: 'params',
  data () {
    return {
      msg: 'params page'
    }
  }
}
</script>

在App.vue文件里加入我们的标签。这时候我们可以直接利用url传值了

params

  1. 使用path来匹配路由,然后通过query来传递参数 router-link跳转Query

对应路由配置:

   {
     path: '/query',
     name: 'Query',
     component: Query
   }

于是我们可以获取参数:

this.$route.query.queryId 六、vue-router配置子路由(二级路由) 实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL中各段动态路径也按某种结构对应嵌套的各层组件,例如:

image 如何实现下图效果(H1页面和H2页面嵌套在主页中)? image 1.首先用标签增加了两个新的导航链接

主页 H1页面 H2页面 2.在HelloWorld.vue加入标签,给子模板提供插入位置

 <template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <router-view></router-view>
  </div>
</template>

3.在components目录下新建两个组件模板 H1.vue 和 H2.vue 两者内容类似,以下是H1.vue页面内容:

 <template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        msg: 'I am H1 page,Welcome to H1'
      }
    }
  }
</script>
修改router/index.js代码,子路由的写法是在原有的路由配置下加入children字段。
   routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
      children: [{path: '/h1', name: 'H1', component: H1},//子路由的<router-view>必须在HelloWorld.vue中出现
        {path: '/h2', name: 'H2', component: H2}
      ]
    }
  ]

七、单页面多路由区域操作 在一个页面里我们有两个以上区域,我们通过配置路由的js文件,来操作这些区域的内容

1.App.vue文件,在下面新写了两行标签,并加入了些CSS样式

<template>
  <div id="app">
    <img src="./assets/logo.png">
       <router-link :to="{name:'HelloWorld'}"><h1>H1</h1></router-link>
       <router-link :to="{name:'H1'}"><h1>H2</h1></router-link>
    <router-view></router-view>
    <router-view name="left" style="float:left;width:50%;background-color:#ccc;height:300px;"/>
    <router-view name="right" style="float:right;width:50%;background-color:yellowgreen;height:300px;"/>
  </div>
</template>

2.需要在路由里配置这三个区域,配置主要是在components字段里进行

export default new Router({
    routes: [
      {
        path: '/',
        name: 'HelloWorld',
        components: {default: HelloWorld,
          left:H1,//显示H1组件内容'I am H1 page,Welcome to H1'
          right:H2//显示H2组件内容'I am H2 page,Welcome to H2'
        }
      },
      {
        path: '/h1',
        name: 'H1',
        components: {default: HelloWorld,
          left:H2,//显示H2组件内容
          right:H1//显示H1组件内容
        }
      }
    ]
  })

上边的代码我们编写了两个路径,一个是默认的‘/’,另一个是‘/Hi’.在两个路径下的components里面,我们对三个区域都定义了显示内容。最后页面展示如下图:

image 八.route 和router 的区别 我们先将这两者console.log打印出来:

image image $route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。

① $route.path 字符串,对应当前路由的路径,总是解析为绝对路径,如 "/order"。

② $route.params 一个 key/value 对象,包含了 动态片段 和 全匹配片段, 如果没有路由参数,就是一个空对象。

route.query
一个 key/value 对象,表示 URL 查询参数。
例如,对于路径 /foo?user=1,则有route.query.user为1, 如果没有查询参数,则是个空对象。

④ $route.hash 当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。

⑤ $route.fullPath 完成解析后的 URL,包含查询参数和 hash 的完整路径。

⑥ $route.matched 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。

⑦ $route.name 当前路径名字

$router 是“路由实例”对象,即使用 new VueRouter创建的实例,包括了路由的跳转方法,钩子函数等。

$router常见跳转方法:

<button @click="goToMenu" class="btn btn-success">Let's order! .....

<script>
  export default{
    methods:{
      goToMenu(){
        this.$router.go(-1)//跳转到上一次浏览的页面
        this.$router.replace('/menu')//指定跳转的地址
        this.$router.replace({name:'menuLink'})//指定跳转路由的名字下
        this.$router.push('/menu')//通过push进行跳转
        this.$router.push({name:'menuLink'})//通过push进行跳转路由的名字下
      }
    }
  }
</script>

router.push和router.replace的区别:

使用push方法的跳转会向 history 栈添加一个新的记录,当我们点击浏览器的返回按钮时可以看到之前的页面。 使用replace方法不会向 history 添加新记录,而是替换掉当前的 history 记录,即当replace跳转到的网页后,‘后退’按钮不能查看之前的页面。 九、 如何设置404页面 用户会经常输错页面,当用户输错页面时,我们希望给他一个友好的提示页面,这个页面就是我们常说的404页面。vue-router也为我们提供了这样的机制。

设置我们的路由配置文件(/src/router/index.js) { path:'', component:Error } 这里的path:''就是输入地址不匹配时,自动显示出Error.vue的文件内容

在/src/components/文件夹下新建一个Error.vue的文件。简单输入一些有关错误页面的内容。

<template>
    <div>
        <h2>{{ msg }}</h2>
    </div>
</template>
<script>
export default {
  data () {
    return {
      msg: 'Error:404'
    }
  }
}
</script>

此时我们随意输入一个错误的地址时,便会自动跳转到404页面

axios

Axios 是什么?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

Axios 功能

1.从浏览器中创建 XMLHttpRequests

2.从 node.js 创建 http 请求

3.支持 Promise API

4.拦截请求和响应

5.转换请求数据和响应数据

6.取消请求

7.自动转换 JSON 数据

8.客户端支持防御 XSRF

Axios 使用

执行 GET 请求

axios.get('/user?id=12345')
  .then(function (response) {
    console.log(response);
  })

执行 POST 请求

axios.post('/user', {
    name: 'zxm'
  })
  .then(function (response) {
    console.log(response);
  })

源码解读

  • lib/axios.js
'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

// 重点 createInstance 方法
// 先眼熟一个代码 下面讲完工具函数会再具体来讲解 createInstance
function createInstance(defaultConfig) {
    // 实例化 Axios
  var context = new Axios(defaultConfig);
    // 自定义 bind 方法 返回一个函数()=> {Axios.prototype.request.apply(context,args)}
  var instance = bind(Axios.prototype.request, context);
    // Axios 源码的工具类
  utils.extend(instance, Axios.prototype, context);

  utils.extend(instance, context);

  return instance;
}
// 传入一个默认配置   defaults 配置先不管,后面会有具体的细节
var axios = createInstance(defaults);


// 下面都是为 axios 实例化的对象增加不同的方法。
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;
  • lib/ util.js 工具方法
module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  deepMerge: deepMerge,
  extend: extend,
  trim: trim
};

// a, b,thisArg 参数都为一个对象
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
      // 如果指定了 thisArg 那么绑定执行上下文到 thisArg
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

Axios 实例源码

'use strict';
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 核心方法 request
Axios.prototype.request = function request(config) {
  // ... 单独讲
};

// 合并配置将用户的配置 和默认的配置合并
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 这个就是给 Axios.prototype 上面增加 delete,get,head,options 方法
// 这样我们就可以使用 axios.get(), axios.post() 等等方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
     // 都是调用了 this.request 方法
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;


上面的所有的方法都是通过调用了 this.request 方法

那么我们就来看这个 request 方法,个人认为是源码内的精华也是比较难理解的部分,使用到了 Promise 的链式调用,也使用到了中间件的思想。

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
    // 合并配置
  config = mergeConfig(this.defaults, config);
    // 请求方式,没有默认为 get
  config.method = config.method ? config.method.toLowerCase() : 'get';

    // 重点 这个就是拦截器的中间件
  var chain = [dispatchRequest, undefined];
    // 生成一个 promise 对象
  var promise = Promise.resolve(config);

    // 将请求前方法置入 chain 数组的前面 一次置入两个 成功的,失败的
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
	// 将请求后的方法置入 chain 数组的后面 一次置入两个 成功的,失败的
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

   // 通过 shift 方法把第一个元素从其中删除,并返回第一个元素。
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

InterceptorManager 拦截器源码

'use strict';
var utils = require('./../utils');

function InterceptorManager() {
    // 存放方法的数组
  this.handlers = [];
}
// 通过 use 方法来添加拦截方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 通过 eject 方法来删除拦截方法
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 添加一个 forEach 方法,这就是上述说的 forEach
InterceptorManager.prototype.forEach = function forEach(fn) {
    // 里面还是依旧使用了 utils 的 forEach, 不要纠结这些 forEach 的具体代码
    // 明白他们干了什么就可以
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;


dispatchRequest 源码

  • lib/ core/ dispatchRequest.js
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 请求取消时候的方法,暂不看
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
    // 请求没有取消 执行下面的请求
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }
  config.headers = config.headers || {};
	// 转换数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
    // 合并配置
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
    // 这里是重点, 获取请求的方式,下面会将到
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
	// 难道了请求的数据, 转换 data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
      // 失败处理
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};


看了这么多,我们还没看到是通过什么来发送请求的,现在我们看看在最开始实例化 createInstance 方法中我们传入的 defaults 是什么

var axios = createInstance(defaults);

  • lib/ defaults.js
'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
// getDefaultAdapter 方法是来获取请求的方式
function getDefaultAdapter() {
  var adapter;
  // process 是 node 环境的全局变量
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是 node 环境那么久通过 node http 的请求方法
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
   // 如果是浏览器啥的 有 XMLHttpRequest 的就用 XMLHttpRequest
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
    // adapter 就是请求的方法
  adapter: getDefaultAdapter(),
	// 下面一些请求头,转换数据,请求,详情的数据
    // 这也就是为什么我们可以直接拿到请求的数据时一个对象,如果用 ajax 我们拿到的都是 jSON 格式的字符串
    // 然后每次都通过 JSON.stringify(data)来处理结果。
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;


作为vue的全家桶技术栈,vue、vuex、vue-Router、axios是必须需要掌握的,掌握的越深,你对前端架构的掌控能力越强,希望大家可以在前端技术有所作为!