解决innerHTML插入js不运行问题

最近改了一个老项目, 里面的页面请求大部分是通过ajax请求后来渲染的jsp页面, 然后再用innerHTML插入到当前页. 但是这就遇到了一个问题, jsp里引入的js库以及一些js代码就无法运行了, 所以就探索了一下innerHTML以及解析js的一些方法

1. innerHTML介绍

有两个功能, 一个是可以获取指定DOM的HTML元素, 另一个就是替换指定DOM的HTML元素

2. innerHTML插入js会发生什么

什么也不会发生, 因为用 innerHTML 插入文本到网页中有可能成为网站攻击的媒介,从而产生潜在的安全风险问题。所以HTML 5 中指定不执行由 innerHTML 插入的 <script>标签。
w3help上说

IE6 IE7 IE8
使用 innerHTML 方法插入脚本时,SCRIPT 元素必须设置 defer 属性。

firefox
先将被插入 HTML 代码的元素从其父元素中移除,然后使用innerHTML插入包含SCRIPT元素的代码,最后将这个元素恢复至原父元素中,则经过此操作后SCRIPT中的脚本可以被执行。

对于实际来说, 我认为存在问题, 所以搜索了其他资料来解决问题

3. 有什么取代innerHTML的方法

3.1 document.write

在有deferredasynchronous 属性的 script 中,document.write 会被忽略,控制台会显示 "A call to document.write() from an asynchronously-loaded external script was ignored" 的报错信息。

3.2 eval

可以用ajax获取外部js脚本, 然后通过eval去加载外部的js脚本和内联js脚本. 但是eval会存在安全问题

3.3 document.createElement

创建script标签对象插入DOM, 接下来也就是用这个方法来实现一个类, 进行html字符串的解析插入

4. 自建InnerHTML类

完整代码: https://github.com/klren0312/ZInnerHTML/blob/master/ZInnerHTML.ts
之所以使用ts, 可以更好的规范类型, 看懂实现的原理

4.1 初始化变量

首先就是初始化三个变量, 用于存放解析的html和js外部文件地址, 以及创建的script标签对象

 globalHtmlArr: Array<string> = [] // 存放除去script的html
 globalScriptArr: Array<string> = [] // 存放 script标签对象的数组
 globalScriptSrcArr: Array<string> = [] // 存放script的src中js文件地址

4.2 工具方法

清空数组方法, 用于清楚缓存数据; 创建guid的方法用于区别创建的script标签对象

  /**
   * @description 清除全局数组
   */
  cleanGlobal () {
    this.globalHtmlArr = []
    this.globalScriptArr = []
    this.globalScriptSrcArr = []
  }

  /**
   * @description 生成guid
   * @return {string} guid字符串
   */
  createGuid() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c: string): string {
      const r: number = Math.random()*16|0
      const v: number = c === 'x' ? r : ( r & 0x3 | 0x8)
      return v.toString(16)
    })
  }

4.3 核心方法set

首先是分割html字符串; 以及创建一个对象数组, text属性用来存放解析出来的js脚本, src用于存放解析出来的外部js脚本文件地址

 const htmlArr: Array<string> = html.split(/<\/script>/i)
 let scripts: Array<{
      text: string,
      src: string
 }> = []

然后是循环分割的html字符串数组, 将js和html字符串分门别类存入缓存变量中

for (let i: number = 0, len: number = htmlArr.length; i < len; i++) {
      // 获取 <script 前的字符串
      this.globalHtmlArr[i] = htmlArr[i].replace(/<script[\s\S]*$/ig, "")
      scripts[i] = {
        text: '',
        src: ''
      }
      scripts[i].text = htmlArr[i].substring(this.globalHtmlArr[i].length, htmlArr[i].length)
      scripts[i].src = scripts[i].text.substring(0, scripts[i].text.indexOf('>') + 1)
      // 正则匹配src后的字符串
      const srcMatch: RegExpMatchArray = scripts[i].src.match(/src\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|([^\s]*)[\s>])/i)
      if (srcMatch) { // 存在src
        if (srcMatch[2]) { // src后面使用双引号
          scripts[i].src = srcMatch[2]
        } else if (srcMatch[3]) { // src后面使用单引号
          scripts[i].src = srcMatch[3]
        } else if (srcMatch[4]) { // src后面没引号
          scripts[i].src = srcMatch[4]
        } else {
          scripts[i].src = ''
        }
      } else { // js代码
        scripts[i].src = ''
        scripts[i].text = scripts[i].text.substring(scripts[i].text.indexOf('>') + 1, scripts[i].text.length)
        // 去除注释代码
        scripts[i].text = scripts[i].text.replace(/^\s*<\!--\s*/g, "")
      }
    }

最后就是, 循环缓存的script数组和html数组, 创建script标签对象, 并插入到指定dom中; 拼接html字符串, 并插入到指定的dom中

let documentBuffer: string = ''
    // 循环插入运行js
    for (let i: number = 0, len = scripts.length; i < len; i++) {
      const script: HTMLScriptElement = document.createElement('script')
      if (scripts[i].src) { // 若是src引入的js
        script.src = scripts[i].src
        script.defer = true // dom加载后加载, 只会在src引入的方式下生效
        if (typeof (this.globalScriptSrcArr[script.src]) === 'undefined') {
          this.globalScriptSrcArr[script.src] = true
        }
      } else { // 反之若是js代码
        script.text = scripts[i].text
      }
      script.type = 'text/javascript'
      script.id = this.createGuid()
      this.globalScriptArr[script.id] = script
      // 添加脚本
      document.getElementsByTagName('head').item(0).appendChild(this.globalScriptArr[script.id])
      documentBuffer += this.globalHtmlArr[i]
      document.getElementById(id).innerHTML = documentBuffer

      // 删除脚本
      document.getElementsByTagName('head').item(0).removeChild(document.getElementById(script.id))
      delete this.globalScriptArr[script.id]
    }

还有收尾工作, 判断是否在html字符串里存在有script标签剩余. 有剩余, 则再走一遍set; 没有, 则插入dom

    if (documentBuffer.match(/<\/script>/i)) {
      this.set(id, documentBuffer)
    } else {
      document.getElementById(id).innerHTML = documentBuffer
    }

结果

demo地址: https://codepen.io/klren0312/pen/zYqMRxy

image.png

参考资料

h3help相关说明
MDN上的innerHTML文档
Run script tags in innerHTML content

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350