本节摘要:文档对象模型把页面解析成一棵可编程的节点树,脚本文档操作接口是行为班改写骨架的合规通道。本节讲树的查找(选择器接口)、节点的创建与插入、内容与样式的修改,以及往页面注入外部内容前必须过的安全关——文本与标记的接口区别。看板的工单渲染函数在本节落地。
浏览器把结构班交付的文档解析成一棵节点树:文档是根,元素是枝干,文字是叶子。脚本文档对象接口就是这棵树的操纵杆——1.2 节画的"合规通道"至此兑现:行为班不碰结构班的源文件,运行时在树上做增删改。你在元素面板里看到的那棵树,就是脚本操作的直接对象,两边实时同步。

查找是所有动作的起点。文档接口提供一对选择器方法:取第一个匹配与取全部匹配,选择器语法与样式班完全同源——3.1 节学的选择器,在这里原样复用,这是三班共用一套命名体系的又一处红利。修改动作里最高频的是换文本、换类名:换类名触发样式班的规则切换,正是 1.2 节徽章切片的"换挂钩"。
// 查找:一对选择器方法走天下 const table = document.querySelector(".order-table"); // 取第一个匹配:符合选择器的第一个元素,没有则 null const rows = document.querySelectorAll(".order-table tbody tr"); // 取全部匹配:静态节点列表,可用取项循环遍历 const form = document.getElementById("new-order-form"); // 按标识直取:历史久但仅此一技,新代码统一用选择器方法 // 修改:文本、类名、属性三路 const badge = rows[0].querySelector(".badge"); badge.textContent = "已完成"; // 换纯文本:安全接口,标签不解析 badge.classList.remove("badge-running"); // 摘旧挂钩 badge.classList.add("badge-done"); // 挂新挂钩——样式班规则自动接管 badge.dataset.no = "MO-1042"; // data 属性:往元素上挂自定义数据,命名转成短横线风格
// 创建与插入:新工单上树 function appendRow(order) { const tr = document.createElement("tr"); // 创建:造出游离的新行 for (const text of [order.no, order.product, order.process, order.owner]) { const td = document.createElement("td"); td.textContent = text; // 纯文本写入:安全接口 tr.appendChild(td); // 插入:单元格挂进行 } const td = document.createElement("td"); td.innerHTML = `<span class="badge">${order.status}</span>`; // 标记接口:这里喂的是自己拼的标记骨架——status 值要先过白名单 tr.appendChild(td); table.tBodies[0].appendChild(tr); // 整行挂进表体 } appendRow({ no: "MO-1046", product: "压板", process: "精铣", owner: "陈师傅", status: "pending" }); // 页面上立刻多出一行——脚本改树的直接见证 // 删除与清空:下树动作 // tr.remove(); // 节点自请出树 // table.tBodies[0].replaceChildren(); // 清空容器:批量重画的前置动作
4.3 节立下的投影纪律在此落地:写一个渲染函数,输入工单数组,输出整段表格内容。数据变了就调它,页面整体重画——而不是散在各处直接改页面文字。重画初看"浪费",实际让状态来源唯一(数据层),页面永远与数据一致,出问题只查一处。
// 看板渲染函数:数据 → 页面的一次性投影 const STATUS_LABEL = { pending: "待开工", running: "加工中", done: "已完成" }; function renderOrders(orders) { const tbody = document.querySelector(".order-table tbody"); tbody.replaceChildren(); // 先清空:保证页面=数据 for (const o of orders) { const tr = document.createElement("tr"); tr.innerHTML = ` <td>${o.no}</td> <td>${o.product}</td> <td>${o.process}</td> <td>${o.owner}</td> <td>${o.priority === "urgent" ? "加急" : "常规"}</td> <td><span class="badge badge-${o.status}">${STATUS_LABEL[o.status]}</span></td>`; // 拼标记模板:字段值都来自内部数据; // 一旦字段来自用户输入,必须先转义(见下文安全关) tbody.appendChild(tr); } } renderOrders(orders); // 每次数据变化后重调此函数:登记、改状态、删除,入口统一
// 文本转义:用户内容上墙前的消毒工序 function escapeText(s) { const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; return String(s).replace(/[&<>"']/g, (ch) => map[ch]); } // 把标记符号转义成实体字符:尖括号变成可见的字符, // 浏览器不再把它们当成标签解析 // 安全与效率的对照用法 td.textContent = userInput; // 首选:纯文本接口天然免疫 td.innerHTML = `<b>${escapeText(userInput)}</b>`; // 要加标记时:内容过转义再拼 // 绝不做的事:td.innerHTML = userInput
图 1 右侧的红框是本节真正的重点。往页面写内容有两个接口:纯文本接口把输入当字符,标记接口把输入当代码。操作工在"产品名称"里填一段带尖括号的文本,若直接进标记接口,浏览器会把它当标记执行——页面被注入了不属于你的内容和脚本。看板是内部工具也逃不掉:填单的有实习生、有供应商、有离职前心情不佳的老员工。防御纪律两条:用户内容一律走纯文本接口;确需拼标记时,内容先过转义再进模板。5.4 节会把这套防御升级成完整攻防演练。
⚠️ 常见坑:脚本在头部加载时元素还没出生——文档从上往下解析,脚本执行时后面的元素尚不在树上,查找返回空值。三个解法按优先级:脚本放身体末尾;加载时用等文档解析完毕的事件;引用时加延迟执行标记(1.3 节的承诺机制)。
💡 关键直觉:所有"页面操作"先翻译成树的语言再动手——查是找节点、改是改节点的属性或内容、增是造节点挂上去、删是让节点出树。翻译习惯养成后,元素面板就是你脚本的慢放镜。
树能改了,下一节接上用户的手:事件处理,让点击与输入真正驱动这棵树。