有时候咱们会有把一整段 HTML 动态塞进页面的需求,例如渲染了一个模板,从服务器端获取了一段广告代码等。通常状况下咱们使用 container.innerHTML
便可。可是当 HTML 中出现 script
标签时,直接使用 innerHTML
并不会执行它。javascript
<div id="test">Hello HTML</div>
<script> document.getElementById('test').innerHTML = 'Hello JS'; </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.min.js"></script>
<script src="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>
标签无一例外没有执行。html
咱们知道经过 appendChild
把 <script>
标签直接塞进页面是能够执行和加载里面的 js 的(JSONP
就是经过这种方法实现的,参见以前的文章:JSONP 的实现 - 知乎专栏。java
因此其实咱们须要作的就只是把全部的 <script>
找出来,而后经过 appendChild
塞到页面里便可。node
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
。react
而下面的 <script>
依赖前面的 <script>
执行加载完成是一个很是常见的需求,由于在正常的静态网页里就是这样的,虽然全部的远程脚本都是异步加载的,但后面的 <script>
会等待前面的加载执行后才开始执行。jquery
为了让异常处理和异步流程的控制更方便,咱们让 runScript
返回一个 Promise,而后只须要一个简单的 reduce
就能够把异步逻辑串联起来:git
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
。github
其实这里有一点和直接渲染不一致的地方,就是脚本的加载也是同步的,后面的脚本会等待以前的脚本执行完才会加载,不过从 js 层面彷佛没有办法解决这个问题。web
熟悉 JQuery 的同窗可能知道 $.html
其实会直接执行里面的 <script>
标签,不过是同步的,在 $.html
的代码中,能够看到 jQuery 判断知足必定条件下直接使用 innerHTML
,随便执行一个 $('body').html(test<script></script>)
而后打个断点,ajax
能够看到这里作了一个简单的正则判断,若是碰到 <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。
除了写进去再用 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
早已经完成,若是有须要,咱们可能要在脚本加载完成后,从新触发一下
setHTMLWithScript(document.body, rawHTML)
.then(() => {
var DOMContentLoadedEvent = document.createEvent('Event');
DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true);
document.dispatchEvent(DOMContentLoadedEvent);
})复制代码
在静态页面中,<script>
标签里若是出现 document.write
,会直接在 <script>
插入的位置写入,这种方法常被用于广告投放脚原本定位本身的位置。
而当咱们在动态插入时文档已经关闭,会直接 write
到整个页面上,若是有必要能够暂时替换 document.write
来实现。
getCurrentScript
是另外一个定位 <script>
标签所在位置的方法,之因此不太经常使用是由于 IE 不兼容它,若是咱们要考虑兼容这个方法新产生的 <script>
标签就不该该往 <head>
里 append,而是插入到原来所在的位置。
以上方法都只是模拟静态 <script>
解析的过程,通常来讲咱们不要求行为彻底一致(毕竟跨域异步加载同步执行这点 JS 就没法模拟),可是能够按照咱们的需求去实现它的行为。
这种方法也只适用于一部分场景,若是有更复杂的 JS 动态加载需求应该考虑使用 requirejs
等 AMD Loader。