Привет, Хабрахабр! По какой-то причине, последнее время никого не удивляет expressjs под капотом каждого второго фреймворка на node.js, но действительно ли он нужен там? Я не говорю про то, что expressjs — это плохо, нет, он справляется со своими задачами, но когда мне понадобился роутинг сложнее чем может дать этот фреймворк, я задумался, а что есть еще в expressjs чтобы его оставить в проекте? К сожалению, кроме webserver в нем нет ничего, интеграция с шаблонизатарами — это мелочь, да и middleware сводятся к простому набору функций, кучи callback hell.
Если открыть доку по 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-example
ссылка на оригинал статьи https://habrahabr.ru/post/327638/
Добавить комментарий