前陣子因為專案需求,我需要一個在使用 npm 更新或安裝模組時回推提示的方式。原本的想法是直接透過 Node 呼叫 Shell Script 來做這件事情,等 Shell Script 做完就將視為動作完成。
但這樣其實有缺點,除了回送的無用資訊很多之外,那就是我們無法讓更新的動作被主程式所監視,還有最重要的是,這樣的方式沒有辦法做出進度條!這很重要!更新的 UI 上無法顯示進度條是我無法接受的事!
發現
之後我就一直在找看看 npm 有沒有提供相關的 SDK,結論是,沒有。大多數的工具也都給了很多的無用資訊,應該說是對我想要做的部分而言相當無用的資訊。最後我在 Cloud 9 的 install-sdk.sh 中發現我想要的部分。
install-sdk.sh 這支 Shell Script 會去擷取位於根目錄的 package.json 檔案內容中關於 dependencies 的內容,然後進行 npm install 的逐個安裝,並且將一般的輸出資訊轉到 /dev/null 去。
這不正是我夢寐以求的功能?
以下是這支 Shell Script 的關鍵部分片段:
1 2 3 4 5 6 7 8 9 10 11 12 |
updateNodeModules() { echo "${magenta}--- Running npm install --------------------------------------------${resetColor}" safeInstall(){ # 利用 nodejs runtime 讀取專案資料夾中的 package.json deps=$("$NODE" -e 'console.log(Object.keys(require("./package.json").dependencies).join(" "))'); for m in ${deps[@]}; do echo "$m"; "$NPM" install --loglevel warn "$m" done } "$NPM" install || safeInstall echo "${magenta}--------------------------------------------------------------------${resetColor}" } |
從上面的片段我們可以看出,他是利用 node runtime 去頗析 package.json
資料並輸出成陣列參數格式讓 Shell 接收這些資料,然後執行相應的迴圈函式。這樣的做法能夠讓我們知道我們正在安裝哪一個模組,若稍加更改的話,我們還能知道我們要安裝的模組個數有多少個。當我們知道了要安裝多少個模組後,我們就能夠根據安裝到第幾個的回傳資訊計算出目前的安裝進度,然後將這些資訊推送到 Web 端,這樣我想要的進度條就能做出來了!
下面這就是我稍加修改後的版本,主要是把用不到的部分去除並加入統計與進度等資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#!/bin/sh # Reference: https://github.com/c9/core/blob/master/scripts/install-sdk.sh NPM=npm NODE=node updateNodeModules() { safeInstall(){ total=$("$NODE" -e 'console.log(Object.keys(require("./package.json").dependencies).length)'); deps=$("$NODE" -e 'console.log(Object.keys(require("./package.json").dependencies).join(" "))'); echo "total $total" i=1 for m in ${deps[@]}; do echo "package $i of $total $m" "$NPM" install --loglevel error "$m" &> /dev/null i=$(($i + 1)) done } safeInstall } updateNodeModules |
執行:
看到那數字跳動的感覺真好,不過我們只完成了一小部分而已。接下來我們要來設計一個簡易的 Node.js 伺服器來做一個即時線上更新的進度條。
建立專案
首先必須做的就是建立一個專案資料夾。
1 2 |
mkdir update-bar |
然後讓 npm 幫你設定 package.json
1 2 |
npm init |
這部分你可以直接快速按下 Enter,採用預設設定即可。
最後是安裝我們需要的 Node.js 模組:
1 2 |
npm install express socket.io --save |
--save
這個參數會將安裝資訊寫入package.json
中。
程式撰寫
建立一個新檔案,就叫 index.js
吧!然後將以下程式碼貼上去,這個是我們伺服器的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
const platform = require('os').platform(); const app = require('express')(); const server = require('http').Server(app); const io = require('socket.io')(server); const spawn = require('child_process').spawn; // 檢測作業系統是否為 Windows const isWin = (platform === 'win32') ? true : false; // HTTP 伺服器設定 const HTTP_PORT = 3000; const HOSTNAME = 'localhost'; // 將 html 推至客戶端 app.get('/', (req, res) => { res.sendFile(__dirname + '/views/index.html'); }); // Socket.io 連線成功 io.on('connection', (socket) => { // 接收到 update 事件 socket.on('update', () => { // 執行 update.sh var update = spawn(__dirname + '/update.sh', [], {shell: isWin}); // 接收 update.sh 回傳值 update.stdout.on('data', (data) => { // 將資料轉成 string var str = `${data}`; // 將接收的資料以 JSON 方式頗析 var progress = JSON.parse(str); // 將資料傳至網頁端 socket.emit('progress', progress); }); }); }); // 啟動伺服器 server.listen(HTTP_PORT, HOSTNAME, () => { console.log('Server started. http://%s:%s', HOSTNAME, HTTP_PORT); }); |
然後再新增一個名為 update.sh
的檔案,沒錯!這個是我們稍早提到的那個東西,不過我們在這裡要稍加變更一下,我們將回傳的內容從單純的字串改成了 JSON 的格式,方便伺服器及網頁上的處理。這部分如果你有留意剛才的index.js
檔案的話,應該會發現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#!/bin/bash -e set -e NPM=npm NODE=node updateNodeModules() { safeInstall(){ total=$("$NODE" -e 'console.log(Object.keys(require("./package.json").dependencies).length)'); deps=$("$NODE" -e 'console.log(Object.keys(require("./package.json").dependencies).join(" "))'); i=1 for m in ${deps[@]}; do echo "{\"item\": $i, \"name\": \"$m\", \"total\": $total}" "$NPM" install --loglevel error "$m" &> /dev/null i=$(($i + 1)) done } safeInstall } updateNodeModules |
最後則是我們的網頁的部分,請新建一個資料夾views
並在這資料夾中建立一個新檔案:index.html
。再將以下的程式碼貼至檔案中。
這個檔案是一個 HTML 檔案,內容很簡單,就只是一個利用 HTML5 提供的 progress 標籤所做的進度條。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
<!DOCTYPE html> <html lang="zh_TW"> <head> <title>NPM Update Bar</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="/socket.io/socket.io.js"></script> <style> progress { background-color: #eee; border: none; height: 18px; border-radius: 2px; -webkit-appearance: none; } .progress.bar { width: 100%; height:20px; } .progress.bar .valbox span{ width: 80%; display: block; text-indent: -9999px; } progress::-webkit-progress-bar { background-color: #eee; border-radius: 2px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset; } progress::-webkit-progress-value { background-image: -webkit-linear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, .1) 33%, rgba(0,0, 0, .1) 66%, transparent 66%), -webkit-linear-gradient(top, rgba(255, 255, 255, .25), rgba(0, 0, 0, .25)), -webkit-linear-gradient(left, #44f, #99f); border-radius: 2px; background-size: 35px 20px, 100% 100%, 100% 100%; animation: run 5s linear infinite; -webkit-animation: run 5s linear infinite; } label[data-value]:after { content: attr(data-value) '%'; position: absolute; right:0; } @keyframes run { 0% {background-position: 0px 0px, 0 0, 0 0} 100% {background-position: -100px 0px, 0 0, 0 0} } </style> </head> <body> <!-- 進度條 --> <div class="progress"> <label id="progress-val" class="progress val" for="progress" data-value="">Progress</label> <progress id="progress" class="progress bar" max="100" value="0"></progress> </div> <script> // Socket.io 連接 var socket = io(); var bar = document.getElementById('progress'); var val = document.getElementById('progress-val'); // 監聽進度事件 socket.on('progress', function (data) { // 重設進度條最大值為回傳值之最大值 bar.max = data.total; // 設定進度條目前的值 bar.value = data.item; // 設定 label 的 data-value 值 val.setAttribute('data-value', Math.floor((data.item / data.total) * 100)); }); // 3 秒後送出更新請求 setTimeout(() => { socket.emit('update'); }, 3000); </script> </body> </html> |
這檔案比較重要的是 JavaScript 的部分,說明已經在程式碼的註解中了~
執行
如果你是使用 VS Code 作為開發工具的話,你可以直接透過內建的 Debugger 來執行程式,快速鍵是 F5
。
若不是使用 VS Code,你需要在終端機中輸入這段命令來啟動伺服器:
1 2 |
node index.js |
伺服器啟動後,開啟瀏覽器然後在網址列進入:http://localhost:3000/
網頁開啟後約三秒便會看見進度條在跳動,不用懷疑,這真的是正在你的系統上安裝記錄於 package.json
中套件的進度資訊!
Code on GitHub: https://github.com/single9/node-update-bar