用户注册页面这东西说简单也简单一个表单几个输入框说麻烦也麻烦真正上线前你会发现光是校验这两个字就能衍生出十几种边界情况。我见过太多项目前端校验写得花里胡哨结果用户把浏览器开发者工具一开、把 disabled 属性一删脏数据照样进库。也见过另一种极端前端什么都不管全甩给后端用户填完点提交等三秒钟弹出一句用户名已存在体验差到想砸键盘。这篇内容围绕一个很具体的场景展开用原生 JavaScript 实现用户注册页面把用户名、密码、验证码这几项的校验逻辑做扎实。关键词就三个——表单校验、正则表达式、验证码。适合谁看刚接触前端表单交互、想把这套东西一次搞明白的开发者也适合已经写过但总觉得校验逻辑散落各处、想重新梳理一遍的人。我会从为什么校验要分层讲起把每一项字段的校验规则、正则怎么写、验证码怎么在前端模拟、错误提示怎么组织一步步拆开讲中间穿插我自己踩过的坑和实际项目里验证过的做法。不依赖任何框架纯原生 JS复制过去就能跑。1. 先把校验这件事的分层想清楚很多人一上来就写if (username )写着写着发现代码越来越长最后变成一个几百行的函数改一个规则要翻半天。问题不在于 JS 本身而在于没想清楚校验的职责该怎么分。我在实际项目里总结下来一个注册表单的校验至少涉及三个层面每个层面的目标和失败后的处理方式完全不同。1.1 前端校验到底该承担什么、不该承担什么前端校验的第一职责是即时反馈让用户在离开输入框的那一刻就知道自己填得对不对而不是等到点提交才被告知。第二职责是减少无效请求明显不合规的数据没必要发到后端去浪费一次往返。但前端校验绝对不能承担的是安全兜底——因为前端代码对用户是完全透明的任何人打开控制台都能绕过你的所有if判断。这个认知很关键。它决定了你写前端校验时的心态你是在帮用户少犯错不是在防攻击。真正的唯一性检查、密码强度最终判定、验证码正确性比对必须由后端来做。前端做的验证码校验只是格式对不对、有没有填不是这个码是不是真的。提示如果你在面试或代码评审里听到有人说前端校验过了就不用后端再校验这基本可以直接判定对方没做过真实项目。两层校验缺一不可前端管体验后端管安全。理解了这一点后面的代码组织就有了方向前端校验逻辑要快、要即时、要友好但不要试图做得密不透风那是后端的活。1.2 即时校验与提交校验的触发时机差异同一个字段在不同时机被校验规则的重心是不一样的。以用户名为例输入过程中input 事件只做轻量提示比如字符类型是否合法、长度是否超限不要在这个阶段弹用户名已存在因为用户还没输完。失焦时blur 事件做完整格式校验给出明确的通过或失败状态。提交时submit 事件把所有字段再整体过一遍防止用户跳过了某些字段的失焦比如直接用鼠标点提交按钮某些浏览器下 blur 和 submit 的触发顺序会有微妙差异。我早期写过一个版本只在 submit 时统一校验结果用户填错一个字段提交后页面刷新所有内容清空用户得从头再填一遍——这种体验在真实产品里是致命的。后来改成即时校验加提交兜底投诉量直接归零。1.3 校验结果的数据结构设计不要用一堆散落的布尔变量isNameOk、isPwdOk、isCodeOk来记录状态字段一多就乱。我习惯用一个对象统一管理const validateState { username: { valid: false, message: }, password: { valid: false, message: }, confirm: { valid: false, message: }, captcha: { valid: false, message: } };每次校验只更新对应字段的valid和message提交时用Object.values(validateState).every(item item.valid)一句话判断能否提交。这个结构的好处是错误提示的渲染可以直接遍历这个对象不用为每个字段单独写 DOM 操作代码。字段增减时改动量也小。2. 用户名校验正则只是起点边界才是重点用户名校验看起来是最简单的一项实际上边界情况最多。长度、字符集、首字符、保留字、前后空格每一项都能让线上出问题。我按实际项目里遇到过的顺序把这块拆开讲。2.1 长度与字符集的规则取舍先定规则。一个合理的用户名规则通常是长度 4 到 16 位允许字母、数字、下划线且必须以字母开头。为什么不允许数字开头因为纯数字开头的用户名在后续做展示、排序、甚至某些系统里做 ID 拼接时容易产生歧义这是行业里比较通行的约定。对应的正则const usernameReg /^[a-zA-Z][a-zA-Z0-9_]{3,15}$/;拆开看^[a-zA-Z]保证首字符是字母[a-zA-Z0-9_]{3,15}表示后续 3 到 15 位加上首字符正好是 4 到 16 位。这里有个容易写错的地方——很多人写成{4,16}结果总长度变成 5 到 17差一位。正则里的量词是作用于它前面那个字符类的不含首字符这个细节必须算清楚。为什么用test()而不是match()因为test()返回布尔值语义更直接性能也略好不需要构造匹配结果数组。校验场景下test()是更合适的选择。2.2 前后空格与不可见字符的处理用户从别处复制粘贴用户名时极容易带上首尾空格甚至带全角空格\u3000或不换行空格\u00A0。如果直接拿原始值去校验正则的^和$会因为空格而判定失败用户看着输入框里明明填对了却一直报错非常困惑。我的处理方式是在校验前先做一次清洗function normalizeUsername(raw) { return raw.replace(/^[\s\u3000\u00A0]|[\s\u3000\u00A0]$/g, ); }清洗完再校验同时把清洗后的值回写到输入框让用户看到实际生效的内容。这一步很多人省略结果就是线上零星出现我明明没输空格的客诉。另外要注意清洗只针对首尾中间的空格不应该被静默删除——中间有空格本身就不符合规则应该明确报错而不是悄悄改掉用户输入。2.3 保留字与敏感词的前端预判有些用户名格式完全合法但不应该被注册比如admin、root、system、null、undefined这类。前端可以做一层预判命中就提示该用户名不可用减少一次后端往返。实现很简单const reservedWords [admin, root, system, null, undefined, test]; if (reservedWords.includes(value.toLowerCase())) { return { valid: false, message: 该用户名不可用请更换 }; }注意这里要转小写再比对因为Admin和admin在大多数系统里被视为同一个。前端这层只是体验优化真正的保留字清单以后端为准前端清单保持精简即可不必追求完全同步——同步成本高收益低。2.4 把校验函数写成纯函数我强烈建议把每个字段的校验写成纯函数输入字符串输出{ valid, message }不碰 DOM、不读全局状态。这样做的好处是测试极其方便而且逻辑和渲染彻底解耦。function validateUsername(raw) { const value normalizeUsername(raw); if (!value) { return { valid: false, message: 请输入用户名 }; } if (value.length 4 || value.length 16) { return { valid: false, message: 用户名长度需为 4-16 位 }; } if (!usernameReg.test(value)) { return { valid: false, message: 需以字母开头仅含字母、数字、下划线 }; } if (reservedWords.includes(value.toLowerCase())) { return { valid: false, message: 该用户名不可用请更换 }; } return { valid: true, message: }; }注意判断顺序先判空再判长度再判格式最后判保留字。顺序有讲究——空值如果直接丢给正则报的是格式错误用户会莫名其妙所以空值必须单独先拦。这种从粗到细的判断顺序是所有校验函数都应该遵循的通用原则。3. 密码校验强度规则背后的真实考量密码这块很多人以为规则越复杂越安全其实不然。规则设计要在安全性和可用性之间找平衡而且前端能做的只是格式把关真正的强度评估和泄露检测得靠后端。3.1 强度规则怎么定才不招人烦常见的密码规则是8 到 20 位必须同时包含字母和数字可选要求包含特殊字符。我倾向于不强制要求特殊字符原因很实际——强制特殊字符会显著提高用户忘记密码的概率而用户为了记住往往会用Password1!这种可预测的变体安全性反而下降。我采用的规则是长度 8 到 20 位至少包含字母和数字两类允许但不强制特殊字符。正则拆成两个判断更清晰function validatePassword(raw) { if (!raw) { return { valid: false, message: 请输入密码 }; } if (raw.length 8 || raw.length 20) { return { valid: false, message: 密码长度需为 8-20 位 }; } if (/\s/.test(raw)) { return { valid: false, message: 密码不能包含空格 }; } const hasLetter /[a-zA-Z]/.test(raw); const hasDigit /\d/.test(raw); if (!hasLetter || !hasDigit) { return { valid: false, message: 密码需同时包含字母和数字 }; } return { valid: true, message: }; }为什么单独拦空格因为空格在密码里是合法的但很多用户在移动端输入时误触空格键自己却看不出来导致登录时怎么输都不对。前端直接禁止空格能省掉大量密码明明对却登不上的排查成本。这是一个典型的规则为体验让路的例子。3.2 确认密码的一致性校验时机确认密码的校验逻辑很简单——和密码框的值比对。但时机很关键。如果用户在密码框里还在输入确认框就实时比对会一直闪红体验很差。我的做法是确认密码框只在自身blur时比对以及密码框内容变化时如果确认框已有值则重新触发一次比对。function validateConfirm(pwd, confirm) { if (!confirm) { return { valid: false, message: 请再次输入密码 }; } if (pwd ! confirm) { return { valid: false, message: 两次输入的密码不一致 }; } return { valid: true, message: }; }还有一个细节当用户修改了密码框而确认框里已经有旧值时必须主动重新校验确认框否则会出现密码改了、确认框还是旧值、但状态显示通过的漏洞。这个联动逻辑很容易漏漏了就是线上 bug。3.3 密码可见性切换的实现细节给密码框加一个眼睛图标切换明文/密文几乎是现代注册页的标配。实现上就是把input的type在password和text之间切换toggleBtn.addEventListener(click, () { const isPwd pwdInput.type password; pwdInput.type isPwd ? text : password; toggleBtn.setAttribute(aria-label, isPwd ? 隐藏密码 : 显示密码); });这里有个容易被忽略的点切换type会导致输入框失去焦点如果用户正在输入光标位置会跳到末尾。解决办法是切换前记录selectionStart切换后恢复。另外aria-label一定要跟着切换否则读屏软件用户完全不知道这个按钮当前是什么状态。无障碍这块很多注册页都做得一塌糊涂其实成本很低。4. 验证码前端模拟与真实接入的边界验证码是注册页里最容易被做歪的一环。前端能做的和不能做的必须分清楚否则要么形同虚设要么过度设计。4.1 前端生成图形验证码的完整实现在没有后端配合的演示或纯前端项目里可以用 Canvas 画一个简单的字符验证码。核心思路是随机生成 4 位字符逐个绘制到 Canvas 上每个字符加随机旋转和偏移再叠加干扰线和噪点。function drawCaptcha(canvas, code) { const ctx canvas.getContext(2d); const w canvas.width, h canvas.height; ctx.fillStyle #f2f4f7; ctx.fillRect(0, 0, w, h); // 干扰线 for (let i 0; i 4; i) { ctx.strokeStyle randomColor(120, 200); ctx.beginPath(); ctx.moveTo(Math.random() * w, Math.random() * h); ctx.lineTo(Math.random() * w, Math.random() * h); ctx.stroke(); } // 字符 const chars code.split(); chars.forEach((ch, i) { ctx.save(); const x (w / chars.length) * i w / chars.length / 2; const y h / 2; ctx.translate(x, y); ctx.rotate((Math.random() - 0.5) * 0.5); ctx.font ${22 Math.random() * 6}px monospace; ctx.fillStyle randomColor(30, 120); ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(ch, 0, 0); ctx.restore(); }); }字符集要排除容易混淆的0/O、1/l/I这是验证码可用性的基本要求。生成后把正确值存在一个闭包变量里比对时用。必须强调这种前端验证码只能防手滑填错防不了任何自动化脚本因为正确值就在前端内存里。真实项目里验证码的生成和校验必须在服务端完成前端只负责展示图片和提交用户输入。4.2 验证码的比对逻辑与刷新机制比对逻辑本身很简单但有两个细节值得说。第一比对时统一转大写或小写避免用户因为大小写问题反复失败——验证码的字符通常不区分大小写这是通行做法。第二无论比对成功还是失败都应该立即刷新验证码防止同一个码被重复使用。function checkCaptcha(input, expected) { const ok input.trim().toUpperCase() expected.toUpperCase(); refreshCaptcha(); // 无论对错都刷新 return ok; }刷新机制上点击图片刷新是标配但别忘了加一个看不清换一张的文字链接因为有些用户不知道图片可以点。这种小细节在真实产品里能明显降低放弃率。4.3 验证码与提交按钮的联动一个常见的设计是验证码没填或填错时提交按钮保持禁用。但我不建议这么做原因有二。其一按钮禁用状态对用户来说是死的用户不知道自己还差什么容易困惑。其二禁用按钮在某些辅助技术下不可聚焦无障碍体验差。更好的做法是按钮始终可点点击时统一校验哪个字段有问题就把焦点移到那个字段并高亮提示。这样用户永远能得到明确的下一步指引而不是面对一个灰掉的按钮干瞪眼。5. 错误提示的组织与无障碍处理校验逻辑写完了怎么把结果呈现给用户是另一半功夫。提示写得好用户自己就能改对写得差用户只能反复试错。5.1 提示文案的写法原则好的错误提示要满足三点说清楚哪里错了、说清楚怎么改、不指责用户。对比一下差的提示好的提示格式错误用户名需以字母开头仅含字母、数字、下划线密码不符合要求密码需 8-20 位且同时包含字母和数字验证码错误验证码不正确已为您刷新请重新输入差的提示等于没说用户只能猜。好的提示直接给出可执行的修正方向。另外提示文案里不要用您输入有误这种带指责意味的表述改成陈述规则即可。5.2 错误状态的视觉与语义双重标记视觉上错误字段加红色边框、下方显示红色提示文字这是基本操作。但只做视觉是不够的——色盲用户可能分辨不出红绿差异读屏软件用户也看不到颜色。所以必须同时做语义标记function setFieldState(input, state) { const tip document.getElementById(input.id -tip); input.classList.toggle(is-error, !state.valid); input.setAttribute(aria-invalid, String(!state.valid)); if (state.message) { input.setAttribute(aria-describedby, input.id -tip); tip.textContent state.message; tip.hidden false; } else { input.removeAttribute(aria-describedby); tip.hidden true; } }aria-invalid和aria-describedby这两个属性是读屏软件理解错误状态的关键加上它们无障碍体验立刻上一个台阶成本却几乎为零。5.3 焦点管理与提交失败后的引导提交时如果有多个字段不合法应该把焦点移到第一个出错的字段而不是最后一个也不是随机一个。用户的心智是从上往下填第一个错误才是他下一步要处理的。function focusFirstError() { const order [username, password, confirm, captcha]; for (const key of order) { if (!validateState[key].valid) { document.getElementById(key).focus(); return; } } }这个order数组显式定义了字段的视觉顺序比依赖 DOM 顺序更可靠——万一以后调整了布局只要同步改这个数组就行逻辑不受影响。6. 那些只有真正上线后才会遇到的坑前面讲的都是应该怎么做这一节讲实际做的时候会怎么翻车。这些都是我在项目里真实遇到或见别人踩过的写出来供参考。6.1 输入法组合输入导致的误判中文用户输入时输入法有一个组合输入阶段composition此时input事件会频繁触发但值还没最终确定。如果在这个阶段就做校验用户拼音还没选完就被判定含非法字符体验极差。解决办法是监听compositionstart和compositionend在组合输入期间跳过校验let composing false; input.addEventListener(compositionstart, () { composing true; }); input.addEventListener(compositionend, () { composing false; runValidate(input); }); input.addEventListener(input, () { if (!composing) runValidate(input); });这个坑在纯英文环境下测不出来一上线遇到中文用户就暴露。用户名虽然通常不允许中文但用户可能先输入中文再删掉过程中一样会触发误判。6.2 自动填充autofill不触发校验浏览器自动填充用户名密码时往往不触发input和change事件导致输入框里明明有值但校验状态还是未填写提交时被拦下。这个问题的排查很费劲因为手动测试时一切正常。应对方式是在页面加载后延迟检查一次各字段的值或者监听animationstart某些浏览器 autofill 会触发一个特定的动画。更稳妥的做法是提交时无条件重新校验所有字段不依赖之前的状态缓存。这也是为什么我一直强调提交时整体再校验一遍——它同时解决了 autofill 和状态不同步两类问题。6.3 移动端键盘类型与输入限制的配合移动端上input的type和inputmode属性决定了弹出什么键盘。用户名框用typetext配合autocapitalizeoff、autocorrectoff、spellcheckfalse避免首字母自动大写和拼写检查干扰。验证码框用inputmodetext并关闭自动大写。这些属性不加移动端用户输入体验会明显打折而且这类问题在桌面端完全测不出来。还有一个细节maxlength属性虽然能限制输入长度但它对粘贴的内容也生效且不会触发校验提示。所以maxlength只作为辅助真正的长度校验还是要在 JS 里做两者配合使用。6.4 提交防抖与重复提交用户手快连点两次提交按钮或者网络慢时反复点击都会产生重复请求。前端必须做防抖提交开始后立即禁用按钮并改变文案如提交中...请求返回后再恢复。let submitting false; form.addEventListener(submit, async (e) { e.preventDefault(); if (submitting) return; // 先整体校验 if (!validateAll()) { focusFirstError(); return; } submitting true; submitBtn.disabled true; submitBtn.textContent 提交中...; try { await fakeSubmit(); } finally { submitting false; submitBtn.disabled false; submitBtn.textContent 注册; } });注意finally里一定要恢复状态否则请求失败后按钮永远卡在提交中用户只能刷新页面。这个错误我在早期项目里犯过排查了半天才发现是异常路径没恢复按钮状态。7. 把整页逻辑串起来的组织方式最后说说代码怎么组织。单个 HTML 文件里塞几百行 JS 也能跑但维护起来痛苦。我的习惯是拆成三层校验规则层纯函数、状态管理层validateState 的更新、视图层DOM 操作和事件绑定。三层之间单向依赖规则层不碰 DOM视图层不写规则。事件绑定上用事件委托统一处理所有输入框的blur和input而不是给每个框单独绑form.addEventListener(blur, (e) { const field e.target.dataset.field; if (field) runValidate(field, e.target); }, true); form.addEventListener(input, (e) { const field e.target.dataset.field; if (field !composing) runValidate(field, e.target, { light: true }); });blur用捕获阶段第三个参数true因为blur不冒泡只有捕获阶段能在父元素上监听到。这个细节很多人不知道导致委托失效。input事件则正常冒泡用默认的冒泡阶段即可。runValidate内部根据field名分发到对应的纯函数更新validateState再调用setFieldState渲染。整个流程清晰加字段只需在规则层加一个函数、在validateState加一项、在 HTML 加>
企业数字化 ERP 产品动态
相关推荐
高通平台LCD与TP驱动Porting:设备树、时序与联调排错 1. 先把 Porting 拆成两条线:整体设计与思路高通平台上的 TP 和 LCD 驱动 Porting,说白了就是让一块陌生的屏幕亮起来、让一层陌生的触摸膜准起来。在手机、平板、POS 机、车机这类整机项目里,这两件事几乎永远绑在一起做:屏不亮&… · 2026/11/3 3:32:35
Android面试题及答案:四大组件、Handler机制与内存泄露考点梳理 简介:这份文档面向准备Android开发岗位面试的初中级工程师与求职者,围绕常见面试考点整理试题及参考答案,可用于系统复习、查漏补缺和面试前自测。内容覆盖Activity生命周期与状态保存、五种常用布局、SharedPreferences与SQLite等数据存储方… · 2026/11/3 3:32:35
琴语言虚拟机架构:融合代数计算与符号执行的技术解析 1. 项目背景与核心概念解析"云藏山鹰代数信息系统"是一个融合了形式化方法与程序语言理论的实验性项目,其核心组件"神游思想之琴语言"(以下简称琴语言)采用了一种独特的虚拟机架构来实现代数计算与符号推理。从代码段编号… · 2026/11/3 3:32:35
AI工程体系从零构建:Python、TypeScript、Rust与Julia分层实践 1. 为什么“从零构建AI工程体系”不是写个模型脚本那么简单很多人看到“AI Engineering from Scratch”这个标题,第一反应是:不就是用PyTorch搭个ResNet,再跑个MNIST?或者用Hugging Face加载个BERT微调一下?——这确实… · 2026/11/3 4:05:40
Canvas 2D代码雨实战:清凉视觉参数、拖尾原理与性能优化 1. 把“清凉”拆解为可执行的视觉参数盛夏做视觉,最怕的就是画面上再加一堆高饱和暖色。眼睛在室外已经被晒了一整天,打开屏幕再来一片橙红,那种燥是不需要解释的。所以当我决定做一场夏日清凉主题的代码雨,第一件事不是打开编辑器… · 2026/11/3 4:05:40
TensorFlow 2024入门指南:环境搭建、模型开发与PyTorch选型对比 说实话,2024年还主动搜索tensorflow的人,我是真挺佩服的。这名字已经不像前几年那么刷屏,可它依然是工程界绕不开的一个关键词。原来大家一上来就问“tensorflow安装”,现在更多人问的是“tensorflow和pytorch到底选哪个”。但我始… · 2026/11/3 4:05:39
DeepSeek V3与R1提示语设计差异:从指令到推理的模型选择与优化指南 简介:这份PDF资料是清华大学新媒沈阳团队推出的《DeepSeek从入门到精通(第二版)》,聚焦DeepSeek如何赋能职场应用,面向希望借助AI提升工作效率的职场人、内容创作者与研究者,也适合想系统理解大模型能力边界… · 2026/11/3 4:05:39
PyTorch多元芯片适配难题:Torch-FL如何终结碎片化 “这模型明明在 NVIDIA 上跑得好好的,换个国产加速卡就各种算子不支持、显存报错、速度还慢了十倍。”过去两年,我在好几个项目群里看到类似的吐槽。PyTorch 作为目前 AI 工程界事实上的标准框架,在多元 AI 芯片时代反而成了“适配地狱”——… · 2026/11/3 4:05:39
PSIM光伏阵列建模与MPPT仿真:从I-V曲线到C语言DLL实现 简介:面向光伏发电系统仿真入门与电力电子电路建模学习者,这份文档围绕光伏电池外特性建模展开,讲解如何借助PSIM搭建简单且准确的光伏阵列模型,并在此基础上构建带MPPT功能的DC-DC变换器仿真模型,通过不同光照与温度条… · 2026/11/3 4:04:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18