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-router 。http 模块有一个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。
看起来不错!我们现在正通过3000端口提供内容。这很好,但我们仍然没有提供依赖于路由的内容。例如,如果你导航到http://localhost:3000/test-route,你会看到同样的 "Hello World!"信息。在任何现实世界的应用中,我们都希望我们提供给用户的内容取决于所提供的URL中的内容。
添加和查找路线
我们需要能够向我们的应用程序添加任何数量的路由,并在该路由被调用时执行正确的路由处理函数。为了做到这一点,我们将向我们的模块添加一个routes 数组。此外,我们将创建addRoute 和findRoute 函数。从概念上讲,代码可能看起来是这样的。
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);
};
我们将使用我们的get 和post 方法中的addRoute 方法。findRoute方法简单地返回routes 中与提供的method 和url 匹配的第一个元素。
在下面的片段中,我们添加了数组和两个函数。此外,我们修改了我们的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;
})();
最后,让我们在传递给我们的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,你应该得到一个 "未找到路由 "的消息。成功了!
现在我们要确认我们确实可以在我们的应用程序中添加/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这样的框架之所以如此令人难以置信,是因为它们受到了很多了不起的开发者的关注。它们有强大的设计,而且往往比任何单一的开发者所能部署的解决方案更加高效和安全。