主要问题是,当页面中显示 xx 分钟,或 xx 小时,是由服务器处理还是客户端处理。
举个例子,我们有一个资源,比如帖子吧 posts,数据返回如下
|
|
我们所有的接口,统一都会有 created_at
这个数据,格式统一为 yyyy-mm-dd HH:mm:ss
。
"5 分钟前"
created_at
按照自己的需求做格式化哪一种解决方法更好呢
服务器能把这个时间处理好,返回给客户端吗?
我们完成一套接口,肯定是希望接口足够通用,我一点也不想考虑这个地方显示 “60 秒前” 还是 “1 分钟前”,鬼知道今天的设计稿和明天的设计稿还会不会是同一个样子。所以接口的通用性,应该体现在,我能满足任何设计稿,任何客户端的显示,但是当设计稿改变了,接口不会做乱七八糟的调整。
created_at
这种字段不可能改变,格式必须统一。
能不能新加一个字段,返回 x 分钟前
新加一个字段,比如我们叫 created_at_diff
,那么接口看起来是这个样子
|
|
接口居然跟设计稿耦合了,接口需要根据产品的一些显示逻辑而改变。抛开设计稿,把这个接口说通,就是,这个接口除了提供完整的资源数据之外,还细心的告诉客户端,你可以这样显示数据。为了让客户端有更好的用户体验,接口也是操碎了心啊。这个接口既然操了这份心,其他的接口是不是也要多操心一下。
那么为什么这个逻辑不能做到客户端里面去呢?谁家的接口会有这种东西啊?
客户端的本地时间可能不准,跟服务器时间做运算的时候可能有异常,所以建议服务器处理
其实这是一个时间同步的问题,每一个 http 请求的 response 中其实都会有一个 Date
的 header。这个为了说明报文是什么时候创建的
Date →Sat, 19 Aug 2017 16:09:39 GMT
格式也是固定统一的,客户端可以根据这个来完成与创建时间的计算。
或者这个时间差异可以放在最开始,app启动,用户登录,或者获取用户信息的时候,在全局存放时间差,这样也能供其他地方显示使用。
再退一步,2017-08-20 15:13:52
和 Sat, 19 Aug 2017 16:09:39 GMT
这样两个时间格式不统一啊,处理不方便,那么我们统一所有接口返回 X-SERVER-DATETIME → 2017-08-20 15:13:52
可好?
客户端的运算会耗时,列表数据很多的时候,会卡顿,建议服务器处理
这样的时间计算也会卡顿?这该如何来界定,找几个手机,5000次计算测试耗时?怎么都感觉这个计算量是不是太少了点…
客户端其实哪怕有一丁点的性能损耗,其实都可以由服务器处理
虽然不同意,但是 “额。。。”
设计稿变动的时候,客户端的变动成本大,需要重新发布新版本,由服务器处理好了,客户端直接显示,这样变动的成本会低很多
客户端显示成什么样,应该由自己控制,行间距啊,最多能显示多少个字,显示的美不美观等等。
举个例子,如果现在是客户端直接拿接口数据字符串 “5 分钟前” 显示在某个位置,某一天,产品说,这样不好,显示出来完整的时间,于是服务器数据返回了 “2017年08月20日 15:13:52”,客户端显示不下了,产品说,试试把精确到分钟,于是服务器返回 “2017年08月20日 15:13”,好像可以了。
这个例子感觉不应该存在,本来页面完全由客户端控制,变成了由服务器接口控制,是不是没有把各自该做的事情理清楚。页面变动就是应该客户端发版本,这样新旧版本之间都不会出现任何关系,也不会涉及到接口的改变。
作为一个接口,不能稳定通用,整天跟着产品样式做变动,感觉好难受。
ios 或 android 做这样的数据处理会麻烦很多,服务端处理起来特别方便。
逻辑其实都是一样的,根据当前时间与数据时间,做几个对比,有一些if else,只是用不同的语言去实现。不确定是否真的那么麻烦。
考虑到,每个人的理解,说服大家的难度,工时,工期,人力成本,结果是接口增加了一个 created_at_diff
的字段。
客有过主人者⑴,见其灶直突⑵,旁有积薪⑶,客谓主人:“更为曲突⑷,远徙其薪⑸,不者且有火患”。主人默然不应。俄而果失火,邻里共救之,幸而得息。于是杀牛置酒,谢其邻人,灼烂者在于上行⑹,余各以功次坐,而不录言曲突者⑺。人谓主人曰:“向使听客之言⑻,不费牛酒,终亡火患。今论功而请宾,曲突徙薪亡恩泽,焦头烂额为上客耶?”主人乃寤而请之⑼。
――节选自班固《汉书·霍光传》
注释
⑴过:拜访,探望。
⑵突:烟囱。
⑶薪:柴草。
⑷更:改。
⑸徙(xǐ):搬走。
⑹灼:烧炙。烂:烧伤。行(háng):座次。
⑺录:采,取,这里有邀请的意思。
⑻向使:向:原先。使:假使。
⑼寤(wù):醒悟,明白
有一位客人到主人家拜访,见主人家炉灶的烟囱是直的,旁边又堆有柴薪,这位客人便对主人说:‘您的烟囱应改为弯曲的,并将柴薪搬到远处去,不然的话,将会发生火灾!’主人默然,不予理会。不久,主人家果然失火,邻居们共同抢救,幸而将火扑灭。于是,主人家杀牛摆酒,对邻居表示感谢,在救火中烧伤的被请到上座,其余则各按出力大小依次就坐,却没有请那位建议他改弯烟囱的人。有人对这家主人说:‘当初要是听了那位客人的劝告,就不用杀牛摆酒,终究不会有火灾。如今论功请客酬谢,建议改弯烟囱、移走柴薪的人没有功劳,而在救火时被烧得焦头烂额的人才是上客吗?’主人这才醒悟,将那位客人请来。
“曲突徙薪之恩泽,焦头烂额为上客”:建议改弯烟囱、移走柴薪的人没有功劳,而在救火时被烧得焦头烂额的人成了上客
]]>当时的 larave mix 用着挺麻烦的,各种坑,所以我们直接使用的webpack 目前用着还挺顺手的。
这个坑是这样的,当时使用的是 webpack 2.1 的 beta 版本,webpack -p 一直很正常,但是前几天升级到了 webpack 2.2.1 ,相应的 把vue,vue-loader 等各种包也升级到了最新,也调整了 webpack 的配置,但是突然发现,webpack -p 一直报错语法错误。
不明白为什么,只能慢慢定位了,最终发现是 vue component 中只要使用了 es6 的语法,就会报语法错误,不在 component 中的 es6 语法就没问题。这个现象说明是 babel 没有转换 vue component 中的 es6。这我就不明白了,配置大概如下
|
|
原来一直没问题,没理由现在不行了啊,终于让我在 vue-loader 的 issues 中找到了答案。https://github.com/vuejs/vue-loader/issues/350
|
|
意思就是不用 query,用 .babelrc !!!
]]>几个星期以前,lumen 的核心组修正了一个单元测试调用 request 对象的问题,修正之前,request 是空的而且所有的表单数据在单元测试中都无法获取到。
这个问题关系到 Lumen 是如何启动的,简单的说,request 被过早的在启动程序中作为单例实例化了。在单元测试中创建 request 将不能修改 request 对象,因为它已经作为单例实例化了,这就是为什么我们不能在单元测试重建它。
为了解决这个问题,我们延迟了 request 实例的创建,直到应用开始分发 request 到路由,这与 laravel 的启动流程一致。
修正了这个问题,lumen 的用户将不能在你的 Service Providers 的任何地方使用 request 实例。这是因为 Lumen 注册 providers 的时候,request 对象还没有被实例化。
可代替的解决办法是复制所有 Service Providers 中使用 request 对象的代码,移动到一个全局中间件中;这个中间件会在 获取了 request 之后直接运行。
]]>很多人看到 REST 反应就是,利用 http 动词,处理资源, 随便看看就明白了。
这种人很容易就写出这样一种接口,所有的请求统一返回 200,body 中有,success, message, data, 大家的实现不同,但是大致就是这么个意思。
随处也都能看到这样的讨论,比如这里。
我们以一个简单的例子开始,假设我们需要写一套api,功能包括了用户,商品,订单,供应商。
写 REST 接口,首先需要明白资源
这个概念,所有的东西都是资源,我们始终是在操作资源,当然资源需要是个名词。
于是我们定义出这样一些资源,users, products, orders, vendors。注意这里是复数,因为既然是资源,肯定是一堆,是个集合。单复数的概念还是很重要的。
状态码是很重要的,是有意义的,客户端是需要根据状态码做判断的。比如201表示资源被创建了;204 表示请求成功了,但是并没有什么信息需要返回,body 当然是空;202 表示服务器接受了请求,但是还未处理,响应中应该包含相应的指示信息,告诉客户端该去哪里查询这次处理是否真正完成了等等。
你可能会遇到一些同事说它判断不了状态码,一定要解析body,body中需要一个字段表示是否成功…等等,那么你需要做的是跟他讲道理,让他看看 http 协议。
有用的链接 https://httpstatuses.com/
有了资源,我们就会需要对资源进行增删改查,对应到 http 的动词,就是 post,delete,put/patch,get。
以一个商品资源为例
http 动词 | url | 返回状态码 | 描述 |
---|---|---|---|
post | /api/products | 201 | 创建一个商品 |
get | /api/products | 200 | 获取商品列表 |
get | /api/products/{id} | 200 | 获取某个商品信息 |
put | /api/products/{id} | 200 / 204 | 完整的替换某个商品 |
patch | /api/products/{id} | 200 / 204 | 部分更新某个商品 |
delete | /api/products/{id} | 204 | 删除某个商品 |
可参考 rfc7231
用户,商品,订单,供应商 每个都是独立的资源,那么如果我有一个订单详情页面,显示哪个用户买的什么商品,买的谁的商品,什么时间买的。也就是同时获取了4个资源的信息,难道要请求4次吗?
比如订单id为10,用户id为1,商品id为5,供应商id为2。
或者是另一种解决办法,订单详情默认包含了它的商品,用户,供应商信息?
这样是请求了一次,但是如果客户端只需要订单信息,我为什么要进行额外的查询,返回额外的信息。
完美的方案
首先,接口的设计应该站在资源的角度,关心的不是页面如何显示,而是客户端需要什么资源,而需要什么当然只能是客户端自己决定。其次资源之间是有关联的,我们要利用资源之间的关系,于是可能是下面这样:
|
|
意思就是我需要10号订单的数据,同时需要订单相关的用户,供应商,商品信息。注意这里是单数,表示其相关的单个资源。
有了上面完美的方案,但是资源的数据到底是什么样的,又要怎么嵌套呢?先参考一下这里。
对于资源来说,肯定需要一个统一的结构,也方便我们嵌套。我们先理解一个简单好用的json结构。
|
|
data 中是这个资源的数据,meta 可选,是资源之外,其他的一些信息,比如分页。对于嵌套的资源同样也是这样。那么对于上面的请求,响应应该是下面这样。
|
|
再举个例子
|
|
利用好资源的关系和嵌套我们再补充几个接口
动词 | url | 描述 | includes |
---|---|---|---|
get | /api/vendors/{id}/products | 获取某个供应商的所有商品 | vendor |
get | /api/vendors/{id}/products/{id} | 获取某个供应商的某个商品 | vendor |
get | /api/vendors | 获取供应商列表 | products |
get | /api/user/orders | 我的订单列表 | vendor,products |
get | /api/users/{id}/orders | 某个用户的订单列表 | vendor,products |
注意最后两个,这里是参考了 github,用单数的 user 表示当前用户,因为我们如果有token,服务器就知道我们是谁。
当然这里只是个例子,真实业务我们可能并不能查看别人下的订单,
我们下了一个订单,可能会伴随很多状态,待付款,已付款,已发货,已收货,已取消等等,如何设计api呢?
其实一开始大家很可能会这么写
|
|
这样或许可以,但是我们引入了动词,有没有更好的方法呢?
其实我们始终是在更新订单状态
|
|
或许可以这样,对于资源来说我们就是要把订单的状态改为paid或者canceled。但是对于每个状态,提交的参数和要处理的数据可能有很大的不同,难道都写在一个方法里?
|
|
接口是一个,但是我们接受到请求只有依然是可以进行接口分发的,类似上面这样。
两个方法都是更新资源,而且幂等的,但是 put 是整个替换资源,首先需要判断必填项,然后根据请求替换这个资源。patch 是提交什么更新什么。
第二 put 是可以创建资源的,但是一般只存在于客户端可以指定资源id的情况下
|
|
更新资源id为100的资源,如果不存在则创建。创建的话返回201,更新的话返回200。这种情况很少见,因为现在基本上都是服务器生成id。所以对于我们平时处理的业务,其实大部分是 patch。
有了 api 当然需要区分版本,因为使用时需要更新的。
那么用什么来区分版本其实大体上有两种
|
|
或者利用 header
|
|
一些教程里面觉得放在url上更直观,比如阮一峰的教程,很多人也用 github 作为例子。
但是其实你看看 github api 的第一页,https://developer.github.com/v3/, github 及 其他一些的 rest 教程都是推荐第二种的。
上面是我的个人理解,目前基本是按照这个思路实现的接口,但是依然也有很多地方觉得不完美,不规范。欢迎指正和讨论
]]>php-cs-fixer 还没有支持到 psr4,对于 laravel 来说总是把 namespace 的 App 转为小写,所以需要自定义一些配置。
|
|
先大致计划一下
尽量多看点书吧
于是想整理一下我对数据传输安全的理解。
先看看几个名词定义
mac: 消息认证码 Message Authentication Code
hmac: 密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)
数据安全首先就是保证数据不被第三方得到,那么就得对数据进行加密,那么其实https干的就是这个事情。https保证了server的可靠性,也对数据进行了加密,所以首先应该尽量使用https。
首先就是对于webhook这种通知类的请求,就比如说是支付宝某个订单支付成功了,异步通知我的服务器这个订单支付成功了,即便我们使用了https,也无法知道这个通知是支付宝服务器发起的。
https 是在传输层保证了数据安全,那么在某些情况下
这些时候就需要我们自己做签名的处理,参考微信签名算法和支付宝签名算法
https://github.com/liyu001989/signature
记录一下我设计数据表和字段的几个原则。基本上也是 laravel 默认的行为。
数据表名字为名词复数,users, posts, factories。
很好理解,因为是个表嘛,一条记录是一个user,那么一张表就是users。不要加什么前缀了,基本上一个项目一个数据库了,xx_users的目的是啥啊。
关联关系的字段为名词单数 + 下划线id,user_id, post_id, factory_id。
看情况使用枚举enum
这里有两种观点,一个是tinyint,一个是枚举。tinyint的问题是数字本身没有意义,我必须得找个地方去定义这些个数字,STATUS_PENDING_PAY = 0, STATUS_PAID = 1。基本上也都是定义在模型中了。
但是枚举就很方便,pending_pay, paid, 本身有意义。
http://komlenic.com/244/8-reasons-why-mysqls-enum-data-type-is-evil/
这里也有很多讨论,实在很纠那么就如上所说,不经常变得,选项比较少的,不使用数字的情况下,使用严格模式的enum。其他情况使用tinyint,个人还是很喜欢有意义的enum的。
如果使用枚举定义个状态之类的字段,那么我习惯用动词进行时和过去式,比如pending_pay, paid,successed,canceled。或者全部大写。
isset($post->user->name)
始终为 falseisset 用来检测变量是否设置
首先我们来看官方的一个例子
大致上是下面这个意思
|
|
上面这个例子将永远返回 false,因为 foo 并不是 Post 的属性,而是 __get 取出来的
那么怎么解决上面那个问题呢?使用魔术方法
|
|
看着 laravel 5.1.35 的代码,我们自己写一个简单的例子
先有一个 Model,简单的实现。__get
,__set
,__isset
|
|
然后我们定义一个 Post Moel 和一个 User Moel
|
|
好了来验证一下isset
|
|
分析上面的结果,感觉像是 php 7 isset 方法对对象的判断有了变化,如果先执行一次,$post->user->name,也就是将 user 放在 post 的 relations 中,这样 isset($post->user) 为 true,随后 isset($post->user->name) 才为 true。
最后在 Eloquent model 的 git log 中 找到了答案,
|
|
大致上是 php7 isset 判断的时候,会依次判断。php5.6 则会预加载关系。其实 laravel 也早在5月份就做了相关的处理,所以升级 laravel 后,自然也就没有这个问题了。
]]>http://docs.phpcomposer.com/05-repositories.html 文档里已经有详细的介绍了。其实不管你的私有仓库在哪,是 git 还是 svn,是 bitbucket 还是 gitlab,只要告诉 composer 去哪里找到这个包就可以了,像下面这样:
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/igorw/monolog"
}
],
"require": {
"monolog/monolog": "dev-bugfix"
}
}
url 可以使用 https 和可以是 ssh。https 的话需要输入用户名和密码,不过 composer 会帮助你记录密码到全局的 auth.json 文件中。
事实又一次教育我们,先看文档是多么的重要。
]]>官方安装地址 https://about.gitlab.com/downloads
phphub 教程 https://phphub.org/topics/2568
使用现有nginx服务器 http://www.liaohuqiu.net/cn/posts/non-bundled-web-server-for-gitlab/
配置导入github项目 http://docs.gitlab.com/ee/integration/github.html
支持https,使用cerbot https://certbot.eff.org
阮一峰对各种工作流程对比 http://www.ruanyifeng.com/blog/2015/12/git-workflow.html
]]>不知道为什么,周围很多人都这么用,全都说不出个为什么,但是就是觉得这样是对的,因为他也没遇到过什么大问题。
那么 composer 安装完了以后,到底提不提交 composer.lock,和 vendor 目录呢?
这里 http://docs.phpcomposer.com/01-basic-usage.html#composer.lock-The-Lock-File 有详细的介绍,用来锁定我们安装的包版本的。
通过 composer 下载的各种第三方的包,都会在 vendor 目录里面。
正确的方式是提交 composer.lock,但是不提交 vendor 目录,使用composer install 安装第三方依赖
"laravel/framework": "5.1.*"
,这个时候 larval 最新的版本是 5.1.4,执行 composer install
, larval 5.1.4 被下载到 vendor 目录中,同时生成了 composer.lock 文件,lock 文件中锁定了使用 laravel 5.1.4。我开始了愉快的开发,并把 composer.json 和 composer.lock 两个文件提交到代码库里面。composer install
,因为有composer.lock文件的存在,自然他们下载的也是5.1.4。大家的代码都是一致的,绝对没有问题composer install
,同样的,也是 5.1.4 。本地,同事,服务器的代码都一致,完全没毛病。composer update laravel/framework
,这时 laravel 5.1.7 被下载到 vendor 目录中,同时 composer.lock 更新了锁定说明为 5.1.7。我只需要提交 comopser.lock即可。同事们更新了代码,同样只是执行 composer install
,运维人员同样也是更新代码,composer install
。有了 composer.lock 的保障,所有通过 composer install
下载的包绝对是一样的,那么当然就没有必要提交 vendor 目录。composer update
应该交给指定的人更新,更新完了要测试,毕竟是第三方的库,不能保证他完全正确,又或是版本指定错了等等不确定因素吧。
应该是图片自身的原因,找到如下代码,解决了问题
http://stackoverflow.com/questions/36069618/php-getimagesize-reverses-width-height
|
|
大概是图片的元信息除了问题,EXIF (exchangeable image file format) ,没再细看了,很复杂的样子,先记录一下。
]]>电脑一直是vpn + shadowsocks组合。composer, bower, npm啥的还是需要翻墙的,所以需要的时候就连vpn。
我的服务器是日本的conoha,比digitalOcean新加坡服务器的快很多,毕竟离得近,但是总是觉得不够好用。连接需要一阵,而且不够稳定,偶尔还断开。后来发现命令行也是可以shadowsocks的。
大致都是修改http_proxy,将所有的http请求代理到shadowsocks。
手机当然也想翻墙了,原来用的openvpn,同样是感觉不够好用,后来同事推荐了 shadowrocket
,6块钱,简直是超值,手机也能用shadowsocks,好爽啊。
原来公司服务器在国外,后天突然就弄回国内了,所以各种服务就用不了了,比如paypal支付,facebook,twitter,google等的oauth登录。问了一圈人,没有找到非常好的办法。决定用cow试试,欣慰的是guzzlehttp这个库很好的支持了http_proxy
,只需要设置系统变量即可。于是supervisor把cow挂起来,http_proxy
指向cow, facebook,google登录就搞定了。但是,twitter居然用的oauth1,omnipay也不支持代理,所以我有满世界的找了两个可以使用代理的库,总算是完成了。
shadowsocks挺好用的,平时基本够用了,但是偶尔还是需要vpn的
参考的 ucloud的教程,挺有帮助,https://docs.ucloud.cn/software/vpn/OpenVPN4Ubuntu.html
安装openvpn
|
|
配置 vars
|
|
配置 server
|
|
生成证书
|
|
启动服务,修改iptables
|
|
拷贝配置到本地
主要是这4个文件(ca.crt,client1.crt,client1.key,ta.key)
增加客户端配置文件,大概长这样
client
dev tun
proto tcp
remote xxx.xxx.xx.xx 1194 # 你的服务器ip
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert client1.crt
key client1.key
comp-lzo
verb 3
需求是这样的,如果在订单状态改变的时候给对应的用户发送消息,拽过来同事开始讨论
我们有好多好多的订单,好多好多的地方会改变订单状态,显然2,3两个方案太扯淡了,每个处理逻辑的地方都需要增加代码,处理一堆逻辑,写着就不爽,维护起来不是要疯了?
好吧我们来看看laravel Eloquent save的时候到底触发了哪些事件
|
|
首先触发的当然是saving
,如果saving返回的是false,那么save就失败了,返回false
接着如果$this->exists,就是这个model不是新创建的,那么就需要进行更新操作
|
|
看看更新干了什么,
首先如果这个模型dirty了,也就是脏了,也就是有属性改变了,那么才需要更新
先触发updating
,更新失败了就false,
更新时间戳
这里又判断了count($dirty) > 0,为什么??更updateTimestamps有什么关系吗?没有仔细看,以后再说吧
然后执行update,也就是写入了数据库
updated
那么如果这个对象是新建的,也就是需要执行插入操作,performInsert也做了差不多的事,
creating
,created
。最后回到save方法,如果更新或者插入操作成功了,那么就finishSave来结束save,
finishSave 触发了saved
事件, 最后syncOriginal。
小结一下
saving
->creating
->created
->saved
saving
->updating
->updated
->saved
好了知道了我们知道了model save的时候触发了哪些事件,但是还是没有解决我们的问题啊,到底是因为状态改变了触发的还是因为订单其他的值改变了触发的呢?订单原来的状态怎么获取呢?
这就需要看看isDirty和syncOriginal是怎么回事了
打印过Eloquent 对象的应该都会发现,其实模型会有两个数组,一个original,一个attributes。
这下应该就明白了,初始化一个模型的时候这两个数组是一样的,赋值操作只是改变attributes,所以isDirty也就是根据两个数据来判断模型是否dirty了,模型触发完saved
事件后才会执行syncOriginal,syncOriginal也就是将attributes赋值给original。所以上面触发的所有事件,我们都是能拿到原来的值和变化的值得。
所以我们的问题也就解决了,当模型触发updated
事件的时候,我们根据isDirty([‘status’]) 知道订单状态改变了,然后拿到订单之前是个什么样子,改变后是个什么样子,该发什么消息也就很明朗了。
laravel设计的好牛逼,事实教育我们,要多看文档,多看代码。幸好我机智的看了代码,要是用了2,3两个方案,我不是该哭了?
]]>2016年过去一半了,感觉也没学什么,还是很挫,要学的还有很多