使用 Node/Express 构建企业应用 [英] Building enterprise app with Node/Express

查看:32
本文介绍了使用 Node/Express 构建企业应用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试了解如何使用 Node/Express/Mongo(实际上使用 MEAN 堆栈)构建企业应用程序.

I'm trying to understand how to structure enterprise applciation with Node/Express/Mongo (actually using MEAN stack).

在阅读了 2 本书和一些谷歌搜索(包括类似的 StackOverflow 问题)后,我找不到任何使用 Express 构建大型应用程序的好例子.我读过的所有来源都建议按以下实体拆分应用程序:

After reading 2 books and some googling (including similar StackOverflow questions), I couldn't find any good example of structuring large applications using Express. All sources I've read suggest to split application by following entities:

  • 路线
  • 控制器
  • 模型

但是我看到这个结构的主要问题是控制器就像上帝对象,他们知道reqres对象,负责验证和业务逻辑包含在.

But the main problem I see with this structure is that controllers are like god objects, they knows about req, res objects, responsible for validation and have business logic included in.

另一方面,路由在我看来像是过度设计,因为它们所做的只是将端点(路径)映射到控制器方法.

At other side, routes seems to me like over-engineering because all they doing is mapping endpoints(paths) to controller methods.

我有 Scala/Java 背景,所以我习惯将所有逻辑分成 3 层 - 控制器/服务/道.

I have Scala/Java background, so I have habit to separate all logic in 3 tiers - controller/service/dao.

对我来说,以下陈述是理想的:

For me following statements are ideal:

  • 控制器只负责与 WEB 部分的交互,即编组/解组,一些简单的验证(必需、最小值、最大值、电子邮件正则表达式等);

  • Controllers are responsible only for interacting with WEB part, i.e. marshalling/unmarshalling, some simple validation (required, min, max, email regex etc);

服务层(实际上我在 NodeJS/Express 应用程序中错过了它)只负责业务逻辑和一些业务验证.服务层对 WEB 部分一无所知(即它们可以从应用程序的其他地方调用,而不仅仅是从 web 上下文中调用);

Service layer (which actually I missed in NodeJS/Express apps) is responsible only for business logic, some business validation. Service layer doesn't know anything about WEB part (i.e. they can be called from other place of application, not only from web context);

关于 DAO 层,我很清楚.Mongoose 模型实际上是 DAO,所以这里对我来说最清楚.

Regarding to DAO layer is all clear for me. Mongoose models are actually DAO, so it most clear thing to me here.

我认为我看到的例子非常简单,它们只展示了 Node/Express 的概念,但我想看一些真实世界的例子,其中涉及大部分业务逻辑/验证.

I think examples I've seen are very simple, and they shows only concepts of Node/Express, but I want to look at some real world example, with much of the business logic/validation involved in.

我不清楚的另一件事是缺少 DTO 对象.考虑这个例子:

Another thing isn't clear to me is absent of DTO objects. Consider this example:

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
}

req.body 中的 JSON 对象作为参数传递以创建 Mongo 文档.对我来说闻起来很糟糕.我想使用具体的类,而不是原始 JSON

There JSON object from req.body is passed as parameter for creating Mongo document. It smells bad for me. I would like to work with concrete classes, not with raw JSON

谢谢.

推荐答案

控制器是上帝的对象,除非你不希望它们如此...
  ——你别说zurfyx(╯°□°)╯︵┻━┻

Controllers are God objects until you don't want them to be so...
   – you don't say zurfyx (╯°□°)╯︵ ┻━┻

只是对解决方案感兴趣? 跳到最新部分结果".

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

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

在开始回答之前,让我为使这种响应方式比通常的 SO 长度更长而道歉.控制器本身什么都不做,这都是关于整个 MVC 模式的.因此,我觉得有必要详细了解有关路由器 <-> 控制器 <-> 服务 <-> 模型的所有重要细节,以便向您展示如何以最小的责任实现适当的隔离控制器.

Prior getting started with the answer, let me apologize for making this response way longer than the usual SO length. Controllers alone do nothing, it's all about the whole MVC pattern. So, I felt like it was relevant to go through all important details about Router <-> Controller <-> Service <-> Model, in order to show you how to achieve proper isolated controllers with minimum responsibilities.

让我们从一个小的假设案例开始:

Let's start with a small hypothetical case:

  • 我想要一个 API,通过 AJAX 为用户搜索提供服务.
  • 我想要一个 API,它也可以通过 Socket.io 为相同的用户搜索提供服务.

让我们从 Express 开始.这很容易,不是吗?

Let's start with Express. That's easy peasy, isn't it?

routes.js

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

控制器/user.js

controllers/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 部分:

Now let's do the Socket.io part:

由于这不是 socket.io 问题,我将跳过样板.

Since that's not a socket.io question, I'll skip the boilerplate.

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 }) 重复两次.这可能是一项服务.
  • if (username === ''). We had to write the controller validator twice. What if there were n controller validators? Would we have to keep two (or more) copies of each up to date?
  • User.find({ username }) is repeated twice. That could possibly be a service.

我们刚刚编写了两个控制器,分别附加到 Express 和 Socket.io 的确切定义.它们在其生命周期中很可能永远不会中断,因为 Express 和 Socket.io 都倾向于向后兼容.但是,它们不可重复使用.为 Hapi 更改 Express?您将不得不重做所有控制器.

We have just written two controllers that are attached to the exact definitions of Express and Socket.io respectively. They will most likely never break during their lifetime because both Express and Socket.io tend to have backwards compatibility. BUT, they are not reusable. Changing Express for Hapi? You will have to redo all your controllers.

另一种可能不那么明显的难闻的气味......

Another bad smell that might not be so obvious...

控制器响应是手工制作的..json({ error: what })

The controller response is handcrafted. .json({ error: whatever })

RL 中的 API 不断变化.将来,您可能希望您的响应是 { err: what } 或者更复杂(和有用)的东西,例如: { error: what, status: 500 }

APIs in RL are constantly changing. In the future you might want your response to be { err: whatever } or maybe something more complex (and useful) like: { error: whatever, status: 500 }

我不能称之为解决方案,因为那里有无数的解决方案.这取决于您的创造力和需求.以下是一个不错的解决方案;我在一个相对较大的项目中使用它,它似乎运行良好,它修复了我之前指出的所有问题.

I can't call it the solution because there is an endless amount of solutions out there. It is up to your creativity, and your needs. The following is a decent solution; I'm using it in a relatively large project and it seems to be working well, and it fixes everything I pointed out before.

我会去模型 -> 服务 -> 控制器 -> 路由器,让它有趣到最后.

I'll go Model -> Service -> Controller -> Router, to keep it interesting until the end.

我不会详细介绍模型,因为这不是问题的主题.

I won't go into details about the Model, because that's not the subject of the question.

您应该具有如下类似的猫鼬模型结构:

You should be having a similar Mongoose Model structure as the following:

模型/用户/validate.js

models/User/validate.js

export function validateUsername(username) {
  return true;
}

您可以在此处阅读有关适用于 mongoose 4.x 验证器的适当结构的更多信息.

You can read more about the appropriate structure for mongoose 4.x validators here.

模型/用户/index.js

models/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 mongoose 控制字段的基本用户架构.

Just a basic User Schema with an username field and created updated mongoose-controlled fields.

我在此处包含 validate 字段的原因是让您注意,您应该在这里进行大多数模型验证,而不是在控制器中.

The reason why I included the validate field here is for you to notice that you should be doing most model validation in here, not in the controller.

Mongoose Schema 是到达数据库之前的最后一步,除非有人直接查询 MongoDB,否则您将始终放心,每个人都会通过您的模型验证,这比将它们放在控制器上更安全.并不是说前面示例中的单元测试验证器是微不足道的.

Mongoose Schema is the last step before reaching the database, unless someone queries MongoDB directly you will always rest assured that everyone goes through your model validations, which gives you more security than placing them on your controller. Not to say that unit testing validators as they are in the previous example is trivial.

此处阅读更多相关信息和这里.

Read more about this here and here.

该服务将充当处理器.给定可接受的参数,它会处理它们并返回一个值.

The service will act as the processor. Given acceptable parameters, it'll process them and return a value.

大多数时候(包括这个),它会使用 Mongoose 模型并返回一个 Promise(或回调;但是我肯定会将 ES6 与 Promise 结合使用,如果您还没有这样做的话).

Most of the times (including this one), it'll make use of Mongoose Models and return a Promise (or a callback; but I would definitely use ES6 with Promises if you are not doing so already).

服务/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 块吗?不,因为我们稍后会做一个很酷的技巧,而且我们不需要为这种情况定制一个.

At this point you might be wondering, no catch block? Nope, because we're going to do a cool trick later and we don't need a custom one for this case.

其他时候,一个简单的同步服务就足够了.确保您的同步服务从不包含 I/O,否则您将阻塞 整个 Node.js 应用程序.js 线程.

Other times, a trivial sync service will suffice. Make sure your sync service never includes I/O, otherwise you will be blocking the whole Node.js thread.

服务/user.js

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

控制器

我们希望避免重复的控制器,因此我们将为每个操作只有一个控制器.

控制器/user.js

controllers/user.js

export function getUser(username) {
}

这个签名现在怎么样了?漂亮吧?因为我们只对用户名参数感兴趣,所以不需要取req, res, next等无用的东西.

How does this signature look like now? Pretty, right? Because we're only interested in the username parameter, we don't need to take useless stuff such as req, res, next.

让我们添加缺失的验证器和服务:

Let's add in the missing validators and service:

控制器/user.js

controllers/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 怎么样,不会使我的应用程序崩溃吗?- 嘘,等等.我们还没有完成.

Still looks neat, but... what about the throw new Error, won't that make my application crash? - Shh, wait. We're not done yet.

所以在这一点上,我们的控制器文档看起来是这样的:

So at this point, our controller documentation would look sort of:

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

@returns 中规定的价值"是什么?还记得之前我们说过我们的服务可以是同步的也可以是异步的(使用 Promise)?getUserService 在这种情况下是异步的,但 isChucknorris 服务不会,所以它只会返回一个值而不是一个 Promise.

What's the "value" stated in the @returns? Remember that earlier we said that our services can be both sync or async (using Promise)? getUserService is async in this case, but isChucknorris service wouldn't, so it would simply return a value instead of a Promise.

希望每个人都能阅读文档.因为他们需要处理一些与其他控制器不同的控制器,其中一些需要 try-catch 块.

Hopefully everyone will read the docs. Because they will need to treat some controllers different than others, and some of them will require a try-catch block.

由于我们不能相信开发人员(包括我在内)在尝试之前阅读文档,此时我们必须做出决定:

Since we can't trust developers (this includes me) reading the docs before trying first, at this point we have to make a decision:

  • 控制器强制Promise返回
  • 始终返回承诺的服务

⬑ 这将解决不一致的控制器返回(而不是我们可以省略我们的 try-catch 块的事实).

IMO,我更喜欢第一个选项.因为在大多数情况下,控制器会链接最多的 Promise.

IMO, I prefer the first option. Because controllers are the ones which will chain the most Promises most of the times.

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

如果我们使用 ES6 Promise,我们也可以使用 Promise 的一个很好的属性来做到这一点:Promise 可以在它们的生命周期内处理非 Promise 并且仍然保持返回一个 Promise:

If we are using ES6 Promise we can alternatively make use of a nice property of Promise to do so: Promise can handle non-promises during their lifespan and still keep returning a Promise:

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

如果我们调用的唯一服务没有使用Promise,我们可以自己做一个.

If the only service we call doesn't use Promise, we can make one ourselves.

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

回到我们的例子,它会导致:

Going back to our example it would result in:

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

在这种情况下,我们实际上并不需要 Promise.resolve(),因为 getUserService 已经返回了一个 Promise,但我们希望保持一致.

We don't actually need Promise.resolve() in this case as getUserService already returns a Promise, but we want to be consistent.

如果您对 catch 块感到疑惑:我们不想在控制器中使用它,除非我们想对其进行自定义处理.通过这种方式,我们可以利用两个已经内置的通信渠道(错误消息的例外和成功消息的返回)通过各个渠道传递我们的消息.

If you are wondering about the catch block: we don't want to use it in our controller unless we want to do it a custom treatment. This way we can make use of the two already built-in communication channels (the exception for errors and return for success messages) to deliver our messages through individual channels.

代替 ES6 Promise .then,我们可以使用较新的 ES2017 async/await (现在官方)在我们的控制器中:

Instead of ES6 Promise .then, we can make use of the newer ES2017 async / await (now official) in our controllers:

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

注意function前面的async.

终于有了路由器,耶!

所以我们还没有对用户做出任何回应,我们只有一个控制器,我们知道它总是返回一个 Promise(希望有数据).哦!如果 throw new Error 被调用 或某些服务 Promise 中断,这可能会引发异常.

So we haven't responded anything to the user yet, all we have is a controller that we know that it ALWAYS returns a Promise (hopefully with data). Oh!, and that can possibly throw an exception if throw new Error is called or some service Promise breaks.

路由器将以统一的方式控制请求并将数据返回给客户端,无论是一些现有数据,nullundefined data 或错误.

The router will be the one that will, in an uniform way, control petitions and return data to clients, be it some existing data, null or undefined data or an error.

路由器将是唯一具有多个定义的路由器.其中的数量将取决于我们的拦截器.在假设的情况下,它们是 API(使用 Express)和 Socket(使用 Socket.io).

Router will be the ONLY one that will have multiple definitions. The number of which will depend on our interceptors. In the hypothetical case these were API (with Express) and Socket (with Socket.io).

让我们回顾一下我们必须做的事情:

Let's review what we have to do:

我们希望我们的路由器将 (req, res, next) 转换为 (username).一个简单的版本应该是这样的:

We want our router to convert (req, res, next) into (username). A naive version would be something like this:

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);
  }
});

虽然它运行良好,但如果我们在所有路由中复制粘贴此代码段,则会导致大量代码重复.所以我们必须做一个更好的抽象.

Although it would work well, that would result in a huge amount of code duplication if we copy-pasted this snippet in all our routes. So we have to make a better abstraction.

在这种情况下,我们可以创建一种伪路由器客户端,它接受一个 promise 和 n 参数并执行路由和 return 任务,就像它会做的一样在每条路线中.

In this case, we can create a sort of fake router client that takes a promise and n parameters and does its routing and return tasks, just like it would do in each of the routes.

/**
 * 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 with socket.io(SocketClient.js"部分).

If you are interested in knowing more about this trick, you can read about the full version of this in my other reply in React-Redux and Websockets with socket.io ("SocketClient.js" section).

使用 controllerHandler 后,您的路线会是什么样子?

How would your route look like with the controllerHandler?

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

干净的一行,就像开头一样.

A clean one line, just like in the beginning.

它只适用于那些使用 ES6 Promises 的人.ES2017 async/await 版本对我来说已经很好了.

It only applies to those who use ES6 Promises. ES2017 async / await version already looks good to me.

出于某种原因,我不喜欢必须使用 Promise.resolve() 名称来构建初始化 Promise.只是不清楚那里发生了什么.

For some reason, I dislike having to use Promise.resolve() name to build the initialize Promise. It's just not a clear what's going on there.

我宁愿用更容易理解的东西替换它们:

I'd rather replace them for something more understandable:

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

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

现在您知道 chain 标志着 Promises 链的开始.阅读您代码的每个人也是如此,如果没有,他们至少会假设这是一个服务功能链.

Now you know that chain marks the start of a chain of Promises. So does everyone who reads your code, or if not, they at least assume it's a chain a service functions.

Express 确实有一个默认的错误处理程序,您应该使用它来捕获至少最意外的错误.

Express does have a default error handler which you should be using to capture at least the most unexpected errors.

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: '⁽ƈ ͡ (ुŏ̥̥̥̥םŏ̥̥̥̥) ु' });
});

此外,您可能应该使用类似 debugwinston 而不是 console.error,这是更专业的日志处理方式.

What's more, you should probably be using something like debug or winston instead of console.error, which are more professional ways to handle logs.

这就是我们将它插入controllerHandler的方式:

And that's is how we plug this into the controllerHandler:

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

我们只是将任何捕获的错误重定向到 Express 的错误处理程序.

We are simply redirecting any captured error to Express' error handler.

Error 被认为是在 Javascript 中抛出异常时封装错误的默认类.如果您真的只想跟踪自己控制的错误,我可能会将 throw Error 和 Express 错误处理程序从 Error 更改为 ApiError,您甚至可以通过将其添加到状态字段来使其更适合您的需求.

Error is considered the default class to encapsulate errors in when throwing an exception in Javascript. If you really only want to track your own controlled errors, I'd probably change the throw Error and the Express error handler from Error to ApiError, and you can even make it fit your needs better by adding it the status field.

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

附加信息

自定义例外

您可以在任何时候通过 throw new Error('whatever') 或使用 new Promise((resolve, reject) => reject('whatever')).你只需要玩玩 Promise.

Additional information

Custom exceptions

You can throw any custom exception at any point by throw new Error('whatever') or by using new Promise((resolve, reject) => reject('whatever')). You just have to play with Promise.

这是一个非常固执的观点.IMO ES6(甚至 ES2017,现在拥有一组官方功能)是处理基于 Node 的大型项目的合适方式.

That's very opinionated point. IMO ES6 (or even ES2017, now having an official set of features) is the appropriate way to work on big projects based on Node.

如果您尚未使用它,请尝试查看 ES6 功能和 ES2017Babel 转译器.

If you aren't using it already, try looking at ES6 features and ES2017 and Babel transpiler.

这只是完整的代码(之前已经展示过),没有注释或注释.您可以通过向上滚动到相应部分来检查有关此代码的所有内容.

That's just the complete code (already shown before), with no comments or annotations. You can check everything regarding this code by scrolling up to the appropriate section.

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

controllers/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();
}

模型/用户/index.js

models/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;

模型/用户/validate.js

models/User/validate.js

export function validateUsername(username) {
  return true;
}

这篇关于使用 Node/Express 构建企业应用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆