ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue+Element UI实现封面配置中心:六个技术点的融合实战

Vue+Element UI实现封面配置中心:六个技术点的融合实战 做后台管理系统的人应该都有体会需求单越短暗坑越深。我前不久接到一个编号 128 的需求单内容是给运营做一个“封面配置中心”。需求描述只有三句话支持上传封面图、封面标签要去重、可以在封面上标记热点位置。结果一拆解自定义滑块、去重函数、Set 集合运算、Grid 网格布局、el-upload 上传封面、获取鼠标在盒子内的坐标六个技术点全挤在一个页面里而且没有一个是能简单照着文档抄完就交差的。这篇文章就是这次实战的完整复盘从需求拆解、技术选型到关键代码和踩坑记录都会写到。给同样在用 Vue 和 Element UI 做后台、并且正在跟这些前端细节死磕的朋友一个参考。整个方案不依赖复杂框架都是浏览器原生能力和轻量代码就能解决的场景理解了之后迁移到 React 或者其他 UI 库也不难。1. 需求拆解与整体方案设计1.1 128号需求单封面配置中心到底要做什么先说说这个需求单本身。运营侧的“封面配置中心”核心任务是管理所有内容的封面图文章封面、专题封面、推荐位配图都从这里产生。当时整理下来有五个明确动作上传封面运营要上传图片并且要能预览、能删除、能回显之前传过什么。打标签每张封面上传后要打分类标签比如“焦点图”“首页推荐”“专题头图”而且标签池是全局共享的历史标签要自动去重。热点标记运营要在封面上点一下记录某个位置。比如一张商品封面图上把商品本体的坐标位置存下来后续给前端做精确跳转用。预览与筛选封面列表要支持按标签筛选还要能通过一个滑块调节列表卡片的大小方便运营在“只看缩略图”和“看细节”之间切换。自适应布局列表本身要适配不同屏幕宽度不能出现固定列数在窄屏上挤成一团的问题。把这个需求单摊开看其实每个动作都对应了一个明确的技术点。上传打标对应 el-upload标签去重对应 Set 和去重函数热点标记对应鼠标坐标获取卡片大小调节对应自定义滑块自适应铺排对应 Grid 网格布局。需求本身没什么高深的业务逻辑但把六个技术点放在同一个页面里做集成各种细节问题就全冒出来了。我记得当时拿到需求后第一反应是列了一个“技术点-需求点对照表”每个需求都标注了建议实现方案和风险点。这个习惯是从之前几个后台项目里养成的很多页面看起来功能不复杂但一旦有交互联动牵一发动全身。先把需求拆透后面写代码才不会反复返工。1.2 技术选型为什么是 Set、Grid、el-upload 这一套组合确定方案的时候我其实犹豫过几个替代项最后都否决了这里分享下理由。自定义滑块没有引第三方轮子也没有直接用 el-slider。原因有两个一是设计稿里的滑块样式跟 Element UI 默认的差异很大要改到一致反而比手写更费劲二是这个滑块的业务语义有点特别——它不是调数值而是调封面卡片的缩放比例我需要完全控制样式和交互节奏。手写一个滑块其实只依赖 mousedown、mousemove、mouseup 和 getBoundingClientRect代码也就几十行可控性高得多。去重函数和 Set 组合核心考虑是不想为一个小功能引入 lodash。项目是存量后台工具链里有 lodash 但不是所有页面都引了为了 uniq 和 intersection 两个函数全量引入不值当。ES6 的 Set 原生支持集合运算的模拟配合展开运算符写出的去重函数可读性不比 lodash 差而且足够轻。Grid 布局是这几个 CSS 方案里最合适的。封面卡片列表是一个典型的二维网格场景Flex 做这种“多行多列且宽度自适应”的布局需要额外计算换行Grid 的 repeat(auto-fill, minmax(...)) 一行就能搞定。而且 Grid 对子元素的排序、对齐、区域命名都有原生支持写起来非常顺手。el-upload 就不用说了存量项目已经引了 Element UI直接用它封装好的上传组件最稳。真正的难点在于把 el-upload 的默认行为和业务逻辑接上。默认走 action 发请求的模式在我们这里根本用不了因为上传要经过自建的图片服务必须用 http-request 重写上传过程。鼠标坐标获取用 getBoundingClientRect 而不是 offsetX/offsetY是最稳妥的选择。后面会详细说原因这里先给结论offsetX 的参照对象是事件触发时的 target一旦鼠标落在子元素上坐标基准就乱了而 getBoundingClientRect 拿到的始终是目标盒子的边界稳定得多。这几个选型组合在一起整体方案就是用 Vue 2 加 Element UI 搭页面骨架CSS Grid 负责列表布局手写滑块利用 Set 做标签数据管理el-upload 做上传入口坐标采集单独抽一个工具函数。模块边界清楚后面每个点都方便单独调试。2. 关键技术原理与实现细节2.1 自定义滑块从 mousedown 到 mouseup 的手写实现自定义滑块的实现思路其实很朴素轨道是一个带相对定位的容器轨道内部有一个填充条和一个拖拽手柄两者都根据当前百分比计算宽度和 left 值。交互的核心是事件链路鼠标按下时记录状态鼠标移动时更新百分比鼠标抬起时解除状态。先看 HTML 和样式部分template div classcustom-slider refslider mousedownonMouseDown div classslider-track div classslider-fill :style{ width: percent % }/div /div div classslider-handle :style{ left: percent % }/div /div /template style scoped .custom-slider { position: relative; height: 24px; cursor: pointer; user-select: none; } .slider-track { position: absolute; top: 50%; left: 0; right: 0; height: 4px; transform: translateY(-50%); background: #e4e7ed; border-radius: 2px; } .slider-fill { height: 100%; background: #409eff; border-radius: 2px; } .slider-handle { position: absolute; top: 50%; width: 14px; height: 14px; margin-left: -7px; margin-top: -7px; border: 2px solid #409eff; background: #fff; border-radius: 50%; transition: box-shadow .2s; } /style注意几个细节handle 的 margin-left 和 margin-top 都是负的一半目的是让手柄真正的中心点对准当前百分比位置而不是左上角。user-select: none 避免拖动过程中选中页面文本导致体验发飘。这些细节直接影响滑块的手感。接下来的事件处理是关键。mousemove 和 mouseup 必须绑定到 document 上而不是绑定在滑块元素上。这个细节很多人会踩坑如果只绑定在滑块元素上鼠标拖拽一旦滑出滑块范围事件就断了滑块会停在某个位置不动。绑定到 document 后只要鼠标没有松开无论移动到页面哪个位置都能继续计算并更新。export default { name: CustomSlider, props: { value: { type: Number, default: 50 } }, data() { return { percent: this.value } }, methods: { onMouseDown(e) { e.preventDefault() document.addEventListener(mousemove, this.onMouseMove) document.addEventListener(mouseup, this.onMouseUp) // 按下时也立即计算一次让点击轨道可以跳转 this.onMouseMove(e) }, onMouseMove(e) { const rect this.$refs.slider.getBoundingClientRect() if (rect.width 0) return let percent ((e.clientX - rect.left) / rect.width) * 100 percent Math.max(0, Math.min(100, percent)) this.percent Math.round(percent * 10) / 10 }, onMouseUp() { document.removeEventListener(mousemove, this.onMouseMove) document.removeEventListener(mouseup, this.onMouseUp) this.$emit(input, this.percent) } } }为什么用 getBoundingClientRect 而不是事件对象自带的 offsetX因为 offsetX 表示鼠标相对于当前 target 的偏移量。当鼠标点在 handle 上时target 是 handleoffsetX 就是相对 handle 的坐标当鼠标点在轨道上时target 又变成了轨道。这样同一个 mousedown在不同元素的坐标基准完全不同计算出来的百分比自然就是错的。getBoundingClientRect 拿到的 rect 是滑块根元素在视口中的实际位置和尺寸e.clientX 也是视口坐标两者相减得到的差值就是鼠标距离滑块左边缘的真实距离再除以宽度归一化成百分比这在任何浏览器里结果都是一致的。这个方案还能顺便处理边界Math.max(0, Math.min(100, percent)) 把百分比强制限制在 0 到 100 之间不会出现拖过头变成负数的情况。这里还有一个很容易忽略的点滑块在 window resize 之后rect 会变化但滑块本身的位置也变化了所以每次 mousemove 都重新 getBoundingClientRect 是最稳妥的。性能上完全不需要担心一次 getBoundingClientRect 的开销远小于一次重绘mousemove 频率再高也扛得住。如果应用里有复杂动画可以在这基础上加个 requestAnimationFrame 做节流但大多数场景用不到。2.2 去重函数与 Set 集合运算标签池去重的正确写法Set 的第一个使用场景非常直接数组去重。运营从前端输入标签或者从接口拉取历史标签全汇到一个数组里去重可以用一行代码const uniqueTags [...new Set(tagList)]这里有个日常容易忽略的点Set 对 NaN 的处理。在 Set 内部使用的是 SameValueZero 算法所以 NaN 是等于 NaN 的去重时两个 NaN 会被当成同一个值。而对普通对象来说Set 比较的是引用地址不是深层结构所以两个内容相同但引用不同的对象在 Set 里就是两个不同的元素。如果标签不是基础类型数组而是对象数组比如标签对象自带 id 和 name要按 id 去重直接用 Set 就不行了。这种场景我习惯用 Map 来做function uniqueBy(arr, key) { const map new Map() arr.forEach(item { if (!map.has(item[key])) { map.set(item[key], item) } }) return [...map.values()] }原理很简单Map 的 key 可以是任何值用 item[key] 作为 Map 的键利用 Map 自身对 key 的去重特性保留第一次出现的对象。最终 [...map.values()] 把值拿出来转成数组。我们项目里标签池的需求不只是去重还要做集合运算。运营在配置页里可以多选标签来筛选封面选中的标签集合和全量标签集合之间需要计算交集、差集。用 Set 来做// 交集a 中同时存在于 b 的元素 function intersect(a, b) { const setB new Set(b) return [...new Set(a)].filter(item setB.has(item)) } // 并集a 和 b 的全部元素去重 function union(a, b) { return [...new Set([...a, ...b])] } // 差集a 中有、b 中没有的元素 function difference(a, b) { const setB new Set(b) return [...new Set(a)].filter(item !setB.has(item)) }收藏过这段代码的话你会发现所有运算都遵循同一个套路把数组转成 Set然后用 filter 或者展开运算去遍历判断。时间复杂度是 O(n m)比数组里用 indexOf 嵌套遍历的 O(n*m) 快一个量级。当标签数量只有几十上百个时差距不明显但如果是上万级的数据indexOf 方案会明显卡顿。还有一个和 Set 相关的经典坑JSON.stringify 一个 Set 实例只会得到空对象 {}。原因很简单Set 没有可枚举属性序列化时直接被忽略。所以如果要把去重后的数据交给接口保存或者塞进 localStorage一定先转数组[...set] 或者 Array.from(set)。标签池的完整业务逻辑我当时是这样写的接口返回一个全量标签数组运营新增标签时先本地合并到全量数组再用 uniqueBy 按标签名字段去重最后转回数组交给接口。前端展示用这个数组生成一个 el-checkbox 组合选中项传给筛选函数用 intersect 算出匹配的封面列表。整个过程不依赖后端额外接口纯前端就能完成。2.3 Grid 网格布局封面列表的自适应铺排方案封面列表要在一屏里展示多张图不同运营的屏幕宽度还不一样。Flex 布局做这个不是不行但需要手动 flex-wrap 再配合计算宽度碰到边距、换行、最后一行不满的情况处理起来非常啰嗦。Grid 布局在这个场景下是最自然的方案核心代码就一段.cover-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }这段代码的语义是每一行尽量多放列每列的最小宽度是 160px如果有剩余空间所有列等分。屏幕越宽列数自动越多屏幕太窄排不下一列时自动换行成更少列。gap 统一控制间距不用再给每个卡片配 margin。auto-fill 和 auto-fit 的区别值得单独说。两者都是“自动填充”但 auto-fill 在项目数量不够铺满一行时仍然会保留空的网格轨道轨道宽度按 minmax 定义auto-fit 则会自动折叠空轨道让已有的卡片撑满整行。如果想让一排只有一张大图时也占满宽度应该用 auto-fit。封面列表这里我用的是 auto-fill因为列表通常数量较多差别不大。Grid 还有个非常好用的特性是 grid-template-areas适合做页面内部区域的命名布局。我们的封面预览区就是用它来排的预览大图、标签列表、操作按钮三个区域可以按命名划分.cover-panel { display: grid; grid-template-columns: 2fr 1fr; grid-template-areas: preview tags preview actions; gap: 16px; } .preview-area { grid-area: preview; } .tags-area { grid-area: tags; } .actions-area { grid-area: actions; }这样在模板里每个子元素只要写一个 class 名就自动落到对应区域不需要反复写 grid-column / grid-row 的数值。当布局结构调整时只需要改 grid-template-areas 的字符串排列不用动子元素上的属性维护成本低很多。唯一的限制是 grid-template-areas 的区域必须是矩形不能出现 L 形划分这一点实测下来一般场景都够用。实现封面卡片内部布局时我也用了 Grid卡片左上角显示选中的勾右下角显示标签数量中间是封面图。用绝对定位也能实现但 Grid 配合 justify-items 和 align-items 控制别名会干净很多。这些布局代码不强依赖极端 hack浏览器支持也很广泛项目里可以放心用。2.4 el-upload 上传封面从文档配置到业务联动el-upload 是 Element UI 里功能完整度很高的组件默认支持选择文件、上传进度、删除、预览但这些默认行为都是围绕 action 指向后端接口的模式设计的。现实中后端上传往往要走独立的图片服务甚至是前端先传临时文件再调业务接口关联资源所以必须对上传行为做定制。最核心的一个配置是 http-request。只要传了这个函数el-upload 就不会走默认的 XMLHttpRequest 上传而是把这个函数当成“上传执行器”函数的入参是一个 options 对象里面包含文件、文件名、onProgress、onSuccess、onError 这些回调。我们在这个函数里写自己的上传逻辑el-upload action# :http-requesthandleUpload :before-uploadbeforeUpload :file-listcoverList :on-removehandleRemove acceptimage/* list-typepicture el-button typeprimary上传封面/el-button /el-uploadmethods: { beforeUpload(file) { const isImage file.type.indexOf(image) 0 if (!isImage) { this.$message.error(只能上传图片文件) return false } if (file.size / 1024 / 1024 2) { this.$message.error(封面图片不能超过 2MB) return false } return true }, async handleUpload(options) { const formData new FormData() formData.append(file, options.file) // 上传到自建图片服务 const res await uploadToImageService(formData) // 上传成功调用 onSuccess失败调用 onError options.onSuccess(res.data) // 业务联动把封面信息加入列表 this.coverList.push({ name: options.file.name, url: res.data.url }) }, handleRemove(file) { // 删除时同步移除列表项 } }几个容易踩的坑before-upload 返回 false 可以阻止本次上传但要在异步校验场景里需要直接用 async 函数返回 Promise比如先调接口检查图片是否重复上传。我建议上传之前加一个校验逻辑拿文件的 md5 或文件的宽高去接口比对避免运营重复传同一张图。action# 不能省虽然 http-request 接管了请求但 el-upload 内部对 action 是必填校验的不填会报错。把 action 设为一个无意义的 # 地址是常见的处理方式。自定义 http-request 模式下默认的 on-success 事件不会触发状态更新完全依赖你在 http-request 里手动调用 options.onSuccess。我当时在这个点上卡了半天上传成功后列表一直不刷新后来才发现回调要自己调。还有一个细节是 file-list 的受控问题。list-type 和 file-list 是展示态删除操作走 on-remove。如果你既想用组件自带的上传列表又想把封面数据同步到自己的业务数组里需要注意双向同步不要只维护 file-list 而忘记同步复制一份数据给后端保存。我们运营端的封面配置最终要存的是 url、名称、标签列表不是组件里的 file 对象所以数据模型要提前设计好。2.5 获取鼠标在盒子内的坐标归一化与热点标记热点标记这个需求听上去简单让用户点击封面图把点击位置记下来。但实际上手的时候遇到一个问题——拿到的坐标到底是以什么为参照的。第一反应是直接用事件的 offsetX 和 offsetY但它们依赖 target。如果封面上盖了一个透明的热点层或者预览图内部有子元素offsetX 就是相对那个子元素的坐标就完全错位了。更麻烦的是不同浏览器对 offsetX 的实现历史上存在差异虽然现代浏览器都支持了但为了兼容性最好别赌。我用的是 getBoundingClientRect 方案function getBoxPoint(event, box) { const rect box.getBoundingClientRect() const x event.clientX - rect.left const y event.clientY - rect.top // 归一化为 0~1 const nx rect.width 0 ? x / rect.width : 0 const ny rect.height 0 ? y / rect.height : 0 return { x, y, nx, ny } }换算过程很简单关键是为什么要归一化。运营在配置时看到的封面预览图和最终在用户端展示的封面尺寸几乎肯定不一样。如果把点击坐标记成绝对像素值预览图 800px 宽和线上 375px 宽对应的是完全不同的位置。只有把坐标转成“相对宽高的百分比”后续渲染时用 left: nx * 100% 和 top: ny * 100% 定位才能在不同尺寸的容器下指向同一个位置。这个方案还有一个实际应用热点标记渲染。在预览图上放一个绝对定位的标记点标记点的 left 和 top 用百分比值这样不管预览容器怎么变标记点都会落在同一位置。template div classpreview-box refpreviewBox clickhandleClick img :srccurrentCover.url / div v-for(dot, index) in dots :keyindex classhot-dot :style{ left: dot.nx * 100 %, top: dot.ny * 100 % } /div /div /template script methods: { handleClick(e) { if (!this.currentCover) return const box this.$refs.previewBox const point getBoxPoint(e, box) // 记录本次标记坐标 this.dots.push({ nx: Math.round(point.nx * 1000) / 1000, ny: Math.round(point.ny * 1000) / 1000 }) } } /script坐标保留三位小数就够用了大多数业务根本不需要精确到千分之一以下。如果要求用户在封面上只能标记一个点每次点击前先 clear 旧点或者加一个开关来控制是否允许继续添加这类产品逻辑按需调整即可。还有一个跟坐标采集相关的小坑预览容器如果有 padding 或 bordergetBoundingClientRect 返回的是包含 padding 和 border 的实际边界但图片本身是在 content 区域。一般预览容器为了显示干净不会加 padding加了的话需要额外减去 padding 值我建议直接用 content-box 来算样式上保证 box 没有额外内边距。3. 完整实操把五个技术点串成一个可用的页面3.1 页面骨架两栏布局与组件拆分在实际项目里我把整个页面拆成了三个子组件CoverUploader负责上传和预览、CoverSlider自定义滑块、CoverListGrid 封面列表。父组件负责持有封面数据和标签数据子组件只负责展示和抛事件。这样每个技术点都隔离在自己组件内部调试时打开一个组件就能定位问题。页面骨架用 Grid 搭一个大两栏布局.cover-page { display: grid; grid-template-columns: 420px 1fr; gap: 20px; }左侧 420px 是固定宽度操作区放上传和预览右侧是自适应封面列表区宽度由 Grid 的 1fr 自动分配。窄屏下这套布局会有点挤所以我加了一个媒体查询在宽度小于 1024px 时改成单列。组件拆分时需要注意 props 和事件的边界。CoverUploader 输入一个 currentCover 对象输出 upload 事件和 dot-add 事件CoverSlider 输入 value输出 inputCoverList 输入 sortedList 和 tagList输出 select 事件和 filter-change 事件。这样父子之间的数据流是单向的页面复杂度上升后也不会乱。3.2 上传与预览联动el-upload 到封面画布的流转流程是运营点击上传按钮 → 选图 → 组件显示缩略图列表 → 选中一张封面作为当前封面 → 当前封面展示在左侧预览画布上 → 运营在画布上点击添加热点标记。这个流程里el-upload 的上传结果不能只留在组件内部必须提升到父组件。我在 http-request 里上传成功后把新封面的信息 emit 给父组件父组件把它 push 到封面数组再设置 currentCover 指向这条新记录。因为 currentCover 是响应式的预览区会立刻刷新。上传封面的时候我还做了个额外的处理用 URL.createObjectURL 生成一个本地预览 URL用于在上传过程中先展示等接口返回正式 URL 后再替换。这个细节让整个页面在上传大图时不会白屏体验提升明显。3.3 标签去重与筛选Set 运算在前端的落地标签数据的流转是父组件从接口拿到全量标签数组经过 uniqueBy 去重后展示在左侧下方的标签区运营可以勾选标签。勾选变化时父组件用 intersect 计算当前选中标签和所有封面标签的交集把匹配的封面过滤后传给 CoverList。实际操作中封面和标签是多对多的关系一张封面有多个标签一个标签下有多张封面。筛选时我直接用 every/some 的逻辑一个封面满足“包含所有选中标签”才显示。这个用 Set 写起来很直观const selectedSet new Set(this.selectedTags) const filtered this.coverList.filter(cover { const coverTagSet new Set(cover.tags) return [...selectedSet].every(tag coverTagSet.has(tag)) })这里把 cover.tags 也转成 Set 是为了让 has 查询是 O(1) 而不是数组 indexOf 的 O(n)。虽然标签数量少性能提升不明显但写法更清晰。3.4 滑块调节与热点标记交互细节的串联滑块调节的对象是 CoverList 里的卡片尺寸。我没有让滑块直接改每张卡片的宽度而是通过 Grid 的 minmax 最小值来控制滑块值越大minmax 的最小值越大卡片显示越大每行显示数量越少。组件之间通过 v-model 同步滑块值父组件拿到后计算 gridStyle 对象动态绑定到列表容器上。computed: { gridStyle() { return { gridTemplateColumns: repeat(auto-fill, minmax(${this.coverSize}px, 1fr)) } } }这个方法比操作每张卡片的 width 优雅得多因为 Grid 会自动处理换行和多余空间分配。coverSize 的默认值是 160px滑块范围从 120 到 400。滑块在调节过程中会频繁触发 input 事件我在 computed 里绑定的样式本身就依赖响应式不需要额外节流但如果滑块值还用了昂贵的后续计算建议加个 debounce。热点标记那一块除了坐标采集还要考虑编辑体验。运营点出来的热点如果没有视觉反馈很容易点重复。我在画布上做了两个表现点击瞬间先加一个临时小圆点同时右侧显示坐标数值如果运营点击“清除标记”全部点移除。这样既满足了坐标采集也让运营知道自己的点击是有效的。4. 踩坑记录与经验总结4.1 高频问题速查表问题现象根本原因解决方案滑块拖出边界后手柄卡住mousemove 只绑在滑块元素上将 mousemove/mouseup 绑定到 document点击滑块轨道无反应只在 handle 上处理了 mousedown在根元素统一处理 mousedown按下即计算滑块百分比偶尔是负数鼠标跑到盒子左侧Math.max 兜底到 0对象数组用 Set 去重失败Set 按引用比较对象改用 Map 按字段去重JSON.stringify(Set) 得到空对象Set 无枚举属性先转数组再序列化el-upload 上传后列表不更新http-request 模式下 on-success 不会自动调用在手动执行 options.onSuccess上传时 action 为必填Element UI 内部校验填 action# 配合 http-request 使用offsetX 坐标不准参照对象是 target 而不是目标盒子用 getBoundingClientRect 计算热点标记在组件尺寸变化后偏移存了绝对像素坐标存归一化坐标渲染用百分比Grid auto-fill 最后一排留白auto-fill 保留空轨道根据产品需要换 auto-fit这张表里的每一条都是我在这个项目里真实遇到过的基本覆盖了同类页面的高频坑。保存下来下次直接查。4.2 几个容易被忽略的实战细节先说去重函数的边界情况。用 Map 按字段去重时如果 key 字段的值可能是 undefined 或 null会出现多条 undefined 记录只保留一条的意外。实际业务里我建议加一个判定item[key] null 时直接保留该条记录或者用 Symbol 之类的唯一 key 兜底避免误删数据。再说滑块组件的一个细节组件销毁时一定要移除 document 上的监听器否则会内存泄漏。在 Vue 2 里通过 beforeDestroy 钩子移除在 Vue 3 里是 onBeforeUnmount。这个问题在长列表页面尤其明显页面切走再切回来如果监听器没清干净可能出现多个滑块互相抢事件的情况。最后说坐标采集的性能。getBoundingClientRect 是同步的布局读取操作频繁调用会强制浏览器重新计算布局理论上会拖慢性能。但实际在热点标记场景里点击频率远低于滚动和拖拽直接调用完全没问题。只有在 mousemove 持续触发且每次都取 rect 的场景才需要优化我一般是把 rect 缓存在 mousedown 里mousemove 时复用除非容器尺寸会实时变动。这次项目的收获倒不是这几个 API 本身而是它们在同一个页面上组合时的边界处理。每个技术点单拆出来都很简单但真正开发时一半时间都在处理“两个技术点的交界处”滑块和 Grid 的联动、el-upload 和父组件状态同步、鼠标坐标和热点渲染的归一化……这些交界处才是前端工作里真正考验经验的地方。最后分享一个我自己的习惯遇到多技术点集成的页面先把每个技术点写成一个独立的小 demo跑通之后再往真实页面里拼。这样出了问题定位范围会小很多排查效率高得不是一点半点。希望这篇复盘对你有用。
返回列表