当前位置:

浅出 Node + React 的微服务项目8. 引入 MongoDB

访客 2024-04-24 725 0

引入MongoDB

  • 你可以点击这里查看本文的GithubREADME项目链接也是这个哦

在K8S中创建MongoDB

  • 无需手动本地安装,直接用我们之前的dockerimage->pod.container的技术
  • pod的container配置image,K8S将给我们自动pull进来
apiVersion:apps/v1kind:Deploymentmetadata:name:auth-mongo-deplspec:replicas:1selector:matchLabels:app:auth-mongotemplate:metadata:labels:app:auth-mongospec:containers:-name:auth-mongo#pullimageimage:mongo---apiVersion:v1kind:Servicemetadata:name:auth-mongo-srvspec:selector:app:auth-mongoports:-name:dbprotocol:TCP#port是每个Node在Kubernetes的cluster中的port#targetPort是Pod的portport:27017targetPort:27017cdticketing/infra/k8s/skaffolddevkubectlgetpods
  • skaffold一直在watch./infra/k8s/*变化,所以直接skaffold自动化启动即可

⬆backtotop

连接到MongoDB

//index.tsimportexpressfrom"express";import"express-async-errors";import{json}from"body-parser";//引入importmongoosefrom"mongoose";import{currentUserRouter}from"./routes/current-user";import{signinRouter}from"./routes/signin";import{signoutRouter}from"./routes/signout";import{signupRouter}from"./routes/signup";import{errorHandler}from"./middleware/error-handler";import{NotFoundError}from"./errors/not-found-error";constapp=express();app.use(json());app.use(currentUserRouter);app.use(signinRouter);app.use(signoutRouter);app.use(signupRouter);app.all("*",async(req,res)=>{thrownewNotFoundError();});app.use(errorHandler);conststart=async()=>{try{//连接到K8S内部的srv的clusterDomain和serviceIPPortawaitmongoose.connect("mongodb://auth-mongo-srv:27017/auth",{useNewUrlParser:true,useUnifiedTopology:true,useCreateIndex:true,});console.log("ConnectedtoMongoDb");}catch(err){console.log(err);}app.listen(3000,()=>{console.log("Listeningonport3000!");});};start();

⬆backtotop

用户登录的工作流

⬆backtotop

让TypeScript和Mongoose搭配

目前遇到的问题#1withTSMongoose

  • 创建一个Document
    Typescript能确保我们提供正确的属性,但MongoDB要确保约束我们传入的属性是比较困难的
newUser({email:"test@test.com",password:"lk325kj2"});

目前遇到的问题#2withTSMongoose
我们传递给User构造函数的属性不一定与用户可用的属性匹配
如下:MongoDB不仅要存我们给定的字段,还要自动生成其他字段

constuser=newUser({email:"test@test.com",password:"lk325kj2"});console.log(user);//{email:'..',password:'..',createdAt:'..',updatedAt:'..'}

⬆backtotop

创建UserModel

//user.tsimportmongoosefrom"mongoose";//创建一个新的schemaconstuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});//.model()方法会生成shcema的副本,在调用.model()方之前,请确保已添加了要使用的的所有shcema。//集合User在数据库中的name是UserconstUser=mongoose.model("User",userSchema);export{User};

⬆backtotop

用户属性的类型检查

解决遇到的问题#1withTSMongoose
Typescript能确保我们提供正确的属性,但MongoDB要确保约束我们传入的属性是比较困难的
约束newUser(attrs)的attrs属性即可

//user.tsimportmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到那些约束interfaceUserAttrs{email:string;password:string;}//!!!!!注意这不是TypeScriptconstuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});constUser=mongoose.model("User",userSchema);//以后在要创建User的场景中,用buildUser进行创建constbuildUser=(attrs:UserAttrs)=>{returnnewUser(attrs);};export{User,buildUser};

⬆backtotop

给Model增加静态属性

  • 因为我们需要在创建的时候进行传入值的类型约束,所以需要给model抽象出一个build方法的interface,每次通过build来创建,build里面有类型约束
  • Q:怎么给model抽象出一个build方法的interface?
  • A:先继承Model,在继承后的UserModel静态属性中新增build,最后因为UserModel仍然是一个抽象的,需要传给mongoose.model<Doc,Model>让他理解,所以UserModel应该是一个interface
//user.tsimportmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到哪些传参约束interfaceUserAttrs{email:string;password:string;}//一个interface,描述了//对于UserModel的属性的约束interfaceUserModelextendsmongoose.Model<any>{build(attrs:UserAttrs):any;}constuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});userSchema.statics.build=(attrs:UserAttrs)=>{returnnewUser(attrs);};constUser=mongoose.model<any,UserModel>("User",userSchema);export{User};

⬆backtotop

约束UserDocument中的属性

解决遇到的问题#2withTSMongoose
我们传递给User构造函数的属性不一定与用户可用的属性匹配
MongoDB不仅要存我们给定的字段,还要自动生成其他字段
约束UserDocument中的字段即可

importmongoosefrom"mongoose";//一个interface,描述了在//创建用户(传值到数据库)的时候需要做到哪些传参约束interfaceUserAttrs{email:string;password:string;}//一个interface,描述了//对于UserModel用户集合的属性的约束interfaceUserModelextendsmongoose.Model<UserDoc>{build(attrs:UserAttrs):UserDoc;}//一个interface,描述了//对于UserDocument单个独立的用户的属性的约束interfaceUserDocextendsmongoose.Document{email:string;password:string;}constuserSchema=newmongoose.Schema({email:{type:String,required:true,},password:{type:String,required:true,},});userSchema.statics.build=(attrs:UserAttrs)=>{returnnewUser(attrs);};constUser=mongoose.model<UserDoc,UserModel>("User",userSchema);export{User};

⬆backtotop

model的泛型是什么意思

  • 有对Document和Model(因为Model中有N个Document,所以要依赖于Document)的约束
  • 返回值是Model约束后的结果,返回的是Modelxx集合
//index.d.tsexportfunctionmodel<TextendsDocument,UextendsModel<T>>(name:string,schema?:Schema,collection?:string,skipInit?:boolean):U;

⬆backtotop

创建用户

//signup.tsimportexpress,{Request,Response}from"express";import{body,validationResult}from"express-validator";import{User}from"../models/user";import{RequestValidationError}from"../errors/request-validation-error";constrouter=express.Router();router.post("/api/users/signup",[body("email").isEmail().withMessage("Emailmustbevalid"),body("password").trim().isLength({min:4,max:20}).withMessage("Passwordmustbebetween4and20characters"),],async(req:Request,res:Response)=>{consterrors=validationResult(req);if(!errors.isEmpty()){thrownewRequestValidationError(errors.array());}const{email,password}=req.body;//查找有无用户创建constexistingUser=awaitUser.findOne({email});if(existingUser){console.log("Emailinuse");returnres.send({});}constuser=User.build({email,password});awaituser.save();res.status(201).send(user);});export{routerassignupRouter};

⬆backtotop

新增400请求错误Error

//bad-request-error.tsimport{CustomError}from"./custom-error";exportclassBadRequestErrorextendsCustomError{statusCode=400;constructor(publicmessage:string){super(message);Object.setPrototypeOf(this,BadRequestError.prototype);}serializeErrors(){return[{message:this.message}];}}//signup.tsif(existingUser){thrownewBadRequestError("Emailinuse");}

⬆backtotop

记得给Password加hash

  • 创建用户
  • 密码验证

⬆backtotop

增加PasswordHashing功能

  • 密码加密要用到crypto,因为是异步进行的,所以需要promisify化,这里我们来复习下Promisify、PromisifyAll、Promise.all
//1.promisifyfunctiontoPrimisify(fn){returnfunction(...args){returnnewPromise(function(resolve,reject){fn(...args,(err,data)=>{err?reject(err):resolve(data)})})}letread=toPrimisify(fs.readFile);read('./1.txt','utf8').then(res=>{console.log(res)});//2.promisifyAllfunctiontoPromisifyAll(obj){Object.keys(obj).forEach((item,index)=>{if(typeofobj[item]=='function')obj[item'Async']=toPrimisify(obj[item])})}toPromisifyAll(fs);fs.readFileAsync('./2.txt','utf8').then(res=>{console.log(res)});//3.promise.allfunctionpromiseAll(promises){returnnewPromise(function(resolve,reject){if(!Array.isArray(promises)){returnreject(newTypeError("argumentmustbeanarray"))}varcountNum=0;varpromiseNum=promises.length;varresolvedvalue=newArray(promiseNum);for(vari=0;i<promiseNum;i){(function(i){Promise.resolve(promises[i]).then(function(value){countNum;resolvedvalue[i]=value;if(countNum===promiseNum){returnresolve(resolvedvalue)}},function(reason){returnreject(reason))})(i)}})}varp1=Promise.resolve(1),p2=Promise.resolve(2),p3=Promise.resolve(3);promiseAll([p1,p2,p3]).then(function(value){console.log(value)})
  • 同时,也要用到nodejs的内置类Buffer
    JavaScript语言自身只有字符串数据类型,没有二进制数据类型。
    但在处理像TCP流或文件流时,必须使用到二进制数据。因此在Node.js中,定义了一个Buffer类,该类用来创建一个专门存放二进制数据的缓存区。
    在Node.js中,Buffer类是随Node内核一起发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可以让Node.js处理二进制数据,每当需要在Node.js中处理I/O操作中移动的数据时,就有可能使用Buffer库。原始数据存储在Buffer类的实例中。一个Buffer类似于一个整数数组,但它对应于V8堆内存之外的一块原始内存。
//password.tsimport{scrypt,randomBytes}from"crypto";import{promisify}from"util";//convertcallbackscryptfunctiontoasyncawaituseconstscryptAsync=promisify(scrypt);exportclassPassword{staticasynctoHash(password:string){constsalt=randomBytes(8).toString("hex");constbuf=(awaitscryptAsync(password,salt,64))asBuffer;return`${buf.toString("hex")}.${salt}`;}}

⬆backtotop

比较HashedPassword

//password.tsimport{scrypt,randomBytes}from"crypto";import{promisify}from"util";//convertcallbackscryptfunctiontoasyncawaituseconstscryptAsync=promisify(scrypt);exportclassPassword{staticasynccompare(storedPassword:string,suppliedPassword:string){const[hashedPassword,salt]=storedPassword.split(".");constbuf=(awaitscryptAsync(suppliedPassword,salt,64))asBuffer;returnbuf.toString("hex")===hashedPassword;}}

⬆backtotop

MongoosePre-SaveHooks

  • 这个hooks主要是在做[xxxaction]操作之前,需要做的事情
  • 我们这里当然是在保存password之前,进行toHash操作
//user.tsuserSchema.pre("save",asyncfunction(done){if(this.isModified("password")){consthashed=awaitPassword.toHash(this.get("password"));this.set("password",hashed);}done();//completeasyncwork});

⬆backtotop

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~