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/css

HybridStart提供什麼

開發模式

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

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

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

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

 

開發中咱們第一個要實現的就是root頁的初始化功能,好比檢查登陸狀態,而後決定是跳轉到登陸頁仍是主頁,而後再去實現登陸頁 or 主頁。github

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

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

 

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

代碼組織

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

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

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

源碼自己也是一個示例項目,上傳平臺便可編譯。

相關文章
相關標籤/搜索