专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

「最佳实践」手把手教你从零实现一个前端轻量化部署脚手架

ins518 2024-09-24 18:04:35 技术文章 10 ℃ 0 评论

原 作 者:笪笪

原文链接:https://sourl.cn/tbWpMZ


背景

传统的前端代码手工部署流程如下:

传统的手工部署需要经历:

  1. 打包,本地运行 npm run build 打包生成 dist 文件夹。
  2. ssh 连接服务器,切换路径到 web 对应目录下。
  3. 上传代码到 web 目录,一般通过 xshell 或者 xftp 完成。

传统的手工部署存在以下缺点:

  1. 每次都需要打开 xshell 软件与服务器建立连接。
  2. 当负责多个项目且每个项目都具有测试环境和线上环境时,容易引起部署错误。

个人之前非常悲剧地遇到过一次,由于同时负责四个项目,八个环境。一天同时可能修改多个项目,头晕脑胀,将测试环境代码部署到线上环境了,欲哭无泪。

全自动化的部署其实可以采用 jenkins 实现,jenkins 可以根据 gitlab push 或者 merge 事件自动打包代码到 web 目录,可以参考:

Jenkins+Docker 自动化部署 vue 项目[1]

采用 jenkins 部署是很方便,但是也存在安装配置麻烦、打包占用服务器资源等缺点。

由于我们的服务器常年高负载运行,曾出现 jenkeins 打包把服务器打崩的情况,因此只能逼着博主采用轻量部署的方案来实现自动化部署了(果然技术方案都是被逼出来的,哈哈)。

1. 方案调研

思考:
能不能运行类似 npm run deploy 一个脚本就直接将我们的代码打包、部署到服务器上的 web 目录?

经过一番调研:发现 node-ssh、archiver 可以满足我们的需求。

1.1.node-ssh

node-ssh 是一个基于 ssh2 的轻量级 npm 包,主要用于 ssh 连接服务器、上传文件、执行命令。

使用指南:

const node_ssh = require('node-ssh')
const ssh = new node_ssh()

用到的 api:

  1. ssh.connect:连接服务器
ssh.connect({
  host: 'localhost',
  username: 'steel',
  privateKey: '/home/steel/.ssh/id_rsa'
})
  1. ssh.putFile:上传文件
ssh.putFile('/home/steel/Lab/localPath', '/home/steel/Lab/remotePath').then(function() {
  console.log("The File thing is done")
}, function(error) {
  console.log("Something's wrong")
  console.log(error)
})
  1. ssh.execCommand:执行远端服务器命令
ssh.execCommand('hh_client --json', { cwd:'/var/www' }).then(function(result) {
  console.log('STDOUT:' + result.stdout)
  console.log('STDERR:' + result.stderr)
})

1.2.archiver

archiver 是一个用于生成存档的 npm 包,主要用于打包生成 zip、rar 等。

使用指南:

const archiver = require('archiver');

// 设置压缩类型及级别
const archive = archiver('zip', {
  zlib: { level: 9 },
}).on('error', err => {
  throw err;
});

// 创建文件输出流
const output = fs.createWriteStream(__dirname + '/dist.zip');

// 通过管道方法将输出流存档到文件
archive.pipe(output);

// 从 subdir 子目录追加内容并重命名
archive.directory('subdir/', 'new-subdir');

// 完成打包归档
archive.finalize();

1.3. 部署方案

部署方案设计如下:

流程如下:

  1. 读取配置文件,包含服务器 host、port、web 目录及本地目录等信息
  2. 本地打包,npm run build 生成 dist 包
  3. 打包成 zip,使用 archiver 将 dist 包打包成 dist.zip
  4. 连接服务器,node-ssh 读取配置连接服务器
  5. 上传 zip,使用 ssh.putFile 上传 dist.zip
  6. 解压缩 zip,使用 ssh.execCommand 解压 dist.zip
  7. 删除本地 dist.zip,使用 fs.unlink 删除本地 dist.zip

具体代码:

// deploy.js

const path = require('path');
const fs = require('fs');
const childProcess = require('child_process');
const node_ssh = require('node-ssh');
const archiver = require('archiver');
const { successLog, errorLog, underlineLog } = require('../utils/index');
const projectDir = process.cwd();

let ssh = new node_ssh(); // 生成 ssh 实例

// 部署流程入口
function deploy(config) {
  const { script } = config;
  try {
    console.log(`\n(1)${script}`);
    childProcess.execSync(`${script}`);
    successLog('打包成功');
    startZip(config);
  } catch (err) {
    errorLog(err);
    process.exit(1);
  }
}

// 开始打包
function startZip(config) {
  let { distPath, host } = config;
  distPath = path.resolve(projectDir, distPath);
  console.log('(2)打包成 zip');
  const archive = archiver('zip', {
    zlib: { level: 9 },
  }).on('error', err => {
    throw err;
  });
  const output = fs.createWriteStream(`${projectDir}/dist.zip`).on('close', err => {
    if (err) {
      console.log('关闭 archiver 异常:', err);
      return;
    }
    successLog('zip 打包成功');
    console.log(`(3)连接 ${underlineLog(host)}`);
    uploadFile(config);
  });
  archive.pipe(output);
  archive.directory(distPath, '/');
  archive.finalize();
}

// 上传文件
function uploadFile(config) {
  const { host, port, username, password, privateKey, passphrase, } = config;
  const sshConfig = {
    host,
    port,
    username,
    password,
    privateKey,
    passphrase
  };
  ssh.connect(sshConfig)
    .then(() => {
      successLog(`  SSH 连接成功 `);
      console.log(`(4)上传 zip 至目录 ${underlineLog(config.webDir)}`);
      ssh.putFile(`${projectDir}/dist.zip`, `${config.webDir}/dist.zip`)
        .then(() => {
          successLog(`  zip 包上传成功 `);
          console.log('(5)解压 zip 包');
          statrRemoteShell(config);
        })
        .catch(err => {
          errorLog('文件传输异常', err);
          process.exit(0);
        });
    })
    .catch(err => {
      errorLog('连接失败', err);
      process.exit(0);
    });
}

// 执行 Linux 命令
function runCommand(command, webDir) {
  return new Promise((resolve, reject) => {
    ssh.execCommand(command, { cwd: webDir })
      .then(result => {
        resolve();
        // if (result.stdout) {
        //   successLog(result.stdout);
        // }
        if (result.stderr) {
          errorLog(result.stderr);
          process.exit(1);
        }
      })
      .catch(err => {
        reject(err);
      });
  });
}

// 开始执行远程命令
function statrRemoteShell(config) {
  const { webDir } = config;
  const commands = [`cd ${webDir}`, 'pwd', 'unzip -o dist.zip && rm -f dist.zip'];
  const promises = [];
  for (let i = 0; i < commands.length; i += 1) {
    promises.push(runCommand(commands[i], webDir));
  }
  Promise.all(promises)
    .then(() => {
      successLog('解压成功');
      console.log('(6)开始删除本地 dist.zip');
      deleteLocalZip(config);
    })
    .catch(err => {
      errorLog('文件解压失败', err);
      process.exit(0);
    });
}

// 删除本地 dist.zip 包
function deleteLocalZip(config) {
  const { projectName, name } = config;
  fs.unlink(`${projectDir}/dist.zip`, err => {
    if (err) {
      errorLog('本地 dist.zip 删除失败', err);
    }
    successLog('本地 dist.zip 删除成功 \ n');
    successLog(`\n 恭喜您,${underlineLog(projectName)}项目 ${underlineLog(name)}部署成功了 ^_^\n`);
    process.exit(0);
  });
}

module.exports = deploy;

2. 脚手架实践

问题
上面的方案已经可以完成一个项目的自动化部署,但是再有一个新的项目要接入自动化部署,是不是又得把整个文件拷贝过去,是不是非常麻烦?

因此可以将自动化部署做成一个脚手架 fe-deploy-cli,支持生成部署配置模板、脚本部署,只需一条命令即可部署到对应环境中。

与脚手架相关的 npm 包:

  • commander:node.js 命令行界面的完整解决方案
  • download-git-repo:git 仓库代码下载
  • ora:显示加载中的效果
  • inquirer:用户与命令交互的工具
  • child_process:npm 内置模块,用于执行 package.json 中的打包 script

2.1. 初始化

初始化需要在 github 上新建一个部署配置 git 仓库,执行 deploy init 通过 download-git-repo 从 git 上拉取配置模板。

// init.js

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const download = require('download-git-repo');
const ora = require('ora');
const { successLog, infoLog, errorLog } = require('../utils/index');
let tmp = 'deploy';
const deployPath = path.join(process.cwd(), './deploy');
const deployConfigPath = `${deployPath}/deploy.config.js`;
const deployGit = 'dadaiwei/fe-deploy-cli-template';

// 检查部署目录及部署配置文件是否存在
const checkDeployExists = () => {
    if (fs.existsSync(deployPath) && fs.existsSync(deployConfigPath)) {
        infoLog('deploy 目录下的 deploy.config.js 配置文件已经存在,请勿重新下载');
        process.exit(1);
        return;
    }
    downloadAndGenerate(deployGit);
};

// 下载部署脚本配置
const downloadAndGenerate = templateUrl => {
    const spinner = ora('开始生成部署模板');
    spinner.start();
    download(templateUrl, tmp, { clone: false }, err => {
        if (err) {
            console.log();
            errorLog(err);
            process.exit(1);
        }
        spinner.stop();
        successLog('模板下载成功,模板位置:deploy/deploy.config.js');
        infoLog('请配置 deploy 目录下的 deploy.config.js 配置文件');
        process.exit(0);
    });
};

module.exports = () => {
    checkDeployExists();
};

2.2. 设定配置

通过修改 deploy.config.js,设定 dev(测试环境)和 prod(线上环境)的配置。

// deploy.config.js

module.exports = {
  privateKey: '', // 本地私钥地址,位置一般在 C:/Users/xxx/.ssh/id_rsa,非必填,有私钥则配置
  passphrase: '', // 本地私钥密码,非必填,有私钥则配置
  projectName: '', // 项目名称
  dev: { // 测试环境
    name: '测试环境',
    script: "npm run build", // 测试环境打包脚本
    host: '', // 测试服务器地址
    port: 22, // ssh port,一般默认 22
    username: '', // 登录服务器用户名
    password: '', // 登录服务器密码
    distPath: 'dist',  // 本地打包 dist 目录
    webDir: '',  // // 测试环境服务器地址
  },
  prod: {  // 线上环境
    name: '线上环境',
    script: "npm run build", // 线上环境打包脚本
    host: '', // 线上服务器地址
    port: 22, // ssh port,一般默认 22
    username: '', // 登录服务器用户名
    password: '', // 登录服务器密码
    distPath: 'dist',  // 本地打包 dist 目录
    webDir: '' // 线上环境 web 目录
  }
  // 再还有多余的环境按照这个格式写即可
}

2.3. 注册部署命令

注册部署命令就是从 deploy.config.js 中读取 dev 和 prod 配置,然后通过 program.command 注册 dev 和 prod command,运行 deploy dev 或者 deploy prod 即进入 1.3 节的部署流程。

// 部署流程
function deploy() {
    // 检测部署配置是否合理
    const deployConfigs = checkDeployConfig(deployConfigPath);
    if (!deployConfigs) {
        process.exit(1);
    }

    // 注册部署命令,注册后支持 deploy dev 和 deploy prod
    deployConfigs.forEach(config => {
        const { command, projectName, name } = config;
        program
            .command(`${command}`)
            .description(`${underlineLog(projectName)}项目 ${underlineLog(name)}部署 `)
            .action(() => {
                inquirer.prompt([
                    {
                        type: 'confirm',
                        message: `${underlineLog(projectName)}项目是否部署到 ${underlineLog(name)}?`,
                        name: 'sure'
                    }
                ]).then(answers => {
                    const { sure } = answers;
                    if (!sure) {
                        process.exit(1);
                    }
                    if (sure) {
                        const deploy = require('../lib/deploy');
                        deploy(config);
                    }
                });

            });
    });
}

3. 使用指南

前提条件:能通过 ssh 连上服务器即可。

适用对象:目前还在采用手工部署又期望快速实现轻量化部署的小团队或者个人项目,毕竟像阿里这种大公司都有完善的前端部署平台。

使用指南:fe-deploy-cli/README.md[2]

3.1. 安装

npm i fe-deploy-cli -g

查看版本,安装成功:

3.2. 初始化部署模板

deploy init

在当前项目下生成了 deploy.config.js

3.3. 修改部署配置

部署配置文件位于 deploy 文件夹下的 deploy.config.js, 一般包含 dev(测试环境)和 prod(线上环境)两个配置,再有多余的环境配置形式与之类似,只有一个环境的可以删除另一个多余的配置(比如只有 prod 线上环境,请删除 dev 测试环境配置)。

具体配置信息请参考配置文件注释:

module.exports = {
  privateKey: '', // 本地私钥地址,位置一般在 C:/Users/xxx/.ssh/id_rsa,非必填,有私钥则配置
  passphrase: '', // 本地私钥密码,非必填,有私钥则配置
  projectName: 'hivue', // 项目名称
  dev: { // 测试环境
    name: '测试环境',
    script: "npm run build-dev", // 测试环境打包脚本
    host: '10.240.176.99', // 测试服务器地址
    port: 22, // ssh port,一般默认 22
    username: 'root', // 登录服务器用户名
    password: '123456', // 登录服务器密码
    distPath: 'dist',  // 本地打包 dist 目录
    webDir: '/var/www/html/dev/hivue',  // // 测试环境服务器地址
  },
  prod: {  // 线上环境
    name: '线上环境',
    script: "npm run build", // 线上环境打包脚本
    host: '10.240.176.99', // 线上服务器地址
    port: 22, // ssh port,一般默认 22
    username: 'root', // 登录服务器用户名
    password: '123456', // 登录服务器密码
    distPath: 'dist',  // 本地打包 dist 目录
    webDir: '/var/www/html/prod/hivue' // 线上环境 web 目录
  }
  // 再还有多余的环境按照这个格式写即可
}

3.4. 查看部署命令(该步可省略)

配置好 deploy.config.js,运行:

deploy --help

查看部署命令:

3.5. 测试环境部署

测试环境部署采用的是 dev 的配置:

deploy dev

先有一个确认,确认后进入部署流程,脚本自动完成 6 步操作后,恭喜您,部署成功!!!

3.5. 线上环境部署

线上环境部署采用的是 prod 的配置:

deploy prod

部署流程和测试环境部署相同:

4. 优化(番外篇)

上面已经实现了脚手架自动化部署,评论区看到有一个老哥的评论:

查了下 ssh.putDirectory 支持上传目录,于是针对之前的部署流程和代码进行了优化,去除了 archiver 打包 zip、上传 zip、解压 zip 的过程,部署核心代码 deploy.js 采用 async await 替换 Promise 优化了下。最新版本是 0.08 版本。

4.1. 部署流程优化

部署流程优化为:

流程如下:

  1. 读取配置文件,包含服务器 host、port、web 目录及本地目录等信息,生成 dev、prod command。
  2. 本地打包,npm run build 生成 dist 包。
  3. 连接服务器,node-ssh 读取配置连接服务器。
  4. 清空远端 web 目录,使用 ssh.execCommand 执行 cd xxx 和 rm -rf * 命令。
  5. 上传 dist 目录,使用 ssh.putDirectory 直接上传 dist 到 web 目录。

核心代码之前采用 Promise 写法,优化为采用 async await 方式:

// deploy.js

const path = require('path');
const childProcess = require('child_process');
const node_ssh = require('node-ssh');
const { successLog, errorLog, underlineLog } = require('../utils/index');
const projectDir = process.cwd();

let ssh = new node_ssh(); // 生成 ssh 实例

// 部署流程入口
async function deploy(config) {
  const { script, webDir, distPath, projectName, name } = config;
  execBuild(script);
  await connectSSH(config);
  await clearOldFile(config.webDir);
  await uploadDirectory(distPath, webDir);
  successLog(`\n 恭喜您,${underlineLog(projectName)}项目 ${underlineLog(name)}部署成功了 ^_^\n`);
  process.exit(0);
}

// 第一步,执行打包脚本
function execBuild(script) {
  try {
    console.log(`\n(1)${script}`);
    childProcess.execSync(`${script}`);
    successLog('打包成功');
  } catch (err) {
    errorLog(err);
    process.exit(1);
  }
}

// 第二步,连接 SSH
async function connectSSH(config) {
  const { host, port, username, password, privateKey, passphrase, distPath } = config;
  const sshConfig = {
    host,
    port,
    username,
    password,
    privateKey,
    passphrase
  };
  try {
    console.log(`(2)连接 ${underlineLog(host)}`);
    await ssh.connect(sshConfig);
    successLog('SSH 连接成功');
  } catch (err) {
    errorLog(`  连接失败 ${err}`);
    process.exit(1);
  }
}

// 运行命令
async function runCommand(command, webDir) {
  await ssh.execCommand(command, { cwd: webDir });
}

// 第三步,清空远端目录
async function clearOldFile(webDir) {
  try {
    console.log('(3)清空远端目录');
    await runCommand(`cd ${webDir}`, webDir);
    await runCommand(`rm -rf *`, webDir);
    successLog('远端目录清空成功');
  } catch (err) {
    errorLog(`  远端目录清空失败 ${err}`);
    process.exit(1);
  }
}

// 第四步,上传文件夹
async function uploadDirectory(distPath, webDir) {
  try {
    console.log(`(4)上传文件到 ${underlineLog(webDir)}`);
    await ssh.putDirectory(path.resolve(projectDir, distPath), webDir, {
      recursive: true,
      concurrency: 10,
    });
    successLog('文件上传成功');
  } catch (err) {
    errorLog(`  文件传输异常 ${err}`);
    process.exit(1);
  }
}

module.exports = deploy;

脚手架初始化、设定配置、注册部署命令及使用指南与之前版本保持一致。
测试环境和部署环境打包流程打印信息有所变化。

4.2. 测试环境部署

测试环境部署采用的是 dev 的配置。

deploy dev

去除压缩 zip 的过程,操作步骤变成 4 步,恭喜您,部署成功!!!

4.3. 线上环境部署

线上环境部署采用的是 prod 的配置。

deploy prod

线上环境部署与测试环境流程相同:

结语

以上就是博主关于前端轻量化部署脚手架的一点小实践,觉得有收获的可以关注一波,点赞一波,下载一波,使用一波,码字不易,万分感谢。

感谢 LoneYin[3] 同学的建议,多交流沟通,才会有更好的 idea,才能共同进步。

git 地址fe-deploy-cli[4]

关注我

大家好,这里是 FEHub,每天早上 9 点更新,为你严选优质文章,与你一起进步。

如果喜欢这篇文章,记得点赞,转发。让你的好基友和你一样优秀。

  • 感谢大家的支持
  • 吃饭时加个鸡腿
  • 咱们明天见 ?:)?

欢迎关注 「FEHub」,每天进步一点点

参考资料

[1]

Jenkins+Docker 自动化部署 vue 项目: https://juejin.im/post/5db9474bf265da4d1206777e

[2]

fe-deploy-cli/README.md: https://github.com/dadaiwei/fe-deploy-cli/blob/master/README.md

[3]

LoneYin: https://juejin.im/user/5a069f6b6fb9a044fc4435c7

[4]

fe-deploy-cli: https://github.com/dadaiwei/fe-deploy-cli

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表