我們通常會(huì)說一個(gè)人的英文夠不夠地道,夠不夠 native,同樣在使用 vue 的時(shí)候希望大家也能夠更地道的書寫 vue 代碼。
這可能需要我們拋棄一些思想,比如我認(rèn)為 jQuery 中頁面/組件的狀態(tài)變更是事件驅(qū)動(dòng)的,而 Vue/React 中則更多的是數(shù)據(jù)驅(qū)動(dòng),即 data/prop 的變化引起頁面展示的變化。
所以,請(qǐng)不要在 Vue 中帶入其他框架的思想,一個(gè)典型的特例就是 濫用 watch 監(jiān)聽數(shù)據(jù)變化來生成新的數(shù)據(jù)。
computed 衍生數(shù)據(jù)
vue 官網(wǎng)的介紹中 computed 數(shù)據(jù)直譯為“計(jì)算屬性”,但從功能應(yīng)用上個(gè)人覺得叫做“衍生數(shù)據(jù)”更為貼切。
computed 中定義的數(shù)據(jù)大體有兩種用法:
-
衍生,即通過
props、data與其他數(shù)據(jù)(如import導(dǎo)入的外部數(shù)據(jù))的組合、運(yùn)算生成新的數(shù)據(jù)對(duì)象 -
代理,即通過定義的
set、get方法實(shí)現(xiàn)通過衍生取數(shù)據(jù),修改該數(shù)據(jù)時(shí),映射/更新到他的衍生
衍生示例
{
data () {
return {
menuList: [], // 賬戶下的菜單列表,通過請(qǐng)求接口獲得
}
},
computed: {
hasPagePermission () { // 是否具備當(dāng)前頁面的權(quán)限
const currentPath = this.$route.path
return this.menuList.includes(currentPath)
}
}
}
例子中 hasPagePermission 數(shù)據(jù)即為衍生數(shù)據(jù),路由變化時(shí) vue 自動(dòng)根據(jù)當(dāng)前路由和用戶的菜單列表 menuList 計(jì)算出用戶是否有該路由的權(quán)限。
代理示例
{
data () {
return {
firstName: '',
familyName: ''
}
},
computed: {
fullName: {
get () {
return `${this.firstName} ${this.familyName}`
},
set (val) {
const [ firstName, familyName ] = val.split(' ')
this.firstName = firstName
this.familyName = familyName
}
}
}
}
例子來源于官網(wǎng),這個(gè)不需要過多解釋了。
watch 數(shù)據(jù)監(jiān)聽
注冊(cè)數(shù)據(jù)變化的 callback,參數(shù)依次時(shí)新數(shù)據(jù)、舊數(shù)據(jù)。
在對(duì)對(duì)象型數(shù)據(jù)監(jiān)聽時(shí)注意設(shè)置 deep 屬性為 true 達(dá)到對(duì)象深層屬性/值變化時(shí)能夠觸發(fā)回調(diào)。
大多數(shù)你覺得需要用到 watch 的場景其實(shí)更適合用 computed,比如 代理示例 中,多數(shù)人會(huì)選擇監(jiān)聽 $route 來更新 data 定義的 hasPagePermission,當(dāng)然功能是可以實(shí)現(xiàn)的,但是不是那個(gè)味兒。
建議在 數(shù)據(jù)驅(qū)動(dòng)組件狀態(tài)變化 時(shí)使用 watch。如 checkbox-group,當(dāng)勾選了 Thanos 時(shí),需要禁用掉 Doctor Strange、Scarlet Witch 等選項(xiàng)時(shí),可以使用 watch 監(jiān)聽 checkbox-group 綁定的 v-model,在其 handler 中更新選項(xiàng)屬性。這個(gè)思想可以理解為數(shù)據(jù)驅(qū)動(dòng)型,當(dāng)然如果你在 checkbox-group 的 change 事件回調(diào)中修改選項(xiàng)屬性也是可以的(事件驅(qū)動(dòng)型),但個(gè)人更偏向于在 vue 中盡量使用數(shù)據(jù)驅(qū)動(dòng)型的處理模式。
v-model 實(shí)現(xiàn)自定義 input 組件
v-model 其實(shí)是塊語法糖:
- 通過
props.value接收父組件的數(shù)據(jù) - 通過
this.$emit('input', DATA)拋出數(shù)據(jù)給父組件
基于以上,我們可以實(shí)現(xiàn)自定義的 input 組件
自定義 checkbox-group 組件
{
template: `
<div class="checkbox-group">
<label v-for="ele in choices" @click="handleCheck"><input type="checkbox" :checked="value.includes(ele)" :value="ele">{{ele}}</label>
</div>
`,
props: {
value: Array,
choices: Array
},
methods: {
handleCheck (e) {
const currentOptionVal = e.target.value
let result = []
if (e.target.checked) {
result = [ ...this.value, currentOptionVal ]
} else {
result = this.value.filter(ele => ele !== currentOptionVal)
}
this.$emit('input', result)
}
}
}
demo 及 源碼 點(diǎn)擊 這里
mixin 混入
混入,通過它可以抽離/封裝公共邏輯,在需要時(shí)混入組件中就可以直接用其中的方法、數(shù)據(jù)了。
可以理解為它是一個(gè)沒有 template 的抽象組件。
混入 mixin 后,組件中定義的數(shù)據(jù)/方法會(huì)覆蓋掉 mixin 中定義的同名數(shù)據(jù)/方法。
其邏輯有點(diǎn)像面向?qū)ο笳Z言中的繼承。
合理使用 mixin 可以充分發(fā)揚(yáng)程序員懶的美德。
使用場景:多組件邏輯重復(fù)。
比如,報(bào)表業(yè)務(wù)開發(fā)中多個(gè)頁面都是篩選表單 + 展示表格 + 分頁,附加上loading、導(dǎo)出等 feature,那么這部分功能就可以通過 mixin 進(jìn)行封裝抽離。
示例
const TableDataMixin = {
/*
* 表格數(shù)據(jù)展示/導(dǎo)出邏輯 mixin
*
* 使用時(shí)需要在 data/computed 中配置以下數(shù)據(jù)
* 1. 數(shù)據(jù)請(qǐng)求 api/參數(shù):loadDataApi / loadDataParam
* 2. 數(shù)據(jù)導(dǎo)出 api/參數(shù):exportDataApi / exportDataParam
*/
data () {
return {
tableData: [], // 表格數(shù)據(jù)
tableLoading: false, // loading 狀態(tài)
pagination: { // 分頁
currentPage: 1,
pageSize: 10,
total: 0
},
tableExporting: false // exporting 狀態(tài)
}
},
computed: {
shouldDisableExport () { // 是否需要禁用 export
return this.tableLoading || this.tableExporting
}
},
methods: {
loadTableData () {
this.tableLoading = true
api.get(this.loadDataApi, this.loadDataParam)
.then(({ data: { status, message, data: { data, total } } }) => { // 解析 api 返回?cái)?shù)據(jù),項(xiàng)目中應(yīng)該是統(tǒng)一格式的,如 { status, message, data }
this.tableData = data
this.tablePagination.total = total
this.tableLoading = false
})
.catch(err => {
console.error('load table data failed:', err)
this.tableLoading = false
})
},
exportTableData () {
this.tableExporting = true
api.get(this.exportDataApi, this.exportDataParam)
.then(({ data: { status, message, data: fileURI } }) => { // 解析 api 返回?cái)?shù)據(jù),項(xiàng)目中應(yīng)該是統(tǒng)一格式的,如 { status, message, data }
window.location.href = fileURI
this.tableExporting = false
})
.catch(err => {
console.error('export table data failed:', err)
this.tableExporting = false
})
},
handlePaginationChange ({ currentPage, pageSize }) {
this.tbalePagination.currentPage = currentPage
this.tbalePagination.pageSize = pageSize
}
}
}
有了以上 mixin 后就不需要在每個(gè)頁面中寫一遍獲取/導(dǎo)出數(shù)據(jù)的邏輯了,只需混入后配置相關(guān)數(shù)據(jù)即可:
const TablePage = Vue.component('my-page', {
mixins: [ TableDataMixin ],
data () {
return {
formData: {},
loadDataApi: 'YOUR LOAD DATA API',
exportDataApi: 'YOUR EXPORT DATA API'
}
},
computed: {
loadDataParam () {
const { currentPage, pageSize } = this.tablePagination
return Object.assign({ currentPage, pageSize }, this.formData, { /* some other params */ })
},
exportDataParam () {
return Object.assign({}, this.formData, { /* some other params */ })
}
}
})
而對(duì)于頁面中導(dǎo)出按鈕點(diǎn)擊回調(diào)、分頁變化回調(diào)、篩選表單提交后進(jìn)行的數(shù)據(jù)檢索等都可以直接使用 TableDataMixin 中的 exportTableData | handlePaginationChange | loadTableData 函數(shù),而表格數(shù)據(jù)讀取、分頁數(shù)據(jù)讀寫等也可以直接綁定 tableData | tablePagination 等。
directive 自定義指令
指令也是代碼封裝/復(fù)用的大殺器。
詳細(xì)介紹可以自行參考 vue 官方文檔 自定義指令。
使用場景 不限于一些需要底層 dom 操作的情況,如任何 ui 框架中的 v-loading 等,或者官方文檔中提到的 v-focus。
這里以一個(gè)頁標(biāo)題為例,在 SPA 開發(fā)中,每個(gè)頁面一般都具備一個(gè)單獨(dú)的 document.title,在每個(gè)頁面的掛載/更新鉤子中設(shè)置 document.title 顯然太繁瑣,那么我們可以通過自定義指令解決:
自定義頁標(biāo)題指令
const Title = {
inserted: function (el, binding, vnode, oldVnode) {
const { value: title = 'DEFALT TITLE' } = binding
document.title = title
},
update: function (el, binding, vnode, oldVnode) {
const { value: title = 'DEFALT TITLE' } = binding
document.title = title
}
}
const Page = Vue.component('my-page', {
directives: [ Title ],
template: `
<div class="my-page" v-title="My Page">
<!-- page content -->
</div>
`
})
以上來源于個(gè)人開發(fā)中的一些反思總結(jié),如有不同意見可以在評(píng)論中回復(fù)。
后續(xù)開發(fā)中如果發(fā)現(xiàn)其他的 little tricks 會(huì)進(jìn)一步更新。