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

查看:86
本文介绍了使用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层-控制器/服务/dao.

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层对我来说很清楚.猫鼬模型实际上是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:

  • 我想拥有一个通过AJAX为用户搜索提供服务的API.
  • 我想拥有一个API,该API还可以通过Socket.io提供相同的用户搜索.

让我们从Express开始.很简单,不是吗?

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

routes.js

routes.js

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

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

嗯,这儿闻起来...

Uhm, something smells here...

  • 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: whatever })

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

API一直在变化.将来,您可能希望您的回复是{ err: whatever }或更复杂(更有用)的内容,例如:{ error: whatever, 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 }

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

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:

models/User/validate.js

models/User/validate.js

export function validateUsername(username) {
  return true;
}

您可以在此处详细了解猫鼬4.x验证器的适当结构.

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

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

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.

在大多数情况下(包括这一次),它将使用猫鼬模型并返回 Promise (或回调;但是如果您尚未使用ES6,我肯定会将其与Promises一起使用).

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).

services/user.js

services/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,否则将阻止整个节点. 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.

services/user.js

services/user.js

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

控制器

我们要避免重复的控制器,因此每个动作只有 a 个控制器.

controllers/user.js

controllers/user.js

export function getUser(username) {
}

此签名现在看起来如何?漂亮吧?因为我们只对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:

controllers/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,我更喜欢第一种选择.因为控制器是大多数时候会链接最多Promises的控制器.

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:

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.

Notice async in front of the function.

最后是路由器,是的!

所以我们还没有对用户做出任何响应,我们所拥有的只是一个控制器,我们知道它始终会返回Promise(希望有数据).哦!如果throw new Error is called或某些服务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.

如果您有兴趣了解有关此技巧的更多信息,则可以在我的其他答复中的

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标志着应许链的开始.读取您的代码的每个人也是如此,否则,他们至少会认为这是服务功能的链条.

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

此外,您可能应该使用类似 debug

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 (甚至是

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 功能和 Babel 转译器.

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

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

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

services/user.js

services/user.js

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

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

models/User/validate.js

models/User/validate.js

export function validateUsername(username) {
  return true;
}

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

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