自助终端(办事大厅、医院、政务一体机等)上的业务系统,往往通过本地 HTTP 服务与读卡器、密码键盘等外设交互。真机联调成本高、环境难凑齐时,用 Express 起一个轻量服务,按约定 JSON 返回「开设备、读证、关设备」等结果,就能在浏览器或终端页面里先把流程跑通。本文将介绍如何用 Express 模拟读取二代身份证这一类接口(含
open/getData/close等动作)。
一、场景与目标:为什么要单独起一个服务?
真实环境里,终端侧软件可能通过固定端口访问本机读卡服务;开发阶段没有硬件时,需要行为一致、可重复的假服务:
- 动作分步:先
open(打开/就绪)、再getData(拉取证件照与文本信息)、必要时close,与常见 SDK 调用顺序一致 - 数据形态:返回体里带
Name、IDCode、ImageBase64String等字段,前端或中间层可按真实协议解析 - 技术选型:Express 负责路由与 JSON,几行
res.json就能模拟成功/失败,配合 CORS 便于本地页面直接调
二、项目结构
| 路径 | 说明 |
|---|---|
bin/www | 进程入口:创建 HTTP 服务并监听端口 |
app.js | 配置 CORS、解析大体积 JSON、挂载路由 |
routes/index.js | 浏览器访问根路径时的欢迎页(可选,用于确认服务已启动) |
routes/users.js | 核心:POST /server,按 header.handlerId 模拟读卡器各阶段响应 |
views/、public/ | 页面与静态资源 |
package.json | npm start → node ./bin/www |
三、服务如何启动:bin/www 与 app.js
3.1 入口 bin/www:监听端口
package.json 指定 "start": "node ./bin/www"。这里完成三件事:
require('../app')加载 Express 应用;http.createServer(app)创建 Node HTTP 服务;server.listen(port),本仓库默认 8075(可用环境变量PORT覆盖)。
bin/www 启动核心:
// 引入在 app.js 中配置好的 Express 应用
var app = require('../app');
// debug 命名空间,开发时可通过 DEBUG 环境变量打开监听日志
var debug = require('debug')('express-app:server');
var http = require('http');
// 端口:优先读环境变量 PORT,否则用本仓库默认 8075;normalizePort 在同文件下方定义
var port = normalizePort(process.env.PORT || '8075');
app.set('port', port);
// 用 Node 原生 http 包装 Express,得到可 listen 的 Server 实例
var server = http.createServer(app);
server.listen(port);
server.on('error', onError); // 端口占用、权限等错误
server.on('listening', onListening); // 成功监听后打印 debug 信息
3.2 app.js:中间件顺序(先跨域,再解析,再进读证件路由)
自助终端页面可能跑在不同端口或本地文件页,全局 CORS 让浏览器预检 OPTIONS 能通过;大体积 body 则对应带 Base64 头像等字段的 JSON。顺序仍是:先经过的中间件先执行。
- 全局 CORS:
app.all('*', ...)写响应头;OPTIONS直接200,其它next() - 视图:
jade - 日志与解析:
morgan、express.json、body-parser等,保证POST /server能拿到req.body - 路由:
app.use('/server', usersRouter)—— 所有读证模拟接口都在此前缀下 - 404 / 错误页:未命中路由或异常时统一处理。
**app.js **:
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
// ---------- 全局 CORS:终端页跨端口调用时常见 ----------
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", '*');
res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials","true");
res.header("X-Powered-By",' 3.2.1')
if(req.method === "OPTIONS"){
res.sendStatus(200); // 预检请求快速返回 200,不进入后续业务路由
} else {
next();
}
});
// ---------- 模板:根路径欢迎页 ----------
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
// ---------- 通用中间件:日志、解析 JSON/表单、Cookie、静态资源 ----------
app.use(logger('dev'));
app.use(express.json({limit: '50mb'}));
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json({
limit: '10000kb'
}));
app.use(bodyParser.urlencoded({
limit: '10000kb',
extended: true,
parameterLimit:5000000000,
}));
// ---------- 业务路由:/ 为欢迎页;/server 为读证模拟 ----------
app.use('/', indexRouter);
app.use('/server', usersRouter);
// ---------- 无匹配路由 → 404 ----------
app.use(function(req, res, next) {
next(createError(404));
});
// ---------- 错误页 ----------
app.use(function(err, req, res, next) {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
四、路由层
4.1 routes/index.js:确认服务已启动
GET / 渲染简单欢迎页,便于浏览器打开根地址判断 Express 是否起来。
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
4.2 routes/users.js:模拟自助终端读身份证(POST /server)
URL 怎么拼
app.js 中为 app.use('/server', usersRouter)。子路由文件里写 router.post('/') 表示挂载在 /server 前缀之下,因此终端或前端应请求:
POST /server(示例:http://localhost:8075/server)。
请求体约定(与终端侧协议对齐)
发送 JSON,通过 req.body.header.handlerId 区分步骤,例如:
handlerId(示例) | 含义(模拟) |
|---|---|
open | 打开读卡器 / 就绪 |
getData | 读取身份证芯片信息(姓名、身份证号、照片 Base64 等) |
close | 关闭设备 |
核心代码
- 用一个
POST入口模拟外设服务:getData成功时返回Body.result中的证件字段; - 模块级变量
time(初值 20000)在每次成功读证后递减,用于模拟「前若干次与之后响应不同」等测试需求。
var express = require('express');
var router = express.Router();
// 模块级计数:配合 getData 模拟多次读证后的行为差异(完整见文件后半段)
var time = 20000;
// app.use('/server', usersRouter) → 此处 post('/') 即 POST /server
router.post('/', function(req, res, next) {
console.log('time', time);
console.log('req.header.handlerId', req.body.header.handlerId);
if (req.body.header.handlerId === 'open') {
// 模拟读卡器就绪
res.json({
header: {
serviceId: "IDCardReaderServiceDemo1",
time: "266ms",
code: "100",
msg: "",
handlerId: "open"
},
Body: {
code: "200",
errorMsg: null,
result: null
}
});
} else if (req.body.header.handlerId === 'getData') {
if (time > 0) {
// 模拟二代证读证成功:固定演示数据
res.json({
header: {
serviceId: "IDCardReaderServiceDemo2",
time: "266ms",
code: "000",
msg: "",
handlerId: "getData"
},
Body: {
resCode: "200",
errorMsg: null,
result: {
Name: "黑小虎",
Sex: 1,
Nation: 1,
Birthday: "1900-10-19",
Address: "XXXXXXXXXXXX",
IDCode: "432301195705207528",
Department: "***黑虎山",
StartDate: "2008.03.04",
EndDate: "2018.03.04",
ImageBase64String: "图片base64String",
FingerInfo: null,
NewAddress: null,
SecurityModuleNumber: null,
}
}
});
time--;
} else {
// ... time 耗尽时的响应,以及 close、打印等分支
五、小结
本次只是简单实现,后续还有很多点可以扩展,仅供大家参考。