如何优雅的设计 React 组件
如今的 Web 前端已被 React、Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式。那么,为什么大家会觉得 jQuery “过时了”呢?一来,文章《No JQuery! 原生 JavaScript 操作 DOM》就直截了当的告诉你,现在用原生 JavaScript 可以非常方便的操作 DOM 了。其次,jQuery 的便利性是建立在有一个基础 DOM 结构的前提下的,看上去是符合了样式、行为和结构分离,但其实 DOM 结构和 JavaScript 的代码逻辑是耦合的,你的开发思路会不断的在 DOM 结构和 JavaScript 之间来回切换。 尽管现在的 jQuery 已不再那么流行,但 jQuery 的设计思想还是非常值得致敬和学习的,特别是 jQuery 的插件化。如果大家开发过 jQuery 插件的话,想必都会知道,一个插件要足够灵活,需要有细颗粒度的参数化设计。一个灵活好用的 React 组件跟 jQuery 插件一样,都离不开合理的属性化( So! 接下来我们就以万能的 TODO LIST 为例,一起来设计一款 React 的 实现基本功能TODO LIST 的功能想必我们应该都比较了解,也就是 TODO 的添加、删除、修改等等。本身的功能也比较简单,为了避免示例的复杂度,显示不同状态 TODO LIST 的导航(全部、已完成、未完成)的功能我们就不展开了。 约定目录结构先假设我们已经拥有一个可以运行 React 项目的脚手架(ha~ 因为我不是来教你如何搭建脚手架的),然后项目的源码目录 . ├── components ├── containers │ └── App │ ├── app.scss │ └── index.js ├── index.html └── 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( <AppContainer> <Component /> </AppContainer>,document.getElementById('app') ); }; render(App); if (module.hot) { module.hot.accept('containers/App',() => { let nextApp = require('containers/App').default; render(nextApp); }); } 接下来看 基本的目录结构看起来已经完成,接下来我们实现下主容器组件 实现主容器我们先来看下主容器组件 import React,{ Component } from 'react'; import styles from './app.scss'; class App extends Component { constructor(props) { super(props); this.state = { todos: [] }; } render() { return ( <div className={styles.container}> <h2 className={styles.header}>Todo List Demo</h2> <div className={styles.content}> <header className={styles['todo-list-header']}> <input type="text" className={styles.input} ref={(input) => this.input = input} /> <button className={styles.button} onClick={() => this.handleAdd()} > Add Todo </button> </header> <section className={styles['todo-list-content']}> <ul className={styles['todo-list-items']}> {this.state.todos.map((todo,i) => ( <li key={`${todo.text}-${i}`}> <em className={todo.completed ? styles.completed : ''} onClick={() => this.handleStateChange(i)} > {todo.text} </em> <button className={styles.button} onClick={() => this.handleRemove(i)} > Remove </button> </li> ))} </ul> </section> </div> </div> ); } handleAdd() { ... } handleRemove(index) { ... } handleStateChange(index) { ... } } export default App; 我们可以像上面这样把所有的业务逻辑一股脑的塞进主容器中,但我们要考虑到主容器随时会组装其他的组件进来,将各种逻辑堆放在一起,到时候这个组件就会变得无比庞大,直到“无法收拾”。所以,我们得分离出一个独立的 分离组件TodoList 组件在 . ├── components +│ └── TodoList +│ ├── index.js +│ └── todo-list.scss ├── containers │ └── App │ ├── app.scss │ └── index.js ... 然后我们将 ... import styles from './todo-list.scss'; export default class TodoList extends Component { ... render() { return ( <div className={styles.container}> - <header className={styles['todo-list-header']}> + <header className={styles.header}> <input type="text" className={styles.input} ref={(input) => this.input = input} /> <button className={styles.button} onClick={() => this.handleAdd()} > Add Todo </button> </header> - <section className={styles['todo-list-content']}> + <section className={styles.content}> - <ul className={styles['todo-list-items']}> + <ul className={styles.items}> {this.state.todos.map((todo,i) => ( <li key={`${todo}-${i}`}> <em className={todo.completed ? styles.completed : ''} onClick={() => this.handleStateChange(i)} > {todo.text} </em> <button className={styles.button} onClick={() => this.handleRemove(i)} > Remove </button> </li> ))} </ul> </section> </div> ); } ... } 有没有注意到上面 ... module.exports = { ... module: { rules: [ { test: /.s?css/,use: [ 'style-loader',{ loader: 'css-loader',options: { modules: true,localIdentName: '[name]--[local]-[hash:base64:5]' } },... ] } ] } ... }; 我们再来看下该组件的代码输出后的结果: <div data-reactroot="" class="app--container-YwMsF"> ... <div class="todo-list--container-2PARV"> <header class="todo-list--header-3KDD3"> ... </header> <section class="todo-list--content-3xwvR"> <ul class="todo-list--items-1SBi6"> ... </ul> </section> </div> </div> 从上面 webpack 的配置和输出的 HTML 中可以看到, 回到正题,我们再来看下分离 import TodoList from 'components/TodoList'; ... class App extends Component { render() { return ( <div className={styles.container}> <h2 className={styles.header}>Todo List Demo</h2> <div className={styles.content}> <TodoList /> </div> </div> ); } } export default App; 抽离通用组件作为一个项目,当前的 但是,如何拆分组件才是最合理的呢?我觉得这个问题没有最好的答案,但我们可以从几个方面进行思考:可封装性、可重用性和灵活性。比如拿 好,我们先拿 input 和 button 下手,在 . ├── components +│ ├── Button +│ │ ├── button.scss +│ │ └── index.js +│ ├── Input +│ │ ├── index.js +│ │ └── input.scss │ └── TodoList │ ├── index.js │ └── todo-list.scss ...
... export default class Button extends Component { render() { const { className,children,onClick } = this.props; return ( <button type="button" className={cn(styles.normal,className)} onClick={onClick} > {children} </button> ); } }
... export default class Input extends Component { render() { const { className,value,inputRef } = this.props; return ( <input type="text" className={cn(styles.normal,className)} defaultValue={value} ref={inputRef} /> ); } } 由于这 2 个组件自身不涉及任何业务逻辑,应该属于纯渲染组件(木偶组件),我们可以使用 React 轻量的无状态组件的方式来声明: ... const Button = ({ className,onClick }) => ( <button type="button" className={cn(styles.normal,className)} onClick={onClick} > {children} </button> ); 是不是觉得酷炫很多! 另外,从 我们再回到上面的 ... import Button from 'components/Button'; import Input from 'components/Input'; ... export default class TodoList extends Component { render() { return ( <div className={styles.container}> <header className={styles.header}> <Input className={styles.input} inputRef={(input) => this.input = input} /> <Button onClick={() => this.handleAdd()}> Add Todo </Button> </header> ... </div> ); } } ... 拆分子组件然后继续接着看 ... export default class TodoList extends Component { render() { return ( <div className={styles.container}> ... <section className={styles.content}> {this.renderItems()} </section> </div> ); } renderItems() { return ( <ul className={styles.items}> {this.state.todos.map((todo,i) => ( <li key={`${todo}-${i}`}> ... </li> ))} </ul> ); } ... } 上面的代码看似降低了 然后我们预览下现在的目录结构: . ├── components │ ... │ └── TodoList +│ ├── components +│ │ └── Todos +│ │ ├── index.js +│ │ └── todos.scss │ ├── index.js │ └── todo-list.scss
... const Todos = ({ data: todos,onStateChange,onRemove }) => ( <ul className={styles.items}> {todos.map((todo,i) => ( <li key={`${todo}-${i}`}> <em className={todo.completed ? styles.completed : ''} onClick={() => onStateChange(i)} > {todo.text} </em> <Button onClick={() => onRemove(i)}> Remove </Button> </li> ))} </ul> ); ... 再看拆分后的 render() { return ( <div className={styles.container}> ... <section className={styles.content}> <Todos data={this.state.todos} onStateChange={(index) => this.handleStateChange(index)} onRemove={(index) => this.handleRemove(index)} /> </section> </div> ); } 增强子组件到目前为止,大体上的功能已经搞定,子组件看上去拆分的也算合理,这样就可以很容易的增强某个子组件的功能了。就拿 . ├── components │ ... │ └── TodoList │ ├── components +│ │ ├── Todo +│ │ │ ├── index.js +│ │ │ └── todo.scss │ │ └── Todos │ │ ├── index.js │ │ └── todos.scss │ ├── index.js │ └── todo-list.scss 先看下 ... import Todo from '../Todo'; ... const Todos = ({ data: todos,i) => ( <li key={`${todo}-${i}`}> <Todo {...todo} onClick={() => onStateChange(i)} /> <Button onClick={() => onRemove(i)}> Remove </Button> </li> ))} </ul> ); export default Todos; 我们先不关心 <Todo {...todo} + editable={editable} onClick={() => onStateChange(i)} /> 然后,我们再思考下,在
我们先来实现下 render() { const { completed,text,editable,onClick } = this.props; return ( <span className={styles.wrapper}> <em className={completed ? styles.completed : ''} onClick={onClick} > {text} </em> {editable && <Button> Edit </Button> } </span> ); } 显然实现这一步似乎没什么 luan 用,我们还需要点击 Edit 按钮后能显示 render() { const { completed,onStateChange } = this.props,{ editing } = this.state; return ( <span className={styles.wrapper}> {editing ? <Input value={text} className={styles.input} inputRef={input => this.input = input} /> : <em className={completed ? styles.completed : ''} onClick={onStateChange} > {text} </em> } {editable && <Button onClick={() => this.handleEdit()}> {editing ? 'Update' : 'Edit'} </Button> } </span> ); } 最后, 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); } } 需要注意的是,我们传递的是更新后的内容,在数据没有任何变化的情况下通知父组件是毫无意义的。 我们再回过头来修改下 <Todo {...todo} editable={editable} - onClick={() => onStateChange(i)} + onStateChange={() => onStateChange(i)} + onUpdate={(value) => onUpdate(i,value)} /> 而最终我们又在
<Todos editable data={this.state.todos} + onUpdate={(index,value) => this.handleUpdate(index,value)} onStateChange={(index) => this.handleStateChange(index)} onRemove={(index) => this.handleRemove(index)} />
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 }); } 组件数据管理既然
根据这几点,我们可以对 首先,对 export default class TodoList extends Component { constructor(props) { super(props); this.state = { todos: props.todos }; } ... } TodoList.defaultProps = { todos: [] }; 然后,再新增一个内部方法 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! 因为 我们回顾下 React 的生命周期,父组件传递到子组件的 props 的更新数据可以在 componentWillReceiveProps(nextProps) { const nextTodos = nextProps.todos; if (Array.isArray(nextTodos) && !_.isEqual(this.state.todos,nextTodos)) { this.setState({ todos: nextTodos }); } } 注意代码中的 结尾由于本人对 React 的了解有限,以上示例中的方案可能不一定最合适,但你也看到了
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |