2026-10-13 10:43:00
这里是保罗,似乎过去一年我已经很久都没有在博客上面发表一篇比较正式的文章了。我也失业了几个月了,一直没写文总结落地,没有在博客上发表过一篇文章,还是有点愧疚的呢。
最近,我认识的一位朋友看上去遇到了一个不小的难题,已经准备退圈,于是我想到这么一个话题可以和大家聊聊,也正好可以总结一下,我从 2024 年开始至今(2026 年)去漫展玩摄影的一些心得体会。
以下内容均为手写,没有 AI 优化。因为涉及到一些现实客观情况,可能戳到某些人的痛处,本文没有针对任何人,请不要盲目对号入座。
首先来说说我为啥会想去做漫展摄影。
说直接一些,就是想找机会认识女生,锻炼社交。先不要急着反驳我,你去尝试做摄影,难道就绝对不会有这个原因吗。别看群里大家一直都在说什么“还是性压抑”,我不认为你就从来没有过这样的想法。只是加入到这个圈子之后,你的想法和观念肯定会发生变化的。
从我第一次去漫展,再到第一次拿起相机给一位陌生女生拍照的时候,能感觉得到她们绝大多数都是热情的。我起初非常羞涩不敢开口,集邮(合影)都没有胆量,非常害怕和陌生人搭话(虽然现在也是)。但她们不会嫌弃你长得好不好看,拍照技术差不差,会不会聊天什么的,能给你的后续成长和进步有很大的自信心。

角色克鲁鲁,出镜老师 @虾米,应该是我第一次用闪光灯拍到的模特老师之一,你之前也应该在我其他文章里看到过。
是的没错,于是我开始发现这件事挺有意思。约拍这件事不仅可以培养你的沟通和表达能力,你还可以把自己拍到的成片变成作品发布出去(当然前提是经过模特同意),可以积累出自己的个人影响力。包括我之前一直在做前端也一样,因为前端它既结合了编程,又结合了设计,也是所有人眼见为实的东西,并不是非常抽象且难以表达的。
况且自己本来就很喜欢动漫相关的东西呀,只是你从“周边收集者”的身份变成了“创作者”的身份而已。
当你真正开始执着于打磨自己的摄影技术之后,你就会发现“拍出片”这件事就挺好玩的,哪里会想什么别的奇奇怪怪的事啊。当然在这里我也得提一嘴,在这个圈子里面的确鱼龙混杂,也存在大量的所谓摄影从头到尾就是想认识女生乃至谈对象的,他们在拍摄过程甚至会对模特做出一些很过激的行为(比如拍摄角度奇怪,想看脚什么的),这样的人在圈子里面很难混,风评变差不说,情节严重的还会被送上“瓜条”,对当事人带来很大的困扰。
既然都说到瓜条这件事儿了,那我就要谈谈这里最可怕的地方。
你完全可以认为这就是一种网络人身攻击,还可能从“单打独斗”发展成“群殴”。去漫展的有不少都是在上学的年轻人,他们精力旺盛时间也充分(不像我们社畜大半天都在上班,下班也累死累活了,谁愿意这么折磨自己),就是很喜欢去做这些东西出来,愿意陪你耗。先除去我上面说的过激行为的问题(这个真得挂),其实有不少摄影都曾经拿到过瓜条,他们最大的问题并不是技术,主要还是和模特的沟通问题导致的。
我看过不少瓜条,其中也不缺一些列表里的熟人,我认为绝大多数的瓜条都存在一定夸大现实和带个人主观偏见的情况,就像你写社媒的文章一样,不挂一个吸睛的标题,怎么能吸引人家点进来,一样的道理嘛。瓜条的目的就是要把苦主自己放在“绝对正确”的位置上。比如照片质量问题,审美本身就是一个特别主观的东西,无论你拍的好与差,只要你被挂上瓜条肯定都会说你“拍的丑”呀。还有直接放原片出来说事的,其实原片看着黑漆漆还真不一定是摄影技术问题,尤其是拍暗调风格的。
不过有一说一,我感觉和某些年轻人沟通真的挺累的。他们的思考方向和你完全不同,你在谈 A,他偏要和你扯到 B,压根不在一个频道。这时候你也别想着尝试去解释啥,既然她认为你不对那你就认了呗。这里我奉劝看热闹还是不要随便和其他人八卦(尤其是写/转发瓜条的人),看到别人的瓜条自己私下乐呵一下就好了,真没有必要转发评论什么的,把自己也参与进去,因为你很难判断这件事是否会影响/伤害到其他人。
再一个是圈子里,的确会存在众多的小群体,一旦你和他们意见不合,他们完全可以互相“串通”好对付你。不然你看时代峰峻一个 TFBoys 都能拆出这么多个不同的粉丝群体,每一个还或多或少相互对立不是吗。放在漫展圈子里面也差不多,肯定是和自己玩的好的在一块嘛。
某些极端群体可能会把“蛐蛐他人”本身当作一种正确行为,先不说实际当事人的对与错,你不附和你就要被孤立。我相信大多数成年人都会有自己的判断,没必要无脑跟风。只要你在这个圈子里混,或多或少都会被其他不认识的人议论的,只要你看不见,就认为它不存在不就好(冬泳怪鸽:闲话终日有,不听自然无)。自己没做出什么不合适的事情也不怕被别人曝光,咱也没有必要花心思在这方面对吧,就算你知道有人蛐蛐你了又能如何呢,每个出名的大明星不都是有一群人在下面指手画脚吗...
其实不单单是漫展这个圈子,你参与到其他圈子也会一样遇到类似的事情的(早在玩博客、程序员这个圈子之前我也有遇到过比较恶心的网络攻击,当时是在某个国漫的圈子)。遇到此类情况,心态管理非常重要,如果实在不会说话,那就尽量保持沉默,一个巴掌拍不响,沉默也不失为一种可靠的战术。
博客/程序员圈子也确实是一个很不错的群体,说话普遍比较成熟但也可能比较直接(比如我),没有人会去比技术实力的强弱,你掌握的东西往往也不会成为独门秘籍,只要你态度不差不做伸手党,基本不会有人说啥,大多数人都愿意和你分享和交流。
所以权衡下来,我目前还是希望把摄影当成一个单纯的爱好。如果你想考虑在这个圈子全职做摄影,恐怕并不是那么容易。
漫展摄影现状如何
首先现在漫展的摄影确实也比以往更多了更卷了,这是现实情况,能感受得到现在的展馆空间越来越局限了,摄影区的人比以前越来越多,包括我身边认识的摄影朋友,他们也陆陆续续从单灯机顶闪变成了自己的灯阵,我也一样。最严重的时候呢,甚至你可以发现摄影要比模特多的情况,当然这都是那种小型的展会概率比较大。
我在珠海参加过一次名为“SN魂”的漫展,这个主办是第一次在珠海办展,没有太多经验。邀请的嘉宾是假面骑士的萩野崇,一个面向特摄观众的。自然大多数的 游戏/Cosplay 玩家应该都没有太大的兴趣,这次的展宣发挺糟糕,现场的 Coser 非常少,我也是第一次遇到拍照被拒绝的情况。

我现在绝大多数的摄影对象都是现场抓来的,以及自己认识时间比较长的朋友,基本都是互勉(免费)。他们玩的时间也不短了,比较有经验,摆姿势也相对自然流畅一些。当然少数情况能通过介绍接到那么几个单子,就当是磨练自己了,和平时一样积极对待就行了。
做漫展摄影赚钱吗
想靠全职二次元摄影去挣钱,其实也是挣的辛苦钱。像我这样的“萌新”大多数都是 10 块钱一张左右,当然有一些比较自信的敢收贵一些,服务质量对不对得上价格就不好说了。技术比较厉害的,可以在一场漫展接单拍 10 多套图,假设一套图 6 张,那么一场下来你也就 700 块钱,况且也不是每一个月都有漫展的,有也是两到三场这样子,档期基本都集中在寒暑假和各种节假日期间。
因此只靠拍场照肯定是不够的,想要多赚一些只能周末/假期去拍影棚和外景,一般按时长收费,出片质量也更高,整体收入也可以更高一些。
前期需要你了解各种不同角色的设定,萝莉/御姐,是活泼还是病娇,是帅气还是可爱的,选择一套适合他的动作框架,最好提前去现场踩点等等,才知道怎么去做构图和拍摄。灯光的操作也很重要,看你想要表达怎么样的画面情绪,亮调还是暗调。当然,像我这种“萌新”都是拍亮的为主,也是最容易上手的,基本都不会翻车。
前期拍摄完了,你还要做后期呢,至少也要做一次简单调色,复杂一些的话还要精修一下,全身上下各种细节,最后调整一下背景或者加一下额外的道具什么的。一般模特老师有自己的想法,像我这种都是简单修一下明显的问题,具体的脸部等细节都让对方自己调整。
截至写文的现在,我的后期技术也是靠摄影朋友的日常沟通以及网上各种教程学到的,依旧只能算是半桶水吧。
跟那些卷王相比,我确实差太多了。前期引导动作,我认为自己确实是个弱项。后期比如那种很复杂的大合成背景,需要 3D 建模经验,我完全没这个机会学好。现在也有很多的后期是靠 AI 生成,然后用 PS 抠图。这我倒是会一些,也已经运用到了现在的作品上了。
这里也放一些我的近期作品,我主要都在拍摄米哈游的游戏角色为主,其他番剧题材自己看过的也略有拍一些,但不多。

角色大昔涟,出镜老师 @酸奶,摄于 8.2 珠海 AS

角色小吱,出镜老师 @Serena,摄于 7.18 广州萤火虫

角色知更鸟,出镜老师 @晚寻,摄于 7.12 上海 BW

角色爱芮,出镜老师 @理子,摄于 7.5 广州绝区零二周年线下生日会

角色开拓者,出镜老师 @雨霖,摄于 6.14 起源城市客厅

角色艾雅法拉,出镜老师 @栗鱼,摄于 5.3 珠海 AS

角色李元芳,出镜老师 @肥鱼,摄于 3.22 中山淋唔到
作品主要放在 我的网站、小红书和抖音账号(搜晓螺就行),也欢迎各位关注支持一下,如无意外一直都会玩,当然失业了会降低一点频次...
玩了这么久的摄影,有什么新的发现吗?
首先最明显的肯定是摄影技术的提升呀。加入到一个新的行业当中,总会发现一些新的机会,扩展了我做程序员以外的视野。光在这方面我都做了不少个小应用了。
比如我的 接单图 用的是网页排版,接入到自己的小窝之后直接更新资源,从而无需手动 PS 反复调整制作。
拿到 Cos 老师的返图之后,我为了偷师自学修图技巧又做了一款 对比工具,这个项目后面还加入了拼图功能,方便发布社交媒体。
我还尝试过制作一个面向影棚的一套自部署全栈应用,整合预约、记账等功能,但目前没有去找开影棚的朋友沟通,暂时搁置,没抽时间继续打磨。
后期还想规划一个自建的摄影资源库项目,并且将 AI Agent 接入进去。主要收集各种动作姿势啊,构图啊,角色设定相关,以及各种想法。(勤能补拙了属于是,多拍多记录多收集)
哦对了,我已经上线了一个 Vibe Coding 的全栈项目 Memento,它的主要功能是记录博主与 AI 的聊天或是各种群聊记录,相较于博客有着更清晰明了的排版方式,适合我这种在文章内简单介绍,又额外提供完整对话内容的使用情况(文章后面带有实际使用案例)。目前软件闭源维护,欢迎找我申请试用!
玩摄影真能认识到女朋友吗
真的不要想着能够通过拍照去认识自己的女朋友,这个观念本身就是存在争议的。你和模特的本质上是一种合作关系,就和你去上班的同事一样。
但不可否认的是,确实有相当一部分人都是和自己的同事做对象乃至结婚的。在摄影圈里面也一样,的确有模特跟自己最熟悉的摄影在一起了。
经历这样的感情关系也挺复杂的,一旦某一次观念不合分手了什么的,可能都会影响双方各自在圈子里面的评价,因此在发展感情关系的时候对双方都做出了不小的考验。比如说你是否接受自己的女朋友给其他的摄影拍,她是否可能会和其他摄影聊的更开心,这看上去是一件小事,可能有不少人还是接受不了。
关于入行做摄影,有什么好说的吗?
你需要真的对摄影这方面有一定的热情,早在我购买相机之前,我其实就是一个喜欢大量通过拍照记录生活(手机摄影)的人,只是现在有了相机之后把这件事做得看上去更“专业”了一些,还会去研究调色技巧啊,后期啊相关的各种东西。
再一个是你一定会跨入到一个关系微妙的人际交际圈当中,这也是我做了这么多年技术和博主也很少会遇到的情况。沟通技巧非常重要,实在不会说话的话,尽量保持沉默,也能防止产生一些不必要的误会。比如前面提到的瓜条问题,个人情绪和臆想毫无胜算,必须要有实际的经历做支撑。
之前我就产生过一次比较严重的沟通失误,我没有站队任何人,只是想了解事件细节,结果对方已经把我当敌人了,认识了这么久的哥们关系就这么破裂了。他后面还把我给踢出群了,当时对我心态影响也不小(毕竟是第一个二摄相关的群聊)他要还是坚持这么认为我也没办法,只希望以后在漫展不会相互打扰了。
复盘:三人关系修复建议
最后也是最重要的一件事,你手上一定要有足够的马尼。摄影确实是一个极其花钱的爱好(但也有省钱的玩法,看你能不能接受老单反的对焦)。至少从我这里来说,光各种设备累积起来肯定掏了几万块钱了。风光摄影需要长枪大炮,光一个长焦镜头都快一万左右了。人像摄影对镜头的要求略低一些,三次元摄影我不太了解,但二摄这边对灯光设备相关的需求会更高。
如果想要丰富画面的效果,光在相机上面扣一个机顶闪肯定是不够的。像我这样买一个大功率的闪光灯,一个就要 2000 块钱左右了,再加上灯架等设备,去一趟漫展必须推一辆小车,你完全可以理解成「负重前行」了。但我觉得问题不大,就当健身了。
要是上面的这些问题,你都认为自己完全能克服没有问题,那么你确实可以考虑进来玩玩,丰富一下自己的生活爱好。
以上就是我的本次分享,仅作为个人建议,仅作参考,有什么问题可以在文章后续提出。
2026-06-18 16:46:00
继续失业在家无所事事中,打算找点 Vibe Coding 能落地的实际业务,巩固一下后端和运维相关知识。看到之前认识的一摄影师哥们被公司集体裁员后,目前选择自己创业开影棚,于是在想能不能做一个这样的系统结合实际,于是便有了此项目。
前几天冲凉想到之后初步规划过,写了一份约 2500 多字的文档,随后让 AI 输出了一份完整的「产品设计文档」和对应项目的「技术说明文档」,我再核实确认修改了细节。本文仅作过程和踩坑记录,不包含具体 Prompt 和对应的业务细节。
制作一个影棚管理系统,主要面向于摄影工作室场景开发,包含官网(作品展示、预约影棚等)、管理后台(管理员维护)、后端三个项目。
今天的 Vibe Coding 已经完成制作了官网、服务端、后台三端的核心代码,期间也确实遇到不少问题,这里简单记录一下。模型主要是采用 GPT-5.5,部分流程自动降为 Cursor 的 Composer 2.5。
官网方面,我首先是另外找 AI 对话探讨得到一套靠谱的设计,最终得到结果如下,其实从生成结果来看,整体还不错,就是顶上那个菜单风格真的是有点太熟悉了,哈哈。
整体风格:
- 高级感
- 沉浸式
- 年轻化
- 二次元审美
- 作品驱动
- 内容社区风格
避免:
- 企业官网风格
- 传统婚纱摄影网站
- 影楼宣传页
- 粉色二次元模板风
- 动漫论坛风格
参考风格:
- Pinterest
- Instagram
- Behance
- ArtStation
- 小红书
- Linear
- Raycast
视觉关键词:
- Dark Mode First
- Editorial Design
- Cinematic
- Immersive
- Premium
- Modern
- Elegant
- Gallery Focused
- Large Typography
- Minimal UI
- Rich Photography Showcase

后端方面,问题主要是 Prisma 的接入相关的坑比较大,还引出了一个 ESM 和 TypeScript 打包配置的问题。
最开始遇到的问题是 PrismaClient 实例失效,直接报错找不到,排查后发现是 schema.prisma 的配置问题,需要指定 output 路径到当前项目下,它默认存的位置有问题,导致 TS 识别不到。这里基本对着 官方文档 接就能解决,AI 自己写的有问题。我自己写的是如下,只要放在当前项目内就行。
generator client {
provider = "prisma-client"
output = "./generated"
}
Prisma 接入方式从 7x 版本后发生了变化,包括 seed.ts 文件,需要手动传递对应的适配器包过去才行,AI 这里就搞晕了,用了旧版本的写法,我的解决方式是直接把最新文档的链接甩过去让它修改,问题真的解决了吗... 并没有!
import "dotenv/config";
import { Pool } from "pg";
import { PrismaPg } from "@prisma/adapter-pg";
// 这里必须写 .js 不然找不到文件
import { PrismaClient } from "./generated/client.js";
import * as bcrypt from "bcrypt";
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL is not set");
}
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
此时报错提示找不到模块(错误信息被删了我也找不到了)
这种 CommonJS 和 ESM 的打包问题,其实我自己也理解的不是很透彻,首先第一步 Prisma 要你在 package.json 里面增加 "type": "module" 就能使 NodeJS 认为当前包默认走 ESM 模式运行,因此你得确保构建后的产物也是 ESM 的代码(之前我有一个「小窝后端 Ext」项目是 KoaJS,用 ESBuild 构建成了 ESM 的 JS,就能直接运行,本质上就是 node index.js 这种执行方式),而 NestJS 自己有一套 CLI 工具,本质上是包装了一次 TSC,具体生成啥根据 type。说白了问题就这样解决:
type=module)moduleFormat=cjs)、NestJS 默认 CommonJStype=module)Prisma 官方文档 的说明,就要求将包设置成 ESM 模式。
In package.json, add the type field set to "module"
{
"type": "module"
}
我选择了这种方式,于是所有的 import 全部报错,都需要手动加入 .js 后缀才能正常访问。我看别人的项目没这个问题,为什么呢,后面单独列个说明。
而 NestJS 自己文档 中的描述则截然相反,要求将 Prisma 自己的输出构建为 CommonJS 模式...
Set moduleFormat in the generator to cjs
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs"
}
The moduleFormat configuration is required because Prisma v7 ships as an ES module by default, which does not work with NestJS's CommonJS setup. Setting moduleFormat to cjs forces Prisma to generate a CommonJS module instead of ESM.
这就是集成第三方最坑的问题,双方各有其词,似乎就没有最正确的实践。
以上问题解决后,项目基本就正常跑起来了。
对比配置后发现是 tsconfig.json 有两个关键配置:
{
"module": "esnext",
"moduleResolution": "bundler"
}
在我这个 NestJS 后端项目写的均是 nodenext。这两个到底啥区别,这是 AI 的解释:
第一个:前端打包器专用
module: esnext:编译输出原生 ESM 语法不降级,保留 import/export,不转成 CommonJS;仅给打包器(Vite/Webpack/Rollup)使用,不能直接丢给 Node 运行。
moduleResolution: bundler:TS 模拟打包器解析规则,适配前端工程习惯。
第二个:Node 原生运行专用
module: nodenext:跟随新版 Node 模块规范,自动根据 package.json type、.mts/.cts 区分输出 ESM/CJS 产物,产物可直接 node dist/main.js 运行。
moduleResolution: nodenext:1:1 复刻 Node.js 原生模块解析逻辑,严格遵循 Node ESM/CJS 互操作规则。
Nest CLI 编译底层逻辑
nest start = nest build + 运行 dist 主文件;nest build 底层调用原生 tsc 编译,完全遵循你 tsconfig.json 里的 module/moduleResolution 配置,Nest 不做特殊转译,仅做装饰器元数据收集。
问:也就是说第一种模式是走了一次打包器,需要打包器自己解决引入问题。而我这个是直接用 NestJS 的 CLI 直接构建输出了,它本身并没有打包的功能。是这样吗
1. 先分清楚两个概念:tsc 编译 vs 打包(bundle)
① module: esnext, moduleResolution: bundler
不代表自动走打包器,只是让 TS 的类型解析规则模拟打包器(webpack/vite/rollup)。
bundler 只是一套解析逻辑:允许省略 .js、自动识别 index、兼容前端库的导出字段;nest build(底层纯 tsc),不会触发任何打包,只会输出一堆零散 dist 文件。所以你的理解一半对一半错:
正确部分:这套配置是给打包器环境设计的,依赖打包器运行时补齐导入路径、处理模块;
错误部分:TS / Nest CLI 本身不会自动调用打包器,只是类型校验规则对齐打包器。
② Nest CLI 原生能力
默认 nest build = 调用 tsc 做转译,不打包:
.ts 文件.js 文件,没有代码合并、没有依赖内联;nest start 只是 node dist/main.js 直接跑编译后的零散文件,全程无打包步骤。Nest 只有一种情况会打包:你手动启用 webpack 构建(nest build --webpack),否则完全无打包逻辑。
2. 为什么 esnext + bundler 配 Nest 直接跑会崩
import './utils'(不带 .js);"type":"module" 时,node 会把文件当成 CommonJS,遇到 import 直接报错。这套配置的设计初衷:
源码交给 vite/webpack 打包,打包器内部会处理路径补全、模块格式兼容,不会直接用 node 执行 tsc 输出的裸文件。
3. nodenext 为什么适配 Nest(纯 tsc 编译后直接 node 跑)
moduleResolution: nodenext 是模拟 Node.js 原生模块解析规则:
import './utils.js';nest start 直接 node dist 就能正常加载所有模块。一句话总结你的疑问
esnext + bundler 这套规则依赖打包器处理导入路径/模块兼容,但 Nest 默认 CLI 只是 tsc 转译、不带打包;nest build,就不能用 bundler 模式,否则编译产物无法直接 Node 运行;nodenext(或传统 commonjs),而非 bundler。所以解决办法无非就两个,要么继续走 nodenext 模式直接用(NestJS 官方默认 Starter 模板就是这个),每个文件携带 .js,要么用 bundler 自己配置走个打包器,就免除这个困扰了。
2026-06-09 23:44:00
这次面试,主要还是对面主动联系为主,应该是看我写了 NextJS 和电商相关经历吧,他们认为和岗位比较匹配,于是得到了这次宝贵的面试经历。面试过程还是挺紧张刺激的,毕竟人家问到的,不一定你平时经常用。
HR 面没有具体考察技术,但我看也问了不少涉及技术、协作相关的话题,和一般公司的人事还是有些不同,整体回答我感觉正常水平吧。以下回答都是在对话里面完成的,所以可以先思考再回答,并完整摘录。
HR:你说说你工作的心得吗,之前工作的部门规模,参与其中的角色作用
答:上家公司挺好的 是创业公司自营业务 大家相处起来氛围也很好 我们团队 20 人左右,我参与的项目里面 10 人这样。因为做了四年 我这边参与维护过的东西也多,就以近期的 AI 项目来说我这边主要是参与制作一个无限画布的功能,因为工期有一点紧,代码质量有一些不够好,我就去对这块做各种优化改进,以及最影响使用的一些性能问题吧 主要是涉及到 React 的组件渲染这块
HR:因为公司的全技术都是老板做起来的,他比较喜欢直接开会对接工作,我们没有产品经理,需求大多是一起讨论、简单文档,原型和说明反而要技术这边编写。你之前有没有经历过没有产品、直接对接技术负责人/老板的工作模式?遇到需求比较模糊、不完整的时候,你一般怎么处理?
答:有的 我第一份工作就是这样 当时在旅行社那边技术负责人就是和其他部门做对接 相当于是直接对接具体使用系统的人 不过当时这块的具体流程应该没有这么细 都是他们大佬带头开干 我这边做对接为主
不过结合之前的工作经历 其实我们也并非每个开发任务都有很详细的产品文档,也有不少是口头需求 临时改动 不过都是针对用户反馈和数据去做的调整 这种需求模糊挺多的,一般分两种情况,一个是业务层面的,这就只能拉上级详细确认,另外一种是交互层面的,我会自己简单设计方案让产品看一下,或者他觉得有更好的方案,再决定用哪个更好的方案
HR:今晚 8 点 45 分左右,进行技术面试,你时间上可以么
答:可以
HR:你期待工资多少?
答:(想先了解上班时长)公司正常上班时间是?
HR:10~7点,期间有1个小时休息,
答:双休吗
HR:是的
答:也就是 12-1 点休息这样是吗
HR:不是固定的,因为多数 11 点半左右开始开会到 1 点左右,你最近的工作时间和待遇如何
答:之前工作时间都比较固定 早上也是 10 点这样,中午是休息 2 小时,晚上我一般 6 点半到七点走,看任务完成情况吧
HR:我们每天会有 IT 技术会议,老板亲自带队跟进进度、同步需求、解决问题。需要你按时参会、主动汇报进度、主动反馈卡点。前后端错开开会,例如今天中午前端开,明天后端开
HR:我们团队没有测试,所有功能都是开发自己自测、自己验收、自己保证上线质量。你平时开发完,有没有自测的习惯?一般会检查哪些内容?如果上线后出现自己没测出的问题,你会怎么处理?
答:功能自测肯定是必须的 包括我每次提交代码之前 都会人工确认一次 确保不会把临时改动也带上去
不过也得分情况比如说有些功能需要有多个前置步骤,可能就会跳过一部分主要测改动后的部分,如果时间允许的话肯定需要完整跑几次
上线后出现问题的情况客观来说肯定会存在 不过因为代码合并产生的冲突导致的比较多 比如多个人在改同一个模块的情况下 所以每次合并的时候还是得确认清楚比较好 关于线上问题当时我们的方案是做 sentry 监听线上的报错信息,以及 clarity 跟踪用户的操作
所以还有一种方案就是 尽可能拆出不同的项目 比如说商城是前台,管理业务时另外一个后台项目,这样也能确保不会相互影响
前端这边有几位同事一起是吗
HR:你是指什么?
就是你说的前端开会
HR:目前 IT 团队配置前 2 后 2,大家都会参与
HR:你上一份薪资待遇如何?
答:税前 xx-xx
技术面考察的知识点非常丰富,基本上简历上能问的都问了一遍,面试官还是很有水平的。简单复盘下来,有些问题我完全答对,有些回答不够直接明了但也答中关键点了,有些比如 Jenkins、Electron 相关的就确实是不太清楚,也着实是没有答上。以下回答部分为印象记忆,可能和实际有出入。
面试官:http 1.1 2 3 的主要区别是什么
答:印象中区别在于连接重用,之前用过 Lighthouse 提示我有些资源走的是 1.1,优化过
面试官:1.1 和 2 呢
答:没回答上
复盘:这里主要区别的确是并发,1.1 和 2 的最大区别,而 3 增加了 QUIC,使用 UDP 协议
AI 的回答挺详细的,这里分享出来记录 传送门
面试官:这里不行不要紧,那这个浏览器内核有哪一些
答:主要三个,Safari、Chrome、IE
面试官:它们的内核是什么,这是浏览器
答:Blink(Chrome),还有一个叫什么来着,Webkit(Safari)
复盘:其实还有一个 Firefox 浏览器,用的是 Gecko,不过其实很少人用,IE 是 Trident
面试官:可以说明下 Restful API 的 Methods 吗
答:主要用的是 GET、POST、PUT、DELETE 这些
面试官:POST、PUT、PATCH 的区别是啥
答:我理解 POST、PUT 都是提交数据,PATCH 是更新
复盘:这里就答错了,POST 是创建,PUT 是全量更新、PATCH 是部分更新,参考 传送门(之前自己小窝的后端也有这个问题,一个更新接口既要支持全量更新也要支持部分更新咋弄了,我倒是用了一套很诡异的实现,默认按照 PATCH 的逻辑形式来,传什么改什么,全量传默认等同于 PUT 了其实
面试官:Tailwind 里面 Container 是干什么用的
答:Container 主要是容器查询,比如根据容器大小去设置子元素的宽高啥的
面试官:Tailwind 里面哪个呢,怎么用
答:(这里我理解成 md:xx sm:xx 这个东西了,他问的是 className 里面写的那个)这里没有很好回答出来
复盘:直接看 文档 吧,就是这样的一个东西
<div class="@container">
<div class="flex flex-col @md:flex-row">
<!-- ... -->
</div>
</div>
我理解其实就是基于 CSS 容器查询的一个实现。最近确实用到了这个东西,不过是在我 Vue 3 哪个主页项目上,用的还是纯 CSS,哈哈。
面试官:Grid 和 Flex 的区别
答:现在我 Grid 用的多一些,Flex 兼容性更好,两者主要区别一个是宫格,宽高固定,一个是宽高自由可以单独指定。
面试官:block、inline-block、inline 区别是啥
答:block 就是最常见的占满一行的,inline-block 我记得是基线对齐那种模式,主要是和 inline 一起,inline 就是常见的文本排版,这个 inline-block 稍微有点不一样是个整体,也参与到那个基线对齐
复盘:这里其实也算答到了一点,如果更具体一点比如说 inline 的 margin 上下无效、padding 不影响行高计算估计更加分,依旧是 AI 传送门
面试官:align-items 和 justify-content 区别是啥
答:一个垂直居中一个横向居中(实际上这个根据 Flex 的对齐方式,会有出入)
面试官:justify-content 的 space-around、space-between、space-evenly 区别是啥
答:三个 div 举例的话,一个是左右有间距、一个是中间有间距,再一个是比较居中(这里其实没讲清楚,但大概意思一样)
复盘:其实这几个都不咋常用的,大多都是宫格形式布局... 直接上 MDN 链接
space-around:每个元素周围分配相同的空间
面试官:React 的生命周期
答:现在新版 React 已经没有生命周期函数了,都是改成 useEffect 这种 Hooks 了。(他其实想问我组件挂载到结束,React 都干了些啥,但是一开始没表达清楚我当时也没转过来,死磕在 Vue 那种生命周期函数上了,之后回答完全偏了)
复盘:我的理解是这样,首先组件挂载,先执行渲染函数,得到虚拟 DOM 然后加入渲染队列,渲染成实际 DOM,再执行 useEffect,更新组件状态同理,最后卸载组件。还是看 AI 完整解答 吧,它讲的更详细。
面试官:useCallback 和 useMemo 的区别
答:一个是类似 Vue computed 合并计算结果,一个是缓存函数,都需要传 deps(完美回答)
面试官:为什么这段 TypeScript 后面要加一个 as const
const obj = {
city: {
name: "ShangHai",
zip: 200000,
}
} as const;
答:as const 主要是确定变量类型,假如不写的话就有可能识别成 string 或者联合类型 string | number 这种,就不方便去判断具体类型
复盘:也算是讲到了一点但回答其实不太准确,其实它会把所有值设置成 readonly 表示这个值不会发生任何变化,并且每个对象的类型固定,这里涉及到了“类型拓宽”概念,传送门。
面试官:SPA、MPA 区别是啥
答:SPA 就是 React 这种(我理解成 CSR 了),MPA 就是 NextJS 那种(其实这里是 SSR)
复盘:这里基本答错。CSR、SSR、SSG 是一个概念,SPA、MPA 又是另外一个概念,现在的框架基本上都是融合操作,并不能算具体的 SPA 了,MPA 其实就是之前 PHP 那种老的,依旧 AI 传送门
面试官:interface 和 type 什么区别
答:我这边常用可能还是 interface,type 主要是做联合类型,虽说 interface 也可以做(其实说的是 extends,继承和联合还是有区别的)
复盘:其实区别也挺多,不过建议定义对象类型优先使用 interface,其次再去使用 type。传送门
面试官:我看到你提到了 Electron,我要在视窗里面建立独立的悬浮部分怎么做
答:这个确实没做过(上一次搞 Electron 都是 2 年前了,很多 API 也没印象了,彻底挂)
复盘:传送门,其实我也不确定 AI 的回答是不是他想问的。
面试官:Electron 的 contextBridge 是干什么用的
答:我记得是主进程和子进程去通信使用的,一个前台一个后台,需要用这个传数据
复盘:直接看 文档 吧,确实差不多是这样子,但我认为这个回答并不是很好,毕竟真的好久没摸过 Electron 了(看了眼我 Electron Fiddle 是 24 年安装的,之后一直吃灰)近 2 年都在搞 Felo,他们也没有说需要做客户端。
面试官:来一道 IQ 题(印象记得是猜谁才是凶手,只有一个人回答是正确的)
复盘:这题似乎没有标准答案,主要看你怎么分析每个分支下会产生什么结果,第一次遇到这样的面试题,确实给困惑住了,这是 AI 对这道题内容的猜测,传送门 应该差不多是这样子。
面试官:你不是做了个 AI 项目么,你觉得当时最核心的业务目标是什么,是提高用户留存还是付费还是验证产品方向呢(其实一开始也没听清楚)
答:主要是文员这块吧,我们做的是 PPT、网页 什么的,竞争也挺大的
面试官:你觉得哪些功能是正需求哪些是伪需求
答:我觉得网页多一些
复盘:其实这里我也不知道如何回答,毕竟是运营和产品的策略嘛,我们感觉没啥用也得做不是么,仔细想想似乎这种需求挺多的,有一些还直接被其他优先级更高的需求给埋没掉了,印象最深的是要给 PPT 里面的 SVG 做一个编辑器,虽然我引入了一个库尝试了一下但是发现有个不小的 Bug 挺难解决的,就没继续看了
面试官:你推动切换组件库和优化代码这块本质上是解决什么问题
答:组件数据重构是开发的时候不够顺
面试官:你这个是个人项目还是公司项目,公司项目怎么协调这个平衡
答:因为当时是我们大量推 Vibe Coding 嘛,每个人自己做一套,缺乏统一约束嘛,所以就想着给它做稳定。
面试官:你有没有考虑过需求排期问题,怎么解决的
答:有需求肯定是需求为主嘛,这些都是有空去优化的,当然我也希望一开始流程就能写清晰,这样最好
面试官:那你项目约定怎么做好
答:首先第一个就是组件目录结构那些嘛,哪些公用哪些单独的,Vibe Coding 也是一样你得指清楚哪里哪里嘛,就这块比较重要一些
面试官:我看你用过 Jenkins,那你解释一下 WebHook 是干嘛用的
答:主要是触发构建,你肯定要去 Git 平台绑定这个 Hook 才能发这个创建请求嘛
面试官:Jenkins SCM 是什么
答:这个词挺熟悉的,但是没想起来是什么,是以 Jenkins 本地作为驱动的啥(彻底挂)
复盘:SCM = Source Code Management(源码管理),其实是 Jenkins 里面拉项目仓库代码的方式,传送门 虽说这个 AI 的回答我看的也挺懵的,实际使用差不多就是设一下仓库地址,拉到临时的位置,就可以用 Shell 去做构建等操作了。我自己写的一个简单的 CI 工具,直接只有 Shell 功能,直接在服务器构建,其他全没有...
面试官:Nginx 你主要拿去干什么,有弄过 SSL 吗
答:网站托管,反向代理,我现在用 Caddy 多一些,因为它自动处理 SSL,不用我自己搞,之前用面板操作所以用 Nginx,它里面有个签发 SSL 的工具这样。
挂
HR:“中午好,上周的面试结果出来,很抱歉,本次未能入围,谢谢您参与面试。祝您早日找到适合的工作。”
2026-05-19 16:29:00
众所周知,威联通 NAS 内置的“内网穿透”服务在国内是有些问题的,myQNAPcloud 实际上是需要一个公网 IP 才能链接到他们家。此前博主使用的是「自建 FRP 服务器」方案来实现反向代理实现穿透,后续 Docker 一直没配置好且无法安装新的镜像,搁置了许久。
其实问题本源在于国内访问 DockerHub 的网络环境并不友好,只需要简单配置下即可。这里博主使用的是几年前购买的 TS212P3 型号(也是当时入门买的最低配的)作为演示。
本文章于 26.04.30 立坑,结果于 26.05.19 花了半天时间才写完...
首先打开 ContainerStation,点击左侧「属性」菜单项,在右侧页面点击切换到「Registry 服务器」Tab。
这里我们使用 https://docker.1panel.live 这个镜像源,确保国内的网络环境也能正常拉取镜像。

从 DockerHub 上获取 最新的镜像地址,这里我选择的是 latest-arm64 版本,因为 TS212P3 是 ARM 处理器的,并不支持传统 X86 的。
docker pull cloudflare/cloudflared:latest-arm64
这是正常的获取镜像的命令,但是我们需要替换成对应镜像源获取。点击 ContainerStation 左侧「镜像」菜单项,在右侧页面点击「提取」按钮,Registry 服务器选择我们刚创建的,「镜像文件名称」和「映像版本」分别输入 cloudflare/cloudflared 和 latest-arm64,点击「提取」。

此时镜像已经拉取完成,我们前往 CloudFlare 后台开通 Tunnel 服务。
登录到 CloudFlare 后台,点击「保护与连接 -> 联网 -> Tunnels (新)」左侧菜单项,在右侧页面点击「创建隧道」按钮,输入「隧道名称」,点击「创建隧道」。

点击 Docker 选项,下面会出现你的启动指令以及对应 Token,这里直接复制出来。
docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token {你的 Token}
回到刚刚获取镜像的 ContainerStation 界面,点击镜像右侧「+」按钮,在出现的弹窗里面修改「进入点」的命令,粘贴前面在 CloudFlare 后台拿到的启动指令,修改前面部分改成 cloudflared。
点击「高级设置」,点击「网络」Tab 页,「网络模式」选择「Host」主机模式,最后点击「创建」。

此时应该能创建成功并正常运行,在 CloudFlare 后台也会提示“您的隧道现已可以使用”。
回到 CloudFlare 后台,找到刚创建的 Tunnel 实例,点击「添加路由」按钮,选择「已发布的应用程序」选项。之后指定自己的主域名和二级域名,在「服务 URL」处输入 http://localhost:5000

此时访问你设置的对应 URL,应该就能正常访问 NAS 后台界面了。
2026-03-16 11:03:00

列表的漫展圈年轻人比较多,特意做成了“瓜条”形式希望能被更多人看到。这里也提供了由 AI 改写的大众阅读版本,方便各位博友阅读,大致表达是相似的。
受害者:博主
骗子:自称"王艳"的一位阿姨
简要说明:博主热心帮助一位陌生人借钱坐车,却遭遇诈骗。骗子自称是某物流公司的高层员工(生意人),说自己出差后将手机和钱包落在了司机车上,而自己是外地来的孤身一人,丈夫已离异且是限制高消费人员。实际上这些内容全部是编造的,她就是个骗子。
3 月 13 日下班后,博主正常休息吃完晚饭,在回公司的路上遇到这位阿姨。她主动过来向博主问路,说自己要去一个很远的地方(深圳宝安机场)。博主正常打开地图给她简单指路,她表示自己没有手机没办法乘车和购票,需要抵达那个位置联系对应的送货司机。博主当时就问她为什么要去这么远的地方,她强调自己是做生意的,在某个物流公司工作(没听清具体名字),在附近谈客户。她还询问博主觉得在附近开一家店的潜力如何,博主表示附近的制造业工厂确实挺多,或许有这个需求(但博主并不是内行人员,她问博主这样的问题博主又该如何回答呢)。
她还会用各种常见话题和博主套近乎,比如问博主年纪多大,是否有谈女朋友。等博主放松警惕后,她开始询问博主的行业、工作地点和薪资,以及是否是本地人、家乡在哪等(博主出于个人保护,都模糊回答了)。她说自己平常在南京发展,如果有机会博主去那边做客,她会请博主吃饭,还强调出门在外多一个朋友能多一份机遇(实际上这是博取信任的话术)。
为了表示自己能还钱,她要加博主微信。那个微信号用的是一个男人的头像,博主当时就表示怀疑,但她说是和丈夫离婚了,这个号现在实际上是她在使用。她还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给博主。
博主用支付宝给她换了现金后,打算给她拍照留存。她当即破防表示博主对她不信任,要求删除照片。博主表示现在骗子太多,这属于正常防范。删除照片后博主本以为事情结束了,但她仍不断用话术影响博主。博主出于好心最终还是借给了她。她当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)。
她承诺第二天还钱,但实际并未归还。截止到这篇文章编写时间,微信好友申请依旧处于未通过状态。
现在简单复盘一下,其实她全程的对话充满了各种套路和漏洞。博主此前也遇到过几次街头骗局并成功逃脱,无奈这次对方特别精明,博主着实是大意了。
疑点一:打车方式不合理
从最开始的问路就挺多问题了。她一直强调自己出差要公司报销打车的费用,必须要开发票,而打出租车不能提前支付,只能打表后结算费用。这样她就引导博主以现金支付。
博主和她去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际上店长可能已经在警示博主这个人有问题,让博主好好想想,但当时博主因工作疲惫没想到这个可能性)。
疑点二:询问博主的工作安排
因为博主已经说自己在工作了,她还询问博主明天还要不要上班。这实际上很有可能是担心自己骗了博主之后再次在附近作案会被抓获。因为是小额诈骗,警方一般是不予处理的。
疑点三:不向儿子求助
既然她都有收入这么高的儿子了,怎么不能直接求助他呢?这也是一个巨大的疑问。其实完全可以让博主用手机联系她儿子,然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会用其他理由推辞,比如已经和儿子分家了什么的)。
疑点四:情绪激动后的反向施压
她情绪激动后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 博主,明显表示博主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术,恶人先告状,继续利用了博主的善良)。微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之博主根本没有办法找到她。
人在外地的身份是自己给的,今天她是企业员工,明天就可以是霸道总裁。所有的身份信息都无法核实。
博主的这次经历再次向各位亲朋好友发出警钟:面对陌生人一定要提高警惕,对方可能会疯狂输出话术影响你的判断。如果来不及思考,则最好提出找警察或直接拒绝。
苦主热心帮助陌生人借钱坐车惨遭背刺
苦主:匿了
瓜主:自称“王艳”的一位阿姨
省流版:苦主热心帮助陌生人借钱坐车惨遭背刺,瓜主称自己是某物流公司的高层员工(生意人),出差后将手机和钱包落在了司机车上,而自己是外地过来孤身一人,丈夫离异且是限制高消费人员。实则内容全部编造,骗子一个。
苦主在 3 月 13 日下班后正常休息吃晚饭,回公司路上遇到瓜主表示要问路,说自己要去一个很远的地方(深圳宝安机场)。苦主也是正常打开地图简单指路,瓜主表示自己没有手机没办法乘车和购票,需要抵达该位置联系对应的送货司机。苦主当即问为什么要去这么远的地方,瓜主强调自己是做生意的,在某个物流公司(没听清),在附近谈客户,还询问我附近开一家店的潜力如何,苦主表示附近的制造业工厂确实蛮多,或许有这个需求(但苦主并不是内行人员,你问我这样的问题我又要如何回答呢)
瓜主还会以各种常用话题和你套近乎,比如年纪多大,是否有谈女朋友,等你放松警惕后,开始询问你的行业、工作地点和薪资,以及是否本地人,家乡在哪等(苦主出于个人保护,均模糊回答)。瓜主说自己平常在南京发展,如果有机会前往做客,瓜主会请苦主吃饭,强调出门在外多一个朋友能多一份机遇,实际为博取信任的洗脑话术)
为了表示自己能还钱加了个微信,该微信号是一个男人的头像,苦主当即表示怀疑,但瓜主说是和自己丈夫离婚了,这个号现在实际上是瓜主在使用。瓜主还说自己有个儿子做主播,月收入可达到 2W,表示自己是生意人不差这点钱还给苦主。苦主为其借钱(ZFB 换现金)后,打算给瓜主拍照留存,瓜主当即破防表示苦主对她不信任要求删除照片,苦主表示骗子太多属于正常防范,删除照片后本以为事情结束,但瓜主后续依旧持续输出洗脑苦主,苦主出于好心最终还是借给了瓜主。对方当即表示感谢,并附上各种祝愿(祝你工作顺利,生意兴隆?)
对方承诺第二天还,但实际未还,截止到瓜条编写时间,微信好友依旧处于未通过状态。
现在简单复盘一下,其实瓜主全程的对话也是充满各种套路和 Bug 疑点的。苦主此前也遇到过几次街头骗局并成功逃脱,无奈此次对方特别精明,苦主着实是大意了。
从最开始的问路就挺多问题了,瓜主一直强调自己出差要公司报销打车的费用(必须要开发票,而打出租车不能提前支付,只能打表后结算费用,后续引导苦主以现金支付)
苦主和瓜主去了附近的一家便利店,店长当即表示无法提供这么大面额的现金(实际可能已经在警示苦主这个人有问题,好好想想,但当时苦主因工作疲惫没想到这个可能性)
因为苦主已经说自己在工作了,瓜主还询问过苦主明天还要不要上班(实际很有可能是担心自己骗了苦主后再次在附近作案被抓获)因为是小额诈骗,警方一般是不予处理的。
既然你都有收入这么高的儿子了,怎么不能直接求助他呢,这也是一个巨大的疑问,其实完全可以让苦主手机联系对方然后即时转账,也就不存在借钱这么一回事了(当然她很有可能会以其他的理由推辞,比如已经和儿子分家了什么的)
瓜主破防后还在强调自己的着装以及已经加了微信,表示自己是清白的,当即开始反向 PUA 苦主,明显表示苦主在冤枉普通人。“你把阿姨当作什么人了”“你这是在侮辱我”(实际上这也是一种洗脑话术不对么,恶人先告状,继续利用了你的善良)微信号可以是假的,好友是可以不通过的,手机号也可以是空号,总之你根本没有办法找到对方。
人在外的身份是自己给的,今天我是企业员工,明天我就可以是霸道总裁。苦主的此次经历再次向各位亲朋好友发出警钟,面对陌生人一定要提高警惕,对方可能会疯狂输出洗脑影响你的判断,如果来不及思考,则最好提出找警察或直接拒绝。
最后让我们来看看这位丑陋的女人,必须得曝光出来:


2026-03-07 17:57:00
前几天 我将 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} 不存在`);
};
我依次尝试了一些改动,发现都没解决...
styles.module.less -> photo-collection.module.less).collection 下样式只写一条 background 规则)if 和 else 的形式只好求助牛逼的 Claude Opus 4.6,初步排查原因是开发模式和生产模式下生产的 CSS Hash 不一致。
找到根本原因了!
_collection_f6c82_1
_collection_x8r5c_1
._collection_f6c82_1(和客户端一致,和服务端不一致)这就是问题所在:CSS Module 的类名哈希在 SSR 构建和客户端构建之间不一致。
具体表现:
class 是 _collection_x8r5c_1
._collection_f6c82_1 的规则这与 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-modules 的 from 路径不一致 → 哈希不同
这是 Vite 在处理 CSS 预处理器(LESS/SCSS/Stylus)与 CSS Modules 组合时的一个长期存在的 SSR 一致性问题,在 LESS 这里表现得尤为明显。
你用 .css 替换 .less 已经是最干净彻底的修复方式了,不需要任何额外配置。我之前加的 generateScopedName 现在可以去掉,因为它只是用来绕过这个问题的。
这里我单独重命名更新了那个出问题的 .module.less 文件后,其实并没有彻底解决问题。
奇怪的是,第一种方法,那怕采用最直接的 文件名 + 路径 生成 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 计算
你说得对——generateScopedName 的 filename 参数在两次构建里看上去是一样的。这是因为 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 打包结果 |
|---|---|---|
| PhotoCollection | 1 个页面([[slug]]) | → 打进该页面专属的 独立 chunk |
| Pager | 5 个页面 | → 被提取到 shared chunk |
| ThemePanel | layout(全局) | → 被提取到 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 整体优化重写,拥抱最新技术吧!