用nodejs实现项目监听、打包、自动发送压缩包或消息到企业微信

1,498 阅读5分钟

前言

  • 起因
    • 2023年了,有的项目因为各种原因还是没有用上ci/cd🌝,所以还是传统项目打包完,自己手动压缩,然后上传企业微信,本文目的就是将这部分工作交给机器,平均每次打包能节约2分钟吧😁(因为经常忘记是不是打包完了,所以可能还要比2分钟长)
  • 你可以获取到什么知识
    • 企业微信发消息(文本消息,文件消息)
    • 在nodejs里面上传文件,前端一般是点击上传按钮选择文件,然后走formData那一套,那nodejs怎么做呢,可以看一下下面的实现
    • nodejs里面将文件夹变成zip,监听文件变化

总需求

运行本项目A,在项目B打包完成后,A项目检测有没有已经压缩的zip,有就删除,重新压缩,压缩完成后,向企业微信(钉钉/飞书等支持webhook的工具都可以,本文以企业微信为例)发信息或者发送这个压缩包(个人建议还是发消息,压缩包只支持20M以下(压缩包大于20M可能要考虑优化了哈哈😃),并且要走企业微信上传,数据无价🙂)

初始化

  • 本文就用到了两个包 axios 与 archiver
  • axios 只是为了发请求,你可以用任何一种你用的熟练或者内置的情况下就用项目的请求库
  • archiver 是打包的库,compressing也可以不过我打包出来的zip总是在奇怪的位置,懂哥可以补充一下
  • 其实也可以用 linux 命令 zip,不过这种就怕删库🙃,加上你要装 压缩工具(zip这种) 到你的命令行
yarn init -y / npm init -y

// package.json
{
  "scripts": {
  	"start": "node index.js"
  }
}

// 装包 如果是内置到项目里面,这里建议加 -D,如果是想外置就随意
yarn add axios archiver -D

公共部分

  • 在企业微信群里面可以申请机器人,会有一个 webhook 地址就是下面这个 weixinUploadUrl ,自己把对应位置的 weixinApiKey 复制出来就行
  • 按下面的格式就是我想 等 D:/workspace/front/dist 这个文件夹生成完的时候自动压缩成 dist.zip
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const axios = require('axios');
const FormData = require('form-data');

const sourceDir = 'D:/workspace/front'; // 监听的目录
const watchFileName = 'dist'; // 监听的文件夹名称
const zipFile = `dist.zip`;
const weixinApiKey = '这是你的企业微信机器人 key'; // 企业微信机器人 机器人key
const weixinUploadUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${weixinApiKey}&type=file`;

监听

  • 监听部分用node 自带的 fs.watch,内置这个js文件这一步不需要
  • 外置麻烦就在这里,比如dist文件夹从生成,到里面文件完全生成完毕,我不知道里面所有文件什么时候完全生成完毕,所以开始复制时我直接延时一分钟再打包,当然如果有朋友能知道怎么优化我这边调一下最好
// 监听 dist 文件夹 部分
fs.watch(sourceDir, { recursive: true }, (event, filename) => {
  if (filename !== watchFileName !== 'rename') {
    return;
  }
  setTimeout(compress, 60 * 1000) // 
});

压缩

  • 如果不想走企业微信上传那就注释下面提到的那一行,就发个消息通知自己,自己复制压缩包发给同事就行,相信就直接走企业微信上传,然后机器人帮你发这个zip到群里
// 【压缩部分】
async function compress() {
  const distPath = path.join(sourceDir, watchFileName);
  // 这里加了 .. 是回到上一层,开始用的时候发现打出来的zip跑到 dist文件夹里面去了这里做修正
  // 有优化建议的可以提一提,优化一下
  const distZipPath = path.join(distPath, '..', zipFile);

  // 判断是否有 dist 文件夹
  if (!fs.existsSync(distPath)) {
    console.log(
      `[${new Date().toLocaleString()}]: 未检测到 ${watchFileName} 文件夹,不进行压缩`
    );
    return;
  }

  const isDirEmpty = (() => {
    const dirItems = fs.readdirSync(distPath);
    return dirItems.length === 0;
  })();

  if (isDirEmpty) {
    console.log(
      `[${new Date().toLocaleString()}]: ${watchFileName} 文件夹为空,不进行压缩`
    );
    return;
  }

  // 判断文件是否存在,存在则删除
  if (fs.existsSync(distZipPath)) {
    fs.unlinkSync(distZipPath);
    console.log(`已删除文件:${distZipPath}`);
  }

  const output = fs.createWriteStream(distZipPath);
  const archive = archiver('zip', { zlib: { level: 9 } });

  output.on('close', async function () {
    console.log(
      `[${new Date().toLocaleString()}]: 压缩成功,共 ${archive.pointer()} 个字节`
    );

    let media_id = '';

    // 将打包完成的文件上传企业微信  【看这里 上文提到可以注释的这一行】
    media_id = await uploadZip(distZipPath);

    // 发送企业微信 信息
    sendWeixinMessage(media_id);
  });

  archive.on('error', function (err) {
    console.error(`[${new Date().toLocaleString()}]: 压缩失败: ${err}`);
  });

  archive.pipe(output);

  archive.glob('**/*.*', {
    cwd: distPath,
    dot: false,
    matchBase: false,
  });

  archive.finalize();
}

上传企业微信

  • 不相信企业微信数据安全的可以忽略不用这一步
  • 这也可以当作在node环境下上传文件的示例
// 【企业微信上传文件部分】
async function uploadZip(distZipPath) {
  // 发送文件到企业微信
  const bufferData = fs.readFileSync(distZipPath);

  const formData = new FormData();
  formData.append('media', bufferData, { filename: zipFile });
  const config = {
    headers: {
      'Content-Type': `multipart/form-data; boundary=${formData._boundary}`,
    },
  };

  try {
    const {
      data: { media_id },
    } = await axios.post(weixinUploadUrl, formData, config);
    console.log(
      `[${new Date().toLocaleString()}]: 文件上传成功,media_id为 ${
        media_id
      }`
    );
    return media_id;
    
  } catch (err) {
    console.error(`[${new Date().toLocaleString()}]: 文件上传失败: ${err}`);
  }
};

发送信息

// 【企业微信发送信息部分】
async function sendWeixinMessage(mediaId = null, message = '') {
  const messageUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weixinApiKey}`;
  const messageContent = message || `${zipFile}压缩完成`;
  if (mediaId) {
    // 这块是走了上传的情况,走了上传就会得到这个mediaId
		// 企业微信根据这个 mediaId找到你上传的文件,让机器人发
    const requestBody = {
      msgtype: 'file',
      file: {
        media_id: mediaId,
      },
    };
    await axios.post(messageUrl, requestBody);
  }
  // 这块就是企业微信发消息最简单的部分 详细可以参考 申请机器人的那个文档
  const messageBody = {
    msgtype: 'text',
    text: {
      content: messageContent,
    },
  };
  await axios.post(messageUrl, messageBody);
  console.log(
    `[${new Date().toLocaleString()}]: 已向企业微信发送消息:${messageContent}`
  );
}

实现代码

index.js

const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const axios = require('axios');
const FormData = require('form-data');

// const sourceDir = 'D:/workspace/frontend'; // 监听的目录
const sourceDir = 'D:/workspace/江海证券/front'; // 监听的目录
const watchFileName = 'dist'; // 监听的文件夹名称
const zipFile = `dist.zip`;
const weixinApiKey = '475e214e-8cb5-4c83-b360-670bcad2f282'; // 企业微信机器人 机器人1 key
const weixinUploadUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${weixinApiKey}&type=file`;

// 【企业微信发送信息部分】
async function sendWeixinMessage(mediaId = null, message = '') {
  const messageUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weixinApiKey}`;
  const messageContent = message || `${zipFile}压缩完成`;
  if (mediaId) {
    const requestBody = {
      msgtype: 'file',
      file: {
        media_id: mediaId,
      },
    };
    await axios.post(messageUrl, requestBody);
  }
  const messageBody = {
    msgtype: 'text',
    text: {
      content: messageContent,
    },
  };
  await axios.post(messageUrl, messageBody);
  console.log(
    `[${new Date().toLocaleString()}]: 已向企业微信发送消息:${messageContent}`
  );
}

// 【压缩部分】
async function compress() {
  const distPath = path.join(sourceDir, watchFileName);
  const distZipPath = path.join(distPath, '..', zipFile);

  // 判断是否有 dist 文件夹
  if (!fs.existsSync(distPath)) {
    console.log(
      `[${new Date().toLocaleString()}]: 未检测到 ${watchFileName} 文件夹,不进行压缩`
    );
    return;
  }

  const isDirEmpty = (() => {
    const dirItems = fs.readdirSync(distPath);
    return dirItems.length === 0;
  })();

  if (isDirEmpty) {
    console.log(
      `[${new Date().toLocaleString()}]: ${watchFileName} 文件夹为空,不进行压缩`
    );
    return;
  }

  // 判断文件是否存在,存在则删除
  if (fs.existsSync(distZipPath)) {
    fs.unlinkSync(distZipPath);
    console.log(`已删除文件:${distZipPath}`);
  }

  const output = fs.createWriteStream(distZipPath);
  const archive = archiver('zip', { zlib: { level: 9 } });

  output.on('close', async function () {
    console.log(
      `[${new Date().toLocaleString()}]: 压缩成功,共 ${archive.pointer()} 个字节`
    );

    let media_id = '';

    // 将打包完成的文件上传企业微信
    // media_id = await uploadZip(distZipPath);

    // 发送企业微信 信息
    sendWeixinMessage(media_id);
  });

  archive.on('error', function (err) {
    console.error(`[${new Date().toLocaleString()}]: 压缩失败: ${err}`);
  });

  archive.pipe(output);

  archive.glob('**/*.*', {
    cwd: distPath,
    dot: false,
    matchBase: false,
  });

  archive.finalize();
}

// 【企业微信上传文件部分】
async function uploadZip(distZipPath) {
  // 发送文件到企业微信
  const bufferData = fs.readFileSync(distZipPath);

  const formData = new FormData();
  formData.append('media', bufferData, { filename: zipFile });
  const config = {
    headers: {
      'Content-Type': `multipart/form-data; boundary=${formData._boundary}`,
    },
  };

  try {
    const {
      data: { media_id },
    } = await axios.post(weixinUploadUrl, formData, config);
    console.log(
      `[${new Date().toLocaleString()}]: 文件上传成功,media_id为 ${
        media_id
      }`
    );
    return media_id;
    
  } catch (err) {
    console.error(`[${new Date().toLocaleString()}]: 文件上传失败: ${err}`);
  }
};

let timer = null; 
//【监听部分】 监听 dist 文件夹 部分
fs.watch(sourceDir, { recursive: true }, (event, filename) => {
  if (filename !== watchFileName) {
    return;
  }
  clearTimeout(timer)
  // 这一段用来测试打印 计算得出下面的 10s,我这边应该是8S左右,目前没有一个好一点 简单一点 的办法监听dist文件夹里面的东西完全生成成功
  console.log(
    `[${new Date().toLocaleString()}]: 检测到 ${watchFileName} 文件夹变化,开始打印`
  );

  timer = setTimeout(() => {
    console.log(
      `[${new Date().toLocaleString()}]: 检测到 ${watchFileName} 文件夹变化,开始压缩...`
    );
    compress();
  }, 10000); // 延迟 10s 后压缩,如果没有变化就正常进行压缩
});

内置方案

外置的情况下,不能完全准确的实现项目打包完毕开始压缩,这一点可以通过把这个js文件内置项目里面,但是项目会多装一个archiver包,如果接受内置的话,方案应该是算比较完美的,并且可以去掉监听的部分,直接压缩,可以用 npm scripts 自带的钩子实现,大体逻辑如下

// package.json
{
	"scripts": {
  	"build": "xxx " // 项目打包代码
    "postbuild": "node 文中的js代码" // postXXX 会在 XXX 执行完成后执行
  }
}

优化空间与扩展

  • 优化空间
    • 外置的情况可以整一个对象,项目A 地址:wx key 这种,监听多个文件夹,不过并发啥的就更复杂了,理论不会有🤣,一般不会同时打包3个项目,3个项目同时打包完吧😆,交给有缘人优化
    • 监听 dist 文件夹的内容完全生成完毕,目前没有准确监听手段除非内置js到项目
  • 扩展
    • 这文章主要抛砖引玉,企业微信/钉钉/飞书等机器人发消息一般都是用在服务器CI/CD上面,同时呢,这个类似的服务你可以运行在服务器上面,实现自动签到,每日提醒,每日天气等各种地方

感谢

感谢各位看到这里,第一次发文章,可能有点啰嗦,喜欢的可以帮我在对应的这个github点个star😁 github 附带 内置与外置版本 github.com/qinbuff/mov…