您现在的位置是:首页 >学无止境 >20230531----重返学习-redux总步骤-TaskOA-react路由管理方案react-router-dom网站首页学无止境

20230531----重返学习-redux总步骤-TaskOA-react路由管理方案react-router-dom

方朝端 2024-07-15 00:01:02
简介20230531----重返学习-redux总步骤-TaskOA-react路由管理方案react-router-dom

day-082-eighty-two-20230531-redux总步骤-TaskOA-react路由管理方案react-router-dom

redux总步骤

  1. 确定基础骨架目录。

    • fang/f20230531/src/store/index.js
    • fang/f20230531/src/store/reducers/
      • fang/f20230531/src/store/reducers/index.js
      • fang/f20230531/src/store/reducers/demoReducer.js
      • fang/f20230531/src/store/reducers/taskReducer.js
    • fang/f20230531/src/store/action-types.js
    • fang/f20230531/src/store/actions/
      • fang/f20230531/src/store/actions/index.js
      • fang/f20230531/src/store/actions/demoAction.js
      • fang/f20230531/src/store/actions/taskAction.js
  2. 编写基础骨架代码。

    1. 定义派发标识。

      • fang/f20230531/src/store/action-types.js 定义派发标识。

        export const DEMO_INCREMENT = `DEMO_INCREMENT`
        
    2. 创建基础reducer骨架。

      • fang/f20230531/src/store/reducers/demoReducer.js 创建基础reducer骨架。

        import * as AT from '../action-types'
        let initial={
        
        }
        
        export default function demoReducer(state=initial,action){
          state={...state}
          switch(action.type){
            //...
            default:
          }
          return state
        }
        
      • fang/f20230531/src/store/reducers/taskReducer.js 创建基础reducer骨架。

        import * as AT from '../action-types'
        let initial={
        
        }
        
        export default function taskReducer(state=initial,action){
          state={...state}
          switch(action.type){
            //...
            default:
          }
          return state
        }
        
    3. 编写具体reducer业务逻辑。

      • fang/f20230531/src/store/reducers/demoReducer.js 编写具体reducer业务逻辑。

        import * as AT from "../action-types";
        let initial = {
          num: 0,
        };
        
        export default function demoReducer(state = initial, action) {
          state = { ...state };
          let { type, step = 1 } = action;
          switch (type) {
            //...
            case AT.DEMO_INCREMENT:
              state.num += Number(step);
              break;
            default:
          }
          return state;
        }
        
        /* // 期望派发;
        dispatch({
          type:'DEMO_INCREMENT',
          step:10,
        }) */
        
    4. 合并reducer并导出。

      • fang/f20230531/src/store/reducers/index.js 合并reducer并导出。

        import {combineReducers} from 'redux'
        import demoReducer from './demoReducer'
        import taskReducer from './taskReducer'
        const reducer = combineReducers({
          demo:demoReducer,
          task:taskReducer,
        })
        export default reducer
        
    5. 创建redux公共容器。

      • fang/f20230531/src/store/index.js 创建redux公共容器。

        import { legacy_createStore } from "redux";
        import reducer from './reducers'
        const store = legacy_createStore(reducer)
        export default store
        
    6. 创建action的基础骨架。

      • fang/f20230531/src/store/actions/demoAction.js 创建action的基础骨架。

        import * as AT from '../action-types'
        const demoAction = {
        
        }
        export default demoAction
        
      • fang/f20230531/src/store/actions/taskAction.js 创建action的基础骨架。

        import * as AT from '../action-types'
        const taskAction = {
        
        }
        export default taskAction
        
    7. 修改具体action的派发逻辑。

      • fang/f20230531/src/store/actions/demoAction.js 修改具体action的派发逻辑。

        import * as AT from '../action-types'
        const demoAction = {
          increment(step=1){
            return {
              type:AT.DEMO_INCREMENT,
              step,
            }
          }
        }
        export default demoAction
        
    8. 合并action并导出。

      • fang/f20230531/src/store/actions/index.js 合并action并导出。

        import demoAction from "./demoAction";
        import taskAction from "./taskAction";
        const action = {
          demo:demoAction,
          task:taskAction,  
        }
        export default action
        
  3. 使用react-redux把公共状态容器放到根组件上下文中。

    • fang/f20230531/src/index.jsx 根组件创建上下文对象,并注入公共状态容器。

      // redux
      import store from './store'
      import {Provider} from 'react-redux'
      
      root.render(
          <Provider store={store}>
            //....
          </Provider>
      )
      
      import React from 'react'
      import ReactDOM from 'react-dom/client'
      /* ANTD */
      import { ConfigProvider } from 'antd'
      import dayjs from 'dayjs'
      import zhCN from 'antd/locale/zh_CN'
      import 'dayjs/locale/zh-cn'
      /* 组件&样式 */
      import './index.less'
      import Demo from './views/Demo'
      
      // redux
      import store from './store'
      import {Provider} from 'react-redux'
      
      dayjs.locale('zh-cn')
      
      const root = ReactDOM.createRoot(document.getElementById('root'))
      root.render(
        <ConfigProvider locale={zhCN}>
          <Provider store={store}>
            <Demo />
          </Provider>
        </ConfigProvider>
      )
      
  4. 业务组件从上下文对象中获取或使用公共状态容器中的公共状态。

    • fang/f20230531/src/views/Demo.jsx 单个业务组件使用。

      const Demo = function Demo(props) {
        let { num, increment } = props;
      };
      export default connect((state) => state.demo, action.demo)(Demo);
      
      import React from "react";
      import { Button } from "antd";
      import styled from "styled-components";
      import { connect } from "react-redux";
      import action from "@/store/actions";
      
      // 组件样式
      const DemoStyle = styled.div`
        box-sizing: border-box;
        margin: 20px auto;
        padding: 10px 20px;
        width: 200px;
        border: 1px solid #ddd;
      
        & > span {
          display: block;
          line-height: 35px;
          font-size: 18px;
        }
      `;
      
      const Demo = function Demo(props) {
        let { num, increment } = props;
        return (
          <DemoStyle>
            <span>{num}</span>
            <Button
              type="primary"
              onClick={() => {
                increment(10);
              }}
            >
              新增
            </Button>
          </DemoStyle>
        );
      };
      // export default Demo;
      export default connect((state) => state.demo, action.demo)(Demo);
      

react-redux的数据流转

  • react-redux没有使用中间件的数据流转图:

  • react-redux使用中间件后的数据流转图:

    • 实际流程中,并不一定是这样的,这里是为了好理解。中间件会跳来跳去。
    • 中间件流程中,可能会有多个中间件插件,中间件插件会自上而下执行,之后才是到reducer。
    • 中间件种类:
      • redux-logger 输出派发日志。
      • redux-thunk 处理异步派发的。
      • redux-
  • fang/f20230531/src/store/index.js 简单使用中间件-打印日志。

import { legacy_createStore, applyMiddleware } from "redux";
import reduxLogger from "redux-logger";//输出派发日志---方便调试。
import reduxThunk from "redux-thunk";//输出派发日志。
import reduxPromise from "redux-promise";//输出派发日志。

import reducer from "./reducers";
const store = legacy_createStore(reducer, applyMiddleware(reduxLogger));
export default store;
  • fang/f20230531/src/store/index.js 分环境。
import { legacy_createStore, applyMiddleware } from "redux";
import reducer from "./reducers";

import reduxLogger from "redux-logger"; //输出派发日志---方便调试。
const env = process.env.NODE_ENV;
let middleware = [];
// 非生产环境下使用的中间件。
if (env !== "production") {
  middleware.push(reduxLogger);
}

const store = legacy_createStore(reducer, applyMiddleware(...middleware));
export default store;
  • 处理异步派发- 基于redux-promise。

    • fang/f20230531/src/store/index.js

      import { legacy_createStore, applyMiddleware } from "redux";
      import reducer from "./reducers";
      
      import reduxLogger from "redux-logger"; //输出派发日志---方便调试。
      import reduxThunk from "redux-thunk"; //输出派发日志。
      import reduxPromise from "redux-promise"; //输出派发日志。
      const env = process.env.NODE_ENV;
      let middleware = [];
      // let middleware = [reduxPromise];
      // 非生产环境下使用的中间件。
      if (env !== "production") {
        middleware.push(reduxLogger);
      }
      
      middleware.push(reduxPromise)
      
      const store = legacy_createStore(reducer, applyMiddleware(...middleware));
      export default store;
      
    • fang/f20230531/src/store/actions/demoAction.js

      import * as AT from '../action-types'
      
      import API from '@/api'
      const demoAction = {
      
        /* // 基于redux-promise实现异步派发。默认返回的是Promise实例。dispatch()执行时,会报错`Actions must be plain objects.`;
        当我们应用了redux-promise中间件,点击按钮的时候,是基于其重写的dispatch实现派发的。
        @1. dispatch(action.demo.increment(10))
          - dispatch是重写的函数。
          - action.demo.increment执行的结果是一个promise实例。
          - 在reduxPromise中间件的内部会监测promise实例的结果。
        @2. 如果实例是失败态,则不做任何处理!如果实例是成功的:
          - 在中间件的内部,再基于store.dispatch进行派发
            - store.dispatch(promise实例的值-也就是函数返回的action对象)
          - 而此派发会通知reducer执行,以此来修改状态。 */
        async increment(step=1){
          await API.delay()
          return {
            type:AT.DEMO_INCREMENT,
            step,
          }
        }
      
        /* // 同步派发。
        increment(step=1){
          return {
            type:AT.DEMO_INCREMENT,
            step,
          }
        } */
      }
      export default demoAction
      
    • 基于redux-promise实现异步派发。

      • 当我们应用了redux-promise中间件,点击按钮的时候,是基于其重写的dispatch实现派发的。
        1. dispatch(action.demo.increment(10))
          • dispatch是重写的函数。
          • action.demo.increment执行的结果是一个promise实例。
          • 在reduxPromise中间件的内部会监测promise实例的结果。
        2. 如果实例是失败态,则不做任何处理!如果实例是成功的:
          • 在中间件的内部,再基于store.dispatch进行派发
            • store.dispatch(promise实例的值-也就是函数返回的action对象)
          • 而此派发会通知reducer执行,以此来修改状态。
  • 处理异步派发- 基于redux-thunk。

    • fang/f20230531/src/store/index.js

      import { legacy_createStore, applyMiddleware } from "redux";
      import reducer from "./reducers";
      
      import reduxLogger from "redux-logger"; //输出派发日志---方便调试。
      import reduxThunk from "redux-thunk"; //可以执行异步派发任务-基于action中直接属性的同步函数返回的是一个函数-react官方推荐的,但不太好用。
      import reduxPromise from "redux-promise"; //可以执行异步派发任务-基于action中直接属性的是一个异步函数-返回的是一个对象-使用起来更简单。
      const env = process.env.NODE_ENV;
      let middleware = [];
      // let middleware = [reduxPromise];
      // 非生产环境下使用的中间件。
      if (env !== "production") {
        middleware.push(reduxLogger);
      }
      
      middleware.push(reduxPromise); //可以与"redux-thunk"一起使用。
      middleware.push(reduxThunk); //可以与"redux-promise"一起使用。
      
      const store = legacy_createStore(
        reducer,
        applyMiddleware(...middleware) //这里是处理中间间的,函数内部每个入参都是一个中间件,可以传入多个。用数组再解构,是更方便处理的。
      );
      export default store;
      
    • fang/f20230531/src/store/actions/demoAction.js

      import * as AT from "../action-types";
      
      import API from "@/api";
      const demoAction = {
        /* // 基于redux-thunk中间件处理异步派发。
          1. 也对dispatch进行了重写
              - 单击按钮的时候,是基于重写的dispatch进行派发的。
              - 执行action.demo.increment获取到的是一个函数。
                - 之前返回出去的是一个用于store.dispatch()进行派发任务的行为对象。
                - 现在是一个用于redux-thunk中间件内部执行的函数。
                  - redux-thunk中间件内部执行时,会传入store.dispatch作为第一个入参。即该函数的第一个形参就是表示store.dispatch。
              - thunk中间件内部,会把获取的这个函数执行,并且把store.dispatch传递给这个函数。
                - 即该函数的第一个形参就是表示store.dispatch。
          2. 我们一般要在这个函数中发送异步请求,等待请求成功后,需要手动基于store.dispatch进行派发。
        */
        // 基于redux-thunk中间件处理异步派发。
        increment(step = 1) {
      
          // 下方本来要返回一个对象的,但现在要进行异步,则改为返回一个函数。而redux-thunk中间件会执行该函数,并在执行时,传入store.dispatch。
          return async (dispatch) => {
            // dispatch:store.dispatch。
      
            await API.delay();
            dispatch({
              type: AT.DEMO_INCREMENT,
              step,
            });
          };
        },
      
        /* // 基于redux-promise实现异步派发。默认返回的是Promise实例。dispatch()执行时,会报错`Actions must be plain objects.`;
          当我们应用了redux-promise中间件,点击按钮的时候,是基于其重写的dispatch实现派发的。
            @1. dispatch(action.demo.increment(10))
              - dispatch是重写的函数。
              - action.demo.increment执行的结果是一个promise实例。
              - 在reduxPromise中间件的内部会监测promise实例的结果。
            @2. 如果实例是失败态,则不做任何处理!如果实例是成功的:
              - 在中间件的内部,再基于store.dispatch进行派发
                - store.dispatch(promise实例的值-也就是函数返回的action对象)
              - 而此派发会通知reducer执行,以此来修改状态。
        */
        /* // 基于redux-promise实现异步派发。
        async increment(step=1){
          await API.delay()
          return {
            type:AT.DEMO_INCREMENT,
            step,
          }
        } */
      
        /* // 同步派发。
        increment(step=1){
          return {
            type:AT.DEMO_INCREMENT,
            step,
          }
        } */
      };
      export default demoAction;
      
    • 基于redux-thunk中间件处理异步派发。

      1. 也对dispatch进行了重写。
        • 单击按钮的时候,是基于重写的dispatch进行派发的。
        • 执行action.demo.increment获取到的是一个函数。
          • 之前返回出去的是一个用于store.dispatch()进行派发任务的行为对象。
          • 现在是一个用于redux-thunk中间件内部执行的函数。
            • redux-thunk中间件内部执行时,会传入store.dispatch作为第一个入参。即该函数的第一个形参就是表示store.dispatch。
        • thunk中间件内部,会把获取的这个函数执行,并且把store.dispatch传递给这个函数。
          • 即该函数的第一个形参就是表示store.dispatch。
      2. 我们一般要在这个函数中发送异步请求,等待请求成功后,需要手动基于store.dispatch进行派发。

redux-logger源码

  • redux-logger源码可以在node_modules中看到。
    • /node_modules/redux-logger/src/index.js

TaskOA

  • 对于增删改查–减少服务器请求版思路:

    • 查-发起数据查询请求,查询全部数据。
      • 切换数据-根据全部数据筛选数据,用于页面渲染。
    • 删-发送删除请求,修改本地总数据,不发起数据查询请求。
    • 增-发送新增请求,发起数据查询请求。
    • 改-发送修改请求,修改本地总数据,不发起数据查询请求。
  • redux作用。

    • 把数据缓存在本地,减少对服务器请求。想更新,让用户手动刷新一次。
    • 公共状态管理,跨组件通信。
  • 用redux修改旧页面流程:

    1. 组件第一次渲染的时候,我们首先判断redux容器中是否存储了“全部任务”

      • 存储了:啥都不干
      • 没存储:向服务器发送请求,异步获取全部的任务,然后存储到redux中
    2. 表格中需要呈现的数据,是依赖于 redux中的全部任务 和 选中的状态,筛选出来的!

      • 只要redux容器中的全部任务变了 或者 选中状态变了,则重新筛选出最新的数据进行渲染!
    3. 点击新增:

      • 发请求告诉服务器新增
      • 如果新增成功,则派发任务,从服务器重新获取最新的全部任务,存储到redux中!
    4. 点击删除/完成

      • 正常和服务器通信
      • 派发任务,把redux中存储的全部任务中的某一条,进行删除和修改!
  • 用redux修改旧页面的步骤:

    • fang/f20230531/src/store/action-types.js

      export const DEMO_INCREMENT = `DEMO_INCREMENT`
      
      export const TAST_QUERY_ALL_lIST = `TAST_QUERY_ALL_lIST`
      export const TAST_REMOVE = `TAST_REMOVE`
      export const TAST_UPDQTE = `TAST_UPDQTE`
      
    • fang/f20230531/src/store/actions/taskAction.js

      import * as AT from "../action-types";
      import API from "@/api";
      const taskAction = {
        // 获取全部任务的异步派发。
        async queryAllList() {
          let list = [];
          try {
            let result = await API.queryTaskList();
            if (+result.code === 0) {
              list = result.list;
            }
          } catch (error) {
            console.log(`error-->`, error);
          }
      
          // 返回派发的对象;
          return {
            type: AT.TAST_QUERY_ALL_lIST,
            list,
          };
        },
      
        // 删除任务的同步派发。
        removeTask(id) {
          return {
            type: AT.TAST_REMOVE,
            id,
          };
        },
      
        // 修改任务的同步派发。
        updateTask(id) {
          return {
            type: AT.TAST_UPDQTE,
            id,
          };
        },
      };
      export default taskAction;
      
    • fang/f20230531/src/store/reducers/taskReducer.js

      import * as AT from "../action-types";
      let initial = {
        list: null, //null说明还没有从服务器获取过。空数组或其它数组,都表示已经从服务器获取到了。
      };
      
      export default function taskReducer(state = initial, action) {
        state = { ...state };
        let { type, list, id } = action;
        switch (type) {
          //...
          case AT.TAST_QUERY_ALL_lIST:
            state.list = list;
            break;
          case AT.TAST_REMOVE:
            if (Array.isArray(state.list)) {
              state.list = state.list.filter((item) => +item.id !== +id);
            }
            break;
          case AT.TAST_UPDQTE:
            if (Array.isArray(state.list)) {
              state.list = state.list.map((item) => {
                if (+item.id === +id) {
                  item.state = 2;
                  item.complete = new Date().toLocaleString("zh-CN", {
                    hour12: false,
                  });
                }
                return item;
              });
            }
            break;
          default:
        }
        return state;
      }
      
    • fang/f20230531/src/views/Task.jsx

      import { useState, useEffect, useMemo } from "react";
      import styled from "styled-components";
      
      import { connect } from "react-redux";
      import action from "@/store/actions";
      
      /* 组件样式 */
      const TaskStyle = styled.div`
        box-sizing: border-box;
      `;
      
      /* 组件视图 */
      const Task = function Task(props) {
        // 获取属性信息;
        let { list, queryAllList, removeTask, updateTask } = props;
      
        // 定义状态
        let [loading, setLoading] = useState(false)
      
        //   组件第一次渲染完毕,先判断一下redux中是否存在全部任务,如果没有存在,则完成一次异步派发。
        useEffect(() => {
          (async ()=>{
      
          if (!list) {
              setLoading(true)
            // 默认派发一次。
            let xxx = await queryAllList();//只有使用的是redux-promise中间件,才保证派发返回的结果是promise实例,才可以用await做监听。
            console.log('xxx',xxx);
            setLoading(false)
          }
          })()
        }, []);
        // 依赖于redux中的全部任务-list,和当前选中的状态selectedIndex,筛选表格需要的数据。
        let data = useMemo(() => {
          let arr = list || [];
          if (selectedIndex === 1 || selectedIndex === 2) {
            arr = arr.filter((item) => {
              return +item.state === selectedIndex;
            });
          }
          return arr;
        }, [list, selectedIndex]);
      
        // 确认新增任务
        const submit = async () => {
              setLoading(true)
              await queryAllList();
              setLoading(false)
        };
      
        // 删除任务
        const handleRemove = async (id) => {
              removeTask(id)
        };
      
        // 修改任务
        const handleUpdate = async (id) => {
              updateTask(id)
        };
      
        return (
          <TaskStyle>
            //...
          </TaskStyle>
        );
      };
      export default connect((state) => state.task, action.task)(Task);
      
      import { useState, useEffect, useMemo } from "react";
      import styled from "styled-components";
      import {
        Button,
        Tag,
        Table,
        Popconfirm,
        Modal,
        Form,
        Input,
        DatePicker,
        message,
      } from "antd";
      import _ from "@/assets/utils";
      import dayjs from "dayjs";
      import API from "@/api";
      import { connect } from "react-redux";
      import action from "@/store/actions";
      
      /* 组件样式 */
      const TaskStyle = styled.div`
        box-sizing: border-box;
        margin: 0 auto;
        width: 800px;
      
        .header-box {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 10px 0;
          border-bottom: 1px solid #ddd;
      
          .title {
            font-size: 20px;
            font-weight: normal;
          }
        }
      
        .tag-box {
          margin: 15px 0;
      
          .ant-tag {
            padding: 5px 15px;
            margin-right: 15px;
            font-size: 14px;
            cursor: pointer;
          }
        }
      
        .ant-btn-link {
          padding: 4px 5px;
        }
      
        .ant-modal-header {
          margin-bottom: 20px;
        }
      `;
      
      /* 组件视图 */
      const Task = function Task(props) {
        // 获取属性信息;
        let { list, queryAllList, removeTask, updateTask } = props;
      
        // 定义表格列
        const columns = [
          {
            title: "编号",
            dataIndex: "id",
            width: "6%",
            align: "center",
          },
          {
            title: "任务描述",
            dataIndex: "task",
            width: "50%",
            ellipsis: true,
          },
          {
            title: "状态",
            dataIndex: "state",
            width: "10%",
            align: "center",
            render: (text) => (+text === 1 ? "未完成" : "已完成"),
          },
          {
            title: "完成时间",
            width: "16%",
            align: "center",
            render(text, record) {
              let { state, time, complete } = record;
              time = +state === 1 ? time : complete;
              return _.formatTime(time, "{1}/{2} {3}:{4}");
            },
          },
          {
            title: "操作",
            width: "18%",
            render(text, record) {
              let { state, id } = record;
              return (
                <>
                  <Popconfirm
                    title="您确定要删除此任务吗?"
                    onConfirm={handleRemove.bind(null, id)}
                  >
                    <Button type="link" danger>
                      删除
                    </Button>
                  </Popconfirm>
      
                  {+state === 1 ? (
                    <Popconfirm
                      title="您确定要把此任务设置为已完成吗?"
                      onConfirm={handleUpdate.bind(null, id)}
                    >
                      <Button type="link">完成</Button>
                    </Popconfirm>
                  ) : null}
                </>
              );
            },
          },
        ];
      
        // 定义状态
        //   let [data, setData] = useState([]),
        let [loading, setLoading] = useState(false),
          [selectedIndex, setSelectedIndex] = useState(0);
        let [modalVisible, setModalVisible] = useState(false),
          [confirmLoading, setConfirmLoading] = useState(false);
        let [formIns] = Form.useForm();
      
        //   组件第一次渲染完毕,先判断一下redux中是否存在全部任务,如果没有存在,则完成一次异步派发。
        useEffect(() => {
          (async ()=>{
      
          if (!list) {
              setLoading(true)
            // 默认派发一次。
            let xxx = await queryAllList();//只有使用的是redux-promise中间件,才保证派发返回的结果是promise实例,才可以用await做监听。
            console.log('xxx',xxx);
            setLoading(false)
          }
          })()
        }, []);
        // 依赖于redux中的全部任务-list,和当前选中的状态selectedIndex,筛选表格需要的数据。
        let data = useMemo(() => {
          let arr = list || [];
          if (selectedIndex === 1 || selectedIndex === 2) {
            arr = arr.filter((item) => {
              return +item.state === selectedIndex;
            });
          }
          return arr;
        }, [list, selectedIndex]);
      
        //   // 第一次渲染完&每一次选中状态改变,都要从服务器重新获取数据
        //   useEffect(() => {
        //     initData();
        //   }, [selectedIndex]);
      
        //   // 从服务器获取指定状态的任务
        //   const initData = async () => {
        //     setLoading(true);
        //     try {
        //       let { code, list } = await API.queryTaskList(selectedIndex);
        //       if (+code !== 0) list = [];
        //       setData(list);
        //     } catch (_) {}
        //     setLoading(false);
        //   };
      
        // 关闭Modal对话框
        const closeModal = () => {
          setModalVisible(false);
          setConfirmLoading(false);
          formIns.resetFields();
        };
      
        // 确认新增任务
        const submit = async () => {
          try {
            await formIns.validateFields();
            let { task, time } = formIns.getFieldsValue();
            time = time.format("YYYY-MM-DD HH:mm:ss");
            setConfirmLoading(true);
            let { code } = await API.insertTaskInfo(task, time);
            if (+code === 0) {
              message.success("恭喜您,新增成功~");
              closeModal();
      
              // initData();
      
              // 新增成功后,需要派发异步任务,同步最新的数据到redux中。
              setLoading(true)
              await queryAllList();
              setLoading(false)
            } else {
              message.error("很遗憾,新增失败,请稍后再试~");
            }
          } catch (_) {}
          setConfirmLoading(false);
        };
      
        // 删除任务
        const handleRemove = async (id) => {
          try {
            let { code } = await API.removeTaskById(id);
            if (+code === 0) {
              message.success("恭喜您,删除成功");
              // let arr = data.filter((item) => +item.id !== +id);
              // setData(arr);
      
              // 派发删除的同步任务。
              removeTask(id)
              return;
            }
            message.error("很遗憾,删除失败,请稍后再试");
          } catch (_) {}
        };
      
        // 修改任务
        const handleUpdate = async (id) => {
          try {
            let { code } = await API.updateTaskById(id);
            if (+code === 0) {
              message.success("恭喜您,修改成功");
              // let arr = data.map((item) => {
              //   if (+item.id === +id) {
              //     item.state = 2;
              //     item.complete = dayjs().format("YYYY-MM-DD HH:mm:ss");
              //   }
              //   return item;
              // });
              // setData(arr);
      
              //派发修改的同步任务。
              updateTask(id)
              return;
            }
            message.error("很遗憾,修改失败,请稍后再试");
          } catch (_) {}
        };
      
        return (
          <TaskStyle>
            <div className="header-box">
              <h2 className="title">TASK OA 任务管理系统</h2>
              <Button type="primary" onClick={() => setModalVisible(true)}>
                新增任务
              </Button>
            </div>
      
            <div className="tag-box">
              {["全部", "未完成", "已完成"].map((item, index) => {
                return (
                  <Tag
                    key={index}
                    color={selectedIndex === index ? "#1677ff" : ""}
                    onClick={() => {
                      if (selectedIndex === index) return;
                      setSelectedIndex(index);
                    }}
                  >
                    {item}
                  </Tag>
                );
              })}
            </div>
      
            <Table
              columns={columns}
              dataSource={data}
              loading={loading}
              pagination={false}
              rowKey="id"
              size="middle"
            />
      
            <Modal
              title="新增任务窗口"
              okText="确认提交"
              keyboard={false}
              maskClosable={false}
              getContainer={false}
              confirmLoading={confirmLoading}
              open={modalVisible}
              afterClose={closeModal}
              onCancel={closeModal}
              onOk={submit}
            >
              <Form
                colon={false}
                layout="vertical"
                validateTrigger="onBlur"
                form={formIns}
                initialValues={{
                  task: "",
                  time: dayjs().add(1, "day"),
                }}
              >
                <Form.Item
                  name="task"
                  label="任务描述"
                  rules={[{ required: true, message: "任务描述是必填项" }]}
                >
                  <Input.TextArea rows={4} />
                </Form.Item>
                <Form.Item
                  name="time"
                  label="预期完成时间"
                  rules={[{ required: true, message: "请先选择预期完成时间" }]}
                >
                  <DatePicker showTime />
                </Form.Item>
              </Form>
            </Modal>
          </TaskStyle>
        );
      };
      
      export default connect((state) => state.task, action.task)(Task);
      

react路由管理方案react-router-dom

  • web端用的是react-router-dom。原生用的是react-router。
  • SPA:单页面应用。
    • 一个项目只有一个页面,我们在这个页面中,基于前端路由机制,控制组件的显示和隐藏或销毁。
      • 页面不更新,体验比较好。
  • MPA:多页面应用。

SEO优化

  • 简单介绍SEO搜索引擎优化:
    1. 搜索引擎(例如:谷歌、必应bing、百度、360、…)都有一个工具:爬虫、小蜘蛛。

      • 它们会按照相关的规则,去抓取与收录各个网站中的信息-即网页源代码中可以看到的信息,把信息收录到自己的搜索引擎库中。
        • 根据收录的内容和相应的规则,计算出网站与关键词的权重。
      • 当用户基于搜索引擎搜索某个关键词的时候,会去库中进行匹配,把匹配的内容呈现出来,而且权重越高的越靠前!
    2. SEO优化,就是尽可能提高网站的权重,并且让搜索引擎收录更多的信息。

          <title>网易云音乐</title>
          <meta name="keywords" content="关键字" />
          <meta name="description" content="" />
          ....
      
      • 这个title与keywords-content关键字与keywords-description网页描述是需要做的。
    3. SEM竞价排名:看谁花钱多!

      • 会有标识之类的东西,要花钱。
  • 目前SEO优化已经不是主流的优化方案了!
  • 如果网站是JavaScript动态渲染的,在其源代码中,是看到动态绑定的信息的!也就不利于SEO优化!
    • 例如:
      • 基于vue/react等框架开发的网站。
      • 基于客户端渲染的网站。
  • 想做SEO优化,不能基于客户端渲染,需要基于服务器进行渲染!
    • 传统方式:前后端不分离-php、jsp…
    • 新方案:SSR服务器渲染-Server Side Render服务器端渲染。
      • vue:vue + nuxt.js + Node。
      • react:react + next.js + Node。
      • 前端工程师踏入全栈工程师的第一步。

路由设计模式

hash哈希路由

  • 原理:每一次路由切换或组件切换,都是改变了页面的哈希值。

    • 而哈希值的变化,不会引起页面的刷新!
    • 我们可以基于hashchange事件,监听哈希值的变化,用最新的哈希值,去路由表中进行匹配,把匹配的组件拿过来渲染。
      • 之前渲染的组件销毁。
  • 代码示例:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>前端路由机制实现</title>
      </head>
    
      <body>
        <nav class="nav-box">
          <a href="#/">首页</a>
          <a href="#/product">产品中心</a>
          <a href="#/personal">个人中心</a>
        </nav>
        <!-- view-box是用来匹配渲染的组件的。 -->
        <div class="view-box"></div>
    
        <!-- IMPORT JS -->
        <script>
          const viewBox = document.querySelector(".view-box");
    
          // 构建路由表-什么哈希值,匹配什么组件/内容。
          const routes = [
            { path: "/", components: "首页的内容" },
            { path: "/product", components: "产品中心的内容" },
            { path: "/personal", components: "个人中心的内容" },
    
            { path: "/me", redirect: "/personal" }, //重定向。
    
            { path: "/my", alias: "/personal" }, //别名。
    
            { path: "*", components: "404错误页面" }, //404页面。
          ];
    
          //监听哈希值的变化,根据最新的哈希值,和路由表进行匹配,把匹配的内容放在viewBox容器中渲染!
          const matchRoute = function matchRoute() {
            // console.log(`location.hash-->`, location.hash);
            let hash = location.hash.substring(1);
            let item = routes.find((item) => item.path === hash);
    
            // 重定向。
            if(item?.redirect){
                location.hash = `#${item.redirect}`;
                return
            }
    
            // 别名。
            if(item?.alias){
                item = routes.find((item1) => item1.path === item.alias);
            }
    
            if (!item) {
              // 没有找到匹配项,再去找path=`*`的。
              item = routes.find((item) => item.path === `*`);
            }
            if (!item) {
              // 正常的没找到,*的也没找到。
              viewBox.innerHTML = ``;
              return;
            }
    
            // 如果找到了,则把匹配的内容进行渲染!
            viewBox.innerHTML = item.components;
          };
          window.addEventListener("hashchange", matchRoute);
    
          // 第一次进入页面,让页面的哈希值默认是首页的值`#/`
          location.hash = `#/`;
          matchRoute();
        </script>
      </body>
    </html>
    

history浏览器路由

  • 原理:利用H5中的historyAPI实现的,在History.prototype上提供了页面地址跳转的方法,基于这些方法跳转,页面是不会刷新的,只是改了URL地址。

    • API-History
    • 常见方法:
      • pushState:跳转到新的地址,新增一条历史记录。
      • replaceState:跳转到新的地址,替换现在的历史记录。
      • forward 前进一步,只会在现在历史记录中跳转,不会新增历史记录。
      • back 后退一步。
      • go 跳转到指定的那一步。
        • go(-1) --> back();
        • go(1) --> forward();
  • 当基于pushState与replaceState进行跳转的时候,我们会在跳转之后,执行路由匹配的方法。

  • 当基于forward与back与go进行跳转的时候-也就是前进后退等操作,我们可以基于popstate事件,监听到路由跳转,然后执行路由匹配的跳转。

  • 路由匹配的方法:

    • 获取最新的地址,和路由表中的path进行匹配。
    • 把匹配的内容或组件,放在指定的容器中渲染。
  • 平时项目中使用何种方式:

    • 最常用的是哈希路由!
      • 只不过有人认为,哈希这种方式比较丑,而且改变的是哈希值,并不是地址,有点不真实!也不利于SEO优化!
    • 而且History路由,必须让服务器给予支持才可以!因为我们路由跳转的地址是不存在的。页面不刷新还好,一旦刷新,则重新向服务器发送请求,此地址在服务器并不存在,服务器默认会返回404,这样肯定不行,我们需要服务器在访问地址不存在的情况下,返回客户端的依然是我们唯一的那个页面!
      • SPA单页面应用,只有一个真实的页面,其余跳转到的地址,没有对应的其它页面。

进阶参考

  1. API-History
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。