这是 后端开发者从零做一个移动应用 的后端部分第二篇。介绍下一个新项目,后端该如何从零去搭建。我们先假设这个项目由两部组成
提供给wap站点、app使用的api;
提供给运营人员使用的管理后台。
整个项目采用 Phalcon,项目的demo可以 参阅
备注:跟随文章进度,项目持续更新,最后会与配套的wap app形成一个整体
项目最终至少会包含以下内容:
小米消息推送
支付集成(支付宝、招商、微信)
基于 Codeception 的api测试
登陆api(这部分采用oauth2,会基于 'bshaffer/oauth2-server-php' 做)
项目结构回顾
后端系统一般都是采用 MVC 结构(这里均以PHP为例),M 代表模型,V 代表视图,C 代表控制器。我在啰嗦几句
Model指的是数据模型,这个数据模型包括你的Mysql中的表结构,或者redis的缓存对象结构都可以。它代表一个数据操作单元。
View指的展示给用户浏览、直接操作的界面,这个大家都懂,不多说
Controller 控制器,主要是为了隔离 View 与 Model 直接打交道,他做为一个中间人,两头传递小纸条。
在我过往的项目中,我主要的困惑在于,业务逻辑是放在 C 还是放在 M。
从对象角度出发,业务逻辑无非就是操作数据,要么读取,要么修改,那么应该放在M层,因为一个对象应该有自己的属性与方法。
业务放在M中
实际工作中我们常常有这样的场景,比如:读取一个游戏列表数据,数据包括游戏的详情以及游戏的版本信息以及下载信息。因为游戏app会存在升级,因此一个游戏会对应多个包。那么这里至少存在两个model
游戏详情model,包括游戏的名称,logo等基本信息
游戏的包信息model,包括包所属平台,大小,下载地址,版本信息等
那么这个动作的方法应该封装在哪里呢?以前的做法是,分别封装对应的操作到对应的model,然后在控制器中分别调用。说回到这里,游戏model封装了查询游戏列表的method,然后包model封装了根据游戏id查询包信息的method。
然后我们在控制器中分别调用这个两个方法,然后再进行组装,把游戏对应的包设置到对应的游戏中。
那么有一个问题,假设我们在游戏详情这个控制器方法中,需要返回一个相关游戏的集合,难道又重复一次上面的操作?
有人会说把处理游戏部分抽离成一个公共方法,那么假设是要在新闻详情里边调用呢?这根本不该在同一个控制器里边啊!业务放在C中
上面我们把方法放在model中遇到了复用的小麻烦,那么继续看看放到controller中会怎样?
这个时候的一个好处是:我们可以使用连接查询,将刚刚的2次查询,通过连接查询1次完成,对于mysql的时间减少了,程序性能提升,然后对查询结果啪啪啪处理完成。
好吧,不往后面说了,相信大家已经发现了,这个查询过程还是不可复用。自然而然的,我们这里应该想到,将它提炼成一个方法,无法满足其他控制器使用(一个控制器调用另外一个控制器的想法想都别想啊)。那么只能提炼成一个类了,这个类来封装所有的业务。
这样之后,任何需要游戏列表数据的地方,直接调用这个GameServer(假设封装的业务逻辑都放在xxxServer中)就可以获得相同的数据,然后如果业务变动,我们也只需要改动这一处,所有地方得到的数据也将会是一致的。
因此通过回顾,我们得出我们的后端项目需要一个server的层次,来存放业务逻辑。
Server层存在的意义
分离出来的这一层,集中涵盖了所有的业务功能,极大的提高了代码的复用性,除了不同控制器不同方法的直接使用,还包括了不同模块之间的复用。
但是在不同模块之前服用,server层也需要考虑一些额外的东西,比如我们有一个app api模块,有一个后台管理模块。那么都是获取列表数据,可能给app api模块可能不需要某些字段,但是后台管理需要知悉全部内容,以及后台用户权限上的一些问题。这些部分可以继续进行拆分,与server组合。需要结合自己的业务来进行管理。
我个人实践过程中代码的另外一个好处是,server层从某种层度上让C层变得简单,这让团队中的新人能够快速上手接触代码。比如小明是团队新人,那么在他熟悉所使用框架的前提下,他可以立刻在C层开始做事情,因为这里没有业务,有的只是验证客户端传过来的数据,以及对server层的调用返回。通过这个过程可以加速其融入团队的进程。
统一的返回格式
约定api返回的数据格式,这基本上是系统开发开始的第一步,原先常用的方式就是在每个控制器中通过
return json_encode([ 'msg' => 'ok',// 携带的信息,可以用来前端 alert 提示用户 'data' => [// 具体数据 ... ... ], 'code' => '0', // 0表示成功,其他表示对应错误])
那么这里首先遇到的第一个问题,为了简化前端对类型的判断,基本上所有的字段值,都是返回字符串形式。那么 data 里边的内容就需要在每个控制器中进行处理字符串、utf-8编码等问题。要重复代码,就算你抽离成一个方法,也需要面对该问题。好点的解决方案是在返回数据的拦截器(每一个框架都有类似的概念)内进行统一的处理。
像上面这样的代码写法,带来的额外问题可能有,字段名称打错,比如: code 写成 cdoe ,data 写成 date。为程序代码额外的风险(尤其是bug修复时最容易出现该情况)
那么一种解决办法就该由此想到,采用对象的方式来规范化返回的数据结构。比如我们定义一个类:
class ResultData { /** * 返回的信息提示 * @var string $msg */ private $msg; /** * 返回的数据结构 * @var array|object|string */ private $data; /** * api 状态码 * @var int $apiCode * @see ApiCode */ private $apiCode; public function __construct(int $apiCode, string $msg = 'ok', $data = null) { $this->apiCode = strval($apiCode); $this->msg = trim(strval($msg)); $this->data = $data; } /** * 获取数据结果 * @return array */ public function getRetData() { if (! is_array($this->data) && is_object($this->data) && method_exists($this->data, 'toArray')) { $this->data = $this->data->toArray(); } // valueToString 将data的value转化为 string 并且做utf-8转码 $result = [ 'code' => $this->apiCode, 'msg' => $this->msg, 'data' => $this->data ? ArrayUtil::valueToString($this->data) : [], ]; if (! APP_ENV_PROD) {// 测试环境显示 api 的处理时间信息 方便优化 $result['use_time'] = microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']; } return $result; }}
有了上面这个类,我们所有的服务层或者controller都应该用它作为返回值。然后在拦截器中统一进行json encode即可。这样子即减少了犯错的可能性,同时对统一处理数据的地方做了统一管理集中到 ResultData 中,那么以后有什么特殊变动,调整一处,处处生效。
其它问题
另外还有关于 oauth2 如何集成到项目中等等问题,这部分均放到 x-api 项目中进行说明,纸上说来终觉浅嘛。
日志的记录也是系统开发非常重要的部分,这部分没什么太多说的,用规范的格式,存储指定的数据(介质可以是:db、file)。
系统开发中应该拒绝使用 var_dump
、echo
这些方式进行调试,另外建议采用:PhpStorm IDE来进行系统开发。
后续分享
接下来会完善一个 x-api
的基本结构,以及php自动化测试部分文档教程,然后后端部分就告一段落。(本系列的分享主要集中在代码层面,不涉及相关系统部署问题)
GitHub: