IOS9兼容性问题解决: Attempting to change configurable attribute

问题出现

快报前方测试传来情报:IOS版在IOS9系统下没法请求和展现文中广告!html

排查和定位

  • 首先确认bug出现环境:老机型 IOS9,其余高版本的IOS机型正常
  • 排除法缩小问题范围:请远在北京的这位测试同窗经过HTTP代理抓包的方式查看是否拉取了咱们的jssdk,以及是否发起了广告拉取请求。结果是:js已拉取,但下一步的ajax请求未发出;这便说明问题确定出如今jssdk的加载或执行过程中了。
  • 检查了 babel 编译配置,目标 broswser 中写的是 "latest 3 safari version",经检查的确不包含 safari9,因而改为 "last 10 safari version",然而让测试测验后并不能奏效。
  • 请求IOS终端同窗帮忙查看终端日志,寻找js报错的缘由。

    这里因为 webpack 默认的打包方式会将模块打包为 eval() 执行块,很是不利于定位代码具体位置。所以我将 webpack 打包配置的 devtool 修改成 "source-map", 这样打包出来的js基本跟源码一致。webpack

    最终,终端同窗给出报错日志以下:ios

image.png

报错信息为: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)"

image.png

仔细观察错误堆栈,发现问题出如今源码 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 都不可用:

image.png

所以,果断换一个实现思路:经过 innerHTML 把dom字符串转换为一个父div的子dom节点,而后经过父div的 firstElementChild 方法把这个dom节点拿出来:

const tmp = document.createElement('div')
tmp.innerHTML = renderedHtml
const frag = this.adEl = tmp.firstElementChild

而firstElementChild的兼容性就好多了:

image.png
至此,问题算解决了。

总结

不一样版本的浏览器的确会有不少细节上不一样的实现,咱们写代码时最好多注意些:

  * 对于已知的差别,作好特性检测和兼容

  * 对于未知的,尽可能写代码时防患于未然。例如本文的场景下,就要记得不要采用跟一些保留字冲突的属性名,很明显:假如基础知识更扎实一些便不会犯下错误。

  * 对于一些较偏门的 API (尤为是从网上抄来的),要最好去查一下规范和 can i use 的支持状况

相关文章
相关标签/搜索