使用 Docker 和 Node 快速实现一个在线的 QRCode 解码服务

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2018年12月09日
统计字数: 5453字
阅读时间: 11分钟阅读
本文链接: https://soulteary.com/2018/12/09/use-docker-and-node-to-quickly-implement-an-online-qrcode-decoding-service.html


使用 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

最近结束了休假,换了新公司,手头事情比较多,写文章的速度会慢一些,不过没有关系,草稿箱里的东西积累的再多一些,文章的质量会再上一层楼,一起期待一下吧。

目录
相关文章
|
14天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实战指南
【9月更文挑战第6天】在数字化时代的潮流中,后端开发作为支撑现代Web和移动应用的核心,其重要性不言而喻。本文将深入浅出地介绍如何使用Node.js及其流行的框架Express来搭建一个高效、可扩展的后端服务。通过具体的代码示例和实践技巧,我们将探索如何利用这两个强大的工具提升开发效率和应用性能。无论你是后端开发的新手还是希望提高现有项目质量的老手,这篇文章都将为你提供有价值的见解和指导。
|
26天前
|
存储 负载均衡 监控
检索服务elasticsearch节点(Node)
【8月更文挑战第23天】
37 5
|
24天前
|
JavaScript 前端开发 中间件
构建高效后端服务:Node.js与Express框架的完美搭档
【8月更文挑战第28天】在追求高性能、可扩展和易维护的后端开发领域,Node.js和Express框架的组合提供了一种轻量级且灵活的解决方案。本文将深入探讨如何利用这一组合打造高效的后端服务,并通过实际代码示例展示其实现过程。
|
19天前
|
存储 Linux Docker
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
关于如何在CentOS 7.6上安装Docker、介绍Docker存储引擎以及服务进程关系的实战案例。
65 3
CentOS 7.6安装Docker实战案例及存储引擎和服务进程简介
|
25天前
|
安全 Go Docker
Go服务Docker Pod不断重启排查和解决
该文章分享了Go服务在Docker Pod中不断重启的问题排查过程和解决方案,识别出并发写map导致fatal error的问题,并提供了使用sync.Map或concurrent-map库作为并发安全的替代方案。
19 4
|
25天前
|
监控 JavaScript 前端开发
深入浅出Node.js: 打造高效的后端服务
【8月更文挑战第27天】在这个数字化飞速发展的时代,后端技术成为支撑互联网服务的基石。Node.js作为其中的佼佼者,以其非阻塞I/O模型、事件驱动架构和丰富的生态系统,赢得了开发者的青睐。本文将带领读者深入理解Node.js的核心概念,通过实际案例分析其应用模式,并探讨如何利用Node.js构建高效、可扩展的后端服务。无论你是初学者还是有经验的开发者,都能在这篇文章中找到提升你的Node.js技能的灵感和知识。
|
25天前
|
Java Docker Python
启动docker服务需要的三个重要文件
这篇文章介绍了启动Docker服务所需的三个重要文件:Dockerfile、build_image.sh和run.sh。文章提供了Java和Python两个版本的Dockerfile示例,并解释了每个阶段的作用,如基础镜像的选择、构建环境的设置、以及如何通过参数传递环境变量。build_image.sh脚本用于执行Docker镜像的构建、标记和推送过程,而run.sh脚本则用于执行具体的运行命令,包括设置Java参数和执行jar文件。 文章还强调了这些文件应由项目负责人维护,并根据项目需求自行修改启动命令参数。
15 2
|
25天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的深度整合
【8月更文挑战第27天】 在现代Web开发中,后端服务的高效性至关重要。本文深入探讨了如何利用Node.js的非阻塞I/O特性和Express框架的简洁性来打造高性能的后端服务。我们将通过具体案例,展示如何在不牺牲代码可读性和可维护性的前提下,实现高效的请求处理和服务端逻辑。文章旨在为开发者提供一个清晰的指导,帮助他们在构建后端服务时做出更明智的技术选择。
|
1月前
|
存储 缓存 JavaScript
构建高效后端服务:Node.js与Express框架的实战应用
【8月更文挑战第2天】在数字化时代的浪潮中,后端服务的构建成为了软件开发的核心。本文将深入探讨如何利用Node.js和Express框架搭建一个高效、可扩展的后端服务。我们将通过实际代码示例,展示从零开始创建一个RESTful API的全过程,包括路由设置、中间件使用以及数据库连接等关键步骤。此外,文章还将触及性能优化和安全性考量,旨在为读者提供一套完整的后端开发解决方案。让我们一同走进Node.js和Express的世界,探索它们如何助力现代Web应用的开发。
|
2月前
|
Java Serverless Docker
函数计算产品使用问题之使用Docker镜像部署的Web服务如何获取客户端的真实IP
阿里云Serverless 应用引擎(SAE)提供了完整的微服务应用生命周期管理能力,包括应用部署、服务治理、开发运维、资源管理等功能,并通过扩展功能支持多环境管理、API Gateway、事件驱动等高级应用场景,帮助企业快速构建、部署、运维和扩展微服务架构,实现Serverless化的应用部署与运维模式。以下是对SAE产品使用合集的概述,包括应用管理、服务治理、开发运维、资源管理等方面。