jQuery 这个库用了十几年,很多人对它的认知就是“一个 jquery.min.js 引进去就完事”。直到某天做安全审查,发现老项目还在跑 jQuery 1.x,去官网下载页一看,文件五花八门,又开始研究漏洞修复记录和各个文件到底有什么用。今天这篇就把我在升级过程中的整理完整写出来,既有文件功能对照,也有漏洞修复思路和落地步骤,给同样被困在老项目里的同学做个参考。
1. 先把 jQuery 包里的这些文件认全
很多人的项目里只有一个 jquery.min.js,但对 jQuery 的发行包结构并不熟悉。尤其到了 3.x,官方直接给了好几种文件,名字就差一两个词,功能却差不少。用错文件轻则功能缺失,重则上线后白屏,所以第一步就是把dist目录里这些文件搞清楚。
1.1 一个发行包里到底躺着哪些文件
以 jQuery 3.7.x 为例,你从 npm 安装或者官网下载后,核心文件一般都在dist/目录下:
| 文件名 | 压缩过吗 | 包含模块 | 适合场景 |
|---|---|---|---|
| jquery.js | 否 | 完整版 | 本地开发调试,报错信息可读,能直接打断点 |
| jquery.min.js | 是 | 完整版 | 生产环境常规选型,体积小,加载快 |
| jquery.slim.js | 否 | 删减版 | 只用选择器和事件处理的调试场景 |
| jquery.slim.min.js | 是 | 删减版 | 生产环境极限压缩,体积最小,但功能少 |
| jquery.min.map | 否 | 源码映射 | 浏览器 DevTools 调试压缩代码时使用 |
为什么会有压缩版和未压缩版?因为生产环境在意传输体积和解析速度。压缩的不是简单去掉空格,它还会把变量名换成短名称、删除注释、合并表达式,最终代码几乎不可读。jquery.min.js的大小通常在 90KB 左右,未压缩版jquery.js则在 270KB 上下,差距很明显。
但压缩带来的副作用就是报错信息难懂。比如线上代码报了个n.fn is not a function,你看半天也看不出是哪一段逻辑。这时候.map文件就有用了,也就是 source map 源码映射。浏览器在加载压缩脚本时,如果压缩文件末尾有//# sourceMappingURL=jquery.min.map,DevTools 就会自动把压缩代码中的行号和变量位置映射到原始源码上,方便你定位问题。
这里要提醒一句,source map 在生产环境属于敏感文件。它会把完整的源码结构暴露出来,如果项目是挂在公网上的,我一般不建议默认把.map文件一起部署。真正需要排查线上压缩代码问题时,临时把 map 放到同目录,定位完再移除,是更稳妥的做法。
1.2 完整版和 slim 版不是“多几个方法”的区别
slim 版本是 jQuery 3.x 开始引入的概念,它做的事情不是简单去掉某个 API,而是直接拿掉了两个大模块:Ajax和Effects。
拿掉 Ajax,意味着$.ajax、$.get、$.post、load()这一类方法全部不存在了。拿掉 Effects,意味着animate()、fadeIn()、fadeOut()、slideUp()、slideDown()这些动画方法也都没了。如果你在引了 slim 的页面里调用$.ajax(),控制台会直接报$.ajax is not a function,动画相关方法也同理。
那 slim 存在的意义是什么?如果你的项目只用了 jQuery 的选择器、事件绑定、DOM 操作这些核心能力,根本没有 Ajax 和动画需求,引 slim 可以省掉一部分体积。它更适合那种“杀鸡用牛刀但牛刀又太大”的场景。
判断项目能不能用 slim,我的习惯是把代码里所有$.ajax、$.get、$.post、animate、fade、slide全局搜索一遍,一个都搜不到,才敢换。只要有一个地方用到了,老老实实上完整版,别为了省几十 KB 给自己埋雷。另外,在 webpack/Vite 这类构建工具项目里,import $ from 'jquery'默认导入的是完整版,一定要用 slim 得写成import $ from 'jquery/dist/jquery.slim.min.js',不写清楚构建工具不会自动帮你切。
1.3 jquery-migrate.js 也是包里的常客,但它不是 jQuery 本体
很多人会把 jquery-migrate.js 当成 jQuery 的一部分,其实它是一个独立的辅助插件。作用一句话就能说清楚:当你的项目从旧版本 jQuery 升级到新版本时,旧代码里那些已经删除或废弃的 API 不会直接让整个程序崩掉,migrate 会在控制台打印警告,告诉你哪些地方用了不兼容的写法。
举例来说,项目里如果还写着$.parseJSON、.bind()、.size(),新版本 jQuery 里这些 API 要么没了,要么是废弃状态。migrate 会把它们找出来,输出类似 “JQMIGRATE: $.parseJSON is deprecated” 的提示,并尽量用兼容逻辑把功能顶上。
migrate 是过渡期工具,不建议长期放在线上。它本身会带来额外的修正逻辑和性能损耗,正确用法是:升级期引入,跑一遍测试,按警告改完代码后,再从项目里移除。这一点后面实战部分再细讲。
2. 漏洞修复记录:3.5.0 是绕不开的分水岭
jQuery 的漏洞历史里有一组编号经常被安全扫描工具拎出来说:CVE-2020-11022 和 CVE-2020-11023。这两个漏洞的修复版本都是 jQuery 3.5.0,也就是说,凡是低于 3.5.0 的版本,都存在被利用的风险。很多老项目长期停留在 1.x、2.x、3.4.x,安全审查一查就是一个高危,原因就在这里。
2.1 这两个漏洞到底是怎么回事
要理解这个漏洞,先要知道 jQuery 的一个使用习惯:$(htmlString)可以直接把一个 HTML 字符串变成 DOM 元素。比如$("<div class='box'>hello</div>"),jQuery 会识别出这是一个 HTML 片段而不是选择器,然后把它插入页面。
问题出在 jQuery 内部对 HTML 字符串的预处理。老版本里有一个函数叫htmlPrefilter,它的职责之一是先用正则对传入的 HTML 做一次修正,把一些畸形标签或历史遗留写法处理成标准形态。这套逻辑设计之初是为了兼容老浏览器的,但是正则理解不了真正的 HTML 语法,它只能按模式匹配。
攻击者要做的事情,就是构造一个能让这个正则预处理“判断失误”的字符串。一旦字符串没被正则拦截住,它就会被原样交给浏览器的innerHTML解析。浏览器解析 HTML 的规则和 jQuery 的预期并不完全一致,于是某些包含在标签属性里或者藏在特殊标签结构中的脚本逻辑就被执行了。这是典型的 XSS 跨站脚本攻击。
用大白话说,jQuery 老版本想当安检员,但它只凭一张标签贴纸判断来者身份,攻击者换了个包装就混过去了。整个过程不需要用户交互,也不需要登录态,只要页面上有接口数据或 URL 参数被拼进 jQuery 的 HTML 处理链里,就可能中招。
2.2 影响范围和触发条件
CVE-2020-11022 影响的是 jQuery 3.5.0 之前的所有版本,CVE-2020-11023 影响的是 3.0.0 到 3.4.1 的版本,但无论如何,结论都一样:低于 3.5.0 的 jQuery 在安全层面是不合格的。
不过也别被“高危漏洞”四个字吓到。漏洞触发是有前提条件的:必须有不受信任的 HTML 内容传入 jQuery 的$()、.html()、.append()、.prepend()这类接口。如果你的项目所有 HTML 都是后端模板里的静态字符串,没有任何用户输入或第三方数据参与拼接,那实际风险很低。但问题在于,大多数老项目都存在评论渲染、URL 参数回显、接口返回富文本这类场景,你很难保证每一处都做了严格过滤。
这也是安全修复中最尴尬的地方:漏洞真实存在,触发路径不一定能靠肉眼找全。所以不管当前有没有被实际利用,版本升级是唯一稳妥的出路。
2.3 jQuery 3.5.0 到底改了什么
jQuery 3.5.0 的修复并不是简单给过滤正则加几个字符,而是调整了整个处理链路的重心。
新版本里,htmlPrefilter不再负责那种“修改 HTML 字符串形态”的工作,它在处理 HTML 片段时会更依赖浏览器原生的解析能力,比如使用<template>元素这类标准机制去解析和承载 HTML。浏览器原生解析器是按照 HTML 规范逐字符处理的,不会再被正则那种“看起来像不像标签”的判断方式绕过去。
换句话说,新版把安全边界交还给了浏览器和开发者。jQuery 自己不再尝试替你做“聪明的清洗”,因为从设计上就不该由 jQuery 来判断一段 HTML 是否安全。这带来的一个明显变化是:升级后如果有些老页面依赖 jQuery 当年那种“自动修正畸形 HTML”的行为,展示效果可能会出现细小的差异,后面回归测试时要注意。
顺带一提,3.5.0 之后,jQuery 的版本演进并没有停,3.6.x、3.7.x 一直在发布。对我来说,3.5.0 只是一个必须跨过的最低门槛,实际项目里我会直接升到 3.7.x 的最新稳定版,避免在修复版上又堆积了半年的旧债。
3. 实战:给老项目做一次安全升级
升级 jQuery 说难不难,说简单也不简单。文件一换容易,真正的坑在兼容性和业务回归上。我习惯把整个过程拆成“体检—换包—迁移—回归”四步走。
3.1 升级前先搞清楚现状
动手换文件之前,先回答几个问题:
- 当前项目用的 jQuery 版本是多少?
- 有多少第三方插件依赖 jQuery?它们对版本有没有硬性要求?
- 代码里有没有把用户输入、接口数据直接拼进 HTML 字符串?
查版本最简单的方式是打开浏览器控制台,执行jQuery.fn.jquery,会直接输出当前版本号。如果是全局引用的老项目,也可以在页面源码里搜jquery找到 script 标签。
紧接着要清点第三方插件。这一步经常被忽略,却是翻车重灾区。很多老插件停留在 jQuery 1.x/2.x 时代,直接配上 jQuery 3.7.x 可能根本跑不起来,比如某些旧的轮播图、表单验证、弹窗组件。我的做法是:把所有 script 引用列个清单,逐个确认插件的兼容版本。如果一个插件已经停止维护且明确不兼容新 jQuery,就得提前想好替代方案,要么找功能等价的现代库,要么自己用原生代码重写那一小块功能。
最后,把代码里所有可能接收外部数据的 HTML 操作点列出来,重点搜.html()、.append()、.prepend()、.after()、.before()、$(htmlString)这几种调用。不用急着改,升级前先知道风险面在哪里。
3.2 不同部署方式的具体升级操作
老项目最常见的两种部署方式:一种是纯静态页面,script 直接引文件;另一种是用了构建工具打包。
纯静态页面的流程最直接:
- 下载对应版本的
jquery.min.js,我一般从 npm 包或官方渠道拿文件,确保文件没有被篡改。 - 把原来的
<script src="jquery-1.x.min.js"></script>替换成新版本。 - 页面里所有依赖 jQuery 的插件 script 引用顺序不要动,必须保证 jQuery 在插件之前加载。
- 在测试环境全量回归一遍,重点看控制台有没有报错。
如果是构建工具项目,操作就变成 npm 层面的升级:
npm install jquery@3.7.1 --save在模块代码里,保持原来的import $ from 'jquery'即可,重新构建后自然用的是新版本。这里有个细节留意一下:如果项目通过 ProvidePlugin 之类的插件把$自动注入所有模块,升级后要重新构建并检查有没有缓存问题。
另外,使用 CDN 加载 jQuery 的项目,建议给 script 加上 SRI(子资源完整性)校验。SRI 的作用是确保浏览器加载的脚本内容和预期一致,即使 CDN 被篡改,浏览器也会拒绝执行。生成校验值的方式是用 openssl 对文件做 SHA-384 哈希,然后转成 base64:
openssl dgst -sha384 -binary jquery.min.js | openssl base64 -A把输出填到 script 标签的integrity属性里,再加上crossorigin="anonymous"。如果升级后打开页面发现报错、jQuery 没有定义,先检查 integrity 是否匹配,这是最常见的问题之一。
3.3 用 jQuery Migrate 处理不兼容代码
换上新版本后,业务代码不会立刻全崩,但会出现各种怪异现象。原因是 3.x 版本删除了一部分 API,同时对另一些 API 做了行为调整。这时候就轮到 jquery-migrate 上场了。
操作顺序是这样:
- 在页面里引入 migrate 插件,版本要和 jQuery 主版本匹配。比如升到 3.7.x,就引入对应 3.x 的 migrate。
- 打开控制台,跑一遍项目的核心流程。
- 控制台会输出所有 “JQMIGRATE” 开头的警告,逐条定位到业务代码并修改。
- 确认所有警告清零后,移除 migrate,再跑一遍回归。
实际使用中,最高频的兼容问题集中在这几个 API 上:
| 旧写法 | 新推荐写法 | 说明 |
|---|---|---|
.bind(event, fn) | .on(event, fn) | bind/off 在 3.0 后被移除 |
.unbind(event) | .off(event) | 同上 |
.delegate() | .on() | 事件委托统一用 on |
.size() | .length | size 方法早就被移除了 |
.andSelf() | .addBack() | 方法改名 |
$.parseJSON(text) | JSON.parse(text) | 直接调用原生 JSON |
$.isArray(arr) | Array.isArray(arr) | 原生方法 |
$.trim(str) | String.prototype.trim | 建议直接字符串原生处理 |
migrate 警告是升级时最容易忽视的资源,它比任何文档都准确,直接告诉你哪一行代码出了问题。但要注意,migrate 的兼容逻辑是尽力而为,不可能 100% 还原旧行为。真正稳妥的做法是把警告当作必改项,而不是当作可以忽略的提示。
3.4 回归测试要覆盖哪些点
说到回归测试,很多人以为页面能打开、控制台没报错就够了,其实还会漏掉很多细节。根据我实际踩过的坑,至少要覆盖:
- 所有动态绑定的事件是否正常触发,特别是用
.on()绑定到未来元素的委托事件。 - 页面里所有 Ajax 请求是否正常发出和拿到返回值,如果项目里同时引了 slim 版本,这一步会直接暴露问题。
- 所有动画效果是否正常执行,
fadeIn、slideUp这一类方法有没有失效。 - 使用
$(document).ready()的页面是否在 DOM 加载后正确执行,jQuery 3.x 对 ready 回调的异步行为有调整,个别场景下执行顺序和旧版不一样。 - 用到了
:hidden、:visible、:animated这类伪类选择器的判断结果是否和之前一致。
回归测试最好找一份业务核心流程清单,从页面初始化到用户操作都走一遍。我的经验是:升级 jQuery 这种基础库,宁可多花一个下午做全量回归,也不要觉着“就是换个文件而已”。
4. 修复之外:日常怎么跟 XSS 和平相处
版本升级能堵住 jQuery 自身的漏洞,但解决不了所有 XSS 问题。说白了,jQuery 3.5.0 修的是“jQuery 被绕过的漏洞”,不等于代码里可以肆无忌惮地往里插 HTML。安全习惯如果不变,升级只是暂时安心。
4.1 插入内容时要严格区分 html() 和 text()
我见过非常多的 XSS 问题,根源不是用了老版本的库,而是分不清html()和text()的语义。
$(el).html(content)会把 content 当作 HTML 解析,标签会被浏览器真正解释执行。$(el).text(content)会把 content 当作文本节点处理,哪怕里面写了<script>也只是显示成字符,不会执行。
所以有一个非常朴素但是有效的规则:凡是来自用户输入、URL 参数、接口返回、第三方平台的内容,默认一律用text()插入。只有业务明确要求展示富文本时,才考虑走 HTML 渲染通道。
如果有内部模板需要把变量插到 HTML 字符串里,也要先做转义。最常见的转义函数长这样:
function escapeHtml(str) { return String(str) .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }把变量交给escapeHtml()之后再拼 HTML 字符串,能挡掉大部分通过<script>、事件属性、伪协议注入的攻击。这函数很简单,但真到了业务代码里,很多人嫌麻烦就直接拼了,等出了事又怪框架。
4.2 富文本内容必须走白名单过滤
有些业务确实需要用户输入富文本,比如发帖、评论带格式、后台编辑公告。这种情况千万不要直接$(el).html(userRichText),正确的姿势是先让富文本经过一个白名单清洗库处理。业内比较成熟的做法是使用 DOMPurify 这类工具,它会把 HTML 里允许的标签和属性过滤一遍,把onclick、onerror、javascript:这类危险内容直接剥掉。
白名单过滤的另一个好处是,它对新旧版本的 jQuery 都有效。就算哪天某个版本的 jQuery 又曝出 HTML 处理漏洞,只要入口处的数据已经被清洗过,攻击链就断在前面了。把安全边界前置,永远好过在库内部跟漏洞赛跑。
4.3 依赖安全要纳入例行检查
客户端库的安全问题有一个特点:代码不跑起来,你根本感知不到它存在漏洞。老项目最怕的就是那种“上线后没人动,一检查全高危”的状态。所以依赖安全检查要变成例行动作,而不是安全审查来了才突击。
npm 项目可以直接跑npm audit,它会根据已安装包的版本和公开漏洞库做比对,列出哪些依赖存在漏洞、影响程度如何、建议升级到什么版本。如果项目里还有一堆没纳入依赖管理的静态文件,那就得手动维护一份“前端资源清单”,定期过一遍每个库的最新版本和安全公告。
我个人的习惯是给老项目建立版本升级节奏,不用追求每次都升到最新,但至少要保证所有线上依赖不在已知高危漏洞列表里。像 jQuery 这种底层库,升一次级影响面大,所以更要趁早做,别拖到被扫描工具逼着改的时候才动手。
5. 常见问题与排查实录
升级过程中,有几类问题几乎每个项目都会遇到。整理成速查表,可以直接照着排查。
| 问题现象 | 常见原因 | 排查与解决办法 |
|---|---|---|
$.ajax is not a function | 引用了 slim 版本,Ajax 模块被裁掉 | 换成完整版jquery.min.js,或确认项目不用 Ajax 再用 slim |
fadeIn is not a function/ 动画全部失效 | slim 版本没有 Effects 模块 | 换成完整版,检查所有动画调用 |
页面打开提示jQuery is not defined | script 加载顺序颠倒、SRI 校验失败、CDN 文件被拦截 | 确认 jQuery 先于插件加载;检查 integrity 值与文件是否匹配;本地部署一份备用 |
| 控制台出现大量 JQMIGRATE 警告 | 业务代码还在用旧版 API | 借助 migrate 插件逐条定位并改写,改完移除 migrate |
| 压缩版报错后看不到具体业务位置 | 生产环境使用 min 文件,变量名被压缩 | 本地开发用未压缩版;线上排查时临时引入 map 文件,定位后移除 |
| 升级后所有事件都绑不上 | 代码里混用了.bind()、.delegate()等旧 API | 全局搜索.bind(、.delegate(、.unbind(,统一改成.on()/.off() |
| 动态添加的元素点击无效 | 直接绑定的旧逻辑遗漏了委托事件 | 检查是否用了.on('click', selector, fn)做事件委托,动态元素必须靠委托 |
这里重点说下 “jQuery is not defined” 这个现象,它看着像是库没引进来,实际原因可能很隐蔽。除了 script 顺序和 SRI 校验失败外,还有一个高频场景:页面里同时引入了多个版本的 jQuery,后加载的版本把全局$覆盖了,而老插件保存的还是旧版本引用。这种情况下控制台报错的位置常常千奇百怪,排查时要先确认全局只有一个 jQuery 实例。
另外一个坑是升级后事件出现“执行两次”。这种情况通常不是因为 jQuery 本身出了问题,而是代码里同时用了$(document).ready(fn)和DOMContentLoaded监听,或者某段初始化逻辑在页面里被加载了不止一次。升级前这些重复调用可能被旧版本的执行顺序掩盖了,升级后异步行为变化让问题暴露出来。排查时直接在控制台给事件绑定打 log,看看触发了几次,往往能快速定位到是哪个文件重复初始化。
还有一个很容易被忽略的点,就是升级后个别选择器结果变了。jQuery 3.x 对部分伪类选择器和属性选择器的处理方式更贴近浏览器原生标准,旧版本里一些“错误但能用”的写法结果会和以前不一样。比如$("div:first")这类写法,在 3.x 里性能和结果都有细微调整。遇到选择器结果异常时,先别怀疑是新版 bug,大概率是旧代码依赖了一些已经被修正的怪癖行为。
我个人操作这类升级时,有一个固定的习惯:先把文件版本换掉,让业务方在测试环境跑一遍,看能不能正常运转;再挂上 migrate 插件,花一个完整迭代把所有弃用 API 清干净;最后移除 migrate,再做一轮全量回归。这样能把“升级 jQuery”这件事拆成两个独立的风险点,出问题的时候好定位。另外,在安全这件事上,永远不要指望任何库帮你兜底,入库的内容先做转义和过滤,比事后打补丁管用得多。