組件化通用模式

1、前言

模式是一種規律或者說有效的方法,因此掌握某一種實踐總結出來的模式是快速學習和積累的較好方法,模式的對錯須要本身去把握,可是隻有量的積累纔會發生質的改變,多思考老是好的。(下面的代碼實例更可能是 React 相似的僞代碼,不必定可以執行,函數相似的玩意更容易簡單描述問題)javascript

2、前端的關注點遷移

這篇文章主要介紹如今組件化的一些模式,以及設計組件的一些思考,那麼爲何是思考組件呢?由於如今前端開發過程是以組件爲基本單位來開發。在組件化被普及(由於說起的時間是很早的或者說有些廠實現了本身的一套可是在整個前端還未是一種流行編寫頁面的單元)前,咱們的大多數聚焦點是資源的分離,也就是 HTML、CSS、JavaScript,分別負責頁面信息、頁面樣式、頁面行爲,如今咱們編程上的聚焦點更多的是聚焦在數據組件css

關注點遷移

可是有時候會發現只關心到這一個層級的事情在某些業務狀況下搞不定,好比組件之間的關係、通訊、可擴展性、複用的粒度、接口的友好度等問題,因此須要在組件上進行進一步的延伸,擴展一下組件所參考的視角,延伸到組件模塊組件系統的概念來指導咱們編寫代碼。html

組件級別

概念可能會比較生硬,可是你若是有趣的理解成搭積木的方式可能會更好擴展思路一點。前端

3、數據之於組件

在說組件以前,先來講下數據的事情,由於如今數據對於前端是很重要的,其實這是一個前、後端技術和工做方式演變造成的,之前的數據行爲和操做都是後端處理完成以後,前端基本拿到的就是直接可用的 View 展現數據,可是隨着後端服務化,須要提供給多個端的數據以及先後端分離工做模式的造成,前端就變得愈來愈複雜了,其實 SPA 的造成也跟這些有必定關係,一是體驗可能對於用戶好,二是演變決定了這種方式。此時,前端的數據層就須要設計以及複用一些後端在這一層級的成熟模式,在這裏就產生了一種思想的交集。java

好比如今有一個 RadioGroup 組件,而後有下面 2 種數據結構能夠選擇:ios

items = [{
    id: 1,
    name: 'A',
    selected: true
}, {
    id: 2,
    name: 'B',
    selected: false
}];
data = {
    selected: 1
    items: [{
        id: 1,
        name: 'A'
    }, {
        id: 2,
        name: 'B'
    }]
};

那麼咱們的組件描述(JSX)會怎麼寫呢?
第一種:typescript

items.map(item =>
    return <CheckBox key={`checkbox-${item.id}`}
                label={item.name}
                selected={item.selected}
                onClick={this.handleClick} />
);

第二種:編程

data.items.map(item => 
    const isSelected = item.id === data.selected;
    
    return <Checkbox key={`checkbox-${item.id}`
                label={item.name}
                selected={isSelected}
                onClick={this.handleClick}/>
);

固然,數據結構的選擇上是根據需求,由於不一樣的數據結構有不一樣的優點,好比這裏第二種相似 Dict 的查詢很方便,數據也很乾淨,第一種渲染是比較直接的,可是要理解組件的編寫方式其實很大程度上會跟數據產生一種關係,有時候編寫發現問題能夠返過來思考是否換種結構就變簡單了。json

數據就談這些吧,否則都能單獨開話題了,接下來看下組件,若是要學習模式就須要採集樣本而後去學習與總結,這裏咱們來看下 Android && iOS 中的組件長什麼樣子,而後看是否能給咱們平常編寫 Web 組件提供點靈感,篇幅有限,原本是應該先看下 GUI 的方式。bootstrap

4、iOS 端的組件概覽

假設,先摒棄到 Web 組件的形態比其餘端豐富,若是不假設那麼這套估計不是那麼適用。

4.1 iOS

iOS 的 View 聲明可以經過一個故事板的方式,特別爽,好比這裏給按鈕的狀態設定高亮、選中、失效這種,方便得很。

iOS

看完界面,直接的感受下,而後咱們來看下這個故事板的源碼,上面是 XML 的描述,描述了組件的 View 有哪些部件以及 ViewController 裏面映射的屬性,用來將 View 和 ViewController 進行解耦。

<!-- 結構描述 -->
<scenes>
  <scene sceneID="tne-QT-ifu">
    <objects>
      <viewController title=「Login" customClass="ViewController">
        ...
        <view key="view" contentMode="scaleToFill"></view>
        ...
        <!-- 這裏就是描述 vm 關聯對象的地方,ios 裏面可能稱之爲 outlet -->
        <connections>
          <outlet property="passwordTextField"/>
          <outlet property="tipValidLabel"/>
        </connections>
      </viewController>
    </objects>
  </scene>
</scenes>

<!-- 狀態 & 樣式描述 -->
<!-- 單獨一個 button 組件描述 -->
<button>
  <state key="normal" title="Login">
      <color key="titleColor" red="1" green="1" blue="1" alpha="1"/>
  </state>
</button>

我這裏定義的按鈕狀態、顏色都在這裏,分別給他們命名:結構描述樣式描述

那麼具體怎麼給用戶交互,比較編程化的東西在 ViewController,來看下代碼:

// 數據行爲描述
// connection 中關聯的鉤子
@IBOutlet private weak var passwordTextField: UITextField!
@IBOutlet private weak var tipValidLabel: UILabel!

// 一個密碼輸入框的驗證邏輯,最後綁定給 tipValidLabel、loginButton 組件狀態上
let passwordValid: Observable<Bool> = passwordTextField.rx.text.orEmpty
    .map { newPassword in newPassword.characters.count > 5 }

passwordValid
    .bind(to: tipValidLabel.rx.isHidden)
    .disposed(by: disposeBag)

passwordValid
    .bind(to: loginButton.rx.isEnabled)
    .disposed(by: disposeBag)

上面代碼總體能夠看作是響應式的對象,綁定3個組件之間的交互,密碼不爲空以及大於5個字符就執行 bind 地方,主要是同步另外2個組件的狀態。其實也不須要看懂代碼,這只是爲了體會客戶端組件的方式的例子,ViewController 我這裏就叫:數據行爲描述。這樣就有組件最基本的三個描述了:結構、樣式、數據行爲,雖然樣本很少,可是這裏直接描述它們就是一個組件的基本要素,整個故事板和 swift 代碼很好的描述。

5、什麼是組件?

5.1 組件描述
  1. 結構描述
  2. 樣式描述
  3. 數據描述

對於組件來講,也是一份代碼的集合,基本組成要素仍是須要的,可是這三種要素存在和之前的 HTML, CSS, JS 三種資源的分離是不同的,到了組件開發,更多的是關注如何將這些要素鏈接起來,造成咱們須要的組件。

好比 React 中對這三要素的描述用一個 .js 文件所有描述或者將結構、數據包裹在一塊兒,樣式描述分離成 .<style> 文件,這裏就可能會造成下面 2 種形式的組件編寫。

=> 3 -> (JSX + styled-components)

// 組件樣式
const Title = styled.h1`
    font-size: 1.5em;
    text-align: center;
`;

// 組件內容
<Title>Hello World!</Title>

=> 2 + 1 -> (JSX + CSS Module)

export default function Button(props) {
    // 分離的樣式,經過結構化 className 來實現鏈接
    const buttonClass = getClassName(['lv-button', 'primary']);

    return (
        <button onClick={props.onClick} className={buttonClass}>
            {props.children}
        </button>
    );
}

可能最開始不少不習慣這樣寫,或者說不接受這類理念,那麼再看下 Angular 的實現方式,也有 2 種:

(1) 採用元數據來裝飾一個組件行爲,而後樣式和結構可以經過導入的方式鏈接具體實現文件。

@Component({
    selector: 'app-root',
    // 結構模板
    templateUrl: './app.component.html',
    // 樣式模板
    styleUrls: ['./app.component.css']
})
// 等同於上面描述的 iOS 組件的 ViewController
export class AppComponent { }

(2) 與第一種方式不一樣的地方是可以直接將結構和樣式寫到元數據中。

@Component({
    selector: 'app-root',
    template: `
        <style>
            h1 { font-weight: normal; }
        </style>
        
        <h1>{{title}}</h1>
        <ul>
            <li *ngFor="let item of items">{{ item }}</li>
        </ul>
    `,
    // styles: ['h1 { font-weight: normal; }']
})
export class AppComponent {
    title = 'Hello Angular';
    items: number[] = [1, 2, 3];
}

不管實現的形式如何,其實基本不會影響太多寫代碼的邏輯,樣式是目前前端工程化的難點和麻煩點,因此適合本身思惟習慣便可。這裏須要理解的是學習一門以組件爲核心的技術,都可以先找到要素進行理解和學習,構造最簡單的部分。

5.2 組件特性

雖然有了描述一個組件的基本要素,可是還遠不足以讓咱們開發一箇中大型應用,須要關注其餘更多的點。這裏提取組件基本都有的特性:

1. 註冊組件

將組件拖到故事板

2. 組件接口(略)

別人家的代碼可以修改組件的部分

3. 組件自屬性

組件建立之初,就有的一些固定屬性

4. 組件生命週期

組件存在到消失如何控制以及資源的整合

5. 組件 Zone

組件存在於什麼空間下,或者說是上下文,極可能會影響到設計的接口和做用範圍,好比 React.js 可用於寫瀏覽器中的應用,React Native 能夠用來寫相似原生的 App,在設計上大多數能雷同,可是平臺的特殊地方也許就會出現對應的代碼措施)

這些主要就是拿來幫助去看一門不懂的技術的時候,只要是組件的範圍,就先看看有沒有這些東西的概念能不能聯想幫助理解。

具體來看下代碼是如何來落地這些模式的。

1.組件註冊,其實註冊就是讓代碼識別你寫的組件

(1) 聲明即定義,導入即註冊

export SomeOneComponent {};
import {SomeOneComponent} from 'SomeOneComponent';

(2) 直接了當的體現註冊的模式

AppRegistry.registerComponent('ReactNativeApp', () => SomeComponent);

(3) 擁有模塊來劃分組件,以模塊爲單位啓動組件

@NgModule({
    // 聲明要用的組件
    declarations: [
        AppComponent,
        TabComponent,
        TableComponent
    ],
    // 導入須要的組件模塊
    imports: [
        BrowserModule,
        HttpModule
    ],
    providers: [],
    // 啓動組件, 每種平臺的啓動方式可能不同
    bootstrap: [AppComponent]
})
export class AppModule { }

2.組件的自屬性

好比 Button 組件,在平時場景下使用基本須要綁定一些自身標記的屬性,這些屬性可以認爲是一個 Component Model 所應該擁有,下面用僞代碼進行描述。

// 將用戶的 touch, click 等行爲都抽象成 pointer 的操做
~PointerOperateModel {
    selected: boolean;
    disabled: boolean;
    highlighted: boolean;
    active: boolean;
}

ButtonModel extends PointerOperateModel { }
LinkModel extends PointerOperateModel { }
TabModel extends PointerOperateModel { }
...

// 或者是具備對立的操做模型
~ToggleModel {
    on: boolean;
}

OnOffModel extends ToggleModel { }
SwitchModel extends ToggleModel { }
MenuModel extends ToggleModel { }

...

// 組件的使用
this.ref.attribute = value;
this.ref.attribute = !value;

這些操做若是須要更少的代碼,也許可以這樣:

~ObserverState<T> {
    set: (value: T) => void;
    get: (value: T) => T;
    changed: () => void;
    cacheQueue: Map<string, T>;
    private ___observe: Observe;
}

Model extends ObserverState { }

基本上組件的這些屬性是遍及在咱們整個代碼開發過程當中,因此是很重要的點。這裏還有一個比較重要的思考,那就是表單的模型,這裏不擴展開來,能夠單獨立一篇文章分析。

3.組件的聲明週期

與其說是生命週期,更多的是落地時候的代碼鉤子,由於咱們要讓組件與數據進行鏈接,也許須要在特定的時候去操做一份數據。在瀏覽器(宿主)中,要知道具體是否已經可用是一個關鍵的點,因此任何在這個平臺的組件都會有這類週期,若是沒有的話用的時候就會很蛋疼。

最簡單的路線是:

mounted => update => destory

可是每每實際項目會至少加一個東西,那就是異常,因此就可以開分支了,可是更清晰的應該是平行的週期方式。

mounted => is error => update => destory

4.組件 Zone

組件在不一樣的 Zone 下可能會呈現不一樣的狀態,這基本上是受外界影響的,而後本身作出反應。這裏能夠針對最基本的組件使用場景舉例,可是這個 Zone 是一種泛化概念。

好比咱們要開發一個彈框組件:Modal,先只考慮一個最基本需求:彈框的位置,這個彈框到底掛載到哪兒?

  1. 掛載到組件內部;
  2. 掛載到最近的容器節點下;
  3. 掛載到更上層的容器,以致於 DOM 基礎節點。

每一種場景下的彈框,對於每種組件的方案影響是不一樣的:

  1. 組件內部,若是組件產生了 render,極可能受到影響;
  2. 掛載到最近的容器組件,看似問題不大,可是業務組件的拆、合是不定的,對於不定的需求極可能代碼會改變,可是這種方案是不錯的,不用寫太遠,固然在 React 16 有了新的方案;
  3. 掛載到更高的層級,這種方案適合項目對彈框需求依賴比較強的狀況吧,由於受到的影響更小,彈框其實對於前端更強調的是一種渲染或者說是一種交互。

5.組件的遞歸特性

組件可以擁有遞歸是一個很重要的縱向擴展的特性,每一種庫或者框架都會支持,就要看支持對於開發的天然度,好比:

// React
this.props.children

// Angular
<ng-content></ng-content>
基本上能夠認爲如今面向組件的開發是更加貼近追求的設計即實現的理想,由於這是面向對象方法論不容易具有的,組件是一種更高抽象的方法,一個組件也許會有對象分析的插入,可是對外的表現是組件,一切皆組件後通過積累,這將大大提高開發的效率。

6、如何設計組件?

通過前面的描述,知道了組件的概念和簡單組件的編寫方法,可是掌握了這些東西在實際項目中仍是容易陷入蛋痛的地步,由於組件只是組成一個組件模塊的基礎單元,慢慢的開發代碼的過程當中,咱們須要良好的去組織這些組件讓咱們的模塊即實現效果的同時也擁有必定的魯棒性和可擴展性。這裏將組件的設計方法分爲 2 個打點:

  1. 橫向分類
  2. 縱向分層

其實這種思路是一直以來都有的,這裏套用到平時本身的組件設計過程當中,讓它幫助咱們更容易去設計組件。

組件設計

這種設計的方法論是一個比較容易掌握和把握的,由於它的模型是一個二維的(x, y)兩個方向去拆、合本身的組件。注意,這裏基本上的代碼操做單元是組件,由於這裏咱們要組裝的目標是模塊^0^感受很好玩的樣子,舉例來描述一下。

好比咱們如今來設計比較經常使用的下拉列表組件(DropDownList),最簡單的有以下作法:

class DropDownList {
    render() {
        return (
            <div>
                <div>
                    <Button onClick={this.handleClick}>請選擇</Button>
                </div>
                <DropDownItems>
                {this.props.dataSource.map((itemData, index) => <DropDownItem></DropDownItem>)}
                </DropDownItems>
            </div>
        );
    }
}

如今本身玩的往上加點需求,如今我須要加一個列表前面都加一個統一的 icon, 首先咱們要作的確定是要有一個 Icon 的組件,這個設計也比較依賴場景,目前咱們先設計下拉。如今就有2種方案:

  1. 在 DropDownList 組件裏面加一個判斷,動態加一個組件就行;
  2. 從新寫一個組件叫 DropDownIconList。

第一種方案比較省事,可是其實寫個 if...else... 算是一個邏輯分支的代碼,之後萬一要加一個 CheckBox 或者 Radio 組件在前面...

第二種方案看上去美好,可是容易出現代碼變多的狀況,這時候就須要再從新分析需求變化以及變化的趨勢。

這時候按垂直和水平功能上,這裏拆分 DropDownIconList 組件能夠當作一個水平的劃分,從垂直的狀況來看,將下拉這一個行爲作成一個組件叫 DropDown,最後就變成了下面的樣子:

class DropDown  {
    render() {
        <div>
            <div>
                <p onClick={this.handleClick}>請選擇</p>
            </div>
            <div>{this.props.children}</div>
        </div>
    }
}

class DropDownList {
    render() {
        return (
            <DropDown onClick={this.handleClick} selected={selectedItems}>
                <DropDownItems>
                    {this.props.dataSource.map((itemData, index) => <DropDownItem></DropDownItem>)}
                </DropDownItems>
            </DropDown>
        );
    }
}

class DropDownIconList {
    render() {
        return (
            <DropDown onClick={this.handleClick} selected={selectedItems}>
                <DropDownItems>
                    {this.props.dataSource.map((itemData, index) => <DropDownIconItem></DropDownIconItem>)}
                </DropDownItems>
            </DropDown>
        );
    }
}

這樣的缺點就是存在多個組件,也許會有冗餘代碼,優勢就是之後增長相似組件,不會將代碼的複雜度都加到一份代碼中,好比我要再加一個下拉里面分頁、加入選中的項、下拉內容分頁、下拉的無限滾動等等,都是不用影響以前那份代碼的擴展。

7、讓組件鏈接起來

組件化的開發在結構上是一種分形架構的體現,是一個應用引向有序組件構成的過程。組件系統的複雜度能夠理解成 f(x) = g(x) + u(x), g(x) 表示特有功能,u(x)表示功能的交集或者說有必定重合度的集合。組件彈性體如今 u(x) -> 0(趨近)的過程當中,這個論點可參考:面向積木(BO)的方法論與分形架構

上面的過程當中,有了組件組件模塊,既然有了基礎的實體,那麼他們或多或少會有溝通的需求(活的模塊)。基本上如今主流的方案能夠用下面的圖來表示。

數據流

咱們提取一下主要的元素:

  1. Component 實體
  2. Component 實體的集合:Container
  3. Action
  4. Action 的操做流
  5. Service Or Store
  6. 狀態同步中心
  7. Observable Effect

若是要說單向數據流和雙向綁定的體現基本能夠理解成體如今虛線框選的位置,若是組件或者Store是一個觀察的模型,那麼方案實現後就極可能往雙向綁定靠近。若是是手動黨鏈接 ViewValue 和 ModelValue,按照一條流下來能夠理解成單向流。雖然沒有按定義徹底約束,可是代碼的落地上會造成這種模式,這塊細講也會是一個單獨的話題,等以後文章再介紹各類模式。

組件的關係可以體如今包含、組合、繼承、依賴等方面,若是要更好的鬆耦合,通常就體如今配置上,配置就是一種天然的聲明式,這是聲明式的優點同時也是缺點。

以上是一些對組件的思考,碼字好累,不必定很深刻,可是但願可以幫助到剛踏入組件化前端開發的小夥伴~若是以爲有幫助請幫忙推薦,也能夠閱讀原文:小擼的博客

相關文章
相關標籤/搜索