阅读视图

三脚架到货了

早上九点,顺风打电话来:"快递到了,放前台了啊。" 估摸着是那三脚架到了

这东西没有真不行,晚上出门一抹黑,不用长曝光,啥都拍不了

我有三个需求:轻量化、收纳短、伸缩1.5m,再三选择后,下单了富宝图旗下新品,空气三号 LITE

图一拆包后,我直接蒙了

周日在群里扯淡,看到 菜鸟之志 推荐的八爪鱼支架,我笑了,这什么玩意,长得那么招笑

真的受够了这种秒懂的日子

还别说,这工具不错,小巧,轻便,灵活度很高,推荐

八爪鱼是送的,但图三这个快拆套装我感觉买亏了。加了一百块钱,就换回这么一小块铁片,关键脚架本身就带一个

出门前纠结了半天要不要背双肩包,最后想想算了,索性直接挂在包拉链上了

有没有一种吕布骑狗的感觉,我这破尼康配上这个架子,多少有点高攀了

今天全程下雨,走在西湖边吹冷风,冻得头疼

回家发现买的电池也到了,价格方面,只有原装的二分之一,应该够我折腾了

天知道这支架是哪个小作坊代工的,漆水和碳布的质量,不敢苟同,我最垃圾的鱼竿,破烧火棍的碳纤维材料都比这个强,不如高性能的铝材料,甚至不如玻璃钢

他的旋转设计也有缺陷,不容易锁紧,又不敢用力拧,因为碳布根本受不了这种扭矩,哪怕你有交叉工艺,也顶不住,这不符合物理规律

再加上碳纤维这东西本身就是消耗品,树脂会氧化。三脚架这玩意,风吹雨打的,扭矩承受力只会越来越差。网上已经看到不少人把它拧断了,总之不推荐

  •  

MySQL 要没落了

这两天突发奇想,在赶一个新项目

从早上 8 点写到晚上 11 点,除了去卫生间,人基本没动过,饭也没吃

这不是我多能坐得住,我只是

妄想一步登天,做不完就死磕。十几个小时是日常,通宵是常态,哪个月没有 4~5 次 20h + 作战记录?

GitHub Commits 一眼就能看出来:我从凌晨提交到凌晨,电子厂的狗都不会这么干

也许这就是倔驴 + 心流?

而我,天生心流圣体

老实说,我对“心流”没概念,只是前阵子偶然了解到。所以我并不明白,但我知道那是一种状态

当然,不吃饭是不行的

晚上 11:30,厨房炒个菜,微波炉热两个馒头,回屋再冲一大杯黑咖啡,两勺燕麦,少许牛奶,就算齐活

坐回电脑前,打开 BlogFinder,随手一翻,看到了阮一峰的周刊

心想,周五了

低头再看,周六了

然后,我看到了他的第一个推荐文章,心里“咯噔”一下:

点进去,翻译看看,整篇文章的核心观点就是:

如果你在意开源软件,就应该停止使用 MySQL

MySQL 虽然是基于 GPL v2 开源,实际上已经没有开源精神,因为 MySQL 正走向封闭

开发活跃度呈现断崖式下跌

这是 Otto 在 1 月 11 日给出的截图

数据显示,从去年 9 月开始,到今年 1 月 11 日,MySQL 几乎完全断更

我看到这里,第一反应是去查官方信息 MySQL 核心团队博客,无果后,再 Google 一下,事情就对上了

早在 9 月 11 日,外媒就曝出 Oracle 大幅裁减 MySQL 核心员工 (约 70 人) 的消息,甚至还有传闻称,MySQL 团队已被并入 HeatWave 部门,Oracle 要把资源优先投入 AI

新闻刚出来时,MySQL 还没断更

现在再把这张“断崖式提交图”放出来,那可谓是深水炸弹

更有意思的是,Otto 还特意把图做成黑白,再配上大字标语当封面

手段可见一斑,毕竟,这人是 MariaDB Foundation (玛利亚数据库基金会) CEO

而 MariaDB 是 MySQL 的最重要的分支与直接竞争者

当年 Oracle 宣布收购 Sun Microsystems 的当天,MySQL 原始作者之一 Monty Widenius 就发起了 MariaDB fork

有意思的是:

  • MariaDB:是以 Monty 的小女儿玛丽亚 (Maria) 的名字命名
  • MySQL :则以大女儿 (My) 命名

就连 MariaDB 的 LOGO 海狮,也是有故事背景的

The Story of our Sea Lion

It happened when Monty and his older daughter My were snorkeling on one of the islands in the Galapagos. Something big, brown and fast suddenly appeared at an arm’s distance, laughing in their faces. Fond memories of this fast and funny creature, scaring the tourists, popped into Monty’s mind when asked picking a logo for MariaDB. He wanted to adhere to the tradition of animals as symbols of Open Source projects.

闭门造车

下面是 Otto 的举证,他认为 MySQL 所有开发工作都在 Oracle 内部进行

而公开的 Bug 追踪器只是个摆设,Oracle 内部有自己的系统

更离谱的是,他声称:

那些提交 PR 的人,除了石沉大海,就只剩下第二个选择

被 Oracle 员工重新编写并抹去原作者的贡献记录,原作者仅在博客中被提及其名

Otto 还证言道:

我在 AWS 负责 RDS MySQL 和 MariaDB 团队时,部门员工极其抗拒向 MySQL 提交代码,因为Oracle 的接纳态度极其恶劣

技术倒退

MySQL 8.0.29 将 ALTER TABLE 的默认行为改为 in-place

结果导致大量生产环境数据库崩溃、数据损坏

而这个问题,直到 8.0.32 才算彻底修完

更大的问题在于:

自 MySQL 8.0 发布以来,整整六年,没有一次真正意义上的大版本更新,几乎没有像样的功能可以讲

甚至,有传言新版本的 MySQL 性能大幅下降,之后被 MySQL 性能专家 Mark Callaghan 进行基准测试后得出MySQL 9.5 的吞吐量比 8.0 版本竟然低了 15%

最后,Otto 顺势给出了“迁移方案”:

说自家的 MariaDB 作为 MySQL 的亲兄弟,有极高的兼容性,对于传统 LAMP 架构,可以无缝切换

他说得也没错,MariaDB 起源于 MySQL 5.1 分支,早期开发策略几乎完全对标 MySQL,以至于后来被开源社区认可,尤其在 Debian、Ubuntu、Fedora 等发行版中,直接成为默认数据库

说实话,我已经不想看下去了,我只是个普通人,一个喜欢编程的小学生

从初一第一次接触数据库开始,就是 MySQL

从上学、工作,到后来失业

我靠它吃了很多年饭

刚毕业那会儿,我为了面试,背得最多的就是 MySQL

对着镜子:一会儿 Indexing、一会儿 Concurrency 人都要疯了

所以,我对 MySQL,是有感情的

现在它要没落了,心里说不出的滋味

虽然国内教学体系还在用 MySQL,但现实已经很清楚了

MySQL 不可能再回到从前老大哥的位置了

哎,天道如来,也罢

  •  

民族劣根性

今天聊聊所谓的“劣根性”,这是碰都不能碰的滑梯

自西贝事件以来,某些“二流报纸”先后两次下场定调,蹭流量的自媒体紧跟实事充当“叼盘”

群众一看,纷纷跳出来表态,生怕队站得慢了

就好像你官媒一说话,事情就“定性”了?下面的人尾巴竖得比狗都直,太激进,太可怕了

也是,宪法都随便改的国家,这确实算不了什么

陈丹青曾对“原则问题”有过精辟的论述:

中国人是不讲原则的,这既是最坏的地方,也是最好的地方

哎,就什么东西他妈的“有用”,就拿来用。没用就打倒他,结果翻出来还有用,再拿出来

说句难听的,民族劣根性

话到嘴边打不住,我大抵是要挨骂了

毕竟你在这个国家,你说他不好,他们就会说你是洋奴

如果你说外国好,走狗是免不了的

如果你说要入籍日本,汉奸的帽子也要扣上

但是,若你闭口不言,悄悄入籍日本,那你就是成功人士

若入了外籍,还说很爱国,恭喜你,摸到名利的门槛了

若你把老婆孩子移居境外,自己却在国内教别人如何爱国,这最起码是个处长

写到这,我心里很难受

我爱我的国家,我为中国的悠久历史文化而自豪,我为唐宋八大家的文学思想而感到骄傲

可如今,一切都变了,真的乱了套

昧着良心说假话的人,生活的有滋有味

而寻找真理追求真相的人,却苦不堪言,甚至还有失去自由的危险

回望当年的庐山会议,多少人为了活命出卖同僚?

有多少人,眼睁睁看着彭老总从成都被揪到北京批斗游街?一度打到瘫痪,囚于暗室,死不瞑目

又有多少人,为了党的先进性和纯洁性,为了完成自我革命,而检举揭发与自己同床共枕几十年的爱人,甚至是自己的孩子

有党性,没人性

这句话,我曾在王局的节目评论区看到过

而我认为,真正有党性的人,凤毛麟角

如毛的秘书李锐、中央党校教授蔡霞等等,他们在坚持原则的同时,从不失有人性的光辉

那些只剩党性、泯灭人性的东西,他们哪来的信仰?不过是借党性之名,道貌岸然地活着,却早已不是人了

说到底,中国人是没有信仰的,都是为了活着,活着最要紧

至于独立思考是什么,早已抛在脑后

想一想,中国发展了千年文明,从先秦诸子百家到宋明理学,再到改革开放引进西方科学

可回过头看,中国人连独立思考都未曾普及,尤其是批判性思维和质疑权威的能力,那是碰都不能碰的滑梯

所以中国人就缺乏鉴赏能力,什么都是和稀泥,没有是非,没有标准

就喜欢讲道德,讲儒家那点奴性思维,以至于,法治社会,人没有人格,法律没有尊严

在这种环境下,人性都畸形了:

一方面逆来顺受,自甘卑贱屈辱贫寒而不自知

另一方面,一朝得势,便以贵凌贱以富凌贫,加倍压迫自己的同胞

这种轮回,在史书里转了数千年,直到今天,天安门依然贴着“万岁”,纯奴性

  •  

实现自动拼图

这事半个月前就计划好了,本想放到 Photosuite 下个版本里

拖到现在,若不是为了昨天拍的一套图,我也懒得折腾

就是为了这口醋,才包的这顿饺子

我的目的很简单:不引入新的语法,而是通过换行来实现拼图

在 Markdown 中连续插入多张图片,只要它们之间没有非空白内容,就应被视为一个整体自动组合

![图1](1.jpg)
![图2](2.jpg)

![图3](3.jpg)

如何检测相邻图片

拼图分组思路由贪心算法实现:

当扫描到一张图片时,以它为起点向后遍历,将所有连续相邻的图片依次纳入同一组

一旦相邻关系被打断,就停止扩展,并根据组内图片数量决定是否生成拼图(不足两张则原样保留)

难点在于“相邻”的定义:

因为 Markdown 编译为 HTML 后,图片容器之间可能夹杂换行符或空白文本节点

所有,不能直接依赖 nextSibling 进行判断

// src/modules/imageGrid.ts

/**
 * 检查两个容器是否相邻
 * 
 * @param container1 - 第一个容器
 * @param container2 - 第二个容器
 * @returns 是否相邻
 */
function areContainersAdjacent(container1: HTMLElement, container2: HTMLElement): boolean {
  // 获取两个容器的父元素
  const parent1 = container1.parentElement;
  const parent2 = container2.parentElement;

  // 如果父元素不同,不是相邻的
  if (parent1 !== parent2 || !parent1) return false;

  // 获取父元素的所有子元素
  const siblings = Array.from(parent1.children);
  const index1 = siblings.indexOf(container1);
  const index2 = siblings.indexOf(container2);

  // 检查是否相邻(中间只能有文本节点或空白节点)
  if (index2 !== index1 + 1) {
    // 检查中间是否只有空白文本节点
    let hasNonWhitespace = false;
    let node = container1.nextSibling;
    while (node && node !== container2) {
      if (node.nodeType === Node.ELEMENT_NODE) {
        hasNonWhitespace = true;
        break;
      }
      if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
        hasNonWhitespace = true;
        break;
      }
      node = node.nextSibling;
    }
    return !hasNonWhitespace && node === container2;
  }

  return true;
}

在解决了相邻判定的问题之后,分组逻辑本身就变得很简单了

当 Photosuite 扫描到一张图片时,会从当前位置向后查找连续相邻的图片,并将它们归为一组

出于美观考虑,我这里将单组图片数量限制为最多三张

// src/modules/imageGrid.ts

function processImageGrids(root: Element) {
  // 获取所有图片元素
  const images = Array.from(root.querySelectorAll("img"));

  // 用于标记已处理的图片
  const processed = new Set<Element>();

  for (let i = 0; i < images.length; i++) {
    const img = images[i];

    // 跳过已处理的图片
    if (processed.has(img)) continue;

    // 检查是否可以形成拼图
    const gridImages = [img];
    processed.add(img);

    // 查找连续的图片(最多3张)
    for (let j = i + 1; j < images.length && gridImages.length < 3; j++) {
      const nextImg = images[j];

      // 检查两个图片容器是否相邻
      const currentContainer = ensurePhotosuiteContainer(gridImages[gridImages.length - 1]);
      const nextContainer = ensurePhotosuiteContainer(nextImg);

      if (areContainersAdjacent(currentContainer, nextContainer)) {
        gridImages.push(nextImg);
        processed.add(nextImg);
      } else {
        break;
      }
    }

    // 如果找到了多张连续的图片(2-3张),创建拼图
    if (gridImages.length >= 2) {
      createImageGrid(gridImages);
    }
  }
}

我用 Boardmix 做了一个流程图,通俗易懂:

如何处理不同比例的图片

把图片简单地放进一个 Flex 容器并不难

如果一张是横图(16:9),另一张是竖图(9:16),直接使用 flex: 1 只会让它们宽度相同,却无法保证高度一致,结果要么高低不齐,要么图片被强行拉伸

要让多张图片在同一行里等高对齐,关键不在 Flex,而在比例关系

直观来说:哪张图片更“扁”,就应该占更宽的位置;哪张更“瘦”,就占得窄一些,这样它们的高度才能最终一致

用更具体的话说:

每张图片在一行中所占的宽度,应该和它本身的宽高比成正比

宽高比越大(越横),分到的宽度就越多;宽高比越小(越竖),分到的宽度就越少

因此,在等高布局下,可以把每张图片的宽度理解为:

每张图片的宽度占比 ≈ 自身宽高比 ÷ 所有图片宽高比之和

其中:宽高比 = 宽 / 高

基于这个思路,我没有让 Flex 自由分配空间,而是先计算好每张图片应占的宽度,再通过 flex-basis 精确控制布局

具体实现上,会先异步获取每张图片的宽高比,计算出总比例后,将容器宽度按比例拆分。同时考虑到图片之间的间距(gap),使用 calc() 对最终宽度进行修正,避免累计误差

// src/modules/imageGrid.ts

/**
 * 异步更新拼图项宽度
 * 基于图片宽高比计算宽度,使得所有图片高度一致
 */
async function updateGridDimensions(images: HTMLImageElement[], gridItems: HTMLElement[]) {
  const ratios: number[] = [];

  // 获取所有图片的宽高比
  for (const img of images) {
    const ratio = await resolveImageRatio(img);
    ratios.push(ratio);
  }

  // 计算总比例
  const totalRatio = ratios.reduce((sum, r) => sum + r, 0);
  const gapCount = gridItems.length - 1;

  // 设置每张图片的宽度百分比
  gridItems.forEach((item, index) => {
    if (totalRatio > 0) {
      const ratio = ratios[index];
      const percent = (ratio / totalRatio) * 100;
      
      // 使用 calc 计算实际宽度:(比例% * 100) - (gap总宽 * 比例占比)
      // 公式: calc(33.33% - (2 * var(--gap) * 0.3333))
      const widthCalc = `calc(${percent}% - (${gapCount} * var(--photosuite-grid-gap, 4px)) * ${ratio / totalRatio})`;
      
      // 设置 flex-basis 和 max-width
      item.style.flex = `0 0 ${widthCalc}`;
      item.style.maxWidth = widthCalc;
    }
  });
}

这样一来,不管图片比例多么悬殊,它们在拼图中都会自然对齐

高度一致、宽度合理、边缘整齐,同时也避免了任何形式的拉伸或裁剪

布局样式

样式只负责布局:Flex + gap + 响应式间距,没有额外装饰,所有空间都留给图片本身

这里通过 CSS 变量统一管理间距;在移动端则适当缩小 gap,以保证有限屏幕宽度下的视觉紧凑度

// src/styles/image-grid.scss

/* 拼图容器 */
.photosuite-grid {
  --photosuite-grid-gap: 4px;
  
  display: flex;
  gap: var(--photosuite-grid-gap);
  width: 100%;
  margin: 0;
  padding: 0;
  align-items: flex-start;
}

/* 移动端保持拼图布局,但减小间距 */
@media (max-width: 768px) {
  .photosuite-grid {
    --photosuite-grid-gap: 2px;
  }
}

在交互层面,加了点人情味,当鼠标悬停在图片上时(拼图状态),会有一个轻微的放大效果

注:使用该功能无需配置,默认为 imageGrid: true

拼图状态下不显示 EXIF 和标签,它们在编译阶段并未生成

至此,完成

感谢 Claude Code、Gemini、ChatGPT 对项目的大力支持

安装

Photosuite 已发布至 npm,可直接安装:

pnpm add photosuite
# or
npm install photosuite
# or
yarn add photosuite

参考

  •  

优化了一下博客的交互细节

晚上刷 1900 的博客,我忽然想起了 主题配色切换动画

随手输入关键字 “主题” 一搜,果然找到了那篇:给博客主题切换加个动画

看到左上角的发布日期时,不禁有些感叹,原来这已经是半年前的事了

主题切换动画

实现思路与 1900 类似,本质上是利用 View Transitions API 接管视图更新,从而实现遮罩动画

通过 document.startViewTransition 捕获 DOM 快照,再配合 CSS 的 clip-path(裁剪路径)实现平滑过渡:

const transition = document.startViewTransition(() => {
  themeValue = themeValue === "light" ? "dark" : "light";
  setPreference(true);
});

transition.ready.then(() => {
  // 从上至下的裁剪路径
  const clipPath = [
    'inset(0 0 100% 0)', // 开始:底部被完全裁剪
    'inset(0 0 0 0)',    // 结束:完全显示
  ];
  
  document.documentElement.animate(
    { clipPath: clipPath },
    {
      duration: 1000, 	 // 长动画,可能会掉帧
      easing: "ease-out",
      pseudoElement: "::view-transition-new(root)",
    }
  );
});

返回按钮

此外,我还给博客增加了两个功能按钮,灵感借鉴自 SeerSu,他的博客设计非常线性,我很喜欢

  • 返回上一页(如果存在浏览记录),否则返回首页
class="fixed bottom-6 md:bottom-8 z-50 w-10 h-10 rounded-md bg-white/40 dark:bg-gray-900/40 backdrop-blur-md border border-white/50 dark:border-gray-700/50 text-gray-700 dark:text-gray-200 shadow-sm hover:bg-white/70 dark:hover:bg-gray-800/70 hover:scale-110 hover:shadow-lg transition-all duration-300 ease-out translate-y-8 opacity-0 pointer-events-none flex items-center justify-center gap-1"

整体视觉设计上,采用了玻璃风格,悬停时有缩放效果

回到顶部

当页面向下滚动到一定范围,该按钮才会浮现,并自动调整“返回按钮”的位置

// 监听 BackToTop
window.addEventListener("backToTopVisibilityChange", e => {
  // 如果回到顶部按钮出现,返回按钮自动向上移动让出位置
  updateVerticalPosition(e.detail.visible);
});
  •  

晨跑,破六

熟悉我的朋友都知道,我是一个懒人。懒得说话,懒得吃饭

所以像我这样的人,按理说对运动是没什么兴趣的

但耐不住压力大,我急需一种方式来发泄情绪

毕竟,人是可以活活憋死的

杭州城内,我最爱的一条路:南山路

我之前的发泄方式主要是骑车,但现在碳轮坏了,暂时没钱换,于是便转向了登山徒步

不可否认,我非常喜欢这种方式,但徒步太耗时间了,我不可能每天都拨出大半天做这件事

至于跑步,我没想过。仅有的尝试也纯粹是出于好奇。可以说,这辈子我跑过的次数,一个巴掌就能数得过来

2025年12月31日,早晨七点。我实在憋不住了,穿上体能服直接冲了出去

跑出一公里后,我有些后悔没戴帽子和手套。苏堤两岸吹来的冷风冻得我头皮发麻

跑到岳湖附近时,想吐。我手撑着树干呕了几分钟,深感不适,当场掉头慢跑回家了,全程九公里

纯菜逼,回到家后,连饭都吃不下

第二天,也就是 2026 年的第一天。杭州下了一场暴雨,我便没出门

跑步的第二天,差点破六

如果不算年前的那次,从 1 月 2 号到今天,是我连续晨跑的第六天

以前我习惯通宵写代码,而现在为了凌晨能跑起来,作息也调整了

不管手头有什么几把事,统统往后排,每天 12 点前必须睡觉

 咖啡 + 牛奶 + 燕麦

由于晚饭吃得少(或者干脆不吃),凌晨醒来时明显的饿

所以出发前,我都会冲点麦片。这东西挺容易消耗,有饱腹感,即便吃半碗也不影响马上跑步

凌晨 6 点 13 分的湖滨路

插播一个有意思的小插曲:跑步的第二天,经过湖滨银泰时,我貌似和马云擦肩而过

当时我俩距离不过半米,几秒钟后我才反应过来,那个人很可能是他

毕竟马云的相貌还是很出众的,有一定辨识度,可惜当时光线差,他还戴着帽子

回家后我特意查了他近期的行程,啥也没搜到

但如果他当时在杭州,我敢说大概率是他

因为马云一直有户外运动的爱好,之前在杭州爬山、骑车也常被网友偶遇

雷峰夕照

闻子状雷峰,老僧挂偏裻。日日看西湖,一生看不足
—— 张岱《雷峰塔》

今天的天气极好,跑过苏堤时,恰好瞥见了西湖十景之七的“雷峰夕照”

可惜当时站位欠佳,视线被桥两侧的树木挡了大半

不过我也没打算专门跑到桥下去取景,毕竟我是出来跑步的,不是来搞摄影的

日志里的这些照片,每张的拍摄停留时间都不会超过 5 秒

因为我我习惯在跑步过程中提前打开相机,停下的瞬间,让远景对准参考线便按下快门,不过一瞬间

其中不乏有运动过程中拍下的照片,所以有些画面难免会有重影或模糊

实际上我这几天的跑步,都没怎么在中途停下过,也就是所谓的不间断

骑行更是如此,即便是两百公里,我也宁愿在车上慢踩,而不会选择停下

当然,凡事皆有意外

昨天跑得确实有点过火,到 11 公里时,左腿膝盖突然刺痛难忍

眼看只剩最后两公里,我实在不想放弃,便拖着左腿蹦着跑,那种刺痛感真是钻心刺骨,疼得我直咬牙

最后还是认怂了,不过我发现只要不刻意发力,刺痛感就不会有,于是快走了两公里。回到家一看配速:6:13,绝了

凌晨雨跑,环西湖

5 号那天是我人生第一次“雨跑”

刚出门,万松岭的路面就已经全湿了,手掌伸出去能感觉到细密的水滴

就这点降水量想挡住我跑步?门都没有

毕竟我衣服都穿好了,脱是不可能脱的

白堤

雨跑给我的真实感受其实就两个字:湿、重

汗水夹杂着雨水,上半身很快就湿透了,体感并不算舒服

我想,产生这种不适感,也许是因为跑步时注意力还不够集中

不像骑行,如果你行驶中分神,就有可能出事故

所以骑行时,人的感官会高度聚焦,从而忽略身体的琐碎不适

显然,我跑步还没达到那种境界

开水煮西蓝花

除了跑步,最近我还尝试着自己动手做饭

凌晨 5-6 点起床,跑完步刚好是早餐时间

到家后的第一件事通常是点根烟暖暖身,如果出汗多就洗个澡,然后从冰箱拿点能吃的搞一搞

我的早餐口味偏清淡。比如这盘西兰花,什么调料都不放,开水烫一下就可以出锅

鸡蛋 + 火腿 + 韭菜 + 上海青

南德调味料,河南的老乡应该都很熟悉,家乡话叫“南呆”

这大概是我早餐里口味最重的一道菜了,再油腻的我也吃不下

炒鸡蛋出锅时,高压锅里的米粥也熬好了。趁热再去微波炉里热个馒头,一顿饭就齐活了

大概就是这样

破六

  •  

尼康 z30 开箱小记

有些年头没摸相机了。上一部相机还是 2019 年买的“佳能 EOS 200D”,那时 Fooleap 还没有退网,我还在做程序员 《我的第一部单反相机》

一晃眼,六年过去了。一切都变了,我刚入圈时,Fooleap 还在为哄女朋友而苦恼,现在孩子都会打酱油了。当初认识的博友大多已停更,中间我也曾断过更。不过后来因为喜 欢上户外活动,又把博客捡了起来。现在的博客大部分内容被户外生活占据,也是我持续更新的动力

前天,我把 EXIF 样式重写了,但图中的照片画质实在太烂,这对于一个完美主义者来说是无法接受的。一时间想到了相机,早年那台佳能 200D 因为闲置太久早已出掉, 看了些博主评测,最终圈定了两款:“佳能 R50”和“尼康 Z30”。因为 R50 阉割了热靴接口,我选择了尼康 Z30

京东尼康旗舰店下的单,16-50mm 套机,价格是 4869 元。顺丰走了两天半,属实有点慢。

下单送了四样东西:

  • 64G 内存卡
  • 屏幕钢化膜
  • 座充
  • 相机包

如果不要这些赠品可以少 100 元,但想了想没必要,毕竟存储卡总是要买的,差价不大。等以后手头宽裕了再换更好的

上手的第一感觉:这相机真的很小!还没有三星手机大,感觉完全可以揣进兜里,便携性满分

实际试拍了不少照片,全程 M 档,不是过曝就是欠曝。晚上去西湖大道的桥上拍车流,又没控制好,画面还是过曝了

先这样吧,接着回 B 站看摄影教程去了

  •  

读苏轼有感(1)

今天看了一本关于苏轼的传记,其中有首诗词让我感到震惊的同时又有一种无力感

人生到处知何似?应似飞鸿踏雪泥

回想我这活了二十多年,不过在欲望的追求与失落之间摇摆,实在没有意思

难道说人,这种欲望下的产物,只能困于这悲苦之中不得解脱吗?

没有欲望的人还有人性吗?还是人吗?

大悲寺的和尚不食人间烟火,却求佛问道,这难道不是一种欲望吗?

  •  

西湖阅读初体验

昨天跑步回来后,大腿肌肉疼痛难忍,上下台阶非常吃力

毕竟很久没跑了,突然跑了十几公里,身体难免吃不消

今天早上照旧六点起床,一路上尽量不做停留,所以没有拍照

跑步路线和昨天不太一样

从断桥下来,沿北山街直行,右转进入环城西路,结果居然跑错了,跑到了延安路

坚持跑完全程回到家后,发现跑得有点超纲,距离比昨天还要长:13.73 km、1h 55m

不过以后不能这样了,我在想如何调整晨跑的时间

以我目前的能力,绕西湖一圈接近两个小时,实在太奢侈,毕竟上午还有其他事情要做

今天的早餐吃得比较晚,九点半才吃

眼看中午快到了,11:50了,我还是不饿,想着出去走走,但大腿是不允许的

索性拿了两本书去西湖坐坐:一本是尚未看完的“C3环球游记”,一本是未曾读过的“阅读的方法”

因为住在西湖边,这几天走了很多次。即便是中午,西湖边依旧非常冷,我特地穿了红色高领毛衣和一件轻薄款的带帽羽绒服

出门时,大街上不少异样眼光看我这入冬装扮,我也不当回事。我看到的路人大多穿的是短袖和轻薄外套

不知道小学生们在做什么户外活动,老师带队,人数大约有500人

在西湖水边找到空位的椅子不容易,唯一空着的椅子,还不能坐

终于抢到一个位置,坐下五分钟后,我旁边的那人走了,我独享了

低头看书,抬头便是西湖美景

自进了柳浪闻莺后,我便把羽绒服脱掉了,确实热

坐下十分钟后,北风打脸一般呼呼的吹,冻得我发抖。遮阳帽和羽绒服帽子都戴上了,还是不行

我只好侧身坐着看书,就这个姿势,坚持了两个小时

可乐喝得有些多了,到了下午一点,我想去小便,但卫生间距离有点远

如果我一走,这个位置肯定会被占,就这样憋到两点

是的,憋不住了,从卫生间回来后,没有再去西湖边,而是在柳浪闻莺公园坐着

这个地方离西湖大约两百米,四周树木茂密,风吹不到我这

虽然这里不冷,但小蚊虫实在太多,到了三点左右,我便回家了

我住在山脚下,地方比较潮湿,小蚊子很多,这两天夜里都是蒙着头睡的

 另外,还买了一个护腰椅,BKT 加大版,以后坐在床上玩电脑会舒服一些了

  •  

二零二五 - 六七月读书随笔

书犹药也,善读之可以医愚

C3环球游记Ⅲ:加勒比没有那么远 | 徒步进藏 | 不去会死

二手海淘

这三本书买的有段时间了,在 端午骑行:倍鱼线 里我随口提过一嘴,算起来,另外两本书吃灰近四周了。最近没心情敲代码,索性把书捡了起来。

第一本看完的是《不去会死》,作者是小日子过得还不错的“石田裕辅”。

这本书还是从 Pluskid 博主 2022 书单里看到的,我一眼瞄到封面是自行车,当时就放下鼠标,开始在在各个平台寻找这本实体书。由于发行较早,市面上大都是二手货,而且是非彩印版本。唯一的全新版本是台版的,八月开售(繁体)。

我在手机上扒拉了很久,终于在某宝一个“倒江湖”的小商贩那里,以27元下单了(定价38)。我也没怀着捡漏的心态买这玩意,只希望它不缺页就行了。

端午傍晚回家,看到快递,拆包后看到实物,如获至宝啊!上海译文出版,上海书刊印刷!而且是全彩印刷版本,还是2012年5月第一版的第一批印刷!看这纸张和文字印刷质量,正版无疑,且无一缺页。

不去会死,在版编目(CIP)数据

不去会死

端午过后的第一天,六月一号下午,我背着单肩挎包,带着书,骑着我的公路车,溜达到义乌植物园,找了一个石凳子,一坐就是一下午,把这本书给炫完了。怎么说呢?这本书实在太他妈的精彩了!

想想看,打工三年攒了5400美金出发,期间还被土匪抢劫3000美金,就这样骑行环游世界七年半、横跨五大洲、八十七个国家!

p.s.不要问没钱怎么活,这大哥擅长钓鱼、自制鱼干

阿根廷,巴塔哥尼亚,1997年2月

遭遇土匪顶枪抢劫

在秘鲁·纳斯卡的沙漠公路骑行时,石田君惨遭绑架,连人带车被三个持枪土匪拖进沙漠深处。

当时土匪拿枪顶着他,把他的双手反绑在背后,脚也被绑了。土匪为了敛财,裤头都给他扒光了。他还以为土匪要强奸他,哈哈哈。就这样,这大哥还在讲条件,恳求留下自行车。

结果土匪把护照和全部财产的暗袋都抢走了,还有露营用品、药品和工具,所有装备洗劫一空。好在土匪太慌了,地上还散落一些杂物和掉落的二百美金。最重要的自行车,由于土匪来不及抢走也留下了。

事后他联系保险公司索赔,但是片警连抢劫证明都不给开,开口嘴就是“Money”。

后来他回忆说:“当时一点都不紧张。相反,土匪紧张得像热锅上的蚂蚁。”三个土匪还在他面前内讧,大吵一架,土匪跑之前还主动帮他穿好裤子。

秘鲁,在纳斯卡的沙漠遇到土匪后,1996年

清田君

除了主人公石田裕辅,让我印象深刻的还有"清田君",很有趣的一个人,特别是他那句:

啊啊,真好吃,真好吃啊!

起初他们在"乔治王子城"相识,石田裕辅这样描述他的:

  • 头发朝四面八方乱长
  • 头大得出奇,整个巨大的蘑菇头
  • 脸黑的像烤焦的面包
  • 眼睛细长锐利
  • 体格矮小粗壮
  • 罗圈腿

中途他们因路线分开过几次,幸运的是,每次都能在异国他乡相遇!

以至于后来清田君也遭遇土匪抢劫,还被枪打伤,一度悲惨,一路要饭.....

大树下看书,好不快活

卖香菇的老伯

骑行到波兰首都华沙时,石田君看中了当地特产蘑菇,纯吃货,天天煲汤

在骑行穿越森林小路时,他看到一个残障老伯在路边卖香菇,出于对老伯自力更生的感动,他拿出了一个兹罗提(波兰货币)铜板(相当于40日元),满脸笑容的指着蘑菇说:"给我一个兹罗提的份。"

老伯看他拿一个铜板,马上就变脸了,嘴里一直喊着:“Nie Nie!(不!)” 然后不停地说着什么。

由于波兰语他压根不懂,他以为老伯嫌他那点钱少。又掏出一张五兹罗提钞票,老伯当场就爆发怒吼:“Nie Nie!” 然后从衣袋里拿出自己的钱包,刻意在石田君面前取出几张钞票。

石田君看到老伯从钱包里取钞票,他心都凉了,以为老伯想要更多。就在这时,老伯不断的把香菇装进袋子里,嘴里还是不停用波兰语快速说着什么,石田君连忙阻止,但是老伯一点都没有停下来的动作。

这时候石田君脑海里冒出一个词:“Present?” 他带着疑问随口一说,老伯用力点头回应。

这时候石田君才明白,老伯不停的用波兰语表达的意思可能就是:

我怎么能从你这个贫困的旅客身上拿钱呢?

波兰,卖香菇的老伯,1997年10月

最近,我在网上查石田君的个人信息。它还有个人博客,2003年就开始运作了,日更级别!

石田ゆうすけのエッセイ蔵:https://yusukeishida.jugem.jp

五月初,他在日本·静冈骑行撞山,摔断八根肋骨和锁骨。这是康复之旅的第一张照片

徒步进藏

这是一个京城富姐(凡凡)从成都出发徒步拉萨的故事。纯徒步,不搭车!

大路书:

成都 - 拉萨,2160公里,3304800步(迪卡侬计步器统计),90天

90天:2014年5月2日 — 7月30日(休整15天)

2160公里:

成都大件路 → 雅安 → 泸定 → 康定 → 新都桥 → 雅江 → 理塘 → 巴塘 → 芒康 → 左贡 → 邦达 → 八宿 → 然乌 → 波密 → 通麦 → 鲁朗 → 八一 → 工布江达 → 墨竹工卡 → 达孜 → 拉萨布达拉宫

凡凡的背包有些重了,出发时的重量在40斤,属于重装范畴。我这小体格是接受不了,毕竟这是肩包,不是驼包。

书店不让带书

说到这我要吐槽一下,进书店不让带书?

我就问你,你新华书店作为国企,内地从事图书行业的老大哥,你这样做真的好吗?

是,不让带包,降低被盗风险。但就论书这东西,不看书的不会偷,看书的更不会偷。

你们这一刀切的政策是和谁学的?开在商场的西西弗人流量不比你大?人家敢说带包不让进吗?今天不让进,明天他就倒闭。

就你这还文化行业呢,呸,恶心!中国的国企都是他妈的社会寄生虫,从此以后不在新华书店买一本书。

义乌,新华书店(图书大厦店)

转战西西弗

自从离开上海,我所在的十八线小城市再也见不到西西弗了。算下来,我已经有一年半没有去过了。在上海那段时间,我几乎每天上午都会去西西弗,买一杯最便宜的咖啡,提着书包去占座。因为西西弗离我们公司非常近,公司在万象城五楼,而西西弗在三楼,所以我站在五楼的视角俯瞰,完全可以看着他们开门营业,每天必抢到黄金位置。

下午两点下班,我没有回家睡觉的欲望。我要么出去骑车去外滩,要么去西西弗看书,没有第三个选择。所以,坐电梯下3楼,一头扎进L333A-A号,直到听见 Going home。

其实,待在西西弗,我的目的不止是看书。在下班后,店里几乎每天都会给我带来福报,这个帝王蟹客人晚宴要用,那个东星斑规格不行。接到骚扰电话后,我就得先打电话联系档口,然后起身开车去江阳市场,所以西西弗是一个很好的选择。

义乌,西西弗,印悦城店

实话说,以我目前的经济,来西西弗有些奢侈了。一杯咖啡四十块,还非常难喝。

但是我在义乌找不到晚上六点后可以去看书的地方了,求推荐。

几年前在杭州林隐寺买的包,很适合装书

正在读 C3环球游记

最爱的姿势:躺着,用抱枕托着书

  •  

十月份看完的第一本书

为这本书,我近两天几乎废寝忘食,心中感慨颇多,最深刻的收获便是对人性与形式的深刻理解。许多事情在当时看来似乎是理所当然,但如今回首,才发现那时的自己是多么稚嫩,甚至有些可笑

微信读书 · 沧浪之水

这是我第一次尝试电子书,有声加文字的双重体验感觉不错,美中不足的是,机械式的发音让情感显得苍白,在这个时代,做个拟人化的语音合成也不难啊,腾讯读书这块业务还是小众,资源太少了,很多我想看的书都找不到,涉及敏感话题的搜都搜不到,哎

  •  

我的第一部单反相机

咱老百姓,今儿真啊么真高兴!嘿嘿,我的第一部单反相机"佳能EOS 200D 银色单反套机",今早9:18快递到了!纸箱镇楼! 受苦了宝贝...发货之前忘了指定快递服务,默认发了圆通。看这箱子样子,想起以前我去快递分拣兼职,活太累了管它什么物品都是暴力扔...

纸箱镇楼

大大小小拆完了就是这样样子了(32G SD卡、新秀丽蓝色双肩包、限量20年礼盒、Luce波点内胆包、EOS 200D机身、电池充电器\线 LC-E17C、锂电池LP-E17、相机宽背带 EW-400D、EF-s 18-55m f/4-5.6 IS STM)

所有的物品

看到这个狗头挺烦的,也怪自己吧...在天猫买件肯高UV镜55mm。买回来发现装不上...一看狗头58mm。

18-55mm狗头

为了业余摄影,还特地在博客开了个"摄影"分栏,感觉自己这次博客要大改了一下,需要做个几个相册,就那种卡片视图效果,不过我博客逻辑有点特殊,估计需要点时间,让我头疼的是图片,以后摄影照片一张5MB-10MB,就Github在国内这个接入速度有点吃不消,还是要想办法无备案DNS加速。

EOS 200D机身 + 18-55mm狗头合影

这里有点尴尬...本想来拍几张预热看看,没电来了,晚上准备骑行去广场玩玩

尴尬...刚买回来需要充电

装到内胆包

  •  

聊聊贾跃亭

跨年前夜,刷到贾跃亭发视频唱了一首北京北京,多少是想家了,希望他下周能回国吧

点开评论区,还是一个吊样:嘲讽和谩骂。我就纳闷了,骂他究竟能让你舒服多少?

有人指责他财务造假。说句不中听的,就这个环境,大到顶层建筑,小到微企个体,谁没做过一些违背良心的事?区别无非在于,有的你可以随便骂,有的你一句话就成了冒犯君主不敬之罪

还有人替供应商鸣不平,“他们收不到货款,哪里错了?” 真鸡巴扯淡,既然选择创业,就得承担风险。你作为下游供应链,和企业就是一根绳上的蚂蚱,这是何尝不是投资?而不是去餐馆吃饭,不满意就说菜难吃

就这一点,和那些把买房当理财的人有什么区别?
涨了炫耀,跌了骂娘,骂开发商、骂社会不公,甚至媒体曝光,集体诉讼。就好像风险只该由别人承担。这种逻辑,本身就很扯淡

看着这些评论,我只想笑
就单论一个人创业十余年,面对失败,面对别人的冷嘲热讽,仍选择向前走,你有这个魄力吗?

这还不算个爷们?

换位思考,把你放在贾跃亭的位置,给你三个选项,你会怎么做?你还会坚持自己的梦想吗?

  1. 回国,放下梦想,不能造车,改行谋生
  2. 打工,一辈子望到头,几乎没有还债的可能
  3. 出国,坚持梦想,继续创业,继续赌
  •  

我的博客 2025

从 2018 年 8 月 31 日开始,我们一起走过了 2675 个昼夜

你的评论,让博友之间距离更近

这一年,你写下了 233 条回复
世界偶尔沉默,而你选择了回应

你还记得吗? 1 月 7 日
你在“骑行郑州 · 四环”日志里,写下了今年首条回复
@小彦 初次落笔,幸好有你

不算太长短的字句,都留下痕迹

今年,你写了 48 篇日志,约 8.5 万字。这个数字占过去七年总和的 40%
相当于写完了两部呐喊

11 月是你的灵感高峰,相当于去年一整年的 60%
10 月 15 日,你写下了今年字数最多的一篇日志

全文 4000 +,字字惊心,你爽了吗?

热爱是唯一的通行证

这一年,你的博客收获了

12352 次页面访问
339 条访客评论

这些回声,将你的声音推向更远的地方

你的年度口头禅是:“实在太爽了!”

你最感兴趣的的领域是:骑行,全年提及 177 次,贯穿 28 篇日志

更有意思的是,你有 20 篇日志是在凌晨(00:00-05:00)完成的,你是真的不睡觉吗?

希望新的一年,你能遇见更多同频的人

2025 年,共有 103 位新朋友 闯入你的世界,TA 们通常在凌晨至中午出没

年度首席读者是 @obaby
TA 留下 37 条评论,真诚的人同路亦同心
特此颁发“年度最佳嘴替奖”:感谢这一年所有的欲言又止,都被你温柔接住

和你互动最多的是 @1900
聊得这么热乎,很难不让人怀疑:这到底是爱情的火花?还是那种“懂的都懂”的男上加男?

关于 2025:你想打包什么

这一年,真的要过去了

时间会被打包

记忆将会存档

最后,让我们用这一句年度金句,为你留下一份属于 2025 的真实源文件:

「时隔六年,再次为热爱脱皮。痛苦如影随形,却也因此更加坚定」

  •  

Photosuite:一个为博客而生的图片处理方案

每次给博客搞拆迁,最让人割舍不下的,往往不是瓦片和墙皮,而是那些被我用到形成肌肉记忆的家具。由于Jekyll 和 Astro 说着不同方言,导致家具根本拿不到“异地安置指标”,我只能含泪签字

Photosuite 正是在这样的背景下诞生的

它由 Vite + Typescript 开发,拥有我博客图像的核心能力,包括:

  • 灯箱

  • EXIF 展示

  • 图片说明

  • 图片路径自动补全

Example Picture

上面的图片及其所有样式,仅通过下面这一行最普通的 Markdown 语法生成,而且我只需要输入文件名:

![Example Picture](example-picture.jpg)

设计思路

Photosuite 利用 Remark 和 Rehype 插件生态,在 Markdown 编译阶段完成图片处理,避免在运行时增加负担:

- 构建期
  ├→ Remark:补全图片 URL
  └→ Rehype:读取图片 EXIF 并写入 HTML
  ↓
- 运行期
  ├→ photosuite(opts) 入口:按需动态 import
  ├→ glightbox 模块:把图变成可点击灯箱
  ├→ imageAlts 模块:用 alt 自动生成 caption
  └→ exif 模块:加载 EXIF 样式,清理空条

图片路径解析

如前所述,Photosuite 的首要职责是补全图片 URL

设计目标很简单:在 Markdown 中只写文件名,其余交给 Photosuite 处理

整体思路如下:

- 使用标准 Markdown 语法插入图片,只填写文件名
  例:![Example Picture](demo.jpg)

- 配置一个基础 URL 作为图片域名
  https://cdn.example.com/images

- 子目录来源有两种策略:
  a. 通过 Frontmatter 指定图片目录(默认)
     imageDir: 2025-12-22-photosuite

  b. 以当前 Markdown 文件名作为目录(去除后缀)
     2025-12-22-photosuite.md

- 最终生成的完整路径一致,例如:
  https://cdn.example.com/images/2025-12-22-photosuite/demo.jpg
  
- 稍作调整即可实现按年 / 月分类等更复杂的目录结构。

实现逻辑:

  1. 使用 Remark 插件遍历 Markdown AST 中的所有 image 节点
  2. 判断图片 URL 是否为“短链接”(无协议、非绝对路径、非显式相对路径 ../
  3. 按配置策略拼接完整 URL(域名 + 目录 + 文件名)
  4. 重写 AST 节点的 url 属性

EXIF 展示

EXIF 本身并不是 Photosuite 的核心创新点,毕竟,也不是我实现的

我只是在 HTML 生成之前,我通过 exiftool-vendored.js 提取图片参数,并将其以文本形式注入到 DOM 中,仅此而已,零 JS 成本、零性能开销

遍历 HTML AST → 找到 img → 解析图片 → 提取 EXIF → 重写节点结构

HTTP 图片的临时下载策略

function isHttpUrl(u: string): boolean {
  const x = new URL(u);
  return x.protocol === "http:" || x.protocol === "https:";
}

对于网络图片,Photosuite 不会直接让 exiftool 处理 URL,而是

  • 下载到 os.tmpdir()
  • 生成随机文件名
  • finally 中清理
const dl = await downloadToTemp(src);
filePath = dl.path;
cleanup = dl.cleanup;

可配置字段

exiftool-vendored.js 解析的 EXIF 数据非常多。这里 Photosuite 做了封装处理,除默认字段外,还可以任意搭配

默认展示字段:

['Model', 'LensModel', 'FocalLength', 'FNumber', 'ExposureTime', 'ISO', 'DateTimeOriginal']

通过 formatField 做语义化输出:

case 'FNumber':
  return `ƒ/${Number(value).toFixed(1)}`;
case 'ExposureTime':
  return value >= 1 ? `${value}s` : `1/${Math.round(1 / value)}s`;
case 'ISO':
  return `ISO ${value}`;

最终输出示例:

ILCE-7CM2 · E 28-200mm F2.8-5.6 A071 · 51.0 mm · ƒ/3.5 · 1/1250 · ISO 1000 · 2025/10/4

实际上,我最初我选择的是 MikeKovarik/exifr

它号称是速度最快、功能最全的 JavaScript EXIF 解析库
结果,我 Nikon z30 拍摄的照片它居然识别不出来机身?我尼康就低人一等吗!果断 PASS!

按需加载

Photosuite 的所有功能都是模块化设计的,样式同样如此

当你关闭某个功能时:

  • 对应的 JavaScript 不会加载

  • 相关 CSS 也不会出现在页面中

实现逻辑:

  1. 检查配置中的 scope(作用域选择器)是否存在于页面中

    • 若不存在,直接终止执行
  2. 根据功能开关配置:

    • enableLightbox, enableAlts, enableExif 并行、动态 import() 对应模块

DOM 标准化

Photosuite 设计了一套统一的 DOM 规范,供所有模块共享

核心思想是:

先把来源复杂的图片元素统一成稳定结构,再在此基础上扩展功能

开关 Photosuite 任意功能都会生成不同的结构,以下是所有功能开启时的最终结构:

<div class="photosuite-item">
	<div class="photosuite-exif"></div>
  <a class="glightbox" href="...">
    <img ... />
  </a>
  <div class="photosuite-caption">alt</div>
</div>

关键逻辑

export function ensurePhotosuiteContainer(el: Element): HTMLElement {
  let target: Element = el;

  // 如果 img 被 a.glightbox 包裹,则提升包裹层级
  if (
    el.tagName.toLowerCase() === "img" &&
    el.parentElement?.tagName.toLowerCase() === "a" &&
    el.parentElement.classList.contains("glightbox")
  ) {
    target = el.parentElement;
  }

  // 如果已经在 photosuite-item 中,直接复用
  const parent = target.parentElement as HTMLElement | null;
  if (parent?.classList.contains("photosuite-item")) {
    return parent;
  }

  // 创建统一容器
  const wrapper = document.createElement("div");
  wrapper.className = "photosuite-item";

  // 用 wrapper 替换 target
  parent?.replaceChild(wrapper, target);
  wrapper.appendChild(target);

  return wrapper;
}

有了这个保证之后,后续逻辑可以完全不关心图片来源

// 查找主体
container.querySelector("img");

// 添加 UI(caption)
container.appendChild(caption);

// 查询数据:有就显示,没有就清理(EXIF)
container.querySelector(".photosuite-exif");

安装

Photosuite 已发布至 npm,可直接安装:

pnpm add photosuite
# or
npm install photosuite
# or
yarn add photosuite

快速开始

配置 Photosuite 非常简单,以Astro为例:

import { defineConfig } from 'astro/config';
import photosuite from 'photosuite';
import "photosuite/dist/photosuite.css";

export default defineConfig({
  integrations: [
    photosuite({
      // [必填] 生效范围选择器
      // 建议限定在文章容器内,避免影响站点其他区域。支持多个选择器,用逗号分隔
      scope: '#main',
    })
  ]
});
import "photosuite/dist/photosuite.css";

Photosuite 基于 Vite + TypeScript 开发,理论上适用于多种架构

如果您在配置中遇到任何问题,欢迎联系我,为爱发电,无偿奉献

后续计划

  1. 引入 Lozad.js 实现图片懒加载
  2. 构建期获取图片尺寸,生成占位符,避免布局抖动
  3. 适配更多博客架构
  4. 进一步优化 Photosuite 的按需加载机制

相关资料

如果 Photosuite 对您有帮助,欢迎在 GitHub 点个 ⭐️
它不会让代码跑得更快,但会让我写得更勤快

PS:如果您正在使用 Photosuite
欢迎告诉我您的博客地址,我会把它展示在项目页面中

  •  

尼康 z30 开箱小记

有些年头没摸相机了。上一部相机还是 2019 年买的“佳能 EOS 200D”,那时 Fooleap 还没有退网,我还在做程序员 《我的第一部单反相机》

一晃眼,六年过去了。一切都变了,我刚入圈时,Fooleap 还在为哄女朋友而苦恼,现在孩子都会打酱油了。当初认识的博友大多已停更,中间我也曾断过更。不过后来因为喜 欢上户外活动,又把博客捡了起来。现在的博客大部分内容被户外生活占据,也是我持续更新的动力

前天,我把 EXIF 样式重写了,但图中的照片画质实在太烂,这对于一个完美主义者来说是无法接受的。一时间想到了相机,早年那台佳能 200D 因为闲置太久早已出掉, 看了些博主评测,最终圈定了两款:“佳能 R50”和“尼康 Z30”。因为 R50 阉割了热靴接口,我选择了尼康 Z30

京东尼康旗舰店下的单,16-50mm 套机,价格是 4869 元。顺丰走了两天半,属实有点慢。

下单送了四样东西:

  • 64G 内存卡
  • 屏幕钢化膜
  • 座充
  • 相机包

如果不要这些赠品可以少 100 元,但想了想没必要,毕竟存储卡总是要买的,差价不大。等以后手头宽裕了再换更好的

上手的第一感觉:这相机真的很小!还没有三星手机大,感觉完全可以揣进兜里,便携性满分

实际试拍了不少照片,全程 M 档,不是过曝就是欠曝。晚上去西湖大道的桥上拍车流,又没控制好,画面还是过曝了

先这样吧,接着回 B 站看摄影教程去了

  •  

我的第一次 GitHub PR

我一直觉得 Artalk 的 ui 设计的很不协调,特别评论框下方的“ 评论数、通知中心"
这并非重要功能,但是官方没有做开关控制。实现后,感觉很实用,可以推一下

确定上游

$ git remote remove upstream
$ git remote add upstream https://github.com/ArtalkJS/Artalk.git
$ git remote -v
origin  git@github.com:achuanya/Artalk-ui.git (fetch)
origin  git@github.com:achuanya/Artalk-ui.git (push)
upstream        https://github.com/ArtalkJS/Artalk.git (fetch)
upstream        https://github.com/ArtalkJS/Artalk.git (push)

因为我 main 分支有非常多改动,比如评论框 UI 等等。那些我不想提交,这里必须创建一个干净的分支

创建分支

$ git fetch upstream
# 基于上游仓库创建一个干净分支
$ git checkout -b clean-pr-branch upstream/master
branch 'clean-pr-branch' set up to track 'upstream/master'.
Switched to a new branch 'clean-pr-branch'

cherry-pick 指定 commit

$ git cherry-pick 6302e4216cc5c1f34df49475ef99e81d883a2d79
CONFLICT (modify/delete): conf/artalk-ui-example.yml deleted in HEAD and modified in 6302e421 新增两个前端配置开关,用于控制列表头部显示).  Version 6302e421 (新增两个前端配置开关,用于控制列表头部显示) of conf/artalk-ui-example.yml left in tree.
Auto-merging conf/artalk.example.simple.yml
Auto-merging conf/artalk.example.yml
Auto-merging ui/artalk/src/defaults.ts
Auto-merging ui/artalk/src/style/list.scss
error: could not apply 6302e421... 新增两个前端配置开关,用于控制列表头部显示
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"

解决冲突

$ git status
On branch clean-pr-branch
Your branch is up to date with 'upstream/master'.

You are currently cherry-picking commit 6302e421.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
        modified:   conf/artalk.example.simple.yml
        modified:   conf/artalk.example.yml
        modified:   conf/artalk.example.zh-CN.yml
        modified:   conf/artalk.example.zh-TW.yml
        modified:   ui/artalk/src/defaults.ts
        modified:   ui/artalk/src/list/list.ts
        modified:   ui/artalk/src/plugins/list/count.ts
        modified:   ui/artalk/src/style/list.scss
        modified:   ui/artalk/src/types/config.ts

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
        deleted by us:   conf/artalk-ui-example.yml

$ git rm conf/artalk-ui-example.yml
rm 'conf/artalk-ui-example.yml'

$ git cherry-pick --continue
[clean-pr-branch 0b5de4d3] 新增两个前端配置开关,用于控制列表头部显示
 Date: Wed Nov 26 02:23:21 2025 +0800
 9 files changed, 61 insertions(+), 3 deletions(-)
 
 $ git push origin clean-pr-branch
Enumerating objects: 39, done.
Counting objects: 100% (39/39), done.
Delta compression using up to 16 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (20/20), 2.75 KiB | 281.00 KiB/s, done.
Total 20 (delta 18), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Resolving deltas: 100% (18/18), completed with 18 local objects.
remote: This repository moved. Please use the new location:
remote:   git@github.com:achuanya/artalk-ui.git
remote:
remote: Create a pull request for 'clean-pr-branch' on GitHub by visiting:
remote:      https://github.com/achuanya/artalk-ui/pull/new/clean-pr-branch
remote:
To github.com:achuanya/Artalk-ui.git
 * [new branch]        clean-pr-branch -> clean-pr-branch

大致意思就是上游没有这个文件 conf/artalk-ui-example.yml 这是我复制的备份配置,用处不大,删了然后推走

使用 GitHub CLI 创建 PR

$ gh auth login
? Where do you use GitHub? GitHub.com
? What is your preferred protocol for Git operations on this host? HTTPS
? Authenticate Git with your GitHub credentials? Yes
? How would you like to authenticate GitHub CLI? Login with a web browser

! First copy your one-time code: C42D-217C
Press Enter to open https://github.com/login/device in your browser...
✓ Authentication complete.
- gh config set -h github.com git_protocol https
✓ Configured git protocol
✓ Logged in as achuanya

$ gh pr create --repo ArtalkJS/Artalk --base master --head achuanya:clean-pr-branch

Creating pull request for achuanya:clean-pr-branch into master in ArtalkJS/Artalk

? Title (required) 新增两个界面配置开关,用于控制:左侧“评论数”、右侧“通知中心” 的显示控制
? Body <Received>
? What's next? Submit
https://github.com/ArtalkJS/Artalk/pull/1113

地址都给出了,说明 PR 已经创建好了
不过 Artalk 官方已经断更几年了,审核过与否都不重要,就当玩了

  •  

Introducing Astro Lhasa 1.0

Astro Lhasa 是一款简洁、响应式、无障碍友好、并具备良好 SEO 的 Astro 博客主题

该主题基于 Astro Paper,并借鉴了好大哥 Fooleap 博客的布局设计

特性

  • [x] 极快的加载性能
  • [x] 内置 SEO 优化
  • [x] 多语言支持(i18n)
  • [x] 支持 Markdown、Mdx
  • [x] 无障碍支持(键盘/旁白)
  • [x] 响应式设计(适配手机到桌面)
  • [x] 本地模糊搜索
  • [x] 草稿文章与置顶
  • [x] GLightbox 图片灯箱
  • [x] 超链接将会在新标签页打开
  • [x] 自动生成 sitemap 与 RSS
  • [x] Expressive Code 代码高亮
  • [x] 全站懒加载滚动分页、返回顶部按钮
  • [x] 基于时间的明暗模式切换(自动适应系统主题)
  • [x] 深度适配 Artalk 评论系统(布局结构和主题配色)
  • [x] 自动展开和折叠的文章目录(可在文章中配置开关)
  • [x] 为 img 标签自动添加原始宽高,支持缓存
  • [x] 图片美化:EXIF 样式、Alt 标签(长条与角标)
  • [x] 图片懒加载,且 src 只需要输入文件名,编译时自动补全路径

Lighthouse 评分

项目结构

/
├── public/
│   ├── assets/
│   └── pagefind/ # auto-generated when build
├── posts/
│   ├── life
│   └── technology
│       └── 2025-11-25-introducing-astro-lhasa-1-0.mdx
├── src/
│   ├── assets/
│   │   ├── icons/
│   │   └── images/
│   ├── components/
│   ├── i18n/
│   ├── layouts/
│   ├── pages/
│   ├── styles/
│   ├── utils/
│   ├── config.ts
│   ├── constants.ts
│   └── content.config.ts
├── ecosystem.config.cjs
└── astro.config.ts

Astro 会根据 src/pages/ 中的 .astro、.md、.mdx 文件自动生成路由

静态资源(如图片)可以放在 public

博客文章都放在 posts 下,创建文件夹后,按照文件夹名自动分类文章

技术栈

主要框架 - Astro 5
语言与类型 - TypeScript
样式 - Tailwind CSS v4@tailwindcss/vite@tailwindcss/typography
代码高亮 - Astro Expressive Code(行号、可折叠段落)
Markdown/MDX 扩展 - remark-mathrehype-katexrehype-figurerehype-slugrehype-autolink-headingsrehype-external-linksrehype-wrap-all 静态搜索 - Pagefind Default UI
图片灯箱 - GLightbox
OG 图像 - Satori + Resvg(站点与文章均支持)
构建优化 - astro-compressor@zokki/astro-minify
友联订阅 - RSS Lhasa
评论系统 - Artalk ui
部署 - Docker/Nginx/Pm2(含 Dockerfiledocker-compose.ymlecosystem.config.cjs
代码规范 - ESLint 9 + Prettier 3

使用方法

启动项目:

pnpm install
pnpm run dev

也可以使用 Docker:

docker build -t astropaper .
docker run -p 4321:80 astropaper

Google 网站验证

你可以通过环境变量轻松在 Astro Lhasa 中添加 Google 网站验证 HTML 标签

# 在你的环境变量文件 (.env) 中
PUBLIC_GOOGLE_SITE_VERIFICATION=your-google-site-verification-value

命令

所有命令都在项目的根目录下,通过终端运行

Command Action
pnpm install 安装依赖
pnpm run dev 启动本地开发服务器,访问地址为 localhost:4321
pnpm run build 构建生产环境网站到 ./dist/
pnpm run preview 在部署之前预览本地构建
pnpm run format:check 检查代码格式是否符合 Prettier 标准
pnpm run format 使用 Prettier 格式化代码
pnpm run sync 为所有 Astro 模块生成 TypeScript 类型 Learn more
pnpm run lint 使用 ESLint 进行代码检查
docker compose up -d 使用 Docker 运行 Astro Lhasa,你可以通过在 dev 命令中提供的相同主机名和端口访问
docker compose run app npm install 你可以在 Docker 容器内运行上述任何命令
docker build -t astrolhasa . 构建 Docker 镜像
docker run -p 4321:80 astrolhasa 在 Docker 中运行 Astro Lhasa,网站将在 http://localhost:4321 上可访问
pm2 start ecosystem.config.cjs 使用 Pm2 启动 Astro Lhasa,访问地址为 localhost:4321

警告!Windows PowerShell 用户可能需要安装 concurrently,如果想在开发过程中运行诊断命令(例如:astro check --watch & astro dev)

License

本项目采用 Anti 996 License 授权协议发布,详情请查阅 LICENSE

  •  

读苏轼有感(1)

今天看了一本关于苏轼的传记,其中有首诗词让我感到震惊的同时又有一种无力感

人生到处知何似?应似飞鸿踏雪泥

回想我这活了二十多年,不过在欲望的追求与失落之间摇摆,实在没有意思

难道说人,这种欲望下的产物,只能困于这悲苦之中不得解脱吗?

没有欲望的人还有人性吗?还是人吗?

大悲寺的和尚不食人间烟火,却求佛问道,这难道不是一种欲望吗?

  •