“中国要复兴、富强,必须在开源软件领域起到主导作用,为了国家安全和人类发展,责无旁贷,我们须为此而奋斗”——By:云客
当看到JSON API时,脑海中是不是就想到了它是一个服务器和各种客户端定义的接口?客户端可能是浏览器、app、微信小程序等等,然后它们和服务器通过JSON格式来相互传输数据。那么这个接口是随意自定义的吗?实际上为了跨应用,JSON API被设计成了一个通用规范,详见:
http://jsonapi.org/
该规范说明客户端应该如何请求获取或修改资源,以及服务器应该如何响应这些请求,它的目标是在不影响可读性、灵活性、可发现性的情况下,实现最小化请求数和传输的数据量,目前是1.0版本,后续版本仍处于发展修订中,在我们平时的项目中也应该遵循该规范。
在Drupal中JSON API是由核心模块jsonapi负责实现的,注意Drupal的JSON API实现仅支持实体,换句话说JSON API模块完全是基于Drupal的实体(包括配置实体)而实现的,这充分发挥了Drupal数据结构的优越性,不支持其他自定义的数据,但Drupal提供了REST API支持任意数据,她们间的区别见后。
JSON API模块遵循开箱即用的极简原则,没有太多的配置,其设计充分考虑到了Drupal的权限控制,换句话说是在其权限控制之下运作的,当启用后,默认情况下如果权限具备则全部实体数据就都可读取了。
实体数据在JSON API中被称为资源resources,采用HTTP方法POST、DELETE 、PATCH、GET分别进行资源的增、删、改、查。
获取数据:
首先来看看如何通过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的过滤器参数非常强大,她支持条件,以及按OR或AND构成的条件组,这样可以多层嵌套组合多种条件,我们先从最基本的单个条件开始讲起,一个单条件按如下格式即可:
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
实际上Drupal的JSON 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新建操作一样,需要一些必须的请求头和用户认证方法:
Authorization:Basic YXBpOmFwaQ==
然后采用DELETE方法访问接口即可:
http://example.com/jsonapi/node/article/{{article_uuid}}
服务器返回204响应状态码,没有消息体
文件上传:
现在JSON API已经支持文件上传了,详见:
https://www.drupal.org/node/3024331
请同时参考《云客drupal源码分析》的文件上传相关内容
JSON API:
是完全基于实体的,因此仅限于实体内容(包括配置实体),专注于Drupal的优势
RESTfull:
可用于任意数据,任意数据格式、逻辑、http方法,可配置性很强,但复杂度高,本身不支持排序、分页等,这些由“源”决定
注意:在RESTful中是用PUT方法进行更新,而不是PATCH,(但Drupal的RESTful模块并不是,依然是PATCH),它们主要有以下区别:
PUT是整体更新,且是幂等的(幂等idempotent:一个操作执行任意次对系统的影响跟一次是相同)
PATCH是对PUT的补充,意为局部更新,不用把完整信息对象传过去,且不是幂等的
幂等性在网络故障的情况下非常重要,发出请求但没有收到回复时,是重新发送还是先判断是否建立再发送从而避免信息重复呢?此时必须回答幂等性问题
JSON API不能做什么:
仅处理实体的CURD操作,不处理业务逻辑,比如用户登录、修改密码、
补充:
1、官网文档:
https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module
2、JSON API规范:https://jsonapi.org
3、对JSON API的加强模块:https://www.drupal.org/project/jsonapi_extras,其提供前缀修改、类型别名、资源禁用等等
4、配置实体通过JSON API只能读取
反馈互动