Express 4.x API
express()
express()用来创建一个Express的程序。express()方法是express模块导出的顶层方法。
var express = require('express');
var app = express();Methods
express.static(root, [options])
express.static是Express中唯一的内建中间件。它以server-static模块为基础开发,负责托管 Express 应用内的静态资源。
参数root为静态资源的所在的根目录。
参数options是可选的,支持以下的属性:
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| dotfiles | 是否响应点文件。供选择的值有"allow","deny"和"ignore" | String | "ignore" |
| etag | 使能或者关闭etag | Boolean | true |
| extensions | 设置文件延期回退 | Boolean | true |
| index | 发送目录索引文件。设置false将不发送。 | Mixed | "index.html" |
| lastModified | 设置文件在系统中的最后修改时间到Last-Modified头部。可能的取值有false和true。 | Boolean | true |
| maxAge | 在Cache-Control头部中设置max-age属性,精度为毫秒(ms)或则一段ms format的字符串 | Number | 0 |
| redirect | 当请求的pathname是一个目录的时候,重定向到尾随"/" | Boolean | true |
| setHeaders | 当响应静态文件请求时设置headers的方法 | Funtion |
如果你想获得更多关于使用中间件的细节,你可以查阅Serving static files in Express。
Application()
app对象一般用来表示Express程序。通过调用Express模块导出的顶层的express()方法来创建它:
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('hello world!');
});
app.listen(3000);app对象具有以下的方法:
- 路由HTTP请求;具体可以看app.METHOD和app.param这两个例子。
- 配置中间件;具体请看app.route。
- 渲染HTML视图;具体请看app.render。
- 注册模板引擎;具体请看app.engine。
它还有一些属性设置,这些属性可以改变程序的行为。获得更多的信息,可以查阅Application settings。
Properties
- n.属性;财产;资产;地产
app.locals
app.locals对象是一个javascript对象,它的属性就是程序本地的变量。
app.locals.title
// => 'My App'
app.locals.email
// => 'me@myapp.com'一旦设定,app.locals的各属性值将贯穿程序的整个生命周期,与其相反的是res.locals,它只在这次请求的生命周期中有效。
在程序中,你可以在渲染模板时使用这些本地变量。它们是非常有用的,可以为模板提供一些有用的方法,以及app级别的数据。通过req.app.locals(具体查看req.app),Locals可以在中间件中使用。
app.locals.title = 'My App';
app.locals.strftime = require('strftime');
app.locals.email = 'me@myapp.com';app.mountpath
app.mountpath属性是子程序挂载的路径模式。 app.mountpath记录子程序被挂载的路径,方便内部获取挂载位置
一个子程序是一个express的实例,其可以被用来作为路由句柄来处理请求。var express = require('express');
var app = express(); // the main app
var admin = express(); // the sub app
admin.get('/', function(req, res) {
console.log(admin.mountpath); // /admin
res.send('Admin Homepage');
});
app.use('/admin', admin); // mount the sub app 挂在钩子它和req对象的baseUrl属性比较相似,除了req.baseUrl是匹配的URL路径,而不是匹配的模式。如果一个子程序被挂载在多条路径模式,app.mountpath就是一个关于挂载路径模式项的列表,如下面例子所示。
var admin = express();
admin.get('/', function(req, res) {
console.log(admin.mountpath); // ['adm*n', '/manager']
res.send('Admin Homepage');
});
var secret = express();
secret.get('/', function(req, res) {
console.log(secret.mountpath); // /secr*t
res.send('Admin secret');
});
admin.use('secr*t', secret); // load the 'secret' router on '/secr*t', on the 'admin' sub app
//在 '/secr*t' 上加载 'secret' 路由,在 'admin' 子应用上
app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager' , on the parent app 在父应用程序上,将“admin”路由加载到“/adm*n”和“/manager”上
Events
app.on('mount', callback(parent))
当子程序被挂载到父程序时,mount事件被发射。父程序对象作为参数,传递给回调方法。
var admin = express();
admin.on('mount', function(parent) {
console.log('Admin Mounted');
console.log(parent); // refers to the parent app 指的是母应用程序
});
admin.get('/', function(req, res) {
res.send('Admin Homepage');
});
app.use('/admin', admin);
Methods
app.all(path, callback[, callback ...] 全局统一逻辑(如认证、权限校验)
场景 1:全局统一逻辑(如认证、权限校验) 如果你的应用需要 “所有请求必须先经过认证”,用app.all('*', ...)可以一次性搞定,无需给每个路由单独加逻辑。
场景 2:特定路径前缀的统一处理(如 API 接口白名单)
如果只需要对某一类路径(比如/api开头的接口)应用统一逻辑,只需把路径参数从*改为/api/*即可。
app.all方法和标准的app.METHOD()`方法相似,除了它匹配所有的HTTP动词。
app.all是 Express 中一个特殊的路由方法,它不像app.get(只处理 GET 请求)、app.post`(只处理 POST 请求)那样限制 HTTP 方法,而是对指定路径的 “所有 HTTP 方法” 都生效。
对于给一个特殊前缀映射一个全局的逻辑处理,或者无条件匹配,它是很有效的。例如,如果你把下面内容放在所有其他的路由定义的前面,它要求所有从这个点开始的路由需要认证和自动加载一个用户。记住这些回调并不是一定是终点:loadUser可以在完成了一个任务后,调用next()方法来继续匹配随后的路由。
app.all('*', requireAuthentication, loadUser);或者这种相等的形式:
app.all('*', requireAuthentication);
app.all('*', loadUser);另一个例子是全局的白名单方法。这个例子和前面的很像,然而它只是限制以/api开头的路径。
app.all('/api/*', requireAuthentication);app.delete(path, callback[, callback ...])
它会匹配客户端发送的 DELETE 类型请求,且请求路径与 path 参数一致时,执行后续的 callback 回调函数(通常是处理删除逻辑的中间件或业务函数)。
HTTP 协议中,DELETE 方法的语义是 “请求服务器删除指定的资源”(比如删除一篇文章、一个用户、一条订单记录等)。app.delete 正是为了对应这种语义,让开发者能清晰地定义 “删除操作” 的路由。
- RESTful API 设计:在 RESTful 风格中,
DELETE方法是 “删除资源” 的标准动作,app.delete是实现这一动作的核心路由方法。 - 任何需要 “删除数据” 的业务场景(如删除评论、删除文件、取消订单等)。
路由HTTP DELETE请求到有特殊回调方法的特殊的路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果不能满足当前路由的处理条件,那么你可以传递控制到随后的路由。
app.delete('/', function(req, res) {
res.send('DELETE request to homepage');
});app.disable(name)
app.enable(name) | 启用名为 name 的设置项(设为 true) | 无(支持链式调用) |
|---|---|---|
app.enabled(name) | 检查 name 设置项是否已启用 | 布尔值(true/false) |
app.disable(name) | 禁用名为 name 的设置项(设为 false) | 无(支持链式调用) |
app.disabled(name) | 检查 name 设置项是否已禁用 | 布尔值(true/false) |
app.disable(name):主动禁用名为name的设置项(设为false);- 隐藏技术栈信息:通过
app.disable('x-powered-by')移除X-Powered-By响应头,避免暴露服务器使用 Express,提高安全性; - 验证设置状态:用
app.disabled(name)检查某个设置是否生效(比如判断路由是否启用了大小写敏感); - 动态调整应用行为:根据环境(开发 / 生产)禁用或启用某些设置(比如生产环境禁用详细错误信息)
设置类型为布尔的设置名为name的值为false,此处的name是app settings table中各属性的一个。调用app.set('foo', false)和调用app.disable('foo')是等价的。
比如:
app.disable('trust proxy');
app.get('trust proxy');
// => falseapp.disabled(name)
返回true如果布尔类型的设置值name被禁用为false,此处的name是app settings table中各属性的一个。
app.disabled('trust proxy');
// => true
app.enable('trust proxy');
app.disabled('trust proxy');
// => falseapp.enable(name)
app.enable(name):主动启用指定的设置项(将其值设为true);app.enabled(name):查询指定的设置项是否已启用(返回true或false)
设置布尔类型的设置值name为true,此处的name是app settings table中各属性的一个。调用app.set('foo', true)和调用app.enable('foo')是等价的。
app.enable('trust proxy');
app.get('trust proxy');
// => trueapp.enabled(name)
返回true如果布尔类型的设置值name被启动为true,此处的name是app settings table中各属性的一个。
app.enabled('trust proxy');
// => false
app.enable('trust proxy');
app.enabled('trust proxy');
// => trueapp.engine(ext, callback) 注册自定义模板引擎
app.engine(ext, callback) 的核心作用是让 Express 支持特定扩展名的模板文件,通过注册自定义或第三方的渲染逻辑,实现动态 HTML 页面的生成。它是 Express 模板渲染系统的 “扩展接口”,让开发者可以灵活选择适合的模板引擎。
注册给定引擎的回调,用来渲染处理ext文件。
默认情况下,Express需要使用require()来加载基于文件扩展的引擎。例如,如果你尝试渲染一个foo.jade文件,Express在内部调用下面的内容,同时缓存require()结果供随后的调用,来加速性能。
app.engine('jade', require('jade').__express);使用下面的方法对于那些没有提供开箱即用的.__express方法的模板,或者你希望使用不同的模板引擎扩展。
比如,使用EJS模板引擎来渲染.html文件:
app.engine('html', require('ejs').renderFile);在这个例子中,EJS提供了一个.renderFile方法,这个方法满足了Express规定的签名规则:(path, options, callback),然而记住在内部它只是ejs.__express的一个别名,所以你可以在不做任何事的情况下直接使用.ejs扩展。
一些模板引擎没有遵循这种规范,consolidate.js库映射模板引擎以下面的使用方式,所以他们可以无缝的和Express工作。
var engines = require('consolidate');
app.engine('haml', engines.haml);
app.engine('html', engines.hogan);app.get(name)
获得设置名为name的app设置的值,此处的name是app settings table中各属性的一个。
如下:
app.get('title');
// => undefined
app.set('title', 'My Site');
app.get('title');
// => 'My Site'app.get(path, callback [, callback ...])
路由HTTP GET请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。
app.get('/', function(req, res) {
res.send('GET request to homepage');
});app.listen(port, [hostname], [backlog], [callback])
app.listen() 是 Express 应用的 “启动入口”,通过它可以:
- 让应用监听指定端口,接收客户端请求;
- 简化 HTTP 服务器的创建(替代原生
http.createServer()的繁琐代码); - 灵活支持 HTTP/HTTPS 协议,适应不同的部署需求。
开发中最常用的形式是 app.listen(端口, 启动回调),比如 app.listen(3000, () => { ... }),这是启动 Express 应用的标准写法。
绑定程序监听端口到指定的主机和端口号。这个方法和Node中的http.Server.listen()是一样的。
var express = require('express');
var app = express();
app.listen(3000);通过调用express()返回得到的app实际上是一个JavaScript的Function,被设计用来作为一个回调传递给Node HTTP servers来处理请求。这样,其就可以很简便的基于同一份代码提供http和https版本,所以app没有从这些继承(它只是一个简单的回调)。
var express = require('express');
var https = require('https');
var http = require('http');
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);app.listen()方法是下面所示的一个便利的方法(只针对HTTP协议):
app.listen = function() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};app.METHOD(path, callback [, callback ...])
路由一个HTTP请求,METHOD是这个请求的HTTP方法,比如GET,PUT,POST等等,注意是小写的。所以,实际的方法是app.get(),app.post(),app.put()等等。下面有关于方法的完整的表。
获取更多信息,请看routing guide。
Express支持下面的路由方法,对应与同名的HTTP方法:
|
|
|
如果使用上述方法时,导致了无效的javascript的变量名,可以使用中括号符号,比如,app['m-search']('/', function ...你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没有满足当前路由的处理条件,那么传递控制到随后的路由。
| 方法 | 作用范围 | 场景 |
|---|---|---|
next() | 当前路由 / 中间件链内 | 执行同路由的下一个回调(正常流程传递) |
next('router') | 跳出当前路由,交给下一个路由 | 前置条件不满足时,跳过当前路由剩余逻辑 |
本API文档把使用比较多的HTTP方法app.get(),app.post,app.put(),app.delete()作为一个个单独的项进行说明。然而,其他上述列出的方法以完全相同的方式工作。
app.all()是一个特殊的路由方法,它不属于HTTP协议中的规定的方法。它为一个路径加载中间件,其对所有的请求方法都有效。
app.all('/secret', function (req, res) {
console.log('Accessing the secret section...');
next(); // pass control to the next handler
});app.param([name], callback)
app.param` 的回调是一个函数,但更具体地说,它是一种特殊的中间件(参数中间件)。它具备中间件的核心特征(处理请求、传递控制权),同时专门针对路由参数做预处理,是中间件在 “参数处理场景” 下的细分形式。
app.param([name], callback) 是 Express 中用于统一处理路由参数的方法,核心作用是:当路由路径中出现指定参数(如 :id、:user)时,自动触发预设的回调函数,实现参数验证、数据加载等逻辑的复用。
一、核心作用:给路由参数绑定 “预处理逻辑”
路由参数(如 /user/:id 中的 :id)是动态变化的(比如 id 可能是 123、456)。app.param 允许你为这些参数绑定一个 “触发器”:每当路由中包含该参数时,先执行触发器逻辑,再进入路由的实际处理回调。
常见用途:
- 验证参数格式(如
id是否为数字); - 自动加载关联数据(如通过
id查询用户信息,并存到req对象中,供后续路由使用); - 统一处理参数错误(如参数无效时直接返回 404)。
二、基本用法:单参数绑定
语法:app.param(参数名, 回调函数)
回调函数的参数固定为:(req, res, next, 参数值, 参数名)
req:请求对象;res:响应对象;next:传递控制权的函数;参数值:当前路由中该参数的实际值(如/user/123中:id的值是123);参数名:参数的名称(如id)
三、参数为数组:批量绑定多个参数
如果需要处理多个参数(如 /user/:id/:page 中的 id 和 page),可以将参数名放在数组中,app.param 会按数组顺序依次触发回调。
// 给 :id 和 :page 两个参数绑定预处理逻辑
app.param(['id', 'page'], (req, res, next, value, paramName) => {
console.log(`处理参数 ${paramName},值为:${value}`);
// 简单验证:id 和 page 必须是数字
if (isNaN(Number(value))) {
return res.status(400).send(`参数 ${paramName} 必须是数字`);
}
next(); // 验证通过,继续下一个参数或路由
});
// 路由:包含 :id 和 :page 参数
app.get('/user/:id/:page', (req, res) => {
res.send('参数验证通过');
});处理参数 id,值为:123
处理参数 page,值为:5 访问 /user/abc/5 时:id 参数验证失败,返回 400 错误,page 的回调和路由回调都不会执行。
四、关键特性:执行时机与范围
只执行一次 per 请求:即使多个路由匹配同一个参数(如两个
/user/:id路由),app.param回调在一次请求中只会执行一次。app.param('id', (req, res, next, id) => { console.log('param 回调执行'); // 只打印一次 next(); }); // 两个匹配 /user/:id 的路由 app.get('/user/:id', (req, res, next) => { console.log('路由1'); next(); }); app.get('/user/:id', (req, res) => { console.log('路由2'); res.end(); });访问
/user/123时,输出:param 回调执行 路由1 路由2
局部性:不被继承:
app.param定义的回调只对当前app(或路由实例)有效,挂载的子应用(sub-app)不会继承。例如:主应用定义的
app.param('id', ...),不会影响挂载到主应用的子应用中的:id参数。
给路由参数添加回调触发器,这里的name是参数名或者参数数组,function是回调方法。回调方法的参数按序是请求对象,响应对象,下个中间件,参数值和参数名。
如果name是数组,会按照各个参数在数组中被声明的顺序将回调触发器注册下来。还有,对于除了最后一个参数的其他参数,在他们的回调中调用next()来调用下个声明参数的回调。对于最后一个参数,在回调中调用next()将调用位于当前处理路由中的下一个中间件,如果name只是一个string那就和它是一样的(就是说只有一个参数,那么就是最后一个参数,和数组中最后一个参数是一样的)。
例如,当:user出现在路由路径中,你可以映射用户加载的逻辑处理来自动提供req.user给这个路由,或者对输入的参数进行验证。
app.param('user', function(req, res, next, id) {
User.find(id, function(error, user) {
if (err) {
next(err);
}
else if (user){
req.user = user;
} else {
next(new Error('failed to load user'));
}
});
});对于Param的回调定义的路由来说,他们是局部的。它们不会被挂载的app或者路由继承。所以,定义在app上的Param回调只有是在app上的路由具有这个路由参数时才起作用。
在定义param的路由上,param回调都是第一个被调用的,它们在一个请求-响应循环中都会被调用一次并且只有一次,即使多个路由都匹配,如下面的例子:
app.param('id', function(req, res, next, id) {
console.log('CALLED ONLY ONCE');
next();
});
app.get('/user/:id', function(req, res, next) {
console.log('although this matches');
next();
});
app.get('/user/:id', function(req, res) {
console.log('and this mathces too');
res.end();
});当GET /user/42,得到下面的结果:
CALLED ONLY ONCE
although this matches
and this matches tooapp.param(['id', 'page'], function(req, res, next, value) {
console.log('CALLED ONLY ONCE with', value);
next();
});
app.get('/user/:id/:page', function(req. res, next) {
console.log('although this matches');
next();
});
app.get('/user/:id/:page', function (req, res, next) {
console.log('and this matches too');
res.end();
});
当执行GET /user/42/3,结果如下:
CALLED ONLY ONCE with 42
CALLED ONLY ONCE with 3
although this matches
and this mathes too下面章节描述的app.param(callback)在v4.11.0之后被弃用。通过只传递一个回调参数给app.param(name, callback)方法,app.param(naem, callback)方法的行为将被完全改变。这个回调参数是关于app.param(name, callback)该具有怎样的行为的一个自定义方法,这个方法必须接受两个参数并且返回一个中间件。
这个回调的第一个参数就是需要捕获的url的参数名,第二个参数可以是任一的JavaScript对象,其可能在实现返回一个中间件时被使用。
这个回调方法返回的中间件决定了当URL中包含这个参数时所采取的行为。
在下面的例子中,app.param(name, callback)参数签名被修改成了app.param(name, accessId)。替换接受一个参数名和回调,app.param()现在接受一个参数名和一个数字。
var express = require('express');
var app = express();
app.param(function(param, option){
return function(req, res, next, val) {
if (val == option) {
next();
}
else {
res.sendStatus(403);
}
}
});
app.param('id', 1337);
app.get('/user/:id', function(req, res) {
res.send('Ok');
});
app.listen(3000, function() {
console.log('Ready');
}); 在这个例子中,app.param(name, callback)参数签名保持和原来一样,但是替换成了一个中间件,定义了一个自定义的数据类型检测方法来检测user id的类型正确性。
app.param(function(param, validator) {
return function(req, res, next, val) {
if (validator(val)) {
next();
}
else {
res.sendStatus(403);
}
}
});
app.param('id', function(candidate) {
return !isNaN(parseFloat(candidate)) && isFinite(candidate);
});在使用正则表达式来,不要使用
.。例如,你不能使用/user-.+/来捕获user-gami,用使用[\\s\\S]或者[\\w\\>W]来代替(正如/user-[\\s\\S]+/)。//captures '1-a_6' but not '543-azser-sder' router.get('/[0-9]+-[[\\w]]*', function);//captures '1-a_6' and '543-az(ser"-sder' but not '5-a s'
router.get('/[0-9]+-[[\S]]*', function);//captures all (equivalent to '.*')
router.get('[[\s\S]]*', function);app.param 是处理路由参数的 “利器”,通过它可以: 集中验证参数格式,避免在每个路由中重复编写; 自动加载关联数据(如用户、商品),简化后续路由逻辑; 统一处理参数错误,减少冗余代码。
app.path()
通过这个方法可以得到app典型的路径,其是一个string。
var app = express()
, blog = express()
, blogAdmin = express();
app.use('/blog', blog);
app.use('/admin', blogAdmin);
console.log(app.path()); // ''
console.log(blog.path()); // '/blog'
console.log(blogAdmin.path()); // '/admin'如果app挂载很复杂下,那么这个方法的行为也会很复杂:一种更好用的方式是使用req.baseUrl来获得这个app的典型路径。
app.post(path, callback, [callback ...])
路由HTTP POST请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。
app.post('/', function(req, res) {
res.send('POST request to homepage')
});app.put(path, callback, [callback ...])
路由HTTP PUT请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。
app.put('/', function(req, res) {
res.send('PUT request to homepage');
});app.render(view, [locals], callback)
一、核心作用
app.render 的本质是 “模板渲染引擎的调用入口”,它会:
- 找到指定名称的模板文件(如
email对应views/email.ejs或views/email.hbs,取决于你配置的模板引擎); - 将
locals中的数据(如{name: 'Tobi'})注入模板,生成最终的 HTML 字符串; - 通过
callback回调函数返回结果(成功时返回 HTML,失败时返回错误信息)。
关键区别:它和 res.render() 功能相似,但 res.render() 会自动将渲染后的 HTML 作为响应发送给客户端,而 app.render() 只生成 HTML,不发送 —— 你可以用这个 HTML 做其他事情(比如发邮件、存缓存)。
二、参数详解
语法:app.render(view, [locals], callback)三个参数的作用如下:
| 参数 | 类型 | 说明 |
|---|---|---|
view | 字符串 | 必选,模板文件的名称(无需写路径和扩展名,Express 会按 view engine 配置自动查找)。 |
locals | 对象 | 可选,注入模板的本地数据(如 {name: '张三'},模板中可通过 {{name}} 调用)。 |
callback | 函数 | 必选,渲染完成后的回调,格式为 (err, html) => {}:- err:渲染失败时的错误对象;- html:渲染成功后的 HTML 字符串。 |
三、与 res.render() 的关键区别
很多人会混淆这两个方法,核心差异在于 “是否自动发送响应” 和 “使用场景”,具体对比如下:
| 对比维度 | app.render() | res.render() |
|---|---|---|
| 输出方式 | 不发送响应,通过 callback 返回 HTML | 自动将 HTML 作为响应发送给客户端 |
| 核心用途 | 生成 HTML 后需二次处理(如发邮件) | 直接响应页面给客户端(如渲染网页) |
| 调用依赖 | 可在任何地方调用(无需 req/res) | 只能在路由 / 中间件中调用(依赖 res 对象) |
// 1. res.render():直接渲染并发送页面给客户端
app.get('/index', (req, res) => {
res.render('index', { title: '首页' }); // 渲染 index 模板,直接响应 HTML
});
// 2. app.render():生成 HTML 后自行处理(比如打印或发邮件)
app.get('/send-email', (req, res) => {
// 生成 email 模板的 HTML
app.render('email', { name: 'Tobi' }, (err, html) => {
if (err) return res.status(500).send('渲染失败');
// 这里可以用 HTML 做其他事(比如调用邮件服务发送)
console.log('邮件内容 HTML:', html);
res.send('邮件 HTML 已生成,准备发送'); // 手动给客户端响应
});
});四、典型使用场景
app.render() 适合需要 “先获取 HTML,再做额外操作” 的场景,常见例子:
1. 生成邮件内容
很多邮件需要 HTML 格式(如注册欢迎邮件、营销邮件),此时用 app.render() 生成 HTML 后,传给邮件发送库(如 nodemailer),而不是响应给客户端。
const nodemailer = require('nodemailer'); // 邮件发送库
// 生成邮件 HTML 并发送
app.post('/send-welcome-email', (req, res) => {
const userEmail = req.body.email;
// 1. 用 app.render() 生成邮件模板的 HTML
app.render('welcome-email', { username: '张三' }, (err, emailHtml) => {
if (err) return res.status(500).send('邮件模板渲染失败');
// 2. 用 nodemailer 发送 HTML 邮件
const transporter = nodemailer.createTransport({ /* 邮件配置 */ });
transporter.sendMail({
to: userEmail,
subject: '欢迎注册',
html: emailHtml // 用渲染好的 HTML 作为邮件内容
}, (err) => {
if (err) return res.send('邮件发送失败');
res.send('欢迎邮件已发送');
});
});
});2. 预渲染 HTML 并缓存
对于访问频繁的页面(如首页),可以提前用 app.render() 生成 HTML,存入缓存(如 Redis),后续请求直接从缓存取 HTML 发送,减少重复渲染的性能消耗。
const redis = require('redis'); // Redis 缓存库
const client = redis.createClient();
// 预渲染首页 HTML 并存入缓存
app.get('/preload-index', (req, res) => {
app.render('index', { title: '首页' }, (err, html) => {
if (err) return res.status(500).send('预渲染失败');
// 存入 Redis,过期时间 1 小时
client.set('index-html', html, 'EX', 3600);
res.send('首页 HTML 已预渲染并缓存');
});
});
// 从缓存获取 HTML 响应
app.get('/', (req, res) => {
client.get('index-html', (err, cachedHtml) => {
if (cachedHtml) {
return res.send(cachedHtml); // 直接用缓存的 HTML
}
// 缓存不存在时,用 res.render() 实时渲染
res.render('index', { title: '首页' });
});
});五、注意点:视图缓存
- 开发环境(
process.env.NODE_ENV === 'development'):视图缓存默认关闭,每次app.render()都会重新读取模板文件,方便调试。 - 生产环境(
process.env.NODE_ENV === 'production'):视图缓存默认开启,模板文件会被缓存到内存,重复渲染时无需重新读取文件,提升性能。 - 若需在开发环境开启缓存,可手动设置:
app.set('view cache', true)。
总结
app.render() 是 Express 提供的 “HTML 生成工具”,核心价值在于脱离 “渲染即响应” 的绑定,让你能灵活处理渲染后的 HTML(发邮件、存缓存等)。它和 res.render() 的关系可以理解为:res.render() = app.render() + 自动发送响应。
通过callback回调返回一个view渲染之后得到的HTML文本。它可以接受一个可选的参数,可选参数包含了这个view需要用到的本地数据。这个方法类似于res.render(),除了它不能把渲染得到的HTML文本发送给客户端。
将
app.render()当作是可以生成渲染视图字符串的工具方法。在res.render()内部,就是使用的app.render()来渲染视图。如果使能了视图缓存,那么本地变量缓存就会保留。如果你想在开发的过程中缓存视图,设置它为
true。在生产环境中,视图缓存默认是打开的。
app.render('email', function(err, html) {
// ...
});
app.render('email', {name:'Tobi'}, function(err, html) {
// ...
});app.route(path)
返回一个单例模式的路由的实例,之后你可以在其上施加各种HTTP动作的中间件。使用app.route()来避免重复路由名字(例如错字错误)--说的意思应该是使用app.router()这个单例方法来避免同一个路径多个路由实例。
var app = express();
app.route('/events')
.all(function(req, res, next) {
// runs for all HTTP verbs first
//首先运行所有 HTTP 动词的处理程序// 可以把它看作特定路由的中间件!
// think of it as route specific middleware!
})
.get(function(req, res, next) {
res.json(...);
})
.post(function(req, res, next) {
// maybe add a new event...
})
app.set(name, value)
给name设置项赋value值,name是app settings table中属性的一项。
对于一个类型是布尔型的属性调用app.set('foo', ture)等价于调用app.enable('foo')。同样的,调用app.set('foo', false)等价于调用app.disable('foo')。
可以使用app.get()来取得设置的值:
app.set('title', 'My Site');
app.get('title'); // 'My Site'Application Settings
如果name是程序设置之一,它将影响到程序的行为。下边列出了程序中的设置。
这些是 Express.js 框架的应用配置项(Application Settings),核心作用是统一控制 Express 应用的核心行为,比如路由匹配规则、数据格式处理、缓存策略、环境适配等,让开发者能根据项目需求定制框架的运行方式,而不用修改框架源码。
1. 路由匹配规则类:控制 URL 如何被识别
这类配置决定了 Express 如何匹配客户端请求的 URL,是路由设计的基础。
case sensitive routing(区分大小写路由)
控制 URL 是否区分大小写。比如启用后,
/Foo和/foo会被视为两个不同的路由;默认不启用,两者处理逻辑一致。
strict routing(严格路由)
控制 URL 末尾的斜杠是否影响匹配。比如启用后,
/foo和/foo/是两个不同的路由;默认不启用,两者处理逻辑一致。
subdomain offset(子域名偏移量)
处理多子域名场景时,用于 “忽略” 域名中前面的几个部分,准确识别子域名。比如默认值为 2 时,
a.b.example.com会被识别为子域名a.b(忽略最后两个部分example.com)。
2. 数据处理类:控制请求 / 响应的数据格式
这类配置负责处理客户端与服务器之间的数据交互格式,比如 JSON、URL 参数等。
jsonp callback name(JSONP 回调名)
定义 JSONP 请求的默认回调函数名。默认是?callback=,比如请求/?callback=handleData时,响应会包裹在handleData(...)中。
json replacer /json spaces(JSON 处理)
json replacer:自定义 JSON 序列化规则的回调函数,比如过滤响应中不需要的字段。json spaces:设置后会返回 “格式化缩进” 的 JSON 响应(比如值为 2 时缩进 2 个空格),默认不启用(返回压缩的 JSON),开发时启用方便调试。
query parser(URL 参数解析器)
控制如何解析 URL 中的查询参数(比如?name=foo&age=18)。默认用extended模式(支持复杂参数,如数组),也可禁用(false)或用原生simple模式。
3. 缓存优化类:提升请求响应效率
这类配置通过缓存机制减少重复计算或数据传输,是性能优化的关键。
etag(ETag 响应头)
控制是否生成 HTTP 的 ETag 头,用于客户端缓存验证。默认值为
weak(弱 ETag,基于资源语义匹配),可根据需求改为强 ETag 或禁用,避免重复传输未修改的资源(客户端会返回 304 状态码)。
view cache(视图模板缓存)
控制是否缓存 “编译后的模板文件”(比如 EJS、Pug 模板)。生产环境默认开启(减少重复编译,提升速度),开发环境建议关闭(修改模板后无需重启服务即可生效)。
4. 环境与安全类:适配运行环境 & 降低安全风险
这类配置用于适配不同运行环境(开发 / 生产),并规避基础安全问题。
env(环境模式)定义 Express 运行的环境,默认优先读取系统环境变量NODE_ENV,没有则为development
(开发环境)。框架会根据环境自动调整行为,比如生产环境会关闭错误详情提示。
trust proxy(信任反向代理)当 Express 部署在反向代理(如 Nginx、Apache)后面时,需启用此配置,才能通过X-Forwarded-*
头获取客户端真实 IP;默认禁用,不启用会导致req.ip拿到的是代理服务器的 IP。
- x-powered-by(X-Powered-By 头)控制是否在响应头中添加X-Powered-By: Express,默认启用。生产环境建议禁用,避免暴露服务器使用的框架,降低被针对性攻击的风险。
5. 视图模板类:控制模板文件的查找与渲染
这类配置仅在使用 Express 模板引擎(如 EJS、Pug)时生效,负责管理模板文件的路径和默认引擎。
- views(模板文件目录)指定模板文件存放的目录(或目录数组,数组会按顺序查找模板)。默认路径是项目根目录下的views文件夹(如process.cwd() + '/views')。
- view engine(默认模板引擎)指定默认的模板引擎,比如设置为ejs后,渲染模板时无需写后缀(如res.render('index')会自动查index.ejs)。
| 属性 | 类型 | 值 | 默认 |
|---|---|---|---|
| case sensitive routing | Boolean | 启用区分大小写。 | 不启用。对/Foo和/foo处理是一样。 |
| env | String | 环境模型。 | process.env.NODE_ENV(NODE_ENV环境变量)或者"development" |
| etag | Varied | 设置ETag响应头。可取的值,可以查阅etag options table。更多关于HTTP ETag header。 | weak |
| jsonp callback name | String | 指定默认JSONP回调的名称。 | ?callback= |
| json replacer | String | JSON替代品回调 | null |
| json spaces | Number | 当设置了这个值后,发送缩进空格美化过的JSON字符串。 | Disabled |
| query parser | Varied | 设置值为false来禁用query parser,或者设置simple,extended,也可以自己实现query string解析函数。simple基于Node原生的query解析,querystring。 | "extend" |
| strict routing | Boolean | 启用严格的路由。 | 不启用。对/foo和/foo/的路由处理是一样。 |
| subdomain offset | Number | 用来删除访问子域的主机点分部分的个数 | 2 |
| trust proxy | Varied | 指示app在一个反向代理的后面,使用x-Forwarded-*来确定连接和客户端的IP地址。注意:X-Forwarded-*头部很容易被欺骗,所有检测客户端的IP地址是靠不住的。trust proxy默认不启用。当启用时,Express尝试通过前端代理或者一系列代理来获取已连接的客户端IP地址。req.ips属性包含了已连接客户端IP地址的一个数组。为了启动它,需要设置在下面trust proxy options table中定义的值。trust proxy的设置实现使用了proxy-addr包。如果想获得更多的信息,可以查阅它的文档 | Disable |
| views | String or Array | view所在的目录或者目录数组。如果是一个数组,将按在数组中的顺序来查找view。 | process.cwd() + '/views' |
| view cache | Boolean | 启用视图模板编译缓存。 | 在生成环境默认开启。 |
| view engine | String | 省略时,默认的引擎被扩展使用。 | |
| x-powered-by | Boolean | 启用X-Powered-By:ExpressHTTP头部 | true |
Options for trust proxy settings
查阅Express behind proxies来获取更多信息。
| Type | Value |
|---|---|
| Boolean |
如果为 |
| IP addresses |
一个IP地址,子网,或者一组IP地址,和委托子网。下面列出的是一个预先配置的子网名列表。
使用下面方法中的任何一种来设置IP地址:
当指定IP地址之后, 这个IP地址或子网会被设置了这个IP地址或子网的`app`排除在外, 最靠近程序服务的没有委托的地址将被看做客户端IP地址。 |
| Number |
信任从反向代理到app中间小于等于n跳的连接为客户端。 |
| Function |
客户自定义委托代理信任机制。如果你使用这个,请确保你自己知道你在干什么。
|
Options for etag settingsETag功能的实现使用了etag包。如果你需要获得更多的信息,你可以查阅它的文档。
| Type | Value |
|---|---|
| Boolean |
设置为 |
| String |
如果是strong,使能strong ETag。如果是weak,启用weak ETag。
|
| Function |
客户自定义`ETag`方法的实现. 如果你使用这个,请确保你自己知道你在干什么。
|
app.use([path,], function [, function...])
挂载中间件方法到路径上。如果路径未指定,那么默认为"/"。
一个路由将匹配任何路径如果这个路径以这个路由设置路径后紧跟着"/"。比如:
app.use('/appale', ...)将匹配"/apple","/apple/images","/apple/images/news"等。中间件中的
req.originalUrl是req.baseUrl和req.path的组合,如下面的例子所示。app.use('/admin', function(req, res, next) { // GET 'http://www.example.com/admin/new' console.log(req.originalUrl); // '/admin/new' console.log(req.baseUrl); // '/admin' console.log(req.path);// '/new' });
在一个路径上挂载一个中间件之后,每当请求的路径的前缀部分匹配了这个路由路径,那么这个中间件就会被执行。
由于默认的路径为/,中间件挂载没有指定路径,那么对于每个请求,这个中间件都会被执行。
// this middleware will be executed for every request to the app.
//这个中间件将会对应用的每个请求执行。
app.use(function(req, res, next) {
console.log('Time: %d', Date.now());
next();
});中间件方法是顺序处理的,所以中间件包含的顺序是很重要的。
// this middleware will not allow the request to go beyond it
app.use(function(req, res, next) {
res.send('Hello World');
});
// this middleware will never reach this route
app.use('/', function(req, res) {
res.send('Welcome');
});
路径可以是代表路径的一串字符,一个路径模式,一个匹配路径的正则表达式,或者他们的一组集合。
will match paths starting with /abcd 将匹配以 /abcd 开头的路径
| Type | Example |
|---|---|
| Path |
|
| Path Pattern |
|
| Regular Expression |
|
| Array |
|
方法可以是一个中间件方法,一系列中间件方法,一组中间件方法或者他们的集合。由于router和app实现了中间件接口,你可以像使用其他任一中间件方法那样使用它们。
| Usage | Example |
|---|---|
| 单个中间件 |
|
| 一系列中间件 |
|
| 一组中间件 |
|
| 组合 |
|
下面是一些例子,在Express程序中使用express.static中间件。
为程序托管位于程序目录下的public目录下的静态资源:
// GET /style.css etc
app.use(express.static(__dirname + '/public'));在/static路径下挂载中间件来提供静态资源托管服务,只当请求是以/static为前缀的时候。
// GET /static/style.css etc.
app.use('/static', express.static(express.__dirname + '/public'));通过在设置静态资源中间件之后加载日志中间件来关闭静态资源请求的日志。
app.use(express.static(__dirname + '/public'));
app.use(logger());托管静态资源从不同的路径,但./public路径比其他更容易被匹配:
app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/files'));
app.use(express.static(__dirname + '/uploads'));严格匹配
在 Express 中,app.use(path, middleware)的路径匹配规则是前缀匹配(即只要请求路径以设置的path开头就会匹配),所以/apple会匹配/apple、/apple/xxx等所有子路径。
如果需要严格匹配仅/apple路径(不匹配任何子路径),有两种常用方案:
方案 1:使用 HTTP 动词路由(推荐)
Express 的app.get()、app.post()等路由方法默认是精确路径匹配(除非路径中包含通配符),例如:
const express = require('express');
const app = express();
// 仅匹配 GET 请求的 /apple 路径,不匹配 /apple/images 等
app.get('/apple', (req, res) => {
res.send('仅匹配 /apple');
});
// 如果需要处理所有HTTP方法(如GET/POST/PUT等),可以用 app.all()
app.all('/apple', (req, res) => {
res.send('所有方法的 /apple 都匹配');
});app.METHOD()(如get、post、all)的路径匹配规则是 “完全等于”,因此只会响应精确的/apple路径。
方案 2:在app.use中手动判断路径
如果必须使用app.use(比如需要挂载中间件),可以在中间件内部通过req.path判断路径是否完全等于/apple,不匹配则跳过:
app.use('/apple', (req, res, next) => {
// 仅当请求路径严格等于 /apple 时才处理
if (req.path === '/apple') {
res.send('仅匹配 /apple');
} else {
// 路径不匹配时,交给后续中间件/路由处理
next();
}
});这里req.path会返回当前请求的路径(不含查询参数),通过严格等于/apple的判断,实现仅匹配目标路径。
核心区别总结
- app.use(path, ...):路径是前缀匹配(包含子路径),适合挂载通用中间件(如静态资源、日志等)。
- app.get(path, ...)等路由方法:路径是精确匹配(默认不含子路径),适合定义具体的接口路由。
根据你的需求,推荐使用方案 1(app.get或app.all),更符合 Express 的设计习惯。
Request
req对象代表了一个HTTP请求,其具有一些属性来保存请求中的一些数据,比如query string,parameters,body,HTTP headers等等。在本文档中,按照惯例,这个对象总是简称为req(http响应简称为res),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
如下例子:
app.get('/user/:id', function(req, res) {
res.send('user' + req.params.id);
});其实你也可以这样写:
app.get('/user/:id', function(request, response) {
response.send('user' + request.params.id);
});Properties
在Express 4中,req.files默认在req对象中不再是可用的。为了通过req.files对象来获得上传的文件,你可以使用一个multipart-handling(多种处理的工具集)中间件,比如busboy,multer,formidable,multipraty,connect-multiparty或者pez。
req.app
一、先搞懂核心:req.app到底是什么?
在 Express 中,req.app是请求对象(req)上的一个内置属性,它的核心作用是:
持有当前 Express 应用实例(也就是主文件中const app = express()创建的那个app)的引用。
简单说:主文件里你创建了app实例,当客户端发起请求时,Express 会自动把这个app实例 “附着” 到req.app上,让你在任何处理请求的地方(比如中间件、路由处理函数)都能访问到主app。
二、结合你的代码示例,看它怎么用?
你给的两个文件(index.js主文件、mymiddleware.js中间件模块),正好体现了req.app的典型场景:
- 主文件(index.js):挂载中间件
const express = require('express');
// 1. 创建Express应用实例(这就是最终会被req.app引用的对象)
const app = express();
// 2. 配置app的参数(比如设置"views"视图目录路径)
app.set('views', './views'); // 这里给app设置了一个"views"配置项
// 3. 挂载中间件:访问"/viewdirectory"时,执行mymiddleware.js导出的函数
app.get("/viewdirectory", require('./mymiddleware.js'));
app.listen(3000);- 中间件模块(mymiddleware.js):用req.app获取主 app
// 导出一个中间件函数(参数req是请求对象,res是响应对象)
module.exports = function(req, res) {
// 关键操作:通过req.app获取主文件的app实例,再用app.get('views')读取配置
const viewsDir = req.app.get('views');
res.ewsDir = req.app.get('views');
res.send('The views directory is ' + viewsDir); // 最终返回配置的视图目录
};- 实际运行效果:
当你访问http://localhost:3000/viewdirectory时,中间件会通过req.app.get('views')拿到主文件中设置的./views,页面会显示:
The views directory is ./views
三、为什么需要这种模式?核心是 “模块解耦”
你可能会问:“直接在mymiddleware.js里require('./index.js')获取app不行吗?”
答案是不推荐,因为会导致「循环依赖」和「模块耦合」:
- 循环依赖:index.js require 了mymiddleware.js,如果mymiddleware.js再 require index.js,会造成代码逻辑死循环;
- 模块耦合:中间件强依赖主文件,一旦主文件的app变量名改了(比如改成myApp),中间件就会报错,无法复用。
而req.app完美解决了这些问题:
- 中间件不用关心主文件的app怎么定义,只要通过req.app就能拿到实例;
- 中间件可以独立复用(比如放到其他 Express 项目里,只要主项目设置了views,它依然能正常工作)。
四、扩展:req.app还能做什么?
除了读取配置(app.get()),还能调用主app的其他方法,比如:
// 在中间件中用req.app获取其他配置
const port = req.app.get('port'); // 读取端口配置
// 在中间件中用req.app挂载新路由(少见,但可行)
req.app.get('/new-route', (req, res) => {
res.send('新路由');
});
// 在中间件中访问app上的自定义属性
req.app.dbConnection.query(/* ... */); // 比如访问主app上挂载的数据库连接总结关键知识点
- req.app的本质:请求对象上引用 Express 主应用实例的属性;
- 核心用途:在独立中间件 / 路由模块中,安全地访问主app的配置、方法、属性;
这个属性持有express程序实例的一个引用,其可以作为中间件使用。
如果你按照这个模式,你创建一个模块导出一个中间件,这个中间件只在你的主文件中require()它,那么这个中间件可以通过req.app来获取express的实例。
例如:
// index.js
app.get("/viewdirectory", require('./mymiddleware.js'));// mymiddleware.js
module.exports = function(req, res) {
res.send('The views directory is ' + req.app.get('views'));
};req.baseUrl
其核心作用是:
返回当前请求所匹配的「路由实例挂载路径」(即express.Router()创建的路由模块被挂载到主应用的具体路径)
一个路由实例挂载的Url路径。
var greet = express.Router();
greet.get('/jp', function(req, res) {
console.log(req.baseUrl); // greet
res.send('Konichiwa!');
});
app.use('/greet', greet);即使你使用的路径模式或者一系列路径模式来加载路由,baseUrl属性返回匹配的字符串,而不是路由模式。下面的例子,greet路由被加载在两个路径模式上。
app.use(['/gre+t', 'hel{2}o'], greet); // load the on router on '/gre+t' and '/hel{2}o'当一个请求路径是/greet/jp,baseUrl是/greet,当一个请求路径是/hello/jp,req.baseUrl是/hello。req.baseUrl和app对象的mountpath属性相似,除了app.mountpath返回的是路径匹配模式。
三、重点区分:req.baseUrl vs app.mountpath
你提到 “req.baseUrl和app.mountpath相似”,但两者有核心区别,必须分清:
对比项req.baseUrl app.mountpath作用对象属于「请求对象(req)」,随请求变化属于「路由实例(如 greet)」,挂载后固定返回内容实际匹配的「路径字符串」(如/greet)原始的「路径匹配模式」(如/gre+t)场景示例请求/greet/jp时,返回/greetgreet.mountpath返回['/gre+t', 'hel{2}o']
| 维度 | req.baseUrl | app.mountpath |
|---|---|---|
| 所属对象 | 请求对象(req) | 应用实例(app/ 子应用) |
| 含义 | 当前请求匹配的挂载路径(动态) | 应用被挂载的所有路径(静态) |
| 返回值类型 | 字符串 | 字符串或数组 |
| 作用 | 处理请求时,明确当前请求的基准路径 | 应用自身了解被挂载到哪些路径(调试等) |
了解概念后,需要知道它在项目中的价值,常见场景有:
- 动态生成路径前缀:比如路由模块需要生成基于挂载路径的链接(如req.baseUrl + '/cn' 可生成 /greet/cn);
- 路由分组判断:比如多个路由模块挂载到不同路径,通过req.baseUrl判断当前请求属于哪个路由分组(如/user组、/admin组);
- 日志记录:记录请求对应的路由挂载点,方便排查 “请求走了哪个路由模块”
req.body
在请求的body中保存的是提交的一对对键值数据。默认情况下,它是undefined,当你使用比如body-parser和multer这类解析body数据的中间件时,它是填充的。
下面的例子,给你展示了怎么使用body-parser中间件来填充req.body。
var app = require('express');
var bodyParser = require('body-parser');
var multer = require('multer');// v1.0.5
var upload = multer(); // for parsing multipart/form-data
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({extended:true})); // for parsing application/x-www-form-urlencoded
app.post('/profile', upload.array(), function(req, res, next) {
console.log(req.body);
res.json(req.body);
});req.cookies
一、先搞懂前提:req.cookies依赖cookie-parser中间件
req.cookies不是 Express 内置的 “原生属性”——必须先安装并配置cookie-parser中间件,才能通过req.cookies获取请求中的 Cookie。
如果不装这个中间件,req.cookies会是undefined(或不存在),哪怕请求携带了 Cookie 也拿不到。
1. 第一步:安装cookie-parser
通过 npm 安装中间件包(项目根目录执行):
npm install cookie-parser --save2. 第二步:在 Express 中配置中间件
在主文件(如index.js)中引入并挂载cookie-parser,注意挂载顺序要在路由之前(中间件需先于路由生效):
const express = require('express');
const cookieParser = require('cookie-parser'); // 引入中间件
const app = express();
// 配置cookie-parser:不带参数(基础模式,用于解析普通Cookie)
app.use(cookieParser());
// 之后再定义路由(此时路由中可通过req.cookies获取Cookie)
app.get('/get-cookie', (req, res) => {
// 这里就能正常使用req.cookies了
console.log(req.cookies);
res.send('已获取Cookie:' + JSON.stringify(req.cookies));
});
app.listen(3000, () => {
console.log('服务器运行在3000端口');
});二、核心用法:req.cookies是什么?怎么用?
1. req.cookies的本质
当客户端(浏览器、Postman 等)发起请求时,会把本地存储的 Cookie 通过请求头Cookie 传递给服务器(格式如:Cookie: name=tj; age=20)。
cookie-parser中间件会自动解析这个Cookie请求头,把键值对转换成JavaScript 对象,挂载到req.cookies上 —— 所以req.cookies本质是「解析后的 Cookie 键值对对象」。
- 若请求没带 Cookie:req.cookies是空对象{};
- 若请求带了 Cookie:req.cookies是键值对对象(如{ name: 'tj', age: '20' })。
2. 完整实战:客户端传 Cookie → 服务器用req.cookies获取
我们分「客户端如何传 Cookie」和「服务器如何获取」两步演示:
(1)客户端传递 Cookie 的两种方式
方式 1:浏览器自动传递(需先让服务器设置 Cookie 到浏览器)
比如先写一个 “设置 Cookie” 的路由,让浏览器存储 Cookie:
// 服务器设置Cookie到客户端(浏览器)
app.get('/set-cookie', (req, res) => {
// res.cookie(键名, 键值, 配置):向客户端设置Cookie
res.cookie('name', 'tj'); // 普通Cookie,默认会话级(关闭浏览器失效)
res.cookie('age', '20', { maxAge: 24 * 60 * 60 * 1000 }); // 有效期1天
res.send('已向浏览器设置Cookie');
});- 访问http://localhost:3000/set-cookie后,浏览器会存储name=tj和age=20两个 Cookie;
- 之后再访问http://localhost:3000/get-cookie时,浏览器会自动把这两个 Cookie 通过Cookie请求头传给服务器,服务器就能通过req.cookies拿到。
方式 2:手动设置(如 Postman 测试)
如果用 Postman 测试,可在请求的「Headers」中手动添加Cookie字段:
- 键:Cookie
- 值:name=tj; age=20(多个 Cookie 用分号 + 空格分隔)
发送请求后,服务器req.cookies会得到{ name: 'tj', age: '20' }。
(2)服务器获取 Cookie(核心代码)
// 第 1 步:安装并引入 cookie-parser(必须)
const express = require('express');
const cookieParser = require('cookie-parser'); // 引入中间件
const app = express();
// 第 2 步:在所有路由之前配置 cookie-parser(必须)
app.use(cookieParser()); // 这行是 req.cookies 能生效的关键!
// 第 3 步:你的 /get-cookie 路由(此时 req.cookies 已被解析)
app.get('/get-cookie', (req, res) => {
const allCookies = req.cookies;
console.log('所有Cookie:', allCookies); // 输出 { name: 'tj', age: '20' }(前提是客户端已携带这些Cookie)
const userName = req.cookies.name;
const userAge = req.cookies.age;
res.send(`
所有Cookie:${JSON.stringify(allCookies)}<br>
用户名:${userName}<br>
年龄:${userAge}
`);
});
// 启动服务器
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});访问http://localhost:3000/get-cookie,页面会显示解析后的 Cookie 内容。
三、扩展特性:签名 Cookie 与req.signedCookies
普通 Cookie 存在安全隐患 —— 客户端(浏览器)可以随意修改 Cookie 值(比如把age=20改成age=100)。
cookie-parser支持签名 Cookie:服务器用密钥对 Cookie 加密,客户端无法篡改(篡改后服务器能识别),此时需要用req.signedCookies获取。
- 配置签名密钥
挂载cookie-parser时传入「密钥字符串」(自定义,如my-secret-key),用于加密签名 Cookie:
// 传入密钥:此时中间件同时支持解析普通Cookie和签名Cookie
app.use(cookieParser('my-secret-key')); - 服务器设置签名 Cookie
设置 Cookie 时添加signed: true选项,表示这是签名 Cookie:
app.get('/set-signed-cookie', (req, res) => {
// 设置签名Cookie:必须加signed: true
res.cookie('userid', '123456', {
signed: true, // 标记为签名Cookie
maxAge: 24 * 60 * 60 * 1000
});
res.send('已设置签名Cookie(userid=123456)');
});- 服务器获取签名 Cookie(用req.signedCookies)
- 普通 Cookie 仍用req.cookies获取;
- 签名 Cookie 必须用req.signedCookies获取(若用req.cookies会拿到加密后的字符串,而非原始值):
app.get('/get-signed-cookie', (req, res) => {
// 获取签名Cookie(自动验证完整性,篡改后会返回false)
const userId = req.signedCookies.userid;
console.log('签名Cookie(userid):', userId); // 输出 '123456'(未篡改时)
// 若客户端篡改了Cookie(如把userid改成'789012'),userId会变成false
if (userId === false) {
res.send('警告:Cookie已被篡改!');
return;
}
res.send(`签名Cookie(用户ID):${userId}`);
});四、实际开发场景举例
场景 1:用户登录状态验证
用户登录成功后,服务器设置签名 Cookie(如token=xxx),后续请求通过req.signedCookies.token验证登录状态:
// 登录接口:设置签名Cookie
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 假设验证通过
if (username === 'admin' && password === '123456') {
// 设置登录令牌(签名Cookie,有效期2小时)
res.cookie('token', 'admin-token-123', {
signed: true,
maxAge: 2 * 60 * 60 * 1000
});
res.send('登录成功,已设置登录Cookie');
return;
}
res.send('账号密码错误');
});
// 需要登录的接口:验证签名Cookie
app.get('/user-info', (req, res) => {
const loginToken = req.signedCookies.token;
if (!loginToken) {
res.status(401).send('未登录,请先登录');
return;
}
// 验证token有效性(如查数据库)
if (loginToken === 'admin-token-123') {
res.send('用户信息:admin,角色:超级管理员');
return;
}
res.status(403).send('登录失效,请重新登录');
});场景 2:记住用户偏好设置
用户选择 “深色模式” 后,服务器设置普通 Cookie,下次请求通过req.cookies.theme加载偏好:
// 设置主题偏好
app.get('/set-theme/dark', (req, res) => {
res.cookie('theme', 'dark', { maxAge: 30 * 24 * 60 * 60 * 1000 }); // 有效期30天
res.send('已设置主题为深色模式');
});
// 加载首页时读取主题
app.get('/', (req, res) => {
const theme = req.cookies.theme || 'light'; // 默认浅色模式
res.send(`当前主题:${theme},<a href="/set-theme/dark">切换深色模式</a>`);
});五、常见问题与注意事项
- req.cookies是undefined
- 检查是否安装并挂载了cookie-parser;
- 检查app.use(cookieParser())是否在路由之前(中间件需先生效)。
- 签名 Cookie 拿到false?
- 客户端篡改了 Cookie,或服务器签名密钥(如my-secret-key)被修改;
- 设置 Cookie 时忘了加sign: true,或获取时用了req.cookies而非req.signedCookies。
- Cookie 中文乱码?
- 客户端传递中文 Cookie 时,需先 URL 编码(如encodeURIComponent('张三'));
- 服务器获取后用decodeURIComponent(req.cookies.name)解码。
总结关键知识点
- 依赖前提:必须安装并配置cookie-parser,否则req.cookies不存在;
- 本质:解析Cookie请求头后的键值对对象,空请求时为{};
- 核心用法:req.cookies.键名获取单个 Cookie,req.cookies获取所有;
- 安全扩展:签名 Cookie 需传密钥,用req.signedCookies获取,防篡改;
- 场景:登录验证、用户偏好、会话保持等。
当使用cookie-parser中间件的时候,这个属性是一个对象,其包含了请求发送过来的cookies。如果请求没有带cookies,那么其值为{}。
// Cookie: name=tj
req.cookies.name
// => "tj"获取更多信息,问题,或者关注,可以查阅cookie-parser。
req.fresh
指示这个请求是否是新鲜的。其和req.stale是相反的。
当cache-control请求头没有no-cache指示和下面中的任一一个条件为true,那么其就为true:
if-modified-since请求头被指定,和last-modified请求头等于或者早于modified响应头。if-none-match请求头是*。if-none-match请求头在被解析进它的指令之后,和etag响应头的值不相等
ps:If-None-Match作用: If-None-Match和ETag一起工作,工作原理是在HTTP Response中添加ETag信息。 当用户再次请求该资源时,将在HTTP Request 中加入If-None-Match信息(ETag的值)。如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。否则将返回200状态和新的资源和Etag. 使用这样的机制将提高网站的性能
req.fresh
// => true一、概述:req.fresh 是什么?
- 本质:Express 框架为请求对象(req)提供的 布尔型内置属性,用于判断「客户端本地缓存的资源是否与服务器最新资源一致」(即缓存是否 “有效”)。
- 核心作用:简化 HTTP 缓存逻辑处理,开发者无需手动编写缓存验证代码,直接通过 req.fresh 的 true/false 结果决定响应方式(返回 304 复用缓存 / 返回 200 新数据)。
- 与 req.stale 的关系:两者完全相反 ——req.fresh = true 时 req.stale = false(缓存有效),req.fresh = false 时 req.stale = true(缓存无效)。
二、核心特性:req.fresh 的 “自动检查” 机制
req.fresh 是 Express 封装好的自动检查属性,内部已实现 HTTP 缓存协议的验证逻辑,无需开发者手动比对请求头与响应头。其自动检查流程分为 3 步:
- 前提:服务器需提前设置 “缓存标识”
Express 自动检查的基础是 服务器为资源设置的缓存标识(二选一或两者都设),这是开发者唯一需要手动操作的步骤:
- Last-Modified:资源最后修改时间(GMT 格式),如 Wed, 25 Oct 2025 08:00:00 GMT。
- ETag:资源内容的唯一 “指纹”(内容不变则 ETag 不变),如 "abc123-def456"(通常由文件哈希、版本号生成)。
代码示例(设置缓存标识):
app.get('/api/data', (req, res) => {
// 手动设置 2 种缓存标识(二选一也可)
res.set({
'Last-Modified': 'Wed, 25 Oct 2025 08:00:00 GMT', // 资源最后修改时间
'ETag': '"data-v1.0.0"' // 资源内容的唯一标识
});
// 此时 Express 已开始自动检查,req.fresh 已生成
});- 自动步骤 1:读取客户端的 “缓存验证头”
客户端(如浏览器、Postman)会自动携带 缓存验证请求头(基于本地缓存的标识生成),Express 会自动读取这些头:
- 若客户端缓存有 Last-Modified,则携带 If-Modified-Since: <缓存的Last-Modified>;
- 若客户端缓存有 ETag,则携带 If-None-Match: <缓存的ETag>;
- (注:浏览器会自动处理这一步,开发者无需手动设置请求头)。
- 自动步骤 2:Express 内部比对验证
Express 按 HTTP 缓存协议规则,自动比对 “客户端验证头” 与 “服务器缓存标识”,最终生成 req.fresh 的值:
| 比对场景 | 条件(满足任一,且无 Cache-Control: no-cache) | req.fresh 结果 |
|---|---|---|
| 基于 Last-Modified | 客户端 If-Modified-Since ≥ 服务器 Last-Modified(资源未更新) | true |
| 基于 ETag | 客户端 If-None-Match = *(接受任意缓存) | true |
| 基于 ETag | 客户端 If-None-Match 与服务器 ETag 一致(内容未变) | true |
| 上述条件均不满足 | 资源已更新或无有效缓存验证信息 | false |
三、实际使用:3 步完成缓存处理
结合 req.fresh 的自动检查特性,完整缓存处理流程仅需 3 步:
步骤 1:设置缓存标识(服务器端)
为资源设置 Last-Modified 或 ETag(静态资源可借助 express.static 自动生成,动态资源需手动设置)。
步骤 2:根据 req.fresh 决定响应
- 若 req.fresh = true:返回 304 Not Modified,告知客户端复用本地缓存(不传输资源数据);
- 若 req.fresh = false:返回 200 OK + 最新资源数据(客户端更新本地缓存)。
步骤 3:测试验证
浏览器首次请求会返回 200 + 资源,再次请求会携带验证头,若缓存有效则返回 304。
完整代码示例(动态 API 缓存)
const express = require('express');
const app = express();
// 模拟动态资源(假设数据最后修改时间为 2025-10-25,版本号 v1.0.0)
const mockData = { content: '用户列表数据', version: 'v1.0.0' };
const lastModifiedTime = 'Wed, 25 Oct 2025 08:00:00 GMT';
const etagValue = `"user-list-${mockData.version}"`;
app.get('/api/users', (req, res) => {
// 步骤 1:手动设置缓存标识
res.set({
'Last-Modified': lastModifiedTime,
'ETag': etagValue,
'Cache-Control': 'public, max-age=3600' // 客户端缓存有效期 1 小时
});
// 步骤 2:根据 req.fresh 处理响应
if (req.fresh) {
// 缓存有效:返回 304,不传输数据
console.log('缓存有效,复用本地缓存');
return res.status(304).end();
}
// 缓存无效:返回 200 + 最新数据
console.log('缓存无效,返回新数据');
res.status(200).json(mockData);
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});静态资源缓存(自动生成标识)
Express 内置的 express.static 中间件会 自动为静态资源(图片、JS、CSS)生成 Last-Modified 和 ETag,无需手动设置,可直接使用 req.fresh:
// 静态资源目录:public
const staticMiddleware = express.static('public', {
maxAge: '1h' // 客户端缓存有效期 1 小时
});
app.use('/static', (req, res, next) => {
// 先执行静态资源中间件(自动生成缓存标识)
staticMiddleware(req, res, () => {
// 再根据 req.fresh 处理(可选,默认已处理)
if (req.fresh) {
res.status(304).end();
}
next();
});
});四、关键注意事项
- 必须设置缓存标识,否则 req.fresh 始终为 false
Express 无法凭空判断缓存有效性,若未设置 Last-Modified 或 ETag,req.fresh 会直接返回 false(视为缓存无效)。
- Cache-Control: no-cache 会强制 req.fresh = false
若请求头带 Cache-Control: no-cache(客户端强制验证缓存),即使比对通过,req.fresh 也会为 false,需重新返回资源(但仍可通过 304 优化)。
- 浏览器自动携带验证头,无需手动处理
客户端首次请求后,浏览器会自动存储 Last-Modified 和 ETag,后续请求会自动携带 If-Modified-Since 和 If-None-Match,开发者无需干预。
- ETag 比 Last-Modified 更可靠
- Last-Modified 精度仅到秒(若资源 1 秒内多次修改,会误判);
- ETag 基于内容哈希(内容不变则标识不变),适合动态资源或高频更新的资源。
- 多服务器部署需统一 ETag 生成规则
若后端是多台服务器,需确保同一资源在不同服务器上生成的 ETag 一致(如基于文件内容哈希,而非服务器本地路径),否则会导致 req.fresh 判断异常。
五、总结
| 核心点 | 说明 |
|---|---|
| 本质 | 自动判断缓存有效性的布尔属性(true= 有效,false= 无效) |
| 自动检查逻辑 | 比对请求头(If-Modified-Since/If-None-Match)与服务器标识(Last-Modified/ETag) |
| 开发者操作 | 仅需手动设置缓存标识,后续逻辑由 Express 自动完成 |
| 核心价值 | 简化缓存处理,减少无效数据传输,提升接口性能 |
| 适用场景 | 静态资源(图片 / JS/CSS)、动态 API 接口(数据更新频率低) |
通过 req.fresh,开发者可轻松实现符合 HTTP 协议的缓存逻辑,无需深入理解底层比对细节,大幅提升开发效率。
req.hostname
包含了源自HostHTTP头部的hostname。
当trust proxy设置项被设置为启用值,X-Forwarded-Host头部被使用来代替Host。这个头部可以被客户端或者代理设置。
// Host: "example.com"
req.hostname
// => "example.com"| 属性 | 含义 | 关键特点 | 示例结果 |
|---|---|---|---|
| req.hostname | 请求的主机名(域名 / IP) | 自动去掉端口,受 trust proxy 影响 | "example.com" |
| req.get('Host') | 原始 Host 请求头(未处理) | 可能包含端口,不受 trust proxy 影响 | "example.com:8080" |
| req.ip | 客户端的 IP 地址 | 受 trust proxy 影响(获取真实 IP) | "192.168.1.200" |
req.ips
当trust proxy设置项被设置为启用值,这个属性包含了一组在X-Forwarded-For请求头中指定的IP地址。不然,其就包含一个空的数组。这个头部可以被客户端或者代理设置。
一、核心背景:先懂trust proxy的作用
trust proxy是 Express 的全局配置项,核心作用是:
控制 Express 是否 “信任” 请求经过的代理服务器,并决定是否使用代理传递的头信息(如X-Forwarded-For、X-Forwarded-Host)来修正请求相关属性(如req.ips、req.ip、req.hostname)。
默认情况下,trust proxy值为false(禁用状态):
- Express 不相信任何代理服务器,会直接将 “最直接发起请求的设备 IP” 视为客户端 IP(比如如果有 Nginx 代理,会把 Nginx 的 IP 当客户端 IP,而非真实用户 IP);
- req.ips会是空数组(因为不解析X-Forwarded-For头)。
只有当trust proxy设置为启用值时,Express 才会信任指定的代理服务器,解析X-Forwarded-For头,进而req.ips才会返回请求经过的所有 IP 列表。
二、关键解析:trust proxy的 “启用值” 具体指什么?
“启用值” 不是单一值,而是符合 Express 规则、能让trust proxy生效的多类合法配置。根据使用场景(如代理数量、安全需求),可分为以下 5 种类型:
| 启用值类型 | 具体形式 | 适用场景 | 说明 |
|---|---|---|---|
| 1. 布尔值true | app.set('trust proxy', true) | 仅信任 “直接连接到 Express 的 1 层代理” | 简单场景(如只有 1 台 Nginx 代理),Express 会认为 “直接发起请求的设备就是可信代理”,解析其传递的X-Forwarded-For头。 |
| 2. 具体 IP 地址 | app.set('trust proxy', '192.168.1.100') | 信任指定的单个代理 IP | 明确知道代理服务器的固定 IP(如内网 Nginx 的 IP 为192.168.1.100),仅信任该 IP 传递的头信息。 |
| 3. IP 段(CIDR) | app.set('trust proxy', '192.168.1.0/24') | 信任某一网段内的所有代理 | 代理 IP 在固定网段(如内网代理集群都在192.168.1.x网段),用 CIDR 格式表示信任范围(/24表示前 24 位为网段,后 8 位任意)。 |
| 4. IP 列表(数组) | app.set('trust proxy', ['192.168.1.100', '10.0.0.0/8']) | 信任多个代理 IP / 网段 | 存在多层代理或多个独立代理(如 “Nginx+CDN”),将所有可信代理的 IP / 网段放入数组。 |
| 5. 自定义函数 | app.set('trust proxy', (ip) => ip.startsWith('192.168.')) | 复杂的信任规则判断 | 通过函数动态判断:传入 “发起请求的设备 IP”,返回true表示信任,false表示不信任(如信任所有192.168.开头的内网 IP)。 |
三、为什么 “启用值” 会影响req.ips?(核心关联逻辑)
req.ips的取值完全依赖trust proxy的配置,两者的关联逻辑如下:
- 前提:代理服务器会添加X-Forwarded-For头
当请求经过代理服务器(如 Nginx、CDN)时,代理会自动在请求头中添加X-Forwarded-For,格式为:
X-Forwarded-For: 真实客户端IP, 中间代理IP1, 最外层代理IP
(顺序:从客户端到最靠近 Express 的代理,用逗号分隔)
例:用户(IP:123.45.67.89)→ CDN(203.0.113.1)→ Nginx(192.168.1.100)→ Express,此时X-Forwarded-For为:123.45.67.89, 203.0.113.1, 192.168.1.100。
- trust proxy启用后:Express 解析X-Forwarded-For生成req.ips
当trust proxy设置为启用值时,Express 会:
① 验证 “发起请求的设备 IP” 是否在可信列表中(即是否为配置的代理 IP);
② 若可信,解析X-Forwarded-For头,将其中的 IP 按顺序拆分为数组,赋值给req.ips;
③ 最终req.ips的顺序为:[真实客户端IP, 中间代理IP1, 中间代理IP2, ...](最外层代理 IP 会被排除,因为它是 “直接发起请求的 IP”,已被验证为可信代理)。
示例:结合启用值的req.ips取值
假设配置:app.set('trust proxy', '192.168.1.100')(信任 Nginx 的 IP),请求链路为:
用户(123.45.67.89) → CDN(203.0.113.1) → Nginx(192.168.1.100) → Express
- 代理添加的X-Forwarded-For:123.45.67.89, 203.0.113.1, 192.168.1.100
- Express 验证:直接请求 IP 是192.168.1.100(在可信列表中),解析X-Forwarded-For;
- 最终req.ips:"[123.45.67.89", "203.0.113.1"](排除了可信代理 IP192.168.1.100)。
- trust proxy禁用时:req.ips为空数组
若trust proxy为默认值false,即使请求头有X-Forwarded-For,Express 也会:
- 不信任任何代理,忽略X-Forwarded-For头;
- req.ips始终为空数组([]);
- req.ip会被设为 “直接发起请求的 IP”(如 Nginx 的192.168.1.100),而非真实用户 IP。
四、安全注意事项:启用trust proxy必须规避的风险
X-Forwarded-For头可被客户端或恶意攻击者伪造(比如手动构造X-Forwarded-For: 1.2.3.4),若错误配置trust proxy,会导致req.ips/req.ip获取到伪造的 IP,引发安全问题(如 IP 黑名单失效、日志统计错误)。
因此,设置 “启用值” 时必须遵循:
- 禁止在公网环境随意设trust proxy: true:
公网环境中,“直接发起请求的 IP” 可能是恶意设备,设true会信任所有未知代理,导致伪造 IP 被采信。
- 仅信任明确的代理 IP / 网段:
必须将真实代理(如自己的 Nginx、合作的 CDN)的 IP / 网段写入配置(如'[192.168.1.0/24', '203.0.113.0/24']),不包含任何未知 IP。
- 多层代理需完整配置所有可信节点:
若有 “CDN→Nginx→Express” 三层链路,需将 CDN 和 Nginx 的 IP / 网段都加入可信列表,避免中间代理被误判为不可信。
五、实战示例:不同场景下的trust proxy配置与req.ips效果
场景 1:单 Nginx 代理(内网部署)
- 环境:Express 运行在192.168.1.200,Nginx(代理)IP 为192.168.1.100,用户通过 Nginx 访问。
- 配置:app.set('trust proxy', '192.168.1.100')
- 请求X-Forwarded-For:123.45.67.89, 192.168.1.100
- req.ips结果:"[123.45.67.89"](正确获取真实用户 IP)。
场景 2:CDN+Nginx 双层代理
- 环境:用户→CDN(IP:203.0.113.5)→Nginx(192.168.1.100)→Express。
- 配置:app.set('trust proxy', '[203.0.113.5', '192.168.1.100'])
- 请求X-Forwarded-For:123.45.67.89, 203.0.113.5, 192.168.1.100
- req.ips结果:"[123.45.67.89", "203.0.113.5"](正确保留客户端和 CDN 的 IP)。
场景 3:内网代理网段
- 环境:代理集群 IP 都在10.0.0.x网段(如10.0.0.5、10.0.0.6)。
- 配置:app.set('trust proxy', '10.0.0.0/24')(信任整个10.0.0.x网段)
- 请求X-Forwarded-For:123.45.67.89, 10.0.0.5
- req.ips结果:"[123.45.67.89"]。
六、总结:核心要点速记
| 核心问题 | 答案 |
|---|---|
| “启用值” 是什么? | 让trust proxy生效的配置,包括true、具体 IP、IP 段、IP 数组、自定义函数。 |
| 启用后有什么用? | 信任指定代理,解析X-Forwarded-For头,让req.ips返回请求经过的所有 IP。 |
| 为什么影响req.ips? | 禁用时忽略X-Forwarded-For(req.ips空),启用时解析该头生成 IP 列表。 |
| 安全关键? | 仅信任明确的代理 IP / 网段,禁止公网设trust proxy: true,防止伪造 IP。 |
例如,如果X-Forwarded-For是client,proxy1,proxy2,req.ips就是["clinet", "proxy1", "proxy2"],这里proxy2就是最远的下游。
req.originalUrl
req.url不是一个原生的Express属性,它继承自Node's http module。
这个属性很像req.url;然而,其保留了原版的请求链接,允许你自由地重定向req.url到内部路由。比如,app.use()的mounting特点可以重定向req.url跳转到挂载点。
// GET /search?q=something
req.originalUrl
// => "/search?q=something"一、先明确核心区别:req.originalUrl vs req.url
在讲用途前,必须先分清两者的差异 —— 这是实战中选择使用的关键:
| 属性 | 特点 | 示例(场景:app.use('/api', apiRouter),请求 /api/user?id=1) |
|---|---|---|
| req.originalUrl | 保留完整原始请求 URL,永不修改 | /api/user?id=1(完整保留用户请求的路径 + 参数) |
| req.url | 会被 Express 内部修改(如路由挂载、中间件重写),仅保留「当前路由上下文的路径」 | /user?id=1(被 /api 挂载点截取,仅保留子路由路径) |
正是因为 req.originalUrl 的「不可修改性」,它在需要准确获取用户原始请求信息的场景中至关重要。
二、实战核心用途
- 日志记录:保存准确的用户请求轨迹
场景:所有生产环境的服务都需要记录访问日志(如用户访问了哪个 URL、带了什么参数),用于问题排查、用户行为分析。此时必须用 req.originalUrl,否则日志会记录被修改后的 req.url,导致无法追溯真实请求。
代码示例(日志中间件):
// 全局日志中间件:记录每个请求的原始URL、客户端IP、时间等
app.use((req, res, next) => {
const logInfo = {
timestamp: new Date().toISOString(),
clientIp: req.ip,
// 关键:用 originalUrl 记录完整原始请求(含路径+查询参数)
originalRequest: req.originalUrl,
method: req.method,
statusCode: res.statusCode
};
// 写入日志文件或日志系统(如ELK)
console.log(JSON.stringify(logInfo));
next();
});
// 测试:当请求 /api/user?id=123 时,日志会记录 originalRequest: "/api/user?id=123"
// 若用 req.url,会记录 "/user?id=123",丢失 "/api" 挂载点,无法知道真实请求路径- 登录拦截:记住用户「原始请求路径」,登录后原路跳转
场景:用户未登录时访问需要权限的页面(如 /admin/dashboard?date=today),系统会拦截并跳转至登录页;登录成功后,需要自动跳回用户原本想访问的「原始路径」,而不是固定首页。此时必须用 req.originalUrl 保存原始路径,否则会丢失挂载点或参数。 (生产环境建议用 Redis/MongoDB 等持久化存储)。
代码示例(登录拦截中间件):
// 登录拦截中间件(需权限的路由前使用)
const requireAuth = (req, res, next) => {
if (!req.session.isLogin) {
// 关键:将原始请求URL存入session,用于登录后跳转
req.session.redirectUrl = req.originalUrl;
// 跳转到登录页
return res.redirect('/login');
}
next();
};
// 需权限的路由:访问 /admin/dashboard 会触发拦截
app.get('/admin/dashboard', requireAuth, (req, res) => {
res.send('管理员控制台');
});
// 登录接口:登录成功后跳回原始路径
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === '123456') {
req.session.isLogin = true;
// 从session中获取原始路径,若无则跳首页 (生产环境建议用 Redis/MongoDB 等持久化存储)。
const redirectUrl = req.session.redirectUrl || '/';
// 跳回原始请求(如 "/admin/dashboard?date=today")
return res.redirect(redirectUrl);
}
res.send('账号密码错误');
});- 子路由 / 挂载路由:获取完整原始路径(避免相对路径问题)
场景:当使用 app.use() 挂载子路由(如将 /api 路径交给 apiRouter 处理),子路由内部若需要生成「完整 URL 链接」(如返回带完整路径的图片 URL、跳转链接),必须用 req.originalUrl 的前缀,否则会生成相对路径错误。
代码示例(子路由场景):
// 1. 创建子路由 apiRouter
const apiRouter = express.Router();
// 子路由接口:返回用户信息,包含用户头像的完整URL
apiRouter.get('/user', (req, res) => {
// 需求:生成头像的完整URL(如 "http://domain.com/api/avatar/123.jpg")
// 关键:用 req.originalUrl 提取原始路径的前缀(/api),避免硬编码
const originalPrefix = req.originalUrl.split('/')[1]; // 从 "/api/user" 中提取 "api"
const avatarUrl = `/${originalPrefix}/avatar/${req.query.userId}.jpg`;
res.json({
id: req.query.userId,
name: '张三',
avatar: avatarUrl // 最终生成 "/api/avatar/123.jpg"(正确)
});
});
// 2. 挂载子路由:所有 /api 开头的请求交给 apiRouter 处理
app.use('/api', apiRouter);
// 测试:请求 /api/user?userId=123 时
// req.url 是 "/user?userId=123"(无法获取 "/api" 前缀)
// req.originalUrl 是 "/api/user?userId=123"(可提取 "/api" 前缀,生成正确的完整URL)- URL 参数 / 查询参数的完整保留:避免参数丢失
场景:有些中间件需要处理「完整的原始请求参数」(如 URL 中的查询参数 ? 后内容、路径参数),而 req.url 可能在路由匹配后丢失部分参数(或被重写),此时必须用 req.originalUrl 解析完整参数。
代码示例(参数验证中间件):
// 参数验证中间件:检查请求是否带了 "token" 查询参数
const checkToken = (req, res, next) => {
// 方式1:用 req.originalUrl 解析完整查询参数
const originalUrl = new URL(req.originalUrl, `http://${req.hostname}`);
const token = originalUrl.searchParams.get('token');
// 方式2:若用 req.url,当 req.url 被修改时会丢失参数(如 /api/user 变成 /user)
// const url = new URL(req.url, `http://${req.hostname}`); // 可能出错
if (!token) {
return res.status(401).send('缺少 token 参数');
}
next();
};
// 测试:请求 /api/data?token=abc123 时
// 用 originalUrl 能正确解析出 token=abc123
// 若 req.url 被修改为 /data?token=abc123(挂载点 /api 被截取),虽能解析,但统一用 originalUrl 更可靠
app.get('/api/data', checkToken, (req, res) => {
res.send('数据请求成功');
});- 反向代理 / 网关场景:传递原始请求 URL 给后端服务
场景:当 Express 作为「API 网关」或「反向代理」(如前端请求先到 Express 网关,再由网关转发到后端微服务),网关需要将「用户原始请求 URL」传递给后端服务,以便后端知道真实的用户请求路径(而非网关转发后的路径)。
代码示例(网关转发):
const axios = require('axios');
// Express 作为网关,转发请求到后端用户服务
app.use('/api/user', async (req, res) => {
try {
// 后端服务需要知道用户的原始请求URL(含完整路径和参数)
const response = await axios({
method: req.method,
url: 'http://backend-user-service:3001' + req.originalUrl, // 传递原始URL
data: req.body
});
res.send(response.data);
} catch (err) {
res.status(500).send('服务转发失败');
}
});
// 测试:用户请求 /api/user?page=1,网关转发到后端的 URL 是 "http://backend-user-service:3001/api/user?page=1"
// 若用 req.url,转发的是 "http://backend-user-service:3001/user?page=1",后端会找不到路由三、总结:哪些场景必须用 req.originalUrl?
核心判断标准:当业务需要「准确的用户原始请求信息」,且该信息可能被 Express 内部操作(路由挂载、中间件)修改时,必须用 req.originalUrl。具体对应:
- 日志记录、审计跟踪(需真实请求轨迹);
- 登录 / 权限拦截后的原路跳转(需记住原始路径);
- 子路由 / 挂载路由中生成完整 URL(需原始路径前缀);
- 解析完整查询参数、路径参数(需原始 URL 完整信息);
- 反向代理、API 网关(需传递原始请求给后端)。
反之,若仅需「当前路由上下文的路径」(如子路由内部匹配子路径),则用 req.url 即可(如 apiRouter 内部判断 req.url === '/user')。
req.params
一个对象,其包含了一系列的属性,这些属性和在路由中命名的参数名是一一对应的。例如,如果你有/user/:name路由,name属性可作为req.params.name。这个对象默认值为{}。
// GET /user/tj
req.params.name
// => "tj"当你使用正则表达式来定义路由规则,捕获组的组合一般使用req.params[n],这里的n是第几个捕获租。这个规则被施加在无名通配符匹配,比如/file/*的路由:
// GET /file/javascripts/jquery.js
req.params[0]
// => "javascripts/jquery.js"一、先明确核心:req.params 是什么?
req.params 是 Express 请求对象上的对象属性,默认值为 {},其键值对来源有两种:
- 命名路由参数:路由中用 :参数名 定义的动态部分(如 /user/:id 中的 id);
- 正则路由捕获组:正则路由中用 () 包裹的捕获内容(如 /file/(\w+).html 中的捕获组)。
它的核心优势是:将动态路径 “结构化”,无需手动解析 URL 字符串即可获取关键参数(比字符串分割高效、易维护)。
二、实战核心场景(附代码示例)
1. 场景 1:资源详情查询(最高频)
业务需求:访问「用户详情页」「文章详情页」「订单详情页」时,需通过路径传递资源 ID(如 /user/123 表示查询 ID=123 的用户),此时用 req.params 直接获取 ID。
代码示例(用户详情接口):
// 1. 定义动态路由::userId 是命名参数,匹配路径中的任意字符串
app.get('/user/:userId', (req, res) => {
// 2. 通过 req.params 获取路由中的 userId(默认是字符串,需手动转数字)
const userId = Number(req.params.userId);
// 3. 业务逻辑:根据 userId 查询数据库(模拟)
const user = {
id: userId,
name: '张三',
age: 28
};
// 4. 返回结果
res.json({
success: true,
data: user
});
});
// 测试:
// 请求 GET /user/456 → req.params = { userId: '456' } → userId 转成 456 → 返回 ID=456 的用户
// 请求 GET /user/789 → req.params = { userId: '789' } → 返回 ID=789 的用户为什么不用 req.query?
若用查询参数(如 /user?id=123),路径不够直观,且不符合 RESTful API 规范(REST 推荐用路径表示资源,查询参数表示筛选 / 分页)。
2. 场景 2:资源关联查询(多层动态路由)
业务需求:查询「某个用户的订单列表」「某篇文章的评论列表」,需同时传递两个动态参数(如 /user/123/orders 表示查询用户 123 的订单)。
代码示例(用户订单接口):
// 1. 多层动态路由::userId(用户ID) + :orderStatus(订单状态,可选)
app.get('/user/:userId/orders/:orderStatus?', (req, res) => {
// 2. 获取多个参数:userId(必传)、orderStatus(可选,未传则为 undefined)
const { userId, orderStatus } = req.params;
const status = orderStatus || 'all'; // 默认查所有状态
// 3. 业务逻辑:根据 userId 和 status 查询订单(模拟)
const orders = [
{ id: 101, userId: 123, status: 'paid', amount: 99 },
{ id: 102, userId: 123, status: 'shipped', amount: 199 }
].filter(order => status === 'all' || order.status === status);
res.json({
success: true,
data: {
userId,
orderStatus: status,
orders
}
});
});
// 测试:
// 请求 GET /user/123/orders → req.params = { userId: '123', orderStatus: undefined } → 返回所有状态订单
// 请求 GET /user/123/orders/paid → req.params = { userId: '123', orderStatus: 'paid' } → 返回已支付订单3. 场景 3:正则路由匹配(特定格式参数)
业务需求:限制路由参数的格式(如「日期必须是 YYYY-MM 格式」「ID 必须是数字」),用正则路由捕获符合格式的参数,不符合则不匹配该路由。
代码示例(按年月筛选文章):
// 1. 正则路由::year(4位数字) + :month(2位数字),用 () 定义捕获组
app.get('/articles/:year(\\d{4})/:month(\\d{2})', (req, res) => {
// 2. 获取正则匹配的参数(自动过滤不符合格式的请求)
const { year, month } = req.params;
// 3. 业务逻辑:查询该年月的文章(模拟)
const articles = [
{ id: 1, title: '2025年10月文章', publishTime: '2025-10-01' },
{ id: 2, title: '2025年10月另一篇', publishTime: '2025-10-15' }
];
res.json({
success: true,
data: {
date: `${year}-${month}`,
articles
}
});
});
// 测试:
// 请求 GET /articles/2025/10 → 匹配路由 → req.params = { year: '2025', month: '10' }
// 请求 GET /articles/2025/13 → 月份 13 不符合 \\d{2}(01-12)→ 不匹配该路由(返回 404)
// 请求 GET /articles/25/10 → 年份 25 不符合 \\d{4} → 不匹配该路由补充:若用非正则路由(如 /articles/:year/:month),需手动校验参数格式(如 if (year.length !== 4) { 返回参数错误 }),而正则路由可直接过滤无效请求,减少代码量。
4. 场景 4:API 版本控制(路径版)
业务需求:API 迭代时用路径区分版本(如 /api/v1/users 是 v1 版,/api/v2/users 是 v2 版),通过 req.params 获取版本号,转发到对应版本的处理逻辑。
代码示例(多版本 API):
// 1. 定义版本路由::version 匹配 v1/v2
app.use('/api/:version', (req, res, next) => {
const { version } = req.params;
// 根据版本号转发到对应路由模块
if (version === 'v1') {
require('./routes/v1')(req, res, next); // v1 版路由
} else if (version === 'v2') {
require('./routes/v2')(req, res, next); // v2 版路由
} else {
res.status(400).json({ success: false, msg: '不支持的 API 版本' });
}
});
// v1 版路由模块(routes/v1.js)
module.exports = (req, res, next) => {
// v1 版接口逻辑(如旧版用户列表)
if (req.path === '/users') {
res.json({ version: 'v1', data: [{ id: 1, name: 'v1-张三' }] });
}
};
// v2 版路由模块(routes/v2.js)
module.exports = (req, res, next) => {
// v2 版接口逻辑(如新版用户列表,新增 age 字段)
if (req.path === '/users') {
res.json({ version: 'v2', data: [{ id: 1, name: 'v2-张三', age: 28 }] });
}
};
// 测试:
// 请求 GET /api/v1/users → 返回 v1 版数据
// 请求 GET /api/v2/users → 返回 v2 版数据
// 请求 GET /api/v3/users → 返回“不支持的版本”5. 场景 5:无名通配符路由(捕获完整路径)
业务需求:实现 “文件下载”“静态资源代理” 等场景,需捕获路径中的完整子路径(如 /file/javascripts/jquery.js 中的 javascripts/jquery.js),用 * 通配符配合 req.params[0] 获取。
代码示例(文件下载接口):
// 1. 通配符路由:* 匹配任意子路径,捕获的内容存到 req.params[0]
app.get('/file/*', (req, res) => {
// 2. 获取完整子路径(req.params[0] 对应 * 匹配的内容)
const filePath = req.params[0];
// 3. 业务逻辑:拼接真实文件路径,返回文件(模拟)
const realPath = path.join(__dirname, 'public', filePath);
// 检查文件是否存在
if (fs.existsSync(realPath)) {
res.download(realPath); // 触发下载
} else {
res.status(404).send('文件不存在');
}
});
// 测试:
// 请求 GET /file/javascripts/jquery.js → req.params[0] = 'javascripts/jquery.js' → 下载对应文件
// 请求 GET /file/images/logo.png → req.params[0] = 'images/logo.png' → 下载 logo.png三、实战注意事项
- 参数类型默认是字符串:
如 /user/123 中 req.params.userId 是 '123'(字符串),若需数字类型,需手动转换(Number(req.params.userId)),否则可能导致数据库查询错误(如 id = '123' vs id = 123)。
- 路由参数的顺序优先级:
动态路由需放在固定路由之后,否则会覆盖固定路由。例如:
app.get('/user/profile', (req, res) => { res.send('用户资料页') }); // 固定路由
app.get('/user/:userId', (req, res) => { res.send('用户详情页') }); // 动态路由
// 若顺序颠倒,请求 /user/profile 会被 /user/:userId 匹配(req.params.userId = 'profile')- 与 req.query 的区别:
- req.params:路径中的动态参数(如 /user/:id → id=123),用于标识 “哪个资源”;
- req.query:URL 中 ? 后的查询参数(如 /user?page=1 → page=1),用于 “筛选 / 分页 / 排序”;
实战中需根据场景选择,例如:/user/:userId/orders?page=1&size=10(userId 是 params,page/size 是 query)。
- 特殊字符处理:
若参数包含特殊字符(如 /、?),需先 URL 编码(如 /user/张三 → /user/%E5%BC%A0%E4%B8%89),Express 会自动解码到 req.params 中。
四、总结:req.params 的核心价值
| 场景 | 核心作用 | 代码示例片段 |
|---|---|---|
| 资源详情查询 | 获取资源 ID(如用户 ID、文章 ID) | app.get('/user/:userId', (req) => { ... }) |
| 资源关联查询 | 同时获取多个关联参数(如用户 ID + 订单状态) | app.get('/user/:userId/orders/:status', ...) |
| 正则格式校验 | 过滤不符合格式的参数(如日期、数字 ID) | app.get('/articles/:year(\d{4})', ...) |
| API 版本控制 | 区分不同版本的 API 路径 | app.use('/api/:version', ...) |
| 通配符路径捕获 | 获取完整子路径(如文件路径) | app.get('/file/*', (req) => { req.params[0] }) |
req.params 是 Express 动态路由的 “灵魂”,它让路径从 “固定字符串” 变成 “可配置的动态模板”,大幅提升了 API 设计的灵活性和可维护性,是所有后端开发者必须熟练掌握的核心属性。
req.path
包含请求URL的部分路径。
// example.com/users?sort=desc
req.path
// => "/users"当在一个中间件中被调用,挂载点不包含在req.path中。你可以查阅app.use()获得跟多的信息。req.path 是 Express 中用于获取「请求 URL 中的路径部分」的属性(不含域名、端口、查询参数),其核心特点是 简洁性 和 专注于路径本身(过滤掉参数、域名等无关信息)。在实战中,它主要用于需要「基于路径做逻辑判断」但又不需要完整 URL 信息的场景,以下结合具体业务场景展开:
一、先明确 req.path 的核心特征
对比其他 URL 相关属性,req.path 的定位更 “轻量化”:
| 属性 | 示例(请求 https://example.com/api/user?id=123) | 核心区别 |
|---|---|---|
| req.path | /api/user | 仅保留路径部分(无查询参数、域名) |
| req.url | /api/user?id=123 | 包含路径 + 查询参数 |
| req.originalUrl | /api/user?id=123 | 保留完整原始路径(同 req.url 但不被挂载点修改) |
| req.hostname | example.com | 仅包含域名 |
正是这种 “只保留路径” 的特性,让 req.path 在需要 “路径匹配”“路径过滤” 的场景中更简洁高效。
二、实战核心用途(附代码示例)
- 日志记录:简洁记录访问路径(过滤冗余信息)
场景:日志中需要记录 “用户访问了哪个接口路径”,但不需要携带查询参数(参数可能包含敏感信息,或对统计无用)。req.path 直接提供纯净的路径,避免手动截取 URL。
代码示例(简易访问日志):
// 记录每个请求的路径、方法、时间(忽略参数)
app.use((req, res, next) => {
const log = {
time: new Date().toLocaleString(),
method: req.method,
path: req.path, // 仅记录路径,如 "/api/user"
ip: req.ip
};
console.log(`[访问日志] ${JSON.stringify(log)}`);
next();
});
// 测试:
// 请求 GET /api/user?id=123 → log.path = "/api/user"(不含 ?id=123)
// 请求 POST /admin/login → log.path = "/admin/login"优势:相比 req.url(含参数),req.path 记录的日志更简洁,且避免参数中特殊字符导致的日志格式混乱。
2. 权限控制:基于路径拦截敏感接口
场景:某些接口(如 /admin/* 管理员接口)需要验证权限,可通过 req.path 判断路径是否匹配敏感前缀,无需解析完整 URL。
代码示例(权限中间件):
// 权限中间件:拦截 /admin 开头的路径,验证是否为管理员
const checkAdmin = (req, res, next) => {
// 用 req.path 判断是否为管理员路径(忽略参数)
if (req.path.startsWith('/admin')) {
if (req.session.role === 'admin') {
// 管理员:允许访问
return next();
} else {
// 非管理员:拒绝访问
return res.status(403).send('无管理员权限');
}
}
// 非管理员路径:直接放行
next();
};
// 应用中间件
app.use(checkAdmin);
// 管理员接口
app.get('/admin/dashboard', (req, res) => {
res.send('管理员控制台');
});
// 普通用户接口
app.get('/user/profile', (req, res) => {
res.send('用户资料');
});
// 测试:
// 非管理员请求 /admin/dashboard → 403 错误
// 管理员请求 /admin/dashboard → 正常访问
// 任意用户请求 /user/profile → 正常访问(路径不匹配 /admin)3. 路由分发:在中间件中动态转发请求
场景:当需要在一个中间件中根据不同路径分发到不同处理逻辑(类似简易路由),req.path 可直接用于判断,无需处理参数。
代码示例(API 版本自动转发):
// 中间件:根据路径中的 /v1 /v2 自动转发到对应版本的处理函数
app.use('/api', (req, res, next) => {
const path = req.path; // 注意:挂载点 /api 不包含在 req.path 中
// 例如:请求 /api/v1/user → req.path = "/v1/user"
if (path.startsWith('/v1')) {
// 转发到 v1 版本处理函数
handleV1(req, res, next);
} else if (path.startsWith('/v2')) {
// 转发到 v2 版本处理函数
handleV2(req, res, next);
} else {
res.status(400).send('API 版本不存在(需 /v1 或 /v2)');
}
});
// v1 版本处理
function handleV1(req, res) {
res.send(`V1 接口:${req.path}`);
}
// v2 版本处理
function handleV2(req, res) {
res.send(`V2 接口:${req.path}`);
}
// 测试:
// 请求 /api/v1/user → req.path = "/v1/user" → 触发 handleV1 → 返回 "V1 接口:/v1/user"
// 请求 /api/v2/order → req.path = "/v2/order" → 触发 handleV2 → 返回 "V2 接口:/v2/order"关键细节:当中间件挂载在 /api 下时,req.path 不包含 /api(只从挂载点后开始计算),简化了路径判断逻辑(无需手动截取 /api 前缀)。
4. 静态资源服务:验证请求路径合法性
场景:提供静态资源(如图片、JS)时,需限制只能访问指定目录下的资源(防止路径遍历攻击,如 ../../etc/passwd),可通过 req.path 检查路径是否合规。
代码示例(安全的静态资源中间件):
const path = require('path');
const fs = require('fs');
// 自定义静态资源中间件:仅允许访问 public 目录下的资源
app.use('/static', (req, res) => {
// req.path 不包含 /static(挂载点),如请求 /static/images/logo.png → req.path = "/images/logo.png"
const relativePath = req.path;
// 拼接真实文件路径(resolve 可防止路径遍历攻击)
const realPath = path.resolve(__dirname, 'public', relativePath.slice(1)); // 去掉开头的 /
// 检查路径是否在 public 目录下(防止越权访问)
if (realPath.startsWith(path.resolve(__dirname, 'public'))) {
// 路径合法:返回文件
if (fs.existsSync(realPath)) {
return res.sendFile(realPath);
} else {
return res.status(404).send('文件不存在');
}
} else {
// 路径非法(如包含 ../ 尝试访问上级目录):拒绝
return res.status(403).send('非法访问');
}
});
// 测试:
// 请求 /static/images/logo.png → 合法 → 返回 logo.png
// 请求 /static/../../etc/passwd → realPath 会跳出 public 目录 → 被拦截(403)5. 路径匹配:忽略参数的接口统计
场景:统计 “每个接口的访问次数”(忽略参数差异),例如 /user 接口无论参数是 ?id=1 还是 ?id=2,都算同一个接口的访问量。req.path 可直接作为统计的 key。
代码示例(接口访问统计):
// 用对象记录每个路径的访问次数
const visitCount = {};
// 统计中间件
app.use((req, res, next) => {
const path = req.path;
// 初始化或累加次数
visitCount[path] = (visitCount[path] || 0) + 1;
next();
});
// 提供统计结果接口
app.get('/stats', (req, res) => {
res.json({ visitCount });
});
// 测试:
// 多次请求 /user?id=1 → visitCount["/user"] 累加
// 多次请求 /user?id=2 → 同样累加 visitCount["/user"](因为 path 相同)
// 访问 /stats → 看到 { "/user": 5, ... } 等统计结果三、与其他路径属性的选择原则
- 若需 完整原始 URL(含参数)(如日志溯源、跳转记录)→ 用 req.originalUrl;
- 若需 路径 + 参数(如解析查询参数)→ 用 req.url;
- 若需 仅路径部分(过滤参数、域名)(如权限判断、路径统计)→ 用 req.path;
- 若需 域名 → 用 req.hostname。
四、总结:req.path 的核心价值
req.path 的核心优势是 “聚焦路径本身”,过滤掉查询参数、域名等冗余信息,让 “基于路径的逻辑判断” 更简洁高效。其高频使用场景包括:
- 日志记录(简洁路径);
- 权限控制(路径前缀匹配);
- 路由分发(中间件内路径判断);
- 静态资源安全校验;
- 忽略参数的接口统计。
在这些场景中,使用 req.path 可避免手动截取 URL 字符串的麻烦,减少代码量并降低出错风险。
req.protocol
请求的协议,一般为http,当启用TLS加密,则为https。
当trust proxy设置一个启用的参数,如果存在X-Forwarded-Proto头部的话,其将被信赖和使用。这个头部可以被客户端或者代理设置。
req.ptotocol
// => "http"在 Express 实战开发中,req.protocol 的核心作用是获取当前请求的通信协议(http 或 https),尤其在需要依赖协议做逻辑判断、资源生成或安全控制的场景中不可或缺。以下是其典型实战用途,结合场景和代码示例说明:
一、构建绝对 URL(核心场景)
开发中经常需要生成绝对链接(而非相对路径),例如:
- 邮件验证 / 密码重置链接(需用户点击跳转回系统);
- 分享链接(如文章、商品分享到外部平台);
- OAuth 授权回调地址(第三方服务需明确回调的完整 URL)。
此时必须通过 req.protocol 确定协议前缀,再结合 req.get('host')(获取域名 / 端口)和路径,拼接出完整 URL。
代码示例:生成用户验证链接
// 假设路由:POST /user/send-verify-email
app.post('/user/send-verify-email', (req, res) => {
const userId = req.user.id;
const verifyToken = 'xxx-xxx-xxx'; // 生成的唯一验证令牌
// 1. 用 req.protocol 确定协议(http/https)
const protocol = req.protocol;
// 2. 用 req.get('host') 获取域名+端口(如 localhost:3000 或 example.com)
const host = req.get('host');
// 3. 拼接绝对验证链接
const verifyUrl = `${protocol}://${host}/user/verify?token=${verifyToken}&userId=${userId}`;
// 4. 发送邮件(示例用 console 模拟)
console.log('发送验证链接到用户邮箱:', verifyUrl);
// 输出:http://localhost:3000/user/verify?token=xxx&userId=123(开发环境)
// 或:https://example.com/user/verify?token=xxx&userId=123(生产环境)
res.send('验证邮件已发送');
});二、强制 HTTPS 跳转(安全场景)
生产环境中,为保障数据传输安全,通常要求所有请求强制使用 HTTPS。此时需通过 req.protocol 判断当前请求是否为 http,若是则重定向到对应的 https 地址。
注意:若应用部署在反向代理后(如 Nginx、Apache),需先配置 app.set('trust proxy', true),否则 req.protocol 会读取代理与应用之间的协议(通常是 http),导致判断错误。
代码示例:全局 HTTPS 强制跳转中间件
// 全局中间件:所有请求先经过此判断
app.use((req, res, next) => {
// 1. 判断是否为生产环境 + 请求协议是 http
if (process.env.NODE_ENV === 'production' && req.protocol === 'http') {
// 2. 重定向到 https 地址(保留原路径和查询参数)
return res.redirect(301, `https://${req.get('host')}${req.originalUrl}`);
}
next(); // 协议正确,继续执行后续逻辑
});三、动态生成资源 URL(前端适配)
当前端需要加载后端动态生成的资源(如用户头像、导出的 Excel 文件)时,若直接使用相对路径(/uploads/avatar.jpg),在 HTTPS 环境下可能触发浏览器 “混合内容” 警告(HTTP 资源嵌入 HTTPS 页面)。
此时需通过 req.protocol 动态生成资源的完整 URL,确保协议与当前请求一致。
代码示例:返回用户信息(含头像完整 URL)
app.get('/user/profile', (req, res) => {
const user = {
id: 123,
name: '张三',
// 用 req.protocol 拼接头像完整 URL
avatarUrl: `${req.protocol}://${req.get('host')}/uploads/avatar-123.jpg`
};
res.json(user);
// 输出:{"id":123,"name":"张三","avatarUrl":"https://example.com/uploads/avatar-123.jpg"}
});四、请求日志记录(排查与统计)
在生产环境中,日志是排查问题、分析请求的重要依据。日志中通常需要记录请求协议,以便:
- 区分 http 和 https 的请求量(判断 HTTPS 覆盖率);
- 定位协议相关的错误(如 http 请求触发的安全限制)。
代码示例:记录请求日志(含协议)
// 日志中间件
app.use((req, res, next) => {
const logInfo = {
timestamp: new Date().toISOString(),
method: req.method, // 请求方法(GET/POST)
protocol: req.protocol, // 请求协议(http/https)
url: req.originalUrl, // 原始 URL
ip: req.ip // 请求 IP
};
// 写入日志文件(或日志系统,如 ELK)
console.log(JSON.stringify(logInfo));
// 输出示例:{"timestamp":"2024-05-20T10:30:00.000Z","method":"GET","protocol":"https","url":"/user/profile","ip":"127.0.0.1"}
next();
});五、适配反向代理环境(生产部署关键)
在生产环境中,应用通常不会直接暴露给客户端,而是通过反向代理(如 Nginx、Cloudflare、AWS ALB)转发请求(代理负责处理 SSL 终止,即客户端→代理用 HTTPS,代理→应用用 HTTP)。
此时若未配置 trust proxy,req.protocol 会错误地读取为 http(代理到应用的协议);只有配置 app.set('trust proxy', true) 后,Express 才会信任代理传递的 X-Forwarded-Proto 头部(该头部记录客户端实际使用的协议),使 req.protocol 返回正确的 https。
配置示例:生产环境适配反向代理
// 生产环境下,信任反向代理(确保 req.protocol 正确)
if (process.env.NODE_ENV === 'production') {
// 1. 信任代理(可指定具体代理 IP,更安全,如 app.set('trust proxy', '127.0.0.1'))
app.set('trust proxy', true);
// 2. 此时 req.protocol 会读取 X-Forwarded-Proto 头部(如客户端用 HTTPS,则返回 https)
}Nginx 配合配置(需传递 X-Forwarded-Proto 头部):
server {
listen 443 ssl;
server_name example.com;
# SSL 配置(证书等)
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
# 转发请求到 Express 应用(假设运行在 3000 端口)
proxy_pass http://localhost:3000;
# 传递关键头部,让 Express 正确识别协议和 IP
proxy_set_header X-Forwarded-Proto $scheme; # $scheme 即当前协议(https)
proxy_set_header X-Forwarded-For $remote_addr;
}
}总结:req.protocol 的核心价值
它是 Express 中获取请求协议的 “权威来源”,尤其在需要依赖协议做安全控制(HTTPS 强制)、资源生成(绝对 URL)、日志统计的场景中,是确保功能正确性和安全性的关键属性。
注意事项:
- 生产环境若有反向代理,必须配置 trust proxy,否则 req.protocol 会失真;
- 避免硬编码 http/https,优先使用 req.protocol 实现环境自适应。
req.query
一个对象,为每一个路由中的query string参数都分配一个属性。如果没有query string,它就是一个空对象,{}。
// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"
req.query.shoe.color
// => "blue"
req.query.shoe.type
// => "converse"req.query 是 Express 中用于获取 URL 查询参数(即 ? 后面的键值对,如 ?page=1&size=10)的核心属性,返回一个解析后的对象。它在实战中是处理 “动态筛选、分页、排序、搜索” 等需求的基础工具,以下结合具体业务场景展开其用法:
一、先明确 req.query 的本质
- 来源:URL 中 ? 后的参数(如 https://example.com/users?page=2&status=active 中的 page=2 和 status=active)。
- 格式:解析为键值对对象,参数值默认是字符串(如上述 URL 中 req.query = { page: '2', status: 'active' })。
- 特点:支持嵌套结构(如 ?filter[age]=18&filter[gender]=male → req.query.filter = { age: '18', gender: 'male' })和数组(如 ?ids=1&ids=2 → req.query.ids = ['1', '2'])。
二、实战核心场景(附代码示例)
- 数据分页(最高频场景)
业务需求:列表接口(如用户列表、订单列表)需要支持分页,通过 page(页码)和 size(每页条数)控制返回数据范围。
核心逻辑:用 req.query 获取分页参数,转换为数字类型,设置默认值(避免参数缺失导致错误),再传递给数据库查询。
// 示例:用户列表接口(支持分页)
app.get('/users', async (req, res) => {
// 1. 从 req.query 获取分页参数,转换为数字(默认值:page=1,size=10)
const page = parseInt(req.query.page) || 1;
const size = parseInt(req.query.size) || 10;
// 2. 校验参数合理性(避免页码为0或负数,每页条数过大)
const validPage = Math.max(page, 1); // 页码最小为1
const validSize = Math.min(Math.max(size, 1), 100); // 每页条数 1-100 之间
// 3. 计算数据库查询的偏移量(skip = (页码-1) * 每页条数)
const skip = (validPage - 1) * validSize;
// 4. 数据库查询(以 MongoDB 为例)
const users = await User.find()
.skip(skip)
.limit(validSize);
const total = await User.countDocuments(); // 总条数
// 5. 返回分页结果
res.json({
data: users,
pagination: {
page: validPage,
size: validSize,
total,
totalPages: Math.ceil(total / validSize) // 总页数
}
});
});
// 测试:
// 请求 GET /users?page=2&size=15 → 返回第2页,每页15条数据
// 请求 GET /users → 用默认值(page=1,size=10)- 数据筛选(多条件过滤)
业务需求:列表接口需要支持按条件筛选(如订单列表按状态、时间范围筛选;商品列表按分类、价格区间筛选)。
核心逻辑:用 req.query 获取筛选参数,动态构建数据库查询条件(避免硬编码)。
// 示例:订单列表接口(支持多条件筛选)
app.get('/orders', async (req, res) => {
// 1. 从 req.query 获取筛选参数
const {
status, // 订单状态:'pending'(待支付)、'paid'(已支付)、'cancelled'(已取消)
minAmount, // 最小金额
maxAmount, // 最大金额
startDate, // 开始日期(如 '2024-01-01')
endDate // 结束日期
} = req.query;
// 2. 动态构建查询条件(初始为空对象)
const queryConditions = {};
// 状态筛选(若传递了 status,则添加条件)
if (status) {
queryConditions.status = status;
}
// 金额范围筛选(若传递了 minAmount 或 maxAmount)
if (minAmount || maxAmount) {
queryConditions.amount = {};
if (minAmount) queryConditions.amount.$gte = Number(minAmount); // 大于等于
if (maxAmount) queryConditions.amount.$lte = Number(maxAmount); // 小于等于
}
// 时间范围筛选(按创建时间)
if (startDate || endDate) {
queryConditions.createdAt = {};
if (startDate) queryConditions.createdAt.$gte = new Date(startDate);
if (endDate) queryConditions.createdAt.$lte = new Date(endDate);
}
// 3. 数据库查询(MongoDB 示例)
const orders = await Order.find(queryConditions);
res.json({ data: orders });
});
// 测试:
// 请求 GET /orders?status=paid&minAmount=100&startDate=2024-05-01 → 筛选已支付、金额≥100、5月1日后的订单
// 请求 GET /orders?maxAmount=50 → 筛选金额≤50的订单- 数据排序(指定字段和方向)
业务需求:列表接口支持按指定字段排序(如按创建时间升序 / 降序、按价格从高到低)。
核心逻辑:用 req.query 获取排序字段(sortBy)和排序方向(sortOrder),动态构建排序规则。
// 示例:商品列表接口(支持排序)
app.get('/products', async (req, res) => {
// 1. 获取排序参数(默认按创建时间降序)
const sortBy = req.query.sortBy || 'createdAt'; // 排序字段:'price'、'createdAt' 等
const sortOrder = req.query.sortOrder === 'asc' ? 1 : -1; // 1=升序,-1=降序(默认降序)
// 2. 校验排序字段(只允许指定字段,防止注入风险)
const allowedSortFields = ['createdAt', 'price', 'sales']; // 允许的排序字段
const validSortBy = allowedSortFields.includes(sortBy) ? sortBy : 'createdAt';
// 3. 数据库查询(按条件排序)
const products = await Product.find()
.sort({ [validSortBy]: sortOrder }); // 动态排序规则
res.json({ data: products });
});
// 测试:
// 请求 GET /products?sortBy=price&sortOrder=asc → 按价格升序排列
// 请求 GET /products?sortBy=sales → 按销量降序排列(默认降序)- 搜索功能(关键词匹配)
业务需求:实现简单的关键词搜索(如搜索文章标题、商品名称中包含某关键词)。
核心逻辑:用 req.query.q 获取搜索关键词,通过数据库的模糊匹配语法(如 MongoDB 的 $regex)实现搜索。
// 示例:文章搜索接口
app.get('/articles', async (req, res) => {
// 1. 获取搜索关键词(q 是约定的参数名,如 ?q=Node.js)
const keyword = req.query.q;
// 2. 构建搜索条件(若有关键词,则模糊匹配标题或内容)
const searchConditions = keyword
? {
$or: [
{ title: { $regex: keyword, $options: 'i' } }, // 标题包含关键词(忽略大小写)
{ content: { $regex: keyword, $options: 'i' } } // 内容包含关键词
]
}
: {}; // 无关键词则返回所有
// 3. 数据库查询
const articles = await Article.find(searchConditions);
res.json({
data: articles,
keyword,
count: articles.length
});
});
// 测试:
// 请求 GET /articles?q=Express → 搜索标题或内容包含 Express 的文章
// 请求 GET /articles → 返回所有文章- 传递配置参数(控制返回数据格式)
业务需求:允许客户端控制接口返回的数据格式(如是否返回精简信息、是否包含关联数据)。
核心逻辑:用 req.query 获取配置参数(如 ?simple=true 表示返回精简数据),动态调整响应内容。
// 示例:用户详情接口(支持返回精简/完整信息)
app.get('/users/:id', async (req, res) => {
const userId = req.params.id;
// 获取配置参数:simple=true 则返回精简信息
const isSimple = req.query.simple === 'true';
// 查询用户数据
const user = await User.findById(userId);
if (!user) return res.status(404).json({ msg: '用户不存在' });
// 根据配置返回不同格式
if (isSimple) {
// 精简信息:只返回 id、name、avatar
res.json({
data: {
id: user._id,
name: user.name,
avatar: user.avatar
}
});
} else {
// 完整信息:包含所有字段(如手机号、邮箱、注册时间等)
res.json({
data: {
id: user._id,
name: user.name,
avatar: user.avatar,
phone: user.phone,
email: user.email,
createdAt: user.createdAt
}
});
}
});
// 测试:
// 请求 GET /users/123?simple=true → 返回精简用户信息
// 请求 GET /users/123 → 返回完整用户信息- 处理数组参数(多选筛选)
业务需求:支持多选条件筛选(如筛选多个分类的商品、多个状态的订单)。
核心逻辑:URL 中数组参数的格式为 ?ids=1&ids=2&ids=3,req.query 会自动解析为数组 { ids: ['1', '2', '3'] },直接用于数据库的 $in 查询。
// 示例:筛选多个分类的商品
app.get('/products', async (req, res) => {
// 获取分类 ID 数组(如 ?categoryIds=1&categoryIds=2 → [ '1', '2' ])
const categoryIds = req.query.categoryIds;
// 构建查询条件(若有分类 ID 数组,则筛选包含这些 ID 的商品)
const query = categoryIds
? { categoryId: { $in: categoryIds.map(id => Number(id)) } } // 转换为数字数组
: {};
const products = await Product.find(query);
res.json({ data: products });
});
// 测试:
// 请求 GET /products?categoryIds=1&categoryIds=3 → 返回分类 ID 为 1 或 3 的商品三、实战注意事项
- 参数类型转换:
req.query 的所有值默认是 字符串,需手动转换为数字、布尔值等类型(如分页的 page 转数字,simple=true 转布尔值)。
错误示例:if (req.query.page === 1) → 永远为 false(因为 req.query.page 是 '1'),需改为 if (parseInt(req.query.page) === 1)。
- 默认值与边界校验:
必须设置默认值(如 page 默认 1),并校验参数合理性(如 size 不能超过 100,避免一次请求过多数据导致性能问题)。
- 安全过滤:
- 对于排序、筛选字段,需限制为允许的范围(如 sortBy 只能是 ['createdAt', 'price']),防止恶意参数(如 SQL 注入、NoSQL 注入)。
- 避免在查询参数中传递敏感信息(如密码、token),此类信息应放在请求头或请求体中。
- 复杂参数的解析:
对于嵌套对象(如 ?filter[age]=18),Express 会自动解析为 req.query.filter = { age: '18' },可直接用于构建多层查询条件。
- 编码问题:
查询参数中若包含特殊字符(如空格、中文、&),浏览器会自动 URL 编码(如空格→%20,中文→%E5%BC%A0%E4%B8%89),Express 会自动解码,无需手动处理。
四、总结:req.query 的核心价值
它是处理 “客户端动态请求配置” 的核心工具,几乎所有列表、搜索类接口都依赖它实现分页、筛选、排序等功能。其核心优势是:
- 无需手动解析 URL 字符串,直接获取结构化的键值对;
- 支持复杂结构(数组、嵌套对象),满足多样的筛选需求;
- 与数据库查询逻辑天然适配,可动态构建查询条件。
掌握 req.query 的处理逻辑(类型转换、默认值、校验),是开发灵活、健壮的 API 接口的基础。
req.route
当前匹配的路由,其为一串字符。比如:
app.get('/user/:id?', function userIdHandler(req, res) {
console.log(req.route);
res.send('GET')
})前面片段的输出为:
{ path:"/user/:id?"
stack:
[
{ handle:[Function:userIdHandler],
name:"userIdHandler",
params:undefined,
path:undefined,
keys:[],
regexp:/^\/?$/i,
method:'get'
}
]
methods:{get:true}
}req.route 是 Express 中用于获取当前请求所匹配的路由的详细信息的属性,返回一个包含路由路径、处理函数、请求方法等元数据的对象。它的实战价值主要体现在路由信息的动态获取、调试、日志记录等场景,尤其在复杂路由配置或需要基于当前路由动态处理逻辑时发挥作用。
一、先明确 req.route 的核心信息
req.route 包含当前匹配路由的关键元数据,以示例 app.get('/user/:id?', handler) 为例,其结构如下:
{
path: "/user/:id?", // 路由定义的路径(含参数规则)
stack: [ // 路由对应的处理函数栈
{
handle: [Function: userIdHandler], // 路由处理函数
method: 'get' // 请求方法
}
],
methods: { get: true } // 支持的请求方法(get/post等)
}这些信息直接反映了当前请求匹配的路由规则,是动态获取路由配置的 “入口”。
二、实战核心用途
- 精细化日志记录:追踪请求匹配的具体路由
在生产环境中,日志不仅需要记录请求的 URL,有时还需要明确该请求匹配了哪个路由规则(尤其是路由包含参数或通配符时),以便排查 “路由匹配异常” 问题。req.route.path 可直接提供路由定义的原始路径(如 /user/:id?),补充日志维度。
代码示例(路由级日志):
// 全局日志中间件:记录请求匹配的路由规则
app.use((req, res, next) => {
// 仅在路由匹配后(进入路由处理函数前),req.route 才存在
if (req.route) {
const log = {
url: req.originalUrl, // 客户端请求的原始URL(如 /user/123)
matchedRoute: req.route.path, // 匹配的路由规则(如 /user/:id?)
method: req.method,
timestamp: new Date().toISOString()
};
console.log(`[路由日志] ${JSON.stringify(log)}`);
// 输出示例:{"url":"/user/123","matchedRoute":"/user/:id?","method":"GET","timestamp":"2024-05-20T10:30:00.000Z"}
}
next();
});
// 定义带参数的路由
app.get('/user/:id?', (req, res) => {
res.send(`用户ID:${req.params.id || '未提供'}`);
});价值:当客户端请求 /user/123 时,日志能同时记录 “原始 URL” 和 “匹配的路由规则”,便于区分 “路由参数不同但匹配同一规则” 的请求(如 /user/123 和 /user/456 都匹配 /user/:id?)。
- 路由级权限控制:基于路由规则动态验证权限
某些场景下,权限控制需要基于路由的原始定义(而非请求的 URL)。例如:“所有 /admin/* 路由需要管理员权限”,但直接通过 req.path 判断可能遗漏复杂路由(如 /admin/user/:id/edit),而 req.route.path 可直接获取路由规则,更精准匹配。
代码示例(基于路由规则的权限中间件):
// 权限中间件:根据路由规则判断是否需要管理员权限
const checkAdminRoute = (req, res, next) => {
if (!req.route) {
return next(); // 未匹配路由,直接放行(后续可能返回404)
}
// 获取当前路由的原始路径(如 /admin/user/:id/edit)
const routePath = req.route.path;
// 定义需要管理员权限的路由规则(支持通配符)
const adminRoutes = ['/admin/*', '/system/setting'];
// 判断当前路由是否在管理员路由列表中
const isAdminRoute = adminRoutes.some(adminPath => {
// 简单匹配:若路由规则以 adminPath 开头(实际可结合正则增强)
return routePath.startsWith(adminPath.replace('*', ''));
});
if (isAdminRoute) {
// 管理员路由:验证权限
if (req.session.role === 'admin') {
return next();
} else {
return res.status(403).send('需要管理员权限');
}
}
// 非管理员路由:直接放行
next();
};
// 应用中间件
app.use(checkAdminRoute);
// 管理员路由示例
app.get('/admin/user/:id/edit', (req, res) => {
res.send('编辑管理员用户');
});
// 普通路由示例
app.get('/user/profile', (req, res) => {
res.send('用户个人资料');
});价值:通过 req.route.path 直接基于路由定义判断权限,避免因 URL 参数变化(如 /admin/user/123/edit 和 /admin/user/456/edit)导致的权限判断失效。
- 调试与开发工具:快速定位路由匹配问题
开发阶段,若遇到 “请求未按预期匹配路由”(如请求 /user 却匹配到 /user/:id),可通过打印 req.route 快速确认当前匹配的路由规则,排查路由定义顺序、参数规则是否有误。
代码示例(开发环境调试):
// 仅在开发环境启用调试
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
// 进入路由处理函数后打印路由信息
const originalSend = res.send;
res.send = function(body) {
if (req.route) {
console.log(`[路由调试] 请求 ${req.method} ${req.originalUrl} 匹配路由:`, req.route.path);
// 输出示例:[路由调试] 请求 GET /user 匹配路由:/user/:id?
}
return originalSend.call(this, body);
};
next();
});
}
// 路由定义(注意顺序:带参数的路由可能覆盖固定路由)
app.get('/user/:id?', (req, res) => { res.send('用户详情') });
app.get('/user', (req, res) => { res.send('用户列表') }); // 此路由永远不会被匹配,因为上面的 /user/:id? 会优先匹配价值:通过调试日志可发现 “路由定义顺序错误”(如带参数的路由覆盖了固定路由),快速定位问题。
- 动态生成 API 文档:自动提取路由信息
开发 API 文档时,若需自动同步路由定义(如路径、支持的方法),可通过 req.route 在请求处理中收集路由信息,再汇总生成文档(尤其适合开发阶段的临时文档)。
代码示例(简易 API 文档生成):
// 收集路由信息的数组(开发环境用)
const apiDocs = [];
// 中间件:收集路由信息(去重)
app.use((req, res, next) => {
if (req.route && process.env.NODE_ENV === 'development') {
const routeInfo = {
path: req.route.path,
method: Object.keys(req.route.methods)[0].toUpperCase(), // 提取请求方法(get→GET)
description: '未定义描述' // 可扩展为从路由注释中提取
};
// 去重:避免同一路由被多次请求时重复添加
if (!apiDocs.some(doc => doc.path === routeInfo.path && doc.method === routeInfo.method)) {
apiDocs.push(routeInfo);
}
}
next();
});
// 提供 API 文档接口
app.get('/api-docs', (req, res) => {
res.json({
apiList: apiDocs,
generatedAt: new Date().toISOString()
});
});
// 定义示例路由
app.get('/user/:id', (req, res) => { res.send('用户详情') });
app.post('/user', (req, res) => { res.send('创建用户') });测试:多次请求 /user/123 和 /user 后,访问 /api-docs 会返回自动收集的路由信息:
{
"apiList": [
{ "path": "/user/:id", "method": "GET", "description": "未定义描述" },
{ "path": "/user", "method": "POST", "description": "未定义描述" }
],
"generatedAt": "2024-05-20T10:30:00.000Z"
}价值:减少手动维护 API 文档的成本,确保文档与实际路由定义同步。
三、注意事项
- 可用性限制:req.route 仅在请求成功匹配路由后才存在(进入路由处理函数或后续中间件),未匹配路由的请求(如 404)中 req.route 为 undefined。
- 内部实现依赖:req.route 包含 Express 内部路由的元数据(如 stack 数组),这些细节可能随版本变化,建议仅依赖稳定字段(path、methods),避免直接操作 stack 等内部属性。
- 性能考量:频繁读取 req.route 并处理(如日志、文档生成)可能增加轻微性能开销,建议仅在开发环境或必要的生产场景中使用。
四、总结:req.route 的核心价值
req.route 的核心作用是动态获取当前请求匹配的路由元数据,其实战场景集中在:
- 精细化日志(记录路由规则,辅助问题排查);
- 路由级权限控制(基于路由定义判断权限);
- 开发调试(快速定位路由匹配问题);
- 动态文档生成(自动同步路由信息)。
虽然它的使用频率低于 req.query、req.params 等属性,但在需要 “基于路由定义而非请求 URL” 处理逻辑的场景中,是不可替代的工具。
req.secure
一个布尔值,如果建立的是TLS的连接,那么就为true。等价与:
'https' == req.protocol;一、先明确 req.secure 的核心特性
- 本质:布尔值(true/false),true 表示请求通过 HTTPS(TLS 加密),false 表示通过 HTTP(未加密)。
- 等价关系:req.secure === (req.protocol === 'https'),但 req.secure 更简洁,适合直接用于条件判断。
- 代理场景依赖:若应用部署在反向代理(如 Nginx、CDN)后,需配置 trust proxy 并确保代理传递 X-Forwarded-Proto 头部,否则 req.secure 会因 “代理→应用” 使用 HTTP 而误判为 false(后续场景会详细说明)。
二、实战核心场景(附代码示例)
- 强制 HTTPS 跳转(生产环境必用)
业务需求:生产环境中,为保障数据传输安全(如用户密码、支付信息),需将所有 HTTP 请求自动重定向到 HTTPS 地址,避免未加密的请求泄露敏感数据。
核心逻辑:通过 req.secure 判断当前协议,若为 false(HTTP),则用 res.redirect 重定向到相同路径的 HTTPS 地址。
// 全局中间件:强制 HTTPS 跳转(仅生产环境启用)
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
// 若请求未通过 HTTPS,且不是内部跳转(避免循环)
if (!req.secure && req.get('host') !== 'localhost') {
// 重定向规则:http://example.com/path → https://example.com/path
const httpsUrl = `https://${req.get('host')}${req.originalUrl}`;
return res.redirect(301, httpsUrl); // 301 永久重定向,利于 SEO
}
next();
});
}
// 测试:
// 请求 HTTP://example.com/login → 自动重定向到 HTTPS://example.com/login
// 请求 HTTPS://example.com/login → 正常进入后续逻辑关键补充(反向代理适配):
若应用通过 Nginx 代理(Nginx 处理 SSL 终止),需配置 Nginx 传递 X-Forwarded-Proto 头部,并在 Express 中启用 trust proxy,否则 req.secure 会误判为 false:
// Express 配置(生产环境)
app.set('trust proxy', true); // 信任代理,让 Express 采信代理传递的头部
// Nginx 配置(关键部分)
server {
listen 80; // 监听 HTTP 端口
server_name example.com;
# HTTP 请求直接重定向到 HTTPS(也可在 Nginx 层完成,减少 Express 压力)
return 301 https://$host$request_uri;
}
server {
listen 443 ssl; // 监听 HTTPS 端口
server_name example.com;
ssl_certificate /path/to/cert.pem; // SSL 证书
location / {
proxy_pass http://localhost:3000;
# 传递协议信息,让 Express 正确识别 req.secure
proxy_set_header X-Forwarded-Proto $scheme; // $scheme 为 "https"
}
}- 敏感接口保护(仅允许 HTTPS 访问)
业务需求:部分核心接口(如登录、支付、修改密码)对安全性要求极高,需强制限制仅允许 HTTPS 访问,拒绝所有 HTTP 请求,防止敏感数据在传输中被窃取。
核心逻辑:在敏感接口的路由前添加中间件,通过 req.secure 判断协议,若为 false 则直接返回 403 错误。
// 中间件:仅允许 HTTPS 访问
const requireHttps = (req, res, next) => {
// 开发环境允许 HTTP(方便调试),生产环境强制 HTTPS
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.status(403).json({
success: false,
msg: '该接口仅支持 HTTPS 访问,请使用安全链接'
});
}
next();
};
// 敏感接口:登录(仅允许 HTTPS)
app.post('/api/login', requireHttps, (req, res) => {
const { username, password } = req.body;
// 登录逻辑...
res.json({ success: true, msg: '登录成功' });
});
// 普通接口:获取商品列表(允许 HTTP/HTTPS)
app.get('/api/products', (req, res) => {
res.json({ success: true, data: [] });
});
// 测试:
// 生产环境 HTTP 请求 /api/login → 返回 403 错误
// 生产环境 HTTPS 请求 /api/login → 正常处理登录- 动态设置安全响应头(HTTPS 专属)
业务需求:某些安全相关的响应头(如 Strict-Transport-Security,简称 HSTS)仅在 HTTPS 环境下有效,用于强制浏览器后续仅用 HTTPS 访问当前域名,避免 “降级攻击”(浏览器被诱导用 HTTP 访问)。
核心逻辑:通过 req.secure 判断,仅在 HTTPS 下设置 HSTS 等专属头。
app.use((req, res, next) => {
// 仅在 HTTPS 下设置 HSTS 头(有效期 1 年,包含子域名)
if (req.secure) {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
// 其他 HTTPS 专属头(如防止 XSS 的 Content-Security-Policy)
res.setHeader('Content-Security-Policy', "default-src 'self'");
}
// 通用安全头(HTTP/HTTPS 均适用)
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});
// 效果:
// HTTPS 请求 → 响应头包含 Strict-Transport-Security 和 Content-Security-Policy
// HTTP 请求 → 仅包含 X-XSS-Protection- 日志记录:标记请求安全级别
业务需求:生产环境日志需记录每个请求的 “安全级别”(是否通过 HTTPS),便于后续审计(如排查 “敏感操作是否通过加密连接”)或分析 HTTP/HTTPS 访问占比。
核心逻辑:将 req.secure 转换为 “http/https” 字符串,存入日志字段。
// 日志中间件
app.use((req, res, next) => {
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl,
ip: req.ip,
// 记录请求的安全协议(http/https)
protocol: req.secure ? 'https' : 'http',
statusCode: res.statusCode
};
console.log(JSON.stringify(log));
// 示例输出:
// {"timestamp":"2024-05-21T09:30:00.000Z","method":"POST","url":"/api/login","ip":"127.0.0.1","protocol":"https","statusCode":200}
next();
});- 动态生成资源 URL(避免混合内容警告)
业务需求:前端页面中若加载的资源(如图片、JS、CSS)协议与页面协议不一致(如 HTTPS 页面加载 HTTP 资源),浏览器会触发 “混合内容警告”,甚至阻断资源加载。需根据 req.secure 动态生成与当前协议匹配的资源 URL。
// 接口:返回商品信息(含商品图片 URL)
app.get('/api/products/:id', (req, res) => {
const productId = req.params.id;
const product = {
id: productId,
name: '手机',
// 动态生成图片 URL:与当前请求协议一致
imageUrl: `${req.secure ? 'https' : 'http'}://${req.get('host')}/images/${productId}.jpg`
};
res.json({ success: true, data: product });
});
// 效果:
// HTTPS 请求 → imageUrl: "https://example.com/images/1.jpg"
// HTTP 请求 → imageUrl: "http://example.com/images/1.jpg"
// 避免前端加载资源时出现混合内容警告三、关键注意事项
- 反向代理下的配置陷阱:
若应用部署在反向代理后(如 Nginx 处理 SSL),“客户端→代理” 用 HTTPS,但 “代理→应用” 用 HTTP,此时默认 req.secure 会返回 false(错误)。需通过以下两步修正:
- Express 配置 app.set('trust proxy', true)(信任代理);
- 代理(如 Nginx)传递 X-Forwarded-Proto: https 头部(告诉 Express 客户端实际用的是 HTTPS)。
- 开发环境的灵活性:
开发环境(如本地 localhost:3000)通常不配置 SSL,可通过环境变量判断是否启用 req.secure 相关逻辑(如开发环境跳过 HTTPS 强制跳转),避免影响开发效率。
- 与 req.protocol 的选择:
- 若需 布尔判断(是否为 HTTPS),用 req.secure(更简洁,如 if (req.secure));
- 若需 获取协议字符串(如拼接 URL 时需要 'http' 或 'https'),用 req.protocol(如 const proto = req.protocol)。
四、总结:req.secure 的核心价值
req.secure 是 Express 中 判断请求安全级别 的核心属性,其高频实战场景围绕 “安全控制” 和 “协议适配”:
- 生产环境强制 HTTPS 跳转,保障数据传输安全;
- 敏感接口拒绝 HTTP 访问,防止敏感信息泄露;
- 动态设置 HTTPS 专属安全头,增强应用安全性;
- 日志记录请求协议,便于审计和分析;
- 生成与当前协议匹配的资源 URL,避免混合内容警告。
掌握 req.secure 的用法(尤其是反向代理下的配置),是生产环境 Express 应用保障安全性的基础。
req.signedCookies
当使用cookie-parser中间件的时候,这个属性包含的是请求发过来的签名cookies,这个属性取得的是不含签名,可以直接使用的值。签名的cookies保存在不同的对象中来体现开发者的意图;不然,一个恶意攻击可以被施加在req.cookie值上(它是很容易被欺骗的)。记住,签名一个cookie不是把它藏起来或者加密;而是简单的防止篡改(因为签名使用的加密是私人的)。如果没有发送签名的cookie,那么这个属性默认为{}。
// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
req.signedCookies.user
// => "tobi"为了获取更多的信息,问题或者关注,可以参阅cookie-parser。
一、核心前提:理解签名 Cookie 的本质
依赖 cookie-parser 中间件:需在 Express 中初始化 cookie-parser 并传入密钥(用于生成 / 验证签名),否则无法使用签名 Cookie:
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
// 初始化 cookie-parser,密钥用于签名Cookie(生产环境需用环境变量存储,避免硬编码)
app.use(cookieParser('your-strong-secret-key-123')); 签名 vs 普通 Cookie:
- 普通 Cookie:存储在 req.cookies,客户端可随意篡改(如将 userId=1 改为 userId=2);
- 签名 Cookie:存储在 req.signedCookies,设置时会用密钥生成签名(附加在 Cookie 值后,如 tobi.CP7AWaXDfAKIRfH49dQz),客户端篡改后签名验证失败,该 Cookie 会被排除在 req.signedCookies 之外。
- 关键提醒:签名 Cookie不是加密(内容仍可见),仅用于验证 “是否被篡改”;若需隐藏内容,需额外加密。
二、实战核心场景(附代码示例)
1. 用户身份标识与会话保持(最高频场景)
业务需求:用户登录后,需在客户端存储身份标识(如 userId),后续请求通过该标识识别用户。若用普通 Cookie,用户可篡改 userId 冒充他人;用签名 Cookie 可防止此风险。
代码示例:登录 + 身份验证流程
// 1. 登录接口:设置签名Cookie(存储 userId)
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 模拟数据库验证(实际需查库)
if (username === 'admin' && password === '123456') {
const userId = '1001'; // 从数据库获取的用户ID
// 设置签名Cookie:signed: true 表示启用签名
res.cookie(
'userId', // Cookie名
userId, // Cookie值
{
signed: true, // 关键:启用签名
httpOnly: true, // 防止前端JS读取(防XSS)
maxAge: 24 * 60 * 60 * 1000 // 有效期1天
}
);
return res.json({ success: true, msg: '登录成功' });
}
res.status(401).json({ success: false, msg: '账号密码错误' });
});
// 2. 身份验证中间件:从 signedCookies 获取 userId
const authMiddleware = (req, res, next) => {
// 从签名Cookie中获取 userId(若被篡改,req.signedCookies.userId 为 undefined)
const userId = req.signedCookies.userId;
if (!userId) {
return res.status(403).json({ success: false, msg: '未登录或登录已过期' });
}
// 验证通过:将 userId 挂载到 req 上,供后续接口使用
req.userId = userId;
next();
};
// 3. 需登录的接口:使用身份验证中间件
app.get('/user/profile', authMiddleware, (req, res) => {
// 从 req.userId 获取已验证的用户ID(无需担心篡改)
res.json({
success: true,
data: { userId: req.userId, username: 'admin', avatar: '/avatar.jpg' }
});
});
// 测试:
// 登录后客户端Cookie:userId=1001.CP7AWaXDfAKIRfH49dQz(带签名)
// 若用户篡改Cookie为 userId=1002.xxx → req.signedCookies.userId 为 undefined → 拦截登录价值:彻底防止 “篡改 Cookie 冒充他人” 的攻击,是用户会话管理的安全基础。
2. 传递不可篡改的客户端配置
业务需求:客户端需获取应用的关键配置(如 “当前环境是否为测试环境”“接口版本号”),但不允许用户篡改这些配置(否则可能导致功能异常或安全漏洞)。
代码示例:动态设置并验证客户端配置
// 1. 初始化接口:给新客户端设置签名配置Cookie
app.get('/init-config', (req, res) => {
const appConfig = {
env: process.env.NODE_ENV || 'development', // 环境(生产/测试)
apiVersion: 'v2' // 接口版本
};
// 将配置JSON字符串存入签名Cookie(需序列化,Cookie值不能含特殊字符)
res.cookie(
'appConfig',
JSON.stringify(appConfig),
{ signed: true, httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 }
);
res.json({ success: true, msg: '配置初始化完成' });
});
// 2. 接口请求中间件:验证并解析配置
app.use((req, res, next) => {
// 从签名Cookie获取配置
const configStr = req.signedCookies.appConfig;
if (configStr) {
// 解析配置并挂载到 req 上
req.appConfig = JSON.parse(configStr);
} else {
// 无有效配置:重定向到初始化接口
return res.redirect('/init-config');
}
next();
});
// 3. 业务接口:使用验证后的配置
app.get('/api/data', (req, res) => {
// 根据配置的环境和版本返回对应数据
const { env, apiVersion } = req.appConfig;
res.json({
success: true,
env,
apiVersion,
data: env === 'development' ? '测试环境数据' : '生产环境数据'
});
});
// 测试:
// 若用户篡改 appConfig 为 '{"env":"production","apiVersion":"v1"}' → 签名验证失败 → 重新初始化配置价值:确保客户端使用的配置是服务端下发的原始值,避免篡改导致的功能异常。
重要 3. 辅助 CSRF 攻击防护
业务需求:CSRF(跨站请求伪造)防护需客户端携带 “CSRF Token”(与用户会话绑定),该 Token 需存储在客户端且不可篡改(否则 CSRF 防护失效)。签名 Cookie 是存储 CSRF Token 的安全选择。
代码示例:CSRF Token 生成与验证
const crypto = require('crypto');
// 1. 生成CSRF Token并存入签名Cookie
const generateCsrfToken = (req, res) => {
// 生成随机Token(32位十六进制字符串)
const csrfToken = crypto.randomBytes(16).toString('hex');
// 存入签名Cookie(与用户会话绑定)
res.cookie('csrfToken', csrfToken, { signed: true, httpOnly: true, sameSite: 'strict' });
return csrfToken;
};
// 2. 页面渲染接口:返回CSRF Token(供前端表单提交使用)
app.get('/form-page', (req, res) => {
const csrfToken = generateCsrfToken(req, res);
// 前端页面通过模板渲染获取Token(如EJS模板:<input type="hidden" name="_csrf" value="<%= csrfToken %>">)
res.send(`
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="${csrfToken}">
<input type="text" name="content" placeholder="输入内容">
<button type="submit">提交</button>
</form>
`);
});
// 3. CSRF验证中间件
const csrfMiddleware = (req, res, next) => {
// 从请求体获取前端提交的Token(需先解析表单数据,如用 express.urlencoded())
const submittedToken = req.body._csrf;
// 从签名Cookie获取服务端下发的Token
const serverToken = req.signedCookies.csrfToken;
if (!submittedToken || !serverToken || submittedToken !== serverToken) {
return res.status(403).json({ success: false, msg: 'CSRF验证失败' });
}
// 验证通过:重新生成Token(防止重复使用)
generateCsrfToken(req, res);
next();
};
// 4. 表单提交接口:启用CSRF验证
app.post('/submit', express.urlencoded({ extended: true }), csrfMiddleware, (req, res) => {
res.json({ success: true, msg: '提交成功', content: req.body.content });
});
// 测试:
// 若攻击者伪造表单提交(无有效CSRF Token或篡改Token)→ 验证失败 → 拦截请求价值:作为 CSRF 防护的关键环节,确保 Token 不被篡改,大幅降低跨站请求伪造的风险。
三、实战注意事项
- 密钥安全管理:
- 初始化 cookie-parser 的密钥(如 'your-strong-secret-key')必须高强度(建议用随机字符串),且不能硬编码在代码中,需用环境变量(如 process.env.COOKIE_SECRET)存储;
- 密钥变更后,所有已下发的签名 Cookie 会验证失败(用户需重新登录),生产环境变更需谨慎。
- Cookie 内容限制:
- 签名 Cookie 的内容仍可见(仅防篡改),禁止存储敏感信息(如密码、token),仅存储非敏感标识(如 userId、配置参数);
- Cookie 值不能含空格、逗号等特殊字符,需先序列化(如 JSON.stringify)或编码(如 encodeURIComponent)。
- 必要的 Cookie 属性:
- 必加 httpOnly: true:防止前端 JS 通过 document.cookie 读取 Cookie(防 XSS 攻击);
- 建议加 sameSite: 'strict' 或 'lax':限制 Cookie 仅在同域请求中携带(辅助防 CSRF);
- 合理设置 maxAge 或 expires:避免 Cookie 永久有效(降低泄露风险)。
- 验证失败的处理:
- 若 req.signedCookies 中未获取到预期值(如 userId 为 undefined),需视为 “验证失败”,引导用户重新操作(如重新登录、重新初始化配置),不可直接忽略。
四、总结:req.signedCookies 的核心价值
它是保障 “客户端传递数据可信度” 的关键工具,实战中主要用于:
- 用户身份标识与会话管理(防篡改 userId,避免冒充);
- 不可篡改的客户端配置传递(防功能异常);
- CSRF Token 存储(增强跨站攻击防护)。
相比普通 Cookie(req.cookies),req.signedCookies 解决了 “数据完整性” 问题,是生产环境中处理 Cookie 相关安全需求的必选方案。
req.stale
指示这个请求是否是stale(陈旧的),它与req.fresh是相反的。更多信息,可以查看req.fresh。
req.stale
// => truereq.subdomains
请求中域名的子域名数组。
// Host: "tobi.ferrets.example.com"
req.subdomains
// => ["ferrets", "tobi"]req.xhr
req.xhr 是 Express 中用于判断 当前请求是否为 AJAX 请求 的布尔属性,核心依据是请求头 X-Requested-With 是否等于 XMLHttpRequest(这是 jQuery、axios 等传统 AJAX 库默认携带的头信息)。它的实战价值集中在 “区分请求类型以适配不同响应逻辑”,比如前后端交互中的格式返回、局部刷新处理等场景。
一个布尔值,如果X-Requested-With的值为XMLHttpRequest,那么其为true,其指示这个请求是被一个客服端库发送,比如jQuery。
req.xhr
// => true
一、先明确 req.xhr 的核心逻辑
- 判断依据:req.xhr === true 等价于 req.get('X-Requested-With') === 'XMLHttpRequest',前者是 Express 封装的便捷属性;
- 适用范围:传统 AJAX 库(jQuery、axios 旧版本)默认会携带 X-Requested-With 头,但现代前端框架(Vue/React + axios 新版本、fetch)可能不默认携带,需手动配置(后续注意事项会说明);
- 核心作用:区分 “AJAX 请求”(前端异步交互)和 “普通请求”(页面跳转、直接访问 URL),从而返回不同格式的响应(如 JSON vs HTML)。
二、实战核心场景(附代码示例)
1. 前后端交互:区分响应格式(最高频场景)
业务需求:同一接口可能被 “AJAX 异步调用” 和 “普通页面访问” 两种方式触发(如 /login 接口:前端表单 AJAX 提交 vs 用户直接访问 /login URL)。需根据请求类型返回不同内容:
- AJAX 请求:返回 JSON 数据(便于前端处理错误提示、成功回调);
- 普通请求:返回 HTML 页面(如登录页、跳转后的首页)。
代码示例:登录接口的多格式响应
// 登录接口:同时支持 AJAX 提交和普通页面访问
app.post('/login', express.urlencoded({ extended: true }), (req, res) => {
const { username, password } = req.body;
const loginSuccess = username === 'admin' && password === '123456';
if (loginSuccess) {
// 登录成功:根据请求类型返回不同响应
if (req.xhr) {
// AJAX 请求:返回 JSON(前端接收后跳转页面,如 window.location.href = '/home')
res.json({ success: true, msg: '登录成功', redirectUrl: '/home' });
} else {
// 普通请求:直接重定向到首页(如用户通过表单提交按钮跳转)
res.redirect('/home');
}
} else {
// 登录失败:根据请求类型返回不同响应
if (req.xhr) {
// AJAX 请求:返回 JSON 错误(前端显示“账号密码错误”提示)
res.status(401).json({ success: false, msg: '账号或密码错误' });
} else {
// 普通请求:返回登录页面 + 错误提示(通过模板渲染)
res.render('login', { errorMsg: '账号或密码错误' }); // 假设用 EJS/Handlebars 模板
}
}
});
// 前端 AJAX 提交示例(jQuery):
// $.post('/login', { username: 'admin', password: '123456' }, (data) => {
// if (data.success) window.location.href = data.redirectUrl;
// else alert(data.msg);
// });
// 测试:
// AJAX 提交失败 → 返回 { success: false, msg: '账号或密码错误' }(401);
// 直接访问 /login 提交失败 → 返回登录页面(含错误提示)。价值:避免 “AJAX 请求收到 HTML 页面” 或 “普通请求收到 JSON 字符串” 的异常,适配不同交互场景的前端需求。
2. 页面局部刷新:仅返回必要数据(减少传输量)
业务需求:前端通过 AJAX 实现 “局部刷新”(如列表页的分页加载、表单提交后刷新部分内容),无需返回完整 HTML 页面,仅需返回局部数据(如列表数据、更新后的状态),以减少网络传输量。
代码示例:商品列表分页(局部刷新)
// 商品列表接口:支持 AJAX 局部刷新和普通页面访问
app.get('/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const size = 10;
// 模拟数据库查询商品列表
const products = await Product.find().skip((page-1)*size).limit(size);
const total = await Product.countDocuments();
if (req.xhr) {
// AJAX 请求(分页加载):仅返回列表数据和分页信息(JSON)
res.json({
success: true,
data: products,
pagination: { page, size, total, totalPages: Math.ceil(total/size) }
});
} else {
// 普通请求(首次访问页面):返回完整 HTML 页面(模板渲染)
res.render('product-list', {
products,
pagination: { page, size, total, totalPages: Math.ceil(total/size) }
});
}
});
// 前端 AJAX 分页示例(axios):
// document.getElementById('next-page').onclick = async () => {
// const res = await axios.get('/products?page=2');
// if (res.data.success) {
// // 仅更新列表 DOM,无需刷新页面
// renderProductList(res.data.data);
// updatePagination(res.data.pagination);
// }
// };
// 测试:
// AJAX 请求 /products?page=2 → 返回 JSON 数据(仅数据,无 HTML);
// 直接访问 /products → 返回完整商品列表页面(含 HTML 结构)。价值:局部刷新场景下仅传输必要数据,比返回完整 HTML 减少 90% 以上的传输量,提升交互速度。
3. 权限验证:AJAX 专属的无权限反馈
业务需求:用户登录过期或无权限访问接口时,需根据请求类型返回不同处理:
- AJAX 请求:返回 401/403 JSON 提示(前端接收后弹出登录弹窗或跳转登录页);
- 普通请求:直接重定向到登录页(如用户访问 /home 时登录过期)。
代码示例:权限中间件的差异化反馈
// 全局权限中间件
const requireAuth = (req, res, next) => {
const isLogin = !!req.session.userId; // 假设用 session 存储登录状态
if (!isLogin) {
if (req.xhr) {
// AJAX 请求:返回 401 JSON(前端处理:弹出登录弹窗)
return res.status(401).json({ success: false, msg: '登录已过期,请重新登录', needLogin: true });
} else {
// 普通请求:重定向到登录页(附加原路径,登录后跳转回来)
return res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
}
}
next();
};
// 需登录的接口
app.get('/home', requireAuth, (req, res) => {
if (req.xhr) {
res.json({ success: true, data: { username: 'admin', role: 'super' } });
} else {
res.render('home', { username: 'admin' });
}
});
// 前端 AJAX 无权限处理示例:
// axios.interceptors.response.use(
// (res) => res,
// (err) => {
// if (err.response?.status === 401 && err.response.data.needLogin) {
// // 弹出登录弹窗或跳转登录页
// window.location.href = `/login?redirect=${encodeURIComponent(window.location.href)}`;
// }
// return Promise.reject(err);
// }
// );
// 测试:
// 登录过期后 AJAX 请求 /home → 返回 401 JSON(前端弹登录窗);
// 登录过期后直接访问 /home → 重定向到 /login?redirect=%2Fhome。价值:避免 AJAX 请求因无权限被强制返回登录页 HTML(导致前端解析 JSON 报错),提升用户体验。
4. 避免 AJAX 请求触发页面渲染(防异常)
业务需求:某些路由仅用于 “页面渲染”(如 /about 关于页),若被 AJAX 误调用,无需返回完整 HTML,仅需提示 “不支持 AJAX 请求”,避免前端接收冗余数据或解析错误。
代码示例:禁止 AJAX 访问页面路由
// 关于页路由:仅支持普通页面访问,禁止 AJAX 请求
app.get('/about', (req, res) => {
if (req.xhr) {
// AJAX 请求:返回错误提示(告知不支持该请求类型)
return res.status(400).json({ success: false, msg: '该页面不支持 AJAX 请求,请直接访问 URL' });
}
// 普通请求:返回关于页 HTML
res.render('about', { title: '关于我们', content: '这是公司介绍...' });
});
// 测试:
// AJAX 请求 /about → 返回 400 JSON 错误;
// 直接访问 /about → 返回关于页 HTML。价值:明确路由的使用场景,避免误调用导致的前端异常(如 AJAX 接收 HTML 后尝试解析为 JSON 报错)。
5. 日志统计:标记 AJAX 请求比例
业务需求:统计应用中 “AJAX 交互请求” 和 “普通页面请求” 的占比,分析用户行为(如用户更倾向于异步交互还是页面跳转),用于优化产品设计。
代码示例:请求类型统计日志
// 统计中间件:记录请求是否为 AJAX
app.use((req, res, next) => {
const log = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl,
isAjax: req.xhr ? 'AJAX' : 'Normal', // 标记请求类型
ip: req.ip
};
console.log(JSON.stringify(log));
// 示例输出:
// {"timestamp":"2024-05-22T11:30:00.000Z","method":"GET","url":"/products?page=2","isAjax":"AJAX","ip":"127.0.0.1"}
next();
});价值:通过日志分析 AJAX 请求占比,若占比高说明用户更依赖异步交互,需优先优化 AJAX 接口性能;若占比低,需优化页面跳转体验。
三、实战注意事项
- 现代前端框架的配置问题:
若不配置,req.xhr 会误判为 false,导致响应逻辑错误。
- axios 新版本(0.27+)默认不携带 X-Requested-With 头,需手动配置:
// axios 全局配置:添加 AJAX 标识头
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';- fetch API 完全不默认携带该头,需手动设置:
// fetch 请求添加头
fetch('/api/data', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});- 不可作为安全判断依据:
X-Requested-With 头可被客户端伪造(如手动构造该头发送普通请求),因此 req.xhr 仅用于 “响应格式适配”“交互逻辑区分”,不能用于安全校验(如判断是否为可信前端请求)。
- 与 “前后端分离” 的适配:
纯前后端分离项目(前端用 Vue/React 构建,后端仅提供 API)中,所有请求均为 AJAX 请求,req.xhr 可能始终为 true,此时该属性作用减弱(可统一返回 JSON);但混合项目(既有页面渲染也有 AJAX 交互)中,req.xhr 仍不可或缺。
四、总结:req.xhr 的核心价值
req.xhr 的核心作用是 “区分请求类型,实现差异化响应”,实战中解决的核心问题是:
- 避免 AJAX 请求接收 HTML 页面、普通请求接收 JSON 字符串的异常;
- 适配不同请求场景的用户体验(如 AJAX 无权限弹弹窗,普通请求重定向);
- 优化局部刷新场景的传输效率(仅返回必要数据)。
它是混合开发模式(页面渲染 + AJAX 交互)中连接前后端的关键 “桥梁属性”,虽在纯前后端分离项目中作用减弱,但仍是 Express 开发者必须掌握的基础工具。
Methods
req.accepts(types)
作用:判断客户端(如浏览器、App)能接受什么格式的响应内容(比如 JSON、HTML),返回服务器支持的最佳匹配。
通俗理解:相当于客户端说 “我能看懂这些格式”,服务器查自己 “会返回这些格式”,挑一个客户端最想要的。
细节:
- 参数types可以是格式名(如'json')、MIME 类型(如'application/json')、数组(如['json', 'html']);
- 返回值:匹配到的格式(如'json'),没匹配到返回undefined(此时服务器应返回 406 错误,告诉客户端 “我没有你能看懂的格式”);
- 例子:客户端请求头Accept: text/html, application/json,服务器用req.accepts(['json', 'html'])会返回'html'(因为客户端更优先要 html)。
检查这个指定的内容类型是否被接受,基于请求的Accept HTTP头部。这个方法返回最佳匹配,如果没有一个匹配,那么其返回undefined(在这个case下,服务器端应该返回406和"Not Acceptable")。type值可以是一个单的MIME type字符串(比如application/json),一个扩展名比如json,一个逗号分隔的列表,或者一个数组。对于一个列表或者数组,这个方法返回最佳项(如果有的话)。
// Accept: text/html
req.accepts('html');
// => "html"
// Accept: text/*, application/json
req.accepts('html');
// => "html"
req.accepts('text/html');
// => "text/html"
req.accepts(['json', 'text']);
// => "json"
req.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
req.accepts('image/png');
req.accepts('png');
// => undefined
// Accept: text/*;q=.5, application/json
req.accepts(['html', 'json']);
// => "json"
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
req.acceptsCharsets(charset[, ...])
返回指定的字符集集合中第一个的配置的字符集,基于请求的Accept-CharsetHTTP头。如果指定的字符集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
作用:判断客户端能接受什么字符集(比如 utf-8、gb2312),返回服务器支持的第一个匹配项。
通俗理解:字符集是文本的 “编码规则”(比如 utf-8 能显示中文,gb2312 也能显示中文),这个方法确定用哪种规则给客户端返回文本,避免乱码。
细节:
- 参数是字符集名称(如'utf-8'、'gb2312');
- 返回值:匹配到的字符集(如'utf-8'),没匹配到返回false;
- 例子:客户端接受utf-8, gb2312,服务器支持这两个,用req.acceptsCharsets('utf-8', 'gb2312')会返回'utf-8'(客户端更优先)。
req.acceptsEncodings(encoding[, ...])
返回指定的编码集合中第一个的配置的编码,基于请求的Accept-EncodingHTTP头。如果指定的编码集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
作用:判断客户端能解压什么编码格式(比如 gzip、deflate),返回服务器支持的第一个匹配项。
通俗理解:数据传输时可以压缩(比如 gzip 能把文件变小),这个方法确定用哪种压缩方式,客户端能解压才有效。
细节:
- 参数是编码方式(如'gzip'、'deflate');
- 返回值:匹配到的编码(如'gzip'),没匹配到返回false;
- 例子:客户端支持gzip,服务器用req.acceptsEncodings('gzip')返回'gzip',就可以用 gzip 压缩响应,减少传输流量。
req.acceptsLanguages(lang [, ...])
返回指定的语言集合中第一个的配置的语言,基于请求的Accept-LanguageHTTP头。如果指定的语言集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
作用:判断客户端接受什么语言(比如中文zh-CN、英文en),返回服务器支持的第一个匹配项。
通俗理解:多语言网站用这个方法确定返回哪种语言的内容(比如给中文用户返回中文页面,英文用户返回英文页面)。
细节:
- 参数是语言代码(如'zh-CN'、'en');
- 返回值:匹配到的语言(如'zh-CN'),没匹配到返回false;
- 例子:客户端请求头Accept-Language: zh-CN, en;q=0.8(优先中文),服务器用req.acceptsLanguages('zh-CN', 'en')返回'zh-CN',就返回中文内容。
req.get(field)
返回指定的请求HTTP头部的域内容(不区分大小写)。Referrer和Referer的域内容可互换。
req.get('Content-type');
// => "text/plain"
req.get('content-type');
// => "text/plain"
req.get('Something')
// => undefined其是req.header(field)的别名。
作用:获取请求头里指定字段的值(不区分大小写),别名是req.header(field)。
通俗理解:客户端发请求时会带一些 “说明信息”(比如 “我是谁”“我发的是什么格式数据”),这个方法用来读取这些信息。
细节:
- 参数是请求头字段名(如'Content-Type'、'User-Agent');
- 返回值:字段对应的值(如'application/json'),没找到返回undefined;
- 例子:req.get('Content-Type')可以知道客户端发的是 JSON 还是表单数据;req.get('User-Agent')能知道是浏览器还是手机 App 发来的请求。
req.is(type)
如果进来的请求的Content-type头部域匹配参数type给定的MIME type,那么其返回true。否则返回false。
// With Content-Type: text/html; charset=utf-8
req.is('html');
req.is('text/html');
req.is('text/*');
// => true
// When Content-Type is application/json
req.is('json');
req.is('application/json');
req.is('application/*');
// => true
req.is('html');
// => false获取更多信息,或者如果你有问题或关注,可以参阅type-is。
作用:检查客户端发送的数据类型(Content-Type)是否匹配指定类型,返回true或false。
通俗理解:判断客户端发过来的是 “什么东西”(比如是 JSON 还是图片),用来做数据校验(比如接口只接受 JSON,就拒绝其他类型)。
细节:
- 参数可以是简单类型(如'json')、MIME 类型(如'application/json')、通配符(如'application/*');
- 返回值:匹配返回true,不匹配返回false;
- 例子:客户端发的是 JSON(Content-Type: application/json),req.is('json')返回true,req.is('html')返回false。
req.param(naem, [, defaultValue])
过时的。可以在适合的情况下,使用req.params,req.body或者req.query。
返回当前参数name的值。
// ?name=tobi
req.param('name')
// => "tobi"
// POST name=tobi
req.param('name')
// => "tobi"
// /user/tobi for /user/:name
req.param('name')
// => "tobi"按下面给出的顺序查找:
- req.params
- req.body
- req.query
可选的,你可以指定一个defaultValue来设置一个默认值,如果这个参数在任何一个请求的对象中都不能找到。
直接通过
req.params,req.body,req.query取得应该更加的清晰-除非你确定每一个对象的输入。Body-parser中间件必须加载,如果你使用req.param()。详细请看req.body。作用:(已过时,不推荐用)从路由参数(req.params)、POST 数据(req.body)、URL 查询参数(req.query)里按顺序找指定名字的参数值,找不到返回默认值。
问题与替代方案:
- 模糊了参数来源(不知道是从 URL、表单还是路由来的),容易出错;
- 现在推荐直接用req.params(路由参数)、req.body(POST 数据)、req.query(URL 参数),更清晰。
- 例子:要获取name参数,直接用req.query.name(URL 参数)、req.body.name(表单数据)或req.params.name(路由参数)。
总结
些方法主要用于 “客户端与服务器的沟通协商”:
- accepts系列(内容、字符集、编码、语言):确定服务器返回什么格式 / 编码 / 语言的内容,让客户端能正确处理;
- req.get:读取客户端发过来的 “说明信息”;
- req.is:校验客户端发过来的数据类型;
- req.param:已过时,用具体的params/body/query替代。
Response
res对象代表了当一个HTTP请求到来时,Express程序返回的HTTP响应。在本文档中,按照惯例,这个对象总是简称为res(http请求简称为req),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
例如:
app.get('/user/:id', function(req, res) {
res.send('user' + req.params.id);
});这样写也是一样的:
app.get('/user/:id', function(request, response) {
response.send('user' + request.params.id);
});
Properties
res.app
这个属性持有express程序实例的一个引用,其可以在中间件中使用。res.app和请求对象中的req.app属性是相同的。
res.app 的本质是路由 / 中间件中访问 Express 应用实例的 “快捷通道”,其核心作用是:
- 便捷获取全局配置(如环境变量、密钥);
- 访问应用级共享资源(如数据库连接、缓存);
- 在子应用中复用主应用的资源和配置;
- 动态调整应用行为(如维护模式切换)。
它简化了模块间的依赖传递,让全局资源的访问更直接,是 Express 模块化开发中连接局部逻辑与全局应用的重要桥梁
res.headersSent
布尔类型的属性,指示这个响应是否已经发送HTTP头部。
res.headersSent 用于判断响应头部是否已发送,核心作用是:
- 避免重复发送响应(如多次
res.send); - 防止修改已发送的头部(头部发送后无法再修改),确保响应处理正确。
app.get('/', function(req, res) {
console.log(res.headersSent); // false
res.send('OK'); // send之后就发送了头部
console.log(res.headersSent); // true
});res.locals
res.locals 是 Express 中请求级别的临时数据容器,仅在当前请求的响应周期内有效,核心作用是高效向视图传递动态数据,避免重复代码。
实战中主要用途:
- 共享请求相关数据到视图:比如当前登录用户、请求路径、权限状态等,在中间件中统一设置后,所有视图模板可直接访问(无需在每个
res.render中重复传递)。 - 传递临时消息:如表单提交的错误提示、操作成功信息,存入
res.locals后视图可直接渲染。 - 预处理视图所需数据:在中间件中提前计算通用数据(如用户是否为管理员),视图按需使用,简化路由逻辑。
与 app.locals 互补:app.locals 存全局不变数据(如网站名称),res.locals 存请求级动态数据(如当前用户)
res.locals是 Express 框架中请求级别的临时数据容器,用于在整个请求-响应周期内共享数据(尤其是视图渲染)。与 app.locals(应用级全局数据)互补,形成完整的数据传递体系。
- 请求级别隔离:每个请求拥有独立的
res.locals,数据互不干扰 - 自动清理:响应结束后数据自动销毁,无内存泄漏风险
- 模板直连:无需手动传递,视图模板可直接读取数据
- 中间件共享:任意中间件可读写,实现跨中间件数据传递
6大实战应用场景
1. 用户状态全局传递
// 认证中间件
app.use((req, res, next) => {
res.locals.user = req.user; // 从session获取用户
res.locals.isAdmin = req.user?.role === 'admin';
res.locals.isLoggedIn = !!req.user;
next();
});
// 视图模板直接使用
<% if (isLoggedIn) { %>
<div>欢迎, <%= user.name %>!</div>
<% } %>2. 动态路径高亮
// 导航激活状态
app.use((req, res, next) => {
res.locals.currentPath = req.path; // 获取请求路径
next();
});
// 导航模板
<a href="/dashboard"
class="<%= currentPath === '/dashboard' ? 'active' : '' %>">
控制台
</a>3. 表单错误处理
// 处理登录失败
app.post('/login', (req, res) => {
const user = User.authenticate(req.body);
if (!user) {
res.locals.errors = ['用户名或密码错误'];
return res.render('login'); // 自动携带errors
}
// 登录成功...
});
// 登录模板
<% if (errors) { %>
<div class="alert">
<% errors.forEach(err => { %>
<p><%= err %></p>
<% }) %>
</div>
<% } %>4. 多步骤流程数据暂存
// 订单创建流程
app.post('/checkout/step1', (req, res) => {
res.locals.orderData = {
items: req.body.items,
shippingType: 'express'
};
res.redirect('/checkout/step2');
});
app.get('/checkout/step2', (req, res) => {
// 直接使用上一步数据
const { items } = res.locals.orderData;
res.render('checkout-step2', { items });
});5. 动态配置注入
// 根据用户加载主题配置
app.use(async (req, res, next) => {
if (res.locals.user) {
res.locals.theme = await UserConfig.getTheme(req.user.id);
} else {
res.locals.theme = 'light'; // 默认主题
}
next();
});
// CSS模板
<style>
:root {
--primary-color: <%= theme.primaryColor %>;
}
</style>6. API响应增强
// 统一API响应格式
app.use('/api', (req, res, next) => {
res.apiSuccess = (data) => {
res.json({ success: true, data, ...res.locals.apiMeta });
};
res.locals.apiMeta = {
timestamp: Date.now(),
version: 'v2.1'
};
next();
});
// 路由中使用
app.get('/api/user', (req, res) => {
res.locals.apiMeta.requestId = generateId(); // 动态添加元数据
res.apiSuccess({ name: 'John' });
});
// 输出结果:
// {
// success: true,
// data: { name: "John" },
// timestamp: 1630000000,
// version: "v2.1",
// requestId: "req_123"
// }性能优化技巧
- 惰性加载:对耗时操作使用 getter 延迟执行
Object.defineProperty(res.locals, 'heavyData', {
get: () => computeExpensiveData(),
enumerable: true
});- 数据压缩:存储前精简数据结构
res.locals.userProfile = {
id: user.id,
name: user.name,
// 避免存储整个user对象
};- 缓存策略:复用相同请求的数据
function fetchGlobalStats() {
if (!res.locals.cachedStats) {
res.locals.cachedStats = db.query('SELECT ...');
}
return res.locals.cachedStats;
}常见陷阱与解决方案
问题1:异步数据未就绪
// 错误示例
app.use(async (req, res, next) => {
res.locals.data = await fetchData(); // 异步操作
next(); // 可能在没有完成时调用next
});
// 正确方案
app.use((req, res, next) => {
fetchData().then(data => {
res.locals.data = data;
next();
}).catch(next);
});问题2:中间件顺序错误
// 需要确保顺序
app.use(setUser); // 必须先设置用户
app.use(setTheme); // 依赖用户数据
function setTheme(req, res, next) {
// 安全访问已初始化的用户数据
if (res.locals.user) {
res.locals.theme = getTheme(res.locals.user.id);
}
next();
}问题3:命名冲突
// 使用命名空间避免冲突
res.locals.myLib = {
utils: require('./custom-utils'),
config: { /* ... */ }
};
// 模板中使用
<%= myLib.utils.formatDate(post.createdAt) %>高级模式:动态扩展
1. 类型增强(TS用户)
declare global {
namespace Express {
interface Locals {
user?: User;
isMobile: boolean;
trackingId: string;
}
}
}
// 使用时有类型提示
res.locals.trackingId = 'UA-123456';2. 响应时间注入
// 计算请求处理时间
app.use((req, res, next) => {
res.locals.startTime = process.hrtime();
res.on('finish', () => {
const [sec, nanosec] = process.hrtime(res.locals.startTime);
console.log(`处理时间: ${sec * 1000 + nanosec / 1e6}ms`);
});
next();
});3. 链式操作支持
// 扩展链式API
res.locals
.setFlash('success', '操作成功')
.trackEvent('purchase')
.setHeader('X-Custom', 'value');最佳实践总结
- 单一职责原则:每个中间件只设置特定数据
- 防御性编程:访问前检查
res.locals.user?.id - 数据脱敏:永远不存储敏感信息(密码、token)
- 性能监控:记录大对象的内存使用
- 清除机制:响应完成后手动清除敏感引用
res.on('finish', () => {
res.locals.creditCard = null; // GC友好
});通过合理运用 res.locals,可构建出高度可维护的 Express 应用,减少 40% 以上的重复数据传递代码,同时保持清晰的请求生命周期数据流。
一个对象,其包含了本次请求的响应中的变量和因此它的变量只提供给本次请求响应的周期内视图渲染里使用(如果有视图的话)。
其他方面,其和app.locals是一样的。
这个参数在导出请求级别的信息是很有效的,这些信息比如请求路径,已认证的用户,用户设置等等。
app.use(function(req, res, next) {
res.locals.user = req.user;
res.locals.authenticated = !req.user.anonymous;
next();
});Methods
res.append(field [, value])
res.append()方法在Expresxs4.11.0以上版本才支持。在指定的field的HTTP头部追加特殊的值value。如果这个头部没有被设置,那么将用value新建这个头部。value可以是一个字符串或者数组。
注意:在res.append()之后调用app.set()函数将重置前面设置的值。
res.append('Lind', ['<http://localhost>', '<http://localhost:3000>']);
res.append('Set-Cookie', 'foo=bar;Path=/;HttpOnly');
res.append('Warning', '199 Miscellaneous warning');res.append(field [, value]) 是 Express 中用于向响应头部追加值的方法(若头部不存在则创建),核心区别于 res.set(field, value)(覆盖已有头部值)。它在需要为同一个个响应头部设置多个值或分步骤动态添加值的场景中至关重要,以下是详细实战场景及用法:
一、核心特性回顾
- 追加而非覆盖:若头部个头部(如 Link、Set-Cookie)已存在值,res.append 会在原有基础上添加新值,而 res.set 会直接覆盖;
- 支持多值类型:value 可以是字符串(单个值)或数组(多个值);
- 头部自动标准化:字段名会自动转换为标准格式(如 link 转为 Link)。
二、实战核心场景(附代码示例)
- 处理支持多值的 HTTP 头部(如 Link、Vary)
部分 HTTP 头部天生支持多个值(用逗号分隔),例如 Link(用于关联资源)、Vary(用于缓存控制)。res.append 可便捷地为这些头部添加多个值,无需手动拼接字符串。
示例:为 API 响应添加多个关联资源链接(Link 头部)
Link 头部常用于 RESTful API 中指示分页链接(上一页、下一页),需要包含多个
app.get('/api/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const totalPages = 5; // 假设总页数为5
// 1. 追加“下一页”链接(若当前不是最后一页)
if (page < totalPages) {
res.append('Link', `<http://example.com/api/posts?page=${page+1}>; rel="next"`);
}
// 2. 追加“最后一页”链接
res.append('Link', `<http://example.com/api/posts?page=${totalPages}>; rel="last"`);
// 3. 响应数据
res.json({
data: [{ id: 1, title: '文章1' }],
page,
totalPages
});
});响应头部效果:
Link: <http://example.com/api/posts?page=2>; rel="next", <http://example.com/api/posts?page=5>; rel="last"价值:无需手动拼接逗号分隔的字符串,分步添加更清晰,避免格式错误(如遗漏逗号)。
- 设置多个 Cookie(Set-Cookie 头部)
Set-Cookie 头部是特殊的多值头部:每个 Cookie 必须作为单独的值(不能用逗号拼接),浏览器会按顺序解析所有 Cookie。res.append 是设置多个 Cookie 的最佳方式(res.set 会覆盖之前的 Cookie)。
示例:一次响应设置多个 Cookie
app.post('/login', (req, res) => {
const userId = '1001';
const token = 'xxx-xxx-xxx';
// 1. 追加“用户ID”Cookie(HttpOnly 防止JS读取)
res.append('Set-Cookie', `userId=${userId}; Path=/; HttpOnly; Max-Age=86400`);
// 2. 追加“认证token”Cookie(Secure 仅HTTPS传输)
res.append('Set-Cookie', `token=${token}; Path=/; Secure; Max-Age=86400`);
// 3. 追加“主题偏好”Cookie
res.append('Set-Cookie', `theme=dark; Path=/; Max-Age=31536000`);
res.json({ success: true, msg: '登录成功' });
});响应头部效果:
Set-Cookie: userId=1001; Path=/; HttpOnly; Max-Age=86400
Set-Cookie: token=xxx-xxx-xxx; Path=/; Secure; Max-Age=86400
Set-Cookie: theme=dark; Path=/; Max-Age=31536000为什么不用 res.cookie?
res.cookie 内部也是通过 res.append('Set-Cookie', ...) 实现的,直接用 res.append 更灵活(可自定义复杂 Cookie 选项)。
- 跨域资源共享(CORS)的多值配置
CORS 相关头部(如 Access-Control-Allow-Methods、Access-Control-Allow-Headers)常需要支持多个值(如允许 GET、POST、PUT 方法)。res.append 可分步骤添加这些值,尤其适合在中间件中动态配置。
示例:CORS 中间件中追加允许的方法和头部
// CORS 基础配置中间件
app.use((req, res, next) => {
// 允许的源(假设已处理)
res.set('Access-Control-Allow-Origin', '*');
// 1. 追加允许的 HTTP 方法(基础方法)
res.append('Access-Control-Allow-Methods', ['GET', 'POST']);
next();
});
// 针对特定路由追加更多方法
app.use('/api/admin', (req, res, next) => {
// 2. 为管理员路由追加 PUT、DELETE 方法
res.append('Access-Control-Allow-Methods', ['PUT', 'DELETE']);
next();
});
// 最终响应头部(针对 /api/admin 路由):
// Access-Control-Allow-Methods: GET,POST,PUT,DELETE价值:不同中间件可按需追加 CORS 配置,避免在一个地方硬编码所有值,适合模块化开发(如普通路由和管理员路由允许的方法不同)。
- 缓存控制:动态追加 Cache-Control 指令
Cache-Control 头部支持多个缓存指令(如 public、max-age=3600、must-revalidate),res.append 可分步骤添加这些指令(注意:Cache-Control 的多个值用逗号分隔,res.append 会自动处理)。
示例:根据环境动态追加缓存指令
app.get('/static/image.jpg', (req, res) => {
// 基础缓存指令:公开缓存,有效期1小时
res.append('Cache-Control', 'public, max-age=3600');
// 生产环境追加“必须重新验证”指令
if (process.env.NODE_ENV === 'production') {
res.append('Cache-Control', 'must-revalidate');
}
// 发送图片
res.sendFile('/path/to/image.jpg');
});响应头部效果(生产环境):
Cache-Control: public, max-age=3600, must-revalidate- 中间件协作:分步设置同一头部
在多中间件处理流程中,不同中间件可能需要为同一头部添加不同值(如日志中间件添加 X-Log-ID,权限中间件添加 X-Permission)。res.append 确保这些值不会相互覆盖。
示例:多中间件协作设置 X-Extra-Info 头部
// 日志中间件:添加请求ID
app.use((req, res, next) => {
const logId = `log-${Date.now()}`;
res.append('X-Extra-Info', `logId=${logId}`);
next();
});
// 权限中间件:添加用户权限
app.use((req, res, next) => {
const permission = req.user?.role || 'guest';
res.append('X-Extra-Info', `permission=${permission}` );
next();
});
// 路由响应
app.get('/info', (req, res) => {
res.send('信息页');
});响应头部效果:
X-Extra-Info: logId=log-1620000000000, permission=admin若用 res.set 会怎样?
后执行的中间件会覆盖前一个的值,最终只能保留 permission=admin,丢失 logId。
三、关键注意事项
- 与 res.set 的区别:
- res.set(field, value):覆盖该头部的所有值(若已存在);
- res.append(field, value):在原有值基础上追加(若不存在则创建)。
例如:res.set('Link', 'a'); res.append('Link', 'b') → 最终 Link: a,b;
而 res.set('Link', 'a'); res.set('Link', 'b') → 最终 Link: b。
- res.append 后调用 res.set 会覆盖:
若先 res.append('Link', 'a'),再 res.set('Link', 'b'),最终头部为 Link: b(set 覆盖了之前的 append)。
- 并非所有头部都支持多值:
部分头部(如 Content-Type、Content-Length)仅允许单个值,res.append 对这些头部的效果与 res.set 一致(覆盖),需避免误用。
- 数组参数的便捷性:
传递数组 res.append('Link', ['a', 'b']) 等价于分两次调用 res.append('Link', 'a') 和 res.append('Link', 'b'),推荐用数组简化代码。
四、总结:res.append 的核心价值
它是处理多值响应头部或动态分步设置头部的核心工具,解决了 res.set 无法追加值的问题。实战中最常用于:
- 设置多个 Cookie(Set-Cookie);
- 配置 CORS 多方法 / 多头部;
- 构建 Link 等关联资源头部;
- 多中间件协作设置同一头部。
掌握 res.append 能让响应头部的配置更灵活、模块化,尤其适合复杂场景下的头部管理。
res.attachment([filename])
设置HTTP响应的Content-Disposition头内容为"attachment"。如果提供了filename,那么将通过res.type()获得扩展名来设置Content-Type,并且设置Content-Disposition内容为"filename="parameter。
res.attachment();
// Content-Disposition: attachment
res.attachment('path/to/logo.png');
// Content-Disposition: attachment; filename="logo.png"
// Content-Type: image/png一、先搞懂:res.attachment 到底是干嘛的?
核心功能:让浏览器把服务器返回的内容当成 “需要下载的文件”,而不是直接在页面上显示。
比如:
- 服务器返回一张图片,浏览器默认会直接显示;用了 res.attachment 后,浏览器会弹出 “保存文件” 的对话框;
- 服务器返回一段文本,浏览器默认会直接打印在页面上;用了 res.attachment 后,会变成下载一个 .txt 文件。
它的本质是通过设置 Content-Disposition: attachment 这个响应头,告诉浏览器 “这是个要下载的附件”—— 这是 HTTP 协议约定的 “下载触发信号”。
二、实战核心场景(附代码示例)
1. 基础场景:触发固定文件下载(如图片、文档)
开发中最常见的 “点击下载按钮下载文件” 需求,比如下载官网 logo、用户上传的附件、帮助文档等,用 res.attachment 可快速配置下载行为。
示例:用户点击 “下载 logo” 按钮,获取服务器的图片文件
// 路由:处理“下载logo”请求
app.get('/download/logo', (req, res) => {
// 1. 调用 res.attachment,传入文件名(含后缀)
// 作用:
// - 设置 Content-Disposition: attachment; filename="logo.png"(告诉浏览器下载,文件名是logo.png)
// - 自动根据后缀 .png 设置 Content-Type: image/png(告诉浏览器这是图片类型)
res.attachment('logo.png');
// 2. 发送文件内容(必须配合发送内容,否则下载的文件是空的)
// __dirname 是当前文件所在目录,这里假设 logo.png 在 public 文件夹下
const logoPath = `${__dirname}/public/logo.png`;
res.sendFile(logoPath); // 把图片文件内容返回给浏览器
});效果:用户访问 http://你的域名/download/logo 时,浏览器会弹出 “保存文件” 对话框,默认文件名是 logo.png,保存后打开就是这张图片。
2. 进阶场景:动态生成文件并下载(如导出 Excel、CSV)
开发中常需要 “导出数据” 功能(比如导出用户列表、订单报表),这些文件不是服务器提前存好的,而是动态生成的 ——res.attachment 能让动态内容直接以文件形式下载。
示例:导出用户数据为 CSV 文件(用文本模拟动态内容)
// 路由:处理“导出用户列表”请求
app.get('/export/users', (req, res) => {
// 1. 动态生成 CSV 内容(实际开发中可能从数据库查询数据后拼接)
// CSV 格式:每行是一条数据,逗号分隔字段
const userCsv = `用户ID,用户名,手机号\n1,张三,13800138000\n2,李四,13900139000`;
// 2. 调用 res.attachment,设置下载的文件名(含 CSV 后缀)
// 自动设置 Content-Type: text/csv(浏览器知道这是CSV文件)
res.attachment(`用户列表_${new Date().toLocaleDateString()}.csv`);
// 文件名加日期,避免用户多次下载覆盖(如“用户列表_2025-10-25.csv”)
// 3. 发送动态生成的 CSV 内容
res.send(userCsv);
});效果:用户点击 “导出用户列表” 后,浏览器下载一个以当天日期命名的 CSV 文件,打开后是结构化的用户数据,可直接用 Excel 打开。
3. 特殊场景:强制下载浏览器默认会 “直接打开” 的文件
有些文件类型(如 PDF、TXT、HTML),浏览器默认会直接在页面上打开(而不是下载),但业务需要 “必须下载”(比如用户上传的 PDF 合同,需要下载保存)——res.attachment 能强制改变这个行为。
示例:强制下载 PDF 合同,避免浏览器直接打开
app.get('/download/contract/:id', (req, res) => {
const contractId = req.params.id; // 合同ID
const contractPath = `${__dirname}/contracts/${contractId}.pdf`; // PDF 文件路径
// 关键:调用 res.attachment,即使是 PDF,浏览器也会触发下载
res.attachment(`${contractId}_用户合同.pdf`);
// 发送 PDF 文件内容
res.sendFile(contractPath, (err) => {
if (err) {
res.status(404).send('合同文件不存在');
}
});
});对比效果:
- 不调用 res.attachment:浏览器打开 http://.../download/contract/123 会直接显示 PDF 内容;
- 调用后:浏览器会弹出 “保存 PDF 文件” 对话框,强制下载。
4. 灵活场景:只触发下载行为,不指定固定文件名
如果下载的内容是动态的(比如用户自定义的文本),没有固定文件名,可只调用 res.attachment()(不传参数),仅触发下载行为,让浏览器用默认名(如 “download”)。
示例:下载用户输入的自定义文本
app.post('/download/custom-text', express.text(), (req, res) => {
const userText = req.body; // 用户提交的自定义文本(通过 express.text() 解析)
// 只调用 res.attachment(),不指定文件名
// 此时响应头是 Content-Disposition: attachment(仅告诉浏览器下载,不指定文件名)
res.attachment();
// 发送用户的文本内容,浏览器会默认下载为“download.txt”(不同浏览器可能有差异)
res.send(userText);
});三、实战关键细节(避坑必看)
- res.attachment 只是 “配置下载头”,必须配合 “发送内容”
它只负责设置响应头,不会自动发送文件内容 —— 如果只调用 res.attachment('logo.png') 而不调用 res.sendFile/res.send,下载的文件会是空的!
- 传 filename 时,会自动帮你做两件事
- 设 Content-Disposition: attachment; filename="xxx":控制下载的默认文件名;
- 设 Content-Type:根据文件名后缀自动匹配 MIME 类型(如 .xlsx 对应 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,.json 对应 application/json),不用手动调用 res.set('Content-Type', ...)。
- 和 res.download 的区别(别搞混)
- res.download(path[, filename]):是 “一键下载”,内部会先调用 res.attachment(filename),再自动发送指定路径的文件(相当于 res.attachment + res.sendFile 的组合);
- res.attachment([filename]):更灵活,适合 “动态生成内容”(如前面的 CSV 示例),或需要自定义文件内容发送逻辑的场景。
简单说:固定文件用 res.download 更简洁,动态内容用 res.attachment + res.send 更灵活。
- 文件名含中文或特殊字符?需要编码
如果文件名有中文(如 “用户合同.pdf”),部分旧浏览器可能显示乱码,需手动编码:
const filename = encodeURIComponent('用户合同.pdf'); // 编码中文
res.attachment(filename);
// 或手动设置响应头(更兼容)
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${filename}`);四、总结:res.attachment 的核心价值
它是实现 “文件下载功能” 的核心工具,实战中主要解决:
- 让浏览器 “下载而非显示” 内容(覆盖浏览器默认行为);
- 统一控制下载文件名和文件类型(自动匹配 MIME 类型);
- 灵活配合动态生成的内容(如导出报表)或固定文件(如附件),满足各种下载需求。
只要开发中涉及 “用户下载文件” 的场景,基本都离不开它的配置。
res.cookie(name, value [,options])
设置name和value的cookie,value参数可以是一串字符或者是转化为json字符串的对象。
options是一个对象,其可以有下列的属性。
| 属性 | 类型 | 描述 |
|---|---|---|
| domain | String | 设置cookie的域名。默认是你本app的域名。 |
| expires | Date | cookie的过期时间,GMT格式。如果没有指定或者设置为0,则产生新的cookie。 |
| httpOnly | Boolean | 这个cookie只能被web服务器获取的标示。 |
| maxAge | String | 是设置过去时间的方便选项,其为过期时间到当前时间的毫秒值。 |
| path | String | cookie的路径。默认值是/。 |
| secure | Boolean | 标示这个cookie只用被HTTPS协议使用。 |
| signed | Boolean | 指示这个cookie应该是签名的。 |
res.cookie()所作的都是基于提供的options参数来设置Set-Cookie头部。没有指定任何的options,那么默认值在RFC6265中指定。
使用实例:
res.cookie('name', 'tobi', {'domain':'.example.com', 'path':'/admin', 'secure':true});
res.cookie('remenberme', '1', {'expires':new Date(Date.now() + 90000), 'httpOnly':true});maxAge是一个方便设置过期时间的方便的选项,其以当前时间开始的毫秒数来计算。下面的示例和上面的第二条功效一样。
res.cookie('rememberme', '1', {'maxAge':90000}, "httpOnly":true);你可以设置传递一个对象作为value的参数。然后其将被序列化为Json字符串,被bodyParser()中间件解析。
res.cookie('cart', {'items':[1, 2, 3]});
res.cookie('cart', {'items':[1, 2, 3]}, {'maxAge':90000});当我们使用cookie-parser中间件的时候,这个方法也支持签名的cookie。简单地,在设置options时包含signed选项为true。然后res.cookie()将使用传递给cookieParser(secret)的密钥来签名这个值。
res.cookie('name', 'tobi', {'signed':true});一、先搞懂:res.cookie 到底是干嘛的?
核心功能:在客户端浏览器中 “存一小段数据”(这小段数据就是 Cookie),后续客户端再发请求时,会自动把这些 Cookie 带给服务器,实现 “状态保持”。
比如:用户登录后,服务器用 res.cookie 存一个 userId=1001 到浏览器,下次用户访问时,服务器就能通过 Cookie 知道 “这是 1001 号用户”,不用重新登录。
本质是通过设置响应头 Set-Cookie 实现的 —— 服务器在响应里加一句 Set-Cookie: name=tobi,浏览器就会把 name=tobi 存起来。
二、实战核心作用(附场景代码)
1. 用户会话管理:记住登录状态(最高频场景)
用户登录后,用 Cookie 存 “身份标识”(如用户 ID、临时 token),避免每次请求都重新登录。常结合 httpOnly、maxAge 等参数保证安全和有效期。
示例:登录后设置 “记住我” Cookie
app.post('/login', (req, res) => {
const { username, password, rememberMe } = req.body;
// 模拟数据库验证:假设用户名密码正确,获取用户ID
const userId = 1001;
// 配置Cookie参数
const cookieOpts = {
httpOnly: true, // 关键:前端JS无法读取这个Cookie(防XSS攻击,避免Cookie被偷)
path: '/', // 全站都能访问这个Cookie
signed: true // 签名Cookie(防篡改,需配合cookie-parser中间件)
};
// 如果勾选“记住我”,设置7天有效期;否则会话结束后失效(关闭浏览器就没)
if (rememberMe) {
cookieOpts.maxAge = 7 * 24 * 60 * 60 * 1000; // 7天(毫秒数)
}
// 1. 设置签名Cookie:存用户ID(后续用req.signedCookies.userId获取)
res.cookie('userId', userId, cookieOpts);
// 2. 响应登录成功
res.json({ success: true, msg: '登录成功' });
});作用:用户下次访问时,服务器通过 req.signedCookies.userId 拿到用户 ID,直接识别身份,不用重新登录。
2. 存储客户端配置:记住用户偏好
存用户的个性化设置(如主题、语言、字体大小),下次用户访问时,服务器根据 Cookie 返回对应配置,提升体验。
示例:保存用户主题偏好
// 用户切换主题时调用的接口
app.post('/set-theme', (req, res) => {
const { theme } = req.body; // 前端传的主题:'dark'(深色)或 'light'(浅色)
// 设置Cookie:存主题,有效期1年,仅当前域名可用
res.cookie(
'userTheme', // Cookie名
theme, // Cookie值
{
maxAge: 365 * 24 * 60 * 60 * 1000, // 1年有效期
path: '/',
domain: '.example.com' // 子域名也能共享(如 blog.example.com 和 shop.example.com 都能用)
}
);
res.json({ success: true, msg: '主题已保存' });
});
// 首页接口:根据Cookie返回对应主题
app.get('/', (req, res) => {
// 从Cookie获取主题,默认浅色
const userTheme = req.cookies.userTheme || 'light';
// 渲染页面时传入主题
res.render('index', { theme: userTheme });
});作用:用户下次访问时,页面会自动加载之前保存的主题,不用重新切换。
3. 跟踪临时状态:存储短期数据
存短期有效的临时数据(如购物车临时商品、表单填写进度、验证码时效),适合不需要长期保存的场景。
示例:存储购物车临时商品(未登录用户)
// 添加商品到购物车(未登录用户)
app.post('/cart/add', (req, res) => {
const { productId, count } = req.body;
// 1. 先获取已有购物车Cookie(如果没有,初始化为空数组)
const cart = req.cookies.cart ? JSON.parse(req.cookies.cart) : [];
// 2. 新增/更新商品(这里简化逻辑)
const existingItem = cart.find(item => item.id === productId);
if (existingItem) {
existingItem.count += count; // 已有商品,增加数量
} else {
cart.push({ id: productId, count }); // 新商品,加入购物车
}
// 3. 重新设置Cookie:存JSON字符串(Cookie值只能是字符串),有效期1天
res.cookie(
'cart',
JSON.stringify(cart), // 把数组转成字符串
{ maxAge: 24 * 60 * 60 * 1000, path: '/shop' } // 仅购物相关页面生效
);
res.json({ success: true, cart });
});作用:未登录用户添加的商品会存在 Cookie 里,刷新页面或重新访问时,购物车数据不会丢失。
4. 安全相关:配合参数防攻击
通过 secure、httpOnly、signed 等参数,让 Cookie 更安全,避免被篡改或窃取(这部分是实战中的 “安全必选项”)。
示例:设置安全的认证 Cookie
app.post('/auth', (req, res) => {
const authToken = 'xxx-xxx-xxx'; // 生成的认证token
res.cookie(
'authToken',
authToken,
{
httpOnly: true, // 防XSS:前端JS拿不到,避免被脚本窃取
secure: process.env.NODE_ENV === 'production', // 仅HTTPS环境下生效(防中间人窃取)
signed: true, // 防篡改:用cookie-parser密钥签名,篡改后服务器能识别
sameSite: 'strict' // 防CSRF:仅同域请求带这个Cookie,避免跨站伪造请求
}
);
res.json({ success: true });
});作用:最大程度降低 Cookie 被攻击的风险,保护用户认证信息。
三、实战避坑注意事项(重中之重)
1. 安全相关:这 3 个参数必须重视
- httpOnly: true 必加:前端无法通过 document.cookie 读取 Cookie,能有效防止 XSS 攻击(比如黑客注入脚本偷 Cookie)。
✅ 正确:存用户 ID、token 等敏感信息时,必须加 httpOnly: true;
❌ 错误:存主题、语言等非敏感信息,可不加,但敏感信息绝不能不加。
- secure: true 生产环境必加:仅允许 HTTPS 协议传输 Cookie,避免 HTTP 协议下 Cookie 被中间人窃取。
注意:开发环境(localhost)用 HTTP,可设 secure: false;生产环境必须设 secure: true。
- signed: true 防篡改:需要先初始化 cookie-parser(secret)(传密钥),设置后 Cookie 会被签名,篡改后 req.signedCookies 中会取不到值(返回 undefined)。
比如:设置 res.cookie('userId', 1001, { signed: true }),浏览器存的 Cookie 是 userId=1001.signedValue,篡改后服务器识别为无效。
2. 过期时间:expires 和 maxAge 别搞混
- maxAge:用 “当前时间到过期的毫秒数” 设置(如 72460601000 是 7 天),更直观,推荐用;
- expires:用 GMT 格式的日期对象设置(如 new Date(Date.now() + 72460601000)),和 maxAge 效果一样,但容易写错格式;
- 注意:不设置过期时间的话,Cookie 是 “会话 Cookie”—— 关闭浏览器就消失,适合临时状态(如未勾选 “记住我” 的登录)。
3. 路径和域名:控制 Cookie 的生效范围
- path:指定哪些路径的请求会带这个 Cookie(默认 /,全站生效)。
比如:path: '/shop' 表示只有访问 /shop 开头的路径(如 /shop/cart)才带这个 Cookie,其他路径(如 /user)不带,减少冗余。
- domain:指定哪些域名能访问这个 Cookie(默认当前域名,如 example.com)。
比如:domain: '.example.com'(注意前面的点),表示 example.com 和它的子域名(如 blog.example.com、shop.example.com)都能共享这个 Cookie;
❌ 错误:不能设为其他域名(如 baidu.com),浏览器会拒绝,因为 Cookie 有 “同源策略”。
4. 数据限制:Cookie 不是 “无限仓库”
- 大小限制:每个 Cookie 最大约 4KB,超过会被浏览器截断,无法存储;
❌ 错误:别用 Cookie 存大量数据(如完整的用户信息、大列表),适合存小数据(ID、token、配置项)。
- 数量限制:每个域名下的 Cookie 数量约 20-50 个(不同浏览器有差异),太多会影响请求速度(每次请求都要带所有 Cookie)。
5. 数据格式:Cookie 值只能是字符串
- 如果要存对象 / 数组(如购物车数组),必须先转成 JSON 字符串(JSON.stringify()),取的时候再转回来(JSON.parse());
示例:res.cookie('cart', JSON.stringify([1,2,3])),取的时候 const cart = JSON.parse(req.cookies.cart)。
- 中文或特殊字符(如空格、逗号)必须编码:用 encodeURIComponent() 处理,否则会被浏览器自动截断或乱码;
示例:res.cookie('username', encodeURIComponent('张三')),取的时候 decodeURIComponent(req.cookies.username)。
6. 敏感信息:绝不能存 Cookie
- Cookie 是明文存储的(即使签名,内容也能被看到),绝不能存密码、银行卡号等敏感信息;
✅ 正确:存非敏感的标识(如用户 ID、临时 token),敏感信息要存在服务器(如数据库、Redis)。
7. 依赖中间件:签名 Cookie 需 cookie-parser
- 如果用 signed: true,必须在 Express 中初始化 cookie-parser 并传密钥,否则签名无效,req.signedCookies 取不到值;
正确配置:
const cookieParser = require('cookie-parser');
app.use(cookieParser('your-strong-secret-key')); // 密钥要复杂,用环境变量存四、总结:res.cookie 的核心价值
它是实现 “客户端与服务器状态保持” 的核心工具,实战中主要解决:
- 用户登录状态记住(避免重复登录);
- 客户端个性化配置存储(主题、语言);
- 短期临时数据跟踪(购物车、表单进度);
- 配合安全参数(httpOnly、secure、signed)保障数据安全。
只要开发中需要 “跨请求保存客户端状态”,基本都离不开 res.cookie,但一定要注意安全和数据限制,避免踩坑。
res.clearCookie(name [,options])
根据指定的name清除对应的cookie。更多关于options对象可以查阅res.cookie()。
res.cookie('name', 'tobi', {'path':'/admin'});
res.clearCookie('name', {'path':'admin'});一、先搞懂:res.clearCookie(name [, options]) 是什么?
核心功能
清除客户端浏览器中指定名称的 Cookie,是 res.cookie 的 “反向操作”。
比如:用户登录时用 res.cookie 存了 userId=1001,登出时就用 res.clearCookie('userId') 删掉这个 Cookie,让客户端失去登录状态。
核心逻辑(关键!)
浏览器通过 “Cookie 名称 + path + domain” 唯一标识一个 Cookie—— 清除时必须和设置时的 path、domain 完全一致,否则浏览器找不到对应的 Cookie,清除会失败!
(比如:设置时 path: '/admin',清除时没带 path,浏览器会默认按当前路径找,导致清不掉)
二、实战场景:res.cookie 与 res.clearCookie 配合使用
场景 1:用户登出 —— 清除登录状态 Cookie(高频)
登录时用 res.cookie 存身份标识(如 userId),登出时必须用 res.clearCookie 清除,避免用户退出后仍保留登录状态。
// 1. 登录:设置登录 Cookie(带 path 和 domain)
app.post('/login', (req, res) => {
const userId = 1001;
res.cookie('userId', userId, {
path: '/', // 全站生效
domain: '.example.com', // 子域名共享(如 blog.example.com)
httpOnly: true, // 防XSS
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天有效期
});
res.json({ success: true, msg: '登录成功' });
});
// 2. 登出:清除登录 Cookie(必须匹配 path 和 domain)
app.post('/logout', (req, res) => {
res.clearCookie('userId', {
path: '/', // 和设置时一致!否则清不掉
domain: '.example.com' // 和设置时一致!
});
res.json({ success: true, msg: '登出成功,已清除登录状态' });
});场景 2:切换用户 / 配置 —— 清除旧的临时数据 Cookie
用户切换账号、更新配置(如主题、语言)时,需清除旧的 Cookie,避免新旧数据冲突。
// 1. 设置主题 Cookie
app.post('/set-theme', (req, res) => {
const { theme } = req.body;
res.cookie('userTheme', theme, {
path: '/',
maxAge: 30 * 24 * 60 * 60 * 1000 // 30天有效期
});
res.json({ success: true, msg: '主题已保存' });
});
// 2. 切换账号:清除旧主题 Cookie(避免新用户用旧主题)
app.post('/switch-account', (req, res) => {
// 清除旧主题 Cookie
res.clearCookie('userTheme', { path: '/' });
// 清除旧登录 Cookie(后续会设置新账号的 Cookie)
res.clearCookie('userId', { path: '/', domain: '.example.com' });
res.json({ success: true, msg: '请重新登录新账号' });
});场景 3:Cookie 失效 / 过期 —— 主动清理无效 Cookie
若 Cookie 因配置变更(如域名修改)或过期逻辑调整,需主动清除客户端残留的无效 Cookie,避免干扰新逻辑。
// 中间件:检测并清理旧域名的无效 Cookie
app.use((req, res, next) => {
// 旧域名是 old.example.com,新域名是 example.com,需清除旧域名的 Cookie
if (req.get('host').includes('example.com')) {
res.clearCookie('userId', {
path: '/',
domain: '.old.example.com' // 匹配旧域名的 Cookie
});
}
next();
});三、关键注意事项(避坑必看)
1. 清除时必须匹配 path 和 domain(最容易踩的坑!)
- 若设置时指定了 path: '/admin',清除时必须带 path: '/admin'(默认 path: '/' 不匹配);
- 若设置时指定了 domain: '.example.com',清除时必须带相同 domain(默认当前域名不匹配);
- 反例:
res.cookie('test', '123', { path: '/admin' }); // 设置时 path 是 /admin
res.clearCookie('test'); // 错误:没带 path,浏览器按当前路径找,清不掉
res.clearCookie('test', { path: '/admin' }); // 正确:匹配 path,清除成功2. 无需匹配 httpOnly、secure、signed 等参数
清除时只需要匹配 “标识 Cookie 的关键参数”(name、path、domain),无需管 httpOnly、secure 等安全参数 —— 即使设置时带了这些,清除时不带也能成功。
// 设置时带 httpOnly 和 secure
res.cookie('authToken', 'xxx', {
path: '/',
httpOnly: true,
secure: true
});
// 清除时无需带 httpOnly 和 secure,只需匹配 path
res.clearCookie('authToken', { path: '/' }); // 成功清除3. 清除后必须发送响应,否则客户端收不到指令
res.clearCookie 本质是设置 Set-Cookie: name=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/ 响应头,必须通过 res.json、res.send 等方法发送响应,客户端才能收到并执行清除操作。
// 错误:只清除不发响应,客户端不会执行
app.post('/logout', (req, res) => {
res.clearCookie('userId', { path: '/' });
// 缺少 res.json / res.send,客户端收不到清除指令
});
// 正确:清除后发响应
app.post('/logout', (req, res) => {
res.clearCookie('userId', { path: '/' });
res.json({ success: true }); // 必须发送响应
});4. 无法清除前端设置的非 httpOnly Cookie
- 若 Cookie 是前端通过 document.cookie = 'name=123' 设置的(非 httpOnly),服务器用 res.clearCookie 也能清除(只要匹配 path 和 domain);
- 若 Cookie 是服务器设置的 httpOnly: true,前端无法清除,只能通过服务器 res.clearCookie 清除(保障安全)。
四、总结:两者的核心配合价值
| 方法 | 作用 | 关键关联点 |
|---|---|---|
| res.cookie | 给客户端存 Cookie(设状态) | 需指定 path、domain 等标识参数 |
| res.clearCookie | 给客户端删 Cookie(清状态) | 必须匹配 res.cookie 的标识参数 |
两者是 “设置 - 清除” 的闭环,主要用于:
- 用户登录 / 登出的状态管理;
- 客户端配置的保存与更新;
- 无效 Cookie 的清理与维护。
核心原则:清除时的 path 和 domain 必须和设置时完全一致,否则清除会失效!
res.download(path, [,filename], [,fn])
传输path指定文件作为一个附件。通常,浏览器提示用户下载。默认情况下,Content-Disposition头部"filename="的参数为path(通常会出现在浏览器的对话框中)。通过指定filename参数来覆盖默认值。
当一个错误发生时或者传输完成,这个方法将调用fn指定的回调方法。这个方法使用res.sendFile()来传输文件。
res.download('/report-12345.pdf');
res.download('/report-12345.pdf', 'report.pdf');
res.download('report-12345.pdf', 'report.pdf', function(err) {
// Handle error, but keep in mind the response may be partially-sent
// 处理错误,但请记住响应可能已部分发送// 因此需要检查 res.headersSen
// so check res.headersSent
if (err) {
} else {
// decrement a download credit, etc.
减少一个下载额度,等等。
}
});一、核心功能
一键实现 “服务器文件下载”:传入文件在服务器的路径,自动完成「设置下载响应头 + 读取并发送文件内容」的全流程,浏览器会触发 “保存文件” 对话框(而非直接显示文件)。
本质是 res.attachment(设下载头)和 res.sendFile(发文件)的 “组合封装”—— 比单独用这两个方法更简洁,不用手动拼接逻辑。
与 res.attachment 的关键区别(避免混淆)
| 方法 | 核心逻辑 | 适用场景 |
|---|---|---|
| res.attachment | 仅设置 Content-Disposition: attachment 头(告诉浏览器 “要下载”),需手动发送文件内容 | 动态生成内容下载(如导出 CSV) |
| res.download | 既设置下载头,又自动读取服务器文件并发送内容(内部调用 res.sendFile) | 服务器已存在的固定文件下载(如报表、模板) |
二、开发核心作用
场景 1:基础固定文件下载(高频)
开发中最常见的 “下载服务器预存文件” 需求,比如用户下载报表、模板文件、帮助文档等,用 res.download 只需传文件路径,无需额外配置下载头。
示例:用户下载月度销售报表
// 路由:下载2025年10月的销售报表(报表已存在服务器指定路径)
app.get('/download/sales-report', (req, res) => {
// 1. 服务器上文件的绝对路径(推荐用绝对路径,避免相对路径混乱)
// __dirname 是当前路由文件所在目录,报表存在 ./public/reports 下
const reportPath = `${__dirname}/public/reports/202510_sales.pdf`;
// 2. 调用 res.download 触发下载
// 效果:浏览器弹出“保存文件”对话框,默认文件名是“202510_sales.pdf”
res.download(reportPath);
});作用:无需手动设置 Content-Disposition 头和读取文件,一行代码实现文件下载,简化基础下载逻辑。
场景 2:自定义下载文件名(优化用户体验)
服务器文件的原始名可能复杂(如 202510_sales_12345.pdf),用户下载时希望显示简洁名称(如 10月销售报表.pdf),可通过 filename 参数自定义。
示例:自定义报表下载文件名
app.get('/download/sales-report', (req, res) => {
const reportPath = `${__dirname}/public/reports/202510_sales_12345.pdf`;
// 第二个参数:自定义下载时的文件名(浏览器显示的名称)
res.download(
reportPath,
'2025年10月销售报表.pdf', // 用户看到的文件名
(err) => { // 第三个参数:下载完成/出错的回调
if (err) {
// 下载出错处理(如文件不存在)
res.status(404).json({ success: false, msg: '报表文件不存在' });
}
}
);
});效果:服务器文件名为 202510_sales_12345.pdf,用户下载时显示 2025年10月销售报表.pdf,更易识别。
场景 3:下载后回调处理(业务拓展)
下载完成或出错后,需要执行额外逻辑(如记录下载日志、扣减用户下载次数、统计下载量),可通过第三个 fn 参数实现回调。
示例:记录下载日志 + 扣减下载次数
// 假设用户有“每月3次报表下载权限”,存在数据库中
app.get('/download/sales-report', async (req, res) => {
const userId = req.signedCookies.userId; // 从登录Cookie获取用户ID(之前学的签名Cookie)
const reportPath = `${__dirname}/public/reports/202510_sales.pdf`;
// 1. 先校验用户下载权限(假设从数据库查剩余次数)
const user = await User.findById(userId);
if (user.downloadQuota <= 0) {
return res.status(403).json({ success: false, msg: '下载次数已用完' });
}
// 2. 下载文件+回调处理
res.download(
reportPath,
'10月销售报表.pdf',
async (err) => {
if (err) {
console.error('下载出错:', err);
return res.status(500).json({ success: false, msg: '下载失败' });
}
// 3. 下载成功:扣减次数+记录日志(核心回调逻辑)
user.downloadQuota -= 1;
await user.save();
// 记录下载日志到数据库
await DownloadLog.create({
userId,
fileName: '10月销售报表.pdf',
downloadTime: new Date()
});
// 4. 告知用户剩余次数
res.json({ success: true, msg: '下载成功', remainingQuota: user.downloadQuota });
}
);
});作用:将 “文件下载” 与 “业务逻辑” 绑定,下载完成后自动执行权限更新、日志记录,无需额外接口。
场景 4:结合权限控制的私密文件下载
某些文件(如用户个人账单、企业内部文档)需登录后才能下载,可先通过 req.signedCookies 验证登录状态,再执行 res.download。
示例:登录后才能下载个人账单
// 1. 权限中间件:验证是否登录
const requireLogin = (req, res, next) => {
if (!req.signedCookies.userId) {
return res.status(401).json({ success: false, msg: '请先登录' });
}
next();
};
// 2. 个人账单下载接口(需登录)
app.get('/download/my-bill', requireLogin, (req, res) => {
const userId = req.signedCookies.userId;
// 账单文件按用户ID分目录存储(如 ./bills/1001/202510.pdf)
const billPath = `${__dirname}/bills/${userId}/202510.pdf`;
res.download(billPath, `我的2025年10月账单.pdf`, (err) => {
if (err) {
res.status(404).json({ success: false, msg: '账单文件不存在' });
}
});
});作用:确保私密文件仅授权用户可下载,结合之前学的签名 Cookie 实现身份验证,形成 “登录 - 授权 - 下载” 的完整流程。
三、开发关键注意事项(避坑必看)
1. 必须用 “服务器可访问的文件路径”
- 路径可以是绝对路径(推荐,如 __dirname + '/public/report.pdf')或相对路径(不推荐,易受当前执行目录影响);
- 若文件不存在,回调函数的 err 会有值,必须处理(否则浏览器会一直等待,最终报 500 错误)。
2. 大文件下载无需手动处理分片
res.download 内部基于 res.sendFile,会自动处理大文件的 “流式传输”(分片发送,不占用大量内存),无需手动用 fs.createReadStream 实现。
3. 回调函数中处理错误需判断 res.headersSent
若下载过程中出错(如文件读取到一半失败),可能已部分发送响应,需用 res.headersSent 判断是否已发送头信息,避免重复发送响应:
res.download(reportPath, (err) => {
if (err) {
// 若响应未发送,返回错误;已发送则只打日志
if (!res.headersSent) {
res.status(500).send('下载失败');
} else {
console.error('下载中途出错:', err);
}
}
});4. 避免下载 “非公开文件” 到客户端
- 仅用于下载 “允许用户获取的文件”(如报表、模板),禁止用 res.download 传输服务器配置文件(如 config.js)、数据库文件等敏感资源;
- 若需下载动态生成的内容(如实时导出的 Excel),推荐用 res.attachment + 动态生成内容(如 res.attachment('data.xlsx'); res.send(excelBuffer)),而非 res.download(它仅支持服务器已存在的文件)。
四、总结 核心开发价值
它是开发中 “服务器固定文件下载” 的最优工具,核心解决:
- 简化下载逻辑:一键完成 “设下载头 + 发文件”,无需手动拼接 res.attachment 和 res.sendFile;
- 优化用户体验:支持自定义下载文件名,避免原始文件名复杂;
- 拓展业务能力:下载后回调可实现日志、权限更新等逻辑;
- 兼容权限控制:结合签名 Cookie 等身份验证,实现私密文件下载。
只要开发中涉及 “用户下载服务器已存在的文件”,res.download 就是最简洁高效的选择,比手动实现更易维护且不易出错。
res.end([data] [, encoding])
结束本响应的过程。这个方法实际上来自Node核心模块,具体的是response.end() method of http.ServerResponse。
用来快速结束请求,没有任何的数据。如果你需要发送数据,可以使用res.send()和res.json()这类的方法。
res.end();
res.status(404).end();一、核心本质
res.end 是 Node 原生 http 模块的底层方法(Express 直接继承),核心作用是 “强制结束当前响应流程”—— 不管有没有数据要返回,调用后响应就会关闭,客户端会收到 “响应已完成” 的信号。
它的定位是 “极简响应工具”:
- 不需要返回复杂数据(如 JSON、HTML)时用它;
- 仅需快速结束请求(如返回空响应、简单字符串)时用它;
- 底层所有 Express 响应方法(如 res.send、res.json)最终都会调用 res.end 来完成响应关闭。
与 res.send/res.json 的关键区别(避免混淆)
| 方法 | 核心能力 | 适用场景 | 底层逻辑 |
|---|---|---|---|
| res.end | 仅结束响应,可带简单原始数据(如字符串),不处理格式 | 无数据响应、简单字符串响应 | 原生 http 方法,无额外处理 |
| res.send | 结束响应 + 自动处理数据格式(字符串→文本、对象→JSON)+ 设置 Content-Type | 返回文本、HTML、简单 JSON | 内部处理后调用 res.end |
| res.json | 结束响应 + 强制将数据转为 JSON 格式 + 设置 Content-Type: application/json | 返回结构化 JSON 数据 | 内部转 JSON 后调用 res.end |
一句话总结:res.end 是 “裸奔的底层方法”,res.send/res.json 是 “带格式处理的封装方法”。
二、开发核心作用(附实战场景代码)
场景 1:无数据快速响应(高频基础场景)
不需要返回任何数据,仅需告诉客户端 “请求已处理完成”(如健康检查接口、注销接口的空响应),用 res.end 最轻量。
示例 1:健康检查接口(监控系统常用)
监控系统会定期请求 /health 接口,判断服务是否存活,只需返回 200 状态码 + 空响应即可:
// 健康检查接口:仅返回 200 状态码,无数据
app.get('/health', (req, res) => {
// 无需返回数据,调用 res.end() 快速结束响应
res.end();
// 等价于:res.status(200).end()(默认状态码就是200)
});效果:客户端收到 200 OK 状态码,响应体为空,确认服务正常。
示例 2:注销接口(清除 Cookie 后无数据返回)
用户注销时,清除登录 Cookie 后无需返回数据,直接结束响应:
app.post('/logout', (req, res) => {
// 1. 清除登录Cookie(之前学的 res.clearCookie)
res.clearCookie('userId', { path: '/', signed: true });
// 2. 无数据返回,结束响应
res.end();
});场景 2:返回简单原始字符串(无需格式处理)
仅需返回简短文本(如 “成功”“失败”“404 Not Found”),且不需要 Express 自动处理格式,用 res.end 更高效。
示例:404 页面返回简单提示
自定义 404 中间件,返回简单文本提示,无需渲染 HTML 页面:
// 404 中间件:所有未匹配的路由都会走到这里
app.use((req, res) => {
// 1. 设置 404 状态码
res.status(404);
// 2. 返回简单字符串,用 res.end 结束响应(指定编码避免中文乱码)
res.end('页面不存在(404)', 'utf8');
// 若用 res.send('页面不存在(404)') 效果一致,但 res.end 更底层
});效果:客户端收到 404 Not Found 状态码,响应体为 页面不存在(404)。
场景 3:流式传输结束后关闭响应(不懂)
当用 Node 原生流(如 fs.createReadStream)发送大文件(如视频、大型日志)时,流传输完成后需要调用 res.end 手动关闭响应。
示例:用流发送大日志文件,结束后调用 res.end
const fs = require('fs');
const path = require('path');
// 下载大日志文件(用原生流,避免内存占用过高)
app.get('/download/big-log', (req, res) => {
const logPath = path.join(__dirname, 'logs/big-log.txt');
// 1. 创建文件读取流
const readStream = fs.createReadStream(logPath, 'utf8');
// 2. 流传输完成后,调用 res.end 关闭响应
readStream.on('end', () => {
console.log('日志文件传输完成');
res.end(); // 关键:流结束后必须关闭响应,否则客户端会一直等待
});
// 3. 流错误时,返回错误并关闭响应
readStream.on('error', (err) => {
console.error('读取日志失败:', err);
if (!res.headersSent) {
res.status(500).end('日志文件读取失败');
} else {
res.end(); // 若已发送部分响应,仅关闭
}
});
// 4. 将流管道到响应对象(发送文件内容)
readStream.pipe(res);
});注意:Express 的 res.sendFile/res.download 内部已封装流结束后的 res.end,无需手动调用;但用原生流时必须自己处理。
场景 4:强制中断响应(特殊业务需求)
某些场景下需要 “提前中断响应”(如检测到非法请求时,不返回完整数据直接关闭),用 res.end 强制终止。
示例:检测到非法 IP,直接中断响应
// 非法IP拦截中间件
app.use((req, res, next) => {
const illegalIPs = ['192.168.1.100', '10.0.0.5']; // 黑名单IP
const clientIP = req.ip; // 获取客户端IP
if (illegalIPs.includes(clientIP)) {
// 检测到非法IP,设置403状态码并强制中断响应
res.status(403).end('禁止访问:非法IP');
return; // 必须return,避免继续执行后续中间件
}
next();
});效果:非法 IP 请求会直接收到 403 Forbidden + 简单提示,响应立即关闭,不执行后续业务逻辑。
三、开发关键注意事项(避坑必看)
1. res.end 之后不能再操作响应(响应已关闭)
调用 res.end 后,响应流程已终止,再调用任何响应方法(如 res.send、res.json、res.status)都会报错(Can't set headers after they are sent):
app.get('/test', (req, res) => {
res.end('结束响应');
// 错误:res.end 后再调用 res.send,会抛错
res.send('这行代码会报错');
});2. 发送中文需指定 encoding 参数(否则乱码)
res.end 默认不处理编码,发送中文时需显式指定 encoding: 'utf8',否则客户端可能显示乱码:
// 正确:指定 utf8 编码
res.end('中文内容', 'utf8');
// 错误:不指定编码,中文可能乱码
res.end('中文内容');3. 不自动处理数据格式(传对象会变成 [object Object])
res.end 仅发送 “原始数据”,不会像 res.json 那样将对象转为 JSON 格式,传对象会得到字符串 [object Object]:
// 错误:传对象会变成 [object Object]
res.end({ success: true });
// 正确:若需返回对象,用 res.json;若用 res.end,需手动转JSON字符串
res.end(JSON.stringify({ success: true }), 'utf8');4. 状态码需在 res.end 之前设置
res.status(code) 必须在 res.end 之前调用,否则状态码不会生效(默认 200):
// 正确:先设状态码,再结束响应
res.status(400).end('参数错误');
// 错误:先结束响应,再设状态码(无效,状态码还是200)
res.end('参数错误');
res.status(400);四、总结:res.end 的核心开发价值
它是 Express 中 “最底层的响应结束工具”,核心解决:
- 无数据响应场景(如健康检查、空注销),比 res.send 更轻量;
- 简单字符串响应场景(如 404 提示),避免不必要的格式处理;
- 原生流传输后的响应关闭,以及特殊场景下的强制中断响应;
- 所有上层响应方法(res.send/res.json/res.download)的底层依赖。
记住:简单场景用 res.end,复杂数据用 res.send*/res.json,不要用 res.end 处理结构化数据(如对象、数组),避免格式问题。
res.format(object)
进行内容协商,根据请求的对象中AcceptHTTP头部指定的接受内容。它使用req.accepts()来选择一个句柄来为请求服务,这些句柄按质量值进行排序。如果这个头部没有指定,那么第一个方法默认被调用。当不匹配时,服务器将返回406"Not Acceptable",或者调用default回调。Content-Type请求头被设置,当一个回调方法被选择。然而你可以改变他,在这个方法中使用这些方法,比如res.set()或者res.type()。
下面的例子,将回复{"message":"hey"},当请求的对象中Accept头部设置成"application/json"或者"*/json"(不过如果是*/*,然后这个回复就是"hey")。
res.format({
'text/plain':function() {
res.send('hey');
},
'text/html':function() {
res.send('<p>hey</p>');
},
'application/json':function() {
res.send({message:'hey'});
},
'default':function() {
res.status(406).send('Not Acceptable');
}
})除了规范化的MIME类型之外,你也可以使用拓展名来映射这些类型来避免冗长的实现:
res.format({
text:function() {
res.send('hey');
},
html:function() {
res.send('<p>hey</p>');
},
json:function() {
res.send({message:'hey'});
}
})一、核心本质
res.format 是 Express 封装的 “内容协商工具”,核心作用是 “根据客户端的 Accept 请求头,自动返回对应格式的响应”—— 无需手动用 req.accepts 判断格式再写一堆 if-else,直接传一个 “格式 - 回调” 对象,它会帮你完成 “匹配 - 执行” 全流程。
底层逻辑:
- 读取请求头 Accept(客户端声明的可接受格式,如 text/html, application/json);
- 内部调用 req.accepts 匹配你传入的格式键(如 'text/html'、'json');
- 执行第一个匹配的回调函数(返回对应格式响应);
- 若没有匹配,执行 default 回调(或默认返回 406 “Not Acceptable”)。
它的定位是 “多格式响应的语法糖”,把 “判断格式” 和 “返回响应” 的逻辑整合,让代码更简洁。
与 req.accepts 的关键区别(避免混淆)
| 方法 | 核心能力 | 代码复杂度 | 适用场景 |
|---|---|---|---|
| res.format | 自动匹配 Accept 格式 + 执行对应响应回调 | 低(一行对象配置) | 多格式响应(需快速整合逻辑) |
| req.accepts | 仅返回匹配的格式,需手动写响应逻辑 | 高(需写 if-else 判断) | 需自定义格式匹配后的复杂逻辑 |
一句话总结:req.accepts 是 “只给答案的判断题”,res.format 是 “给答案 + 自动做题的解答题”。
例:同样是返回 HTML/JSON 格式,两种写法对比:
// 用 req.accepts:需手动判断+写响应
app.get('/data', (req, res) => {
const type = req.accepts(['html', 'json']);
if (type === 'html') res.render('data');
else if (type === 'json') res.json({ data: '...' });
else res.status(406).send('不支持');
});
// 用 res.format:自动匹配+执行回调
app.get('/data', (req, res) => {
res.format({
'text/html': () => res.render('data'),
'application/json': () => res.json({ data: '...' }),
default: () => res.status(406).send('不支持')
});
});二、开发核心作用(附实战场景代码)
场景 1:多客户端适配(高频核心场景)
同一接口需适配不同客户端(浏览器→HTML、App→JSON、命令行工具→文本),用 res.format 一键实现不同格式响应,无需重复写判断逻辑。
示例:用户列表接口(多客户端适配)
// 模拟从数据库获取的用户数据
const users = [
{ id: 1, name: '张三', age: 28 },
{ id: 2, name: '李四', age: 30 }
];
app.get('/users', (req, res) => {
res.format({
// 1. 浏览器请求(Accept: text/html)→ 返回HTML页面
'text/html': () => {
// 用模板引擎渲染用户列表页面(如EJS)
res.render('user-list', { users });
},
// 2. App/第三方系统请求(Accept: application/json)→ 返回JSON
'application/json': () => {
res.json({ success: true, data: users });
},
// 3. 命令行工具请求(如curl,Accept: text/plain)→ 返回纯文本
'text/plain': () => {
// 拼接用户信息为文本(每行一个用户)
const text = users.map(u => `ID: ${u.id}, 姓名: ${u.name}`).join('\n');
res.send(text);
},
// 4. 无匹配格式 → 自定义406响应
default: () => {
res.status(406).json({
success: false,
msg: '仅支持 HTML、JSON、纯文本格式'
});
}
});
});效果:
- 浏览器访问 → 看到美化的用户列表页面;
- Postman 设 Accept: application/json → 拿到 JSON 数据;
- curl 命令请求 → 拿到纯文本列表。
场景 2:用扩展名简化配置(减少冗余)
无需写完整 MIME 类型(如 text/html),可直接用扩展名(如 html、json)作为对象键,Express 会自动映射到对应的 MIME 类型,代码更简洁。
示例:简化版多格式响应
app.get('/greeting', (req, res) => {
res.format({
// 扩展名 "html" → 对应 MIME 类型 "text/html"
html: () => res.send('<h1>你好!</h1>'),
// 扩展名 "json" → 对应 MIME 类型 "application/json"
json: () => res.json({ message: '你好!' }),
// 扩展名 "text" → 对应 MIME 类型 "text/plain"
text: () => res.send('你好!'),
default: () => res.status(406).send('不支持的格式')
});
});注意:常用扩展名与 MIME 映射(Express 内置):
- html → text/html、json → application/json、text → text/plain、xml → application/xml。
场景 3:结合错误处理的多格式响应
接口报错时(如用户不存在),也可通过 res.format 返回不同格式的错误信息,避免客户端解析异常(如 JSON 客户端收到 HTML 错误页)。
示例:用户详情接口(错误多格式处理)
app.get('/users/:id', (req, res) => {
const userId = parseInt(req.params.id);
const user = users.find(u => u.id === userId);
if (!user) {
// 用户不存在 → 多格式返回错误
return res.format({
html: () => res.status(404).render('404', { msg: '用户不存在' }),
json: () => res.status(404).json({ success: false, msg: '用户不存在' }),
text: () => res.status(404).send('错误:用户不存在'),
default: () => res.status(406).send('不支持的格式')
});
}
// 用户存在 → 多格式返回用户信息
res.format({
html: () => res.render('user-detail', { user }),
json: () => res.json({ success: true, data: user }),
text: () => res.send(`ID: ${user.id}, 姓名: ${user.name}, 年龄: ${user.age}`)
});
});效果:
- 浏览器访问不存在的用户 → 看到 404 页面;
- App 请求不存在的用户 → 拿到 404 JSON 错误。
场景 4:手动修改自动设置的 Content-Type
res.format 会根据匹配的格式自动设置 Content-Type 响应头(如匹配 html 则设 text/html),若需自定义(如返回 XML 但设为 application/xhtml+xml),可在回调中手动用 res.set 修改。
示例:自定义 Content-Type
app.get('/custom-xml', (req, res) => {
res.format({
// 匹配 "application/xml",但手动设为 "application/xhtml+xml"
'application/xml': () => {
const xml = '<user><name>张三</name></user>';
// 手动覆盖自动设置的 Content-Type
res.set('Content-Type', 'application/xhtml+xml').send(xml);
},
default: () => res.status(406).send('仅支持 XML 格式')
});
});三、开发关键注意事项(避坑必看)
1. 尊重 Accept 头的优先级(q 值)
客户端可通过 q 值设置格式优先级(0~1,默认 1),res.format 会按 q 值从高到低匹配,而非对象键的顺序。
例:客户端 Accept 头为 text/html;q=0.8, application/json;q=0.9(JSON 优先级更高),即使 html 在对象中排前面,也会优先执行 json 回调:
res.format({
html: () => res.send('<h1>html</h1>'), // 虽在前面,但 q 值低,不执行
json: () => res.json({ msg: 'json' }) // q 值高,优先执行
});2. default 回调必须写(避免默认 406 页面)
若不写 default 回调,且无匹配格式,Express 会返回默认的 406 页面(纯文本 “Not Acceptable”),体验差;写 default 可自定义响应内容(如 JSON 格式的错误信息),更适配客户端。
3. 回调中只能执行一次响应(响应一旦发送就结束)
每个 res.format 中只有一个回调会执行(匹配到的那个),且回调中必须调用 res.send/res.render 等响应方法;若回调中未调用响应方法,客户端会一直等待(超时)。
错误示例:回调中未发送响应:
res.format({
json: () => {
console.log('json'); // 仅打印,未调用 res.json,客户端超时
},
default: () => res.status(406).send('不支持')
});4. 键的格式要规范(避免匹配失败)
- 写 MIME 类型时必须完整(如 text/plain,不能写 text;application/json,不能写 json/application);
- 写扩展名时必须是 Express 内置映射的(如 html/json/text,不能自定义扩展名如 myformat)。
四、总结:res.format 的核心开发价值
它是 Express 中 “多格式响应的最优工具”,核心解决:
- 简化内容协商逻辑:不用手动写 req.accepts + if-else,一个对象搞定多客户端适配;
- 统一响应格式管理:将不同格式的响应逻辑集中在一个 res.format 中,代码更易维护;
- 提升客户端兼容性:自动适配浏览器、App、命令行等不同客户端的格式需求,避免解析异常。
记住:只要接口需要返回多种格式(如同时支持 HTML 页面和 JSON 数据),就优先用 res.format,比手动判断高效且不易出错。
res.get(field)
返回field指定的HTTP响应的头部。匹配是区分大小写。
res.get('Content-Type');
// => "text/plain"一、核心本质
res.get(field) 是 Express 提供的 “获取已设置的响应头字段值” 的便捷方法,核心作用是 查询当前响应对象中已配置的 HTTP 响应头内容(比如查询 Content-Type 是 text/html 还是 application/json)。
关键定位与对比:
- 和 req.get(field) 的区别:req.get 是 “获取客户端发来的请求头”(如 Accept、User-Agent),res.get 是 “获取服务器要发给客户端的响应头”(如 Content-Type、Cache-Control);
- 底层关联:对应 Node 原生 http 模块的 res.getHeader(field) 方法,但 Express 未做额外处理(字段名匹配区分大小写,这是核心细节,和 req.get 的 “不区分大小写” 完全不同)。
二、实战核心作用(附场景代码)
res.get 的核心价值是 “查询响应头状态”,常用于验证、动态调整、日志记录等场景,结合你之前学过的 res.set、res.append、res.format 等方法配合使用:
场景 1:验证响应头是否正确设置(高频基础场景)
设置响应头(如 Content-Type、Cache-Control)后,用 res.get 检查是否配置正确,避免因代码疏忽导致响应头错误(如本该返回 application/json,却设成了 text/plain)。
示例:验证 res.format 自动设置的 Content-Type
app.get('/data', (req, res) => {
res.format({
'text/html': () => {
res.render('data');
// 检查自动设置的 Content-Type 是否为 text/html
const contentType = res.get('Content-Type');
console.log('HTML 响应头:', contentType); // 输出 "text/html; charset=utf-8"(模板引擎默认加 charset)
},
'application/json': () => {
res.json({ data: 'test' });
// 检查自动设置的 Content-Type 是否为 application/json
const contentType = res.get('Content-Type');
console.log('JSON 响应头:', contentType); // 输出 "application/json; charset=utf-8"
},
default: () => res.status(406).send('不支持')
});
});作用:开发 / 调试时快速确认响应头是否符合预期,避免客户端因响应头错误导致解析异常(如 JSON 被当成文本解析)。
场景 2:动态调整响应头(基于已有配置)
根据已设置的响应头内容,动态追加或修改其他响应头,实现 “条件化配置”。
示例:根据 Cache-Control 动态追加 Expires 头
app.get('/static/image.jpg', (req, res) => {
// 1. 先设置基础缓存头
res.set('Cache-Control', 'public, max-age=3600');
// 2. 用 res.get 获取已设置的 Cache-Control
const cacheControl = res.get('Cache-Control');
// 3. 若包含 "public",则追加 Expires 头(1小时后过期)
if (cacheControl?.includes('public')) {
const expiresTime = new Date(Date.now() + 3600 * 1000).toUTCString();
res.set('Expires', expiresTime);
console.log('已追加 Expires 头:', res.get('Expires')); // 输出过期时间
}
// 发送图片
res.sendFile(__dirname + '/public/image.jpg');
});作用:避免硬编码所有响应头,根据已有配置动态调整,让代码更灵活。
场景 3:响应头日志记录(调试 / 监控)
将最终发送给客户端的响应头记录到日志(如格式、缓存时间、跨域配置),用于问题排查(如客户端反馈乱码,可查日志确认 Content-Type 是否正确)。
示例:记录所有响应头到日志
// 全局中间件:响应发送后记录响应头
app.use((req, res, next) => {
// 监听响应完成事件(确保所有响应头已设置)
res.on('finish', () => {
const log = {
url: req.originalUrl,
method: req.method,
// 用 res.get 获取关键响应头
contentType: res.get('Content-Type'),
cacheControl: res.get('Cache-Control'),
corsAllowOrigin: res.get('Access-Control-Allow-Origin'),
statusCode: res.statusCode
};
console.log('响应日志:', JSON.stringify(log));
// 示例输出:{"url":"/data","method":"GET","contentType":"application/json; charset=utf-8","cacheControl":"public, max-age=3600","corsAllowOrigin":"*","statusCode":200}
});
next();
});作用:出现问题时可回溯响应头配置,快速定位 “响应头设置错误” 类问题(如跨域失败,查日志发现 Access-Control-Allow-Origin 未设置)。
场景 4:错误处理中检查响应头状态
在错误处理中间件中,用 res.get 检查响应头是否已发送(或已设置特定头),避免重复设置导致报错(如 Can't set headers after they are sent)。
示例:错误处理中判断 Content-Type 是否已设置
// 全局错误处理中间件
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
// 检查是否已设置 Content-Type(避免重复设置)
const existingContentType = res.get('Content-Type');
if (!existingContentType) {
// 未设置则默认设为 JSON 格式
res.set('Content-Type', 'application/json');
}
res.status(statusCode).send({
success: false,
msg: err.message || '服务器内部错误'
});
});作用:确保错误响应的响应头不冲突,避免因重复设置 Content-Type 导致客户端解析异常。
三、关键注意事项(避坑必看)
1. 字段名 区分大小写(最容易踩的坑!)
res.get 对字段名的匹配是 严格区分大小写 的,而 req.get 不区分 —— 这是两者最核心的差异,必须牢记:
res.set('Content-Type', 'text/html'); // 正确设置
res.get('Content-Type'); // => "text/html"(正确,大小写匹配)
res.get('content-type'); // => undefined(错误,小写不匹配)
res.get('CONTENT-TYPE'); // => undefined(错误,全大写不匹配)建议:始终用标准 HTTP 响应头的大小写格式(首字母大写,其余小写,如 Content-Type、Cache-Control、Access-Control-Allow-Origin)。
2. 仅能获取 已设置 的响应头
若响应头未通过 res.set、res.append、res.format 等方法设置,res.get 会返回 undefined:
// 未设置 X-Custom-Header
res.get('X-Custom-Header'); // => undefined
// 设置后再获取
res.set('X-Custom-Header', 'test');
res.get('X-Custom-Header'); // => "test"3. 部分响应头由 Express/Node 自动设置
有些响应头(如 Date、Connection)会由 Express 或 Node 底层自动设置,无需手动配置,也可通过 res.get 获取:
// 获取自动设置的 "Date" 头(响应发送时间)
res.get('Date'); // => "Sat, 25 Oct 2025 12:00:00 GMT"
// 获取自动设置的 "Connection" 头(连接状态)
res.get('Connection'); // => "keep-alive"4. 与 Node 原生 res.getHeader 的关系
res.get(field) 是 Node 原生 res.getHeader(field) 的 “直接映射”,两者效果完全一致 ——Express 未做额外处理,仅提供更简洁的命名:
res.set('Content-Type', 'application/json');
res.get('Content-Type'); // => "application/json"(Express 方法)
res.getHeader('Content-Type'); // => "application/json"(Node 原生方法)四、总结:res.get 的核心开发价值
它是 Express 中 “查询响应头状态的便捷工具”,核心解决:
- 验证响应头配置:确保 res.set/res.append/res.format 等方法设置的响应头正确;
- 动态调整响应头:基于已有响应头配置,条件化追加 / 修改其他头;
- 日志与调试:记录响应头用于监控,或排查 “响应头相关” 的客户端问题;
- 错误处理兼容:避免重复设置响应头导致的报错。
记住:res.get 的核心是 “查询” 而非 “设置”,使用时务必注意 字段名区分大小写,避免因大小写错误导致获取不到响应头。
res.json([body])
发送一个json的响应。这个方法和将一个对象或者一个数组作为参数传递给res.send()方法的效果相同。不过,你可以使用这个方法来转换其他的值到json,例如null,undefined。(虽然这些都是技术上无效的JSON)。
res.json(null);
res.json({user:'tobi'});
res.status(500).json({error:'message'});
一、核心本质
res.json([body]) 是 Express 封装的 “专门发送 JSON 格式响应” 的便捷方法,核心作用是:
- 自动将传入的 body(对象、数组、null 等)转为 JSON 字符串;
- 自动设置响应头 Content-Type: application/json(确保客户端识别为 JSON 数据);
- 结束响应流程(底层调用 res.end,无需额外手动关闭)。
关键定位与对比(和 res.send 的核心差异):
| 方法 | 核心能力 | 自动设置 Content-Type | 处理 null/undefined | 适用场景 |
|---|---|---|---|---|
| res.json | 仅发送 JSON 格式响应,自动转 JSON 字符串 | 是(application/json) | 支持(转为 null/undefined 字符串,虽非严格 JSON 但客户端可解析) | API 接口返回结构化数据(对象 / 数组) |
| res.send | 发送任意格式响应(文本 / HTML/JSON),仅对对象自动转 JSON | 否(文本→text/plain,对象→application/json) | 不支持(undefined 会转为空响应,null 会转为空文本) | 发送文本、HTML 或简单 JSON |
一句话总结:res.json 是 “API 接口的专属 JSON 工具”,比 res.send 更聚焦、更避免客户端解析异常。
二、实战核心作用(附高频场景代码)
res.json 是 前后端分离 API 开发的核心方法,几乎所有返回结构化数据的接口(如登录、列表查询、详情获取)都会用到,以下是最常见的实战场景:
场景 1:API 接口返回成功数据(高频基础场景)
后端给前端(如 Vue/React 项目)返回业务数据(如用户信息、列表数据)时,用 res.json 统一格式,确保前端能直接用 JSON.parse 解析(实际前端框架会自动解析)。
示例 1:登录成功返回用户信息
结合你之前学的 res.cookie 登录场景,登录成功后返回用户基本信息(不含敏感数据):
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 模拟数据库校验:假设登录成功,获取用户数据
const user = { id: 1001, username: 'admin', role: 'super' };
// 1. 设置登录 Cookie(之前学的签名 Cookie)
res.cookie('userId', user.id, { httpOnly: true, signed: true });
// 2. 用 res.json 返回成功数据(自动转 JSON + 设 Content-Type)
res.json({
success: true, // 业务状态:成功/失败标识(前端快速判断)
data: user, // 核心数据:用户信息
msg: '登录成功' // 提示信息:前端可显示给用户
});
});响应效果:
- 响应头:Content-Type: application/json; charset=utf-8;
- 响应体:{"success":true,"data":{"id":1001,"username":"admin","role":"super"},"msg":"登录成功"};
- 前端接收后可直接用 res.data.id 读取用户 ID,无需手动解析。
示例 2:列表查询返回分页数据
前端请求 “用户列表” 接口,后端返回分页数据(总数、当前页、列表),用 res.json 结构化返回:
// 模拟数据库分页查询
const getUsers = (page = 1, pageSize = 10) => {
const total = 50; // 总条数
const list = Array.from({ length: pageSize }, (_, i) => ({
id: (page - 1) * pageSize + i + 1,
username: `user${i + 1}`,
status: 'active'
}));
return { total, list, page, pageSize };
};
app.get('/api/users', (req, res) => {
const page = parseInt(req.query.page) || 1;
const pageSize = parseInt(req.query.pageSize) || 10;
const userPageData = getUsers(page, pageSize);
// 用 res.json 返回分页数据
res.json({
success: true,
data: userPageData, // 分页数据:total/list/page/pageSize
msg: '查询成功'
});
});前端受益:可直接按 res.data.list 渲染列表,res.data.total 计算总页数,无需处理格式转换。
场景 2:API 接口返回错误信息(统一错误格式)
接口报错时(如参数错误、权限不足),用 res.json 返回结构化错误信息,配合 res.status 设置 HTTP 状态码,让前端既能识别错误类型,又能显示友好提示。
示例:权限不足返回错误响应
结合你之前学的登录权限校验,用户无管理员权限时返回错误:
// 管理员权限中间件
const requireAdmin = (req, res, next) => {
const userRole = req.signedCookies.role || 'user'; // 从 Cookie 取角色
if (userRole !== 'admin') {
// 用 res.status(403) 设 HTTP 状态码,res.json 返回错误信息
return res.status(403).json({
success: false,
msg: '权限不足:仅管理员可访问此接口',
errorCode: 'FORBIDDEN' // 自定义错误码:前端可按错误码做特殊处理(如跳转登录)
});
}
next();
};
// 管理员专属接口(需权限校验)
app.get('/api/admin/users', requireAdmin, (req, res) => {
// 管理员查询所有用户(逻辑省略)
res.json({ success: true, data: [], msg: '查询成功' });
});响应效果:
- HTTP 状态码:403(客户端可快速识别 “禁止访问”);
- 响应体:{"success":false,"msg":"权限不足:仅管理员可访问此接口","errorCode":"FORBIDDEN"};
- 前端可按 success: false 显示错误提示,按 errorCode 决定是否跳转权限申请页面。
场景 3:处理非严格 JSON 有效值(如 null/undefined)
虽然 null 是 JSON 有效值(undefined 不是),但实际开发中可能需要返回 “空数据” 标识(如用户暂无订单),res.json 会自动处理这些特殊值,避免客户端解析报错。
示例:用户暂无订单返回 null
app.get('/api/user/orders', (req, res) => {
const userId = req.signedCookies.userId;
// 模拟查询:用户暂无订单
const userOrders = null;
// 用 res.json 返回 null(自动转为 JSON 字符串 "null",设 Content-Type)
res.json({
success: true,
data: userOrders, // 前端接收后可判断 data === null,显示“暂无订单”
msg: '查询成功'
});
});对比 res.send:若用 res.send({ data: null }),虽也能返回 JSON,但 res.json 更明确,且处理 undefined 时更友好(res.json({ data: undefined }) 会返回 {"data":undefined},而 res.send 会返回 {"data":} 导致 JSON 解析失败)。
三、关键注意事项(避坑必看)
1. 自动设置 Content-Type: application/json(无需手动加)
res.json 会强制覆盖之前设置的 Content-Type,确保客户端识别为 JSON 数据,无需手动调用 res.set('Content-Type', 'application/json'):
// 即使先设为 text/plain,res.json 也会覆盖
res.set('Content-Type', 'text/plain');
res.json({ msg: 'test' }); // 最终响应头仍是 application/json2. 支持的 body 类型(避免传入不可转 JSON 的值)
可传入的有效类型:对象、数组、null、布尔值、数字、字符串;
不可传入的类型:函数(会被转为 null)、循环引用对象(会抛错,如 const a = {}; a.b = a; res.json(a))。
错误示例(循环引用):
const user = { name: 'admin' };
user.self = user; // 循环引用:user.self 指向 user 本身
res.json(user); // 抛错:Converting circular structure to JSON3. 状态码需在 res.json 之前设置
若需返回非 200 的 HTTP 状态码(如 400 参数错误、401 未登录),必须在 res.json 之前调用 res.status(code),否则状态码会默认是 200:
// 正确:先设状态码,再返回 JSON
res.status(400).json({ success: false, msg: '参数错误' });
// 错误:先返回 JSON,再设状态码(状态码无效,仍是 200)
res.json({ success: false, msg: '参数错误' });
res.status(400);4. 与 res.send 的核心差异(何时优先用 res.json)
- 写 API 接口(返回结构化数据):优先用 res.json,语义更清晰,避免 Content-Type 配置错误;
- 发送文本 / HTML:用 res.send,res.json 会把 HTML 标签转为 JSON 字符串(如 res.json('
test
') 会返回 "<h1>test</h1>",前端显示标签文本而非渲染 HTML)。
四、总结:res.json 的核心开发价值
它是 前后端分离 API 开发的 “标配工具”,核心解决:
- 简化 JSON 响应流程:不用手动 JSON.stringify(body) + res.set('Content-Type', 'application/json'),一行代码搞定;
- 确保客户端兼容性:自动设置标准 JSON 响应头,避免前端因 Content-Type 错误导致解析失败(如把 JSON 当文本处理);
- 统一响应格式:便于前后端约定 “success+data+msg” 的标准结构,降低沟通成本;
- 处理特殊值:友好支持 null 等空数据标识,避免 res.send 处理 undefined 导致的解析异常。
记住:只要接口需要返回结构化数据(90% 以上的 API 场景),就优先用 res.json,比 res.send 更专业、更不易出错。
res.jsonp([body])
发送一个json的响应,并且支持JSONP。这个方法和res.json()效果相同,除了其在选项中支持JSONP回调。
res.jsonp(null)
// => null
res.jsonp({user:'tobi'})
// => {"user" : "tobi"}
res.status(500).jsonp({error:'message'})
// => {"error" : "message"}默认情况下,jsonp的回调方法简单写作callback。可以通过jsonp callback name设置来重写它。
下面是一些例子使用JSONP响应,使用相同的代码:
// ?callback=foo
res.jsonp({user:'tobo'})
// => foo({"user":"tobi"})
app.set('jsonp callback name', 'cb')
// ?cb=foo
res.status(500).jsonp({error:'message'})
// => foo({"error":"message"})一、核心本质
res.jsonp([body]) 是 res.json 的 “跨域增强版”,核心作用是 发送支持 JSONP 格式的响应,解决前端跨域请求数据的问题。它与 res.json 的关键差异如下:
| 方法 | 核心能力 | 跨域支持 | 响应格式(示例) | 适用场景 |
|---|---|---|---|---|
| res.json | 发送 JSON 响应,自动设 application/json 头 | 不支持(受浏览器同源策略限制) | {"user":"tobi"} | 同域 API 接口 |
| res.jsonp | 发送 JSONP 响应,用回调函数包裹 JSON 数据 | 支持(突破同源策略) | foo({"user":"tobi"})(foo 是回调名) | 跨域 API 接口(兼容旧浏览器) |
关键前提:JSONP 是什么?解决了什么问题?
- 同源策略:浏览器限制 “不同域名、端口、协议” 的前端页面,不能直接通过 AJAX 请求后端接口(比如前端 http://localhost:3000 不能直接请求后端 http://localhost:4000/api);
- JSONP 原理:利用 ),后端直接拼接返回会导致 XSS 攻击。Express 已默认过滤部分恶意字符,但仍建议手动校验:
app.get('/api/safe/jsonp', (req, res) => { // 获取回调函数名(默认是 callback,或自定义的 cb) const callbackName = req.query[app.get('jsonp callback name') || 'callback']; // 校验回调名:仅允许字母、数字、下划线(避免恶意字符) if (!/^[a-zA-Z0-9_]+$/.test(callbackName)) { return res.status(400).send('非法回调函数名'); } // 校验通过,发送 JSONP 响应 res.jsonp({ msg: '安全的 JSONP 响应' }); });3. 与 CORS 的选择:优先用 CORS,其次用 JSONP
- CORS:支持所有 HTTP 方法(GET/POST/PUT 等),安全性更高(可配置跨域白名单),现代浏览器均支持,是首选方案;
- JSONP:仅支持 GET,安全性较低(易受 XSS 攻击),但可兼容旧浏览器(IE8/9);
- 建议:新项目优先用 CORS(后端配置 res.set('Access-Control-Allow-Origin', '允许的域名')),仅在需要兼容旧浏览器时用 JSONP。
4. 响应头自动设置为 application/javascript
res.jsonp 会自动将响应头 Content-Type 设为 application/javascript(而非 application/json),确保浏览器识别为脚本文件并执行回调函数,无需手动设置。
5. 支持的 body 类型与 res.json 一致
可传入对象、数组、null、数字、字符串等(同 res.json),但不支持循环引用对象(会抛错),也不支持函数(会转为 null)。
总结:res.jsonp 的核心开发价值
它是 Express 中 “兼容跨域请求(尤其旧浏览器)” 的专用工具,核心解决:
- 突破同源策略:让不同域名 / 端口的前端页面能获取后端数据;
- 兼容旧浏览器:为 IE8/9 等不支持 CORS 的浏览器提供跨域方案;
- 简化 JSONP 开发:无需手动拼接 “回调函数 + JSON 数据” 的字符串,一行代码完成响应。
记住:现代项目优先用 CORS 解决跨域,仅在需要兼容旧浏览器或第三方接口强制要求 JSONP 时,才用 res.jsonp;且使用时务必注意过滤恶意回调名,避免 XSS 风险。
res.links(links)
连接这些
links,links是以传入参数的属性形式提供,连接之后的内容用来填充响应的Link HTTP头部。res.links({ next:'http://api.example.com/users?page=2', last:'http://api.example.com/user?page=5' });效果:
Link:<http://api.example.com/users?page=2>;rel="next", <http://api.example.com/users?page=5>;rel="last"二、Link 响应头的格式规则(res.links 自动生成)
res.links 接收一个 “关联关系→URL” 的对象(如 { next: 'xxx', last: 'xxx' }),自动将其转换为符合 HTTP 标准的 Link 头格式,无需手动拼接字符串(避免格式错误)。
基础格式
单个关联资源的格式:<关联资源URL>; rel="关联关系"
多个关联资源用 逗号 分隔:
; rel="rel1", ; rel="rel2" 关键字段说明
组成部分含义与要求<关联资源URL>必须是 绝对 URL(含 http:///https:// 或完整域名),不能用相对路径;用 < 和 > 包裹(HTTP 标准要求)。rel="关联关系"定义 “当前资源与关联资源的关系”,是 Link 头的核心标识;可使用 HTTP 标准关系(如 next/prev/first/last),也可自定义(如 author/related)。
一、核心本质
res.links(links) 是 Express 封装的 “专门设置 Link HTTP 响应头” 的便捷方法,核心作用是:
- 接收一个 “rel 关系 → URL” 的对象(如 { next: 'http://...', last: 'http://...' });
- 自动将对象转为 标准 Link 头格式(
; rel="rel值"),无需手动拼接字符串; - 覆盖或设置完整的 Link 响应头,帮助客户端快速获取 “关联资源链接”(如下一页、最后一页、相关资源)。
关键对比:手动设置 vs res.links
- 手动设置(用 res.append):需自己拼接格式,易出错
res.append('Link', '<http://api.example.com/users?page=2>; rel="next"'); res.append('Link', '<http://api.example.com/users?page=5>; rel="last"');- 用 res.links:一行代码搞定,自动格式化
res.links({ next: 'http://api.example.com/users?page=2', last: 'http://api.example.com/users?page=5' });两者最终效果一致,但 res.links 更简洁、更符合 HTTP 规范,避免手动拼接的格式错误(如遗漏 <、> 或 rel 引号)。
关键前提:Link 头是什么?解决了什么问题?
- Link 头定义:HTTP 标准响应头,用于返回 “当前资源的关联资源 URL”,客户端可通过解析该头快速获取相关链接,无需从响应体中提取;
- 核心价值:符合 RESTful API 规范(让接口更具 “自描述性”),减少客户端与服务端的耦合 —— 比如分页接口,客户端不用解析响应体的 page/totalPage 计算下一页 URL,直接读 Link 头的 next 链接即可。
二、实战核心作用(附高频场景代码)
res.links 主要用于 RESTful API 场景,尤其适合需要返回 “关联资源链接” 的接口(如分页、关联数据),让客户端更高效地获取后续资源:
场景 1:API 分页(最高频场景)
列表类接口(如用户列表、文章列表)返回分页数据时,用 res.links 设置 next(下一页)、last(最后一页)、prev(上一页)、first(第一页)链接,客户端直接通过 Link 头跳转,无需手动计算 URL。
示例:用户列表分页接口
const express = require('express'); const app = express(); // 模拟数据库分页查询:返回用户列表+分页信息 const getUsers = (page = 1, pageSize = 10) => { const totalUsers = 50; // 总用户数 const totalPages = Math.ceil(totalUsers / pageSize); // 总页数 const users = Array.from({ length: pageSize }, (_, i) => ({ id: (page - 1) * pageSize + i + 1, username: `user${i + 1}` })); return { users, totalPages, currentPage: page }; }; // 分页接口:用 res.links 设置分页链接 app.get('/api/users', (req, res) => { const currentPage = parseInt(req.query.page) || 1; const pageSize = parseInt(req.query.pageSize) || 10; const { users, totalPages } = getUsers(currentPage, pageSize); // 1. 构建分页链接对象(rel 关系 → URL) const paginationLinks = {}; // 下一页:当前页 < 总页数时存在 if (currentPage < totalPages) { paginationLinks.next = `http://api.example.com/api/users?page=${currentPage + 1}&pageSize=${pageSize}`; } // 最后一页:总页数 > 1 时存在 if (totalPages > 1) { paginationLinks.last = `http://api.example.com/api/users?page=${totalPages}&pageSize=${pageSize}`; } // 上一页:当前页 > 1 时存在 if (currentPage > 1) { paginationLinks.prev = `http://api.example.com/api/users?page=${currentPage - 1}&pageSize=${pageSize}`; } // 第一页:当前页 > 1 时存在 if (currentPage > 1) { paginationLinks.first = `http://api.example.com/api/users?page=1&pageSize=${pageSize}`; } // 2. 用 res.links 设置 Link 响应头 res.links(paginationLinks); // 3. 返回分页数据(响应体仍包含分页信息,兼容不支持 Link 头的客户端) res.json({ success: true, data: { users, pagination: { currentPage, totalPages, pageSize } } }); }); app.listen(3000, () => { console.log('服务器运行在 http://localhost:3000'); });响应效果:
当请求 http://localhost:3000/api/users?page=2&pageSize=10(总页数 5)时,响应头会包含:
Link: <http://api.example.com/api/users?page=3&pageSize=10>; rel="next", <http://api.example.com/api/users?page=5&pageSize=10>; rel="last", <http://api.example.com/api/users?page=1&pageSize=10>; rel="prev", <http://api.example.com/api/users?page=1&pageSize=10>; rel="first"客户端受益:
- 前端无需计算 nextPage = currentPage + 1 再拼接 URL,直接解析 Link 头的 next 字段即可跳转下一页;
- 符合 RESTful 规范,接口更具通用性(如第三方客户端也能通过 Link 头快速适配分页)。
场景 2:关联资源链接(RESTful API 规范)
在返回资源详情时,用 res.links 设置 “关联资源” 的链接(如文章详情返回 “作者链接”“相关文章链接”),让客户端一键获取相关数据,无需额外请求接口查询关联 URL。
示例:文章详情接口返回关联链接
// 模拟文章数据 const article = { id: 101, title: 'Express res.links 用法', content: '...', authorId: 202, relatedArticleIds: [102, 103] }; // 文章详情接口 app.get('/api/articles/:id', (req, res) => { const articleId = parseInt(req.params.id); if (articleId !== article.id) { return res.status(404).json({ success: false, msg: '文章不存在' }); } // 构建关联资源链接 const relatedLinks = { author: `http://api.example.com/api/users/${article.authorId}`, // 作者详情链接 related1: `http://api.example.com/api/articles/${article.relatedArticleIds[0]}`, // 相关文章1 related2: `http://api.example.com/api/articles/${article.relatedArticleIds[1]}` // 相关文章2 }; // 设置 Link 头 res.links(relatedLinks); // 返回文章详情 res.json({ success: true, data: article }); });响应效果:
响应头 Link 会包含:
Link: <http://api.example.com/api/users/202>; rel="author", <http://api.example.com/api/articles/102>; rel="related1", <http://api.example.com/api/articles/103>; rel="related2"客户端受益:
- 想查看文章作者时,直接用 author 对应的 URL 发起请求,无需先查 “作者 ID 对应的 URL 规则”;
- 关联资源的 URL 由服务端统一维护,后续若 URL 规则变更(如 /api/users 改为 /api/authors),只需服务端修改,前端无需适配。
场景 3:追加已有 Link 头(配合 res.get)
若已通过 res.links 或 res.append 设置过 Link 头,想追加新的链接,需先通过 res.get('Link') 获取已有值,再手动拼接后重新设置(res.links 会覆盖原有 Link 头,而非追加)。
示例:追加 “评论列表” 链接到已有 Link 头
app.get('/api/articles/:id', (req, res) => { // 1. 先设置分页相关链接 res.links({ next: 'http://api.example.com/api/articles?page=2', last: 'http://api.example.com/api/articles?page=5' }); // 2. 追加“评论列表”链接:先获取已有 Link 头 const existingLink = res.get('Link') || ''; const commentLink = '<http://api.example.com/api/articles/101/comments>; rel="comments"'; // 拼接新链接(已有值则加逗号分隔) const newLink = existingLink ? `${existingLink}, ${commentLink}` : commentLink; // 3. 重新设置 Link 头 res.set('Link', newLink); // 返回文章数据 res.json({ success: true, data: article }); });响应效果:
最终 Link 头会包含原有分页链接和新增的评论链接:
Link: <http://api.example.com/api/articles?page=2>; rel="next", <http://api.example.com/api/articles?page=5>; rel="last", <http://api.example.com/api/articles/101/comments>; rel="comments"三、关键注意事项(避坑必看)
1. 对象的 “键” 是 rel 属性,“值” 必须是绝对 URL
- 键(rel属性):表示 “当前资源与关联资源的关系”,需符合 HTTP 规范(常用值:next/prev/first/last/author/self 等,自定义值如 related 也可);
- 值(URL):必须是 绝对 URL(包含 http:///https:// 和完整域名),不能用相对路径(如 /api/users?page=2)—— 否则客户端无法识别(尤其跨域场景);
❌ 错误:res.links({ next: '/api/users?page=2' })(相对路径);
✅ 正确:res.links({ next: 'http://api.example.com/api/users?page=2' })(绝对 URL)。
2. res.links 会覆盖原有 Link 头(非追加)
若已通过 res.links 或 res.set 设置过 Link 头,再次调用 res.links 会覆盖之前的内容,而非追加。如需追加,需按 “场景 3” 的方式,先获取已有值再拼接。
// 第一次设置 Link 头 res.links({ next: 'http://...?page=2' }); // 第二次调用会覆盖,之前的 next 链接消失 res.links({ last: 'http://...?page=5' }); // 最终 Link 头仅包含 last 链接,无 next 链接3. 客户端需主动解析 Link 头(非自动生效)
res.links 仅负责 “设置”Link 头,客户端需手动解析该头才能获取链接(浏览器不会自动跳转或加载关联资源)。前端解析示例(原生 JavaScript):
// 前端请求分页接口后,解析 Link 头 fetch('http://localhost:3000/api/users?page=2') .then(response => { // 1. 获取 Link 头 const linkHeader = response.headers.get('Link'); if (!linkHeader) return; // 2. 解析 Link 头为对象(格式:"<URL1>; rel="rel1", <URL2>; rel="rel2"") const links = {}; linkHeader.split(',').forEach(link => { const [urlPart, relPart] = link.trim().split('; '); const url = urlPart.slice(1, -1); // 去掉 < 和 > const rel = relPart.split('=')[1].slice(1, -1); // 去掉 rel=" 和 " links[rel] = url; }); // 3. 使用解析后的链接(如下一页) console.log('下一页链接:', links.next); // 输出 http://api.example.com/api/users?page=3 });4. 配合响应体数据,兼容低版本客户端
部分旧客户端(或简易接口测试工具)不支持解析 Link 头,因此实际开发中,建议在 响应体中同时返回关联链接信息(如分页接口的 pagination.nextUrl),确保兼容性:
res.json({ success: true, data: { users, pagination: { currentPage: 2, totalPages: 5, nextUrl: 'http://api.example.com/api/users?page=3', // 响应体中也包含 next 链接 lastUrl: 'http://api.example.com/api/users?page=5' } } });四、总结:res.links 的核心开发价值
它是 Express 中 “符合 RESTful API 规范的关联链接工具”,核心解决:
- 简化 Link 头设置:无需手动拼接
; rel="rel值" 格式,一行代码生成标准响应头,避免格式错误; - 提升接口自描述性:让客户端通过 Link 头快速获取关联资源(分页、作者、相关数据),减少接口文档依赖;
- 降低前后端耦合:关联资源的 URL 由服务端统一维护,后续 URL 规则变更时,前端无需修改代码。
记住:只要开发 RESTful API(尤其分页、关联资源场景),就优先用 res.links 设置 Link 头,让接口更规范、更易维护。
res.location(path)
设置响应的
LocationHTTP头部为指定的path参数。res.location('/foo/bar'); res.location('http://example.com'); res.location('back');当
path参数为back时,其具有特殊的意义,其指定URL为请求对象的Referer头部指定的URL。如果请求中没有指定,那么其即为"/"。Express传递指定的URL字符串作为回复给浏览器响应中的
Location头部的值,不检测和操作,除了back这个参数。浏览器会将用户重定向到location设置的url或者Referer的url(back参数的情况)一、核心本质
res.location(path) 是 Express 封装的 “专门设置 Location HTTP 响应头” 的基础方法,核心作用是:
- 接收一个路径参数(path),直接将其作为 Location 头的值写入响应;
- 仅对特殊值 back 进行处理(自动解析为请求的 Referer 头,无则 fallback 到 /);
- 不主动触发重定向、不校验路径合法性、不修改路径格式(除 back 外),仅负责 “写入头部”。
关键对比:手动设置 vs res.location
- 手动设置(用 res.set):需显式指定头部名,且需自己处理 back 逻辑
// 手动处理 back 逻辑 const redirectUrl = req.get('Referer') || '/'; res.set('Location', redirectUrl);- 用 res.location:一行代码搞定,自动解析 back
res.location('back'); // 等价于上面的手动逻辑两者最终均设置 Location 头,但 res.location 简化了 back 参数的处理,避免重复编码。
关键前提:Location 头是什么?解决了什么问题?
- Location 头定义:HTTP 标准响应头,用于告知客户端 “目标资源的位置”,常与 重定向状态码(如 301/302/307)配合使用;
- 核心价值:作为重定向的 “坐标指引”—— 浏览器检测到带重定向状态码的响应时,会自动跳转到 Location 头指定的 URL;也可用于非重定向场景(如资源创建后返回新资源地址,符合 RESTful 规范)。
二、实战核心作用(附高频场景代码)
res.location 本身不触发跳转,需配合 状态码 或前端逻辑生效,主要用于重定向、资源定位等场景:
场景 1:配合状态码实现服务端重定向(最常用)
res.location 仅写 Location 头,需手动添加重定向状态码(如 302 临时重定向、301 永久重定向),浏览器才会自动跳转。
示例:登录成功后重定向到首页
const express = require('express'); const app = express(); app.use(express.urlencoded({ extended: true })); // 登录接口:成功后重定向到首页 app.post('/login', (req, res) => { const { username, password } = req.body; if (username === 'admin' && password === '123456') { // 1. 设置 Location 头(目标路径:首页) res.location('/home'); // 2. 添加 302 状态码(触发浏览器跳转) return res.status(302).send(); } // 失败:返回上一页(用 back 参数) res.location('back'); res.status(401).send('用户名或密码错误'); }); // 首页路由 app.get('/home', (req, res) => { res.send('欢迎来到首页'); }); app.listen(3000);响应效果:
登录成功时,响应头包含:
Location: /home Status: 302 Found浏览器收到后自动跳转到 http://localhost:3000/home。
场景 2:back 参数实现 “返回上一页”(表单提交失败常用)
当 path 为 back 时,自动读取请求的 Referer 头(即上一页 URL),无则默认跳转到根路径(/),适合表单提交失败、操作取消等场景。
示例:表单提交失败返回上一页
// 注册接口:失败返回表单页 app.post('/register', (req, res) => { const { email } = req.body; // 模拟邮箱格式校验失败 if (!email.includes('@')) { // 无需手动获取 Referer,back 自动处理 res.location('back'); // 带错误信息返回,前端可读取并显示 return res.status(400).send('邮箱格式错误,请重新填写'); } // 成功:返回新用户详情页(绝对 URL 示例) res.location('http://api.example.com/users/1001'); res.status(201).send('注册成功'); });特殊逻辑说明:
- 若用户从 http://localhost:3000/register-form 提交表单,Referer 为该地址,back 会指向此页面;
- 若 Referer 不存在(如直接访问接口),则 Location 头为 /,跳转到根路径。
场景 3:RESTful API 资源创建后返回新资源地址
符合 RESTful 规范:当创建资源(如 POST 请求)成功时,用 Location 头返回新资源的 URL,客户端可通过该地址查询详情。
示例:创建文章后返回文章详情地址
// 模拟文章数据库 let articles = []; let id = 1; // 创建文章接口(POST) app.post('/api/articles', (req, res) => { const { title, content } = req.body; const newArticle = { id, title, content }; articles.push(newArticle); id++; // 1. 设置 Location 头(新文章的绝对 URL) res.location(`http://api.example.com/api/articles/${newArticle.id}`); // 2. 201 状态码表示“资源创建成功” res.status(201).json(newArticle); }); // 文章详情接口 app.get('/api/articles/:id', (req, res) => { const article = articles.find(a => a.id === parseInt(req.params.id)); res.json(article); });响应效果:
创建成功时,响应头包含:
Location: http://api.example.com/api/articles/1 Status: 201 Created客户端可通过 Location 头直接请求新文章详情,无需手动拼接 URL。
三、关键注意事项(避坑必看)
1. res.location 不触发重定向,必须配合状态码
这是最易混淆的点!res.location 仅写 Location 头,不会让浏览器自动跳转,需搭配以下状态码:
- 301:永久重定向(浏览器会缓存该跳转关系);
- 302:临时重定向(默认,不缓存);
- 307:临时重定向(保留请求方法,如 POST 提交后跳转仍用 POST);
- 201:资源创建成功(不跳转,仅返回新资源地址)。
❌ 错误用法(仅设置头,不设状态码):
res.location('/home'); res.send(); // 浏览器不会跳转,仅显示空白页✅ 正确用法(加状态码):
res.location('/home').status(302).send();2. 路径类型的不同处理逻辑
path 支持 3 种类型,处理方式不同,需根据场景选择:
路径类型 示例 处理逻辑 适用场景 相对路径(根级) /home 拼接当前域名,生成绝对 URL 站内跳转 相对路径(层级) ../users 基于当前请求路径解析(如从 /admin/info 跳转到 /users) 站内层级跳转 绝对 URL http://example.com 直接作为 Location 头的值,不修改 跨域跳转、外部链接 ❌ 跨域跳转错误示例(用相对路径):
res.location('/home'); // 会被解析为当前域名下的 /home,无法跨域✅ 跨域跳转正确示例(用绝对 URL):
res.location('https://baidu.com'); // 正确指向外部域名3. back 参数依赖 Referer 头,存在局限性
back 的实现依赖请求头 Referer,但该头可能被浏览器禁用或篡改,需注意:
- 隐私模式下,浏览器可能不发送 Referer,此时 back 会跳转到 /;
- 若需可靠的 “返回上一页”,建议前端在请求中携带目标路径(如 ?redirect=/register-form),服务端优先读取该参数:
// 更可靠的返回逻辑:优先用请求参数,再用 back const redirectUrl = req.query.redirect || 'back'; res.location(redirectUrl);4. 与 res.redirect 的区别(避免混淆)
res.redirect 是更高层的封装,内部调用了 res.location 并自动设置状态码,两者分工明确:
特性 res.location res.redirect 核心功能 仅设置 Location 头 设置 Location 头 + 自动加状态码(默认 302) 触发跳转 否(需手动加状态码) 是(自动触发) 代码简洁性 较低(需多写一行状态码) 较高(一行搞定:res.redirect('/home')) 使用建议:
- 简单重定向用 res.redirect(更简洁);
- 需自定义状态码(如 301/201)、或仅返回资源地址(不跳转)时,用 res.location。
四、总结:res.location 的核心开发价值
它是 Express 中 “Location 头的基础操作工具”,核心解决 3 个问题:
- 简化头部设置:无需手动处理 Location 头的格式,一行代码完成写入;
- 优化 “返回上一页” 逻辑:back 参数自动解析 Referer,避免重复编码;
- 适配多场景路径:支持相对路径、绝对 URL,兼容站内跳转与跨域需求。
记住:res.location 是 “指引方向”,需配合状态码或前端逻辑才能 “触发行动”—— 服务端重定向加 30x 状态码,资源定位用 201 状态码,返回上一页用 back 参数,这是它的核心使用准则。
res.redirect([status,] path)
重定向来源于指定
path的URL,以及指定的HTTP status codestatus。如果你没有指定status,status code默认为"302 Found"。res.redirect('/foo/bar'); res.redirect('http://example.com'); res.redirect(301, 'http://example.com'); res.redirect('../login');重定向也可以是完整的URL,来重定向到不同的站点。
res.redirect('http://google.com');重定向也可以相对于主机的根路径。比如,如果程序的路径为
http://example.com/admin/post/new,那么下面将重定向到http://example.com/admim:res.redirect('/admin');重定向也可以相对于当前的URL。比如,来之于
http://example.com/blog/admin/(注意结尾的/),下面将重定向到http://example.com/blog/admin/post/new。res.redirect('post/new');如果来至于
http://example.com/blog/admin(没有尾部/),重定向post/new,将重定向到http://example.com/blog/post/new。如果你觉得上面很混乱,可以把路径段认为目录(有'/')或者文件,这样是可以的。相对路径的重定向也是可以的。如果你当前的路径为http://example.com/admin/post/new,下面的操作将重定向到http://example.com/admin/post:res.redirect('..');back将重定向请求到referer,当没有referer的时候,默认为/。res.redirect('back');一、核心本质
res.redirect 是 Express 封装的 “一键触发浏览器重定向” 的高层方法,核心逻辑是:
- 内部调用 res.location(path):先设置 Location 响应头(指定跳转目标路径);
- 自动设置重定向状态码:若未传 status,默认 302(临时重定向),传则用指定状态码;
- 自动结束响应:无需手动调用 res.send()/res.end(),内部已完成响应关闭。
对比 res.location(关键!之前学过的核心关联):
特性 res.location(path) res.redirect([status,] path) 核心能力 仅设置 Location 头(不触发跳转) 设置 Location 头 + 自动加状态码 + 触发跳转 状态码 需手动设置(否则无跳转) 默认 302,可自定义(如 301/307) 响应结束 需手动调用 res.send() 内部自动结束响应 适用场景 资源定位(如 201 状态码返回地址) 主动跳转(登录后、表单失败等) 一句话总结:res.redirect = res.location + 自动状态码 + 自动结束响应,是 “即开即用的重定向工具”。
关键前提:重定向状态码的选择(必懂!)
不同状态码对应不同的重定向语义,直接影响浏览器行为(如缓存、请求方法保留),常用状态码:
状态码 名称 核心语义 浏览器行为 302 Found 临时重定向(默认) 不缓存跳转关系,下次请求仍走原路径 301 Moved Permanently 永久重定向 缓存跳转关系,下次直接跳目标路径 307 Temporary Redirect 临时重定向(保留请求方法) POST 请求跳转后仍用 POST(302 可能转 GET) 308 Permanent Redirect 永久重定向(保留请求方法) 同 307,但缓存跳转关系 “直接跳转用 redirect,要显内容用 location;前者省心省代码,后者灵活需补全。”
二、实战核心作用(附高频场景代码 + 路径解析)
res.redirect 是开发中 “页面跳转” 的核心方法,覆盖登录、表单处理、跨域跳转等场景,重点解析路径解析逻辑(用户示例中易混淆的点):
场景 1:登录成功 / 失败后的跳转(最常用)
登录成功跳首页,失败跳回登录页(用 back 或指定路径),自动处理状态码与响应结束。
示例:登录跳转完整逻辑
const express = require('express'); const app = express(); app.use(express.urlencoded({ extended: true })); // 登录页路由 app.get('/login', (req, res) => { res.send(` <form method="POST" action="/login"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <button type="submit">登录</button> </form> `); }); // 登录接口:用 res.redirect 跳转 app.post('/login', (req, res) => { const { username, password } = req.body; if (username === 'admin' && password === '123456') { // 1. 成功:302 临时重定向到首页(默认 302,可省略不写) return res.redirect('/home'); // 若需永久重定向(如域名变更),传状态码:res.redirect(301, '/home') } // 2. 失败:用 back 返回上一页(即登录页,带错误提示) res.redirect('back'); }); // 首页路由 app.get('/home', (req, res) => { res.send('欢迎来到首页!'); }); app.listen(3000);路径解析:
- res.redirect('/home'):绝对路径(根级),无论当前请求路径是什么,都跳转到 http://localhost:3000/home;
- res.redirect('back'):自动读取 Referer 头(此处为 /login),跳回登录页。
场景 2:相对路径跳转(用户示例重点解析)
res.redirect 支持相对路径(相对当前请求 URL),但需注意 “当前路径是否带尾部 /”,这是最易踩坑的点,结合用户示例拆解:
当前请求 URL 调用 res.redirect(...) 跳转目标 URL 解析逻辑(类比 “目录 / 文件”) http://a.com/admin/post/new(无尾部 /) ../login http://a.com/admin/login 当前路径视为 “文件”(new),.. 回到 “admin” 目录 http://a.com/blog/admin/(带尾部 /) post/new http://a.com/blog/admin/post/new 当前路径视为 “目录”(admin/),直接拼接子路径 http://a.com/blog/admin(无尾部 /) post/new http://a.com/blog/post/new 当前路径视为 “文件”(admin),post/new 拼到上级目录 http://a.com/admin/post/new .. http://a.com/admin/post .. 表示 “上级目录”,从 new 回到 post 示例:相对路径跳转演示
// 当前请求 URL:http://localhost:3000/blog/admin/(带 /) app.get('/blog/admin/', (req, res) => { res.redirect('post/new'); // 跳转到 http://localhost:3000/blog/admin/post/new }); // 当前请求 URL:http://localhost:3000/blog/admin(无 /) app.get('/blog/admin', (req, res) => { res.redirect('post/new'); // 跳转到 http://localhost:3000/blog/post/new });场景 3:跨域 / 外部站点跳转(用户示例:跳谷歌)
直接传完整绝对 URL,可跳转到其他域名的站点,无需手动处理跨域(浏览器自动跳转)。
示例:跳转到外部站点
// 跳转谷歌(跨域) app.get('/go-google', (req, res) => { // 301 永久重定向(告诉浏览器“这个地址永久移到谷歌了”,下次直接跳) res.redirect(301, 'https://google.com'); }); // 跳转其他站点的具体路径 app.get('/go-example', (req, res) => { res.redirect('http://example.com/admin'); });状态码选择:
- 若跳转目标是永久不变的(如旧域名跳新域名),用 301 永久重定向(浏览器缓存,提升后续访问速度);
- 若跳转是临时的(如登录后跳首页),用默认 302 即可。
场景 4:RESTful 资源创建后重定向(配合 303 状态码)
虽然资源创建常用 201 状态码返回地址,但有时需跳转到新资源的详情页(如创建文章后跳文章页),用 303 状态码(保留 POST 请求方法后跳转,避免重复提交)。
示例:创建文章后跳详情页
let articles = []; let id = 1; // 创建文章(POST) app.post('/api/articles', (req, res) => { const newArticle = { id, title: req.body.title }; articles.push(newArticle); id++; // 303:临时重定向,保留 POST 方法,跳转到新文章详情页 res.redirect(303, `/api/articles/${newArticle.id}`); }); // 文章详情页(GET) app.get('/api/articles/:id', (req, res) => { const article = articles.find(a => a.id === parseInt(req.params.id)); res.send(`<h1>${article.title}</h1>`); });三、关键注意事项(避坑必看)
1. 状态码选择:避免滥用 301 永久重定向
301 会被浏览器缓存,若后续需要修改跳转目标,缓存会导致用户无法访问新地址(需手动清除缓存)。
- 推荐:临时跳转(登录、表单处理)用默认 302;
- 谨慎用 301:仅当跳转目标永久不变时(如旧域名跳新域名)。
2. back 参数的局限性(同 res.location)
back 依赖 Referer 头,若浏览器禁用或隐私模式下,Referer 不存在,会默认跳转到 /(根路径)。
- 优化方案:前端传 redirect 参数,服务端优先使用(避免依赖 Referer):
// 前端请求:/submit?redirect=/form app.post('/submit', (req, res) => { const redirectUrl = req.query.redirect || 'back'; res.redirect(redirectUrl); });3. 不能重复调用 res.redirect(响应已结束)
res.redirect 内部会自动结束响应(调用 res.end()),若重复调用会报错 Can't set headers after they are sent:
app.get('/test', (req, res) => { res.redirect('/home'); // 错误:redirect 已结束响应,再调用会抛错 res.redirect('/login'); });4. 相对路径跳转:优先用绝对路径避免混乱
若对相对路径的解析逻辑不熟悉,建议优先用 绝对路径(如 /home、http://example.com),避免因 “尾部 /” 导致的跳转错误。
四、总结:res.redirect 的核心开发价值
它是 Express 中 “页面跳转的一站式工具”,核心解决 3 个问题:
- 简化重定向流程:无需手动调用 res.location + res.status + res.send,一行代码搞定;
- 适配多场景跳转:支持绝对路径、相对路径、跨域路径、back 参数,覆盖登录、表单、外部跳转等需求;
- 自动处理状态码:默认 302 临时重定向,可自定义 301/307/308,满足不同语义需求。
对比 res.location 的选择准则:
- 90% 的跳转场景(登录、表单、外部链接)用 res.redirect(简洁高效);
- 仅当需要 “返回资源地址但不跳转”(如 RESTful 201 创建资源)时,用 res.location + res.status(201)。
记住:res.redirect 是 “直接触发跳转的最终工具”,而 res.location 是 “仅设置地址的基础工具”,根据是否需要主动跳转选择即可。
res.render(view [, locals] [, callback])
渲染一个视图,然后将渲染得到的HTML文档发送给客户端。可选的参数为:
locals,定义了视图本地参数属性的一个对象。callback,一个回调方法。如果提供了这个参数,render方法将返回错误和渲染之后的模板,并且不自动发送响应。当有错误发生时,可以在这个回调内部,调用next(err)方法。
本地变量缓存使能视图缓存。在开发环境中缓存视图,需要手动设置为true;视图缓存在生产环境中默认开启。
// send the rendered view to the client res.render('index'); // if a callback is specified, the render HTML string has to be sent explicitly res.render('index', function(err, html) { res.send(html); }); // pass a local variable to the view res.render('user', {name:'Tobi'}, function(err, html) { // ... });一、先明确核心定位:res.render 是 “动态 HTML 生成工具”
res.render 是 Express 中 模板引擎(如 EJS、Pug、Handlebars)的核心调用方法,其本质作用是:
- 读取指定的 “模板文件”(如 index.ejs、user.pug);
- 将 locals 参数中的动态数据(如用户名、订单列表)注入模板;
- 渲染生成完整的 HTML 字符串;
- 自动将 HTML 发送给客户端(若未传 callback),或通过回调返回 HTML(需手动发送)。
关键对比:res.render vs res.send(开发中最易混淆)
维度 res.render(view [, locals] [, callback]) res.send([body]) 核心能力 模板 + 数据 → 动态生成 HTML 并返回 直接发送静态内容(字符串、Buffer、JSON) 数据动态性 支持动态注入数据(如用户信息、列表数据) 静态内容,需手动拼接动态数据(繁琐) 代码复用性 支持模板组件复用(如导航栏、页脚) 无复用性,需重复写相同 HTML 结构 适用场景 动态页面(用户中心、商品详情、博客列表) 简单响应(纯文本、静态 HTML、JSON 接口) 示例 res.render('user', { name: '张三' }) res.send(' Hello 张三
')一句话总结:res.send 是 “静态内容搬运工”,res.render 是 “动态 HTML 工厂”—— 前者适合简单场景,后者是开发动态网页的核心。
二、实际开发中的 5 个高频业务场景
res.render 的核心价值是 “用模板化思维开发动态页面”,覆盖项目中 90% 以上的 HTML 页面需求,以下是最常见的场景:
场景 1:动态用户中心页面(注入用户个性化数据)
用户登录后,需显示 “用户名、头像、订单列表” 等个性化数据,通过 locals 将数据注入模板,动态生成用户专属页面。
示例(用 EJS 模板):
- 模板文件 views/user.ejs(EJS 用 <%= %> 渲染变量):
<!-- 模板中直接使用 locals 里的 user 数据 --> <!DOCTYPE html> <html> <head> <title><%= user.name %> 的个人中心</title> </head> <body> <h1>欢迎 <%= user.name %>!</h1> <img src="<%= user.avatar %>" alt="头像" style="width:100px"> <h3>我的订单(共 <%= orders.length %> 个)</h3> <ul> <!-- 模板循环渲染订单列表 --> <% orders.forEach(order => { %> <li>订单号:<%= order.id %> | 金额:¥<%= order.amount %></li> <% }) %> </ul> </body> </html>- 后端接口用 res.render 注入数据:
// 假设已通过登录态获取当前用户 ID app.get('/user/profile', (req, res) => { const currentUserId = req.signedCookies.userId; // 从 Cookie 取登录用户 ID // 1. 模拟从数据库查询用户数据和订单数据 const userData = { name: '张三', avatar: '/images/avatar-zhangsan.jpg', level: 'VIP3' }; const userOrders = [ { id: 'ORD20250101', amount: 199 }, { id: 'ORD20250105', amount: 299 } ]; // 2. 调用 res.render:注入数据到模板,生成 HTML 并返回 // locals 参数:user 和 orders 会成为模板中的全局变量 res.render('user', { user: userData, orders: userOrders, pageTitle: '个人中心' // 额外注入页面标题 }); });效果:客户端收到的是完整 HTML(数据已嵌入),浏览器直接渲染出带用户信息和订单的个性化页面。
场景 2:公共组件复用(减少重复 HTML 代码)
项目中 “导航栏、页脚、侧边栏” 等公共部分,可通过模板的 “引入机制” 复用,res.render 渲染时会自动拼接这些组件,避免每个页面重复写相同代码。
示例(EJS 模板引入公共导航栏):
- 公共导航栏模板 views/components/header.ejs:
<!-- 公共导航栏:所有页面都可引入 --> <nav style="background:#f5f5f5;padding:10px"> <a href="/" style="margin-right:20px">首页</a> <a href="/user/profile" style="margin-right:20px">个人中心</a> <a href="/logout">退出登录</a> </nav>- 首页模板 views/index.ejs 引入导航栏:
<!-- 引入公共导航栏(EJS 用 include 语法) --> <% include ./components/header %> <!DOCTYPE html> <html> <body> <h1>首页内容</h1> <p>当前时间:<%= currentTime %></p> </body> </html>- 后端渲染首页:
app.get('/', (req, res) => { res.render('index', { currentTime: new Date().toLocaleString() // 注入当前时间 }); });效果:渲染后的首页 HTML 会自动包含导航栏,后续修改导航栏只需改 header.ejs,所有页面同步更新,极大减少维护成本。
场景 3:多语言 / 主题适配(根据条件渲染不同内容)
通过 locals 传递 “语言标识、主题类型” 等参数,模板根据参数动态切换内容(如中文 / 英文、浅色 / 深色主题),无需开发多个页面。
示例(多语言页面):
- 模板 views/about.ejs 根据 lang 参数切换语言:
<!DOCTYPE html> <html> <head> <title><%= lang === 'en' ? 'About Us' : '关于我们' %></title> </head> <body> <h1><%= lang === 'en' ? 'About Our Team' : '关于我们的团队' %></h1> <p><%= lang === 'en' ? 'We focus on web development' : '我们专注于 Web 开发' %></p> </body> </html>- 后端根据请求参数切换语言:
// 访问 /about?lang=en 显示英文,默认中文 app.get('/about', (req, res) => { const lang = req.query.lang || 'zh'; // 从 URL 参数取语言标识 res.render('about', { lang }); // 注入语言参数 });效果:
- 访问 /about → 显示中文页面;
- 访问 /about?lang=en → 显示英文页面,实现 “一套模板,多语言适配”。
场景 4:带回调的自定义处理(如捕获模板错误、二次加工 HTML)
当需要捕获模板渲染错误(如模板文件不存在、变量未定义),或对生成的 HTML 进行二次加工(如压缩、添加水印)时,可传 callback 参数手动处理。
示例(捕获模板错误 + 压缩 HTML):
const htmlMinify = require('html-minifier'); // 第三方 HTML 压缩库 app.get('/minified-page', (req, res) => { res.render('index', { currentTime: new Date().toLocaleString() }, (err, html) => { // 1. 捕获模板渲染错误 if (err) { console.error('模板渲染错误:', err); return res.status(500).send('页面渲染失败'); // 返回错误提示 } // 2. 二次加工:压缩 HTML(去除空格、注释,减少传输量) const minifiedHtml = htmlMinify.minify(html, { removeComments: true, // 移除注释 collapseWhitespace: true // 压缩空格 }); // 3. 手动发送压缩后的 HTML res.send(minifiedHtml); }); });作用:
- 避免模板错误导致服务器崩溃(通过 callback 捕获错误,返回友好提示);
- 优化性能(压缩 HTML 减少传输体积,提升加载速度)。
场景 5:定制化错误页面(404、500 页面)
用 res.render 渲染自定义错误页面,比直接用 res.send 写 HTML 更灵活,且能复用公共组件(如导航栏)。
示例(404 页面):
- 404 模板 views/errors/404.ejs:
<% include ../components/header %> <!-- 复用导航栏 --> <!DOCTYPE html> <html> <body> <div style="text-align:center;margin-top:50px"> <h1 style="color:red">404 - 页面不存在</h1> <p><%= message || '您访问的页面已被删除或地址错误' %></p> <a href="/">返回首页</a> </div> </body> </html>- 后端 404 中间件:
// 404 中间件:所有未匹配的路由都会走到这里 app.use((req, res) => { res.status(404).render('errors/404', { message: `您访问的路径 ${req.path} 不存在` // 注入自定义错误信息 }); });效果:用户访问不存在的路径时,会看到带导航栏、自定义提示的 404 页面,体验比默认的 “Cannot GET /xxx” 友好得多。
三、res.render 的核心价值(为什么实际开发离不开它?)
- 解耦前后端代码
后端负责提供数据(如从数据库查用户信息),前端负责模板渲染(HTML 结构 + 样式),分工明确,便于团队协作(前端开发者只需改模板,无需动后端逻辑)。
- 提升代码复用性
公共组件(导航栏、页脚)只需写一次,所有页面通过 include 引入,修改时 “一处改,处处生效”,减少重复劳动。
- 动态内容生成更高效
无需手动拼接 HTML 字符串(如 res.send('
Hello ' + username + '
')),模板语法(如 EJS 的 <%= %>、循环)更直观,降低代码出错率。- 支持视图缓存优化
生产环境中,res.render 会自动缓存渲染后的模板(避免每次请求都重新读取模板文件),提升服务器响应速度;开发环境可手动关闭缓存(app.set('view cache', false)),方便实时调试模板。
四、避坑注意事项(实际开发中易踩的坑)
- 模板路径必须正确(基于 view 目录)
Express 默认模板目录是项目根目录的 views 文件夹,res.render('user') 会查找 views/user.ejs(需先配置模板引擎,如 app.set('view engine', 'ejs'));若模板在子目录,需写相对路径(如 res.render('errors/404') 对应 views/errors/404.ejs)。
- locals 变量未定义会导致模板报错
模板中使用的变量(如 <%= user.name %>)必须在 locals 中传入,否则会报 “user is not defined” 错误;建议对可选变量设默认值(如 <%= user?.name || '匿名用户' %>,ES6 可选链语法)。
- 传 callback 时必须手动发送响应
若传了 callback(如 res.render('index', (err, html) => { ... })),res.render 不会自动发送 HTML,需手动调用 res.send(html),否则客户端会一直等待超时。
- 开发环境关闭视图缓存
开发中修改模板后,需实时看到效果,需在代码中关闭缓存:
if (process.env.NODE_ENV === 'development') { app.set('view cache', false); // 开发环境关闭视图缓存 }生产环境会自动开启缓存,无需额外配置。
五、总结:res.render 的适用边界
业务需求 推荐方法 不推荐方法 原因 动态 HTML 页面(用户中心、商品详情) res.render res.send res.render 支持模板复用和动态数据注入 静态 HTML / 纯文本响应 res.send res.render 无需模板,res.send 更简洁 API 接口(返回 JSON) res.json res.render res.json 自动设 Content-Type: application/json 页面跳转 res.redirect res.render res.redirect 直接触发跳转,无需渲染页面 一句话选择准则:只要需要生成 “带动态数据的 HTML 页面”,就用 res.render;其他简单响应(JSON、静态文本、跳转)用对应的专用方法。
res.send([body])
发送HTTP响应。
body参数可以是一个Buffer对象,一个字符串,一个对象,或者一个数组。比如:res.send(new Buffer('whoop')); res.send({some:'json'}); res.send('<p>some html</p>'); res.status(404).send('Sorry, we cannot find that!'); res.status(500).send({ error: 'something blew up' });对于一般的非流请求,这个方法可以执行许多有用的的任务:比如,它自动给
Content-LengthHTTP响应头赋值(除非先前定义),也支持自动的HEAD和HTTP缓存更新。
当参数是一个Buffer对象,这个方法设置Content-Type响应头为application/octet-stream,除非事先提供,如下所示:res.set('Content-Type', 'text/html'); res.send(new Buffer('<p>some html</p>'));当参数是一个字符串,这个方法设置
Content-Type响应头为text/html:res.send('<p>some html</p>');当参数是一个对象或者数组,Express使用JSON格式来表示:
res.send({user:'tobi'}); res.send([1, 2, 3]);一、核心本质
res.send([body]) 是 Express 中 最通用的 HTTP 响应方法,核心作用是:
- 支持多类型响应体:可接收 Buffer、字符串、对象 / 数组、null 等,自动适配处理逻辑;
- 自动完成基础配置:
- 设 Content-Length 头(避免浏览器 “一直等待响应”);
按 body 类型自动设 Content-Type 头(如字符串→text/html,对象→application/json);
自动结束响应:内部调用 res.end(),无需手动关闭响应流程。
对比 res.json/res.end(关键!关联你已学的方法):
特性 res.send([body]) res.json([body]) res.end([data]) 支持 body 类型 Buffer、字符串、对象 / 数组、null 仅对象 / 数组 /null(自动转 JSON) 仅 Buffer、字符串(不处理格式) 自动设 Content-Type 按类型适配(字符串→text/html,对象→application/json) 强制 application/json 仅 Buffer→application/octet-stream,字符串→无(需手动设) 自动转 JSON 对象 / 数组会转 JSON,但非 “强制”(可被手动覆盖 Content-Type) 强制转 JSON,优先级高于手动 Content-Type 不转(对象会变成 [object Object]) 核心定位 通用响应工具(HTML、文本、简单 JSON 都能用) 专门 JSON 响应(API 接口首选) 底层结束响应(无数据 / 简单字符串) 一句话总结:res.send 是 “万能响应接口”,能处理大多数简单场景;res.json 是 “JSON 专用接口”,res.end 是 “底层结束工具”,三者分工互补。
关键前提:不同 body 类型的自动处理逻辑
res.send 的核心优势是 “智能适配”,不同 body 类型的处理逻辑直接影响开发使用,必须先明确:
body 类型 处理逻辑 自动 Content-Type 示例 字符串 直接发送,支持 HTML 标签渲染 text/html; charset=utf-8 res.send(' Hello
')对象 / 数组 自动转 JSON 字符串,支持 JSON.stringify 兼容类型 application/json; charset=utf-8 res.send({ name: 'tobi' }) Buffer 发送二进制数据(不解析内容) application/octet-stream res.send(Buffer.from('whoop')) null/ 无参 发送空响应(仅设 Content-Length: 0) 无(或 text/plain,视版本略有差异) res.send(null) / res.send() 二、实战核心作用(附高频场景代码)
res.send 是开发中 “最常用的响应方法之一”,覆盖简单页面、文本提示、二进制数据、基础 API 等场景,重点解析不同 body 类型的用法:
场景 1:发送 HTML 内容(渲染简单页面 / 片段)
当需要返回少量 HTML(如静态提示页、简单表单),无需模板引擎时,用 res.send 直接发送 HTML 字符串,自动设 text/html 头,浏览器会渲染标签。
示例:简单登录页(无需模板引擎)
const express = require('express'); const app = express(); // 登录页:用 res.send 发送 HTML 字符串 app.get('/login-page', (req, res) => { const html = ` <!DOCTYPE html> <html> <head><title>登录</title></head> <body> <form method="POST" action="/login"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <button type="submit">登录</button> </form> </body> </html> `; // 自动设 Content-Type: text/html,浏览器渲染 HTML res.send(html); }); app.listen(3000);优势:无需引入 EJS/Pug 等模板引擎,快速实现简单页面,适合原型开发或静态提示(如 404 页)。
场景 2:发送简单文本提示(非 HTML 内容)
需要返回纯文本(如接口状态提示、日志信息)时,可手动覆盖 Content-Type 为 text/plain,避免浏览器将文本解析为 HTML。
示例:纯文本状态提示
// 文本提示接口 app.get('/text-tip', (req, res) => { // 手动设 Content-Type 为 text/plain,确保浏览器显示纯文本(不解析标签) res.set('Content-Type', 'text/plain'); // 发送文本,浏览器会显示 "操作成功!当前时间:2025-10-25"(不渲染 <br>) res.send(`操作成功!\n当前时间:${new Date().toLocaleDateString()}`); }); // 404 纯文本提示(替代 HTML 页面) app.use((req, res) => { res.status(404) .set('Content-Type', 'text/plain') .send('404 - 页面不存在'); });场景 3:发送二进制数据(如图片片段、文件流片段)
当需要返回二进制内容(如小图片、加密数据)时,传入 Buffer 对象,res.send 会自动设 Content-Type: application/octet-stream(或手动指定具体类型如 image/png)。
示例:返回小图片二进制数据
const fs = require('fs'); // 读取小图片为 Buffer,用 res.send 发送 app.get('/small-image', (req, res) => { // 读取图片文件为 Buffer(假设图片小于 1MB,大文件建议用流) const imageBuffer = fs.readFileSync(__dirname + '/public/small-logo.png'); // 手动设 Content-Type 为 image/png(避免默认的 application/octet-stream) res.set('Content-Type', 'image/png'); // 发送 Buffer,浏览器会渲染图片 res.send(imageBuffer); });注意:大文件(如超过 10MB)不建议用 res.send(会把文件全部读入内存,占用资源),需用 res.sendFile 或流(fs.createReadStream)。
场景 4:发送简单 JSON 数据(非 API 接口场景)
虽然 res.json 是 JSON 专用方法,但简单场景下(如返回少量状态数据),res.send 也能自动处理对象 / 数组,效果与 res.json 一致。
示例:简单状态响应
// 检查用户名是否存在(简单 JSON 响应) app.get('/check-username', (req, res) => { const username = req.query.username; // 模拟数据库查询:admin 已存在 const isExist = username === 'admin'; // 发送对象,res.send 自动转 JSON,设 Content-Type: application/json res.send({ success: true, data: { isExist } }); // 等价于 res.json({ success: true, data: { isExist } }) });何时优先用 res.json:API 接口需明确返回 JSON(避免后续代码意外覆盖 Content-Type)、处理 undefined 等特殊值时,res.json 更可靠。
场景 5:结合状态码返回错误信息
开发中常需返回 “状态码 + 错误文本 / JSON”,res.send 可链式结合 res.status(),快速实现错误响应。
示例:多类型错误响应
// 参数错误:返回文本提示 app.get('/validate', (req, res) => { const age = parseInt(req.query.age); if (isNaN(age)) { res.status(400).send('参数错误:age 必须是数字'); } }); // 服务器错误:返回 JSON 错误信息 app.get('/server-error', (req, res) => { try { // 模拟代码报错 throw new Error('数据库连接失败'); } catch (err) { res.status(500).send({ success: false, error: err.message, code: 'DB_ERROR' }); } });三、关键注意事项(避坑必看)
1. 自动 Content-Type 可被手动覆盖(需注意顺序)
res.send 会按 body 类型自动设 Content-Type,但可通过 res.set() 手动覆盖,需确保覆盖操作在 res.send() 之前:
// 正确:先设 Content-Type,再 send(字符串按 text/plain 解析,不渲染 HTML) res.set('Content-Type', 'text/plain').send('<h1>Hello</h1>'); // 错误:先 send 再设,Content-Type 已被自动设置,覆盖无效 res.send('<h1>Hello</h1>').set('Content-Type', 'text/plain');2. 对象 / 数组转 JSON 有局限性(不支持循环引用)
res.send 处理对象 / 数组时,内部会调用 JSON.stringify(),若对象存在循环引用(如 a.b = a),会抛错 Converting circular structure to JSON:
// 错误:循环引用对象 const obj = { name: 'test' }; obj.self = obj; // obj.self 指向自身 res.send(obj); // 抛错- 大文件不建议用 res.send(内存占用过高)
res.send 会将 body 全部读入内存后再发送,若处理大文件(如 100MB 视频),会导致 Node 进程内存飙升,甚至崩溃。
- 正确方案:大文件用 res.sendFile(自动用流)或原生流:
// 大文件推荐用 res.sendFile res.sendFile(__dirname + '/public/large-video.mp4');4. 不能重复调用 res.send(响应已结束)
res.send 内部会调用 res.end() 结束响应,重复调用会报错 Can't set headers after they are sent:
app.get('/test', (req, res) => { res.send('第一次响应'); // 错误:重复调用,响应已结束 res.send('第二次响应'); });5. null 与无参的区别(空响应处理)
- res.send(null):发送空响应,Content-Length: 0,Content-Type 可能为 text/plain;
- res.send():等价于 res.send(null),效果一致;
- 场景:需返回 “无内容但成功” 的响应(如 DELETE 接口删除成功),可直接用 res.status(204).send()(204 状态码表示 “无内容”)。
四、总结:res.send 的核心开发价值
它是 Express 中 “最灵活的基础响应工具”,核心解决 3 个问题:
- 覆盖多场景响应需求:无需切换 res.json/res.end,一个方法处理 HTML、文本、二进制、简单 JSON;
- 简化开发流程:自动设 Content-Length/Content-Type,无需手动配置基础响应头;
- 降低学习成本:新手无需记忆多个响应方法,先用 res.send 处理大多数简单场景。
选择准则:
- 发送 HTML / 纯文本 / 小二进制数据:优先用 res.send;
- 发送 JSON 格式 API 响应:优先用 res.json(语义更清晰,处理特殊值更可靠);
- 仅结束响应无数据:用 res.end();
- 发送大文件:用 res.sendFile 或流。
记住:res.send 是 “通用工具”,但并非 “所有场景最优解”,需根据具体需求搭配其他响应方法。
res.sendFile(path [, options] [, fn])
res.sendFile()从Express v4.8.0开始支持。传输
path指定的文件。根据文件的扩展名设置Content-TypeHTTP头部。除非在options中有关于root的设置,path一定是关于文件的绝对路径。
下面的表提供了options参数的细节:属性 描述 默认值 可用版本 maxAge 设置 Cache-Control的max-age属性,格式为毫秒数,或者是ms format的一串字符串0 root 相对文件名的根目录 lastModified 设置 Last-Modified头部为此文件在系统中的最后一次修改时间。设置false来禁用它Enable 4.9.0+ headers 一个对象,包含了文件相关的HTTP头部。 dotfiles 是否支持点开头文件名的选项。可选的值"allow","deny","ignore" "ignore" 当传输完成或者发生了什么错误,这个方法调用
fn回调方法。如果这个回调参数指定了和一个错误发生,回调方法必须明确地通过结束请求-响应循环或者传递控制到下个路由来处理响应过程。
下面是使用了所有参数的使用res.sendFile()的例子:app.get('/file/:name', function(req, res, next) { var options = { root:__dirname + '/public', dotfile:'deny', headers:{ 'x-timestamp':Date.now(), 'x-sent':true } }; var fileName = req.params.name; res.sendFile(fileName, options, function(err) { if (err) { console.log(err); res.status(err.status).end(); } else { console.log('sent', fileName); } }); });res.sendFile提供了文件服务的细粒度支持,如下例子说明:app.get('/user/:uid/photos/:file', function(req, res) { var uid = req.params.uid , file = req.params.file; req.user.mayViewFilesFrom(uid, function(yes) { if (yes) { res.sendFile('/upload/' + uid + '/' + file); } else { res.status(403).send('Sorry! you cant see that.'); } }); })获取更多信息,或者你有问题或者关注,可以查阅send。
一、res.sendFile 到底解决什么问题?
res.sendFile 是 Express 中 “让浏览器直接预览服务器文件” 的核心方法,核心优势在于:
- 自动适配文件类型:根据文件扩展名(如 .png、.html、.pdf)自动设置 Content-Type 响应头,无需手动配置;
- 流式传输大文件:不将文件全部读入内存,而是分片发送,避免大文件(如 100MB 视频)导致的内存溢出;
- 细粒度控制:通过 options 参数配置缓存、根目录、隐藏文件访问权限等,满足复杂场景需求。
关键区分:和 res.download 仅差一个响应头 ——res.download 自动加 Content-Disposition: attachment(触发下载),res.sendFile 默认加 Content-Disposition: inline(浏览器预览)。
二、options 参数全解析(每个属性的实战用法)
文档中 options 的 5 个属性是核心,下面结合具体场景说明 “什么时候用、怎么用、避坑点”:
1. root:简化路径,避免绝对路径冗余
作用:指定 “相对路径的根目录”,后续 path 参数只需传 “相对于根目录的文件名”,不用每次写完整绝对路径。
实战场景:项目中静态文件(图片、HTML)集中存放在 ./public 目录,用 root 统一管理路径。
示例:
const express = require('express'); const app = express(); const path = require('path'); // 配置 root 为 ./public 目录(用 path.resolve 确保绝对路径,兼容不同系统) const staticRoot = path.resolve(__dirname, 'public'); // 接口1:访问 ./public/logo.png(只需传 "logo.png",无需完整路径) app.get('/logo', (req, res) => { res.sendFile('logo.png', { root: staticRoot }, (err) => { if (err) res.status(404).send('Logo 不存在'); }); }); // 接口2:访问 ./public/pages/help.html(相对 root 的子路径) app.get('/help', (req, res) => { res.sendFile('pages/help.html', { root: staticRoot }, (err) => { if (err) res.status(404).send('帮助页不存在'); }); });避坑点:
- root 必须是 绝对路径(推荐用 path.resolve 或 path.join(__dirname, 'xxx')),不能用相对路径(如 root: './public' 在某些环境下可能失效);
- 若同时传 root 和完整绝对路径的 path,root 会被忽略(以 path 为准)。
2. maxAge:控制浏览器缓存,减少重复请求
作用:设置 Cache-Control 头的 max-age 属性,指定浏览器缓存文件的时间(毫秒或 ms 格式字符串),优化重复访问速度。
实战场景:静态资源(如 logo、CSS、JS)长期不变,设置较长缓存时间;动态更新的文件(如每日报表)设置短缓存。
示例:
// 1. 长期不变的静态资源:缓存 7 天(支持 ms 格式字符串,更直观) app.get('/static/css/main.css', (req, res) => { res.sendFile('css/main.css', { root: staticRoot, maxAge: '7d' // 等价于 7*24*60*60*1000 = 604800000 毫秒 }, (err) => { if (err) res.status(404).send('CSS 文件不存在'); }); }); // 2. 动态更新的文件:缓存 10 分钟(用毫秒数) app.get('/daily-report.pdf', (req, res) => { const reportPath = `reports/${new Date().toLocaleDateString()}.pdf`; // 每日生成新报表 res.sendFile(reportPath, { root: staticRoot, maxAge: 10 * 60 * 1000 // 10 分钟后缓存失效,重新请求 }, (err) => { if (err) res.status(404).send('今日报表未生成'); }); });避坑点:
- 若同时设置 maxAge 和 headers: { 'Cache-Control': 'xxx' },headers 中的配置会覆盖 maxAge(headers 优先级更高);
- 开发环境建议设 maxAge: 0(禁用缓存),避免修改文件后浏览器仍加载旧缓存。
3. lastModified:利用文件修改时间优化缓存
作用:自动将 Last-Modified 响应头设为 “文件在服务器上的最后修改时间”,浏览器下次请求时会带 If-Modified-Since 头,服务器对比时间后,若文件未修改则返回 304(不传输文件内容),减少流量消耗。
实战场景:大多数静态文件(如 HTML、图片),无需手动维护 “文件更新时间”,依赖系统自动判断。
示例:
app.get('/article/:id.html', (req, res) => { const articlePath = `articles/${req.params.id}.html`; res.sendFile(articlePath, { root: staticRoot, lastModified: true, // 启用自动设置 Last-Modified(默认就是 true,可省略) maxAge: '1h' // 1 小时内优先用缓存,超过后对比 lastModified }, (err) => { if (err) res.status(404).send('文章不存在'); }); });避坑点:
- 若文件是动态生成的(如每次请求都重新生成相同文件名的文件),需设 lastModified: false,避免浏览器因 “文件时间未变” 加载旧内容;
- 304 响应仅返回状态码和头信息,无响应体,需确保客户端能处理 304(现代浏览器均支持)。
4. headers:自定义响应头,传递额外信息
作用:添加自定义 HTTP 响应头,可用于传递文件元信息(如文件所有者、生成时间)、跨域配置、安全验证等。
实战场景:需要给文件预览接口添加 “身份标识”“来源说明”,或自定义缓存策略。
示例:
// 给用户个人照片添加自定义头,标记所有者和访问时间 app.get('/user/:uid/avatar', (req, res) => { const uid = req.params.uid; res.sendFile(`avatars/${uid}.png`, { root: staticRoot, headers: { 'X-File-Owner': uid, // 自定义头:文件所有者 ID 'X-Sent-Time': new Date().toISOString(), // 自定义头:响应发送时间 'Cache-Control': 'public, max-age=86400' // 自定义缓存策略(覆盖 maxAge) } }, (err) => { if (err) res.status(404).send('头像不存在'); }); });避坑点:
- 自定义头建议以 X- 开头(HTTP 规范约定,避免与标准头冲突);
- 若需跨域访问这些自定义头,需在 CORS 配置中添加 Access-Control-Expose-Headers: X-File-Owner, X-Sent-Time,否则前端无法读取。
5. dotfiles:控制隐藏文件(. 开头文件)的访问权限
作用:设置是否允许访问 “以点开头的文件”(如 .gitignore、.DS_Store、.env),可选值:
- allow:允许访问;
- deny:拒绝访问,返回 403;
- ignore:忽略(视为文件不存在,返回 404)(默认值)。
实战场景:避免服务器敏感隐藏文件(如 .env 配置文件)被外部访问,同时支持必要的隐藏文件(如 .well-known 用于 SSL 验证)。
示例:
// 1. 禁止访问所有隐藏文件(防止泄露 .env、.gitignore) app.get('/hidden/:file', (req, res) => { res.sendFile(req.params.file, { root: staticRoot, dotfiles: 'deny' // 访问 .xxx 文件会返回 403 }, (err) => { if (err) res.status(err.status).send(err.message); }); }); // 2. 允许访问 .well-known 目录下的隐藏文件(用于 SSL 验证) app.get('/.well-known/:file', (req, res) => { res.sendFile(`.well-known/${req.params.file}`, { root: staticRoot, dotfiles: 'allow' // 允许访问此目录下的隐藏文件 }, (err) => { if (err) res.status(404).send('验证文件不存在'); }); });避坑点:
- 默认 dotfiles: 'ignore' 会将隐藏文件视为不存在(返回 404),而非 403,避免暴露 “服务器存在该文件” 的信息;
- 生产环境绝不能设 dotfiles: 'allow' 全局生效,仅在必要目录(如 .well-known)单独配置。
三、回调函数(fn)的错误处理(实战必写)
文档强调 “错误发生时必须处理响应”,否则客户端会一直等待超时。下面拆解常见错误类型及处理逻辑:
常见错误类型(err.code)
err.code 含义 处理方案 ENOENT 文件不存在 返回 404 + “文件不存在” 提示 EACCES 服务器无文件读取权限 返回 403 + “权限不足” 提示,检查服务器文件权限 EISDIR 路径指向目录而非文件 返回 400 + “路径不是文件” 提示 ENAMETOOLONG 文件名过长 返回 414 + “文件名过长” 提示 实战错误处理示例
app.get('/file/:name', (req, res) => { const fileName = req.params.name; res.sendFile(fileName, { root: path.resolve(__dirname, 'public'), dotfiles: 'deny' }, (err) => { if (!err) { console.log(`成功传输文件:${fileName}`); return; } // 按错误类型处理 switch (err.code) { case 'ENOENT': res.status(404).json({ success: false, msg: `文件 ${fileName} 不存在` }); break; case 'EACCES': res.status(403).json({ success: false, msg: `无权限读取文件 ${fileName}` }); console.error('权限错误:', err.path); // 记录错误路径,便于排查服务器权限 break; default: res.status(500).json({ success: false, msg: '服务器读取文件失败', code: err.code }); console.error('文件传输错误:', err); // 记录完整错误日志 } }); });四、高频实战场景拓展
场景 1:结合路由参数动态返回文件(用户专属文件)
如文档中的 “用户照片” 示例,通过路由参数 uid 和 file 定位用户专属文件,先校验权限再返回,避免越权访问。
// 模拟用户权限校验:当前登录用户只能看自己的照片 const checkPhotoPermission = (currentUserId, targetUserId) => { return currentUserId === targetUserId; // 简化逻辑:自己看自己的 }; app.get('/user/:uid/photos/:file', (req, res) => { const currentUserId = req.signedCookies.userId; // 从登录 Cookie 取当前用户 ID const targetUserId = req.params.uid; const photoFile = req.params.file; // 1. 未登录:返回 401 if (!currentUserId) { return res.status(401).send('请先登录'); } // 2. 权限校验失败:返回 403 if (!checkPhotoPermission(currentUserId, targetUserId)) { return res.status(403).send('无权限查看此用户照片'); } // 3. 权限通过:返回照片文件 res.sendFile(`${targetUserId}/photos/${photoFile}`, { root: path.resolve(__dirname, 'uploads'), // 用户文件存放在 ./uploads 目录 maxAge: '1d' // 照片缓存 1 天 }, (err) => { if (err) res.status(404).send('照片不存在'); }); });场景 2:处理中文文件名(避免乱码)
中文文件名可能导致路径解析错误,需用 encodeURIComponent 编码后再传递,服务器端解码后处理。
// 前端:编码中文文件名(如“我的文档.pdf”→“%E6%88%91%E7%9A%84%E6%96%87%E6%A1%A3.pdf”) // 后端:解码并返回文件 app.get('/file/chinese', (req, res) => { const encodedFileName = req.query.name; // 接收编码后的文件名 if (!encodedFileName) { return res.status(400).send('请传入文件名'); } // 解码中文文件名 const fileName = decodeURIComponent(encodedFileName); res.sendFile(fileName, { root: staticRoot, headers: { // 手动设置 Content-Disposition,确保中文文件名在浏览器中正确显示 'Content-Disposition': `inline; filename*=UTF-8''${encodedFileName}` } }, (err) => { if (err) res.status(404).send('中文文件不存在'); }); });五、总结:res.sendFile 的核心使用准则
- 路径处理优先用 root + 相对路径:用 path.resolve 确保 root 是绝对路径,避免跨系统兼容性问题;
- 缓存配置按需选择:静态资源设长缓存(maxAge: '7d')+ lastModified: true,动态资源设短缓存(maxAge: '10m');
- 错误处理必须写:按 err.code 分类处理,避免客户端超时;
- 隐藏文件严格控制:默认 dotfiles: 'ignore',必要场景才设 allow,禁止全局开放;
- 大文件必用此方法:流式传输优势明显,优于 res.send(会读入内存)。
只要开发中涉及 “浏览器直接预览服务器文件” 的场景(如图片、HTML、PDF),res.sendFile 就是最优选择,灵活配置 options 可满足绝大多数需求。
res.sendStatus(statusCode)
设置响应对象的
HTTP status code为statusCode并且发送statusCode的相应的字符串形式作为响应的Body。res.sendStatus(200); // equivalent to res.status(200).send('OK'); res.sendStatus(403); // equivalent to res.status(403).send('Forbidden'); res.sendStatus(404); // equivalent to res.status(404).send('Not Found'); res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error')如果一个不支持的状态被指定,这个HTTP status依然被设置为
statusCode并且用这个code的字符串作为Body。res.sendStatus(2000); // equivalent to res.status(2000).send('2000');一、先看透本质:res.sendStatus 是 “语法糖”,不是 “新功能”
核心定义
res.sendStatus(statusCode) 的本质是 “设置 HTTP 状态码 + 发送对应文本响应体” 的简化写法,等价于:
res.status(statusCode).send(状态码对应的标准文本);比如:
- res.sendStatus(200) → res.status(200).send('OK')
- res.sendStatus(404) → res.status(404).send('Not Found')
关键对比:res.sendStatus vs res.status(开发中最易混淆)
维度 res.sendStatus(statusCode) res.status(statusCode) 核心能力 设状态码 + 发文本响应体(一步完成) 仅设状态码(不发响应体,需后续调用 send/json) 响应体内容 固定文本(如 403→'Forbidden') 自定义(可发文本、JSON、HTML 等) 响应结束时机 自动结束(内部调用 send) 不结束(需手动调用 send/json/end) 适用场景 简单状态响应(无复杂数据) 复杂响应(需自定义响应体) 一句话总结:res.sendStatus 是 “快速响应工具”,res.status 是 “基础状态配置工具”—— 前者是后者的 “快捷版”,但灵活性更低。
二、高频实战场景(这些情况用它最爽)
res.sendStatus 仅适合 “无需自定义响应体,只需告知状态” 的场景,下面是开发中最常见的 4 类场景:
场景 1:简单成功响应(无数据返回)
当接口仅需告知 “操作成功”,无需返回业务数据(如注销、删除、状态更新),用 res.sendStatus(200) 简化代码。
示例:用户注销接口
// 注销:清除登录 Cookie 后,仅需告知“成功” app.post('/logout', (req, res) => { // 1. 清除登录 Cookie(之前学的 res.clearCookie) res.clearCookie('userId', { path: '/', signed: true }); // 2. 用 sendStatus 快速返回成功状态(响应体是 'OK') res.sendStatus(200); }); // 删除一条记录:无需返回数据,仅需告知“删除成功” app.delete('/api/records/:id', (req, res) => { const recordId = req.params.id; // 模拟数据库删除逻辑(如 db.records.deleteOne({ id: recordId })) const deleteSuccess = true; // 假设删除成功 if (deleteSuccess) { res.sendStatus(200); // 成功→200 OK } else { res.sendStatus(500); // 失败→500 服务器错误 } });场景 2:权限 / 访问控制失败(无额外说明)
当用户无权限访问接口(如普通用户访问管理员接口)、IP 被拉黑等,仅需告知 “禁止访问”,无需复杂错误说明。
示例:管理员接口权限校验
// 管理员权限中间件 const requireAdmin = (req, res, next) => { const userRole = req.signedCookies.role || 'user'; // 从 Cookie 取角色 if (userRole !== 'admin') { // 无权限→403 Forbidden,响应体是 'Forbidden' return res.sendStatus(403); } next(); }; // 管理员专属接口(需权限校验) app.get('/api/admin/users', requireAdmin, (req, res) => { // 管理员查询用户列表(逻辑省略) res.json({ users: [], total: 0 }); // 复杂响应,用 res.json }); // IP 拉黑校验中间件 app.use((req, res, next) => { const blockedIPs = ['192.168.1.100', '10.0.0.5']; // 拉黑 IP 列表 if (blockedIPs.includes(req.ip)) { // 拉黑 IP→403 Forbidden return res.sendStatus(403); } next(); });场景 3:资源不存在(简单提示)
当用户请求的资源(如文章、用户、文件)不存在,且无需额外说明 “为什么不存在”,用 res.sendStatus(404) 快速响应。
示例:文章详情接口(资源不存在)
// 模拟文章数据库 const articles = [ { id: 1, title: 'Express 教程' }, { id: 2, title: 'Node.js 实战' } ]; // 文章详情接口 app.get('/api/articles/:id', (req, res) => { const articleId = parseInt(req.params.id); const article = articles.find(a => a.id === articleId); if (article) { // 资源存在→返回 JSON 数据(复杂响应,用 res.json) res.json({ success: true, data: article }); } else { // 资源不存在→简单响应,用 sendStatus(404) res.sendStatus(404); // 响应体是 'Not Found' } });场景 4:自定义业务状态码(非标准 HTTP 码)
当需要用 “非标准 HTTP 状态码” 表示业务状态(如 “数据待审核”“余额不足”),res.sendStatus 会自动将状态码作为文本响应体。
示例:订单支付接口(余额不足)
// 模拟用户余额查询 const getUserBalance = (userId) => { const users = { 1001: 50, 1002: 200 }; // 用户 ID→余额 return users[userId] || 0; }; // 订单支付接口 app.post('/api/order/pay', (req, res) => { const { userId, orderAmount } = req.body; const balance = getUserBalance(userId); if (balance >= orderAmount) { // 余额足够→支付成功,返回订单信息(复杂响应) res.json({ success: true, orderId: 'ORD123456' }); } else { // 余额不足→自定义业务状态码 2001,响应体是 '2001' res.sendStatus(2001); } });前端处理:
前端收到状态码 2001 和响应体 '2001',可映射为 “余额不足” 提示,实现业务状态的传递。
三、避坑注意事项(这 5 个错误 90% 的人会犯)
1. 不能自定义响应体(想改文本?用 res.status + send)
res.sendStatus 的响应体是 固定的标准文本(如 400→'Bad Request'),无法修改。若需自定义文本(如 “参数错误:请传入用户名”),必须用 res.status + send:
❌ 错误用法(想自定义 400 响应体):
// 无效!sendStatus 会忽略自定义文本,响应体仍是 'Bad Request' res.sendStatus(400, '请传入用户名');✅ 正确用法(用 res.status + send):
res.status(400).send('参数错误:请传入用户名');2. 不能重复调用(响应已结束,再调用会报错)
res.sendStatus 内部会调用 res.send() 结束响应,重复调用会触发 “响应已发送” 错误(Can't set headers after they are sent):
❌ 错误用法(重复调用):
app.get('/test', (req, res) => { res.sendStatus(200); // 错误:响应已结束,再调用会抛错 res.sendStatus(400); });✅ 正确用法(仅调用一次):
app.get('/test', (req, res) => { if (req.query.valid) { res.sendStatus(200); } else { res.sendStatus(400); } });3. 复杂响应(JSON/HTML)不能用它(必须用 res.status + json)
若需返回 JSON 格式的错误信息(如 { success: false, msg: '权限不足' }),res.sendStatus 无法满足,必须用 res.status + res.json:
❌ 错误用法(用 sendStatus 返回 JSON):
// 无效!响应体是 'Forbidden',不是 JSON res.sendStatus(403).json({ success: false, msg: '权限不足' });✅ 正确用法(用 res.status + json):
res.status(403).json({ success: false, msg: '权限不足' });4. 非标准状态码的文本是 “状态码本身”(不是自定义描述)
若传入非标准 HTTP 状态码(如 2001、3000),res.sendStatus 的响应体是 “状态码的字符串形式”(如 2001→'2001'),不是你期望的 “余额不足”:
res.sendStatus(2001); // 响应体是 '2001',不是 '余额不足'解决办法:
需自定义描述时,用 res.status + send:
res.status(2001).send('余额不足');5. 不要和 res.end() 一起用(会导致响应体被覆盖)
res.end() 会清空响应体,若在 res.sendStatus 后调用 res.end(),会导致响应体为空:
❌ 错误用法(sendStatus 后调用 end):
app.get('/test', (req, res) => { res.sendStatus(200); res.end(); // 响应体被清空,客户端收到空响应 });✅ 正确用法(无需调用 end):
app.get('/test', (req, res) => { res.sendStatus(200); // 内部已调用 send,无需再 end });四、总结:res.sendStatus 的 “能用” 与 “不能用”
能用的场景(3 类)
- 简单成功响应:无数据返回(如注销、删除)→ 用 res.sendStatus(200);
- 权限 / 资源错误:无额外说明(如 403 无权限、404 不存在)→ 用 res.sendStatus(403/404);
- 自定义业务状态码:仅需传递状态码(如 2001 余额不足)→ 用 res.sendStatus(2001)。
不能用的场景(3 类)
- 需自定义响应体:如 “参数错误:请传入用户名”→ 用 res.status(400).send(...);
- 需返回 JSON/HTML:如 API 错误的 JSON 信息→ 用 res.status(500).json(...);
- 需后续操作响应:如设置响应头后再发响应→ 用 res.status(200).set(...)。
res.sendStatus 的唯一价值是 “简化代码”—— 把 “设状态码 + 发文本” 的两步操作变成一步,适合简单场景。但它的灵活性极低,复杂场景必须用 res.status 配合 send/json。
记住:能用 res.sendStatus 的场景,都能用 res.status + send 替代;但能用 res.status + send 的场景,不一定能用 res.sendStatus 替代。
res.set(field [, value])
设置响应对象的HTTP头部
field为value。为了一次设置多个值,那么可以传递一个对象为参数。res.set('Content-Type', 'text/plain'); res.set({ 'Content-Type':'text/plain', 'Content-Length':'123', 'ETag':'123456' })其和
res.header(field [,value])效果一致。一、先看透本质:res.set 是 “响应头设置工具”,与 res.header 完全等价
res.set(field [, value]) 是 Express 中 设置 HTTP 响应头的基础方法,核心作用是:
- 支持 “单个响应头” 设置(传两个参数:头名称 + 头值);
- 支持 “多个响应头” 批量设置(传一个对象:键为头名称,值为头值);
- 与 res.header(field [, value]) 完全等价(仅是别名,功能无任何差异),开发中可互换使用。
关键关联:与 res.get 的 “设置 - 获取” 闭环
你之前学过的 res.get(field) 是 “获取已设置的响应头”,而 res.set 是 “设置响应头”,两者形成完整的响应头操作流程:
// 1. 用 res.set 设置响应头 res.set('Content-Type', 'text/plain'); res.set({ 'Cache-Control': 'max-age=3600', 'X-App-Version': 'v1.0.0' }); // 2. 用 res.get 获取已设置的响应头 const contentType = res.get('Content-Type'); // → "text/plain" const cacheControl = res.get('Cache-Control'); // → "max-age=3600"二、高频实战场景(这些情况必须用它)
res.set 的核心价值是 “自定义响应头”,覆盖缓存、跨域、自定义业务信息等场景,下面是开发中最常用的 5 类场景:
场景 1:覆盖默认 Content-Type(修正响应类型)
当 res.send/res.sendFile 自动设置的 Content-Type 不符合需求时(如希望纯文本用 text/plain 而非 text/html),用 res.set 手动覆盖。
示例:强制纯文本响应(覆盖 res.send 的默认设置)
app.get('/plain-text', (req, res) => { // 1. 手动设置 Content-Type 为 text/plain(res.send 默认会设为 text/html) res.set('Content-Type', 'text/plain'); // 2. 发送文本,浏览器会按纯文本显示(不解析 <h1> 标签) res.send('<h1>这是纯文本,不是 HTML</h1>'); }); // 示例2:设置 JSON 响应,但强制指定字符编码 app.get('/json-with-charset', (req, res) => { // 手动设置 Content-Type 为 application/json; charset=utf-8(确保中文不乱码) res.set('Content-Type', 'application/json; charset=utf-8'); res.send({ 用户名: '张三', 年龄: 28 }); });场景 2:配置缓存策略(控制浏览器缓存)
通过 res.set 设置 Cache-Control、Expires 等缓存头,精细化控制资源的缓存行为,补充 res.sendFile 中 maxAge 的不足(如设置 “不缓存”“私有缓存”)。
示例:设置资源 “不缓存”(覆盖默认缓存)
// 动态接口(如用户信息):禁止浏览器缓存,每次都请求最新数据 app.get('/api/user/info', (req, res) => { // 设置缓存头:禁止缓存(no-store)、每次验证(no-cache) res.set({ 'Cache-Control': 'no-store, no-cache, must-revalidate', 'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT' // 过期时间设为过去,强制不缓存 }); // 返回用户信息(每次请求都最新) res.json({ userId: req.signedCookies.userId, username: 'admin' }); }); // 示例2:设置“私有缓存”(仅客户端缓存,代理服务器不缓存) app.get('/user/dashboard', (req, res) => { res.set('Cache-Control', 'private, max-age=1800'); // 客户端缓存 30 分钟 res.send('用户仪表盘内容(仅当前用户可见,代理不缓存)'); });场景 3:跨域资源共享(CORS 配置)
当前端页面与后端接口不在同一域名时,需用 res.set 设置 CORS 相关头(如 Access-Control-Allow-Origin),允许前端跨域请求。
示例:基础 CORS 配置(允许指定域名跨域)
// 全局 CORS 中间件:允许 http://localhost:3000(前端)跨域请求 app.use((req, res, next) => { // 1. 允许的前端域名(生产环境替换为真实域名,如 https://example.com) res.set('Access-Control-Allow-Origin', 'http://localhost:3000'); //d res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); // 3. 允许的自定义请求头(如 X-Token) res.set('Access-Control-Allow-Headers', 'Content-Type, X-Token'); next(); }); // 跨域接口:前端可从 http://localhost:3000 访问 app.get('/api/cross-domain/data', (req, res) => { res.json({ success: true, data: '跨域数据' }); });场景 4:设置自定义业务头(传递额外信息)
在响应头中添加自定义业务标识(如请求 ID、接口版本、服务器节点),方便调试、监控或前端业务逻辑处理(自定义头建议以 X- 开头,符合 HTTP 规范)。
示例:传递请求 ID 和接口版本
// 生成唯一请求 ID(模拟分布式追踪场景) const generateRequestId = () => { return 'req-' + Math.random().toString(36).substr(2, 10); }; // 全局中间件:给每个响应添加自定义头 app.use((req, res, next) => { const reqId = generateRequestId(); // 1. 自定义头:请求唯一标识(用于日志追踪) res.set('X-Request-ID', reqId); // 2. 自定义头:接口版本(方便前端适配不同版本) res.set('X-Api-Version', 'v2.1.0'); // 3. 自定义头:服务器节点(用于集群部署监控) res.set('X-Server-Node', 'node-123'); next(); }); // 接口响应中会包含上述自定义头 app.get('/api/data', (req, res) => { res.send('业务数据'); });前端受益:
前端可通过 response.headers.get('X-Request-ID') 获取请求 ID,若接口报错,可将该 ID 反馈给后端,快速定位日志。
场景 5:配合 res.sendFile 设置特殊头(补充文件元信息)
在 res.sendFile 传输文件前,用 res.set 设置文件相关的特殊头(如文件 MD5、下载次数),补充 res.sendFile 选项中 headers 的配置。
示例:传输文件时添加 MD5 和下载次数
const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); // 用于计算文件 MD5 // 计算文件 MD5(用于校验文件完整性) const getFileMd5 = (filePath) => { const buffer = fs.readFileSync(filePath); return crypto.createHash('md5').update(buffer).digest('hex'); }; // 下载文件接口:添加 MD5 和下载次数头 app.get('/download/with-meta/:filename', (req, res) => { const fileName = req.params.filename; const file path = path.join(__dirname, 'public/files', fileName); // 1. 计算文件 MD5 const fileMd5 = getFileMd5(filePath); // 2. 模拟下载次数(从数据库获取) const downloadCount = 125; // 3. 设置文件相关头 res.set({ 'X-File-MD5': fileMd5, // 文件 MD5(前端可校验完整性) 'X-Download-Count': downloadCount, // 下载次数(前端显示) 'Content-Disposition': 'inline' // 浏览器预览(非下载) }); // 4. 传输文件 res.sendFile(filePath, (err) => { if (err) res.status(404).send('文件不存在'); }); });三、避坑注意事项(这 6 个错误必须避免)
- 响应头名称不区分大小写(HTTP 规范)
HTTP 响应头名称是 不区分大小写 的,res.set('content-type', 'text/plain') 和 res.set('Content-Type', 'text/plain') 效果完全一致,但建议用 “首字母大写,其余小写” 的规范写法(如 Content-Type、Cache-Control),提高可读性。
// 以下三种写法等价 res.set('content-type', 'text/plain'); res.set('Content-Type', 'text/plain'); res.set('CONTENT-TYPE', 'text/plain');- 重复设置会覆盖(多值头需用 res.append)
res.set 重复设置同一个头时,后设置的值会覆盖前一个(单值头特性);若需设置 “多值头”(如 Set-Cookie、Access-Control-Allow-Headers),需用 res.append(追加而非覆盖)。
❌ 错误:重复设置覆盖(仅最后一个生效)
res.set('Cache-Control', 'max-age=3600'); res.set('Cache-Control', 'no-cache'); // 覆盖前一个,最终是 no-cache✅ 正确:多值头用 res.append
// Set-Cookie 是多值头,需追加(每个 append 新增一个 Cookie) res.append('Set-Cookie', 'userId=1001; httpOnly=true'); res.append('Set-Cookie', 'role=admin; httpOnly=true'); // 最终响应头会有两个 Set-Cookie 条目- 设置头必须在 “响应发送前”(否则无效)
res.set 必须在 res.send/res.json/res.sendFile/res.end 等 “发送响应” 的方法之前调用,若在之后调用,响应已发送,头设置会无效(甚至报错)。
❌ 错误:响应发送后设置头(无效)
app.get('/test', (req, res) => { res.send('响应已发送'); res.set('Content-Type', 'text/plain'); // 无效,响应已结束 });✅ 正确:发送前设置头
app.get('/test', (req, res) => { res.set('Content-Type', 'text/plain'); // 发送前设置,有效 res.send('响应已发送'); });- 避免手动设置 Content-Length(自动处理更可靠)
Content-Length(响应体长度)通常由 Express 自动计算并设置(如 res.send/res.json 会自动计算),手动设置易出错(如长度与实际响应体不匹配,导致客户端解析异常),非特殊需求不建议手动设置。
❌ 不推荐:手动设置 Content-Length
res.set('Content-Length', '10'); // 若实际响应体长度不是 10,会出错 res.send('123456789'); // 长度是 9,与设置的 10 不匹配,客户端可能截断✅ 推荐:自动处理(Express 会帮你算)
res.send('123456789'); // 自动设置 Content-Length: 9- 自定义头建议以 X- 开头(避免与标准头冲突)
虽然 HTTP 规范不强制,但自定义业务头建议以 X- 为前缀(如 X-Request-ID、X-Api-Version),明确区分 “标准头” 和 “自定义头”,避免未来与新增的标准头冲突。
❌ 不推荐:无 X- 前缀的自定义头(可能与标准头冲突)
res.set('Request-ID', 'req-123'); // 不推荐,无 X- 前缀✅ 推荐:带 X- 前缀的自定义头
res.set('X-Request-ID', 'req-123'); // 推荐,明确是自定义头- 部分头由浏览器 / 服务器自动控制(不建议手动改)
有些响应头由浏览器或 Express/Node 底层自动管理,手动修改可能导致异常,如:
- Date:响应发送时间,自动设置;
- Connection:连接状态(如 keep-alive),自动管理;
- Transfer-Encoding:分块传输编码,大响应自动启用。
除非有明确的底层优化需求,否则不建议手动修改这些头。
四、总结:res.set 的核心价值与使用准则
res.set 是 Express 中 “最基础、最灵活的响应头设置工具”,核心解决:
- 覆盖默认响应头(如修正 Content-Type、缓存策略);
- 配置跨域、自定义业务信息等特殊需求;
- 与 res.get 配合,实现响应头的 “设置 - 获取” 闭环。
关键关系澄清
方法 与 res.set 的关系 适用场景 res.header 完全等价(别名),功能无差异 与 res.set 互换,按团队规范选择 res.get “获取” 响应头,与 res.set 形成 “设置 - 获取” 闭环 验证头设置、调试、动态逻辑 res.append “追加” 多值头(如 Set-Cookie),res.set 是 “覆盖” 多值头场景(如多个 Cookie) - 基础设置用 res.set:单个 / 多个头的覆盖设置(如 Content-Type、CORS);
- 多值头用 res.append:如 Set-Cookie、Access-Control-Allow-Headers;
- 设置时机要靠前:必须在 res.send/res.json 等发送响应前调用;
- 自定义头加 X- 前缀:避免与标准头冲突,提高可读性;
- 自动头不手动改:如 Content-Length、Date 等,交给 Express 自动处理。
记住:res.set 是响应头的 “万能设置工具”,几乎所有响应头相关的需求都能通过它实现,是开发中不可或缺的基础方法。
res.status(code)
使用这个方法来设置响应对象的HTTP status。其是Node中response.statusCode的一个连贯性的别名。
res.status(403).end(); res.status(400).send('Bad Request'); res.status(404).sendFile('/absolute/path/to/404.png');一、先看透本质:res.status 是 “状态码配置工具”,支持链式调用
res.status(code) 是 Express 中 设置 HTTP 响应状态码的基础方法,核心作用是:
- 直接设置响应的状态码(如 200、404、500),是 Node 原生 response.statusCode 的 “链式别名”(调用后返回 res 对象本身,支持链式调用);
- 仅负责 “设置状态码”,不发送响应体、不结束响应流程,需配合 res.send/res.json/res.end 等方法完成响应。
关键对比:res.status vs res.sendStatus(最易混淆的两个方法)
维度 res.status(code) res.sendStatus(code) 核心能力 仅设置状态码(无响应体,不结束响应) 设置状态码 + 发送对应文本响应体(结束响应) 响应体 无(需后续手动添加) 固定文本(如 404→'Not Found') 链式调用 支持(返回 res 对象) 不支持(已结束响应,返回 undefined) 灵活性 高(可自定义响应体类型 / 内容) 低(仅固定文本响应) 示例 res.status(404).send('页面丢了') res.sendStatus(404)(响应体是 'Not Found') 一句话总结:res.status 是 “灵活的状态码配置器”,res.sendStatus 是 “固定的状态码 + 文本快捷工具”—— 前者适合需要自定义响应体的场景,后者适合简单文本响应。
二、高频实战场景(所有需要自定义状态码的场景都离不开它)
res.status 的核心价值是 “配合其他响应方法,实现自定义状态码 + 自定义响应体”,覆盖错误处理、成功场景、特殊业务状态等几乎所有开发场景:
场景 1:错误处理(最核心场景,覆盖 4xx/5xx 状态码)
开发中 90% 的 res.status 用于错误响应,需根据错误类型设置对应状态码,并返回自定义错误信息(如 JSON 格式的错误详情)。
示例 1:参数验证失败(400 Bad Request)
结合你之前学的 Joi 验证,参数错误时返回 400 状态码 + 自定义 JSON 错误:
const Joi = require('joi'); // 登录参数验证规则 const loginSchema = Joi.object({ username: Joi.string().required(), password: Joi.string().min(6).required() }); app.post('/login', (req, res) => { // Joi 验证参数 const { error } = loginSchema.validate(req.body); if (error) { // 1. 设置 400 状态码(参数错误) // 2. 返回 JSON 格式的错误信息(自定义响应体) return res.status(400).json({ success: false, msg: '参数错误:' + error.details[0].message, code: 'INVALID_PARAM' // 自定义错误码,前端可据此处理 }); } // 验证通过,继续登录逻辑... res.status(200).json({ success: true, msg: '登录成功' }); });示例 2:权限不足(403 Forbidden)与资源不存在(404 Not Found)
// 权限中间件:仅管理员可访问 const requireAdmin = (req, res, next) => { const role = req.signedCookies.role || 'user'; if (role !== 'admin') { // 403 状态码 + 自定义文本响应 return res.status(403).send('权限不足:仅管理员可操作'); } next(); }; // 管理员接口:权限不足返回 403 app.get('/admin/users', requireAdmin, (req, res) => { res.json({ success: true, data: [] }); }); // 资源不存在:返回 404 + JSON 错误 app.get('/api/articles/:id', (req, res) => { const article = findArticleById(req.params.id); // 模拟查询文章 if (!article) { return res.status(404).json({ success: false, msg: `ID为${req.params.id}的文章不存在`, code: 'ARTICLE_NOT_FOUND' }); } res.json({ success: true, data: article }); });示例 3:服务器错误(500 Internal Server Error)
捕获代码异常时,返回 500 状态码,避免暴露敏感错误信息:
app.get('/api/unsafe-operation', (req, res) => { try { // 模拟可能抛错的操作(如数据库查询失败) throw new Error('数据库连接超时'); } catch (err) { // 记录错误日志(仅后端可见,不返回给前端) console.error('服务器错误:', err); // 返回 500 状态码 + 友好提示(不暴露具体错误) res.status(500).json({ success: false, msg: '服务器内部错误,请稍后重试', code: 'SERVER_ERROR' }); } });场景 2:成功场景的特殊状态码(非默认 200)
除了默认的 200(成功),部分成功场景需用特殊状态码(如 201 资源创建成功、204 无内容成功),配合 res.status 实现。
示例 1:资源创建成功(201 Created)
符合 RESTful 规范:创建资源(如 POST 请求新增文章)成功后,返回 201 状态码 + 新资源信息:
// 新增文章接口 app.post('/api/articles', (req, res) => { const { title, content } = req.body; // 模拟数据库创建文章(返回新文章 ID) const newArticle = { id: Date.now(), // 用时间戳模拟唯一 ID title, content, createTime: new Date() }; // 201 状态码(资源创建成功)+ 返回新资源 JSON res.status(201).json({ success: true, msg: '文章创建成功', data: newArticle }); });示例 2:无内容成功(204 No Content)
删除资源成功后无需返回内容,用 204 状态码 + 空响应:
// 删除文章接口 app.delete('/api/articles/:id', (req, res) => { const articleId = req.params.id; // 模拟数据库删除文章 const deleteSuccess = true; if (deleteSuccess) { // 204 状态码(无内容成功)+ 结束响应(res.end()) res.status(204).end(); } else { res.status(404).json({ success: false, msg: '文章不存在' }); } });场景 3:配合其他响应方法(链式调用全场景)
res.status 支持链式调用,可与 res.send/res.json/res.sendFile/res.redirect 等几乎所有响应方法结合,覆盖文本、JSON、文件、重定向等需求。
结合的方法 示例代码(场景) 作用 res.send res.status(400).send('参数错误,请检查') 文本响应 + 自定义状态码 res.json res.status(201).json({ id: 123 }) JSON 响应 + 特殊成功状态码(201) res.sendFile res.status(404).sendFile('./404.png') 404 页面(图片)+ 404 状态码 res.redirect res.status(301).redirect('/new-url') 301 永久重定向 + 新地址 示例:404 页面用图片响应(替代文本)
const path = require('path'); // 404 中间件:返回图片+404状态码 app.use((req, res) => { const notFoundImagePath = path.join(__dirname, 'public', '404.png'); // 链式调用:设置 404 状态码 → 发送图片文件 res.status(404).sendFile(notFoundImagePath, (err) => { if (err) { // 图片不存在时,降级返回文本 res.status(404).send('404 - 页面不存在'); } }); });场景 4:自定义业务状态码(非标准 HTTP 码)
虽然 HTTP 标准状态码是 100-599,但部分业务需用自定义状态码(如 2001 余额不足、2002 数据待审核),res.status 也支持设置(需前端配合解析)。
示例:订单支付余额不足(自定义 2001 状态码)
app.post('/api/order/pay', (req, res) => { const { userId, amount } = req.body; const balance = getUserBalance(userId); // 模拟查询余额 if (balance < amount) { // 自定义 2001 状态码(余额不足)+ JSON 响应 res.status(2001).json({ success: false, msg: '余额不足,当前余额:' + balance, code: 'INSUFFICIENT_BALANCE' }); } else { // 支付成功:200 状态码 res.json({ success: true, msg: '支付成功' }); } });三、避坑注意事项(这 4 个错误必须避免)
1. 状态码需在 “发送响应前” 设置(否则无效)
res.status 必须在 res.send/res.json/res.end 等 “发送响应” 的方法之前调用,若在之后调用,响应已发送,状态码设置无效(甚至报错)。
❌ 错误:发送响应后设置状态码
app.get('/test', (req, res) => { res.send('响应已发送'); res.status(404); // 无效,状态码仍是默认 200 });✅ 正确:发送前设置状态码
app.get('/test', (req, res) => { res.status(404).send('响应已发送'); // 有效,状态码是 404 });2. 状态码范围建议遵循 HTTP 标准(100-599)
虽然 res.status 支持设置任意数字(如 2001、9999),但非标准状态码可能导致部分客户端(如旧浏览器、接口测试工具)解析异常,建议优先使用 HTTP 标准状态码:
- 1xx:信息响应(如 100 Continue);
- 2xx:成功响应(如 200 OK、201 Created);
- 3xx:重定向(如 301 永久重定向、302 临时重定向);
- 4xx:客户端错误(如 400 参数错、403 权限错、404 资源错);
- 5xx:服务器错误(如 500 内部错、503 服务不可用)。
3. 避免重复设置状态码(后设置的覆盖前一个)
若多次调用 res.status,最后一次调用的状态码生效(覆盖前一个),需注意代码执行顺序。
app.get('/test', (req, res) => { res.status(404); // 被覆盖 res.status(500); // 最终生效的状态码是 500 res.send('服务器错误'); });4. res.status 不结束响应,必须配合发送方法
res.status 仅设置状态码,不发送响应体、不结束响应流程,若仅调用 res.status(404) 而不调用 res.send/res.end,客户端会一直等待超时。
❌ 错误:仅设置状态码,不发送响应
app.get('/test', (req, res) => { res.status(404); // 客户端一直等待,最终超时 });✅ 正确:设置状态码后,调用发送方法
app.get('/test', (req, res) => { res.status(404).end(); // 结束响应,客户端收到 404 });四、总结:res.status 的核心价值与使用准则
res.status 是 Express 中 “响应状态码的基础配置核心”,所有需要自定义状态码的场景都离不开它,核心解决:
- 灵活适配错误场景:4xx 客户端错误、5xx 服务器错误,返回自定义响应体;
- 支持特殊成功状态码:201 资源创建、204 无内容,符合 RESTful 规范;
- 链式调用串联响应流程:与 res.send/res.json/res.sendFile 等无缝结合,简化代码。
- 错误场景优先用 4xx/5xx:
- 客户端问题(参数错、权限错、资源错)→ 4xx;
- 服务器问题(代码错、数据库错)→ 5xx;
- 成功场景按需选择:
- 普通成功 → 200(可省略,默认就是 200);
- 资源创建 → 201;
- 无内容成功 → 204;
- 链式调用注意顺序:res.status(code) 必须在发送响应方法(send/json 等)之前;
- 区分 res.status 与 res.sendStatus:
- 需自定义响应体 → 用 res.status(code).xxx;
- 简单文本响应 → 用 res.sendStatus(code)。
记住:res.status 是 “响应状态的总开关”,几乎所有非默认状态码的响应,都需要通过它来配置 —— 它是 Express 响应体系中最基础、最核心的方法之一。
res.type(type)
程序将设置
Content-TypeHTTP头部的MIME type,如果这个设置的type能够被mime.lookup解析成正确的Content-Type。如果type中包含了/字符,那么程序会直接设置Content-Type为type。res.type('.html'); // => 'text/html' res.type('html'); // => 'text/html' res.type('json'); // => 'application/json' res.type('application/json'); // => 'application/json' res.type('png'); // => image/png:一、先看透本质:res.type 是 “简化 Content-Type 设置” 的工具
res.type(type) 是 Express 中 快速设置 Content-Type 响应头 的便捷方法,核心逻辑是:
- 接收 type 参数(可以是文件扩展名,如 html/png;也可以是完整 MIME 类型,如 text/plain);
- 若 type 不含 /(如 html/.json),则通过 mime.lookup 工具(依赖 node-mime 库)自动映射为标准 MIME 类型;
- 若 type 含 /(如 application/json/image/png),则直接将其设为 Content-Type 头的值;
- 仅设置 Content-Type,不发送响应、不结束流程,需配合 res.send/res.end 等方法完成响应。
关键对比:res.type vs res.set ('Content-Type', ...)(最易混淆)
维度 res.type(type) res.set('Content-Type', value) 核心能力 简化 MIME 类型设置(支持扩展名映射) 完整控制 Content-Type 头(含 charset 等) 使用便捷性 高(如 res.type('html') 即设 text/html) 低(需写完整 MIME 字符串,如 text/html; charset=utf-8) 字符编码(charset) 不自动添加(需额外用 res.set 补充) 可直接包含(如 application/json; charset=utf-8) 示例 res.type('json') → application/json res.set('Content-Type', 'application/json; charset=utf-8') 一句话总结:res.type 是 “MIME 类型快捷设置键”,适合快速指定类型;res.set('Content-Type') 是 “完整头部配置器”,适合需自定义 charset 等细节的场景。
二、高频实战场景(所有需快速设置 Content-Type 的场景都用它)
res.type 的核心价值是 “简化代码”,覆盖 HTML、JSON、图片、下载文件等常见类型设置,尤其适合通过 “文件扩展名” 快速映射 MIME 类型:
场景 1:返回 HTML 内容(覆盖 res.send 的默认行为)
当用 res.send 发送 HTML 字符串时,默认会设 Content-Type: text/html,但需明确类型或修正默认值时,用 res.type('html') 更直观。
示例:明确设置 HTML 类型
app.get('/custom-html', (req, res) => { // 明确设置 Content-Type: text/html(虽默认也是,但代码更易读) res.type('html'); // 发送 HTML 内容,浏览器正常渲染 res.send(` <div style="color: red;"> 这是通过 res.type('html') 设置的 HTML 内容 </div> `); });场景 2:返回 JSON 字符串(而非用 res.json)
当需手动拼接 JSON 字符串(而非传对象)时,res.send 会默认设 Content-Type: text/plain,需用 res.type('json') 修正为 JSON 类型,确保客户端正确解析。
示例:手动拼接 JSON 字符串
app.get('/manual-json', (req, res) => { // 1. 手动拼接 JSON 字符串(模拟复杂场景下的自定义 JSON) const userJson = '{"id":1001,"username":"admin","role":"super"}'; // 2. 设置 Content-Type: application/json(否则客户端按文本解析) res.type('json'); // 3. 发送 JSON 字符串,客户端可正常用 JSON.parse 解析 res.send(userJson); }); // 对比:若不设 res.type('json'),客户端收到的 Content-Type 是 text/plain,需手动转换场景 3:发送图片 / 二进制文件(明确文件类型)
用 res.sendFile 发送图片时,虽会自动按扩展名设 Content-Type,但需手动覆盖类型(如将 .png 设为 image/x-png)或明确类型时,用 res.type 更灵活。
示例:发送图片并明确类型
const path = require('path'); app.get('/custom-image', (req, res) => { const imagePath = path.join(__dirname, 'public', 'logo.png'); // 1. 手动设置 Content-Type: image/png(覆盖自动设置,确保一致性) res.type('png'); // 等价于 res.type('image/png') // 2. 发送图片文件,浏览器按图片渲染 res.sendFile(imagePath, (err) => { if (err) res.status(404).send('图片不存在'); }); }); // 示例2:发送自定义二进制文件(如 .bin 格式,映射为 application/octet-stream) app.get('/binary-file', (req, res) => { const binPath = path.join(__dirname, 'files', 'data.bin'); res.type('bin'); // 自动映射为 application/octet-stream res.sendFile(binPath); });场景 4:返回纯文本(避免被解析为 HTML)
用 res.send 发送纯文本时,默认会设 Content-Type: text/html,导致特殊字符(如 <)被浏览器解析为标签,需用 res.type('text') 设为 text/plain。
示例:返回含特殊字符的纯文本
app.get('/plain-text-with-tags', (req, res) => { // 1. 设置 Content-Type: text/plain(纯文本,不解析 HTML 标签) res.type('text'); // 等价于 res.type('text/plain') // 2. 发送含 < 标签的文本,浏览器会显示原始字符(不渲染为标签) res.send('这是纯文本中的 <h1> 标签,不会被渲染'); }); // 对比:不设 res.type('text'),浏览器会将 <h1> 解析为标题标签,显示“这是纯文本中的 ”(标签被渲染)场景 5:配合下载文件(指定下载类型)
用 res.download 下载文件时,若需自定义下载文件的 Content-Type(而非默认按扩展名映射),可先用 res.type 设类型,再调用 res.download。
示例:下载 CSV 文件并指定类型
app.get('/download-csv', (req, res) => { const csvPath = path.join(__dirname, 'exports', 'data.csv'); // 1. 设 Content-Type: text/csv(确保下载时浏览器识别为 CSV 类型) res.type('csv'); // 自动映射为 text/csv // 2. 触发下载,文件名设为“用户数据.csv” res.download(csvPath, '用户数据.csv', (err) => { if (err) res.status(404).send('CSV 文件不存在'); }); });三、避坑注意事项(这 5 个细节必须掌握)
1. 扩展名可带点或不带点(效果一致)
res.type 对扩展名的处理很灵活,带 .(如 .html/.json)或不带(如 html/json)均可,最终都会映射为正确的 MIME 类型:
res.type('html'); // → Content-Type: text/html res.type('.html'); // → 同上,效果一致 res.type('json'); // → Content-Type: application/json res.type('.json'); // → 同上,效果一致2. 含 / 的 type 会直接作为 Content-Type
若 type 中包含 /(如 text/plain/image/jpeg),res.type 会跳过 MIME 映射,直接将其设为 Content-Type 头的值,适合指定非标准 MIME 类型:
// 标准 MIME 类型:直接设置 res.type('text/plain'); // → Content-Type: text/plain // 非标准 MIME 类型:适合自定义文件格式 res.type('application/x-custom'); // → Content-Type: application/x-custom3. 不自动添加字符编码(charset),需手动补充
res.type 仅设置 MIME 类型,不包含 charset(如 utf-8),若需指定字符编码(避免中文乱码),需配合 res.set 补充:
// 错误:仅设类型,中文可能乱码(Content-Type: application/json) res.type('json').send({ 用户名: '张三' }); // 正确:补充 charset(Content-Type: application/json; charset=utf-8) res.type('json').set('charset', 'utf-8').send({ 用户名: '张三' }); // 或直接用 res.set 写完整头部(更简洁) res.set('Content-Type', 'application/json; charset=utf-8').send({ 用户名: '张三' });4. 优先级高于自动设置的 Content-Type
若先通过 res.type 设类型,后续调用 res.send/res.sendFile 等方法时,res.type 的设置会覆盖自动生成的 Content-Type:
// 1. 先设 type 为 text/plain res.type('text'); // 2. 发送 HTML 字符串,Content-Type 仍是 text/plain(覆盖默认的 text/html) res.send('<h1>这会被当作纯文本显示</h1>');5. 不支持的扩展名会映射为 application/octet-stream
若 type 是无法识别的扩展名(如 .abc/xyz),mime.lookup 会默认映射为 application/octet-stream(二进制流类型),浏览器会触发下载而非预览:
// 无法识别的扩展名 .abc,映射为 application/octet-stream res.type('abc').send('未知格式内容'); // 响应头:Content-Type: application/octet-stream(浏览器会下载该内容)四、总结:res.type 的核心价值与使用准则
res.type 是 Express 中 “快速设置 Content-Type 的快捷工具”,核心解决:
- 简化 MIME 类型设置:通过扩展名(如 html/png)快速映射,无需记完整 MIME 字符串;
- 提高代码可读性:res.type('json') 比 res.set('Content-Type', 'application/json') 更简洁,意图更明确;
- 灵活覆盖自动类型:在 res.send/res.sendFile 自动设置不符合需求时,快速修正。
- 快速指定常见类型用 res.type:
- HTML → res.type('html');
- JSON → res.type('json');
- 图片 → res.type('png')/res.type('jpg');
- 纯文本 → res.type('text')。
- 需指定 charset 用 res.set:
如 res.set('Content-Type', 'application/json; charset=utf-8'),或 res.type('json').set('charset', 'utf-8')。
- 非标准 MIME 类型直接传完整字符串:
如 res.type('application/x-custom')(含 /,直接设置)。
- 优先级注意:
res.type 会覆盖后续方法的自动类型设置,需注意调用顺序(先设类型,再发响应)。
记住:res.type 是 “效率工具”,适合 80% 的常见类型场景;res.set('Content-Type') 是 “全能工具”,适合需自定义 charset 或非标准类型的场景 —— 两者配合使用,覆盖所有 Content-Type 设置需求。
res.vary(field)
一、核心定义
“控制缓存判断依据” 的工具
res.vary(field) 是 Express 中 添加 / 追加 Vary 应答头部 的专用方法,核心作用是:
- 接收 field 参数(如 User-Agent、Accept、Content-Type),告知缓存服务器(CDN / 代理 / 浏览器缓存):“判断两个请求是否相同,需同时比对这些字段”;
- 若当前响应无 Vary 头部,直接添加 Vary: field;若已有,则追加字段(而非覆盖);
- 仅控制 Vary 头部,不影响响应内容 / 状态码,需配合 res.render/res.send/res.type 等方法使用,且必须在响应发送前调用。
关键前提:Vary 头部的核心意义(用户 PS 补充的深化)
Vary 头部是 缓存的 “判断标尺”,解决 “相同 URL 但不同请求头,是否返回同一缓存” 的问题:
- 例 1:若响应头含 Vary: User-Agent,CDN 会认为 “Chrome 请求” 和 “Safari 请求” 是不同请求,需分别缓存(避免 PC 端缓存返回给移动端);
- 例 2:若响应头无 Vary,CDN 会仅按 URL 判断,无论请求头如何,都返回同一缓存(可能导致内容不匹配,如返回 HTML 给需要 JSON 的客户端)。
关键对比:res.vary vs 手动设置 Vary(用 res.set)
维度 res.vary(field) res.set('Vary', value) 核心能力 追加 Vary 字段(支持多次调用) 覆盖 Vary 头部(单次设置,重复调用会覆盖) 重复调用效果 追加字段(如 res.vary('A').vary('B') → Vary: A,B) 覆盖前值(如 res.set('Vary','A').set('Vary','B') → Vary: B) 便捷性 高(无需手动拼接逗号,避免格式错误) 低(需手动拼接多字段,如 Vary: A,B) 示例 res.vary('User-Agent').vary('Accept') res.set('Vary', 'User-Agent, Accept') 二、实战核心场景(需添加 Vary 头部的典型情况)
Vary 头部仅在 “同一 URL 根据请求头返回不同内容” 时需要添加,以下是高频场景,部分结合你之前学的 res.type 方法:
场景 1:根据 User-Agent 返回不同内容(适配移动端 / PC 端)
当同一接口根据浏览器 / 设备类型(User-Agent)返回不同内容(如移动端简化页面、PC 端完整页面)时,需添加 Vary: User-Agent,避免缓存混淆。
示例:渲染文档时适配设备类型
app.get('/docs', (req, res) => { const userAgent = req.get('User-Agent'); // 判断是否为移动端(简化逻辑,实际需用ua-parser等库) const isMobile = /mobile|android|iphone/i.test(userAgent); // 1. 添加 Vary: User-Agent → 告知缓存按设备类型区分 res.vary('User-Agent'); // 2. 根据设备类型渲染不同模板,配合 res.type 确保内容类型正确 if (isMobile) { res.type('html'); // 明确HTML类型 res.render('docs-mobile'); // 移动端模板(简化版) } else { res.type('html'); res.render('docs-desktop'); // PC端模板(完整版) } });缓存效果:CDN 会分别缓存 “移动端 /docs” 和 “PC 端 /docs”,用户用手机访问时返回移动端缓存,PC 访问时返回 PC 缓存,避免错乱。
场景 2:根据 Accept 请求头返回不同内容(如 HTML/JSON)
当用 res.type 动态设置 Content-Type(如根据 Accept 请求头返回 HTML 或 JSON)时,需添加 Vary: Accept,告知缓存按 “请求接收的内容类型” 区分。
示例:结合 res.type 动态返回不同格式
app.get('/data', (req, res) => { const acceptType = req.accepts(['html', 'json']); // 判断客户端接受的类型 // 1. 添加 Vary: Accept → 告知缓存按Accept类型区分 res.vary('Accept'); // 2. 根据Accept类型用 res.type 设置Content-Type,返回不同内容 if (acceptType === 'html') { res.type('html'); // 设为text/html res.send('<h1>数据列表:ID1 - 张三</h1>'); // HTML内容 } else if (acceptType === 'json') { res.type('json'); // 设为application/json res.send({ id: 1, username: '张三' }); // JSON内容 } else { res.status(406).send('不支持的内容类型'); } });缓存效果:CDN 会分别缓存 “Accept: text/html” 和 “Accept: application/json” 的响应,客户端下次请求时,若 Accept 类型匹配则返回对应缓存,避免返回错误格式。
场景 3:检查并添加 Vary 头部(确保没有时才加)
若需避免重复添加相同的 Vary 字段(如中间件中可能多次调用),可先通过 res.get('Vary') 检查是否已存在,不存在再添加。
示例:安全添加 Vary: User-Agent(无则加)
// 全局中间件:处理设备适配,确保Vary: User-Agent存在 app.use((req, res, next) => { const existingVary = res.get('Vary'); // 检查Vary头部是否已包含User-Agent(不区分大小写) const hasUserAgentVary = existingVary ? existingVary.split(',').some(field => field.trim().toLowerCase() === 'user-agent') : false; // 没有则添加Vary: User-Agent if (!hasUserAgentVary) { res.vary('User-Agent'); } next(); }); // 后续接口会自动继承Vary: User-Agent(无需重复调用) app.get('/home', (req, res) => { res.type('html'); res.send('<h1>首页(自动适配设备)</h1>'); });场景 4:多维度区分缓存(同时添加多个 Vary 字段)
当同一 URL 需根据 “设备类型 + 内容类型” 双重判断时,可多次调用 res.vary 追加字段,最终 Vary 头部会包含所有维度。
示例:按 User-Agent + Accept 双重区分缓存
app.get('/profile', (req, res) => { const userAgent = req.get('User-Agent'); const acceptType = req.accepts(['html', 'json']); const isMobile = /mobile/i.test(userAgent); // 1. 追加Vary: User-Agent 和 Vary: Accept(多次调用res.vary) res.vary('User-Agent').vary('Accept'); // 2. 双重维度返回不同内容 if (isMobile && acceptType === 'html') { res.type('html'); res.send('<h1>移动端个人主页</h1>'); } else if (!isMobile && acceptType === 'html') { res.type('html'); res.send('<div style="width:800px">PC端个人主页</div>'); } else if (acceptType === 'json') { res.type('json'); res.send({ username: 'admin', isMobile }); } });缓存效果:CDN 会按 “User-Agent + Accept” 的组合缓存,共 4 种缓存(移动端 HTML、PC 端 HTML、移动端 JSON、PC 端 JSON),确保每个场景都返回正确内容。
三、避坑注意事项(Vary 头部的关键细节)
1. res.vary 是 “追加” 而非 “覆盖”,多次调用生效
多次调用 res.vary 会在 Vary 头部添加多个字段(用逗号分隔),而非覆盖之前的设置,这是与 res.set('Vary', ...) 最大的区别:
res.vary('User-Agent'); // Vary: User-Agent res.vary('Accept'); // Vary: User-Agent, Accept(追加,非覆盖) res.set('Vary', 'Content-Type'); // 覆盖为 Vary: Content-Type(谨慎使用)2. Vary 字段不区分大小写,但建议按标准写法
HTTP 头部字段不区分大小写(如 user-agent 和 User-Agent 等价),但为了兼容性和可读性,建议按 HTTP 标准写法(首字母大写,连字符分隔):
✅ 推荐:res.vary('User-Agent')、res.vary('Accept')
❌ 不推荐:res.vary('user-agent')、res.vary('accept')(虽生效,但不规范)
3. 必须在 “响应发送前” 调用
res.vary 需在 res.send/res.render/res.end 等 “发送响应” 的方法之前调用,否则 Vary 头部无法生效(响应已发送,无法修改头部):
❌ 错误:res.send('内容').vary('User-Agent');(响应已发送,Vary 无效)
✅ 正确:res.vary('User-Agent').send('内容');(发送前添加)
4. 避免 “过度 Vary” 导致缓存碎片化
Vary 字段越多,缓存的 “细分维度” 越多,可能导致 CDN 缓存命中率下降(如同时加 User-Agent/Accept/Cookie,会产生大量缓存副本)。仅在 “确实需要区分” 时添加,避免不必要的字段:
❌ 过度 Vary:res.vary('User-Agent').vary('Accept').vary('Cookie').vary('Referer');
✅ 合理 Vary:仅添加 “影响内容差异” 的字段(如仅 User-Agent,若内容仅随设备变化)
5. 与 res.type 的配合:动态 Content-Type 需 Vary
当用 res.type 根据请求头动态设置 Content-Type 时(如场景 2),必须添加对应的 Vary 字段(如 Accept),否则缓存会将不同 Content-Type 的响应视为同一内容,导致客户端解析错误。
四、总结:res.vary 的核心价值与使用准则
res.vary 是 “控制缓存精准度” 的核心工具,解决 “同一 URL 返回不同内容时的缓存区分问题”,确保:
- 缓存服务器不返回 “不匹配请求头” 的缓存(如移动端缓存给 PC 端);
- 客户端能获取到符合自身需求的内容(如需要 JSON 时不返回 HTML)。
- 什么时候需要加 Vary?
- 内容随 User-Agent 变化(设备适配)→ 加 Vary: User-Agent;
- 内容随 Accept 变化(多格式响应)→ 加 Vary: Accept;
- 内容随 Cookie 变化(用户个性化内容)→ 加 Vary: Cookie。
- 如何配合其他方法?
- 与 res.render 配合:渲染不同模板前加 Vary(如场景 1);
- 与 res.type 配合:动态设置 Content-Type 前,加对应 Vary 字段(如场景 2);
- 与 res.status 配合:状态码不影响 Vary,只需确保 Vary 在发送前添加。
- 关键原则
- “内容变则 Vary 加”:只要同一 URL 的内容因请求头不同而变化,就必须加对应的 Vary 字段;
- “宁少勿多”:不添加无关 Vary 字段,避免缓存碎片化。
记住:Vary 头部是 “缓存的导航灯”,res.vary 是 “开关导航灯” 的便捷按钮 —— 合理使用能让缓存既高效又精准,避免因缓存错乱导致的业务问题。
在没有Vary应答头部时增加Vary应答头部。
ps:vary的意义在于告诉代理服务器/缓存/CDN,如何判断请求是否一样,vary中的组合就是服务器/缓存/CDN判断的依据,比如Vary中有User-Agent,那么即使相同的请求,如果用户使用IE打开了一个页面,再用Firefox打开这个页面的时候,CDN/代理会认为是不同的页面,如果Vary中没有User-Agent,那么CDN/代理会认为是相同的页面,直接给用户返回缓存的页面,而不会再去web服务器请求相应的页面。通俗的说就相当于
field作为了一个缓存的key来判断是否命中缓存res.vary('User-Agent').render('docs');
评论