企业级产品的开发流程中,产品经理将表达交互的原型图给设计师,设计师最终将UI图提供给工程师,工程师此时需要将UI图分解,逆推需要实现的功能。如何合理分解UI图,正是本系列文章的核心。通过合理地分解UI图,确定组件功能的界限,帮助工程师探索和养成自己的React编程最佳实践。
目录
一、功能分析
图一是TodoList设计图,从界面上看分“上、中、下”三个部分:
- 上:新增待办
- 中:待办列表
- 下:改变列表分类
二、数据分析
当“上”部分添加数据后,“中”部分会显示新增的数据,因此考虑使用数组list保存待办事项数据,具体的每一项是个JS对象。
let list = [
{
// 待办事项JS对象
},
{},
...];
当“下”部分切换类型之后,“中”部分会切换显示相应类型的待办事项,因此考虑JS对象具有completed字段保存状态,且具有name字段保存事项名称,以及id作为唯一标识。
let plan = {
id: 0, // 唯一标识
name: 'xxx', // 待办事项名称
completed: false // 待办事项完成状态
}
当然,我们需要一个全局状态filter保存“下”部分中选择的类型,便于“中”部分显示相应类型的列表数据。
最终得到组件状态的数据结构如下,这个状态可以考虑放在组件顶层:
let state = {
list =[
{
id: 0, // 唯一标识
name: 'xxx', // 待办事项名称
completed: false // 待办事项完成状态
},
{},
...],
filter: 'all'
};
三、组件拆分
最简单的拆分方式,按照界面可以粗粒度地拆为三部分:
- AddTodo组件:对应界面“上”部分
- TodoList组件:对应界面“中”部分
- Filter组件:对应界面“下”部分
深入一点思考,细化组件的功能:
AddTodo组件,可以改变列表list,因此有个方法去执行这个改变;
TodoList组件,可以显示list,因此有个状态list保存数据;可以改变列表list,因此有个方法去执行这个改变;
Filter组件,可以显示当前选中类型filter,因此有个状态filter保存数据;可以改变选中类型filter,因此有个方法去执行这个改变。
更进一步,AddTodo组件和TodoList组件关联度很高,可以考虑都放在Todos组件下;TodoList组件中的每行看起来都是差不多,可以考虑复用成TodoItem组件;Filter组件中的每个按钮也差不多,可以考虑复用成Link组件。
可以看到,显示功能必定对应状态,改变功能必定对应方法,这里涉及组件React组件设计方法,可以参考笔者另一篇博文《React实战_如何设计高质量组件》。
四、目录结构
filter文件夹下存放Filter组件相关文件,具有views文件夹和index.js文件。
todos文件夹下存放Todos组件相关文件,具有views文件夹和index.js文件。
views文件夹下存放更细粒度的组件,index.js文件作为组件模块的统一入口(目前仅输出views,后期可扩展输出状态)。
五、组件实现
第二部分中说过,将组件的状态统一放在顶层,通过props将传递给子组件。根据“分而治之”的思想,我们并不是将整个state都放在根组件,而是将list放在Todos组件,filter放在Filter组件分别管理,这样能避免混乱。
AddTodo组件
前面分析过,AddTodo组件具有改变list功能,我们需要创建一个组件内部的方法clickHandler,其中能获取input的值,并执行父组件的方法,用于改变父组件中的list。
import React, { createRef } from 'react';
const inputRef = createRef(); // 使用hook
export default function AddTodo({ onAdd }) {
const clickHandler = () => {
let value = inputRef.current.value.trim();
if (value) {
// 执行父组件的方法,修改list数据
onAdd(value);
// 添加后将input框置空,提高用户体验
inputRef.current.value = "";
}
}
return (
<React.Fragment>
<input ref={inputRef} />
<button onClick={clickHandler}>添加</button>
</React.Fragment>
)
}
TodoList组件
TodoList组件具有显示特定类型list和改变list功能,我们可以在Todos组件创建方法clickHandler,将其作为props传递给TodoList组件,TodoList以及子组件均为“受控组件”。显然TodoList依赖list和filter做渲染,也能通过props从父组件获取。
import React from 'react';
import TodoItem from './todo-item';
import { FilterTypes } from '../../constants';
export default function TodoList({ list, onClick, filter }) {
return (
<ul>
{
list.filter(item => { // 根据filter过滤不符合的类型
if (filter === FilterTypes.COMPLETED) {
return item.completed
} else if (filter === FilterTypes.UNCOMPLETED) {
return !item.completed
} else {
return item
}
}).map(item => <TodoItem // 渲染符合条件的TodoItem
key={item.id}
text={item.text}
onClick={() => onClick(item.id)}
completed={item.completed} />)
}
</ul>
)
}
TodoItem组件
TodoItem组件是完全受控的组件,它的显示内容完全由props决定,根据UI图我们可以知道它需要text、completed和onClick属性。
import React from 'react';
export default function TodoItem({ text, onClick, completed }) {
return (
<div onClick={onClick}>
<span>{text} </span>
<span>
完成:
<input type="checkbox" readOnly checked={completed} />
</span>
</div>
)
}
Todos组件
Todos组件相对复杂点,因为list和clickHandler方法都放在里面,它将AddTodo组件和TodoList组件关联了起来。
import React, { useState } from 'react';
import deepcopy from 'deepcopy';
import AddTodo from './add-todo';
import TodoList from './todo-list';
export default function ({ filter }) {
// 整个todos组件中的状态list保存在这里
const [list, setList] = useState([]);
// 传递给AddTodo组件
const addHandler = (text) => {
let item = {
id: (Math.random() * 10000).toFixed(0),
text: text,
completed: false
}
setList([...list, item]);
}
// 传递给能TodoList组件
const clickHandler = (id) => {
let newList = deepcopy(list);
newList.forEach(item => {
if (item.id === id) {
item.completed = !item.completed;
}
});
setList(newList);
}
return (
<div className="todos">
{/* 渲染AddTodo组件 */}
<AddTodo onAdd={addHandler} />
{/* 渲染TodoList组件 */}
<TodoList list={list} filter={filter} onClick={clickHandler} />
</div>
)
}
Filter组件
Filter组件具有显示和改变filter类型的功能,我们可以在TodoApp根组件创建方法setFilter,将其作为props传递给Filter组件,Filter以及子组件均为“受控组件”。显然Filter依赖filter做渲染,也能通过props从父组件获取。
import React from 'react';
import Link from './link';
import { FilterTypes } from '../../constants';
const types = Object.keys(FilterTypes);
export default function Filter({ filter, setFilter }) {
return (
<div style={{ display: 'flex' }}>
{
types.map(
type =>
<Link
key={type}
type={type}
filter={filter}
onClick={setFilter} />)
}
</div>
)
}
Link组件
Link组件具有显示当前状态功能,其状态完全由父组件控制,可以通过props从父组件获取filter。
import React from 'react';
import { FilterTypes } from '../../constants';
export default function Link({ filter, onClick, type }) {
return (
<div
className={filter === FilterTypes[type] ? 'link link--active' : 'link'}
onClick={() => onClick(FilterTypes[type])}>
{type}
</div>
)
}
六、总结
本文主要介绍了如何通过React实现,完全基于函数式组件的待办事项TodoList。假设工程师的起点都是拿到UI图,本文对TodoList的UI图进行了功能分析、数据分析,在此基础上进行了组件拆分,定下了初步的目录结构,最终按照之前的分析工作实现各个粒度的组件,大的组件都封装成一个模块,由统一的index.js文件暴露给调用者,保证了组件的高内聚、低耦合。