前端工程化是從軟件工程衍生出的概念, 它是指以工程化的方法構(gòu)建和維護(hù)有效射亏、實(shí)用和高質(zhì)量的軟件。
前端從刀耕火種到現(xiàn)在社區(qū)空前繁榮的發(fā)展歷史卓起,也是工程化的演變歷史衫生。從演變的歷史來看,前端工程化主要分模塊化心包,組件化类咧,工具化馒铃,自動(dòng)化蟹腾。
本篇文章主要談?wù)劷M件化以及組件化的實(shí)踐方案。
組件化
組件并不是一個(gè)新的概念区宇。c++ 中就提出過這樣的概念娃殖,組件是對數(shù)據(jù)和方法的封裝,即對象议谷。
前端中的組件則是對一塊視圖區(qū)域的封裝炉爆,多個(gè)組件組合成了一個(gè)復(fù)合組件,多個(gè)組件組合成了一個(gè)頁面卧晓,所以頁面也是組件芬首。
一個(gè)組件中包含了完整視圖結(jié)構(gòu),樣式表以及交互邏輯逼裆。它是封閉的結(jié)構(gòu)郁稍,對外提供屬性的輸入用來滿足用戶的多樣化需求,對內(nèi)自己管理內(nèi)部狀態(tài)來滿足自己的交互需求胜宇,所以組件的封裝也是對象的封裝耀怜,同樣要做到高內(nèi)聚恢着,低耦合。
組件化的目的是為了實(shí)現(xiàn)代碼的更高層次的復(fù)用财破,提高開發(fā)效率掰派,同樣它也是前端工程化的基礎(chǔ)。組件化的頁面不僅僅利于開發(fā)者在進(jìn)行單元測試左痢,同樣也有益于測試的有效進(jìn)行靡羡。
組件化的實(shí)踐
Web Components
Web Components 是W3C提出的組件化規(guī)范。允許開發(fā)者使用規(guī)范提供的API 以及HTML 模板來實(shí)現(xiàn)可重用的定制元素俊性,與HTML 原本支持的標(biāo)簽元素相同亿眠,可以直接在頁面中使用。
Web Components 主要有三種技術(shù)構(gòu)成:
- Custom elements(自定義元素):一組JavaScript API磅废,允許您定義custom elements及其行為纳像,然后可以在的用戶界面中按照需要使用它們。
- Shadow DOM(影子DOM):一組JavaScript API拯勉,用于將封裝的“影子”DOM樹附加到元素(與主文檔DOM分開呈現(xiàn))并控制其關(guān)聯(lián)的功能竟趾。通過這種方式,可以保持元素的功能私有宫峦,這樣它們就可以被腳本化和樣式化岔帽,而不用擔(dān)心與文檔的其他部分發(fā)生沖突。
-
HTML templates(HTML模板):
<template>
和<slot>
元素使您可以編寫不在呈現(xiàn)頁面中顯示的標(biāo)記模板导绷。然后它們可以作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用犀勒。
相關(guān)的開發(fā)教程,請參見 Web Components
但是由于規(guī)范不是很完善妥曲,現(xiàn)只有Firefox(版本63)贾费、Chrome和Opera都默認(rèn)支持Web組件; Safari支持許多web組件特性,但比前面的瀏覽器少; Edge正在開發(fā)一個(gè)實(shí)現(xiàn)檐盟。
Stencil Js
Stencil 是由Ionic 團(tuán)隊(duì)開發(fā)出的前端視圖庫褂萧,類似react。它可以將開發(fā)者開發(fā)的組件編譯成 Web Components 葵萎,可以直接運(yùn)行在瀏覽器中导犹。抹平了前端不同技術(shù)架構(gòu)造成的隔離,比如采用Stencil 開發(fā)的組件羡忘,在Vue 和 React 中都可以使用谎痢,不需要再進(jìn)行額外的兼容工作。
Angular Directives / VUE Components / React Components / Backbone Components / Anything... Components
社區(qū)的視圖庫或者框架提供的組件解決方案卷雕,是當(dāng)前最具代表性的組件化的實(shí)踐节猿,但是不同的框架之間的隔離也同樣是前端需要考慮的問題。
封裝React 組件
封裝組件最主要要遵循開閉原則和單一職責(zé)原則爽蝴,這也是封裝一個(gè)高內(nèi)聚低耦合的組件的核心沐批。
簡而言之纫骑,就是首先要知道你的組件是做什么的,并且它只做這一件事九孩;其次對你的組件的對外的屬性輸出和對內(nèi)的狀態(tài)管理有明確和清晰的認(rèn)知先馆,什么開放給用戶,什么自己管理躺彬。
1.界定一個(gè)組價(jià)的Scope
這里推薦React 官方文檔上拆分一個(gè)頁面成多個(gè)組件的方法煤墙,React 設(shè)計(jì)哲學(xué)。
- FilterableProductTable (橙色): 是整個(gè)示例應(yīng)用的整體
- SearchBar (藍(lán)色): 接受所有的用戶輸入
- ProductTable (綠色): 展示數(shù)據(jù)內(nèi)容并根據(jù)用戶輸入篩選結(jié)果
- ProductCategoryRow (天藍(lán)色): 為每一個(gè)產(chǎn)品類別展示標(biāo)題
- ProductRow (紅色): 每一行展示一個(gè)產(chǎn)品
對應(yīng)的層級(jí):
FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
2.區(qū)分Props 和 State
在React 中 UI = Component(Props, State)宪拥。
Props 是組件對外的接口仿野,不可變。組件內(nèi)可以引用其他組件她君,組件之間的引用形成了一個(gè)樹狀結(jié)構(gòu)(組件樹)脚作,如果下層組件需要使用上層組件的數(shù)據(jù)或方法,上層組件就可以通過下層組件的props屬性進(jìn)行傳遞缔刹,因此props是組件對外的接口球涛。
State 是組件對內(nèi)的狀態(tài),可變校镐。state必須能代表一個(gè)組件UI呈現(xiàn)的完整狀態(tài)集亿扁,即組件對應(yīng)UI的任何改變,都可以從state的變化中反映出來鸟廓;同時(shí)从祝,state還必須是代表一個(gè)組件UI呈現(xiàn)的最小狀態(tài)集,即state中的所有狀態(tài)都是用于反映組件UI的變化引谜,沒有任何多余的狀態(tài)牍陌,也不需要通過其他狀態(tài)計(jì)算而來的中間狀態(tài)。
組件中用到的一個(gè)變量是不是應(yīng)該作為組件state煌张,可以通過下面的4條依據(jù)進(jìn)行判斷:
- 這個(gè)變量是否是通過props從父組件中獲饶派摹?如果是骏融,那么它不是一個(gè)狀態(tài)。
- 這個(gè)變量是否在組件的整個(gè)生命周期中都保持不變萌狂?如果是档玻,那么它不是一個(gè)狀態(tài)。
- 這個(gè)變量是否可以通過state 或props 中的已有數(shù)據(jù)計(jì)算得到茫藏?如果是误趴,那么它不是一個(gè)狀態(tài)。
- 這個(gè)變量是否在組件的render方法中使用务傲?如果不是凉当,那么它不是一個(gè)狀態(tài)枣申。這種情況下,這個(gè)變量更適合定義為組件的一個(gè)普通屬性(除了props 和 state以外的組件屬性 )看杭,例如組件中用到的定時(shí)器忠藤,就應(yīng)該直接定義為this.timer,而不是this.state.timer楼雹。
同時(shí)模孩,并不是組件中用到的所有變量都是組件的State!當(dāng)存在多個(gè)組件共同依賴同一個(gè)狀態(tài)時(shí)贮缅,一般的做法是狀態(tài)上移榨咐,將這個(gè)狀態(tài)放到這幾個(gè)組件的公共父組件中。
在項(xiàng)目中開發(fā)組件
在項(xiàng)目中開發(fā)組件谴供,最重要的是明確組件的作用域块茁。
- 業(yè)務(wù)組件,它會(huì)包含你的業(yè)務(wù)邏輯桂肌,方便你對代碼的分割龟劲。
- UI組件(以上提出的關(guān)于組價(jià)的封裝的方法論都是UI 組件), 即一個(gè)公共的組件,它所包含的應(yīng)該只有該UI 組件的交互邏輯轴或。如果當(dāng)你覺得它會(huì)有一些公共的業(yè)務(wù)邏輯的時(shí)候昌跌,請?jiān)龠M(jìn)行二次封裝,即在該UI 組件的基礎(chǔ)上在進(jìn)行一層業(yè)務(wù)的包裹照雁。
封裝UI組件
社區(qū)中優(yōu)秀的React 組件庫有很多蚕愤,但是不同的公司有不同的設(shè)計(jì)語言,如果單純的采用某社區(qū)UI庫的話饺蚊,比如Ant-design, 勢必不能滿足業(yè)務(wù)的需求和風(fēng)格的需求萍诱。
所以對于我們來說,我們現(xiàn)階段采用的封裝復(fù)合公司風(fēng)格的UI 組件時(shí)污呼, React-component裕坊,在這個(gè)基礎(chǔ)庫上進(jìn)行二次開發(fā)。
- React-component, 是Ant-design 的組件基礎(chǔ)庫燕酷,它提供了完善的API以及簡單的樣式籍凝,所以我們需要對它的樣式進(jìn)行定制,滿足公司的設(shè)計(jì)語言苗缩。
import * as React from "react";
import styles from "./Switch.less";
import RcSwitch from "rc-switch";
import "rc-switch/assets/index.css";
type SwitchChangeEventHandler = (
checked: boolean,
event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
) => void;
interface SwitchProps {
className?: string;
prefixCls?: string;
disabled?: boolean;
checkedChildren?: React.ReactNode;
unCheckedChildren?: React.ReactNode;
onChange?: SwitchChangeEventHandler;
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
onClick?: SwitchChangeEventHandler;
tabIndex?: number;
checked?: boolean;
defaultChecked?: boolean;
loadingIcon?: React.ReactNode;
style?: React.CSSProperties;
title?: string;
text?: string[];
}
interface SwitchState {
checked: boolean;
}
export default class Switch extends React.PureComponent<SwitchProps, SwitchState> {
constructor(props) {
super(props);
this.state = {
checked: !!props.checked
};
}
...
...
...
render() {
const { text } = this.props;
const { checked } = this.state;
const label = checked ? text[0] : text[1];
return (
<div className={styles.switch}>
{text && text.length && <span className={styles.label}>{label}</span>}
<RcSwitch {...this.props} className={styles.bgColor} onChange={this.onChangeHandler} />
</div>
);
}
}