开发

Fetch EXIF升级了

减少查询次数和流量,优化缓存方式,支持Docker部署

学习Node.js后,可使用MongoDB和Node.js实现修改摄影作品的EXIF和GPS信息查询功能,优化了性能和可部署性。重构了数据库模型和获取信息函数,通过docker部署简化项目运行。运行整个接口只需使用docker-compose.yml即可。

  1. #Node.js
  2. #MongoDB
  3. #Express
  4. #API
  5. #CRUD
  6. #Docker
  7. #Database

407

An aerial view of a large cargo ship sailing through the water, surrounded by other ships and a bustling port.An aerial view of a large cargo ship sailing through the water, surrounded by other ships and a bustling port.

最近在学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