☰
基于EasyUI和jQuery封装企业级日期时间选择器组件库实践
2026/10/2 15:30:49 网站建设 项目流程

这套组件库做出来有一阵子了,一直想找机会把它拆开聊聊。起因说起来也简单,那阵子公司内部接了三个后台项目,全是老牌ERP和OA系统的延续开发,页面里大量用到日期时间选择器。一开始没当回事,EasyUI自带的datebox、datetimebox本身就能用,可真到了业务场景里才发现,需求远比想象中复杂:订单查询要"本周、本月、本季度"的快捷筛选,报表模块要做时间范围的快速联动,工单系统要求输入后自动格式化,还有一堆老项目遗留的日期格式要兼容。每个项目单独写,那就是三份差不多的代码反复粘贴改来改去,维护起来痛不欲生。所以最后拍板:基于EasyUI和jQuery封装一个统一的企业级日期时间选择器组件库,按标准jQuery插件开发规范来做。这篇博客就是把当时的设计思路、实现过程和踩过的一些坑都说一遍,给还在维护jQuery系老项目的朋友一个参考。

1. 为什么还在用EasyUI和jQuery?企业级项目的现实选择

1.1 老技术栈在企业里的真实生态

聊这个组件库之前,得先回答一个很多人会问的问题:202X年了,为什么还要折腾EasyUI和jQuery?

答案很简单:因为线上系统还在用。很多传统企业核心业务跑得最稳的系统,都是十年前甚至更早用jQuery系技术栈搭起来的。人事系统、财务流程、仓储管理、ERP后台,这套东西至今还在每天产生真实业务数据。系统稳定运行着,没人敢拍板说"再造一把",可需求不会消失,新功能还得往上加。

在这种背景下,前端技术选型不是"哪个时髦用哪个",而是"哪个能让新功能和旧系统稳定兼容"。EasyUI和jQuery这套组合,恰恰是这些老系统里存量最大、开发人员最熟的技术方案。与其在旧系统里嵌一个React应用再解决通信和构建问题,不如顺着现有技术栈,把常用组件沉淀成一套可复用的组件库,提升后续所有项目的开发效率。我们这套日期时间选择器就是在这个逻辑下诞生的。

1.2 技术上为什么还够用

说句公道话,在特定场景下,EasyUI + jQuery的性能表现并不差。jQuery本身只是一个DOM操作库,开销很低;EasyUI的组件也足够成熟,各种浏览器兼容性、表单交互细节都有多年打磨的积累。特别是对IE11这类旧环境,现代框架要做一堆polyfill才能跑,jQuery基本开箱即用。

我们的目标不是做一个替代主流前端框架的UI库,而是在既有技术栈内做一次局部升级:把散落在各个项目里的日期选择代码,收敛成一套符合jQuery插件规范、模块化、可复用的组件。这套组件依然依赖EasyUI的基础能力和jQuery的生态,但对业务开发者来说,使用体验可以接近现代框架的组件化开发。这是务实的工程决策,也符合渐进式重构的思路。

2. 组件库的整体设计与架构思路

2.1 需求梳理:日期时间选择器到底要解决什么问题

开始写代码前,我把几个项目里的日期时间需求仔细捋了一遍,列出一张需求清单:

需求分类具体描述来源项目
基础选择日期选择、时间选择、日期+时间组合选择,支持格式化输出所有项目通用
快捷选项今天、昨天、本周、本月、本季度、本年,一键填充订单报表、财务统计
范围联动开始时间不能晚于结束时间,结束时间不能早于开始时间报表筛选、工单区间查询
回填限制最小时间、最大时间限制,避免选择未来日期或超过业务允许范围考勤补卡、排产计划
自定义格式化不同业务要求"YYYY-MM-DD"、"YYYY年MM月DD日"、"MM/DD HH:mm"等多样格式各项目混杂历史格式
兼容旧数据旧系统存下来的字符串日期格式不统一,组件需要自动识别数据迁移、历史单据

这张清单就是组件库的需求边界。明确了核心是选择器、范围联动和快捷方式,其他花哨功能先不做,避免组件膨胀。

2.2 模块化设计:高内聚、低耦合的落地方式

架构上把组件库拆成了几个独立模块,每个模块只做一件事:

  • 核心选择器模块(picker核心):负责日期面板的渲染、翻页、选中交互,不关心业务逻辑。
  • 时间范围模块(range):管理开始/结束时间的关联逻辑,负责两边参数互相约束。
  • 格式化工具模块(format):解析各种日期输入格式,输出标准化的日期字符串。
  • 快捷项模块(shortcut):注册"本周、本月"等快捷键,点击后自动计算时间区间。
  • 适配器模块(bridge):负责对接EasyUI的datebox和datetimebox,把自定义内容挂靠到EasyUI组件上。

模块之间通过配置项和事件通信,互不直接依赖。后续如果我只需要"纯日期选择"而没有快捷项,完全可以只引入核心模块和格式化工具,打包体积更小。这个设计思路后来验证了确实好用——有个项目只需要日期范围联动,我在引入时直接把shortcut模块跳过,省了不少带宽和解析开销。

3. 按jQuery插件规范实现组件的核心机制

3.1 jQuery插件的标准骨架

既然标题里写明"采用标准的jQuery插件开发规范",这里就详细说一下这套规范在我们组件里的落地方式。一个符合规范、可被团队其他人维护的jQuery插件,通常具备以下特征:

  1. 避免全局污染:插件作用域封装在一个IIFE(立即执行函数)里,只对外暴露一个jQuery方法名。
  2. 默认配置统一管理:通过$.fn.组件名.defaults暴露默认配置,用户可以用$.fn.组件名.defaults覆盖全局默认值。
  3. 实例级配置合并:插件方法内部通过$.extend({}, $.fn.组件名.defaults, options)把默认和实例配置合并。
  4. 支持链式调用:方法返回调用对象this,确保插件可以继续链式调用其他jQuery方法。
  5. 方法路由:通过第一个参数决定是"初始化"还是要调用某个内置方法,比如$('#id').dateRangePicker('getValue')。

我们的组件库即遵循这套规范。以核心选择器为例,插件入口长这样:

(function ($, window) { 'use strict'; var DatePicker = function (element, options) { this.$element = $(element); this.options = $.extend(true, {}, DatePicker.defaults, options); this._init(); }; DatePicker.prototype._init = function () { // 初始化逻辑 }; // 标准jQuery插件方法 $.fn.datePicker = function (options) { return this.each(function () { var $this = $(this); var instance = $this.data('datePicker'); if (!instance) { instance = new DatePicker(this, options); $this.data('datePicker', instance); } }); }; // 全局默认配置 $.fn.datePicker.defaults = { format: 'YYYY-MM-DD', minDate: null, maxDate: null, shortcuts: ['today', 'week', 'month'] }; })(jQuery, window);

这段代码里最关键的两个细节是$.extend(true, {}, ...)深合并和this.each()循环处理。如果只做浅合并,配置里的嵌套对象会被整体覆盖,比如我自定义了一个快捷项{label: '近三天', dayOffset: -3},浅合并会把整个shortcuts数组替换掉,导致默认项全丢。深合并则能逐字段覆盖,保留默认值和实例配置的精细组合。

3.2 EasyUI datebox的扩展思路与实现

EasyUI的datebox本身是一个成熟控件,但它有两个痛点:一是快捷选项不好扩展,二是集成业务逻辑(比如范围联动)要在外部写大量onSelect回调。我们的做法是把datebox作为底层面板,在其基础上包一层自定义逻辑,通过$.extend重写部分内置方法。

EasyUI允许开发者重写组件的默认行为,这正是我们需要的入口。对于日期格式化,EasyUI依赖parser和formatter。组件库通过扩展这两个方法,让datebox理解我们传入的各种日期格式,同时输出业务要求的格式。

// 扩展EasyUI datebox的默认方法 $.extend($.fn.datebox.defaults, { // 重写formatter,支持规则配置 formatter: function (date) { var opts = $(this).datebox('options'); return dateFmt.optsFormatter(date, opts.customFormat || opts.formatter); }, // 重写parser,识别多种输入格式 parser: function (s) { var opts = $(this).datebox('options'); return dateFmt.optsParser(s, opts.parserFormats || ['YYYY-MM-DD', 'YYYY/MM/DD']); }, // 增加自定义事件回调 onRangeChange: function (start, end) { } });

关键点在于:我们不是在替换EasyUI,而是在它的扩展机制上做增强。这样既保留了EasyUI官方组件的稳定性,又让组件库有了独立业务逻辑的附着力。

3.3 核心代码解析:配置合并背后的"为什么"

配置合并这件事,看起来简单,实际上决定了整个组件的灵活度。我先把核心合并逻辑再贴一遍,然后展开说说从"能跑"到"好用"的几个关键调整:

var DateRangePicker = function (element, options) { this.$element = $(element); this.options = $.extend(true, {}, DateRangePicker.defaults, options); this._init(); }; DateRangePicker.defaults = { format: 'YYYY-MM-DD', linkStart: null, // 关联的开始输入框jQuery对象 linkEnd: null, // 关联的结束输入框jQuery对象 quickOptions: [ { text: '今天', value: 'today' }, { text: '本周', value: 'thisWeek' }, { text: '本月', value: 'thisMonth' }, { text: '本季度', value: 'thisQuarter' }, { text: '本年', value: 'thisYear' } ] };

第一次写这个组件时,我用的是浅合并。结果在某个项目里配置了一个quickOptions: [{ text: '近7天', value: 'last7Days' }],没想到默认的五项全没了,页面上只剩一个"近7天"。查了半天才发现是浅合并把整个数组覆盖了——$.extend({}, defaults, options)对数组的合并方式就是直接覆盖,根本不逐个元素合并。换成$.extend(true, {}, ...)之后一切正常。

第二个调整是linkStart和linkEnd的设计。数据范围联动有这么一种实现方式:在开始日期变化的回调里手动设置结束输入框的min属性。第一次做成了硬编码,可组件本身对输入框id是未知的。后来把关联逻辑抽象成配置项,由外界传入关联输入框的jQuery对象,组件内部统一注册回调,这样不同页面传不同的关联目标就行,组件本体不再有任何页面的痕迹。

4. 实操过程与完整实现解析

4.1 目录结构与构建脚本

组件库采用模块化目录结构,每个模块独立文件,最后用构建脚本打包成单文件交付:

src/ |-- core/ | |-- picker.js // 选择器核心逻辑 | +-- definiton.js // 公共常量与类型定义 |-- modules/ | |-- range.js // 范围联动模块 | |-- shortcut.js // 快捷选项模块 | +-- format.js // 日期格式化与解析模块 |-- bridge/ | +-- easyui-adapter.js // EasyUI适配层 |-- css/ | |-- date-picker.css | +-- shortcut.css +-- dist/ +-- jquery-date-picker.bundle.js

构建脚本不用webpack这种大家伙,用最简单的Grunt或者纯Node脚本合并文件就行。老项目的历史包袱通常比较重,引入webpack等构建工具要考虑与现有构建链路集成,直接提供一个合并好的成品文件反而最省事。我们最后采用的方案是:开发时分别维护模块文件,发布时用一个Node脚本做简单拼接,同时打包出min版本。

4.2 日期面板的渲染与交互实现

日期面板是组件库视觉和交互的核心。EasyUI的datebox自带面板,但样式和交互定制起来受限制。我们选择自建面板,方法上参考EasyUI的日历逻辑,但用组件库自己的方式实现,这样后续定制完全不受上游牵制。

面板渲染的骨架逻辑大致如下:

function renderCalendar(year, month, selectedDate) { var firstDay = new Date(year, month - 1, 1); var startWeek = firstDay.getDay(); // 0=周日 var daysInMonth = new Date(year, month, 0).getDate(); var calendarHtml = '<div class="dp-calendar">'; // 渲染星期表头 calendarHtml += '<div class="dp-week-header">' + ['日', '一', '二', '三', '四', '五', '六'].map(function (w) { return '<span>' + w + '</span>'; }).join('') + '</div>'; // 渲染日期格子 var dayCount = 0; var usedCells = startWeek + daysInMonth; var totalCells = Math.ceil(usedCells / 7) * 7; for (var i = 0; i < totalCells; i++) { if (i % 7 === 0) calendarHtml += '<div class="dp-week-row">'; var dayNum = i - startWeek + 1; if (dayNum < 1 || dayNum > daysInMonth) { calendarHtml += '<span class="dp-day dp-day-empty"></span>'; } else { var isSelected = selectedDate && selectedDate.getFullYear() === year && selectedDate.getMonth() === month - 1 && selectedDate.getDate() === dayNum; var dayClass = isSelected ? 'dp-day dp-day-selected' : 'dp-day'; calendarHtml += '<span class="' + dayClass + '">var rangeModule = new RangeManager({ startInput: $('#startTime'), endInput: $('#endTime'), onStartChange: function (startDate) { // 结束input的最小时间不能早于开始时间 this.endPicker.setMinDate(startDate); // 如果结束时间比开始时间还早,自动修正 if (this.endPicker.getDate() && this.endPicker.getDate() < startDate) { this.endPicker.setDate(null); } }, onEndChange: function (endDate) { // 开始input的最大时间不能晚于结束时间 this.startPicker.setMaxDate(endDate); } });

快捷选项模块就更直观了,它本质上是一组把"相对日期"翻译成"绝对日期"的函数。比如"本季度"的实现:

function getQuarterRange() { var now = new Date(); var year = now.getFullYear(); var quarter = Math.floor(now.getMonth() / 3); var startMonth = quarter * 3; var endMonth = startMonth + 2; return { start: new Date(year, startMonth, 1), end: new Date(year, endMonth, 31) }; }

这里有个小细节很容易踩坑:JavaScript的new Date(year, month, 31)当月份只有30天时会自动进位到下月初。所以要取"某月最后一天"不能硬写31,要用new Date(year, month + 1, 0)——月份+1且天数为0,代表上个月最后一天。

4.4 接入EasyUI:让组件无缝融入既有项目

组件库的对外入口是一个适配层,把EasyUI的datebox和我们的组件桥接起来。具体做法是:将自定义面板挂接到EasyUI组件的"弹出框"里,用EasyUI的show和hide方法控制显示隐藏,并同步监听日期选中事件。

$.fn.easyuiDatePicker = function (options) { return this.each(function () { var $input = $(this); if (!$input.data('easyuiDatePicker')) { $input.datebox({ // 关闭EasyUI自带的日历面板 panel: 'custom-date-picker-panel', width: 220, onSelect: function (date) { // 这里交给我们的组件来处理实际交互 } }); var customPicker = $('<div>').datePicker(options); $input.data('easyuiDatePicker', { customPicker: customPicker, input: $input }); } }); };

实战下来,这种"挂靠"方式有一个明显好处:不需要改动EasyUI源码,所有扩展都是基于官方API进行的。老项目的统一登录、权限模块、表单校验都是围绕EasyUI组件建立的,新增组件没有破坏任何既有逻辑,升级风险几乎为零。

5. 常见问题与排查技巧实录

5.1 典型问题速查

现象可能原因解决方案
日期格式化后比实际少一天没有指定时区,浏览器把日期串当成UTC解析解析"YYYY-MM-DD"时手动new Date(year, month - 1, day),不要用new Date("2023-05-01")
二次打开面板时选中状态丢失选中日期存在临时变量,没有在_init时恢复在_init里读取已有值并调用面板的setSelected方法
快捷项"本月"范围算错只取了当前月第一天,忘了结束日期要取下个月的第0天结束日期用new Date(year, month + 1, 0)
范围联动后不能取消已选日期联动逻辑里每次变化都直接修正结束时间,没给用户清空的机会增加一个"允许清空"配置项,联动只设置最小/最大值,不强制填充结束时间
IE11里日期面板渲染位置偏移jQuery的position()在隐藏元素上拿不到正确值在面板显示完成后再获取位置并调用position方法
多个日期选择器在同一页面互相干扰共用同一个面板对象,切换目标时没有重新绑定事件每个实例维护独立面板,并且用data属性绑定当前选中的输入框

5.2 值得说说的坑

第一个大坑是EasyUI日期格式化和自定义组件的event循环问题。一开始在datebox的onSelect回调里调用我们的组件更新UI,结果触发了datebox的再次渲染,导致选择事件陷入死循环。排查了大半天,最后发现是因为我们的格式化输出结果通过$input.val()写回,触发了datebox的valueChange监听,重新调用了onSelect。解决办法是在写回前设一个防重入标志位,等赋值完成后再恢复。

第二个坑是性能。这组件库在处理大月份、大量日期格子时并不慢,但在老IE里反复操作会有明显的延迟。优化手段挺笨但有效:减少DOM操作次数。原来是一天一个div地append,改成拼好一整月的HTML字符串一次性插入,渲染时间缩短了三分之二。第二个优化是事件委托,整个面板只在容器上挂一个click监听,通过>$('#date').datePicker({ disabledDays: function (date) { var day = date.getDay(); return day === 0 || day === 6; // 周六周日不可选 } });

"特殊日期高亮"可以加一个highlightDays数组配置。因为核心面板只负责渲染和事件冒泡,具体哪天能被选中完全是配置决定的,扩展非常灵活。

6.2 与现代前端方案共存的可能性

可能有人会觉得,这组件库是不是意味着拒绝现代前端方案?事实上完全可以让它们并行。我们在这套组件库的接入层之外,保留了一个纯jQuery的API口子,可以让React、Vue项目中通过ref或wrapper直接调用。比如在React中:

useEffect(() => { const picker = $(dateInputRef.current).datePicker({ format: 'YYYY-MM-DD', shortcuts: ['thisMonth', 'lastMonth'] }); return () => { $(dateInputRef.current).datePicker('destroy'); }; }, []);

这种做法特别适合老系统局部上新框架的过渡期。新功能用React写,但日期选择器这种存量组件直接复用,让重构而不必一次重写到底。

6.3 版本、文档与团队协作细节

组件库做出来只算完成一半,另一半是要让团队用起来。我们的做法是把组件库当成一个独立项目维护,使用Git版本管理,每个版本附上CHANGELOG说明新增、修复和破坏性变更。文档方面不用那种大型文档站,一个精简的README加上几个可运行示例页面就够了。示例页面的价值怎么强调都不为过——我看到太多组件库文档写得不差,但就是没示例,使用者无从下手。我们为这个组件库写了"基础用法、范围联动、快捷选项、自定义格式化、接入EasyUI"五个示例页面,团队成员复制粘贴就能跑。

个人在实际操作里的体会是:任何组件库,如果使用者超过五个项目、使用时间超过三个月,就一定会有文档覆盖不到的需求冒出来。这时候别急着往组件库里加功能,先看看能不能通过在业务侧包一层代码解决。组件越收敛、越克制,它的生命力反而越长。我们这套组件库上线之后,几乎每周都有人提新需求,但真正最终合并进库的功能只占三成,其他都是在业务侧封装掉的。守住这个边界,组件库才不会变成一个大杂烩。

最后再分享一个小技巧:如果团队里有人抱怨说"这个日期选择器怎么和另一个项目的不一样",多半不是组件实现的问题,而是文档考古失败了。给每个接入项目在代码里留一行// 基于组件库 v2.1.0 接入,配置文件见 docs/example-range.html,能省掉无数个"我们那边不是这样"的对比排查。老技术栈的项目多了,这类隐性约定反而是最有价值的资产。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询