Node +Koa 进阶

238 阅读7分钟

承接上回,我们同事分享了node+koa基础搭建,那么本篇文章就继续由浅入深继续探究!!!本文主要涵盖内容:node接口化标准、fs文件模块示例、日志查询中间件、部署。下面就让我们一探究竟吧!

通过上次分享,我们已经得到了一个比较贴近于业务项目的node项目,大家可以先熟悉一下目录结构,对下面的文章阅读是有帮助的哦~

image.png

1. node接口化标准

image.png 无论是我们生活中还是在工作coding中,都有着一套流程,比如早上起来闹钟响了,我们要刷牙 穿衣服吃饭,最后坐地铁公交上班。

那么对于程序员来讲,每个负责的业务项目也是,比如在我搭建的node项目中,在utils/requestEnc.js文件 创建了Response类,里面有针对于请求的返回体&成功失败方法 在下面代码例子,可以看到我在返回体里封装了四部分,分别是code:代表请求状态;message:返回信息;data:业务数据,lid:唯一标识。还额外封装了几个静态方法,可以在请求后拿到结果通过code判断再次封装。

const uuid = (pid = '40.60:7001') => {
    // 查询日志区分
    return String(pid) + String(new Date().getTime());
};
class Response {
    // 统一返回结构 data里面有什么
    constructor(code,message,data,lid) {
        this.code = code;
        this.message = message;
        this.data = data; //业务数据
        this.lid = lid;   // 唯一标识
    }
    // 请求的状态
    // 200 成功
    static success(data,message='请求成功啦啦-->>') {
        return new Response(0,message,data,uuid);
    }
    //失败
    static error(code,message,data=null){
        return new Response(code,message,data,uuid);
    }
    // 无权访问
    static permission(code=403,message='您无权访问哦-->>',data=null){
        return new Response(code,message,data,uuid);
    }
    // 用户未登录
    static notLoginIn(code=401,message='您未登录哦-->>',data=null){
        return new Response(code,message,data,uuid);
    }
}
exports.Response = Response;

2. fs文件模块示例

fs文件模块是Node.Js内置的文件系统模块,允许我们读取、写入、删除和操作文件以及目录,能够以标准的POSIX函数为模型的方式与文件系统进行交互。

接下来 ,开始进入到我们的实操喽,本着在业务开发中,我们平时操作文件也比较多,所以就以fs模块为例子,演示增删查操作~~,下图就是我们的fs页面,涉及的操作有:获取文章列表、上传文章、删除文章。

image.png

image.png 所有的接口写在了routes里面,分为controller/services文件。controller具体定义接口,services主要写实现 1. 获取文章列表:通过请求/getDoc接口,拿到存储文件的绝对路径,通过fs.readdirSync获取该目录下的所有文件,在通过fs.statSync拿到文件信息,具体代码实现如下:
const getArticleList = async () => {
  // 拿到当前文件存储的目录
  try {
    // 绝对路径
    const directoryPath = path.resolve(__dirname, "../articles");
    if (directoryPath) {
      // sync同步事件 阻塞
      const files = fs.readdirSync(directoryPath);
      // 拿到所有的文件路径 join拼接
      const filesPath = files.map((file) => path.join(directoryPath, file));
      const fileInfo = filesPath.map((filePath) => ({
        name: path.basename(filePath),
        type: fs.statSync(filePath).isDirectory() ? "directory" : "file",
        size: fs.statSync(filePath).size,
        birthtime: fs.statSync(filePath).birthtime,
      }));
      return { data: fileInfo, code: 0 };
    }
  } catch (err) {
    console.log(err, "read article error--->>>");
    return { code: 500, data: "读取文件发生异常啦---->>>>" };
  }
};

2. 上传功能:fse.copy传入虚拟路径和真实存放路径

const fse = require("fs-extra");
const writeArticleDetail = async (file, filepath) => {
  try {
    // 虚拟路径-》实际路径
    await fse.copy(file.filepath, filepath);
    return { data: "已经成功写入文件啦---->>>" };
  } catch (err) {
    console.log(err, "write article error--->>>");
    return { code: 500, data: "啊哦,写入文件失败--->>>" };
  }
};

4. 删除功能:通过传入fileName,进行fs.remove

const deleteArticleDetail = async (fsPath) => {
  console.log(fsPath, "fsPath-->>>>");
  try {
    // 注意2:不返回任何值 undefined
    if (!(await fse.exists(fsPath))) {
      return { code: 500, data: "文件不存在-->>" };
    }
    await fse.remove(fsPath);
    return { code: 0, data: "成功删除文件--->>" };
  } catch (err) {
    console.log(err, "delete article error--->>>");
    return { code: 500, data: "删除失败啦--->>>" };
  }
};

📝 例子中使用到的fs的api&坑点记录

image.png

image.png

3. 日志查询中间件

常用的日志查询中间件如下,该项目选择使用koa-log4为例;

image.png

image.png koa-log4是可以自定义查询日志等级,自己定义日志输出格式,我们可以思考一下查询日志都需要哪些信息,可以自己根据想要的封装,我认为其中必不可少的应该要包括:请求方式、响应状态、请求url、域名、请求时间等等; 我在config/log-config中重新定义日志查询格式,在middleware中引入。

/config/log-config
/**
 * 日志生成及格式化
 */
const log4j = require("koa-log4");

const msFn = () => {
  const date = new Date();
  const milliseconds = date.getMilliseconds().toString().padEnd(3, "0");
  return `${milliseconds}`;
};
//服务错误日志
const formatError = (ctx, err, costTime) => {
  const {
    url,
    response: { status },
    request: {
      header: { host },
      method,
    },
  } = ctx;
  const msg = ctx.body || {};
  console.log(msg, "msgggggg");
  const Header = JSON.stringify(ctx.request.header);
  const SourceIp =
    ctx.request.ip ||
    ctx.request.headers["x-forwarded-for"] ||
    ctx.request.connection;
  return `.${msFn()}|${url}|${method}|status=${status}|host=${String(
    host
  )}|SourceIp=${SourceIp}|${
    msg.lid || "60.40"
  }}|${costTime}|${Header}|body=${msg}|error=${err}`;
};
//服务日常日志
const formatRes = (ctx, costTime) => {
  // console.log(ctx.body.lid);
  const {
    url,
    response: { status },
    request: {
      header: { host },
      method,
    },
  } = ctx;
  const msg = ctx.body;
  const Header = JSON.stringify(ctx.request.header);
  const SourceIp =
    ctx.request.ip ||
    ctx.request.headers["x-forwarded-for"] ||
    ctx.request.connection;
  return `.${msFn()}|${url}|${method}|status=${status}|host=${String(
    host
  )}|SourceIp=${SourceIp}|${
    ctx.body.lid
  }|${costTime}|${Header}|body=${JSON.stringify(msg || {})}`;
};
//服务code=500日志
const formatFatal = (ctx, costTime) => {
  const {
    url,
    request: {
      header: { host },
      method,
    },
  } = ctx;
  const msg = ctx.body;
  const Header = JSON.stringify(ctx.request.header);
  const SourceIp =
    ctx.request.ip ||
    ctx.request.headers["x-forwarded-for"] ||
    ctx.request.connection;
  return `.${msFn()}|${url}|${method}|status=${ctx.body.code}|host=${String(
    host
  )}|SourceIp=${SourceIp}|${
    ctx.body.lid
  }}|${costTime}|${Header}|body=${JSON.stringify(msg || {})}`;
};

const levels = {
  trace: log4j.levels.TRACE,
  debug: log4j.levels.DEBUG,
  info: log4j.levels.INFO,
  warn: log4j.levels.WARN,
  //程度中等、可以被捕获到的 比如数据库查询失败、用户输入验证失败等等,不会阻止程序的运行 404
  error: log4j.levels.ERROR,
  //程度叫高、程序无法运行的错误,比如内存溢出等等,会阻止
  fatal: log4j.levels.FATAL,
};
// log4j配置
log4j.configure({
  // 指定输出文件类型和文件名
  appenders: {
    //常规输出
    console: { type: "console" },
    //日常日志
    info: {
      type: "dateFile", // 输出的类型
      pattern: "yyyy-MM-dd.log", // 格式
      filename: "logs/access.log", // 地址
      layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
    },
    //错误日志
    error: {
      type: "dateFile",
      filename: "logs/server_error.log",
      pattern: "yyyy-MM-dd.log",
      layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
    },
    //500日志
    fatal: {
      type: "dateFile",
      filename: "logs/server_fatal.log",
      pattern: "yyyy-MM-dd.log",
      layout: { type: "pattern", pattern: "fe-web|%d{yyyy-MM-dd hh:mm:ss}%m" },
    },
  },
  // 设置日志输出级别
  categories: {
    default: {
      appenders: ["console"],
      level: "debug",
    },
    info: {
      appenders: ["info"],
      level: "info",
    },
    error: {
      appenders: ["error"],
      level: "error",
    },
    fatal: {
      appenders: ["fatal"],
      level: "fatal",
    },
  },
});
// 抛出日志记录方法
/**
 * 日志输出 level为debug
 * @param { string } content
 */
const debug = (content) => {
  let logger = log4j.getLogger("debug");
  logger.level = levels.debug;
  logger.debug(formatRes(content));
};
/**
 * 日志输出 level为info
 * @param { string } ctx
 */
const info = (ctx, costTime) => {
  let logger = log4j.getLogger("info");
  logger.level = levels.info;
  logger.info(formatRes(ctx, costTime));
};
/**
 * 日志输出 level为error
 * @param { string } ctx
 */
const error = (ctx, err, costTime) => {
  let logger = log4j.getLogger("error");
  logger.level = levels.error;
  logger.error(formatError(ctx, err, costTime));
};
/**
 * 日志输出 level为fatal
 * @param { string } ctx
 */
const fatal = (ctx, costTime) => {
  let logger = log4j.getLogger("fatal");
  logger.level = levels.fatal;
  logger.fatal(formatFatal(ctx, costTime));
};

module.exports = {
  debug,
  info,
  error,
  fatal,
};

最后引入使用 切记:在app.use挂载的时候 日志中间件一定一定是最先挂载!!!! 洋葱模型,先进后出,我们要获取所有的接口日志

image.png

image.png

4. 部署

使用esbuild打包

  1. 确保安装node; node -v检查;
  2. 安装esbuild npm install esbuild;
  3. 配置esbuild文件;
  4. package.json 添加命令"build": "node build.js";
  5. 执行命令 npm run build;
  6. 查看输出文件

image.png 部署到服务器

首先申请机器,检查node版本是否匹配,如果我们本地使用node版本为14,机器使用的是16,我们不可能更改机器上线node版本,更改本地如果代价太大也不划算,这里我们可以使用docker进行环境隔离。node端口只能进行一次操作,我们可以安装pm2,进行进程管理。

image.png

Docker是一组平台即服务(PaaS)的产品。它基于操作系统层级的虚拟化技术,将软件与其依赖项打包为容器。托管容器的软件称为Docker引擎。Docker能够帮助开发者在轻量级容器中自动部署应用程序,并使得不同容器中的应用程序彼此隔离,高效工作。该服务有免费和高级版本。

#dockerFile文件编写
# 使用官方的 Node.js 作为基础镜像
FROM node:16-alpine

# 设置工作目录为 /app
WORKDIR /app

# 设置 npm 源为淘宝源
RUN npm config set registry https://registry.npmmirror.com

# 全局安装 pm2
RUN npm install -g pm2

# 将当前目录下的 dist、logs、public、articles 文件夹及内容复制到 /app 下
COPY dist /app/dist
COPY logs /app/logs
COPY public /app/public
COPY articles /app/articles

# 暴露端口(如果需要)
EXPOSE 7001
# 设置容器启动时执行的命令(使用 pm2 启动应用)
CMD ["pm2-runtime", "start", "dist/app.json"]

# docker build -t my-app .
# docker run -d \
#   -p 17001:7001 \
#   -v /home/logs/node-share:/app/logs \
#   my-app

最后就可以在我们申请的机器上面,看到我们的请求和返回数据啦!!

image.png