前端元件化開發,已經有多年的歷史了,不管是服務端渲染,還是前端SPA,都有了比較成熟的元件化開發的方案。
隨著元件化開發的普及,前端社群中貢獻了很多不錯的前端元件,都提供開箱即用的方案,使得更好的發揮元件化的優勢。
前端團隊內,如果有人對前端元件化的理解不夠深入,就不能開發出好的元件,會給專案的維護帶來更大的成本。
閱讀全文約 8 分鐘。
作者:zollero
本文首發於:
知乎專欄 前端微志
微信公眾號 前端微志
這幾年,從陷入 “React、Vue 和 Angular 哪個效能好?”的爭論,到現在的各個框架(庫)的生態越來越完善,討論效能差距已經沒有價值了。而國內的前端娛樂圈,最火的就是 React 和 Vue 了,而 Angular 由於歷史原因,在國內的佔有率確實不高。
隨著前端生態 jade、less、scss、typeScript 和 webpack 等工具的完善,前端的元件化開發效率已經有了很大的提升。
特別是像 Ant Design、Element UI、iView 這些優秀的前端元件庫的流行,更是將元件化開發發揮到了極致。開發一個前端頁面已經變得非常的高效,特別是在做管理系統的頁面,腳手架搭建、新增依賴包、配置路由、建立頁面、引入元件,很快的就可以構建一個系統。
如果你需要 SEO,React 和 Vue 的 SSR 框架 Next.js 和 Nuxt.js 更是提供了開箱即用的整合方案,也使開發“同構頁面系統“(Google It)變得更加簡單。
下面切入正題,深入探討下前端元件。
什麼是前端元件化開發
首先,我們要搞明白什麼是前端元件化開發?
你應該遇到過,將一個頁面的幾百行,甚至上千行的程式碼邏輯寫在一個 js 檔案中的情況。通常這種程式碼都很難讀下去,更別說要維護起來,新增新功能,移除一些老功能了,因為你不知道改動一個地方,會不會出現意想不到的 bug。
這個時候,你就需要利用元件化開發,拆分功能,封裝元件,單獨維護。
現代化前端框架通常都是實現 MVVM 的方案,資料層(M)和 檢視層(V)相互連線,同時變更,使得頁面互動保持高度的一致性。
如果你熟悉 Java,Python,Go 等後端開發語言,你應該對 package (包)的概念很熟悉,前端的元件化在概念上與後端的 package 很相似,只不過前端的元件涉及到更多的是展示和互動方面的邏輯。當然,前端元件與後端架構的微服務概念類似,可以理解成一個元件就是一個服務元件,只提供某個服務。
前端元件化開發,就是將頁面的某一部分獨立出來,將這一部分的 資料層(M)、檢視層(V)和 控制層(C)用黑盒的形式全部封裝到一個元件內,暴露出一些開箱即用的函式和屬性供外部元件呼叫。
一個前端元件,包含了 HTML、CSS、JavaScript,包含了元件的模板、樣式和互動等內容,基本上涵蓋了元件的所有的內容,外部只要按照元件設定的屬性、函式及事件處理等進行呼叫即可,完全不用考慮元件的內部實現邏輯,對外部來說,元件是一個完全的黑盒。
元件可以多層封裝,通過呼叫多個小元件,最後封裝成一個大元件,供外部呼叫。比如:一個 Input 輸入框 是一個元件,一個 Select下拉選擇框 也是一個元件,可以用 form 在這兩個元件上包裝一層,就是一個 Form 的元件。
有一些比較常用的前端元件,像 vue-router,vuex,react-router,redux,mobx 等,都是基於 Vue 和 React 的元件,它們只專注於 路由、狀態儲存 的工作,並且把這些事情做好。
只要利用好元件化開發,開發一個頁面,就像是搭積木一樣,將各個元件拼接到一起,最後融合到一起,就是一個完整的系統。
元件化開發的優點
說到底,前端的元件化開發,可以很大程度上降低系統各個功能的耦合性,並且提高了功能內部的聚合性。這對前端工程化及降低程式碼的維護來說,是有很大的好處的。
耦合性的降低,提高了系統的伸展性,降低了開發的複雜度,提升開發效率,降低開發成本。
元件封裝的好,加班也少了,bug 也少了,就有更多時間喝喝咖啡、打打農藥了。:)
怎麼設計一個元件
既然前端元件化開發這麼好,那要怎麼設計一個好的元件呢?
經過多次實踐,總結了一些元件設計時的要點。
專一
要想設計一個好的元件,元件也需要專一。
設計元件要遵循一個原則:一個元件只專注做一件事,且把這件事做好。
一個功能如果可以拆分成多個功能點,那就可以將每個功能點封裝成一個元件,當然也不是元件的顆粒度越小越好,只要將一個元件內的功能和邏輯控制在一個可控的範圍內即可。
舉個例子。頁面上有一個 Table 列表和一個分頁控制元件,就可以將 Table 封裝為一個元件,分頁控制元件 封裝成一個元件,最後再把 Table元件 和 分頁元件 封裝成一個元件。Table 元件還可以再拆分成多個 table-column 元件,及展示邏輯等。
可配置性
一個元件,要明確它的輸入和輸出分別是什麼。
元件除了要展示預設的內容,還需要做一些動態的適配,比如:一個元件內有一段文字,一個圖片和一個按鈕。那麼字型的顏色、圖片的規則、按鈕的位置、按鈕點選事件的處理邏輯等,都是可以做成可配置的。
要做可配置性,最基本的方式是通過屬性向元件傳遞配置的值,而在元件初始化的宣告週期內,通過讀取屬性的值做出對應的顯示修改。還有一些方法,通過呼叫元件暴露出來的函式,向函式傳遞有效的值;修改全域性 CSS樣式;向元件傳遞特定事件,並在元件內監聽該事件來執行函式等。
在做可配置性時,為了讓元件更加健壯,保證元件接收到的是有效的屬性、函式接收到的是有效的引數,需要做一些校驗。
一. 屬性的值的校驗
對屬性的值進行校驗,一般要考慮以下幾個方面。
1.屬性值的型別是否是有效的。如果某個屬性要求傳遞一個陣列,那麼傳遞過來的值不是陣列時,就要丟擲異常,並給出對應的提示。
2.屬性是否是必填的。有的屬性的值,是元件內不可缺少的時,就要是必填的,在元件初始化時要做是否傳遞的檢查,如果沒有傳遞,則需要丟擲異常,並給出相應的提示。如果屬性不是必填的,可以設定一個預設值,當屬性沒有被設定時,就使用預設值。
得益於 React、Vue 內部實現的屬性檢查,且這些屬性檢查會在元件初始化階段預設執行,你可以很容易的給元件設定屬性的檢查。React 中可以使用 React.PropTypes 進行型別檢查設定,Vue 中只需要給元件設定 props 即可。
在 React 中進行型別檢查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// title.jsx (Title元件) import React, { Component, PropTypes } from 'react'; export default class Title extends Component { constructor(props) { super(props); } static propTypes = { title: PropTypes.string.isRequired } render() { const { title } = this.props; return ( <p>{ title }</p> ) } } |
在 Vue 中進行型別檢查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// title.vue (Title元件) <template> <p>{{ title }}</p> </template> <script> export default { props: { title: { type: String, required: true } } } </script> |
二. 函式的引數的校驗
函式的引數校驗,只要按照傳統的方法進行校驗即可。在函式內部頂部判斷引數的值和型別,如果不滿足要求,則丟擲異常,並給出相應的提示。
判斷一個函式的第一個必填,且為 String 格式
1 2 3 4 5 6 7 8 9 10 11 |
// ES6 語法 changeTitle(title) { if (typeof title !== 'string') { throw new Error('必須傳入一個 title,才能修改 title。') } // 滿足條件,可以進行修改 this.title = title // vue 語法 this.setState({ // react 語法,修改state的值 title }) } |
生命週期
一個元件,需要明確知道在生命週期的不同階段做該做的事。
初始化階段,讀取屬性的值,如果需要做資料和邏輯處理的話,在這個階段進行。
屬性值變化時,如果屬性發生變化,且需要對變化後的資料進行處理的話,在這個階段進行處理。
元件銷燬階段,如果元件已經建立了一些可能會對系統產生一些副作用的東西,可以在這個階段進行清除。比如 timeInterval、timeout 等。
如果元件在渲染的時候報錯,需要展示錯誤資訊。React v16 中提供了 componentDidCatch 生命週期函式,Vue v2.5 中提供了 errorCaptured 的鉤子函式。
React 中提供了一些生命週期函式:componentWillMount,componentDidMount,componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,componentDidUpdate,render,componentWillUnmount,componentDidCatch(React v16)。
Vue 中提供了一些生命週期函式:beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed,errorCapture,errorCaptured(Vue v2.5)。
每個生命週期的具體用法,請參考官方詳細文件。
事件傳遞
Vue 中傳遞事件很簡單,只需要在子元件內使用 this.$emit(‘event1’) 即可向外傳遞一個事件 event1,在父元件呼叫該子元件時,只需要監聽 event1 事件,並給出對應事件處理邏輯即可。
Vue中事件傳遞
Vue子元件定義 child-component.vue
1 2 3 4 5 6 7 8 9 10 |
Vue.component('child-component', { methods: { emitEvent() { this.$emit('event1', 'This is a event.') } }, mounted() { this.emitEvent() } }) |
Vue 父元件呼叫子元件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<template> <div> <child-component @event1="eventHandler" /> </div> </template> <script> import childComponent from './child-component' export default { components: { childComponent }, methods: { eventHandler(event) { console.log(event) } } } </script> |
而在 React 中,官方沒有給出元件間的事件傳遞解決方案,這也是 React 中比較坑的一點。不過,還是可以使用其他方式來實現。
React 中,父元件可以使用 props 向子元件傳值,而子元件向父元件傳值,需要在父元件內定義函式並通過屬性傳遞給子元件,在子元件內通過呼叫該屬性對應的函式,傳入引數,傳遞給父元件內的函式,並在父元件的該函式中做邏輯的處理。
React 子元件定義 child-component.js
1 2 3 4 5 6 7 8 9 10 11 |
class ChildComponent extends React.Components { render() { return ( <button onClick={ () => { this.props.clickHandler('This is a click') } }></button> ) } } |
React 父元件呼叫子元件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import ChildComponent from './child-component' class ParentComponent extends React.Components { clickHandler(message) { console.log(message) } render() { return ( <child-component clickHandler={ this.clickHandler.bind(this) } /> ) } } |
後記
前端元件化開發的實踐,是一個很長的過程,堅持並持續優化,帶動系統整體的優化。
後面會再更新文章,講講“怎麼開發一個 vue 第三方元件”。
後續還會寫更多關於 React 和 Vue 的文章,歡迎關注。