[Из песочницы] Знакомство с Koa или coroutine в nodejs

Предисловие Меня уже очень давно привлекает 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.

© Habrahabr.ru