聊聊元件設計

羅輯思維前端開發團隊發表於2019-08-16

前言

元件化思想並不是前端獨有的,但卻是前端技術的延伸 任何軟體開發過程,或多或少都有那麼一些元件化的需求

隨著三大框架崛起,前端元件化逐漸成為前端開發的迫切需求,一種主流,一種共識,它不僅提高開發效率,同時也降低了維元件內聚原則護成本 開發者們不需要再面對一堆晦澀難懂的程式碼,轉而只需要關注以元件⽅式存在的程式碼⽚段 這是一場新的挑戰!

文章開始之前,明確本文的邊界

  • 從前端工程談到元件化開發
  • 元件的設計原則
  • 元件的職能劃分及利弊
  • 元件設計的邊界
  • 落實到具體業務中如何做
  • 一些感悟
  • 總結

一個面試題引發的思考

面試官通常會問 寫過前端通用元件嗎?
複製程式碼

你可能會自信的表示: sure!

emm..是的嗎?

從前端工程談到元件化開發

前端工程經歷的三個階段

1. 庫/框架選型

image
確定技術選型,為專案節省許多工程量 後來三大框架的橫空出世,解放了不少生產力

2. 簡單構建優化

image

解決完開發效率,還需要兼顧執行效能, 故而選擇某種構建工具,對程式碼進行壓縮,校驗,之後再以頁面為單位進行簡單的資源合併

3. JS/CSS模組化開發

image

解決了基本開發效率和執行效率之後,開始考慮維護效率了

分而治之(以分解降低複雜度)是軟體工程中的重要思想,是複雜系統開發和維護的基石,模組化就是前端的分治手段

因此,模組化強調的是拆分,最大的價值就是分治,意味著不管你將來是否要複用這塊兒程式碼,都有將他們拆成一個模組的理由

將一個大問題,不斷的拆解為各個小問題進行分析研究,然後再組合到一起(分而治之原則)

模組化的方案

  • JS模組化
無模組化->函式寫法->物件寫法->自執行函式->CommonJS/AMD/CMD->ES6 Module
複製程式碼
  • CSS模組化
css模組化是在less,sass等前處理器的支援下實現的
複製程式碼

做到這些就夠了嗎?

當然是不夠的

模組化強調的是拆分,無論是從業務角度還是從架構、技術角度,模組化首先意味著將程式碼、資料等內容按照其職責不同分離

單純的橫向拆分業務功能模組有一些問題

聊聊元件設計

  • 程式導向的程式碼 隨著業務的發展不利於維護
隨著業務發展,”過程線“也會越來越長,其他專案成員根據各自需要,在”過程線“ 加插各自邏輯,最終這個頁面的邏輯變得難以維護
我們需要擺脫【一瀉而下】式的程式碼編寫
複製程式碼
  • 僅僅有JS/CSS模組化是不夠的,UI(頁面)的分治也比較迫切
除了JS和CSS,介面也需要拆分,如何讓模組化思想融入HTML語言

複製程式碼

4. 元件化開發(本文重點)

元件化開發的演變

在大肆宣揚元件化開發概念之前,也經歷了尋求元件化最佳實踐的階段

頁面結構模組化

image
簡單來說就是把頁面想象成樂高機器人,需要不同零件組裝,然後將各個部分拼到一起

落實到實際開發中像這樣

聊聊元件設計

我們可以獲取資訊

  • 頁面pageModel包含了 tabContainerlistContainerimgsContainer 三個模組
  • 我們根據不同的業務邏輯封裝了不同型別的model
  • 每個model有自己的資料,模板,邏輯,已經算是一個完整的功能單元

咦?嗅到一絲元件化的味道

N年前微軟的元件化的解決方案 HTML Component

歷史總有遺?

早在N年前微軟提出過一套解決方案,名為HTML Component

jsworke

是一個比較完整的元件化方案了,但卻沒能夠進入標準,默默地消失了,今天的角度來看,它可以說是生不逢時

WebComponents 標準

當時”所謂的元件“

  • 此時的元件基本上只能達到某個功能單元上的集合,資源都是資源都是鬆散地分散在三種資原始檔中
  • 而且元件作用域暴露在全域性作用域下,缺乏內聚性很容易就會跟其他元件產生衝突(如最簡單的 css 命名衝突)

於是 W3C 按耐不住了,制定一個 WebComponents 標準,為元件化的未來指引了明路

大致四部分功能

  • <template> 定義元件的 HTML模板能力
  • Shadow Dom 封裝元件的內部結構,並且保持其獨立性
  • Custom Element 對外提供元件的標籤,實現自定義標籤
  • import 解決元件結合和依賴載入

我們思考一下,可行的實踐化方案需要具備哪些能力

  • 資源高內聚(元件資源內部高內聚,元件資源由自身載入控制)
  • 作用域獨立(內部結構密封,不與全域性或其他元件產生影響)
  • 自定義標籤(定義元件的使用方式)
  • 可相互組合(元件間組裝整合)
  • 介面規範化(元件介面有統一規範,或者是生命週期的管理)

三大框架出現

今天的前端生態裡面 React,Angular和Vue三分天下,即使它們定位不同,但核心的共同點就是提供了元件化的能力,算是目前是比較好的元件化實踐

1. Vue.js採用了JSON的方法描述一個元件
import PageContainer from './layout/PageContainer'
import PageFilter from './layout/PageFilter'

export default {
  install(Vue) {
    Vue.component('PageContainer', PageContainer)
    Vue.component('PageFilter', PageFilter)
  }
}

複製程式碼

還提供了SFC(Single File Component,單檔案元件)‘.vue’檔案格式

<template>
//...
</template>

<script>
  export default {
    data(){}
  }
</script>

<style lang="scss">
//...
</style>
複製程式碼
2. React.js發明了JSX,把CSS和HTML都塞進JS檔案裡
class Tabs extends React.Component {
    render() {
        if (!this.props.items) {
            console.error('Tabs中需要傳入資料');
            return null;
        }
        const propId = this.props.id;
        return (
            <ul className={this.props.className}>
              <li>測試</li>
            </ul>
        );
    }
}
複製程式碼
Angular.js選擇在原本的HTML上擴充套件
<input type="text" ng-model="firstname">

var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) {
    $scope.firstname = "John";
});
複製程式碼

標準下的資源整合

image

具有以下特點

  • 每個元件對應一個目錄,元件所需的各種資源都在這個目錄下就近維護;(最具軟體工程價值)
  • 頁面上的每個獨立的可視/可互動區域視為一個元件;
  • 由於元件具有獨立性,可以自由組合;
  • 頁面是元件的容器,負責組合元件形成功能完整的介面;
  • 當不需要某個元件,或者想要替換元件時,可以整個目錄刪除/替換

應用結構圖

image

  • 分子是由原子組成的,分子分成原子,原子也可以重新組合成新的分子
  • 一個介面是由獨立的分子元件搭建而成,分子元件由原子元件構成,這些原子可通過不同的組合方式,組成新分子元件,繼而重組構成新的介面

模組化與元件化對比

從整體概念來講

  • 模組化是一種分治的思想,訴求是解耦,一般指的是js模組,比如用來格式化時間的模組
  • 元件化是模組化思想的實現手段,訴求是複用,包含了templatestylescript,script又可以由各種模組組成

從複用的角度來講

  • 模組一般是專案範圍內按照專案業務內容來劃分的,比如一個專案劃分為子系統、模組、子模組,程式碼分開就是模組
  • 元件是按照一些小功能的通用性和可複用性抽象出來的,可以跨專案,是可複用的模組

從歷史發展角度來講

隨著前端開發越來越複雜、對效率要求越來高,由專案級模組化開發,進一步提升到通用功能元件化開發,模組化是元件化的前提,元件化是模組化的演進

元件的設計原則

元件化方案下,我們需要具有元件化設計思維,它是一種【整理術】幫助我們高效開發整合

  1. 標準性
任何一個元件都應該遵守一套標準,可以使得不同區域的開發人員據此標準開發出一套標準統一的元件
複製程式碼
  1. 獨立性
描述了元件的細粒度,遵循單一職責原則,保持元件的純粹性
屬性配置等API對外開放,元件內部狀態對外封閉,儘可能的少與業務耦合
複製程式碼
  1. 複用與易用
UI差異,消化在元件內部(注意並不是寫一堆if/else)
輸入輸出友好,易用
複製程式碼
  1. 追求短小精悍

  2. 適用SPOT法則

Single Point Of Truth,就是儘量不要重複程式碼,出自《The Art of Unix Programming》
複製程式碼
  1. 避免暴露元件內部實現
  2. 避免直接操作DOM,避免使用ref
使用父元件的 state 控制子元件的狀態而不是直接通過 ref 操作子元件

複製程式碼
  1. 入口處檢查引數的有效性,出口處檢查返回的正確性
  2. 無環依賴原則(ADP)

設計不當導致環形依賴示意圖

image

影響

元件間耦合度高,整合測試難 一處修改,處處影響,交付週期長 因為元件之間存在迴圈依賴,變成了“先有雞還是先有蛋”的問題

那倘若我們真的遇到了這種問題,就要考慮如何處理

消除環形依賴

我們的追求是沿著逆向的依賴關係即可尋找到所有受影響的元件

建立一個共同依賴的新元件

image

  1. 穩定抽象原則(SAP)
- 元件的抽象程度與其穩定程度成正比,
- 一個穩定的元件應該是抽象的(邏輯無關的)
- 一個不穩定的元件應該是具體的(邏輯相關的)
- 為降低元件之間的耦合度,我們要針對抽象元件程式設計,而不是針對業務實現程式設計
複製程式碼
  1. 避免冗餘狀態
如果一個資料可以由另一個 state 變換得到,那麼這個資料就不是一個 state,只需要寫一個變換的處理函式,在 Vue 中可以使用計算屬性

如果一個資料是固定的,不會變化的常量,那麼這個資料就如同 HTML 固定的站點標題一樣,寫死或作為全域性配置屬性等,不屬於 state

如果兄弟元件擁有相同的 state,那麼這個state 應該放到更高的層級,使用 props 傳遞到兩個元件中

複製程式碼
  1. 合理的依賴關係
父元件不依賴子元件,刪除某個子元件不會造成功能異常

複製程式碼
  1. 扁平化引數
除了資料,避免複雜的物件,儘量只接收原始型別的值
複製程式碼
  1. 良好的介面設計
把元件內部可以完成的工作做到極致,雖然提倡擁抱變化,但介面不是越多越好

如果常量變為 props 能應對更多的場景,那麼就可以作為 props,原有的常量可作為預設值。

如果需要為了某一呼叫者編寫大量特定需求的程式碼,那麼可以考慮通過擴充套件等方式構建一個新的元件。

保證元件的屬性和事件足夠的給大多數的元件使用。

複製程式碼
  1. API儘量和已知概念保持一致

元件的職能劃分

那有了元件設計的“API”,就一定能開發出高質量的元件嗎?

元件最大的不穩定性來自於展現層,一個元件只做一件事,基於功能做好職責劃分

根據經驗,我將元件應分為以下幾類

  • 基礎元件(通常在元件庫裡就解決了)
  • 容器型元件(Container)
  • 展示型元件(stateless)
  • 業務元件
  • 通用元件
    • UI元件
    • 邏輯元件
  • 高階元件(HOC)

基礎元件

為了讓開發者更關注業務邏輯,湧現出了很多優秀的UI元件庫 比如antdelement-ui,我們只需要呼叫API便能滿足大部分的業務場景,前端角色後置了,開發變得更簡單了

容器型元件

一個容器性質的元件,一般當作一個業務子模組的入口,比如一個路由指向的元件

image

特點

  • 容器元件內的子元件通常具有業務或資料依賴關係
  • 集中/統一的狀態管理,向其他展示型/容器型元件提供資料(充當資料來源)和行為邏輯處理(接收回撥)
  • 如果使用了全域性狀態管理,那麼容器內部的業務元件可以自行呼叫全域性狀態處理業務
  • 業務模組內子元件的通訊等統籌處理,充當子級元件通訊的狀態中轉站
  • 模版基本都是子級元件的集合,很少包含DOM標籤
  • 輔助程式碼分離

表現形式?(vue)

<template>
<div class="purchase-box">
  <!-- 麵包屑導航 -->
  <bread-crumbs />
  <div class="scroll-content">
    <!-- 搜尋區域 -->
    <Search v-show="toggleFilter" :form="form"/>
    <!--展開收起區域-->
    <Toggle :toggleFilter="toggleFilter"/>
    <!-- 列表區域-->
    <List :data="listData"/>
  </div>
</template>
複製程式碼

展示型(stateless)元件

主要表現為元件是怎樣渲染的,就像一個簡單的模版渲染過程

image

特點

  • 只通過props接受資料和回撥函式,不充當資料來源
  • 可能包含展示和容器元件 並且一般會有Dom標籤和css樣式
  • 通常用props.children(react) 或者slot(vue)來包含其他元件
  • 對第三方沒有依賴(對於一個應用級的元件來說可以有)
  • 可以有狀態,在其生命週期內可以操縱並改變其內部狀態,職責單一,將不屬於自己的行為通過回撥傳遞出去,讓父級去處理(搜尋元件的搜尋事件/表單的新增事件)

表現形式?(vue)

 <template>
 <div class="purchase-box">
    <el-table
      :data="data"
      :class="{'is-empty': !data ||  data.length ==0 }"
      >
      <el-table-column
        v-for = "(item, index) in listItemConfig"
        :key="item + index" 
        :prop="item.prop" 
        :label="item.label" 
        :width="item.width ? item.width : ''"
        :min-width="item.minWidth ? item.minWidth : ''"
        :max-width="item.maxWidth ? item.maxWidth : ''">
      </el-table-column>
      <!-- 操作 -->
      <el-table-column label="操作" align="right" width="60">
        <template slot-scope="scope">
          <slot :data="scope.row" name="listOption"></slot>
        </template>
      </el-table-column>
      <!-- 列表為空 -->
      <template slot="empty">
        <common-empty />
      </template>
    </el-table>
    
 </div>
  </template>
<script>
  export default {
    props: {
        listItemConfig:{ //列表項配置
        type:Array,
        default: () => {
            return [{
                prop:'sku_name',
                label:'商品名稱',
                minWidth:200
            },{
                prop:'sku_code',
                label:'SKU',
                minWidth:120
            },{
                prop:'product_barcode',
                label:'條形碼',
                minWidth:120
            }]
      }
    }}
  }
</script>
複製程式碼

業務元件

通常是根據最小業務狀態抽象而出,有些業務元件也具有一定的複用性,但大多數是一次性元件

image

通用元件

可以在一個或多個APP內通用的元件

UI元件

  • 介面擴充套件類元件,比如彈窗

image

特點:複用性強,只通過 props、events 和 slots 等元件介面與外部通訊

表現形式?(vue)

<template>
  <div class="empty">
    <img src="/images/empty.png" alt>
    <p>暫無資料</p>
  </div>
</template>
複製程式碼

邏輯元件

  • 不包含UI層的某個功能的邏輯集合

高階元件(HOC)

高階元件可以看做是函數語言程式設計中的組合 可以把高階元件看做是一個函式,他接收一個元件作為引數,並返回一個功能增強的元件

高階元件可以抽象元件公共功能的方法而不汙染你本身的元件 比如 debouncethrottle

用一張圖來表示

jsworke

React中高階元件是比較常用的元件封裝形式,Vue官方內建了一個高階元件keep-alive通過維護一個cache實現資料持久化,但並未推薦使用HOC :(

在 React 中寫元件就是在寫函式,函式擁有的功能元件都有

Vue更像是高度封裝的函式,能夠讓你輕鬆的完成一些事情,但與高度的封裝相對的就是損失一定的靈活,你需要按照一定規則才能使系統更好的執行
複製程式碼

表現形式?(react)

品牌車系滑動的動畫

聊聊元件設計

各類元件協同組成業務模組

image

容器/展示元件

對比圖

image

引入容器元件的概念只是一種更好的組織方式

  • 容器元件專門負責和store通訊,把資料通過props傳遞給展示元件,展示元件如果資料需要更新,需要傳遞迴調給容器元件,在容器元件中執行具體操作(業務邏輯)來獲取更新結果
  • 展示型元件不再直接和store耦合,而是通過props介面來定義所需的資料和方法,複用性與正確性更能保證
  • 展示型元件直接和store通訊的話,那麼一個展示型元件就會收到限制,因為你在store裡面的欄位已經限制他的使用次數和使用的位置
  • 各司其職,不易出錯,即使出錯,也能快速定位問題

既然如此,那我什麼時候引入容器元件,什麼時候引入展示元件

引入容器元件的時機

優先考慮展示元件,當你意識到有一些中間元件不使用它繼承的props而是轉而傳遞給他們的子級,每次子級元件需要更多資料時,你都需要重新調整這些中間元件,那麼,這時候就要考慮引入容器元件

容器元件和展示元件的區別並沒有被嚴格定義,它們的區別不在技術上而是目的性上

這裡有幾個供參考的點

  • 容器元件傾向於有狀態,展示元件傾向於無狀態,這不是硬性規定,它們都是可以有狀態的
  • 不要把分離容器元件和展示元件當做教條,如果你不確定該元件是容器元件還是展示元件,就暫時不要分離,寫成展示元件,也許是為時尚早。彆著急!
  • 這是一個持續的重構過程,不用試圖一次就把它做好,習慣這種模式就會培養起一種直覺,知道何時引入容器 就像你知道何時封裝一個函式那樣兒

進行元件職能劃分的利弊

優點

  • 更好的關注分離
用這種方式寫元件,你可以更好的理解你的app和你的ui,甚至會逐漸形成你自己的開發套路
複製程式碼
  • 複用性高
一個元件只做一件事,解除了元件的耦合帶來更高複用性
複製程式碼
  • 它是app的調色版,設計師可以隨意調整它的ui而不用改變app的邏輯
  • 這會強制你提取“佈局元件”,達到更高的易用性
  • 提高健壯性
由於展示元件和容器元件是通過prop介面來連線,可以利用props的校驗機制來增強程式碼的可靠性,混合的元件就沒有這種好處

舉個?(Vue)
  props: {
    editData: Object,
    statusConfig: {
      type: Object,
      default() {
        return {
          isShowOption: true, //是否有操作欄
          isShowSaveBtn: false
        };
      }
    }
  }
複製程式碼
  • 可測試性
元件做的事情更少了,測試也會變得容易
容器元件不用關心UI的展示,只關心資料和更新
展示元件只是呈現傳入的props,寫單元測試的時候也非常容易mock資料層
複製程式碼

所謂的缺點

  • 因為容器元件/展示元件的拆分,初期會增加一些學習成本
  • 由於需要封裝一個容器,包裝一些資料和介面給展示元件,會增加一些工作量
  • 在展示元件內對props的宣告會帶來少量的工作

長遠來看,利大於弊

元件設計的邊界

物極必反,躍躍欲試前,思考以下幾個問題以引導完善元件的設計

頁面層級不宜巢狀超過三層,切勿過度設計

超過三層之後可見元件的資料傳遞的過程就會變得越複雜
複製程式碼

這個元件可否(有必要)再分?

  • 劃分粒度的根據實際情況權衡,太小會提升維護成本,太大又不夠靈活和高複用性
  • 每一個元件都應該有其獨特的劃分目的的,有的是為了複用實現,有的是為了封裝複雜度清晰業務實現
  • 元件劃分的依據通常是業務邏輯、功能,要考慮各元件之間的關係是否明確,及可複用度
  • 如果它只是幾行程式碼,那麼最終可能會建立更多的程式碼來分離它,有必要嗎?我這麼做的好處是否超過了成本?
  • 如果你當前的邏輯不太可能出現在其他地方,那麼將它嵌入其中更好,如果需要,你可以隨時抽離,畢竟元件化沒有終點
  • 效能會受到影響嗎? 如果狀態頻繁更改,並且當前在一個較大的,關係比較緊密的元件裡,為了避免效能受到影響最好抽離出來
  • 是否打破了一個邏輯上有意義的實體,倘若抽離的話,這個程式碼被複用的概率有多大

這個元件的依賴是否可再縮減?

縮減元件依賴可以提高元件的可複用度

這個元件是否對其它元件造成侵入?

  • 封裝性不足或自身越界操作,就可能對自身之外造成了侵入
  • 一個元件不應對其它兄弟元件造成直接影響
較常見的一種情況是:元件執行時對window物件新增resize監聽事件以實現元件響應視窗尺寸變化事件,這種需求的更好替代方案是:元件提供重新整理方法,由父元件實現呼叫

次優的方案是,當元件destroy前清理恢復
複製程式碼

這個元件可否複用於其它類似場景中?

需要考慮需要適用的不同場景,在元件介面設計時進行必要的相容

這個元件當別人用時,會怎麼想?

介面設計符合規範和大眾習慣,儘量讓別人用起來簡單易上手,易上手是指更符合直覺。

假如業務需要不需要這個功能,是否方便清除?

各元件之前以組合的關係互相配合,也是對功能需求的模組化抽象,當需求變化時可以將實現以模組粒度進行調整

上文提到的各種準則僅僅描述了一種開發理念,也可以認為是一種開發規範,倘若你認可這規範,對它的分治策略產生了共鳴,那我們就可以繼續聊聊它的具體實現了

問自己一個問題

你心中的相對完美的元件是什麼樣子的?

落實到具體業務中如何做

劃分依據

明確你的元件劃分依據,目前是兩種

  • 根據業務劃分
  • 根據技術劃分
  1. 我更多的是根據業務去設計我應用中的元件樹,可能會畫個草圖或xmind,它可以幫我統觀全域性
  2. 明確各個元件的邊界,內部state的設計,props的設計以及與其他元件的關係(需要回撥出去的事件)
  3. 明確各個元件的定位與職能劃分,設計好父子元件、兄弟元件的通訊機制
  4. 搭架子
  5. 架子有了,開始填空

切割模版(頁面結構模組化)

這是最容易想到的方法,當一個元件渲染了很多元素,就需要嘗試分離這些元件的渲染邏輯 我們以掘金頁面為例

jsworke

大體上看,可以分為Part1,Part2,Part3

初步開發

<template>
  <div id="app">
    <div class="panel">
      <div class="part1 left">
        <!--內容-->
      </div>
      <div class="part1 right">
        <!--內容-->
      </div>
      <div class="part1 right">
        <!--內容-->
      </div>
  </div>
</template>

複製程式碼

問題:

  • 程式碼量大,難以維護,難以測試
  • 有些許重複量

化繁為簡

<template>
  <div id="app">
      <part1 />
      <part2 />
      <part3 /> 
  </div>
</template>

複製程式碼

好處:

  • 同之前的方式相比,這個微妙的改進是革命性的
  • 解決了測試困難,維護困難的問題

問題:

  • 沒有解決程式碼重複的問題,這種按模組劃分,複用性低

但我看過很多專案的程式碼,就是這麼幹的,認為自己做了元件化,抽象的還不錯(@_@)

元件抽象

它們有相似的外層,part2和part3更有相似的titlebar,除了業務內容,完全就是一模一樣

?(vue)

<template>
  <div class="part">
    <header>
      <span>{{ title }}</span>
    </header>
    <slot name="content" />
  </div>
</template>


複製程式碼

我們將part內可以抽象的資料都做成了props,利用slot去做模版 那麼我們在開發相應Part1,Part2時

?(vue)

<template>
  <div id="app">
      <part title="亦舒">
        <div slot="content">----</div>
      </part>
      <part title="興隆臻園戶型">
        <div slot="content">-----</div>
      </part>
  </div>
</template>
複製程式碼

更具代表性的示例圖

jsworke

  • UI差異在哪裡定義?

在業務邏輯層處理

首先要明確一點,這些差異並不是元件本身造成的,是你自己的業務邏輯造成的,所以容器元件(父元件)應該為此買單
複製程式碼
  • 資料差異在哪裡定義?

結合元件本身和業務上下文將差異合理的消除在內部

比如part3中,其他的part只有一個類似更多>>的link,但是它卻有多個(一居,二居...)
這裡我推薦將這種差異體現在元件內部,設計方法也很多:
比如可以將link陣列化為links;
比如可以將更多>>看作是一個default的link,而多餘的部分則是使用者自定義的特殊link,這兩者合併組成了links。使用者自定義的預設是沒有的,需要引用元件時進行傳入。


複製程式碼
  • 元件命名規則?

元件設計初期,就應該擁有不耦合業務的名字

一個通用的或者說未來可能通用的,要有相對合理的命名,比如 Search,List,儘量不要出現與業務耦合過深的業務名詞,通用元件與業務無關,只與自身抽象的元件有關
我們在設計元件初期,就應該有這種思想,等到真正可以抽出公用元件了,再去苦逼的名改名字?
庫通常都想讓廣大開發者用,我們在設計元件時,可以降低標準到先做到你的整個APP中通用
複製程式碼

元件劃分細粒度的考量(抽之有度)

元件設計規則明明白白寫著我們要遵循單一職責原則,這也帶來了上文聊過的過度抽象(元件化)的問題,我們結合具體的業務聊一下

jsworke

要實現徽章元件,它有兩部分組成

  • 按鈕
  • 右上角提示(小紅點/icon)

兩者都是符合單一職責的,可以將其抽離成一個獨立元件,但是通常不要這麼做

因為同一個app的風格必將是統一的,除此之外沒別的應用場景了,就像上文所說的,抽離元件之前,多問自己為什麼以及投入/產出比,沒有絕對的規則
複製程式碼

tips

單一職責元件要建立在可複用的基礎上,對於不可複用的單⼀職責元件我們僅僅作為獨立元件的內部元件即可

某二手車網站體現其細粒度的例子

jsworke

思考,如果讓你實現你會如何設計... 我當初是這麼設計的

jsworke

index.js(react)

<div className="select-brand-box" onTouchStart={touchStartHandler} onTouchMove={touchMoveHandler} onTouchEnd={touchEndHandler.bind(this, touchEndCallback)}>
     <NavBar></NavBar>
     <Brand key="brands-list" {...brandsProps} />
     <Series key="series-list" {...seriesProps} >
 </div>
 
 export default BrandHoc(index);

複製程式碼

Brand.js(react)

<div className="brand-box">
    <div className="brand-wrap" ref="brandWrap">
        <p className="brands-title hot-brands-title">熱門品牌</p>
        <FlexLayout onClick={hotBrandClick}>
            <HotBrands HotBrands={hotBrands} />
        </FlexLayout>
        {!isHideStar && <UnlimitType {...unlimitProps} />}
        <AllBrands {...brandsProps} />
    </div>
    <AsideLetter {...asideProps} />
    {showPop ? <PopTips key="pop-tips" tip={currentLetter} /> : null}
    {showBrandLoading ? <Loading /> : null}
</div>
            

複製程式碼

FlexLayout.js(react)

jsworke

這個示例幾乎涵蓋了所有的規則

  • 首先元件的設計是根據業務劃分的,所以右側字母導航(AsideLetter)才沒有在最外層的容器元件,否則通訊問題會佔用一部分篇幅,事實上這是有解的
  • 入口元件是容器元件,事實上把它當做一個規則就行了,業務邏輯的載體
  • 除了容器元件外,其他的元件都被抽成公用的了,二手車平臺類似的場景非常多

jsworke

  • 賣車平臺類似的圖文混排多且形態各不相同,應用場景廣泛,抽!UI差異消化在元件內部,參考FlexLayout.js,給定default props
  • 可提取的元件過多(業務驅動)導致通訊困難如何解決? 那說明你需要新增可管理狀態的容器元件,上例中Brand,Series也是容器元件,負責管理子元件的大小事宜
  • 細粒度的考量,考慮付出產出比
<p className="brands-title hot-brands-title">熱門品牌</p> 只有一行,直接寫就完了

複製程式碼
  • 元件抽離的過程就是無限向無狀態(展示型)元件無限靠近的過程

通用性考量

元件的形態(UI)永遠是千變萬化的,但是其行為(邏輯)是固定的,因此通用元件的祕訣之⼀就是將DOM 結構的控制權交給開發者,元件只負責⾏為和最基本的DOM結構

這是一個顯眼的栗子

某一天,你接到這樣兒的需求

jsworke

開心,簡單,三下五除二寫完了

突然有一天又有這樣兒的需求

jsworke

emm..可定製?之前的select沒法用了,怎麼做?要修改上一個或者再寫一個嗎? 一旦出現了這種情況,證明之前的元件需要重新設計了

實現通用性設計的關鍵一點是放棄對Dom的掌控

那麼問題又來了,那麼多需要自定義的地方,那元件會不會很難用?

通用性設計在將Dom結構決定權交給開發者的同時指定預設值

這裡是一個新鮮出爐(vue)?

List元件

jsworke

父元件?(vue)及slot

模版(虛擬碼)
<template>
<List :data="tableData[item.type]" :loading="loading" @loadMore="loadMore" :noMore="noMore">
    <a v-if="item.type == 0" slot="listOption" slot-scope="childScope" class="edit-btn" @click="edit(childScope.data)" v-bind:key="childScope.data.id">{{Status[childScope.data.status]['text']}}</a>
</List>
</template>

config(虛擬碼)
export const Status = {
  //....
  1: {
    label: '草稿',
    type: '',
    text: '編輯',
    class: 'note'
  }}
  //...
複製程式碼

又有一個栗子(vue)

jsworke

  • Dialog只負責基礎的邏輯,交出控制權給到業務,至於你的業務需要什麼,在容器元件(業務邏輯層)去處理

忍不住放上磐石業務的反面例子

jsworke

難用無非是兩方面的問題

  1. 不肯移交控制權
  2. 沒有API文件

所有的業務邏輯與場景都包含在元件內部,外界只通過變數來控制,初衷是好的,但是隨著業務發展,元件越來越龐大,開發者也越來越力不從心了

剛好現階段UI改版,我們的工作量就由只改樣式直接轉化為推倒重來了,又沒有詳細的文件,工作量瞬間翻了N倍?寶寶心裡苦寶寶不說

善用設計模式

其實一開始,我並沒有專門去套用設計模式,完全是業務驅使 你一定見到過這樣兒的

jsworke

一旦這樣兒的邏輯多了,那是不是就跟業務耦合了,跟業務耦合多了,那元件自然沒有什麼通用性了,即使我們不考慮到通用性,那寫的累吧?

考慮下這樣寫會不會好一點

config(虛擬碼)
export const Status = {
  4: {
    label: '部分入庫',
    type: '',
    text: '檢視'
  }
}
模版(vue)
<a v-if="item.type == 0" slot="listOption" slot-scope="childScope" class="edit-btn" @click="edit(childScope.data)" v-bind:key="childScope.data.id">{{Status[childScope.data.status]['text']}}</a>

複製程式碼

世界上本沒有設計模式,寫的人多了,就自成一套脫穎而出進而被歷史銘記了!不僅如此,一部分看似複雜的業務如果合理設計配置項,可以會為你省去一大篇js

一些感悟

像磐石這種底層的業務支援系統,離不開大量的列表,查詢,編輯,詳情等,我一般會花30秒搭好架子,像但不限於下面這種

jsworke

  • index:模組入口(承擔容器職責)
  • api:整塊業務的API
  • components 業務元件集合
1. Form:表單 一般會被add.vue(編輯) 和edit.vue(詳情)引用
2. List:列表
3. Search: 搜尋元件
4. 其他業務中有但卻沒看到的基本上都已經抽離到common了 比如麵包屑導航,收起展開功能等
複製程式碼
  • libs 頁面的各種配置

具體體現(磐石剛剛重構的模組)

採購模組結構圖

image

form

image

edit

image

無論有多少種狀態,只在edit這層容器維護

要這麼做的原因

  • components中的元件只是暫存,都有可能被升級成通用元件,所以命名要注意,一類的保持了統一,防止業務耦合
  • bug有跡可循,資料的問題我一定從外向裡排查,樣式問題從裡向外排查,定位問題快
  • 與重複程式碼做鬥爭,時刻保持一種強迫症的心態去整理各個模組,形成自己的編碼風格,進而團隊風格才有可能統一

總結

  • 對於元件設計,充分的準備固然,但在現實世界中,切實的結果才是最重要的,元件設計也不要過度設計更不要停滯不前,該做的時候就去做,發現不好就去改
  • 有空閒時間就去思考早期不夠理想的程式碼,它可以作為我們向前發展的基礎
  • 技術在變遷,但元件化的核心並沒有改變,目標仍然是在API設計儘可能接近原生的情況下完成複用、解耦、封裝、抽象的目標,最終服務於開發,提高效率降低錯誤率
  • 元件化是對實現的分層,是更有效地程式碼組合方式
  • 元件化是對資源的重組和優化,從而使專案資源管理更合理,方便拔插、方便整合、方便刪除、方便刪除後重新加入
  • 這種化繁為簡的思想在後端開發中的體現是微服務,而在前端開發中的體現就是元件化
  • 元件化有利於單元測試與自測效率對重構較友好
  • 新人加入可以直接分配元件進行開發、測試,而非需要熟悉整個專案,可以從一個元件的開發使新進人員比較快速熟悉專案、瞭解到開發規範
  • 你的直接責任可能是編寫程式碼,但你的終極目標是在建立產品

最後說一句

元件化沒有終點,day day up

參考連結

相關文章