(實(shí)戰(zhàn)篇)Vue + Node.js 從 0 到 1 實(shí)現(xiàn)自動(dòng)化部署工具
點(diǎn)擊上方?前端Q,關(guān)注公眾號(hào)
回復(fù)加群,加入前端Q技術(shù)交流群
最近寫了一個(gè)自動(dòng)化部署的 npm 包 zuo-deploy[1],只需點(diǎn)擊一個(gè)按鈕,就可以執(zhí)行服務(wù)器部署腳本,完成功能更新迭代。客戶端使用 Vue + ElementUI,服務(wù) koa + socket + koa-session 等。基礎(chǔ)功能代碼 300 行不到,已開源在 github。zuoxiaobai/zuo-deploy 歡迎 Star、Fork。這里介紹下具體實(shí)現(xiàn)細(xì)節(jié)、思路。
目錄結(jié)構(gòu)
├──?bin?#?命令行工具命令
│???├──?start.js?#?zuodeploy?start?執(zhí)行入口
│???└──?zuodeploy.js?#?zuodeploy?命令入口,在?package.json?的?bin?屬性中配置
├──?docImages?#?README.md?文檔圖片?
├──?frontend?#?客戶端頁面/前端操作頁面(koa-static?靜態(tài)服務(wù)指定目錄)
│???└──?index.html?#?Vue?+?ElementUI?+?axios?+?socket.io
├──?server?#?服務(wù)端
│???├──?utils
│???│???├──?logger.js?#?log4js?
│???│???└──?runCmd.js?#?node?child_process?spawn(執(zhí)行?shell?腳本、pm2?服務(wù)開啟)
│???└──?index.js?#?主服務(wù)(koa?接口、靜態(tài)服務(wù)?+?socket?+?執(zhí)行?shell?腳本)
├──?.eslintrc.cjs?#?eslint?配置文件?+?prettier
├──?args.json?#?用于?pm2?改造后,跨文件傳遞端口、密碼參數(shù)
├──?CHANGELOG.md?#?release?版本功能迭代記錄
├──?deploy-master.sh?#?用于測(cè)試,當(dāng)前目錄開啟服務(wù)偶,點(diǎn)擊部署按鈕,執(zhí)行該腳本
├──?index.js?#?zuodeploy?start?執(zhí)行文件,用于執(zhí)行?pm2?start?server/index.js?主服務(wù)?
├──?package.json?#?項(xiàng)目描述文件,npm?包名、版本號(hào)、cli?命令名稱、
├──?publish.sh?#?npm?publish(npm包)?發(fā)布腳本
└──?README.md?#?使用文檔
復(fù)制代碼
前后端技術(shù)棧、相關(guān)依賴
前端/客戶端 靜態(tài) html + css,非前端工程化,庫都以 cdn 形式引入,通過庫以 UMD 打包方式暴露的全局變量使用 vue3,MVVM 框架,不用操作 dom element-plus,基礎(chǔ)表單樣式統(tǒng)一、美化 axios,請(qǐng)求接口 socket.io,接收實(shí)時(shí)部署 log 服務(wù)端 普通接口,可能需要等完全部署好后,才能拿到結(jié)果 基于 Node.js 技術(shù)棧,無數(shù)據(jù)庫 commander,用于生成的命令 zuodeploy 運(yùn)行時(shí)幫助文檔、提示,zuodeploy start 執(zhí)行入口 prompts,參照 vue-create,引導(dǎo)用戶輸入端口、密碼 koa,http 服務(wù)端, 提供接口、靜態(tài)服務(wù)運(yùn)行容器(類似 nginx、tomcat 等) koa-bodyparser,用于解析 post 請(qǐng)求參數(shù)(login 鑒權(quán)接口需要) koa-router,用于不同接口(路徑,比如 /login, /deploy等)執(zhí)行不同的方法 koa-session,用于接口鑒權(quán),防止他人獲取到部署接口后瘋狂請(qǐng)求部署 koa-static,靜態(tài)服務(wù)器,類似 nginx 啟動(dòng)靜態(tài)服務(wù) socket.io,socket 服務(wù)端,當(dāng) git pull, npm run build 部署時(shí)間較長時(shí),實(shí)時(shí)發(fā)送 log 到前端 log4js,帶時(shí)間戳的 log 輸出 pm2,直接執(zhí)行,當(dāng) terminal 結(jié)束服務(wù)會(huì)被關(guān)掉,用 pm2 以后臺(tái)方式靜默執(zhí)行
基礎(chǔ)功能實(shí)現(xiàn)思路
最初目標(biāo):前端頁面點(diǎn)擊部署按鈕,可以直接讓服務(wù)器執(zhí)行部署,并將部署 log 返回給前端
怎么去實(shí)現(xiàn)?
1.要有一個(gè)前端頁面,給出 部署 按鈕,日志顯示區(qū)域。 2.前端頁面與服務(wù)器交互,必須要有一個(gè)服務(wù)端 server 2.1 提供接口,前端頁面點(diǎn)擊部署,請(qǐng)求該接口,知道什么時(shí)候要執(zhí)行部署, 2.2 后端接口接收到請(qǐng)求后,怎么執(zhí)行部署任務(wù), 2.3 shell 腳本執(zhí)行的 log,怎么搜集并發(fā)送給前端。同上,spawn 支持 log 輸出
技術(shù)棧確定:
1.Vue + ElementUI 基本頁面布局+基本邏輯,axios 請(qǐng)求接口數(shù)據(jù) 2.使用 node 技術(shù)棧來提供 服務(wù)端 server 2.1 使用 koa/koa-router 實(shí)現(xiàn)接口 2.2 部署一般是執(zhí)行 shell 腳本,node 使用內(nèi)置子進(jìn)程 spawn 可以執(zhí)行 shell 腳本文件、跑 terminal 下運(yùn)行的命令操作 2.3 spawn 執(zhí)行時(shí),子進(jìn)程 stdout, stderr 可以獲取到腳本執(zhí)行 log,收集后返回給前端
考慮到前端頁面的部署問題,可以與 koa server 服務(wù)放到一起,使用 koa-static 開啟靜態(tài)文件服務(wù),支持前端頁面訪問
這里不使用前端工程化 @vue/cli ,直接使用靜態(tài) html,通過 cdn 引入 vue 等
1.客戶端 Vue+ElementUI+axios
前端服務(wù)我們放到 frontend/index.html,koa-static 靜態(tài)服務(wù)直接指向 frontend 目錄就可以訪問頁面了

核心代碼如下:
注意:cdn 鏈接都是 // 相對(duì)路徑,需要使用 http 服務(wù)打開頁面,不能以普通的 File 文件形式打開!可以等到后面 koa 寫好后,開啟服務(wù)再訪問
<head>
??<title>zuo-deploytitle>
??
??<link?rel="stylesheet"?href="http://unpkg.com/element-plus/dist/index.css"?/>
??
??<script?src="http://unpkg.com/vue@next">script>
??
??<script?src="http://unpkg.com/element-plus">script>
??<script?src="https://unpkg.com/axios/dist/axios.min.js">script>
head>
<body>
??<div?id="app"?style="margin:0?20px;">
????<el-button?type="primary"?@click="deploy">部署el-button>
????<div>
??????<p>部署日志:p>
??????<div?class="text-log-wrap">
????????<pre>{{?deployLog?}}pre>
??????div>
????div>
??div>
??<script>
????const?app?=?{
??????data()?{
????????return?{
??????????deployLog:?'點(diǎn)擊按鈕進(jìn)行部署',
????????}
??????},
??????methods:?{
????????deploy()?{
??????????this.deployLog?=?'后端部署中,請(qǐng)稍等...'
??????????axios.post('/deploy')
????????????.then((res)?=>?{
??????????????//?部署完成,返回?log
??????????????console.log(res.data);
??????????????this.deployLog?=?res.data.msg
????????????})
????????????.catch(function?(err)?{
??????????????console.log(err);
????????????})
????????}
??????}
????}
????Vue.createApp(app).use(ElementPlus).mount('#app')
??script>
body>
復(fù)制代碼
2.服務(wù)端koa+koa-router+koa-static
koa 開啟 http server,寫 deploy 接口處理。koa-static 開啟靜態(tài)服務(wù)
//?server/index.js
const?Koa?=?require("koa");
const?KoaStatic?=?require("koa-static");
const?KoaRouter?=?require("koa-router");
const?path?=?require("path");
const?app?=?new?Koa();
const?router?=?new?KoaRouter();
router.post("/deploy",?async?(ctx)?=>?{
??//?執(zhí)行部署腳本
??let?execFunc?=?()?=>?{};
??try?{
????let?res?=??await?execFunc();
????ctx.body?=?{
??????code:?0,
??????msg:?res,
????};
??}?catch?(e)?{
????ctx.body?=?{
??????code:?-1,
??????msg:?e.message,
????};
??}
});
app.use(new?KoaStatic(path.resolve(__dirname,?"../frontend")));
app.use(router.routes()).use(router.allowedMethods());
app.listen(7777,?()?=>?console.log(`服務(wù)監(jiān)聽?${7777}?端口`));
復(fù)制代碼
將項(xiàng)目跑起來
在當(dāng)前項(xiàng)目目錄,執(zhí)行 npm init初始化 package.jsonnpm install koa koa-router koa-static --save安裝依賴包node server/index.js運(yùn)行項(xiàng)目,注意如果 7777 端口被占用,需要換一個(gè)端口
訪問 http:// 127.0.0.1:7777 就可以訪問頁面,點(diǎn)擊部署就可以請(qǐng)求成功了

3.Node執(zhí)行shell腳本并輸出log到前端
node 內(nèi)置模塊 child_process 下 spawn 執(zhí)行 terminal 命令,包括執(zhí)行 shell 腳本的 sh 腳本文件.sh 命令
下來看一個(gè) demo,新建一個(gè) testExecShell 測(cè)試目錄,測(cè)試效果
//?testExecShell/runCmd.js
const?{?spawn?}?=?require('child_process');
const?ls?=?spawn('ls',?['-lh',?'/usr']);?//?執(zhí)行?ls?-lh?/usr?命令
ls.stdout.on('data',?(data)?=>?{
??//?ls?產(chǎn)生的?terminal?log?在這里?console
??console.log(`stdout:?${data}`);
});
ls.stderr.on('data',?(data)?=>?{
??//?如果發(fā)生錯(cuò)誤,錯(cuò)誤從這里輸出
??console.error(`stderr:?${data}`);
});
ls.on('close',?(code)?=>?{
??//?執(zhí)行完成后正常退出就是?0?
??console.log(`child?process?exited?with?code?${code}`);
});
復(fù)制代碼
運(yùn)行 node testExecShell/runCmd.js 就可以使用 node 執(zhí)行 ls \-lh /usr,并通過 ls.stdout 接收到 log 信息并打印

回到正題,這里需要執(zhí)行 shell 腳本,可以將 ls \-lh /usr 替換為 sh 腳本文件.sh 即可。下面來試試
//?testExecShell/runShell.js
const?{?spawn?}?=?require('child_process');
const?child?=?spawn('sh',?['testExecShell/deploy.sh']);?//?執(zhí)行?sh?deploy.sh?命令
child.stdout.on('data',?(data)?=>?{
??//?shell?執(zhí)行的?log?在這里搜集,可以通過接口返回給前端
??console.log(`stdout:?${data}`);
});
child.stderr.on('data',?(data)?=>?{
??//?如果發(fā)生錯(cuò)誤,錯(cuò)誤從這里輸出
??console.error(`stderr:?${data}`);
});
child.on('close',?(code)?=>?{
??//?執(zhí)行完成后正常退出就是?0?
??console.log(`child?process?exited?with?code?${code}`);
});
復(fù)制代碼
創(chuàng)建執(zhí)行的 shell 腳本,可以先 sh estExecShell/deploy.sh 試試是否有可執(zhí)行,如果沒執(zhí)行權(quán)限,就添加(chmod +x 文件名)
#?/testExecShell/deploy.sh
echo?'執(zhí)行?pwd'
pwd
echo?'執(zhí)行?git?pull'
git?pull
復(fù)制代碼
運(yùn)行 node testExecShell/runShell.js 就可以讓 node 執(zhí)行 deploy.sh 腳本了,如下圖

參考:child\_process \- Node.js 內(nèi)置模塊筆記[2]
4.deploy接口集成執(zhí)行shell腳本功能
修改之前的 deploy 接口,加一個(gè) runCmd 方法,執(zhí)行當(dāng)前目錄的 deploy.sh 部署腳本,完成后接口將執(zhí)行 log 響應(yīng)給前端
//?新建?server/indexExecShell.js,將?server/index.js?內(nèi)容拷貝進(jìn)來,并做如下修改
const?rumCmd?=?()?=>?{
??return?new?Promise((resolve,?reject)?=>?{
????const?{?spawn?}?=?require('child_process');
????const?child?=?spawn('sh',?['deploy.sh']);?//?執(zhí)行?sh?deploy.sh?命令
????let?msg?=?''
????child.stdout.on('data',?(data)?=>?{
??????//?shell?執(zhí)行的?log?在這里搜集,可以通過接口返回給前端
??????console.log(`stdout:?${data}`);
??????//?普通接口僅能返回一次,需要把?log?都搜集到一次,在?end?時(shí)?返回給前端
??????msg?+=?`${data}`
????});
????child.stdout.on('end',?(data)?=>?{
??????resolve(msg)?//?執(zhí)行完畢后,接口?resolve,返回給前端
????});
????child.stderr.on('data',?(data)?=>?{
??????//?如果發(fā)生錯(cuò)誤,錯(cuò)誤從這里輸出
??????console.error(`stderr:?${data}`);
??????msg?+=?`${data}`
????});
????child.on('close',?(code)?=>?{
??????//?執(zhí)行完成后正常退出就是?0?
??????console.log(`child?process?exited?with?code?${code}`);
????});
??})
}
router.post("/deploy",?async?(ctx)?=>?{
??try?{
????let?res?=??await?rumCmd();?//?執(zhí)行部署腳本
????ctx.body?=?{
??????code:?0,
??????msg:?res,
????};
??}?catch?(e)?{
????ctx.body?=?{
??????code:?-1,
??????msg:?e.message,
????};
??}
});
復(fù)制代碼
修改完成后,運(yùn)行 node server/indexExecShell.js 開啟最新的服務(wù),點(diǎn)擊部署,接口執(zhí)行正常,如下圖

執(zhí)行的是當(dāng)前目錄的 deploy.sh,沒有對(duì)應(yīng)的文件。將上面 testExeclShell/deploy.sh 放到當(dāng)前目錄再點(diǎn)擊部署

這樣自動(dòng)化部署基礎(chǔ)功能基本就完成了。
功能優(yōu)化
1.使用 socket 實(shí)時(shí)輸出 log
上面的例子中,普通接口需要等部署腳本執(zhí)行完成后再響應(yīng)給前端,如果腳本中包含 git pull、npm run build 等耗時(shí)較長的命令,就會(huì)導(dǎo)致前端頁面一直沒log信息,如下圖

測(cè)試 shell
echo?'執(zhí)行?pwd'
pwd
echo?'執(zhí)行?git?pull'
git?pull
git?clone[email protected]:zuoxiaobai/zuo11.com.git?#?耗時(shí)較長的命令
echo?'部署完成'
復(fù)制代碼

這里我們改造下,使用 socket.io[3] 來實(shí)時(shí)將部署 log 發(fā)送給前端
socket.io 分為客戶端、服務(wù)端兩個(gè)部分
客戶端代碼
<script?src="https://cdn.socket.io/4.4.1/socket.io.min.js">script>
<script>
??//?vue?mounted?鉤子里面鏈接?socket?服務(wù)端
??mounted()?{
????this.socket?=?io()?//?鏈接到?socket?服務(wù)器,發(fā)一個(gè)?http?請(qǐng)求,成功后轉(zhuǎn)?101?ws?協(xié)議
????//?訂閱部署日志,拿到日志,就一點(diǎn)點(diǎn)?push?到數(shù)組,顯示到前端
????this.socket.on('deploy-log',?(msg)?=>?{
??????console.log(msg)
??????this.msgList.push(msg)
????})
??},??
script>
復(fù)制代碼
后端 koa 中引入 socket.io 代碼
//?server/indexSoket.js
//?npm?install?socket.io?--save
const?app?=?new?Koa();
const?router?=?new?KoaRouter();
//?開啟?socket?服務(wù)
let?socketList?=?[];
const?server?=?require("http").Server(app.callback());
const?socketIo?=?require("socket.io")(server);
socketIo.on("connection",?(socket)?=>?{
??socketList.push(socket);
??console.log("a?user?connected");?//?前端調(diào)用?io(),即可連接成功
});
//?返回的?socketIo?對(duì)象可以用來給前端廣播消息
runCmd()?{
??//?部分核心代碼
??let?msg?=?''
??child.stdout.on('data',?(data)?=>?{
????//?shell?執(zhí)行的?log?在這里搜集,可以通過接口返回給前端
????console.log(`stdout:?${data}`);
????socketIo.emit('deploy-log',?`${data}`)?//socket?實(shí)時(shí)發(fā)送給前端
????//?普通接口僅能返回一次,需要把?log?都搜集到一次,在?end?時(shí)?返回給前端
????msg?+=?`${data}`
??});
??//?...
??child.stderr.on('data',?(data)?=>?{
????//?如果發(fā)生錯(cuò)誤,錯(cuò)誤從這里輸出
????console.error(`stderr:?${data}`);
????socketIo.emit('deploy-log',?`${data}`)?//?socket?實(shí)時(shí)發(fā)送給前端
????msg?+=?`${data}`
??});
}
//?app.listen?需要改為上面加入了?socket?服務(wù)的?server?對(duì)象
server.listen(7777,?()?=>?console.log(`服務(wù)監(jiān)聽?${7777}?端口`));
復(fù)制代碼
我們?cè)谥暗?demo 中加入上面的代碼,即可完成 socket 改造,node server/indexSocket.js,打開 127.0.0.1:7777/indexSocket.html,點(diǎn)擊部署,即可看到如下效果。完成 demo 訪問地址[4]


相關(guān)問題
關(guān)于 http 轉(zhuǎn) ws 協(xié)議,我們可以通過打開 F12 NetWork 面板看前端的 socket 相關(guān)連接步驟
GET http://127.0.0.1:7777/socket.io/?EIO=4&transport=polling&t=Nz5mBZk獲取 sidPOST http://127.0.0.1:7777/socket.io/?EIO=4&transport=polling&t=Nz5mBaY&sid=DKQAS0fxzXUutg0wAAAGGET http://127.0.0.1:7777/socket.io/?EIO=4&transport=polling&t=Nz5mBav&sid=DKQAS0fxzXUutg0wAAAGws://127.0.0.1:7777/socket.io/?EIO=4&transport=websocket&sid=DKQAS0fxzXUutg0wAAAG
ws 這個(gè)里面可以看到 socket 傳的數(shù)據(jù)

http 請(qǐng)求成功狀態(tài)碼一般是 200, ws Status Code 為 101 Switching Protocols
2.部署接口添加鑒權(quán)
上面只是用接口實(shí)現(xiàn)的功能,并沒有加權(quán)限控制,任何人知道接口地址后,可以通過 postman 請(qǐng)求該接口,觸發(fā)部署。如下圖

為了安全起見,我們這里為接口添加鑒權(quán),前端增加一個(gè)輸入密碼登錄的功能。這里使用 koa-session 來鑒權(quán),只有登錄態(tài)才能請(qǐng)求成功
//?server/indexAuth.js
//?npm?install?koa-session?koa-bodyparser?--save
//?..
const?session?=?require("koa-session");
const?bodyParser?=?require("koa-bodyparser");?//?post?請(qǐng)求參數(shù)解析
const?app?=?new?Koa();
const?router?=?new?KoaRouter();
app.use(bodyParser());?//?處理?post?請(qǐng)求參數(shù)
//?集成?session
app.keys?=?[`自定義安全字符串`];?//?'some?secret?hurr'
const?CONFIG?=?{
??key:?"koa:sess"?/**?(string)?cookie?key?(default?is?koa:sess)?*/,
??/**?(number?||?'session')?maxAge?in?ms?(default?is?1?days)?*/
??/**?'session'?will?result?in?a?cookie?that?expires?when?session/browser?is?closed?*/
??/**?Warning:?If?a?session?cookie?is?stolen,?this?cookie?will?never?expire?*/
??maxAge:?0.5?*?3600?*?1000,?//?0.5h
??overwrite:?true?/**?(boolean)?can?overwrite?or?not?(default?true)?*/,
??httpOnly:?true?/**?(boolean)?httpOnly?or?not?(default?true)?*/,
??signed:?true?/**?(boolean)?signed?or?not?(default?true)?*/,
??rolling:?false?/**?(boolean)?Force?a?session?identifier?cookie?to?be?set?on?every?response.?The?expiration?is?reset?to?the?original?maxAge,?resetting?the?expiration?countdown.?(default?is?false)?*/,
??renew:?false?/**?(boolean)?renew?session?when?session?is?nearly?expired,?so?we?can?always?keep?user?logged?in.?(default?is?false)*/,
};
app.use(session(CONFIG,?app));
router.post("/login",?async?(ctx)?=>?{
??let?code?=?0;
??let?msg?=?"登錄成功";
??let?{?password?}?=?ctx.request.body;
??if?(password?===?`888888`)?{?//?888888?為設(shè)置的密碼
????ctx.session.isLogin?=?true;
??}?else?{
????code?=?-1;
????msg?=?"密碼錯(cuò)誤";
??}
??ctx.body?=?{
????code,
????msg,
??};
});
router.post("/deploy",?async?(ctx)?=>?{
??if?(!ctx.session.isLogin)?{
????ctx.body?=?{
??????code:?-2,
??????msg:?"未登錄",
????};
????return;
??}
??//?有登錄態(tài),執(zhí)行部署
})
復(fù)制代碼
前端相關(guān)改動(dòng),加一個(gè)密碼輸入框、一個(gè)登錄按鈕
<div?class="login-area">
??<div?v-if="!isLogin">
????<el-input?v-model="password"?type="password"?style="width:?200px;">el-input>
????
????<el-button?type="primary"?@click="login">登錄el-button>
??div>
??<div?v-else>已登錄div>
div>
<script>
data()?{
??return?{
????isLogin:?false,
????password:?''
??}
},
methods:?{
??login()?{
????if?(!this.password)?{
??????this.$message.warning('請(qǐng)輸入密碼')
??????return
????}
????axios.post('/login',?{?password:?this.password?})
??????.then((response)?=>?{
????????console.log(response.data);
????????let?{?code,?msg?}?=?response.data
????????if?(code?===?0)?{
??????????this.isLogin?=?true
????????}?else?{
??????????this.$message.error(msg)
????????}
??????})
??????.catch(function?(err)?{
????????console.log(err);
????????this.$message.error(err.message)
??????})
??}
}
script>
復(fù)制代碼
node server/indexAuth.js,打開 127.0.0.1:7777/indexAuth.html,登錄成功之后才能部署


3.封裝成一個(gè)npm包c(diǎn)li工具
為什么封裝成 npm 包,使用命令行工具開啟服務(wù)。主要是簡單易用,如果不使用命令行工具形式,需要三步:
先下載代碼到服務(wù)器 npm install node index.js 或者 pm2 start index.js -n xxx 開啟服務(wù)
改成 npm 包命令行工具形式只需要下面兩步,而且更節(jié)省時(shí)間
npm install zuo-deploy pm2 -g 運(yùn)行 zuodeploy start 會(huì)自動(dòng)使用 pm2 開啟服務(wù)
下面先來看一個(gè)簡單的例子,創(chuàng)建一個(gè) npm 包并上傳到 npm 官方庫步驟
需要有 npm 賬號(hào),如果沒有可以到 www.npmjs.com/[5] 注冊(cè)一個(gè),我的用戶名是 'guoqzuo' 創(chuàng)建一個(gè)文件夾,用于存放 npm 包內(nèi)容,比如 npmPackage 在該目錄下,運(yùn)行 npm init 初始化一個(gè) package.json,輸入的 name 就是 npm 包名,這里我設(shè)置 name 為 'zuoxiaobai-test' 包名有兩種形式,普通包 vue-cli,作用域包 @vue/cli,區(qū)別參見 npm包前面加\@是什么意思\(vue-cli與\@vue/cli的區(qū)別\)[6] 一般默認(rèn)入口為 index.js,暴露出一個(gè)變量、一個(gè)方法
//?index.js
module.exports?=?{
??name:?'寫一個(gè)npm包',
??doSomething()?{
????console.log('這個(gè)npm暴露一個(gè)方法')
??}
}
復(fù)制代碼
這樣就可以直接發(fā)布了,創(chuàng)建一個(gè) publish 腳本,并執(zhí)行(linux 下 chmod +x publish.sh;./publish.sh;)
#?publish.sh
npm?config?set?registry=https://registry.npmjs.org
npm?login?#?登陸?,如果有?OTP,?郵箱會(huì)接收到驗(yàn)證碼,輸入即可
#?登錄成功后,短時(shí)間內(nèi)會(huì)保存狀態(tài),可以直接?npm?pubish
npm?publish?#?可能會(huì)提示名稱已存在,換個(gè)名字,獲取使用作用域包(@xxx/xxx)
npm?config?set?registry=https://registry.npm.taobao.org?#?還原淘寶鏡像
復(fù)制代碼

到 npmjs.org 搜索對(duì)應(yīng)包就可以看到了

使用該 npm 包,創(chuàng)建 testNpm/index.js
const?packageInfo?=?require('zuoxiaobai-test')
console.log(packageInfo)?
packageInfo.doSomething()
復(fù)制代碼
在 testNpm 目錄下 npm init 初始化 package.json,再 npm install zuoxiaobai-test --save; 再 node index.js,執(zhí)行情況如下圖,調(diào)用 npm 包正常

這樣我們就知道怎么寫一個(gè) npm 包,并上傳到 npm 官方庫了。
下面,我們來看怎么在 npm 包中集成 cli 命令。舉個(gè)例子:在 npm install @vue/cli \-g 后,會(huì)在環(huán)境變量中添加一個(gè) vue 命令。使用 vue create xx 可初始化一個(gè)項(xiàng)目。一般這種形式就是 cli 工具。
一般在 package.json 中有一個(gè) bin 屬性,用于創(chuàng)建該 npm 包的自定義命令
//?package.json
"bin":?{
????"zuodeploy":?"./bin/zuodeploy.js"
??},
復(fù)制代碼
上的配置意思是:全局安裝 npm install xx -g 后,生成 zuodeploy 命令,運(yùn)行該命令時(shí),會(huì)執(zhí)行 bin/zuodeploy.js
本地開發(fā)時(shí),配置好后,在當(dāng)前目錄下運(yùn)行 sudo npm link 即可將 zuodeploy 命令鏈接到本地的環(huán)境變量里。任何 terminal 里面運(yùn)行 zuodeploy 都會(huì)執(zhí)行當(dāng)前項(xiàng)目下的這個(gè)文件。解除可以使用 npm unlink
一般 cli 都會(huì)使用 commander 來生成幫助文檔,管理指令邏輯,代碼如下
//?bin/zuodeploy.js
#!/usr/bin/env?node
const?{?program?}?=?require("commander");
const?prompts?=?require("prompts");
program.version(require("../package.json").version);
program
??.command("start")
??.description("開啟部署監(jiān)聽服務(wù)")?//?description?+?action?可防止查找?command拼接文件
??.action(async?()?=>?{
????const?args?=?await?prompts([
??????{
????????type:?"number",
????????name:?"port",
????????initial:?7777,
????????message:?"請(qǐng)指定部署服務(wù)監(jiān)聽端口:",
????????validate:?(value)?=>
??????????value?!==?""?&&?(value?3000?||?value?>?10000)
??????????????`端口號(hào)必須在?3000?-?10000?之間`
????????????:?true,
??????},
??????{
????????type:?"password",
????????name:?"password",
????????initial:?"888888",
????????message:?"請(qǐng)?jiān)O(shè)置登錄密碼(默認(rèn):888888)",
????????validate:?(value)?=>?(value.length?6???`密碼需要?6?位以上`?:?true),
??????},
????]);
????require("./start")(args);?//?args?為?{?port:?7777,?password:?'888888'?}
??});
program.parse();
復(fù)制代碼
使用 commander 可以快速管理、生成幫助文檔,分配具體指令的執(zhí)行邏輯

上面的代碼中,指定了 start 指令,zuodeploy start 執(zhí)行時(shí)會(huì)先通過 prompts 以詢問的方式搜集參數(shù),再執(zhí)行 bin/start.js

在 start.js 中,我么可以將 server/index.js 的代碼全部拷貝過去即可完成 zuodeploy start 開啟服務(wù),點(diǎn)擊部署的功能
4.穩(wěn)定性提高-pm2改造
為了提升穩(wěn)定性,我們可以在 start.js 中以代碼的方式執(zhí)行 pm2 src/index.js 這樣服務(wù)更穩(wěn)定可靠,另外可以再加入 log4js 輸出帶時(shí)間戳的 log,這樣有利于排查問題。
具體代碼參考:zuo-deploy -github[7] 所有測(cè)試 demo 地址: zuo-deploy 實(shí)現(xiàn) demo - fedemo -github[8]
最后
將上面零碎的知識(shí)點(diǎn)匯聚到一起就是 zuo-deploy 的實(shí)現(xiàn),代碼寫的比較隨意,歡迎 star、fork、提改進(jìn) PR!
其他問題
前端/客戶端為什么只有一個(gè) html 沒有使用工程化
前端工程化方式組織代碼比較重,沒必要 這里功能比較簡單、只有部署按鈕、部署 log 查看區(qū)域、鑒權(quán)(輸入密碼)區(qū)域 便于部署,直接 koa-static 開啟靜態(tài)服務(wù)即可訪問,無需打包構(gòu)建
為什么從 type: module 改為普通的 CommonJS
package.json 里面配置 type: module 后默認(rèn)使用 ES Modules,有些 node 方法會(huì)有一些問題
雖然可以通過修改文件后綴為 .cjs 來解決,但文件多了,還不如直接去掉 type: module 使用 node 默認(rèn)包形式
__dirname報(bào)錯(cuò)。__dirname對(duì)于 cli 項(xiàng)目來講非常重要。當(dāng)你需要使用當(dāng)前項(xiàng)目內(nèi)文件,而非 zuodeploy start 執(zhí)行時(shí)所在目錄的文件時(shí),需要使用 __dirnamerequire("../package.json") 改為 import xx from '../package.json' 引入 JSON 文件時(shí)會(huì)出錯(cuò)
關(guān)于本文
作者:做前端的左小白
https://juejin.cn/post/7070921715492061214
聲明:文章著作權(quán)歸作者所有,如有侵權(quán),請(qǐng)聯(lián)系小編刪除。
往期推薦
秒啊!答好這5個(gè)問題,就入門Docker了 你知道如何提升JSON.stringify()的性能嗎? Vue3!煥然一新的 Vue3 中文文檔來了!
最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...
點(diǎn)個(gè)在看支持我吧





