Подтвердить что ты не робот

Приложение для создания корпоративного приложения с Node/Express

Я пытаюсь понять, как структурировать корпоративное приложение с помощью Node/Express/Mongo (фактически используя стек MEAN).

После чтения 2 книг и некоторых поисковых запросов (включая похожие вопросы StackOverflow) я не смог найти хороший пример структурирования больших приложений с помощью Express. Все источники, которые я прочитал, предлагают разделить приложение следующими объектами:

  • маршруты
  • Контроллеры
  • Модели

Но основная проблема, которую я вижу в этой структуре, заключается в том, что контроллеры похожи на объекты-боги, они знают о req, res объектах, ответственных за проверку и включающих бизнес-логику.

С другой стороны, маршруты мне кажутся слишком сложными, потому что все, что они делают, - это сопоставление конечных точек (путей) с методами контроллера.

У меня есть фона Scala/Java, поэтому у меня есть привычка разделять всю логику на 3 уровня - контроллер/сервис/дао.

Для меня идеальны следующие утверждения:

  • Контроллеры отвечают только за взаимодействие с WEB-частью, т.е. сортировку/разборку, некоторую простую проверку (требуется, мин, макс, регулярное выражение электронной почты и т.д.);

  • Уровень сервиса (который на самом деле я пропустил в приложениях NodeJS/Express) отвечает только за бизнес-логику, некоторую проверку бизнеса. Уровень сервиса ничего не знает о WEB-части (т.е. Они могут быть вызваны из другого места приложения не только из веб-контекста);

  • Что касается уровня DAO, для меня все понятно. Модели Mongoose на самом деле DAO, поэтому для меня это самая ясная вещь.

Я думаю, что примеры, которые я видел, очень просты, и они показывают только концепции Node/Express, но я хочу посмотреть на какой-то реальный пример, в котором участвует большая часть бизнес-логики/валидации.

ИЗМЕНИТЬ:

Мне еще не ясно, нет ли объектов DTO. Рассмотрим этот пример:

const mongoose = require('mongoose');
const Article = mongoose.model('Article');
exports.create = function(req, res) {
    // Create a new article object
    const article = new Article(req.body);
    // saving article and other code
}

Здесь объект JSON из req.body передается как параметр для создания документа Mongo. Мне плохо пахнет. Я хотел бы работать с конкретными классами, а не с сырым JSON

Спасибо.

4b9b3361

Ответ 1

Контроллеры - это объекты Бога, пока вы не хотите, чтобы они были такими...   - вы не говорите zurfyx (╯ ° □ °) ╯ (┻━┻

Просто интересно решение? Перейти на последний раздел "Результат" .

┬──┬◡ ノ (° - ° ノ)

Прежде чем начать с ответа, позвольте мне извиниться за то, что этот ответ намного длиннее обычной длины SO. Контроллеры сами по себе ничего не делают, все о шаблоне MVC. Итак, мне показалось, что важно проследить все важные подробности о Router ↔ Controller ↔ Service ↔ Model, чтобы показать вам, как достичь надлежащих изолированных контроллеров с минимальными обязанностями.

Гипотетический случай

Начнем с небольшого гипотетического случая:

  • Я хочу иметь API, который служит для поиска пользователей через AJAX.
  • Я хочу иметь API, который также обслуживает один и тот же поиск пользователя через Socket.io.

Начнем с Express. Это простое peasy, не так ли?

routes.js

import * as userControllers from 'controllers/users';
router.get('/users/:username', userControllers.getUser);

Контроллеры/user.js

import User from '../models/User';
function getUser(req, res, next) {
  const username = req.params.username;
  if (username === '') {
    return res.status(500).json({ error: 'Username can\'t be blank' });
  }
  try {
    const user = await User.find({ username }).exec();
    return res.status(200).json(user);
  } catch (error) {
    return res.status(500).json(error);
  }
}

Теперь давайте сделаем раздел Socket.io:

Поскольку это не вопрос socket.io, я пропущу шаблон.

import User from '../models/User';
socket.on('RequestUser', (data, ack) => {
  const username = data.username;
  if (username === '') {
    ack ({ error: 'Username can\'t be blank' });
  }
  try {
    const user = User.find({ username }).exec();
    return ack(user);
  } catch (error) {
    return ack(error);
  }
});

Ух, здесь что-то пахнет...

  • if (username === ''). Нам пришлось дважды написать контролер проверки. Что делать, если были проверенные валидаторы n? Должны ли мы сохранить две (или более) копии каждого обновления?
  • User.find({ username }) повторяется дважды. Это может быть услуга.

Мы только что написали два контроллера, которые привязаны к точным определениям Express и Socket.io соответственно. Они, скорее всего, никогда не сломаются во время их жизни, потому что и Express, и Socket.io имеют обратную совместимость. НО, они не могут использоваться повторно. Изменение Express для Hapi? Вам придется переделать все ваши контроллеры.

Еще один неприятный запах, который может быть не столь очевидным...

Ответ контроллера - ручной. .json({ error: whatever })

API в RL постоянно меняются. В будущем вам может потребоваться, чтобы ваш ответ был { err: whatever } или, может быть, более сложным (и полезным), например: { error: whatever, status: 500 }

Пусть начнется (возможное решение)

Я не могу назвать это решением, потому что есть бесконечное количество решений. Это зависит от вашего творчества и ваших потребностей. Следующее - достойное решение; Я использую его в относительно большом проекте и, похоже, работает хорошо, и он исправляет все, что я указывал раньше.

Пойду Model → Service → Controller → Router, чтобы он был интересным до конца.

Model

Я не буду вдаваться в подробности о Модели, потому что это не предмет вопроса.

Вы должны иметь аналогичную структуру модели Mongoose следующим образом:

модели/User/validate.js

export function validateUsername(username) {
  return true;
}

Вы можете узнать больше о соответствующей структуре для валидаторов mongoose 4.x здесь.

модели/User/index.js

import { validateUsername } from './validate';

const userSchema = new Schema({
  username: { 
    type: String, 
    unique: true,
    validate: [{ validator: validateUsername, msg: 'Invalid username' }],
  },
}, { timestamps: true });

const User = mongoose.model('User', userSchema);

export default User;

Просто базовая пользовательская схема с полем имени пользователя и created updated полями, контролируемыми мангустами.

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

Mongoose Schema - последний шаг перед достижением базы данных, если только кто-то не запросит MongoDB напрямую, вы всегда будете уверены, что все пройдут проверку вашей модели, что дает вам больше безопасности, чем размещение их на вашем контроллере. Не говоря уже о том, что валидаторы модульного тестирования, как они в предыдущем примере, тривиальны.

Подробнее об этом здесь и здесь.

Сервис

Служба будет действовать как процессор. Учитывая приемлемые параметры, он обработает их и вернет значение.

В большинстве случаев (включая этот) он использует Mongoose Models и возвращает Promise (или обратный вызов, но Я бы определенно использовал ES6 с Promises, если вы уже этого не делаете).

услуги/user.js

function getUser(username) {
  return User.find({ username}).exec(); // Just as a mongoose reminder, .exec() on find 
                               // returns a Promise instead of the standard callback.
}

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

В других случаях достаточно тривиальной службы синхронизации. Убедитесь, что ваша служба синхронизации никогда не включает ввод-вывод, иначе вы будете блокировать целую цепочку Node.js.

услуги/user.js

function isChucknorris(username) {
  return ['Chuck Norris', 'Jon Skeet'].indexOf(username) !== -1;
}

контроллер

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

Контроллеры/user.js

export function getUser(username) {
}

Как выглядит эта подпись сейчас? Довольно, верно? Поскольку нас интересует только параметр имени пользователя, нам не нужно принимать бесполезные вещи, такие как req, res, next.

Добавьте в отсутствующие валидаторы и службу:

Контроллеры/user.js

import { getUser as getUserService } from '../services/user.js'

function getUser(username) {
  if (username === '') {
    throw new Error('Username can\'t be blank');
  }
  return getUserService(username);
}

По-прежнему выглядит аккуратно, но... как насчет throw new Error, разве это не приведет к сбою моего приложения? -Ты, подожди. Мы еще не закончили.

Итак, в этот момент наша документация контроллера будет выглядеть примерно так:

/**
 * Get a user by username.
 * @param username a string value that represents user username.
 * @returns A Promise, an exception or a value.
 */

Какое значение указано в @returns? Помните, что раньше мы говорили, что наши сервисы могут быть как синхронными, так и асинхронными (с использованием Promise)? getUserService является асинхронным в этом случае, но isChucknorris служба не будет, поэтому он просто вернет значение вместо обещания.

Надеемся, что все прочтут документы. Потому что им нужно будет обрабатывать некоторые контроллеры, отличные от других, и для некоторых из них потребуется блок try-catch.

Так как мы не можем доверять разработчикам (это включает меня), читая документы перед первым попыткой, на этом этапе мы должны принять решение:

  • Контроллеры для принудительного возврата Promise
  • Сервис всегда возвращает обещание

⬑ Это позволит решить несогласованный возврат контроллера (а не тот факт, что мы можем опустить наш блок try-catch).

IMO, я предпочитаю первый вариант. Потому что контроллеры - это те, которые в большинстве случаев будут связывать большинство Promises.

return findUserByUsername
         .then((user) => getChat(user))
         .then((chat) => doSomethingElse(chat))

Если мы используем ES6 Promise, мы можем в качестве альтернативы использовать красивое свойство Promise для этого: Promise может обрабатывать не promises в течение своей жизни и все равно продолжать возвращать Promise:

return promise
         .then(() => nonPromise)
         .then(() => // I can keep on with a Promise.

Если единственная услуга, которую мы вызываем, не использует Promise, мы можем сделать ее сами.

return Promise.resolve() // Initialize Promise for the first time.
  .then(() => isChucknorris('someone'));

Возвращаясь к нашему примеру, это приведет к:

...
return Promise.resolve()
  .then(() => getUserService(username));

Нам действительно не нужно Promise.resolve() в этом случае, поскольку getUserService уже возвращает Promise, но мы хотим быть последовательными.

Если вы задаетесь вопросом о блоке catch: мы не хотим использовать его в нашем контроллере, если мы не хотим сделать это обычным образом. Таким образом, мы можем использовать два уже встроенных канала связи (исключение для ошибок и возврат сообщений о успехе) для доставки наших сообщений по отдельным каналам.

Вместо ES6 Promise .then мы можем использовать в наших контроллерах новый ES2017 async / await (теперь официальный):

async function myController() {
    const user = await findUserByUsername();
    const chat = await getChat(user);
    const somethingElse = doSomethingElse(chat);
    return somethingElse;
}

Обратите внимание на async перед function.

маршрутизатор

Наконец, маршрутизатор, yay!

Итак, мы еще ничего не ответили пользователю, все, что у нас есть, - это контроллер, который знает, что он ВСЕГДА возвращает Promise (надеюсь, с данными). Oh!, и это может вызвать исключение, если throw new Error is called или некоторая служба Promise ломается.

Роутером будет тот, который будет единообразно управлять петициями и возвращать данные клиентам, будь то некоторые существующие данные, null или undefined data или ошибка.

Маршрутизатор будет ТОЛЬКО, который будет иметь несколько определений. Количество которых будет зависеть от наших перехватчиков. В гипотетическом случае это были API (с Express) и Socket (с Socket.io).

Давайте рассмотрим, что мы должны делать:

Мы хотим, чтобы наш маршрутизатор преобразовал (req, res, next) в (username). Наивная версия будет примерно такой:

router.get('users/:username', (req, res, next) => {
  try {
    const result = await getUser(req.params.username); // Remember: getUser is the controller.
    return res.status(200).json(result);
  } catch (error) {
    return res.status(500).json(error);
  }
});

Хотя это будет хорошо работать, это приведет к огромному дублированию кода, если мы скопируем этот фрагмент во всех наших маршрутах. Поэтому мы должны сделать лучшую абстракцию.

В этом случае мы можем создать своего рода фальшивый клиент-маршрутизатор, который выполняет обещание и параметры n, выполняет задачи маршрутизации и return, как это было бы в каждом из маршрутов.

/**
 * Handles controller execution and responds to user (API Express version).
 * Web socket has a similar handler implementation.
 * @param promise Controller Promise. I.e. getUser.
 * @param params A function (req, res, next), all of which are optional
 * that maps our desired controller parameters. I.e. (req) => [req.params.username, ...].
 */
const controllerHandler = (promise, params) => async (req, res, next) => {
  const boundParams = params ? params(req, res, next) : [];
  try {
    const result = await promise(...boundParams);
    return res.json(result || { message: 'OK' });
  } catch (error) {
    return res.status(500).json(error);
  }
};
const c = controllerHandler; // Just a name shortener.

Если вам интересно узнать больше об этом трюке, вы можете прочитать о полной версии этого в моем другом ответе в React-Redux и Websockets с socket.io (Раздел "SocketClient.js" ).

Как выглядит ваш маршрут с помощью controllerHandler?

router.get('users/:username', c(getUser, (req, res, next) => [req.params.username]));

Чистая одна строка, как и в начале.

Дополнительные необязательные шаги

Контроллер Promises

Это относится только к тем, кто использует ES6 Promises. Версия ES2017 async / await уже выглядит хорошо для меня.

По какой-то причине мне не нравится использовать имя Promise.resolve() для создания инициализации Promise. Просто не ясно, что там происходит.

Я бы скорее заменил их на что-то более понятное:

const chain = Promise.resolve(); // Write this as an external imported variable or a global.

chain
  .then(() => ...)
  .then(() => ...)

Теперь вы знаете, что chain обозначает начало цепочки Promises. Так же, как и все, кто читает ваш код, а если нет, они, по крайней мере, считают его цепочкой функций обслуживания.

Экспресс-обработчик ошибок

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

router.use((err, req, res, next) => {
  // Expected errors always throw Error.
  // Unexpected errors will either throw unexpected stuff or crash the application.
  if (Object.prototype.isPrototypeOf.call(Error.prototype, err)) {
    return res.status(err.status || 500).json({ error: err.message });
  }

  console.error('~~~ Unexpected error exception start ~~~');
  console.error(req);
  console.error(err);
  console.error('~~~ Unexpected error exception end ~~~');


  return res.status(500).json({ error: '⁽ƈ ͡ (ुŏ̥̥̥̥םŏ̥̥̥̥) ु' });
});

Что еще, вы, вероятно, должны использовать что-то вроде debug или winston вместо console.error, которые являются более профессиональными способами обработки журналов.

И вот как мы подключаем это к controllerHandler:

  ...
  } catch (error) {
    return res.status(500) && next(error);
  }

Мы просто перенаправляем любую обработанную ошибку для обработчика ошибок Express.

Ошибка как ApiError

Error считается классом по умолчанию для инкапсуляции ошибок при бросании исключения в Javascript. Если вы действительно хотите отслеживать свои собственные контролируемые ошибки, я бы, вероятно, изменил обработчик ошибок throw Error и Express от Error до ApiError, и вы даже можете улучшить его, добавив его статус поле.

export class ApiError {
  constructor(message, status = 500) {
    this.message = message;
    this.status = status;
  }
}

Дополнительная информация

Пользовательские исключения

Вы можете бросить любое настраиваемое исключение в любой точке throw new Error('whatever') или с помощью new Promise((resolve, reject) => reject('whatever')). Вам просто нужно играть с Promise.

ES6 ES2017

Это очень упрямый момент. IMO ES6 (или даже ES2017, теперь имеющий официальный набор функций) является подходящим способом работы с большими проектами на основе Node.

Если вы еще не используете его, попробуйте посмотреть ES6 и ES2017 и Babel transpiler.

Результат

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

router.js

const controllerHandler = (promise, params) => async (req, res, next) => {
  const boundParams = params ? params(req, res, next) : [];
  try {
    const result = await promise(...boundParams);
    return res.json(result || { message: 'OK' });
  } catch (error) {
    return res.status(500) && next(error);
  }
};
const c = controllerHandler;

router.get('/users/:username', c(getUser, (req, res, next) => [req.params.username]));

Контроллеры/user.js

import { serviceFunction } from service/user.js
export async function getUser(username) {
  const user = await findUserByUsername();
  const chat = await getChat(user);
  const somethingElse = doSomethingElse(chat);
  return somethingElse;
}

услуги/user.js

import User from '../models/User';
export function getUser(username) {
  return User.find({}).exec();
}

модели/User/index.js

import { validateUsername } from './validate';

const userSchema = new Schema({
  username: { 
    type: String, 
    unique: true,
    validate: [{ validator: validateUsername, msg: 'Invalid username' }],
  },
}, { timestamps: true });

const User = mongoose.model('User', userSchema);

export default User;

модели/User/validate.js

export function validateUsername(username) {
  return true;
}

Ответ 2

Каждый имеет свой собственный способ деления проекта на определенные папки. структура, которую я использую,

  • конфигурации
  • журналы
  • маршруты
  • Контроллеры
  • Модели
  • услуги
  • Utils
  • app.js/server.js/index.js(любое имя, которое вы предпочитаете)
Папка конфигурации

содержит файлы конфигурации, такие как настройки подключения к базе данных для всей фазы разработки, такие как "производство", "разработка", "тестирование"

Пример

'use strict'
var dbsettings = {
    "production": {
//your test settings
    },
    "test": {

    },
    "development": {
        "database": "be",
        "username": "yourname",
        "password": "yourpassword",
        "host": "localhost",
        "connectionLimit": 100
    }
}
module.exports = dbsettings

папка журнала содержит журналы журналов соединений для отладки

Контроллер

предназначен для проверки ваших данных req и бизнес-логики

Пример

const service = require("../../service")
const async = require("async")
exports.techverify = (data, callback) => {

    async.series([
        (cb) => {
            let searchObject = { accessToken: data.accessToken }
            service.admin.get(searchObject, (err, result) => {
                if (err || result.length == 0) {
                    callback(err, { message: "accessToken is invalid" })
                } else {
                    delete data.accessToken
                    service.tech.update(data, { verified: true }, (err, affe, res) => {
                        if (!err)
                            callback(err, { message: "verification done" })
                        else
                            callback(err, { message: "error occured" })
                    })
                }
            })
        }
    ])
}
Модели

предназначены для определения вашей схемы db

Пример схемы mongoDb

'use strict'
let mongoose = require('mongoose');
let schema = mongoose.Schema;
let user = new schema({
    accesstoken: { type: String },
    firstname: { type: String },
    lastname: { type: String },
    email: { type: String, unique: true },
    image: { type: String },
    phoneNo: { type: String },
    gender: { type: String },
    deviceType: { type: String },
    password: { type: String },
    regAddress: { type: String },
    pincode: { type: String },
    fbId: { type: String, default: 0 },
    created_at: { type: Date, default: Date.now },
    updated_at: { type: Date, default: Date.now },
    one_time_password: { type: String },
    forgot_password_token: { type: String },
    is_block: { type: Boolean, default: 0 },
    skin_type: { type: String },
    hair_length: { type: String },
    hair_type: { type: String },
    credits: { type: Number, default: 0 },
    invite_code: { type: String },
    refered_by: { type: String },
    card_details: [{
        card_type: { type: String },
        card_no: { type: String },
        card_cv_no: { type: String },
        created_at: { type: Date }
    }]
});
module.exports = mongoose.model('user', user);

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

запросы с использованием мангуста

'use strict'
const modelUser = require('../../models/user');
exports.insert = (data, callback) => {
    console.log('mongo log for insert function', data)
    new modelUser(data).save(callback)
}
exports.get = (data, callback) => {
    console.log('mongo log for get function', data)
    modelUser.find(data, callback)
}
exports.update = (data, updateData, callback) => {
    console.log('mongo log for update function', data)
    modelUser.update(data, updateData, callback);
}
exports.getWithProjection = (data, projection, callback) => {
    console.log('mongo log for get function', data)
    modelUser.find(data, projection, callback)
}

utils для общей функции утилиты, которая обычно используется в вашем проекте, может быть, как шифрование, дешифрование пароля и т.д.

Пример

exports.checkPassword = (text, psypherText) => {
    console.log("checkPassword executed")
    console.log(text, psypherText)
    return bcrypt.compareSync(text, psypherText)
}
exports.generateToken = (userEmail) => {
    return jwt.sign({ unique: userEmail, timeStamp: Date.now }, config.keys.jsonwebtoken)
}

Ответ 3

Ответ rohit salaria в основном объясняет ту же структуру приложения, к которой вы привыкли в java.

  • Контроллеры - это контроллеры в Java
  • Модели - это уровень доступа к данным
  • Службы - это уровень обслуживания

У меня есть несколько замечаний. Первый и самый важный - это не Java. Это может показаться очевидным, но просто посмотрите на свой вопрос и убедитесь, что вы ищете тот же опыт разработки с теми же концепциями, которые вы использовали в мире Java. Мое следующее замечание - просто объяснение этого.

Отсутствует DTO. В Java они просто необходимы, период. В веб-приложении Java, где вы храните свои данные в реляционной базе данных и отправляете и получаете данные в интерфейсе JSON, естественно, что вы преобразовываете данные в объект Java. Однако в приложении Node все работает как javascript и JSON. Это одна из сильных сторон платформы. Поскольку JSON является общим форматом данных, нет необходимости писать код или зависеть от библиотек для перевода между форматом данных ваших слоев.

Передача объекта данных непосредственно из запроса в модель. Почему нет? Наличие JSON в качестве общего формата данных из интерфейсного элемента базы данных позволяет легко синхронизировать модель данных вашего приложения между всеми вашими слоями. Конечно, вам не обязательно идти этим путем, но этого достаточно в большинстве случаев, так почему бы не использовать его? Что касается проверки, то это делается в модели, где она принадлежит в соответствии с теорией MVC (а не в контроллере, где часто лень и прагматизм):

Для последней мысли я хочу добавить, что это не лучшая платформа, когда дело доходит до масштабирования размера проекта. Это нокаут, но Java лучше в этом аспекте.

Ответ 4

Я выполнил то, что вы пытаетесь сделать. По существу, в традиционной структуре Route/Controller/Model отсутствует слой. Короткий ответ заключается в том, что в области node это еще не развилось, как вы этого хотите, так что для работы с объектами приходят пользовательские вещи.

Несколько советов для начала:

  • Используйте TypeScript вместо JavaScript.
  • Заменить экспресс с помощью HapiJS

Наиболее эффективным способом, который я нашел для достижения этой цели, является наличие объекта, который имеет статические методы, которые обращаются к модели, а затем импортируют их в ваши контроллеры. Теперь - это занимает больше времени для настройки, чем просто после документов на серверах node, но, как только вы закончите, soooooo прост в обслуживании и разделение труда для более крупных команд является фантастическим (когда команда может буквально быть посвящена маршруты/контроллеры, в то время как другой управляет DAO/моделями).

// controller
import Article from 'models/Article';
export ArticleController {

    class GET {
        handler( req, res ){
            return Article.find(req.params.id);
        }
    }

    class POST {

        validator: {
            // this is where you ensure req.payload is going to be sufficient for the article constructor
            payload: {
                name: joi.string().required()
            }
        }

        handler( req, res ){
            const oArticle = new Article(req.payload);
            oArticle.save();
        }
    }

}

//Article
export class Article {
    public id: string;
    public name: string;

    constructor(data){
        // over-simplified logic to load data into object for example
        // there are some edge cases you need to figure out
        Object.assign(this, data);
    }

    public static find( id ){
       // get the article from your DAO - pseudo code
       const data = DAO.getArticleDataById(id);
       return new Article(data);
    }

    public save(){
        // save this object using DAO
    }
}

Ответ 5

простое и основное правило

  • Сохранять компоненты, близкие друг к другу.

  • Разделите страницу на компоненты и работайте

  • Все зависимые компоненты должны быть вместе

  • общие вещи должны храниться независимо от всех других компонентов.

Наконец, каждый язык сладок. Просто так, как вы знакомы с языком. Вы можете выиграть битву, только если вы знакомы с вами мечом.

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

main Module

`the main module` 

subModule

 `the sub module structure`

shared Module

`keep the shared folder as a separate module`

Надеюсь, это поможет:)