React框架基础 - 4、todosList案例实战(下)

300 阅读22分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

todolist案例

修改任务名称(双击编辑)

任务界面双击选中某个任务,进入编辑状态,显示文本输入框,可输入或修改任务名称,回车确认保存

创建两个新指令:点击后调用接口修改数据、修改本地数据

通过控制类名来控制是否处于编辑状态,编辑状态类名为editing

编辑状态下会默认展示当前任务名称,可以进行修改

双击触发第一个指令,传递数据,之后调用接口修改任务编辑状态,然后将数据再向下传递正式修改数据

新指令根据传递过来的id值修改数据(需要深拷贝)

这时需要修改main组件的结构,让编辑状态可以根据数据展示

// src/Store/Actions/todos.action.js  新增指令

// 引入 createAction 对 action 进行简化
import {createAction} from 'redux-actions'

// 初始化数据指令
export const getTodos = createAction('getTodos')
export const getTodos_success = createAction('getTodos_success')

// 添加任务指令
export const addTodos = createAction('addTodos')
export const addTodos_success = createAction('addTodos_success')

// 删除任务指令
export const removTodos = createAction('removTodos')
export const removTodos_success = createAction('removTodos_success')

// 修改状态指令
export const modifyTodos = createAction('modifyTodos')
export const modifyTodos_success = createAction('modifyTodos_success')

// 筛选指令
export const screenTodos = createAction('screenTodos')

// 清除已完成任务指令
export const clearCompletedTodos = createAction('clearCompletedTodos')
export const clearCompletedTodos_success = createAction('clearCompletedTodos_success')

// 修改任务编辑状态指令
export const modifyEditing = createAction('modifyEditing')
export const modifyEditing_success = createAction('modifyEditing_success')
// src/Store/Sagas/todos.saga.js  书写指令拦截和拦截后回调函数

// 引入 saga 的两个方法,takeEvery 捕捉指令并拦截,put将指令传递给 store
import {takeEvery, put} from 'redux-saga/effects'
// 引入 axios 请求接口数据
import axios from 'axios'

// 捕捉指令后要执行的函数
function* loaderData() {
  // 获取数据
  const {data} = yield axios.get('http://localhost:3005/api/todos')
  // 调用 put 方法将数据传递给其他指令
  yield put({type: 'getTodos_success', payload: data})
}

// 添加任务指令拦截后执行的函数
function* addTodos(action) {
  // 调用接口添加任务
  const {data} = yield axios.post('http://localhost:3005/api/todos', {taskName: action.payload})
  // 将接口返回的数据交给下一个指令处理
  yield put({type: 'addTodos_success', payload: data.task})
}

// 删除任务指令拦截后执行的函数
function* removTodos(action) {
  // 调用接口根据 ID 删除任务
  const {data} = yield axios.delete('http://localhost:3005/api/todos', { params: {id: action.payload}})
  // 调用另一个指令,把要删除的 id 传递过去
  yield put({type: 'removTodos_success', payload: data.tasks.id})
}

// 捕捉修改状态指令后的回调函数
function* modifyTodos(action) {
  // 调用接口修改状态
  const {data} = yield axios.put('http://localhost:3005/api/todos/isCompleted', action.payload)
  // 调用另一个指令,把接口返回的数据传递给下一个指令
  yield put({type: 'modifyTodos_success', payload: data.task})
}


// 捕捉 clearCompletedTodos 指令后的回调函数
function* clearCompletedTodos() {
  yield axios.delete('http://localhost:3005/api/todos/clearCompleted')
  yield put({type: 'clearCompletedTodos_success'})
}

// 任务编辑状态修改指令拦截后回调
function* modifyEditing(action) {
  yield axios.put('http://localhost:3005/api/todos/isEditing', action.payload)
  yield put({type: 'modifyEditing_success', payload: action.payload})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
  // 捕捉 removTodos 指令,捕捉后执行 removTodos 方法
  yield takeEvery('removTodos', removTodos)
  // 捕捉 modifyTodos 指令,然后调用 modifyTodos 方法
  yield takeEvery('modifyTodos', modifyTodos)
  // 捕捉 clearCompletedTodos 指令,然后调用 clearCompletedTodos 方法
  yield takeEvery('clearCompletedTodos', clearCompletedTodos)
  // 捕捉任务编辑状态修改指令
  yield takeEvery('modifyEditing', modifyEditing)
}
// src/Store/Reducers/todos.reducer.js  书写实际操作数据的指令函数

// 引入 handleActions 简化书写,注意这里是 handleActions 而不是 handleAction
import {handleActions} from 'redux-actions'
// 引入实际更改数据的指令,中间件saga拦截的指令就不需要引入了
import {getTodos_success, addTodos_success, removTodos_success, modifyTodos_success, screenTodos, clearCompletedTodos_success, modifyEditing_success} from '../Actions/todos.action'

// 原始数据
const data = {
  // 空的列表
  todos: [],
  // 添加一个参数,作为筛选关键词,默认为all(全部)
  show: 'all'
}

// 创建指令函数,修改下面全部 return 的数据,将筛选关键词也传递下去
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: (state, action) => ({todos: action.payload, show: state.show}),
  // 添加任务
  [addTodos_success]: (state, action) => ({todos: [...state.todos, action.payload], show: state.show}),
  // 删除任务
  [removTodos_success]: (state, action) => {
    // 根据传递过来的 ID 获得在任务列表中的 下标
    const index = state.todos.findIndex(item => item.id === action.payload)
    // 深拷贝任务列表
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 从任务列表中删除指定的任务
    todos.splice(index, 1)
    // 用修改后的 todos 替换掉原本的 todos
    return { todos, show: state.show }
  },
  // 修改状态
  [modifyTodos_success]: (state, action) => {
    // 根据传递过来的 ID 获得在任务列表中的 下标
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝任务列表
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 从任务列表中替换指定位置数据
    todos[index] = action.payload
    // 用修改后的 todos 替换掉原本的 todos
    return { todos, show: state.show }
  },
  // 筛选
  [screenTodos]: (state, action) => {
    // 修改关键词并返回
    return {...state, show: action.payload}
  },
  // 清除已完成任务
  [clearCompletedTodos_success]: (state, action) => {
    // 筛选出全部未完成任务组成新数组
    const todos = state.todos.filter(item => !item.isCompleted)
    return {...state, todos}
  },
  // 修改编辑状态
  [modifyEditing_success]: (state, action) => {
    // 获取要修改的任务索引值
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 修改
    todos[index].isEditing = action.payload.isEditing
    // 替换掉原来的数据
    return {...state, todos}
  }
}, data)

// 导出
export default todosReducer
// src/Components/Main.  // 组件添加双击修改编辑状态的事件,并且根据isEditing修改类名

import {Component} from 'react'
// 引入 bindActionCreators 用来自动生成 action 指令函数
import {bindActionCreators} from 'redux'
// connect 获取数据并添加指令方法
import {connect} from 'react-redux'
// 获取全部指令
import * as todoAction from '../Store/Actions/todos.action'

class Main extends Component {
	// 生命周期:组件挂载后
	componentDidMount() {
		// 调用组件的指令函数 getTodos() 获取数据并让 props 拿到
		this.props.getTodos()
	}
	// 删除任务函数
	removTodos(id) {
		// 把要删除的任务的 id 传递给指令
		this.props.removTodos(id)
	}
	modifyTodos(ev, id) {
		// 把要修改状态的任务需要的数据传递过去(要修改为的状态, id值)
		this.props.modifyTodos({id, isCompleted: ev.target.checked})
	}
  render() {
    return (
      <section className="main">
				<input className="toggle-all" type="checkbox" />
				<ul className="todo-list">
					{this.props.todos.map(item => {
						return (
							// 遍历全部的待办事项列表,注意这里设置一下是否添加 completed 类名,如果已完成添加,未完成不添加,并且添加一个判断是否编辑的新条件
							<li key={item.id} className={(item.isCompleted ? 'complete' : '') + (item.isEditing ? 'editing' : '')}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中,添加状态修改事件 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} onChange={(ev) => this.modifyTodos(ev, item.id)} />
									{/* 添加双击事件,直接触发指令,传递两个参数,其中 isEditing 设置为固定值 */}
									<label onDoubleClick={() => this.props.modifyEditing({id: item.id, isEditing: true})}>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								{/* 编辑状态下默认value值为当前任务名 */}
								<input className="edit" value={item.taskName} readOnly/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	// 参数调用自定义的函数,筛选需要的数据然后使用
	todos: filterTodos(store.todosReducer.todos, store.todosReducer.show)
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

// 筛选函数
// 参数1:要筛选的数据,参数2:筛选关键词
function filterTodos(todos, text) {
	// 判断筛选关键词
	switch (text) {
		// 关键词为all直接返回全部数据
		case 'all':
			return todos
		// 关键词为active筛选出所有未完成
		case 'active':
			return todos.filter(item => !item.isCompleted)
		// 关键词为completed筛选出所有已完成
		case 'completed':
			return todos.filter(item => item.isCompleted)
		default:
			return
	}
}

// 导出并调用 connect 注入数据和指令
export default connect(myData, myAction)(Main)

修改任务名称(失焦保存)

编辑状态下的任务文本框失去焦点后触发事件,传递事件对象本身和任务id值

先修改状态,让编辑的任务恢复未编辑状态(可以使用上一步的指令)

然后真正修改任务名称,传递新任务名和id

saga拦截指令调用接口修改数据,然后触发另一个指令修改本地数据

//src/Store/Actions/todos.action.js  添加新指令

// 引入 createAction 对 action 进行简化
import {createAction} from 'redux-actions'

// 初始化数据指令
export const getTodos = createAction('getTodos')
export const getTodos_success = createAction('getTodos_success')

// 添加任务指令
export const addTodos = createAction('addTodos')
export const addTodos_success = createAction('addTodos_success')

// 删除任务指令
export const removTodos = createAction('removTodos')
export const removTodos_success = createAction('removTodos_success')

// 修改状态指令
export const modifyTodos = createAction('modifyTodos')
export const modifyTodos_success = createAction('modifyTodos_success')

// 筛选指令
export const screenTodos = createAction('screenTodos')

// 清除已完成任务指令
export const clearCompletedTodos = createAction('clearCompletedTodos')
export const clearCompletedTodos_success = createAction('clearCompletedTodos_success')

// 修改任务编辑状态指令
export const modifyEditing = createAction('modifyEditing')
export const modifyEditing_success = createAction('modifyEditing_success')

// 修改保存新任务名指令
export const saveName = createAction('saveName')
export const saveName_success = createAction('saveName_success')
// src/Store/Sagas/todos.saga.js  书写新指令拦截和回调函数

// 引入 saga 的两个方法,takeEvery 捕捉指令并拦截,put将指令传递给 store
import {takeEvery, put} from 'redux-saga/effects'
// 引入 axios 请求接口数据
import axios from 'axios'

// 捕捉指令后要执行的函数
function* loaderData() {
  // 获取数据
  const {data} = yield axios.get('http://localhost:3005/api/todos')
  // 调用 put 方法将数据传递给其他指令
  yield put({type: 'getTodos_success', payload: data})
}

// 添加任务指令拦截后执行的函数
function* addTodos(action) {
  // 调用接口添加任务
  const {data} = yield axios.post('http://localhost:3005/api/todos', {taskName: action.payload})
  // 将接口返回的数据交给下一个指令处理
  yield put({type: 'addTodos_success', payload: data.task})
}

// 删除任务指令拦截后执行的函数
function* removTodos(action) {
  // 调用接口根据 ID 删除任务
  const {data} = yield axios.delete('http://localhost:3005/api/todos', { params: {id: action.payload}})
  // 调用另一个指令,把要删除的 id 传递过去
  yield put({type: 'removTodos_success', payload: data.tasks.id})
}

// 捕捉修改状态指令后的回调函数
function* modifyTodos(action) {
  // 调用接口修改状态
  const {data} = yield axios.put('http://localhost:3005/api/todos/isCompleted', action.payload)
  // 调用另一个指令,把接口返回的数据传递给下一个指令
  yield put({type: 'modifyTodos_success', payload: data.task})
}


// 捕捉 clearCompletedTodos 指令后的回调函数
function* clearCompletedTodos() {
  yield axios.delete('http://localhost:3005/api/todos/clearCompleted')
  yield put({type: 'clearCompletedTodos_success'})
}

// 任务编辑状态修改指令拦截后回调
function* modifyEditing(action) {
  yield axios.put('http://localhost:3005/api/todos/isEditing', action.payload)
  yield put({type: 'modifyEditing_success', payload: action.payload})
}

// 修改保存新任务名回调函数
function* saveName(action) {
  yield axios.put('http://localhost:3005/api/todos', action.payload)
  yield put({type: 'saveName_success', payload: action.payload})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
  // 捕捉 removTodos 指令,捕捉后执行 removTodos 方法
  yield takeEvery('removTodos', removTodos)
  // 捕捉 modifyTodos 指令,然后调用 modifyTodos 方法
  yield takeEvery('modifyTodos', modifyTodos)
  // 捕捉 clearCompletedTodos 指令,然后调用 clearCompletedTodos 方法
  yield takeEvery('clearCompletedTodos', clearCompletedTodos)
  // 捕捉任务编辑状态修改指令
  yield takeEvery('modifyEditing', modifyEditing)
  // 补货修改保存新任务名指令
  yield takeEvery('saveName', saveName)
}
// src/Store/Reducers/todos.reducer.js  书写新指令修改数据逻辑

// 引入 handleActions 简化书写,注意这里是 handleActions 而不是 handleAction
import {handleActions} from 'redux-actions'
// 引入实际更改数据的指令,中间件saga拦截的指令就不需要引入了
import {getTodos_success, addTodos_success, removTodos_success, modifyTodos_success, screenTodos, clearCompletedTodos_success, modifyEditing_success, saveName_success} from '../Actions/todos.action'

// 原始数据
const data = {
  // 空的列表
  todos: [],
  // 添加一个参数,作为筛选关键词,默认为all(全部)
  show: 'all'
}

// 创建指令函数,修改下面全部 return 的数据,将筛选关键词也传递下去
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: (state, action) => ({todos: action.payload, show: state.show}),
  // 添加任务
  [addTodos_success]: (state, action) => ({todos: [...state.todos, action.payload], show: state.show}),
  // 删除任务
  [removTodos_success]: (state, action) => {
    // 根据传递过来的 ID 获得在任务列表中的 下标
    const index = state.todos.findIndex(item => item.id === action.payload)
    // 深拷贝任务列表
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 从任务列表中删除指定的任务
    todos.splice(index, 1)
    // 用修改后的 todos 替换掉原本的 todos
    return { todos, show: state.show }
  },
  // 修改状态
  [modifyTodos_success]: (state, action) => {
    // 根据传递过来的 ID 获得在任务列表中的 下标
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝任务列表
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 从任务列表中替换指定位置数据
    todos[index] = action.payload
    // 用修改后的 todos 替换掉原本的 todos
    return { todos, show: state.show }
  },
  // 筛选
  [screenTodos]: (state, action) => {
    // 修改关键词并返回
    return {...state, show: action.payload}
  },
  // 清除已完成任务
  [clearCompletedTodos_success]: (state, action) => {
    // 筛选出全部未完成任务组成新数组
    const todos = state.todos.filter(item => !item.isCompleted)
    return {...state, todos}
  },
  // 修改编辑状态
  [modifyEditing_success]: (state, action) => {
    // 获取要修改的任务索引值
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 修改
    todos[index].isEditing = action.payload.isEditing
    // 替换掉原来的数据
    return {...state, todos}
  },
  // 修改保存新任务名
  [saveName_success]: (state, action) => {
    // 获取要修改的任务索引值
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 修改
    todos[index].taskName = action.payload.taskName
    // 替换掉原来的数据
    return {...state, todos}
  }
}, data)

// 导出
export default todosReducer
// src/Components/Main.js  组件编辑状态下失去焦点保存书写事件函数

import {Component} from 'react'
// 引入 bindActionCreators 用来自动生成 action 指令函数
import {bindActionCreators} from 'redux'
// connect 获取数据并添加指令方法
import {connect} from 'react-redux'
// 获取全部指令
import * as todoAction from '../Store/Actions/todos.action'

class Main extends Component {
	// 生命周期:组件挂载后
	componentDidMount() {
		// 调用组件的指令函数 getTodos() 获取数据并让 props 拿到
		this.props.getTodos()
	}
	// 删除任务函数
	removTodos(id) {
		// 把要删除的任务的 id 传递给指令
		this.props.removTodos(id)
	}
	modifyTodos(ev, id) {
		// 把要修改状态的任务需要的数据传递过去(要修改为的状态, id值)
		this.props.modifyTodos({id, isCompleted: ev.target.checked})
	}
	// 修改保存新任务名
	saveEdit(data) {
		// 触发指令修改编辑状态
		this.props.modifyEditing({id: data.id, isEditing: false})
		// 触发指令保存新名称
		this.props.saveName(data)
	}
  render() {
    return (
      <section className="main">
				<input className="toggle-all" type="checkbox" />
				<ul className="todo-list">
					{this.props.todos.map(item => {
						return (
							// 遍历全部的待办事项列表,注意这里设置一下是否添加 completed 类名,如果已完成添加,未完成不添加,并且添加一个判断是否编辑的新条件
							<li key={item.id} className={(item.isCompleted ? 'complete' : '') + (item.isEditing ? 'editing' : '')}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中,添加状态修改事件 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} onChange={(ev) => this.modifyTodos(ev, item.id)} />
									{/* 添加双击事件,直接触发指令,传递两个参数,其中 isEditing 设置为固定值 */}
									<label onDoubleClick={() => this.props.modifyEditing({id: item.id, isEditing: true})}>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								{/* 编辑状态下默认value值为当前任务名,失去焦点后调用方法保存新名称 */}
								<input className="edit" defaultValue={item.taskName} onBlur={(ev) => this.saveEdit({id: item.id, taskName: ev.target.value})}/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	// 参数调用自定义的函数,筛选需要的数据然后使用
	todos: filterTodos(store.todosReducer.todos, store.todosReducer.show)
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

// 筛选函数
// 参数1:要筛选的数据,参数2:筛选关键词
function filterTodos(todos, text) {
	// 判断筛选关键词
	switch (text) {
		// 关键词为all直接返回全部数据
		case 'all':
			return todos
		// 关键词为active筛选出所有未完成
		case 'active':
			return todos.filter(item => !item.isCompleted)
		// 关键词为completed筛选出所有已完成
		case 'completed':
			return todos.filter(item => item.isCompleted)
		default:
			return
	}
}

// 导出并调用 connect 注入数据和指令
export default connect(myData, myAction)(Main)

实例代码优化

为了避免之后修改请求地址前缀方便,将前缀单独抽离出来

利用相应拦截器将需要使用的数据直接返回(老师使用了then,而我使用的是对象解构)

可以将reducer的函数进行单独提取,这样更易读

// src/Store/axios.js  设置URL地址前缀和响应拦截器

// 引入axios
import axios from 'axios'

// 设置默认的前缀
axios.defaults.baseURL = 'http://localhost:3005/api'

// 设置响应拦截器直接输出data,就可以在saga内部直接使用
// 因为我使用的对象解构方式,所以这里不使用了
// axios.interceptors.response.use(res => res.data)
// src/Store/index.js  引入并直接运行

// 引入 createStore 创建仓库,applyMiddleware 创建中间件
import {createStore, applyMiddleware} from 'redux'
// 引入中间件 saga
import reduxSage from 'redux-saga'
// 引入仓库需要使用的 reducer
import reducer from './Reducers'
// 引入需要使用 saga
import todoSaga from './Sagas/todos.saga'
// 引入并直接运行axios设置
import './axios'

// 创建 saga
const saga = reduxSage()
// 创建 store 数据仓库,并创建中间件
const store = createStore(reducer, applyMiddleware(saga))
// saga 调用 run 方法使 saga 生效
saga.run(todoSaga)

export default store
// src/Store/Sagas/todos.saga.js  修改全部接口删除前缀

// 引入 saga 的两个方法,takeEvery 捕捉指令并拦截,put将指令传递给 store
import {takeEvery, put} from 'redux-saga/effects'
// 引入 axios 请求接口数据
import axios from 'axios'

// 捕捉指令后要执行的函数
function* loaderData() {
  // 获取数据
  const {data} = yield axios.get('/todos')
  // 调用 put 方法将数据传递给其他指令
  yield put({type: 'getTodos_success', payload: data})
}

// 添加任务指令拦截后执行的函数
function* addTodos(action) {
  // 调用接口添加任务
  const {data} = yield axios.post('/todos', {taskName: action.payload})
  // 将接口返回的数据交给下一个指令处理
  yield put({type: 'addTodos_success', payload: data.task})
}

// 删除任务指令拦截后执行的函数
function* removTodos(action) {
  // 调用接口根据 ID 删除任务
  const {data} = yield axios.delete('/todos', { params: {id: action.payload}})
  // 调用另一个指令,把要删除的 id 传递过去
  yield put({type: 'removTodos_success', payload: data.tasks.id})
}

// 捕捉修改状态指令后的回调函数
function* modifyTodos(action) {
  // 调用接口修改状态
  const {data} = yield axios.put('/todos/isCompleted', action.payload)
  // 调用另一个指令,把接口返回的数据传递给下一个指令
  yield put({type: 'modifyTodos_success', payload: data.task})
}


// 捕捉 clearCompletedTodos 指令后的回调函数
function* clearCompletedTodos() {
  yield axios.delete('/todos/clearCompleted')
  yield put({type: 'clearCompletedTodos_success'})
}

// 任务编辑状态修改指令拦截后回调
function* modifyEditing(action) {
  yield axios.put('/todos/isEditing', action.payload)
  yield put({type: 'modifyEditing_success', payload: action.payload})
}

// 修改保存新任务名回调函数
function* saveName(action) {
  yield axios.put('/todos', action.payload)
  yield put({type: 'saveName_success', payload: action.payload})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
  // 捕捉 removTodos 指令,捕捉后执行 removTodos 方法
  yield takeEvery('removTodos', removTodos)
  // 捕捉 modifyTodos 指令,然后调用 modifyTodos 方法
  yield takeEvery('modifyTodos', modifyTodos)
  // 捕捉 clearCompletedTodos 指令,然后调用 clearCompletedTodos 方法
  yield takeEvery('clearCompletedTodos', clearCompletedTodos)
  // 捕捉任务编辑状态修改指令
  yield takeEvery('modifyEditing', modifyEditing)
  // 补货修改保存新任务名指令
  yield takeEvery('saveName', saveName)
}
// src/Store/Reducers/todos.reducer.js  将全部saga没有拦截指令的逻辑单独封装

// 引入 handleActions 简化书写,注意这里是 handleActions 而不是 handleAction
import {handleActions} from 'redux-actions'
// 引入实际更改数据的指令,中间件saga拦截的指令就不需要引入了
import {getTodos_success, addTodos_success, removTodos_success, modifyTodos_success, screenTodos, clearCompletedTodos_success, modifyEditing_success, saveName_success} from '../Actions/todos.action'

// 原始数据
const data = {
  // 空的列表
  todos: [],
  // 添加一个参数,作为筛选关键词,默认为all(全部)
  show: 'all'
}

// 将全部指令函数逻辑单独封装
const getTodos = (state, action) => ({todos: action.payload, show: state.show})
const addTodos = (state, action) => ({todos: [...state.todos, action.payload], show: state.show})
const removTodos = (state, action) => {
  // 根据传递过来的 ID 获得在任务列表中的 下标
  const index = state.todos.findIndex(item => item.id === action.payload)
  // 深拷贝任务列表
  const todos = JSON.parse(JSON.stringify(state.todos))
  // 从任务列表中删除指定的任务
  todos.splice(index, 1)
  // 用修改后的 todos 替换掉原本的 todos
  return { todos, show: state.show }
}
const modifyTodos = (state, action) => {
  // 根据传递过来的 ID 获得在任务列表中的 下标
  const index = state.todos.findIndex(item => item.id === action.payload.id)
  // 深拷贝任务列表
  const todos = JSON.parse(JSON.stringify(state.todos))
  // 从任务列表中替换指定位置数据
  todos[index] = action.payload
  // 用修改后的 todos 替换掉原本的 todos
  return { todos, show: state.show }
}
const screenTodosData = (state, action) => {
    // 修改关键词并返回
    return {...state, show: action.payload}
  }
const clearCompletedTodos = (state, action) => {
    // 筛选出全部未完成任务组成新数组
    const todos = state.todos.filter(item => !item.isCompleted)
    return {...state, todos}
  }
const modifyEditing = (state, action) => {
    // 获取要修改的任务索引值
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 修改
    todos[index].isEditing = action.payload.isEditing
    // 替换掉原来的数据
    return {...state, todos}
  }
const saveName = (state, action) => {
    // 获取要修改的任务索引值
    const index = state.todos.findIndex(item => item.id === action.payload.id)
    // 深拷贝
    const todos = JSON.parse(JSON.stringify(state.todos))
    // 修改
    todos[index].taskName = action.payload.taskName
    // 替换掉原来的数据
    return {...state, todos}
  }


// 创建指令函数,修改下面全部 return 的数据,将筛选关键词也传递下去
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: getTodos,
  // 添加任务
  [addTodos_success]: addTodos,
  // 删除任务
  [removTodos_success]: removTodos,
  // 修改状态
  [modifyTodos_success]: modifyTodos,
  // 筛选
  [screenTodos]: screenTodosData,
  // 清除已完成任务
  [clearCompletedTodos_success]: clearCompletedTodos,
  // 修改编辑状态
  [modifyEditing_success]: modifyEditing,
  // 修改保存新任务名
  [saveName_success]: saveName
}, data)

// 导出
export default todosReducer

immutable使用

安装:npm install immutable

react中如果直接修改state中的数据,界面是不会更新的,所以我们每次修改的时候都需要对数据进行一次深拷贝,比较麻烦而且也不属于我们的业务逻辑

immutable是facebook推出的工具库,会把数据变成一个不可变的对象,避免误操作也不可打点调用,想修改书需要使用内部方法,其中常用的方法有:

  • fromJS(数据对象):可将对象变为不可变对象
  • setIn(修改数据,[修改属性],修改值):修改数据
  • getIn(读取数据,[读取属性]):使用数据
  • mergeDeep(修改数据,合并的属性:合并的值):合并/添加数据
  • removeIn(修改数据,[要删除的数据,删除索引]):删除数据(可以使用getIn获取索引)
  • updateIn(修改数据,[要修改数据,索引值],更新的值(函数返回)):替换/更新数据

注意:不能使用打点访问数据,需要使用getIn()

当一些数据无法满足方法的时候可能需要修改saga传递过来的数据

// src/Store/Reducers/todos.reducer.js  使用immutable方法改造

// 引入 handleActions 简化书写,注意这里是 handleActions 而不是 handleAction
import {handleActions} from 'redux-actions'
// 引入实际更改数据的指令,中间件saga拦截的指令就不需要引入了
import {getTodos_success, addTodos_success, removTodos_success, modifyTodos_success, screenTodos, clearCompletedTodos_success, modifyEditing_success, saveName_success} from '../Actions/todos.action'
// 引入 immutable 相关方法
import { fromJS, setIn, mergeDeep, getIn, removeIn, updateIn } from 'immutable'

// 原始数据
const data = fromJS({
  // 空的列表
  todos: [],
  // 添加一个参数,作为筛选关键词,默认为all(全部)
  show: 'all'
})

// 将全部指令函数逻辑单独封装
// 获取任务列表,使用 setIn 修改
const getTodos = (state, action) => setIn(state, ['todos'], action.payload)
// 添加任务,使用 mergeDeep 合并
const addTodos = (state, action) => mergeDeep(state, {todos: [action.payload]})
// 删除任务
const removTodos = (state, action) => {
  // 获取索引值,使用getIn
  const index = getIn(state, ['todos']).findIndex(item => item.id === action.payload)
  // 使用 removeIn 删除
  return removeIn(state, ['todos', index])
}
// 修改任务状态
const modifyTodos = (state, action) => {
  // 获取索引值,使用getIn
  const index = getIn(state, ['todos']).findIndex(item => item.id === action.payload.id)
  // 使用 updateIn 修改/更新
  return updateIn(state, ['todos', index], () => action.payload)
}
// 过滤任务,使用setIn修改
const screenTodosData = (state, action) => setIn(state, ['show'], action.payload)
// 清除全部已完成任务
const clearCompletedTodos = (state, action) => {
  // 获取全部未完成任务
  const todos = getIn(state, ['todos']).filter(item => !item.isCompleted)
  // 使用 setIn 全部修改
  return setIn(state, ['todos'], todos)
}
// 修改编辑状态
const modifyEditing = (state, action) => {
    // 获取索引值,使用getIn
    const index = getIn(state, ['todos']).findIndex(item => item.id === action.payload.id)
    // 使用 updateIn 修改/更新
    return updateIn(state, ['todos', index], () => action.payload)
  }
// 保存新名称
const saveName = (state, action) => {
    // 获取索引值,使用getIn
    const index = getIn(state, ['todos']).findIndex(item => item.id === action.payload.id)
    // 使用 updateIn 修改/更新
    return updateIn(state, ['todos', index], () => action.payload)
  }


// 创建指令函数,修改下面全部 return 的数据,将筛选关键词也传递下去
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: getTodos,
  // 添加任务
  [addTodos_success]: addTodos,
  // 删除任务
  [removTodos_success]: removTodos,
  // 修改状态
  [modifyTodos_success]: modifyTodos,
  // 筛选
  [screenTodos]: screenTodosData,
  // 清除已完成任务
  [clearCompletedTodos_success]: clearCompletedTodos,
  // 修改编辑状态
  [modifyEditing_success]: modifyEditing,
  // 修改保存新任务名
  [saveName_success]: saveName
}, data)

// 导出
export default todosReducer
// src/Store/Sagas/todos.saga.js  有些数据不满足要求可能会修改一下saga传递过来的数据

// 引入 saga 的两个方法,takeEvery 捕捉指令并拦截,put将指令传递给 store
import {takeEvery, put} from 'redux-saga/effects'
// 引入 axios 请求接口数据
import axios from 'axios'

// 捕捉指令后要执行的函数
function* loaderData() {
  // 获取数据
  const {data} = yield axios.get('/todos')
  // 调用 put 方法将数据传递给其他指令
  yield put({type: 'getTodos_success', payload: data})
}

// 添加任务指令拦截后执行的函数
function* addTodos(action) {
  // 调用接口添加任务
  const {data} = yield axios.post('/todos', {taskName: action.payload})
  // 将接口返回的数据交给下一个指令处理
  yield put({type: 'addTodos_success', payload: data.task})
}

// 删除任务指令拦截后执行的函数
function* removTodos(action) {
  // 调用接口根据 ID 删除任务
  const {data} = yield axios.delete('/todos', { params: {id: action.payload}})
  // 调用另一个指令,把要删除的 id 传递过去
  yield put({type: 'removTodos_success', payload: data.tasks.id})
}

// 捕捉修改状态指令后的回调函数
function* modifyTodos(action) {
  // 调用接口修改状态
  const {data} = yield axios.put('/todos/isCompleted', action.payload)
  // 调用另一个指令,把接口返回的数据传递给下一个指令
  yield put({type: 'modifyTodos_success', payload: data.task})
}


// 捕捉 clearCompletedTodos 指令后的回调函数
function* clearCompletedTodos() {
  yield axios.delete('/todos/clearCompleted')
  yield put({type: 'clearCompletedTodos_success'})
}

// 任务编辑状态修改指令拦截后回调
function* modifyEditing(action) {
  const { data } = yield axios.put('/todos/isEditing', action.payload)
  yield put({type: 'modifyEditing_success', payload: data.task})
}

// 修改保存新任务名回调函数
function* saveName(action) {
  const { data } = yield axios.put('/todos', action.payload)
  yield put({type: 'saveName_success', payload: data.task})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
  // 捕捉 removTodos 指令,捕捉后执行 removTodos 方法
  yield takeEvery('removTodos', removTodos)
  // 捕捉 modifyTodos 指令,然后调用 modifyTodos 方法
  yield takeEvery('modifyTodos', modifyTodos)
  // 捕捉 clearCompletedTodos 指令,然后调用 clearCompletedTodos 方法
  yield takeEvery('clearCompletedTodos', clearCompletedTodos)
  // 捕捉任务编辑状态修改指令
  yield takeEvery('modifyEditing', modifyEditing)
  // 补货修改保存新任务名指令
  yield takeEvery('saveName', saveName)
}
// src/Components/Main.js  组件使用immutable改造

import {Component} from 'react'
// 引入 bindActionCreators 用来自动生成 action 指令函数
import {bindActionCreators} from 'redux'
// connect 获取数据并添加指令方法
import {connect} from 'react-redux'
// 获取全部指令
import * as todoAction from '../Store/Actions/todos.action'
// 引入immutable
import { getIn } from 'immutable'

class Main extends Component {
	// 生命周期:组件挂载后
	componentDidMount() {
		// 调用组件的指令函数 getTodos() 获取数据并让 props 拿到
		this.props.getTodos()
	}
	// 删除任务函数
	removTodos(id) {
		// 把要删除的任务的 id 传递给指令
		this.props.removTodos(id)
	}
	modifyTodos(ev, id) {
		// 把要修改状态的任务需要的数据传递过去(要修改为的状态, id值)
		this.props.modifyTodos({id, isCompleted: ev.target.checked})
	}
	// 修改保存新任务名
	saveEdit(data) {
		// 触发指令修改编辑状态
		this.props.modifyEditing({id: data.id, isEditing: false})
		// 触发指令保存新名称
		this.props.saveName(data)
	}
  render() {
    return (
      <section className="main">
				<input className="toggle-all" type="checkbox" />
				<ul className="todo-list">
					{this.props.todos.map(item => {
						return (
							// 遍历全部的待办事项列表,注意这里设置一下是否添加 completed 类名,如果已完成添加,未完成不添加,并且添加一个判断是否编辑的新条件
							<li key={item.id} className={(item.isCompleted ? 'completed' : '') + ' ' + (item.isEditing ? 'editing' : '')}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中,添加状态修改事件 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} onChange={(ev) => this.modifyTodos(ev, item.id)} />
									{/* 添加双击事件,直接触发指令,传递两个参数,其中 isEditing 设置为固定值 */}
									<label onDoubleClick={() => this.props.modifyEditing({id: item.id, isEditing: true})}>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								{/* 编辑状态下默认value值为当前任务名,失去焦点后调用方法保存新名称 */}
								<input className="edit" defaultValue={item.taskName} onBlur={(ev) => this.saveEdit({id: item.id, taskName: ev.target.value})}/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	// 参数调用自定义的函数,筛选需要的数据然后使用,使用immutable的getIn方法
	todos: filterTodos(getIn(store.todosReducer, ['todos']), getIn(store.todosReducer, ['show']))
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

// 筛选函数
// 参数1:要筛选的数据,参数2:筛选关键词
function filterTodos(todos, text) {
	// 判断筛选关键词
	switch (text) {
		// 关键词为all直接返回全部数据
		case 'all':
			return todos
		// 关键词为active筛选出所有未完成
		case 'active':
			return todos.filter(item => !item.isCompleted)
		// 关键词为completed筛选出所有已完成
		case 'completed':
			return todos.filter(item => item.isCompleted)
		default:
			return
	}
}

// 导出并调用 connect 注入数据和指令
export default connect(myData, myAction)(Main)
// src/Components/Footer.js  组件使用immutable改造

import {Component} from 'react'
// 引入 redux 相关组件
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as todoAction from '../Store/Actions/todos.action'
// 引入immutable
import { getIn } from 'immutable'

class Footer extends Component {
	// 未完成任务统计方法
	count(array) {
		// 筛选全部未完成任务组成数组,并且返回该数组长度
		return array.filter(item => !item.isCompleted).length
	}
	// 三个筛选按钮点击事件
	filters(text){
		// 触发筛选指令,将参数传递过去
		this.props.screenTodos(text)
	}
	// 清除已完成点击事件
	clearCompleted() {
		// 触发指令,清除已完成不需要传递参数
		this.props.clearCompletedTodos()
	}
  render() {
    return (
      <footer className="footer">
				<span className="todo-count">
					{/* 未完成任务统计,调用方法 */}
					<strong>{this.count(this.props.todos)}</strong> 未完成
				</span>
				<ul className="filters">
					<li>
						{/* 添加点击筛选事件 */}
						<span onClick={() => this.filters('all')}>全部</span>
					</li>
					<li>
						{/* 添加点击筛选事件 */}
						<span onClick={() => this.filters('active')}>未完成</span>
					</li>
					<li>
						{/* 添加点击筛选事件 */}
						<span onClick={() => this.filters('completed')}>已完成</span>
					</li>
				</ul>
				{/* 按钮添加点击事件 */}
				<button className="clear-completed" onClick={() => this.clearCompleted()} >清除已完成</button>
			</footer>
    )
  }
}

// 数据
const myData = store => ({
	// 使用immutable的getIn方法
	todos: getIn(store.todosReducer, ['todos'])
})
// 指令
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

export default connect(myData, myAction)(Footer)
// src/Components/Header.js  组件使用immutable改造

import {Component} from 'react'
// 引入redux需要使用的方法和指令
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as todoAction from '../Store/Actions/todos.action'
// 引入immutable

import { getIn } from 'immutable'

class Header extends Component {
  // input标签事件函数
  addTodos(ev) {
    // 01、判断抬起的案件是不是回车键
    if(ev.keyCode === 13) {
      // 02、如果是回车键获取内容并去除两侧空白
      const value = ev.target.value.trim()
      // 03、判断去除空白后的结果是为空
      if (value.length > 0) {
        // 04、如果有内容。把数据传递给指令方法
        this.props.addTodos(value)
      }
      // 05、只要抬起回车键就清空内容,不管是不是为空
      ev.target.value = ''
    }
  }
  render() {
    return (
      <header className="header">
        <h1>todos</h1>
        {/* 给 input 添加键盘抬起事件 */}
        <input className="new-todo" placeholder="还有什么任务没有完成?" autoFocus onKeyUp={(ev) => this.addTodos(ev)} />
      </header>
    )
  }
}

// redux数据
const myData = store => ({
  // 使用immutable的getIn方法
  todos: getIn(store.todosReducer, ['todos'])
})
// 指令
const myAction = dispatch => ({
  ...bindActionCreators(todoAction, dispatch)
})
export default connect(myData, myAction)(Header)