Vue Wiki

Table of Contents

Vue 总纲:

Vue 基础

安装

Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性,但它支持所有兼容 ECMAScript 5 的浏览器。

安装方式:

  • 直接用 <script> 引入, Vue 被注册为一个全局变量;
  • NPM ;
  • Vue CLI (命令行工具)。

介绍

Vue 是一套用于构建用户界面的 渐进式框架 ,被设计为自底之上逐层应用, 核心库 只关注视图层,易于第三方库和既有项目整合。

1. 声明式渲染

Vue.js 的 核心 是一个允许采用简洁的 模板语法 ↓ 来声明式地将 数据 渲染进 DOM 的系统。

我们不仅可以把数据绑定到 DOM 文本或 attribute ,还可以绑定到 DOM 结构,如下:

<div id="app">
  <!-- 文本插值 -->
  {{ message }}

  <!-- 绑定属性 -->
  <span v-bind:title="message">
    鼠标悬停几秒钟查看此处动态绑定的提示信息!
  </span>

  <!-- 绑定到 DOM 结构 -->
  <p v-if="seen">现在你看到我了</p>

  <!-- 绑定数组的数据来渲染一个项目列表 -->
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

为了让用户和你的应用进行交互,可以用 v-on 指令添加一个 事件监听器 ;还提供了 v-model 指令,轻松实现表单输入和应用状态之间的 双向绑定 。如下:

<div id="app">
  <p>{{ message }}</p>

  <!-- 事件监听 -->
  <button v-on:click="reverseMessage">反转消息</button>

  <!-- 双向绑定 -->
  <input v-model="message" />
</div>

2. 组件化应用构建

在 Vue 里,一个组件 本质 上是一个拥有预定义选项的一个 Vue 实例。

组件系统 是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用,几乎任意类型的应用界面都可以抽象为一个组件树。

Vue 实例

1. 创建一个 Vue 实例

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:

var vm = new Vue({
  // 选项对象
})

当创建一个 Vue 实例时,你可以传入一个 选项对象 ,该教程主要描述的就是如何使用这些选项来创建你想要的行为。

一个 Vue 应用由一个通过 new Vue 创建的 根 Vue 实例 ,以及可选的嵌套的、可复用的 组件树 组成。

根实例
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

注意,所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象 (一些根实例特有的选项除外)。

2. 数据与方法

当一个 Vue 实例被创建时,它将 data 对象中的所有的属性加入到 Vue 的 响应式系统 中。当这些属性的值发生改变时,视图将会产生“响应”(重新渲染),即匹配更新为新的值。

唯一的例外是使用 Object.freeze() ,这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。

!注意:只有当实例被创建时就已经存在于 data 中的属性才是响应式的。

除了数据属性,Vue 实例还暴露了一些有用的 实例属性与方法 ,它们都有前缀 $ ,以便与用户定义的属性区分开来,详见 API 参考

3. 实例生命周期

每个 Vue 实例在被创建时都要经过一系列的 初始化过程 ,如:

  • 设置数据监听;
  • 编译模板;
  • 将实例挂载到 DOM 并在数据变化时更新 DOM 等。

同时在这个过程中也会运行一些叫做 生命周期钩子 的函数,这给了用户在不同阶段添加自己的代码的机会。

温馨提示:

不要选项属性或回调上 使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod()) 。因为箭头函数并没有 this ,this 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

模板语法

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。

在底层的实现上,Vue 将 模板 编译成 虚拟 DOM 渲染函数 。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。

插值

<div id="app">
  <!-- 1. 文本插值 -->
  <!-- 双大括号会将数据解释为普通文本 -->
  <span>Message: {{ msg }}</span>
  <!-- *1. v-once -->
  <!-- 一次性地插值,当数据改变时,插值处的内容不会更新 -->
  <span v-once>这个将不会改变: {{ msg }}</span>

  <!-- 2. 原始 HTML -->
  <!-- 为了输出真正的 HTML,你需要使用 v-html 指令 -->
  <p>Using v-html directive: <span v-html="rawHtml"></span></p>

  <!-- 3. Attribute -->
  <!-- 使用 v-bind 指令 -->
  <!-- 其中,对于布尔 attribute (它们只要存在就意味着值为 true) -->
  <div v-bind:id="dynamicId"></div>
</div>

迄今为止,在我们的模板中,我们一直都只绑定简单的属性键值。但实际上,对于 所有的 数据绑定,Vue.js 都提供了 完全的 JavaScript 表达式支持。

<div id="app">  
  <!-- 4. 使用 JavaScript 表达式 -->
  {{ number + 1 }}
  {{ ok ? 'YES' : 'NO' }}
  {{ message.split('').reverse().join('') }}
  <div v-bind:id="'list-' + id"></div>
</div>

这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个 限制 就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。

<div id="app">
  <!-- 这是语句,不是表达式 -->
  {{ var a = 1 }}

  <!-- 流控制也不会生效,请使用三元表达式 -->
  {{ if (ok) { return message } }}
</div>

指令

指令 (Directives) 是带有 v- 前缀的特殊 attribute,它的 值预期 是单个 JavaScript 表达式 ( v-for 是例外情况,稍后我们再讨论)。指令的 职责 是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

1. 参数

一些指令能够接收一个“参数”,在指令名称之后以冒号表示,如:

<div id="app">
  <!-- 属性绑定 -->
  <!-- href → url -->
  <a v-bind:href="url">...</a>
  <!-- 事件监听 -->
  <!-- click → doSomething -->
  <a v-on:click="doSomething">...</a>  
</div>

2. 动态参数

从 2.6.0 开始,可以用 方括号 括起来的 JavaScript 表达式作为一个指令的参数:

<div id="app">
  <a v-bind:[attributeName]="url"> ... </a>
  <a v-on:[eventName]="doSomething"> ... </a>
</div>

这里的 attributeName / eventName 会被作为一个 JavaScript 表达式进行动态求值, 求得的值 将会作为最终的参数来使用。

→ 对动态参数的值的约束

动态参数预期会求出一个 字符串 ,异常情况下值为 null 。这个特殊的 null 值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。

→ 对动态参数表达式的约束

动态参数表达式有一些 语法约束 ,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。变通的办法是使用没有空格或引号的表达式,或用 计算属性 替代这种复杂表达式。

在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要 避免使用大写字符 来命名键名,因为浏览器会把 attribute 名全部 强制转为小写

<div id="app">
  <!-- 这会触发一个编译警告 -->
  <a v-bind:['foo' + bar]="value"> ... </a>

  <!-- 在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。
       除非在实例中有一个名为“someattr”的 property,否则代码不会工作。
    -->
  <a v-bind:[someAttr]="value"> ... </a>
</div>

3. 修饰符

修饰符 (modifier) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。

<div id="app">
  <form v-on:submit.prevent="onSubmit">...</form>
</div>

其中, .prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()

缩写

Vue 为 v-bindv-on 这两个最常用的指令,提供了特定简写:

<div id="app">
  <a v-bind:href="url">...</a>
  <!-- 缩写 ↓ -->
  <a :href="url">...</a>

  <a v-on:click="doSomething">...</a>
  <!-- 缩写 ↓ -->
  <a @click="doSomething">...</a>
</div>

它们看起来可能与普通的 HTML 略有不同,但 : 与 @ 对于 attribute 名来说 都是合法字符 ,在所有支持 Vue 的浏览器都能被正确地解析。

计算属性和侦听器

模板内的表达式非常便利,但是设计它们的初衷是 用于简单运算 的,在模板中放入太多的逻辑会让模板过重且难以维护。

1. 计算属性

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

<div id="app">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

其中,我们声明了一个计算属性 reversedMessage ,提供的函数将用作属性 vm.reversedMessagegetter 函数。

计算属性 默认 只有 getter ,不过在需要时你也可以提供一个 setter ,如下:

// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

此时再运行 vm.fullName = 'John Doe' 时,setter 会被调用, vm.firstNamevm.lastName 也会相应地被更新。

2. 计算属性缓存 vs 方法

!计算属性是基于它们的 响应式依赖 进行缓存的。 只在相关响应式依赖发生改变时它们才会重新求值,相比之下,每当触发重新渲染时,调用方法将 总会再次执行 函数。

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A 。如果没有缓存,我们将不可避免的多次执行 A 的 getter! 如果你不希望有缓存,请用方法来替代。

3. 计算属性 vs 侦听属性

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动: 侦听属性 。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。

// 侦听属性
watch: {
  firstName: function (val) {
    this.fullName = val + ' ' + this.lastName
  },
  lastName: function (val) {
    this.fullName = this.firstName + ' ' + val
  }
}
// vs
// 计算属性
computed: {
  fullName: function () {
    return this.firstName + ' ' + this.lastName
  }
}

4. 侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。当需要在数据变化时 执行异步开销较大的操作 时,这个方式是最有用的。

示例代码请参考 → 侦听器示例

表单输入绑定

NEXT 基础用法

  • State "NEXT" from "TODO" [2020-02-11 Tue 21:47]

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建 双向数据绑定 。它会根据控件类型 自动选取 正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

v-model忽略 所有表单元素的 value、checked、selected 属性的初始值,而总是将 Vue 实例的数据作为 数据来源 。你应该通过 JavaScript 在组件的 data 选项中 声明初始值

v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

1. 文本

i.e. text

<!-- 使用 value 属性 -->
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

2. 多行文本

i.e. textarea

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<!-- 使用 value 属性 -->
<textarea v-model="message" placeholder="add multiple lines"></textarea>

在文本区域插值 ( <textarea>{{text}}</textarea> ) 并不会生效,应用 v-model 来代替。

3. 复选框

i.e. checkbox

单个复选框,绑定到布尔值;多个复选框,绑定到同一个数组。

4. 单选按钮

i.e. radio

绑定到选择的值。

5. 选择框

i.e. select/option

单选时,绑定到选择项值;多选时,绑定到一个数组。

NEXT 值绑定

  • State "NEXT" from "TODO" [2020-02-11 Tue 21:47]

对于 单选按钮复选框选择框 的选项,v-model 绑定的值通常是 静态字符串 (对于复选框也可以是布尔值)。

但是有时我们可能想把值绑定到 Vue 实例的一个 动态属性 上,这时可以用 v-bind 实现,并且这个属性的值可以不是字符串。

修饰符

.lazy

在默认情况下, v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转变为使用 change 事件进行同步:

<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg" >

.number

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符。

<input v-model.number="age" type="number">

这通常很有用,因为即使在 type="number" 时,HTML 输入元素的值也 总会返回字符串 。如果这个值无法被 parseFloat() 解析,则会返回原始的值。

.trim

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符。

<input v-model.trim="msg">

TODO 在组件上使用 v-model

HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 v-model 一起使用!

组件基础

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

组件的复用

一个组件(除根实例外)的 data 选项 必须是一个函数 ,因此每个实例可以维护一份被返回对象的 独立的拷贝

data: function () {
  return {
    count: 0
  }
}

组件的组织

通常一个应用会以一棵嵌套的组件树的形式来组织,为了能在模板中使用,这些组件 必须先注册 以便 Vue 能够识别。这里有两种组件的注册类型: 全局注册局部注册

详见 组件注册 ↓

通过 Prop 向子组件传递数据

Prop 是你可以在组件上注册的一些自定义 attribute ,当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个属性。

一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop 。你会发现我们能够在 组件实例 中访问这个值,就像访问 data 中的值一样。

单个根元素

!注意,每个组件必须只有一个根元素。

监听子组件事件

Vue 实例提供了一个 自定义事件的系统 来解决这个问题, 父级组件 可以像处理 native DOM 事件一样通过 v-on 监听 子组件 实例的任意事件。

<!-- 父组件 -->
<blog-post
  ...
  v-on:enlarge-text="postFontSize += 0.1"
></blog-post>

同时子组件可以通过 调用内建的 $emit 方法传入事件名称 来触发一个事件:

<!-- 子组件 -->
<div class="blog-post">
  ...
  <button v-on:click="$emit('enlarge-text')">
    Enlarge text
  </button>
  ...
</div>

1. 使用事件抛出一个值

有的时候用一个事件来 抛出一个特定的值 是非常有用的。例如我们可能想让 <blog-post> 组件决定它的文本要放大多少。这时可以使用 $emit第二个参数 来提供这个值:

<div class="blog-post">
  ...
  <button v-on:click="$emit('enlarge-text', 0.1)">
    Enlarge text
  </button>
  ...
</div>

然后当在父级组件监听这个事件的时候,我们可以 通过 $event 访问 到被抛出的这个值:

<blog-post
  ...
  v-on:enlarge-text="postFontSize += $event"
></blog-post>

或者,如果这个事件处理函数是一个方法:

<blog-post
  ...
  v-on:enlarge-text="onEnlargeText"
></blog-post>

那么,这个值将会作为 第一个参数 传入这个方法:

methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

2. 在组件上使用 v-model

// TODO….

通过插槽分发内容

和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:

<alert-box>
  Something bad happened.
</alert-box>

可能会渲染出这样的东西:

Error! Something bad happened.

幸好,Vue 自定义的 <slot> 元素让这变得非常简单:

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

如你所见,我们只要在需要的地方加入插槽就行了——就这么简单!

详见 插槽 ↓

动态组件

有的时候,在不同组件之间进行动态切换是非常有用的,可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现:

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component v-bind:is="currentTabComponent"></component>

其中, currentTabComponent 可以是:

  • 已注册 组件的名字 ,或
  • 一个组件的 选项对象

请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器

了解更多 动态组件 & 异步组件 ↓

解析 DOM 模板时的注意事项

有些 HTML 元素,诸如 <ul>、<ol>、<table><select> ,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr><option> ,只能出现在其它某些特定的元素内部。

<!-- 自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并
     导致最终渲染结果出错。
  -->
<table>
  <blog-post-row></blog-post-row>
</table>

↓ 幸好这个特殊的 is attribute 给了我们一个变通的办法 ↓

<table>
  <tr is="blog-post-row"></tr>
</table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  • 字符串(例如: template: '...'
  • 单文件组件( .vue
  • <script type="text/x-template">

深入了解组件

组件注册

1. 组件名

定义组件名的方式有两种:

  • 使用 kebab-case
  • 使用 PascalCase

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name><MyComponentName> 都是可接受的。

!注意,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

2. 全局注册

Vue.component 来创建的组件是 全局注册 的。

在注册之后,可以用在任何新创建的 Vue 根实例 ( new Vue ) 的模板中;在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以 相互使用

3. 局部注册

可以通过一个普通的 JavaScript 对象来定义组件,然后在 components 选项中定义你想要使用的组件:

// 定义组件
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }

// 注册组件
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

对于 components 对象中的每个属性来说,其 属性名 就是自定义元素的 名字 ,其 属性值 就是这个组件的 选项对象

!注意:局部注册的组件在其子组件中 不可用 需要在目标子组件的 components 选项中定义后才能使用。

4. 模块系统

4.1 在模块系统中局部注册

// ComponentB 组件

// 引入组件
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

// 组件 components 选项中定义组件
export default {
  components: {
    ComponentA,
    ComponentC
  },
  // ...
}

现在 ComponentAComponentC 都可以在 ComponentB 的模板中使用了。

4.2 基础组件的自动化全局注册

可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为 基础组件 ,它们会在各个组件中被频繁的用到,所以会导致很多组件里都会有一个包含基础组件的长列表:

import BaseButton from './BaseButton.vue'
import BaseIcon from './BaseIcon.vue'
import BaseInput from './BaseInput.vue'

export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

而只是用于模板中的一小部分:

<BaseInput
  v-model="searchText"
  @keydown.enter="search"
/>
<BaseButton @click="search">
  <BaseIcon name="search"/>
</BaseButton>

幸好如果你使用了 webpack (或在内部使用了 webpack 的 Vue CLI 3+),那么就可以使用 require.context 只全局注册这些非常通用的基础组件。

!全局注册的行为必须在根 Vue 实例 (通过 new Vue ) 创建之前 发生。

来看一份可以让你在应用入口文件(比如 src/main.js )中 全局导入基础组件 的示例代码:

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

const requireComponent = require.context(
  // 其组件目录的相对路径
  './components',
  // 是否查询其子目录
  false,
  // 匹配基础组件文件名的正则表达式
  /Base[A-Z]\w+\.(vue|js)$/
)

requireComponent.keys().forEach(fileName => {
  // 获取组件配置
  const componentConfig = requireComponent(fileName)

  // 获取组件的 PascalCase 命名
  const componentName = upperFirst(
    camelCase(
      // 获取和目录深度无关的文件名
      fileName
        .split('/')
        .pop()
        .replace(/\.\w+$/, '')
    )
  )

  // 全局注册组件
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

NEXT Prop

  • State "NEXT" from [2020-02-12 Wed 00:54]

1. Prop 的大小写(camelCase vs kebab-case)

HTML 中的 attribute 名是 大小写不敏感 的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名。

重申一次,如果你使用字符串模板,那么这个限制就不存在了。

2. Prop 类型

可以 字符串数组 形式列出 prop ,还可以以 对象 形式列出 prop ,这些属性的名称和值分别是 prop 各自的名称和类型。

3. 传递静态或动态 Prop

可以给 prop 传入一个 静态的值 ,还可以通过 v-bind 给 prop 动态赋值

<!-- 静态的值 -->
<blog-post title="My journey with Vue"></blog-post>

<!-- 动态赋予一个变量的值 -->
<blog-post v-bind:title="post.title"></blog-post>
<!-- 动态赋予一个复杂表达式的值 -->
<blog-post
  v-bind:title="post.title + ' by ' + post.author.name"
></blog-post>

3.1 传入一个字符串

在上述示例中,我们传入的值都是字符串类型的,但实际上 任何类型的值 都可以传给一个 prop 。

3.2 传入一个数字

<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:likes="42"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:likes="post.likes"></blog-post>

3.3 传入一个布尔值

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。-->
<blog-post is-published></blog-post>

<!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:is-published="false"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:is-published="post.isPublished"></blog-post>

3.4 传入一个数组

<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post v-bind:comment-ids="[234, 266, 273]"></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:comment-ids="post.commentIds"></blog-post>

3.5 传入一个对象

<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。-->
<blog-post
  v-bind:author="{
    name: 'Veronica',
    company: 'Veridian Dynamics'
  }"
></blog-post>

<!-- 用一个变量进行动态赋值。-->
<blog-post v-bind:author="post.author"></blog-post>

3.6 传入一个对象的所有属性

如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用 不带参数的 v-bind (取代 v-bind:prop-name )。

<blog-post v-bind="post"></blog-post>
<!-- 等价于 -->
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>

4. 单项数据流

!所有的 prop 都使得其父子 prop 之间形成了一个 单向下行绑定 父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你 不应该 在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

这里有两种常见的试图改变一个 prop 的情形:

4.1 这个 prop 用来传递一个初始值

在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值。

props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

4.2 这个 prop 以一种原始的值传入且需要进行转换

在这种情况下,最好使用这个 prop 的值来定义一个计算属性。

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

!注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。

5. Prop 验证

我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。

为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。

类型检查

type 可以是原生构造函数,还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。

原生构造函数

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

自定义构造函数

function Person(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
}

如此,来验证 author prop 的值是否是通过 new Person 创建的。

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

6. 非 Prop 的 Attribute

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 prop 定义的 attribute 。

因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的 attribute,而这些 attribute 会被添加到这个 组件的根元素 上。

例如,想象一下你通过一个 Bootstrap 插件使用了一个第三方的 <bootstrap-date-input> 组件,这个插件需要在其 <input> 上用到一个 data-date-picker attribute。我们可以将这个 attribute 添加到你的组件实例上:

<bootstrap-date-input data-date-picker="activated"></bootstrap-date-input>

然后这个 data-date-picker="activated" attribute 就会自动添加到 <bootstrap-date-input> 的根元素上。

6.1 替换/合并已有的 Attribute

想象一下 <bootstrap-date-input> 的模板是这样的:

<input type="date" class="form-control">

为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名:

<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

对于绝大多数 attribute 来说,从外部提供给组件的值会 替换 掉组件内部设置好的值。所以如果传入 type="text" 就会替换掉 type="date" 并把它破坏!庆幸的是, classstyle attribute 会稍微智能一些,即两边的值会被 合并 起来,从而得到最终的值 form-control date-picker-theme-dark

6.2 TODO 禁用 Attribute 继承

如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false

这尤其适合配合实例的 $attrs 属性使用,该属性包含了传递给一个组件的 attribute 名和 attribute 值。

{
  required: true,
  placeholder: 'Enter your username'
}

有了 inheritAttrs: false$attrs ,你就可以手动决定这些 attribute 会被赋予哪个元素。在撰写基础组件的时候是常会用到的:

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
}

!注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。

这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素:

<base-input
  v-model="username"
  required
  placeholder="Enter your username"
></base-input>

NEXT 自定义事件

  • State "NEXT" from "TODO" [2020-02-25 Tue 12:04]

事件名

不同于组件和 prop ,事件名 不存在 任何自动化的 大小写转换 ,而是触发的事件名需要 完全匹配 监听这个事件所用的名称。

举个例子,如果触发一个 camelCase 名字的事件:

this.$emit('myEvent')

则监听这个名字的 kebab-case 版本是不会有任何效果的:

<!-- 没有效果 -->
<my-component v-on:my-event="doSomething"></my-component>

不同于组件和 prop ,事件名不会被用作一个 JavaScript 变量名或属性名,所以就没有理由使用 camelCase 或 PascalCase 了。

但在 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),如 v-on:myEvent 将会变成 v-on:myevent 导致 myEvent 不可能被监听到,因此, !推荐你始终使用 kebab-case 的事件名。

自定义组件的 v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。 model 选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用 v-model 的时候:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里 lovingVue 的值将会传入这个名为 checked 的 prop 。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会更新。

!注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

NEXT 将原生事件绑定到组件

  • State "NEXT" from [2020-02-25 Tue 12:56]

你可能有很多次想要在一个组件的根元素上直接监听一个 原生事件 。这时,你可以使用 v-on.native 修饰符:

<base-input v-on:focus.native="onFocus"></base-input>

.sync 修饰符

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

!我们推荐以 update:myPropName 的模式触发事件取而代之。

举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

this.$emit('update:title', newTitle)

然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如:

<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

<text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync="doc.title + '!'" 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

<text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个属性 (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync="{ title: doc.title }" ,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

NEXT 插槽

  • State "NEXT" from "TODO" [2020-02-17 Mon 16:19]

插槽内容

Vue 实现了一套 内容分发 的 API,将 <slot> 元素作为承载分发内容的出口。我们来看一段示例:

这里有一个组件 <navigation-link> ,它的模板内容为:

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

然后,我们可以向这边这样使用该组件:

<navigation-link url="/profile">
  Your Profile
</navigation-link>

如此,组件渲染的时候, <slot></slot> 将会被替换为 "Your Profile"

!插槽内可以包含 任何模板代码 ,甚至 其它组件

<!-- 1. 任何模板代码 -->
<navigation-link url="/profile">
  <!-- 添加一个 Font Awesome 图标 -->
  <span class="fa fa-user"></span>
  Your Profile
</navigation-link>
<!-- 2. 其它组件 -->
<navigation-link url="/profile">
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link>

!如果 <nabigation-link> 没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容 都会被抛弃

后备内容

有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在 没有提供内容 的时候被渲染。

比如,我们有一个组件 <submit-button> ↓ :

<button type="submit">
  <slot>Submit</slot>
</button>

当我们使用该组件时,渲染如下:

<!-- 没提供内容,使用默认内容 Submit -->
<submit-button></submit-button>
<!-- ↓ ↓ ↓  -->
<button type="submit">
  Submit
</button>

<!-- 提供内容,使用提供内容 Save -->
<submit-button>Save</submit-button>
<!-- ↓ ↓ ↓  -->
<button type="submit">
  Save
</button>

具名插槽

有时我们需要多个插槽, <slot> 元素有一个特殊的 attribute:name ,可以用来定义额外的插槽。

一个不带 name<slot> 出口会带有隐含的名字 “default”

<!-- 组件 <base-layout> 模板内容 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称。任何没有被包裹在带有 v-slot<template> 中的内容都会被视为默认插槽的内容,如下:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <!-- 等价于 -->
  <!-- <template v-slot:default> -->
  <!--   <p>A paragraph for the main content.</p> -->
  <!--   <p>And another one.</p> -->
  <!-- </template> -->

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

↓ 会被渲染为 ↓

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

编译作用域

当你想在一个插槽中使用数据时,例如:

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>

该插槽跟模板的其它地方一样可以访问 相同的实例属性 (也就是相同的“作用域”),而 不能 访问 <navigation-link> 的作用域,比如 url 便是访问不到的。

<navigation-link url="/profile">
  Clicking here will send you to: {{ url }}
  <!--
  这里的 `url` 会是 undefined,因为 "/profile" 是
  _传递给_ <navigation-link> 的而不是
  在 <navigation-link> 组件*内部*定义的。
  -->
</navigation-link>

!父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

DONE 作用域插槽

  • State "DONE" from "TODO" [2020-02-16 Sun 22:03]

有时让插槽内容能够访问子组件中才有的数据是很有用的。

例如,设想要给带有如下模板的 <current-user> 组件,如下:

<span>
  <slot>{{ user.lastName }}</slot>
</span>

我们可能要换掉备用内容,用名而非姓来显示,如下:

<current-user>
  {{ user.firstName }}
</current-user>

然而,上述代码 并不会正常工作 ,因为只有 <current-user> 组件可以访问到 user 而我们提供的内容是在父级渲染的。

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:

<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>

绑定在 <slot> 元素上的 attribute 被称为 插槽 prop 。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps ,但你也可以使用任意你喜欢的名字。

1. 独占默认插槽的缩写语法

就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:

<current-user v-slot="slotProps">
<!-- 等价于 -->
<!-- 
<current-user v-slot:default="slotProps">
-->
  {{ slotProps.user.firstName }}
</current-user>

注意默认插槽的缩写语法不能和具名插槽 混用 ,因为它会导致作用域不明确。只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法。

2. 解构插槽 Prop

作用域插槽的内部 工作原理 是将你的插槽内容包括在一个传入单个参数的函数里:

function(slotProps) {
    // 插槽内容
}

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式

所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person

<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

动态插槽名

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

NEXT 具体插槽的缩写

  • State "NEXT" from "TODO" [2020-02-16 Sun 21:18]

v-onv-bind 一样, v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 # 。然而,和其它指令一样,该缩写只在其有参数的时候才可用。

NEXT 动态组件 & 异步组件

  • State "NEXT" from [2020-02-12 Wed 10:52]

1. 动态组件

可以用一个 <keep-alive> 元素将其动态组件包裹起来,以保持这些组件的状态,避免反复重渲染导致的性能问题。

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

注意这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。

详见 keep-alive

2. 异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。

为了简化,Vue 允许你以一个 工厂函数 的方式定义你的组件,这个工厂函数会 异步解析 你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

Vue.component('async-example', function(resolve, reject) {
    setTimeout(function() {
        // 向 `resolve` 回调传递组件定义
        resolve({
            template: '<div>I am async!</div>'
        })
    }, 1000)
})

如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。

这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己,一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能 一起配合使用:

Vue.component('async-webpack-example', function(resolve) {
    // 这个特殊的 `require` 语法将会告诉 webpack
    // 自动将你的构建代码切成多个包,这些包
    // 会通过 Ajax 请求加载
    require(['./my-async-component'], resolve)
})

你可以在工厂函数中返回一个 Promise ,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:

Vue.component(
    'async-webpack-example',
    // 这个 `import` 函数会返回一个 `Promise` 对象。
    () => import('./my-async-component')
)

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

new Vue({
    // ...
    components: {
        'my-component': () => import('./my-async-component')
    }
})

2.1 处理加载状态

2.3.0+ 新增,这里的异步组件工厂函数也可以返回一个如下格式的 对象

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

NEXT 处理边界情况

  • State "NEXT" from [2020-02-12 Wed 11:38]

1. 访问元素 & 组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素,不过也确实在一些情况下做这些事情是合适的。

1.1 访问根实例

在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问。

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用,对于 demo 或非常小型的有少量组件的应用来说这是很方便的,中大型应用推荐使用 Vuex 来管理应用的状态。

1.2 访问父级组件实例

$root 类似, $parent 属性可以用来从一个 子组件访问父组件 的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

1.3 访问子组件实例或子元素

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref 这个 attribute 为子组件赋予一个 ID 引用。

<base-input ref="usernameInput"></base-input>

现在在你已经定义了这个 ref 的组件里,你可以使用:

this.$refs.usernameInput

refv-for 一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些 子组件的数组

$refs 只会在组件 渲染完成之后生效 ,并且它们 不是响应式 的。这仅作为一个用于直接操作子组件的“逃生舱” – 你应该 避免 在模板或计算属性中访问 $refs

1.4 依赖注入

由于,使用 $parent 属性无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项: provideinject

  • provide 选项允许我们指定我们想要提供给后代组件的数据/方法;
  • 在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性。

来看一段伪代码:

// 祖先组件
provide: function () {
  return {
    getMap: this.getMap
  }
}

// --------------------

// 后代组件
inject: ['getMap']

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性;
  • 后代组件不需要知道被注入的属性来自哪里。

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式 耦合 起来,使重构变得更加困难。同时所提供的属性是 非响应式的 。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。

如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

2. 程序化的事件侦听器

3. 循环引用

4. 模板定义的替代品

5. 控制更新

5.1 强制更新

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。

5.2 通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容 只计算一次 然后 缓存 起来。

再说一次,试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的 – 再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。

TODO 过渡 & 动画

可复用性 & 组合

NEXT 混入

  • State "NEXT" from [2020-02-12 Wed 11:54]

1. 基础

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含 任意组件选项 。当组件使用混入对象时,所有混入对象的选项将被 “混合”进入 该组件本身的选项。

2. 选项合并

“依次合并,组件优先”

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

  • 数据对象 在内部会进行递归合并,并在发生冲突时以 组件数据优先
  • 同名钩子函数 将合并为一个 数组 ,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
  • 值为 对象 的选项,例如 methods、componentsdirectives ,将被合并为同一个 对象 。两个对象键名冲突时,取组件对象的键值对。

注意: Vue.extend() 也使用同样的策略进行合并。

3. 全局混入

混入也可以进行全局注册( Vue.mixin )。使用时格外小心!一旦使用全局混入,它将影响 每一个 之后创建的 Vue 实例。

4. 自定义选项合并策略

TODO 自定义指令

TODO 渲染函数 & JSX

插件

插件通常用来为 Vue 添加 全局功能

插件的功能范围没有严格的限制 – 一般有下面几种:

  • 添加全局方法或属性,如 vue-custom-element
  • 添加全局资源:指令/过滤器/过渡等,如 vue-touch
  • 通过全局混入来添加一些组件选项,如 vue-router
  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  • 一个库,提供自己的 API ,同时提供上面提到的一个或多个功能,如 vue-router

1. 使用插件

通过全局方法 Vue.use() 使用插件,它需要在你调用 new Vue() 启动应用 之前 完成:

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
// 也可以传入一个可选的选项对象
// Vue.use(MyPlugin, { someOption: true })

new Vue({
  // ...组件选项
})

Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。

Vue.js 官方提供的一些插件 (例如 vue-router ) 在检测到 Vue 是可访问的全局变量时会 自动调用 Vue.use() 。然而在像 CommonJS 这样的模块环境中,你应该 始终显式地调用 Vue.use()

// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')

// 不要忘了调用此方法
Vue.use(VueRouter)

awesome-vue 集合了大量由社区贡献的插件和库。

2. 开发插件

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或属性
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的 文本格式化

过滤器可以用在两个地方: 双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的 尾部 ,由“管道”符号指示:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

组件选项中定义本地的过滤器

你可以在一个组件的选项中定义本地的过滤器:

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

在创建 Vue 实例之前全局定义过滤器

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

当全局过滤器和局部过滤器重名时,会采用局部过滤器。

!过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。

{{ message | filterA('arg1', arg2) }}

其中, filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 arg1 作为第二个参数,表达式 arg2 的值作为第三个参数。

工具

单文件组件

单元测试

TypeScript 支持

生产环境部署

以下大多数内容在你使用 Vue CLI 时都是默认开启的。该章节仅跟你自定义的构建设置有关。

1. 开启生产环境模式

详见 开启生产环境模式

2. 模板预编译

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在 运行时 被编译为渲染函数。

预编译模板最简单的方式就是使用单文件组件 – 相关的构建设置会自动把 预编译处理 好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。

3. 提取组件的 CSS

当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段“无样式内容闪烁 (fouc)”。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。

4. 跟踪运行时错误

如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数 (如果已设置)。利用这个钩子函数来配合错误跟踪服务是个不错的主意。比如 Sentry ,它为 Vue 提供了官方集成。

规模化

路由

对于大多数单页面应用,都推荐使用官方支持的 vue-router 库

从零开始简单的路由

如果你只需要非常简单的路由而不想引入一个功能完整的路由库,可以像这样动态渲染一个页面级的组件:

// 定义组件
const NotFound = { template: '<p>Page not found</p>' }
const Home = { template: '<p>home page</p>' }
const About = { template: '<p>about page</p>' }

// 自定义规则
const routes = {
  '/': Home,
  '/about': About
}


new Vue({
  el: '#app',
  data: {
    currentRoute: window.location.pathname
  },
  computed: {
    // 不同路径,不同组件
    ViewComponent () {
      return routes[this.currentRoute] || NotFound
    }
  },
  // 渲染对应路径的组件
  render (h) { return h(this.ViewComponent) }
})

结合 HTML5 History API,你可以建立一个麻雀虽小五脏俱全的客户端路由器。可以查看 实例应用

整合第三方路由

当然,你也可以整合第三方路由 ,如 Page.js 或者 Director ,整合起来也很简单。这里有一个使用了 Page.js 的 完整示例

状态管理

1. 类 Flux 状态管理的官方实现

由于状态 零散地分布 在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。为了解决这个问题,Vue 提供 Vuex 。

2 .简单状态管理起步使用

经常被忽略的是,Vue 应用中原始 数据 对象的 实际来源 – 当访问数据对象时,一个 Vue 实例只是简单的 代理访问

所以,如果你有一处需要被 多个实例间共享的状态 ,可以简单地通过维护一份数据来实现共享:

const sourceOfTruth = {}        // 原始数据对象的实际来源

const vmA = new Vue({
  data: sourceOfTruth           // 实例代理而已
})

const vmB = new Vue({
  data: sourceOfTruth           // 实例代理而已
})

此时,当 sourceOfTruth 发生变化,=vmA= 和 vmB 都将自动的更新引用它们的视图,子组件们的每个实例也会通过 this.$root.$data 去访问。

现在我们有了 唯一的数据来源sourceOfTruth ),但是,调试将会变为噩梦。任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。

为了解决这个问题,我们采用一个简单的 store 模式

var store = {
    debug: true,
    state: {
        message: 'Hello!'
    },
    setMessageAction(newValue) {
        if(this.debug) console.log('setMessageAction triggered with', newValue)
        this.state.message = newValue
    },
    clearMessageAction() {
        if(this.debug) console.log('clearMessageAction triggered')
        this.state.message = ''
    }
}

其中,需要注意, 所有 storestate 的改变,都放置在 store 自身的 action 中去处理。 这种 集中式状态管理 能够被更容易地理解哪种类型的 mutation 将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。

此外,每个实例/组件仍然可以拥有和管理自己的 私有状态

var vmA = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

var vmB = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

!重要的是,注意你 不应该 在 action 中 替换原始的状态对象 – 组件和 store 需要 引用同一个共享对象 ,mutation 才能够被观察。

接着我们继续延伸约定,组件 不允许直接修改 属于 store 实例的 state ,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 Flux 架构。

如此约定的 好处 在于,我们能够记录所有 store 中发生的 state 改变(如上述例子中通过在相应 action 中设置 log 记录 bug 之前发生了什么),同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。

详见 Vuex ↓

TODO 服务端渲染

TODO 安全

内在 – 深入响应式原理

Vue 最独特的特性之一,是其非侵入性的响应式系统。 数据模型 仅仅是 普通的 JavaScript 对象 。而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接。

在这个章节,我们将研究一下 Vue 响应式系统的底层的细节。

1. 如何追踪变化

!当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时 通知变更

Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 “接触”过的数据属性 记录为 依赖 。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

2. 检测变化的注意事项

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性 必须data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许 动态添加 根级别 的响应式属性。但是, 可以 使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性, 也可以 使用 vm.$set 实例方法,它是全局 Vue.set 方法的别名。

有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign()_.extend() 。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起 创建一个新的对象

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

3. 声明响应式属性

由于 Vue 不允许 动态添加根级响应式属性,所以你必须在 初始化实例前 声明 所有根级响应式属性 ,哪怕只是一个空值。

这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data 对象就像组件状态的结构 (schema)。提前声明所有的响应式属性,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。

4. 异步更新队列

Vue 在更新 DOM 时是 异步 执行的。

只要侦听到数据变化,Vue 将开启一个 队列 ,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种 在缓冲时去除重复数据 对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserversetImmediate ,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value' ,该组件 不会立即重新渲染 。当刷新队列时,组件会在 下一个 事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样 回调函数将在 DOM 更新完成后被调用

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message'             // 更改数据
vm.$el.textContent === 'new message'   // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent)   // → '未更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // → '已更新'
      })
    }
  }
})

因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:

methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

Vue CLI

介绍

该系统的组件

Vue CLI 有几个独立的部分 – 如果你看到了我们的 源代码 ,你会发现这个仓库里同时管理了多个单独发布的包。

1. CLI

CLI (@vue/cli) 是一个 全局安装 的 npm 包,提供了终端里的 vue 命令。

  • vue create 快速创建一个新项目的脚手架;
  • vue serve 构建新想法的原型(需要全局依赖 @vue/cli-service-global );
  • vue ui 通过一套图形化界面管理你的所有项目。

2. CLI 服务

CLI 服务 (@vue/cli-service) 是一个开发环境依赖。它是一个 npm 包, 局部安装 在每个 @vue/cli 创建的项目中

CLI 服务是构建于 webpackwebpack-dev-server 之上的。它包含了:

  • 加载其它 CLI 插件的核心服务;
  • 一个针对绝大部分应用优化过的内部的 webpack 配置;
  • 项目内部的 vue-cli-service 命令,提供 serve、buildinspect 命令。

详见 CLI 服务 ↓

3. CLI 插件

CLI 插件是向你的 Vue 项目提供可选功能的 npm 包,例如 Babel/TypeScript 转译、ESLint 集成、单元测试和 end-to-end 测试等。

Vue CLI 插件的名字以 @vue/cli-plugin- (内建插件) 或 vue-cli-plugin- (社区插件) 开头,非常容易使用。

当你在项目内部运行 vue-cli-service 命令时,它会 自动解析并加载 package.json 中列出的所有 CLI 插件。

插件可以作为项目创建过程的一部分,或在后期加入到项目中。它们也可以被归成一组可复用的 preset (详见 插件和 Preset ↓ )。

安装

npm install -g @vue/cli
# OR
yarn global add @vue/cli

# 查看版本
vue --version

基础

快速原型开发

你可以使用 vue servevue build 命令对单个 *.vue 文件进行快速原型开发,不过这需要先额外安装一个 全局的 扩展:

npm install -g @vue/cli-service-global

vue serve缺点 就是它需要安装全局依赖,这使得它在不同机器上的一致性不能得到保证。因此这只适用于快速原型开发。

!总是可以通过 vue serve -hvue build -h 了解它们的用法。

vue serve 使用了和 vue create 创建的项目相同的默认设置 (webpack、Babel、PostCSS 和 ESLint)。

它会在当前目录 自动推导 入口文件 – 入口可以是 main.js、index.js、App.vueapp.vue 中的一个,你也可以显式地指定入口文件。

创建一个项目

vue create my-project

!总是可以通过 vue create -h 了解更多。

NEXT 插件和 Preset

  • State "NEXT" from [2020-02-12 Wed 17:55]

1. 插件

Vue CLI 使用了一套基于插件的架构。

如果你查阅一个新创建项目的 package.json ,就会发现依赖都是以 @vue/cli-plugin- 开头的。插件可以 修改 webpack 的内部配置,也可以向 vue-cli-service 注入 命令。在项目创建的过程中,绝大部分列出的特性都是通过插件来实现的。

1.1 在现有的项目中安装插件

每个 CLI 插件都会包含一个 (用来创建文件的) 生成器 和一个 (用来调整 webpack 核心配置和注入命令的) 运行时插件

当你使用 vue create 来创建一个新项目的时候,有些插件会根据你选择的特性被预安装好。如果你想在一个已经被创建好的项目中安装一个插件,可以使用 vue add 命令:

vue add eslint

vue add 的设计意图是为了安装和调用 Vue CLI 插件。这并不意味着替换掉普通的 npm 包,你仍然需要选用包管理器。

!我们推荐在运行 vue add 之前将项目的最新状态提交,因为该命令可能调用插件的文件生成器并很有可能更改你现有的文件。

1.2 项目本地的插件

2. Preset

一个 Vue CLI preset 是一个包含创建新项目所需预定义选项和插件的 JSON 对象 ,让用户无需在命令提示中选择它们。

vue create 过程中保存的 preset 会被放在你的 home 目录下的一个配置文件中 (~/.vuerc)。你可以通过直接编辑这个文件来调整、添加、删除保存好的 preset。

这里有一个 preset 的示例:

{
  "useConfigFiles": true,
  "cssPreprocessor": "sass",
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb",
      "lintOn": ["save", "commit"]
    },
    "@vue/cli-plugin-router": {},
    "@vue/cli-plugin-vuex": {}
  }
}

Preset 的数据会被插件生成器用来生成相应的项目文件。除了上述这些字段,你也可以为集成工具添加配置:

{
  "useConfigFiles": true,
  "plugins": {...},
  "configs": {
    "vue": {...},
    "postcss": {...},
    "eslintConfig": {...},
    "jest": {...}
  }
}

这些额外的配置将会根据 useConfigFiles 的值被合并到 package.json 或相应的配置文件中。例如,当 "useConfigFiles": true 的时候, configs 的值将会被合并到 vue.config.js 中。

1. Preset 插件的版本管理

2. 允许插件的命令提示

3. 远程 Preset

4. 加载文件系统中的 Preset

CLI 服务

1. 使用命令

在一个 Vue CLI 项目中, @vue/cli-service 安装了一个名为 vue-cli-service 的命令。你可以在 npm scripts 中以 vue-cli-service 、或者从终端中以 ./node_modules/.bin/vue-cli-service 访问这个命令。

这是你使用默认 preset 的项目的 package.json

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  }
}

你可以通过 npm 或 Yarn 调用这些 script:

npm run serve
# OR
yarn serve

如果你可以使用 npx (最新版的 npm 应该已经自带),也可以直接这样调用命令:

npx vue-cli-service serve

2. vue-cli-service serve

3. vue-cli-service build

4. vue-cli-service inspect

5. 查看所有的可用命令

有些 CLI 插件会向 vue-cli-service 注入额外的命令。例如 @vue/cli-plugin-eslint 会注入 vue-cli-service lint 命令。你可以运行以下命令查看所有注入的命令:

npx vue-cli-service help

6. 缓存和并行处理

7. Git Hook

8. 配置时无需 Eject

TODO 开发

浏览器兼容性

HTML 和静态资源

CSS 相关

webpack 相关

环境变量和模式

构建目标

部署

TODO 配置参考

TODO 插件开发指南

NEXT Vue Loader

  • State "NEXT" from "TODO" [2020-02-14 Fri 12:32]

Vue Loader 官方文档

介绍

Vue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs) ↓ 的格式撰写 Vue 组件。它提供了很多炫酷的特性:

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • ?允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 <style><template> 中引用的资源当作模块依赖来处理;
  • 为每个组件模拟出 scoped CSS;
  • 在开发过程中使用热重载来保持状态。

简而言之,webpack 和 Vue Loader 的结合为你提供了一个现代、灵活且极其强大的前端工作流,来帮助撰写 Vue.js 应用。

起步

如果你不想手动设置 webpack,我们推荐使用 Vue CLI 直接创建一个项目的脚手架。通过 Vue CLI 创建的项目会针对多数常见的开发需求进行预先配置,做到开箱即用。

如果 Vue CLI 提供的内建没有满足你的需求,或者你乐于从零开始创建你自己的 webpack 配置,那么请继续阅读这篇指南。

手动设置

1. 安装

你应该将 vue-loadervue-template-compiler 一起安装 – 除非你是使用自行 fork 版本的 Vue 模板编译器的高阶用户:

npm install -D vue-loader vue-template-compiler

vue-template-compiler 需要独立安装的原因是你可以单独指定其版本。

每个 vue 包的新版本发布时,一个相应版本的 vue-template-compiler 也会随之发布。编译器的版本必须和基本的 vue 包保持同步,这样 vue-loader 就会生成兼容运行时的代码。这意味着 你每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler

2. webpack 配置

Vue Loader 的配置和其它的 loader 不太一样。除了通过一条规则将 vue-loader 应用到所有扩展名为 .vue 的文件上之外,请 确保 在你的 webpack 配置中添加 Vue Loader 的插件。

!这个插件是必须的。 它的职责是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。例如,如果你有一条匹配 /\.js$/ 的规则,那么它会应用到 .vue 文件里的 <script> 块。

一个更完整的 webpack 配置示例看起来像这样:

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      // 它会应用到普通的 `.js` 文件
      // 以及 `.vue` 文件中的 `<script>` 块
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      // 它会应用到普通的 `.css` 文件
      // 以及 `.vue` 文件中的 `<style>` 块
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    // 请确保引入这个插件来施展魔法
    new VueLoaderPlugin()
  ]
}

你也可以在 选项参考 ↓ 查阅所有可用的 loader 选项。

如果你在开发一个库或多项目仓库 (monorepo),请注意导入 CSS 是具有副作用的 。请确保在 package.json 中移除 "sideEffects": false ,否则 CSS 代码块会在生产环境构建时被 webpack 丢掉。

处理资源路径

当 Vue Loader 编译单文件组件中的 <template> 块时,它也会将所有遇到的资源 URL 转换为 webpack 模块请求

例如,下面的模板代码片段:

<img src="../image.png">

↓ 将会被编译成为 ↓

createElement('img', {
    attrs: {
        src: require('../image.png') // 现在这是一个模块的请求了
    }
})

默认下列标签/特性的组合会被转换,且这些组合时可以使用 transformAssetUrls ↓ 选项进行配置的。

{
  video: ['src', 'poster'],
  source: 'src',
  img: 'src',
  image: ['xlink:href', 'href'],
  use: ['xlink:href', 'href']
}

此外,如果你配置了为 <style> 块使用 css-loader,则你的 CSS 中的资源 URL 也会被同等处理。

转换规则

资源 URL 转换会遵循如下规则:

  • 如果路径是绝对路径 (例如 /images/foo.png ),会原样保留。
  • 如果路径以 . 开头,将会被看作相对的模块依赖,并按照你的本地文件系统上的目录结构进行解析。
  • 如果路径以 ~ 开头,其后的部分将会被看作模块依赖。这意味着你可以用该特性来引用一个 Node 依赖中的资源:
<img src="~some-npm-package/foo.png">
  • 如果路径以 @ 开头,也会被看作模块依赖。如果你的 webpack 配置中给 @ 配置了 alias,这就很有用了。所有 vue-cli 创建的项目都 默认配置 了将 @ 指向 /src

相关的 Loader

因为像 .png 这样的文件不是一个 JavaScript 模块,你需要配置 webpack 使用 file-loader 或者 url-loader 去合理地处理它们。通过 Vue CLI 创建的项目已经把这些 预配置好了

为什么

转换资源 URL 的好处是:

  • file-loader 可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存。此外,这意味着 你可以就近管理图片文件,可以使用相对路径而不用担心部署时 URL 的问题。 使用正确的配置,webpack 将会在打包输出中自动重写文件路径为正确的 URL。
  • url-loader 允许你有条件地将文件转换为内联的 base-64 URL (当文件小于给定的阈值),这会减少小文件的 HTTP 请求数。如果文件大于该阈值,会自动的交给 file-loader 处理。

NEXT 使用预处理器

  • State "NEXT" from [2020-02-14 Fri 11:52]

在 webpack 中,所有的预处理器需要匹配对应的 loader。Vue Loader 允许你使用其它 webpack loader 处理 Vue 组件的某一部分。它会根据 lang 特性以及你 webpack 配置中的规则自动推断出要使用的 loader。

Scoped CSS

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。这类似于 Shadow DOM 中的样式封装。它有一些注意事项,但不需要任何 polyfill。它通过使用 PostCSS 来实现以下转换:

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>

↓ 转换结果 ↓

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

1. 混用本地和全局样式

你可以在一个组件中同时使用有 scoped 和非 scoped 样式:

<style>
/* 全局样式 */
</style>

<style scoped>
/* 本地样式 */
</style>

2. 子组件的根元素

使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的 根节点 会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。

3. 深度作用选择器

4. 动态生成的内容

通过 v-html 创建的 DOM 内容不受 scoped 样式影响,但是你仍然可以通过深度作用选择器来为他们设置样式。

5. 还有一些要留意

  • Scoped 样式不能代替 class 。考虑到浏览器渲染各种 CSS 选择器的方式,当 p { color: red } 是 scoped 时 (即与特性选择器组合使用时) 会慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red } ,性能影响就会消除。
  • 在递归组件中小心使用后代选择器! 对选择器 .a .b 中的 CSS 规则来说,如果匹配 .a 的元素包含一个递归子组件,则所有的子组件中的 .b 都将被这个规则匹配。

TODO CSS Modules

CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统。 vue-loader 提供了与 CSS Modules 的一流集成,可以作为模拟 scoped CSS 的替代方案。

TODO 热重载

TODO 函数式组件

TODO 自定义块

TODO CSS 提取

TODO 代码校验(Linting)

TODO 测试

DONE 单文件组件规范

  • State "DONE" from "NEXT" [2020-02-14 Fri 13:03]
  • State "NEXT" from "TODO" [2020-02-14 Fri 12:36]

简介

.vue 文件是一个自定义的文件类型,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 <template>、<script><style> ,还允许添加可选的自定义块:

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

<custom1>
  This could be e.g. documentation for the component.
</custom1>

vue-loader 会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。

vue-loader 支持使用非默认语言,比如 CSS 预处理器,预编译的 HTML 模版语言,通过设置语言块的 lang 属性。例如,你可以像下面这样使用 Sass 语法编写样式:

<style lang="sass">
  /* write Sass! */
</style>

更多细节可以在 使用预处理器 ↑ 中找到。

语言块

1. 模板

  • 每个 .vue 文件 最多包含一个 <template> 块;
  • 内容将被提取并传递给 vue-template-compiler 为字符串,预处理为 JavaScript 渲染函数,并最终注入到从 <script> 导出的组件中。

2. 脚本

  • 每个 .vue 文件最多包含一个 <script> 块;
  • 这个脚本会作为一个 ES Module 来执行;
  • 它的默认导出应该是一个 Vue.js 的 组件选项对象 ,也可以导出由 Vue.extend() 创建的扩展对象,但是普通对象是更好的选择;
  • 任何匹配 .js 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <script> 块的内容中。

3. 样式

  • 默认匹配: /\.css$/
  • 一个 .vue 文件可以包含多个 <style> 标签;
  • <style> 标签可以有 scoped 或者 module 属性 (查看 scoped CSS ↑CSS Modules ↑ ) 以帮助你将样式封装到当前组件。具有不同封装模式的多个 <style> 标签可以在同一个组件中混合使用;
  • 任何匹配 .css 文件 (或通过它的 lang 特性指定的扩展名) 的 webpack 规则都将会运用到这个 <style> 块的内容中。

4. 自定义块

可以在 .vue 文件中添加额外的自定义块来实现项目的特定需求,例如 <docs> 块。 vue-loader 将会使用标签名来查找对应的 webpack loader 来应用在对应的块上。webpack loader 需要在 vue-loader 的选项 loaders 中指定。

更多细节,查看 自定义块 ↑

5. Src 导入

如果喜欢把 .vue 文件分隔到多个文件中,你可以通过 src 属性导入外部文件:

<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>

需要注意的是 src 导入遵循和 webpack 模块请求相同的路径解析规则,这意味着:

  • 相对路径需要以 ./ 开始
  • 你可以从 NPM 依赖中导入资源:
<!-- import a file from the installed "todomvc-app-css" npm package -->
<style src="todomvc-app-css/index.css">

在自定义块上同样支持 src 导入,例如:

<unit-test src="./unit-test.js">
</unit-test>

语法高亮/ IDE 支持

目前有下列 IDE/编辑器 支持语法高亮:

  • Sublime Text
  • VS Code
  • Atom
  • Vim
  • Emacs
  • Brackets
  • JetBrains IDEs (WebStorm、PhpStorm 等)

非常感谢其他编辑器/IDE 所做的贡献!如果在 Vue 组件中没有使用任何预处理器,你可以把 .vue 文件当作 HTML 对待。

注释

在语言块中使用该语言块对应的注释语法 (HTML、CSS、JavaScript、Jade 等)。顶层注释使用 HTML 注释语法: <!-- comment contents here -->

TODO 选项参考

transformAssetUrls

Vue Router

Vue Router 官方文档

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

安装

1. 直接下载/CDN

在 Vue 后面加载 vue-router,它会 自动安装 的:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vue-router.js"></script>

2. NPM

npm install vue-router

如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

如果使用全局的 script 标签,则无须如此 (手动安装)。

基础

起步

用 Vue.js + Vue Router 创建单页应用,是非常简单的。使用 Vue.js ,我们已经可以通过组合组件来组成应用程序,当你要把 Vue Router 添加进来,我们需要做的是, 将组件 ( components ) 映射到路由 ( routes ) ,然后告诉 Vue Router 在哪里渲染它们。

来看一个基本例子:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>

要注意,当 <router-link> 对应的路由匹配成功,将自动设置 class 属性值 .router-link-active

// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
  routes // (缩写) 相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

// 现在,应用已经启动了!

!通过注入路由器,我们可以在任何组件内通过 this.$router 访问路由器,也可以通过 this.$route 访问当前路由。

??? this.$routerrouter 使用起来完全一样。我们使用 this.$router 的原因是我们并不想在每个独立需要封装路由的组件中都导入路由。

动态路由匹配

我们经常需要把某种模式 匹配到的所有路由 ,全都映射到 同个组件

例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用 “动态路径参数” (dynamic segment) 来达到这个效果:

const User = {
  template: '<div>User</div>'
}

const router = new VueRouter({
  routes: [
    // 动态路径参数 以冒号开头
    { path: '/user/:id', component: User }
  ]
})

一个 “路径参数” 使用冒号 : 标记。当匹配到一个路由时,参数值会被设置到 this.$route.params ,可以在每个组件内使用。

你可以在一个路由中设置 多段“路径参数” ,对应的值都会设置到 $route.params 中。如下:

模式 匹配路径 $route.params
/user/:username /user/evan { username: 'evan' }
/user/:username/post/:post_id /user/evan/post/123 { username: 'evan', post_id: '123' }

除了 $route.params 外,$route 对象还提供了其它有用的信息,例如, $route.query (如果 URL 中有查询参数)、 $route.hash 等等。 详见 API 文档

1. 响应路由参数的变化

提醒一下,当使用路由参数时,例如从 /user/foo 导航到 /user/bar原来的组件实例会被复用 。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着 组件的生命周期钩子不会再被调用

复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象:

const User = {
  template: '...',
  watch: {
    '$route' (to, from) {
      // 对路由变化作出响应...
    }
  }
}

或者使用 2.2 中引入的 beforeRouteUpdate 导航守卫 ↓

const User = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

2. 捕获所有路由或 404 Not found 路由

常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 ( * ):

{
  // 会匹配所有路径
  path: '*'
}
{
  // 会匹配以 `/user-` 开头的任意路径
  path: '/user-*'
}

当使用通配符路由时,请确保路由的顺序是正确的,也就是说 含有通配符的路由应该放在 最后 。路由 { path: '*' } 通常用于客户端 404 错误。

当使用一个通配符时, $route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分:

// 给出一个路由 { path: '/user-*' }
this.$router.push('/user-admin')
this.$route.params.pathMatch // 'admin'
// 给出一个路由 { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

3. 高级匹配模式

vue-router 使用 path-to-regexp 作为路径匹配引擎,所以支持很多高级的匹配模式,例如:可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。

4. 匹配优先级

有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序: 谁先定义的,谁的优先级就最高

嵌套路由

实际生活中的应用界面,通常由多层嵌套的组件组合而成。同样地,URL 中各段动态路径也按某种结构对应嵌套的各层组件,例如:

/user/foo/profile                     /user/foo/posts
+------------------+                  +-----------------+
| User             |                  | User            |
| +--------------+ |                  | +-------------+ |
| | Profile      | |  +------------>  | | Posts       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+

借助 vue-router ,使用嵌套路由配置,就可以很简单地表达这种关系。

一个被渲染组件同样可以包含自己的嵌套 <router-view> ,要在嵌套的出口中渲染组件,需要在 VueRouter 的参数中使用 children 配置:

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User,
      children: [
        {
          // 当 /user/:id/profile 匹配成功,
          // UserProfile 会被渲染在 User 的 <router-view> 中
          path: 'profile',
          component: UserProfile
        },
        {
          // 当 /user/:id/posts 匹配成功
          // UserPosts 会被渲染在 User 的 <router-view> 中
          path: 'posts',
          component: UserPosts
        }
      ]
    }
  ]
})

!要注意,以 / 开头的嵌套路径会被当作根路径。 这让你充分的使用嵌套组件而无须设置嵌套的路径。

你会发现, children 配置就是像 routes 配置一样的路由配置数组,所以呢,你可以嵌套多层路由。

此时,基于上面的配置,当你访问 /user/foo 时,User 的出口是不会渲染任何东西,这是因为没有匹配到合适的子路由。如果你想要渲染点什么,可以提供一个 空的 子路由:

const router = new VueRouter({
  routes: [
    {
      path: '/user/:id', component: User,
      children: [
        // 当 /user/:id 匹配成功,
        // UserHome 会被渲染在 User 的 <router-view> 中
        { path: '', component: UserHome },

        // ...其他子路由
      ]
    }
  ]
})

NEXT 编程式的导航

  • State "NEXT" from [2020-02-12 Wed 16:28]

除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。

router.push(location, onComplete?, onAbort?)

注意:在 Vue 实例内部,你可以通过 $router 访问路由实例。因此你可以调用 this.$router.push

命名路由

有时候,通过一个名称来标识一个路由显得更方便一些,特别是在 链接 一个路由,或者是执行一些 跳转 的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。

const router = new VueRouter({
  routes: [
    {
      path: '/user/:userId',
      name: 'user',
      component: User
    }
  ]
})

要链接到一个命名路由,可以给 router-linkto 属性传一个对象:

<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>

↓ 等价于

router.push({ name: 'user', params: { userId: 123 }})

这两种方式都会把路由导航到 /user/123 路径。

NEXT 命名视图

  • State "NEXT" from [2020-02-12 Wed 16:38]

有时候想 同时 (同级) 展示多个视图 ,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候 命名视图 就派上用场了。

你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default

<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>

一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: Foo,
        a: Bar,
        b: Baz
      }
    }
  ]
})

嵌套命名视图

// TODO ….

NEXT 重定向和别名

  • State "NEXT" from "TODO" [2020-02-13 Thu 22:19]

1. 重定向

重定向 的意思时,当用问访问 /a 时, URL 将会被替换成 /b ,然后匹配路由为 /b

重定向的目标可以是一个 字符串路径/路径对象

const router = new VueRouter({
    routes: [
        { path: '/a', redirect: '/b' }
        // OR
        // { path: '/a', redirect: { path: '/b' }}
    ]
})

重定向的目标也可以是一个 命名的路由

const router = new VueRouter({
    routes: [
        { path: '/a', redirect: { name: 'foo' }}
    ]
})

甚至是一个 方法 ,动态返回重定向目标:

const router = new VueRouter({
    routes: [
        { path: '/a', redirect: to => {
            // 方法接收目标路由作为参数
            // return 重定向的字符串路径/路径对象
        }}
    ]
})

?TODO 注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。在下面这个例子中,为 /a 路由添加一个 beforeEachbeforeLeave 守卫并不会有任何效果。

2. 别名

/a别名/b ,意味着,当用户访问 /b 时, URL 会 保持/b ,但是路由匹配则为 /a ,就像用户访问 /a 一样。

const router = new VueRouter({
    routes: [
        { path: '/a', component: A, alias: '/b'}
    ]
})

“别名”的功能让你可以自由地将 UI 结构映射到任意的 URL,而不是受限于配置的嵌套路由结构。

路由组件传参

在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。

使用 props 将组件和路由解耦:

取代与 $route 的耦合 ↓

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

↓ 通过 props 解耦

const User = {
    props: ['id'],
    template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User, props: true },

    // 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
    {
      path: '/user/:id',
      components: { default: User, sidebar: Sidebar },
      props: { default: true, sidebar: false }
    }
  ]
})

这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。

1. 布尔模式

如果 props 被设置为 trueroute.params 将会被设置为组件属性。

2. 对象模式

如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。

const router = new VueRouter({
  routes: [
    { path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
  ]
})

3. 函数模式

你可以创建一个函数返回 props 。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。

const router = new VueRouter({
  routes: [
    { path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
  ]
})

URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props ,请使用包装组件,这样 Vue 才可以对状态变化做出反应。

TODO HTML5 History 模式

进阶

ANCHOR 导航守卫

  • State "NEXT" from [2020-02-12 Wed 16:57]

“导航”表示路由正在发生改变。

正如其名, vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中: 全局的 , 单个路由独享的 , 或者 组件级的

!记住参数或查询的改变并 不会触发进入/离开的 导航守卫。 你可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

1. 全局前置守卫

你可以使用 router.beforeEach 注册一个 全局前置守卫

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
    // ...
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收 三个参数

  • to: Route :即将要进入的目标路由对象;
  • from: Route :当前导航正要离开的路由;
  • next: Function :一定要调用该方法来 resolve 这些钩子,执行效果依赖 next 方法的调用参数。

next 方法相关:

  • next() :进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
  • next(false) :中断当前的导航。如果浏览器的 URL 该表了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
  • next('/') 或者 next({ path: '/' }) :跳转到了一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置注入 replace: truename: 'home' 之类的选项以及任何用在 router-linkto prop 或 router.push 中的选项。
  • next(error) :(2.4.0+)如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

!确保要调用 next 方法,否则钩子就不会被 resolved 。

2. 全局解析守卫

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似, 区别 是在导航被确认之前, 同时在所有组件内守卫和异步路由组件被解析之后 ,解析守卫就被调用。

3. 全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子 不会接受 next 函数不会改变导航本身

router.afterEach((to, from) => {
    // ...
})

4. 路由独享的守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

const router = new VueRouter({
    routes: [
        {
            path: '/foo',
            component: Foo,
            beforeEnter: (to, from, next) => {
                // ...
            }
        }
    ]
})

这些守卫与全局前置守卫的方法参数是一样的。

5. 组件内的守卫

最后,你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

beforeRouteEnter 守卫 不能 访问 this ,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next访问组件实例 。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。 对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为 没有必要了

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

6. TODO 完整的导航解析流程

  1. 导航被触发;
  2. 在失活的组件里调用离开守卫;
  3. 调用全局的 beforeEach 守卫;
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件;
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+);
  9. 导航被确认;
  10. 调用全局的 afterEach 钩子;
  11. 出发 DOM 更新;
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

TODO 路由元信息

TODO 过渡动效

TODO 数据获取

TODO 滚动行为

NEXT 路由懒加载

  • State "NEXT" from [2020-02-13 Thu 23:14]

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。

如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue 的 异步组件 ↑ 和 Webpack 的 代码分割功能 ,轻松实现路由组件的懒加载。

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2 中,我们可以使用 动态 import 语法来定义代码分块点 (split point):

import('./Foo.vue') // 返回 Promise

注意:如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。

结合这两者,这就是如何定义一个 能够被 Webpack 自动代码分割的异步组件

const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用 Foo

const router = new VueRouter({
  routes: [
    { path: '/foo', component: Foo }
  ]
})

把组件按组分块

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk ,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

NEXT Vuex

  • State "NEXT" from [2020-02-13 Thu 00:29]

Vuex 官方文档

安装

安装方式:

  • 直接下载/CDN 引用;
  • Npm/Yarn (模块打包系统中,必须显示通过 Vue.use() 来安装 Vuex)。

!Vuex 依赖 Promise 。

如果你支持的浏览器并没有实现 Promise (比如 IE),那么你可以使用一个 polyfill 的库,例如 es6-promise。

介绍

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

把组件的共享状态抽取出来,以一个 全局单例模式 管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此 – 如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式 ↑ 就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

开始

每一个 Vuex 应用的 核心 就是 store(仓库),“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是 响应式 的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 你不能直接改变 store 中的状态。改变 store 中的状态的 唯一途径 就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

最简单的 Store

创建过程很简单 – 仅需要提供一个初始 state 对象和一些 mutation

// 如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++
        }
    }
})

现在,我们就可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment')
console.log(store.state.count)  // → 1

再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count ,是因为我们想要更明确地追踪到状态的变化。

这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。

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

核心概念

State

1. 单一状态树

Vuex 使用 单一状态树 – 是的,用一个对象就包含了 全部 的应用层级状态。至此它便作为一个 “唯一数据源” 而存在。

这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突,我们仍然可以将状态和状态变更事件分布到各个子模块中。

2. 在 Vue 组件中获得 Vuex 状态

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

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

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

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

如何解决呢?

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

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

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

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

3. mapState 辅助函数

当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

// 在独单构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
    // ...
    computed: mapState({
        // 箭头函数可是代码更简练
        count: state => state.count,

        // 传字符串参数 'count' 等同于 'state => state.count'
        countAlias: 'count',

        // 为了能够使用 'this' 获取局部状态,必须使用床柜函数
        countPlusLocalState(state) {
            return state.count + this.localCount
        }
    })
}

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个 字符串数组

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

4. 对象展开运算符

mapState 函数返回的是一个 对象 。我们如何将它与局部计算属性混合使用呢?

通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

5. 组件仍然保有局部状态

使用 Vuex 并 意味着你需要将 所有的 状态放入 Vuex 。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。

Getter

getter 可以认为是 store 的计算属性。

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

computed: {
    doneTodoCount() {
        return this.$store.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)
        }
    }
})

1. 通过属性访问

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

store.getters.doneTodos         // → [{ id: 1, text: '...', done: true }]

Getter 也可以接受其他 getter 作为第二个参数:

getters: {
    // ...
    doneTodoCount: (state, getters) => {
        return getters.doneTodos.length
    }
}
store.getters.doneTodoCount     // → 1

我们也很容易在任何组件中使用它:

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

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

2. 通过方法访问

你可以通过让 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 在通过方法访问时,每次都会去进行调用,而 不会缓存结果

3. mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果你想将一个 getter 属性领取一个名字,使用对象形式:

mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutation

!更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。

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

const store = new Vuex.Store({
    store: {
        count: 1
    },
    mutations: {
        // 'increment': (state) => {
        //     // 变更状态
        //     state.count++
        // }
        // i.e.
        increment(state) {
            state.count++
        }
    }
})

你不能直接调用一个 mutation handler。这个选项 更像是事件注册 :“当触发一个类型为 increment 的 mutation 时,调用此函数。”

要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')

1. 提交载荷(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
})

2. 对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

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

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

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

3. Mutation 需遵守 Vue 的响应规则

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

  • 最好提前在你的 store 中初始化好所有所需属性。
  • 当需要在对象上添加新属性时,你应该
  • 使用 Vue.set(obj, 'newProp', 123) , 或者
  • 以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }

4. 使用常量替代 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
    }
  }
})

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

5. Mutation 必须是同步函数

!一条重要的原则就是要记住 mutation 必须是同步函数。

为什么呢?请参考下面的例子:

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

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

6. 在组件中提交 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')`
    })
  }
}

7. 下一步: Action

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中, mutation 都是同步事务

store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。

为了处理异步操作,让我们来看一看 Action。

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

让我们来注册一个简单的 action:

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 stategetters

当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

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

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

1. 分发 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 产生的副作用(即状态变更)。

2. 在组件中分发 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')`
    })
  }
}

3. 组合 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 才会执行。

NEXT Module

  • State "NEXT" from [2020-02-13 Thu 00:20]

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状态

1. 模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是 模块的局部状态对象

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
    }
  }
}

2. 命名空间

项目结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  • 应用层级的状态应该集中到单个 store 对象中。
  • 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  • 异步逻辑都应该封装到 action 里面。

只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

请参考 购物车示例

插件

严格模式

表单处理

测试

热重载

Date: 2020-02-11 Tue 15:33

Author: Jack Liu

Created: 2020-02-25 Tue 13:04

Validate