1. 为什么还要聊一个“老家伙”
早上翻代码库的时候,看到一个老项目里引了一整排jquery-ui.css、jquery-ui.js,心里忽然有点感慨。在 React、Vue 统治前端的现在,聊 jQuery UI 这件事本身就带着点“考古”的意思。但如果你翻过那些仍在稳定运行的内部系统、运营后台、旧版 ERP,会发现这个“老家伙”活得好好的,并且短时间内还会继续活下去。
jQuery UI 是什么?一句话说清楚:它是建立在 jQuery 之上的一套官方 UI 组件库。当你写出$( "#datepicker" ).datepicker()这样一行代码,页面上就出现一个完整的日期选择器——下拉选择年、月、日,支持范围限制、语言切换、日期格式化。不需要你自己拼 HTML 结构、绑定事件、处理边界逻辑,几十行甚至上百行的前端代码被压缩成一次方法调用。
这套组件库能做的事远不止日期控件。它覆盖了交互类组件(拖拽、缩放、排序、选择)、控件类组件(手风琴、页签、对话框、进度条、滑动条、自动补全、按钮、菜单、微调按钮)、以及一组视觉效果类组件(特效、切换效果、位置计算、CSS 工具类)。这等于给了你一把完整的瑞士军刀,而不是一颗单独的螺丝钉。
这篇文章不是来劝你新项目用 jQuery UI,而是帮你把它看透:它为什么能扛这么多年、核心组件怎么选型、真实项目里怎么落地不踩坑、线上遇到问题怎么排查。无论你是被分配去维护老系统,还是想快速搭一个内部工具页,这几十年的精华都还有用武之地。
写这篇文章的时候,我默认你已经知道“jQuery 是什么”以及“怎么在 HTML 里引用一个 JS 文件”。如果你完全没接触过,也没关系,后面每个概念我都会用最直白的方式解释一遍。
2. 整体架构拆解:先搞清楚它由什么组成
2.1 组件的三条分类线
jQuery UI 的组件不是一盘散沙,它们按功能边界分成了三类。理解这个分类,是你后续学会“按需加载”的基础。
第一类是交互类(Interactions)。这类组件解决的是“用户用鼠标或手指直接操作页面元素”的问题。典型代表是 Draggable(拖拽)、Droppable(放置)、Resizable(调整大小)、Selectable(框选)、Sortable(排序)。比如你做一个任务看板,希望卡片能从一个列表拖到另一个列表,Sortable 三行代码就能实现。
第二类是控件类(Widgets)。这类组件解决的是“页面需要一个完整功能控件”的问题。比如 Accordion(手风琴折叠面板)、Tabs(页签)、Dialog(模态对话框)、Progressbar(进度条)、Slider(滑动条)、Datepicker(日期选择器)、Autocomplete(自动补全下拉)、Menu(菜单)、Spinner(数字步进器)、Tooltip(提示气泡)。这些是平时用得最多的部分。
第三类是效果类(Effects)。包括特效切换(Add Class、Toggle Class)、动画缓动(Easing)、以及位置计算(Position)。这些工具单独拿出来看都不起眼,但在组合使用时能解决大量布局和动画问题。你写$( el ).position({ my: "center", at: "center", of: target }),一个弹框就能稳稳地居中在目标元素上,不用为父级position: relative这类破事操心(排查归排查,平时能省则省)。
2.2 版本脉络与文件结构
如果去官网下载 jQuery UI,会发现一个压缩包里有四个东西:jquery-ui.js(完整组件逻辑)、jquery-ui.css(全套样式)、结构化的 images 文件夹(图标和控件背景图)、以及jquery-ui.structure.css(纯结构样式,不含皮肤)。官方还提供了一个自定义下载页面,你勾选自己需要的模块,它会自动生成一份精简版的 JS 包。
这里有个非常关键的常识:jQuery UI 不是独立运行的,它依赖 jQuery 核心库。不同版本的 jQuery UI 对 jQuery 版本有最低要求。比如 jQuery UI 1.12.x 要求 jQuery 1.7 及以上,1.13.x 则可以配合 jQuery 3.x 稳定工作。以前经常有人拿着 jQuery 1.4 去跑 jQuery UI 1.11,结果各种方法找不到,然后在社区里发帖求助——这种问题几乎全是版本不匹配导致的。你在接手老项目时,第一件事不是读业务逻辑,而是先核对依赖版本。
我自己在维护一个 5 年前的运营后台时,就遇到过这种坑。系统里已经有一个老版本的 jQuery UI,后来为了一个图表插件引了一份新 jQuery,于是页面上出现了两个全局$,所有 UI 控件全部失效。排查了整整一个下午,最后在控制台里看到Uncaught TypeError: $(...).datepicker is not a function才反应过来。所以如果你要在现有页面里引 jQuery UI,开局第一件事就是确认:这个页面已经有几个 jQuery 了?
2.3 主题系统:为什么同一套组件在不同项目里颜值不同
jQuery UI 的样式设计非常有意思。它把组件的样式抽象成了“结构样式”和“主题样式”两层。结构样式负责尺寸、布局、位置,比如对话框的标题栏多高、关闭按钮放哪,这些和外观颜值无关。主题样式负责颜色、圆角、阴影、字体,好比给同一件衣服换不同的布料和颜色。
所以官方给你准备了十几个主题:基础款(Base)、平滑款(Smoothness)、乌黑款(UI Darkness)、烈日款(Hot Sneaks)等。你只需要换一个 CSS 文件,整个页面的控件颜值瞬间改变,业务逻辑代码一行动都不用动。这就是结构样式与主题样式分离的意义——前端设计里“换肤”这件事,在这套老框架里早就是原生能力了。
如果你有品牌定制需求,官方还提供了一个 ThemeRoller 可视化工具。你调整一下主色、辅色、圆角大小、字体,它能实时预览并生成一份定制版 CSS。我在多个对接甲方的项目里用过它,甲方说要“科技蓝”或者“商务灰”,我调完参数把生成文件丢进项目,前后不超过十分钟。这套玩具到现在依然能打,这也是它值得多聊一笔的原因。
3. 核心组件选型:哪个场景用哪个,别拿锤子去拧螺丝
3.1 常用组件速查表
jQuery UI 组件很多,但日常真实项目中大约只有六七个是高频的。我根据自己的经验整理了一张速查表,你在设计页面结构时可以拿着它对照:
| 业务诉求 | 推荐组件 | 典型用法 |
|---|---|---|
| 多个内容块折叠展开 | Accordion | $("#acc").accordion({ collapsible: true }) |
| 页签切换不同内容区域 | Tabs | $("#tabs").tabs({ active: 1 }) |
| 弹窗提示或表单填写 | Dialog | $("#dlg").dialog({ modal: true, buttons: [...] }) |
| 选择日期 | Datepicker | $("#dp").datepicker({ dateFormat: "yy-mm-dd" }) |
| 列表或卡片拖拽排序 | Sortable | $("#list").sortable({ axis: "y" }) |
| 输入关键词自动提示 | Autocomplete | $("#ac").autocomplete({ source: data }) |
| 进度或流程提示 | Progressbar | $("#pb").progressbar({ value: 60 }) |
这一张表看着简单,但每一条背后都有无数项目积累下来的约定俗成。比如 Datepicker 的dateFormat默认值是mm/dd/yy(美国写法),国内项目几乎都需要显式改成yy-mm-dd,不改的话后端起了一堆字符串日期的格式问题。这不是组件有 bug,而是“默认值是按美国习惯设计的”这个事实。
3.2 拖拽与排序:一个看板任务卡的小案例
聊完表格,我来拆一个实际场景:任务看板。假设你做一个轻量级项目管理页,有“待处理”“进行中”“已完成”三列,每列下面是一堆任务卡片。需要支持两个能力:第一,任务卡片在列内上下拖动调整优先级;第二,把卡片从一列拖到另一列改变状态。
传统写法你得监听mousedown、mousemove、mouseup,手动算坐标、判断拖到了哪个区域、再操作 DOM 节点移动。这里既麻烦,还容易在小屏幕设备上出怪问题。用 Sortable 怎么写?给你看核心代码:
$(function() { $(".task-column").sortable({ connectWith: ".task-column", placeholder: "task-placeholder", receive: function(event, ui) { var status = $(this).data("status"); var taskId = ui.item.data("task-id"); // 这里发起一个 AJAX 请求,通知后端更新任务状态 } }); });connectWith让三列之间互通拖拽,placeholder指定拖动时的占位样式,receive事件在卡片从一个列表放到另一个列表时触发。这套逻辑写完大约十来行,放在原生事件体系里,你可能要写一个专门的状态管理模块才能达到同等效果。
当然,高级定制必须写点额外代码。比如你希望卡片只能跨列拖、不能在同一列内重新排序,可以加sort: false限制同列排序。又比如你希望拖拽结束以后把新顺序一次性提交,可以在stop事件里遍历列表,把每个卡片的>$("#birthday").datepicker({ dateFormat: "yy-mm-dd", changeMonth: true, changeYear: true, yearRange: "1950:2025", monthNamesShort: ["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"], dayNamesMin: ["日","一","二","三","四","五","六"] });
这里有个很实用的隐藏技巧:yearRange可以动态拼接字符串。比如当前年份new Date().getFullYear()为 2025,你想让用户只能选未来三年内的日期,可以写成yearRange: "2025:2028"。这个值可以是常量,也可以是运行时算出来的变量,官方文档其实支持。
还有一个常见需求是限制日期范围。比如酒店预订页面,入住日期不能早于今天、离店日期不能早于入住日期。做法是监听日期选择事件,动态设置另一个输入框的minDate参数。代码不复杂,但对日期逻辑的梳理要清晰,否则会出现日期边界差一天的问题。
4. 实操案例:从零搭一个配置管理弹窗
4.1 目标拆解与 HTML 骨架
为了把前面讲的知识点合起来用,我们做一个具体项目:配置管理弹窗。这个弹窗用于一个后台系统的“用户偏好设置”,包含以下内容:顶部标题栏带关闭按钮;主体是一个页签结构,分为“基本设置”“通知偏好”“界面主题”三个栏;每个栏里放表单控件,包括输入框、单选按钮、复选按钮、日期选择器、滑动条;底部有“保存”和“取消”两个按钮。交互要求:模态弹窗打开时背景遮罩、按 ESC 可以关闭、点击遮罩不关闭(防止用户误触丢失已填内容)。
这个弹窗如果纯手写,你需要处理模态遮罩的层级问题、ESC 事件监听、页签切换时的面板显隐、日期控件的初始化时机、按钮事件绑定,零零散散不下几百行。用 jQuery UI 组件组合,工作量能压缩到一个令人舒适的范围。
先写 HTML 结构。注意 Dialog 的内容区里直接嵌套 Tabs,jQuery UI 对嵌套结构是支持良好的:
<div id="configDialog" title="用户偏好设置"> <div id="configTabs"> <ul> <li><a href="#tab-basic">基本设置</a></li> <li><a href="#tab-notify">通知偏好</a></li> <li><a href="#tab-theme">界面主题</a></li> </ul> <div id="tab-basic"> <p>用户名:<input type="text" id="nickname"></p> <p>生日:<input type="text" id="birthday"></p> </div> <div id="tab-notify"> <p><input type="checkbox" id="notify-email" checked> 邮件通知</p> <p><input type="checkbox" id="notify-sms"> 短信通知</p> </div> <div id="tab-theme"> <p>界面缩放:<input type="text" id="zoomSlider"></p> <p>字体偏好:<select id="fontChoice"> <option value="default">默认字体</option> <option value="serif">衬线字体</option> <option value="mono">等宽字体</option> </select></p> </div> </div> </div>4.2 初始化脚本与配置细节
接下来写初始化脚本。这里面有一个非常典型的注意事项:必须先初始化 Dialog,再初始化 Tabs,再初始化 Dialog 内部的其他控件。为什么?因为 Dialog 在初始化时会把内容区移入一个专用容器,如果你先初始化了 Tabs,Dialog 搬运 DOM 结构时可能会触发一些不可预期的排序问题。稳妥的顺序永远是从外到里。
$(function() { $("#configDialog").dialog({ modal: true, autoOpen: false, width: 560, buttons: [ { text: "保存", click: function() { saveConfig(); } }, { text: "取消", click: function() { $(this).dialog("close"); } } ] }); $("#configTabs").tabs(); $("#birthday").datepicker({ dateFormat: "yy-mm-dd", changeMonth: true, changeYear: true, yearRange: "1950:2025" }); $("#zoomSlider").slider({ min: 80, max: 140, value: 100, slide: function(event, ui) { // 实时预览缩放效果,ui.value 是当前滑动的值 $("#configDialog").css("font-size", ui.value + "%"); } }); $("#openConfigBtn").on("click", function() { $("#configDialog").dialog("open"); }); });这段代码里藏着两个值得细说的点。第一,buttons数组里的“保存”按钮点击后会调用saveConfig(),这个函数在真实项目里通常要做表单校验、收集数据、AJAX 提交。你在写的时候应该把收集逻辑独立成一个方法,不要把一堆赋值代码堆在 click 里。第二,slider的slide事件在鼠标拖动过程中会连续触发,用它做实时预览非常顺手。但你得注意this指向,在事件回调里this指的是滑动条容器,要拿弹窗用$("#configDialog")显式获取,否则容易踩到上下文指向错误。
4.3 按键绑定与遮罩行为定制
Dialog 组件默认支持 ESC 关闭,这是它内置的行为。如果你希望 ESC 不关闭(比如弹窗里有一个正在输入的多行文本,用户按 ESC 是想退出输入法或取消当前编辑),可以禁用这个行为。具体做法是做一个小的扩展配置,在打开弹窗时手动绑定一层键盘监听,并阻止冒泡。
$("#configDialog").on("dialogopen", function() { $(document).on("keydown.dialogEsc", function(e) { if (e.key === "Escape") { e.stopPropagation(); // 做你自己的最小化或提示操作 } }); }); $("#configDialog").on("dialogclose", function() { $(document).off("keydown.dialogEsc"); });这里用到了dialogopen和dialogclose事件。它们的好处是让你有能力在弹窗生命周期的不同节点做定制。比如每次打开时重置表单,每次关闭时清空临时状态,这些都是业务需求里的高频操作。我在上面代码里对keydown事件加了.dialogEsc命名空间,关闭时也要精确解绑,避免全局事件越积越多。这种命名空间式的事件管理思路在 jQuery 生态里是标准做法,放到今天用原生addEventListener也是一样的哲学。
点击遮罩不关闭,这个需求需要在 Dialog 初始化时配置。jQuery UI 的modal为 true 时会生成一个.ui-widget-overlay遮罩层。你要拦截对遮罩的点击,简单做法是在dialogopen后给遮罩层绑一个阻止默认行为的监听:
$("#configDialog").on("dialogopen", function() { $(".ui-widget-overlay").on("click", function(e) { e.preventDefault(); }); });注意,jQuery UI 的模态弹窗本身默认点击遮罩是不关闭的,所以如果你的需求只是“点击遮罩别关”,这个配置不是必需的,是组件默认行为。但如果你看到一些网上代码里监听click去做关闭,那是他们自己加的“点遮罩关闭”功能。搞清楚默认行为和二次定制的区别,能帮你少写很多无用代码。
5. 常见问题与排查技巧实录
5.1 现象一:控件方法报“is not a function”
这是新人入坑 jQuery UI 时最常遇到的错误。表现形式多种多样,比较典型的是控制台输出:
Uncaught TypeError: $(...).accordion is not a function排查思路按优先级从高到低:
第一,确认 jQuery UI 的源码文件有没有真的加载成功。打开Network面板,看jquery-ui.js的请求状态是不是 200,文件内容是不是完整的(如果不放心,直接在源码里搜accordion这个单词)。
第二,确认加载顺序。必须先加载 jQuery 核心,再加载 jQuery UI,顺序反了必报错。这个坑和“先有鸡还是先有蛋”无关,纯粹是依赖关系。
第三,确认是不是加载的是自定义精简版。官网自定义下载的包里只有你勾选过的组件,如果你勾选了Core但没勾选Accordion,页面自然就没有accordion方法。我之前帮别人排查过一个项目,对方信誓旦旦说下载了完整版,打开源码一看,文件大小只有 30KB,明显是自定义配置产物。
第四,确认页面里是不是有多个 jQuery 实例。前面提到过,这种情况在引入第三方插件时尤其常见。两个版本共存时,$很可能指向的是没有绑定 jQuery UI 插件的那一个。
5.2 现象二:Dialog 弹窗出现位置偏移
有时候对话框会在屏幕中间偏下、或者左边偏移很大。绝大多数情况是因为 CSS 里对body或容器元素设置了position: relative或transform。jQuery UI 的 Dialog 默认通过绝对定位来计算显示位置,如果它所在的父级元素position是relative,定位参照物就变了。
更麻烦的是transform属性。如果页面的某个容器设置了transform(比如做动画或缩放),它会创建一个新的 containing block,弹窗会在这个容器内部找定位基准。
解决思路是主动控制弹窗的 append 目标。Dialog 初始化时可以通过appendTo参数指定它挂载到哪个容器下。如果你希望对话框始终相对于body居中,可以设置:
$("#configDialog").dialog({ appendTo: "body", position: { my: "center", at: "center", of: window } });position参数其实是由 jQuery UI 的 Position 工具实现的,这也是我前面说 Position 单独拎出来也很有用的原因。你可以在任意元素上执行position()方法实现同样的定位逻辑,不必依赖 Dialog 组件本身的形态。
5.3 现象三:样式被全局 CSS 污染
jQuery UI 用了一堆通用类名,比如.ui-widget、.ui-state-default、.ui-corner-all。如果你的项目自己定义了同名的类名,两者就会打架。我看到过一个真实案例:项目公共样式表里写了一个.ui-widget { font-size: 12px; },结果整个后台所有弹窗、日期控件里的文字全变成了 12px,和设计稿差了十万八千里。
这种问题的排查思路很朴素:打开开发者工具,看目标元素的样式计算面板,找到哪些样式来自自己的 CSS,哪些来自 jQuery UI 的 CSS。然后把冲突项移到项目样式表的更低位覆盖,或者给弹窗根节点加一个自定义类名,用#configDialog .ui-widget这样的后代选择器去提高特异性。这个方法能解决九成样式冲突。
另一个技巧是不要随意修改 jQuery UI 的 css 文件本身。正确的定制方式是写一个自己的样式文件,在它后面加载,用更高的选择器优先级覆盖默认样式。这样下次升级 jQuery UI 版本时,你可以直接换文件,不用在一堆修改过的原生样式里翻找改了什么。
5.4 现象四:动态添加的元素没有响应交互
页面上通过 AJAX 加载了一段 HTML,里面包含了一个<select>或按钮,加载完成后你期待它已经是一个 Slider 或 Tooltip,但实际它就是个朴素的普通元素。
原因是:jQuery UI 的初始化方法是即时执行一次的,它不会自动监听未来新添加到 DOM 里的元素。你必须在动态元素插入完成后,再手动调用一次组件初始化方法。写起来大概是这样:
$.ajax({ url: "/api/config", success: function(html) { $("#settingsPanel").html(html); $("#settingsPanel .tooltipped").tooltip(); $("#settingsPanel .zoom").slider({ min: 50, max: 150, value: 100 }); } });这个“先插入 DOM,再初始化组件”的顺序在 jQuery UI 时代是铁律。很多人忘了第二步,弹窗打开了,里面的滑块却拉不动,就是这个原因。这个坑在做单页应用(SPA)时尤其常见——路由切换后视图变化了,新视图的组件没有经过初始化。维护老项目时,你要养成一个习惯:所有动态渲染的区域,渲染完成之后统一做一个初始化容器内组件的工作。我自己的方案是抽一个initWidgets(root)函数,内部遍历传入容器的所有匹配选择器的元素,统一调用初始化方法。这样插入新模块时,只要在success后调一句initWidgets($("#settingsPanel"))就够了。
6. 主题定制与性能优化:老项目里同样重要
6.1 用 ThemeRoller 定制企业品牌风格
如果你嫌默认主题不够好看,又不想写一大堆覆盖样式,官方提供的 ThemeRoller 值得认真研究。它是一个可视化工具,左侧调整颜色,右侧实时预览各组件效果。你调完以后直接把生成的压缩版 CSS 下载下来替换默认的jquery-ui.css即可。
这个工具的威力在于“一致性”:你调好主色、辅助色、文字色、边框色、圆角半径,它生成的所有组件样式都基于这套变量。日期选择器、按钮、对话框、进度条,颜色和谐统一,不需要你一个个手动写覆盖样式。我曾在一次活动中临时改了主题色,用 ThemeRoller 五分钟生成新 CSS 替换,整个后台页面的观感焕然一新,开发量几乎为零。
唯一的注意点:ThemeRoller 生成的 CSS 类名与默认版本完全一致,所以如果你之前写过很多基于默认类名的覆盖样式,替换后需要回归测试一遍,看看有没有原本被“覆盖镇压”的默认样式又冒出来。建议每次更换主题后,系统性点开每个组件的各个状态(正常、悬停、选中、禁用)检查一遍。
6.2 只加载你要用的模块
jQuery UI 全家桶的 JS 未压缩版大约 500KB,压缩版也有将近 250KB,这还不算 CSS 和图片资源。对于后台系统可能无所谓,但对于面向用户的页面,这个体积会拖慢首屏速度。
解决办法很直接:去官网的“Download Builder”页面,只勾选你实际用到的组件,生成一个精简版 JS。比如你只用了 Dialog 和 Datepicker,最终生成的 JS 可能只有 80KB,压缩后可能 40KB 出头,加载体验完全不在一个量级。
这里有一个隐藏细节:组件之间存在依赖关系。比如 Dialog 依赖 Draggable 和 Resizable(因为对话框默认支持拖动改变位置和尺寸),Tabs 不依赖其他组件,但如果你用了 Autocomplete,它内部依赖 Menu 和 Position。官网的 Download Builder 会自动帮你把依赖项一起勾选上,但你自己手工合并文件时就必须搞清楚这个依赖树,否则会出现组件方法缺失。我给一个很容易记的口诀:交互类组件是底层,控件类组件是上层,效果类组件是工具。勾选时先看自己的控件依赖哪些交互类,再把对应的交互类一并勾上。
6.3 图片合并与加载优化
jQuery UI 的主题样式里包含大量图标,用的是 CSS 雪碧图(一种把很多小图标拼在一张图上的技术)。默认情况下这已经算优化过了。真正拖慢加载的不是图片文件大小,而是不必要的文件请求层级。
老浏览器时代,CSS 里每引用一张背景图都会发一个 HTTP 请求。jQuery UI 1.10 之后已经用 CSS 雪碧图优化过一轮,但你如果在原版 CSS 上做了大量自定义图片替代,就要自己检查最终生成了几张图标图。如果你看到请求列表里有十几张小图,说明某些组件引用了不同版本的图标图片,这种时候可以把它们手动合并成一张雪碧图,再用background-position定位。
不过说老实话,对大多数内部系统来说,这种优化属于“可以做但不必须”。我见过很多团队对后台页面也做极限性能优化,结果花费的时间和收益完全不成比例。我的建议是:jQuery UI 相关文件如果能压缩加载,就开启合并压缩;如果本来就是非首屏弹窗里才用到的组件,完全可以延迟加载,不需要一开始就把整包拖下来。等用户点击“打开弹窗”按钮时再动态加载 JS,配合 localStorage 缓存,体验会好很多。
7. 个人经验里的三个“反直觉”建议
聊到最后,分享三个我在这套组件上摸爬滚打总结出的建议,希望能帮你在真实项目里少走弯路。
第一个建议是:不要因为 jQuery UI “老”就避而不谈,也不要因为“简单”就轻视配置细节。老项目里它的存在是合理的历史选择,新项目里如果你要快速做一个不需要复杂数据绑定的后台界面,拿它来搭原型效率依然很高。我用它搭内部数据管理页面,从零到能用的时间往往比现代框架还要短,尤其是团队里没人熟悉现代框架的前提下。
第二个建议是:学会读 jQuery UI 的源码和事件机制。jQuery UI 的组件几乎都遵循同一个模式:_create方法负责初始化结构,_init负责每次调用时重置状态,_destroy负责销毁清理。理解这个模式后,你自己写自定义扩展组件时就有了骨架,不愁无处下手。我早期写过一个基于 jQuery UI 风格的自定义组件,就是模仿了它的模式,后来维护起来特别省心。
第三个建议是:找一个能长期跟踪稳定主题的人维护样式体系。jQuery UI 的组件样式零散,如果人人改一点,最后会变成一团乱麻。我见过最乱的一个项目,一个对话框的颜色在三个 CSS 文件里被定义了五次,每次升级都有人叫苦。如果你有权限,最好把这些样式收敛到一个文件里,并且标注清楚哪些是官方覆盖、哪些是业务定制。这个动作带来的长期好处,比任何一次技术升级都值钱。
关于 jQuery UI,能聊的其实还很多:Autocomplete 的远程数据源、Accordion 的排序联动、Tooltip 的定位逻辑、Menu 的键盘导航。但万事开头难,先把前面这些组件选型、初始化顺序、主题定制、问题排查这条主线吃透,你已经有能力去应付绝大多数真实开发场景。剩下那些边角料功能,等你真正遇到了,翻翻文档,结合这篇文章里的思路,自然能迎刃而解。