使用 Proxy 添加魔术属性/方法

使用 Proxy 实现魔术属性/方法。

最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

const obj = { name: 'meathill' };
new Proxy(obj, {
  get(target, property) {
    // 如果对象中有要求的属性或方法,则返回
    if (property in target) {
      return target[property];
    }
    // 没有的话,进行其它处理
    return 'hello';
  }
});

obj.name // 'meathill'
obj.age // 'hello'
obj.sex // 'hello'

接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

参考

  • 阮一峰的 ES6
  • 《深入理解 ES6》

Safari 下 Date 不支持”2018-01-01 00:00:00“

Chrome 可以支持非标准格式时间如 2018-01-01 00:00:00,safari 不支持,所以有时会导致错误。

前两天发现一个小程序的问题,Android 正常,iPhone 出错。我们都知道,Debug 的关键在定位,如果是某些特殊环节,不常见的错误,就会浪费很多时间。

这个 Bug 也是如此,反复拉锯之后终于发现,问题出在下面这句:

let a = new Date(`${date} 00:00:00`);

date 是服务器端返回的值,我是把它和后面的 00:00:00 连起来,记作某天零点零刻,和今天的零点零刻做减法,计算日期差,并按照日期差来决定接下来的逻辑。这段代码在开发工具(包括 Mac)、Android 手机上运行都正常,只有在 iPhone 上不正常,于是我打开 Safari——苹果这点做得不错,桌面版 Safari 环境和 iOS 几乎没有差别,该出的问题一定会出——果然复现了这个问题。

按照规范,中国的日期格式是:“2018/01/01”,Safari 只支持这个格式。而 2018-01-012018-01-01T00:00:00 ISO 格式,Safari 也支持,但是会以格林威治时间为准,和我们有8小时的时差。Chrome 和 Android 内嵌的 WebView(基于 Blink 或者 Webkit)则都支持,所以在本地和 Android 手机上没有问题。

JavaScript 异步开发全攻略

把之前写的《JavaScript 异步开发全攻略》更新了内容,然后放在 Gitbook 上,https://meathill.gitbooks.io/javascript-async-tutorial/content/ 欢迎阅读分享,因为我会不时维护,请关注,star。

之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

JavaScript 异步开发全攻略

bower.json 指定版本

bower 支持 dependencies 里用 “#+版本号” 指定特定版本的依赖。

为了使用部署脚本部署代码而不是登录到服务器上用命令行,我需要尽量简化部署步骤。

相对来说,composer 会简单一些,因为只要提交 composer.lock 文件然后 composer install --no-dev 即可。经测试 bower install --production 它是不会自己去更新了安装新依赖的,只会从缓存里安装。所以需要想个办法。

Value must be a valid semver range, a Git URL, or a URL (inc. tarball and zipball).

按照官网上的描述,只允许版本号、Git URL 或者安装包 URL。然则我试了一下,写 “#+版本号” 也是可以的,所以我们现在是这样:

{
  "dependencies": {
    "tiger-prawn": "#68fa36ceb88c1b0e9b9472b6901d957f424e50b7"
  }
}

在Chrome 扩展中使用 Handlebars

Chrome 扩展中无法直接使用 Handlebars,最好使用预编译功能在开发环境中将模板处理好然后直接使用。

Chrome 扩展可以访问到各种用户敏感数据,比如 cookie 之类,所以 Chrome 团队对它的限制非常严格(见 Using eval in Chrome Extensions. Safely.),比如常规环境下完全不能使用 evalnew Function()。这给我们使用 Handlebars 之类的模板工具带来不小的麻烦。

一个解决方案就是使用上文中介绍的 sandbox。将可能使用到 eval 的代码放到一个独立沙盒中,不让它访问到那些敏感信息;然后通过 postMessage API 与之进行数据通讯,完成模板生成工作。

我觉得这个方案不太理想。首先我不喜欢 <iframe>;其次每次渲染模板还要 post 来 post 去,渲染结果也是异步传递(这点暂时存疑)。

另一套方案,则是利用 Handlebars 的预编译功能,将模板在开发环境中编译成函数,在扩展中直接使用。这样做的好处也很明显,因为发布插件时也会先处理代码,这个时候将模板处理完,工作效率更高。

等写完再补充具体代码吧。

使用 Promise 封装 FileReader

使用 Promise 封装已有的 API 可以写出更好看的代码。本文演示如何封装 FileReader,以及如何使用。

Promise 在处理异步的时候是个很好的选择,可以减少嵌套层次,让代码更好读,逻辑更清晰。ES6 将其加入规范,jQuery 3.0 也修改实现向规范靠拢(3.0 发布公告)。一些新增元素比如 .fetch() 原生就 “thenable”,不过大多数以往的 API 还要依赖回调,这个时候,我们只要将它们重新封装,就能避开嵌套陷阱,享受 Promise 带来的愉悦体验。

Promise 一般用法

先来看下 Promise 的一般用法。

// 声明 Promise 对象
var p = new Promise(function (resolve, reject) {
  // 不管啥时候,该执行then了,就调用 resolve
  setTimeout(function () { 
    resolve(1);
  }, 5000);

  // 或者不管啥问题,就调用 reject
  if (somethingWrong) {
    reject('2');
  }      
});

// 使用 Promise 对象
p.then(function (num) {
  // 对应上面的 resolve
  console.log(num); // 1
}, function (num) {
  // 对应上面的 reject
  console.log(num); // 2
});

Promise 的驱动模型并不复杂:任何操作,假定它只有两个结果,成功或者失败。那么只需要在合适的时间调用合适的程序,进入合适的后续步骤即可。.then() 顾名思义,就是下一步的意思,当前面的 Promise 有了结果——即调用 resolve 或者 reject——之后,就启动对应的处理函数。

Promise 实例创建后就会开始执行,判定结果需要我们自己来,比如加载成功,或者满足某个条件,等等。通过串联 .then() 则可以完成一系列操作。每次调用 .then() 都会创建一个新的 Promise 实例,它会静静等待前面的实例状态改变后再开始执行。

封装 FileReader

接下来开始封装。思路很简单,FileReader 除了提供各种 read 方法,还有几个事件钩子,其中 onerroronload 很明显可以作为判断任务是否完成的依据。加载成功的话,就需要用到文件内容,所以将文件或文件内容传递到下一步也十分必要。

最后完成的代码如下:

function reader (file, options) {
  options = options || {};
  return new Promise(function (resolve, reject) {
    let reader = new FileReader();

    reader.onload = function () {
      resolve(reader);
    };
    reader.onerror = reject;

    if (options.accept && !new RegExp(options.accept).test(file.type)) {
      reject({
        code: 1,
        msg: 'wrong file type'
      });
    }

    if (!file.type || /^text\//i.test(file.type)) {
      reader.readAsText(file);
    } else {
      reader.readAsDataURL(file);
    }
  });
}

为了能真正派上用场,里面还有一些验证文件类型的操作,不过跟本文主旨无关,略过不表。这段代码的核心是创建一个 Promise 对象,等待 FileReader 读取完成后调用 resolve 方法,或者出现问题时调用 reject 方法。

Github Gist 里也放了一份。

使用刚才封装好的函数

接下来就可以在项目中使用了:

reader(file)
  .then(function (reader) {
    console.log(reader.result);
  })
  .catch(function (error) {
    console.log(error);
  });

.then() 支持两个参数,第一个在 Promise 成功时启动,第二个自然在失败时启动。用 .catch() 可以实现同样地效果。Promise 的好处除了可读性更佳以外,返回的 Promise 对象还可以任意传递,继续进行链式调用,有很大想象空间。

继续 .then()

于是我们不妨串联更多操作(本来想写个断点续传的,回头再说吧):

reader(file)
  .then(function (reader) {
    return new Promise(function (resolve, reject) {
      // 就随便暂停个5秒吧……
      setTimeout(function () {
        resolve(reader.result); 
      }, 5000);
    });
  })
  .then(function (content) {
    console.log(content);
  });

总结

这其实是我第一次用 Promise,上次翻译 jQuery 发布公告的时候我它也只是一知半解,对它的解读也糊里糊涂。我很喜欢在业余项目中学习使用新技术,最近开发 Chrome 插件的时候就尝试了一把,感觉不错。使用过程比我想象的复杂也比我想象的简单,这套设计很棒,能解决不少实际问题,也给了我很大启发,将来我应该会把很多地方的实现都做这样的修改。


参考文献

除去第一段的各个链接,还有一些文章值得一看。

ECMAScript 6 入门:Promise 对象

jQuery 3.0 beta 发布

jQuery 官方在10周年之际发布了最新的3.0版本。以下是新版本值得注意的地方。

原创不够,译文来凑。

跟上篇一样是编译,不准备逐字翻。比如,我会把“we”译成“jQuery官方团队”,或者“他们”。

正文开始。


(初译版,待校正)

歪果仁也要双喜临门,于是 jQuery 官方团队选在 jQuery 面世10周年之际发布 3.0 beta。大家还记得上周发布的1.x和2.x小版本更新吧,他们日后会继续维护这俩分支,一段时间,当然只改bug。因为3.0才是未来嘛!

需要支持IE6-8的可怜虫请继续使用1.12分支上的最新版。

没有兼容版了

only-one

看过 alpha 发布公告的同学可能还记得,他们起初准备同时发布3.0和“3.0兼容版”,适配老浏览器。但是现在他们想通了。微软今年1月12日宣布放弃IE8910,jQuery 会保守一些,不过至少不打算支持IE8,所以就放弃所谓的兼容版,以后就只有一个版本了。

尽管大版本号发生变化,jQuery 团队仍然认为升级不会太麻烦。大变化是有,不过影响应该没有很大,而且他们还开发了3.0专用迁移插件,可以帮助我们找到代码中的兼容性问题。所以,请尽早使用新版本,并及时将体验反馈给他们,这样才能让jQuery 越来越好。

你可以直接通过CDN使用:

https://code.jquery.com/jquery-3.0.0-beta1.js

https://code.jquery.com/jquery-3.0.0-beta1.min.js

或者用NPM安装到本地

    npm install jquery@3.0.0-beta1

重点变化

接下来就是需要关注的新功能、升级、以及 Bug 修正了。完整列表见于 Github

.show().hide()

刚启动3.0的时候,他们尝试将这两个方法修改为“删除行内 display:none 样式”(.show())和 “增加行内 display:none 样式”(.hide())。这样可以极大的简化实现所需的代码,并且显著改善性能(计算量大幅下降了嘛)。但是,这给广大用户带来了不小的麻烦,因为移除 display:none 很多时候并不能让元素显示出来,比如有其它CSS将它置为隐藏。最终 jQuery 团队不得不承认没有办法完成期望中的简化。

于是他们放弃了这次尝试。不过,即便如此,他们还是想办法改善了隐藏大量元素时的性能。

.data() 的注意事项

为了兼容 HTML5 dataset 规范,jQuery 团队升级了 .data() 实现。如今所有的 key 都会从短线连接(a-bc-de)转换成驼峰式(aBcDe),数字不再转换。于是,“foo-bar”转换后和“fooBar”是一样的,但“foo-42”和“foo42”就不一样。当用户直接使用 .data() 取所有数据时,就需要注意这个区别,尤其不要再误用 .data('foo42') 取代 .data('foo-42')

问题汇报处

jQuery.Deferred 现在兼容 Promises/A+

Promise 我用的比较少,看到的文档也不多,不太清楚里面的几个名词怎么翻译,所以我尽量用括号备注。

jQuery.Deferred 得到升级,兼容 Promises/A+ 和 ES2015 Promises,并且已经通过 Promises/A+ Compliance Test Suite 认证。这意味着 .then() 的使用机制发生了非常显著的变动:

  • .then() 回调函数里抛出的异常,会成为失败(rejection)处理函数的参数。之前,异常会冒泡,中断函数执行,并永久性锁死上下级 Deferred 对象。
  • .then() 返回的 Deferred 对象,如果它的回调函数抛出异常,将会调用失败(rejection)处理函数,并作为参数传进去;如果返回其它不能继续 .then() 的对象,就会调用成功(fulfillment)处理函数,返回值也作为参数传进去。以前,失败处理函数返回任何值都会将其置为失败。
  • 回调函数将固定为异步执行。以前它们在绑定或者解决时会被立即执行。
  • 进度的回调函数不会再把它绑定的 Deferred 对象标记为完成。

以下代码演示当上级 Deferred 触发 rejected 时,下级调用失败回调函数之后的结果:

    var parent = jQuery.Deferred();
    var child = parent.then( null, function() {
      return "bar";
    });
    var callback = function( state ) {
      return function( value ) {
        console.log( state, value );
        throw new Error( "baz" );
      };
    };
    var grandchildren = [
      child.then( callback( "fulfilled" ), callback( "rejected" ) ),
      child.then( callback( "fulfilled" ), callback( "rejected" ) )
    ];
    parent.reject( "foo" );
    console.log( "parent resolved" );

在 jQuery 3.0 中,会先输出“parent resolved”,然后再执行回调函数;然后下级 Deferred 进入失败状态,执行函数,返回“bar”;“bar”被转化为三级 Deferred 的成功,于是输出“fulfilled bar”;接着,抛出错误“baz”,导致三级函数进入错误处理;最后,输出“rejected baz”。如果是之前的版本,下级 Deferred 会认为上级 Deferred 失败,进入错误处理,输出“rejected bar”;并且在未捕获的错误“baz”被抛出后,整个进程立刻被终止;此时,由于三级函数未处理完,“parent resolved”也不会输出。

捕获异常不仅对在浏览器里调试有帮助,在失败后的回调函数中处理它们,也使得代码更加直观合理。请谨记,这也意味着使用 Promise 模式的时候,要至少设置一个函数处理失败。不然的话,所有错误都会被忽略掉。

如果你还想使用以前的代码,可以用 .pipe() 函数替换 .then()。后者虽然已经被标记为不建议使用,但它接口一样,而且会暂时延续之前的逻辑。

我们还开发了辅助调试调试 Promises/A+ 的工具。如果你觉得有些错误好像没触发出来,可以试用之

jQuery.when 函数也升级了,现在可以传入任何支持 .then() 的对象,包括原生 Promise 对象。

https://github.com/jquery/jquery/issues/1722
https://github.com/jquery/jquery/issues/2102

Deferreds 对象增加 .catch() 方法

Promise 对象增加 .catch()方法, 作为 .then(null, fn) 的别名,专门处理失败。

问题汇报处

移除 jQuery.ajax 的 Deferred 同名方法的特殊用法

jqXHR 是 Promise 对象,同时也有一些专有方法,比如 .abort(),用于取消请求。

现在,越来越多的开发者已经在异步中(如AJAX)使用 Promise 模式,如此一来,jQuery.ajax 返回的对象再包含特殊用法就不合时宜了。

success, error, complete(这些将被移除)
done, fail, always(这些应该会保留)

需要注意的是,那些继续存在的回调函数不会有任何变化,只有 Promise 的方法会受影响。

问题汇报处

错误不再被悄无声息地消失

有时候我们难免会想:“window 的偏移值(window.offset)是多少?”等你回过味儿来你会发现这个问题其实挺蠢的,window 怎么会有偏移呢?

过去,jQuery 面对这种情况,从来不会抛出错误,而是尽量返回有意义的值,比如刚才那个问题,就返回 { top: 0, left: 0 }。3.0之后,他们尝试不再什么乱七八糟的代码都兼容,而是直接抛出错误,让用户不要忽略这些问题。大家可以试用 beta 版,看看你的代码有没有“参数无效”之类的错误。

问题汇报处

.width().height().css('width').css('height')都将返回小数

之前,jQuery 取宽高的时候会返回四舍五入之后的整数值。有些浏览器可以返回次像素,比如 IE 和 Firefox,有些用户需要这些精细的数据来调整布局。jQuery 官方团队认为这项变化对大多数人没有影响,不过如果你受此困扰,也请告知他们。

问题汇报处

移除废弃的事件别名

jQuery 1.8之后弃用的 .load.unload.error 方法被正式移除。以后都请使用 .on() 注册事件侦听器。

问题汇报处

使用 requestAnimationFrame 改善动画效果

新版 jQuery 在支持 requestAnimationFrame 的平台上会自动使用它来改善性能。除去 IE9 和 低于4.4的Android,都可以藉此让动画效果更平滑,占用更少的CPU时间,降低移动设备的电力损耗。

其实 jQuery 团队几年前就曾尝试使用这项技术,但是当时遇到很严重的兼容性问题,以致于不得不放弃。如今他们采用新策略,当浏览器 tab 不显示时挂起动画,这样就可以规避大部分问题。不过这样一来,那些必须依赖动画全局实时播放的功能就无法实现了。

.unwrap( selector )

3.0之前,.unwrap() 方法不接受任何参数。如今,用户可以通过传入选择器来移除指定的外部容器。

问题汇报处

jQuery.fn.domManip 不能再使用了

1.12 和 2.2 版本把 jQuery.dir,jQuery.siblingjQuery.buildFragmentjQuery.access,和 jQuery.swap 都修改为私有函数。现在,jQuery.fn.domManip 也一样。它们未来都只允许内部使用,不会被载入使用文档。官方团队认为这样做可以避免用户困惑。

https://github.com/jquery/jquery/pull/2182
https://github.com/jquery/jquery/issues/2224
https://github.com/jquery/jquery/issues/2225

jQuery 自定义选择器提速

拜 Paul Irish 在 Google 的工作所赐,新版 jQuery 可以躲过一些坑,比如前文提到的 :visible,通过减少冗余代码,获得了17倍的速度提升!

但用户仍需小心,即便经过优化,:visible:hidden 选择器仍然会消耗大量的系统资源,因为它们需要浏览器检查元素是否显示在页面上。在最坏的情况下,有可能需要重新计算全部 CSS 样式和页面布局!当然也不是说不要用(不然还写它做甚),只是记得测试一下,看有没有因此导致性能问题。

这项工作他们 1.12/2.2 时就完成了,只不过拿到这里来说。

问题汇报处


原文:jQuery 3.0 Beta Released

的pathname在不同浏览器中表现不同

的pathname在不同浏览器中表现不同。

<a href="download://gamename/游戏名称">Download</a>

在桌面版Chrome 32里,这样的<a>,其pathname会被解析成“//gamename/游戏名称”。

在Android 4.2的WebView里,会被解析成“/游戏名称”。

Nervenet + Backbone

使用Backbone时,配合Nervenet能取得很好的效果。

介绍Nervenet起源时我提到,Nervenet一名很大程度上来自于Backbone。使用Backbone进行开发期间,我体会到很多方便之处,也发现仍有不少障碍横垣在前,于是总结之前的经验,取Robotlegs之长,做出了这个框架。当然,Nervenet并非专门针对Backbone开发,自然也不会依赖它,不过由于设计初衷和后期实现,结合Nervenet使用Backbone时着实有一些优势。

context.createInstance创建实例

我们知道,依赖注入的对象一般都是类的实例;类实例化时,可能会用到一些尚未注入的属性,所以通常需要在注入后调用实例的postConstruct方法,才能彻底完成构造。Backbone为了实现其独特的架构,将真正的构造函数construct隐藏了起来,开发者操作的initialize函数实际上是初始化函数,从执行顺序来讲与上文中的postConstruct基本一致。同时,Backbone的几大基础类都接受传入初始化对象,即new Backbone.View(options);中的optionsoptions中若包含elmodel等属性,将直接用来填充类自身的属性,甚至从页面当中找到dom结构操作。

若采用普通的依赖注入,则需要在每个类中再实现一个postConstruct方法,增加维护成本和迁移成本。使用Nervenet提供的context.createInstance方法,可以在实例化对象时,扫描参数对象,完成注入,只需要保留initialize函数,非常简单,而且可以直接沿用之前的代码。

var view = context.createIntance(Backbone.View, {
  model: '{{$model}}'
});

从代码可以看出,要完成上述功能,还必须实现一个特性:“注入指定类型的对象”。Javascript是弱类型语言,代码中不包含对象类型,所以之前的类库只能根据“属性名”注入。对于Backbone来说,modelcollection这些都是特有的关键字,要注入的也多是它们,而要注入model,就必须先map一个值到model这个key中——这意味着同一组map只能给单一Backbone.View对象注入依赖,在实际开发难以想象。Nervenet的优势在于可以由“属性值”指定注入的内容,这样不同的View,可以指定不同的model,问题迎刃而解。

// 给不同的view注入不同的model
context.createInstance(Backbone.View, {
  model: '{{$user-model}}'
});

context.createInstance(Backbone.View, {
  model: '{{$job-model}}'
});

infuse.js的解决方案是“子注入器”,通过创建子注入器,构造不同的map,就可以对不同对象注入不同的值了)

mediatorMap管理mediator

了解Backbone的人都知道,虽然名为View,但其实Backbone.View更接近一般意义上的Mediator,而通常意义上的View则由HTML+CSS负责实现。我通常使用Backbone.View开发组件,组件拼接起来就是完整功能的页面嘛。所以我就设计了一个功能来帮助我管理mediator,就是context.mediatorMap,它主要提供三个方法:

.map(selector, MediatorClass[, options]),通常和.check(dom)连用。前者将选择器和类关联,后者则检查某个DOM节点下是否有符合选择器的节点,并为它们自动创建mediator

// 先这么注册一下
context.mediatorMap.map('.checkbox', MyCheckbox, options);

// 然后就可以用了
context.mediatorMap.check(document.getElementById('my-form'));

.createMediator(dom, MediatorClass[, args]),直接为某DOM节点创建mediator

// 直接为DOM创建mediator
context.mediatorMap.createMediator(document.body, MyGUI);

创建mediator的过程基本相同,自动注入依赖,并把目标DOM节点会作为第一个参数,或者第一个参数对象的el属性传给mediator的构造函数——后者当然是为了兼容Backbone。

Backbone基于jQuery,所以操作一个DOM和操作一组DOM对它来说没有什么区别,留意到这点后,我在Nervenet中也作相应照顾,可以通过options设置isSingle=true;表示使用者希望使用一个实例来管理所有符合要求的节点。默认false,会针对每个DOM节点创建独立的mediator

可惜的是,HTML没有提供方便的手段获取DOM节点的变化,所以暂时只好使用手工检查了;另外,querySelectorAll这个方法也存在一定的兼容性问题,所以,目前mediatorMap仍然只是0.1.2版的测试功能,以后可能会有各种改动。

jQuery append script在chrome下无法触发load事件

在chrome下加载外部js,应使用document.head.appendChild,用jQuery可能导致load事件不触发。

尼玛折腾了一早上,居然这样写会导致事件无法触发:

var script = document.createElement('script');
script.src = 'somefile.js';
script.onload = function () { console.log('xx'); };
$('head').append(script);

最后一句换成纯js就解决了:

document.head.appendChild(script);