組件化思想並非前端獨有的,但倒是前端技術的延伸 任何軟件開發過程,或多或少都有那麼一些組件化的需求css
隨着三大框架崛起,前端組件化逐漸成爲前端開發的迫切需求,一種主流,一種共識,它不只提升開發效率,同時也下降了維組件內聚原則護成本 開發者們不須要再面對一堆晦澀難懂的代碼,轉而只須要關注以組件⽅式存在的代碼⽚段 這是一場新的挑戰!前端
面試官一般會問 寫過前端通用組件嗎?
複製代碼
你可能會自信的表示: sure!vue
emm..是的嗎?react
前端工程經歷的三個階段git
解決完開發效率,還須要兼顧運行性能, 故而選擇某種構建工具,對代碼進行壓縮,校驗,以後再以頁面爲單位進行簡單的資源合併github
解決了基本開發效率和運行效率以後,開始考慮維護效率了web
分而治之(以分解下降複雜度)是軟件工程中的重要思想,是複雜系統開發和維護的基石,模塊化就是前端的分治手段面試
所以,模塊化強調的是拆分,最大的價值就是分治,意味着無論你未來是否要複用這塊兒代碼,都有將他們拆成一個模塊的理由編程
將一個大問題,不斷的拆解爲各個小問題進行分析研究,而後再組合到一塊兒(分而治之原則)element-ui
無模塊化->函數寫法->對象寫法->自執行函數->CommonJS/AMD/CMD->ES6 Module
複製代碼
css模塊化是在less,sass等預處理器的支持下實現的
複製代碼
固然是不夠的
模塊化強調的是拆分,不管是從業務角度仍是從架構、技術角度,模塊化首先意味着將代碼、數據等內容按照其職責不一樣分離
單純的橫向拆分業務功能模塊有一些問題
隨着業務發展,」過程線「也會愈來愈長,其餘項目成員根據各自須要,在」過程線「 加插各自邏輯,最終這個頁面的邏輯變得難以維護
咱們須要擺脫【一瀉而下】式的代碼編寫
複製代碼
除了JS和CSS,界面也須要拆分,如何讓模塊化思想融入HTML語言
複製代碼
在大肆宣揚組件化開發概念以前,也經歷了尋求組件化最佳實踐的階段
落實到實際開發中像這樣
咱們能夠獲取信息
tabContainer
,listContainer
和 imgsContainer
三個模塊咦?嗅到一絲組件化的味道
歷史總有遺🐖
早在N年前微軟提出過一套解決方案,名爲HTML Component
是一個比較完整的組件化方案了,但卻沒可以進入標準,默默地消失了,今天的角度來看,它能夠說是生不逢時
當時」所謂的組件「
因而 W3C 按耐不住了,制定一個 WebComponents 標準,爲組件化的將來指引了明路
大體四部分功能
<template>
定義組件的 HTML模板能力咱們思考一下,可行的實踐化方案須要具有哪些能力
今天的前端生態裏面 React,Angular和Vue三分天下,即便它們定位不一樣,但核心的共同點就是提供了組件化的能力,算是目前是比較好的組件化實踐
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>
複製代碼
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>
);
}
}
複製代碼
<input type="text" ng-model="firstname">
var app = angular.module('myApp', []);
app.controller('formCtrl', function($scope) {
$scope.firstname = "John";
});
複製代碼
template
,style
,script
,script又能夠由各類模塊組成隨着前端開發愈來愈複雜、對效率要求越來高,由項目級模塊化開發,進一步提高到通用功能組件化開發,模塊化是組件化的前提,組件化是模塊化的演進
組件化方案下,咱們須要具備組件化設計思惟,它是一種【整理術】幫助咱們高效開發整合
任何一個組件都應該遵照一套標準,可使得不一樣區域的開發人員據此標準開發出一套標準統一的組件
複製代碼
描述了組件的細粒度,遵循單一職責原則,保持組件的純粹性
屬性配置等API對外開放,組件內部狀態對外封閉,儘量的少與業務耦合
複製代碼
UI差別,消化在組件內部(注意並非寫一堆if/else)
輸入輸出友好,易用
複製代碼
追求短小精悍
適用SPOT法則
Single Point Of Truth,就是儘可能不要重複代碼,出自《The Art of Unix Programming》
複製代碼
使用父組件的 state 控制子組件的狀態而不是直接經過 ref 操做子組件
複製代碼
設計不當致使環形依賴示意圖
影響
組件間耦合度高,集成測試難 一處修改,到處影響,交付週期長 由於組件之間存在循環依賴,變成了「先有雞仍是先有蛋」的問題
那假若咱們真的遇到了這種問題,就要考慮如何處理
消除環形依賴
咱們的追求是沿着逆向的依賴關係便可尋找到全部受影響的組件
建立一個共同依賴的新組件
- 組件的抽象程度與其穩定程度成正比,
- 一個穩定的組件應該是抽象的(邏輯無關的)
- 一個不穩定的組件應該是具體的(邏輯相關的)
- 爲下降組件之間的耦合度,咱們要針對抽象組件編程,而不是針對業務實現編程
複製代碼
若是一個數據能夠由另外一個 state 變換獲得,那麼這個數據就不是一個 state,只須要寫一個變換的處理函數,在 Vue 中可使用計算屬性
若是一個數據是固定的,不會變化的常量,那麼這個數據就如同 HTML 固定的站點標題同樣,寫死或做爲全局配置屬性等,不屬於 state
若是兄弟組件擁有相同的 state,那麼這個state 應該放到更高的層級,使用 props 傳遞到兩個組件中
複製代碼
父組件不依賴子組件,刪除某個子組件不會形成功能異常
複製代碼
除了數據,避免複雜的對象,儘可能只接收原始類型的值
複製代碼
把組件內部能夠完成的工做作到極致,雖然提倡擁抱變化,但接口不是越多越好
若是常量變爲 props 能應對更多的場景,那麼就能夠做爲 props,原有的常量可做爲默認值。
若是須要爲了某一調用者編寫大量特定需求的代碼,那麼能夠考慮經過擴展等方式構建一個新的組件。
保證組件的屬性和事件足夠的給大多數的組件使用。
複製代碼
那有了組件設計的「API」,就必定能開發出高質量的組件嗎?
組件最大的不穩定性來自於展示層,一個組件只作一件事,基於功能作好職責劃分
根據經驗,我將組件應分爲如下幾類
爲了讓開發者更關注業務邏輯,涌現出了不少優秀的UI組件庫 好比antd
,element-ui
,咱們只須要調用API便能知足大部分的業務場景,前端角色後置了,開發變得更簡單了
一個容器性質的組件,通常看成一個業務子模塊的入口,好比一個路由指向的組件
DOM
標籤<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>
複製代碼
主要表現爲組件是怎樣渲染的,就像一個簡單的模版渲染過程
<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>
複製代碼
一般是根據最小業務狀態抽象而出,有些業務組件也具備必定的複用性,但大多數是一次性組件
能夠在一個或多個APP內通用的組件
特色:複用性強,只經過 props、events 和 slots 等組件接口與外部通訊
<template>
<div class="empty">
<img src="/images/empty.png" alt>
<p>暫無數據</p>
</div>
</template>
複製代碼
高階組件能夠看作是函數式編程中的組合 能夠把高階組件看作是一個函數,他接收一個組件做爲參數,並返回一個功能加強的組件
高階組件能夠抽象組件公共功能的方法而不污染你自己的組件 好比 debounce
與 throttle
用一張圖來表示
React中高階組件是比較經常使用的組件封裝形式,Vue官方內置了一個高階組件keep-alive經過維護一個cache實現數據持久化,但並未推薦使用HOC :(
在 React 中寫組件就是在寫函數,函數擁有的功能組件都有
Vue更像是高度封裝的函數,可以讓你輕鬆的完成一些事情,但與高度的封裝相對的就是損失必定的靈活,你須要按照必定規則才能使系統更好的運行
複製代碼
品牌車系滑動的動畫
對比圖
既然如此,那我何時引入容器組件,何時引入展現組件
優先考慮展現組件,當你意識到有一些中間組件不使用它繼承的props而是轉而傳遞給他們的子級,每次子級組件須要更多數據時,你都須要從新調整這些中間組件,那麼,這時候就要考慮引入容器組件
容器組件和展現組件的區別並無被嚴格定義,它們的區別不在技術上而是目的性上
用這種方式寫組件,你能夠更好的理解你的app和你的ui,甚至會逐漸造成你本身的開發套路
複製代碼
一個組件只作一件事,解除了組件的耦合帶來更高複用性
複製代碼
因爲展現組件和容器組件是經過prop接口來鏈接,能夠利用props的校驗機制來加強代碼的可靠性,混合的組件就沒有這種好處
舉個🌰(Vue)
props: {
editData: Object,
statusConfig: {
type: Object,
default() {
return {
isShowOption: true, //是否有操做欄
isShowSaveBtn: false
};
}
}
}
複製代碼
組件作的事情更少了,測試也會變得容易
容器組件不用關心UI的展現,只關心數據和更新
展現組件只是呈現傳入的props,寫單元測試的時候也很是容易mock數據層
複製代碼
長遠來看,利大於弊
物極必反,躍躍欲試前,思考如下幾個問題以引導完善組件的設計
超過三層以後可見組件的數據傳遞的過程就會變得越複雜
複製代碼
縮減組件依賴能夠提升組件的可複用度
較常見的一種狀況是:組件運行時對window對象添加resize監聽事件以實現組件響應視窗尺寸變化事件,這種需求的更好替代方案是:組件提供刷新方法,由父組件實現調用
次優的方案是,當組件destroy前清理恢復
複製代碼
須要考慮須要適用的不一樣場景,在組件接口設計時進行必要的兼容
接口設計符合規範和大衆習慣,儘可能讓別人用起來簡單易上手,易上手是指更符合直覺。
各組件以前以組合的關係互相配合,也是對功能需求的模塊化抽象,當需求變化時能夠將實現以模塊粒度進行調整
上文提到的各類準則僅僅描述了一種開發理念,也能夠認爲是一種開發規範,假若你承認這規範,對它的分治策略產生了共鳴,那咱們就能夠繼續聊聊它的具體實現了
問本身一個問題
你心中的相對完美的組件是什麼樣子的?
明確你的組件劃分依據,目前是兩種
這是最容易想到的方法,當一個組件渲染了不少元素,就須要嘗試分離這些組件的渲染邏輯 咱們以掘金頁面爲例
大致上看,能夠分爲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>
複製代碼
更具表明性的示例圖
在業務邏輯層處理
首先要明確一點,這些差別並非組件自己形成的,是你本身的業務邏輯形成的,因此容器組件(父組件)應該爲此買單
複製代碼
結合組件自己和業務上下文將差別合理的消除在內部
好比part3中,其餘的part只有一個相似更多>>的link,可是它卻有多個(一居,二居...)
這裏我推薦將這種差別體如今組件內部,設計方法也不少:
好比能夠將link數組化爲links;
好比能夠將更多>>看做是一個default的link,而多餘的部分則是用戶自定義的特殊link,這二者合併組成了links。用戶自定義的默認是沒有的,須要引用組件時進行傳入。
複製代碼
組件設計初期,就應該擁有不耦合業務的名字
一個通用的或者說將來可能通用的,要有相對合理的命名,好比 Search,List,儘可能不要出現與業務耦合過深的業務名詞,通用組件與業務無關,只與自身抽象的組件有關
咱們在設計組件初期,就應該有這種思想,等到真正能夠抽出公用組件了,再去苦逼的名更名字?
庫一般都想讓廣大開發者用,咱們在設計組件時,能夠下降標準到先作到你的整個APP中通用
複製代碼
組件設計規則明明白白寫着咱們要遵循單一職責原則,這也帶來了上文聊過的過分抽象(組件化)的問題,咱們結合具體的業務聊一下
要實現徽章組件,它有兩部分組成
二者都是符合單一職責的,能夠將其抽離成一個獨立組件,可是一般不要這麼作
由於同一個app的風格必將是統一的,除此以外沒別的應用場景了,就像上文所說的,抽離組件以前,多問本身爲何以及投入/產出比,沒有絕對的規則
複製代碼
單一職責組件要創建在可複用的基礎上,對於不可複用的單⼀職責組件咱們僅僅做爲獨立組件的內部組件便可
思考,若是讓你實現你會如何設計... 我當初是這麼設計的
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)
這個示例幾乎涵蓋了全部的規則
<p className="brands-title hot-brands-title">熱門品牌</p> 只有一行,直接寫就完了
複製代碼
組件的形態(UI)永遠是變幻無窮的,可是其行爲(邏輯)是固定的,所以通用組件的祕訣之⼀就是將DOM 結構的控制權交給開發者,組件只負責⾏爲和最基本的DOM結構
這是一個顯眼的栗子
某一天,你接到這樣兒的需求
開心,簡單,三下五除二寫完了
忽然有一天又有這樣兒的需求
emm..可定製?以前的select無法用了,怎麼作?要修改上一個或者再寫一個嗎? 一旦出現了這種狀況,證實以前的組件須要從新設計了
實現通用性設計的關鍵一點是放棄對Dom的掌控
通用性設計在將Dom結構決定權交給開發者的同時指定默認值
這裏是一個新鮮出爐(vue)🌰
List組件
父組件🌰(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)
忍不住放上磐石業務的反面例子
難用無非是兩方面的問題
全部的業務邏輯與場景都包含在組件內部,外界只經過變量來控制,初衷是好的,可是隨着業務發展,組件愈來愈龐大,開發者也愈來愈力不從心了
恰好現階段UI改版,咱們的工做量就由只改樣式直接轉化爲推倒重來了,又沒有詳細的文檔,工做量瞬間翻了N倍😭寶寶內心苦寶寶不說
其實一開始,我並無專門去套用設計模式,徹底是業務驅使 你必定見到過這樣兒的
一旦這樣兒的邏輯多了,那是否是就跟業務耦合了,跟業務耦合多了,那組件天然沒有什麼通用性了,即便咱們不考慮到通用性,那寫的累吧?
考慮下這樣寫會不會好一點
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秒搭好架子,像但不限於下面這種
1. Form:表單 通常會被add.vue(編輯) 和edit.vue(詳情)引用
2. List:列表
3. Search: 搜索組件
4. 其餘業務中有但卻沒看到的基本上都已經抽離到common了 好比麪包屑導航,收起展開功能等
複製代碼
採購模塊結構圖
form
edit
不管有多少種狀態,只在edit這層容器維護
最後說一句
組件化沒有終點,day day up