JianghuJS-后端接口(Reource)

学习目标

  • 理解 jianghujs resource 协议概念。
  • 学习使用 resource 协议标准,根据需求场景搭建数据页面。

1. Resource协议说明

Resource 是江湖JS 后端接口中的一种协议,江湖JS对不同端之间所传递的数据所做的规范与设计,接口协议数据结构设计上,进行了统一和简化,能够做到在不同通讯通道间无缝切换。

在江湖JS里,一个HTTP请求/Socket请求对应数据库_resource表中的一行数据。

Resource协议的原理是根据请求body里的 pageIdactionId去匹配_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 or service 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请求的响应数据

参考用例

  1. const package = {
  2. packageId: '123456',
  3. packageType: 'httpRequest',
  4. appData: {
  5. appId: `${window.appInfo.appId}`,
  6. pageId: 'protocolDemo',
  7. actionId: 'insertItem',
  8. userAgent: 'demo_userAgent',
  9. authToken: localStorage.getItem(`${window.appInfo.appId}_authToken`),
  10. actionData: {
  11. studentId: 'G00003',
  12. classId: '2021-01级-02班',
  13. gender: 'male',
  14. level: '02',
  15. name: '小虾米',
  16. },
  17. where: { 字段A: 'A', 字段B: 'B' },
  18. whereIn: { name: ['张三丰', '张无忌'] },
  19. whereLike: { 字段A: 'A', 字段B: 'B' },
  20. whereOptions: [['name', '=', 'zhangshan'],['level', '>', 3]],
  21. whereOrOptions: [['name', '=', 'zhangshan'],['level', '>', 3]],
  22. offset: 10,
  23. limit: 20,
  24. orderBy: [{ column: 'email' }, { column: 'age', order: 'desc' }],
  25. }
  26. };
  27. const result = await axios(package);
  28. const rows = result.data.appData.resultData.rows;
  29. console.log('rows', rows);

4. jianghuAxios 发起 Resource 请求

jianghuAxios是江湖JS框架进行了简单的axios封装,在使用 jianghuAxios 发起 Resource 请求时,需要指定要请求的资源类型、请求的参数等信息。

代码来源: basic项目中的doUiAction.html

  1. // 引入jianghuAxios
  2. {% include 'common/jianghuAxios.html' %}
  3. // 使用jianghuAxios
  4. <script>
  5. async getTableData() {
  6. const result = await window.jianghuAxios({
  7. data: {
  8. appData: {
  9. pageId: 'doUiAction',
  10. actionId: 'selectItemList',
  11. actionData: {},
  12. where: {},
  13. }
  14. }
  15. });
  16. this.tableData = result.data.appData.resultData.rows;
  17. },
  18. </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包含的参数。比较常用的场景是:resourceTypesql的协议时对请求参数进行校验。

  1. {
  2. "type": "object",
  3. "additionalProperties": true,
  4. "required": [ "aaa" ],
  5. "properties": {
  6. "aaa": { "type": "string" },
  7. "bbb": { "anyOf": [{ "type": "string" }, { "type": "null" }] },
  8. },
  9. }

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

insertdeleteupdateselectjhInsertjhUpdatejhDelete

  • insertupdatedeleteselect 是默认的sql操作类型;
  • jhInsertjhUpdatejhDelete 操作数据的同时会将数据记录到_record_history表 ,为数据的操作保留轨迹;

8. 动态数据查询

支持在resourceData中配置wherewhereLikewhereInwhereOptionswhereOrOptions 等条件来实现动态数据查询。

字段 作用 解释 参考值
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

    1. { "table": "_view01_cgg_member_app_directory", "operation": "select", "where": { "loginId": "ctx.userInfo.user.userId" } }
  • wherekey为要用作查询的字段名,ctx.userInfo.user.userId为一段可被eval执行的js语句字符串,如果需要纯字符串,可以把内容加上单引号"'string'"

实现逻辑(以下底层实现的内容了解即可)

controller/controllerUtil/resourceUtil.js中,读取resourceDatawhere条件进行相关操作。

  1. //支持在 resourceData 中配置 where、whereLike、whereIn、whereOr 的动态数据
  2. async function buildWhereConditionFromResourceData(resourceData, ctx, userInfo) {
  3. if (!resourceData) {
  4. return '';
  5. }
  6. const backendAppData = {};
  7. //如:{ "where": { "field1": "ctx.someData" } }
  8. [ 'where', 'whereLike', 'whereIn' ].forEach(appDataKey => {
  9. const expressionObject = resourceData[appDataKey];
  10. if (!expressionObject) {
  11. return;
  12. }
  13. const valueObject = {};
  14. _.forEach(expressionObject, (value, key) => {
  15. valueObject[key] = eval(value);
  16. });
  17. backendAppData[appDataKey] = valueObject;
  18. });
  19. //如:{ "whereOptions": "ctx.someList" }
  20. [ 'whereOptions', 'whereOrOptions' ].forEach(appDataKey => {
  21. const expression = resourceData[appDataKey];
  22. if (!expression) {
  23. return;
  24. }
  25. backendAppData[appDataKey] = eval(expression);
  26. });
  27. return buildWhereConditionFromAppData(backendAppData);
  28. }

9. resourceHook

协议的前置和后置service方法。

参考用例

  1. { "before": [{ "service": "service文件名", "serviceFunction": "service方法名" }], "after": [{ "service": "service文件名", "serviceFunction": "service方法名" }] }
  • before:在sql执行之前,框架中间件httpResourceHook.js 会读取并运行service方法
  • after:在sql执行之后,框架中间件httpResourceHook.js 会读取并运行service方法

实现逻辑

  1. if (beforeHooks) {
  2. for (const beforeHook of beforeHooks) {
  3. const { service, serviceFunction } = beforeHook;
  4. checkServiceFunction(service, serviceFunction);
  5. await ctx.service[service][serviceFunction](ctx.request.body.appData.actionData, ctx);
  6. }
  7. }
  8. if (afterHooks) {
  9. for (const afterHook of afterHooks) {
  10. const { service, serviceFunction } = afterHook;
  11. checkServiceFunction(service, serviceFunction);
  12. await ctx.service[service][serviceFunction](ctx.request.body.appData.actionData, ctx);
  13. }
  14. }

10. service

service方法目录:/app/service/**

参考用例

  1. 'use strict';
  2. // 文件位置: /app/service/student.js
  3. // resourceData: { "service": "student", "serviceFunction": "appendStudentInfoToUserInfo" }
  4. const Service = require('egg').Service;
  5. // 文件名+'Service' = StudentService
  6. class StudentService extends Service {
  7. async appendStudentInfoToUserInfo() {
  8. const studentInfo = await this.app.jianghuKnex('student_basic').where({studentId: this.ctx.userInfo.user.userId}).first();
  9. this.ctx.userInfo.studentInfo = studentInfo || { classId: null };
  10. }
  11. }
  12. module.exports = StudentService;

代码逻辑

  1. //resourceData 存放的数据
  2. { "service": "文件名.js", "serviceFunction": "serviceFunction" }
  3. //接口请求 -> route -> ResourceController.httpRequest 方法
  4. //根据 resourceType == 'service' 调用 `serviceResource`
  5. ...
  6. case resourceTypeEnum.service:
  7. resultData = await serviceResource({ ctx, body });
  8. ctx.body = httpResponse.success({ packageId, appData: resultData });
  9. break;
  10. ...
  11. async function serviceResource({ ctx, body }) {
  12. ...
  13. //注意: 这里必须 'ctx.service[serviceName][methodName]' 这样 写; 否则service无法获取egg 相关属性
  14. //ctx.service `service` 提前挂载在上下文中了,可以直接调用
  15. const resultData = await ctx.service[service][serviceFunction](actionData, ctx);
  16. //返回结果给前端调用
  17. return resultData;
  18. }

** ctx **

  • 框架的全局上下文,在相关servicecontrollermiddleware中可以使用this.ctx调用。
  • 上下文中,包含apprequestresponse等,更多详细信息可断点查看。

    参考用例

    1. const { allowPageList } = this.ctx.userInfo;
    2. const userAgent = this.ctx.request.body.appData.userAgent || '';
    3. const { authToken } = this.ctx.request.body.appData;

** app **

  • app上下文中,默认包含service方法middleware方法knexjianghuKnexconfiglogger_cache
  • 用户可以自己添加数据到app中,方便全局使用。

11. jianghuKnex

  • jianghuKnex是对knex库的常用方法的封装;
  • 比如以下示例中的jhInsert方法,会自动添加框架数据规范的一些通用属性operationoperationByUserIdoperationByUseroperationAt等信息;

代码逻辑

  1. ...
  2. jhInsert: async params => {
  3. const operation = 'jhInsert';
  4. if (Array.isArray(params)) {
  5. params = params.map(item => {
  6. return { ...item, operation, operationByUserId, operationByUser, operationAt };
  7. });
  8. } else {
  9. params = { ...params, operation, operationByUserId, operationByUser, operationAt };
  10. }
  11. //插入数据产生的新idList
  12. const ids = await target.insert(params);
  13. //保留操作历史
  14. await backupNewDataListToRecordHistory({ ids, table, knex, operation, requestBody, operationByUserId, operationByUser, operationAt });
  15. return ids;
  16. },
  17. ...

参考用例

  1. jianghuKnex(${tableName}).insert({ 字段:xxx, 字段:xxx });
  2. jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).update({ 字段: 内容, 字段: 内容 });
  3. jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).select();
  4. jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).softDelete();
  5. jianghuKnex(${tableName}).where({ 字段:xxx, 字段:xxx }).softUpdate({ 字段: 内容, 字段: 内容 });

12. requestDemo & responseDemo

  • 请求Demo 和 响应Demo
  • jiangHuConfig.updateRequestDemoAndResponseDemo 设置为true时框架会自动保存最新一条协议请求的请求&响应作为Demo

requestDemo

  1. {
  2. "appData":{
  3. "pageId":"allPage",
  4. "actionId":"userInfo",
  5. "actionData":{
  6. },
  7. "appId":"jianghujs_demo_basic",
  8. "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"
  9. },
  10. "packageId":"1644935324054_6592543",
  11. "packageType":"httpRequest"
  12. }

responseDemo

框架会自动保存最新一条协议返回作为demo保存

  1. {
  2. "packageId":"1644935324054_6592543",
  3. "packageType":"httpResponse",
  4. "status":"success",
  5. "timestamp":"2022-02-15T22:28:45+08:00"
  6. }

小结

通过以上课程,我们了解了在江湖JS中如何发起Resource请求。可以试试以下小练习:

  • 定义一张客户表&客户订单表,使用Service Resource把每个客户的总金额算出来,然后显示在页面上

思路:可以使用 Service Resource 对客户订单表进行查询,然后计算每个客户的总金额,并将结果展示在页面上。具体实现方式可以先定义一个 Service 层的方法,用于查询客户订单表并计算总金额;然后在页面中使用 Service Resource 调用该方法,并将结果展示在页面上。