Nodejs MVC framework или очередной велосипед

Привет, Хабрахабр! По какой-то причине, последнее время никого не удивляет expressjs под капотом каждого второго фреймворка на node.js, но действительно ли он нужен там? Я не говорю про то, что expressjs — это плохо, нет, он справляется со своими задачами, но когда мне понадобился роутинг сложнее чем может дать этот фреймворк, я задумался, а что есть еще в expressjs чтобы его оставить в проекте? К сожалению, кроме webserver в нем нет ничего, интеграция с шаблонизатарами — это мелочь, да и middleware сводятся к простому набору функций, кучи callback hell.

Если открыть доку по node.js и мельком посмотреть на то количество модулей, которые есть в ядре, — можно открыть много нового для себя. Как вы уже догадались, речь пойдет про очередной велосипед.

Сразу скажу, что многие финты были позаимствованы с php-фреймворков.

Зависимости, которые я все же оставил в проекте:

async, hashids, mime-types, sequelize, validator, pug

1) давайте определимся со структурой проекта:

Структура фреймворка

— dashboard — основной модуль проекта
— bin файлы для старта приложения
— config конфиги нашего приложения
— migrations миграции
— modules модули
— views основные view

Структура проекта

— base Базовые классы
— behaviors первичные бихеверы, которые могут понадобиться в 90% проектов
— console классы, которые нужны для старта приложения в консольном режиме
— helpers папка с различными хелперами
— modules модули, которые нужны в 90% проектов (миграции, рендер статики)
— web классы, нужные для работы в режиме web-приложения

2) Как запустить web приложение:

Создадим файл bin/server.js

Файл 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

./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), это я к тому, что никогда не знаешь в какой момент времени твой экшен закончил свое выполнение.

Решение, которое я предлагаю:

base/Request.js

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);     }   } 

base/Module.js

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);   } 

Т.е. мы получили единую точку запуска всех контролерров.

Ну и сам контроллер:

modules/dashboard/controllers/IndexController.js

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");   } } 

modules/identity/controllers/IdentityController.js

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
ссылка на оригинал статьи https://habrahabr.ru/post/327638/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *