我试图了解如何使用Node / Express / Mongo(实际上使用MEAN堆栈)来构造企业应用。
在阅读了2本书和一些谷歌搜索(包括类似的问题)之后,我找不到使用Express构建大型应用程序的任何好例子。我读过的所有资料都建议按以下实体拆分应用程序:
但我这个结构看主要问题是,控制器是神一样的物体,他们知道req
,res
对象,负责验证和 有业务逻辑 包含英寸
另一方面,路由在我看来就像过度设计,因为它们所做的只是将端点(路径)映射到控制器方法。
我有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
谢谢。
控制器是上帝的对象,直到您不希望它们成为…
–您不要说zurfyx(╯°□°)╯︵┻━┻
只是对解决方案感兴趣? 跳至 最新部分 “结果” 。
┬──┬◡ノ(°-°ノ)
在开始回答之前,让我为使响应时间比通常的SO长度长而道歉。控制器本身什么也没做,只是关于整个MVC模式。因此,我觉得遍历有关路由器<->控制器<->服务<->模型的所有重要细节是有意义的,以便向您展示如何以最小的责任来实现适当的隔离控制器。
让我们从一个小的假设案例开始:
让我们从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;
}
您可以在此处]阅读有关mongoose 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,则可以选择使用的一个不错的属性Promise
:Promise
可以在生命周期内处理非承诺,并且仍然继续返回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
。
最后是路由器,是的!
因此,我们还没有对用户做出任何响应,我们所拥有的只是一个控制器,我们知道该控制器始终会返回一个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的React-
Redux和Websockets中阅读 “ SocketClient.js”部分),了解有关此
技巧 的完整版本。
使用时,您的路线如何controllerHandler
?
router.get('users/:username', c(getUser, (req, res, next) => [req.params.username]));
干净的一行,就像开始时一样。
它仅适用于使用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
标志着承诺链的开始。读取您的代码的每个人也是如此,否则,他们至少会认为这是服务功能的链条。
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的错误处理程序。
Error
被视为在Javascript中引发异常时封装错误的默认类。如果您真的只想跟踪自己的受控错误,我可能会将和throwError
错误错误处理程序从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
。
这是很自以为是的观点。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();
}
型号/用户/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;
}
通过调用API接口,将企业已有应用接入企业微信,并展示在工作台中,供成员使用。 创建应用 1 / 创建新的应用 01/02在【管理后台】>【企业应用】> 【自建应用】中选择【+创建应用】。 02/02完成应用logo/应用名称/应用介绍/可见范围等基本设置。 2 / 接口设置 创建应用后,根据应用将满足的办公场景,选择不同的API接口。目前支持: 发送消息/接收消息/自动回复 网页授权及JS-SD
目标: 用 Docker 镜像的方式搭建 Node Express 应用 本项目代码:node-express-docker-sample Demo :http://yeting-front-node-express-docker-sample.daoapp.io/ Node Express 应用搭建 首先,借助 Yeomen Express generator 生成一个 Node Express
由企业内部开发的应用,可连接企业内部系统,仅能在本企业内使用 1、进入企业管理平台-应用中心,创建应用 2、填写应用基本信息 3、从通讯录中选择应用可见范围,应用仅会出现在可见范围内员工的客户端上
本文向大家介绍详解node+express+ejs+bootstrap构建项目,包括了详解node+express+ejs+bootstrap构建项目的使用技巧和注意事项,需要的朋友参考一下 node+express+ejs+bootstrap是前端常用的项目结构,分享给大家,具体如下: 您可以通过node-express_jb51.rar 来克隆我创建好的项目结构,也可以通过下面的方式一步一步手
企业授权应用流程 PDF版下载 企业的系统管理员从管理后台-应用市场发起授权安装第三方应用,安装后如流平台会将授权凭证、授权信息等推送给服务商后台。 详细步骤: 企业管理员授权安装应用 企业的系统管理员从管理后台应用市场发起授权安装第三方应用; 授权成功,回调临时授权码 a) 授权成功后,如流企业平台会回调应用开发信息中填写的安装事件回调地址,GET参数上带上临时授权码auth_code; b)
我正在编写一个使用React作为前端的程序,以及一个用于后端的Express/Node API,然后在MongoDB数据库中执行CRUD操作。现在,我正在使用本地JS抓取()API在我的前端执行GET/POST操作。GET请求工作正常,但是我的POST请求似乎不工作。在我的前端,我有一个表单和一个表单提交处理程序,如下所示: 然后在端口5002上的Express API上,我有: 然而,当表单提交