使用Node / Express构build企业应用程序
我试图了解如何使用Node / Express / Mongo构build企业应用程序(实际上使用MEAN堆栈)。
在阅读2本书和一些Googlesearch(包括类似的StackOverflow问题)之后,我找不到使用Express构build大型应用程序的好例子。 我读过的所有资源都build议通过以下实体来分割应用程序:
- 路线
- 控制器
- 楷模
但是我看到这个结构的主要问题是控制器就像上帝对象,他们知道req
, res
对象,负责validation,并且包含业务逻辑 。
另一方面,路由在我看来似乎是过度工程,因为他们所做的只是将端点(path)映射到控制器方法。
我有Scala / Java背景,所以我有习惯把3层中的所有逻辑分开 – controller / service / dao。
对我而言,下列陈述是理想的:
-
控制器只负责与WEB部分交互,即编组/解组,一些简单的validation(必需,最小,最大,电子邮件正则expression式等);
-
服务层(实际上我错过了NodeJS / Express应用程序)只负责业务逻辑,一些业务validation。 服务层不知道任何关于WEB部分的东西(也就是说,他们可以从其他应用程序中调用,而不仅仅是从web上下文中调用)。
-
关于DAO层对我来说都是清楚的。 mongoose模型实际上是DAO,所以这里最清楚的是我在这里。
我认为我见过的例子非常简单,只显示了Node / Express的概念,但是我想看看一个真实世界的例子,其中包含许多业务逻辑/validation。
编辑:
还有一件事是不清楚的,就是缺lessDTO对象。 考虑这个例子:
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对象作为创buildMongo文档的parameter passing。 对我来说味道不好 我想用具体的类,而不是原始的JSON
谢谢。
控制者是上帝的对象,直到你不希望他们如此…
– 你不说zurfyx(□°□°)╯(┻━┻
只是对解决scheme感兴趣? 跳转到最新的“结果”部分。
┬──┬◡ノ(° – °ノ)
事先开始的答案,让我道歉,使这种响应方式比通常的SO长度更长。 单独的控制器什么都不做,全都是关于整个MVC模式。 所以,我觉得有必要仔细阅读有关Router < – > Controller < – > Service < – > Model的所有重要细节,以便向您展示如何以最小的责任实现适当的隔离控制器。
假设的情况
我们从一个小的假设案例开始:
- 我想要一个API,通过AJAX为用户search提供服务。
- 我想要一个API,也可以通过Socket.io提供相同的用户search。
我们从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 === '')
。 我们必须两次写入控制器validation器。 如果有n
控制器validation器呢? 我们是否需要保留两份(或更多)的副本? -
User.find({ username })
重复两次。 这可能是一个服务。
我们刚刚写了两个控制器,分别附在Express和Socket.io的确切定义上。 他们很可能永远不会中断,因为Express和Socket.io往往具有向后兼容性。 但是 ,它们不可重用。 改变expression哈比 ? 你将不得不重做所有的控制器。
另一个可能不是那么明显的难闻的气味…
控制器的响应是手工制作的。 .json({ error: whatever })
RL中的API正在不断变化。 在将来你可能希望你的回答是{ err: whatever }
或者更复杂(有用的),比如: { error: whatever, status: 500 }
让我们开始吧(一个可能的解决scheme)
我不能称之为解决scheme,因为那里有无数的解决scheme。 这取决于你的创造力和你的需求。 以下是一个体面的解决scheme; 我在一个相当大的项目中使用它,它似乎运作良好,它修复了我以前指出的一切。
我会去模型 – >服务 – >控制器 – >路由器,保持有趣,直到结束。
模型
我不会详细介绍这个模型,因为这不是问题的主题。
你应该有一个类似的Mongoose模型结构如下:
车型/用户/ validate.js
export function validateUsername(username) { return true; }
您可以在这里阅读更多关于mongoose4.xvalidation器的适当结构。
车型/用户/ 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控制的字段。
我在这里包含validate
字段的原因是为了让您注意,您应该在此处进行大多数模型validation,而不是在控制器中。
Mongoose Schema是到达数据库之前的最后一步,除非有人直接查询MongoDB,否则您始终可以放心,每个人都会通过模型validation,这比您将其放置在控制器上更安全。 不要说像前面的例子那样unit testingvalidation器是微不足道的。
阅读更多关于这里和这里 。
服务
该服务将充当处理器。 给定可接受的参数,它将处理它们并返回一个值。
大多数情况下(包括这一个),它将使用Mongoose模型并返回一个Promise (或者一个callback;但是如果你还没有这样做的话, 我肯定会使用带有Promise的 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) { }
这个签名现在如何? 漂亮吧? 因为我们只对用户名参数感兴趣,所以我们不需要像req, res, next
那样无用的东西。
让我们添加缺失的validation器和服务:
控制器/ 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
的“价值”是@returns
? 记得早些时候我们说过,我们的服务可以是同步的或asynchronous的(使用Promise
)? 在这种情况下, getUserService
是asynchronous的,但是它不是,所以它只会返回一个值而不是一个Promise。
希望每个人都会阅读文档。 因为他们需要处理一些与其他控制器不同的控制器,其中一些控制器需要一个try-catch
块。
由于我们不能相信开发者(这包括我)在先尝试阅读文档,现在我们必须做出决定:
- 控制器强制
Promise
返回 - 服务总是返回一个承诺
⬑这将解决不一致的控制器返回(而不是我们可以省略我们的try-catch块)。
海事组织,我更喜欢第一个选项。 因为pipe制员是大多数时候会把最多的承诺链接起来的人。
return findUserByUsername .then((user) => getChat(user)) .then((chat) => doSomethingElse(chat))
如果我们使用ES6 Promise,我们可以使用Promise的一个很好的属性来做到这一点: Promise
可以在它们的寿命期间处理非承诺,并且仍然不断返回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承诺。那么,我们可以在我们的控制器中使用更新的ES2017 async / await
( 现在是官方的 ):
async function myController() { const user = await findUserByUsername(); const chat = await getChat(user); const somethingElse = doSomethingElse(chat); return somethingElse; }
注意function
前面的async
。
路由器
最后路由器,耶!
所以我们还没有对用户做任何响应,我们所拥有的只是一个控制器,我们知道它总是返回一个Promise
(希望与数据)。 噢!,如果throw new Error is called
或某些服务Promise
中断,可能会抛出exception。
路由器将是一个统一的方式,控制请求和返回数据给客户端,无论是现有的数据, 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); } });
虽然这样做可能会奏效,但是如果我们在所有路线中复制粘贴了这个代码片段,那么这会导致大量的代码重复。 所以我们必须做出更好的抽象。
在这种情况下,我们可以创build一个虚假的路由器客户端,它承担了一个承诺和n
参数,并执行路由和return
任务,就像它在每个路由中所做的一样。
/** * Handles controller execution and responds to user (API Express version). * Web socket has a similar handler implementation. * @param promise Controller Promise. Ie getUser. * @param params A function (req, res, next), all of which are optional * that maps our desired controller parameters. Ie (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和带有socket.io的Websockets (“SocketClient.js”部分)的回复中阅读完整版本。
controllerHandler
路线如何?
router.get('users/:username', c(getUser, (req, res, next) => [req.params.username]));
干净的一条线,就像刚开始时一样。
其他可选步骤
控制器承诺
它只适用于那些使用ES6承诺的人。 ES2017 async / await
版本已经看起来不错。
出于某种原因,我不喜欢使用Promise.resolve()
名称来构build初始化Promise。 这只是不清楚发生了什么事情。
我宁愿把它们换成更容易理解的东西:
const chain = Promise.resolve(); // Write this as an external imported variable or a global. chain .then(() => ...) .then(() => ...)
现在你知道这个chain
标志着一连串的诺言的开始。 那么每个阅读你的代码的人,或者如果不是,他们至less会认为这是一个链式的服务function。
Expresserror handling程序
Express确实有一个默认的error handling程序,您应该使用它来捕获至less最意外的错误。
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); }
我们只是将任何捕获的错误redirect到Express'error handling程序。
错误为ApiError
在JavaScript中引发exception时, Error
被认为是封装错误的默认类。 如果你真的只想跟踪你自己控制的错误,我可能throw Error
和Expresserror handling程序从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'))
来抛出任何自定义exception。 你只需要玩Promise
。
ES6 ES2017
这是非常自负的一点。 IMO ES6 (甚至ES2017 ,现在拥有一套正式的function)是基于Node开展大型项目的合适方式。
如果您还没有使用它,请尝试查看ES6function以及ES2017和Babel转换器。
结果
这只是完整的代码(以前已经显示),没有评论或注释。 您可以通过滚动到相应的部分来检查有关此代码的所有信息。
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; }
每个人都有自己的方式将项目分成若干个文件夹。 我使用的结构是
- configuration
- 日志
- 路线
- 控制器
- 楷模
- 服务
- utils的
- app.js / server.js / index.js(任何你喜欢的名字)
config文件夹包含像“生产”,“开发”,“testing”等所有开发阶段的数据库连接设置的configuration文件。
例
'use strict' var dbsettings = { "production": { //your test settings }, "test": { }, "development": { "database": "be", "username": "yourname", "password": "yourpassword", "host": "localhost", "connectionLimit": 100 } } module.exports = dbsettings
日志文件夹包含您的连接日志错误日志进行debugging
控制器用于validation您的请求数据和业务逻辑
例
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);
服务用于编写数据库查询避免在控制器中写入查询尝试在此文件夹中写入查询并在控制器中调用它
查询使用mongoose
'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是您的项目中常用的常用实用function,如encryption,解密密码等
例
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) }
rohit salaria的回答基本上解释了你在java中使用的相同的应用程序结构。
- 控制器是Java中的控制器
- 模型是数据访问层
- 服务是服务层
不过我有几点意见。 第一个也是最重要的是,这不是Java。 这听起来很明显,但只是看看你的问题,并看到你正在寻找与Java世界中使用的相同的概念相同的开发经验。 我以下的评论只是对此的解释。
缺lessDTO。 在Java中,他们只是需要的时期。 在Java Web应用程序中,将数据存储在关系数据库中,并以JSON方式向前端发送和接收数据,将数据转换为Java对象是很自然的事情。 然而在Node应用程序中,一切都是javascript和JSON。 这是平台的强项之一。 使用JSON作为通用数据格式,不需要编写代码或依赖库来在图层的数据格式之间进行转换。
将数据对象直接从请求传递给模型。 为什么不? 将JSON作为从前端到数据库的常见数据格式,使您可以轻松地在所有图层之间同步应用程序的数据模型。 当然,你不必这样做,但是大部分时间就够了,为什么不使用它呢? 至于validation,它是在模型中完成的,它根据MVC理论(而不是在懒惰和实用主义经常提到的控制器)来说属于它。
对于最后的想法,我想补充说,这不是项目规模缩放的最佳平台。 根本就是点头蝙蝠,但Java在这方面更好。
我已经完成了你想要做的事情。 传统的路由/控制器/模型结构中实质上缺less一层。 简单的答案是,这并没有在节点的领域发展,你想如何呢 – 所以有自定义的东西要做,如果你想操纵的对象。
几个build议,以开始:
- 使用TypeScript而不是JavaScript
- 用HapiJSreplaceExpress
我发现实现这一目标的最有效的方法是让对象具有访问模型的静态方法,然后将其导入到控制器中。 现在 – 这需要花费更多的时间来设置,而不仅仅是遵循节点服务器上的文档 – 但是一旦完成,维护起来非常简单,大型团队的分工非常棒(一旦团队可以专注于路由/控制器,而另一个pipe理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 } }
简单和基本的规则
-
保持组件相互靠近。
-
把页面分成组件和工作
-
所有的依赖组件应该在一起
-
共享的东西应该独立于所有其他组件。
最后每种语言都是甜蜜的 只是你对语言的熟悉程度。如果你熟悉你的剑,你只能赢得这场战斗。
我正在使用NodeJS,Angular2开发Angular2应用程序,我将帮助您使用我的目录结构。
`主模块`
“子模块结构”
将共享文件夹保存为一个单独的模块
希望能帮助到你 :)