若是是跟我同樣以前從未接觸過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
,一個防護跨站腳本攻擊的庫。
我有一點代碼潔癖,體如今我不喜歡任何二次封裝的東西,我但願經過最短的路徑去觸及功能實現的關鍵,因此抱着這樣的目的,最開始我連官方的js SDK
也不用,直接調用引擎api開發業務,我認爲這是最快、性能最高的方式。
然而事實是,引擎提供的api效率真心不高,並且可靠性堪憂,當年用Appcan開發第一個項目的時候,簡直難受的想死,bug多到"舉步維艱"你能想象嗎,轉到APICloud後雖然沒有這麼多明顯的bug了,但部分api偶發性失靈仍是有的,這種問題基本就沒辦法了,後來看了一些對混合應用實現原理的介紹才知道,這玩意原本就是個hack,反射弧就是比較長,體驗上"不利索"啊,偶發性的失靈啊,也就能夠理解了,其實難怪,要真能像調用原生同樣快那還要原生幹什麼。
因此後來我改變了思路,不能再面向引擎編程了,由於你不知道一個api背後是怎樣實現的,就不知道這個api的真實使用成本,因此我開始接受二次封裝,而且原則上儘可能少的使用引擎能力。
一開始是修改官方的js SDK
,將無用的功能刪掉,將須要的功能加上,改着改着發現這個js SDK
跟個人需求差異太大,乾脆就重寫了一個,該有的有,該擴的擴,用起來很爽。隨着開發的深刻,愈來愈發現其實利用有限的幾個api就能夠實現絕大多數需求,若是仔細研究引擎的api,會發現真有些功能是非必需的,或者說是語法糖,怎麼說呢,感受就是api"設計的不優雅"。甚至有的功能實現還不如js模擬來的效果好,背後的開發質量可見一斑。
在這樣的目的和原則下,引擎api被二次封裝進了app
對象,除了經常使用核心方法被直接掛載在app
上以外,還包括了app.crypto
、app.ls
、app.window
、app.ajax
幾個模塊。
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方法須要channel
和page
兩個參數,page
是可選的,調用時將是這樣:
app.openView('home'); //url: "/view/home/temp.html", name: "home" app.openView('member','set'); //url: "/view/member/set/temp.html", name: "member_set"
還不錯,name
和url
都解決了,屬性pageParam
的處理相對複雜,咱們放在後面說,先來看animation
和subType
。
這兩個屬性是最應該被封裝掉的,頁面切換的動畫類型確定要集中到一個全局配置中管理,調用時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
方式也沒有廢棄,若是傳遞的參數是對象的話,pageParam
和localStorage
兩種方式都生效,能夠經過api.pageParam
的方式也能夠取到值。
通過這些封裝,打開頁面的語法已經很是簡單了,但app.openView()
還有不少其餘功能,好比以彈窗形式打開頁面、以帶標題欄的形式打開頁面、打開新頁面同時關閉當前頁面、或者打開一個網頁,這些功能的實現都相對複雜,就不一一展開了,這裏只着重介紹封裝思路,若是有興趣能夠去HybridStart 文檔看一看。
吹了半天,還得回到選型上來,我並不以爲多數項目適合這種方案,我甚至以爲只有少數項目,或者只有項目的起步時期,能夠用這種方案快速上馬快速迭代,我理想中的混合應用形態是原生爲主web爲輔的,但從一個前端的角度看,我並無發現更好的可行性方案,有人可能會說React Native
,但那個東西仍是須要原生開發基礎的好嗎,並且若是APICloud在UI組件方面再進一步,貌似也能夠接近React Native
的效果。
總之,若是你以爲本身的項目正好適合這個方案的話,這個框架可能對你有幫助。
源碼: Github
源碼自己也是一個示例項目,上傳平臺便可編譯。