了解Express.js来创建你自己的Node HTTP请求路由器

81 阅读5分钟

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

开始学习

让我们从模仿Express的 "Hello World "应用程序开始。我们将稍作修改,因为我们不会拉入Express,而是拉入一个我们自己创建的模块。

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

mkdir diy-node-router
cd diy-node-router
npm init -y

验证你的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"
}

接下来,我们将创建我们的index.js 文件。在这个文件中,我们将复制快递 "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}!`));

这与express "Hello World "的例子本质上是一样的。根据这段代码,我们知道我们的router 模块应该是一个函数,在调用时返回一个app 对象。这个对象应该有一个listen 方法来开始监听一个端口上的请求,还有一个get 方法来设置get 请求处理。我们还将设置一个post 方法,因为我们最终会希望我们的应用程序能够处理帖子。

构建diy-router模块的脚手架

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

mkdir src
cd src
touch diy-router.js

我们不想一下子咬掉太多东西,所以让我们先创建一个模块,导出必要的方法。

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;
})();

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

在这一点上,我们可以回到我们的根目录,用node运行我们的应用程序。

node .

如果一切顺利,你会看到像下面这样的输出。

Get method called!
Listen method called!

很好,所有的东西都连在一起了!现在,让我们开始为http请求提供内容。

处理HTTP请求

为了获得一些基本的HTTP请求处理功能,我们将node内置的http 模块引入到我们的diy-routerhttp 模块有一个createServer 方法,它接收一个带有请求和响应参数的函数。每次有HTTP请求被发送到listen 方法中指定的端口时,这个函数就会被执行。下面的示例代码显示了如何使用http 模块来返回端口8080 的文本 "Hello World"。

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

我们想在我们的模块中使用这种功能,但我们需要让用户指定他们自己的端口。此外,我们要执行一个用户提供的回调函数。让我们在diy-router 模块的listen 方法中使用这个例子的功能,并确保在端口和回调函数方面更加灵活。

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;
})();

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

node .

我们看到控制台中记录了以下内容。

Get method called!
Example app listening on port 3000!

这是个好迹象。让我们打开我们最喜欢的网络浏览器,导航到http://localhost:3000。

image.png

看起来不错!我们现在正通过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);
};

我们将使用我们的getpost 方法中的addRoute 方法。findRoute方法简单地返回routes 中与提供的methodurl 匹配的第一个元素。

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

注意:由于routes 数组以及addRoutefindRoute 方法只能在模块内访问,我们可以使用我们的 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;
})();

最后,让我们在传递给我们的createServer 方法的函数中采用findRoute 函数。当一个路由被成功找到时,我们应该调用与之相关的处理函数。如果没有找到该路由,我们应该返回一个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.');

现在让我们把它纳入我们的模块。当我们在做的时候,我们将添加一个额外的代码,为我们的响应对象创建一个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;
})();

让我们看看这个动作再次,从根目录中运行你的应用程序。

node .

你应该看到,应用程序正在3000端口上提供服务。在你的浏览器中,导航到http://localhost:3000。你应该看到 "Hello World!"但是现在,如果你导航到http://localhost:3000/test-route,你应该得到一个 "未找到路由 "的消息。成功了!

image.png

现在我们要确认我们确实可以在我们的应用程序中添加/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}!`));

重新启动服务器并导航到http://localhost:3000/test-route。如果你看到 "测试测试",你已经成功设置了路由

注意:如果你已经玩够了,你可以在这里结束了这是关于路由的一个很好的入门知识。如果你想更深入地挖掘,并能从我们的路由中提取参数,请继续阅读!

提取路由参数

在现实世界中,我们的url字符串中很可能有参数。例如,假设我们有一个用户组,想根据url字符串中的参数来获取一个用户。我们的url字符串最终可能是这样的:/user/:username ,其中username 代表一个与用户相关的唯一标识。

为了创建这个函数,我们可以开发一些正则表达式规则来匹配任何url参数。我建议我们不要这样做,而是使用一个叫做route-parser 的伟大模块来为我们做这件事。route-parser 模块为每个路由创建一个新的对象,该对象有一个match 方法,其中包含所有的正则表达式魔法。为了对我们的模块进行必要的修改,请执行以下操作。

从命令行安装该模块。

npm i route-parser

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

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

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

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

接下来,我们将更新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) };
};

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

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

所以我们的最终模块将看起来像这样。

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;
})();

让我们来测试一下!在我们的index.js 文件中,我们将添加一个新的用户端点,看看我们是否可以通过改变我们的url查询字符串在用户之间切换。修改你的index.js 文件,如下所示。这将根据所提供的请求的params属性来过滤我们的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}!`));

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

node

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

Hello, John Doe!

Hello, Jane Smith!

结论

在这篇文章中,我们观察到,虽然Express是一个令人难以置信的工具,但我们可以通过实现我们自己的自定义模块来复制其路由功能。通过这样的练习,真的有助于拉开 "帷幕",让你意识到真的没有任何 "魔法 "在发挥作用。也就是说,我绝对不会建议你在下一个Node项目中使用自己的框架。像Express这样的框架之所以如此令人难以置信,是因为它们受到了很多了不起的开发者的关注。它们有强大的设计,而且往往比任何单一的开发者所能部署的解决方案更加高效和安全。