Если открыть доку по node.js и мельком посмотреть на то количество модулей, которые есть в ядре, — можно открыть много нового для себя. Как вы уже догадались, речь пойдет про очередной велосипед.
Сразу скажу, что многие финты были позаимствованы с php-фреймворков.
Зависимости, которые я все же оставил в проекте:
async, hashids, mime-types, sequelize, validator, pug
1) давайте определимся со структурой проекта:
— bin файлы для старта приложения
— config конфиги нашего приложения
— migrations миграции
— modules модули
— views основные view
— behaviors первичные бихеверы, которые могут понадобиться в 90% проектов
— console классы, которые нужны для старта приложения в консольном режиме
— helpers папка с различными хелперами
— modules модули, которые нужны в 90% проектов (миграции, рендер статики)
— web классы, нужные для работы в режиме web-приложения
2) Как запустить web приложение:
Создадим файл bin/server.js
import Application from "dok-js/dist/web/Application";
import path from "path";
const app = new Application({
basePath: path.join(__dirname, ".."),
id: "server"
});
app.run();
export default app;
После чего наше приложение будет пытаться загрузить конфинг из ./config/server.js
import path from "path";
export default function () {
return {
default: {
basePath: path.join(__dirname, ".."),
services: {
Database: {
options: {
instances: {
db: {
database: "example",
username: "example",
password: "example",
params: {
host: "localhost",
dialect: "postgres"
}
}
}
}
},
Server: {
options: {
port: 1987
}
},
Router: {
options: {
routes: {
"/": {
module: "dashboard",
controller: "index",
action: "index"
},
"/login": {
module: "identity",
controller: "identity",
action: "index"
},
"/logout": {
module: "identity",
controller: "identity",
action: "logout"
},
"GET /assets/<filePath:.*>": {
module: "static",
controller: "static",
action: "index",
params: {
viewPath: path.join(__dirname, "..", "views", "assets")
}
},
"/<module:\w+>/<controller:\w+>/<action:\w+>": {}
}
}
}
},
modules: {
identity: {
path: path.join(__dirname, "..", "modules", "identity", "IdentityModule")
},
dashboard: {
path: path.join(__dirname, "..", "modules", "dashboard", "DashboardModule")
}
}
}
};
}
Вот мы и пришли к тому моменту, который мне не дал юзать роуты от expressjs. Как вы видите, текущий вариант роутов очень гибкий и позволяет тонко настраивать приложение, тут я брал идеи с yii2.
Теперь боль номер два: контроллеры и экшены, которые нам навязывает expressjs и большинство nodejs-фреймворков. Это как правило анонимная функция (я понимаю, что это нужно для производительности), которая на вход получает request и response и делает с ними все что угодно, т.е. если вам нужно будет в середине проекта воткнуть логгер, к примеру, для логирования всех респонзов, будь добр прорефакторить почти все приложение, и не дай бог пропустить вызов колбека который делает next(request, response), это я к тому, что никогда не знаешь в какой момент времени твой экшен закончил свое выполнение.
Решение, которое я предлагаю:
async run(ctx) {
this.constructor.parse(ctx);
try {
ctx.route = App().getService("Router").getRoute(ctx.method, ctx.url);
} catch (e) {
return App().getService("ErrorHandler").handle(404, e.message);
}
try {
return App().getModule(ctx.route.moduleName).runAction(ctx);
} catch (e) {
return App().getService("ErrorHandler").handle(500, e.message);
}
}
async runAction(ctx) {
const {controllerName, actionName} = ctx.route;
const controller = this.createController(controllerName);
if (!controller[actionName]) {
throw new Error(`Action "${actionName}" in controller "${controllerName}" not found`);
}
const result = await this.runBehaviors(ctx, controller);
if (result) {
return result;
}
return controller[actionName](ctx);
}
Т.е. мы получили единую точку запуска всех контролерров.
Ну и сам контроллер:
import Controller from "dok-js/dist/web/Controller";
import AccessControl from "dok-js/dist/behaviors/AccessControl";
export default class IndexController extends Controller {
getBehaviors() {
return [{
behavior: AccessControl,
options: [{
actions: ["index"],
roles: ["user"]
}]
}];
}
indexAction() {
return this.render("index");
}
}
import Controller from "dok-js/dist/web/Controller";
import SignInForm from "../data-models/SignInForm";
export default class IdentityController extends Controller {
async indexAction(ctx) {
const data = {};
data.meta = {
title: "Авторизация"
};
if (ctx.method === "POST") {
const signInForm = new SignInForm();
signInForm.load(ctx.body);
const $user = await signInForm.login(ctx);
if ($user) {
return this.redirectTo("/", 301);
}
data.signInForm = signInForm;
}
return this.render("sign-in", data);
}
logoutAction(ctx) {
ctx.session.clearSession();
return this.redirectTo("/", 302);
}
}
Так же сразу скажу, что конструктор контроллера вызывается 1 раз и затем складывается в кеш.
Сам фреймворк еще сыроват, но на него можно посмотреть на гитхабе:
github.com/kalyuk/dok-js
Также набросал небольшой пример, там есть еще консольное приложение, котрое запускает миграции:
github.com/kalyuk/dok-js-example
Комментарии (28)
amokrushin
30.04.2017 14:16+4контроллеры и экшены, которые нам навязывает expressjs
нет в expressjs никаких контроллеров и экшенов, есть только middleware
Насчет гибкости роутера, как в этом фреймворке можно исключить парсинг кук и работу с сессией например для маршрута «GET /assets/<filePath:.*>»? В expressjs это тривиальная задача.
т.е. если вам нужно будет в середине проекта воткнуть логгер
Не понятно в чем проблема:
router.get('/user/:id',user.getById)
router.get('/user/:id', [loggerBefore, user.getById, loggerAfter])
kalyukdo
30.04.2017 14:28Ваш пример с логером хорош, но давайте не забывать сколько у вас будет копипаста, особенно если мы говорим о большем проекте, но тут уже больше дело вкуса и удобства.
Что касается отключения обработки сессии и куки, а в чем проблема, и зачем их отключать? если мы говорим про отдачу статики, так даже любую статику нужно отдавать только разрешенным пользователям, в противном случае ее можно один раз отдать и затем закешировать nginx (и тут вообще можно будет забыть про конекты к Вашему приложению)amokrushin
30.04.2017 15:54Вопрос не в том зачем отключать, а в гибкости. Не нравится пример с куками, есть аналогичный по сути пример с cors. Допустим возникла необходимость на конкретный маршрут API, либо на определенный неймспейс отдавать заголовок «Access-Control-Allow-Origin: *», в случае с expressjs вставляем middleware перед любым нужным маршрутом. Если нужно что-то по сложнее чем *, то берем готовый middleware из npm, а в вашем случае, на каждую типовую задачу придется писать свой велосипед.
kalyukdo
30.04.2017 16:01так кто мешает поставить тот-же behavior который добавит этот заголовок?
внутри экшена вы можете без проблем рулить заголовками.
Ваше замечание справедливо.
как в этом фреймворке можно исключить парсинг кук и работу с сессией
и подумать действительно стоит в эту сторону
f0rk
01.05.2017 08:52Когда-то давно писал middleware для express примерно с таким api:
const group = require('group-middleware'); group(function () { router.get('/user/:id',user.getById); router.get('...'); }) .before(auth); .after(resultFormatter);
Решает проблему копипасты?
kalyukdo
01.05.2017 11:49Интерестное решение, а как онорешает кейс:
Имеем 3 роута, перед кадым нужно выполнить миделварину со своими параметрами, затем еще два роута также с общей миделвариной, ну и затем перед всеми тремя еще одну.
Неспорю немного претянутый кейс и тут наверно больше подойдет выражение: «на вкус и цвет все фломастеры разные», но я почему склоняюсь к тому что, контроллер должен решать что и когда запускать, это субьективное мнение
jankovsky
01.05.2017 07:09-7чем не понравился react?
kalyukdo
01.05.2017 07:10+2причем тут реакт? реакт это клиент, а тут речь идет про серверный фреймворк
jankovsky
01.05.2017 07:12-10Подучите матчасть господин. React уже и на сервере рендерится.
webmasterx
01.05.2017 07:19+2рендеринг не означает что есть полноценный серверный фреймворк для работы с бд например
jankovsky
01.05.2017 07:24-10Вы хотя бы вникните в суть. На ваш комментарий подсказываю шепотом "Redux".
zxcabs
01.05.2017 11:05Боже, зачем так делать? https://github.com/kalyuk/dok-js/blob/2d283f8a967f36a569debdf6d2ef4898de632bb5/src/base/Application.js#L31
constructor(config) { super(config); global.$App = this; this.$cache.modules = {}; }
kalyukdo
01.05.2017 11:32Если вы про глобальную переменную, то она скоро будет убрана, все компоненты уже начали использовать сервис для получения инстанса приложения, осталось дорефакторить
jankovsky
01.05.2017 23:51-2Еще и в конструкторе вызывать конструктор через super() вообще не комильфо.
kalyukdo
02.05.2017 05:28+1super вызывается для того чтобы код, который был написан в конструкторе родителя, так же отработал, если вы знаете более элегантный способ вызова, дайте пример кода, или вы все также
я хотя бы понимаю о чем говорю, любезнейший
сотрясаете воздух без живых примеров и обоснований?
babylon
03.05.2017 13:02@kalyukdo если это серверный фреймворк, то в ES6 есть setPrototypeOf c помошью которого можно "плагинизировать" инстансы, заменяя наследование композицией. Всё остальное немногое убрать в ядро. Ну я бы так сделал. Дело вкуса конечно.
kalyukdo
03.05.2017 13:27Спасибо, нужно будет подумать в эту сторону, это еще очень сырое решение, ради подобных комментариев я и разместил статью, чтобы услышать критику и пожелания
webmasterx
Попахивает подтверждением фразы «На javascript каждый должен создать свой фреймворк»
И зачем вы его писали, если уже знакомы с PHP?
Чем-то похоже на Yii, но кое-что почему-то переименовано
kalyukdo
Ну для начала, php и nodejs не нужно сравнивать, у них принцип работы совсем разный, просто на nodejs мне не хватало инструментов.
>> Чем-то похоже на Yii, но кое-что почему-то переименовано
С этого фреймворка я брал удобные для меня конструкции и подходы
redfs
Вы точно не первый :) Jii чем-то не подошёл?
kalyukdo
Да, я видел, но там автор полностью переносит фреймворк, в yii2 у меня был затык в AR, с вложенными атрибутами (JSONB), для решения этой задачи я использую sequelizejs,
не удержалсяну и как было уже сказано: