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

491 阅读22分钟

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

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

todolist案例

初始化项目

  • 新建react项目
  • 对自动创建的react项目进行初始化,删除不需要文件并修改相应内容

组件拆分

  • 老师给了一份静态的HTML怠慢,结构可以直接拷贝
  • 分为header、main和footer三个组件
  • 样式文件也直接使用老师给的css文件,使用全局引入即可
需要创建的目录结构如下:

src目录
- Comprnents --------------------- 组件目录
  - Header.js
  - Main.js
  - Footer.js
// src/index.js  引入全局样式

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
// 引入全局Css样式文件
import './css/index.css'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
// src/App.js  引入三个组件并使用

import {Component} from 'react'
// 引入三个子组件
import Header from './Components/Header'
import Main from './Components/Main'
import Footer from './Components/Footer'

class App extends Component {
  render() {
    return (
      <div className="todoapp">
        {/* 使用子组件 */}
        <Header />
        <Main />
        <Footer />
      </div>
    )
  }
}
export default App
//src/Components/Header.js  Header组件

import {Component} from 'react'

class Header extends Component {
  render() {
    return (
      <header className="header">
        <h1>todos</h1>
        <input className="new-todo" placeholder="还有什么任务没有完成?" autoFocus />
      </header>
    )
  }
}
export default Header
// src/Components/Main.js  Main组件

import {Component} from 'react'

class Main extends Component {
  render() {
    return (
      <section className="main">
				<input className="toggle-all" type="checkbox" />
				<ul className="todo-list">
					<li className="completed">
						<div className="view">
							<input className="toggle" type="checkbox" checked readOnly/>
							<label>品味JavaScript</label>
							<button className="destroy"></button>
						</div>
						<input className="edit" value="Create a TodoMVC template" readOnly/>
					</li>
					<li>
						<div className="view">
							<input className="toggle" type="checkbox" readOnly />
							<label>买一个宠物</label>
							<button className="destroy"></button>
						</div>
						<input className="edit" value="Rule the web" readOnly/>
					</li>
				</ul>
			</section>
    )
  }
}
export default Main
// src/Components/Footer.js  Footer组件

import {Component} from 'react'

class Footer extends Component {
  render() {
    return (
      <footer className="footer">
				<span className="todo-count">
					<strong>0</strong> item left
				</span>
				<ul className="filters">
					<li>
						<span>All</span>
					</li>
					<li>
						<span>Active</span>
					</li>
					<li>
						<span>Completed</span>
					</li>
				</ul>
				<button className="clear-completed">Clear completed</button>
			</footer>
    )
  }
}
export default Footer

任务列表显示

  • 这里使用redux创建store管理数据,获取数据使用老师提供的接口
  • 先书写指令action - 使用redux-actions简化代码
    • 发送请求获取数据
    • 异步操作结束后触发新指令
  • 然后书写reducer - 同样适用redux-actions简化代码 - 注意reducer需要进行拆分合并
    • 默认给一个空数组,当异步请求执行后,把数据添加给空数组,然后组件获取数据就可以了
  • 将store的创建、中间件saga单独创建一个文件,放在store下面的index.js中
  • 将saga异步获取数据和触发另一个指令单独书写在saga目录中,注意老师这里书写的指令名称使用的action输出的
  • main组件中调用数据,设置好dispatch
  • 在组件挂载完成后获取数据,将数据交给props
需要创建的目录结构如下:

src目录
- Comprnents --------------------- 组件目录
  - Header.js
  - Main.js
  - Footer.js
- store -------------------------- redux数据管理目录
  - Actions ---------------------- 指令目录
    - todos.action.js
  - Reducers --------------------- reducer目录
    - index.js 
    - todos.reducer.js 
  - Saga ------------------------- redux-saga目录
    - todos.saga.js
  - index ------------------------ store创建和saga文件
  
// src/Store/Actions/todos.action.js  todos的指令

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

// 导出action
export const getTodos = createAction('getTodos')
export const addTodos = createAction('addTodos')
//src/Store/Reducers/todos.reducer.js  设置todos的数据和指令函数

// 引入 handleActions 简化书写,注意这里是 handleActions 而不是 handleAction
import {handleActions} from 'redux-actions'
// 引入实际更改数据的指令,另一个 getData 不能直接更改数据就不需要引入了
import {addTodos} from '../Actions/todos.action'

// 原始数据
const data = {
  // 空的列表
  todos: []
}

// 创建指令函数
const todosReducer = handleActions({
  [addTodos]: (state, action) => ({todos: action.payload})
}, data)

// 导出
export default todosReducer
// src/Store/Reducers/index.js  合并并导出reducer

// 引入 combineReducers 用来合并 reducer
import {combineReducers} from 'redux'
// 引入需要合并的 reducer
import todosReducer from './todos.reducer'

// 执行合并并导出
export default combineReducers({
  todosReducer: todosReducer
})
// src/Store/index.js  仓库的创建、中间件创建、saga倒入单独书写在一个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'

// 创建 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  todos的saga中间件

// 引入 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: 'addTodos', payload: data})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
}
// src/Components/Main.js  main组件使用指令和数据

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()
	}
  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" : ''}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted === true} readOnly/>
									<label>{item.taskName}</label>
									<button className="destroy"></button>
								</div>
								<input className="edit" value="Create a TodoMVC template" readOnly/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	todos: store.todosReducer.todos
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

// 导出并调用 connect 注入数据和指令
export default connect(myData, myAction)(Main)
//src/index.js  将之前创建的 store 传递给 app

import React from 'react'
import ReactDOM from 'react-dom'
// 引入 Provider 将 store 向后传递
import {Provider} from 'react-redux'
import App from './App'
// 引入全局Css样式文件
import './css/index.css'
// 引入创建的 store
import store from './Store/index'

ReactDOM.render(
  <React.StrictMode>
    {/* 传递store */}
    <Provider store={store}><App /></Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

为了统一指令名称,这里把addTodos改名为getTodos_success,后面也会使用_success作为后缀成对命名指令

添加任务实现

添加任务在header组件中实现

  • 抬起键盘的时候判断是否是回车键(keyCode=13),如果是回车键调用后端接口添加任务
    • 注意这时需要判断文本框内容是否为空,注意去掉两侧空白 - trim(),为空弹出提示要求填入
  • 添加任务依旧使用指令完成,组件把添加的数据传递给指令,注意想要使用指令就要对组件进行redux处理
  • 中间件添加依旧交给saga完成,saga可以获取到拦截的指令信息,调用接口后接口会把要添加的数据返回给我们,我们把返回的诗句传给下一个指令
  • 在reducre中使用指令进行添加即可
  • 最后清空文本框
// 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')
// 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('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})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
}
// src/Store/Reducers/todos.reducer.js  数据中真正的添加任务

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

// 原始数据
const data = {
  // 空的列表
  todos: []
}

// 创建指令函数
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: (state, action) => ({todos: action.payload}),
  // 添加任务
  [addTodos_success]: (state, action) => ({todos: [...state.todos, action.payload]})
}, data)

// 导出
export default todosReducer
// src/Components/Header.js  Header组件中使用redux

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

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 => ({
  todos: store.todosReducer.todos
})
// 指令
const myAction = dispatch => ({
  ...bindActionCreators(todoAction, dispatch)
})
export default connect(myData, myAction)(Header)

任务删除实现

删除的实现在main组件中实现

  • 点击删除按钮的时候获取要删除的任务的ID值
  • 在真正删除任务之前我们给一个确认步骤(confirm())
  • 删除依旧使用指令完成,配合saga中间件,将要删除的id传递给指令
    • saga拦截指令后调用后台接口执行删除,接口会吧要删除的数据返回给我们
  • 在reducer中利用这个id使用findIndex方法查找所对应的索引值
  • 使用深拷贝拷贝任务数组,然后删除指定任务最后替换原来的数组(因为我们不能直接修改列表)
// 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')
// 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('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})
}

// 导出 saga
export default function* getTodos() {
  // 捕捉 getTodos 指令,捕捉到之后执行 loaderData 方法
  yield takeEvery('getTodos', loaderData)
  // 捕捉a ddTodos 指令,捕捉后执行 addTodos 方法
  yield takeEvery('addTodos', addTodos)
  // 捕捉 removTodos 指令,捕捉后执行 removTodos 方法
  yield takeEvery('removTodos', removTodos)
}
// 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)
	}
  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" : ''}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} readOnly/>
									<label>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								<input className="edit" value="Create a TodoMVC template" readOnly/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	todos: store.todosReducer.todos
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

// 导出并调用 connect 注入数据和指令
export default connect(myData, myAction)(Main)
// src/Store/Reducers/todos.reducer.js  修改真正的数据

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

// 原始数据
const data = {
  // 空的列表
  todos: []
}

// 创建指令函数
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: (state, action) => ({todos: action.payload}),
  // 添加任务
  [addTodos_success]: (state, action) => ({todos: [...state.todos, action.payload]}),
  // 删除任务
  [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 }
  }
}, data)

// 导出
export default todosReducer

修改任务状态

状态修改在main组件中实现

  • 任务状态取决于任务的 isCompleted属性,所以我们修改状态操作主要需要操作的就是这个属性
  • 点击复选框按钮的时候获取当前任务id值
  • 然后依旧使用指令功能
  • saga拦截指令,进行异步请求,请求后后台会吧要修改状态的任务返回给我们
  • 把要删除任务信息传递给下一个指令
  • 在reducer中/进行数据修改操作,同样需要深拷贝,修改完之后替换掉现有数据
// 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')
// 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('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})
}

// 导出 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)
}
// src/Store/Reducers/todos.reducer.js 书写真正修改数据指令代码

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

// 原始数据
const data = {
  // 空的列表
  todos: []
}

// 创建指令函数
const todosReducer = handleActions({
  // 初始化数据
  [getTodos_success]: (state, action) => ({todos: action.payload}),
  // 添加任务
  [addTodos_success]: (state, action) => ({todos: [...state.todos, action.payload]}),
  // 删除任务
  [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 }
  },
  // 修改状态
  [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 }
  }
}, 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})
	}
  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" : ''}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中,添加状态修改事件 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} onChange={(ev) => this.modifyTodos(ev, item.id)} />
									<label>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								<input className="edit" value="Create a TodoMVC template" readOnly/>
							</li>
						)
					})}
				</ul>
			</section>
    )
  }
}
// connect第一个参数 - 数据
const myData = store => ({
	todos: store.todosReducer.todos
})
// connect第二个参数 - 指令函数
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

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

计算未完成功能

计算功能在footer组件中实现

  • 组件拿到全部列表
  • 对列表进行过滤,筛选出全部未完成的任务进行统计即可
// src/Components/Footer.js 因为项目初始化会自动火球数据,所以footer直接使用即可

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

class Footer extends Component {
	// 未完成任务统计方法
	count(array) {
		// 筛选全部未完成任务组成数组,并且返回该数组长度
		return array.filter(item => !item.isCompleted).length
	}
  render() {
    return (
      <footer className="footer">
				<span className="todo-count">
					{/* 未完成任务统计,调用方法 */}
					<strong>{this.count(this.props.todos)}</strong> 未完成
				</span>
				<ul className="filters">
					<li>
						<span>All</span>
					</li>
					<li>
						<span>Active</span>
					</li>
					<li>
						<span>Completed</span>
					</li>
				</ul>
				<button className="clear-completed">Clear completed</button>
			</footer>
    )
  }
}

const myData = store => ({
	todos: store.todosReducer.todos
})
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})
export default connect(myData, myAction)(Footer)

任务筛选实现

筛选功能在footer组件中实现

  • 筛选不需要进行异步请求,所以可以不使用saga中间件处理
  • 需要考虑修改状态不能直接修改原始数据,否则切换的时候可能出现问题
  • 在数据上添加一个属性,用来表示当前数据筛选方式
  • 在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')
// src/Components/Footer.js  footer组件添加点击事件并且调用指令

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

class Footer extends Component {
	// 未完成任务统计方法
	count(array) {
		// 筛选全部未完成任务组成数组,并且返回该数组长度
		return array.filter(item => !item.isCompleted).length
	}
	// 三个筛选按钮点击事件
	filters(text){
		// 触发筛选指令,将参数传递过去
		this.props.screenTodos(text)
	}
  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')}>All</span>
					</li>
					<li>
						{/* 添加点击筛选事件 */}
						<span onClick={() => this.filters('active')}>Active</span>
					</li>
					<li>
						{/* 添加点击筛选事件 */}
						<span onClick={() => this.filters('completed')}>Completed</span>
					</li>
				</ul>
				<button className="clear-completed">Clear completed</button>
			</footer>
    )
  }
}

const myData = store => ({
	todos: store.todosReducer.todos
})
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})
export default connect(myData, myAction)(Footer)
// 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} 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}
  }
}, 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})
	}
  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" : ''}>
								<div className="view">
									{/* 是否选中复选框 - 根据数据判断是否选中,添加状态修改事件 */}
									<input className="toggle" type="checkbox" checked={item.isCompleted} onChange={(ev) => this.modifyTodos(ev, item.id)} />
									<label>{item.taskName}</label>
									{/* 添加点击方法删除任务 */}
									<button className="destroy" onClick={() => this.removTodos(item.id)}></button>
								</div>
								<input className="edit" value="Create a TodoMVC template" 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)

删除已完成任务

清除功能在footer组件中实现

  • 点击清除按钮的时候调用指令,需要二次确认
  • 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')
// 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('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'})
}

// 导出 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)
}
// 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} 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}
  }
}, data)

// 导出
export default todosReducer
// src/Components/Footer.js  Footer组件添加事件触发清除已完成

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

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 => ({
	todos: store.todosReducer.todos
})
// 指令
const myAction = dispatch => ({
	...bindActionCreators(todoAction, dispatch)
})

export default connect(myData, myAction)(Footer)