栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

使用Node / Express构建企业应用

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

使用Node / Express构建企业应用

控制器是上帝的对象,直到您不希望它们成为…
–您不要说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都倾向于具有向后兼容性,因此它们一生中很可能永远不会中断。
但是 ,它们不可重用。更改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
,这会使我的应用程序崩溃吗?-嘘,等等。我们还没有完成。

因此,在这一点上,我们的控制器文档看起来像:

什么是“价值”

@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
(希望能返回数据)。哦!如果
thrownew 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
任务,就像在每个路由中一样。

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的错误处理程序。

错误为ApiError

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

ES6 ES2017

这是很自以为是的观点。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.jsexport 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;}


转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/469225.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号