普通视图

发现新文章,点击刷新页面。
昨天以前保罗的小宇宙

记一次被街头诈骗的经历

作者 Paul
2026年3月16日 11:03

街头借钱诈骗套路复盘.webp

列表的漫展圈年轻人比较多,特意做成了“瓜条”形式希望能被更多人看到。这里也提供了由 AI 改写的大众阅读版本,方便各位博友阅读,大致表达是相似的。

大众版(AI 改写)

受害者:博主
骗子:自称"王艳"的一位阿姨

简要说明:博主热心帮助一位陌生人借钱坐车,却遭遇诈骗。骗子自称是某物流公司的高层员工(生意人),说自己出差后将手机和钱包落在了司机车上,而自己是外地来的孤身一人,丈夫已离异且是限制高消费人员。实际上这些内容全部是编造的,她就是个骗子。

事件经过

3 月 13 日下班后,博主正常休息吃完晚饭,在回公司的路上遇到这位阿姨。她主动过来向博主问路,说自己要去一个很远的地方(深圳宝安机场)。博主正常打开地图给她简单指路,她表示自己没有手机没办法乘车和购票,需要抵达那个位置联系对应的送货司机。博主当时就问她为什么要去这么远的地方,她强调自己是做生意的,在某个物流公司工作(没听清具体名字),在附近谈客户。她还询问博主觉得在附近开一家店的潜力如何,博主表示附近的制造业工厂确实挺多,或许有这个需求(但博主并不是内行人员,她问博主这样的问题博主又该如何回答呢)。

她还会用各种常见话题和博主套近乎,比如问博主年纪多大,是否有谈女朋友。等博主放松警惕后,她开始询问博主的行业、工作地点和薪资,以及是否是本地人、家乡在哪等(博主出于个人保护,都模糊回答了)。她说自己平常在南京发展,如果有机会博主去那边做客,她会请博主吃饭,还强调出门在外多一个朋友能多一份机遇(实际上这是博取信任的话术)。

为了表示自己能还钱,她要加博主微信。那个微信号用的是一个男人的头像,博主当时就表示怀疑,但她说是和丈夫离婚了,这个号现在实际上是她在使用。她还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给博主。

博主用支付宝给她换了现金后,打算给她拍照留存。她当即破防表示博主对她不信任,要求删除照片。博主表示现在骗子太多,这属于正常防范。删除照片后博主本以为事情结束了,但她仍不断用话术影响博主。博主出于好心最终还是借给了她。她当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)。

她承诺第二天还钱,但实际并未归还。截止到这篇文章编写时间,微信好友申请依旧处于未通过状态。

事后复盘:骗局中的疑点

现在简单复盘一下,其实她全程的对话充满了各种套路和漏洞。博主此前也遇到过几次街头骗局并成功逃脱,无奈这次对方特别精明,博主着实是大意了。

疑点一:打车方式不合理

从最开始的问路就挺多问题了。她一直强调自己出差公司报销打车的费用,必须要开发票,而打出租车不能提前支付,只能打表后结算费用。这样她就引导博主以现金支付

博主和她去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际上店长可能已经在警示博主这个人有问题,让博主好好想想,但当时博主因工作疲惫没想到这个可能性)。

疑点二:询问博主的工作安排

因为博主已经说自己在工作了,她还询问博主明天还要不要上班。这实际上很有可能是担心自己骗了博主之后再次在附近作案会被抓获。因为是小额诈骗,警方一般是不予处理的。

疑点三:不向儿子求助

既然她都有收入这么高的儿子了,怎么不能直接求助他呢?这也是一个巨大的疑问。其实完全可以让博主用手机联系她儿子,然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会用其他理由推辞,比如已经和儿子分家了什么的)。

疑点四:情绪激动后的反向施压

她情绪激动后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 博主,明显表示博主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术,恶人先告状,继续利用了博主的善良)。微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之博主根本没有办法找到她。

总结

人在外地的身份是自己给的,今天她是企业员工,明天就可以是霸道总裁。所有的身份信息都无法核实。

博主的这次经历再次向各位亲朋好友发出警钟:面对陌生人一定要提高警惕,对方可能会疯狂输出话术影响你的判断。如果来不及思考,则最好提出找警察或直接拒绝

瓜条版(原创)

苦主热心帮助陌生人借钱坐车惨遭背刺

苦主:匿了
瓜主:自称“王艳”的一位阿姨

省流版:苦主热心帮助陌生人借钱坐车惨遭背刺,瓜主称自己是某物流公司的高层员工(生意人),出差后将手机和钱包落在了司机车上,而自己是外地过来孤身一人,丈夫离异且是限制高消费人员。实则内容全部编造,骗子一个。

事件经过

苦主在 3 月 13 日下班后正常休息吃晚饭,回公司路上遇到瓜主表示要问路,说自己要去一个很远的地方(深圳宝安机场)。苦主也是正常打开地图简单指路,瓜主表示自己没有手机没办法乘车和购票,需要抵达该位置联系对应的送货司机。苦主当即问为什么要去这么远的地方,瓜主强调自己是做生意的,在某个物流公司(没听清),在附近谈客户,还询问我附近开一家店的潜力如何,苦主表示附近的制造业工厂确实蛮多,或许有这个需求(但苦主并不是内行人员,你问我这样的问题我又要如何回答呢)

瓜主还会以各种常用话题和你套近乎,比如年纪多大,是否有谈女朋友,等你放松警惕后,开始询问你的行业、工作地点和薪资,以及是否本地人,家乡在哪等(苦主出于个人保护,均模糊回答)。瓜主说自己平常在南京发展,如果有机会前往做客,瓜主会请苦主吃饭,强调出门在外多一个朋友能多一份机遇,实际为博取信任的洗脑话术

为了表示自己能还钱加了个微信,该微信号是一个男人的头像,苦主当即表示怀疑,但瓜主说是和自己丈夫离婚了,这个号现在实际上是瓜主在使用。瓜主还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给苦主。苦主为其借钱(ZFB 换现金)后,打算给瓜主拍照留存,瓜主当即破防表示苦主对她不信任要求删除照片,苦主表示骗子太多属于正常防范,删除照片后本以为事情结束,但瓜主后续依旧持续输出洗脑苦主,苦主出于好心最终还是借给了瓜主。对方当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)

对方承诺第二天还,但实际未还,截止到瓜条编写时间,微信好友依旧处于未通过状态。

事后复盘

现在简单复盘一下,其实瓜主全程的对话也是充满各种套路和 Bug 疑点的。苦主此前也遇到过几次街头骗局并成功逃脱,无奈此次对方特别精明,苦主着实是大意了。

从最开始的问路就挺多问题了,瓜主一直强调自己出差公司报销打车的费用(必须要开发票,而打出租车不能提前支付,只能打表后结算费用,后续引导苦主以现金支付

苦主和瓜主去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际可能已经在警示苦主这个人有问题,好好想想,但当时苦主因工作疲惫没想到这个可能性)

因为苦主已经说自己在工作了,瓜主还询问过苦主明天还要不要上班(实际很有可能是担心自己骗了苦主后再次在附近作案被抓获)因为是小额诈骗,警方一般是不予处理的。

既然你都有收入这么高的儿子了,怎么不能直接求助他呢,这也是一个巨大的疑问,其实完全可以让苦主手机联系对方然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会以其他的理由推辞,比如已经和儿子分家了什么的)

瓜主破防后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 苦主,明显表示苦主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术不对么,恶人先告状,继续利用了你的善良)微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之你根本没有办法找到对方。

总结

人在外的身份是自己给的,今天我是企业员工,明天我就可以是霸道总裁。苦主的此次经历再次向各位亲朋好友发出警钟,面对陌生人一定要提高警惕,对方可能会疯狂输出洗脑影响你的判断,如果来不及思考,则最好提出找警察或直接拒绝

最后让我们来看看这位丑陋的女人,必须得曝光出来:

骗子“王艳”

虚假的微信号

记一次升级 Nuxt 4 的诡异问题

作者 Paul
2026年3月7日 17:57

前几天 我将 Nuxt 从 3 升级到了 4,升级之后才发现有一个页面出现了异常,CSS 样式出现丢失。

出现问题的 CSS 文件是 styles.module.less,它是一个 CSS Modules 形式的 Less 文件。对应组件的引入方式有些特别,是根据文章内容来渲染的。

// 组件映射表
const componentMap: Record<string, Component> = {
  "photo-collection": PhotoCollection,
};

// 渲染组件的函数
const renderComponent = (componentName: string, props: any) => {
  const Component = componentMap[componentName];

  if (Component) {
    return h(Component, props);
  }

  // 如果组件不存在,返回错误提示
  return h("blockquote", {}, `组件 ${componentName} 不存在`);
};

我依次尝试了一些改动,发现都没解决...

  1. 重命名文件(styles.module.less -> photo-collection.module.less
  2. 修改文件内容(.collection 下样式只写一条 background 规则)
  3. 将引入方式调整成 ifelse 的形式

generateScopedName 解决方案

只好求助牛逼的 Claude Opus 4.6,初步排查原因是开发模式和生产模式下生产的 CSS Hash 不一致。


找到根本原因了!

  • 客户端 JS bundle 用的哈希:_collection_f6c82_1
  • 服务端 SSR bundle 用的哈希:_collection_x8r5c_1
  • CSS 文件 里的规则:._collection_f6c82_1(和客户端一致,和服务端不一致)

这就是问题所在:CSS Module 的类名哈希在 SSR 构建和客户端构建之间不一致。

具体表现:

  1. 服务端渲染 HTML 时,生成的 class_collection_x8r5c_1
  2. 浏览器加载的 CSS 文件里只有 ._collection_f6c82_1 的规则
  3. 两者对不上,样式完全不生效

这与 componentMap 的写法无关,所有前面的方案当然也都没用——核心问题是 Vite 在 SSR 和 client 两次构建中,传给 CSS Modules 的 id(文件路径)不一致,导致哈希不同。

修复方案是在 nuxt.config.ts 里显式配置 generateScopedName,让两次构建使用同样的规则生成类名,消除哈希不一致问题:

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string) {
          const normalizedPath = filename
            .replace(/\?.*$/, "")
            .replace(/\\/g, "/");
          const relativePath = path.relative(process.cwd(), normalizedPath).replace(/\\/g, "/");
          const hash = crypto
            .createHash("md5")
            .update(relativePath)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

总感觉这是一个很奇怪的修复方式,理论上 Vite 自己的配置都是经过验证的,自己魔改的配置越多,后续的升级只会更容易出现问题,想着能不能从其他方面再继续分析下其他的解决方法。

修改后缀名方案

我把这件事告诉给了 @Innei,先被他 PUA 了一遍

怎么还在用这么古老的技术栈

骂归骂,能有些解决问题的思路也好。他说他现在使用的“不古老”的方式是 vanilla-extract 这个库,看上去是类似 CSS in JS 的那种编写方式,但是实际背后处理返回的是一个 CSS 文件,看上去亮点主要是 TypeScript 友好。

用 Less 有啥优势

我说没有啥优势,只是好过全局 CSS 而已(因为是 CSS Modules 形式),当时没转过来指的是 Less 本身。

选择 Less 主要是主题色盘的色值可以直接用函数生成,而不是自己算好再写上去。并且它有 CSS 嵌套功能,从小窝后台一直用了好几年,也没啥问题就一直在用。

当然现在这些特性其实原生都有支持了,颜色计算有 color-mix,嵌套兼容性还不够好但也能用(Chrome 要求 120 版,iOS Safari 要求 17.2 的系统),继续沿用也只是为了保持兼容性而已。

我尝试直接将文件的后缀从 .less 改成 .css,居然也是解决的。Innei 看到我的惊喜若狂(卧槽,去掉还真好了,什么原理啊),什么话都没说。

为什么,什么原理

其实到这里就已经找到了最合适的方案了,因为简单直接还不需要改配置。我其实也好奇这两个修改方式为什么有效,也让 AI 替我分析了一下:

修改后缀名的原理


Vite 处理 CSS Modules 时,哈希是根据传给 postcss-modules 的 from(即文件路径)计算的。

.module.css(纯 CSS)的处理流程:

style.module.css → postcss-modules(from = 原始文件路径)→ 哈希

路径简单直接,SSR 和 client 两次构建完全一致 → 哈希相同

.module.less(LESS 预处理器)的处理流程:

style.module.less → LESS 编译器 → 中间产物(id 可能附带 ?used / ?inline 等 query)
                                         ↓
                              postcss-modules(from = 中间产物的路径)→ 哈希

Client build 和 SSR build 内部对中间产物的 id 处理方式不同,传给 postcss-modulesfrom 路径不一致 → 哈希不同

这是 Vite 在处理 CSS 预处理器(LESS/SCSS/Stylus)与 CSS Modules 组合时的一个长期存在的 SSR 一致性问题,在 LESS 这里表现得尤为明显。

你用 .css 替换 .less 已经是最干净彻底的修复方式了,不需要任何额外配置。我之前加的 generateScopedName 现在可以去掉,因为它只是用来绕过这个问题的。


这里我单独重命名更新了那个出问题的 .module.less 文件后,其实并没有彻底解决问题。

generateScopedName 的原理

奇怪的是,第一种方法,那怕采用最直接的 文件名 + 路径 生成 Hash,也是正常的,根本不像 AI 说的那样,是文件路径的问题。

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string, css: string) {
          const hash = crypto
            .createHash("md5")
            .update(filename)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

我继续追问它,为什么简化了这个处理函数,也能解决 CSS 的异常:


真相:问题不在 filename,在 Vite 内部的 hash 计算

你说得对——generateScopedNamefilename 参数在两次构建里看上去是一样的。这是因为 Vite 在把 filename 传给用户定义的 generateScopedName 之前,已经做了 clean url 处理(去掉 query 参数)。

但 Vite 自己内部不走这个用户函数。它有一套独立的 hash 计算逻辑,用的是原始的 Vite module ID(可能带 query 参数)。

用简图表示:

.module.less(SSR build)  → id = "/path/style.module.less?used"  → hash = x8r5c
.module.less(client build)→ id = "/path/style.module.less"       → hash = f6c82
.module.css(SSR build)   → id = "/path/style.module.css"        → hash = abc12
.module.css(client build)→ id = "/path/style.module.css"        → hash = abc12 ✅

LESS/SCSS 等预处理器文件在 SSR 构建过程中会被打上 ?used 这类内部标记(Vite 用它区分 SSR 上下文里"已使用"的 CSS),导致原始 id 和 client build 不一样。纯 CSS 文件不走预处理器流程,没有这个标记,id 始终一致。

用户定义 generateScopedName 之所以能修复问题,不是因为 replace 做了什么归一化,而是因为只要你定义了这个函数,Vite 就把 hash 计算权交给你,并传入 cleaned filename。你写 filename + name 直接 hash 就够了,两次构建拿到的是同一个 cleaned filename,自然一致。

所以结论是

方案原因
换成 .module.css绕开了预处理器流程,Vite 不会附加 ?used 标记
加 generateScopedName接管了 hash 计算,Vite 给你的是 cleaned filename

你换 .css 是最干净的解法,不引入任何额外配置。如果项目里还有其他用 .module.less/.module.scss 的组件,遇到同样问题时,加一个简单的 generateScopedName 也能解决,不需要任何 replace,直接用 filename + name 哈希就够了。


这个答案我也不确定是否正确,我就将自己的“所见所得”告诉 AI,因为我发现其他的 .module.less 并没有遇到这个问题。其实这里误导了 AI,后面的内容就当看看笑话吧。

因为那些没有问题的 .module.less 其实都是走的 CSR 渲染,用了 <ClientOnly> 组件包裹,一个页面通过路由方式进入的都是 CSR,必须刷新才是 SSR,而我验证其他组件发现没问题,都是因为走了 CSR 模式

这时候 AI 给我的“猜测”就开始有偏差了,说是什么代码分割的问题,还说是因为我这个组件只有一个页面在用,实际我也验证过了和这个根本没关系,一个组件被多个页面使用也是一样的问题(地址栏直接输入对应 URL,走的是 SSR 渲染)


关键差异:代码分割(Code Splitting)
看引用次数:

组件被引用的页面数Vite 打包结果
PhotoCollection1 个页面([[slug]])→ 打进该页面专属的 独立 chunk
Pager5 个页面→ 被提取到 shared chunk
ThemePanellayout(全局)→ 被提取到 shared chunk
Modal、Notice 等多个页面→ shared chunk

Vite 发现 PhotoCollection 只有一个页面用,就把它和它的 CSS 打进了 [[slug]] 页面专属的 code-split chunk 里。

问题就出在这里:在 Vite 的 SSR 构建中,对于这种"只属于某一个页面"的 code-split chunk,模块 ID 的生成方式和 client build 不一致(SSR build 会附加额外标记来防止 CSS 被注入到所有页面),导致 postcss-modules 拿到不同的 hash 输入,输出不同的类名。

Shared chunk 里的组件因为 ID 在两次构建中是稳定一致的,所以没有这个问题。

PhotoCollection(独立 chunk):
  SSR build:    id = "/path/photo-collection.module.less?used"  → x8r5c ❌
  client build: id = "/path/photo-collection.module.less"       → f6c82
Pager(shared chunk):
  SSR build:    id = "/path/pager.module.less"   → a1b2c ✅
  client build: id = "/path/pager.module.less"   → a1b2c

如果你把 PhotoCollection 改成也在两个以上的页面引用,Vite 就会把它提取进 shared chunk,.module.less 的问题自然也就消失了。 所以这个问题的触发条件非常精确:.module.less + 只被单个页面引用。


这个推理也是错误的,我继续追问它另外一个组件的 .module.less 没问题,其实也是有问题的(因为是 CSR),最后它也没办法了。

总结

如果想要彻底修复这个 Bug,需要将所有的 .module.less 都改成 .module.css 才行。它并不是其中一个文件才会引发的。

要么就是按照上面的方案修改 generateScopedName 配置,看来这个的确是 Vite 内部和 Less 衔接导致的异常,只是我一直没找到具体的证据。

当然也如 @Innei 所言,Less 现在确实被淘汰了,Antd 也早就迁移变成了自己的 CSS in JS 实现,我还是让 AI 整体优化重写,拥抱最新技术吧!

记一次升级 Nuxt 4 的诡异问题

作者 Paul
2026年3月7日 17:57

前几天 我将 Nuxt 从 3 升级到了 4,升级之后才发现有一个页面出现了异常,CSS 样式出现丢失。

出现问题的 CSS 文件是 styles.module.less,它是一个 CSS Modules 形式的 Less 文件。对应组件的引入方式有些特别,是根据文章内容来渲染的。

// 组件映射表
const componentMap: Record<string, Component> = {
  "photo-collection": PhotoCollection,
};

// 渲染组件的函数
const renderComponent = (componentName: string, props: any) => {
  const Component = componentMap[componentName];

  if (Component) {
    return h(Component, props);
  }

  // 如果组件不存在,返回错误提示
  return h("blockquote", {}, `组件 ${componentName} 不存在`);
};

我依次尝试了一些改动,发现都没解决...

  1. 重命名文件(styles.module.less -> photo-collection.module.less
  2. 修改文件内容(.collection 下样式只写一条 background 规则)
  3. 将引入方式调整成 ifelse 的形式

generateScopedName 解决方案

只好求助牛逼的 Claude Opus 4.6,初步排查原因是开发模式和生产模式下生产的 CSS Hash 不一致。


找到根本原因了!

  • 客户端 JS bundle 用的哈希:_collection_f6c82_1
  • 服务端 SSR bundle 用的哈希:_collection_x8r5c_1
  • CSS 文件 里的规则:._collection_f6c82_1(和客户端一致,和服务端不一致)

这就是问题所在:CSS Module 的类名哈希在 SSR 构建和客户端构建之间不一致。

具体表现:

  1. 服务端渲染 HTML 时,生成的 class_collection_x8r5c_1
  2. 浏览器加载的 CSS 文件里只有 ._collection_f6c82_1 的规则
  3. 两者对不上,样式完全不生效

这与 componentMap 的写法无关,所有前面的方案当然也都没用——核心问题是 Vite 在 SSR 和 client 两次构建中,传给 CSS Modules 的 id(文件路径)不一致,导致哈希不同。

修复方案是在 nuxt.config.ts 里显式配置 generateScopedName,让两次构建使用同样的规则生成类名,消除哈希不一致问题:

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string) {
          const normalizedPath = filename
            .replace(/\?.*$/, "")
            .replace(/\\/g, "/");
          const relativePath = path.relative(process.cwd(), normalizedPath).replace(/\\/g, "/");
          const hash = crypto
            .createHash("md5")
            .update(relativePath)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

总感觉这是一个很奇怪的修复方式,理论上 Vite 自己的配置都是经过验证的,自己魔改的配置越多,后续的升级只会更容易出现问题,想着能不能从其他方面再继续分析下其他的解决方法。

修改后缀名方案

我把这件事告诉给了 @Innei,先被他 PUA 了一遍

怎么还在用这么古老的技术栈

骂归骂,能有些解决问题的思路也好。他说他现在使用的“不古老”的方式是 vanilla-extract 这个库,看上去是类似 CSS in JS 的那种编写方式,但是实际背后处理返回的是一个 CSS 文件,看上去亮点主要是 TypeScript 友好。

用 Less 有啥优势

我说没有啥优势,只是好过全局 CSS 而已(因为是 CSS Modules 形式),当时没转过来指的是 Less 本身。

选择 Less 主要是主题色盘的色值可以直接用函数生成,而不是自己算好再写上去。并且它有 CSS 嵌套功能,从小窝后台一直用了好几年,也没啥问题就一直在用。

当然现在这些特性其实原生都有支持了,颜色计算有 color-mix,嵌套兼容性还不够好但也能用(Chrome 要求 120 版,iOS Safari 要求 17.2 的系统),继续沿用也只是为了保持兼容性而已。

我尝试直接将文件的后缀从 .less 改成 .css,居然也是解决的。Innei 看到我的惊喜若狂(卧槽,去掉还真好了,什么原理啊),什么话都没说。

为什么,什么原理

其实到这里就已经找到了最合适的方案了,因为简单直接还不需要改配置。我其实也好奇这两个修改方式为什么有效,也让 AI 替我分析了一下:

修改后缀名的原理


Vite 处理 CSS Modules 时,哈希是根据传给 postcss-modules 的 from(即文件路径)计算的。

.module.css(纯 CSS)的处理流程:

style.module.css → postcss-modules(from = 原始文件路径)→ 哈希

路径简单直接,SSR 和 client 两次构建完全一致 → 哈希相同

.module.less(LESS 预处理器)的处理流程:

style.module.less → LESS 编译器 → 中间产物(id 可能附带 ?used / ?inline 等 query)
                                         ↓
                              postcss-modules(from = 中间产物的路径)→ 哈希

Client build 和 SSR build 内部对中间产物的 id 处理方式不同,传给 postcss-modulesfrom 路径不一致 → 哈希不同

这是 Vite 在处理 CSS 预处理器(LESS/SCSS/Stylus)与 CSS Modules 组合时的一个长期存在的 SSR 一致性问题,在 LESS 这里表现得尤为明显。

你用 .css 替换 .less 已经是最干净彻底的修复方式了,不需要任何额外配置。我之前加的 generateScopedName 现在可以去掉,因为它只是用来绕过这个问题的。


这里我单独重命名更新了那个出问题的 .module.less 文件后,其实并没有彻底解决问题。

generateScopedName 的原理

奇怪的是,第一种方法,那怕采用最直接的 文件名 + 路径 生成 Hash,也是正常的,根本不像 AI 说的那样,是文件路径的问题。

export default defineNuxtConfig({
  vite: {
    css: {
      modules: {
        generateScopedName(name: string, filename: string, css: string) {
          const hash = crypto
            .createHash("md5")
            .update(filename)
            .digest("hex")
            .slice(0, 5);
          return `_${name}_${hash}`;
        },
      },
    },
  },
});

我继续追问它,为什么简化了这个处理函数,也能解决 CSS 的异常:


真相:问题不在 filename,在 Vite 内部的 hash 计算

你说得对——generateScopedNamefilename 参数在两次构建里看上去是一样的。这是因为 Vite 在把 filename 传给用户定义的 generateScopedName 之前,已经做了 clean url 处理(去掉 query 参数)。

但 Vite 自己内部不走这个用户函数。它有一套独立的 hash 计算逻辑,用的是原始的 Vite module ID(可能带 query 参数)。

用简图表示:

.module.less(SSR build)  → id = "/path/style.module.less?used"  → hash = x8r5c
.module.less(client build)→ id = "/path/style.module.less"       → hash = f6c82
.module.css(SSR build)   → id = "/path/style.module.css"        → hash = abc12
.module.css(client build)→ id = "/path/style.module.css"        → hash = abc12 ✅

LESS/SCSS 等预处理器文件在 SSR 构建过程中会被打上 ?used 这类内部标记(Vite 用它区分 SSR 上下文里"已使用"的 CSS),导致原始 id 和 client build 不一样。纯 CSS 文件不走预处理器流程,没有这个标记,id 始终一致。

用户定义 generateScopedName 之所以能修复问题,不是因为 replace 做了什么归一化,而是因为只要你定义了这个函数,Vite 就把 hash 计算权交给你,并传入 cleaned filename。你写 filename + name 直接 hash 就够了,两次构建拿到的是同一个 cleaned filename,自然一致。

所以结论是

方案原因
换成 .module.css绕开了预处理器流程,Vite 不会附加 ?used 标记
加 generateScopedName接管了 hash 计算,Vite 给你的是 cleaned filename

你换 .css 是最干净的解法,不引入任何额外配置。如果项目里还有其他用 .module.less/.module.scss 的组件,遇到同样问题时,加一个简单的 generateScopedName 也能解决,不需要任何 replace,直接用 filename + name 哈希就够了。


这个答案我也不确定是否正确,我就将自己的“所见所得”告诉 AI,因为我发现其他的 .module.less 并没有遇到这个问题。其实这里误导了 AI,后面的内容就当看看笑话吧。

因为那些没有问题的 .module.less 其实都是走的 CSR 渲染,用了 <ClientOnly> 组件包裹,一个页面通过路由方式进入的都是 CSR,必须刷新才是 SSR,而我验证其他组件发现没问题,都是因为走了 CSR 模式

这时候 AI 给我的“猜测”就开始有偏差了,说是什么代码分割的问题,还说是因为我这个组件只有一个页面在用,实际我也验证过了和这个根本没关系,一个组件被多个页面使用也是一样的问题(地址栏直接输入对应 URL,走的是 SSR 渲染)


关键差异:代码分割(Code Splitting)
看引用次数:

组件被引用的页面数Vite 打包结果
PhotoCollection1 个页面([[slug]])→ 打进该页面专属的 独立 chunk
Pager5 个页面→ 被提取到 shared chunk
ThemePanellayout(全局)→ 被提取到 shared chunk
Modal、Notice 等多个页面→ shared chunk

Vite 发现 PhotoCollection 只有一个页面用,就把它和它的 CSS 打进了 [[slug]] 页面专属的 code-split chunk 里。

问题就出在这里:在 Vite 的 SSR 构建中,对于这种"只属于某一个页面"的 code-split chunk,模块 ID 的生成方式和 client build 不一致(SSR build 会附加额外标记来防止 CSS 被注入到所有页面),导致 postcss-modules 拿到不同的 hash 输入,输出不同的类名。

Shared chunk 里的组件因为 ID 在两次构建中是稳定一致的,所以没有这个问题。

PhotoCollection(独立 chunk):
  SSR build:    id = "/path/photo-collection.module.less?used"  → x8r5c ❌
  client build: id = "/path/photo-collection.module.less"       → f6c82
Pager(shared chunk):
  SSR build:    id = "/path/pager.module.less"   → a1b2c ✅
  client build: id = "/path/pager.module.less"   → a1b2c

如果你把 PhotoCollection 改成也在两个以上的页面引用,Vite 就会把它提取进 shared chunk,.module.less 的问题自然也就消失了。 所以这个问题的触发条件非常精确:.module.less + 只被单个页面引用。


这个推理也是错误的,我继续追问它另外一个组件的 .module.less 没问题,其实也是有问题的(因为是 CSR),最后它也没办法了。

总结

如果想要彻底修复这个 Bug,需要将所有的 .module.less 都改成 .module.css 才行。它并不是其中一个文件才会引发的。

要么就是按照上面的方案修改 generateScopedName 配置,看来这个的确是 Vite 内部和 Less 衔接导致的异常,只是我一直没找到具体的证据。

当然也如 @Innei 所言,Less 现在确实被淘汰了,Antd 也早就迁移变成了自己的 CSS in JS 实现,我还是让 AI 整体优化重写,拥抱最新技术吧!

记一次移动端 Safari 调试踩坑

作者 Paul
2026年1月22日 02:54

最近在做公司项目一个 H5 版本的页面优化,合并到测试环境之后,在手机 Safari 下有部分样式问题,需要联机即时调试更快的解决,但在联机过程遇到了不少坑,这里简单记录一下问题和解决方法。

如果想要在移动端真机上调试,就不能使用 localhost 这种地址了,使用局域网 IP 地址又有可能遇到请求后端 API 出现跨域的问题,我的解决方法是选择建立一个与测试、生产环境共享根域的二级域名(当然前提后端也允许了该二级域名的跨域行为),再路由器上设置 Hosts 连接到开发机的方案。

比如 paul.me 是生产环境,local.paul.me 就是本地开发环境,强制指定 IP 到开发机,这样就能保留此前传递和获取登录态的逻辑代码

访问失败问题

起初在另外一台 Mac 电脑上直接访问开发机对应的页面,访问失败,使用 ping IP 则正常,未分析到具体原因,重新连接网络后得到了解决。

重定向问题

我在手机上打开本地调试地址后,发现会被强制使用 HTTPS,可我完全是本地服务,怎么可能会有重定向?通过搜索得知,这里使用到了一个叫 HSTS 的技术,因为访问生产环境的时候就是根域名,可能在根域名下存在 HSTS 记录,导致所有子域都会强制重定向一次。

在 iPhone 的 Safari 上,需要在 Safari 设置里面找到网站数据,将整个域名下的清空(包括登录态等均被删除),之后重新打开浏览器访问本地调试环境,就没遇到 307 跳转了。

联机调试崩溃

我原本想用电脑上的 iOS 模拟器来试试的,但是发现 Safari 的调试工具一直有问题,无论是“元素”还是“控制台”都是空白无内容的,重开多次均无效。后尝试使用 iPhone 13 真机调试,但是首次设置就遇到了登录态设置上去但没用的情况。

最后我的解决方法是直接在控制台里面设置 Cookie,然后去调整 Cookie 对应的域名和 Path 值为 /,后续请求其他页面就没有掉登录态的情况。

但是在我需要调试的关键页面,手机一打开就出现崩溃甚至闪退的情况。经过检查发现本地调试环境该页面的打包代码居然有 100MB+,可能就是这个原因导致的,手机直接不堪重负。

因为该页面的路由完全和 PC 共享,仅仅只是一个 if 条件判断来返回手机版的页面,因此这里有完整打包的 PC 端的代码(屎山),目前我直接注释掉了 PC 端的代码,NextJS 自动给我去除掉了,JS 文件瞬间缩小到了 2MB,调试终于得以正常进行...

样式问题记录

简单来说就是有一个 button 元素,内部有一个 div 设置了 aspect-ratio: 1/1 强制设置比例,结果发现元素塌陷,并没有支撑起来。对于这个问题我发现有个最简单的办法,就是将元素替换成 div,就解决了。挺诡异的,我只知道 button 相较于 div 会有一个额外的 appearence 属性,但我修改它也并没有用。

具体 Demo 可见 Safari 按钮高度问题

这件事说明目前还是不能完全信任 AI 写出来的代码,依旧得自己充分验证才知道有没有问题。

记一次装机没有一次点亮的排查过程

作者 Paul
2026年1月5日 18:18

上周日我帮同事组装了一台主机,配件弄好之后一步一步安装,到最后发现按下电源键居然没有任何反应,我顿时有点慌了...

硬件

配置单

  • CPU:英特尔 酷睿 i5 12700KF
  • 主板:微星 B650M-A WIFI
  • 内存:阿斯加特 16+16G DDR4 3600
  • 硬盘:闪迪 西部数据 SN510C 1TB
  • 显卡:华硕 DUAL RTX 5060
  • 电源:微星 铜牌 650W A650BNL
  • 散热器:利民 PA120SE

按道理主板这玩意也不会这么脆弱,除非从最开始压 CPU 就压坏了,那就确实是完全不可逆的了。参考了网上的一些视频,我最后总结了下面的排查方案和过程。

硬件排查

排查过程主要还是从硬件开始,首先是电源方面是否完全接到位,一个是主板的接口,一个是 CPU 的接口,这两个供电缺一不可。这里我看 CPU 部分主板有 8 + 4 Pin 的接口,而我只接了其中一部分,我首先把这里额外的 4 Pin 接上,这里的 4 Pin 似乎是给更高规格的 CPU 预留的,我也不是很确定接与不接是否可能有什么影响。

接上后发现并没有用,和我自己的华硕主板不同的是他这款微星的主板并没有自带灯光,没启动电源之前没有任何明显的效果,我尝试插了个 USB 设备上去也是没电的,但有可能在首次启动之前它就是不会有任何供电的。

接下来我在排查电源键的接线,是否可能接错,不排除线本身的问题的话可以试试螺丝刀短接一次电源部分(我之前就是没机箱这样裸板启动),结果我发现这也没有用。

那么这里最大的嫌疑就是电源接口压根就不在这里,主板本身的安装说明写的位置不太清楚,我在网上搜同型号的主板安装教程,发现这里确实是接错位置了,接到了另外一处孔位一致的地方。

这时我按下电源键,主板上的风扇灯亮起来了,这让我悬着的心放了下来,但是屏幕却始终黑屏,主板上的 Debug 灯也一直在亮,提示在 Boot 状态,为什么呢。

这里我首先怀疑的是显卡,果不其然显卡的风扇被一处线头卡住了,但解决好之后也并不管用,可显卡风扇能转说明接线和电源肯定也是 OK 的,为什么呢?

在我录制视频给 @提莫 看的时候,发现显示器突然就能亮了,非常的奇怪。并且能够正常的进入主板 BIOS 设置以及 Windows PE 系统。待我将 Windows 11 写入磁盘重启后,似乎它怎么重启都无法点亮。强制断电之后有概率能进去,但是提示系统损坏需要重新部署安装。

我重新部署了一次系统镜像,重启后也是一样的问题,只要按下开机键之后就一直黑屏,似乎走到部署系统阶段之后就是稳定黑屏状态。

现在的系统都是 UEFI 模式混合启动,从开机的瞬间就已经到系统层面了,这里有可能是软件问题导致的。

软件排查

我在网上搜索类似案例,得到的方案绝大多数都在说是电源问题,但有一个人的经验给了我非常大的帮助,说是装机不能使用 4K 屏幕。

巧的是我去年确实就弄了一个 4K 显示器作为主力使用,客厅的电视也是 4K 的,就没有那种低分辨率的屏幕了啊,怎么办呢。死马当活马医,试了下客厅电视,结果能用,被电视识别成了 1080P 模式。

安装 Windows 的界面也因此正常显示,虽然有花屏的情况(但我更怀疑是电视的问题,因为打开设置界面也是花的),在这里我只能说都 2025 年了,怎么这 Windows 内置的驱动还是这么垃圾,4K 显示器居然都不能原生支持...

提莫:Windows 驱动太老了,因为默认的驱动是 CPU 软解,那个叫做 Microsoft 基本显示适配器来着?那个是 CPU 做的,因为没 5060 的驱动

至此我在客厅电视完成了系统和驱动的安装,之后再插入到 4K 显示器操作,尝试了几次都能正常启动。后续安装了游戏《鸣潮》简单测试了一下性能和流畅度,4K 60 帧的刷新率还是轻轻松松。

鸣潮实机

卡提

2024 年终总结

作者 Paul
2025年9月12日 00:55

这篇总结酝酿的时间也挺长了,主要还是写了不少去漫展的事情。今年主要加强了在摄影方面的技能点,写码方面没有以前那么上进,已有项目的维护不多,也没有开新项目了。

阅读指南:

本文内容较长,图片数量也比较多,流量预警!如果速度较慢可以尝试使用科学上网!
如果是博客和项目相关可直接往下阅读,漫展相关可直接点击 传送门

项目与开源

新版小窝前台

新版小窝前台截图

3 月末,我酝酿了几个月的新版设计风格,正式上线。主要使用了 Vue 3 + Nuxt 框架来开发,依旧是手搓组件和功能,虽说旧版依旧还保留着,只是前端部分已经不再维护了。

此后都在不断优化各种小细节,这些改动我认为比较值得列出来:

  • 4/12:背包页设备点击可查看详情
  • 4/13:在首页和歌单页以波形动画形式,展示当前播放的音乐
  • 5/28:获取表情数据更换为 setting/list 新接口,并改为状态管理存储初始值

    • Nuxt 和 Pinia 集成度不错,能直接初始化值到 Store 里
  • 6/15:修改 SEO 关键词,解决 SVGO 砍掉动画的问题,更新滚动条样式

    • 这个修改记录了一个比较奇葩的问题
  • 7/27:新增在读详情快照页,调整在读页面布局

    • 给在读增加了快照功能,以对抗链接可能失效的问题
  • 9/8:相册单独的灯箱组件,展示照片 EXIF 详细参数

    • 为相册单独制作了一个灯箱组件,算是正式把相册“专业化”了,摄影佬的那种感觉。
  • 12/24:优化日记列表页媒体过多的阅读体验,默认折叠显示剩余图片数量

    • 这条记录的更改几乎都在使用 AI 编写代码,当然样式细节还是由本人优化

小窝后端

基本上都是以服务前端为主的改动,也是简单挑出一些值得列出来的:

  • 2/18:新增在读功能
  • 2/27:增加 useTokenPermission 授权方案,在读增加接口支持 Token 授权

    • 给插件做了一个类似“授权码”的机制,不需要登录即可执行某些接口的操作
  • 3/27:尝试解决 B 站视频不能获取的问题

    • B 站再一次更新了 API 的校验,参考网上的方案尝试解决
  • 4/14:API 请求错误显示成 JSON

    • 询问 AI 用了一种兜底的方案,防止 PHP 逻辑出错打印出非 JSON 格式的内容
  • 4/21:修复路由 GET 的参数只带一个问号去请求会把问号当成请求名称的一部分的问题
  • 4/21:新增追番进度更新接口

    • 通过插件来实施更新追番进度,数据维护更自动化了
  • 5/7:引入 Composer 和 PHPMailer

    • 芜湖,终于在 PHP 上使用包管理器引入第三方代码了
  • 5/10:基础页面的 Sitemap 功能,后续需要补上日记、项目的

    • 目前博客的流量依旧远大于小窝,增加 Sitemap 功能或许能让小窝的收录比之前更好?
  • 5/27:增加判断是否有丢失源文件的媒体接口

    • 之前不是因为我误操作丢失过很长一段时间的图片么,这个接口是进一步检查缺失问题的,目前 404 的媒体应该减少到 30 条以内了
  • 7/27:完善在读接口的快照参数

    • 配合前端完成快照功能
  • 7/27:新增快照内容转存图片资源的接口

    • 就是提取快照里的图片转存一份到服务器上,不然可能会有防盗链的问题,不过后台并没有此功能的入口
  • 9/19:兼容 PHP8

  • 9/20:不知道为什么查表结果不是 string 而是 int 了,先改了吧

    • 不知道是升级了 PHP 版本还是 Medoo,返回数据类型变了导致我的判断出错了
  • 9/21:修改跨域相关设置,允许我的网站访问

    • 后台地址更换成二级域名来部署了,又遇到了一些新的问题尝试解决
  • 11/20:概括 RSS 日记中事情实际发生的时间

    • 主要是让 Follow 订阅的用户能了解原文的实际发生时间

小窝后台

  • 1/11:批量上传媒体,未上传完毕阻拦跳转其他页面
  • 1/11:批量上传媒体,中间出现错误会直接结束循环并提示错误,可重新编辑上传队列
  • 1/14:分页器仅显示部分页码
  • 1/20:修复通知组件动画异常问题

    • 这就涉及到 React 的渲染机制了,如果 key 使用 index 则是原地重新 document.createElement 了(猜测)
    • 反过来至少用时间戳+随机字符串定义一个唯一值,则动画正常,不会因为其中一个元素消失重新播放
  • 2/19:新增在读功能
  • 2/29:在读字段扩展,增加作者、站点名、标签
  • 3/5:杂项内容修改保存前校验 JSON 格式是否合法
  • 5/30:useRequest Hooks 新增缓存功能

    • 虽然只是函数名称作为键名,主要是给相册分类使用
  • 6/12:新增精选媒体功能
  • 7/12:页面新增插图参数
  • 8/21:编辑在读文章快照内容
  • 9/21:增加跨域允许设置 Cookie 相关逻辑
  • 12/6:日记音乐播放器改为全局,页眉可控制(AI 写的
  • 12/11:存储音乐播放列表功能

小窝工具箱插件

小窝工具箱插件截图

在去年的总结里我首次提到了这个项目,但我似乎并没有讲清楚它到底是干嘛的。它主要就是程序自动化为了提高效率而生的,随着网站功能的不断更新,手动维护网站上的数据只会越来越难,这个项目就是为了解决这个问题,直接把对应功能集成在浏览器上,点击一下就能完成我曾经需要手动复杂操作的流程。

例如我的赛博手办展示页,需要不断的增加新手办的图片和资料,按照以往只能使用 FTP 手动上传并维护 JSON 数据,而现在只需要打开 B 站对应的购买页面,就可以自动填写信息,提交表单后也同时完成了手办图片的上传。

以及追番进度的保存,原先只能在后台手动填写编辑,现在虽说还是没能做到完全自动化,需要进入对应番剧的详情页面后手动点击按钮完成同步,但也比原先要更快捷了!

还有一些方便我写内容的实用功能,例如复制当前网页链接,以 Markdown 格式复制到剪贴板,都是浏览器没有内置,但对于我而言使用频繁的琐碎需求。

因为这个插件而给网站新开发的「在看」功能,倒是没有特别多的突破和改进。我主要在 7 月份增加了快照功能,用于保存网站的文章内容,以对抗国内动不动 404 的特色。想要抓取的更精准,只能根据不同的网站做一些适配,或者后期直接允许选择内容。

社交

或许是我这几年没做出过太惊艳的东西,线上社交平台方面的进步不是特别大,而且编程方面的技术提升也不算太高(不像隔壁 @Innei 已经是 NextJS 人气博主了)。下半年的社交活动都主要在线下了,通过漫展和摄影,认识了不少新的朋友,在后面我也会单独详细介绍。

人生就像一列有去无回的列车,中途陆陆续续有人上车,也有人下车。你会遇到形形色色的人,有人只是擦肩而过,有人会短暂停留,有人陪你看沿途的风景,很少有人陪你到一直到终点!但是值得庆幸的是一路上都有人上车陪你。

这句话我挺认同的,有时候一段关系就慢慢的变得很难维持了。有位好兄弟结婚了之后,可能圈子变了吧,就几乎没有主动和我有过联系了,而我也不知道如何再继续和他保持联系。

漫展上的邂逅

看过我此前写的博客/日记的朋友应该都知道我比较腼腆、社恐,且圈子小(同学/同事/社团/网站博主/同行)没有多少异性朋友。自从上班之后就已经被工作困住,几乎没有什么别的认识人的机会。这样下去怎么可能会遇到一个能互相看对眼的人呢?

我决定扩展交际圈,并尝试着克服社恐。我的群友给我提出了一些想法,去年 11 月(应该是),他们首次提到了去漫展这个话题。

我了解到本地比较有知名度的 AS 漫展,加了企鹅群一段时间,也在观察着他们平时讨论的内容,找机会与他们互动。刚开始的时候其实挺困难的,群里都是陌生面孔,发“找搭子”也没人理会,加群之后开了几次展子我都没有去。

今年 6 月,再三打听后我注意到一个免费展,在约不到朋友的情况下,我还是选择独自一人去看了看。

第一次去漫展

在现场我顿时觉得非常的尴尬,没有一个认识的人,也感觉自己和他们完全不在一个频道。我在群里尝试捞人混个脸熟,在大哥 @Hydra 的激励下,我总算得到了第一张集邮的照片,我第一次这么主动找人拍照,表现得特别腼腆(表情很猥琐),但这次来能拍到就不亏了!

胡桃、琳妮特集邮

6 月末有一次原神的 Only 免费展,我叫上了朋友过来。但这次我依然挺怂的,不过大哥 @Hydra 现场给我演示了几次集邮的常规操作,我算是能鼓起勇气了,陆续找了几位老师集邮。大哥和我介绍了 @江晚 老师,他帮我们一起合了影。现场还设有一些小游戏,我最后得到了一份奖品吧唧。

芙宁娜集邮

青雀集邮

7 月暑假已经开始了,此前在苹果店联动活动认识加好友的 @小优,她居然也开始玩 Cosplay 了!那天她出了原神里的愚人众「少女」,约了我出来一起逛富华里,了解到这里的书店就是珠海的二次元圣地,主要是因为这里有卖各种二次元周边。她买了自己推「艾尔海森」的周边,而我最后买了一只「纳西妲」的指偶。这是我少数能被女生约出来的经历,对如此社恐+家里蹲的我来说可以说是极其稀缺了!

那天也把至尊同学叫了过来,他本以为就我一个人,合照的 Coser 老师是相互不认识的。我们一起逛了书店闲聊,喝了饮料吃了顿饭。他还尝试穿了她的 COS 服体验了一下,场面一度非常的搞笑(只要我不尴尬,尴尬的就是路人)

8 月初,我去了第一场珠海最大的 AS 展。它位于会展中心,这次来见到的 Coser 非常之多,集邮已经是必须的操作了,我尝试着和她们“扩列”加好友返图了。这次扩了 @灵音(可琳)、@娓君君(知更鸟)和 @白学公主(刻晴)等几位老师。

可琳集邮,Coser 灵音

知更鸟集邮,Coser 娓君君

刻晴集邮,Coser 白学公主

奈亚子集邮,Coser 里川

扩列之后就有机会看到各位老师的各种 COS 照,乃至一些日常,就有了和他们互动的机会。或许什么时候就有话题闲聊开始做朋友了呢?我认为单纯去找他们集邮碰碰面可能还不太会让他们对你有比较深刻的印象(毕竟是纯路人嘛)不如选择一个自己在漫展中的新身份,常见的要么就是 Coser,要么就是摄影了。

出 COS 需要会的技术还不少,买一件 C 服穿着还仅仅只是自娱自乐呢!还需要化妆和准备道具之后,才算是个基本合格的 Coser,能模仿出其性格体态的话就更还原。或许摄影会比较适合我,自己平时就比较习惯用手机拍照记录生活(不过基本是风光和各种日常),为什么不考虑买一台相机,扩展风光摄影的同时,来尝试下人像摄影呢?

8 月中旬有一次 TC 展,结果我在功课做得不多的情况下,急急忙忙就买了一台我认为价位能接受的索尼 A6700,想着能借此机会来学习相机的使用。这次主要发现了套机镜头 18135 光圈较小上的缺点,需要拉长焦距才能有更好的虚化效果。在场内灯光环境一般的情况下,某些时候并不能拍到较为满意的效果。

带上装备的漫展

10 月初的国庆节也有一次漫展。这次我做足功课准备了新的大光圈镜头,拍摄的虚化效果非常好,但发现买的“豆腐块”常亮补光灯在这里几乎没有任何作用,我也对这次的拍摄做了一个简单的总结。这次漫展的集邮就特别少了,几乎都单独给他们拍照了。

这次认识了 @巧乐兹 和 @淮南枝 老师,给她们拍了互勉场照(不过问题还是出在灯光,会显得很黑),@灵音 和 @江晚 老师也在场,既然是列表了我当然是要找她们拍的。

国庆漫展

11 月初,在前面几次展子的失败经历之后,我终于下定决心买了一盏机顶闪光灯,是神牛的 V860,直接一步到位。并且在前一天的万圣节之夜去了富华里练习使用,虽说参数的调教还有些问题,但至少拍出曝光正常能看的照片了。

1102 漫展小记 - 拍照史诗级提升

这次付费展的照片相较于此前的可以说是史诗级的提升了!在内场出申鹤的 @小希 老师说今天一个给她拍的摄影都没有,@Hydra 大哥说我这一上去情绪价值不是瞬间就拉满了。在一旁出提纳里的老师是她亲友,给她充分的指导了动作,她问我借了相机给她拍两张,Coser 老师拿相机拍照的样子感觉就非常有意思。

申鹤,Coser 小希

小提老师

克鲁鲁,Coser 虾米

1110 淋唔到漫展小记

11 月中旬在中山影视城有一次漫展,也是我第一次参加以外拍场景为主的漫展。这次叫了老朋友 @MJ 过来。我和他一起逛展,结果这次展子他一个人都没有集邮,就挺可惜的。@灵音、@渲离染 等几位老师他们组队约了个团片,看他们拍的这么认真,我也不好意思蹭拍… 去找了 @梦华 摄影老师蹭了他的模特(这里敲一下黑板后面会考),就是打闪没打好拍炸了。不过最后找了认识的 @白学公主 老师拍了两张,她这次出的绫华非常漂亮。

教堂

玩偶

神里绫华

11 月末的珠海第一届 SN 魂展,主办方估计是想打造成“特摄”主题的,但是市场调研不足,导致出现会展中心这样的大场地却没几个人来玩这样的尴尬情况,我甚至觉得用“摄影比 Coser 多”来形容都好不夸张。

与三月七的约定

@灵音 老师邀请我和她一起逛展,她作为我的主要拍摄对象。逛展期间发生了令人尴尬的事,也有开心的事。我第一次找 Coser 老师单独拍照被拒了,但是品尝到了 @灵音 老师的手工自制小蛋糕,非常好吃。这次闪灯也比之前稍微熟练了一些(但也有过曝的照片,但 RAW 拉回来还是绰绰有余),漫展结束后跟着她认识的摄影一起在场外拍外景,多好的学习机会。拍摄结束后一起在富华里搓了一顿饭,认识了 @柠檬茶、@阿卓、@小安 几位很厉害的摄影老师,以及出小樱的 @卿安 老师。

三月七,Coser 灵音

12 月初在澳门有第一届 IMAC 展,这个展邀请了很多大咖嘉宾,其中有我认识的 @谢莹 老师,她是「刻晴」和「嘉明」的配音演员。这不冲都不行啊!这也是第一次排签售,我不是 SVIP,人看着不算多但还是排了快一个小时。这次展拍的照片蛮多的,我当时并没有来得及写笔记详细记录,就在这里简单介绍一下。

珊瑚宫心海,Coser 小予

这次展遇到了 Coser @理如 和 @阿卓 摄影老师,他买了 VIP 提前摆好了灯阵,我借他的灯阵也跟着拍了一些 Coser 老师,其中认识了出星期日的 @善信 老师。我们那天晚上漫展结束之后还去吃了顿烧烤。

说起来当初加这个 @理如 老师的过程也是很好笑的,我“视奸”了她空间很多次,她居然来主动加我。上次在中山影视城漫展拍炸了的那个 Coser 老师其实是她,好在这次借了 @阿卓 摄影老师给她拍了一张效果还行。

1 月买了引闪器,首次在春节前的中山 AS 展使用,借了 @杰哥 摄影老师的灯阵来玩了一下,不过效果比较平,不太能看出来是用了几盏灯的样子… 还需要继续努力~

旅游

今年去了最多次的城市是深圳,七月份跟着亲戚的车去 Citywalk 了一次,逛了市区一些大商场,和同学 @BB 见了面,吃了一顿日式汉堡肉。十月份去了沙头角、大梅沙和深圳人才公园。

十一月份我自己坐大巴去了两趟,只是为了修我的 MacBook Air,当然和 @MJ 去了深圳的二次元胜地逛玩,还是不亏的。十二月年末也去了一趟,和 @MJ、@凯文 两人一起吃饭聊天和闲逛,途中还体验了深圳刚开通的地铁 13 号线。

深圳次元之旅 / 拜访苹果官方直营店

又去一趟深圳

深圳苹果直营店

修电脑

爱莉

腾讯

这几次在深圳的经历给我的最大感受就是吃的品种特别丰富(但大商场里面价格也蛮贵的),或许是因为大城市总人口多,外来人口也比较多。

其次是澳门,办理了新版的卡式港澳通行证之后不去转转就没意思了。五月是时隔多年(2009)后第一次去澳门玩,和爸妈一起过去,主要在澳门半岛转悠。去了大三巴、大炮台、金莲花广场等著名景点,品尝了小吃街的牛杂,也进了赌场参观表演。

九月第二次去,主要是氹仔那边的酒店为主,首先去了威尼斯人酒店闲逛,去了官也街品尝美食,去伦敦人、巴黎人等酒店拍照打卡,去美高梅蹭了杯招牌奶茶。最后在澳门教科文中心参观画展,见证了我大舅的书画作品被澳门基金会收藏的过程。

其他城市就只有 1-2 次。二月春节期间去了广州番禺的一些小众景点拍照打卡,当天返程还去了中山华侨公园和紫马岭公园,这里的一些现代建筑非常出片。八月份去了香港 Citywalk,主要在九龙和湾仔区,去体验了富士相机和索尼的镜头,在星光大道和明星的手印合影,和 @凯文 见了面并一起吃饭聊天。

这里图一直没弄,先鸽着吧==

数码设备

JBL Flip 6

2 月,我打算购置一个新的蓝牙音箱,一是提高平时听歌的音质,二是方便携带,这款音箱的性价比我认为还不错。和我表哥的 Bose 音箱一起听几乎没什么差异,而他那款比我的更贵。

红米 4K 27 寸显示器

3 月,我原先使用长达 7 年的戴尔,开机时出现了闪烁的横线,看上去是面板出现了问题。再三考虑下还是决定买个 4K 高清的,而不是高刷玩游戏的。看了评测视频后,认为红米的这一款性价比不错,甚至比它自家小米品牌的还要好…

这台显示器配合 MacBook 的体验确实不错,在 Windows 下玩游戏,原先 1080P 的画面必须拉到 2K 才能显示成此前小窗的尺寸,显卡的功耗也上去了,不过我现在玩游戏的时间其实也没有之前那么多了…

索尼 A6700

8 月,我购入了人生第一台相机,索尼 A6700,是在我能承受价格范围之内最好的机型。虽说 A7C2 会更好,但是镜头方面肯定是要比半画幅更贵一些,它裸机的价格就能买到 A6700 带 18135 套头了。

别人都在说,这个价格你为什么不买全画幅。这里还要考虑到我当时的使用场景,因为拿到相机几天之后我就带去漫展拍照学习使用了,当时也是急急忙忙的做了选择。

不过现在看来,我的确可以再花更长的时间先买一台 A7C2,再认真挑选镜头。不过预算也会增加 1-3K 吧。机器开箱之后就掉价了,现在再卖掉并不划算。

A6700 实际上也很不错,并没有让我失望。起初拍的不好其实是补光问题,带上闪光灯效果马上就不一样了。相较于部分老相机来说,这台新机器对焦迅速的优势也显现了出来,它更容易拍到正常对焦的照片。并且还能直接录制 120 帧的 4K 视频,后期还可以考虑买个云台拍点小视频了。

既然相机都买了,大光圈镜头总要配一个吧,闪光灯也得来一个吧,蹭别人的灯总要个引闪器吧,要是自己单打独斗是不是也要有个柔光箱?摄影,就是用光的艺术!把钱花光的艺术!

MacBook Air 送修

11 月末,我去年购入的 MacBook Air 居然也送修了!在我手上的 MacBook 用一台就坏一台?不过还在质保期内,免费维修,就是苦了我去了两趟深圳,不过还好期间和 @MJ 去了深圳一些不错的地方,也算是 City Walk 了一波。

iPad Air 6

12 月初,我再三考虑下还是选择购入了 iPad Air 6,M2 芯片 256G 的版本,相较于新出的 Mini 7,我还是觉得它的性能会更耐用一些。它的尺寸 10 寸确实更大没那么便携,实际用多了也接受了,回看 Mini 怎么这么小!

这个尺寸拍完照片立马就能打开看效果,甚至可以直接修图,卷死其他摄影(不是)。不过 Creator App 并不支持通过软件删除照片,这点还是有些不太方便。(结果实际几次拍摄都没现场修过)

番剧

今年主要看了这几部番剧,《间谍过家家》、《甘城光辉游乐园》和《亚托莉》,都是日常系。

《间谍过家家》就不用多说了吧,其实是因为之前只看了 1-2 集后鸽了,今年直接从第一季补番到第二季了。还去电影院看了剧场版,这个剧场版的剧情可以说是把“浮夸”这个词发挥到极致了,非常建议一看!

《甘城光辉游乐园》是游乐园经营主题的一个番,我曾经是非常喜欢玩《过山车大亨:3》的玩家,因此对这个题材的作品有着不错的好奇心。

《亚托莉》其实是因为喜欢她这个可爱的角色形象,GalGame 没花精力推看一下这个我觉得也不错,虽说看评论说这部番有改写过一部分情节。

截至写下这行文字的这一天,我还看了一集《魔女之旅》,我觉得接下来应该再补一下《孤独摇滚》、《莉可丽丝》和《葬送的芙丽莲》,都是此前很热门的番剧。

情感

自去年主动勾搭过一次女生之后,就再也没有第二次了。一方面我认为找群友做军师的过程过于超纲了,拿捏不住他给我设定的身份。再一个是在父母与其他人无形对自己施加了压力的情况下,我很难摆脱“一定要让她成为我女朋友”的这种观念(即便自己或许只是想做个朋友而已吧)。这件事让我 Emo 了一晚彻夜无眠,令我至今历历在目。

今年家里人并没有安排和介绍过任何人,有相亲活动我也没参加过。

但我还是决定以朋友相处的方式多和女生交流互动,比较松弛有度。可眼看自己年纪也是越来越大了,还有多少时间让我有试错的机会呢?假如真的有人对自己感兴趣了,又该如何识别出她的小心思呢,她是否对其他人也这样呢,这对我来说确实是一个很伤脑筋的问题啊。

总结

在这一年,我在漫展上结交了很多新朋友,总算是在前几年的短板上首次得到了突破,或许能让我没那么社恐了。通过摄影也极大的增加了我想出门走的意愿,但以此同时也减少了当宅男玩游戏、看番剧和写代码的时间。

年末公司开始 996 加班后,发扬爱好的时间进一步被缩短,编码能力的提升更多只能通过公司项目来实现,我应该尽可能的抓住一些机会。个人网站方面还需要更进一步的思考如何保证质量的同时,借助编程和 AI 的力量尽可能的提高维护效率(例如照片的处理等等)。作息方面依旧没有得到改善,除了保证充足的睡眠外,还需要提高一些时间使用效率。

自问自答

玩摄影并不一定能赚钱,学来有啥意义?

摄影本身确实没有什么意义,意义都是自己赋予的不是么。记录当下,在未来能够重新回忆起来,就是摄影本身的意义吧。况且学会摄影也可以给未来的对象拍呀,怎么想都是好处大于坏处吧。游戏帐号什么的不一定自己能持续留住,但是写文,摄影,做项目的成绩绝对是持续积累的。

给对象拍?那你找到对象了吗?有进展么?

依旧没有,凭实力单身了,在漫展上也不是所有摄影都有女朋友啊(更何况还有一些是先有对象才学摄影的不是么),我目前还比较享受摄影过程的,相较于期待找个对象,不如先期待来一个固模吧...

现在在摄影和编程技术方面,你要如何权衡?

现在 996 工作时间比较长,只能在工作方面多花心思看看能不能提升点啥了,AI 其实也是个很好的自学工具,前提是你能很好的调教它。长期在电脑面前工作也比较容易导致职业病(久坐、长时间看屏幕等),有时间休息还是尽量出门走走,让眼睛也好好休息下,这点时间就尽量安排给摄影活动了,无论是拍风光还是人像我觉得都好。

立下 Flag

去年和前年并没有立下任何 Flag,主要是总结的拖延非常严重,已经没有写的必要了好吧。现在回过头来发现就算写了似乎都挺难完成的,好在今年看上去似乎有了那么一丁点转机,看看能不能努力一下吧。

[] 学习一个后端框架,能挺好快速出活的那种
[] 继续学习提升摄影技术,按需购买新设备
[] 看下能否将主页相册遗漏的照片都补全了
[] 争取找到第一任女朋友?

往期

朋友们的总结

@Innei:2024 · 前路未尽,初心犹在

这一年,从乌镇跨年开始,经历了公司团建去大理的愉快旅程,但之后突如其来的裁员让人陷入抑郁,最终加入了 RSS3,参与了 Follow 项目的开发,体验到了开源工作的成就感。
在游戏方面,通关了《黑神话:悟空》和《最后生还者》,感受颇深。尽管只去了南京旅行,但年底买了特斯拉 Model 3,技术上写了多篇技术文章,对技术的理解更深入。虽然对未来抱有悲观态度,但仍希望明年会更好。

@林陌青川:野花做了一场玫瑰梦

这是一篇深情的 2024 年终回忆录。博主林陌青川记录了与“汪小姐”从网络技术群相识,到各自在互联网大厂(携程、字节、B站)实习奋斗,最终在上海奔现的故事。
文章回顾了两人从错过的遗憾到低谷期的相互陪伴,以及见面时同游静安寺与外滩的心动瞬间。尽管博主自比“野花”仰望优秀的她,感叹现实差距,但他仍视这段缘分为人生的“上上签”,以此文致敬这段美好的相遇。

将 MO3 音乐导出成 WAV/MP3/OGG 等格式

作者 Paul
2025年8月31日 21:20

近期开始回忆杀状态,在 Steam 上购买并游玩了一些以前玩过的经典小游戏,其中包括来自 PopCap 宝开出品的《吞食鱼:2》。

可能人老了吧,新的游戏都玩不动了,要么烧脑要么太肝,还是这种休闲小游戏玩法简单又上头!

当时这部作品也有人将它翻译成《大鱼吃小鱼》,其中“吞食鱼”这个说法来源自《幻想游戏》系列。它也推出过很多“不同版本”的续集(其实就是一个整合包),汇聚了很多来自不同厂商的休闲游戏。

吞食鱼 2.webp

游戏玩到了,BGM 也在反复洗脑了,那么如何将游戏里面的音乐提取出来继续听呢。了解博主过去黑历史的朋友应该知道,当时老喜欢用各种方式去提取各种游戏的资源素材了,其中《摩尔庄园》、《疯狂农场》等游戏都是我的常顾对象,收集各种游戏的 BGM 并且偶尔拿出来播放算是我的一个小众爱好。

我找到了当年的一些素材提取工具(文件的修改日期为 2015 年,实际可能更早),其中有一款工具叫做 XMPlay,它可以用于 MO3 音乐的播放和提取。

MO3 是一种音频文件格式,主要用于模块化音乐,类似于其他 MOD 格式(如 IT、XM、S3M 等)。它由 Ian Luck 为 BASSMOD 引擎开发,MO3 文件的一个显著特点是它使用 MP3 或 Ogg Vorbis 格式来编码音频样本,这使得文件体积相对较小,同时能够保持较高的音质

参考:MO3 是什么格式,为什么现在几乎没人使用了

对于模块化音乐,我的理解就是它将每一个音乐节拍的声音都作为一个片段存储,播放过程则是将所有片段循环复用,有点类似于 MID 音频格式,但是 MID 格式外放出来的音色受操作系统、播放软件等的影响较大

时隔已久,我甚至都忘记了当年是怎么使用它提取音乐了,一番研究后总算是找到了办法。

顺带一提,我 Google 之后得到的结果往往都是要你付费购买某些软件,几乎没什么中文文章提到过这些东西,看来确实有些小众啊,那么我就水一篇文章简单记录一下

首先从 un4seen 下载并打开 XMPlay 播放器,这里我使用的旧版本,界面稍微比较复古。

XMPlay 播放器界面.webp

正常打开一首 MO3 音乐播放(吞食鱼游戏目录下有个 music 文件夹里面就是),此时从播放器主界面右键 -> Options -> Output -> Output Device 选择 "WAV Writer" -> Apply

XMPlay 播放器导出界面.webp

此时点击音乐播放按钮,就会弹出对应的另存为界面,依次确认导出,所有的音乐都会导出成功。

如果导出的音乐存在部分内容丢失或中断的情况,可能是因为你在播放器已经开始播放了,点击“停止播放”按钮再试一次即可

目前博主已知使用 MO3 格式作为背景音乐解决方案的游戏包括:

  • 吞食鱼:2(Feeding Frenzy)
  • 幻幻球(Peggle)
  • 仓鼠球(Hamster Ball)

理论上多数同期游戏都采用了这个方案,性价比特别高,比存储完整的 MP3、OGG 等格式都小,保真度还高。可惜从宝开《植物大战僵尸》等作品开始,都采用了 PAK 打包普通的音乐格式,此方案不再流行,也算是时代的眼泪了吧!

该写好代码吗?写好了也可能变得不好了

作者 Paul
2025年6月9日 11:52

最近同事分享了一篇文章:《该写好代码吗?我也迷茫了。写的代码好被替代,写的代码差到处救火》,引发了我的思考。我问了问他的看法,是写好还是不写好呢。

答案可以说是意料之中,“正常写就好”。他觉得正常写也会有 Bug,代码也不会很糟糕。确实是这样,因为即便你当时认为写好了,这需求变化速度实在太快,后续很有可能就不能满足需求了,就产生了所谓的 Bug。

这让我很自然的联想到我现在在公司做的 Felo 搜索 项目,确切来说它现在的定位已经渐渐不是一个纯粹的搜索网站了,而是各种 AI 工具的混合体,主要竞争对手是 Perplexity、Genspark、天工 等。

前端方面我的评价是基本上已经乱的一锅粥,原先的逻辑很多不能满足现在的需求,就是一个超级缝合怪,既要 Perplexity 的那种搜索总结功能,又要 Genspark 的 Agent 型对话交互模式... 两种完全定位不同的东西被强行融合到了一起。但是换个角度来说,大公司做的东西不也是堆屎山么,不见得有多好,除非推倒重新开发。我们现在 996,就是飞快的加功能,老大天天都想发布新版本,怎么可能愿意干这种事?

最近我们在做的 AI 生成 PPT 功能也差不多是这样子,越演越烈,产品交互方面并不统一,此前 PPT 是一个独立页面,任意入口点击后打开新窗口生成,之后是独立一套交互流程。而现在为了兼容“PPT Agent”模式(单独一个工具页,通过上传文档什么的触发创建一条记录),强行让其他入口套用它的逻辑。作为对比,和它入口旁边的同级功能,思维导图、智能图形,目前都不会单独创建新的记录(虽然它们也各自有一个 Agent 模式)。

原先交互:打开一条搜索记录,找到生成的 PPT,打开独立页面
现在交互:上述入口保留,生成的 PPT 会产生单独一条搜索记录,生成成功后则点击直接打开独立页面,并且作为 PPT 其入口还不在“文档库”里面,很迷

后续需求还说得兼容之前搜索的逻辑,把原先那套模式给搬到新的 UI 上面,主打一个“抄谁不像谁”,代码逻辑的耦合性实在太强,很难想象之后要产生多少 Bug... 只能感慨地说一声,现在它已经彻头彻尾变成了“Felopark”了,这样的架构,代码也不太可能能写得多好了。

所以回到最前面的话题,写代码正常发挥即可。至于可替代性什么的,只要你的能力不差,去哪里都会发光发亮不是么。

排查了一个导致页面白屏的问题

作者 Paul
2024年12月21日 01:29

我在公司维护的 Felo Search 项目近期收到了大量投诉,部分页面会出现白屏现象。我负责去重点排查,有位同事用自己的手机测试后,发现在 iOS 16 系统下会稳定出现,这大概率就是 JS 执行出错导致了。我的分析过程大致如下:

首先在 Mac 上安装 iOS 16 较低版本的模拟器(我选择安装了 15.2,因为有用户反馈使用 15 系统),通过 Mac 上的 Safari 调试在 iOS 模拟器内的 Safari,并通过控制台定位错误。

SyntaxError: Invalid regular expression: invalid group specifier name

错误信息表明存在一个不受支持的正则表达式规则(我想起来之前写过一篇文章,也是正则的问题),但由于 JS 代码被压缩无法定位到具体行数,我将该文件复制出来并格式化,通过另外一个网站去加载该存在异常的 JS 文件。

初步定位到具体的出错代码属于外部依赖库,只能通过检查近期依赖项的变动进一步确认,大概率是某个依赖升级后导致。检查 Git 提交记录发现 package.json 文件并没有明显改动,只好继续检查 pnpm-lock.yaml 文件,发现存在锁文件版本被降级的情况(pnpm 9x 版本,后续有人用了低于 9x 的版本安装了依赖,就会导致依赖锁被破坏)

最终发现有一个叫 mdast-util-gfm-autolink-literal 的库被升级了,比较此前发布的版本后发现从 2.0.0 升级到了 2.0.1,通过 why 命令可查出为什么它被安装。

pnpm why mdast-util-gfm-autolink-literal

dependencies:
@tryfabric/martian 1.2.4
└─┬ remark-gfm 1.0.0
  └─┬ mdast-util-gfm 0.1.2
    └── mdast-util-gfm-autolink-literal 0.1.3
remark-gfm 4.0.0
└─┬ mdast-util-gfm 3.0.0
  └── mdast-util-gfm-autolink-literal 2.0.1

我去找了下该库关于 2.0.1 版本的一些改动和发布信息,他的确使用到了一个不受支持的正则表达式规则,还有人对此提了 Issues,结果作者明确拒绝向下兼容,坚持要用。很明显这对于一个商业化项目来说是绝对不允许的,相当于是直接就砍掉这大半用户了...

这个正则表达式的规则叫“反向断言”,我早在 2021 年就写过文章 《JS 正则使用反向断言及踩坑》,结果没想到它的兼容性居然在今天都还能差的这么离谱。

我最终的解决方案就是对比旧版本的 pnpm-lock.yaml 内容,直接修改了对应的版本号和签名,亲测可用。

使用 Docker 自动化部署的 NextJS 镜像大小优化

作者 Paul
2024年7月5日 14:35

近期要把公司的新官网项目给收尾了,准备打包部署发布到线上环境,我们主要采用的 CircleCI 和 K8S 负责 CICD,就是期间经常会遇到 K8S 的超时错误导致构建失败。

虽然不清楚具体的错误原因,但我发现构建过程中 Dockerfile 生产出来的镜像文件实在是太大了,达到了惊人的 1G 多,想着这样传输镜像的时间肯定会慢,是否因此导致构建失败的概率提升呢?前文详见日记《继续准备 NextJS 新官网项目(二)

这篇文章我们将以官方的配置文件作为基础二次修改,将应用的构建过程放在当前系统环境来完成,最后将产物打包成 Docker 镜像,以实现大小优化。

修改配置

想着之前自己部署 NuxtJS 的时候发现它在生产环境下最终运行的是一个 server.mjs 文件,这意味着我或许并不需要安装一大堆 node_modules 依赖,然后再执行 pnpm build && pnpm start 的方式来启动服务。这些最终构建好的代码,小到不足 50MB。

那么 NextJS 可以吗,简单搜索看了下,它是可以做到的。我是从它们官方提供的 Dockerfile 里面找到的这个设置项 output,比较隐蔽。

Next.js can automatically create a standalone folder that copies only the necessary files for a production deployment including select files in node_modules.

To leverage this automatic copying you can enable it in your next.config.js:

module.exports = {
  output: "standalone",
}

修改成这种模式后,意味着项目生产环境的启动方式不再是 pnpm start 了,继续这样操作的时候 NextJS 的命令行工具也会对此进行提示。

"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.

旧版 Dockerfile

那么在此之前我是怎么做的呢,这是项目之前的 Dockerfile,可以看到构建、运行应用的过程均在里面完成(并非阶段构建),也因此导致最后的镜像略大。

FROM node:20.15-alpine AS runner

# 定义一个名为 ENV 的参数,默认值为 dev
ARG BUILD_ENV=prod

# Create app directory
WORKDIR /app

RUN addgroup --system --gid 941 nodejs
RUN adduser --system --uid 941 nextjs

COPY . ./

WORKDIR ./

# 如果 BUILD_ENV 为 dev,则复制 .env.dev 到 .env.local
RUN if [ "$BUILD_ENV" = "dev" ]; then cp .env.dev .env.local; fi

RUN chmod 0777 .

RUN npx --yes pnpm install

RUN npx pnpm build

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["npm", "start"]

调整后的 Dockerfile

考虑到我司已经在使用 CircleCI 负责构建应用,K8S 只负责打包并运行构建结果即可,我根据官方的 Dockerfile 最终整理出了一份自己的,供各位参考:

FROM node:20.15-alpine AS runner

ENV NODE_ENV production

# Create app directory
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY public /app/public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --chown=nextjs:nodejs .next/standalone ./
COPY --chown=nextjs:nodejs .next/static .next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["ls", "-l"]

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

这份 Dockerfile 相较于前面的版本,他多出了一个复制 static(位于项目内 .next/static) 和 public(位于项目内 /public)文件的步骤,据官方描述说是这些文件应由 CDN 处理,但实际情况我们用的 CDN 属于融合 CDN(不知道是不是这么说,类似 CloudFlare 那种自动缓存和回源的),因此不需要额外处理单独托管的静态文件。

因为没有在 Dockerfile 里面安装依赖和构建应用了,因此需要在当前的系统环境下,已经通过 pnpm build 完成 NextJS 的构建过程。

使用 Jenkins 或其他方式

我自己的服务器并没有强大的资源和性能,只有一个机器跑多个服务的使用场景。如果改用传统 Jenkins + SSH + PM2 的部署方式,也是一样轻松了不少,以往需要在运行机器上执行极其缓慢的 pnpm build 也将提前在 Jenkins 机器上完成。通过 SCP 的方式传输构建产物,到运行机器上只需替换掉对应的资源,重启 PM2 就能完成,这里就不再具体提供实现过程了,有需要建议自行尝试摸索。

Caddy 简单配置允许跨域的反向代理

作者 Paul
2024年6月27日 21:08

这 NextJS 可真是把我给恶心 🤢 到了,项目里使用 next-international 这个库配置了站点多语言,按照其文档中的 配置说明,需要修改 middleware 中间件的配置。

而项目当中遇到了跨域的接口请求,不知道什么原因后端配置不生效。于是我打算增加 next.config.js 文件中编写的 rewrites 规则。结果我发现一旦使用了 NextJS 的中间件,这些 rewrites 配置居然通通全部直接无视了 🤡🤡🤡

const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/paul/:slug*/',
        destination: 'https://paul.ren/:slug*/',
      }
    ]
  }
};

// 此时访问 项目链接/paul 显示 404

因为注释掉多语言需要加入中间件的代码之后,rewrites 中的规则重新生效了...

也不确定这是不是 Bug,但这种巨型框架层面的 Bug 可不是我一个小彩笔能解决的,Bug 解决不了,但需求仍然要继续做的。我想到直接使用 Caddy 来帮我做这件事,反向代理某一个业务的接口之后强行设置 CORS 的 Header 头,允许任意的跨域请求,随意使用。

使用 Docker

通过 Docker 使用 Caddy 是最简单的方法,且不会干预到实体系统环境,这里我使用了 Docker Desktop 作为演示。点击顶部搜索栏搜索 caddy,下载最新版本的镜像,点击 Run 按钮使用它创建启动容器。

在设置里填入对应的端口号,这里我只设置 80 端口的映射,确保端口号没有被占用就行。

Docker.jpg

修改配置文件

设置完成后容器将会自动启动并且持续运行,使用浏览器访问对应映射好的端口号(我这是 7888),如果能正常访问则服务正常运行,就可以继续设置了。此时点开容器面板选择 Exec Tab 进入该容器的终端界面,输入以下命令进入配置文件的编辑界面:

vi /etc/caddy/Caddyfile

修改配置文件的内容,具体如下:

:80 {
  reverse_proxy https://paul.ren {
    header_up Host {upstream_hostport}
    header_up X-Real-IP {remote}
    header_up X-Forwarded-For {remote}
    header_up X-Forwarded-Port {server_port}
    header_up X-Forwarded-Proto {scheme}
  }

  @cors_preflight {
    method OPTIONS
    header Origin *
  }

  handle @cors_preflight {
    respond 204
    header Access-Control-Allow-Origin "*"
    header Access-Control-Allow-Methods "GET, POST, OPTIONS"
    header Access-Control-Allow-Headers "*"
  }

  header {
    Access-Control-Allow-Origin *
    Access-Control-Allow-Methods "GET, POST, OPTIONS"
  }
}

保存后重启容器,使用浏览器重新访问对应链接,应该会直接显示对应「被反向代理」网站的内容,说明反向代理配置成功。

之后修改 NextJS 项目那边的环境变量,使接口请求经过我们配置好的 Caddy 代理,如无意外则一切正常使用,这就变相解决了 NextJS 内置的反向代理存在 Bug 的问题。

拖沓了一年的 2022 年终总结

作者 Paul
2023年12月15日 17:37

个人感觉 2022 年编写的日记质量都蛮高的,不如采用原先的分类形式+时间排序链接对应日记的形式,算是个小的创新?

由于相册内容堆积了将近一年的量没上传,再加上本次总结涉及到的信息量略大整理起来比较有难度,所以咕咕到了现在。2023 年的总结,估计还是不太适合继续沿用这样的模式了。

截至今天(2023 年 12 月 23 日),这篇总结依旧没有被完成,此时此刻我还得开始筹划编写 2023 年的总结了,先挂在这里后续随着图片上传再慢慢完善吧。

技术

安排了一个直播间模板

用 PS 自制了一个直播间的模版,还以此写了一个网页弹幕机(目前已经失效,暂不考虑重写了),和其他人项目不同的特点是可以使用浏览器的语音播报功能。装修的貌似是挺漂亮了,但观众却还是只有那几个人。

曲线救国配上了 CI/CD

上家是使用 Jenkins 完成项目的自动化部署流程的,只要 Git 提交代码,就会触发一个 WebHook 发送到 Jenkins 对应的链接,无疑这么做能大大节省新代码上线所需要的人力成本。

而我的服务器资源匮乏,决定采用了曲线救国的方法,将自己家的 Windows 电脑通过 Frp 转发服务到 Gitea 服务器上就实现了这个操作,这么做部署性能强劲,不可能造成其他服务卡顿,缺点就是不想一直开机(有独立显卡功耗高),不能保证随时可用。也许弄一个远程启动电源的方案可以解决这个问题。

配置 Jenkins 期间主要遇到了发送 Curl 消息时出现编码错误的问题,是 @Eric 大佬帮我更换系统集成环境解决的。

  • 日记链接

以及各种个性化的操作,但基本都是用系统环境变量解决。例如获取 Git 最新提交记录的内容,并发送给机器人

编写了一段自动化 Shell

主要还是年末操作 Jenkins 的时候误删了上传的所有文件,这种大批量的老数据最后的备份只有 2021 年的,之后产生的新数据都只能重新更新上去,这种反复枯燥的操作,只能把希望放在编写自动化脚本上了。

在春节假期期间,我成功编写了一段自动化生成文章占位图的 Shell 代码,可以将不同尺寸不同分辨率的动漫插画,统一处理成 16:10 的分辨率,并且保持原有的比例进行裁切。原理是遍历文件夹下所有图片,并采用 imgmagick 工具进行处理,最终重新编号导出到指定的文件夹下。

相册图片则还需要后期补上,估计加上 Curl 的流程就可以实现。拍摄的照片是最容易的,少数内容是手动拼接或打码的则还需要重新手工处理。

开源

今年和去年一样,并没有新增什么好看实用的开源项目,以个人学习尝试与经验分享的角度分别开设了两个文档类项目。前者记录我折腾捣鼓过程的草稿性质代码,后者分享我编写代码的一些个人实践经验。都是我入职新公司之后发起的。

其他项目维护的并不频繁,我的 Single 和 Fantasy 主题也没增加什么新的特色功能(追求稳定)

项目

个人网站 依旧是我主要的维护对象,包括但不限于以此发起的功能代码维护、日常内容编写,各种小设计等等。隔壁 @Innei 的开源项目 Mix Space 越来越有人气了,但我的程序还是挺垃圾的,考虑到自己服务器配置较低、老数据兼容等各种问题(我日记的数据最早从 2018 年就开始积累起来了),因此暂时不考虑更换后端的技术栈。

PHP 它也的确足够用了不是么,为了学技术故意上一套看似很厉害的方案,实际配置起来还更麻烦的。关于自己个人网站的维护经历,我在后期也会整理出一篇文章,欢迎持续关注本博客。

保罗的小窝

前端方面,前台有计划考虑使用 Vue 3 和 Nuxt,目前推出了一个 测试版本,重新设计了一套新的 UI,依旧是水绿色为主要颜色设计的,这个颜色和初音未来的主题色十分的相似呢。

新版小窝前台

后端方面,主要重构了项目相关内容,将 JSON 存储改为了数据库存储,这样大概可以优化程序执行的内存占用(所有的项目内容都存在了一个 JSON 文件里面,而且后台是直接编辑 JSON 文件而不是提供表单式的界面)

  • 统一 Media, Say, Note 接口传入传出都是 boolean 参数类型而不是 0 和 1
  • 重构前台 JS 代码,替换为自己的 Pjax 实现,简单实现生命周期的效果(3-14)
  • 小窝媒体视频格式支持(6-27)

【待完善】

小窝后台 Vite

偶然通过 QQ 空间看到了这个后台项目设计起初的模样,原来早在 2020 年就换成了现在的这套设计风格了呀。去年增加了换配色的功能,但感觉并不满意,考虑接下来结合 Less 实现更多样化的配色,尝试增加更多样化的底纹?

小窝后台 Vite

  • 各个页面和组件的代码整理,拆分成独立的模块化 CSS 而不是全局 CSS
  • 引入在上家项目实践编写的 useStat Hooks,替换掉原先的 useParams
  • 增加抽屉通用组件,编写日记媒体编辑器组件,使用弹窗形式创建新媒体,提交将自动绑定该日记
  • Input 组件加入 enterKeyHint,回车自动失焦
  • 语录增加长评论字段,可供筛选
  • 批量上传媒体页面,可批量编辑详情信息,按序依次上传
  • 日志页面增加详情抽屉,可查询 IP 归属地;优化日志列表页面展示
  • 接口请求函数统一独立到 service 处,结合 TS 类型判断,不在页面上编写请求函数
  • 优化按钮音乐播放器逻辑,解决播放中切歌未还原状态的问题
  • 增加日记字数统计和计时(待续)功能
  • 媒体增加索引选择器组件,可供关联到现有的日记
  • 增加评论管理页面(但前端还没有接入评论功能)
  • 新增 InputHidden 组件默认隐藏输入的内容
  • 日记编辑页面可直接选择推送到企鹅群(和 @Eric 的机器人做对接)
  • 增加访问凭证管理页面(但还没有接入到任何服务)
  • 增加 useMenu Hooks 管理页面展示菜单项,提高复用性
  • 增加产品管理页面
  • 增加 StringArrayInput 组件,可供编辑字符串数组到内容和排序

其他项目

奇趣起始页

  • 替换新 Logo,增加搜狗搜索,修复用户名模式数据错误的问题,解决重置本地设置错误问题,调整导航项 DOM 结构,修改版权展示时间
  • 优化代码命名和逻辑,增加可读性,更新为现代 JavaScript 写法,窗口关闭按钮改为事件委托,修复用户模式下的 Bug(无法进行设置)
  • 导入导出用户设置,删除 setStorage 的无效逻辑
  • 增加夜间模式(需要在设备上开启),调整部分色彩设定,搜索方式下拉框显示对应图标
  • 拖动排序+样式更正 @苏莫
  • 识别运行环境,在线模式下不允许进行拖拽编辑和设置。顺便给在线模式提供 Fallback,初始化成本地的效果
  • 新增管理导航项目抽屉,可即时的修改看到的东西
  • 解决增加站点时,出现拖拽功能失效的问题(还调整了拖拽初始化功能)并增加了已选项高亮效果
  • 修改设置后,直接修改当前展示的背景效果,而无需刷新
  • 减淡动画和视觉效果 @戴兜

Single 主题

  • 打印样式优化
  • 提升夜间模式暗度,调整配色,优化代码格式,顶部菜单增加键盘交互方式

Fantasy 主题(赞助版)

  • 更新奇趣框架,修复样式异常
  • 完成遗留的文章目录树功能,手机版也支持
  • 顶部菜单和归档页面只展示一级分类的文章,不显示二级
  • 新版随机文章图片算法
  • 追番页和友链页不要跟踪目录树
  • 修复登录用户无法进行评论的问题
  • 获取番剧内容失败的纠错处理
  • 优化引用块样式,而不是原先的大虚线,顺带解决夜间模式下的颜色问题
  • 修复二维码分享和剪贴板无法复制的问题
  • 支持移除 Prism 的场景
  • 更新为现代 JavaScript 写法,格式化部分语句

一些整活过程

一些与项目维护(包括公司项目)与整活的过程,可以看下这些日记:

工作

今年的工作和求职过程可以说是坎坎坷坷,好在我运气不错,有个比较好的结局。至于为什么离职了,那你大可听我继续往下细说。

3 月初被叫去谈话了一次,说是公司接下来开始全员居家办公,公司的电脑什么的大概率要被收回去,只能用自己电脑干活。这说明了一个问题,就是公司的现金流估计紧张起来了。虽然公司也有不少同事是远程工作的,但我还是喜欢出门走走。

明天要把公司电脑的数据清空一下了,接下来我就是全程在家办公的「死宅」一个了,在颠废的路上持续前进着,工资没涨、技术没进步...

在干活的某一天,因为此前我的一些个人想法,有部分图片资源没经过打包器,导致发布生产环境时命中了旧的缓存,但又不允许清除 CDN 缓存(可能是没有管理权限)被老板说了一句,这个操作简单但反复,修改的项目很多,能用简单的办法解决谁想用难的呢。

老板:你得搞掂这个事,不然换图片这个事都搞不掂,是不是该考虑考虑是否胜任当前岗位?

我从这句话感觉到他貌似 Diss 了我,有那种“随时被开”的威胁口气。


后续就被老板说工作态度消极,扣了不少工资(年前公司已经全员扣了一次了,年初的时候还说有机会调整正常,实际并没有,这能不消极吗)

下午和老板私下谈话了,他还是觉得我现在这样的工作态度有问题,最后结果就是继续降工资咯,我也不知道还会在这里待多久了。

做业务真的就是“解决问题”而已,即便是用最烂,可维护性最差的方法也多多少少能做出来。也许这就是为什么公司此前的项目平均水准都不太高的原因吧。写了这么久的 React 之后,第一感觉就是接触到好的项目源码之后,即便代码看不懂,但之后会逐渐明白他为什么要这么做,有什么好处,眼光确实是会不一样的。接下来这段时间我还是尽量多去看看书和文章吧,不然再这样下去迟早也会废了的。

这件事发生之后,我只想着怎么能在这里继续挖掘出剩余的潜在价值了,合作关系已经开始逐渐崩塌,呆不长久了。


随后我的事情很快就被传到隔壁深圳同学那了,真是丢脸丢到外地去了。

再仔细思考一下当时和老板谈话的时候,他口中说的能继续加薪的同事,其实干的杂活也确实不少,甚至晚上经常加班解决(所有人的提交记录都有个可视化图,他确实显得特别积极),这要是不给加就很过分了。
我依然还在尝试着把代码写得更好,但这样做貌似在公司里体现的意义并不大。毕竟客户确实可不是看你代码写的好不好的,而是看能不能及时的满足需求而已。写得好,也就只是对自己的工作负责,接下来修改起来也会没那么吃力些。

甚至因为这件事情,我爸还想过我要不要去转行做设计,我自然是拒绝了。一是我完全没经验,二是我缺乏必要的技术。例如插画设计我就完全不会,同行竞争力很低,就算有机会面试不也是现场被拒啊。


我把这件事情发到了脉脉,并没有人抓住重点不说,还被人咬文嚼字继续 PUA 了一顿,这种事情发生之后,一度把我的心态搞到炸裂。估计是因为大厂员工普遍高压力无处释放把,自己过的不顺也好好 PUA 别人一把,反正彻底对这个平台无语了。但在意外看了一份别人本科生的简历过后,我感觉我也许并不算太差。

继续呆在这家公司,一个人孤军奋战维护一个“屎坑”后台项目,并不能让我获得什么新的能力提升,和朋友、群友们聊过之后,果断的选择了裸辞。我的学历不高,学历焦虑还是挺严重的,但是升学考试对我来说还是太困难了。

准备简历的过程也比较麻烦,相较于之前的简历,主要做了这些改进:

  1. 展示自己折腾项目遇到的难题,用数字可视化形式展示
  2. 不要写上学时的个人奖项了(@Lencx)

离职相关的事情也和朋友聊过,@Eric 准备去字节继续做开发,@Kevin 打算去香港读研,@MJ 想着去外国留学持续提升。不得不说,他们个个都比我要强多了!

失业期间我也在网上刷(学)着各种面试题,之后我在 Boss 上投递简历陆续去面试,一共面试了 3 家。

在 @Innei 的引介下认识了现在公司的老板,进而入职了现在的公司(在此之前还面试了两家都没成,我也没有什么合适的选择了)入职后阅读代码的能力还是 Debug 的能力都有所提升,由于维护的是一个在线会议项目,我也借此熟悉了 WebRTC 及一系列的相关技术(虽然都不算深入),团队氛围还是工作环境整体都是比较不错的。

面试之途

这些都是我入职公司之后的一些事,我挑了一些或许比较有料的内容出来。可能和上面的有所重复,但我不想修改了,整理起来确实太困难了。

...

我在新公司里主要做的事情基本都是迭代产品和修复 Bug(必要时重构),也算是上手项目代码比较合适的方法。即便如此也不能保证自己编写过程中不会产生新的 Bug,因为你没法想象其他人之前会用什么奇怪的方式写。在你的认知里「这个东西」是这个作用,在他们的认知里或许是别的作用(还有一些 Bug 的修复,解决了问题但命名与实际作用不符),想要彻底解决这种问题就只能慢慢重构优化了。

按照大佬们的计划,今年暂时没有自己主导的新项目。但我认为在接下来的一年一定会有的!

生活

送行了两位老人

今年年初我奶奶去世了。到老家的第一天去探望的大姑丈,后续也因病去世。人生有时候就是这么短暂,所以忙碌过后,还是好好享受下生活吧。

本来奶奶年纪大了手脚不灵活,想着买车之后开车回家,让奶奶亲自坐上我开的车,看来这个想法现在是做不到了啊。

走夜路

又一次送行

并没有选择做房奴

约了好多家中介看了好几次房,新楼盘和老楼盘都看了下,但几乎都是过了过眼瘾。毕竟我家境确实一般,并不像其他人家里有矿想买就买,在我上班之前家里基本上就是“月光族”,这也就是为什么我一台爸妈买的游戏本能从上学用到上班还在坚持了。在家里收入不高的情况下,的确应该节俭一些。

看房

亲戚的婚礼

参加了一次隆重的婚礼,朋友圈也围观了不少人的婚礼。可能是我与他们关系普通,或者对方家庭条件问题只叫了家里人吧,我并没有被他们受邀参加。对于我这种从来没谈过恋爱的人来说,只能是无比的羡慕啊。

婚礼现场

购置鱼缸

和老妈商量后,给家里购置了鱼缸,养了点小宠物的感觉还行,有活物之后,可以闲着没事对着它发呆了。

购置了鱼缸

晚上回到家,我老妈下单的鱼缸送到了。花时间拼装了下水泵,清洁了缸体和装饰物,就差点小鱼了。我还是更希望能再去淘点大件的装饰物(小桥城堡房子什么的),这样使得整体内容就更加丰富一些了。

首次做了造型

年前花 300 多块钱做了一套造型。虽然看上去效果还不错,但是依旧没有女生会多瞧我一眼。

寻找可爱之物

现在日常喜欢在淘宝、B 站、咸鱼等平台上查找可爱的周边,像是动漫抱枕,手办,T 恤等等,因为实在是太可爱了,简直是猛男的最爱啊,忍不住的说不定就下手了啊。

遭遇过两次诈骗

第一次是在西区里遇到一个骑电动车的做推广,说是什么新店开业参与抽奖,结果要求我给他看支付宝的花呗记录什么的。感觉就挺有问题。

疑似诈骗店铺

第二次则是在 🐑 了的期间,我妈接到了一个自称京东客服的电话,他很熟练的爆出我家地址,在对方要求我安装钉钉共享屏幕我就感觉到不对劲了,上网搜索之后就是实打实的诈骗,保留了对话语音,但是我懒,没整理出对应的日记。

始终存在的学历焦虑

感觉现在本科就是入场券,IT 行业内普遍认为本科达到了烂大街的水平了。因为这个原因,我有几位同学也尝试开始自学考试升本。

他那句话「往死里学」让我印象深刻,(考本科,尤其是数学)明明就是自己不擅长的事情,为什么还要反复再逼自己去做呢。没有爱好支撑,凭什么去坚持下去?问他几点钟睡觉,结果也是两三点。近期互联网公司这么多的猝死事件,实在是让我感到害怕。

但他们在深圳平常工作就挺劳累了,下班还时不时在群里约其他同学上号玩游戏,想要在这种疲惫的环境下好好学习还是非常考验一个人的心态的。


也有其他朋友提供了建议,例如远程授课的海外大学,就是需要语言能力,以及不少的 Money,你懂的。


刷微信的时候又看到了那种提升学历的广告文,说是考试会变难,什么专业都要考高等数学和英语了,这么搞真的是难上加难咯!(虽然计算机专业也必须要考)不过得知了自学考试的概念,看百科上的说明说是这种考试比全日制要难,可现在的工作大多数都需要全日制吧,自学考试的水分和全日制比,难道还是全日制的更吃香么?

也有人说,求职的时候公司看你不是全日制本科,也会直接 Pass 掉,既然如此那我还有什么信心去争取呢,不如多提升一下自己的技术水平或影响力,争取获得更多的内推机会更合适。

新冠与健康

三月中旬左右去打了最后一针疫苗,至少自己不会变成重症患者了。

三月底,自己住的地方直接来了一个密切接触者,直接整栋楼封楼了,每个人都要做登记。

吃着晚饭的时候,外面突然响起了敲门声,在想着是不是楼下邻居又来找麻烦(噪音)了。结果我妈看到是穿着防护服的工作人员,没错,我们这栋楼被封控了...

封楼

六月离职后独自一人出去吃了顿麦当劳,结果却感冒了。@Innei 说估计是离职之后太焦虑了免疫力下降了,我觉得有道理。在有工作的时候感到不快,在没工作的的时候感到更不快了!

11 月底,疫情又开始扩散起来了,公司办公地点附近一片区域全部封锁。一部分同事不能过来上班,结果没两天,公司所在位置直接被封了。那天刚下车准备上班,结果就只能回家远程办公了。

管控区域

结果年初突然宣布全部解除封锁,也不提供免费且强制性的核酸检测了,自然年底都如约而至“羊”了,全员陆陆续续感染新冠在家休息,还好公司所有前端几乎都是错开休息的,项目方面貌似并没有太大的影响。

近期疫情已经逐渐常态化了,我这开始不提倡低风险人群做核酸,且开始付费做核酸。价格 2.5 混管,13 块单管。呆在家的好处自然是能降低感染风险,但女朋友嘛,总不可能天上飞下来一只吧...

后续我爸单位也有同事 🐑 了,导致我们一家自觉“隔离”,不参加聚会等活动了,少吃了一顿大餐。

圣诞节那天,我妈也说自己开始有症状(估计也是自己同事传过来的),但我还没事,因为常态化之后,只要没症状就得去上班。结果那天去到公司立马就开始喉咙不舒服了... 之后就开始在家休息,我妈吐槽说现在你们吃的都是 🐑 人做的 🐑 餐了。

我妈发烧了,而且头微微疼,可能羊了,但我还没有症状,按照规定即使我 🐑 也得正常回公司上班

发烧了

甚至我不舒服的其中一天,我妈那还遇到了诈骗电话,还好我人没有彻底傻掉。加上她并不会操作,骗子气急败坏,什么话都让他说出来了。

这鬼病毒都 6 天了,结果自己还没完全好起来。还干了一件或许终身难忘的一件事情,为了配置自动化备份网站的功能,直接把自己网站的图片资源全部删除了,糟透了!躲得过初一,还是躲不过初五啊...

为了弥补这个错误,我只好开始研究自动化脚本,以快速可靠的方法覆盖掉 404 的资源(截止 2023 年 12 月 31 日,依旧存在不少资源无法被恢复)详见上面 编写了一段自动化 Shell 章节。

恋与提瓦特

还没开始就结束了。这件事情的起源还是因为我在 B 站一个原神的视频下方发了一条评论。

原神这么火 我还是遇不到一个玩原神的女朋友 😭

其他一句话简单概括

游戏

游戏方面基本上和去年一样,由于购入了新的台式主机,因此玩了四海兄弟和大表哥,感觉需要时间才能好好体验,不像现在的快餐游戏随时肝完体力就想赶紧下线做其他事情。

换设备之前的体验是真的糟心,《四海兄弟:重制版》最低画质分辨率 30 FPS 都不能稳定,也不知道我是怎么玩下去的==

番剧

依旧是休闲萌系番。

  • RPG 不动产
  • 测不准的阿波连同学

数码

给老爸买了台红米

老爸用的红米 Note 8 购于 2020 年,今年年末他的手机电池续航下降,我细看出现了鼓包的现象,他嫌弃手机比较慢,就给他更换了一台红米 Note 12,是刚出不久的新机。原先想买一台二手更大内存的红米 K30,到货后感觉屏幕有些许瑕疵,考虑到后期的系统更新年限,我还是选择退货买了新的机型。

自己的第一台台式电脑

9 月组装了自己的第一台台式电脑,是真正意义上自己的电脑,自己的工资,自己完成的组装。其中显卡是大头,去掉它不到 5K,加上它差不多 9.5K 了。而上一台台式电脑还是我上小学的时候爸妈通过亲戚介绍去电脑城装的,配置很低,就是一般的办公配置。

组件

内部

这台电脑的配置如下:

主板:华硕 B660M D4 Wifi
CPU:英特尔 i5 12400
内存:金士顿 32G DDR4
硬盘:西数 1TB SN570(不太够用,现在非常后悔)
GPU:蓝宝石 RX6750 XT 超白金

公司配了台 M1 Mac Mini

我想加设一台显示器提高办公效率,找老板申请了,最终选择了小米的 27 寸 2K 分辨率的一款。之前用的 Windows 主机居然带不动两台显示器(Intel 你也太不争气了吧),于是征用了公司一台闲置的 Mac Mini,M1 处理器,8 + 256 的丐版,但貌似还能胜任我的日常工作。

工位

2022-11-12:简单买了点东西的双十一 / 公司配了显示器和 Mac

淘了台二手 iPad Mini 6

年前购买了 iPad Mini 6,不是全新的,屏幕上有细微划痕,边框无磕碰。就是在环境温度很低(15 - 20度左右)的时候貌似性能释放会有问题,出现游戏画面掉帧,不知道是我机器的问题还是通病。

这个现象在我 iPhone 上也偶尔会出现,并不是百分之百会有

iPad

后续这台机器玩了几个月就给弄坏寄修了,CPU 烧坏不能用 Wifi 上网,低价出给维修的了,血亏血亏!

年后购买了一台雷鸟电视

家里的 39 寸联想智能电视购于 2014 年,今年除夕前一天就坏了,春晚都没得看了。年后参考网上的评测还是选择了它,价格最有性价比,具体体验咋样就后期再说吧。

雷鸟电视

愿望

  • 持续寻找一只心仪的可爱的对象
  • 持续提升自己的技术能力,节奏就看自己了,不要完全躺平了就好
  • 多做一些有意思的开源项目、持续写文输出
  • 调整一个较为良好的作息时间

往期

朋友们的总结

如果你对这类文章都比较感兴趣,也欢迎你来看看我朋友的同类文章,写的都挺不错的 😂

@Innei:2022 · 在绝望中前行
@折影轻梦:2022,没有记忆的一年

相较以往写总结的人更少了,而我也咕到了现在才完成。首先感谢你能看到这里,其次感谢在这一年里不断让我进步,让我快乐的各位朋友们,祝你们新年快乐~

记浏览器获取麦克风音源,断开后依旧占用显示小红点的 Bug

作者 Paul
2022年9月24日 21:54

公司的项目使用了声网 SDK 实现语音通话,但在其他同事接入了另外一个功能后,被发现断开麦克风结束通话后,浏览器窗口上显示“小红点”,依旧占用麦克风的情况。这很容易导致客户认为我们在继续“监听”他说话,影响使用体验。这是一个遗留很久的 Bug,一直都没有人找出具体的原因,但在今天,我终于有了新的发现!查看日记全文

小红点

我打算使用浏览器的原生方法,实现一个获取麦克风源并使用播放器实时播放的功能,简单模拟使用麦克风并消除占用的过程。如果能在这个最小的代码示例里成功复现一样的“小红点”占用效果,就需要好好检查下对应的库是否存在产生此问题的代码了。

使用 navigator.mediaDevices.getUserMedia() 获取源,再使用 getAudioTracks() 方法成功获取到麦克风的轨道。按照公司项目的源代码,应该要把这个轨道复制到另外一个源里。

const stream = await navigator.mediaDevices.getUserMedia(constraints);
let newStream = new MediaStream();

const audioTracks = stream.getAudioTracks();

audioTracks.forEach(item => {
  newStream.addTrack(item);
});

想要浏览器对应的窗口停止占用麦克风,则需要停止该轨道对麦克风声音的实时获取。使用 MediaStreamTrack 的 stop() 方法就可以实现,执行后我浏览器上的“小红点”确实消失了。

const newStreamTracks = newStream.getTracks();

newStreamTracks.forEach(item => {
  // 只要 stop 理论上就会停止占用麦克风设备了
  item.stop();
});

期间通过使用 MDN 查询文档,发现 MediaStreamTrack 有一个叫做 clone方法,该方法将返回复制的轨道对象,主要可见变化是更换了其 id

我尝试将它应用到 我的代码 里面,之后打算停止占用,结果“小红点”确实出现了无法消失,始终占用的情况。

audioTracks.forEach(item => {
  // newStream.addTrack(item);
  // ! 只要使用 clone 方法,就会导致 stop 轨道时,浏览器依旧显示小红点(麦克风设备使用中)
  newStream.addTrack(item.clone());
});
经过 Google 后发现,貌似存在类似的 问题,属于浏览器内核的 Bug。但保罗还是比较菜,就没有就此问题继续研究了。

这就已经有了不小的发现了,接下来需要排查下是否是某个第三方库执行了类似的代码所致。我们项目除了声网以外,还使用到了另外一个服务,也需要引用当前用户使用的麦克风源。

将声网 SDK 返回的麦克风源替换为其内置的方法(会返回第一个麦克风源,假设你有多个麦克风,则会触发另外一个麦克风音源不对的 Bug)后,“小红点”并没有始终显示的情况。所以基本上可以确认是声网 SDK 返回的轨道可能是 clone 过的,导致“小红点”始终显示,该 SDK 的源码闭源,但我也从压缩混淆的代码里找到了类似的 clone 方法。

clone(t, r, i, n) {
  const o = this._mediaStreamTrack.clone();
  return new e(o, t, r, i, n)
}

上面这么多的差分测试我觉得已经足够说明问题,SDK 代码内部的具体情况,我这边就不细探了。

知道了问题产生的原因,却没有好的解决办法。接下来可能会就此问题向声网那边提工单,看看他们那边的回复了。有一说一,第一次解决这种疑难杂症,还是挺有成就感的!

以下内容于 9.27 日补充:

给同事看了本文提供的最小 Demo,他那边修改了下,其实只需要将 clone() 前后的所有 MediaStreamTrack 都 stop() 掉即可完成释放了。也就是说 clone() 之后就完全新增了一个占用项,只要有一个没有释放,就会存在“小红点”的占用效果。

const newStreamTracks = newStream.getTracks();

// 原流所有轨道
audioTracks.forEach(item => {
  item.stop();
});
// 新流的所有轨道
newStreamTracks.forEach(item => {
  // 只要 stop 理论上就会停止占用麦克风设备了
  item.stop();
});

那问题就变成了两种可能性,一个是 React Hooks 编写逻辑问题,导致反复获取到新的 clone() 后的轨道。再一个就是“那另外一个服务”是否是将传入的轨道持续占用,而没有提供对应的销毁代码?这个问题还需要写一个最小复现,从而排除下是第三方的问题还是 Hooks 逻辑的问题。

ViteJS 反向代理遇到的小坑

作者 Paul
2022年4月26日 23:29

公司有一个项目的后端正在陆陆续续重构着,期间需要将新旧 API 一起混用。而且我们都知道,Cookie 默认有同源策略,我们本地开发的环境下,域名地址一般为 localhost127.0.0.1,而直接请求其他站点的接口,即便它返回了 Set-Cookie 头,你也会发现并没有作用。

注意:本篇教程适用于 Vite V2 版本,V3 版本不适用

使用反向代理

想要解决这类问题最简单的办法就是设置反向代理,使得多个来自不同站点的 API 聚合在同一个站点下。在服务器环境下最常见的解决方法就是使用 Nginx 的虚拟主机配置来实现,但在本地开发环境下这种操作就显得略微复杂,而且并不是所有前端都会用 Nginx(偏后端/运维)。幸运的是,ViteJS 内置了基于 node-http-proxy 实现的反向代理功能,平时开发我基本上都在用。

这是来自 官方文档 的示例,其中有一段写了 rewrite 参数的配置:

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://jsonplaceholder.typicode.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

发现了问题

我起初也是参考着这个来的,写了一段将本地 /api 指向 https://paul.ren/api(脱敏)这样的配置,当我想要增加一个 /apiNew 的代理时,发现它并不起作用。

本文有些水,说白了,就是一条“小杠”酿大祸,想看结果可以直接往下跳过...
"/api": {
  target: "https://paul.ren/api",
  changeOrigin: true,
  rewrite: (path) => path.replace(/^\/api/, "")
},
"/apiNew": {
  target: "https://v2.paul.ren/api",
  changeOrigin: true,
  rewrite: (path) => path.replace(/^\/apiNew/, "")
}

然后页面访问 http://localhost:3300/apiNew/login(脱敏)接口后返回 404,这就奇怪了,为什么失败了呢?

排查流程

我首先想排查下 rewrite 参数干了什么。可 ViteJS 的文档上也没有给具体的说明,就翻了翻其 源码,看起来是把我访问路由的 path 给替换了一波。

访问 `http://localhost:3300/apiNew/login`
得到 path `/apiNew/login`
rewrite 就是把 `/apiNew/login` 的 前缀去掉,变成 `/login`,
最后得到代理地址 `https://v2.paul.ren/api/login`

看起来我的设置是正确的,可实际访问确实如此吗?尝试过在配置项里面写 console.log 函数,没有用,就想着能不能让 ViteJS 把所有信息打印出来,这种情况肯定是属于异常行为了。

开启日志输出

在官方文档用关键词 debug 未果,换了个 log 去搜,有了新的答案,logLevel

这个参数同样也在 vite.config.ts 里面设置,默认是 info,我改成 warn 之后,报错信息果然就出来了。

vite:proxy /apiNew/captcha/ -> https://paul.ren/api +0ms

可以看到,我访问 /apiNew,实际上走了 /api 的规则!一切水落石出了!这么一看,/apiNew 还确实符合 /api 这个规则...

解决方法

那解决办法就很清楚了,我把 /api 这条规则的适配改成 /api/,就无法匹配 /apiNew 了,最终我的配置项改成了这样子,问题也就成功解决咯!

"/api/": {
  target: "https://paul.ren/api/",
  changeOrigin: true,
  rewrite: (path) => path.replace(/^\/api/, "")
},
"/apiNew/": {
  target: "https://v2.paul.ren/api/",
  changeOrigin: true,
  rewrite: (path) => path.replace(/^\/apiNew/, "")
}

还有一个方法就是把这两个规则互相对调,也就是让 /apiNew 优先级更高,更快被匹配到,但是这样感觉做就是埋坑行为,规则一旦多起来就很麻烦了...

番外话

其实最开始是公司的实习生发现后端接口的 Cookie 没法设置上去,就私下开了个腾讯会议问我,我让她按我的方法设置反向代理之后就触发了这样一个问题了。说实话我对这个问题还蛮有兴趣,毕竟从一开始我就没明白 rewrite 这个参数是怎么用的,今天这样一出虽然耗费了将近一小时左右的时间,而且最终解决方案及其「简单粗暴」。但我感觉还是有些许意义的,以后遇到类似问题就多了一个好排查的点了。

一道前端面试题,如何流畅插入 1W 条数据

作者 Paul
2022年3月22日 22:48

昨天上午,群友 @小陈 在群里发了几个自称隔壁转发的面试题,其中有一道题目我感觉比较有意思。

在一个页面上,以追加方式追加 1 万个 div,每个 div 里显示一个数字,依次为 1、2、3 直到 10000,但不是一下子全显示出来,动态的从 1 依次显示到 10000,你会怎么做,请说明你的思路或代码片段。

刚看到这道题,我以为是要手写实现一个虚拟 DOM 和虚拟滚动(React 太入魔了),但这难度有亿点点高啊!@玩水 大佬很快给出了自己的答案:requestAnimationFramecreateDocumentFragment

前者是浏览器完成渲染一次后的回调函数,执行一次后即失效。

MDN:你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame()

后者则是我完全没认识过的一个 API,我把它理解成了一个“中间件”(就是中介,类似暂存区的东西),而实际上的“中间件”应该不是这样的意思。

我突然想到 React 有一个 Fragment(或者是 <>)标签,它们应该是同一种东西。这种节点的特点就是,你可以一次性将多个子元素插入,当你把多个存放了多个子元素的 Fragment 分别插入到一个父节点里,实际上父节点里只会出现 Fragment 的所有子节点,其本身并非是一个“真实”存在的节点。

如果你没看懂我上面那句话的意思,不妨看看这段代码里数组之间的关系,你就绝对明白了:

// Before
const a = [1, 2, 3, 4, 5];
const b = [6, 7, 8, 9, 10];
const dom = [];

// After
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] // createDocumentFragment
[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] // createElement

回过头来看,这道题本身也是存在一些“文字游戏”的,我起初并没有使用 Fragment,单纯的用 requestAnimationFrame 实现了「动态依次显示到 1000」的效果(肉眼可见,速度比较慢)。

let i = 0;

function frame() {
  if(i < 1000){
    const div = document.createElement('span');
    div.innerHTML = i;
    document.body.appendChild(div);
    i++;

    // 重点,自己调用自己实现刷新
    window.requestAnimationFrame(frame);
  }
}

window.requestAnimationFrame(frame);

我让 @小陈 发出了自己的题解,大致意思就是使用 createDocumentFragment 先提前创建 100 个节点,再插入到 DOM 里面触发 requestAnimationFrame 函数,实现一次执行插入 100 条数据的效果,而这也是符合「不是一下子全显示出来,依次显示」这个说法的。

至于为什么不要“一下子全显示出来”,主要原因还是浏览器本身的渲染机制吧。如果你用一个真实 DOM 来不断执行 appendChild 实现添加节点,就会不断触发浏览器的重新渲染,肯定会造成卡顿和性能浪费。

实际上,现代浏览器也对这种代码做了优化,遇到这种频繁操作最终会合并成一个操作去执行,实际执行上的差异依旧存在,但会小很多。我只能说,牛逼!面试题果然就是面试题啊。

这个时候就需要不在视图上显示的 DOM 来实现这种功能,前面也提到了 createElement 也能实现,只是节点上的差异,这样插入元素到视图里,就既流畅又能同时在子节点创建时就自带注册事件等功能了。

在 JQ 时代还有一个很常见的一个操作就是遍历生成字符串,最后用 innerHTML 进行修改,但这种办法只能再用 getElementsByClassName 一类的办法重新捕获节点,再注册事件了。

❌
❌