sun_iit

sun_iit 查看完整档案

上海编辑常州信息职业技术学院  |  计算机网络技术 编辑  |  填写所在公司/组织 blog.notfound.top 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

sun_iit 收藏了文章 · 2019-08-20

HTTP缓存和浏览器的本地存储

一、HTTP缓存

http请求做为影响前端性能极为重要的一环,因为请求受网络影响很大,如果网络很慢的情况下,页面很可能会空白很久。对于首次进入网站的用户可能要通过优化接口性能和接口数量来解决。但是,对于重复进入页面的用户,除了浏览器缓存,http缓存可以很大程度对已经加载过的页面进行优化。

1.缓存位置

clipboard.png

从缓存位置上来看,分为4种,从上往下依次检查是否命中,如果但都没有命中则重新发起请求。
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。
Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。它可以一边解析js/css文件,一边网络请求下一个资源。
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
绝大部分的缓存都来自Disk Cache,在HTTP 的协议头中设置。
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

2.用户操作对缓存的影响

clipboard.png

clipboard.png

下面主要说一下前端优化能入手的地方,也就是强缓存协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

clipboard.png

3.强缓存

浏览器在第一次访问接口后的response headers里会携带一些字段,这些字段决定关于这个请求的缓存情况,
与强缓存相关的header字段有两个:

1、expires:过气网红,这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如Mon, 10 Jun 2015 21:31:12 GMT,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源

2、cache-control:新星:max-age=number,这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;
no-cache:不使用本地缓存。需要使用协商缓存,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
  注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires

强缓存时段命中,会直接从缓存中返回数据,返回值200;这一时间段,不管接口内容有没有变化都不会进行请求更新。

4.协商缓存

当没有强缓存时,会向服务端寻求帮助,也就是问一下服务端有没有更改,向接口判断是否有缓存。如果命中协商缓存则返回304状态码,并且从本地返回缓存内容。如果没有命中,则重新发起请求。
协商缓存需要跟服务端通过特殊标示连接,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。

具体过程如下:

Last-Modified/If-Modified-Since

1.浏览器第一次跟服务器请求一个资源,respone的header里加上Last-Modified:表示这个资源在服务器上的最后修改时间

2.浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header:上一次请求时返回的Last-Modified的值

3.服务器再次收到资源请求时,会判断最后修改时间是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容,Last-Modified会被修改为最新的值。如果没有变化,服务器返回304 Not Modified,Last-Modified不会修改,response header中不会再添加Last-Modified的header

4.浏览器收到304的响应后,就会从缓存中加载资源

Etag/If-None-Match

由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

1.一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

2.某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

3.某些服务器不能精确的得到文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

二、浏览器本地存储

浏览器本地缓存最常用的是cookie、localStroage、sessionStroage、webSql、indexDB。

1.cookie使用

cookie的用法很简单,可以通过服务端设置,js也可以通过documnet.cookie="名称=值;"(不要忘记以;分割)来设置。
cookie的字符串可以用encodeURIComponent()来保证它不包含任何逗号、分号或空格(cookie值中禁止使用这些值).
cookie一般用做为登陆态保存、密码、个人信息等关键信息保存使用,所以为了安全也是遵守同源策略原则的。
可以通过下面参数具体设置:
;path=path (例如 '/', '/mydir') 如果没有定义,默认为当前文档位置的路径。
;domain=domain (例如 'example.com', 'subdomain.example.com') 如果没有定义,默认为当前文档位置的路径的域名部分。与早期规范相反的是,在域名前面加 . 符将会被忽视,因为浏览器也许会拒绝设置这样的cookie。如果指定了一个域,那么子域也包含在内。
;max-age=max-age-in-seconds (例如一年为606024*365)
;expires=date-in-GMTString-format 如果没有定义,cookie会在对话结束时过期这个值的格式参见Date.toUTCString()
;secure (cookie只通过https协议传输)
;HttpOnly 限制web页面程序的browser端script程序读取cookie

缺点
容量有限制,不能超过4kb
在请求头上带着数据安全性差

2.localStorage和sessionStorage使用

html5新增本地存储,localStorage生命周期是永久,除非主动清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除。而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。也是遵守同源策略原则

// 1、保存数据到本地
// 第一个参数是保存的变量名,第二个是赋给变量的值
localStorage.setItem('key', 'value');
//复杂类型储存需要**利用JSON.stringify**将对象转换成字符串;
//利用**JSON.parse**将字符串转换成对象
// 2、从本地存储获取数据
localStorage.getItem('key');
// 3、从本地存储删除某个已保存的数据
localStorage.removeItem('key');
// 4、清除所有保存的数据
localStorage.clear();

3. Web SQL

WebSQL是前端的一个独立模块,是web存储方式的一种,我们调试的时候会经常看到,只是一般很少使用。并且,当前只有谷歌支持,ie和火狐均不支持。
主要方法:

1.openDatabase:这个方法使用现有的数据库或者新建的数据库创建一个数据库对象。
2.transaction:这个方法让我们能够控制一个事务,以及基于这种情况执行提交或者回滚。
3.executeSql:这个方法用于执行实际的 SQL 查询。

4.indexDB

IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
具体概念参考:参考文章

查看原文

sun_iit 赞了文章 · 2019-08-20

HTTP缓存和浏览器的本地存储

一、HTTP缓存

http请求做为影响前端性能极为重要的一环,因为请求受网络影响很大,如果网络很慢的情况下,页面很可能会空白很久。对于首次进入网站的用户可能要通过优化接口性能和接口数量来解决。但是,对于重复进入页面的用户,除了浏览器缓存,http缓存可以很大程度对已经加载过的页面进行优化。

1.缓存位置

clipboard.png

从缓存位置上来看,分为4种,从上往下依次检查是否命中,如果但都没有命中则重新发起请求。
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。
Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。它可以一边解析js/css文件,一边网络请求下一个资源。
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
绝大部分的缓存都来自Disk Cache,在HTTP 的协议头中设置。
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

2.用户操作对缓存的影响

clipboard.png

clipboard.png

下面主要说一下前端优化能入手的地方,也就是强缓存协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

clipboard.png

3.强缓存

浏览器在第一次访问接口后的response headers里会携带一些字段,这些字段决定关于这个请求的缓存情况,
与强缓存相关的header字段有两个:

1、expires:过气网红,这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如Mon, 10 Jun 2015 21:31:12 GMT,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源

2、cache-control:新星:max-age=number,这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;
no-cache:不使用本地缓存。需要使用协商缓存,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
  注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires

强缓存时段命中,会直接从缓存中返回数据,返回值200;这一时间段,不管接口内容有没有变化都不会进行请求更新。

4.协商缓存

当没有强缓存时,会向服务端寻求帮助,也就是问一下服务端有没有更改,向接口判断是否有缓存。如果命中协商缓存则返回304状态码,并且从本地返回缓存内容。如果没有命中,则重新发起请求。
协商缓存需要跟服务端通过特殊标示连接,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。

具体过程如下:

Last-Modified/If-Modified-Since

1.浏览器第一次跟服务器请求一个资源,respone的header里加上Last-Modified:表示这个资源在服务器上的最后修改时间

2.浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header:上一次请求时返回的Last-Modified的值

3.服务器再次收到资源请求时,会判断最后修改时间是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容,Last-Modified会被修改为最新的值。如果没有变化,服务器返回304 Not Modified,Last-Modified不会修改,response header中不会再添加Last-Modified的header

4.浏览器收到304的响应后,就会从缓存中加载资源

Etag/If-None-Match

由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

1.一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

2.某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

3.某些服务器不能精确的得到文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

二、浏览器本地存储

浏览器本地缓存最常用的是cookie、localStroage、sessionStroage、webSql、indexDB。

1.cookie使用

cookie的用法很简单,可以通过服务端设置,js也可以通过documnet.cookie="名称=值;"(不要忘记以;分割)来设置。
cookie的字符串可以用encodeURIComponent()来保证它不包含任何逗号、分号或空格(cookie值中禁止使用这些值).
cookie一般用做为登陆态保存、密码、个人信息等关键信息保存使用,所以为了安全也是遵守同源策略原则的。
可以通过下面参数具体设置:
;path=path (例如 '/', '/mydir') 如果没有定义,默认为当前文档位置的路径。
;domain=domain (例如 'example.com', 'subdomain.example.com') 如果没有定义,默认为当前文档位置的路径的域名部分。与早期规范相反的是,在域名前面加 . 符将会被忽视,因为浏览器也许会拒绝设置这样的cookie。如果指定了一个域,那么子域也包含在内。
;max-age=max-age-in-seconds (例如一年为606024*365)
;expires=date-in-GMTString-format 如果没有定义,cookie会在对话结束时过期这个值的格式参见Date.toUTCString()
;secure (cookie只通过https协议传输)
;HttpOnly 限制web页面程序的browser端script程序读取cookie

缺点
容量有限制,不能超过4kb
在请求头上带着数据安全性差

2.localStorage和sessionStorage使用

html5新增本地存储,localStorage生命周期是永久,除非主动清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,sessionStorage仅在当前会话下有效,关闭页面或浏览器后被清除。而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信。也是遵守同源策略原则

// 1、保存数据到本地
// 第一个参数是保存的变量名,第二个是赋给变量的值
localStorage.setItem('key', 'value');
//复杂类型储存需要**利用JSON.stringify**将对象转换成字符串;
//利用**JSON.parse**将字符串转换成对象
// 2、从本地存储获取数据
localStorage.getItem('key');
// 3、从本地存储删除某个已保存的数据
localStorage.removeItem('key');
// 4、清除所有保存的数据
localStorage.clear();

3. Web SQL

WebSQL是前端的一个独立模块,是web存储方式的一种,我们调试的时候会经常看到,只是一般很少使用。并且,当前只有谷歌支持,ie和火狐均不支持。
主要方法:

1.openDatabase:这个方法使用现有的数据库或者新建的数据库创建一个数据库对象。
2.transaction:这个方法让我们能够控制一个事务,以及基于这种情况执行提交或者回滚。
3.executeSql:这个方法用于执行实际的 SQL 查询。

4.indexDB

IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
具体概念参考:参考文章

查看原文

赞 87 收藏 64 评论 4

sun_iit 收藏了文章 · 2019-08-07

Laravel 获取当前 Guard 分析 —源自电商购物车的实际需求

果酱社区 产品中关于购物车的需求比较复杂,我们基于 overture/laravel-shopping-cart 扩展出了更加符合电商需求的购物车包,之前有文章进行过简单的介绍: Laravel shopping cart : 电商购物车包,线上完美运行中

源码地址: ibrand/laravel-shopping-cart

原需求

最开始扩展这个包时是因为以下需求:

  • 用户登录后的购物车数据需要存储在数据库中。因为客户希望能够直观的看到目前购物车中商品信息,以便推送优惠信息来促使转化。虽然我们按照 GA 的标准把数据传送过去了,但是我们发现 GA中数据并不是非常准确。
  • 用户在商城中的购物车数据
  • 导购使用导购小程序代用户下单或结账时加入的购物车数据,不和用户购物车数据同步。

原解决方案

最初需求出来的时候,我们通过不同的 Guard 来作为用户购物车数据的区分,因为商城和导购是两种不同的用户系统,所以当时在购物车 ServiceProvider 中的代码如下:

            $currentGuard = null;
            $user = null;
            $guards = array_keys(config('auth.guards'));
            foreach ($guards as $guard) {
                if ($user = auth($guard)->user()) {
                    $currentGuard = $guard;
                    break;
                }
            }
            if ($user) {
                //The cart name like `cart.{guard}.{user_id}`: cart.api.1
                $cart->name($currentGuard.'.'.$user->id);
            }else{
                throw new Exception('Invalid auth.');
            }

通过循环遍历目前所有的 Guards 来获取目前请求中用户所属的 guard 值和用户对象,本来在新需求未增加时,一切都运行的挺正常。

新需求

18年新增的需求:

  • 用户门店扫码(二维码或条形码)自助下单的购物车数据要和商城的购物车数据区分

也就是现在存在三种购物车数据类型

  • 用户在商城的购物车数据
  • 用户在线下门店中自助下单的购物车数据
  • 导购的购物车数据

新需求解决方案

新需求出现的时候,为了区分购物车数据,肯定是直接新建一个 guard,所以在 config/auth.php 中添加了 shop guard 代码如下。

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],

        'api' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],

        'shop' => [
            'driver' => 'passport',
            'provider' => 'users',
        ],

        'clerk' => [
            'driver' => 'passport',
            'provider' => 'clerk',
        ],
    ],

问题产生

本以为会运行良好,但是我们忽略了一个细节,apishop 两个 guard 的 provider 是一样的,因为都是属于用户的,而 api 又定义在 shop 前面,所以当执行如下代码时会出现问题,因为循环到 auth('api')->user() 的时候就退出了,导致了 shop guard 的数据也会存储成 api guard.

           foreach ($guards as $guard) {
                if ($user = auth($guard)->user()) {
                    $currentGuard = $guard;
                    break;
                }
            }

解决方案

之前工程师未发现合适的方法,所以采用了循环遍历 guards 来判断当前请求已认证 guard,当新需求产生后,这个方法不再适用,但是又不能对当前的购物车包进行大改,所以只有再找解决办法,

思路

在 Laravel 中 request()->user() 都会获取到正确已认证的 guard user 数据,所以准备决定从这里的源码入手。

源码分析

request()->user() 实际调用的代码是 Illuminate\Http\Request class 中 user() 方法

    public function user($guard = null)
    {
        return call_user_func($this->getUserResolver(), $guard);
    }

追踪源码到 Illuminate\Auth\AuthServiceProvider class,具体执行的代码为:return call_user_func($app['auth']->userResolver(), $guard);

    protected function registerRequestRebindHandler()
    {
        $this->app->rebinding('request', function ($app, $request) {
            $request->setUserResolver(function ($guard = null) use ($app) {
                return call_user_func($app['auth']->userResolver(), $guard);
            });
        });
    }

继续追踪源码到 Illuminate\Auth\AuthManager class

    public function shouldUse($name)
    {
        $name = $name ?: $this->getDefaultDriver();

        $this->setDefaultDriver($name);

        $this->userResolver = function ($name = null) {
            return $this->guard($name)->user();
        };
    }

到这里其实就结束了,我们发现 request()->user() 最终执行的代码是 $this->guard($name)->user() 。所以我们需要查看下 shouldUser 方法是在哪里调用的。

仍然看源码 Illuminate\Auth\Middleware\Authenticate:

    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }

代码最终到这里基本比较清楚了,已认证用户的请求会通过 Authenticate middleware ,并且把系统默认的 guard 设置为当前请求的 guard.

//获取到已认证用户的 guard
       foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

设置已认证 guard 为默认 guard,代替 config('auth.defaults.guard') 中的值

    public function shouldUse($name)
    {
        $name = $name ?: $this->getDefaultDriver();

        $this->setDefaultDriver($name);

        $this->userResolver = function ($name = null) {
            return $this->guard($name)->user();
        };
    }

最终方案

所以获取到当前请求的 Guard 值,可以直接通过 AuthManager class 中的 getDefaultDriver() 即可。

if ($defaultGuard = $app['auth']->getDefaultDriver()) {
   $currentGuard = $defaultGuard;
   $user = auth($currentGuard)->user();
}

讨论交流

联系我们

查看原文

sun_iit 收藏了文章 · 2019-07-31

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

sun_iit 赞了文章 · 2019-07-31

RESTful API 设计规范

RESTful API 设计规范

该仓库整理了目前比较流行的 RESTful api 设计规范,为了方便讨论规范带来的问题及争议,现把该文档托管于 Github,欢迎大家补充!!

Table of Contents

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见:RFC 2119

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*
如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目
后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构
至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法
2 创建新的资源 必须 使用 POST 方法
3 更新资源 应该 使用 PUT 方法
4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法URL描述
GET/zoos列出所有的动物园(ID和名称,不要太详细)
POST/zoos新增一个新的动物园
GET/zoos/{zoo}获取指定动物园详情
PUT/zoos/{zoo}更新指定动物园(整个对象)
PATCH/zoos/{zoo}更新动物园(部分对象)
DELETE/zoos/{zoo}删除指定动物园
GET/zoos/{zoo}/animals检索指定动物园下的动物列表(ID和名称,不要太详细)
GET/animals列出所有动物(ID和名称)。
POST/animals新增新的动物
GET/animals/{animal}获取指定的动物详情
PUT/animals/{animal}更新指定的动物(整个对象)
PATCH/animals/{animal}更新指定的动物(部分对象)
GET/animal_types获取所有动物类型(ID和名称,不要太详细)
GET/animal_types/{type}获取指定的动物类型详情
GET/employees检索整个雇员列表
GET/employees/{employee}检索指定特定的员工
GET/zoos/{zoo}/employees检索在这个动物园工作的雇员的名单(身份证和姓名)
POST/employees新增指定新员工
POST/zoos/{zoo}/employees在特定的动物园雇佣一名员工
DELETE/zoos/{zoo}/employees/{employee}从某个动物园解雇一名员工
超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。
  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&per_page=100:指定第几页,以及每页的记录数。
  • ?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pageper_page

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

GET /trades?status=closed&sort=sortby=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

{
    "access_token": "token....",
    "token_type": "Bearer",
    "expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
    "error": "invalid_token"
}
Laravel 开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": 0,
    "msg": "success",
    "data": {
        "username": "username"
    }
}

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "code": -1,
    "msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码描述
1xx代表请求已被接受,需要继续处理
2xx请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx重定向
4xx客户端原因引起的错误
5xx服务端原因引起的错误
只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功。

必须强调的是,所有 API一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

{
    "message": "您查找的资源不存在",
    "error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:19:27 GMT
Connection: keep-alive

{"error_code":403002,"message":"用户已禁用"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

{
    "message": "直接展示给终端用户的错误信息",
    "error_code": "业务错误码",
    "error": "供开发者查看的错误信息",
    "debug": [
        "错误堆栈,必须开启 debug 才存在"
    ]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
    "user": {
        "id":1,
        "nickname":"fwest",
        "username": "example"
    }
}

正确示例:

1、获取单个资源详情

{
    "id": 1,
    "username": "godruoyi",
    "age": 88,
}

2、获取资源集合

[
    {
        "id": 1,
        "username": "godruoyi",
        "age": 88,
    },
    {
        "id": 2,
        "username": "foo",
        "age": 88,
    }
]

3、额外的媒体信息

{
    "data": [
        {
            "id": 1,
            "avatar": "https://lorempixel.com/640/480/?32556",
            "nickname": "fwest",
            "last_logined_time": "2018-05-29 04:56:43",
            "has_registed": true
        },
        {
            "id": 2,
            "avatar": "https://lorempixel.com/640/480/?86144",
            "nickname": "zschowalter",
            "last_logined_time": "2018-06-16 15:18:34",
            "has_registed": true
        }
    ],
    "meta": {
        "pagination": {
            "total": 101,
            "count": 2,
            "per_page": 2,
            "current_page": 1,
            "total_pages": 51,
            "links": {
                "next": "http://api.example.com?page=2"
            }
        }
    }
}
其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
    "id": 1,
    "avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
    "nickname": "fwest",
    "last_logined_time": "2018-05-29 04:56:43",
    "created_at": "2018-06-16 17:55:55",
    "updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive
这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=https://example.com" />

        <title>Redirecting to https://example.com</title>
    </head>
    <body>
        Redirecting to <a href="https://example.com">https://example.com</a>.
    </body>
</html>

所有 API一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期
客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该 在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

版权声明

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

建议参考

restful-api-design-references

Principles of good RESTful API Design(译)

理解 RESTful 架构

RESTful API 设计指南

HTTP 状态码- 维基百科

LICENSE

MIT License

Copyright (c) 2018 godruoyi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

查看原文

赞 190 收藏 228 评论 34

sun_iit 收藏了文章 · 2019-07-05

PHP 对输入变量名的自动转换的问题与源码分析

原文地址:https://www.hongweipeng.com/i...

起步

表单提交到PHP脚本时,底层的PHP会做一层转换。将一些符号转成下划线 _

20190701132308.png

实际上这层转换中会发生很多意想不到的情况。

列举这些情况

20190701134005.png

一个简单的测试就出现了意外,一个是单个 [ 也会被替换,对于 array 的输入, key 不会做转换。于是我多多测了一下,得出如下列表:

<input name="a.b" />        转为: $_REQUEST["a_b"]
<input name="a b" />        转为: $_REQUEST["a_b"]
<input name="a[b" />        转为: $_REQUEST["a_b"]
<input name="a]b" />        转为: $_REQUEST["a]b"]
<input name="a-b" />        转为: $_REQUEST["a-b"]
<input name=" ab" />        转为: $_REQUEST["ab"]
<input name="ab " />        转为: $_REQUEST["ab "]
<input name="arr[a.b]" />   转为: $_REQUEST["arr"]["a.b"]
<input name="ar.r[a.b]" />  转为: $_REQUEST["ar_r"]["a.b"]
<input name="arr[a[b]]" />  转为: $_REQUEST["arr"]["a[b"]
<input name="arr[a[]x]" />  转为: $_REQUEST["arr"]["a["]
<input name="arr[]ab" />    转为: $_REQUEST["arr"][0]
<input name="arr[a]b" />    转为: $_REQUEST["arr"]["a"]
<input name="arr[a.b" />    转为: $_REQUEST["arr_a.b"]
<input name="arr[[a.b" />   转为: $_REQUEST["arr_[a.b"]

这个转换机制十分诡异是吧。查了一下,在 Bug#77172 convert error on receiving variables from external sources 中提出了 id[]_text 转换成 id[] 的问题,采取的结果是补全文档上的说明。

另外也有几个讨论是否关闭这层转换:

这三个 Request 都还是 open 状态,还没有结果,其中关于关闭转换的讨论早在06年就提出来了。我不清楚 PHP 为什么会做这个转换,目的是什么。据我所知的 java,Django 都不会做转换的。

PHP对于外部输入的变量都会转换的,这就涉及到了 $_POST, $_GET, $_FILES, $_COOKIE, $_REQUEST 这些变量了。

源码分析

虽然我没有阅读过php源码,在朋友的帮助下,关于这部分的转换代码在 main/php_variables.cphp_register_variable_ex 函数中 php_variables.c#L68 ,源码精简了下流程:

PHPAPI void php_register_variable_ex(char *var_name, zval *val, zval *track_vars_array)
{
    char *p = NULL;
    char *ip = NULL;        /* index pointer */
    char *index;
    char *var, *var_orig;

    /* ignore leading spaces in the variable name */
    while (*var_name==' ') { // 忽略前置空格
        var_name++;
    }

    for (p = var; *p; p++) {
        if (*p == ' ' || *p == '.') {   // 空格和点替换成下划线
            *p='_';
        } else if (*p == '[') {
            is_array = 1;     // 如果遇到 [ 则视为数组,is_array 设为1
            ip = p;
            *p = 0;
            break;
        }
    }
    ...
}

这里可以看出,忽略前置空格是最先做的动作;当遇到第一个 [ 时,php则认为数数组,不再进行转换,设置了 is_array = 1 就 break 了。

这个 is_array 有什么用呢,往下看:

if (is_array) {
    int nest_level = 0;
    while (1) {
        char *index_s;
        size_t new_idx_len = 0;
        ip++;    // [ 的下一个字符
        index_s = ip;
        if (*ip==']') { // 如果下一个字符就已经是],表示没有设置key
            index_s = NULL;
        } else {
            ip = strchr(ip, ']');  // 查找剩余字符串中的 ]
            if (!ip) {
                /* PHP variables cannot contain '[' in their names, so we replace the character with a '_' */
                *(index_s - 1) = '_'; // 如果没找到,则将 [ 替换成下划线

                index_len = 0;
                if (index) {
                    index_len = strlen(index);
                }
                goto plain_var;
                return;
            }
            *ip = 0;
            new_idx_len = strlen(index_s);  // key 的长度到第一个出现 ] 为止
        }
    }
    ...
}

到此,转化处理的过程就很清晰了,对于数组情况的变量名,分为两种:

  1. 没找到 ] 与其匹配,该变量名不是数组,将 [ 替换成下划线,后续字符串不做处理;
  2. ] 与其匹配,取到第一个出现 ] 的位置作为 key ,舍弃后面的字符。

对于情况1 就很奇怪了,如果输入是 arr[[a.b 那么就会转成成 arr_[a.b 了。

总结

鉴于当前的转换规则总结的规律如下:

  1. 在第一个 [ 之前的字符中,忽略前置的空格,将 .空格 替换成下划线 _
  2. 在第一个 [ 之后的字符,不再进行替换处理:

    • 若后续字符中没有] 时,第一个 [ 替换成 _ ,后续字符串不做转换;
    • 若后续字符中] 时,取到第一次出现 ] 的位置作为 key,舍弃后续字符。

另外,谁能告诉我PHP的这层转换的设计初衷是什么啊。

查看原文

sun_iit 收藏了文章 · 2019-04-16

yii2的上传只需这一篇技术文就足够了

很多新人对上传这种操作有些害怕、摸不着头脑,今天阿北带你学习一边yii2 UploadFile类,看看yii2如何帮你轻松的完成上传操作。

老样子,先来个目录

  • 初步熟悉UploadedFile类

  • 从Model开始的上传

  • 没有Model的上传

UploadedFile类

UploadedFile类位于 vendor/yiisoft/yii2/web/UploadedFile.php

它提供了比如 getInstancegetInstancesgetInstanceByNamesaveAs等诸多方法
详情可以参考速查表 http://nai8.me/tool-sc.html#s...

从Model开始的上传

我们先来设计一个简单的会员表,实现对其头像的上传功能。

图片描述

然后我们新建一个action并写下如下代码。

// @app/FileController.php
public function actionIndex(){
    $model = User::findOne(1);
    if(Yii::$app->request->isPost){
        $file = UploadedFile::getInstance($model,'avatar');
        var_dump($file);
    }

    return $this->render('index',[
        'model'=>$model
    ]);
    // 视图里代码 <?= $form->field($model,'avatar')->fileInput();?>
}

其实UploadedFile只是对上传文件信息的再次封装并提供一组用于操作该文件的方法,就像上面代码一样,我们将对UploadedFile获取的上传对象进行打印,看看它到底是什么?

在这里我们使用yii2-debug来观察这一切。

图片描述

而通过UploadedFile::getInstance接收后形成的对象 $file 如下图

图片描述

在这里你看到了,对于UploadedFile对象,它的属性和上传文件的信息一致,我们暂时认为 $file = 上传文件 + 一些方法,并且它可以像正常Model接收数据一样获取上传的文件。

既然我们已经得到了文件,它就在$file,那么现在我们将它保存到指定的目录就好了。

这要用到 UploadedFile 提供的saveAs方法,当然在这里你也可以用 getExtension 获取文件扩展名等等。

但是要注意,此刻我们操作的是一个具体的对象,saveAs是保存这个文件、getExtension是获得此文件的扩展名,这些都是对象方法,不是静态方法,初学者自行体会。

// 继续编写actionIndex
...
if(Yii::$app->request->isPost){
    $file = UploadedFile::getInstance($model,'avatar');
    $path = 'data/'.time().".".$file->getExtension();
    if($file->saveAs(Yii::getAlias("@webroot").'/'.$path) === true){
        $model->avatar = $path;
    }
    $model->update();
}
...

要注意的是,saveAs的参数是一个服务器物理路径。

执行后,我数据库中已经有了该头像的相对路径。

图片描述

当然这是最简单的,你也可以通过Model的rules 或 action自己的逻辑实现对文件的验证,比如扩展名、比如大小、比如等等。

这一切貌似很轻松就完成了,我们不再用去处理蹩脚的原生PHP上传文件,但是有几种特殊情况还需要说下。

如何一下上传并接受多个文件

比如我的user增加了一列identity_card,代表身份证,我现在要用来存放身份证的正反面图片

按照上面的需求,视图肯定是要变动下,将identity_card变为一个数组

<?= $form->field($model,'identity_card[]')->fileInput();?>
<?= $form->field($model,'identity_card[]')->fileInput();?>

然后我们使用 UploadedFile::getInstances来完成,核心代码如下

...
if(Yii::$app->request->isPost){
    $files = UploadedFile::getInstances($model,'identity_card'); // 不是identity_card[]
    var_dump($files);
}
...

getInstances能帮我们接收一个数组类型的fileInput,看看结果

图片描述

然后我们可以对这个$files结果集进行遍历等操作,据具体场景而定即可。

没有Model的上传

上面都是通过一个模型来接收客户传递过来的文件,它很方便,我们接下来介绍下jie'sho不通过Model如何去处理一个文件或一组文件上传接收问题。

在这里我不计划写任何代码,给大家一个图,也许明白的够透彻。

图片描述

话说其实 getInstanceByNamegetInstancesByName才是UploadedFile真正核心的函数,getInstance和getInstances最后也是调用了它们。

现在你会了么?接收一个或一组文件。

还有些小细节

getBaseName()

该函数能获取上传文件的原始名,需要注意的是返回的名字不包含扩展名。

getExtension()

该函数可以获取上传文件的扩展名,比如一个文件为a.png,则该函数返回的是png而不是image/png,另外扩展名均处理为小写了已经。

几个变量

另外如果我们也可以直接调用一些变量

  • type 返回比如image/png这样的媒体类型

  • size 返回文件的大小

  • name 文件原始名(含扩展名)

  • tempName 系统存放文件的临时名

以上就是UploadedFile的所有用法和一些变量,希望对你有用。

更多yii2技术干货 欢迎来到我的小站 http://nai8.me

查看原文

sun_iit 收藏了文章 · 2019-04-12

还我一个干净的Mac OS(如何彻底删除不需要的App)

当初我买MacBook Air的时候特地选择了高配版本,硬盘空间是128G(2011年的古董机器),在最开始的1-2年内使用还不错,后来发现越来越力不从心,为了尽可能腾出空间,几乎把所有的照片、视频和PDF文档都移到了云上,也经常用CleanMyMac等工具进行清理,但还是发现空间被一些已经卸载的App蚕食,既然没有更好的方案,那就自己写一个工具来清理吧。

主界面

前几节内容是比较繁琐的技术介绍,不感兴趣的同学可以直接跳到工具的使用那一节

App最喜欢的几个目录

Mac和Windows操作系统有一个很大的不同,大部分App是没有安装程序的,一般下载下来就是一个dmg文件,解开之后直接将App拖到应用程序目录下就可以了,所以给人感觉卸载也就是将App拖到废纸篓然后清空。如果真这样做就大错特错,即使一个最简单的App都会在下面几个目录中或多或少留下纪念,这些目录一般有:

  • ~/Library

  • ~/Library/Application Support

  • ~/Library/Application Support/CrashReporter

  • ~/Library/Caches

  • ~/Library/Containers

  • ~/Library/LaunchAgents

  • ~/Library/Preferences

  • ~/Library/PreferencePanes

如果一个程序是通过pkg方式安装,或者是在第一次运行时请求管理员权限,那一般还会在如下几个目录中留点纪念:

  • /Library

  • /Library/Application Support

  • /Library/Extensions

  • /Library/LaunchAgents

  • /Library/LaunchDaemons

  • /Library/PreferencePanes

  • /Library/Preferences

以上都还是只是列出了部分,不同的App由于需要还会在其他目录中安装一些文件,比如字处理软件一般会在/Library/Fonts下面安装字体,有些扩展组件会放在/Library/ScriptingAdditions下面。

限于篇幅,我不能一一介绍所有目录的内容,选几个重点的目录大概说一下:

~/Library/Application Support

这个目录可以说是App最重要的文件目录,一些App本身并不大,但是需要的支持组件和内容非常多,特别是采用第三方插件和缓存网络内容的软件,例如我用的Dash,本身的大小只有24MB左右,但是由于经常浏览资料,所以缓存了很多网络内容,使得它的Support目录达到了1.37GB!可想而知,如果只是将App扔到废纸篓,几乎不会节省任何空间。
Dash的占用空间情况

~/Library/Preferences

这个目录下保存的是App设置,一般来说,稍微有点规模的软件都有自己的配置信息,放在这里的文件虽然占空间不多,但是清理掉还是有好处的,例如有些App不能正常工作,即使卸载重装也不行,往往就是配置文件出错了,把这里清理掉之后再重装就会有比较好的效果。

~/Library/Containers

这个目录对于App Store上下载的软件来说,是最重要的一个目录,由于Apple的限制,AppStore的软件都在沙箱中运行,每个软件在沙箱都有自己的一个完整空间,对于App来说,它以为自己在一个正常的目录系统中运行,但实际上不是,操作系统重定位了读写位置到沙箱之中。
要是卸载软件的时候,只是简单的在Launchpad中点一下叉来删除,那不知道又有多少空间莫名其妙地消失了,我曾经用这种方式卸载了网易云音乐,结果后来才发现,整整417MB的空间不见了……
网易云音乐卸载之后仍然占用大量空间

~/Library/LaunchAgents

Mac上的随机启动方式之一,熟悉OS X操作系统的同学应该立刻想到了launchctl,这里就是存放启动配置文件的地方,大部分App只会往这里写启动信息,请神容易送神难,要送神,只有用第三方工具或是自己手动清理了,还有,LaunchAgents 和 LaunchDaemons 是有一些区别的,两者虽然都是随机启动,详细对比请看LaunchAgents VS LaunchDaemons

本工具的概述

直到现在我还在想,要是用Cocoa来写这个工具的话,程序会小很多,现在这个程序压缩包有40+MB,其实绝大部分都是Electron、React的框架库,真正的代码也就几个文件,加起来不到1M大小。而之所以选择用JavaScript来做,其实就是想验证一下React开发一个桌面应用到底会怎样,JavaScript是不是有一统江湖的本事?

部分代码

用JavaScript最大的顾虑也许是性能问题,程序虽然难度不大,也有几个运算比较密集的地方,一是plist信息的解析,二是正则表达式。我将plist解析放到外部库去做了,正则表达式仍然用Google的V8引擎,目前来看还行。

程序的I/O比较多,需要频繁统计文件占用空间大小,查找相关文件,读取文件信息,虽然JavaScript的最大好处是异步操作,但是在统计出结果之前,界面能做出再多的响应也没有什么意义,因此在有些地方,我没有使用异步操作,要是你感觉界面有点卡,那是我的原因,不是React的错:)

再给大家推荐一下Mac系统上自带的AppleScript,这个是Mac上的瑞士军刀,能和Windows上的Powershell有得一比,我在这个软件中最后的清理工作实质上就是通过AppleScript来完成的,例如清理工作首先要关闭目标程序,你要是自己编程打算怎样实现?枚举系统进程然后kill?用AppleScript就能非常优雅地完成,例如你打开Safari,再同时开一个终端窗口,然后在终端中输入

osascript -e 'quit app "safari"' 

名字的来由

来自于同名电影《Total Recall》,奎德在一次虚拟体验之后,突然发现自己原来不是一个默默无闻的工人,而是一个超级特工,那自己到底是什么呢?也许真正的自己永远也找不回来了,但是至少不是现在这样浑浑噩噩。

我也想借这个意思表示即使通过努力清理,也不一定能还原到最初的状态,但至少不会这么糟糕。

工具的使用

这个好像没有什么太多可以说的了,我只是对比了和CleanMyMac的清理效果,至少,在大部分情况下,我感觉还不错。

卸载百度云的对比

CleanMyMac的查找情况

可以清楚地看到,CleanMyMac没有找到百度云的Application Support目录,此外,只找到了登录项,没有找到启动项和启动文件。

Total Recall的查找情况

Total Recall不仅找到了CleanMyMac所有的内容,还找到了Application Support,这里有1.14MB空间,此外还找到了LaunchDeamons里面的启动项和启动文件,还发现了com.baidu.netdiskmac.BDYunFinderInstaller这么一个鬼……

比较一下卸载AppStore上的App

感觉找到了挺多的啊……

其实只有两个,别忽悠了

CleanMyMac列出了一大堆感觉挺吓人的,仔细看才发现其实都是Containers\com.tencent.xinWeChat这个目录里面的子目录,还拿一个系统临时目录来充数(电脑重启后系统临时目录都会清空),卸载的时候其实只要删除沙箱目录就万事大吉。

存在的不足

  • 有一类软件是以Package方式发布的,里面有复杂的安装步骤,例如Microsoft Office 2016 for Mac这样的,对于这类软件,需要分析的内容比较多,打算在完善后将Package卸载整合

  • 软件界面很简陋,一是不大会玩CSS和HTML布局,二是初次使用React,前端高手可以指点我一下?

参考内容

  • 《深入解析MAC OS X & IOS操作系统》 这本书对于想了解Mac系统的人来说是不二之选,值得拥有

  • Information Property List Key Reference App里最重要的文件Info.plist的介绍,开发必备

  • Electron 如果打算用Electron构建一个桌面App,必须从这里开始

  • React React的官方网站,虽然我的界面做得挺难看,那是因为我还不怎么会玩css

  • Total Recall 这……

查看原文

sun_iit 赞了文章 · 2019-04-12

还我一个干净的Mac OS(如何彻底删除不需要的App)

当初我买MacBook Air的时候特地选择了高配版本,硬盘空间是128G(2011年的古董机器),在最开始的1-2年内使用还不错,后来发现越来越力不从心,为了尽可能腾出空间,几乎把所有的照片、视频和PDF文档都移到了云上,也经常用CleanMyMac等工具进行清理,但还是发现空间被一些已经卸载的App蚕食,既然没有更好的方案,那就自己写一个工具来清理吧。

主界面

前几节内容是比较繁琐的技术介绍,不感兴趣的同学可以直接跳到工具的使用那一节

App最喜欢的几个目录

Mac和Windows操作系统有一个很大的不同,大部分App是没有安装程序的,一般下载下来就是一个dmg文件,解开之后直接将App拖到应用程序目录下就可以了,所以给人感觉卸载也就是将App拖到废纸篓然后清空。如果真这样做就大错特错,即使一个最简单的App都会在下面几个目录中或多或少留下纪念,这些目录一般有:

  • ~/Library

  • ~/Library/Application Support

  • ~/Library/Application Support/CrashReporter

  • ~/Library/Caches

  • ~/Library/Containers

  • ~/Library/LaunchAgents

  • ~/Library/Preferences

  • ~/Library/PreferencePanes

如果一个程序是通过pkg方式安装,或者是在第一次运行时请求管理员权限,那一般还会在如下几个目录中留点纪念:

  • /Library

  • /Library/Application Support

  • /Library/Extensions

  • /Library/LaunchAgents

  • /Library/LaunchDaemons

  • /Library/PreferencePanes

  • /Library/Preferences

以上都还是只是列出了部分,不同的App由于需要还会在其他目录中安装一些文件,比如字处理软件一般会在/Library/Fonts下面安装字体,有些扩展组件会放在/Library/ScriptingAdditions下面。

限于篇幅,我不能一一介绍所有目录的内容,选几个重点的目录大概说一下:

~/Library/Application Support

这个目录可以说是App最重要的文件目录,一些App本身并不大,但是需要的支持组件和内容非常多,特别是采用第三方插件和缓存网络内容的软件,例如我用的Dash,本身的大小只有24MB左右,但是由于经常浏览资料,所以缓存了很多网络内容,使得它的Support目录达到了1.37GB!可想而知,如果只是将App扔到废纸篓,几乎不会节省任何空间。
Dash的占用空间情况

~/Library/Preferences

这个目录下保存的是App设置,一般来说,稍微有点规模的软件都有自己的配置信息,放在这里的文件虽然占空间不多,但是清理掉还是有好处的,例如有些App不能正常工作,即使卸载重装也不行,往往就是配置文件出错了,把这里清理掉之后再重装就会有比较好的效果。

~/Library/Containers

这个目录对于App Store上下载的软件来说,是最重要的一个目录,由于Apple的限制,AppStore的软件都在沙箱中运行,每个软件在沙箱都有自己的一个完整空间,对于App来说,它以为自己在一个正常的目录系统中运行,但实际上不是,操作系统重定位了读写位置到沙箱之中。
要是卸载软件的时候,只是简单的在Launchpad中点一下叉来删除,那不知道又有多少空间莫名其妙地消失了,我曾经用这种方式卸载了网易云音乐,结果后来才发现,整整417MB的空间不见了……
网易云音乐卸载之后仍然占用大量空间

~/Library/LaunchAgents

Mac上的随机启动方式之一,熟悉OS X操作系统的同学应该立刻想到了launchctl,这里就是存放启动配置文件的地方,大部分App只会往这里写启动信息,请神容易送神难,要送神,只有用第三方工具或是自己手动清理了,还有,LaunchAgents 和 LaunchDaemons 是有一些区别的,两者虽然都是随机启动,详细对比请看LaunchAgents VS LaunchDaemons

本工具的概述

直到现在我还在想,要是用Cocoa来写这个工具的话,程序会小很多,现在这个程序压缩包有40+MB,其实绝大部分都是Electron、React的框架库,真正的代码也就几个文件,加起来不到1M大小。而之所以选择用JavaScript来做,其实就是想验证一下React开发一个桌面应用到底会怎样,JavaScript是不是有一统江湖的本事?

部分代码

用JavaScript最大的顾虑也许是性能问题,程序虽然难度不大,也有几个运算比较密集的地方,一是plist信息的解析,二是正则表达式。我将plist解析放到外部库去做了,正则表达式仍然用Google的V8引擎,目前来看还行。

程序的I/O比较多,需要频繁统计文件占用空间大小,查找相关文件,读取文件信息,虽然JavaScript的最大好处是异步操作,但是在统计出结果之前,界面能做出再多的响应也没有什么意义,因此在有些地方,我没有使用异步操作,要是你感觉界面有点卡,那是我的原因,不是React的错:)

再给大家推荐一下Mac系统上自带的AppleScript,这个是Mac上的瑞士军刀,能和Windows上的Powershell有得一比,我在这个软件中最后的清理工作实质上就是通过AppleScript来完成的,例如清理工作首先要关闭目标程序,你要是自己编程打算怎样实现?枚举系统进程然后kill?用AppleScript就能非常优雅地完成,例如你打开Safari,再同时开一个终端窗口,然后在终端中输入

osascript -e 'quit app "safari"' 

名字的来由

来自于同名电影《Total Recall》,奎德在一次虚拟体验之后,突然发现自己原来不是一个默默无闻的工人,而是一个超级特工,那自己到底是什么呢?也许真正的自己永远也找不回来了,但是至少不是现在这样浑浑噩噩。

我也想借这个意思表示即使通过努力清理,也不一定能还原到最初的状态,但至少不会这么糟糕。

工具的使用

这个好像没有什么太多可以说的了,我只是对比了和CleanMyMac的清理效果,至少,在大部分情况下,我感觉还不错。

卸载百度云的对比

CleanMyMac的查找情况

可以清楚地看到,CleanMyMac没有找到百度云的Application Support目录,此外,只找到了登录项,没有找到启动项和启动文件。

Total Recall的查找情况

Total Recall不仅找到了CleanMyMac所有的内容,还找到了Application Support,这里有1.14MB空间,此外还找到了LaunchDeamons里面的启动项和启动文件,还发现了com.baidu.netdiskmac.BDYunFinderInstaller这么一个鬼……

比较一下卸载AppStore上的App

感觉找到了挺多的啊……

其实只有两个,别忽悠了

CleanMyMac列出了一大堆感觉挺吓人的,仔细看才发现其实都是Containers\com.tencent.xinWeChat这个目录里面的子目录,还拿一个系统临时目录来充数(电脑重启后系统临时目录都会清空),卸载的时候其实只要删除沙箱目录就万事大吉。

存在的不足

  • 有一类软件是以Package方式发布的,里面有复杂的安装步骤,例如Microsoft Office 2016 for Mac这样的,对于这类软件,需要分析的内容比较多,打算在完善后将Package卸载整合

  • 软件界面很简陋,一是不大会玩CSS和HTML布局,二是初次使用React,前端高手可以指点我一下?

参考内容

  • 《深入解析MAC OS X & IOS操作系统》 这本书对于想了解Mac系统的人来说是不二之选,值得拥有

  • Information Property List Key Reference App里最重要的文件Info.plist的介绍,开发必备

  • Electron 如果打算用Electron构建一个桌面App,必须从这里开始

  • React React的官方网站,虽然我的界面做得挺难看,那是因为我还不怎么会玩css

  • Total Recall 这……

查看原文

赞 37 收藏 57 评论 43

sun_iit 收藏了文章 · 2019-03-21

一文弄懂Nginx的location匹配

由于团队在进行前后端分离,前端接管了Nginx和node层,在日常的工作中,跟Nginx打交道的时候挺多的。之前对location的匹配规则是一知半解的,为了搞明白location是如何匹配的,查了些资料总结此文。希望能给大家带来帮助。

语法规则

location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }

语法规则很简单,一个location关键字,后面跟着可选的修饰符,后面是要匹配的字符,花括号中是要执行的操作。

修饰符

  • = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
  • ~ 表示该规则是使用正则定义的,区分大小写。
  • ~* 表示该规则是使用正则定义的,不区分大小写。
  • ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

匹配过程

对请求的url序列化。例如,对%xx等字符进行解码,去除url中多个相连的/,解析url中的...等。这一步是匹配的前置工作。

location有两种表示形式,一种是使用前缀字符,一种是使用正则。如果是正则的话,前面有~~*修饰符。

具体的匹配过程如下:

首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来。

如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。

然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。

如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。

基于以上的匹配过程,我们可以得到以下两点启示:

  1. 使用正则定义的location在配置文件中出现的顺序很重要。因为找到第一个匹配的正则后,查找就停止了,后面定义的正则就是再匹配也没有机会了。
  2. 使用精确匹配可以提高查找的速度。例如经常请求/的话,可以使用=来定义location。

示例

接下来我们以一个例子来具体说明一下匹配过程。

假如我们有下面的一段配置文件:

location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /user/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

请求/精准匹配A,不再往下查找。

请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

请求/user/index.html匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。
请求/user/1.jpg匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

请求/images/1.jpg匹配D。首先进行前缀字符的查找,找到最长匹配D。但是,特殊的是它使用了^~修饰符,不再进行接下来的正则的匹配查找,因此使用D。这里,如果没有前面的修饰符,其实最终的匹配是E。大家可以想一想为什么。

请求/documents/about.html匹配B。因为B表示任何以/开头的URL都匹配。在上面的配置中,只有B能满足,所以匹配B。

location @name的用法

@用来定义一个命名location。主要用于内部重定向,不能用来处理正常的请求。其用法如下:

location / {
    try_files $uri $uri/ @custom
}
location @custom {
    # ...do something
}

上例中,当尝试访问url找不到对应的文件就重定向到我们自定义的命名location(此处为custom)。

值得注意的是,命名location中不能再嵌套其它的命名location

URL尾部的/需不需要

关于URL尾部的/有三点也需要说明一下。第一点与location配置有关,其他两点无关。

  1. location中的字符有没有/都没有影响。也就是说/user//user是一样的。
  2. 如果URL结构是https://domain.com/的形式,尾部有没有/都不会造成重定向。因为浏览器在发起请求的时候,默认加上了/。虽然很多浏览器在地址栏里也不会显示/。这一点,可以访问baidu验证一下。
  3. 如果URL的结构是https://domain.com/some-dir/。尾部如果缺少/将导致重定向。因为根据约定,URL尾部的/表示目录,没有/表示文件。所以访问/some-dir/时,服务器会自动去该目录下找对应的默认文件。如果访问/some-dir的话,服务器会先去找some-dir文件,找不到的话会将some-dir当成目录,重定向到/some-dir/,去该目录下找默认文件。可以去测试一下你的网站是不是这样的。

总结

location的配置有两种形式,前缀字符和正则。查找匹配的时候,先查找前缀字符,选择最长匹配项,再查找正则。正则的优先级高于前缀字符。

正则的查找是按照在配置文件中的顺序进行的。因此正则的顺序很重要,建议越精细的放的越靠前。

使用=精准匹配可以加快查找的顺序,如果根域名经常被访问的话建议使用=

查看原文

认证与成就

  • 获得 30 次点赞
  • 获得 48 枚徽章 获得 1 枚金徽章, 获得 14 枚银徽章, 获得 33 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-12-14
个人主页被 841 人浏览