Frontend Engineer · OA Coding

Amazon Frontend OA:Vanilla JS 愿望清单,状态与 DOM 合同怎么守?

初始5行、每次多5行、30行禁用,同一按钮切换愿望状态;真实DOM动作序列独立验证。

题目重点:初始5行、每次多5行、30行禁用,同一按钮切换愿望状态;真实DOM动作序列独立验证。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 训练输入30个唯一ID商品、名称为文本;调用方提供root div/countLabel/loadMoreButton,原OA DOM/selector合同仍待核。
  • 初始5、load每次+5、最多30;每行独立愿望状态,加入显示消息/Remove,移除恢复。默认英文文案是训练选择,题目Added to wishlisht可能为拼写回忆差异,支持addedMessage配置。
  • 样例为原生JS模块,不改网站UI。正式OA如果只编译TS入口须把逻辑放实际打包文件;本文不伪造原repo入口名。

解题路线

  1. 唯一source of truth为visible与selected ID集合;DOM只展示,不以button文本猜状态。两不同商品同名也不能用name当key,ID先规范为String并检唯一。
  2. 初始append5个div行,load只append新增行,不重建已显示节点,保留已选状态和focus/事件。count随visible更新,30时load disabled;不要异步错误时先把visible永久加了却没有行。
  3. 每行创建一个button,click只切selected成员、同一button文案与aria-pressed,status文本对应状态。不通过两个隐藏/显示button替代,因为source明确该做法未过其测试。
  4. root下结构按给定合同,不能看示例影片table就擅自额外加table;文本用textContent,商品名称中的<符号不成为HTML。官方测试未知时自己的合理结构仍不能声称完全等价原DOM。
  5. a11y用真实button和按下状态,可补加载/加入通知的live region,新增内容焦点保持在load或按明确交互策略移动;本文样例不覆盖完整无障碍验收。
  6. 独立jsdom测试100条×300动作历史,以另一个Set/visible模型核DOM数量、按钮数量/身份、状态文案、aria、30cap与文本安全。不是只调getState就称DOM验证。

手动走一遍

初始5行,第二行加入后load一次变10行,第二行仍是同一个Remove按钮;再移除恢复。重复load至30后disabled,多点不会超过30。30,000动作状态在jsdom独立核对通过,包括名字含<script>仍是普通文本。

JavaScript 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

// Teaching DOM contract: caller supplies the div root, count label and load button.
// Exactly 30 unique-ID products, supplied strings, no framework or table wrapper.
export function mountWishlist({ root, countLabel, loadMoreButton, products,
  addedMessage = "Added to wishlist" }) {
  if (products.length !== 30 || new Set(products.map(p => String(p.id))).size !== 30) {
    throw new Error("30 products with distinct IDs required");
  }
  root.replaceChildren();
  const selected = new Set();
  let visible = 0;
  function addFive() {
    const next = Math.min(visible + 5, 30);
    for (let i = visible; i < next; i++) {
      const product = products[i];
      const id = String(product.id);
      const row = root.ownerDocument.createElement("div");
      row.dataset.productId = id;
      const name = root.ownerDocument.createElement("span");
      name.textContent = `${id} ${product.name}`;
      const button = root.ownerDocument.createElement("button");
      button.type = "button";
      button.textContent = "Add to wishlist";
      button.setAttribute("aria-pressed", "false");
      const status = root.ownerDocument.createElement("span");
      button.addEventListener("click", () => {
        if (selected.has(id)) selected.delete(id);
        else selected.add(id);
        const added = selected.has(id);
        button.textContent = added ? "Remove from wishlist" : "Add to wishlist";
        button.setAttribute("aria-pressed", String(added));
        status.textContent = added ? addedMessage : "";
      });
      row.append(name, button, status);
      root.append(row);
    }
    visible = next;
    countLabel.textContent = String(visible);
    loadMoreButton.disabled = visible === 30;
  }
  loadMoreButton.addEventListener("click", addFive);
  addFive();
  return { getState: () => ({ visible, selected: new Set(selected) }) };
}

复杂度

每次load最多创建5行,单次toggle期望O(1)集合更新/常量DOM节点修改;总30行状态O(30)。若正式变无界列表,需要窗口化/分页与异步请求合同,不能拿固定30上限推无限可扩展。

易错点与追问

容易失分的边界

  • 用文案推状态。
  • 两个button切hidden。
  • 每load重建全部丢状态/focus。
  • div下擅加table。
  • 只改JS文件实际入口却是TS。
  • 商品名innerHTML注入。

继续深挖

  • 异步load失败/双击怎样保持幂等?
  • 多实例挂载如何避免重复listener?
  • 键盘与screen reader怎样通知新增行?
  • selected持久化后数据换版本如何恢复?
✓已复制微信号!