近幾年來,JavaScript 可謂風生水起,從後端到前端,從 mobile 到 desktop,各種 module 滿天飛,信手拈來就是一個 web app。不過,「沒碰過 IE,別說你會做前端」,本人從超新手的角度出發,整理最近修正 IE 相容性遇到的坑與解法,給自己日後留個參考。

(撰於 2017-07-15,基於 IE 11/Edge 15)

Contents

Issues

Fullscreen breaks my layout!

  • Issue:使用 Web fullscreen API 放大至全螢幕時,整個 document 寬度剩不到一半。
  • Platform:IE 11

第一個先來簡單的 fullscreen API!雖然 fullscreen API 還沒有去掉 prefix,不過這個 bug 說實在也太大了!沒關係,加個 width 解決,簡單明瞭!

html {
  min-width: 100%;
}

Can an image fit its container with aspect ratio?

IE 沒支援一直是 object-fit 的一大硬傷,許多 polyfill 也做得不是很完全,但我們還有歷久不衰的 background style,善用 background-sizebackground-position 就可以做到這些效果。

需注意的是,background options 並沒有 onload 的 callback,許多 progressive image loading 效果可能無法使用。我們可以利用一個 detached <img> element 先讀取圖檔至 memory, onload 時再 assign 到 backgroundImage 中,雖然這樣有點醜,反正你都要 call onload 了,是有差醜這點喔 XD

var preload = new Image()
preload.onload = function () {
  var real = document.querySelector('img')
  real.backgroundImage = `url("${preload.src}")`
}
preload.src = 'uri/to/a/large/image'

Do you know your id?

  • Issue:XMLDocument 缺乏 Element#id 的 API。
  • Platform:IE 11/Edge 15

也就是說,id attribute 對 IE 們來說,並非特殊的 property,僅為普通的 attribute,所以 ID selector #someid,或是 someElement.id 這類 API 通通不能用。

解法嗎?用 attribute 代替啊!

image#myid { display: none; }
/* become */
image[id="myid"] { display: none; }
document.querySelector('img').id
/* become */
document.querySelector('img').getAttribute('id')

Where are my children?

  • Issue:XMLDocument 缺乏 Element#children 的 getter API。
  • Platform:IE 11/Edge 15

這 Issue 滿討厭的,通常我們只想取得 Element,在 IE 卻被迫遍歷 childNodes 過濾出 nodeType === 1 的 node。MDN 上有可用的 polyfill,但本人每次跑 mocha 測試都會拋出 Illegal Invocation,看起來是 this.childNodes getter 的 this binding 錯誤?有點莫名,只好稍微繞點路修改一下,反正都做 polyfill 了,不差這些醜 code。以下做個參考:

// Node#children/Element#children polyfill (for Edge 15, IE 11)
//
// From: https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children
// Overwrites native 'children' prototype.
// Adds Document & DocumentFragment support for IE9 & Safari.
// Returns array instead of HTMLCollection.
;(function (constructor) {
  function childNodesProp (prototype) {
    return Object.getOwnPropertyDescriptor(prototype, 'childNodes')
  }
  if (constructor &&
    constructor.prototype &&
    !constructor.prototype.hasOwnProperty('children')) {
    Object.defineProperty(constructor.prototype, 'children', {
      get: function () {
        var i = 0
        var node
        var nodesProp = childNodesProp(constructor.prototype) ||
          childNodesProp(Object.getPrototypeOf(constructor.prototype))
        var nodes = nodesProp.get.call(this)
        var children = []
        while (node = nodes[i++]) {
          if (node.nodeType === 1) {
            children.push(node)
          }
        }
        return children
      }
    })
  }
})(/* window.Node || */ window.Element) // 加 `Node` 是給 SVG 使用,on demand 囉

Give me some animated SVG

雖然說被偉大的 Google 給 deprecate 了,但也遭受不少反彈聲音,在 Web animation API 還沒普及之前,我們可以利用 inline css animation 來實現簡單的動畫效果。例如無腦的的 loading(首次親手寫 SVG,獻醜了)。

順便提一下,Edge/IE 的 CSS transform 並不支援 SVG,僅能透過 transform attribute 來達成效果。Poor IE!

<svg x="0px" y="0px" width="24px" height="30px" viewBox="0 0 20 20">
  <style type="text/css">
    rect {
      transform-origin: center center;
      animation: 0.5s animate infinite ease-in-out alternate;
    }
    rect:nth-of-type(2) { animation-delay: 150ms; }
    rect:nth-of-type(3) { animation-delay: 300ms; }

    @keyframes animate {
      from { opacity: 0.2; transform: scaleY(1); }
      to { opacity: 0.8; transform: scaleY(2); }
    }
  </style>
  <rect x="0" y="5" width="4" height="10" fill="#333" opacity="0.2"></rect>
  <rect x="8" y="5" width="4" height="10" fill="#333"  opacity="0.2"></rect>
  <rect x="16" y="5" width="4" height="10" fill="#333"  opacity="0.2"></rect>
</svg>

How an element removes itself?

  • Issue:IE 的 DOM Node 缺乏 remove method,無法自殺(自殺不能解決問題,珍惜生命,遠離 IE
  • Platform:IE 11

這個坑大大的掛在 Can I use 裡,但也只有用到才會發現,如果你是 Mac 用戶,調試 IE 很辛苦,最後發現是這種狗屎 bug,這輩子鐵定再也不碰任何 M 社出品的玩意兒(結果卻用 VS Code 打這篇文章 XD)。

解法很簡單,如果 codebase 不大的話,將 el.remove() 改成 el.parentNode.removeChild(el) 即可輕鬆解決。如果你想本人一樣,最近工作上使用 Web Components,必須 import Custom Elements 的 polyfill,寫了 support IE 11,卻偷偷調用 remove API,你玩我嗎?

既然官方沒有任何要修改的動靜,自己加個 Polyfill 總行了吧?

// Element#remove polyfill (for IE 11)
// (MUST import before custom-elements polyfill)
// from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
// Issue discussion:https://github.com/webcomponents/custom-elements/issues/103
;(function (arr) {
  arr.forEach(function (item) {
    if (item.hasOwnProperty('remove')) { return }
    Object.defineProperty(item, 'remove', {
      configurable: true,
      enumerable: true,
      writable: true,
      value: function remove() { this.parentNode.removeChild(this) }
    })
  })
})([Element.prototype, CharacterData.prototype, DocumentType.prototype])

Same CSS, weird flexbox behavior

  • Issue:各種 flexbox 的 bug
  • Platform:IE 11

眾所週知,IE 的 flexbox 實現有許多 bug,本人也遇到不少,幸好碼農們也整理出 bug list 供大家避坑,這裡就不多做解釋了。

(EXTRA) Read the fxxking standard before using custom elements

  • Issue:在 custom elements connected 前,不要對他操作 DOM(appendChild、setAttribute)。
  • Platform:Chrome 54+

這其實不算是 bug,而是一個標準,Chrome 實實在在地做出來了,polyfill 卻沒有檢查這些 requirements。根據 Custom elements constructor 的標準

The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.

我們不能在 constructor 中有任何 attribute/childNodes 的操作,也不應涉及設計 render 相關動作,官方標準建議可以在 constructor 中做:

  • Setup event listeners.
  • Setup default/initial values.
  • Setup shadow root.
  • Truly one-time initializaiton.(因為 connectedCallback 很可能跑不只一次)

實務上,大部分的工作都會延遲到 connectedCallback,尤其當這個 elements 以互動/呈現為主,constrcutor 反而僅剩加 flag 防止二次渲染,以及一些 private field 的設定。

其實 web components 的 polyfill 坑也不少,希望未來有一天能夠普及(但 attribute 不能傳遞 function context 就難受想哭)。

Tools for dealing with compatible issues

為了相容各大瀏覽器,前端工程比想像中更加複雜,往往設計好一些 feature,卻因為相容性問題,被迫對架構做出修正。如果你手上的專案不需要支援這些老舊的瀏覽器,恭喜你,這種機遇不是人人都有!如果你跟本人同樣不幸,只能為了需求妥協,這邊推薦幾個好物參考用,希望能節省各位的加班費。

Compatible tables

有時候不知道某個 feature 在各平台上支援程度如何,一般我們會看兩個網站:

前者專注在 Browser support 的 Web API,許多 CSS 或 DOM 的 feature 都有詳細的資訊,不過有時關鍵字搜尋不易找到。當然還有各種 Plugincommandline tool 可以安裝。

後者就比較陽春一點,沒有實用的搜尋功能,但一些 JavaScript 基礎語法就要靠它了解支援程度。例如 Array 的各種 fuctional methods,我從來都不記得哪個在哪裡有實作。(不過 Babel/TypeScript 大火,還有人在管 JS 相容性嗎 XD)

Cross browser testing services

跨瀏覽器測試是前端工程最為複雜惱人的部分之一,各家瀏覽器在每個平台上都有不同的表現,一般企業很難有齊全的測試環境,所以就有許多 cross browser testing services 興起,比較有名的幾家如:

透過這些跨平台測試工具,可以全面檢測自家的 codebase,減少建置 infrastructure 的負擔,加上他們都有豐富的 API 與文件,整合 CI/CD 更為方便,價格有不會太貴,個人認為開發前端的公司考慮一下。

因為 Mozilla 與 Microsoft 有跟 BrowserStack 合作,目前測試 Edge、Firefox 都完全免費,所以本人就像牆頭草一樣選擇 BrowserStack 了。

Virtual machines

如果買不起上面的 testing services,這邊有個省錢(但很累人)的方法,就是使用 VirtualBox + Windows 10 Insider Preview 測試 Edge 和 IE,Insider Preview 只要註冊就可以用了!

因為怕被黑,細節就不多講,還是建議大家直接使用正版,或是買別人的測試服務吧。

Conclusion

俗諺有云:「IE 是前端工程的墳墓」,與 IE 交往,切勿急躁,想清楚,多溝通,有時候她鬧點彆扭就讓著吧,不要疾病亂投 polyfill,破壞你與 IE 之間的愛架構絕對更痛苦。

往後若有其他 IE 相容性的 Issue,本文會再繼續更新。