# Part3-Koa与MySQL

## 一、Koa2安装

创建一个空白目录，然后进入终端，并在终端对koa进行安装：

```shell
# 项目初始化
$ npm init -y

# 安装koa2
$ npm i koa2 -S

# 安装nodemon
$ npm i nodemon -D
```

## 二、入口文件

在项目根目录创建 `app.js` 文件，并在上一步操作中生成的 `package.json` 里配置：

```json
{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js"
  },
}
```

在 `app.js` 中：

```js
const Koa = require('koa2');
const app = new Koa();
const port = 9000;

/* 
	解释下面这段代码：
	app.use()方法是：将给定的中间件方法添加到此应用程序。简单说就是调用中间件
	app.use() 返回 this, 因此可以链式表达
*/
app.use(async (ctx)=>{
    ctx.body = "Hello, Koa";
  	// ctx.body是ctx.response.body的简写
})

app.listen(port, ()=>{
    console.log('Server is running at http://localhost:'+port);
})
```

然后运行 `npm run start` ，并在浏览器输入 `http://localhost:9000/` 即可看到页面效果。

> 提示：
>
> 为了以后对端口和IP的管理方便，其实可以将IP与端口放在一个文件导出，然后导入使用，这样就可以方便管理：
>
> ```js
> // 生产环境域名：http://xxx.com    开发环境域名：http://localhost
> const host = "http://localhost";
> // 生产环境端口：自定义         开发环境域名：9000
> const port = 9000;
>
> module.exports = {
>     host, port
> }
> ```
>
> 然后到app.js或者需要使用IP与端口的文件中导入使用：
>
> ```js
> const {host, port} = require("./utils")
>
> app.listen(port, ()=>{
>     console.log(`Server is running at ${host}:${port}`);
> })
> ```

## 三、洋葱模型

学Koa必须要了解 `洋葱模型` :

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwjyczwj30da0c30w0.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwjyczwj30da0c30w0.jpg)

`Koa` 和 `Express` 都会使用到中间件，Express的中间件是顺序执行，从第一个中间件执行到最后一个中间件，发出响应：

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwr650yj30oj08bwep.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwr650yj30oj08bwep.jpg)

Koa是从第一个中间件开始执行，遇到 `next` 进入下一个中间件，一直执行到最后一个中间件，在逆序，执行上一个中间件 `next` 之后的代码，一直到第一个中间件执行结束才发出响应。

[![](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwvfbaaj30mm0fxwfe.jpg)](https://tva1.sinaimg.cn/large/008eGmZEgy1gplrwvfbaaj30mm0fxwfe.jpg)

对于这个洋葱模型，我们用代码来解释一下。假如把上面的代码改写成：

```js
const Koa = require('koa2');
const app = new Koa();
const port = 9000;

app.use(async (ctx, next)=>{
    console.log(1)
    await next();
    console.log(2)
})

app.use(async (ctx, next)=>{
    console.log(3)
    await next();
    console.log(4)
})

app.use(async (ctx)=>{
    console.log(5)
})

app.listen(port, ()=>{
    console.log('Server is running at http://localhost:'+port);
})
```

那么在浏览器刷新后，控制台得到的顺序是：

```
1
3
5
4
2
```

现在可以看到，我们通过 `next`可以先运行下个中间件，等中间件结束后，再继续运行当前 `next()` 之后的代码。

## 四、路由安装

当需要匹配不同路由时，可以安装：

```shell
$ npm i koa-router
```

将 `app.js` 修改：

```js
const Koa = require('koa2');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const port = 9000;

router.get('/', async (ctx)=>{
    ctx.body = "根路径";
})

router.get('/manage', async (ctx)=>{
    ctx.body = "管理系统";
})

app.use(router.routes(), router.allowedMethods());

app.listen(port, ()=>{
    console.log(`Server is running at http://localhost:${port}`);
})
```

此时，到浏览器刷新并在地址栏最后添加 `/manage` 即可得到根路径内容和列表模块内容。

备注：

```js
// 调用router.routes()来组装匹配好的路由，返回一个合并好的中间件
// 调用router.allowedMethods()获得一个中间件，当发送了不符合的请求时，会返回 `405 Method Not Allowed` 或 `501 Not Implemented`

allowedMethods方法可以做以下配置：
app.use(router.allowedMethods({ 
    // throw: true, // 抛出错误，代替设置响应头状态
    // notImplemented: () => '不支持当前请求所需要的功能',
    // methodNotAllowed: () => '不支持的请求方式'
}))
```

## 五、路由拆分

当项目较大，路由较多时，我们需要划分模块。此时，就需要对路由进行拆分。这个项目的后端要服务于Web官网和后台管理系统，因此我们将路由划分成两个模块：web与manage。

### 1、创建 `router` 文件夹

创建router文件夹，并在其中创建：`index.js` （路由总入口文件）、`manage/index.js` （manage模块路由入口文件）、`web/index.js` （web模块路由入口文件）：

```js
// app.js
const Koa = require("koa2");
const router = require("./router")
const app = new Koa();
const port = 9000;

// 调用router中间件
app.use(router.routes(), router.allowedMethods());

app.listen(port, ()=>{
    console.log(`Server is running at http://localhost:${port}`);
})

// index.js
const Router = require("koa-router");
const manage = require("./manage");
const web = require("./web");
const router = new Router();

router.get("/", async ctx=>{
    ctx.body = "根路径"
})

router.use("/manage", manage.routes(), manage.allowedMethods());
router.use("/web", web.routes(), web.allowedMethods());

module.exports = router;


// manage/index.js
const Router = require("koa-router")
const router = new Router();

router.get('/', async ctx=>{
    ctx.body = "管理系统"
})

module.exports = router;


// web/index.js
const Router = require("koa-router")
const router = new Router();

router.get('/', async ctx=>{
    ctx.body = "官网"
})

module.exports = router;
```

到浏览器刷新 `localhost:9000/manage` 与 `localhost:9000/web` 即可得到manage和web两个模块返回的数据。

### 2、路由重定向（了解）

那么有同学会问了，如果我想直接从 `localhost:9000` 重定向到 `localhost:9000/home` 该怎么办？

我们可以在 `router/index.js` 中做如下配置：

```js
router.use('/home', home.routes(), home.allowedMethods());
...
router.redirect('/', '/home');
```

### 3、404无效路由

如果被访问到无效路由，那么我们可以统一返回404页面：

在 `router` 下 `errorPage.js` :

```js
const Router = require('koa-router');
const errorPage = new Router();

errorPage.get('/', async (ctx) => {
    ctx.body = "访问页面不存在";
})

module.exports = errorPage;
```

在 `router/index.js` 中：

```js
const errorPage = require("./errorPage")

// 404页面路由
router.use("/404", errorPage.routes(), errorPage.allowedMethods());
```

在 `app.js` 中引用：

```js
// 匹配不到页面的全部跳转去404
app.use(async (ctx, next) => {
    await next();
    if (parseInt(ctx.status) === 404) {
        ctx.response.redirect("/404")
    }
})
app.use(router.routes(), router.allowedMethods());
```

## 六、后端允许跨域（了解）

前端想跨域，可以设置proxy。如果后端允许跨域，可以如下操作：

```js
// 安装koa2-cors
$ cnpm i koa2-cors

// 引入koa2-cors中间件
const cors = require("koa2-cors");
// 这里cors中间件一定要写在路由之前
app.use(cors());

app.use(router.routes(), router.allowedMethods())
```

## 七、读取静态资源文件

首先安装 `koa-static`，命令行代码如下：

```shell
$ yarn add koa-static
```

然后在项目的根目录下创建 `assets` 后，将图片资源（图片自己随便找一张）文件夹 `images` 放到其中。我们假定404页面需要返回一张错误警告图，可以在 `app.js` 中执行以下操作：

```js
// 引入
const path = require('path')
const static = require('koa-static')

// 获取静态资源文件夹
app.use(static(path.join(__dirname, '/assets')));
...
app.use(router.routes(), router.allowedMethods())
```

假设其中有一张图片叫做 `404.gif`，那么我们打开浏览器，访问：`http://localhost:9000/images/404.gif` 即可得到图片。这里注意：

> 路径上不需要写assets，因为我们已经指定了访问资源时， <http://localhost:9000> 自动指向 assets 文件夹。由此，我们知道数据库中图片的地址只需要填写 `/images/404.gif` 即可。

如果我们希望打开404页面就显示这张图，就需要做如下步骤：

### 1、安装mime-types

```shell
$ npm i mime-types
```

### 2、使用fs读取文件

修改errorPage.js：

```js
const Router = require("koa-router")
const router = new Router();
const fs = require("fs")
const path = require("path")
const mime = require("mime-types")

router.get('/', async ctx=>{
    const filePath = path.join(__dirname, "../assets/images/404.gif");
    const file = fs.readFileSync(filePath); // 读取文件
    const mimeType = mime.lookup(filePath)  // 读取文件类型
    ctx.set("content-type", mimeType);  // 设置返回类型（这一步很重要）
    ctx.body = file;    // 返回图片
})

module.exports = router;
```

## 八、MySQL安装【软件还需整理】

### 1、Windows卸载MySQL

如果你曾装过MySQL，请务必先卸载干净。

首先检查电脑是否安装过MySQL，Windows执行：开始->运行->输入 `services.msc`。如果看到有MySQL，就代表安装了，如图：

[![image-20211203162439294](https://i.loli.net/2021/12/03/jeX8dk9sGlqvyS2.png)](https://i.loli.net/2021/12/03/jeX8dk9sGlqvyS2.png)

如果没有，就可以忽略删除MySQL这一步了。但如果看到了这一项，请右键先停止这项服务，然后：

快捷键 `win+r` 输入 `regedit` 进入注册表，找到 `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Eventlog\Application\MySQL` 文件夹并对其进行删除。

> 如果有以下两个文件夹：
>
> 删除 HKEY\_LOCAL\_MACHINE\SYSTEM\ControlSet002\Services\Eventlog\Application\MySQL文件夹。 删除 HKEY\_LOCAL\_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\Application\MySQL的文件夹。 注册表里没有这两个文件，就不用删除了。

然后找到mysql的安装位置，将mysql文件夹删掉：

[![image-20211203163147634](https://i.loli.net/2021/12/03/ucwgfV5kWP6jKCF.png)](https://i.loli.net/2021/12/03/ucwgfV5kWP6jKCF.png)

再到控制面板搜索 `mysql` ，确认已经没有了才可以，否则也进行卸载。

然后是另一个地方的：C:\Program Files (x86)\MySQL

[![这里写图片描述](https://i.loli.net/2021/12/03/S1LG5xvVmTyOiP2.png)](https://i.loli.net/2021/12/03/S1LG5xvVmTyOiP2.png)

还有一个隐藏文件下的，先点击【查看】->勾选【隐藏的项目】：

[![这里写图片描述](https://i.loli.net/2021/12/03/Xjd46eHkxouOcrP.png)](https://i.loli.net/2021/12/03/Xjd46eHkxouOcrP.png)

然后返回 `C:/`，可以看到一个名为ProgramData的文件夹，点开找到里面的MySQL，删除就行了

[![这里写图片描述](https://i.loli.net/2021/12/03/msqdCxkSoXTgQ7r.png)](https://i.loli.net/2021/12/03/msqdCxkSoXTgQ7r.png)

到此位置，恭喜你将mysql删干净了。

### 2、MySQL安装（Mac）

**Mac安装包**：链接：<https://pan.baidu.com/s/1InjXljIzRO945iXlN6Pn3w> 提取码：ndpb

### 3、MySQL安装（Windows）

**Windows64位安装包**：链接：<https://pan.baidu.com/s/1OmdD-QldkvPGl0kQokbxJg> 提取码：ndpb

下载完成后，直接解压并放到指定的目录中。例如：D:\server\mysql-5.7.23-winx64。

在mysql目录中新建 `my.ini`，并进行如下配置：

```
[client]
# 设置mysql客户端默认字符集
default-character-set=utf8

[mysqld]
# skip-grant-tables
#设置3306端口
port = 3306
# 设置mysql的安装目录
basedir=D:\\server\\mysql-5.7.23-winx64
# 设置mysql数据库的数据的存放目录
datadir=D:\\server\\mysql-5.7.23-winx64\\data
# 允许最大连接数
max_connections=200
# 服务端使用的字符集默认为8比特编码的latin1字符集
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
```

进入 `mysql目录` 下的 `bin目录`，命令行执行：

```shell
# 初始化数据库
$ .\mysqld --initialize --console
```

假如你得到：

```shell
...
2018-04-20T02:35:05.464644Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: sQjjZSffo6)C
...
```

代表初始化成功，并且 `sQjjZSffo6)C` 就是你的登录密码，可以在登陆后修改。你可以通过以下命令查询mysql版本：

```shell
$ .\mysqladmin --version

# 得到：mysqladmin  Ver 8.42 Distrib 5.7.23, for Win64 on x86_64
```

然后以管理员身份进入这个bin目录，并输入以下安装命令：

```shell
$ .\mysqld install
```

启动输入以下命令即可启动mysql：

```shell
$ net start mysql
```

[![image-20211203170450386](https://i.loli.net/2021/12/03/mM2qEfAcuDP95IB.png)](https://i.loli.net/2021/12/03/mM2qEfAcuDP95IB.png)

如果你出现了上图的错误，可以执行：

```shell
$ .\mysqld --remove
Service successfully removed.

$ .\mysqld --install
Service successfully installed.

$ net start mysql
MySQL 服务正在启动 .
MySQL 服务已经启动成功。
```

这样就算是mysql安装并启动成功了。

### 4、修改MySQL密码

现在修改mysql密码，重新打开 `my.ini`，把这一行的注释解掉：

```
skip-grant-tables
```

然后重启mysql：

```shell
$ net stop mysql
$ net start mysql
```

此时的mysql密码已经被取消，可以随意输入密码进入：

```shell
$ .\mysql -uroot -p
```

然后修改密码：

```shell
$ mysql> use mysql;
$ mysql> update user set authentication_string=password("123456") where user="root";
$ mysql> flush privileges;  # 刷新权限
$ mysql> quit;				# 退出mysql
```

> 注意：密码字段名 5.7 版本的是 **authentication\_string**，之前的为 **password**。

然后重新回到 `my.ini` 把刚刚那行注释掉：

```
# skip-grant-tables;
```

重启mysql：

```shell
$ net stop mysql
$ net start mysql
```

再一次尝试连接mysql：

```shell
$ .\mysql -uroot -p123456
```

出现以下界面，代表成功：

[![image-20211203171816525](https://i.loli.net/2021/12/03/cXK56281hPJLBOW.png)](https://i.loli.net/2021/12/03/cXK56281hPJLBOW.png)

由于版本等问题，建议在连接mysql成功后，再一次执行：

```shell
$ mysql> alter user user() identified by "123456";
```

## 十、Navicat安装【软件还需整理】

`Navicat for MySQL` 是一套全面的前端工具为数据库管理、开发和维护提供了一款直观而强大的图形界面，能同时连接 MySQL 和 MariaDB 数据库。

有关 `Navicat for MySQL` 软件的下载和安装，大家可以百度搜索，当然，我这里也给大家找到一篇文章：

<https://www.cnblogs.com/yanghongtao/p/10976526.html>

### 1、Windows安装

**Windows64位版本**：链接：<https://pan.baidu.com/s/1fly3pVOE_6oyauOEbLj1Vw> 提取码：ndpb

下载解压后直接双击两个.exe程序进行安装与绿化：

[![image-20211203172743984](https://i.loli.net/2021/12/03/9PdBFVAQzWK8lyE.png)](https://i.loli.net/2021/12/03/9PdBFVAQzWK8lyE.png)

### 2、Mac安装

**M1 Mac版本**：链接：<https://pan.baidu.com/s/14xIcYUKIbSBAynNS7eD15w> 提取码：ndpb

打开直接拽入应用程序即可。

## 十一、数据库基本操作语句

### 1、数据库的连接与断开

如果希望在系统全局均可连接mysql，可以在系统环境变量配置mysql的bin目录。

```shell
# 连接数据库
$ mysql -uroot -p123456

# 断开数据库
$ mysql> quit;
# 或者
$ mysql> exit;
```

### 2、数据库基本操作

数据库的创建、删除、显示与选择。

```shell
# 创建数据库
$ mysql> create database test;

# 删除数据库（不要轻易使用）
$ mysql> drop database test;

# 显示数据库
$ mysql> show databases;

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| test               |
+--------------------+
8 rows in set (0.04 sec)

# 选择数据库
$ mysql> use test;
```

### 3、表操作

每个数据库都是由若干张表构成，每张表都是用来存储字段与数据的。以下是常用的表操作：

#### a. 创建表：

```
# 创建一张用户表（包含id、用户名与密码）
/*
	INT 			整数型
	PRIMARY KEY   	主键约束
	AUTO_INCREMENT	自增约束
	VARCHAR			长字符串
	COMMENT			字段注释
*/
create table user (
	id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(30) COMMENT '用户名称',
    password VARCHAR(30) COMMENT '用户密码'
);

Query OK, 0 rows affected (0.02 sec)
```

什么是自增约束？

> 如果字段id被定义为AUTO\_INCREMENT，在插入一行数据的时候，自增值的行为如下：
>
> 1.如果插入数据时id字段指定为0、null或未指定值，那么就把这个表当前的AUTO\_INCREMENT值填到自增字段
>
> 2.如果插入数据时id字段指定了具体的值，就直接使用语句里指定的值

#### b. 删除表（不要随便使用）：

```
drop table user;

Query OK, 0 rows affected (0.01 sec)
```

#### c. 查看表：

```
show tables;

+----------------+
| Tables_in_test |
+----------------+
| user           |
+----------------+
1 row in set (0.00 sec)
```

#### d. 描述表（查看该表的字段）：

```
describe user;

+----------+-------------+------+-----+---------+----------------+
| Field    | Type        | Null | Key | Default | Extra          |
+----------+-------------+------+-----+---------+----------------+
| id       | int(11)     | NO   | PRI | NULL    | auto_increment |
| username | varchar(30) | YES  |     | NULL    |                |
| password | varchar(30) | YES  |     | NULL    |                |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
```

### 4、Navicat使用

打开安装好的Navicat软件，连接后其实也可以看到刚刚的这些数据：

[![image-20211203175853171](https://i.loli.net/2021/12/03/QDZy79oHc4z1bTN.png)](https://i.loli.net/2021/12/03/QDZy79oHc4z1bTN.png)

### 5、表数据的增删改查

以上表操作中我们创建了一张user表，现在我们通过对这张表进行CRUD（增删改查）。

#### a. 增加表数据

```
$ mysql> INSERT INTO user VALUES (null, "张三", "123456");
$ mysql> INSERT INTO user VALUES (null, "李四", "234567");
```

这里要注意：id是自增的字段，填写0或null都会从1开始自增。

#### b. 查询表数据

如果想要查询user表中所有的数据：

```
$ mysql> SELECT * FROM user;

+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  1 | 张三     | 123456   |
|  2 | 李四     | 234567   |
+----+----------+----------+
2 rows in set (0.00 sec)
```

#### c. 更新表数据

假设现在想要修改李四的密码：

```
$ mysql> UPDATE user SET password="999999" WHERE username="李四";

# 更新后可以查询李四的密码
$ mysql> SELECT password FROM user WHERE username="李四";

+----------+
| password |
+----------+
| 999999   |
+----------+
1 row in set (0.00 sec)
```

#### d. 删除表数据

假设现在想要删除张三这条数据：

```
$ mysql> DELETE FROM user WHERE username="张三";

# 删除后查询表数据
$ mysql> SELECT * FROM user;

+----+----------+----------+
| id | username | password |
+----+----------+----------+
|  2 | 李四     | 999999   |
+----+----------+----------+
1 row in set (0.00 sec)
```

有关数据库基本操作语句的介绍就到这里，后续在项目中会有更多实战性应用。

## 十二、NodeJs中操作MySQL

### 1、创建连接池

NodeJs中想要连接MySQL，需要先在项目内安装mysql库：

```shell
npm i mysql
```

在操作下面的代码时，先创建一个数据库，本项目将创建一个名为cms的数据库。另外，在cms中添加一张user表，然后填充部分数据：

```
# 创建数据库cms
$ mysql> CREATE DATABASE cms;
# 选择数据库cms
$ mysql> USE DATABASE cms;
# 创建user表
$ mysql> CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(30) COMMENT "用户名称",
    password VARCHAR(30) COMMENT "用户密码"
);
# 新增user表数据
$ mysql> INSERT INTO user VALUES (null, "张三", "123456");
$ mysql> INSERT INTO user VALUES (null, "李四", "aaasss");
```

然后到utils.js中，引入mysql并创建连接池：

```js
// 引入mysql
const mysql = require("mysql");

// 创建连接池
const pool = mysql.createPool({
    host: "localhost",  // 连接的服务器(代码托管到线上后，需改为内网IP，而非外网)
    port: 3306, // mysql服务运行的端口
    database: "cms", // 选择某个数据库
    user: "root",   // 用户名
    password: "123456", // 用户密码
})

//对数据库进行增删改查操作的基础
const query = (sql,callback) => {
    pool.getConnection(function(err,connection){
        connection.query(sql, function (err,rows) {
            callback(err,rows)
            connection.release()
        })
    })
}

module.exports = {
    host, port, query
}
```

### 2、Node中使用连接池

假设用户访问/manage即可查询数据库cms中表user的数据。这样的话，需要找到manage/index.js：

```js
const Router = require("koa-router")
const router = new Router();
const utils = require("../../utils")

router.get('/', async ctx=>{
    let data = await new Promise((resolve, reject)=>{
        // 写一句sql语句
        var sql = `SELECT * FROM user`;
        utils.query(sql, (err, data)=>{
            if(err) reject(err);
            resolve(data);  // 返回拿到的数据
        })
    })
    ctx.body = data;    // 将查询结果返回到页面中
})

module.exports = router;
```

打开浏览器访问<http://localhost:9000>，即可得到返回的数据：

[![image-20211205104329066](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfwyi4xsj305v060dfu.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gxdfwyi4xsj305v060dfu.jpg)

### 3、查询指定字段

使用逗号隔开字段，这种选择方式会返回一个数组

```
SELECT username,password 	FROM user
```

至此，我们已经可以实现在Koa中操作数据库。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/cms/part3koa-yu-mysql.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
