本文使用「署名 4.0 國際 (CC BY 4.0)」許可協議,歡迎轉載、或從新修改使用,但須要註明來源。 署名 4.0 國際 (CC BY 4.0)css
本文做者: 蘇洋html
建立時間: 2018年12月09日
統計字數: 5453字
閱讀時間: 11分鐘閱讀
本文連接: https://soulteary.com/2018/12...前端
本文將會介紹如何使用 Docker、Node、JavaScript、Traefik完成一個簡單的二維碼解析服務,所有代碼在 300 行之內。vue
最近折騰文章相關的東西比較多,其中有一個現代化要素其實挺麻煩的,就是二維碼。node
不管是「生成動態、靜態的二維碼」,仍是「對已經生成的二維碼進行解析」,其實都不難實現。只是在平常工做中若是隻是基於命令行去操做,會很不方便。react
因此花了點時間,實現了一個簡單的 QRCode 在線解析工具,在完成這個工具以後,本來須要「打開終端,定位文件,執行命令,等待結果」就簡化成了「打開網頁,CTRL+V
粘貼,片刻展現結果」,固然,由於額外提供了接口,因此也能夠當一個無狀態服務使用。git
核心邏輯其實很簡單,僞代碼三行就差很少了,好比。github
const uploadContentByUser = req.body.files; const decodeContent = decodeImage(uploadContentByUser); const result = decodeQR.load(decodeContent);
可是實際使用的狀況,出於性能的考慮,我不會過度使用新語法進行代碼封裝,更傾向儘量使用「原生」的回調模式進行異步編程,避免各類「wrapper」形成沒必要要的損耗。docker
由於最終的目的是「在瀏覽器裏一個粘貼/拖拽操做就完事」。因此咱們須要將上面的核心邏輯展開,根據「簡單項目不過分封裝」的思想,代碼會膨脹爲下面三十行左右的樣子。express
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}));
可是,本例中我其實只須要一個入口頁面就能知足需求,根本不須要外部資源,好比 vue
、react
、jq
、各類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
這類庫,也不須要 Vue
、React
這類框架,直接寫腳本就是了。
腦補我須要的界面,上面是一個數據交互的區域,下面是個人交互結果列表,由於頁面也沒幾個元素,因此直接使用腳本進行元素的建立和操做吧。
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
進行服務化。
配合 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
最近結束了休假,換了新公司,手頭事情比較多,寫文章的速度會慢一些,不過沒有關係,草稿箱裏的東西積累的再多一些,文章的質量會再上一層樓,一塊兒期待一下吧。