作者: Zeeko

  • 为什么中国人均汽车保有量这么低?

    中国汽车保有量有多低?

    各国人均汽车拥有量列表

    2023 年统计数据:

    总人口 14.1亿 https://www.stats.gov.cn/xxgk/jd/sjjd2020/202401/t20240118_1946711.html

    总汽车 3.36 亿 https://www.gov.cn/lianbo/bumen/202401/content_6925362.htm

    每千人汽车数 238

    感觉上中国的人均汽车保有量不应该这么低

    收入太低导致汽车保有量低?

    对比分析几个国家的用车成本与人均可支配收入的关系

    • 用车成本:一辆车在燃料、税费、保养、维护上一年的支出,排除车辆折旧跟车贷
      • 为了方便跟美国的统计结果比较,年行驶里程假定为 24,000 公里
    • 人均可支配收入:一个人一年的税前总收入 – 税费
      • 美国是这么计算的,不太好确认中国跟南非是否也是如此计算
    美国中国南非
    千人汽车保有量868238232
    用车成本占人均可支配收入比例43.78%48.07% (燃油)
    13.95% (新能源)
    83.28%
    中国、美国、南非用车成本与人均可支配收入对比

    实际上,美国人每年平均驾驶里程数大概在1.4万英里(约2.25万公里);中国人每年平均驾驶里程大概在 1.5 万公里;南非人每年平均驾驶里程在 1.2 万公里左右 。考虑到这点对燃料的影响后,实际上中国的用车成本只会比美国更低。

    从这方面来看,用车成本似乎并不是导致中国人均汽车保有量较低的原因。

    中国用车成本与人均可支配收入

    数据来源:懂车帝排行榜最近一年(2024年8月)销量最高的10万元以下轿车:

    人均可支配收入数据:

    2023年1月17日,国家统计局发布数据显示,2022年全国居民人均可支配收入36,883元,比上年增长5.0%,扣除价格因素,实际增长2.9%。分城乡看,城镇居民人均可支配收入49,283元,增长3.9%,扣除价格因素,实际增长1.9%;农村居民人均可支配收入20,133元,增长6.3%,扣除价格因素,实际增长4.2%。

    中国居民收入的五档划分,从数据看差距_澎湃号·政务_澎湃新闻-The Paper

    中国用车成本占可支配收入比例:

    • 燃油车:17728/36883=48.07%
    • 新能源车:5116/36883=13.95%

    美国用车成本与人均可支配收入

    用车成本的数据来源(2023年)

    Based on AAA’s data, the average monthly cost of owning and operating a car is $1,015. The organization used six cost categories to determine their average: depreciation, finance costs, fuel, insurance, government taxes and fees, and maintenance, repair and tires. They based their numbers on vehicles that were driven for approximately 15,000 miles a year and assumed a five-year ownership period. Your own rate will likely differ from the average since it is based on factors unique to you, your car and your situation.

    The average cost of owning a car

    折旧费数据来源

    According to AAA, depreciation costs car owners an average of $3,334 annually.

    美国人均可支配收入(2023年):20,204.9 美元

    美国用车成本占可支配收入比例:

    1015*12-3334 / 20204.9 = 43.78%

    南非用车成本与人均可支配收入

    用车成本数据来源 The real cost of owning a standard R350,000 car in South Africa – BusinessTech

    每年用车成本(按24,000公里计算燃料消耗,排除车贷):

    (11167-7222+1150) * 12 = 61140 ZAR

    南非总可支配收入:4,449,450 百万 ZAR(2023)

    南非人口60,604,992(2022)

    南非人均可支配收入:4,449,450,000,000 / 60,604,992 = 73,417.76 ZAR

    用车成本占可支配收入比例:61140 / 73,417.76 = 83.28%

    中国人用车需求不足?

    美国人每年平均驾驶里程数大概在1.4万英里(约2.25万公里);中国人每年平均驾驶里程大概在 1.5 万公里;南非人每年平均驾驶里程在 1.2 万公里左右

    根据易车网的数据,大部分国人用车的高频场景是通勤,所以中国人对车辆的需求应该还算比较刚需的。

    中国人倾向使用共享出行?

    中国跟南非千人汽车保有量跟人均驾驶里程相差不太 ,南非在人均 GDP、人均可支配收入比中国低的情况下,而汽车保有量大,可能有共享出行性价比低的原因。

    南非公共交通并不发达,且网约车价格昂贵:

    由于南非基本没有像国内那样的公交系统,都是私人运营线路,当地人用小巴来跑这些线路,在南非这个词就叫 Taxi Bus

    可靠数据是南非 Uber 有 13000 个兼职或者专职司机(20 年数据),而中国滴滴是有 1166 万个兼职或专职司机(19 年数据)

    记得我 2015 年刚去南非没买车的时候,我为了去二手车店看车,打了个 Uber,区区 10 公里的路,打了 150 兰特,约 80 人民币,当时国内打车也就 20,30 的样子,当时真的让我觉得肉疼。后来赶紧自己买车买个几十公里也就是个油钱,也就很少打车了。

    杂谈南非打车

    中国则全然不同,公交覆盖广,而且网约车的普及率也高:

    城市交通出行大调查-36氪

    我认为大家倾向使用共享出行的方式的主要原因还是价格因素:

    • 城市公交都有补贴
    • 网约车定价超低:
      • 身边统计学,成都的网约车单价大概在2元/公里,按每年 1.2 万公里的出行里程来算,大概需要 2.4 万元;虽然比养车成本高,但如果算上自有车辆的折旧费、车贷,网约车可能跟自有车费用差不多甚至更便宜。

    结论

    中国共享出行比自有车辆成本更低,且覆盖了主要出行的上下班通勤场景,满足了当前阶段的出行需求,所以汽车保有量会保持在一个较低的位置。

    如果要让中国千人汽车保有量提升到 400?

    • 减少公交补贴
    • 网约车涨价
  • Migrate blog site to Hawk Host

    I migrated this blog site to Hawk Host last weekend, it doesn’t take much time, about 2~3 hours.

    • Create a new database and a database user for WordPress
    • Upload public_html with rsync FTP is too slow
      • rsync -avhP -e ssh ./public_html host-server:~/path/to/website/public_html
    • Update DNS records on CloudFlare

    My previous hosting service provider set up a mailer for me.

    After migration, the mail notification didn’t work, so I installed an SMTP plugin for WordPress.

    WP Mail SMTP by WPForms – The Most Popular SMTP and Email Log Plugin – WordPress plugin | WordPress.org

    Reference

  • DataStory SOW Model

    The problem

    In a workflow-style data processing system, if all the data sources are granted to complete, how do we ensure that the execution of a processing task can reach its complete state?

    Terms

    NameDescription
    Data StreamAn observable stream of async data emitted by something.
    SourceNode (Abbr. S)A node that creates a granted-to-complete Data Stream for the next node in the Flow, which is usually the start point of a data processing task.
    OperatorNode (Abbr. O)A node that observes the Data Stream from the previous node in the Flow, and creates a granted-to-complete Data Stream for the next node; which is usually used to transform data, fetch data from API using the incoming data, etc.
    WatcherNode (Abbr. W)A node that observes Data Stream from the previous node; which is usually used to inspect the data processing state, e.g. logging, and reporting progress.
    FlowA list consists of SourceNode, OperatorNode, and WatcherNode, it must start with SourceNode and end with WatcherNode (S, O….O, W) or completely consist of OperatorNode(O, O, …. O).
    DiagramA set of Flows

    Define the complement

    S

    The S creates a granted-to-complete data stream, when the data stream completes, S itself also reaches its complete state.

    The data stream S created is consumed by the next node (e.g., O, W) in the Flow list.

    type S<T> = DataStream<T>
    

    O

    In the Flow list, every incoming data from the previous node, makes O create a new granted-to-complete data stream for the next node. Therefore, O is state less, it doesn’t have a complete state, it propagates the complete state from the previous node to the next node.

    type O<A, B> = DataStream<A> => DataStream<B>
    

    As you can see, the O is a function that takes data stream in and out.

    W

    In the Flow list, W consumes data from the previous node, when the previous node comes to the complete state, the W reaches to its complete state.

    type W<T> = DataStream<T> => void
    

    Flow

    Stateful flow (S, O, …. O, W)

    If flow starts with a S and ends with a W, it is a stateful flow.

    When the last W, comes to a complete state, the Flow reaches to complete state.

    The complete state is propagated from the beginning S to the last W via many Os.

    type StatefullFlow<A, B> = {
      source: S<A>;
      operators: O[];
      watcher: W<B>;
    }
    

    Stateless flow (O, O, …. O, O)

    The stateless flow behaves like an O, it creates a granted-to-complete data stream based on the incoming data.

    type StatelessFlow = O[];
    

    Diagram

    When every flow in the Diagram completes, the diagram reaches a complete state, which means the current running data process task is completed.

    To have a granted-to-complete Diagram, we need to make sure:

    1. All S, O in the diagram are correctly implemented to create a granted-to-complete data stream, which can be verified by doing code reviews and writing unit tests for these nodes’ implementation
    2. A method to prevent an Infinite Flow

    The Infinite Flow

    For the OperatorNodes, we can have a compose function, it connects the 2 OperatorNode and returns a newly composed OperatorNode.

    function compose(op1: O<A, B>, op2: O<B,C>): O<A, C>;
    

    This enables us to convert any Stateless Flow into an OperatorNode.

    We can also have a delegate function for OperatorNode:

    function delegate(operatorRef: ()=> O<A, B>): O<A, B>;
    

    It creates a new O, which creates the data stream using the returned O of operatorRef when it receives incoming data from the previous node.

    With the help of compose and delegate, it is possible to reuse stateless flow in the diagram:

    Since the reuse is based on reference, it is possible to make reference circles in the diagram, which may lead to infinite processing.

    Branch

    To avoid this problem, we need to add the branching ability to OperatorNode.

    function branch(selector: (data: T) => O<T,T>): O<T, T>
    

    branch creates a new O, when data arrives at O, it sends the data to the O’ returned by selector(data), then pipe the output of O’ to the next node.

    If there is no need to do branching for some data, the selector function can return a identity O:

    function identity: O<T, T>
    

    It just outputs what it received.

    With the help of branch and identity, we can now add an exit branch for a circular flow, but the exit branch can not be verified until running the data processing task.

    To avoid infinite processing at runtime, we need to implement a delegation depth detection at runtime.

    Delegation Depth Detection

    The delegation depth actually means “How many times does the data pass through a DeledateNode“, it simply comes to an idea that attaching a delegateCount property to the data inside the data stream. When data passed through a DelegateNode created by delegate, the delegateCount increases.

    When the delegateCount reaches a limit, let’s say, 200k, the DelegateNode should drop the data and write a warning log.

    Answer to the beginning question

    1. Make sure all S, and O are correctly implemented to create a granted-to-complete data stream
    2. Use compose, delegate to allow circular reference in the Diagram
    3. Use branch to add an exit branch to the circular flow
    4. Use delegate to avoid infinite-traveling data in the flow at runtime
  • 2024 新年,我的 EDC 立大功

    春节假期,我的 EDC (Every Day Carry,每日携带物品)立了大功,不管是修理门锁还是处理小伤口,都派上了用场。接下来,我要和你们聊聊这些随身小物怎样解我燃眉之急。

    假期之前,带我用的

    春节假期前我按照“带你用的,用你带的”思路,给自己配置了一个可以塞进衣服口袋的随身 EDC 小包,里面放有我精心挑选的工具小物,旨在帮助我应对日常所需和一些较为紧急的情况。我的物品配置理念可以总结为三个原则:带能用的、带常用的、以及带“我”要用的。

    三个原则

    首先,选择装备应基于个人的知识储备,确保每一件装备都是你能够熟练使用的。这意味着,即使是最高端的工具,如果你不能熟练使用,它也不应该出现在你的 EDC 中。其次,针对高频场景选择用得上的装备,这要求我们对日常生活有深刻的观察和了解,确保 EDC 中的每一样物品都能在常见情况下派上用场。最后,每个人的生活方式和需求都有所不同,因此,配置 EDC 时还应考虑个人的独特需求,确保它真正适合自己。

    多功能工具

    我的 EDC 中携带的多功能工具很少,只有一把“钥匙”( https://geekey.com ),这个小工具在国内可以买到款式类似但价格更加亲民的盗版。我选择它主要是为了这些功能:

    • 开瓶器
    • 割绳锯齿
    • 扁头撬棒(一字螺丝刀)

    像瑞士军刀这样的多功能工具也有这些功能,但是国内公共交通中的安检标准过于灵活,我不想在外出的时候因为安检惹上任何麻烦,就只好选择了这个小钥匙。

    个人卫生

    受限于我贫瘠的医疗知识储备,我的随身医疗物品只能处理小创伤或者让人很“急”的症状:

    • 碘伏棉片:清理伤口,物品消毒
    • 丁腈手套:消毒后接触伤口
    • 氯雷他定片:应对突发的过敏症状
    • 蒙脱石散:应对急性腹泻
    • 创口贴、医用透气胶布:应对简单的小伤口,比如鞋子磨脚
    • 一张便签:记录药物的用法用量、保质期
    • 棉棒:无聊的时候可以掏掏耳朵
    • 牙线:由于牙缝太宽每次吃肉过后必须得清理牙缝
    • 唇膏:

    物品收纳

    除了上面介绍的多功能钥匙之外,其他所有的装备都会装进一个小包。为了方便塞进衣服口袋,我选择了一款钱包大小可以平铺展开的拉链小包。它比我的手机略宽一些,但是要矮很多,能放下手机的口袋一般也能装下它。

    小包尺寸
    尺寸示意
    内部收纳空间
    内部收纳布局

    扩展收纳

    除了小包本身之外,为了应对临时的大体积收纳场景,我在小包中放了一个小号的食品保鲜袋跟一个常规尺寸的垃圾袋。外出溜街的时候,食品保鲜袋可以装洗净的水果,垃圾袋除了用来装垃圾之外,还可以当作临时的防潮坐垫。

    春节假期,用我带的

    正如我所展示的,我的 EDC 配置基本涵盖了从基础工具到个人卫生等多方面的需求,它们在我刚刚度过的春节假期中,表现尤为突出。

    除夕前两天,我回到了重庆小镇上的老家。然而,一进家门就发现卧室的门锁有些松动。这时,我的多功能钥匙便派上了用场,螺丝刀部分轻松紧固了那些松掉的螺丝,门锁也恢复了正常。

    除夕当天,准备年夜饭的热闹景象中发生了小意外。在宰杀鸡鸭时,家里人不小心划伤了手。幸好,我随身携带的碘伏棉片立刻就派上了用场,迅速对伤口进行了消毒处理,紧接着用创口贴包扎好,避免了伤口的进一步感染。

    重庆的年夜饭中,腊肉香肠从不缺席。但美中不足的是,它们经常会卡在我的牙缝里。饭后,我便用我随身携带的牙线棒轻松清理,保持了口腔的舒适。

    重庆这边年夜饭的另一特点就是量特别大,饭菜准备过多,导致我们不得不吃剩菜。加之除夕之后天气突然升温,有些剩菜吃后让人“打标枪”(川渝方言,指腹泻),幸运的是,我随身携带的蒙脱石散迅速缓解了这一不适,止泻效果显著。

    写在最后

    正如这次春节假期的经历所展示的,我能够在需要时用上我带的东西,这说明了我在前面提到的三条配置原则是可行且实用的:带能用的、带常用的、以及带“我”要用的。在我看来,合理配置 EDC 是一种生活的艺术,需要我们思考自己的知识和技能,梳理日常生活和个性需求,从而定制出真正用得上的 EDC。

    如果你看完文章后也想试着打造自己的 EDC 随身小包,下面的参考资料对你可能有所帮助:

    一些搜索关键词

  • 轻松备份,安心生活:个人电脑全盘备份指南

    有没有想过,如果电脑因为一些意外原因突然罢工,所有的宝贵记忆和重要文件就这么不翼而飞?这可不是危言耸听,但好消息是,有了全盘备份,这种噩梦就可以避免了。

    为什么要备份?

    想象一下,你的笔记本电脑突然不工作了,所有的工作文档、个人照片以及定制的系统设置都无法访问。这种情况下,如果你有一个最新的系统全盘备份,那么你可以快速地在新电脑上恢复工作环境。这就像是给数据做了一个时间胶囊,无论发生什么,都能回到过去,将你的数字生活恢复到最后一次备份的状态。

    全盘备份是什么?

    系统全盘备份与仅备份用户数据不同,它是对电脑上所有内容的完整克隆,包括操作系统、应用程序、设置和所有文件。这意味着在恢复过程中,你无需担心哪些隐藏的配置文件没被备份或者某些应用程序无法正常工作,因为一切都会被还原到备份时的状态。

    怎么选备份软件?

    作为一个对价格敏感的用户,我的选择标准很实在:便宜、好用、快速、还得能省空间。我找到了两款不错的备份软件,一款适合 Windows,一款适合 Linux:

    要求Dism++ (Windows)restic(Linux)
    成本免费软件,备份会被压缩,硬盘占用较小开源软件,备份会被压缩,硬盘占用较小
    备份格式WIM,标准的 Windows 部署文件格式开源的专有备份格式
    备份速度不比直接复制文件慢多少 🚗速度超快 🚀
    增量备份不支持增量备份 😥支持增量备份 😃
    其他优势WePE 内置工具支持加密备份文件;支持查看备份系统中的文件内容

    我选择 Dism++ 和 restic 的原因是它们都能够提供简洁且高效的备份解决方案。Dism++ 的备份文件格式是 WIM,这是 Windows 的标准部署格式,意味着即使未来 Dism++ 不再更新,我也能找到方法来恢复我的数据。Restic 虽然使用的是开源的专有备份格式,但它的速度和增量备份功能使得它成为 Linux 环境下的理想选择。这两款工具都解决了我对备份速度和便捷性的需求。

    备份计划

    我的备份策略是定期且频繁的备份。对于 Windows 系统,我每季度备份一次 C 盘;而 Linux 系统则更频繁,每周都要备份一次,因为这是我的主力办公电脑,这样的策略确保了在任何紧急情况下我都能迅速恢复工作环境。

    数据恢复测试与体验

    案例 1:备份并恢复 Windows 系统

    • 准备:使用带有 WePE 的 U 盘进行离线备份,将备份文件存储在一块旧的 1TB 移动硬盘上。
    • 结果:整个系统原本有 250GB ,被压缩成大概 80GB WIM 镜像,恢复后一切正常,就连原系统 Surface Book 的特殊驱动都在。所以还需要手动清理一些不必要的驱动程序。手动清理 Windows 驱动有风险,如果旧驱动没有清理干净可能导致新驱动不生效,可以参考这篇文章尝试修复。

    案例 2:备份并恢复 Linux 系统

    • 准备:使用 systemrescue Live CD 的 U 盘恢复备份,备份文件存储在运行 restic server 的 NAS 上。
    • 结果:原系统大概 300 GB,备份文件总体积约为 40GB ,恢复速度快。尽管需要手动修复 bootloader,但对于有经验的用户来说,这不是难题。

    安全与隐私

    备份时,我确保不通过公网传输数据。而且,为了防止家里 NAS 遭遇不测,我还会把加密后的备份放到云盘,实现异地存储。不过得提醒一下,Dism++ 不支持加密,所以要先用别的工具加密再上传,确保隐私安全。

    结论

    全盘备份就像是给电脑的生命买了份保险,无论遇到什么灾难,都能让你的数字生活重回正轨。备份或许听起来有点技术性,但实际操作起来并不复杂,而且它给你带来的安心,是任何金钱都买不到的。

  • 使用 cloudflare tunnel 在线上预览本地的开发服务器

    Cloudflare Tunnel 是一项服务,允许开发者将其本地开发服务器通过安全的隧道连接到 Cloudflare 的服务器,并发布到互联网上。这意味着:

    1. 其他人可以直接通过互联网访问你本机的开发服务器
    2. 本地的 HTTP 服务可以自动获得由 Cloudflare 提供的 https 证书

    Cloudflare 在这篇文章中介绍了如何将本地项目发布到互联网,但是对于前端开发服务器的场景来说,缺少一些必要的配置说明,例如,稳定的域名、网页缓存策略等。如果你遇到了开发服务器通过 Cloudflare tunnel 访问后出现了一些诡异的问题,不妨参考下面的步骤来检查一下自己的配置。

    创建本地管理的 tunnel

    在开始之前,请确保你准备好了这些东西:

    • 在本地安装 cloudflared
    • 在 cloudflare 上绑定了自己的域名

    接着,参考这里的说明,创建一个本地管理的 tunnel。完成这里的步骤后,你应该会得到一份 config.yaml 文件,默认存储在 ~/.cloudflared 目录下。

    Cloudflare 的文档中使用了 url 字段配置反向代理,但我建议使用下面的 ingress 配置,这样你可以在同一个配置文件中声明多个服务。

    tunnel: <Tunnel ID>
    credentials-file: /path/to/<Tunnel ID>.json
    
    ingress:
      - hostname: ng-serve.zeeko.dev
        path: /api/.*
        service: http://localhost:3000
      - hostname: ng-serve.zeeko.dev
        service: http://localhost:4200
      # this is a required fallback rule
      - service: http_status:503

    禁用 Cloudfare 缓存

    你的 Cloudflare 帐户很可能默认开启了请求缓存功能,在一些使用场景下,例如,webpack dev server,这个自带的缓存功能会让 dev server 变得很鬼畜,我们需要手动在 Cloudflare 控制面板禁用缓存。

    添加 webpack dev server 白名单

    如果你在使用 webpack dev server,记得把绑定的域名添加到 allowedHosts 中,避免 HMR 失败。

  • 修复小新 Pad Pro 2021 运行 GSI 时的亮度问题

    This entry is part 3 of 3 in the series 小新 Pad Pro 2021

    上回书说道,我给小新 Pad Pro 2021(aka. Lenovo J716F)装上了 GSI 系统。当时用的联想 Y700 的模块勉强修复了自动亮度失效的问题。最近我心血来潮,经历了快 10 小时的摸索,终于为 J716F 制作出了专属的 Magisk 模块 —— lenovo-j716f-gsi-fix 年轻人的第一个 Magisk 模块

    这个模块与上一篇文章中的修复模块冲突,请先卸载之前的模块,再启用这个模块。重启平板之后,你应该会发现:

    • 亮度调节的最低档不再是「伸手不见五指」之黑了
    • 在系统设置中的 Phh Treble Settings / Lenovo features 中可以启用 DT2W

    不过可惜的是,磁吸锁屏开关功能还是无效。可能是缺少驱动程序导致的

    解决问题的历程

    自动亮度、自动旋转、亮度等级

    参考 Y700 模块的内容,自动亮度、亮度等级这类设置可以通过 vendor_hardware_overlay 控制。最简单的做法就是从原厂镜像中提取这些配置文件,然后通过 Magisk 模块安装到设备上。

    对于小新 Pad 而言,提取原厂文件的方法很简单,直接「只读」挂载原厂镜像的 system.img 文件就好了:

    sudo mount -t ext4 -o loop,ro ./system.img ./payload-fs/sys
    

    接着参考这篇帖子的内容 操作,就可以得到原厂的 hardware overlay。这篇帖子用到了 phhusson/vendor_hardware_overlay 项目,该项目并不能支持所有的厂商 hardware overlay 配置,需要手动删除不支持的配置。这是一个非常繁琐的体力操作,我制作了一个小工具来简化这一个过程:

    1. 把全部的 vender hardware overlay 文件拷贝到 phhusson/vendor_hardware_overlay 项目
    2. 执行下面的代码,导出所有不兼容的配置到一个临时文件中
    # 在 phhusson/vendor_hardware_overlay 根目录下执行
    ./tests/tests.sh | grep J716F | tee test-out
    
    1. 手动编译这个工具 https://github.com/ZeekoZhu/projects/tree/gsi-overlay-tool/apps/gsi-overlay-tool ,使用 fix 命令批量删除所有不兼容的配置
    ./dist/apps/gsi-overlay-tool/net7.0/GsiOverlayTool fix /path/to/test-out /path/to/phhusson/vendor_hardware_overlay/overlay/
    

    得到设备的 overlay 文件后,打包生成一个 apk 文件,放到一个 Magisk 模块中即可(参考操作)。

  • 网上冲浪指南 – 002

    This entry is part 2 of 3 in the series 网上冲浪指南

    Kitty https://sw.kovidgoyal.net/kitty/

    Kitty 是我现在的主力终端模拟器,它使用 GPU 渲染,性能强劲,并且功能强大:

    • 支持 CLI 命令操作 Kitty 界面(窗口、Tab),可以方便地用脚本打造自己的终端工作环境
    • 支持图像协议,cat 一个图片、视频会直接在终端展示,而不是被二进制输出糊一脸

    分享一下我使用 Kitty 快速启动工作环境的 fish shell 脚本:

    function work-start
      cd ~/path/to/project
      # 分屏打开一个新的 kitty 窗口
      set win1 (kitty @ launch --location split --copy-env --cwd current)
      # 分屏打开一个新的 kitty 窗口
      set win2 (kitty @ launch --location split --copy-env --cwd current)
      # 在窗口 1 中登录远程开发服务器
      kitty @ send-text --match id:$win1 'ssh foo@remote-server -t "screen -r dev"\n'
      # 在窗口 2 中启动代码同步服务
      kitty @ send-text --match id:$win2 'lsyncd ./frontend.lsyncd\n'
      # 光标聚焦回当前窗口
      kitty @ focus-window --match id:$KITTY_WINDOW_ID
    end
    
    在终端查看图片

    Chezmoi https://www.chezmoi.io

    Chezmoi 是一个基于 Git 管理 dotfiles 的命令行工具,我的个人目录下有各种各样的配置文件,为了能够在台式机跟笔记本上有相同的体验,我会使用 Chezmoi 定期在两台电脑上同步配置。Chezmoi 的这些功能我认为非常重要,能够极大地简化在多设备上维护配置文件的操作:

    • 文件模板,允许我们在应用配置之前使用变量替换模板文件中的占位符,这样类似主机名、用户名这样的每台机器上可能不尽相同的信息就可以从配置文件提取出来,方便复用配置模板
    • 密码管理器,像 ssh key 这样的信息并不适合明文存储在 Git 中,好在 Chezmoi 的模版语法中允许我们调用常用的密码管理器,从中读取信息

    搞机工具箱 https://www.coolapk.com/apk/com.byyoung.setting

    这是一款功能丰富过头的 Android 手机工具箱,其功能包括但远不限于:

    • Magisk 模块管理
      • 模块安装、卸载、编辑、更新、备份
      • 音量键救转
    • Xposed 模块管理
      • 模块安装、卸载、编辑、更新、备份
    • 应用管理
      • 系统、用户应用安装、卸载、冻结、编辑权限、多开、提取 apk、设置程序语言、备份、还原、IFW 设置
      • 替换国产系统恶心人的应用安装器
    • 系统工具
      • SWAP 设置
      • 查看 Wifi 密码
      • 修改 Host

    这些功能只是我最常用的,它们只是搞机工具箱全部功能的冰山一角,而且由于功能过多且晦涩,导致用户体验很差,上手难度比较高。
    不过总的来说,瑕不掩瑜,是一款不可多得的搞机必备应用。

  • 网上冲浪指南 – 001

    网上冲浪指南 – 001

    This entry is part 1 of 3 in the series 网上冲浪指南

    AstroNvim https://astronvim.com

    一个全功能的 NVim 发行版,开箱即可获得非常完整的 IDE 体验,预设的快捷键很符合我这个前 SpaceVim 用户的使用直觉。它跟 SpaceVim 有很多不同的地方:

    • AstroNvim 使用 lua 作为配置语言,官方提供了一个用户配置模板帮助用户自定义 AstroNvim。
    • 正如它的名称所示,AstroNvim 只支持 neovim 。

    这是我的 AstroNvim 配置 https://github.com/ZeekoZhu/AstroNvim

    Burning Vocabulary https://burningvocabulary.com

    这是一款简单好用的背单词浏览器插件,网上冲浪遇到不认识的单词,只需要用鼠标选中它,插件就会自动查询翻译并展示翻译结果,并且在之后的浏览中,会自动在网页上标注查过的生词。我个人认为,类似的浏览器插件有很多,但上手难度比它低、使用的顺滑度比它高不多:

    我现在会把 Burning Vocabulary 翻译的目标语言设置成英文(就是用英文解释英文),跟沙拉词典搭配使用,因为沙拉词典支持的词典源更加丰富,让我暂时还不能放弃。

    System Rescue https://www.system-rescue.org

    这是一个 Linux 系统急救工具箱,基于 Arch Linux 开发,一共 700M 的镜像大小,可以使用 Ventoy 制作成 Live CD 使用。

    最近我在扩容工作用笔记本 /root 分区时发现了它,自带图形界面跟 GParted,比单纯的 Arch Install ISO 要方便很多。

  • 给大代码库的类型检查提速

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

    当代码库达到一定规模后,在每次提交前进行完整的 TypeScript 类型检查会非常耗时,就像下面的代码库,执行完整的检查需要 47s,如果每次代码提交时执行类型检查,开发者都得等上接近 1 分钟才能完成 git commit,极大的拉低了开发体验。

    ───────────────────────────────────────────────────────────────────────────────
    Language                 Files     Lines   Blanks  Comments     Code Complexity
    ───────────────────────────────────────────────────────────────────────────────
    TypeScript                3721    271374    23335     21340   226699      16639
    
     nx run-many -t check --all
    
          nx run icons:check (3s)
          nx run visual:check (4s)
          nx run eslint-plugin-merico:check (4s)
          nx run vdev-api:check (176ms)
          nx run nx-plugin:check (3s)
          nx run illustrations:check (4s)
          nx run shared-lib_temp:check (4s)
          nx run fekit:check (12s)
          nx run charts:check (14s)
          nx run main:check (39s)
    
     —————————————————————————————
    
     >  NX   Successfully ran target check for 10 projects (47s)
    

    那么为什么会这么慢呢?责任真的全在 nodejs 的性能上?

    为什么这么慢

    TypeScript 文档中这样写道:

    Rather than doing a full check of all d.ts files, TypeScript will type check the code you specifically refer to in your app’s source code.

    所以当你的 monorepo 中存在项目间引用的时候,执行 tsc -p lib-bob/tsconfig.json 会同时在 lib-bob 及其依赖的 lib-alice 上运行类型检查,这意味着,如果你在 monorepo 中对所有项目分别执行 tsc -p xx/tsconfig.json,被其他项目依赖的项目,会被重复检查多次!

    为了避免这种问题,TypeScript 的 build mode 提供了一种基于增量构建的解决方案。启用 build mode 后,庞大的代码库可以被拆分成多个项目分别被 tsc 构建,tsc 会缓存每个项目的构建结果,当一个项目被其他项目引用的时候,这个项目不会被重新构建而是使用之前的构建结果。在后续的执行中,只有发生变动的项目才会被重新构建。

    如何配置 build mode

    启用 build mode 的方法比较简单,你只需要调整 tsconfig.json 中以下几个选项即可:

    • compilerOptions.composite: true,用来标记这个项目可以被增量构建
    • compilerOptions.noEmit: false,启用增量构建必须允许 tsc 生成文件
    • compilerOptions.outDir: path/to/tsc-out,设置生成文件(包括缓存文件)的路径,记得加入 gitignore
    • compilerOptions.emitDeclarationOnly: true,如果你使用 tsc 构建 js 文件,可以不设置这个选项
    • references,把依赖项目的 tsconfig 路径添加到这里,让 tsc 可以使用这些依赖项目的构建缓存
    • compilerOptions.rootDir,不建议设置,避免构建缓存生成到 outDir 目录之外

    现在,你就可以使用 tsc -b (注意,不是 tsc -p) 来构建你的项目了,同样还是开头展示的代码库,现在重新运行类型检查:

     time nx run-many -t check --all --skip-nx-cache
    
          nx run illustrations:check (3s)
          nx run icons:check (5s)
          nx run visual:check (5s)
          nx run shared-lib_temp:check (781ms)
          nx run vdev-api:check (5s)
          nx run eslint-plugin-merico:check (832ms)
          nx run nx-plugin:check (2s)
          nx run fekit:check (10s)
          nx run fekit-e2e:check (2s)
          nx run charts:check (12s)
          nx run main:check (27s)
    
     ——————————————————————————————
    
     >  NX   Successfully ran target check for 11 projects (44s)
    
    
    ________________________________________________________
    Executed in   44.50 secs    fish           external
       usr time  119.40 secs  213.00 micros  119.40 secs
       sys time    6.17 secs   67.00 micros    6.17 secs 
    

    看起来似乎长进不大,再运行一遍试试?

     time nx run-many -t check --all --skip-nx-cache
    
          nx run illustrations:check (752ms)
          nx run visual:check (760ms)
          nx run icons:check (768ms)
          nx run shared-lib_temp:check (732ms)
          nx run fekit:check (880ms)
          nx run vdev-api:check (1s)
          nx run eslint-plugin-merico:check (731ms)
          nx run charts:check (900ms)
          nx run nx-plugin:check (756ms)
          nx run fekit-e2e:check (773ms)
          nx run main:check (1s)
    
     —————————————————————————————
     >  NX   Successfully ran target check for 11 projects (4s)
     
    
    ________________________________________________________
    Executed in    4.49 secs    fish           external
       usr time   13.51 secs    0.00 micros   13.51 secs
       sys time    1.86 secs  369.00 micros    1.86 secs
    

    类型检查速度变得飞快,这是因为 tsc 复用了第一次生成的构建缓存。如果你这时检查 outDir,你就会发现除了 d.ts 之外,tsc 还生成了 tsbuildinfo 文件,这就是 tsc 用来判断是否可以复用 d.ts 的关键。

     ls dist/out-tsc/icons
    components  tsconfig.build.tsbuildinfo
    

    可以想象的是,在平常开发过程中,每个 commit 通常只会修改一小部分文件,如果我们把代码库中较大的项目(例如上面的 main 项目,代码量有 18w 行)进一步拆分,就可以更加充分地利用增量构建的特性,运行类型检查的范围就会更小,速度就会更快。

    维护 monorepo 中的项目依赖

    细粒度的拆分项目在提高类型检查速度的同时,又会带来一个新的问题,由开发者人工维护项目之间的 references 字段会变得很麻烦,而且容易出错。为了减少人工操作的负担,我开发了一个小工具 —— ts-sync-ref,它会分析项目的 monorepo 内依赖,进而更新 tsconfig.json 的 references 字段。

    # 安装 ts-sync-ref
    npm install -g @zeeko/ts-sync-ref
    # 更新 my-lib 的依赖到 tsconfig.json 中的 references 字段
    cd /path/to/my-monorepo
    ts-sync-ref -p packages/my-lib/tsconfig.json -f 'packages/my-lib/src/**/*.ts'
    

    你可以阅读 ts-sync-ref 的说明文档、代码,进一步了解使用方式及其实现原理。

    结论

    Monorepo 的组织方式有很多种,不管你在用什么工具管理你的 TypeScript monorepo,不妨试试通过 TypeScript 的 Project Reference 将大代码库拆分成多个项目,不仅可以提升类型检查、构建速度,还可以减少编辑器的内存占用,极大地提高开发体验。

    不过,需要注意的是,直接启用 Project Reference 可能会给你的项目带来一些影响,导致现有的构建脚本出错,例如:

    最后,如果你有兴趣的话,也欢迎你在评论区分享你的项目执行类型检查的方案。