01.16 「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

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

原 作 者:笪笪

原文鏈接: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 連接服務器、上傳文件、執行命令。

使用指南:

<code>const node_ssh = require('node-ssh')
const ssh = new node_ssh()/<code>

用到的 api:

  1. ssh.connect:連接服務器
<code>ssh.connect({
host: 'localhost',
username: 'steel',
privateKey: '/home/steel/.ssh/id_rsa'
})/<code>
  1. ssh.putFile:上傳文件
<code>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)
})/<code>
  1. ssh.execCommand:執行遠端服務器命令
<code>ssh.execCommand('hh_client --json', { cwd:'/var/www' }).then(function(result) {
console.log('STDOUT:' + result.stdout)
console.log('STDERR:' + result.stderr)
})/<code>

1.2.archiver

archiver 是一個用於生成存檔的 npm 包,主要用於打包生成 zip、rar 等。

使用指南:

<code>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();/<code>

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

具體代碼:

<code>// 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 {/> 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;/<code>

2. 腳手架實踐

問題
上面的方案已經可以完成一個項目的自動化部署,但是再有一個新的項目要接入自動化部署,是不是又得把整個文件拷貝過去,是不是非常麻煩?

因此可以將自動化部署做成一個腳手架 fe-deploy-cli,支持生成部署配置模板、腳本部署,只需一條命令即可部署到對應環境中。

與腳手架相關的 npm 包:

  • commander:node.js 命令行界面的完整解決方案
  • download-git-repo:git 倉庫代碼下載
  • ora:顯示加載中的效果
  • inquirer:用戶與命令交互的工具
  • child_process:npm 內置模塊,用於執行 package.json 中的打包>

2.1. 初始化

初始化需要在 github 上新建一個部署配置 git 倉庫,執行 deploy init 通過 download-git-repo 從 git 上拉取配置模板。

<code>// 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();
};/<code>

2.2. 設定配置

通過修改 deploy.config.js,設定 dev(測試環境)和 prod(線上環境)的配置。

<code>// deploy.config.js

module.exports = {
privateKey: '', // 本地私鑰地址,位置一般在 C:/Users/xxx/.ssh/id_rsa,非必填,有私鑰則配置
passphrase: '', // 本地私鑰密碼,非必填,有私鑰則配置
projectName: '', // 項目名稱
dev: { // 測試環境
name: '測試環境',
/> host: '', // 測試服務器地址
port: 22, // ssh port,一般默認 22
username: '', // 登錄服務器用戶名
password: '', // 登錄服務器密碼

distPath: 'dist', // 本地打包 dist 目錄
webDir: '', // // 測試環境服務器地址
},
prod: { // 線上環境
name: '線上環境',
/> host: '', // 線上服務器地址
port: 22, // ssh port,一般默認 22
username: '', // 登錄服務器用戶名
password: '', // 登錄服務器密碼
distPath: 'dist', // 本地打包 dist 目錄
webDir: '' // 線上環境 web 目錄
}
// 再還有多餘的環境按照這個格式寫即可
}/<code>

2.3. 註冊部署命令

註冊部署命令就是從 deploy.config.js 中讀取 dev 和 prod 配置,然後通過 program.command 註冊 dev 和 prod command,運行 deploy dev 或者 deploy prod 即進入 1.3 節的部署流程。

<code>// 部署流程
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);
}
});

});
});
}/<code>

3. 使用指南

前提條件:能通過 ssh 連上服務器即可。

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

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

3.1. 安裝

<code>npm i fe-deploy-cli -g/<code>

查看版本,安裝成功:

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

3.2. 初始化部署模板

<code>deploy init/<code>
「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

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

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

3.3. 修改部署配置

部署配置文件位於 deploy 文件夾下的 deploy.config.js, 一般包含 dev(測試環境)和 prod(線上環境)兩個配置,再有多餘的環境配置形式與之類似,只有一個環境的可以刪除另一個多餘的配置(比如只有 prod 線上環境,請刪除 dev 測試環境配置)。

具體配置信息請參考配置文件註釋:

<code>module.exports = {
privateKey: '', // 本地私鑰地址,位置一般在 C:/Users/xxx/.ssh/id_rsa,非必填,有私鑰則配置
passphrase: '', // 本地私鑰密碼,非必填,有私鑰則配置
projectName: 'hivue', // 項目名稱
dev: { // 測試環境
name: '測試環境',
/> 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: '線上環境',
/> host: '10.240.176.99', // 線上服務器地址
port: 22, // ssh port,一般默認 22
username: 'root', // 登錄服務器用戶名
password: '123456', // 登錄服務器密碼

distPath: 'dist', // 本地打包 dist 目錄
webDir: '/var/www/html/prod/hivue' // 線上環境 web 目錄
}
// 再還有多餘的環境按照這個格式寫即可
}/<code>

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

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

<code>deploy --help/<code>

查看部署命令:

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

3.5. 測試環境部署

測試環境部署採用的是 dev 的配置:

<code>deploy dev/<code>

先有一個確認,確認後進入部署流程,腳本自動完成 6 步操作後,恭喜您,部署成功!!!

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

3.5. 線上環境部署

線上環境部署採用的是 prod 的配置:

<code>deploy prod/<code>

部署流程和測試環境部署相同:

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

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 方式:

<code>// 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 {/> 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;/<code>

腳手架初始化、設定配置、註冊部署命令及使用指南與之前版本保持一致。
測試環境和部署環境打包流程打印信息有所變化。

4.2. 測試環境部署

測試環境部署採用的是 dev 的配置。

<code>deploy dev/<code>

去除壓縮 zip 的過程,操作步驟變成 4 步,恭喜您,部署成功!!!

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

4.3. 線上環境部署

線上環境部署採用的是 prod 的配置。

<code>deploy prod/<code>

線上環境部署與測試環境流程相同:

「最佳實踐」手把手教你從零實現一個前端輕量化部署腳手架

結語

以上就是博主關於前端輕量化部署腳手架的一點小實踐,覺得有收穫的可以關注一波,點贊一波,下載一波,使用一波,碼字不易,萬分感謝。

感謝 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


分享到:


相關文章: