2025-09-12 00:55:00
这篇总结酝酿的时间也挺长了,主要还是写了不少去漫展的事情。今年主要加强了在摄影方面的技能点,写码方面没有以前那么上进,已有项目的维护不多,也没有开新项目了。
阅读指南:
本文内容较长,图片数量也比较多,流量预警!如果速度较慢可以尝试使用科学上网!
如果是博客和项目相关可直接往下阅读,漫展相关可直接点击 传送门

3 月末,我酝酿了几个月的新版设计风格,正式上线。主要使用了 Vue 3 + Nuxt 框架来开发,依旧是手搓组件和功能,虽说旧版依旧还保留着,只是前端部分已经不再维护了。
此后都在不断优化各种小细节,这些改动我认为比较值得列出来:
5/28:获取表情数据更换为 setting/list 新接口,并改为状态管理存储初始值
6/15:修改 SEO 关键词,解决 SVGO 砍掉动画的问题,更新滚动条样式
7/27:新增在读详情快照页,调整在读页面布局
9/8:相册单独的灯箱组件,展示照片 EXIF 详细参数
12/24:优化日记列表页媒体过多的阅读体验,默认折叠显示剩余图片数量
基本上都是以服务前端为主的改动,也是简单挑出一些值得列出来的:
2/27:增加 useTokenPermission 授权方案,在读增加接口支持 Token 授权
3/27:尝试解决 B 站视频不能获取的问题
4/14:API 请求错误显示成 JSON
4/21:新增追番进度更新接口
5/7:引入 Composer 和 PHPMailer
5/10:基础页面的 Sitemap 功能,后续需要补上日记、项目的
5/27:增加判断是否有丢失源文件的媒体接口
7/27:完善在读接口的快照参数
7/27:新增快照内容转存图片资源的接口
9/19:兼容 PHP8
9/20:不知道为什么查表结果不是 string 而是 int 了,先改了吧
9/21:修改跨域相关设置,允许我的网站访问
11/20:概括 RSS 日记中事情实际发生的时间
1/20:修复通知组件动画异常问题
5/30:useRequest Hooks 新增缓存功能

在去年的总结里我首次提到了这个项目,但我似乎并没有讲清楚它到底是干嘛的。它主要就是程序自动化为了提高效率而生的,随着网站功能的不断更新,手动维护网站上的数据只会越来越难,这个项目就是为了解决这个问题,直接把对应功能集成在浏览器上,点击一下就能完成我曾经需要手动复杂操作的流程。
例如我的赛博手办展示页,需要不断的增加新手办的图片和资料,按照以往只能使用 FTP 手动上传并维护 JSON 数据,而现在只需要打开 B 站对应的购买页面,就可以自动填写信息,提交表单后也同时完成了手办图片的上传。
以及追番进度的保存,原先只能在后台手动填写编辑,现在虽说还是没能做到完全自动化,需要进入对应番剧的详情页面后手动点击按钮完成同步,但也比原先要更快捷了!
还有一些方便我写内容的实用功能,例如复制当前网页链接,以 Markdown 格式复制到剪贴板,都是浏览器没有内置,但对于我而言使用频繁的琐碎需求。
因为这个插件而给网站新开发的「在看」功能,倒是没有特别多的突破和改进。我主要在 7 月份增加了快照功能,用于保存网站的文章内容,以对抗国内动不动 404 的特色。想要抓取的更精准,只能根据不同的网站做一些适配,或者后期直接允许选择内容。
或许是我这几年没做出过太惊艳的东西,线上社交平台方面的进步不是特别大,而且编程方面的技术提升也不算太高(不像隔壁 @Innei 已经是 NextJS 人气博主了)。下半年的社交活动都主要在线下了,通过漫展和摄影,认识了不少新的朋友,在后面我也会单独详细介绍。
人生就像一列有去无回的列车,中途陆陆续续有人上车,也有人下车。你会遇到形形色色的人,有人只是擦肩而过,有人会短暂停留,有人陪你看沿途的风景,很少有人陪你到一直到终点!但是值得庆幸的是一路上都有人上车陪你。
这句话我挺认同的,有时候一段关系就慢慢的变得很难维持了。有位好兄弟结婚了之后,可能圈子变了吧,就几乎没有主动和我有过联系了,而我也不知道如何再继续和他保持联系。
看过我此前写的博客/日记的朋友应该都知道我比较腼腆、社恐,且圈子小(同学/同事/社团/网站博主/同行)没有多少异性朋友。自从上班之后就已经被工作困住,几乎没有什么别的认识人的机会。这样下去怎么可能会遇到一个能互相看对眼的人呢?
我决定扩展交际圈,并尝试着克服社恐。我的群友给我提出了一些想法,去年 11 月(应该是),他们首次提到了去漫展这个话题。
我了解到本地比较有知名度的 AS 漫展,加了企鹅群一段时间,也在观察着他们平时讨论的内容,找机会与他们互动。刚开始的时候其实挺困难的,群里都是陌生面孔,发“找搭子”也没人理会,加群之后开了几次展子我都没有去。
今年 6 月,再三打听后我注意到一个免费展,在约不到朋友的情况下,我还是选择独自一人去看了看。
在现场我顿时觉得非常的尴尬,没有一个认识的人,也感觉自己和他们完全不在一个频道。我在群里尝试捞人混个脸熟,在大哥 @Hydra 的激励下,我总算得到了第一张集邮的照片,我第一次这么主动找人拍照,表现得特别腼腆(表情很猥琐),但这次来能拍到就不亏了!

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


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

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

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




扩列之后就有机会看到各位老师的各种 COS 照,乃至一些日常,就有了和他们互动的机会。或许什么时候就有话题闲聊开始做朋友了呢?我认为单纯去找他们集邮碰碰面可能还不太会让他们对你有比较深刻的印象(毕竟是纯路人嘛)不如选择一个自己在漫展中的新身份,常见的要么就是 Coser,要么就是摄影了。
出 COS 需要会的技术还不少,买一件 C 服穿着还仅仅只是自娱自乐呢!还需要化妆和准备道具之后,才算是个基本合格的 Coser,能模仿出其性格体态的话就更还原。或许摄影会比较适合我,自己平时就比较习惯用手机拍照记录生活(不过基本是风光和各种日常),为什么不考虑买一台相机,扩展风光摄影的同时,来尝试下人像摄影呢?
8 月中旬有一次 TC 展,结果我在功课做得不多的情况下,急急忙忙就买了一台我认为价位能接受的索尼 A6700,想着能借此机会来学习相机的使用。这次主要发现了套机镜头 18135 光圈较小上的缺点,需要拉长焦距才能有更好的虚化效果。在场内灯光环境一般的情况下,某些时候并不能拍到较为满意的效果。
10 月初的国庆节也有一次漫展。这次我做足功课准备了新的大光圈镜头,拍摄的虚化效果非常好,但发现买的“豆腐块”常亮补光灯在这里几乎没有任何作用,我也对这次的拍摄做了一个简单的总结。这次漫展的集邮就特别少了,几乎都单独给他们拍照了。
这次认识了 @巧乐兹 和 @淮南枝 老师,给她们拍了互勉场照(不过问题还是出在灯光,会显得很黑),@灵音 和 @江晚 老师也在场,既然是列表了我当然是要找她们拍的。
11 月初,在前面几次展子的失败经历之后,我终于下定决心买了一盏机顶闪光灯,是神牛的 V860,直接一步到位。并且在前一天的万圣节之夜去了富华里练习使用,虽说参数的调教还有些问题,但至少拍出曝光正常能看的照片了。
这次付费展的照片相较于此前的可以说是史诗级的提升了!在内场出申鹤的 @小希 老师说今天一个给她拍的摄影都没有,@Hydra 大哥说我这一上去情绪价值不是瞬间就拉满了。在一旁出提纳里的老师是她亲友,给她充分的指导了动作,她问我借了相机给她拍两张,Coser 老师拿相机拍照的样子感觉就非常有意思。



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



11 月末的珠海第一届 SN 魂展,主办方估计是想打造成“特摄”主题的,但是市场调研不足,导致出现会展中心这样的大场地却没几个人来玩这样的尴尬情况,我甚至觉得用“摄影比 Coser 多”来形容都好不夸张。
@灵音 老师邀请我和她一起逛展,她作为我的主要拍摄对象。逛展期间发生了令人尴尬的事,也有开心的事。我第一次找 Coser 老师单独拍照被拒了,但是品尝到了 @灵音 老师的手工自制小蛋糕,非常好吃。这次闪灯也比之前稍微熟练了一些(但也有过曝的照片,但 RAW 拉回来还是绰绰有余),漫展结束后跟着她认识的摄影一起在场外拍外景,多好的学习机会。拍摄结束后一起在富华里搓了一顿饭,认识了 @柠檬茶、@阿卓、@小安 几位很厉害的摄影老师,以及出小樱的 @卿安 老师。

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

这次展遇到了 Coser @理如 和 @阿卓 摄影老师,他买了 VIP 提前摆好了灯阵,我借他的灯阵也跟着拍了一些 Coser 老师,其中认识了出星期日的 @善信 老师。我们那天晚上漫展结束之后还去吃了顿烧烤。
说起来当初加这个 @理如 老师的过程也是很好笑的,我“视奸”了她空间很多次,她居然来主动加我。上次在中山影视城漫展拍炸了的那个 Coser 老师其实是她,好在这次借了 @阿卓 摄影老师给她拍了一张效果还行。
1 月买了引闪器,首次在春节前的中山 AS 展使用,借了 @杰哥 摄影老师的灯阵来玩了一下,不过效果比较平,不太能看出来是用了几盏灯的样子… 还需要继续努力~
今年去了最多次的城市是深圳,七月份跟着亲戚的车去 Citywalk 了一次,逛了市区一些大商场,和同学 @BB 见了面,吃了一顿日式汉堡肉。十月份去了沙头角、大梅沙和深圳人才公园。
十一月份我自己坐大巴去了两趟,只是为了修我的 MacBook Air,当然和 @MJ 去了深圳的二次元胜地逛玩,还是不亏的。十二月年末也去了一趟,和 @MJ、@凯文 两人一起吃饭聊天和闲逛,途中还体验了深圳刚开通的地铁 13 号线。




这几次在深圳的经历给我的最大感受就是吃的品种特别丰富(但大商场里面价格也蛮贵的),或许是因为大城市总人口多,外来人口也比较多。
其次是澳门,办理了新版的卡式港澳通行证之后不去转转就没意思了。五月是时隔多年(2009)后第一次去澳门玩,和爸妈一起过去,主要在澳门半岛转悠。去了大三巴、大炮台、金莲花广场等著名景点,品尝了小吃街的牛杂,也进了赌场参观表演。
九月第二次去,主要是氹仔那边的酒店为主,首先去了威尼斯人酒店闲逛,去了官也街品尝美食,去伦敦人、巴黎人等酒店拍照打卡,去美高梅蹭了杯招牌奶茶。最后在澳门教科文中心参观画展,见证了我大舅的书画作品被澳门基金会收藏的过程。
其他城市就只有 1-2 次。二月春节期间去了广州番禺的一些小众景点拍照打卡,当天返程还去了中山华侨公园和紫马岭公园,这里的一些现代建筑非常出片。八月份去了香港 Citywalk,主要在九龙和湾仔区,去体验了富士相机和索尼的镜头,在星光大道和明星的手印合影,和 @凯文 见了面并一起吃饭聊天。
这里图一直没弄,先鸽着吧==
2 月,我打算购置一个新的蓝牙音箱,一是提高平时听歌的音质,二是方便携带,这款音箱的性价比我认为还不错。和我表哥的 Bose 音箱一起听几乎没什么差异,而他那款比我的更贵。
3 月,我原先使用长达 7 年的戴尔,开机时出现了闪烁的横线,看上去是面板出现了问题。再三考虑下还是决定买个 4K 高清的,而不是高刷玩游戏的。看了评测视频后,认为红米的这一款性价比不错,甚至比它自家小米品牌的还要好…
这台显示器配合 MacBook 的体验确实不错,在 Windows 下玩游戏,原先 1080P 的画面必须拉到 2K 才能显示成此前小窗的尺寸,显卡的功耗也上去了,不过我现在玩游戏的时间其实也没有之前那么多了…
8 月,我购入了人生第一台相机,索尼 A6700,是在我能承受价格范围之内最好的机型。虽说 A7C2 会更好,但是镜头方面肯定是要比半画幅更贵一些,它裸机的价格就能买到 A6700 带 18135 套头了。
别人都在说,这个价格你为什么不买全画幅。这里还要考虑到我当时的使用场景,因为拿到相机几天之后我就带去漫展拍照学习使用了,当时也是急急忙忙的做了选择。
不过现在看来,我的确可以再花更长的时间先买一台 A7C2,再认真挑选镜头。不过预算也会增加 1-3K 吧。机器开箱之后就掉价了,现在再卖掉并不划算。
A6700 实际上也很不错,并没有让我失望。起初拍的不好其实是补光问题,带上闪光灯效果马上就不一样了。相较于部分老相机来说,这台新机器对焦迅速的优势也显现了出来,它更容易拍到正常对焦的照片。并且还能直接录制 120 帧的 4K 视频,后期还可以考虑买个云台拍点小视频了。
既然相机都买了,大光圈镜头总要配一个吧,闪光灯也得来一个吧,蹭别人的灯总要个引闪器吧,要是自己单打独斗是不是也要有个柔光箱?摄影,就是用光的艺术!把钱花光的艺术!
11 月末,我去年购入的 MacBook Air 居然也送修了!在我手上的 MacBook 用一台就坏一台?不过还在质保期内,免费维修,就是苦了我去了两趟深圳,不过还好期间和 @MJ 去了深圳一些不错的地方,也算是 City Walk 了一波。
12 月初,我再三考虑下还是选择购入了 iPad Air 6,M2 芯片 256G 的版本,相较于新出的 Mini 7,我还是觉得它的性能会更耐用一些。它的尺寸 10 寸确实更大没那么便携,实际用多了也接受了,回看 Mini 怎么这么小!
这个尺寸拍完照片立马就能打开看效果,甚至可以直接修图,卷死其他摄影(不是)。不过 Creator App 并不支持通过软件删除照片,这点还是有些不太方便。(结果实际几次拍摄都没现场修过)
今年主要看了这几部番剧,《间谍过家家》、《甘城光辉游乐园》和《亚托莉》,都是日常系。
《间谍过家家》就不用多说了吧,其实是因为之前只看了 1-2 集后鸽了,今年直接从第一季补番到第二季了。还去电影院看了剧场版,这个剧场版的剧情可以说是把“浮夸”这个词发挥到极致了,非常建议一看!
《甘城光辉游乐园》是游乐园经营主题的一个番,我曾经是非常喜欢玩《过山车大亨:3》的玩家,因此对这个题材的作品有着不错的好奇心。
《亚托莉》其实是因为喜欢她这个可爱的角色形象,GalGame 没花精力推看一下这个我觉得也不错,虽说看评论说这部番有改写过一部分情节。
截至写下这行文字的这一天,我还看了一集《魔女之旅》,我觉得接下来应该再补一下《孤独摇滚》、《莉可丽丝》和《葬送的芙丽莲》,都是此前很热门的番剧。
自去年主动勾搭过一次女生之后,就再也没有第二次了。一方面我认为找群友做军师的过程过于超纲了,拿捏不住他给我设定的身份。再一个是在父母与其他人无形对自己施加了压力的情况下,我很难摆脱“一定要让她成为我女朋友”的这种观念(即便自己或许只是想做个朋友而已吧)。这件事让我 Emo 了一晚彻夜无眠,令我至今历历在目。
今年家里人并没有安排和介绍过任何人,有相亲活动我也没参加过。
但我还是决定以朋友相处的方式多和女生交流互动,比较松弛有度。可眼看自己年纪也是越来越大了,还有多少时间让我有试错的机会呢?假如真的有人对自己感兴趣了,又该如何识别出她的小心思呢,她是否对其他人也这样呢,这对我来说确实是一个很伤脑筋的问题啊。
在这一年,我在漫展上结交了很多新朋友,总算是在前几年的短板上首次得到了突破,或许能让我没那么社恐了。通过摄影也极大的增加了我想出门走的意愿,但以此同时也减少了当宅男玩游戏、看番剧和写代码的时间。
年末公司开始 996 加班后,发扬爱好的时间进一步被缩短,编码能力的提升更多只能通过公司项目来实现,我应该尽可能的抓住一些机会。个人网站方面还需要更进一步的思考如何保证质量的同时,借助编程和 AI 的力量尽可能的提高维护效率(例如照片的处理等等)。作息方面依旧没有得到改善,除了保证充足的睡眠外,还需要提高一些时间使用效率。
玩摄影并不一定能赚钱,学来有啥意义?
摄影本身确实没有什么意义,意义都是自己赋予的不是么。记录当下,在未来能够重新回忆起来,就是摄影本身的意义吧。况且学会摄影也可以给未来的对象拍呀,怎么想都是好处大于坏处吧。游戏帐号什么的不一定自己能持续留住,但是写文,摄影,做项目的成绩绝对是持续积累的。
给对象拍?那你找到对象了吗?有进展么?
依旧没有,凭实力单身了,在漫展上也不是所有摄影都有女朋友啊(更何况还有一些是先有对象才学摄影的不是么),我目前还比较享受摄影过程的,相较于期待找个对象,不如先期待来一个固模吧...
现在在摄影和编程技术方面,你要如何权衡?
现在 996 工作时间比较长,只能在工作方面多花心思看看能不能提升点啥了,AI 其实也是个很好的自学工具,前提是你能很好的调教它。长期在电脑面前工作也比较容易导致职业病(久坐、长时间看屏幕等),有时间休息还是尽量出门走走,让眼睛也好好休息下,这点时间就尽量安排给摄影活动了,无论是拍风光还是人像我觉得都好。
去年和前年并没有立下任何 Flag,主要是总结的拖延非常严重,已经没有写的必要了好吧。现在回过头来发现就算写了似乎都挺难完成的,好在今年看上去似乎有了那么一丁点转机,看看能不能努力一下吧。
[] 学习一个后端框架,能挺好快速出活的那种
[] 继续学习提升摄影技术,按需购买新设备
[] 看下能否将主页相册遗漏的照片都补全了
[] 争取找到第一任女朋友?
@Innei:2024 · 前路未尽,初心犹在
这一年,从乌镇跨年开始,经历了公司团建去大理的愉快旅程,但之后突如其来的裁员让人陷入抑郁,最终加入了 RSS3,参与了 Follow 项目的开发,体验到了开源工作的成就感。
在游戏方面,通关了《黑神话:悟空》和《最后生还者》,感受颇深。尽管只去了南京旅行,但年底买了特斯拉 Model 3,技术上写了多篇技术文章,对技术的理解更深入。虽然对未来抱有悲观态度,但仍希望明年会更好。
@林陌青川:野花做了一场玫瑰梦
这是一篇深情的 2024 年终回忆录。博主林陌青川记录了与“汪小姐”从网络技术群相识,到各自在互联网大厂(携程、字节、B站)实习奋斗,最终在上海奔现的故事。
文章回顾了两人从错过的遗憾到低谷期的相互陪伴,以及见面时同游静安寺与外滩的心动瞬间。尽管博主自比“野花”仰望优秀的她,感叹现实差距,但他仍视这段缘分为人生的“上上签”,以此文致敬这段美好的相遇。
2025-08-31 21:20:00
近期开始回忆杀状态,在 Steam 上购买并游玩了一些以前玩过的经典小游戏,其中包括来自 PopCap 宝开出品的《吞食鱼:2》。
可能人老了吧,新的游戏都玩不动了,要么烧脑要么太肝,还是这种休闲小游戏玩法简单又上头!
当时这部作品也有人将它翻译成《大鱼吃小鱼》,其中“吞食鱼”这个说法来源自《幻想游戏》系列。它也推出过很多“不同版本”的续集(其实就是一个整合包),汇聚了很多来自不同厂商的休闲游戏。

游戏玩到了,BGM 也在反复洗脑了,那么如何将游戏里面的音乐提取出来继续听呢。了解博主过去黑历史的朋友应该知道,当时老喜欢用各种方式去提取各种游戏的资源素材了,其中《摩尔庄园》、《疯狂农场》等游戏都是我的常顾对象,收集各种游戏的 BGM 并且偶尔拿出来播放算是我的一个小众爱好。
我找到了当年的一些素材提取工具(文件的修改日期为 2015 年,实际可能更早),其中有一款工具叫做 XMPlay,它可以用于 MO3 音乐的播放和提取。
MO3 是一种音频文件格式,主要用于模块化音乐,类似于其他 MOD 格式(如 IT、XM、S3M 等)。它由 Ian Luck 为 BASSMOD 引擎开发,MO3 文件的一个显著特点是它使用 MP3 或 Ogg Vorbis 格式来编码音频样本,这使得文件体积相对较小,同时能够保持较高的音质
对于模块化音乐,我的理解就是它将每一个音乐节拍的声音都作为一个片段存储,播放过程则是将所有片段循环复用,有点类似于 MID 音频格式,但是 MID 格式外放出来的音色受操作系统、播放软件等的影响较大
时隔已久,我甚至都忘记了当年是怎么使用它提取音乐了,一番研究后总算是找到了办法。
顺带一提,我 Google 之后得到的结果往往都是要你付费购买某些软件,几乎没什么中文文章提到过这些东西,看来确实有些小众啊,那么我就水一篇文章简单记录一下
首先从 un4seen 下载并打开 XMPlay 播放器,这里我使用的旧版本,界面稍微比较复古。

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

此时点击音乐播放按钮,就会弹出对应的另存为界面,依次确认导出,所有的音乐都会导出成功。
如果导出的音乐存在部分内容丢失或中断的情况,可能是因为你在播放器已经开始播放了,点击“停止播放”按钮再试一次即可
目前博主已知使用 MO3 格式作为背景音乐解决方案的游戏包括:
理论上多数同期游戏都采用了这个方案,性价比特别高,比存储完整的 MP3、OGG 等格式都小,保真度还高。可惜从宝开《植物大战僵尸》等作品开始,都采用了 PAK 打包普通的音乐格式,此方案不再流行,也算是时代的眼泪了吧!
2025-06-09 11:52:00
最近同事分享了一篇文章:《该写好代码吗?我也迷茫了。写的代码好被替代,写的代码差到处救火》,引发了我的思考。我问了问他的看法,是写好还是不写好呢。
答案可以说是意料之中,“正常写就好”。他觉得正常写也会有 Bug,代码也不会很糟糕。确实是这样,因为即便你当时认为写好了,这需求变化速度实在太快,后续很有可能就不能满足需求了,就产生了所谓的 Bug。
这让我很自然的联想到我现在在公司做的 Felo 搜索 项目,确切来说它现在的定位已经渐渐不是一个纯粹的搜索网站了,而是各种 AI 工具的混合体,主要竞争对手是 Perplexity、Genspark、天工 等。
前端方面我的评价是基本上已经乱的一锅粥,原先的逻辑很多不能满足现在的需求,就是一个超级缝合怪,既要 Perplexity 的那种搜索总结功能,又要 Genspark 的 Agent 型对话交互模式... 两种完全定位不同的东西被强行融合到了一起。但是换个角度来说,大公司做的东西不也是堆屎山么,不见得有多好,除非推倒重新开发。我们现在 996,就是飞快的加功能,老大天天都想发布新版本,怎么可能愿意干这种事?
最近我们在做的 AI 生成 PPT 功能也差不多是这样子,越演越烈,产品交互方面并不统一,此前 PPT 是一个独立页面,任意入口点击后打开新窗口生成,之后是独立一套交互流程。而现在为了兼容“PPT Agent”模式(单独一个工具页,通过上传文档什么的触发创建一条记录),强行让其他入口套用它的逻辑。作为对比,和它入口旁边的同级功能,思维导图、智能图形,目前都不会单独创建新的记录(虽然它们也各自有一个 Agent 模式)。
原先交互:打开一条搜索记录,找到生成的 PPT,打开独立页面
现在交互:上述入口保留,生成的 PPT 会产生单独一条搜索记录,生成成功后则点击直接打开独立页面,并且作为 PPT 其入口还不在“文档库”里面,很迷
后续需求还说得兼容之前搜索的逻辑,把原先那套模式给搬到新的 UI 上面,主打一个“抄谁不像谁”,代码逻辑的耦合性实在太强,很难想象之后要产生多少 Bug... 只能感慨地说一声,现在它已经彻头彻尾变成了“Felopark”了,这样的架构,代码也不太可能能写得多好了。
所以回到最前面的话题,写代码正常发挥即可。至于可替代性什么的,只要你的能力不差,去哪里都会发光发亮不是么。
2024-12-21 01:29:00
我在公司维护的 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 内容,直接修改了对应的版本号和签名,亲测可用。
2024-10-12 11:28:00
前段时间我购买了几年的 CloudCone 特价机炸了,导致服务中断快一个星期,再加上没有定期做备份,服务无法快速恢复。我开始寻找其他服务商的机器,兜兜转转最终选择了搬瓦工的,因为看它比较老牌,口碑还不错,且国内直连的线路良好,在此发出一个 我的邀请码,你可以通过这个链接购买服务器,为博主提供微薄返利以资助网站的持续运营。
另外小扯一句,你可能会注意到我博客的更新频率有所下降。但其实是因为我以此同时还在维护另外一个名为 保罗的小窝 的网站。个人对于博客的定义是稍微正式的内容,而这个小窝更多的是分享随笔和流水账,各位可以自行选择订阅。
此前一直都在用 OneInstack 脚本来部署环境,尝试用它重新部署,却发现 MariaDB 无法正常安装。国内国外访问同一个包的下载地址居然行为是不一样的(国外出现 404)。我选择构建安装,时间太长太复杂,且选择 Caddy 作为 Web 服务器后,居然虚拟主机都没有创建成功,我怀疑脚本是不是根本就没做好 Caddy 的适配...
OneInstack 此前也爆过供应链挂马的问题,据说是被国内公司收购了之后出现,虽然作者承诺修复,但我仍对其安全性存在质疑。再三抉择后,只能忍痛放弃 OneInstack,打算从零开始学习部署属于自己的 Ubuntu 服务器环境了!
你需要一定的 Linux 使用基础才能更好的阅读本文,实际安装过程可能有些许曲折,本文对部分流程做了次序优化,可能存在欠缺需要自行分析和解决(例如某些软件包需要自行安装),虽然有一部分在编写过程中在虚拟机上重新执行确认过,但还是以实际操作为准吧!
选择自己纯手工配置环境最主要的原因就是 PHP,它除了一个 FPM 服务以外,还需要一个 Web 服务器才能将项目跑起来,它并不能像 NodeJS 那样自己就是一个 HTTP 服务。再加上数据库服务放在相对实际的环境下可以更好的编写脚本实现一键备份等功能。使用 K8S / Docker 部署应用会更方便,但受限于成本、服务器数量和硬件配置等因素,自己纯手工配置环境依旧是首选方案。
PHP 8.3
NodeJS
在使用 Caddy 之前我都是选择 Nginx 作为 Web 服务器,Nginx 技术成熟但配置起来没有 Caddy 那么容易,Caddy 作为后起之秀其评价也还不错,它还自带 SSL 证书签发功能,而且不会出现因 HTTPS 证书导致的 IP 泄漏问题。此前也使用过它 部署环境 有些许经验,这次直接使用它作为生产环境我认为也是没有问题的!
MariaDB 是开源版本的 MySQL,此前保罗一直在用,因此就没有考虑 MySQL。
PHP 版本此前都是 7x,最新版本都是 8x,如果你要使用 PHP 的包管理器 Composer 管理项目,那么直接用 PHP 8 是最省时的选择(如果要用 7x,你还需要旧版本的 Composer,太麻烦了,我一个新手根本不会弄,还不如升级自己的代码)
phpMyAdmin 是一款运行在 PHP 下的老牌数据库管理软件,我们首先安装它也能确认 PHP 是否能正常与数据库进行连接。
NodeJS 也是我现在使用 Nuxt (Vue) 和 Remix (React) 框架构建应用所必备的,这里我们使用 FNM 安装,并配置 PM2 用于持续化启动网站。
在开始之前我也推荐安装一些实用软件,你可以根据自己的需求选择安装。
安装 htop 以实时查看服务器配置:
sudo apt install vim htop fastfetch
安装 net-tools 以查看网络相关信息
sudo apt install net-tools
安装 trash-cli,防止文件直接删除,无法被恢复:
sudo apt install trash-cli
vim ~/.bash_aliases
alias rm='trash-put'
安装 ufw 以控制服务器的入站出站流量,选择性打开服务端口,防止被外部 IP 扫描减少安全风险。如果你的云服务商支持在后台自定义设置防火墙规则(例如阿里云),那么可以选择不安装。
sudo apt update
sudo apt install ufw
# 设置默认策略为拒绝所有传入连接
sudo ufw default deny incoming
# 设置默认允许所有传出连接
sudo ufw default allow outgoing
# 允许 TCP 端口 6755
sudo ufw allow 6755/tcp
# 检查状态
sudo ufw status verbose
# 启用 UFW
sudo ufw enable
安装 openssh-server 以允许远程操控服务器。如果是服务商提供的 Ubuntu,可能已经默认安装,直接跳过。
sudo apt install openssh-server
编辑配置文件,修改端口,禁用密码访问,改为使用公私钥形式访问服务器。
sudo vim /etc/ssh/sshd_config
Port 5678
PubkeyAuthentication yes
PasswordAuthentication no
编辑文件 authorized_keys 添加私钥,以后将会用主机的公钥与服务器中的私钥配对连接。如果没有密钥对可以使用 ssh-keygen 生成一个,这里建议使用 ssh-keygen -t ed25519 指定算法生成(因为 Mac 那边新版本已经不支持默认的 RSA 了)
vim ~/.ssh/authorized_keys
重启服务器的 sshd 服务
sudo systemctl restart sshd
(可选)配置主机的 config 文件,指定使用证书命中到服务器。
Host MyServer
Port 5678
User root
Hostname 10.7.9.103
PreferredAuthentications publickey
IdentityFile ~/.ssh/myserver_ed25519
参考 官网说明 安装即可。
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Caddy 已经完成安装并自动启动。如果你选择安装了 ufw 防火墙,允许外部访问 80 443 端口:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
新建站点目录
mkdir /var/www
mkdir /var/www/html
此时我们还不需要配置任何网站,你可以使用 wget 或在浏览器上访问服务器对应的 IP,确认 Caddy 已经正常启动。
sudo apt install mariadb-server
sudo mysql_secure_installation
注意这里
Enter current password for root第一次要求输入密码直接跳过,到后续第二次询问Change the root password?再设置。至于它为什么会问两次密码,我也不是很清楚。我尝试过只在第一次要求的时候输入,但在第二次询问时跳过,这样做会导致最后什么密码都无法登录...
NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!
In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
haven't set the root password yet, you should just press enter here.
Enter current password for root (enter for none):
OK, successfully used password, moving on...
Setting the root password or using the unix_socket ensures that nobody
can log into the MariaDB root user without the proper authorisation.
You already have your root account protected, so you can safely answer 'n'.
Switch to unix_socket authentication [Y/n] n
... skipping.
You already have your root account protected, so you can safely answer 'n'.
Change the root password? [Y/n] n
... skipping.
By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them. This is intended only for testing, and to make the installation
go a bit smoother. You should remove them before moving into a
production environment.
Remove anonymous users? [Y/n] y
... Success!
Normally, root should only be allowed to connect from 'localhost'. This
ensures that someone cannot guess at the root password from the network.
Disallow root login remotely? [Y/n] y
... Success!
By default, MariaDB comes with a database named 'test' that anyone can
access. This is also intended only for testing, and should be removed
before moving into a production environment.
Remove test database and access to it? [Y/n] y
- Dropping test database...
... Success!
- Removing privileges on test database...
... Success!
Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.
Reload privilege tables now? [Y/n] y
... Success!
Cleaning up...
All done! If you've completed all of the above steps, your MariaDB
installation should now be secure.
Thanks for using MariaDB!
输入一条命令完成安装:
sudo apt install redis-server -y
在安装之前,我们还需要新增一个 Ubuntu 软件源:
sudo add-apt-repository ppa:ondrej/php
sudo apt update
安装 php、composer 及其对应的扩展(你可以根据自己的需要来安装):
sudo apt install php8.3 php8.3-curl php8.3-fpm php8.3-mysql php8.3-redis php8.3-mbstring php8.3-xml
sudo apt install composer
⚠️ 注意:使用apt安装部分 PHP 扩展时可能会导致安装apache2依赖项,后续需要将其禁用,可能使用apt来安装并不是一个最佳方案
编辑 php.ini 配置文件,取消生产环境隐藏报错的设置,好让我们接下来更好的排查问题,后期可视情况还原。
vim /etc/php/8.3/fpm/php.ini
error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING
display_errors = On
通过命令 service php8.3-fpm status 查看当前 php8.3-fpm 服务配置文件的路径。
include=/etc/php/8.3/fpm/pool.d/*.conf
可以看到引入了一个 pool 的配置文件,ls 列出当前文件夹下所有文件。
ls /etc/php/8.3/fpm/pool.d/
www.conf
使用 vim /etc/php/8.3/fpm/pool.d/www.conf 查看配置文件,即可看到 sock 的地址。
listen = /run/php/php8.3-fpm.sock
复制下来,后续我们需要将路径粘贴在 Caddy 的配置里面,后续粘贴到 Caddy 的字符串如下:
unix//run/php/php8.3-fpm.sock
首先创建所需的文件夹:
sudo mkdir /var/www/
sudo mkdir /var/www/html
sudo mkdir /var/www/html/default
还记得前面提到的 PHP Socks 路径嘛,这里将会用到,首先开始编辑 Caddyfile 配置文件:
vim /etc/caddy/Caddyfile
新增一条虚拟主机记录,由于访问地址是服务器 IP 自身,无法签 SSL 证书,因此直接选择自签即可。
10.7.9.103 {
tls internal
root * /var/www/html/default
php_fastcgi unix//run/php/php8.3-fpm.sock
file_server
}
重启服务,理论上没有任何问题。
service caddy restart
我们可以创建一个 index.php 文件,测试是否可以正常连接到 PHP:
vim /var/www/html/default/index.php
<?php
phpinfo();
使用浏览器访问链接 https://10.7.9.103,理论上将会显示当前 PHP 的运行信息。
从 官网 上下载最新版本,之后解压在 /var/www/html/default/phpMyAdmin 目录下。
官网上的版本已经内置了所需要的 Composer 依赖,我尝试过本地自行安装,但遇到了错误,原因不明,也不想花心思继续排查了,只要自己项目正常就行。
使用浏览器访问链接 https://10.7.9.103/phpMyAdmin,输入账号密码,你可能会遇到如下错误:
mysqli::real_connect(): (HY000/1698): Access denied for user 'root'@'localhost'`
此时主要检查两种情况,一个是前面密码的设置问题(可以用 mariadb 命令尝试登录),另外一个则可能是 root 账号所对应的 Host 的问题。
截至本文编写过程在虚拟机里重新执行确认时,并未出现这个错误,就挺奇怪的,但还是提供一下之前的解决方案
使用 mariadb -u root -p 命令登录,输入下面的命令,这里参考了 OneInstack 的源码。
# xxxx 是你的密码,可以和之前设置的一致
GRANT ALL PRIVILEGES ON *.* TO root@'localhost' IDENTIFIED BY "xxxx" WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON *.* TO root@'127.0.0.1' IDENTIFIED BY "xxxx" WITH GRANT OPTION;
FLUSH PRIVILEGES;
再次尝试,应该就能正常登录了。
我注册了 CloudFlare R2 存储桶来完成服务器的备份数据存储,实测国内的银联信用卡也可以使用。可以在服务器上使用命令行工具 rclone 来将文件存储至 R2 存储桶,后期整理一个备份脚本定期执行即可。
https://developers.cloudflare.com/r2/examples/rclone/
Ensure you are running rclone v1.59 or greater (rclone downloads ↗). Versions prior to v1.59 may return HTTP 401: Unauthorized errors
CloudFlare 文档描述对 rclone 版本有要求,apt 下的似乎比较老,因此直接根据 官网教程 直接安装最新版本(截至本文编写过程,最新版本是 v1.68.0)
sudo -v ; curl https://rclone.org/install.sh | sudo bash
先在 CloudFlare 后台新建 R2 存储桶,并创建一个 API 令牌以供 rclone 程序进行连接。你将会得到以下几个数据:
在编写自动化脚本之前,需要添加对应的存储桶配置,后期可直接使用对应的命令和配置上传文件。
rclone config
No remotes found, make a new one?
n) New remote
s) Set configuration password
q) Quit config
n/s/q> n
Enter name for new remote.
name> cloudflare
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
4 (Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, GCS, HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Minio, Netease, Petabox, RackCorp, Rclone, Scaleway, SeaweedFS, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu and others) -> 6 (Cloudflare R2 Storage)
Option provider.
Choose your S3 provider.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
6 / Cloudflare R2 Storage
\ (Cloudflare)
Option env_auth.
Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
Only applies if access_key_id and secret_access_key is blank.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).
1 / Enter AWS credentials in the next step.
\ (false)
2 / Get AWS credentials from the environment (env vars or IAM).
\ (true)
回车继续,输入 access_key_id 和 secret_access_key
Option access_key_id.
AWS Access Key ID.
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
access_key_id>
Option secret_access_key.
AWS Secret Access Key (password).
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
secret_access_key>
Option region.
Region to connect to.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
1 / R2 buckets are automatically distributed across Cloudflare's data centers for low latency.
\ (auto)
region> 1
Option endpoint.
Endpoint for S3 API.
Required when using an S3 clone.
Enter a value. Press Enter to leave empty.
endpoint>
Edit advanced config?
y) Yes
n) No (default)
y/n> n
Configuration complete.
Options:
- type: s3
- provider: Cloudflare
- access_key_id:
- secret_access_key:
- region: auto
- endpoint:
Keep this "cloudflare" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> y
配置完成后,可使用命令列出存储桶的数据和上传文件,确保连接正常。
rclone tree cloudflare:存储桶名称
理论上能够正常列出存储桶下的内容,如果不能正常显示,可以从下面几个方向排查:
打包生成备份的过程在这里不作详细介绍,大致就是使用 mysqldump 导出数据库,之后使用 tar 打包压缩站点数据,最后使用 rclone copy 命令将所有文件上传,定期手动或自动化执行都是可以的。
cd ~/backup/2024-10-21/
mysqldump -u root -p home > home.sql
rclone copy . cloudflare:存储桶名称/2024-10-21/
在 官网 上选择对应的环境,即可生成一键安装脚本,我选择了当前最新的 LTS 版本和 fnm,你可根据自己的需要安装对应的版本。
# installs fnm (Fast Node Manager)
curl -fsSL https://fnm.vercel.app/install | bash
# activate fnm
source ~/.bashrc
# download and install Node.js
fnm use --install-if-missing 20
# verifies the right Node.js version is in the environment
node -v # should print `v20.17.0`
# verifies the right npm version is in the environment
npm -v # should print `10.8.2`
接着来安装 PM2,这是项目持久化运行在服务器上所必备的。
pm2 startup
pm2 start ./ecosystem.config.cjs(应用程序的示例,以实际为准)
pm2 save
这样服务器重启后,也能自动启动 pm2 服务以及对应的应用程序。
配置一个 NodeJS 项目到 Caddy 非常简单,基本上就只是一个端口转发规则而已。
paul.ren {
reverse_proxy localhost:3001
# 添加以下配置以处理 /upload 路径
handle /upload/* {
root * /var/www/html/legacy.paul.ren/public
file_server
}
handle /static/* {
root * /var/www/html/legacy.paul.ren/public
file_server
}
}
可以编辑 ~/.bash_aliases 文件自定义自己的快捷命令,快速定位到 Caddyfile 配置等。
感谢 @提莫 同学对本文中一些内容的指正,由于本人的运维经验不足,虽然已经花了不少时间重试其中的某些步骤,但仍然可能有所不足和缺漏,有什么问题可以在下方留言,我会尽力为大家解答。
2024-07-05 14:35:00
近期要把公司的新官网项目给收尾了,准备打包部署发布到线上环境,我们主要采用的 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
standalonefolder that copies only the necessary files for a production deployment including select files innode_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,可以看到构建、运行应用的过程均在里面完成(并非阶段构建),也因此导致最后的镜像略大。
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"]
考虑到我司已经在使用 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 + SSH + PM2 的部署方式,也是一样轻松了不少,以往需要在运行机器上执行极其缓慢的 pnpm build 也将提前在 Jenkins 机器上完成。通过 SCP 的方式传输构建产物,到运行机器上只需替换掉对应的资源,重启 PM2 就能完成,这里就不再具体提供实现过程了,有需要建议自行尝试摸索。