手上有个管理后台要加课程视频播放页需求写得轻描淡写——放个视频带进度条和倍速。真动手才发现原生video标签在真实业务里几乎处处受限m3u8 播不了、控制条样式各家浏览器长得不一样、清晰度切换要自己造轮子、播放失败连个像样的错误提示都拿不到。后来换成 video.js 重写了一遍才算把 Vue 里的视频播放这件事做扎实。这篇就聊我在 Vue 项目里集成 video.js 的完整过程组件怎么封装才不泄漏内存、播放源怎么按格式分流、控制条怎么改成贴合自家设计的样子、以及那些只在真机上才会冒出来的坑。前端初中级、正在给项目加视频模块的同学可以直接照着做踩过的坑我也都写进去了。1. 先想清楚为什么在 Vue 里还要额外引一个播放器1.1 原生 video 标签在业务里的四道坎很多人第一反应是不就是个 video 标签吗。单机播放本地 mp4确实够了。但只要业务稍微复杂一点问题就成串出现。第一道坎是流媒体格式。现在视频平台基本都用 HLS 切片分发地址后缀是 m3u8。桌面端 Chrome、Firefox 对 m3u8 支持非常有限你直接把 m3u8 塞进src控制台不会报错播放器就是一片黑error事件也可能不触发排查起来非常难受。要么引 hls.js 做 MSE 接管要么找个已经把这层包好的播放器。第二道坎是 UI 一致性。原生控制条长什么样完全由浏览器决定Safari 一套、Chrome 一套、安卓 WebView 又是另一套设计稿上的圆角进度条、悬浮控制栏基本没法还原。你可以加controls之外自己画但自己画就意味着进度拖拽、缓冲区间显示、音量、全屏、键盘快捷键这些全要自己实现工作量并不小。第三道坎是事件与状态。原生 video 的事件命名不统一loadedmetadata、canplay、canplaythrough的触发时机在不同浏览器上差异明显加载失败时error对象里的code含义也得自己查表翻译。要做加载中骨架重试按钮错误文案靠原生事件拼会比较脏。第四道坎是可扩展。倍速、画中画、字幕、截图、清晰度切换、播放进度记忆、埋点上报这些能力原生要么没有要么各浏览器支持程度不一。每一个都得单独做兼容散在业务代码里很快就失控。1.2 三个候选方案的取舍我为什么选 video.js当时摆在面前的选择有三个我简单对比了一下。方案优点短板适合场景原生 video 手写控制条零依赖、体积最小兼容成本高扩展全靠自己简单预览、短视频列表video.js生态成熟、控件体系完整、插件多、内置 HLS包体偏大、样式全局污染需处理中后台、教育、点播类业务轻量播放器库体积小、API 简洁自定义控件能力弱复杂需求要二次开发移动端 H5 播放页选 video.js 的核心原因是它把播放器抽象成了一个可以增删组件的对象而不是一个黑盒。player上有getChild、addChild、registerComponent这套东西意味着我要在控制条上加一个笔记按钮、要把倍速菜单改成中文标签、要在右下角挂一个自定义水印都不用改源码注册组件挂上去就行。这个可扩展性在业务迭代里价值很大。另一个现实考量是 HLS。video.js 从 7 开始内置了 VHSHTTP Streaming 模块m3u8 开箱可用不需要额外装 hls.js 再手动桥接。少一层依赖就少一类诡异问题。代价也要说清楚video.js 完整包压缩后依然不小如果你的页面只是偶尔弹个视频预览用它有点重这种情况我会建议按需动态加载别打进首屏。2. 装依赖和理清对象模型别急着写组件2.1 版本选择和安装现在主流是 8.xVue 3 项目直接装npm install video.js样式文件必须单独引这是新手最容易漏的一步——不引样式播放器能播但所有控件会堆在页面上看着像坏了import videojs from video.js import video.js/dist/video-js.css如果你用的是 Vue 28.x 一样能用API 没有本质差别只是组件写法换回 Options API 而已。版本上我建议锁死小版本video.js 在 7 到 8 之间改过一些控件类的命名跨大版本升级时自定义组件的className可能要跟着调。还有一个细节video.js/dist/video-js.css是全局样式里面定义了.video-js及其子元素的一大堆规则。它不一定和你的 UI 框架冲突但一定会在全局作用域里存在。后面第 5 节我会讲怎么把它约束住。2.2 player 到底是什么三个必须先搞懂的概念不搞懂这三个东西后面所有问题都会变成玄学。player 实例。videojs(el, options, readyCallback)返回的就是 player。它不是 DOM也不是 video 元素而是一个包装对象。调用player.play()、player.currentTime()、player.src()走的是它的方法底层才转给真正的video元素。tech技术层。player 底下挂着一个 techHTML5 环境下就是Html5tech它才是真正持有videoDOM 的那一层。要拿原生元素用player.el().querySelector(video)或player.tech().el()。截图、跨域设置、playsinline这类原生属性操作都得下沉到这层。组件树。控制条、大播放按钮、进度条、音量、字幕菜单在 video.js 里都是组件挂在 player 的 children 上。player.controlBar是控制条组件player.controlBar.playToggle是播放按钮。你要隐藏某个控件最干净的做法是player.controlBar.removeChild(PictureInPictureToggle)而不是用 CSSdisplay:none硬摁——CSS 摁掉后组件还在键盘 Tab 依然能聚焦到它无障碍上是个瑕疵。我见过最多的事故是把videojs()的返回值随手存在一个局部变量里组件卸载时没 dispose。结果路由来回切几次页面上看不见播放器内存里堆了五六个 player 实例每个都在后台跑定时器、监听 resize最后页面卡到爆。第 3 节会把销毁这件事写死。3. 封装一个真正能复用的 Vue 播放器组件3.1 模板结构和初始化时机模板部分我建议用原生 video 标签作为容器让 video.js 去接管它template div classplayer-wrapper :stylewrapperStyle video refvideoRef classvideo-js vjs-big-play-centered playsinline webkit-playsinline x5-playsinline /video /div /templatevjs-big-play-centered让大播放按钮居中不加的话它默认在左上角视觉上很怪。playsinline系列属性是为了让 iOS 和部分安卓 WebView 在小窗内播放而不是强制全屏——这个属性如果只在 JS 里设有时会被 tech 初始化覆盖所以模板上先写一遍更稳妥。初始化时机很关键。不要在setup的同步代码里调videojs()那时候 DOM 还没挂上。用onMounted加一层nextTickonMounted(() { nextTick(() { initPlayer() }) })nextTick在这里不是玄学保险而是有实际作用的如果你的播放器外面包了v-if、v-show或者处于transition动画中DOM 的实际尺寸在 mounted 那一刻可能还是 0video.js 会按 0 宽高算布局导致进度条挤成一团。多等一个 tick容器尺寸稳定了再初始化这类控件错位问题基本不会出现。3.2 props 设计把可变的东西都外置组件要复用就不能把源地址、封面、自动播放这些写死。我的 props 大概是这个样子const props defineProps({ src: { type: String, required: true }, type: { type: String, default: }, poster: { type: String, default: }, autoplay: { type: Boolean, default: false }, muted: { type: Boolean, default: false }, startTime: { type: Number, default: 0 }, options: { type: Object, default: () ({}) } })其中type留空是有意为之让组件自己去猜 MIME。原因很实际后端返回的播放地址经常带一堆 query 参数?signxxxt123你在调用处手写type很容易写错写错了 video.js 会走错解析分支表现就是一直转圈。猜类型比人靠谱。options这个 prop 是逃生舱。基础配置在组件里定好特殊页面要传特殊配置时直接在外部展开覆盖const player videojs(videoRef.value, { controls: true, preload: auto, fluid: true, aspectRatio: 16:9, playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 2], ...props.options }, () { emit(ready, player) })这里fluid: true配合aspectRatio: 16:9是个很实用的组合播放器宽度跟随父容器高度按比例自动算不用写一行响应式 CSS。注意fluid和fill是互斥的两个都开会出现高度计算打架画面被拉伸。3.3 生命周期与销毁内存泄漏的重灾区这段代码是整个组件里最不能省的部分onBeforeUnmount(() { if (player) { try { player.pause() } catch (e) { // 播放器可能已处于异常状态忽略 } player.dispose() player null } })dispose()会做这些事移除所有事件监听、清掉定时器、销毁组件树、把包装出来的 DOM 从页面上摘掉。少了它video.js 内部注册的resize监听、VHS 的缓冲轮询都不会停。注意dispose()之后不能再访问 player 的任何方法访问会直接抛错。所以我在调用后立刻置 null后续所有逻辑都加if (!player) return的守卫。还有一个坑值得单独说重复初始化。同一个 DOM 节点被videojs()调用两次第二次会拿到一个已经初始化过的元素行为不可预期常见表现是控制条渲染两份或者直接报错。稳妥的写法是先查再建function initPlayer() { if (!videoRef.value || player) return const existed videojs.getPlayer(videoRef.value) if (existed) { existed.dispose() } player videojs(videoRef.value, { /* ... */ }) }videojs.getPlayer()传 DOM 元素也能查这个用法文档里提得不多但在 KeepAlive 场景下救过我一次——页面被缓存后重新激活DOM 还在player 却被上一轮的销毁逻辑干掉了重新初始化时如果不先清残留播放器会处于半死状态。至于 KeepAlive我的建议是onActivated里检查 player 是否存在不存在就重建onDeactivated里暂停播放但不销毁。全销毁再重建的代价是每次切回来都要重新拉流体验很差。4. 播放源适配mp4、m3u8 和换源的正确姿势4.1 MIME 类型判断别靠肉眼前面说了让组件自己猜类型具体实现就是一个后缀映射function guessType(url) { if (!url) return video/mp4 const clean url.split(?)[0].split(#)[0].toLowerCase() if (clean.endsWith(.m3u8)) return application/x-mpegURL if (clean.endsWith(.mpd)) return application/dashxml if (clean.endsWith(.webm)) return video/webm if (clean.endsWith(.ogv)) return video/ogg if (clean.endsWith(.mov)) return video/quicktime return video/mp4 }先切掉 query 和 hash 再判断这一步很多人会漏。曾经有个接口返回的地址是xxx.mp4?authabc.m3u8这种形式不切参数就会误判成 HLS然后播放器一直卡在加载。设置源的标准写法player.src({ src: props.src, type: props.type || guessType(props.src) })也可以传数组video.js 会按顺序挑第一个能播的。多清晰度场景下如果不想自己做切换 UI直接把多个 source 塞进去浏览器会选它支持的格式——但注意这不会生成清晰度菜单只是格式兜底别混淆。4.2 换源保持进度一个必须处理的体验细节业务里换源太常见了切清晰度、切换下一集、用户手动选线路。直接player.src(newUrl)会导致进度清零、播放状态变回暂停。用户点了切高清结果回到第一秒这个体验基本等于 bug。我的处理模板是记录再恢复function switchSource(url, type) { if (!player || !url) return const resumeAt player.currentTime() || 0 const wasPlaying !player.paused() const rate player.playbackRate() player.src({ src: url, type: type || guessType(url) }) player.one(loadedmetadata, () { const duration player.duration() if (resumeAt 0 Number.isFinite(duration) resumeAt duration - 1) { player.currentTime(resumeAt) } player.playbackRate(rate) if (wasPlaying) { player.play().catch(() {}) } }) }几个点解释一下。用player.one而不是player.on避免每次换源都叠加一个监听。判断resumeAt duration - 1是为了防止源文件比原视频短时seek 到超出末尾导致直接触发ended。playbackRate也要恢复因为部分浏览器在换源后会重置成 1。play()返回 Promise被自动播放策略拦截时会 reject必须 catch否则控制台一片红。提示Number.isFinite(duration)这个判断不能省。直播流或者元数据还没就绪时duration()返回的是Infinity或NaN直接比较会得到意料之外的结果。4.3 直播流的特殊处理如果源是直播有两个地方要改。一是控制条上不要显示进度条和总时长二是缓冲策略要调。隐藏进度条用组件移除的方式player.ready(() { const bar player.controlBar bar.removeChild(ProgressControl) bar.removeChild(RemainingTimeDisplay) })缓冲策略在html5.vhs里配html5: { vhs: { overrideNative: true, maxPlaylistRetries: 3, liveRangeSafeTimeDelta: 0.5 } }overrideNative: true的意思是强制用 VHS 接管而不是让浏览器走原生 HLS 播放。这个开关在 Safari 上尤其重要——Safari 原生支持 HLS会绕过 VHS导致你基于 VHS 做的清晰度切换、错误处理全部失效。代价是 MSE 的性能略逊于原生需要自己权衡。5. 控制条改造让播放器长得像自家产品5.1 样式覆盖的正确层级video.js 的默认皮肤是深色半透明如果产品是浅色风格直接改会很别扭。有两种改法。一种是覆盖 CSS注意作用域。在 Vue 的 scoped style 里必须用深度选择器否则选择器打不进 video.js 内部生成的 DOM.player-wrapper :deep(.video-js) { width: 100%; font-family: inherit; } .player-wrapper :deep(.vjs-control-bar) { background: linear-gradient(transparent, rgba(0, 0, 0, 0.65)); height: 44px; } .player-wrapper :deep(.vjs-big-play-button) { width: 64px; height: 64px; line-height: 64px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.5); margin-left: -32px; margin-top: -32px; }大播放按钮那个margin-left/margin-top必须跟宽高联动用百分比写会错位。这是很常见的翻车点改了宽高忘了改负 margin按钮就跑偏到右下角去了。另一种是换主题类。video.js 生态里有几套官方主题包装完之后在播放器容器上加对应的类名即可适合不想手写一大堆 CSS 的项目。不过主题包能改的有限品牌色特别鲜艳的话还是得自己覆盖。5.2 注册自定义控件产品经理常见的需求是控制条上加个截图按钮或者加个记笔记。video.js 的组件注册就是为这个设计的const Component videojs.getComponent(Component) const Button videojs.getComponent(Button) class SnapshotButton extends Button { constructor(player, options) { super(player, options) this.controlText(截图) } buildCSSClass() { return vjs-snapshot-control vjs-control vjs-button } handleClick() { this.player_.trigger(snapshot) } } videojs.registerComponent(SnapshotButton, SnapshotButton)然后在初始化时插进控制条player.ready(() { player.controlBar.addChild(SnapshotButton, {}, player.controlBar.children().length - 2) })addChild的第三个参数是位置索引。放在倒数第二位是因为最后一位通常留给全屏按钮截图插在它前面视觉上更顺。这个索引别写死成数字用children().length动态算更稳。截图本身要注意跨域。如果视频源在另一个域名浏览器出于安全策略会把 canvas 标记为污染toDataURL直接抛错。解决办法是给 video 元素设crossOrigin anonymous同时服务端必须返回允许跨域的响应头两者缺一不可player.ready(() { const videoEl player.el().querySelector(video) videoEl.crossOrigin anonymous })这个属性和src的加载顺序有讲究必须在设置 src 之前设否则第一次加载不会带上 CORS 头截图依然失败。5.3 倍速菜单中文化playbackRates传到 2.0 之后菜单里显示的是 2x 这种英文格式。要做中文可以在播放器 ready 后遍历菜单项改文本player.ready(() { const menu player.controlBar.getChild(PlaybackRateMenuButton) if (!menu || !menu.items) return menu.items.forEach((item) { if (item.el_) { const rate parseFloat(item.label_ || item.options_.label) item.el_.textContent ${rate}倍速 } }) })这段代码依赖内部属性跨大版本升级时有失效风险所以务必加空值守卫。更稳的做法是自己写一个菜单按钮组件完全掌控渲染代价是要多写几十行。6. 问题排查实录这些坑我都踩过6.1 初始化阶段的典型故障现象高概率原因处理方式控件全部堆在页面上没有样式没引 video-js.css补上样式导入报错找不到vjs_getProperty同一节点重复初始化先getPlayer判断再 dispose页面报window is not definedSSR 环境直接 import改动态 import 或客户端渲染播放器宽度正常高度为 0容器初始尺寸为 0nextTick 后再初始化或给容器定高大播放按钮跑到角落改了宽高没同步负 margin联动调整 margin多个播放器互相干扰容器 id 重复取消手写 id交给 video.js 生成SSR 这条单独说一下。如果项目用服务端渲染import videojs from video.js在 Node 环境执行时就会碰window页面直接 500。处理办法是延迟到客户端let player null onMounted(async () { const { default: videojs } await import(video.js) await import(video.js/dist/video-js.css) player videojs(videoRef.value, { /* ... */ }) })动态导入同时解决了首屏体积问题一举两得。6.2 播放阶段的排查顺序播放失败时不要瞎试按这个顺序查效率最高。第一步看player.error()。在error事件里打出来player.on(error, () { const err player.error() console.warn(播放器错误, err err.code, err err.message) })错误码含义1 是加载被中止2 是网络错误3 是解码失败4 是格式不支持。遇到 4先确认 MIME 类型猜对了没有遇到 2去看是不是签名过期或者跨域被拦。第二步看网络面板。HLS 的话会有 m3u8 索引请求加一堆 ts 分片请求如果索引请求返回 403那是鉴权问题跟播放器没关系如果索引返回 200 但分片全挂大概率是 CDN 的防盗链没放行。第三步看自动播放策略。现在浏览器普遍禁止带声音的自动播放autoplay: true加muted: false的组合在大多数场景下会被静默拦截。表现是画面停在首帧控制台有一条 play() failed 警告。解法是先静音再播player.muted(true) player.play().catch(() { player.muted(false) })先静音满足策略播起来之后如果用户有交互再取消静音。这个套路在弹窗型播放场景里基本是标配。6.3 移动端专属的几个坑iOS 上全屏行为不受控。用player.requestFullscreen()在 iOS 上会走原生全屏此时你的自定义控件全部失效控制权交回系统。如果产品要求全屏也要显示自定义控件得做伪全屏——把播放器容器position: fixed铺满视口然后自己加退出按钮。安卓 WebView 上X5 内核需要x5-playsinline属性才能真正内联播放。另外部分机型在视频开始播放后会触发页面重绘导致滚动位置跳变处理方式是把播放器容器设成独立层加transform: translateZ(0)提升合成层。还有一点移动端timeupdate的触发频率比桌面端低用它驱动进度条会出现一顿一顿的更新。要做丝滑进度条用requestAnimationFrame在播放中主动读currentTime()暂停时停止循环。这个改动不大但对观感提升很明显。7. 工程化收尾体积、多实例和监控7.1 按需加载与多实例管理video.js 不适合无脑打进首屏。如果视频模块只是详情页里的一个 tab用动态 import 把播放器拆成独立 chunk用户点开视频再加载首屏能省下好几百 KB。多实例的场景要格外小心。列表页里每个卡片一个预览播放器十几个实例同时初始化再加上每个都在预加载带宽和内存都会炸。我的做法是单实例复用只保留一个播放器实例切换卡片时用switchSource换地址而不是销毁重建。这样既省资源又不会出现十几个 player 同时在后台跑缓冲。如果确实需要多实例给每个实例一个独立的容器 id 或者干脆都不写 id同时限制同时预加载的数量超过就设置preload: none。7.2 埋点与错误上报播放相关的数据对业务很有价值起播耗时、卡顿次数、完播率。这几个都能从事件里拿。起播耗时是play事件触发时的时间戳减去初始化时间戳。卡顿用waiting事件计数每触发一次加一。完播看ended。播放失败上报error事件里的 code 和 message同时带上 src 的前半段别带完整签名方便定位是哪个源出问题。有个细节容易忽略用户频繁拖动进度条会短时间内触发大量seeking事件直接往上报接口打会把服务打崩。加个节流或者只在seeked时上报一次最终位置。我在实际用下来最大的体会是video.js 这类库的价值不在于它替你把播放做完了而在于它把播放器拆成了可组装的零件。控件要改、行为要改、数据要采都能找到挂载点不用去动源码。真正花时间的从来不是初始化那几行代码而是销毁时机、换源恢复、跨域截图这些边角而这些恰恰是文档里最不会细说的部分。要是你的项目里视频模块不止一处早点把播放器封成一个带 props 和完整生命周期的组件后面每次加需求都是在调参数而不是重写。
企业数字化 ERP 产品动态
相关推荐
Paperclip协议:AI Agent的数据授权与上下文注入标准 1. 项目概述:Paperclip 不是回形针,而是一个被严重误读的 AI 工程实践入口“Paperclip”这个词在中文技术社区里,最近半年几乎成了一个幽灵式热词——它频繁出现在掘金、知乎、V2EX 的前端面试讨论帖里,和 React 面经、Node.js 安… · 2026/11/3 4:46:51
Python填空题高频陷阱:从id到默认参数的51道题解析 简介:这份 Python 练习题库文档面向正在学习 Python 基础语法、准备课程测验或面试复习的初学者与进阶者,帮助读者通过填空与代码题快速检验对核心知识点的掌握程度。内容围绕标准库 math、None 空类型、列表/元组/字符串等有序序列展开,并覆… · 2026/11/3 4:46:51
PPT截图改不动?图转PPT实测:图片还原成可编辑源文件 周五下午四点半,项目群弹出一条消息:“周一上午汇报,PPT 周一早上八点前发我。”你打开素材文件夹,里面是三十多张截图——系统架构图、性能压测表、监控大屏、上周评审会拍的白板照片,还有客户发来的一份“参考版式”… · 2026/11/3 4:46:51
2026年网络安全趋势与从业者能力转型 1. 网络安全行业的现状与挑战网络安全行业近年来经历了爆炸式增长,但同时也面临着前所未有的挑战。随着数字化转型加速,网络攻击手段日益复杂化,从传统的病毒、木马发展到现在的APT攻击、供应链攻击、AI驱动的自动化攻击等。攻击者不再只是单… · 2026/11/3 8:20:06
孔明锁笼中取宝拆装教程:三通原理与步骤详解 孔明锁里最让人又爱又恨的一类,就是“笼中取宝”。第一次拿到手的时候,大多数人都会经历同一个心理过程:这玩意儿看着明明是个规整的立方体,六根木条互相咬合,中间还卡着一颗珠子,晃一晃能听见响࿰… · 2026/11/3 8:20:06
Ubuntu 18.04.5桌面版amd64 ISO下载校验与安装排障 简介:这份docx文档面向需要获取Ubuntu 18.04.5桌面版amd64镜像的Linux学习者与开发者,整理了该长期支持版本的镜像基本信息、适用平台与获取线索,适合用于虚拟机环境搭建、旧版本兼容性测试或入门体验。压缩包内仅含1个docx文件,大… · 2026/11/3 8:20:06
DeepSeek从入门到精通:模型选型与提示语工程实战 简介:这份由清华大学新闻与传播学院新媒体研究中心元宇宙文化实验室出品的《DeepSeek从入门到精通》PDF讲义,面向希望系统掌握大模型应用的学习者、开发者与内容从业者,帮助读者从基础认知走向实际使用。压缩包仅含1个PDF文件,约4… · 2026/11/3 8:20:06
AgentScope多智能体开发框架:从模型封装到RAG服务化与Java落地 1. AgentScope到底是什么:一个多智能体开发框架能帮我解决什么问题先交代一下背景。AgentScope这个名字,可能你已经在开源社区刷到过很多次了。我在实际项目里测过不少Agent开发框架,从早期的单模型封装到自己维护一套任务调度,多… · 2026/11/3 8:20:06
旧2.5寸硬盘别扔!用USB3.0硬盘盒和转接口低成本复活指南 老笔记本退役之后,拆下来的那块2.5寸硬盘往往是最尴尬的存在——容量不大不小,扔了可惜,留着又没地方插。我手上就攒了好几块这种“鸡肋盘”,有早年机械盘,也有后来换下来的SATA固态。真正让它们重新干活的转折点&… · 2026/11/3 8:19:06
双极步进电机控制实战: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/3 6:04:24
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/3 5:52:46
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/3 6:04:24
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/3 5:52:46