在前端工程化中,前端開發人員終於在不斷的提升本身的地位,不再是簡單的切圖仔了。固然,隨之而來的就是咱們的工做內容變得愈來愈多,變得愈來愈繁瑣。不只要不斷的學習新的前端技術內容,並且還要獨立維護前端的部署工做。所以,如何能快速的進行工程化內容的部署,就是一件很是有價值的事情。
對於前端的部署來講,其實主要就是將編譯後的工程化項目(以vue來講就是將npm run build的dist文件夾)直接部署到對應的服務器的指定目錄下便可,其餘的內容咱們暫時不在此處作過多的講解。html
所以,目前通常會使用SFTP工具(如:FileZilla:https://www.filezilla.cn/,Cyberduck:https://cyberduck.io/)來上傳構建好的前端代碼到服務器。這種方式雖然也是比較快的,可是每次要本身進行打包編譯=》壓縮=》打開對應的工具=》找到對應的路徑,手動的將相關的文件上傳到對應的服務中。而且爲了部署到不一樣的環境中(通常項目開發包括:dev開發環境、test測試環境、uat測試環境、pro測試環境等)還要重複的進行屢次的重複性操做。這種方式不只繁瑣,並且還下降了咱們程序員的身份。因此咱們須要用自動化部署來代替手動 ,這樣一方面能夠提升部署效率,另外一方面開發人員能夠少作一些他們覺得的無用功。前端
話很少說,說幹就幹。首先,咱們先梳理一下通常的部署流程,主要是先將本地的工程化代碼進行打包編譯=>經過ssh登陸到服務器=>將本地編譯的代碼上傳到服務器指定的路徑中,具體流程以下:
所以,經過代碼實現自動化部署腳本,主要須要實現以下,下面以vue項目來爲工程項目來具體講解:vue
3.經過ssh鏈接遠程服務器node
爲了在可以實現以上的幾個步驟,咱們須要引入一些依賴包,以此來進行相關步驟的操做和日誌的打印輸出
大體流程以下:
git
爲了可以將部署相關的內容,如服務器信息,部署的路徑等內容進行統一的管理,以便進行將來的可視化配置。所以,在項目中建立一個獨立的文件夾,統一管理部署相關的文件,而且創建一個config.js和ssh.js文件,分別用於配置相關的部署信息和具體的部署腳本。其中,config.js中的配置具體以下:程序員
const config = { // 開發環境 dev: { host: '', username: 'root', password: '', catalog: '', // 前端文件壓縮目錄 port: 22, // 服務器ssh鏈接端口號 privateKey: null // 私鑰,私鑰與密碼二選一 }, // 測試環境 test: { host: '', // 服務器ip地址或域名 username: 'root', // ssh登陸用戶 password: '', // 密碼 catalog: '', // 前端文件壓縮目錄 port: 22, // 服務器ssh鏈接端口號 privateKey: null // 私鑰,私鑰與密碼二選一 }, // 線上環境 pro: { host: '', // 服務器ip地址或域名 username: 'root', // ssh登陸用戶 password: '', // 密碼,請勿將此密碼上傳至git服務器 catalog: '', // 前端文件壓縮目錄 port: 22, // 服務器ssh鏈接端口號 privateKey: null // 私鑰,私鑰與密碼二選一 } } module.exports = { // publishEnv: pro, publishEnv: config.pro, // 發佈環境 buildDist: 'dist', // 前端文件打包以後的目錄,默認dist buildCommand: 'npm run build', // 打包前端文件的命令 readyTimeout: 20000, // ssh鏈接超時時間 deleteFile: true, // 是否刪除線上上傳的dist壓縮包 isNeedBuild: true // s是否須要打包 }
壓縮打包的內容,使用JSZIP插件,初始化一個const zip = new JSZIP(),而後在按照對應的語法進行具體的實現,其實現過程主要是經過的先遞歸讀取相關的文件,而後加入到zip對象中,最後經過插件進行具體的壓縮,最後寫入到磁盤中。具體代碼以下:
遞歸讀取打包文件github
// 讀取文件 readDir (obj, nowPath) { const files = fs.readdirSync(nowPath) // 讀取目錄中的全部文件及文件夾(同步操做) files.forEach((fileName, index) => { // 遍歷檢測目錄中的文件 // console.log(fileName, index) // 打印當前讀取的文件名 const fillPath = nowPath + '/' + fileName const file = fs.statSync(fillPath) // 獲取一個文件的屬性 if (file.isDirectory()) { // 若是是目錄的話,繼續查詢 const dirlist = zip.folder(fileName) // 壓縮對象中生成該目錄 this.readDir(dirlist, fillPath) // 從新檢索目錄文件 } else { obj.file(fileName, fs.readFileSync(fillPath)) // 壓縮目錄添加文件 } }) }
壓縮文件夾下的全部文件算法
// 壓縮文件夾下的全部文件 zipFile (filePath) { return new Promise((resolve, reject) => { let desc = '*******************************************\n' + '*** 正在壓縮 ***\n' + '*******************************************\n' console.log(chalk.blue(desc)) this.readDir(zip, filePath) zip .generateAsync({ // 設置壓縮格式,開始打包 type: 'nodebuffer', // nodejs用 compression: 'DEFLATE', // 壓縮算法 compressionOptions: { // 壓縮級別 level: 9 } }) .then(content => { fs.writeFileSync( path.join(rootDir, '/', this.fileName), content, 'utf-8' ) desc = '*******************************************\n' + '*** 壓縮成功 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) resolve({ success: true }) }).catch(err => { console.log(chalk.red(err)) reject(err) }) }) }
使用child_process的exec來運行npm run build腳本打包shell
// 打包本地前端文件 buildProject () { return new Promise((resolve, reject) => { exec(Config.buildCommand, async (error, stdout, stderr) => { if (error) { console.error(error) reject(error) } else if (stdout) { resolve({ stdout, success: true }) } else { console.error(stderr) reject(stderr) } }) }) }
經過ssh2的client來建立ssh鏈接npm
// 鏈接服務器 connectServer () { return new Promise((resolve, reject) => { const conn = this.conn conn .on('ready', () => { resolve({ success: true }) }) .on('error', (err) => { reject(err) }) .on('end', () => { const desc = '*******************************************\n' + '*** SSH鏈接已結束 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) }) .on('close', () => { const desc = '*******************************************\n' + '*** SSH鏈接已關閉 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) }) .connect(this.server) }) }
判斷上傳路徑是否存在
// 判斷文件是否存在,若是不存在則進行建立文件夾 await sshCon.execSsh( ` if [[ ! -d ${sshConfig.catalog} ]]; then mkdir -p ${sshConfig.catalog} fi ` )
經過client的sftp講本地的壓縮包上傳到指定的服務器的對應地址
// 上傳文件 uploadFile ({ localPath, remotePath }) { return new Promise((resolve, reject) => { return this.conn.sftp((err, sftp) => { if (err) { reject(err) } else { sftp.fastPut(localPath, remotePath, (err, result) => { if (err) { reject(err) } resolve({ success: true, result }) }) } }) }) }
經過exec來執行ssh命令的
// 執行ssh命令 execSsh (command) { return new Promise((resolve, reject) => { return this.conn.exec(command, (err, stream) => { if (err || !stream) { reject(err) } else { stream .on('close', (code, signal) => { resolve({ success: true }) }) .on('data', function (data) { }) .stderr.on('data', function (data) { resolve({ success: false, error: data.toString() }) }) } }) }) }
解壓上傳的壓縮文件
desc = '*******************************************\n' + '*** 上傳文件成功,開始解壓文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) const zipRes = await sshCon .execSsh( `unzip -o ${sshConfig.catalog + '/' + fileName} -d ${sshConfig.catalog}` ) .catch((e) => {}) if (!zipRes || !zipRes.success) { console.error('----解壓文件失敗,請手動解壓zip文件----') console.error(`----錯誤緣由:${zipRes.error}----`) return false } else if (Config.deleteFile) { desc = '*******************************************\n' + '*** 解壓文件成功,開始刪除上傳的壓縮包 ***\n' + '*******************************************\n' console.log(chalk.green(desc))
刪除對應的壓縮包
desc = '*******************************************\n' + '*** 解壓文件成功,開始刪除上傳的壓縮包 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改 const deleteZipRes = await sshCon .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`) .catch((e) => {}) if (!deleteZipRes || !deleteZipRes.success) { console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----')) console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`)) return false }
封裝關閉的服務器鏈接
// 結束鏈接 endConn () { this.conn.end() if (this.connAgent) { this.connAgent.end() } }
封裝刪除本地的壓縮包
// 刪除本地文件 deleteLocalFile () { return new Promise((resolve, reject) => { fs.unlink(path.join(rootDir, '/', this.fileName), function (error) { if (error) { const desc = '*******************************************\n' + '*** 本地文件刪除失敗 ***\n' + '*******************************************\n' console.log(chalk.yellow(desc)) reject(error) } else { const desc = '*******************************************\n' + '*** 刪除成功 ***\n' + '*******************************************\n' console.log(chalk.blue(desc)) resolve({ success: true }) } }) }) }
在項目的package.json中配置命令
"ssh": "node ./build/ssh.js"
// SSH鏈接,上傳,解壓,刪除等相關操做 async function sshUpload (sshConfig, fileName) { const sshCon = new SSH(sshConfig) const sshRes = await sshCon.connectServer().catch((e) => { console.error(e) }) if (!sshRes || !sshRes.success) { const desc = '*******************************************\n' + '*** ssh鏈接失敗 ***\n' + '*******************************************\n' console.log(chalk.red(desc)) return false } let desc = '*******************************************\n' + '*** 鏈接服務器成功,開始上傳文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) // 判斷文件是否存在,若是不存在則進行建立文件夾 await sshCon.execSsh( ` if [[ ! -d ${sshConfig.catalog} ]]; then mkdir -p ${sshConfig.catalog} fi ` ) const uploadRes = await sshCon .uploadFile({ localPath: path.join(rootDir, '/', fileName), remotePath: sshConfig.catalog + '/' + fileName }) .catch((e) => { console.error(e) }) if (!uploadRes || !uploadRes.success) { console.error('----上傳文件失敗,請從新上傳----') return false } desc = '*******************************************\n' + '*** 上傳文件成功,開始解壓文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) const zipRes = await sshCon .execSsh( `unzip -o ${sshConfig.catalog + '/' + fileName} -d ${sshConfig.catalog}` ) .catch((e) => {}) if (!zipRes || !zipRes.success) { console.error('----解壓文件失敗,請手動解壓zip文件----') console.error(`----錯誤緣由:${zipRes.error}----`) return false } else if (Config.deleteFile) { desc = '*******************************************\n' + '*** 解壓文件成功,開始刪除上傳的壓縮包 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改 const deleteZipRes = await sshCon .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`) .catch((e) => {}) if (!deleteZipRes || !deleteZipRes.success) { console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----')) console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`)) return false } } // 結束ssh鏈接 sshCon.endConn() return true }
// 執行前端部署 ;(async () => { const file = new File() let desc = '*******************************************\n' + '*** 開始編譯 ***\n' + '*******************************************\n' if (Config.isNeedBuild) { console.log(chalk.green(desc)) // 打包文件 const buildRes = await file .buildProject() .catch((e) => { console.error(e) }) if (!buildRes || !buildRes.success) { desc = '*******************************************\n' + '*** 打包出錯,請檢查錯誤 ***\n' + '*******************************************\n' console.log(chalk.red(desc)) return false } console.log(chalk.blue(buildRes.stdout)) desc = '*******************************************\n' + '*** 編譯成功 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) } // 壓縮文件 const res = await file .zipFile(path.join(rootDir, '/', Config.buildDist)) .catch(() => {}) if (!res || !res.success) return false desc = '*******************************************\n' + '*** 開始部署 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) const bol = await sshUpload(Config.publishEnv, file.fileName) if (bol) { desc = '\n******************************************\n' + '*** 部署成功 ***\n' + '******************************************\n' console.log(chalk.green(desc)) file.stopProgress() } else { process.exit(1) } })()
const { exec } = require('child_process') const path = require('path') const JSZIP = require('jszip') const fs = require('fs') const Client = require('ssh2').Client const Config = require('./config.js') const chalk = require('chalk') const zip = new JSZIP() // 前端打包文件的目錄 const rootDir = path.resolve(__dirname, '..') /** * ssh鏈接 */ class SSH { constructor ({ host, port, username, password, privateKey }) { this.server = { host, port, username, password, privateKey } this.conn = new Client() } // 鏈接服務器 connectServer () { return new Promise((resolve, reject) => { const conn = this.conn conn .on('ready', () => { resolve({ success: true }) }) .on('error', (err) => { reject(err) }) .on('end', () => { const desc = '*******************************************\n' + '*** SSH鏈接已結束 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) }) .on('close', () => { const desc = '*******************************************\n' + '*** SSH鏈接已關閉 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) }) .connect(this.server) }) } // 上傳文件 uploadFile ({ localPath, remotePath }) { return new Promise((resolve, reject) => { return this.conn.sftp((err, sftp) => { if (err) { reject(err) } else { sftp.fastPut(localPath, remotePath, (err, result) => { if (err) { reject(err) } resolve({ success: true, result }) }) } }) }) } // 執行ssh命令 execSsh (command) { return new Promise((resolve, reject) => { return this.conn.exec(command, (err, stream) => { if (err || !stream) { reject(err) } else { stream .on('close', (code, signal) => { resolve({ success: true }) }) .on('data', function (data) { }) .stderr.on('data', function (data) { resolve({ success: false, error: data.toString() }) }) } }) }) } // 結束鏈接 endConn () { this.conn.end() if (this.connAgent) { this.connAgent.end() } } } /* * 本地操做 * */ class File { constructor () { this.fileName = this.formateName() } // 刪除本地文件 deleteLocalFile () { return new Promise((resolve, reject) => { fs.unlink(path.join(rootDir, '/', this.fileName), function (error) { if (error) { const desc = '*******************************************\n' + '*** 本地文件刪除失敗 ***\n' + '*******************************************\n' console.log(chalk.yellow(desc)) reject(error) } else { const desc = '*******************************************\n' + '*** 刪除成功 ***\n' + '*******************************************\n' console.log(chalk.blue(desc)) resolve({ success: true }) } }) }) } // 讀取文件 readDir (obj, nowPath) { const files = fs.readdirSync(nowPath) // 讀取目錄中的全部文件及文件夾(同步操做) files.forEach((fileName, index) => { // 遍歷檢測目錄中的文件 // console.log(fileName, index) // 打印當前讀取的文件名 const fillPath = nowPath + '/' + fileName const file = fs.statSync(fillPath) // 獲取一個文件的屬性 if (file.isDirectory()) { // 若是是目錄的話,繼續查詢 const dirlist = zip.folder(fileName) // 壓縮對象中生成該目錄 this.readDir(dirlist, fillPath) // 從新檢索目錄文件 } else { obj.file(fileName, fs.readFileSync(fillPath)) // 壓縮目錄添加文件 } }) } // 壓縮文件夾下的全部文件 zipFile (filePath) { return new Promise((resolve, reject) => { let desc = '*******************************************\n' + '*** 正在壓縮 ***\n' + '*******************************************\n' console.log(chalk.blue(desc)) this.readDir(zip, filePath) zip .generateAsync({ // 設置壓縮格式,開始打包 type: 'nodebuffer', // nodejs用 compression: 'DEFLATE', // 壓縮算法 compressionOptions: { // 壓縮級別 level: 9 } }) .then(content => { fs.writeFileSync( path.join(rootDir, '/', this.fileName), content, 'utf-8' ) desc = '*******************************************\n' + '*** 壓縮成功 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) resolve({ success: true }) }).catch(err => { console.log(chalk.red(err)) reject(err) }) }) } // 打包本地前端文件 buildProject () { return new Promise((resolve, reject) => { exec(Config.buildCommand, async (error, stdout, stderr) => { if (error) { console.error(error) reject(error) } else if (stdout) { resolve({ stdout, success: true }) } else { console.error(stderr) reject(stderr) } }) }) } // 中止程序以前需刪除本地壓縮包文件 stopProgress () { this.deleteLocalFile() .catch((e) => { console.log(chalk.red('----刪除本地文件失敗,請手動刪除----')) console.log(chalk.red(e)) process.exit(1) }) .then(() => { const desc = '*******************************************\n' + '*** 已刪除本地壓縮包文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) process.exitCode = 0 }) } // 格式化命名文件名稱 formateName () { // 壓縮包的名字 const date = new Date() const year = date.getFullYear() const month = date.getMonth() + 1 const day = date.getDate() const timeStr = `${year}_${month}_${day}` return `${Config.buildDist}-${timeStr}-${Math.random() .toString(16) .slice(2)}.zip` } } // SSH鏈接,上傳,解壓,刪除等相關操做 async function sshUpload (sshConfig, fileName) { const sshCon = new SSH(sshConfig) const sshRes = await sshCon.connectServer().catch((e) => { console.error(e) }) if (!sshRes || !sshRes.success) { const desc = '*******************************************\n' + '*** ssh鏈接失敗 ***\n' + '*******************************************\n' console.log(chalk.red(desc)) return false } let desc = '*******************************************\n' + '*** 鏈接服務器成功,開始上傳文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) // 判斷文件是否存在,若是不存在則進行建立文件夾 await sshCon.execSsh( ` if [[ ! -d ${sshConfig.catalog} ]]; then mkdir -p ${sshConfig.catalog} fi ` ) const uploadRes = await sshCon .uploadFile({ localPath: path.join(rootDir, '/', fileName), remotePath: sshConfig.catalog + '/' + fileName }) .catch((e) => { console.error(e) }) if (!uploadRes || !uploadRes.success) { console.error('----上傳文件失敗,請從新上傳----') return false } desc = '*******************************************\n' + '*** 上傳文件成功,開始解壓文件 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) const zipRes = await sshCon .execSsh( `unzip -o ${sshConfig.catalog + '/' + fileName} -d ${sshConfig.catalog}` ) .catch((e) => {}) if (!zipRes || !zipRes.success) { console.error('----解壓文件失敗,請手動解壓zip文件----') console.error(`----錯誤緣由:${zipRes.error}----`) return false } else if (Config.deleteFile) { desc = '*******************************************\n' + '*** 解壓文件成功,開始刪除上傳的壓縮包 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改 const deleteZipRes = await sshCon .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`) .catch((e) => {}) if (!deleteZipRes || !deleteZipRes.success) { console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----')) console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`)) return false } } // 結束ssh鏈接 sshCon.endConn() return true } // 執行前端部署 ;(async () => { const file = new File() let desc = '*******************************************\n' + '*** 開始編譯 ***\n' + '*******************************************\n' if (Config.isNeedBuild) { console.log(chalk.green(desc)) // 打包文件 const buildRes = await file .buildProject() .catch((e) => { console.error(e) }) if (!buildRes || !buildRes.success) { desc = '*******************************************\n' + '*** 打包出錯,請檢查錯誤 ***\n' + '*******************************************\n' console.log(chalk.red(desc)) return false } console.log(chalk.blue(buildRes.stdout)) desc = '*******************************************\n' + '*** 編譯成功 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) } // 壓縮文件 const res = await file .zipFile(path.join(rootDir, '/', Config.buildDist)) .catch(() => {}) if (!res || !res.success) return false desc = '*******************************************\n' + '*** 開始部署 ***\n' + '*******************************************\n' console.log(chalk.green(desc)) const bol = await sshUpload(Config.publishEnv, file.fileName) if (bol) { desc = '\n******************************************\n' + '*** 部署成功 ***\n' + '******************************************\n' console.log(chalk.green(desc)) file.stopProgress() } else { process.exit(1) } })()