后端快速上手

后端快速上手

接下来,我们将开始一个简单的图书项目,来帮你熟悉整个项目的开发流程。

视图控制

打开 app/api/v1 文件夹,在该文件夹里新建 book.js 文件。我们从 lin-mizar 中导入 LinRouter 来创建 API 视图:

const { LinRouter } = require("lin-mizar");

而后,初始化一个名为 bookApi 的 Router,并创建一个匿名视图函数:

WARNING

请记住此处视图函数所代表的意思,在后续的章节中它会非常重要!!!严格上来说,koa的中间件机制是没有严格意义上的视图函数的,但是我们仍然希望通过这种概念来区分普通中间件和真正的视图函数。

const { LinRouter } = require("lin-mizar");

const bookApi = new LinRouter({
  prefix: "/v1/book"
});

bookApi.get("/:id", async ctx => {
  ctx.json({
    msg: "hello, I am a book"
  });
});

如果你熟悉 koa 或者 koa-router,应该可以发现这与 koa 的 API 开发几乎一致,不过我们提供了一个 LinRouter,它实际上继承了 koa-router,并提供了更多的方法来帮助我们进行权限管理。

到此一个简单的图书 API 开发就实现了,但是我们此时运行程序并不能访问到该 API。得益于 Lin loader 的便利,我们只需要在当前模块导出bookApi,即可自动挂载到主路由上。

module.exports = { bookApi };

现在,我们已经可以通过 HTTP 请求到该 API 了,运行程序。然后通过 curl(你可以使用任何你喜爱的测试工具,postman 甚至浏览器都行)访问<http://127.0.0.1:5000/v1/book/1>。

如果一切顺利,你就可以在 terminal 中看到下面返回数据:

{
  "msg": "hello, I am a book"
}

模型使用

打开 app/models 文件夹,在该文件夹下新建 book.js 文件,并加入以下内容:

"use strict";

const { InfoCrudMixin } = require("lin-mizar/lin/interface");
const { merge } = require("lodash");
const { Sequelize, Model } = require("sequelize");
const { db } = require("lin-mizar/lin/db");

class Book extends Model {
  toJSON() {
    let origin = {
      id: this.id,
      title: this.title,
      author: this.author,
      summary: this.summary,
      image: this.image,
      create_time: this.createTime
    };
    return origin;
  }
}

Book.init(
  {
    id: {
      type: Sequelize.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    title: {
      type: Sequelize.STRING(50),
      allowNull: false
    },
    author: {
      type: Sequelize.STRING(30),
      allowNull: true,
      defaultValue: "未名"
    },
    summary: {
      type: Sequelize.STRING(1000),
      allowNull: true
    },
    image: {
      type: Sequelize.STRING(100),
      allowNull: true
    }
  },
  merge(
    {
      tableName: "book",
      modelName: "book",
      sequelize: db
    },
    InfoCrudMixin.options
  )
);

module.exports = { Book };

我们新建了一个 Book 的 Model,该模型混入了 InfoCrudMixin,有关于InfoCrudMixin 的详细信息,将在后续详细介绍,本章我们注重于 CMS 的开发使用。

TIP

这里我们还在Book.prototype上定义了一个toJSON函数。我们推荐你为每一个模型类定义一个属于自己的 toJSON 函数,它会在 json 序列化的时候提供巨大的便利。

js 会在 JSON.stringfy()的时候默认的调用类的 toJSON 函数。在 koa 中,当我们对ctx.body 进行赋值时,尤其是使 body 等于某个模型时,koa 会对模型默认调用 stringfy函数,因此我们可以在 toJSON 函数中定义你需要传输到前端的模型数据。

有了该模型后,我们打开 tests/helper 目录下的 fake_book.js 文件,并运行fake_book.js 文件,如果你的操作正确,你的数据库将会在多出两条书籍的数据。

node tests/helper/fake_book.js

下面,我们继续完善 app/api/v1/book.js 下的匿名视图函数,在上一步中,我们只是简单的返回了一条信息,但是在真正的开发中,数据库几乎是任何应用都摆脱不了的一环。

我们重写函数:

const { LinRouter } = require("lin-mizar");
const { Book } = require("../../models/book");
const { getSafeParamId } = require("../../libs/util");

const bookApi = new LinRouter({
  prefix: "/v1/book"
});

bookApi.get("/:id", async ctx => {
  const id = getSafeParamId(ctx);
  const book = await Book.findOne({
    where: {
      id,
      delete_time: null
    }
  });
  ctx.json(book);
});

module.exports = { bookApi };

在上面重写的 book.js 文件中,我们使用到了 sequelize 提供的便捷查询 API,详细使用请参考官网 后端快速上手 - 图1 。

至此,我们再次运行根目录下的 starter.js 文件,并通过 curl 再次访问<http://127.0.0.1:5000/v1/book/1>,我们会得到如下数据:

{
  "author": "Randal E.Bryant",
  "create_time": 1539702050000,
  "id": 1,
  "image": "https://img3.doubanio.com/lpic/s1470003.jpg",
  "summary": "********"
}

TIP

请记住,如果你没有 sequelize 的任何基础,甚至没有任何 orm 的使用经历,我们强烈建议你学习一下 sequelize 的使用。

参数校验

在应用的开发中,尤其是在 Web 领域中,对于任何从用户传入的数据都是无规则可循的,我们无法去预测用户传入的数据,因此参数(数据)校验是开发中不可或缺的一环。

我们打开 app/validators/book.js 文件,向其中 添加 如下内容:

## ********
## ********

class BookSearchValidator extends LinValidator {
  constructor () {
    super();
    this.q = new Rule("isNotEmpty", "必须传入搜索关键字");
  }
}

module.exports = { BookSearchValidator };

我们新建了一个 BookSearchValidator 的校验类,当前端传入数据时必须携带 q 这个参数,它被用来查询书籍。

然后,我们在 app/api/v1/book.js 文件中新增一个视图函数用来搜索图书:

const { Op } = require("sequelize");

bookApi.get("/search/one", async ctx => {
  const v = await new BookSearchValidator().validate(ctx);
  const book = await Book.findOne({
    where: {
      title: {
        [Op.like]: `%${v.get("query.q")}%`
      },
      delete_time: null
    }
  });
  if (!book) {
    throw new NotFound({ msg: "没有找到相关书籍" });
  }
  ctx.json(book);
});

最后,我们再次运行根目录下的 starter.js 文件,并通过 curl 再次访问<http://127.0.0.1:5000/v1/book/search/one>,我们会得到如下数据:

{
  "error_code": 10030,
  "msg": "必须传入搜索关键字",
  "url": "/v1/book/search/one"
}

很明显我们未输入参数 q,因此校验未通过。我们修改 url 为<http://127.0.0.1:5000/v1/book/search/one?q=C>,会得到如下数据:

{
  "author": "(美)Brian W. Kernighan",
  "create_time": 1539702050000,
  "id": 2,
  "image": "https://img3.doubanio.com/lpic/s1106934.jpg",
  "summary": "在计算机发展的历史上,没有哪一种程序设计语言像C语言这样应用广泛。本书原著即为C语言的设计者之一Dennis M.Ritchie和著名计算机科学家Brian W.Kernighan合著的一本介绍C语言的权威经典著作。我们现在见到的大量论述C语言程序设计的教材和专著均以此书为蓝本。原著第1版中介绍的C语言成为后来广泛使用的C语言版本——标准C的基础。人们熟知的“hello,World\"程序就是由本书首次引入的,现在,这一程序已经成为众多程序设计语言入门的第一课。\n原著第2版根据1987年制定的ANSIC标准做了适当的修订.引入了最新的语言形式,并增加了新的示例,通过简洁的描述、典型的示例,作者全面、系统、准确地讲述了C语言的各个特性以及程序设计的基本方法。对于计算机从业人员来说,《C程序设计语言》是一本必读的程序设计语 言方面的参考书。",
  "title": "C程序设计语言"
}

一般情况下,我们都推荐使用校验器来对参数进行校验,因为校验器具有良好的扩展性。当然你觉得每次定义一个校验器较为麻烦,你也可以定义如getSafeParamId这样的帮助函数来进行少量参数的校验。

自定义异常

在刚才的图书搜索 API 中,当程序没有找到相关的书籍时,会抛出一个 NotFound 的异常。NotFound 是 Lin 基础库提供的一种异常,现在有如下需求:我们需要自定义一个BookNotFound 的异常来专门表示图书未找到。打开 app/libs/err-code.js,在其中添加如下内容:

"use strict";

const { HttpException } = require("lin-mizar");
const assert = require("assert");
const { isInteger } = require("lodash");

class BookNotFound extends HttpException {
  constructor(ex) {
    super();
    this.code = 404;
    this.msg = "没有找到相关图书";
    this.errorCode = 80010;
    if (ex && ex.code) {
      assert(isInteger(ex.code));
      this.code = ex.code;
    }
    if (ex && ex.msg) {
      this.msg = ex.msg;
    }
    if (ex && ex.errorCode) {
      assert(isInteger(ex.errorCode));
      this.errorCode = ex.errorCode;
    }
  }
}

module.exports = { BookNotFound };

WARNING

BookNotFound异常继承自HttpException,所有继承自HttpException的子类异常均会被框架的异常处理机制所捕捉到,并以固定的信息结构返回给前端,方便前端快速捕捉错误。在code、msg和error_code中,msg 和 error_code 异常重要,msg 能直接的告诉前端错误信息,error_code 可以让前端快速判断是何种异常。

然后更改图书搜索未找到图书的异常:

const { Op } = require("sequelize");
const { BookNotFound } = require("../../libs/err-code");

## *******

bookApi.get("/search/one", async ctx => {
  const v = await new BookSearchValidator().validate(ctx);
  const book = await Book.findOne({
    where: {
      title: {
        [Op.like]: `%${v.get("query.q")}%`
      },
      delete_time: null
    }
  });
  if (!book) {
    throw new BookNotFound();
  }
  ctx.json(book);
});

再次运行程序,如果未找到图书,则前端会得到如下提示信息:

{
  "error_code": 80010,
  "msg": "没有找到相关图书",
  "url": "/v1/book/search/one?q=ll"
}

完善 API

上面,我们陆续地完成了查询图书和搜索图书两个 API,此处我们再补上创建、更新、删除和获取图书这四个 API。

  • 创建图书
bookApi.post("/", async ctx => {
  const v = await new CreateOrUpdateBookValidator().validate(ctx);
  const book = await Book.findOne({
    where: {
      title: v.get("body.title"),
      delete_time: null
    }
  });
  if (book) {
    throw new Forbidden({
      msg: "图书已存在"
    });
  }
  const bk = new Book();
  bk.title = v.get("body.title");
  bk.author = v.get("body.author");
  bk.summary = v.get("body.summary");
  bk.image = v.get("body.image");
  bk.save();
  ctx.success({
    msg: "新建图书成功"
  });
});
  • 更新图书
bookApi.put("/:id", async ctx => {
  const v = await new CreateOrUpdateBookValidator().validate(ctx);
  const id = getSafeParamId(ctx);
  const book = await Book.findById(id);
  if (!book) {
    throw new NotFound({
      msg: "没有找到相关书籍"
    });
  }
  book.title = v.get("body.title");
  book.author = v.get("body.author");
  book.summary = v.get("body.summary");
  book.image = v.get("body.image");
  book.save();
  ctx.success({
    msg: "更新图书成功"
  });
});
  • 删除图书
bookApi.linDelete(
  "deleteBook",
  "/:id",
  {
    auth: "删除图书",
    module: "图书",
    mount: false
  },
  async ctx => {
    const id = getSafeParamId(ctx);
    const book = await Book.findOne({
      where: {
        id,
        delete_time: null
      }
    });
    if (!book) {
      throw new NotFound({
        msg: "没有找到相关书籍"
      });
    }
    book.destroy();
    ctx.success({
      msg: "删除图书成功"
    });
  }
);
  • 获取所有图书
bookApi.get("/", async ctx => {
  const books = await Book.findAll();
  if (books.length < 1) {
    throw new NotFound({
      msg: "没有找到相关书籍"
    });
  }
  ctx.json(books);
});

我们完成了一个关于图书 CRUD 的简单 demo,事实上很多应用都是重复如此操作,希望你能熟练掌握这套流程。

详细代码参考示例项目book 后端快速上手 - 图2 。

小结

到此,我们运行了示例的工程项目,并通过 starter 完成了一个简单的图书 API 的开发,我们使用了如下几点:

  • 使用红图创建 API
url description method
/v1/book/:id 查询指定 id 的书籍 GET
/v1/book/:id 更新指定 id 的书籍 PUT
/v1/book/:id 删除指定 id 的书籍 DELETE
/v1/book/search/one 根据关键字q搜索书籍 GET
/v1/book/ 创建图书 POST
/v1/book/ 获取所有图书 GET
* 使用 Model 层创建和查询数据
  • 使用 LinValidator 来校验参数
  • 继承 HttpException 来自定义异常当然在本章中,我们只是一个快速上手的指导,如果你熟悉 koa 及其生态,那么你完全可以按照此种模式开发自己的应用,后续我们会讨论项目开发的基础约定和流程。

后端开发完成!移步前端部分?