Skip to content

Latest commit

 

History

History
664 lines (503 loc) · 26.8 KB

File metadata and controls

664 lines (503 loc) · 26.8 KB

六、参数和路由

回顾一下,Express.js 应用的典型结构(通常是一个server.jsapp.js文件)大致由这些部分组成,顺序如下:

  1. 依赖关系:导入依赖关系的一组语句
  2. 实例化:创建对象的一组语句
  3. 配置:配置系统和自定义设置的一组语句
  4. 中间件:为每个传入请求执行的一组语句
  5. Routes :定义服务器路由、端点和页面的一组语句
  6. Bootup :一组启动服务器并让它在特定端口监听传入请求的语句

本章包括第五类,路由和我们在路由中定义的 URL 参数。这些参数以及 app.param()中间件是必不可少的,因为它们允许应用访问 URL 中从客户端传递的信息(例如,books/proexpressjs)。这是 REST APIs 最常见的约定。例如,http://hackhall.com/api/posts/521eb002d00c970200000003路由将使用 521 EB 002d 00 c 9702000000003 的值作为帖子 ID。

参数是在请求的 URL 的查询字符串中传递的值。如果我们没有 Express.js 或类似的库,只能使用核心的 Node.js 模块,我们就必须通过某种require('querystring').parse(url)require('url').parse(url, true)函数“诡计”从HTTP.request ( http://nodejs.org/api/http.html#http_http_request_options_callback)对象中提取参数

让我们仔细看看如何为特定的 URL 参数定义特定的规则或逻辑。

参数

从 URL 中提取参数的第一种方法是在请求处理程序(route)中编写一些代码。如果您需要在其他路由中重复这个片段,您可以抽象代码并手动将相同的逻辑应用到许多路由。(To abstract code 的意思是重构代码,以便可以在其他地方重用和/或更好地组织。这提高了代码的可维护性和可读性。)

例如,假设我们需要用户资料页面(/v1/users/azat定义为/v1/users/:username)和管理页面(/v1/admin/azat定义为/v1/admin/:username)上的用户信息。一种方法是定义一个查找用户信息的函数(findUserByUsername),并在每条路线中调用这个函数两次。这是我们实现它的方式(示例ch6/app.js):

var users = {
  'azat': {
    email: 'hi@azat.co',
    website: 'http://azat.co',
    blog: 'http://webapplog.com'
  }
};

var findUserByUsername = function (username, callback) {
  // Perform database query that calls callback when it's done
  // This is our fake database
  if (!users[username])
    return callback(new Error(
      'No user matching '
       + username
      )
    );
  return callback(null, users[username]);
};

app.get('/v1/users/:username', function(request, response, next) {
  var username = request.params.username;
  findUserByUsername(username, function(error, user) {
    if (error) return next(error);
    return response.render('user', user);
  });
});

app.get('/v1/admin/:username', function(request, response, next) {
  var username = request.params.username;
  findUserByUsername(username, function(error, user) {
    if (error) return next(error);
    return response.render('admin', user);
  });
});

您可以使用$ node app命令运行 ch6 文件夹中的应用。然后,打开一个新的终端选项卡/窗口,并使用以下内容来处理 GET 请求:

$ curl http://localhost:3000/v1/users/azat

To see this:

user profile</h2><p>http://azat.co</p><p>http://webapplog.com</p>

并且随着

$ curl http://localhost:3000/v1/admin/azat

要看这个:

admin: user profile</h2><p>hi@azat.co</p><p>http://azat.co</p><p>http://webapplog.com</p><div><Practical>Node.js is your step-by-step guide to learning how to build scalable real-world web applications, taking you from installing Express.js to writing full-stack web applications with powerful libraries such as Mongoskin, Everyauth, Mongoose, Socket.IO, Handlebars, and everything in between.</Practical></div>

Image 注意 Windows 用户可以从http://curl.haxx.se/download.html .下载 CURL

或者,你可以在http://bit.ly/JGSQwr使用 Postman Chrome 扩展。或者,对于 GET 请求,您可以使用浏览器——只需转到 URL。浏览器不会发出上传或删除请求,只有当您提交表单时,它才会发出发布请求。

最后一种方法是使用 jQuery 发出 AJAX/XHR 请求,但是要注意跨源限制,这意味着在服务器上使用相同的域或 CORS 头。或者你可以在你的浏览器中简单地进入http://localhost:3000/v1/users/azat(见图 6-1 )和http://localhost:3000/v1/admin/azat(见图 6-2 )。

9781484200384_Fig06-01.jpg

图 6-1 。用户名 URL 参数被解析并用于查找用户页面上显示的信息(例如 ch6)

9781484200384_Fig06-02.jpg

图 6-2 。用户名 URL 参数被解析并用于查找显示在管理页面上的信息(例如 ch6)

admin.jade模板 ( 图 6-2 )与user.jade ( 图 6-1 )的内容略有不同,以帮助您区分这两个页面/路径,因此您可以确保它们都能正确解析和使用参数。

即使在将大部分代码抽象成findUserByUsername()函数 之后,我们仍然以笨拙的代码结束。如果我们使用中间件方法,代码会变得稍微好一点。想法是编写一个定制的中间件 findUserByUsernameMiddleware,并将其用于需要用户信息的每个路由。下面是如何重构相同的两条路由并使用/v2前缀(前缀通常用于区分 REST API 版本):

var findUserByUsername = function (username, callback) {
  // Perform database query that calls callback when it's done
  // This is our fake database!
  if (!users[username])
    return callback(new Error(
      'No user matching '
       + username
      )
    );
  return callback(null, users[username]);
};
var findUserByUsernameMiddleware = function(request, response, next){
  if (request.params.username) {
    console.log('Username param was detected: ', request.params.username)
    findUserByUsername(request.params.username, function(error, user){
      if (error) return next(error);
      request.user = user;
      return next();
    })
  } else {
    return next();
  }
}
// The v2 routes that use the custom middleware
app.get('/v2/users/:username',
  findUserByUsernameMiddleware,
  function(request, response, next){
  return response.render('user', request.user);
});
app.get('/v2/admin/:username',
  findUserByUsernameMiddleware,
  function(request, response, next){
  return response.render('admin', request.user);
});

中间件 findUserByUsernameMiddleware 检查参数(request.params.username)是否存在,如果存在,则继续获取信息。这是一个更好的模式,因为它保持了路由的精简和逻辑的抽象。然而,Express.js 有一个更好的解决方案。它类似于中间件方法,但是它通过自动执行参数存在检查(即检查参数是否在请求中)使我们的生活变得更简单。遇见app.param()法!

app.param()

只要给定的字符串(例如,username)出现在路由的 URL 模式中,并且服务器接收到与该路由匹配的请求,就会触发对app.param()的回调。例如,使用app.param('username', function(req, res, next, username){...})app.get('/users/:username', findUser)时,每次我们有一个请求/username/azat/username/tjholowaychuk,就会执行app.param()中的关闭(在findUser之前)。

app.param()方法与app.use()非常相似,但是它提供值(在我们的例子中是username)作为函数的第四个,也是最后一个参数。在这个代码片段中,用户名将具有来自 URL 的值(例如,'azat'代表/users/azat):

app.param('username', function (request, response, next, username) {
  *// ... Perform database query and*
  *// ... Store the user object from the database in the req object*
  req.user = user;
  return next();
});

不需要额外的代码行,因为我们有由app.param()填充的req.user对象:

app.get('/users/:username', function(request, response, next) {
  *//... Do something with req.user*
  return res.render(req.user);
});

这条路线也不需要额外的代码。我们免费得到req.user,因为前面定义了app.param():

app.get('/admin/:username', function(request, response, next) {
  *//... Same thing, req.user is available!*
  return res.render(user);
});

下面是我们如何将 param 中间件插入我们的应用的另一个例子:

app.param('id', function(request, response, next, id){
  *// Do something with id*
  *// Store id or other info in req object*
  *// Call next when done*
  next();
});

app.get('/api/v1/stories/:id', function(request, response){
  *// Param middleware will be executed before and*
  *// We expect req objects to already have needed info*
  *// Output something*
  res.send(data);
});

Image 提示如果你有一个大型应用,有很多版本的 API 和 routes (v1、v2 等。),那么最好用Router类/对象来组织这些路由的代码。您创建一个Router对象,并将其挂载到一个路径上,比如/api/api/v1。路由只是var app = express()对象的精简版。关于Router类的更多细节将在本章后面提供。

下面是一个将 param 中间件插入到一个应用中的例子,该应用在req.db中有一个 Mongoskin/Monk 类型的数据库连接:

app.param('id', function(request, response, next, id){
  req.db.get('stories').findOne({_id: id}, function (error, story){
    if (error) return next(error);
    if (!story) return next(new Error('Nothing is found'));
    req.story = story;
    next();
  });
});

app.get('/api/v1/stories/:id', function(request, response){
  res.send(req.story);
});

或者,我们可以使用多个请求处理程序,但概念保持不变:我们可以预期在执行这段代码之前会抛出一个req.story对象或错误,因此我们抽象出获取参数及其各自对象的公共代码/逻辑。这里有一个例子:

app.get('/api/v1/stories/:id', function(request, response, next) {
  *//do authorization*
  },
  *//we have an object in req.story so no work is needed here*
  function(request, response) {
    *//output the result of the database search*
    res.send(story);
});

Image 注意授权和输入卫生是驻留在中间件中的很好的候选者。关于 OAuth 和 Express.js 的广泛示例,请参考实用 node . js1(Apress,2014)。

param()函数特别酷,因为我们可以在路线中组合不同的变量;例如:

app.param('storyId', function(request, response, next, storyId) {
  *// Fetch the story by its ID (storyId) from a database*
  *// Save the found story object into request object*
  *request.story = story;*

});
app.param('elementId', function(request, response, next, elementId) {
  *// Fetch the element by its ID (elementId) from a database*
  *// Narrow down the search when request.story is provided*
  *// Save the found element object into request object*
  *request.element = element;*
});
app.get('/api/v1/stories/:storyId/elements/:elementId', function(request, response){
  // Now we automatically get the story and element in the request object
  res.send({ story: request.story, element: request.element});
});
app.post('/api/v1/stories/:storyId/elements', function(request, response){
  // Now we automatically get the story in the request object
  // We use story ID to create a new element for that story
  res.send({ story: request.story, element: newElement});
});

总之,通过定义 app.param 一次,它的逻辑将为具有匹配 URL 参数名称的每个路由触发。您可能想知道,“它与编写自己的函数并调用它,或者与编写自己的定制中间件有什么不同?”它们都可以正确地执行代码,但是 param 是一种更好的方法。我们可以重构我们之前的例子来展示不同之处。

让我们回到ch6项目。如果我们重构前面来自ch6/app.js的示例,并使用v3作为新的路由前缀,我们可能会得到如下优雅的代码:

app.param('v3Username', function(request, response, next, username){
  console.log(
    'Username param was is detected: ',
    username
  )
  findUserByUsername(
    username,
    function(error, user){
      if (error) return next(error);
      request.user = user;
      return next();
    }
  );
});

app.get('/v3/users/:v3Username',
  function(request, response, next){
    return response.render('user', request.user);
  }
);
app.get('/v3/admin/:v3Username',
  function(request, response, next){
    return response.render('admin', request.user);
  }
);

因此,提取参数很重要,但定义路线更重要。定义路由也是使用app.param()从 URL 参数中提取值的一种替代方法——当一个参数只使用一次时,推荐使用这种方法。如果不止一次使用,param 是更好的模式。

在前五章中已经定义了许多路线。在下一节中,我们将更详细地探索如何定义各种 HTTP 方法,链中间件,抽象中间件代码,以及定义所有方法路由。

路由

Express.js 是一个 Node.js 框架,它提供了一种将路由组织成更小的子部分(路由—Router类/对象的实例)的方法。在 Express.js 3.x 和更早的版本中,定义路由的唯一方式是使用app.VERB()模式,我们将在接下来介绍。然而,从 Express.js v4.x 开始,使用新的Router类是推荐的通过router.route(path)定义路线的方式。我们将首先介绍传统方法。

app。动词()

每个路由都是通过一个应用对象上的方法调用定义的,第一个参数是 URL 模式(也支持正则表达式2);也就是app.METHOD(path, [callback...], callback)

例如,要定义一个 GET /api/v1/stories端点:

app.get('/api/v1/stories/', function(request, response){
  // ...
})

或者,为 POST HTTP 方法和相同的路由定义一个端点:

app.post('/api/v1/stories', function(request, response){
  // ...
})

也支持 DELETE、PUT 和其他方法。更多信息,参见http://expressjs.com/api.html#app.VERB

我们传递给get()post()方法的回调被称为请求处理程序(在第 7 章中有详细介绍),因为它们接受请求(req),处理请求,并写入响应(res)对象。例如:

app.get('/about', function(request, response){
  res.send('About Us: ...');
});

我们可以在一个路由中有多个请求处理器。除了第一个和最后一个之外,它们都将处于流程的中间(它们被执行的顺序),因此得名中间件。它们接受第三个参数/函数next,当被调用时(next(),将执行流切换到下一个处理程序。例如,我们有三个执行授权、数据库搜索和输出的功能:

app.get('/api/v1/stories/:id', function(request, response, next) {
  *// Do authorization*
  *// If not authorized or there is an error*
  *// Return next(error);*
  *// If authorized and no errors*
  return next();
}), function(request, response, next) {
  *// Extract id and fetch the object from the database*
  *// Assuming no errors, save story in the request object*
  request.story = story;
  return next();
}), function(request, response) {
  *// Output the result of the database search*
  res.send(response.story);
});

名称next()是一个任意的约定,这意味着您可以使用任何您喜欢的名称来代替next()。Express.js 使用函数中参数的顺序来确定它们的含义。故事的 ID 是 URL 参数,我们需要它在数据库中查找匹配的条目。

现在,如果我们有另一条路线/admin呢?我们可以定义多个请求处理程序,它们执行资源的认证、验证和加载:

app.get('/admin',
  function(request, response, next) {
    *// Check active session, i.e.,*
    *// Make sure the request has cookies associated with a valid user session*
    *// Check if the user has administrator privileges*
    return next();
  },  function(request, response, next){
    *// Load the information required for admin dashboard*
    *// Such as user list, preferences, sensitive info*
    return next();
  }, function(request, response) {
    *// Render the information with proper templates*
    *// Finish response with a proper status*
    res.end();
   })

但是如果/admin的一些代码,比如授权/认证,是从/stories复制过来的呢?下面的代码完成了同样的事情,但是通过使用命名函数,更加简洁:

var auth = function (request, response, next) {
  // ... Authorization and authentication
  return next();
}
var getStory = function (request, response, next) {
  // ... Database request for story
  return next();
}
var getUsers = function (request, response, next) {
  // ... Database request for users
  return next();
}
var renderPage = function (request, response) {
  if (req.story) res.render('story', story);
  else if (req.users) res.render('users', users);
  else res.end();
}

app.get('/api/v1/stories/:id', auth, getStory, renderPage);
app.get('/admin', auth, getUsers, renderPage);

另一个有用的技术是将回调作为数组的项来传递,这得益于arguments JavaScript 机制的内部工作方式: 3

var authAdmin = function (request, response, next) {
  // ...
  return next();
}
var getUsers = function (request, response, next) {
  // ...
  return next();
}
var renderUsers = function (request, response) {
  // ...
  res.end();
}
var admin = [authAdmin, getUsers, renderUsers];
app.get('/admin', admin);

路由和中间件中的请求处理程序之间的一个明显区别是,我们可以通过调用next('route');来绕过链中的其余回调。如果在前面使用/admin路由的例子中,请求在第一次回调中认证失败,这可能会很方便,在这种情况下没有必要继续。如果有多条路线匹配同一个 URL,您还可以使用next()跳转到下一条路线。

请注意,如果我们传递给app.VERB()的第一个参数包含查询字符串(例如/?debug=true,Express.js 将忽略该信息。例如,app.get('/?debug=true', routes.index);将被完全视为app.get('/', routes.index);

以下是最常用的表述性状态转移(REST) 服务器架构 HTTP 方法及其在 Express.js 中的对应方法以及简要含义:

  • GET:app.get()—检索实体或实体列表
  • HEAD:app.head()—与 GET 相同,只是没有主体
  • 发布:app.post()—提交新实体
  • PUT:app.put()—通过完全替换来更新实体
  • 补丁:app.patch()—部分更新实体
  • 删除:app.delete()app.del()—删除现有实体
  • 选项:app.options()—检索服务器的功能

Image 提示HTTP 方法是每个 HTTP(S)请求的特殊属性,类似于它的头或主体。在浏览器中打开 URL 是 GET 请求,提交表单是 POST 请求。其他类型的请求,如 PUT、DELETE、PATCH 和 OPTIONS,只能通过 CURL、Postman 或定制的应用(前端和后端)等特殊客户端获得。

有关 HTTP 方法的更多信息,请参考 RFC 2616 ( http://tools.ietf.org/html/rfc2616)及其“方法定义”部分(第 9 节)。

app.all()

app.all()方法允许在特定路径上执行指定的请求处理程序,而不管请求的 HTTP 方法是什么。在定义全局或名称空间逻辑时,这个过程可能是救命稻草,如下例所示:

app.all('*', userAuth);
...
app.all('/api/*', apiAuth);

尾随斜线

默认情况下,结尾带有斜杠的路径被视为与正常路径相同。要关闭此功能,请使用app.enable('strict routing');app.set('strict routing', true);。你可以在第三章中了解关于设置选项的更多信息。

路由类别

Router类是一个只有中间件和路由的 mini Express.js 应用。这对于根据它们执行的业务逻辑抽象某些模块很有用。例如,所有的/users/*路由可以在一个路由中定义,而所有的/posts/*路由可以在另一个路由中定义。好处是,在我们用router.path()在路由中定义了 URL 的一部分之后(见下一节),我们不需要一遍又一遍地重复它,就像使用app.VERB()方法一样。

以下是创建路由实例的示例:

var express = require('express');
var router = express.Router(options);
// ... Define routes
app.use('/blog', router);

其中options是可以具有以下属性的对象:

  • caseSensitive : Boolean,表示是否将名称相同但字母大小写不同的路由视为不同,默认为false;例如,如果设置为false,那么/Users/users相同。
  • strict : Boolean,表示是否将名称相同但尾部有无斜杠的路由视为不同,默认为false;例如,如果设置为false,那么/users/users/相同。

router.route(路径)

router.route(path)方法用于链接 HTTP 动词方法。例如,在一个创建、读取、更新和删除(CRUD) 服务器中,对于/posts/:id URL(例如/posts/53fb401dc96c1caa7b78bbdb)有 POST、GET、PUT 和 delete 端点,我们可以如下使用Router类:

var express = require('express');
var router = express.Router();
// ... Importations and configurations
router.param('postId', function(request, response, next) {
  // Find post by ID
  // Save post to request
  request.post = {
    name: 'PHP vs. Node.js',
    url: 'http://webapplog.com/php-vs-node-js'
  };
  return next();
});

router
  .route('/posts/:postId')
  .all(function(request, response, next){
    // This will be called for request with any HTTP method
  })
  .post(function(request, response, next){
  })
  .get(function(request, response, next){
    response.json(request.post);
  })
  .put(function(request, response, next){
    // ... Update the post
    response.json(request.post);
  })
  .delete(function(request, response, next){
    // ... Delete the post
    response.json({'message': 'ok'});
  })

Router.route(path)方法提供了链接方法的便利,这是一种比为每条路线重新键入router更有吸引力的结构化代码的方式。

或者,我们可以使用router.VERB(path, [callback...], callback)来定义路线,就像我们使用app.VERB()一样。同样,router.use()router.param()方法的工作原理与app.use()app.param()相同。

回到我们的示例项目(在ch6文件夹中),我们可以用Router实现v4/users/:usernamev4/admin/:username:

router.param('username', function(request, response, next, username){
  console.log(
    'Username param was detected: ',
    username
  )
  findUserByUsername(
    username,
    function(error, user){
      if (error) return next(error);
      request.user = user;
      return next();
    }
  );
})
router.get('/users/:username',
  function(request, response, next){
    return response.render('user', request.user);
  }
);
router.get('/admin/:username',
  function(request, response, next){
    return response.render('admin', request.user);
  }
);
app.use('/v4', router);

如您所见,router.get()方法没有提到v4。通常,router.get()router.param()方法被抽象成一个单独的文件。这样,主文件(在我们的例子中是app.js)保持精简,易于阅读和维护——这是一个很好的遵循原则!

请求处理程序

Express.js 中的请求处理程序与核心 Node.js http.createServer()方法中的回调惊人地相似,因为它们只是带有reqres参数的函数(匿名、命名或方法):

var ping = function(req, res) {
  console.log('ping');
  res.end(200);
};

app.get('/', ping);

此外,我们可以利用第三个参数next()来控制流程。这与错误处理的主题密切相关,错误处理将在第 9 章的中介绍。下面是两个请求处理程序的简单例子,pingpong,其中前者在打印一个单词 ping 后跳到后者:

var ping = function(req, res, next) {
  console.log('ping');
  return next();
};
var pong = function(req, res) {
  console.log('pong');
  res.end(200);
};
app.get('/', ping, pong);

当请求出现在/路线上时,Express.js 调用ping(),在这种情况下它充当中间件(因为它在中间!).Ping 完成后,用res.end()调用 pong 完成响应。

return关键词也很重要。例如,如果在第一个中间件中认证失败,我们不想继续处理请求:

*// Instantiate app and configure error handling*

*// Authentication middleware*
var checkUserIsAdmin = function (req, res, next) {
  if (req.session && req.session._admin !== true) {
    return next (401);
  }
  return next();
};

*// Admin route that fetches users and calls render function*
var admin = {
  main: function (req, res, next) {
    req.db.get('users').find({}, function(e, users) {
      if (e) return next(e);
      if (!users) return next(new Error('No users to display.'));
      res.render('admin/index.html', users);
   });
  }
};

*// Display list of users for admin dashboard*
app.get('/admin', checkUserIsAdmin, admin.main);

关键字return是必不可少的,因为如果我们不在next(e)调用中使用它,即使有错误和/或我们没有任何用户,应用也会试图呈现(res.render())。例如,以下可能是一个坏主意,因为在我们调用next()之后,这将在错误处理程序中触发适当的错误,流程继续并试图呈现页面:

var admin = {
  main: function (req, res, next) {
    req.db.get('users').find({}, function(e, users) {
      if (e) next(e);
      if (!users) next(new Error('No users to display.'));
      res.render('admin/index.html', users);
   });
  }
};

我们应该使用这样的东西:

if (!users) return next(new Error('No users to display.'));
res.render('admin/index.html', users);

或者类似这样的东西:

if (!users)
  return next(new Error('No users to display.'));
else
  res.render('admin/index.html', users);

摘要

在本章中,我们介绍了 Express.js 应用典型结构的两个主要方面:定义路线和提取 URL 参数。我们探索了如何将它们从 URL 中取出并在请求处理程序中使用它们的三种不同方式(req.params、定制中间件和app.param())。您了解了如何为各种 HTTP 方法定义路由。最后,我们深入研究了充当 mini Express.js 应用的Router类,并使用Router类为示例项目实现了另一组路由。

每次我们定义路由(或中间件)时,我们都在回调中使用匿名函数定义或命名函数来定义请求处理程序。请求处理器通常有三个参数:request(或req)、response(或res)和next。在下一章中,您将了解更多关于这些对象的内容,以及在 Express.js 中,它们与核心 Node.js http模块的requestresponse有何不同。了解这些差异将为您提供更多的特性和功能!


1T0】

2T0】

3 参见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments