1 Star 0 Fork 1

siffer/KOA

forked from jeyarLin/KOA 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
MIT

一、项目初始化

npm init -y
git init
// 创建.gitignore文件

二、搭建项目

1.安装 koa

npm install koa

2.编写最基本的 app

创建 src/main.js

const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
  ctx.body = "hello api";
});
app.listen(3000, () => {
  console.log("server is running on http://localhost:3000");
});

运行方式一:
node src/main.js

三、项目的基本优化

1.自动重启服务

安装 nodemon 工具

npm i nodemon

编写 package.json 脚本

"scripts": {
    "dev":"nodemon ./src/main.js",
    "test": "echo \"Error: no test specified\" && exit 1"
},

执行 'npm run dev'启动服务

2.读取配置文件

安装 dotenv,读取根目录中的.env 文件,将配置写 process.env 中

npm i dotenv

创建.env 文件

APP_PORT=8000

创建 src/config/config.default.js

const dotenv = require("dotenv");

dotenv.config();

// console.log(process.env.APP_PORT);

module.exports = process.env;

改写 main.js

const Koa = require("koa");
const { APP_PORT } = require("./config/config.default");

const app = new Koa();

app.use((ctx, next) => {
  ctx.body = "hello api";
});

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

四、添加路由

路由:根据不同的 URL,调用对应处理函数

1 安装 koa-router

npm i koa-router

步骤

  • 导入包
  • 实例化对象
  • 编写路由
  • 注册中间件

2 编写路由

创建/src/router 目录,编写 user.route.js

const Router = require("koa-router");

const router = new Router({ prefix: "/users" });

// GET /users/
router.get("/", (ctx, next) => {
  ctx.body = "hello users";
});

module.exports = router;

3 改写 mian.js

const Koa = require("koa");
const { APP_PORT } = require("./config/config.default");
const userRouter = require("./router/user.route");

const app = new Koa();

app.use(userRouter.routes());

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

五、目录结构优化

1 将 http 服务和 app 业务拆分

新建 src/app/index.js

const Koa = require("koa");

const userRouter = require("../router/user.route");

const app = new Koa();

app.use(userRouter.routes());

module.exports = app;

改写 mian.js

const { APP_PORT } = require("./config/config.default");
const app = require("./app");

app.listen(APP_PORT, () => {
  console.log(`server is running on http://localhost:${APP_PORT}`);
});

2 将路由和控制器拆分

  • 路由:解析 URL,分发给控制器方法。
  • 控制器:处理不同的业务
  • 改写 user.route.js
const Router = require("koa-router");

const { register, login } = require("../controller/user.controller");

const router = new Router({ prefix: "/users" });

// 注册接口
router.post("/register", register);

// 登录接口
router.post("/login", login);

module.exports = router;

创建 controller/user.controller.js

class UserController {
  async register(ctx, next) {
    ctx.body = "注册成功";
  }
  async login(ctx, next) {
    ctx.body = "登录成功";
  }
}

module.exports = new UserController();

卸载 npm un nodemon ;安装 npm i nodemon -D

六、解析 body 拆分service层

1 安装 koa-body

npm i koa-body

2 注册中间件

改写app/index.js

const Koa = require("koa");
const { koaBody } = require("koa-body");

const userRouter = require("../router/user.route");

const app = new Koa();

app.use(koaBody());
app.use(userRouter.routes());

module.exports = app;

3 解析请求的数据 改写controller/user.controller.js

const { createUser } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1.获取数据
    // console.log(ctx.request.body);
    const { user_name, password } = ctx.request.body;
    // 2.操作数据库
    const res = await createUser(user_name, password);
    console.log(res);
    // 3.返回结果
    ctx.body = ctx.request.body;
  }
  async login(ctx, next) {
    // ctx.body = "登录成功";
    console.log(ctx.request.body);
    ctx.body = ctx.request.body;
  }
}

module.exports = new UserController();

4 拆分service层 -service层主要是做数据库处理 -创建src/service/user.service.js

class UserService {
  async createUser(user_name, password) {
    // todo 写入数据库
    return "写入数据库成功";
  }
}
module.exports = new UserService();

集成sequelize

sequelize ORM数据库工具

ORM:对象关系映射

  • 数据表映射(对应)一个类
  • 数据表中的数据行(记录)对应一个对象
  • 数据表字段对应对象的属性
  • 数据表的操作对应对象的方法

1 安装sequelize

npm i mysql2 sequelize

2 连接数据库

src/db/seq.js

const { Sequelize } = require("sequelize");
const {
  MYSQL_HOST,
  MYSQL_PORT,
  MYSQL_USER,
  MYSQL_PWD,
  MYSQL_DB,
} = require("../config/config.default");
// 方法 3: 分别传递参数 (其它数据库)
const sequelize = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PWD, {
  host: MYSQL_HOST,
  // 选择一种支持的数据库:
  // 'mysql', 'mariadb', 'postgres', 'mssql', 'sqlite', 'snowflake', 'db2' or 'ibmi'
  dialect: "mysql",
});

sequelize
  .authenticate()
  .then(() => {
    console.log("数据库链接成功");
  })
  .catch((err) => {
    console.log("数据库链接失败", err);
  });

// node src/db/seq.js

module.exports = sequelize;

3 编写配置文件

.env

APP_PORT = 8000

MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_USER = root
MYSQL_PWD = 123456
MYSQL_DB = dbtest1

八 创建User模型

sequelize主要通过Model对应数据表 创建src/model/user.model.js

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

// https://www.sequelize.cn/
const seq = require("../db/seq");
// 创建模型(Model zd_user -> 表 zd_users)
const User = seq.define("zd_user", {
  // id会被sequelize自动创建,管理
  user_name: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    comment: "用户名,唯一",
  },
  password: {
    type: DataTypes.CHAR(64),
    allowNull: false,
    comment: "密码",
  },
  is_admin: {
    type: DataTypes.BOOLEAN,
    allowNull: false,
    defaultValue: 0,
    comment: "是否为管理员,0:不是管理员(默认);1:是管理员",
  },
});

// 强制同步数据库(创建数据表)
// User.sync({ force: true });

// node src/model/user.model.js

module.exports = User;

九、添加用户

所有数据库的操作都在 Service 层完成, Service 调用 Model 完成数据库操作

改写src/service/user.service.js

const User = require("../model/user.model");

class UserService {
  async createUser(user_name, password) {
    // 插入数据
    // User.create({
    //   user_name: user_name,
    //   password: password,
    // });

    // await表达式:promise对象的值
    const res = await User.create({ user_name, password });
    return res.dataValues;
  }
}
module.exports = new UserService();

同时, 改写user.controller.js

const { createUser } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1.获取数据
    // console.log(ctx.request.body);
    const { user_name, password } = ctx.request.body;

    // 2.操作数据库
    const res = await createUser(user_name, password);
    // console.log(res);
    // 3.返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }
  async login(ctx, next) {
    // ctx.body = "登录成功";
    console.log(ctx.request.body);
    ctx.body = ctx.request.body;
  }
}

module.exports = new UserController();

十、 错误处理

在控制器中, 对不同的错误进行处理, 返回不同的提示错误提示, 提高代码质量

src/controller/user.controller.js

const { createUser, getUserInfo } = require("../service/user.service");

class UserController {
  async register(ctx, next) {
    // 1.获取数据
    // console.log(ctx.request.body);
    const { user_name, password } = ctx.request.body;
    // 合法性
    if (!user_name || !password) {
      console.error("用户名或密码为空", ctx.request.body);
      ctx.status = 400;
      ctx.body = {
        code: "10001",
        message: "用户名或密码为空",
        result: "",
      };
      return;
    }
    // 合理性
    if (getUserInfo({ user_name })) {
      ctx.status = 409;
      ctx.body = {
        code: "10002",
        message: "用户已经存在",
        result: "",
      };
      return;
    }
    // 2.操作数据库
    const res = await createUser(user_name, password);
    // console.log(res);
    // 3.返回结果
    ctx.body = {
      code: 0,
      message: "用户注册成功",
      result: {
        id: res.id,
        user_name: res.user_name,
      },
    };
  }
  async login(ctx, next) {
    // ctx.body = "登录成功";
    console.log(ctx.request.body);
    ctx.body = ctx.request.body;
  }
}

module.exports = new UserController();

在service中封装函数getUserInfo

const User = require("../model/user.model");

class UserService {
  async createUser(user_name, password) {
    // 插入数据
    // await表达式:promise对象的值
    const res = await User.create({ user_name, password });
    return res.dataValues;
  }
  async getUserInfo({ id, user_name, password, is_admin }) {
    const whereOpt = {};

    id && Object.assign(whereOpt, { id });
    user_name && Object.assign(whereOpt, { user_name });
    password && Object.assign(whereOpt, { password });
    is_admin && Object.assign(whereOpt, { is_admin });

    const res = await User.findOne({
      attributes: ["id", "user_name", "password", "is_admin"],
      where: whereOpt,
    });

    return res ? res.dataValues : null;
  }
}
module.exports = new UserService();

十一、拆分中间件

为了使代码的逻辑更加清晰, 我们可以拆分一个中间件层, 封装多个中间件函数

1 拆分中间件

添加src/middleware/user.middleware.js

const { getUserInfo } = require("../service/user.service");
const { userFormateError, userAlreadyExisted } = require("../constant/err.type");

const userValidator = async (ctx, next) => {
  const { user_name, password } = ctx.request.body;
  // 合法性
  if (!user_name || !password) {
    console.error("用户名或密码为空", ctx.request.body);
    ctx.app.emit("error", userFormateError, ctx);
    return;
  }
  await next();
};

const verifyUser = async (ctx, next) => {
  const { user_name } = ctx.request.body;
  // 合理性 deferent
  const res = await getUserInfo({ user_name });
  if (res) {
    console.error("用户名已经存在", { user_name });
    ctx.app.emit("error", userAlreadyExisted, ctx);
    return;
  }
  await next();
};

module.exports = { userValidator, verifyUser };

2 统一错误处理

  • 在出错的地方使用ctx.app.emit提交错误
  • 在 app 中通过app.on监听

编写统一的错误定义文件

src/constant/err.type.js

module.exports = {
  userFormateError: {
    code: '10001',
    message: '用户名或密码为空',
    result: '',
  },
  userAlreadyExisted: {
    code: '10002',
    message: '用户已经存在',
    result: '',
  },
}

3 错误处理函数

src/app/errHandler.js

module.exports = (err, ctx) => {
  let status = 500;
  switch (err.code) {
    case "10001":
      status = 400;
      break;
    case "10002":
      status = 409;
      break;
    default:
      status = 500;
  }
  ctx.status = status;
  ctx.body = err;
};

改写app/index.js

const errHandler = require('./errHandler')
// 统一的错误处理
app.on('error', errHandler)

十二. 加密

在将密码保存到数据库之前, 要对密码进行加密处理

123123abc (加盐) 加盐加密

1 安装 bcryptjs

npm i bcryptjs

2 编写加密中间件

src/middleware/user.middleware.js

  const bcrypt = require('bcryptjs');

const crpytPassword = async (ctx, next) => {
  const { password } = ctx.request.body

  const salt = bcrypt.genSaltSync(10)
  // hash保存的是 密文
  const hash = bcrypt.hashSync(password, salt)

  ctx.request.body.password = hash

  await next()
}

3 在 router 中使用

改写user.router.js

const {
  userValidator,
  verifyUser,
  crpytPassword,
} = require("../middleware/user.middleware");

// 注册接口
router.post("/register", userValidator, verifyUser, crpytPassword, register);

十四、验证登录

改写 src/middleware/user.middleware.js

const {
  userDoesNotExist,
  invalidPassword,
  userLoginError,
} = require("../constant/err.type");

const verifyLogin = async (ctx, next) => {
  // 1. 判断用户是否存在(不存在:报错)
  const { user_name, password } = ctx.request.body;

  try {
    const res = await getUserInfo({ user_name });

    if (!res) {
      console.error("用户名不存在", { user_name });
      ctx.app.emit("error", userDoesNotExist, ctx);
      return;
    }

    // 2. 密码是否匹配(不匹配: 报错)
    if (!bcrypt.compareSync(password, res.password)) {
      ctx.app.emit("error", invalidPassword, ctx);
      return;
    }
  } catch (err) {
    console.error(err);
    return ctx.app.emit("error", userLoginError, ctx);
  }

  await next();
};

module.exports = { userValidator, verifyUser, crpytPassword, verifyLogin };

定义错误类型 src/constant/err.type.js

module.exports = {
  userFormateError: {
    code: '10001',
    message: '用户名或密码为空',
    result: '',
  },
  userAlreadyExisted: {
    code: '10002',
    message: '用户已经存在',
    result: '',
  },
  userRegisterError: {
    code: '10003',
    message: '用户注册错误',
    result: '',
  },
  userDoesNotExist: {
    code: '10004',
    message: '用户不存在',
    result: '',
  },
  userLoginError: {
    code: '10005',
    message: '用户登录失败',
    result: '',
  },
  invalidPassword: {
    code: '10006',
    message: '密码不匹配',
    result: '',
  },
}

改写路由

// 登录接口
router.post('/login', userValidator, verifyLogin, login)

十四、用户的认证

登录成功后, 给用户颁发一个令牌 token, 用户在以后的每一次请求中携带这个令牌.

jwt: jsonwebtoken

  • header: 头部
  • payload: 载荷
  • signature: 签名

1 颁发 token

1) 安装 jsonwebtoken

npm i jsonwebtoken

2) 在控制器中改写 login 方法

async login(ctx, next) {
  const { user_name } = ctx.request.body

  // 1. 获取用户信息(在token的payload中, 记录id, user_name, is_admin)
  try {
    // 从返回结果对象中剔除password属性, 将剩下的属性放到res对象
    const { password, ...res } = await getUserInfo({ user_name })

    ctx.body = {
      code: 0,
      message: '用户登录成功',
      result: {
        token: jwt.sign(res, JWT_SECRET, { expiresIn: '1d' }),
      },
    }
  } catch (err) {
    console.error('用户登录失败', err)
  }
}

3) 定义私钥

.env定义

JWT_SECRET = xzd

2 用户认证

1) 创建 auth 中间件

const jwt = require('jsonwebtoken')

const { JWT_SECRET } = require('../config/config.default')

const { tokenExpiredError, invalidToken } = require('../constant/err.type')

const auth = async (ctx, next) => {
  const { authorization } = ctx.request.header
  const token = authorization.replace('Bearer ', '')
  console.log(token)

  try {
    // user中包含了payload的信息(id, user_name, is_admin)
    const user = jwt.verify(token, JWT_SECRET)
    ctx.state.user = user
  } catch (err) {
    switch (err.name) {
      case 'TokenExpiredError':
        console.error('token已过期', err)
        return ctx.app.emit('error', tokenExpiredError, ctx)
      case 'JsonWebTokenError':
        console.error('无效的token', err)
        return ctx.app.emit('error', invalidToken, ctx)
    }
  }

  await next()
}

module.exports = {
  auth,
}

2) 改写 router

// 修改密码接口
router.patch('/', auth, (ctx, next) => {
  console.log(ctx.state.user)
  ctx.body = '修改密码成功'
})

新的内容

MIT License Copyright (c) 2023 jeyarLin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

简介

nodeJs koa2 展开 收起
MIT
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/siffer/koa.git
git@gitee.com:siffer/koa.git
siffer
koa
KOA
master

搜索帮助