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