本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或从新修改使用,但须要注明来源。 署名 4.0 国际 (CC BY 4.0)css
本文做者: 苏洋html
建立时间: 2018年12月09日 统计字数: 5453字 阅读时间: 11分钟阅读 本文连接: soulteary.com/2018/12/09/…前端
本文将会介绍如何使用 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
最近结束了休假,换了新公司,手头事情比较多,写文章的速度会慢一些,不过没有关系,草稿箱里的东西积累的再多一些,文章的质量会再上一层楼,一块儿期待一下吧。
我如今有一个小小的折腾群,里面汇集了一些喜欢折腾的小伙伴。
在不发广告的状况下,咱们在里面会一块儿聊聊软件、HomeLab、编程上的一些问题,也会在群里不按期的分享一些技术沙龙的资料。
喜欢折腾的小伙伴欢迎扫码添加好友。(请注明来源和目的,不然不会经过审核) 关于折腾群入群的那些事