Vue.js 2.0 快速上手 - 基礎篇
Vue 2.0 出來也有一段時間了,作爲一個有志向的全面發展好青年,在征服 Vue 1.x,React,React Native 後,爲了之後能更快遷移公司的項目到 Vue 2.x,於是決定先看看 Vue 2.0。
鑑於部分讀者可能不瞭解 Vue,先簡單看看各種特性。
本文假設你有一定的 HTML 基礎,並熟悉一種或以上編程語言(那就能看懂 JS 了)。
模板語法
Vue 提供了一堆數據綁定語法。
- {{ text }} 文本插值
- {{{ raw_html }}} HTML 輸出
- v-bind HTML 屬性插值。如
- JavaScript 表達式。直接在 mustache、屬性插值裏面使用各種表達式(加減乘除、三元運算、方法調用等)。
- 過濾器(有點類似 Shell 命令中的管道,可以定義過濾器來對原始值進行變化)。
- 指令。之前提到的 v-bind 也是一種指定,其他包括 v-on: 系列(dom 事件的監聽)、v-for、v-model等。
Vue 實例
Vue 實例,實則也就是 ViewModel(數據 + 函數),都是通過構造函數 Vue 創建的:
var data = { a: 1 }
var vm = new Vue({
el: '#example',
data: data,
created: function () {
// `this` points to the vm instance
console.log('a is: ' + this.a)
}
})
vm.$data === data // -> true
vm.$el === document.getElementById('example') // -> true
// $watch is an instance method
vm.$watch('a', function (newVal, oldVal) {
// this callback will be called when `vm.a` changes
})
Vue 實例都有自己的生命週期,比如 created, mounted, updated 以及 destroyed。所有方法被 called 的時候,this 都指向所在的 Vue 實例。
Lifecycle 圖如下:
計算屬性和監聽器
計算屬性
其實就是一個需要計算的 getter:
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
</div>
和使用 method 的區別在於,計算屬性根據它的依賴被緩存,即如果 message 沒有被修改,下次 get 不會進行重複計算,而 method 則每次調用都會重新計算。這也意味着如 Date.now() 這樣返回的計算屬性會永遠得不到更新。
Setter
默認情況下,計算屬性只有一個 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 = 'MarkZhai'
的時候,firstName 和 lastName 都會被更新。
監聽器
Vue 的 watch 也可以用來做類似的事:
<div id="demo">{{ fullName }}</div>
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
對比一下計算屬性版本:
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
看上去好像簡單了很多,那還要 Watcher 幹啥呢。。。主要應用場景是異步或耗時操作:
<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
<script src="https://unpkg.com/[email protected]/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: {
// whenever question changes, this function will run
question: function (newQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.getAnswer()
}
},
methods: {
// _.debounce is a function provided by lodash to limit how
// often a particularly expensive operation can be run.
// In this case, we want to limit how often we access
// yesno.wtf/api, waiting until the user has completely
// finished typing before making the ajax request. To learn
// more about the _.debounce function (and its cousin
// _.throttle), visit: https://lodash.com/docs#debounce
getAnswer: _.debounce(
function () {
var vm = this
if (this.question.indexOf('?') === -1) {
vm.answer = 'Questions usually contain a question mark. ;-)'
return
}
vm.answer = 'Thinking...'
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
})
},
// This is the number of milliseconds we wait for the
// user to stop typing.
500
)
}
})
</script>
如此,使用 watch 讓我們可以進行異步操作(訪問 API),限制操作間隔,並設置中間狀態直到獲得了真正的答案。
除了使用 watch option,也可以用 vm.$watch API。
Class 和 Style 綁定
除了數據綁定,常見的還有 style、class 的綁定(正如很久以前在 JQuery 中常用的)。
對象語法
我們可以傳遞一個對象給 v-bind:class
來動態切換 classes:
<div class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>
對應的 active 和 text-danger 則通過 data 傳遞過來。
我們也可直接通過 data 把 class 傳遞過來
<div v-bind:class="classObject"></div>
data: {
classObject: {
active: true,
'text-danger': false
}
}
當然我們也能使用上面提到的 computed 來進行對應屬性,如 active 的計算。
數組語法
可以直接傳遞一個數組給 v-bind:class:
<div v-bind:class="[activeClass, errorClass]">
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
也可以寫成
<div v-bind:class="[isActive ? activeClass : '', errorClass]">
<div v-bind:class="[{ active: isActive }, errorClass]">
綁定內聯樣式
跟 class 差不多:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
或者直接綁定到 style:
<div v-bind:style="styleObject"></div>
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
類似的,也有數組綁定。
條件綁定
v-if
其實就是個標籤啦
<h1 v-if="ok">Yes</h1>
<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>
因爲 v-if
必須附加到一個單一 element 上,那如果我們想切換多個元素呢?可以使用 template
元素:
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-show
也可以用 v-show
來做條件顯示的邏輯,
<h1 v-show="ok">Hello!</h1>
區別在於
v-show
不支持template
和v-else
v-if
是 lazy 的,不會渲染沒有走到的條件。而v-show
只是簡單的基於 CSS 的切換。所以v-show
的初始 render 代價較高。- 由於
v-if
是真實的渲染,切換後原來的 dom 會被 destroyed,而新的 dom 會被重新創建。所以切換代價更高。
所以如果切換得較爲頻繁可以使用 v-show
,如果在運行時不太會改變則可以使用 v-if
。
列表渲染
v-for
其實就是個循環標籤啦:
<ul id="example-2">
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
對應的 vm 實例:
var example2 = new Vue({
el: '#example-2',
data: {
parentMessage: 'Parent',
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
})
模板 v-for
跟 v-if
類似,我們也能在 template
上使用 v-for
:
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider"></li>
</template>
</ul>
對象 v-for
也能使用 v-for
遍歷對象的屬性:
<ul id="repeat-object" class="demo">
<li v-for="value in object">
{{ value }}
</li>
</ul>
new Vue({
el: '#repeat-object',
data: {
object: {
FirstName: 'John',
LastName: 'Doe',
Age: 30
}
}
})
看到 value,那肯定還有 key 了:
<div v-for="(value, key) in object">
{{ key }} : {{ value }}
</div>
如果再加上 index:
<div v-for="(value, key, index) in object">
{{ index }}. {{ key }} : {{ value }}
</div>
其他還有像是 v-for="n in 10"
這種用法,就不加上例子了。
組件 v-for
input 輸出內容到 newTodoText,每次點擊 enter 都會觸發 addNewTodo,然後添加 item 到 todos,觸發新的 li 添加進去:
<div id="todo-list-example">
<input
v-model="newTodoText"
v-on:keyup.enter="addNewTodo"
placeholder="Add a todo"
>
<ul>
<li
is="todo-item"
v-for="(todo, index) in todos"
v-bind:title="todo"
v-on:remove="todos.splice(index, 1)"
></li>
</ul>
</div>
Vue.component('todo-item', {
template: '\
<li>\
{{ title }}\
<button v-on:click="$emit(\'remove\')">X</button>\
</li>\
',
props: ['title']
})
new Vue({
el: '#todo-list-example',
data: {
newTodoText: '',
todos: [
'Do the dishes',
'Take out the trash',
'Mow the lawn'
]
},
methods: {
addNewTodo: function () {
this.todos.push(this.newTodoText)
this.newTodoText = ''
}
}
})
key
當 vue 在更新被 v-for 渲染的列表時候,會使用就地 patch 的策略,而不是根據元素改變的順序。我們可以提供 key 來做這個排序:
<div v-for="item in items" :key="item.id">
<!-- content -->
</div>
如此,item 會根據 id 來做排序。
數組改變監測
替換方法(mutation)
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
這些方法會改變原來的 array,並自動觸發 view 的更新。
替換 array
- filter()
- concat()
- slice()
這幾個方法會返回新的 array,如:
example1.items = example1.items.filter(function (item) {
return item.message.match(/Foo/)
})
附加說明
如果
- 直接 set array 的值,如 vm.items[indexOfItem] = newValue
- 修改 array 的長度,如 vm.items.length = newLength
都是沒法觸發更新的,需要使用
Vue.set(example1.items, indexOfItem, newValue)
// Array.prototype.splice`
example1.items.splice(indexOfItem, 1, newValue)
example1.items.splice(newLength)
過濾/排序
配合 computed 以及 filter,或者也可以使用 v-for 的條件渲染:
<li v-for="n in even(numbers)">{{ n }}</li>
data: {
numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
even: function (numbers) {
return numbers.filter(function (number) {
return number % 2 === 0
})
}
}
事件處理
監聽事件
使用 v-on
指令監聽 DOM 的各種事件,如:
<div id="example-1">
<button v-on:click="counter += 1">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</div>
var example1 = new Vue({
el: '#example-1',
data: {
counter: 0
}
})
除了直接寫 JS 語句,也可以直接在 v-on 中調用 methods 中定義的事件,還可以進行傳參:
<div id="example-3">
<button v-on:click="say('hi')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
new Vue({
el: '#example-3',
methods: {
say: function (message) {
alert(message)
}
}
})
我們可能也希望直接把 event 給傳遞到方法中(比如在方法裏 preventDefault 或者 stopPropagation),也很 easy,直接使用特殊的 $event 變量就行了。
事件修飾符
除了像上面這樣,在 method 裏面對 event 進行操作,我們還可以使用事件修飾符(Event Modifier):
- .stop
- .prevent
- .capture
- .self
使用如:
<!-- the click event's propagation will be stopped -->
<a v-on:click.stop="doThis"></a>
<!-- the submit event will no longer reload the page -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- modifiers can be chained -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- just the modifier -->
<form v-on:submit.prevent></form>
<!-- use capture mode when adding the event listener -->
<div v-on:click.capture="doThis">...</div>
<!-- only trigger handler if event.target is the element itself -->
<!-- i.e. not from a child element -->
<div v-on:click.self="doThat">...</div>
Key 修飾符
通用的有使用 keyCode 的:
<input v-on:keyup.13="submit">
其他 alias 別名有
- enter
- tab
- delete (captures both “Delete” and “Backspace” keys)
- esc
- space
- up
- down
- left
- right
我們也可以自己通過全局的 config 定義其他別名,如:
// enable v-on:keyup.f1
Vue.config.keyCodes.f1 = 112
表單輸入綁定
基本使用
text
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
如此,用戶的輸入會直接反映到 data 中的 message,然後更新到
。
多行的用 textarea
替換 input
就行了。
Checkbox
單個的:
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
多個的則可以綁到一個 array :
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>Checked names: {{ checkedNames }}</span>
Radio
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>
Select
<select v-model="selected">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Selected: {{ selected }}</span>
Selected: C
多選的在 select 後面加個 multiple,然後對應的會綁定到數組。
還可以結合 v-for
進行動態渲染:
<select v-model="selected">
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
<span>Selected: {{ selected }}</span>
new Vue({
el: '...',
data: {
selected: 'A',
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
]
}
})
值綁定
默認地,像上面這樣,最後 v-model 綁定到的對象,其值會是一個 靜態字符串(或者 true/false),有時候我們想要將其值綁定到一個動態屬性,就可以使用 v-bind
來達到目的。
比如對於 input:
<input
type="checkbox"
v-model="toggle"
v-bind:true-value="a"
v-bind:false-value="b">
// when checked:
vm.toggle === vm.a
// when unchecked:
vm.toggle === vm.b
甚至對象:
<select v-model="selected">
<!-- inline object literal -->
<option v-bind:value="{ number: 123 }">123</option>
</select>
// when selected:
typeof vm.selected // -> 'object'
vm.selected.number // -> 123
修飾符
.lazy
默認地,v-model 在每次 input 事件後都會同步輸入到數據。加上 lazy 修飾符後就會在 change 事件後才同步:
<input v-model.lazy="msg" >
.number
會自動把輸入轉爲 number:
<input v-model.number="age" type="number">
這還是挺有用的,因爲就算限制了 input 的 type 爲 number,元素的 value 仍然會返回 string。
.trim
好像不用多說了?大家都懂吧。
<input v-model.trim="msg">
組件
現代的前端框架,通常都是組件化的了。整個應用的搭建,其實就是組件的拼接。自然 Vue 也不會忘了這個。
使用組件
註冊
註冊一個全局組件,只需要 Vue.component(tagName, options)
即可,如:
Vue.component('my-component', {
// options
})
實際渲染出來的 dom 則定義在 template option 中,如:
// register
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// create a root instance
new Vue({
el: '#example'
})
局部註冊
局部註冊只需要放在 Vue 實例中:
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
// ...
components: {
// <my-component> will only be available in parent's template
'my-component': Child
}
})
使用則像:
<div id="example">
<my-component></my-component>
</div>
Dom 模板解析限制
當使用 Dom 作爲模板(比如使用 el
選項來使用已有內容加載元素),將會受到一些因爲 HTML 工作原理而導致的限制,因爲 Vue 只能在瀏覽器解析後才獲取模板數據並進行處理。比如
可以通過以下方法使用字符串模板,就不會有這些限制:
<script type="text/x-template">
- JavaScript 內聯模板字符串
- .vue 組件
所以,儘量使用字符串模板(string templates)吧。
data 必須是函數
大部分被傳進 Vue 構造函數的選項都能在組件內使用,除了一個特殊情況:data 必須是函數。
如
data: function () {
return {
counter: 0
}
}
而不能是一個在 parent context 的 var(會被多個組件實例共享)或者 object(控制檯會報錯)。
組合組件
組件通常會被一起使用,大部分情況下會有 父——子 關係,如 組件A 在其模板中使用了 組件B。如此,就不免會有相互間的通訊,父親需要傳遞數據給兒子,而兒子則需要通知父親其內部發生的某些事件。
然而,爲了讓組件能避免耦合從而提高複用性和可維護性,又需要使它們相對隔離。
在 Vue.js 中,這種 父——子 組件關係可以被總結爲 props down, events up ,即父組件通過 props 傳遞數據給子組件,而子組件通過 event 發消息給父組件。
熟悉 React 的話,你可能會想到 props 和 state。
props
通過 props 傳遞數據
每個組件都是相互隔離的,所以無法在子組件的 template 中引用父組件的數據。數據只能通過 props 傳遞。
比如我們可以這麼註冊子組件:
Vue.component('child', {
// 申明 props
props: ['message'],
// 跟 data 一樣,可以在 vm (this.message) 和 template 中直接使用
template: '<span>{{ message }}</span>'
})
然後如此傳遞 props:
<child message="hello!"></child>
camelCase vs. kebab-case
因爲 HTML 屬性的限制(大小寫敏感),所以使用 non-string templates 時,camelCased 的屬性必須使用對應的 kebab-case 版本:
Vue.component('child', {
// camelCase in JavaScript
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
<child my-message="hello!"></child>
Again, if you’re using string templates, then this limitation does not apply.
所以都說了,用字符串模板吧。
動態 props
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
如此,my-message 在父組件被改變的時候,都會傳遞更新到子組件。
字面量語法 vs 動態語法
當我們使用
<comp some-prop="1"></comp>
的時候,實際傳遞的是一個字符串,而不是 number 2,如果要傳遞 JavaScript number,則需要使用 v-bind
<comp v-bind:some-prop="1"></comp>
單向數據流
所有的 props 都是單向往下的,父組件 property 更新會影響子組件的,反過來則不會。這樣避免了子組件誤更改父組件狀態,以及應用數據流難以理解。
另外,每次父組件中對應屬性發生改變,子組件中的所有 props 都會被更新爲最新的值。所以在子組件中,不應該對 props 進行更改。
你可能會辯解說傳進來的只是個初始值,或者是個需要計算才能得出真正要的格式的值,但對前者你應該使用本地 data 屬性來引用初始值,後者則應該通過 computed 來做。
prop 檢查
可以對組件接受的 props 定義要求,如:
Vue.component('example', {
props: {
// 基本類型檢查 (`null` 表示接受任何類型)
propA: Number,
// 多種可能的類型
propB: [String, Number],
// 一個必須的 string
propC: {
type: String,
required: true
},
// 一個帶默認值的 number
propD: {
type: Number,
default: 100
},
// 對象/數組的默認值須通過一個工廠方法返回
propE: {
type: Object,
default: function () {
return { message: 'hello' }
}
},
// 自定義檢驗器函數
propF: {
validator: function (value) {
return value > 10
}
}
}
})
自定義事件
我們已經學習了父組件如何傳遞屬性給子組件,那子組件怎麼向上發送數據呢?答案就是自定義事件。
使用 v-on
所有 Vue 實例都實現了 Events 接口 ,即:
- 通過 $on(eventName) 監聽事件
- 通過 $emit(eventName) 觸發事件
如此,我們可以這樣來傳遞事件(定義了 2 個 button,可以發送 increment 事件給父組件觸發 incrementTotal)。
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
template: '<button v-on:click="increment">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
this.counter += 1
this.$emit('increment')
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})
表單輸入
有時候在組件中,我們會有輸入,比如 input,其實 input 中的 v-model
就是:
<input v-bind:value="something" v-on:input="something = $event.target.value">
的一個語法糖。
類似地,爲了讓組件支持 v-model
,它必須:
- 接受 value prop
- 用新的值發送一個 input 事件
讓我們來看看實踐
<div id="v-model-example">
<p>{{ message }}</p>
<my-input
label="Message"
v-model="message"
></my-input>
</div>
Vue.component('my-input', {
template: '\
<div class="form-group">\
<label v-bind:for="randomId">{{ label }}:</label>\
<input v-bind:id="randomId" v-bind:value="value" v-on:input="onInput">\
</div>\
',
props: ['value', 'label'],
data: function () {
return {
randomId: 'input-' + Math.random()
}
},
methods: {
onInput: function (event) {
this.$emit('input', event.target.value)
}
},
})
new Vue({
el: '#v-model-example',
data: {
message: 'hello'
}
})
這個接口不僅能被用在組件內的表單輸入,還能被用在你自己發明的各種組件輸入,像是:
<voice-recognizer v-model="question"></voice-recognizer>
<webcam-gesture-reader v-model="gesture"></webcam-gesture-reader>
<webcam-retinal-scanner v-model="retinalImage"></webcam-retinal-scanner>
非父子組件通訊
有時候兩個組件可能需要互相通訊,但卻不是父子關係。在簡單的場景下,你可以使用一個空的 Vue 實例來作爲中央事件總線(event bus):
var bus = new Vue()
// 在組件 A 的方法中
bus.$emit('id-selected', 1)
// 在組件 B 的 created 中
bus.$on('id-selected', function (id) {
// ...
})
在更復雜的場景下,你可能需要考慮使用 狀態管理模式 ,其實就是 Vuex 了(Vue 版 Redux)。
使用 Slot 分發內容
在使用組件的時候,經常會像這樣組合:
有兩點需要注意的:
- 組件不知道在其掛載點內可能出現的的內容。這是由使用 父組件所決定的。
- 組件很可能有它自己的模板。
爲了讓組件可以組合,我們需要一種方式來混合父組件的內容與子組件自己的模板。這個處理稱爲內容分發。Vue.js 實現了一個內容分發 API,參照了當前 Web 組件規範草稿 中 Slot 的 proposal,使用特殊的
單個Slot
直接看例子吧:
<div>
<h2>I'm the child title</h2>
<slot>
This will only be displayed if there is no content
to be distributed.
</slot>
</div>
父組件這樣使用它:
<div>
<h1>I'm the parent title</h1>
<my-component>
<p>This is some original content</p>
<p>This is some more original content</p>
</my-component>
</div>
最後渲染出來的結果是:
<div>
<h1>I'm the parent title</h1>
<div>
<h2>I'm the child title</h2>
<p>This is some original content</p>
<p>This is some more original content</p>
</div>
</div>
也就是外面的內容被插入到了slot裏面。
具名 Slot
如果你需要多個 slot,也很簡單:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
父組件這麼使用
<app-layout>
<h1 slot="header">Here might be a page title</h1>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<p slot="footer">Here's some contact info</p>
</app-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>
在設計需要組合到一起的組件時,內容分發 API 是非常有用的機制。
動態組件
你可以使用同一個掛載點,並動態地將其切換爲其他 Component。只需要使用保留的 元素並動態綁定到它的 it 屬性:
var vm = new Vue({
el: '#example',
data: {
currentView: 'home'
},
components: {
home: { /* ... */ },
posts: { /* ... */ },
archive: { /* ... */ }
}
})
<component v-bind:is="currentView">
<!-- 當 vm.currentView 改變後 component 類型也會發生改變 -->
</component>
如果你希望的話,也可以直接綁定到 component 對象:
var Home = {
template: '<p>Welcome home!</p>'
}
var vm = new Vue({
el: '#example',
data: {
currentView: Home
}
})
keep-alive
如果把切換出去的組件保留在內存中,可以保留它的狀態或避免重新渲染。爲此可以添加一個 keep-alive 指令參數:
更多詳情可以看 的 API 文檔 。
其他雜項
編寫可複用組件
When authoring components, it’s good to keep in mind whether you intend to reuse it somewhere else later. It’s OK for one-off components to be tightly coupled, but reusable components should define a clean public interface and make no assumptions about the context it’s used in.
一個 Vue 組件的 API 由 3 者組成 —— props, events, 以及 slots:
- Props 允許外部環境傳遞數據到組件內。
- Events 允許組件觸發外部環境的副效應(side effects)。
- Slots 允許外部環境來插入內容到組件的視圖結構內。
通過 v-bind 和 v-on 的簡寫語法,template 可以乾淨簡潔地傳遞意圖:
<my-component
:foo="baz"
:bar="qux"
@event-a="doThis"
@event-b="doThat"
>
<img slot="icon" src="...">
<p slot="main-text">Hello!</p>
</my-component>
:foo 是 v-bind:foo 的簡寫,@event-a 則是 v-on:event-a 的簡寫。
子組件引用
儘管我們有 props 和 events,有時候你可能仍然需要在 JavaScript 中直接操作子組件。爲此你必須使用 ref 分配一個 reference ID 給子組件。
<div id="parent">
<user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 訪問子組件實例
var child = parent.$refs.profile
當 ref 和 v-for 一起使用的時候,你得到的 ref 將會是一個包含了從數據源鏡像的數組或者對象。
$refs 只有在組件被渲染後才能獲得,而且它不是響應式的。也就意味着只是一個直接子組件操作的逃生口 —— 你應該避免在模板或者 computed 屬性中使用 $refs。
異步組件
在大型應用中,我們需要把 app 分成一個個小塊,只在真正需要的時候纔去加載組件。爲了簡化這個,Vue 允許把組件定義爲一個工廠方法,並異步去解析組件定義。Vue 僅僅會在組件真正需要被渲染的時候纔會去觸發該工廠方法,然後把結果緩存下來給以後的再渲染。如:
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
工廠方法接受一個 resolve 回調,會在從服務器獲取到組件定義後被觸發。也可以使用 reject(reason) 來指出加載失敗了。這裏的 setTimeout 只是用來做簡單的演示,如何去獲取 component 完全取決於你。一個推薦的方法是和 Webpack 的 code-splitting 功能一塊兒使用異步組件:
Vue.component('async-webpack-example', function (resolve) {
// 這個特殊的 require 語法會讓 Webpack 去自動把你的編譯後代碼分割成 通過 Ajax 請求加載的 bundles
require(['./my-async-component'], resolve)
})
也可以在 resolve 方法中返回一個 Promise,比如通過 Webpack 2 + ES2015 語法可以這麼做:
Vue.component(
'async-webpack-example',
() => System.import('./my-async-component')
)
然後 Browserify 不支持異步組件,擁抱 Webpack 吧。
組件命名規範
在註冊的時候,使用是隨意的:
components: {
'kebab-cased-component': { /* ... */ },
'camelCasedComponent': { /* ... */ },
'TitleCasedComponent': { /* ... */ }
}
但是在 HTML 模板中,必須使用 kebab-case 的,也就是上面的第一種。但如果是字符串模板(string template)的話,則可以隨意使用。如果你的組件不使用 slot 進行屬性傳遞,甚至可以直接寫成自閉的(也僅支持字符串模板,因爲瀏覽器不支持自閉合的自定義元素)。
遞歸組件
組件可以在它自己的模板中遞歸自身。然而,他們只能通過 name 選項來這麼做:
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
像上面這樣的組件會陷入 “max stack size exceeded” 錯誤,所以需要讓遞歸變成條件性的(比如使用 v-if 指令,並最終返回一個 false)。當你在全局通過 Vue.component 註冊一個組件的時候,一個全局的 ID 會被自動設置爲組件的 name 選項。
內聯模板
當子組件中存在 inline-template 這個特殊屬性的時候,它會使用其內容作爲模板,而不會把它當做分發內容。如此模板就變得更靈活了。
<my-component inline-template>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</my-component>
但是 inline-template 讓模板的作用域難以理解,並且不能緩存模板編譯結果。最佳實踐是通過 template option 在組件內部定義模板,或者在 .vue 文件中的模板元素中定義。
X-Templates
另一個在 script 元素內部定義模板的方法是通過 type text/x-template,然後通過 id 引用模板。像這樣:
<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})
在極小的應用或者大型模板的 demo 的時候可能會有用,其他情況下應該儘量避免。因爲這樣會把它和其他模板定義給隔離開。
v-once 定義簡單的靜態組件
在 Vue 裏面渲染純淨的 HTML 元素是很快的,但有時候你可能需要一個包含了很多靜態內容的組件。這種情況下,你可以通過在根元素加上 v-once 指令確保它只被評估了一次然後就被緩存下來了,像是這樣:
Vue.component('terms-of-service', {
template: '\
<div v-once>\
<h1>Terms of Service</h1>\
... 很多靜態內容 ...\
</div>\
'
})
Vue 1.x TO 2.0
通過使用 vue-migration-helper 可以快速掃出需要替換的代碼。
主要有以下幾類:
- 官方依賴,比如 vue-resource、vue-router(這個升級完接口也改了不少, 遷移向導 )、vue-loader、vue-hot-reload-api、vue-invalidate(還在升級中)等。
- 第三方庫。
- UI組件庫,比如 vux(目前計劃是11月發佈適配 2.0 的版本)、餓了麼前端提供的那些(並沒有給出更新計劃)。
- 組件間通訊,不能再使用 dispatch,而需要使用全局 eventbus 或者 vuex。
- 各種 API 廢棄,見 issue 2873 ,像是 attached、activated 這些生命週期 API 都被幹掉了。
具體一點的話,像是 $index 現在必須使用 index 了(在 v-for 中顯示聲明),Filters 也不能像以前那樣到處用了,等等等等,要升級還是挺痛苦的。
尾聲
差不多也就是這樣了。如果你是一個有一定經驗並懂得基本 HTML 和 CSS 的高級工程師,我相信幾天你就能看完並上手它了,畢竟對比 React 那一整套東西,還是相對簡單的。