Skip to content

定义好 库表结构 后,下一步就是直接开发接口了。

yiapi 封装了大量的细节,提供了大量的内置功能,没有那么多弯弯绕绕的开发流程。

总体的大步骤就这几步:

  1. 定义数据库的库表
  2. 写接口,写业务。
  3. 让客户端调用接口。
  4. 完事。

接口前置说明

所有的接口,都放在 apis 目录下,该目录下的每个 .js 文件,都会被作为一个接口定义。

同时,可以给接口分门别类,继续创建子目录即可。

比如,订单相关的接口可以放到 apis/order 目录下,如果你有增删改查四个接口,那么可以这样组织:

  • apis/order/insert.js 添加订单
  • apis/order/delete.js 删除订单
  • apis/order/update.js 修改订单
  • apis/order/select.js 查询订单

同时呢,每个目录下,都要有一个 _meta.js 文件,内容如下:

javascript
export const metaConfig = {
    // 目录名称
    dirName: '订单接口组',
    // 接口名称
    apiNames: {
        insert: '添加订单',
        delete: '删除订单',
        select: '查询订单',
        update: '修改订单'
    }
};

_meta.js 文件必需具名导出 metaConfig 变量。

其中有两个必需属性,一个是 dirName 表示这一些列接口的含义。

一个是 apiNames 属性,用来映射其中每个接口的名称。

接口定义示例

接下来,我们来看一下,在 yiapi 中,一个接口是如何写的。

javascript
// 工具函数
import { fnRoute, fnSchema } from '@yicode/yiapi/fn.js';
// 配置文件
import { httpConfig } from '@yicode/yiapi/config/http.js';
import { schemaHelperConfig } from '@yicode/yiapi/config/schemaHelper.js';
// 数据库表
import { tableData } from '../../tables/example.js';
// 接口元数据
import { metaConfig } from './_meta.js';

export default async (fastify) => {
    // 当前文件的路径,fastify 实例
    fnRoute(import.meta.url, fastify, metaConfig, {
        // 请求参数约束
        schemaRequest: {
            type: 'object',
            properties: {
                nickname: fnSchema(tableData.nickname),
                age: fnSchema(tableData.age)
            },
            required: ['title']
        },

        // 执行函数
        apiHandler: async (req, res) => {
            const trx = await fastify.mysql.transaction();
            try {
                const newsModel = trx('news');

                const result = await newsModel //
                    .clone()
                    .insertData({
                        title: req.body.title,
                        thumbnail: req.body.thumbnail,
                        content: req.body.content
                    });

                await trx.commit();
                return {
                    ...httpConfig.INSERT_SUCCESS,
                    data: result
                };
            } catch (err) {
                await trx.rollback();
                fastify.log.error(err);
                return httpConfig.INSERT_FAIL;
            }
        }
    });
};

注意

每个接口,都用一个单独的文件来定义,请不要多个接口共用一个文件。

最简单的接口

每个接口的格式如下:

javascript
// 工具函数
import { fnRoute } from '@yicode/yiapi/fn.js';
// 接口元数据
import { metaConfig } from './_meta.js';
// 接口定义
export default async (fastify) => {
    // 当前文件的路径,fastify 实例
    fnRoute(import.meta.url, fastify, metaConfig, {
        method: 'POST', // 可选
        schemaRequest: {}, // 必需
        schemaResponse: {}, // 可选
        apiHandler: async () => {
            // 必须
            return {
                code: 0,
                msg: '第一个接口'
            };
        }
    });
};

如果你想定义一个最简单的接口,那么除去以上可选部分,其他的就是必须要有的。

注意

请保证每个接口都必包含以上元素,否则就是无效接口。

这里,我们来对这个最简接口进行说明。

🔖 fnRoute 的导出

这是接口定义的核心,必须从 @yicode/yiapi/fn.js 中导出 fnRoute 这个函数,才能定义接口。

🔖 metaConfig 接口元数据

然后,从 ./_meta.js 文件中,导出 metaConfig 以做备用。

注意

metaConfig 必须使用具名定义,具名导出,也不能换成别的名字导出,否则接口会报错。

🔖 导出默认函数

每个接口,都必需对外导出一个如下所示的默认 async 函数。

javascript
export default async (fastify) => {};

其中,函数的参数使用 fastify 接收。

因为 yiapi 接口快速开发框架是在 fastify 框架的基础上进行的深度封装。

这里我们致敬和感谢开源社区的 fastify 和它的开发者们,就没必要改名了。

🔖 fnRoute 的定义

fnRoute 函数接收 4 个参数,分别是:

  1. import.meta.url 当前文件路径
  2. fastify fastify 实例
  3. metaConfig 接口元数据
  4. object 接口具体逻辑

前三个参数,所有接口都是一样的,不要改名称,也不要改顺序。

第四个参数,就是我们编写具体业务逻辑的地方,这是一个 object 对象,一共也有 4 个属性,分别是:

  1. method(可选) 请求方法 (仅支持 POSTGET,默认为 POST)
  2. schemaRequest 请求参数约束
  3. schemaResponse(可选) 返回参数约束
  4. apiHandler 接口具体逻辑

那么,methodschemaResponse 是可选的,你写不写都没关系。

yiapi 的设计理念就是,所有接口都用 POST 请求,实在不行再用 GET 请求。

提高对接口定义的灵活度,不同的功能,不由 Restful 范式的 deleteputpostget 来定义,而是由路由来定义。

比如订单的增删改查,分别对应 /order/insert/order/delete/order/update/order/select

而不是 post orderdelete orderput orderget order

Restful 范式表达的动作是有限的,超出它表达范围之外的东西用哪个都别扭。

而用路由可以表达无数种动作,请大家摒弃这种曾经盛极一时,实则漏洞百出的范式。

注意

本框架不支持、不认同、也不使用 Restful 规范。

如果跟本框架理念不合,请移驾别处。

接口的请求和返回都是用 json 数据格式。

请求参数验证

这是接口定义中,一个非常重要的内容,也就是 schemaRequest 部分的定义。

javascript
// 工具函数
import { fnRoute, fnSchema } from '@yicode/yiapi/fn.js';
// 数据库表
import { tableData } from '../../tables/example.js';
// 接口元数据
import { metaConfig } from './_meta.js';
// 接口定义
export default async (fastify) => {
    // 当前文件的路径,fastify 实例
    fnRoute(import.meta.url, fastify, metaConfig, {
        method: 'POST', // 可选
        // 请求参数约束
        schemaRequest: {
            type: 'object',
            properties: {
                nickname: fnSchema(tableData.nickname),
                age: fnSchema(tableData.age)
            },
            required: ['title']
        },
        // 返回参数约束
        schemaResponse: {}, // 可选
        apiHandler: async () => {
            // 必须
            return {
                code: 0,
                msg: '第一个接口'
            };
        }
    });
};

如上所示,schemaRequest 的值,必须是一个 object 类型,用来接受客户端调用接口传入的对象参数。

如果我们用的是 axios,那么请求接口大致如下:

javascript
axios({
    url: 'http://127.0.0.1:3000/api/example/insert',
    data: {
        nickname: '陈随易',
        age: 18
    }
});

schemaRequest 就是 json-schema 协议,对与 json-schema 不了解的,可以看查看 https://json-schema.apifox.cn 学习和了解。

其实不了解也没事,我们的 fnSchema 函数已经对此进行了封装。

并且此时呢,我们在上一个章节 库表定义 的内容就发挥作用了。

js
{
    nickname: fnSchema(tableData.nickname),
    age: fnSchema(tableData.age)
}

可以看到,properties 属性的值中,就同时用到了 fnSchema 函数和 库表定义 中的字段。

那么按照我们上个章节对库表的定义来看:

js
nickname: {
    name: '昵称',
    type: 'string',
    default: '',
    max: 50
}

对于 nickname 字段来说,其值必须为字符串,且不能超过 50 个字符,否则接口逻辑就不会执行。

这就是对接口参数的约束和使用。

业务逻辑开发

定义好接口的参数验证,保证拿到的参数是需要的,正确的,合法的之后,下一步就是写具体的业务逻辑,然后返回给客户端了。

这就是 apiHandler 部分的内容。

js
apiHandler: async () => {
    // 必须
    return {
        code: 0,
        msg: '查询成功',
        data: {
            id: 1,
            name: '陈随易',
            age: 18,
            like: 'book'
        }
    };
};

那么最简单的就是,直接返回一个对象,必要的 2 个属性是 codemsg

code0 表示一切正常,其他则为各种异常。

msg 则是接口的提示信息。

如果要返回数据,则返回 data 即可。

以上只是一个小示例,真正的业务开发,肯定是需要对接数据库的,数据库的使用如下:

🔖 无事务

js
// 执行函数
apiHandler: async (req, res) => {
    try {
        const newsModel = fastify.mysql.table('news');
        const result = await newsModel //
            .clone()
            .insertData({
                nickname: req.body.nickname,
                age: req.body.age
            });

        return {
            ...httpConfig.INSERT_SUCCESS,
            data: result
        };
    } catch (err) {
        fastify.log.error(err);
        return httpConfig.INSERT_FAIL;
    }
};

🔖 有事务

js
// 执行函数
apiHandler: async (req, res) => {
    const trx = await fastify.mysql.transaction();
    try {
        const newsModel = trx('news');

        const result = await newsModel //
            .clone()
            .insertData({
                nickname: req.body.nickname,
                age: req.body.age
            });

        await trx.commit();
        return {
            ...httpConfig.INSERT_SUCCESS,
            data: result
        };
    } catch (err) {
        await trx.rollback();
        fastify.log.error(err);
        return httpConfig.INSERT_FAIL;
    }
};

以上功能,就是将客户端请求的参数插入到数据库中,然后返回插入的数据库的自增 ID。

那么,这就是接口的定义和使用了,整个开发流程,就这两步反复操作。

关于数据库的方法,配置的含义等,请看后续文章。

何以解忧,唯有代码。不忘初心,方得始终。