يمكن أن يكون إطار جافا سكريبت آخر.

في الصيف الماضي ، أثناء إعداد مقالة لـ Habr ، لم أكن كسولًا جدًا لتعبئة النموذج الخاص بي لتطبيقات الواجهة الخلفية على Node.js في حزمة npm ، مما يجعله أداة مساعدة لبدء سريع.


لم يكن هناك أمل في أن يستخدم أي شخص آخر هذه الحزمة في البداية. ومع ذلك ، عندما قررت تحديث القالب من خلال تقديم الميزات التي أحتاجها ، لاحظت أن حزمة npm تحتوي على عشرات التنزيلات في الأسبوع ، والمشروع على github يحتوي على 12 نجمة. سلمت بلطف من قبل الناس الطيبين ، بالتأكيد لدعم لي ، وليس للمشروع. فقط 12 نجمة ، ولكن كان كافياً بالنسبة لي أن أقرر أنني سأطور الهيكل كأنه ضروري ليس فقط بالنسبة لي.


على الرغم من حقيقة أنني قررت في البداية إنشاء إطار خفيف الوزن لتطبيقات الواجهة الخلفية ، تمكنت خلال عملية التطوير من إقناع نفسي بأن هذه الدراجة ليست ضرورية. ولا ينبغي أن يكون هيكل السيارة إطارًا ، بل أداة عالمية لإنشاء التطبيقات من القوالب.


صورة


في النسخة الأولى ، كان منطق النص النصي بدائيًا.


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

:


صورة


. , .


, : 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 . , .


صورة


, 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

— . , , , :


صورة


قررت ألا أقوم بجولة طويلة مع وصف لجميع ميزات الكاركاس ، ولكن لتقديم ملاحظة صغيرة لتقصي الحقائق لغرض العرض وجمع التعليقات: ستكون أي ملاحظات مفيدة بالنسبة لي لفهم إلى أين أذهب بعد ذلك وما إذا كان الأمر يستحق ذلك. في هذه الأثناء ، تعد كتابة الوثائق أولوية.


مستودع الذبيحة في جيثب ؛

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


All Articles