是否有任何方法可以从通用的HTML片段创建文档片段?

Is there any way to create a document fragment from a generic piece of HTML?

本文关键字:片段 HTML 创建 文档 任何 方法 是否      更新时间:2023-09-26

我正在开发一个应用程序,该应用程序使用一些客户端模板来呈现数据,大多数javascript模板引擎返回带有标记的简单字符串,并由开发人员将该字符串插入DOM。

我搜索了一下,看到很多人建议使用空div,将其innerHTML设置为新字符串然后遍历该div的子节点,像这样

var parsedTemplate = 'something returned by template engine';
var tempDiv = document.createElement('div'), childNode;
var documentFragment = document.createDocumentFragment;
tempDiv.innerHTML = parsedTemplate;
while ( childNode = tempDiv.firstChild ) {
    documentFragment.appendChild(childNode);
}

和TADA, documentFragment现在包含解析的模板。然而,如果我的模板恰好是一个tr,在它周围添加div并没有达到预期的行为,因为它添加了td的内容在行内。

有人知道解决这个问题的好方法吗?现在,我正在检查将插入解析模板的节点,并从其标记名称创建元素。我甚至不确定是否还有其他方法可以做到这一点。

在搜索的过程中,我在w3邮件列表中看到了这个讨论,但不幸的是,没有有用的解决方案。

您可以使用DOMParser作为XHTML来避免dom喜欢执行的HTML "自动更正":

var parser = new DOMParser(),
doc = parser.parseFromString('<tr><td>something returned </td><td>by template engine</td></tr>', "text/xml"),
documentFragment = document.createDocumentFragment() ;
documentFragment.appendChild( doc.documentElement );
//fragment populated, now view as HTML to verify fragment contains what's expected:
var temp=document.createElement('div');
temp.appendChild(documentFragment);
console.log(temp.outerHTML); 
    // shows: "<div><tr><td>something returned </td><td>by template engine</td></tr></div>"

与使用朴素的innerHTML和临时div:

形成对比
var temp=document.createElement('div');
temp.innerHTML='<tr><td>something returned </td><td>by template engine</td></tr>';
console.log(temp.outerHTML); 
// shows: '<div>something returned by template engine</div>' (bad)
通过将模板处理为XHTML/XML(确保其格式良好),我们可以改变HTML的常规规则。DOMParser的覆盖范围应该与对documentFragment的支持相关,但是在firefox的一些旧版本(个位数版本)上,您可能需要使用importNode()。

作为可重用的函数:

function strToFrag(strHTML){
   var temp=document.createElement('template');
    if( temp.content ){
       temp.innerHTML=strHTML;
       return temp.content;
    }
    var parser = new DOMParser(),
    doc = parser.parseFromString(strHTML, "text/xml"),
    documentFragment = document.createDocumentFragment() ;
    documentFragment.appendChild( doc.documentElement );
    return documentFragment;
}

这是一个未来,而不是现在,但HTML5定义了一个<template>元素,将为您创建片段。你可以这样做:

var parsedTemplate = '<tr><td>xxx</td></tr>';
var tempEL = document.createElement('template');
tempEl.innerHTML = parsedTemplate;
var documentFragment = tempEl.content;

它目前在Firefox中工作。看到

理想的方法是使用来自HTML5的<template>标签。您可以以编程方式创建模板元素,将.innerHTML分配给它,所有解析的元素(甚至是表的片段)都将出现在template.content属性中。这已经帮你搞定了。但是,这只存在于最新版本的Firefox和Chrome中。

如果存在模板支持,就像这样简单:

function makeDocFragment(htmlString) {
    var container = document.createElement("template");
    container.innerHTML = htmlString;
    return container.content;
}  

返回的结果就像documentFragment一样。你可以直接附加它,它就像documentFragment一样解决了问题,除了它有支持.innerHTML赋值的优势,它允许你使用部分形成的HTML片段(解决了我们需要的两个问题)。

但是,模板支持还不是无处不在,所以你需要一个后备方法。处理回退的蛮力方法是偷看HTML字符串的开头,查看它以什么类型的选项卡开始,并为该类型的标记创建适当的容器,并使用该容器将HTML分配给它。这是一种蛮力方法,但它有效。对于只能合法存在于特定类型容器中的任何类型的HTML元素,都需要这种特殊处理。我在下面的代码中包含了一堆这些类型的元素(尽管我没有试图使列表详尽无遗)。下面是代码和一个工作的jsFiddle链接。如果您使用的是最新版本的Chrome或Firefox,则代码将采用使用模板对象的路径。如果是其他浏览器,它会创建相应类型的容器对象。

var makeDocFragment = (function() {
    // static data in closure so it only has to be parsed once
    var specials = {
        td: {
            parentElement: "table", 
            starterHTML: "<tbody><tr class='xx_Root_'></tr></tbody>" 
        },
        tr: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        thead: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        caption: {
            parentElement: "table",
            starterHTML: "<tbody class='xx_Root_'></tbody>"
        },
        li: {
            parentElement: "ul",
        },
        dd: {
            parentElement: "dl",
        },
        dt: {
            parentElement: "dl",
        },
        optgroup: {
            parentElement: "select",
        },
        option: {
            parentElement: "select",
        }
    };
    // feature detect template tag support and use simpler path if so
    // testing for the content property is suggested by MDN
    var testTemplate = document.createElement("template");
    if ("content" in testTemplate) {
        return function(htmlString) {
            var container = document.createElement("template");
            container.innerHTML = htmlString;
            return container.content;
        }
    } else {
        return function(htmlString) {
            var specialInfo, container, root, tagMatch, 
                documentFragment;
            // can't use template tag, so lets mini-parse the first HTML tag
            // to discern if it needs a special container
            tagMatch = htmlString.match(/^'s*<([^>'s]+)/);
            if (tagMatch) {
                specialInfo = specials[tagMatch[1].toLowerCase()];
                if (specialInfo) {
                    container = document.createElement(specialInfo.parentElement);
                    if (specialInfo.starterHTML) {
                        container.innerHTML = specialInfo.starterHTML;
                    }
                    root = container.querySelector(".xx_Root_");
                    if (!root) {
                        root = container;
                    }
                    root.innerHTML = htmlString;
                }
            }
            if (!container) {
                container = document.createElement("div");
                container.innerHTML = htmlString;
                root = container;
            }
            documentFragment = document.createDocumentFragment();
            // start at the actual root we want
            while (root.firstChild) {
                documentFragment.appendChild(root.firstChild);
            }
            return documentFragment;
        }
    }
    // don't let the feature test template object hang around in closure
    testTemplate = null;
})();
// test cases
var frag = makeDocFragment("<tr><td>Three</td><td>Four</td></tr>");
document.getElementById("myTableBody").appendChild(frag);
frag = makeDocFragment("<td>Zero</td><td>Zero</td>");
document.getElementById("emptyRow").appendChild(frag);
frag = makeDocFragment("<li>Two</li><li>Three</li>");
document.getElementById("myUL").appendChild(frag);
frag = makeDocFragment("<option>Second Option</option><option>Third Option</option>");
document.getElementById("mySelect").appendChild(frag);

带有几个测试用例的工作演示:http://jsfiddle.net/jfriend00/SycL6/

使用此函数

    支持IE11
  • 不必符合xml。"& lt; td hidden>测试"

function createFragment(html){
    var tmpl = document.createElement('template');
    tmpl.innerHTML = html;
    if (tmpl.content == void 0){ // ie11
        var fragment = document.createDocumentFragment();
        var isTableEl = /^[^'S]*?<(t(?:head|body|foot|r|d|h))/i.test(html);
        tmpl.innerHTML = isTableEl ? '<table>'+html : html;
        var els        = isTableEl ? tmpl.querySelector(RegExp.$1).parentNode.childNodes : tmpl.childNodes;
        while(els[0]) fragment.appendChild(els[0]);
        return fragment;
    }
    return tmpl.content;
}

@dandavis的解决方案将只接受ie11中符合xml的内容。
我不知道是否还有其他的标签需要考虑?