使用Node / Express构建企业应用


71

我试图了解如何使用Node / Express / Mongo(实际上使用MEAN堆栈)来构造企业应用。

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

  • 路线
  • 控制器
  • 楷模

但我这个结构看主要问题是,控制器是神一样的物体,他们知道reqres对象,负责验证和有业务逻辑包含英寸

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

我有Scala / Java背景,因此我习惯于将所有逻辑分为3层-控制器/服务/ dao。

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

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

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

  • 关于DAO层对我来说很清楚。猫鼬模型实际上是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

谢谢。



您可以根据自己的需要来构建它。您不必总是直接在控制器下创建文章,而可以始终将其分隔到不同目录下的另一层,例如Service.createArticle并要求使用它(就像Java中的import一样)以供使用
rocketspacer

关于req.body对象,如果您使用了诸如body-parser之类的快速中间件,它应该已经转换为Javascript对象。是的,即使它不再是原始JSON,我们也不会直接将其传递给DTO构造函数。它需要通过一些验证和消毒处理
rocketspacer '17

3
构建项目没有对与错,不同的企业项目具有不同的结构,因为它们执行不同的操作。有些需要支持诸如socket.io之类的实时框架,有些需要具有后台守护程序来使用消息队列,以及其他一些东西。只需按照您了解的方式来构建应用程序,结构就会随着您的应用程序规模而改变
rocketspacer

Answers:


135

控制器是上帝的对象,直到您不希望它们成为...
   –您不要说zurfyx(╯°□°)╯︵┻━┻

只是对解决方案感兴趣? 跳至最新部分“结果”

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

在开始回答之前,让我为使响应时间比通常的SO长度长而道歉。控制器本身什么也没做,只是关于整个MVC模式。因此,我觉得遍历有关路由器<->控制器<->服务<->模型的所有重要细节是有意义的,以便向您展示如何以最小的责任来实现适当的隔离控制器。

假设情况

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

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

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

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 for Hapi吗?您将必须重做所有控制器。

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

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

RL中的API不断变化。将来,您可能希望您的响应{ err: whatever }变得更复杂或更有用(例如):{ error: whatever, status: 500 }

让我们开始吧(可能的解决方案)

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

我将转到模型->服务->控制器->路由器,直到最后都保持有趣。

模型

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

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

模型/用户/validate.js

export function validateUsername(username) {
  return true;
}

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

型号/用户/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模型并返回Promise(或回调;但是如果您还没有这样做的话,我肯定会在Promises中使用ES6)。

服务/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障碍吗?不会,因为稍后我们将做一个很酷的把戏,在这种情况下我们不需要自定义。

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

服务/user.js

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

控制者

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

控制器/user.js

export function getUser(username) {
}

这个签名现在看起来如何?漂亮吧 因为我们只对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's username.
 * @returns A Promise, an exception or a value.
 */

什么是“价值” @returns?还记得我们之前说过我们的服务可以同步还是异步(使用Promise)?getUserService在这种情况下是异步的,但isChucknorris服务不会同步,因此它将仅返回值而不是Promise。

希望每个人都能阅读文档。因为它们将需要将某些控制器与其他控制器区别对待,并且其中某些控制器将需要try-catch阻塞。

由于我们不能信任开发人员(包括我在内)在尝试之前先阅读文档,因此在这一点上,我们必须做出决定:

  • 管制员强迫Promise退货
  • 永远回信的服务

⬑这样可以解决控制器返回不一致的问题(不是可以忽略try-catch块的事实)。

IMO,我更喜欢第一选择。因为控制器是大多数时候会链接最多Promises的控制器。

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

如果我们使用的是ES6 Promise,则可以选择使用的一个不错的属性PromisePromise可以在生命周期内处理非承诺,并且仍然继续返回a 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

路由器

最后是路由器,是的!

因此,我们还没有对用户做出任何响应,我们所拥有的只是一个控制器,我们知道该控制器始终会返回a Promise(希望能返回数据)。哦!如果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);
  }
});

尽管效果很好,但是如果我们在所有路径中复制粘贴此代码段,都会导致大量代码重复。因此,我们必须做出更好的抽象。

在这种情况下,我们可以创建一种伪造的路由器客户端,该客户端接受一个Promise和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.

如果您有兴趣了解更多有关此技巧的信息,可以在我的其他回复中通过socket.io(“ SocketClient.js”部分)在React-Redux和Websockets中阅读有关此技巧的完整版本。

使用时,您的路线如何controllerHandler

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

干净的一行,就像开始时一样。

进一步的可选步骤

控制器承诺

它仅适用于使用ES6 Promises的用户。ES2017async / await版本对我来说已经不错了。

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

我宁愿将它们替换为更易于理解的内容:

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

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

现在您知道这chain标志着承诺链的开始。读取您的代码的每个人也是如此,否则,他们至少会认为这是服务功能的链条。

快速错误处理程序

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

而且,您可能应该使用debugwinston之类的东西来代替console.error,这是处理日志的更专业的方法。

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

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

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

错误为ApiError

Error被视为在Java语言中引发异常时封装错误的默认类。如果您真的只想跟踪自己的受控错误,则可以将throw Error和错误错误处理程序从Error更改为ApiError,甚至可以通过在status字段中添加它来使其更适合您的需求。

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功能以及ES2017Babel 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();
}

型号/用户/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

export function validateUsername(username) {
  return true;
}

1
我喜欢你的controllerHandler把戏。但是,如果我想使用模板引擎渲染一些视图,可以请给我建议吗?我是说res.render('index', {title: 'Hello'})。在这种情况下,控制器和路由的外观如何?
MyTitle

1
@MyTitle我将避免修改控制器。{title: 'Hello'}可以是已经发送到的当前对象值handleController。“索引”可能是函数上的一个额外参数handleController,因为没有控制器对将要呈现结果的视图感兴趣。根据您的需要,您可能希望传递2个或更多视图以处理不同类型的结果或错误,但是首先,我只将其传递给唯一的视图文件名,然后在“ Express Error Handler”中设置默认错误之一。 ”(当前为res.json)。
zurfyx

如何在控制器中获取应用实例。我需要一些触发事件有app.emit('some event')
Amarjit辛格

整洁的controllerHandler实现
Donovan Keating

6

每个人都有自己的方法将项目划分为某些文件夹。我使用的结构是

  • 配置
  • 日志
  • 路线
  • 控制器
  • 楷模
  • 服务
  • 实用程序
  • app.js / server.js / index.js(您喜欢的任何名称)

config文件夹包含用于开发的所有阶段(例如“生产”,“开发”,“测试”)的配置文件(例如数据库连接设置)

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

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

日志文件夹包含您的连接日志错误日志以进行调试

控制器用于验证您的需求数据和业务逻辑

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" })
                    })
                }
            })
        }
    ])
}

模型用于定义数据库模式

示例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)
}

0

rohit salaria的答案基本上解释了您在Java中使用过的相同应用程序结构。

  • 控制器是Java中的控制器
  • 模型是数据访问层
  • 服务是服务层

我有几句话。第一个也是最重要的一个是,这不是Java。听起来似乎很明显,但是只要看看您的问题,便会发现您是在寻找与Java世界中使用过的概念相同的开发经验。我的以下言论仅是对此的解释。

缺少DTO。在Java中,它们只是必需的。在Java Web应用程序中,您将数据存储在关系数据库中,并以JSON的形式向前端发送和接收数据,您自然会将数据转换为Java对象。但是,在Node应用程序中,所有内容都是JavaScript和JSON。这是该平台的优势之一。由于JSON是常见的数据格式,因此无需编写代码或依赖库来在图层的数据格式之间进行转换。

将数据对象直接从请求传递到模型。为什么不?使用JSON作为从前端到数据库的通用数据格式,您可以轻松地在所有层之间同步应用程序的数据模型。当然您不必走这种方式,但是在大多数情况下就足够了,那么为什么不使用它呢?至于验证,它是在模型中完成的,它是根据MVC理论确定的(而不是在懒惰和实用主义经常说的控制器中:)。

最后我想补充一点,当涉及到项目规模扩展时,这不是最佳平台。一点也不奇怪,但是Java在这方面更好。


0

简单而基本的规则

  1. 使相关的组件彼此靠近。

  2. 将页面分为多个部分并工作

  3. 所有相关组件都应该在一起

  4. 共享的事物应该与其他所有组件保持独立。

最后,每种语言都很甜美。就是您对语言的熟悉程度。只有熟悉剑的人才能赢得战斗。

我正在使用NodeJS,Angular2开发Angular2应用程序,我将为您提供目录结构的帮助。

主模块

`主要模块` 

子模块

 子模块结构

共享模块

将共享文件夹保留为单独的模块

希望能帮助到你 :)


我们需要一个共享文件夹吗?
Menai Ala Eddine-阿拉丁

在整个项目中共享的,通用的组件,服务,指令。
manish kumar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.