概念

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

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

索引

官网:https://vuejs.org
中文官网:https://cn.vuejs.org
API:https://cn.vuejs.org/v2/api/

安装vue

在线使用:

<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

安装教程:https://cn.vuejs.org/v2/guide/installation.html

可以直接下载并使用<script>引用:
开发版本:https://cn.vuejs.org/js/vue.js
生产版本:https://cn.vuejs.org/js/vue.min.js

使用npm安装:npm install vue

使用CLI:https://cli.vuejs.org/

API

参见:https://cn.vuejs.org/v2/api/
可以直接浏览器内全文搜索查找。

基本语法

声明式渲染

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

通过 “Mustache”语法 (双大括号) 进行文本插值,使数据和 DOM 建立关联,所有东西都是响应式的。修改 app.message 的值,则展示的信息会相应地更新。

v-bind 特性被称为指令。指令带有前缀 v-,以表示它们是 Vue 提供的特殊特性。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。一些指令能够接收一个“参数”,在指令名称之后以冒号表示。
Mustache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令。
下面例子里,v-bind指令的意思是:“将这个元素节点的 title 特性和 Vue 实例的 message2 属性保持一致”。

<div id="app">
  <!-- 2个大括号来引用数据 -->
  {{ message }}
  <!-- 使用v-bind标签来绑定数据 -->
  <span v-bind:title="message2">
    鼠标悬停几秒钟查看此处动态绑定的提示信息!
  </span>
</div>

<script type="text/javascript">
var app = new Vue({
  // 绑定的元素
  el: '#app',
  // 导出的数据
  data: {
    message: 'Hello Vue!'
    message2: '页面加载于 ' + new Date().toLocaleString()
  }
})
</script>

动态参数

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

<!--
注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。
-->
<a v-bind:[attributeName]="url"> ... </a>

表单输入绑定

v-model 指令,它能轻松实现表单输入和应用状态之间的双向绑定。

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

<div id="app-6">
  <p>{{ message }}</p>
  <input v-model="message">
</div>

<script>
var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})
</script>

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

  • text 和 textarea 元素使用 value 属性和 input 事件;
  • checkbox 和 radio 使用 checked 属性和 change 事件;(单个复选框,绑定到布尔值;多个复选框,绑定到同一个数组)
  • select 字段将 value 作为 prop 并将 change 作为事件。

自定义输入组件也可以使用v-model。

model可以使用.lazy,.number,.trim修饰符。

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

<!-- 自动将用户的输入值转为数值类型 -->
<input v-model.number="age" type="number">

<!-- 自动过滤用户输入的首尾空白字符 -->
<input v-model.trim="msg">

数据绑定中进行数据处理

使用JS表达式

对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持:

<html>
{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

<!-- 有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。 -->
<!-- 这是语句,不是表达式 -->
{{ var a = 1 }}
<!-- 流控制也不会生效,请使用三元表达式 -->
{{ if (ok) { return message } }}

</html>

计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。对于任何复杂逻辑,你都应当使用计算属性。

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

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

<script>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello',
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  computed: {
    // 计算属性的 getter (计算属性的 getter 函数是没有副作用的)
    // Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  },
  // 侦听属性,等价于:computed: { fullName: function () { return this.firstName + ' ' + this.lastName } }
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})
</script>

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。
然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

<p>Reversed message: "{{ reversedMessage() }}"</p>

<script>
// 在组件中
methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}
computed: {
  // 下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖
  now: function () {
    return Date.now()
  }
}
</script>

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

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter,现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。

当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>

<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 发生改变,这个函数就会运行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
    // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
    // 请参考:https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = 'Questions usually contain a question mark. ;-)'
        return
      }
      this.answer = 'Thinking...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>

Class 与 Style 的数据绑定

在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

Class的数据绑定

  1. 我们可以传给 v-bind:class 一个对象,以动态地切换 class。
  2. 可以在这个对象中传入更多属性来动态切换多个 class。
  3. v-bind:class 指令也可以与普通的 class 属性共存。
  4. 绑定的数据对象不必内联定义在模板里。
  5. 也可以在v-bind:class 指令中绑定一个返回对象的计算属性。
  6. 我们可以把一个数组传给 v-bind:class,以应用一个 class 列表。
  7. 可以在数组中,结合使用对象语法。
  8. 当在一个自定义组件上使用 class 属性时,这些 class 将被添加到该组件的根元素上面。这个元素上已经存在的 class 不会被覆盖。
<!-- 对象语法 -->
<div
  class="static"
  v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>

<!-- 等价的表示形式 -->
<div v-bind:class="classObject"></div>

<!-- 数组语法 -->
<div v-bind:class="[activeClass, errorClass]"></div>
<!-- 数组内可以使用对象 -->
<div v-bind:class="[{ active: isActive }, errorClass]"></div>

<script>
data: {
  // 对象语法中使用,渲染结果为:<div class="static active"></div>
  isActive: true,
  hasError: false,

  // 等价的表示形式
  classObject: {
    active: true,
    'text-danger': false
  },

  // 数组语法中使用,渲染结果为: <div class="active text-danger"></div>
  activeClass: 'active',
  errorClass: 'text-danger'
}
</script>

Style的数据绑定

  1. v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。
  2. 绑定的数据对象不必内联定义在模板里。
  3. 对象语法常常结合返回对象的计算属性使用。
  4. v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上。
  5. 当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。
  6. 从 2.3.0 起你可以为 style 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值。
<!-- 对象语法 -->
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

<!-- 等价表达 -->
<div v-bind:style="styleObject"></div>

<!-- 数组语法,其中,baseStyles和overridingStyles都是对象 -->
<div v-bind:style="[baseStyles, overridingStyles]"></div>

<!-- 这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

<script>
data: {
  // 对象语法使用
  activeColor: 'red',
  fontSize: 30,

  // 等价表达
  styleObject: {
    color: 'red',
    fontSize: '30px'
  }
}
</script>

条件与循环

条件渲染

v-if指令

条件使用v-if指令:
(注:永远不要把 v-if 和 v-for 同时用在同一个元素上。)

<div id="app-3">
  <p v-if="seen">现在你看到我了</p>
  </div>

<script>
var app3 = new Vue({
  el: '#app-3',
  data: {
    seen: true
  }
})
</script>

因为 v-if 是一个指令,所以必须将它添加到一个元素上。如果想切换多个元素,可以把一个 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 元素。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

可以使用 v-else 指令来表示 v-if 的“else 块”。
2.1.0 新增 v-else-if 。

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key 属性即可:

<!-- 每次切换时,输入框都将被重新渲染。 -->
<!-- 但是,<label> 元素仍然会被高效地复用(只是变换文字内容),因为它们没有添加 key 属性。 -->
<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="email-input">
</template>
v-show指令

v-show:带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display。
v-show 不支持 元素。

<h1 v-show="ok">Hello!</h1>

对比 vi-if:

  1. v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
  2. v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
  3. v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

列表(数组)渲染

循环使用v-for指令。
我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>

<script>
var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: '学习 JavaScript' },
      { text: '学习 Vue' },
      { text: '整个牛项目' }
    ]
  }
})
</script>

v-for 还支持一个可选的第二个参数,即当前项的索引。

<li v-for="(item, index) in items">...</li>

也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法:

<div v-for="item of items"></div>

也可以用 v-for 来遍历一个对象的属性。
在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。

<ul id="v-for-object" class="demo">
  <!-- 也可以提供第二个的参数为 property 名称 (也就是键名):v-for="(value, name) in object" -->
  <!-- 还可以用第三个参数作为索引:v-for="(value, name, index) in object -->
  <li v-for="value in object">
    {{ value }}
  </li>
</ul>

<script>
new Vue({
  el: '#v-for-object',
  data: {
    object: {
      title: 'How to do lists in Vue',
      author: 'Jane Doe',
      publishedAt: '2016-04-10'
    }
  }
})
</script>

建议尽可能在使用 v-for 时提供 key attribute,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素。
如果不指定key,默认使用“就地更新”的策略。如果数组中数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素。

我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以在 v-for 中使用计算属性或者方法。例如:n in computedVar/method()

v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。例如:v-for="n in 10"

可以利用带有 v-for 的 来循环渲染一段包含多个元素的内容。

在自定义组件上,你可以像在任何普通元素上一样使用 v-for 。2.2.0+ 的版本里,当在组件上使用 v-for 时,key 现在是必须的。
然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 prop:

<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>

数据更新检测

Vue 将被侦听的数组的变异方法(mutation method,变异方法,顾名思义,会改变调用了这些方法的原始数组,例如:push(), pop(), shift(), splice(), unshift(), sort(), reverse() )进行了包裹,所以它们也将会触发视图更新。
非变异 (non-mutating method) 方法,例如 filter()、concat() 和 slice() 。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

注意:当你利用索引直接设置一个数组项时,或者当你修改数组的长度时,Vue 并不能检测到数组的变动:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的,使用 Vue.set(vm.items, indexOfItem, newValue) 或者 vm.items.splice(indexOfItem, 1, newValue) 代替,也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:vm.$set(vm.items, indexOfItem, newValue)
vm.items.length = 2 // 不是响应性的,使用 vm.items.splice(newLength) 代替

Vue 不能检测对象属性的添加或删除:

var vm = new Vue({
  data: {
    a: 1,
    userProfile: {
      name: 'Anika'
    }
  }
})
// `vm.a` 现在是响应式的

vm.b = 2
// `vm.b` 不是响应式的

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

// 为已有对象赋值多个新属性,应该用两个对象的属性创建一个新的对象
vm.userProfile = Object.assign({}, vm.userProfile, {
  sex: 'female',
  favoriteColor: 'Vue Green'
})

事件监听器

用 v-on 指令添加一个事件监听器,通过它调用在 Vue 实例中定义的方法:

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">反转消息</button>

  <!-- `greet` 是在下面定义的方法名 -->
  <button v-on:click="greet">Greet</button>

  <!-- 有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法: -->
	<button v-on:click="warn('Form cannot be submitted yet.', $event)">
	  Submit
	</button>
</div>

<script>
var app5 = new Vue({
  el: '#app-5',
  data: {
    message: 'Hello Vue.js!'
  },
  // 导出方法
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  },
   greet: function (event) {
     // `this` 在方法里指向当前 Vue 实例
     alert('Hello ' + this.message + '!')
     // `event` 是原生 DOM 事件
     if (event) {
       alert(event.target.tagName)
     }
   },
  warn: function (message, event) {
    // 现在我们可以访问原生事件对象
    if (event) event.preventDefault()
    alert(message)
  }
})
</script>

所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。使用 v-on 有几个好处:

  1. 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
  2. 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
  3. 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。

修饰符

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

<!-- .prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault() -->
<form v-on:submit.prevent="onSubmit">...</form>

事件修饰符

Vue.js 为 v-on 提供了事件修饰符,专门去处理 DOM 事件细节。
支持:.stop, .prevent, .capture, .self, .once(2.1.4 新增), .passive(2.3.0 新增)。
.once 修饰符还能被用到自定义的组件事件上,其它只能对原生的 DOM 事件起作用。

<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成  -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>

按键修饰符

Vue 允许为 v-on 在监听键盘事件时添加按键修饰符。其中按键名称参见:https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
按键码

为了在必要的情况下支持旧浏览器,Vue 提供了绝大多数常用的按键码的别名:.enter,.tab,.delete (捕获“删除”和“退格”键),.esc,.space,.up,.down,.left,.right。推荐优先使用别名。
你还可以通过全局 config.keyCodes 对象自定义按键修饰符别名:

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

系统修饰键

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器:.ctrl,.alt,.shift,.meta(在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。)(前面均为2.1.0 新增),.exact(2.5.0 新增),.left,.right,.middle(2.2.0 新增)

<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。 -->
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

<!-- 这些修饰符会限制处理函数仅响应特定的鼠标按钮。 -->
<!-- .left,.right,.middle -->

指令的缩写

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

<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>

<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>

组件

在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。每用一次组件,就会有一个它的新实例被创建。

组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。

Vue的组件和原生自定义组件并不相同,具体参见:https://cn.vuejs.org/v2/guide/index.html#与自定义元素的关系

注册组件

在 Vue 中注册组件很简单:

<div id="app-7">
  <ol>
    <!--
      现在我们为每个 todo-item 提供 todo 对象
      todo 对象是变量,即其内容可以是动态的。
      我们也需要为每个组件提供一个“key”,稍后再
      作详细解释。
    -->
    <todo-item
      v-for="item in groceryList"
      v-bind:todo="item"
      v-bind:key="item.id"
    ></todo-item>
  </ol>
</div>

<script>
// 定义名为 todo-item 的新组件
Vue.component('todo-item', {
  // todo-item 组件现在接受一个
  // "prop",类似于一个自定义特性。
  // 这个 prop 名为 todo。
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})

var app7 = new Vue({
  el: '#app-7',
  data: {
    groceryList: [
      { id: 0, text: '蔬菜' },
      { id: 1, text: '奶酪' },
      { id: 2, text: '随便其它什么人吃的东西' }
    ]
  }
})
</script>

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。
全局注册的组件可以用在其被注册之后的任何 (通过 new Vue) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。

// 全局注册。
Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })
// 其后的vue实例,其对应的组件树<div id="app">中所有子组件都可以使用
new Vue({ el: '#app' })

<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

可以使用 JavaScript 的模板字符串来让多行的模板更易读。
模板字符串使用反引号 () 来代替普通字符串中的用双引号和单引号。在模版字符串内使用反引号(`)时,需要在它前面加转义符(\)。
模板字符串在 IE 下并没有被支持,所以如果你需要在不 (经过 Babel 或 TypeScript 之类的工具) 编译的情况下支持 IE,请使用折行转义字符取而代之。

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <div v-html="post.content"></div>
    </div>
  `
})

组件名和Prop名

组件名,推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

推荐使用 kebab-case (短横线分隔命名) 定义一个组件,例如 <my-component-name>。
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name> 和 <MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

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

局部注册

全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。

局部注册方式:

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

new Vue({
  el: '#app',
  components: {
    // 对于 components 对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

注意,局部注册的组件在其子组件中不可用。

// 如果希望 ComponentA 在 ComponentB 中可用:
var ComponentA = { /* ... */ }

var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

在模块系统中局部注册

我们推荐创建一个 components 目录,并将每个组件放置在其各自的文件中。然后,在局部注册之前导入每个你想使用的组件。

// ComponentB.vue 文件
import ComponentA from './ComponentA'
import ComponentC from './ComponentC'

export default {
  components: {
    // 让 ComponentA 和 ComponentC 都可以在 ComponentB 的模板中使用:
    ComponentA,
    ComponentC
  },
  // ...
}

自动化全局注册

示例代码:

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 实例 (通过 new Vue) 创建之前发生
  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,
    // 那么就会优先使用 `.default`,
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

Prop

可以以对象形式列出 prop,这些属性的名称和值分别是 prop 各自的名称和类型:

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

使用Prop时也可以传入任何类型:

<!-- 传入一个数组:即便数组是静态的,我们仍然需要 `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>

<!-- 如果你想要将一个对象的所有属性都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name) -->
post: {
  id: 1,
  title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>
<!-- 以上等价于 -->
<blog-post
  v-bind:id="post.id"
  v-bind:title="post.title"
></blog-post>

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。
这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。注意,在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。

如果确实需要在子组件使用父组件的Prop属性,有2个建议的方法:

// 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

// 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

可以为组件的 prop 指定验证要求,为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

组件可以接受任意的特性,而这些特性会被添加到这个组件的根元素上。
对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。
class 和 style 特性会稍微智能一些,即两边的值会被合并起来:

<!-- <bootstrap-date-input>的模板如下: -->
<input type="date" class="form-control">

<!-- class 最终的值:form-control date-picker-theme-dark -->
<bootstrap-date-input
  data-date-picker="activated"
  class="date-picker-theme-dark"
></bootstrap-date-input>

如果你不希望组件的根元素继承特性,你可以在组件的选项中设置 inheritAttrs: false。
注意, inheritAttrs: false 选项不会影响 style 和 class 的绑定。

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  template: `
    <label>
      {{ label }}
      <input
        // 实例的 $attrs 属性包含了传递给一个组件的特性名和特性值,可以让父组件的任意特性来作用到子组件
        v-bind="$attrs"
        v-bind:value="value"
        v-on:input="$emit('input', $event.target.value)"
      >
    </label>
  `
})

组件的数据

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

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。需要组件内的<input>满足以下条件:

  • 将其 value 特性绑定到一个名叫 value 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
<!-- v-model本质 -->
<input v-model="searchText">
等价于:
<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

<!-- 组件上的v-model的本质 -->
<custom-input v-model="searchText"></custom-input>
等价于:
<custom-input
  v-bind:value="searchText"
  v-on:input="searchText = $event"
></custom-input>
所以,组件要支持:
Vue.component('custom-input', {
  props: ['value'],
  template: `
    <input
      v-bind:value="value"
      v-on:input="$emit('input', $event.target.value)"
    >
  `
})

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

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

使用 .sync 修饰符对一个 prop 进行“双向绑定”。类似 v-model,带有 .sync 修饰符的 v-bind 不能和表达式一起使用,只能提供你想要绑定的属性名。

<script>
// 触发更新事件
this.$emit('update:title', newTitle)
</script>

<text-document v-bind:title.sync="doc.title"></text-document>
<!-- 等价于 -->
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
插槽

通过插槽方式使用数据,插槽内可以包含任何模板代码,包括 HTML。

<!-- 如果 <alert-box> 没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。 -->
<alert-box>
  <!-- 添加一个 Font Awesome 图标 -->
  <span class="fa fa-user"></span>
  Something bad happened.
</alert-box>

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <!-- 插槽,接收组件标签内的内容 -->
      <slot></slot>
    </div>
  `
})

<!-- 为插槽指定默认值,当使用组件并且不提供任何插槽内容时会渲染默认值 -->
<button type="submit">
  <slot>Submit</slot>
</button>

<!-- 动态指令参数也可以用在 v-slot 上,来定义动态的插槽名: -->
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
插槽跟模板的其它地方一样可以访问相同的实例属性 (也就是相同的“作用域”),而不能访问所属组件的作用域。

为了让 属性 在父级的插槽内容中可用,我们可以将 该属性 作为 <slot> 元素的一个特性绑定上去。绑定在 <slot> 元素上的特性被称为插槽 prop。

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

<!-- 让 user 在父级的插槽内容中可用的例子,组件定义: -->
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>
<!-- 在父级作用域中,我们可以给 v-slot 带一个值来定义我们提供的插槽 prop 的名字:-->
<current-user>
  <!-- 当被提供的内容有且只有默认插槽时,下述可以缩写为 v-slot="slotProps" -->
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>
<!-- 等价方式:使用 ES2015 解构来传入具体的插槽 prop -->
<!-- 实际上, ES2015 解构还提供了prop 重命名和默认值功能,例如:
{ user: person }
{ user = { firstName: 'Guest' } }
 -->
<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>

通过具名插槽的方式来使用多个插槽。
v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header。
同理,v-slot:default=“slotProps” 可以缩写为 #default=“slotProps”。

<!-- 组件的模板内容 -->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <!-- 不带 name 会带有隐含的名字“default”。 -->
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!-- 使用组件时:我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容 -->
  <p>A paragraph for the main content.</p>
<template v-slot:default>
  <p>And another one.</p>
</template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

组件的事件

子组件可以通过调用内建的 $emit 方法 并传入事件名称来触发一个事件;
注:事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。并且, v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。因此,推荐你始终使用 kebab-case 的事件名。
父级组件可以像处理 native DOM 事件一样通过 v-on 监听子组件实例的任意事件。

Vue.component('blog-post', {
  props: ['post'],
  template: `
    <div class="blog-post">
      <h3>{{ post.title }}</h3>
      <!-- 抛出一个名为enlarge-text的事件 -->
	  <button v-on:click="$emit('enlarge-text')">
	    Enlarge text
	  </button>
      <div v-html="post.content"></div>
    </div>
  `
})

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

可以使用 $emit 的第二个参数来使用事件抛出一个值;
当在父级组件监听这个事件的时候,可以通过 $event 访问到被抛出的这个值。

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

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

<!-- 如果这个事件处理函数是一个方法,这个值将会作为第一个参数传入这个方法 -->
<blog-post
  ...
  v-on:enlarge-text="onEnlargeText"
></blog-post>
<script>
methods: {
  onEnlargeText: function (enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}
</script>

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

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

Vue 提供了一个 $listeners 属性,它是一个对象,里面包含了作用在这个组件上的所有原生事件的监听器。

Vue.component('base-input', {
  inheritAttrs: false,
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

is特性和动态组件

使用is特性可以在不同组件之间进行动态切换:

<!-- 组件会在 `currentTabComponent` 改变时改变,currentTabComponent是已注册组件的名字,或一个组件的选项对象 -->
<component v-bind:is="currentTabComponent"></component>

<!-- 在组件间切换时,Vue 默认是每次都创建了一个新的 currentTabComponent 组件实例,如果希望组件实例能够被在它们第一次被创建的时候缓存下来需要使用keep-alive标签 -->
<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive>

有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。
例外,从以下来源使用模板的话,这条限制是不存在的:

  • 字符串 (例如:template: ‘…’)
  • 单文件组件 (.vue)
  • <script type=“text/x-template”>
<!-- 错误代码 -->
<table>
  <blog-post-row></blog-post-row>
</table>

<!-- 正确代码 -->
<table>
  <tr is="blog-post-row"></tr>
</table>

异步组件

Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。
一个推荐的做法是将异步组件和 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')
)

异步组件工厂函数也可以返回一个如下格式的对象,以支持处理加载状态等:

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

组件构建应用

在一个大型应用中,有必要将整个应用程序划分为组件,以使开发更易管理。这里有一个 布局Layout 例子,以展示使用了组件的应用模板是什么样的:

<div id="app">
  <app-nav></app-nav>
  <app-view>
    <app-sidebar></app-sidebar>
    <app-content></app-content>
  </app-view>
</div>

vue实例

  1. 每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:new Vue({ // 选项 })
  2. 一个 Vue 应用由一个通过 new Vue 创建的根 Vue 实例,以及可选的嵌套的、可复用的组件树组成。
  3. 当一个 Vue 实例被创建时,它将 data 对象中的所有的属性加入到 Vue 的响应式系统中。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
  4. 值得注意的是只有当实例被创建时就已经存在于 data 中的属性才是响应式的;后来添加一个新的属性,这个属性的改动将不会触发任何视图的更新。
  5. 使用 Object.freeze(),这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。
  6. 除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀 $,以便与用户定义的属性区分开来。例如 vue.$data来引用实例vue的data对象。

实例生命周期的钩子

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

例如:

new Vue({
  data: {
    a: 1
  },
  // created就是一个钩子
  created: function () {
    // `this` 指向 vm 实例
    console.log('a is: ' + this.a)
  }
})

更多的钩子函数和被调用时间参见 生命周期图示:
Vue及Element UI&vue-element-admin简记

模板语法

Vue.js 使用了基于 HTML 的模板语法,所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。

高级语法

组件相关的边界情况