聊一聊前端組件化
這里我們一起來學(xué)習(xí)前端組件化的知識(shí),而組件化在前端架構(gòu)里面是最重要的一個(gè)部分。
講到前端架構(gòu),其實(shí)前端架構(gòu)中最熱門的就有兩個(gè)話題,一個(gè)就是組件化,另一個(gè)就是架構(gòu)模式。組件化的概念是從開始研究如何擴(kuò)展 HTML 標(biāo)簽開始的,最后延伸出來的一套前端架構(gòu)體系。而它最重要的作用就是提高前端代碼的復(fù)用性。
架構(gòu)模式就是大家特別熟悉的 MVC, MVVM 等設(shè)計(jì)模式,這個(gè)話題主要關(guān)心的就是前端跟數(shù)據(jù)邏輯層之間的交互。
所以說,前端架構(gòu)當(dāng)中,組件化可以說是重中之重。在實(shí)際工程當(dāng)中,其實(shí)組件化往往會(huì)比架構(gòu)模式要更重要一些。因?yàn)榻M件化直接決定了一個(gè)前端團(tuán)隊(duì)代碼的復(fù)用率,而一個(gè)好的組件化體系是可以幫助一個(gè)前端團(tuán)隊(duì)提升他們代碼的復(fù)用率,從而也提升了團(tuán)隊(duì)的整體效率。
因?yàn)閺?fù)用率提高了,大家重復(fù)編寫的代碼量就會(huì)降低,效率就會(huì)提高,從而團(tuán)隊(duì)中的成員的心理和心智負(fù)擔(dān)就會(huì)少很多。
!! 所以學(xué)習(xí)組件化可以是說是非常重要的
這里我們先從了解什么是組件化和一個(gè)組件的基本組成部分開始。
組件的基本概念
組件都會(huì)區(qū)分為模塊和對(duì)象,組件是與 UI 強(qiáng)相關(guān)的,所以某種意義上我們可以認(rèn)為組件是特殊的模塊或者是特殊的對(duì)象。
!! 組件化既是對(duì)象也是模塊
組件化的特點(diǎn)是可以使用樹形結(jié)構(gòu)來進(jìn)行組合,并且有一定的模版化的配置能力。這個(gè)就是我們組件的一個(gè)基本概念。
對(duì)象與組件的區(qū)別
首先我們來看對(duì)象,它有三大要素:
- 屬性 —— Properties
- 方法 —— Methods
- 繼承關(guān)系 —— Inherit
在 JavaScript 中的普通對(duì)象可以用它的屬性,方法和繼承關(guān)系來描述。而這里面的繼承,在 JavaScript 中是使用原型繼承的。
這里說的 “普通對(duì)象” 不包含復(fù)雜的函數(shù)對(duì)象或者是其他的特殊對(duì)象,而在 JavaScript 當(dāng)中,屬性和方法是一體的。
相對(duì)比組件,組件里面包含的語義要素會(huì)更豐富一點(diǎn),組件中的要素有:
- 屬性 —— Properties
- 方法 —— Methods
- 繼承 —— Inherit
- 特性 —— Attribute
- 配置與狀態(tài) —— Config & State
- 事件 —— Event
- 生命周期 —— Lifecycle
- 子組件 —— Children
Properties 和 Attribute 在英語的含義中是有很大的區(qū)別的,但是往往都會(huì)翻譯成 “屬性”。如果遇到兩個(gè)單詞都出現(xiàn)的時(shí)候,就會(huì)把 Attribute 翻譯為 “特性”,把 Properties 翻譯成 “屬性”。這兩個(gè)要素要怎么區(qū)分呢?這里在文章的后面會(huì)和大家一起詳細(xì)了解。
接下來就是組件的 Config,它就是對(duì)組件的一種配置。我們經(jīng)常會(huì)在一個(gè)構(gòu)造函數(shù)創(chuàng)建一個(gè)對(duì)象的時(shí)候用到 Config ,我們傳入這個(gè)構(gòu)造函數(shù)的參數(shù)就叫 “ Config”(配置)。
同時(shí)組件也會(huì)有 state(狀態(tài))。當(dāng)用戶去操作或者是一些方法被調(diào)用的時(shí)候,一個(gè) state 就會(huì)發(fā)生變化。這種就是組件的狀態(tài),是會(huì)隨著一些行為而改變的。而 state 和 properties、attributes、config 都有可能是相識(shí)或者相同的。
event 就是 “事件” 的意識(shí),而一個(gè)事件是組件往外傳遞的。我們的組件主要是用來描述 UI 這樣的東西,基本上它都會(huì)有這種事件來實(shí)現(xiàn)它的某種類型的交互。
每一個(gè)組件都會(huì)有生命周期 lifecycle,這個(gè)一會(huì)兒在文章的后面會(huì)詳細(xì)的展開學(xué)習(xí)。
組件的 children 是非常重要的一部分,children 也是組件當(dāng)中一個(gè)必要的條件,因?yàn)闆]有 children 組件就不可能形成樹形結(jié)構(gòu),那么描述界面的能力就會(huì)差很多。
之前有一些比較流行的拖拽系統(tǒng),我們可以把一些寫好的 UI 組件拖到頁面上,從而建立我們的系統(tǒng)界面。但是后面發(fā)現(xiàn)除了可以拖拽在某些區(qū)域之外,還需要一些自動(dòng)排序,組件嵌套組件的功能需求。這個(gè)時(shí)候組件與組件之間沒有樹形結(jié)構(gòu)就不好使了。
最后組件在對(duì)象的基礎(chǔ)上添加了很多語義相關(guān)的概念,也是這樣使得組件變成了一種非常適合描述 UI 的概念。
組件 Component
我們用一張圖來更深入的了解組件。
組件最直接產(chǎn)生變化的來源就是用戶的輸入和操作,比如說當(dāng)一個(gè)用戶在我們的選擇框組件中選中了一個(gè)選項(xiàng)時(shí),這個(gè)時(shí)候我們的狀態(tài) state,甚至是我們的子組件 children 都會(huì)發(fā)生變化。
圖中右邊的這幾種情況就是組件的開發(fā)者與組件的關(guān)系。其中一種就是開發(fā)者使用了組件的標(biāo)記代碼 Markup Code,來對(duì)組件產(chǎn)生影響。其實(shí),也就是開發(fā)者通過組件特性 Attribute 來更改組件的一些特征或者是特性。
!! Attribute 是一種聲明型的語言,也是標(biāo)記型代碼 Markup Code。而 Markup Code 也不一定是我們的 HTML 這種 XML 類的語言。在標(biāo)記語言的大生態(tài)中,其實(shí)有非常多的語言可以用來描述一個(gè)界面的結(jié)構(gòu)。但是最主流的就是基于 XML 體系的。在我們 Web 領(lǐng)域里面最常見的就是 XML 。而 JSX 也可以理解為一種嵌入在編程語言里面的 XML 結(jié)構(gòu)。
開發(fā)者除了可以用 Attribute,也可以用 Property 來影響組件。這個(gè)組件本身是有 Property (屬性)的,當(dāng)開發(fā)者去修改一個(gè)組件的屬性時(shí),這個(gè)組件就會(huì)發(fā)生變化。而這個(gè)就是與對(duì)象中的 屬性 Property 是一樣的概念。
!! Attribute 和 Property 是不是一樣的呢?有的時(shí)候是,有的時(shí)候也不是,這個(gè)完全取決于組件體系的設(shè)計(jì)者。組件的實(shí)現(xiàn)者或者是設(shè)計(jì)者可以讓 attribute 和 property 統(tǒng)一。甚至我們把 state、config、attribute、property 四者都全部統(tǒng)一也是可以的。
然后就是 方法 method,它是用于描述一個(gè)復(fù)雜的過程,但是在 JavaScript 當(dāng)中的 Property 是允許有 get 和 set 這樣的方法的,所以最終 method 和 property 兩者的作用也是差不多的。
那么這里我們可以確定一個(gè)概念,使用組件的開發(fā)者會(huì)使用到 method 和 property,這些組件的要素。但是如果一個(gè)開發(fā)組件的開發(fā)者需要傳遞一個(gè)消息給到使用組件的程序員,這個(gè)時(shí)候就需要用到 事件 event。當(dāng)一個(gè)組件內(nèi)部因?yàn)槟撤N行為或者事件觸發(fā)到了變化時(shí),組件就會(huì)給使用者發(fā)送 event 消息。所以這里的 event 的方向就是反過來的,從組件往外傳輸?shù)摹?/p>
通過這張圖我們就可以清楚知道組件的各個(gè)要素的作用,以及他們的信息流轉(zhuǎn)方向。
特性 Attribute
在所有組件的要素中,最復(fù)雜的無非就是 Attribute 和 Property。
我們從 Attribute 這個(gè)英文單詞的理解上,更多是在強(qiáng)調(diào)描述性。比如說我們描述一個(gè)人,頭發(fā)很多、長相很帥、皮膚很白,這些都是屬于 Attribute,也可以說是某一樣?xùn)|西的特性和特征方面的描述。
而 Property 更多的是一種從屬關(guān)系。比如,我們?cè)陂_發(fā)中經(jīng)常會(huì)發(fā)現(xiàn)一個(gè)對(duì)象,它有一個(gè) Property 是另外一個(gè)對(duì)象,那么大概率它們之間是有一個(gè)從屬關(guān)系的,子對(duì)象是從屬于父對(duì)象。但是這里也有一種特殊情況,如果我們是弱引用的話,一個(gè)對(duì)象引用了另外一個(gè)對(duì)象,這樣就是完全是另一個(gè)概念了。
上面講的就是這兩個(gè)詞在英文中的區(qū)別,但是在實(shí)際運(yùn)用場景里面他們也是有區(qū)別的。
因?yàn)?Property 是從屬關(guān)系的,所以經(jīng)常會(huì)在我們面向?qū)ο罄锩媸褂?。?Attribute 最初就是在我們 XML 里面中使用。它們有些時(shí)候是相同的,有些時(shí)候又是不同的。
Attribute 對(duì)比 Property
這里我們用一些例子來看看 Attribute 和 Property 的區(qū)別。我們可以看看它們?cè)?HTML 當(dāng)中不等效的場景。
Attribute:
- <my-component attribute="v" />
- <script>
- myComponent.getAttribute('a')
- myComponent.setAttribute('a', value)
- </script>
- HTML 中的 Attribute 是可以通過 HTML 屬性去設(shè)置的
- 同時(shí)也可以通過 JavaScript 去設(shè)置的
Property:
- myComponent.a = 'value';
這里就是定義某一個(gè)元素的 a = ‘value’
這個(gè)就不是 attribute 了,而是 property
!! 很多同學(xué)都認(rèn)為這只是兩種不同的寫法,其實(shí)它們的行為是有區(qū)別的。
Class 屬性
- <div class="class1 class2"></div>
- <script>
- var div = document.getElementByTagName('div');
- div.className // 輸出就是 class1 class2
- </script>
早年 JavaScript 的 Class 是一個(gè)關(guān)鍵字,所以早期 class 作為關(guān)鍵詞是不允許做為屬性名的。但是現(xiàn)在這個(gè)已經(jīng)被改過來了,關(guān)鍵字也是可以做屬性名的。
為了讓這個(gè)關(guān)鍵字可以這么用,HTML 里面就做了一個(gè)妥協(xié)的設(shè)計(jì)。在 HTML 中屬性仍然叫做 class 但是在 DOM 對(duì)象中的 property 就變成了 className。但是兩者還是一個(gè)互相反射的關(guān)系的,這個(gè)神奇的關(guān)系會(huì)經(jīng)常讓大家掉一些坑里面。
比如說在 React 里面,我們寫 className它自動(dòng)就把 Class 給設(shè)置了。
Style 屬性
現(xiàn)在 JavaScript 語言中,已經(jīng)沒有 class 和 className 兩者不一致的問題了。我們是可以使用 div.class 這樣的寫法的。但是 HTML 中就還是不支持 class 這個(gè)名字的,這個(gè)也就是一些歷史包袱導(dǎo)致的問題。
有些時(shí)候 Attribute 是一個(gè)字符串,而在 Property 中就是一個(gè)字符串語義化之后的對(duì)象。最典型的就是 Style 。
- <div class="class1 class2" style="color:blue"></div>
- <script>
- var div = document.getElementByTagName('div');
- div.style // 這里就是一個(gè)對(duì)象
- </script>
在 HTML 里面的 Style 屬性他是一個(gè)字符串,同時(shí)我們可以使用 getAttribute 和 setAttribute 去取得和設(shè)置這個(gè)屬性。但是如果我們用這個(gè) Style 屬性,我們就會(huì)得到一個(gè) key 和 vaule 的結(jié)構(gòu)。
Href 屬性
在 HTML 中 href 的 attribute 和 property 的意思就是非常相似的。但是它的 property 是經(jīng)過 resolve 過的 url。
比如我們的 href 的值輸入的是 "//m.taobao.com"。這個(gè)時(shí)候前面的 http 或者是 https 協(xié)議是根據(jù)當(dāng)前的頁面做的,所以這里的 href 就需要編譯一遍才能響應(yīng)當(dāng)前頁面的協(xié)議。
做過 http 到 https 改造的同學(xué)應(yīng)該都知道,在讓我們的網(wǎng)站使用 https 協(xié)議的時(shí)候,我們需要把所有寫死的 http 或者 https 的 url 都要改成使用 // 。
所以在我們 href 里面寫了什么就出來什么的,就是 attribute。如果是經(jīng)過 resolve 的就是我們的 property 了。
- <a href="//m.taobao.com"></a>
- <script>
- var a = document.getElementByTagName('a');
- // 這個(gè)獲得的結(jié)果就是 "http://m.taobao.com", 這個(gè) url 是 resolve 過的結(jié)果
- // 所以這個(gè)是 Property
- a.href;
- // 而這個(gè)獲得的是 "//m.taobao.com", 跟 HTML 代碼中完全一致
- // 所以這個(gè)是 Attribute
- a.getAttribute('href');
- </script>
在上面的代碼中我們也可以看到,我們可以同時(shí)訪問 property 和 attribute。它們的語義雖然非常的接近,但是它們不是一樣的東西。
不過如果我們更改了任何一方,都會(huì)讓另外一方發(fā)生改變。這個(gè)是需要我們?nèi)プ⒁獾默F(xiàn)象。
Input 和 value
這個(gè)是最神奇的一對(duì),而 value 也是特別的坑。
我們很多都以為 property 和 attribute 中的 value 都是完全等效的。其實(shí)不是的,這個(gè) attribute 中的 input 的 value 相當(dāng)于一個(gè) value 的默認(rèn)值。不論是用戶在 input 中輸入了值,還是開發(fā)者使用 JavaScript 對(duì) input 的 value 進(jìn)行賦值,這個(gè) input 的 attribute 是不會(huì)跟著變的。
而在 input 的顯示上是會(huì)優(yōu)先顯示 property,所以 attribute 中的 value 值就相當(dāng)于一個(gè)默認(rèn)值而已。這就是一個(gè)非常著名的坑,早期同學(xué)們有使用過 JQuery 的話,我們會(huì)覺得里面的 prop 和 attr 是一樣的,沒想到在 value 這里就會(huì)踩坑。
所以后來 JQuery 庫就出了一個(gè)叫 val 的方法,這樣我們就不需要去想 attribute 還是 property 的 value,直接用它提供的 val 取值即可。
!! 這里一方面是一起增強(qiáng)一下 HTML 的 property 和 attribute 的知識(shí)。另一方面就是讓我們認(rèn)識(shí)到,就算是非常頂級(jí)的計(jì)算機(jī)專家設(shè)計(jì)的標(biāo)簽系統(tǒng),也出現(xiàn)兩個(gè)差不多的屬性不等效的問題。那么如果讓我們?nèi)ピO(shè)計(jì)一個(gè)標(biāo)簽系統(tǒng),我們會(huì)讓 property 和 attribute 等效還是不等效呢? 等學(xué)習(xí)完整個(gè)組件化的知識(shí)后,我們一起來回答一下這個(gè)問題。
如何設(shè)計(jì)組件狀態(tài)
這里我們來分析一下,property、attribute、state、config 在組件設(shè)計(jì)中都有什么區(qū)別。
這里 Winer 老師給我們整理了一個(gè)表格,分成了四個(gè)場景:
- Markup set —— 用標(biāo)簽去設(shè)置
- JavaScript Set —— 使用 JavaScript 代碼去設(shè)置
- JavaScript Change —— 使用 JavaScript 代碼去改變
- User Input Change —— 終端用戶的輸入而改變
Markup set | JavaScript set | JavaSscript Change | User Input Change | |
---|---|---|---|---|
❌ | ✅ | ✅ | ❓ | property |
✅ | ✅ | ✅ | ❓ | attribute |
❌ | ❌ | ❌ | ✅ | state |
❌ | ✅ | ❌ | ❌ | config |
那么我們一個(gè)一個(gè)來講述一下:
- Property
- 它是不能夠被 markup 這種靜態(tài)的聲明語言去設(shè)置的
- 但是它是可以被 JavaScript 設(shè)置和改變的
- 大部分情況下 property 是不應(yīng)該由用戶的輸入去改變的,但是小數(shù)情況下,可能是來源于我們的業(yè)務(wù)邏輯,才有可能會(huì)接受用戶輸入的改變
Attribute
- 用戶的輸入就不一定會(huì)改變它,與 Property 同理
- 是可以由 markup,JavaScript 去設(shè)置的,同時(shí)也是可以被 JavaScript 所改變的
State
- 狀態(tài)是會(huì)由組件內(nèi)部去改變的,它不會(huì)從組件的外部進(jìn)行改變。如果我們想設(shè)計(jì)一個(gè)組件是從外部去改變組件的狀態(tài)的話,那么我們組件內(nèi)部的 state 就失控了。因?yàn)槲覀儾恢澜M件外部什么時(shí)候會(huì)改變我們組件的 state,導(dǎo)致我們 state 的一致性無法保證。
- 但是作為一個(gè)組件的設(shè)計(jì)者和實(shí)踐者,我們一定要保證用戶輸入是能改變我們組件的 state 的。比如說用戶點(diǎn)擊了一個(gè) tab,然后點(diǎn)中的 tab 就會(huì)被激活,這種交互一般都會(huì)用 state 去控制的。
Config
- Config 在組件中是一個(gè)一次性生效的東西,它只會(huì)在我們組件構(gòu)造的時(shí)候觸發(fā)。所以它是不可更改的。也是因?yàn)樗牟豢筛男?,所以我們通常?huì)把 config 留給全局。通常每個(gè)頁面都會(huì)有一份 config,然后拿著這個(gè)在頁面內(nèi)去使用。
組件生命周期 Lifecycle
講到生命周期,我們最容易想到的會(huì)有兩個(gè),一個(gè)是 created 一個(gè)是 destroy。世界萬物的生命必定會(huì)有 出生 和 死亡,這兩個(gè)生命周期。
那么在這兩個(gè)開始與結(jié)束之間有什么生命周期呢?我們就需要想一下,一個(gè)組件在構(gòu)造到銷毀之間都會(huì)發(fā)生什么事情。
一個(gè)組件有一個(gè)非常重要的事情,就是它被創(chuàng)建之后,它有沒有被顯示出來。這里就涉及生命周期中的 mount,也就是組件有沒有被掛載到 “屏幕的這棵樹上”。這個(gè)生命周期我們可以在 React 和 Vue 里面看到,我們經(jīng)常會(huì)使用這個(gè)生命周期,在組件被掛載后做一些相應(yīng)的初始化操作。
有掛載那必然就會(huì)有卸載,所以組件中的 mount 和 unmount 是一組生命周期。而這個(gè)掛載與卸載的整個(gè)生命周期是可以反復(fù)的發(fā)生的,我們可以掛上去然后卸下來,然后再掛上去,這樣反復(fù)又反復(fù)的走這個(gè)生命周期。
所以在 unmount 之后,我們是可以回到 created 構(gòu)建組件的這個(gè)生命周期的狀態(tài)。
那么組件還會(huì)在什么時(shí)候發(fā)生狀態(tài)更變呢?這里我們就有兩種情況:
- 程序員使用代碼去改變或者設(shè)置這個(gè)組件的狀態(tài)
- 用戶輸入時(shí)影響了組件的狀態(tài)
比如說我們用戶點(diǎn)了一下按鈕或者 Tab,這個(gè)時(shí)候就會(huì)觸發(fā)這個(gè)組件的狀態(tài)更變。同時(shí)也會(huì)產(chǎn)生一個(gè)組件的生命周期,而這個(gè)生命周期就是 Render 渲染或者 Update 更新。
所有這些生命周期加在一起就是我們一個(gè)組件完整的生命周期。我們看到的所謂 willMount、didMount 無非就是這個(gè)生命周期之中更細(xì)節(jié)的位置。下面我給大家附上一張完整的生命周期的圖。
Children
最后我們來講一下 Children(子組件)的概念。Children 是構(gòu)建組件樹最重要的一個(gè)組件特性,并且在使用中其實(shí)有兩種類型的 Children:
- Content 型 Children —— 我們有幾個(gè) Children,但是最終就能顯示出來幾個(gè) Children。這種類型的 Children,它的組件樹是非常簡單的。
- Template 型 Children —— 這個(gè)時(shí)候整個(gè) Children 它充當(dāng)了一個(gè)模版的作用。比如說我們?cè)O(shè)計(jì)一個(gè) list,但是最后的結(jié)果不一定就與我們 Children 代碼中寫的一致。因?yàn)槲覀?List 肯定是用于多個(gè)列表數(shù)據(jù)的,所以 list 的表示數(shù)量是與我們傳入組件的 data 數(shù)據(jù)所相關(guān)的。如果我們有 100 個(gè)實(shí)際的 children 時(shí),我們的 list 模版就會(huì)被復(fù)制 100 份。
在設(shè)計(jì)我們的組件樹的 children 的時(shí)候,一定要考慮到這兩種不同的場景。比如我們?cè)?React中,它沒有 template 型的 children,但是它的 children 可以傳函數(shù),然后這個(gè)函數(shù)可以返回一個(gè) children。這個(gè)時(shí)候它就充當(dāng)了一個(gè)模版型 children 的作用了。那么在 Vue 里面當(dāng)我們?nèi)プ鲆恍o盡的滾動(dòng)列表的時(shí)候,這個(gè)對(duì) Vue 的模版型 children 就有一定的要求。
結(jié)束語
這里我們就學(xué)習(xí)完了整個(gè)組件的概念和知識(shí)了,下一篇文章我們就會(huì)一起來設(shè)計(jì)和搭建一個(gè)組件系統(tǒng),并且了解到它的各方各面的實(shí)踐知識(shí)。我們還會(huì)用一些典型的組件和典型的功能來讓大家對(duì)組件的實(shí)現(xiàn)有一定的了解。
本文轉(zhuǎn)載自微信公眾號(hào)「技術(shù)銀河」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系技術(shù)銀河公眾號(hào)。