相关文章推荐

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>.
var x = document.getElementById('test');

innerHTML

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

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

以下情况将被保留:

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

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

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

HTML 标签。(如<img>)

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

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

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

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

    innerText

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

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

    对 HTML 标签进行解析;

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

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

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

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

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

    innerText 只返回可见的元素。

    会触发 reflow。

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

    textContent

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

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

    对 HTML 标签进行剔除;

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

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

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

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

    不会触发 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 过滤库,而其本质离不开本文所说的方法,那么在阅读完本文后大家有没有对这块知识更加对深入理解一点呢。

     
    推荐文章