关于node.js:node项目从0到1实战

42次阅读

共计 7770 个字符,预计需要花费 20 分钟才能阅读完成。

前言

本文以 koa 框架为例,从 0 到 1 搭建一个后端服务,涵盖了后端服务的根本内容,实现 api 的性能。

适宜人群:

  1. 未残缺搭建过 node 服务的人
  2. 学写了 koa,想实际的人
  3. 正在应用 koa 搭建 node 服务的人

注释

为了实现 api 接口申请,咱们思考几个问题:

  1. 整个服务程序的解决流程与错误处理
  2. 接口路由
  3. 接口调用权限
  4. 接口缓存
  5. 拜访数据库

除了服务程序自身,还要思考工程相干的(本文不开展讲):

  • 日志解决
  • 监控报警
  • 疾速复原

认识一下罕用中间件

request 参数解析插件

参数分 3 种:

  • url search
  • url parameter
  • POST body

koa-bodyparser: 把 request body 上的数据挂载到 ctx.request.body,反对 json / text / xml / form (不反对 multipart)

文件缓存相干

  • koa-static: 动态文件系统,反对 maxagegzip 等属性。这个中间件配合上面的中间件更好用:

    • 搭配 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-getkoa-etag 实现整套缓存流程。
应用缓存 demo,次要知识点:

  1. 缓存设置:

      if (ttl) {ctx.response.set('Cache-Control', `max-age=${ttl}`);
      } else {ctx.response.set('Cache-Control', 'no-store');
      }
  2. 生成 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 须要满足的条件
  1. 惟一 ID,代表举世无双的用户账号
  2. 有效期,生效后须要从新登录,用于爱护用户账号
简陋的实现
  1. 通过 UUID 实现惟一 ID
  2. 通过 Redis 缓存有效期来等效 token 有效期
优雅的实现

应用 jsonwebtoken(JWT): https://github.com/auth0/node…
特点:

  1. 加密 / 解密 机制
  2. 生成惟一 ID
  3. 可反对有效期设置

举个🌰:
服务端生成 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 还能够自带参数,这能够省去将用户信息存在数据库的步骤,只是在计算的时候须要耗费性能。

利用里的实现:

  1. 首先,失去新 token 之后存储到本地
  2. 每次申请在 x-access-token 里携带 token
// config.js
export const LOCAL_KEY = `${你的域名}-token`; // 这样防止反复

// request.js
const 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 过期

后续解决:

  1. 从新登陆之后查看重定向地址进行跳转
  2. 批改明码和生效时候 要疏导到从新登录
  3. 退出登陆之后

    • 对于 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.html

const 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();
  }
})
// cors
app.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. 创立 app
app = 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. 监听 port
app.listen(PORT, () => {console.log(`port ${PORT} is listening ~`);
});

错误处理

  • uncaughtException
  • unhandledRejection
// gracefulShutdown: 关机程序。能够了解为遇到谬误时候的对立解决
// Server start
app.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:
      - main
jobs:
  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 ✅

后记

首次尝试,不免有考虑不周之处,还请读者指出来,一起学习提高 ~

正文完
 0