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