Node.js 做一個 NPM 模組更新或安裝的進度條

前陣子因為專案需求,我需要一個在使用 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 的關鍵部分片段:

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 端,這樣我想要的進度條就能做出來了!

下面這就是我稍加修改後的版本,主要是把用不到的部分去除並加入統計與進度等資訊。

#!/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 伺服器來做一個即時線上更新的進度條。

建立專案

首先必須做的就是建立一個專案資料夾。

mkdir update-bar

然後讓 npm 幫你設定 package.json

npm init

這部分你可以直接快速按下 Enter,採用預設設定即可。

最後是安裝我們需要的 Node.js 模組:

npm install express socket.io --save

--save 這個參數會將安裝資訊寫入 package.json 中。

程式撰寫

建立一個新檔案,就叫 index.js 吧!然後將以下程式碼貼上去,這個是我們伺服器的部分。

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檔案的話,應該會發現。

#!/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 標籤所做的進度條。

<!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" >

這檔案比較重要的是 JavaScript 的部分,說明已經在程式碼的註解中了~

執行

如果你是使用 VS Code 作為開發工具的話,你可以直接透過內建的 Debugger 來執行程式,快速鍵是 F5 若不是使用 VS Code,你需要在終端機中輸入這段命令來啟動伺服器:
node index.js

伺服器啟動後,開啟瀏覽器然後在網址列進入:http://localhost:3000/

網頁開啟後約三秒便會看見進度條在跳動,不用懷疑,這真的是正在你的系統上安裝記錄於 package.json 中套件的進度資訊!

Code on GitHub: https://github.com/single9/node-update-bar

獨夜: