若是是跟我同样以前从未接触过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
源码自己也是一个示例项目,上传平台便可编译。