当前位置: 首页 > 后端技术 > Node.js

Node项目从0到1实战

时间:2023-04-03 16:41:30 Node.js

前言本文以koa框架为例,搭建一个从0到1的后端服务,涵盖了后端服务的基本内容和api的功能实现.适合人群:没有完全搭建过节点服务的人已经学会了写koa,想练习的人正在使用koa搭建节点服务。为了实现API接口请求,我们考虑几个问题:整个服务程序的处理流程和错误处理接口路由接口调用权限接口缓存访问数据库除了服务程序本身,还要考虑工程相关的问题(本文不再展开):日志处理监控告警快速恢复知道常见的中间件请求参数解析插件参数分为三种:urlsearchurlparameterPOSTbodykoa-bodyparser:将请求body上的数据挂载到ctx.request.body,supportjson/text/xml/form(不支持multipart)文件缓存相关koa-static:静态文件系统,支持maxage、gzip等属性。该中间件与以下中间件配合使用效果更好:koa-conditional-get用于新鲜度检测,koa-etag用于协商缓存,koa-mount用于路径控制,例如访问/public时返回文件内容koa-mount:多个子应用合并到一个父应用程序中。(也可以通过path控制中间件的挂载)koa-conditional-get:使协商缓存生效(304判断)缓存机制:https://juejin.cn/post/684490...浏览器缓存:https://segmentfault.com/a/11...koa-etag:支持etag/if-none-match协商缓存接口缓存接口缓存需要配合Redis实现接口缓存。Redis是一种开源(BSD许可)内存数据结构存储,用作数据库、缓存和消息代理。这里使用了一个Node使用的npm:ioredis也需要搭配koa-conditional-get和koa-etag来实现整个缓存过程。使用缓存demo,主要知识点:缓存设置:if(ttl){ctx.response.set('Cache-Control',`max-age=${ttl}`);}else{ctx.response.set('Cache-Control','no-store');}生成rediskey:method+url+requestbodyconstkey=`spacex-cache:${hash(`${method}${url}${JSON.stringify(ctx.request.body)}`)}`;Http安全koa-helmet:头盔通过设置Httpheaders使应用程序更加安全。参考:https://juejin.cn/post/684490...CORSkoa-cors:CORS(Cross-OriginResourceAccess)设置跨域资源访问的几个关键头设置:Access-Control-Allow-CredentialsAccess-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Max-Agedebugkoa-pino-logger:loggermiddlewarelogindesignusingtokenorsession-cookie?token:重计算,轻存储session:重存储,轻计算详情点击这里>>这里我们以token验证为例。Token实现了token需要满足UniqueID的条件,UniqueID代表用户账号的唯一有效期,失败后需要重新登录,用于保护用户账号。简单实现通过UUID实现ID唯一,利用Redis缓存的有效期等同于token的有效期。优雅实现使用jsonwebtoken(JWT):https://github.com/auth0/node...特点:加密/解密机制生成唯一ID,支持有效期设置。例如:服务端生成token:consttoken=jwt.sign({//加密码参数username:'myName',password:'myPassword'},'MY_SECRET',//密码{expiresIn:60*60//设置有效期});token:类似:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZGFwIjoiemh1YmVubGVpIiwicGFzc3dvcmQiOiJ6MTM2NjU0Mjc4NzEiLCJpYXQiOjE2NDM0NTEzNDQsImV4cCI6MTY0NDA1NjE0NH0.1aewCZmMIkQWoJiZWmdobcPwGY7BuPzWMygf3aw7Z6g服务端解析token:constdecoded=jwt.verify(token,'MY_SECRET');console.log(decoded);//outputresult//{//username:'myName',//password:'myPassword'//}我们发现token是可以自带参数的,可以省去将用户信息存入数据库的步骤,只是在计算的时候比较消耗性能。应用中的实现:首先,获取新的token,存储在本地。每个请求都携带x-access-token中的token//config.jsexportconstLOCAL_KEY=`${你的域名}-token`;//这避免了重复//request.jsconstaxiosInstance:AxiosInstance=axios.create(requestConfig);axiosInstance.interceptors.request.use(config=>{config.headers['x-access-token']=localStorage.getItem(LOCAL_KEY)||'';//带上token(不要把这个设置放在axios中.create:它不会实时更新)//在发送请求之前做一些事情returnconfig;},error=>{//对请求错误做一些事情returnPromise.reject(error);});...Summary本节介绍server-sidetoken的生成、解析以及在前端HTTP请求中的使用。关于用户信息的设计用户信息的设计要区分用户权限的设计。用户表设计:usernameldapppasswordavatarZhangSanzhangsan01z123456https://avatar.com/z123456Lisilisil000l123456https://avatar.com/l000用户权限用户权限是对用户信息的增强,每个用户都关联了一系列的权限。用户权限可以认为是业务端的实现,信息更丰富,业务更重。权限表设计:ldappproductpermissionzhangsan01product13zhangsan01product21lisiproduct17用户身份验证/失效机制:登录时重新生成token并修改密码重新生成token失效:注销时Token过期后续处理:重新登录后检查重定向地址跳转修改密码以及失败时,您需要引导重新登录和注销。token有本地方式,直接删除本地token即可,然后进行第2步。对于session方式,需要让sessionId失效。接口设计接口设计这里就不展开了,可以参考RestfulApi接口,验证哪些接口需要验证用户身份,哪些不需要验证,哪些不需要验证,如何跳过验证,以及如何在其中传递用户信息获取用户信息后的一个事务Authdemo中对auth的解释点这里>>这个例子用在路由中,我们会用另一种方法,写在最外层,实现按需验证。这避免了在任何使用它的地方引入这个中间件。koa-unless:当条件满足时有条件地跳过一个中间件.authmiddlefile:varverifyToken=async(ctx,next)=>{constreq=ctx.request;consttoken=req.body.token||请求.查询.token||req.headers["x-access-token"];};if(!token){ctx.body={code:0,err_code:401,err_msg:'401'};}else{try{constdecoded=jwt.verify(token,TOKEN_KEY);req.user=已解码;//挂载数据awaitnext();}catch(err){ctx.body={code:0,err_code:401,err_msg:'401'}}}};verifyToken.unless=require('koa-unless');module.exports=verifyToken;app.jsconstverifyToken=require('middleware/auth');...//认证app.use(verifyToken.unless({path:[//设置没有auth中间件的路径/\/login/,//接口usedforlogin],}));//进入路由处理app.use(routes());...passuseridentity:module.exports=async(ctx,next)=>{constkey=ctx.request.headers['spacex-key'];if(key){constuser=awa它db.collection('users').findOne({key});if(user?.key===key){ctx.state.roles=user.roles;//挂载到ctx.state,传递给中间件awaitnext();返回;}}ctx.status=401;ctx.body='https://youtu.be/RfiQYRn7fBg';};路由设计需要考虑Restful设计方法无权限处理接口结构&错误信息设计使用路由使用koa-route参考demo:子模块管理api,入口文件整体导出使用auth中间件使用ORM语法和模型[this文章涉及】使用Redis作为接口缓存数据库连接(MYSQL版)第1版:使用koa-mysql手写SQL语句。问题是:需要自己抽象sql语法,因为sql语句可以根据功能进行抽象(比如抽象条件查询)。如果全部手写,就需要写更多。//来自:https://chenshenhai.github.io/koa2-note/note/mysql/info.htmlconstmysql=require('mysql')//创建数据池constpool=mysql.createPool({host:'127.0.0.1',//数据库地址user:'root',//数据库用户密码:'123456'//数据库密码database:'my_database'//选择数据库})//在数据池pool中进行session操作.getConnection(function(err,connection){connection.query('SELECT*FROMmy_table',(error,results,fields)=>{//结束会话connection.release();//throwif(error)throwerror;})})第二版:使用sequlize(orm)什么是ORM?ORM是一种通过实例对象的语法来完成对关系数据库的操作的技术。代表有:sequelize/openrecord/typeorm缺点:性能问题->如果不是写特别复杂或者特殊的SQL,可以不用考虑这个问题。用面向对象的方式写SQL总感觉怪怪的。->数据库信息存储的习惯性问题【作者未解】用户名密码存储:如何安全保存,使用时不泄露密码?数据库权限问题:连接管理员还是普通用户?跨域限制在整体顺序处理中的好处:防止其他网站调用,控制请求量,防止使用接口工具调用,配合用户认证进一步控制请求量(无账号不能accessed)//检查referer,防止postmanThiscallapp.use(async(ctx,next)=>{const{referer=''}=ctx.header;if(ENV==='production'&&!referer.includes(HOST)){ctx.response.body='NotAllowOriginRequest';}else{awaitnext();}})//corsapp.use(cors({origin:(ctx)=>{//设置可以访问此服务的源域returnENV==='development'?'http://127.0.0.1:8080':'https://www.xxx.com';},credentials:true,allowMethods:['GET','POST','PUT','PATCH','DELETE'],allowHeaders:['Content-Type','Accept',],}));app.js//1.创建appapp=newKoa();//2.加载辅助中间件app.use(conditional());app.use(etag());app.use(bodyParser());app.use(helmet());...//其他中间件//3.域名校验app.use(referer())//referer验证app.use(cors());//4.用户身份校验app.use(verifyToken.unless({path:[/\/login/],}));//5.进入路由app.use(routes());//0.正在监听portapp.listen(PORT,()=>{console.log(`port${PORT}islistening~`);});错误处理uncaughtExceptionunhandledRejection//gracefulShutdown:关闭程序可以理解为遇到错误时统一处理//Serverstartapp.on('ready',()=>{SERVER.listen(PORT,'0.0.0.0',()=>{logger.info(`Runningonport:${PORT}`);//处理终止命令process.on('SIGTERM',gracefulShutdown);//处理中断process.on('SIGINT',gracefulShutdown);//防止在未捕获的异常时脏退出:process.on('uncaughtException',gracefulShutdown);//防止在未处理的承诺拒绝时脏退出process.on('unhandledRejection',gracefulShutdown);});});参考:https://github.com/r-spacex/S...SpaceX代码错误中间件:https://sourcegraph.com/github...logger中间件(用于调试):https://sourcegraph.com/github...部署到远程服务器GitHubAction更多实践参考这里:GithubActionWorkflow实践服务器服务快速恢复:pm2部署优点:监听文件变化,自动重启程序,支持性能监控【同样重要】负载均衡时自动重启程序崩溃【重要】服务器重启时自动重启【重要】自动化部署项目自动部署到远程服务器服务器条件:服务代码克隆到/data/server/crm-server,这样只需要gitpull每一次。installnodeglobalinstallPM2name:servicedeploymenton:push:branches:-mainjobs:build:runs-on:ubuntu-lateststeps:-name:executingremotesshcommandsusingpassworduses:appleboy/ssh-action@masterwith:主机:${{secrets.HOST}}用户名:${{secrets.USERNAME}}密码:${{secrets.PASSWORD}}端口:${{secrets.PORT}}脚本:|cd/data/server/crm-servergitcheckoutmaingitpull#如果你的远程服务器是安装了nvm的node,你需要如下exportexportPATH=$PATH:/home/ubuntu/.nvm/versions/node/v16。5.0/binpm2linkyourpm2Link#[可选]添加pm2监控pm2restartstart.sh#启动pm2start.sh最后执行:$cross-envPORT=8080ENV=productionnodeapp.jsngix配置[可选]更详细的nginx配置可以参考前端应该以自己掌握的nginx知识,将前端文件(/data/www文件夹下)和后端API部署到同一台服务器上。以http://www.ddup.info为例:server{...location^~/crm/api{proxy_passhttp://www.ddup.info:8080;}定位器n^~/crm{root/data/www;indexindex.htmlindex.htm;try_files$uri$uri//crm/index.html;}位置/{根/数据/www;indexindex.htmlindex.htm;try_files$uri/app/index.html;}}思考:合理的后端工程结构参考express目录结构:https://github.com/expressjs/...参考egg目录结构:https://eggjs.org/zh-cn/basic...目录结构:staticfiles:staticviewlayer:ejstemplatedatamodel:models?service:servicerouting:routes?middleware:middleware?定时任务:jobs?Postscript第一次尝试,不可避免如果有些东西没有想好出来了,还请大家指出,大家一起学习进步~