摘要:盡管現在的已不再那么流行,但的設計思想還是非常值得致敬和學習的,特別是的插件化。那么,如何解決我們回顧下的生命周期,父組件傳遞到子組件的的更新數據可以在中獲取。當然,如何設計取決于你自己的項目,正所謂沒有最好的,
作者:曉冬
本文原創,轉載請注明作者及出處
如今的 Web 前端已被 React、Vue 和 Angular 三分天下,一統江山十幾年的 jQuery 顯然已經很難滿足現在的開發模式。那么,為什么大家會覺得 jQuery “過時了”呢?一來,文章《No JQuery! 原生 JavaScript 操作 DOM》就直截了當的告訴你,現在用原生 JavaScript 可以非常方便的操作 DOM 了。其次,jQuery 的便利性是建立在有一個基礎 DOM 結構的前提下的,看上去是符合了樣式、行為和結構分離,但其實 DOM 結構和 JavaScript 的代碼邏輯是耦合的,你的開發思路會不斷的在 DOM 結構和 JavaScript 之間來回切換。
盡管現在的 jQuery 已不再那么流行,但 jQuery 的設計思想還是非常值得致敬和學習的,特別是 jQuery 的插件化。如果大家開發過 jQuery 插件的話,想必都會知道,一個插件要足夠靈活,需要有細顆粒度的參數化設計。一個靈活好用的 React 組件跟 jQuery 插件一樣,都離不開合理的屬性化(props)設計,但 React 組件的拆分和組合比起 jQuery 插件來說還是簡單的令人發指。
So! 接下來我們就以萬能的 TODO LIST 為例,一起來設計一款 React 的 TodoList 組件吧!
實現基本功能TODO LIST 的功能想必我們應該都比較了解,也就是 TODO 的添加、刪除、修改等等。本身的功能也比較簡單,為了避免示例的復雜度,顯示不同狀態 TODO LIST 的導航(全部、已完成、未完成)的功能我們就不展開了。
約定目錄結構先假設我們已經擁有一個可以運行 React 項目的腳手架(ha~ 因為我不是來教你如何搭建腳手架的),然后項目的源碼目錄 src/ 下可能是這樣的:
. ├── components ├── containers │ └── App │ ├── app.scss │ └── index.js ├── index.html └── index.js
我們先來簡單解釋下這個目錄設定。我們看到根目錄下的 index.js 文件是整個項目的入口模塊,入口模塊將會處理 DOM 的渲染和 React 組件的熱更新(react-hot-loader)等設置。然后,index.html 是頁面的 HTML 模版文件,這 2 個部分不是我們這次關心的重點,我們不再展開討論。
入口模塊 index.js 的代碼大概是這樣子的:
// import reset css, base css... import React from "react"; import ReactDom from "react-dom"; import { AppContainer } from "react-hot-loader"; import App from "containers/App"; const render = (Component) => { ReactDom.render(, document.getElementById("app") ); }; render(App); if (module.hot) { module.hot.accept("containers/App", () => { let nextApp = require("containers/App").default; render(nextApp); }); }
接下來看 containers/ 目錄,它將放置我們的頁面容器組件,業務邏輯、數據處理等會在這一層做處理,containers/App 將作為我們的頁面主容器組件。作為通用組件,我們將它們放置于 components/ 目錄下。
基本的目錄結構看起來已經完成,接下來我們實現下主容器組件 containers/App。
實現主容器我們先來看下主容器組件 containers/App/index.js 最初的代碼實現:
import React, { Component } from "react"; import styles from "./app.scss"; class App extends Component { constructor(props) { super(props); this.state = { todos: [] }; } render() { return (); } handleAdd() { ... } handleRemove(index) { ... } handleStateChange(index) { ... } } export default App;Todo List Demo
this.input = input} /> {this.state.todos.map((todo, i) => (
- this.handleStateChange(i)} > {todo.text}
))}
我們可以像上面這樣把所有的業務邏輯一股腦的塞進主容器中,但我們要考慮到主容器隨時會組裝其他的組件進來,將各種邏輯堆放在一起,到時候這個組件就會變得無比龐大,直到“無法收拾”。所以,我們得分離出一個獨立的 TodoList 組件。
分離組件 TodoList 組件在 components/ 目錄下,我們新建一個 TodoList 文件夾以及相關文件:
. ├── components +│ └── TodoList +│ ├── index.js +│ └── todo-list.scss ├── containers │ └── App │ ├── app.scss │ └── index.js ...
然后我們將 containers/App/index.js 下跟 TodoList 組件相關的功能抽離到 components/TodoList/index.js 中:
... import styles from "./todo-list.scss"; export default class TodoList extends Component { ... render() { return (-); } ... }+ this.input = input} /> -+ - +
{this.state.todos.map((todo, i) => (
- this.handleStateChange(i)} > {todo.text}
))}
有沒有注意到上面 render 方法中的 className,我們省去了 todo-list* 前綴,由于我們用的是 CSS MODULES,所以當我們分離組件后,原先在主容器中定義的 todo-list* 前綴的 className ,可以很容易通過 webpack 的配置來實現:
... module.exports = { ... module: { rules: [ { test: /.s?css/, use: [ "style-loader", { loader: "css-loader", options: { modules: true, localIdentName: "[name]--[local]-[hash:base64:5]" } }, ... ] } ] } ... };
我們再來看下該組件的代碼輸出后的結果:
...... ...
從上面 webpack 的配置和輸出的 HTML 中可以看到,className 的命名空間問題可以通過語義化 *.scss 文件名的方式來實現,比如 TodoList 的樣式文件 todo-list.scss。這樣一來,省去了我們定義組件 className 的命名空間帶來的煩惱,從而只需要從組件內部的結構下手。
回到正題,我們再來看下分離 TodoList 組件后的 containers/App/index.js:
import TodoList from "components/TodoList"; ... class App extends Component { render() { return (抽離通用組件); } } export default App;Todo List Demo
作為一個項目,當前的 TodoList 組件包含了太多的子元素,如:input、button 等。為了讓組件“一次編寫,隨處使用”的原則,我們可以進一步拆分 TodoList 組件以滿足其他組件的使用。
但是,如何拆分組件才是最合理的呢?我覺得這個問題沒有最好的答案,但我們可以從幾個方面進行思考:可封裝性、可重用性和靈活性。比如拿 h1 元素來講,你可以封裝成一個 Title 組件,然后這樣
好,我們先拿 input 和 button 下手,在 components/ 目錄下新建 2 個 Button 和 Input 組件:
. ├── components +│ ├── Button +│ │ ├── button.scss +│ │ └── index.js +│ ├── Input +│ │ ├── index.js +│ │ └── input.scss │ └── TodoList │ ├── index.js │ └── todo-list.scss ...
Button/index.js 的代碼:
... export default class Button extends Component { render() { const { className, children, onClick } = this.props; return ( ); } }
Input/index.js 的代碼:
... export default class Input extends Component { render() { const { className, value, inputRef } = this.props; return ( ); } }
由于這 2 個組件自身不涉及任何業務邏輯,應該屬于純渲染組件(木偶組件),我們可以使用 React 輕量的無狀態組件的方式來聲明:
... const Button = ({ className, children, onClick }) => ( );
是不是覺得酷炫很多!
另外,從 Input 組件的示例代碼中看到,我們使用了非受控組件,這里是為了降低示例代碼的復雜度而特意為之,大家可以根據自己的實際情況來決定是否需要設計成受控組件。一般情況下,如果不需要獲取實時輸入值的話,我覺得使用非受控組件應該夠用了。
我們再回到上面的 TodoList 組件,將之前分離的子組件 Button,Input 組裝進來。
... import Button from "components/Button"; import Input from "components/Input"; ... export default class TodoList extends Component { render() { return (拆分子組件); } } ...this.input = input} /> ...
然后繼續接著看 TodoList 的 items 部分,我們注意到這部分包含了較多的渲染邏輯在 render 中,導致我們需要浪費對這段代碼與上下文之間會有過多的思考,所以,我們何不把它抽離出去:
... export default class TodoList extends Component { render() { return (...); } renderItems() { return ({this.renderItems()}
上面的代碼看似降低了 render 的復雜度,但仍然沒有讓 TodoList 減少負擔。既然我們要把這部分邏輯分離出去,我們何不創建一個 Todos 組件,把這部分邏輯拆分出去呢?so,我們以“就近聲明”的原則在 components/TodoList/ 目錄下創建一個子目錄 components/TodoList/components/ 來存放 TodoList 的子組件 。why?因為我覺得 組件 Todos 跟 TodoList 有緊密的父子關系,且跟其他組件間也不太會有任何交互,也可以認為它是 TodoList 私有的。
然后我們預覽下現在的目錄結構:
. ├── components │ ... │ └── TodoList +│ ├── components +│ │ └── Todos +│ │ ├── index.js +│ │ └── todos.scss │ ├── index.js │ └── todo-list.scss
Todos/index.js 的代碼:
... const Todos = ({ data: todos, onStateChange, onRemove }) => (
再看拆分后的 TodoList/index.js :
render() { return (增強子組件...); }this.handleStateChange(index)} onRemove={(index) => this.handleRemove(index)} />
到目前為止,大體上的功能已經搞定,子組件看上去拆分的也算合理,這樣就可以很容易的增強某個子組件的功能了。就拿 Todos 來說,在新增了一個 TODO 后,假如我們并沒有完成這個 TODO,而我們又希望可以修改它的內容了。ha~不要著急,要不我們再拆分下這個 Todos,比如增加一個 Todo 組件:
. ├── components │ ... │ └── TodoList │ ├── components +│ │ ├── Todo +│ │ │ ├── index.js +│ │ │ └── todo.scss │ │ └── Todos │ │ ├── index.js │ │ └── todos.scss │ ├── index.js │ └── todo-list.scss
先看下 Todos 組件在抽離了 Todo 后的樣子:
... import Todo from "../Todo"; ... const Todos = ({ data: todos, onStateChange, onRemove }) => (
我們先不關心 Todo 內是何如實現的,就如我們上面說到的那樣,我們需要對這個 Todo 增加一個可編輯的功能,從單純的屬性配置入手,我們只需要給它增加一個 editable 的屬性:
onStateChange(i)} />
然后,我們再思考下,在 Todo 組件的內部,我們需要重新組織一些功能邏輯:
根據傳入的 editable 屬性來判斷是否需要顯示編輯按鈕
根據組件內部的編輯狀態,是顯示文本輸入框還是文本內容
點擊“更新”按鈕后,需要通知父組件更新數據列表
我們先來實現下 Todo 的第一個功能點:
render() {
const { completed, text, editable, onClick } = this.props;
return (
{text}
{editable &&
}
);
}
顯然實現這一步似乎沒什么 luan 用,我們還需要點擊 Edit 按鈕后能顯示 Input 組件,使內容可修改。所以,簡單的傳遞屬性似乎無法滿足該組件的功能,我們還需要一個內部狀態來管理組件是否處于編輯中:
render() {
const { completed, text, editable, onStateChange } = this.props,
{ editing } = this.state;
return (
{editing ?
this.input = input}
/> :
{text}
}
{editable &&
}
);
}
最后,Todo 組件在點擊 Update 按鈕后需要通知父組件更新數據:
handleEdit() { const { text, onUpdate } = this.props; let { editing } = this.state; editing = !editing; this.setState({ editing }); if (!editing && this.input.value !== text) { onUpdate(this.input.value); } }
需要注意的是,我們傳遞的是更新后的內容,在數據沒有任何變化的情況下通知父組件是毫無意義的。
我們再回過頭來修改下 Todos 組件對 Todo 的調用。先增加一個由 TodoList 組件傳遞下來的回調屬性 onUpdate,同時修改 onClick 為 onStateChange,因為這時的 Todo 已不僅僅只有單個點擊事件了,需要定義不同狀態變更時的事件回調:
onStateChange(i)} + onStateChange={() => onStateChange(i)} + onUpdate={(value) => onUpdate(i, value)} />
而最終我們又在 TodoList 組件中,增加 Todo 在數據更新后的業務邏輯。
TodoList 組件的 render 方法內的部分示例代碼:
this.handleUpdate(index, value)} onStateChange={(index) => this.handleStateChange(index)} onRemove={(index) => this.handleRemove(index)} />
TodoList 組件的 handleUpdate 方法的示例代碼:
handleUpdate(index, value) { let todos = [...this.state.todos]; const target = todos[index]; todos = [ ...todos.slice(0, index), { text: value, completed: target.completed }, ...todos.slice(index + 1) ]; this.setState({ todos }); }組件數據管理
既然 TodoList 是一個組件,初始狀態 this.state.todos 就有可能從外部傳入。對于組件內部,我們不應該過多的關心這些數據從何而來(可能通過父容器直接 Ajax 調用后返回的數據,或者 Redux、MobX 等狀態管理器獲取的數據),我覺得組件的數據屬性的設計可以從以下 3 個方面來考慮:
在沒有初始數據傳入時應該提供一個默認值
一旦數據在組件內部被更新后應該及時的通知父組件
當有新的數據(從后端 API 請求的)傳入組件后,應該重新更新組件內部狀態
根據這幾點,我們可以對 TodoList 再做一番改造。
首先,對 TodoList 增加一個 todos 的默認數據屬性,使父組件在沒有傳入有效屬性值時也不會影響該組件的使用:
export default class TodoList extends Component { constructor(props) { super(props); this.state = { todos: props.todos }; } ... } TodoList.defaultProps = { todos: [] };
然后,再新增一個內部方法 this.update 和一個組件的更新事件回調屬性 onUpdate,當數據狀態更新時可以及時的通知父組件:
export default class TodoList extends Component { ... handleAdd() { ... this.update(todos); } handleUpdate(index, value) { ... this.update(todos); } handleRemove(index) { ... this.update(todos); } handleStateChange(index) { ... this.update(todos); } update(todos) { const { onUpdate } = this.props; this.setState({ todos }); onUpdate && onUpdate(todos); } }
這就完事兒了?No! No! No! 因為 this.state.todos 的初始狀態是由外部 this.props 傳入的,假如父組件重新更新了數據,會導致子組件的數據和父組件不同步。那么,如何解決?
我們回顧下 React 的生命周期,父組件傳遞到子組件的 props 的更新數據可以在 componentWillReceiveProps 中獲取。所以我們有必要在這里重新更新下 TodoList 的數據,哦!千萬別忘了判斷傳入的 todos 和當前的數據是否一致,因為,當任何傳入的 props 更新時都會導致 componentWillReceiveProps 的觸發。
componentWillReceiveProps(nextProps) { const nextTodos = nextProps.todos; if (Array.isArray(nextTodos) && !_.isEqual(this.state.todos, nextTodos)) { this.setState({ todos: nextTodos }); } }
注意代碼中的 _.isEqual,該方法是 Lodash 中非常實用的一個函數,我經常拿來在這種場景下使用。
結尾由于本人對 React 的了解有限,以上示例中的方案可能不一定最合適,但你也看到了 TodoList 組件,既可以是包含多個不同功能邏輯的大組件,也可以拆分為獨立、靈巧的小組件,我覺得我們只需要掌握一個度。當然,如何設計取決于你自己的項目,正所謂:沒有最好的,只有更合適的。還是希望本篇文章能給你帶來些許的小收獲。
iKcamp官網:http://www.ikcamp.com
訪問官網更快閱讀全部免費分享課程:《iKcamp出品|全網最新|微信小程序|基于最新版1.0開發者工具之初中級培訓教程分享》。
包含:文章、視頻、源代碼
iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、當當開售。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/89657.html
摘要:右側展現對應產品。我們使用命名為的對象表示過濾條件信息,如下此數據需要在組件中進行維護。因為組件的子組件和都將依賴這項數據狀態。化應用再回到之前的場景,我們設計化函數,進一步可以簡化為對于的偏應用即上面提到的相信大家已經理解了這么做的好處。 showImg(https://segmentfault.com/img/remote/1460000014458612?w=1240&h=663...
摘要:右側展現對應產品。我們使用命名為的對象表示過濾條件信息,如下此數據需要在組件中進行維護。因為組件的子組件和都將依賴這項數據狀態。化應用再回到之前的場景,我們設計化函數,進一步可以簡化為對于的偏應用即上面提到的相信大家已經理解了這么做的好處。 showImg(https://segmentfault.com/img/remote/1460000014458612?w=1240&h=663...
摘要:但是隨著程序邏輯越來越復雜,業務邏輯代碼跟代碼混到一起就變得越來越難以維護,所以就有了開發模式。其實只是給加了點糖上面這種在中寫類似代碼的語法被稱為。你可以理解為擴展版的。尤其是對一些相對還比較流行的框架或技術,更是如此。 這是《玩轉 React》系列的第三篇,看到本篇的標題,了解過 React 的同學可能已經大致猜到我要講什么了,本篇中要講的內容對于剛接觸 React 的同學來說,可...
摘要:但是隨著程序邏輯越來越復雜,業務邏輯代碼跟代碼混到一起就變得越來越難以維護,所以就有了開發模式。其實只是給加了點糖上面這種在中寫類似代碼的語法被稱為。你可以理解為擴展版的。尤其是對一些相對還比較流行的框架或技術,更是如此。 這是《玩轉 React》系列的第三篇,看到本篇的標題,了解過 React 的同學可能已經大致猜到我要講什么了,本篇中要講的內容對于剛接觸 React 的同學來說,可...
閱讀 1662·2019-08-30 12:51
閱讀 656·2019-08-29 17:30
閱讀 3696·2019-08-29 15:17
閱讀 852·2019-08-28 18:10
閱讀 1355·2019-08-26 17:08
閱讀 2169·2019-08-26 12:16
閱讀 3429·2019-08-26 11:47
閱讀 3497·2019-08-23 16:18