mkrtcjs-core
v2.0.0
Published
Core package for MkrtcJS — includes dependency injection, metadata handling, and base architecture utilities.
Maintainers
Readme
Быстрые ссылки
Философия
MkrtcJS - это фреймворк над фреймворком nextjs. Проект вдохновлен такими фреймворками как nestjs и angularjs и так же как они использует декларативный подход в своей основе, только без излишеств.
MkrtcJS - предлагает использовать пяти ступенчатый архитектурный подход. Где:
Нулевой уровень - это провайдеры. Пример: (HttpProvider, CacheProvider, и пр.). Провайдеры это кассы нулевого уровня. Провайдеры своего рода каркас проекта. Провйдеры помечаются декоратором @Injectable() что даст другим(
сервисы, сущности, репозитории) возможность внедрят в себя провайдер.Первый уровень - это репозитории для общения с API. Все репозитории помечаются декоратором @Repository().Репозитории могут внедрять в себя провайдеров с помощью декоратора @Inject().
Второй уровень - это сущности описывающие конкретные сущности. Пример: (UserEntity, ProductEntity, и пр.). Все сущности помечаются декоратором @Entity(). Все сущности так же как и репозитории могут внедрять в себя провадйеров и репозиториев.
Третий уровень - это сервисы. Сервисы обычно создаются рядом с компонентом, и служат "головой" компонента. Сервисы отвечают за всю логику компонента, а так же управляют реактивным состоянием с помощью декораторов @State() и @UseState. Сервисы внутри себя могут внедрять как провайдеров так и репозиториев.
Четвёртый уровень - это компоненты. В mkrtcjs компоненты максимально тупые(в хорошем смысле) и не реализуют никакую логику. Любой компонент использует в себя hook useService(Service) для получения реактивного состояния а так же сервис для обработки клиентских событий(клик, ввод и пр.)
Установка
npm i mkrtcjs-coreПодключение
Чтобы обеспечить максимальную совместимость с серверными декораторами, советуем сразу из middleware.ts вернуть bootstrap. Это даст возможность на стороне сервера использовать @Req() декоратор.
// middleware.ts
import { bootstrap } from "mkrtcjs-core";
import { NextRequest } from "next/server";
export async function middleware(req: NextRequest){
return await bootstrap(req);
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}Чтобы обеспечить связь между серверной и клиентской частью приложения, оберните все что внутри <body></body> компонентом <MkrtcRootProvider />.
// layout.tsx
import { MkrtcRootProvider } from "mkrtcjs-core";
export default function RootLayout({children}: Readonly<{children: React.ReactNode;}>) {
return (
<html lang="en">
<body>
<MkrtcRootProvider>
{children}
</MkrtcRootProvider>
</body>
</html>
);
}Примеры
Клиентские декораторы
@Service()
@Service - это декоратор класса, который создаёт из обычного класса сервис.
Использование:
import { Service } from "mkrtcjs-core/client";
@Service(options)
export class MyService {}Аргументы:
options?:ServiceOptions
Декоратор @Service() позволяет объявить/обновить реактивное состояние, следить за изменениями реактивного состоянии а так же внедрять в себя репозитории и провайдеры.
Пример:
// ./services/my.service.ts
import { Service, State, UseStateFactory } from "mkrtcjs-core/client"
import { OnInit, Inject } from "mkrtcjs-core";
import { UserRepository } from "@Repositories";
export interface MyServiceState{
user: UserEntity | null;
loading: boolean;
}
const UseMyServiceState = UseStateFactory.create<MyService, MyServiceState>();
@Service()
export class MyService implements MyServiceState{
@State<UserEntity | null>(null)
public user: UserEntity | null;
@State<boolean>(false)
public loading: boolean;
@Inject(UserRepository)
private userRepo!: UserRepository;
@OnInit()
private _onInit(){
this.getUser();
}
@UseMyServiceState.return("user")
@UseMyServiceState.autoToggle("loading")
private getUser(){
return this.userRepo.init();
}
}Использование в компоненте:
// ./my.component.tsx
"use client"
import { useService } extends "mkrtcjs-core/client";
import { MyService, MyServiceState } from "./services/my.service";
export const MyComponent = () => {
const [service, {user, loading}] = useService<MyService, MyServiceState>(MyService, ["user", "loading"]);
if(loading) return <div>...loading</div>;
return (
{user && <span>{user.username}</span>}
)
}ВАЖНО: Обязательно импортируйте
useServiceизmkrtcjs-core/client. Не передавайте вuseServiceконструктор класса не обернутый в декоратор @Service()
@InjectService()
Декоратор @InjectService() - Внедрят другой сервис в совйство сервиса.
Использование
import {InjectService} from "mkrtcjs-core/client"
@InjectService(scope?: string | null, options?: InjectServiceOptions)
private readonly userService: UserService;Аргументы
scope?: string | null- Название инициализированного сервиса. Например еси в другом компоенте вы сделалиuseService(MyService, [...state], {scope: "service1"})и сейчас хотите внедрить именно этот сервис, то вам необхоимо использовать@InjectService('service1')options?: InjectServiceOptions- InjectServiceOptions
Пример
// my-another.service.ts
import {Service} from "mkrtcjs-core/client"
@Service()
export class MyAnotherService{
...
}
// my.service.ts
import {Service, InjectService} from "mkrtcjs-core/client"
@Service()
export class MyService{
@InjectService()
private readonly myAnotherService: MyAnotherService;
}@State()
@State() - Говорит сервису что данное свйоство касса является реактивым состоянием, и необходимо перерисовать компанент каждый раз, когда данное свойство меняется
использование:
import { State } from "mkrtcjs-core/client";@State(initialValue, options)
public user: UserEntity | null;Параметры:
initialValue?: T- значение по умолчанию.default = nulloptions?:- StateOptions
Пример:
import { Service, State } from "mkrtcjs-core/client";
interface MyServiceState{
loading: boolean;
}
@Service()
export class MyService implements MyServiceState{
@State<boolean>(false)
public loading: boolean;
}@UseState
Декоратор @UseState - позволяет гибко управлять реактивным состоянием. Вообще @UseState сам по себе не декоратор, а обычный объект, декоратором являются его методы, которых аж 7 штук.
ВАЖНО: Любой метод, который будет обернут декоратором @UseState будет возвращать Promise.
И так по порядку, сначала создадим 3 свойства с реактивным состоянием
@State<UserEntity | null>(null)
public user: UserEntity | null;
@State<boolean>(false)
public loading: boolean;
@State<number>(0)
public counter: number;И так, как нам взаимодействовать с состоянием?
Для начала создадим декоратор с помощью фабрики UseStateFactory. Это позволит каждый раз не передавать тип сервиса и реактивного состояния в @UseState.
import { UseStateFactory } from "mkrtcjs-core/client";
const UseUserState = UseStateFactory.create<UserService, UserServiceState>();Отличие UseUserState от обычного UseState в том, что первый уже типизирован под наш сервис.
И так, теперь рассмотрим какие методы нам дает @UseState() и как с их помощь менять реактивное состояние.
@UseService.return(key)- как уже понятно из названия, кладёт в реактивное состояниеkeyвозвращаемое значение метода. Исполльзование@UseState.return(key) // key = "hello world" public myMethod(){ return "hello world" }Параметры:
key- Название свойства куда нужно положить значение
Пример:
@UseState.return("user") public initUser(){ return this.userRepo.init(); }@UseState.before(key, updater)- Меняет рекативное состояние до вызова метода.Пример:
@UseState.before("loading", () => true) public initUser(){ return this.userRepo.init(); }Параметры:
key: string- Название ключа куда нужно положить значениеupdater:Updater - callback который должен возвращать новое значение
@UseState.after(key, afterUpdater)- Меняет рекативное состояние после вызова метода.Пример:
@UseState.after("loading", () => false) @UseState.after("user", (current, returnValue) => returnValue) public initUser(){ return this.userRepo.init(); }Параметры:
key: string- Название ключа куда нужно положить значениеupdater:AfterUpdater - callback который должен возвращать новое значение
Методы
@UseState.increment(key)и@UseState.decrement(key)- название уже говорит само за себя: increment увеличивает значение реактивного состояния на 1, decrement наоборот уменьшает на 1.ВАЖНО! Для того чтобы методы работали, значение реактивного состояния обязательно должен быть Числом, иначе выкинет ошибку
Пример:
// +1 @UseState.increment("count") public addNumber(){} // -1 @UseState.decrement("count") public subtractNumber(){}Параметры:
key: string- Ключ свойства
Методы
@UseState.toggle(key)и@UseState.autoToggle(key)- переключатель boolean значений. Отличие их в том, что.toggle()просто переключает 1 раз, а autoToggle - до вызова устанавливает значение свойства наtrueа послеfalse, удобно для всяких loader-ов.Пример:
@State(false) public modalOpen: boolean; @State(false) public loading: boolean; @UseState.toggle("modalOpen") // modalOpen = true @UseState.autoToggle("loading") // loading = before true after false public getUser(id: number){ return this.userRepo.findOnr(id); }Параметры:
key: string- Ключ свойства
Метод
@UseState.patch(key)- принимает в аргументы название свойства, и возвращает все методы которые есть у обычного @UseState кроме.patchс тем отличием, что остальным методам больше не нужно указатьkey.Пример:
@(UseState.patch("loading").autoToggle()) // Если вырражения декоратора состоят более одного метода, нужно весь декоратор обернуть скобакми @UseState.return("user") public initUser(){ return this.userRepo.init(); }Параметры:
key: string- Ключ свойства
Пример:
@UseUserState.increment("counter") // При каждом вызове, counter += 1
@UseUserState.autoToggle("loading") // до вызова true, после - false
@UseUserState.return("user") // кладем то что вернётся метод, в "user"
public async getUser(id: number){
return await this.userRepo.findOne(id);
}
@(UseUserState.patch("counter").increment())
// что тут произошло? Мы с помощью дженерика указываем тип аргументов, в виде массив. В аргументы callback функции к нам падают Текущее значение, экземпляр текущего класса(сервиса) И аргументы с дженерик типом в виде массива
@UseUserState.before<[number, string]>("loading", (currentValue, instance, [arg1, arg2]) => true)
@UseUserState.after("loading", () => false)
// Тут вторым дженериком мы указываем тип возвращаемого значения, который после к нам попадет вторым аргументом в updater
@UseUserState.after<[number, string], UserEntity>("user", (cur, rv) => rv)
public testBeforeAfter(arg1: number, arg2: string): UserEntity {
// code
}@Watch()
@Watch() - это декоратор, который вызывает метод, при изменении конкретного рекативного свойства.
использование:
import { Watch } from "mkrtcjs-core/client";
@Watch(key)
private watch(key, next, prev){}Аргументы:
key: string | string[] | "*"- название стейтов за которыми нужно следить. Еслиkey = "*"то будет следить за всеми свойствами.
Аргументы в методе:
key: string- Ключ стейта на котором произошло изменение.next: T- новое значение стейтаprev?: T- старое значение стейта
ВАЖНО: @Watch() будет вызвать метод всегда, даже если в реативное состояние положили одно и то же значение.
ЗАМЕТКА: Старайтесь максимально избегать использования
@Watch("*"). Это может повлиять на производительность, особенно если состояний много.
@UseEffect()
@UseEffect работает идентично @Watch() с тем отличием, что @UseEffect будет вызвать метод, только если новое значение реактивного состояния отличается от старого.
@Timer()
Декоратор @Timer() как уже понятно из название создает таймер.
Использование:
import { Timer } from "mkrtcjs-core/client";
@Timer(key, options)
public method(){}key: string;- Название стейтаoptions:TimerOptions - Настройки таймера
Перед использованием @Timer() обязательно обявите реактивное состояние со значением
isTimer: true
Пример:
import { State, Timer } from "mkrtcjs-core/client";
import type { ITimer } from "mkrtcjs-core/types";
@State<ITimer | null>(null, {isTimer: true})
public timer: ITimer;
@Timer("timer", {ms: 5000, tickRate: 1000})
public sendSmsCode(timer: Timer, ...args){}ВАЖНО!!! Не важно какое вы укажите значение по умолчанию для свойства, при использовании
@Timerзначение свойства будет переопределен на ITimer
@OnPathChange()
Декоратор @OnPathChange - Вызывает метод каждый раз когда url путь меняется. Метод всегда принимает 1 параметр - pathname;
Использование:
import { OnPathChange } from "mkrtcjs-core/client";
@OnPathChange()
private _onPathChange(pathname: string){}@UseNavigator()
Декоратор @UseNavigator() дает возможность использовать в аргументах метода следующие декораторы:
@Router()- Значение из netxjs/useRouter();@Pathname()- Значение из nextjs/usePathname();
Использование:
import { UseNavigator, Router, Pathname } from "mkrtcjs-core/client";
@UseRouter()
public method(@Router() router: AppRouterInstance, @Pathname() pathname: string){}ВАЖНО: Декоратор
@UseNavigator()не будет вызвать метод при каждом изменении url пути. Учтите это при разработке.
Общие декораторы
@Injectable()
Декоратор @Injectable() внутри себя создаёт экземпляр класса и дальше передаётся через декоратор @Inject()
Ограничения:
- Класс обернутый декоратором @Injectable() в конструктор может принимать только другие классы обернутые декоратором @Injectable() Советы:
- Оберните декоратором Injectable() только провайдеры и репозитории, а так же все остальные классы, которые могут быть Single tone.
Использование:
// ./http-client.ts
import { Injectable } from "mkrtcjs-core";
@Injectable()
export class HttpClient{
// you code
}@Inject()
Декоратор @Inject() - внедряет класс, в свойство класса.
ВАЖНО: Конструктор класса который необходимо внедрить, обязательно должен быть обернутым декоратором @Injectable(), иначе внедрение не произойдёт.
Использование:
// ./user.repository.ts
import { Repository, Injectable, Inject } from "mkrtcjs-core";
import { HttpClient } from "@/shared";
import { UserEntity, IUserEntity } from "@/entities";
@Repository()
@Injectable()
export class UserRepository{
@Inject()
private readonly httpClient: HttpClient;
public async findAll(): Promise<UserEntity[]>{
const users = await this.httpClient.get<IUserEntity>("/user");
return users.map(user => new UserEntity(user));
}
}@Repository()
Декоратор @Repository() - говорит классу о том, что он может в себя внедрять все классы помеченные декоратором @Injectable(). Пример здесь. Может использоваться в связке с декоратором @Injectable().
@Entity()
Декоратор @Entity() - говорит классу о том, что он может в себя внедрять все классы помеченные декоратором @Injectable(). Не может использоваться в связке с декоратором @Injectable().
Пример:
// ./user.entity.ts
import { Entity, Inject } from "mkrtcjs-core";
import type { IUserEntity } from "./user.interface.ts";
import { UserRepository } from "@/repositories"
@Entity()
export class UserEntity implements IUserEntity{
public readonly id: number;
public name: string;
// ...
@Inject(UserRepository)
private readonly userRepository: UserRepository;
constructor(user: IUserEntity){
Object.assign(this, user);
}
public async save(): Promise<UserEntity>{
return await this.userRepository.update(this.id, this);
}
}@OnInit()
Декоратор @OnInit() вызывает метод при инициализации сервиса.
Пример:
import { OnInit } from "mkrtcjs-core";
@OnInit()
private async _onInit(){
await this.getUsers();
}Теперь при первом использовании useService() будет вызван _onInit() метод.
ЗАМЕТКА!!! Название метода может быть любым.
ЗАМЕТКА!!! Рекомендуем сделать метод приватным.
ВАЖНО!!! Метод вызовется только при инициализации сервиса. Т.е. если вы в родительском классе внедрили сервис с помощью хука [[#useService()]], а дальше в дочернем компонент тоже пытаетесь внедрить сервис, то инициализация не произойдёт, поскольку сервис уже инициализирован в родительском компоненте, а в дочернем компоненте вы его получите из DI контейнера.
@Catch()
Декоратор @Catch() автоматом оборачивает метод в try/catch.
Использование:
import { Catch } from "mkrtcjs-core";
@Catch<Instance, args[], Error>(handler: CatchHandle<Instance, args[], Error>, options?: CatchOptions)
public method(){
if(1 !== 2) throw new Error();
}handler:CatchHandle - callback функция, которая отработает в случае ошибки.options?:CatchOptions - Настройки
Хуки
useService()
Хук useService нужен для того, чтобы внедрить сервис в компонент.
Использование
import {useService} from "mkrtcjs-core/client";
useService<ServiceClass, ServiceState>(service: ServiceClass, state: ServiceState, options?: UseServiceOptions)Аргументы
service: ServiceClass- Конструктор сервиса.state: ServiceState- Тип рекативного состояния.options?: UseServiceOptions- UseServiceOptions. Настройки.
Пример
// user.service.ts
import { Service, State, UseStateFactory } from "mkrtcjs-core/client";
import { Inject, OnInit } from "mkrtcjs-core";
import { UserRepository } from "@/repositories";
import { UserEntity } from "@/entities";
const UseState = UseStateFactory.create<UserService, UserState>();
export interface UserState{
loading: boolean;
users: UserEntity[];
}
export class UserService implements UserState{
@State(false)
public loading: boolean;
@State([])
public users: UserEntity[];
@Inject()
private readonly userRepo: UserRepository;
@OnInit()
private _onInit(){
this.findAllUsers();
}
@UseState.autotoggle("loading")
@UseState.return("users")
private async findAlllUsers(){
return await userRepo.findAll();
}
@UseState.autotoggle("loading")
public sayHello(user: UserEntity){
console.log(`user: ${user.name} say hello`);
}
}
// use-user-service.hook.ts
import { UseServiceFactory } from "mkrtjs-core/client";
import { UserService, UserState } from "./user.service";
export const useUserService = UseServiceFactory.create<UserService, UserState>(UserService, {isGlobal: false, scope: "alternativeService"});
// UserComponent.tsx
import { useService } from "mkrtcjs-core/client";
import { useUserService } from "./use-user-service.hook";
import { UserService, UserState } from "./user.service";
export const UserComponent = () => {
const [service, {users, loading}] = useService<UserService, UserState>(UserService, ["loading", "users"]);
// or
const [service, {users, loading}] = useUserService(["loading", "users"]);
return (
{loading ?
<div>loading...</div> :
<ul>
{users.map(user => (
<li onClick={() => service.sayHello(user)} key={user.id}>{user.name}</li>
))}
</ul>
}
)
}Типы
ServiceOptions
interface ServiceOptions {
isGlobal?: boolean;
}isGlobal?: boolean- Будет ли сервис глобальный или нет. Глобальный сервис не удаляется при демонтировании родительского компонента, либо при отсутствии владельцев. Свойство можно переопределить в useService()
StateOptions
interface StateOptions{
isTimer?: boolean;
timerOptions?: StateTimerOptions
}isTimer?: boolean- еслиtrue, то @State() будет иметь тип ITimer.timerOptions?:StateTimerOptions - Настройки таймера. Будет работать только еслиisTimer = true.
StateTimerOptions
interface StateTimerOptions{
delay?: number;
}delay: number- через сколько миллисекунд запустить таймер.
ITimer
interface ITimer {
completed: boolean;
left: number;
ms: number;
}ms: number- Насколько миллисекунд был запущен таймер.left: number- Сколько миллисекунд осталось до завершения таймера.completed: boolean- Запрещён ли отчёт таймера.
TimerOptions
interface TimerOptions {
tickRate: number;
ms: number;
onTick?: (timer: ITimer) => void;
}tickRate: number;- Раз в сколькоmsвызвать ре-рендер компонента.ms: number;- Насколькоmsзапустить таймерonTick?: (timer: ITimer) => void;- callback который будет вызван каждыйtickRate.
Updater
type Updater<S, C, A> = (current: S, args: A, instance: C) => S;current: T- Текущее значение состояния.args: A[]- Аргументы метода.instance: C- Экземпляр текущего сервиса.
AfterUpdater
type AfterUpdater<S, R, C, A> = (current: S, returnValue: R, args: A, instance: C) => S;current: T- Текущее значение состояния.returnValue- Возвращаемое значение метода.args: A[]- Аргументы метода.instance: C- Экземпляр текущего сервиса.
CatchHandle
interface CatchHandle<I, A, E extends Error> {
instance: I;
args: A;
exp: E;
}instance: I;- экземпляр класса где используется декораторargs: A[];- аргументы методаError: E;- тип ошибки который может выкинуть метод
CatchOptions
interface CatchOptions {
rethrow?: boolean;
useReturn?: boolean;
}useReturn: boolean;- еслиtrue, то метод вернет callback при ошибкеreThrow: boolean;- еслиtrue, то выкинет ошибку наружу
InjectServiceOptions
interface InjectServiceOptions{
init?: boolean;
}init?: boolean- Укажитеtrueесли не уверены, что сервис будет инициализирован до инициаллизации текущего сервиса. Тогда, перед внедреием, пройдет провека и если сервис не будет инициализирован, то произойдет его инициализация.
ВЖНО - Если не указать значение
trueи при инициализации текущего касса, внедряемый сервис не будет инициализирован, то выкинется ошибкаService [ServiceName] not inited.
UseServiceOptions
interface UseServiceOptions {
scope?: string;
isGlobal?: boolean;
}scope?: string- Название сервиса. Укажите если хотите несколько раз использовать useService() и чтобы у всех было свое реактивное состояние. Если укажите, то при использовании декоратора @InjectService необходимо будет первым параметром передатьscope.isGlobal?: boolean- Будет ли сервис глобальным. По умолчанию:false.
