处理URL

在hello-koa工程中,我们处理http请求一律返回相同的HTML,这样虽然非常简单,但是用浏览器一测,随便输入任何URL都会返回相同的网页。

buduijin

正常情况下,我们应该对不同的URL调用不同的处理函数,这样才能返回不同的结果。例如像这样写:

  1. app.use(async (ctx, next) => {
  2. if (ctx.request.path === '/') {
  3. ctx.response.body = 'index page';
  4. } else {
  5. await next();
  6. }
  7. });
  8. app.use(async (ctx, next) => {
  9. if (ctx.request.path === '/test') {
  10. ctx.response.body = 'TEST page';
  11. } else {
  12. await next();
  13. }
  14. });
  15. app.use(async (ctx, next) => {
  16. if (ctx.request.path === '/error') {
  17. ctx.response.body = 'ERROR page';
  18. } else {
  19. await next();
  20. }
  21. });

这么写是可以运行的,但是好像有点蠢。

应该有一个能集中处理URL的middleware,它根据不同的URL调用不同的处理函数,这样,我们才能专心为每个URL编写处理函数。

koa-router

为了处理URL,我们需要引入koa-router这个middleware,让它负责处理URL映射。

我们把上一节的hello-koa工程复制一份,重命名为url-koa

先在package.json中添加依赖项:

  1. "koa-router": "7.0.0"

然后用npm install安装。

接下来,我们修改app.js,使用koa-router来处理URL:

  1. const Koa = require('koa');
  2. // 注意require('koa-router')返回的是函数:
  3. const router = require('koa-router')();
  4. const app = new Koa();
  5. // log request URL:
  6. app.use(async (ctx, next) => {
  7. console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
  8. await next();
  9. });
  10. // add url-route:
  11. router.get('/hello/:name', async (ctx, next) => {
  12. var name = ctx.params.name;
  13. ctx.response.body = `<h1>Hello, ${name}!</h1>`;
  14. });
  15. router.get('/', async (ctx, next) => {
  16. ctx.response.body = '<h1>Index</h1>';
  17. });
  18. // add router middleware:
  19. app.use(router.routes());
  20. app.listen(3000);
  21. console.log('app started at port 3000...');

注意导入koa-router的语句最后的()是函数调用:

  1. const router = require('koa-router')();

相当于:

  1. const fn_router = require('koa-router');
  2. const router = fn_router();

然后,我们使用router.get('/path', async fn)来注册一个GET请求。可以在请求路径中使用带变量的/hello/:name,变量可以通过ctx.params.name访问。

再运行app.js,我们就可以测试不同的URL:

输入首页:http://localhost:3000/

url-index

输入:http://localhost:3000/hello/koa

url-hello

处理post请求

router.get('/path', async fn)处理的是get请求。如果要处理post请求,可以用router.post('/path', async fn)

用post请求处理URL时,我们会遇到一个问题:post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!

所以,我们又需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body中。

koa-bodyparser就是用来干这个活的。

我们在package.json中添加依赖项:

  1. "koa-bodyparser": "3.2.0"

然后使用npm install安装。

下面,修改app.js,引入koa-bodyparser

  1. const bodyParser = require('koa-bodyparser');

在合适的位置加上:

  1. app.use(bodyParser());

由于middleware的顺序很重要,这个koa-bodyparser必须在router之前被注册到app对象上。

现在我们就可以处理post请求了。写一个简单的登录表单:

  1. router.get('/', async (ctx, next) => {
  2. ctx.response.body = `<h1>Index</h1>
  3. <form action="/signin" method="post">
  4. <p>Name: <input name="name" value="koa"></p>
  5. <p>Password: <input name="password" type="password"></p>
  6. <p><input type="submit" value="Submit"></p>
  7. </form>`;
  8. });
  9. router.post('/signin', async (ctx, next) => {
  10. var
  11. name = ctx.request.body.name || '',
  12. password = ctx.request.body.password || '';
  13. console.log(`signin with name: ${name}, password: ${password}`);
  14. if (name === 'koa' && password === '12345') {
  15. ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
  16. } else {
  17. ctx.response.body = `<h1>Login failed!</h1>
  18. <p><a href="/">Try again</a></p>`;
  19. }
  20. });

注意到我们用var name = ctx.request.body.name || ''拿到表单的name字段,如果该字段不存在,默认值设置为''

类似的,put、delete、head请求也可以由router处理。

重构

现在,我们已经可以处理不同的URL了,但是看看app.js,总觉得还是有点不对劲。

still-buduijin

所有的URL处理函数都放到app.js里显得很乱,而且,每加一个URL,就需要修改app.js。随着URL越来越多,app.js就会越来越长。

如果能把URL处理函数集中到某个js文件,或者某几个js文件中就好了,然后让app.js自动导入所有处理URL的函数。这样,代码一分离,逻辑就显得清楚了。最好是这样:

  1. url2-koa/
  2. |
  3. +- .vscode/
  4. | |
  5. | +- launch.json <-- VSCode 配置文件
  6. |
  7. +- controllers/
  8. | |
  9. | +- login.js <-- 处理login相关URL
  10. | |
  11. | +- users.js <-- 处理用户管理相关URL
  12. |
  13. +- app.js <-- 使用koajs
  14. |
  15. +- package.json <-- 项目描述文件
  16. |
  17. +- node_modules/ <-- npm安装的所有依赖包

于是我们把url-koa复制一份,重命名为url2-koa,准备重构这个项目。

我们先在controllers目录下编写index.js

  1. var fn_index = async (ctx, next) => {
  2. ctx.response.body = `<h1>Index</h1>
  3. <form action="/signin" method="post">
  4. <p>Name: <input name="name" value="koa"></p>
  5. <p>Password: <input name="password" type="password"></p>
  6. <p><input type="submit" value="Submit"></p>
  7. </form>`;
  8. };
  9. var fn_signin = async (ctx, next) => {
  10. var
  11. name = ctx.request.body.name || '',
  12. password = ctx.request.body.password || '';
  13. console.log(`signin with name: ${name}, password: ${password}`);
  14. if (name === 'koa' && password === '12345') {
  15. ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
  16. } else {
  17. ctx.response.body = `<h1>Login failed!</h1>
  18. <p><a href="/">Try again</a></p>`;
  19. }
  20. };
  21. module.exports = {
  22. 'GET /': fn_index,
  23. 'POST /signin': fn_signin
  24. };

这个index.js通过module.exports把两个URL处理函数暴露出来。

类似的,hello.js把一个URL处理函数暴露出来:

  1. var fn_hello = async (ctx, next) => {
  2. var name = ctx.params.name;
  3. ctx.response.body = `<h1>Hello, ${name}!</h1>`;
  4. };
  5. module.exports = {
  6. 'GET /hello/:name': fn_hello
  7. };

现在,我们修改app.js,让它自动扫描controllers目录,找到所有js文件,导入,然后注册每个URL:

  1. // 先导入fs模块,然后用readdirSync列出文件
  2. // 这里可以用sync是因为启动时只运行一次,不存在性能问题:
  3. var files = fs.readdirSync(__dirname + '/controllers');
  4. // 过滤出.js文件:
  5. var js_files = files.filter((f)=>{
  6. return f.endsWith('.js');
  7. });
  8. // 处理每个js文件:
  9. for (var f of js_files) {
  10. console.log(`process controller: ${f}...`);
  11. // 导入js文件:
  12. let mapping = require(__dirname + '/controllers/' + f);
  13. for (var url in mapping) {
  14. if (url.startsWith('GET ')) {
  15. // 如果url类似"GET xxx":
  16. var path = url.substring(4);
  17. router.get(path, mapping[url]);
  18. console.log(`register URL mapping: GET ${path}`);
  19. } else if (url.startsWith('POST ')) {
  20. // 如果url类似"POST xxx":
  21. var path = url.substring(5);
  22. router.post(path, mapping[url]);
  23. console.log(`register URL mapping: POST ${path}`);
  24. } else {
  25. // 无效的URL:
  26. console.log(`invalid URL: ${url}`);
  27. }
  28. }
  29. }

如果上面的大段代码看起来还是有点费劲,那就把它拆成更小单元的函数:

  1. function addMapping(router, mapping) {
  2. for (var url in mapping) {
  3. if (url.startsWith('GET ')) {
  4. var path = url.substring(4);
  5. router.get(path, mapping[url]);
  6. console.log(`register URL mapping: GET ${path}`);
  7. } else if (url.startsWith('POST ')) {
  8. var path = url.substring(5);
  9. router.post(path, mapping[url]);
  10. console.log(`register URL mapping: POST ${path}`);
  11. } else {
  12. console.log(`invalid URL: ${url}`);
  13. }
  14. }
  15. }
  16. function addControllers(router) {
  17. var files = fs.readdirSync(__dirname + '/controllers');
  18. var js_files = files.filter((f) => {
  19. return f.endsWith('.js');
  20. });
  21. for (var f of js_files) {
  22. console.log(`process controller: ${f}...`);
  23. let mapping = require(__dirname + '/controllers/' + f);
  24. addMapping(router, mapping);
  25. }
  26. }
  27. addControllers(router);

确保每个函数功能非常简单,一眼能看明白,是代码可维护的关键。

Controller Middleware

最后,我们把扫描controllers目录和创建router的代码从app.js中提取出来,作为一个简单的middleware使用,命名为controller.js

  1. const fs = require('fs');
  2. function addMapping(router, mapping) {
  3. ...
  4. }
  5. function addControllers(router, dir) {
  6. ...
  7. }
  8. module.exports = function (dir) {
  9. let
  10. controllers_dir = dir || 'controllers', // 如果不传参数,扫描目录默认为'controllers'
  11. router = require('koa-router')();
  12. addControllers(router, controllers_dir);
  13. return router.routes();
  14. };

这样一来,我们在app.js的代码又简化了:

  1. ...
  2. // 导入controller middleware:
  3. const controller = require('./controller');
  4. ...
  5. // 使用middleware:
  6. app.use(controller());
  7. ...

经过重新整理后的工程url2-koa目前具备非常好的模块化,所有处理URL的函数按功能组存放在controllers目录,今后我们也只需要不断往这个目录下加东西就可以了,app.js保持不变。

参考源码

url-koa

url2-koa

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

处理URL - 图5 处理URL - 图6