内容简介:执行 innerHTML 里的 script
有时候我们会有把一整段 HTML 动态塞进页面的需求,例如渲染了一个模板,从服务器端获取了一段广告代码等。一般情况下我们使用 container.innerHTML
即可。但是当 HTML 中出现 script
标签时,直接使用 innerHTML
并不会执行它。
一个例子
<divid="test">Hello HTML</div> <script> document.getElementById('test').innerHTML = 'Hello JS'; </script> <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script> <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.min.js"></script> <script> ReactDOM.render(React.createElement('div', null, 'Hello React'), document.getElementById('test')); </script>
一个常见的例子里包含普通的 HTML 内容, <script>
里的 inline script,通过 src
引用的外部 script。如果我们尝试直接用 innerHTML
赋值只会得到一个 Hello HTML
。而后面的 <script>
标签无一例外没有执行。
appendChild
我们知道通过 appendChild
把 <script>
标签直接塞进页面是可以执行和加载里面的 js 的( JSONP
就是通过这种方法实现的,参见之前的文章: JSONP 的实现 - 知乎专栏 。
所以其实我们需要做的就只是把所有的 <script>
找出来,然后通过 appendChild
塞到页面里即可。
function runScript(script){ // 直接 document.head.appendChild(script) 是不会生效的,需要重新创建一个 const newScript = document.createElement('script'); // 获取 inline script newScript.innerHTML = script.innerHTML; // 存在 src 属性的话 const src = script.getAttribute('src'); if (src) newScript.setAttribute('src', src); document.head.appendChild(newScript); document.head.removeChild(newScript); } function setHTMLWithScript(container, rawHTML){ container.innerHTML = rawHTML; const scripts = container.querySelectorAll('script'); for (let script of scripts) { runScript(script); } }
执行顺序
当我们尝试用上面的 setHTMLWithScript(document.body, html)
时有一个问题,就是 script
的加载和执行并非同步的,我们会得到一个 Hello, JS
。
而下面的 <script>
依赖前面的 <script>
执行加载完成是一个非常常见的需求,因为在正常的静态网页里就是这样的,虽然所有的远程脚本都是异步加载的,但后面的 <script>
会等待前面的加载执行后才开始执行。
为了让异常处理和异步流程的控制更方便,我们让 runScript
返回一个 Promise,然后只需要一个简单的 reduce
就可以把异步逻辑串联起来:
function runScript(script){ return new Promise((reslove, rejected) => { // 直接 document.head.appendChild(script) 是不会生效的,需要重新创建一个 const newScript = document.createElement('script'); // 获取 inline script newScript.innerHTML = script.innerHTML; // 存在 src 属性的话 const src = script.getAttribute('src'); if (src) newScript.setAttribute('src', src); // script 加载完成和错误处理 newScript.onload = () => reslove(); newScript.onerror = err => rejected(); document.head.appendChild(newScript); document.head.removeChild(newScript); if (!src) { // 如果是 inline script 执行是同步的 reslove(); } }) } function setHTMLWithScript(container, rawHTML){ container.innerHTML = rawHTML; const scripts = container.querySelectorAll('script'); return Array.prototype.slice.apply(scripts).reduce((chain, script) => { return chain.then(() => runScript(script)); }, Promise.resolve()); }
得到预期的 Hello React
。
其实这里有一点和直接渲染不一致的地方,就是脚本的加载也是同步的,后面的脚本会等待之前的脚本执行完才会加载,不过从 js 层面似乎没有办法解决这个问题。
JQuery.html
熟悉 JQuery 的同学可能知道 $.html
其实会直接执行里面的 <script>
标签,不过是同步的,在 $.html
的代码中,可以看到 jQuery 判断满足一定条件下直接使用 innerHTML
,随便执行一个 $('body').html(test<script></script>)
然后打个断点,
可以看到这里做了一个简单的正则判断,如果碰到 <script><style><link>
标签就用 jQuery 自己实现的 append
,继续追踪下去,
显然 jQuery 在这里完全没有考虑 <script>
前后的依赖。对于 inline script 的标签也是直接通过 eval
实现的而不是新建一个插入到文档里。
JQuery 也有几个 issue 讨论是否要按照顺序执行,但最后决定保持现状: Scripts in inner html are not exectuted sequentially in order · Issue #2538 · jquery/jquery 。
其他
createContextualFragment
除了写进去再用 querySelectorAll
把 script 全都拿出来复制一遍外,IE11 以上的浏览器也可以通过 createContextualFragment
直接把 html 转换成 DOM 节点然后 append 到页面上:
var tagString = "<div>I am a div node</div><script>console.log('test')</script>"; var range = document.createRange(); // make the parent of the first div in the document becomes the context node range.selectNode(document.body); var documentFragment = range.createContextualFragment(tagString); undefined document.body.appendChild(documentFragment)
也可以用这种方法来实现上面的功能。
兼容性
上面的代码都只是顺手的探索,没有考虑兼容性方面的问题,例如 IE 不支持 script 的 onload 事件等,可能需要 onreadystatechange
来实现。
DOMContentLoaded
DOMContentLoaded
早已经完成,如果有需要,我们可能要在脚本加载完成后,重新触发一下
setHTMLWithScript(document.body, rawHTML) .then(()=> { var DOMContentLoadedEvent = document.createEvent('Event'); DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true); document.dispatchEvent(DOMContentLoadedEvent); })
document.write
在静态页面中, <script>
标签里如果出现 document.write
,会直接在 <script>
插入的位置写入,这种方法常被用于广告投放脚本来定位自己的位置。
而当我们在动态插入时文档已经关闭,会直接 write
到整个页面上,如果有必要可以暂时替换 document.write
来实现。
getCurrentScript
getCurrentScript
是另一个定位 <script>
标签所在位置的方法,之所以不太常用是因为 IE 不兼容它,如果我们要考虑兼容这个方法新产生的 <script>
标签就不应该往 <head>
里 append,而是插入到原来所在的位置。
总结
以上方法都只是模拟静态 <script>
解析的过程,一般来说我们不要求行为完全一致(毕竟跨域异步加载同步执行这点 JS 就无法模拟),但是可以按照我们的需求去实现它的行为。
这种方法也只适用于一部分场景,如果有更复杂的 JS 动态加载需求应该考虑使用 requirejs
等 AMD Loader。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 命令执行与代码执行的小结
- Go如何保证gorountine执行完毕后继续执行
- [译] 理解 JavaScript 中的执行上下文和执行栈
- 熔断器 Hystrix 源码解析 —— 命令执行(二)之执行隔离策略
- 面试:谈谈你对 MyBatis 执行过程之 SQL 执行过程理解
- 前端进击的巨人(一):执行上下文与执行栈,变量对象
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
iPhone开发实战
2009-10 / 69.00元
《iPhone开发实战》全面探讨了iPhone平台的两种编程方式——Web开发和SDK编程。全书结合示例对这两种编程方式的基本流程、基本原理和基本原则给出了详细而通俗的讲解。在Web开发方面,分别介绍了三个iPhone Web库,即WebKit、iUI和Canvas,并讨论了Web开发环境Dashcode,最后阐述Web应用程序的调试。在SDK开发方面,详细描述其各种组件和功能,包括Xcode、I......一起来看看 《iPhone开发实战》 这本书的介绍吧!
JS 压缩/解压工具
在线压缩/解压 JS 代码
XML 在线格式化
在线 XML 格式化压缩工具