Автор оригинала: Evan Bechtol.
Независимо от того, являетесь ли вы новичком или экспертом на Node.js, в начале каждого проекта важно, чтобы вы создали звукового архитектурного ландшафта. Это позволит вам вырастить ваш проект при обеспечении читаемости, тестирования и ремонтопригодности (Просто назвать несколько нефункциональные требования ) Отказ
Прочитав эту статью, вы сможете:
- Создать интуитивно понятный и чистый структуру проекта
- Понять разницу между концепциями; Контроллеры, погрузчики, услуги
- Создание чистых модулей для вашей бизнес-логики
Оглавление
- Концепции
- Структура папки проекта
- 3-слойный (сервисный) архитектура
- Сервисный слой
- Тестирование подразделений
- Уровень контроллера
- Погрузчики
- Конфигурации приложений
- Пример репозитория
Концепции
Вот несколько концепций, с которыми вы должны быть знакомы, когда вы проходите через эту статью. Не волнуйтесь, если вы не являетесь экспертом с ними, просто понимаете их значение и как структура позволяет нам использовать эти концепции.
Структура папки проекта
Ниже приведена структура то, что я использую в качестве шаблона почти во всех моих проектах Node.js. Это позволяет нам реализовать Разделение опасений для нашего приложения.
src │ index.js # Entry point for application └───config # Application environment variables and secrets └───controllers # Express controllers for routes, respond to client requests, call services └───loaders # Handles all startup processes └───middlewares # Operations that check or maniuplate request prior to controller utilizing └───models # Database models └───routes # Express routes that define API structure └───services # Encapsulates all business logic └───test # Tests go here
3-слой архитектуры
Здание по принципу Разделение опасений Что мы говорили ранее, цель состоит в том, чтобы полностью извлечь и отделить нашу бизнес-логику от нашего API. В частности, мы никогда не Хотите нашу бизнес-логику присутствовать на наших маршрутах или контроллерах. На рисунке ниже вы увидите ровно Как наше приложение будет течь.
- Контроллеры получают входящие клиентские запросы, и они используют услуги
- Услуги содержат всю бизнес-логику, а также могут звонить на уровень доступа к данным
- Слой доступа к данным взаимодействует с базой данных путем выполнения запросов
- Результаты передаются обратно до сервисного слоя.
- Сервисный слой может затем передать все обратно к контроллеру
- Контроллер может затем ответить на клиента!
Вопрос: Почему я не могу просто разместить свою бизнес-логику внутри моего контроллера?
Это большой вопрос! Поскольку наши маршруты (в этом случае) созданы, созданные с помощью Express Framework, есть тонна дополнительного пуха, который добавляется к req
и res
объекты. Если мы хотим проверить нашу бизнес-логику, мы теперь поручены на бремя создания издевательства этих целых объектов ! Инкапсулируя все нашу деловую логику внутри услуг, мы можем проверить его без надменения экспресс req
или res
Объекты ️!
Сервисный слой
Сервисный слой инкапсулирует и тезисывает всю нашу бизнес-логику от остальной части приложения.
Сервисный слой должен :
- Содержать деловую логику
- Используйте уровень доступа к данным для взаимодействия с базой данных
- Быть рамочным агностическим
Сервисный слой не должен :
- Быть предоставленным
req
илиres
объекты - Ручка реагировать на клиентов
- Предоставить что-нибудь, связанное с HTTP транспортный слой; Коды состояния, заголовки и т. Д.
- Напрямую взаимодействовать с базой данных
- Быть предоставленным
Пример
Здесь наш контроллер импортирует службу, которую мы будем использовать для создания постов. Обратите внимание, что у нас нет бизнес-логики в этом файле!
// controllers/Post/index.js const PostService = require( "../services/PostService" ); const PostServiceInstance = new PostService(); module.exports = { createCord }; /** * @description Create a cord with the provided body * @param req {object} Express req object * @param res {object} Express res object * @returns {Promise<*>} */ async function createCord ( req, res ) { try { // We only pass the body object, never the req object const createdCord = await PostServiceInstance.create( req.body ); return res.send( createdCord ); } catch ( err ) { res.status( 500 ).send( err ); } }
Наш сервис реализует всю нашу логику и может использовать уровень доступа к данным для взаимодействия с базой данных! Как только наша логика достигнет результата, мы возвращаем данные (или ошибку, если их произошло) для контроллера.
// services/PostService.js const MongooseService = require( "./MongooseService" ); // Data Access Layer const PostModel = require( "../models/post" ); // Database Model class PostService { /** * @description Create an instance of PostService */ constructor () { // Create instance of Data Access layer using our desired model this.MongooseServiceInstance = new MongooseService( PostModel ); } /** * @description Attempt to create a post with the provided object * @param postToCreate {object} Object containing all required fields to * create post * @returns {Promise<{success: boolean, error: *}|{success: boolean, body: *}>} */ async create ( postToCreate ) { try { const result = await this.MongooseServiceInstance.create( postToCreate ); return { success: true, body: result }; } catch ( err ) { return { success: false, error: err }; } } } module.exports = PostService;
Тестирование подразделений
Создание тщательных тестов для вашего кода важно для обеспечения необходимости пригодности вашего кода, и надежным. Если вы следуете мыслей Mindset Тестовое развитие (TDD) тогда вы должны создавать модульные тесты до Вы начинаете писать любой код. Это позволяет нам обеспечить, чтобы мы написали минимальное количество кода, необходимого для удовлетворения требований под рукой, и после завершения разработки наши тесты уже есть!
Есть много модулей и способов проверки вашего кода, но в этом примере мы будем использовать комбинацию моча , Чай и NYC Отказ Это даст нам много гибкости для создания модульных тестов, а также дайте нам знать, сколько кода покрыт нашими тестами!
1) Чтобы начать, добавьте эти 3 модуля в наш проект
NPM I-S NYC Mocha Chai
2) Теперь создайте новый каталог в тестовом каталоге для наших пост-тестов
src │ index.js # Entry point for application ... // Other directories └───test # Tests go here └─── Post # All tests for 'Posts' go here | index.js
3) Открыть тест/post/index.js и вставьте следующий код
const assert = require( "chai" ).assert; const mocha = require( "mocha" ); const PostService = require( "../../services/PostService" ); // Import the service we want to test mocha.describe( "Post Service", () => { const PostServiceInstance = new PostService(); mocha.describe( "Create instance of service", () => { it( "Is not null", () => { assert.isNotNull( PostServiceInstance ); } ); it( "Exposes the createPost method", () => { assert.isFunction( PostServiceInstance.create ); } ); } ); } );
4) Запустите тесты, используя следующую команду
Mocha Test/* – Спецификация
Вы должны получить выход, похожий на это:
Post Service Create instance of service √ Is not null √ Exposes the createPost method 2 passing (29ms)
Теперь я знаю, что мы не делали это в настоящей моде TDD; Мы написали код до написание теста. Но потому что эта статья более сосредоточена на трехслойной архитектуре, я чувствовал, что важно сначала ввести Концепция услуг до модульных тестов. Просто знайте, что если вы должны были нанести это на практике самостоятельно, это принесет вам преимущественно вам, чтобы написать тесты до кода Отказ
Теперь, когда вы написали свои первые тесты, пришло время изучить, как мы используем наши услуги в контроллере!
Уровень контроллера
Уровень контроллера отвечает за обработку клиентских запросов и отвечать на них. Просто подтвердить очень важный момент, Этот слой никогда не должен содержать бизнес-логику! Мы используем только услуги, передав данные, которые им нужны, а не req
или res
сами объекты. Это позволяет нашим услугам оставаться основой Agnostic!
Я показал пример слоя контроллера выше, который вы также можете найти здесь (Нет необходимости восстанавливать колесо) Отказ
/** * @description Create a cord with the provided body * @param req {object} Express req object * @param res {object} Express res object * @returns {Promise<*>} */ async function createCord ( req, res ) { try { // We only pass the body object, never the req object const createdCord = await PostServiceInstance.create( req.body ); return res.send( createdCord ); } catch ( err ) { res.status( 500 ).send( err ); } }
Погрузчики
Погрузчики абстрактны все наши процессы запуска приложения в конкретные модули. Это позволяет нам инкапсулировать и поддерживать разделение опасений. Если вы выбросите все в свою точку входа в приложение, она очень быстро загромождается.
Чтобы по-настоящему водить эту точку домой, сравните этот пример с тем ниже его. Спросите себя, что было бы легче поддерживать, что было бы легче масштабировать и расширяться, что было бы легче удалить позже, если больше не нужно.
Что не делать (плохо)
const bodyParser = require( 'body-parser' ); const config = require( './config' ); const express = require( 'express' ); const morgan = require( 'morgan' ); const path = require( 'path' ); const routes = require( './routes' ); const rfs = require( 'rotating-file-stream' ); const compression = require( 'compression' ); let fs = require( 'fs' ), logDir = path.join( __dirname, config.logDir ); // Check for 'logs' directory fs.access( logDir, ( err ) => { if ( err ) { fs.mkdirSync( logDir ); } } ); // Initialize express instance, and log rotation let app = express(), accessLogStream = rfs( 'access.log', { interval : '1d', path : logDir } ); // Setup views and pathing app.set( 'view engine', 'html' ); app.set( 'views', path.join( __dirname, 'public' ) ); // Serve static content app.use( express.static( path.join( __dirname, 'public' ) ) ); app.use( express.static( path.join( __dirname, 'node_modules' ) ) ); // Set up middleware app.use( morgan( 'dev', { stream : accessLogStream } ) ); app.use( compression() ); app.use( bodyParser.urlencoded( { extended : false, limit : '20mb' } ) ); app.use( bodyParser.json( { limit : '20mb' } ) ); // Pass app to routes routes( app ); // Start application app.listen( config.port, () => { console.log( 'Now listening on', config.port ); } );
Что делать (хорошо)
const config = require( "./config" ); const mongoose = require( "mongoose" ); const logger = require( "./services/Logger" ); const mongooseOptions = { useCreateIndex: true, useNewUrlParser: true, autoReconnect: true }; mongoose.Promise = global.Promise; // Connect to the DB an initialize the app if successful mongoose.connect( config.dbUrl, mongooseOptions ) .then( () => { logger.info( "Database connection successful" ); // Create express instance to setup API const ExpressLoader = require( "./loaders/Express" ); new ExpressLoader(); } ) .catch( err => { //eslint-disable-next-line console.error( err ); logger.error( err ); } );
Очень легко сказать, какой файл является более ремонтом, читабельным, масштабируемым и т. Д. Теперь, чтобы показать вам, как выглядит погрузчик. В приведенном выше примере вы заметите, что у нас есть один загрузчик, Экспресс-служба
Отказ Вот как загрузчик структурирован
const bodyParser = require( "body-parser" ); const express = require( "express" ); const morgan = require( "morgan" ); const path = require( "path" ); const routes = require( "../routes" ); const compression = require( "compression" ); const logger = require( "../services/Logger" ); const config = require( "../config" ); class ExpressLoader { constructor () { const app = express(); // Setup error handling, this must be after all other middleware app.use( ExpressLoader.errorHandler ); // Serve static content app.use( express.static( path.join( __dirname, "uploads" ) ) ); // Set up middleware app.use( morgan( "dev" ) ); app.use( compression() ); app.use( bodyParser.urlencoded( { extended: false, limit: "20mb" } ) ); app.use( bodyParser.json( { limit: "20mb" } ) ); // Pass app to routes routes( app ); // Start application this.server = app.listen( config.port, () => { logger.info( `Express running, now listening on port ${config.port}` ); } ); } get Server () { return this.server; } /** * @description Default error handler to be used with express * @param error Error object * @param req {object} Express req object * @param res {object} Express res object * @param next {function} Express next object * @returns {*} */ static errorHandler ( error, req, res, next ) { let parsedError; // Attempt to gracefully parse error object try { if ( error && typeof error === "object" ) { parsedError = JSON.stringify( error ); } else { parsedError = error; } } catch ( e ) { logger.error( e ); } // Log the original error logger.error( parsedError ); // If response is already sent, don't attempt to respond to client if ( res.headersSent ) { return next( error ); } res.status( 400 ).json( { success: false, error } ); } } module.exports = ExpressLoader;
Что мы сделали, абстрагировали нашу логику Startup для Express в один файл. Это позволяет нам легко удалить/заменить рамки позже, если мы выберем. Это также делает его гораздо легче отладки или отслеживать проблемы, а не для проверки нескольких мест, или файл монстра.
Конфигурации приложений
Вы должны позаботиться о том, чтобы никогда не выдержать ваши прикладные секреты и конфигурации. Последнее, что вы хотите, – это злонамеренное предприятие, чтобы все задние двери были открытыми для них, чтобы делать все, что они хотят! Есть некоторые фантастические модули там; Доценв Что может дать вашу удивительную функциональность для защиты ваших приложений секретов. Но ради простоты мы собираемся создать index.js
Файл в нашем config
. каталог. Это проведет все наши параметры конфигурации приложения для нас, которые мы можем использовать в наших других файлах.
// config/index.js const config = { dbUrl: process.env.DBURL || "mongodb://localhost/test-db", port: process.env.PORT || 3000, env: process.env.NODE_ENV || "development", logDir: process.env.LOGDIR || "logs", viewEngine: process.env.VIEW_ENGINE || "html" }; module.exports = config;
Это так просто! Тогда вы можете просто импортировать файл, где вам это нужно, и ссылаться на переменные.
Пример репозитория
Вы можете найти пример репо, который демонстрирует, как эта структура может быть реализована на мой github
Ссылки и ресурсы
Я не могу взять кредит на все здесь, и, безусловно, получил помощь! Пожалуйста, проверьте эти другие ресурсы, которые вдохновили меня.