
Have you ever had a desire to rewrite everything from scratch, “score” on compatibility and do everything “according to the mind”? Most likely KoaJS was created that way. This framework has been developed by the Express team for several years. Expresovtsy about these 2 frameworks write like this:
Philosophically, Koa aims to “fix and replace node,” whereas Express “augments node” [From a philosophical point of view, Koa seeks to “fix and replace a node” while Express “expands a node”].
Koa is not burdened with legacy-code support, from the first line you dive into the world of modern ES6 (ES2015), and in version 2 there are already constructions from the future standard ES2017. In my company, this framework has been in production for 2 years already, one of the projects (
AUTO.RIA ) works at a load of half a million visitors per day. Despite its bias towards modern / experimental standards, the framework is more stable than Express and many other frameworks with the CallBack-style approach. This is not due to the framework itself, but to the modern JS constructions that it uses.
')
In this article, I want to share my koa development experience. In the first part, the framework itself will be described and a bit of theory on the organization of the code on it, in the second we will create a small rest-service on koa2 and bypass all the rakes that I have already stepped on.
Some theory
Let's take a simple example, write a function that reads data into an object from a JSON file. For clarity, we will do without "reqiure ('my.json')":
const fs = require('fs'); function readJSONSync(filename) { return JSON.parse(fs.readFileSync(filename, 'utf8')) }
Whatever problem
happens when calling
readJSONSync , we will handle this exception. Everything is great here, but there is a big obvious disadvantage: this function runs synchronously and blocks the flow for the entire duration of the reading.
Let's try to solve this problem in the nodejs style with the help of callback-functions:
const fs = require('fs'); function readJSON(filename, callback) { fs.readFile(filename, 'utf8', function (err, res) { if (err) return callback(err); try { res = JSON.parse(res); callback(null, res); } catch (ex) { callback(ex); } }) }
Here everything is fine with asynchronous, but the convenience of working with the code has suffered. There is another possibility that we will forget to check for the presence of the error 'if (err) return callback (err)' and if an exception occurs when reading the file, everything will “fall out”, the second inconvenience is that we have already plunged one step in called, callback hell. If there are a lot of asynchronous functions, the nesting will grow and the code will be read very hard.
Well, let's try to solve this problem in a more modern way,
let's design the function
readJSON with promis :
const fs = require('fs'); function readJSON(filename) { return new Promise(function(resolve,reject) { fs.readFile(filename,'utf8', function (err, res) { if (err) reject(err); try { res = JSON.parse(res); resolve(res); } catch (e) { reject(e); } }) }) }
This approach is a bit more progressive, because we can “expand” a large complex nesting into a chain of then ... then ... then, it looks approximately like this:
readJSON('my.json') .then(function (res) { console.log(res); return readJSON('my2.json') }).then(function (res) { console.log(res); }).catch(function (err) { console.log(err); } );
This situation, for the time being, does not change significantly, there is a cosmetic improvement in the beauty of the code, it may have become clearer what is being done. The situation has dramatically changed the appearance of
generators and the
co library, which became the basis of the koa v1 engine.
Example:
const fs = require('fs'), co = require('co'); function readJSON(filename) { return function(fn) { fs.readFile(filename,'utf8', function (err, res) { if (err) fn(err); try { res = JSON.parse(res); fn(null,res); } catch (e) { fn(e); } }) } }
In the place where the
yield directive is used, the execution of the asynchronous
readJSON occurs . readJSON needs to be redone a bit. This design code is called thunk-function. There is a special library that makes the function written in nodejs-style into the thunk-function
thunkify .
What does this give us? The most important thing is the code in the part where we call yield, is executed sequentially, we can write
console.log(yield readJSON('my.json')); console.log(yield readJSON('my2.json'));
and get a consistent execution by first reading 'my.json' then 'my2.json'. And this is already a "callback goodbye." Here the “ugliness” lies in the fact that we use the feature of the generators work not for their intended purpose, the thunk-function is something non-standard and rewrite everything for koa into such a format “not ice”. It turned out that not everything is so bad, yield can be done not only for the thunk-function, but also a promise or even an array of promises or an object with promises.
Example:
console.log( yield { 'myObj': readJSON('my.json'), 'my2Obj': readJSON('my2.json') } );
It seemed that you couldn’t think better, but they did. They made it so that everything was “directly” designated. Meet
Async Funtions :
import fs from 'fs' function readJSON(filename) { return new Promise(function (resolve, reject) { fs.readFile(filename, 'utf8', function (err, res) { if (err) reject(err); try { res = JSON.parse(res); resolve(res) } catch (e) { reject(e) } }) }) }
Do not rush to run, your language will not understand this syntax without
babel . Koa 2 works exactly in this style. You have not yet scrambled?
Let's look at how this "killer kolbek" works:
import fs from 'fs'
similarly
var fs = require('fs')
with promisam already familiar.
() => {} Is the so-called “arrow function”, similar to the
function () {} entry. The arrow function has a slight difference - context: this refers to the object in which the arrow function is initialized.
async before the function indicates that it is asynchronous, the result of such a function is also a promise. Since, in our case, after executing this function, there is nothing to do there, we omitted the then or catch call. It could be as shown below, and this will also work:
(async() => { console.log(await readJSON('my.json')) })().catch (function(e) { console.log(e) })
await is a place where you have to wait for the asynchronous function (promise) to be executed and then work with the result that it returned or handle the exception. To some extent, this resembles the yield of generators.
The theory is over - we can start the first launch of KoaJS.
Meet koa
“Hello world” for koa:
const Koa = require('koa'); const app = new Koa();
a function that is passed as an argument in app.use is called middleware. Minimalist, isn't it? In this example, we see a shortened version of the record of this function. In Koa terminology, middleware can be of three types:
- common function
- async function
- generatorFunction
Also from the point of view of the code execution phase, middleware is divided into two phases: before (upstream) request processing and after (downstream). These phases are separated by the next function, which is transmitted in middleware.
common function
async function (works with babel transpiler)
app.use(async (ctx, next) => { const start = new Date(); await next(); const ms = new Date() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });
generatorFunction
In the case of this approach, it is necessary to include the
co library, which since version 2.0 is no longer part of the framework:
app.use(co.wrap(function *(ctx, next) { const start = new Date(); yield next(); const ms = new Date() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }));
Legacy middleware from koa v1 is also supported. I hope in the upstream examples it is clear where upstream / downstream. (If not - write to the comments)
In the
context of the ctx request, there are 2 important for us
request and
response objects. In the process of writing middleware, we will analyze some properties of these objects; you can get a full list of properties and methods that you can use in your application by the links provided.
It's time to move on to practice, until I have quoted all the ECMAScript documentation
We write our first middleware
In the first example, we will extend the functionality of our “Hello world” and add an additional header to the response, which will indicate the processing time of the request, another middleware will write to the log all requests to our application. Go:
const Koa = require('koa'); const app = new Koa();
The first middleware saves the current date and writes a header to the response at the downstream stage.
The second one does the same thing, only writes not to the heading, but outputs it to the console.
It should be noted that if the next method is not called in middleware, then all middleware that are connected after the current one will not take part in processing requests.
When testing an example, do not forget to connect babel
Error handler
With this task, koa is doing great. For example, we want in case of any error to respond to the user in the json-format 500 error and the message property with information about the error.
The very first middleware we write the following:
app.use(async (ctx, next) => { try { await next(); } catch (err) {
Everything, you can try to throw an exception using 'throw new Error ("My error") in any middleware or provoke an error in another way, it will "pop up" to our handler along the chain and the application will respond correctly.
I think that this knowledge should be enough for us to create a small REST service. We will certainly deal with this in the second part of the article, if, of course, it is interesting to someone other than me.
useful links