directory
- The use of koa
- Read the KOA source code briefly
- What does CTX mount
- Next built the Onion model
- How to ensure correct execution of middleware with asynchronous code
- Fixed confusion caused by multiple calls to Next
- Handle exceptions based on event drivers
The use of koa
Koa is very simple to use, written after introducing dependencies
const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) = > {
console.log(ctx)
})
app.listen(4000)
Copy the code
Then open http://127.0.0.1:4000 in the browser to access
If body is Not returned, KOA defaults to Not Found
ctx
Expand the code further to see what’s on top of CTX
// ...
console.log(ctx)
console.log('native req ----') // Node native req
console.log(ctx.req.url)
console.log(ctx.request.req.url)
console.log('koa request ----') // KOA encapsulates request
console.log(ctx.url)
console.log(ctx.request.url)
// native req ----
// /
// /
// koa request ----
// /
// /
// ...
Copy the code
The above code stored in the warehouse, self – help.
The KOA website states that CTX has a set of request and Response property aliases.
ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
// ctx.url proxies ctx.request.url
Copy the code
next
The following code is stored in the warehouse, for yourself.
Use next to see what works
const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) = > {
console.log(1)
next()
console.log(2)
})
app.use((ctx, next) = > {
console.log(3)
next()
console.log(4)
})
app.use((ctx, next) = > {
console.log(5)
next()
console.log(6)
})
app.listen(4000)
/ / 1
/ / 3
/ / 5
/ / 6
/ / 4
/ / 2
Copy the code
As you can see from the code print above, next serves as a placeholder. You can view it as the following
app.use((ctx, next) = > {
console.log(1)
app.use((ctx, next) = > {
console.log(3)
app.use((ctx, next) = > {
console.log(5)
next()
console.log(6)})console.log(4)})console.log(2)})Copy the code
This is the Onion model.
What if some middleware has asynchronous code?
const Koa = require('koa')
let app = new Koa()
// Asynchronous functions
const logger = () = > {
return new Promise((resolve, reject) = > {
setTimeout(_= > {
console.log('logger')
resolve()
}, 1000)
})
}
app.use((ctx, next) = > {
console.log(1)
next()
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
await logger()
next()
console.log(4)
})
app.use((ctx, next) = > {
console.log(5)
next()
console.log(6)
})
app.listen(4000)
/ / 1
/ / 3
/ / 2
/ / wait for 1 s
// logger
/ / 5
/ / 6
/ / 4
Copy the code
At this time the print result is not the expected result, we expect 1 -> 3 -> 1S logger -> 5-> 6-> 4 ->2
At this point we need to add an await before next
// ...
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(2)})// ...
Copy the code
Read the KOA source code briefly
Koa aims to be a smaller, more expressive, and more robust Web development framework.
The source code is also very lightweight and easy to read.
Four core documents
application.js
: Simple encapsulationhttp.createServer()
And integratecontext.js
context.js
: Broker and consolidaterequest.js
andresponse.js
request.js
: Based on nativereq
Encapsulated is betterresponse.js
: Based on nativeres
Encapsulated is better
Get started
The following code is stored in the warehouse, as needed.
Koa is implemented in ES6 with two core methods app.listen() and app.use((CTX, next) =< {… })
Implement app.listen() in application.js
const http = require('http')
class Koa {
constructor () {
// ...
}
// Process user requests
handleRequest (req, res) {
// ...} listen (... args) {let server = http.createServer(this.handleRequest.bind(this)) server.listen(... args) } }module.exports = Koa
Copy the code
What does CTX mount
This can be seen from the simple use of CTX above
ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy
Copy the code
We need all of the above objects, which are ultimately proxied to CTX objects.
Create context. Js/request. Js/response. Js three files
Request. The contents of js
const url = require('url')
let request = {}
module.exports = request
Copy the code
The response. Js content
let response = {}
module.exports = response
Copy the code
The context. Js content
let context = {}
module.exports = context
Copy the code
Import the above three files in application.js and place them on the instance
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa extends Emitter{
constructor () {
super(a)// object.create breaks the prototype chain
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
}
Copy the code
Since it cannot be assigned directly with an equal sign, the original variable will be tampered with when modifying its attributes because the object references the same memory space.
So break the dependency with the object.create method, which is equivalent to
function create (parentPrototype) {
function F () {}
F.prototype = parentPrototype
return new F()
}
Copy the code
User requests are then processed and request/Response is brokered on CTX
// Create context
createContext (req, res) {
let ctx = this.context
/ / request
ctx.request = this.request
ctx.req = ctx.request.req = req
/ / response
ctx.response = this.response
ctx.res = ctx.response.res = res
return ctx
}
handleRequest (req, res) {
let ctx = this.createContext(req, res)
return ctx
}
Copy the code
In context.js, the proxy is implemented using __defineGetter__ / __defineSetter__, which is a variant of the Object.defineProperty() method and can be set separately without overwriting the Settings.
let context = {}
// Define the getter
function defineGetter (key, property) {
context.__defineGetter__ (property, function () {
return this[key][property]
})
}
// Define the setters
function defineSetter (key, property) {
context.__defineSetter__ (property, function (val) {
this[key][property] = val
})
}
/ / agent request
defineGetter('request'.'path')
defineGetter('request'.'url')
defineGetter('request'.'query')
/ / agent for the response
defineGetter('response'.'body')
defineSetter('response'.'body')
module.exports = context
Copy the code
In Request.js, encapsulation is implemented using the property accessor provided by ES5
const url = require('url')
let request = {
get url () {
return this.req.url // This is the object called ctx.request
},
get path () {
let { pathname } = url.parse(this.req.url)
return pathname
},
get query () {
let { query } = url.parse(this.req.url, true)
return query
}
/ /... More to be perfected
}
module.exports = request
Copy the code
In Response.js, encapsulation is achieved using the property accessor provided by ES5
let response = {
set body (val) {
this._body = val
},
get body () {
return this._body // This is the called object ctx.response
}
/ /... More to be perfected
}
module.exports = response
Copy the code
The above implementation encapsulates request/ Response and proxies it to CTX
ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy
Copy the code
Next built the Onion model
Use ((CTX, next) =< {… })
Use is used to store middleware, such as cookies, session, static… Wait for a bunch of processing functions, and execute them in onion form.
constructor () {
// ...
// Store the middleware array
this.middlewares = []
}
// Use middleware
use (fn) {
this.middlewares.push(fn)
}
Copy the code
A bunch of registered middleware is expected to execute when processing user requests
// Combine middleware
compose (middlewares, ctx) {
function dispatch (index) {
// The iteration terminating condition completes the middleware
// Then return the promise of success
if (index === middlewares.length) return Promise.resolve()
let middleware = middlewares[index]
// let the first function finish, if there is asynchrony, need to see if there is any await
// Return a promise
return Promise.resolve(middleware(ctx, () = > dispatch(index + 1)))}return dispatch(0)}// Process user requests
handleRequest (req, res) {
let ctx = this.createContext(req, res)
this.compose(this.middlewares, ctx)
return ctx
}
Copy the code
The above dispatch iteration functions are used in many places, such as recursively deleting directories, and are at the heart of KOA.
How to ensure correct execution of middleware with asynchronous code
Promises are returned primarily to handle cases where the middleware contains asynchronous code
After all middleware execution is complete, the page needs to be rendered
// Process user requests
handleRequest (req, res) {
let ctx = this.createContext(req, res)
res.statusCode = 404 // The default 404 will be changed when body is set
let ret = this.compose(this.middlewares, ctx)
ret.then(_= > {
if(! ctx.body) {// Body is not set
res.end(`Not Found`)}else if (ctx.body instanceof Stream) { / / flow
res.setHeader('Content-Type'.'text/html; charset=utf-8')
ctx.body.pipe(res)
} else if (typeof ctx.body === 'object') { / / object
res.setHeader('Content-Type'.'text/josn; charset=utf-8')
res.end(JSON.stringify(ctx.body))
} else { / / string
res.setHeader('Content-Type'.'text/html; charset=utf-8')
res.end(ctx.body)
}
})
return ctx
}
Copy the code
Need to consider multiple situations to do compatibility.
Fixed confusion caused by multiple calls to Next
Run the following tests with the above code
The execution result will be
// 1 => 3 =>1s,logger => 4
// => 3 =>1s,logger => 4 => 2
Copy the code
It doesn’t meet our expectations
Because the execution is as follows
In step 2, the next function is not allowed to be called more than once within a middleware function. For example, the next function is not allowed to be called more than once within a middleware function because the compose index is already 2.
The solution is to use flag as the subscript of the functional middleware that runs in the Onion model. If next is run twice in one middleware, the index will be lower than flag.
/** * Combined middleware *@param {Array<Function>} middlewares
* @param {context} ctx
*/
compose (middlewares, ctx) {
let flag = -1
function dispatch (index) {
// 3) flag Records the running middleware subscripts
// 3.1) If a middleware calls next twice then index will be less than flag
// if (index <= flag) return Promise.reject(new Error('next() called multiple times'))
flag = index
// 2) Iteration termination condition: middleware is finished
// 2.1) then return the promise of success
if (index === middlewares.length) return Promise.resolve()
// 1) let the first function finish, if there is asynchron, need to see if there is any await
// 1.1) must return a promise
let middleware = middlewares[index]
return Promise.resolve(middleware(ctx, () = > dispatch(index + 1)))}return dispatch(0)}Copy the code
Handle exceptions based on event drivers
How do you handle exceptions that occur in middleware?
Node is event-driven, so we simply inherit the Events module
const Emitter = require('events')
class Koa extends Emitter{
// ...
// Process user requests
handleRequest (req, res) {
// ...
let ret = this.compose(this.middlewares, ctx)
ret.then(_= > {
// ...
}).catch(err= > { // Handler exception
this.emit('error', err)
})
return ctx
}
}
Copy the code
Then do catch exception above, use as follows
const Koa = require('./src/index')
let app = new Koa()
app.on('error'.err= > {
console.log(err)
})
Copy the code
The test case code is stored in the repository, as needed.
conclusion
From the above we have implemented a simple KOA, request/ Response.js file needs to be extended to support more attributes.
The complete code and test cases are stored at @careteen/ KOA. If you are interested, go to debug.