新闻详情

新闻详情

首页 / 资讯中心 / 详情

Vue手写签名组件封装:Canvas高清屏适配与贝塞尔平滑实践

发布时间:2026/9/29 1:15:34来源:尧图网络
Vue手写签名组件封装:Canvas高清屏适配与贝塞尔平滑实践
大概两年前我做物流签收项目产品扔过来一句话“给收件人加个手写签名功能让用户直接在屏幕上签。”当时我觉得这有什么难的一块canvas画布监听几个鼠标和触摸事件最后把图存下来就算完。结果半天写完第二周就被打脸签出来的字断断续续保存的图片在手机上糊成一片放进弹窗里画布直接空白。后来我痛定思痛把签名功能抽成了一个Vue独立组件一步步把这几个问题全解决掉顺带总结了可复用的封装思路。这篇文章不是丢一段demo就完事。我会从“为什么要独立封装”讲起接着聊技术选型再到Canvas核心绘制、高清屏适配、兼容性坑位最后给出完整接入项目的流程。如果你正准备在Vue项目里做电子签名、手写签收或任何需要用户“落笔确认”的功能这篇文章应该能帮你省掉不少排查时间。1. 为什么手写签名值得单独抽成一个组件而不是页面里写死一块画布1.1 真实业务里“签名”几乎是无处不在的隐形需求我最初以为只有物流签收会用到签名后来发现完全不是这么回事。同一个公司内部客服平板上的退货确认要签名线下门店的电子小票要签名合同审批的App里要签名甚至展会访客登记都要在平板上画个名字。虽然业务形态不同但技术需求几乎一模一样一块可绘制的画布把用户在屏幕上的笔迹变成一张图片。也就是说一旦第一个业务接入了签名功能第二个、第三个业务马上会来问“你们那个签字的东西能不能给我也用一下”这时候如果你把canvas逻辑写在某个页面组件里就只能复制粘贴然后看着两处代码越改越不一致。把签名抽成独立组件本质上是在做一个“一次开发、多处复用”的能力沉淀。对业务方来说他们并不关心canvas怎么初始化、图片怎么导出他们只想要一个能签字的框以及一个能拿到签名结果的接口。1.2 独立组件的价值复用、隔离、可测试“独立组件”不是把代码从一个文件搬到另一个文件就叫独立而是要做到对外只暴露少量props、事件和方法内部实现完全封装。这个边界清晰以后价值会非常明显。复用性多个页面、多条业务线可以直接引不用重复写canvas初始化逻辑。隔离性签名涉及的坐标系转换、像素比处理、事件兼容性全在组件内部消化页面代码保持干净。可测试性独立组件可以单独调试不依赖某个业务页面出问题能快速定位是组件的问题还是业务的问题。定制性不同业务可能有不同尺寸、线宽、颜色要求通过props传入即可不需要改组件源码。我见过很多项目是把签名逻辑直接写在业务页面里的页面里一堆canvas操作代码看着就很头疼。等到产品说“我们这个页面也要加签名”你就得复制几百行代码过去还要祈祷别漏掉某个变量。所以后来我养成了习惯凡是这种逻辑独立、多个地方可能用到的功能先封装成组件再说。2. 自己写Canvas还是引signature_pad我最后选了前者2.1 signature_pad确实成熟但它在实际项目里存在几个尴尬点签名画布这个领域开源社区已经有一个比较出名的库叫signature_pad而且网上也有不少人基于它封装了Vue组件。老实说如果只想快速搞定一个demo直接装依赖用它没问题它有现成的笔迹平滑、导出图片、清空画布这些能力。但在正经产品里我最终没有选它原因很现实对比维度signature_pad方案自研Canvas组件体积引入库封装层多一份依赖单文件组件几KB搞定定制性改样式/增功能要看源码扩展受限完全在自己手里随意改调试成本出问题要查库源码和封装层自己的代码断点随便打升级风险第三方依赖版本更新可能带来破坏性变更无外部依赖版本自控学习成本团队成员要了解库的API和配置参数只要懂基础Canvas就行还有一个很现实的问题signature_pad的笔迹平滑效果需要调参数而且默认参数在部分安卓WebView上画出来的字依然有毛刺。业务方不会管你用的是不是成熟开源库他们只看到“这字怎么这么丑”。自己的组件我可以把线宽、贝塞尔平滑、像素比适配全部做到位出了问题也能立刻修。2.2 自研组件的路线图和对外能力清单当然我不否认某些场景下直接用开源库更合适。如果是内部工具、快速原型、或者对签名清晰度要求不高的场景直接上signature_pad完全没问题。但如果你像我一样要把签名这个能力长期沉淀在项目里自研一次后续维护起来会轻松很多。我最终设计的组件对外能力清单是这样的propswidth组件宽度、height组件高度、lineWidth笔迹粗细、strokeStyle笔迹颜色、backgroundColor画布背景色v-model支持组件内部把签名结果同步成一个base64字符串方便接入表单方法getSignature()获取图片、clear()清空、undo()撤销上一笔、isEmpty()判断是否签过名事件不需要额外自定义事件统一通过input事件对外输出结果这套接口设计遵循一个原则调用方只关心“签名”这个业务概念不关心Canvas技术细节。组件外面的人不需要知道什么像素比、坐标系他们只需要签个字、拿图、提交。3. 一条签名笔迹从按下到抬笔Canvas里发生了什么3.1 画布的基础配置线宽、颜色、端点样式一个都不能含糊很多人第一次写canvas签名最容易忽略的就是lineCap和lineJoin这两个属性。如果不设置笔画端点是方形的默认值是butt和miter快速划线时会出现明显的锯齿感。设置了round之后笔迹端点会变成圆头线条转折处也更圆润视觉上更接近真实笔触。下面这段代码是组件初始化画布的核心逻辑initCanvas() { const canvas this.$refs.canvas if (!canvas) return const rect canvas.getBoundingClientRect() if (!rect.width || !rect.height) return // 容器不可见时跳过 const dpr window.devicePixelRatio || 1 canvas.width rect.width * dpr canvas.height rect.height * dpr canvas.style.width rect.width px canvas.style.height rect.height px const ctx canvas.getContext(2d) ctx.scale(dpr, dpr) ctx.lineCap round ctx.lineJoin round ctx.lineWidth this.lineWidth ctx.strokeStyle this.strokeStyle // 背景色填充不填的话导出图片可能是透明底 ctx.fillStyle this.backgroundColor ctx.fillRect(0, 0, rect.width, rect.height) this.ctx ctx }注意一个关键点canvas.width和canvas.height是位图尺寸而canvas.style.width和canvas.style.height是CSS显示尺寸。这两个必须分开设置否则会出现“画布超大但显示被压缩”或者“画布铺满但位图过小”的问题。3.2 事件采集与坐标系转换为什么必须用getBoundingClientRect签名组件的核心交互就是三个事件按下、移动、抬起。在Vue模板里我使用了Pointer事件因为pointerdown/pointermove/pointerup可以同时覆盖鼠标和触摸屏不用分别监听mousedown/touchstart两套事件。canvas refcanvas classsignature-pad__canvas pointerdownonPointerDown pointermoveonPointerMove pointeruponPointerUp pointercancelonPointerUp /canvas拿到事件对象以后第一步是把屏幕坐标转换成画布坐标。这里有个非常常见的坑如果直接用e.clientX和e.clientY去绘制当canvas不在页面左上角时笔迹会整体偏移。正确做法是减去canvas的偏移量getPos(e) { const rect this.$refs.canvas.getBoundingClientRect() if (e.touches e.touches[0]) { return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top } } return { x: e.clientX - rect.left, y: e.clientY - rect.top } }这里必须用getBoundingClientRect()而不是offsetLeft/offsetTop。因为getBoundingClientRect()返回的是元素相对于视口的实时位置页面发生滚动时这个值会自动更新而offsetLeft/offsetTop是相对于offsetParent的跟视口坐标不是一个体系在滚动页面里会直接画偏。3.3 笔迹平滑用二次贝塞尔曲线替代直线段如果只是简单地在pointermove里把上一个点和当前点连成直线快速书写时笔画会显得生硬甚至能看到明显的折角。解决方案是用二次贝塞尔曲线在两个点的中点之间做平滑过渡。我的实现方式是维护一个points数组记录当前一笔的所有落点。移动时把新点push进数组然后只绘制最新的一段增量onPointerDown(e) { e.preventDefault() this.drawing true this.hasSign true const pos this.getPos(e) this.points [pos] try { this.$refs.canvas.setPointerCapture(e.pointerId) } catch (err) { // 部分浏览器不支持setPointerCapture忽略即可 } }, onPointerMove(e) { if (!this.drawing) return e.preventDefault() const pos this.getPos(e) // 过滤掉距离过近的点避免无效绘制 if (this.points.length) { const prev this.points[this.points.length - 1] const dist Math.sqrt(Math.pow(pos.x - prev.x, 2) Math.pow(pos.y - prev.y, 2)) if (dist 1) return } this.points.push(pos) this.drawIncrement(this.points) }增量绘制函数的核心逻辑是取最新三个点用前两个点的中点作为起点用中间点作为控制点画一段到后两个点中点的二次贝塞尔曲线drawIncrement(points) { if (points.length 2) return const ctx this.ctx ctx.save() ctx.lineWidth this.lineWidth ctx.strokeStyle this.strokeStyle ctx.lineCap round ctx.lineJoin round ctx.beginPath() if (points.length 2) { ctx.moveTo(points[0].x, points[0].y) ctx.lineTo(points[1].x, points[1].y) } else { const p0 points[points.length - 3] const p1 points[points.length - 2] const p2 points[points.length - 1] const mid1 { x: (p0.x p1.x) / 2, y: (p0.y p1.y) / 2 } const mid2 { x: (p1.x p2.x) / 2, y: (p1.y p2.y) / 2 } ctx.moveTo(mid1.x, mid1.y) ctx.quadraticCurveTo(p1.x, p1.y, mid2.x, mid2.y) } ctx.stroke() ctx.restore() }如果你只点了一下屏幕就抬起来没有触发任何movepoints数组里只有一个点直接画线是画不出来的。所以我在pointerup里单独处理了这种场景画一个小圆点onPointerUp(e) { if (!this.drawing) return e.preventDefault() this.drawing false if (this.points.length 1) { const pos this.points[0] this.ctx.beginPath() this.ctx.arc(pos.x, pos.y, this.lineWidth / 2, 0, Math.PI * 2) this.ctx.fillStyle this.strokeStyle this.ctx.fill() } if (this.points.length) { this.strokes.push({ points: [...this.points], lineWidth: this.lineWidth, strokeStyle: this.strokeStyle }) } this.points [] this.emitChange() }3.4 撤销和清空把“每一笔”当成一个可回退的历史记录签错名字、写歪了用户往往想撤销上一笔而不是整个清空。为了实现这个我在组件里维护了一个strokes数组每一笔从pointerdown到pointerup之间采集到的所有点作为一个历史记录。undo() { if (!this.strokes.length) return this.strokes.pop() this.hasSign this.strokes.length 0 this.redrawAll() this.emitChange() }redrawAll的逻辑是先清空整块画布然后把strokes数组里剩余的每一笔用完整路径重新绘制一遍。因为strokes里存的是整笔的points数组重绘时可以用贝塞尔曲线完整还原而不是只画最后一段增量所以视觉上跟原笔迹几乎一致。redrawAll() { const canvas this.$refs.canvas const rect canvas.getBoundingClientRect() this.ctx.fillStyle this.backgroundColor this.ctx.fillRect(0, 0, rect.width, rect.height) this.strokes.forEach(stroke { this.drawFullStroke(stroke.points, stroke.lineWidth, stroke.strokeStyle) }) }这里有一个很容易被忽略的细节每次重设canvas的width和height或者清空画布后当前上下文的一些属性会被重置所以redrawAll里要重新设置fillStyle、lineWidth、strokeStyle等配置否则重绘出来的笔迹可能颜色不对或者线宽不对。4. 高清屏适配、容器尺寸和图片导出最容易出乱子的三个细节4.1 devicePixelRatio到底怎么影响签名清晰度很多人在普通电脑浏览器上测试签名组件一切正常到了手机上就发现字是糊的。这个问题十有八九出在devicePixelRatio设备像素比上。现在的手机屏幕基本都有2倍或3倍的物理像素密度。如果canvas的位图尺寸只等于CSS尺寸那么一个CSS像素里就要塞2到3个物理像素的绘制结果最终看起来就模糊。正确做法是把canvas的位图尺寸放大到CSS尺寸的dpr倍然后通过ctx.scale(dpr, dpr)把所有绘图坐标统一回CSS像素坐标系。这也就是我在前面initCanvas()里那两行代码的核心意义canvas.width rect.width * dpr canvas.height rect.height * dpr ctx.scale(dpr, dpr)注意ctx.scale(dpr, dpr)执行之后后续所有fillRect/arc/lineTo的坐标都按CSS像素来算你不需要在绘制逻辑里手动乘dpr。这一点搞明白了dpr相关的坑基本就避开了。顺带一提getImageData返回的像素数组是按位图实际尺寸排列的如果你做空画布检测或者像素级处理要记得用的是canvas.width而不是CSS宽度这一点也容易踩。4.2 容器尺寸变化ResizeObserver和Modal弹窗的“0尺寸”问题签名组件经常被放进Modal/弹窗里而弹窗通常是用v-if或者v-show控制的。如果弹窗还没渲染完成组件就在mounted里初始化画布canvas.getBoundingClientRect()拿到的长宽可能就是0。画布位图尺寸一旦设成0后面你画什么都没用。我的解决思路是分两层第一在initCanvas()里加一道保护拿到rect后先判断宽度和高度是否为0是的话直接跳过避免把canvas设成0尺寸。if (!rect.width || !rect.height) return第二引入ResizeObserver监听组件容器尺寸变化。当弹窗渲染完成、容器从0变成真实尺寸时ResizeObserver会触发回调重新执行initCanvas()。这样即使初始时机不对尺寸变成非0后也能自动补救。mounted() { this.initCanvas() if (window.ResizeObserver) { this.resizeObserver new ResizeObserver(() { this.initCanvas() }) this.resizeObserver.observe(this.$el) } }, beforeDestroy() { if (this.resizeObserver) { this.resizeObserver.disconnect() } }这里有一个细节要注意canvas.width重新赋值会清空画布内容所以initCanvas()执行完以后要调用redrawAll()把strokes数组里的历史笔迹重新画上去否则用户签了一半、窗口缩放一下签名就没了。如果项目支持的环境里没有ResizeObserver也可以在组件上暴露一个resize()方法给业务方让业务方在弹窗打开完成后手动调用。两种方式各有利弊我倾向于优先用ResizeObserver真机兼容性在目标环境里测过没问题。4.3 导出签名图片格式、质量和背景的取舍组件最终要输出一张图片我用的是Canvas标准的toDataURLgetSignature() { const canvas this.$refs.canvas return canvas.toDataURL(image/png) }这里有几个容易忽略的点image/png是无损格式签名是线条图形用PNG最合适放大不糊、边缘清晰。quality参数对PNG无效它只对image/jpeg有效。如果业务方明确要求JPG格式才能用canvas.toDataURL(image/jpeg, 0.9)但签名图里最好保留一条纯色线条否则JPG的压缩伪影会让字显得脏。导出的图片背景取决于canvas当时fillRect填充的颜色。我在组件里默认背景是白色这样导出的签名图就是白底黑字方便和合同/单据合成。如果需要透明背景可以把backgroundColor设置成同时跳过背景填充逻辑。还有一点关于“图片够不够清晰”的经验如果业务方说要拿签名图和身份证上的签名做比对导出尺寸不能太小。canvas位图尺寸已经乘了dpr所以默认输出的图片在手机上是够清晰的。要是还不放心可以在getSignature里提供一个放大导出的选项把canvas内容临时放大再导出来不过对于绝大多数签名场景dpr适配后已经足够。5. 踩坑实录识别率低、老WebView和滚动穿透5.1 手写签名识别率低不是识别库的问题是采集质量的问题在热搜词里看到“手写签名识别率低”我特别有感触。很多人以为识别率低是后端比对算法的问题实际上很多时候是采集端就没做好常见的问题有这么几个图片太糊没有做dpr适配或者导出时用了压缩率很高的JPG线条边缘全是噪点。笔画断裂没有做贝塞尔平滑快速书写时点与点之间是折线甚至有些点因为采样间隔过大直接断开。背景不干净签名区域受容器背景影响导出的图带了灰底、阴影或者渐变比对系统预处理时提取不到干净的笔迹轮廓。尺寸过小画布高度只有100px签名区域太小用户写着写着就出界最终成图不完整。那段时间我把组件改了三个地方一是全面适配devicePixelRatio二是引入中点贝塞尔平滑并过滤距离小于1像素的冗余点三是在导出时保证纯色背景。改完以后识别率相关的问题基本消失在沟通列表里。5.2 pointer事件兼容性兜底老WebView里的双轨事件现代浏览器对PointerEvent的支持已经很好了但如果你要兼容一些老安卓WebView或者特定政企环境里的浏览器就得做一层降级处理。我当时的降级策略是这样的const supportsPointer window.PointerEvent if (supportsPointer) { canvas.addEventListener(pointerdown, this.onPointerDown) canvas.addEventListener(pointermove, this.onPointerMove) canvas.addEventListener(pointerup, this.onPointerUp) } else { canvas.addEventListener(mousedown, this.onPointerDown) canvas.addEventListener(mousemove, this.onPointerMove) canvas.addEventListener(mouseup, this.onPointerUp) canvas.addEventListener(touchstart, this.onPointerDown) canvas.addEventListener(touchmove, this.onPointerMove) canvas.addEventListener(touchend, this.onPointerUp) }但双轨事件做起来有个容易翻车的地方鼠标事件里坐标直接取e.clientX/e.clientY触摸事件里坐标在e.touches[0].clientX里。我在getPos()里做了一层兼容判断把两种取值方式统一起来。这个方法对鼠标、触摸、PointerEvent都能正确处理。另外低版本浏览器对touch-action的支持不可靠光靠CSS可能挡不住页面滚动。可以在touchmove事件里显式调用e.preventDefault()但要注意注册事件时得传{ passive: false }否则浏览器会忽略preventDefault调用。5.3 滚动穿透与坐标偏移两个“看不见但很烦”的交互问题滚动穿透这个问题的现象是用户用手指在签名板上写字写几笔之后整个页面也跟着滚动签名区域的位置一变笔迹就歪了。解决办法是双管齐下第一在canvas的CSS里加上touch-action: none告诉浏览器不要处理这里的触摸手势。.signature-pad__canvas { display: block; width: 100%; height: 100%; touch-action: none; }第二在移动端事件里补一个e.preventDefault()兜底。如果只靠CSS某些老浏览器还是会穿透。坐标偏移的问题则出在页面发生滚动时。如果我用的是pageX/pageY那它本身是相对整个文档的坐标canvas在视口中的位置会因为滚动而变化画出来的笔迹坐标就乱了。所以我坚持用clientX/clientY - getBoundingClientRect()这套组合因为clientX/clientY是相对视口的而getBoundingClientRect()拿到的也是相对视口的矩形位置两者天然对齐滚动之后依然正确。这两个问题都不难解决但是都属于“不真机实测根本发现不了”的类型。我第一次在手机上测的时候页面滚动了整整一圈当时还以为是自己手抖写歪了后来才发现是滚动穿透在捣乱。6. 把组件放进业务里注册、调用、校验、上传一条龙6.1 从局部注册到第一行模板代码假设组件文件在src/components/SignaturePad.vue在业务页面里先引入并注册import SignaturePad from /components/SignaturePad.vue export default { components: { SignaturePad } }然后模板里直接使用signature-pad refsignPad v-modelform.signature :line-width2.5 stroke-style#333333 background-color#ffffff height200px /如果你有很多页面都要用也可以把它注册成全局组件import SignaturePad from ./components/SignaturePad.vue Vue.component(SignaturePad, SignaturePad)全局注册的代价是增加了一点首屏包体积但换来的是业务页面零成本接入。看团队偏好没有绝对的对错。6.2 提交前的空签名校验用户没签名就直接点提交是最常见的业务校验场景。组件内部维护了hasSign标志isEmpty()方法可以直接告诉业务方当前有没有有效签名submitForm() { if (this.$refs.signPad.isEmpty()) { this.$message.warning(请先完成签名) return } // 继续处理提交逻辑 }这个标志位在每次pointerdown时置为true在clear()和undo()后根据strokes数组重新计算。不用getImageData遍历像素的原因很简单遍历大画布的像素数组有性能损耗在低端手机上可能会卡顿维护一个状态标志位更轻量。6.3 Base64转Blob上传后端组件通过getSignature()返回的是base64字符串后端接口接收的通常是文件流。所以提交前需要把base64转成Blob再放到FormData里function dataURLtoBlob(dataUrl) { const arr dataUrl.split(,) const mime arr[0].match(/:(.*?);/)[1] const bstr atob(arr[1]) const n bstr.length const u8arr new Uint8Array(n) for (let i 0; i n; i) { u8arr[i] bstr.charCodeAt(i) } return new Blob([u8arr], { type: mime }) } async function uploadSignature() { const base64 this.$refs.signPad.getSignature() const blob dataURLtoBlob(base64) const formData new FormData() formData.append(signature, blob, sign_${Date.now()}.png) const res await axios.post(/api/upload/signature, formData, { headers: { Content-Type: multipart/form-data } }) console.log(res.data.url) }如果业务表单是JSON提交也可以考虑直接把base64字符串放在JSON字段里传给后端由后端去转存。两种方式都可行关键看后端接口约定。从数据量角度说普通签名图base64一般也就几十KB放JSON里不会造成太大压力但如果是长文签名或者大尺寸画布还是转Blob文件上传更稳。6.4 v-model与业务表单联动的注意事项我在组件里已经通过$emit(input, base64)把签名结果同步出去了所以业务侧可以直接用v-modelform.signature绑定签名数据。这个设计有几个省心的地方表单回显时你拿到的是base64字符串可以直接塞给img显示提交时不用额外调用getSignature()直接读form.signature就行清空画布时组件会emit一个空字符串表单里绑定的值也会同步清空。唯一要注意的是组件内部不会监听外部value的变化去主动清空画布因为画布状态和value的关系不是双向同步的画布是用户输入源value只是结果展示。如果业务方需要在某个时机强制清空画布调this.$refs.signPad.clear()才是正确方式而不是通过改value来触发。7. 一点关于封装尺度的个人心得写完这个组件又改了三轮之后我最大的体会是封装组件不是越通用越好而是越“够用且好用”越好。什么叫够用就是当前业务和可见的近未来需求能被覆盖。我的组件里做了撤销、清空、空值校验、v-model、dpr适配这些能力但没做笔锋模拟、压感、多模板背景这些花哨功能。不是做不到而是当前业务用不上提前做进去只会让组件的props和内部逻辑越来越复杂维护成本直线上升。什么叫好用就是接入方不需要看源码也能用得明白。他能用props改尺寸和颜色能用ref调用方法能通过v-model拿到结果这就够了。至于canvas里有多少坑、dpr怎么算那些是组件内部的事业务方不应该也不需要关心。最后再分享一个真机测试的建议签名组件一定要在真实手机浏览器或者App的WebView里测不要只在电脑的开发者工具里模拟。我在开发者工具里怎么测都没问题一上真机就暴露了坐标偏移、滚动穿透、模糊这三个问题。原因很简单模拟器里的触摸事件和PointerEvent行为和真实设备差异很大而签名恰恰是一个高度依赖触摸体验的功能。架好设备用手指好好写几个字比看十遍代码都管用。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AIGC智能体(本质、结构以及如何构建)培训P 2026/9/29 2:00:25

AIGC智能体(本质、结构以及如何构建)培训P

本 130 页 PPT 适配大模型、AI 智能体类咨询方案与企业内部 AI 培训课件。系统讲解 AIGC 智能体核心本质、任务环境分类、技术架构,拆解 LLM 智能体感知‑记忆‑规划‑执行完整工作闭环。覆盖不同类型智能体原理、构建方法,结合行业调研,梳理…

阅读更多 →
电缆损伤检测数据集 | 7800张YOLO电力巡检数据集 2026/9/29 2:00:25

电缆损伤检测数据集 | 7800张YOLO电力巡检数据集

电缆损伤检测数据集 | 7800张YOLO电力巡检数据集 适用于输电线路巡检、无人机巡线与目标检测研究 一、数据集概述 本数据集针对电缆常见损伤类型进行标注,共包含约7800张高质量标注图像,专为电缆表面损伤检测模型训练、验证与测试设计。电缆作为电力传…

阅读更多 →
判断与循环 2026/9/29 2:00:18

判断与循环

流程控制语句 1. 顺序结构 2. 分支语句 if 语句 if 语句的第一种格式: if (关系表达式) {语句体; }关系表达式为真则执行语句体,否则不执行。 注意事项: 大括号的开头可以另起一行书写,但建议写在第一行的末尾。在语句体中&#x…

阅读更多 →
ESP32-CAM图像传输实战:从硬件接线到M-JPEG流稳定推送 2026/9/29 2:00:18

ESP32-CAM图像传输实战:从硬件接线到M-JPEG流稳定推送

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
电机控制进阶:从直流电机到FOC的完整学习路径 2026/9/29 2:00:18

电机控制进阶:从直流电机到FOC的完整学习路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
工控系统安全:从PLC确定性原理到现场可执行的三层防御 2026/9/29 2:00:18

工控系统安全:从PLC确定性原理到现场可执行的三层防御

1. 这不是演习:当工厂的PLC突然“听不懂人话”,我们才真正看清工业控制系统安全的底牌“全球视野下的工业控制系统安全,15大震惊全球工业安全事件带来的警钟长鸣”——这个标题里藏着的不是危言耸听的新闻稿,而是过去二十年里&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉