豆包手机为什么会被其他厂商抵制?它的工作原理是什么?
之所以会想写这个,首先是因为在知乎收到了这个推荐的问题,实际上不管是 AutoGLM 还是豆包 AI 手机,会在这个阶段被第三方厂商抵制并不奇怪,比如微信和淘宝一直以来都很抵制这种外部自动化操作,而非这次中兴的 AI 豆包手机出来才抵制,毕竟以前搞过微信自动化客服应该都知道,一不小心就会被封号。

另外也是刚好看到, B 站的 UP 主老戴深入分析了豆包手机的内部工作机制的视频,视频介绍了从 AI 助手如何读取屏幕、捕捉数据和模拟操作的真实流程,所以对于 AI 手机又有了个更深刻的认知,在这个基础上,更不难理解为什么 AI 手机这种自动化 Agent 会被第三方厂商抵制,推荐大家看原视频:b23.tv/pftlDX8 。

那么豆包的 AI 手机是怎么工作的呢?实际上和大家想的可能不一样,它并没有使用无障碍服务(Accessibility Service),而是使用了更底层的实现方案:
豆包手机利用底层的系统权限,直接从 GPU 缓冲区获取原始图像数据并注入输入事件,而非依赖截屏或无障碍服务,此外手机还在一个独立的虚拟屏幕中执行后台任务,并将图像低频发送至云端进行推理,云端则返回操作指令。
在视频里, UP 主通过深度拆解豆包手机,分析手机在系统层面的服务分工、数据抓取和模型推理路径,例如aikernel被 UP 主推断为手机端侧 AI 的核心进程,内存占用特性(Native堆高达160M)表明它可能是一个本地AI推理框架:

另外
aikernel异常高的Binder数量,证明有大量外部进程通过 RPC 调用它,进一步印证了其系统级服务的角色 。
而 autoaction是豆包手机 AI 自动操作的关键,这个 APK 权限允许直接从 GPU 渲染的图形缓冲区读取数据,而不是通过上层截图:


而且目前看,豆包手机的 AI 能够捕获受保护的视频输出,这意味着它可以绕过银行 App 等应用的反截图/录屏限制 ,因为很多银行 App 很多是通过 DRM(数字版权管理) 或应用内安全设置来防止截屏和录屏:

另外,Agent 在操作手机过程也不是直接使用系统的 Accessibility Service ,而是通过调用系统隐藏API injectInputEvent 来控制手机, AI 通过 INJECT_EVENTS 权限直接注入输入事件来模拟屏幕点击,权限高于无障碍 API,并且是系统签名:


同时,豆包手机在执行自动操作时,会利用一个与物理屏幕分辨率相同的“无头”虚拟屏幕在后台运行,且拥有独立的焦点,不影响用户在前台的操作,这其实就是内存副屏的概念, 虚拟屏幕的画面由 GPU 合成后,对应的缓冲区信息会直接被autoaction消费,再次证实 AI 无需通过截图 API 即可获取屏幕内容 :

最后,豆包手机在自动化操作时,会频繁地(每3到5秒)与 obriccloud.com (字节的服务) 服务器通信,发送约 250K的单帧图片进行推理。
云端在接收图片后,会返回约 1K 的数据,内容是告诉手机下一步要执行的 7 种指令之一,如打开应用、点击、输入、滑动等等,整个自动化 Agent 的推理和路径规划主要在云端完成,云端思考后将执行步骤指令发回本地执行,本地任务很轻:


那么,这整个过程你看下来有什么感觉?如果你是第三方厂商,你会不会同样抵制这种数据收集和处理的行为?特别是绕过现有大家对系统 API 的理解,这种操作途径是否能被友商们接受?
所以目前的这种操作,被微信和淘宝抵制很正常,不管是隐私的边界,还有安全操作的规范,用户对于自己某个产品内容被收集的信息程度,这些都还处于蛮荒状态,数据安全和隐私的边界范围还不可控,并且 Agent 的托管行为,也明显侵犯到了友商们的利益链条。
就像是 UP 主说的,AI Agent 的出现将动摇移动互联网的底层商业逻辑——注意力经济,使“注意力”这一硬通货的重要性降低 ,实际上换作另一个概念就是碎片化时间:
以前你的碎片化时间都是被各种 App 消费了,比如广告和沉浸引导,但是 Agent 的出现,它明显将这部分时间给托管了,那么数据和时间都被 Agent 服务收集,对于友商们来说,不就是成了单纯的功能性服务商了吗?
另外,说实话像 AutoGLM 这种功能目前的支持,最大受益者不是用户而是灰产,不管是用诈骗还是黄牛,他们都是这种自动化下的第一受益者,所以规范和监管,特别是安全和隐私条款是必须,比如就像 UP 主说的:
豆包手机的 AI 在自动化操作过程中,哪些数据会被发送到云端服务器?
很多人对于 agent 和自动化能力的范畴并不理解,它们可以获取隐私的边界是什么,安全操作的规范是什么,这些都是需要支持和统一边界。
比如 Android 16 实际上官方是有规划 Appfunction Api 的,它的目的是让应用只公布自己开放给 AI 的能力,这样也许边界感更强。
当然,从历史的角度看,Agent 手机势不可挡,就像谷歌自己未来新的 Android PC 系统 Aluminium OS 也是会结合 Gemini Agent 等特点,这是历史进程的必然,但是这个过程中,如何统一规范和监管这是很重要的过程,毕竟 AI 的效应和能力,可比之前更加强,就像 UP 主说的,新的 AI 寡头可能会形成更中心化、更强势的权力,且马太效应更明显 。
那么,你觉得未来谁家的 Agent 设备会成为新时达的寡头?或者不是手机而是眼镜?
视频链接
来源:juejin.cn/post/7582469532326920228
如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)

大家好😁。
上个月,我们公司的内部敏感文档(PRD)截图,竟然出现在了竞品的群里。
老板大发雷霆,要求技术部彻查:到底是谁泄露出去的?😠
但问题是,文档是纯文本的,截图上也没有任何显式的水印(那种写着员工名字的大黑字,太丑了,产品经理也不让加)。
怎么查?
这时候,我默默地打开了我的VS Code,给老板演示了一个技巧:
老板,其实泄露的那段文字里,藏着那个人的工号,只是你肉眼看不见。
今天,我就来揭秘这个技术——基于零宽字符(Zero Width Characters)的盲水印技术。学会这招,你也能给你的页面加上隐形追踪器。
先科普一下,什么叫零宽字符?
在Unicode字符集中,有一类神奇的字符。它们存在,但不占用任何宽度,也不显示任何像素。
简单说,它们是隐形的。
最常见的几个:
\u200b(Zero Width Space):零宽空格\u200c(Zero Width Non-Joiner):零宽非连字符\u200d(Zero Width Joiner):零宽连字符
我们可以在Chrome控制台里试一下:
console.log('A' + '\u200b' + 'B');
// 输出: "AB"
// 看起来和普通的 "AB" 一模一样
但是,如果我们检查它的长度:
console.log(('A' + '\u200b' + 'B').length);
// 输出: 3
看到没?😁

它的原理是什么?
原理非常简单,就是利用这些隐形字符,把用户的信息(比如工号User_9527),编码进一段正常的文本里。
步骤如下:
- 准备密码本 :我们选两个零宽字符,代表二进制的
0和1。
\u200b代表0\u200c代表1- 再用
\u200d作为分割符。
- 加密(编码) :
- 把工号字符串(如 9527)转成二进制。
- 把二进制里的 0/1 替换成对应的零宽字符。
- 把这串隐形字符串,插入到文档的文字中间。
- 解密(解码) :
- 拿到泄露的文本,提取出里面的零宽字符。
- 把零宽字符还原成 0/1。
- 把二进制转回字符串,锁定👉这个内鬼。
是不是很神奇?🤣
只需要30行代码实现抓内鬼工具
不废话,直接上代码。你可以直接复制到控制台运行。
加密函数 (Inject Watermark)
// 零宽字符字典
const zeroWidthMap = {
'0': '\u200b', // Zero Width Space
'1': '\u200c', // Zero Width Non-Joiner
};
function textToBinary(text) {
return text.split('').map(char =>
char.charCodeAt(0).toString(2).padStart(8, '0') // 转成8位二进制
).join('');
}
function encodeWatermark(text, secret) {
const binary = textToBinary(secret);
const hiddenStr = binary.split('').map(b => zeroWidthMap[b]).join('');
// 将隐形字符,插入到文本的第一个字符后面
// 你也可以随机分散插入,更难被发现
return text.slice(0, 1) + hiddenStr + text.slice(1);
}
// === 测试 ===
const originalText = "公司机密文档,严禁外传!";
const userWorkId = "User_9527";
const watermarkText = encodeWatermark(originalText, userWorkId);
console.log("原文:", originalText);
console.log("带水印:", watermarkText);
console.log("肉眼看得出区别吗?", originalText === watermarkText); // false
console.log("长度对比:", originalText.length, watermarkText.length);


当你把 watermarkText 复制到微信、飞书或者任何地方,那串隐形字符都会跟着一起被复制过去。
解密函数的实现
现在,假设我们拿到了泄露出去的这段文字,怎么还原出是谁干的?
// 反向字典
const binaryMap = {
'\u200b': '0',
'\u200c': '1',
};
function decodeWatermark(text) {
// 1. 提取所有零宽字符
const hiddenChars = text.match(/[\u200b\u200c]/g);
if (!hiddenChars) return '未发现水印';
// 2. 转回二进制字符串
const binaryStr = hiddenChars.map(c => binaryMap[c]).join('');
// 3. 二进制转文本
let result = '';
for (let i = 0; i < binaryStr.length; i += 8) {
const byte = binaryStr.slice(i, i + 8);
result += String.fromCharCode(parseInt(byte, 2));
}
return result;
}
// === 测试抓内鬼 ===
const leakerId = decodeWatermark(watermarkText);
console.log("抓到内鬼工号:", leakerId); // 输出: User_9527
微信或者飞书 复制出来的文案 👇

这种水印能被清除吗?
当然可以,但前提是你知道它的存在。
对于不懂技术的普通员工,他们复制粘贴文字时,根本不会意识到自己已经暴露了🤔
如果遇到了懂技术的内鬼,他可能会:
- 手动重打一遍文字:这样水印肯定就丢了(但这成本太高)🤷♂️
- 用脚本过滤:如果他知道你用了零宽字符,写个正则
text.replace(/[\u200b-\u200f]/g, '')就能清除。
虽然它不是万能的,但它是一种极低成本、极高隐蔽性的防御手段。
技术本身就没什么善恶。
我分享这个技术,不是为了让你去监控谁,而是希望大家多掌握一种防御性编程的一个思路。
在Web开发中,除了明面上的UI和交互,还有很多像零宽字符这样隐秘的角落,藏着一些技巧。
下次如果面试官问你:除了显式的水印,你还有什么办法保护页面内容?
你可以自信地抛出这个方案,绝对能震住全场😁。

来源:juejin.cn/post/7578402574653112372
从一线回武汉的真实感受
从北京回武汉差不多六年了,感慨颇多, 谈谈真实感受。
1 IT 公司
我们先把 IT 公司做一个分类整理 : 
从表中来看,武汉的 IT 公司确实不算少 ,主要集中于光谷,但我需要强调一下:
1、在大厂的眼里,武汉的定位是第二研发中心,看中的是武汉海量的研发人力资源以及较低的薪资水平。
2、第二研发中心做的并非核心业务,而且第二研发中心的权限往往不够。所以第二研发中心往往也被称为外包中心,这也是武汉很多朋友都说武汉是外包之城的原因。
3、武汉的技术氛围很差,高水平的研发人员相对较少,无论是管理者还是研发人员和一线相比是有绝对差距的。
接下来,聊聊薪资。
武汉 IT 薪资和一线差距很大,我预估月薪应该是一线的 50% 到 60% 左右,年终奖一般都是 1 ~ 2 个月,少部分公司会有股票,社保/公积金相对较低。
假如你在互联网公司,达到了阿里 P7 左右,我建议暂时不回武汉,因为武汉的薪资、技术氛围真的可能让你失望,还不如在一线多攒钱,等资金充裕了,回武汉更加合适点(一线挣钱,武汉花,很美!)。
2 大武汉
我们经常会将武汉说成“大武汉”,官方数据显示,武汉的行政面积达 8569.15 平方公里,相当于0.52个北京、1.35个上海或 4.29 个深圳。
武汉被长江和众多湖泊自然分割,形成了"三镇鼎立"的独特格局——汉口、武昌、汉阳各自为政又浑然一体。

为了连接这片水域纵横的土地,仅长江上就架起了十余座大桥,每一座都是城市发展的见证者。
回武汉的第一年,每天驱车从金银湖到关山大道,真有一种跋山涉水翻山越岭的感觉。
大江大湖造就了大武汉的壮阔景观,从金银湖的潋滟波光到南湖的静谧秀美,从堤角的市井烟火到欢乐谷的现代活力,处处都是令人惊叹的滨水景观。
- 城市夜景

- 长江大桥

3 文化
武汉的城市文化非常多元 ,有的时候,你甚至想不明白,为什么这么多迥异的文化元素集中于同一个城市。
01 码头文化
武汉因水而兴,自古就是“九省通衢”的商贸重镇。
汉口的码头文化塑造了武汉人直爽、讲义气的性格,“不服周”“讲胃口”的方言里,藏着码头工人的豪迈与坚韧。清晨的吉庆街、户部巷,热干面的芝麻香混合着面窝的酥脆,老武汉的一天就在这样的烟火气中开始。

02 过早
武汉人“过早”(吃早餐)的仪式感全国闻名,热干面、豆皮、糊汤粉、牛肉粉……一个月可以不重样 。



03 科教中心
武汉坐拥武汉大学、华中科技大学等近百所高校,是中国三大科教中心之一。
樱花纷飞的武大、梧桐成荫的华科、文艺范十足的昙华林,让这座城市既有历史的厚重感,又有青春的朝气。




04 省博物馆
湖北省博物馆是中国最重要的国家级博物馆之一,推荐各位同学来武汉时一定要去看一看。
1、越王勾践剑 : 锋芒依旧的王者之剑

2、曾侯乙编钟:奏响穿越时空的旋律

3、曾侯乙尊盘:青铜铸造的巅峰之作

4 生活
在武汉生活其实很方便,拿医疗资源来讲,我在北京望京看牙经常挂不到号,在武汉不可能发生这种情况,因为我家附近有两家三甲医院,平常看病就医都很方便。
武汉的景点非常多,周末我经常开车带老婆、孩子去东湖、九峰山动物园、植物园等景点游玩。
因为离父母近,也有了更多时间陪陪父母,他们年纪大了,总会感到孤独,我在他们身边,他们也会感觉好一点。
总而言之,相比在北京,我更加有归属感,而且幸福感更强。
有点遗憾的是,在武汉工作,一直感觉很别扭 :
- 讯飞的业务是 TOG 的项目,很多产品、项目质量堪忧,有的接近劣质的边缘,同时合肥管理人员所体现出的低素质,让我的价值观受到了极大的刺激。
- 武汉不应该仅仅作为人力资源之城,或者说是外包之城。
我曾经对老婆讲:“我有点后悔离开北京,在武汉,最高的 offer 可以拿到接近 53 w ,要是这六年还在北京,运气好的话,手里的现金可以多个 100 w 吧!”
老婆听了我的幻想,笑了笑,回道:“那可能依依都不可能出生呢”。
我想了想: “也是,现在其实挺幸福的”。

来源:juejin.cn/post/7494836390532136986
从美团全栈化看 AI 冲击:前端转全栈,是自救还是必然 🤔🤔🤔
我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于
Tiptap的富文本编辑器、NestJs后端服务、AI集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了Tiptap的深度定制、性能优化和协作功能的实现等核心难点。
如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。
据 大厂日报 称,美团履约团队近期正在推行"全栈化"转型。据悉,终端组的部分前端同学在 11 月末左右转到了后端组做全栈(前后端代码一起写),主要是 agent 相关项目。内部打听了一下,团子目前全栈开发还相对靠谱,上线把控比较严格。
这一消息在技术圈引起了广泛关注,也反映了 AI 时代下前端工程师向全栈转型的必然趋势。但更重要的是,我们需要深入思考:AI 到底给前端带来了什么冲击?为什么前端转全栈成为了必然选择?
最近,前端圈里不断有"前端已死"的话语流出。有人说 AI 工具会替代前端开发,有人说低代码平台会让前端失业,还有人说前端工程师的价值正在快速下降。这些声音虽然有些极端,但确实反映了 AI 时代前端面临的真实挑战。
一、AI 对前端的冲击:挑战与机遇并存
1. 代码生成能力的冲击
冲击点:
- 低复杂度页面生成:AI 工具(如 Claude Code、Cursor)已经能够快速生成常见的 UI 组件、页面布局
- 重复性工作被替代:表单、列表、详情页等标准化页面,AI 生成效率远超人工
- 学习门槛降低:新手借助 AI 也能快速产出基础代码,前端"入门红利"消失
影响:
传统前端开发中,大量时间花在"写页面"上。AI 的出现,让这部分工作变得极其高效,甚至可以说,只会写页面的前端工程师,价值正在快速下降。这也正是"前端已死"论调的主要依据之一。
2. 业务逻辑前移的冲击
冲击点:
- AI Agent 项目激增:如美团案例中的 agent 相关项目,需要前后端一体化开发
- 实时交互需求:AI 应用的流式响应、实时对话,要求前后端紧密配合
- 数据流转复杂化:AI 模型调用、数据处理、状态管理,都需要全栈视角
影响:
纯前端工程师在 AI 项目中往往只能负责 UI 层,无法深入业务逻辑。而 AI 项目的核心价值在于业务逻辑和数据处理,这恰恰是后端能力。
3. 技术栈边界的模糊
冲击点:
- 前后端一体化趋势:Next.js、Remix 等全栈框架兴起,前后端代码同仓库
- Serverless 架构:边缘函数、API 路由,前端开发者需要理解后端逻辑
- AI 服务集成:调用 AI API、处理流式数据、管理状态,都需要后端知识
影响:
前端和后端的边界正在消失。只会前端的前端工程师,在 AI 时代会发现自己"够不着"核心业务。
4. 职业发展的天花板
冲击点:
- 技术深度要求:AI 项目需要理解数据流、算法逻辑、系统架构
- 业务理解能力:全栈开发者能更好地理解业务全貌,做出技术决策
- 团队协作效率:全栈开发者减少前后端沟通成本,提升交付效率
影响:
在 AI 时代,只会前端的前端工程师,职业天花板明显。而全栈开发者能够:
- 独立负责完整功能模块
- 深入理解业务逻辑
- 在技术决策中发挥更大作用
二、为什么前端转全栈是必然选择?
1. AI 项目的本质需求
正如美团案例所示,AI 项目(特别是 Agent 项目)的特点:
- 前后端代码一起写:业务逻辑复杂,需要前后端协同
- 数据流处理:AI 模型的输入输出、流式响应处理
- 状态管理复杂:对话状态、上下文管理、错误处理
这些需求,纯前端工程师无法独立完成,必须掌握后端能力。
2. 技术发展的趋势
- 全栈框架普及:Next.js、Remix、SvelteKit 等,都在推动全栈开发
- 边缘计算兴起:Cloudflare Workers、Vercel Edge Functions,前端需要写后端逻辑
- 微前端 + 微服务:前后端一体化部署,降低系统复杂度
3. 市场需求的转变
- 招聘要求变化:越来越多的岗位要求"全栈能力"
- 项目交付效率:全栈开发者能独立交付功能,减少沟通成本
- 技术决策能力:全栈开发者能更好地评估技术方案
三、后端技术栈的选择:Node.js、Python、Go
对于前端转全栈,后端技术栈的选择至关重要。不同技术栈有不同优势,需要根据项目需求选择。
1. Node.js + Nest.js:前端转全栈的最佳起点
优势:
- 零语言切换:JavaScript/TypeScript 前后端通用
- 生态统一:npm 包前后端共享,工具链一致
- 学习成本低:利用现有技能,快速上手
- AI 集成友好:LangChain.js、OpenAI SDK 等完善支持
适用场景:
- Web 应用后端
- 实时应用(WebSocket、SSE)
- 微服务架构
- AI Agent 项目(如美团案例)
学习路径:
- Node.js 基础(事件循环、模块系统)
- Nest.js 框架(模块化、依赖注入)
- 数据库集成(TypeORM、Prisma)
- AI 服务集成(OpenAI、流式处理)
2. Python + FastAPI:AI 项目的首选
优势:
- AI 生态最完善:OpenAI、LangChain、LlamaIndex 等原生支持
- 数据科学能力:NumPy、Pandas 等数据处理库
- 快速开发:语法简洁,开发效率高
- 模型部署:TensorFlow、PyTorch 等模型框架
适用场景:
- AI/ML 项目
- 数据分析后端
- 科学计算服务
- Agent 项目(需要复杂 AI 逻辑)
学习路径:
- Python 基础(语法、数据结构)
- FastAPI 框架(异步、类型提示)
- AI 库集成(OpenAI、LangChain)
- 数据处理(Pandas、NumPy)
3. Go:高性能场景的选择
优势:
- 性能优秀:编译型语言,执行效率高
- 并发能力强:Goroutine 并发模型
- 部署简单:单文件部署,资源占用少
- 云原生友好:Docker、Kubernetes 生态完善
适用场景:
- 高并发服务
- 微服务架构
- 云原生应用
- 性能敏感场景
学习路径:
- Go 基础(语法、并发模型)
- Web 框架(Gin、Echo)
- 数据库操作(GORM)
- 微服务开发
4. 技术栈选择建议
对于前端转全栈的开发者:
- 首选 Node.js:如果目标是快速转全栈,Node.js 是最佳选择
- 学习成本最低
- 前后端代码复用
- 适合大多数 Web 应用
- 考虑 Python:如果专注 AI 项目
- AI 生态最完善
- 适合复杂 AI 逻辑
- 数据科学能力
- 学习 Go:如果追求性能
- 高并发场景
- 微服务架构
- 云原生应用
建议:
- 第一阶段:选择 Node.js,快速转全栈
- 第二阶段:根据项目需求,学习 Python 或 Go
- 长期目标:掌握多种技术栈,根据场景选择
四、总结
AI 时代的到来,给前端带来了深刻冲击:
- 代码生成能力:低复杂度页面生成被 AI 替代
- 业务逻辑前移:AI 项目需要前后端一体化
- 技术边界模糊:前后端边界正在消失
- 职业天花板:只会前端的前端工程师,发展受限
前端转全栈,是 AI 时代的必然选择。
对于技术栈选择:
- Node.js:前端转全栈的最佳起点,学习成本低
- Python:AI 项目的首选,生态完善
- Go:高性能场景的选择,云原生友好
正如美团的全栈化实践所示,全栈开发还相对靠谱,关键在于:
- 选择合适的技术栈
- 建立严格的开发流程
- 持续学习和实践
对于前端开发者来说,AI 时代既是挑战,也是机遇。转全栈,不仅能应对 AI 冲击,更能打开职业发展的新空间。那些"前端已死"的声音,其实是在提醒我们:只有不断进化,才能在这个时代立足。
来源:juejin.cn/post/7581999251368460340
10年深漂,放弃高薪,回长沙一年有感
大明哥是 2014 年一个人拖着一个行李箱,单身杀入深圳,然后在深圳一干就是 10 年。
10 年深漂,经历过 4 家公司,有 20+ 人的小公司,也有上万人的大厂。
体验过所有苦逼深漂都体验过的难。坐过能把人挤怀孕的 4 号线,住过一天见不到几个小时太阳的城中村,见过可以飞的蟑螂。欣赏过晚上 6 点的晚霞,但更多的是坐晚上 10 点的地铁看一群低头玩手机的同行。
10 年虽然苦、虽然累,但收获还是蛮颇丰的。从 14年的 5.5K 到离职时候的 xxK。但是因为种种原因,于 2023年 9 月份主动离职离开深圳。
回长沙一年,给我的感觉就是:除了钱少和天气外,样样都比深圳好。
生活
在回来之前,我首先跟我老婆明确说明了我要休息半年,这半年不允许跟我提任何有关工作的事情,因为在深圳工作了 10 年真的太累,从来没有连续休息超过半个月的假期。哪怕是离职后我也是无缝对接,这家公司周五走,下家公司周一入职。
回来后做的第一件事情就是登出微信、删除所有闹钟、手机设置全天候的免打扰,全心全意,一心一意地陪女儿和玩,在这期间我不想任何事情,也不参与任何社交,就认真玩,不过顺便考了个驾-照。
首先说消费。
有很多人说长沙是钱比深圳少,但消费不比深圳低。其实不然,我来长沙一年了,消费真的比深圳低不少。工作日我一天的消费基本上可以控制在 40 左右,但是在深圳我一天几乎都要 80 左右。对比
| 长沙 | 深圳 | |
| 早 | 5+ | 5+ |
| 中 | 15 ~ 25 | 25 ~ 35 |
| 晚 | 10 ~ 15,不加班就回家吃 | 25 ~ 35,几乎天天加班 |
同时,最近几个月我开始带饭了,周一到超时买个百来块的菜,我一个人可以吃两个星期。
总体上,一个月消费长沙比深圳低 1000 左右(带饭后更低了)。
再就是日常的消费。如果你选择去长沙的商城里面吃,那与深圳其实差不多了多少,当然,奶茶方面会便宜一些。但是,如果你选择去吃长沙的本土菜,那就会便宜不少,我跟我朋友吃饭,人均 50 左右,不会超过 70,选择美团套餐会更加便宜,很多餐馆在支持美团的情况下,选择美团套餐,两个人可以控制在 30 ~ 40 之间。而深圳动不动就人均 100+。
当然,在消费这块,其实节约的钱与少的工资,那就是云泥之别,可忽略不计。
再说生活这方面。
在长沙这边我感觉整体上的幸福感比深圳要强蛮多,用一句话说就是:深圳都在忙着赚钱,而长沙都在忙着吃喝玩乐加洗脚。我说说跟我同龄的一些高中和大学同学,他们一毕业就来长沙或者来长沙比较早,所以买房就比较早,尤其是 16 年以前买的,他们的房贷普遍在 3000 左右,而他们夫妻两的工资税后可以到 20000,所以他们这群人周末经常约着一起耍。举两个例子来看看他们的松弛感:
- 晚上 10 点多喊我去吃烧烤,我以为就是去某个夜市撸串,谁知道是开车 40+公里,到某座山的山顶撸串 + 喝酒。这是周三,他们上班不上班我不知道,反正我是不上班。
- 凌晨 3 点多拉我出来撸串
跟他们这群人我算是发现了,大部分的聚会都是临时起意,很少提前约好,主打就是一个随心随意。包括我和同事一样,我们几乎每个月都会出来几次喝酒(我不喜欢喝酒,偶尔喝点啤酒)、撸串,而且每次都是快下班了,某个人提议今晚喝点?完后,各回各家。
上面是好的方面,再说不好的。
长沙最让我受不了的两点就是天气 + 交通。
天气我就不说了,冬天冻死你,夏天热死你。去年完整体验了长沙的整个冬天,是真他妈的冷,虽然我也是湖南人,但确实是把我冻怕了。御寒?不可能的,全靠硬抗。当然,也有神器:火桶子,那是真舒服,我可以在里面躺一整天。
交通,一塌糊涂,尤其是我每天必经的西二环,简直惨不忍睹,尤其是汽车西站那里,一天 24 小时都在堵,尤其是周一和周五,高德地图上面是红的发黑。所以,除非特殊情况,我周一、周五是不开车的,情愿骑 5 公里小电驴去坐地铁。
然后是一大堆违停,硬生生把三车道变成两车道,什么变道不打灯,实线变道,双黄线调头见怪不怪了,还有一大群的小电驴来回穿梭,对我这个新手简直就是恐怖如斯(所以,我开车两个月喜提一血,4S点维修报价 9800+)。
美食我就不说了,简直就是吃货的天堂。
至于玩,我个人觉得长沙市内没有什么好玩的,我反而喜欢去长沙的乡里或者周边玩。所以,我实在是想不通,为什么五一、国庆黄金周长沙是这么火爆,到底火爆在哪里???
还有一点就是,在深圳我时不时就犯个鼻炎,回了长沙一年了我一次都没有,不知道什么原因。
工作
工资,长沙这边的钱是真的少,我现在的年收入连我深圳的三分之一都没有,都快到四分之一了。
当然,我既然选择回来了,就会接受这个低薪,而且在回来之前我就已经做好了心理建设,再加上我没有房贷和车贷,整体上来说,每个月略有结余。
所以,相比以前在深圳赚那么多钱但是无法和自己家人在一起,我更加愿意选择少赚点钱,当然,每个人的选择不同。我也见过很多,受不了长沙的低工资,然后继续回深圳搬砖的。
公司,长沙这边的互联网公司非常少,说是互联网荒漠都不为过。大部分都是传统性的公司,靠国企、外包而活着,就算有些公司说自己是互联网公司,但也不过是披着互联网的羊皮而已。而且在这里绝大多数公司都是野路子的干法,基建差,工作环境也不咋地,福利待遇与深圳的大厂更是没法比,比如社保公积金全按最低档交。年假,换一家公司就清零,我进入公司的时候,我一度以为我有 15 天,一问人事,试用期没有,转正后第一年按 5 天折算,看得我一脸懵逼。
加班,整体上来说,我感觉比深圳加班得要少,当然,大小周,单休的公司也不少,甚至有些公司连五险一金都不配齐,劳动法法外之地名副其实。
同时,这边非常看重学历,一些好的公司,非 985 、211 不要,直接把你门焊死,而这些公司整体上来说工资都很不错,40+ 起码是有的(比如某银行,我的履历完美契合,但就是学历问题而被拒),在长沙能拿这工资,简直就是一种享受,一年就是一套房的首付。
最后,你问我长沙工资这么低,你为什么还选择长沙呢?在工作和生活之间,我只不过选择了生活,仅此而已!!
来源:juejin.cn/post/7457175937736163378
当上组长一年里,我保住了俩下属
前言
人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。
就在上周,"苟住"群里的一个小伙伴也苟不住了。

在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。
1. 组织变动,意外晋升
两年前加入公司,依然是一线搬砖的码农。
干到一年的时候公司空降了一位号称有诸多大厂履历的大佬来带领研发,说是要给公司带来全新的变化,用技术创造价值。
大领导第一件事:抓人事,提效率。
在此背景下,公司不少有能力的研发另谋出处,也许我看起来人畜无害,居然被提拔当了小组长。
2. 领取任务,开启副本
当了半年的小组长,我的领导就叫他小领导吧,给我传达了大领导最新规划:团队需要保持冲劲,而实现的手段就是汰换。
用人话来说就是:
当季度KPI得E的人,让其填写绩效改进目标,若下一个季度再得到E,那么就得走人
我们绩效等级是ABCDE,A是传说中的等级,B是几个人有机会,大部分人是C和D,E是垫底。
而我们组就有两位小伙伴得到了E,分别是小A和小B。
小领导意思是让他们直接走得了,大不了再招人顶上,而我想着毕竟大家共事一场,现在大环境寒气满满,我也是过来人,还想再争取争取。
于是分析了他们的基本资料,他俩特点还比较鲜明。
小A资料:
- 96年,单身无房贷
- 技术栈较广,技术深度一般,比较粗心
- 坚持己见,沟通少,有些时候会按照自己的想法来实现功能
小B资料:
- 98年,热恋有房贷
- 技术基础较薄弱,但胜在比较认真
- 容易犯一些技术理解上的问题
了解了小A和小B的历史与现状后,我分别找他们沟通,主要是统一共识:
- 你是否认可本次绩效评估结果?
- 你是否认可绩效改进的点与风险点(未达成被裁)?
- 你是否还愿意在这家公司苟?
最重要是第三点,开诚布公,若是都不想苟了,那就保持现状,不要浪费大家时间,我也不想做无用功。
对于他们,分别做了提升策略:
对于小A:
- 每次开启需求前都要求其认真阅读文档,不清楚的地方一定要做记录并向相关人确认
- 遇到比较复杂的需求,我也会一起参与其中梳理技术方案
- 需求开发完成后,CR代码看是否与技术方案设计一致,若有出入需要记录下来,后续复盘为什么
- 给足时间,保证充分自测
对于小B:
- 每次需求多给点时间,多出的时间用来学习技术、熟悉技术
- 要求其将每个需求拆分为尽可能小的点,涉及到哪些技术要想清楚、弄明白
- 鼓励他不懂就要问,我也随时给他解答疑难问题,并说出一些原理让他感兴趣的话可以继续深究
- 分配给他一些技术调研类的任务,提升技术兴趣点与成就感
3. 结束?还是是另一个开始?
半年后...
好消息是:小A、小B的考核结果是D,达成了绩效改进的目标。
坏消息是:据说新的一轮考核算法会变化,宗旨是确保团队血液新鲜(每年至少得置换10%的人)。
随缘吧,我尽力了,也许下一个是我呢?

来源:juejin.cn/post/7532334931021824034
2小时个人公司:一个全栈开发的精益创业之路
一、前言
这不是一个单纯的技术教学专栏,而是一个 “技术人商业实践手记” 。记录一个全栈开发者如何用“每天2小时”的投入,系统性地从0到1打造一个能产生持续价值(无论是金钱、影响力还是个人成长)的“个人公司”。
二、为什么投入难有回报?
相信许多技术人都有过类似经历:
- 激情开始:某个阶段干劲满满,规划通过技术项目增加收入
- 目标宏大:开发开源系统、搭建博客、编写组件库,期待财务自由
- 现实骨感:投入大量时间后,发现自己仍在原地踏步
我的亲身教训
我尝试过几乎所有主流博客方案:
- WordPress → Hexo → VuePress/VitePress → Halo
但结果都是:上线时分享给朋友,然后... 再无下文。这完全背离了建站的初衷:打造个人影响力,为专业机会铺路。
所谓的“知识整理”和“自我提升”,很多时候只是自我安慰的借口。
三、为什么创建这个专栏?
一个清醒的认知
真正有效的盈利方法很少有人会无偿分享。
看看最近的AI创业热潮:
- 如果AI创业真的那么赚钱,为什么有人选择教学而不是扩大规模?
- 答案很现实:教学比实际创业更有利可图
坦诚的价值交换
您可能会问:你和他们有什么不同?
我坦然承认:这个专栏有我个人的目标。但区别在于:
- 我追求价值互换,而非单向收割
- 您获得的是我真实的实战经验
- 我获得的是关注度和影响力积累
- 未来可能开启付费模式,但规则透明
这是一种基于相互尊重的成长模式。
核心价值主张
| 传统模式 | 我的方式 |
|---|---|
| 隐藏真实目的 | 坦诚价值交换 |
| 过度承诺结果 | 分享真实过程 |
| 单向知识输送 | 双向成长陪伴 |
简单来说:您吸取我的经验教训,我通过您的关注获得成长机会。公平,透明。
接下来的内容,我将具体分享如何用“每天2小时”打造真正有价值的个人产品...
来源:juejin.cn/post/7566289235368919049
公司开始严查午休…
最近刷到一条有关午睡的吐槽帖子,可能之前有小伙伴也看到过,事情大致是这样的:
有阿里同学在职场社区发帖吐槽,公司严查午休,13:34 公司纪委直接敲门,提醒别休息了,然后还一遍又一遍的巡逻……

说实话,第一眼刷到这个帖子的时候,脑子里的画面感的确有点强......就帖子来看,其实 1:30 这个时间本身没有看出太大毛病,很多公司比这还早呢,关键是氛围的突然变化的确让人会感到非常不适应,估计这也是帖主的主要槽点。
这里所谓的公司纪委我猜是类似行政或者 HRG 之类的巡查人员?中午午休时间一到,就开始挨个房间开灯、敲门,有的甚至还敲隔板,进行巡逻式提醒。另外话说回来,阿里那么大,可能不同部门或者不同 bu 在这件事情的要求上可能也太不一样吧,了解的同学可以说说,这个咱就不好过多评论了。
那说回午休这件事本身,我倒是见过几个公司的午休文化。
记得之前在某通信设备商工作时,那里的午休文化是刻在骨子里的。到了中午,是真的鼓励大家带床午休。
12 点多吃完饭,整层楼的灯基本都会关掉,大家纷纷拿出自己的小折叠床,开始午睡休息。午休时间到点了再集体把灯打开,那种集体休整的仪式感,会让下午的工作效率更高。
再比如像互联网大厂里的腾讯,每天中午也是可以午休的,茶水间的咖啡机上甚至会贴着“尽量不要在午休期间使用”的牌子,十分人性化。另外,我记得他们之前校招入职礼盒里是不是好像还发过毯子还是披肩来着?这正好可以用于午休,都不用自己买了。
这种对员工休息的尊重和保护,说实话,真的是会让人感觉到温暖的。
关于午休这个事情,我个人觉得对于程序员来说还是非常有必要的。
毕竟,面对高强度的工作,没有好的休息,靠强撑着眼皮盯着屏幕,产出的未必是价值,更多的是低效的“摸鱼”和潜在的健康风险。
就拿我自己来说,搬砖工作日我基本都是要午睡的。
原因很简单,因为我晚上一般睡得都比较晚,而早上基本 7 点就起来了,第二天中午如果不睡一会,那完了,整个下午基本都废掉了,不管是开会还是写东西,整个人都会非常地不在状态。
同理在我的小团队内部也是,我们也是很鼓励大家午睡的。
所以我们团队同学基本人手一个午休折叠床+毯子,而且如果工位这里躺不开,大家也可以去会议室那里午睡休息。
我们一般是大家中午去吃饭的时候最后走的同学办公室关灯,然后下午 1 点 40 由 HR 那边同学统一开灯,大家对于这个习惯早已约定俗成、相沿成习。
但是有一点,也是和文章开头的帖子有很大不同的是,我们的同学在开灯时,不会像文章开头帖子那样还强行给你敲门整出一波动静,我们即便灯开了,大家也还是可以稍微再躺一下,缓个几分钟再慢慢起来都没啥问题。
试想一下,要是像文首帖子说的那样,突然有人来咔咔给你一顿敲门,或者说甚至还敲隔板,那不得给人吓一机灵?
面对文章开头的吐槽贴,虽然别人的事情我们也管不了,但是看问题也不能只看表面。
透过这个吐槽帖,反映的是职场的一些微妙变化,这背后,其实折射出的或许是一种“越来越收紧”的职场环境。
那如果你正好处于这种正在收紧的工作环境之中,作为普通个体,你会怎么做呢?
这里我也想稍微多聊两句。
**首先,千万不要因为环境的变化而让自己陷入情绪不满与内耗。**很早之前的文章里我就写过,要理性地看待工作关系。
职场本质是价值交换的契约关系,这没有问题,那付诸技术和专业的同时,也要保持清醒的边界意识:既不愤世嫉俗,也不天真幼稚。
其次,要学会“物理防御”,在规则允许的缝隙内尽量对自己好一点吧。
千万不要因为环境变紧了,就主动放弃自己的需求。毕竟,身体是自己的,健康是自己的,只是方式我觉得可以更灵活变通一些。
就拿这个帖子里「午睡收紧」这件事情来说,如果公司不让关灯,那咱就搞一个好一点的眼罩和降噪耳机行不行?
如果公司不让躺睡,那咱是不是可以买个质量好一点的颈枕,即便靠在椅子、或者趴在桌子上眯一会是不是也能舒服一点?
如果中午休息时间不够,我们是不是可以充分利用碎片化时间来缓一缓,比如利用下午茶或者拿快递的时间去楼下透透气,或者在工位上做几个简单的拉伸动作。
如此之类,等等等等,大家也可以自己多想想办法。
记住,无论职场环境如何变迁,身体是自己的,健康也是自己的,先把自己身体照顾好,再去谈理想谈工作,大家觉得呢?
好了,那以上就是今天的内容分享了,希望能对大家有所启发,我们下篇见。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7587619946189946931
🌸 入职写了一个月全栈next.js 感想
背景介绍
- 最近组内要做0-1的新ai产品, 招我进来就是负责这个ai产品,启动的时候这个季度就剩下两个月了,天天开会对齐进度,一个月就已经把基础版本给做完了,想要接入到现有的业务上面,时间方面就特别紧张,技术选型怎么说呢, leader用ai写了一个版本 我们在现有的代码进行二次开发这样, 全栈next.js 要学习的东西太多了 又没有前端基础,没有ai coding很难完成任务(十几分钟干完我一天的工作 claude4.5效果还不错 进度推的特别快), 自从trae下架了claude,后面就一直cursor claude 4.5了。
- nextjs+ts+tailwindcss+shadcn ui现在是mvp套餐,startup在梭哈,时间就是生产力哪需要那么多差异化样式直接一把,有的💰才开始做细节,你会发现慢慢也💩化了。
- Nextjs 是全栈框架 可以很快把一个MVP从零到一完整跑起来。 你要是抬杠说什么高并发负载均衡啥的,你的用户数量真多到需要考虑性能的时候,你已经不需要自己考虑了(小红书看到的一段话 挺符合场景的)
- next.js 写后端 确实比较轻量 只能做一些curd的操作 socket之类的不太合适 其他api 还是随便开发 给我的感受就是前端能够直接操作db,前后端仓库可以不分离,业务逻辑还是一定要分离的 看看开源的next.js 项目的架构设计结构是怎么样的 学习/模仿/改造。
- 语言只是工具,适合最重要,技术没有银弹
- nextjs.org/ github.com/vercel/next…

项目的时间线
项目从启动到这周 大概是5周的时间
- 10/28-10/31 Week 1
- 项目初始化/需求讨论/设计文档/
- 后端next.js, typescript技术熟悉 项目运行/调试
- 基础框架搭建 设计表结构ddl, 集成mysql, 编写crud接口阶段
- 11/03-11/07 Week 2
- 产品PRD 提供
- xxxx等表设计
- 11/10-11/14 Week 3
- xxxxx 基本功能完结
- @xxxx 讲解项目结构/规范
- 11/17-11/21 Week 4
- 首页样式/逻辑 优化
- 集成统一登录调研
- 部署完成
- 11/24-11/28 Week 5
- 服务推理使用Authorization鉴权 对内接口使用Cookies (access_token) 鉴权 开发
- xxxx 表设计表设计 逻辑开发
- xxx设计 设计开发
- 联调xxxx
5周时间 功能基本完成了 剩下的就是部署到线上 进行场景实践了
前端技术栈
- Next.js 14:选择 App Router 架构,支持服务端渲染和 API Routes
- TypeScript 5.4:强类型语言提升代码质量和可维护性
- React 18:利用并发特性和 Suspense 提升用户体验
- Zustand:轻量级状态管理,替代 Redux 降低复杂度
- Ant Design + Radix UI:组件库组合,平衡美观性和可访问性
React + TypeScript react.dev/
- 优势:类型安全:TypeScript 提供编译时类型检查,减少运行时错误 ✅ 组件化开发:高度可复用的组件设计 ✅ 生态成熟:丰富的第三方库和工具链 ✅ 开发体验:优秀的 IDE 支持和调试工具
- 劣势: ❌ 学习曲线:TypeScript 对新手有一定门槛 ❌ 编译时间:大型项目编译可能较慢 ❌ 配置复杂:类型定义需要额外维护
UI 组件方案 Ant Design + Radix UI 混合方案
- 优势: ✅ 快速开发:Ant Design 提供完整的企业级组件 ✅ 无障碍性:Radix UI 提供符合 WAI-ARIA 标准的组件 ✅ 定制灵活:Radix UI 无样式组件便于自定义 ✅ 中文支持:Ant Design 对中文界面友好
- 劣势: ❌ 包体积大:两个 UI 库增加了打包体积 ❌ 样式冲突:需要注意两个库的样式隔离❌ 维护成本:需要同时维护两套组件系统
Tailwind CSS
- 优势: ✅ 开发效率高:原子化类名,快速构建 UI ✅ 体积优化:生产环境自动清除未使用的样式 ✅ 一致性:设计系统内置,确保视觉一致 ✅ 响应式:便捷的响应式设计工具
- 劣势: ❌ 类名冗长:HTML 可能变得难以阅读 ❌ 学习成本:需要记忆大量类名 ❌ 非语义化:类名不直观反映元素意义
ant design x
ahooks
后端技术栈
- Prisma 6.18:现代化 ORM,类型安全且支持 Migration
- MySQL:成熟的关系型数据库,满足复杂查询需求
- Redis (ioredis) :高性能缓存,支持多种数据结构
- Winston:企业级日志系统,支持日志轮转和结构化输出
- Zod:运行时类型验证,保障 API 数据安全
Next.js API Routes
- 优势: ✅ 统一代码库:前后端在同一项目中 ✅ 类型共享:TypeScript 类型可在前后端复用 ✅ 开发效率:无需配置跨域、代理等 ✅ 部署简单:单一应用部署
- 劣势: ❌ 扩展性限制:无法独立扩展后端服务 ❌ 性能瓶颈:Node.js 单线程可能成为瓶颈 ❌ 微服务困难:不适合复杂的微服务架构
Prisma ORM
- 优势: ✅ 类型安全:自动生成 TypeScript 类型 ✅ 迁移管理:声明式 schema,易于版本控制 ✅ 查询性能:生成优化的 SQL 查询 ✅ 关系处理:直观的关系查询 API ✅ 多数据库支持:支持 MySQL、PostgreSQL、SQLite 等
- 劣势: ❌ 复杂查询:某些复杂 SQL 可能需要原始查询 ❌ 生成代码体积:生成的 client 文件较大 ❌ 版本升级:大版本升级可能需要迁移
踩坑记录
主要是记录一些开发过程中踩坑 和设计问题
- node js 项目 jean部署
- 自定义配置/dockerfile配置 没有类似项目参考 健康检查问题 加上环境变量配置多环境 一步一步
- next.js 中 用middleware进行接口拦截鉴权 里面有prisma path import 直接出现了Edge Runtime 异常 自定义auth 解决
- npm build 项目 踩坑
- 静态渲染流程 动态api 警告 强制动态渲染
- 其他组件 document 不支持build问题
- 保存多场景模式+构建版本管理第一版考虑的太少了,发现有问题 后面又重构了一版本
- xxx日志目前还没有接入 要不就是日志文件 要不就是console.log 目前看日志的方式是去容器化运行日志看了 后续集群部署就比较麻烦了
- ant design 版本降低到6.0以下 ant-design x 用不了2.0.0 的一些对话组件
Next.js实践的项目记录
苏州 trae friends线下黑客松 📒
- 去Trae pro-Solo模式 苏州线下hackathon一趟, 基本都是一些独立开发者,一人一公司,三个小时做出一个产品用Trae-solo coder模式,不得不说trae内部集成的vercel部署很丝滑 react项目一键deploy访问 完全不用关系域名服务器, solo模式其实就是混合多种model使用进行输出 想要的效果还是得不断的调试 thiking太长,对于前后端分离项目 也能够同时关联进行思考规划。
- 1点多到4点 coding时间 从0-1生成项目 使用trae pro solo模式 就3个小时 做不了什么大的东西 那就做个日语50音的网站呗 现场酒店的网基本用不了 我数据也很卡 用的旁边干中学老师的热点 用next.js tailwindcss ant design deepseek搭建的网页 够用了 最后vercel部署 trae自带集成 挺方便的 solo模式还是太慢了 接受不了 网站地址是 traekanastudio1ssw.vercel.app/ 功能就是假名+ai生成例句和单词 我都没有路演 最后拿优秀奖可能是我部署了吧 大部分人没部署 优秀奖就是卫衣了 蹭了一天的饭加零食 爽吃
- http://www.xiaohongshu.com/explore/692… 小红书当时发的帖子 可以领奖品

Typescript的AI方向 langchain/langgraph支持ts
- 最近在看的ts的ai框架 发现langchain 是支持ts的, langchain-chat 主要是使用langchain+langgraph 对ts进行实践 traechat-apps4y6.vercel.app/
- 部署还踩坑了 MCP 在 Vercel 上不生效是因为 Vercel 是 serverless 环境,不支持运行持久的子进程。让我帮你解决这个问题:
- 主要是对最近项目组内要用的到mcp/function call 进行实践操作 使用modelscope 上面开源的mcp进行尝试 使用vercel进行部署。
- 最近看到小红书上面的3d 粒子 圣诞树有点火呀,自己也尝试下 效果很差 自己弄的提示词 可以去看看帖子上的提示词去试试 他们都是gemini pro 3玩的 我也去弄个gemini pro 3 账号去玩玩。
- 还有一个3d粒子 跟着音乐动的的效果 下面的提示词可以试试
帮我构建一个极简科幻风格的 3D 音乐可视化网页。
视觉上参考 SpaceX 的冷峻美学,全黑背景,去装饰化。核心是一个由数千个悬浮粒子组成的‘生命体’,它必须能与声音建立物理连接:低音要像心脏搏动一样冲击屏幕,高音要像电流一样瞬间穿过点阵。
重点实现一种‘ACID 流体’视觉引擎:让粒子表面的颜色不再是静态的,而是像两种粘稠的荧光液体一样,在失重环境下互相吞噬、搅拌、流动,且流速由音乐能量驱动。

- docs.langchain.com/oss/javascr…
- http://www.modelscope.ai/home
- vercel.com
- http://www.modelscope.ai/mcp


ai方向 总结
- a2a解决的是agent之间如何配合工作的问题 agent card定义名片 名称 版本 能力 语言 格式 task委托书 通信方式http 用户 客户端是主控 接受用户需求 制定具体任务 向服务器发出需求 任务分发 接受响应 服务器是各类部署好的agent 遵循一套结构化模式
- mcp 解决的llm自主调用功能和工具问题
- mcp 是解决 function call 协议的碎片化问题,多 agent 主要是为了做上下文隔离
- 比如说手机有一个system agent 然后各个app有一个agent,用户语音输入买咖啡,然后system agent调用瑞幸agent 这样就是非侵入式 让app暴露系统a2a接口,感觉比mcp要更合理一点,不是单纯让app暴露tools,系统agent只需要做路由
- 而且有一点我觉得挺有意思的,就是自己的agent花的token是自己的钱,如果自己的agent找别人的agent,让它执行任务啥的,花的不就是别人的钱……
- Dify:更像宜家的模块化家具,提供可视化工作流、预置模板,甚至支持“拖拽式”编排AI能力。比如,你想做一
个智能客服,只需在界面里连接对话模型、知识库和反馈按钮,无需写一行代码
python 和ts 在ai上面的比较
- Python 依然是 AI 训练和科研的王者,PyTorch、TensorFlow、scikit-learn 这些生态太厚实了,训练大模型你离不开它。
- TS 在底层 AI 能力上还没那么能打,GPU 加速、模型优化这些,暂时还得靠 Python 打底。
- Python 搞理论和模型,TypeScript卷体验和交付
个人学习记录
主要还是前端和ai方面的知识点学习的比较多吧
- Typescript 语法基础+进阶 / Next.js 开发指南/React 开发指南
- ahooks 组件 使用 ahooks.js.org/zh-CN/hooks…
- ant design x 使用 ant-design-x.antgroup.com/components/…
- prisma orm框架 +mysql github.com/prisma/pris…
- dotenv 读取配置文件 github.com/dotenvx/dot…
- fastmcp 项目构建使用 原理
- Agent2Agent google协议内部详情
- swagger.io/specificati… OpenAPI 规范 一个 OpenAPI 描述(OAD)可以由一个 JSON 或 YAML 文档组成
- github.com/yossi-lee/s… 根据Swagger3规范,一键将Web服务转换为MCP
- http://www.jsonrpc.org/specificati… JSON-RPC 是一种无状态、轻量级的远程过程调用(RPC)协议
- github.com/agno-agi/ag… 多智能体框架
- roadmap.sh/ai-engineer ai工程师的roadmap 很全
- github.com/ChromeDevTo… *可以集成到cursorz中 *AI 能够直接控制和调试真实的 Chrome 浏览器
- http://www.nano-banana.ai/ Nano Banana Pro (V2) 文生图 图生图
- aistudio.google.com/prompts/new… gemini ai studio
Vibe Coding
- 先叠甲, 我没有前端的开发经验,第一次写前端项目,项目里面90%的前端代码都是ai 生成的,能够让你一个不会前端的同学也快速完成mvp版本/需求任务。我虽然很推ai coding 很喜欢用, 即时反馈带来的成就感, 但是对于生成的代码是不是屎山 大概率可能是了, 因为前期 AI速度快,制造屎山的速度更快。无论架构设计多优秀,也难避免屎山代码的宿命: 需求一直在变,你的架构设计是针对老的需求,随着新的需求增加,老的架构慢慢的就无法满足了,需要重构。
- 一起开发的前端同事都说ai生成那些样式互相影响了,样式有tailwindcss 有自定义的css 每个模块又有不同 大概率出问题 有冲突,就是💩山。
- 最大的开发障碍就是内心的偏见 不愿意放弃现在所擅长的东西 带着这份偏见不愿意去学习
对于ai coding 的话 用过trae-pro/cursor/qoder/copilot/codex等等 最终还是cursor claude 4.5用的最舒服

- 基本一周一个cursor pro账号 买号都花了快1k了。

You have used up your included usage and are on pay-as-you-go which is charged based on model API rates. You have spent $0.00 in on-demand usage this month.


- 最后就是需要学好英语 前端的技术文档都是英文的 虽然有中文的翻译版本, 但没有自己直接去看官方的强 难免有差异, 我现在都是用插件进行web翻译去看的 很累。
- 现在时间是凌晨 11/30/02:36 喝了两瓶酒。这个周末我要重温甜甜的恋爱 给我也来一颗药丸 给时间是时间 让过去过去, 年底想去日本跨年了


来源:juejin.cn/post/7577713754562838580
性能飙升4倍,苹果刚发布的M5给人看呆了
2025 年 10 月 15 日,苹果公司正式发布全新 M5 芯片。作为继 M4 之后的又一代 Apple Silicon 处理器,M5 采用第三代 3nm 制程工艺,在 AI 运算、图形性能与能效方面实现全面突破,标志着苹果在“端侧 AI”赛道上的又一次重大跨越。
目前,M5 已率先搭载于 14 英寸 MacBook Pro、新一代 iPad Pro 与 Apple Vision Pro,并同步开启预订。

一、核心亮点:GPU 首次集成神经加速单元
M5 最引人注目的革新,在于其 GPU 架构的彻底重构:
- 全新 10 核 GPU,每个核心均内置独立 Neural Accelerator(神经加速单元)
- GPU 的 AI 计算峰值性能较 M4 提升超 4 倍
- 相比初代 M1,AI 性能提升 超过 6 倍
苹果硬件技术高级副总裁 Johny Srouji 表示:“M5 标志着 Apple 芯片在 AI 性能上的又一次重大跨越。”
这一设计打破了传统 CPU/GPU/Neural Engine 三者分离的 AI 计算模式,使 GPU 本身具备原生 AI 推理能力,特别适合图像生成、视频处理、空间计算等高负载场景。
二、三大核心模块全面升级
1. CPU:更高能效,更强多线程
- 10 核 CPU 架构:6 个高能效核心 + 4 个高性能核心
- 多线程性能较 M4 提升最高达 15%
- 搭载“全球最快 CPU 核心”,兼顾性能与续航

2. 神经引擎(Neural Engine):协同加速 AI 任务
- 16 核神经引擎,专为机器学习优化
- 与 GPU/CPU 中的神经加速单元协同工作
- 在 Apple Vision Pro 上可极速生成“空间化照片”或个性化 Persona
- 为 Apple Intelligence 提供高效本地运行支持,如 Image Playground 响应更迅捷
官方介绍,M5 芯片的设计是围绕AI展开的,采用新一代 GPU 架构,每个计算单元均针对 AI 进行优化。10 核 GPU 中各个核心内置有专用神经加速器,峰值 GPU 计算性能达到 M4 的 4 倍有余,AI 峰值性能更达到 M1 的 6 倍以上。
3. 统一内存架构:带宽与容量双突破
- 内存带宽高达 153 GB/s,比 M4 提升近 30%
- 支持最高 32GB 统一内存
- 整个 SoC 共享同一内存池,大幅提升多任务与大模型本地运行能力
实测场景:用户可同时运行 Adobe Photoshop + Final Cut Pro + 后台上传大文件,系统依然流畅。
三、软件生态深度协同:Metal 4 + Core ML 赋能开发者
M5 的硬件革新离不开软件栈的配合。苹果通过以下框架释放 M5 的全部潜能:
| 框架 | 作用 |
|---|---|
| Core ML | 自动调度 CPU/GPU/Neural Engine,优化模型推理 |
| Metal Performance Shaders | 加速图形与计算任务 |
| Metal 4 | 新增 Tensor API,允许开发者直接对 GPU 中的神经加速单元编程 |
开发者可通过 Metal 4 构建专属 AI 加速方案,例如在本地运行 Draw Things(扩散模型绘图) 或 webAI 上的大语言模型。
四、实际应用场景:端侧 AI 落地加速
M5 的强大 AI 能力已在多个场景中体现:
- Apple Vision Pro:实时生成 3D 空间照片、个性化虚拟形象
- iPad Pro:本地运行 Stable Diffusion 类模型,秒级出图
- MacBook Pro:AI 编程助手、视频智能剪辑、语音实时转写等任务无需联网
- Apple Intelligence:所有 AI 功能均在设备端完成,保障隐私与低延迟
五、能效与环保:性能提升,功耗更低
尽管性能大幅跃升,M5 仍延续苹果芯片高能效传统:
- 采用先进 3nm 工艺,晶体管密度更高、漏电更少
- 在相同性能下功耗显著低于竞品
- 支持苹果 “Apple 2030”碳中和计划,从材料、制造到运输全链路减碳
这意味着新款 MacBook Pro、iPad Pro 和 Vision Pro 在获得更强性能的同时,续航更长、发热更低、环境足迹更小。
M5虽然又上面几个方面的提升,但是是否值得为M5献上自己的血汗钱,先看看用过的网友怎么说?
一个眼光长远的网友说到:

这里也期待下M6的到来!

当然如果你是一个AI重度开发者或者使用者,M5也是值得冲一把的。
下面就来看看者3款产品。
M5 MacBook Pro
外观和之前差别不大, 14 英寸 120Hz 3024*1964 的刘海屏,峰值亮度为 1600nits。

三个 Thunderbolt 4 端口、一个 HDMI 接口、一个 SD 卡槽、一个 3.5 耳机插孔。

最大的提升就是你可以拥有 4TB 的存储规格,但是价格和 512GB 相比差了 9000 块,相当于1TB的价格接近3000块大洋。

内存方面有16GB 和 24GB 以及 32GB,具体价格如下

对于普通的开发场景选择16GB就可以了,如果是做大数据,AI大模型开发可以选择32GB。
iPad Pro
屏幕采用双层 120Hz OLED ,11 英寸分辨率为 24201668,13 英寸分辨率为 27522064。

颜色一样提供深空黑色和银色两种可选

Vision Pro
新版的Vision Pro 除了从 M2+R1 组合升级到 M5+R1 之外,最显眼的变化就是升级了新的双针织头戴套。

续航也从原来的 2 小时扩展至 3 小时,最高刷新率也从 100Hz 增至 120Hz,有效减少了观看物理环境时的运动模糊。
不过价格最低高达9000,确实劝退了很多人。
总结:M5 是 Apple Intelligence 的“终极载体”
如果说 M1 是 Apple Silicon 的起点,M2/M3 是成熟,M4 是 AI 探索,那么 M5 就是 Apple Intelligence 的落地基石。
通过 GPU 内置神经加速单元 + 统一内存 + 软件深度协同,M5 不仅是一颗芯片,更是苹果构建“端侧 AI 生态系统”的核心引擎。
未来,随着更多 AI 应用向本地迁移,M5 将成为开发者与用户拥抱下一代人机交互的关键硬件平台。
端侧 AI 的时代,已经到来。
来源:juejin.cn/post/7563856713163915290
2025快手直播至暗时刻:当黑产自动化洪流击穿P0防线,我们前端能做什么?🤷♂️
兄弟们,前天的瓜都吃了吗?🤣

说实话,作为一名还在写代码的打工仔,看到前天晚上快手那个热搜,我手里捧着的咖啡都不香了,后背一阵发凉。
12月22日晚上10点,正是流量最猛的时候,快手直播间突然失控。不是服务器崩了,而是内容崩了——大量视频像洪水一样灌进来。紧接着就是官方无奈的拔网线,全站直播强行关停。第二天开盘,股价直接跌了3个点。
这可不是普通的 Bug,这是P0 级中的 P0。
很多群里在传内鬼或者0day,但看了几位安全圈大佬(360、奇安信)的复盘,我发现这事儿比想象中更恐怖:这是一次教科书级别的黑产自动化降维打击。
今天不谈公关,咱们纯从技术角度复盘一下:假如这事儿发生在你负责的项目里,你的前端代码能抗住几秒?
当脚本比真人还多还快时?
这次事故最骚的地方在于,黑产根本不按套路出牌。
以前的攻击是 DDoS,打你的带宽,让你服务不可用。
这次是 Content DDoS(内容拒绝服务)。
1. 前端防线形同虚设
大家有没有想过,黑产是怎么把视频发出来的?
他们绝对不会坐在手机前,一个一个点开始直播。他们用的是群控、是脚本、是无头浏览器(Headless Browser)。
这意味着什么?
意味着你前端写的那些 if (user.isLogin)、那些漂亮的 UI 拦截、那些弹窗提示,在黑客眼里全是空气。他们直接逆向了你的 API,拿到了推流接口,然后几万个并发调用。
2. 审核系统被饱和式攻击
后端通常有人工+AI 审核。平时 QPS 是 1万,大家相安无事。
昨晚,黑产可能瞬间把 QPS 拉到了 100万。
云端 AI 审核队列直接爆了,人工审核员估计鼠标都点冒烟了也审不过来。一旦阈值被击穿,脏东西就流到了用户端。
那前端背锅了吗?
虽然核心漏洞肯定在后端鉴权和风控逻辑(大概率是接口签名泄露),但咱们前端作为 离黑客最近的一层皮,如果做得好,绝对能把攻击成本拉高 100 倍。
来,如果不幸遇到了这种自动化脚本攻击,咱们前端手里还有什么牌?🤔
别把 Sign 算法直接写在 JS 里!
很多兄弟写接口签名,直接在 request.js 里写个 md5(params + salt) 完事。
大哥,Chrome F12 一开,Sources 一搜,断点一打,你的盐(Salt)就裸奔了。
防范操作:直接上 WASM (WebAssembly)
把核心的加密、签名逻辑,用 C++ 或 Rust 写,编译成 .wasm 文件给前端调。
黑客想逆向 WASM?那成本可比读 JS 代码高太多了。这就是给他们设的第一道坎。
你的用户,可能根本不是人
黑产用的是脚本。脚本和真人的操作是有本质区别的。
不要只会在登录页搞个滑块,没用的,现在的图像识别早破了。
要在 关键操作(比如点击开始直播) 前,采集一波数据:
- 鼠标轨迹:真人的轨迹是曲线(贝塞尔曲线),脚本通常是直线。
- 点击间隔:脚本是毫秒级的固定间隔,人是有随机抖动的。
// 伪代码,简单的是不是人检测
function isHuman(events) {
// 如果鼠标轨迹过于平滑或呈绝对直线 -> 机器人
if (analyzeTrajectory(events) === 'perfect_linear') return false;
// 如果点击时间间隔完全一致 -> 机器人
if (checkTiming(events) === 'fixed_interval') return false;
return true;
}
把这些行为数据打分,随着请求发给后端。分低的,直接拒绝推流。
既然防不住内鬼,那就给他打标
这次很多人怀疑是内部泄露了接口文档或密钥。说实话,这种事防不胜防。
但是,前端可以搞 盲水印。
在你的 Admin 管理后台、文档平台,加上肉眼看不见的 Canvas 水印(把员工 ID 编码进背景图的 RGB 微小差值里,具体大家自己去探索😖)。
一旦截图流出,马上就能解码出是哪个员工泄露的。威慑力 > 技术本身。
或者试试这个技巧 👉 如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)
安全复盘
这次快手事件,其实就死在了一个逻辑上: 后端太信任通过了前端流程的请求。
我们写代码时常犯的错误:
- 前端校验过手机号格式了,后端不用校验了吧?
- 必须点了按钮才能触发这个请求,所以这个接口很安全。
大错特错!
2025 年了,兄弟们。在 Web 的世界里,不相信前端 才是保命法则。
任何从客户端发来的数据,都要默认它是有毒的。
之前我都发过类似的文章:为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!
希望对你们有帮助👆
这次是快手,下次可能就是咱们的公司。
尤其是年底了,黑灰产也要冲业绩(虽然这个业绩有点缺德😖)。
建议大家上班时看看这几件事:
- 查一下核心接口(支付、发帖、推流)有没有做签名校验。
- 看看有没有做频率限制(Rate Limiting),前端后端都要看。
- 搜一下你们的代码仓库,看看有没有把公司的 Key 或者源码传上去(这个真的很常见!)。
前端不只是画页面的,关键时刻,咱们也是安全防线的一部分。
别等到半夜被运维电话叫醒,那时候就真只能甚至想重写简历了🤣。

来源:juejin.cn/post/7586944874526539814
节食正在透支程序员的身体
引言
记得我刚去北京工作的那段时间,由于工作原因开始吃外卖,加上缺乏运动,几个月胖了20斤。
当时心想这不行啊,我怕我拍婚纱照的时候扣不上西服的扣子,我决心减肥。
在我当时的认知里,只要对自己狠一点、饿一饿,就能瘦成理想状态。于是我晚上不吃饭,下班后去健身房跑5公里,1个月的时间瘦了15斤。我很自豪,身边的人说我明显精神多了。
可减肥这事远比我想的复杂,由于没有对应的增肌训练,我发现在做一些力量训练的时候,比之前没减肥前更吃力了。
我这才意识到,自己不仅减掉了脂肪,还减掉了不少肌肉。
我当时完全没有意识到这套方法的问题,也不知道如何科学评估身体组成变化——减肥是成功了,但减的不止是“脂肪”,还有“体能”。
上篇文章提到我对节食减肥的做法并不是特别认可,那科学的方法应该是怎么样的呢,我做了如下调研。
重新理解“减肥”这件事
想系统性地弄清楚减肥到底是怎么回事,我先从最直接的方式开始:看看别人都是怎么做的。
我先去搜了小红书、抖音等平台,内容五花八门,有节食的,有吃减肥药的,也有高强度训练比如HIIT的,还有各种花里胡哨的明星减肥法。
他们动不动就是瘦了十几斤,并且减肥前后的对比非常强烈,我都有种立刻按照他们的方式去试试的冲动。
大部分攻略中都会提到一个关键词“节食”,看来“少吃”几乎成了所有减肥成功者的共识。
我接着去谷歌搜索“节食 减肥”关键字,排名比较靠前的几篇文章是这几篇。

搜索引擎搜出来的一些内容,却讲了一些节食带来的一些不良影响,比如反弹、肌肉流失、代谢下降、饥饿激素紊乱...
这时候我很疑惑,社交媒体上“万人点赞”的有效手段,在官方媒体中的描述,完全不同。
我还需要更多的信息,为此我翻了很多关于节食减肥的书籍。
我在《我们为什么吃(太多)》这本书里看到了一个美国的实验。
美国有一档真人秀节目叫《超级肥胖王》。节目挑选了一些重度肥胖的人,所有参赛者通过高强度节食和锻炼项目,减掉好几十千克的重量。
但研究追踪发现,6年之后,他们平均都恢复了41千克的体重。而且相比六年前,他们的新陈代谢减少了700千卡以上,代谢率严重下降。
有过节食减肥经历的朋友可能都会有过反弹的经历,比如坚持一周较高强度的节食,两天可能就涨回来了。前一阵子一个朋友为了拍婚纱照瘦了很多,最近拍完回了一趟老家,再回北京一称胖了10斤,反弹特别多。
并且有另外一项研究者实验发现,极端节食后,我们体内负责刺激食欲的激素水平比节食前高出了24%,而且进食后获得的饱腹感也更低了。
也就是说你的大脑不知道你正在节食还是遇到了饥荒,所以它会努力的调节体重到之前的水平。
高强度节食是错误的。
正确选项
或许你想问,什么才是正确的减肥方式呢?
正确的做法因人而异,脱离身体状况谈减肥就是耍流氓。
最有参考价值的指标是BMI,我国肥胖的BMI标准为:成人BMI≥28 kg/m²即为肥胖,24.0≤BMI<28.0 kg/m²为超重,BMI<18.5 kg/m²为体重过低,18.5≤BMI<24.0 kg/m²为正常范围。
比如我目前30岁,BMI超过24一点,属于轻微超重。日常生活方式并不是很健康,在办公室对着电脑一坐就是一天。如果我想减肥,首先考虑多运动,如跑步、游泳。
但如果我的BMI达到28,那么就必须要严格控制饮食,叠加大量的有氧运动。
如果针对50岁以上的减肥,思路完全不一致。这个年纪最重要的目标是身体健康,盲目节食会引发额外问题:肌肉流失、骨质疏松、免疫力下降。
这时候更需要的是调整饮食结构,保证身体必要的营养摄入。如果选择运动,要以安全为第一原则,选择徒手深蹲、瑜伽、快走、游泳这些风险性较小的运动。
但无论你什么年龄、什么身体情况,我翻了很多资料,我挑了几种适合各种身体情况的减重方式:

第一个是好好吃。饮食上不能依赖加工食品,比如薯片、面包、饼干,果汁由于含糖量很高,也要少喝。
吃好的同时还要学会感受自己的吃饱感,我们肯定都有过因为眼前的食物太过美味,哪怕肚子已经饱了,我们还是强行让自己多吃两口。
最好的状态就是吃到不饿时停止吃饭,你需要有意识的觉察到自己饱腹感的状态。我亲身实践下来吃饭的时候别刷手机、看视频,对于身体的敏感度就会高很多,更容易感觉到饱腹感。
第二个是多睡。有研究表明缺乏睡眠会导致食欲激素升高,实验中每天睡4.5小时和每天睡8.5小时两组人群,缺觉的人每天会多摄入300千卡的能量。
我很早之前就听过一个词叫“过劳肥”。之前在互联网工作时就见过不少人,你眼看着他入职的时候还很瘦,半年或者一年后就发福了,主要就是经常熬夜或者睡眠不足还会导致内分泌紊乱和代谢异常。
最近一段时间娃晚上熬到11点睡,早上不到七点就起床,直接导致我睡眠不足。最直观的感受就是自己对于情绪控制能力下降了,更容易感受到压力感,因此会希望通过多吃、吃甜食才缓解自己的状态。
第三个就是锻炼。这里就是最简单的能量守恒原则了,只要你运动就会消耗热量,那你说我工作很忙,没时间跑步、跳绳、游泳,还有一个最简单的办法。
那就是坚持每天走一万步,研究表明每天走一万步,就能把肥胖症的风险降低31%,而且这是维护代谢健康最简单的办法了,而且走一万步的好处还有特别多,就不一一说了。
如果一开始一万步太多,那就从每天5000步开始,逐渐增加,每一步都算数。
这三种方法看起来见效慢,却正是打破节食陷阱的长期解法。这也就引出了接下来我想说的,如果节食减肥会反弹人,也有一定的副作用,为什么很多人依然把节食当成减肥的首选呢?
系统性的问题在哪
首先追求确定性和掌控感。节食是一种快速见效的方式,今天饿了一天肚子,明天早上上秤就发现轻了两斤,这种快速反馈和高确定性,会让你更有掌控感。
我在节食+跑步的那段时间,真的是做到了每周都能掉秤,这种反馈就给了我很强的信心。其实工作之后,生活中这样高确定的性的事情已经越来越少了。
节食带来的确定性反馈,就像生活中为数不多还能掌控的事情,让人心甘情愿的付出代价。但我们却很少意识到,看似“自律”的背后,其实正一点点破坏着我们的身体基础。
其次是大部分时候,我们不需要了解身边事物的科学知识。
绝大部分人对营养、代谢的理解非常有限。毕竟我们并不需要详细控制体重的科学方式,体重也能保持的不错。偶尔大吃大喝一段时间,发现自己胖了,稍微控制一下体重也就降回来了。
但一旦你下定决心减肥,简单的理解就远远不够了,你就容易做出错误的判断,比如节食。短期更容易见效,确定性更高,但长远来看只能算下策。
你得有那种看到体检结果突然异常,就赶紧上网查询权威的医学解释一般的态度才行,根据自己的情况用科学的方式控制体重。
而不是只想到节食。
这是东东拿铁的第89篇原创文章,感谢阅读,全文完,喜欢请三连。
来源:juejin.cn/post/7542086955077648434
中国四大软件外包公司
在程序员的职业字典里,每次提到“外包”这两个字,似乎往往带着一种复杂的况味,不知道大家对于这个问题是怎么看的?
包括我们在逛职场社区时,也会经常刷到一些有关外包公司讨论或选择的求职帖子。
的确,在如今的 IT 职场大环境里,对于许多刚入行的年轻人,或者很多寻求机会的开发者来说,外包公司或许也是求职过程中的一个绕不开的备选项。
今天这篇文章,我们先来聊一聊 IT 江湖里经常被大家所提起的“四大软件外包公司”,每次打开招聘软件,相信不少同学都刷到过他们的招聘信息。
他们在业内也一度曾被大家戏称为“外包四大金刚”,可能不少同学也能猜到个大概。
1、中软国际
中软可以说是国内软件外包行业的“老大哥”之一,拥有约 8 万名员工,年收入规模高达 170 亿。
而且中软的业务版图确实很大,在国内外 70 个城市重点布局,在北京、西安、深圳、南京等地均拥有自有产权的研发基地。
提起中软,很多同学的第一反应是它和华为的“深度绑定”。
的确,华为算是中软比较大的合作伙伴之一,同样,这种紧密的合作关系,让中软在通信、政企数字化等领域获得了不少份额。
在中软的体系里,经常能看到一种非常典型的“正规化”打法。它的流程比较规范,制度也非常完善。这对于刚毕业的大学生或者想要转行进入 IT 的人来说,算是一个不错的“练兵场”。
不过近年来,中软也在拼命转型,试图摆脱单纯的外包标签,在 AIGC 和鸿蒙生态上投入了不少精力。
2、软通动力
如果说上面的中软是“稳扎稳打”的代表,那么软通给人的感觉就是“迅猛扩张”。
软通虽然成立时间比中软晚了几年,但发展势头却非常迅猛。
根据第三方机构的数据显示,软通动力在 IT 服务市场的份额已经名列前茅,甚至在某些年份拔得头筹。
软通这家公司一直给人的印象是“大而全”。它的总部在北京,员工规模甚至达到了 90000 人。
而软通动力的上市,一度给行业打了一剂强心针。它的业务线覆盖了从咨询到 IT 服务的全生命周期,包含了金融、能源、智能制造、ICT 软硬件、智能化产品等诸多方面。
3、东软集团
如果说前两家是后来居上的代表,那么东软就是老牌子软件公司的代表。
成立于 1991 年的东软,是中国上市较早的软件公司之一,早在 1996 年就上市了。
东软最初创立于东北大学,后来通过国际合作进入汽车电子领域,并逐渐踏上产业化发展之路,其创始人刘积仁博士也算是软件行业的先驱大佬了。
东软的业务重心很早就放在了医疗健康、智慧城市和汽车电子等这几个领域。
说不定现在很多城市的医院里,跑着的 HIS 系统有可能就是东软做的。
虽然近年来东软也面临着转型阵痛,但它在医疗和智慧城市等领域的积淀,依然是其他外包公司难以撼动的。
4、文思海辉(中电金信)
这家公司的发展历程比较特殊,它经历过文思创新和海辉软件的合并,后来又加入了中国电子(CEC)的阵营,成为中国电子旗下的一员,并且后来又进一步整合为了中电金信。
所以它现在更多地以“中电金信”的身份出现。
文思海辉的强项在于金融和数智化领域,尤其银行业 IT 项目这一块做了非常多,市场份额也很大。
那除了上面这几个“外包巨头”之外,其实很多领域还有很多小型外包公司,有的是人力资源外包,有的则是项目外包。
每次提到「外包」这个词,可能不少同学都会嗤之以鼻,那这里我也来聊聊我自己对于外包的一些个人看法和感受。
说实话,我没有进过外包公司干过活,但是呢,我和不少外包公司的工作人员共事过,一起参与过项目。
记得老早之前我在通信公司工作时,我们团队作为所谓的“甲方”,就和外包员工共事过有大半年的样子,一起负责公司的核心网子项目。
有一说一,我们团队整体对外包同事都是非常友好的。
我看网上有那种什么外包抢了红包要退钱、什么提醒外包注意素质不要偷吃的零食的事情,有点太离谱、太夸张了,这在我们团队那会是从来没有发生过的。
大家平时在一起上班的氛围也挺融洽,大家一起该聊天聊天,该开玩笑开玩笑,该一起吃饭一起吃饭,在相处方面并没有什么区别。
但是,不同地方的确也有。
比方说,他们上班时所带的工牌带子颜色就和我们不太一样,这一眼就能看出来,另外平时做的事情也有点不太一样。
我记得当时项目的一些抓包任务、测试任务、包括一些标注任务等等都是丢给外包同事那边来完成,我们需要的是结果、是报告。
另外对于项目文档库和代码库的权限也的确有所不同,核心项目代码和文档确实是不对外包同事那边开放的。
除此之外,我倒并没有觉得有什么太多的不同。
那作为程序员,我们到底该如何看待这些外包公司呢?
这就好比是一个围城,城外的人有的想进去,城里的人有的想出来。
每次一提到外包,很多人的建议都是不要进,打亖别去。但是,这里有个前提是,首先得在你有的选的情况下,再谈要不要选的问题。
不可否认的是,外包公司确实有它的短板。最被人诟病的两点,一个“职业天花板”问题、一个“归属感缺失”问题。
但是在当下的就业环境里,我们不得不承认的是,外包公司也承担了 IT 行业“蓄水池”的角色。
毕竟并不是每个人一毕业就能拿到互联网大厂的 offer,也并不是每个人都有勇气去创业公司搏一把。
对于有些学历一般、技术基础一般或者刚转行的程序员来说,外包也提供了另外一个选择。
而如果你现在正在外包或者正在考虑加入外包,那这里我也想说几句肺腑之言:
第一,不要把外包作为职业生涯的终点,而应该把它看作一个跳板或过渡。
如果你刚毕业进不去大厂,或者在一二线城市没有更好的选择,那外包可以为你提供一个接触正规项目流程的机会(当然前提是要进那种正规的外包),我们也可以把它看昨一个特殊的职场驿站。
在那里的每一天,你都要问问自己:我学到了什么?我的技术有没有长进?我的视野有没有开阔?
第二,一定要警惕“舒适区”。
很多同学在外包待久了,可能会陷入一种拿工资办事的机械式工作中,看起来很舒适,实际上很危险。
注意,一定要利用能接触到的资源,去学习项目的技术架构和业务流程,去想办法提升自己的核心竞争力,而不是仅仅为了完成工时。
最后我想说的是,无论你是在大厂做正式员工,还是在小团队里打拼,亦或是在外包公司里默默耕耘,最终决定职业高度的,并不是工牌上公司的名字,而是会多少技术,懂多少业务,能解决多少问题,大家觉得呢?
好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7585839411122454574
那个把代码写得亲妈都不认的同事,最后被劝退了🤷♂️
大家好😁。
上上周,我们在例会上送别了团队里的一位技术大牛,阿K。
说实话,阿K 的技术底子很强。他能手写 Webpack 插件,熟读 ECMA 规范,对 Chrome 的渲染管线了如指掌。
但最终,CTO 还是决定劝退他了。

理由很残酷,只有一句话: 你的代码,团队里没人敢接手。🤷♂️
为了所谓的极致性能,牺牲代码的可读性,到底值不值?
事件的开始
我们有一个很普通的后台管理系统重构。
阿K 负责最核心的权限校验模块。这本是一个很简单的逻辑:后端返回一个权限列表,前端判断一下用户有没有某个按钮的权限。
普通人(比如我)大概会这么写:
// 一眼就能看懂
const hasPermission = (userPermissions, requiredPermission) => {
return userPermissions.includes(requiredPermission);
};
if (hasPermission(currentUser.permissions, 'DELETE_USER')) {
showDeleteButton();
}
但是,阿K 看到这段代码时,露出了鄙夷的神情😒。
includes 这种遍历操作太慢了!我们要处理的是十万级的用户并发(并没有),必须优化!
于是,他闭关三天,重写了整个模块。
Code Review 的时候,我们所有人都傻了。屏幕上出现了一堆我们看不懂的天书😖:
// 全程位运算,没有任何注释
const P = { r: 1, w: 2, e: 4, d: 8 };
const _c = (u, p) => (u & p) === p;
// 这里甚至用了一个位移掩码生成的哈希表
const _m = (l) => l.reduce((a, c) => a | (P[c] || 0), 0);
// 像不像一段乱码?
const chk = (u, r) => _c(_m(u.r), P[r]);
我问他:阿K,这 _c 和 _m 是啥意思?能加个注释吗?
阿K 振振有词: 好的代码不需要注释!位运算是计算机执行最快的操作,比字符串比对快几百倍!这不仅仅是代码,这是对 CPU 的尊重,是艺术!
我: 。 。 。 。🤣
在那个没有性能瓶颈的后台管理系统里,他为了那肉眼不可见的 0.0001 毫秒提升,制造了一个维护麻烦。
屎山💩崩溃的那一天
灾难发生在两个月后。
业务方突然提了一个需求: 权限逻辑要改,现在支持‘反向排除’权限,而且权限字段要从数字改成字符串组。
那天,阿K 正好去年假了,手机关机😒。
任务落到了刚入职的实习生小李头上。
小李打开 permission.js,看着满屏的 >>、&、| 和单字母变量,整个人僵在了工位上。
他试图去理解那个位移掩码的逻辑,但他发现,只要改动一个字符,整个系统的权限就全乱套了——管理员突然看不了页面,实习生突然能删库了🤔。
这代码有毒吧…… 小李在第 10 次尝试修复失败后,差点哭出来😭。
因为这个模块的逻辑过于晦涩,且和其他模块高度耦合(阿K 为了复用,把这些位运算逻辑注入到了全局原型链里),我们根本不敢动。
结果是:那个简单的需求,被硬生生拖了一周。 业务方投诉到了 CTO 那里。
CTO 看了眼代码,沉默了三分钟,然后问了一句:
写这玩意儿的人,是觉得以后都不用维护了吗?😥
过早优化是万恶之源 !
阿K 回来后,很不服气。他觉得是我们技术太菜,看不懂他的高级操作。
他拿出了 Chrome Profiler 的截图,指着那微乎其微的差距说:看!我的写法比你们快了 40%!
但他忽略了软件工程中最重要的一条公式:
代码价值 = (实现功能 + 可维护性) / 复杂度
过早优化是万恶之源 ! ! !
在 99% 的业务场景下,V8 引擎已经足够快了。
- 你把
forEach改成while倒序循环,性能确实提升了,但代码变得难读了。 - 你把清晰的
switch-case改成了晦涩的lookup table还没有类型提示,Bug 率上升了。 - 你为了省几个字节的内存,用各种黑魔法操作对象,导致后来的人根本不敢碰😖。
这种所谓的性能优化,其实是程序员的自嗨。
它是用团队的维护成本,去换取机器那一瞬间的快感。它不是优化,它是给项目埋雷。
什么样的代码才是好代码?
后来,我们将阿K 的那坨代码 通过 chatGPT 全部推倒重写。
1️⃣ 权限定义(语义清晰)
// permissions.ts
export enum Permission {
READ = 'read',
WRITE = 'write',
EDIT = 'edit',
DELETE = 'delete',
}
2️⃣ 用户模型
// user.ts
import { Permission } from './permissions';
export interface User {
id: string;
permissions: Permission[];
}
3️⃣ 权限校验函数(核心)
// auth.ts
import { Permission } from './permissions';
import { User } from './user';
export function hasPermission(
user: User,
required: Permission
): boolean {
return user.permissions.includes(required);
}
4️⃣ 批量权限校验
export function hasAllPermissions(
user: User,
required: Permission[]
): boolean {
return required.every(p => user.permissions.includes(p));
}
export function hasAnyPermission(
user: User,
required: Permission[]
): boolean {
return required.some(p => user.permissions.includes(p));
}
5️⃣ 判断方法
if (!hasPermission(user, Permission.DELETE)) {
throw new Error('No permission to delete');
}
用回了用户权限结构清晰可见的,权限判断,一眼就懂。甚至都不需要注释🤷♂️
虽然跑分慢了那么一丁点(用户根本无感知),但任何一个新来的同事,只要 5 分钟就能看懂并上手修改。
这件事给我留下了深刻的教训:
- 代码是写给人看的,顺便给机器运行。
如果一段代码只有你现在能看懂,那它就是垃圾代码;如果一段代码连你一个月后都看不懂,那它就是有害代码。
- 不要在非瓶颈处炫技。
如果页面卡顿是因为 DOM 节点太多,你去优化 JS 的变量赋值速度,那就是隔靴搔痒。找到真正的瓶颈(Network, Layout, Paint),再对症下药。
- 可读性 > 巧技。
简单的逻辑,是对同事最大的善意。
阿K 走的时候,还是觉得自己怀才不遇,觉得这家公司配不上他的技术🤣。
我祝他未来前程似锦。
但我更希望看到这篇文章的你,下次在想要按下键盘写一段绝妙的、只有你看懂的单行代码时,能停下来想一想:
如果明天我离职了,接手的人会不会骂娘?

毕竟,我们不想让亲妈都不认识代码,我们更不想让同事在那骂娘。
谢谢大家👏
来源:juejin.cn/post/7585897699603693594
Arco Design 停摆!字节跳动 UI 库凉了?
1. 引言:设计系统的“寒武纪大爆发”与 Arco 的陨落
在 2019 年至 2021 年间,中国前端开发领域经历了一场前所未有的“设计系统”爆发期。伴随着企业级 SaaS 市场的崛起和中后台业务的复杂度攀升,各大互联网巨头纷纷推出了自研的 UI 组件库。这不仅是技术实力的展示,更是企业工程化标准的话语权争夺。在这一背景下,字节跳动推出了 Arco Design,这是一套旨在挑战 Ant Design 霸主地位的“双栈”(React & Vue)企业级设计系统。
Arco Design 在发布之初,凭借其现代化的视觉语言、对 TypeScript 的原生支持以及极具创新性的“Design Lab”设计令牌(Design Token)管理系统,迅速吸引了大量开发者的关注。它被定位为不仅仅是一个组件库,而是一套涵盖设计、开发、工具链的完整解决方案。然而,就在其社区声量达到顶峰后的短短两两年内,这一曾被视为“下一代标准”的项目却陷入了令人费解的沉寂。
截至 2025 年末,GitHub 上的 Issues 堆积如山,关键的基础设施服务(如 IconBox 图标平台)频繁宕机,官方团队的维护活动几乎归零。对于数以万计采用了 Arco Design 的企业和独立开发者而言,这无疑是一场技术选型的灾难。
本文将深入剖析 Arco Design 从辉煌到停摆的全过程。我们将剥开代码的表层,深入字节跳动的组织架构变革、内部团队的博弈(赛马机制)、以及中国互联网大厂特有的“KPI 开源”文化,为您还原整件事情的全貌。
2. 溯源:Arco Design 的诞生背景与技术野心
要理解 Arco Design 为何走向衰败,首先必须理解它诞生时的宏大野心及其背后的组织推手。Arco 并不仅仅是一个简单的 UI 库,它是字节跳动在高速扩张期,为了解决内部极其复杂的国际化与商业化业务需求而孵化的产物。

2.1 “务实的浪漫主义”:差异化的产品定位
Arco Design 在推出时,鲜明地提出了“务实的浪漫主义”这一设计哲学。这一口号的提出,实际上是为了在市场上与阿里巴巴的 Ant Design 进行差异化竞争。
- Ant Design 的困境:作为行业标准,Ant Design 以“确定性”著称,其风格克制、理性,甚至略显单调。虽然极其适合金融和后台管理系统,但在需要更强品牌表达力和 C 端体验感的场景下显得力不从心。
- Arco 的切入点:字节跳动的产品基因(如抖音、TikTok)强调视觉冲击力和用户体验的流畅性。Arco 试图在中后台系统中注入这种基因,主张在解决业务问题(务实)的同时,允许设计师发挥更多的想象力(浪漫)。
这种定位在技术层面体现为对 主题定制(Theming) 的极致追求。Arco Design 并没有像传统库那样仅仅提供几个 Less 变量,而是构建了一个庞大的“Design Lab”平台,允许用户在网页端通过可视化界面细粒度地调整成千上万个 Design Token,并一键生成代码。这种“设计即代码”的早期尝试,是 Arco 最核心的竞争力之一。
2.2 组织架构:GIP UED 与架构前端的联姻
Arco Design 的官方介绍中明确指出,该系统是由 字节跳动 GIP UED 团队 和 架构前端团队(Infrastructure FrontEnd Team) 联合推出的。这一血统注定了它的命运与“GIP”这个业务单元的兴衰紧密绑定。
2.2.1 GIP 的含义与地位
“GIP” 通常指代 Global Internet Products(全球互联网产品)或与之相关的国际化/商业化业务部门。在字节跳动 2019-2021 年的扩张期,这是一个充满活力的部门,负责探索除了核心 App(抖音/TikTok)之外的各种创新业务,包括海外新闻应用(BuzzVideo)、办公套件、以及各种尝试性的出海产品。
- UED 的话语权:在这一时期,GIP 部门拥有庞大的设计师团队(UED)。为了统一各条分散业务线的设计语言,UED 团队急需一套属于自己的设计系统,而不是直接沿用外部的 Ant Design。
- 技术基建的配合:架构前端团队的加入,为 Arco Design 提供了工程化落地的保障。这种“设计+技术”的双驱动模式,使得 Arco 在初期展现出了极高的完成度,不仅有 React 版本,还同步推出了 Vue 版本,甚至包括移动端组件库。
2.3 黄金时代的技术堆栈
在 2021 年左右,Arco Design 的技术选型是极具前瞻性的,这也是它能迅速获得 5.5k Star 的原因之一:
- 全链路 TypeScript:所有组件均采用 TypeScript 编写,提供了优秀的类型推导体验,解决了当时 Ant Design v4 在某些复杂场景下类型定义不友好的痛点。
- 双框架并进:@arco-design/web-react 和 @arco-design/web-vue 保持了高度统一的 API 设计和视觉风格。这对于那些技术栈不统一的大型公司极具吸引力,意味着设计规范可以跨框架复用。
- 生态闭环:除了组件库,Arco 还发布了 arco-cli(脚手架)、Arco Pro(中后台模板)、IconBox(图标管理平台)以及 Material Market(物料市场)。这表明团队不仅是在做一个库,而是在构建一个类似 Salesforce Lightning 或 SAP Fiori 的企业级生态。
然而,正是这种庞大的生态铺设,为日后的维护埋下了巨大的隐患。当背后的组织架构发生震荡时,维持如此庞大的产品矩阵所需的资源将变得不可持续。
3. 停摆的证据:基于数据与现象的法医式分析
尽管字节跳动从未发布过一份正式的“Arco Design 停止维护声明”,但通过对代码仓库、社区反馈以及基础设施状态的深入分析,我们可以断定该项目已进入实质性的“脑死亡”状态。
3.1 代码仓库的“心跳停止”
对 GitHub 仓库 arco-design/arco-design (React) 和 arco-design/arco-design-vue (Vue) 的提交记录分析显示,活跃度在 2023 年底至 2024 年初出现了断崖式下跌。

3.1.1 提交频率分析
虽然 React 版本的最新 Release 版本号为 2.66.8(截至文章撰写时),但这更多是惯性维护。
- 核心贡献者的离场:早期的高频贡献者(如 sHow8e、jadelike-wine 等)在 2024 年后的活跃度显著降低。许多提交变成了依赖项升级(Dependabot)或极其微小的文档修复,缺乏实质性的功能迭代。
- Vue 版本的停滞:Vue 版本的状态更为糟糕。最近的提交多集中在构建工具迁移(如迁移到 pnpm)或很久以前的 Bug 修复。核心组件的 Feature Request 长期无人响应。
3.1.2 积重难返的 Issue 列表
Issue 面板是衡量开源项目生命力的体温计。目前,Arco Design 仓库中积累了超过 330 个 Open Issue。
- 严重的 Bug 无人修复:例如 Issue #3091 “tree-select 组件在虚拟列表状态下搜索无法选中最后一个” 和 Issue #3089 “table 组件的 default-expand-all-rows 属性设置不生效”。这些都是影响生产环境使用的核心组件 Bug,却长期处于 Open 状态。
- 社区的绝望呐喊:Issue #3090 直接以 “又一个没人维护的 UI 库” 为题,表达了社区用户的愤怒与失望。更有用户在 Discussion 中直言 “这个是不是 KPI 项目啊,现在维护更新好像都越来越少了”。这种负面情绪的蔓延,通常是一个项目走向终结的社会学信号。
3.2 基础设施的崩塌:IconBox 事件
如果说代码更新变慢还可以解释为“功能稳定”,那么基础设施的故障则是项目被放弃的直接证据。
- IconBox 无法发布:Issue #3092 指出 “IconBox 无法发布包了”。IconBox 是 Arco 生态中用于管理和分发自定义图标的 SaaS 服务。这类服务需要后端服务器、数据库以及运维支持。
- 含义解读:当一个大厂开源项目的配套 SaaS 服务出现故障且无人修复时,这不仅仅是开发人员没时间的问题,而是意味着服务器的预算可能已经被切断,或者负责运维该服务的团队(GIP 相关的基建团队)已经被解散。这是项目“断供”的最强物理证据。
3.3 文档站点的维护降级
Arco Design 的文档站点虽然目前仍可访问,但其内容更新已经明显滞后。例如,关于 React 18/19 的并发特性支持、最新的 SSR 实践指南等现代前端话题,在文档中鲜有提及。与竞争对手 Ant Design 紧跟 React 官方版本发布的节奏相比,Arco 的文档显得停留在 2022 年的时光胶囊中。
4. 深层归因:组织架构变革下的牺牲品
Arco Design 的陨落,本质上不是技术失败,而是组织架构变革的牺牲品。要理解这一点,我们需要将视线从 GitHub 移向字节跳动的办公大楼,审视这家巨头在过去三年中发生的剧烈动荡。

4.1 “去肥增瘦”战略与 GIP 的解体
2022 年至 2024 年,字节跳动 CEO 梁汝波多次强调“去肥增瘦”战略,旨在削减低效业务,聚焦核心增长点。这一战略直接冲击了 Arco Design 的母体——GIP 部门。
4.1.1 战略投资部的解散与业务收缩
2022 年初,字节跳动解散了战略投资部,并将原有的投资业务线员工分流。这一动作标志着公司从无边界扩张转向防御性收缩。紧接着,教育(大力教育)、游戏(朝夕光年)以及各类边缘化的国际化尝试业务(GIP 的核心腹地)遭遇了毁灭性的裁员。
4.1.2 GIP 团队的消失
在多轮裁员中,GIP 及其相关的商业化技术团队是重灾区。
- 人员流失:Arco Design 的核心维护者作为 GIP UED 和架构前端的一员,极有可能在这些轮次的“组织优化”中离职,或者被转岗到核心业务(如抖音电商、AI 模型 Doubao)以保住职位。
- 业务目标转移:留下来的人员也面临着 KPI 的重置。当业务线都在为生存而战,或者全力以赴投入 AI 军备竞赛时,维护一个无法直接带来营收的开源 UI 库,显然不再是绩效考核中的加分项,甚至是负担。
4.2 内部赛马机制:Arco Design vs. Semi Design
字节跳动素以“APP 工厂”和“内部赛马”文化著称。这种文化不仅存在于 C 端产品中,也渗透到了技术基建领域。Arco Design 的停摆,很大程度上是因为它在与内部竞争对手 Semi Design 的博弈中败下阵来。
4.2.1 Semi Design 的崛起
Semi Design 是由 抖音前端团队 与 MED 产品设计团队 联合推出的设计系统。
- 出身显赫:与 GIP 这个边缘化的“探索型”部门不同,Semi Design 背靠的是字节跳动的“现金牛”——抖音。抖音前端团队拥有极其充裕的资源和稳固的业务地位。
- 业务渗透率:Semi Design 官方宣称支持了公司内部“近千个平台产品”,服务 10 万+ 用户。它深度嵌入在抖音的内容生产、审核、运营后台中。这些业务是字节跳动的生命线,因此 Semi Design 被视为“核心资产”。
4.2.2 为什么 Arco 输了?
在资源收缩期,公司高层显然不需要维护两套功能高度重叠的企业级 UI 库。选择保留哪一个,不仅看技术优劣,更看业务绑定深度。
- 技术路线之争:Semi Design 在 D2C(Design-to-Code)领域走得更远,提供了强大的 Figma 插件,能直接将设计稿转为 React 代码。这种极其强调效率的工具链,更符合字节跳动“大力出奇迹”的工程文化。
- 归属权:Arco 属于 GIP,GIP 被裁撤或缩编;Semi 属于抖音,抖音如日中天。这几乎是一场没有悬念的战役。当 GIP 团队分崩离析,Arco 自然就成了没人认领的“孤儿”。
4.3 中国大厂的“KPI 开源”陷阱
Arco Design 的命运也折射出中国互联网大厂普遍存在的“KPI 开源”现象。
- 晋升阶梯:在阿里的 P7/P8 或字节的 2-2/3-1 晋升答辩中,主导一个“行业领先”的开源项目是极具说服力的业绩。因此,很多工程师或团队 Leader 会发起此类项目,投入巨大资源进行推广(刷 Star、做精美官网)。
- 晋升后的遗弃:一旦发起人成功晋升、转岗或离职,该项目的“剩余价值”就被榨干了。接手的新人往往不愿意维护“前人的功劳簿”,更愿意另起炉灶做一个新的项目来证明自己。
- Arco 的轨迹:Arco 的高调发布(2021年)恰逢互联网泡沫顶峰。随着 2022-2024 年行业进入寒冬,晋升通道收窄,维护开源项目的 ROI(投入产出比)变得极低,导致项目被遗弃。
5. 社区自救的幻象:为何没有强有力的 Fork?
面对官方的停摆,用户自然会问:既然代码是开源的(MIT 协议),为什么没有人 Fork 出来继续维护?调查显示,虽然存在一些零星的 Fork,但并未形成气候。

5.1 Fork 的现状调查
通过对 GitHub 和 Gitee 的检索,我们发现了一些 Fork 版本,但并未找到具备生产力的社区继任者。
- vrx-arco:这是一个名为 vrx-arco/arco-design-pro 的仓库,声称是 "aro-design-vue 的部分功能扩展"。然而,这更像是一个补丁集,而不是一个完整的 Fork。它主要解决特定开发者的个人需求,缺乏长期维护的路线图。
- imoty_studio/arco-design-designer:这是一个基于 Arco 的表单设计器,并非组件库本身的 Fork。
- 被动 Fork:GitHub 显示 Arco Design 有 713 个 Fork。经抽样检查,绝大多数是开发者为了阅读源码或修复单一 Bug 而进行的“快照式 Fork”,并没有持续的代码提交。
5.2 为什么难以 Fork?
维护一个像 Arco Design 这样的大型组件库,其门槛远超普通开发者的想象。
- Monorepo 构建复杂度:Arco 采用了 Lerna + pnpm 的 Monorepo 架构,包含 React 库、Vue 库、CLI 工具、图标库等多个 Package。其构建脚本极其复杂,往往依赖于字节内部的某些环境配置或私有源。外部开发者即使拉下来代码,要跑通完整的 Build、Test、Doc 生成流程都非常困难。
- 生态维护成本:Arco 的核心优势在于 Design Lab 和 IconBox 等配套 SaaS 服务。Fork 代码容易,但 Fork 整个后端服务是不可能的。失去了 Design Lab 的 Arco,就像失去了灵魂的空壳,吸引力大减。
- 技术栈锁定:Arco 的一些底层实现可能为了适配字节内部的微前端框架或构建工具(如 Modern.js)做了特定优化,这增加了通用化的难度。
因此,社区更倾向于迁移,而不是接盘。
6. 用户生存指南:现状评估与迁移策略
对于目前仍在使用 Arco Design 的团队,局势十分严峻。随着 React 19 的临近和 Vue 3 生态的演进,Arco 将面临越来越多的兼容性问题。
6.1 风险评估表
| 风险维度 | 风险等级 | 具体表现 |
|---|---|---|
| 安全性 | 🔴 高危 | 依赖的第三方包(如 lodash, async-validator 等)若爆出漏洞,Arco 不会发版修复,需用户手动通过 resolutions 强行覆盖。 |
| 框架兼容性 | 🔴 高危 | React 19 可能会废弃某些 Arco 内部使用的旧生命周期或模式;Vue 3.5+ 的新特性无法享受。 |
| 浏览器兼容性 | 🟠 中等 | 新版 Chrome/Safari 的样式渲染变更可能导致 UI 错位,无人修复。 |
| 基础设施 | ⚫ 已崩溃 | IconBox 无法上传新图标,Design Lab 可能随时下线,导致主题无法更新。 |

6.2 迁移路径推荐
方案 A:迁移至 Semi Design(推荐指数:⭐⭐⭐⭐)
如果你是因为喜欢字节系的设计风格而选择 Arco,那么 Semi Design 是最自然的替代者。
- 优势:同为字节出品,设计语言的命名规范和逻辑有相似之处。Semi 目前维护活跃,背靠抖音,拥有强大的 D2C 工具链。
- 劣势:API 并非 100% 兼容,仍需重构大量代码。且 Semi 主要是 React 优先,Vue 生态支持相对较弱(主要靠社区适配)。

方案 B:迁移至 Ant Design v5/v6(推荐指数:⭐⭐⭐⭐⭐)
如果你追求极致的稳定和长期的维护保障,Ant Design 是不二之选。
- 优势:行业标准,庞大的社区,Ant Gr0up 背书。v5 版本引入了 CSS-in-JS,在定制能力上已经大幅追赶 Arco 的 Design Lab。
- 劣势:设计风格偏保守,需要设计师重新调整 UI 规范。
方案 C:本地魔改(推荐指数:⭐)
如果项目庞大无法迁移,唯一的出路是将 @arco-design/web-react 源码下载到本地 packages 目录,作为私有组件库维护。
- 策略:放弃官方更新,仅修复阻塞性 Bug。这需要团队内有资深的前端架构师能够理解 Arco 的源码。

7. 结语与启示
Arco Design 的故事是现代软件工程史上的一个典型悲剧。它证明了在企业级开源领域,康威定律(Conway's Law) 依然是铁律——软件的架构和命运取决于开发它的组织架构。
当 GIP 部门意气风发时,Arco 是那颗最耀眼的星,承载着“务实浪漫主义”的理想;当组织收缩、业务调整时,它便成了由于缺乏商业造血能力而被迅速遗弃的资产。对于技术决策者而言,Arco Design 的教训是惨痛的:在进行技术选型时,不能仅看 README 上的 Star 数或官网的精美程度,更要审视项目背后的组织生命力和维护动机。

目前来看,Arco Design 并没有复活的迹象,社区也没有出现强有力的接棒者。这套组件库正在数字化浪潮的沙滩上,慢慢风化成一座无人问津的丰碑。
来源:juejin.cn/post/7582879379441745963
桌面应用开发,Flutter 与 Electron如何选
前言:这一年来我基本处于断更的状态,我知道在AI时代,编码的成本已经变得越来越低,技术分享的流量必然会下降。但这依然是一个艰难的过程,日常斥责自己没有成长,没有作品。
除了流量问题、巨量的工作,更多的原因是由于技术栈的变化。我开始使用Electron编写一个重要的AI产品,并且在 Flutter 与 Electron 之间来回拉扯......
背景
我们对 Flutter 技术的应用,不仅是在移动端APP,在我们的终端设备也用来做 OS 应用,跨Android、Windows、Linux系统。
在 Flutter 上,我们是有所沉淀的,但是当我们决定研发一款重要的PC应用时,依然产生了疑问:Flutter 这门技术,真的能满足我们在核心桌面应用的研发需求吗?
最终,基于官方能力、技术生态、roadmap等一系列原因,我们放弃在核心应用上使用 Flutter,转而代之选择了 Electron !
这篇文章将从这几个月使用 Electron 的切实体验,从不同角度,对 Flutter 和 Electron 这两款支持跨端桌面应用开发技术,做一个详细的对比。
Flutter VS Electron
| 维度 | Flutter | Electron |
|---|---|---|
| 发布时间 | 2021 年 3 月 宣布支持桌面端 | 2013 年 4 月发布,发布即支持 |
| 核心场景 | 移动APP跨端 | 桌面应用跨端 |
| 官方网站 | flutter.dev | http://www.electronjs.org |
| 开发文档 | docs.flutter.dev | http://www.electronjs.org/docs |
| 插件包管理 | Pub(pub.dev),提供大量 UI 组件、工具类库 | npm(http://www.npmjs.com),依赖前端生态,插件丰富(如 electron-builder 打包工具) |
| 研发组织 | Github |
方案成熟度
毫无疑问,在方案成熟度上 Electron 是碾压 Flutter 的存在。
1. 多进程能力
- Flutter 目前还是单进程的能力,只能通过创建 isolate 来实现部分耗时任务,但是内存也是不共享的。
- Electron 集成了 Nodejs 服务,
自带多进程的能力,且提供了完整的跨进程机制(IPC「Inter-Process Communication」)。
2. 多窗口支持
- Flutter 目前不支持多窗口。由于其是自绘引擎,本身还是依赖原生进程提供的桌面窗口,所以需要原生与 Flutter 引擎不断的进行沟通对接,才能很好的使用多窗口能力。
目前官方只是提供了 demo 来验证多窗口的可行性,但截止发文还没有办法在公版试用。 - Electron 将 Chromium 的核心模块打包到发行包中,
借助浏览器的能力,可以随意开辟新的窗口(如: BrowserWindow)
3. 开发语言
- Flutter 使用dart语言开发,采用声明式UI进行布局,插件管理使用官方的 pub 社区,学习和使用成本不算高。
- Electron 使用JavaScript/TypeScript + HTML/CSS 的前端技术栈进行开发,社区也完全跟前端一致,
非常丰富但鱼龙混杂。
4. 原生能力的支持
- Flutter 本质是一个 UI 框架,原生能力需要通过编写插件去调用,或者通过 FFI 调用,成本是很高的,你很难找到一个懂多端原生技术的开发。
- Electron 有 node 环境,node.js 很多原生模块,可以直接调用到系统的能力,非常的高效。
开发体验和技术生态
1. 调试工具
- Flutter 的调试工具,主要是依赖 IDE 本身的断点调试能力,以及自研的Flutter Inspector、devTools。
在UI定位、性能监控方面,基本可以满足。但由于是个 UI 框架,对于原生容器是无法进行调试的,这在混合开发过程中是个比较大的痛点。 - Electron 就是个浏览器,对于主进程和node子进程,有 Inspect 的机制; UI 层就更方便了,就是浏览器的调试器一模一样。生产环境下调试成本也低。
2. 打包编译
Flutter 是通过自绘引擎生成原生应用包,而 Electron 是将网页技术(HTML/CSS/JS)包裹在 Chromium 内核中。
底层技术架构的区别,直接决定了 Electron 的打包相对 Flutter 有些困难,且包体积很大。
| 对比维度 | Flutter | Electron |
|---|---|---|
| 打包原理 | 编译成目标平台的原生二进制代码,搭配自绘引擎(Skia) | 封装 Chromium 内核 + Node.js 环境,运行网页资源 |
| 最终产物 | 与原生应用格式一致(如 .apk/.ipa/.exe) | 包含浏览器内核的独立应用包 |
| 跨平台方式 | 一份代码编译成多平台原生包,需分别打包 | 一份代码打包成多平台包,内核随应用分发 |
| 应用体积 | 较小(基础包约 10-20MB) | 较大(基础包约 50-100MB,内核占主要体积) |
3. 官方和社区的活跃性
- Flutter 官方在桌面端的推进很慢,很多基础能力都没有太多的推进。同时在 roadmap 中,重心都偏向移动端和 web 端。
- Electron 由于产品的体量和成熟度,稳定的在更新,每个版本都会带来一些新的特性。

4. 研发团队
| 技能维度 | Flutter | Electron |
|---|---|---|
| 核心语言 | Dart,需理解其异步逻辑、Widget 组件化思想 | JavaScript/TypeScript,前端开发者可无缝衔接 |
| UI 技术 | Flutter 内置 Widget 体系,需学习其布局(Row/Column)、状态管理(Provider/Bloc) | HTML/CSS,可复用前端生态(Vue/React/Element UI 等) |
| 原生交互 | 需了解 Android(Kotlin/Java)、iOS(Swift/OC)基础,复杂功能需写原生插件 | 依赖 Node.js 模块或现成插件,无需深入原生开发 |
| 工程化工具 | 依赖 Flutter CLI、Android Studio/Xcode(打包配置) | 依赖 npm/yarn、webpack/vite(前端构建工具) |
可以看出,Flutter至少需要 1-2 名熟悉 Dart 的开发者,还需要有原生开发能力,技术门槛是比较高的;而 Electron 以前端开发者为主,熟悉 Node.js 即可完成所有开发,是可以快速上手的。
同时前端开发也比 Flutter 开发要更容易招聘。
结语
笔者本身是 Flutter 的忠实维护者,我认为 Flutter 的 Impeller 图形渲染引擎将不断完善,能在各个端达到更好的渲染速度和效果;同时 Flutter 目前的多窗口方案,让我们可以充分的相信可以多个窗口共用一份内存,而不需要通过进程间通信机制
但是,在 Flutter 暂未成熟的阶段,桌面核心产品还是用 Electron 进行开发会更加合适。我们 也期待未来 Electron 可以多集成WebAssembly来提升计算密集型任务的性能,减少 Chromium 内核的高内存占用。
来源:juejin.cn/post/7578719771589066762
偷看浏览器后台,发现它比我忙多了
为啥以前 IE 老“崩溃”,而现在开一堆标签页的 Chrome 还能比较稳?答案都藏在浏览器的「进程模型」里。
作为一个还在学习前端的同学,我经常听到几个关键词:
进程、线程、多进程浏览器、渲染进程、V8、事件循环……
下面就是我目前的理解,算是一篇学习笔记式的分享
一、先把概念捋清:进程 vs 线程
- 进程(Process)
- 操作系统分配资源的最小单位。
- 拥有独立的内存空间、句柄、文件等资源。
- 不同进程间默认互相隔离,通信要通过 IPC。
- 线程(Thread)
- CPU 调度、执行代码的最小单位。
- 共享所属进程的资源(内存、文件句柄等)。
- 一个进程里可以有多个线程并发执行。
简单理解:
- 进程 = 一个“应用实例” (开一个浏览器窗口就是一个进程)。
- 线程 = 应用里的很多“小工人” (一个负责渲染页面,一个负责网络请求……)。
二、单进程浏览器:旧时代的 IE 模型
早期的浏览器(如旧版 IE)基本都是单进程架构:整个浏览器只有一个进程,里面开多个线程来干活。
可以想象成这样一张图(对应你给的“单进程浏览器”那张图):
- 顶部是「代码 / 数据 / 文件」等资源。
- 下面有多个线程:
- 页面线程:负责页面渲染、布局、绘制。
- 网络线程:负责网络请求。
- 其他线程:例如插件、定时任务等。
- 所有线程共享同一份进程资源。
在这个模型下:
- 页面渲染、JavaScript 执行、插件运行,都挤在同一个进程里。
- 某个插件或脚本一旦崩溃、死循环、内存泄漏,整个浏览器都会被拖垮。
- 多开几个标签页,本质上依旧是同一个进程里的不同页面线程, “一荣俱荣,一损俱损” 。
这也是很多人对 IE 的经典印象:
“多开几个页面就卡死,崩一次,所有标签页一起消失”。
三、多进程浏览器:Chrome 的现代架构
Chrome 采用的是多进程、多线程混合架构。打开浏览器时,大致会涉及这些进程:
- 浏览器主进程(Browser Process)
- 负责浏览器 UI、地址栏、书签、前进后退等。
- 管理和调度其他子进程(类似一个大管家)。
- 负责部分存储、权限管理等。
- 渲染进程(Render Process)
- 核心任务:把 HTML / CSS / JavaScript 变成用户可以交互的页面。
- 布局引擎(如 Blink)和 JS 引擎(如 V8)都在这里。
- 默认情况下,每个标签页会对应一个独立的渲染进程。
- GPU 进程(GPU Process)
- 负责 2D / 3D 绘制和加速(动画、3D 变换等)。
- 统一为浏览器和各渲染进程提供 GPU 服务。
- 网络进程(Network Process)
- 负责资源下载、网络请求、缓存等。
- 插件进程(Plugin Process)
- 负责运行如 Flash、扩展等插件代码,通常放在更严格的沙箱里。
你给的第一张图,其实就是这么一个多进程架构示意图:中间是主进程,两侧是渲染、网络、GPU、插件等子进程,有的被沙箱保护。

四、单进程 vs 多进程:核心差异一览(表格对比)
下面这张表,把旧式单进程浏览器和现代多进程浏览器的差异总结出来,适合在文章中重点展示:
| 对比维度 | 单进程浏览器(典型:旧版 IE) | 多进程浏览器(典型:Chrome) |
|---|---|---|
| 进程模型 | 整个浏览器基本只有一个进程,多标签页只是不同线程 | 浏览器主进程 + 多个子进程(渲染、网络、GPU、插件…),标签页通常独立渲染进程 |
| 稳定性 | 任意线程(脚本、插件)崩溃,可能拖垮整个进程,浏览器整体崩溃 | 某个标签页崩溃只影响对应渲染进程,其他页面基本不受影响 |
| 安全性 | 代码都在同一进程运行,权限边界模糊,攻击面大 | 利用多进程 + 沙箱:渲染进程、插件进程被限制访问系统资源,需要通过主进程/IPC |
| 性能体验 | 多标签共享资源,某个页面卡顿,容易拖慢整体;UI 和页面渲染耦合严重 | 不同进程之间可以更好地利用多核 CPU,重页面操作不会轻易阻塞整个浏览器 UI |
| 内存占用 | 单进程内存相对集中,但一旦泄漏难以回收;崩溃时损失全部状态 | 多进程会有一定内存冗余,但某个进程关闭/崩溃后,其内存可被系统直接回收 |
| 插件影响 | 插件崩溃 = 浏览器崩溃,体验极差 | 插件独立进程 + 沙箱,崩溃影响有限,可以单独重启 |
| 维护与扩展 | 所有模块耦合在一起,改动风险大 | 进程边界天然分层,更利于模块化演进和大规模工程化 |

五、别被“多进程”骗了:JS 依然是单线程
聊到这里,很多同学容易混淆一个点:
浏览器是多进程的,那 JavaScript 是不是也多线程并行执行了?
答案是否定的:主线程上的 JavaScript 依然是单线程模型。区别在于:
- 在渲染进程内部,有一个主线程负责:
- 执行 JavaScript。
- 页面布局、绘制。
- 处理用户交互(点击、输入等)。
- JS 代码仍然遵循:同步任务立即执行,异步任务丢进任务队列,由事件循环(Event Loop)调度。
为什么要坚持单线程?
- DOM 是单线程模型,多个线程同时改 DOM,锁会非常复杂。
- 前端开发心智成本可控;不必像多线程语言那样到处考虑锁和竞态条件。
多进程架构只是把:
- “这个页面的主线程 + 渲染 + JS 引擎”
放在一个单独的进程里(渲染进程)。
这也是为什么:
- 一个页面 JS 写了死循环,会卡死那一个标签页。
- 但其他标签页通常还能正常使用,因为它们在完全不同的渲染进程内。
六、从架构看体验:为什么我们更喜欢现在的浏览器?
站在前端开发者角度,多进程架构带来的直接收益有:
- 更好的容错性
- 更高的安全等级
- 更顺滑的交互体验
- 更容易工程化演进
当然,代价也很现实:多进程 = 更高的内存占用。这也是为什么:
- 多开几十个标签,任务管理器里能看到很多浏览器相关进程。
- 但换来的是更好的稳定性、安全性和扩展性——在现代硬件下,这是可以接受的 trade-off。
七、总结
- 早期浏览器采用单进程 + 多线程模式,所有页面、脚本、插件都在同一个进程里,一旦出问题就“全军覆没”。
- 现代浏览器(代表是 Chrome)使用多进程架构:主进程负责调度,各个渲染、网络、GPU、插件进程各司其职,并通过沙箱强化隔离。
- 尽管浏览器整体是多进程的,但单个页面里的 JavaScript 依然是单线程 + 事件循环模型,这点没有变。
- 从用户体验、前端开发、安全性、稳定性来看,多进程架构几乎是全面碾压旧时代单进程浏览器的一次升级。
来源:juejin.cn/post/7580263284338311209
数组判断?我早不用instanceof了,现在一行代码搞定!
传统方案
1. Object.prototype.toString.call 方法
原理:通过调用 Object.prototype.toString.call(obj) ,判断返回值是否为 [object Array] 。
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]';
}
缺陷:
- ES6 引入
Symbol.toStringTag后,可被人为篡改。例如:
const obj = {
[Symbol.toStringTag]: 'Array'
};
console.log(Object.prototype.toString.call(obj)); // 输出 [object Array]
- 若开发通用型代码(如框架、库),该漏洞会导致判断失效。
2. instanceof 方法
原理:判断对象原型链上是否存在 Array 构造函数。
function isArray(obj){
return obj instanceof Array;
}
缺陷:
- 可通过
Object.setPrototypeOf篡改原型链,导致误判。例如:
const obj = {};
Object.setPrototypeOf(obj, Array.prototype);
console.log(obj instanceof Array); // 输出 true,但 obj 并非真正数组
- 跨 iframe 场景失效。不同 iframe 中的
Array构造函数不共享,导致真数组被误判为非数组。例如:
const frame = document.querySelector('iframe');
const Array2 = frame.contentWindow.Array;
const arr = new Array2();
console.log(arr instanceof Array); // 输出 false,但 arr 是真正数组
ES6 原生方法
方法:使用 Array.isArray 静态方法。
console.log(Array.isArray(arr));
优势:
- 该方法由JavaScript引擎内部实现,直接判断对象是否由 Array 构造函数创建,不受原型链、 Symbol.toStringTag 或跨 iframe 影响;
- 完美解决所有边界场景。
总结
判断数组的方法中 Array.isArray 是唯一准确且无缺陷的方案。其他方法(如Object.prototype.toString.call、instanceof)均存在局限性,仅在特定场景下可用。
来源:juejin.cn/post/7579849892614094884
HTML5 自定义属性 data-*:别再把数据塞进 class 里了!
前言:由于“无处安放”而引发的混乱
在 HTML5 普及之前,前端开发者为了在 DOM 元素上绑定一些数据(比如用户 ID、商品价格、状态码),可谓是八仙过海,各显神通:
- 隐藏域流派:到处塞
<input type="hidden" value="123">,导致 HTML 结构像个堆满杂物的仓库。 - Class 拼接流派:
<div class="btn item-id-8848">,然后用正则去解析 class 字符串提取 ID。这简直是在用 CSS 类名当数据库用,类名听了都想离家出走。 - 自定义非标属性流派:直接写
<div my_id="123">。虽然浏览器大多能容忍,但这就好比在公共泳池里裸泳——虽然没人抓你,但不合规矩且看着尴尬。
直到 HTML5 引入了 data-* 自定义数据属性,这一切终于有了“官方标准”。
第一阶段:基础——它长什么样?
data-* 属性允许我们在标准 HTML 元素中存储额外的页面私有信息。
1. HTML 写法
语法非常简单:必须以 data- 开头,后面接上你自定义的名称。
codeHtml
<!-- ❌ 错误示范:不要大写,不要乱用特殊符号 -->
<div data-User-Id="1001"></div>
<!-- ✅ 正确示范:全小写,连字符连接 -->
<div
id="user-card"
data-id="1001"
data-user-name="juejin_expert"
data-value="99.9"
data-is-vip="true"
>
用户信息卡片
</div>
2. CSS 中的妙用
很多人以为 data-* 只是给 JS 用的,其实 CSS 也能完美利用它。
场景一:通过属性选择器控制样式
/* 当 data-is-vip 为 "true" 时,背景变金 */
div[data-is-vip="true"] {
background: gold;
border: 2px solid orange;
}
场景二:利用 attr() 显示数据
这是一个非常酷的技巧,可以用来做 Tooltip 或者计数器显示。
div::after {
/* 直接把 data-value 的值显示在页面上 */
content: "当前分值: " attr(data-value);
font-size: 12px;
color: #666;
}
第二阶段:进阶——JavaScript 如何读写?
这才是重头戏。在 JS 中操作 data-* 有两种方式:传统派 和 现代派。
1. 传统派:getAttribute / setAttribute
这是最稳妥的方法,兼容性最好(虽然现在也没人要兼容 IE6 了)。
const el = document.getElementById('user-card');
// 读取
const userId = el.getAttribute('data-id'); // "1001"
// 修改
el.setAttribute('data-value', '100');
特点:读出来永远是字符串。哪怕你存的是 100,取出来也是 "100"。
2. 现代派:dataset API (推荐 ✨)
HTML5 为每个元素提供了一个 dataset 对象(DOMStringMap),它将所有的 data-* 属性映射成了对象的属性。
这里有个大坑(或者说是规范),请务必注意:
HTML 中的 连字符命名 (kebab-case) 会自动转换为 JS 中的 小驼峰命名 (camelCase) 。
const el = document.getElementById('user-card');
// 1. 访问 data-id
console.log(el.dataset.id); // "1001"
// 2. 访问 data-user-name (注意变身了!)
console.log(el.dataset.userName); // "juejin_expert"
// ❌ el.dataset.user-name 是语法错误
// ❌ el.dataset['user-name'] 是 undefined
// 3. 修改数据
el.dataset.value = "200";
// HTML 会自动变成 data-value="200"
// 4. 删除数据
delete el.dataset.isVip;
// HTML 中的 data-is-vip 属性会被移除
💡 敲黑板:dataset 里的属性名不支持大写字母。如果你在 HTML 里写 data-MyValue="1", 浏览器会强制转为小写 data-myvalue,JS 里就得用 dataset.myvalue 访问。所以,HTML 里老老实实全小写吧。
第三阶段:深入——类型陷阱与性能权衡
1. 一切皆字符串
不管你赋给 dataset 什么类型的值,最终都会被转为字符串。
el.dataset.count = 100; // HTML: data-count="100"
el.dataset.active = true; // HTML: data-active="true"
el.dataset.config = {a: 1}; // HTML: data-config="[object Object]" -> 灾难!
避坑指南:
- 如果你要存数字,取出来时记得 Number(el.dataset.count)。
- 如果你要存布尔值,判断时不能简单用 if (el.dataset.active),因为 "false" 字符串也是真值!要用 el.dataset.active === 'true'。
- 千万不要试图在 data-* 里存复杂的 JSON 对象。如果非要存,请使用 JSON.stringify(),但在 DOM 上挂载大量字符串数据会影响性能。
2. 性能考量
- 读写速度:dataset 的访问速度在现代浏览器中非常快,但在极高频操作下(比如每秒几千次),直接操作 JS 变量肯定比操作 DOM 快。
- 重排与重绘:修改 data-* 属性会触发 DOM 变更。如果你的 CSS 依赖属性选择器(如 div[data-status="active"]),修改属性可能会触发页面的重排(Reflow)或重绘(Repaint)。
第四阶段:实战——优雅的事件委托
data-value 最经典的用法之一就是在列表项的事件委托中。
需求:点击列表中的“删除”按钮,删除对应项。
<ul id="todo-list">
<li>
<span>学习 HTML5</span>
<!-- 把 ID 藏在这里 -->
<button class="btn-delete" data-id="101" data-action="delete">删除</button>
</li>
<li>
<span>写掘金文章</span>
<button class="btn-delete" data-id="102" data-action="delete">删除</button>
</li>
</ul>
const list = document.getElementById('todo-list');
list.addEventListener('click', (e) => {
// 利用 dataset 判断点击的是不是删除按钮
const { action, id } = e.target.dataset;
if (action === 'delete') {
console.log(`准备删除 ID 为 ${id} 的条目`);
// 这里发送请求或操作 DOM
// deleteItem(id);
}
});
为什么这么做优雅?
你不需要给每个按钮都绑定事件,也不需要去分析 DOM 结构(比如 e.target.parentNode...)来找数据。数据就在元素身上,唾手可得。
总结与“禁忌”
HTML5 的 data-* 属性是连接 DOM 和数据的一座轻量级桥梁。
什么时候用?
- 当需要把少量数据绑定到特定 UI 元素上时。
- 当 CSS 需要根据数据状态改变样式时。
- 做事件委托需要传递参数时。
什么时候别用?(禁忌)
- 不要存储敏感数据:用户可以直接在浏览器控制台修改 DOM,千万别把 data-password 或 data-user-token 放在这。
- 不要当数据库用:别把几 KB 的 JSON 数据塞进去,那是 JS 变量或者是 IndexDB 该干的事。
- SEO 无用:搜索引擎爬虫通常不关心 data-* 里的内容,重要的文本内容还是要写在标签里。
最后一句:
代码整洁之道,始于不再乱用 Class。下次再想存个 ID,记得想起那个以 data- 开头的帅气属性。
Happy Coding! 🚀
来源:juejin.cn/post/7575119254314401818
前端发版总被用户说“没更新”?一文搞懂浏览器缓存,彻底解决!
有时候我们发了新版,结果用户看到的还是老界面。
你:“我更新了啊!”
用户:“我这儿没变啊!”
然后你俩开始互相怀疑人生。
那咋办?总不能让用户都清缓存吧?
当然不能。
我们得让浏览器自己知道“该换新的了”。
核心思路就一条:让静态资源的文件名变一变。
浏览器靠文件名判断是不是同一个文件。
文件名变了,它就会重新下载。
方法1:加时间戳(简单粗暴)
以前:
<script src="/js/app.js"></script>
现在:
<script src="/js/app.js?v=20250901"></script>
或者用时间戳:
<script src="/js/app.js?t=1725153600"></script>
发版的时候,改一下v 或t的值,浏览器看到后发现文件名不一样,就会重新下载。
优点:简单,立马见效
缺点:每次发版都得手动改,容易忘记。
方法2:用构建工具加hash(推荐!)
这是现在最主流的做法。
你用Webpack、Vite、Rollup这些工具打包时,它会自动给文件名加一串hash:
<script src="/js/app.a1b2c3d.js"></script>
你代码一改,hash就变。
比如下次变成:
<script src="/js/app.e4f5g6h.js"></script>
浏览器看到后发现文件名不一样,会自动拉新文件。
使用时需要检查你的打包配置,确保输出文件带hash。
Vite配置(vite.config.js):
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})
Vue CLI(vue.config.js):
module.exports = {
filenameHashing: true, // 默认就是 true,别关掉!
}
只要这个开着,JS/CSS 文件名就会变,浏览器就会更新。
优点:全自动,不用操心
优点:用户无感知,体验好
优点:还能利用缓存(没改的文件hash不变,继续用旧的)
来看看Vue的项目
只要你用的是Vue CLI、Vite或 Webpack打包,发版时默认就解决了缓存问题。
因为它们会自动给文件名加hash。
比如你打包后:
dist/
├── assets/app.8a2b1f3.js
├── assets/chunk-vendors.a1b2c3d.js
└── index.html
你改了代码,再打包,hash就变了:
assets/app.x9y8z7w.js # 新文件
虽说是这样,但为啥还有人卡在旧版本?
文件名带hash,但index.html这个入口文件本身可能被缓存了!
流程:
index.html里引用了app.8a2b1f3.js- 用户第一次访问,加载了
index.html和对应的JS - 你发新版,
index.html指向app.x9y8z7w.js - 但用户浏览器缓存了旧的
index.html,还在引用app.8a2b1f3.js - 结果:页面还是旧的
这是入口文件缓存导致的发版无效。
解决方案
方法1:让index.html不被缓存
这是最简单有效的办法。
配置Nginx,让index.html不缓存:
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
这样每次用户访问,都会重新下载最新的 index.html,自然就拿到新的 JS 文件名。
注意:其他静态资源(js/css)可以长期缓存,只有
index.html要禁缓存。
方法2:根据版本号来控制
每一次更新都新建一个文件夹

然后修改Nginx配置
location / {
root /home/server/html/yudao-vue3/version_1_2_5;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
最后:
缓存这事看着小,真出问题能让我们忙半天。
提前设好机制,发版才能睡得香。
搞定!
我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《Elasticsearch 太重?来看看这个轻量级的替代品 Manticore Search》
《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》
《别学23种了!Java项目中最常用的6个设计模式,附案例》
《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》
来源:juejin.cn/post/7545252678936100918
前端部署,又有新花样?
大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 DevOps 的工作。
但一旦回到个人项目,情况就完全不一样了。写个小博客、搭个文档站,或者搞个 demo 想给朋友看,部署往往成了最大的拦路虎。
常见的选择无非是 Vercel、Netlify 或 GitHub Pages。它们表面上“一键部署”,但细节其实并不轻松:要注册平台账号、要配置域名,还得接受平台的各种限制。国内的一些云服务商(比如阿里云、腾讯云)管控更严格,操作门槛也更高。更让人担心的是,一旦平台宕机,或者因为地区网络问题导致访问不稳定,你的项目可能随时“消失”在用户面前。虽然这种情况不常见,但始终让人心里不踏实。
很多时候,我们只是想快速上线一个小页面,不想被部署流程拖累,有没有更好的方法?
一个更轻的办法
前段时间我发现了一个开源工具 PinMe,主打的就是“极简部署”。

它的使用体验非常直接:
- 不需要服务器
- 不用注册账号
- 在项目目录敲一条命令,就能把项目打包上传到 IPFS 网络
- 很快,你就能拿到一个可访问的地址
实际用起来的感受就是一个字:爽。
整个过程几乎没有繁琐配置,不需要绑定平台账号,也不用担心流量限制或收费。
这让很多场景变得顺手:
- 临时展示一个 demo,不必折腾服务器
- 写了个静态博客,不想搞 CI/CD 流程
- 做了个活动页或 landing page,随时上线就好
以前这些需求可能要纠结“用 GitHub Pages 还是 Vercel”,现在有了 PinMe,直接一键上链就行。
体验一把
接下来看看它在真实场景下的表现:部署流程有多简化?访问速度如何?和传统方案相比有没有优势?
测试项目
为了覆盖不同体量的场景,这次我选了俩类项目来测试:
- 小型项目:fuwari(开源的个人博客项目),打包体积约 4 MB。
- 中大型项目:Soybean Admin(开源的后台管理系统),打包体积约 15 MB。
部署项目
PinMe 提供了两种方式:命令行 和 可视化界面。

这两种方式我们都来试一下。
命令行部署
先全局安装:
npm install -g pinme
然后一条命令上传:
pinme upload <folder/file-path>
比如上传 Soybean Admin,文件大小 15MB:

输入命令之后,等着就可以了:

只用了两分钟,终端返回的 URL 就能直接访问项目的控制页面。还能绑定自己的域名:

点击网站链接就可以看到已经部署好的项目,访问速度还是挺快的:

同样地,上传个人博客也是一样的流程。

部署完成:

可视化部署
不习惯命令行?PinMe 也提供了网页上传,进度条实时显示:

部署完成后会自动进入管理页面:

经过测试,部署速度和命令行几乎一致。
其他功能
历时记录
部署过的网站都能在主页的 History 查看:

历史部署记录:

也可以用命令行:
pinme list
历史部署记录:

删除网站
如果不再需要某个项目,执行以下命令即可:
pinme rm
PinMe 背后的“硬核支撑”
如果只看表层,PinMe 就像一个极简的托管工具。但要理解它为什么能做到“不依赖平台”,还得看看它背后的底层逻辑。
PinMe 的底层依赖 IPFS,这是一个去中心化的分布式文件系统。
要理解它的意义,得先聊聊“去中心化”这个概念。
传统互联网是中心化的:你访问一个网站时,浏览器会通过 DNS 找到某台服务器,然后从这台服务器获取内容。这条链路依赖强烈,一旦 DNS 被劫持、服务器宕机、服务商下线,网站就无法访问。

去中心化的思路完全不同:
- 数据不是放在单一服务器,而是分布在全球节点中
- 访问不依赖“位置”,而是通过内容哈希来检索
- 只要有节点存储这份内容,就能访问到,不怕单点故障
这意味着:
- 更稳定:即使部分节点宕机,内容依然能从其他节点获取。
- 防篡改:文件哪怕改动一个字节,对应的 CID 也会完全不同,从机制上保障了前端资源的完整性和安全性。
- 更自由:不再受制于中心化平台,文件真正由用户自己掌控。
当然,IPFS 地址(哈希)太长,不适合直接记忆和分享。这时候就需要 ENS(Ethereum Name Service)。它和 DNS 类似,但记录存储在以太坊区块链上,不可能被篡改。比如你可以把 myblog.eth 指向某个 IPFS 哈希,别人只要输入 ENS 域名就能访问,不依赖传统 DNS,自然也不会被劫持。

换句话说:
ENS + IPFS = 内容去中心化 + 域名去中心化

前端个人项目瞬间就有了更高的自由度和安全性。
一点初步感受
PinMe 并不是要取代 Vercel 这类成熟平台,但它带来了一种新的选择:更简单、更自由、更去中心化。
如果你只是想快速上线一个小项目,或者对去中心化部署感兴趣,PinMe 值得一试。
- 官网:pinme.eth.limo/
- Github:github.com/glitternetw…
这是一个完全开源的项目,开发团队也会持续更新。如果你在测试过程中有想法或需求,不妨去 GitHub 提个 Issue —— 这不仅能帮助项目成长,也能让它更贴近前端开发的实际使用场景!
来源:juejin.cn/post/7547515500453380136
快到 2026 年了:为什么我们还在争论 CSS 和 Tailwind?
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777
老实说,我对 Tailwind CSS 的看法有些复杂。它像是一个总是表现完美的朋友,让我觉得自己有些不够好。
我一直是纯 CSS 的拥护者,直到最近,我才意识到,Tailwind 也有其独特的优点。
然而,虽然我不喜欢 Tailwind 的一些方面,但它无疑为开发带来了更多的选择,让我反思自己做决定的方式。
问题
大家争论的,不是 "哪个更好",而是“哪个让你觉得更少痛苦”。
对我来说,Tailwind 有时带来的是压力。比如:
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:shadow-lg transition duration-300 ease-in-out transform hover:-translate-y-1"
>
Click me
</button>
它让我想:“这已经不再是简单的 HTML,而是样式类的拼凑。”
而纯 CSS 则让我感到平静、整洁:
.button {
background-color: #3b82f6;
color: white;
font-weight: bold;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: #2563eb;
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
transform: translateY(-4px);
}
纯 CSS 让我觉得自己在“写代码”,而不是“编排类名”。
背景说明
为什么要写这篇文章呢?因为到了 2026 年,CSS 和 Tailwind 的争论已经不再那么重要。
- Tailwind 发布了 v4,速度和性能都大大提升。
- 纯 CSS 也在复兴,容器查询(container queries)、CSS 嵌套(nesting)和 Cascade Layers 这些新特性令人振奋。
- 还有像 Panda CSS、UnoCSS 等新兴工具在不断尝试解决同样的问题。
这让选择变得更加复杂,也让开发变得更加“累”。
Tailwind 的优缺点
优点:
- 减少命名烦恼:你不再需要为类命名。只需使用 Tailwind 提供的类名,省去了命名的麻烦。
- 设计一致性:使用 Tailwind,你的设计系统自然一致,避免了颜色和间距不统一的麻烦。
- 编辑器自动补全:Tailwind 的 IntelliSense 使得开发更加高效,输入类名时有智能提示。
- 响应式设计更简单:通过简单的类名就能实现响应式设计,比传统的媒体查询更简洁。
缺点:
- HTML 看起来乱七八糟:多个类名叠加在一起,让 HTML 看起来复杂且难以维护。
- 构建步骤繁琐:你需要一个构建工具链来处理 Tailwind,这对某些项目来说可能显得过于复杂。
- 调试困难:开发者工具中显示的类名多而杂,调试时很难快速找到问题所在。
- 不够可重用:Tailwind 的类名并不具备良好的可重用性,你可能会不断复制粘贴类,而不是通过自定义组件来实现复用。
纯 CSS 的优缺点
优点:
- 更干净的代码结构:HTML 和 CSS 分离,代码简洁易懂。
- 无构建步骤:只需简单的
<link>标签引入样式表,轻松部署。 - 现代特性强大:2025 年的 CSS 已经非常强大,容器查询和 CSS 嵌套让你可以更加灵活地进行响应式设计。
- 自定义属性:通过 CSS 变量,你可以轻松实现全站的样式管理,改一个变量,所有样式立即生效。
缺点:
- 命名仍然困难:即使有 BEM 或 SMACSS 等方法,命名仍然是一项挑战。
- 保持一致性需要更多约束:没有像 Tailwind 那样的规则,纯 CSS 需要更多的自律来保持一致性。
- 生态碎片化:不同团队和开发者采用不同的方式来组织 CSS,缺少统一标准。
- 没有编辑器自动补全:不像 Tailwind,纯 CSS 需要手动编写所有的类名和样式。
到 2026 年,你该用哪个?
Tailwind 适合:
- 使用 React、Vue 或 Svelte 等组件化框架的开发者
- 需要快速开发并保证一致性的团队
- 不介意添加构建步骤并依赖工具链的人
纯 CSS 适合:
- 小型项目或静态页面
- 喜欢简洁代码、分离 HTML 和 CSS 的开发者
- 想要完全掌控样式并避免复杂构建步骤的人
两者结合:
- 你可以在简单的页面中使用纯 CSS,在复杂的项目中使用 Tailwind 或两者结合,以此来平衡灵活性与效率。
真正值得关注的 2026 年趋势
- 容器查询:响应式设计不再依赖视口尺寸,而是根据容器的尺寸进行调整。
- CSS 嵌套原生支持:你可以直接在 CSS 中使用嵌套,避免了依赖预处理器。
- Cascade Layers:这让你能更好地管理 CSS 优先级,避免使用
!important来解决冲突。 - View Transitions API:它让页面过渡更平滑,无需依赖 JavaScript。
这些新特性将极大改善我们的开发体验,无论是使用纯 CSS 还是借助 Tailwind。
结尾
不管是 Tailwind 还是纯 CSS,都有它们的优缺点。关键是要根据项目需求和个人偏好做出选择。
至于我:我喜欢纯 CSS,因为它更干净,HTML 更直观。但是如果项目需求更适合 Tailwind,那我也会使用它。
2026 年的开发趋势,将让我们有更多选择,让我们能够用最适合的工具解决问题,而不是纠结于某种工具是否“最好”。
来源:juejin.cn/post/7568674364042330166
老板,我真干活了...
辛苦写了3天的代码,突然一下,全部消失了
我说我每次都git add .了,但是他就是消失了
你会相信我吗
这次真的跳进黄河都洗不清,六月都要飞雪了
一个安静的夜晚,主包正在和往常一样敲着代码,最后一个优化完成以后执行了git add .
看着长长的暂存区,主包想是时候git commit -m了
变故就发生在一瞬间,commit校验返回失败,伴随着电脑的终端闪烁了一下,主包的 所 有 改 动都消失了,文件内容停留在上次的提交...
老板,你相信我吗?我真的干活了
不过幸好,我的Gemini在我的指导下一步一步帮我把3天的劳动成果还原了,下面记录一下整个事件过程
首先。我已经执行了 git add .,这意味着 Git 已经将这些文件的内容以「blob 对象」的形式保存到本地 .git/objects 目录中,这些数据就不会凭空消失。
解决步骤
第一步:不要乱动
立即停止在仓库中执行任何覆写的操作!避免覆盖磁盘上的 blob 对象
- ❌ 不要执行
git reset --hard/git clean -fd/git gc/git prune; - ❌ 不要往仓库目录写入新文件
第二步:检查 Git 状态
git status查看当前暂存区的状态,如果暂存区有文件的话可以通过get checkout -- .进行恢复,可是我没有了,我的暂存区和我的脑袋一样空空

第三步:拉取悬空文件
如果 git status 显示「Working Tree Clean」,且 git checkout -- . 没效果,说明暂存区被清空,但 Git 仍保存了 git add 时的 blob 对象
需通过 git fsck --lost-fond 找回
该指令会扫描 .git/objects/ 目录,找出所有没有被任何分支/标签引用的对象,列出 悬空的 commits、blobs、trees(优先找最新的commit)
第四步:验证每个commit
执行 git show --stat <commit ID> 可以验证该悬空 commit 内容

- 如果「提交信息 / 时间 / 作者」匹配你刚才中断的 commit → 这就是包含你所有改动的快照;
- 如果显示
initial commit或旧提交信息 → 这是历史悬空 commit,换列表里下一个时间最新的 commit ID 重试。

- 这里会列出该 commit 中所有改动的文件路径 + 行数变化 → 如果能看到你丢失的文件(如
src/App.js),说明找对了; - 如果显示
0 files changed→ 这个 commit 是空的,换其他 commit ID 重试。
第五步:恢复提交
方式 1:仅恢复文件到工作区(推荐,不修改 HEAD)
git checkout commitId -- .
方式 2:#### 直接将 HEAD 指向该 commit(完成提交)
git reset --hard commitId // 等同于完成当时的 commit 操作
到这里基本就已经恢复了,可以check一下更改的文件,如果不全可以继续执行checkout进行恢复,如果已经完成了就尽快commit以防发生别的变故啦~
最后,还是简单讲解一下为什么优先恢复悬空commit,commit、tree、Blob的区别
核心结论先摆清楚
| 对象类型 | 中文名称 | 核心作用 | 类比(便于理解) | 能否直接恢复你的文件? |
|---|---|---|---|---|
| Blob | 数据对象 | 存储单个文件的内容(无路径) | 一本书里的某一页内容 | 能,但需匹配原文件路径 |
| Tree | 树对象 | 存储目录结构(文件 / 子目录映射) | 一本书的目录(章节→页码) | 不能直接恢复,仅辅助找路径 |
| Commit | 提交对象 | 存储完整的提交快照(关联 tree + 作者 / 时间 / 信息) | 一本书的版本记录(含目录 + 修改说明) | 最优选择,一键恢复所有文件 |
一、逐个拆解:悬空 blob/commit/tree 到底是什么?
Git 仓库的所有内容(文件、目录、提交记录)最终都会被存储为「对象」,存放在 .git/objects 目录下;「悬空」意味着这些对象没有被任何分支 / 标签 / HEAD 引用(比如 commit 中断、reset 后、删除分支等),但只要没执行 git gc(垃圾回收),就不会消失。
1. 悬空 Blob(数据对象)—— 「只存内容,不管路径」
- 本质:Git 中最小的存储单元,仅保存「单个文件的原始内容」,不包含文件名、路径、修改时间等信息;
- 举例:你修改了
src/App.js并执行git add .,Git 会把App.js的内容打包成一个 blob 对象(比如你看到的ec0529e46516594593b1befb48740956c8758884),存到.git/objects里; - 悬空原因:执行
git add后生成了 blob,但 commit 中断 / 执行git reset清空暂存区,导致这个 blob 没有被 tree/commit 引用; - 恢复特点:能拿到文件内容,但不知道原文件路径(比如你只知道 blob 是一段 JS 代码,却不知道它原本是
src/App.js还是src/Page.js)。
2. 悬空 Tree(树对象)—— 「只存目录结构,不存内容」
- 本质:描述「目录层级 + 文件映射关系」,相当于「文件路径 ↔ blob ID」的对照表,也能包含子 tree(对应子目录);
- 举例:一个 tree 对象可能记录:
src/ (子tree) → tree ID: bb0065eb...
package.json → blob ID: e90a82fe...
src/App.js → blob ID: ec0529e4...
- 悬空原因:Tree 是 commit 的「子对象」,如果 commit 变成悬空(比如 reset 后),对应的 tree 也会悬空;
- 恢复特点:仅能看到「哪些 blob 对应哪些路径」,但本身不存储文件内容,需结合 blob 才能恢复完整文件。
3. 悬空 Commit(提交对象)—— 「完整的提交快照」
- 本质:Git 中最高级的对象,是「一次提交的完整记录」,包含:
- 指向一个 root tree(根目录的 tree 对象)→ 能拿到整个项目的目录结构 + 所有 blob;
- 作者、提交时间、提交信息;
- 父 commit ID(如果是后续提交);
- 举例:你执行
git commit -m "修改App.js"时,Git 会生成一个 commit 对象,关联 root tree(包含所有文件路径 + blob),记录你的操作信息; - 悬空原因:commit 执行中断、
git reset --hard后原 HEAD 指向的 commit 无引用、删除分支后分支上的 commit 无引用; - 恢复特点:✅ 最优选择!通过一个 commit 对象,能一键恢复「该提交时刻的所有文件(路径 + 内容)」,不用手动匹配 blob 和路径。
二、为什么你该优先恢复「悬空 Commit」?
你之前执行了 git add . + 尝试 git commit,大概率 Git 已经生成了 commit 对象(只是没被 HEAD 引用,变成悬空)—— 恢复 commit 有 2 个核心优势:
- 一键恢复所有文件:commit 关联了 root tree,能直接拿到「所有文件的路径 + 对应的 blob 内容」,执行
git checkout <commit ID> -- .就能把所有文件恢复到工作区,不用逐个处理 blob; - 不用手动匹配路径:如果只恢复 blob,你需要逐个查看 blob 内容,再手动命名 / 放到原路径;而 commit 直接包含路径信息,恢复后文件路径和名称完全和丢失前一致。
三、实操场景:不同悬空对象该怎么用?
场景 1:有可用的悬空 Commit(优先选)
# 1. 找时间最新的悬空 commit
git fsck --lost-found | grep 'dangling commit' | awk '{print $3}' | while read c; do
echo "$c | $(git log -1 --format='%ai' $c)"
done | sort -k2 -r
# 2. 验证该 commit 包含你的文件
git show --stat <最新的commit ID>
# 3. 一键恢复所有文件到工作区
git checkout <commit ID> -- .
场景 2:只有悬空 Blob/Tree(无可用 Commit)
# 1. 先通过 tree 找「blob ID ↔ 文件路径」的映射
git ls-tree -r <tree ID> # 列出该 tree 下的所有文件路径+blob ID
# 2. 按路径恢复 blob 内容
git cat-file -p <blob ID> > <原文件路径> # 比如 git cat-file -p ec0529e4 > src/App.js
场景 3:只有悬空 Blob(无 Tree/Commit)
只能批量导出 blob,通过内容匹配原文件:
mkdir -p recover
for blob in $(git fsck --lost-found | grep 'dangling blob' | awk '{print $3}'); do
git cat-file -p $blob > recover/$blob
# 自动补文件后缀(如 .js/.json)
file_type=$(file -b --mime-type recover/$blob | awk -F '/' '{print $2}')
[ "$file_type" != "octet-stream" ] && mv recover/$blob recover/$blob.$file_type
done
四、关键提醒:避免悬空对象被清理
Git 的 git gc(垃圾回收)默认会清理「超过 14 天的悬空对象」,所以恢复前务必:
- 不要执行
git gc/git prune; - 恢复完成后,尽快执行
git commit让对象被 HEAD 引用,避免后续被清理; - 如果暂时没恢复完,可执行
git fsck --full检查所有悬空对象,确认未被清理。
总结来说:优先找悬空 Commit(一键恢复)→ 其次用 Tree 匹配 Blob 路径 → 最后批量导出 Blob 手动匹配,这是最高效的恢复路径
来源:juejin.cn/post/7581678032336519210
2025博客框架选择指南:Hugo、Astro、Hexo该选哪个?

引言
2025年了,想搭个博客,在Hugo、Hexo、Astro之间纠结了一周,还是不知道选哪个?
我完全理解这种感受。去年我也在这几个博客框架之间来回折腾,看了无数对比文章,最后发现大部分都是2022-2023年的过时内容。性能数据变了、框架更新了、生态也不一样了。每次看完一篇文章就更纠结,到底该听谁的?
更让人崩溃的是,深夜折腾博客配置,第二天还要上班。花了三个月研究框架,结果一篇文章都没写出来。这种感觉,相信你也经历过。
这篇文章基于2024-2025年最新数据,用真实的构建时间测试、实际的使用体验,帮你在5分钟内做出最适合的选择。不是告诉你"哪个最好",而是"哪个最适合你"。
我会用9大框架的最新性能对比、3分钟决策矩阵,帮你避免90%新手会踩的坑。说实话,早点看到这篇文章,我能省好几周时间。
为什么2025年还在聊博客框架?
静态博客真的还有必要吗?
老实讲,我一开始也觉得搭博客是个过时的想法。但用了一年多,真香。
静态博客和WordPress这类动态博客的本质区别,就是"提前做好"和"现场制作"的区别。静态网站生成器(SSG)会在你写完文章后,就把所有页面生成好,像做批量打印一样。访客来了直接看成品,速度飞快。WordPress这类动态博客呢,每次有人访问就现场从数据库拉数据、拼装页面,就像现场手工做菜。
为什么静态博客成为主流趋势?
说白了就是:快、便宜、不用操心服务器。
WordPress需要租服务器,一个月怎么也得几十块钱起步。静态博客呢?GitHub Pages、Vercel、Cloudflare Pages全都免费托管。我现在博客一年花费0元,连域名都是之前买的。
性能上更没得比。静态页面的Lighthouse评分能轻松拿到95+,WordPress想到90分都费劲。用户打开页面,一眨眼就加载完了,这种体验真的会让人爱上写博客。
那什么时候该选动态博客?
也不是说静态博客就天下无敌。如果你要做复杂的功能,比如:
- 多人协作发布(需要后台管理)
- 电商集成(要处理支付、订单)
- 复杂的用户系统(评论、权限管理)
这些场景,WordPress或者Ghost确实更合适。但老实说,大部分个人博客和技术博客,真用不到这些。
一句话总结:个人博客、技术文档、作品展示,选静态博客框架准没错。需要复杂功能、多人协作,才考虑动态博客。
性能对决 - 谁是速度之王?
性能这块,说实话是我最关心的。刚开始用Gatsby的时候,每次改一点内容,重新构建等十几分钟,真的想砸电脑。
构建速度:差距大到惊人
先说结论:Hugo是速度之王,没有之一。
看看这组2024年的实测数据:
| 框架 | 构建10000页用时 | 平均每页速度 |
|------|----------------|-------------|
| Hugo | 2.95秒 | <1ms |
| Hexo | 45秒(1000页) | ~45ms |
| Jekyll | 187.15秒 | ~18ms |
| Gatsby | 30分钟+ | ~180ms |
第一次看到Hugo的构建速度,真的惊到我了。10000篇文章,不到3秒!这意味着啥?你改个标题、修个错别字,按个保存,页面刷新,博客就更新完了。这种即时反馈,爽到飞起。
Hexo呢,中型博客(100-1000文章)表现也挺不错。我之前300篇文章的博客,15秒就构建完成,完全够用。
Gatsby...嗯,别提了。我用它做过一个项目,200多篇文章,每次构建5分钟起步。后来文章多了,直接放弃了。
页面加载速度:Astro异军突起
2024年Astro成了最大黑马。根据HTTP Archive的数据:
- Astro:中位传输大小889KB(最轻)
- Hugo:1,174KB(平衡不错)
- Next.js:1,659KB(功能多,体积大)
Astro的"零JavaScript默认"策略真的厉害。页面只加载必要的JS,不像其他框架,把整个React/Vue库都塞给用户。结果就是,页面打开速度飞快,用户体验特别好。
实际项目表现:别只看数字
老实讲,如果你博客不到100篇文章,选啥框架都差不多。几秒和十几秒的区别,你感受不出来。
但如果你计划长期写作,文章会越来越多,提前选个性能好的框架,能省很多麻烦。我见过太多人,开始用了Gatsby,写到500篇文章,构建慢到受不了,迁移框架,那个痛苦...
推荐组合:
- 小型博客(<100文章):随便选,都够用
- 中型博客(100-1000文章):Hugo、Hexo、Astro
- 大型站点(1000+文章):闭眼选Hugo
开发体验 - 哪个最好用?
性能再好,用起来糟心,也是白搭。
学习曲线:新手别踩坑
说实话,Hugo的Go模板语法,我当初学了好久才上手。那些{{ range }}、{{ with }}的语法,刚开始真的看懵了。虽然性能无敌,但新手上来就选Hugo,可能会被劝退。
新手友好度排名:
- Hexo(最友好):Node.js生态,中文文档多到看不过来。配置就是一个
_config.yml文件,改几个参数就能跑起来。我当时半小时就搭好了第一个博客。 - Jekyll(友好):Ruby生态,官方文档写得特别清楚,按着步骤来不会错。GitHub Pages原生支持,push一下就自动部署。
- VuePress(需要前端基础):如果你会Vue,上手很快。不会的话,还得先学Vue,成本就高了。
- Gatsby/Next.js(陡峭):要懂React、GraphQL,配置复杂。我看到配置文件就头大。
我的建议:如果你和我一样是Node.js开发者,Hexo真的是顺手。装个npm包,改改配置,半小时搞定。前端技术栈是React?那Astro或Next.js更合适。别纠结了,新手就选Hexo,错不了。
配置和自定制:平衡艺术
Hexo和Jekyll的配置简单直接,一个YAML文件搞定。但也有缺点:想做复杂定制,就得深入源码改,不太灵活。
Gatsby的灵活性确实强,GraphQL数据层可以接各种数据源。但坦白说,个人博客真用不到那么复杂的功能。就像买了辆跑车在市区开,性能过剩了。
Astro走了个中间路线,既灵活又不复杂。支持多框架(React、Vue、Svelte随便混),但配置没那么吓人。这种平衡感,我挺喜欢的。
开发工具和调试
现代框架在开发体验上真的吊打老框架。
Vite驱动的Astro和VuePress,热重载快到飞起。改个内容,不到1秒页面就更新了。Webpack那套老架构,等待时间能让人发呆。
Hugo虽然是传统框架,但速度快,改完刷新也很即时。这点体验还不错。
生态系统 - 主题与插件谁更丰富?
框架再好,没主题也白搭。谁想从零开始写CSS啊。
主题生态:Hexo中文世界称王
Hexo真的是中文博客的天选之子。200+中文主题,风格各异,总有一款适合你。我用的那个主题,中文文档详细到连怎么改字体颜色都写得清清楚楚。
Hugo主题虽然有300+,但质量参差不齐。有些特别精美,有些就是demo级别。很多主题文档是英文的,踩坑全靠自己摸索。
Jekyll作为老牌框架,主题数量最多,但很多都是好几年前的设计风格了,看着有点过时。
Gatsby和Astro的主题,走的是现代化路线,设计感很强。如果你追求视觉效果,这两个不错。
插件和扩展能力
Jekyll的插件生态最庞大,毕竟历史最久。想要啥功能,基本都能找到插件。
Gatsby依托npm生态,插件也超级丰富。但很多插件其实是为商业项目设计的,个人博客可能用不上。
Hexo插件生态在Node.js圈很成熟,常用的评论、搜索、SEO优化,都有现成插件。我装了七八个插件,没遇到过兼容问题。
社区活跃度:看数据说话
2024-2025年的数据挺有意思:
- Astro:增长最快,npm下载量2024年9月达到300万。Netlify调查显示,它是2024年开发者关注度最高的框架。
- Hugo:GitHub星标6万+,稳定增长,老牌强者。
- Hexo:中文社区最活跃,知乎、掘金、CSDN到处都是教程。
说实话,社区活跃度对新手很重要。遇到问题,能搜到中文解决方案,省太多时间了。
部署与SEO - 上线才是王道
博客搭好了,不上线有啥用?
部署平台:全都免费真香
现在部署静态博客,真的太简单了。
GitHub Pages:Jekyll原生支持,push代码就自动部署。其他框架需要配个GitHub Actions,也就多写几行配置,5分钟搞定。
Vercel/Netlify:这俩是我最推荐的。拖个仓库进去,自动识别框架,自动构建部署。都有免费额度,个人博客完全够用。我现在用的Vercel,一年没花过一分钱。
Cloudflare Pages:性能特别好,CDN全球分布。免费额度也很大,速度比GitHub Pages快不少。
老实讲,2025年部署静态博客,已经没有技术门槛了。真的,比你想象的简单。
SEO:静态博客天生优势
所有静态博客框架,SEO都友好。为啥?生成的都是纯HTML,搜索引擎最爱这个。
区别在于细节:
- Hugo和Jekyll:成熟稳定,sitemap、RSS自动生成,SEO基础功能齐全。
- Astro和Next.js:在现代SEO实践上更领先,支持更细致的元数据管理,结构化数据也更方便。
- Hexo:通过插件实现SEO功能,也挺完善,中文SEO教程多。
说白了,只要你写好内容、优化好关键词、页面结构合理,用哪个框架SEO都不会差。别太纠结这个。
长期维护成本:别选冷门框架
这个坑我踩过。之前用过一个小众框架,开始挺好,半年后发现作者不更新了。后来依赖库升级,博客直接跑不起来,迁移框架花了整整一周。
低维护框架:Hugo、Jekyll。成熟稳定,基本不会出幺蛾子。我Hugo博客跑了一年多,一次问题都没遇到。
需要关注更新:Gatsby、Next.js。依赖多,更新频繁,偶尔会遇到breaking changes。如果你不想经常折腾,慎选。
平衡选手:Astro、Hexo。更新有节制,兼容性做得不错。
决策框架 - 如何选择最适合你的?
好了,前面说了这么多数据和对比,到底该怎么选?
3分钟快速决策矩阵
别想太多,回答三个问题:
问题1:你的技术栈是什么?
- 熟悉Node.js → Hexo(新手)/ Astro(追求现代化)
- React开发者 → Gatsby(重型)/ Astro(轻量)/ Next.js(全栈)
- Vue开发者 → VuePress(博客+文档)/ Gridsome
- 技术栈不限 → Hugo(性能第一)/ Jekyll(求稳)
如果你和我一样是Node.js开发者,Hexo真的顺手。装个npm包,改改配置,半小时搞定。
问题2:你的项目规模是多大?
- <100文章 → 随便选,性能差异你感受不出来
- 100-1000文章 → Hugo(快)/ Hexo(够用)/ Astro(现代)
- 1000+文章或大型文档站 → Hugo(一骑绝尘,没得比)
我现在500多篇文章,用的Hexo,45秒构建完成,完全够用。如果文章继续增长到1000+,可能会换Hugo。
问题3:你的经验水平如何?
- 新手 → Hexo(中文资源多)/ Jekyll(文档友好)
- 前端开发者 → Astro(现代化体验)/ VuePress(Vue技术栈)
- 性能极客 → Hugo(速度无敌,值得学Go模板)
- 求稳用户 → Jekyll(GitHub原生支持,最省心)
典型场景具体推荐
个人技术博客:
- 中文用户:Hexo(生态强,主题多)
- 国际化:Hugo(性能好,英文资源丰富)
- 追求现代化:Astro(体验好,性能也不错)
技术文档站点:
- Docusaurus(Facebook出品,专为文档设计)
- VuePress(Vue生态,中文支持好)
大型内容站点:
- Hugo(1000+页面,只有它能扛住)
现代化项目网站:
- Astro(灵活性+性能的最佳平衡)
- Next.js(需要动态功能时选它)
我的真实建议
说实话,别再纠结了。我见过太多人花三个月研究框架,一篇文章没写。框架真的只是工具,内容才是核心。
选择建议:
- 90%的人:选Hexo或Hugo,够用了
- 前端开发者:Astro值得尝试,体验很现代
- 新手怕选错:Hexo,中文教程多到看不完,遇到问题都能搜到答案
- 性能焦虑症患者:闭眼选Hugo,速度真的无敌
记住:框架可以迁移(内容都是Markdown,搬家成本不高),但荒废的时间回不来。先选一个动起来,边用边优化,这才是正道。
避坑指南与最佳实践
最后说说那些大坑,我替你踩过了。
5个常见错误
错误1:过度追求完美框架,迟迟不开始
这个坑我踩得最深。当年对比了两个月框架,看了几十篇文章,结果还是不确定。后来一个前辈跟我说:"先选一个动起来,框架不满意可以换,但浪费的时间回不来。"
说白了,内容才是博客的核心,框架只是工具。没有完美的框架,只有最适合当下的选择。
错误2:只看主题外观,忽略框架本质
看到某个Hugo主题特别炫酷,就选了Hugo。结果发现Go模板语法学不会,自定义主题难如登天。最后用了半年,还是换回Hexo。
主题可以定制、可以换,但框架的性能、生态、维护性,这些本质特性才是长期影响你的因素。
错误3:新手直接上Gatsby/Next.js被劝退
我一朋友,刚学前端,听说Gatsby牛逼,直接上手。结果GraphQL不会、React不熟、配置看不懂,折腾两周直接放弃了。
老实说,Gatsby和Next.js真的不适合新手。它们是给有经验的开发者准备的工具。新手想快速上线博客,Hexo或Jekyll才是正确选择。
错误4:忽略长期维护成本
选了个冷门框架,一开始挺好,半年后作者不更新了。依赖库升级,博客跑不起来。迁移框架,痛苦得要死。
看框架选择的三个指标:
- GitHub更新频率(至少每月有commit)
- 社区规模(遇到问题能找到人问)
- 中文资源(新手必看,能省80%时间)
错误5:花80%时间折腾框架,20%写内容
我见过太多人,陷入"完美主义陷阱"。CSS改来改去、插件装了卸卸了装,就是不写文章。
记住80/20法则:80%精力写内容,20%折腾框架。够用就好,别追求极致完美。
框架迁移建议
万一真选错了,想换框架怎么办?
其实没那么可怕。所有静态博客框架,内容都是Markdown,迁移成本不高。我从Hexo迁移到Hugo,内容迁移只花了1小时。主要是配置和主题要重新搞,但也就半天时间。
迁移原则:
- 先有后优:快速上线 > 完美配置
- 内容优先:写够50篇文章再考虑迁移,不然没必要
- 不影响SEO:做好301重定向,URL结构尽量保持一致
2025年趋势展望
根据2024-2025的数据和社区动向,我预测:
- Astro会继续增长:岛屿架构是未来趋势,零JavaScript默认太香了
- Hugo保持性能王者地位:大型站点没得选
- Hexo中文生态持续稳定:中文博客的首选不会变
- 传统框架逐步被取代:Jekyll虽然稳定,但新项目会越来越少
但说实话,趋势只是参考。选框架还是要看自己的需求和技术栈。
结论
回到最开始的问题:2025年博客框架该怎么选?
一句话总结:
- 新手首选:Hexo(中文资源丰富,主题多,上手快)
- 前端开发者:Astro(性能+灵活性的最佳平衡,现代化体验)
- 性能极客:Hugo(速度无敌,适合大型站点)
- 文档站点:Docusaurus/VuePress(专为文档设计)
- 求稳:Jekyll(GitHub原生支持,最省心)
但老实讲,选择框架只需要5分钟,写好内容需要一辈子。
别再纠结了。选一个顺手的,开始行动吧。写第一篇文章,比研究框架重要一百倍。
我当初纠结了两个月,现在回头看,那段时间完全是浪费。早点开始写,现在可能已经有200篇文章了。
行动建议:
- 根据上面的决策矩阵,花5分钟选一个框架
- 找个主题,1小时搭好环境
- 写第一篇文章,哪怕只有500字
- 发布上线,享受成就感
记住:框架不重要,内容才重要。够用就好,专注写作。
评论区说说你的选择和理由?我很好奇大家最后都选了什么。如果有问题,我会尽量回复的。
本文首发自个人博客
来源:juejin.cn/post/7578714735307849754
为什么说低代码谎言的破灭,是AI原生开发的起点?
几年前,公司老板、产品经理,甚至隔壁行政的同事,都拿着一份花里胡哨的低代码方案,眼睛放光地跟你说:“小张啊,你来看看,未来!拖拉拽就能上线,咱们再也不用养那么多程序员了!”
我当时啥心情?表面上“嗯嗯嗯,是是是,很有前景”,心里一万头羊驼在奔腾。你懂个锤子啊,我一直认为它是解决了一类问题,引入了一大堆的复杂。
这玩意儿的核心是啥?说白了,就是想用一套“万能模板”去解决所有问题。听着是挺美,但咱都是写代码的,你心里清楚,软件开发最难的,从来不是那几行CRUD,而是那些“该死”的个性化业务逻辑。认同?!
低代码,本质上就是把这些逻辑,从代码文件里,挪到了一堆图形化的配置框里。你以为是解放了生产力?扯淡。你只是换了个地方“坐牢”。

从“卧槽,牛逼”到“卧槽,什么玩意儿”
AI写代码一段时间,比如Cursor、Claude Code、codex这些,我当时的第一反应是:“卧-槽,牛-逼!”
你跟它说:“帮我写个用户登录接口,用JWT,密码要bcrypt加密,加上参数校验。”
唰一下,代码出来了。连注释都给你写得明明白白。那一瞬间,真有种未来已来的感觉。
这不就是低代码想干但没干成的事儿吗?
低代码是给你一堆乐高积木,说:“来,你自己拼个宇宙飞船。” 你吭哧吭哧拼了半天,发现给你的零件根本就不够,而且拼出来的玩意儿四不像,还飞不起来。
AI原生开发是啥?你直接跟管家说:“多啦 A 梦,给我造个能飞的玩意儿,要快,要帅。” 贾维斯直接给你把图纸、零件、引擎全整出来了。你只需要当那个总设计师,告诉它你的“意图”,然后去检查、微调。
你品,你细品。一个让你当“拼装工”,一个让你当“设计师”。格局一样吗?
我为啥突然这么大感触?老黄-全栈工程师,前阵子接了个项目,有个模块特别急。按老路子,前端画页面、后端写接口、联调... 怎么也得一周吧?
他说他用Claude Code,把需求拆成几个点,啪啪啪跟AI对话。
“给我个Vue3+TS的表格页,能分页,能搜索,数据接口是/api/users”
“后端用Go写这个接口,连PostgreSQL,把分页逻辑做了。”
“加个逻辑,角色是admin才能看到删除按钮。”
一下午,真的,就一个下午,他说,一个带前后端的完整功能雏形就出来了。代码质量还不低,结构清晰,拿过来改改就能用。
而用低代码呢?他说他可能还在研究那个破平台的“数据源”到底该怎么连,或者某个组件的某个奇葩属性到底藏在哪个配置项里。那种抓狂,谁用谁知道。
我想,老黄的这种感觉你肯定有过,如果可以选择,你会选择用地代码去拖拉拽吗?我想大概率你是不会了,回不去了。
别跟我扯什么“AI搞不定复杂逻辑”
我知道,肯定有人会跳出来说:“哥们,你这是偷换概念!简单的CRUD当然AI快,你让AI搞个复杂的风控引擎试试?”
每次听到这话,我就想笑。
兄弟,你是不是忘了我们现代软件架构的核心思想是啥了?高内聚、低耦合!
一个复杂的系统,不就是一堆简单的、正交的子系统组合起来的吗?你告诉我,哪个“复杂业务逻辑”是不能拆解的?如果不能,那不是AI的问题,是你的架构设计有问题。
领域驱动设计(DDD)讲了这么多年,不就是为了把业务模型理清楚,把边界划分明白吗?
在AI原生时代,架构师的核心能力,不再是堆砌代码,而是精准地拆解问题,然后把拆解后的子问题清晰地描述给AI。
低代码平台最大的死穴就在这儿。它试图用一个大而全的“黑箱”包办一切,结果就是耦合度极高。你想改其中一根“毛细血管”,可能得把整个“心脏”都停了。这种系统,业务稍微一变,就得推倒重来,维护成本高到让你怀疑人生。我亲眼见过一个团队,被他们选的低代码平台折磨得死去活-来,最后整个项目烂尾。
AI生成的代码呢?那是“白箱”。清清楚楚的源代码,你想怎么改就怎么改,想怎么扩展就怎么扩展。这才是咱们工程师该有的掌控感!
所以,有人焦虑了?
聊到这,估计有些年轻的哥们儿开始焦虑了:“老哥,照你这么说,以后是不是不用写代码了?我们都要被AI干掉了?”
我说:慌啥。
AI不是来取代你的,是来升级你的工具箱的。,取带你的是那些用 AI 比你用得更深,更好的人。人家都开车了,你还走路,你搁那怪谁呢?
低代码,是想把你变成一个“组件配置工程师”,说实话,这玩意儿真没啥技术含量,他无非就是把一些固化的规则和业务组件放在了一个系统中,让你去组合,去连接,也确实容易被替代。
但AI原生开发,对人的要求,其实是更高了。
你需要有更强的架构能力,去拆解复杂问题。
你需要有更强的业务理解能力,去把需求转化成清晰的指令。
你需要有更强的代码审美,去判断AI生成的代码是“精品”还是“垃圾”。
说白了,AI帮你干了那些重复、枯燥的体力活,让你能把全部精力,都放在 “思考” 这个最有价值的事情上。
写在最后,一点不成熟的牢骚
我也不知道这股风会刮多久,技术这玩意儿,日新月异。今天你觉得牛逼的东西,明天可能就是一堆垃圾。
AI 原生开发,它不是一个工具的改变,更像是一种“思维范式”的革命。它在逼着我们从“如何实现”的工匠思维,转向“做什么、为什么做”的创造者思维。
低代码的时代,可能还没开始,就已经结束了。它只是一个过渡期的妥协品,一个想走捷径但最终掉进坑里的解决方案。
至于未来会怎么样?我也不知道。可能我们以后写的不是代码,而是“提示词”;面试考的不是算法,而是你跟AI“对话”的能力。
谁说得准呢?
反正我是已经把主力开发工具换成cursor、Claude Code、codex 这些了。一边写,一边让AI帮我优化,或者干脆让它写大段的模板代码,这感觉,真挺爽的。
当然,这都是我个人的一点看法,不一定对,欢迎来杠。评论区聊聊?
来源:juejin.cn/post/7551214653553066019
“全栈模式”必然导致“质量雪崩”!和个人水平关系不大
在经济下行的大背景下,越来越多的中小型企业开始放弃“前后端分离”的人员配置,开始采用“全栈式开发”的模式来进行研发费用的节省。
这方法真那么好吗?
作为一名从“全栈开发”自我阉割成“前端开发”的逆行研发,我有很多话想说。
先从一个活生生的真实案例开始吧。
我认识一个非常优秀的全栈开发,因为名字最后一个字是阳,所以被大家称为“阳神”。
1. “阳神”的“神狗二相性”
阳神当然是牛逼的。
他不仅精通后端开发,更是对前端了解的非常深。这样来说吧:
当他作为后端开发时,他可以是那群后端同事里库表设计最清晰,代码最规范,效率最高的后端。
当他作为前端开发时,他除了比几位高级别前端稍逊一点外,效率和UI还原性都非常高,还会主动封装组件减少耦合。
但是非常奇怪的事情总是会发生,因为一旦阳神不是全职的“后端”或者“前端”时,一旦让他同时操刀“后端+前端”开发任务,作为一名“全栈”来进行业务推进时,他的表现会让人感到惊讶:
他会写出设计糟糕,不规范,职责混乱的代码。
这个现象我把他戏称为“阳神”的“神狗二相性”,作为单一职责时他是“阳神”,同时兼任多职时,他就有非常大的可能降格为“阳狗”。

为什么呢?这是阳神主观上让自己写更糟糕的代码吗?
不是的兄弟,不是的。
这是系统性的崩塌,几乎不以人的意志为转移。换我去也是一样,换你去也是一样。
2. 分工粗化必然导致技术细节的差异
从前,在软件开发的古老行会里,一个学徒需要花很多年才能出师,专门做一把椅子,或者专门雕一朵花。现在,你被要求从伐木到抛光,从结构力学到表面美学,全部一手包办。
生产力在发展,对人的技能要求也在发展。
因此“分工细化”成为了工业革命之后完全不可逆的趋势。
在 IT 产业上也是如此。
“软件开发”经过多年被细化出了前端开发、后端开发、客户端开发、大数据开发 等等多种不同的细分职业。
但是现在有人想通过 粗化 职业分功来达到 “提效” 的目的,在我眼中这就是和客观规律对着干。
人的精力是守恒的。当你需要同时关心useEffect的依赖数组会不会导致无限渲染,和kubectl的配置能不能正确拉起Pod时,你的注意力就被稀释了。你不再有那种“针对一个领域,往深里钻,钻到冒油”的奢侈。
当你脑袋里冒出了一个关于前端工程化优化的问题时,身为全栈的你会本能地冒出另一个念头:
在整个全栈体系内,前端工程化优化是多么边角料且无关痛痒的问题啊,我去深入研究和解决它的性价比实在太低了,算了不想了。
如此一来,无论是后端的性能问题还是前端的性能问题都会变得无关紧要。

结果是,只有业务问题是全栈开发要关心的问题。
2. “岗位对立”与“自我妥协”
在日常开发中,前端开发和后端开发之间互相吐槽争论是再正常不过的话题,而且争论的核心非常简单易懂:
前端:这事儿不能在后端做吗?
后端:这事儿前端不能做吗?
可以的,兄弟,最后你会发现都是可以的,代码里大部分的事情无论是在浏览器端完成还是在服务器里完成都是可行的。
但是,总有一个“哪方更适合做”吧?
- 一个大屏页面的几万几十万条的数据统计,是应该后端做还是前端做?
- 业务数据到Echarts展示数据的格式转换应该后端做还是前端做?
- 用户数据权限的过滤应该后端做还是前端做?
- 一个列表到底要做真分页还是假分页?
- 列表已经返回了全量实体信息,为什么还要再增加一个详情接口?
这都是日常开发时前端和后端都会去争论思考的问题,身处不同的职位,就会引入不同的立场和思考。
- 前端需要去思考页面刷新后状态的留存,js单线程下大量数据处理的卡顿,页面dom树爆表的困境。
- 后端也需要思考并发下服务器资源和内存的分配,可能的死锁问题,以及用户的无状态token如何处理等。
前后端的“争吵”和观点输出是不可避免的。
真理总是越辩越清晰的,后续讨论出的结果多半是最有利于当前现状的。
但如果“前后端”都是同一个人呢?
全栈模式,完美地消灭了这种“有益的摩擦”。当你自己和自己联调时,你不会给自己提挑战灵魂的问题。你不会问:“这个API设计是否RESTful?”因为你赶时间。你也不会纠结:“这个组件的可访问性够好吗?”因为你还得去部署服务器。
这两种思想在你的大脑里打架,最终往往不是最优解胜出,而是最省事的那个方案活了下来。
于是,你的代码里充满了“差不多就行”的妥协。这种妥协,一两个无所谓,当成百上千个“差不多”堆积起来时,质量的基础就酥了。
内部摩擦的消失,使得代码在诞生之初就缺少了一道质量校验的工序。它顺滑地流向生产环境,然后,在某个深夜,轰然引爆。
3. 工程的“不可能三角”
软件开发领域有一个著名的“不可能三角”:
快、好、省,你只能选两样。

全栈模式,在管理者眼中,完美地实现了“省”(一个人干两个人的活)和“快”(省去沟通成本)。那么,被牺牲掉的是谁?
雪崩时,没有一片雪花是无辜的。但更重要的是,当结构性雪崩发生时,问责任何一片雪花,都意义不大。
至于“快、好、省”这三兄弟怎么选?
那主要看老板的认知和他的钱包了。
来源:juejin.cn/post/7555387521451606068
面试官:CDN是怎么加速网站访问的?
做前端项目的时候,经常听到"静态资源要放CDN"这种说法:
- CDN到底是什么,为什么能加速?
- 用户访问CDN的时候发生了什么?
- 前端项目怎么配置CDN?
先说结论
CDN(Content Delivery Network)的核心原理:把内容缓存到离用户最近的服务器上。
没有CDN时,北京用户访问美国服务器,数据要跨越太平洋。有了CDN,数据从北京的CDN节点返回,快得多。
flowchart LR
subgraph 没有CDN
U1[北京用户] -->|跨越太平洋| S1[美国服务器]
end
subgraph 有CDN
U2[北京用户] -->|就近访问| C2[北京CDN节点]
C2 -.->|首次回源| S2[美国服务器]
end
CDN的工作流程
当用户访问一个CDN加速的资源时,会经过这几步:
1. DNS解析
用户访问 cdn.example.com,DNS会返回离用户最近的CDN节点IP。
# 在北京查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 101.37.27.xxx # 北京节点
# 在上海查询
$ dig cdn.example.com
;; ANSWER SECTION:
cdn.example.com. 60 IN A 47.100.99.xxx # 上海节点
同一个域名,不同地区解析出不同IP,这就是CDN的智能调度。
2. 缓存判断
请求到达CDN节点后,节点检查本地是否有缓存:
- 缓存命中:直接返回,速度最快
- 缓存未命中:向源服务器获取,然后缓存起来
flowchart TD
A[用户请求] --> B[CDN边缘节点]
B --> C{本地有缓存?}
C -->|有| D[直接返回]
C -->|没有| E[回源获取]
E --> F[源服务器]
F --> G[返回内容]
G --> H[缓存到边缘节点]
H --> D
classDef hit fill:#d4edda,stroke:#28a745,color:#155724
classDef miss fill:#fff3cd,stroke:#ffc107,color:#856404
class D hit
class E,F,G,H miss
3. 缓存策略
CDN根据HTTP头决定怎么缓存:
# 典型的静态资源响应头
Cache-Control: public, max-age=31536000
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
max-age=31536000:缓存1年public:允许CDN缓存ETag:文件指纹,用于验证缓存是否过期
为什么CDN能加速
1. 物理距离更近
光在光纤中的传播速度约为 20 万公里/秒。北京到美国往返 2 万公里,光传输就要 100ms。
CDN把内容放到离用户近的地方,这个延迟几乎可以忽略。
2. 分散服务器压力
没有CDN,所有请求都打到源服务器。有了CDN,只有第一次请求(缓存未命中)才需要回源。
假设一张图片被访问 100 万次:
- 没有CDN:源服务器处理 100 万次请求
- 有CDN:源服务器只处理几十次(各节点首次回源)
3. 边缘节点优化
CDN服务商的边缘节点通常有这些优化:
- 自动压缩:Gzip/Brotli压缩
- 协议优化:HTTP/2、HTTP/3
- 连接复用:Keep-Alive连接池
- 智能路由:选择最优网络路径
前端项目配置CDN
1. 构建配置
以Vite为例,配置静态资源的CDN地址:
// vite.config.js
export default defineConfig({
base: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
build: {
rollupOptions: {
output: {
// 文件名带hash,便于长期缓存
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
})
Webpack配置类似:
// webpack.config.js
module.exports = {
output: {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com/'
: '/',
filename: 'js/[name].[contenthash].js',
}
}
2. 上传到CDN
构建完成后,把dist目录的文件上传到CDN。大多数CDN服务商都提供CLI工具或API:
# 阿里云OSS + CDN
aliyun oss cp -r ./dist oss://your-bucket/
# AWS S3 + CloudFront
aws s3 sync ./dist s3://your-bucket/
# 七牛云
qshell qupload2 --src-dir=./dist --bucket=your-bucket
3. 缓存策略配置
关键是区分两类文件:
带hash的静态资源(JS、CSS、图片):长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
HTML文件:不缓存或短期缓存
location ~* \.html$ {
expires 0;
add_header Cache-Control "no-cache, must-revalidate";
}
为什么这样设计?因为HTML是入口文件,它引用的JS/CSS带有hash。更新代码时:
- 新的JS文件会有新的hash(
app.abc123.js→app.def456.js) - HTML文件引用新的JS文件
- 用户获取新HTML后,会加载新的JS文件
- 旧的JS文件在CDN上继续存在,不影响正在访问的用户
4. DNS配置
把CDN域名配置成CNAME指向CDN服务商:
# 阿里云CDN
cdn.example.com. IN CNAME cdn.example.com.w.kunlunsl.com.
# Cloudflare
cdn.example.com. IN CNAME example.com.cdn.cloudflare.net.
常见问题
跨域问题
CDN域名和主域名不同,字体文件、AJAX请求可能遇到跨域。
解决方案是在CDN配置CORS头:
# CDN服务器配置
add_header Access-Control-Allow-Origin "https://example.com";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
或者在源服务器的响应里加上CORS头,CDN会透传。
缓存更新问题
发布新版本后,用户还是看到旧内容?
方案一:文件名带hash(推荐)
前面已经提到,文件名带hash,新版本就是新文件,不存在缓存问题。
方案二:主动刷新缓存
// 调用CDN服务商的刷新API
await cdnClient.refreshObjectCaches({
ObjectPath: 'https://cdn.example.com/app.js\nhttps://cdn.example.com/app.css',
ObjectType: 'File'
});
方案三:URL加版本号
<script src="https://cdn.example.com/app.js?v=1.2.3"></script>
不太推荐,因为有些CDN会忽略查询参数。
HTTPS证书
CDN域名也需要HTTPS证书。大多数CDN服务商提供免费证书或支持上传自有证书。
配置方式:
- 在CDN控制台申请免费证书(通常是DV证书)
- 或上传自己的证书(用Let's Encrypt申请)
# 用certbot申请泛域名证书
certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com"
回源优化
如果源服务器压力大,可以启用"回源加速"或"中间源":
用户 → 边缘节点 → 中间源 → 源服务器
中间源作为二级缓存,减少对源服务器的请求。多数CDN服务商默认开启这个功能。
CDN选型
| CDN服务商 | 优势 | 适用场景 |
|---|---|---|
| 阿里云CDN | 国内节点多,生态完整 | 国内业务为主 |
| 腾讯云CDN | 游戏加速好,直播支持强 | 游戏、直播 |
| Cloudflare | 全球节点,有免费套餐 | 出海业务、个人项目 |
| AWS CloudFront | 与AWS生态集成 | 已用AWS的项目 |
| Vercel/Netlify | 前端项目一站式部署 | JAMStack项目 |
个人项目推荐Cloudflare,免费套餐够用,全球节点覆盖好。
企业项目根据用户分布选择。面向国内用户,阿里云/腾讯云更合适;面向全球用户,Cloudflare或CloudFront。
验证CDN效果
浏览器开发者工具
打开Network面板,关注这几个指标:
- TTFB(Time To First Byte):首字节时间,越小越好
- 响应头中的缓存信息:
X-Cache: HIT表示命中CDN缓存
命令行测试
# 查看响应头
curl -I https://cdn.example.com/app.js
# 输出示例
HTTP/2 200
cache-control: public, max-age=31536000
x-cache: HIT from CN-Beijing
在线工具
- Pingdom - 全球多点测速
- GTmetrix - 性能分析
- WebPageTest - 详细的加载瀑布图
小结
CDN加速的核心原理:
- 就近访问:DNS智能解析,把用户引导到最近的节点
- 缓存机制:边缘节点缓存内容,减少回源
- 协议优化:HTTP/2、压缩、连接复用
前端配置CDN的关键:
- 构建时配置publicPath:指向CDN域名
- 文件名带hash:便于长期缓存
- HTML不缓存:确保用户能获取到最新入口
- 处理跨域:配置CORS头
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB
来源:juejin.cn/post/7582438310103613486
为什么大部分程序员成不了架构师?
很多程序员初学编程那会,几乎都有一个成为架构师的梦想。
❝
毕竟不想当架构师的程序员不是一个好程序员。
这里有几个架构师需要具备的能力模型:
❝
技术深度和广度:
- 具备深厚的技术功底,同时对相关领域非常熟悉与了解。
经验积累:
- 具备在某一领域,有非常丰富的行业经验。
- 具体涉及到系统设计、性能优化、风险管理等方面。
业务理解和沟通能力:
- 需要理解业务需求,将业务目标转化为系统设计。
- 需要与不同角色进行高效的沟通,包括与非技术人员的沟通。
领导和管理能力:
- 在一些情况下,架构师可能需要领导团队、制定技术方向。
学习和适应能力:
- 需要不断学习新的技术和趋势,并将其应用到实际项目中。
其实有些程序员可能更喜欢专注于编码本身。
❝
对于涉及更广泛系统设计和管理方面的工作不感兴趣。
他们可能更倾向于深入技术领域而非走向管理和架构方向。
不过能成为架构师还有几个点很关键:
❝
想成为架构师至少要有一个好平台,还要有毅力钻研技术并付诸实践。
- 而且要经历各种各样的场景。
最好还要有一个好团队一起努力,毕竟一个人的精力是有限的。
不过并非每个程序员都适合成为架构师,不同人有不同的兴趣和职业目标。
有啥其他看法,欢迎在评论区留言讨论。
❝
想看技术文章的,可以去我的个人网站:hardyfish.top/
- 目前网站的内容足够应付基础面试(
P6)了!
每日一题
题目描述
❝
给你一个 非空 整数数组
nums,除了某个元素只出现一次以外,其余每个元素均出现两次。
找出那个只出现了一次的元素。
示例 1 :
ini体验AI代码助手代码解读复制代码输入:nums = [2,2,1]
输出:1
示例 2 :
ini体验AI代码助手代码解读复制代码输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
ini体验AI代码助手代码解读复制代码输入:nums = [1]
输出:1
解题思路
❝
位运算
数组中的全部元素的异或运算结果即为数组中只出现一次的数字。
代码实现
Java代码:
Java体验AI代码助手代码解读复制代码class Solution {
public int singleNumber(int[] nums) {
int single = 0;
for (int num : nums) {
single ^= num;
}
return single;
}
}
Python代码:
Python体验AI代码助手代码解读复制代码class Solution:
def singleNumber(self, nums: List[int]) -> int:
return reduce(lambda x, y: x ^ y, nums)
Go代码:
Go体验AI代码助手代码解读复制代码func singleNumber(nums []int) int {
single := 0
for _, num := range nums {
single ^= num
}
return single
}
复杂度分析
❝
时间复杂度:
O(n),其中n是数组长度。
- 只需要对数组遍历一次。
空间复杂度:
O(1)。
链接:https://juejin.cn/post/7459671967306940431
数据可视化神器Heat.js:让你的数据热起来
😱 我发现了一个「零依赖」的数据可视化宝藏!
Hey,前端小伙伴们!今天必须给你们安利一个「让数据说话」的神器——Heat.js!这可不是一个普通的JavaScript库,而是一个能让你的数据「热」起来的魔法工具!

想象一下,当你有一堆枯燥的日期数据,想要以直观、炫酷的方式展示出来时,Heat.js就像一个魔法师,「唰」的一下就能把它们变成色彩斑斓的热图、清晰明了的图表,甚至还有详细的统计分析!
🤩 这个库到底有什么「超能力」?
1. 「零依赖」轻量级选手,绝不拖你后腿 🦵
在这个「依赖地狱」的时代,Heat.js简直就是一股清流!它零依赖,体积小得惊人,加载速度快得飞起!再也不用担心引入一个库就拖慢整个页面加载速度了~
2. 「四种视图」任你选,总有一款适合你 🔄
Heat.js提供了四种不同的视图模式:
- Map视图:就像GitHub贡献图一样炫酷,用颜色深浅展示日期活跃度
- Chart视图:把数据变成专业的图表,让趋势一目了然
- Days视图:专注于展示每一天的详细数据
- Statistics视图:直接给你算出各种统计数据,懒人福音!
想换个姿势看数据?只需轻轻一点,瞬间切换~
3. 「51种语言」支持,真正的「世界公民」🌍
担心你的国际用户看不懂?不存在的!Heat.js支持51种语言,从中文、英文到阿拉伯语、冰岛语,应有尽有!你的应用可以轻松走向全球,再也不用为语言本地化发愁了~
4. 「数据导入导出」无所不能,数据来去自由 📤📥
想导出数据做进一步分析?没问题!Heat.js支持导出为CSV、JSON、XML、TXT、HTML、MD和TSV等多种格式,任你选择!
想导入已有数据快速生成热图?同样简单!支持从JSON、TXT、CSV和TSV导入,甚至还支持拖拽上传,简直不要太方便!
5. 「12种主题」随意切换,颜值与实用并存 💅
担心热图不好看?Heat.js提供了12种精心设计的主题,包括暗黑模式和明亮模式,让你的数据可视化既专业又美观!无论你的网站是什么风格,都能找到匹配的主题~
💡 这个神奇的库可以用来做什么?
1. 「活动追踪」,让你的用户活跃起来 📊
想展示用户的登录活跃度?想用热图展示文章的发布频率?Heat.js帮你轻松实现!就像GitHub的贡献图一样,让你的用户看到自己的「努力成果」,成就感满满!
2. 「数据分析」,让你的决策更明智 🧠
通过Heat.js的Statistics视图,你可以快速获取数据的各种统计信息,比如最活跃的月份、平均活动频率等。这些数据可以帮助你做出更明智的产品决策,优化用户体验!
3. 「趋势展示」,让你的报告更有说服力 📈
想在报告中展示某个指标的变化趋势?Heat.js的Chart视图可以将枯燥的数据变成直观的图表,让你的报告更有说服力,老板看了都说好!
🛠️ 如何用最简单的方式用上这个神器?
第一步:「把宝贝抱回家」📦
npm install jheat.js
或者直接使用CDN:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.js.min.css">
<script src="https://cdn.jsdelivr.net/gh/williamtroup/Heat.js@4.5.1/dist/heat.min.js"></script>
第二步:「给它找个家」🏠
<div id="heat-map" data-heat-js="{ 'views': { 'map': { 'showDayNames': true } } }">
<!-- 这里将显示你的热图 -->
</div>
第三步:「喂它数据」🍽️
// 添加日期数据
let newDateObject = new Date();
$heat.addDate("heat-map", newDateObject, "Trend Type 1", true);
// 移除日期数据(如果需要)
// $heat.removeDate("heat-map", newDateObject, "Trend Type 1", true);
三步搞定!就是这么简单!
🎯 为什么Heat.js值得你拥有?
1. 「简单易用」,小白也能轻松上手 👶
Heat.js的API设计非常友好,文档也很详细,即使是JavaScript初学者也能快速上手。几个简单的步骤,就能实现专业级的数据可视化效果!
2. 「高度定制」,满足你的各种需求 ⚙️
无论是颜色、样式,还是功能配置,Heat.js都提供了丰富的选项。你可以根据自己的需求,定制出独一无二的数据可视化效果!
3. 「响应式设计」,在任何设备上都完美展示 📱💻
Heat.js完全支持响应式设计,无论是在手机、平板还是电脑上,都能完美展示。你的数据可视化效果将在任何设备上都一样出色!
4. 「TypeScript支持」,框架党福利 🎉
如果你使用React、Angular等现代前端框架,Heat.js的TypeScript支持会让你用得更爽!类型定义清晰,代码提示完善,开发体验一流!
🚀 最后想说的话...
在这个「数据为王」的时代,如何让数据更直观、更有说服力,是每个开发者都需要面对的挑战。而Heat.js,就是帮助你征服这个挑战的绝佳工具!
它轻量级、零依赖、功能强大、易于使用,无论是个人项目还是企业应用,都能轻松胜任。最重要的是,它让数据可视化不再是一件复杂的事情,而是一种乐趣!
所以,还等什么呢?赶紧去GitHub上给Heat.js点个Star⭐,然后在你的项目中用起来吧!相信我,它一定会给你带来惊喜!
✨ 祝大家的数据可视化之路一帆风顺,让我们一起用Heat.js让数据「热」起来!✨
来源:juejin.cn/post/7578161740467421235
解决网页前端中文字体包过大的几种方案
最近想给我的博客的网页换个字体,在修复了历史遗留的一些bug之后,去google fonts上找了自己喜欢的字体,本地测试和自己的设备发现没问题后,便以为OK了。
但是当我接朋友的设备打开时,发现网页依然是默认字体。这时候我才发现,我的设备能够翻墙,所以能够使用Google CDN服务,但是对于我的其他读者们,在大陆内是访问不了Google的,便也无法渲染字体了。
于是为了解决这个问题,我尝试了各种办法比如格式压缩,子集化(Subset),分包等等,最后考虑到本站的实际情况选用了一种比较邪门的方法,让字体压缩率达到了惊人的98.5%!于是,这篇文章就是对这个过程的总结。也希望这篇文章能够帮助到你。😊
想要自定义网站的字体,最重要的其实就是字体包的获取。大体上可以分为两种办法:在线获取和网站本地部署。
在线获取──利用 CDN 加速服务
CDN(Content Delivery Network) 意为内容配送网络。你可以简单理解为是一种“就近给你东西”的互联网加速服务。
传统不使用 CDN 服务的是这样的: User ←→ Server,如果相聚遥远,效果显然很差。
使用了 CDN 服务是这样的: User ←→ CDN Nodes ←→ Server,CDN 会提前把你的网站静态资源缓存到各个节点,但你需要时可以直接从最近的节点获取。
全球有多家CDN服务提供商,Google Fonts使用的CDN服务速度很快。所以如果在网络畅通的情况下,使用Google Fonts API是最简单省事的!
你可以直接在文件中导入Google fonts API:
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Merriweather:ital,opsz,wght@0,18..144,733;1,18..144,733&family=Noto+Serif+SC:wght@500&display=swap');
这样网站它便会自动向最近的Google CDN节点请求资源。
当然,这些都是建立在网络状态畅通无阻的情况下。大陆用户一般使用不了Google服务,但并不意味着无法使用CDN服务。国内的腾讯云,阿里云同样提供高效的服务,但具体的规则我并不了解,请自行阅读研究。
本地部署
既然用不了在线的,那就只能将字体包文件一并上传到服务器上了。
这种做法不需要依赖外部服务,但缺点是字体包的文件往往很大,从进入网站到彻底加载完成的时间会及其漫长!而且这种问题尤其在中日韩(CJK)字体上体现的十分明显。
以本站为例,我主要采用了三种字体:Merriweather, Inter, Noto Serif SC. 其中每种字体都包含了Bold和Regular两种格式。前面两种都属于西文字体,每种格式原始文件大小都在200kb-300kb,但是到了思源宋体这里,仅仅一种格式的字体包大小就达到了足足14M多。如果全部加载完,恐怕从进入网站到完全渲染成功,需要耽误个2分钟。所以将原始字体包文件上传是极不可取的做法!
为了解决这个问题,我在网上查阅资料,找到了三种做法。
字体格式转换(WOFF2)
WOFF2 (Web Open Font Format 2.0) 是一种专为 Web 设计的字体文件格式,旨在提供更高的压缩率和更快的加载速度,也是是目前在 Web 上部署自定义字体的推荐标准。它本质上是一种将 TTF 或 OTF 字体数据进行高度压缩后的格式,目前已经获得了所有主流浏览器的广泛支持。
我们可以找一个在线的字体格式转化网站来实现格式的转化。本文我们以NotoSerifSC-Bold.ttf为例,转换后的NotoSerifSC-Bold.woff2文件只有5.8M左右,压缩率达到了60%!
但是,这仍旧是不够的,仅两个中文字体包加起来也已经快12M,还没有算上其他字体。这对于一个网页来说依然是灾难性的。我们必须寻找另一种方法。
子集化处理(Subset)
中国人都知道,虽然中文的字符加起来有2万多个,但是我们平常交流基本只会用到3000多个,范围再大一点,6000多个字符已经可以覆盖99%的使用场景。这意味着:
我们根本不需要保留所有字符,而只需要保留常用的几千个汉字即可。
于是这就给了我们解决问题的思路了。
首先我们可以去寻找中文常用汉字字符表,这里我获取的资源是 All-Chinese-Character-Set。我们将文件下载解压后,可以在里面找到各种各样按照字频统计的官方文件。这里我们就以《通用规范汉字表》(2013年)一级字和二级字为例。我们创建一个文档char_set.txt并将一级字和二级字的内容全部复制进去。这份文档就是我们子集化的对照表。
接着我们需要下载一个字体子集化工具,这里使用的是Python中的fonttools库,它提供了许多工具(比如我们需要的pyftsubset)可以在命令行中执行子集化、字体转化字体操作。
我们安装一下这个库和对应的依赖(在这之前确保你的电脑上安装了Python和pip,后者一般官方安装会自带)
pip install fonttools brotli zopfli
然后找到我们字体包对应的文件夹,将原来的char_set.txt复制到该文件夹内,在该文件下打开终端,然后以NotoSerifSC-Bold.ttf为例,输入以下命令:
pyftsubset NotoSerifSC-Bold.ttf --output-file=NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli
过一会就能看到会输出一个NotoSerifSC-Bold.subset.woff2的文件。
我们欣喜的发现这个文件的大小竟然只有980KB。至此,我们已经已经将压缩率达到了93%!到这一步,其实直接部署也并没有十分大问题,不过从加载进去到完全渲染,可能依然需要近十秒左右,我们依然还有优化空间。
分包处理实现动态加载
这个方法是我阅读这篇文章了解到的,但是遗憾的是我并没有在自己的网站上实现,不过失败的尝试也让我去寻找其它的方法,最终找到适用本站的一种极限字体渲染的方法,比这三种的效果还要好。下面我依然简单介绍一下这个方法的原理,想更了解可以通过看到最后通过参考资料去进一步了解。
在2017年,Google Fonts团队提出切片字体,因为他们发现:绝大部分网站只需要加载CJK字体包的小部分内容即可覆盖大部分场景。基于适用频率统计,他们将字符分成多个切片,再按 Unicode 编码对剩余字符进行分类。
怎么理解呢?他其实就是把所有的字符分成许多个小集合,每个集合里面都包含一定数量的字符,在靠前的一些集合中,都是我们常用的汉字,越到后,字形越复杂,使用频率也越低。当网页需要加载字体文件时,它是以切片为单位加载的。这意味,只有当你需要用到某个片区的字符时,这个片区才会被加载。
这种方式的好处时,能够大大加快网站加载速率。我们不用每次都一次性把全部字符加载,而是按需加载。这项技术如今已经被Noto Sans字体全面采用。
但是我们需要本地部署的话,需要多费一点功夫。这里我们利用中文网字计划的在线分包网站来实现。
我们将需要的字体上传进行分包,可以观察到输出结果是一系列以哈希值命名的woff2文件。分包其实就是做切分,把每个切分后的区域都转化为一份体积极小的woff2文件。
下载压缩包,然后可以将里面的文件夹导入你的项目,并引用文件夹下的result.css即可。理论上,当网站需要加载渲染某个字体时,它会根据css里面的规则去寻找到对应的分包再下载。每个包的体积极小,网站加载的速度应该提升的很明显。

我的实践──将字符压缩到极限
我的方法可以理解为子集化的一种,只不过我的做法更加的极端一些──只保留文章出现的字符!
根据统计结果,截止到这篇post发布,我的文章总共出现的所有字符数不到1200个(数据来源见下文),所以我们可以做的更激进一些,只需将文章出现的中文字符全部记录下来,制成一张专属于自己网站的字符表,然后在每次发布文章时动态更新,这样我们能够保证字体完整渲染,并且处于边界极限状态!
实现这个个性化字符表char_set.txt的核心是一个提取文章中文字符的算法。这部分我是通过Gemini生成了一个update_lists.cpp文件,他能够识别_posts/下面所有文章,并输出到根目录的char_set.txt中,你可以根据代码内容进行自定义的修改:
/**
* @file update_lists.cpp
* @brief Scans Markdown files in /_posts/ and updates char_set.txt in root.
* @author Gemini
* @date 2025-11-28
*/
#include
#include
#include
#include
#include
#include
namespace fs = std::filesystem;
namespace char_collector {
const std::string kRegistryFilename = "char_set.txt";
const std::string kMarkdownExt = ".md";
const uint32_t kCJKStart = 0x4E00;
const uint32_t kCJKEnd = 0x9FFF;
bool NextUtf8Char(std::string::const_iterator& it,
const std::string::const_iterator& end,
uint32_t& out_codepoint,
std::string& out_bytes) {
if (it == end) return false;
unsigned char c1 = static_cast<unsigned char>(*it);
out_bytes.clear();
out_bytes += c1;
if (c1 < 0x80) { out_codepoint = c1; it++; return true; }
if ((c1 & 0xE0) == 0xC0) {
if (std::distance(it, end) < 2) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
out_codepoint = ((c1 & 0x1F) << 6) | (c2 & 0x3F);
out_bytes += *(it + 1); it += 2; return true;
}
if ((c1 & 0xF0) == 0xE0) {
if (std::distance(it, end) < 3) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
out_codepoint = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); it += 3; return true;
}
if ((c1 & 0xF8) == 0xF0) {
if (std::distance(it, end) < 4) return false;
unsigned char c2 = static_cast<unsigned char>(*(it + 1));
unsigned char c3 = static_cast<unsigned char>(*(it + 2));
unsigned char c4 = static_cast<unsigned char>(*(it + 3));
out_codepoint = ((c1 & 0x07) << 18) | ((c2 & 0x3F) << 12) |
((c3 & 0x3F) << 6) | (c4 & 0x3F);
out_bytes += *(it + 1); out_bytes += *(it + 2); out_bytes += *(it + 3); it += 4; return true;
}
it++; return false;
}
bool IsChineseChar(uint32_t codepoint) {
return (codepoint >= kCJKStart && codepoint <= kCJKEnd);
}
class CharManager {
public:
CharManager() = default;
void LoadExistingChars(const std::string& filepath) {
std::ifstream infile(filepath);
if (!infile.is_open()) {
std::cout << "Info: " << filepath << " not found or empty. Starting fresh." << std::endl;
return;
}
std::string line;
while (std::getline(infile, line)) {
ProcessString(line, false);
}
std::cout << "Loaded " << existing_chars_.size()
<< " unique characters from " << filepath << "." << std::endl;
}
void ScanDirectory(const std::string& directory_path) {
if (!fs::exists(directory_path)) {
std::cerr << "Error: Directory '" << directory_path << "' does not exist." << std::endl;
return;
}
for (const auto& entry : fs::directory_iterator(directory_path)) {
if (entry.is_regular_file() &&
entry.path().extension() == kMarkdownExt) {
ProcessFile(entry.path().string());
}
}
}
void SaveNewChars(const std::string& filepath) {
if (new_chars_list_.empty()) {
std::cout << "No new Chinese characters found." << std::endl;
return;
}
std::ofstream outfile(filepath, std::ios::app);
if (!outfile.is_open()) {
std::cerr << "Error: Could not open " << filepath << " for writing." << std::endl;
return;
}
for (const auto& ch : new_chars_list_) {
outfile << ch;
}
std::cout << "Successfully added " << new_chars_list_.size()
<< " new characters to " << filepath << std::endl;
}
private:
std::unordered_set existing_chars_;
std::vector new_chars_list_;
void ProcessFile(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) return;
std::cout << "Scanning: " << fs::path(filepath).filename().string() << std::endl;
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
ProcessString(content, true);
}
void ProcessString(const std::string& content, bool track_new) {
auto it = content.begin();
auto end = content.end();
uint32_t codepoint;
std::string bytes;
while (NextUtf8Char(it, end, codepoint, bytes)) {
if (IsChineseChar(codepoint)) {
if (existing_chars_.find(bytes) == existing_chars_.end()) {
existing_chars_.insert(bytes);
if (track_new) {
new_chars_list_.push_back(bytes);
}
}
}
}
}
};
}
int main() {
char_collector::CharManager manager;
manager.LoadExistingChars(char_collector::kRegistryFilename);
manager.ScanDirectory("_posts");
manager.SaveNewChars(char_collector::kRegistryFilename);
return 0;
}
然后我们在终端编译一下再运行即可:
clang++ update_lists.cpp -o update_lists && ./update_lists
然后我们就会发现这张独属于本站的字符表生成了!🥳
为了方便操作,我们把原始的ttf文件放入仓库的/FontRepo/下(最后记得在.gitignore添加这个文件夹!),然后稍微修改一下之前子集化的命令就可以了:
pyftsubset /FontRepo/NotoSerifSC-Bold.ttf --output-file=/assets/fonts/noto-serif-sc/NotoSerifSC-Bold.subset.woff2 --flavor=woff2 --text-file=char_set.txt --no-hinting --with-zopfli
可以看到,最终输出的文件只有200K!压缩率达到了98.5%!
但是这个方法就像前面说的,处于字体渲染的边界。但凡多出一个字符表中的符号,那么这个字符就无法渲染,会回退到系统字体,看起来格外别扭。所以,在每次更新文章前,我们都需要运行一下./update_lists。此外,还存在一个问题,每次更新产生新的子集化文件时,都需要把旧的子集化文件删除,防止旧文件堆积。
这些过程十分繁琐而且耗费时间,所以我们可以写一个bash脚本来实现这个过程的自动化。我这里同样是求助了Gemini,写了一个build_fonts.sh:
#!/bin/bash
set -e # 遇到错误立即停止执行
# ================= 配置区域 =================
# 字体源文件目录
SRC_DIR="FontRepo"
# 字体输出目录
OUT_DIR="assets/fonts/noto-serif-sc"
# 字符列表文件
CHAR_LIST="char_set.txt"
# C++ 更新工具
UPDATE_TOOL="./updateLists"
# 确保输出目录存在
if [ ! -d "$OUT_DIR" ]; then
echo "创建输出目录: $OUT_DIR"
mkdir -p "$OUT_DIR"
fi
# ================= 第一步:更新字符表 =================
echo "========================================"
echo ">> [1/3] 正在更新字符列表..."
if [ -x "$UPDATE_TOOL" ]; then
$UPDATE_TOOL
else
echo "错误: 找不到可执行文件 $UPDATE_TOOL 或者没有执行权限。"
echo "请尝试运行: chmod +x updateLists"
exit 1
fi
# 检查 char_set.txt 是否成功生成
if [ ! -f "$CHAR_LIST" ]; then
echo "错误: $CHAR_LIST 未找到,字符表更新可能失败。"
exit 1
fi
echo "字符列表更新完成。"
# ================= 定义子集化处理函数 =================
process_font() {
local font_name="$1" # 例如: NotoSerifSC-Regular
local input_ttf="$SRC_DIR/${font_name}.ttf"
local final_woff2="$OUT_DIR/${font_name}.woff2"
local temp_woff2="$OUT_DIR/${font_name}.temp.woff2"
echo "----------------------------------------"
echo "正在处理字体: $font_name"
# 检查源文件是否存在
if [ ! -f "$input_ttf" ]; then
echo "错误: 源文件 $input_ttf 不存在!"
exit 1
fi
# 2. 调用 fonttools (pyftsubset) 生成临时子集文件
# 使用 --obfuscate-names 可以进一步减小体积,但这里只用基础参数以保证稳定性
echo "正在生成子集 (TTF -> WOFF2)..."
pyftsubset "$input_ttf" \
--flavor=woff2 \
--text-file="$CHAR_LIST" \
--output-file="$temp_woff2"
# 3. & 4. 删除旧文件并重命名 (更新逻辑)
if [ -f "$temp_woff2" ]; then
if [ -f "$final_woff2" ]; then
echo "删除旧文件: $final_woff2"
rm "$final_woff2"
fi
echo "重命名新文件: $temp_woff2 -> $final_woff2"
mv "$temp_woff2" "$final_woff2"
echo ">>> $font_name 更新成功!"
else
echo "错误: 子集化失败,未生成目标文件。"
exit 1
fi
}
# ================= 第二步 & 第三步:执行转换 =================
echo "========================================"
echo ">> [2/3] 开始字体子集化处理..."
# 处理 Regular 字体
process_font "NotoSerifSC-Regular"
# 处理 Bold 字体
process_font "NotoSerifSC-Bold"
echo "========================================"
echo ">> [3/3] 所有任务圆满完成!"
如此一来,以后每次更新完文章,都只需要在终端输入./build_fonts.sh就可以完成字符提取、字体包子集化、清除旧字体包文件的过程了。

一点感想
在这之前另外讲个小故事,我尝试更换字体之前发现自定义的字体样式根本没有用,后来检查了很久,发现竟然是2个月前AI在我代码里加的一句font-family:'Noto Serif SC',而刚好他修改的又是优先级最高的文件,所以后面怎么修改字体都没有用。所以有时候让AI写代码前最好先搞清除代码的地位i,并且做好为AI代码后果负全责的准备。
更改网站字体其实很多时候属于锦上添花的事情,因为很多读者其实并不会太在意网站的字体。但不幸的是我对细节比较在意,或者说有种敝帚自珍的感觉吧,想慢慢地把网站装饰得舒适一些,所以才总是花力气在一些细枝末节的事情上。更何况,我是懂一点点设计的,有时候看见一些非常丑的Interface心里是很难受的。尽管就像绝大部分人理解不了设计师在细节上的别有用心一样,绝大部分人也不会在意一个网站的字体如何,但是我自己的家,我想装饰地好看些,对我来说就满足了。
更不要说,如果不去折腾这些东西,怎么可能会有这篇文章呢?如果能够帮助到一些人,也算是在世界留下一点价值了。
参考资料及附录
- 参考资料
a. 网页中文字体加载速度优化
b. 缩减网页字体大小
- 让Gemini生成代码时的Prompt:
---Prompt 1---
# 任务名称:创建脚本实现对字符的收集
请利用C++来完成一下任务要求:
1. 该脚本能够读取项目目录下的markdown文件,并且能够识别当中所有的中文字符,将该中文字符与`/char_test/GeneralUsedChars.txt`的字符表进行查重比较:
若该字在表中存在,则跳过,处理下一个字;
若不存在,则将该字添加到表中,然后继续处理下一个字符
2. 请设计一个高效的算法,尤其是在字符查重的过程中,你需要设计一个高效且准确率高的算法
3. 请注意脚本的通用性,你需要考虑到这个项目以后可能会继续增加更多的markdown文件,所以你不应该仅仅只是处理现有的markdown文件,还需要考虑到以后的拓展性
4. 如果可以的话,尽可能使用C++来实现,因为效率更高
---Prompt 2---
可以了,现在我要求你编写一个脚本以实现自动化,要求如下:
1. 脚本运行时,首先会调用项目根目录下的updateLists可执行文件,更新char_set.txt
2. 接着,脚本会调用fonttools工具,对路径在`/FontRepo/`下的两个文件进行ttf到woff2的子集化转化,其中这两个字体文件的名字分别为`NotoSerifSC-Regular.ttf`和`NotoSerifSC-Bold.ttf`。
3. 转化好的子集文件应该输出到 `/assets/fonts/noto-serif-sc/`文件夹下。
4. 将`/assets/fonts/noto-serif-sc/`文件夹下原本已经存在的两个字体文件`NotoSerifSC-Bold.woff2`和`NotoSerifSC-Regular.woff2`删除,然后将新得到子集化文件重新命名为这两个删除了的文件的名字。这一步相当于完成了字体文件的更新
请注意文件的命名,尤其是不要搞错字号,新子集文件和旧子集文件。
请注意在子集化步骤的bash命令,环境已经安装好fonttools及其对应依赖,你可以参考下面这个命令来使用,或者使用更好更稳定的用法:
pyftsubset --flavor=woff2 --text-file= --output-file=
(再次注意输出路径)
- 最终实践效果(以
NotoSerifSC-Bold为例)处理方式 字体包体积 压缩率 无处理 14.462M 0% 格式转化 5.776M 60.06% 子集化处理 981K 93.21% 分包处理 依据动态加载量而定 无 我的实践 216K 98.5%
来源:juejin.cn/post/7578699866181238822
一个AI都无法提供的html转PDF方案
这也许就是AI无法代替人的原因,只需一行代码就可以实现纯前端 html 转矢量 pdf 的功能
// 引入 dompdf.js库
import dompdf from "dompdf.js";
dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});
实现效果(复杂表格)

1. 在线体验
2. Git 仓库地址 (欢迎 Star⭐⭐⭐)
3. 生成 PDF
在前端生态里,把网页内容生成 PDF 一直是一个常见但不简单的需求。从报表导出、小票生成、合同下载到打印排版,很多项目或多或少都会遇到。市面上常见的方案大致有以下几类:
- 服务端渲染 PDF(后端库如 wkhtmltopdf、PrinceXML 等)
- 客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF
- 前端调用相关 pdf 生成库来生成 PDF(如 pdfmake,jspdf,pdfkit)
但是这些方案都有各自的局限性,
- 比如服务端渲染 PDF 对服务器资源要求高,需要后端参与。
- html2canvas + jsPDF 需要将 html 内容渲染为图片,再将图片封装为 PDF,速度会比较慢,而且生成体积会比较大,内容会模糊,限制于 canvas 生成高度,不能生成超过 canvas 高度的内容。
- 而前端调用相关 pdf 生成库来生成 PDF 则需要对相关库有一定的了解,api 比较复杂,学习使用成本很高。
使用 jspdf 生成如图简单的 pdf

就需要如此复杂的代码,如果要生成复杂的 pdf, 比如包含表格、图片、图表等内容,那使用成本就更高了。
function generateChinesePDF() {
// Check if jsPDF is loaded
if (typeof window.jspdf === "undefined") {
alert("jsPDF library has not finished loading, please try again later");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
// Note: Default jsPDF does not support Chinese, this is just a demo
// In real projects you need to add Chinese font support
doc.setFontSize(16);
doc.text("Chinese Text Support Demo", 20, 30);
doc.setFontSize(12);
doc.text("Note: Default jsPDF does not support Chinese characters.", 20, 50);
doc.text("You need to add Chinese font support for proper display.", 20, 70);
// Draw some graphics for demonstration
doc.setFillColor(255, 182, 193);
doc.rect(20, 90, 60, 30, "F");
doc.setTextColor(0, 0, 0);
doc.text("Pink Rectangle", 25, 108);
doc.setFillColor(173, 216, 230);
doc.rect(100, 90, 60, 30, "F");
doc.text("Light Blue Rectangle", 105, 108);
doc.save("chinese-example.pdf");
}
但是现在,有了 dompdf.js,你只需要一行代码,就可以完成比这个复杂 10 倍的 PDF 生成任务,html页面所见即所得,可以将复杂的css样式转化成pdf
dompdf(document.querySelector("#capture")).then(function (blob) {
//文件操作
});
而且,dompdf.js 生成的 PDF 是矢量的,非图片式的,高清晰度的,文字可以选中、复制、搜索等操作(在支持的 PDF 阅读器环境下),区别于客户端将 HTML 渲染为图片(如 html2canvas + jsPDF)然后再封装为 PDF。
具体可以去体验 立即体验 https://dompdfjs.lisky.com.cn
4. dompdf.js 是如何实现的?
其实 dompdf.js 也是基于 html2canvas+jspdf 实现的,但是为什么 dompdf.js 生成的 pdf 文件可以二次编辑,更清晰,体积小呢?
不同于普通的 html2canvas + jsPDF 方案,将 dom 内容生成为图片,再将图片内容用 jspdf 绘制到 pdf 上,这就导致了生成的 pdf 文件体积大,无法编辑,放大后会模糊。
html2canvas 原理简介
1. DOM 树遍历
html2canvas 从指定的 DOM 节点开始,递归遍历所有子节点,构建一个描述页面结构的内部渲染队列。
2. 样式计算
对每个节点调用 window.getComputedStyle() 获取最终的 CSS 属性值。这一步至关重要,因为它包含了所有 CSS 规则(内联、内部、外部样式表)层叠计算后的最终结果。
3. 渲染模型构建
将每个 DOM 节点和其计算样式封装成渲染对象,包含绘制所需的完整信息:位置(top, left)、尺寸(width, height)、背景、边框、文本内容、字体属性、层级关系(z-index)等。
4. Canvas 上下文创建
在内存中创建 canvas 元素,获取其 2D 渲染上下文(CanvasRenderingContext2D)。
5. 浏览器绘制模拟
按照 DOM 的堆叠顺序和布局规则,遍历渲染队列,将每个元素绘制到 Canvas 上。这个过程实质上是将 CSS 属性"翻译"成对应的绘制 API 调用:
| CSS 属性 | 传统 Canvas API | dompdf.js 中的 jsPDF API |
|---|---|---|
background-color | ctx.fillStyle + ctx.fillRect() | doc.setFillColor() + doc.rect(x, y, w, h, 'F') |
border | ctx.strokeStyle + ctx.strokeRect() | doc.setDrawColor() + doc.rect(x, y, w, h, 'S') |
color, font-family, font-size | ctx.fillStyle, ctx.font + ctx.fillText() | doc.setTextColor() + doc.setFont() + doc.text() |
border-radius | arcTo() 或 bezierCurveTo() 创建剪切路径 | doc.roundedRect() 或 doc.lines() 绘制圆角 |
image | ctx.drawImage() | doc.addImage() |
核心创新:API 替换,底层是封装了 jsPDF 的 API
dompdf.js 的关键突破在于改造了 html2canvas 的 canvas-renderer.ts 文件,将原本输出到 Canvas 的绘制 API 替换为 jsPDF 的 API 调用。这样就实现了从 DOM 直接到 PDF 的转换,生成真正可编辑、可搜索的 PDF 文件,而不是传统的图片格式。
目前实现的功能
1. 文字绘制 (颜色,大小)
2. 图片绘制 (支持 jpeg, png 等格式)
3. 背景,背景颜色 (支持合并单元格)
4. 边框,复杂表格绘制 (支持合并单元格)
5. canvas (支持多种图表类型)
6. svg (支持 svg 元素绘制)
7. 阴影渲染 (使用 foreignObjectRendering,支持边框阴影渲染)
8. 渐变渲染 (使用 foreignObjectRendering,支持背景渐变渲染)
7.使用
安装
npm install dompdf.js --save
CDN 引入
<script src="https://cdn.jsdelivr.net/npm/dompdf.js@latest/dist/dompdf.js"></script>
基础用法
import dompdf from "dompdf.js";
dompdf(document.querySelector("#capture"), {
useCORS: true, //是否允许跨域
})
.then(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "example.pdf";
document.body.appendChild(a);
a.click();
})
.catch(function (err) {
console.log(err, "err");
});
写在最后
dompdf.js 让前端 PDF 生成变得前所未有的简单:无需后端、无需繁琐配置、一行代码即可输出矢量、可检索、可复制的专业文档。无论是简历、报告还是发票,它都能轻松胜任。 欢迎在你的项目中使用它 。
如果它帮到了你,欢迎去 github.com/lmn1919/dom… 点个 Star,提优化,共建项目。
来源:juejin.cn/post/7559886023661649958
从前端的角度出发,目前最具性价比的全栈路线是啥❓❓❓
我正在筹备一套前端工程化体系的实战课程。如果你在学习前端的过程中感到方向模糊、技术杂乱无章,那么前端工程化将是你实现系统进阶的最佳路径。它不仅能帮你建立起对现代前端开发的整体认知,还能提升你在项目开发、协作规范、性能优化等方面的工程能力。
✅ 本课程覆盖构建工具、测试体系、脚手架、CI/CD、Docker、Nginx 等核心模块,内容体系完整,贯穿从开发到上线的全流程。每一章节都配有贴近真实场景的企业级实战案例,帮助你边学边用,真正掌握现代团队所需的工程化能力,实现从 CRUD 开发者到工程型前端的跃迁。
详情请看前端工程化实战课程
学完本课程,对你的简历和具体的工作能力都会有非常大的提升。如果你对此项目感兴趣,或者课程感兴趣,可以私聊我微信 yunmz777
今年大部分时间都是在编码上和写文章上,但是也不知道自己都学到了啥,那就写篇文章来盘点一下目前的技术栈吧,也作为下一年的参考目标,方便知道每一年都学了些啥。

我的技术栈
首先我先来对整体的技术做一个简单的介绍吧,然后后面再对当前的一些技术进行细分吧。
React、Typescript、React Native、mysql、prisma、NestJs、Redis、前端工程化。
React
React 这个框架我花的时间应该是比较多的了,在校期间已经读了一遍源码了,对这些原理已经基本了解了。在随着技术的继续深入,今年毕业后又重新开始阅读了一遍源码,对之前的认知有了更深一步的了解。
也写了比较多跟 React 相关的文章,包括设计模式,原理,配套生态的使用等等都有一些涉及。
在状态管理方面,redux,zustand 我都用过,尤其在 Zustand 的使用上,我特别喜欢 Zustand,它使得我能够快速实现全局状态管理,同时避免了传统 Redux 中繁琐的样板代码,且性能更优。也对 Zustand 有比较深入的了解,也对其源码有过研究。
NextJs
Next.js 是一个基于 React 的现代 Web 开发框架,它为开发者提供了一系列强大的功能和工具,旨在优化应用的性能、提高开发效率,并简化部署流程。Next.js 支持多种渲染模式,包括服务器端渲染(SSR)、静态生成(SSG)和增量静态生成(ISR),使得开发者可以根据不同的需求选择合适的渲染方式,从而在提升页面加载速度的同时优化 SEO。
在路由管理方面,Next.js 采用了基于文件系统的路由机制,这意味着开发者只需通过创建文件和文件夹来自动生成页面路由,无需手动配置。这种约定优于配置的方式让路由管理变得直观且高效。此外,Next.js 提供了动态路由支持,使得开发者可以轻松实现复杂的 URL 结构和参数化路径。
Next.js 还内置了 API 路由,允许开发者在同一个项目中编写后端 API,而无需独立配置服务器。通过这种方式,前后端开发可以在同一个代码库中协作,大大简化了全栈开发流程。同时,Next.js 对 TypeScript 提供了原生支持,帮助开发者提高代码的可维护性和可靠性。
Typescript
今年所有的项目都是在用 ts 写了,真的要频繁修改的项目就知道用 ts 好处了,有时候用 js 写的函数修改了都不知道怎么回事,而用了 ts 之后,哪里引用到的都报红了,修改真的非常方便。
今年花了一点时间深入学习了一下 Ts 类型,对一些高级类型以及其实现原理也基本知道了,明年还是多花点时间在类型体操上,除了算法之外,感觉类型体操也可以算得上是前端程序员的内功心法了。
React Native
不得不说,React Native 不愧是接活神器啊,刚学完之后就来了个安卓和 ios 的私活,虽然没有谈成。
React Native 和 Expo 是构建跨平台移动应用的两大热门工具,它们都基于 React,但在功能、开发体验和配置方式上存在一些差异。React Native 是一个开放源代码的框架,允许开发者使用 JavaScript 和 React 来构建 iOS 和 Android 原生应用。Expo 则是一个构建在 React Native 之上的开发平台,它提供了一套工具和服务,旨在简化 React Native 开发过程。
React Native 的核心优势在于其高效的跨平台开发能力。通过使用 React 语法和组件,开发者能够一次编写应用的 UI 和逻辑,然后部署到 iOS 和 Android 平台。React Native 提供了对原生模块的访问,使开发者能够使用原生 API 来扩展应用的功能,确保性能和用户体验能够接近原生应用。
Expo 在此基础上进一步简化了开发流程。作为一个开发工具,Expo 提供了许多内置的 API 和组件,使得开发者无需在项目中进行繁琐的原生模块配置,就能够快速实现设备的硬件访问功能(如摄像头、位置、推送通知等)。Expo 还内置了一个开发客户端,使得开发者可以实时预览应用,无需每次都进行完整的构建和部署。
另外,Expo 提供了一个完全托管的构建服务,开发者只需将应用推送到 Expo 服务器,Expo 就会自动处理 iOS 和 Android 应用的构建和发布。这大大简化了应用的构建和发布流程,尤其适合不想处理复杂原生配置的开发者。
然而,React Native 和 Expo 也有各自的局限性。React Native 提供更大的灵活性和自由度,开发者可以更自由地集成原生代码或使用第三方原生库,但这也意味着需要更多的配置和维护。Expo 则封装了很多功能,简化了开发,但在需要使用某些特定原生功能时,开发者可能需要“弹出”Expo 的托管环境,进行额外的原生开发。
样式方案的话我使用的是 twrnc,大部分组件都是手撸,因为有 cursor 和 chatgpt 的加持,开发效果还是杠杠的。
rn 原理也争取明年能多花点时间去研究研究,不然对着盲盒开发还是不好玩。
Nestjs
NestJs 的话没啥好说的,之前也都写过很多篇文章了,感兴趣的可以直接观看:
对 Nodejs 的底层也有了比较深的理解了:
Prisma & mysql
Prisma 是一个现代化的 ORM(对象关系映射)工具,旨在简化数据库操作并提高开发效率。它支持 MySQL 等关系型数据库,并为 Node.js 提供了类型安全的数据库客户端。在 NestJS 中使用 Prisma,可以让开发者轻松定义数据库模型,并通过自动生成的 Prisma Client 执行类型安全的查询操作。与 MySQL 配合时,Prisma 提供了一种简单、直观的方式来操作数据库,而无需手动编写复杂的 SQL 查询。
Prisma 的核心优势在于其强大的类型安全功能,所有的数据库操作都能通过 Prisma Client 提供的自动生成的类型来进行,这大大减少了代码中的错误,提升了开发的效率。它还包含数据库迁移工具 Prisma Migrate,能够帮助开发者方便地管理数据库结构的变化。此外,Prisma Client 的查询 API 具有很好的性能,能够高效地执行复杂的数据库查询,支持包括关系查询、聚合查询等高级功能。
与传统的 ORM 相比,Prisma 使得数据库交互更加简洁且高效,减少了配置和手动操作的复杂性,特别适合在 NestJS 项目中使用,能够与 NestJS 提供的依赖注入和模块化架构很好地结合,提升整体开发体验。
Redis
Redis 和 mysql 都仅仅是会用的阶段,目前都是直接在 NestJs 项目中使用,都是已经封装好了的,直接传参调用就好了:
import { Injectable, Inject, OnModuleDestroy, Logger } from "@nestjs/common";
import Redis, { ClientContext, Result } from "ioredis";
import { ObjectType } from "../types";
import { isObject } from "@/utils";
@Injectable()
export class RedisService implements OnModuleDestroy {
private readonly logger = new Logger(RedisService.name);
constructor(@Inject("REDIS_CLIENT") private readonly redisClient: Redis) {}
onModuleDestroy(): void {
this.redisClient.disconnect();
}
/**
* @Description: 设置值到redis中
* @param {string} key
* @param {any} value
* @return {*}
*/
public async set(
key: string,
value: unknown,
second?: number
): Promise<Result<"OK", ClientContext> | null> {
try {
const formattedValue = isObject(value)
? JSON.stringify(value)
: String(value);
if (!second) {
return await this.redisClient.set(key, formattedValue);
} else {
return await this.redisClient.set(key, formattedValue, "EX", second);
}
} catch (error) {
this.logger.error(`Error setting key ${key} in Redis`, error);
return null;
}
}
/**
* @Description: 获取redis缓存中的值
* @param key {String}
*/
public async get(key: string): Promise<string | null> {
try {
const data = await this.redisClient.get(key);
return data ? data : null;
} catch (error) {
this.logger.error(`Error getting key ${key} from Redis`, error);
return null;
}
}
/**
* @Description: 设置自动 +1
* @param {string} key
* @return {*}
*/
public async incr(
key: string
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.incr(key);
} catch (error) {
this.logger.error(`Error incrementing key ${key} in Redis`, error);
return null;
}
}
/**
* @Description: 删除redis缓存数据
* @param {string} key
* @return {*}
*/
public async del(key: string): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.del(key);
} catch (error) {
this.logger.error(`Error deleting key ${key} from Redis`, error);
return null;
}
}
/**
* @Description: 设置hash结构
* @param {string} key
* @param {ObjectType} field
* @return {*}
*/
public async hset(
key: string,
field: ObjectType
): Promise<Result<number, ClientContext> | null> {
try {
return await this.redisClient.hset(key, field);
} catch (error) {
this.logger.error(`Error setting hash for key ${key} in Redis`, error);
return null;
}
}
/**
* @Description: 获取单个hash值
* @param {string} key
* @param {string} field
* @return {*}
*/
public async hget(key: string, field: string): Promise<string | null> {
try {
return await this.redisClient.hget(key, field);
} catch (error) {
this.logger.error(
`Error getting hash field ${field} from key ${key} in Redis`,
error
);
return null;
}
}
/**
* @Description: 获取所有hash值
* @param {string} key
* @return {*}
*/
public async hgetall(key: string): Promise<Record<string, string> | null> {
try {
return await this.redisClient.hgetall(key);
} catch (error) {
this.logger.error(
`Error getting all hash fields from key ${key} in Redis`,
error
);
return null;
}
}
/**
* @Description: 清空redis缓存
* @return {*}
*/
public async flushall(): Promise<Result<"OK", ClientContext> | null> {
try {
return await this.redisClient.flushall();
} catch (error) {
this.logger.error("Error flushing all Redis data", error);
return null;
}
}
/**
* @Description: 保存离线通知
* @param {string} userId
* @param {any} notification
*/
public async saveOfflineNotification(
userId: string,
notification: any
): Promise<void> {
try {
await this.redisClient.lpush(
`offline_notifications:${userId}`,
JSON.stringify(notification)
);
} catch (error) {
this.logger.error(
`Error saving offline notification for user ${userId}`,
error
);
}
}
/**
* @Description: 获取离线通知
* @param {string} userId
* @return {*}
*/
public async getOfflineNotifications(userId: string): Promise<any[]> {
try {
const notifications = await this.redisClient.lrange(
`offline_notifications:${userId}`,
0,
-1
);
await this.redisClient.del(`offline_notifications:${userId}`);
return notifications.map((notification) => JSON.parse(notification));
} catch (error) {
this.logger.error(
`Error getting offline notifications for user ${userId}`,
error
);
return [];
}
}
/**
* 获取指定 key 的剩余生存时间
* @param key Redis key
* @returns 剩余生存时间(秒)
*/
public async getTTL(key: string): Promise<number> {
return await this.redisClient.ttl(key);
}
}
前端工程化
前端工程化这块花了很多信息在 eslint、prettier、husky、commitlint、github action 上,现在很多项目都是直接复制之前写好的过来就直接用。
后续应该是投入更多的时间在性能优化、埋点、自动化部署上了,如果有机会的也去研究一下 k8s 了。
全栈性价比最高的一套技术
最近刷到一个帖子,讲到了

我目前也算是一个小全栈了吧,我也来分享一下我的技术吧:
- NextJs
- React Native
- prisma
- NestJs
- taro (目前还不会,如果有需求就会去学)
剩下的描述也是和他下面那句话一样了(毕业后对技术态度的转变就是什么能让我投入最小,让我最快赚到钱的就是好技术)
总结
学无止境,任重道远。
最后再来提一下这两个开源项目,它们都是我们目前正在维护的开源项目:
如果你想参与进来开发或者想进群学习,可以添加我微信 yunmz777,后面还会有很多需求,等这个项目完成之后还会有很多新的并且很有趣的开源项目等着你。
来源:juejin.cn/post/7451483063568154639
还在用html2canvas?介绍一个比它快100倍的截图神器!
在日常业务开发里,DOM 截图 几乎是刚需场景。
无论是生成分享卡片、导出报表,还是保存一段精美排版内容,前端同学都绕不开它。
但问题来了——市面上的截图工具,比如 html2canvas,虽然用得多,却有一个致命缺陷:慢!
普通截图动辄 1 秒以上,大一点的 DOM,甚至能直接卡到怀疑人生,用户体验一言难尽。
大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章
最近发现一个保存速度惊艳到我的库snapDOM 。
这货在性能上的表现,完全可以用“碾压”来形容:
- 👉 相比 html2canvas,快 32 ~ 133 倍
- 👉 相比 modern-screenshot,也快 2 ~ 93 倍

以下是官方基本测试数据:
| 场景 | snapDOM vs html2canvas | snapDOM vs dom-to-image |
|---|---|---|
| 小元素 (200×100) | 32 倍 | 6 倍 |
| 模态框 (400×300) | 33 倍 | 7 倍 |
| 整页截图 (1200×800) | 35 倍 | 13 倍 |
| 大滚动区域 (2000×1500) | 69 倍 | 38 倍 |
| 超大元素 (4000×2000) | 93 倍 🔥 | 133 倍 |
📊 数据来源:snapDOM 官方 benchmark(基于 headless Chromium 实测)。
⚡ 为什么它这么快?
二者的实现原理不同
html2canvas 的实现方式
- 原理:
通过遍历 DOM,把每个节点的样式(宽高、字体、背景、阴影、图片等)计算出来,然后在<canvas>上用 Canvas API 重绘一遍。 - 特点:
- 需要完整计算 CSS 样式 → 排版 → 绘制。
- 复杂 DOM 时计算量极大,比如渐变、阴影、字体渲染都会消耗 CPU。
- 整个过程基本是 模拟浏览器的渲染引擎,属于“重造轮子”。
所以一旦 DOM 大、样式复杂,html2canvas 很容易出现 1s+ 延迟甚至卡死。
snapDOM 的实现方式
原理:利用浏览器 原生渲染能力,而不是自己模拟。
snapDOM 的 captureDOM 并不是自己用 Canvas API 去一笔一笔绘制 DOM(像 html2canvas 那样),而是:
- 复制 DOM 节点(prepareClone)
- → 生成一个“克隆版”的 DOM,里面包含了样式、结构。
- 把图片、背景、字体都转成 inline(base64 / dataURL)
- → 确保克隆 DOM 是完全自包含的。
- 用
<foreignObject>包在 SVG 里面
- → 浏览器原生支持直接渲染 HTML 片段到 SVG → 再转成 dataURL。
所以核心就是:
👉 利用浏览器自己的渲染引擎(SVG foreignObject)来排版和绘制,而不是 JS 重造渲染过程。
如何使用 snapDOM
snapDOM 上手非常简单, 学习成本比较低。
1. 安装
通过npm 安装:
npm install @zumer/snapdom
或者直接用 CDN 引入:
<script src="https://cdn.jsdelivr.net/npm/@zumer/snapdom/dist/snapdom.min.js"></script>
2. 基础用法
只需要一行代码,就能把 DOM 节点“变”成图片:
// 选择你要截图的 DOM 元素
const target = document.querySelector('.card');
// 导出为 PNG 图片
const image = await snapdom.toPng(target);
// 直接添加到页面
document.body.appendChild(image);
3. 更多导出方式
除了 PNG,snapDOM 还支持多种输出格式:
// 导出为 JPEG
const jpeg = await snapdom.toJpeg(target);
// 导出为 SVG
const svg = await snapdom.toSvg(target);
// 直接保存为文件
await snapdom.download(target, { format: 'png', filename: 'screenshot.png' });
4. 导出一个这样的海报图

开发中生成海报并保存到, 是非常常见的需求,以前使用html2canvas,也要写不少代码, 还要处理图片失真等问题, 使用snapDOM,真的一行代码能搞定。
<div ref="posterRef" class="poster">
....
</div>
<script setup lang="ts">
const downloadPoster = async () => {
if (!posterRef.value) {
alert("海报元素未找到");
return;
}
try {
// snapdom 是 UMD 格式,通过全局 window.snapdom 访问
const snap = (window as any).snapdom;
if (!snap) {
alert("snapdom 库未加载,请刷新页面重试");
return;
}
await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});
} catch (error) {
console.error("海报生成失败:", error);
alert("海报生成失败,请重试");
}
};
</script>
相比传统方案需要大量配置和兼容性处理,snapDOM 真正做到了 一行代码,极速生成。无论是分享卡片、营销海报还是报表导出,都能轻松搞定。
await snap.download(posterRef.value, {
format: "png",
filename: `tech-poster-${Date.now()}`
});
大家好,我是芝士,欢迎点此扫码加我微信 Hunyi32 交流,最近创建了一个低代码/前端工程化交流群,欢迎加我微信 Hunyi32 进群一起交流学习,也可关注我的公众号[ 前端界 ] 持续更新优质技术文章
总结
在前端开发里,DOM 截图是一个常见但“让人头疼”的需求。
- html2canvas 代表的传统方案,虽然功能强大,但性能和体验常常拖后腿;
- 而 snapDOM 借助浏览器原生渲染能力,让截图变得又快又稳。
一句话:
👉 如果你还在为截图慢、卡顿、模糊烦恼,不妨试试 snapDOM —— 可能会刷新你对前端截图的认知。 🚀
来源:juejin.cn/post/7542379658522116123
10年老前端吐槽Tailwind CSS:是神器还是“神坑”?
作为一个老前端人比大家虚长几岁,前端技术的飞速发展,从早期的 jQuery 到现代的 React、Vue,再到 CSS 框架的演变。最近几年,Tailwind CSS 成为了前端圈的热门话题,很多人称它为“神器”,但也有不少人认为它是“神坑”。今天,我就从实际项目经验出发,吐槽一下 Tailwind CSS 的弊端。
HTML 代码臃肿,可读性差
例如:
<div class="p-4 bg-white rounded-lg shadow-md text-gray-800 hover:bg-gray-100">
Content
</div>
以至于前端同学前来吐槽,这和写style有个毛线区别。
虽然tailwind提供了@apply方法,将常用的实用类代码提取到css中,来减少html的代码量
.btn {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}
可以一个大型项目样式复杂,常常看到这样的场景:
.btn1 {
@apply p-4 bg-blue-500 text-white rounded-lg hover:bg-blue-600;
}
.btn2 {
@apply p-6 bg-blue-500 text-blue rounded-lg hover:bg-blue-600;
}
.btn3 {
@apply p-8 bg-blue-500 text-red rounded-lg hover:bg-blue-600;
}
.btn4 {
@apply p-10 bg-blue-500 rounded-lg hover:bg-green-600;
}
读起来都费劲。
增加了前端同学的学习成本
开发是必须学习大量的tailwind的实用类,并且要花时间学习这些命名规则,例如:
p-4是 padding,m-4是 margin。text-sm是小字体,text-lg是大字体。bg-blue-500是背景色,text-blue-500是文字颜色。
国内的项目大家都知道,不是在赶工期,就是在赶工期的路上,好多小伙伴开发的时候直接就上style了。
当然这样也有好处,能使用JIT模式,按需生成css,减少文件的大小。避免了以前项目中好多无用的css。
动态样式支持有限
有时候需要动态生成的类会导致错误,例如:
<div class="text-{{ color }}-500 bg-{{ bgColor }}-100">
Content
</div>
如果color被复制为green,但是系统并没有定义 text-green-500 这个类。让项目变得难以调试和维护。
总结
Tailwind CSS 是一把双刃剑,它既能为开发带来极大的便利,也可能成为项目的“神坑”。作为开发者,我们需要根据项目需求,合理使用 Tailwind,并通过一些最佳实践规避它的弊端。希望这篇文章能帮助你在项目中更好地使用 Tailwind CSS,享受它带来的便利,同时避免踩坑!
如果你有更多关于 Tailwind CSS 的问题或经验分享,欢迎在评论区留言讨论! 😊
来源:juejin.cn/post/7484638486994681890
🔄一张图,让你再也忘不了浏览器的事件循环(Event Loop)了
一、前言
下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。
后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。
当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。

下面纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。
后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。
当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。

二、概念
事件循环是JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task Queue和Microtask Queue这两个队列)并需要运行的代码。
事件循环是JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task Queue和Microtask Queue这两个队列)并需要运行的代码。
三、为什么需要事件循环
JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。
事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程。
JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。
事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程。
四、事件循环流程图用法演示
演示一:小菜一碟
先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。
console.log(1)
function funcOne() {
console.log(2)
}
function funcTwo() {
funcOne()
console.log(3)
}
funcTwo()
console.log(4)
控制台输出:
1 2 3 4
下图为调用栈执行流程

每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。
先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。
console.log(1)
function funcOne() {
console.log(2)
}
function funcTwo() {
funcOne()
console.log(3)
}
funcTwo()
console.log(4)
控制台输出:
1 2 3 4
下图为调用栈执行流程

每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。
演示二:小试牛刀
setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。
console.log(1)
setTimeout(() => {
console.log('setTimeout', 2)
}, 0)
const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})
setTimeout(() => {
console.log('setTimeout', 5)
}, 10)
promise.then(res => {
console.log('then', res)
})
console.log(6)
控制台输出:
1 promise 3 6 then 4 setTimeout 2 setTimeout 5
setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。
console.log(1)
setTimeout(() => {
console.log('setTimeout', 2)
}, 0)
const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})
setTimeout(() => {
console.log('setTimeout', 5)
}, 10)
promise.then(res => {
console.log('then', res)
})
console.log(6)
控制台输出:
1 promise 3 6 then 4 setTimeout 2 setTimeout 5
流程图执行-步骤一:
先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听或对应的任务队列。

- 执行
console.log(1),控制台输出1。 - 执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数
() => {console.log('setTimeout', 2)},放到宏任务队列等待。 - 执行创建
Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4。 - 执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数
() => { console.log('setTimeout', 5) }放到后台监听。 - 执行
promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。
先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听或对应的任务队列。

- 执行
console.log(1),控制台输出1。 - 执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数
() => {console.log('setTimeout', 2)},放到宏任务队列等待。 - 执行创建
Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4。 - 执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数
() => { console.log('setTimeout', 5) }放到后台监听。 - 执行
promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。
流程图执行-步骤二:
上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环。

- 扫描微任务队列,执行
4 => { console.log('then', 4) }回调函数,控制台输出then 4。 - 微任务队列为空,扫描宏任务队列,执行
() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2。 - 每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把
() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。 - 微任务队列为空,扫描宏任务队列,执行
() => { console.log('setTimeout', 5) },控制台输出setTimeout 5。
上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环。

- 扫描微任务队列,执行
4 => { console.log('then', 4) }回调函数,控制台输出then 4。 - 微任务队列为空,扫描宏任务队列,执行
() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2。 - 每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把
() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。 - 微任务队列为空,扫描宏任务队列,执行
() => { console.log('setTimeout', 5) },控制台输出setTimeout 5。
演示三:稍有难度
setTimeout+Promise组合拳+多层嵌套Promise
console.log(1)
setTimeout(() => {
console.log('setTimeout', 10)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(7)
new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)
new Promise((resolve, reject) => {
resolve('嵌套第三层 Promise')
}).then(res => {
console.log(res)
})
})
Promise.resolve(6).then(res => {
console.log(res)
})
}).then(res => {
console.log(res)
})
new Promise((resolve, reject) => {
console.log(3)
Promise.resolve(8).then(res => {
console.log(res)
})
resolve(9)
}).then(res => {
console.log(res)
})
console.log(4)
上一个演示说明了流程图执行的详细步骤,下面就不多加赘叙了,直接看图!
talk is cheap, show me the chart

上图,调用栈同步代码执行完成,开始事件循环,先看微任务队列,发现不为空,按顺序执行微任务事件:

上图,已经把刚才排队的微任务队列全部清空了。但是在执行第一个微任务时,发现还有嵌套微任务,则把该任务放到微任务队列末尾,然后接着一起执行完所有新增任务。

最后微任务清空后,接着执行宏任务。到此全部事件已执行完毕!
控制台完整输出顺序:
1 2 3 4 5 6 7 8 9 10
演示四:setTimeout伪定时
setTimeout并不是设置的定时到了就马上执行,而是把定时回调放在task queue任务队列当中进行等待,待主线程调用栈中的同步任务执行完成后空闲时才会执行。
const startTime = Date.now()
setTimeout(() => {
const endTime = Date.now()
console.log('setTimeout cost time', endTime - startTime)
// setTimeout cost time 2314
}, 100)
for (let i = 0; i < 300000; i++) {
// 模拟执行耗时同步任务
console.log(i)
}
控制台输出:
1 2 3 ··· 300000 setTimeout cost time 2314
下图演示了其执行流程:

演示五:fetch网络请求和setTimeout
获取网络数据,fetch回调函数属于微任务,优于setTimeout先执行。
setTimeout(() => {
console.log('setTimeout', 2)
}, 510)
const startTime = Date.now()
fetch('http://localhost:3000/test').then(res => {
const endTime = Date.now()
console.log('fetch cost time', endTime - startTime)
return res.json()
}).then(data => {
console.log('data', data)
})
下图当前Call Stack执行栈执行完同步代码后,由于fetch和setTimeout都是宏任务,所以走宏任务Web API流程后注册这两个事件回调,等待定时到后了,由于定时回调是个普通的同步函数,所以放到宏任务队列;等待fetch拿到服务器响应数据后,由于fetch回调为一个Promise对象,所以放到微任务队列。

经过多番刷新网页测试,下图控制台打印展示了setTimeout延时为510ms,fetch请求响应同样是510ms的情况下,.then(data => { console.log('data', data) })先执行了,也是由于fetch基于Promise实现,所以其回调为微任务。

五、结语
这可能只是简单的JavaScript代码执行事件循环流程,目的也是让大家更直观理解其中原理。实际执行过程可能还会读取堆内存获取引用类型数据、操作dom的方法,可能还会触发页面的重排、重绘等过程、异步文件读取和写入操作、fetch发起网络请求,与服务器建立连接获取网络数据等情况。
但是,它们异步执行的回调函数都会经过图中的这个事件循环过程,从而构成完整的浏览器事件循环。
来源:juejin.cn/post/7577395040592756746
单点登录:一次登录,全网通行
大家好,我是小悟。
- 想象一下你去游乐园,买了一张通票(登录),然后就可以玩所有项目(访问各个系统),不用每个项目都重新买票(重新登录)。这就是单点登录(SSO)的精髓!
SSO的日常比喻
- 普通登录:像去不同商场,每个都要查会员卡
- 单点登录:像微信扫码登录,一扫全搞定
- 令牌:像游乐园手环,戴着就能证明你买过票
下面用代码来实现这个"游乐园通票系统":
代码实现:简易SSO系统
import java.util.*;
// 用户类 - 就是我们这些想玩项目的游客
class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
// getters 省略...
}
// 令牌类 - 游乐园手环
class Token {
private String tokenId;
private String username;
private Date expireTime;
public Token(String username) {
this.tokenId = UUID.randomUUID().toString();
this.username = username;
// 令牌1小时后过期 - 游乐园晚上要关门的!
this.expireTime = new Date(System.currentTimeMillis() + 3600 * 1000);
}
public boolean isValid() {
return new Date().before(expireTime);
}
// getters 省略...
}
// SSO认证中心 - 游乐园售票处
class SSOAuthCenter {
private Map<String, Token> validTokens = new HashMap<>();
private Map<String, User> users = new HashMap<>();
public SSOAuthCenter() {
// 预先注册几个用户 - 办了年卡的游客
users.put("zhangsan", new User("zhangsan", "123456"));
users.put("lisi", new User("lisi", "abcdef"));
}
// 登录 - 买票入场
public String login(String username, String password) {
User user = users.get(username);
if (user != null && user.getPassword().equals(password)) {
Token token = new Token(username);
validTokens.put(token.getTokenId(), token);
System.out.println(username + " 登录成功!拿到游乐园手环:" + token.getTokenId());
return token.getTokenId();
}
System.out.println("用户名或密码错误!请重新买票!");
return null;
}
// 验证令牌 - 检查手环是否有效
public boolean validateToken(String tokenId) {
Token token = validTokens.get(tokenId);
if (token != null && token.isValid()) {
System.out.println("手环有效,欢迎继续玩耍!");
return true;
}
System.out.println("手环无效或已过期,请重新登录!");
validTokens.remove(tokenId); // 清理过期令牌
return false;
}
// 登出 - 离开游乐园
public void logout(String tokenId) {
validTokens.remove(tokenId);
System.out.println("已登出,欢迎下次再来玩!");
}
}
// 业务系统A - 过山车
class SystemA {
private SSOAuthCenter authCenter;
public SystemA(SSOAuthCenter authCenter) {
this.authCenter = authCenter;
}
public void accessSystem(String tokenId) {
System.out.println("=== 欢迎来到过山车 ===");
if (authCenter.validateToken(tokenId)) {
System.out.println("过山车启动!尖叫声在哪里!");
} else {
System.out.println("请先登录再玩过山车!");
}
}
}
// 业务系统B - 旋转木马
class SystemB {
private SSOAuthCenter authCenter;
public SystemB(SSOAuthCenter authCenter) {
this.authCenter = authCenter;
}
public void accessSystem(String tokenId) {
System.out.println("=== 欢迎来到旋转木马 ===");
if (authCenter.validateToken(tokenId)) {
System.out.println("木马转起来啦!找回童年记忆!");
} else {
System.out.println("请先登录再玩旋转木马!");
}
}
}
// 测试我们的SSO系统
public class SSODemo {
public static void main(String[] args) {
// 创建认证中心 - 游乐园大门
SSOAuthCenter authCenter = new SSOAuthCenter();
// 张三登录
String token = authCenter.login("zhangsan", "123456");
if (token != null) {
// 拿着同一个令牌玩不同项目
SystemA systemA = new SystemA(authCenter);
SystemB systemB = new SystemB(authCenter);
systemA.accessSystem(token); // 玩过山车
systemB.accessSystem(token); // 玩旋转木马
// 登出
authCenter.logout(token);
// 再尝试访问 - 应该被拒绝
systemA.accessSystem(token);
}
// 测试错误密码
authCenter.login("lisi", "wrongpassword");
}
}
运行结果示例:
zhangsan 登录成功!拿到游乐园手环:a1b2c3d4-e5f6-7890-abcd-ef1234567890
=== 欢迎来到过山车 ===
手环有效,欢迎继续玩耍!
过山车启动!尖叫声在哪里!
=== 欢迎来到旋转木马 ===
手环有效,欢迎继续玩耍!
木马转起来啦!找回童年记忆!
已登出,欢迎下次再来玩!
=== 欢迎来到过山车 ===
手环无效或已过期,请重新登录!
请先登录再玩过山车!
用户名或密码错误!请重新买票!
总结一下:
单点登录就像:
- 一次认证,处处通行 🎫
- 不用重复输入密码 🔑
- 安全又方便 👍
好的SSO系统就像好的游乐园管理,既要让游客玩得开心,又要确保安全!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海
来源:juejin.cn/post/7577599015426228259
别让认知天花板,变成你的职业终点——技术人如何走出信息茧房
我们都活在自己编织的真相里吗?

最近参加了两位前端同事的转正答辩。他们的总结都很真诚,也很努力,但两人之间那种微妙的认知落差,却让我久久不能平静。
第一位同事踏实勤恳,技术中规中矩,对自己的评价也相对保守。他清楚自己的短板,也知道自己需要提升,但他把改变的希望寄托在别人身上——希望有人来指导他、推动他。可实际上,他并没有拿出具体的行动或方案。没有外力,他似乎就只能停在原地。看着他,我忽然想起几年前的自己:是不是也曾这样,一边焦虑,一边等待别人来拯救?
第二位同事则完全不同。他自信满满,言谈间流露出对自身能力的高度认可,甚至认为自己已经达到了“高级工程师”的水平。他在团队协作、沟通表达上也自认表现优异,列举了不少“高光时刻”。但当我站在更高的视角去看这些“成果”时,却感到一种强烈的错位——他的“高级”,建立在一个极其有限的认知框架之上。
他对跨团队协作的理解,停留在“配合顺畅”;对技术深度的衡量,止步于“功能能跑”。他看不到系统设计中的耦合隐患,意识不到架构演进背后的权衡逻辑,更缺乏对业务本质的追问。
那一刻我突然明白:一个人最深的局限,往往不是能力不足,而是根本不知道自己不知道。
而更令人警觉的是,这种认知盲区,并非个例。它像一层透明的茧,包裹着我们每一个人。我们常称之为“信息茧房”,但或许更准确的说法是:认知茧房。
被这个时代悄悄做局了

我们生活在一个信息爆炸的时代,但获取信息的方式,却被前所未有地“驯化”了。
算法精准推送你喜欢的内容,社交圈层不断强化你已有的观点,搜索引擎只呈现你愿意相信的答案。你读的每一篇文章、看的每一段视频、加入的每一个群聊,都在悄悄加固这层茧。
久而久之,我们开始相信:
- 我看到的就是真实的世界;
- 我认同的就是正确的道理;
- 我周围人的共识就是普世的价值。
于是,偏执悄然滋生。我们变得难以倾听异见,习惯性地把不同声音归为“愚蠢”或“别有用心”。沟通不再是交换思想,而成了立场的对抗。我们活在由自己偏好构建的“回音室”里,每一次回响都让我们更加确信:我没错,错的是世界。
那位自认“高级”的同事,问题不在技术本身,而在于他无法感知更高维度的存在。他没见过真正的系统复杂性,没经历过技术债务的反噬,也没体会过从0到1推动变革的艰难。他的“高级”,只是井底之蛙眼中的天空。
偏听则暗,兼听则明

信息茧房最危险的地方,在于它让人变得偏执。偏执的人很难沟通,再加上“傻子共振效应”(相似认知的人互相强化),整个世界的理解就越来越狭隘。
在这个被算法投喂的时代,你看到的,都是你想看的;你喜欢的,也被不断推给你。这不断强化你的认知,形成你的价值观,同时也压缩了你的视野、削弱了你的思辨能力,最终让你陷入一个自我闭环的逻辑牢笼。
你开始以为自己了解世界的运作原理,甚至以为自己看清了本质。但最致命的是——你不觉得自己被局限了。
我不禁反思:
我知道的,是不是错的?
我是不是也在自我麻痹?
我是不是一只井底之蛙,而我身边的人,也都是井底之蛙?我们彼此认同,形成联盟,却浑然不觉自己被困在同一个井里。
更可怕的是,这种“共识”会让我们坚信:我们不在茧房里。
睁眼看世界,看到的未必是真实
你看到的世界,可能是别人想让你看到的,也可能是你自己想让自己看到的。你可能并不知道真实的世界是什么样,但你以为你知道。
如今的大数据和智能推荐,正不断按照你的喜好,一层层加固你的信息茧房。人类的惰性,又被海量信息投喂成“奶头乐”——不加思考,被动消费,逐渐沦为信息时代的消费品。
当然,我写下这些,也可能带着某种激进甚至偏激。但正因如此,才更要停下来问一问:
我是否也陷在这样的焦油坑里?
我是不是也自以为是、固步自封?
是不是只听自己想听的,只信自己愿意信的?
是不是被网络言论洗脑,被算法圈套套牢?
我常常做一些自以为不错的事,但别人可能完全不会那样做。这提醒我:我的“正确”,未必是世界的尺度。
不要被无意义的信息重塑价值观。真正的出路,必须从内在出发。
认知决定你能活出怎样的人生

我们常说:“人赚不到认知以外的钱。”其实这句话可以更广义地理解为:人活不出认知以外的人生。
你的决策、判断、人际关系、职业发展,甚至对幸福的定义,都被你当下的认知边界框定。如果你的认知是扁平的、片面的、情绪化的,那么无论多努力,你也只能在同一个维度里打转。
要破局,第一步是承认自己被困住了。
这不是自我否定,而是一种清醒的自知。真正的成长,始于一个简单的念头:
“这样是不是太受限了?”
这个念头,就是打破茧房的第一道裂痕。
如何让裂痕扩大,直至破茧?

主动“反向输入”:让异见成为养分
不要只读你认同的书,只听你支持的声音。刻意去接触那些让你不适的观点:
- 读一本政治立场与你对立的作者写的书;
- 看一部挑战你价值观的纪录片;
- 和一个你平时不会交往的人深入聊一次天。
重点不是说服对方,而是理解:他为什么这么想?
拓展“认知半径”:走出同类人的圈子
你周围的人,往往和你有相似的背景、价值观和信息来源。这种“同温层”会不断强化你的既有认知。试着去接触:
- 不同行业的人(比如医生、教师、手艺人);
- 不同年龄段的人(年轻人的焦虑,老人的智慧);
- 不同文化背景的人(你会发现,很多你视为“理所当然”的事,在别处根本不存在)。
成年后,我们的圈子越来越小,思维越来越固化。这时候,更需要主动打破圈层的束缚。
3保持“空杯心态”:允许自己被推翻
最可怕的不是无知,而是以为自己知道。定期问自己:
- 我三年前相信什么?现在还信吗?
- 如果我现在的观点是错的,世界会是什么样子?
- 有没有可能,我引以为傲的“成就”,其实只是低水平的重复?
这种自我质疑,不是自我贬低,而是一种精神上的“排毒”。
站在更高的维度看问题
当我开始带团队后,才发现下属的问题会暴露无遗。这也反过来提醒我:站得更高,才能看得更清。
或许我们永远无法抵达“全知”的境界,但至少可以仰望星空。
写到这里,我也在警惕:
这会不会是另一种“认知优越感”?
我是否也在用“破茧”的叙事,构建一个新的茧?
很可能。
但关键不在于是否彻底摆脱茧房——那几乎不可能——而在于是否保有觉察和挣扎的意愿。
我们无法看到全貌,但可以努力多转几个角度;
我们无法摆脱偏见,但可以学会与之共处并保持警惕;
我们可能永远都是井底之蛙,但至少可以抬头,看看那圈之外,是否还有星光。
或许,我们永远无法完全摆脱茧房

这个世界越来越复杂,而我们的认知工具却未必同步进化。
算法在固化我们,信息在淹没我们,社交在同化我们。
但人之所以为人,正是因为我们有反思的能力,有超越当下的渴望。
不必追求绝对的“正确”,也不必幻想彻底的“觉醒”。
只需在每一个自以为是的瞬间,轻轻问一句:
“我是不是,又忘了抬头?”
认知的破局,不在远方,就在此刻的怀疑与开放之中
来源:juejin.cn/post/7580592190020517922
老板:能不能别手动复制路由了?我:写个脚本自动扫描
起因
周五快下班,老板过来看权限配置页面。
"这个每次都要手动输路径?"
"对,现在是这样。"我打开给他看:
角色:运营专员
路由路径:[手动输入] /user/list
组件路径:[手动输入] @/views/user/List.vue
"上次运营配错了,/user/list 写成 /user/lists,页面打不开找了半天。能不能做个下拉框,直接选?"
我想了想:"可以,但得先有个页面列表。"
"那就搞一个,现在页面这么多,手动输容易出错。"
确实,项目现在几十个页面,每次配置权限都要翻代码找路径,复制粘贴,还担心复制错。
解决办法
周末琢磨了一下,其实就是缺个"页面清单"。views 目录下都是页面文件,扫描一遍不就有了?
写了个 Node 脚本,自动扫描 views 目录,生成路由映射表。配置权限的时候下拉框选,还能搜索。
实现
两步:扫描文件 + 生成映射。
扫描 .vue 文件
function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
getAllVueFiles(filePath, filesList); // 递归子目录
} else if (file.endsWith(".vue")) {
filesList.push(filePath); // 收集 .vue 文件
}
});
return filesList;
}
生成映射文件
function start() {
const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);
// 兼容 Windows 路径
files = files.map(item => item.replace(/\\/g, "/"));
// 拼接成 import 映射
let str = "";
files.forEach(item => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});
// 写入文件
fs.writeFileSync(
path.resolve(__dirname, "../router/all.router.js"),
`export const ROUTERSDATA = {\n${str}}`
);
}
最后生成的文件大概是这样:
// src/router/all.router.js
export const ROUTERSDATA = {
"@/views/Home.vue": () => import("@/views/Home.vue"),
"@/views/About.vue": () => import("@/views/About.vue"),
"@/views/user/List.vue": () => import("@/views/user/List.vue"),
}
怎么用
权限配置页面
<template>
<el-select
v-model="selectedRoute"
filterable
placeholder="搜索并选择页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path">
{{ path }}
</el-option>
</el-select>
</template>
<script setup>
import { ROUTERSDATA } from '@/router/all.router.js'
// 后台返回的权限路由配置
const permissionRoutes = [
{ path: '/user/list', component: '@/views/user/List.vue' },
{ path: '/order/list', component: '@/views/order/List.vue' }
]
// 直接从映射表取组件
const routes = permissionRoutes.map(route => ({
path: route.path,
component: ROUTERSDATA[route.component] // 这里直接用
}))
</script>
好处:
- 下拉框自动包含所有页面
- 支持搜索,输入 "user" 就能找到所有用户相关页面
- 新加页面自动出现在列表里
动态路由
后台返回权限配置,前端从映射表取组件:
function generateRoutes(backendConfig) {
return backendConfig.map(item => ({
path: item.path,
component: ROUTERSDATA[item.component] // 直接用
}))
}
效果
周一把代码提上去,改了权限配置页面:

<!-- 配置页面 -->
<el-select v-model="route" filterable placeholder="搜索页面">
<el-option
v-for="(component, path) in ROUTERSDATA"
:key="path"
:label="path"
:value="path" />
</el-select>
老板过来试了一下,在下拉框输入 "user" 就搜到所有用户相关页面。
"嗯,这个好用。新加页面也会自动出现在这里吧?"
"对,每次启动项目会自动扫描。"
"行,那就这样。"
后来发现还有些意外收获:
- 新人看这个映射表就知道项目有哪些页面
- 后台只存路径字符串,数据库干净
- 顺带解决了手动 import 几十个路由的问题
在 package.json 加个脚本:
{
"scripts": {
"dev": "node src/start/index.js && vite"
}
}
每次 npm run dev 会先扫描 views 目录,生成最新的映射表。
完整代码
// src/start/index.js
const fs = require("fs");
const path = require("path");
function getAllVueFiles(dir, filesList = []) {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
getAllVueFiles(filePath, filesList);
} else if (file.endsWith(".vue")) {
filesList.push(filePath);
}
});
return filesList;
}
function start() {
console.log("[自动获取全部可显示页面]");
const viewsDir = path.resolve(__dirname, "../views");
let files = getAllVueFiles(viewsDir);
// 统一路径分隔符,兼容 Windows 反斜杠
files = files.map((item) => item.replace(/\\/g, "/"));
let str = "";
// 构造 import 映射:"@/views/xxx.vue": ()=>import("@/views/xxx.vue")
files.forEach((item) => {
let n = item.replace(/.*src\//, "@/");
str += `"${n}":()=>import("${n}"),\r\n`;
});
const routerFilePath = path.resolve(__dirname, "../router/all.router.js");
// 将映射写入路由聚合文件,供路由动态引用
fs.writeFileSync(
routerFilePath,
`
export const ROUTERSDATA = {
${str}
}`,
);
console.log("[./src/router/all.router.js 写入]");
}
start();
注意事项
记得把生成的 src/router/all.router.js 加到 .gitignore,毕竟是自动生成的文件,没必要提交。
# .gitignore
src/router/all.router.js
后来
用了一个多月,运营配置权限再也没出过错。上周老板说:"这个功能不错,其他项目也加上。"
代码其实挺简单的,但确实解决了问题。
来源:juejin.cn/post/7582808491583504420
进入外包,我犯了所有程序员都会犯的错!
前言
前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。

本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。
1. 上岸折戟尘沙
本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。
上网课期间就向往大城市,于是毕业后去了深圳,找到了一家中等IT公司(人数500+)搬砖,住着宝安城中村,来往繁华南山区。
待了三年多,自知买房变深户无望,没有归属感,感觉自己也没那么热爱技术,于是乎想回老家考公务员,希望待在宇宙的尽头。
24年末,匆忙备考,平时工作忙里偷闲刷题,不出所料,笔试卒,梦碎。
2. 误入外包
复盘了备考过程,觉得工作占用时间过多,想要找一份轻松点且离家近的工作,刚好公司也有大礼包的指标,于是主动申请,辞别深圳,前往徽京。
Boss上南京的软件大部分是外包(果然是外包之都),前几年外包还很活跃,这些年外包都沉寂了不少,找了好几个月,断断续续有几个邀约,最后实在没得选了,想着反正就过渡一下挣点钱不寒碜,接受了外包,作为WX服务某为。薪资比在深圳降了一些,在接受的范围内。
想着至少苟着等待下一次考公,因此前期做项目比较认真,遇到问题追根究底,为解决问题也主动加班加点,同为WX的同事都笑话我说比自有员工还卷,我却付之一笑。
直到我经历了几件事,正所谓人教人教不会,事教人一教就会。
3. 我在外包的二三事
有一次,我提出了自有员工设计方案的衍生出的一个问题,并提出拉个会讨论一下,他并没有当场答应,而是回复说:我们内部看看。
而后某天我突然被邀请进入会议,聊了几句,意犹未尽之际,突然就被踢出会议...开始还以为是某位同事误触按钮,然后再申请入会也没响应。
后来我才知道,他们内部商量核心方案,因为权限管控问题,我不能参会。
这是我第一次体会到WX和自有员工身份上的隔阂。
还有一次和自有员工一起吃饭的时候,他不小心说漏嘴了他的公积金,我默默推算了一下他的工资至少比我高了50%,而他的毕业院校、工作经验和我差不多,瞬间不平衡了。
还有诸如其它的团建、夜宵、办公权限、工牌等无一不是明示着你是外包员工,要在外包的规则内行事。
至于转正的事,头上还有OD呢,OD转正的几率都很低,好几座大山要爬呢,别想了。
3. 反求诸己
以前网上看到很多吐槽外包的帖子,还总觉得言过其实,亲身经历了才刻骨铭心。
我现在已经摆正了心态,既来之则安之。正视自己WX的身份,给多少钱干多少活,给多少权利就承担多少义务。
不攀比,不讨好,不较真,不内耗,不加班。
另外每次当面讨论的时候,我都会把工牌给露出来,潜台词就是:快看,我就是个外包,别为难我😔~
另外我现在比较担心的是:
万一我考公还是失败,继续找工作的话,这段外包经历会不会是我简历的污点😢
当然这可能是我个人感受,其它外包的体验我不知道,也不想再去体验了。
对,这辈子和下辈子都不想了。
附南京外包之光,想去或者不想去的伙伴可以留意一下:

来源:juejin.cn/post/7511582195447824438
为什么 SVG 能在现代前端中胜出?
如果你关注前端图标的发展,会发现一个现象:
过去前端图标主要有三种方案:
- PNG 小图(配合雪碧图)
- Iconfont
- SVG
到了今天,大部分中大型项目都把图标系统全面迁移到 SVG。
无论 React/Vue 项目、新框架(Next/Remix/Nuxt),还是大厂的设计规范(Ant Design、Material、Carbon),基本都默认 SVG。
为什么是 SVG 胜出?
为什么不是 Iconfont、不是独立 PNG、不是雪碧图?
答案不是一句“清晰不失真”这么简单。
下面从前端实际开发的角度,把 SVG 胜出的原因讲透。
一、SVG 为什么比位图(PNG/JPG)更强?
① 矢量图永不失真(核心优势)
PNG/JPG 是位图,只能按像素存图。
移动端倍率屏越来越高(2x、3x、4x……),一张 24px 的 PNG 在 iPhone 高分屏里可能看起来糊成一团。
SVG 是矢量图,数学计算绘制:
- 任意缩放不糊
- 任意清晰度场景都不怕
- 深色模式也不会变形
这点直接解决了前端图标领域长期存在的一个痛点:适配成本太高。
② 体积小、多级复用不浪费
同样一个图标:
- PNG 做 1x/2x/3x 需要三份资源
- SVG 只要一份
而且:
- SVG 本质是文本
- gzip 压缩非常有效
在 CDN 下,通常能压到个位数 KB,轻松复用。
③ 图标换色非常容易
PNG 改颜色很麻烦:
- 设计师改
- 重新导出
- 重新上传/构建
Iconfont 的颜色只能统一,只能覆盖轮廓颜色,多色很麻烦。
SVG 则非常灵活:
.icon {
fill: currentColor;
}
可以跟随字体颜色变化,支持 hover、active、主题色。
深浅模式切换不需要任何额外资源。
④ 支持 CSS 动画、交互效果
SVG 不只是图标文件,它是 DOM,可以直接加动画:
- stroke 动画
- 路径绘制动画
- 颜色渐变
- hover 发光
- 多段路径动态控制
PNG 和 Iconfont 都做不到这种级别的交互。
很多现代 UI 的微动效(Loading、赞、收藏),都是基于 SVG 完成。
二、SVG 为什么比 iconfont 更强?
Iconfont 在 2015~2019 年非常火,但明显已经退潮了。
原因有以下几个:
① 字体图标本质是“字符”而不是图形
这带来大量问题:
● 不能多色
只能 monochrome,彩色图标很难实现。
● 渲染脆弱
在 Windows 某些字体渲染环境下会出现:
- 发虚
- 锯齿
- baseline 不一致
● 字符冲突
不同项目的字体图标可能互相覆盖。
相比之下,SVG 是独立图形文件,没有这些问题。
② iconfont 需要加载字体文件,失败会出现“乱码方块”
如果字体文件没加载成功,你会看到:
☐ ☐ ☐ ☐
这在弱网、支付类页面、海外环境都非常常见。
SVG 就没有这个风险。
③ iconfont 不利于按需加载
字体文件通常包含几十甚至几百个图标:
一次加载很重,不够精细。
SVG 可以做到按需加载:
- 一个组件一个 SVG
- 一个页面只引入用到的部分
- 可组合、可动态切换
对于现代构建体系非常友好。
三、SVG 为什么比“新版雪碧图”更强?
即便抛开 iconfont,PNG 雪碧图也完全被淘汰。
原因很简单:
- 雪碧图文件大
- 缓存粒度差
- 不可按需加载
- 维护复杂
- retina 适配麻烦
- 颜色不可动态变更
而 SVG 天生具备现代开发所需的一切特性:
- 轻量化
- 组件化
- 可变色
- 可动画
- 可 inline
- 可自动 tree-shaking
雪碧图本质上是为了“减少请求数”而生的产物,
但在 HTTP/2/3 中已经没有价值。
而 SVG 不是 hack,而是自然适配现代 Web 的技术方案。
四、SVG 为什么能在工程体系里更好地落地?
现代构建工具(Vite / Webpack / Rollup)原生支持 SVG:
- 转组件
- 优化路径
- 压缩
- 自动雪碧(symbol sprite)
- Tree-shaking
- 资源分包
这让 SVG 完全融入工程体系,而不是外挂方案。
例如:
import Logo from './logo.svg'
你可以:
- 当组件使用
- 当资源下载
- 当背景图
- 动态注入
工程化友好度是它胜出的关键原因之一。
五、SVG 胜出的根本原因总结
不是 SVG “长得好看”,也不是趋势,是整个现代前端生态把它推到了最合适的位置。
1)协议升级:HTTP/2/3 让雪碧图和 Iconfont 的优势全部消失
2)设备升级:高分屏让位图模糊问题暴露得更明显
3)工程升级:组件化开发需要精细化图标
4)体验升级:动画、主题、交互都离不开 SVG
一句话总结:
SVG 不只是“更清晰”,而是从工程到体验全面适配现代前端的图标方案,因此胜出。
来源:juejin.cn/post/7577691061034172462
软件工程师必须要掌握的泳道图
作者:面汤放盐 / uzong
在软件开发的世界里,我们习惯用代码表达逻辑,但当系统涉及多个角色、多个服务、甚至跨团队协作时,光靠代码注释或口头沟通,往往不够。这时候,一张清晰的流程图,胜过千行文档。
泳道图 :它可能不像 UML 那样“高大上”,也不如架构图那样宏观,但在梳理业务流程、厘清责任边界、排查系统瓶颈时,它真的非常实用。
1. 什么是泳道图
泳道图的核心思想很简单:把流程中的每个步骤,按执行者(人、系统、模块)分组排列,就像游泳池里的泳道一样,各走各道,互不干扰又彼此关联。

每一列就是一个“泳道”,代表一个责任主体。流程从左到右、从上到下流动,谁在什么时候做了什么,一目了然
一眼就能看出:谁干了什么,谁依赖谁,边界是什么。
与流程的差异点:
- 流程图聚焦:“步骤顺序”,侧重 “先做什么、再做什么”,适合梳理线性业务流程;
- 泳道图聚焦: “流转通道”,侧重 “什么东西在什么约束下通过什么路径流转”,适合拆解复杂、多路径、有规则约束的流转场景(如分布式系统数据同步、供应链物料流转、微服务请求链路等)
2. 泳道图分类
2.1. 垂直泳道图
垂直泳道图采取上下布局结构,主要强调职能群体。这种布局方式更适合于展示跨职能任务和流程中,各职能部门或角色之间的垂直关系和职能分工。

2.2. 水平泳道图
水平泳道图则采用左右布局结构,重点在于事件进程的展示。这种布局方式更适合于强调事件或过程的水平流动,以及不同阶段或部门在流程中的水平参与

3. 泳道图组成元素
泳池: 泳池是泳道图的外部框架,泳道、流程都包含于泳池内。
泳道: 泳池里可以创建多个泳道。
流程: 实际的业务流程。
部门: 通过部门或者责任来区分,明确每个部门/人/信息系统负责完成的任务环节。
阶段: 通过任务阶段来区分,明确每个阶段需要处理的任务环节。
4. 泳道图应用场景
4.1. 项目管理
展示项目从启动到完成的各个阶段,明确每个团队或成员在项目中的角色和职责,便于进行项目管理和监控,同时促进团队协作和沟通

4.2. 业务流程分析
展示业务流程的各个环节和涉及的不同部门或职能。通过分析泳道图,可以发现业务流程中的瓶颈、冗余环节或不合理之处,进而进行流程优化和改进。

4.3. 系统设计
展示系统的整体架构和各个组件之间的关系,描述系统的工作流程,包括数据的输入、处理、输出等各个环节,有助于系统开发人员更好地理解系统的功能和需求

5. 更多参考模板
故障处理多维泳道图

资源扩容泳道图

6. 最后
我刚工作时,看到导师抛出一份精致的泳道图,把一团乱麻的问题讲得透亮,心里特别佩服。直到自己多年后用上才真正体会:在面对跨部门协作、复杂故障排查、关键流程设计时,掏出这么一张图,往往就是高效沟通的开始。
作为开发者,我们常陷入“只要代码跑得通就行”的思维惯性。但软件不仅是机器执行的指令,更是人与人协作的媒介。泳道图这样的工具,本质上是在降低认知成本——让复杂的事情变得可沟通、可验证、可迭代。 其实泳道图的核心不是“画图”,而是“梳理流程、明确权责”。
在跨部门、协同需求、故障分析等关键场景使用泳道图是非常合适,并且也能把问题讲清楚。技术世界充满了抽象和复杂性,而优秀工程师的能力之一,就是创建合适的可视化工具,让复杂问题变得简单可见。
本文中的大部分图片来源于 ProcessOn,ProcessOn 是一个非常不错的画图软件,功能强大,界面优美。
来源:juejin.cn/post/7580423629164068916
我发现很多程序员都不会打日志。。。
你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。
没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!
你赶紧打开服务器,看着比你发量都少的报错信息:

你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!
你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……
原本大好的摸鱼时光,就这样无了。
这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?

你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……
鱼皮皱了皱眉:这么久?你没打日志吗?
你很是疑惑:谁是日志?为什么要打它?

鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!
⭐️ 本文对应视频版:bilibili.com/video/BV1K7…
什么是日志?
鱼皮打开电脑,给你看了一段代码:
@Slf4j
public class UserService {
public void batchImport(List<UserDTO> userList) {
log.info("开始批量导入用户,总数:{}", userList.size());
int successCount = 0;
int failCount = 0;
for (UserDTO userDTO : userList) {
try {
log.info("正在导入用户:{}", userDTO.getUsername());
validateUser(userDTO);
saveUser(userDTO);
successCount++;
log.info("用户 {} 导入成功", userDTO.getUsername());
} catch (Exception e) {
failCount++;
log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
}
}
log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
}
}
你看着代码里的 log.info、log.error,疑惑地问:这些 log 是干什么的?
鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。

你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?
怎么打日志?
鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了~

日志框架的使用非常简单,先获取到 Logger 日志对象。
1)方法 1:通过 LoggerFactory 手动获取 Logger 日志对象:
public class MyService {
private static final Logger logger = LoggerFactory.getLogger(MyService.class);
}
2)方法 2:使用 this.getClass 获取当前类的类型,来创建 Logger 对象:
public class MyService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
}
然后调用 logger.xxx(比如 logger.info)就能输出日志了。
public class MyService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public void doSomething() {
logger.info("执行了一些操作");
}
}
效果如图:

小阿巴:啊,每个需要打日志的类都要加上这行代码么?
鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。
@Slf4j
public class MyService {
public void doSomething() {
log.info("执行了一些操作");
}
}
上面的代码等同于 “自动为当前类生成日志对象”:
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(MyService.class);
你咧嘴一笑:这个好,爽爽爽!

等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?
System.out.println("开始导入用户" + user.getUsername());
鱼皮摇了摇头:千万别这么干!
首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。

而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。

你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?
鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。

在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。
打日志的 8 大最佳实践
1、合理选择日志级别
第一招,日志分级。
你好奇道:日志还有级别?苹果日志、安卓日志?
鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。

其中 DEBUG、INFO、WARN 和 ERROR 用的最多。
- 调试用的详细信息用 DEBUG
- 正常的业务流程用 INFO
- 可能有问题但不影响主流程的用 WARN
- 出现异常或错误的用 ERROR
log.debug("用户对象的详细信息:{}", userDTO); // 调试信息
log.info("用户 {} 开始导入", username); // 正常流程信息
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username); // 警告信息
log.error("用户 {} 导入失败", username, e); // 错误信息
你挠了挠头:俺直接全用 DEBUG 不行么?
鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?

在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。

你点点头:俺明白了,不同的场景用不同的级别!
2、正确记录日志信息
鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?
log.info("用户 {} 开始导入", username);
你回忆了一下:对哦,那是啥啊?
鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。
你挠了挠头:我直接用字符串拼接不行吗?
log.info("用户 " + username + " 开始导入");
鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。

你点点头:确实,而且参数化日志比字符串拼接看起来舒服~

鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:
try {
// 业务逻辑
} catch (Exception e) {
log.error("用户 {} 导入失败", username, e); // 注意这个 e
}
这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。

你抱拳:学会了,我这就去打日志!
3、把控时机和内容
很快,你给批量导入程序的代码加上了日志:
@Slf4j
public class UserService {
public BatchImportResult batchImport(List<UserDTO> userList) {
log.info("开始批量导入用户,总数:{}", userList.size());
int successCount = 0;
int failCount = 0;
for (UserDTO userDTO : userList) {
try {
log.info("正在导入用户:{}", userDTO.getUsername());
// 校验用户名
if (StringUtils.isBlank(userDTO.getUsername())) {
throw new BusinessException("用户名不能为空");
}
// 保存用户
saveUser(userDTO);
successCount++;
log.info("用户 {} 导入成功", userDTO.getUsername());
} catch (Exception e) {
failCount++;
log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
}
}
log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
return new BatchImportResult(successCount, failCount);
}
}
光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。

但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?
鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。
对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。

你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!
鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。

你双眼放光:这个好,爽爽爽!

鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。

你拍拍胸脯:必须的!
4、控制日志输出量
一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?
你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……
好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!

鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。
你有点委屈:不是你让我多打日志的么?那我应该怎么办?
鱼皮:你需要控制日志的输出量。
1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:
if ((i + 1) % 100 == 0) {
log.info("批量导入进度:{}/{}", i + 1, userList.size());
}
2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:
StringBuilder logBuilder = new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
processUser(userDTO);
logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());
3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/app.log</file>
<!-- 只允许 INFO 级别及以上的日志通过 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
5、统一日志格式
你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?
鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。
<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
这样输出的日志更整齐易读:

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。

在 Java 代码中,可以为 MDC 设置属性值:
@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
// 1. 设置 MDC 上下文信息
MDC.put("requestId", generateRequestId());
MDC.put("userId", String.valueOf(request.getUserId()));
try {
log.info("用户请求处理完成");
// 执行具体业务逻辑
userService.batchImport(request.getUserList());
return Result.success();
} finally {
// 2. 及时清理MDC(重要!)
MDC.clear();
}
}
然后在日志配置文件中就可以使用这些值了:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<!-- 包含 MDC 信息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
</encoder>
</appender>
这样,每个请求、每个用户的操作一目了然。

6、使用异步日志
你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?
鱼皮:当然有,可以使用 异步日志。
正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。
你眼睛一亮:这么厉害?怎么开启?
鱼皮:很简单,只需要修改一下配置文件:
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize> <!-- 队列大小 -->
<discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
<neverBlock>false</neverBlock> <!-- 队列满时是否阻塞,false 表示会阻塞 -->
<appender-ref ref="FILE" /> <!-- 引用实际的日志输出目标 -->
</appender>
<root level="INFO">
<appender-ref ref="ASYNC" />
</root>
不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。

所以要权衡一下,看你的系统更注重性能还是日志的完整性。
你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。
7、日志管理
接下来的很长一段时间,你混的很舒服,有 Bug 都能很快发现。
你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!

直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!
你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。
你查了一下,发现日志文件竟然占了 200GB 的空间!

你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?
你尴尬地倒了个立,这样眼泪就不会留下来。

鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。
你好奇道:怎么管理?我每天登服务器删掉一些历史文件?
鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。
首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

还可以开启日志压缩功能,进一步节省磁盘空间:
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- .gz 后缀会自动压缩 -->
<fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!
8、集成日志收集系统
两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。
如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。

你简直要疯了!
于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……
鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。
你好奇:ELK 是啥?伊拉克?
阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。
简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。

这样一来,我们可以方便地集中搜索、查看、分析日志。

你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!
鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。
结局
至此,你已经掌握了打日志的核心秘法。

只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?
阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。
阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!
你要是以后不打日志,我就打你!
更多
来源:juejin.cn/post/7569159131819753510
分库分表正在被淘汰
前言
“分库分表这种架构模式会逐步的被淘汰!” 不知道在哪儿看到的观点
如果我们现在在搭建新的业务架构,如果说你们未来的业务数据量会达到千万 或者上亿的级别 还在一股脑的使用分库分表的架构,那么你们的技术负责人真的就应该提前退休了🙈
如果对未来的业务非常有信心,单表的数据量能达到千万上亿的级别,请使用NewSQL 数据库,那么NewSQL 这么牛,分布库分表还有意义吗?
今天虽然写的是一篇博客,但是更多的是抱着和大家讨论的心态来的,所以大家目前有深度参与分库分表,或者NewSQL 的都可以在评论区讨论!
什么是NewSQL
NewSQL 是21世纪10年代初出现的一个术语,用来描述一类新型的关系型数据库管理系统(RDBMS)。它们的共同目标是:在保持传统关系型数据库(如Oracle、MySQL)的ACID事务和SQL模型优势的同时,获得与NoSQL系统类似的、弹性的水平扩展能力
NewSQL 的核心理念就是 将“分库分表”的复杂性从应用层下沉到数据库内核层,对上层应用呈现为一个单一的数据库入口,解决现在 分库分表的问题;
分库分表的问题
分库分表之后,会带来非常多的问题;比如需要跨库联查、跨库更新数据如何保证事务一致性等问题,下面就来详细看看分库分表都有那些问题
- 数据库的操作变得复杂
- 跨库 JOIN 几乎不可行:原本简单的多表关联查询,因为表被分散到不同库甚至不同机器上,变得异常困难。通常需要拆成多次查询,在应用层进行数据组装,代码复杂且性能低下。
- 聚合查询效率低下:
COUNT(),SUM(),GR0UP BY,ORDER BY等操作无法在数据库层面直接完成。需要在每个分片上执行,然后再进行合并。 - 分页问题:
LIMIT 20, 10这样的分页查询会变得非常诡异。你需要从所有分片中获取前30条数据,然后在应用层排序后取第20-30条。页码越大,性能越差。
- 设计上需要注意的问题
- 分片键(Sharding Key)的选择:如果前期没有设计好,后期数据倾斜比较严重
- 全局唯一ID需要提前统一设计,规范下来
- 分布式事务问题,需要考虑使用哪种方式去实现(XA协议,柔性事务)
选择TiDB还是采用mysql 分库分表的设计
数据量非常大,需要满足OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 且预算充足(分布式数据库的成本也是非常高的这一点非常的重要),并且是新业务新架构落地 优先推荐使用TiDB。
当然实际上选择肯定是需要多方面考虑的,大家有什么观点都可以在评论区讨论。
可以看看一个资深开发,深度参与TiDB项目,他对TiDB的一些看法:



1 什么是TiDB?
TiDB是PingCAP公司研发的开源分布式关系型数据库,采用存储计算分离架构,支持混合事务分析处理(HTAP) 。它与MySQL 5.7协议兼容,并支持MySQL生态,这意味着使用MySQL的应用程序可以几乎无需修改代码就能迁移到TiDB。
🚀目标是为用户提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB 适合高可用、强一致要求较高、数据规模较大等各种应用场景。
官方文档:docs.pingcap.com/zh/tidb/dev…
TiDB五大核心特性
TiDB之所以在分布式数据库领域脱颖而出,得益于其五大核心特性:
- 一键水平扩容或缩容:得益于存储计算分离的架构设计,可按需对计算、存储分别进行在线扩容或缩容,整个过程对应用透明。
- 金融级高可用:数据采用多副本存储,通过Multi-Raft协议同步事务日志,只有多数派写入成功事务才能提交,确保数据强一致性。
- 实时HTAP:提供行存储引擎TiKV和列存储引擎TiFlash,两者之间的数据保持强一致,解决了HTAP资源隔离问题。
- 云原生分布式数据库:通过TiDB Operator可在公有云、私有云、混合云中实现部署工具化、自动化。
- 兼容MySQL 5.7协议和生态:从MySQL迁移到TiDB无需或只需少量代码修改,极大降低了迁移成本。
2 TiDB与MySQL的核心差异
虽然TiDB兼容MySQL协议,但它们在架构设计和适用场景上存在根本差异。以下是它们的详细对比:
2.1 架构差异
表1:TiDB与MySQL架构对比
| 特性 | MySQL | TiDB |
|---|---|---|
| 架构模式 | 集中式架构 | 分布式架构 |
| 扩展性 | 垂直扩展,主从复制 | 水平扩展,存储计算分离 |
| 数据分片 | 需要分库分表 | 自动分片,无需sharding key |
| 高可用机制 | 主从复制、MGR | Multi-Raft协议,多副本 |
| 存储引擎 | InnoDB、MyISAM等 | TiKV(行存)、TiFlash(列存) |
2.2 性能表现对比
性能方面,TiDB与MySQL各有优势,主要取决于数据量和查询类型:
- 小数据量简单查询:在数据量百万级以下的情况下,MySQL的写入性能和点查点写通常优于TiDB。因为TiDB的分布式架构在少量数据时无法充分发挥优势,却要承担分布式事务的开销。
- 大数据量复杂查询:当数据量达到千万级以上,TiDB的性能优势开始显现。一张千万级别表关联查询,MySQL可能需要20秒,而TiDB+TiKV只需约5.57秒,使用TiFlash甚至可缩短到0.5秒。
- 高并发场景:MySQL性能随着并发增加会达到瓶颈然后下降,而TiDB性能基本随并发增加呈线性提升,节点资源不足时还可通过动态扩容提升性能。
2.3 扩展性与高可用对比
MySQL的主要扩展方式是一主多从架构,主节点无法横向扩展(除非接受分库分表),从节点扩容需要应用支持读写分离。而TiDB的存储和计算节点都可以独立扩容,支持最大512节点,集群容量可达PB级别。
高可用方面,MySQL使用增强半同步和MGR方案,但复制效率较低,主节点故障会影响业务处理[]。TiDB则通过Raft协议将数据打散分布,单机故障对集群影响小,能保证RTO(恢复时间目标)不超过30秒且RPO(恢复点目标)为0,真正实现金融级高可用。
2.4 SQL功能及兼容性
虽然TiDB高度兼容MySQL 5.7协议和生态,但仍有一些重要差异需要注意:
不支持的功能包括:
- 存储过程与函数
- 触发器
- 事件
- 自定义函数
- 全文索引(计划中)
- 空间类型函数和索引
有差异的功能包括:
- 自增ID的行为(TiDB推荐使用AUTO_RANDOM避免热点问题)
- 查询计划的解释结果
- 在线DDL能力(TiDB更强,不锁表支持DML并行操作)
3 如何选择:TiDB还是MySQL?
选择数据库时,应基于实际业务需求和技术要求做出决策。以下是具体的选型建议:
3.1 选择TiDB的场景
TiDB在以下场景中表现卓越:
- 数据量大且增长迅速的OLTP场景:当单机MySQL容量或性能遇到瓶颈,且数据量达到TB级别时,TiDB的水平扩展能力能有效解决问题。
例如,当业务数据量预计将超过TB级别,或并发连接数超过MySQL合理处理范围时。 - 实时HTAP需求:需要同时进行在线事务处理和实时数据分析的场景。
传统方案需要OLTP数据库+OLAP数据库+ETL工具,TiDB的HTAP能力可简化架构,降低成本和维护复杂度。 - 金融级高可用要求:对系统可用性和数据一致性要求极高的金融行业场景。
TiDB的多副本和自动故障转移机制能确保业务连续性和数据安全。 - 多业务融合平台:需要将多个业务数据库整合的统一平台场景。
TiDB的资源管控能力可以按照RU(Request Unit)大小控制资源总量,实现多业务资源隔离和错峰利用。 - 频繁的DDL操作需求:需要频繁进行表结构变更的业务。
TiDB的在线DDL能力在业务高峰期也能平稳执行,对大表结构变更尤其有效。
3.2 选择MySQL的场景
MySQL在以下情况下仍是更合适的选择:
- 中小规模数据量:数据量在百万级以下,且未来增长可预测。
在这种情况下,MySQL的性能可能更优,且总拥有成本更低。 - 简单读写操作为主:业务以点查点写为主,没有复杂的联表查询或分析需求。
- 需要特定MySQL功能:业务依赖存储过程、触发器、全文索引等TiDB不支持的功能。
- 资源受限环境:硬件资源有限且没有分布式数据库管理经验的团队。
MySQL的运维管理相对简单,学习曲线较平缓。
3.3 决策参考框架
为了更直观地帮助决策,可以参考以下决策表:
| 考虑因素 | 倾向TiDB | 倾向MySQL |
|---|---|---|
| 数据规模 | TB级别或预计快速增长 | GB级别,增长稳定 |
| 并发需求 | 高并发(数千连接以上) | 低至中等并发 |
| 查询类型 | 复杂SQL,多表关联 | 简单点查点写 |
| 可用性要求 | 金融级(RTO<30s,RPO=0) | 常规可用性要求 |
| 架构演进 | 微服务、云原生、HTAP | 传统单体应用 |
| 运维能力 | 有分布式系统管理经验 | 传统DBA团队 |
4 迁移注意事项
如果决定从MySQL迁移到TiDB,需要注意以下关键点:
- 功能兼容性验证:检查应用中是否使用了TiDB不支持的MySQL功能,如存储过程、触发器等。
- 自增ID处理:将AUTO_INCREMENT改为AUTO_RANDOM以避免写热点问题。
- 事务大小控制:注意TiDB对单个事务的大小限制(早期版本限制较严,4.0版本已提升到10GB)。
- 迁移工具选择:使用TiDB官方工具如DM(Data Migration)进行数据迁移和同步。
- 性能测试:迁移前务必进行充分的性能测试,特别是针对业务关键查询的测试。
5 总结
TiDB和MySQL是适用于不同场景的数据库解决方案,没有绝对的优劣之分。MySQL是优秀的单机数据库,适用于数据量小、架构简单的场景;数据量大了之后需要做分库分表。而TiDB作为分布式数据库,专注于解决大数据量、高并发、高可用性需求下的数据库瓶颈问题,但是成本也是非常的高
本人没有使用过NewSQL ,还望各位大佬批评指正
来源:juejin.cn/post/7561245020045918249
告别终端低效,10个让同事直呼卧槽的小技巧
在 IDE 横行的今天,我们这些程序员依然需要跟终端打交道,三五年下来,谁还没踩过一些坑,又或者自己琢磨出一些能让效率起飞的小窍门呢?
今天不聊那些 ls -la 比 ls 好用之类的基础知识,只分享那些真正改变我工作流、甚至让旁边同事忍不住探过头来问“哥们,你这手速没单身30年练不下来吧”的实战技巧。

快速定位系统性能瓶颈
服务器或者自己电脑突然变卡,得快速知道是谁在捣鬼。
# 查看哪个目录最占硬盘空间(只看当前目录下一级)
du -ah --max-depth=1 | sort -rh | head -n 10
# 按 CPU 使用率列出排名前 10 的进程
ps aux --sort=-%cpu | head -n 11
# 按内存使用率列出排名前 10 的进程
ps aux --sort=-%mem | head -n 11
环境和配置管理?交给专业的来
以前,管理本地开发环境简直是一场灾难。一会儿要配 PHP,一会儿又要弄 Node.js,还得装个 Python。各种环境变量、数据库配置写在 .bashrc 或 .zshrc 里,像这样:
# 老办法:用函数切换环境
switch_env() {
if [ "$1" = "proj_a" ]; then
export DB_HOST="localhost"
export DB_PORT="3306"
echo "切换到项目 A 环境"
elif [ "$1" = "proj_b" ]; then
export DB_HOST="127.0.0.1"
export DB_PORT="5432"
echo "切换到项目 B 环境"
fi
}
这种方式手动维护起来很麻烦,项目一多,配置文件就变得特别臃肿,切换起来也容易出错。
但是,时代变了,朋友们。现在我处理本地开发环境,都用 ServBay。
请注意,ServBay不是命令行工具,而是一个集成的本地开发环境平台。它把程序员常用的语言,比如 PHP、Node.js、Python、Go、Rust 都打包好了,需要哪个版本点一下就行,完全不用自己去折腾编译和环境变量。
18.00.15@2x.png" loading="lazy" src="https://www.imgeek.net/uploads/article/20251214/459b28b5fdb4e57a34956d28d1655e18.jpg"/>
数据库也一样,无论是 SQL(MySQL, PostgreSQL)还是 NoSQL(Redis, MongoDB),都给你准备得妥妥的。而且它支持一键部署本地 AI,适合vibe coder。
用了 ServBay 之后,上面那些复杂的环境切换脚本我早就删了。所有环境和配置管理都通过一个清爽的图形界面搞定,我变强了,也变快了。
一行搞定网络调试
简单测试一下端口通不通,或者 API 能不能访问,完全没必要打开 Postman 那么重的工具。
# 检查本地 3306 端口是否开放
nc -zv 127.0.0.1 3306
# 快速给 API 发送一个 POST 请求
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"username":"test","role":"admin"}'
目录间的闪转腾挪:pushd 和 popd
还在用一连串的 cd ../../.. 来返回之前的目录吗?那也太“复古”了。试试目录栈吧。
# 你当前在 /Users/me/workspace/project-a/src
pushd /etc/nginx/conf.d
# 这时你瞬间移动到了 Nginx 配置目录,并且终端会记住你来的地方
# 在这里查看和修改配置...
vim default.conf
# 搞定之后,想回去了?
popd
# “嗖”的一下,你又回到了 /Users/me/workspace/project-a/src
pushd 可以多次使用,它会把目录一个个地压入一个“栈”里。可以用 dirs -v 查看这个栈,然后用 pushd +N 跳到指定的目录。对于需要在多个不相关的目录之间反复横跳的场景,这就是大杀器。
文件操作的骚操作
用 cp 复制大文件时,看着光标一动不动,你是不是也曾怀疑过电脑是不是死机了?
# 安装 rsync (macOS 自带,Linux 大部分也自带)
# 复制文件并显示进度条
rsync -avh --progress source-large-file.zip /path/to/destination/
查找文件,find 命令固然强大,但参数复杂得像咒语。我更推荐用 fd,一个更快、更友好的替代品。
# 安装 fd (brew install fd / apt install fd-find)
# 查找所有 tsx 文件
fd ".tsx$"
# 查找并删除所有 .log 文件
fd ".log$" --exec rm {}
批量重命名文件,也不用再写复杂的脚本了。
# 比如把所有的 .jpeg 后缀改成 .jpg
for img in *.jpeg; do
mv "$img" "${img%.jpeg}.jpg"
done
历史命令的魔法:!! 和 !$
这个绝对是手残党和健忘症患者的良药。最常见的场景就是,刚敲了一个需要管理员权限的命令,然后……
# 信心满满地创建目录
mkdir /usr/local/my-app
# 得到一个冷冰冰的 "Permission denied"
# 这时候别傻乎乎地重敲一遍,优雅地输入:
sudo !!
# 这行命令会自动展开成:sudo mkdir /usr/local/my-app
!! 代表上一条完整的命令。而 !$ 则更精妙,它代表上一条命令的最后一个参数。
# 创建一个藏得很深的项目目录
mkdir -p projects/a-very-long/and-nested/project-name
# 紧接着,想进入这个目录
cd !$
# 是的,它会自动展开成:cd projects/a-very-long/and-nested/project-name
# 或者,想在那个目录下创建一个文件
touch !$/index.js
自从熟练掌握了这两个符号,我的键盘方向上键和 Ctrl+C 的使用频率都降低了不少。
进程管理不用抓狂
以前杀个进程,得先 ps aux | grep xxx,然后复制 PID,再 kill -9 PID,一套操作下来黄花菜都凉了。现在,我们可以更直接一点。
# 按名字干掉某个进程
pkill -f "gunicorn"
# 优雅地请所有 Python 脚本进程“离开”
pkill -f "python.*.py"
对于我们这些经常和端口打交道的开发者来说,端口被占用的问题更是家常便饭。下面这个函数,我把它写进了我的 .zshrc 里,谁用谁知道。
# 定义一个函数,专门用来释放被占用的端口
free_port() {
lsof -i tcp:$1 | grep LISTEN | awk '{print $2}' | xargs kill -9
echo "端口 $1 已释放"
}
# 比如,干掉占用 8000 端口的那个“钉子户”
free_port 8000
给 Git 整个外挂
把这些别名(alias)加到 ~/.gitconfig 文件里,每天能省下无数次敲击键盘的力气。
[alias]
co = checkout
br = branch
ci = commit -m
st = status -sb
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'
# 把暂存区的修改撤销回来
unstage = reset HEAD --
# 彻底丢弃上一次提交,但保留代码改动
undo = reset --soft HEAD~1
# 一键推送当前分支到远程
pushup = "!git push --set-upstream origin $(git rev-parse --abbrev-ref HEAD)"
还有一个我特别喜欢的,一键清理已经合并到主干的本地分支,分支列表干净清爽。
git branch --merged main | grep -v "*|main|develop" | xargs -n 1 git branch -d
文本处理,快准狠
从日志里捞个邮箱,或者快速格式化一坨 JSON,都是日常操作。
# 从文件里提取所有 URL
grep -oE 'https?://[a-zA-Z0-9./-]+' access.log
# 格式化粘贴板里的 JSON (macOS)
pbpaste | jq .
# 从 API 响应中只提取需要的字段
curl -s 'https://api.github.com/users/torvalds' | jq '.name, .followers'
把重复劳动变成自动化脚本
真正拉开效率差距的,是把那些每天都在重复的操作,变成一个可以随时呼叫的函数或脚本。
# 比如,创建一个新前端项目的完整流程
new_react_project() {
npx create-react-app "$1" && cd "$1"
git init && git add .
git commit -m "🎉 Initial commit"
# 自动在 VS Code 中打开
code .
}
# 比如,在执行危险操作前,快速打包备份当前目录
backup() {
local fname="backup-$(date +%Y%m%d-%H%M).tar.gz"
tar -czvf "$fname" . --exclude-from=.gitignore
echo "备份完成: $fname"
}
把这些函数写进 .bashrc 或 .zshrc,下次再做同样的事情时,只需要敲一个命令就搞定了。
写在最后
这些技巧本身并不复杂,但它们就像肌肉记忆,一旦养成习惯,就能在日常工作中节省大量时间。对程序员来说,时间就是头发。
来源:juejin.cn/post/7573988811916017714
做中国人自己的视频编辑UI框架,WebCut正式开源

朋友们晚上好啊,这段时间我在忙着完成最新的开源项目WebCut。这个项目是我这小半年来唯一的开源新项目,这对比我过去几年的一些开源事迹来说,真的是一段低产荒。不过这是正常的,没有任何人可以长时间的一直发布新项目,这种沉寂,正是因为我把时间和精力都投入在其他事情上,所以其实是好事。之所以要发起和开源这个项目,说起来还是有些背景,下面我会来聊一聊关于这个项目的一些背景,以及过程中在技术上的一些探索。
没有合适的视频编辑UI框架😭
过去半年,我连续发布了多款与视频相关的产品,这些产品或多或少都需要用户参与视频编辑操作,比如给视频添加字幕、对视频进行裁剪、给视频配音等等,这些在视频编辑器中常见的视频处理工具,在Web端其实需求非常巨大,特别是现在AI领域,制作各种个样的视频的需求非常多。而这些需求,可能并不需要在产品中载入一整个视频编辑器,而是只需要几个简单的部件来实现编辑功能。然而回望开源市场,能够支持这种编辑能力的项目少之又少,虽然有一些项目呈现了视频编辑的能力,然而,要么项目太大,一个完整的视频编辑器甩在开发者脸上,要么过于底层,没有UI界面。如果我只是想有一个视频预览,再有轨道和几个配置界面,就没法直接用这些项目。包括我自己在内,每次有与视频相关的功能,都要把之前在另外一个产品中实现的编辑能力移植到新产品中,而且要调的细节也很多。正是这种求而不得的现状,促使我打算自己写一个视频编辑器项目。
初始想法💡:拼积木
我可不是从0开始的,因为我已经开发过很多次视频编辑相关的功能了。我还在Videa项目中完整实现了一个视频编辑器。因此,我的想法是把我之前做过的功能,整理一遍,就可以得到想要的组件或逻辑。有了这个工具包之后,我只需要在将来的新产品中复用这些代码即可。于是我建立了一个独立的npm包,来把所有功能集中放在一起。随着持续的迭代,我发现其实这里面是有规律的。
视频编辑器我们都用过,像剪映一样,有各种功能,细节也很多。但是,当我们把视频编辑的功能放到某个具体的产品中时,我们是不可能直接把整个编辑器给用户的。实际上,我们最终呈现的产品形态,基本上都是剪映的子集,而且是很小很小的子集,可能只是整个剪映1%的功能,最终给到用户操作可能只是非常简单的一次性操作,而页面也很轻量,属于用户即用即走,用完就关再也不会来第二次的那种。正是这种看似功能点很小,但实际上需要为它单独定制,技术上的成本可以用巨大来描述的场景,让我觉得这是一个需要认真对待的点。
我的计划是采用组件化的思想,把一个视频编辑器拆成一个一个的组件,把一个完整的剪映编辑器,拆成一个按钮一个按钮的积木。当我们面对产品需求时,就从这些积木中挑选,然后组合成产品经理所描述的功能,同时,具体这些积木怎么布局,则根据设计稿调整位置,还可以用CSS来覆盖组件内部的样式,达到与设计稿媲美的效果。

上面是我用AI做的一张示意图,大概就是这个意思,把一个编辑器拆的细碎,然后要什么功能就把对应的组件拿来拼凑一下。比如要对视频进行静音裁剪,就只要把预览区和轨道区拿出来,然后自己再增加一些能力上去。这样,开发者在面对各种各样的需求时,就能快速搭建起界面效果,而不需要从头实现界面和视频处理能力。

通过不同的组合方式,配合开发者自己的布局,就可以创建符合需求的视频编辑界面。
没那么容易😥:外简内繁的接口艺术
虽然想法很容易有,但是要做成成品,并且发布为可用的库可没那么轻松。要知道视频编辑器的功能非常多,多到我们无法用简单的文字描述它们。那么,如何才能站在开发者的角度,让这件事变得简单呢?
开发库永远面临着一个矛盾:灵活性和规范性之间的矛盾。灵活性就是暴露又多又细的接口,让开发者可以用不同参数,玩出花活,例如知名项目echarts早期就是这种。这种对于刚上手的开发者而言,无言是一场灾难,他们甚至不知道从哪里开始,但是一旦完全掌握,就相当于拥有了一个武器库,可以为所欲为。而规范性则是强制性规则比较多,只能暴露少量接口,避免开发者破坏这种规范,例如很多前端UI组件库就是这样。这两者之间的矛盾,是每一个库或框架开发者最难平衡的。
在视频编辑器上,我认为有些东西是固定的,几乎每一个需求都会用到,例如视频预览播放器、控制按钮等,但是有些功能是低频功能,例如媒体库、字幕编辑等。因此,对于高频和低频的功能,我用两种态度去处理。
高频的功能,拆的很细,拆到一个按钮为止,例如视频播放器,看上去已经很单一了,但是,我还要把预览屏幕和播放按钮拆开,屏幕和按钮如何布局就可以随意去处理。类似的还有导出按钮、分割片段按钮等等。把这些工具拆分为一个一个最小的单元,由开发者自由布局,这样就可以很好的去适配产品需求。
对于低频功能,则直接导出为大组件,这样就可以在一个组件内完成复杂的逻辑,减少开发这些组件时需要跨组件控制状态的心智成本。比如媒体库,里面包含了媒体的管理、上传、本地化等等,这些逻辑还是很复杂的,如果还是按照细拆思路,那么实现起来就烦得要死。因此,这类工具我都是只暴露为一个大的组件。
同时,为了方便开发者在某些情况下快捷接入,我会设计一些自己看到的不错的编辑器UI,然后用上诉的工具把它们搭出来,这样,开发者如果在产品需求中发现仓库里已经有功能一致的组件,就不需要自己去组合,直接使用对应组件即可。
以模仿剪映为例,开发者只需要接入WebCutEditor这个组件即可:
<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>
<template>
<WebCutEditor />
</template>
这样就可以得到一个界面接近于剪映的视频编辑器。
数据驱动,视频编辑的DSL
经过我的研究,发现对于单纯的视频编辑而言,编辑器其实只需要两份数据,就可以解决大部分场景下的需求。一份是编辑的素材数据,一份是视频的配置数据。
素材数据
它包含素材文件本身的信息、素材的组织信息、素材的属性信息。
文件信息
我通过opfs将文件存在本地,并且在indexedDB中存储每一个文件的关联信息。包含文件的类型、名称、大小等。在一个域名下,每一个文件的实体(也就是File对象)只需要一份,通过file-md5值作为索引存在opfs中。而一个文件可能会在多处被使用,indexedDB中则是记录这些信息,多个关联信息可同时链接到同一个File。另外,基于indexedDB的特性,还可以实现筛选等能力。
素材组织信息
主要是指当把素材放在视频时间轨道中时,所需要的数据结构。包含轨道列表、素材所对应的文件、素材对应时间点、播放时的一些属性等等信息。这些信息综合起来,我们就知道,在视频的某一个时刻,应该播放什么内容。
素材属性信息
在播放中,素材以什么方式呈现,如文本的样式、视频音频的播放速度、动画、转场等。
配置数据
主要指视频本身的信息,在导出时这些配置可以直接体现出来,例如视频的分辨率、比例、速率等,视频是否要添加某些特殊的内容,例如水印等。
基于素材数据和配置数据,我们基本上可以完整的知道当前这个视频的编辑状态。通过数据来恢复当前的编辑状态,变得可行,这可以抵消用户在浏览器中经常执行“刷新”操作带来的状态丢失。同时,这份数据也可以备份到云端,实现多端的同步(不过需要同时同步File,速度肯定会受影响)。而且由于数据本身是纯序列化的,因此,可以交给AI来进行处理,例如让AI调整一些时间、样式等可基于纯序列化数据完成的功能。这就让我们的编辑器变得有更多的玩法。
发布上线🌏
经过几天的工作,我终于把代码整理完整,经过调试之后,基本可用了,便迫不及待的准备与大家分享。现在,你可以使用这个项目了。
由于底层是由Vue3作为驱动的,因此,在Vue中使用有非常大的优势,具体如下:
npm i webcut
先安装,安装之后,你就可以在Vue中如下使用。
<script setup>
import { WebCutEditor } from 'webcut';
import 'webcut/esm/style.css';
</script>
<template>
<WebCutEditor />
</template>
或者如果你的项目支持typescript,你可以直接从源码进行引入,这样就不必主动引入css:
<script setup lang="ts">
import { WebCutEditor } from 'webcut/src';
</script>
如果是非Vue的项目,则需要引用webcomponents的构建产物:
import 'webcut/webcomponents';
import 'webcut/webcomponents/style.css';
export default function Some() {
return <webcut-editor></webcut-editor>;
}
如果是直接在HTML中使用,可以直接引入webcomponents/bundle,这样包含了Vue等依赖,就不需要另外构建。
<script src="https://unpkg.com/webcut/webcomponents/bundle/index.js"></script>
<link rel="stylesheet" href="https://unpkg.com/webcut/webcomponents/bundle/style.css" />
<webcut-editor></webcut-editor>
如果是想自己布局,则需要引入各个很小的组件来自己布局。在这种情况下,你必须引入WebCutProvider组件,将所有的子组件包含在内。
<webcut-provider>
<webcut-player></webcut-player>
<webcut-export-button></webcut-export-button>
</webcut-provider>
未来展望
当前,WebCut还是处于很初级的阶段,实现了最核心的能力,我的目标是能够为开发者们提供一切需要的组件,并且不需要复杂的脚本处理就可以获得视频编辑的全部功能。还有很多功能没有实现,在计划中:
- 历史记录功能,包含撤销和重做功能
- 内置样式的字体
- 花字,比内置样式更高级的文本
- 轨道里的素材吸附能力
- 视频的轨道分离(音频分离)
- 音视频的音量调节
- 单段素材的下载导出
- 整个视频导出时可以进行分辨率、码率、速率、编码、格式的选择,支持只导出音频
以上这些都是编辑的基本功能。还有一些是从视频编辑定制化的角度思考的能力:
- 动画(帧)支持
- 转场过渡效果支持
- 扩展功能模块,这部分可能会做成收费的,下载模块后,通过一个接口安装一下,就可以支持某些扩展功能
- AI Agent能力,通过对话来执行视频编辑,降低视频编辑的门槛
- 视频模板,把一些流行的视频效果片段做成模板,在视频中直接插入后,与现有视频融合为模板中的效果
- 基于AI来进行短剧创作的能力
要实现这些能力,需要大量的投入,虽然现在AI编程非常火热,但是真正能够完美实现的,其实还比较少,因此,这些工作都需要小伙伴们的支持,如果你对这个项目感兴趣,可以通过DeepWiki来了解项目代码的底层结构,并fork项目后,向我们提PR,让我们一起共建一个属于我们自己的视频编辑UI框架。
最后,你的支持是我前进的动力,动动你的小手,到我们的github上给个start,让我们知道你对此感兴趣。
来源:juejin.cn/post/7579819594270900262
程序员越想创业,越不要急着动手
昨天晚上,我和老婆聊了一个创业点子。
一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。
能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧!
她听完以后跟我说,这个项目还是太定制化,和我之前做的一个项目很像。
那个项目一开始,我和朋友设想的是可以做完一个以后再推到不同的学校,但最后没有达到期望。不同的甲方想要的和我们做的差异很大,没办法推广,都得定制。
但我觉得这次不一样,她说了好几遍,发现我一直“冥顽不灵”,就不再说了。
今天休息,在写完专栏《转型 AI 工程师》第二篇以后,我想起之前老婆分享给我的一些抖音视频还没看,打开看了以后,发现她分享的都是一些赚钱妙招,有些真的很打破我的认知,让我大受震撼。
好些项目是我头一次见到,在这之前脑子里完全没有概念。这时我才明白她昨天晚上跟我说的话。对比别人讲的这些,我想做的的确是太小众、太定制了。
为什么我听不进去?
冯新(原真格基金投资合伙人)说过:创业企业的成长本质是创始人认知边界的突破,而『不知道自己不知道』的认知茧房,正是创始人成长的最大障碍。
过去八九年,我两耳不闻窗外事、一心只想搞技术,对商业机会的认知非常少。如今想要参与进 AI 这波浪潮,却不知道从何做起。
在前辈跟我说了诉求后,我像落水的人抓住绳子一样,脑子里都是那个新项目的画面:问题都有哪些、怎么解决、做完怎么推广到更多公司等等。这些画面在我脑子里不断循环,占据了全部的注意力。
而老婆说的"大规模复制的模式",在我脑子里没有画面,所以就完全听不进去。就像戴着VR眼镜,你只能看到眼镜里的世界,别人跟你说外面的世界是什么样,你根本想象不出来。
我想这也是为什么很多孩子你跟他讲话他不听,很多年轻人长辈跟他讲话他也不听。不是他们不想听,而是他们脑子里只能看到当前自己见到的、听到的、想到的一些事。
我不是第一个犯这种错的人
后来我查了一下,发现我这种情况不是个例。
哈佛商学院有个研究发现,90% 的创业者倒在头 18 个月。“最大的敌人不是市场或竞争对手,而是创业者自己"。
具体来说,就是对「快速试错」和「尽早动手」等观点的误解,导致在错误道路上浪费了太多资源。这种情况被叫做「错误的起步」——省略初期的全面审慎思考,直接进入执行阶段。
现在这个能源项目,如果我按照昨晚的想法继续推进,很可能又会掉进一样的坑。
在写下这篇文章的时候,我想明白了动手之前要调研的情况:
- 这个需求是不是真的普遍存在?
- 不同公司的差异有多大?
- 推广的成本和难度是什么?
- 有没有更标准化的切入点?
如何拓宽认知边界?
老婆分享的那几个抖音视频,讲的赚钱方式,有些我从来没想到过,也没接触过这些行业,脑子里完全没有画面。但看完以后,我突然明白了一件事:原来赚钱的方式有这么多种,而我一直在自己的一亩三分地里打转。
对于像我这样想要创业的人来说,今天最大的感悟是:一定要多看,一定要先知道「猪是怎么跑的」,哪怕吃不到猪肉,至少知道猪是怎么跑的,心里有了一个概念。
具体怎么做?我目前想到这些,欢迎你评论区留言:
1、主动搜索不同行业的赚钱案例
不是为了照搬,而是为了拓宽认知边界。
我在日历里加上了日程---每周花30分钟,专门看别人有什么小众赚钱模式,他们是怎么发现机会的,怎么切入市场的,怎么实现标准化的。抖音、小红书、知乎、即刻,都是很好的信息源。
关键是要看那些你从来没想到过的行业。比如我今天看到的几个案例:直播切片、广告媒介采买。
2、用 AI 筛选出适合自己的机会
看得多了,就会发现很多机会。接下来需要进行筛选。
我的优势是 AI 技术,所以我会重点关注那些「传统行业+AI」的机会。不是去做通用大模型,而是找那些可以用 AI 提升效率的垂直领域。
3、经常问自己三个问题
为了避免再次陷入「执行陷阱」,我给自己设了一个自我检查清单,每周问自己三个问题:
- 我现在做的事,是在拓宽认知边界,还是重复的经验复用?
- 我看到的机会,受众有多少,其中有多少人愿意付费?
- 这个项目如果一年内没有成果,我会坚持吗?坚持的原因是什么
4、搭建一个「商机捕获系统」
这几天我一直在想,能不能用 AI 搭建一个系统,自动帮我捕获各种赚钱商机?
我试了几个方案,发现真的可行。
这个系统的核心不是找到一个具体的项目,而是持续拓宽认知边界,让自己能看到更多的可能性。具体怎么搭建,我准备放到 转型 AI 工程师专栏 的最后大作业部分,这个点子比之前想的「深度研究助手」更有价值。
最后想说的
今天突然有这个感想,没想到越写越多,差不多了收个尾吧。
像我这样两耳不闻窗外事、一心只想搞技术的老程序员,想要创业不要急着动手。
不是说不要行动,而是说在行动之前,先拓宽自己的认知边界。
如果你脑子里只有一种赚钱方式,那你只能在这一种方式里打转。如果你脑子里有十种、一百种赚钱方式,你才能找到最适合自己的那一种。
我自己的经历就是教训。那个学校项目失败了,现在这个项目如果不调整思路,很可能又会失败。
但好在,我现在意识到了这个问题。
希望这篇文章对你也有启发。
以上。
来源:juejin.cn/post/7582854603061379114
我为什么放弃了XMind和亿图,投向了这款开源绘图工具的怀抱?
关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
思维导图、流程图应该是每个程序员都会用到的绘图工具。Xmind和亿图曾是我的首选工具,但是免费版功能受限,高级功能需付费,用起来总是差点意思。虽然通过其他方式正常使用(大家都懂得),但是软件的更新根本不敢动,一旦更新就会前功尽弃......而且两款工具总会来回切换,虽已习惯,但稍显麻烦!
直到不久前逛GitHub发现了一款开源的且可以在线使用的工具:Drawnix。界面简约,满足日常基本绘图需求,且支持 mermaid 语法转流程图等,用起来非常丝滑。整理一下分享给大家!
02 简介

2.1 名称的由来
Drawnix ,源于绘画( Draw)与凤凰( Phoenix )的灵感交织。凤凰象征着生生不息的创造力,而 Draw代表着人类最原始的表达方式。在这里,每一次创作都是一次艺术的涅槃,每一笔绘画都是灵感的重生。
创意如同凤凰,浴火方能重生,而 Drawnix 要做技术与创意之火的守护者。
2.2 框架
Drawnix 的定位是一个开箱即用、开源、免费的工具产品,它的底层是 Plait框架,Plait 是作者公司开源的一款画图框架,代表着公司在知识库产品上的重要技术沉淀。
Drawnix 是插件架构,与前面说到开源工具比技术架构更复杂一些,但是插件架构也有优势,比如能够支持多种 UI 框架(Angular、React),能够集成不同富文本框架(当前仅支持 Slate框架),在开发上可以很好的实现业务的分层,开发各种细粒度的可复用插件,可以扩展更多的画板的应用场景。
GitHub地址:
2.3 特性
- 💯 免费 + 开源
- ⚒️ 思维导图、流程图
- 🖌 画笔
- 😀 插入图片
- 🚀 基于插件机制
- 🖼️ 📃 导出为 PNG, JSON(
.drawnix) - 💾 自动保存(浏览器缓存)
- ⚡ 编辑特性:撤销、重做、复制、粘贴等
- 🌌 无限画布:缩放、滚动
- 🎨 主题模式
- 📱 移动设备适配
- 📈 支持 mermaid 语法转流程图
- ✨ 支持 markdown 文本转思维导图(新支持 🔥🔥🔥)
2.4 安装
提供Docker部署:
docker pull pubuzhixing/drawnix:latest
也可以在线使用:
地址:
03 最佳实践
实践我们采用在线方式,如果注重安全或者本地使用可以使用Docker部署。
3.1 界面说明
通过https://drawnix.com/进入之后,所有功能如图:

中间工具栏分别表示:
- 拖拽
- 选中
- 思维导图
- 文字
- 手绘
- 箭头
- 流程图
- 插入图片
- 格式转换
功能都比较简单,思维导图、流程图以及格式转化是常用的工具。

3.2 思维导图
思维导图的使用方式和Xmind的用法极为相似。选中节点,然后Tab键就可以添加同级子节点,也可以利用+号添加。-号并不是删除,而是合并。删除也非常简单,选中直接Delete即可。

3.3 流程图
流程图的图形相对来说比较简单,只有七项。

选中之后直接绘制即可,图形之间使用箭头链接即可。
3.4 格式转化
目前支持两种格式转化成流程图:

Mermaid

仅支持流程图、序列图和类图
Markdown

仅支持思维导图。
04 小结
Drawnix的极简设计满足日常绘图的需要,可能在很多功能上不如Xmind或者亿图,但是它确实两者的结合。是一款不错的绘图软件。
来源:juejin.cn/post/7582104240986963994
作为前端你必须要会的CICD
前言
这是一篇属于面向前端的关于CICD的入门文章,其旨在:
- 入门掌握
CI CD的用法 - 学习
CI和CD的含义及其实现细节 - 基于
GitLab展示如何给自己手上的项目添加CICD的流程
学习本文你需要注意的事情
- 你的项目必须是支持Node版本
16.20.0 - 笔者的CentOS安装Node18以上的版本底层库不支持,如果你想安装高版本的Node请先解决CentOS版本低的问题
- 本文采用的是CentOSLinux操作系统
- 本文的操作系统版本截图在下方

OK 如果你已经明白了我上面说的注意事项 那我们事不宜迟,直接开始本文的内容吧。
安装Node
(第一种)安装 NVM
请注意 如果使用这种安装方式在后面执行runner的时候在gitlab-runner账户会报错找不到Node 这是因为Linux的系统的用户环境隔离问题
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
更新环境变量
source ~/.bashrc
验证NVM是否安装成功
command -v nvm
使用NVM安装Node.js:
nvm install node
安装指定版
nvm install 16.20.0
使用 NVM 切换到安装的 Node.js 版本
nvm use node
nvm use node 16.20.0
验证安装:
node -v
npm -v
npx -v
(第二种)使用系统级 Node.js 安装
wget https://nodejs.org/dist/v16.20.0/node-v16.20.0-linux-x64.tar.xz
解压
tar xf node-v12.9.0-linux-x64.tar.xz
复制
cp -rf /root/node-v16.20.0-linux-x64 /usr/local/node
打开编辑配置文件
vim /etc/profile
在文件的最后一行加上这句话
export PATH=$PATH:/usr/local/node/bin
重载系统配置文件
source /etc/profile
这次在切换用户到gitlab-runner就可以查看到你安装的版本了
安装 Nginx
安装相关依赖
- zlib 开启 gzip 需要
- openssl 开启 SSL 需要
- pcre rewrite模块需要
- gcc-c++ C/C++ 编译器
yum -y install gcc-c++ zlib zlib-devel openssl openssl-devel pcre pcre-devel
下载压缩包
wget https://nginx.org/download/nginx-1.18.0.tar.gz
解压
tar -zxvf nginx-1.18.0.tar.gz
cd ./nginx-1.18.0
./configure
make
make install
查看安装路径
whereis nginx
编辑环境变量
vim /etc/profile
在文件最下面添加这两行
export NGINX_HOME=/usr/local/nginx
export PATH=$NGINX_HOME/sbin:$PATH
更新配置文件
source /etc/profile
查看nginx是否安装完成
nginx -v
开放 80 端口 如果不想一次性一个一个的放行端口,可以关闭防火墙
firewall-cmd --permanent --zone=public --add-port=80/tcp
查看防火墙的状态
systemctl status firewalld.service
关闭防火墙的状态
systemctl stop firewalld.service
查看已经放行的端口
firewall-cmd --zone=public --list-ports
重载防火墙
firewall-cmd --reload
启动
nginx
安装 GitLab
安装 GitLab,需要的时间比较长
yum -y install https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitlab-ce-14.3.0-ce.0.el7.x86_64.rpm
编辑配置文件
vim /etc/gitlab/gitlab.rb
修改配置文件

重载配置文件,
gitlab-ctl reconfigure
开放 1874 端口
firewall-cmd --permanent --zone=public --add-port=1874/tcp
查看已经放行的端口
firewall-cmd --zone=public --list-ports
重载防火墙
firewall-cmd --reload
修改gitlab的root用户密码
1.进入到gitlab控制面板中 gitlab-rails console -e production
2.执行命令: user = User.where(id: 1).first,此 user 则表示 root 用户
3、修改密码
执行命令:user.password = '12345678'修改密码
再次执行 user.password_confirmation = '12345678' 确认密码
4、保存密码
执行命令: user.save!
5、退出控制台
执行命令: exit

验证是否修改成功
http://192.168.80.130:1874/users/sign_in 把Ip换成自己的Ip 输入root的用户名和密码尝试进行登录,正常创建项目进行测试
配置 CI/CD
下载
wget -O /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
分配运行权限
chmod +x /usr/local/bin/gitlab-runner
创建用户
useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
安装 在 /usr/local/bin/gitlab-runner 这个目录下
gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
运行
gitlab-runner start
新建 runner
注册 runner
gitlab-runner register
输入 gitlab 的访问地址
输入 runner token
打开 http://192.168.80.130:1874/admin/runners 页面查看

runner 描述,随便填
测试vue项目部署
runner tag
ceshi
Enter optional maintenance note for the runner:
直接回车走过
输入(选择) shell 最后一步选择执行的脚本
shell

注册完成后,就可以在 http://192.168.80.130/admin/runners 里面看到创建的 runner。
nginx 配置项目访问地址
创建目录
mkdir -pv /www/wwwroot/dist
分配权限 如果后面执行脚本命令提示没有权限那就是这个地方有问题
chown gitlab-runner /www/wwwroot/dist/
(备用)如果权限有问题可以使用这个命令单独给这个目录设置上gitlab-runner用户权限
sudo chown -R gitlab-runner:gitlab-runner /www/wwwroot/
开放 3001 端口
firewall-cmd --permanent --zone=public --add-port=3001/tcp
重载防火墙 .
firewall-cmd --reload
打开 nginx 配置文件
vim /usr/local/nginx/conf/nginx.conf
在第一个 server 下方 (nginx 默认的,端口为80),加上下面的内容
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增开始
server {
listen 3001;
server_name localhost;
location / {
root /www/wwwroot/dist;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
#新增结束
重新加载配置文件
nginx -s reload
编写 .gitlab-ci.yml 文件
# 阶段
stages:
- build
- deploy
# 缓存 node_modules 减少打包时间,默认会清除 node_modules 和 dist
cache:
paths:
- node_modules/
# 拉取项目,打包
build:
stage: build
tags:
- ceshi
before_script:
# - export PATH=/usr/local/bin:$PATH
- node --version
- npm --version
- echo "开始构建"
script:
- cd ${CI_PROJECT_DIR}
- npm install
- npm run build
only:
- main
artifacts:
paths:
- dist/
# 部署
deploy:
stage: deploy
tags:
- ceshi
script:
- rm -rf /www/wwwroot/dist/*
- cp -rf ${CI_PROJECT_DIR}/dist/* /www/wwwroot/dist/
only:
- main
浏览器打开3001端口
提交代码到main分支上,如果想改为提交到其他分支上也可以进行自动部署就需要改only参数为分支名称
来源:juejin.cn/post/7546420270421999642
一年多三次考试,总算过了系统架构师
前言
2024/12/27更新:实体证书也出来啦,如下:

2024/12/20更新:电子证书出来啦,如下:

算上这次,我其实已经参加了三次考试,先贴上这三次的成绩,相信大家也能感受到我的心情:
虽然这次总算通过了考试,但看到综合知识的成绩还是心有余悸,由于前两次考试的打击(都是差一门案例没有通过,上半年只差了两分),这次考试前只写了两套半的综合知识真题和在考前一天准备了大半天(背论文模板和知识点集锦的 pdf),而综合知识也是自己信心最足的一门,结果这次压线通过,所以还是建议大家只要报名了考试,还是要认真准备,至少把往年综合知识的真题刷一刷,避免最后发现只有综合知识未通过而追悔莫及。
我也在Github上分享了几个我备考用到的文档资料,大家自行取用。
PS:这次考试通过还要感谢我女友的祝福,我们在今年5.23相识(上半年考试前两天,然后考试也是差两分),再加上这次的压线通过,感受到了冥冥之中自有天意(❁´◡`❁)。
考试注意点
从去年下半年开始,软考统一由笔试改为机考,虽然不用再担心写字速度太慢或者不美观导致论文扣分,但要注意的是键盘只能使用考场提供的,因此很多人可能不太习惯。就我这几次的考试经验来说,两个小时写论文还是比较紧凑的,剩余时间都不超过10分钟,还要用这点时间去通读检查一遍论文有没有什么错别字,因此在考前准备一个论文模板还是十分必要的,这样就可以在写模板内容的同时去构思正文,对于时间充分的小伙伴来说,也可以计时去练习写几篇论文。另外需要注意从2024年开始,系统架构师改为一年两考(不通过也可以趁热打铁立刻准备下一次的考试了),上午考综合知识和案例(总共四小时,分别两小时,综合知识写完可提前半小时交卷去写案例),下午考论文(两个小时),考试时间安排如下:
| 考试时间 | 考试科目 |
|---|---|
| 8:30—12:30 | 综合知识、案例分析 |
| 14:30—16:30 | 论文 |
备考经历
第一次(2 ~ 3个月):看完某赛视频全集(无大数据相关)+ 某赛知识点集锦 + 写完历年综合知识真题 + 案例论文对着答案看一遍(写了几道质量属性和数据库相关的案例题)+ 准备并背诵一个论文模板。
第二次(0.5 ~ 1个月):这次将上次的看视频改为了看教材(把考试重点的几个章节内容都混了个眼熟),然后其他准备都差不多,只是准备时间有相应减少。
第三次(1 ~ 2天):两套半综合知识真题 + 大致浏览一遍知识点集锦 + 背诵论文模板。
备考主要有以下注意点:
- 视频课不管是哪一家都无所谓,但需要注意架构师考试在22年12月更新了考试大纲,所以需要留意视频的版本不可太老,然后就是不管是在B站、闲鱼还是原价购买都不会有什么差别,只需保证视频内容完整即可。
- 各个机构的模拟题不要过多在意,尤其是考纲之外的题目,可作为对个人学习情况的测试。
- 近三次的考试由于是机考,只能在网上找到部分回忆版,不再有完整版真题,这个可自行搜索了解。
- 如果是第一次备考,建议还是至少 2 ~ 3 个月,除非基础特别好,不然还是建议将视频课看完(至少看完核心内容,计算机基础部分的优先级最低),这样至少可以保证综合知识问问拿下,还有就是真题特别特别特别重要。
备考方式
综合知识
就我的经验来讲,我觉得综合知识是最可控的部分,只需将视频课 + 重要知识点集锦 + 历年综合知识真题过一遍,综合知识是完全不需要担心的。还有就是遇到考纲之外的真题,比如今年有一道题是:一项外观设计专利里面相似设计最多有几个,像这种基本无再考可能的题,只需要看到答案后混个眼熟就可以。除此之外还有一部分反复考的知识点:构件、4 + 1视图、ABSD、DSSA、架构评估(质量属性)、系统架构风格、项目时间和成本计算以及软件测试,这些内容需要格外留意,有时间的话,可以把教材上相关知识的内容过一遍。除此之外,一定要记得考试时相信自己的第一感觉,不确定的题目不要修改答案。
案例分析
案例分析的题型变化比较大,更考验平时的技术积累,不过第一道必选题近几年都是和质量属性相关(除了23年下半年是大数据),然后就是 Redis 的考频也比较高,近三次考试有两次涉及(以往也有涉及),在24年上半年甚至精确到了命令的考察。此外,近几次案例也都考到了关于技术架构图的填空题,所以建议练习一下往年的相关题型,再到 ProcessOn 之类的平台找几个技术架构图看看。
案例考察的范围比较广,因此建议在高频考点上多加复习和准备。然后遇到不熟悉的知识点也不要慌,更不要空着不写,可以分点试着写一些或者硬凑一些相关的内容,能得一分是一分。如果时间充足,还是建议把往年的案例真题按照时间由近到远认真看一看,即使是一些视频中说的考试概率很低的知识点(Hibernate和设计模式)在前两次的考试和论文中也都有涉及,尤其是项目和技术经验不是那么丰富的小伙伴(比如我自己)需要注意这点。
论文
虽然看到很多小伙伴都说论文难写还会卡分,但因为我三次考试也都只有案例未过,论文虽然分数不高,但也都过了合格线,这里也分享一下我的写作经验。
我觉得写论文只需要记住真实项目 + 技术点讨论 + 论点点题并结合项目分析 + 项目中遇到的问题点这几点即可,即使内容有点流水账也无伤大雅,最重要的是写的让项目看起来真实,是自己做的,除了摘要和开头结尾可以找模板进行参考,正文部分还是需要自己结合论点去写,不能全是理论而没有一点技术点(使用到的各种工具和服务也都可以说,例如代码评审使用和项目管理相关的)的讨论。就以我这次的论文结构为例,首先是摘要部分(250字以内):
2022年12月,我所在公司承接了某区xxx的开发项目。我在该项目中担任系统架构设计师的职务,负责需求分析和系统的架构设计等工作。该项目主要提供xxx、xxx和xxx功能。本文将结合作者的实践,以xxx项目为例,论述xxx在系统开发中的具体应用。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。在xxx模块使用了xxx,解决了xxx问题。实践证明,采用xxx,提升了软件的开发效率和质量。整个项目历时一年多,于今年6月正式上线运行,整个系统运行稳定,达到了预期的目标的要求。
然后是开头和结尾(800字左右):
......。(项目背景,150字左右)
正是在这一背景下,2022年12月,我们公司承接了xxx项目,在本项目中,我担任系统架构师的职务,负责需求分析和系统的架构设计等工作。经过对项目的调研和对用户需求的分析,我们确认了系统应当具有以下功能:xxx,xxx,xxx。基于以上的需求,我们采用xxx解决了xxx问题。(300字左右,这部分介绍功能的部分可以和摘要内容有重合)
经过团队的共同努力,本项目按时交付,于今年6月顺利交付并上线,到目前运行稳定,不管是xxx使用xxx,还是xxx使用xxx都反馈良好。但在实施的过程中也遇到了一些问题,xxx。而如何让xxx更xxx是一项长期的工作,还有很多问题需要在实践中不断探索,在理论中深入研究并加以解决。只有这样,xxx才能不断地优化和发展,xxx。(350字左右)
最后是正文,由于我写的是软件维护(具体包含完善性维护、预防性维护、改正性维护、适应性维护),所以我首先用200 ~ 300字描述了这四种维护的具体含义(可以用自己的语言去描述,不需要和书上完全一致)。然后针对每种维护,再分四段用250 ~ 300字去结合项目和技术点具体去讨论我在每种维护中所做的工作。
当然上面只是我的一些论文写作经验,至少最近三次都是按照这个模板和套路去写,也都通过了。不过大家还是要结合自己的项目去做一些修改,建议多找几个论文综合一下,然后结合自己的语言去写一个属于自己的模板( •̀ ω •́ )✧。
感想
经过这三次的备考和考试经历,我觉得除了一些实力外,运气也占了一部分。就像这次的案例考了我熟悉的也简单的质量属性和 Cache Aside 缓存策略,前两次都有涉及到大数据这个我不熟悉的相关知识,也是我挂在案例的原因之一,所以大家如果考试遇到不熟悉的题或者分数还差一点,不妨再试一两次,相信自己可以的(●'◡'●)。如果大家有什么问题,也可以留言交流讨论。
来源:juejin.cn/post/7449570539884265524
当一个前端学了很久的神经网络...👈🤣
前言

最近在学习神经网络相关的知识,并做了一个简单的猫狗识别的神经网络,结果如图。
虽然有点绷不住,但这其实是少数情况,整体的猫狗分类正确率已经来到 90% 了。
本篇文章是给大家介绍一下我是如何利用前端如何做神经网络-猫狗训练的。如果觉得这篇文章有些复杂,那么也可以看看我的上一篇更简单的鸢尾花分类
步骤概览
还是掏出之前那个步骤流程,我们只需要按照这个步骤就可以训练出自己的神经网络
- 处理数据集
- 定义模型
- 神经网络层数
- 每层节点数
- 每层的激活函数
- 编译模型
- 训练模型
- 使用模型
最终的页面是这样的

处理数据集
- 首先得找到数据集,本次使用的是这个 http://www.kaggle.com/datasets/li… 2000 个猫图,2000 个狗图,足够我们使用(其实我只用了其中 500 个,电脑跑太慢了)
- 由于这些图片大小不一致,首先我们需要将其处理为大小一致。这一步可以使用 canvas 来做,我统一处理成了 128 * 128 像素大小。
const preprocessImage = (img: HTMLImageElement): HTMLCanvasElement => {
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const ctx = canvas.getContext("2d");
if (!ctx) return canvas;
// 保持比例缩放并居中裁剪
const ratio = Math.min(128 / img.width, 128 / img.height);
const newWidth = img.width * ratio;
const newHeight = img.height * ratio;
ctx.drawImage(
img,
(128 - newWidth) / 2,
(128 - newHeight) / 2,
newWidth,
newHeight
);
return canvas;
};
这里可能就有同学要问了:imooimoo,你怎么返回了 canvas,不应该返回它 getImageData 的数据点吗。我一开始也是这样想的,结果 ai 告诉我,tfjs 是可以直接读取 canvas 的,牛。
tf.browser.fromPixels() // 可以接受 canvas 作为参数
- 将其处理为 tfjs 可用的对象
// 加载单个图片并处理为 tfjs 对应格式
const loadImage = async (category: "cat" | "dog", index: number): Promise<ImageData> => {
const imgPath = `src/pages/cat-dog/image/${category}/${category}.${index}.jpg`;
const img = new Image();
img.src = imgPath;
await new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
return {
path: imgPath,
element: img,
tensor: tf.browser.fromPixels(preprocessImage(img)).div(255), // 归一化
label: category === "cat" ? 0 : 1,
};
};
// 加载全部图片
const loadDataset = async () => {
const images: ImageData[] = [];
for (const category of ["cat", "dog"]) {
for (let i = 1000; i < 1500; i++) { // 这里只使用了后 500 张,电脑跑不动
try {
const imgData = await loadImage(category, i);
images.push(imgData);
} catch (error) {
console.error(`加载${category === "cat" ? "猫" : "狗"}图片失败: ${category}.${i}.jpg`, error);
}
}
}
return images;
};
定义模型 & 编译模型
由于我们的主题是图片识别,图片识别一般会需要用到几个常用的层
- 最大池化层:用于缩小图片,节约算力。但也不能太小,否则很糊会提取不出东西。
- 卷积层:用于提取图片特征
- 展平层:将多维的结果转为一维
有同学可能想问为什么会有多维。首先是三维的颜色,输入就有三维;卷积层的每一个卷积核,都会使结果增加维度,所以后续的维度会很高。这张图比较形象,最后就只会剩下一维,方便机器进行计算。
// 创建卷积神经网络模型
const createCNNModel = () => {
const model = tf.sequential({
layers: [
// 最大池化层:降低特征图尺寸,增强特征鲁棒性
tf.layers.maxPooling2d({
inputShape: [128, 128, 3], // 输入形状 [高度, 宽度, 通道数]
poolSize: 2, // 池化窗口尺寸 2x2
strides: 2, // 滑动步长:每次移动 n 像素,使输出尺寸减小到原先的 1/n
}),
// 卷积层:用于提取图像局部特征
tf.layers.conv2d({
filters: 32, // 卷积核数量,决定输出特征图的深度
kernelSize: 3, // 卷积核尺寸 3x3
activation: "relu", // 激活函数:修正线性单元,解决梯度消失问题
padding: "same", // 边缘填充方式:保持输出尺寸与输入相同
}),
// 展平层:将多维特征图转换为一维向量
tf.layers.flatten(),
// 全连接层(输出层):进行最终分类
tf.layers.dense({
units: 2, // 输出单元数:对应猫/狗两个类别
activation: "softmax", // 激活函数:将输出转换为概率分布
}),
],
});
// 编译模型,参数基本写死这几个就对了
model.compile({
optimizer: "adam",
loss: "categoricalCrossentropy",
metrics: ["accuracy"],
});
console.log("模型架构:");
model.summary();
return model;
};
这里实际上只需要额外注意两点:
- 卷积层的激活函数
activation: "relu",这里理论上是个非线性激活函数就行。但是我个人更喜欢 relu,函数好记,速度和效果又不错。 - 输出层的激活函数
activation: "softmax",由于我们做的是分类,最后必须是这个。
训练模型
训练模型可以说的就不多了,也就是提供一下你的模型、训练集就可以开始了。这里有俩参数可以注意下
- epochs: 训练轮次
- validationSplit: 验证集比例,用于测算训练好的模型准确程度并优化下一轮的模型
// 训练模型
const trainModel = async (
model: tf.Sequential,
xData: tf.Tensor4D,
yData: tf.Tensor2D
) => {
setTrainingLogs([]); // 清空之前的训练日志
await model.fit(xData, yData, {
epochs: 10, // 训练轮数
batchSize: 4,
validationSplit: 0.4,
callbacks: {
onEpochEnd: (epoch, logs) => {
if (!logs) return;
setTrainingLogs((prev) => [
...prev,
{
epoch: epoch + 1,
loss: Number(logs.loss.toFixed(4)),
accuracy: Number(logs.acc.toFixed(4)),
},
]);
},
},
});
};
整体页面
基本就是这样了,稍微写一下页面,基本就完工了


总结
别慌,神经网络没那么可怕,核心步骤就那几步,冲冲冲。
来源:juejin.cn/post/7477540557787938852













