Начало

Давным давно, в далекой галактике, при использовании NodeJS джедаю стало сложно следить за кодом и зависимостями.

Но он помнил, он видел магию автоматического внедрения зависимостей, так как был в клане Angular.

И вот, молодой джедай, воодушевленный могуществом TypeScript решил познать черную магию декораторов, он хотел понять как эта магия работает…

И пошел странствовать в поисках мудрости — пошел в гугл, попал на хабр, все узнал.

Конец.

Прелюдия

По моему мнению на чистом JavaScript слишком неудобно писать сложные приложения, и TypeScript позволяет значительно упростить эту задачу.

Меня заинтересовала возможность получения типов параметров конструктора, и пока экспериментировал с декораторами, у меня получился вполне рабочий инжектор зависимостей, и было решено использовать это во благо.

Озарение

Быстро появилось представление, о том как можно упростить написание бекенда на ноде, в результате получилось довольно красивое описание класса приложения:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Application()
export class App {
    // Предварительная настройка
    constructor(public server?: HttpServer,
                public db?: MongoDB,
                public service?: MyService) {
        server.use(bodyParser.json());
    }

    // Подготовка правил роутинга
    initRoutes() {
        this.server.registerController('/some/', TestController);
    }

    // Запуск
    async run(port: number) {
        await this.server.listen(port);
        console.log(`Server successfully started on ${port} port`);
    }

    // Завершение
    stop() {
        this.server.close();
    }
}

Dependency Injection через конструктор, для упрощения тестирования. Минусом является то, что в конструкторе не укажешь интерфейс, а если и укажешь то надо будет явно указывать зависимость, поэтому в тестах приходилось некоторые тестовым зависимостям некрасиво явно присваивать тип any настройка роутинга и контроллеров в классе приложения а не в самом контроллере. Все роуты подключаются в одном месте, что позволяет легко найти нужный контроллер любого роута или поменять пути класс приложения не связан с http сервером, что позволяет, например, слушать на разных портах или же вообще не пользоваться http сервером

В результате стартовый скрипт выглядел довольно просто

1
2
3
4
const app = new App();
app.db.connect('mongodb://user:pwd@mongo:27017/dbname');
app.initRoutes();
app.run(80);

Собственно даже комментировать нечего. В дальнейшем, подключение к базе данных лучше выполнять в async методе класса Application.

Контроллер

Практически всегда разные группы обработчиков запросов клиентов имеют общую кодовую базу и общие зависимости, поэтому логично объединить их в один класс, и через декоратор прописать детали:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Controller()
export class TestController {
    constructor(private service: MyService) { }

    @Action('get', '/')
    actionIndex(req: express.Request, res: express.Response) {
        res.json({
            action: 'index',
        });
    }

    @Action(['get', 'post'], '/view')
    actionView(req: express.Request, res: express.Response) {
        res.json({
            action: 'view',
        });
    }
}

Учитывая строку this.server.registerController('/’, TestController); такое приложение будет обрабатывать следующие пути:

GET /some/

GET /some/view

POST /some/view

Удобно, пути в пределах контроллера прописываются рядом с обработчиком, сами контроллеры прописываются централизованно, под капотом — уже знакомый express. Ну и конечно же DI

Сервис

Сервисы я хотел что бы были тоже с автоматическим внедрением зависимостей (да, да, опыт разработки на Angular сказывается):

1
2
3
4
5
6
7
8
@Service()
export class MyService {
    constructor(private http: HttpClient) {}

    async getContent(url: string): Promise {
        return await this.http.get(url);
    }
}

Темная сторона силы

Нормального способа разрешать циклические зависимости я не нашел, однако циклические зависимости — антипаттерн так что это особо и не требуется.

А вот отследить циклическую зависимость надо, но, к сожалению, достоверно не получится в рамках текущей концепции декораторов.

Для работы фреймворка требуются некоторый настройки в tsconfig.json

1
2
3
4
5
6
7
{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Декораторы работают иначе если компилировать в ES3 и ниже, поэтому в опциях компилятора target требуется указывать не ниже ES5.

Также для работы фреймворка требуются Reflect.