Vue 做爲 MVVM 框架一員,不論是寫業務仍是基礎服務,都少不了書寫組件。本文總結一下書寫業務組件的一些心得。javascript
咱們知道,只要是組件,就須要在引用的時候與 view 或者其餘組件進行相關的交互,即 props 傳值,$emit 觸發事件,
使用 $refs 調用組件方法等,與寫在同一個文件相比,耗費的精力明顯更多。那爲何須要拆分出組件呢?我認爲有兩種目的:
複用和隔離。html
在業務代碼中,會有大量相似的界面,保證交互惟一,即便咱們有了相似 element-ui 或者 iview 這種基礎組件庫,
咱們一樣須要爲這些基礎組件添加 props 或者 events,只有一處使用時,沒有任何問題,當你的業務中出現兩次、三次甚至更多時,
代碼中會出現大量重複的代碼,並且這些代碼在線上可能會慢慢露出一些深層次的 bug,要修復這些 BUG,就須要 n 倍的時間去寫一樣的代碼,
讓人抓狂。因此咱們頁面一樣須要像 js 抽出公用的方法同樣抽出公用組件,這就是複用的目的。vue
複用針對的是代碼重複問題,而隔離則是針對代碼邏輯過於複雜的問題。一般咱們要實現一個複雜的邏輯,它是一個扁平化的多邏輯並行問題,
人腦對於同時思考是有必定限制的,過於複雜就很難一下考慮全面。
首先須要抽象出它的目的,而後對實現進行分層,讓每一層只解決一個簡單的問題,這些層合起來造成一個完整的解決方案;
或者將問題拆分紅幾塊,每塊之間具備必定的聯繫,每次思考時只須要考慮局部的邏輯便可。
不論是分層、分塊仍是混合式,它的目的是對進行隔離,從而簡化問題。若是某個頁面 js + template 行數很是多(1k+),
這個時候就能夠考慮是否是要對部分功能拆解,便於在後續添加新功能,或者修改 BUG 的時候更爲方便定位到問題的代碼,
不會出現改錯函數的問題。java
須要注意的是,雖然複用和隔離是讓邏輯更爲清晰,但使用本身寫的組件會讓項目的入手難度提升,須要先了解總體的設計,
才能針對性的修改代碼或者添加新的功能,得失各半。node
網上有關於組件設計的基本原則:http://www.fly63.com/article/detial/996,
內容比較多,下面進行一些經常使用的原則概括。react
以前提到的組件拆分目的:複用與隔離,對於隔離的類型,組件業務必然很重,此時雖然要保證組件儘量簡單,
而複用類型的,通用性更強,因此功能越單一,使用起來就越方便。咱們知道 react 有一個概念:container/component,
即 component 只是渲染組件,而 container 纔是產生業務的組件,咱們 Vue 也能夠依照這個理念進行設計。
即把數據處理等帶有反作用的工做放在父組件中,而子組件只進行展現或操做,經過事件的方式讓父組件進行處理,
保證邏輯歸一,後續維護也更爲方便。或者使用 slot 等相似高階組件的方式來簡化當前組件的內容。git
和純函數相似,設計的一個組件不該該對父組件產生反作用,從而達到引用透明(引用屢次不影響結果)。
數據操做前必須進行復制。好比須要添加額外的鍵值,或者須要對數組類型的數據進行操做,會對原始數據產生影響,
須要使用解構的方式進行復制:github
const newData = { ...oldData } const newList = [...oldList]
注:引用類型的 props 千萬不要直接修改對象,雖然可以達到傳遞數據的目的,但會產生反作用,若是有其餘地方用到該數據,可能產生未知的影響。vue-cli
Vue 提供了類型檢查工具,只在 dev 狀況下生效,雖然和 JSON Schema 相比功能比較少,但可以作基本的類型檢查了,
咱們只須要在 props 時不使用字符串型,而是爲它定義詳細的類型, 併爲它設置默認值(vue-cli 的 eslint 嚴格模式已經強制要求):element-ui
['name1'] // 不規範寫法 { name1: { type: String, default: undefined } }
組件拆分出來以後,拆成幾層或者是拆成幾塊,影響文件的數量。若是層級比較多,各類 props 傳遞,事件傳遞,維護成本比較高。
舉例:若是是一個二級的列表,即有多個一級列表,一級列表各有一級列表,這個時候應該怎麼拆分呢?
按單一原則,咱們可能須要拆分紅如下幾個:一級列表卡片自己,二級列表卡片,二級列表承載組件,一級列表承載組件。
這種劃分,組件是三級,兩塊,數據的傳遞就會比較困難。若是一級卡片列表不復雜,咱們能夠將幾個 v-for 與組件自己合併,
即一級列表承載組件+一級列表卡片+二級列表卡片,二級列表卡片。這種處理方式保證全部的數據處理在第一層上,二級卡片只作渲染,
保證邏輯處理集中在一個組件,維護也比較方便。固然,若是一級卡片很是複雜,或者數據須要大量的處理,須要根據狀況把最細的進行合併。
對於通用類型組件,咱們要求它儘量的短小精悍,調用起來更爲簡單,因此不能設計太多的參數。基礎組件庫不能符合這個要求,
主要是由於基礎組件庫須要儘量增長普適性,不會由於沒有某個經常使用的屬性,致使該組件須要複製一份重寫,再加上日積月累的 pull request,
屬性和參數必然會愈來愈多。而咱們在業務中使用,徹底不須要這麼多的配置,若是有重大差異,從新複製一份,對於後續的維護反而更方便。
因此是否新增長屬性仍是拷貝一份,是根據後續該組件是否會產生比較大的發展方向差別來決定的。
Vue 組件與 React 組件有比較大的區別,模板的設計更偏向於 HTML,因此要實現相似 react 的高階組件的需求一般比較少,
而高階組件集成度太高,對於業務來講,當業務愈來愈複雜,組件內部邏輯將拆分困難,未必是件好事,因此咱們只討論普通的組件設計。
組件設計是考慮組件通信方式,主要分爲如下幾個方面:向下傳值,向上傳值,僞雙向綁定,方法調用。
向下傳值就是父級傳給子級數據。前面已經提到了,在 props 傳值儘可能對傳入數據進行類型校驗,保證儘快發現問題。除此以外,也有一些注意事項。
傳值類型若是是引用類型的 Object 類型,那麼儘可能給它默認值,防止 undefined。
default: () => ({})
其次,父級在賦值時,不要使用 a=newData
這種寫法,而是使用 Object.assign
來保證能準確觸發組件更新。
還有另一種方式,但不方便聲明全部對象內的數據時,可使用 this.$set(this, 'key', newData)
,保證對象必定會被監聽到。
Vue 2.0 須要使用 $emit
進行事件向上冒泡, 父組件進行事件的監聽就能夠進行處理。
Vue 2.0 提供了語法糖,支持雙向綁定,使得Vue 進行雙向傳遞數據極爲方便,不須要既向上傳值又向下傳值。
固然它不是真正的綁定,而是封裝了以前提到的向下傳值和向上傳值,簡單的語法糖。它分爲兩類:v-model 和 .sync 修飾符
數據傳遞支持各類類型,不過建議傳遞的數據使用數組而不要使用對象類型,對象類型可能會出現渲染監聽失敗的問題。
v-model 使用的是 value
屬性和 input
事件,父組件會自動把 input 事件的值賦給對應的變量。
在設計組件中,若是有雙向的數據傳遞,且符合組件設計目的,應該優先使用 v-model 來實現數據的控制,
這樣的組件更符合 Vue 組件的標準。
要注意的是,若是是自行寫 render 函數,雙向綁定要本身實現。
.sync
修飾符和 v-model 比較相似,不過它的 props 能夠是自定義的,而向上傳值時方式爲:
this.$emit('update:propsName', val)
本質上和 v-model 是相似的。sync 修飾符相比於 v-model,語義化更好,用起來更方便
有了 props 和 emit ,咱們已經基本可以實現大部分功能了,但總有些子組件的層次控制或者數據控制沒法經過這種方式實現,
這個時候,組件間的交互就須要使用子組件的 Methods 來定義,使用 this.$refs.組件ref
來調用它的方法。
好比說 el-tree 組件,設置選中和非選中,只靠數據傳遞,沒法保證設計選中狀態,因此它提供了一些方法來進行手動選擇。
在設計組件時,使用方法進行控制應該是最後才考慮的,由於咱們一般沒法一眼看出某個方法是否應該支持外部調用,
只能經過看文檔才能得知相關的方法
除組件外,Vue 提供了一些機制用於減小項目中的代碼重複率。
插件機制須要在 Vue 初始化的時候引入。看下 vue-meta 的插件入口寫法:
/** * Plugin install function. * @param {Function} Vue - the Vue constructor. */ export default function VueMeta (Vue, options = {}) { // set some default options const defaultOptions = { keyName: VUE_META_KEY_NAME, contentKeyName: VUE_META_CONTENT_KEY, metaTemplateKeyName: VUE_META_TEMPLATE_KEY_NAME, attribute: VUE_META_ATTRIBUTE, ssrAttribute: VUE_META_SERVER_RENDERED_ATTRIBUTE, tagIDKeyName: VUE_META_TAG_LIST_ID_KEY_NAME } // combine options options = assign(defaultOptions, options) // bind the $meta method to this component instance Vue.prototype.$meta = $meta(options) // store an id to keep track of DOM updates let batchID = null // watch for client side component updates Vue.mixin({ beforeCreate () { // Add a marker to know if it uses metaInfo // _vnode is used to know that it's attached to a real component // useful if we use some mixin to add some meta tags (like nuxt-i18n) if (typeof this.$options[options.keyName] !== 'undefined') { this._hasMetaInfo = true } // coerce function-style metaInfo to a computed prop so we can observe // it on creation if (typeof this.$options[options.keyName] === 'function') { if (typeof this.$options.computed === 'undefined') { this.$options.computed = {} } this.$options.computed.$metaInfo = this.$options[options.keyName] } }, created () { // if computed $metaInfo exists, watch it for updates & trigger a refresh // when it changes (i.e. automatically handle async actions that affect metaInfo) // credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux) if (!this.$isServer && this.$metaInfo) { this.$watch('$metaInfo', () => { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) }) } }, activated () { if (this._hasMetaInfo) { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, deactivated () { if (this._hasMetaInfo) { // batch potential DOM updates to prevent extraneous re-rendering batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, beforeMount () { // batch potential DOM updates to prevent extraneous re-rendering if (this._hasMetaInfo) { batchID = batchUpdate(batchID, () => this.$meta().refresh()) } }, destroyed () { // do not trigger refresh on the server side if (this.$isServer) return // re-render meta data when returning from a child component to parent if (this._hasMetaInfo) { // Wait that element is hidden before refreshing meta tags (to support animations) const interval = setInterval(() => { if (this.$el && this.$el.offsetParent !== null) return clearInterval(interval) if (!this.$parent) return batchID = batchUpdate(batchID, () => this.$meta().refresh()) }, 50) } } }) }
要的本質是使用 prototype 設置獨立變量,而後使用 mixins 注入相關的方法。能夠看到,基本上每一個生命週期都會處理到。
mixin 不只使用在插件中,直接使用也是能夠的。關於 mixins 可看官方文檔:https://cn.vuejs.org/v2/guide/mixins.html.
以前提到組件儘量參數少,但參數過少,組件沒法實現某些定製化的要求,而咱們組件可能有多個層次,
這種狀況下咱們須要將當前組件的父組件的其餘屬性透傳給子組件,將父組件其餘事件監聽給子組件,寫法以下:
<div name="main"> <input v-on='$listeners' v-bind="$attrs" /> </div>
正常狀況下是不推薦業務組件直接操做 DOM 的,但有時候要寫組件監聽事件,這種狀況下必定要注意在 destroyed 時候進行 removeEventListener。