JianghuJS-后端接口(Reource)
学习目标
- 理解 jianghujs resource 协议概念。
- 学习使用 resource 协议标准,根据需求场景搭建数据页面。
1. Resource协议说明
Resource 是江湖JS 后端接口中的一种协议,江湖JS对不同端之间所传递的数据所做的规范与设计,接口协议数据结构设计上,进行了统一和简化,能够做到在不同通讯通道间无缝切换。
在江湖JS里,一个HTTP请求/Socket请求对应数据库_resource
表中的一行数据。
Resource协议的原理是根据请求body里的 pageId
& actionId
去匹配_resource
表中的数据,然后执行对应的 sql操作或者 service方法。
2. Sql Resource 和 Service Resource
我们把请求resource 分成两类:
sql resource
: 针对 SQL 语句的操作,可以实现较为简单的 CRUD 操作。service resource
: 后端根据请求找出/app/service下对应的service,执行对应的方法并将结果返回给前端.
3. Resource请求 & Resource响应
- 请求:
/${appId}/resource
- 请求方式:
POST
- 请求头:
- 'Content-Type': 'application/json'
- 'Accept': 'application/json'
- 请求body
where offset limit参数只有在 sql resource场景有用
协议字段 类型 描述 packageId String 必填✅ 协议包的唯一id; 可以使用时间戳 + 随机数生成; 比如: 1622720431076_3905352 packageType String 必填✅ 协议包类型; 比如:'httpRequest', 'socketForward', 'socketRequest' appData Object 必填✅ 协议包数据 --appId String 必填✅ 应用ID; 比如: demo_xiaoapp --pageId String 必填✅ 页面ID; 比如: demoPage --actionId String 必填✅ 操作ID; 比如: selectStudentList --authToken String 必填✅ 用户令牌 --userAgent String 选填 客户端浏览器信息; 通过 window.navigator.userAgent
获取--where Object 选填 where条件; 比如: { name: '张三丰', classId: '2021-01级-02班' } --whereIn Object 选填 where in查询条件; 比如: {name: ['张三丰', '张无忌']} --whereLike Object 选填 where 模糊查询条件; 比如: { name: '张%' } --whereOptions Array 选填 where条件 ; 比如: [['name', '=', 'zhangshan'],['level', '>', 3],['name', 'like', '%zhang%']] --whereOrOptions Array 选填 where or 条件; 比如: [['name', '=', 'zhangshan'],['level', '>', 3],['a', 100]] --offset Number 选填 查询起始位置; 比如: 0 --limit Number 选填 查询条数,默认查所有; 比如: 10 --orderBy Array 选填 排序; 比如: [{ column: 'age', order: 'desc' }] --actionData Object 选填 操作数据,比如:要保存或更新的数据... { name: '张三丰', level: '03' } - 响应body
字段 类型 描述 packageId String 必返回✅,请求唯一标识。 packageType String 必返回✅,标识数据包类型; httpResponse, socketResponse status String 必返回✅,标识业务状态; success: 成功, fail: 失败 timestamp String 必返回✅,响应时间, E.g: 2021-05-24 17:56:59,408" appData Object 必返回✅, sql resource
orservice resource
执行后的结果--appId String 必返回✅ 应用ID; 比如: demo_xiaoapp --pageId String 必返回✅ 页面ID; 比如: demoPage --actionId String 必返回✅ 操作ID; 比如: selectStudentList --errorCode String 错误码, 当status==='fail'时返回, E.g: token_expired, request_body_invalid --errorReason String 错误码说明, 当status==='fail'时返回, E.g: token失效, params字段字段却失 --errorReasonSupplement String 错误码说明 补充, E.g: "id must not be null" --resultData Object 必返回✅ resource请求的响应数据
参考用例
const package = {
packageId: '123456',
packageType: 'httpRequest',
appData: {
appId: `${window.appInfo.appId}`,
pageId: 'protocolDemo',
actionId: 'insertItem',
userAgent: 'demo_userAgent',
authToken: localStorage.getItem(`${window.appInfo.appId}_authToken`),
actionData: {
studentId: 'G00003',
classId: '2021-01级-02班',
gender: 'male',
level: '02',
name: '小虾米',
},
where: { 字段A: 'A', 字段B: 'B' },
whereIn: { name: ['张三丰', '张无忌'] },
whereLike: { 字段A: 'A', 字段B: 'B' },
whereOptions: [['name', '=', 'zhangshan'],['level', '>', 3]],
whereOrOptions: [['name', '=', 'zhangshan'],['level', '>', 3]],
offset: 10,
limit: 20,
orderBy: [{ column: 'email' }, { column: 'age', order: 'desc' }],
}
};
const result = await axios(package);
const rows = result.data.appData.resultData.rows;
console.log('rows', rows);
4. jianghuAxios 发起 Resource 请求
jianghuAxios是江湖JS框架进行了简单的axios封装,在使用 jianghuAxios 发起 Resource 请求时,需要指定要请求的资源类型、请求的参数等信息。
代码来源: basic
项目中的doUiAction.html
// 引入jianghuAxios
{% include 'common/jianghuAxios.html' %}
// 使用jianghuAxios
<script>
async getTableData() {
const result = await window.jianghuAxios({
data: {
appData: {
pageId: 'doUiAction',
actionId: 'selectItemList',
actionData: {},
where: {},
}
}
});
this.tableData = result.data.appData.resultData.rows;
},
</script>
5. Resource配置
在Jianghu框架中接口可以直接手动通过项目数据库的_resource
表进行配置:
字段 | 说明 |
---|---|
accessControlTable | 数据规则控制表 |
resourceHook | 协议的前置和后置操作方法'](#hook) |
pageId | 接口的一级名称 |
actionId | 接口的二级名称,结合一级名称可以快速定位接口 |
desc | 协议的描述说明 |
resourceType | 协议类型; sql: 处理简单的crud 操作; service:手动创建相关service处理复杂的逻辑; |
appDataSchema | appData 参数数据结构的校验 |
resourceData | 协议的具体实现配置 |
requestDemo | Demo仅供开发者参考使用 |
responseDemo | Demo仅供开发者参考使用 |
operation | 操作; softInsert softUpdate softDelete select |
参考用例
id | accessControlTable | resourceHook | pageId | actionId | desc | resourceType | appDataSchema | resourceData | requestDemo | responseDemo |
---|---|---|---|---|---|---|---|---|---|---|
1 | access_control_student_basic | { "before": [{ "service": "student", "serviceFunction": "studentSomeFunction" }], "after": [] } | frontendDemo01 | selectItemList | 查询列表 | sql | { "table": "student_basic", "operation": "select" } | |||
2 | allPage | userInfo | 获取用户信息 | service | {"service": "user", "serviceFunction": "userInfo"} |
6. 数据规则控制表(accessControlTable)
用户维度配置数据规则
参考用例
1._resource
.accessControlTable的内容为表名 access_control_student
2.创建表 access_control_student
3.针对特定用户添加数据规则
id | userId | username | resourceData |
---|---|---|---|
1 | G00001 | 洪七公 | { "where":{"level": "02"} } |
7. appData参数校验(appDataSchema)
协议请求参数数据结构的校验, 参考文档: ajv参数校验
参考用例
additionalProperties 默认值为true,表示只能传
properties
包含的参数。比较常用的场景是:resourceType
为sql
的协议时对请求参数进行校验。
{
"type": "object",
"additionalProperties": true,
"required": [ "aaa" ],
"properties": {
"aaa": { "type": "string" },
"bbb": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
},
}
resourceType & resourceData
sql
: 后端根据请求组装出sql,执行并将结果返回给前端service
: 后端根据请求找出/app/service下对应的service,执行对应的方法并将结果返回给前端
简单的数据增删改查,建议适用sql。复杂的数据操作,需要编写
service
方法,对数据或者多个表进行处理。
参考用例
resourceType | resourceData |
---|---|
sql | { "table": "${tableName}", "operation": "select" } |
sql | { "table": "${tableName}", "operation": "jhInsert" } |
sql | { "table": "${tableName}", "operation": "jhDelete" } |
sql | { "table": "${tableName}", "operation": "jhUpdate" } |
service | { "service": "service文件名", "serviceFunction": "service方法名" } |
- operation
insert
、delete
、update
、select
、jhInsert
、jhUpdate
、jhDelete
insert
、update
、delete
、select
是默认的sql
操作类型;jhInsert
、jhUpdate
、jhDelete
操作数据的同时会将数据记录到_record_history
表 ,为数据的操作保留轨迹;
8. 动态数据查询
支持在resourceData
中配置where
、whereLike
、whereIn
、whereOptions
、whereOrOptions
等条件来实现动态数据
查询。
字段 | 作用 | 解释 | 参考值 |
---|---|---|---|
where | 基础 kv 结构查询条件 | where k=v |
"where": {"studentName": "张三"} |
whereLike | 模糊查询 | where k like v |
"whereLike": {"studentName": "张%"} |
whereOrOptions | or 查询 | where k1=v1 or k2=v2 |
"whereOrOptions": [['name', '=', 'zhangshan'],['level', '>', 3]] |
whereOptions | knex 原生的 where 三元查询 | where k1 op1 v1 and k2 op2 v2 |
whereOptions: [['name', '=', 'zhangshan'],['level', '>', 3]] |
whereIn | in 查询 | where k in (v1, v2, v3) |
"whereIn": {"studentId": [1,2,3]} |
whereKnex | 直接写 knex 语句(仅 resource 数据可用) | 见 knex 文档 | "whereKnex": ".where({studentName: '张三'})" |
fieldList | 要查询的字段(仅 resource 数据可用) | - | "fieldList": ["id", "name"] |
excludedFieldList | 不查询的字段(仅 resource 数据可用) | - | "excludedFieldList": ["secret"] |
rawSql | 直接执行 Sql(仅 resource 数据可用) | - | "rawSql":"select * from student where id = 1;" |
offset | 同 mysql offset,排序用 | - | "offset": 10 |
limit | 同 mysql limit,排序用 | - | "limit": 10 |
orderBy | 排序 | - | "orderBy": [{ column: 'email' }, { column: 'age', order: 'desc' }] |
参考用例
在
_resource
表中配置resourceData
{ "table": "_view01_cgg_member_app_directory", "operation": "select", "where": { "loginId": "ctx.userInfo.user.userId" } }
where
的key
为要用作查询的字段名,ctx.userInfo.user.userId
为一段可被eval
执行的js
语句字符串,如果需要纯字符串,可以把内容加上单引号"'string'"
。
实现逻辑(以下底层实现的内容了解即可)
controller/controllerUtil/resourceUtil.js
中,读取resourceData
的where
条件进行相关操作。
//支持在 resourceData 中配置 where、whereLike、whereIn、whereOr 的动态数据
async function buildWhereConditionFromResourceData(resourceData, ctx, userInfo) {
if (!resourceData) {
return '';
}
const backendAppData = {};
//如:{ "where": { "field1": "ctx.someData" } }
[ 'where', 'whereLike', 'whereIn' ].forEach(appDataKey => {
const expressionObject = resourceData[appDataKey];
if (!expressionObject) {
return;
}
const valueObject = {};
_.forEach(expressionObject, (value, key) => {
valueObject[key] = eval(value);
});
backendAppData[appDataKey] = valueObject;
});
//如:{ "whereOptions": "ctx.someList" }
[ 'whereOptions', 'whereOrOptions' ].forEach(appDataKey => {
const expression = resourceData[appDataKey];
if (!expression) {
return;
}
backendAppData[appDataKey] = eval(expression);
});
return buildWhereConditionFromAppData(backendAppData);
}
9. resourceHook
协议的前置和后置service
方法。
参考用例
{ "before": [{ "service": "service文件名", "serviceFunction": "service方法名" }], "after": [{ "service": "service文件名", "serviceFunction": "service方法名" }] }
before
:在sql
执行之前,框架中间件httpResourceHook.js
会读取并运行service
方法after
:在sql
执行之后,框架中间件httpResourceHook.js
会读取并运行service
方法
实现逻辑
if (beforeHooks) {
for (const beforeHook of beforeHooks) {
const { service, serviceFunction } = beforeHook;
checkServiceFunction(service, serviceFunction);
await ctx.service[service][serviceFunction](ctx.request.body.appData.actionData, ctx);
}
}
if (afterHooks) {
for (const afterHook of afterHooks) {
const { service, serviceFunction } = afterHook;
checkServiceFunction(service, serviceFunction);
await ctx.service[service][serviceFunction](ctx.request.body.appData.actionData, ctx);
}
}
10. service
service方法目录:/app/service/**
参考用例
'use strict';
// 文件位置: /app/service/student.js
// resourceData: { "service": "student", "serviceFunction": "appendStudentInfoToUserInfo" }
const Service = require('egg').Service;
// 文件名+'Service' = StudentService
class StudentService extends Service {
async appendStudentInfoToUserInfo() {
const studentInfo = await this.app.jianghuKnex('student_basic').where({studentId: this.ctx.userInfo.user.userId}).first();
this.ctx.userInfo.studentInfo = studentInfo || { classId: null };
}
}
module.exports = StudentService;
代码逻辑
//resourceData 存放的数据
{ "service": "文件名.js", "serviceFunction": "serviceFunction" }
//接口请求 -> route -> ResourceController.httpRequest 方法
//根据 resourceType == 'service' 调用 `serviceResource`
...
case resourceTypeEnum.service:
resultData = await serviceResource({ ctx, body });
ctx.body = httpResponse.success({ packageId, appData: resultData });
break;
...
async function serviceResource({ ctx, body }) {
...
//注意: 这里必须 'ctx.service[serviceName][methodName]' 这样 写; 否则service无法获取egg 相关属性
//ctx.service `service` 提前挂载在上下文中了,可以直接调用
const resultData = await ctx.service[service][serviceFunction](actionData, ctx);
//返回结果给前端调用
return resultData;
}
** ctx **
- 框架的全局上下文,在相关
service
、controller
、middleware
中可以使用this.ctx
调用。 - 上下文中,包含
app
、request
、response
等,更多详细信息可断点查看。参考用例
const { allowPageList } = this.ctx.userInfo;
const userAgent = this.ctx.request.body.appData.userAgent || '';
const { authToken } = this.ctx.request.body.appData;
** app **
app
上下文中,默认包含service方法
、middleware方法
、knex
、jianghuKnex
、config
、logger
、_cache
。- 用户可以自己添加数据到
app
中,方便全局使用。
11. jianghuKnex
jianghuKnex
是对knex
库的常用方法的封装;- 比如以下示例中的
jhInsert
方法,会自动添加框架数据规范的一些通用属性operation
、operationByUserId
、operationByUser
、operationAt
等信息;
代码逻辑
...
jhInsert: async params => {
const operation = 'jhInsert';
if (Array.isArray(params)) {
params = params.map(item => {
return { ...item, operation, operationByUserId, operationByUser, operationAt };
});
} else {
params = { ...params, operation, operationByUserId, operationByUser, operationAt };
}
//插入数据产生的新idList
const ids = await target.insert(params);
//保留操作历史
await backupNewDataListToRecordHistory({ ids, table, knex, operation, requestBody, operationByUserId, operationByUser, operationAt });
return ids;
},
...
参考用例
jianghuKnex(${tableName}).insert({ 字段:xxx, 字段:xxx });
jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).update({ 字段: 内容, 字段: 内容 });
jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).select();
jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).softDelete();
jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).softUpdate({ 字段: 内容, 字段: 内容 });
12. requestDemo & responseDemo
- 请求Demo 和 响应Demo
- 当
jiangHuConfig.updateRequestDemoAndResponseDemo
设置为true时框架会自动保存最新一条协议请求的请求&响应作为Demo
requestDemo
{
"appData":{
"pageId":"allPage",
"actionId":"userInfo",
"actionData":{
},
"appId":"jianghujs_demo_basic",
"userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36"
},
"packageId":"1644935324054_6592543",
"packageType":"httpRequest"
}
responseDemo
框架会自动保存最新一条协议返回作为demo保存
{
"packageId":"1644935324054_6592543",
"packageType":"httpResponse",
"status":"success",
"timestamp":"2022-02-15T22:28:45+08:00"
}
小结
通过以上课程,我们了解了在江湖JS中如何发起Resource请求。可以试试以下小练习:
- 定义一张客户表&客户订单表,使用Service Resource把每个客户的总金额算出来,然后显示在页面上
思路:可以使用 Service Resource 对客户订单表进行查询,然后计算每个客户的总金额,并将结果展示在页面上。具体实现方式可以先定义一个 Service 层的方法,用于查询客户订单表并计算总金额;然后在页面中使用 Service Resource 调用该方法,并将结果展示在页面上。