作者: Zeeko

  • 一款人体工学键盘,但是免费

    对于数字时代的打工人来说,人体工程学键盘更像是劳保用品,可以在一定程度上缓解长期使用键盘带来的手部不适。这篇文章中,我将向你介绍如何把手边的键盘变得更加“符合人体工学”。

    不标准的指法

    标准指法其实很不科学,按照标准指法摆放双手会让手腕呈现出一种非常怪异的姿势,我个人建议不要使用标准指法,除非你在使用类似 XBows 等专门为标准指法优化的人体工程学键盘。

    标准指法示例
    配合标准指法的手掌摆放示意

    下面的图片可能是很多人目前正在使用的指法,使用这种指法已经比标准指法要“人体工程学”很多了,至少在按键的时候不用扭曲左手手腕:

    每根手指负责按一种颜色的按键,食指分别放在 F 跟 J 上

    总之,我建议你不要迷信标准指法,可以根据自身条件在标准指法上改造,避免以扭曲的姿势使用键盘。

    用大拇指分担小拇指的功能

    如果你仔细观察,你会发现键盘的并非是按照字母区的中心对称排布的,这意味着我们的右手小拇指需要承担更多的功能,对于程序员来说更是如此,在代码中经常会用到的各种符号也更加集中在右手小拇指区:

    常见的 87 键键盘,很多笔记本电脑采用类似的布局

    同样的,如果我们经常要用到位于数字区的符号,也得让小拇指配合按住 Shift 键帮助输入。这样看来,力气最小的小拇指除了需要托住手机之外,还得频繁地用来打字,是时候给小拇指减负了。

    根据我的个人经验,大拇指力气很大,但用处很少(只用来痛快地敲击空格键),很适合用来为小拇指分担工作。如果你并非天赋异禀,那么你的大拇指能够轻松按到的按键应该是 alt 键跟空格键。空格键的主要用途是输入单个空格,或者把输入法的候选词提交到输入框中。alt 键通常用作修饰键,只在按组合键的时候才会用到,不过据我观察,大多数人只会用到一个 alt 键,另一个 alt 键往往常年吃灰。

    使用 Kmonad 打造免费人体工程学键盘

    把这两个按键利用起来,作为符号输入的组合键,就可以有效地把小拇指的工作分给有力的大拇指。想要完美地实现这个功能,通常需要依赖具有可编程功能的键盘。Kmonad 就是一款用来给“任意键盘”编程的开源软件,它支持 Windows、Linux、macOS 这些主流的操作系统,允许用户使用类似 Lisp 的配置语法给自己的键盘编程。你可以查看 Kmonad 的项目说明来学习如何使用这款软件,在这里我会分享我的“人体工程学”键盘配置思路。

    Kmonad 中最常用的功能是“层”,每一层下的键盘按键功能都可以自定义,每层中可以设置一些按键用来切换键盘当前所在层,只要你的键盘按键足够多,你可以为创建无限的按键层。要实现上面描述的字母区输入符号的功能,就需要单独创建一个符号层,然后通过空格键或者 alt 键切换。

    对于我个人而言,我几乎用不到长按空格键,所以我就把“按住空格超过 200 毫秒”作为临时切换到符号层的按键操作。

    按住空格键激活的符号输入层

    我的符号层除了安排了符号之外,还安排了一些功能键(回车键、退格键以及方向键)跟按键宏(在 F 键位上的 =>),主要是为了减少右手小拇指的移动距离,避免扭曲手腕。可以看到就算把方向键也挪到了字母区,还是留了不少空位,之后可以根据自己的习惯再把空位填满。

    其实除了空格键很少会被长按之外,数字区的按键也很少被长按,本着物尽其用的原则,我把长按数字区超过 300 ms 设置成了的对应的符号,这样既可以避免使用小拇指按住 Shift 键,又可以保留输入符号的按键位置习惯。

    添加快捷键层

    为了提高一些软件的使用效率,我们或多或少都会用到一些快捷键,但是一些快捷键的键位设计可能跟我们的生理结构相违背(你可以试试用一只手按 ctrl+shift+t)。这些快捷键在提高效率的同时,也在折磨我们的手指关节,因此,一些具有人体工程学结构的键盘,会把修饰键放在离大拇指比较近位置,以减少修饰键到字母区的平均距离,避免移动手掌造成手指关节的扭曲。

    上面提到,在普通的键盘上,空格键跟 alt 键都是大拇指容易触及的常用按键,我已经把长按空格键分配给了符号层,所以就用右侧的 alt 键作为新的“快捷键层”的切换入口。

    按住右侧 alt 键激活的快捷键层

    其他的小技巧

    我还搜集了一些其他的“键盘小妙招”,同样可以帮助你减少按键带来的手掌移动,保护你的手腕,这些功能经过我的验证都可以通过 Kmonad 实现。

    • 交换 Ctrl 跟 CapsLock
    • Auto Shift,长按字母键输入对应的大写字符,就跟手机上的虚拟键盘一样
    • Shift 粘滞键,单击 Shift 后按下其他按键就可以实现按住 Shift 的组合键输入
    • Tap Dance,给同一个按键设置“单击”、“双击”以及对应击键次数后长按的功能,例如,单击输入 Esc 双击输入 CapsLock、单击长按切换按键层。

    最后,其实我也不太确定按照这篇文章改造的键盘是否真的能够降低手部劳损的速度,至少在我个人的体验上,经过这样的一番改造的确减少了很多需要扭曲手腕的按键场景,对手部的疲劳有一定的缓解作用。如果你正被类似“键盘手” 、“鼠标手”的症状困扰,还是建议先去正规医院检查治疗,这篇文章介绍的方法最多只能作为一个低成本的辅助手段。


    我的 Kmonad 键盘配置

  • 2022 年,我的 Wayland 工作电脑设置

    Wayland 比 Xorg 要快很多,在没有独立显卡的笔记本电脑上对桌面动画性能提升还是很明显的,忍受不了卡顿掉帧的桌面动画,我决定切换到 Wayland 。

    DE

    试用了各种 DE、WM 后,我的最终选择是 KDE,KDE 支持为不同分辨率的屏幕设置不同的 DPI,并且能够清晰地在不同的屏幕上缩放 XWayland 应用,使之适配多个分辨率不同的显示器。

    只有真正去折腾的人才知道,让 Linux 桌面适配多个分辨率差异很大的屏幕有多痛苦,KDE Wayland 能够以最小的代价实现让人满意的效果。

    在 XOrg 下,为了实现为多块屏幕设置不同分数缩放,我使用的是全局设置两倍缩放,然后通过 xrandr 压缩到合适分辨率的做法,这种方式比较消耗性能,但还在可以勉强忍受的范围内。

    Wayland 下的 GNOME 虽然支持为不同显示器设置不用的缩放系数,但是分数缩放的支持比较差,使用 XWayland 渲染的软件会先按照 1 倍渲染,然后被系统强制放大到指定的倍率,其效果可想而知,满脸的马赛克锯齿。

    Wayland 下的 KDE 就厉害多了,可以直接在系统设置中针对不同的屏幕设置分数缩放,而且性能还很不错,最重要的是,KDE 会自动管理那些不支持 Wayland 的软件的缩放系数,最终可以让我们得到非常 Nice 的显示效果。

    左侧是笔记本内置屏幕(1920×1080, 14″,150%)上的 KDE 系统设置,使用原生 Wayland 渲染,右边是外接显示器上(3840×2160, 24″,200%)基于 Chromium 的 Obsidian,使用 XWayland 渲染。

    打工人必备

    飞书 XWayland

    Wayland 下的飞书跟 XOrg 下的使用体验没有太大区别,主要是 Wayland 下共享桌面需要一些额外的设置,可以参考下面的做法:

    1. 启用 Chromium 的一个 flag: enable-webrtc-pipewire-capturer
    2. 安装 xdg-desktop-portal-kde

    这种做法对于其他的基于 Chromium 的软件应该同样使用,其中的原理可以查看这篇文章

    截图与录屏

    截图工具用的是 Flameshot,虽然对多显示器的支持比较早期,但是用的顺手。KDE 自带的 spectacle 能用,但在我的电脑连接外置显示器的时候比较卡,而且不太符合我的使用习惯。

    录屏比较麻烦,我之前用的 SimpeScreenRecorder 并不支持 Wayland,找了一圈发现很多带图形界面的录屏软件在 KDE Wayland 下要么卡顿,要么功能上有 Bug。还有 Sway 用户推荐的各种命令行工具,感觉用起来会很不方便。最终我还是决定用 OBS 来录屏,属于是用牛刀来杀鸡了,但是至少稳定高效。

    输入法

    对于 KDE 用户来说,直接安装 fcitx5 应该就可以开箱即用了,如果发现输入法候选框没有在有些应用中出现,可以参考这里的说明试试。

    开发必备

    JetBrains 系列

    JetBrains 所有基于 Idea 的软件以及 Fleet 目前都只能在 XWayland 下运行,不过就我目前的使用体验来看,没有遇到任何阻断性的问题。

    WebStorm 2022
    Fleet

    KDE 会为 Fleet 设置 1.5 倍缩放(跟我设置的全局缩放一致),但实际上 Fleet 的 UI 库似乎会跟随全局缩放自动调整,所以得手动在 Fleet 中把缩放系数改为 75%,把 KDE 设置的缩放抵消掉。

    终端模拟器

    虽然用的 KDE,但是我没有选择使用 Konsole 作为终端模拟器,在我看来,响应更加迅速、跟 Shell 融和地更好的 kitty 更有吸引力。如果你发现无法在 kitty 窗口中调用 fcitx5,可以参考这里

    设置全局唤起 kitty 的快捷键需要一点 tricks,KDE Wayland 把设置自定义快捷键的设置面板换掉了,需要通过执行 QT_QPA_PLATFORM=xcb systemsettings 找回旧的快捷键设置面板。

  • 工欲善其事,如何利其器?CLI 篇

    工欲善其事,必先利其器,这篇文章将从 CLI 工具这个方面向你介绍“如何提高在 CLI 环境下的工作效率”。

    Shell

    Shell 是一个用来提供与操作系统内核(Kernel)交互的软件。

    常见的 Shell 分为两种:

    • Command Line Interface (CLI) 命令行界面
    • Graphic User Interface (GUI) 图形用户界面

    通常情况下,Shell 主要是指 CLI 程序,常见的 CLI shell 有 Bash、Zsh、Fish、PowerShell、Cmd 等。

    为什么会有 CLI Shell ?

    早期的计算机主机存放在专门的机房中,用户想要操作计算机需要通过终端(Terminal)连接,终端就是计算机的输入输出设备。最早的终端无法显示图像,只能通过屏幕跟键盘展示、输入字符,所以最早的 Shell 软件就以 CLI 的形式问世了。

    随着技术的进步发展,只能用来传输文本的终端设备几乎都被显示器替代了,GUI 取代了 CLI 的在人机交互方面的主要地位。

    CLI 优势

    CLI 最大的优势在于可编程,CLI shell 通常也是一个脚本语言解释器,可以通过编写 Shell 脚本来自动化完成一些复杂、重复的操作,这是 GUI 难以比拟的。

    在 GUI 成为主要人机交互界面的今天,终端模拟器(Terminal Emulator)是我们操作 CLI 的必备工具。一个优秀的终端模拟器通常会提供下面几个功能:

    • 多 Tab 切换
    • 窗口分屏
    • 搜索命令行输出(ScrollBack)
    • 自动识别链接

    这些功能也是提高 CLI 使用效率的必备功能,如果你的终端模拟器不支持这些功能,我推荐你使用下面的这些软件:

    • Windows
      • Windows Terminal
      • ConEmu
    • macOS
      • iTerm2
    • Linux
      • Konsole
      • Kitty

    我非常建议你设置 Tab 切换跟窗口分屏快捷键,这些操作可以极大地提升你使用 CLI 时的工作效率。

    设置一个用户友好的 CLI Shell

    Fish Shell 是一个智能的、用户友好的 Shell 程序。相比 Bash,它有如下优点:

    • 自动补全
    • 简单易学的脚本语法
    • 基于 Man 的智能提示与补全
    • 开箱即用,只要安装上就能够获得生产力的提升

    安装 Fish

    不建议将默认 Shell 修改为 Fish,因为 Fish 不遵循 POSIX Shell 规范,不一定跟其他的程序兼容,更好的做法是将兼容 POSIX 的 Shell 作为默认 Shell,然后在终端模拟器中将默认启动的 Shell 设置为 Fish。

    CLI 好伙伴

    • man CLI 程序说明书
    • bat 文本文件查看工具
    • tldr man 手册太长不看,就看 TLDR
    • fd 文件搜索工具
    • fuck 智能的命令行纠错工具
    • exa 更加友好的 ls 替代品

    进一步提升 Fish 的易用性

    • Fisher Fish 插件管理器
    • fzf fzf 命令行模糊搜索程序
    • z 智能的目录跳转工具
    • starship 高性能的 Shell 提示符

    fzf

    通过 fisher 安装 PatrickF1/fzf.fish

    • ctrl+r 搜索历史
    • ctrl+alt+f 从当前目录开始搜索文件,按 Tab 可以多选

    更多快捷键可以查看 GitHub Readme

    常用快捷键

    function fish_user_key_bindings
        # 使用 Ctrl + F 补全整行
        bind -M insert \cf accept-autosuggestion
        bind \cf accept-autosuggestion
    end
    • alt+. 将上个命令的最后一部分填到当前命令行末尾
    • alt+s 用 sudo 执行上条命令

    更多快捷键请查看文档

    私人订制,打造合适自己的 CLI 工具

    alias 给常用的命令设置别名,方便快速输入。

    funced/funcsave/function function 比 alias 更加完善,可以用来封装更加复杂的操作。

  • 如何使用 nx 缓存构建产物

    This entry is part 3 of 4 in the series Nx Monorepo Experience

    There are only two hard things in Computer Science: cache invalidation and naming things.

    Phil Karlton

    nx 的缓存机制如何工作

    How Caching Works 这篇文章中非常详细地说明了 nx 缓存构建结果的机制:
    nx 会计算当前执行的 target 的 Hash 作为 cache key,在 target 执行完成后把构建输出(包括终端输出、构建结果文件)作为缓存内容存储起来。

    通过配置 project.json 中 target 的 inputs 选项,我们可以调整能够让缓存失效的文件、环境变量,具体的配置方法可以查看这里

    在编写文件 glob 模式的时候,有两个特殊的变量:

    • {projectRoot} :当前项目的根目录
    • {workspaceRoot}:nx 工作区的根目录

    然而不管是在 inputs 还是 outputs 配置中,路径都是相对于 workspace root 的,所以 {workspaceRoot} 变量基本上没啥作用。

    如何为项目添加构建缓存

    缓存失效一直都是计算机领域的一个难题,在配置 nx 构建缓存的时候,我有以下两点建议:

    • inputs 配置应该宁滥毋缺,因为缓存不同步的严重性远大于缓存命中率低
    • outputs 配置必须不多不少,避免无关文件被错误地覆盖或者构建结果缺失

    要做到上面两点需要精确地控制 inputs 跟 outputs 的文件匹配模式,这里推荐几种帮你减少工作量方法:

    在拆分模块需要注意,尽量采用类似 Module Federation 或者 Webpack DLLPlugin 等机制,这种构建方式能够真正实现分模块打包。如果没有采用类似的机制,所有的模块都会在入口模块处被重新处理,所以除了入口模块之外,其他的模块没必要 minify ,这样可以加快打包速度。

    共享缓存执行结果

    nx 缓存默认存储在本地,独乐乐不如众乐乐,通过添加 nx-remotecache-minio,我们可以把 nx 缓存存储在任意与 S3 兼容的对象存储服务器上。如此,只要项目中的任意一位成员甚至 CI 服务器构建过某个模块,其他人都可以跳过构建直接从远程服务器拉取构建结果。

    需要注意的是,nx-remotecache-minio 没有处理访问对象存储超时的情况,所以一旦遇到对象存储服务器不可用的情况,记得及时终止构建,并将 NX_CACHE_MINIO_URL 环境变量设置成一个无法解析的地址,避免构建命令卡住。

  • 拒绝重复劳动,自动为项目添加 target

    This entry is part 2 of 4 in the series Nx Monorepo Experience

    完成了自制的插件过后,我们得往需要使用 lingui 的项目中添加相关的 target,项目少还好说,项目一多,这就变成纯体力劳动了。还好, nx 提供了 Project Inference 机制,给插件加上几行代码,就可以让 nx 自动为项目添加合适的 target。

    与 executor 不同,project inference 功能需要在 nx.json 中注册:

    {
      "plugins": [
        "my-nx-plugin"
      ]
    }

    Project inference 功能由 my-nx-plugin 模块默认导出的两个变量来实现:

    • projectFilePatterns 主要用来识别项目文件,项目目录中匹配的文件会作为参数传递给 registerProjectTargets
    • registerProjectTargets 是一个用来根据项目文件推断 targets 的函数,它返回一个 Record<string, TargetConfiguration>,即我们在 project.json/targets 中编写的内容。

    一个给项目添加 lingui target 的实现如下:

    import { workspaceRoot } from '@nrwl/devkit';
    import * as path from 'path';  
    import * as fs from 'fs';
    import type { TargetConfiguration } from '@nrwl/devkit';
    
    export const projectFilePatterns = ['package.json'];
    
    export function registerProjectTargets(projectFilePath: string): Record<string, TargetConfiguration> {
     // 通过导入的 workspaceRoot 变量来获取当前 nx workspace 的根目录,这样可以将 projectFilePath 转换为绝对路径
     const projectRoot = path.join(workspaceRoot, path.dirname(projectFilePath));  
     return { 
       ...linguiTargets(projectRoot),  
     };  
    }
    
    function linguiTargets(projectRoot) {
      const packageJSON = fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8');
      // 只要项目依赖了 lingui,就给它添加 lingui target
      if (packageJSON.indexOf('lingui') > 0) {
        return {
          lingui: {
            executor: 'nx-plugin:lingui',
            options: {
              locales: path.join(projectRoot, 'locales'),
            }
          }
        }
      }
    }

    在实现这个功能的时候需要注意几点 registerProjectTargets 必须是个同步函数,所以不能使用任何异步 API,也没法用之前提到的方法来引用 ESM。另外,nx 出于性能考量,会缓存 registerProjectTargets 的结果,所以在 Debug 的时候,一定要记得设置环境变量 NX_CACHE_PROJECT_GRAPH=false

    在处理文件路径的时候,建议尽量使用绝对路径,使用相对路径需要思考是相对于 workspace root 还是 cwd,用绝对路径可以减少这方面的心智负担。

    虽然功能实现了,但是现在还不能立马使用,需要将插件源码转译成 CJS 代码。前面的文章提到过用 postinstall 来编译自定义 nx 插件,在实现了今天的功能后,编译自定义插件的命令必须不能用 nx 来执行,因为 nx 执行任何命令都会加载插件模块,如果我们的插件没有编译成 CJS,通过 nx 调用的编译插件命令也会失败。

    Project Inference 功能非常强大,可以很大地减少我们维护项目 targets 的负担,在我看来,这算得上 nx 的旗舰功能了,后面我也会再演示一些其他使用 Project Reference 的典型场景。

  • 使用这个技巧,再也不会搞乱 Git 用户信息了

    在使用 GitHub 的时候,经常在公司帐户跟个人帐户之间切换,难免会造成这样的困扰:个人项目中的代码不小心用公司帐户提交了。

    Commit with wrong git user

    等到发现的时候,才发现已经来不及修改了——代码已经被推送到了远程仓库,强行修改历史会带来很多冲突。

    经过一番寻找,发现了这个答案,原来 Git 2.13 引入了一项新功能“Conditional Includes”,可以实现对不同路径下的仓库引用不同 gitconfig 的功能。

    我习惯把公司项目放在 ~/projects/company 目录下,个人项目随缘摆放。所以我给 ~/.gitconfig 添加了如下配置:

    [includeIf "gitdir:company/**"]
     path = ~/.gitconfig.company
    • gitdir: 表示按照 .git 文件夹的路径来匹配 gitconfig
    • company/** 是一个 glob 表达式,在 includeIf 中会自动展开为 **/company/**/.git,用来匹配 /any/path/to/company/any/project/.git
    • path 则是指要引用的 gitconfig 的路径,被引用的文件内容会覆盖 ~/.gitconfig 的配置,从而拥有更高的优先级。

    接着,只需要在 ~/.gitconfig.company 添加公司帐户的相关信息就好了:

    [user]  
     name = your-name
     email = your-email

    掌握了这个小技巧,再也不会用错误的帐户提交正确的代码了。

  • 年轻人的第一个 nx 插件

    This entry is part 1 of 4 in the series Nx Monorepo Experience

    我们的项目中使用 lingui 作为 i18n 方案,在之前的实践中,每个项目都在 package.json 中维护了调用 lingui 提取 i18n 文本的命令,除此之外,在每个项目的 CI 中也包含了检查 i18n 文本是否存在遗漏的脚本。

    把项目迁移到 nx 管理的 monorepo 后,就可以着手把各个项目中重复维护的 lingui 相关脚本交给 nx 插件来实现,减少维护 lingui 相关脚本的负担。

    添加 lingui executor

    lingui 并没有提供官方的 nx 插件,但我们自己在 monorepo 中创建一个 nx plugin 来实现相关的功能。一般来说,nx 插件能够提供三种能力:

    1. executors,用来执行工具命令,例如,编译、运行测试
    2. generators,用来创建模板文件
    3. 自动推断项目类型,可以根据项目类型自动地为项目添加相关的开发脚本

    对于 lingui 插件来说,只需要用到其中的 executor,在项目的 project.json 中添加 lingui executor,就可以通过 nx lingui 来调用 lingui cli

    借助 nx 提供的 nx-plugin 插件,可以很快的把实现这些功能的基本模板搭建起来。

    实现 Lingui Executor

    Lingui Executor 的内容非常简单,只需要调用 lingui cli 就行了。这里我推荐使用 zx 来执行命令,zx 开箱即用,帮你处理好了调用命令行程序的一切工作,包括参数解析、命令行输出捕获、glob 匹配。唯一需要注意的是,zx 目前只提供 ESM 模块,而 nx 暂时无法调用 ESM 模块插件。为了兼容这两者之间的模块格式,我们的插件只能编译为 CJS 模块,通过 import('zx') 来引用 zx 中的函数。方便起见,我封装了 zx 中最常用的 $ 函数,使之可以自然地在 CJS 中使用:

    import { logger } from '@nrwl/devkit';
    export async function $$(template: TemplateStringsArray, ...args: any) {  
     const { $ } = await import('zx');  
     try {
       // 强制终端彩色输出
       $.env.FORCE_COLOR = '1';  
       // 关闭命令回显
       $.verbose = false;  
       if ($$.cwd) {  
         $.cwd = $$.cwd;  
       }  
       const process = $(template, ...args);  
       process.stdout.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       process.stderr.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       return await process;  
     } catch (e) {  
       logger.error(e.stdout);  
       logger.error(e.stderr);  
       throw e;  
     }  
    }  
      
    $$.cwd = undefined as string | undefined;

    需要注意的是,zx 执行命令的 stdio 必须通过 @nrwl/devkit 输出到终端,否则 nx 的缓存机制将无法取得 executor 执行过程中产生的命令行输出内容。

    export default async function runExecutor(options: ILinguiExecutorSchema, context: ExecutorContext) {    
     if (!options.localesDir) {  
       logger.error('options.localesDir is required');  
       return { success: false };  
     }  
     const localesDirPath = path.resolve(context.root, options.localesDir);  
     if (!context.projectName) {  
       return {  
         success: true,  
       };  
     }  
     const projectCwd = await getProjectCwd(context);  
     $$.cwd = projectCwd;
     try {
       await $$`yarn exec lingui extract --clean`;
     } catch (e) {
       return { success: false };
     } 
     return {  
       success: true,  
     };  
    }

    编译插件

    在运行 nx 命令时,nx 插件会被执行,所以需要在开发者调用任何 nx 命令之前就把本地项目中的插件编译成 nx 可以加载的 CJS 模块。我的做法是给本地插件加上一个 postinstall 脚本,这样在运行 yarn install 的时候,nx 插件就会被自动地编译,不需要手动干预。

    nx 默认生成的模板会使用 TypeScript 将代码编译为 CJS 模块,但 tsc 的编译速度比较慢,测试下来稍微有点影响实际的开发体验,我们可以用 swc 作为 tsc 的替代品,编译速度会快上很多。

    另一个用来编译插件的选项是 esbuild,但 esbuild 的目标并不是替代 tsc,在某些行为上跟 tsc 有不少差异,需要对项目做很多改动,才能适配 nx 执行 executor 的机制。对于 nx 插件来说,executor 跟 generator 都是插件模块的入口(entry),而 esbuild 是设计来将代码打包成一个 Bundle 的。

    最后

    给自己的项目开发 nx 插件是一件非常简单的事情,但是由于 nx 加载插件的限制,需要注意插件的编译方式以及 ESM 的引用方式。

  • 修改 WordPress 主题默认字体

    我很喜欢 WordPress 2022 主题的设计,但是这款主题默认使用的是衬线字体,我看着很不舒服,经过断断续续的折腾,终于把找到了最合适的方法来修改这款主题的默认字体。

    TL;DR

    在不使用第三方插件的前提下,为了让主题的修改在展示页面跟编辑页面同时生效,必须要借助主题文件编辑器。打开编辑器,选中 WordPress 2022 主题,在右边找到 theme.json

    Theme File Editor in Menu
    theme.json

    接着将文件中所有的 var(--wp--preset--font-family--source-serif-pro) 替换为 var(--wp-preset--font-family--system-font) 即可。system-font 默认使用的就是用户操作系统的非衬线字体。

    为什么不用自定义 CSS?自定义 CSS 需要同时覆盖展示页面跟管理页面的样式,而且还需要针对不同的 Block 单独编写样式,太麻烦了,直接编辑主题文件可以在所有看得到的地方起效。

  • 让 v2ray 透明代理的 DNS 更聪明一些

    透明代理(TPROXY) | 新 V2Ray 白话文指南 中,作者通过 v2ray 内置的 DNS 功能实现 DNS 查询分流的功能,但 v2ray 内置的 DNS 功能并不完善,实际体验下来速度并不快,于是参考其他网友的文章,试了一下用 SmartDNS 来接管全局网络的 DNS。

    SmartDNS 是一个运行在本地的 DNS 服务器软件,它支持缓存、分流、测速等功能,可以极大地提高 DNS 使用体验。SmartDNS 提供了 OpenWRT 的预编译包,参考官方文档就可以很容易的在 OpenWRT 上完成安装配置。

    启动 SmartDNS

    在 v2ray 的 TProxy 的网络环境下,v2ray 会接管来自局域网的 DNS 查询请求,为了让 SmartDNS 能够正常工作,需要修改之前设置的 iptables 规则,让来自 53 端口的 UDP 请求不再被转发到 v2ray:

    - iptables -t mangle -A V2RAY -d 192.168.0.0/16 -p udp ! --dport 53 -j RETURN # 直连局域网,53 端口除外(因为要使用 V2Ray 的 DNS)
    + iptables -t mangle -A V2RAY -d 192.168.0.0/16 -p udp -j RETURN # 直连局域网

    再添加一些上游 DNS 服务器地址:

    # 国内域名服务器,设置分组名为 cn
    server-tls 233.5.5.5  -group cn
    # 国外域名服务器,使用默认分组 default
    server-tls 1.1.1.1
    server-tls 8.8.8.8

    接着,设置 SmartDNS 监听 53 端口,并关闭 dnsmasq 服务:

    启用 SmartDNS

    如果一切顺利,你应该可以通过命令查询到 smartdns 这个域名:

    $ dog smartdns
    # A smartdns. 10m00s   192.168.68.4

    实现 DNS 查询分流

    为了能够在不造成 DNS 污染的情况下尽可能地使用国内 CDN,需要借助 dnsmasq-china-list 项目提供的域名列表实现查询分流。

    先 clone 这个项目到本地,然后执行下面的命令来生成 SmartDNS 规则:

    $ make smartdns-domain-rules SERVER=cn
    $ ls *.domain.smartdns.conf
    # accelerated-domains.china.domain.smartdns.conf  apple.china.domain.smartdns.conf  google.china.domain.smartdns.conf

    再把这些生成出来的文件复制到 OpenWRT 中,并修改 SmartDNS 的自定义设置:

    conf-file /etc/smartdns/conf.d/accelerated-domains.china.domain.smartdns.conf
    conf-file /etc/smartdns/conf.d/apple.china.domain.smartdns.conf
    conf-file /etc/smartdns/conf.d/bogus-nxdomain.china.smartdns.conf
    conf-file /etc/smartdns/conf.d/google.china.domain.smartdns.conf

    设置 v2ray 路由规则

    在完成 DNS 查询分流后,还需要更新一下 v2ray 的路由规则,让 v2ray 能够正确的把流量发送到正确的 outbound,以下是一个国内直连白名单策略的设置:

    {
      "outbounds": [
        {
          // 配置省略,请参考白话文
          "tag": "proxy"
        },
        {
          // 配置省略,请参考白话文
          "tag": "direct"
        }
      ],
      "routing": {
        // 优先按域名匹配规则,无匹配时再将域名解析为 ip 进行匹配
        "domainStrategy": "IPIfNonMatch",
        "rules": [
          {
            // 国外 DNS 需要代理
            "ip": [
              "8.8.8.8",
              "1.1.1.1"
            ],
            "outboundTag": "proxy",
            "type": "field"
          },
          {
            // 国内 DNS 必须直连
            "ip": [
              "223.5.5.5"
            ],
            "outboundTag": "direct",
            "type": "field"
          },
          {
            // 需要直连的域名白名单:国内域名、国内 CDN 等
            "domain": [
              "geosite:private",
              "geosite:apple-cn",
              "geosite:google-cn",
              "geosite:category-games@cn",
              "geosite:cn",
              "ntp.org",
              "domain:mi.com",
              "domain:ls.apple.com",
              // steam 优先选择国内 CDN,如果无效可以删掉
              "domain:cm.steampowered.com",
              "YOUR_VPS_ADDRESS"
            ],
            "outboundTag": "direct",
            "type": "field"
          },
          {
            // 需要直连的 IP 地址白名单:局域网地址、国内地址
            "ip": [
              "geoip:private",
              "geoip:cn",
              "YOUR_VPS_IP"
            ],
            "outboundTag": "direct",
            "type": "field"
          }
        ]
      }
    }
    

    这份配置文件中用到的 geosite.dat 来自 Loyalsoldier/v2ray-rules-dat,其中的国内域名列表也是基于上面的 dnsmasq-china-list 项目生成的。这样就可以保证 SmartDNS 解析出来的国内域名也一定会被 v2ray 发送到 direct outbound。

  • 什么时候应该避免 barrel import

    TL;DR
    不要从导出当前模块的 barrel file 导入其他模块。

    什么是 Barrel file

    用来重新导出当前目录下的子模块,缩短外部 import 路径,通常命名为 index.ts

    DO NOT

    上图存在循环依赖,如果 pluign-c 尝试在顶层访问 PluginB,有可能会得到 undefined,这取决于 plugin-b、plugin-c 在 barrel 中的顺序

    DO