文章目录
- 概述
- koa和express对比
- koa下载安装
- 使用
- 1.创建koa项目文件目录
- 2. 创建koa服务
- 3. 添加路由 koa-router
- 4. 数据库服务 mongodb
- 5. 添加请求参数json处理 koa-bodyparser
- 6. 用户接口举例
- 7.引入koa一些常用插件
- 8.用户登录验证 koa-jwt
- 9.webpack生产打包
- 来源
概述
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
koa和express对比
- Koa采用洋葱模型
通常都会说Koa是洋葱模型,这重点在于中间件的设计。但是按照上面的分析,会发现Express也是类似的,不同的是Express中间件机制使用了Callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理Koa的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,
Koa不是立即响应,是整个中间件处理完成在最外层进行了响应,而Express则是立即响应。
- Koa更轻量
koa不提供内置的中间件;
koa不提供路由,而是把路由这个库分离出来了(koa/router)
- Context对象
koa增加了一个Context的对象,作为这次请求的上下文对象(在koa2中作为中间件的第一个参数传入)。同时Context上也挂载了Request和Response两个对象。Express类似, 这两个对象都提供了大量的便捷方法辅助开发这样的话对于在保存一些公有的参 数的话变得更加合情合理。
- 异步流程控制
express采用callback来处理异步,koa采用async/await。
async/await使用同步的写法来处理异步,明显好于callback和promise,
- 中间件模型
express基于connect中间件,线性模型;
koa中间件采用洋葱模型(对于每个中间件,在完成了-些事情后,可以非常优雅的将控制权传递给下一个中间件,并能够等待它完成,当后续的中间件完成处理后,控制权又回到了自己)
同步代码
同步方法没有什么区别:
- 01-express-同步.js
const express = require("express")
const app = express()
app.use((req, res, next) => {
console.log("111111")
next()
console.log("333333")
res.send("hello world")
})
app.use((req, res, next) => {
// 同步操作
console.log("22222")
})
app.listen(3000)
运行输出
111111
22222
333333
- 01-koa-同步 .js
const Koa = require("koa")
const app = new Koa()
app.use((ctx, next) => {
console.log("111111")
next()
console.log("333333")
ctx.body("hello world")
})
app.use((ctx, next) => {
// 同步操作
console.log("22222")
})
app.listen(3000)
运行输出:
111111
22222
333333
异步代码
next()表示可以执行下一个中间件,当下一个中间件执行完成之后,如果上一个中间件没有执行完,再返回上一个中间件继续执行。
- 01-express-异步.js
const express = require("express")
const app = express()
app.use(async (req, res, next) => {
console.log("111111")
await next()
console.log("444444")
res.send("hello world")
})
app.use(async (req, res, next) => {
console.log("22222")
// 异步操作
await delay(1000)
console.log("33333")
})
function delay(time) {
return new Promise((resolve, reject) => {
setTimeout(resolve,time)
})
}
app.listen(3000)
运行输出:
111111
22222
444444
33333
由于next()返回的不是promise对象因此await不起作用,所以输出不会像我们所想输出
- 01-koa-异步.js
const Koa = require("koa")
const app = new Koa()
app.use((ctx, next) => {
console.log("111111")
next()
console.log("444444")
})
app.use((ctx, next) => {
console.log("22222")
// 异步操作
delay(1000)
console.log("33333")
})
function delay(time) {
return new Promise((resolve, reject) => {
setTimeout(resolve,time)
})
}
app.listen(3000)
运行输出:
111111
22222
33333
444444
koa洋葱模型,正常执行。
koa下载安装
npm init
npm i koa
Koa基本框架
const Koa = require("koa")
const app = new Koa()
// ctx=context 相当于res和req的合并
app.use((ctx, next) => {
})
app.listen(3000)
使用
1.创建koa项目文件目录
我们学习的第一步就是先搭好项目目录,这里会有我们项目中使用到的任何东西。
// 创建项目文件夹 也可手动创建
mkdir koa-app
// 进入项目文件
cd koa-app
// 添加koa依赖
// 根据自己的包管理器执行自己的命令 我这里以yarn举例
yarn add koa -S
// 新建入口文件
echo >index.js
// 创建变量管理目录 const
mkdir const
// 创建数据库管理目录 database
mkdir database
// 创建中间件管理目录 middlewares
mkdir middlewares
// 创建路由管理目录 router
mkdir router
// 创建静态资源管理目录 static
mkdir static
// 创建工具类管理目录 utils
mkdir utils
// 创建html文件管理目录 view
// 主要用于测试自己接口
mkdir const
// 创建webpack打包文件
echo >webpack.config.js
这时候我们生成的目录结构大致如下
--koa-app
--const
--database
--middlewares
--router
--static
--utils
--view
-- index.js
-- packjson.js
-- webpack.config.js
2. 创建koa服务
这时候我们就可以创建koa服务,启动后就可以访问服务器目录了。
// index.js
const Koa = require("koa");
const app = new Koa();
app.use(async ctx => {
ctx.body = "hellO 欢迎使用koa"
})
app.listen(3000);
// 启动koa服务
node index.js
// 访问服务
在浏览器地址栏输入 localhost:3000
启动服务后我们打开浏览器就能看到"hellO 欢迎使用koa"说明我们koa程序运行成功。
3. 添加路由 koa-router
我们后台服务已经搭建好了,那下一步必不可少的就是路由管理了,这里我们使用koa-router插件
// /router/index.js
// 引入koa-router
const Router = require('koa-router');
// 引入user路由对象
const user = require('./user/index.js');
const view = require('./view/index.js')
const goods = require('./goods/index.js');
const category = require('./category/index.js');
const upload = require('./upload/index.js')
const rule = require('./rule/index.js')
const menu = require('./menu/index.js')
const role = require('./role/index.js')
const managerUser = require('./managerUser/index.js')
const attribute = require('./attribute/index.js')
// 生成新的router对象
let router = new Router();
// 添加路由管理
router.use('/api/user', user.routes())
router.use('/view', view.routes())
router.use('/api/goods', goods.routes())
router.use('/api/category', category.routes())
router.use('/api/upload', upload.routes())
router.use('/api/rule', rule.routes())
router.use('/api/menu', menu.routes())
router.use('/api/role', role.routes())
router.use('/api/managerUser', managerUser.routes())
router.use('/api/attribute', attribute.routes())
// 导出路由
module.exports = router
这里我是以自己写好的项目文件直接复制了,如果是测试的话不需要导入那么多路由对象,导入一个自己已经写好的就行了。
接下来我们就需要修改index.js文件与编写user路由
// /router/user/index.js
router.get('/list', async (ctx) => {
ctx.body = {
code: 200,
message: '访问成功'
}
})
// index.js
const Koa = require("koa");
const router = require("./router/index.js"); // 路由
const app = new Koa();
// 添加路由中间件
app.use(router.routes()).use(router.allowedMethods());
app.use(async ctx => {
ctx.body = "hellO 欢迎使用koa"
})
app.listen(3000);
这时候我们重新启动koa服务后,访问localhost/3000/api/user/list 就能获取ctx.body的内容了。 做到这里我们已经实现了自己的第一个接口了。剩下就是去数据库里面获取数据就形成了后台数据服务了。 是不是很棒呢!
4. 数据库服务 mongodb
这里因为学习的是mongodb数据库,所以例子都会是以mongodb数据库为例。其实用mysql的同学也可以自己去看一下mysql的引。
数据库的引入主要是做了2个步骤, 第一连接数据库,第二创建数据model对象,并执行数据库操作。 mongodb使用的是mdb语句去做的查询,mysql则是使用的sql语句。
当然每个数据库特性都不一样,在什么项目中使用什么数据库都需要在搭建项目目录的时候考虑到的,比如mysql, oracle 都是关系型的,在做一些数据关联性强的一些网站上更加适用比如电商,金融,证券,医疗等。 而非关系型的mongodb数据因为数据结构更加多变,适用与一些日记管理,博客,官网等
话不多说,我们来创建我们的数据库服务吧
添加依赖
// 添加依赖
yarn add glob mongoose -S
创建mongosse文件
// 添加mongoose文件 /database/index.js
// 添加mongosse
const mongoose = require('mongoose')
// 数据库访问地址
const db = "mongodb://127.0.0.1/waimai"
// glob :提供匹配文件路径的方法,可以快速地找 到需要读取的文件
const glob = require('glob');
const { resolve } = require('path')
// 初始化文档模式
exports.initSchemas = async () => {
await glob.sync(resolve(__dirname, './schema', './*.js')).forEach((v) => {
require(v)
})
}
exports.connect = () => {
// 连接数据库
mongoose.connect(db)
return new Promise((resolve, reject) => {
// 添加数据库断开监听事件
mongoose.connection.on('disconnected', () => {
console.log('数据库断开---------------')
mongoose.connect(db)
})
// 添加数据库启动监听事件
mongoose.connection.on('open', () => {
console.log('数据库连接---------------1')
mongoose.connect(db)
resolve();
})
})
}
// index.js 引入mongoose文件
// moogose初始化
const { connect, initSchemas } = require("./database/index");
(async () => {
await connect();
await initSchemas();
})();
我们重启服务后就能连接到mongodb数据库了, 在conosle里面我们能看到 数据库连接字样
5. 添加请求参数json处理 koa-bodyparser
添加新依赖
yarn add koa-bodyparser -D
更新index.js
const bodyParser = require("koa-bodyparser"); // requeast请求
app.use(bodyParser());
6. 用户接口举例
添加新依赖
yarn add bcrypt -D
创建mongoose.model模型
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let ObjectId = Schema.Types.ObjectId;
// const bcrypt = require('bcrypt');
const SALT_WORK_FACTOR = 10;
// 前台用户表接口
const userSchema = new Schema({
UserId: ObjectId,
userName: {
unique: true,
type: String
},
passWord: String,
avator: String,
hashPassword: String,
nikeName: String,
address: String,
isBlack: Boolean,
sex: String,
createAt: {
type: Date,
default: Date.now(),
},
lastLoginAt: {
type: Date,
default: Date.now(),
},
})
// 每次存储时都要执行,加盐加密
userSchema.pre('save', function (next){
bcrypt.genSalt(SALT_WORK_FACTOR,(err,salt)=>{
if(err) return next(err)
bcrypt.hash(this.passWord,salt,(err,hash)=>{
if(err) return next(err)
this.hashPassword = hash
next()
})
})
})
// 添加自定义方法
userSchema.methods = {
// 对比密码一致性
comparePassword: (_password, hashPassword) => {
return new Promise((resolve, reject) => {
// 对比密码方法
bcrypt.compare(_password, hashPassword, (err, isMatch) => {
if(!err) resolve(isMatch);
reject(err)
})
})
}
}
// 发布模型
module.exports = mongoose.model('User', userSchema)
添加用户接口
const Router = require('koa-router');
const mongoose = require('mongoose')
let router = new Router();
const User = require('../../database/schema/User')
// 用户注册
router.post('/register', async (ctx) => {
const userName = ctx.request.body.userName;
const passWord = ctx.request.body.passWord;
let newUser = new User({
userName,
passWord,
})
.save()
.then((res) => {
ctx.body = {
code: 200,
message: "添加用户成功",
};
})
.catch((err) => {
ctx.body = {
code: 500,
message: "添加失败" + err,
};
});;
})
router.get('/list', async (ctx) => {
// 引入user模型
// const User = mongoose.model('User');
const uid = ctx.request.query.uid || '';
// 分页 page
let page = ctx.request.body.page || 1;
// 分页每页数量
let limit = ctx.request.body.limit || 8;
// 上一次获取位置
const start =(page - 1)*limit;
// console.log( userName, User, 'User')
const result = await User.find().exec()
console.log(result, 'result')
ctx.body = {
code: 200,
data: result.slice((page-1)*limit, page*limit),
page: {
page: page,
limit,
total: result.length ,
lastPage: parseInt(result.length / limit)
}
}
// ctx.body = ctx.request.body;
})
module.exports = router;
这时候我们就已经写好了用户添加接口与用户列表接口。因为用户的密码需要保密,我们在这里用了bcrypt去做了加盐加密,考虑到了bcrypt的加密是不可逆的所以我们这里用了passWord对原密码做了保存。
这里我们使用了schema的自定义方法与 schema的钩子函数
- userSchema.methods
向由该 schema 编译的 model 构造的 document 添加一个实例方法.
- userSchema.pre
给 schema 定义一个前置钩子 (pre hook)
7.引入koa一些常用插件
处理跨域问题 koa2-cors
yarn add koa2-cors -D
// index,js
const koa2cors = require("koa2-cors"); // 配置跨域
app.use(koa2cors({
origin: "*",
maxAge: 5,
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));
添加静态文件目录 koa-static
yarn add koa-static -D
// index.js
const koaStatic = require("koa-static"); // 静态目录
app.use(koaStatic("./"));
添加websokit服务 koa-websocket
yanr add koa-websocket -S
// index.js
const websocket = require("koa-websocket"); // socket
// 这时候app需用被websoket包裹
const app = websocket(new Koa());
// 建立socket连接
app.ws.use(async (ctx) => {
// the websocket is added to the context as `ctx.websocket`.
ctx.websocket.send("我是服务器");
ctx.websocket.on("message", function (message) {
// do something
const msg = message.toString("utf-8");
console.log("客户端发来消息", msg);
});
});
添加xss防御
yarn add xss -S
// index.js
const xss = require('./middlewares/xss.js') // xss
app.use(xss())
// /middlewares/xss.js
const xss = require("xss"); // 需要 npm install xss -S
const xssHandler = () => {
return async (ctx, next) => {
try {
const body = ctx.request.body;
for (const key in body) {
if (typeof body[key] === "string") {
body[key] = xss(body[key]);
}
}
// 一定要添加await
await next();
} catch (error) {
// console.error(error)
throw error;
}
};
};
module.exports = xssHandler;
图片文件处理 koa-multer
yarn add koa-multer -D
// /router/upload/index.js
const Router = require('koa-router');
let router = new Router();
const multer = require('koa-multer');
//配置
const storage = multer.diskStorage({
//配置图片上传的目录
destination: function (req, file, cb) {
console.log('destination')
cb(null, 'static/images/'); //注意路径必须存在
},
//图片上传完成重命名
filename: function (req, file, cb) {
console.log('filename')
// 获取后缀名
var fileFormat = file.originalname.split('.');
cb(null, Date.now() + '.' + fileFormat[fileFormat.length - 1]);
},
});
const upload = multer({ storage: storage });
router.post('/img', upload.single('file'), async ctx => {
console.log(ctx.req.file, 'ctx.req.file')
ctx.body = {
code: 200,
data: {
filename: ctx.req.file.filename,//返回文件名
path: ctx.req.file.destination + ctx.req.file.filename
}
}
})
module.exports = router;
请求参数验证 Joi
访问接口时会先校验参数是否传对,如果对继续后面的逻辑,如果参数校验不对则会直接返回错误信息给前端。
yarn add Joi -D
// /router/user/index.js
const Joi = require("joi");
const validateSchemaJoi = require("../../middlewares/validateSchemaJoi");
const userSchema = Joi.object({
userName: Joi.string().min(1).required(),
});
// 用户注册
router.post('/register', validateSchemaJoi("post", userSchema), async (ctx) => {
// 注册流程
})
// /middlewares/validateSchemaJoi
function validateSchemaJoi(method, schema) {
async function validateSchema (ctx, next) {
let data = undefined;
if (method === 'get') {
data = ctx.request.query;
} else {
data = ctx.request.body;
}
const { value, error } = schema.validate(data);
if (error) {
ctx.body = {
code: 400,
error
};
} else {
next();
}
}
return validateSchema;
}
module.exports = validateSchemaJoi;
8.用户登录验证 koa-jwt
用户验证有3种方式
1。cookie 2. session 3. token
这里我们就以token来做用户验证。
添加依赖
yarn add koa-jwt jsonwebtoken -S
用户登录添加token返回
// /router/user/index.js
var jwt = require('jsonwebtoken');
router.post('/login', async (ctx) => {
const userName = ctx.request.body.userName;
const passWord = ctx.request.body.passWord;
// 查询用户是否存在
await User.findOne({ userName: userName }).exec().then(async result => {
// 如果用户名存在
if(result) {
let newUser = new User();
// 校验用户密码
await newUser.comparePassword(passWord, result.hashPassword).then(isMatch => {
// 如果用户校验成功
if(isMatch) {
// 生成token
const token = jwt.sign({userName: result.userName}, 'secret', { expiresIn: '2h' });
// 返回给前端
ctx.body = {
code: 200,
message: isMatch,
data: {
token: token,
uid: result._id
}
}
}else {
ctx.body = {
code: 500,
message: isMatch
}
}
})
}else {
ctx.body = {
code: 500,
message: '用户名不存在!'
}
}
}).catch(err => {
// console.log('result----err')
ctx.body = {
code: 500,
message: err,
}
})
})
添加token白名单 不拦截请求
const { jwtWhiteList } = require("./const/jwtWhiteList"); // token白名单
// /const/jwtWhiteList.js
const jwtWhiteList = [
/^\/api\/user\/login/,
/^\/view/,
/^\/static/,
"/api/managerUser/login",
"/api/goods/getGoodsDetailsInfo",
"/api/upload/img"
]
module.exports = {
jwtWhiteList
}
添加路由token验证拦截
token路由拦截主要做了以下这几件事
1.koa-jwt对每个请求头部信息进行token校验,如果用户校验失败就返回401,过滤掉白名单的请求。
2.在路由中间件上面添加中间件,当用户token失效后我们就会走401步骤 返回用户token失效信息,让前端去重定向到登录页
3.用户token都是有时效性的,当然时效性越短越好,因为没用数据库去存储token所以在项目重启后可能会有失效问题,没验证过。我这默认是2小时,当小于一半的失效时间时我就会生成新的token交予前端重新生成。也就是所谓的token续存机制。
// index.js
const jwt = require("koa-jwt"); // token验证
const jwtToken = require('jsonwebtoken');
// 路由拦截器中间件
app.use(function (ctx, next) {
// console.log("ce0", ctx.header.authorization)
if (ctx.header && ctx.header.authorization) {
const parts = ctx.header.authorization.split(" ");
if (parts.length === 2) {
//取出token
const scheme = parts[0];
const token = parts[1];
if (/^Bearer$/i.test(scheme)) {
try {
const decoded = jwtToken.verify(token, 'secret',{ complete: true });
// iat: 签发时间 exp: 过期时间
const { iat, exp, userName } = decoded.payload;
const nowTime = new Date().getTime()/1000;
const lastTime = (exp - nowTime)/60;
// 当前事件离过期时间还剩一半的时候更新token 如果过期就走401
if(decoded && 0 < lastTime && lastTime< ((exp-iat)/60)/2) {
// console.log('更新token0')
const newToken = jwtToken.sign({userName: userName}, 'secret', { expiresIn: '2h' });
// console.log('更新token1', newToken)
ctx.res.setHeader('Authorization', newToken)
}
} catch (error) {
console.log("ce3")
//token过期
}
}
}
}
return next().catch((err) => {
if (401 == err.status || err.status === 301) {
ctx.status = 401;
ctx.body = {
code: err.status,
message: "token已经失效!!!!"
};
// ctx.body = {error: err.originalError ? err.originalError.message : err.message};
} else {
throw err;
}
});
});
// 添加token中间件
app.use(jwt({ secret: "secret" }).unless({ path: jwtWhiteList }));
9.webpack生产打包
这里就做了简单的js打包,打包后的文件体积会变小,因为webpack设置mode为生产环境后默认就做了许多处理。
// webpack.config.js
const webpack = require("webpack");
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const nodeExternals = require("webpack-node-externals");
// const MinifyPlugin = require('babel-minify-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
entry: "./index.js",
mode: "production",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[name].js",
},
target: "node",
externals: [nodeExternals()], //node 打包可去除一些警告
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"], //兼容es6,并添加.babelrc
},
},
],
},
],
},
plugins: [
// 清楚dist
new CleanWebpackPlugin(),
// js压缩
// split切片
// 复制静态目录
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, './static'),
to: path.resolve(__dirname, './dist/static')
}
]
})
// new MinifyPlugin() //压缩js
],
};
// packjson.js 添加启动指令
"build": "webpack --progress --config webpack.config.js",
"prd_server": "node ./dist/main.js"
来源
你需要的koa入门教学
koa框架