• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们实现了 mini calendar,为啥要加个 mini 呢?

因为它与真实用的 Calendar 组件相比,还是过于简单了。

这节我们再来写一个复杂一些的,真实项目用的 Calendar 组件:

用 cra 创建个项目:

npx create-react-app --template typescript calendar-component

先不着急写,我们先理一下思路:

日历组件的核心是什么?

是拿到每月的天数,每月的第一天是星期几。

比如这个月:

我们知道这个月有 30 天,第一天是周三,那就知道如何显示这个月的日历了。

那如何知道每月的天数呢?

上节讲过,用 Date 的 api 就可以。

当然,也可以用 dayjs,它封装了这些:

安装 dayjs:

npm install --save dayjs

在 test.js 写如下代码:

const dayjs = require("dayjs");

console.log(dayjs("2023-11-1").daysInMonth());

console.log(dayjs("2023-11-1").startOf("month").format("YYYY-MM-DD"));

console.log(dayjs("2023-11-1").endOf("month").format("YYYY-MM-DD"));

创建一个 dayjs 的对象,然后用 daysInMonth 方法可以拿到这个月的天数,用 startOf、endOf 可以拿到这个月的第一天和最后一天的日期。

跑一下:

这次 Calendar 组件我们用 dayjs 的 api 来实现。

很多组件库的 Calendar 组件都是基于 dayjs 设置和返回日期的。

比如 antd 的:

下面正式来写 Calendar 组件。

创建 src/Calendar/index.tsx

import "./index.scss";

function Calendar() {
    return <div className="calendar"></div>;
}

export default Calendar;

还有样式 src/Calendar/index.scss

.calendar {
    width: 100%;

    height: 200px;
    background: blue;
}

这里用到了 scss,需要安装下用到的包:

npm install --save sass

然后在 App.tsx 里引入 Calendar 组件:

import Calendar from "./Calendar";

function App() {
    return (
        <div className="App">
            <Calendar></Calendar>
        </div>
    );
}

export default App;

跑一下:

npm run start

这样,sass 就引入成功了。

这个组件可以分为 Header 和 MonthCalendar 两个组件。

我们先写下面的 MonthCalender 组件:

首先是周日到周六的部分:

src/Calendar/MonthCalendar.tsx

function MonthCalendar() {
    const weekList = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];

    return (
        <div className="calendar-month">
            <div className="calendar-month-week-list">
                {weekList.map((week) => (
                    <div className="calendar-month-week-list-item" key={week}>
                        {week}
                    </div>
                ))}
            </div>
        </div>
    );
}

export default MonthCalendar;

先把周日到周一渲染出来,然后在 src/Calendar/index.scss 里写下样式:

.calendar {
    width: 100%;
}

.calendar-month {
    &-week-list {
        display: flex;
        padding: 0;
        width: 100%;
        box-sizing: border-box;
        border-bottom: 1px solid #ccc;

        &-item {
            padding: 20px 16px;
            text-align: left;
            color: #7d7d7f;
            flex: 1;
        }
    }
}

样式用 display:fex 加 flex:1,这样就是每个列表项平分剩余空间,然后加上 padding。

在 src/Calendar/index.tsx 里引入:

import MonthCalendar from "./MonthCalendar";
import "./index.scss";

function Calendar() {
    return (
        <div className="calendar">
            <MonthCalendar />
        </div>
    );
}

export default Calendar;

这样,上面的 week list 就完成了:

然后是下面部分:

思路前面分析过了,就是拿到当前月份的天数和第一天是星期几,前后用上个月和下个月的日期填充。

我们给 Calendar 组件加一个 value 的 props,也就是当前日期。

value 我们选择用 Dayjs 类型,当然,用 Date 也可以。

import { Dayjs } from "dayjs";
import MonthCalendar from "./MonthCalendar";
import "./index.scss";

export interface CalendarProps {
    value: Dayjs;
}

function Calendar(props: CalendarProps) {
    return (
        <div className="calendar">
            <MonthCalendar {...props} />
        </div>
    );
}

export default Calendar;

在 MonthCalendar 也加上 props:

interface MonthCalendarProps extends CalendarProps {}

在 App.tsx 传入参数:

这样,MonthCalendar 就可以根据传入的 value 拿到当前的月份信息了。

我们加一个 getAllDays 方法,打个断点:

function getAllDays(date: Dayjs) {
    const daysInMonth = date.daysInMonth();
    const startDate = date.startOf("month");
    const day = startDate.day();
}
const allDays = getAllDays(props.value);

然后创建个调试配置:

点击调试启动:

可以看到,拿到了这个月的天数,是 30 天。

接下来我们边调试边写。

不管这个月有多少天,我们日历都是固定 6 * 7 个日期:

所以创建一个 6 * 7 个元素的数组,这个月第一天之前的用第一天的日期 -1、-2、-3 这样计算出来:

function getAllDays(date: Dayjs) {
    const daysInMonth = date.daysInMonth();
    const startDate = date.startOf("month");
    const day = startDate.day();

    const daysInfo = new Array(6 * 7);

    for (let i = 0; i < day; i++) {
        daysInfo[i] = {
            date: startDate.subtract(day - i, "day").format("YYYY-MM-DD"),
        };
    }

    debugger;
}

11 月 1 日是星期三:

那也就是要在之前填充星期日、星期一、星期二,这 3 天的日期:

这里用 dayjs 的 subtract 方法就可以计算当前日期 -1、-2、-3 的日期。

再写一段逻辑,点击刷新:

function getAllDays(date: Dayjs) {
    const daysInMonth = date.daysInMonth();
    const startDate = date.startOf("month");
    const day = startDate.day();

    const daysInfo = new Array(6 * 7);

    for (let i = 0; i < day; i++) {
        daysInfo[i] = {
            date: startDate.subtract(day - i, "day").format("YYYY-MM-DD"),
        };
    }

    for (let i = day; i < daysInfo.length; i++) {
        daysInfo[i] = {
            date: startDate.add(i - day, "day").format("YYYY-MM-DD"),
        };
    }

    debugger;
}

这个循环就是填充剩下的日期的,从 startDate 开始 +1、+2、+3 计算日期。

hover 上去可以看到,计算的结果是对的:

然后把 format 删掉,这里不需要格式化。再添加一个属性标识是否是当前月份的。

function getAllDays(date: Dayjs) {
    const startDate = date.startOf("month");
    const day = startDate.day();

    const daysInfo = new Array(6 * 7);

    for (let i = 0; i < day; i++) {
        daysInfo[i] = {
            date: startDate.subtract(day - i, "day"),
            currentMonth: false,
        };
    }

    for (let i = day; i < daysInfo.length; i++) {
        const calcDate = startDate.add(i - day, "day");

        daysInfo[i] = {
            date: calcDate,
            currentMonth: calcDate.month() === date.month(),
        };
    }

    return daysInfo;
}

就是先 -1、-2、-3 计算本月第一天之前的日期,然后从第一天开始 +1、+2、+3 计算之后日期。

返回值处打个断点,刷新下:

当前月份的日期、之前几天的日期、之后几天的日期都有了。

这样,日历的数据就准备好了。

其实上一节我们也是这么做的,只不过用的是 Date 的 api,而这节换成 dayjs 的 api 了。

再声明下返回的数组的类型:

const daysInfo: Array<{ date: Dayjs; currentMonth: boolean }> = new Array(
    6 * 7
);

数据准备好了,接下来就可以渲染了:

<div className="calendar-month-body">{renderDays(allDays)}</div>
function renderDays(days: Array<{ date: Dayjs; currentMonth: boolean }>) {
    const rows = [];
    for (let i = 0; i < 6; i++) {
        const row = [];
        for (let j = 0; j < 7; j++) {
            const item = days[i * 7 + j];
            row[j] = (
                <div className="calendar-month-body-cell">
                    {item.date.date()}
                </div>
            );
        }
        rows.push(row);
    }
    return rows.map((row) => (
        <div className="calendar-month-body-row">{row}</div>
    ));
}

这里就是把 6 * 7 个日期,按照 6 行,每行 7 个来组织成 jsx。

scss 部分如下:

image.png

&-body {
    &-row {
        height: 100px;
        display: flex;
    }
    &-cell {
        flex: 1;
        border: 1px solid #eee;
        padding: 10px;
    }
}

每行的每个单元格用 flex:1 来分配空间,然后设置个 padding。

渲染出来是这样的:

然后当前月和其他月份的日期加上个不同颜色区分:

function renderDays(days: Array<{ date: Dayjs; currentMonth: boolean }>) {
    const rows = [];
    for (let i = 0; i < 6; i++) {
        const row = [];
        for (let j = 0; j < 7; j++) {
            const item = days[i * 7 + j];
            row[j] = (
                <div
                    className={
                        "calendar-month-body-cell " +
                        (item.currentMonth
                            ? "calendar-month-body-cell-current"
                            : "")
                    }>
                    {item.date.date()}
                </div>
            );
        }
        rows.push(row);
    }
    return rows.map((row) => (
        <div className="calendar-month-body-row">{row}</div>
    ));
}

color: #ccc;
&-current {
    color: #000;
}

这样,我们的日历就基本完成了:

切换日期是在 Header 部分做的,接下来写下这部分:

写下 src/Calendar/Header.tsx:

function Header() {
    return (
        <div className="calendar-header">
            <div className="calendar-header-left">
                <div className="calendar-header-icon">&lt;</div>
                <div className="calendar-header-value">2023 年 11 月</div>
                <div className="calendar-header-icon">&gt;</div>
                <button className="calendar-header-btn">今天</button>
            </div>
        </div>
    );
}

export default Header;

还有对应的样式:

.calendar-header {
    &-left {
        display: flex;
        align-items: center;

        height: 28px;
        line-height: 28px;
    }

    &-value {
        font-size: 20px;
    }

    &-btn {
        background: #eee;
        cursor: pointer;
        border: 0;
        padding: 0 15px;
        line-height: 28px;

        &:hover {
            background: #ccc;
        }
    }

    &-icon {
        width: 28px;
        height: 28px;

        line-height: 28px;

        border-radius: 50%;
        text-align: center;
        font-size: 12px;

        user-select: none;
        cursor: pointer;

        margin-right: 12px;
        &:not(:first-child) {
            margin: 0 12px;
        }

        &:hover {
            background: #ccc;
        }
    }
}

这部分就是用 flex + margin 来实现布局,就不展开讲了。

渲染出来是这样的:

这样我们就完成了布局部分。

案例代码上传了小册仓库。

总结

这节我们开始实现一个真实的 Calendar 组件。

我们不再用 Date 获取当前月、上个月、下个月的天数和星期几,而是用 dayjs 的 api。

我们完成了布局部分,包括用于切换月份的 Header 和每个月的日期 MonthCalender 组件。

我们使用 sass 来管理样式。

上面的周几、下面的日期我们都是用的 flex 布局,这样只要外层容器大小变了,内层就会跟着变。

下节我们来实现具体的组件逻辑。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
7.组件实战:迷你Calendar
Next
9.组件实战:Calendar日历组件(下)