遷移iOS API到前端並實現前後端分離(非Node.js)

風之等待發表於2019-04-10

物件導向

  1. 封裝
    使用ES5的寫法。ES6 class不支援私有屬性,不少瀏覽器暫時不支援ES6語法,雖然有babel,還是很容易搞成和IE8以下不相容,不採用。
function A(){    
  var privateAttr = 'a'    
  /*使用私有屬性的公有方法必須在建構函式中宣告。為了減少記憶體的損耗,可以在建構函式中宣告get/set方法或者在prototype中實現帶有需要使用的    
    私有屬性作為引數的公共方法,然後再在構造方法中宣告對外的公共方法 */   
  this.sayHello = function(){    
      this._sayHello(privateAttr)    
  }    
}    
A.prototype._sayHello = function(privateAttr){    
  console.log(""+privateAttr)    
}
複製程式碼

靜態方法和屬性和其他面嚮物件語言類似,為類物件的屬性和方法。

A.staticAttr  = 10    
A.staticFunction = function(){}    
複製程式碼

另一種方式,減少方法對記憶體的佔用,我越來越傾向於使用這種寫法。

(function () {
       /*map用於存放該類所有物件的私有屬性。需要使用陣列實現的相容補丁。WeakMap也可以是Map/Object。小心記憶體洩露!*/
        var map = new WeakMap()     
        window.A = function () {    
            var data = {    
                a:10,    
                b:20    
            }    
            map.set(this,data)    
        }    
        A.prototype.getA = function () {    
            var data = map.get(this)    
            return data.a    
        }    
        A.prototype.setA = function (a) {    
            var data = map.get(this)    
            data.a = a    
        }    
        A.prototype.getB = function () {    
            var data = map.get(this)    
            return data.b    
        }    
 })()
複製程式碼
  1. 繼承
Function.prototype.extend = function(superClass,publicObject,staticObject) {    
          if(typeof this  === 'function'){    
              if(typeof superClass === 'function' ){    
                  var Super = function(){}    
                  Super.prototype = superClass.prototype    
                  this.prototype = new Super()    
                  this.prototype.constructor = this    
              }    
              //可能刪除以下程式碼    
              if(typeof publicObject === 'object'){    
                  this.prototype.shallowCopy(publicObject)    
              }    
              if(typeof staticObject === 'object'){    
                  this.shallowCopy(staticObject)    
              }    
          }    
}    
function Super(){    
}    
function Sub(){    
  Super.call(this,....)    
}    
Sub.extend(Super) 
複製程式碼
  1. 多型
    由於部分公共方法存放在prototype中,部分存在物件中,建議使用變數存放父類方法,然後在子類方法中利用此變數呼叫父類方法。
function Super(){    
  this.sayHello = function(){}    
}    
function Sub(){    
  Super.call(this,....)    
  var superSayHello = this.sayHello //如果是原型方法則直接呼叫。    
  this.sayHello = function(){    
      superSayHello()    
      ...    
  }    
}    
Sub.extend(Super) 
複製程式碼
  1. 過載
    常用的js過載是利用js的變數可以儲存不同型別的值,在函式內部使用條件判斷變數型別以實現過載。這裡使用在函式名後面加數字進行假過載,以下是對 建構函式BCSView的過載。
export function BCSView(element, style) {    
   //以下注釋程式碼是想讓js和swift的建立物件的語法一致。    
   //swift建立物件語法 var view = BCSView()    
   //js部分類如Object,Date支援這種建立物件的語法,部分類如Set,Map不支援(Chrome下驗證)    
   // 而且多這麼一步會導致所有類都需要如此寫,不但麻煩而且還要包裝系統自帶不支援的類,故不為了swift而swift。 
   // if (!this || this.constructor !== BCSView){    
   //     return new BCSView(element, style)    
   // }    
   Function.requireArgumentNumber(1)    
   this.layer = element    
   if(typeof style === 'object'){    
       style.position = 'absolute'    
   }else{    
       style = {position:'absolute'}    
   }    
   this.setStyle(style)    
   this.subViews = generateSubViews(this.layer)    
   /* 方便除錯 */    
   this.layer.setAttribute('view',this.getClass())    
}    
   
function BCSView1(style,elementType) {    
   elementType = elementType || 'div'    
   var element = document.createElement(elementType)    
   if(this.constructor === BCSView1){    
       /*通過new BCSView1構建*/    
       return new BCSView(element,style)    
   }else{    
       BCSView.call(this,element,style)    
   }    
}  
複製程式碼

html5相容補丁(polyfill)

    從HTML5-Cross-browser-Polyfills 中挑選相容性較好的補丁,並按照IE的版本分別打包。使用時根據需要呼叫browser.applyPatches(PatchEnum.Audio,....),所有可用的補丁名稱存放 在PatchEnum中,其中有不少補丁可以相容IE6。我沒有全部測試過這些補丁,而且還有部分是個人程式碼...以下是驗證過相容到IE6的補丁清單:

  • JSON(預設啟用)
  • ES5(預設啟用,不相容部分語法)
  • Dom2(預設啟用)
  • LocalStorage/SessionStorage
  • Promise
  • fetch(IE67不支援跨域) 和fetchJSONP
  • canvas
  • Video/Audio
  • Css3 BackgroundsBorders(PIE_IE678_uncompressed補丁)
  • Console(預設啟用,防止IE9及以下非除錯時報錯)
  • Transform
  • Placeholder
  • CSS3Filter
  • MathJax

前後端分離

    目前,主流將B/S和C/S看成兩種獨立的架構。本人更願意將B/S看成是一種特殊C/S架構,http協議建立在tcp協議上就是佐證!這也是這個項 目叫做BC/S的原因。BC/S即Browser client/Server架構的簡稱,即將瀏覽器看作和iOS/Android類似的終端(事實也是如此)。它們的開發思路很類似, 以iOS和前端作為對比:html css對應iOS的StoryBoard(StoryBoard是xml檔案)和配置檔案,javascript對應OC/Swift。甚至於javascript有 document.getElementById API,而Android有findViewById....那麼如何在瀏覽器實現前後端分離呢?單頁面應用完全可以做到前後端分離,有問題的 是多頁面應用。能否在單頁面應用的基礎上,將json資料傳遞給新開啟的頁面呢?答案是肯定的。可以用來在頁面間傳遞資料的有localStorage, sessionStorage,window.name。其中localStorage是全域性的不合適;sessionStorage在Chrome開啟新頁面的瞬間有bug(不知道算不算),而且IE6 的sessionStorage使用window.name進行相容,故只能使用window.name。瀏覽器開啟新頁面的API為window.open(url,windowname....)。過程如下:

  • 舊頁面獲取到JSON資料後呼叫BCSViewController.prototype.openWindow方法,將json資料傳遞給新頁面
    (這裡的新頁面可以是在本視窗開啟的新頁面也可以是在新視窗中開啟的新頁面)的window.name
  • 新頁面開啟後呼叫loadPageInfo方法,從window.name獲取JSON資料。
    有沒有可能發生會話上的問題?能否通過瀏覽器cookie相關API解決?

實現程式碼如下:

  • 舊頁面獲取到JSON資料後呼叫
BCSViewController.prototype.openWindow = function (pageInfo,windowName,newWindow) {  
         Function.requireArgumentNumber(arguments,2)  
         Function.requireArgumentType(pageInfo,'object')  
        if(pageInfo instanceof Object){  
            var url,name,object,WindowNameEnum = window.WindowNameEnum  
            if (pageInfo.url) {  
                url = pageInfo.url  
            }else if(pageInfo.pathname){  
                url = location.href.replace(location.pathname,'') + pageInfo.pathname  
            }else{  
                throw new TypeError('invalid url & pathname')  
            }  
            windowName = windowName || WindowNameEnum.SELF  
            newWindow= newWindow ||window.open('',windowName)  
            if(newWindow){  
                if (newWindow.name) {  
                    object = JSON.parse(newWindow.name)  
                }else {  
                    object = {}  
                }  
                object[BCSViewController.key] = pageInfo  
                /*bluebird內部機制未知,但感覺會在呼叫外部函式JSON.stringify時切換以讓出CPU。  
                 此時開啟另一個頁面會導致頁面loadPageInfo執行,目前放在這個位置似乎可以順利執行*/  
                name = JSON.stringify(object)  
                newWindow.name = name  
                newWindow.location.assign(url)  
            }else{  
                throw new OpenWindowException('Failed to open window.Please check if url is correct ' +// jshint ignore:line  
                    'or popup new window function is blocked!')  
            }  
        }else{  
            throw new TypeError('invalid pageinfo') // jshint ignore:line  
        }  
    }  
複製程式碼
  • 新頁面中呼叫
BCSViewController.prototype.loadPageInfo = function (dataURL,errorCallback,method,data) {  
        var pageinfo,key = BCSViewController.key  
        if(window.name){  
            var object = JSON.parse(window.name)  
            pageinfo = object[key]  
            if(pageinfo){  
                try{  
                    delete object[key]  
                }catch(e){  
                    object[key] = undefined  
                }  
                window.name = JSON.stringify(object)  
            }  
        }  
        if(!pageinfo && dataURL ){  
            method = method || window.HttpMethodEnum.GET  
            var request = new XMLHttpRequest()  
            if (request !== null) {  
                /* window.location.search 使用者可能輸入引數 */  
                request.open(method, '' + dataURL + window.location.search, false)  
                request.onreadystatechange = function () {  
                    if (request.readyState === 4) {  
                        switch (request.status) {  
                            case 302:  
                            case 200:  
                                if (request.responseText) {  
                                    pageinfo = JSON.parse(request.responseText)  
                                }  
                                break  
                            default:  
                                errorCallback && errorCallback(request.status, request.statusText)  
                                break  
                        }  
                    }  
                }  
                request.send(data)  
            }else{  
                throw new TypeError('XMLHttpRequest is not supported')  
            }  
        }  
        if(pageinfo && pageinfo.title){  
            document.title = pageinfo.title  
        }  
        return pageinfo  
    }  
複製程式碼

優點:

  1. 在瀏覽器端實現前後端分離
  2. 將頁面動態資料的渲染放在瀏覽器端,降低伺服器的運算負擔。(沒有實際測試過,只是覺得生成JSON資料的開銷總歸比渲染頁面的開銷小)

缺點:
每個頁面多一次請求。
示例:src/main/webapp/WEB-INF/html/bcs1.html

以iOS開發思想編寫前端應用

  1. MVVM
    MVC

    MVVM
        這是iOS很經典的MVC和MVVM設計模式圖,可以應用到Android,理論上也可以應用到html應用。可能寫慣了React或者Vue的朋友會 鄙視它。本人學了點React和Vue的皮毛,依然還是覺得iOS的MVVM比較好(上文的前後端分離的方式可能會讓人覺得Vue不錯),個人看法,拒絕在這個問題 上打口水戰,就是這麼專制。哈哈。尤其想吐槽的是React Redux和VueX。這兩個玩意兒無非是用來存放model資料,卻因為html應用一直在使用函式程式設計 思維,將一個本來使用物件導向的單例設計模式即可解決的問題變成一個需要使用3-4步的框架才能搞定的問題。單例設計模式的意思是建立一個整個應用全 局都能訪問的物件,這個物件是其類的唯一一個例項物件。由於可以通過物件.constructor得到建構函式,目前個人認為較好的單例模式如下:
    a.java 懶漢餓漢隨便挑一個吧,js多執行緒應該沒什麼人用。
(function () {  
    var singleton = new Model()  
    function Model() {  
        if(singleton){  
            throw new TypeError(this.getClass() + ' could be instantiated only once!')  
        }  
        ...  
    }  
    Model.getInstance = function () {  
        return singleton  
    }  
    window.Model = Model //export  
})()  
複製程式碼

b.swift

(function () {  
       var singleton = new Model()  
       function Model() {  
           if(singleton){  
                 throw new TypeError(this.getClass() + ' could be instantiated only once!')   
           }  
           ...  
       }  
       Model['default'] = singleton  
       window.Model = Model //export  
})() 
複製程式碼

c. Symbol方式

  1. 觀察者模式
    iOS有兩種觀察者模式的實現,一個是KVO,另一個是NotificationCenter.default。和iOS類似的js版KVO使用defineProperty實現,其使用範圍和 Vue一樣,區別是KVO是單向的,而Vue是雙向的。KVO只支援公有直接屬性(不支援私有屬性和path)。 不支援defineProperty的瀏覽器如IE8及以下可 以使用NotificationCenter.default,沒有深入研究pubsub,但應該就是pubsub機制,只不過為符合swift語法,才寫成這個樣子。
function Model(){  
  var map = new ListMap()   
  //啟用KVO功能
  //enableKVO是在Object.prototype中的擴充套件,故請放棄使用jQuery的想法,也沒有使用jQuery必要!  
  this.enableKVO(map) 
}  
複製程式碼
  1. BCSView和iOS UIView
    UIView是iOS整個UIKit框架的基石。UIView有個layer屬性。BCS仿照UI框架,BCSView同樣有layer屬性,而這個layer屬性就是html元素。開發者可 以通過呼叫BCSView的API操作layer,也可以直接獲取layer。BCSView的layer的預設css樣式含有position = 'absolute'。 據說這種樣式可以讓 瀏覽器在進行重繪/重排時只針對這個元素,而不會影響到其他元素,契合iOS的UIView,只能說軟體世界中很多原理其實是一樣的。 雖然BCS山寨UIKit 框架,但考慮到實際情況,並不可能完全模仿。例如使用setStyle設定BCSView的外觀,iOS對外觀的設定實在太麻煩了....
    BCSView.prototype.setStyle = function (cssObject) {  
        var cssText = ''  
        for(var name in cssObject){  
            if(cssObject.hasOwnProperty(name)){  
                cssText += name.replace(/([A-Z])/g,function(match){  
                        return '-'+match.toLowerCase()  
                    }) + ":" + cssObject[name] + ';'  
            }  
        }  
        if( typeof( this.layer.style.cssText ) !== 'undefined' ) {  
            this.layer.style.cssText += ';' + cssText  
        } else {  
            this.layer.setAttribute('style',cssText);  
        }  
    }
複製程式碼

cssObject的內容和React類似:

    var cssObject =  {  
                bottom:'0px',  
                width:'100%'  
    }  
複製程式碼

看到這裡,做前端的哥們該吐槽了。本人雖然前端程式碼寫的少,但也看了不少(右鍵就能看谷歌的css我會亂講?^_^),個人認為css其實已經臃腫不堪,即 使使用less,stylus並不能改變現狀,因為html的功能越來越強大,頁面越來越複雜,跟iOS/Android APP並沒有太大差距。為什麼不將特殊樣式直接寫 到元素的內聯樣式中,讓內嵌或者外部樣式只負責通用樣式,例如帶有公司風格的樣式,反而讓內聯樣式白白留空?style.cssText只會讓瀏覽器重繪一次 和設定class效果是一樣的。jQuery對樣式的修改可能也全部都是在內聯樣式中完成的。React和Vue對樣式的處理甚至元件這個概念也是化整為零的思路, 畢竟同一個元件的元素,外觀,js程式碼都在同一個檔案中,方便修改。利用document.body生成的BCSView物件作為所有view的window屬性。

  1. 手勢識別
    已經實現語法和swift類似的手勢識別器,以及NavigationController的基本功能(bar上的按鈕沒實現...)詳見例子 BC-S/src/main/webapp/WEB-INF/html/controller.html。其中test.js的程式碼不是標準的MVVM設計模式,請勿吐槽。在山寨手勢識別時發現iOS 各個手勢識別器的識別機制並不是很一致。猜測iOS的不同識別器的實現程式碼是不同人寫的。我只想盡量貼近iOS,並無改進想法。
  2. 除錯
    使用Firefox Version46 3DView除錯DOM,可以獲得和Xcode類似的體驗,不過很差。

未完成的工作

  • 我還沒想到表示swift協議/代理概念的好方法。目前是想弄個協議/代理的清單,讓開發者自行復制黏貼到程式碼中,再實現相應的方法。
  • 極度缺乏測試。
  • 大部分UI類未完成。
  • IE9以上,低版本Chrome,Firefox,Opera及手機端相容驗證。
  • 驗證dom4.js已經包含KeyboardEvent,classList,document.head等polyfill。
  • 整理補丁和參考程式碼作者清單。
  • 尚未加入svg,webform 驗證和input屬性功能補丁。
  • 驗證patchBackgroundBorder可以修復IE6 PNG圖片透明問題。

這個專案目前還是半成品。我最大的問題是做前端的朋友對這個專案有何看法?有沒有應用到實際專案中的價值? 歡迎想貢獻程式碼和提供測試幫助的朋友。
專案地址:github.com/Ken-W-P-Hua…

相關文章