前言
本文以 koa 框架为例,从0到1 搭建一个后端服务,涵盖了后端服务的根本内容,实现 api 的性能。
适宜人群:
- 未残缺搭建过node服务的人
- 学写了koa, 想实际的人
- 正在应用 koa 搭建 node 服务的人
注释
为了实现 api 接口申请,咱们思考几个问题:
- 整个服务程序的解决流程与错误处理
- 接口路由
- 接口调用权限
- 接口缓存
- 拜访数据库
除了服务程序自身,还要思考工程相干的(本文不开展讲):
- 日志解决
- 监控报警
- 疾速复原
认识一下罕用中间件
request 参数解析插件
参数分3种:
- url search
- url parameter
- POST body
koa-bodyparser: 把request body 上的数据挂载到 ctx.request.body, 反对 json / text / xml / form (不反对 multipart)
文件缓存相干
koa-static: 动态文件系统, 反对
maxage
、gzip
等属性。这个中间件配合上面的中间件更好用:- 搭配 koa-conditional-get 做新鲜度检测 和 配合 koa-etag 做协商缓存
- 搭配 koa-mount 做门路管制,比方拜访 /public 时候才去返回文件内容
- 搭配 koa-etag 做 e-tag治理
- koa-mount:多个子利用合成一个父利用。(也能够用作为通过 path 管制 middleware 的挂载应用 )
koa-conditional-get : 让协商缓存失效(304 断定)
- 缓存机制:https://juejin.cn/post/684490...
- 浏览器缓存: https://segmentfault.com/a/11...
- koa-etag: 反对 etag/ if-none-match 协商缓存
接口缓存
接口缓存须要配合 Redis 来做,实现接口高速缓存。
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker.
这里用到了一个 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'); }
生成 redis key: method + url + request body
const key = `spacex-cache:${hash(`${method}${url}${JSON.stringify(ctx.request.body)}`)}`;
Http 安全性
koa-helmet: helment 通过设置 Http 头来使应用程序更加平安。
参考:https://juejin.cn/post/684490...
CORS
koa-cors: CORS(跨域资源拜访)设置
跨域资源拜访几个要害的 header 设置:
- Access-Control-Allow-Credentials
- Access-Control-Allow-Origin
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
- Access-Control-Max-Age
debug
- koa-pino-logger: logger middleware
登录设计
应用 token 还是 session-cookie?
- token: 重计算,轻存储
- session: 重存储,轻计算
具体理解戳这里>> 咱们这里采纳 token 验证为例。
token 实现
token须要满足的条件
- 惟一ID,代表举世无双的用户账号
- 有效期,生效后须要从新登录,用于爱护用户账号
简陋的实现
- 通过UUID 实现惟一ID
- 通过 Redis 缓存有效期来等效 token 有效期
优雅的实现
应用 jsonwebtoken(JWT): https://github.com/auth0/node...
特点:
- 加密/解密 机制
- 生成惟一ID
- 可反对有效期设置
举个 :
服务端生成 token:
const token = jwt.sign({ // 加密参数 username:'myName', password:'myPassword'}, 'MY_SECRET', // 明码{ expiresIn: 60 * 60 // 设置有效期 });
token:相似 :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZGFwIjoiemh1YmVubGVpIiwicGFzc3dvcmQiOiJ6MTM2NjU0Mjc4NzEiLCJpYXQiOjE2NDM0NTEzNDQsImV4cCI6MTY0NDA1NjE0NH0.1aewCZmMIkQWoJiZWmdobcPwGY7BuPzWMygf3aw7Z6g
服务端解析token:
const decoded = jwt.verify(token,'MY_SECRET');console.log(decoded);// 输入后果// { // username:'myName',// password:'myPassword'// }
咱们发现token 还能够自带参数,这能够省去将用户信息存在数据库的步骤,只是在计算的时候须要耗费性能。
利用里的实现:
- 首先,失去新 token 之后存储到本地
- 每次申请在 x-access-token 里携带 token
// config.jsexport const LOCAL_KEY = `${你的域名}-token`; // 这样防止反复// request.jsconst axiosInstance: AxiosInstance = axios.create(requestConfig);axiosInstance.interceptors.request.use(config => { config.headers['x-access-token'] = localStorage.getItem(LOCAL_KEY) || ''; // 带上 token(不要把这个设置放到axios.create 外面: 不会实时更新) // 在发送申请之前做些什么 return config;}, error => { // 对申请谬误做些什么 return Promise.reject(error);});...
总结
这一大节介绍了服务端 token
的生成、解析和前端http 申请实战中的应用。
对于用户信息的设计
用户信息的设计要辨别用户权限设计
用户信息
用户信息是更加通用的信息,只蕴含纯正的用户自身信息,比方:用户名、ldap、明码、头像这种。
用户表设计:
username | ldap | password | avatar |
---|---|---|---|
张三 | zhangsan01 | z123456 | https://avatar.com/z123456 |
李四 | lisil000 | l123456 | https://avatar.com/l000 |
用户权限
用户权限则是在用户信息上的增强,每个用户都关联着一系列权限。用户权限能够认为是业务侧的实现,信息更丰盛,业务更重。
权限表设计:
ldap | product | permission |
---|---|---|
zhangsan01 | product | 13 |
zhangsan01 | product | 21 |
lisi | product | 17 |
用户身份失效/生效 机制
失效:
- 登陆的时候从新生成token
- 批改了明码的时候从新生成token
生效:
- 退出登陆的时候
- token过期
后续解决:
- 从新登陆之后查看重定向地址进行跳转
- 批改明码和生效时候 要疏导到从新登录
退出登陆之后
- 对于 token存在本地的形式,间接删除本地 token 即可,而后会进行3
- 对于session 形式,则须要让 sessionId 生效
接口设计
接口设计
这里不开展讲,能够参考 Restful Api
接口验证
- 哪些接口须要验证用户身份,如何验证
- 哪些不须要验证,如何跳过验证
- 获取用户信息之后如何在一次事务之中传递用户信息
Auth 解说
demo中的auth戳这里>> 这个例子用在了路由外面,咱们将采纳另一个办法,写在最外层,实现按需校验。这样能够防止每个用到的中央都引入这个中间件。
koa-unless : Conditionally skip a middleware when a condition is met.
auth middle file:
var verifyToken = async(ctx, next) => { const req = ctx.request; const token = req.body.token || req.query.token || req.headers["x-access-token"];}; if (!token) { ctx.body = { code: 0, err_code: 401, err_msg:'401' }; }else{ try { const decoded = jwt.verify(token, TOKEN_KEY); req.user = decoded; // 挂载数据 await next(); } catch (err) { ctx.body = { code: 0, err_code: 401, err_msg:'401' } } }};verifyToken.unless = require('koa-unless');module.exports = verifyToken;
app.js
const verifyToken = require('middleware/auth');...// 身份验证app.use(verifyToken.unless({ path: [ // 设置不应用 auth 中间件的 path /\/login/, // 登录应用的接口 ],}));// 进入路由解决app.use(routes());...
用户身份传递:
module.exports = async (ctx, next) => { const key = ctx.request.headers['spacex-key']; if (key) { const user = await db.collection('users').findOne({ key }); if (user?.key === key) { ctx.state.roles = user.roles; // 挂载到 ctx.state上,传递到前面的中间件 await next(); return; } } ctx.status = 401; ctx.body = 'https://youtu.be/RfiQYRn7fBg';};
路由设计
须要思考
- Restful 设计办法
- 没有权限的解决
- 接口构造&报错信息设计
应用路由
应用 koa-route
参考 demo :
- 分模块治理 api,入口文件整体导出
- 应用了 auth middleware
- 应用了
ORM
语法和 modlel 【本文有波及】 - 应用
Redis
做 接口缓存
数据库连贯(MYSQL 版)
第一版: 应用 koa-mysql手写SQL语句
问题是:须要本人形象 sql 语法。因为sql 语句依据性能能够形象(比方形象 条件查问),如果全副手写会写的比拟多。
// from: https://chenshenhai.github.io/koa2-note/note/mysql/info.htmlconst mysql = require('mysql')// 创立数据池const pool = mysql.createPool({ host : '127.0.0.1', // 数据库地址 user : 'root', // 数据库用户 password : '123456' // 数据库明码 database : 'my_database' // 选中数据库})// 在数据池中进行会话操作pool.getConnection(function(err, connection) { connection.query('SELECT * FROM my_table', (error, results, fields) => { // 完结会话 connection.release(); // 如果有谬误就抛出 if (error) throw error; })})
第二版: 应用 sequlize ( orm)
什么是 ORM
? ORM 就是通过实例对象的语法,实现关系型数据库的操作的技术。代表有: sequelize
/ openrecord
/ typeorm
毛病:
- 性能问题 -> 不写特地简单或者非凡的sql能够不必思考这个问题
- 面向对象形式写SQL,总感觉怪怪的。 -> 习惯问题
数据库信息存储【作者未解决】
- 用户名和明码存储:怎么平安的存起来,在应用时不裸露明码?
- 数据库权限问题: 连贯管理员还是普通用户?
整体程序
解决跨域
限度跨域的益处:
- 避免在别的网站被调用,管制申请量
- 避免应用接口工具调用
- 配合用户身份验证能够进一步管制申请量(没有账户的不能拜访)
// 查看 referer, 避免 postman 这种调用app.use(async (ctx, next) => { const { referer = ''} = ctx.header; if(ENV === 'production' && !referer.includes(HOST)){ ctx.response.body = 'Not Allow Origin Request'; }else{ await next(); }})// corsapp.use(cors({ origin:(ctx) => { // 设置可拜访这个服务的起源域 return ENV === '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 = new Koa();//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} is listening ~`);});
错误处理
- uncaughtException
- unhandledRejection
// gracefulShutdown: 关机程序。能够了解为遇到谬误时候的对立解决// Server startapp.on('ready', () => { SERVER.listen(PORT, '0.0.0.0', () => { logger.info(`Running on port: ${PORT}`); // Handle kill commands process.on('SIGTERM', gracefulShutdown); // Handle interrupts process.on('SIGINT', gracefulShutdown); // Prevent dirty exit on uncaught exceptions: process.on('uncaughtException', gracefulShutdown); // Prevent dirty exit on unhandled promise rejection process.on('unhandledRejection', gracefulShutdown); });});
参考: https://github.com/r-spacex/S... SpaceX代码
error middleware: https://sourcegraph.com/githu...
logger middle (用于 debug) : https://sourcegraph.com/githu...
部署到近程服务器
服务器服务疾速复原: pm2部署
劣势:
- 监听文件变动,主动重启程序
- 反对性能监控【也重要】
- 负载平衡
- 程序解体主动重启【重要】
- 服务器重新启动时主动重新启动【重要】
- 自动化部署我的项目
主动部署到近程服务器
服务器条件:
- 服务代码克隆到
/data/server/crm-server
下,这样只须要每次git pull
即可。 - 装置 node
- 全局装置 PM2
name: 服务部署on: push: branches: - mainjobs: build: runs-on: ubuntu-latest steps: - name: executing remote ssh commands using password uses: appleboy/ssh-action@master with: host: ${{ secrets.HOST }} username: ${{ secrets.USERNAME }} password: ${{ secrets.PASSWORD }} port: ${{ secrets.PORT }} script: | cd /data/server/crm-server git checkout main git pull # 如果你的近程服务器是用nvm装的node,须要上面的 export export PATH=$PATH:/home/ubuntu/.nvm/versions/node/v16.5.0/bin pm2 link 你的pm2链接 # 【可选】增加 pm2监控 pm2 restart start.sh # 启动 pm2
start.sh 最终执行:
$ cross-env PORT=8080 ENV=production node app.js
ngix 配置【可选】
我这里是把前端文件(/data/www文件夹下)和后端API都部署到了同一台服务器上,以 http://www.ddup.info 为例:
server { ... location ^~ /crm/api { proxy_pass http://www.ddup.info:8080; } location ^~ /crm { root /data/www; index index.html index.htm; try_files $uri $uri/ /crm/index.html; } location / { root /data/www; index index.html index.htm; try_files $uri /app/index.html; }}
思考:一个正当的后端工程构造
- 参考
express
目录构造: https://github.com/expressjs/... - 参考下
egg
目录构造: https://eggjs.org/zh-cn/basic...
目录构造:
- 动态文件:static
- view层:ejs 模版
- 数据模型: models✅
- 服务: service
- 路由: routes✅
- 中间件: middleware✅
- 定时工作: jobs ✅
后记
首次尝试,不免有考虑不周之处,还请读者指出来,一起学习提高 ~