发现最近看到的框架入门程序都变成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,
items,
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, items, filter, hasCompleted } = state;
let item = items[action.id];
delete items[action.id];
if(item.active) count--;
state = {
...state,
items,
count,
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, filter, 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, filter, count, showedItems } = state;
let item = items[id];
item.active = !item.active;
items = {
...items,
[id]: item
};
if(item.active) count++; // 如果变为active
else count--; // 如果变为completed
state = {
...state,
items,
count,
showedItems: getShowedItems(items, filter),
hasCompleted: Object.keys(items).length !== count
}
local.set(JSON.stringify(state));
}
return state;
}
exports.toggleAll = function(state = defaultTodo, action) {
const { type, active } = action;
if(type === TODO_TOGGLE_ALL) {
let { items, filter, 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,
items,
count,
showedItems: include(active, filter) ? getShowedItems(items, 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, filter, showedItems } = state;
let item = items[id];
item.value = action.value;
items = {
...items,
[id]: item
};
state = {
...state,
items,
showedItems: include(item.active, filter) ? getShowedItems(items, 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, filter, 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,
hasCompleted,
filter
}
} = state;
return {
count,
hasCompleted,
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=[], count, toggleAll, changeValue, deleteItem, toggleActive, 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,
count,
hasCompleted
}
} = state;
return {
showedItems,
count,
hasCompleted
};
}
export default connect(mapStateToProps, { deleteItem, toggleActive, toggleAll, 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>
)
}
}
写组件的时候,感觉代码贴出来看看就好了。需要讲解的不多。。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。