首页
壁纸
统计
友链
Search
1
Nestjs概述-中文
10 阅读
2
ExpressAPI
10 阅读
3
NestJS新手入门核心模块对比表笔记
9 阅读
4
JavaScript企业数据处理实用指南
9 阅读
5
Vue2详细笔记
8 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
12
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
12
篇与
的结果
2026-02-17
ExpressAPI
Express 4.x APIexpress()express()用来创建一个Express的程序。express()方法是express模块导出的顶层方法。var express = require('express'); var app = express();Methodsexpress.static(root, [options])express.static是Express中唯一的内建中间件。它以server-static模块为基础开发,负责托管 Express 应用内的静态资源。参数root为静态资源的所在的根目录。参数options是可选的,支持以下的属性:属性描述类型默认值dotfiles是否响应点文件。供选择的值有"allow","deny"和"ignore"String"ignore"etag使能或者关闭etagBooleantrueextensions设置文件延期回退Booleantrueindex发送目录索引文件。设置false将不发送。Mixed"index.html"lastModified设置文件在系统中的最后修改时间到Last-Modified头部。可能的取值有false和true。BooleantruemaxAge在Cache-Control头部中设置max-age属性,精度为毫秒(ms)或则一段ms format的字符串Number0redirect当请求的pathname是一个目录的时候,重定向到尾随"/"BooleantruesetHeaders当响应静态文件请求时设置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。Propertiesn.属性;财产;资产;地产app.localsapp.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.mountpathapp.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”上 Eventsapp.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); Methodsapp.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方法: checkout connect copy delete get head lock merge mkactivity mkcol move m-search notify options patch post propfind proppatch purege put report search subscribe trace unlock unsubscribe 如果使用上述方法时,导致了无效的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 routingBoolean启用区分大小写。不启用。对/Foo和/foo处理是一样。envString环境模型。process.env.NODE_ENV(NODE_ENV环境变量)或者"development"etagVaried设置ETag响应头。可取的值,可以查阅etag options table。更多关于HTTP ETag header。weakjsonp callback nameString指定默认JSONP回调的名称。?callback=json replacerStringJSON替代品回调nulljson spacesNumber当设置了这个值后,发送缩进空格美化过的JSON字符串。Disabledquery parserVaried设置值为false来禁用query parser,或者设置simple,extended,也可以自己实现query string解析函数。simple基于Node原生的query解析,querystring。"extend"strict routingBoolean启用严格的路由。不启用。对/foo和/foo/的路由处理是一样。subdomain offsetNumber用来删除访问子域的主机点分部分的个数2trust proxyVaried指示app在一个反向代理的后面,使用x-Forwarded-*来确定连接和客户端的IP地址。注意:X-Forwarded-*头部很容易被欺骗,所有检测客户端的IP地址是靠不住的。trust proxy默认不启用。当启用时,Express尝试通过前端代理或者一系列代理来获取已连接的客户端IP地址。req.ips属性包含了已连接客户端IP地址的一个数组。为了启动它,需要设置在下面trust proxy options table中定义的值。trust proxy的设置实现使用了proxy-addr包。如果想获得更多的信息,可以查阅它的文档DisableviewsString or Arrayview所在的目录或者目录数组。如果是一个数组,将按在数组中的顺序来查找view。process.cwd() + '/views'view cacheBoolean启用视图模板编译缓存。在生成环境默认开启。view engineString省略时,默认的引擎被扩展使用。 x-powered-byBoolean启用X-Powered-By:ExpressHTTP头部trueOptions for trust proxy settings查阅Express behind proxies来获取更多信息。 TypeValue Boolean 如果为true,客户端的IP地址作为X-Forwarded-*头部的最左边的条目。如果为false,可以理解为app直接与英特网直连,客户端的IP地址衍生自req.connection.remoteAddress。false是默认设置。 IP addresses 一个IP地址,子网,或者一组IP地址,和委托子网。下面列出的是一个预先配置的子网名列表。 loopback - 127.0.0.1/8, ::1/128 linklocal - 169.254.0.0/16, fe80::/10 uniquelocal - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7 使用下面方法中的任何一种来设置IP地址: app.set('trust proxy', 'loopback') // specify a single subnet app.set('trust proxy', 'loopback, 123.123.123.123') // specify a subnet and an address app.set('trust proxy', 'loopback, linklocal, uniquelocal') // specify multiple subnets as CSV app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']) // specify multiple subnets as an array 当指定IP地址之后, 这个IP地址或子网会被设置了这个IP地址或子网的`app`排除在外, 最靠近程序服务的没有委托的地址将被看做客户端IP地址。 Number 信任从反向代理到app中间小于等于n跳的连接为客户端。 Function 客户自定义委托代理信任机制。如果你使用这个,请确保你自己知道你在干什么。 app.set('trust proxy', function (ip) { if (ip === '127.0.0.1' || ip === '123.123.123.123') return true; // trusted IPs else return false; }) Options for etag settingsETag功能的实现使用了etag包。如果你需要获得更多的信息,你可以查阅它的文档。 TypeValue Boolean 设置为true,启用weak ETag。这个是默认设置。设置false,禁用所有的ETag。 String 如果是strong,使能strong ETag。如果是weak,启用weak ETag。 Function 客户自定义`ETag`方法的实现. 如果你使用这个,请确保你自己知道你在干什么。 app.set('etag', function (body, encoding) { return generateHash(body, encoding); // consider the function is defined }) 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 // will match paths starting with /abcd app.use('/abcd', function (req, res, next) { next(); }) Path Pattern // will match paths starting with /abcd and /abd app.use('/abc?d', function (req, res, next) { next(); }) // will match paths starting with /abcd, /abbcd, /abbbbbcd and so on app.use('/ab+cd', function (req, res, next) { next(); }) // will match paths starting with /abcd, /abxcd, /abFOOcd, /abbArcd and so on app.use('/ab*cd', function (req, res, next) { next(); }) // will match paths starting with /ad and /abcd app.use('/a(bc)?d', function (req, res, next) { next(); }) Regular Expression // will match paths starting with /abc and /xyz app.use(//abc|/xyz/, function (req, res, next) { next(); }) Array // will match paths starting with /abcd, /xyza, /lmn, and /pqr app.use(['/abcd', '/xyza', //lmn|/pqr/], function (req, res, next) { next(); }) 方法可以是一个中间件方法,一系列中间件方法,一组中间件方法或者他们的集合。由于router和app实现了中间件接口,你可以像使用其他任一中间件方法那样使用它们。 Usage Example 单个中间件 // 局部中间件函数 app.use(function (req, res, next) { next(); }) // 挂载 Router const router = express.Router(); router.get('/', function (req, res, next) { next(); }) app.use(router); // 挂载子应用 const subApp = express()
2026年02月17日
10 阅读
0 评论
0 点赞
2026-02-16
Nestjs概述-中文
INTRODUCTION一、简介Nest(NestJS)是一款用于构建高效、可扩展的 Node.js 服务端应用框架。采用渐进式 JavaScript 设计,基于并完整支持 TypeScript(同时也支持纯 JavaScript 开发)。融合了三大编程范式:OOP:面向对象编程FP:函数式编程FRP:函数式响应式编程底层原理:默认使用成熟的 HTTP 服务框架 Express。也可灵活配置切换为 Fastify。框架特点:在 Express / Fastify 等通用 Node 框架之上做了一层上层抽象。同时直接暴露底层原生 API,开发者可以自由使用平台上大量的第三方模块。Node.js 服务端、TS 优先、三大编程范式、Express/Fastify 底层、抽象 + 原生 API 双支持二、设计理念近年来,得益于 Node.js,JavaScript 已经成为前后端通用的“网络通用语言”。这也催生了 Angular、React、Vue 等优秀前端框架,大幅提升开发效率,让前端应用更快速、可测试、易扩展。但在 Node.js(服务端 JavaScript)生态中:虽然已有大量优质库、工具和辅助模块但没有一个能有效解决架构问题NestJS 提供开箱即用的应用架构,让开发者和团队可以轻松构建:高可测试性高可扩展性低耦合易维护的应用程序。其架构设计深受 Angular 启发。三、安装方式 1:使用 Nest CLI 搭建(推荐新手)通过命令行脚手架直接生成项目目录、核心文件和标准目录结构。# 全局安装 CLI npm i -g @nestjs/cli # 创建项目 nest new project-name提示:如需创建更严格类型检查的 TypeScript 项目,可添加 --strict 参数:nest new project-name --strict方式 2:Git 克隆官方起步模板# 克隆 TS 模板 git clone https://github.com/nestjs/typescript-starter.git project cd project npm install npm run start提示:如果不想克隆 Git 历史,可以使用 degit。启动后访问:http://localhost:3000/如需纯 JavaScript 版本,将仓库地址换成 javascript-starter.git 即可。方式 3:从零手动安装(不推荐新手)只安装核心包,需自己配置项目文件,最少依赖:NestJS 四大核心依赖作用核心必装依赖(缺一不可,支撑 Nest 基础运行):@nestjs/core:Nest 核心引擎,提供应用初始化、模块挂载、依赖注入底层实现,是应用运行基石。@nestjs/common:通用工具库,提供控制器、服务等核心装饰器及异常处理、管道等基础组件,简化开发。rxjs:响应式编程库,处理异步数据流,支撑 Nest 异步操作(如请求、数据库查询)。reflect-metadata:元数据反射工具,支撑装饰器识别和依赖注入机制,保障组件自动装配。补充:CLI 搭建/克隆模板会自动安装,从零搭建需手动安装这四个依赖。OVERVIEWfirst steps一、入门基础入门目标通过构建基础CRUD应用,掌握Nest核心基础知识和关键组成部分,覆盖入门级核心知识点。语言支持兼容TypeScript(推荐)和纯JavaScript,纯JS需搭配Babel编译器(Nest依赖最新语言特性)。示例默认使用TypeScript,代码片段可点击右上角按钮,切换为纯JavaScript语法。前置条件必须安装Node.js,且版本≥20,否则无法正常搭建和运行Nest项目。二、项目搭建(推荐CLI方式)npm i -g @nestjs/cli # 全局安装Nest CLI工具 nest new project-name # 创建新的Nest项目(project-name为自定义项目名)提示:添加--strict标志,可创建启用TypeScript严格特性集的项目。项目核心目录与文件CLI搭建后会生成src目录,包含5个核心文件,分工明确:app.controller.ts:基础控制器,包含单一路由,负责接收和响应请求。app.controller.spec.ts:控制器的单元测试文件,用于测试控制器功能。app.module.ts:应用的根模块,是Nest应用的核心组织单元。app.service.ts:基础服务,包含单个方法,用于封装业务逻辑。main.ts:应用入口文件,通过NestFactory创建应用实例并启动。三、平台支持Nest是平台无关框架,开箱即支持两种HTTP平台:Express(默认)和Fastify。默认使用@nestjs/platform-express包,Express成熟稳定、社区资源丰富;Fastify高性能、低开销,可按需切换。无需指定平台类型,仅当需要访问底层平台API时,才需在创建应用实例时指定类型。四、应用运行命令npm run start # 普通启动,监听HTTP请求 npm run start:dev # 热更新启动,监听文件变化,自动重新编译并重启服务器 npm run start -- -b swc # 加速构建(构建速度提升20倍)补充:应用启动后,打开浏览器访问http://localhost:3000/,可看到Hello World! 提示,说明启动成功。五、代码检查与格式化CLI生成的项目默认预装eslint(代码检查工具)和prettier(代码格式化工具),支持IDE集成和无头环境使用。npm run lint # 检查代码规范,并自动修复可修复的问题 npm run format # 格式化代码,统一代码风格Controllers一、控制器核心定位Controllers(控制器)负责处理客户端传入的请求,并向客户端返回响应,是Nest应用中处理请求的核心组件。核心作用:接收特定请求、执行对应逻辑、返回响应,路由机制决定了哪个控制器处理哪个请求。一个控制器可包含多个路由,每个路由对应不同的请求处理逻辑(动作)。创建方式:通过类和装饰器定义,装饰器为类添加元数据,让Nest生成请求与控制器的路由映射。提示:使用CLI命令快速生成CRUD控制器(带内置验证):nest g resource [name];快速生成普通控制器:nest g controller [name]。二、核心基础:路由(Routing)路由装饰器核心用法@Controller():定义基础控制器,可指定路由路径前缀(如@Controller('cats')),用于分组相关路由、减少重复代码。HTTP请求方法装饰器:@Get()、@Post()、@Put()、@Delete()等,修饰控制器方法,指定该方法处理的HTTP请求类型。路由路径拼接:控制器前缀 + 方法装饰器中的路径(可选),例如:@Controller('cats') + @Get('breed') → 路由为GET /cats/breed。基础示例(cats.controller.ts)import { Controller, Get } from '@nestjs/common'; @Controller('cats') // 路由前缀:/cats export class CatsController { @Get() // 处理GET /cats请求 findAll(): string { return 'This action returns all cats'; } }路由通配符支持模式匹配路由,常用作为通配符(匹配路径末尾任意字符),例如:@Get('abcd/') 可匹配 /abcd/123、/abcd/abc 等路径。注意:Express v5及以上对路由要求更严格,Nest提供兼容层,可正常使用通配符;中间带的路由(如ab*cd)仅Express支持(需命名通配符),Fastify不支持。三、请求处理核心细节请求对象(Request Object)通过@Req()(或@Request())装饰器注入底层平台(默认Express)的请求对象,需导入express的Request类型(需安装@types/express)。无需手动获取请求参数,Nest提供专用装饰器,简化开发(常用装饰器如下):@Req() / @Request()获取完整请求对象(req)@Param(key?)获取路由参数(req.params),可指定具体key(如@Param('id'))@Body(key?)获取请求体(req.body),可指定具体key,常用于POST/PUT请求@Query(key?)获取查询参数(req.query),可指定具体key@Headers(name?)获取请求头(req.headers),可指定具体请求头名称路由参数(Route Parameters)用于接收URL中的动态数据(如GET /cats/1,获取id为1的猫),通过@Param()装饰器访问。// 路由:GET /cats/:id @Get(':id') findOne(@Param('id') id: string): string { return `This action returns a #${id} cat`; }注意:带参数的路由需声明在静态路由之后,避免拦截静态路由请求。请求体(Request Payloads)通过@Body()装饰器获取请求体,TypeScript环境下需定义DTO(数据传输对象),指定请求体格式。DTO推荐用类(而非接口),因为接口会在编译时被移除,Nest无法在运行时引用(如管道验证需依赖DTO元数据)。// create-cat.dto.ts(DTO定义) export class CreateCatDto { name: string; age: number; breed: string; } // cats.controller.ts(使用DTO) import { Controller, Post, Body } from '@nestjs/common'; import { CreateCatDto } from './dto/create-cat.dto'; @Controller('cats') export class CatsController { @Post() // 处理POST /cats请求 create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat'; } }查询参数(Query Parameters)通过@Query()装饰器获取URL中的查询参数(如GET /cats?age=2&breed=Persian),支持复杂查询参数(需配置HTTP适配器的查询解析器)。四、响应处理(Response Handling)两种响应方式标准方式(推荐):直接返回值,Nest自动处理序列化和状态码。返回对象/数组:自动序列化为JSON;返回基础类型(字符串、数字):直接返回值。默认状态码:GET/PUT/DELETE等为200,POST为201,可通过@HttpCode()装饰器修改。库特定方式:通过@Res()(或@Response())注入底层响应对象(如Express的res),手动控制响应。注意:使用@Res()后,Nest会禁用标准方式;需手动调用res.send()/res.json()等,否则请求会挂起。兼容两种方式:添加passthrough: true(@Res({ passthrough: true })),可手动设置响应头/ cookies,同时让Nest处理返回值。状态码与响应头设置状态码:@HttpCode(状态码) 装饰器(需从@nestjs/common导入),动态状态码可通过响应对象或抛出异常设置。响应头:@Header(键, 值) 装饰器,或通过响应对象手动设置(res.header())。import { Controller, Post, HttpCode, Header } from '@nestjs/common'; @Controller('cats') export class CatsController { @Post() @HttpCode(204) // 设置状态码为204 @Header('Cache-Control', 'no-store') // 设置响应头 create() { return 'This action adds a new cat'; } }重定向(Redirection)通过@Redirect(url, 状态码) 装饰器实现重定向,状态码默认302;动态重定向可返回一个包含url和statusCode的对象,覆盖装饰器参数。五、控制器进阶特性1. 子域名路由(Sub-domain Routing)@Controller()装饰器可传入host选项,指定请求的主机名(如@Controller({ host: 'admin.example.com' })),支持动态主机名(用:占位符),通过@HostParam()获取主机名参数。注意:Fastify不支持嵌套路由器,使用子域名路由推荐用默认Express适配器。2. 异步处理(Asynchronicity)完全支持async/await,异步方法返回Promise,Nest自动解析。支持返回RxJS Observable流,Nest自动订阅,流完成后返回最终值。3. 状态共享(State Sharing)Nest中几乎所有资源(数据库连接池、单例服务等)都在请求间共享,Node.js不使用“请求/响应多线程无状态模型”,因此使用单例实例是安全的;特殊场景需请求级生命周期,可配置注入作用域。???六、控制器注册与完整示例控制器注册控制器必须属于某个模块,需在@Module()装饰器的controllers数组中注册(通常注册在根模块AppModule或对应功能模块)。// app.module.ts import { Module } from '@nestjs/common'; import { CatsController } from './cats/cats.controller'; @Module({ controllers: [CatsController], // 注册CatsController }) export class AppModule {}完整CRUD控制器示例import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common'; import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto'; @Controller('cats') export class CatsController { @Post() // 新增 create(@Body() createCatDto: CreateCatDto) { return 'This action adds a new cat'; } @Get() // 查询所有 findAll(@Query() query: ListAllEntities) { return `This action returns all cats (limit: ${query.limit} items)`; } @Get(':id') // 查询单个 findOne(@Param('id') id: string) { return `This action returns a #${id} cat`; } @Put(':id') // 更新 update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) { return `This action updates a #${id} cat`; } @Delete(':id') // 删除 remove(@Param('id') id: string) { return `This action removes a #${id} cat`; } }七、关键提示DTO用于规范请求数据格式,配合ValidationPipe可实现请求数据校验和过滤(过滤非白名单属性)。库特定响应方式(@Res())会使代码平台依赖,增加测试难度,非必要不推荐使用。CLI生成器可自动创建控制器及相关模板,提升开发效率。Provides一、Providers 核心定位Providers(提供者)是 Nest 的核心概念,许多基础 Nest 类(如服务、仓库、工厂、工具类等)都可作为提供者。核心思想:可作为依赖被注入,使对象之间形成各种关联关系,对象间的“连接”工作主要由 Nest 运行时系统处理。核心作用:封装业务逻辑、数据处理(如数据存储与查询)等复杂任务,供控制器(Controller)调用,实现“控制器负责请求响应、提供者负责业务逻辑”的分离。本质:普通 JavaScript/TypeScript 类,需在 Nest 模块中声明为提供者才能被 Nest 管理和注入。提示:推荐遵循 SOLID 原则设计和组织依赖,让代码更具可维护性和扩展性。**SOLID 原则是 5 条面向对象设计(OOP)的核心准则 S(单一职责原则) 核心:一个 Provider(如 Service)只负责一项核心职责,不承担无关逻辑。 O(开放 / 封闭原则) 核心:Provider 对扩展开放、对修改封闭。新增功能时,通过扩展类 / 方法实现,不改动原有代码。 L(里氏替换原则) 核心:子类可完全替代父类,且不影响程序正常运行(父类与子类遵循同一接口 / 规范)。 I(接口隔离原则) 核心:不强迫 Provider 依赖它用不到的接口 / 方法,拆分细化接口,避免 “胖接口”。 D(依赖倒置原则) 核心:依赖抽象(接口 / 抽象类),不依赖具体实现;高层模块(如 Controller)不依赖低层模块(如 Service),二者都依赖抽象。**二、核心实现:Services(服务)Services 是最常用的 Providers 类型,专门用于封装业务逻辑,是控制器的“业务处理助手”。服务的创建与基础示例通过 @Injectable() 装饰器标记类为服务(提供者),CLI 命令快速生成服务:nest g service [name](如 nest g service cats)。// cats.service.ts(服务定义) import { Injectable } from '@nestjs/common'; import { Cat } from './interfaces/cat.interface'; // 导入接口规范数据格式 @Injectable() // 标记此类为Nest可管理的提供者 export class CatsService { private readonly cats: Cat[] = []; // 模拟数据存储 // 新增猫的逻辑 create(cat: Cat) { this.cats.push(cat); } // 查询所有猫的逻辑 findAll(): Cat[] { return this.cats; } }2. 配套接口(Interface)通常会用接口(Interface)规范数据格式(如 Cat 接口),仅用于 TypeScript 类型校验,编译后会被移除。// interfaces/cat.interface.ts export interface Cat { name: string; age: number; breed: string; }3. 服务的使用(注入到控制器)通过构造函数注入服务,控制器即可调用服务中的方法,实现业务逻辑分离。// cats.controller.ts(使用CatsService) import { Controller, Get, Post, Body } from '@nestjs/common'; import { CreateCatDto } from './dto/create-cat.dto'; import { CatsService } from './cats.service'; import { Cat } from './interfaces/cat.interface'; @Controller('cats') export class CatsController { // 构造函数注入CatsService,private关键字简化声明+初始化 constructor(private catsService: CatsService) {} @Post() async create(@Body() createCatDto: CreateCatDto) { // 调用服务的create方法处理新增逻辑 this.catsService.create(createCatDto); } @Get() async findAll(): Promise<Cat[]> { // 调用服务的findAll方法获取数据 return this.catsService.findAll(); } }三、核心机制:依赖注入(Dependency Injection)依赖注入(DI)是 Nest 的核心设计模式,Nest 基于 TypeScript 的特性,让依赖管理变得简单。核心原理:Nest 会根据构造函数中声明的类型,自动解析并注入对应的依赖实例(如构造函数中声明 private catsService: CatsService,Nest 会自动创建/获取 CatsService 实例并注入)。单例特性:默认情况下,提供者是单例的(整个应用生命周期内只有一个实例),多次注入会获取同一个实例。优势:降低代码耦合度,便于测试和维护,无需手动创建和管理依赖实例。四、Providers 核心特性1. 作用域(Scopes)提供者的生命周期(作用域)默认与应用生命周期一致:应用启动时:所有依赖被解析,提供者实例化。应用关闭时:所有提供者被销毁。可选作用域:可配置为“请求作用域”(request-scoped),即每个请求对应一个提供者实例,生命周期与单个请求绑定(详见注入作用域章节)。2. 自定义提供者(Custom Providers)Nest 内置控制反转(IoC)容器,支持多种方式定义提供者,不止于类:支持类型:普通值、类、异步工厂、同步工厂等。核心用途:灵活配置依赖,如注入第三方库、模拟测试依赖等(详见依赖注入章节)。3. 可选提供者(Optional Providers)用于处理“非必需依赖”(如可选的配置对象),即使依赖未被提供,也不会报错,可使用默认值。通过 @Optional() 装饰器标记为可选依赖,通常配合自定义提供者的令牌使用。import { Injectable, Optional, Inject } from '@nestjs/common'; @Injectable() export class HttpService<T> { // @Optional() 标记为可选依赖,@Inject() 指定自定义令牌 constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {} }4. 属性注入(Property-based Injection)除了常用的“构造函数注入”,还可通过@Inject() 装饰器直接注入到类属性上。适用场景:子类继承父类时,避免通过 super() 层层传递依赖(非必要不推荐,构造函数注入更清晰)。import { Injectable, Inject } from '@nestjs/common'; @Injectable() export class HttpService<T> { // 直接注入到属性上 @Inject('HTTP_OPTIONS') private readonly httpClient: T; }五、提供者注册(Provider Registration)提供者必须在某个 Nest 模块中注册,才能被 Nest 识别、管理和注入(与控制器注册类似)。注册方式:在模块的 @Module() 装饰器中,将提供者添加到 providers 数组中。// app.module.ts(注册CatsService) import { Module } from '@nestjs/common'; import { CatsController } from './cats/cats.controller'; import { CatsService } from './cats/cats.service'; @Module({ controllers: [CatsController], // 注册控制器 providers: [CatsService], // 注册提供者(服务) }) export class AppModule {}注册后,Nest 即可正常解析控制器(如 CatsController)对该提供者的依赖。六、目录结构参考结合此前的控制器、DTO、接口,整合提供者后的标准目录结构如下:src ├─ cats │ ├─ dto │ │ └─ create-cat.dto.ts │ ├─ interfaces │ │ └─ cat.interface.ts │ ├─ cats.controller.ts │ └─ cats.service.ts ├─ app.module.ts └─ main.ts七、手动实例化(补充)默认情况下,Nest 自动处理依赖的解析和实例化,特殊场景下可手动获取/实例化提供者:Module reference:动态获取已存在的提供者实例或动态实例化提供者。Standalone applications:在 bootstrap() 函数中获取提供者(如独立应用、启动时使用配置服务)。八、关键提示@Injectable() 装饰器是核心:必须给提供者类添加该装饰器,否则 Nest 无法识别和管理。分离原则:控制器只处理 HTTP 请求/响应,复杂业务逻辑全部交给提供者(服务),提升代码可维护性。动态获取已存在的提供者实例或动态实例化提供者 (默认情况下,Nest 会在应用启动时(bootstrap 阶段),自动解析所有依赖、实例化提供者(单例),并在需要时(如控制器注入)自动注入。而 “动态获取 / 动态实例化”,是打破这种 “启动时自动处理” 的固定模式,在应用运行过程中(比如请求处理中、某个逻辑执行时),手动获取已经存在的提供者实例,或手动创建新的提供者实例(按需实例化,而非启动时就创建)。)Modules一、Modules 核心定位Modules是被@Module()装饰器标记的类,核心作用是作为应用“组织单元”,封装相关控制器、提供者,管理依赖、划分功能边界,让应用结构清晰可维护。核心特性:每个Nest应用至少有一个根模块(AppModule),是Nest构建应用图(解析依赖关系)的起点。使用场景:小型应用可仅用根模块;中大型应用拆分多个模块,每个模块封装一组紧密相关功能(贴合SOLID原则)。提示:CLI快速生成模块:nest g module [name](如nest g module cats)。二、@Module() 装饰器核心配置@Module()接收一个对象,包含4个核心属性(按需配置,无需全部填写),用于描述模块配置:providers模块内的提供者(如Service),由Nest实例化,可在当前模块内共享。controllers模块内的控制器,Nest自动实例化并注册路由。imports需导入的模块列表,导入模块需导出当前模块所需提供者(导入才可使用)。exports模块对外暴露的提供者(模块“公共API”),其他模块导入后可使用,可导出提供者本身或其令牌。关键规则:模块默认封装提供者,仅可注入“当前模块内提供者”或“其他导入模块明确导出的提供者”。三、常见模块类型及用法1. 功能模块(Feature Modules)最常用,封装某一特定功能的控制器和提供者,划分功能边界(如Cats相关功能封装为CatsModule)。示例:创建并使用CatsModule// cats/cats.module.ts(功能模块定义) import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], }) export class CatsModule {}需导入根模块才可使用:// app.module.ts(根模块导入) import { Module } from '@nestjs/common'; import { CatsModule } from './cats/cats.module'; @Module({ imports: [CatsModule] }) export class AppModule {}2. 共享模块(Shared Modules)Nest模块默认是单例,可在多模块间共享同一提供者实例,只需导出该提供者即可。核心用法:在exports数组中添加需共享的提供者,其他模块导入该模块即可使用(共享同一实例)。优势:避免重复注册,减少内存占用,保证状态一致性。// cats/cats.module.ts(共享CatsService) import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], exports: [CatsService] // 导出供其他模块共享 }) export class CatsModule {}3. 模块重导出(Module Re-exporting)模块可重导出其导入的模块,其他模块导入当前模块后,即可使用被重导出模块的功能,无需单独导入。// core.module.ts(重导出CommonModule) import { Module } from '@nestjs/common'; import { CommonModule } from './common.module'; @Module({ imports: [CommonModule], exports: [CommonModule] // 重导出 }) export class CoreModule {}4. 全局模块(Global Modules)用于提供全局可用的提供者(如工具类、数据库连接),无需重复导入,用@Global()标记。核心用法:添加@Global(),同时导出需全局共享的提供者;只需注册一次(通常由根模块注册)。注意:不推荐过度使用,会增加模块耦合,优先用“导入+导出”共享功能。// cats/cats.module.ts(全局模块) import { Module, Global } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Global() @Module({ controllers: [CatsController], providers: [CatsService], exports: [CatsService] // 必须导出才可全局使用 }) export class CatsModule {}5. 动态模块(Dynamic Modules)可在运行时配置的模块,适用于灵活定制场景(如根据配置创建数据库连接)。核心用法:通过静态方法(通常forRoot()/forRootAsync())返回DynamicModule,动态配置providers、exports。特性:静态方法可同步/异步返回;动态配置扩展(不覆盖)模块默认配置。// database/database.module.ts(动态模块) import { Module, DynamicModule } from '@nestjs/common'; import { createDatabaseProviders } from './database.providers'; import { Connection } from './connection.provider'; @Module({ providers: [Connection], exports: [Connection] }) export class DatabaseModule { static forRoot(entities = [], options?): DynamicModule { const providers = createDatabaseProviders(options, entities); // 动态生成提供者 return { module: DatabaseModule, providers, exports: providers }; } }使用示例(导入时传配置):// app.module.ts import { Module } from '@nestjs/common'; import { DatabaseModule } from './database/database.module'; import { User } from './users/entities/user.entity'; @Module({ imports: [DatabaseModule.forRoot([User])] }) export class AppModule {}四、模块相关补充1. 模块中的依赖注入 模块类可注入提供者(用于自身配置),但不能作为提供者被注入(会导致循环依赖)。模块(如 UserModule)管理着服务(UserService),若服务再注入模块,会形成「模块 → 服务 → 模块」的循环 模块是“管理者”(管理控制器、提供者),可以用提供者来辅助自己工作,但模块不能当“被管理者”(不能被当作提供者,供其他组件注入使用),否则会出现依赖循环,Nest无法解析。// cats/cats.module.ts(模块注入提供者) import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService] }) export class CatsModule { constructor(private catsService: CatsService) {} // 模块注入 } 标准目录结构src ├─ cats // 功能模块目录 │ ├─ dto // 数据传输对象 │ ├─ interfaces // 接口定义 │ ├─ cats.controller.ts │ ├─ cats.module.ts // 模块文件 │ └─ cats.service.ts // 提供者文件 ├─ app.module.ts // 根模块 └─ main.ts // 入口文件五、关键提示模块核心价值:划分功能边界、管理依赖、实现复用,是中大型Nest应用的核心组织方式。exports注意:仅导出其他模块需用的提供者,减少模块耦合。全局模块vs共享模块:优先用共享模块(结构清晰),避免过度使用全局模块。动态模块核心:运行时配置,适配多场景(如多数据库、环境差异化配置)。Middleware一、核心概念:NestJS 中间件是什么?NestJS 中间件本质上和 Express 中间件等价,是在路由处理器执行前被调用的函数/类,它能访问 request(请求)、response(响应)对象,以及请求-响应周期中的 next() 函数,主要作用包括:执行任意代码(如日志打印、权限校验);修改请求/响应对象(如添加请求头、解析参数);结束请求-响应周期(如直接返回错误响应);调用下一个中间件(必须调用 next(),否则请求会“挂起”)。二、NestJS 中间件的两种实现方式1. 类式中间件(推荐:需要依赖注入时) nestmiddleware+injectable适用于中间件需要注入服务/依赖的场景,必须实现 NestMiddleware 接口,并使用 @Injectable() 装饰器。// src/common/middleware/logger.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class LoggerMiddleware implements NestMiddleware { // 核心方法:use 接收 req、res、next 三个参数 use(req: Request, res: Response, next: NextFunction) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`); // 必须调用 next(),否则请求会卡住 next(); } }2. 函数式中间件(推荐:无依赖时)更简洁,无需类和装饰器,适合简单场景(如基础日志、跨域)。// src/common/middleware/logger.middleware.ts import { Request, Response, NextFunction } from 'express'; // 直接导出一个函数即可 export function logger(req: Request, res: Response, next: NextFunction) { console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`); next(); }三、中间件的应用方式1. 局部应用(指定路由/控制器)通过模块实现 NestModule 接口,在 configure() 方法中使用 MiddlewareConsumer 绑定中间件,支持:指定单个/多个路由;指定请求方法(GET/POST 等);指定控制器;排除特定路由。示例1:绑定到指定路由(GET /cats)// src/app.module.ts import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common'; import { LoggerMiddleware } from './common/middleware/logger.middleware'; import { CatsController } from './cats/cats.controller'; @Module({ controllers: [CatsController], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply(LoggerMiddleware) // 也可以用函数式:apply(logger) .exclude( // 排除不需要应用的路由 { path: 'cats/ignore', method: RequestMethod.GET }, // 排除 GET /cats/ignore 'cats/*/delete' // 通配符排除 ) .forRoutes( { path: 'cats', method: RequestMethod.GET }, // 仅绑定 GET /cats CatsController // 也可以直接绑定整个控制器(所有 /cats 下的路由) ); } }2. 全局应用(所有路由)通过 app.use() 绑定到所有路由,注意:全局中间件无法使用依赖注入(类式中间件不行),优先用函数式。// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { logger } from './common/middleware/logger.middleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局应用中间件(所有路由都会触发) app.use(logger); await app.listen(3000); } bootstrap();3. 多个中间件顺序执行apply() 方法可传入多个中间件,按传入顺序执行(如先跨域 → 再安全校验 → 再日志)。// src/app.module.ts import * as cors from 'cors'; import * as helmet from 'helmet'; // 安全相关中间件 import { logger } from './common/middleware/logger.middleware'; // 在 configure 中 consumer .apply(cors(), helmet(), logger) // 顺序:cors → helmet → logger .forRoutes(CatsController);四、关键注意事项通配符路由:'abcd/*':匹配 abcd/1、abcd/abc,但不匹配 abcd/;'abcd/{*splat}':加花括号后,可匹配 abcd/、abcd/123 等所有以 abcd/ 开头的路由;Express/Fastify 差异:两者中间件的方法签名不同,需根据适配器调整;全局中间件依赖注入:如果全局中间件需要依赖,不要用 app.use(),而是在模块中通过 forRoutes('*') 绑定类式中间件:// 模块中绑定全局类式中间件(支持依赖注入)默认中间件:使用 Express 适配器时,NestJS 会默认注册 json() 和 urlencoded() 中间件,如需自定义,创建应用时关闭:总结NestJS 中间件分类式(支持依赖注入)和函数式(简洁无依赖),核心是实现 use() 方法/函数并调用 next();中间件可局部绑定(指定路由/控制器,通过 MiddlewareConsumer)或全局绑定(所有路由,通过 app.use()); configure(consumer: MiddlewareConsumer) configure(consumer:middlewareconsumer)关键技巧:用 exclude() 排除特定路由、用通配符匹配批量路由、多个中间件按 apply() 传入顺序执行。通过这些方式,你可以灵活实现日志、权限校验、参数解析、跨域处理等通用功能,让业务代码更聚焦核心逻辑。Exception filters一、Exception filters 核心概念Nest 内置异常层(exceptions layer),负责处理应用中所有未被手动处理的异常;当应用代码未捕获异常时,该层会自动捕获并返回用户友好的响应。默认由内置全局异常过滤器处理,核心负责 HttpException 及其子类异常;对于未识别的异常(非 HttpException 及其子类),默认返回如下 JSON 响应:{ "statusCode": 500, "message": "Internal server error" }提示:全局异常过滤器部分支持 http-errors 库,任何包含 statusCode 和 message 属性的抛出异常,都会被正确格式化返回(而非默认的 500 异常)。二、抛出标准异常(Throwing standard exceptions)Nest 从 @nestjs/common 暴露内置 HttpException 类,适用于 HTTP REST/GraphQL API,推荐在错误场景下返回标准 HTTP 响应对象。2.1 基础示例// cats.controller.ts import { Get, HttpException, HttpStatus } from '@nestjs/common'; @Get() async findAll() { throw new HttpException('Forbidden', HttpStatus.FORBIDDEN); }客户端调用该接口时,响应如下:{ "statusCode": 403, "message": "Forbidden" }2.2 HttpException 构造函数参数response(必选):定义 JSON 响应体,可传入字符串(仅覆盖 message)或对象(覆盖整个响应体);status(必选):定义 HTTP 状态码,推荐使用 HttpStatus 枚举(来自 @nestjs/common);options(可选):传入错误原因(cause),不序列化到响应体,仅用于日志记录。2.3 自定义响应体 + 错误原因示例// cats.controller.ts @Get() async findAll() { try { await this.service.findAll() } catch (error) { throw new HttpException({ status: HttpStatus.FORBIDDEN, //httpstatus/forbidden error: 'This is a custom message', }, HttpStatus.FORBIDDEN, { cause: error // 错误原因,用于日志 }); } }对应响应:{ "status": 403, "error": "This is a custom message" }三、异常日志(Exceptions logging)默认情况下,异常过滤器不记录 HttpException 及其子类、WsException、RpcException 等内置异常(视为正常应用流程);这些异常均继承自 IntrinsicException(来自 @nestjs/common),用于区分“正常应用异常”和“非预期异常”;若需记录这些异常,需自定义异常过滤器。四、自定义异常(Custom exceptions)多数场景可直接使用内置异常,若需定制,推荐继承 HttpException 构建异常层级,Nest 会自动识别并处理响应。示例:自定义 ForbiddenException// forbidden.exception.ts import { HttpException, HttpStatus } from '@nestjs/common'; export class ForbiddenException extends HttpException { constructor() { super('Forbidden', HttpStatus.FORBIDDEN); } }使用方式(与内置异常一致):// cats.controller.ts @Get() async findAll() { throw new ForbiddenException(); }五、内置 HTTP 异常(Built-in HTTP exceptions)Nest 提供一系列继承自 HttpException 的标准异常(均来自 @nestjs/common),覆盖常见 HTTP 错误场景:BadRequestException、UnauthorizedException、NotFoundException、ForbiddenExceptionNotAcceptableException、RequestTimeoutException、ConflictException、GoneExceptionHttpVersionNotSupportedException、PayloadTooLargeException、UnsupportedMediaTypeExceptionUnprocessableEntityException、InternalServerErrorException、NotImplementedExceptionImATeapotException、MethodNotAllowedException、BadGatewayExceptionServiceUnavailableException、GatewayTimeoutException、PreconditionFailedException内置异常使用示例(带原因和描述)throw new BadRequestException('Something bad happened', { cause: new Error(), // 错误原因(日志用) description: 'Some error description' // 错误描述(响应中显示) });对应响应:{ "message": "Something bad happened", "error": "Some error description", "statusCode": 400 }六、异常过滤器(Exception filters)内置过滤器可处理多数场景,若需完全控制异常层(如添加日志、自定义响应格式),可自定义异常过滤器,控制响应流程和内容。6.1 自定义 HttpExceptionFilter(捕获 HttpException)// http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; import { Request, Response } from 'express'; @Catch(HttpException) // 绑定要捕获的异常类型 export class HttpExceptionFilter implements ExceptionFilter { // 必须实现 catch 方法 catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); // 切换到 HTTP 上下文 const response = ctx.getResponse<Response>(); // 获取响应对象 const request = ctx.getRequest<Request>(); // 获取请求对象 const status = exception.getStatus(); // 获取异常状态码 // 自定义响应 response .status(status) .json({ statusCode: status, timestamp: new Date().toISOString(), path: request.url, }); } }提示:所有异常过滤器需实现 ExceptionFilter<T> 接口,T 为要捕获的异常类型,必须实现 catch(exception: T, host: ArgumentsHost) 方法。警告:若使用 @nestjs/platform-fastify,需用 response.send() 替代 response.json(),并导入 fastify 相关类型。6.2 @Catch() 装饰器用于绑定过滤器要捕获的异常类型,可传入单个类型或多个逗号分隔的类型,仅处理指定类型的异常。6.3 Arguments Host强大的工具对象,适用于所有执行上下文(HTTP、微服务、WebSocket),用于获取当前上下文的请求、响应等对象。示例中通过 host.switchToHttp() 切换到 HTTP 上下文,再通过 getResponse()、getRequest() 获取对应对象。七、绑定过滤器(Binding filters)通过 @UseFilters() 装饰器绑定(来自 @nestjs/common),支持方法级、控制器级、全局级三种作用域,推荐传入类(而非实例),支持依赖注入并减少内存占用。7.1 方法级作用域(Method-scoped)// cats.controller.ts import { Post, UseFilters, Body } from '@nestjs/common'; import { HttpExceptionFilter } from './http-exception.filter'; import { CreateCatDto } from './dto/create-cat.dto'; @Post() @UseFilters(HttpExceptionFilter) // 传入类,推荐方式 async create(@Body() createCatDto: CreateCatDto) { throw new ForbiddenException(); }也可传入实例(不推荐):@UseFilters(new HttpExceptionFilter())7.2 控制器级作用域(Controller-scoped)作用于控制器内所有路由处理器:// cats.controller.ts import { Controller, UseFilters } from '@nestjs/common'; import { HttpExceptionFilter } from './http-exception.filter'; @Controller() @UseFilters(HttpExceptionFilter) export class CatsController {}7.3 全局级作用域(Global-scoped)作用于整个应用的所有控制器和路由处理器,有两种实现方式。方式1:main.ts 中使用 useGlobalFilters()// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from './http-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalFilters(new HttpExceptionFilter()); // 传入实例 await app.listen(process.env.PORT ?? 3000); } bootstrap();警告:useGlobalFilters() 不支持网关(gateways)或混合应用;且该方式注册的全局过滤器,无法注入依赖(脱离模块上下文)。方式2:模块中使用 APP_FILTER 令牌(支持依赖注入)// app.module.ts import { Module } from '@nestjs/common'; import { APP_FILTER } from '@nestjs/core'; import { HttpExceptionFilter } from './http-exception.filter'; @Module({ providers: [ { provide: APP_FILTER, useClass: HttpExceptionFilter, // 传入过滤器类 }, ], }) export class AppModule {}提示:该方式注册的过滤器仍为全局,推荐在过滤器所在的模块中配置;可注册多个过滤器,依次添加到 providers 数组即可。八、捕获所有异常(Catch everything)若需捕获所有未处理异常(无论类型),只需让 @Catch() 装饰器为空(不传入任何参数)。以下为平台无关的实现(使用 HTTP 适配器,不直接依赖 Express/Fastify 原生对象):import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; @Catch() // 空装饰器,捕获所有异常 export class CatchEverythingFilter implements ExceptionFilter { constructor(private readonly httpAdapterHost: HttpAdapterHost) {} catch(exception: unknown, host: ArgumentsHost): void { const { httpAdapter } = this.httpAdapterHost; const ctx = host.switchToHttp(); // 确定响应状态码 const httpStatus = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; // 自定义响应体 const responseBody = { statusCode: httpStatus, timestamp: new Date().toISOString(), path: httpAdapter.getRequestUrl(ctx.getRequest()), }; // 发送响应 httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus); } }警告:若同时使用“捕获所有异常”的过滤器和“捕获特定异常”的过滤器,需先声明“捕获所有”的过滤器,确保特定过滤器能正常生效。九、继承内置过滤器(Inheritance)若需扩展内置全局异常过滤器的行为,可继承 BaseExceptionFilter,并调用 super.catch() 委托异常处理给父类。9.1 自定义继承过滤器// all-exceptions.filter.ts import { Catch, ArgumentsHost } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; @Catch() export class AllExceptionsFilter extends BaseExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { // 可添加自定义逻辑(如日志) super.catch(exception, host); // 委托给父类处理 } }警告:方法级、控制器级的继承过滤器,不可用 new 实例化,需让框架自动实例化。9.2 全局继承过滤器的注册方式方式1:main.ts 中注入 HttpAdapter// main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); const { httpAdapter } = app.get(HttpAdapterHost); // 传入 httpAdapter 实例化过滤器 app.useGlobalFilters(new AllExceptionsFilter(httpAdapter)); await app.listen(process.env.PORT ?? 3000); } bootstrap();方式2:模块中使用 APP_FILTER 令牌(推荐,支持依赖注入)// app.module.ts @Module({ providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter, }, ], }) export class AppModule {}Pipes一、Pipes 核心概念Pipe 是一个使用 @Injectable() 装饰器注解、并实现 PipeTransform 接口的类,主要作用于控制器路由处理器的参数,在方法被调用前介入,对参数进行处理后,再将(可能经过转换的)参数传递给路由处理器。1.1 Pipes 的两大典型用途转换(transformation):将输入数据转换为期望的格式(例如,将字符串转换为整数);验证(validation):校验输入数据,若合法则直接原样传递;若不合法则抛出异常。提示:Pipes 运行在异常区域内。这意味着当 Pipe 抛出异常时,会由异常层(全局异常过滤器和当前上下文应用的任何异常过滤器)处理;一旦 Pipe 抛出异常,后续的控制器方法将不会执行。这是在系统边界校验外部输入数据的最佳实践。二、内置 Pipes(Built-in pipes)Nest 提供了多个可直接开箱即用的内置 Pipes,均从 @nestjs/common 包导出,覆盖常见的转换和验证场景:ValidationPipe、ParseIntPipe、ParseFloatPipe、ParseBoolPipeParseArrayPipe、ParseUUIDPipe、ParseEnumPipe、DefaultValuePipeParseFilePipe、ParseDatePipe2.1 内置 Pipe 示例:ParseIntPipeParseIntPipe 是转换类 Pipes 的典型示例,确保方法处理器的参数被转换为 JavaScript 整数;若转换失败则抛出异常。以下示例同样适用于其他 Parse* 系列 Pipes(ParseBoolPipe、ParseFloatPipe 等)。三、绑定 Pipes(Binding pipes)使用 Pipe 需将其实例绑定到合适的上下文,常用参数级绑定(针对特定路由方法的参数),也支持方法级、控制器级、全局级绑定。绑定方式分为“传入类”(推荐,支持依赖注入)和“传入实例”(可自定义配置)。3.1 参数级绑定(最常用)示例1:传入类(默认配置)@Get(':id') async findOne(@Param('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); }效果:若请求为 GET localhost:3000/abc(非数字 id),Nest 会抛出如下异常,阻止 findOne() 方法执行:{ "statusCode": 400, "message": "Validation failed (numeric string is expected)", "error": "Bad Request" }示例2:传入实例(自定义配置)通过实例化 Pipe 并传入配置,自定义异常状态码等行为:@Get(':id') async findOne( @Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE })) id: number, ) { return this.catsService.findOne(id); }3.2 其他参数类型的绑定示例1:绑定到查询参数(@Query)@Get() async findOne(@Query('id', ParseIntPipe) id: number) { return this.catsService.findOne(id); }示例2:ParseUUIDPipe 绑定(校验 UUID)@Get(':uuid') async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) { return this.catsService.findOne(uuid); }提示:ParseUUIDPipe 默认支持 UUID 3、4、5 版本,若需指定特定版本,可在 Pipe 配置中传入 version 参数。提示:验证类 Pipes(如 ValidationPipe)的绑定方式与转换类 Pipes 略有不同,详见后续“绑定验证 Pipes”章节。更多验证技巧可参考官方“Validation techniques”文档。四、自定义 Pipes(Custom pipes)尽管 Nest 提供了完善的内置 Pipes,但可通过自定义 Pipes 实现个性化需求。自定义 Pipes 需实现 PipeTransform 接口,并编写 transform() 方法。4.1 自定义 Pipe 基础结构所有自定义 Pipes 必须实现 transform() 方法,该方法接收两个参数:value:当前处理的方法参数(路由处理器接收前的值);metadata:当前处理参数的元数据,描述参数的类型、元类型等信息。基础示例:空验证 Pipe(Identity Function)// validation.pipe.ts import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common'; @Injectable() export class ValidationPipe implements PipeTransform { transform(value: any, metadata: ArgumentMetadata) { return value; // 原样返回参数,无任何处理 } }提示:PipeTransform<T, R> 是泛型接口,T 表示输入值类型,R 表示 transform() 方法的返回值类型。4.2 参数元数据(ArgumentMetadata)metadata 参数是 ArgumentMetadata 类型的对象,包含以下属性:export interface ArgumentMetadata { type: 'body' | 'query' | 'param' | 'custom'; // 参数类型(body/query/param/自定义) metatype?: Type<unknown>; // 参数的元类型(如 String、Number,未声明类型则为 undefined) data?: string; // 装饰器中传入的字符串(如 @Body('name') 中的 'name',空则为 undefined) }警告:TypeScript 接口在转译时会消失,若方法参数类型声明为接口(而非类),则 metatype 值为 Object。五、基于 Schema 的验证(Schema based validation)通过 Schema 定义参数规则,实现统一、可复用的验证逻辑,常用 Zod 库实现(Schema 简洁、API 易读)。5.1 基于 Zod 的自定义验证 Pipe步骤1:安装依赖$ npm install --save zod步骤2:实现 ZodValidationPipeimport { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { ZodSchema } from 'zod'; export class ZodValidationPipe implements PipeTransform { constructor(private schema: ZodSchema) {} // 接收 Zod Schema 作为参数 transform(value: unknown, metadata: ArgumentMetadata) { try { const parsedValue = this.schema.parse(value); // 校验参数 return parsedValue; // 校验通过,返回解析后的值 } catch (error) { throw new BadRequestException('Validation failed'); // 校验失败,抛出异常 } } }5.2 绑定 Zod 验证 Pipe步骤1:定义 Zod Schema 和 DTO 类型import { z } from 'zod'; // 定义 Zod Schema export const createCatSchema = z .object({ name: z.string(), age: z.number(), breed: z.string(), }) .required(); // 从 Schema 推导 DTO 类型 export type CreateCatDto = z.infer<typeof createCatSchema>;步骤2:通过 @UsePipes() 绑定到方法// cats.controller.ts import { Post, UsePipes, Body } from '@nestjs/common'; import { ZodValidationPipe } from './zod-validation.pipe'; import { createCatSchema, CreateCatDto } from './dto/create-cat.dto'; @Post() @UsePipes(new ZodValidationPipe(createCatSchema)) // 传入 Schema 实例化 Pipe async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }提示:@UsePipes() 装饰器从 @nestjs/common 导入。警告:zod 库要求在 tsconfig.json 文件中启用 strictNullChecks 配置。六、基于 Class Validator 的验证(Class validator)结合 class-validator 库,使用装饰器实现验证逻辑,让 DTO 类成为参数验证的唯一数据源(无需单独定义 Schema)。该方式仅支持 TypeScript,不支持原生 JavaScript。6.1 实现步骤步骤1:安装依赖$ npm i --save class-validator class-transformer步骤2:给 DTO 类添加验证装饰器// create-cat.dto.ts import { IsString, IsInt } from 'class-validator'; export class CreateCatDto { @IsString() // 验证 name 为字符串 name: string; @IsInt() // 验证 age 为整数 age: number; @IsString() // 验证 breed 为字符串 breed: string; }提示:更多 class-validator 装饰器用法,可参考其官方文档。步骤3:实现 ValidationPipe// validation.pipe.ts import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; import { validate } from 'class-validator'; import { plainToInstance } from 'class-transformer'; @Injectable() export class ValidationPipe implements PipeTransform<any> { async transform(value: any, { metatype }: ArgumentMetadata) { // 跳过原生 JavaScript 类型的参数(无验证装饰器) if (!metatype || !this.toValidate(metatype)) { return value; } // 将普通对象转换为带类型的实例(让 class-validator 能识别装饰器) const object = plainToInstance(metatype, value); // 执行验证 const errors = await validate(object); // 验证失败,抛出异常 if (errors.length > 0) { throw new BadRequestException('Validation failed'); } // 验证通过,原样返回 return value; } // 辅助方法:判断是否需要验证(排除原生 JavaScript 类型) private toValidate(metatype: Function): boolean { const types: Function[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); } }提示:Nest 已内置 ValidationPipe,功能比自定义示例更完善,本示例仅用于演示自定义验证 Pipe 的实现逻辑。内置 ValidationPipe 的详细用法可参考官方文档。说明:class-transformer 库与 class-validator 同属一个作者,兼容性极佳,用于将普通对象转换为带类型的实例,让验证装饰器生效。步骤4:绑定 ValidationPipe可绑定到参数级(仅验证单个参数),也支持方法级、控制器级、全局级绑定:// cats.controller.ts @Post() async create( @Body(new ValidationPipe()) createCatDto: CreateCatDto, // 参数级绑定 ) { this.catsService.create(createCatDto); }参数级绑定适用于验证逻辑仅针对单个参数的场景。七、全局级 Pipes(Global scoped pipes)通用的 Pipes(如 ValidationPipe)可设置为全局级,作用于整个应用的所有控制器和路由处理器,无需重复绑定。7.1 方式1:main.ts 中使用 useGlobalPipes()// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from './validation.pipe'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); // 注册全局 Pipe await app.listen(process.env.PORT ?? 3000); } bootstrap();注意:混合应用中,useGlobalPipes() 不会为网关和微服务注册 Pipes;非混合微服务应用中,useGlobalPipes() 会全局挂载 Pipes。缺点:该方式注册的全局 Pipe 脱离模块上下文,无法注入依赖。7.2 方式2:模块中使用 APP_PIPE 令牌(推荐)通过模块 providers 注册,支持依赖注入,注册后仍为全局生效:// app.module.ts import { Module } from '@nestjs/common'; import { APP_PIPE } from '@nestjs/core'; import { ValidationPipe } from './validation.pipe'; @Module({ providers: [ { provide: APP_PIPE, useClass: ValidationPipe, // 传入全局 Pipe 类 }, ], }) export class AppModule {}提示:建议在 Pipe 所在的模块中配置;可注册多个全局 Pipe,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。八、转换场景实战(Transformation use case)转换类 Pipes 的核心作用是修改输入参数,使其符合路由处理器的期望格式(返回值会完全覆盖原参数值),常见场景:类型转换、默认值填充、参数预处理等。8.1 示例1:自定义 ParseIntPipe以下为简化版 ParseIntPipe(Nest 内置版本更完善),演示转换类 Pipe 的实现逻辑:// parse-int.pipe.ts import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'; @Injectable() export class ParseIntPipe implements PipeTransform<string, number> { transform(value: string, metadata: ArgumentMetadata): number { const val = parseInt(value, 10); // 将字符串转换为整数 if (isNaN(val)) { // 转换失败,抛出异常 throw new BadRequestException('Validation failed'); } return val; // 转换成功,返回整数 } }绑定使用:@Get(':id') async findOne(@Param('id', new ParseIntPipe()) id) { return this.catsService.findOne(id); }8.2 示例2:根据 ID 查询实体通过 Pipe 抽象“根据 ID 查询实体”的逻辑,减少路由处理器的重复代码:@Get(':id') findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) { return userEntity; // Pipe 已返回查询到的实体,直接返回 }说明:UserByIdPipe 接收 id 作为输入,查询数据库并返回 UserEntity 实例,实现代码可自行扩展。九、默认值填充(Providing defaults)Parse 系列 Pipes 要求参数必须存在(null/undefined 会抛出异常),若需处理参数缺失的场景,可使用 DefaultValuePipe 先填充默认值,再由 Parse Pipe 处理。示例:默认值 + Parse* Pipe 组合使用@Get() async findAll( // 默认值为 false,再转换为布尔值 @Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean, // 默认值为 0,再转换为整数 @Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number, ) { return this.catsService.findAll({ activeOnly, page }); }Guards一、Guards 核心概念Guard 是一个使用 @Injectable() 装饰器注解、并实现 CanActivate 接口的类,核心职责是根据运行时的特定条件(如权限、角色、访问控制列表等),决定请求是否能被路由处理器处理,这一过程通常被称为“授权”。在传统 Express 应用中,授权(及与其协作的认证)通常由中间件处理。中间件适合处理认证(如令牌验证、给请求对象附加属性),因为这些操作与特定路由上下文及其元数据关联不强;但中间件本身“无感知”,不知道调用 next() 后会执行哪个处理器。与中间件不同,Guards 可访问 ExecutionContext 实例,能精确知晓接下来要执行的内容。它与异常过滤器、管道、拦截器类似,可在请求/响应周期的正确节点插入处理逻辑,且支持声明式使用,能保持代码简洁、复用(DRY 原则)。提示:Guards 在所有中间件执行完毕后运行,但在任何拦截器或管道之前执行。二、授权 Guard(Authorization guard)授权是 Guards 的典型应用场景——特定路由仅允许具有足够权限的调用者(通常是已认证用户)访问。下面实现的 AuthGuard 假设用户已认证(请求头中包含令牌),其核心逻辑是提取并验证令牌,根据验证结果决定请求是否可继续。2.1 AuthGuard 基础实现// auth.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class AuthGuard implements AuthGuard { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { const request = context.switchToHttp().getRequest(); return validateRequest(request); // 自定义令牌验证逻辑 } }提示:若需真实场景的认证机制实现示例,可参考官方对应章节;如需更复杂的授权示例,可查看官方相关文档。2.2 canActivate() 方法核心说明所有 Guards 必须实现 canActivate() 方法,该方法返回一个布尔值(或 Promise、Observable),用于控制请求的后续流程:返回 true:请求将被正常处理(路由处理器执行);返回 false:Nest 将拒绝请求,阻止路由处理器执行。注:validateRequest() 函数的逻辑可根据需求灵活设计,示例重点展示 Guards 在请求周期中的作用。三、执行上下文(Execution context)canActivate() 方法接收唯一参数——ExecutionContext 实例,该实例继承自 ArgumentsHost(在异常过滤器章节中已介绍)。示例中通过 context.switchToHttp().getRequest() 获取请求对象,与异常过滤器中使用 ArgumentsHost 的方式一致。除了继承 ArgumentsHost 的辅助方法,ExecutionContext 还新增了多个辅助方法,可提供当前执行过程的更多细节,有助于构建可跨多个控制器、方法和执行上下文的通用 Guards。更多关于 ExecutionContext 的用法,可参考官方文档。AuthGuard:NestJS 的 “路由安全门卫”,用于接口的认证 / 授权,在请求到达 Controller 前验证权限,支持局部 / 全局使用;Observable:RxJS 的 “异步数据流容器”,用于处理多值、可取消的异步场景,是 NestJS 处理实时数据、微服务的核心方式,比 Promise 更灵活。四、基于角色的认证(Role-based authentication)下面实现一个更实用的 Guards——仅允许具有特定角色的用户访问路由。先从基础模板开始,后续逐步完善功能(当前默认允许所有请求)。4.1 RolesGuard 基础模板// roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class RolesGuard implements CanActivate { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { return true; // 暂时允许所有请求 } }五、绑定 Guards(Binding guards)与管道、异常过滤器类似,Guards 支持三种作用域:控制器级、方法级、全局级。通过 @UseGuards() 装饰器绑定,该装饰器可接收单个参数或多个逗号分隔的参数,方便一次性应用多个 Guards。5.1 控制器级绑定作用于控制器内所有路由处理器:import { Controller, UseGuards } from '@nestjs/common'; import { RolesGuard } from './roles.guard'; @Controller('cats') @UseGuards(RolesGuard) // 绑定到整个控制器 export class CatsController {}提示:@UseGuards() 装饰器从 @nestjs/common 导入。两种绑定方式传入类(推荐):如上述示例,由框架负责实例化,支持依赖注入;传入实例:直接实例化 Guards,适用于需要自定义配置的场景: `@Controller('cats') @UseGuards(new RolesGuard()) // 传入实例 export class CatsController {} `5.2 方法级绑定仅作用于单个路由处理器,在方法上添加 @UseGuards() 装饰器即可:@Controller('cats') export class CatsController { @Post() @UseGuards(RolesGuard) // 仅绑定到 create 方法 async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); } }5.3 全局级绑定(Global scoped guards)作用于整个应用的所有控制器和路由处理器,有两种实现方式。方式1:main.ts 中使用 useGlobalGuards()// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { RolesGuard } from './roles.guard'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalGuards(new RolesGuard()); // 注册全局 Guard await app.listen(process.env.PORT ?? 3000); } bootstrap();注意:混合应用中,useGlobalGuards() 默认不会为网关和微服务注册 Guards(可参考官方“混合应用”章节修改此行为);非混合微服务应用中,useGlobalGuards() 会全局挂载 Guards。缺点:该方式注册的全局 Guard 脱离模块上下文,无法注入依赖。方式2:模块中使用 APP_GUARD 令牌(推荐)通过模块 providers 注册,支持依赖注入,注册后仍为全局生效:// app.module.ts import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { RolesGuard } from './roles.guard'; @Module({ providers: [ { provide: APP_GUARD, useClass: RolesGuard, // 传入全局 Guard 类 }, ], }) export class AppModule {}提示:建议在 Guard 所在的模块中配置;可注册多个全局 Guard,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。六、为处理器设置角色(Setting roles per handler)当前的 RolesGuard 仅能固定允许/拒绝请求,无法根据不同路由的角色要求灵活控制。Nest 提供自定义元数据功能,可通过 Reflector.createDecorator 静态方法创建装饰器,或使用内置 @SetMetadata() 装饰器,将角色信息附加到路由处理器上。6.1 方式1:使用 Reflector 创建 @Roles() 装饰器Reflector 是 Nest 内置工具类,从 @nestjs/core 导出,用于创建自定义元数据装饰器:// roles.decorator.ts import { Reflector } from '@nestjs/core'; // 创建 Roles 装饰器,接收字符串数组(角色列表) export const Roles = Reflector.createDecorator<string[]>();6.2 为路由处理器附加角色元数据使用 @Roles() 装饰器标注路由,指定该路由允许的角色:// cats.controller.ts import { Post, UseGuards, Body } from '@nestjs/common'; import { Roles } from './roles.decorator'; import { CreateCatDto } from './dto/create-cat.dto'; @Post() @Roles(['admin']) // 仅允许 admin 角色访问该接口 async create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto); }6.3 方式2:使用内置 @SetMetadata() 装饰器除了自定义装饰器,也可直接使用 Nest 内置的 @SetMetadata() 装饰器附加元数据,具体用法可参考官方文档。七、整合实现 RolesGuard(Putting it all together)完善 RolesGuard 逻辑:通过 Reflector 提取当前路由的角色元数据,对比当前用户的角色与路由要求的角色,根据匹配结果决定是否允许请求。完整实现// roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Roles } from './roles.decorator'; @Injectable() export class RolesGuard implements CanActivate { // 注入 Reflector,用于提取元数据 constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { // 提取当前路由处理器的角色元数据(通过 Roles 装饰器附加) const roles = this.reflector.get(Roles, context.getHandler()); // 若未设置角色元数据,默认允许请求 if (!roles) { return true; } // 获取当前请求对象,提取用户信息(假设 request.user 包含用户角色) const request = context.switchToHttp().getRequest(); const user = request.user; // 对比用户角色与路由要求的角色(matchRoles 为自定义逻辑) return matchRoles(roles, user.roles); } }提示:在 Node.js 生态中,通常会将已认证用户信息附加到 request.user 上。实际应用中,可在自定义认证 Guard(或中间件)中实现这一关联,更多细节可参考官方认证相关章节。警告:matchRoles() 函数的逻辑可根据需求灵活设计(如精确匹配、包含匹配等),示例重点展示 Guards 如何结合元数据实现角色授权。提示:如需更详细的元数据提取用法,可参考官方“执行上下文”章节中的“反射与元数据”部分。八、Guards 异常处理默认异常响应当用户权限不足(Guards 返回 false)时,Nest 会自动返回如下响应:{ "statusCode": 403, "message": "Forbidden resource", "error": "Forbidden" }底层逻辑:当 Guards 返回 false 时,框架会抛出 ForbiddenException。自定义异常响应若需返回自定义错误,可在 Guards 中直接抛出特定异常(如未授权异常):import { UnauthorizedException } from '@nestjs/common'; // 在 canActivate 方法中抛出异常 throw new UnauthorizedException();注:Guards 中抛出的任何异常,都会由异常层(全局异常过滤器和当前上下文应用的任何异常过滤器)处理。提示:若需真实场景的授权实现示例,可参考官方对应章节。Interceptors一、Interceptors 核心概念Interceptor(拦截器)是一个使用 @Injectable() 装饰器注解、并实现 NestInterceptor 接口的类,其设计灵感来源于面向切面编程(AOP)技术,核心作用是在路由处理器执行的前后插入自定义逻辑,对请求/响应流进行灵活控制和处理。Interceptors 的核心能力在方法执行前/后绑定额外逻辑;转换函数返回的结果(响应数据);转换函数抛出的异常;扩展基础函数的行为;根据特定条件完全重写函数(如实现缓存功能)。二、基础用法(Basics)所有拦截器必须实现 intercept() 方法,该方法接收两个核心参数,构成拦截器的核心工作流。2.1 intercept() 方法参数ExecutionContext(执行上下文):与 Guards 中的 ExecutionContext 完全一致,继承自 ArgumentsHost,封装了当前请求的执行信息(如请求对象、路由处理器元数据等)。 ArgumentsHost 是一个包装原始处理器参数的工具类,会根据应用类型(HTTP、微服务等)提供不同的参数数组,详细用法可参考异常过滤器章节中的相关内容。CallHandler(调用处理器):实现了 handle() 方法,用于触发路由处理器的执行。若在 intercept() 方法中不调用 handle(),则路由处理器将完全不会执行。2.2 核心工作原理intercept() 方法本质上是对请求/响应流的“包裹”,因此可以在路由处理器执行前后分别实现自定义逻辑:在调用 next.handle() 之前编写的代码,会在路由处理器执行前执行;next.handle() 会返回一个 RxJS Observable,该流包含路由处理器的返回结果,通过 RxJS 操作符可对响应流进行后续处理(如转换、异常捕获),实现“执行后”的逻辑。提示:在面向切面编程(AOP)术语中,调用 next.handle()(即触发路由处理器执行)的操作称为“切入点(Pointcut)”,是插入额外逻辑的关键节点。2.3 示例说明以 POST /cats 请求为例:该请求最终会触发 CatsController 中的 create() 方法。若拦截器中未调用 handle(),则 create() 方法不会执行;一旦调用 handle() 并返回其 Observable,create() 方法会被触发,且可通过 Observable 操作符对响应流进行后续处理。三、执行上下文(Execution context)ExecutionContext 继承自 ArgumentsHost,除了拥有 ArgumentsHost 的所有辅助方法(如获取请求对象),还新增了多个辅助方法,可提供当前执行过程的更多细节(如当前路由处理器、控制器信息)。这些方法有助于构建通用型拦截器,使其能够跨多个控制器、方法和执行上下文(如 HTTP、微服务)工作。更多关于 ExecutionContext 的用法,可参考官方文档。四、调用处理器(Call handler)CallHandler 的核心方法是 handle(),其核心作用是触发后续的路由处理器执行,并返回一个 RxJS Observable,该 Observable 包含路由处理器的返回结果(响应数据)或抛出的异常。通过 handle() 返回的 Observable,可借助 RxJS 丰富的操作符(如 tap、map、catchError)对响应流进行灵活处理,这是拦截器实现“转换响应、捕获异常”等能力的核心基础。五、切面拦截(Aspect interception)切面拦截是拦截器的典型应用场景之一,例如记录用户交互(存储用户调用记录、异步分发事件、计算请求耗时等)。以下是一个简单的 LoggingInterceptor(日志拦截器),演示拦截器的基础实现。5.1 LoggingInterceptor 实现// logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { console.log('Before...'); // 路由处理器执行前的逻辑 const now = Date.now(); return next .handle() .pipe( // tap 操作符:监听流的终止(正常/异常),不干扰响应流 tap(() => console.log(`After... ${Date.now() - now}ms`)), // 路由处理器执行后的逻辑 ); } }提示:NestInterceptor<T, R> 是泛型接口,T 表示 Observable 的类型(支持响应流),R 表示 Observable 包裹的值的类型。说明:拦截器与控制器、提供者、Guards 等一样,可通过构造函数注入依赖。5.2 关键操作符说明示例中使用了 RxJS 的 tap() 操作符,其作用是:在 Observable 流正常终止或异常终止时,执行自定义逻辑(如日志打印),但不会修改或干扰响应流本身,适合用于记录日志、统计耗时等场景。六、绑定 Interceptors(Binding interceptors)与 Pipes、Guards 类似,拦截器支持三种作用域:控制器级、方法级、全局级。通过 @UseInterceptors() 装饰器绑定,该装饰器可接收单个参数或多个逗号分隔的参数,方便一次性应用多个拦截器。提示:@UseInterceptors() 装饰器从 @nestjs/common 导入。6.1 控制器级绑定作用于控制器内所有路由处理器:// cats.controller.ts import { Controller, UseInterceptors } from '@nestjs/common'; import { LoggingInterceptor } from './logging.interceptor'; @Controller('cats') @UseInterceptors(LoggingInterceptor) // 绑定到整个控制器 export class CatsController {}效果:调用 CatsController 中的任何路由(如 GET /cats、POST /cats),都会触发 LoggingInterceptor 的逻辑,控制台会输出:Before... After... 1ms两种绑定方式传入类(推荐):如上述示例,由框架负责实例化,支持依赖注入;传入实例:直接实例化拦截器,适用于需要自定义配置的场景: `@Controller('cats') @UseInterceptors(new LoggingInterceptor()) // 传入实例 export class CatsController {} `6.2 方法级绑定仅作用于单个路由处理器,在方法上添加 @UseInterceptors() 装饰器即可:@Controller('cats') export class CatsController { @Get() @UseInterceptors(LoggingInterceptor) // 仅绑定到 findAll 方法 async findAll() { return this.catsService.findAll(); } }6.3 全局级绑定(Global scoped interceptors)作用于整个应用的所有控制器和路由处理器,有两种实现方式。方式1:main.ts 中使用 useGlobalInterceptors()// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { LoggingInterceptor } from './logging.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.useGlobalInterceptors(new LoggingInterceptor()); // 注册全局拦截器 await app.listen(process.env.PORT ?? 3000); } bootstrap();方式2:模块中使用 APP_INTERCEPTOR 令牌(推荐)通过模块 providers 注册,支持依赖注入,注册后仍为全局生效。解决了方式1“无法注入依赖”的问题(方式1注册的全局拦截器脱离模块上下文):// app.module.ts import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LoggingInterceptor } from './logging.interceptor'; @Module({ providers: [ { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, // 传入全局拦截器类 }, ], }) export class AppModule {}提示:建议在拦截器所在的模块中配置;可注册多个全局拦截器,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。七、响应映射(Response mapping)由于 next.handle() 返回 RxJS Observable,可借助 map() 操作符轻松修改响应流中的数据,实现响应格式的统一转换(如给所有响应包裹一层固定结构)。警告:响应映射功能不支持库特定的响应策略(禁止直接使用 @Res() 对象)。7.1 TransformInterceptor 实现(统一响应格式)该拦截器将路由处理器返回的结果,包裹到 { data: T } 结构中,实现全局响应格式统一:// transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // 定义统一响应格式的接口 export interface Response<T> { data: T; } @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> { // map 操作符:修改响应流中的数据 return next.handle().pipe(map(data => ({ data }))); } }7.2 效果演示若路由处理器返回空数组 [](如 GET /cats),经过拦截器处理后,客户端收到的响应将是:{ "data": [] }提示:Nest 拦截器同时支持同步和异步的 intercept() 方法,必要时可直接将方法改为 async。7.3 示例2:空值转换将响应中的所有 null 值转换为空字符串 '',绑定全局后可作用于所有接口:import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Injectable() export class ExcludeNullInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next .handle() .pipe(map(value => value === null ? '' : value )); // 空值转换逻辑 } }八、异常映射(Exception mapping)借助 RxJS 的 catchError()操作符,可捕获路由处理器抛出的异常,并将其转换为自定义异常,实现全局异常统一处理。8.1 ErrorsInterceptor 实现(异常转换)// errors.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, BadGatewayException, CallHandler, } from '@nestjs/common'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; @Injectable() export class ErrorsInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next .handle() .pipe( // catchError 操作符:捕获流中的异常并转换 catchError(err => throwError(() => new BadGatewayException())), ); } }效果:无论路由处理器抛出何种异常,都会被捕获并转换为 BadGatewayException(状态码 502)。九、流重写(Stream overriding)拦截器可根据特定条件,完全阻止路由处理器的执行,直接返回自定义的响应流(如实现缓存功能)。核心是:不调用 next.handle(),而是返回一个新的 Observable。9.1 CacheInterceptor 实现(基础缓存)以下是一个简单的缓存拦截器示例(实际场景需考虑缓存过期时间、缓存失效、缓存大小等因素):// cache.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable, of } from 'rxjs'; @Injectable() export class CacheInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const isCached = true; // 模拟缓存存在(实际场景需判断缓存是否有效) if (isCached) { return of([]); // 返回自定义响应流,不执行路由处理器 } return next.handle(); // 缓存不存在,执行路由处理器并返回其响应流 } }关键说明使用 RxJS 的 of() 操作符创建新的 Observable,返回固定响应 [];当 isCached = true 时,不调用 next.handle(),路由处理器不会执行,直接返回缓存数据;若要实现通用缓存方案,可结合 Reflector(参考 Guards 章节)和自定义装饰器,为不同路由配置不同缓存规则。十、更多操作符应用(More operators)借助 RxJS 丰富的操作符,可实现更多实用功能。以下示例实现“请求超时处理”:当路由处理器在指定时间内未返回响应,自动终止并抛出超时异常。10.1 TimeoutInterceptor 实现(请求超时)// timeout.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common'; import { Observable, throwError, TimeoutError } from 'rxjs'; import { catchError, timeout } from 'rxjs/operators'; @Injectable() export class TimeoutInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe( timeout(5000), // 设置超时时间:5秒 catchError(err => { // 捕获超时异常,转换为 RequestTimeoutException(状态码 408) if (err instanceof TimeoutError) { return throwError(() => new RequestTimeoutException()); } // 非超时异常,原样抛出 return throwError(() => err); }), ); }; };效果说明若路由处理器执行时间超过 5 秒,请求会被自动终止,客户端收到 408(Request Timeout)响应。可在抛出异常前添加自定义逻辑(如释放资源、记录超时日志)。Custom decorators一、装饰器核心基础Nest 框架的核心构建基于 装饰器(Decorators) 这一语言特性。装饰器在许多常用编程语言中是成熟的概念,但在 JavaScript 领域仍相对较新。1.1 装饰器定义(ES2016 标准)ES2016 装饰器是一个返回函数的表达式,可接收三个参数:目标(target)、名称(name)和属性描述符(property descriptor)。使用方式:在需要装饰的对象(类、方法、属性)顶部,添加 @ 前缀 + 装饰器名称。装饰器可用于修饰类、方法或属性。提示:若需深入理解装饰器工作原理,建议参考官方推荐的相关文章。二、参数装饰器(Param decorators)Nest 提供了一组实用的内置参数装饰器,可直接用于 HTTP 路由处理器中,用于快速提取请求中的相关数据,对应底层 Express(或 Fastify)的请求对象。2.1 内置参数装饰器列表以下是 Nest 内置参数装饰器及其对应的 Express/Fastify 对象:Nest 装饰器对应底层对象@Request()、@Req()req(完整请求对象)@Response()、@Res()res(完整响应对象)@Next()next(下一步中间件函数)@Session()req.session(请求会话对象)@Param(param?: string)req.params(路由参数对象);传入参数名时,获取 req.params[param]@Body(param?: string)req.body(请求体对象);传入参数名时,获取 req.body[param]@Query(param?: string)req.query(查询参数对象);传入参数名时,获取 req.query[param]@Headers(param?: string)req.headers(请求头对象);传入参数名时,获取 req.headers[param]@Ip()req.ip(请求客户端 IP 地址)@HostParam()req.hosts(请求主机信息)2.2 自定义参数装饰器的意义在 Node.js 生态中,常见做法是将自定义属性附加到 req 对象上(如认证后的用户信息 req.user),此时需要在每个路由处理器中手动提取:const user = req.user;自定义参数装饰器可将这一重复操作封装,提升代码可读性和复用性,可在所有控制器中统一复用。三、自定义参数装饰器实现Nest 提供 createParamDecorator()方法,用于创建自定义参数装饰器,该方法接收两个参数:装饰器工厂函数、执行上下文(ExecutionContext)。3.1 基础实现(提取 req.user)创建 @User() 装饰器,用于快速提取请求对象中的 user 属性:// user.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const User = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { // 切换到 HTTP 上下文,获取请求对象 const request = ctx.switchToHttp().getRequest(); // 返回 req.user 属性 return request.user; }, );3.2 装饰器使用方式在控制器路由处理器中,直接使用 @User() 装饰器即可提取用户信息:@Get() async findOne(@User() user: UserEntity) { console.log(user); // 直接获取 req.user }四、传递数据到装饰器(Passing data)当装饰器的行为需要根据特定条件调整时,可通过 data 参数(装饰器工厂函数的第一个参数),向装饰器传递自定义参数。典型场景:创建可根据属性名,提取 req.user 中特定字段的自定义装饰器。4.1 场景假设认证层验证请求后,会在 req.user 中附加用户实体,格式如下:{ "id": 101, "firstName": "Alan", "lastName": "Turing", "email": "alan@email.com", "roles": ["admin"] }4.2 带参数的装饰器实现修改 @User() 装饰器,支持传入属性名,提取对应字段(若未传入字段名,则返回完整 user 对象):// user.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export const User = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const user = request.user; // 若传入 data(属性名),则返回 user[data],否则返回完整 user 对象 return data ? user?.[data] : user; }, );4.3 带参数的装饰器使用传入属性名,即可提取 req.user 中的特定字段,提升代码可读性:@Get() async findOne(@User('firstName') firstName: string) { console.log(`Hello ${firstName}`); // 输出:Hello Alan }可通过传入不同属性名,提取 user 对象中的不同字段;若 user 对象结构复杂,该方式可大幅简化路由处理器代码。提示:TypeScript 用户可利用 createParamDecorator<T>() 泛型确保类型安全。例如 createParamDecorator<string>((data, ctx) => ...),或在工厂函数中指定 data: string;若两者都省略,data 类型将为 any。五、与管道配合使用(Working with pipes)Nest 会将自定义参数装饰器与内置参数装饰器(@Body()、@Param()、@Query())同等对待,这意味着管道(Pipes)也会对自定义装饰器标注的参数执行验证/转换逻辑。此外,可直接将管道应用到自定义参数装饰器上,需注意设置 validateCustomDecorators: true(默认不验证自定义装饰器标注的参数)。5.1 示例:结合 ValidationPipe@Get() async findOne( // 将 ValidationPipe 应用到 @User() 装饰器,开启自定义装饰器验证 @User(new ValidationPipe({ validateCustomDecorators: true })) user: UserEntity, ) { console.log(user); }提示:必须设置 validateCustomDecorators: true,否则 ValidationPipe 不会验证自定义装饰器标注的参数。六、装饰器组合(Decorator composition)Nest 提供 applyDecorators() 辅助方法,用于将多个装饰器组合成一个单一装饰器,适用于将一组相关装饰器(如认证、授权相关)封装复用。6.1 组合装饰器实现创建 @Auth() 装饰器,组合元数据设置、Guard 应用、Swagger 文档注解等多个装饰器:// auth.decorator.ts import { applyDecorators } from '@nestjs/common'; export function Auth(...roles: Role[]) { return applyDecorators( SetMetadata('roles', roles), // 设置角色元数据 UseGuards(AuthGuard, RolesGuard), // 应用认证、角色守卫 ApiBearerAuth(), // Swagger Bearer 认证注解 ApiUnauthorizedResponse({ description: 'Unauthorized' }), // Swagger 未授权响应注解 ); }6.2 组合装饰器使用使用单一 @Auth() 装饰器,即可应用所有组合的装饰器,简化代码:@Get('users') @Auth('admin') // 仅需一行,应用所有组合的装饰器 findAllUsers() {}警告:@nestjs/swagger 包中的 @ApiHideProperty() 装饰器不可组合,与 applyDecorators() 函数配合使用时会失效。简单记:@ApiHideProperty() 别放进 applyDecorators(),要么单独用,要么换 @ApiProperty({ hidden: true }) 组合。
2026年02月16日
10 阅读
0 评论
4 点赞
1
2
3