控制器是上帝的对象,直到你不希望它们如此......
- 你不要说 zurfyx (╯°□°)╯︵ ┻━┻
只是对解决方案感兴趣? 跳到最新部分“结果”。
┬──┬◡ノ(° -°ノ)
在开始回答之前,让我为使此响应比通常的 SO 长度更长而道歉。单独的控制器什么都不做,这都是关于整个 MVC 模式的。因此,我觉得有必要详细了解路由器 控制器 服务 模型的所有重要细节,以便向您展示如何以最少的责任实现适当的隔离控制器。
假设情况
让我们从一个小的假设案例开始:
- 我想要一个通过 AJAX 为用户搜索提供服务的 API。
- 我想要一个 API 也可以通过 Socket.io 为相同的用户搜索提供服务。
让我们从 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 都倾向于向后兼容。 但是,它们不可重复使用。更改 Hapi 的 Express?您将不得不重做所有控制器。
另一种可能不那么明显的难闻气味......
控制器响应是手工制作的。 .json({ error: whatever })
RL 中的 API 不断变化。将来您可能希望您的回复是 { err: whatever } 或者更复杂(且有用)的内容,例如:{ error: whatever, status: 500 }
让我们开始吧(一个可能的解决方案)
我不能称它为 解决方案,因为那里有无穷无尽的解决方案。这取决于您的创造力和您的需求。以下是一个不错的解决方案;我在一个相对较大的项目中使用它,它似乎运行良好,它修复了我之前指出的所有问题。
我会去模型 -> 服务 -> 控制器 -> 路由器,让它保持有趣直到结束。
型号
我不会详细介绍模型,因为这不是问题的主题。
您应该具有类似的 Mongoose 模型结构,如下所示:
models/User/validate.js
export function validateUsername(username) {
return true;
}
您可以阅读更多关于 mongoose 4.x 验证器的适当结构here。
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 控制字段的基本用户架构。
我在此处包含 validate 字段的原因是为了让您注意到您应该在此处进行大多数模型验证,而不是在控制器中。
Mongoose Schema 是到达数据库之前的最后一步,除非有人直接查询 MongoDB,否则您将始终放心,每个人都会通过您的模型验证,这比将它们放在控制器上为您提供更多安全性。并不是说像前面示例中那样对验证器进行单元测试是微不足道的。
阅读更多关于 here 和 here 的信息。
服务
该服务将充当处理器。给定可接受的参数,它会处理它们并返回一个值。
大多数时候(包括这个),它会使用Mongoose Models 并返回一个Promise(或一个回调;但I would definitely 使用带有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,否则您将阻塞 the whole Node.js thread。
服务/user.js
function isChucknorris(username) {
return ['Chuck Norris', 'Jon Skeet'].indexOf(username) !== -1;
}
控制器
我们希望避免重复的控制器,因此每个操作只有 一个 控制器。
控制器/user.js
export function getUser(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 返回
- 始终返回 Promise 的服务
⬑ 这将解决不一致的控制器返回(不是我们可以省略 try-catch 块的事实)。
IMO,我更喜欢第一个选项。因为控制器是大多数时候会链接最多 Promise 的控制器。
return findUserByUsername
.then((user) => getChat(user))
.then((chat) => doSomethingElse(chat))
如果我们使用 ES6 Promise,我们也可以使用 Promise 的一个不错的属性来执行此操作:Promise 可以在其生命周期内处理 non-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 块:我们不想在控制器中使用它,除非我们想对其进行自定义处理。这样我们就可以利用两个已经内置的通信通道(错误异常和成功消息返回)通过单独的通道传递我们的消息。
我们可以在控制器中使用较新的 ES2017 async / await (now official),而不是 ES6 Promise .then:
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);
}
});
虽然它会运行良好,但如果我们将这个 sn-p 复制粘贴到所有路由中,将会导致大量代码重复。所以我们必须做一个更好的抽象。
在这种情况下,我们可以创建一种假路由器客户端,它接受一个承诺和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.
如果您有兴趣了解有关此技巧的更多信息,您可以阅读我在React-Redux and Websockets with socket.io(“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 标志着 Promise 链的开始。每个阅读您的代码的人也是如此,或者如果没有,他们至少认为它是一个服务功能链。
表达错误处理程序
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 的错误处理程序。
错误为 ApiError
Error 被认为是在 Javascript 中引发异常时封装错误的默认类。如果您真的只想跟踪自己的受控错误,我可能会将 throw Error 和 Express 错误处理程序从 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')) 引发任何自定义异常。你只需要玩Promise。
ES6 ES2017
这是非常自以为是的观点。 IMO ES6(甚至是 ES2017,现在有一组官方功能)是处理基于 Node 的大型项目的合适方式。
如果您还没有使用它,请尝试查看ES6 功能和ES2017 和Babel 转译器。
结果
这只是完整的代码(之前已经显示),没有 cmets 或注释。您可以通过向上滚动到相应的部分来检查有关此代码的所有内容。
路由器.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();
}
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
export function validateUsername(username) {
return true;
}