开发
Fetch EXIF升级了
减少查询次数和流量,优化缓存方式,支持Docker部署
学习Node.js后,可使用MongoDB和Node.js实现修改摄影作品的EXIF和GPS信息查询功能,优化了性能和可部署性。重构了数据库模型和获取信息函数,通过docker部署简化项目运行。运行整个接口只需使用docker-compose.yml即可。
- #Node.js
- #MongoDB
- #Express
- #API
- #CRUD
- #Docker
- #Database
407
最近在学Node,准备写个后台。刚好看完如何CRUD,决定给这个博客利用到的一个小项目做个升级。
之前的问题
如果你随便点开本站的摄影作品,会发现在每张图下面会显示拍摄数据。
Fetch EXIF的功能很简单,接收一个图片的URL,返回图片的拍摄信息。
关于这个功能之前写过文章:利用ChatGPT实现一个Node.js API。
但这个基本由ChatGPT完成的API有一些问题,当时我急于完成功能,没有考虑性能。用了这么久,虽然并没有真正遇到性能瓶颈,但一直想优化一下。
之前的API有两个性能问题。
第一个是EXIF和GPS的提取。此前我将这两个查询分成了两个部分,但实际上GPS是EXIF中的一部分。既然已经有了缓存,其实可以将这两部分查询合为一个,分别返回不同的信息即可。
这样做的话,每个页面可以节省一次查询。(因为一个摄影作品只会读一次GPS,EXIF是每张图都有)。
第二个是缓存的方式。当初我采取了ChatGPT的建议,将查询的数据通过node-cache
存入内存。照理说就算有上万张图片,那点信息存内存也是够的。但毕竟不是持久方案,程序一旦重启,图片就得重新查询、缓存。如果能将数据存入数据库,应该就能节省很多下载图片的流量。(响应速度其实没区别)
重构
于是重构了这个项目。首先定义了MongoDB的Schema:
typescript
const mongoose = require('mongoose');
const imageSchema = new mongoose.Schema({
url: { type: String, required: true, unique: true },
exif: {
Maker: { type: String, default: 'unknown' },
Model: { type: String, default: 'unknown' },
ExposureTime: { type: String, default: 'unknown' },
FNumber: { type: String, default: 'unknown' },
iso: { type: String, default: 'unknown' },
FocalLength: { type: String, default: 'unknown' },
LensModel: { type: String, default: 'unknown' }
},
gps: {
latitude: { type: Number, required: true, default: 0 },
longitude: { type: Number, required: true, default: 0 }
},
createdAt: { type: Date, expires: '30d', default: Date.now }
})
module.exports = {
Raw: mongoose.model('Raw', imageSchema)
}
然后再重构获取拍摄信息的函数,将EXIF和GPS信息一次查询并存入:
typescript
const exifr = require("exifr");
const { Raw } = require("./database");
//将快门数据转换成更容易理解的格式
function formatShutterTime(shutterTime) {
if (!shutterTime) return "0";
const time = parseFloat(shutterTime);
if (time >= 1) {
return time.toFixed(2);
}
const fraction = Math.round(1 / time);
return `1/${fraction}`;
}
//将axios改为直接使用fetch(),查询到数据后存入数据库
async function getRaw(url) {
try {
const response = await fetch(url);
const data = new Uint8Array(await response.arrayBuffer());
const rawData = await exifr.parse(data);
const image = new Raw({
url,
exif: {
Maker: rawData.Make || "unknown",
Model: rawData.Model || "unknown",
ExposureTime: formatShutterTime(rawData.ExposureTime),
FNumber: rawData.FNumber || "unknown",
iso: rawData.ISO || "unknown",
FocalLength: rawData.FocalLength || "unknown",
LensModel: rawData.LensModel || "unknown",
},
gps: {
latitude: rawData.latitude,
longitude: rawData.longitude,
},
});
await image.save();
return image;
} catch (err) {
throw err;
}
}
module.exports = getRaw;
最后在主入口函数,稍微修改之前的逻辑,先从数据库中查找,再根据请求返回EXIF或GPS信息:
typescript
const express = require("express");
const mongoose = require("mongoose");
const getRaw = require("./lib/getRaw");
const { Raw } = require("./lib/database");
const app = express();
const port = 1216;
//连接数据库
mongoose
.connect(process.env.MONGODB_URL || "mongodb://localhost:27017/exif", {
useNewUrlParser: true,
})
.then(() => console.log("Connected to MongoDB"))
.catch((err) => console.error("Filed to connect to MongoDB", err));
//以图片url查询数据库,若没有,调用getRaw()
app.get("/exif", async (req, res) => {
const url = req.query.url;
const data = await Raw.findOne({ url: url });
if (!data) {
const rawData = await getRaw(url);
return res.json(rawData.exif);
} else {
return res.json(data.exif);
}
});
app.get("/gps", async (req, res) => {
const url = req.query.url;
const data = await Raw.findOne({ url: url });
if (!data) {
const rawData = await getRaw(url);
return res.json(rawData.gps);
} else {
return res.json(data.gps);
}
});
app.listen(port, () => console.log(`Fetch EXIF is running on port ${port}!`));
这时候运行起一个MongoDB,再npm start
,接口就成功运行了。
Docker部署
因为引入数据库,其他人部署会比较麻烦,于是我做了个Docker Image,通过一个docker-compose.yml
就能运行起整个接口。
dockerfile
version: '3'
services:
mongo:
image: mongo:5.0.17
restart: always
ports:
- 27017:27017
networks:
- app-network
volumes:
- ./data:/data/db
api:
image: darmau/fetch-exif:2.0
restart: always
ports:
- 1216:1216
networks:
- app-network
environment:
MONGODB_URL: mongodb://mongo:27017/exif
depends_on:
- mongo
networks:
app-network:
driver: bridge