分类目录归档:技术

使用 Find Duplicates 插件清理 Calibre 书库

我在导入 Kindle 的图书时,错误的导入了两遍,导致我的 Calibre 仓库快速膨胀,因此,我希望清理掉其中重复的书籍,减少二次存储。

清理使用的是 Find Duplicates 这个插件

安装

在 Calibre 的「首选项」-「插件安装」页面,搜索 Find Duplicates,就可以找到这个插件,双击安装,并重新启动,即可使用该插件。

205m0u

如果你默认选择的是在主菜单展示工具,则会像我一样,在页面顶部有一个新的入口

lb787o

使用

使用非常简单,点击按钮,进入搜索页面,可以配置对比的元素,可以是二进制对比(更精准)或者是 基于某个特定的标签对比(比如 ISBN),也可以使用默认的标题作者对比。根据你的喜好,你可以选择合适的方式进行对比。

在下方的结果输出中,可以选择“Show All Groups at once with highlighting”,这样可以在一个列表中快速看到所有需要清理的图书,快速删除,达成目标。

点击确定,执行后的结果就是这样子的了,你只需要在重复的两本书中,选择你要删除的那一本即可。

如何 Debug 爬虫无法成功爬取的问题

在写爬虫的时候,我们会遇到最常见的问题是浏览器访问是一切正常的,但到代码编写的时候,就发现无法正常爬取。

y39ss5

这个时候往往是我们的爬虫所模拟的行为和代码里是不同的(比如浏览器拥有 30 个参数,但我们的代码中只有一个参数),从而导致最终执行的效果不同。而想要在代码中实现和浏览器相同的效果,最重要的是完全复制浏览器的行为,以便于让代码去模拟

所以关键在于找到从30个参数正常运转,到1个参数不运转的关键参数,毕竟我们不想在代码当中添加太多的 Magic Value 来解决一些问题。所以要找到关键的参数。

获取现场

首先,我们需要获取到和浏览器一样的数据,因为这个是我们进行后续的基础,有了它,我们才能进行从 30 个参数减少到 1 个参数。在执行这个步骤时,你需要借助 Chrome Devtools,来获取到现场。

使用 F12 打开 Chrome DevTools,切换到 「网络」Tab ,并刷新页面,以重新加载请求。页面正常加载完成后,你就可以从中找到你要 Debug 的请求。在这个请求上点击右键,选择「复制」,并选择「以 cURL 格式复制」,复制以后,你会得到如下的内容。这就是你在使用浏览器时,实际发送给服务器的请求。

你可以将这段命令放在 Terminal 中运行,你会看到和浏览器中的一样的内容输出。

此时,我们看复制出来的命令,其中包含了链接(我们的一个的参数),以及大量的 Header,这些 Header 中的某一个可能就是服务器将我们视为爬虫的 Header,然后拒绝我们的。

curl 'https://www.baidu.com/' \
  -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' \
  -H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7' \
  -H 'Cache-Control: no-cache' \
  -H 'Connection: keep-alive' \
  -H 'Cookie: BIDUPSID=1B455AFF07892965CF63335283C0BD80; PSTM=1690036933; BD_UPN=123253; BDUSS=BDcmp1YzFSeTRDLXVGZlNBbDJKZ08ya1lMQUpBVTlEaWM5WE9mV25YWn5EfmxrRVFBQUFBJCQAAAAAAAAAAAEAAADwPbowsNe084aq4MIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH-C0WR~gtFkan; BDUSS_BFESS=BDcmp1YzFSeTRDLXVGZlNBbDJKZ08ya1lMQUpBVTlEaWM5WE9mV25YWn5EfmxrRVFBQUFBJCQAAAAAAAAAAAEAAADwPbowsNe084aq4MIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH-C0WR~gtFkan; delPer=0; BD_CK_SAM=1; ZFY=sMxi79JqlMJjPMSJ3gQr5ht5g0MCtkIqefc2OTMspV4:C; BD_HOME=1; BDRCVFR[feWj1Vr5u3D]=I67x6TjHwwYf0; rsv_jmp_slow=1691762441727; BAIDUID=1B455AFF07892965CF63335283C0BD80:SL=0:NR=10:FG=1; sug=0; sugstore=1; ORIGIN=2; bdime=0; BAIDUID_BFESS=1B455AFF07892965CF63335283C0BD80:SL=0:NR=10:FG=1; PSINO=2; COOKIE_SESSION=161794_0_4_3_2_11_1_0_4_4_1_0_161839_0_9_0_1691922269_0_1691922260%7C4%230_0_1691922260%7C1; MCITY=-131%3A; H_PS_PSSID=36558_39217_38876_39118_39198_26350_39138_39100; BA_HECTOR=8gah01agag8g2kala0a12l2o1idr2ba1o; RT="z=1&dm=baidu.com&si=8e7a4596-4b9f-45c3-bf30-f7dffaddd79d&ss=llewny2r&sl=3&tt=1ag&bcn=https%3A%2F%2Ffclog.baidu.com%2Flog%2Fweirwood%3Ftype%3Dperf&ld=89j&ul=104t&hd=105p"' \
  -H 'DNT: 1' \
  -H 'Pragma: no-cache' \
  -H 'Sec-Fetch-Dest: document' \
  -H 'Sec-Fetch-Mode: navigate' \
  -H 'Sec-Fetch-Site: none' \
  -H 'Sec-Fetch-User: ?1' \
  -H 'Upgrade-Insecure-Requests: 1' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --compressed
Code language: JavaScript (javascript)

使用二分法测试 Header

如果要查出哪些 Header 是关键参数,一个比较便捷的方式是采用二分法的方式来调试这些 Header。

具体操作时,你只需要删除一半的 Header,并再次发送 cURL,看是否可以正确获得我们想要的内容。如果你发现删除的这一半并不会让你拿到的返回结果报错,就可以继续使用二分法删除 Header,直到定位到真正会影响到我们正确获取结果的 Header 为止。

当你测试到某个 header 被移除后回导致无法正确返回内容,接下来你可以把剩下的 header 继续使用二分法来删除,测试具体会影响结果的 Header,直到删无可删之时,就说明我们已经拿到了能正常运转的最简参数。

此时,你就可以放心的将上述逻辑放在你的代码中来进行维护。

除此之外,你还可以使用上述的逻辑,把你的爬虫逻辑放进单元测试,这样当目标网站调整了爬虫逻辑之后,你就会快速发现。

ACME.sh 续签不成功下的 Debug 记录

我的域名证书目前基本上 100% 使用 acme.sh,免费大碗,唯一的问题是需要续签。对于一些涉及到 CDN 场景的可能不太友好。不过我的 CDN 往往也不太使用主域名,所以也可以接受。

最近在给一个域名续签的时候,出现了报错的问题

续签代码:

 "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh"
Code language: JavaScript (javascript)

报错如下:

[Sun Aug 13 06:02:43 PM CST 2023] Renew: 'replace.com'
[Sun Aug 13 06:02:43 PM CST 2023] Renew to Le_API=https://acme.zerossl.com/v2/DV90
[Sun Aug 13 06:02:43 PM CST 2023] Using CA: https://acme.zerossl.com/v2/DV90
[Sun Aug 13 06:02:44 PM CST 2023] Multi domain='DNS:replace.com,DNS:feishu.io'
[Sun Aug 13 06:02:44 PM CST 2023] Getting domain auth token for each domain
[Sun Aug 13 06:02:48 PM CST 2023] Getting webroot for domain='replace.com'
[Sun Aug 13 06:02:48 PM CST 2023] Getting webroot for domain='feishu.io'
[Sun Aug 13 06:02:48 PM CST 2023] Verifying: replace.com
[Sun Aug 13 06:02:49 PM CST 2023] Processing, The CA is processing your order, please just wait. (1/30)
[Sun Aug 13 06:02:53 PM CST 2023] replace.com:Verify error:"error":{
[Sun Aug 13 06:02:53 PM CST 2023] Please add '--debug' or '--log' to check more details.
[Sun Aug 13 06:02:53 PM CST 2023] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
[Sun Aug 13 06:02:55 PM CST 2023] Error renew replace.com.
Code language: JavaScript (javascript)

在出现问题之后,可以通过在命令中添加 --debug 的方式,来实现查看响应的日志,debug 的命令如下:

 "/root/.acme.sh"/acme.sh --cron --debug --home "/root/.acme.sh"
Code language: JavaScript (javascript)

则在新的输出中,说明了我具体卡在哪里了

[Sun Aug 13 06:04:49 PM CST 2023] replace.com:Verify error:"error":{
[Sun Aug 13 06:04:49 PM CST 2023] Debug: get token url.
[Sun Aug 13 06:04:49 PM CST 2023] GET
[Sun Aug 13 06:04:49 PM CST 2023] url='http://replace.com/.well-known/acme-challenge/rm-enWjHphDeyjXtfXu2mi1V-D6cZY8EHAe_Gi7TmC4'
[Sun Aug 13 06:04:49 PM CST 2023] timeout=1
[Sun Aug 13 06:04:49 PM CST 2023] _CURL='curl --silent --dump-header /root/.acme.sh/http.header  -L  --connect-timeout 1'
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
[Sun Aug 13 06:04:49 PM CST 2023] ret='0'
Code language: HTML, XML (xml)

acme.sh 的报错非常清晰,我的报错是因为我的文件目录中 /.well-known/ 中的验证文件找不到了,所以报错。那我只需要去检查我的 Nginx 配置即可。这里因为我的 Nginx 配置根目录不在默认指明的目录(因为用了 Docker ,nginx 只是一个代理),所以我需要将其修改为真正的 webroot。

修改 ~/.acme.sh/replace.com/replace.com.conf 中的 Le_Webroot 配置为真实的 webroot,再重新签名,就域名成功签名了。

给 mdbook 增加备案号显示

由于 Gitbook 长期不维护,为了保障整个架构持续可用,所以导致我不得不从 Gitbook 迁移到 mdbook。

在使用 mdbook 的时候,有个问题是,我的不少电子书域名都是备案过后的,mdbook 又不像 gitbook 支持可以在目录里跳转外链,因此我需要自己实现给 mdbook 添加一个外链。

实现思路

由于没办法直接通过在 Markdown 当中实现跳转外链,那么一个比较简单粗暴的方法就是直接在模板当中添加,刚好 mdbook 在文档中有说明具体的操作方法

你只需要在 mdbook 的根目录中创建一个 theme 文件夹,并生成一个 index.hbs 文件,用于渲染页面。

接下来,你只需要在适当的位置添加上你需要的代码即可(下方的 toc 后面的 有序列表便是我手动添加的)

   <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <div class="sidebar-scrollbox">
                {{#toc}}{{/toc}}

                 <ol class="chapter">
                    <li class="chapter-item expanded "><a href="https://beian.miit.gov.cn/" target="_blank">这里是你的备案号</a></li>
                </ol>

            </div>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>

        </nav>
Code language: HTML, XML (xml)

参考文档

https://github.com/rust-lang/mdBook/issues/1354

APILetter S1E5 如何设计一个符合 RESTFul 风格的批量操作的 OpenAPI 接口?

批量创建、批量更新、批量删除

批量获取说完了,接下来我们来聊聊批量更新,实际上批量更新、批量创建虽然有场景,但也不多,在这种场景下,我们已经很难像批量获取那样,在原有资源上进行操作,而是需要借助批量资源来实现批量操作。

以用户资源(User)为例,当我们需要对其进行批量创建、删除、更新时,我们需要创建一个批量资源 BulkUser,并通过对 BulkUser 操作,来创建用户。Bulk User 本质上是将请求的多个资源转换为了异步的任务,在发起后,开发者可以在任务结果中查询具体的值来使用。

如何理解异步的任务?
这里异步的任务更多是一种设计表现,并不强制要求一定异步。异步的表现设计和相关的接口实现,是为了给后续留出纵向扩展的空间。既 无论是否行为是否真实异步,都需要在返回结果中返回任务 ID & 任务状态,以便于开发者自行实现异步处理的逻辑。

{
    "code":0,
    "data":{
       "job":{          "id":"123",          "status":"ok"        },
        "results":[
            {
            ...
            }
        ]
    }
}
Code language: JavaScript (javascript)

批量创建

批量创建用户的操作和创建单个用户的操作是比较接近的,主要差异点在于 Path 上有区别,且传递参数时,会传递多个资源的属性。

批量创建用户

# request
POST /bulk_users/

{
    "users":[
        {
            // user1
            ...
        },
        {
            // user2
            ...
        }
    ]
}
# response
HTTP/1.1 200 OK
{
    "code":0,
    "data":[
       {
            // user1
            ...
        },
        {
            // user2
            ...
        }
    ]
}
Code language: PHP (php)

批量更新用户

批量更新时,你已经知道了你需要更新的资源的 ID,因此,可以这样设计的你的接口:

#request
PUT /bulk_users?id[]=1&id[]=2

{
    "gender":"other"
}

# response
HTTP/1.1 204 No Content
{
 "code":0,
    "data":[
       {
            // id=1
            ...
        },
        {
            // id=2
            ...
        }
    ]
}
Code language: PHP (php)

批量删除

有了上面的几个例子,批量删除就比较好定义了。就像这样:

#request
DELETE /bulk_users?id[]=1&id[]=2

# response
HTTP/1.1 200 OK
{
 "code":0,
    "data":[
       {
            // id=1
           "status":"deleted"
        },
        {
            // id=2
            "status":"deleted"
        }
    ]
}
Code language: PHP (php)

总结

批量操作在获取场景,可以考虑通过 List + Filter 的方式,或搜索的方式来实现一套更加标准的搜索接口,而规避提供定制化的自定义接口。从规范的视角,两者都是符合规范的,也可以都对用户提供,并不互斥。而对于没办法复用的创建、更新、删除,则可以考虑使用创建异步任务的方式,来实现批量操作,给开发者一个明确的异步预期,让开发者可以自行查询业务的实现方式。

在 Render.com 上部署 Django 4.2

最近在写 Linux 中国的翻译工具的时候,后端我使用的是 Django,版本则选择了 Django 4.2,Python 3.11。在部署 Django 的时候,我选择使用 Render.com 来部署。 不过,在部署的时候,我遇到了一些问题,Render 官方提供的 Getting Started with Django on Render 会部署错误,所以有了今天这篇文章, 告诉大家如何把最新的 Django 4.2 部署到 Render 上。

初始化项目

Render 没有使用 pip,而是使用 Poetry 来管理 Django 项目的,因此,你需要使用 Poetry 来完成项目的初始化。

poetry init #初始化 Poetry 的 配置文件
poetry add django gunicorn # 添加依赖 Django 和 gunicorn
poetry run django-admin startproject linuxondjango .
Code language: PHP (php)

初始化项目基本上就是用 Poetry 替代 pip ,这里没有需要针对 Render 特化的部分,就不做过多的介绍。

编写逻辑代码

当你完成了项目的初始化之后,可以编写你自己的业务逻辑代码,这部分不再多讲,可以正常开发使用。

配置项目以支持 Render 的服务端环境。

1. 从环境变量中读取 Secret Key

Django 使用 Secret Key 作为 Session 加密等一些加密场景的 Salt 和 Seed,所以在 Django Admin 创建项目时,会默认生成一个 Session。不过出于安全考虑,最好不要将其放在代码中,而是在服务端生成后,通过环境变量来存储,避免代码泄露后导致的 session 被解密。

你需要在 settings.py 中,添加如下代码,来替代默认的 key。

import os
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('SECRET_KEY', default='your secret key')
Code language: PHP (php)

2. 在环境变量中读取 Debug 配置

Render 会自动配置一些环境变量,因此,你可以直接通过判断当前环境上下文来确认当前是否是在 Render 的服务端,如果不在,则配置 Debug 为 True,来解决线上不使用 Debug 模式的需求。

DEBUG = 'RENDER' not in os.environ
Code language: JavaScript (javascript)

3. 从环境变量中读取可用域名

Django 是有域名配置的,非配置域名,无法访问当前应用,因此,你需要在 Render 当中读取域名,来确保可以正常访问。当然,如果你自己配置了自己的域名,也可以直接手动写在 ALLOWED_HOSTS 当中。

ALLOWED_HOSTS = []

RENDER_EXTERNAL_HOSTNAME = os.environ.get("RENDER_EXTERNAL_HOSTNAME")
if RENDER_EXTERNAL_HOSTNAME:
    ALLOWED_HOSTS.append(RENDER_EXTERNAL_HOSTNAME)

Code language: JavaScript (javascript)

配置 render.yml 来支持 Render BluePrint

你可以直接复制下面的内容,来作为你的项目的启动配置。其中 build.sh 为构建项目的配置。

build.sh

build.sh 当中最重要的是重新安装 Poetry,因为我使用的是 Python 3.11.4, 和 Render 默认的 Python 3.7 不匹配,所以没办法直接用默认的 Poetry,需要自动手动升级 Poetry。

#!/usr/bin/env bash
# exit on error
set -o errexit

pip install --upgrade pip; pip install poetry;  # 重新安装一下最新的 Poetry,因为默认的 Poetry 的版本比较低。
poetry install

python manage.py collectstatic --no-input
python manage.py migrate
Code language: PHP (php)

render.yml

Render 当中,最重要的是 startCommandPYTHON_VERSION ,startCommand 这里是我使用 gunicorn 来启动 Django 应用,而 PYTHON_VERSION 则是用来设定具体的 Python 版本,这里我根据我自己的需求,选择了 Python 3.11.4。

databases:
  - name: linuxondjango-db
    databaseName: mysite
    user: mysite
    plan: free

services:
  - type: web
    name: linuxondjango
    plan: free
    runtime: python
    buildCommand: "./build.sh"
    startCommand: "gunicorn linuxondjango.wsgi:application"
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: linuxondjango-db
          property: connectionString
      - key: SECRET_KEY
        generateValue: true
      - key: WEB_CONCURRENCY
        value: 4
      - key: PYTHON_VERSION # 这里的 python version 是用来指定 Python 版本的,比如这里我用的是 3.11.4。
        value: 3.11.4
Code language: PHP (php)

总结

Render 的教程总体来说没啥大问题,但是在一些小的点上,需要你自己简单 Hack 一下,比如需要自己升级一下 Poetry、设定 Python 版本。如果你也在用高版本的 Django & Render,希望这篇文章 可以帮到你。

如何解决 Kindle 在 M1 系列设备上无法访问的问题

问题

在使用 M1 的时候,我遇到一个很麻烦的问题是 M1 无法识别出我的 Kindle系统无法自动加载 M1 设备,这导致习惯于导入标注并使用 Klib 管理的我来说,等于用 Kindle 的功能不齐全了。

使用 macOS 自带的系统工具,也可以看到系统 Kindle 的磁盘,但无法加载。

d2b5ca33bd970f64a6301fa75ae2eb22 29
磁盘工具的展示

如果使用磁盘工具尝试加载,也会报错 com.apple.DiskManagement.disenter错误-119930872

d2b5ca33bd970f64a6301fa75ae2eb22 30
报错的提示

无法使用系统工具加载。

解决方案

在参考了 jakevin 的分享后,我使用如下方式来解决我的 Kindle 挂载问题。

查询外置设备

执行如下命令,可以使用系统自带的 diskutil 查看目前有哪些磁盘。我在这里补充了 grep,来筛选出只有外置磁盘的设备。

diskutil list | grep external -A2
Code language: PHP (php)
d2b5ca33bd970f64a6301fa75ae2eb22 31

手动挂载设备

执行如下命令,来手动挂载 Kindle。

sudo mkdir /Volumes/Kindle # 创建一个新的挂载点,挂载 Kindle
sudo mount -t msdos /dev/disk4 /Volumes/Kindle/ # 使用 mount 命令,挂载 /dev/disk4(你根据需要换成你自己的设备。)
Code language: PHP (php)

一般来说, Kindle 的默认格式化是 fat32 格式,所以用上面的命令就行,但如果你的 Kindle 是 ex-fat 格式,则可以使用如下命令挂载。

sudo /sbin/mount_exfat /dev/disk4 /Volumes/Kindle/ #这里使用的是 mount_exfat。
Code language: PHP (php)

如此操作,便可以让 M1 识别 Kindle 了。

d2b5ca33bd970f64a6301fa75ae2eb22 32

给你的 console.log 添加一些特定的输出

在写 Node.js 代码时,常常会使用 console.log 来输出内容,以便于调试。但默认的 console.log 只能标准的输出,在很多需要上下文 debug 的时候,可能信息是不足的。除了使用 debugger 以外,你还可以试着改造 console.log

在你的 index.js 顶部添加如下代码,即可实现在使用 console.log 时自动在前面加上时间信息。当然,你也可以实现自己需要的上下文,比如当前的文件、当前的行数等。

console.log = (function() {
  var console_log = console.log;
  return function() {
    var args = [];
    args.push(`${new Date().toLocaleString()}` + ' -> ');
    for(var i = 0; i < arguments.length; i++) {
      args.push(arguments[i]);
    }
    console_log.apply(console, args);
  };
})();
Code language: JavaScript (javascript)

这个函数的逻辑不复杂,对 console.log 进行了覆盖,写如了新的函数,并通过 arguments 将开发者传入的参数重新打印,以确保不丢失开发者传入的参数。

如何将 Zed 编辑器设置为你的命令行默认编辑器

我最近在使用 Zed 作为我的主要编辑器,在编辑一些命令行文件时,也会使用 Zed 来编辑。但有些时候,一些应用程序的命令会自动调用默认的编辑器,这个时候会默认使用 nano 或者 vim ,而不是 Zed,体验略差。所以我希望将命令行默认的编辑器修改为 Zed 编辑器。

如果需要将 Zed 作为自己的命令行编辑器,首先需要确认 Zed 是否支持等待模式(wait model),在命令行中执行 zed --help,可以看到 Zed 是支持等待模式的。接下来就可以进行后续的步骤来进行测试了。

d2b5ca33bd970f64a6301fa75ae2eb22 3

这里为是在写 Rails 时用到的,因此,我继续使用 rails credentials 来测试。执行如下命令来确认该命令是否可用。

EDITOR="zed --wait" rails credentials:edit
Code language: JavaScript (javascript)

执行后,可以正常唤起,则说明整个链路已经通畅,接下来只需要将其配置在系统的默认环境变量里即可,将如下代码放在 .zshrc 中即可。

export EDITOR="zed --wait"
Code language: JavaScript (javascript)

其他编辑器的命令参考

# Sublime Text
export EDITOR="subl --wait"
# VSCode
export EDITOR="code --wait"
Code language: PHP (php)

为 Next.js 加上 Git Commit 版本号

在开发服务端应用的时候,由于服务端应用本身的特性,其实是没有一个明确的版本的概念。毕竟不需要专门下载,理论上每次都是最新的,所以也没有版本的概念。

但在实际开发调试过程中,我们又的确需要关注版本的概念,因为会影响具体的表现形态,所以就需要有一个前后端协调的版本号概念,来帮助我们更好的定位问题,避免前后端之间的扯皮。

一个比较好的思路是,虽然服务端没有版本号概念,但大部分时候会有一个对应的 Commit ID(毕竟现在开发项目完全不用版本控制工具的还是挺少见的)。所以,你可以选择将 Commit ID 作为版本号,进行输出,从而让协作者知道当前线上跑的版本,便于 debug。

在具体实现时,有两种方式:

1. 将 Commit ID 放在 Header 里

我自己平时会把 Vercel 和 Next.js 提供的 API Route 作为一个简单的 Serverless FaaS 环境来使用,因此一个诉求便是在 API Route 当中返回具体的 Commit ID。而为了避免对代码的侵入,将其放在 Response Header 当中是比较合适的。

d2b5ca33bd970f64a6301fa75ae2eb22 2
添加完成的效果。

而如果你希望和我一样,达成对特定路由下的返回结果添加特定的 Header(比如上面截图中 x-build-sha 就是我添加的 Commit ID 的 Header),则需要借助于 Next.js 提供的自定义 Header 能力

通过在 next.config.js 当中的 header 属性中添加具体的配置,来实现对特定的路径下添加自定义 Header。

module.exports = {
  async headers() {
    return [
      {
        source: '/about',
        headers: [
          {
            key: 'x-custom-header',
            value: 'my custom header value',
          },
          {
            key: 'x-another-custom-header',
            value: 'my other custom header value',
          },
        ],
      },
    ];
  },
};
Code language: JavaScript (javascript)

这里面比较关键的是 source 字段,这个字段定义了究竟哪些路由下会返回特定的 Header。比如上面的这段配置就是只给 /about 添加具体的 Header。你可以使用 /:path* 来匹配所有路由,从而实现给所有路由都添加上具体的 Header。

以我为例,我在线上跑的配置实际上是下面这段配置:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [{ key: 'X-Build-SHA', value: process.env.VERCEL_GIT_COMMIT_SHA }]
      }
    ];
  }
}

module.exports = nextConfig
Code language: JavaScript (javascript)

在上面这段配置中,我给所有的路径都配置了一个 x-build-sha 的 header ,并从进程的变量中提取出 VERCEL_GIT_COMMIT_SHA 变量(这个变量在 Vercel 的部署环境中指向具体的 Commit ID)的值,将其返回。

2. 将 Commit ID 放在 UI 里

除了在 Header 中返回,如果你是需要去 Debug UI 的话,版本号同样重要,这个时候,你可以选择将 Commit ID 放在界面上,从而实现快速找到 Commit ID。

在 Vercel 部署的 Next.js 上,有一批 Next.js 框架所属的环境变量, 可以直接在 UI 当中引用(上面的 VERCEL_GIT_COMMIT_SHA 是不能在 UI 中直接引用的)。

只需要在特定的位置,加入SHA: {process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA} ,就可以展示具体的 Commit ID。

(除了 Next.js,其他框架也有类似的框架变量可以使用,你可以根据自己的需求来选择)。

总结

在服务端 Debug 时,将你的 Commit ID 以某种方式返回可以有效的帮助快速定位问题,试着给你的 Next.js 添加上这个 Commit ID,来加速你的问题排查吧~