应使用 createElement + textContent 防 XSS,模板字符串仅作结构骨架;优先用 template 标签配合 cloneNode(true) 渲染列表;高频渲染需通过 DocumentFragment 批量插入;响应式可用 renderList 封装全量重绘。
直接用 innerHTML 拼接字符串时,如果变量含 HTML 字符(比如 <、"),会破坏结构甚至引发 XSS。必须先转义再插入,或改用更安全的方案。
推荐用 document.createElement + textContent 组合:对每个字段单独设 textContent,天然防注入;若真需渲染富文本,用 DOMPurify.sanitize() 处理后再塞进 innerHTML。
item.innerHTML = `<li>${name}</li>` —— name 是用户输入就危险const li = document.createElement('li'); li.textContent = name;
const template = `<ul class="list"></ul>`
<template> 是浏览器原生支持的离线 DOM 容器,内容不渲染、不执行脚本、不加载资源,适合做可复用的列表项模板。比拼接字符串再 innerHTML 更可靠,尤其在需要绑定事件或保留子节点状态时。
但注意:<template> 内容必须用 content.cloneNode(true) 复制,直接 appendChild 会移动节点而非复制。
立即学习“前端免费学习笔记(深入)”;
<template id="item-template"><li class="item"><span class="title"></span></li></template>
const tmpl = document.getElementById('item-template').content;
const el = tmpl.cloneNode(true); el.querySelector('.title').textContent = item.title; list.appendChild(el);
true 参数,否则只克隆顶层 <template> 节点每次新增一项都调用 createElement 或克隆 <template> 是安全的,但高频操作(如滚动加载上千条)会导致卡顿。关键不是“少创建”,而是“少触发重排重绘”。
把所有新节点先塞进 DocumentFragment,最后一次性 append 到目标容器,能显著减少 layout 回流次数。
items.forEach(item => list.appendChild(createItem(item))); —— 每次 append 都可能触发重排const frag = document.createDocumentFragment(); items.forEach(item => frag.appendChild(createItem(item))); list.appendChild(frag);
<template>,同样先往 frag 里 append 克隆节点,最后再挂载原生没有响应式数据绑定,但可以用 Proxy 监听数组变化,配合模板自动更新 DOM。不过要注意:数组方法如 push、splice 不会触发 set,得重写或用 Reflect.set + 手动通知。
更务实的做法是封装一个 renderList 函数,接收数据数组和渲染函数,每次数据变就全量重绘——只要列表不超过几百项,性能完全够用。
function renderList(container, data, renderItem) { container.innerHTML = ''; data.forEach(item => container.appendChild(renderItem(item))); }
renderList(listEl, items, item => createItem(item));
Array.prototype.push——改写原型易出兼容问题,且无法捕获字面量赋值<template> 适合结构固定、字段不多的列表;一旦涉及条件分支(比如不同状态显示不同子组件)、嵌套列表或复杂交互,手写模板很快失控,这时候该上轻量框架或自定义 element 了。