Знакомство с Koa или coroutine в nodejs

в 7:03, , рубрики: javascript, koa, koajs, node.js, web-разработка, Веб-разработка

Предисловие

Меня уже очень давно привлекает javascript в качестве единого языка для веб-разработки, но до недавнего времени все мои изыскания оканчивались чтением документации nodejs и статей о том, что это callback`овый ад, что разработка на нем приносит лишь боль и страдания. Пока не обнаружил, что в harmony появился оператор yield, после чего я наткнулся на koa, и пошло поехало.

В чем соль

Собственно, koa во многом похож на предшественника — express, за исключением вездесущих callback`ов. Вместо них он использует обещания (promises) или thunk (не знаю, как это можно перевести). В некоторых местах даже сохранена некоторая обратная совместимость через группу функций co, написанных создателем koa и многими последователями. Любая функция, которая раньше использовала callback, может быть thunkify`цирована для использования с co или koa.

Выглядит это так:

var func = function(opt){
    return function(done){
        /* … */
        (…) && done(null, data) || done(err)
    }
}

var func2 = function(opt){
    return function(done){
        oldFuncWithCallback(opt, function(err, data){
            (...) && done(null, data) || done(err)
        }
    }
}

co(function*{
    /* … */
    var result = yield func(opt);
    var result2 = yield func2(opt);

    var thunkify = require('thunkify');
    var result3 = yield thunkify(oldFuncWithCallback(opt))
})()

При этом в result вернется data, а done(err) вызовет исключение, и функцию вы не покидаете, как это было бы с callback`ом, и интерпретатор не блокируете, выполнение переходит к следующему yield`у, и выглядит это изящно, другими словами — просто сказка.

Время писать код

Koa основан на middleware`ях, также, как express, но теперь они выполняются как сопрограмма, подобно tornado в python. Дальше пойдет код простого сайта и мои мысли.

Структура проекта:

  • node_modules
  • src — Здесь весь исходный код
    • server — Сервер
    • app — Папка с приложениями
    • public — Статика
    • template — Шаблоны
    • config — Файлы конфигурации

Так как предыдущим моим увлечением был Django, кому-то может показаться, что это сказалось на организацию кода в проекте, возможно это правда, мне нравится организовывать код в модули.

src/server/index.js

'use strict';
var koa = require('koa');
var path = require('path');

var compose = require('koa-compose');
// эта утилита позволяет композировать набор middleware`й в одну

var app = module.exports = koa();
// выглядит знакомо

var projectRoot = __dirname;
var staticRoot = path.join(projectRoot, '../public');
var templateRoot = path.join(projectRoot, '../template');
// нечто подобное мы делали в settings.py в django

var middlewareStack = [
    require('koa-session')(), // расширяет контекст свойством session
    require('koa-less')('/less', {dest: '/css', pathRoot: staticRoot}), 
    // компилирует less в css, если был запрошен файл со стилями, имеет много интересных опций
    require('koa-logger')(), // логирует все http запросы
    require('koa-favicon')(staticRoot + '/favicon.png'),
    require('koa-static')(staticRoot), // отдает статику, удобно для разработки, лучше конечно делать это nginx`ом
    require('koa-views')(templateRoot, {'default': 'jade'}) // Jade еще одна причина любви к nodejs
];

require('koa-locals')(app); 
// добавляет объект locals к контексту запроса, в который вы можете записывать все, что угодно

app.use(compose(middlewareStack));
/* 
все перечисленные middleware должны возвращать функцию генератор, так же мы можем проинициировать здесь что-то сложное
и долгое, никаких лишних callback`ов тут не будет и интерпретатор не заткнется, а продолжит выполнение, 
вернувшись, когда будет время
*/

var routes = require('./handlers');
app.use(function *(next) {
    // в качестве this, middleware получает app, который в последствии расширяет и передает дальше
    this.locals.url = function (url, params) {
        return routes.url(url, params);
    };

    yield next
});
/* 
так выглядит типовой middleware, в данном случае эта конструкция добавляет функцию url, которую можно использовать в шаблонах, 
либо где то еще, для получения абсолютных урлов по имени и параметрам
*/

app.use(routes.middleware());

Нужно помнить, что эта цепочка вызывается каждый раз, когда сервер получает запрос. Чтобы лучше понять порядок их выполнения, можно воспользоваться таким примером:

app.use(function*(next){
    console.log(1)
    yield heavyFunc()
    console.log(2)
    yield next
})
app.use(function*(next){
    console.log(3)
    yield next
})

Каждый запрос в консоль будет выведено

1
3
2

Далее в папке с сервером я кладу handlers.js, модуль, который регистрирует приложения из папки src/app.

src/server/handlers.js

var Router = require('koa-router');
var router = new Router();

function loadRoutes(obj, routes){
    routes.forEach(function(val){
        var func = val.method.toLowerCase() == 'get' ? obj.get :
            val.method.toLowerCase() == 'post' ? obj.post :
            val.method.toLowerCase() == 'all' ? obj.all : obj.get;
        return func.call(obj, val.name, val.url, val.middleware)
    })
}

loadRoutes(router, require('src/app/home').routes); // Так подключается приложение из папки app

module.exports = router;

Модуль инкапсулирует метод loadRoutes, который принимает только что созданный экземпляр маршрутизатора и список объектов, содержащих информацию о маршрутах. На примере home я покажу, как выглядят приложения для работы с этим модулем:

src/app/home.js

function* index(next){
    yield this.render('home/index', {
        Hello: 'World!'
    })
}

var routes = [
    {method: 'get', name: 'index', url: '/', middleware: index}
];

exports.routes = routes;

Выглядит очень просто и органично, тут я пошел немного дальше модульности, предложенной в django, мне понравилась полная обособленность модуля от остального приложения, включая собственные маршруты. Конечно, при таком подходе может возникнуть конфликт урлов и вы получите не то, что ожидали. Можно добавлять название приложения, либо использовать koa-mount, либо улучшить регистратор для предотвращения дубликатов.

Надо сказать, что для рендера страницы нужно заполнить this.body, чем и занимается this.render, либо передать выполнение дальше, с помощью yield next, иначе в теле страницы вы получите «Not Found». Если ни один из middleware не заполнил body и продолжил выполнение, правильную страницу 404 можно отрисовать, поместив в конец src/server/index.js такую middleware:

app.use(function*(){
    this.status = 404;
    yield this.render('service/404')
    // либо редирект, либо что угодно
})

Заключение

На сладкое решил оставить обработку ошибок. От адептов nodejs и express слышал, что это требует недюжей внимательности к каждому callback`у и даже она не всегда помагает. Если вспомнить порядок выполнения middleware, глобальную обработку можно осуществить, просто добавив следующий код в начало обработки запроса:

app.use(function* (next){
    try {
        yield next
    } catch (err) {
        this.app.emit('error', err, this); // транслировать тело ошибки в консоль
        yield this.render('service/error', {
            message: err.message,
            error: err
        })
    }
)

Этим мы заключаем весь код проекта в try..catch, ну и не забывайте, что app — это прежде всего eventEmitter. По-моему, это просто гениально. Модулей для koa уже написано великое множество, почти каждый модуль для express уже адаптирован для koa, такие как mongoose, passport, request и многие другие. Так мы получили асинхронное программирование, которое приносит радость и фан. К томуже небезызвестный TJ остается поддерживать koa.

Философски, Koa стремится исправить и заменить nodejs, в то время как Express расширяет nodejs.

Отрывок из начала статьи, koa-vs-express.

Спасибо, что читали. Всего хорошего, всем nodejs.

Автор: freydev

Источник

* - обязательные к заполнению поля