快报前方测试传来情报:IOS版在IOS9系统下没法请求和展现文中广告!html
这里因为 webpack 默认的打包方式会将模块打包为 eval() 执行块,很是不利于定位代码具体位置。所以我将 webpack 打包配置的 devtool 修改成 "source-map", 这样打包出来的js基本跟源码一致。webpack
最终,终端同窗给出报错日志以下:ios
报错信息为:Attempting to change configurable attribute。但因为是编译后polyfill以后的代码,由于较难判断出来是谁形成的。只看到报错的函数为:_definePropertyweb
通过仔细阅读报错消息,咱们能够得出结论:这是由于咱们修改了一个 unconfigurable 的属性。ajax
咱们知道,在 ES5 中,JavaScript 提供了一个 Object.defineProperty 的方法,从而能够定义属性的 descriptor;而对于定义为 "configurable: false" 的属性来讲,它是没法被修改的(特指经过Object.defineProperty再次修改描述,或经过 delete 运算符删除),而对于定义为 "writebale: false" 的属性来讲,是指的它没法被赋值运算符"="来修改。浏览器
那么,很明显咱们的错误提醒说明咱们的代码中作了 Object.defineProperty 或 delete 一个不可更改的属性的操做。因而,咱们看看是谁调用了 _defineProperty 这个函数,最终找到bundle.js中这么两句代码:babel
_defineProperty(KbArticleCenter, "name", 'kb-article-center'); _defineProperty(KbArticleCenter, "instances", []);
其中 KbArticleCenter 在个人源码中是一个 class ,而 name 和 instances 是两个类静态成员。源码以下所示:dom
class KbArticleCenter { static name = 'kb-article-center' static instances = [] // ......... 省略一堆类的成员定义代码 }
难道说:类的静态成员在 babel 编译以后,会出现不兼容 IOS9 的状况? 带着疑问我去搜索了 plugin-proposal-class-properties 插件的issue,但并无收获。函数
最后,仍是回到编译后的代码来查看,突然间恍然大悟,咱们知道:一个 class 类在 babel 编译后实际上会转换为一个普通的 JavaScript 函数,以下:测试
function KbArticleCenter(options) { // ..... 省略一坨构造函数代码 this.init(); }
而咱们的静态成员则会被经过 Object.defineProperty 的方式直接添加到该函数自身上面。例如咱们在类型中定义的 static name 属性则被转变为: _defineProperty(KbArticleCenter, "name", 'kb-article-center');
然而,别忘了,对于 JavaScript 函数来讲,它自身便拥有一个同名的 name 属性,咱们这里若是又经过 defineProperty 的方式重写它,则意味着必需要求原来的 name 属性是能够 configurable 的 (即 configuable: true)。
在正常的现代浏览器中,咱们一个 JavaScript 函数的 name 属性其实默认 configuable 是 true 的。例如以下代码的输出结果中显示 name 是可 configurable 的:
var foo = function() {} Object.getOwnPropertyDescriptor(foo, 'name') // configurable: true // enumerable: false // value: "foo" // writable: false
然而,我深入怀疑在 safari9 当中,name 属性是 uncofigurable 的。因为没有测试机,因此直接将 name 属性改为 compName,从新打包交给测试验证!
交给测试验证后,终端看日志出现了新的报错:"Unhandled Promise Rejection: NotSupportedError (DOM Exception 9)"
仔细观察错误堆栈,发现问题出如今源码 initDom 函数的 createContextualFragment 位置处。咱们贴出此处的代码:
const frag = this.adEl = document.createRange().createContextualFragment(renderedHtml).firstElementChild
此处代码的功能是基于 artTemplate 渲染出来的dom字符串生成一个原生dom节点,这里的思路是借助了 Range 类型的 createContextualFragment 方法。其中 Range 接口表示一个包含节点与文本节点的一部分的文档片断,经过 createContextualFragment 便可把一段html内容转换为 DocumentFragment 文档片断。
为何不用 document.createDocumentFragment来建立文档片断呢?由于咱们这里是基于字符串建立dom,而不是直接建立dom。
然而,查阅MDN发现,createContextualFragment 是一个实验性的 API,尽可能不要在生产环境使用。事实上咱们发现,整个 Range API 在 ios9 都不可用:
所以,果断换一个实现思路:经过 innerHTML 把dom字符串转换为一个父div的子dom节点,而后经过父div的 firstElementChild 方法把这个dom节点拿出来:
const tmp = document.createElement('div') tmp.innerHTML = renderedHtml const frag = this.adEl = tmp.firstElementChild
而firstElementChild的兼容性就好多了:
至此,问题算解决了。
不一样版本的浏览器的确会有不少细节上不一样的实现,咱们写代码时最好多注意些:
* 对于已知的差别,作好特性检测和兼容
* 对于未知的,尽可能写代码时防患于未然。例如本文的场景下,就要记得不要采用跟一些保留字冲突的属性名,很明显:假如基础知识更扎实一些便不会犯下错误。
* 对于一些较偏门的 API (尤为是从网上抄来的),要最好去查一下规范和 can i use 的支持状况