HybridStart混合應用開發框架

轉自我的博客,原文地址:http://refined-x.com/2017/06/26/%E5%9F%BA%E4%BA%8EAPICloud%E7%9A%84%E6%B7%B7%E5%90%88%E5%BA%94%E7%94%A8%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6/

HybridStart提供什麼

開發模式

如果是跟我一樣之前從未接觸過APP開發的前端,我認爲首先需要知道的是,APP不同於web的地方是需要很多初始化操作,比如判斷是否已登錄、數據預取、檢查更新、註冊推送、註冊全局監聽等等,經過這個過程後APP才能打開第一個頁面,進入頁面的生命週期。

APICloud裏有一個非常重要但官方沒怎麼強調的概念叫根頁面(root),就是APP啓動後第一個打開的那個頁面,這個頁面非常特殊,相當於其他所有頁面的父頁面,它被關閉了意味着APP退出,他無法被其他頁面調用關閉方法關閉,它是到達其他頁面的必經之路。綜合這些特徵,這個頁面非常適合用來做APP初始化,初始化完成後再立即切換到首頁或者登錄頁,這時用戶看到了第一個頁面,但實際上是APP打開的第二個頁面。

APP啓動後root頁就常駐後臺,對於安卓機還需要在可能返回到root頁的頁面上做返回鍵攔截,提示退出APP而不允許返回到root,因爲root是個只有js代碼的空白頁。那麼混合應用的頁面生命週期就應該是:

root -> index(exit) <=> page <=> page ...

 

開發中我們第一個要實現的就是root頁的初始化功能,比如檢查登錄狀態,然後決定是跳轉到登錄頁還是主頁,然後再去實現登錄頁 or 主頁。

APP的數據交互幾乎全部依靠後端接口,因此很有必要事先約定一個交互格式,方便統一做異常處理。比如最簡單的先把json的大結構定下來,起碼狀態、數據、提示信息字段都得有,對於列表數據還需要一個信息總數字段,這樣下來一個基本的交互格式就像這樣:

{
    "status": "Y",      //請求的狀態 "Y"/"N",也可以根據情況擴展其他
    "data": [{...}],    //請求的數據 數組或對象
    "msg": "",          //【可選】服務端提示信息
    "count": [number]   //【可選】當獲取列表數據時,需附加count數據指明列表總數,用於前端分頁
}

 

這樣我們就可以封裝一個數據請求方法,在方法裏對某些情況做自動處理,比如當發現status不是"Y"的時候就自動提示msg字段的信息,就不用在每一個業務邏輯裏寫錯誤處理了。

代碼組織

稍微複雜點的APP有個幾十近百的頁面很正常,所以APP代碼組織首先要解決的是頁面組織。

頁面肯定得放在一起管理,但又不能直接羅列在一起,那就先建一個view/文件夾,然後按功能模塊分二級文件夾,把會員相關頁面都放進member/,商品頁面都放進product/……;頁面的腳本和樣式也不希望內聯,最好每個頁面對應模板、樣式、腳本三個文件,那就將他們三個也裝進文件夾,以頁面名稱命名。這樣頁面文件就形成了channel-page-pagefile的結構,目錄就變成了這樣:

view/
 |--- member/               //會員欄目
 |      |--- info/              //會員信息頁
 |      |        |--- temp.html
 |      |        |--- style.css
 |      |        `--- script.js
 |      `--- set/               //會員設置頁
 |              |--- temp.html
 |              |--- style.css
 |              `--- script.js
 |      
 |--- home/                 //APP首頁
 |      |--- temp.html
 |      |--- style.css
 |      `--- script.js
 ...

 

這樣即使有再多的頁面,找起來也有跡可循,不至於在文件堆裏看花了眼,將頁面樣式和腳本拆分出來也是爲了開發方便,因爲頁面代碼一旦很長,上上下下的巴拉css和js也挺痛苦的,不如拆開乾淨利索,反正都是本地文件,幾乎沒什麼加載問題,將頁面用文件夾的形式管理還有一個好處,就是可以將頁面的獨有資源放在各自文件夾內管理,比如圖片就不需要全部丟進公用文件夾了,將來打開一看一大堆圖片,都分不清哪個有用哪個沒用。

然後是腳本組織,APP開發需要寫大量的js,組織js的目的就是層層過濾,將非業務代碼過濾出去,使注意力可以更多的放在業務腳本的開發上。

首先我們肯定要將類庫剝離出來,在類庫和業務之間再劃分出插件、服務、公用腳本。

公用腳本就是類似返回按鈕的監聽、圖片點擊的監聽、兼容性處理等,每個頁面都得引用它(除了root),可以把他們都抽到common.js裏,方便統一修改;還有一些業務上常用的方法,比如格式化、查座標等等,不是每個頁面都能用到,但也很有必要集中在一起管理,暫且就叫他server.js;另外還有一些插件類的腳本,比如上傳、表單驗證,這種就分別封裝成模塊,一起放進modules/文件夾;最後是類庫,也是框架的核心,我們稱之爲core.js,這裏面放的是常用類庫以及對引擎接口做二次封裝,二次封裝至少有三個好處,一是可以精簡api,如果看過APICloud的文檔感覺還好的話,建議去看一下Appcan的文檔,那醉人的api設計,簡直欲仙欲死;二是底層引擎的api假如更新了,不需要修改業務代碼,只改core.js中對應的封裝就好了;三是便於更換底層,實際上這個框架的雛形就是基於Appcan實現的,後來棄坑轉到APICloud無非就是換了一套底層api,框架自身api沒有大的改動。

最後剩下的就是散落在各個頁面裏的script.js了,那麼最終的腳本組織是這樣的:

|--- sdk/
|       |--- modules/
|       |      |--- upload.js
|       |      |--- ...
|       |--- core.js
|       |--- server.js
|       `--- common.js
|--- view/
|       |--- page/
|       |       |--- script.js
|       |--- ...

 

css以及其他靜態資源的組織就很簡單了,沒必要細講,再上一個完整的目錄結構吧:

  |-- docs/                 //文檔(不需要上傳打包平臺)
  |-- error/                //app錯誤頁
  |-- res/                  //app靜態資源(圖片、模板等)
  |-- sdk/ 
  |    |-- modules/             //插件模塊
  |    |-- font/                //字體圖標
  |    |-- core.js              //核心庫
  |    |-- server.js            //業務方法
  |    |-- common.js            //頁面公用代碼
  |    `-- ui.css               //公共樣式
  |-- view/                 //app頁面
  |-- config.js             //框架配置
  `-- config.xml            //APICloud配置

 

技術棧

js分的這麼零碎肯定離不開模塊化,因此整個項目是基於seajs實現的模塊化加載;DOM操作用的jQuery 2.x,很多人覺得做混合應用還上jQuery太low,我要說多webview模式讓混合應用真的很像一個網站,DOM操作少不了,當然你大可換成zepto或自己封裝幾個方法去用,我覺得差別不見得有多大,都是本地資源差個幾KB有區別嗎;模板引擎用的etpl,這個很有用,大量的異步數據渲染,沒有模板引擎不行。

類庫都是直接將壓縮後的代碼放進core.js頂部,理論上可以隨意增刪改,但上述三個類庫在其後的app對象實現中也有應用,因此不能直接刪掉。除這三個以外的類庫如果不需要可以刪,比如xss.js,一個防禦跨站腳本攻擊的庫。

HybridStart的意義

目的及原則

我有一點代碼潔癖,體現在我不喜歡任何二次封裝的東西,我希望通過最短的路徑去觸及功能實現的關鍵,所以抱着這樣的目的,最開始我連官方的js SDK也不用,直接調用引擎api開發業務,我認爲這是最快、性能最高的方式。

然而事實是,引擎提供的api效率真心不高,而且可靠性堪憂,當年用Appcan開發第一個項目的時候,簡直難受的想死,bug多到"舉步維艱"你能想象嗎,轉到APICloud後雖然沒有這麼多明顯的bug了,但部分api偶發性失靈還是有的,這種問題基本就沒辦法了,後來看了一些對混合應用實現原理的介紹才知道,這玩意本來就是個hack,反射弧就是比較長,體驗上"不利索"啊,偶發性的失靈啊,也就可以理解了,其實難怪,要真能像調用原生一樣快那還要原生幹什麼。

所以後來我改變了思路,不能再面向引擎編程了,因爲你不知道一個api背後是怎樣實現的,就不知道這個api的真實使用成本,所以我開始接受二次封裝,並且原則上儘量少的使用引擎能力

一開始是修改官方的js SDK,將無用的功能刪掉,將需要的功能加上,改着改着發現這個js SDK跟我的需求差別太大,乾脆就重寫了一個,該有的有,該擴的擴,用起來很爽。隨着開發的深入,越來越發現其實利用有限的幾個api就可以實現絕大多數需求,如果仔細研究引擎的api,會發現真有些功能是非必需的,或者說是語法糖,怎麼說呢,感覺就是api"設計的不優雅"。甚至有的功能實現還不如js模擬來的效果好,背後的開發質量可見一斑。

在這樣的目的和原則下,引擎api被二次封裝進了app對象,除了常用核心方法被直接掛載在app上之外,還包括了app.cryptoapp.lsapp.windowapp.ajax幾個模塊。

app.openView

app對象裏封裝了所有混合應用開發需要的功能,但是很多瑣碎的功能實現都儘量的被隱藏起來了,可能開發中只需要修改一個配置就能使用,目的就是爲了簡化開發。這裏我們就說一下app.openView()這個方法,這個方法用來打開一個頁面,可以說是開發中最常用的方法,藉此也讓大家對HybridStart到底做了什麼有一個感性的認識。

首先我們看引擎本來提供的api是什麼樣的:

api.openWin({
    name: 'page1',          //爲窗口命名,方便調用關閉方法將其關閉
    url: './page1.html',    //頁面路徑
    pageParam: {            //參數
        name: 'test'
    },
    animation: 'push',      //動畫效果
    subType: 'form_right'   //動畫方向
});

 

這個方法的配置項還有很多,列出來的是開發中最常用到的幾個,即便只是這幾個配置每次寫也已經夠羅嗦了,app.openView()可以說就是對這個api 的封裝,希望通過各種方式在不犧牲功能的前提下簡化配置,那我們就從這幾個配置入手,挨個來看怎麼簡化。

name屬性用來爲一個窗口命名,這個名稱將來可以用於調用某些方法對其進行操作。我們要省掉這個配置就只能自動生成,但這個名稱日後還有用,所以不能隨機生成,必須有一定的規律,這裏可以結合頁面組織來解決,按照我們前面講的規則組織後頁面分爲兩種,一級頁面"/view/channel/temp.html"和二級頁面"/view/channel/page/temp.html",規律還是很明顯的,只要提供頁面所屬的channel名稱以及如果是二級頁面的話再加上page名稱,就可以定位到這個頁面,並且通過channel + "_" + page來得到一個唯一的name值。那我們就先假定openView方法需要channelpage兩個參數,page是可選的,調用時將是這樣:

app.openView('home');           //url: "/view/home/temp.html", name: "home"

app.openView('member','set');   //url: "/view/member/set/temp.html", name: "member_set"

 

還不錯,nameurl都解決了,屬性pageParam的處理相對複雜,我們放在後面說,先來看animationsubType

這兩個屬性是最應該被封裝掉的,頁面切換的動畫類型肯定要集中到一個全局配置中管理,調用時animation可以省掉;動畫方向配置基本上就是個僞需求,打開自然就是右推,關閉自然就是左推,分別封裝進打開和關閉頁面方法裏就好了,subType也可以省掉。

現在來看pageParam,用來給頁面傳參,參數格式是Object。好,這個需求必須有,我們要讓app.openView()支持傳參,語法將變成這個樣子:

app.openView(param[Object], channel[String], page[String]);

因爲page是可選的,放在最後便於實現,因此將param參數放到前面。好像看上去也還行,但肯定還會有其他配置,不能一再的往上加參數吧,怎麼辦。

這裏有一條經驗,頁面傳參多數發生在從列表頁打開詳細頁的時候,這時我們傳的參數是一個id,也就是一個字符串,實際上絕大多數情況下的頁面傳參都只是一個字符串,需要Object的情況不多,基於這個前提,我們將param參數擴展一下,既可以接受字符串也可以接受對象,當接受字符串時將該值作爲參數傳遞給新頁面,當是對象時允許該對象包含對openView方法的所有配置,當然其中也包括了頁面參數,說起來有點繞,看代碼:

app.openView('newsID', 'news', 'detail');           //實際開發中最常用的字符串傳參

app.openView(null, 'home');                         //如果不需要傳參,抱歉必須傳一個null/undefined佔位

app.openView({                                      //Object類型的參數得這麼傳
    param: Object
}, 'home');  

app.openView({                                      //這裏還可以配置openView方法的其他參數
    duration: 350
}, 'home');

 

這樣所有的問題都解決了,但有一個小瑕疵,就是沒有參數必須傳null/undefined佔位,因爲page參數已經是可省的了,param參數實在沒辦法再做判斷,不過這個null/undefined傳的也不是一點意義沒有,這裏又得說來話長了。

前面說過給頁面傳參有兩種方法,一種是通過api提供的pageParam,另一種是通過localStorage跨頁面存取值,pageParam的問題是新頁面取值比較慢,取值代碼可能是這樣的:

//原生功能就緒回調
app.ready(function(){
    var pageParam = api.pageParam; 
    //基於pageParam的後續操作,比如頁面渲染、表單驗證,事件綁定
    ...
});

 

app.ready()是框架封裝的原生功能就緒回調,這是一個異步回調,通常,爲了提高腳本響應速度我們會把不需要原生能力的操作放在app.ready()之外,使其同步執行,問題在於,如果基於頁面參數的後續操作恰好是不需要原生能力的,但爲了等待取參數,也必須被放進app.ready()內執行,這就很不爽了。

所以框架提倡的傳參方式是用localStorage,在新頁面可以同步取值,這種方式唯一的問題是可能造成資源浪費,各種參數放進本地,怎麼清理?我的方法是約定一個專門用來傳參的鍵crossParam,每次傳參都寫進這裏,反覆擦寫最終留下的只是最後一次的參數值,app.openView()已經對此做了封裝,參數將自動存進localStorage.crossParam,參數如果是對象類型將做JSON.stringfiy()處理,因此如果傳的是對象,取值後需要自己做JSON.parse()處理

//同步取得頁面參數
var param = app.ls('crossParam');       
//執行不需要原生能力的操作
...

app.ready(function(){
    //執行需要原生能力的操作
    ...
})

 

回到app.openView()方法第一個參數必須佔位的問題,他的意義在於,當app.openView()檢測到null/undefined時會將本地存儲中的crossParam鍵刪掉,將造成浪費的可能性降至最低。

當然,官方的pageParam方式也沒有廢棄,如果傳遞的參數是對象的話,pageParamlocalStorage兩種方式都生效,可以通過api.pageParam 的方式也可以取到值。

經過這些封裝,打開頁面的語法已經非常簡單了,但app.openView()還有很多其他功能,比如以彈窗形式打開頁面、以帶標題欄的形式打開頁面、打開新頁面同時關閉當前頁面、或者打開一個網頁,這些功能的實現都相對複雜,就不一一展開了,這裏只着重介紹封裝思路,如果有興趣可以去HybridStart 文檔看一看。

後記

吹了半天,還得回到選型上來,我並不覺得多數項目適合這種方案,我甚至覺得只有少數項目,或者只有項目的起步時期,可以用這種方案快速上馬快速迭代,我理想中的混合應用形態是原生爲主web爲輔的,但從一個前端的角度看,我並沒有發現更好的可行性方案,有人可能會說React Native,但那個東西還是需要原生開發基礎的好嗎,而且如果APICloud在UI組件方面再進一步,貌似也可以接近React Native的效果。

總之,如果你覺得自己的項目正好適合這個方案的話,這個框架可能對你有幫助。

源碼: Github

源碼本身也是一個示例項目,上傳平臺即可編譯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章