Koa项目搭建教程
前言
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,相较于express更小、更快、更加灵活。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。支持ES6/7语法,代码可读性更高。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
最近摸鱼学习了一下 Koa
,直接采用实战的方式学习,内容仅供参考。
学习目标
- 如何搭建运行Koa项目
- 如何编写Koa中间件
- 如何实现日志管理
- 如何实现路由配置
- 如何实现权限控制
- 如何实现数据库操作
- 最终实现开箱即用
一、环境准备
- Node:V14.x版本及以上
- npm:6.x 及以上
- Koa:V2.x
- sequelize:V5.x版本及以上
- Mysql:mariadb
二、搭建项目&安装依赖
这里没有采用koa的脚手架进行创建,自己手动搭建项目,能够了解收获更多Koa相关的知识。
创建项目
// 创建根目录
mkdir node-koa2-mysql
// 进入目录
cd node-koa2-mysql
// 初始化项目
npm init
安装koa依赖包
// koa
npm install koa -s
npm install koa-bodyparser -s
// 热重启
npm install nodemon -s -d
// 路由中间件
npm install koa-router -s
// 数据库中间件
npm install sequelize -s
npm install mysql2 -s
// 密码加密插件
npm install bcrypt -s
// 跨域中间件
npm install koa2-cors -s
// joa-jwt中间件
npm install koa-jwt -s
// jsonwebtoken,插件用于生成、校验、解码jwt
npm install jsonwebtoken -s
// 静态资源中间件
npm install koa-static-cache -s
// 日志中间件
npm install log4js -s
// 网络安全中间件
npm install koa-helmet -s
目录结构
完整的项目目录结构如下:
node-koa2-mysql
├─ app.js
├─ bin
│ └─ http.js
├─ logs
├─ package-lock.json
├─ package.json
├─ pm2.config.js
└─ src
├─ controllers
│ ├─ index.js
│ └─ user.js
├─ logs
│ ├─ koa-template.log
│ └─ koa-template.log.-2022-02-09
├─ middlewares
│ ├─ cors.js
│ ├─ jwt.js
│ ├─ logger.js
│ └─ response.js
├─ models
│ └─ user.js
├─ routers
│ └─ routes.js
├─ services
│ ├─ index.js
│ └─ user.js
└─ utils
├─ config.js
├─ db.js
└─ error.js
新建app.js
根目录下创建app.js文件。
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const app = new Koa()
console.log("----------------------启动中----------------------");
app.use(bodyParser())
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa'
})
module.exports = app
创建通用配置文件
// src/utils/config.js
'use strict'
const path = require('path')
module.exports = {
port: '3001',
// 配置成自己的数据库服务器配置
database: {
dbName: 'koa_test',
host: '127.0.0.1',
user: 'root',
password: 'root',
port: '3306',
},
// jwt密钥
security: {
secretKey: '2022',
expiresIn: 60 * 60 * 24 * 30
},
logPath: path.resolve(__dirname, '../logs/koa-template.log'),
logLevel: 'info'
}
新建www
bin目录下创建www文件
// bin/www
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app')
const http = require('http')
const config = require('../src/utils/config')
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || config.port)
// app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app.callback())
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10)
if (isNaN(port)) {
// named pipe
return val
}
if (port >= 0) {
// port number
return port
}
return false
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges')
process.exit(1)
break
case 'EADDRINUSE':
console.error(bind + ' is already in use')
process.exit(1)
break
default:
throw error
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address()
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port
console.log('Listening on ' + bind)
}
三、服务管理
本地开发启动服务(nodemon)
nodemon是一种工具,可以自动检测到目录中的文件更改时通过重新启动应用程序来调试基于node.js的应用程序。
安装依赖
// 热重启
npm install nodemon -s -d
启动服务
//修改package.json ,scripts中新增命令:
"scripts":{
"dev": "nodemon ./bin/www",
}
服务器启动服务(pm2)
PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等,而且使用非常简单。
pm2常用命令
启动进程/应用 pm2 start bin/www 或 pm2 start app.js
重命名进程/应用 pm2 start app.js --name wb123
添加进程/应用watch pm2 start bin/www --watch
结束进程/应用 pm2 stop www
结束所有进程/应用 pm2 stop all
删除进程/应用 pm2 delete www
删除所有进程/应用 pm2 delete all
列出所有进程/应用 pm2 list
查看某个进程/应用具体情况 pm2 describe www
查看进程/应用的资源消耗情况 pm2 monit
查看pm2的日志 pm2 logs
若要查看某个进程/应用的日志,使用 pm2 logs www
重新启动进程/应用 pm2 restart www
重新启动所有进程/应用 pm2 restart all
安装依赖
npm install -g pm2
新增配置文件
// 根目录下新增pm2.config.js
module.exports = {
apps: [{
name: 'API',
script: './bin/www',
args: 'one two',
instances: 1,
autorestart: true,
watch: true,
ignore_watch: [ // 不用监听的文件
'node_modules',
'logs'
],
error_file: "./logs/app-err.log", // 错误日志文件
out_file: "./logs/app-out.log", // 正常日志文件
log_date_format: "YYYY-MM-DD HH:mm:ss",
max_memory_restart: '1G',
env_pro: {
"NODE_ENV": "production",
"REMOTE_ADDR": ""
},
env_dev: {
"NODE_ENV": "development",
"REMOTE_ADDR": ""
},
env_test: {
"NODE_ENV": "test",
"REMOTE_ADDR": ""
}
}]
};
启动服务
//修改package.json ,scripts中新增命令:
"scripts":{
"start": "pm2 start pm2.config.js",
"stop": "pm2 stop pm2.config.js"
}
四、日志处理
日志管理中间件(log4js)
日志对任何的应用来说都是至关重要的。在Nodejs中使用koa框架并没有自带的日志模块,由于我的整个项目是基于koa框架,所以我选择log4js来完成日志记录的功能。
安装依赖
npm install log4js -s
封装logger.js
middlewares目录下创建文件logger.js
// src/middlewares/logger.js
'use strict'
const fs = require('fs')
const path = require('path')
const log4js = require('log4js')
const config = require('../utils/config')
const logsDir = path.parse(config.logPath).dir
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir)
}
log4js.configure({
appenders: {
console: { type: 'console' },
dateFile: { type: 'dateFile', filename: config.logPath, pattern: '-yyyy-MM-dd' },
},
categories: {
default: {
appenders: ['console', 'dateFile'],
level: 'info'
}
},
pm2: true
})
const logger = log4js.getLogger('[Default]')
logger.level = config.logLevel;
module.exports = {
logInfo: async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
const remoteAddress = ctx.headers['x-forwarded-for'] || ctx.ip || ctx.ips ||
(ctx.socket && (ctx.socket.remoteAddress || (ctx.socket.socket && ctx.socket.socket.remoteAddress)))
const logText = `${ctx.method} ${ctx.status} ${ctx.url} 请求参数: ${JSON.stringify(ctx.request.body)} 响应参数: ${JSON.stringify(ctx.body)} - ${remoteAddress} - ${ms}ms`
logger.info(logText)
},
logger
}
更新app.js
使用日志中间件的时候,必须放在第一个中间件,保证所以的请求及操作会先经过logger进行记录再到下一个中间件
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const { logInfo } = require('./src/middlewares/logger')
console.log("----------------------启动中----------------------");
const app = new Koa()
app.use(logInfo) // 日志中间件
app.use(bodyParser())
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa'
})
module.exports = app
五、跨域处理
跨域处理中间件(koa2-cors)
在前后端接口请求中,由于浏览器的限制,会出现跨域的情况。koa中可以通过cors中间件实现跨域处理。
安装依赖
npm install koa2-cors -s
封装cors.js
middlewares目录下创建文件cors.js
'use strict'
module.exports = {
origin: function (ctx) { //设置允许来自指定域名请求
const whiteList = ['1.15.224.138', 'localhost:3000']; //可跨域白名单
let url = ctx.header.host
if (whiteList.includes(url)) {
return url //注意,这里域名末尾不能带/,否则不成功,所以在之前我把/通过substr干掉了
}
return 'http://localhost::3000' //默认允许本地请求3000端口可跨域
},
maxAge: 5, //指定本次预检请求的有效期,单位为秒。
credentials: true, //是否允许发送Cookie
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], //设置所允许的HTTP请求方法
allowHeaders: ['Content-Type', 'Authorization', 'Accept'], //设置服务器支持的所有头信息字段
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'] //设置获取其他自定义字段
}
更新app.js
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors')
const { logInfo } = require('./src/middlewares/logger')
const corsConfig = require('./src/middlewares/cors')
console.log("----------------------启动中----------------------");
const app = new Koa()
app.use(logInfo) // 日志中间件
app.use(bodyParser())
app.use(cors(corsConfig)) // cors管理
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa'
})
module.exports = app
六、数据处理
数据统一处理中间件
这个中间件主要是用来对返回前端的响应进行处理,通过统一封装,规范数据响应格式。
封装response.js
middlewares目录下创建文件response.js
// src/middlewares/response.js
'use strict'
const { logger } = require('./logger')
/**
* 正常响应
* 回传的格式遵循这样的格式:{ code: 0, msg: any data: any }
* @param {*} ctx
*/
const success = async (ctx, next) => {
if (ctx.result !== undefined) {
ctx.type = 'json'
ctx.body = {
code: 200,
msg: ctx.msg || '',
data: ctx.result
}
}
await next()
}
/**
*
* 统一异常处理
* @param {*} ctx
* @param {*} next
* @return {*} { code: '错误代码', msg: '错误信息' }
*/
const error = async (ctx, next) => {
await next().catch(err => {
if (err.code == null) {
logger.error(err.stack)
}
ctx.body = {
code: err.code || -1,
data: null,
msg: err.message.trim()
}
ctx.status = 200 // 前端根据code判断异常
return Promise.resolve()
})
}
module.exports = {
success,
error
}
更新app.js
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors')
const { logInfo } = require('./src/middlewares/logger')
const corsConfig = require('./src/middlewares/cors')
const { error, success } = require('./src/middlewares/response')
console.log("----------------------启动中----------------------")
const app = new Koa()
app.use(logInfo) // 日志中间件
app.use(error) // 统一异常处理
app.use(bodyParser())
app.use(cors(corsConfig)) // cors管理
app.use(success) // response正常响应管理
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa'
})
module.exports = app
七、安全处理
安全中间件(koa-helmet)
koa-helmet 可以帮助你的 app 抵御一些比较常见的安全 web 安全隐患
安装依赖
npm install koa-helmet -s
更新app.js
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors')
const helmet = require("koa-helmet")
const { logInfo } = require('./src/middlewares/logger')
const corsConfig = require('./src/middlewares/cors')
const { error, success } = require('./src/middlewares/response')
console.log("----------------------启动中----------------------")
const app = new Koa()
app.use(logInfo) // 日志管理
app.use(error) // 统一异常处理管理
app.use(bodyParser())
app.use(helmet()) // 网络安全中间件
app.use(cors(corsConfig)) // cors管理
app.use(success) // response正常响应管理
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa'
})
module.exports = app
八、登录控制
身份鉴权中间件(koa-jwt)
JSON Web Token(JWT)是一种流行的 RESTful API 鉴权方案。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息。
安装依赖
// joa-jwt中间件
npm install koa-jwt -s
// jsonwebtoken,插件用于生成、校验、解码jwt
npm install jsonwebtoken -s
封装jwt.js
middlewares目录下创建文件jwt.js
// src/middlewares/jwt.js
'use strict'
const koaJwt = require('koa-jwt')
const jwt = require('jsonwebtoken')
const config = require('../utils/config')
module.exports = {
/**
* 获取用户token
* @param {string} data 加密信息
* @return {string} 返回生成的Token
*/
signToken: (data) => {
try {
return jwt.sign({
data: data,
}, config.security.secretKey, { expiresIn: config.security.expiresIn })
} catch (error) {
console.log(error)
}
next()
},
/**
* 验证用户token值
* @static
* @param {string} token 用户token
* @return {*} {Object}
*/
verifyToken: async (ctx, next) => {
try {
ctx.jwtData = await jwt.verify(ctx.request.headers.authorization, config.security.secretKey)
await next()
} catch (err) {
throw { code: 401, message: err.message }
}
}
}
八、路由配置
本篇教程中,我们将实现以下路由作为demo:
POST /api/user/login :登录(获取 JWT Token)
GET /api/user/:id :获取用户信息
GET /api/user/login :获取用户列表
安装依赖
npm install koa-router -s
实现Controller
创建 controllers
目录,存放控制器代码。
创建 index.js
,代码如下:
// src/controllers/index.js
'use strict'
const fs = require('fs')
const files = fs.readdirSync(__dirname).filter(file => file !== 'index.js')
const controllers = {}
for (const file of files) {
if (file.toLowerCase().endsWith('js')) {
const controller = require(`./${file}`)
controllers[`${file.replace(/\.js/, '')}`] = controller
}
}
module.exports = controllers
创建 UserController
,代码如下:
// src/controllers/UserController.js
'use strict'
const jwt = require('../middlewares/jwt')
const config = require('../utils/config')
const login = (ctx, next) => {
const { userName, password } = ctx.request.body
if (!userName || !password) {
console.log('用户名或密码不能为空')
}
if (userName !== 'admin') {
ctx.result = ''
ctx.msg = '用户不存在'
} else if (userName === 'admin' && Number(password) !== 1) {
console.log(userName, password)
ctx.result = ''
ctx.msg = '用户密码错误'
} else {
ctx.result = jwt.signToken(ctx.request.body)
}
return next()
}
const getUserInfo = (ctx, next) => {
if (ctx.jwtData) {
// 此处根据解密后的jwtData去获取用户详情
ctx.result = {
name: 'admin',
role: '管理员'
}
}
return next()
}
module.exports = {
login,
getUserInfo
}
实现路由
创建 src/routes.js
把控制器挂载到对应的路由上;将路由分为两套,一套权限控制路由需要进行鉴权访问,另外一套公共路由不做访问限制,如下图:
// src/router/index.js
'use strict'
const Router = require('koa-router')
const controllers = require('../controllers')
const jwt = require('../middlewares/jwt')
// -------------公共路由(无需token校验)-----------------------
const publicRouter = new Router()
publicRouter.prefix('/api')
publicRouter.post('/user/login', controllers.userController.login)
// --------------私有路由(需token校验)----------------------
const privateRouter = new Router()
privateRouter.prefix('/api')
privateRouter.use(jwt.verifyToken) // 校验token
privateRouter.get('/user/:id', controllers.userController.getUserInfo)
module.exports = {
publicRouter, privateRouter
}
修改app.js
将 router 注册为中间件,修改app.js:
'use strict'
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors')
const helmet = require("koa-helmet")
const { logInfo } = require('./src/middlewares/logger')
const corsConfig = require('./src/middlewares/cors')
const { error, success } = require('./src/middlewares/response')
const { publicRouter, privateRouter } = require('./src/routes/routes')
console.log("----------------------启动中----------------------")
const app = new Koa()
app.use(logInfo) // 日志管理
app.use(error) // 统一异常处理管理
app.use(bodyParser())
app.use(helmet()) // 网络安全中间件
app.use(cors(corsConfig)) // cors管理
app.use(publicRouter.routes(), publicRouter.allowedMethods()) // 公共路由
app.use(privateRouter.routes(), privateRouter.allowedMethods()) // 私有路由
app.use(success) // response正常响应管理
module.exports = app
测试请求
使用postman来进行请求测试,如下图:
获取token
非法token
有效token请求
十、DB操作
安装依赖
// 数据库中间件
npm install sequelize -s
npm install mysql2 -s
// 密码加密插件
npm install bcrypt -s
// 时间处理插件
npm install dayjs -s
创建数据库配置
// src/utils/db.js
'use strict'
const { Sequelize, Model } = require('sequelize');
const { dbName, host, port, user, password } = require('./config').database;
const sequelize = new Sequelize(dbName, user, password, {
dialect: 'mysql',
host,
port,
timezone: '+08:00',
logging: true,
define: {
//creat_time update_time delete_time
timestamps: true,
paranoid: true,
createdAt: 'created_at',
updateAt: 'update_at',
deleteAt: 'delete_at',
underscored: true
}
})
module.exports = {
sequelize
}
实现Services
创建 services
目录,主要用来存放处理数据库以及服务等逻辑代码。
创建 index.js
,代码如下:
// src/services/index.js
'use strict'
const fs = require('fs')
const files = fs.readdirSync(__dirname).filter(file => file !== 'index.js')
const services = {}
for (const file of files) {
if (file.toLowerCase().endsWith('js')) {
const service = require(`./${file}`)
services[`${file.replace(/\.js/, '')}`] = service
}
}
module.exports = services
创建数据模型:UserModel
,代码如下:
// src/models/user.js
'use strict'
const bcrypt = require('bcrypt')
const dayjs = require('dayjs')
const {
Sequelize,
DataTypes,
Model
} = require('sequelize')
const { sequelize } = require('../utils/db')
class User extends Model { }
User.init({
//主键:不能重复 不能为空
//设计编号系统 id
//考虑并发 自增长导致报错
userId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userName: {
type: DataTypes.STRING,
allowNull: false,
},
password: {
type: DataTypes.STRING,
allowNull: false,
//观察者模式
set(val) {
const salt = bcrypt.genSaltSync(10)// 10 计算机生成盐的成本 安全系数
const pwd = bcrypt.hashSync(val, salt)//密码加盐 防止彩虹攻击
this.setDataValue('password', pwd)
}
},
status: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
},
role: {
type: DataTypes.STRING(50),
},
createTime: {
type: DataTypes.DATE, defaultValue: DataTypes.NOW,
get() {
return dayjs(this.getDataValue('createTime')).format('YYYY-MM-DD HH:mm:ss')
}
},
updateTime: {
type: DataTypes.DATE, defaultValue: DataTypes.NOW,
get() {
return dayjs(this.getDataValue('updateTime')).format('YYYY-MM-DD HH:mm:ss')
}
}
}, {
sequelize,
tableName: 'user'
})
module.exports = {
User
}
创建 UserService
,代码如下:
// src/services/userService.js
'use strict'
const { User } = require('../models/user')
const user = {
/**
* @Description: 登录
* @params: { String } userName
* @params: { String } password
* @return: { Object | null }
*/
async login(userName, password) {
return await User.findOne({
where: {
userName,
password
}
})
},
/**
* @Description: 获取用户信息
* @params: { String } 用户ID
* @return: { Object | null }
*/
async getUserInfo(userId) {
return await User.findByPk(userId)
},
/**
* @Description: 获取用户列表
* @params: { String } 用户ID
* @return: { Array | null }
*/
async getUserList() {
return await User.findAll({
attributes: ['userId', 'userName', 'status', 'role', 'createTime']
})
}
}
module.exports = user
更新控制器
修改 UserController
,代码如下:
// src/controllers/userController.js
'use strict'
const jwt = require('../middlewares/jwt')
const userService = require('../services').userService
/**
* 登录
* @param {*} ctx
* @param {*} next
* @returns
*/
const login = async (ctx, next) => {
const { userName, password } = ctx.request.body
if (!userName || !password) {
console.log('用户名或密码不能为空')
}
const user = await userService.login(
userName,
password
)
if (user?.dataValues) {
ctx.result = jwt.signToken(user?.dataValues)
}
return next()
}
/**
* 获取用户详情
* @param {*} ctx
* @param {*} next
* @returns
*/
const getUserInfo = async (ctx, next) => {
if (ctx.jwtData) {
// 此处根据解密后的jwtData去获取用户详情
const user = await userService.getUserInfo(ctx.jwtData.data.
userId)
const { userId, userName, status, role } = user.dataValues
ctx.result = {
userId, userName, status, role
}
}
return next()
}
/**
* 获取用户列表
* @param {*} ctx
* @param {*} next
* @returns
*/
const getUserList = async (ctx, next) => {
if (ctx.jwtData) {
// 此处根据解密后的jwtData去获取用户详情
const userList = await userService.getUserList()
ctx.result = userList
}
return next()
}
module.exports = {
login,
getUserInfo,
getUserList
}
更新路由
// src/router/index.js
'use strict'
const Router = require('koa-router')
const controllers = require('../controllers')
const jwt = require('../middlewares/jwt')
// -------------公共路由(无需token校验)-----------------------
const publicRouter = new Router()
publicRouter.prefix('/api')
publicRouter.post('/user/login', controllers.userController.login)
// --------------私有路由(需token校验)----------------------
const privateRouter = new Router()
privateRouter.prefix('/api')
privateRouter.use(jwt.verifyToken) // 校验token
privateRouter.get('/userlist', controllers.userController.getUserList)
privateRouter.get('/user/:id', controllers.userController.getUserInfo)
module.exports = {
publicRouter, privateRouter
}