前端輕量化部署腳手架實踐

背景

傳統的前端代碼手工部署流程以下:html

手工部署流程

傳統的手工部署須要經歷:前端

  • 1.打包,本地運行npm run build打包生成dist文件夾。
  • 2.ssh鏈接服務器,切換路徑到web對應目錄下。
  • 3.上傳代碼到web目錄,通常經過xshell或者xftp完成。

傳統的手工部署存在如下缺點:vue

  • 1.每次都須要打開xshell軟件與服務器創建鏈接。
  • 2.當負責多個項目且每一個項目都具備測試環境和線上環境時,容易引發部署錯誤。
    (我的以前很是悲劇地遇到過一次,因爲同時負責四個項目,八個環境。一天同時可能修改多個項目,頭暈腦脹,將測試環境代碼部署到線上環境了,欲哭無淚)

全自動化的部署其實能夠採用jenkins實現,jenkins能夠根據gitlab push或者merge事件自動打包代碼到web目錄,能夠參考:node

Jenkins+Docker自動化部署vue項目git

採用jenkins部署是很方便,可是也存在安裝配置麻煩、打包占用服務器資源等缺點。github

因爲咱們的服務器常年高負載運行,曾出現jenkeins打包把服務器打崩的狀況,所以只能逼着博主採用輕量部署的方案來實現自動化部署了(果真技術方案都是被逼出來的,哈哈)。web

1.方案調研

思考:
能不能運行相似npm run deploy一個腳本就直接將咱們的代碼打包、部署到服務器上的web目錄?shell

通過一番調研:發現node-ssharchiver能夠知足咱們的需求。npm

1.1.node-ssh

node-ssh是一個基於ssh2的輕量級npm包,主要用於ssh鏈接服務器、上傳文件、執行命令。json

使用指南:

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'
})
複製代碼
  • 2.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)
  })
複製代碼
  • 3.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連上服務器便可。

適用對象:目前還在採用手工部署又指望快速實現輕量化部署的小團隊或者我的項目,畢竟像阿里這種大公司都有完善的前端部署平臺。

使用指南:github.com/dadaiwei/fe…

3.1.安裝

npm i fe-deploy-cli -g
複製代碼

查看版本,安裝成功

版本

3.2.初始化部署模板

deploy init
複製代碼

初始化部署模板

在當前項目下生成了deploy.config.js

生成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優化了下。

4.1.部署流程優化

部署流程優化爲:

部署腳本優化

流程以下:

  • 1.讀取配置文件,包含服務器host、port、web目錄及本地目錄等信息,生成dev、prod command。
  • 2.本地打包,npm run build生成dist包。
  • 3.鏈接服務器,node-ssh讀取配置鏈接服務器。
  • 4.清空遠端web目錄,使用ssh.execCommand執行cd xxxrm -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同窗的建議,多交流溝通,纔會有更好的idea,才能共同進步。

git地址github.com/dadaiwei/fe… (歡迎star,感謝感謝)

相關文章
相關標籤/搜索