You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript简易递归模板引擎递归子节点无法追加问题

极简递归JS模板引擎子节点挂载失败问题

实现逻辑

开发一款支持递归特性的极简JavaScript模板引擎,核心设计为:

  • 定义单个<template></template>标签作为模板基座
  • 标签内设置两类特殊占位字段:{{字段名}}用于普通文本替换,!!R 字段名!!标记递归渲染位置
  • 渲染时递归标记位置会被模板自身用对应子数据递归渲染替换

初始实现代码

JavaScript 部分

class Template {
  constructor(template, container) {
    this.template = template;
    this.container = container;
    this.node = [];
  }

  render_single(template, node, data) {
    // 替换{{.*?}}格式的普通占位符
    node.innerHTML = template.content.children[0].innerHTML.replace(/{{(.*?)}}/g, (match) => {
      var key = match.split(/{{|}}/).filter(Boolean)[0];
      if (key in data) {
        return data[key];
      } else {
        console.log("WARNING: " + key + " not found in data array");
      }
      return "";
    });

    node.innerHTML = node.innerHTML.replace(/!!(.*?)!!/g, (match) => {
      var key = match.split(/!!|!!/).filter(Boolean)[0];
      switch (key.substr(0, 1)) {
        case 'R': // 递归渲染标记
          var rkey = key.substr(2);
          if (rkey in data) {
            var rnode = node.querySelector('.' + rkey);
            var rtemplate = new Template(this.template, rnode);
            rtemplate.render(data[rkey]);
          }
          break;
      }
      return "";
    });
    this.container.append(node);
  }

  render(data) {
    for (var j = 0; j < data.length; j++) {
      for (var i = 0; i < this.template.length; i++) {
        var node = this.template[i].content.children[0].cloneNode(true);
        this.render_single(this.template[i], node, data[j]);
      }
    }
  }
}

window.onload = function() {
  var data = [{
      name: "Test1",
      childs: [{
          name: "Test Childs 1"
        },
        {
          name: "Test Childs 2"
        },
      ]
    },
    {
      name: "Test2"
    },
    {
      name: "Test3"
    },
    {
      name: "Test4"
    },
  ];
  var container = document.querySelector('#test');
  var template = container.getElementsByTagName('template');

  test = new Template(template, container);
  test.render(data);
}

HTML 模板部分

<div id="test">
  <template>
    <div class="test">
      <p>Template {{name}}</p>
      <div class="childs">!!R childs!!</div>
    </div>
  </template>
</div>

问题现象

  • 仅顶层节点可以正常追加到容器中,递归生成的子节点无法挂载到对应目标节点
  • 测试时如果将递归子节点追加到document.body(即上述JS代码中递归逻辑里的rnode替换为document.body),子节点可以正常渲染
  • 疑问:向父节点追加子节点前,是否需要执行特定操作更新父节点状态?

问题原因

不需要额外更新父节点状态,问题出在DOM操作和字符串替换的执行顺序冲突:

  1. 处理递归标记的逻辑写在了String.replace()的回调函数中,此时正则替换还没执行完成,在回调里给临时DOM节点rnode插入递归生成的子节点后,replace会返回替换完成的新字符串(递归标记!!R childs!!已经被替换为空字符串)
  2. 紧接着执行node.innerHTML = 替换后的新字符串时,浏览器会重新解析这段HTML生成全新的DOM结构,之前在回调里操作的旧.childs节点连同插入的所有递归子节点,会被新生成的空.childs节点直接覆盖销毁,自然无法显示。
  3. 换成document.body能正常渲染的原因是,document.body从来没有被重新赋值innerHTML覆盖,插入的节点会一直保留,不存在被销毁的问题。

修复方案

把递归渲染逻辑从字符串替换的回调里移出来,等普通占位符替换完成、HTML稳定渲染为DOM节点后,再查找递归容器做子节点渲染,避免后续的innerHTML赋值覆盖已经插入的递归内容。
修正后的render_single方法代码:

render_single(template, node, data) {
  // 1. 先替换所有普通{{}}占位符,生成替换完普通文本的HTML字符串
  const plainHtml = template.content.children[0].innerHTML.replace(/{{(.*?)}}/g, (match) => {
    const key = match.slice(2, -2).trim();
    if (key in data) return data[key];
    console.log(`WARNING: ${key} 未在数据集中找到`);
    return "";
  });
  // 2. 将普通文本HTML写入节点,生成稳定DOM结构
  node.innerHTML = plainHtml;
  // 3. 遍历所有递归标记,直接在已生成的DOM上做递归渲染,不会被后续操作覆盖
  const regRecursive = /!!R\s+([a-zA-Z0-9_]+)!!/g;
  let matchRes;
  while ((matchRes = regRecursive.exec(plainHtml)) !== null) {
    const rkey = matchRes[1];
    if (rkey in data && Array.isArray(data[rkey])) {
      const rnode = node.querySelector(`.${rkey}`);
      const rInstance = new Template(this.template, rnode);
      rInstance.render(data[rkey]);
    }
  }
  this.container.append(node);
}

内容的提问来源于stack exchange,提问作者Tobias Binkowski

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 05:27:46