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

Предисловие


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

В чем соль


Собственно, koa во многом похож на предшественника — express, за исключением вездесущих callback`ов. Вместо них он использует сопрограммы, в которых управление может быть передано подпрограммам(thunk), обещаниям(promise), массиву с подпрограммами\соообещаниями, либо объекту. В некоторых местах даже сохранена некоторая обратная совместимость через группу функций 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.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 12
  • 0
    Можно было бы добавить пару строк про await/async из es7, т.к. их трансляция в генераторы тривиальна.
    • +7
      Вместо них он использует обещания (promises)

      Вот вы написали, что соль в обещаниях соль koa, но в ваших же примерах кода их нет.
      Promises можно прикрутить куда угодно, даже если в этом «куда угодно» их нет в «коробке», а соль koa в том, что он написан с использованием генераторов, которые таки заменяют собой и promis`ы и callback`и.
      • 0
        Дельное замечание, здесь я хотел сказать, что koa может использовать промисы точно также как генераторы, из «коробки», если хотите, при этом конечный код остается неизменным. Более детальные примеры обязательно будут позже, они несколько выходят за рамки этой статьи, моей целью было просто познакомить программистов nodejs+express и людей которые смотрят в сторону ноды, с этим замечательным фреймворком, так как не нашел ни одной подобной статьи.

        Спасибо, немного поправил текст, который вы процитировали.
      • 0
        Красота Koa в простоте.

        Эту красоту можно увидеть на простых примерах:

        var koa = require('koa');
        var app = koa();
        
        app.use(function *(){
          this.body = 'Hello World';
        });
        
        app.listen(3000);
        


        В статье все хорошо расписано, но стоит посмотретьт примеры из основной документации koajs.com
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Как и с express нужно немного подождать пока еще сыровато, ну не подумайте что я такой зануда, но на продакшен не покатит. Как развлечение вполне.
            • 0
              Не подумайте, что я такой зануда, но что именно сыровато? Конечно, фейсбук я бы делать пока не рискнул, но хабр — вполне. Paypal переходит на nodejs, а вам сыро?
              • +1
                Я же просил не воспринимать меня как зануду, nodejs это уже давно вполне отличная технология. Я имею ввиду koa, начнем с того что оно требует последней ноды, а это уже не хорошо. Второе это подход я всегда считал (это сугубо мое мнение) что ад колбеков это просто неправильное понимание асинхронного подхода и архитектурные просчеты. Но идея мне их нравится. Но пока увы такой подход с учетом требования не стоит использовать в продакшене (Саму ноду уже давно можно).
                • –1
                  требует последней ноды, а это уже не хорошо
                  Да почему же?
                  • +1
                    Нечетные версии ноды — нестабильные. 0.11 служит местом обкатки новых изменений. А стабильный функционал и багфикс будет уже в 0.12 версии, поэтому koa в production можно будет использовать только 0.12 версии.
                    • 0
                      Кому-то надо обкатывать 11 версию, иначе она так и не станет стабильной:)
              • 0
                automoto.ua. Проект написан на Koa. Посешаемость чуть более 10 000 уник. пользов./день.

                На продакшине катит.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.