MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 中的路由基础与高级用法

2023-12-313.6k 阅读

Node.js Express 中的路由基础

在 Node.js 的 Express 框架中,路由是至关重要的概念。路由决定了应用如何响应客户端对特定端点(URI)的请求,以及使用何种 HTTP 方法(GET、POST、PUT、DELETE 等)。

基本路由定义

最基础的路由定义方式是使用 Express 实例的app.METHOD()方法,其中METHOD是 HTTP 方法名的小写形式,例如getpost等。以下是一个简单的示例:

const express = require('express');
const app = express();

// 定义一个 GET 请求的路由
app.get('/', (req, res) => {
    res.send('Hello, World!');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在上述代码中,app.get('/')定义了一个处理根路径('/')的 GET 请求的路由。当客户端发送一个 GET 请求到根路径时,服务器会调用回调函数,在这个例子中,回调函数使用res.send('Hello, World!')向客户端返回文本“Hello, World!”。

路由参数

有时我们需要在路由中传递参数,这在处理动态内容时非常有用。例如,我们可能有一个用户资料页面,不同用户的资料通过不同的 ID 来访问。在 Express 中,可以通过在路由路径中使用冒号(:)来定义参数。

const express = require('express');
const app = express();

// 定义一个带有参数的路由
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    res.send(`User ID: ${userId}`);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,/users/:id表示一个接受参数id的路由。当客户端请求/users/123时,req.params.id的值将是'123',服务器会返回“User ID: 123”。

多个参数

路由也可以接受多个参数。例如:

const express = require('express');
const app = express();

// 定义一个带有多个参数的路由
app.get('/products/:category/:productId', (req, res) => {
    const category = req.params.category;
    const productId = req.params.productId;
    res.send(`Category: ${category}, Product ID: ${productId}`);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里/products/:category/:productId接受两个参数categoryproductId。如果客户端请求/products/electronics/456,服务器将返回“Category: electronics, Product ID: 456”。

可选参数

在 Express 中,还可以定义可选参数。这可以通过在参数名后面加上问号(?)来实现。

const express = require('express');
const app = express();

// 定义一个带有可选参数的路由
app.get('/posts/:postId?', (req, res) => {
    if (req.params.postId) {
        res.send(`Post ID: ${req.params.postId}`);
    } else {
        res.send('All posts');
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,/posts/:postId?表示postId是一个可选参数。如果客户端请求/posts/789,服务器将返回“Post ID: 789”;如果请求/posts,服务器将返回“All posts”。

路由匹配规则

理解 Express 中的路由匹配规则对于正确定义和管理路由至关重要。

顺序匹配

Express 按照路由定义的顺序来匹配请求。这意味着如果有两个相似的路由,先定义的路由会优先匹配。例如:

const express = require('express');
const app = express();

// 先定义这个路由
app.get('/users', (req, res) => {
    res.send('All users');
});

// 后定义这个路由
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    res.send(`User ID: ${userId}`);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,如果客户端请求/users,会匹配到第一个路由,返回“All users”。只有当请求类似于/users/123时,才会匹配到第二个路由。

精确匹配

Express 路由默认是精确匹配的。例如,app.get('/about', ...)只会匹配/about路径,而不会匹配/about//about/other。如果想要匹配以/about开头的所有路径,可以使用路径模式匹配。

路径模式匹配

可以使用正则表达式来进行路径模式匹配。例如:

const express = require('express');
const app = express();

// 使用正则表达式匹配以 /images/ 开头,后面跟数字的路径
app.get(/^\/images\/\d+$/, (req, res) => {
    res.send('Matched an image path with a number');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,/images/123会匹配到该路由,但/images/text不会匹配。

路由处理函数

路由处理函数是当路由匹配成功时执行的函数。它接受req(请求对象)和res(响应对象)作为参数。

单个处理函数

前面的例子中我们已经看到了单个处理函数的使用,例如:

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

这里的箭头函数就是一个简单的路由处理函数,它直接向客户端发送文本。

多个处理函数

一个路由可以使用多个处理函数,这些函数会按照定义的顺序依次执行。例如:

const express = require('express');
const app = express();

const logger = (req, res, next) => {
    console.log(`Request received at ${new Date()}`);
    next();
};

const authenticate = (req, res, next) => {
    // 简单的模拟认证,这里假设所有请求都通过认证
    console.log('User authenticated');
    next();
};

app.get('/protected', [logger, authenticate], (req, res) => {
    res.send('This is a protected route');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,/protected路由使用了loggerauthenticate两个中间件函数,然后才是最终的处理函数。logger函数记录请求时间,authenticate函数模拟认证过程,最后一个函数向客户端发送响应。

异步处理函数

在处理函数中经常会遇到异步操作,例如读取文件、查询数据库等。可以使用async/await来处理异步操作。

const express = require('express');
const app = express();
const fs = require('fs').promises;

app.get('/file', async (req, res) => {
    try {
        const data = await fs.readFile('example.txt', 'utf8');
        res.send(data);
    } catch (error) {
        res.status(500).send('Error reading file');
    }
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里使用async/await来读取文件内容,如果读取成功则发送文件内容给客户端,如果出错则返回错误信息。

路由模块化

随着应用程序的增长,将路由逻辑模块化可以提高代码的可维护性和可扩展性。

创建路由模块

可以使用express.Router()来创建一个路由模块。例如,我们创建一个users.js文件来处理用户相关的路由:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.send('All users');
});

router.get('/:id', (req, res) => {
    const userId = req.params.id;
    res.send(`User ID: ${userId}`);
});

module.exports = router;

在这个文件中,我们使用express.Router()创建了一个router对象,并在其上定义了两个路由。最后通过module.exportsrouter对象导出。

使用路由模块

在主应用文件中,可以引入并使用这个路由模块。例如:

const express = require('express');
const app = express();
const usersRouter = require('./users');

app.use('/users', usersRouter);

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这里通过app.use('/users', usersRouter)usersRouter挂载到/users路径下。现在,访问/users/users/:id就会调用users.js中定义的路由处理函数。

高级路由用法

嵌套路由

在 Express 中可以创建嵌套路由,这在处理复杂的层次结构数据时非常有用。例如,我们有一个博客应用,每个文章可能有多个评论。

首先,创建一个articles.js路由模块:

const express = require('express');
const router = express.Router();
const commentsRouter = require('./comments');

router.get('/', (req, res) => {
    res.send('All articles');
});

router.get('/:articleId', (req, res) => {
    const articleId = req.params.articleId;
    res.send(`Article ID: ${articleId}`);
});

router.use('/:articleId/comments', commentsRouter);

module.exports = router;

然后,创建一个comments.js路由模块:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
    res.send('All comments for this article');
});

router.get('/:commentId', (req, res) => {
    const commentId = req.params.commentId;
    res.send(`Comment ID: ${commentId}`);
});

module.exports = router;

在主应用文件中:

const express = require('express');
const app = express();
const articlesRouter = require('./articles');

app.use('/articles', articlesRouter);

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

这样,/articles/123/comments会匹配到comments.js中的路由,实现了嵌套路由。

路由中间件

路由中间件是在路由处理函数之前执行的函数,它可以用于多种目的,如日志记录、认证、错误处理等。

自定义路由中间件

可以创建自定义的路由中间件。例如,一个用于验证用户是否登录的中间件:

const express = require('express');
const app = express();

const isLoggedIn = (req, res, next) => {
    // 简单模拟,假设请求头中有 'logged-in' 字段则表示已登录
    if (req.headers['logged - in']) {
        next();
    } else {
        res.status(401).send('Unauthorized');
    }
};

app.get('/admin', isLoggedIn, (req, res) => {
    res.send('Welcome to the admin area');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,isLoggedIn中间件会在/admin路由的处理函数之前执行,检查用户是否已登录。

错误处理中间件

错误处理中间件用于捕获路由处理函数中抛出的错误。它与普通中间件的区别在于,它接受四个参数:(err, req, res, next)

const express = require('express');
const app = express();

app.get('/error', (req, res) => {
    throw new Error('Something went wrong');
});

app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Something went wrong!');
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个例子中,当/error路由处理函数抛出错误时,错误处理中间件会捕获并处理这个错误,向客户端返回“Something went wrong!”。

路由与 RESTful 架构

RESTful 架构是一种设计网络应用程序的架构风格,Express 路由可以很好地与 RESTful 原则结合。

RESTful 路由设计

在 RESTful 架构中,不同的 HTTP 方法对应不同的操作。例如:

  • GET:用于获取资源,如GET /users获取所有用户,GET /users/123获取特定用户。
  • POST:用于创建新资源,如POST /users创建一个新用户。
  • PUT:用于更新资源,如PUT /users/123更新 ID 为 123 的用户。
  • DELETE:用于删除资源,如DELETE /users/123删除 ID 为 123 的用户。

以下是一个简单的 RESTful 风格的用户路由示例:

const express = require('express');
const app = express();

// 获取所有用户
app.get('/users', (req, res) => {
    // 这里应该查询数据库获取所有用户数据并返回
    res.send('All users');
});

// 获取特定用户
app.get('/users/:id', (req, res) => {
    const userId = req.params.id;
    // 这里应该根据 ID 查询数据库获取特定用户数据并返回
    res.send(`User ID: ${userId}`);
});

// 创建新用户
app.post('/users', (req, res) => {
    // 这里应该从请求体中获取用户数据并保存到数据库
    res.send('User created');
});

// 更新特定用户
app.put('/users/:id', (req, res) => {
    const userId = req.params.id;
    // 这里应该从请求体中获取更新数据并更新数据库中的用户数据
    res.send(`User ID ${userId} updated`);
});

// 删除特定用户
app.delete('/users/:id', (req, res) => {
    const userId = req.params.id;
    // 这里应该从数据库中删除指定 ID 的用户数据
    res.send(`User ID ${userId} deleted`);
});

const port = 3000;
app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

通过这种方式,使用不同的 HTTP 方法和路由来实现对资源的增删改查操作,符合 RESTful 架构的理念。

总结路由的最佳实践

在使用 Express 路由时,有一些最佳实践可以遵循,以确保代码的质量和可维护性。

保持路由简洁

每个路由处理函数应该只做一件事,避免在一个处理函数中包含过多复杂的逻辑。例如,如果一个路由处理函数既需要验证用户,又需要查询数据库并处理业务逻辑,应该将验证用户的部分提取到中间件中,使路由处理函数专注于业务逻辑。

合理命名路由

路由路径应该具有描述性,能够清晰地表达该路由的功能。例如,/users/login/u/l更容易理解。对于资源相关的路由,使用复数形式来表示集合,如/products表示所有产品,/products/123表示特定产品。

错误处理

在每个路由处理函数中都要考虑错误处理。无论是异步操作失败还是业务逻辑出现错误,都应该返回合适的 HTTP 状态码和错误信息。同时,利用全局的错误处理中间件来捕获未处理的错误,确保应用程序不会因为错误而崩溃。

安全考虑

在处理路由时要注意安全问题。例如,对用户输入进行验证和过滤,防止 SQL 注入、XSS 等攻击。对于敏感操作(如删除用户、更新重要信息),要进行严格的权限验证。

性能优化

对于频繁访问的路由,可以考虑使用缓存来提高性能。例如,如果一个路由返回的数据不经常变化,可以将其缓存起来,避免每次都进行数据库查询或其他耗时操作。同时,合理使用中间件,避免不必要的中间件执行,以减少请求处理的时间。

通过遵循这些最佳实践,可以构建出健壮、可维护且高性能的 Express 应用程序,充分发挥路由在应用中的核心作用。无论是小型项目还是大型企业级应用,合理设计和使用路由都是关键。在实际开发中,根据项目的具体需求和规模,灵活运用路由的各种特性和高级用法,不断优化代码,以提供更好的用户体验和服务质量。同时,随着业务的发展和变化,要及时调整和完善路由设计,确保应用程序能够持续稳定地运行。在日常开发中,多参考优秀的开源项目和行业最佳实践案例,不断积累经验,提升自己在 Express 路由应用方面的能力。这样,在面对各种复杂的业务场景时,都能够得心应手地运用 Express 路由来构建高效、可靠的 Web 应用程序。在实际工作中,还需要与团队成员进行良好的沟通和协作,共同制定合理的路由策略,确保整个项目的架构清晰、易于维护。当项目规模扩大时,对路由进行有效的分层和模块化管理,将有助于进一步提高代码的可维护性和扩展性。同时,要关注 Express 框架的更新和发展,及时引入新的特性和优化,使应用程序始终保持在一个较高的水平。总之,熟练掌握和运用 Express 路由是 Node.js 前端开发中至关重要的一环,对于构建高质量的 Web 应用具有不可忽视的作用。