154. 前后端解耦Drupal JSON API

   当看到JSON API时,脑海中是不是就想到了它是一个服务器和各种客户端定义的接口?客户端可能是浏览器、app、微信小程序等等,然后它们和服务器通过JSON格式来相互传输数据。那么这个接口是随意自定义的吗?实际上为了跨应用,JSON API被设计成了一个通用规范,详见:
         http://jsonapi.org/
   该规范说明客户端应该如何请求获取或修改资源,以及服务器应该如何响应这些请求,它的目标是在不影响可读性、灵活性、可发现性的情况下,实现最小化请求数和传输的数据量,目前是1.0版本,后续版本仍处于发展修订中,在我们平时的项目中也应该遵循该规范。

  在DrupalJSON API是由核心模块jsonapi负责实现的,注意DrupalJSON API实现仅支持实体,换句话说JSON API模块完全是基于Drupal的实体(包括配置实体)而实现的,这充分发挥了Drupal数据结构的优越性,不支持其他自定义的数据,但Drupal提供了REST API支持任意数据,她们间的区别见后。

   JSON API模块遵循开箱即用的极简原则,没有太多的配置,其设计充分考虑到了Drupal的权限控制,换句话说是在其权限控制之下运作的,当启用后,默认情况下如果权限具备则全部实体数据就都可读取了。

  实体数据在JSON API中被称为资源resources,采用HTTP方法POSTDELETE PATCHGET分别进行资源的增、删、改、查。

 

获取数据:
首先来看看如何通过JSON API获取数据,采用GET方法访问一下实体的url即可:

/jsonapi/{entity_type_id}/{bundle_id}[/{entity_id}]

在默认情况下JSON API模块采用“/jsonapi”作为URL前缀,但可以修改(详见本篇补充内容),后续地址段依次为:

entity_type_id:实体类型
bundle_id :实体bundle,如果某实体类型没有bundle,那么就以实体类型作为bundle
entity_id:实体ID,即为UUID,如果不存在即是访问列表数据,如果存在即访问单个数据

示例如下:

访问节点下的全部(其实是前五十篇,关于分页见后)文章:

  http://www.你的域名.com/jsonapi/node/article

访问节点下的某一篇文章:

http://www.你的域名.com/jsonapi/node/article/a67bb2b4-d011-4bac-972a-3f3e6b9d672b

服务器会返回一个json响应,消息体是一个JSON API对象,该对象有如下预定义根键:

jsonapi:指示JSON API规范版本号、元数据等
errors:如果发生异常,那么包含错误信息
links:资源的各种连接地址,如自己的、下一篇、上一篇等等
included:包含的和主资源相关的其他资源,如用户账户等
data:一个对象或数组,表示资源数据,其中type表示资源类型,格式为“实体类型--bundle”如“node—article”, id为资源id,其值是uuid,attributes的值是资源值(也就是实体属性值),relationships表示实体引用到的另外一个资源,如用户对象等

更多预定义根键可以参考:

         https://jsonapi.org/format/#document-top-level

以上都是返回一个完整的资源,也可以返回资源的一部分,如下:

http://www.你的域名.com/jsonapi/node/article?fields[node--article]=body,uid,title,created

这将在data中仅返回指定字段内容

 

过滤器:

在明白如何获取资源后,可以进一步过滤需要的资源,使用过滤器参数即可,Drupal的过滤器参数非常强大,她支持条件,以及按ORAND构成的条件组,这样可以多层嵌套组合多种条件,我们先从最基本的单个条件开始讲起,一个单条件按如下格式即可:

http://www.dp9.com/jsonapi/node/article?
&filter[title-filter][condition][path]=title
&filter[title-filter][condition][operator]=CONTAINS
&filter[title-filter][condition][value]=yunke

这里“title-filter”表示指定一个过滤器标识符,“value”表示值,

path”表示资源的路径,即实体的某字段属性,如果是引用字段或字段的子属性值,那么用点号连接各部分,如“some_relationship.1.some_attribute”、“field_phone.country_code”,当过滤配置属性时还可以采用“*”来代替路径的某部分

operator”表示比较操作,支持的比较操作定义在以下位置:

\Drupal\jsonapi\Query\EntityCondition::$allowedOperators

有如下这些:

  public static $allowedOperators = [
    '=', '<>',
    '>', '>=', '<', '<=',
    'STARTS_WITH', 'CONTAINS', 'ENDS_WITH',
    'IN', 'NOT IN',
    'BETWEEN', 'NOT BETWEEN',
    'IS NULL', 'IS NOT NULL',
       ];

注意:在url中以上操作符都会被编码,比如“=”会被编码为“%3D”即“urlencode("=")

以上是完整表示,Drupal提供了很贴心的简写方式,比如:

http://www.dp9.com/jsonapi/node/article?
&filter[title-filter][condition][path]=title
&filter[title-filter][condition][value]=yunke

表示查找标题等于“yunke”的资源,这里操作符被省略了,省略时默认被认为是“=”,可以进一步简写为:

http://www.dp9.com/jsonapi/node/article?
&filter[title][condition][value]=yunke

这里省略了过滤器标识符,还可以进一步采用最精简的方式:

http://www.dp9.com/jsonapi/node/article?
&filter[title] =yunke

那么如何构建条件组呢?假设我们要查询一些用户,条件限制如下:

名字的最后一个是以“J”开始,第一个是“Janis”或“Joan

连接如下:


?filter[rock-group][group][conjunction]=OR

&filter[janis-filter][condition][path]=field_first_name
&filter[janis-filter][condition][operator]=%3D
&filter[janis-filter][condition][value]=Janis
&filter[janis-filter][condition][memberOf]=rock-group

&filter[joan-filter][condition][path]=field_first_name
&filter[joan-filter][condition][operator]=%3D
&filter[joan-filter][condition][value]=Joan
&filter[joan-filter][condition][memberOf]=rock-group

&filter[last-name-filter][condition][path]=field_last_name
&filter[last-name-filter][condition][operator]=STARTS_WITH
&filter[last-name-filter][condition][value]=J

如你所见,我们先定义一个组,然后定义条件时用关键词“memberOf”指示本条件属于哪个组,如果没有该关键词的条件默认为第一级条件,以and方式连接

注意不要混淆过滤器和访问控制的关系,过滤器是用户可以设定的,权限控制是后台逻辑,依然要在后台检查,通常为了提高性能,我们需要用过滤器来过滤掉用户不可访问的内容

常见过滤器:

通过作者的用户名来过滤

filter[uid.name][value]=admin

通过账户的uuid来过滤,这里采用id是因为JSON API规范要求

filter[uid.id][value]=BB09E2CD-9487-44BC-B219-3DC03D6820CD

用一个操作符但有多值的情况:

filter[name-filter][condition][path]=uid.name
filter[name-filter][condition][operator]=IN
filter[name-filter][condition][value][1]=admin
filter[name-filter][condition][value][2]=john

补充:过滤日期值时采用ISO-8601格式

 

分页:

如果要对查询进行分页可以这样:

http://www.dp9.com/jsonapi/node/article/?page[offset]=5&page[limit]=2

实际上DrupalJSON API并不提供总量查询,主要是因为由于会对全部资源做权限检查,从而严重影响性能,为防止ddos攻击,每页数据量被限制为最高50个,如果确实需要更高的每页量,可以使用“JSON:API Page Limit module.”模块,地址为:

https://www.drupal.org/project/jsonapi_page_limit

此外page[limit]的值只是代表返回的数据中最多有这么多数据量,而不是保证返回一定有这么多,即使还有下一页的情况,这是因为后端是以该值去做数据库查询,然后在做权限检查,这可能会将不可访问的实体去除。既然不提供总量查询,每一页数据又可能不固定,那么如何知道是否还有下一页呢?就要靠返回JSON API中的links根键的值了,可能存在如下子健:

first:第一页链接
self: 当前页链接
next: 下一页链接
prev:前一页链接

如果存在next子健那么说明还有数据

 

排序:

在默认情况下资源是按created升序排序的,我们可以指定排序方式,完整写法如下:

sort[sort-created][path]=created
sort[sort-created][direction]=DESC

和过滤器类似,排序也可以有简写

sort=created或者sort=-created,负号表示降序

还能进行多条件排序:

简写:

sort=-created,uid.name

完整写法:

sort[sort-created][path]=created
sort[sort-created][direction]=DESC
sort[sort-author][path]=uid.name

此时按传递顺序确定先后权重

 

版本:

要获取某个资源的某个版本可以这样:

/jsonapi/node/article/ef64bc9a-a80f-4d71-b6c6-095e4aced7a2?resourceVersion=id:6

这里id后面是版本号,还可以用如下方式:

/jsonapi/node/page/{{uuid}}?resourceVersion=rel:latest-version

这里rel表示关系,latest-version表示最新版本,working-copy表示工作副本,目前JSON API模块还不支持获取版本集,版本功能也不是规范的一部分

目前资源的某个“版本”只能读取,暂只支持节点和媒体实体类型,当前drupal还没有针对版本的访问控制机制

 

翻译:

JSON API模块支持很简单的多语言功能,暂不支持高级应用,默认通过drupal的语言协商机制实现,将来打算采用JSON API规范的多语言机制,有一些注意事项:

当前不支持删除某个翻译,只能完整删除

有限的POST支持,即能够以非默认语言创建一个实体,但不允许在其上创建其他语言翻译

 

包含关联资源:

默认情况下JSON API是不包含关联资源的详细信息的,比如用户,那么可以通过关键词“include”带出,比如要带出用户账户数据可以这样:

http://www.dp9.com/jsonapi/node/article/a67bb2b4-d011-4bac-972a-3f3e6b9d672b?include=uid

 

新建操作:

JSON API模块的实体新建操作是通过POST请求完成的,为了你方便测试,建议下载“Postman”做客户端,它可以自定义POST请求的各种参数。

在默认情况下,JSON API模块只允许只读操作,因此需要先在以下配置页中打开写、改、删操作:

“/admin/config/services/jsonapi” 

写操作需要具备权限,因此建立一个有写权限的账户,JSON API模块的账户认证不采用Drupal的标准cookie会话认证机制,而是采用HTTP基本认证,因此需要开启WEB服务中的basic_auth模块

一切准备就绪后就可以提交POST请求了,在请求头中要有以下头:

Accept: application/vnd.api+json
Content-Type:application/vnd.api+json
Authorization:Basic YXBpOmFwaQ==

其中以上的“YXBpOmFwaQ==”来源于:

base64_encode("用户名:密码");

这里是:base64_encode("api:api");

请求体必须是json格式文本,类似如下:


{
  "data": {
    "type": "node--article",
    "attributes": {
      "title": "yunke 文章 title",
      "body": {
        "value": "云客通过json api 传送的内容",
        "format": "plain_text"
      }
    }
  }
}

提交地址为:http://www.你的域名.com/jsonapi/node/article/

注意HTTP方法选择POST,提交后,如果成功,服务器会返回新建文章的JSON API对象,此时就可以在后台看到新建的内容了,如果异常,将返回带错误提示的JSON API对象

JSON API规范中,每个POST请求仅允许创建一个资源,如果需要同时建立关联实体,可以考虑安装以下模块:

https://www.drupal.org/project/subrequests

 

更新操作:

POST新增请求几乎一样,不一样的是采用PATCH请求方法,数据体和请求URL均需要带上uuid,如:

必要的请求头:

Accept: application/vnd.api+json
Content-Type:application/vnd.api+json
Authorization:Basic YXBpOmFwaQ==

body体内容:

{
  "data": {
    "type": "node--article",
    "id": "0c0d992c-7ae7-4f79-a87c-596f17fa2f19",
    "attributes": {
      "title": "云客20210330更新修改测试",
      "body": {
        "value": "云客通过json api 传送的内容",
        "format": "plain_text"
      }
    }
  }
}

采用PATCH请求:

http://www.你的域名.com/jsonapi/node/article/0c0d992c-7ae7-4f79-a87c-596f17fa2f19

服务器返回200状态码,body是修改后的JSON API对象

 

删除操作:

POST新建操作一样,需要一些必须的请求头和用户认证方法:

Accept: application/vnd.api+json
Content-Type:application/vnd.api+json
Authorization:Basic YXBpOmFwaQ==

然后采用DELETE方法访问接口即可:

http://example.com/jsonapi/node/article/{{article_uuid}}

服务器返回204响应状态码,没有消息体

 

文件上传:

现在JSON API已经支持文件上传了,详见:

https://www.drupal.org/node/3024331

请同时参考《云客drupal源码分析》的文件上传相关内容

 

JSON APIRESTfull的区别:

JSON API

是完全基于实体的,因此仅限于实体内容(包括配置实体),专注于Drupal的优势

RESTfull

可用于任意数据,任意数据格式、逻辑、http方法,可配置性很强,但复杂度高,本身不支持排序、分页等,这些由“源”决定

 

注意:在RESTful中是用PUT方法进行更新,而不是PATCH,(但DrupalRESTful模块并不是,依然是PATCH),它们主要有以下区别:

PUT是整体更新,且是幂等的(幂等idempotent:一个操作执行任意次对系统的影响跟一次是相同)

PATCH是对PUT的补充,意为局部更新,不用把完整信息对象传过去,且不是幂等的

幂等性在网络故障的情况下非常重要,发出请求但没有收到回复时,是重新发送还是先判断是否建立再发送从而避免信息重复呢?此时必须回答幂等性问题

 

JSON API不能做什么:

仅处理实体的CURD操作,不处理业务逻辑,比如用户登录、修改密码、

 

补充:

1、官网文档:

https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonap…

2JSON API规范:https://jsonapi.org

3、对JSON API的加强模块:https://www.drupal.org/project/jsonapi_extras,其提供前缀修改、类型别名、资源禁用等等

4、配置实体通过JSON API只能读取

本书共161小节。


目前全部收费内容共295.00元。购买全部

评论 (0)