IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    基于node.js 自动打包项目,并提供http下载

    weiweiyi发表于 2023-02-14 18:35:24
    love 0

    前言

    需求是:当 gitlab 项目,新建tag发布时。通过脚本自动打包成压缩包,并提供http下载。使得项目发布新版本时,用户能够通过url直接下载

    流程图:
    image.png

    服务器配置

    目前用于实现http服务的软件有许多,包括主流的Apache、Nginx,还有微软的IIS等。这里使用apache。

    在学习HTTP协议的时候,我们知道URL实际上指定的就是服务器上特定的文件,所以使用apche中的httpd这个轻量的HTTP server实现一个简单的文件下载服务是再合适不过的了。

    1.安装
    apt-get install apache2
    若有子包选择,则选择httpd

    2.启动

    /etc/init.d/apache2 start

    3.查看启动状态:
    /etc/init.d/apache2 status

    4.尝试访问
    然后,访问服务器的公网ip或域名,就可以看到类似如下界面,此时说明Apache正常工作:
    若需要更改端口, 可以看这篇 修改apache2端口

    image.png

    5.下载

    最后在 /var/www/html 路径下,删除 index.html,上传自己想要被下载的文件,再次访问,就可以进行下载了。

    image.png

    若不想所有人都可以通过该url访问到服务器,还可以加上账号密码验证,可以参考账号密码验证

    image.png

    最后,可以通过 域名/文件名 的方式直接给别人一个链接,进行下载。

    例如:xxxxx.com:port/20230214_myTestProject.zip, 访问该url就可以下载。

    脚本开发

    首先需要借助 gitlab 的 CI/CD

    GitLab CI/CD

    CI ( Continuous Integration) ,中文即持续集成。
    CD ( Continuous Delivery / Continuous Deployment) 持续交付 或 持续布署。
    官方文档

    首先需要安装gitlab-runner,官方安装文档

    编写.gitlab-ci.yml

    设置执行规则

    workflow:
      # 当有tag或merge request时执行
      rules:
        - if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_TAG != null'
    
    # 设置步骤(步骤间串行)
    stages:
       - pack-web
    

    命令行写法

    最开始我是通过直接命令行的形式编写:

    angular-pack:
      tags:
        - docker
      stage: pack-web
      # 找一个提供node的镜像
      image: registry.cn-beijing.aliyuncs.com/mengyunzhi/node-chrome:14.16.0
      variables:
       # 定义压缩包名字,日期为前缀,项目名称为后缀
       ZIP_NAME: "`date +%Y-%m-%d-%H-%M-%S`$CI_PROJECT_NAME"
      before_script:
       - cd web
      script:
        - env
        - npm install -dd
        - npm run build
        - apt install zip
        - zip -r $ZIP_NAME.zip dist
        - apt install sshpass -y
        - sshpass -p <password> scp -P 5022 -o StrictHostKeyChecking=no $ZIP_NAME.zip  <username>@<Server IP>:/var/www/html
      rules:
        - if: $CI_COMMIT_TAG != null
    
    1. 通过npm install 和 npm run build 打包构建,生成dist文件夹
    2. 安装zip, 并使用zip命令将dist文件夹压缩成压缩包
    3. 安装sshpass包,使得scp不需要交互式输入密码。
    4. 使用scp命令, 将文件夹发送到服务器上。 填上服务器的ip,ssh端口,用户名密码,以及传输的目标位置

    直接使用命令很不方便,我们可以编写脚本

    编写node.js脚本

    1.引入第三方模块

    # 提供ssh连接
    const {NodeSSH} = require('node-ssh'); // 提供ssh连接
    const compressing = require('compressing'); //文件压缩
    const ChatBot = require('dingtalk-robot-sender'); //提供钉钉推送

    2.初始化变量

    // 初始化变量
    const projectName = process.env.CI_PROJECT_NAME; // 由环境变量获取的工程名称
    const appDir = `/var/www/html`;       // 应用根位置
    const host = process.env.HOST;        // 服务器ip
    const sshPort = process.env.SSHPORT;    // ssh端口
    const port = process.env.PORT;         // http下载端口
    const username = process.env.USERNAME; // 服务器用户名
    const password = process.env.PASSWORD; // 服务器密码
    const bashUrl = "https://oapi.dingtalk.com/robot/send?access_token=";
    const dingToken = process.env.DINGTOKEN; // 钉钉机器人token
    

    3.连接服务器

    console.log('尝试连接生产服务器');
    const ssh = new NodeSSH();
    await ssh.connect({
      host: `${host}`,
      port: `${sshPort}`,
      username: `${username}`,
      password: `${password}`
    });

    4.压缩文件

    // 定义压缩包名字为 日期_项目名称
    const d_t = new Date();
    let year = d_t.getFullYear();
    let month = ("0" + (d_t.getMonth() + 1)).slice(-2);
    let day = ("0" + d_t.getDate()).slice(-2);
    const zipName = year + month + day + "_" + projectName;
    
    // 压缩文件,参数1:要压缩的文件夹, 参数2: 压缩后的名字
    await compressing.zip.compressDir('dist', `${zipName}.zip`)
      .then(() => {
        console.log('压缩成功');
      })
      .catch(err => {
        console.error(err);
      });

    5.上传到服务器

     console.log('开始上传压缩包');
     // 参数1:为上传的文件名称, 参数2:服务器的路径以及文件名
     await ssh.putFile(`${zipName}.zip`,`${appDir}/${zipName}.zip`)

    6.钉钉推送下载地址

     await dingSendSuccess(zipName);
    
    const dingSendSuccess = async function (zipName) {
      try {
        const robot = new ChatBot({
          webhook: bashUrl + dingToken
        });
        
        let downloadUrl = host + ":" + port + "/" + zipName + ".zip";
        let title = "#### 😀 😃 😄 😁 😆";
        let text = "## 😀 😃 😄 😁 😆\n" +
            `> ### ${projectName} 打包成功!  [下载地址](http://${downloadUrl}) \n`;
    
        await robot.markdown(title, text, {}).then((res) => {
          console.log("响应信息:" + res.data);
        });
      } catch (e) {
        console.log('推送错误', e);
      } finally {
        process.exit(0);
      }
    }

    下载地址为: 服务器ip + http端口 + 文件名

    7.编写失败的钉钉推送

    const dingSendError = async function (error) {
      try {
        const robot = new ChatBot({
          webhook: bashUrl + dingToken
        });
        let title = "#### 😢 👿 😧 💔";
        let text = "## 😢 👿 😧 💔\n" +
            `> ### ${projectName} 打包失败!  \n` +
            `> #### 错误信息: ${error}  \n`;
    
        await robot.markdown(title, text, {}).then((res) => {
          console.log("响应信息:" + res);
        });
    
      } catch (e) {
        console.log('推送错误', e);
      } finally {
        process.exit(0);
      }
    }

    8.完整代码

    // 引入第三方模块
    const {NodeSSH} = require('node-ssh');
    const compressing = require('compressing');
    const ChatBot = require('dingtalk-robot-sender');
    
    // 初始化变量
    const projectName = process.env.CI_PROJECT_NAME; // 由环境变量获取的工程名称
    const appDir = `/var/www/html`;       // 应用根位置
    const host = process.env.HOST;
    const sshPort = process.env.SSHPORT;    // 下载端口
    const port = process.env.PORT;    // 下载端口
    const username = process.env.USERNAME;
    const password = process.env.PASSWORD;
    const bashUrl = "https://oapi.dingtalk.com/robot/send?access_token=";
    const dingToken = process.env.DINGTOKEN;
    
    // 定义开始函数,在文件结尾调用
    const start = async function () {
      try {
        console.log('尝试连接生产服务器');
        const ssh = new NodeSSH();
        await ssh.connect({
          host: `${host}`,
          port: `${sshPort}`,
          username: `${username}`,
          password: `${password}`
        });
    
        const d_t = new Date();
    
        let year = d_t.getFullYear();
        let month = ("0" + (d_t.getMonth() + 1)).slice(-2);
        let day = ("0" + d_t.getDate()).slice(-2);
        const zipName = year + month + day + "_" + projectName;
    
        await compressing.zip.compressDir('dist', `${zipName}.zip`)
            .then(() => {
              console.log('压缩成功');
            })
            .catch(err => {
              console.error(err);
            });
    
        console.log('开始上传压缩包');
        await ssh.putFile(`${zipName}.zip`,
            `${appDir}/${zipName}.zip`)
        await dingSendSuccess(zipName);
    
      } catch (e) {
        console.log('打包发生错误', e);
        await dingSendError(e.message);
      } finally {
        process.exit(0);
      }
    }
    
    const dingSendSuccess = async function (zipName) {
      try {
        const robot = new ChatBot({
          webhook: bashUrl + dingToken
        });
        
        let downloadUrl = host + ":" + port + "/" + zipName + ".zip";
        let title = "#### 😀 😃 😄 😁 😆";
        let text = "## 😀 😃 😄 😁 😆\n" +
            `> ### ${projectName} 打包成功!  [下载地址](http://${downloadUrl}) \n`;
    
        await robot.markdown(title, text, {}).then((res) => {
          console.log("响应信息:" + res.data);
        });
    
      } catch (e) {
        console.log('推送错误', e);
      } finally {
        process.exit(0);
      }
    
    }
    
    const dingSendError = async function (error) {
      try {
        const robot = new ChatBot({
          webhook: bashUrl + dingToken
        });
        let title = "#### 😢 👿 😧 💔";
        let text = "## 😢 👿 😧 💔\n" +
            `> ### ${projectName} 打包失败!  \n` +
            `> #### 错误信息: ${error}  \n`;
    
        await robot.markdown(title, text, {}).then((res) => {
          console.log("响应信息:" + res);
        });
    
      } catch (e) {
        console.log('推送错误', e);
      } finally {
        process.exit(0);
      }
    
    }
    
    start().then().catch(function (error) {
      console.log('发生错误', error);
      process.exit(1);
    });

    9.使用

    将文件传到项目文件夹下,然后将可以通过node pack.js 使用

    angular-pack:
      tags:
        - docker
      stage: pack-web
      image: registry.cn-beijing.aliyuncs.com/mengyunzhi/node-chrome:14.16.0
      before_script:
       - cd web 
      script:
        - env
        - npm install -dd
        - npm run build
        - node pack.js
      rules:
        - if: $CI_COMMIT_TAG != null
    

    10.效果图
    image.png

    期间遇到的问题

    1.文件名过长,无法正常压缩

    1676181683242_A61BAD5A-4F79-4eb9-AA5C-A3D0CC54788E.png

    最开始文件名设置的是,年月日_时分秒_项目名字。

    例如: 2023-2-13_16:55_myTestProject.zip

    但是如图左边红色文件所示,文件名不全,而且后缀没了↑。

    image.png

    通过 console.log(zipName);,却发现打印正常。 最后归结原因为 第三方包的问题。

    解决: 减少压缩包的名称字符

    改为如下: 年月日_项目名字
    20230214__myTestProject.zip

    2.未提供文件名

    1676207150660_2B76D62B-086C-4341-88CA-9D1F385D5A93.png

    当时找了很久,因为报错信息并没有很好地显示问题。只报了failure。

    之后去看了报错的位置,虽然能知道是2699行的原因, 但是代码已经太底层了,分析不出原因

    image.png

    最后通过对比网上的用法:

    发现是因为:没有指出文件的名称,只指出了文件路径

    如图,红框中的字段当时没有写。写上后报错消失。
    image.png

    教训:了解好参数的含义

    3.http包的选择

    做到钉钉推送的时候,本来想用request包来发送请求

    1676209191129_F2948ED0-0F1D-42e2-A885-0BBAEFAED3C1.png

    然后例如下载如下:

    const request = require('request');
    request('http://www.google.com', function (error, response, body) {
      console.error('error:', error); // Print the error if one occurred
      console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
      console.log('body:', body); // Print the HTML for the Google homepage.
    });

    但是发现: request依赖包被弃用了

    image.png

    官网介绍说,2020年2月11日,npm依赖包就完全被弃用了(即不会有任何新的改变了);

    代替方法找到了几个比较推荐的:

    • Axios,Axios有更多的日/周/月下载量,github上获得更多星;更多的followers和更多的fork,支持在browser上使用,但进度展示仅支持在浏览器上显示
    • Got,有更多的版本,更频繁的更新
    • Node.js标准库里的HTTP 和 HTTPS模块,无需安装外部软件包,但是同样地也会带来弊端就是使用起来并不友好,会繁琐很多。

    第一种用的人比较多,比较推荐,

    但是仅仅为了钉钉推送的话,有个更方便的包 dingtalk-robot-sender:文档

    使用也很方便

    let content = '我就是我, 是不一样的烟火';
    let at = {
       "atMobiles": [
        "156xxxx8827", 
        "189xxxx8325"
      ], 
      "isAtAll": false
    };
    // 快速发送文本消息
    robot.text(content, at);

    将该node.js环境配置成docker镜像,可以参考https://segmentfault.com/a/11...



沪ICP备19023445号-2号
友情链接