前言
最近在写的一个分布式调度系统,后端同学需要让我传入cron表达式,给调度接口传参。我去学习,了解了cron表达式的用法,发现有3个通用的表达式刚好符合我们的需求:
需求
- 每天 xx 的时间:
0 11 20 * * ?
上面是每天20:11的cron表达式
- 每周的 xxx 星期 的 xxx 时间
0 14 20 * * WED,THU
上面是 每周星期三,星期四20:14的cron表达式
- 每周的 xxx号 的 xxx时间
0 15 20 3,7 * ?
上面是 每月的3,7号20:15的cron表达式
这三个表达式刚好符合我们的需求,并且每个符号表达的意思也很直观。那么,话不多说,直接开写!
环境
-
react
- 我的版本:"react": "18.2.0",用的函数组件+react hooks
-
moment
- npm install moment
-
semi-design(组件库)
- npm install semi-design
-
typeScript
实现
数据
utils下创建cron.ts,对组件所用到的数据进行统一的管理:
tsx
export const dayOfTheWeekData = [
{ key: 'MON', label: '星期一' },
{ key: 'TUE', label: '星期二' },
{ key: 'WED', label: '星期三' },
{ key: 'THU', label: '星期四' },
{ key: 'FRI', label: '星期五' },
{ key: 'SAT', label: '星期六' },
{ key: 'SUN', label: '星期天' }
];
export const dayOfTheWeekOption = [
{ key: '1', label: '星期一' },
{ key: '2', label: '星期二' },
{ key: '3', label: '星期三' },
{ key: '4', label: '星期四' },
{ key: '5', label: '星期五' },
{ key: '6', label: '星期六' },
{ key: '7', label: '星期天' }
];
export const monthOption = [
{ key: '1', label: '一月' },
{ key: '2', label: '二月' },
{ key: '3', label: '三月' },
{ key: '4', label: '四月' },
{ key: '5', label: '五月' },
{ key: '6', label: '六月' },
{ key: '7', label: '七月' },
{ key: '8', label: '八月' },
{ key: '9', label: '九月' },
{ key: '10', label: '十月' },
{ key: '11', label: '十一月' },
{ key: '12', label: '十二月' }
];
//获取dayOfTheMonthOption的每月对象
function getDayOfTheMonthOption() {
const days = [];
for (let i = 1; i < 32; i += 1) {
days.push({ key: i.toString(), label: i.toString().concat('号') });
}
return days;
}
export const dayOfTheMonthOption = getDayOfTheMonthOption();
组件
到了组件的具体实现,个人感觉我写的注释挺全的,就单挑几个核心重点讲下:
时间转换函数(handleTimeChange)
tsx
//时间选择函数
const handleTimeChange = (time: moment.Moment | null) => {
setSelectTime(time);
if (!time) return;
const currentCron = expression ? expression.split(' ') : [];
const [seconds, , , dayOfMonth, month1, dayOfWeek] = currentCron;
const minutes = moment(time).minutes().toString(); //获取分钟
const hours = moment(time).hours().toString(); //获取小时
let result = null;
if (!Number.isNaN(Number(hours)) && !Number.isNaN(Number(minutes))) {
const minutesAndHour = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space);
if (defaultTimeType === 'everyDay') result = minutesAndHour.concat('* * ?');
if (defaultTimeType !== 'everyDay')
result = minutesAndHour
.concat(dayOfMonth)
.concat(space)
.concat(month1)
.concat(space)
.concat(dayOfWeek);
}
if (result) onChange?.(result);
setExpression(result);
};
- 使用moment函数将time转成数字类型
- minutes = moment(time).minutes().toString(); //获取分钟
- hours = moment(time).hours().toString(); //获取小时
- 获取时间cron字符串minutesAndHour:
lua
const minutesAndHour = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space);
- 拼接得到完整的cron表达式: 1. defaultTimeType === 'everyDay'
result = minutesAndHour.concat('* * ?');
- defaultTimeType !== 'everyDay'
lua
result = minutesAndHour.concat(dayOfMonth).concat(space).concat(month1).concat(space).concat(dayOfWeek);
日期转换函数(handleSelectChange)
tsx
setSelectedValue(data);
const selectValues = data.join(',');
const currentCron = expression ? expression.split(' ') : [];
const [seconds, minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
let result = '';
if (defaultTimeType === 'everyWeek') {
result = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space)
.concat(dayOfMonth)
.concat(space)
.concat(month1)
.concat(space)
.concat(selectValues);
}
if (defaultTimeType === 'everyMonth') {
result = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space)
.concat(data.length ? selectValues : '*')
.concat(space)
.concat(month1)
.concat(space)
.concat(dayOfWeek);
}
if (selectTime) onChange?.(result);
setExpression(result);
- defaultTimeType === 'everyWeek'
tsx
result = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space).concat(dayOfMonth).concat(space).concat(month1).concat(space).concat(selectValues);
- defaultTimeType === 'everyMonth'
tsx
result = seconds.concat(space).concat(minutes).concat(space).concat(hours).concat(space).concat(data.length ? selectValues : '*').concat(space).concat(month1).concat(space).concat(dayOfWeek);
组件全部代码(CronInput)
tsx
import { ConfigProvider, TimePicker } from '@douyinfe/semi-ui';
import { Fragment, useState } from 'react';
import { Select } from '@douyinfe/semi-ui';
import moment from 'moment';
//引入数据
import { dayOfTheMonthOption, dayOfTheWeekData } from '@/utils/cron';
const { Option } = Select;
const format = 'HH:mm';
const defaultCron = '0 * * * * ?';
const space = ' '; //空格
//类型选择
const timeTypes = [
{ key: 'everyDay', label: '每天' },
{ key: 'everyWeek', label: '每周' },
{ key: 'everyMonth', label: '每月' }
];
interface Props {
onChange?: (cron?: string) => void;
}
const CronInput: React.FC<Props> = ({ onChange }) => {
const [defaultTimeType, setDefaultTimeType] = useState(timeTypes[0].key); //选择类型
const [selectedValue, setSelectedValue] = useState<[]>([]); //日期,多选数组
const [selectTime, setSelectTime] = useState<any>(null); //时间
const [expression, setExpression] = useState<string | null>(defaultCron); //bzd
//类型选择函数
const handleTimeTypeChange = (selectValue: string) => {
setDefaultTimeType(selectValue);
setSelectTime(null);
setSelectedValue([]);
setExpression(defaultCron);
};
//时间选择函数
const handleTimeChange = (time: moment.Moment | null) => {
setSelectTime(time);
if (!time) return;
const currentCron = expression ? expression.split(' ') : [];
const [seconds, , , dayOfMonth, month1, dayOfWeek] = currentCron;
const minutes = moment(time).minutes().toString(); //获取分钟
const hours = moment(time).hours().toString(); //获取小时
let result = null;
if (!Number.isNaN(Number(hours)) && !Number.isNaN(Number(minutes))) {
const minutesAndHour = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space);
if (defaultTimeType === 'everyDay') result = minutesAndHour.concat('* * ?');
if (defaultTimeType !== 'everyDay')
result = minutesAndHour
.concat(dayOfMonth)
.concat(space)
.concat(month1)
.concat(space)
.concat(dayOfWeek);
}
if (result) onChange?.(result);
setExpression(result);
};
const handleSelectChange = (data: []) => {
setSelectedValue(data);
const selectValues = data.join(',');
const currentCron = expression ? expression.split(' ') : [];
const [seconds, minutes, hours, dayOfMonth, month1, dayOfWeek] = currentCron;
let result = '';
if (defaultTimeType === 'everyWeek') {
result = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space)
.concat(dayOfMonth)
.concat(space)
.concat(month1)
.concat(space)
.concat(selectValues);
}
if (defaultTimeType === 'everyMonth') {
result = seconds
.concat(space)
.concat(minutes)
.concat(space)
.concat(hours)
.concat(space)
.concat(data.length ? selectValues : '*')
.concat(space)
.concat(month1)
.concat(space)
.concat(dayOfWeek);
}
if (selectTime) onChange?.(result);
setExpression(result);
};
const RenderSelect = ({
placeholder,
data = []
}: {
placeholder: string;
data: { key: string; label: string }[];
}) => {
return (
<Fragment>
<Select
multiple
placeholder={placeholder}
onChange={(val: any) => handleSelectChange(val)}
style={{ marginRight: '16px', width: 'auto' }}
value={selectedValue}
>
{data.map((item: { key: string; label: string }) => (
<Option key={item.key} value={item.key}>
{item.label}
</Option>
))}
</Select>
<ConfigProvider>
<TimePicker
value={selectTime && moment(selectTime, format).toDate()}
format={format}
placeholder="请选择时间"
onChange={(val: any) => handleTimeChange(val)}
/>
</ConfigProvider>
</Fragment>
);
};
return (
<>
<div className={'cron'}>
<Select
// role="cron-type"
style={{ marginRight: '16px', width: 'auto' }}
placeholder="请选择类型"
onChange={(val: any) => handleTimeTypeChange(val)}
value={defaultTimeType}
>
{timeTypes.map((item) => (
<Option key={item.key} value={item.key}>
{' '}
{item.label}
</Option>
))}
</Select>
{defaultTimeType === 'everyDay' && (
<ConfigProvider>
<TimePicker
value={selectTime && moment(selectTime, format).toDate()}
format={format}
placeholder="请选择时间"
onChange={(val: any) => handleTimeChange(val)}
/>
</ConfigProvider>
)}
{defaultTimeType === 'everyWeek' && (
<RenderSelect data={dayOfTheWeekData} placeholder="请选择星期" />
)}
{defaultTimeType === 'everyMonth' && (
<RenderSelect data={dayOfTheMonthOption} placeholder="请选择日期" />
)}
</div>
</>
);
};
export default CronInput;
使用与效果
使用
使用方法很简单,接收onChange传来的cron表达式即可:
tsx
const App: FC<IProps> = (props) => {
const { datas = [] } = props;
let [value, setValue] = useState<string>();
return (
<div>
<CronInput onChange={(cron) => setValue(cron)} />
<div>{value}</div>
</div>
);
};
效果
- 每天
- 每周
- 每月