Vue兼容IE的研究、實踐背景
公司前端框架使用的Vue.js,因為它基於Object.defineProperties來監聽數據變化,而IE8是不支持Object.defineProperties的,所以註定不能兼容IE8。
Vue作者尤雨溪也無意讓Vue支持IE8,見
vuejs/v2.vuejs.org#50
目前Vue.js我們只用在後台界面,網站前台由於要兼容IE8,使用的是jQuery或Backbone,造成技術棧不統一,工程化不夠。
為此想尋求Vue.js兼容IE8的方案。
github上搜索"vue IE8"
找到一個項目
https://github.com/wcflmy/VM4IE8
這個項目的數據監聽是通過一個把一個model的數據,附加到一個dom元素上,
然後ie8下使用Object.defineProperty來監聽dom上屬性的改變
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#Internet_Explorer_8_specific_notes
從Mozilla的資料上來看,有兩大問題,
一個問題:因為model是附在dom上的,會有很多的dom屬性,不能再用for遍歷,
二個問題:數據鍵名要避開所有的dom屬性名,
綜上:基本等於沒有用
另外一個未完成的項目,如
https://github.com/wusfen/vm
https://github.com/wusfen/vue-ie
發郵件問了作者,他用的類似Angular的檢測機制,對所有的異步的函數、方法,進行複寫、注入 $foceUpdate ,如 ajax, setTimeout, Image.onload, ... ,基本覆蓋常用情況。
不太喜歡Angular裡面的zone.js對各種異步函數的污染,在實踐中有遇到過因為Promise被污染導致某些庫不正常(axios?)的情況。
https://www.cnblogs.com/rubylouvre/p/3598133.html
感覺還是Avalon里用到的VBScript方案更靠譜一些
要注意的是
value作為VBScript的特殊值也不能使用作屬性名。
在網上還有一個模板解析器用到了VBScript來作defineProperties數據檢測
https://github.com/purplebamboo/pat/blob/master/build/latest/pat.js
應該是有借鑑Avalon
至此定下方向,使用VBScript實現setter/getter來代替Object.defineProperties來監聽數據變化
在github上再搜索了一些VBScript實現setter/getter的資料
http://webreflection.blogspot.com/2011/03/rewind-getters-setters-for-all-ie-with.html
https://gist.github.com/jeffrafter/189354
有人提到在dojo里就用了VBScript實現setter/getter,
由此看來這不是Avalon的首創。只是很奇怪dojo把VBScript在iframe里去執行,為什麼不用exeScript?
https://download.dojotoolkit.org/release-1.9.7/dojo-release-1.9.7/dojox/lang/observable.js.uncompressed.js
由於Vue1的代碼邏輯比Vue2更直觀,並且沒有使用虛擬dom,有利於debug,所以我計劃先實現Vue1兼容IE8,下次再實現Vue2兼容IE8。
VBScript實現setter/getter測試將Avalon中的 createViewModel 抽取出來,
在IE8下寫了一個頁面測試雙向綁定,成功!
將Pat.js中的define方法抽取出來,
在IE8下寫了一個頁面測試雙向綁定成功,成功!
然後是vue在不管雙向綁定的情況下,在ie8下跑起來,
各種要shim的方法,
包括但不限於以下
IE8下Array實例沒有forEach等方法
IE8下String實例沒有trim等方法
IE8下Function實例沒有bind等方法
IE8下Object沒有keys等方法
在測試中發現IE8下dom元素的cloneNode方法有bug,多個text子節點只clone了一個
IE8下input元素沒有change事件
當然還有最重要的一個,IE8下沒有Object.defineProperties
先找一個es5-shim 把數組等基本對象的成員方法補齊。
https://github.com/es-shims/es5-shim
由於一般的es6-shim庫,代碼量大,而且對Object.defineProperties的實現根本不實用,所以vue1中涉及的es6特有的方法,我要一個個補齊,而不再引用某個庫。
先找了 es5 shim es6 shim 參考
https://github.com/seamus-oconnor/lift-js/tree/master/src/modules/es5/object
https://github.com/paulmillr/es6-shim/blob/master/es6-shim.js
補齊了以下幾個方法/屬性
Object.freeze //無法實現,僅不拋錯 Object.isExtensible //無法實現,僅不拋錯 Object.create Object.getOwnPropertyNames Object.assign Element.prototype.addEventListener Element.prototype.removeEventListener Event.prototype.target Event.prototype.stopPropagation Event.prototype.preventDefault Object.defineProperties // 對於非dom元素,還需要調用 Object.definePropertiesByVBS才返回有監聽屬性改變的VBScript對象 Object.defineProperty // 對於非dom元素,還需要調用 Object.definePropertiesByVBS才返回有監聽屬性改變的VBScript對象
至此vue.js在IE8下載入時不再拋出腳本錯誤
最關鍵的一步,實現Object.definePropertiesByVBS上面的實現過程中,Object.defineProperty和Object.defineProperties被實現為僅配置一個js對象有哪些屬性要被監聽,調用 Object.definePropertiesByVBS 後,會返回一個新的VBScript對象,這個對象上會監聽屬性的改變。
注意:是新的VBScript對象,不是原來的js對象
Object.definePropertiesByVBS的大體邏輯如下
0 Then', '\t\t[' + key + '] = [__proxy](me, "get", "' + key + '")', '\tEnd If', '\tOn Error Goto 0', '\tEnd Property' ) } buffer.push('End Class') buffer.push('Function ' + className + 'F(proxy, cb_poll)', '\tSet ' + className + 'F = (New ' + className + ')(proxy,cb_poll)') buffer.push('End Function') window['parseVB'](buffer.join('\r\n')) var re = window[className + 'F'](proxy, cb_poll) return re }">
window.execScript(['Function parseVB(code)', '\tExecuteGlobal(code)', 'End Function'].join('\r\n'), 'VBScript') Object.defineProperty = function (obj, prop, desc) { obj.__defindeProperties__[prop] = desc } var VB_ID = 0 Object.definePropertiesByVBS = function (obj) { var cb_poll = {} var className = 'VB' + VB_ID++ buffer.push('Class ' + className) for (var key in obj.__defindeProperties__) { var desc = obj.__defindeProperties__[key] cb_poll[key + '_set'] = desc.set buffer.push( '\tPublic Property Let [' + key + '](value)', '\t\tCall [__proxy](me, "set", "' + key + '", value)', '\tEnd Property', '\tPublic Property Set [' + key + '](value)', '\t\tCall [__proxy](me, "set", "' + key + '", value)', '\tEnd Property' ) cb_poll[key + '_get'] = desc.get buffer.push( '\tPublic Property Get [' + key + ']', '\tOn Error Resume Next', '\t\tSet [' + key + '] = [__proxy](me, "get", "' + key + '")', '\tIf Err.Number <> 0 Then', '\t\t[' + key + '] = [__proxy](me, "get", "' + key + '")', '\tEnd If', '\tOn Error Goto 0', '\tEnd Property' ) } buffer.push('End Class') buffer.push('Function ' + className + 'F(proxy, cb_poll)', '\tSet ' + className + 'F = (New ' + className + ')(proxy,cb_poll)') buffer.push('End Function') window['parseVB'](buffer.join('\r\n')) var re = window[className + 'F'](proxy, cb_poll) return re }
上面只是核心代碼示例,僅實現為根據配置返回一個可監聽屬性改變的VBScript對象。
要讓vue實例可以執行原型vue原型鏈上的方法,還要將vue原型鏈上的方法複製到這個VBScript對象上。
對Object.definePropertiesByVBS作出了改進實現了方法 Object.VBVueFactory 來生成vue實例(實際上是VBScript對象)
用這個修改版的vue.js實現了一個todolist,中間排查了若干問題,最終成功跑起來。
至此,完成兼容IE8的Vue.js,使用上與原版Vue.js並沒有區別。耶!