在讀這篇文章以前,我想先安利你們一個東西: html
看到這副黑框眼鏡,你是否是想到了什麼?前端
對,就是它:Vue.js 組件編碼規範。讀過的同窗忽略,沒讀過的同窗有時間的話請花 20 分鐘認真看看,文章的內容都是在承認這篇規範的基礎上展開的。vue
另外,本文中的「最佳實踐」(注意引號),全都是一家之言,不必定對,歡迎各路大佬討論拍磚。ios
組件(component)是 vue 最核心的概念之一,可是正由於這一律念太過寬泛,咱們會在實際開發中看到各類各樣的組件,對開發和維護的同窗帶來了很大的困惑和混亂。這裏我把組件分紅四類:git
顧名思義,view 指的是頁面,你也能夠把它叫作 page。它的定義是:和具體的某一條路由對應,在 vue-router 配置中指定。view 是頁面的容器,是其餘組件的入口。它能夠和 vuex store 通訊,再把數據分發給普通組件。github
全局組件,做爲小工具而存在。例如 toast、alert 等。他的特色是具有全局性,直接嵌套在 root 下,而不從屬於哪一個 view。global component 也和 vuex store 通訊,它單獨地使用 state 中的一個 module,這個 state 中的數據專門用來控制 gloabl component 的顯隱和展現,不和其餘業務實體用到的 state 混淆。
其餘組件想修改它,能夠直接派發相應的 mutation。而要監聽它的變化(好比一個全局的confirm,確認以後在不一樣的組件中觸發不一樣的操做),則使用全局事件總線(event bus)。vue-router
簡單組件。這種組件對應的是 vue 中最傳統的組件概念。它的交互和數據都很少,基本上就是起到一個簡單展現,拆分父組件的做用。這種組件和父組件之間經過最傳統的方式進行通信:父組件將 props 傳入它,而它經過 $emit 觸發事件到父組件。
簡單組件內部是不寫什麼業務邏輯的,它能夠說是生活不能自理,要展現什麼就等着父組件傳入,要幹什麼就 $emit 事件出去讓父組件幹,父組件夠操心的。vuex
複雜組件。這種組件的特色是,內部包含有不少交互邏輯,經常須要訪問接口。另外,展現的數據也每每比較多。以下圖。vue-cli
圖中紅框內部的就是一個複雜組件的實例。它是一個大列表的列表項,展現的數據不少,並且點擊左下角的幾個 button,還會彈出相應的彈窗,彈窗內有複雜的表單須要填寫提交···邏輯能夠說是至關複雜了。若是這時咱們還拘泥於簡單組件的那種通訊方式,衣來伸手飯來張口,啥事兒不幹,那麼:<template>
內的代碼可不得上天了?
<script>
內的代碼可不得上天了?
我畫了一張圖來講明上面這四種 component 的關係,但願能幫助你們更好理解。json
在區分了這四種 component 後,咱們在編碼時就能作到內心有數,如今在寫的組件,到底屬於哪一類?每一類以特定的方式編寫和交互,邏輯上就會清晰不少。 使用 vue-cli 構建的項目中都會有一個目錄叫作 component,之前是一股腦往裏塞,如今能夠在此基礎上再設置幾個子目錄,放置不一樣類型的組件。
先來看一個栗子🌰
假設有一個模態對話框的組件。父組件爲了可以打開模態框,給模態框傳入了一個控制其顯隱的 props,命名爲 visible,type 爲 Boolean,綁定模態框外層的 v-if 指令。那麼,問題來了,若是咱們點擊了模態框內部的關閉按鈕,關閉自身,應該怎麼寫?
固然,最傳統的方式天然仍是模態框拋出事件,父組件中設置監聽,而後修改值。但這種方式無疑有很強的侵入性,無故增長了不少的代碼量。關閉按鈕在模態框內部,關閉本身是我本身的事兒,能不能不讓父組件管這些?
有同窗說了,直接在模態框內部修改 visible 啊。this.visible = false
,不行嗎?
還真不行。若是這麼幹,你會看到如下一堆報錯:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
vue 很明確地告訴你了,做爲子組件,你要安分守己,不準隨便修改老爹傳給你的 props。
那麼咱們應該怎麼辦?
咱們思考一下,若是不容許修改 props 的值,那咱們修改 porps 的······屬性如何?
事實證實,是能夠的。
咱們能夠把上面 visible 的 type 設爲 Object,模態框的顯隱決定於 visible.value。當模態框想要關閉自身時,只需 this.visible.value = false
便可。 這種方式看起來至關方便,但實際是一種投機取巧的方法。上面安利的 Vue.js 組件編碼規範中明確有一條規範,就是 props 原子化,也就是說,props 裏的字段必須是簡單的 String,Number 或 Boolean。這麼作的緣由是:
因此,咱們把 visible 改成 Object,原本就是違反規範的。
vue 中有種已經存在的機制,和現有需求很像,這就是 v-model。在表單中,每個 input,就像一個子組件。在外層經過 v-model 綁定的值能夠在 input 中回顯,而 input 自己的值也能改變。
事實上,v-model 僅僅是一個語法糖,v-model="xxx"
,就至關於 :value="xxx" @input="val=>xxx=val"
。那麼,咱們就能夠利用 v-model 的這種特性來實現咱們的需求。咱們只須要在模態框內部拋出一個 input 事件 this.$emit('input', false)
,就能關閉自身了。
這種方式比較簡潔,也不違反規範,可是容易讓人困惑,覺得這裏是要進行什麼表單操做。
咱們還有沒有什麼更好的方式呢?
若是你是從大版本爲 1 時就開始接觸 vue,那你能夠知道一個修飾符,叫作.sync。若是你是從 2.0 開始接觸的,則極可能不熟悉它。這是由於,vue 在 2.0 版本時把它刪除了,不過好在, 2.3 版本以後,它又回來了。
這個修飾符簡直就是爲咱們這個需求量身定製的。它自己是一個和 v-model 相似的語法糖,咱們要作的,僅僅是在組件內部須要改動值的地方,拋出一個 update 事件。this.$emit('update:foo', newValue)
。既不違反規範,也足夠清晰,能夠說是最佳的解決方案了。惟一的不足之處,就是對版本有一點要求。
數據是 SPA 的核心,而數據的來源都是接口。如何優雅、高效地經過接口請求數據,是開發者必需要關心的問題。在實踐中,我是這樣封裝接口的:
從高層到底層,依次說明。success: false
。對於這兩種錯誤,咱們都要捕獲並處理。下面是示例代碼,可供參考。
if (opt.method === 'post') {
axiosOpt.data = opt.payload
} else if (opt.method === 'get') {
axiosOpt.params = opt.payload
}
if (opt.withFile) {
Object.assign(axiosOpt, { headers: {
'Content-Type': 'multipart/form-data'
}})
}
// 全局請求的 loading,當請求 300 ms 後還沒返回,纔會出現 loading
const timer = setTimeout(() => {
store.dispatch('showLoading', {
text: '加載數據中'
})
}, 300)
try {
// 開始請求
const result = await axios(axiosOpt)
// 若是 300 ms 還沒到,就取消定時器
clearTimeout(timer)
store.dispatch('closeLoading')
if (result.status === 200 && result.statusText === 'OK') {
if (result.data.success) {
return result.data.results || true
} else {
// 請求失敗的 toast
store.dispatch('showAlert', {
type: 'error',
text: `請求失敗${result.data.message ? `,信息:${result.data.message}`: ''}`
})
return false
}
} else {
return false
}
} catch(e) {
clearInterval(timer)
// 請求失敗的 toast
store.dispatch('closeLoading')
store.dispatch('showAlert', {
type: 'error',
text: '請求失敗'
})
return false
}
複製代碼
SPA中,每個 view 中的都有不少數據是須要經過接口請求得到的,若是沒有得到,頁面中就會有不少空白。上面,咱們討論瞭如何封裝好接口請求,下一步就是決定何時請求初始化數據,即,代碼在哪裏寫的問題。實踐下來,有兩個時機是比較理想的。
vue-router 提供了以上兩個生命週期鉤子,分別會在進入路由和路由改變時觸發。這兩個鉤子是寫的 view 中的。
vue-router還提供了一個全局性的 beforeEach 方法,任何一個路由改變時,都會被這個方法攔截,咱們能夠在這個方法中加入咱們本身的代碼,作統一處理。好比,對於全部 view 初始化請求的 action,咱們能夠以特定的名稱命名,如以 _init 做爲後綴等。在 beforeEach 方法內,咱們對當前 view 對應的 store 進行監聽,查找到其中以 _init 命名的 action 並派發。
以上兩種方式各有特色。
對於前者,優勢是數據獲取的代碼和具體的 view 是綁定在一塊兒的,咱們能夠在 view 內部就清晰地看到數據獲取的流程。缺點是,每增長一個頁面,都要在其內部寫一堆初始化代碼,增長了代碼量。 對於後者。優勢是,代碼統一且規整,使用了配置的方式,寫一次便可,不須要每次增長額外的代碼。缺點是比較隱晦,且初始化代碼和 view 自己割裂了。
對於以上兩種方式如何取捨的問題,我傾向於,大型項目用後者,小型項目用前者。
做者:丁香園前端團隊-㍿社長