新增
当前代码的 github 仓库地址 koa-tutorial
https://github.com/egolink0/koa-tutorial.git
导引
通过一步步的练习,最后你可以通过本文对以下方面有基本的了解。
- 了解 RESTful api 规范
- 了解 koa 如何构建 CRUD 接口
- 了解 docker 如何起一个 mysql 服务
- 了解 knex 如何在 koa 系统中与数据库连接
环境要求
在项目练手之前,你需要基本配置好下面这些
必要环境
- nodejs
- docker
- docker compose
非必要
- git
- nodemon
最终所有包版本
"knex": "^2.4.2",
"koa": "^2.14.2",
"koa-bodyparser": "^4.4.0",
"koa-router": "^12.0.0",
"mysql2": "^3.3.5"
初始化服务入口
第一步,初始化项目的基本环境和安装包
git init
npm init -y
npm install koa@latest knex@latest mysql2@latest -S
之后,构建 src/server/index.js 文件,写入基本的入口配置
const Koa = require("koa");
const app = new Koa();
app.use(async (ctx) => {
ctx.body = {
status: 200,
message: "hello world",
};
});
const PORT = 4000;
const server = app.listen(PORT, () => {
console.log(`Server is listening to ${PORT}`);
});
启服务
node ./src/server/index.js
现在,你可以通过浏览器打开 localhost:4000
页面,可以看到页面直接返回结果
{"status":200,"message":"hello world"}
之后就要开始配置数据库
配置数据库
1. 起 mysql 服务
这里,我们使用 mysql 作为对应的数据库。根据下面 yaml 配置 起一个 mysql 的服务
docker-compose -f ./xxx.yaml up -d
version: '3'
services:
db:
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: koa_restful_api
MYSQL_USER: koa # 数据库用户名
MYSQL_PASSWORD: 123456 # koa 对应的用户名密码
ports:
- "3306:3306"
volumes:
- /data/mysql5.7:/var/lib/mysql
此时查看 docker ps
可以看对应的容器,可以进入容器进行 mysql 的操作。
> docker ps # 查看所有运行的容器
> docker exec -it 容器ID bash # 进入正在运行的容器
> mysql -u koa -p # 输入密码,进入 mysql 交互界面
> show databases; # sql
2. knex 配置连接
执行 knex init
初始化 knex 配置后,会在根目录下生成 knexfile.js
文件。修改文件里面的配置,根据 docker 的 mysql 服务配置,修改 development 环境下的配置。
const path = require("path");
const BASE_PATH = path.join(__dirname, "src", "server", "db");
module.exports = {
development: { // development
client: "mysql2",
connection: "mysql://koa:123456@localhost:3306/koa_restful_api", // 根据自己的 mysql 配置修改成相应的
migrations: {
directory: path.join(BASE_PATH, "migrations"),
},
seeds: {
directory: path.join(BASE_PATH, "seeds"),
},
},
}
配置 src/server/db/connection.js 数据库连接实例
const environment = process.env.NODE_ENV || 'development';
const configs = require('../../../knexfile.js');
const knex = require('knex')(configs[environment]);
module.exports = knex;
3. 初始化数据库表结构
运行 knex migrate:make movies
,构建 migrations 初始化文件,修改里面的内容,用于构建相应的表结构
// src/server/db/migrations/xx_movies.js
// 构建表
exports.up = function (knex) {
return knex.schema.createTable("movies", (table) => {
table.increments();
table.string("name").notNullable().unique();
table.string("genre").notNullable();
table.integer("rating").notNullable();
table.boolean("explicit").notNullable();
});
};
// 删除表
exports.down = function (knex) {
return knex.schema.dropTable("movies");
};
指定 dev 环境,并运行构建命令: knex migrate:latest --env development
然后可以看到数据库表结构如下,movies 就生成了。
mysql> show tables; # 查看表 list
+---------------------------+
| Tables_in_koa_restful_api |
+---------------------------+
| knex_migrations |
| knex_migrations_lock |
| movies |
+---------------------------+
3 rows in set (0.00 sec)
mysql> desc movies; # 查看表结构
+----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| name | varchar(255) | NO | UNI | NULL | |
| genre | varchar(255) | NO | | NULL | |
| rating | int(11) | NO | | NULL | |
| explicit | tinyint(1) | NO | | NULL | |
+----------+------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)
4. 数据初始化
有了表,但是还没有数据,此时需要生成 seed 文件,可以初始化一些数据。通过执行 knex seed:make movies_seed
得到 movies_seed 文件,然后修改相应的文件。
// src/server/seeds/movies_seed.js
exports.seed = (knex, Promise) => {
return knex("movies")
.del()
.then(() => {
return knex("movies").insert({
name: "A",
genre: "Fantasy",
rating: 7,
explicit: false,
});
})
.then(() => {
return knex("movies").insert({
name: "B",
genre: "Science Fiction",
rating: 9,
explicit: true,
});
})
.then(() => {
return knex("movies").insert({
name: "C",
genre: "Action",
rating: 5,
explicit: false,
});
});
};
修改完成后,执行 seed 函数往数据库插入一些数据,初始化 knex seed:run --env development
此时,查看数据表,新增了三行新的数据。
mysql> select * from movies;
+----+------+-----------------+--------+----------+
| id | name | genre | rating | explicit |
+----+------+-----------------+--------+----------+
| 1 | A | Fantasy | 7 | 0 |
| 2 | B | Science Fiction | 9 | 1 |
| 3 | C | Action | 5 | 0 |
+----+------+-----------------+--------+----------+
3 rows in set (0.00 sec)
搭建路由
路由部分,使用 koa-router
包, 所以先安装对应的 npm 依赖 npm i koa-router@latest -S
。
RESTful api 规范包含了增删改查的一些原则,下面将开始构建相应的部分。
1. 根路由 /
初始化服务端入口文件 index.js 中 包含了对所有请求的统一返回值 hello world
,但是实际项目中,会出现多个接口的情况,此时就涉及到路由的概念,先创建根地址的路由返回值。
构建 src/server/routes/index.js 文件存放根路由响应数据。
const Router = require("koa-router");
const router = new Router();
router.get("/", async (ctx) => {
ctx.body = {
status: "success",
message: "hello, world!",
};
});
module.exports = router;
并在入口文件处引入
const indexRoutes = require("../server/routes/index");
app.use(indexRoutes.routes());
再次访问 localhost:4000
得到和上述一样的 hello world
JSON 数据
RESTful api 概览
项目需要构建的 api 规范结构包含以下几个部分
URL | http method | desc |
---|---|---|
/api/v1/movies | get | 获取 movies 列表 |
/api/v1/movies/:id | get | 获取一个 movie |
/api/v1/movies | post | 增加一个 movie |
/api/v1/movies/:id | put | 修改一个 movie |
/api/v1/movies/:id | delete | 删除一个 movie |
2. (查)列表 /api/v1/movies
在 src/server/db/queries/movies.js 创建 getMovies 查询数据库数据的部分
const knex = require("../connection");
function getAllMovies() {
return knex("movies").select("*");
}
module.exports = {
getAllMovies,
};
在 src/server/routes/movies.js 中创建 api 接口 get 路由部分
const Router = require("koa-router");
const queries = require("../db/queries/movies");
const router = new Router();
const BASE_URL = `/api/v1/movies`;
router.get(BASE_URL, async (ctx) => {
try {
const movies = await queries.getAllMovies();
ctx.body = {
status: "success",
data: movies,
};
} catch (err) {
console.log(err);
}
});
module.exports = router;
在 src/server/index.js 入口文件处,引入 movies 的路由部分
const movieRoutes = require("../server/routes/movies"); // movies
app.use(movieRoutes.routes()); // movies route
重启服务,此时在浏览器或者 postman 构建请求 localhost:4000/api/v1/movies
可以得到下面的列表信息
{
"status":"success",
"data":[
{
"id":1,
"name":"A",
"genre":"Fantasy",
"rating":7,
"explicit":0
},
{
"id":2,
"name":"B",
"genre":"Science Fiction",
"rating":9,
"explicit":1
},
{
"id":3,
"name":"C",
"genre":"Action",
"rating":5,
"explicit":0
}
]
}
3. (查)某一个电影的详细数据 /api/v1/movies/:id
和上面的步骤一致,先创建 query 方法
function getSingleMovie(id) {
return knex("movies")
.select("*")
.where({ id: parseInt(id) });
}
再构建路由部分
router.get(`/api/v1/movies/:id`, async (ctx) => {
try {
const movie = await queries.getSingleMovie(ctx.params.id);
if (movie.length) {
ctx.body = {
status: "success",
data: movie,
};
} else {
ctx.status = 404;
ctx.body = {
status: "error",
message: "That movie does not exist.",
};
}
} catch (err) {
console.log(err);
}
});
此时访问浏览地址 //localhost:4000/api/v1/movies/1
可以得到 id 为 1 的电影的数据
{
"status": "success",
"data": [{
"id": 1,
"name": "A",
"genre": "Fantasy",
"rating": 7,
"explicit": 0
}]
}
4. (增加)一个电影 post
query 部分
function addMovie(movie) {
return knex("movies").insert(movie); // 返回 [id]
}
路由部分
router.post(`/api/v1/movies`, async (ctx) => {
try {
const movie = await queries.addMovie(ctx.request.body); // koa-bodyparser
if (movie.length) { // 根据数据库返回值处理
ctx.status = 201;
ctx.body = {
status: "success",
data: { id: movie[0] },
};
} else {
ctx.status = 400;
ctx.body = {
status: "error",
message: "Something went wrong.",
};
}
} catch (err) {
ctx.status = 400;
ctx.body = {
status: "error",
message: err.message || "Sorry, an error has occurred.",
};
}
});
使用到了 request 的 body 部分,需要增加解析插件在服务入口 npm i koa-bodyparser@latest -S
const koa = require("koa");
const bodyParser = require("koa-bodyparser"); // koa-bodyparser
const app = new koa();
const PORT = 4000;
const indexRoutes = require("../server/routes/index");
const moviesRoutes = require("../server/routes/movies");
app.use(bodyParser()); // 在路由之前加入中间件
app.use(indexRoutes.routes());
app.use(moviesRoutes.routes());
此时,在 postman 中发送一个 post 请求后,数据库增加了一行,返回了新的一行的 id: 4
5. (改)一个电影信息
query 的 update 部分
function updateMovie(id, movie) {
return knex("movies")
.update(movie)
.where({ id: parseInt(id) });
}
路由部分,使用 put 更新
router.put(`/api/v1/movies/:id`, async (ctx) => {
try {
const movie = await queries.updateMovie(ctx.params.id, ctx.request.body);
if (movie) {
ctx.status = 200;
ctx.body = {
status: "success",
data: { id: movie },
};
} else {
ctx.status = 404;
ctx.body = {
status: "error",
message: "That movie does not exist.",
};
}
} catch (err) {
ctx.status = 400;
ctx.body = {
status: "error",
message: err.message || "Sorry, an error has occurred.",
};
}
});
用 postman 测试,修改 id 为 1 的数据的 name
查看对应的数据库, 第一条数据名称已经更改为 D11
6. (删)一个电影
query 部分
function deleteMovie(id) {
return knex("movies")
.del()
.where({ id: parseInt(id) });
}
路由部分,delete
router.delete(`/api/v1/movies/:id`, async (ctx) => {
try {
const movie = await queries.deleteMovie(ctx.params.id);
if (movie) {
ctx.status = 200;
ctx.body = {
status: "success",
data: { id: movie }, // 返回 id
};
} else {
ctx.status = 404;
ctx.body = {
status: "error",
message: "That movie does not exist.",
};
}
} catch (err) {
ctx.status = 400;
ctx.body = {
status: "error",
message: err.message || "Sorry, an error has occurred.",
};
}
});
删除 id 为 1 的数据
查看数据,id=1 的数据已经被删除了
mysql> select * from movies;
+----+------+-----------------+--------+----------+
| id | name | genre | rating | explicit |
+----+------+-----------------+--------+----------+
| 2 | B | Science Fiction | 9 | 1 |
| 3 | C | Action | 5 | 0 |
| 4 | D | D | 9 | 5 |
+----+------+-----------------+--------+----------+
3 rows in set (0.00 sec)
结束
至此,阅读本文的你,应该已经掌握了在 koa 系统中从 0 到 1 搭建一个 RESTful api 规范的接口流程,下一步,你可以增加 TDD 测试的部分,了解测试驱动开发的流程。
参考
- Building a RESTful API with Koa and Postgres