发现最近看到的框架入门程序都变成Todos
,我花了三个小时才自己实现了一个Todos
...感叹前端入门越来越复杂了,怀念几年前还是hello world
的时代。。。
吐槽不多说,这里主要说的只是React
和Redux
这一块儿,css
样式完全是从这里抄过来的。
代码的结构准备是这个样子的:
转化成代码结构,就是这个样子:
另外,按照官方示例,在Header
左边的toggleAll
按钮放到了Section
中。
Redux
Types/todos.js
在Redux
中,type
是用于action
和reducer
交流的时候的一个flag
,让reducer
知道这是一个什么请求。
我比较习惯把type
单独分离出来,列到一个文件里面,让Redux
的文件更干净,并便于管理。
/** ------------------------- TODO ---------------------*/ export const TODO_INSERT_ITEM = 'TODO_INSERT_ITEM'; export const TODO_DELETE_ITEM = 'TODO_DELETE_ITEM'; export const TODO_SWITCH_FILTER = 'TODO_SWITCH_FILTER'; export const TODO_TOGGLE_ACTIVE = 'TODO_TOGGLE_ACTIVE'; export const TODO_TOGGLE_ALL = 'TODO_TOGGLE_ALL'; export const TODO_CHANGE_VALUE = 'TODO_CHANGE_VALUE'; export const TODO_CLEAR_COMPLETED = 'TODO_CLEAR_COMPLETED';
Actions/todos.js
根据上面列出的type
,列出对应的action creator
import { TODO_INSERT_ITEM,TODO_DELETE_ITEM,TODO_SWITCH_FILTER,TODO_TOGGLE_ACTIVE,TODO_TOGGLE_ALL,TODO_CHANGE_VALUE,TODO_CLEAR_COMPLETED } from '../types'; // 插入一个TODO export function insertItem(value){ return { type: TODO_INSERT_ITEM,value }; } // 删除一个TODO export function deleteItem(id) { return { type: TODO_DELETE_ITEM,id } } // 转换一个TODO的状态 export function switchFilter(filter) { return { type: TODO_SWITCH_FILTER,filter } } // 清楚所有完成的TODO export function clearCompleted(){ return { type: TODO_CLEAR_COMPLETED } } export function toggleActive(id){ return { type: TODO_TOGGLE_ACTIVE,id } } // 转换所有的状态到active export function toggleAll(active){ return { type: TODO_TOGGLE_ALL,active } } // 改变对应TODO的值 export function changeValue(id,value) { return { type: TODO_CHANGE_VALUE,id,value } }
Reducers/todos.js
在reducer
中需要注意几点:
初始化的
state
要从localStorage
中获取每次做出修改,都要重新更新
localStorage
数据没有发生改变的时候,尽量使用原数据,减少
re-render
为了便于查找,我在这里用了
lodash
的uniqueId
方法,给每一个item
加一个id
为了便于储存和展示,我这里包含一个
items
用来保存所有的items
,一个showedItems
用来储存需要展示的items
先提供一个简单的简写localStorage
方法
const local = (function(KEY){ return { set: value=>{ localStorage.setItem(KEY,value) },get: ()=>localStorage.getItem(KEY),check: ()=>localStorage.getItem(KEY) != undefined }; })("todo");
然后几个辅助的方法:
// 制造一个新的item function generateItem(value) { return { id: _.uniqueId(),active: true,value } } // 判断当前的item是否正在展示 function include(active,filter) { return filter === "ALL" || (active && filter === "ACTIVE") || (!active && filter === "COMPLETED"); } // 获取页面上需要展示的items function getShowedItems(items,filter) { let showedItems = [],keys = Object.keys(items); for(let i = 0; i < keys.length; i++){ let item = items[keys[i]]; if(include(item.active,filter)) { showedItems.push(item); } } return showedItems; }
初始化的时候,获取localStorage
中的值,或者给一个默认值:
let defaultTodo; (function(){ if(local.check()) { defaultTodo = JSON.parse(local.get()); } else { defaultTodo = { items: {},filter: "ALL",// ALL,COMPLETED,ACTIVE count: 0,showedItems: [],hasCompleted: false } } })();
注:在这里提一句,由于我不喜欢文档中把所有的处理方法放在一个函数里面的方式,所以我写了一个方法,把reducers分开成多个函数
// 很简单,其实就是循环调用。。。 export function combine(reducers){ return (state,action) => { for(let key in reducers) { if(reducers.hasOwnProperty(key)) { state = reducers[key](state,action) || state; } } return state; } }
下面上所有的reducers
,具体逻辑就不多说了:
let exports = {}; exports.insertItem = function(state = defaultTodo,action) { const type = action.type; if(type === TODO_INSERT_ITEM) { let { count,items,filter,showedItems } = state; let item = generateItem(action.value); items = { ...items,[item.id] : item } count = count + 1; state = { ...state,count,showedItems: filter !== "COMPLETED" ? getShowedItems(items,filter) : showedItems } local.set(JSON.stringify(state)); } return state; } exports.deleteItem = function(state = defaultTodo,action) { const type = action.type; if(type === TODO_DELETE_ITEM && state.items[action.id]) { let { count,hasCompleted } = state; let item = items[action.id]; delete items[action.id]; if(item.active) count--; state = { ...state,showedItems: include(item.active,filter) ? getShowedItems(items,filter) : state.showedItems,hasCompleted: Object.keys(items).length !== count } local.set(JSON.stringify(state)); } return state; } exports.switchFilter = function(state = defaultTodo,action) { const type = action.type; if(type === TODO_SWITCH_FILTER && state.filter !== action.filter) { state = { ...state,filter: action.filter,showedItems: getShowedItems(state.items,action.filter) } local.set(JSON.stringify(state)); } return state; } exports.clearCompleted = function(state = defaultTodo,action) { const type = action.type; if(type === TODO_CLEAR_COMPLETED) { let { items,showedItems } = state; let keys = Object.keys(items); let tempItems = {}; for(let i = 0; i < keys.length; i++) { let item = items[keys[i]]; if(item.active) { tempItems[item.id] = item; } } state = { ...state,items: tempItems,showedItems: filter === "ACTIVE" ? showedItems : getShowedItems(tempItems,filter),hasCompleted: false } local.set(JSON.stringify(state)); } return state; } exports.toggleActive = function(state = defaultTodo,action) { const { type,id } = action; if(type === TODO_TOGGLE_ACTIVE && state.items[id]) { let { items,showedItems } = state; let item = items[id]; item.active = !item.active; items = { ...items,[id]: item }; if(item.active) count++; // 如果变为active else count--; // 如果变为completed state = { ...state,showedItems: getShowedItems(items,hasCompleted: Object.keys(items).length !== count } local.set(JSON.stringify(state)); } return state; } exports.toggleAll = function(state = defaultTodo,active } = action; if(type === TODO_TOGGLE_ALL) { let { items,showedItems } = state; let keys = Object.keys(items); for(let i = 0; i < keys.length; i++) { items[keys[i]].active = active; } let count = active ? keys.length : 0; state = { ...state,showedItems: include(active,filter) : showedItems,hasCompleted: !active } local.set(JSON.stringify(state)); } return state; } exports.changeValue = function(state = defaultTodo,action){ const { type,id } = action; if(type === TODO_CHANGE_VALUE && state.items[id]) { let { items,showedItems } = state; let item = items[id]; item.value = action.value; items = { ...items,[id]: item }; state = { ...state,filter) : showedItems } local.set(JSON.stringify(state)); } return state; } export default combine(exports); // 用combine方法包裹
Reducers
中,我在很多的showedItems
都做了是否发生改变的检查,如果没有发生改变,那么就用原来的,便于在Section
组件中,可以避免不必要的重新渲染。虽然,在我这里似乎没有什么用。不过对复杂的项目还是有必要的。
Views/Todos.js
import React from 'react'; import Header from 'containers/ToDo/Header'; import Footer from 'containers/ToDo/Footer'; import Section from 'containers/ToDo/Section'; import 'components/ToDo/index.scss'; export default class ToDo extends React.Component { constructor(props) { super(props); } render(){ return ( <div id="TODO"> <Header /> <Section /> <Footer /> </div> ) } }
Contianers
Header.js
Header.js
主要负责logo
的渲染,和那个input
框的功能。
利用controlled component
对input
的值进行控制,然后监听键盘来判断是输入还是提交。
import React from 'react'; import { CONTROLS } from 'utils/KEYCODE'; import { connect } from 'react-redux'; import { insertItem } from 'actions/todo'; class Header extends React.Component { constructor(props) { super(props); this.state = { value: "" }; } onChange = (e)=>{ let value = e.target.value; this.setState({ value }); } onKeyDown = (e)=>{ let keyCode = e.keyCode; if(keyCode === CONTROLS.ENTER && this.state.value !== "") { this.props.insertItem(this.state.value); this.setState({ value: "" }); e.preventDefault(); e.stopPropagation(); } } render(){ return ( <header className="todo-header"> <h1>todos</h1> <input type="text" className="insert" value={this.state.value} onChange={this.onChange} onKeyDown={this.onKeyDown} placeholder="What needs to be done?" /> </header> ) } } export default connect(null,{ insertItem })(Header);
Footer.js
Footer
主要是用于展示数量,filter,Clear Completed按钮
import React,{ PropTypes } from 'react'; import { connect } from 'react-redux'; import { switchFilter,clearCompleted } from 'actions/todo'; class Footer extends React.Component { constructor(props) { super(props); } switchFilter = (filter)=>{ this.props.switchFilter(filter.toUpperCase()); } render(){ const { count,hasCompleted,clearCompleted } = this.props; if(count === 0 && !hasCompleted) return null; return ( <footer className="todo-footer"> <span className="counter">{count} items left</span> <ul className="filter"> { ["All","Active","Completed"].map((status,index)=>{ return ( <li key={status} className={status.toUpperCase() === filter ? "active" : ""}> <a href="javascript:;" onClick={()=>{ this.switchFilter(status) }}>{status}</a> </li> ); }) } </ul> { hasCompleted && <button className="clear-completed" onClick={clearCompleted}>Clear completed</button> } </footer> ) } } Footer.propTypes = { count: PropTypes.number.isrequired,hasCompleted: PropTypes.bool.isrequired,filter: PropTypes.oneOf(['ALL','ACTIVE','COMPLETED']).isrequired } function mapStateToProps(state){ let { todo: { count,filter } } = state; return { count,filter } } export default connect(mapStateToProps,{ switchFilter,clearCompleted })(Footer);
Section.js
Section
包含Todos的列表,还有删除,改变状态,修改value,toggle all等功能。
import React,{ PropTypes } from 'react'; import { connect } from 'react-redux'; import { deleteItem,toggleActive,toggleAll,changeValue } from 'actions/todo'; import Item from 'components/ToDo/Item'; class Section extends React.Component { constructor(props) { super(props); } render(){ const { showedItems=[],changeValue,deleteItem,hasCompleted } = this.props; return ( <section className="todo-section"> <input type="checkBox" className="toggle-all" onChange={()=>{ toggleAll(count === 0) }} checked={count === 0 && hasCompleted} /> <ul className="todo-items"> { showedItems.map(item=><Item key={item.id} {...item} onValueChange={changeValue} onItemDelete={deleteItem} toggleActive={toggleActive} />) } </ul> </section> ) } } Section.propTypes = { showedItems: PropTypes.arrayOf(PropTypes.object).isrequired,count: PropTypes.number.isrequired } function mapStateToProps(state) { let { todo: { showedItems,hasCompleted } } = state; return { showedItems,hasCompleted }; } export default connect(mapStateToProps,{ deleteItem,changeValue })(Section);
Components
Item.js
import React from 'react'; import ReactDOM from 'react-dom'; export default class Item extends React.Component { constructor(props) { super(props); this.state = { value: props.value,editing: false }; } componentDidUpdate() { if(this.state.editing) { var node = ReactDOM.findDOMNode(this.edit); node.focus(); } } inputInstance = (input) => { this.edit = input; } onToggle = (e)=>{ this.props.toggleActive(this.props.id,!e.target.checked); } onValueChange = (e)=>{ this.props.onValueChange(this.props.id,e.target.value); this.setState({ editing: false }); } onEditChange = (e)=>{ this.setState({ value: e.target.value }); } onDoubleClick = (e)=>{ this.setState({ editing: true }); } render(){ let { id,active,onItemDelete,onValueChange } = this.props; let { value,editing } = this.state; return ( <li className={editing ? "editing" : ""}> { editing || (<div className="view"> <input type="checkBox" className="toggle" onChange={this.onToggle} checked={!active} /> <label className={ `item-value${active ? "" : " completed"}`} onDoubleClick={this.onDoubleClick} >{value}</label> <button className="delete" onClick={()=>{ onItemDelete(id) }}></button> </div>) } { editing && <input type="text" ref={this.inputInstance} className="edit" onBlur={this.onValueChange} onChange={this.onEditChange} value={value} /> } </li> ) } }
写组件的时候,感觉代码贴出来看看就好了。需要讲解的不多。。。