怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

实习级这一档是干什么的 SKU 矩阵编辑器、营销规则试算、权限体系这些核心台面,实习生大概率碰不到。这一档收的是后台里实习生真的会被分派、但仍然有明确技术含量的活。下面三个模块彼此独立——你可能只做过其中一个,那就只写那一个,不用凑齐三个。
怎么讲才不显得 low,这是这一档最要紧的事 「我接了一个富文本编辑器」和「运营从 Word 里粘贴的内容带着一堆内联样式和 base64 图片,存进去几百 KB、C 端详情页直接卡,所以我做了粘贴白名单清洗和图片转存」——同一件事,后者面试官会顺着追问,而顺着追问恰好是你能答的。前端这一档尤其容易被小看,破解办法是把「用户实际怎么用」讲出来:运营会粘贴、会传 20 张图、会在筛选完之后把链接发给同事,这些真实行为才是需求的来源。
三条自检 一、能说出不这么做会怎样(不清洗粘贴内容 C 端会样式错乱、筛选态不同步链接发出去就是空白页、图表不做空态就是一片空白让人以为坏了);二、能说出你踩过的具体坑(哪次 XSS 被安全扫出来、哪次筛选条件一刷新就没了被运营骂);三、能说出量级(详情内容多大、列表多少条筛选项、图表多少数据点)。三条都有就能写,缺一条先补。
项目背景设定 电商运营后台,Vue 3 + TypeScript + Element Plus + Pinia + Vite。使用者是商品运营和数据运营,日常在这套后台里发商品、查数据。这一页收的是后台里最高频的三块交互,不是核心配置台。
为什么这三块值得写 它们的技术含量都不在「把组件接进来」,而在处理真实用户的真实行为:运营会从 Word 粘贴、会把筛选后的链接发给同事、会在数据为空时怀疑系统坏了。「接入」是一天的活,「治理」是这三个模块真正的工作量,也是面试里能讲出深度的部分。

模块一:商品详情富文本编辑器

  1. 商品详情富文本编辑器(粘贴白名单清洗 + 图片转存 + 服务端二次过滤 + 内容体积治理)★★★
    简历这样写 商品详情富文本编辑(Vue 3 + 富文本编辑器 + 粘贴清洗 + 图片转存 + 体积校验):接入后真正的工作量在治理运营粘贴进来的内容——从 Word 与网页粘贴会带入大量内联样式、外链图片与 base64 数据,导致 C 端详情页样式错乱且体积过大;实现基于白名单的粘贴清洗(只保留约定的标签与属性,其余一律剥离),图片统一拦截并转存到自有存储后替换地址,base64 图片在粘贴时即转为上传;XSS 上明确前端清洗只为体验、服务端过滤才是防护,两侧共用同一份白名单配置;保存前做内容体积与图片数量校验并给出定位提示,避免详情页在 C 端弱网下拖慢首屏。上线后 C 端详情页的样式错乱反馈不再出现,单个详情内容体积从数百 KB 量级降到几十 KB 量级
    展开完整拆解
    为什么要这么设计

    需求听起来很简单:商品详情要能富文本编辑。选个成熟的编辑器接进来,一天就能跑通。但真正的问题在上线之后才出现,而且每一个都不是编辑器本身的问题,是「运营实际怎么用它」的问题。

    一是从 Word 粘贴进来的内容把 C 端搞乱了。运营的商品文案都在 Word 里写好,直接复制粘贴。粘进来带着一大堆内联样式——固定的字号、固定的宽度、微软雅黑字体、甚至 mso- 开头的 Office 私有属性。在后台的编辑器里看着还行,到了 C 端手机上就是灾难:文字溢出屏幕、字号一会儿大一会儿小、图片撑破容器。

    二是图片是外链或者 base64。从别的网站复制的内容,图片地址是别人的域名——对方哪天删了图,我们的商品详情就是一片裂图,而且还有防盗链的问题。从 Word 粘贴的图片更糟,是 base64 内联在 HTML 里,一张图就能让内容体积涨几百 KB

    三是安全扫描报了存储型 XSS。富文本天然允许 HTML,而我一开始以为「编辑器会帮我处理」。安全同学用一个带 onerror 的图片标签直接弹了框,而且因为内容是存库的、会展示给所有访客,这是存储型的,比反射型严重。

    四是详情内容太大拖慢 C 端首屏。有个商品的详情 HTML 有一百多 KB,里面有二十几张没压缩的大图。C 端在弱网下打开这个详情页要好几秒,而后台编辑时完全感觉不到问题——因为办公网很快。

    所以四个改动:粘贴时按白名单清洗(不是等保存时才处理,粘贴那一刻就要干净)、图片统一拦截转存(外链和 base64 都转成自有地址)、服务端做二次过滤(前端清洗只是体验,防护必须在服务端)、保存前校验体积和图片数量并给出可定位的提示。

    这个模块最想说的一句话是:接入编辑器是一天的活,治理它产出的内容是这个需求真正的工作量。而这些治理需求,全都来自「运营实际会怎么用」——他会粘贴、会从别处复制、会传大图。把用户的真实行为想清楚,需求才完整。

    整体链路
    粘贴(拦在入口,不等保存时再补救) │ ├─ 拦截 paste 事件,取 HTML 与纯文本两份 │ ├─ 白名单清洗(白名单而不是黑名单) │ 标签白名单:p / br / strong / em / ul / ol / li / img / a / table… │ 属性白名单:img 只留 src alt / a 只留 href target │ style 一律剥离,只保留少数约定的(如文本对齐) │ 黑名单必然漏,攻击方式会变,白名单是唯一可靠的方向 │ ├─ 特殊来源额外处理 │ Word:剥 mso- 私有属性、去掉 之类的私有标签 │ 网页:去掉 class(对方的样式在我们这里没有意义) │ └─ 提供「粘贴为纯文本」快捷入口 运营只要文字时,这个比清洗更彻底也更省事 图片(三种来源,统一转存) │ ├─ 本地选择上传 → 校验类型与大小 → 压缩 → 上传 → 插入自有地址 ├─ 粘贴 base64 → 粘贴时即转 Blob → 走同一条上传链路 └─ 粘贴外链地址 → 服务端代抓取并转存 → 替换为自有地址 抓取失败则移除该图并提示,不留外链 外链的问题:对方删图就裂、可能有防盗链、可能被换成别的内容 编辑期 │ ├─ 实时显示内容体积与图片数量(让运营自己看到代价) │ ├─ 移动端预览(按手机宽度渲染) │ 后台是宽屏,不预览就发现不了溢出问题 │ └─ 草稿自动保存到本地,防误关页面 保存(三道校验) │ ├─ 1 再清洗一遍(编辑过程中可能通过其他途径引入脏内容) ├─ 2 体积上限 + 图片数量上限 → 超限拒绝并定位到第几张图 └─ 3 提交服务端 服务端(真正的防护层) │ ├─ 用与前端同一份白名单配置再过滤一次 │ 前端清洗只为体验,接口可以被直接调用,防护必须在服务端 │ └─ 落库存清洗后的结果,不存原始输入 C 端渲染 ├─ 富文本容器统一样式重置(图片 max-width、表格滚动、字号继承) └─ 图片懒加载 + 指定尺寸,避免布局跳动
    分步拆解
    1. 清洗必须用白名单,不能用黑名单。黑名单(去掉 script、去掉 onerror)永远漏——攻击方式会变,编码方式会变。白名单的逻辑是「只有明确允许的标签和属性才能留下,其余全剥」,这样新出现的攻击方式默认就被挡了。这一条是这个模块最重要的原则。
    2. 清洗要在粘贴那一刻做,不要等保存。粘贴时清洗,运营立刻看到干净的内容,所见即所得;等保存时才处理,他会发现「我排好的版怎么变了」,来回沟通成本很高。
    3. Word 粘贴要额外处理私有属性和标签。mso- 前缀的样式、<o:p> 这类命名空间标签,通用清洗库不一定覆盖。而 Word 恰好是运营最常用的来源,所以这块要专门测。
    4. 提供「粘贴为纯文本」的入口。很多时候运营只要文字,格式他会在编辑器里重新排。这个入口比任何清洗都彻底,而且实现成本几乎为零,是性价比最高的一条。
    5. 三种来源的图片都要转存到自有存储。本地上传的正常走上传;粘贴的 base64 要在粘贴时转成 Blob 走上传(不能留在 HTML 里);粘贴的外链要服务端代抓取转存。外链不转存的三个风险:对方删图就裂、可能有防盗链、以及对方可能把图换成别的内容——最后一个最隐蔽也最危险。
    6. 转存失败要有明确行为,不能静默留着。抓取失败就移除那张图并提示运营「第几张图片获取失败,请手动上传」。静默保留外链等于埋了一个定时炸弹。
    7. 上传前压缩,但要保留必要清晰度。商品详情图运营会用来展示细节,压得太狠会被投诉。做法是限制最大边长和目标体积,而不是固定压缩比,并且给运营一个「原图上传」的选项用于确实需要高清的场景。
    8. 服务端必须再过一遍,这一条没有商量。前端清洗只解决体验,接口可以被绕过前端直接调用。而且要和前端共用同一份白名单配置(比如从一个共享的 JSON 生成两端的规则),否则两边不一致——前端允许的服务端剥掉了,运营会觉得功能有 bug。
    9. 实时显示内容体积和图片数量。让运营自己看到「这个详情已经 80 KB 了」。把代价可视化,比事后拒绝要好——他会自己删掉几张图,而不是保存时被拦下来一头懵。
    10. 体积超限的提示要能定位。不要只说「内容过大」,要说「共 23 张图片,其中第 5、9、17 张超过 500 KB」。可定位的错误提示是这类功能好不好用的分水岭。
    11. 必须做移动端预览。后台是宽屏,运营在这里看不出任何问题;C 端是手机,宽度只有几百像素。不给预览,溢出和字号问题只能等 C 端反馈。实现只是一个固定宽度的 iframe 或容器,成本很低。
    12. C 端渲染容器要统一样式重置。图片 max-width: 100%、表格外层加横向滚动、字号用相对单位继承。这一条是后台清洗之外的兜底——即使有脏样式漏过来,容器层能压住大部分溢出问题。
    关键决策与取舍

    用成熟编辑器而不是自己造,但清洗逻辑自己写。编辑器自己造是明显的过度设计(光是光标和选区处理就是个深坑)。但清洗规则不能完全依赖编辑器自带的——各家编辑器的默认清洗策略不同,而且它们的目标是「尽量保留用户格式」,我们的目标是「只保留我们能渲染好的格式」,目标不一致。所以清洗这一层我们自己实现并自己维护白名单。

    白名单定得偏严,接受运营的抱怨。比如不允许自定义字号和颜色——运营一开始很不适应,觉得「我想强调一下都不行」。但允许自定义字号的代价是 C 端字号完全失控,而且不同商品的详情看起来风格不统一。折中方案是提供几个预设样式(小标题、重点、提示块),运营选样式而不是选字号。这样既满足了表达需求,又保证了 C 端可控。这个取舍的原则是:给受控的选择,而不是给自由的属性。

    外链图片选择服务端代抓取,而不是直接拒绝粘贴。直接拒绝更简单也更安全,但运营从供应商网站复制商品介绍是真实且高频的工作流,堵死它他会去手动一张张下载再上传,效率极低。代抓取的代价是要处理抓取失败、超时、防盗链、以及潜在的 SSRF 风险(服务端要限制可抓取的域名和协议、禁止内网地址)。SSRF 这一点我最初没想到,是安全评审时被指出的,这也是为什么这个功能必须由服务端做而不是前端直接 fetch。

    踩过的坑一:以为编辑器会处理 XSS,安全扫描直接弹框。用一个带 onerror 的图片标签就绕过了。更值得说的是我当时的第一反应是错的——我在前端加了一层过滤就以为修好了。安全同学直接用接口调试工具绕过前端提交,还是能存进去。这件事让我理解了「前端校验和服务端校验的职责完全不同」:前端是为了体验(让用户立刻知道不行),服务端是为了防护(因为它是唯一无法绕过的关口)。这个认知后来在做任何表单校验时都在用。

    踩过的坑二:清洗把运营排好的表格弄没了。白名单初版忘了加 table 相关标签,运营辛苦排的规格参数表粘进来全变成了纯文本。他找过来的时候我才意识到白名单的风险是「漏掉合法的东西」,而这类问题不会有报错,只会有用户抱怨。修法之外的教训是:白名单要和实际使用场景对齐,不能凭想象列——我后来的做法是先收集一批运营真实粘贴的内容,统计里面出现的标签,再定白名单。

    踩过的坑三:base64 图片在粘贴时没转,保存时才发现内容超限。运营粘贴了一段带十几张图的 Word 内容,编辑器里显示正常,点保存时被体积校验拦下,而他压根不知道问题在哪、也无法删除那些图(在他看来图片就是内容的一部分)。修法是把转存提到粘贴那一刻,粘贴时就显示上传进度。这一条印证了「拦在入口」这个原则:越早处理,用户的心智负担越小。

    没做的部分:没做协同编辑和版本历史。商品详情通常一个人编辑,冲突概率低,做协同是过度设计。但做了一个轻量的替代:保存时如果检测到内容被别人改过(比对版本号),提示并给出对比,而不是直接覆盖。这是用很小的成本覆盖了主要风险。

    数字是怎么测的

    内容体积的对比:取改造前一批真实的商品详情,统计 HTML 字节数的分布(中位数和最大值),改造后同样统计。我的口径是「从数百 KB 量级降到几十 KB 量级」而不是精确数字,因为它高度依赖商品类型(服装详情图多、标品图少)。说量级并说明样本来源,比给一个精确平均值可信——精确值一被追问「你怎么统计的、样本多少个」就容易说不清。

    体积下降的归因要拆开说。下降来自三处:base64 转成外链(这一项贡献最大)、内联样式被剥离、图片压缩。能拆开说明你知道钱花在哪,而不是笼统地说「优化了体积」。

    清洗效果的验证用真实样本回归。收集一批运营真实粘贴过的内容(Word、网页、其他后台),建成固定的测试样本集,每次改白名单都跑一遍,断言输出里不含被禁的标签和属性、且合法标签没有被误删。后半句是关键——我踩过表格被清掉的坑,所以正反两面都要断言。

    XSS 的验证不用统计,用固定的攻击样本集。收集常见的注入形式(事件属性、javascript: 协议、编码绕过、SVG 内嵌脚本),断言清洗后都无害。而且要在服务端接口层面测,不能只测前端——我踩过的坑就是只测了前端。这类用例进 CI 不允许失败。

    C 端样式错乱「不再出现」怎么说:这是个可核对的事实而不是指标——用反馈工单数从有到无来陈述,并说明改造前是每周几例。不要编一个「样式正确率 99%」,那个数字没有测量方法。

    不要报什么:不要报「彻底防住了 XSS」——白名单能挡住已知形式,但安全是持续的事。该说的是「用白名单而不是黑名单、且服务端是防护主体」这个设计,机制比结论可信。也不要报「压缩率 80%」,压缩率取决于原图质量。

    面试追问
    Q:富文本不就是接个编辑器吗,你觉得难点在哪? A:接入确实是一天的活,难点全在治理它产出的内容,而这些需求来自「运营实际怎么用」。三个具体的:一是粘贴——运营的文案在 Word 里,粘进来带一堆内联样式和 mso- 私有属性,后台看着没事,C 端手机上文字溢出、字号错乱。二是图片——粘贴的图可能是 base64(一张就几百 KB)或者外链(对方删图我们就裂,而且对方可以把图换成别的内容)。三是安全——富文本天然允许 HTML,这是存储型 XSS 的天然入口,而且内容会展示给所有访客。这三个问题里没有一个是编辑器能替我解决的,因为编辑器的目标是「尽量保留用户的格式」,我的目标是「只保留我能渲染好的格式」,目标不一致。想清楚这一点之后,工作量就从「选个编辑器」变成了「定一套内容规范并在两端强制它」。
    Q:为什么一定要白名单?黑名单把 script 和 on 开头的属性去掉不就行了? A:因为黑名单要求你穷举所有攻击方式,而攻击方式是会变的。举几个黑名单容易漏的:javascript: 协议放在 href 里、SVG 里内嵌脚本、style 里用表达式、大小写混写、HTML 实体编码绕过、以及各种畸形标签让解析器产生意外结果。你每次都是在补上一次被发现的漏洞,永远落后一步。白名单反过来——只有明确列出的标签和属性能留下,新出现的攻击方式默认就被挡了,因为它不在白名单里。代价是白名单可能漏掉合法的东西(我踩过表格被清掉的坑),但这类错误是用户能立刻发现并反馈的,而黑名单漏掉的攻击是悄无声息的。两种错误的可发现性完全不对称,这是选白名单的根本理由。
    Q:前端已经清洗过了,服务端为什么还要再来一遍?重复工作。 A:因为前端清洗只解决体验,服务端过滤才是防护——这两件事的目标不同,不是重复。前端清洗的价值是让运营立刻看到干净的内容(所见即所得),服务端过滤的价值是它是唯一无法绕过的关口。我踩过这个坑:在前端加了过滤就以为修好了,安全同学直接用接口调试工具绕过前端提交,脏内容照样进库。任何「客户端做的校验」在攻击者眼里都不存在,因为客户端代码在他手上。实现上为了避免两边规则不一致(前端允许的服务端剥掉了,运营会以为是 bug),我们把白名单抽成一份共享配置,两端从同一份生成规则。这个认知可以推广到所有校验:前端校验是为了用户,服务端校验是为了系统,缺一不可但职责不能混。

模块二:列表筛选态与 URL 同步

  1. 列表筛选态与 URL 同步(筛选序列化到地址栏 + 返回保留现场 + 与分页排序的联动语义 + 默认值不入参)★★
    简历这样写 后台列表页的筛选态管理(Vue 3 + Vue Router + Pinia + query 序列化):把筛选、排序、分页统一序列化到路由 query,使筛选结果的链接可分享、可收藏、刷新不丢(原先运营把链接发给同事打开是空白列表);明确三者的联动语义——改筛选条件重置到第一页、改排序不重置、切页只改页码,避免「筛完之后还停在第 5 页导致看似无数据」;默认值不写入 query 以保持链接简短且默认值可演进;区分「从详情返回」保留现场(含滚动位置)与「从菜单重新进入」清空两种语义;筛选项过多时对长参数做压缩并保留可读的主要条件。改造后运营反馈的「刷新后筛选条件丢失」问题不再出现,跨人协作时可直接传链接对齐同一份数据视图。
    展开完整拆解
    为什么要这么设计

    第一版的筛选态放在组件的 data 里,最直觉的写法。功能上没问题,但运营用了两周提了四个问题,每一个都来自我没预料到的真实使用方式

    一是链接发出去是空白的。运营筛出「近 7 天、待发货、金额大于 500」的订单,想让同事一起看,直接复制地址栏发过去。同事打开看到的是默认的全量列表。他们的解决办法是在群里打字描述「你筛一下…」——这件事每天发生,而它本来只需要一个链接。

    二是刷新就丢。运营筛好条件、翻到第 3 页、点开一个订单处理完,回到列表,所有条件都没了要重新选一遍。他一天要处理几十单,这个成本是累积的。

    三是筛完之后看似没数据。他在第 5 页,改了筛选条件,结果新条件只有 2 页数据,而页码还停在 5,列表显示空。他以为是没有符合条件的订单,实际是有的。这个 bug 被报成「筛选功能坏了」。

    四是重置按钮的行为和他的预期不符。他理解的「重置」是回到刚进页面的样子(有个默认的时间范围),而我实现的是清空所有条件。清空之后查询范围变成全量,接口直接慢了下来。

    四个问题的根源是同一个:我把筛选态当成了「组件的内部状态」,而它实际上是「用户当前正在看的数据视图」——这是一个应该被地址栏表达的东西。

    所以核心改动是把筛选、排序、分页统一序列化到路由 query,地址栏成为唯一的状态来源。这一个改动同时解决了前两个问题:链接自带条件所以可分享、刷新时从 query 恢复所以不丢。第三个问题靠明确三者的联动语义解决,第四个靠把「重置」定义清楚。

    这个模块最想说的一句话是:需求不是「实现筛选功能」,是「让运营能把他看到的东西传给别人」。前者一天就做完了,后者才是真实的痛点,而它只有在观察真实使用行为之后才会浮现。

    整体链路
    状态的唯一来源是路由 query(不是组件内部 data) │ ├─ 进入页面 → 从 query 反序列化出筛选 / 排序 / 分页 → 发请求 │ 刷新、分享链接、浏览器前进后退,走的都是这一条路径 │ └─ 用户操作 → 更新 query(replace 或 push)→ 监听 query 变化重新请求 不要「既改内部状态又改 query」,那会有两份真相 序列化规则 │ ├─ 默认值不写入 query │ 好处一:链接短、可读、能看出用户实际筛了什么 │ 好处二:默认值以后可以改,旧链接语义不会跟着变 │ ├─ 类型要能还原:数字 / 布尔 / 数组 / 日期区间 │ query 里全是字符串,反序列化必须按字段声明的类型转 │ 最常见的 bug:页码取出来是 "2" 字符串,参与计算变成拼接 │ └─ 参数过长时压缩,但保留主要条件可读 多选了几十个类目会让 URL 很长 折中:主要条件保持明文,超长的部分压缩成一个短参数 三者的联动语义(这一段必须显式定义,不能靠默认行为) │ ├─ 改筛选条件 → 重置到第 1 页,保留排序 │ 不重置就会出现「新条件只有 2 页,页码停在 5,列表空」 │ ├─ 改排序 → 不重置页码(用户想在当前页换个看法) │ 这一条和上一条相反,是有意的:排序不改变结果集大小 │ ├─ 切换页码 → 只改页码,其余不动 │ └─ 重置按钮 → 回到「刚进页面的默认态」而不是清空所有 清空会让查询范围变成全量,接口可能直接慢下来 要给默认值(如默认近 7 天),并在按钮文案上写清是「重置」 历史记录策略(决定前进后退的手感) │ ├─ 筛选与排序变化 → replace(不产生历史记录) │ 否则用户点一次返回只是退回上一个筛选条件,点几十次才出得去 │ └─ 切换页码 → 也用 replace,同理 两种「进入列表」的语义要分开 │ ├─ 从详情页返回 → 保留现场:筛选 + 分页 + 滚动位置 │ 实现:路由 query 已经带了条件,滚动位置另存并在恢复后回滚 │ └─ 从左侧菜单点进来 → 回到默认态 同一个路由,但用户意图不同:前者是「回来继续」,后者是「重新开始」 判据:看来源(菜单点击时显式带一个重置标记) 配套 ├─ 导出按钮带上当前筛选条件(导出的就是他看到的) ├─ 空结果要区分「无数据」与「条件太窄」,后者给出放宽建议 └─ 请求带序号,返回时丢弃过期响应(快速改条件会有竞态)
    分步拆解
    1. 状态的唯一来源必须是 query,不能既存组件内部又存 query。两份真相一定会不一致——最典型的表现是「点浏览器返回,地址栏变了但列表没变」。做法是把 query 当输入、监听它的变化去请求,组件里只保留表单的临时输入值(用户正在输入但还没点查询的那部分)。
    2. 默认值不写进 query,这一条收益比看起来大。链接短、可读(一眼看出他实际筛了什么),而且默认值以后可以改而不影响旧链接的语义——如果默认的「近 7 天」写进了 URL,以后想改成「近 30 天」,所有旧链接还是 7 天,会造成困惑。
    3. 反序列化必须按字段声明的类型转换。query 里全是字符串。最常见的 bug 是页码取出来是 "2",参与加法变成 "21"。做法是给每个筛选字段声明类型(数字、布尔、数组、日期区间),序列化和反序列化都走这份声明。
    4. 数组和日期区间要定好格式并保持稳定。数组用逗号分隔还是重复参数、日期用什么格式,定下来就不要改——格式一改,用户收藏的旧链接就全废了
    5. 「改筛选重置页码」和「改排序不重置」这两条相反的规则要显式实现。判据是会不会改变结果集的大小:筛选会(所以页码可能越界),排序不会(所以停在当前页是合理的)。这个区分不做,就会出现那个被报成「筛选功能坏了」的空列表 bug。
    6. 兜底:如果请求回来发现页码超出总页数,自动回到最后一页并提示。因为数据是动态的,用户收藏的链接里的页码可能已经越界了。不能让他看到一个空列表还以为没数据。
    7. 历史记录用 replace 而不是 push。如果每次改筛选都 push 一条历史,用户点返回只是退回上一个筛选条件,要点几十次才能真正离开这个页面。这是很影响手感的一个细节。
    8. 「从详情返回」和「从菜单进入」要分开处理,这是这个模块最容易被忽略的一条。同一个路由,但用户意图完全不同:从详情返回是「回来继续处理下一单」,希望现场原样;从菜单点进来是「重新开始一件事」,希望默认态。实现上是菜单点击时显式带一个重置标记,而不是靠猜。
    9. 滚动位置要单独保存,query 里不放它。它不是数据视图的一部分(分享链接时不该带滚动位置)。存在内存或 sessionStorage 里,按路由加筛选条件做键,恢复时要等列表渲染完再回滚,否则内容还没撑起高度,滚不到位。
    10. 重置的语义要定义清楚并和产品对齐。回到默认态还是清空全部?我们选回到默认态,因为清空会让查询范围变成全量,接口可能直接慢下来甚至超时。如果确实需要「查全部」,那应该是一个显式的选项而不是重置的副作用。
    11. 导出要带上当前筛选条件。运营的心智是「导出我现在看到的这些」。如果导出的是全量,他拿到文件才发现不对,白等一轮异步任务。
    12. 快速改条件会有请求竞态,要给请求编号。用户连续改了三次筛选,三个请求并发,返回顺序不一定。做法是每次请求带自增序号,返回时只接受最新那个,其余丢弃——不然会出现「列表显示的是上一次的条件的结果」。
    13. 空结果要区分两种情况。「这个条件下确实没数据」和「条件太窄」的提示应该不同,后者要给出可操作的建议(放宽时间范围、去掉某个条件)。一个只显示「暂无数据」的空态,会让运营怀疑是系统坏了。
    关键决策与取舍

    状态放 query 而不是放全局 store,这是这个模块的核心决定。放 store 也能解决「刷新丢失」(配持久化插件),而且不用处理序列化。但它解决不了「链接可分享」——而这恰恰是运营最痛的那个需求。放 query 的代价是要写序列化和反序列化、要处理类型转换、URL 可能很长。判据是「这个状态需不需要被别人看到」:需要就必须进 URL,因为只有 URL 是可传递的。反过来,滚动位置这类不需要传递的状态就不该进 query。

    默认值不入参,代价是「链接里看不出完整条件」。有人会觉得这是缺点——打开一个链接,不知道时间范围是什么。但实际上默认值是产品定义的、所有人一致的,写出来是冗余信息;而且写进去之后默认值就被冻住了,改产品默认值会让所有旧链接的语义偷偷变化。我认为「默认值可演进」这个收益大于「链接自解释」的损失,而且界面上筛选表单会显示当前的实际值,用户看界面就知道。

    参数压缩只对超长部分做,不对整个 query 做。整个压缩成一个 ?q=xxxx 最简单,但链接完全不可读了——运营和开发都没法一眼看出这个链接筛的是什么,排查问题时很难受。折中是主要条件(时间、状态)保持明文,多选类目这种可能很长的压缩成短参数。这个取舍偏向了可维护性而不是极简。

    踩过的坑一:页码没重置导致的空列表被报成 bug,而我第一反应是去查接口。运营说「筛选功能坏了,筛出来是空的」,我拿他的条件调接口,有数据。反复沟通了很久才发现他当时在第 5 页。这件事的教训不只是「要重置页码」,更是「用户描述的现象和真实原因往往差很远」——所以后来我们在空列表的提示里加上了当前的完整条件(含页码),让现象自己说话。这个改动让后续同类问题的沟通成本大幅下降。

    踩过的坑二:用了 push 导致返回键失效。运营调了七八次筛选条件,想点返回回到上一个页面,结果点了十几次都还在这个列表里。他以为浏览器卡了。改成 replace 之后正常。这个坑说明「每次状态变化都记一条历史」听起来合理,实际上会破坏浏览器最基本的交互——历史记录应该对应「用户认为的页面切换」,而不是对应「状态变化」。

    踩过的坑三:滚动位置恢复得太早,滚不到位。从详情返回,恢复了筛选条件、发请求、然后立刻 scrollTo——但此时列表还没渲染,页面高度不够,滚动被截断在最大高度。修法是等数据渲染完(nextTick 之后,必要时等图片占位撑起高度)再恢复。这类「时序问题」在前端很常见,判据是「我依赖的东西此刻真的存在了吗」。

    没做的部分:没做筛选条件的保存与命名(「我的常用筛选」)。运营提过这个需求,确实有价值,但它需要后端存储和管理界面,超出了当时的排期。临时替代方案是告诉运营「把链接收藏到浏览器书签」——因为筛选态已经在 URL 里了,这个替代方案零成本,而且是前面那个设计带来的意外收益。这一点在面试里值得提,它说明一个好的设计会自然长出别的能力。

    数字是怎么测的

    「刷新后筛选丢失」这类问题的验证用断言型用例,不是统计。构造场景:设置一批筛选条件、刷新页面,断言恢复后的请求参数与刷新前完全一致。同类用例还有:改筛选断言页码回到 1、改排序断言页码不变、从详情返回断言条件和滚动位置都在、从菜单进入断言回到默认态。这五条是这个模块的核心行为,全部可以写成自动化断言,报法是「行为用例全部通过」而不是编百分比。

    竞态的验证要构造慢请求。用工具把第一个请求延迟几秒,然后立刻改条件发第二个请求,断言最终列表显示的是第二个请求的结果。不构造延迟是测不出竞态的(本地网络太快,返回顺序恰好正确)。这一点要说明,否则「我测过竞态」不可信。

    URL 长度:典型场景和最坏场景的字符数(比如常规筛选几十个字符、全选类目时压缩后多少)。要说清为什么关心这个——部分浏览器和服务端网关对 URL 长度有限制,超了会直接失败。报最坏场景比报平均值有意义,因为出问题的是最坏情况。

    「可传链接对齐视图」的效果不好量化,用可核对的事实陈述。比如「运营群里从原来打字描述筛选条件,变成直接发链接」——这是行为的改变,可以观察到。不要编一个「协作效率提升 N%」。

    不要报什么:不要报「性能优化了多少」——这个模块本身不是性能优化,它的价值在正确性和可用性。硬凑性能数字反而会暴露你不清楚自己做的是什么。该报的是行为用例的覆盖和几个具体问题的消失。

    面试追问
    Q:筛选态放 Pinia 加持久化也能做到刷新不丢,为什么非要放 URL? A:因为 store 持久化解决不了最痛的那个需求——链接可分享。运营的真实工作流是「我筛出这批异常订单,发给同事一起处理」,store 里的状态是他本地的,发出去别人看不到。判据我总结成一句:这个状态需不需要被别人看到,需要就必须进 URL,因为只有 URL 是可传递的。反过来滚动位置就不该进 URL——它不是数据视图的一部分,分享链接时带着别人的滚动位置很奇怪。所以我的分法是:能定义「我在看什么数据」的进 query,只影响「我怎么看」的放本地。另外放 URL 还有两个附带收益:浏览器前进后退天然可用,以及运营可以把链接收藏成书签当作「我的常用筛选」——后者本来是个要排期的需求,因为这个设计免费拿到了。
    Q:改筛选重置页码,改排序不重置,这个区别有什么依据?会不会太随意? A:依据是这个操作会不会改变结果集的大小。筛选会——新条件下可能只有 2 页,而用户停在第 5 页,那一页压根不存在,列表就是空的。排序不会——结果集还是那些数据,只是顺序变了,用户在第 3 页换个排序,他的意图通常是「在同样的位置看另一种排法」,把他弹回第 1 页反而是打断。这个判据我认为是稳的,不是随意定的。另外还要有一道兜底:即使按规则重置了,数据也可能在此期间变化(用户收藏的旧链接页码越界),所以请求返回后如果发现页码超出总页数,要自动回到最后一页并提示,而不是显示一个空列表让他以为没数据。我们那个被报成「筛选功能坏了」的 bug 就是这个现象。
    Q:这个功能看起来就是几个 query 参数,你觉得值得写进简历吗? A:值得,但要写对角度。如果写成「实现了列表页筛选功能」,那不值得——那是任何人都会的。值得的部分是「我发现筛选态不是组件的内部状态,而是用户正在看的数据视图,所以它应该被地址栏表达」这个认知转变,以及由它推出的一系列具体决定:默认值不入参(让默认值可演进)、用 replace 不用 push(不破坏返回键)、筛选重置页码而排序不重置(依据是会不会改变结果集大小)、区分从详情返回和从菜单进入(同一路由但用户意图不同)。这些决定每一个都有明确理由,而且都是从运营的真实抱怨里长出来的——链接发出去是空白的、刷新就丢、筛完看似没数据。我认为实习经历里最值钱的不是做了多难的东西,是能证明你会从用户的实际行为里找到真问题。这个模块恰好能证明这一点。

模块三:运营数据看板

  1. 运营数据看板(多图表统一取数编排 + 四种数据状态显式化 + 空值与零值区分 + 按需引入控体积)★★
    简历这样写 运营数据看板(Vue 3 + 图表库按需引入 + 统一取数编排 + 状态机化的空态处理):一个筛选条件驱动多个图表,改为统一编排取数而非各图表自行请求(原先改一次条件并发多个请求,部分失败时看板处于「有的图是新条件、有的是旧条件」的不一致状态);把每个图表的加载中 / 空数据 / 部分缺失 / 请求失败四种状态显式化,且严格区分「无数据」与「数值为零」(缺失日期不补零,补零会造出一个不存在的事实,让运营误读为业务归零);图表库按图表类型按需引入并做路由级懒加载以控制包体积;提供导出为图片满足运营把图放进周报的真实诉求。改造后看板的不一致状态不再出现,首屏包体积因按需引入明显下降。
    展开完整拆解
    为什么要这么设计

    看板的第一版是最直觉的写法:页面上放六个图表组件,每个组件自己 watch 筛选条件、自己请求、自己渲染。跑通很快,问题也来得很快。

    一是看板会处于不一致状态。运营改一次时间范围,六个图表并发发六个请求。其中一个失败了或者慢了,结果是有的图显示的是新时间范围的数据、有的还是旧的,而界面上完全看不出区别。运营对着这个看板做判断,得出的结论是错的。这是最严重的问题——它不是「不好用」,是「会给出错误的信息」。

    二是空数据被当成了业务归零。后端返回的日期序列里,没有订单的那天压根不返回。我为了让折线图连续,在前端把缺失的日期补成了 0。运营看到某天的曲线掉到 0,以为出了事故,紧急拉群。实际情况是那天的数据还没同步完。「没有数据」和「数据是零」是两件完全不同的事,我把它们混成了一件。

    三是空态一片空白让人以为坏了。筛选条件比较窄的时候图表区域什么都没有,运营的反应是刷新页面、然后来问「看板是不是挂了」。一个没有空态设计的图表,和一个加载失败的图表在视觉上是一样的。

    四是包体积。图表库整包引入,首屏 JS 明显变大,而这个看板只有数据运营会用,其他人打开后台也要下载这些代码。

    所以四个改动:取数统一编排(一次筛选变化对应一次完整的数据更新,要么都是新的要么都不动)、四种数据状态显式化并区分空值与零值按需引入加路由懒加载、以及顺带加上运营真正需要的导出图片

    这个模块最想说的一句话是:图表的难点不在画图,在于「数据不完整的时候显示什么」。画图是图表库的事,一行配置;而加载中、无数据、部分缺失、请求失败这四种状态各自该显示什么、以及怎么不让用户误读,这才是要自己想清楚的部分。而我踩的最大的坑(补零)恰好就是在这上面判断错了。

    整体链路
    取数:一个筛选条件 → 一次统一编排(不是每个图各自请求) │ ├─ 筛选变化 → 生成一个批次号 → 编排层统一发起本批次的全部请求 │ ├─ 能合并的接口合并(同维度的多个指标一次取回) │ 减少往返,也天然保证了这几个图的数据同源 │ ├─ 不能合并的并行发起,但都带同一个批次号 │ ├─ 返回时校验批次号:不是当前批次的一律丢弃 │ 解决竞态:连续改三次条件,只接受最后一次的结果 │ └─ 全部返回或超时后统一提交渲染 关键:不允许出现「部分图是新数据、部分是旧数据」 某个图失败 → 该图单独显示失败态,但它显示的绝不是旧数据 四种数据状态,每种都要有明确表现 │ ├─ 加载中 骨架图或占位,保持容器高度(避免布局跳动) │ ├─ 空数据 图形区留白 + 一句可操作的说明 │ 要区分「这个条件下确实没数据」与「条件太窄」 │ 后者给放宽建议,不要只显示「暂无数据」 │ ├─ 部分缺失 已有的照常画,缺失区间用断线或灰底标出 │ 绝不补零 —— 补零会造出一个不存在的事实 │ └─ 请求失败 显式的失败态 + 重试按钮(只重试这一个图) 失败态必须和空态在视觉上明显不同 空值 vs 零值(这一条是这个模块最容易做错的地方) │ ├─ 后端返回的序列必须能表达三种情况: │ 有值且为 0 → 画在 0 的位置,是真实的业务事实 │ 无值(缺失) → 断线或灰底,标注「无数据」 │ 有值但不可信 → 单独标记(比如当天还没跑完的统计) │ └─ 前端不允许把 null 当 0 处理 这不是显示问题,是会让人做出错误决策的正确性问题 体积 ├─ 图表库按用到的图表类型按需引入,不整包引 ├─ 看板路由级懒加载(只有进这个页才下载) └─ 大数据量图表做采样,不把上万个点全交给图表库 导出与协作 ├─ 导出为图片(运营要放进周报,这是真实高频诉求) ├─ 导出的图上带筛选条件与生成时间(否则周报里的图无法自证口径) └─ 筛选条件同步到 URL,看板链接可分享(复用模块二那套) 长时运行 ├─ 组件卸载时销毁图表实例并移除 resize 监听(否则内存持续增长) └─ 自动刷新要能关闭,且刷新时不打断用户正在做的交互
    分步拆解
    1. 取数必须统一编排,不能让每个图表组件自己请求。这一条是这个模块最重要的改动。判据是「这几个图表是不是在描述同一个数据视图」——是的话它们就必须同批次更新,不能各自为政。各自请求的后果不是慢,是看板会显示互相矛盾的信息
    2. 用批次号解决竞态。每次筛选变化生成一个批次号,请求带上它,返回时校验——不是当前批次的直接丢弃。连续改三次条件时,只有最后一次的结果会被渲染。
    3. 某个图失败时,它显示失败态,而不是继续显示旧数据。继续显示旧数据是最危险的行为——界面上看不出这个图和旁边的图不是同一个时间范围的。宁可这个图是空的,也不要它是错的。
    4. 能合并的接口要合并。同一个维度下的多个指标(订单数、金额、客单价)一次取回。除了少一次往返,更重要的是这几个数天然同源,不会出现「订单数是新的、金额是旧的」。
    5. 四种状态每一种都要有明确的视觉表现,而且失败态和空态必须区分。两者在实现上都是「没有图形」,但用户需要知道是「没有数据」还是「拿不到数据」——前者他会去调条件,后者他会去重试或者找开发。给错了信号就会给错行动。
    6. 加载中要保持容器高度。否则数据回来时页面高度突变,用户正在看的位置被顶走。用骨架图占位是最省事的做法。
    7. 绝不把缺失值补成零,这是我踩过最有价值的坑。补零让折线图好看了,但它造出了一个不存在的事实——运营看到曲线掉到 0,以为业务出了事故。正确做法是断线或者用灰底标出「无数据」区间,让「不知道」这个状态被如实表达。
    8. 后端返回的数据结构必须能表达「有值为 0」「无值」两种情况。如果后端直接不返回缺失的日期,前端无法区分。这需要和后端约定接口——返回完整的日期序列,缺失的位置给 null 而不是省略或者 0。这一条是前端要主动提的接口需求。
    9. 「今天」的数据要单独标记。当天的统计通常还没跑完,数值会一直变化且偏低。不标记的话运营每天早上都会以为业务下滑了。做法是给当天的数据点加个标记和说明。
    10. 空态的文案要可操作。不要只写「暂无数据」,要写「当前条件下没有数据,可以尝试放宽时间范围」。可操作的空态和不可操作的空态,用户的下一步行为完全不同。
    11. 图表库按需引入,看板路由懒加载。整包引入会让所有打开后台的人都下载这些代码,而看板只有数据运营用。按需引入的代价是要了解图表库的模块结构,一次性成本;收益是持续的。
    12. 大数据量要采样。把几万个点全交给图表库,渲染会卡而且视觉上也看不出区别(屏幕就那么宽)。采样要在前端做还是后端做取决于数据量——量特别大时应该让后端按需要的粒度聚合,而不是传全量下来再采样。
    13. 导出图片要带上筛选条件和生成时间。运营把图截进周报,如果图上没有口径信息,过两周谁也说不清这张图是什么范围的数据。这个细节成本很低但价值很高。
    14. 组件卸载要销毁图表实例并移除 resize 监听。图表库通常会持有 DOM 引用和监听器,不销毁就是内存泄漏。看板是运营会长时间开着的页面,这一条不做,开一天浏览器就卡了。
    关键决策与取舍

    统一编排的代价是「最慢的那个图决定整体感知速度」。各自请求的话,快的图先出来,用户觉得快;统一编排要等一批数据都回来(或超时)才渲染,感知上会慢一些。但我认为一致性优先于感知速度——因为看板的用途是辅助决策,显示矛盾的信息比慢一秒的危害大得多。缓解办法是给每个图独立的加载态:先都显示骨架图,谁的数据回来了谁先渲染,但渲染的一定是当前批次的数据;这样既保证了一致性,又不用等最慢的那个。

    缺失值断线 vs 补零,选断线,接受图不好看。补零的折线是连续的、好看的;断线的图有缺口,运营一开始还问「这里怎么断了」。但断线是诚实的,补零是撒谎。而且断线引起的疑问会带来一次正确的对话(「那天数据还没同步」),补零引起的误判会带来一次错误的决策。这个取舍的原则是:可视化的第一要求是不误导,其次才是好看。

    采样放前端还是后端,按量级分。几千个点前端采样就行(实现简单、后端不用改);上万个点应该让后端按粒度聚合返回。判据是「传输这些数据本身是否已经成为问题」——如果传输就已经很慢了,前端采样是无意义的,因为代价已经付了。

    踩过的坑一:补零那次差点引发误判。运营看到曲线掉到 0,以为支付链路挂了,紧急拉群。排查了二十分钟才发现是数据同步延迟,而前端把缺失补成了 0。这件事让我理解了「前端的显示选择会直接影响业务决策」——我当时的想法只是「让图好看点」,完全没意识到它在传递一个业务事实。这是我在这个项目里最深的一个教训。

    踩过的坑二:看板不一致状态被发现得很晚。因为它不报错、不白屏,只是几个图的时间范围不一致,而界面上没有任何提示。是运营对着看板算数发现「订单数和金额对不上」才报出来。教训是:这类「静默的错误」必须靠设计避免,而不是靠测试发现——测试的时候网络很快,六个请求都秒回,压根复现不出来。所以我后来的做法是用工具把某个接口延迟,专门测这种不一致场景。

    踩过的坑三:图表实例没销毁,看板开半天浏览器卡死。运营习惯把看板挂在一个标签页里一整天。切换筛选条件时我每次都创建新图表,旧的没销毁,实例和监听器一直累积。修法是统一在卸载和重建前销毁。这类问题短时测试完全测不出来,只能靠长时间挂机加看内存曲线。

    没做的部分:没做图表之间的联动(点柱状图的某一天,其他图跟着筛到那天)。交互上很酷,但需要定义清楚联动的传播规则(点了之后筛选条件要不要变、URL 要不要变、怎么撤销),复杂度不低而运营也没提这个需求。没有明确需求的交互增强,我倾向不做——它会增加维护成本和出 bug 的面积。

    数字是怎么测的

    不一致状态「不再出现」怎么验证:用工具把其中一个接口延迟数秒,改筛选条件,断言在延迟接口返回之前,那个图显示的是加载态而不是旧数据;以及断言延迟接口返回后,它渲染的是当前批次的数据。不构造延迟是测不出来的——这一点必须说明,否则「我测过」不可信。

    空值处理的验证用构造数据:造一个含 null、含 0、含缺失日期的数据集,断言渲染结果里 0 画在零位置、null 是断线、缺失日期不被补成 0。这是纯前端可测的,写成快照或断言都行。

    包体积:看板路由的 chunk 大小以及首屏 JS 的变化(按需引入 + 懒加载之后,不进看板的人少下载多少)。要说清是压缩后还是压缩前的体积,这两个数差很多,不说清容易被追问。

    渲染性能:数据点数量和渲染耗时的对应关系(几百点、几千点、采样后),而不是报一个固定值。因为它完全取决于数据量,给对应关系才能说明采样阈值是怎么定的

    内存泄漏的验证只能靠长时间运行:反复切换筛选条件几十次、或者挂机若干小时,看堆内存曲线是否持续增长。这类问题短时测试测不出来,必须说明测法,否则「没有内存泄漏」是没有依据的。

    不要报什么:不要报「看板加载速度提升 N%」——统一编排实际上可能让整体感知略慢,硬说提速是不诚实的,而且一被追问「你等所有请求都回来才渲染,怎么会更快」就穿了。该报的是「不一致状态消失」「空值不再被误读」「包体积下降」这三件事,它们都是可核对的。

    面试追问
    Q:让每个图表自己请求自己渲染,不是更符合组件化的思路吗? A:组件化在「组件之间互相独立」的前提下是对的,但这几个图表不独立——它们在共同描述同一个数据视图,用户是把它们当成一个整体来读的(看订单数的同时看金额,还会心算客单价)。所以它们的数据必须同批次,否则看板会显示互相矛盾的信息。我们真实踩到的就是这个:运营对着看板算数,发现订单数和金额对不上,因为那是两个时间范围的数据。这个错误的危害不是「不好用」,是「会让人做出错误判断」,而且它不报错不白屏,很难被测出来。所以我的判断是:组件化的边界应该按「是否独立」划,而不是按「是否是同一个视觉单元」划。这几个图在视觉上是独立的卡片,在语义上是一个整体,那取数就该统一编排。至于组件复用性——图表组件本身仍然是纯展示的、可复用的,只是把「取数」这个职责从组件里拿出来了,这反而让组件更纯粹。
    Q:缺失数据补零和断线,这真的有那么重要吗?看起来只是显示细节。 A:它不是显示细节,是正确性问题。我们真实发生过:运营看到某天曲线掉到 0,判断支付链路挂了,紧急拉群拉了值班的人,排查二十分钟才发现是数据同步延迟,前端把缺失补成了 0。补零传递的信息是「那天的成交是零」,这是一个具体的、错误的业务事实;断线传递的信息是「那天的数据我不知道」,这是诚实的。两者对用户的行动指引完全不同——前者会让他去查事故,后者会让他去问数据什么时候补齐。我从这件事学到的是:前端的显示选择会直接影响业务决策,所以「让图好看」不能成为改变数据语义的理由。顺带说一个配套的点:这个修法依赖后端接口能表达「无值」——如果后端直接省略缺失的日期,前端压根区分不出来。所以我还去和后端约定了「返回完整日期序列、缺失位置给 null」,这是前端应该主动提的接口需求,不能只在前端想办法。
    Q:图表库直接整包引入不行吗?现在网速都很快。 A:单看这个看板,整包引入确实影响不大。但问题是这个后台有几十个页面,看板只有数据运营会用——如果图表库进了主包,那所有打开后台的人(大部分是商品运营,压根不看这个看板)都要下载这些代码。成本被分摊给了不需要它的人。所以我的判断依据是「这个依赖的受众占多少比例」:全站都用的(比如 UI 组件库的基础组件)进主包没问题,只有单个页面用的重依赖必须懒加载。另外要说清一个前提:这个优化的价值和用户网络环境相关。如果后台只在公司内网用、大家都是千兆网,那这个优化的实际收益很小,可以往后排;但如果有外部商家或者海外同事要用,首屏体积就是实打实的体验问题。我们的情况是有海外的运营同事,所以做了。脱离用户环境谈性能优化的必要性是没意义的,这一点我会主动说。

没有匹配的内容,换个关键词试试。

项目拆解 · 后台常用交互模块(Vue · 实习级)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据