App架构设计经验之谈

App架构设计经验之谈

1.接口的设计

1.1安全机制的设计

由于App的接口大部分采用RESTful架构,而RESTFul最重要的一个设计原则-客户端与服务器的交互的无状态性所以,当涉及到用户状态时,每次请求都要带上身份验证信息。实现上,大部分都采用token的认证方式,一般流程:

1)用户用密码登录成功后,服务器返回token给客户端;

  (2)客户端将token保存在本地,发起后续的相关请求时,将token发回给服务器;

3)服务器检查token的有效性,有效则返回数据,若无效,分两种情况:

token错误,这时需要用户重新登录,获取正确的token

token过期,这时客户端需要再发起一次认证请求,获取新的token

      然而,此种验证方式存在一个安全性问题:当登录接口被劫持时,黑客就获取到了用户密码和token,后续则可以对该用户做任何事情了。用户只有修改密码才能夺回控制权。

       解决方案:

       方案1:是采用HTTPS

        HTTPSHTTP的基础上添加了SSL安全协议,自动对数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,安全性可以提高很多。不过,SSL也不是绝对安全的,也存在被劫持的可能。另外,服务器对HTTPS的配置相对有点复杂,还需要到CA申请证书,而且一般还是收费的。而且,HTTPS效率也比较低。一般,只有安全要求比较高的系统才会采用HTTPS,比如银行。而大部分对安全要求没那么高的App还是采用HTTP的方式。

        方案2:每个接口的签名。

        OAuth1.0方式:给客户端分配一个密钥,每次请求接口时,将密钥和所有参数组合成源串,根据签名算法生成签名值,发送请求时将签名一起发送给服务器验证(可参考OAuth1.0的签名算法)。这样,黑客不知道密钥,不知道签名算法,就算拦截到登录接口,后续请求也无法成功操作。不过,因为签名算法比较麻烦,而且容易出错,只适合对内的接口。

         OAuth2.0如果你们的接口属于开放的API,则不太适合这种签名认证的方式了,建议还是使用OAuth2.0的认证机制。我们也给每个端分配一个appKey,比如AndroidiOS、微信三端,每个端分别分配一个appKey和一个密钥。没有传appKey的请求将报错,传错了appKey的请求也将报错。这样,安全性方面又加多了一层防御,同时也方便对不同端做一些不同的处理策略。

      手机号+短信验证码

 1)不需要注册,不需要修改密码,也不需要因为忘记密码而重置密码的操作了;

2)用户不再需要记住密码了,也不怕密码泄露的问题了;

3)相对于密码登录其安全性明显提高了。

 

1.2接口数据的设计

       接口的数据一般都采用JSON格式进行传输,不过,需要注意的是,JSON的值只有六种数据类型:

Number:整数或浮点数

String:字符串

Booleantrue  false

Array:数组包含在方括号[]

Object:对象包含在大括号{}

Null:空类型

    传输的数据类型不能超过这六种数据类型。如,尝试传输Date类型("201617 091742 GMT+08:00"),在转换时因不同的解析库解析方式可能不同则可能会转乱,甚至可能直接异常。要避免出错,必须做特殊处理,自己手动去做解析。为了根除这种问题,最好的解决方案是用毫秒数表示日期。

      另外,可能还出现类似字符串的"true""false",或者字符串的数字,甚至还出现过字符串的"null",导致解析错误,尤其是"null",导致App奔溃,后来查了好久才查出来是该问题导致的。这都是因为服务端对数据没处理好,导致有些数据转为了字符串。所以,在客户端,也不能完全信任服务端传回的数据都是对的,需要对所有异常情况都做相应处理

服务器返回的数据结构,一般为:

{

    code0,

    message: "success",

    data: { key1: value1, key2: value2, ... }

}

code: 状态码,0表示成功,非0表示各种不同的错误

message: 描述信息,成功时为"success",错误时则是错误信息

data: 成功时返回的数据,类型为对象或数组

      不同错误需要定义不同的状态码,属于客户端的错误和服务端的错误也要区分,比如1XX表示客户端的错误,2XX表示服务端的错误。这里举几个例子:

0:成功

100:请求错误

101:缺少appKey

102:缺少签名

103:缺少参数

200:服务器出错

201:服务不可用

202:服务器正在重启

        错误信息一般有两种用途:

        一是客户端开发人员调试时看具体是什么错误;

        二是作为App错误提示直接展示给用户看。主要还是作为App错误提示,直接展示给用户看的。所以,大部分都是简短的提示信息。

         data字段只在请求成功时才会有数据返回的。数据类型限定为对象或数组,当请求需要的数据为单个对象时则传回对象,当请求需要的数据是列表时,则为某个对象的数组。这里需要注意的就是,不要将data传入字符串或数字,即使请求需要的数据只有一个,比如token,那返回的data应该为:

// 正确

data: { token: 123456 }

 

// 错误

data: 123456

 

 

1.3接口版本的设计

接口不可能一成不变,在不停迭代中,总会发生变化。接口的变化一般会有几种:

数据的变化,比如增加了旧版本不支持的数据类型

参数的变化,比如新增了参数

接口的废弃,不再使用该接口了

 

为了适应这些变化,必须得做接口版本的设计。实现上,一般有两种做法:

1)每个接口有各自的版本,一般为接口添加个version的参数。

(2)整个接口系统有统一的版本,一般在URL中添加版本号,比如http://api.domain.com/v2

       大部分情况下会采用第一种方式,当某一个接口有变动时,在这个接口上叠加版本号,并兼容旧版本。App的新版本开发传参时则将传入新版本的version

如果整个接口系统的根基都发生变动的话,比如微博API,从OAuth1.0升级到OAuth2.0,整个API都进行了升级。

       有时候,一个接口的变动还会影响到其他接口,但做的时候不一定能发现。因此,最好还要有一套完善的测试机制保证每次接口变更都能测试到所有相关层面。

 

 

2.技术选型

      不同的技术方案,架构也可能完全不同。移动App上的技术主要包括是纯原生开发、Web App或是Hybrid App语言上,若是iOS开发,语言上是选择Objective-C还是Swift?架构模式用MVC,还是MVP,或者MVVM?下面根据我的一些经验对某些方面做点总结分享。

 

2.1原生/H5

根据背景做选择(用实例说明)

1不止要做AndroidiOS App,也要做微信公众号;

  (2H5人员缺乏,只有一两个兼职的可用,而且不可控因素很高;

3)我们对原生比较熟;

4)开发时间只有半个月。

     

     首先,需求上来说,大部分页面用H5实现,可以减少很多工作量。但因为不可控因素太高,而时间又短,风险太大。而我们对原生比较熟,开发效率比较高,很多东西我也控制得了,风险相对比较低。而且,我们的主推产品是App,微信属于辅助性产品,所以,微信要求也没那么高。因此,我决定以原生为主,H5为辅,App大部分页面用原生完成,小部分用WebView加载H5

另外,WebView加载H5也有两种模式,一种是加载服务器的H5页面,一种是加载本地的H5页面。加载服务器的H5页面比较简单,WebView只要load一下URL就可以了。加载本地的H5页面,则需要将H5文件存放在本地,包括关联的CSSJS文件。这种方式相对比较复杂,不过,加载速度会比第一种快很多。我们当前项目基于上面考虑,只能选择第一种方案。

       如果人员和时间资源充足的话,那又如何选型呢?毫无疑问,我会以H5为主,微信和App都有的页面统一用H5App专有的部分,比如导航栏、标题栏、登录等,才用原生实现。另外,WebView里的H5有点击事件时,也许是URL链接,也许是调用JS的,都不会让它直接在该WebView里做跳转,需要拦截下来做些原生处理后跳转到一个新的原生页面,原生页面也许嵌入另一个WebView,用来展示新的H5页面。这是简单的例子,关于Hybrid App详细的设计,以后再讲。另外,关于H5,绝对是大趋势,强烈建议所有App开发人员都去学习。

 

2.2 Objective-C/Swift

我在项目中选择了Swift,主要基于三个原因:

1 Swift真的很简洁,生产效率很高;

2 Swift取代Objective-C是必然的趋势;

3 目前iOS只有我一个人开发,不需要顾虑到团队里没人懂Swift

 

     如果你的团队里没人懂Swift,那还是乖乖用Objective-C吧;如果有一两个懂Swift的,那可以混合开发,并让不懂的人尽快学会Swift;如果都懂了,不用想了,直接上Swift吧。

         当语言上选择了Swift,相应的一些第三方库也面临着选型。比如,依赖库管理,Objective-C时代大部分用CocoaPodsSwift时代,我更喜欢CarthageCarhage是用Swift写的,和CocoaPods相比,轻耦合,也更灵活。我个人也不太喜欢CocoaPods,使用起来比较麻烦,耦合性也较高,我使用过程中也经常出问题,而且还总是不知道该怎么解决,要移除时也是非常麻烦。

再推荐几个关于Swift的第三方库:

1AlamofireSwift版本的网络基础库,和AFNetworking是同一个作者

  (2AlamofireImage:基于Alamofire的图片加载库

3ObjectMapperSwift版本的JsonModel转换库

  (4AlamofireObjectMapperAlamofire的扩展库,结合了ObjectMapper,自动将JSONResponse数据转换为了Swift对象

 

2.3 MVC/MVP/MVVM

先分别简单介绍下这三个架构模式吧:

MVCModel-View-Controller,经典模式,很容易理解,主要缺点有两个:

      1ViewModel的依赖,会导致View也包含了业务逻辑;

     2Controller会变得很厚很复杂。

 

MVPModel-View-PresenterMVC的一个演变模式,将Controller换成了Presenter,主要为了解决上述第一个缺点,将ViewModel解耦,不过第二个缺点依然没有解决。

 

MVVMModel-View-ViewModel,是对MVP的一个优化模式,采用了双向绑定:View的变动,自动反映在ViewModel,反之亦然。

 

       架构模式上,我不会推崇说哪种模式好,每种模式都各有优点,也各有极限性。越高级的模式复杂性越高,实现起来也越难。最近火热的微服务架构,比起MVC,复杂度不知增加了多少倍。

        

         技术选型,决策关键不在于每种技术方案的优劣如何,而在于你团队的水平、资源的多寡,要根据实际情况选择最适合你们当前阶段的架构方案。当团队拓展了,资源也充足了,肯定也是需要再重构的,到时再思考其他更合适更优秀的方案。

 

3.数据层的设计

一个App,从根本上来说,就是对数据的处理,包括数据从哪里来、数据如何组织、数据怎么展示,从职责上划分就是:数据管理、数据加工、数据展示。相对应的也就有了三层架构:数据层、业务层、展示层。

数据层,是三层架构中的最底层,负责数据的管理。它主要的任务就是:

1)调用网络API,获取数据;

  (2)将数据缓存到本地;

3)将数据交付给上一层。

根据这三个任务,数据层可以再拆分为三层:网络层、本地数据层、交付层。

 

3.1网络层

网络层主要就是对网络API的封装。

策略的处理:

        1)不同网络状态的处理

          当网络不可用时,则不应该再去调用API

          当网络可用,但不是WIFI时,有些比较耗流量的操作也应该禁止,比如上传和下载大文件;

          当网络状态不同时,还可以采用不同的网络策略,比如,当网络为WIFI时,当前API可以返回更多更全面的数据,还可以预先加载相关联的其他API

       2)为了节省流量,接口的设计上可以对数据进行简化

          对于一些列表类的接口,可以这么设计:只返回更新的部分,比如,上一次请求返回了10条按时间排序的数据,第一条数据为最新的,id101,当发起下一次请求时,将101id作为参数调用APIAPI查到该id,发现该id之后又新增了两条数据,API则只返回新增的这两条数据

        另外,为了保证程序的健壮性,调用API时,对入参的合法性检查也是很有必要的。而且,也应该定义好本地的错误码和错误信息,保证每个错误都能正常解析。

 

3.2本地数据层

本地数据层主要就是做缓存处理,这需要设计好一套缓存策略。设计缓存策略时,有几个问题需要考虑清楚:

1)哪些需要缓存?哪些不需要缓存?

2)缓存在哪里?数据库?文件?还是内存?

3)缓存时间多长?

 

 3.2.1哪些需要缓存?

      将所有数据都缓存是不明智的,不同的数据应该有不同的缓存策略,比如一个电商App,首页的商品列表数据应该缓存,而且缓存时间应该比较长,而每个商品的详情数据就没必要缓存或缓存时间很短。对于一份数据需不需要缓存,判断标准可以是:用户查看该数据的频率高不高?首页商品列表是用户每次启动都会看到的,而每个商品的详情用户最多只看几次。

 

3.2.2缓存在哪里?

       从内存读取数据是最快的,但内存非常有限。因此,内存一般只用来缓存使用频率非常高的数据。

       文件缓存主要就是图片、音频、视频了。

       数据库可以保存大量数据,主要就是用于保存商品列表、聊天记录之类的关系型数据。

        然而,不管缓存在哪里,都需要限定好缓存的容量,要定期清理,不然会越积越多。

 

3.2.3缓存时间多长?

        首先,每份缓存数据都应该设置一个缓存的有效时间,有效期的起始时间以最后一次被调用的时间为准,当该数据长时间没有再被调用到时,就应该从缓存中清理掉。

        缓存的有效时间应该设多长呢?可以短至一分钟,长至一星期甚至一个月,具体因数据而异。一般内存的缓存时间不宜太长,程序退出基本就要全部清理了。文件缓存可以设置保留一天或一个星期,可以每隔一天清理一次。数据库缓存再久一些也无所谓,但最好还是不要超过一个月。

 

3.3交付层

       交付层其实就是一个向上层开放的交互接口层,是上层向数据层获取数据的入口。上层向数据层请求数据,它是不关心数据层的数据是从缓存获取还是从网络获取的,它只关心结果,数据层能给到它想要的数据结果就OK了。因此,交付层主要就是定义一堆开放的接口或协议。

如果接口或协议非常多,那么,将接口或协议按照模块划分也是有必要的。比如微信,按模块划分有:IM、公众号、朋友圈、钱包、购物、游戏等等。模块之间应该尽量相对独立、松耦合。

 

3.4写在最后

         数据层如果再扩展,还可以再加入日志管理。

 

4.业务层的设计

4.1业务层的职责

        举两个栗子说明一下:

4.1.1 1-新用户注册的例子

        注册时,界面上一般都会要求用户输入手机号、验证码、密码和确认密码。但是,API接口一般只会有三个参数:手机号、验证码和密码,不会有确认密码。因此,调用接口之前,密码和确认密码的一致性检查是必须的。同时,也要检查这些数据是否为空、手机号是否符合规范、验证码是否有效、密码有没有包含了特殊字符等。

       正确姿势就是当所有检查都通过了之后,才调用API接口。最后,调用注册接口成功后,可能还要再调用一次登录接口,并可能将用户登录信息缓存起来,方便用户下次启动应用时自动登录。所有这些都属于业务逻辑处理,也就是业务层的工作。

 4.1.2 2-涉及用户验证的例子

        在一个电商App,当用户浏览某个商品,点击购买时,App首先会判断用户是否已经登录,如未登录,则会跳转到登录页面让用户先登录。如果已经登录,但token已经过期,那需要先去获取新的token,之后才能进行下一步的购物操作。这些逻辑处理,也是业务层的工作。

        因此,简单点说,业务层就是处理业务逻辑,包括数据的检查、业务分支的处理等。比如上面第二个例子,可能很多人就会将用户是否已经登录的判断直接在界面上做处理,当确认登录后,token也是有效的之后,才调用业务层做购买商品的操作,这就是导致业务层沦落为API的数据中转站的直接表现。

 

4.2业务层的交互

        只有真正理解了业务层的职责之后,才能有效地设计业务层与外层的交互接口。业务层向下,与数据层交互;向上,与展示层交互。

       与数据层交互只是调用数据层的接口获取数据,而与展示层交互则需要提供接口给展示层调用。因为业务处理一般属于比较耗时的操作,主要在于底层的网络请求比较耗时,所以提供给展示层的接口数据结果应该以异步的方式提供,因此,接口上就需要提供个回调参数,返回业务处理之后的结果。

 

       业务层可以说是一个数据加工场,处理核心的业务逻辑。其实,只要理解清楚了业务层的职责,业务层就不难实现。

 

5.展示层的设计

        展示层是三层架构中最复杂的一层,需要考虑的包括但不限于界面布局、屏幕适配、文字大小、颜色、图片资源、提示信息、动画等等。一个良好的展示层,应该有较好的可读性、健壮性、维护性、扩展性。

 

5.1 三原则

1)保持规范性:定义好开发规范,包括书写规范、命名规范、注释规范等,并按照规范严格执行;

2)保持单一性:布局就只做布局,内容就只做内容,各自分离好,每个方法、每个类,也只做一件事情;

3)保持简洁性:保持代码和结构的简洁,每个方法,每个类,每个包,每个文件,都不要塞太多代码或资源,感觉多了就应该拆分。

 

 

5.2 工程结构

        工程结构就是模块的划分,无非分为两类:按业务划分或按组件划分。

        比如一个电商App,可能会有首页、附近、分类、我的四大模块,工程结构也根据这四大模块进行划分,Android可能就分为了四个模块包:

com.domain.home 首页

com.domain.nearby 附近

com.domain.category 分类

com.domain.user 我的

        同样的,iOS则分为四个分组:homenearbycategoryuser

之后,每个模块下相应的页面就放入相应的模块。那么,问题来了,商品详情页应该属于哪个模块呢?首页会跳转到商品详情页,附近也会跳转到商品详情页,分类也会跳转到商品详情页,用户查看订单时也能跳转到商品详情页。有些页面,并不能很明显的区分出属于哪个模块的。我接手过的,按业务划分的二手项目中(即不是由我搭建的项目),我要找一个页面时,我认为应该属于A模块的,但在A模块却找不到,问了同事才知道在B模块。类似的情况出现过很多次,而且不止出现在我身上,对业务不熟悉的开发人员都会出现这个问题。而且,对业务不熟悉的开发人员开发新的页面或功能时,如果对业务理解不深,划分出错,那也将成为问题,其他人员要找该页面时更难找到了。

       因此,我更喜欢按组件划分的工程结构,因为组件每个人都懂,不管对业务熟不熟悉,查找起来都明显方便很多。Android按组件划分大致如下:

com.domain.activities 存放所有的Activity

com.domain.fragments 存放所有的Fragment

com.domain.adapters 存放所有的Adapter

com.domain.services 存放所有的Service

com.domain.views 存放所有的自定义View

com.domain.utils 存放所有的工具类

iOS的分组则大致如下:

controllers 存放所有ViewController

cells 存放所有Cell,包括TableViewCellCollectionViewCell

views 存放所有自定义控件或对系统控件的扩展

utils 存放所有的工具类

 

5.3基类的定义

        AndroidActivityFragmentAdapteriOSViewController,分别定义一个基类,将大部分通用的变量和方法定义和封装好,将减少很多工作量,而且有了统一的设置,也会减少代码的混乱。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值