解构一个简单的react项目,用来练手很不错 【,,ԾㅂԾ,,】

1 阅读12分钟

写在前面

本项目来源于黑马的公开课,比较简单,适合对于react半知半解的小白,本文也会面向小白去讲解,目的是帮助大家更好的理解react,更好的使用react,相信对大家会有所帮助【,,ԾㅂԾ,,】。

诸位,让我们步入正题

一、项目功能展示

1.拉取并启运行项目

源码GitHub地址 github.com/Objecteee/B…

在随便一个文件夹的终端运行下面代码即可拉取项目并运行

git clone https://github.com/Objecteee/Bill.git  //拉取项目
cd Bill   // 进入项目文件夹
npm i     // 下载项目依赖
npm run server   // 运行数据库
// -------------------------------
// 此时重新打开一个项目的终端
npm start    //启动项目

image.png 设备窗口调整为iPhone SE体验更佳

2.功能展示——月度账单查看

该功能可以查看用户每月的收入支出等账单详情,并且可以看到具体是哪一天的消费

screenshot_2025-05-29_18-56-00.gif

3.日消费明细查看

该功能可以查看用户的日收入支出明细

screenshot_2025-05-29_18-59-07.gif

4.添加账单

该功能可以让用户添加收入和支出,并可以选择收入支出项和日期

screenshot_2025-05-29_19-02-18.gif

上述功能是不是很简单?但是其中蕴含的知识点是海量的,下面我将如抽丝剥茧带大家解析这个项目

二、项目要点解析

1.项目结构设计

下面是核心目录结构

image.png

我只简单的介绍一下

  • components存放可复用的 React 组件,例如 Icon 可能存放图标组件。
  • constants :用于存放项目中的常量,这里比较简单,文章中不会讲
  • img :存放项目中使用的图片资源,如 月度背景图.jpg
  • pages :存放应用的页面组件,例如 Layout 可能是页面布局组件, MonthNewYear 可能是不同功能的页面组件。
  • router :存放路由配置文件,如 index.js 可能定义了应用的路由规则。
  • store :存放 Redux 状态管理相关的文件,如 index.js 配置了 Redux store, modules 目录下的 billStore.js 定义了账单数据的 reducer 和 actions。

2.项目组件设计

我以截图的形式,形象的给大家展示组件结构

image.png

3.项目路由设计

这一部分,我会讲解项目中的路由设计,该项目的路由设计是比较清晰的

分为一级路由和二级路由,一级路由包括New、Layout组件,一级路由New下包含二级路由Month、Year组件,结构如下

import { createBrowserRouter } from "react-router-dom";
import Layout from "@/pages/Layout";
import New from "@/pages/New";
import Month from "@/pages/Month";
import Year from "@/pages/Year";
const router=createBrowserRouter([
    {
        path:'/',
        element:<Layout/>,
        children:[ 
            {index:true,element:<Month/>},
            {path:'/year',element:<Year/>}
        ]
    }, 
    {
        path:'/new',
        element:<New/>
    }
])
export default router;

其中,/路径下默认展示的是组件。

这里解决大家可能出现的一个疑问,为什么要分一级二级路由,不直接每个路由一个单独的Path呢?

(1) 共享布局,减少重复代码

  • 如果所有路由都是独立的(如 //month/year/new),每个页面都要单独引入 <Layout />,导致代码冗余:

    //  独立路径的冗余写法(每个路由都要重复 Layout)
    { path: '/', element: <Layout><Month /></Layout> },
    { path: '/year', element: <Layout><Year /></Layout> },
    { path: '/new', element: <New /> }
    
  • 嵌套路由让 //year自动继承 <Layout /> ,只需定义一次:

    // 嵌套路由的简洁写法
    {
      path: '/',
      element: <Layout />,  // 父路由定义 Layout
      children: [
        { index: true, element: <Month /> },  // 自动包裹在 Layout 中
        { path: '/year', element: <Year /> }   // 自动包裹在 Layout 中
      ]
    }
    

(2) 逻辑分层更清晰

  • 一级路由 / :代表整个应用的基础布局​(如导航栏、页脚)。

  • 二级路由

    • index:true(默认路由):显示 <Month />(如首页)。
    • /year:显示 <Year />(年度视图)。
  • 独立路由 /new:不需要共享布局(如登录页、弹窗页)。

这样分层后,​代码结构更符合实际业务逻辑,而不是所有路径都平铺在一起。

(3) 动态路由和权限控制更灵活

  • 如果未来需要在 /user/:id 下嵌套多个子页面(如 /user/:id/profile/user/:id/settings),嵌套路由可以轻松扩展:

    {
      path: '/user/:id',
      element: <UserLayout />,
      children: [
        { path: 'profile', element: <Profile /> },  // 访问 /user/123/profile
        { path: 'settings', element: <Settings /> } // 访问 /user/123/settings
      ]
    }
    
  • 权限控制:可以在父路由 (/user/:id) 统一校验用户权限,子路由自动继承。

  • 虽然我们这个简单的项目中并没有用到这一点

此外,使用子路由模式还有其他好处,比如懒加载等,这里不再一一列举,感兴趣的读者可以自行查询


4.项目数据处理

这一部分,我会讲解项目中数据的获取和处理以及展示,这是react的核心,也是我们讲解的核心

①数据获取

项目的数据部署在json-server之中——json-server 是一个零配置的 RESTful API 模拟工具,通过 JSON 文件快速搭建本地测试服务器。 项目中的server包下的data.json即是我们项目的数据

image.png

之后数据由Redux获取到前端。 Redux把获取到的数据存储在全局状态树里。项目里的各个组件能够从这个全局状态树中获取所需数据,以此保证数据的一致性和可维护性,下面我们具体分析一下Redux获取数据的代码,这是项目十分重要的一部分

先来分析src\store\modules\billStore.js中的部分,这部分代码是定义账单数据的状态管理逻辑,包括同步更新方法(设置/添加账单)和异步操作(获取/提交账单数据)。最终导出 reducer 和异步方法供全局 store 和组件使用。

import { createSlice } from '@reduxjs/toolkit';
import axios from "axios";
const billStore=createSlice({
    name:'billStore',
    initialState:{
        billList:[]
    },
    reducers:{
        setBillList(state,action){
            state.billList=action.payload; 
        },
        addBill(state,action){
           state.billList.push(action.payload); 
        }
    }
})

const {setBillList,addBill}=billStore.actions;
const getBillList=()=>{
    return async (dispatch)=>{
        const res=await axios.get('http://localhost:8888/ka')
        dispatch(setBillList(res.data))
    }
}
const addBillList=(data)=>{
    return async (dispatch)=>{
        const res=await axios.post('http://localhost:8888/ka',data)
        dispatch(addBill(res.data))
    }
}
export {getBillList,addBillList};
const reducer=billStore.reducer;
export default reducer;

这里很遗憾的告诉各位,这里并不会深入到react的底层去讲解Redux,或许之后我单独写一篇文章去讲底层?

但是让各位理解代码还是必要的,下面是对代码中关键部分的分析

1.createSlice: Redux Toolkit 的核心 API,用于创建状态切片,大家如何去理解切片呢,暂且理解为应用中某个功能模块的状态和逻辑的封装,或者更通俗一些 —— 想象整个应用的全局状态是一个大蛋糕🍰,而 ​每个切片就是从这个蛋糕上切下来的一块,专门负责管理某个独立功能的数据和逻辑。理解不了也没关系,反正要有这个东西

2.createSlice中定义了很多“东西”,我们可以这样理解name是切片的名称,initialState是切片中的变量,且有初始值,reducers是切片的一些方法的集合,里面有很多方法

3.现在我们来理解一下切片中的方法

  • setBillList中是state.billList=action.payload;即是把billList中的数据全部替换,一般用于初始化数据
  • addBill中是state.billList.push(action.payload);即是在billList中添加数据

4.const {setBillList,addBill}=billStore.actions;``export {getBillList,addBillList};将这两个方法解构并导出,使别的组件可以使用这两个方法去更改billList中的数据

5.下面的代码大家理解为实例化setBillList、addBill方法,dispatch是什么?

dispatch 是 Redux 中唯一能触发状态更新的方法,它把 action 发送给 reducer 来修改 store 中的数据。

const getBillList=()=>{
    return async (dispatch)=>{
        const res=await axios.get('http://localhost:8888/ka')
        dispatch(setBillList(res.data))
    }
}
const addBillList=(data)=>{
    return async (dispatch)=>{
        const res=await axios.post('http://localhost:8888/ka',data)
        dispatch(addBill(res.data))
    }
}

6.billStore.reducer它是 Redux 中真正修改 billList 数据的规则函数,会根据不同的 action(如 setBillList/addBill)决定如何更新状态。

const reducer=billStore.reducer;
export default reducer;

其他先不要管,就这样理解


再来分析src\store\index.js中的部分,这部分代码是整合所有业务模块的 reducer,创建全局 store 实例。自动配置 Redux DevTools 和中间件,使应用支持状态管理和调试。

import { configureStore } from "@reduxjs/toolkit";
import billReducer from "./modules/billStore";
const store=configureStore({
    reducer:{
        bill:billReducer,
    }
})
export default store;

首先,各位可能会问billReducer是哪位,我记得没有导出它呀?这只是对我们默认导出的reducer的重命名,不用管这段代码什么意思,这段代码让我们可以在全局使用store了,其中包括billStore.js中的内容

②数据处理

1.在Layout组件中,进行了数据的初始化

  useEffect(() => {
    dispatch(getBillList())
  }, [dispatch])
  const navigate=useNavigate()

这里dispatch(getBillList())调用了getBillList()方法,使前文提到的data.json中的数据传入到了billList数组中

2.在Nwe组件中调用了数据更新的方法

  const saveBill=()=>{
   const data={
     type:billType,
     money:billType==='pay'?-money:+money,
     date:date? dayjs(date).toISOString() : dayjs().toISOString(),
     useFor:useFor
   } 
   dispatch(addBillList(data))
  }

当用户点击保存按钮之后,可以将data中的数据加入到billList和data.json

3.Month组件中数据的分组

因为我们项目中页面展示的是某月的数据,所以要对数据进行月份分组,以便于方便展示并总汇月收入支出等

  const monthGroup=useMemo(()=>{
   return _.groupBy(billList,(item)=>dayjs(item.date).format('YYYY-MM')) ;
  },[billList])

同理,我们要汇总每一天的收入和支出,所以要对月下的天进行分组

  const dayGroup=useMemo(()=>{
    const groupData=_.groupBy(currentMonthList,(item)=>dayjs(item.date).format('YYYY-MM-DD')) ;
    const keys=Object.keys(groupData)
   return {
    groupData,
    keys
   }
  },[currentMonthList])

这些代码使用了一个鲁大师的库,里面有分组的方法,第一个参数是要分组的内容,第二个参数是依据什么分组,总体还是很好理解的

对了,还有一个dayjs的库,方便日期格式的转换

4.数据的计算

我们要计算一些数据,如收入、支出、结余的计算

  const monthResult=useMemo(()=>{ 
    const pay=currentMonthList.filter(item=>item.type==='pay').reduce((a,c)=>a+c.money,0)
    const income=currentMonthList.filter(item=>item.type==='income').reduce((a,c)=>a+c.money,0)
    return {
      pay,
      income,
      total:income+pay
    }
  },[currentMonthList])

这里举一个例子,这里使用的是useMemo ,什么是useMemo

useMemo是 React 的一个 Hook,用于缓存计算结果,避免组件重新渲染时重复执行复杂运算,仅在依赖项变化时重新计算。

5.项目核心组件逻辑

1.日期选择器的处理

我们先来看一下日期选择器的效果

screenshot_2025-05-29_21-40-00.gif

可以看到有一个^的箭头我们点击可以改变形状点击“确定”、“取消”或者“没有聚焦的地方”都可以隐藏日期选择器,而且选择的结果可以渲染在相应的位置,这是如何实现的呢?

看下面的代码

  const [dateVisible, setDateVisible] = useState(false)
  const [currentDate, setCurrentDate]=useState(()=>{
    return dayjs().format('YYYY-MM')
  }
  useEffect(()=>{
    const nowDate=dayjs().format('YYYY-MM')
    setCurrentMonthList(monthGroup[nowDate]||[]) 
  },[monthGroup])
  const onConfirm=(date)=>{
   setDateVisible(false) 
   const dateStr=dayjs(date).format('YYYY-MM')
   setCurrentMonthList(monthGroup[dateStr]||[])
   setCurrentDate(dateStr)
  }
  // 下面是return的部分
  {/* 时间切换区域 */}
  <div className="date" onClick={() => setDateVisible(true)}>
    <span className="text">
      {currentDate+"月账单"}
    </span>
    <span className={classNames('arrow', dateVisible && 'expand')}></span>
  </div>
  {/* 时间选择器 */}
  <DatePicker
    className="kaDate"
    title="记账日期"
    precision="month"
    visible={dateVisible}
    onCancel={() => setDateVisible(false)}
    onConfirm={onConfirm}
    onClose={() => setDateVisible(false)}
    max={new Date()}
  />
</div>

正是这些代码实现了时间选择器的功能,下面我们一起详细分析一下这些代码

1. 状态管理

这段代码通过useState定义了两个状态变量,这两个变量作用很大

  • dateVisible用于控制日期选择器的显示隐藏状态,初始值为false表示默认不显示;
  • currentDate存储当前选择的年月,使用dayjs().format('YYYY-MM')初始化值为当前月份,格式化为"2023-10"这样的字符串形式。

2. 数据初始化

代码使用useEffect在组件挂载或monthGroup数据变化时执行初始化操作,首先获取当前月份的格式化字符串,然后从monthGroup对象中取出对应月份的账单数据,如果没有数据则设置为空数组,最后通过setCurrentMonthList更新状态,这个副作用函数的依赖项是monthGroup确保数据源更新时重新初始化

这样便完成了时间的初始化(当前日期)和更新

3. 日期确认逻辑

onConfirm函数处理日期选择确认后的操作流程,先关闭日期选择器弹窗,然后将选择的日期对象格式化为"YYYY-MM"字符串,接着从monthGroup中获取新日期对应的账单数据如果没有则使用空数组,最后更新当前日期和账单列表两个状态,触发界面重新渲染显示最新数据

4. 日期显示区域

这部分UI代码实现了一个可点击的日期显示区域,点击时会打开日期选择器,显示内容包括当前年月和一个小箭头图标,其中年月文本通过拼接currentDate状态和固定文字"月账单"组成,箭头图标使用classNames库根据dateVisible状态动态添加expand类名实现旋转效果。

5. 日期选择器组件

代码中配置了一个功能完整的日期选择器组件,设置precision="month"限定只能选择年月,max={new Date()}禁止选择未来日期,提供了确认、取消和关闭三种操作方式的事件回调,其中确认操作触发onConfirm函数,其他操作都执行关闭弹窗的逻辑。

结语

通过这个记账项目,我们完整实践了React的核心开发模式:组件化思维、状态管理和数据驱动UI。 从路由设计到Redux状态管理,再到日期选择器的交互实现,每个环节都体现了React开发的精髓。

建议初学者可以基于此项目进行扩展练习,比如添加账单分类统计、优化加载状态等。记住,掌握React的关键在于理解其"状态-UI"的绑定关系,这个项目为你提供了绝佳的实践案例。愿你在React的学习道路上越走越远!