JS实现HTML实体与字符的相互转换

字符转换的小技巧

Posted by Li Yucang on November 4, 2017

JS 实现 HTML 实体与字符的相互转换

前端开发工作中,经常需要将 HTML 的左右尖括号等转义成实体形式。我们不能把<,>,&等直接显示在最终看到的网页里。需要将其转义后才能在网页上显示。不转义直接将用户输入写到网页上往往是行不通的,很容易出现 XSS 漏洞。

转义字符(Escape Sequence)也称字符实体(Character Entity)。定义转义字符串的主要原因是“<”和“>”等符号已经用来表示 HTML TAG,因此不能直接当作文本中的符号来使用。但有时需求是在 HTML 页面上使用这些符号,所以需要定义它的转义字符串。

这里我们介绍两种比较常见的转义 html 实体的方法。

正则替换实现转义

我们可以通过一个映射表加上正则替换来实现 HTML 实体与字符的相互转换。

var entityMap = {
    escape: {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&apos;',
    },
    unescape: {
      '&amp;': "&",
      '&apos;': "'",
      '&gt;': ">",
      '&lt;': "<",
      '&quot;': '"',
    }
};
var entityReg = {
    escape: RegExp('[' + Object.keys(entityMap.escape).join('') + ']', 'g'),
    unescape: RegExp('(' + Object.keys(entityMap.unescape).join('|') + ')', 'g')
}

// 将HTML转义为实体
function escape(html) {
    if (typeof html !== 'string') return ''
    return html.replace(entityReg.escape, function(match) {
        return entityMap.escape[match]
    })
}

// 将实体转回为HTML
function unescape(str) {
    if (typeof str !== 'string') return ''
    return str.replace(entityReg.unescape, function(match) {
        return entityMap.unescape[match]
    })
}

这种方法灵活性,完整性都比较好,可根据需求添加或减少映射表,且可以运行在任意 JS 环境中。

浏览器 DOM API 实现转义

我们先简单介绍一下接下来需要使用的几个 api,下面是之后会用到的代码:

<div id="test">
   Warning: This element contains <code>code</code> and <strong>strong language</strong>.
</div>

var x = document.getElementById('test');

innerHTML

赋值操作:先对值内容进行模式匹配,然后把处理后的值赋予给 innerHTML 属性。

模式匹配结果将导致 保留将字符转换为 HTML 实体 两个操作。

以下情况将被保留:

  1. HTML 实体(ASCII 实体、符号实体和字符实体)的实体名或实体编号;

  2. 符号实体和字符实体对应的字符;

  3. 没有 HTML 实体与之对应的字符;

  4. HTML 标签。(如<img>)

以下情况将会执行字符转换为 HTML 实体:

  1. ASCII 实体对应的字符(<、>、&)。

也就是说除了单独的 <、>、&会被转换为实体名外,将原封不动地将值赋予给 innerHTML 属性。

取值操作:直接获取 innerHTML 属性值。

x.innerHTML
// => "
// =>   Warning: This element contains <code>code</code> and <strong>strong language</strong>.
// => "

innerText

赋值操作:先将 ASCII 实体对应的字符(<、>、&)转换为实体名,然后把处理后的值赋予给 innerHTML 属性。

取值操作:innerText 的取值实际上就是对 innerHTML 的属性值进行一系列处理,然后返回,具体步骤如下

  1. 对 HTML 标签进行解析;

  2. 对 CSS 样式进行带限制的解析和渲染;

  3. 将 ASCII 实体转换为对应的字符;

  4. 剔除格式信息(如\t、\r 和\n 等),将多个连续的空格合并为一个。

注意,innerText 返回浏览器渲染的内容,具体来说:

  1. innerText 会忽略行内样式和脚本。

  2. innerText 只返回可见的元素。

  3. 会触发 reflow。

x.innerText
// => "Warning: This element contains code and strong language."

textContent

赋值操作:先将 ASCII 实体对应的字符(<、>、&)转换为实体名,然后把处理后的值赋予给 innerHTML 属性。

取值操作:textContent 的取值实际上就是对 innerHTML 的属性值进行一系列处理,然后返回,具体步骤如下

  1. 对 HTML 标签进行剔除;

  2. 将 ASCII 实体转换为相应的字符。

注意:

  1. 对 HTML 标签是剔除不是解析,也不会出现 CSS 解析和渲染的处理,因此<br/>等元素是不生效的。

  2. 不会剔除格式信息和合并连续的空格,因此\t、\r、\n 和连续的空格将生效。

  3. textContent 会原样返回行内样式和脚本。

  4. 不会触发 reflow。

x.textContent
// => "
// =>   Warning: This element contains code and strong language.
// => "

实现

了解了上面说的几个 api,我们就可以动手实现实体字符转义了:

//仅限于包含`&、<、>`的文本转换
function stringToEntity(str){
  var div=document.createElement('div');
  div.innerText=str;
  var res=div.innerHTML;
  return res;
}

其实除了 innerText,还可以通过创建文本节点的方式来完成转义,即使用 document.createTextNode(),完整版代码如下:

// 将HTML转义为实体
function escape(html){
    var elem = document.createElement('div')
    var txt = document.createTextNode(html)
    elem.appendChild(txt)
    return elem.innerHTML;
}
// 将实体转回为HTML
function unescape(str) {
    var elem = document.createElement('div')
    elem.innerHTML = str
    return elem.innerText || elem.textContent
}

这种方法有个缺陷是只能转义“< > & ”,对于单引号,双引号都不转义。另外一些非 ASCII 也不能转义。利用浏览器内部 API 就行了转义和转回(主流浏览器都支持),不具完整性,很明显只能在浏览器环境中使用(比如不能在 Node.js 中跑)。

原理

会自动对文本中存在的 HTML 语法字符(小于号、大于号、和号)进行编码的节点的 innerText 属性。其原理是设置 innerText 会生成当前节点的一个子文本节点(这里相当于调用了 createTextNode),而为了确保只生成一个子文本节点,就需要对文本进行 HTML 编码。innerHTML 虽然也可以做到,但它转变的只是标签中的文本。下面的例子展示了它们的不同。

var div=document.createElement('div');
div.innerText='<p>hello & world</p>';
div.innerText //<p>hello & world</p>"
div.innerHTML //"&lt;p&gt;hello &amp; world&lt;/p&gt;"

div.innerHTML='<p>hello & < world</p>'
div.innerHTML //"<p>hello &amp; &lt;  world</p>"
div.innerText //"hello & < world"

从上面例子中可以看到二者的区别:innerText 会将所有的文本转义(当然也不是全部文本,比如空格就不会),innerHTML 则是对标签內的文本进行转义,标签如<p>就不会转义,但孤立的小于大于号还是会进行转换的。

总结

所谓 HTML 编码其实就是将字符转换为 HTML 实体,这是防止脚本注入的重要手段之一。

在实际开发过程中,我们会使用成熟的 xss 过滤库,而其本质离不开本文所说的方法,那么在阅读完本文后大家有没有对这块知识更加对深入理解一点呢。