理解 Express.js:创建自己的 Node HTTP 请求路由器入门搭建 diy-router 模块处理 HTTP 请求添加和查找路由提取路由器参数最终代码结论

2025-06-10

理解 Express.js:创建自己的 Node HTTP 请求路由器

入门

搭建 diy-router 模块

处理 HTTP 请求

添加和查找路线

提取路由器参数

最终代码

结论

Express 是一个非常棒的 JavaScript 框架,它被用作许多全栈 Web 应用程序的后端。我们中的许多人每天都使用它,并且精通如何使用它,但可能缺乏对它工作原理的理解。今天,我们将在不深入研究 Express 源代码的情况下,重新创建一些路由功能,以便更好地理解该框架的运行环境以及如何处理响应和请求。

如果您想查看最终的源代码,可以在 Github 上找到。为了获得更好的学习体验,请继续和我一起编写代码!

入门

让我们从模拟 Express 的“Hello World”应用程序开始。我们会对它进行一些修改,因为我们不会引入 Express,而是引入我们自己创建的模块。

首先,创建一个新的项目文件夹并使用默认配置启动一个 npm 项目。

mkdir diy-node-router
cd diy-node-router
npm init -y
Enter fullscreen mode Exit fullscreen mode

验证您的package.json文件是否如下所示:

{
  "name": "diy-node-router",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们将创建index.js文件。在这个文件中,我们将复制 Express 的“Hello World”示例,但引入我们自己的模块(我们很快就会创建这个模块)。

const router = require('./src/diy-router');
const app = router();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));
Enter fullscreen mode Exit fullscreen mode

这和“Hello World”示例本质上相同express。基于这段代码,我们知道我们的router模块应该是一个函数,app调用时返回一个对象。这个对象应该包含一个listen用于监听端口请求的方法,以及一个get用于设置get请求处理的方法。我们还需要设置一个post方法,因为我们最终希望我们的应用能够处理用户帖子。

搭建 diy-router 模块

现在我们创建实际的路由器模块。diy-router.js在新src目录中创建文件。

mkdir src
cd src
touch diy-router.js
Enter fullscreen mode Exit fullscreen mode

我们不想一次完成太多,所以我们首先创建一个导出必要方法的模块。

module.exports = (() => {
  const router = () => {
    const get = (route, handler) => {
      console.log('Get method called!');
    };

    const listen = (port, cb) => {
      console.log('Listen method called!');
    };

    return {
      get,
      listen,
    };
  };

  return router;
})();
Enter fullscreen mode Exit fullscreen mode

希望到目前为止一切都说得通:我们创建了一个router函数,当调用时,它会返回一个get和一个listen方法。此时,每个方法都会忽略其参数,只记录它已被调用。然后,这个函数被包装在一个立即调用函数表达式 (IIFE)中。如果您不了解我们为什么使用 IIFE,我们这样做是为了保护数据隐私。在接下来的步骤中,当我们有一些变量和函数不想暴露在模块之外时,这一点会更加明显。

此时,我们可以返回根目录并使用节点运行我们的应用程序。

node .
Enter fullscreen mode Exit fullscreen mode

如果一切顺利,您将看到如下输出:

Get method called!
Listen method called!
Enter fullscreen mode Exit fullscreen mode

完美,一切都连接好了!现在,让我们开始响应 http 请求并提供服务。

处理 HTTP 请求

为了实现一些基本的 HTTP 请求处理功能,我们将 Node 的内置http模块引入到我们的 中diy-router。该http模块有一个createServer方法,该方法接受一个包含请求和响应参数的函数。每次向该方法指定的端口发送 HTTP 请求时,都会执行此函数listen。下面的示例代码展示了如何http使用该模块在端口 上返回文本“Hello World” 8080

http
  .createServer((req, res) => {
    res.write('Hello World!');
    res.end();
  })
  .listen(8080);
Enter fullscreen mode Exit fullscreen mode

我们希望在我们的模块中使用此类功能,但需要让用户指定自己的端口。此外,我们还需要执行用户提供的回调函数。让我们在模块listen的方法中使用此示例功能diy-router,并确保端口和回调函数更加灵活。

const http = require('http');

module.exports = (() => {
  const router = () => {
    const get = (route, handler) => {
      console.log('Get method called!');
    };

    const listen = (port, cb) => {
      http
        .createServer((req, res) => {
          res.write('Hello World!');
          res.end();
        })
        .listen(port, cb);
    };

    return {
      get,
      listen,
    };
  };

  return router;
})();
Enter fullscreen mode Exit fullscreen mode

让我们运行我们的应用程序并看看会发生什么。

node .
Enter fullscreen mode Exit fullscreen mode

我们在控制台中看到以下内容:

Get method called!
Example app listening on port 3000!
Enter fullscreen mode Exit fullscreen mode

这是个好兆头。让我们打开我们最喜欢的网页浏览器,访问http://localhost:3000

你好世界应用程序

看起来不错!我们现在通过 3000 端口提供内容。这很棒,但我们仍然没有提供依赖于路由的内容。例如,如果您导航到http://localhost:3000/test-route,您会看到相同的“Hello World!”消息。在任何实际应用中,我们都希望向用户提供的内容依赖于所提供 URL 中的内容。

添加和查找路线

我们需要能够向应用程序添加任意数量的路由,并在调用该路由时执行正确的路由处理函数。为此,我们将向routes模块添加一个数组。此外,我们将创建addRoutefindRoute函数。理论上,代码可能如下所示:

let routes = [];

const addRoute = (method, url, handler) => {
  routes.push({ method, url, handler });
};

const findRoute = (method, url) => {
  return routes.find(route => route.method === method && route.url === url);
};
Enter fullscreen mode Exit fullscreen mode

我们将使用和方法addRoute中的方法。 findRoute 方法仅返回与提供的和匹配的第一个元素getpostroutesmethodurl

在下面的代码片段中,我们添加了一个数组和两个函数。此外,我们修改了get方法并添加了一个post方法,这两个方法都使用 addRoute 函数将用户指定的路由添加到routes数组中。

注意:由于routes数组和addRoute方法findRoute只能在模块内访问,我们可以使用 IIFE“揭示模块”模式来不在模块外部暴露它们。

const http = require('http');

module.exports = (() => {
  let routes = [];

  const addRoute = (method, url, handler) => {
    routes.push({ method, url, handler });
  };

  const findRoute = (method, url) => {
    return routes.find(route => route.method === method && route.url === url);
  };

  const router = () => {
    const get = (route, handler) => addRoute('get', route, handler);
    const post = (route, handler) => addRoute('post', route, handler);

    const listen = (port, cb) => {
      http
        .createServer((req, res) => {
          res.write('Hello World!');
          res.end();
        })
        .listen(port, cb);
    };

    return {
      get,
      post,
      listen,
    };
  };

  return router;
})();
Enter fullscreen mode Exit fullscreen mode

最后,让我们findRoute在传递给方法的函数中使用该函数createServer。当成功找到路由时,我们应该调用与其关联的处理函数。如果未找到路由,则返回 404 错误,表明未找到该路由。这段代码大致如下所示:

const method = req.method.toLowerCase();
const url = req.url.toLowerCase();
const found = findRoute(method, url);
if (found) {
  return found.handler(req, res);
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Route not found.');
Enter fullscreen mode Exit fullscreen mode

现在让我们把它合并到我们的模块中。同时,我们将添加一段额外的代码,用于send为响应对象创建一个方法。

const http = require('http');

module.exports = (() => {
  let routes = [];

  const addRoute = (method, url, handler) => {
    routes.push({ method, url, handler });
  };

  const findRoute = (method, url) => {
    return routes.find(route => route.method === method && route.url === url);
  };

  const router = () => {
    const get = (route, handler) => addRoute('get', route, handler);
    const post = (route, handler) => addRoute('post', route, handler);

    const listen = (port, cb) => {
      http
        .createServer((req, res) => {
          const method = req.method.toLowerCase();
          const url = req.url.toLowerCase();
          const found = findRoute(method, url);
          if (found) {
            res.send = content => {
              res.writeHead(200, { 'Content-Type': 'text/plain' });
              res.end(content);
            };
            return found.handler(req, res);
          }

          res.writeHead(404, { 'Content-Type': 'text/plain' });
          res.end('Route not found.');
        })
        .listen(port, cb);
    };

    return {
      get,
      post,
      listen,
    };
  };

  return router;
})();
Enter fullscreen mode Exit fullscreen mode

让我们看看实际效果!再次从根目录运行您的应用程序。

node .
Enter fullscreen mode Exit fullscreen mode

你应该看到该应用正在 3000 端口上运行。在浏览器中,导航至http://localhost:3000。你应该看到“Hello World!”。但是现在,如果你导航至http://localhost:3000/test-route,你应该会收到“Route not found”的消息。成功了!

不喜欢的路线

现在我们要确认我们确实可以/test-route在应用程序中将其添加为路由。在 中index.js,设置此路由。

const router = require('./src/diy-router');
const app = router();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));
app.get('/test-route', (req, res) => res.send('Testing testing'));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));
Enter fullscreen mode Exit fullscreen mode

重启服务器并访问http://localhost:3000/test-route。如果看到“Testing Testing”,则表示路由设置成功!

注意:如果你已经玩够了,可以在这里结束了!这篇文章是路由入门的绝佳入门指南。如果你想深入了解,并能够从我们的路由中提取参数,请继续阅读!

提取路由器参数

在现实世界中,我们的 URL 字符串中很可能包含参数。例如,假设我们有一组用户,并希望根据 URL 字符串中的参数获取用户。我们的 URL 字符串最终可能会类似于 ,/user/:username其中username表示与用户关联的唯一标识。

要创建这个函数,我们可以开发一些正则表达式规则来匹配任何 URL 参数。与其这样做,我建议引入一个很棒的模块route-parser来帮我们实现这个功能。该route-parser模块会为每个路由创建一个新的对象,其中包含一个方法,该match方法内置了所有正则表达式的魔法。要在我们的模块中进行必要的更改,请执行以下操作:

从命令行安装模块:

npm i route-parser
Enter fullscreen mode Exit fullscreen mode

在文件的顶部diy-router.js,需要该模块。

const Route = require('route-parser');
Enter fullscreen mode Exit fullscreen mode

addRoute函数中,不要添加计划 url 字符串,而是添加Route类的新实例。

const addRoute = (method, url, handler) => {
  routes.push({ method, url: new Route(url), handler });
};
Enter fullscreen mode Exit fullscreen mode

接下来,我们将更新该findRoute函数。在此更新中,我们使用Route对象的match方法将提供的 URL 与路由字符串进行匹配。换句话说,导航至/user/johndoe将匹配路由字符串/user/:username

如果我们确实找到了匹配项,我们不仅想返回匹配项,还想返回从 url 中提取的参数。

const findRoute = (method, url) => {
  const route = routes.find(route => {
    return route.method === method && route.url.match(url);
  });
  if (!route) return null;
  return { handler: route.handler, params: route.url.match(url) };
};
Enter fullscreen mode Exit fullscreen mode

为了处理这个新功能,我们需要重新审视传递findRoute给 的函数中的调用位置http.createServer。我们需要确保路由中的任何参数都作为属性添加到请求对象中。

if (found) {
  req.params = found.params;
  res.send = content => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end(content);
};
Enter fullscreen mode Exit fullscreen mode

所以我们的最终模块将是这样的:

const http = require('http');
const Route = require('route-parser');

module.exports = (() => {
  let routes = [];

  const addRoute = (method, url, handler) => {
    routes.push({ method, url: new Route(url), handler });
  };

  const findRoute = (method, url) => {
    const route = routes.find(route => {
      return route.method === method && route.url.match(url);
    });

    if (!route) return null;

    return { handler: route.handler, params: route.url.match(url) };
  };

  const get = (route, handler) => addRoute('get', route, handler);
  const post = (route, handler) => addRoute('post', route, handler);

  const router = () => {
    const listen = (port, cb) => {
      http
        .createServer((req, res) => {
          const method = req.method.toLowerCase();
          const url = req.url.toLowerCase();
          const found = findRoute(method, url);

          if (found) {
            req.params = found.params;
            res.send = content => {
              res.writeHead(200, { 'Content-Type': 'text/plain' });
              res.end(content);
            };

            return found.handler(req, res);
          }

          res.writeHead(404, { 'Content-Type': 'text/plain' });
          res.end('Route not found.');
        })
        .listen(port, cb);
    };

    return {
      get,
      post,
      listen,
    };
  };

  return router;
})();
Enter fullscreen mode Exit fullscreen mode

让我们测试一下!在我们的index.js文件中,我们将添加一个新的用户端点,并看看是否可以通过更改 url 查询字符串来在用户之间切换。请按如下方式更改文件。这将根据所提供请求的 params 属性index.js过滤我们的数组。user

const router = require('./src/diy-router');
const app = router();
const port = 3000;

app.get('/', (req, res) => res.send('Hello World!'));
app.get('/test-route', (req, res) => res.send('Testing testing'));
app.get('/user/:username', (req, res) => {
  const users = [
    { username: 'johndoe', name: 'John Doe' },
    { username: 'janesmith', name: 'Jane Smith' },
  ];

  const user = users.find(user => user.username === req.params.username);

  res.send(`Hello, ${user.name}!`);
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));
Enter fullscreen mode Exit fullscreen mode

现在,重新启动您的应用程序。

node
Enter fullscreen mode Exit fullscreen mode

首先导航到http://localhost:3000/user/johndoe,观察内容,然后导航到http://localhost:3000/user/janesmith。您应该分别收到以下响应:

Hello, John Doe!

Hello, Jane Smith!
Enter fullscreen mode Exit fullscreen mode

最终代码

该项目的最终代码可以在 Github 上找到。感谢大家的参与!

结论

在本文中,我们观察到,虽然 Express 是一个非常棒的工具,但我们可以通过实现自己的自定义模块来复制其路由功能。这种练习确实有助于揭开“神秘面纱”,让你意识到这其中并没有什么“魔法”。话虽如此,我绝对不建议你在下一个 Node 项目中使用自己的框架!像 Express 这样的框架如此出色的原因之一是它们受到了众多优秀开发者的关注。它们设计稳健,而且往往比任何单个开发者都能部署的解决方案更高效、更安全。

鏂囩珷鏉ユ簮锛�https://dev.to/nas5w/understanding-express-js-creating-your-own-node-http-request-router-2nli
PREV
使用 Array.Reduce 简化 JavaScript 对象验证 手动验证 创建验证框架 创建可重用的验证函数
NEXT
学习非常有用但经常被忽视的 JavaScript 内置 Set 对象 Set 对象结论