📜 ⬆️ ⬇️

Nodejs MVC framework or regular bike

Hi, Habrahabr! For some reason, lately no one is surprised by expressjs under the hood of every second framework on node.js, but is it really needed there? I'm not talking about the fact that expressjs is bad, no, it does its job, but when I needed routing more difficult than this framework can give, I wondered what else is in expressjs to leave in the project? Unfortunately, besides the webserver there is nothing in it, integration with template engines is a trifle, and middleware comes down to a simple set of functions, heaps of callback hell.

If you open the dock for node.js and glance at the number of modules that are in the kernel, you can discover a lot of new things for yourself. As you may have guessed, we will talk about the next bike.

I must say that many feints were borrowed from php-frameworks.

Dependencies that I left in the project:
')
async, hashids, mime-types, sequelize, validator, pug

1) let's define the structure of the project:

Framework structure
- dashboard - the main module of the project
- bin files to start the application
- config our application configs
- migrations migrations
- modules modules
- views main view

Project structure
- base base classes
- behaviors primary behaviorists that may be needed in 90% of projects
- console classes that are needed to start the application in console mode
- helpers folder with various helpers
- modules modules that are needed in 90% of projects (migration, render statics)
- web classes, necessary for work in a web application mode

2) How to start a web application:

Create a file bin / server.js

File 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; 


After that, our application will try to load the confing from ./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") } } } }; } 


Here we are at the moment that the expressjs did not give me to use the routes. As you can see, the current version of the routes is very flexible and allows you to fine-tune the application, here I took ideas from yii2.

Now the pain is number two: controllers and actions that are imposed on us by expressjs and most of the nodejs frameworks. This is usually an anonymous function (I understand that it is necessary for performance), which receives request and response as input and does everything with them, i.e. if you need to insert a logger in the middle of a project, for example, for logging all responsions, be kind to refactor almost the entire application, and God forbid to skip the callback call that does next (request, response), I mean, you never know at which point in time your action finished its execution.

The solution I propose:

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


Those. we got a single launch point for all controllers.

Well, the controller itself:

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


I also immediately say that the controller constructor is called 1 time and then added to the cache.

The framework itself is still damp, but you can look at it on the githaba:

github.com/kalyuk/dok-js

I also sketched out a small example, there is also a console application that starts the migrations:

github.com/kalyuk/dok-js-example

Source: https://habr.com/ru/post/327638/


All Articles