Author: MarkLin
Learning Objectives:
- Native Node encapsulation
- The middleware
- routing
Koa principle
The code for a nodeJS entry level HTTP service is as follows,
// index.js
const http = require('http')
const server = http.createServer((req, res) = > {
res.writeHead(200)
res.end('hello nodejs')
})
server.listen(3000, () = > {console.log('server started at port 3000')})Copy the code
Koa’s goal is to implement callbacks in a more simplified, streamlined, and modular way, and we want to follow koA’s lead in implementing code as follows:
// index.js
const Moa = require('./moa')
const app = new Moa()
app.use((req, res) = > {
res.writeHeader(200)
res.end('hello, Moa')
})
app.listen(3000, () = > {console.log('server started at port 3000')})Copy the code
So we need to create a moa.js file. The main content of this file is to create a class moa, mainly contains use() and listen() methods
/ / create earth-sized. Js
const http = require('http')
class Moa {
use(callback) {
this.callback = callback } listen(... args) {const server = http.createServer((req, res) = > {
this.callback(req, res) }) server.listen(... args) } }module.exports = Moa
Copy the code
Context
To simplify the API, KOA introduces the concept of context, encapsulates and mounts the original request object REq and response object RES onto the context, and sets getters and setters to simplify operations
// index.js
// ...
// app.use((req, res) => {
// res.writeHeader(200)
// res.end('hello, Moa')
// })
app.use(ctx= > {
ctx.body = 'cool moa'
})
// ...
Copy the code
To achieve the effect of the above code, we need to separate three classes, namely context, request, response, and create the above three JS files respectively.
// request.js
module.exports = {
get url() {
return this.req.url
}
get method() {
return this.req.method.toLowerCase()
}
}
// response.js
module.exports = {
get body() {
return this._body
}
set body(val) = {
this._body = val
}
}
// context.js
module.exports = {
get url() {
return this.request.url
}
get body() = {
return this.response.body
}
set body(val) {
this.response.body = val
}
get method() {
return this.request.method
}
}
Copy the code
Next we need to add a createContext(req, res) method to the Moa class and mount it in the appropriate place in listen() :
// moa.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class Moa {
// ...listen(... args) {const server = http.createServer((req, res) = > {
// Create context
const ctx = this.createContext(req, res)
this.callback(ctx)
/ / responseres.end(ctx.body) }) server.listen(... args) } createContext(req, res) {const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
}
}
Copy the code
The middleware
Koa Intermediate key mechanism: The Koa middleware mechanism is the concept of function composition, which combines a set of functions that need to be executed sequentially into a single function. The arguments of the outer function are actually the return values of the inner function. The onion ring model is a visual representation of this mechanism, which is the essence and difficulty of the Koa source code.
Combination of synchronization functions
Suppose there are three synchronization functions:
// compose_test.js
function fn1() {
console.log('fn1')
console.log('fn1 end')}function fn2() {
console.log('fn2')
console.log('fn2 end')}function fn3() {
console.log('fn3')
console.log('fn3 end')}Copy the code
If we want to combine three functions into a single function and execute them in order, we usually do something like this:
// compose_test.js
// ...
fn3(fn2(fn1()))
Copy the code
Execute node compose_test.js and the following output is displayed:
fn1
fn1 end
fn2
fn2 end
fn3
fn3 end
Copy the code
This should not be called a compose of functions. Instead, we should expect a compose() method to compose the functions for us, and write the code as follows:
// compose_test.js
// ...
const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code
Let’s implement the compose() function,
// compose_test.js
// ...
const compose = (middlewares) = > () => {
[first, ...others] = middlewares
let ret = first()
others.forEach(fn= > {
ret = fn(ret)
})
return ret
}
const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code
As you can see, we end up with the desired output:
fn1
fn1 end
fn2
fn2 end
fn3
fn3 end
Copy the code
Asynchronous function combination
Now that we know about synchronous function combinations, our actual scenarios in middleware are actually asynchronous, so let’s look at how asynchronous function combinations work. First, let’s modify the synchronous functions to make them asynchronous.
// compose_test.js
async function fn1(next) {
console.log('fn1')
next && await next()
console.log('fn1 end')}async function fn2(next) {
console.log('fn2')
next && await next()
console.log('fn2 end')}async function fn3(next) {
console.log('fn3')
next && await next()
console.log('fn3 end')}/ /...
Copy the code
Now we expect the output to look like this:
fn1
fn2
fn3
fn3 end
fn2 end
fn1 end
Copy the code
And we don’t want to change the way we write code,
// compose_test.js
// ...
const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code
So we just need to modify the compose() function to support asynchronous functions:
// compose_test.js
// ...
function compose(middlewares) {
return function () {
return dispatch(0)
function dispatch(i) {
let fn = middlewares[i]
if(! fn) {return Promise.resolve()
}
return Promise.resolve(
fn(function next() {
return dispatch(i + 1)}))}}const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code
Running results:
fn1
fn2
fn3
fn3 end
fn2 end
fn1 end
Copy the code
Perfect!!
Perfect earth-sized
We’ll port the asynchronous compositing code directly to moa.js, but since koA also requires the CTX field, we’ll need to modify the compose() method a bit:
// moa.js
// ...
class Moa {
// ...
compose(middlewares) {
return function (ctx) {
return dispatch(0)
function dispatch(i) {
let fn = middlewares[i]
if(! fn) {return Promise.resolve()
}
return Promise.resolve(
fn(ctx, function () {
return dispatch(i + 1)})}}}}Copy the code
After implementing the compose() method we continue to refine our code by first adding a middlewares to the class construction, which records all the functions that need to be composed, and then recording every callback we call in the use() method. Save it in Middlewares and then call it in the right place:
// moa.js
// ...
class Moa {
constructor() {
this.middlewares = []
}
use(middleware) {
this.middlewares.push(middleware) } listen(... args) {const server = http.createServer(async (req, res) => {
// Create context
const ctx = this.createContext(req, res)
const fn = this.compose(this.middlewares)
await fn(ctx)
/ / responseres.end(ctx.body) }) server.listen(... args) }// ...
}
Copy the code
Let’s add a little code to test it:
// index.js
/ /...
const delay = (a)= > new Promise(resolve= > setTimeout((a)= > resolve()
, 2000))
app.use(async (ctx, next) => {
ctx.body = "1"
await next()
ctx.body += "5"
})
app.use(async (ctx, next) => {
ctx.body += "2"
await delay()
await next()
ctx.body += "4"
})
app.use(async (ctx, next) => {
ctx.body += "3"
})
Copy the code
After running the node index.js command to start the server, we go to the page localhost:3000 to check and find the page 12345!
At this point, our simplified version of Koa has been implemented. Let’s celebrate first!!
Router
Koa also has a very important routing function, feeling that the lack of routing would lack its integrity, so let’s briefly introduce how to implement the routing function.
In fact, the principle of routing is to call the corresponding function according to the address and method, its core is to use a table to record the registered routes and methods, the schematic diagram is as follows:
The usage is as follows:
// index.js
// ...
const Router = require('./router')
const router = new Router()
router.get('/'.async ctx => { ctx.body = 'index page' })
router.get('/home'.async ctx => { ctx.body = 'home page' })
router.post('/'.async ctx => { ctx.body = 'post index' })
app.use(router.routes())
// ...
Copy the code
Create router. Js file in the root directory, and then implement the following code according to the routing principle:
// router.js
class Router {
constructor() {
this.stacks = []
}
register(path, method, middleware) {
this.stacks.push({
path, method, middleware
})
}
get(path, middleware) {
this.register(path, 'get', middleware)
}
post(path, middleware) {
this.register(path, 'post', middleware)
}
routes() {
return async (ctx, next) => {
let url = ctx.url === '/index' ? '/' : ctx.url
let method = ctx.method
let route
for (let i = 0; i < this.stacks.length; i++) {
let item = this.stacks[i]
if (item.path === url && item.method === method) {
route = item.middleware
break}}if (typeof route === 'function') {
await route(ctx, next)
return
}
await next()
}
}
}
module.exports = Router
Copy the code
Loacalhost :3000 test loacalHost :3000 test loacalHost :3000
This article source address: github.com/marklin2012…
Welcome to the bump Lab blog: AOtu.io
Or pay attention to the bump Laboratory public account (AOTULabs), push the article from time to time: