将确切的 innerHTML 还原到 DOM

Posted

技术标签:

【中文标题】将确切的 innerHTML 还原到 DOM【英文标题】:Restore exact innerHTML to DOM 【发布时间】:2015-09-13 16:10:27 【问题描述】:

我想保存 DOM 的 html 字符串,稍后将其恢复为完全相同。代码如下所示:

var stringified = document.documentElement.innerHTML
// later, after serializing and deserializing
document.documentElement.innerHTML = stringified

这在一切都很完美的情况下有效,但是当 DOM 不符合 w3c 标准时,就会出现问题。第一行工作正常,stringified 与 DOM 完全匹配。但是当我从(不符合 w3c 的)stringified 恢复时,浏览器会发挥一些作用,生成的 DOM 与原来的不一样。

例如,如果我的原始 DOM 看起来像

<p><div></div></p>

那么最终的 DOM 会是这样的

<p></p><div></div><p></p>

因为div 元素不允许在p 元素内。有什么方法可以让浏览器使用与页面加载相同的 html 解析并按原样接受损坏的 html?

为什么html一开始就坏了?DOM不是我控制的。

这是一个显示行为http://jsfiddle.net/b2x7rnfm/5/ 的jsfiddle。打开你的控制台。

<body>
    <div id="asdf"><p id="outer"></p></div>
    <script type="text/javascript">
        var insert = document.createElement('div');
        var text = document.createTextNode('ladygaga');
        insert.appendChild(text);
        document.getElementById('outer').appendChild(insert);
        var e = document.getElementById('asdf')
        console.log(e.innerHTML);
        e.innerHTML = e.innerHTML;
        console.log(e.innerHTML); // This is different than 2 lines above!!
    </script>
</body>

【问题讨论】:

浏览器不存储无效标记。它存储 DOM,它尽其所能从初始标记中呈现,允许各种错误。当您访问 innerHTML 时,它会遍历 DOM 因为它当时存在 并构建它的字符串表示形式,这将是有效的(除了无效属性,它们会保留这些;可能还有几个其他小事,但不是像上面这样的结构性错误)。因此,您的 stringified 变量应该已经具有错误更正的 HTML。 It does on Chrome, Firefox, and IE10. 为了扩展 @T.J.Crowder 所说的,Javascript 无法访问原始 HTML 源代码。只能获取到DOM,是解释原始HTML的结果。 我已经用一个最小的 jsfiddle 更新了我的答案,以表明我的意思。我不需要原始的 HTML,我只需要在未来将 DOM 恢复到它的确切当前状态。 我已经简化了这个例子。 DOM 是如何失效的并不重要。如果 DOM 当前无效,操作将无法正常恢复。 @guest271314 请参阅问题中的示例。 【参考方案1】:

如果您需要能够保存和恢复无效的 HTML 结构,您可以通过 XML 来实现。以下代码来自this fiddle。

要保存,请创建一个新的 XML 文档,向其中添加要序列化的节点:

var asdf = document.getElementById("asdf");
var outer = document.getElementById("outer");
var add = document.getElementById("add");
var save = document.getElementById("save");
var restore = document.getElementById("restore");

var saved = undefined;
save.addEventListener("click", function () 
  if (saved !== undefined)
    return; /// Do not overwrite

  // Create a fake document with a single top-level element, as 
  // required by XML.    
  var parser = new DOMParser();
  var doc = parser.parseFromString("<top/>", "text/xml");

  // We could skip the cloning and just move the nodes to the XML
  // document. This would have the effect of saving and removing 
  // at the same time but I wanted to show what saving while 
  // preserving the data would look like    
  var clone = asdf.cloneNode(true);
  var top = doc.firstChild;
  var child = asdf.firstChild;
  while (child) 
    top.appendChild(child);
    child = asdf.firstChild;
  
  saved = top.innerHTML;
  console.log("saved as: ", saved);

  // Perform the removal here.
  asdf.innerHTML = "";
);

要恢复,您可以创建一个 XML 文档来反序列化您保存的内容,然后将节点添加到您的文档中:

restore.addEventListener("click", function () 
  if (saved === undefined)
      return; // Don't restore undefined data!

  // We parse the XML we saved.
  var parser = new DOMParser();
  var doc = parser.parseFromString("<top>" + saved + "</top>", "text/xml");
  var top = doc.firstChild;

  var child = top.firstChild;
  while (child) 
    asdf.appendChild(child);
    // Remove the extra junk added by the XML parser.
    child.removeAttribute("xmlns");
    child = top.firstChild;
  
  saved = undefined;
  console.log("inner html after restore", asdf.innerHTML);
);

使用小提琴,您可以:

    按“添加 LadyGaga...”按钮创建无效的 HTML。

    按“保存并从文档中删除”将结构保存在asdf 并清除其内容。这会将保存的内容打印到控制台。

    按“恢复”以恢复已保存的结构。

上面的代码旨在通用。如果可以对要保存的 HTML 结构做出一些假设,则可以简化代码。例如,blah 不是格式良好的 XML 文档,因为您需要 XML 中的单个顶部元素。所以上面的代码煞费苦心地添加了一个***元素(top)来防止这个问题。通常也不可能只将 HTML 序列化解析为 XML,以便保存操作序列化为 XML。

这比什么都重要。将 HTML 文档中创建的节点移动到 XML 文档或其他我没有预料到的方式可能会产生副作用。我已经在 Chrome 和 FF 上运行了上面的代码。我手头没有 IE 来运行它。

【讨论】:

酷。我将在我的 js 工具包中添加“将所有内容另存为 XML”【参考方案2】:

你可以使用outerHTML,它保持原来的结构:

(基于您的原始样本)

<div id="asdf"><p id="outer"></p></div>

<script type="text/javascript">
    var insert = document.createElement('div');
    var text = document.createTextNode('ladygaga');
    insert.appendChild(text);
    document.getElementById('outer').appendChild(insert);
    var e = document.getElementById('asdf')
    console.log(e.outerHTML);
    e.outerHTML = e.outerHTML;
    console.log(e.outerHTML);
</script>

演示:http://jsfiddle.net/b2x7rnfm/7

【讨论】:

不,使用outerHTML 不能解决问题。我已经使用了您的代码并添加了几个console.log,表明它不起作用。见this fiddle。当您执行e.outerHTML = e.outerHTML 时,浏览器将获取e 所引用的节点并将其替换为新的DOM 节点。所以在赋值之后e 就不再在DOM 树中了。我在小提琴中添加的第一个 console.log 表明了这一点。第二个表明 DOM 结构是 OP 试图避免的。 outerHTML 遵循与 innerHTML 相同的解析规则。【参考方案3】:

您必须克隆节点而不是复制 html。解析规则会强制浏览器在看到div时关闭p

如果您确实需要从该字符串中获取 html 并且它是有效的 xml,那么您可以使用以下代码($ 是 jQuery):

var html = "<p><div></div></p>";
var div = document.createElement("div");
var xml = $.parseXML(html);
div.appendChild(xml.documentElement);
div.innerHTML === html // true

【讨论】:

如果 html 包含 script 标签,这将不起作用。提醒一下,此代码应该能够在任意网页上运行。【参考方案4】:

您不能期望 HTML 被解析为不兼容的 HTML。但是由于编译后的不兼容 HTML 的结构是非常可预测的,您可以像这样创建一个使 HTML 再次不兼容的函数:

function ruinTheHtml() 

var allElements = document.body.getElementsByTagName( "*" ),
    next,
    afterNext;

Array.prototype.map.call( allElements,function( el,i )

    if( el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' ) 

        if(el.textContent === '') 

            next = el.nextSibling;

            afterNext = next.nextSibling;

            if( afterNext.textContent === '' ) 

                el.parentNode.removeChild( afterNext );
                el.appendChild( next );

            

        

    
);


看小提琴: http://jsfiddle.net/pqah8e25/3/

【讨论】:

这仅适用于我的示例。它应该适用于我给你的任何网页。 它应该适用于任何不兼容的 html,因为当您制作不正确的 html 时,它总是会被解析为 。你能给我一个它不起作用的案例吗?【参考方案5】:

看这个例子:http://jsfiddle.net/kevalbhatt18/1Lcgaprc/

MDN cloneNode

var e = document.getElementById('asdf')
console.log(e.innerHTML);
backupElem = e.cloneNode(true);
// Your tinkering with the original
e.parentNode.replaceChild(backupElem, e);
console.log(e.innerHTML);

【讨论】:

如果我可以将所有内容保存在内存中,这将有效,但 html 必须在恢复之前进行序列化/反序列化。 @SergiuToarca 您是否在项目中使用 jquery ?如果是,那么我有一个解决方案 你可以使用任何你喜欢的库。【参考方案6】:

尝试利用Blob,URL.createObjectURL导出html;在导出的html 中包含script 标记,这会从呈现的html 文档中删除&lt;div&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt; 元素

html

<body>
    <div id="asdf">
        <p id="outer"></p>
    </div>
    <script>
        var insert = document.createElement("div");
        var text = document.createTextNode("ladygaga");
        insert.appendChild(text);
        document.getElementById("outer").appendChild(insert);
        var elem = document.getElementById("asdf");
        var r = document.querySelectorAll("[id=outer] ~ *");
        // remove last `div` , `p` elements from `#asdf`
        for (var i = 0; i < r.length; ++i) 
            elem.removeChild(r[i])
        
    </script>
</body>

js

var e = document.getElementById("asdf");   
var html = e.outerHTML;  
console.log(document.body.outerHTML);   
var blob = new Blob([document.body.outerHTML], 
    type: "text/html"
);   
var objUrl = window.URL.createObjectURL(blob);
var popup = window.open(objUrl, "popup", "width=300, height=200");

jsfiddle http://jsfiddle.net/b2x7rnfm/11/

【讨论】:

我无法控制网页,所以我不知道元素 id asdf 是否存在(在大多数页面上它不会存在)。【参考方案7】:

这不适用于您最近的说明,即您必须有一个字符串副本。不过,将其留给可能具有更大灵活性的其他人。


由于使用 DOM 似乎允许您在某种程度上保留无效结构,并且使用 innerHTML 涉及重新解析(正如您所观察到的)副作用,我们必须考虑不使用 @987654322 @:

你可以克隆原版,然后换入克隆版:

var e = document.getElementById('asdf')
snippet.log("1: " + e.innerHTML);
var clone = e.cloneNode(true);
var insert = document.createElement('div');
var text = document.createTextNode('ladygaga');
insert.appendChild(text);
document.getElementById('outer').appendChild(insert);
snippet.log("2: " + e.innerHTML);
e.parentNode.replaceChild(clone, e);
e = clone;
snippet.log("3: " + e.innerHTML);

实例:

var e = document.getElementById('asdf')
snippet.log("1: " + e.innerHTML);
var clone = e.cloneNode(true);
var insert = document.createElement('div');
var text = document.createTextNode('ladygaga');
insert.appendChild(text);
document.getElementById('outer').appendChild(insert);
snippet.log("2: " + e.innerHTML);
e.parentNode.replaceChild(clone, e);
e = clone;
snippet.log("3: " + e.innerHTML);
<div id="asdf">
  <p id="outer">
    <div>ladygaga</div>
  </p>
</div>

<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

请注意,就像innerHTML 解决方案一样,这将清除相关元素上的事件处理程序。您可以通过创建文档片段并将其子项克隆到其中来保留最外层元素的处理程序,但这仍然会丢失子项上的处理程序。


这个较早的解决方案不适用于您,但将来可能适用于其他人:

我之前的解决方案是跟踪您所做的更改,并逐一撤消更改。因此,在您的示例中,这意味着删除 insert 元素:

var e = document.getElementById('asdf')
console.log("1: " + e.innerHTML);
var insert = document.createElement('div');
var text = document.createTextNode('ladygaga');
insert.appendChild(text);
var outer = document.getElementById('outer');
outer.appendChild(insert);
console.log("2: " + e.innerHTML);
outer.removeChild(insert);
console.log("3: " + e.innerHTML);

var e = document.getElementById('asdf')
snippet.log("1: " + e.innerHTML);
var insert = document.createElement('div');
var text = document.createTextNode('ladygaga');
insert.appendChild(text);
var outer = document.getElementById('outer');
outer.appendChild(insert);
snippet.log("2: " + e.innerHTML);
outer.removeChild(insert);
snippet.log("3: " + e.innerHTML);
<div id="asdf">
  <p id="outer">
    <div>ladygaga</div>
  </p>
</div>

<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

【讨论】:

如问题所述,我无法控制 DOM 是如何形成的。我只想恢复一个精确的副本。对于更多上下文,这是在扩展中发生的,因此它应该适用于任何网页。 @SergiuToarca:答案中没有任何内容表明您必须改变 DOM 的形成方式。您的上下文有帮助(有助于将上下文放入问题中!)——例如,您不控制 HTML 更改它的代码。 @SergiuToarca:好消息是cloneNode 解决方案,您不必这样做。 不幸的是,cloneNode 无法工作,因为页面需要在序列化/反序列化后恢复,例如远程。我已经更新了我的问题以澄清这一点。

以上是关于将确切的 innerHTML 还原到 DOM的主要内容,如果未能解决你的问题,请参考以下文章

将循环结果输出到 .innerHTML

如何使用 innerHTML 将对象值正确打印到 DIV?

将EventListener 添加到innerHTML

Angular将样式标签添加到innerHTML

将输入复选框的 DOM 属性更改反映到 innerHTML 属性

在我将它们合并到其他 Javascript 之前,innerHTML 示例运行良好