Podría ser otro marco de JavaScript.

El verano pasado, en el proceso de preparación de un artículo para Habr, no fui demasiado flojo para empacar mi plantilla para aplicaciones de back-end en Node.js en un paquete npm, lo que la convirtió en una utilidad cli para un inicio rápido.


No había esperanza de que alguien más que yo usara este paquete inicialmente. Sin embargo, cuando decidí actualizar la plantilla introduciendo las características que necesitaba, noté que el paquete npm tiene varias docenas de descargas por semana, y el proyecto en el github tiene 12 estrellas. Entregado con amabilidad por buena gente, seguramente para apoyarme, no el proyecto. Solo 12 estrellas, pero fue suficiente para mí decidir que desarrollaré karcass como si fuera necesario no solo para mí.


A pesar de que inicialmente decidí crear un marco ligero para aplicaciones de back-end, durante el proceso de desarrollo logré convencerme de que esta bicicleta no era necesaria. Y ese karcass no debería ser un marco, sino una herramienta universal para crear aplicaciones a partir de plantillas.


imagen


En la primera versión, la lógica del cli-script era primitiva.


  1. .
  2. karcass template .
  3. , ( - , ) ( ).
  4. , npm install.

:


imagen


. , .


, : Application , , . , :


Application.ts
import Express from 'express'
import { AbstractConsoleCommand } from './Base/Console/AbstractConsoleCommand'
import { DbService } from './Database/Service/DbService'
import { HelpCommand } from './Base/Console/HelpCommand'
import { LoggerService } from './Logger/Service/LoggerService'
import { IssueService } from './Project/Service/IssueService'
import { GitlabService } from './Gitlab/Service/GitlabService'
import { LocalCacheService } from './Base/Service/LocalCacheService'
import { ProjectService } from './Project/Service/ProjectService'
import { GroupService } from './Project/Service/GroupService'
import { UserService } from './User/Service/UserService'
import { UpdateProjectsCommand } from './Gitlab/Console/UpdateProjectsCommand'
import { CreateMigrationCommand } from './Database/Console/CreateMigrationCommand'
import { MigrateCommand } from './Database/Console/MigrateCommand'
import { MigrateUndoCommand } from './Database/Console/MigrateUndoCommand'
import IssueController from './Project/Controller/IssueController'
import fs from 'fs'

export class Application {
    public http!: Express.Express

    // Services
    public localCacheService!: LocalCacheService
    public loggerService!: LoggerService
    public dbService!: DbService
    public gitlabService!: GitlabService
    public issueService!: IssueService
    public projectService!: ProjectService
    public groupService!: GroupService
    public userService!: UserService

    // Commands
    public helpCommand!: HelpCommand
    public createMigrationCommand!: CreateMigrationCommand
    public migrateCommand!: MigrateCommand
    public migrateUndoCommand!: MigrateUndoCommand
    public updateProjectsCommand!: UpdateProjectsCommand

    // Controllers
    public issueController!: IssueController

    public constructor(public readonly config: IConfig) {
        if (config.columns.length < 2) {
            throw new Error('There are too few columns :-(')
        }
    }

    public async run() {
        this.initializeServices()
        if (process.argv[2]) {
            this.initializeCommands()
            for (const command of Object.values(this)
                .filter((c: any) => c instanceof AbstractConsoleCommand) as AbstractConsoleCommand[]
            ) {
                if (command.name === process.argv[2]) {
                    await command.execute()
                    process.exit()
                }
            }
            await this.helpCommand.execute()
            process.exit()
        } else {
            this.runWebServer()
        }
    }

    protected runWebServer() {
        this.initCron()
        this.http = Express()
        this.http.use('/', Express.static('vue/dist'))
        this.http.use((req, res, next) => {
            if (req.url.indexOf('/api') === -1) {
                res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate')
                res.header('Expires', '-1')
                res.header('Pragma', 'no-cache')
                return res.send(fs.readFileSync('vue/dist/index.html').toString())
            }
            next()
        })
        this.http.use(Express.urlencoded())
        this.http.use(Express.json())
        this.http.listen(this.config.listen, () => console.log(`Listening on port ${this.config.listen}`))

        this.initializeControllers()
    }

    protected initCron() {
        if (this.config.gitlab.updateInterval) {
            setInterval(async () => {
                if (!this.updateProjectsCommand) {
                    this.updateProjectsCommand = new UpdateProjectsCommand(this)
                }
                await this.updateProjectsCommand.execute()
            }, this.config.gitlab.updateInterval * 1000)
        }
    }

    protected initializeServices() {
        this.localCacheService = new LocalCacheService(this)
        this.gitlabService = new GitlabService(this)
        this.loggerService = new LoggerService(this)
        this.dbService = new DbService(this)
        this.issueService = new IssueService(this)
        this.projectService = new ProjectService(this)
        this.groupService = new GroupService(this)
        this.userService = new UserService(this)
    }

    protected initializeCommands() {
        this.helpCommand = new HelpCommand(this)
        this.createMigrationCommand = new CreateMigrationCommand(this)
        this.migrateCommand = new MigrateCommand(this)
        this.migrateUndoCommand = new MigrateUndoCommand(this)
        this.updateProjectsCommand = new UpdateProjectsCommand(this)
    }

    protected initializeControllers() {
        this.issueController = new IssueController(this)
    }

}

ProjectService.ts
import { AbstractService } from '../../Base/Service/AbstractService'
import { Project } from '../Entity/Project'

export class ProjectService extends AbstractService {

    public get projectRepository() {
        return this.app.dbService.connection.getRepository(Project)
    }

    public async updateProjects(allTime = false) {
        await this.app.groupService.updateGroups()
        for (const data of await this.app.gitlabService.getProjects()) {
            let project = await this.getProject(data.id)

            if (!project) {
                project = this.projectRepository.create({ id: data.id })
            }
            project.name = data.name
            project.url = data.web_url
            project.updatedTimestamp = Math.round(new Date(data.last_activity_at).getTime() / 1000)
            project.groupId = data.namespace && data.namespace.kind === 'group' ? data.namespace.id : null
            await this.projectRepository.save(project)
            await this.app.issueService.updateProjectIssues(project, allTime)
        }
    }

    public async getProject(id: number): Promise<Project|undefined> {
        return id ? this.app.localCacheService.get(`project.${id}`, () => this.projectRepository.findOne(id)) : undefined
    }

}

, , , . , DI-, cli.


Application.ts , « ». .


Application.ts
import CreateExpress, { Express } from 'express';
import { TwingEnvironment, TwingLoaderFilesystem } from 'twing';
import { Container } from '@karcass/container';
import { Cli } from '@karcass/cli';
import { Connection, createConnection } from 'typeorm';
import { CreateMigrationCommand, MigrateCommand, MigrateUndoCommand } from '@karcass/migration-commands';
import { createLogger } from './routines/createLogger';
import { Logger } from 'winston';
import { FrontPageController } from './SampleBundle/Controller/FrontPageController';
import { Message } from './SampleBundle/Entity/Message';
import { MessagesService } from './SampleBundle/Service/MessagesService';

export class Application {
    private container = new Container();
    private console = new Cli();
    private controllers: object[] = [];
    private http!: Express;

    public constructor(public readonly config: IConfig) { }

    public async run() {
        await this.initializeServices();

        if (process.argv[2]) {
            this.initializeCommands();
            await this.console.run();
        } else {
            this.runWebServer();
        }
    }

    protected runWebServer() {
        this.http = CreateExpress();
        this.http.use('/public', CreateExpress.static('public'));
        this.http.use(CreateExpress.urlencoded());
        this.http.listen(this.config.listen, () => console.log(`Listening on port ${this.config.listen}`));

        this.container.add<Express>('express', () => this.http);
        this.container.add(TwingEnvironment, () => new TwingEnvironment(new TwingLoaderFilesystem('src')));

        this.initializeControllers();
    }

    protected async initializeServices() {
        await this.container.addInplace<Logger>('logger', () => createLogger(this.config.logdir));
        const typeorm = await this.container.addInplace(Connection, () => createConnection({
            type: 'sqlite',
            database: 'db/sample.sqlite',
            entities: ['build/**/Entity/*.js'],
            migrations: ['build/**/Migrations/*.js'],
            logging: ['error', 'warn', 'migration'],
        }));
        this.container.add('Repository<Message>', () => typeorm.getRepository(Message));
        this.container.add(MessagesService);
    }

    protected initializeCommands() {
        this.console.add(CreateMigrationCommand, () => new CreateMigrationCommand());
        this.console.add(MigrateCommand, async () => new MigrateCommand(await this.container.get(Connection)));
        this.console.add(MigrateUndoCommand, async () => new MigrateUndoCommand(await this.container.get(Connection)));
    }

    protected async initializeControllers() {
        this.controllers.push(
            await this.container.inject(FrontPageController),
        );
    }

}

TypeScript :


FrontPageController.ts
import { Express } from 'express';
import { Dependency } from '@karcass/container';
import { TwingEnvironment } from 'twing';
import { AbstractController, QueryData } from './AbstractController';
import { MessagesService } from '../Service/MessagesService';

export class FrontPageController extends AbstractController {

    public constructor(
        @Dependency('express') protected express: Express,
        @Dependency(TwingEnvironment) protected twing: TwingEnvironment,
        @Dependency(MessagesService) protected messagesService: MessagesService,
    ) {
        super(express);

        this.onQuery('/', 'get', this.frontPageAction);
        this.onQuery('/', 'post', this.sendMessageAction);
    }

    public async sendMessageAction(data: QueryData) {
        await this.messagesService.addMessage(data.params.text);
        data.res.redirect('/');
    }

    public async frontPageAction() {
        if (await this.messagesService.isEmpty()) {
            await this.messagesService.createSampleMessages();
        }
        return this.twing.render('SampleBundle/Views/front.twig', {
            messages: await this.messagesService.getMessages(),
        });
    }

}

JavaScript, «»:


protected async initializeControllers() {
    this.controllers.push(
        new FrontPageController(
            await this.container.get('express'),
            await this.container.get(TwingEnvironment),
            await this.container.get(MessagesService),
        ),
    );
}

template karcass, : . , .


: TemplateReducer.ts TemplateReducer.js, TemplateReducer, :


interface TemplateReducerInterface {
    getConfigParameters(): Promise<ConfigParametersResult>
    getConfig(): Record<string, any>
    setConfig(config: Record<string, any>): void
    getDirectoriesForRemove(): Promise<string[]>
    getFilesForRemove(): Promise<string[]>
    getDependenciesForRemove(): Promise<string[]>
    getFilesContentReplacers(): Promise<ReplaceFileContentItem[]>
    finish(): Promise<void>
    getTestConfigSet(): Promise<Record<string, any>[]>
}

, , , - , karcass , JavaScript/TypeScript. -. - webpack. , - create-react-app ... , vue create.


TemplateReducerInterface, , . karcass:


hello index.js :


console.log('Hello, [replacethisname]!')

TemplateReducer.js, karcass' :


const reducer = require('@karcass/template-reducer')
const Type = reducer.ConfigParameterType

class TemplateReducer extends reducer.AbstractTemplateReducer {
    getConfigParameters() {
        return [
            { name: 'name', description: 'Please enter your name', type: Type.string },
        ]
    }
    async getFilesContentReplacers() {
        return [
            { filename: 'index.js', replacer: (content) => {
                return content.replace('[replacethisname]', this.config.name)
            } },
        ]
    }
    async finish() {
        console.log(`Application installed, to launch it execute\n  cd ${this.directoryName} && node index.js`)it.`)
    }
}
module.exports = { TemplateReducer }

, , , — @karcass/template-reducer, , package.json:


npm init && npm install @karcass/template-reducer

, getDependenciesForRemove, karcass .


karcass . , .


imagen


, karcass . github.com, :


npx karcass create helloworld https://github.com/karcass-ts/hello-world

, -, :


npx karcass create ooohhhh-ok-show-it

? , TemplateReducer helloworld- :


    getTestConfigSet() {
        return [
            { name: 'testname1' },
            { name: 'testname2' },
        ]
    }

:


npx karcass test www/karcass/hello

— . , , , :


imagen


Decidí no hacer un recorrido largo con una descripción de todas las características del karcass, sino hacer una pequeña nota de investigación con el propósito de presentar y recopilar comentarios: cualquier comentario me será útil para comprender a dónde ir a continuación y si vale la pena. Mientras tanto, escribir documentación es una prioridad.


Repositorio de Karcass en github ;

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


All Articles