Vue 組件設計

Vue 組件設計

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 組件之間的交互設計

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

v-model 使用的是 value 屬性和 input 事件,父組件會自動把 input 事件的值賦給對應的變量。
在設計組件中,若是有雙向的數據傳遞,且符合組件設計目的,應該優先使用 v-model 來實現數據的控制,
這樣的組件更符合 Vue 組件的標準。

要注意的是,若是是自行寫 render 函數,雙向綁定要本身實現。

sync

.sync 修飾符和 v-model 比較相似,不過它的 props 能夠是自定義的,而向上傳值時方式爲:

this.$emit('update:propsName', val)

本質上和 v-model 是相似的。sync 修飾符相比於 v-model,語義化更好,用起來更方便

方法調用

有了 props 和 emit ,咱們已經基本可以實現大部分功能了,但總有些子組件的層次控制或者數據控制沒法經過這種方式實現,
這個時候,組件間的交互就須要使用子組件的 Methods 來定義,使用 this.$refs.組件ref 來調用它的方法。
好比說 el-tree 組件,設置選中和非選中,只靠數據傳遞,沒法保證設計選中狀態,因此它提供了一些方法來進行手動選擇。
在設計組件時,使用方法進行控制應該是最後才考慮的,由於咱們一般沒法一眼看出某個方法是否應該支持外部調用,
只能經過看文檔才能得知相關的方法

簡化與抽離的其餘實現

除組件外,Vue 提供了一些機制用於減小項目中的代碼重複率。

使用插件或者 mixins 實現

插件機制須要在 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 操做

正常狀況下是不推薦業務組件直接操做 DOM 的,但有時候要寫組件監聽事件,這種狀況下必定要注意在 destroyed 時候進行 removeEventListener。

相關文章
相關標籤/搜索