1 | st=>start: index.php入口 |
laravel应用执行流程
Index
应用入口
请求经过web服务器,路由到index.php入口文件。在入口文件中引入vendor/autoload.php
和bootstrap/app.php
文件。其中,前者是文件自动加载规则,后者是应用启动文件。
第一阶段:容器准备阶段
在该文件中,首先实例化应用核心容器Illuminate\Foundation\Application
,即$app
对象,然后单例绑定容器核心服务:
Illuminate\Contracts\Http\Kernel::class
Illuminate\Contracts\Console\Kernel::class
Illuminate\Contracts\Debug\ExceptionHandler::class
$app对象实例化
- 实例化应用核心容器时,传入应用根目录作为参数。以此目录分别衍生出
base/lang/config/public/storage/database/resources/bootstrap
等应用目录 - 然后将应用对象绑定到自身
app
别名与Illuminate\Container\Container::class
接口,同时绑定PackageManifest::class
对象,后续用于对composer.json
文件的解析 - 接下来注册基础服务提供者
EventServiceProvider/LogServiceProvider/RoutingServiceProvider
- 最后进行核心别名的定义,
Illuminate\Contracts\Http\Kernel::class
该接口绑定http请求处理器。用于接收并处理来自web的请求
Illuminate\Contracts\Console\Kernel::class
该接口绑定Console处理器。用于接收来自CLI的命令。
Illuminate\Contracts\Debug\ExceptionHandler::class
该接口绑定异常处理器。用于在应用中出现异常时,做出对应的响应。
第二阶段:容器启动阶段
在做完第一阶段的准备工作后,从容器中解析出Http处理器,Http处理器捕获到请求,开始启动容器。
Http处理器捕获请求
捕获请求也就是$request
对象的初始化。
启动容器
在捕获到请求后,需要先启动容器,容器启动的动作由Http处理器触发,并启动由Http处理器定义的启动文件,启动文件列表如下:
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class
: 加载环境变量配置.env文件\Illuminate\Foundation\Bootstrap\LoadConfiguration::class
: 加载配置目录配置文件\Illuminate\Foundation\Bootstrap\HandleExceptions::class
: 注册异常处理函数\Illuminate\Foundation\Bootstrap\RegisterFacades::class
: 注册Facade,由config/app.php
中定义的aliases和从composer.json中解析出的Facade组成\Illuminate\Foundation\Bootstrap\RegisterProviders::class
: 注册服务提供者,由config/app.php
中定义的providers和从composer.json中解析出的providers组成\Illuminate\Foundation\Bootstrap\BootProviders::class
: 启动服务提供者。
启动服务提供者
在启动服务提供者后,容器就算是完全准备就绪了:容器准备阶段的别名定义,提供了容器可对外服务的接口,而启动服务提供者的过程中,会实例化接口对应的对象,后续执行对象依赖接口时就可以从容器中解析出所需要的对象。
容器启动的重点,在于启动服务提供者,在该阶段完成了大量的基础重要工作。如:文件驱动服务、数据库连接服务、Session服务、Redis服务、视图服务、认证服务、路由服务等,详见config/app.php
中的providers部分。其中,路由服务,会加载路由定义文件,生成路由表,供后续路由解析时做匹配查询。
在容器启动完毕后,进入第三阶段,请求处理阶段。
第三阶段:请求处理阶段
路由解析
路由解析,就是根据当前请求,从路由表中匹配出定义好的路由,匹配会从四个方面进行Uri/Method/Host/Scheme
。命中路由后进行下一阶段,否则抛出路由不存在的异常。
全局中间件过滤
命中路由后,请求经过全局中间的过滤,全局中间定义在Http处理器中,应用默认全局中间件列表如下:
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class
: 检查应用状态\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class
: 检查请求数据大小\App\Http\Middleware\TrimStrings::class
:过滤请求数据值中的首尾空字符\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
:将空的值转化成null\App\Http\Middleware\TrustProxies::class
:配置代理IP
控制器实例化
请求通过全局中间之后,接着就要实例化控制器。因为下一步就要通过路由中间件,路由中间件可能随路由一同定义在路由文件中,也有可能定义在控制器的$middleware属性中。所以,为了收集到完整的路由中间列表,需要先实例化控制器对象,才能从中解析中间件属性。由于控制器的实例化,与路由中间件的过滤存在这样一个顺序关系,导致在控制器的构造函数中,无法获取到在路由中间件中处理的一些状态。比如登录状态,用户认证过程,依赖\Illuminate\Session\Middleware\StartSession::class|\App\Http\Middleware\EncryptCookies::class
等路由中间件的执行结果,控制器构造函数执行时,路由中间件还未执行,因此无法获取用户的登陆状态。
因此,不推荐在控制器的构造函数中做太多的逻辑处理,避免因上述原因导致的错误。如果确实有些场景,需要在构造函数中做一些统一的操作,可以用CallAction方法来代替构造函数,见Illuminate\Routing\ControllerDispatcher::dispatch()
方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return mixed
*/
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);
if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}
return $controller->{$method}(...array_values($parameters));
}
路由中间件过滤
一般路由中间件都会包含下列几个:
\App\Http\Middleware\EncryptCookies::class
: cookie加密与解密,解密是前置部分,加密是后置部分\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class
:添加程序中定义的cookie到响应中。\Illuminate\Session\Middleware\StartSession::class
:启动session\Illuminate\View\Middleware\ShareErrorsFromSession::class
:将验证错误信息系添加到session中,见文档表单验证部分\App\Http\Middleware\VerifyCsrfToken::class
:csrf检查\Illuminate\Routing\Middleware\SubstituteBindings::class
:路由模型绑定解析
运行路由方法并响应
进过上述处理之后,就终于到达了路由方法,也就是我们定义的路由中的控制器方法或闭包方法。执行完毕生成响应返回给浏览器。
需要注意的是,在执行完路由方法之后,还有可能存在后置中间件的方法待执行,比如cookie的加密,关于后置中间件的说明见文档中间件部分。
第四阶段:terminate
terminate是指在响应发送到浏览器之后会执行的方法。terminate方法中非常适合做日志记录的工作,可以完美解决使用log-viewer插件时的log死循环问题。其次,还需要注意的是,在web环境下与Console环境下,terminate方法的区别。在web环境下,Http处理器先执行中间件中定义的terminate方法,然后执行容器$app的terminate方法,而在Console环境下,直接执行$app的terminate方法。
信息管理部PHP小组知识分享
Index
编码规范
php编码规范
目前主流的php编码规范是PSR编码规范。 个人意见是不需要严格去抠规范的细节,但是需要理解规范的意义:
项目的目的在于:通过框架作者或者框架的代表之间讨论,以最低程度的限制,制定一个协作标准,各个框架遵循统一的编码规范,避免各家自行发展的风格阻碍了 PHP 的发展,解决这个程序设计师由来已久的困扰。
对我们的意义在于:规范让我们能编写可维护性更高的代码。
通过给IDE编辑器安装相关插件,可以达到自动规范化代码的目的,具体教程方法可以在网上搜索一下。
只不过,插件可以解决代码形式上的规范问题,能做的毕竟有限,何况在代码可维护性上还会遇到编码规范所不能解决的问题,我将这类问题归纳到编码习惯当中。
编码习惯
编码习惯是经验的总结,是一个不断补充的列表,具体内容见下面的链接。如果你有比较好的经验总结,也可提出来供大家参考。
- 编码习惯 持续更新中…
环境搭建
环境要求
php7.2
工欲善其事,必先利其器。为了提升开发效率,达到良好的编码体验,需要配置好一套自己熟悉的开发环境。这里推荐三种开发环境的搭建方式,分别是本地集成环境、虚拟机环境、docker环境,并就本人的使用经验来阐述各自的优缺点。
本地集成环境
目前可以免费使用的本地集成环境有很多,本人只用过 Wampserver
,可以根据自己的喜好自己做选择。 请注意尽量在官方网站下载集成环境软件,其他来源的软件可能携带病毒或者后门。
优点
- 环境搭建难度低
- 系统资源占用低
缺点
- windows版本的PHP不支持部分扩展
周边服务使用不便或者干脆没有,imagick、redis、nginx等
点评:
本地集成环境基本可以满足日常开发的需要,也是开发者本地必备的环境。但随着项目经验的累积,会遇到本地开发环境难以解决的问题。
虚拟机环境
先在本机上安装Vmware等虚拟机引擎,然后创建一个linux虚拟机,再在linux虚拟机上安装开发环境及周边服务。
优点
- 熟悉linux系统的使用。
- 很大程度上模拟正式环境。如果遇到正式环境上的bug无法在本地环境重现,可以再虚拟机环境上试试。
- 周边服务安装简单。众所周知,很多软件在linux上安装就是一个命令的问题。
- 可以装多个虚拟环境。
缺点
- 环境搭建麻烦
- 系统资源占用升高
- 需要通过ssh工具来对环境进行管理
- 不熟悉linux怎么使用?? 流下的技术不足的泪水…
点评:
推荐使用。就我的使用经验来看,虚拟机更多的是对本地集成环境的补充,一般不会在开发的时候,将代码部署到虚拟机来运行,而是通过虚拟机来提供redis/es/nginx代理等服务。
docker环境
通过docker来搭建开发环境也是一种方式。首先需要安装好docker引擎,然后需要进行镜像制作、镜像编排来完成开发环境的搭建。
优点
- 比较贴合当前技术趋势
- 拥有虚拟机环境的所有优点
- 无需ssh工具就能体验linux的功能
缺点
- 系统资源占用很高,可能比虚拟机环境的资源占用还高。
- 前期工作量大且复杂
- 需要学习docker相关知识。但是只需要会docker、docker-compose两个命令的使用就可以搭建
点评:
入坑吧,少年!教程
php框架
我部门目前php的技术栈主要是laravel和YII,除部分遗留的系统外,新系统均使用laravel进行开发。
laravel
版本要求
laravel 5.5
源码分析系列
- laravel应用执行流程
- laravel ORM源码分析
- laravel 队列部分源码阅读
- laravel 路由模块源码分析
- laravel 框架对__call魔术方法的使用
- laravel Auth源码分析
- laravel Pipeline源码分析
- 邮件发送过滤
- laravel 中间件
- laravel 文件系统
扩展包
YII
最佳实践
TODO
PHP坑爹函数系列- 可配置化
错误与异常的处理- 接口返回定义
- 对象属性修改的注意事项
- input的作用域
如何进行代码结构的规划
Excel单元格自动合并的实现方案
Index
背景
以前在开发有格数据驾驶舱的时候,由于需要展示比较多的表格,而且表格有合并的情况,每个表格的合并规则还不一致。当时需要同时支持导出 Excel
文件的合并,以及返回到接口的数据,供前端展示时合并,这两种情况。通过分析之后,计划通过两种方案来实现:
Excel
模板。模板包含要合并的情况,导出时仅填充数据。- 动态合并规则。按要求,自动对数据项进行合并。
通过模板的方式来解决这个问题,需要面以下下困难:
- 如果合并的情况比较复杂,比如前十行与后十行的合并情况不一致,更进一步,如果这个“十”是变化的,那么模板就无能为力了。
- 返回给前端接口的表格数据,无法共模板这一方案,需要单独想办法解决
由于上述两个原因,决定采用方案二,动态合并规则来实现。总的概括一下我们要解决的问题:
1 | 任意给定一组二维数据,根据自定义的一些规则,来对这组数据进行合并,并列出所有合并区域的起点与终点。 |
思路
如何找出需要合并的区域呢?先假设一个无规则的情况;如果有一个表格,你可以自由的对表格数据进行合并,要如何实现?
找出合并的隐含条件
在无规则情况下,其实默认单元格满足下列两个条件,就可以进行合并:
- 两个单元格的值相等
- 两个单元格的位置相邻
可合并区域的寻找
那么,合并区域的寻找,可以通过以下过程来展开:
- 确定起点:确定数据的起点,假设为
O(0,0)
,标记点O
为合并的起点 - 横向检查:检查
O
右侧的点N(0,1)
,如果点O
与点N
的值相等,那么表示可以合并,继续检查N
右侧的点N1
,直到Nx
的值不等于O
的值,至此,横向检查完毕 - 纵向检查:检查
O
下方的点H(1,0)
, 如果点O
与点H
的值相等,那么从H
点开始,启动第二轮横向检查。如果不想等,那么合并结束,合并区域为O~Nx-1
。Nx-1
表示最后一轮横向检查的终点。
这个过程是一次合并区域检查的过程,每执行一个这样的过程,就会得到一个合并区域,记作 Range[O,Nx-1]
,如果点 O
与点 Nx-1
相等,说明该合并区域就是一个点,可以舍弃掉。
可合并区域的影响
每寻找到一个合并区域 Range[O,Nx-1]
,我们就消除了一个起点,同时,得到了两个新的起点。由于合并区域是一个矩形,矩形有四个点,其中一个是我们选择的起始点,还剩下三个点,由于对角线上的点比较特殊,我们先抛开不谈,还剩下起始点相邻的两个点,这两个点,就是下一次合并的起始点。
假设合并区域的终点为 Nx-1(m,n)
,那么,由点 O
分裂出两个新的起始点
1 | O(0,0) -> O1(0,n), O2(m,0) |
由于合并起始点需要进行一个单位的偏移,对角点会偏移成三个点。但这三个点都有可能被相邻两个点的区域所包含,也有可能不会包含。将对角点考虑进来讨论的话,会大大的增加问题的复杂性,但并不会对结果有积极的意义,弊大于利,所以讨论与编码时,都将这个点忽略
接下来,我们继续以 O1/O2
为起始点,分别进行可合并区域的寻找,就能又找到两个可合并区域,以及,分裂成四个新的起始点。
结束条件
根据上述分析,我们会发现,随着可合并区域不断被找出,起始点不断被分裂成更多的起始点。那么这个循环会一直持续下去吗?并不是,当分裂到数据的边界的时候,一个起始点就只会分裂成一个起始点,这时,起始点的规模就会开始收缩。那么,“数据的边界”,包含哪些情况呢?
- 数据的范围达到给定范围的极限,就是,数据右边或下边没有更多数据了。
- 数据的范围,触及到某个已找到的可合并区域的边界。显然,由此分裂出的开始点,已经被“寻找过”,并不会产生新的可合并区域。
当所有的起始点,都触碰到数据的边界的时候,查找,就结束了,已找到的可合并区域,就是给定数据中,所有的可合并区域。
其他问题
我们按照上述思路,已经归纳出了方案的基本雏形,只不过在实际使用中,可能并不会达到我们想要的结果,因为这当中忽略了几个比较重要的问题。
三角形问题
如果数据中有一个三角形区域 O(0,0) -> A(0,10) -> B(10, 0)
其中所有的单元格的值都相等,按照上述思路,最终寻找的可合并区域是 Range[O(0,0), B(10,0)]
,因为我们在可合并区域的寻找过程中,遵循的是“横向合并优先”,如果我们遵循“纵向合并优先”的话,最终需要的可合并区域是 Range[O(0,0), A(0, 10)]
,然而实际当中,也许这两种情况,都不是我们希望的结果,比如,若我希望合并区域有最大的面积,那么,最终的可合并区域应该是 Range[O(0,0), M(5,5)]
。究竟应该如何取舍,实际上取决于“我们的要求”。
起始点的合并顺序
由于起始点是会逐渐分裂增加的,那么,依据什么来决定,哪个起始点优先进入合并队列呢?考虑一种极端情况,第一个可合并区域将整个数据分成了两个部分:可合并区域的数据都是1,剩下部分的数据,都是2。如果称可合并区域为第二象限,那么以右侧的点开始,得到的合并区域是第一象限与第四象限的组合;如果以下侧的点,开始,得到的合并区域是第三象限与第四象限的组合。所以,起始点的选取顺序,也会导致合并区域结果的差异。
我们如何引入自定义条件
到此为止,我们上述的讨论,都不涉及到自定义条件的问题,而这本身就是需求之一。
点睛之笔-自定义条件
如果讨论至于上述的思路,那么这一方案实际上并无太大用处,因为由于最后三个问题的存在,导致最终合并的结果,很有可能并不是我们想要的。其中,第三个问题,它既是一个问题,又是一个需求,那么,考虑在解决该问题的同时,附带解决其余两个问题。我们将上文的思路,归纳成两个过程:
- 寻找合并区域
- 循环起始点
从顺序上看,寻找合并区域
先于 循环起始点
,从因果关系上看,寻找合并区域
会导致 循环起始点
的主体 起始点
起始点的变化。所以我们先考虑从 寻找合并区域
这一过程中,引入自定义条件。 寻找合并区域
分为两个主要过程,横向检查
与 纵向检查
,我们引入的条件,应该是能影响这两个过程的结束位置。
对于上述 三角形问题
,如果我们要求可合并区域的面积最大,可以转化为这样的一组条件:
- 横向合并到
5
为止 - 纵向合并到
5
为止
这里的两个条件,就是我们的自定义条件,我们把这类条件,归纳为 停止行/停止列
停止行与停止列
停止行与停止列,是自定义条件的核心。他规定合并区域的寻找,在遇到哪些行与列的时候,就停止。因此,我们只需要在寻找合并区域的逻辑当中,引入对停止行与停止列的检查即可。需要留意的是,停止行与停止列的位置可能是变化的,我们在编码时可能需要考虑到这一点。
继承
停止行与停止列可能需要被继承,他所代表的含义是:表格前面部分的停止规则,极有可能对后面的数据生效,但是根据后面的数据以及规则,可能无法计算出合适的停止规则,这时,直接将前面的停止规则继承过来即可。
合并优先序
我们在讨论 三角形问题
时,提到过 横向合并优先/纵向合并优先
的概念,这是指在寻找合并区域时遇到的顺序问题。在循环起始点时,也存在这么一个问题:即应该以左侧的点开始新一轮的合并区域寻找,还是应该以下侧的点,开始新一轮的合并区域寻找。这是两个合并优先序的问题。
先来看一看起始点的分裂情况,假设有以下分裂过程:
1 | O(0,0) -> [N(2, 0),H(0,2)] |
起始点出现的顺序是
1 | N - H - N1 - H1 - N2 - H2 |
分布大概如下表所示
0 O(0,0) | 1 | 2 N(2, 0) | 3 | 4 | 5 N1(5,0) |
---|---|---|---|---|---|
1 | — | — | — | — | — |
2 H(0,2) | N2(1,2) | — | — | — | — |
3 | — | H1(2,3) | — | — | — |
4 H2(0,4) | — | — | — | — | — |
5 | — | — | — | — | — |
如果以开始点出现的顺序进行合并,通过相对位置可知,点 H1
可能会被包含在 N2
开始的可合并区域中。但是按点的顺序来看,H1
的合并先发生,由于已合并区域会形成边界,这时 N2
进行合并的话,不可能再次包含点 H1
。
通过以上示例可以看出,起始点的合并开始顺序,确实会影响最终的合并结果。因此,在每次合并完成后,都需要对已存在的点和新生成的两个点,进行一次排序,用以决定究竟哪个点该进入下一次的合并。
每个点都有一个横坐标与纵坐标,很容易想到的方法是,仅比较每个点的横坐标,越小的点,越先开始合并;或者仅比较每个点的纵坐标,越小的点,越先开始合并。但如果有两个点的某个坐标相同,又恰巧以该坐标决定合并顺序呢,很容易想到,比较应该同时结合横坐标与纵坐标,但以某一项为主;就像如果以横坐标为主,那么横坐标的值充当十位,纵坐标的值充当个位,用以区分其权重。
显然,如果横坐标的排序权重高,我们称为“横向合并优先”,那么每次分裂之后,横向的点都会进入下一个合并队列,纵向的点,进入等待队列,直到横向的数据达到数据范围的极限,此时,等待队列中的开始点,类似下列情况:
1 | Nn - H1 - H2 - H3 - - - Hn |
横向数据范围越大,Hn
中 n
的值越大,等待合并的点越多。如果纵坐标的排序权重高,我们称为“横向合并优先”,情况依然类似。
其实,只要合并不是无序的,无论是横向优先,还是纵向优先,对合并结果的影响并不是很大,尤其是在停止行规则充分时,基本可以达到相同的合并结果。不过因为横向与纵向数据范围的差异,可能导致待合并队列中点的个数差异,进一步影响点排序的效率,因此,此处可以有一个优化,来使排序的效率增加,就是以数据范围小的坐标作为合并优先序。
实现
基于上述思想。我们简单理一下,该如何编码来实现本功能。
基本对象
单元格对象
该对象用来表示点,他需要有一个横坐标属性,纵坐标属性,还需要能计算出左侧的点,右侧的点,以及两个点是否是同一个点。
1 | class Cell |
这个实现并没有任何对点的值的表示,为何要这样处理,我们放在后面来说。
区域对象
区域表示的是一个范围,他有一个开始的点,与结束的点。同时他还需要有获取分裂后的点的能力,需要有判断点是否落在区域中的能力。
1 |
|
数据源对象
数据源对象,就是给定的初始二维数据。只不过我们在编码时,不应该把它具体化,只需要知道,这个对象需要提供哪些功能。因此,数据源对象是什么。我们并不关心,但他需要实现这个接口:
1 | use Cell; |
这里来解释一下,为何单元格对象并不能获取自身的值?因为源数据有可能需要一个很大的存储空间,如果单元格能获取到值,必然需要在某处引用这一数据源,此时,Cell
类会对外部产生一个依赖,并且需要在实例化时主动传入该数据对象。而在执行过程中,会出现大量的点对象,由此可能导致内存占用增加,所以,将单元格的取值过程转嫁给数据源对象。
搜索执行对象
对给定数据源执行搜索,其中包含代码主要的逻辑部分。
停止规则对象
停止规则对象依赖给定数据源,用以计算出动态的停止规则。
执行过程
合并的初始化与进行
1 |
|
这里只列出了主要的检查逻辑。
停止行规则
停止规则对象
1 | class StopRule |
在进行停止行的判定时,有这样的一部分代码:
1 | if ($this->exporter instanceof WithStopRule) { |
其中 exporter
就是一开始给定的原始数据对象,如果该对象实现了 WithStopRule
接口的话,表名该对象定义了自己的停止规则,通过 stopRows/stopColumns
方法获取到原始数据定义的停止规则 $rule
,然后将 $rule
和当前的点 $cell
传给 parserStopRowRule
方法,计算出在当前点 $cell
是否需要停止。
最终代码结构如下:
1 | / |
服务端支持跨域请求
解决 js 跨域的一种方式是直接在服务端设置允许跨域请求,具体原理及规范可以参考HTTP 访问控制( CORS )。问题的关键在于,在创建跨域请求时,请求首部会带上额外信息,这部分不需要手动设置;服务端接收到跨域请求后,设置必要的响应首部信息,完成跨域的请求。
具体设置如下:
Apache
1
2
3
4Header set Access-Control-Allow-Origin *
Header add Access-Control-Allow-Headers "origin, content-type, authorization"
Header always set Access-Control-Allow-Methods "POST, GET, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Credentials trueNginx
1
2
3
4add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Origin,Content-Type';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
如果需要限制能进行跨域请求的域,在 nginx
中可以进行如下设置:1
2
3
4
5
6
7
8
9set $set_cross_origin 'http://www.a.com';
if ($http_origin ~* 'https?://(api.a.lar|localhost:4200)') {
set $set_cross_origin "$http_origin";
}
add_header 'Access-Control-Allow-Origin' '$set_cross_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'Authorization,Origin,Content-Type';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
注意
在服务器是 nginx
时,当响应状态码为40x、50x等错误码时,指令 add_header
会失效。
如果 nginx
的版本大于 1.7.5,可以指定第三个参数 always
来修正这问题;如果版本比较低,需要加入其它模块来完成响应首部信息的添加。具体可以参考Module ngx_http_headers_module、Headers More
2017年终总结
工作成绩
2017年主要参与了SDK的维护与升级,H5SDK的开发与维护,聚合SDK的开发与维护,说说英语的相关功能的参与。
经验教训
SDK的维护与升级,从第一版到第二版有很大的改进。第一是用户体系的升级:经历了从用户名登录到用户名或手机登录的的过程。这一功能的改进,主要是为了解决用户名密码注册流程繁琐,随机注册容易忘记账号的问题。通过手机直接注册,一来可以省去输入密码的环节,二来可以解决用户忘记账号的情况。我觉的在实现过程中出现了一些状况,主要是用户名和手机两个字段的冲突问题。应该一开始就区分注册的来源与登录的来源,比如通过验证码的话,必然是通过手机号进行的操作,那么与之对应的唯一标示字段就是mobile,如果是通过密码进行的操作,与之对应的唯一标示就是用户名。执行这一策略也许会在操作流程上引入一些环节,但是可以避免在系统中引入bug的可能。第二是新增游戏盒子的功能,盒子的定制功能的引入,如果要贯彻下去,应该是要有很大的技术资源投入的,否则不足以支撑比较广泛的定制需求,而基础的定制功能对需求方来说会比较鸡肋。所以现在盒子基本上是只有自己在使用的一个功能。第三是关于SDK后台的问题,随着项目的扩大,功能的增多,后台集成的功能越来越多,对于非技术的使用者来说,不一定了解每一个功能如何使用,对于新进来的管理员来说,需要重复的教。从角色的功能的角度出发,可以按照每个角色的权限范围来做一套系统的使用说明文档。
H5SDK基于SDK已有的功能来进行。由于一开始对目标与需求的不明确导致后续的一系列问题。第一是h5的回调通知与原有游戏App的回调通知格式不一致,导致在与游戏厂商对接的过程中需要做两份不同的文档,给对接的过程增加困扰。第二是一开始没有考虑到h5游戏同时会有App包的情况,在分发的环节出现两个入口的问题,一个apk下载入口,一个web在线入口,并且在对接的时候游戏方要对接两套文档。而且现在已经累积了很多上线的H5游戏,从根本上来解决这个问题变得比较困难了。
在开发H5SDK的过成功,使用到了前端框架VUE,掌握了基本的使用。VUE适用于前端驱动的项目,需要分离前后端,与App的通讯类似,但是由于浏览器端的开放性,在浏览器端做比较复杂的加密并不现实,需要引入其他的安全保障机制。关于VUE的一些使用心得,有一下几点
- 比较轻量级,有比较全面的中文文档支持,入门比较方便。
- 在项目构架层面没有做出太多规范,如果使用者在这一方面的能力不足,会随着项目规模的扩大变得越来越难维护。
- 建议在一些小的项目中做尝试,累积经验。
在做聚合SDK的开发前期,对这一项目的目标与流程有一个完整深入的了解,结合前期SDK开发过程中的经验,规避了一下可能会踩到的坑。项目开发过程中,始终贯彻的一个理念是解构。解构整个流程分为两大部分,与渠道的对接和与CP的对接。两个过程由两个相互解耦的模块负责:AgentSdk
/PSdk
。AgentSdk
负责平台与渠道的对接过程中的签名、验签、参数转换等功能。PSdk
负责与CP对接过程中的签名、验签、参数转换等功能。开发过程中先按照思路整理出必要的文档,随着项目的推进不断对文档进行修改与完善。最终开发完毕,重复使用文档到内部对接、外部对接的过程中去,是一次比较愉快的编程体验。
由于前期需求中有web聊天的功能,找到了一些开源的框架来完成,期间接触到Angular。Angular与VUE是统一类框架。关于Angular的一下使用心得:
- Angular新版本是基于TypeScript来实现,对开发者更友好。
- Angular框架本身包含了很多模块与组件,功能非常强大。
- Angular框架本身在项目构架层面做了很多的工作,开发过程中参考文档来实现,可以是项目进展更顺利,对于大型的前端项目来说是比较好的选择。
在工作之余了解了一下laravel,并阅读了部分模块的源码,对laravel有了一个比较基础的认识。laravel借鉴了很多其他语言的优秀框架的实现,包括container,provider这些概念,以及依赖注入的实现等等。laravel的编码非常规范,用到的一些设计模式和一些编程技巧,用很大的使用价值。即使不能使用到项目中,也建议有时间去了解一下,是非常好的学习资料。
今后打算
多学多看多动手,敢于尝试,从各个方面积极寻找可以提升工作效率的更好的手段。
个人建议
- 线下小组分享的内容要讨论提取精华后运用到工作中去。
- 可以尝试将前后分离的实践
- API通讯过程中可以采用其他的安全措施,目前RSA的加密方式,对于带参数调试的情况支持不够。
- API版本与文档的管理
php返回json数据,int型字段显示为string型的问题
开发过程中遇到,同一个接口在不同环境下返回格式不一致的问题。由于前端使用TypeScript开发,对数据类型敏感,本来应该是int型的数据,接口返回格式表示却为string型,导致运行报错。
由于在本地测试没有问题,在服务端返回错误,基本确定为开发环境导致的错误。通过在网上搜索相关类型的问题,得出是 mysql
引擎返回数据时导致的问题。
可能导致该问题的原因有:
PDO
进行链接时的参数设置:ATTR_EMULATE_PREPARES = false
,ATTR_STRINGIFY_FETCHES = false
php
扩展安装不正确:php-mysql
扩展换成php-mysqlnd
本人遇到的情况属于第二种,第一种情况并未做进一步测试,替换成功后问题解决。1
yum remove php71w-mysql && yum install -y php71w-mysqlnd
php 中的引用计数、写时复制、写时改变
php 中的这三个概念涉及到php 变量的实现,zval结构:1
2
3
4
5
6typedef struct _zval_struct {
zvalue_value value; // 保存变量的值
zend_uint refcount; // 变量引用数
zend_uchar type; // 变量类型
zend_uchar is_ref; // 是否引用
} zval;
其中,refcount、is_ref
是问题的主角。
引用计数
- 例一
1
2
3
4<?php
$var = 1;
$var_dup = $var;
?>
在上述代码中,第一行创建一个变量$var,并为其赋值1。实际上是创建了一个zval 的数据结构,并将变量$var 与之关联,此时zval 的 refcount=1。第二行将变量$var 赋值给新的变量$var_dump,在这一步中,$var_dump实际上也是与前文中的zval 数据结构进行关联,refcount加一。引用计数在赋值的时候发生。这样处理的结果是可以节省内存开销。