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

في النسخة الأولى ، كان منطق النص النصي بدائيًا.
- .
- karcass template .
- , ( - , ) ( ).
- ,
npm install
.
:

. , .
, : Application , , . , :
Application.tsimport 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
public localCacheService!: LocalCacheService
public loggerService!: LoggerService
public dbService!: DbService
public gitlabService!: GitlabService
public issueService!: IssueService
public projectService!: ProjectService
public groupService!: GroupService
public userService!: UserService
public helpCommand!: HelpCommand
public createMigrationCommand!: CreateMigrationCommand
public migrateCommand!: MigrateCommand
public migrateUndoCommand!: MigrateUndoCommand
public updateProjectsCommand!: UpdateProjectsCommand
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.tsimport { 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.tsimport 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.tsimport { 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
— . , , , :

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