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