开发

利用ChatGPT实现一个Node.js API

在工作被AI替代前先让AI帮你工作吧

2023年3月5日
利用ChatGPT实现一个Node.js API
本文共有2042字,预计阅读时间10分钟
在上一版博客中,我没有实现希望中的显示摄影图片的EXIF和地理位置信息。当时立了个flag,要在下一版实现。
因为对于以摄影为主题的网站来说,EXIF和拍摄位置对于其他人来说是很重要的信息,没有这个信息就显得不太专业。
最近开发新版博客,我决定实现这个功能。中间经历了一些弯路,最后在ChatGPT的启发和帮助下,终于还算完成了目标。

初步思路

不管是拍摄数据还是GPS,都是EXIF信息中的一部分,所以这两个问题其实是一个。
对于前端来说最简单的方案就是,在上传图片的时候将EXIF信息提取并存入数据库,用的时候直接请求接口即可。但我用的是开源Headless CMS方案Strapi,并没有这种功能。我相信开发个插件肯定能实现这个功能,但这就超出我的能力范围了。
数据存在Strapi,但图片存在对象存储,CMS返回的实际上只有一个图片URL。如果要提取信息,只能从这个URL先把图片下载下来,再做分析。
NPM有现成的包,比如exifr,将图片传入就会返回其中的EXIF信息。所以如果我获取到原图,将其传入,应该就能拿到EXIF了,之后再用useEffect将数据渲染到界面上就可以了。

尝试

将拿到的图片链接下载后传入exifrparse()方法:
1const res = await fetch(url);
2const blob = await res.blob();
3const rawData = (await exifr.parse(blob)) || {};
如果是本地图片,你会发现能成功提取到EXIF。但用上述代码却发现并不能成功获取。查看错误信息就会发现这个行为触发了浏览器的CORS限制。
什么是CORS?
根据MDN:
跨源资源共享(CORS,或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
意思就是浏览器不允许你随意的向其他服务器发送请求,除非是服务器允许的。在本例中,你在localhost向OSS域名发起了一个GET请求,被浏览器阻止了。要想解决这个问题,可以在OSS那边进行配置,允许外部域名发来这样的请求。
可我用的是一个不知名厂商的OSS,除了一个存取key什么都没给我,连管理文件的图形界面都没有。所以在OSS这边没什么能做的。
如果给OSS套一层CDN,解析为我自己的域名,这样不就能避开跨域了吗?
还真的可以。当我给图片OSS配置了CDN和域名,并在CDN设置了Access-Control-Allow-Origin,页面终于如我所愿,获取到了EXIF信息并渲染到界面上。

土豪行为

我满意地点来点去,看着拍摄信息加载出来,啊,真好看。
但当晚上我瞟了一眼CDN数据,被吓到了。
怎么我一个人就刷出了1G多的流量?
想了一下我发现了这个设计的问题所在。
网页中的所有图片我都采用了next/image来进行压缩优化,实际中并没有直接从OSS下载图片,而是由服务器下载经过压缩后返回给浏览器。这样能减小图片体积提升加载速度。
这个是没问题的。
问题出在获取EXIF的流程上。每一次加载,浏览器都会通过CDN下载到原图,提取EXIF。
下载几M的图片就为了获取那点拍摄数据,而用户看到的还是经过压缩的图。
也就是说,花了比原图还多的流量,看到的质量却不如原图。那我这是图啥啊。
这个方案肯定不能用,光我就花了1G流量,真上线后不知道得花掉多少钱。

ChatGPT给的灵感

我已经想不到有什么方案了,只好咨询ChatGPT。
它告诉我可以在服务器写个接口,由服务器来做下载原图抽取信息的事,我们只需要向这个接口传入图片URL,返回的就是EXIF。
看到这个方案我是拒绝的,因为我从来没敢想我一个设计怎么还能写后台。目前前端、运维这些事就够我头疼了。但想了一下,它的建议其实有道理。
  1. 我的服务器流量很大,基本不用担心流量耗尽;
  2. 查询的结果可以缓存,实际上真正的流量并不会那么大。
那就让ChatGPT帮我实现这个API吧!

第一个Node.js程序

它告诉我,要想提供一个接口,可以使用Express实现。
接下来的每一步的思路都是ChatGPT告诉我的,我只是稍微修改了一下结构。
首先定义路由:
1const express = require("express");
2const app = express();
3const port = 1216;
4
5app.get("/exif", async (req, res) => {
6  //在这里执行查询
7});
8
9app.listen(port, () => console.log(`Exif查询接口正在运行 ${port}!`));
提取信息流程,就可以复用之前前端实现的逻辑:
1const exifr = require("exifr");
2const axios = require("axios");
3
4async function getExif(url) {
5  try {
6    const response = await axios.get(url, { responseType: "arraybuffer" });
7    const rawData = await exifr.parse(response.data);
8    exif.Maker = rawData.Make || "unknown";
9    exif.Model = rawData.Model || "unknown";
10    exif.ExposureTime = formatShutterTime(rawData.ExposureTime) || "unknown";
11    exif.FNumber = rawData.FNumber || "unknown";
12    exif.iso = rawData.ISO || "unknown";
13    exif.FocalLength = rawData.FocalLength || "unknown";
14    exif.LensModel = rawData.LensModel || "unknown";
15    return exif;
16  } catch (err) {
17    throw err;
18  }
19}
formatShutterTime()是一个把获取到的原始快门数据(0.0001)转换成摄影师更熟悉的格式的函数:
1function formatShutterTime(shutterTime) {
2  if (!shutterTime) return "0";
3  const time = parseFloat(shutterTime);
4  if (time >= 1) {
5    return time.toFixed(2);
6  }
7  const fraction = Math.round(1 / time);
8  return `1/${fraction}`;
9}
GPS也是同样的方法,可以把这两个方法提取到单独的文件,在主入口调用。
1const exifr = require("exifr");
2const axios = require("axios");
3
4const GPS = {};
5
6async function getGPS(url) {
7  try {
8    const response = await axios.get(url, { responseType: "arraybuffer" });
9    const rawData = await exifr.parse(response.data);
10    GPS.latitude = rawData.latitude || 0;
11    GPS.longitude = rawData.longitude || 0;
12    return GPS;
13  } catch (err) {
14    throw err;
15  }
16}
17
18module.exports = getGPS;
原本它的回答没有缓存逻辑,在我的提醒下,它推荐使用node-cache并提供如下代码:
1app.get("/exif", async (req, res) => {
2  const url = req.query.url;
3  const cacheKey = `exif:${url}`;
4  let exifData = cache.get(cacheKey);
5
6  if (!exifData) {
7    try {
8      exifData = await getExif(url);
9    } catch (err) {
10      return res.status(500).send(err.message);
11    }
12    cache.set(cacheKey, exifData, 3600);
13    res.json(exifData);
14  } else {
15    res.json(exifData);
16  }
17
18});
上面这段的意思是,收到请求后先在缓存里查询,如果有直接返回缓存的结果;如果没有,将结果存入缓存再返回。

效果

完成的代码在这里查看。
将Node运行起来,看看API有没有成功。
将图片URL作为参数附加到API域名,并使用GET,得到如下返回数据:
1{
2	"Maker":"SONY",
3	"Model":"ILCE-7M4",
4	"ExposureTime":"1/1250",
5	"FNumber":4,
6	"iso":100,
7	"FocalLength":61,
8	"LensModel":"FE 24-105mm F4 G OSS"
9}
成功了!
接下来的工作就简单了,把返回的数据通过useEffect()显示到界面上就可以了。
GPS同理,只不过是将位置传给Mapbox。
最终效果,访问所有摄影点开任何一个摄影就可以看到了。

时代真的变了

整个过程中,ChatGPT给我提供了思路和相关代码。尽管经常有很多错,但思路是正确的。你需要纠正它的错误信息,或者去谷歌深入了解。遇到不解继续询问它,再继续了解,这是一个正循环。
我不知道AI会把我们的工作替代成什么样,但它让我作为一个设计师搞定了以前想都不敢想的功能,我很喜欢。

评论