标签归档:开发经验

给你的 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);
  };
})();

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

linux terminal

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

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

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

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

EDITOR="zed --wait" rails credentials:edit

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

export EDITOR="zed --wait"

其他编辑器的命令参考

# Sublime Text
export EDITOR="subl --wait"
# VSCode
export EDITOR="code --wait"
text

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

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

为什么需要错误码?

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

避免错误码数量过少

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

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

避免错误码数量过多

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

换一种思路来组织错误码

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

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

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

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

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

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

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

总结

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

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

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

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

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

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

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

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

想要使用,也不复杂,只需要使用对应的 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);

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

man in black jacket sitting on white chair

走正道,可能有点慢,但更安全

我在开发 ChatGPT-Feishu 这个项目的时候,并没有选择使用网页版的 chat.openai.com 的服务来进行 Hack。我在社区里看到大量通过 Hack 的方式,来提供了 ChatGPT 的 API 能力。

不可否认,网页版的能力是要比其通过 API 的方式提供的产品能力更强(毕竟模型更新),但对于一个成型的产品而言,稳定是远比能力更强更重要的。能力更强,我们可以通过一些技术手段来实现 — 比如通过数据库来实现多轮对话,可能功能不够强,但确是一个明确可以演进的方向。

Hack 官方未开放的 API ,虽然可以使用,但也带来了极高的维护成本。你可能需要不停的去更新和维护未开放的 API 的实现方式,与官方斗智斗勇。

作为一个 Side Project,没必要做的那么累。

black and white penguin toy

如何处理 Github Action 报出的 remote: Permission to xx x denied to github-actions[bot] 问题

在帮 @MikeyWei 搭建 Beyond-the-World 的网站时,他希望我能够实现 Hexo 自动的部署能力,这样作为一个写作者,他可以只关注于写作(只需要复制粘贴 Markdown)本身,不需要去处理 Hexo 本身的配置问题。所以我便借助 GitHub Action 来实现自动部署,帮助他实现想要的目标。

不过,在执行过程中遇到了一个问题,Github 提示 remote: Permission to xx x denied to github-actions[bot]

GitHub 提示

但我使用的是 Github 默认的 Secret Token ,并没有手动配置,所以并没有发现有配置权限的地方。而过去同样方式又是可以的,所以可能的问题便是 Github 提供了新的配置,导致这个 Token 默认没有权限了。

我的配置文件

仔细研究后发现, Github 的确是新增了一个配置,在项目的 Settings – Actions – General 当中,新增了一个选项 workflow permissions。你可以通过直接修改这个选项,来为你的 Flow 提供默认的读写权限。

不过,由于我不是组织的管理员,所以没办法设置,所以我选择了另外一个方式,在 Action 的 yaml 中添加了 permission 的描述,为这个 Job 新增了写权限,默认的 Token 便拥有了写权限。

延展阅读

https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token

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 当中

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

解决方案

1. 获取具体的 Path

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

2. 找到 Bin 目录

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

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

3. 修改 PATH 环境变量

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

4. 验证配置

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

person using iMac

使用 Github 作为 Logseq 的数据同步

继之前体验 Obsidian ,如今我在使用 Logseq 作为我的日常信息记录:

  1. 有想法就放在 Journal 当中,并通过大纲的方式,让我的想法逐渐变得丰满。
  2. 使用 TAG 来区分不同的内容分类(比如编程、生活之类的)

在使用 Logseq 的时候,必然会涉及到需要做数据同步的问题 —— 没有同步万一跪了怎么办?

好在是 Logseq 提供了 Git 版本控制的能力,你只需要在设置当中开启 Git Commit 的能力,就可以让其自动使用 Git 来添加版本,从而实现将你的变更通过 Git 本身的能力来记录。

Logseq 自带的 Git 功能

当完成了 Logseq 的 Git 初始化后,自然而然的,我们便会想 —— 我能不能将其上传到 GitHub 上来完成存储?即使不分发协作,也可以很好的用来存储。答案当然是可以的,配置版本控制之后,Logseq 的仓库就是一个标准的 Git 仓库,你直接推送即可。

当发现可以推送之后,也就不担心数据的版本化问题了。那随之而来的便是 —— 我如何做数据同步?如何不让我手动上传数据到 Github 当中?

你可以借助 Git 的 Hooks 机制来完成:Github 上的开发者 CharlesChiuGit 有一个项目 Logseq Git Sync 101,其中介绍了如何实现自动的 Git 同步。

你只需将其仓库中的 Pre-commit 和 Post Commit 两个文件放置在 Logseq 目录下的 .git/hooks 目录中,即可借助 Git 自身的 Hook 能力,实现在 Commit 前主动拉取配置,避免出现数据冲突的问题,并在 Commit 之后自动推送结果,实现数据的及时上 Github。

具体操作也不复杂,只需要在 Logseq 根目录的 .git/hooks 目录下创建 pre-commitpost-commit文件即可;随后,将 Logseq-Git-Sync-101 中的文件内容复制到这两个文件中;最后执行 chmod a+x post-commit pre-commit 来实现给其添加可执行权限,即可实现在 Logseq 当中执行操作提前推送一次更新 & 拉取一次内容。

tree 效果

有了 Git Sync 101 ,我几乎可以不用担心同步数据了 —— 毕竟做研发的人,谁电脑上还能没有个 Git 了?

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'

为什么不是 qs

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

待解决问题

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

使用 Taro 的一些小配置

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

1. 开启压缩

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

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

diff 见下

2. 开启 webpack 持久化缓存

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