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 的支持狀況

相關文章
相關標籤/搜索