# Koa快速入门 ## 1. Koa 是什么? > Koa 是 基于Node.js平台的下一代Web开发框架(上一代 就是 Express)。 ## 2. 基本使用 ```js import Koa from 'koa'; import { PORT, HOST } from './config/system.mjs'; const app = new Koa(); app.use((ctx) => { ctx.body = 'Hello, Koa.'; }); app.listen(PORT, HOST, () => { console.log(`服务已启动,URL地址为:${'http://' + HOST + ':' + PORT}`); }); ``` 引入 Koa 类后,通过 `new Koa()`创建app实例。然后在调用对应`listen`方法去监听一个端口号,如果没有报错,即服务启动成功。 然后 `app.use()`方法是给Koa应用程序添加中间件。作用是给所有请求响应数据的。 ## 3. 相关概念 ### 3.1 中间件 > 本质 就是 JavaScript函数。会接受一个参数`ctx`,即上下文对象(整合了Node.js中的request和response两个对象)以及 第二个参数`next`,即为栈中的下一个中间件函数。 > > - 可以在整个请求/响应循环周期内 做任何事情。 > - 还可以修改ctx对象,比如添加路由间共享的数据等 > - 结束请求/响应周期(结束响应) > - 调用下一个中间件 ### 3.2 context对象 > Koa Context 将 node 的 `request` 和 `response` 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 > > **注意:Koa实例app的context属性 是 Context对象的原型。** - `ctx.query` 获取请求URL地址上的查询参数,返回一个Object对象。 - `ctx.body=` 设置响应正文,同时会结束请求/响应周期 - `ctx.status=` 设置HTTP状态码,比如: `ctx.status = 200;` - `ctx.message=` 设置HTTP状态信息 - `ctx.length=` 设置 Content-Length 响应头 - `ctx.type=` 设置 Content-Type 响应头 ### 3.3 路由功能 > Koa非常轻量,并没有内置路由的功能。这样就需要我们自己下载路由功能的中间件--`@koa/router` ```shel npm install @koa/router ``` - 引入Router类 - 通过`new Router()`创建router实例 - 之后即可以通过 router 实例 去定义各种路由端点 - 最后 通过 `app.use(router.routes())`将路由功能注入到应用程序中 - 如果 存在跨域的话,会发生options请求。此时 需要对options请求响应服务端允许哪些请求方法,需要添加这样的代码:`app.use(router.allowedMethods())` **router实例的方法** > url:对应 客户端的请求地址 > > middlewares:对应 对此次请求处理的中间价(可以多个) > > *注意:所有的定义路由的方法 都会返回router实例。在实际开发时,可以选择router实例的连调方式去定义多个路由。* - `router.get(url, middlewares)`定义一个get请求方法的路由 - `router.post(url, middlewares)`定义一个post请求方法的路由 - `router.put(url, middlewares)`定义一个put请求方法的路由 - `router.delete(url, middlewares)`定义一个delete请求方法的路由 ```js import Router from '@koa/router'; const router = new Router(); // 定义一个 get的请求方法 路由 // 第一个参数: 路由的路径 // 第二个参数:路由处理的中间件 router .get('/', (ctx) => { ctx.body = '主页'; }) .get('/about', (ctx) => { ctx.body = '关于-get'; }) .post('/about', (ctx) => { ctx.body = '关于-post'; }); export default router; ``` ### 3.4 处理Get请求方法 > 在Koa框架中,会自动对所有请求地址中的查询字符串进行转译操作,也就是说 自动将查询字符串 转换成 普通Object对象形式。处理完成后,会存储在`ctx.query`上。 比如,我们需要实现分页获取数据时就会像这样: ```js import Router from '@koa/router'; const router = new Router(); // 定义一些静态数据 const users = [ { id: 1, name: '果果', age: 18, }, { id: 2, name: '静静', age: 38, }, { id: 3, name: '欣欣', age: 18, }, { id: 4, name: '诗诗', age: 28, }, ]; router // 获取所有用户数据 .get('/', (ctx) => { //! 如果响应数据类型 赋值为 一个对象,那么koa会自动设置响应头content-type = application/json;chartset=utf-8 ctx.body = users; }) // 分页获取数据 // 思考:1 是get还是post;get 2 需要给服务端 传递数据吗? 当前页码current and 每页条数count .get('/page', (ctx) => { // ? 如何获取用户传递过来的参数current以及count? //! koa框架 会 自动处理所有请求的查询参数 -- 在处理路由时,可以直接通过ctx.query来获取所有的查询参数 // ctx.query 获取所有转译后查询参数对象 // console.log(ctx.query); const { current, count } = ctx.query; // 从总数据的数组中正确的截取出我们想要的那些数据 // 1 用数组的slice方法 进行 数据截取 // 1: [0,1) 2: [1, 2) 3: [2, 3)... m: [(m-1)*count, count * m ) ctx.body = users.slice((current - 1) * count, current * count); }); export default router; ``` ### 3.5 处理Post请求方法 > 在处理Post请求时,如果客户端传递的数据 是 通过请求体body实现的,那么默认Koa是无法处理的转换工作的。 如果想要在所有路由处理响应之前,能够将Post请求体转换成普通对象就需要安装第三方的中间件。 ```bash npm install koa-body ``` 接下来,需要在入口文件`index.js`写下如下代码: ```js import Koa from 'koa'; import serve from 'koa-static'; import config from './app.config.js'; import router from './router/index.mjs'; import { koaBody } from 'koa-body'; const app = new Koa(); // 托管静态文件 app.use(serve(config.public)); // 一定要在路由之前先处理请求体的转换 // 默认对 json and xml and urlencoded 进行转换 app.use(koaBody()); // 添加路由 app.use(router.routes()); // 在options请求下响应支持的请求方法 app.use(router.allowedMethods()); app.listen(config.port, config.host, () => { console.info(`服务已启动,地址为:'http://${config.host}:${config.port}'.`); }); ``` 上述代码中:`app.use(koaBody())`就实现了对json以及urlencoded数据类型的body转换了,之后就可以通过`ctx.request.body`来获取转换后的请求体数据(类型为 普通JS对象) 先这样, ```js import Router from '@koa/router'; const router = new Router(); // 定义一些静态数据 const users = [ { id: 1, name: '果果', age: 18, }, { id: 2, name: '静静', age: 38, }, { id: 3, name: '欣欣', age: 18, }, { id: 4, name: '诗诗', age: 28, }, ]; let uid = 4; // 用来储存当前最新添加用户的id值 router // 获取所有用户数据 .get('/', (ctx) => { //! 如果响应数据类型 赋值为 一个对象,那么koa会自动设置响应头content-type = application/json;chartset=utf-8 ctx.body = users; }) // 添加新用户 .post('/insert', (ctx) => { // console.log(ctx.request.body); users.push({ ...ctx.request.body, id: ++uid }); ctx.body = '添加成功'; }); export default router; ``` ### 3.6 文件上传 > `koa-body`内置支持文件上传。 但是默认是没有开启请求体`multipart/form-data`类型的解析,在对应上传文件的路由上需要单独配置。像这样, ```js // 上传文件 router .put( '/upload', koaBody({ multipart: true, formidable: { uploadDir: './public', // 文件上传后位置 keepExtensions: true, // 保留原文件的后缀名 }, }), (ctx) => { // 获取待上传的文件 // 通过ctx.request.files 获取上传后文件的相关信息,比如newFilename 为 上传文件后的新名称 console.log(ctx.request.files); ctx.body = `/${ctx.request.files.file.newFilename}`; } ); ``` **实际开发时,在上传文件时会指定文件最终存储的目录下,此时除了在请求体中通过file指定要上传的文件之外,在添加一个参数folder 用以指定上传后文件的存储位置。** ![image-20230426164918456](assets/image-20230426164918456.png) ```js import Router from '@koa/router'; import { koaBody } from 'koa-body'; import { copyFile, rm, access, mkdir } from 'node:fs/promises'; import { UPLOAD_DIR, STATIC } from '../app.config.mjs'; const router = new Router(); // 通过router实例的一些方法 比如 get、post等去定义 对应请求方法的路由 router .put( '/upload', koaBody({ multipart: true, formidable: { keepExtensions: true, uploadDir: UPLOAD_DIR, // 设置上传文件的最终位置 }, onError(err, ctx) { ctx.body = { code: 1, msg: err, }; }, }), async (ctx) => { let filename = ctx.request.files.file.newFilename; let srcFile = `./${UPLOAD_DIR}/${filename}`; let destFile = srcFile; // 获取 请求体 中 除了file文件之外的其他数据 const { folder } = ctx.request.body; // 如果用户在上传文件时 指定了目标文件夹 if (folder) { // 1 将默认上传位置的文件 拷贝到 目标的文件夹下 destFile = `./${UPLOAD_DIR}/${folder}/${filename}`; try { // 先确保destFile的文件夹都真实存在 let isExist = await access(`./${UPLOAD_DIR}/${folder}`).catch( () => false ); // isExist 就是 undefined 或者 false if (isExist == false) { // 如果不存在 就创建 await mkdir(`./${UPLOAD_DIR}/${folder}`, { recursive: true }); } await copyFile(srcFile, destFile); // 2 成功后 删除默认位置的文件 await rm(srcFile); } catch (error) { console.error(error); } } // 给客户端响应数据 ctx.body = { code: 0, data: { path: `${UPLOAD_DIR.replace(STATIC, '')}/${folder}/${filename}`, filename, }, msg: '上传成功', }; } ); export default router; ``` ## 4. MySql数据库 > 数据库相关的操作推荐使用管理工具来完成,比如Navicat。 image-20230507111451422 如上,点击【Connection】,选择连接『MySQL』 image-20230507111604025 接下来,填写必要信息 image-20230507112108981 回到Navicat首页,即可看到上面你刚刚新建的连接名,双击即可连接上MySQL数据库,然后右键选择新建一个数据库。 image-20230507112307951 新建数据库时,按下图填写即可 image-20230507112541396 选择上面新建的数据库,双击进入后在Tables上右键选择【New Table】 image-20230507112702286 在中间窗口中,完成表的设计 image-20230507113017560 ### 4.1 表操作 1. C ```sql -- 1 C create 向表中插入数据 -- INSERT INTO (...fields) VALUES (...vals) 这里vals值顺序 和 fields字段顺序对应上 -- INSERT INTO VALUES (...vals); vals 此时和表定义时列顺序对应上 -- 这里可以使用 mysql 内置函数来获取系统时间 INSERT INTO Users(password, username, status, create_time) VALUES ('123456', 'daxia', 1, Now()); INSERT INTO Users VALUE(2, 'guoguo', '654321', 0, NOW()); -- 需要手动指定ID列的值,不方便 INSERT INTO Users(password, username, status, create_time) VALUES ('123456', 'fengqingyang', 1, Now()); ``` 2. R ```sql -- 2 R read 读取表中数据 -- select ...field from -- select username, password FROM Users; -- 选择username和password 两列 -- SELECT * FROM Users; -- 2.1 条件子句where -- 运算符 >, <, >=, <=, <>|!=, = , like 模糊查询 % _, between and -- 查询所有未被禁用的用户 -- SELECT * FROM Users WHERE status = 1; -- SELECT * FROM Users WHERE status <> 0; -- SELECT * FROM Users WHERE status != 0; -- 查询所有用户名中包含 字符 gu 的那些用户 -- SELECT * from Users where username like '%gu%'; -- 查询所有用户名中以 gu 开头 的那些用户 -- SELECT * from Users where username like 'gu%'; -- 查询所有用户名中第二个字符为 u 的那些用户 -- SELECT * from Users where username like '_u%'; -- 选择哪些id值在1-3之间的用户 -- SELECT * from Users where id BETWEEN 1 and 3; -- 2.2 条件间关系 -- and 与 -- or 或 -- 查询那些 id 范围在1-3之间并且密码为123456的用户 -- SELECT * FROM Users WHERE id BETWEEN 1 and 3 AND password = '123456'; -- 查询那些 id 范围在1-3之间或密码为123456的用户 -- SELECT * FROM Users WHERE id BETWEEN 1 and 3 OR password = '123456'; -- 2.3 LIMIT子句。主要用于分页 LIMIT OFFSET -- 页码current = 2, 当前条目count = 1 -- 根据条件 跳过(current - 1)* count 条数据,在取 count 条 数据 SELECT * FROM Users LIMIT 1 OFFSET 1 ; SELECT * FROM Users LIMIT 2,1; -- LIMIT后第一个值 为 offset值 ``` 3. U ```sql -- 3. U update 更新数据 -- UPDATE SET field1=newVal1, field2=neVal2, ... where子句 -- 将所有用户的密码重置为 123456 UPDATE Users SET password='123456'; -- 将大侠用户的密码修改为 654321,并禁用 UPDATE Users SET password='654321',status=0 WHERE username='daxia'; SELECT * from Users; ``` 4. D ```sql -- 4 D delete 删除表数据 -- DELETE FROM where子句 -- 删除风清扬用户 DELETE FROM Users WHERE username='fengqingyang'; DELETE FROM Users; -- 清空表 ``` ### 4.2 代码实现 > 在Koa框架中可以使用第三方的MySql2插件来实现数据库的相关操作,并且`mysql2` 内置 Promise支持。 在`mysql2/promise`包中,含有一个名为`createPool`的方法。该方法用来创建一个连接池对象,该方法需要一个配置对象为入参,其中包含以下属性: - host:数据库所在主机IP地址,本地的话请填写 '127.0.0.1' - user:用户名 - password:密码 - database:操作的数据库名称 - connectionLimit:最大连接数 - ... ```js import { createPool } from 'mysql2/promise'; const pool = createPool({ host: '127.0.0.1', user: 'root', password: 'Jing@123', database: 'shopping', waitForConnections: true, connectionLimit: 10, maxIdle: 10, // max idle connections, the default value is the same as `connectionLimit` idleTimeout: 60000, // idle connections timeout, in milliseconds, the default value 60000 queueLimit: 0, }); ``` 封装查询方法:因为数据库相关操作都是异步操作,极有可能会发生错误,因此封装一个查询方法统一处理错误。 ```js export async function query(sql, values) { try { let [res] = await pool.query(sql, values); return res; } catch (error) { console.log('====', error); return Promise.reject(error); } } ``` 最后,封装一个自定义中间件。实现所有ctx实例都可以拥有一个数据库查询方法。 下面具体实现,利用闭包实现可定制化的中间件。目前该中间件只能自定义在`app.context`上添加的方法名字。 ```js export function DBMiddleware( { queryname } = { queryname: 'execute', } ) { return async (ctx, next) => { ctx.app.context[queryname] = query; await next(); }; } ``` 至此,数据库相关封装就完成了。然后就可以在路由中使用ctx对应方法对数据库进行所需操作,并获取其结果。 **如果无法获取ctx实例时,可以通过db模块中暴露的query函数来进行数据库操作。** ```js router .get('/', async (ctx) => { let res = await ctx.execute('select * from users;'); ctx.body = res === false ? { code: 1, msg: '失败', } : { code: 0, msg: '成功', data: res, }; }) ``` ## 5. 综合实战 > 通过Koa + 各种中间件实现一个API接口服务器。 那么,如果去设计一个比较良好的API接口服务器呢?我们可以从以下几个方面去考虑即可: 1. URI image-20230510101731572 2. 错误码以及返回机制 错误码应该根据 API 的特点和需求来设计。错误码应该明确表明问题的原因,并提供解决方法和建议,以帮助开发人员更快地诊断和解决问题。在 API 文档中列出经过良好设计的错误码,可以提高接口的可用性和可靠性,并减少开发人员的故障排除时间。下面是可供参考的 API 错误码类型: 1. 客户端错误:客户端错误通常表示客户端发送的请求存在问题,例如参数缺失、格式错误、权限不足等。常见的客户端错误码包括:400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found等。 2. 服务器错误:服务器错误通常表示API服务器存在问题,例如服务器内部错误、数据库连接错误、超时等。常见的服务器错误码包括:500 Internal Server Error、502 Bad Gateway、503 Service Unavailable等。 3. 业务错误:业务错误通常表示API无法完成客户端请求,因为请求数据不符合业务规则或API无法处理请求。常见的业务错误码包括:422 Unprocessable Entity、429 Too Many Requests、451 Unavailable For Legal Reasons等。 4. 认证和安全错误:认证和安全错误通常表示API需要验证客户端身份,但无法验证或认证失败。常见的认证和安全错误码包括:401 Unauthorized、403 Forbidden、419 Authentication Timeout、498 Invalid Token等。 5. 限制和配额错误:限制和配额错误通常表示API已经超过了某些限制或配额,例如请求速率超过限制、超过配额等。常见的限制和配额错误码包括:429 Too Many Requests、503 Service Unavailable、509 Bandwidth Limit Exceeded等。 | 分类 | 描述 | | :--- | :--------------- | | 0 | 请求成功 | | 1 | 请求失败 | | 401 | 未登录或登录超时 | | ... | ... | 3. 响应体 为了方便给客户端响应,响应数据会包含三个属性, - 状态码(code), - 信息描述(message), - 响应数据(data)。 客户端根据状态码及信息描述可快速知道接口是否成功,再开始处理数据。 ```JSON { code: 0, message: "查询成功", data: { id: 1, username: "daxia", ... } } ``` ### 5.1 登录接口 在很多的Web项目或H5应用中都会有登录的需求。因为我们开发的网站或者应用程序都会有一些需要验明身份后才可访问的路由,比如访问购物车,个人设置等等。 因此在本实战同样会实现一个登录接口,在开始之前,我们需要搞清楚登录的流程: ![image-20230510095850364](assets/image-20230510095850364.png)= ### 5.2 注册接口 ![image-20230510110535624](assets/image-20230510110535624.png) ### 5.3 分页接口 ![image-20230510110839725](assets/image-20230510110839725.png)