这是 后端开发者从零作一个移动应用 的后端部分第二篇。介绍下一个新项目,后端该如何从零去搭建。咱们先假设这个项目由两部组成php
提供给wap站点、app使用的api;前端
提供给运营人员使用的管理后台。mysql
整个项目采用 Phalcon,项目的demo能够 点这里 参阅git
备注:跟随文章进度,项目持续更新,最后会与配套的wap app造成一个总体github
项目最终至少会包含如下内容:redis
小米消息推送sql
支付集成(支付宝、招商、微信)json
基于 Codeception 的api测试后端
登录api(这部分采用oauth2,会基于 'bshaffer/oauth2-server-php' 作)api
后端系统通常都是采用 MVC 结构(这里均以PHP为例),M 表明模型,V 表明视图,C 表明控制器。我在啰嗦几句
Model指的是数据模型,这个数据模型包括你的Mysql中的表结构,或者redis的缓存对象结构均可以。它表明一个数据操做单元。
View指的展现给用户浏览、直接操做的界面,这个你们都懂,很少说
Controller 控制器,主要是为了隔离 View 与 Model 直接打交道,他作为一个中间人,两头传递小纸条。
在我过往的项目中,我主要的困惑在于,业务逻辑是放在 C 仍是放在 M。
从对象角度出发,业务逻辑无非就是操做数据,要么读取,要么修改,那么应该放在M层,由于一个对象应该有本身的属性与方法。
实际工做中咱们经常有这样的场景,好比:读取一个游戏列表数据,数据包括游戏的详情以及游戏的版本信息以及下载信息。由于游戏app会存在升级,所以一个游戏会对应多个包。那么这里至少存在两个model
游戏详情model,包括游戏的名称,logo等基本信息
游戏的包信息model,包括包所属平台,大小,下载地址,版本信息等
那么这个动做的方法应该封装在哪里呢?之前的作法是,分别封装对应的操做到对应的model,而后在控制器中分别调用。说回到这里,游戏model封装了查询游戏列表的method,而后包model封装了根据游戏id查询包信息的method。
而后咱们在控制器中分别调用这个两个方法,而后再进行组装,把游戏对应的包设置到对应的游戏中。
那么有一个问题,假设咱们在游戏详情这个控制器方法中,须要返回一个相关游戏的集合,难道又重复一次上面的操做?
有人会说把处理游戏部分抽离成一个公共方法,那么假设是要在新闻详情里边调用呢?这根本不应在同一个控制器里边啊!
上面咱们把方法放在model中遇到了复用的小麻烦,那么继续看看放到controller中会怎样?
这个时候的一个好处是:咱们可使用链接查询,将刚刚的2次查询,经过链接查询1次完成,对于mysql的时间减小了,程序性能提高,而后对查询结果啪啪啪处理完成。
好吧,不日后面说了,相信你们已经发现了,这个查询过程仍是不可复用。天然而然的,咱们这里应该想到,将它提炼成一个方法,没法知足其余控制器使用(一个控制器调用另一个控制器的想法想都别想啊)。那么只能提炼成一个类了,这个类来封装全部的业务。
这样以后,任何须要游戏列表数据的地方,直接调用这个GameServer(假设封装的业务逻辑都放在xxxServer中)就能够得到相同的数据,而后若是业务变更,咱们也只须要改动这一处,全部地方获得的数据也将会是一致的。
所以经过回顾,咱们得出咱们的后端项目须要一个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:https://github.com/helei112g