资讯详情

原生JavaScript替代jQuery:前端自动化测试的极简实践

📅 2026/10/18 5:08:44 | 华诺云谱 👁 阅读
原生JavaScript替代jQuery:前端自动化测试的极简实践
前段时间我在维护一个老后台项目的时候实在被折腾得不行。那个系统从八年前就开始用前端清一色jQuery写功能倒是顺手可一跑到自动化测试就头疼脚本要先等jQuery从CDN加载完才能开始操作本地网络一抖动用例就红一片跟无头浏览器集成的时候还得本地再装一份jQuery测试包里凭空多出几十KB。其实细想一下自动化测试要干的事无非是选中元素、模拟点击、填表单、等异步结果、做断言这一套用原生JavaScript完全能干而且干得更干净。所以今天这篇我就把如何用原生JavaScript替代jQuery来做前端自动化测试这件事从头到尾拆给你看。这篇文章适合几类人正在维护老项目、想把测试脚本里jQuery依赖逐步清掉的前端工程师在面试中经常被问“jQuery到底还该不该用”想找一份透彻答案的同学还有那些写测试脚本上瘾、希望少依赖一个库是一库的自动化测试爱好者。看完之后你能得到一份jQuery核心API转原生API的对照表一整套不依赖任何框架的极简测试辅助代码还有几个我实测踩过、几乎每个新手都会掉进去的坑。1. 项目概述为什么用原生JavaScript替代jQuery做自动化测试1.1 需求从哪里来降本、提速、摆脱依赖先把话说清楚我并不是鼓励大家在所有项目里立刻把jQuery扔了。在真实业务开发里jQuery的生态和习惯性代码确实还有存在价值。但“自动化测试”这个场景非常特殊它对稳定性和依赖管理的敏感度远高于普通页面开发。自动化测试脚本本质上是一段“闯进页面里搞破坏”的代码。如果这个代码还依赖jQuery就出现了一个连锁问题测试环境里必须先有jQuery。要么从CDN加载要么本地静态文件引用要么通过包管理器塞进测试框架。无论哪条路都是在给测试链路增加一个可失败的中间环节。我碰到过一种情况某个老系统为了兼容IE8当时还叫这个测试脚本里一直带着jQuery 1.x后来测试节点升级成某个现代无头浏览器老版本jQuery在初始化时直接报错整条CI挂了半天最后排查才发现不是业务代码的问题是测试依赖库自身的兼容性。就算没有这么极端jQuery在自动化测试里也有几个天然短板加载时机不好控制尤其页面本身还在用异步渲染的时候测试代码和业务代码都在等同一个库很容易出现竞态包体积大放到测试容器里要多下载多解析链式API隐藏了DOM节点本身有时候想打印日志还得先用.get(0)把原生节点挖出来非常绕。原生JavaScript就不一样。querySelector、addEventListener、fetch这些API直接长在浏览器或者Node环境里测试脚本启动就是满状态的没有加载顺序问题没有额外体积也没有版本冲突。我想这就是“用原生替代”这件事最核心的需求来源不是讨厌jQuery而是让测试链路的依赖最小化、可控化。1.2 原生替代的整体思路先把jQuery“翻译”成原生API替代的过程我建议不是直接拿起业务代码就开始改而是先在空白处做一张“翻译表”。把之前jQuery的日常写法对应到原生API写法这样替换的时候才有章法也不容易漏。我用一个实际项目里的对照经验整理了一份高频映射表先放出来给你参考应用场景jQuery写法原生JavaScript写法根据CSS选择器选首个元素$(.foo)document.querySelector(.foo)根据CSS选择器选所有元素$(.foo)返回集合document.querySelectorAll(.foo)返回NodeList遍历集合$.each(arr, fn)arr.forEach(fn)或Array.from(nodes).forEach(fn)绑定事件$el.on(click, fn)el.addEventListener(click, fn)主动触发事件$el.trigger(click)el.dispatchEvent(new MouseEvent(click, {bubbles: true}))获取或设置文本$el.text()/$el.text(str)el.textContent获取或设置HTML$el.html()/$el.html(str)el.innerHTML获取或设置输入框值$el.val()/$el.val(str)el.value添加/删除类名$el.addClass(a)/$el.removeClass(a)el.classList.add(a)/el.classList.remove(a)切换类名$el.toggleClass(a)el.classList.toggle(a)设置属性$el.attr(disabled, true)el.setAttribute(disabled, )或el.disabled true读取属性$el.attr(href)el.getAttribute(href)隐藏元素$el.hide()el.style.display none显示元素$el.show()el.style.display 或el.style.removeProperty(display)AJAX请求$.ajax({url, method, data})fetch(url, {method, body})返回Promise等待DOM就绪$(function(){})DOMContentLoaded事件或直接把脚本放到body末尾做完这张表你会立刻发现jQuery在常规项目里真正被高频使用的就是这些能力。原生API覆盖度已经很高个别边角功能比如动画队列、事件命名空间自动化测试里很少用到真遇到了用一小段工具函数几十行就能解决完全没必要为了这些边角拖进来一个整个库。核心的思维转变是一个“对象观”的转变jQuery拿到的是包装过的jQuery对象方法可以链式调用原生拿到的是DOM节点、NodeList或Promise操作更靠近浏览器底层。你只要适应“先取到节点再操作节点”这个流程剩下的都是水到渠成。2. 核心细节解析原生API这样用才能在自动化测试中站稳脚跟2.1 选择器与DOM遍历的替代方案做自动化测试第一步几乎永远是“找到那个元素”。原生浏览器提供的querySelector和querySelectorAll就是我们最大的底气。document.querySelectorAll(.item)返回的是NodeList它长得像数组但不是数组这就有个容易翻车的小细节。比如你写document.querySelectorAll(.item).map(...)在旧一点的浏览器里直接报错“map is not a function”。解决方式我见过三种Array.prototype.slice.call(nodes)、Array.from(nodes)、还有一种更现代的写法[...nodes]。我实测下来最推荐Array.from意图清晰可读性也最好。拿到集合之后遍历就自由了。你想用forEach、filter、reduce都可以。比如我要统计页面上所有错误提示里包含“必填”两个字的条数就能写const errorItems Array.from(document.querySelectorAll(.error-message)); const requiredErrors errorItems.filter(el el.textContent.includes(必填)); console.log(必填错误数量, requiredErrors.length);这段代码放在自动化测试里就是一条很清晰的断言依据。你不需要再用$(.error-message).each(...)然后费劲把jQuery对象转成数组。记得一点querySelector(xxx)只返回第一个匹配元素如果页面里一个都找不到返回null。所以如果后续要调用.click()之类的方法一定先做空值判断不然TypeError会让整个测试脚本中断。这比jQuery返回空集合还要温柔一点jQuery至少还能让你继续链式调用原生API可不行。DOM遍历方面jQuery的parent()、children()、closest()我平时也用得不少。原生对应是parentNode、children、parentElement.closest()。其中closest非常实用比如我从一个被点击的按钮往上找最近的表单容器用button.closest(form)一行就搞定。2.2 事件绑定与模拟用户操作的替代方案自动化测试的重头戏是模拟用户操作。点击、输入、提交、滚动每一样都要靠事件体系来完成。先说绑定addEventListener就是on()的平替基本没有学习成本。要注意的是jQuery还支持事件命名空间比如on(click.myNamespace, fn)原生没有这个东西。但测试代码通常不需要这种精细化控制你只需要记住用removeEventListener清理监听器就行如果之前监听器存在命名空间包裹移除时反而容易踩坑。真正容易踩、也最有意思的是“主动触发事件”。jQuery的.trigger(click)会构造事件并完整走一遍冒泡流程监听器能接到。原生写法则需要你自己一步步来// 模拟点击一个按钮 const btn document.querySelector(#submitBtn); btn.dispatchEvent(new MouseEvent(click, { bubbles: true, cancelable: true })); // 模拟输入框输入这里建议触发InputEvent而不是直接赋值后dispatchEvent const input document.querySelector(#username); input.value testUser; input.dispatchEvent(new Event(input, { bubbles: true }));这里有个特别关键的细节bubbles一定要设为true。如果不设置默认是false事件只在目标元素自身触发不会冒泡到父级。很多页面的委托监听器是挂在document或某个容器上的比如document.addEventListener(click, handler)你模拟的点击事件如果冒泡设置丢了监听器一辈子收不到然后整个测试用例莫名其妙失败排查半天都不明白为什么。另外输入框的模拟不要只设置value就完事。我见过很多自动化测试新手直接写input.value abc然后断言却发现页面没有反应。原因很可能是框架或者业务代码监听了input事件你光改值不触发事件等于“数据变了但没人收到通知”。所以推荐的做法是先设置value再派发一个new Event(input, {bubbles: true})。如果需要监听键盘操作比如按回车触发搜索则用new KeyboardEvent(keydown, {key: Enter, bubbles: true})。2.3 获取修改内容与属性的替代方案内容读取与修改这块有一个词特别提一下textContent和innerHTML的区别。jQuery的$el.text()会读取纯文本内容原生对应el.textContent它不会解析HTML标签安全。而innerHTML则是带标签的完整字符串跟jQuery的html()对应。断言的时候建议优先用textContent避免被页面中隐藏的标签结构干扰也更安全。类名操作在测试中也高频出现。比如我要验证一个按钮在点击后进入了“加载中”状态自然会去检查它有没有is-loading这个类const submitBtn document.querySelector(#submitBtn); submitBtn.dispatchEvent(new MouseEvent(click, { bubbles: true })); // 断言按钮是否进入加载状态 console.assert(submitBtn.classList.contains(is-loading), 按钮应有is-loading类);classList还支持add、remove、toggle、contains原生接口设计得像个小工具箱比jQuery拼字符串的方式现代很多。属性操作上getAttribute/setAttribute跟jQuery的attr基本一致。有一点要注意对于表单元素推荐直接使用DOM属性而不是getAttribute。比如要读输入框的值写el.value而不是el.getAttribute(value)——后者通常拿不到用户输入后的最新值这在自动化测试里是一个非常经典的低级错误。还有一个小点是dataset。如果页面标签上有>const response await fetch(/api/check-version); const data await response.json(); console.assert(data.status ok, 接口状态应为ok);如果要在测试环境里模拟接口一个常见的做法是拦截全局window.fetch。这个思路跟以前拦截$.ajax套路一样但实现更清爽。我给你一个最简的mock写法async function mockFetch(mockMap) { window.fetch async (url, options) { if (mockMap[url]) { return { ok: true, status: 200, json: async () mockMap[url], text: async () JSON.stringify(mockMap[url]) }; } throw new Error(未匹配的mock地址: ${url}); }; }我还见过有人在测试里需要同时兼容XHR和fetch两种情况。在原生体系下拦截fetch的代码比较简单拦截XHR则要重写XMLHttpRequest稍微麻烦一点。但如果你要测试的是一个老项目里面还在大量使用jQuery的$.ajax本质上是XHR那么拦截XMLHttpRequest反而更关键。只是我们要注意应该尽量把测试环境的接口模拟放到更底层的地方而不是针对某一个业务库去做这样以后把jQuery换成别的库测试代码依然能跑。3. 实操过程从零手写一套极简自动化测试辅助库3.1 设定目标不依赖框架也能做端到端断言自动化测试往往被人误解为必须上框架比如动不动就引入一个几十MB的测试套件。其实对于很多中小项目的关键路径验证手写一个几十行的小工具集完全够用而且运行速度快、心智负担小、出了问题也容易排查。我在实践中的做法是维护一个test-helper.js里面放了几个高频工具函数waitFor、fireEvent、assert外加一个简单的输出器。这套东西不依赖任何第三方库既可以在浏览器里跑也可以挂到无头浏览器环境里由测试框架调用。整体思路就是用原生API查询DOM用轮询等待异步状态变化用简单的判断函数做断言最后输出清晰的成功失败日志。3.2 手写断言函数让失败信息对人有帮助断言是自动化测试的灵魂。很多新手写断言就用一个if加throw但报错信息写“test failed”这种事后回看一脸迷茫。我写断言函数时会强制要求传“描述信息”这样跑完一轮测试日志里每一行都清清楚楚。一个简单的断言函数长这样function assert(condition, message) { if (!condition) { throw new Error([断言失败] ${message}); } console.log([通过] ${message}); }在此基础上可以扩展出更多断言function assertEqual(actual, expected, message) { assert(actual expected, ${message}实际值${actual}期望值${expected}); } function assertContains(text, subStr, message) { assert(text.includes(subStr), ${message}未在文本中找到目标关键字); }这里有个小设计建议assertEqual输出实际值和期望值能帮你在失败时省掉大量猜测时间。我一直觉得断言函数就是一种“上天的监控摄像头”失败时要拍到现场而不是笼统地说“有个人进去了”。3.3 手写waitFor当测试需要耐心等待做自动化测试最怕异步。页面刚点击完一个按钮数据还在路途中如果你立刻断言十有八九是失败的。jQuery时代常用的无脑方案是setTimeout比如固定等待500毫秒。这个方案的问题很明显机器性能和网络状况不同固定时间要么等太久拖慢测试要么还没等到就跑了导致偶发失败。更稳健的方案是“轮询等待”隔一小段时间检查一次条件直到条件满足或超过最大时限。我用原生代码实现了一个轻量waitForasync function waitFor(checker, { timeout 5000, interval 100, description 条件 } {}) { const startTime Date.now(); while (Date.now() - startTime timeout) { if (checker()) { console.log([等待结束] ${description} 已满足); return true; } await new Promise(resolve setTimeout(resolve, interval)); } throw new Error([等待超时] ${description} 在 ${timeout}ms 内未满足); }这个函数看上去简单但它解决了异步测试的绝大部分痛点。使用的时候可以检查DOM节点是否出现比如await waitFor( () document.querySelector(.search-suggestion) ! null, { timeout: 3000, description: 搜索建议下拉框出现 } );也可以检查元素状态比如按钮是否可点击await waitFor( () { const btn document.querySelector(#submitBtn); return btn !btn.disabled; }, { timeout: 5000, description: 提交按钮变为可点击 } );我建议把waitFor设计成“默认成功并返回true”因为大多数场景下测试流程期望条件真的成立一旦不成立直接抛错中断反而是最直观的失败反馈。另外提一个实战优化超时错误信息里最好带“当前页面状态快照”比如把当前DOM里跟描述相关的部分截取出来或者把最后一次检查的返回值打出来。没有这个等待超时后你得打开浏览器自己复现非常痛苦。3.4 实战案例测试一个搜索建议下拉框空谈理论容易飘直接上一个我实测过很多次的完整案例。假设我们有一个搜索页面用户在输入框里输入关键字后前端会异步请求接口然后渲染一个搜索建议下拉框里面是一个个可点击的li元素。被测页面核心HTML结构大致如下div classsearch-box input typetext idsearch-input placeholder请输入关键字 / ul classsuggestion-list idsuggestion-list/ul /div输入“原生”之后页面会渲染出类似这样的下拉项ul classsuggestion-list li>async function testSearchSuggest() { // 1. 打开页面这里按无头浏览器环境注入的全局document示例 const input document.querySelector(#search-input); assert(input ! null, 搜索输入框存在); // 2. 模拟用户输入 input.value 原生; input.dispatchEvent(new Event(input, { bubbles: true })); // 3. 等待下拉框出现并断言渲染数量 await waitFor(() { const items document.querySelectorAll(.suggestion-list li); return items.length 0; }, { timeout: 3000, description: 存在搜索建议项 }); const items document.querySelectorAll(.suggestion-list li); assertEqual(items.length, 2, 搜索建议数量应为2个); // 4. 点击第一个建议项并验证输入框内容被填充 items[0].dispatchEvent(new MouseEvent(click, { bubbles: true })); await waitFor(() input.value.includes(原生JavaScript), { timeout: 3000, description: 第一个建议项文本被填入输入框 }); console.log(搜索建议下拉框测试通过); } testSearchSuggest().catch(err { console.error(err.message); // 在这里可以记录失败现场 });这份代码你在自己的测试环境里把document替换成无头浏览器托管的全局对象即可。整个过程没有碰jQuery也没有碰任何测试框架却覆盖了交互、渲染、异步等待、断言四件事。而且每一步都印出了日志跑完一遍就知道哪里挂的、为什么挂的。3.5 扩展一个“无宽删除”技巧让脚本更稳写测试脚本还有一个重要的隐藏需求稳定性。真实页面里总会有意外的弹窗、动画、重绘这些可能会让点击或者断言变得不稳定。除了waitFor我还会推荐两个额外机制。一个是“滚动到目标元素再点击”。在无头浏览器和真实浏览器中如果目标元素在视口之外某些事件无法正常触发。用原生API可以这样做const el document.querySelector(#submitBtn); el.scrollIntoView({ block: center, behavior: instant }); el.dispatchEvent(new MouseEvent(click, { bubbles: true }));另一个是“模糊点击目标”。有些弹层元素会遮住目标导致点击事件落在了错误区域。代码层可以做一个辅助点击前先检查目标元素是否被遮挡。实现方式是document.elementFromPoint(el.getBoundingClientRect().left, el.getBoundingClientRect().top)是否等于目标元素或它的后代。如果不等先隐藏遮挡物再操作。这个细节做实测时经常救你一命尤其是老项目里各种花式定位弹窗。4. 常见问题与排查技巧实录4.1 querySelectorAll返回的不是数组遍历时的三个救命方案NodeList看起来像数组但它不是直接调用.map、.filter会抛TypeError。这里我梳理一下三种转换方式的适用场景方式写法适用场景Array.fromArray.from(nodes)最推荐语义清楚兼容性也好展开语法[...nodes]写法最简洁但代码风格更“新”可读性略差prototype借用[].slice.call(nodes)老旧代码里常见效率略高但不够直观实测下来现代浏览器和测试环境的Node版本都支持Array.from所以我没特别理由不用它。每当看到某个测试脚本里出现奇怪报错第一反应看看是不是忘了Array.from。4.2 为什么dispatchEvent没触发监听器bubbles陷阱这是我被问过最多的问题。代码明明写了btn.dispatchEvent(new Event(click))但页面监听器就是没反应。90%的原因是bubbles没有设成true。回想一下原生事件机制事件派发时有一个传播路径从window往目标元素走捕获再到目标自身再从目标往window走冒泡。如果不指定bubbles: true事件默认不冒泡而绝大多数业务监听器是挂在document或父容器上的自然就收不到。还有个相关的坑是事件类型不匹配。比如你想模拟点击用了new Event(click)虽然没报错但有些浏览器对某些事件有更具体的事件类要求比如拖拽相关建议用DragEvent、键盘用KeyboardEvent、鼠标用MouseEvent。选不对事件类少数监听器会拒绝触发。所以建议按场景选具体的Event子类别图省事全都用new Event。4.3 异步时序测试偶发失败不是代码的锅如果测试不是每次稳定跑过而是偶尔红一下优先怀疑“竞态问题”。最常见的竞态是断言跑得比异步操作早。waitFor虽然解决了大多数问题但有些人会把waitFor放错位置比如在点击前等元素、而不是在点击后等结果这样等于白等。我总结的排查顺序如下先确认等待条件检查的是“结果状态”而不是“初始状态”。比如点了按钮之后要等的应该是按钮变灰而不是按钮存在。检查interval设置是否合理。默认100毫秒比较稳如果页面本身有300毫秒的动画建议interval也调成200左右避免频繁轮询干扰页面性能。确认timeout是否够用。在CI环境跑测试机器负载可能比本地高超时时间要比本地多留30%的余量。如果偶发的是元素模糊不清考虑加上scrollIntoView这个在前面第3节讲过。实在查不出来就在超时分支里输出当前页面的关键DOM状态快照比如所有class包含“error”的元素数量。我看到这一行日志往往几秒钟就能定位脚本是卡在哪个断言上。4.4 老浏览器兼容性哪些原生API有坑既然做“替代jQuery”这件事通常意味着项目有年头。但老浏览器通常跟“现代API”水火不容。我自己实测中最容易出兼容问题的原生API是这些fetch老浏览器没有。不过自动化测试一般跑在较新的浏览器或Node环境真遇到老环境可以垫一个很简短的polyfill或者直接要求测试环境升级。classList在超老版本里支持不全比如不能传多个类名。但只做add、remove、contains基本够用。dataset老浏览器里>function installMockApi(routes) { const origFetch window.fetch; window.fetch async (url, options) { const match routes.find(r url.includes(r.keyword)); if (match) { return { ok: true, status: 200, json: async () match.response, text: async () JSON.stringify(match.response) }; } return origFetch(url, options); }; const origOpen XMLHttpRequest.prototype.open; const origSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open function(method, url, ...rest) { this._mockUrl url; return origOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send function(...args) { const match routes.find(r this._mockUrl.includes(r.keyword)); if (match) { const fakeResponse JSON.stringify(match.response); Object.defineProperty(this, responseText, { value: fakeResponse }); Object.defineProperty(this, status, { value: 200 }); this.onreadystatechange?.(); return; } return origSend.apply(this, args); }; }这个方案我实测过好处是拦截层很薄不需要了解业务代码用的是哪个请求库。坏处是XHR那块的兼容处理比较繁琐尤其onreadystatechange的触发时机要模拟得像模像样否则业务回调不执行。所以如果你只是测试新页面我建议优先用fetch mock真遇到老模块再补XHR那套不要上来就双管齐下。5. 替代过程中值得保存的个人经验再到文章最后我分享几条自己在真实项目中总结出来的体会希望能帮你少踩几个坑。第一点替换jQuery不要追求“一夜完成”。正确的姿势是把项目拆成模块先让测试辅助脚本完全不用jQuery再逐个模块把业务代码里的依赖替换掉每替换一个模块就跑一遍回归测试。我见过有人拍板说“下周把jQuery全删了”结果加班到凌晨三点页面还是一堆undefined就是因为腰斩式重构太难控了。从测试代码下手风险小见效快能持续给你信心。第二点每次用原生API替代一个能力时都值得顺手优化一下代码的健壮性。比如以前写$(.item).text()时如果没匹配到元素jQuery会静默返回空字符串而原生document.querySelector(.item).textContent会直接抛错。很多人觉得“哎好烦”但我觉得这是优点问题暴露得越早成本越低。在测试场景里你更希望一个错误大声喊出来而不是藏到后面某个无关断言里才爆炸。第三点善用无头浏览器的调试能力。原生API写的测试脚本通常很容易挂在浏览器的devtools里跑一遍因为每一行都是直白到不能再直白的浏览器API没有一层封装。我用这种脚本排查过不少复杂的异步问题直接把debugger打在dispatchEvent前面然后一步一步看DOM状态变化比用jQuery时代的黑盒触发方式爽快很多。现在很多无头浏览器也支持原地调试这点优势会越来越大。第四点别忘了记录自己的API映射表。你可以在旧项目里建一份专门的小笔记记录那些在业务里高频出现、并且你已经完成替代的写法。一旦以后在其他项目里又碰到相同套路这份笔记就是你的“翻译词典”能省下大量的回忆时间。说实话这份笔记的价值比这篇博文本身还大。原生JavaScript替代jQuery这件事不是一种对旧技术的嘲弄更多是一种工程上的取舍。在自动化测试这个对稳定性和可控性要求极高的领域把依赖面收窄、把执行路径看清本身就是最朴素的进步。如果你手头正好有一个还在用jQuery的老项目也从今天把测试脚本里的$挑出来把它们一个个换成“querySelector”。起步不用多先换一个文件跑通一轮回归你会立刻感受到这种替换带来的确定性。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑