Ссылки
Прежде чем я начну это краткое изложение «Дао нода» Александра Кондова, на случай, если вам нужен первоисточник большей части того, о чем я говорю, они следуют здесь:
- Дао узла
- Что такое петля событий? — Филип Робертс
- Внутри цикла событий — Джейк Арчибальд
С помощью этих ссылок, я думаю, у нас будет некоторая основа для того, о чем мы будем говорить сегодня в этом блоге.
В качестве образца я использовал вот этот проект
В начале
Независимо от того, какой проект вы будете делать в node, давайте поговорим немного о философии, в node у нас есть идея, что малое прекрасно, только то, что необходимо. Минимализм. Что это дает? У нас есть небольшие пакеты или модули, которые делают что-то очень хорошо и, вероятно, поддерживаются сообществом. Да, NPM или Yarn — это то, что является частью философии Node, и его пакеты несут это с собой. Express является самым большим примером и, в свою очередь, почти синонимом node, TypeScript — буквально JavaScript со специями — также очень хорошо принят… React и многие другие — это просто JS со специями, но очень хорошо сделанными специями.
Настройка
Очевидно, что при создании проекта в 2022 году, мы будем использовать TypeScript, который является решением для работы с увеличением нашей кодовой базы, мы также будем использовать fastify, больше по собственному желанию, поскольку нам нравится их философия и некоторые вещи из коробки, но express по-прежнему является отличным фреймворком/либом node.
Я также хотел бы отметить, что по предпочтениям я использую MongoDB, но это больше относится к тому, как будет храниться информация, чем к тому, как структурирован ваш код.
Каждая модель или домен приложения должны иметь свой собственный каталог и следовать туда со своими сложностями, оставляя самую простую и легкую визуализацию. В примере мы имеем только два домена в нашем приложении petshop — Pets и Customers:
Контроллеры
Когда мы говорим о контроллерах, они являются нашим фронт-эндом, где фронт-энд обращается, спрашивает или просто касается, это наш API. Когда вы думаете об API, он должен быть простым, но в то же время эффективно выполнять свою работу, делать то, что вам нужно. В таком виде мой фронтенд Customer выглядит следующим образом:
export async function CustomerController(fastify: FastifyInstance) {
const customerService = CustomerService(fastify);
const petService = PetService(fastify);
fastify.get<{ Reply: Array<CustomerSchema> }>
('/customers',
async (
request: FastifyRequest, reply: FastifyReply
) => {
const result = await customerService.getAllCustomers()
if (result.length === 0) {
reply.status(404);
throw new Error('No documents found')
}
reply.status(200).send(result);
});
fastify.get<{ Params: { customerID: string }, Reply: CustomerSchema }>
('/customers/:customerID',
async (
request: FastifyRequest<{ Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const result = await customerService.getCustomerById(customerID);
if (!result) {
reply.status(404).send(customerID);
throw new Error('Invalid value');
}
reply.status(200).send(result);
});
fastify.get<{ Params: { customerID: string }, Reply: CustomerSchema }>
('/customers/:customerID/pets',
async (
request: FastifyRequest<{ Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const customer = await customerService.getCustomerById(customerID);
if (!customer) {
reply.status(404).send('Invalid user id');
throw new Error('Invalid user id');
}
if (customer.pets === undefined || customer.pets?.length === 0) {
reply.status(400).send('No pets were added');
throw new Error('No pets were added');
}
const res = await petService.getPetsByIds(customer.pets).toArray();
if (res === null) {
reply.status(500).send('DB broke');
throw new Error('Something is wrong');
}
reply.status(200).send(res);
});
fastify.put<{ Body: CustomerSchema, Reply: CustomerSchema, Params: { customerID: string } }>
('/customers/:customerID',
async (
request: FastifyRequest<{ Body: CustomerSchema, Params: { customerID: string } }>,
reply: FastifyReply
) => {
const {customerID} = request.params;
const customer = request.body;
const result = await customerService.updateCustomer(customerID, customer);
if (result.ok === 0) {
reply.status(400).send(customer);
}
reply.status(200).send(customer);
});
fastify.post<{ Body: CustomerSchema, Reply: CustomerSchema }>
('/customers',
async (
request: FastifyRequest<{ Body: CustomerSchema, Reply: CustomerSchema }>,
reply: FastifyReply
) => {
const customer = request.body;
const createdCustomer = await customerService.createCustomer(customer);
reply.status(200).send(createdCustomer);
});
}
Видя этот контроллер, мы можем сделать некоторые выводы, отличающиеся, но очень похожие на проект на объектно-ориентированном языке, у нас есть инъекция зависимостей в начале, когда мы вызываем два сервиса, и каждый контроллер происходит в контексте Function.
Единственная обязанность контроллера — управлять потоком, вызывать функции и возвращать ошибки или данные, не обращаясь к бизнес-правилам / базе данных.
Давайте проследим за порядком логических частей кода, далее мы поговорим о сервисе и о том, за что он должен отвечать.
Услуги
Когда мы говорим о сервисах, думайте в двух частях, кто вызывает базу данных или контекст и работает с бизнес-правилами. В случае простого проекта, подобного этому, служба вызывает БД и обрабатывает только записи.
export default function PetService(
fastify: FastifyInstance<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>
) {
const db = PetContext(fastify);
const getAllPets = () => {
return db.find().toArray();
}
const getPetById = (id: string) => {
return db.findOne(new ObjectId(id))
}
const getPetsByIds = (ids: Array<string>) => {
const i = ids.map($ => new ObjectId($));
return db.find( {_id: {$in: i}} );
}
const updatePet = (id: string, pet: PetSchema) => {
return db.findOneAndReplace({_id: new ObjectId(id)}, pet);
}
const createPet = (pet: PetSchema) => {
return db.insertOne(pet);
}
const deletePet = (id: string) => {
return db.deleteOne({_id: new ObjectId(id)});
}
return {getAllPets, getPetById, updatePet, createPet, getPetsByIds, deletePet}
}
Как видно из приведенного выше кода, этот сервис представляет собой набор функций, которые получают в параметре код, который будет храниться в базе данных.
Контекст
Контекст или база данных — это файл, в котором мы будем работать. Файл pet-context
— это не что иное, как файл, в котором мы хотим подключиться к источнику данных и задать ему тип или схему.
export default function PetContext(fastify: FastifyInstance<Server, IncomingMessage, ServerResponse, FastifyLoggerInstance>) {
if (fastify.mongo.db !== undefined) {
return fastify.mongo.db.collection<PetSchema>('Pets');
}
throw new Error('No DB collection found')
}
Просто, не так ли? Потому что это mongo и большая часть сложности находится в схеме, но миграции и другие задачи, связанные с данными, должны быть в этом контексте, то есть в каталоге, где экспортируется только БД и скрыты ее особенности, в данном случае это просто экспорт коллекции.
Схема
Схема — это представление ваших данных, это может быть тип + Объект, это место, где будет находиться база вашего домена, если у вас есть схема базы данных и некоторые другие детали, все это будет находиться внутри этого каталога. Главное, чтобы для тех, кто играет в проекте, были понятны домены и возможность расширения через каталоги и файлы.
без лишних слов схема питомца:
export const Pet = Type.Object({
name: Type.String(),
type: Type.Optional(Type.String()),
ownerID: Type.Optional(Type.String()),
});
export type PetSchema = Static<typeof Pet>;
Смотрите, у нас есть Pet, который является схемой базы данных, и его тип, который используется TypeScript. Это та простота, к которой нужно стремиться в проектах узлов, простота, делающая только одно дело, но делающая это одно дело очень хорошо.
Резюме
В целом, мы должны стремиться к простоте и минимализму в наших бэкендах, не пытаться создать больше кода, чем необходимо, всегда стараться оставить энтропию кода близкой к нулю, чтобы обслуживание было возможным.
Я рекомендую читать ссылки, приведенные в начале, поскольку первоисточник, хотя он немного сложнее, является содержанием in natura и часто более эффективен для обучения.