Lynne, a front-end brick-moving engineer who can eat, love, laugh and always have a girl’s heart, pays attention to the way of health care. In the Internet wave, love life and technology.

Writing in the front

Under the current environment, the front-end node does not seem to be a bit past, started to contact THE SSR project, to understand the basic implementation of SSR and how to use, but also know to learn node, but nearly a year began to deeply understand the content of the framework.

The server side rendering basically adopts the Node Express or KOA framework. In order to improve the project engineering and business process, a lot of middleware is needed to deal with it. In order to maintain existing middleware or develop new middleware. In addition, it is imperative to learn Node, Express and KOA in order to better understand SSR and isomorphism.

The statement

A KOA source code learning experience and new, the starting point is to develop middleware, handwritten KOA source code. Currently in Reading Express, an attempt is made to compare the two.

For KOA usage and API usage, please refer to “official Documentation” (koajs.com/), which can be used in conjunction with “Chinese Documentation” (koa.bootcss.com/).

Source code learning ideas

Koa has four core files: application.js, Context. js, request.js, response.js. All of the code adds up to less than 2000 lines, which is pretty light, and a lot of the code is focused on request.js and Response.js handling of request and response headers. The real core code is only a few hundred lines.

In addition, in order to sort out koA’s operating principle and logic more intuitively, or through debugging to go through the process, this article will be combined with debugging source code analysis.

Take the following code debugging as an example:

const Koa = require('koa') const app = new Koa() const convert = require('koa-convert'); Use (async(CTX, next) => {console.log('middleware before await'); const start = new Date() await next(); console.log('middleware after await'); const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) }) app.use(async(ctx, next) => { console.log('response'); ctx.body = "response" }) app.listen(3000);Copy the code

There are many debugging methods for Node. For details, see Node.js Debugging Method.

Personal habit to use the command line node inspect index.js, simple and direct, into the debugging.

A, application

Application.js is a koA entry file that exports koA’s constructors, which contain koA’s main functionality implementations.

Export a constructor, Application, which provides the function API methods, and analyze the function implementation from the main API methods.

1. listen

The Application constructor implements Listen via the HTTP module in Node:

/** * Shorthand for: * * http.createServer(app.callback()).listen(...) * * @param {Mixed} ... * @return {Server} * @api public */ listen (... Args) {debug('listen') const server = http.createserver (this.callback()) // Returns HTTP Class, and handle each individual request with the this.callback() callback. Return server.listen(... Args) // Start HTTP server listening connection to implement KOA server listening connection}Copy the code

2. use

The use method adds all middleware functions it receives to this.middleware so that it can call each middleware in order later, and will say ‘Middleware must be a function! ‘.

/** * Use the given middleware `fn`. * * Old-style middleware will be converted. * * @param {Function} fn * @return {Application} self * @api public */ use(fn) { if (typeof fn ! == 'function') throw new TypeError('middleware must be a function! '); // For generator type middleware functions, the KOA-convert library is used to convert them to be compatible with recursive calls to KOA in KOA2. if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; }Copy the code

3. callback

When the listen function is started, the createServer function returns the result of the callback function.

When the service is started, the callback function is executed to merge the middleware and listen for faulty requests from the framework layer.

It then returns the handleRequest method, which takes req and RES and creates a new KOA context CTX based on node HTTP native REQ and RES each time the server receives the request.

/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */ Callback () {const fn = compose(this.middleware) const fn = compose(this.middleware) this.listenerCount('error')) this.on('error', this.onerror) const handleRequest = (req, res) => { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest }Copy the code

In application.js, middleware is incorporated through Compose and is a core implementation of KOA.

You can see the source code for Koa-compose, which is very simple, with only a few dozen lines:

/** * @param {Array} Middleware takes an Array of middleware functions. * @return {Function} * @api public */ Function compose (middleware) {// compose requires an array of functions [fn,fn,fn...] // If it is not an array, the error if (! Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ') for (const fn of middleware) {// If an item in the array is not a function, throws an error if (Typeof fn! == 'function') throw new TypeError('Middleware must be composed of functions! ') } /** * @param {Object} context * @return {Promise} * @api public */ return function (context, Next) {// Let index = -1 return dispatch(0) function dispatch(I) {if (I <= index) return promise.reject (new) Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (! fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }Copy the code

Compose receives an array of middleware functions and returns a closure that maintains an index of the currently invoked middleware.

Dispatch (I) returns the result of the i-th function in middleware via promise.resolve (), the i-th + 1 function passed in app.use(). The second argument to the app.use() callback is next, so when the code in app.use() is executed to next(), dispatch.bind(null, I + 1)) is executed, the next app.use() callback.

By analogy, the app.use() callbacks are strung together until there is no next, and the edge returns the logic after next() that executes each app.use() in sequence. The result of the first app.use() is finally returned via promise.resolve (). You can think about it in terms of the Onion model.

4. createContext

Let’s look at the createContext function, which is a bunch of assignment operations.

  1. Given that context, request, and Response are imported from context.js, request.js, and Response.js, Create () generates new context, request, and Response objects based on these three objects to prevent the original introduced objects from being polluted.
  2. Request = object.create (this.request) and context.response = object.create (this.response) Object is mounted to the context object. This corresponds to the delegate part of context.js (the delegate part is visible below in the KOA core library), Enable CTX to access ctx.request. XXX and ctx.response. XXX directly through ctx.xxx.
  1. Through a series of assignment operations, the res and REq of the original HTTP request, the Koa instance app, and so on are mounted to the context, Request, and Response objects respectively. Context. Js, request.js, and Response. js can be used to access the original request and corresponding parameters.
const response = require('./response') const context = require('./context') const request = require('./request') /** * Initialize a new context. * * @api private */ createContext (req, res) { const context = Object.create(this.context) const request = context.request = Object.create(this.request) const response = context.response = Object.create(this.response) context.app = request.app = response.app = this context.req =  request.req = response.req = req context.res = request.res = response.res = res request.ctx = response.ctx = context request.response = response response.request = request context.originalUrl = request.originalUrl = req.url context.state  = {} return context }Copy the code

5. handleRequest

After createContext is executed in the callback, the created CTX and the sequentially executed function generated after merging the middleware are passed to handleRequest and executed.

HandleRequest listens for the RES through the onFinished method, and when the RES completes, closes, or fails, the onError callback is executed. After that, it returns the result of the middleware execution. When all the middleware execution is complete, it executes respond and returns the data.

/**
   * Handle request in callback.
   *
   * @api private
*/

handleRequest (ctx, fnMiddleware) {
  const res = ctx.res
  res.statusCode = 404
  const onerror = err => ctx.onerror(err)
  const handleResponse = () => respond(ctx)
  onFinished(res, onerror)
  return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
Copy the code

6. toJSON

* @return {Object} * @api public */ toJSON () {return only(this, [ 'subdomainOffset', 'proxy', 'env' ]) } /** * Inspect implementation. * * @return {Object} * @api public */ inspect () { return this.toJSON() }Copy the code

7. respond

/** * Response helper. */ function respond (ctx) { // allow bypassing koa if (ctx.respond === false) return if (! ctx.writable) return const res = ctx.res let body = ctx.body const code = ctx.status // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null return res.end() } if (ctx.method === 'HEAD') { if (! res.headersSent && ! ctx.response.has('Content-Length')) { const { length } = ctx.response if (Number.isInteger(length)) ctx.length = length } return res.end() } // status body if (body == null) { if (ctx.response._explicitNullBody) { ctx.response.remove('Content-Type') ctx.response.remove('Transfer-Encoding') ctx.length = 0 return res.end() } if (ctx.req.httpVersionMajor >= 2) { body = String(code) } else { body = ctx.message || String(code) } if (! res.headersSent) { ctx.type = 'text' ctx.length = Buffer.byteLength(body) } return res.end(body) } // responses if (Buffer.isBuffer(body)) return res.end(body) if (typeof body === 'string') return res.end(body) if (body instanceof Stream) return body.pipe(res) // body: json body = JSON.stringify(body) if (! res.headersSent) { ctx.length = Buffer.byteLength(body) } res.end(body) }Copy the code

Second, the context. Js

1. cookie

Context. Js uses the get and set methods to set and read cookies.

// Get cookies get cookies () {if (! this[COOKIES]) { this[COOKIES] = new Cookies(this.req, this.res, { keys: this.app.keys, secure: This.request.secure})} return this[COOKIES]}, // Set COOKIES set COOKIES (_cookies) {this[COOKIES] = _cookies}Copy the code

2. delegate

There are a lot of delegate operations in context.js.

Through the delegate, CTX can directly access the properties and methods in the response and request above it, that is, ctx.request. XXX or ctx.response. XXX can be obtained through ctx.xxx.

Delegates are done through the delegates library, delegates delegate the properties and methods of the node beneath the object via proto.definegetter and proto.definesetter. (Proto.definegetter and proto.definesetter are now deprecated by MDN and object.defineProperty () is used instead)

const delegate = require('delegates')

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');
  // ...

delegate(proto, 'request')
  .method('acceptsLanguages')
  .getter('ip');
  // ...
Copy the code

Context. Js exports a context object, which is mainly used to transmit information between middleware and other components. Meanwhile, two objects, Request and Response, are mounted on the context object.

Event and method delegates on request and Response object were delegates using the delegates library for easy user use.

3. toJSON()

Explicitly call.tojson () on each object, otherwise iteration will fail due to getters and cause instance programs like Clone () to fail.

Public */ toJSON() {return {this.request.tojson (), response: this.response.toJSON(), app: this.app.toJSON(), originalUrl: this.originalUrl, req: '<original node req>', res: '<original node res>', socket: '<original node socket>' } }Copy the code

3. Request and Response

1. request

Request.js exports the request object, and uses the get() and set() methods to query the parameters of the request header, such as header, URL, href, method, path, query… Do the processing, mounted to the request object, convenient user access and Settings.

Here basically belongs to the kind of simple code that can be understood at a glance, in line with the mentality of learning to consolidate the foundation, simply take a look at a few examples:

/** * Get origin of URL. ** @return {String} * @api public */ Get origin () {// protocol + hostname return `${this.protocol}://${this.host}` }, /** * Get full request URL. * @return {String} * @api public */ Get href () { `GET http://example.com/foo` if (/^https?:///i.test(this.originalUrl)) return this.originalUrl return this.origin + this.originalUrl }, /** * Get parsed query string. * * @return {Object} * @api public */ get query () { const str = this.querystring const c  = this._querycache = this._querycache || {} return c[str] || (c[str] = qs.parse(str)) }, /** * Parse the "Host" header field host * and support X-Forwarded-Host when a * proxy is enabled. * * @return {String} Hostname :port * @api public */ get host () { Http_forwarded_host (' x-Forwarded-host ') if (! host) { if (this.req.httpVersionMajor >= 2) host = this.get(':authority') if (! host) host = this.get('Host') } if (! host) return '' return host.split(/\s*,\s*/, 1)[0] },Copy the code

2. response

As with request.js, response parameters are handled by get() and set(). Here the big guy on demand, source code to see more, can not always parrot, to find meaningful problems.

set body (val) {
    const original = this._body
    this._body = val

    // no content
    if (val == null) {...return
    }

    // set the status
    if (!this._explicitStatus) this.status = 200

    // set the content-type only if not yet set
    const setType = !this.has('Content-Type')

    // string
    if (typeof val === 'string') {...return
    }

    // buffer
    if (Buffer.isBuffer(val)) {
      ...
      return
    }

    // stream
    if (val instanceof Stream) {
      ...
      return
    }

    // json
    this.remove('Content-Length')
    this.type = 'json'
},
Copy the code

Response.type is assigned in the set accessor of Response.body.

If we write the type setting after the body setting:

app.use(async(ctx, next) => {
  console.log('response');

  ctx.body = {'aaa': '1111'};
  ctx.type = 'html'

  console.log(ctx.type, 'type')})Copy the code

Ok, the type I get is text/ HTML.

But if the body format doesn’t match, response.type will be overwritten as JSON, so you’ll actually get an application/json type:

app.use(async(ctx, next) => {
  console.log('response');
  ctx.type = 'html'
  ctx.body = {'aaa': '1111'};

  console.log(ctx.type, 'type')})Copy the code

The most reasonable order is definitely to assign response.type first, after all, type processing into JSON format is just a back-of-the-line logic.

Write Koa by hand

1. Encapsulate the HTTP module of node

Create an internal MyKoa class, HTTP module based on Node, and implement listen function:

// application.js const http = require('http'); class MyKoa { listen(... args) { const server = http.createServer((req, res) => { res.end('mykoa') }); server.listen(... args); } } module.exports = MyKoa;Copy the code

2. Implement the Use method and simple createContext

We then implement the app.use() method, and since app.use() has ctx.body inside it, we also need to implement a simple CTX object.

Create context.js, export CTX object, get and set CTX.

// context.js module.exports = { get body() { return this._body; }, set body(value) { this._body = value; }},Copy the code
  1. Add the use and createContext methods to the MyKoa class of application.js, and res.end returns ctx.body:
const http = require('http'); const _context = require('./context'); class MyKoa { listen(... args) { const server = http.createServer((req, res) => { const ctx = this.createContext(req, res); this.callback(); res.end(ctx.body); }); server.listen(... args); } use(callback) { this.callback = callback; } createContext(req, res) { const ctx = Object.assign(_context); return ctx; } } module.exports = MyKoa;Copy the code

3. Perfect createContext

To access the request header through CTX and set the information related to the response, such as ctx.query, ctx.message, etc., create response.js and request.js to handle the request header and response header. Mount the Request and Response objects to the CTX object, and implement a delegate function that gives CTX access to properties and methods on the Request and Response objects.

  1. Achieve simple request and response, request through get method, can parse the parameters in req. Url, convert it into an object to return.
// request.js module.exports = { get header() { return this.req.headers }, get method() { return this.req.method }, get url() { return this.req.url }, get query() { const arr = this.req.url.split('? '); if (arr[1]) { const obj = {}; arr[1].split('&').forEach((str) => { const param = str.split('='); obj[param[0]] = param[1]; }); return obj; } return {}; }};Copy the code

In response, get and set message can be used to get and set res.statusMessage:

// response.js module.exports = { get status() { return this.res.statusCode || ''; }, get message() { return this.res.statusMessage || ''; }, set status(code) { return this.res.statusCode = code; }, set message(msg) { this.res.statusMessage = msg; }};Copy the code
  1. Create a new utils.js, export the delegate method, and internally pass object.defineProperty to the passed Object obj to listen for property changes in real time. For example, delegate(CTX, ‘request’) When the value of the request object changes, the CTX on the Request proxy also gets the latest value.

When using getter or setter, add the corresponding key to setters and getters with listen function, and let OBj proxy the corresponding key to proterty when accessing it:

// utils.js module.exports.delegate = function Delegate(obj, property) { let setters = []; let getters = []; let listens = []; function listen(key) { Object.defineProperty(obj, key, { get() { return getters.includes(key) ? obj[property][key] : obj[key]; Obj [property][key] = obj[property][key] = obj[key] Set (val) {if (setters.includes(key)) {obj[property][key] = val; } else {obj[key] = val; // Obj [key] = val; }}}); } this.getter = function (key) { getters.push(key); if (! Listens. Includes (key)) {// prevent repeated calls to listen listen(key); listens.push(key); } return this; }; this.setter = function (key) { setters.push(key); if (! Listens. Includes (key)) {// prevent repeated calls to listen listen(key); listens.push(key); } return this; }; return this; };Copy the code
  1. Use the delegate method to delegate request and response in the context:
// context.js const { delegate } = require('./utils'); const context = (module.exports = { get body() { return this._body; }, set body(value) { this._body = value; }}); Delegate (context, 'request'). Getter ('header'); // Delegate (context, 'request'). delegate(context, 'request').getter('method'); delegate(context, 'request').getter('url'); delegate(context, 'request').getter('query'); delegate(context, 'response').getter('status').setter('status'); delegate(context, 'response').getter('message').setter('message');Copy the code
  1. Improve the createContext function:

Mount REQ and RES to CTX

// application.js
const http = require('http');
const _context = require('./context');
const _request = require('./request');
const _response = require('./response');

class MyKoa {
  // ...
  createContext(req, res) {
    const ctx = Object.assign(_context);
    const request = Object.assign(_request);
    const response = Object.assign(_response);
    ctx.request = request;
    ctx.response = response;
    ctx.req = request.req = req;
    ctx.res = response.res = res;
    return ctx;
  }
}

module.exports = MyKoa;
Copy the code

4. Implement middleware and Onion model

Finally, we can realize the function of app.use() middleware:

  1. In the context of the above koa-compose analysis, the onion model is implemented in utils.js using compose:
// utils.js module.exports.compose = (middleware) => { return (ctx, next) => { let index = -1; return dispatch(0); function dispatch(i) { if (i <= index) return Promise.reject(new Error('error')); index = i; const cb = middleware[i] || next; if (! cb) return Promise.resolve(); try { return Promise.resolve( cb(ctx, function next() { return dispatch(i + 1); })); } catch (error) { return Promise.reject(error); }}}; };Copy the code
  1. In appcation.js, initialize the array of this.middleware and add callback to the array in use() :
// application.js
class MyKoa {
  constructor() {
    this.middleware = [];
  }
  // ...

  use(callback) {
    this.middleware.push(callback);
  }
  // ...
}

module.exports = MyKoa;
Copy the code
  1. In the Listen method createServer, middleware is merged when the request is encountered, and the middleware returns the RES result after execution:
// application.js const { compose } = require('./utils'); class MyKoa { listen(... args) { const server = http.createServer((req, res) => { const ctx = this.createContext(req, res); // Get the middleware function const fn = compose(this.middleware); Fn (CTX).then(() => {// After all middleware is executed, res.end(ctx.body) is returned; }) .catch((err) => { throw err; }); }); server.listen(... args); } / /... } module.exports = MyKoa;Copy the code

5. Test

Introduce our Mykoa and test it with the following service:

const Koa = require('.. /my-koa/application'); const app = new Koa(); app.use((ctx, next) => { ctx.message = 'ok'; 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.use((ctx, next) => { console.log(ctx.header); console.log(ctx.method); console.log(ctx.url); console.log(ctx.query); console.log(ctx.status); console.log(ctx.message); ctx.body = 'hello, my-koa-demo'; }); app.listen(3000, () => { console.log('server is running on 3000... '); });Copy the code

Go to http://localhost:3000/api? Name = ZLX interface, return data is Hello, my-koa-demo. Meanwhile, the node server console prints the following:

1 3 5 {host: '127.0.0.1:3000', connection: 'keep-alive', 'cache-control': 'max-age=0', 'sec-CH-UA ': '"Google Chrome"; v="95", "Chromium"; v="95", "; Not A Brand"; v="99"', 'sec-ch-ua-mobile': '? 0', 'sec-CH-UA-platform ':' macOS ', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36', Accept: 'text/html,application/xhtml+xml,application/xml; Q = 0.9, image/avif, image/webp image/apng, * / *; Q = 0.8, application/signed - exchange; v=b3; Q = 0.9 ', 'the SEC - fetch - site' : 'none', 'the SEC - fetch - mode' : 'navigate', 'the SEC - fetch - user' : '? 1', 'sec-fetch-dest': 'document', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en; Q = 0.9 '} GET /? a=1&b=2 { a: '1', b: '2' } 200 ok 6 4 2Copy the code

A request path of /favicon.ico is also printed, indicating that two page requests were made.

Source code debugging – github.com/Lynn-zuo/no…

Learning route and experience

1. Road Map:

Learn the basics of Node.js (API manual, partial understanding) –> read through a Node.js related book –> learn the framework (combined with the project to understand the API usage) –> read the framework source code –> handwritten simple framework implementation

2. The result

Compared to the complex front-end framework vue source code and packaging tools WebPack, or Rollup, or even the relatively simple Node framework Express, koA’s source code is clean and simple.

However, the functions provided by KOA are limited. Specific functions need to be completed by calling plug-ins. For example, koA-Router middleware needs to be installed for routing configuration, and KOA itself can read/set cookies. However, session processing needs home and KOA-Session middleware to achieve.

Read the KOA source code and express source code:

  1. Unlike Express, which supports processing requests for specific routes, koA’s app.use() method itself does not support processing for specific routes, which must be done by installing the KOA-Router middleware.

  2. Express calls next() in synchronous requests to achieve koA-like onion rings, but not asynchronous requests. This is the difference between the two frameworks for middleware processing.

3.

The first time I felt lonely, and the second time…