-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
让我们用Nestjs来重写一个CNode(中) #19
Comments
兄弟,啥时候更新呀,坐等,强烈需要 |
+1 |
大佬快更新呀 |
围观 |
谢谢,学习了! |
更新(下) |
围观 兄弟你太厉害了!!学习 |
暴力催更! |
大佬太棒啦,帮了不少忙呢,太感谢了,希望快点更新(下)! |
大佬,坐等下篇,太牛逼了 |
Property 'engine' does not exist on type 'INestApplication'. |
下部呢?不更新了? |
爆肝长文啊 |
之前一直没找到好的nestjs实践教程,感谢大佬 |
是大佬 先点赞后学习吧 |
请问下,gRPC的拦截器要怎么写,我useGlobalInterceptors,拦截不到gPRC的请求,只能在Controller上@UseInterceptors 才能生效,有什么好办法做全局的拦截吗? |
// 加上这个泛型就好了 |
我已经收到你的邮件,由于不经常使用所以无法及时回复,有急事请电联,谢谢。
|
我发现比我想象要长,打算把实战部分拆分成中和下来讲解。
通过上篇学习,相信大家对
Nest
有大概印象,但是你还是看不出它有什么特别的地方,下篇将为你介绍项目实战中Nest
如何使用各种特性和一些坑和解决方案。源码这篇主要内容:
项目架构规划设计
一个好的文件结构约定,会让我们开发合作、维护管理,节省很多不必要沟通。
这里我
scr
文件规划:入口文件配置说明
打开
main.ts
文件NestFactory
创建一个app实例,监听3000
端口。create
方法有1-3参数,第一个是入口模块AppModule
, 第二个是一个httpServer
,如果要绑定Express
中间件,需要传递Express
实例。第三个全局配置:app
带方法有哪些INestApplication
下socket.io
库。INestExpressApplication
下express.set()
方法的包装函数。express.engine()
方法的包装函数。express.enable()
方法的包装函数。express.disable()
方法的包装函数。express.static(path, options)
方法的包装函数。express.set('views', path)
方法的包装函数。express.set('view engine', engine)
方法的包装函数。依赖安装
核心依赖
因为目前CNode采用
Egg
编写,里面大量使用与Egg
集成的egg-xxx
包,这里我把相关的连对应的依赖都一一来出来。模板引擎
Egg-CNode
使用egg-view-ejs
,本项目使用ejs
包,唯一缺点没有layout
功能,可以麻烦点,在每个文件引入头和尾即可,也有另外一个包ejs-mate
,它有layout
功能,后面会介绍它怎么使用。redis
Egg-CNode
使用egg-redis
操作redis
,其实它是包装的ioredis
包,我也一直在nodejs里使用这个包,需要安装生产ioredis
和开发@types/ioredis
mongoose
Egg-CNode
使用egg-mongoose
操作mongodb
,Nest
提供了@nestjs/mongoose
,需要安装生产mongoose
和开发@types/mongoose
passport
Egg-CNode
使用egg-passport、egg-passport-github、egg-passport-local
做身份验证,Nest
提供了@nestjs/passport
,需要安装生产passport、passport-github、passport-local
其他依赖在后面用到时候在详细介绍,这几个是比较重要的依赖。
配置 Views 视图模板和 public 静态资源
CNode
使用的是egg-ejs
,为了简单点,减少静态文件编写,我也用ejs
。发现区别就是少了layout
功能,需要我们拆分layout/header.ejs
和layout/footer.ejs
在使用了。但是有一个包可以做到类似的功能
ejs-mate
,这个是@JacksonTian 朴灵大神的作品。新建模板存放
views
文件夹(root/views)和静态资源存放public
文件夹(root/public)注意:
nest-cli
默认只处理src
里面的ts文件,如有其他文件需要自己写脚本处理,gulp
或者webpack
都可以,这里就简单一点,直接把views
和public
放在src
平级的根目录里面了。后面会说怎么处理它们设置问题。模板引擎
安装
ejs-mate
依赖:用法很简单了,关于文件名后缀,默认使用
.ejs
,.ejs
虽然会让它语法高亮,有个坑就html
标签不会自动补全提示。那需要换成.html
后缀。设置模板引擎:
使用模板引擎:
我们在
views
文件夹里面新建一个layout.html
和一个index.html
。写通用的
layout.html
index.html
注意:
@Render()
里面一定要写模板文件名(可以省略后缀),不然访问页面显示是json
对象。访问首页
http://localhost:3000/
看结果。ejs-mate
语法:ejs-mate
兼容ejs语法,语法很简单,这里顺便带一下:说几个常用的写法:
说明:
注意:以上语法基本一样,有一样不一样,
include
需要用partial
代替。他们俩用法一模一样。layout
功能,需要在引用的页面,比如index.html
里面使用<% layout('layout') -%>
,注意:这里'layout'
是指layout.html
。还有一个比较重要的功能是
block
。它是在指定的位置插入自定义内容。类似于angularjs
的transclude
,angular
的<ng-content select="[xxx]"></ng-content>
,vue
的<slot></slot>
。slot
写法:<%- block('head').toString() %>
block('head')
,是一个占位标识符,toString
是合并所有的插入使用join
转成字符串。使用:
append
和prepend
是插入的顺序,append
总是插槽位置插入在最后,prepend
总是插槽位置插入在最前。我们来验证一下。
现在
layout.html
的head
里面写上index.html
的结尾写上访问首页
http://localhost:3000/
看结果。注意:
index.html
里书写block('head').append
的位置不影响它显示插槽的位置,只受定义插槽<%- block('head').toString() %>
还有一个方法
replace
,没看懂怎么用的,文档里面也没有说明,基本append
、prepend
、toString
就够用了。总结:
toString
是定义插槽位置,append
、prepend
往插槽插入指定的内容。他们主要做什么了,layout
载入公共的css
、js
,如果有的页面有不一样地方,就需要插入当前页面的js了,那么一来这个插槽功能就有用,如果使用layout
功能插入,就会包含在layout
位置,无论是语义还是加载都是不合理的。就有了block
的功能,在另一款模板引擎Jade
里面也有同样的功能也叫block
功能。静态资源
public
文件夹里面内容直接拷贝egg-cnode
下的public
的静态资源还需要安装几个依赖:
这几个模块是加载css和js使用,也是@JacksonTian 朴灵大神的作品。
main.ts配置
注意:如果静态文件路径都前缀
/public
,需要使用use
去挂载express.static
路径。只有express
是这样的它的源码是这样写的,如果这样的,你的静态资源路径就是从根目录开始,如果需要加前缀
/public
,就需要express
提供的方式测试我们静态资源路径设置是否正常工作
在
index.html
里面引入public/images/logo.png
图片如果有问题,请找原因,路径是否正确,设置是否正确,如果都ok,还是不能访问,可以联系我。
关于
loader
使用:Loader
可以加载.js
方法也可以加载.coffee
、.es
类型的文件,.css
方法可以加载.less
、.styl
文件。Loader('/public/index.min.js')
是合并后名字.js('/public/libs/jquery-2.1.0.js')
是加载每一个文件地址.done(assets, config.site_static_host, config.mini_assets)
是处理文件,第一个参数合并压缩后的路径(后面讲解),第二个参数静态文件服务器地址,第三个参数是否压缩assets
从哪里来在
package.json
的scripts
配置loader的写法是:
loader <views_dir> <output_dir>
。views_dir
是模板引擎目录,output_dir
是assets.json
文件输出的目录,/
表示根目录。直接运行会报错,这个问题在
egg-node
有人提issues主要是静态资源
css
引用的背景图片和字体地址有错误,需要修改哪些文件:错误信息:
no such file or directory, open 'E:\github\nest-cnode\E:\public\img\glyphicons-halflings.png'
谁引用了它
Error! File:/public/libs/bootstrap/css/bootstrap.css
/public/libs/bootstrap/css/bootstrap.css
大约
2296
和2320
行位置,你可以用查找搜索glyphicons-halflings.png
,默认是background-image: url("../img/glyphicons-halflings.png");
, 替换为上面写法。/public/stylesheets/style.less
大约
850
行位置打包成功以后会输出一个
assets.json
在根目录。assets
指的就是这个json文件,后面我们会讲如果把它们关联起来。静态模板
我们上面已经配置好了模板引擎和静态资源,我们先要去扩展他们,先让页面好看点。
打开cnode,然后右键查看源代码。把里面内容复制,拷贝到
index.html
里去。访问
http://localhost:3000/
就可以瞬间看到和cnode
首页一样的内容了。有模板以后,我们需要改造他们:
DOCTYPE
申明body
标签之外到layout.html
浏览
cnode
所有页面head
内容,除了title
标签内容其他一样基础
layout.html
模板把
index.html
里的head
标签内容都移动到layout.html
的head
,同名的直接替换。替换之后的
layout.html
模板body
标签之内到layout.html
浏览
cnode
所有页面内容,发现头部黑色部分和底部白色部分都是一样的。那么我们需要把它们提取出来。cnode
模板backtotop
和sidebar-mask
是2个和js相关的功能标签,直接保留它们。navbar
对应到header
标签main
对应到main
标签footer
对应到footer
标签main
标签之外内容都放到对应的标签里面改版后的
layout.html
模板把剩下
index.html
里面的styles
和scripts
使用最好是写成
script
和style
文件。main
标签之内到sidebar.html
浏览
cnode
所有主体内容,发现右边侧边栏除了api
页面没有,注册登录找回密码,是另外一种模板内容,其他页面都是一样。当前
index.html
模板替换后的
index.html
模板这样我们首页模板已经完成了。
系统配置和应用配置
系统配置是系统级别的配置,如数据配置,端口,host,签名,加密keys等
应用配置是应用级别的配置,如网站标题,关键字,描述等
系统配置使用
.env
文件,大部分语言都有这个文件,我们需要用dotenv
读取它们里面的内容。dotenv
支持的.env
语法:注意:
.env
文件主要的作用是存储环境变量,也就是会随着环境变化的东西,比如数据库的用户名、密码、静态文件的存储路径之类的,因为这些信息应该是和环境绑定的,不应该随代码的更新而变化,所以一般不会把.env
文件放到版本控制中;我们需要在
.gitignore
文件中排除它们:ConfigModule(配置模块)
当我们使用
process global
对象时,很难保持测试的干净,因为测试类可能直接使用它。另一种方法是创建一个抽象层,即一个ConfigModule
,它公开了一个装载配置变量的ConfigService
。关于配置模块,官网有详细的栗子,这里也是基本类似。这里说一些关键点:
.env
配置文件NODE_ENV
windows
和mac
不一样windows设置
mac设置
你会发现这个很麻烦,有没有什么方便地方了,可以通过
cross-env
来解决问题,它就是解决跨平台设置NODE_ENV的问题,默认情况下,windows不支持NODE_ENV=development的设置方式,加上cross-env就可以跨平台。安装
cross-env
依赖cross-env
设置config
模块:<T = EnvConfig>
是一种什么写法,T
是一个泛型,EnvConfig
是一个默认值,如果使用者不传递就是默认类型,作用类似于函数默认值。默认用2种注册服务的写法,一种是类,一种是工厂。前面基础篇已经提及了,后面讲怎么使用它们。
config
服务:首先,让我们写
ConfigService
类。解析数据都存在
envConfig
里,封装一些获取并转义value
的方法。传递2个参数,一个是
.env
文件路径,一个是验证器,配合Joi
使用,nest
官网文档把配置服务和验证字段放在一起,我觉得这样不是很科学。我在
.env
加一个配置就需要去修改ConfigService
类,它本来就是不需要修改的,我就把验证部分提取出来,这样就不用关心验证问题了。ConfigService
只关心取值问题。上面模块里面还有一个
ConfigToken
服务,它是做什么的了,它叫做令牌。里面写入常量
configToken
并导出ConfigModule
的configToken
也是它。使用
Inject
依赖注入器注入令牌对应的服务InjectConfig
是一个装饰器。装饰器在nest
、angular
有大量实践案例,各种装饰器,让你眼花缭乱。简单科普一下装饰器:
写法:(总共四种:类,属性,方法,方法参数)
执行顺序:
config
2种方式:
普通依赖注入就够玩了,这里用装饰器依赖注入有些画蛇添足,只是说明装饰器和注入器注入令牌用法。
通过app实例取,一般用于系统启动初始化配置,后面还要其他的获取方式,用到在介绍。
Config(应用配置)
应用配置对比系统配置就没有这么麻烦了,大多数数据都可以写死就行了。
参考
cnode-egg
的config/config.default.js
哪里需要直接导入就行了,这个比较简单。
系统配置和应用配置告一段落了,那么接下来需要配置数据。
mongoose连接
关于
mongoDB
安装,创建数据库,连接认证等操作,这里就展开了,这里有篇文章在
.env
文件里面,我们已经配置mongoDB
相关数据。核心模块,只会注入到
AppModule
,不会注入到feature
和shared
模块里面,专门做初始化配置工作,不需要导出任何模块。它里面包括:守卫,管道,过滤器、拦截器、中间件、全局模块、常量、装饰器
其中全局中间件和全局模块需要模块里面注入和配置。
ConfigModule
前面我们已经定义好了
ConfigModule
,现在把它添加到CoreModule
中ConfigValidate.validateInput
是一个验证.env
方法,nest
和官网文档一样.mongooseModule
nest
为我们提供了@nestjs/mongoose
。安装依赖:
配置模块:文档
MongooseModule
提供了2个静态方法:Mongoose.connect()
方法imports,
useFactory,
inject
}):
useFactory
返回对应的Mongoose.connect()
方法参数,imports
依赖模块,inject
依赖服务mongoose.model()
方法@InjectModel
获取mongoose.model
,参数和forFeature
的name
一样。根模块使用: (forRoot和forRootAsync,只能注入一次,所以要在根模块导入)
这里我们需要借助配置模块里面获取配置,需要用到
forRootAsync
如果要写
MongooseOptions
怎么办直接在uri后面写,有个必须的配置要写:
其他配置根据自己需求来添加
如果启动失败会显示:
请检查uri是否正确,如果启动验证,账号是否验证通过,数据库名是否正确等等。
数据库连接成功,我们进行下一步,定义用户表。
用户数据库模块
建立数据模型为后面控制器提供服务
生成文件
shared
模块mongodb
模块user
模块user
服务user
的interface
、schema
、index
。这三个文件无法用命令创建需要自己手动创建。
interface
是ts
接口定义schema
是定义mongodb
的schema
最后完整的
user
文件夹是:定义服务
默认生产的模块文件
在正式写
UserService
之前,我们先思考一个问题,因为操作数据库服务基本都类似,常用几个方法如:一个基本表应该有增删改查这样8个快捷操作方法,如果每个表都写一个这样的,就比较多余了。
Typescript
给我们提供一个抽象类,我们可以把这些公共方法写在里面,然后用其他服务来继承。那我们开始写base.service.ts
:base.service.ts
这里说几个上面没有提到的属性和方法:
那么我们接下来的
UserService
就简单多了user.service.ts
BaseService
是一个泛型,泛型是什么,简单理解就是你传什么它就是什么。T
需要把我们User
类型传进去,返回都是User
类型,使用@InjectModel('User')
注入模型实例,最后赋值给_model
。我们现在数据库
UserService
就已经完成了,接下来就需要定义schema
和interface
。定义schema
有了上面服务的经验,现在是不是你会说
schema
有没有公用的,当然可以呀。我们定一个
base.schema.ts
,思考一下需要抽出来,好像唯一可以抽出来就是:这2个我们可以用抽出来,可以使用
schema
配置参数里面的timestamps
属性,可以开启它,它默认createdAt
和updatedAt
。我们修改它们字段名,使用它们好处,创建自动赋值,修改时候自动更新。注意:它们的存的时间和本地时间相差8小时,这个后面说怎么处理。
那么我们最终的配置就是:
toJSON
是做什么的,我们需要开启显示virtuals
虚拟数据,getters
获取数据。关于schema定义
在创建表之前我们需要跟大家说一下mongoDB的数据类型,具体数据类型如下:
MongoDB
中的字符串必须为UTF-8
。BSON
元素进行比较。ctimestamp
当文档被修改或添加时,可以方便地进行录制。mongoose
使用Schema
所定义的数据模型,再使用mongoose.model(modelName, schema)
将定义好的Schema
转换为Model
。在
Mongoose
的设计理念中,Schema
用来也只用来定义数据结构,具体对数据的增删改查操作都由Model
来执行user.schema.ts
定义interface
因为有些公共的字段,我们在定义
interface
时候也需要抽离出来。使用base.interface.ts
base.interface.ts
interface
文件内容和schema
的基本一样,只需要字段名和类型就好了。user.interface.ts
定义模块
默认生产的模块文件
上面
schema
和service
,都定义好了,接下来我们需要在模块里面注册。user.module.ts
forFeature([{ name: 'User', schema: UserSchema }])
就是MongooseModule
为什么提供的mongoose.model(modelName, schema)
操作定义索引文件
index.ts
其他文件访问
xxx.service.ts
是不是很方便。
shared 模块和 mongodb 模块
mongodb模块
mongodb
模块是管理所有mongodb
文件夹里模块导入导出mongodb.module.ts
shared模块
shared
模块是管理所有shared
文件夹里模块导入导出shared.module.ts
到这里我们
user
数据表模块就基本完成了,接下来就需要使用它们。我们也可以运行npm run start:dev
,不会出现任何错误,如果有错,请检查你的文件是否正确。如果找不到问题,可以联系我。注意:后面我们搭建数据库就不再如此详细说明,只是一笔带过,大家可以看源码。
注册和使用
node-mailer
发送邮件如果有用户模块功能,登陆注册应该说是必备的入门功能。
先说一下我们登陆注册逻辑:
passport、passport-github、passport-local
这三个模块,做身份认证。github
第三方认证登陆(后面会介绍github认证登陆怎么玩)session
和cookie
,30天内免登陆session
和cookie
这里注册、登录、登出、找回密码都放在这个模块里面
生成文件
feature
模块auth
模块auth
服务auth
控制器auth
的dto
dto是字段参数验证的验证类,需要配合各种功能,等下会讲解。
最后完整的
auth
文件夹是:科普知识:async/await
ES7
发布async/await
,也算是异步的解决又一种方案,看一个简单的栗子:
看栗子也能知道
async/await
基本使用规则和条件async
表示这是一个async
函数,await
只能用在这个函数里面await
表示在这里等待promise
返回结果了,再继续执行。await
等待的虽然是promise
对象,但不必写.then(..)
,直接可以得到返回值。try catch
语法捕捉错误await
可以写在for循环里,不必担心以往需要闭包
才能解决的问题 (注意不能使用forEach
,只可以用for/for-of
)在开始之前,前面数据操作有基础服务抽象类,这里控制器和服务也可以抽象出来。是可以抽象出来,但是本项目不决定这么来做,但会做一些抽象的辅助工具。
auth模块
auth.module.ts
feature模块
feature.module.ts
app模块
如果是按我顺序用命令行创建的文件,
feature
模块会自动添加到APP
模块里面,如果不是,需要手动把
feature
模块引入到APP
模块里面。app.module.ts
auth控制器
默认控制器文件
注册
要想登录,就要先注册,那我们先从注册开始。
auth.controller.ts
前面介绍控制器时候已经介绍了
Get
,那么Render
是什么,渲染模板,对应是Express
的res.render('xxxx');
方法。提示:
关于控制器方法命名方式,因为本项目是服务的渲染的,所有会有模板页面和页面请求。模板页面统一加上
View
后缀模板页面请求都是
get
,返回数据会带一个必须字段pageTitle
,当前页面的title
标签使用。页面请求方法命名根据实际情况来。
现在就可以运行开发启动命令看看效果,百分之两百的会报错,为什么?因为找不到模板
auth/register.ejs
文件。那我们就去
views
下去创建一个auth/register.ejs
,随便写的什么,在运行就可以了,浏览器访问:http://localhost:3000/register
。我们需要完善里面的内容了,因为
cnode
屏蔽注册功能,全部走
github
第三方认证登录,所以看不到https://cnodejs.org/signin
这个页面,那么我们可以在源码找到这个页面结构,直接拷贝div#content
里的内容过来。一刷新就页面报错了:
查看命令行提示:
提示我们
csrf
这个变量找不到。csrf
是什么,跨站请求伪造(CSRF或XSRF)是一种恶意利用的网站,未经授权的命令是传播从一个web应用程序的用户信任。
减轻这种攻击可以使用
csurf
包。这里有篇文章浅谈cnode社区如何防止csrf攻击安装所需的包:
在入口文件启动函数里面使用它。
直接这么写肯定有问题,刷新页面控制台报错
Error: misconfigured csrf
下面来说个我经常解决问题方法:
github
的开源依赖包,我们把这个错误复制到它的issues
的搜索框里,如果有类似的问题,就进去看看,能不能找到解决方案,如果没有一个问题,你就可以提issues
。把你的问题的和环境依赖、最好有示例代码,越详细越好,运气好马上有人给你解决问题。
搜索引擎解决问题比如:谷歌、必应、百度。如果有条件首选谷歌,没条件优先必应,其次百度。也是把问题直接复制到输入框,回车就好有一些类似的答案。
就是去一些相关社区提问,和
1
一样,把问题描述清楚。使用必应搜索,发现结果第一个就是问题,和我们一模一样的。
点击链接进去的,有人回复一个收到好评最高,说
app.use(csurf())
要在app.use(cookieParser())
和app.use(session({...})
之后执行。其实我们的这个问题,在csurf说明文档里面已经有写了,使用之前必须依赖
cookieParser
和session
中间件。session
中间件可以选择express-session和cookie-session我们需要安装2个中间件:
在入口文件启动函数里面使用它。
里面有注释,这里就不解释了。
现在刷新还是一样报错
csrf is not defined
。上面已经ok,现在是没有这个变量,我们去
registerView
方法返回值里面加上key是
csrf
,value随便写,返回最后都会被替换的。如果每次都要写一个那就比较麻烦了,需要写一个中间件来解决问题。
在入口文件启动函数里面使用它。
在刷新又报了另外一个错误:
ForbiddenError: invalid csrf token
。验证token
失败。文档里面也有,读取令牌从以下位置,按顺序:
req.body._csrf
- typically generated by thebody-parser
module.req.query._csrf
- a built-in from Express.js to read from the URL query string.req.headers['csrf-token']
- the CSRF-Token HTTP request header.req.headers['xsrf-token']
- the XSRF-Token HTTP request header.req.headers['x-csrf-token']
- the X-CSRF-Token HTTP request header.req.headers['x-xsrf-token']
- the X-XSRF-Token HTTP request header.前端向后端提交数据,常用有2种方式,
form
和ajax
。ajax
无刷新,这个比较常用,基本是主流操作了。form
是服务端渲染使用比较多,不需要js处理直接提交,我们项目大部分都是form
直接提交。一般服务端渲染常用就2种请求,
get
打开一个页面,post
直接form
提交。post
提交都是把数据放在body
体里面,Express
,解析body
需要借助中间件body-parser
。nest
已经自带body-parser
配置。但是我发现好像有bug,原因不明,给作者提issues作者回复速度很快,需要调用
app.init()
初始化才行。还有一个重要的东西
layout.html
模板需要加上csrf
这个变量。接下来要写表单验证了:
我们在
dto
文件夹里面创建一个register.dto.ts
和index.ts
文件register.dto.ts
是一个导出的类,typescript类型,可以是class
,可以interface
,推荐class
,因为它不光可以定义类型,还可以初始化数据。什么叫
dto
, 全称数据传输对象(DTO)(Data Transfer Object),简单来说DTO
是面向界面UI
,是通过UI
的需求来定义的。通过DTO
我们实现了控制器与数据验证转化解耦。dto
中定义属性就是我们要提交的数据,控制器里面这样获取他们。这样是不是很普通,也没有太大用处。如果真的是这样的,我就不会写出来了。如果我提交数据之前需要验证字段合法性怎么办。
nest
也为我们想到了,使用官方提供的ValidationPipe
,并安装2个必须的依赖:因为数据验证是非常通用的,我们需要在入口文件里全局去注册管道。
开始写验证规则,对于这些装饰器使用方法,可以看文档也可以看
.d.ts
文件。IsNotEmpty
不能为空Matches
使用正则表达式Transform
转化数据,这里把英文转成小写。发现一个问题,默认的提供的
NotEquals、Equals
只能验证一个写死的值,那么我验证确认密码怎么办,这是动态的。我想到一个简单粗暴的方式:先用转化装饰器,去判断,
obj
拿到就当前实例类,然后去取它对应属性和当前的值对比,如果是相等就直接返回,如果不是就返回一个标识,再用NotEquals
去判断。这样写不是很友好,我们需要自定义一个装饰器来完成这个功能。
在core新建
decorators
文件夹下建validator.decorators.ts
文件官方文字里面有栗子:直接拷贝过来就行了,改改就好。我们需要改的就是
name
和validate
函数里面的内容,validate
函数返回true验证成功,false验证失败,返回错误消息。验证规则搞定了,现在又有2个新问题了,
Render
方法,可以实现数据显示,但是拿不到当前错误控制器的模板地址。这个是比较致命的问题,其他问题都好解决。解决这个问题,我纠结了很久,想到了2个方法来解决问题。
自定义装饰器+配合
ValidationPipe
+HttpExceptionFilter
实现借助
class-validator
配置参数的context
字段。我们可以在上面写2个字段,一个是
render
,一个是locals
。在实现
render
功能之前,我们需要借助typescript
的一个功能enum
枚举。Nest
里面HttpStatus
状态码就是enum
。我们把所有的视图模板都存在
enum
里面,枚举好处就是映射,类似于key-value
对象。创建视图模板路径枚举
在里面写上:
auth.controller.ts换上枚举:
解决问题之前,我们先看,
ValidationPipe
源码,验证失败之后干了些什么:返回是一个
ValidationError[]
,那ValidationError
里面有什么:最开始我想到是使用
context
来配置3个字段:折腾一遍,功能实现了,就是太麻烦了。每个规则验证装饰器里面都要写
context
一坨。能不能简便一点了。如果我在这个类里面只定义一次是不是好点。
就想到了在
RegisterDto
里写个私有属性,把相关的字段存进去,改进了context
配置:就变成这样的:
这样就比每个规则验证装饰器写
context
配置好了很多,但是这样又有一个问题,会在target
里面多一个__validator_filter__
,有点多余了。需要改进一下,我就想到类装饰器。
类装饰器前面已经说过了,它是装饰器里面最后执行的,用来装饰类。这里有个比较特殊的Reflect。
Reflect
翻译叫反射,应该说叫映射靠谱点。为什么了,它基本就是类似此功能。defineMetadata
定义元数据,有3个参数:第一个是标识key,第二个是存储的数据(获取就是它),第三个就是一个对象。翻译过来就是在 a 对象里面定一个标识 b 的数据为c。有定义就有获取
getMetadata
获取元数据,有2个参数:第一个是标识key,第三个就是一个对象。翻译过来就是在 a 对象里去查一个b 标识,如果有就返回原数据,如果没有就是Undefined。或者是b标识里面去查找a对象。理解差不多。目的是2个都匹配就返回数据。
这玩意简单理解
Reflect
是一个全局对象,defineMetadata
定一个特定标识的数据,getMetadata
根据特定标识获取数据。这里Reflect
用的比较简单就不深入了,Reflect
是es6
新特性一部分。在
Nest
的装饰器大量使用Reflect
。在nodejs
使用,需要借助reflect-metadata
,引入方式import 'reflect-metadata';
。处理完了,dot问题,那么我们接下来要处理异常捕获过滤器问题了。
前面也说,
Nest
执行顺序:客户端请求 ---> 中间件 ---> 守卫 ---> 拦截器之前 ---> 管道 ---> 控制器处理并响应 ---> 拦截器之后 ---> 过滤器
。因为
ValidationPipe
源码里,只要验证错误就直接抛异常new BadRequestException()
,然后就直接跳过控制器处理并响应,走拦截器之后和过滤器了。那么我们需要在过滤器来处理这些问题,这是为什么要这么麻烦原因。
Nest
已经提供一个自定义HttpExceptionFilter
的栗子,我们需要改良一下这个栗子。render
接受3个参数,平常只用前个,第一个是模板路径或者模板,第二个提供给模板显示的数据。这里核心地方在
validationErrorMessage
里:messages
是一个数组,我们每次只显示一个错误消息,总是取第一个即可metadata
是我们根据标识获取的元数据,如果找不到,就抛出异常。注意:message.target
是一个{}
,我们需要获取它的constructor
才行。priorities
获取当前错误字段显示错误提取的优先级列表priority
里面没有配置获取配置[]
, 就直接返回验证规则第一个。提示:这也是{}
坑,默认按字母顺序排列属性的位置。locals
直接去判断配置的locals
,哪些key
可以显示哪些key
不能显示。render
使用。自定义装饰器+自定义
ViewValidationPipe
实现装饰器部分就不用说了,和上面一样,虽然不需要但是后面有用。
ViewValidationPipe
实现:我们这里把
validationErrorMessage
函数直接拿过来了。控制器就需要这么写:
pipe
转换后的结果view
表示出错了,就直接返回locals
,如果没有就接着处理服务逻辑。注意:
(register as any).view
这个view
是不靠谱的,需要返回一个特殊标识,不然页面出现一个view
字段,就挂了。这里我们使用第一种,接着实现服务逻辑。
里面注释也说明的我们要操作的步骤,注册逻辑还是比较简单:
做登录之前完成邮箱激活的功能。
邮箱模块
前面基础已经介绍过
nest
模块,这里邮箱模块是一个通用的功能模块,我们需要抽离出来写成可配置的动态模块。nest
目前没有提供发邮箱的功能模块,我们只能自己动手写了,nodejs
发送邮件最出名使用node-mailer。我们这里也把node-mailer
封装一下。对于一个没有写过动态模块的我,是一脸懵逼,还好作者写很多包装的功能模块:
既然不会写我们可以copy一个来仿写,实现我们要功能就ok了,卷起袖子就是干。
通过观察上面几个模块他们文件结构都是这样的:
我们也来新建一个这样的结构,
core/mailer
建文件就不说了。这一个模块,就需要先从模块开始:
这是我们要实现的2个重要功能,作者写的模块基本是这个套路,有些东西我们不会写,可以先模仿。
forRoot
配置同步模块forRootAsync
配置异步模块我们先说和
node-mailer
相关的,node-mailer
主要分2块:node-mailer
实例,node-mailer
新版解决很多问题,自动去识别不同邮件配置,这对我们来说是一个非常好的消息,不用去做各种适配配置了,只需要按官网的相关配置即可。node-mailer
实例,set
设置配置和use
注册插件,sendMail
发送邮件创建在
createMailerClient
方法里面完成这个方法是一个工厂方法,在介绍这个方法之前,先要回顾一下,
nest
依赖注入自定义服务:值服务:这个一般作为配置,定义全局常量使用,单纯
key-value
形式类服务:这个比较常用,默认就是类服务,如果
provide
和useClass
一样,直接注册在providers
数组里即可。我们只关心provide
注入是谁,不关心useClass
依赖谁。工厂服务:这个比较高级,一般需要依赖其他服务,来创建当前服务的时候,操作使用。定制服务经常用到。
我们在回过头来说上面这个
createMailerClient
方法本来我们可以直接写出一个
Use factory
例子一样的,考虑它需要forRoot
和forRootAsync
都需要使用,我们写成一个函数,使用时候直接调用即可,也可以写成一个对象形式。provide
引入我们定义的常量,至于这个常量是什么,我们不需要关心,如果它变化这个注入者也发生变化,这里不需要改任何代码。也算是配置和程序分离,一种比较好编程方式。inject
依赖其他服务,这里依赖是一个useValue
服务,我们把邮箱配置传递给MAILER_MODULE_OPTIONS
,然后把它放到inject
,这样我们在useFactory
方法里面就可以取到依赖列表。注意:
inject
是一个数组,useFactory
参数和inject
一一对应,简单理解,useFactory
是形参,inject
数组是实参。在
useFactory
里面,我们可以根据参数做相关的操作,这里我们直接获取这个服务即可,然后使用nodemailer
提供的邮件创建方法createTransport
即可。依赖注入和服务重点,我不关心依赖者怎么处理,我只关心注入者给我提供什么。
我们在来说上面这个
MAILER_MODULE_OPTIONS
值服务MAILER_MODULE_OPTIONS
在forRoot
里是一个值服务{ provide: MAILER_MODULE_OPTIONS, useValue: options }
,保存传递的参数。MAILER_MODULE_OPTIONS
在forRootAsync
里是一个特殊处理...this.createAsyncProviders(options)
,后面会讲解这个函数。注意:因为
createMailerClient
依赖它,所以一定要在createMailerClient
方法完成注册。说完通用的创建服务,来说
forRootAsync
里的createAsyncProviders
方法:createAsyncProviders
主要完成的工作是把邮箱配置和邮箱动态模块配置剥离开来,然后根据给定要求分别去处理。createAsyncProviders
方法解释这个函数之前,先看配置参数有接口:
这里面支持2种写法,一种是自定义类,然后使用
useClass
, 一种是自定义工厂,然后使用useFactory
。使用在
MailerService
服务里面完成并且把它导出给其他模块使用@Inject
是一个注入器,接受一个provide
标识、令牌,这里我们拿到了node-mailer
实例send
方法使用rxjs
写法,this.mailer.sendMail(mailMessage)
返回是一个Promise
,Promise
有一些缺陷,rxjs
可以去弥补一下这些缺陷。比如这里使用是rxjs作用就是,
handleRetry()
去判断发送有没有错误,如果有错误,就去重试,默认重试5次,如果还错误就直接抛出异常。tap()
类似一个console
,不会去改变数据流。有2个参数,第一个是无错误的处理函数,第二个是有错误的处理函数。如果发送成功我们需要关闭连接。
toPromise
就更简单了,看名字也知道,把rxjs
转成Promise
。介绍完这个这个模块,那么接下来要说一下怎么使用它们:
模块注册:我们需要在核心模块里面
imports
,因为邮件需要一些配置信息,比如邮件地址,端口号,发送邮件的用户和授权码,如果不知道邮箱配置可参考nodemailer官网。先使用注入依赖
ConfigService
,拿到配置服务,根据配置服务获取对应的配置。进行邮箱配置即可。在页面怎么使用它们,因为本项目比较简单,只有2个地方需要使用邮箱,注册成功和找回密码时候,单独写一个
mail.services
服务去处理它们,并且模板里面内容除了用户名,token等特定的数据是动态的,其他都是写死的。mail.services
这里是实现激活邮件方法,前面写的
mailer
模块,服务里面提供的send
方法,接受四个最基本的参数。this.name
是配置里面获取的name
this.from
是配置里面获取的数据,拼接而成,具体看源码this.host
是配置里面获取的数据,拼接而成,具体看源码from
邮件发起者,to
邮件接收者,subject
显示在邮件列表的标题,html
邮件内容。我们在注册成功时候直接去调用它就好了。
注意:我在本地测试,使用163邮箱作为发送者,用qq注册,就会被拦截,出现在垃圾邮箱里面。
验证注册邮箱
我们实现了发现邮箱的功能,接下来就来尝试验证走注册的功能及验证邮箱验证完成注册。
因为我只要一个发送邮箱的账号,和一个测试邮箱的的账号,我需要去数据库把我之前注册的账号删除了,从新完成注册。
填写信息,点击注册,就会发送一封邮件,是这个样子的:
点击
激活链接
链接跳回来激活账号:接下来我们就来实现
active_account
路由的逻辑创建一个
account.dto
这个很简单理解:需要2个参数,一个name,一个key,name是用户名,key是注册时候我们创建的标识,邮箱,密码,自定义盐混合一起加密。
通用消息模板:
这模板直接拿
cnode
的页面。接下来就是控制器:
我们需要获取
url
的?
后面的参数,需要用到@Query()
装饰器,配合参数验证,最后拿到数据参数,丢给对应的服务去处理业务逻辑。@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name, true);
constructor(
private readonly userService: UserService,
private readonly config: ConfigService,
private readonly mailService: MailService,
) { }
...
}
注释已经写的很清晰的,就不在叙述的问题。接下来讲我们这篇文章的最后一个问题登录,在讲到登录之前需要简单科普一下怎么才算登录,它的凭证是什么?
登录
登录凭证
目前来说比较常用有2种一种是
session+cookie
,一种是JSON Web Tokens
。session+cookie
session+cookie是比较常见前后端一起那种。它是流程大概是这样的:
注意:以上操作都是合法操作,如果个人过失暴露 cookie 给其他人,属于用户个人的行为,比如你在网吧里登录 QQ,服务端没有办法不允许这样操作。而客户端的人应有安全意识,在公共场所及时清空 cookie,或者停止使用一切 [不随 session 关闭而 cookie 失效] 的应用。
JSON Web Tokens
JSON Web Tokens是比较常见前后分离那种。它是流程大概是这样的:
注意:前端是无设防的,不可以信任; 全部的校验都由后端完成
我们这里是前后端一体的,当然选择
session+cookie
。这里有篇文章介绍还行,传送门。我们这里登录需要实现2个,一个是本地登录,一个是第三方github登录。
本地登录
nestjs
已经帮我们封装好了@nestjs/passport
,我们前面已经说了需要下载相关包。本地登录使用passport-local
完成。新写个模板,需要去定义一个枚举ViewsPath 登录地址
和正常注册模板控制器一样,这里多了一项
req.flash('loginError')[0]
,其实它是connect-flash
中间件。其实我们自己写一个也完全没有问题,本身就没有几行代码,既然有轮子就用呗,它是做什么,就是帮我们去session
记录消息,然后去获取,绑定在Request
上。你需要安装它npm install connect-flash -S
。模板直接拷贝
cnode
的登录模板,改了一下请求地址。这里使用守卫,
AuthGuard
首页是@nestjs/passport
。verifyLogin是登录以后操作。为什么封装一个方法,等下github登录成功也是一样的操作。login
方法是passport
的方法,user
就是我们拿到的用户信息。注意:这里的
passport-local
是网上的栗子实现有差别,网上栗子都可以配置,重定向的功能,这是passport文档里面的栗子。
这个坑我也捣鼓很久,无论成功还是失败重定向都需要手动去处理它。成功就是上面我那个
login
。我们需要新增一个
passport
文件夹,里面放passport相关的业务。新建一个
local.strategy.ts
,处理passport-local
这里就比较简单,就这么几行代码,自定义一个本地策略,去继承
@nestjs/passport
一个父类,super需要传递是new LocalStrategy('配置对象')
,validate
是一个抽象方法,我们必须要去实现的,因为@nestjs/passport
也不知道我们是怎么样查询用户是否存在,这个验证方法暴露给我们的去实现。done
就相当于是callback
,标准nodejs回调函数参数,第一个是表示错误,第二个是用户信息。放到
AuthModule
里面去做服务申明。AuthSerializer也是和
passport
相关的,它里面需要实现2个方法serializeUser
,deserializeUser
。我们这里先简单粗暴把所有信息全部存到
session
,先实现功能,其他后面再优化。接下来去服务实现
local
方法:上面都有注释,这里说明一下为什么需要在这里去验证字段信息,这也是使用
@nestjs/passport
坑。验证使用
class-validator
提供的验证器类Validator
,其他验证方法和我们注册保持一致。注释都已经一一说明。错误都使用
throw new UnauthorizedException('错误信息');
这样的方式去抛出,这也是在AuthGuard
源码里面,有个处理请求方法:只要有错误,就回去走错误,这个错误就被
ExceptionFilter
捕获,我们有自定义的HttpExceptionFilter
,等下就来讲它。只有没有错误,成功才会返回user,这时候去走,
serializeUser
,deserializeUser
,passportLocal
最后重定向到首页。注意:抛出异常一定要用
throw
,不用使用return
。用return
就直接走serializeUser
,然后报错了。错误处理,因为这个身份认证只要出错返回都是401,那么我们需要去捕获处理一下,
默认
handleRequest
返回是一个空的,exception.message.message
是undefined
,这是passport
返回,只要用户名或者密码没有填,都会返回这个错误信息,对应我们来捕获错误也是一脸懵逼,我看cndoe
是直接返回信息不全。
,这里就一样简单粗暴处理了。github登录
这个玩意就本地登录简单多了。先说下流程:
我们网站叫
nest-cnode
接下来我们就去实现一下:
先github申请一个认证,应用登记。
一个应用要求 OAuth 授权,必须先到对方网站登记,让对方知道是谁在请求。
所以,我们要先去 GitHub 登记一下。这是免费的。
访问这个网址,填写登记表。
应用的名称随便填,主页 URL 填写
http://localhost:3000
,跳转网址填写http://localhost:3000/github/callback
。提交表单以后,GitHub 应该会返回客户端 ID(client ID)和客户端密钥(client secret),这就是应用的身份识别码。
我们创建一个
github.strategy.ts
需要配置
clientID
,clientSecret
,callbackURL
, 这3个东西,我们上面图里面都有。把它申明到模块里面去。github2个必备的路由:
我们需要github登录时候就去请求
/github
路由,使用守卫,告诉守卫使用github
策略。这个方法随便写,返回都会重定向到github.com,填完登录信息,就会自动跳转到githubCallback
方法里面,req.user
返回就是github给我们提供的所有信息。我们需要去和我们用户系统做关联。服务github方法:
注意:
profile
返回信息可能是个undefined
,因为认证可能会失败,需要去处理一下,不然后面代码全挂了。O(∩_∩)O哈哈~。登录功能基本完成了,需要判断用户登录。
我们需要写一个中间件,
current_user.middleware.ts
因为
passport
登录成功以后,会自动给req
添加一个属性user
,我们只需要去判断它就可以了。注意:
nestjs
中间件和express
中间件有区别:express定义的中间件,如果全局可以直接通过
express.use(中间件)
去申明使用。nestjs定义的中间件不能这么玩,需要在模块里面去申明使用。
我们把全局的中间件都丢到
AppModule
,里面去申明使用。修改一下
AppController
首页:登录前:
登录后:
在弄个退出就完美了:它就更简单了:
就是一波清空操作,调用
passport
的logout
方法。代码已更新,传送门。
欲知后事如何,请听下回分解。
The text was updated successfully, but these errors were encountered: