在前端工程化中,前端開發人員終於在不斷的提升本身的地位,不再是簡單的切圖仔了。固然,隨之而來的就是咱們的工做內容變得愈來愈多,變得愈來愈繁瑣。不只要不斷的學習新的前端技術內容,並且還要獨立維護前端的部署工做。所以,如何能快速的進行工程化內容的部署,就是一件很是有價值的事情。html
對於前端的部署來講,其實主要就是將編譯後的工程化項目(以vue來講就是將npm run build的dist文件夾)直接部署到對應的服務器的指定目錄下便可,其餘的內容咱們暫時不在此處作過多的講解。前端
所以,目前通常會使用SFTP工具(如:FileZilla:www.filezilla.cn/,Cyberduck:cyberduck.io/)來上傳構建好的前端代碼到服務器。這種方式雖然也是比較快的,可是每次要本身進行打包編譯=》壓縮=》打開對應的工具=》找到對應的路徑,手動的將相關的文件上傳到對應的服務中。而且爲了部署到不一樣的環境中(通常項目開發包括:dev開發環境、test測試環境、uat測試環境、pro測試環境等)還要重複的進行屢次的重複性操做。這種方式不只繁瑣,並且還下降了咱們程序員的身份。因此咱們須要用自動化部署來代替手動 ,這樣一方面能夠提升部署效率,另外一方面開發人員能夠少作一些他們覺得的無用功。vue
話很少說,說幹就幹。首先,咱們先梳理一下通常的部署流程,主要是先將本地的工程化代碼進行打包編譯=>經過ssh登陸到服務器=>將本地編譯的代碼上傳到服務器指定的路徑中,具體流程以下: 所以,經過代碼實現自動化部署腳本,主要須要實現以下,下面以vue項目來爲工程項目來具體講解:node
3.經過ssh鏈接遠程服務器 3. 檢查對應的遠程部署路徑是否存在,不存在須要建立 4. 上傳本地的壓縮包 5. 解壓上傳的壓縮包 6. 刪除上傳的壓縮包 7. 關閉ssh鏈接 8. 刪除本地的壓縮包git
爲了在可以實現以上的幾個步驟,咱們須要引入一些依賴包,以此來進行相關步驟的操做和日誌的打印輸出程序員
大體流程以下: github
爲了可以將部署相關的內容,如服務器信息,部署的路徑等內容進行統一的管理,以便進行將來的可視化配置。所以,在項目中建立一個獨立的文件夾,統一管理部署相關的文件,而且創建一個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對象中,最後經過插件進行具體的壓縮,最後寫入到磁盤中。具體代碼以下: 遞歸讀取打包文件shell
// 讀取文件
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)) // 壓縮目錄添加文件
}
})
}
複製代碼
壓縮文件夾下的全部文件npm
// 壓縮文件夾下的全部文件
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腳本打包
// 打包本地前端文件
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鏈接
// 鏈接服務器
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)
}
})()
複製代碼