分类目录归档:技术

linux terminal

如何将 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)
text

为 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,来加速你的问题排查吧~

text

到底多少个错误码才是合理的?

在对外开放 OpenAPI 的时候,错误的设计也是一个极为影响开发者开发体验的设计点。今天我们简单聊聊关于错误码和错误处理

为什么需要错误码?

由于我们不能保证系统可以完全处理用户的请求,因此我们需要通过错误码来告诉开发者发生了不符合预期的情况。不符合预期的情况可能由用户输入错误导致,也可能由内部微服务故障导致。为了建设一个健壮(Robust)的系统,我们需要通过对外暴露一批错误码,帮助开发者更好地处理各种异常情况。

避免错误码数量过少

既然我们的目的是帮助开发者解决异常情况,那么一个合理的答案是:和异常情况匹配的错误码数量是一个好的错误码数量。

如果在某个 API 接口上提供了一个错误码,则意味着我们认为这个接口只会出现一种情况导致的错误。而实际上,很可能会有多种情况导致错误发生。这种情况经常在 OpenAPI 评审过程中被提出来,也是用户在使用 OpenAPI 时常遇到的问题:为什么我找不到这个错误码?对于开发者来说,无论输入何种错误,都会得到相同的错误码,难以定位和解决问题。同一个错误码也意味着你无法提供足够的错误信息来进行排查。例如,常见的 “400 Bad Request” 错误,如果参数很多,排查错误可能是一个极其痛苦的过程。

避免错误码数量过多

另一方面,错误码数量过多也会导致问题。有些 OpenAPI 接口提供了几十个不同的错误码,看起来感觉很不错。但是仔细一看,就会发现这些错误码实际上只是针对不同的字段错误而已,导致错误码数量快速增长。而实际上,可以将参数错误放在同一个错误码中,并通过动态的参数和原因来解决,而不是返回一堆类似的错误码。大量的错误码对于开发者来说,存在记忆困难的问题,在实际编写代码的过程中,也需要编写大量的错误处理逻辑,来兼容我们对外抛出的错误处理逻辑。

换一种思路来组织错误码

如果你无法很好的掌握拆分和组织错误码的粒度,那我可以给你一个建议:按照用户处理错误的手段来拆分错误码。过去从内部视角来组织和错误码,很容易出现错误码过多或过少的情况,而从外部视角来梳理错误码,则可以帮助我们更好的厘清错误码的分类和组织。

用户并不关注我们的系统到底因为什么出现了错误,他们只关心出现了什么样的错误?我应该如何处理这些错误?那错误码可以非常快速的收敛为以下几类:

  • 本资源参数校验错误,对应的处理策略往往是开发者需要查看文档,了解资源参数的限制, 修改参数重新调用。
  • 跨资源参数校验错误,对应的处理策略往往是开发者需要通过其他 API 获取关连资源的 ID 等信息,以便于重新调用。
  • 请求频率太高,对应的处理策略是优化调用的接口和方法,调低并发量。
  • 服务端错误,对应的处理策略是重试,并在重试无效时联系官方人员。

通过上述的分类方式,我们可以快速的将错误码归类到几个大的分类,从而实现合并同类项,收敛错误码但不至于让开发者不知道下一步 Action 的情况。

错误码不重要,错误处理才重要

看到这里,相信你对于文章中提出的什么才是好的错误码设计已经有了答案。但我想说的是,错误码从来都不是核心。实际上,如果我们回看各种编程语言的范式,大多没有错误码这种设计,而是选择将更多的信息通过 exception 这样的形式暴露给开发者。错误码的设计虽然给到开发者一个可以用来做唯一判定的数据,但可以做唯一判定的不一定非要是数字。数字错误码的设计严格来说,并不是一个好的设计,因为他使得你的代码中必然会存在某些特定的 Magic Number,你需要小心的维护这些 Magic Number 来确保向开发者返回错误时不至于返回错误的错误类型和错误码。

对于一个已经存在的 OpenAPI 系统来说,错误码已经成为既定事实,则要做的是让这套系统可以更好的运转下来。但如果你要设计一套全新的错误系统,那么类似 Slack 这样的返回形式,可能是一个更好的选择,既可以规避掉 Magic Number 的问题,又可以确保每一个错误有其对应唯一的枚举值。

p3nta3

总结

好的错误码设计是适度的,你需要学会平衡错误码和对应错误信息的数量,不要太多干扰开发者,但也不可太少,不足以支撑实际的接口调用。好的错误码可以帮助我们和开发者建设更加健壮的系统,减少不必要的沟通成本,也可以让我们每一个使用这个 API 的人,都更加的幸福。

9ac4e9081f7d880526f74a5c114d3369

使用 DSL 管理你的正则表达式

最近在写 LCTT 译文的解析工具,写正则、调试正则的过程非常的痛苦。

比如,下面的这个正则表达式就是我用来提取文章的英文标题的正则。由于 LCTT 的文章元信息存储历史上出现过多次,所以我不得不写一个比较复杂的正则来匹配出想要的结果。

比如,下面的这段代码是我用来从文件路径当中提取出不同内容的正则,看起来十分复杂,对吧?

/(?<collectDate>(?<type>(?<path>\.\/)(?:published))\/(?<yearOrSeries>(?:.+)?)\/(?<month>(?<=\d+)\.(?=\d+))?)(?:⭐️|⭐️⭐️)?(?<title>.+)\.md/

相比于看起来复杂,更加麻烦的是可维护性。正则表达式的可维护性相比于正常我们熟悉的编程语言来说,是差了一大截。基本上如果要对上述这段正则表达式进行修改或者二次开发,难度极高。但借助一些工具,我们可以使用类似 DSL 的方式来管理正则表达式,则可以以一个更友好的方式来维护你的正则表达式。

d2b5ca33bd970f64a6301fa75ae2eb22

比如,以我为例,上面这张图片中的定义,便是上方正则的等价替换。虽然写了更多的代码,但确实可读性、易于理解性和易于维护性,有了显著的提升。

想要使用,也不复杂,只需要使用对应的 DSL 语法来完成定义,即可完成正则表达式的构建。以我上面的这段正则为例,我使用的是 javascript 的 magic-regexp 包,实际使用时,大概是这样的,定义出对应的正则,直接基于其进行匹配即可。

const magicRegxp = require('magic-regexp');
const { createRegExp, exactly, oneOrMore, char, anyOf, carriageReturn, global, multiline, linefeed, maybe, digit } = magicRegxp;

const regExp = createRegExp(
            exactly("./").groupedAs("path")
            .and(anyOf("published")).groupedAs("type")
            .and(exactly("/"))
            .and(maybe(oneOrMore(char)).groupedAs("yearOrSeries"))
            .and(exactly("/"))
            .and(maybe(
           exactly(".").after(oneOrMore(digit)).before(oneOrMore(digit)).groupedAs("month")
            )).as("collectDate")
            .and(maybe(
                exactly("⭐️").or(exactly("⭐️⭐️"))
            ))
            .and(exactly(oneOrMore(char)).groupedAs("title"))
            .and(exactly(".md")
        ));
let result = regExp.exec(file_content);
console.log(result.groups.title);
Code language: JavaScript (javascript)

当然,这样的维护方式,在不同的语言下有着类似的实现,如果你使用 Golang ,那么 Rex,也是一个不错的选择。

MacBook Pro on brown wooden table

如何解决 Gem 安装 Rails 无法执行的问题

如何解决 Gem 安装 Rails 无法执行的问题?

由于我并不在大型生产环境使用 Ruby on Rails,都是在一些自己的 Side Project 上使用 Ruby On Rails,所以一直以来,我都是使用 Homebrew 来安装最新版本的 Ruby & Gem。

最近升级了 M1 以后,重新在配置 Ruby ,突然发现之前的配置失效了。在配置 Rails 时,发现报了个错: Rails is not currently installed on this system. To get the latest version, simply type:

截图
截图

但我可以肯定的是,我已经执行过了 gem install rails,所以出问题的不可能是我没安装,唯一的可能便是我安装的 Rails 没有安装到 PATH 当中

870yd1

所以想要解决这个问题,只需要将我安装 gem 的路径添加到 path 中即可。

解决方案

1. 获取具体的 Path

执行 gem info rails 来获取到我的 rails 安装的路径,这里可以看到,被安装在了 /opt/homebrew/lib/ruby/gems/3.2.0 路径。

04qwth

2. 找到 Bin 目录

使用 cd /opt/homebrew/lib/ruby/gems/3.2.0 进入到 gem 目录。你可以看到这里有个 bin 目录,bin 目录就是我们具体要用的可执行文件。

re9kwr

你可以执行 ./rails -v 来确认版本正确。

rbj0dk

3. 修改 PATH 环境变量

接下来就是修改你的 PATH 环境变量了。在你用的 Shell 的配置文件当中,加入相应的 path 配置,并重启终端,即可完成配置。

4. 验证配置

重启终端后,随便找个目录,执行 rails -v,查看其结果,来验证我们的配置已经生效。如果你可以看到类似下面的结果,则说明你的配置已经生效了~

8c0vdo

d2b5ca33bd970f64a6301fa75ae2eb22 12

PayJS 测试二维码生成工具

简要描述

这个工具主要用于生成 PayJS 的测试支付订单,在 PayJS 官方不提供测试订单工具之后,可以使用这个工具来生成测试订单,简化操作。

下载地址

截图

d2b5ca33bd970f64a6301fa75ae2eb22 12
未生成二维码效果
d2b5ca33bd970f64a6301fa75ae2eb22 13
生成二维码效果

更新日志

0.0.3

  • 设置 key 为密码类型,保护信息安全。

0.0.2

  • 支持 payjs 生成 1 分钱订单,并展示二维码。
text

一个支持 ES3 环境的 querystring

相比于使用 Uniapp / Taro 之类的,我其实更喜欢使用小程序的原生来进行开发。主要是减少中间商赚差价,性能损耗更少一些。当然,也少了不少好用的体验 —— 比如随便引入 NPM 包,好在是现在的小程序开发者工具也提供了 NPM 构建的能力,所以一些基本的使用是没有问题的。

不过,小程序本身环境的特殊性,我在使用 NPM 包的时候还是会有一些谨慎的 —— 要选择尽可能小的、不受平台依赖的包,来缩小小程序的包。所以当我发现一个可以在小程序中使用的包的时候,我就会将其写下来, 以备不时之需。

在涉及到 Web 开发时,一个比较常见的场景是构建 HTTP 中的 QueryString,以便在发送 GET 请求时传递参数。但自己手拼参数还是比较痛苦的,所以用一些 package ,可以有效的提升开发的体验。

TL;DR

你可以在小程序环境中使用 <a href="https://www.npmjs.com/package/querystring-es3">querystring-es3</a> 来进行 querystring 的构建,包的体积不大,可以达到比较好的效果。

const { encode } = require('querystring-es3')

encode({
  page:1,
  pageSize: 10
})
// return 'page=1&pageSize=10'
Code language: JavaScript (javascript)

为什么不是 qs

querystring 的处理包当中,比较出名的除了 node 内置的 querystring 之外,应该就是 qs 了,但实际在使用过程中,小程序的静态分析依赖了 qs,导致开发者使用时要么关闭提醒,要么换包。考虑到我还是希望使用小程序的静态分析,所以就只能替换包了。

待解决问题

  • 实际上我使用 querystring-es3 主要是看到他写的 ES3 compat,但可能其实我可以直接用 query-string ? 需要验证一下。
black and silver laptop computer

几个 Swagger UI 的替代品

Swagger 算是 Code 2 Doc 的默认选项了,各种语言都提供了生成工具,此外官方还提供了一套方便使用的默认 GUI。

d2b5ca33bd970f64a6301fa75ae2eb22 36

不过,Swagger UI 本身其实并不够好用,所以社区就会提供一些 Swagger UI 的替代品,方便大家使用。

ReDoc

ReDoc 是一个前端的开源项目,在 Github 上拥有超过 1W + 的 Star。对我来说,我觉得他最好的点是左中右三栏模式,左侧切换 API,中间查看 API 详情,右侧查看接口调用的 Sample,比 Swagger 官方的 UI 瀑布式的要舒服。

d2b5ca33bd970f64a6301fa75ae2eb22 37

Angular Swagger UI

Angular Swagger UI 同样是一个开源的项目,界面简洁明了,虽然是瀑布式布局,但由于在界面上更加简洁,所以同样可以在同样的屏幕中,获取到更多的信息。

d2b5ca33bd970f64a6301fa75ae2eb22 38
text

使用 Taro 的一些小配置

我自己在使用 Taro 开发小程序的时候,一定会开启的两个配置:

1. 开启压缩

Taro 在预览模式生成的文档比较大,对于小程序的预览来说,不是很友好,所以开发的时候,我经常会打开压缩,以便于在小程序开发者工具进行预览。

修改也比较简单,只需要在你的 package.json 当中加入对应的环境变量 NODE_ENV=production 来支持即可。

diff 见下

d2b5ca33bd970f64a6301fa75ae2eb22 34

2. 开启 webpack 持久化缓存

Taro 可以开启 webpack 的持久化缓存,以便加速 webpack 的构建速度,因此我也会开启这个配置,以提升构建的速度。

d2b5ca33bd970f64a6301fa75ae2eb22 35
APILetter

开发者文档的三种内容层次:字典、概览和教程

不知道,你有没有关注过开发者文档的信息结构。下图是 Google Docs 的 API 文档。

89ug0i

在菜单中 Google Docs 提供了 Home、Guides、Reference、Samples、Support。你有没有想过,这些内容为什么如何设计?在我看来,这是开发者文档内容的三种层次。

第一层:解决有没有的问题

典型代表:API Reference

绝大多数开放平台也好,To Developer 产品也罢,首先要解决的问题是让开发者知道我有什么样的能力。而最简单、最直接最高效的方式,便是 API Reference。

各大产品在进行对外开放时,首先准备的便是一套 API Reference。在 API Reference 当中,你可以看到开发者应该如何调用一个 API;某个 API 的入参是什么、出参是什么;如果出现了失败,应该如何处理这些错误。

当一个 API Reference 为开发者提供了简洁、明确、易懂的文档内容之后,开发者基本就可以准备开始进入开发流程了。

Opinionated Notes:RESTFul API 的 API Reference 理论上不应该存在多层级的资源关系。

优秀的 API Reference 参考:

第二层:解决怎么用的问题

典型代表:API Overview、单个业务领域的 API Guides & API Tutorial

作为开放平台,当完成了 API Reference 之后,便算是及格了。但及格不应该是目标,目标应当是卓越。抵达卓越,则需要更多内容的帮助。

API Reference 回答了「能用什么」的问题,但没有解决「如何使用 API」的问题,特别是复杂的业务场景下,同一个业务内可能会需要多个不同的资源来进行 API 的操作,这个时候,如何使用这些资源,并将这些资源包装、关联起来,就成为极具业务属性的 Know How 知识。

举个例子来说,以服务端 OpenAPI 为例,日历的资源就涉及到用户、日历、日程、会议室、参会人、权限管控等多个不同的资源,如何串联这些 API 才能达成你的业务目标,就成为一个非常重要的说明。对于日历的能力熟悉的开发者可能很快就可以找到调通的方式,但对于不够熟悉的,这些 API 之间的串联,可能需要他花费一天,甚至一周的时间才能真正理解背后的含义。如果想要降低开发者调通过个 API 的时间,那么介绍这些不同 API 之间的关系,其在业务系统之中的含义就尤为重要。

如果你对于服务端的 API 感触不深,那么客户端的 API 可能会让你感受更加深刻,以微信小程序的中的蓝牙设备开发为例,如果你作为一个从未开发过蓝牙能力的开发者,看到了蓝牙提供的这一系列 API,只怕马上头皮发麻,需要每一个都看一遍才能理解其含义和价值;在蓝牙这件事上阻塞个半周一周也是常态。

d2b5ca33bd970f64a6301fa75ae2eb22 31

一个好的 API Guide / Tutorial 可以有效的降低开发者在调试这些 API 过程中所耗费的时间。当然,微信也意识到了这个问题,提供了一套非常好的 API 说明

优秀的 Tutorials 参考

第三层:解决场景化使用的问题

典型案例:Code Sample,Demo App,场景化的使用教程

API Tutorials 可以解决的是面向场景化的单个领域的内容。它帮助开发者很好的解决了一个领域内的产品能力的串联。但在实际的业务场景当中,开发者面对的不是已经拆解到 API 粒度的任务,而是面向场景设计的不同问题。对于开发者来说,把场景拆分成 X 个 API 并没那么容易(特别是如果经验不足的时候),因此,一套标准的 Code Sample ,甚至是 Demo App ,都可以帮助开发者快速解决面向场景化的问题。它可能涉及到了客户端、服务端,或者是面向了多个不同的领域。这些内容帮助开发者可以快速 landing。

以飞书开放平台为例,飞书开放平台在开发教程里提供了多个领域、不同方向的场景化教程(虽然还不够多,远远不够),帮助开发者快速完成某个特定领域的功能的开发。

d2b5ca33bd970f64a6301fa75ae2eb22 32

这些内容可能未必能完全满足开发者的需求,但确实可以给开发者一定的借鉴意义,帮助开发者理解整个业务流程和对应场景下的具体的实现,帮助开发者快速完成业务需求。

第三层之后,应该是什么?

前面三层解决了有什么、怎么用的问题,而在整个应用的生命周期当中,怎么用并不是终极问题。下一步需要解决的是如何用好的问题。不过这个问题不在本次阐述的内容当中,所以我们留一个悬念,下次再说。