Node.js寫一個前端項目部署腳本

zr-deploy

Web 前端項目部署腳本前端

前言

部署流程:(執行 zr-deploy 後)node

  • 選擇部署環境 配置文件 zr-deploy-config.json
  • 打包:執行配置文件的 打包命令 buildCommand 打包項目
  • 壓縮:打包完成後將文件壓縮 local.distDir -> local.distZip
  • 鏈接服務器:node-ssh 鏈接服務器
  • 上傳代碼:上傳文件到項目目錄(server.distDir
  • server.bakeup
    • true: 備份舊的項目文件
    • false: 刪除舊的項目文件
  • 解壓縮項目文件
  • 部署成功

預覽圖gif
預覽圖png

👉預覽圖掛了的話點這裏git

已發佈 npm,👉zr-deploygithub

源碼 github,👉zr-deployshell

md-note 在這裏👉md-notenpm

工具使用

下載

注意 加 -g/global 下載到全局,否則會提示找不到命令!json

這樣也不用每一個項目加這個依賴,只要進到項目目錄下,添加配置文件後,執行 zr-deploy 就能部署了windows

npm i -g zr-deploy
複製代碼

數組

yarn global add zr-deploy
複製代碼

而後在 項目根目錄 新建配置文件 zr-deploy-config.jsonbash

記住 加到 .gitignore,不要把它上傳到 github 上面了

執行

進入項目目錄

zr-deploy
複製代碼

配置文件

  • local

    • buildCommand: 打包命令
    • distDir: 本地打包輸出的路徑
    • distZip: 壓縮打包文件的文件名
  • server

    • name: 選擇的名字
    • host: 服務器 IP
    • username: 服務器的登陸用戶名
    • password: 對應用戶名的密碼
    • distDir: 項目路徑
    • distZipName: 上傳的壓縮文件名
    • bakeup: 是否備份舊目錄

zr-deploy-config.json 格式以下

[
  {
    "local": {
      "buildCommand": "yarn build",
      "distDir": "./docs",
      "distZip": "./dist.zip"
    },
    "server": {
      "name": "服務器1",
      "host": "1.1.1.1",
      "username": "username",
      "password": "password",
      "distDir": "/var/www/xxx/xxx",
      "distZipName": "dist",
      "bakeup": false
    }
  },
  {
    "local": {
      "buildCommand": "yarn build",
      "distDir": "./docs",
      "distZip": "./dist.zip"
    },
    "server": {
      "name": "服務器2",
      "host": "2.2.2.2",
      "username": "username",
      "password": "password",
      "distDir": "/var/www/xxx/xxx",
      "distZipName": "dist",
      "bakeup": false
    }
  }
]
複製代碼

工具說明

目錄結構

.
├── CHANGE_LOG.md
├── Description.md
├── README.md
├── README_zh.md
├── __test__
│   ├── buildDist.t.js
│   ├── compressDist.t.js
│   ├── getConfig.t.js
│   ├── index.test.js
│   └── zr-deploy-config.json
├── bin
│   └── zr-deploy.js
├── package-lock.json
├── package.json
└── src
    ├── buildDist.js
    ├── compressDist.js
    ├── deploy.js
    ├── getConfig.js
    ├── index.js
    ├── selectEnv.js
    └── utils
        ├── getTime.js
        ├── index.js
        └── textConsole.js
複製代碼

項目打包

// src/buildDist.js
const { spawn } = require('child_process');

const build = spawn(cmd, params, {
  shell: process.platform === 'win32', // 兼容windows系統
  stdio: 'inherit', // 打印命令原始輸出
});
複製代碼

多個項目環境

使用 inquirer,從配置文件中選擇

// src\selectEnv.js
const inquirer = require('inquirer');

/** * 選擇部署環境 * @param {*} CONFIG 配置文件內容 */
function selectEnv(CONFIG) {
  return new Promise(async (resolve, reject) => {
    const select = await inquirer.prompt({
      type: 'list',
      name: '選擇部署的服務器',
      choices: CONFIG.map((item, index) => ({
        name: `${item.server.name}`,
        value: index,
      })),
    });
    const selectServer = CONFIG[Object.values(select)[0]];
    if (selectServer) {
      resolve(selectServer);
    } else {
      reject();
    }
  });
}

module.exports = selectEnv;
複製代碼

壓縮文件

yarn add zip-local
複製代碼

進度工具

yarn add ora
複製代碼

調用 ora 返回值的 succeed/fail 會替換原來的參數值(loading)在終端上顯示

const chalk = require('chalk');
const ora = require('ora');

const spinner = ora(chalk.cyan('正在打包... \n')).start();
spinner.succeed(chalk.green('打包完成!\n'));
spinner.fail(chalk.red('打包失敗!\n'));
複製代碼

util.promisify

node.js 內置函數轉化爲 Promise 形式, promisify 包裝一下,方便使用 async/await,記住要調用一下 next(),至關於 Promise.resolve(),否則是不會走到下一步的

注意:普通函數(非 node.js 內置)使用 promisify,調用 next,不傳參數沒問題,傳參數給 next(arg) 時,會走到 catch 去,跟 手動 new Promise() 對比一下,哪一個方便使用哪一個就是了

const { promisify } = require('util');

async function buildDist(cmd, params, next) {
  // ...
  if (next) next();
}

module.exports = promisify(buildDist);
複製代碼

ssh 鏈接服務器

使用 node-ssh 鏈接服務器

yarn add node-ssh
複製代碼
// src\deploy.js
const node_ssh = require('node-ssh');

const SSH = new node_ssh();

/* =================== 三、鏈接服務器 =================== */
/** * 鏈接服務器 * @param {*} params { host, username, password } */
async function connectServer(params) {
  const spinner = ora(chalk.cyan('正在鏈接服務器...\n')).start();
  await SSH.connect(params)
    .then(() => {
      spinner.succeed(chalk.green('服務器鏈接成功!\n'));
    })
    .catch((err) => {
      spinner.fail(chalk.red('服務器鏈接失敗!\n'));
      textError(err);
      process.exit(1);
    });
}

/** * 經過 ssh 在服務器上命令 * @param {*} cmd shell 命令 * @param {*} cwd 路徑 */
async function runCommand(cmd, cwd) {
  await SSH.execCommand(cmd, {
    cwd,
    onStderr(chunk) {
      textError(`${cmd}, stderrChunk, ${chunk.toString('utf8')}`);
    },
  });
}
複製代碼

部署腳本入口 start

// src\index.js
'use strict';

/** * 前端自動部署項目腳本 */
const { textTitle, textInfo } = require('./utils/textConsole');
const getConfig = require('./getConfig');
const selectEnv = require('./selectEnv');
const buildDist = require('./buildDist');
const compressDist = require('./compressDist');
const deploy = require('./deploy');

/* =================== 0、獲取配置 =================== */

/* =================== 一、選擇部署環境 =================== */

/* =================== 二、項目打包 =================== */

/* =================== 三、項目壓縮 =================== */

/* =================== 四、鏈接服務器 =================== */

/* =================== 五、部署項目 =================== */

async function start() {
  const CONFIG = await selectEnv(getConfig());
  if (!CONFIG) process.exit(1);

  textTitle('======== 自動部署項目 ========');
  textInfo('');

  const [npm, ...script] = CONFIG.local.buildCommand.split(' ');

  // await buildDist('yarn', ['build']);
  await buildDist(npm, [...script]);
  await compressDist(CONFIG.local);
  await deploy(CONFIG.local, CONFIG.server);
  process.exit();
}

module.exports = start;
複製代碼

打包代碼 buildDist

能夠用 child_process.spawn 執行 shell 命令 npm/yarn build

spawn 的格式是 child_process.spawn(command[, args][, options]),以數組的形式傳參

// src\buildDist.js
'use strict';

const { promisify } = require('util');
const { spawn } = require('child_process');
const { textError, textSuccess } = require('./utils/textConsole');

/** * 執行腳本 spawn 的封裝 * @param {*} cmd * @param {*} params */
async function buildDist(cmd, params, next) {
  const build = spawn(cmd, params, {
    shell: process.platform === 'win32', // 兼容windows系統
    stdio: 'inherit', // 打印命令原始輸出
  });

  build.on('error', () => {
    textError(`× [script: ${cmd} ${params}] 打包失敗!\n`);
    process.exit(1);
  });

  build.on('close', (code) => {
    if (code === 0) {
      textSuccess('√ 打包完成!\n');
    } else {
      textError(`× 打包失敗![script: ${cmd} ${params}]\n`);
      process.exit(1);
    }
    // 必傳,promisify 回調繼續執行後續函數
    if (next) next();
  });
}

module.exports = promisify(buildDist);
複製代碼

壓縮文件 compressDist

// src\compressDist.js
'use strict';

const fs = require('fs');
const chalk = require('chalk');
const ora = require('ora');
const zipper = require('zip-local');
const { promisify } = require('util');
const { textError } = require('./utils/textConsole');
const { resolvePath } = require('./utils');

/** * 壓縮打包好的項目 * @param {*} LOCAL_CONFIG 本地配置 * @param {*} next */
function compressDist(LOCAL_CONFIG, next) {
  try {
    const { distDir, distZip } = LOCAL_CONFIG;
    const dist = resolvePath(process.cwd(), distDir);
    if (!fs.existsSync(dist)) {
      textError('× 壓縮失敗');
      textError(`× 打包路徑 [local.distDir] 配置錯誤,${dist} 不存在!\n`);
      process.exit(1);
    }

    const spinner = ora(chalk.cyan('正在壓縮...\n')).start();

    zipper.sync.zip(dist).compress().save(resolvePath(process.cwd(), distZip));

    spinner.succeed(chalk.green('壓縮完成!\n'));
    if (next) next();
  } catch (err) {
    textError('壓縮失敗!', err);
  }
}

module.exports = promisify(compressDist);
複製代碼

鏈接服務器 connectServer

yarn add node-ssh
複製代碼
// src\deploy.js
'use strict';

const { promisify } = require('util');
const ora = require('ora');
const chalk = require('chalk');
const node_ssh = require('node-ssh');
const getTime = require('./utils/getTime');
const { resolvePath } = require('./utils');
const { textError, textInfo } = require('./utils/textConsole');

const SSH = new node_ssh();

/* =================== 三、鏈接服務器 =================== */
/** * 鏈接服務器 * @param {*} params { host, username, password } */
async function connectServer(params) {
  const spinner = ora(chalk.cyan('正在鏈接服務器...\n')).start();
  await SSH.connect(params)
    .then(() => {
      spinner.succeed(chalk.green('服務器鏈接成功!\n'));
    })
    .catch((err) => {
      spinner.fail(chalk.red('服務器鏈接失敗!\n'));
      textError(err);
      process.exit(1);
    });
}

/** * 經過 ssh 在服務器上命令 * @param {*} cmd shell 命令 * @param {*} cwd 路徑 */
async function runCommand(cmd, cwd) {
  await SSH.execCommand(cmd, {
    cwd,
    onStderr(chunk) {
      textError(`${cmd}, stderrChunk, ${chunk.toString('utf8')}`);
    },
  });
}

/* =================== 四、部署項目 =================== */
async function deploy(LOCAL_CONFIG, SERVER_CONFIG, next) {
  // ...
}

module.exports = promisify(deploy);
複製代碼

部署項目 deploy

  • 上傳代碼
  • 配置文件夾權限
  • 備份原來的項目(server.bakeuptrue
  • 刪除原來的項目(server.bakeupfalse
  • 解壓縮上傳的項目壓縮文件
  • 解壓縮完成後,刪除壓縮文件
  • 部署成功
// src\deploy.js
'use strict';

const { promisify } = require('util');
const ora = require('ora');
const chalk = require('chalk');
const node_ssh = require('node-ssh');
const getTime = require('./utils/getTime');
const { resolvePath } = require('./utils');
const { textError, textInfo } = require('./utils/textConsole');

const SSH = new node_ssh();

/* =================== 三、鏈接服務器 =================== */
/** * 鏈接服務器 * @param {*} params { host, username, password } */
async function connectServer(params) {
  // ...
}

/** * 經過 ssh 在服務器上命令 * @param {*} cmd shell 命令 * @param {*} cwd 路徑 */
async function runCommand(cmd, cwd) {
  // ...
}

/* =================== 四、部署項目 =================== */
async function deploy(LOCAL_CONFIG, SERVER_CONFIG, next) {
  const {
    host,
    username,
    password,
    distDir,
    distZipName,
    bakeup,
  } = SERVER_CONFIG;

  if (!distZipName || distDir === '/') {
    textError('請正確配置zr-deploy-config.json!');
    process.exit(1);
  }

  // 鏈接服務器
  await connectServer({ host, username, password });
  // privateKey: '/home/steel/.ssh/id_rsa'

  const spinner = ora(chalk.cyan('正在部署項目...\n')).start();

  try {
    // 上傳壓縮的項目文件
    await SSH.putFile(
      resolvePath(process.cwd(), LOCAL_CONFIG.distZip),
      `${distDir}/${distZipName}.zip`
    );

    if (bakeup) {
      // 備份重命名原項目的文件
      await runCommand(
        `mv ${distZipName} ${distZipName}_${getTime()}`,
        distDir
      );
    } else {
      // 刪除原項目的文件
      await runCommand(`rm -rf ${distZipName}`, distDir);
    }

    // 修改文件權限
    await runCommand(`chmod 777 ${distZipName}.zip`, distDir);

    // 解壓縮上傳的項目文件
    await runCommand(`unzip ./${distZipName}.zip -d ${distZipName}`, distDir);

    // 刪除服務器上的壓縮的項目文件
    await runCommand(`rm -rf ./${distZipName}.zip`, distDir);

    spinner.succeed(chalk.green('部署完成!\n'));
    textInfo(`項目路徑: ${distDir}`);
    textInfo(new Date());
    textInfo('');
    if (next) next();
  } catch (err) {
    spinner.fail(chalk.red('項目部署失敗!\n'));
    textError(`catch: ${err}`);
    process.exit(1);
  }
}

module.exports = promisify(deploy);
複製代碼

大功告成

沒有意外的話,退出進程,而後就部署好了

相關文章
相關標籤/搜索