使用 Docker 和 Node 快速實現一個線上的 QRCode 解碼服務

蘇洋發表於2018-12-09

本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或重新修改使用,但需要註明來源。 署名 4.0 國際 (CC BY 4.0)

本文作者: 蘇洋

建立時間: 2018年12月09日 統計字數: 5453字 閱讀時間: 11分鐘閱讀 本文連結: soulteary.com/2018/12/09/…


使用 Docker 和 Node 快速實現一個線上的 QRCode 解碼服務

本文將會介紹如何使用 Docker、Node、JavaScript、Traefik完成一個簡單的二維碼解析服務,全部程式碼在 300 行以內。

最近折騰文章相關的東西比較多,其中有一個現代化要素其實挺麻煩的,就是二維碼

最終結果預覽

不論是“生成動態、靜態的二維碼”,還是“對已經生成的二維碼進行解析”,其實都不難實現。只是在日常工作中如果只是基於命令列去操作,會很不方便。

所以花了點時間,實現了一個簡單的 QRCode 線上解析工具,在完成這個工具之後,原本需要“開啟終端,定位檔案,執行命令,等待結果”就簡化成了“開啟網頁,CTRL+V 貼上,片刻展示結果”,當然,因為額外提供了介面,所以也可以當一個無狀態服務使用。

實現服務端核心解析邏輯

核心邏輯其實很簡單,虛擬碼三行就差不多了,比如。

const uploadContentByUser = req.body.files;
const decodeContent = decodeImage(uploadContentByUser);
const result = decodeQR.load(decodeContent);
複製程式碼

但是實際使用的情況,出於效能的考慮,我不會過分使用新語法進行程式碼封裝,更傾向儘可能使用“原生”的回撥模式進行非同步程式設計,避免各種“wrapper”造成不必要的損耗。

因為最終的目的是“在瀏覽器裡一個貼上/拖拽操作就完事”。所以我們需要將上面的核心邏輯展開,根據“簡單專案不過度封裝”的思想,程式碼會膨脹為下面三十行左右的樣子。

app.post('/api/decode', multipartMiddleware, function(req, res) {
  let filePath = '';

  try {
    if (req.files.imageFile.path) filePath = req.files.imageFile.path;
  } catch (e) {
    return res.json({code: 500, content: 'request params error.'});
  }

  fs.readFile(filePath, function(errorWhenReadUploadFile, fileBuffer) {
    if (errorWhenReadUploadFile) return res.json({code: 501, content: 'read upload file error.'});
    decodeImage(fileBuffer, function(errorWhenDecodeImage, image) {
      if (errorWhenDecodeImage) return res.json({code: 502, content: errorWhenDecodeImage});
      let decodeQR = new qrcodeReader();
      decodeQR.callback = function(errorWhenDecodeQR, result) {
        if (errorWhenDecodeQR) return res.json({code: 503, content: errorWhenDecodeQR});
        if (!result) return res.json({code: 404, content: 'gone with wind'});
        return res.json({code: 200, content: result.result, points: result.points});
      };
      decodeQR.decode(image.bitmap);
    });
  });
});
複製程式碼

上面的邏輯很簡單,主要做了下面幾件事:

  • 接受使用者上傳的檔案
  • 讀取使用者上傳的檔案
  • 解析使用者上傳的檔案
  • 嘗試將檔案中的資訊解碼並反饋使用者

其中依賴了一個 express 三方的中介軟體 multipartMiddleware,我將主要使用它來進行上傳檔案的請求序列化,原始碼十分簡潔,一百行左右,有興趣可以去瀏覽一下。

它的使用也十分簡單,無需配置,只需要兩行就能發揮作用。

const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();
複製程式碼

當然,為了能夠配合客戶端 JavaScript 完成我們的最終目標,我們需要一些額外的程式碼,比如:提供一個瀏覽器可以瀏覽的頁面。

這裡額外提一點,如果使用類 express 的框架,一般會有一個 static 方法,讓你設定一個靜態檔案目錄,可以免程式設計路由邏輯對一些檔案進行對外訪問,比如這樣:

app.use(express.static(__dirname + '/static', {dotfiles: 'ignore', etag: false, extensions: ['html'], index: false, maxAge: '1h', redirect: false}));
複製程式碼

但是,本例中我其實只需要一個入口頁面就能滿足需求,根本不需要外部資源,比如 vuereactjq各種css框架

這個時候,我推薦直接將要展示的頁面使用 fs API 進行記憶體快取,直接提供使用者即可,比如按照下面的程式碼進行編寫,大概十行就能滿足需求。

const indexCache = fs.readFileSync('./index.html');

app.get('/', function(req, res) {
  res.redirect('/index.html');
});

app.get('/index.html', function(req, res) {
  res.setHeader('charset', 'utf-8');
  res.setHeader('Content-Type', 'text/html');
  res.send(indexCache);
});
複製程式碼

當然,如果你想要和 static 方式的檔案一樣,在除錯過程中,可以“熱更新”檔案的話,需要將這個 indexCache 改寫成一個方法,在攔截使用者請求之後,每次都去動態讀取檔案,或者更高階一些,根據檔案最後編輯時間戳,實現一個簡單的 LRU 快取。

實現客戶端互動邏輯

在實現完畢介面後,我們把欠缺的前端互動邏輯補全。

這裡因為沒有什麼重度的操作,介面也很簡單,所以既不需要 jQ 這類庫,也不需要 VueReact 這類框架,直接寫指令碼就是了。

腦補我需要的介面,上面是一個資料互動的區域,下面是我的互動結果列表,因為頁面也沒幾個元素,所以直接使用指令碼進行元素的建立和操作吧。

let uploadBox = document.createElement('textarea');
uploadBox.id = 'upload';
uploadBox.placeholder = 'Paste Here.';
document.body.appendChild(uploadBox);

let list = document.createElement('ul');
list.id = 'result';
document.body.appendChild(list);
複製程式碼

瀏覽器端核心的操作有三個:

  • 接受使用者的拖拽和貼上圖片的操作
  • 將使用者給予的圖片資料進行上傳
  • 對服務端介面解析的結果進行展示

我們先來實現第一個操作,拖拽、貼上富互動功能,大概三十行程式碼就能解決戰鬥。

function getFirstImage(data, isDrop) {
  let i = 0, item;
  let target = isDrop ?
      data.dataTransfer && data.dataTransfer.files :
      data.clipboardData && data.clipboardData.items;

  if (!target) return false;

  while (i < target.length) {
    item = target[i];
    if (item.type.indexOf('image') !== -1) return item;
    i++;
  }
  return false;
}

function getFilename(event) {
  return event.clipboardData.getData('text/plain').split('\r')[0];
}

uploadBox.addEventListener('paste', function(event) {
  event.preventDefault();
  const image = getFirstImage(event);
  if (image) return uploadFile(image.getAsFile(), getFilename(event) || 'image.png');
});

uploadBox.addEventListener('drop', function(event) {
  event.preventDefault();
  const image = getFirstImage(event, true);
  if (image) return uploadFile(image, event.dataTransfer.files[0].name || 'image.png');
});
複製程式碼

如果你需要支援多張圖片上傳,服務端介面需要做一個簡單的改動,我沒有這個需求,就不做了,有興趣可以實踐下,理論上加兩個迴圈就完事。

接著我們繼續實現上傳功能,因為現代的瀏覽器都支援了 fetch,所以實現起來也很簡單,二十多行解決戰鬥:

function getMimeType(file, filename) {
  if (!file) return console.warn('不支援該檔案型別');
  const mimeType = file.type;
  const extendName = filename.substring(filename.lastIndexOf('.') + 1);
  if (mimeType !== 'image/' + extendName) return 'image/' + extendName;
  return mimeType;
}

function uploadFile(file, filename) {
  let formData = new FormData();
  formData.append('imageFile', file);

  let fileType = getMimeType(file, filename);
  if (!fileType || ['jpg', 'jpeg', 'gif', 'png', 'bmp'].indexOf(fileType) > -1) return console.warn('檔案格式不正確');

  formData.append('mimeType', fileType);

  fetch('/api/decode', {method: 'POST', body: formData}).
      then((response) => response.json()).
      then((data) => {
        if (data.code === 200) return addResult(filename, data.content);
        return addResult(filename, data.content);
      }).
      catch((error) => addResult(filename, error));
}
複製程式碼

最後,寫幾條樣式規則,額外優化一下解析結果展示就完事了,比如能夠更輕鬆的複製解析結果。

list.addEventListener('mouseover', function(e) {
  let target = e.target;
  if (target && target.nodeName) {
    if (target.nodeName.toLowerCase() === 'input') {
      target.select();
    }
  }
});

function result(file, text) {
  let li = document.createElement('li');
  li.innerHTML = '<b>' + file + '</b>' + '<input value="' + text + '">';
  document.getElementById('result').appendChild(li);
}
複製程式碼

將程式容器化

如果你認真閱讀了上面的文章,你會發現,實際的程式只有兩個檔案,一個是服務端的 Node 程式,另外一個則是我們的客戶端頁面,但是實際上,我們還需要一個記錄 Node 依賴的 package.json 以及一個使用者構建容器映象的 Dockerfile,最簡化的目錄結構如下:

.
├── Dockerfile
├── index.html
├── index.js
└── package.json
複製程式碼

考慮實際維護,我們還需要額外建立一些其他的問題,不過都不重要,相關的檔案內容,可以瀏覽我稍後提供的原始碼倉庫。

此刻,當我們執行 node index.js,然後在瀏覽器中開啟 localhost:3000 就能實現文章一開頭我們提到的一鍵貼上完成對二維碼的解析操作了。

不過為了部署的便捷,我們還是需要將程式進行容器化操作。我們來著重瀏覽一下容器構建檔案,同樣很簡單,幾行就足夠我們的使用。

FROM node:11.4.0-alpine
MAINTAINER soulteary <soulteary@gmail.com>

RUN apk update && apk add yarn
WORKDIR /app
COPY .  /app
RUN yarn

ENTRYPOINT [ "node", "index.js" ]
複製程式碼

配合簡單的構建命令:

docker build -t 'docker.soulteary.com/decode-qrcode.soulteary.com:0.0.1' .
複製程式碼

稍等一兩分鐘,就能夠獲得一個可以脫離當前環境,隨處執行的容器映象了。如果你想讓容器執行起來,也只需要一條命令,即可。

docker run -it -p 3000:3000 'docker.soulteary.com/decode-qrcode.soulteary.com:0.0.1'
複製程式碼

如果每次都使用這樣的命令,未免麻煩,我們不妨使用 compose 配合 Traefik 進行服務化。

配合 Traefik 進行服務化操作

配合 compose 和 Traefik 使用起來非常簡單,我之前的文章有提過多次,所以這裡就簡單貼出配置檔案示例:

version: '3'

services:

  decode:
    image: docker.soulteary.com/decode-qrcode.soulteary.com:0.0.1
    expose:
      - 3000
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.port=3000"
      - "traefik.frontend.rule=Host:decode-qrcode.lab.com"
      - "traefik.frontend.entryPoints=http,https"

networks:
  traefik:
    external: true
複製程式碼

然後使用 docker-compose -f compose.yml up -d 即可自動啟動服務,並將服務自動註冊到 Traefik 的服務發現上。

如果需要擴容,scale decode=4 即可,如果還不會操作,可以翻閱之前的文章,進一步學習,: )

最後

附上完整示例程式碼: https://github.com/soulteary/decode-your-qrcode

最近結束了休假,換了新公司,手頭事情比較多,寫文章的速度會慢一些,不過沒有關係,草稿箱裡的東西積累的再多一些,文章的質量會再上一層樓,一起期待一下吧。


我現在有一個小小的折騰群,裡面聚集了一些喜歡折騰的小夥伴。

在不發廣告的情況下,我們在裡面會一起聊聊軟體、HomeLab、程式設計上的一些問題,也會在群裡不定期的分享一些技術沙龍的資料。

喜歡折騰的小夥伴歡迎掃碼新增好友。(請註明來源和目的,否則不會通過稽核) 關於折騰群入群的那些事

關於折騰群入群的那些事

相關文章