What is Koa2
The new generation Node.js web framework created by the original team of Express, its code is very simple, it does not provide routing, static services, etc. like Express, it is to solve the Node problem (simplify the operation in Node) and replace it , which itself is a simple middleware framework that needs to be used with each middleware
Chinese document (wild)
Simplest Koa server
const Koa = require('koa')
const app = new Koa()
app.use((ctx) => {
ctx.body = 'Hello World'
})
app.listen(3000, () => {
console.log('3000端口已启动')
})
onion model
This is Koa's onion model
Take a look at what Express middleware looks like:
The request (Request) runs through each middleware directly in turn, and finally returns the response (Response) through the request processing function. Let's take a look at what Koa's middleware looks like:
It can be seen that the Koa middleware does not complete its mission after the request is passed like the Express middleware; on the contrary, the execution of the middleware is clearly divided into two stages. Let's see what Koa middleware looks like
Definition of Koa Middleware
Koa's middleware is a function like this:
async function middleware(ctx, next) {
// 先做什么
await next()
// 后做什么
}
The first parameter is the Koa Context, which is the content passed by the green arrow running through the middleware and the request processing function in the above figure, which encapsulates the request body and the response body (in fact, there are other attributes), which can be passed through ctx.request
and ctx.response
to get, the following are some commonly used attributes:
ctx.url // 相当于 ctx.request.url
ctx.body // 相当于 ctx.response.boby
ctx.status // 相当于 ctx.response.status
For more Context properties, please refer to the Context API documentation
The second parameter of the middleware is next
function: used to transfer control to the next middleware. But the essential difference between it and Express's next
function is that Koa's next
function returns a Promise . After the Promise enters the fulfilled state (Fulfilled), it will go to the middle of the execution. The code for the second stage in the file.
What are the common middleware
Routing middleware - koa-router or @koa/router
Download npm package
npm install koa-router --save
Some tutorials use @koa/router
, and now these two libraries are maintained by the same person and the code is the same. i.e. koa-router === @koa/router (Written as of Aug 23, 2021)
NPM package address: koa-router , @koa/router
how to use
Create the controllers directory in the root directory to store the code related to the controller. The first is HomeController, create controllers/home.js, the code is as follows:
class HomeController {
static home(ctx) {
ctx.body = 'hello world'
}
static async login(ctx) {
ctx.body = 'Login Controller'
}
static async register(ctx) {
ctx.body = 'Register Controller'
}
}
module.exports = HomeController;
implement routing
Then create the routes folder to mount the controller to the corresponding route and create home.js
const Router = require('koa-router')
const { home, login, register } = require('../controllers/home')
const router = new Router()
router.get('/', home)
router.post('/login', login)
router.post('/register', register)
module.exports = router
register route
Create index.js in routes, and put all routes into routes in the future. The purpose of creating index.js is to make the structure more tidy. index.js is responsible for the registration of all routes, and its sibling files are responsible for their own routes.
const fs = require('fs')
module.exports = (app) => {
fs.readdirSync(__dirname).forEach((file) => {
if (file === 'index.js') {
return
}
const route = require(`./${file}`)
app.use(route.routes()).use(route.allowedMethods())
})
}
Note: the role of allowedMethods
- Responds to the option method, telling it which request methods it supports
- 405 (not allowed) and 501 (not implemented) are returned accordingly
Note: It can be seen that the usage of
@koa/router
is basically the same as that of Express Router.
import route
Finally, we need to register the router as middleware, create a new index.js
, and write the code as follows:
const Koa = require('koa')
const routing = require('./routes')
// 初始化 Koa 应用实例
consr app = new Koa()
// 注册中间件
// 相应用户请求
routing(app)
// 运行服务器
app.listen(3000);
Test it with postman
other middleware
- koa-bodyparser - request body parsing
- koa-static - serve static resources
- @koa/cors - cross domain
- koa-json-error - handle errors
- koa-parameter - parameter verification
cnpm i koa-bodyparser -S
cnpm i koa-static -S
cnpm i @koa/cors -S
cnpm i koa-json-error -S
cnpm i koa-parameter -S
const path = require('path')
const Koa = require('koa')
const bobyParser = require('koa-bodyparser')
const koaStatic = require('koa-static')
const cors = require('@koa/cors')
const error = require('koa-json-error')
const parameter = require('koa-parameter')
const routing = require('./routes')
const app = new Koa()
app.use(
error({
postFormat: (e, { stack, ...rest }) =>
process.env.NODE_ENV === 'production' ? rest : { stack, ...rest },
}),
)
app.use(bobyParser())
app.use(koaStatic(path.join(__dirname, 'public')))
app.use(cors())
app.use(parameter(app))
routing(app)
app.listen(3000, () => {
console.log('3000端口已启动')
})
Implement JWT authentication
JSON Web Token (JWT) is a popular RESTful API authentication scheme
Install the relevant npm packages first
cnpm install koa-jwt jsonwebtoken -S
Create config/index.js
to store the JWT Secret constant, the code is as follows:
const JWT_SECRET = 'secret'
module.exports = {
JWT_SECRET,
}
Some routes we want only logged in users to have access to (protected routes), while others are accessible to all requests (unprotected routes). In Koa's onion model, we can do it like this:
It can be seen that all requests can directly access unprotected routes, but protected routes are placed behind the JWT middleware, we need to create a few more files to do JWT experiments
We know that the so-called user (users) is the most common route that requires authentication, so we now create user.js in controllers and write the following code:
class UserController {
static async create(ctx) {
ctx.status = 200
ctx.body = 'create'
}
static async find(ctx) {
ctx.status = 200
ctx.body = 'find'
}
static async findById(ctx) {
ctx.status = 200
ctx.body = 'findById'
}
static async update(ctx) {
ctx.status = 200
ctx.body = 'update'
}
static async delete(ctx) {
ctx.status = 200
ctx.body = 'delete'
}
}
module.exports = UserController
Register JWT middleware
The user's additions, deletions, changes and checks are arranged, and the semantics are obvious. Next, we create user.js in the routes file, and here is the code related to the users route:
const Router = require('koa-router')
const jwt = require('koa-jwt')
const {
create,
find,
findById,
update,
delete: del,
} = require('../controllers/user')
const router = new Router({ prefix: '/users' })
const { JWT_SECRET } = require('../config/')
const auth = jwt({ JWT_SECRET })
router.post('/', create)
router.get('/', find)
router.get('/:id', findById)
router.put('/:id', auth, update)
router.delete('/:id', auth, del)
module.exports = router
In summary, the home.js under the routes file does not need the protection of JWT middleware, and the update and deletion in user.js need the protection of JWT
Test it, you can see that the JWT has worked
So far, we have completed the verification of JWT, but the premise of verification is to issue JWT first, how to issue it, I will give you a signed token when you log in, and bring the token in the request header when you want to update/delete , I was able to verify...
Login is involved here. Let's pause for a while and supplement the knowledge of the database to make the project more complete.
Mongoose joins the battle
If you want to do a complete project, the database is essential. The NoSql database that matches Node is better, which is represented by Mongodb. Of course, if we want to use this database, we need to follow the corresponding library, and this library is mongoose
download mongoose
cnpm i mongoose -S
Connect and configure
Add the connectionStr variable in config/index.js
to represent the database address of the mongoose connection
const JWT_SECRET = 'secret'
const connectionStr = 'mongodb://127.0.0.1:27017/basic'
module.exports = {
JWT_SECRET,
connectionStr,
}
CREATE db/index.js
const mongoose = require('mongoose')
const { connectionStr } = require('../config/')
module.exports = {
connect: () => {
mongoose.connect(connectionStr, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
mongoose.connection.on('error', (err) => {
console.log(err)
})
mongoose.connection.on('open', () => {
console.log('Mongoose连接成功')
})
},
}
Enter the main file index.js
, modify the configuration and start
...
const db = require('./db/')
...
db.connect()
Start the service npm run serve
, that is nodemon index.js
, you can see that mongoose has been connected successfully
Create a data model definition
Create the models
directory in the root directory to store the data model definition file, and create User.js
in it to represent the user model. The code is as follows:
const mongoose = require('mongoose')
const schema = new mongoose.Schema({
username: { type: String },
password: { type: String },
})
module.exports = mongoose.model('User', schema)
For details, you can look at the article Mongoose . Here we look at the behavior. The above code indicates that a data object is created for the operator to operate the database.
Operate the database in the Controller
Then you can add, delete, modify, and query data in the Controller. First we open constrollers/user.js
const User = require('../models/User')
class UserController {
static async create(ctx) {
const { username, password } = ctx.request.body
const model = await User.create({ username, password })
ctx.status = 200
ctx.body = model
}
static async find(ctx) {
const model = await User.find()
ctx.status = 200
ctx.body = model
}
static async findById(ctx) {
const model = await User.findById(ctx.params.id)
ctx.status = 200
ctx.body = model
}
static async update(ctx) {
const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
ctx.status = 200
ctx.body = model
}
static async delete(ctx) {
await User.findByIdAndDelete(ctx.params.id)
ctx.status = 204
}
}
module.exports = UserController
In the above code,
- User.create({xxx}): Create a data in the User table
- User.find(): View all the data in the User table
- User.findById(id): View one of the User tables
- User.findByIdAndUpdate(id, body): Update one of the data in the User table
- User.findByIdAndDelete(id): delete one of the data in the User table
The above is the addition, deletion and modification of the database
With salt
For this, we need to encrypt the password, without it, it is safe.
When you check into the database, you can see the password, which means that the data is open to the developer, and the developer can do anything with the user's account password, which is not allowed.
Download npm package - bcrypt
cnpm i bcrypt --save
Let's go to models/User.js
and transform it
...
const schema = new mongoose.Schema({
username: { type: String },
password: {
type: String,
select: false,
set(val) {
return require('bcrypt').hashSync(val, 10)
},
},
})
...
Add select: false is invisible, set(val) encrypts the value, let's test it
It can be seen that the password is encrypted. Even in the database, the user's password cannot be seen. Does the password entered by the user enter such a string of passwords? Obviously not, if the user enters it, we have to verify it. For example, we login
We enter the constrollers/home
file and transform it,
...
class HomeController {
static async login(ctx) {
const { username, password } = ctx.request.body
const user = await User.findOne({ username }).select('+password')
const isValid = require('bcrypt').compareSync(password, user.password)
ctx.status = 200
ctx.body = isValid
}
...
}
- User.findOne({ username }) can find data without password, because we artificially set select to false. If you want to see it, add select('+password')
- require('bcrypt').compareSync(password, user.password) Authenticate the plaintext password entered by the user with the encrypted password in the database, true is correct, false is incorrect password
Back to JWT
Issue JWT Token in Login
We need to provide an API port so that users can get the JWT Token. The most suitable one is of course the login interface /login
, open controllers/home.js
, and implement it in the login
controller The logic of issuing JWT Token, the code is as follows:
const jwt = require('jsonwebtoken')
const User = require('../models/User')
const { JWT_SECRET } = require('../config/')
class HomeController {
static async login(ctx) {
const { username, password } = ctx.request.body
// 1.根据用户名找用户
const user = await User.findOne({ username }).select('+password')
if (!user) {
ctx.status = 422
ctx.body = { message: '用户名不存在' }
}
// 2.校验密码
const isValid = require('bcrypt').compareSync(password, user.password)
if (isValid) {
const token = jwt.sign({ id: user._id }, JWT_SECRET)
ctx.status = 200
ctx.body = token
} else {
ctx.status = 401
ctx.body = { message: '密码错误' }
}
}
...
}
In login
, we first query the corresponding user according to the user name (the name
field in the request body), if the user does not exist, return 401 directly; if it exists, then pass (bcrypt').compareSync
To verify the plaintext password in the request body password
Whether it is consistent with the encrypted password stored in the database, if it is consistent, pass jwt.sign
the Token, if it is inconsistent, still 401 is returned.
Add access control in User controller
After the middleware and issuance of the Token are completed, the final step is to verify the user's Token in a suitable place to confirm whether it has sufficient permissions. The most typical scenario is that when updating or deleting a user, we want to make sure that the user himself is operating . Open controllers/user.js
const User = require('../models/User')
class UserController {
...
static async update(ctx) {
const userId = ctx.params.id
if (userId !== ctx.state.user.id) {
ctx.status = 403
ctx.body = {
message: '无权进行此操作',
}
return
}
const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
ctx.status = 200
ctx.body = model
}
static async delete(ctx) {
const userId = ctx.params.id
if (userId !== ctx.state.user.id) {
ctx.status = 403
ctx.body = { message: '无权进行此操作' }
return
}
await User.findByIdAndDelete(ctx.params.id)
ctx.status = 204
}
}
module.exports = UserController
Add some users and log in, add the Token to the request header, use DELETE to delete the user, you can see that the status code becomes 204, the deletion is successful
Assertion Handling
When logging in, updating user information, and deleting users, we need if else to judge. This looks stupid. If we can handle it with assertions, the code will look much more elegant. At this time http-assert
came out
// constrollers/home.js
...
const assert = require('http-assert')
class HomeController {
static async login(ctx) {
const { username, password } = ctx.request.body
// 1.根据用户名找用户
const user = await User.findOne({ username }).select('+password')
// if (!user) {
// ctx.status = 401
// ctx.body = { message: '用户名不存在' }
// }
assert(user, 422, '用户不存在')
// 2.校验密码
const isValid = require('bcrypt').compareSync(password, user.password)
assert(isValid, 422, '密码错误')
const token = jwt.sign({ id: user._id }, JWT_SECRET)
ctx.body = { token }
}
...
}
Similarly, process controllers/user
...
static async update(ctx) {
const userId = ctx.params.id
assert(userId === ctx.state.user.id, 403, '无权进行此操作')
const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
ctx.status = 200
ctx.body = model
}
static async delete(ctx) {
const userId = ctx.params.id
assert(userId === ctx.state.user.id, 403, '无权进行此操作')
await User.findByIdAndDelete(ctx.params.id)
ctx.status = 204
}
...
The code looks clean and fresh
parameter verification
We added a middleware before -- koa-parameter
, we just registered this middleware, but it is not used, we need to judge that the data type of username and password is String type and required when creating a user, Enter controllers/user.js
add the code as follows:
...
class UserController {
static async createUser(ctx) {
ctx.verifyParams({
username: { type: 'string', required: true },
password: { type: 'string', required: true },
})
const { username, password } = ctx.request.body
const model = await User.create({ username, password })
ctx.status = 200
ctx.body = model
}
...
}
Github address: koa-basic
References
Time for a cup of tea, get started with Koa2 + MySQL development
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。