node项目从0到1实战

specialCoder -
node项目从0到1实战
前言

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

适合人群:

未完整搭建过node服务的人学写了koa, 想实践的人正在使用 koa 搭建 node 服务的人正文

为了实现 api 接口请求,我们考虑几个问题:

整个服务程序的处理流程与错误处理接口路由接口调用权限接口缓存访问数据库

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

日志处理监控报警快速恢复认识一下常用中间件request 参数解析插件

参数分3种:

url searchurl parameterPOST 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-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,主要知识点:

缓存设置:

  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-CredentialsAccess-Control-Allow-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-MethodsAccess-Control-Max-Agedebugkoa-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.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、密码、头像这种。
用户表设计:

usernameldappasswordavatar张三zhangsan01z123456https://avatar.com/z123456李四lisil000l123456https://avatar.com/l000用户权限

用户权限则是在用户信息上的加强,每个用户都关联着一系列权限。用户权限可以认为是业务侧的实现,信息更丰富,业务更重。
权限表设计:

ldapproductpermissionzhangsan01product13zhangsan01product21lisiproduct17用户身份生效/失效 机制

生效:

登陆的时候重新生成token修改了密码的时候重新生成token

失效:

退出登陆的时候token过期

后续处理:

重新登陆之后检查重定向地址进行跳转修改密码和失效时候 要引导到重新登录

退出登陆之后

对于 token存在本地的方式,直接删除本地 token 即可,然后会进行步骤2对于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 ~`);
});
错误处理uncaughtExceptionunhandledRejection
// 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...

目录结构:

静态文件:staticview层:ejs 模版数据模型: models✅服务: service路由: routes✅中间件: middleware✅定时任务: jobs ✅后记

初次尝试,难免有考虑不周之处,还请读者指出来,一起学习进步 ~

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

node.jskoa2api

扩展阅读

加个好友,技术交流

1628738909466805.jpg