Node.js+pm2+ssh2模塊實(shí)現(xiàn)簡(jiǎn)單的自動(dòng)化部署腳本
安裝ssh2和dotenv模塊
首先,我們需要安裝ssh2和dotenv模塊:
npm install ssh2 dotenv --save
然后,我們需要在項(xiàng)目根目錄下創(chuàng)建一個(gè).env文件,用來存放一些敏感的配置信息,例如服務(wù)器的IP地址、端口號(hào)、用戶名、私鑰等。這樣,我們就可以避免將這些信息暴露在代碼中,也方便我們根據(jù)不同的環(huán)境進(jìn)行切換。.env文件的內(nèi)容如下:
HOST=192.168.1.100 SSHPORT=22 USER=root KEYFILE=~/.ssh/id_rsa SSHKEY=" -----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY----- "
const fs = require('fs'); const Client = require('ssh2').Client; require('dotenv').config();
其中,fs模塊是Node.js內(nèi)置的文件系統(tǒng)模塊,用來讀寫文件;Client是ssh2模塊提供的一個(gè)類,用來創(chuàng)建SSH連接;dotenv模塊是用來加載.env文件中的配置信息到process.env對(duì)象中。
定義一些常量存放SSH連接配置
然后,我們需要定義一些常量,用來存放SSH連接配置和本地目錄路徑和遠(yuǎn)程目錄路徑:
// SSH連接配置 const sshConfig = { host: process.env.HOST || '127.0.0.1', port: process.env.SSHPORT || 22, username: process.env.USER || 'root', privateKey: process.env.SSHKEY || fs.readFileSync(process.env.KEYFILE || '/.ssh/id_rsa').toString(), // 這里使用的是通過密鑰登入,使用密碼登入也是可以的,兩種配置項(xiàng)可以并存,其中一個(gè)失敗了ssh2會(huì)則嘗試另一個(gè)方法 }; // 本地目錄路徑和遠(yuǎn)程目錄路徑 const localDir = __dirname; const remoteDir = '/www/wwwroot/img-service';
其中,我們使用了process.env對(duì)象中的屬性來獲取環(huán)境變量的值,如果沒有定義,則使用默認(rèn)值。注意,私鑰需要轉(zhuǎn)換為字符串格式。
創(chuàng)建Client實(shí)例調(diào)用connect方法建立SSH連接
接著,我們需要?jiǎng)?chuàng)建一個(gè)Client實(shí)例,并調(diào)用connect方法來建立SSH連接:
// 創(chuàng)建SSH連接 const conn = new Client(); conn.on('ready', () => { console.log('SSH連接成功'); // ... }).connect(sshConfig); // 監(jiān)聽error事件 conn.on('error', (err) => { console.error('SSH連接失敗', err); }); // 結(jié)束SSH連接 conn.on('end', () => { console.log('SSH連接已斷開'); });
在ready事件的回調(diào)函數(shù)中,我們需要進(jìn)行部署操作。
具體來說,我們需要做兩件事:
一是執(zhí)行npm run build命令來構(gòu)建項(xiàng)目;
二是將構(gòu)建后的文件上傳到遠(yuǎn)程服務(wù)器上。(當(dāng)然,構(gòu)建指令也可以在連接之前進(jìn)行)
// 項(xiàng)目構(gòu)建 const { execSync } = require('child_process'); execSync('npm run build', { stdio: 'inherit' })
execSync 是 Node.js 的一個(gè)內(nèi)置模塊,它可以同步地執(zhí)行一個(gè)子進(jìn)程,并返回子進(jìn)程的輸出。這樣可以避免異步的回調(diào)地獄,也可以保證構(gòu)建的順序和正確性。stdio 參數(shù)是用來控制子進(jìn)程的輸入輸出的,它可以是一個(gè)數(shù)組或一個(gè)字符串。如果是一個(gè)數(shù)組,那么它表示子進(jìn)程的標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤的流。如果是一個(gè)字符串,那么它表示子進(jìn)程的所有流的模式。inherit 表示子進(jìn)程的流和父進(jìn)程的流相同,也就是說,子進(jìn)程的輸出會(huì)顯示在父進(jìn)程的控制臺(tái)中。
使用sftp進(jìn)行文件上傳
歐克,現(xiàn)在我們寫一下將本地目錄下的所有文件上傳至服務(wù)器上指定目錄的代碼,使用sftp進(jìn)行文件上傳:
// 將本地目錄下的所有文件上傳至服務(wù)器上指定目錄 const uploadPromise = []; conn.sftp((err, sftp) => { if (err) throw err; // 待上傳文件or目錄 const files = ['dist', 'package.json', '.env']; const uploadFile = (file) => { return new Promise((resolve, reject) => { try { const localFilePath = localDir + '/' + file; const remoteFilePath = remoteDir + '/' + file; const readStream = fs.createReadStream(localFilePath); const writeStream = sftp.createWriteStream(remoteFilePath); writeStream.on('close', () => { console.log(`文件 ${file} 上傳成功`); resolve(); }); writeStream.on('error', (err) => { console.log(`文件 ${file} 上傳失?。?{err}`); reject(err); }); readStream.pipe(writeStream); } catch (error) { reject(error); } }); }
同時(shí)我們需要有人解析文件目錄,并執(zhí)行我們的上傳指令:
const uploadDir = (files) => { files.forEach((file) => { // 檢查是否存在文件 const isExist = fs.existsSync(file); const stat = fs.lstatSync(file); if (!isExist) { console.log(`文件 ${file} 不存在`); }else if (stat.isDirectory(file)){ const dirFiles = fs.readdirSync(file); uploadDir(dirFiles.map((dirFile) => file + '/' + dirFile)); }else if (stat.isFile(file)){ uploadPromise.push(uploadFile(file)); } }); } uploadDir(files);
最后,還記得我們收集的Promise
數(shù)組嗎?直接用Promise.all
幫我們處理等待全部文件上傳后的回調(diào):
Promise.all(uploadPromise).then(() => { console.log('所有文件上傳成功'); // 執(zhí)行SSH命令 conn.shell((err, stream) => { if (err) throw err; stream.on('close', () => { console.log('遠(yuǎn)程命令執(zhí)行完畢'); conn.end(); }).on('data', (data) => { console.log('遠(yuǎn)程命令輸出:\n' + data); }).stderr.on('data', (data) => { console.log('遠(yuǎn)程命令錯(cuò)誤:\n' + data); }); stream.end('ls -l /www/wwwroot/img-service\npm2 restart img-service\nexit\n'); }); }).catch((err) => { console.log('上傳失?。? + err); });
歐克,最后附上完整代碼
const fs = require('fs'); const Client = require('ssh2').Client; require('dotenv').config(); // 項(xiàng)目構(gòu)建 const { execSync } = require('child_process'); execSync('npm run build', { stdio: 'inherit' }) // SSH連接配置 const sshConfig = { host: process.env.HOST || '127.0.0.1', port: process.env.SSHPORT || 22, username: process.env.USER || 'root', privateKey: process.env.SSHKEY || fs.readFileSync(process.env.KEYFILE || '/.ssh/id_rsa').toString(), }; // 本地目錄路徑和遠(yuǎn)程目錄路徑 const localDir = __dirname; const remoteDir = '/www/wwwroot/img-service'; // 創(chuàng)建SSH連接 const conn = new Client(); // 監(jiān)聽ready事件 conn.on('ready', () => { console.log('SSH連接成功'); // 將本地目錄下的所有文件上傳至服務(wù)器上指定目錄 const uploadPromise = []; conn.sftp((err, sftp) => { if (err) throw err; const files = ['dist', 'package.json', '.env']; const uploadFile = (file) => { return new Promise((resolve, reject) => { try { const localFilePath = localDir + '/' + file; const remoteFilePath = remoteDir + '/' + file; const readStream = fs.createReadStream(localFilePath); const writeStream = sftp.createWriteStream(remoteFilePath); writeStream.on('close', () => { console.log(`文件 ${file} 上傳成功`); resolve(); }); writeStream.on('error', (err) => { console.log(`文件 ${file} 上傳失敗:${err}`); reject(err); }); readStream.pipe(writeStream); } catch (error) { reject(error); } }); } const uploadDir = (files) => { files.forEach((file) => { // 檢查是否存在文件 const isExist = fs.existsSync(file); const stat = fs.lstatSync(file); if (!isExist) { console.log(`文件 ${file} 不存在`); }else if (stat.isDirectory(file)){ const dirFiles = fs.readdirSync(file); uploadDir(dirFiles.map((dirFile) => file + '/' + dirFile)); }else if (stat.isFile(file)){ uploadPromise.push(uploadFile(file)); } }); } uploadDir(files); Promise.all(uploadPromise).then(() => { console.log('所有文件上傳成功'); // 執(zhí)行SSH命令 conn.shell((err, stream) => { if (err) throw err; stream.on('close', () => { console.log('遠(yuǎn)程命令執(zhí)行完畢'); conn.end(); }).on('data', (data) => { console.log('遠(yuǎn)程命令輸出:\n' + data); }).stderr.on('data', (data) => { console.log('遠(yuǎn)程命令錯(cuò)誤:\n' + data); }); stream.end('ls -l /www/wwwroot/img-service\npm2 restart img-service\nexit\n'); }); }).catch((err) => { console.log('上傳失?。? + err); }); }); }).connect(sshConfig); // 監(jiān)聽error事件 conn.on('error', (err) => { console.error('SSH連接失敗', err); }); // 結(jié)束SSH連接 conn.on('end', () => { console.log('SSH連接已斷開'); });
以上就是Node.js+pm2+ssh2模塊實(shí)現(xiàn)簡(jiǎn)單的自動(dòng)化部署腳本的詳細(xì)內(nèi)容,更多關(guān)于Node.js pm2 ssh2自動(dòng)化部署的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解nodejs通過響應(yīng)回寫的方式渲染頁(yè)面資源
本篇文章主要介紹了詳解nodejs通過響應(yīng)回寫的方式渲染頁(yè)面資源,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04基于nodejs res.end和res.send的區(qū)別
今天小編就為大家分享一篇基于nodejs res.end和res.send的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-05-05node制作一個(gè)視頻幀長(zhǎng)圖生成器操作分享
這篇文章主要介紹了node制作一個(gè)視頻幀長(zhǎng)圖生成器操作分享,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08關(guān)于Node.js的events.EventEmitter用法介紹
本篇文章主要介紹了關(guān)于Node.js的events.EventEmitter用法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04Node.js中的events事件模塊知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家整理的是一篇關(guān)于Node.js中的events事件模塊知識(shí)點(diǎn)總結(jié)內(nèi)容,有興趣的朋友們可以跟著學(xué)習(xí)下。2021-12-12通過實(shí)例了解Nodejs模塊系統(tǒng)及require機(jī)制
這篇文章主要介紹了通過實(shí)例了解Nodejs模塊系統(tǒng)及require機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07windows離線環(huán)境安裝node-sass全過程
文章介紹了如何在Windows系統(tǒng)上安裝和配置node-sass,并提供了一個(gè)詳細(xì)的步驟指南,首先,通過命令行查看支持版本;然后,下載對(duì)應(yīng)版本的node-sass安裝包;接著,在npm配置文件中增加SASS_BINARY_PATH路徑配置;最后,執(zhí)行npmi命令完成安裝2024-12-12node.js利用express自動(dòng)搭建項(xiàng)目的全過程
這篇文章主要給大家介紹了關(guān)于node.js利用express自動(dòng)搭建項(xiàng)目的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04