组件化通用模式

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,按照一条流下来能够理解成单向流。虽然没有按定义彻底约束,可是代码的落地上会造成这种模式,这块细讲也会是一个单独的话题,等以后文章再介绍各类模式。

组件的关系可以体如今包含、组合、继承、依赖等方面,若是要更好的松耦合,通常就体如今配置上,配置就是一种天然的声明式,这是声明式的优点同时也是缺点。

以上是一些对组件的思考,码字好累,不必定很深刻,可是但愿可以帮助到刚踏入组件化前端开发的小伙伴~若是以为有帮助请帮忙推荐,也能够阅读原文:小撸的博客

相关文章
相关标签/搜索