一行生成绝对唯一 ID:别再依赖 Date.now() 了!
在前端开发中,“生成唯一 ID” 是高频需求 —— 从列表项标识、表单临时存储,到数据缓存键值,都需要一个 “绝对不重复” 的标识符。但看似简单的需求下,藏着很多容易踩坑的实现方式,稍有不慎就会引发数据冲突、逻辑异常等问题。
今天我们就来拆解常见误区,带你掌握真正可靠的唯一 ID 生成方案。
一、为什么 “唯一 ID” 比想象中难?
唯一 ID 的核心要求是 “全局不重复”,但前端环境的特殊性(无状态、多标签页、高并发操作),让很多看似合理的方案在实际场景中失效。
下面两种常见实现,其实都是 “伪唯一” 陷阱。
❌ 误区 1:时间戳 + 随机数(Date.now() + Math.random())
很多开发者会直觉性地将 “时间唯一性” 和 “随机唯一性” 结合,写出这样的代码:
// 错误示例:看似合理的“伪唯一”方案
function generateNaiveId() {
// 时间戳转36进制(缩短长度)+ 随机数截取
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 示例输出:l6n7f4v2am50k9m7o4
这种方案的缺陷在高并发场景下会暴露无遗:
- 时间戳精度不足:
Date.now()的精度是毫秒级(1ms),如果同一毫秒内调用多次(比如循环生成、高频接口回调),ID 的 “时间部分” 会完全重复; - 伪随机性风险:
Math.random()生成的是 “非加密级随机数”,其算法可预测,在短时间内可能生成重复的序列,进一步增加冲突概率。
结论:仅适用于低频次、非核心场景(如临时展示用 ID),绝对不能用于生产环境的核心数据标识。
❌ 误区 2:全局自增计数器
另一种思路是维护一个全局变量自增,看似能保证 “有序唯一”:
// 错误示例:自增计数器方案
let counter = 0;
function generateIncrementId() {
return `id-${counter++}`;
}
// 示例输出:id-0、id-1、id-2...
但在浏览器环境中,这个方案的缺陷更致命:
- 无状态丢失:页面刷新、路由跳转后,
counter会重置为 0,之前的 ID 序列会重复; - 多标签页冲突:用户打开多个相同页面时,每个页面的
counter都是独立的,会生成完全相同的 ID(比如两个页面同时生成id-0)。
结论:浏览器环境中几乎毫无实用价值,仅能用于单次会话、单页面的临时标识。
二、王者方案:一行代码实现绝对唯一 —— crypto.randomUUID()
既然简单方案不可靠,我们需要借助浏览器原生提供的 “加密级” 能力。crypto.randomUUID() 就是 W3C 标准推荐的官方解决方案,彻底解决 “唯一 ID” 难题。
1. 用法:一行代码搞定
crypto 是浏览器内置的全局对象(无需引入任何库),专门提供加密相关能力,randomUUID() 方法可直接生成符合 RFC 4122 v4 规范 的 UUID(通用唯一标识符):
// 正确示例:生成绝对唯一ID
const uniqueId = crypto.randomUUID();
// 示例输出:3a6c4b2a-4c26-4d0f-a4b7-3b1a2b3c4d5e
2. 为什么它是 “绝对唯一” 的?
crypto.randomUUID() 的可靠性源于三个核心优势:
- 极低碰撞概率:v4 UUID 由 122 位随机数构成,组合数量高达
2^122(约 5.3×10^36),相当于 “在地球所有沙滩的沙粒中,选中某一颗特定沙粒” 的概率,实际场景中碰撞概率趋近于 0; - 加密级随机性:基于 “密码学安全伪随机数生成器(CSPRNG)”,随机性远优于
Math.random(),无法被预测或破解,避免恶意伪造重复 ID; - 跨环境兼容:生成的 UUID 是全球通用标准格式(8-4-4-4-12 位字符),前端、后端(Node.js、Java 等)、数据库(MySQL、MongoDB)都能直接识别,无需格式转换。
3. 兼容性:覆盖所有现代环境
crypto.randomUUID() 的支持范围已经非常广泛,完全满足绝大多数新项目需求:
- 浏览器:Chrome 92+、Firefox 90+、Safari 15.4+(2022 年及以后发布的版本);
- 服务器:Node.js 14.17+(LTS 版本均支持);
- 框架:Vue 3、React 18、Svelte 等现代框架无任何兼容性问题。
三、兼容性兜底方案(针对旧环境)
如果需要兼容旧浏览器(如 IE11)或低版本 Node.js,可以使用第三方库 uuid(轻量、无依赖),其底层逻辑与 crypto.randomUUID() 一致:
安装依赖:
npm install uuid
# 或 yarn add uuid
使用方式:
// 旧环境兜底方案
import { v4 as uuidv4 } from 'uuid';
const uniqueId = uuidv4();
// 示例输出:同标准UUID格式
四、总结:唯一 ID 生成的 “最佳实践”

对于 2023 年后的新项目,直接使用 crypto.randomUUID() 即可 —— 一行代码、零依赖、绝对可靠,彻底告别 “ID 重复” 的烦恼!
来源:juejin.cn/post/7561781514922688522
前端终于不用再写html,可以js一把梭了,我的ovs(不写html,兼容vue)的语法插件终于上线了
OVSJS 语法预览
语法是这样的:

项目资源
- GitHub 地址: ovsjs 示例代码 (hello.ovs)
- VS Code 语法提示插件: ovs-vscode-client 下载
模仿的kotlin的语法

欢迎大家体验交流!
- 卫星 (WeChat):
alamhubb
如果你对这个语法感兴趣欢迎和我交流,谢谢

来源:juejin.cn/post/7579871631096266778
为何前端圈现在不关注源码了?
大家好,我是双越。前百度 滴滴 资深前端工程师,慕课网金牌讲师,PMP。我的代表作有:
- wangEditor 开源 web 富文本编辑器,GitHub 18k star,npm 周下载量 20k
- 划水AI Node 全栈 AIGC 知识库,包括 AI 写作、多人协同编辑。复杂业务,真实上线。
- 前端面试派 系统专业的面试导航,刷题,写简历,看面试技巧,内推工作。开源免费。
开始
大家有没有发现一个现象:最近 1-2 年,前端圈不再关注源码了。
最近 Vue3.6 即将发布,alien-signal 不再依赖 Proxy 可更细粒度的实现响应式,vapor-model 可以不用 vdom 。
Vue 如此大的内部实现的改动,我没发现多少人研究它的源码,我日常关注的那些博客、公众号也没有发布源码相关的内容。
这要是在 3 年之前,早就开始有人研究这方面的源码了,博客一篇接一篇,跟前段时间的 MCP 话题一样。
还有前端工具链几乎快让 Rust 重构一遍了,rolldown turbopack 等产品使得构建效率大大提升。这要是按照 3 年之前对 webpack 那个研究态度,你不会 rust 就不好意思说自己是前端了。
不光是这些新东西,就是传统的 Vue React 等框架源码现在也没啥热度了,我关注每日的热门博客,几乎很少有关于源码的文章了。
这是为什么呢?
泡沫
看源码,其实是一种泡沫,现在破灭了。所谓泡沫,就是它的真实价值之前一直被夸大,就像房地产泡沫。
前几年是互联网发展的红利期,到处招聘开发人员,大家都拿着高工资,随便跳槽就能涨薪 20% ,大家就会误以为真的是自己的能力值这么多钱。
而且,当年面试时,尤其是大公司,为了筛选出优秀的候选人(因为培训涌入的人实在太多),除了看学历以外,最喜欢考的就是算法和源码。
确实,如果一个技术人员能把算法和源码看明白,那他肯定算是一个合格的程序员,上限不好说,但下限是能保证的。就像一个人名牌大学毕业的,他的能力下限应该是没问题的。
大公司如此面试,其他公司也就跟风,面试题在网络上传播,各位程序员也就跟风学习,很快普及到整个社区。
所以,如果不经思考,表面看来:就是因为我会算法、会源码,有这些技能,才拿到一个月几万甚至年薪百万的工资。
即,源码和算法价值百万。
现状
现在泡沫破灭了。业务没有增长了,之前是红利期,现在是内卷期,之前大量招聘,现在大量裁员。
你看这段时间淘宝和美团掐架多严重,你补贴我补贴,你广告我也广告。如果有新业务增长,他们早就忙着去开疆拓土了,没公司在这掐架。
面试少了,算法和源码也就没有发挥空间了。关键是大家现在才发现:原来自己会算法会源码,也会被裁员,也拿不到高工资了。
哦,原来之前自己的价值并不是算法和源码决定的,最主要是因为市场需求决定的。哪怕我现在看再多的源码,也少有面试机会,那还看个锤子!
现在企业预算缩减,对于开发人员的要求更加返璞归真:降低工资,甚至大量使用外包人员代替。
所以开发人员的价值,就是开发一些增删改查的日常 web 或 app 的功能,什么算法和框架源码,真实的使用场景太少。
看源码有用吗?
答案当然是肯定的。学习源码对于提升个人技术能力是至关重要的,尤其是对于初学者,学习前辈经验是个捷径。
但我觉得看 Vue react 这些源码对于开发提升并不会很直接,它也许会潜移默化的提升你的“内功”,但无法直接体现在工作上,除非你的工作就是开发 Vue react 类的框架。
我更建议大家去看一些应用类的源码,例如 UI 组件库的源码看如何封装复杂组件,例如 vue-admin 看如何封装一个 B 端管理后台。
再例如我之前学习 AI Agent 开发,就看了 langChain 提供的 agent-chat-ui 和 Vercel 提供的 ai-chatbot 这两个项目的源码,我并没有直接看 langChain 的源码。
找一些和你实际开发工作相关的一些优秀开源项目,学习他们的设计,阅读他们的源码,这是最直接有效的。
最后
前端人员想学习全栈 + AI 项目和源码,可关注我开发的 划水AI,包括 AI 写作、多人协同编辑。复杂业务,真实上线。
来源:juejin.cn/post/7531888067218800640
我们需要前端架构师这个职位吗?
前端架构师,这个岗位,在我的技术体系的认知中,是不需要这样的一个岗位的。但是市面上,我发现,在招聘的需求中,很多企业,依然在招聘前端架构师这个岗位。
为什么市场中,依然会有这个岗位呢?
真实的场景中,这个岗位的设立,是否真的是合理的呢?
作为前端出身的同学,这是我一直在思考的一个问题,因为这个问题,关乎一个人的职业发展,代表了我们前端的同学,职业的道路的天花板,到底在哪里。
如果前端架构师,这个岗位,本身是不应该出现的,那么我们前端的职业道路,就不应该走纯技术路线。真正应该走的是,技术兼管理的路线,也就是应该走前端leader的角色,再往上就是技术总监/CTO的职业角色。
如果前端架构师,这个岗位,是应该出现,那么前端的职业道路,其实完全可以走纯技术路线,从开发到前端架构师,然后持续深耕。
不同的方向,对人的要求是不一样的。
走纯技术路线,对一个人的更大要求是专注,持续的技术学习力,持续的自我技术进步,其他方面,作为辅助,协助你的技术能力在职场中进行发挥,你就能在这个行业和岗位上,有一定的立足之地。
走leader的路线,对一个人的要求是全面,关注点在技术,人,事务,项目,管理等等一系列比较杂乱的事情上,它注定了不能过于专注,而是站在高点,俯瞰整个大盘,才能真正的把事情做好。
作为自己,一直以来,努力的方向,也是走技术leader的路线。
真实的市场场景是什么样?
有一句很经典的话,世界是由一帮草台班子组成的。
这句话反馈到真实的工作场景中,就变成了这样的现状。
大多数的人的技术水准,真的让人一言难尽,很多人,真的也就是把技术,当作一份吃饭的差事,大多数人对技术,并没有追逐的热情。
我们这个行业,有太多的小公司了,小公司招聘人的标准,和大公司比,也是真的一言难尽,我见过有些公司,用四五千的薪资,招聘了一些人做事,真的是啥也不会。
大量的中小型企业,招聘技术人员,薪资大概给1万左右,这类研发人员,大多数的水准,大概就是能把项目做出来,会一些框架,仅此而已。
但是我们日常中,遇到的业务问题,往往是复杂的,比如前端的复杂表单问题,不同环境的运行容器差异问题,各种各样的兼容问题,复杂的数据处理和渲染问题等等。这类问题,其实在日常的开发中,很常见,但是往往很多人对此,难以处理。
这个时候,很多企业,潜意识就觉得,招聘一个技术更厉害的人,这个时候,前端架构师这个岗位,其实就出现了,而这也是市场中,需要这个岗位的现实情况。
不同规模企业的前端组织架构到底有哪些差异?
在大公司,我看到的更多的是,前端技术leader的岗位,一般而言,是由一个技术leader,带领团队,完成业务。
比如阿里,比较有钱,一般一个团队中,p8是前端leader,p6/p7是做事的主力,配备部分p4/p5的同学,一起完成业务。整体大概是,一个leader负责统筹全局,团队中真正做事的同学,完全由能力驾驭自己的业务。
再中小型公司,我看到的是,因为成本的原因,招聘的工程师偏于初中级,然后团队中,有那么一两位高级工程师作为主力成员。有些时候,这些高级工程师还会担任leader的角色。
但是这类团队有一些问题,就是因为技术能力问题,无法真正的做到,对企业的业务负责。
1、针对业务场景,无法给出合理的方案/方法。经常性的在日常工作中,这类团队,会提到这个需求改动太大,这个方案没法实现,这个东西做不了等等。但是其实正常的场景中,业务方提出一个需求,本身是有一定的运营目标/目的,这个时候,应该从业务的角度出发,再结合我们互联网技术,针对这个运营目标,提出我们的产品/技术方案,然后执行。
2、无法做出合理的判断。在大多数的场景中,我们对代码的要求是,高内聚和低耦合,代码的结构要清晰,可维护要高。但是还应该有技术之外的一些判断,我们完成一项业务,应该团队配备什么样的成员,市场中哪类程序员好招聘,技术的选型和业务是不是最优解,在人员、技术,业务、成本,这类问题中,如何达到更高的效率最优。
在中小型公司,大家习惯性的把问题简单化,做不了,判断不了,以为招聘一个技术更厉害的人,就能解决当下问题。
那到底需要前端架构师吗?
这个答案很显然,其实当下的市场中,市场有这样的职位诉求,原因就是一些复杂问题,很多企业搞不定。(但是这个问题的解决之道,不在于招聘前端架构师这么一个岗位,而是在于团队内在的一些问题,这些问题恰恰是需要一个前端leader来解决的)
从职业发展的角度来说,其实是不需要前端架构师的。
我们从技术的角度,来分析一下为什么不需要前端架构师。
前端的职责,是对UI负责,我们的工作,主要是针对,不同的容器环境(浏览器、手机app、桌面端内嵌h5等等)、不同的技术(RN、react、小程序等等),实现不同的端上的产品(App、小程序、网页、桌面端应用),我们通过接口协议,同后端进行数据通信。
端上的页面,主体是运行在用户端的设备上,最大的障碍是加载/渲染性能。接口方面,是和用户的网络环境相关。
这些东西,其实从技术的层面上,属于开发的职责。我们不得不承认,前端开发的层面,入手会比后端简单一些,但是做到一定程度,其实要求是要比后端更高的。
所以前端架构师,到底在架构什么东西?它不过就是,针对每一种端上的技术,使用的比普通开发,更好一些。
相比而言,后端有太多的策略性的东西了,哪些数据是业务数据,哪些数据是缓存数据,哪些数据要支持实时查询,哪些数据支持统计查询,后端的基础组件也多,不同的组件,擅长的事情也不一样,适用的场景其实也有差异,kafka、redis这些东西,数据分库和分表,按什么维度分,机器的运行性能,支持的量到底有多大等等。
这些就是后端架构师的存在责任,同一块代码,在不同的场景下是完全不同的。在某些场景中可能是最优解,换一个场景,可能就是最差的代码。
这就是我一直推崇的价值观,前端和后端,不一样,前端不需要架构师。
合理的团队架构到底长什么样?
如我个人所评判的那样,一个团队中,是不需要前端架构师这个角色的,那么对于那些中小型公司的人来说,到底什么样的结构,符合自身最优价值的团队结构呢?
答案是这样的,一位专家级别的前端,带领一两个干活的主力,再加上多个初中级的工程师。专家级别的工程师-也就是leader,保障我们的技术和方案,是行业内标准方案,方向是最优的,一两个高级工程师,辅助这个leader把事情能落地下去,其他所有初中级工程师,就是干活的苦力,也就是团队内的技术体力劳动者。
这也就是,我认可的职业发展中,前端需要leader的原因,但是不需要架构师。
来源:juejin.cn/post/7559053482831052846
别再死磕框架了!你的技术路线图该更新了
先说结论:
前端不会凉,但“只会几个框架 API”的前端,确实越来越难混
这两年“前端要凉了”“全栈替代前端”的声音此起彼伏,本质是门槛重新洗牌:
- 简单 CRUD、纯样式开发被低代码、模板代码和 AI 模型快速蚕食;
- 复杂业务、工程体系、跨端体验、AI 能力集成,反而需要更强的前端工程师去撑住。
如果你对“前端的尽头是跑路转管理”已经开始迷茫,那这篇就是给你看的:别再死磕框架版本号,该更新的是你的技术路线图。
一、先搞清楚:2025 的前端到底在变什么?
框架红海:从“会用”到“用得值”
React、Vue、Svelte、Solid、Qwik、Next、Nuxt……Meta Framework 一大堆,远远超过岗位需求。
现在企业选型更关注:
- 生态成熟度(如 Next.js 的 SSR/SSG 能力)
- 框架在应用生命周期中的角色(渲染策略、数据流转、SEO、部署)
趋势:
- 框架 Meta 化(Next.js、Nuxt)将路由、数据获取、缓存策略整体纳入规范;
- 约定优于配置,不再是“一个前端库”,而是“一套完整解决方案”。
以前是“你会 Vue/React 就能干活”,现在是“你要理解框架在整个应用中的角色”。
工具有 AI,开发方式也在变
AI 工具(如 Cursor、GitHub Copilot X)可以显著提速,甚至替代重复劳动。
真正拉开差距的变成了:
- 你能给 AI 写出清晰、可实现的需求描述(Prompt);
- 你能判断 AI 生成代码的质量、潜在风险、性能问题;
- 你能基于生成结果做出合理抽象和重构。
AI 不是来抢饭碗,而是逼你从“码农”进化成“架构和决策的人”。
业务侧:前端不再是“画界面”,而是“做体验 + 做增长”
- B 端产品:交互工程师 + 低代码拼装师 + 复杂表单处理专家;
- C 端产品:与产品运营深度捆绑,懂 A/B 测试、埋点、Funnel 分析、广告投放链路;
- 跨平台:Web + 小程序 + App(RN/Flutter/WebView)混合形态成为常态。
那些还在喊“切图仔优化 padding”的岗位确实在消失,但对“懂业务、有数据意识、能搭全链路体验”的前端需求更高。
二、别再死磕框架 API:2025 的前端核心能力长什么样?
基石能力:Web 原生三件套,得真的吃透
重点不是“会用”,而是理解底层原理:
- JS:事件循环、原型链、Promise 执行模型、ESM 模块化;
- 浏览器:渲染流程(DOM/CSSOM/布局/绘制/合成)、HTTP/2/3、安全防护(XSS/CSRF)。
这块扎实了,你在任何框架下都不会慌,也更能看懂“框架为什么这么设计”。
工程能力:从“会用脚手架”到“能看懂和调整工程栈”
Vite、Rspack、Turbopack 等工具让工程构建从“黑魔法”变成“可组合拼装件”。
你需要:
- 看懂项目的构建配置(Vite/Webpack/Rspack 任意一种);
- 理解打包拆分、动态加载、CI/CD 流程;
- 能排查构建问题(路径解析、依赖冲突)。
如果你在团队里能主动做这些事,别人对你的“级别判断”会明显不一样。
跨端和运行时:不只会“写 Web 页”
2025 年前端视角的关键方向:
- 小程序/多端框架(Taro、Uni-app);
- 混合方案(RN/Flutter/WebView 通信机制);
- 桌面端(Electron、Tauri)。
建议:
- 至少深耕一个“跨端主战场”(如 Web + 小程序 或 Web + Flutter)。
数据和状态:从“会用 Vuex/Redux”到“能设计状态模型”
现代前端复杂度 70% 在“数据和状态管理”。
进阶点在于:
- 设计合理的数据模型(本地 UI 状态 vs 服务端真相);
- 学会用 Query 库、State Machine 解耦状态与视图。
当你能把“状态设计清楚”,你在复杂业务团队里会非常吃香。
性能、稳定性、可观测性:高级前端的硬指标
你需要系统性回答问题,而不是“瞎猜”:
- 性能优化:首屏加载(资源拆分、CDN)、运行时优化(减少重排、虚拟列表);
- 稳定性:错误采集、日志上报、灰度发布;
- 工具:Lighthouse、Web Vitals、Session Replay。
这块做得好的人往往是技术骨干,且很难被低代码或 AI 直接替代。
AI 时代的前端:不是“写 AI”,而是“让 AI 真正跑进产品”
你需要驾驭:
- 基础能力:调用 AI 平台 API(流式返回处理、增量渲染);
- 产品思维:哪些场景适合 AI(智能搜索、文档问答);如何做权限控制、错误兜底。
三、路线图别再按“框架学习顺序”排了,按角色来选
初中级:从“会用”到“能独立负责一个功能”
目标:
- 独立完成中等复杂度模块(登录、权限、表单、列表分页)。
建议路线:
- 夯实 JS + 浏览器基础;
- 选择 React/Vue + Next/Nuxt 做完整项目;
- 搭建 eslint + prettier + git hooks 的开发习惯。
进阶:从“功能前端”到“工程前端 + 业务前端”
目标:
- 优化项目、推进基础设施、给后端/产品提技术方案。
建议路线:
- 深入构建工具(Webpack/Vite);
- 主导一次性能优化或埋点方案;
- 引入 AI 能力(如智能搜索、工单回复建议)。
高级/资深:从“高级前端”到“前端技术负责人”
目标:
- 设计技术体系、推动长期价值。
建议路线:
- 明确团队技术栈(框架、状态管理、打包策略);
- 主导跨部门项目、建立知识分享机制;
- 评估 AI/低代码/新框架的引入价值。
四、2025 年不要再犯的几个错误
- 只跟着热点学框架,不做项目和抽象
- 选一个主战场 + 一个备胎(React+Next.js,Vue+Nuxt.js),用它们做 2~3 个完整项目。
- 完全忽略业务,沉迷写“优雅代码”
- 把重构和业务迭代绑一起,而不是搞“纯技术重构”。
- 对 AI 持敌视和逃避态度
- 把重复劳动交给 AI,把时间投到架构设计、业务抽象上。
- 把“管理”当成唯一出路
- 做前端架构、性能优化平台、低代码平台的技术专家,薪资和自由度不输管理岗。
五、一个现实点的建议:给自己的 2025 做个“年度规划”
Q1:
- 选定主技术栈(React+Next 或 Vue+Nuxt);
- 做一个完整小项目(登录、权限、列表/详情、SSR、部署)。
Q2:
- 深入工程化方向(优化打包体积、搭建监控埋点系统)。
Q3:
- 选一个业务场景引入 AI 或配置化能力(如智能搜索、低代码表单)。
Q4:
- 输出和沉淀(写 3~5 篇技术文章、踩坑复盘)。
最后:别问前端凉没凉,先问问自己“是不是还停在 2018 年的玩法”
- 如果你还把“熟练掌握 Vue/React”当成简历亮点,那确实会焦虑;
- 但如果你能说清楚:
- 在复杂项目里主导过哪些工程优化;
- 如何把业务抽象成可复用的组件/平台;
- 如何在产品里融入 AI/多端/数据驱动;
那么,在 2025 年的前端市场,你不仅不会“凉”,反而会成为别人眼中的“稀缺”。
别再死磕框架了,更新你的技术路线图,从“写页面的人”变成“打造体验和平台的人”。这才是 2025 年前端真正的进化方向。
来源:juejin.cn/post/7573694361474629659
别再吹性能优化了:你的应用卡顿,纯粹是因为产品设计烂🤷♂️

大家好!
最近面试,我发现一个很有意思的事情。几乎每个高级前端的简历上,都专门开辟了一栏,叫性能优化。
里面写满了各种高大上的名词😖:
使用Virtual List(虚拟列表)优化长列表渲染...
使用Web Worker把复杂计算移出主线程...
使用WASM重写核心算法...
看着这些,我通常会问一个问题:
你为什么要渲染一个有一万条数据的列表?用户真的看得过来吗?
候选人通常会愣住,然后支支吾吾地说:“呃...这是我们产品经理要求的🤷♂️。”
这就是今天我想聊的话题:
在2025年的今天,前端领域90%的所谓性能瓶颈,根本不是技术问题,而是产品问题。
我们这群工程师,拿着最先进的前端技术(Vite, Rust, WASM),却在日复一日地给一坨屎💩(糟糕的产品设计)雕花。
我们正在解决错误的问题
让我们还原一个经典的性能优化现场吧👇。
场景:一个中后台的超级表格(默认大家应该比较熟悉🤔)。
产品经理说需求:这个表格要展示所有订单,大概有50列,每页要展示500条,而且要支持实时搜索,还要支持列拖拽,每个单元格里可能还有下拉菜单...

开发者的第一反应(技术视角) :
- 50列 x 500行 = 25000个DOM节点,浏览器肯定卡死。
- 快!上虚拟滚动(Virtual Scroll)!
- 快!上防抖(Debounce)!
- 快!上Memoization(缓存)!
我们为了这个需求,引入了复杂的 第三方库,写了晦涩难懂的优化代码,甚至为了解决虚拟滚动带来的样式问题(比如高度坍塌、定位异常),又打了一堆补丁。
最后,页面终于不卡了。我们觉得自己很牛逼,技术很强。
但我们从来没问过那个最核心的问题:
人类的视网膜和大脑,真的能同时处理50列 x 500行的数据吗?
答案是:不能。
当屏幕上密密麻麻挤满了数据时,用户的认知负荷已经爆表了。他根本找不到他要看的东西。他需要的不是高性能的渲染,他需要的是筛选和搜索。
我们用顶级的技术,去实现了一个反人类的设计。 这不是优化,这是叫作恶😠。
真正的优化,是从砍需求开始
我曾经接手过一个类似的项目,页面卡顿到FPS只有10。前任开发留下了几千行用来优化渲染的复杂代码,维护起来生不如死。
我接手后,没有改一行渲染代码。
我直接去找了产品总监,把那个页面投在大屏幕上,问了他三个问题:
1.你看这一列 订单原始JSON日志,平均长度3000字符,你把它全展示在表格里,谁会看?
砍掉!改成一个查看详情的按钮,点开再加载。DOM节点减少20%。
2.这50列数据,用户高频关注的真的有这么多吗?
默认只展示核心的8列。剩下的放在自定义列里,用户想看自己勾选。DOM节点减少80%。
3.我就不知道为什么🤷♂️ 要一次性加载500条?用户翻到第400条的时候,他还记得第1条是什么吗?
赶紧砍掉!改成标准的分页,每页20条。DOM节点减少96%。
做完这三件事,我甚至把之前的虚拟滚动代码全删了,回退到了最朴素的<table>标签。
结果呢?
- 页面飞一样快(因为DOM只有原来的1%)。
- 代码极其简单(维护就更简单了🤔)。
- 用户反而更开心了(因为界面清爽了,信息层级清晰了)。
这才是最高级的性能优化:不仅优化了机器的性能,更优化了人的体验。
技术自负的陷阱
为什么我们总是陷在技术优化的泥潭里出不来呢?😒
因为我们有技术自负。
作为工程师,我们潜意识里觉得:承认这个需求做不了(或者做不好),是因为我技术不行。
产品经理要五彩斑斓的黑,我就得给他做出来!
产品经理要在这个页面跑3D地球,我就得去学Three.js!
我们试图用技术去弥补产品逻辑上的懒惰!(非常有触感😖)
因为产品经理懒得思考信息的层级 ,所以他把所有信息一股脑扔给前端,让你去搞懒加载。
技术不是万能的。
浏览器的渲染能力是有上限的,JS的主线程是单核的,移动端的电量是有限的。更重要的是,用户的注意力是极其有限的。
当你发现你需要用极其复杂的新技术才能勉强让一个页面跑起来的时候
请停下来!

这时候,问题的根源通常不在代码里,而可能是在 PRD(需求文档) 里。
说了那么多,该怎么做呢?
下次,当你再面对一个导致卡顿的需求时,别急着打开Profiler分析性能。
请试着做以下几步:
我们真的需要在前端处理10万条数据吗?能不能在后端聚合好,只给我返回结果?
这个图表真的需要实时刷新吗?用户真的能看清1毫秒的变化吗?改成5秒刷新一次行不行?
在这个弹窗里塞个完整地图太卡了。能不能改成:点击缩略图,跳转到专门的地图页面?
你要告诉产品经理: 性能本身,也是一个产品功能。
如果为了塞下更多的功能,牺牲了流畅度这个最核心的功能,那是丢了西瓜捡芝麻。
最好的代码,是 没有代码(No Code)。
同理,最好的性能优化,是没有需求。
作为高级工程师,你的价值不仅仅体现在你会写Virtual List,更体现在你敢不敢在需求评审会上,拍着桌子说:
这个设计怎么这么反人类😠!我们能不能换个更好的方式?🤷♂️
别再给屎山💩雕花了。把那座山推了,才是真正的优化。
关于这个观点你们怎么看?
来源:juejin.cn/post/7573950036897038376
别再滥用 Base64 了——Blob 才是前端减负的正确姿势
一、什么是 Blob?
Blob(Binary Large Object,二进制大对象)是浏览器提供的一种不可变、类文件的原始数据容器。它可以存储任意类型的二进制或文本数据,例如图片、音频、PDF、甚至一段纯文本。与 File 对象相比,Blob 更底层,File 实际上继承自 Blob,并额外携带了 name、lastModified 等元信息 。
Blob 最大的特点是纯客户端、零网络:数据一旦进入 Blob,就活在内存里,无需上传服务器即可预览、下载或进一步加工。
二、构造一个 Blob:一行代码搞定
const blob = new Blob(parts, options);
| 参数 | 说明 |
|---|---|
parts | 数组,元素可以是 String、ArrayBuffer、TypedArray、Blob 等。 |
options | 可选对象,常用字段:type MIME 类型,默认 application/octet-stream;endings 是否转换换行符,几乎不用。 |
示例:动态生成一个 Markdown 文件并让用户下载
const content = '# Hello Blob\n> 由浏览器动态生成';
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'hello.md';
a.click();
// 内存用完即弃
URL.revokeObjectURL(url);
三、Blob URL:给内存中的数据一个“临时地址”
1. 生成方式
const url = URL.createObjectURL(blob);
// 返回值样例
// blob:https://localhost:3000/550e8400-e29b-41d4-a716-446655440000
2. 生命周期
- 作用域:仅在当前文档、当前会话有效;页面刷新、
close()、手动调用revokeObjectURL()都会使其失效 。 - 性能陷阱:不主动释放会造成内存泄漏,尤其在单页应用或大量图片预览场景 。
最佳实践封装:
function createTempURL(blob) {
const url = URL.createObjectURL(blob);
// 自动 revoke,避免忘记
requestIdleCallback(() => URL.revokeObjectURL(url));
return url;
}
四、Blob vs. Base64 vs. ArrayBuffer:如何选型?
| 场景 | 推荐格式 | 理由 |
|---|---|---|
图片回显、<img>/<video> | Blob URL | 浏览器可直接解析,无需解码;内存占用低。 |
| 小图标内嵌在 CSS/JSON | Base64 | 减少一次 HTTP 请求,但体积增大约 33%。 |
| 纯计算、WebAssembly 传递 | ArrayBuffer | 可写、可索引,适合高效运算。 |
| 上传大文件、断点续传 | Blob.slice | 流式分片,配合 File.prototype.slice 做断点续传 。 |
五、高频实战场景
1. 本地图片/视频预览(零上传)
<input type="file" accept="image/*" id="uploader">
<img id="preview" style="max-width: 100%">
<script>
uploader.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
preview.src = url;
preview.onload = () => URL.revokeObjectURL(url); // 加载完即释放
};
</script>
2. 将 Canvas 绘图导出为 PNG 并下载
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'snapshot.png';
a.click();
URL.revokeObjectURL(url);
}, 'image/png');
3. 抓取远程图片→Blob→本地预览(跨域需 CORS)
fetch('https://i.imgur.com/xxx.png', { mode: 'cors' })
.then(r => r.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.querySelector('img').src = url;
});
若出现图片不显示,99% 是因为服务端未返回 Access-Control-Allow-Origin 头 。
六、踩坑指南与性能锦囊
| 坑点 | 解决方案 |
|---|---|
| 内存暴涨 | 每次 createObjectURL 后,务必在合适的时机 revokeObjectURL 。 |
| 跨域失败 | 确认服务端开启 CORS;fetch 时加 {credentials: 'include'} 如需 Cookie。 |
| 移动端大视频卡顿 | 避免一次性读完整文件,使用 blob.slice(start, end) 分段读取。 |
| 旧浏览器兼容 | IE10+ 才原生支持 Blob;如需更低版本,请引入 Blob.js 兼容库。 |
七、延伸:Blob 与 Stream 的梦幻联动
当文件超大(GB 级)时,全部读进内存并不现实。可以借助 ReadableStream 把 Blob 转为流,实现渐进式上传:
const stream = blob.stream(); // 返回 ReadableStream
await fetch('/upload', {
method: 'POST',
body: stream,
headers: { 'Content-Type': blob.type }
});
Chrome 85+、Edge 85+、Firefox 已经支持 blob.stream(),能以流式形式边读边传,内存占用极低。
八、总结:记住“三句话”
- Blob = 浏览器端的二进制数据仓库,File 只是它的超集。
- Blob URL = 指向内存的临时指针,用完后必须手动或自动释放。
- 凡是“本地预览、零上传、动态生成下载”的需求,优先考虑 Blob + Blob URL 组合。
用好 Blob,既能提升用户体验(秒开预览),又能降低服务端压力(无需中转),是每一位前端工程师的必备技能。
来源:juejin.cn/post/7573521516324896795
亲历外企裁员:上午还在写代码,下午工位就空了

引子:不再是“主动的选择”
记得 2018 年的时候,年轻与互联网的兴起,只要简历挂在求职 APP 上,立马就会有猎头或者 HR 来联系。在那时,离职通常伴随着的是薪资增长和职级的跃升。
那时候的我们,仿佛是在草原上逐水草而居的游牧民族,哪里水草丰美就去哪里,从未想过草原也会有枯黄的一天。
然而今天,我第一次以旁观者的身份,见证了一场并非出于自愿的离别。当大环境不再安定、当时代的红利退潮,裸泳的不仅是企业,还有每一个身处其中的个体。
现场:两小时的“消失术”
早在一个月前,空气中就已经弥漫着不安的味道。或者说早在 HC(Headcount)冻结时,就已经埋下了伏笔。
11月的时候,一些原本该续签的合同被搁置,那时候我们就知道,暴风雨要来了。但我没想到,它来得如此迅猛且安静。
早上 9 点,无意间瞥见 Leader 们聚在一间会议室中开会。原以为是他们的例会,没曾想会是裁员的开始键:
Leader 谈话 -> 确认赔偿 -> 签字 -> 交还电脑 -> 离开。
这场“斩首行动”持续了不到两个小时,迅速且安静。整个过程持续到了上午 11 点,尘埃落定。整个部门合计少了四分之一的人。
外企的体面在这一刻展现得淋漓尽致,同时也冷酷得令人心惊。没有多余的废话,只有流程和结果。前一分钟还在和我说要修 CI 错误的同事,后一分钟工位就已经回来和我们告别了。

午餐:焦虑的泡沫
中午,“幸存”下来的同事不约而同地一起去吃饭。
话题不再是往日的“最近哪个 AI 技术栈很火”、“哪个 Node.js 的库可以用到项目里”,而是变成了“现在出去了能做什么”、“还会有下一波吗”。
谈到赔偿,由于不方便透露,只能算是“中规中矩”。即不会像佳能那样上新闻,也不会闹到仲裁。
在六七年前,这笔钱可能是一笔快乐的旅游基金;而在当下的环境中,它更像是一笔“过冬费”,是面对未知的漫长寒冬时,手里仅有的一点余粮。
吃完饭后,大家也是心照不宣地散着步。专门挑了太阳最大的地方走了好久,仿佛可以驱散身上的寒气。
但我们都清楚,我们这些留下来的人,也没有太多庆幸。更多的是一种“兔死狐悲”的无力感。谁也不知道,下一次名单上会不会有自己的名字。
下午:沉默的办公室
回到工位,工作还得继续。代码还在那里,Bug 还没修完,PR 还等着 Merge。
但当看到 PR 中那些点了 Approve 或者留下 Comments 的“前”同事的头像,竟有一丝不想 Merge 的冲动。
发生了这么大的事情后,办公室的氛围自然变了。往常到了下班点,大家可能还会为了赶进度再多待一会儿或者一起聊会天。但今天,一到时间,Leader 们就示意大家早点回去,不要再加班了。
这不仅是一种体恤,更像是一种无声的宣告:当“努力”已经无法对抗“趋势”时,大家默契地选择了“节能模式”。

结语
古人云:“山雨欲来风满楼”。今天的这场裁员,或许只是时代大潮中的一朵浪花。
外企曾经是我们心中的避风港,意味着高薪、体面、Work-Life Balance。但今天的一切告诉我们,在这个充满不确定性的时代,没有绝对的安全岛。
对于我们每一个技术人来说,或许是时候重新审视自己的核心竞争力了。当大潮退去,我们手里握着的,究竟是可以随时变现的技能,还是一张随时可能失效的工牌?
最后的最后,我想说我们组本来人也不多,大家关系也都很好,平时说说笑笑也很开心。衷心祝愿他们能在后面的日子顺利。
来源:juejin.cn/post/7582048028393144370
让网页在 PC 缩放时“纹丝不动”的 4 个技巧
记录一次把「标题、描述、背景图」全部做成“流体响应式”的踩坑与经验
背景
最近给 LUCI OS 官网做首屏改版,需求只有一句话:
“PC 端浏览器随意缩放,首屏内容要像海报一样,几乎看不出形变。”
听起来简单,但「缩放不变形」+「多端自适应」本质上是矛盾的。
经过 3 轮迭代,我们把问题拆成了 4 个小目标,并给出了最简洁的解法。
1. 文本:用 clamp() 一把梭
传统写法给 3~4 个断点写死字号,窗口稍微拉一下就会跳变。
CSS 4 级函数 clamp(MIN, VAL, MAX) 天生就是解决“跳变”的:
- 标题:
text-[clamp(28px,6vw,48px)] - 描述:
text-[clamp(14px,1.2vw,18px)]
一行代码实现「最小值保底、最大值封顶、中间平滑变化」。
浏览器缩放时,字号随 vw 线性变化,肉眼几乎察觉不到阶梯感。
2. 容器:限宽 + 居中 = “锁死”水平形变
再漂亮的字号,如果容器宽度跟着窗口无限拉伸,一样会崩。
做法简单粗暴:
css
复制
max-w-6xl mx-auto
max-w-6xl把最大内容宽度锁死在 1152px;mx-auto保证左右留白始终对称。
窗口继续拉大,两侧只是等比留空,内容区不再变形。
3. 图片(或背景):固定尺寸 + 背景定位
背景图不能跟着 100% 拉伸,否则人物/产品会被拉长。
我们把背景拆成两层:
- 外层:全屏
div,只做黑色渐变遮罩; - 内层:真正的背景图用
css
复制
background: url(...) 50% / cover no-repeat;
max-width: 1280px;
max-height: 800px;
只要窗口没超过 1280×800,背景图始终保持原始比例,居中裁剪。
4. 布局:断点内“锁死”,断点外才变化
Tailwind 的 md:flex-row 之类前缀只在跨断点时生效。
在 同一断点内 我们故意:
- 用固定
gap-32px而非百分比; - 用固定图片宽
md:w-75高md:h-47; - 用
items-center保证垂直居中。
=> 浏览器宽一点点、窄一点点,所有尺寸都不变,自然看不出变化。
直到窗口拉到下一个断点阈值,布局一次切换,干净利落。
最终代码(最简可读版)
tsx
复制
<section className="relative flex items-center justify-center min-h-[400px] md:h-[800px]">
{/* 1. 背景层:固定尺寸 + 居中 */}
<div
className="absolute inset-0 mx-auto"
style={{
maxWidth: 1280,
maxHeight: 800,
background:
'linear-gradient(180deg,rgba(2,2,2,0) 60%,#020202 99%), url(/unlocking_vast_data_potential.png) 50%/cover no-repeat',
}}
/>
{/* 2. 内容层:限宽 + 居中 + clamp */}
<div className="relative z-10 w-full max-w-6xl px-4 text-center">
<h1 className="font-bold text-white text-[clamp(28px,6vw,48px)]">
Unlocking Vast Data Potential
</h1>
<p className="mt-4 mx-auto max-w-5xl text-[clamp(14px,1.2vw,18px)] text-[#8C8B95]">
LUCI OS is powered by Mavi's video understanding engine …
</p>
</div>
</section>
效果
- 1440px 与 1920px 两档分辨率下,标题、描述、背景图的视觉差异 < 2% ;
- 字号、行宽、图片比例在鼠标拖拽窗口时线性变化,无跳变;
- 移动端仍保持完美自适应,无需额外代码。
写在最后
把「响应式」做细,核心就是 “在需要的范围内平滑,在不需要的范围内锁死”。
希望这 4 个小技巧也能帮你把“缩放不变形”真正落地。
来源:juejin.cn/post/7540939051195056143
如果产品经理突然要你做一个像抖音一样流畅的H5
从前端到爆点!抖音级 H5 如何炼成?
在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。
一、先看清本质:抖音 H5 为何丝滑?
抖音 H5 之所以让人欲罢不能,核心在于两点:极低的卡顿率和极致的交互反馈。前者靠性能优化,后者靠精心设计的交互逻辑。比如,你刷视频时的流畅下拉、点赞时的爱心飞舞,背后都藏着前端开发的“小心机”。
二、性能优化:让页面飞起来
(一)懒加载与预加载协同作战
懒加载是 H5 性能优化的经典招式,只在用户即将看到某个元素时才加载它。但光靠懒加载还不够,聪明的抖音 H5 还会预加载下一个可能进入视野的元素。以下是一个基于 IntersectionObserver 的懒加载示例:
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
if ('IntersectionObserver' in window) {
let lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazy Images.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
}
});
(二)图片压缩技术大显神威
图片是 H5 的“体重”大户。抖音 H5 常用 WebP 格式,它在保证画质的同时,能将图片体积压缩到 JPEG 的一半。你可以用以下代码轻松实现图片格式转换:
function compressImage(inputImage, quality) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = inputImage.naturalWidth;
canvas.height = inputImage.naturalHeight;
ctx.drawImage(inputImage, 0, 0, canvas.width, canvas.height);
const compressedImage = new Image();
compressedImage.src = canvas.toDataURL('image/webp', quality);
compressedImage.onload = () => {
resolve(compressedImage);
};
});
}
三、交互设计:让用户欲罢不能
(一)微动画营造沉浸感
在点赞、评论等关键操作上,抖音 H5 会加入精巧的微动画。比如点赞时的爱心从手指位置飞出,这其实是一个 CSS 动画加 JavaScript 事件监听的组合拳。以下是一个简易版的点赞动画代码:
@keyframes flyHeart {
0% {
transform: scale(0) translateY(0);
opacity: 0;
}
50% {
transform: scale(1.5) translateY(-10px);
opacity: 1;
}
100% {
transform: scale(1) translateY(-20px);
opacity: 0;
}
}
.heart {
position: fixed;
width: 30px;
height: 30px;
background-image: url('../assets/heart.png');
background-size: contain;
background-repeat: no-repeat;
animation: flyHeart 1s ease-out;
}
document.querySelector('.like-btn').addEventListener('click', function(e) {
const heart = document.createElement('div');
heart.className = 'heart';
heart.style.left = e.clientX + 'px';
heart.style.top = e.clientY + 'px';
document.body.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
});
(二)触摸事件优化
在移动设备上,触摸事件的响应速度直接影响用户体验。抖音 H5 通过精准控制触摸事件的捕获和冒泡阶段,减少了延迟。以下是一个优化触摸事件的示例:
const touchStartHandler = (e) => {
e.preventDefault(); // 防止页面滚动干扰
// 处理触摸开始逻辑
};
const touchMoveHandler = (e) => {
// 处理触摸移动逻辑
};
const touchEndHandler = (e) => {
// 处理触摸结束逻辑
};
const element = document.querySelector('.scrollable-container');
element.addEventListener('touchstart', touchStartHandler, { passive: false });
element.addEventListener('touchmove', touchMoveHandler, { passive: false });
element.addEventListener('touchend', touchEndHandler);
四、音频处理:让声音为 H5 增色
抖音 H5 的音频体验也很讲究。它会根据用户的操作实时调整音量,甚至在不同视频切换时平滑过渡音频。以下是一个简单的声音控制示例:
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioElement = document.querySelector('audio');
const audioSource = audioContext.createMediaElementSource(audioElement);
const gainNode = audioContext.createGain();
audioSource.connect(gainNode);
gainNode.connect(audioContext.destination);
// 调节音量
function setVolume(level) {
gainNode.gain.value = level;
}
// 音频淡入效果
function fadeInAudio() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 1);
}
// 音频淡出效果
function fadeOutAudio() {
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
}
五、跨浏览器兼容:让 H5 无处不在
抖音 H5 能在各种浏览器上保持一致的体验,这离不开前端开发者的兼容性优化。常用的手段包括使用 Autoprefixer 自动生成浏览器前缀、为老浏览器提供 Polyfill 等。以下是一个为 CSS 动画添加前缀的示例:
const autoprefixer = require('autoprefixer');
const postcss = require('postcss');
const css = '.example { animation: slidein 2s; } @keyframes slidein { from { transform: translateX(0); } to { transform: translateX(100px); } }';
postcss([autoprefixer]).process(css).then(result => {
console.log(result.css);
/*
输出:
.example {
animation: slidein 2s;
}
@keyframes slidein {
from {
-webkit-transform: translateX(0);
transform: translateX(0);
}
to {
-webkit-transform: translateX(100px);
transform: translateX(100px);
}
}
*/
});
打造一个像抖音一样的流畅 H5,需要前端开发者在性能优化、交互设计、音频处理和跨浏览器兼容等方面全方位发力。希望这些技术点能为你的 H5 开发之旅提供助力,让你的产品在激烈的市场竞争中脱颖而出!
来源:juejin.cn/post/7522090635908251686
前端开发人员:以下是如何充分利用 Cursor😍😍😍
前言
我相信作为程序员Cuesor大家一定都很熟悉了,它是一款代码编辑工具,位于 Claude AI、o3、Gemini-3.5-Pro 和 GPT-4.1 等其他顶级 AI 模型之上,帮助我们提高工作效率以及体验。
Cursor AI 入门
导航到 cursor.com 时,可以下载 IDE:

下载并打开代码编辑器后,你将看到以下内容:

在上面的界面中,我们观察到一些事情:
- 代理模式 每当编码时,您都需要使用代理模式。它有助于处理端到端任务
- Ask 如果单击代理模式下拉列表:

你会看到“提问”选项——当你想提出问题时使用此选项,就像使用 ChatGPT 一样。
- Model selection( 模型选择 )
此下拉列表显示要从中选择的最喜欢的模型列表 - Context integration(上下文集成) “添加上下文”按钮,用于使用 @ 符号(@docs、@web 等)
- Chat interface(聊天界面)
“新聊天”,用于开始新的或后续的对话。
如何利用这些功能
传统的 Web 开发项目遵循以下工作流程:

但是在进行 vibe 编码时,您的工作流程将看起来更像这样:

而cursor可以帮你完成设计这一步骤,使用 Cursor AI 生成界面 。首先创建一个新项目;我们将在本例中使用 Next.js。

使用快捷键 Command+K,这非常适合您忘记基本命令

现在我们已经安装了项目,是时候进行提示了。
构建示例项目
使用聊天进行设计提示
导航到新建聊天 。
选择光标代理 ,然后选择您喜欢的模型。对我来说,我更喜欢 Claude 3.5

现在你可以提示它了。这是我的提示的样子:“在我的 page.tsx 中,重新创建 Logrocket 的登录页面。”
这是我们得到的:

接受更改,并使用 npm run dev 运行应用程序
如果生成的和我们所设想的效果不太一样,我们可以通过多种方式使用图像上下文使其更接近。
Attaching an image 附加图像
导航到聊天,单击图像图标,然后附加图像。这应该是您想要复制的设计图像。来不断迭代来达到我们想要的效果。
在迭代时,可能会有时ai误解了我们的意思,或者Cursor 发现这个问题有点难以修复。在这种情况下,我们可以通过单击恢复检查点轻松恢复到以前的设计。这可以在之前的聊天下找到:

模型上下文协议 (MCP) 服务器
MCP 是一种开放标准,使开发人员能够在其数据源和 AI 驱动的工具之间构建安全的双向连接。
Framelink.ai 为 Figma 构建了一个 MCP 服务器,它允许您直接访问和处理 Cursor 中的设计文件。按照本指南轻松设置。
充分利用 Cursor 的更多策略
利用 AI 代理模式
使用光标时,您最常使用 AI 代理模式。这是一个强大的功能,可以:
- 自动安装依赖项(不再需要手动包管理)
- 在整个项目中创建和修改文件
- 在确认后运行终端命令
- 处理复杂的多文件重构
- 端到端实现完整功能
使用 @ 符号进行上下文管理
了解上下文管理可以提升您使用 Cursor 的编码体验。 @ 符号是您在帮助您时告诉 Cursor 要查看什么或优先考虑什么的方式。主要的 @ 类型是:
- @code – 您的整个项目
- @web – 搜索互联网
- @docs – 该工具或库的文档甚至可以是一个框架
- @files 和文件夹 – 特定文件
- 图像 – 拖放屏幕截图/设计
- @cursor 规则 – 甚至您过去的聊天记录
“新聊天”策略
根据经验,我建议您始终为新功能或项目开始新的聊天。当您在旧对话之上提示某些内容时,可能会分散 AI 的注意力并降低响应质量。
Cursor 添加了一项有用的功能,允许您使用旧聊天的摘要开始新的聊天,为您提供两全其美的体验 - 新鲜的上下文而不会丢失重要信息。
使用上面的 “添加 ”按钮创建新聊天,单击上下文选项的 @ ,滚动以选择 “最近的更改”, 单击它,然后继续提示:

总结
Cursor AI 的定价似乎偏高。但其功能确实还行,不过我们也可以选择trae来替代,毕竟Cursor也不比trae强多少
来源:juejin.cn/post/7545725771315478554
脱裤子放屁 - 你们讨厌这样的页面吗?
前言
平时在逛掘金和少数派等网站的时候,经常有跳转外链的场景,此时基本都会被中转到一个官方提供的提示页面。
掘金:

知乎:

少数派:

这种官方脱裤子放屁的行为实在令人恼火。是、是、是、我当然知道这么做有很多冠冕堂皇的理由,比如:
- 防止钓鱼攻击
- 增强用户意识
- 品牌保护
- 遵守法律法规
- 控制流量去向
(以上5点是 AI 告诉我的理由)
但是作为混迹多年的互联网用户,什么链接可以点,什么最好不要点(悄悄的点) 我还是具备判断能力的。
互联网的本质就是自由穿梭,一个 A 标签就可以让你在整个互联网翱翔,现在你每次起飞的时候都被摁住强迫你阅读一次免责声明,多少是有点恼火的。
解决方案
这些中转站的实现逻辑基本都是将目标地址挂在中转地址的target 参数后面,在中转站做免责声明,然后点击继续跳转才跳到目标网站。
掘金:
少数派:
https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F
知乎:
https://link.zhihu.com/?target=https%3A//asciidoctor.org/
所以我们就可以写一个浏览器插件,在这些网站中,找出命中外链的 A 标签,替换掉它的 href 属性(只保留 target 后面的真实目标地址)。
核心函数:
function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}
为此我创建了一个项目仓库 redirect-skipper ,并且将该浏览器插件发布在谷歌商店了 安装地址 。
安装并启用这个浏览器插件之后,在这些网站中点击外链就不会看到中转页面了,而是直接跳转到目标网站。
因为我目前明确需要修改的就是这几个网站,如果大家愿意使用这个插件,且有其他网站需要添加到替换列表的,可以给 redirect-skipper 仓库 提PR。
如果需要添加的网站的转换规则是和 findByTarget 一致的,那么仅需更新 sites.json 文件即可。
如果需要添加的网站的转换规则是独立的,那么需要更新插件代码,合并之后,由我向谷歌商店发起更新。
为了后期可以灵活更新配置(谷歌商店审核太慢了),我默认将插件应用于所有网站,然后在代码里通过 hostname 来判断是否真的需要执行。
{
"$schema": "https://json.schemastore.org/chrome-manifest.json",
"name": "redirect-skipper",
"manifest_version": 3,
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./scripts/redirect-skipper.js"],
"run_at": "document_end"
}
],
}
在当前仓库里维护一份 sites.json 的配置表,格式如下:
{
"description": "远程配置可以开启 Redirect-Skipper 插件的网站 (因为谷歌商店审核太慢了,否则无需通过远程配置,增加复杂性)",
"sites": [
{
"hostname": "juejin.cn",
"title": "掘金"
},
{
"hostname": "sspai.com",
"title": "少数派"
},
{
"hostname": "www.zhihu.com",
"title": "知乎"
}
]
}
这样插件在拉取到这份数据的时候,就可以根据这边描述的网站配置,决定是否执行具体代码。
插件完整代码:
function replaceALinks() {
findByTarget();
}
function observerDocument() {
const mb = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
if (mutation.addedNodes.length) {
replaceALinks();
}
}
}
});
mb.observe(document, { childList: true, subtree: true });
}
// 监听路由等事件
["hashchange", "popstate", "load"].forEach((event) => {
window.addEventListener(event, async () => {
replaceALinks();
if (event === "load") {
observerDocument();
await updateHostnames();
replaceALinks(); // 更新完数据后再执行一次
}
});
});
let hostnames = ["juejin.cn", "sspai.com", "www.zhihu.com"];
function updateHostnames() {
return fetch(
"https://raw.githubusercontent.com/dogodo-cc/redirect-skipper/master/sites.json"
)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok");
})
.then((data) => {
// 如果拉到了远程数据,就用远程的
hostnames = data.sites.map((site) => {
return site.hostname;
});
})
.catch((error) => {
console.error(error);
});
}
// 符合 '?target=' 格式的链接
// https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.apple.com%2Fcn%2Fdesign%2Fhuman-interface-guidelines%2Fapp-icons%23macOS/
// https://sspai.com/link?target=https%3A%2F%2Fgeoguess.games%2F
// https://link.zhihu.com/?target=https%3A//asciidoctor.org/
function findByTarget() {
if (!hostnames.includes(location.hostname)) return;
const linkKeyword = "?target=";
const aLinks = document.querySelectorAll(
`a[href*="${linkKeyword}"]:not([data-redirect-skipper])`
);
if (!aLinks) return;
aLinks.forEach((a) => {
const href = a.href;
const targetIndex = href.indexOf(linkKeyword);
if (targetIndex !== -1) {
const newHref = href.substring(targetIndex + linkKeyword.length);
a.href = decodeURIComponent(newHref);
a.setAttribute("data-redirect-skipper", "true");
}
});
}
更详细的流程可以查看 redirect-skipper 仓库地址
夹带私货
标题历史
- 浏览器插件之《跳过第三方链接的提示中转页》
来源:juejin.cn/post/7495977411273490447
当了leader才发现,大厂最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这3句话挂在嘴边的
“当了 leader 才发现,公司最想裁掉的,不是上班总迟到的,也不是下班搞失联的,而是经常把这 3 句话挂在嘴边的”
这是最近在职场社区里又被聊热起来的一个老话题。
作为一个在职场上混迹了近 9 年的程序员,一路走来亲眼目睹和经历了程序员职场里的各种风雨。从一开始的大头兵到后来负责一个独立的小团队,从一个所谓的 leader 的视角上来看问题,对这个事情的理解似乎又有了一些变化。
在我刚成为小团队负责人时,和许多职场人一样,我以为那些显而易见的职业毛病——比如习惯性迟到、到点就消失联系不上——才是最让管理者头疼的。直到自己坐上这个位置,真实去参与招人、考核、决策,才明白有些表象问题在 leader 眼中其实根本不算什么。
回到文首的话题,说的似乎有些夸张,但在真实的职场里确实是存在的,工作能力当然重要,但是“沟通艺术”和“向上管理”同样也不容忽视。
今天,我们就来聊聊这 3 句看似平常却极具“杀伤力”的话,相信我们平时可能也听过。
1、“这个需求做不了”
这句话相信在平时的需求会或者评审会上经常会听到。
当接到一个新需求或任务时,有些同学的第一反应是:“这个需求做不了”。
当员工说出“这个需求做不了”时,背后的含义有可能是“我不想做”、“我不会做”或“我觉得没必要做”。无论是哪种情况,传递出来的都是拒绝和封闭的态度。
因为现代职场没有绝对“做不了”的事情,只有尚未找到的解决方案或者不够充分的资源支持而已。
如果面对平级的产品经理这样说倒还无所谓,如果面对大领导这样表达那就多少有些欠妥了。
所以遇到这类问题大可不必当场上头去否定,别急着说‘做不了’,我们不妨换一个方式来表达。
表达变换 Tips:
- “这个需求很有挑战,我需要先调研看看”
- “这个需求很有挑战,我需要XX资源和XX支持来实现它”
- “目前有几点困难,我需要先评估一下”
- “我需要XX资源和XX条件的支持,能否帮我协调”
2、“我以前都是这么做的”
当员工不断强调“以前都是这么做的”,他实际上是在拒绝创新,拒绝适应变化,拒绝接受新思想。
在 IT 互联网行业,方法和工具迭代速度极快。之前曾经高效的方法,现在可能已经落后了,而对过往的经验和标签的一味固守往往会阻碍人进行第二层、第三层的进一步思考。
所以面对这种表达情形,我们不妨也可以换一种表达方式。
表达变换 Tips:
- “我过去是这样做的,但不确定是否适合现在的情况,需要先确定一下”
- “过去我们是用XX方法做的,效果是XX。我们也可以探讨一下新方法,看看会不会更好”
- “我能分享一下过去做这类项目的经验教训,供大家参考”
- “我对这个新方法还不太熟,需要一点时间学习一下”
3、“这个不归我负责”
我认为这句话可能是这三句中最致命的一句,因为它通常反映的是一种界限感和团队协作意识的缺失。
“各人自扫门前雪”的心态在某些特殊的场景下确实“杀伤力”极大,尤其如果在上级领导问责面前这么说,那基本真就 gg 了。。
在团队工作中,职责边界清晰本是好事,但过度强调“不归我管”则是一种危险的信号。
而且有时候模糊地带的问题往往就是自己能创造价值的机会,而且即便自己不想插手,也完全没必要这样去表达,我们不妨也换一些表达方式。
表达变换 Tips:
- “这个事情谁更熟悉?我可以协助”
- “这个事情的负责人是xxx,我可以了解之后协助解决”
- “我不太熟悉这个领域,但可以了解一下,谁能给我指点”
- “我们需要先明确一下这个问题的负责人,我可以暂时跟进一下”
所以以上这三点沟通的艺术也是程序员职场生活里切实可能会遇到的问题。当你下次准备说出这三句话中的任何一句时,不妨先停顿三秒,换个表达方式。
虽说向上管理和沟通艺术是被很多程序员所诟病和瞧不上的,但是有时候因为一句上头的话或者表达而导致了某些通道的关闭那也实在是太可惜了,这很不公平,但这往往就是职场的现实。
程序员作为一个有个性的创造性群体要专注精进技术这本身没错,但是职场毕竟也是一个充满人情世故的江湖,掌握一些通用的职场规则和沟通艺术也是十分有必要的,所以埋头赶路的同时也不要忘记看看周围的环境和机会。
在这个快速变化的时代,唯一不变的就是需要不停地适应和成长。共勉。
好了,那以上就是今天的内容分享了,希望能对大家有所启发,我们下篇见。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7551254626882338826
我本是写react的,公司让我换赛道搞web3D
当你在会议室里争论需求时,
智慧工厂的数字孪生正同步着每一条产线的脉搏;
当你对着平面图想象空间时,
智慧小区的三维模型已在虚拟世界精准复刻每一扇窗的采光。
当你在CAD里调整参数时,
数字孪生城市的交通流正实时映射每辆车的轨迹;
当你等待客户确认方案时,
机械臂的3D仿真已预演了十万次零误差的运动路径;
当你用二维图纸解释传动原理时,
可交互的3D引擎正让客户‘拆解’每一个齿轮;
当你担心售后维修难描述时,
AR里的动态指引已覆盖所有故障点;
当你用PS拼贴效果图时,
VR漫游的业主正‘推开’你设计的每一扇门;
当你纠结墙面材质时,
光影引擎已算出了午后3点最温柔的折射角度;
从前端到Web3D,
不是换条赛道,
而是打开新维度。
韩老师说过:再牛的程序员都是从小白开始,既然开始了,就全心投入学好技术。
🔴 工具
所有的api都可以通过threejs官网的document,切成中文,去搜:

🔴 平面
⭕️ Scene 场景
场景能够让你在什么地方、摆放什么东西来交给three.js来渲染,这是你放置物体、灯光和摄像机的地方。

import * as THREE from "three";
// console.log(THREE);
// 目标:了解three.js最基本的内容
// 1、创建场景
const scene = new THREE.Scene();
⭕️ camera 相机

import * as THREE from "three";
// console.log(THREE);
// 目标:了解three.js最基本的内容
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);
// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中
⭕️ 物体 cube
import * as THREE from "three";
// console.log(THREE);
// 目标:了解three.js最基本的内容
// 1、创建场景
const scene = new THREE.Scene();
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75, // 相机的角度
window.innerWidth / window.innerHeight, // 相机的宽高比
0.1, // 相机的近截面
1000 // 相机的远截面
);
// 设置相机位置
camera.position.set(0, 0, 10); // 相机位置 (X轴坐标, Y轴坐标, Z轴坐标)
scene.add(camera); // 相机添加到场景中
// 添加物体
// 创建几何体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); // 创建立方体的几何体 (长, 宽, 高)
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); // MeshBasicMaterial 基础网格材质 ({ color: 0xffff00 }) 颜色
// 根据几何体和材质创建物体
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); // 创建立方体的物体 (几何体, 材质)
// 将几何体添加到场景中
scene.add(cube); // 物体添加到场景中
⭕️ 渲染 render
// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染的尺寸大小 (窗口宽度, 窗口高度)
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement); // 将webgl渲染的canvas内容添加到body
// 使用渲染器,通过相机将场景渲染进来
renderer.render(scene, camera); // 使用渲染器,通过相机将场景渲染进来 (场景, 相机)
⭕️ 效果
效果是平面的:

到这里,还不是3d的,如果要加3d,要加一下控制器。
🔴 3d
⭕️ 控制器
添加轨道。像卫星☄围绕地球🌏,环绕查看的视角:
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 目标:使用控制器查看3d物体
// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement); // 创建轨道控制器 (相机, 渲染器dom元素)
controls.enableDamping = true; // 设置控制器阻尼,让控制器更有真实效果。
function render() {
renderer.render(scene, camera); // 浏览器每渲染一帧,就重新渲染一次
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render); // 浏览器渲染下一帧的时候就会执行render函数,执行完会再次调用render函数,形成循环,每秒60次
}
render();
⭕️ 加坐标轴辅助器
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5); // 坐标轴(size轴的大小)
scene.add(axesHelper);

⭕️ 设置物体移动
// 设置相机位置
camera.position.set(0, 0, 10);
scene.add(camera);

cube.position.x = 3;
// 往返移动
function render() {
cube.position.x += 0.01;
if (cube.position.x > 5) {
cube.position.x = 0;
}
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
⭕️ 缩放
cube.scale.set(3, 2, 1); // xyz, x3倍, y2倍
单独设置
cube.position.x = 3;
⭕️ 旋转
cube.rotation.set(Math.PI / 4, 0, 0, "XZY"); // x轴旋转45度
单独设置
cube.rotation.x = Math.PI / 4;
⭕️ requestAnimationFrame
function render(time) {
// console.log(time);
// cube.position.x += 0.01;
// cube.rotation.x += 0.01;
// time 是一个不断递增的数字,代表当前的时间
let t = (time / 1000) % 5; // 为什么求余数,物体移动的距离就是t,物体移动的距离是0-5,所以求余数
cube.position.x = t * 1; // 0-5秒,物体移动0-5距离
// if (cube.position.x > 5) {
// cube.position.x = 0;
// }
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
⭕️ Clock 跟踪事件处理动画
// 设置时钟
const clock = new THREE.Clock();
function render() {
// 获取时钟运行的总时长
let time = clock.getElapsedTime();
console.log("时钟运行总时长:", time);
// let deltaTime = clock.getDelta();
// console.log("两次获取时间的间隔时间:", deltaTime);
let t = time % 5;
cube.position.x = t * 1;
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
大概是8毫秒一次渲染时间.
⭕️ 不用算 用 Gsap动画库
// 导入动画库
import gsap from "gsap";
// 设置动画
var animate1 = gsap.to(cube.position, {
x: 5,
duration: 5,
ease: "power1.inOut", // 动画属性
// 设置重复的次数,无限次循环-1
repeat: -1,
// 往返运动
yoyo: true,
// delay,延迟2秒运动
delay: 2,
onComplete: () => {
console.log("动画完成");
},
onStart: () => {
console.log("动画开始");
},
});
gsap.to(cube.rotation, { x: 2 * Math.PI, duration: 5, ease: "power1.inOut" });
// 双击停止和恢复运动
window.addEventListener("dblclick", () => {
// console.log(animate1);
if (animate1.isActive()) {
// 暂停
animate1.pause();
} else {
// 恢复
animate1.resume();
}
});
function render() {
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
⭕️ 根据尺寸变化 实现自适应
// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
⭕️ 用js控制画布 全屏 和 退出全屏
window.addEventListener("dblclick", () => {
const fullScreenElement = document.fullscreenElement;
if (!fullScreenElement) {
// 双击控制屏幕进入全屏,退出全屏
// 让画布对象全屏
renderer.domElement.requestFullscreen();
} else {
// 退出全屏,使用document对象
document.exitFullscreen();
}
// console.log(fullScreenElement);
});
⭕️ 应用 图形 用户界面 更改变量
// 导入dat.gui
import * as dat from "dat.gui";
const gui = new dat.GUI();
gui
.add(cube.position, "x")
.min(0)
.max(5)
.step(0.01)
.name("移动x轴")
.onChange((value) => {
console.log("值被修改:", value);
})
.onFinishChange((value) => {
console.log("完全停下来:", value);
});
// 修改物体的颜色
const params = {
color: "#ffff00",
fn: () => {
// 让立方体运动起来
gsap.to(cube.position, { x: 5, duration: 2, yoyo: true, repeat: -1 });
},
};
gui.addColor(params, "color").onChange((value) => {
console.log("值被修改:", value);
cube.material.color.set(value);
});
// 设置选项框
gui.add(cube, "visible").name("是否显示");
var folder = gui.addFolder("设置立方体");
folder.add(cube.material, "wireframe");
// 设置按钮点击触发某个事件
folder.add(params, "fn").name("立方体运动");

🔴 结语
前端的世界,
不该只有Vue和React——
还有WebGPU里等待你征服的星辰大海。"
“当WebGL成为下一代前端的基础设施,愿你是最早站在三维坐标系里的那个人。”
来源:juejin.cn/post/7517209356855164978
🔥 放弃 vw!我在官网大屏适配中踩了天坑,用 postcss-px-to-viewport-8-plugin 实现了 Rem 终极方案
引言:我的大屏适配“翻车”现场
领导拍板,1天内完成官网所有界面的响应式适配,我想了下,这还不简单?postcss-px-to-viewport 安排上,vw 单位一把梭!直到测试同事幽幽地说了句:‘这个屏… 4K 分辨率下好像被拉扁了?’
我心头一紧,打开 3840px 宽的显示器一看——所有图片、文字被无限拉宽,布局直接崩坏。vw 方案的致命缺陷暴露无遗:它只负责缩放,不负责限制最大宽度。 我们的内容在超过 1920px 的屏幕上经历了‘拉伸灾难’。必须寻找一个既能自动缩放,又能优雅限制最大宽度的 终极方案。
一、为什么 VW 不是大屏适配的银弹?
- vw 的本质:
1vw等于视口宽度的1%。视口越宽,元素尺寸越大。 - 理想的适配效果:
- 小于 1920px:等比例缩小。
- 等于 1920px:完美还原设计稿。
- 大于 1920px:内容不再无限放大,而是居中显示,两侧留白(类似
max-width: 1920px; margin: 0 auto;的效果)。
- vw 的困境:它无法实现第三点。在
3840px的 4K 屏上,一个100vw的元素会宽达3840px,远远超出设计预期,导致布局稀疏、元素被拉扁,体验极差。
二、终极方案的选型:REM 王者归来
我们的需求其实有两个:
- 动态缩放:在不同尺寸下,元素能等比缩放。
- 最大限制:有一个绝对单位作为基准,限制最大尺寸。
Rem (Root Em) 单位完美契合!
1rem等于根元素 (<html>) 的font-size大小。- 我们可以通过 JavaScript 动态计算并设置
<html>的font-size。 - 同时,我们可以用 CSS 媒体查询或 JS 逻辑,为
font-size设置一个 最大值,比如16px。这样,当屏幕宽超过1920px时,布局宽度就会稳定在1920px的对应尺寸,实现居中留白。
思路转变:从 px -> vw 变为 px -> rem。
三、核心实战:逆向工程与插件配置
我们的目标是:继续使用高效的 postcss-px-to-viewport-8-plugin 自动将设计稿的 px 转换为 rem,但要破解它的默认公式。
1. 插件的“固执”公式
该插件默认用于转换 vw,它有一个强制逻辑:
// 插件内部大概是这样计算的
function fixedTo(number, unitPrecision) {
// 公式: (px / viewportWidth) * 100
return (number / viewportWidth * 100).toFixed(unitPrecision) + 'vw';
}
我们要把输出单位改成 rem,但公式没变,它依然会套用 (px / viewportWidth) * 100。
2. 逆向计算,破解公式
我们的目标是:让 1920px 的设计稿上,1rem 恰好等于 16px。
- 设:设计稿上一个元素的宽度为
100px。 - 我们希望插件输出:
100px -> Y rem。 - 我们希望在实际
1920px宽的屏幕下:Y rem = 100px。 - 因为
1rem = 16px,所以Y = 100 / 16 = 6.25rem。
现在,我们反向推导插件内部的公式:
插件计算:Y = (100 / viewportWidth) * 100
让两个 Y 相等:
(100 / viewportWidth) * 100 = 100 / 16
两边同时除以 100:
100 / viewportWidth = 1 / 16
解得:
viewportWidth = 100 * 16 = 1600
结论: 将插件的 viewportWidth 设置为 1600,viewportUnit 设置为 rem,它就能输出符合我们需求的 rem 值!
3. 最终 PostCSS 配置
// nuxt.config.ts / vite.config.ts / postcss.config.js
export default {
// ... other config
postcss: {
plugins: [
require('postcss-px-to-viewport-8-plugin')({
// 【核心逆向配置】通过计算得出,让 1920px 设计稿下 1rem = 16px
viewportWidth: 1600, // 设计稿视口宽度(逆向计算值)
viewportHeight: 1080, // 设计稿视口高度(根据实际情况设置,主要用于高宽都固定的元素)
unitToConvert: 'px', // 要转换的单位
unitPrecision: 5, // 转换后的精度
propList: ['*'], // 可以从 px 转为 rem 的属性列表,* 代表所有属性
viewportUnit: 'rem', // 【核心】转换后的单位,我们选择 rem
fontViewportUnit: 'rem', // 字体转换后的单位
selectorBlackList: ['.container-max-width', ''], // 指定不转换的类名
minPixelValue: 1, // 小于 1px 不转换
mediaQuery: true, // 允许在媒体查询中转换
replace: true, // 直接替换值而不添加备用属性
include: [/src/, /node_modules[\/]element-plus/], // 只转换 src 和 element-plus 下的文件
// exclude: [/node_modules/] // 忽略 node_modules
}),
],
},
}
四、动态控制:Nuxt 插件设置根字体大小
光有 rem 还不够,我们需要动态设置 <html> 的 font-size。
// plugins/rem.client.ts
export default defineNuxtPlugin((nuxtApp) => {
const setRem = () => {
const designWidth = 1920 // 我们的设计稿宽度
const baseSize = 16 // 我们希望的最大基准值 (1rem = 16px)
const clientWidth = document.documentElement.clientWidth
// 核心计算公式:缩放比例 = 当前视宽 / 设计稿宽度
const remSize = (clientWidth / designWidth) * baseSize
// 【关键】设置字体大小,并限制其在 12px 到 16px 之间
// 这意味着:屏幕小于 1920px 时会缩小,大于 1920px 时根字体大小稳定在 16px
document.documentElement.style.fontSize = `${Math.min(Math.max(remSize, 12), 16)}px`
}
let timer: NodeJS.Timeout | null = null
const setRemDebounced = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(setRem, 250) // 防抖优化
}
// App 挂载后设置并监听 resize
nuxtApp.hook('app:mounted', () => {
setRem()
window.addEventListener('resize', setRemDebounced)
})
// App 卸载前清理
nuxtApp.hook('app:beforeMount', () => {
window.removeEventListener('resize', setRemDebounced)
if (timer) clearTimeout(timer)
})
})
五、收尾工作:CSS 最大宽度容器
最后,别忘了创建一个最大宽度容器,这是完美收尾的关键。
/* assets/css/global.css */
.container-max-width {
max-width: 1920px; /* 限制最大宽度 */
margin: 0 auto; /* 居中显示 */
}
/* 在布局组件或App.vue中应用 */
<!-- app.vue -->
<template>
<div id="app" class="container-max-width">
<RouterView />
</div>
</template>
总结与展望
- 方案优势:
- 完美适配:实现了“小屏缩放、大屏留白”的理想效果。
- 开发高效:延续了
postcss-px-to-viewport的书写习惯,开发时直接写px。 - 体验优雅:彻底杜绝超宽屏下的布局崩坏问题。
- 注意事项:
- 注意
selectorBlackList要把container-max-width加进去,防止它的max-width: 1920px被转换成rem。 baseSize和mediaQuery结合,可以实现更复杂的响应式逻辑。
- 注意
这个方案是我们团队从 vw 的坑里爬出来后,不断探索得出的最优解,目前已在生产环境稳定运行。如果对你有启发,欢迎点赞、收藏、关注!
来源:juejin.cn/post/7540877562265911332
就因为package.json里少了个^号,我们公司赔了客户十万块

写这篇文章的时候,我刚通宵处理完一个P0级(最高级别)的线上事故,天刚亮,烟灰缸是满的🚬。
事故的原因,说出来你可能不信,不是什么服务器宕机,也不是什么黑客攻击,就因为我们package.json里的一个依赖项,少写了一个小小的^(脱字符号) 。
这个小小的失误,导致我们给客户A的数据计算模块,在一次平平无奇的依赖更新后,全线崩溃。而我们,直到客户的业务方打电话来投诉,才发现问题。
等我们回滚、修复、安抚客户,已经是7个小时后。按照合同的SLA(服务等级协议),我们公司需要为这次长时间的服务中断,赔付客户十万块。
老板在事故复盘会上,倒没说什么重话,只是默默地把合同复印件放在了桌上。

今天,我不想抱怨什么,只想把这个价值 十万块 的教训,原原本本地分享出来,希望能给所有前端、乃至所有工程师,敲响一个警钟。
事故是怎么发生的?
我们先来复盘一下事故的现场。
我们有一个给客户A定制的Node.js数据处理服务。它依赖了我们内部的另一个核心工具库@internal/core。
在项目的package.json里,依赖是这么写的:
{
"name": "customer-a-service",
"dependencies": {
"@internal/core": "1.3.5",
"express": "^4.18.2",
"lodash": "^4.17.21"
// ...
}
}
注意看,express和lodash前面,都有一个^符号,而我们的@internal/core,没有。
这个^代表什么?它告诉npm/pnpm/yarn:“我希望安装1.x.x版本里,大于等于1.3.5的最新版本。”
而没有^,代表什么?它代表:我只安装1.3.5这一个版本,锁死它,不许变。
问题就出在这里。
上周,core库的同事,修复了一个严重的性能Bug,发布了1.3.6版本,并且在公司群里通知了所有人。
我们组里负责这个项目的同学,看到了通知,也很负责任。他想:core库升级了,我也得跟着升。
于是,他看了看package.json,发现项目里用的是1.3.5。他以为,只要他去core库的仓库,把1.3.5这个tag删掉,然后把1.3.6的tag打上去,CI/CD在下次部署时,重新pnpm install,就会自动拉取到最新的代码。
他错了!
最致命的锁死版本
因为我们的依赖写的是"1.3.5",而不是"^1.3.5",所以我们的pnpm-lock.yaml文件里,把这个依赖的解析规则,彻底锁死在了1.3.5。
无论core库的同事怎么发布1.3.6、1.3.7,甚至2.0.0...
只要我们不去手动修改package.json,我们的CI/CD流水线,在执行pnpm install时,永远、永远,都只会去寻找那个被写死的1.3.5版本。
然后,灾难发生了。
core库的同事,在发布1.3.6后,为了保持仓库整洁,就把1.3.5那个旧的git tag给删掉了。
然后,客户A的项目,某天下午需要做一个常规的文案更新,触发了部署流水线。
流水线执行到pnpm install时,pnpm拿着lock文件,忠实地去找@internal/core@1.3.5这个包...
“Error: Package '1.3.5' not found.”
流水线崩溃了。一个本该5分钟完成的文案更新,导致了整个服务7个小时的宕机😖。
十万块换来的血泪教训
事故复盘会上,我们所有人都沉默了。我们复盘的,不是谁的锅,而是我们对依赖管理这个最基础的认知,出了多大的偏差。
^ (Caret) 和 ~ (Tilde) 不是选填,而是必填
^(脱字符) :^1.3.5意味着1.x.x(x >= 5)。这是最推荐的写法。它允许我们自动享受到所有 非破坏性 的小版本和补丁更新(比如1.3.6,1.4.0),这也是npm install默认的行为。~(波浪号) :~1.3.5意味着1.3.x(x >= 5)。它只允许补丁更新,不允许小版本更新。太保守了,一般不推荐。- (啥也不写) :
1.3.5意味着锁死。除非你是react或vue这种需要和生态强绑定的宿主,否则,永远不要在你的业务项目里这么干!
我们团队现在强制规定,所有package.json里的依赖,必须、必须、必须使用^。
关于lock文件
我们以前对lock文件(pnpm-lock.yaml, package-lock.json)的理解太浅了,以为它只是个缓存。
现在我才明白,package.json里的^1.3.5,只是在定义一个规则。
而pnpm-lock.yaml,才是基于这个规则,去计算出的最终答案。
lock文件,才是保证你同事、你电脑、CI服务器,能安装一模一样的依赖树的唯一路径。它必须被提交到Git。
依赖更新,是一个主动的行为,不是被动的
我们以前太天真了,以为只要依赖发了新版,我们就该自动用上。
这次事故,让我们明白:依赖更新,是一个严肃的、需要主动管理和测试的行为。
我们现在的流程是:

- 使用
pnpm update --interactive:pnpm会列出所有可以安全更新的包(基于^规则)。 - 本地测试:在本地跑一遍完整的测试用例,确保没问题。
- 提交PR:把更新后的
pnpm-lock.yaml文件,作为一个单独的PR提交,并写清楚更新了哪些核心依赖。 - CI/CD验证:让CI/CD在
staging环境,用这个新的lock文件,跑一遍完整的E2E(端到端)测试。
这十万块,是技术Leader(我)的失职,也是我们整个团队,为基础不牢付出的最昂贵的一笔学费。
一个小小的^,背后是整个npm生态的依赖管理的核心。
分享出来,不是为了博眼球,是真的希望大家能回去检查一下自己的package.json。
看看你的依赖前面,那个小小的^,它还在吗?😠
来源:juejin.cn/post/7568418604812632073
不容易,35岁的我还在小公司苟且偷生
前言
前几天和前同事闲时聚餐,约了两个月的小聚终于达成了,程序员行业聚少离多,所幸大家的发量还坚挺着。
期间不可避免地聊到了自己的公司、行业状况以及对未来的看法,几杯老酒之后,大家畅所欲言,其中一位老哥侃起了他的职业生涯,既坎坷又无奈,饭后想起来挺有代表性的,征得他同意故记录在此。
以下是老哥的历程。

程序员的前半生
我今年35岁,有房有贷有妻女有老父母。
出生在90年代的农村,从小中规中矩,不惹事不喧哗不突出,三好学生没有我,德智体美没有全面发展。学习也算努力,不算小题做题家,因为只考了个本科。
大学学费全靠助学贷款,勤工俭学补贴日用,埋头苦干成绩也只在年级中等偏下水平。有些同学早早就定下了大学的目标,比如考研、比如出国、比如考公,到了大三的时候大家基本都有了自己的目标。而我的目标就是尽早工作,争取早日还完贷款,因此早早就开始准备找工作。
也许是上天眷顾,不知道怎么就被华为看重了(那会华为还没现在的如日中天,彼时是BAT的天下),稀里糊涂的接受了offer,没想到却是改变了后面十年的决定。
2013年,深圳的夏天阳光明媚,热气扑鼻,提着一个简单的箱子进入了坂田基地。
刚开始,工作上的一切都很新鲜,每个人都在忙碌,虽然不知道他们在忙什么,但感觉很高级的样子。同期入职的同事都比较厉害,很快就适应了工作,而自己还是没完全应对工作内容,于是下班之后继续留在公司学习,顺便蹭饭。
就这样,很快就一年过去了,自己也慢慢熟悉了工作节奏,但是加班也越来越多了。对于自己来说,为了过节点,6点是晚饭时间,9点是下班时间,12点正式下班。
平凡的日子没什么值得留恋,过一天、一个月、一年、四年都没什么两样,四年里学习到了不少的知识,也数了很多次深圳凌晨的路灯数。
作为深漂,没有遇到深圳爱情故事,也对高昂的房价绝望,于是决定回到二线城市,成为一名蓉漂。
2017年,还是和四年前一样的行李箱,出现在了老家的省会城市,只是那时的我没有了助学打款,怀里也攒下了一些血汗钱。
那时互联网行业发展还是如火如荼,前端的需求量也很大,也得益于华为公司发展越来越好,自己的华为经历很快就拿到了几个offer,选了一家初创公司,幻想着能有一番成就。
2018年底,眼看着房价越长越高,某链中介不断地灌输再不买明天就是另一个价了,错过这个村就没这个店了,也许是想有个家,也许是想着父母能到省会里一起住,拿出自己做牛马几年的积蓄加上父母一辈子辛苦攒的小十万的养老钱购买了城区里的新房,那会儿的价格已经比前两年涨了一倍多,妥妥的高位站岗,不过想着自己是刚需也不会卖,因此咬咬牙掏出了全部的积蓄怒而背上了三十年的房贷。
房子的事暂时落定了,全身心的投入到工作中,没想到老板只想骗投资人的钱,产品没弄好投资人不愿跟进了,坚持了三年,期间各种断臂求生,最终还是落了个司破人走的境地。
2020年,30岁的我第一次被动失业了,幸运的是也找到了另一半。为了尽可能节省支出,房子装修的事我们都是亲力亲为,最后花了十多万终于将房子装好了,虽然很简单但毕竟是自己在大城市里的第一套房子,那一刻,感觉十年的付出都是值得的。
背着沉重的房贷,期望能找到一份薪资稍微过得去的工作,于是在简历上优势那行写了:“可加班”。依稀记得有些HR对我进行了灵魂拷问:结婚了吗?有小孩了吗?你都30岁了还能加班吗?。我斩钉截铁地说:只要公司有需要,我定会全力以赴!
2022年,我们的孩子出世了,队友辞去了工作全心全意带小孩,而我更加努力了,毕竟有了四脚吞金兽,不得不肝。
虽然工作很努力,但成果一般,不是公司的技术担当,也不会是技术洼地。
2023年的某一天,和之前的364天一样的平淡,在座位上解Bug的我突然感觉到一阵心悸,呼吸不畅,实在不行了呼唤同事叫了120,去医院一套检查下来没发现什么大问题。医生询问是不是工作压力太大,平时加班很多?我说还好,平时也就加班到9点。医生笑了笑说你这种年轻人我见多了,都是压力大的毛病,平时工作不要久坐盯着屏幕多站起来走走。他让我回家多休息,回去后观察了几天还是偶尔会有心悸,再去了另一个医院进行检查,也是没有明确的诊断结果,只是说可能是这个问题,又可能是另一个问题。
过了1个月后,身体上的问题不见好转,我辞去了工作。
2023年末,找了一家小公司,也就是我现在的公司,工资没有涨,仔细算起来还变相下降了。
还是做的业务需求,也没有领导什么人,管好自己就行,直属上级还是个工作几年的小伙。这家公司主要的特点是不加班,技术难度不高,能做多少就是多少,前提是要报风险,领导也不会强迫加班。
就这样到了2024,神奇的是我已经很久没有心悸的感觉了,不知道是不加班还是心态转变的原因。
家里的小朋友也长大了,会说话了。我现在每天下班最温馨的的是她开着门期待我回家的那一刻,她的期盼的眼神就是我回家的动力。
公司在2024年也裁了不少人,领导也找我谈过问问我的想法,我说:我还是能胜任这份工作的。领导说:公司觉得你年级大了一些,工资虽然不是最高,但不太符合行情,你懂的。我说:我懂,可以接受适当的降薪。
就这样,我挺过了2024,然而过了一周领导走了。
2025年,我35周岁了。
现在的我已经彻底接受自己的平庸的事实了。在学生时代,从来都不出色,也不会垫底,就是那类最容易被忽略的人。在工作时代,不是技术大牛,也不是完全的水货,就是普普通通的程序员。
如果说上半生吃到了什么红利,只能说入坑了计算机这行业,技术给我带了收入,有了糊口的基础。没进股市,却被房价狠狠割了一道。
35岁的我,没有彻底躺平摆烂,也没有足够奋发进取。
35岁的我,有着24年的房贷,还好61岁的时候我还在工作,应该还能还房贷。
35岁的我,不吃海鲜不喝酒,尿酸500+。
35岁的我,人体工学椅也挽救不了腰椎间盘突出。
35岁的我,头发依然浓密,只是白发越来越多。
35岁的我,已经不打游戏,只是会看这各种小说聊以慰藉。
35岁的我,两点一线,每天挤着地铁,看众生百态。
35岁的我,早睡早起,放空自己。
35岁的我,暂时还没有领取毕业大礼包,希望今年还能苟过。
35岁的我,希望经济能够好起来,让如我一般平凡的人能够有活下去的勇气。
诸君,下一年再会~祝你平安喜乐,万事顺遂!
太极分两仪,有程序员也有程序媛:30岁的程序媛,升值加薪与我无缘
来源:juejin.cn/post/7457567782470385705
我删光了项目里的 try-catch,老板:6
相信我们经常这样写bug(不是 👇:

try {
const res = await api.getUser()
console.log('✅ 用户信息', res)
} catch (err) {
console.error('❌ 请求失败', err)
}
看似没问题
- 每个接口都要 try-catch,太啰嗦了!
- 错误处理逻辑分散,不可控!
- 代码又臭又长💨!
💡 目标:不抛异常的安全请求封装
我们希望实现这样的调用👇:
const [err, data] = await safeRequest(api.getUser(1))
if (err) return showError(err)
console.log('✅ 用户信息:', data)
是不是清爽多了?✨
没有 try-catch,却能同时拿到错误和数据。
🧩 实现步骤
1️⃣ 先封装 Axios 实例
// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})
// 🧱 请求拦截器
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => Promise.reject(error)
)
// 🧱 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data
if (res.code !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message || '请求失败'))
}
return res.data
},
(error) => {
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
)
export default service
拦截器的作用:
- ✅ 统一处理 token;
- ✅ 统一处理错误提示;
- ✅ 保证业务层拿到的永远是“干净的数据”。
2️⃣ 封装一个「安全请求函数」
// src/utils/safeRequest.js
export async function safeRequest(promise) {
try {
const data = await promise
return [null, data] // ✅ 成功时返回 [null, data]
} catch (err) {
return [err, null] // ❌ 失败时返回 [err, null]
}
}
这就是关键!
它让所有 Promise 都变得「温柔」——不再抛出异常,而是返回结构化结果。
3️⃣ 封装 API 模块
// src/api/user.js
import request from '@/utils/request'
export const userApi = {
getUser(id) {
return request.get(`/user/${id}`)
},
updateUser(data) {
return request.put('/user', data)
},
}
4️⃣ 在业务层优雅调用
<script setup>
import { ref, onMounted } from 'vue'
import { userApi } from '@/api/user'
import { safeRequest } from '@/utils/safeRequest'
const user = ref(null)
onMounted(async () => {
const [err, data] = await safeRequest(userApi.getUser(1))
if (err) return showError(err)
console.log('✅ 用户信息:', data)
})
</script>
是不是很优雅、数据逻辑清晰、不需要 try-catch、 错误不崩溃。
老板说:牛🍺,你小子有点东西
🧱 我们还可以进一步优化:实现自动错误提示
我们可以给 safeRequest 增加一个选项,让错误自动提示:
// src/utils/safeRequest.js
import { ElMessage } from 'element-plus'
export async function safeRequest(promise, { showError = true } = {}) {
try {
const data = await promise
return [null, data]
} catch (err) {
if (showError) {
ElMessage.error(err.message || '请求失败')
}
return [err, null]
}
}
使用时👇:
const [err, data] = await safeRequest(userApi.getUser(1), { showError: false })
这样你可以灵活控制是否弹出错误提示,
比如某些静默请求就可以关闭提示。
🧠 进阶:TypeScript 支持(超丝滑)
如果你用的是 TypeScript,可以让返回类型更智能👇:
export async function safeRequest<T>(
promise: Promise<T>
): Promise<[Error | null, T | null]> {
try {
const data = await promise
return [null, data]
} catch (err) {
return [err as Error, null]
}
}
调用时:
const [err, user] = await safeRequest<User>(userApi.getUser(1))
if (user) console.log(user.name) // ✅ 自动提示类型
老板:写得很好,下次多写点,明天你来当老板

来源:juejin.cn/post/7565811094734389248
VSCode 推出 绿色版!更强!更智能!
最近,VSCode 再次被推上热门,各大开发社区、推特/X 上都在讨论一个话题—— “VSCode 绿色版(Insiders)功能太强了!”

事实上,VSCode Insiders 并不是一个新名字,但随着微软不断注入 AI 能力、终端增强、Git 大升级、新 UI,它已经变成了真正意义上的:VSCode 超强增强版!

本文将带你全面认识 绿色版都强在哪?为什么开发者都在推荐?你是否应该安装?
🎯 什么是 VSCode 绿色版(Insiders)?
一句话:VSCode 的“更新抢先体验版”,比正式版更快看到新功能!

它和 Stable(正式版)可以共存使用,不会覆盖、不冲突,非常适合爱折腾、追新功能的开发者。
🟢 终端大升级:智能提示终于来了!
VSCode 终端一直是“能用但不好用”;而绿色版直接把它提升到“智能终端”级别。

✨ 1. 命令自动补全(智能建议)
例如你在终端输入:
git ch
它会自动建议:
git checkout
更厉害的是:
- 命令 flag 会被分类展示
- 参数智能提示
- 路径补全更精确
✨ 2. AI 执行命令时使用“真终端渲染器”

绿色版已将 Copilot Agent 的终端输出升级为 xterm.js 真终端渲染器:

ANSI颜色正常显示表格、行列对齐准确- 输出
清晰、结构化
终端从此不再只是“命令窗”,而是智能交互环境。
🌳 Git 管理体验提升:Stash 终于可视化了!
以前 VSCode 对 stash 支持几乎没有,只能敲命令。
这次绿色版直接补上短板!
设置以下选项:
scm.repositories.explorer:truescm.repositories.selectionMode:single

✨ 可视化 Stash 管理器

- 查看所有
stash - 一键
apply / pop / delete - 自动显示
时间戳 - 相同时间段用竖线对齐,界面更清爽
这功能对于频繁切分支、保存临时改动的开发者来说简直是 质的提升。
🤖 AI 全面进化:Copilot Agent 模式上线!
这是绿色版最轰动的更新之一。
✨ 1. Copilot 从“智能补全” → “自动执行任务代理”

它可以自动:
- 修改多个文件
- 执行并分析终端命令
- 管理项目结构
- 重构代码
- 搜索 / 替换文件内容
- 构建 / 运行项目
你只需要一句话:
“帮我给项目增加用户登录功能。”
然后它会自动拆解步骤,帮你完成。
✨ 2. Notebook(Markdown + 代码)也能用 AI 自动编辑

例如:
- 自动创建 Code Cell
- 总结文章内容
- 插入示例代码
- 自动修复错别字
- 做学习笔记
科研、学习、数据分析用户狂喜!
🖥 UI / UX 更现代、更便捷
绿色版对各种细节都做了体验升级:

- Git 同步按钮更直观
- 悬停信息更清晰
- 状态指示更精准
- 布局与配色有实验性改进
整体观感比正式版更轻盈、简洁、顺滑。
🔧 扩展生态更强:插件作者必装
很多新的 API(如 Terminal API、Notebook API、Panel API)都会:
👉 先出现在 Insiders
👉 过几个月再进入正式版
如果你要开发或测试 VSCode 扩展,绿色版是必备。
📥 如何安装 VS Code 绿色版?
安装非常简单:
👉 官方下载安装:https://code.visualstudio.com/insiders
优势:
- 可与正式版并存
- 不影响原环境
- 卸载不留痕
- 随时体验最新功能
✔️ 谁适合使用 VS Code 绿色版?
如果你属于下面任意一个人,请立即安装:
- 喜欢尝鲜
- 重度命令行用户
- Copilot 用户
- 插件开发者
- 关注效率 & 新技术
- 想第一时间体验 VSCode 未来方向
🎉 结语:绿色版不是“内测玩具”,而是更强大的 VSCode
总结一下绿色版带来的增强:
| 功能方向 | 提升 |
|---|---|
| 🔥 终端 | 智能提示 + 真终端渲染 |
| 🌳 Git | 可视化 Stash 管理 |
| 🤖 AI | Copilot 进入自动化时代 |
| 🖥 UI | 更现代、更细腻 |
| 🧩 插件 | 最新 API 先体验 |
一句话:VSCode Insiders 是真正意义上的“VS Code Pro 版”。
如果你还没体验,现在就装一个吧,你会爱上它。
- VSCode Insiders 下载地址:
https://code.visualstudio.com/insiders
来源:juejin.cn/post/7580683234437267519
裁员为什么先裁技术人员?网友一针见血
最近逛职场社区的时候,刷到一个职场话题,老生常谈了,但是每次参与讨论的同学都好多。
这个问题问得比较扎心:
“为什么有些企业的裁员首先从技术人员开始?”

关于这个问题,网上有一个被讨论很多的比喻:
“房子都盖起来了,还需要工人么?”
有一说一,这个比喻虽然刺耳,但却非常形象地揭示了某些企业的用人逻辑,尤其在某些非技术驱动型的公司里。
在某些非技术驱动的公司(比如传统企业转型、或者业务模式成型的公司),其实技术部门很多时候是会被视为「成本中心」,而非「利润中心」的,我相信在这类企业待过的技术同学肯定是深有体会。
就像盖大楼一样,公司需要做一个 App,或者搞一个系统,于是高薪招来一帮程序员“垒代码”。
当这个产品上线,业务跑通了,进入了平稳运营期,公司某些大聪明老板总会觉得“房子”已经盖好了。
这时候,一些开发人员在老板眼里就变成了“冗余”的成本。
大家知道,销售部门、业务部门能直接带来现金流,市场部能带来用户,而技术部门的代码是最看不见摸不着的。
一旦没有新的大项目启动,老板会觉得技术人员坐在那里就是在“烧钱”。
那抛开这个“盖楼”的比喻,在这种非技术驱动的公司里,从纯粹的财务角度来看,裁技术岗往往是因为“性价比”太低。
所以这里我们不得不面对的一个现实是:技术人员通常是公司里薪资最高的一群人。
高薪是一把双刃剑呐。
一个初级程序员的月薪可能抵得上两个行政,一个资深架构师的年薪可能抵得上一个小团队的运营费用。当公司面临现金流危机,需要快速削减成本时,裁掉一个高级技术人员省下来的钱,相当于裁掉好几个非技术岗位人员。
除此之外还有一个比较尴尬的事情那就是,在技术团队中,往往存在着一种“金字塔”结构。
随着工龄增长,薪资涨幅很快,但产出效率(在老板眼里)未必能线性增长。
脑补一下这个场景就知道了:
- 一个 35 岁的高级工程师,月薪 4 万,可能要养家糊口,精力不如 20 多岁的小年轻,加班意愿低。
- 一个 23 岁的小年轻,月薪 1 万 5,充满激情,能扛能造。
这时候某些大聪明老板的算盘就又打起来了:
裁掉一个 4 万的老员工,招两个 1 万 5 的小年轻,代码量翻倍,团队氛围更活跃,成本还降了,这种“优化”在管理层眼里,简直是“降本增效”的典范。
所以综合上面这种种情形分析,这时候,文章开头的那个问题往往也就会逐渐形成了。
所以事就是这么个事,说再多也没用。
既然环境不能左右,那作为个体,我们又该如何自处呢?
这里我不想灌鸡汤,只想务实地聊一聊我所理解的一些对策,希望能对大家有所启发。
同时这也是我给很多后台私信我类似问题小伙伴们的一些共同建议。
1、跳出技术思维,建立业务思维
千万不要只盯着你的 IDE 和那一亩三分地代码,抽空多了解了解业务和流程吧,比如:
- 项目是靠什么赚钱的?
- 你的代码在哪个环节为公司省钱或挣钱?
- 如果你是老板,你会怎么优化现在的系统?
当你能用技术手段去解决业务痛点(比如提升转化率、降低服务器成本)时,你就不再是成本,而是资产。
2、别温水煮青蛙,要保持技能更新
这一点之前咱们这里多次提及,在技术行业,吃“老本”是最危险的。
当今的技术世界变化太快,而作为程序员的我们则恰好处于这一洪流之中,这既是挑战,也是机会。
还是那句话,一定要定期评估一下自己的市场价值:如果明天就离开现在的公司,你的技能和经验是否足以让你在市场上获得同等或更好的位置?
无论在公司工作多久,都要不断更新自己的技能和知识,确保自己始终具有市场竞争力。
3、别让自己的工作经验烂掉,有意识地积累职业资产
这一点我们之前其实也聊过。
除了特定的技术、代码、框架可以作为自己可积累的能力资产之外,其实程序员的职业生涯里也是可以有很多可固化和可积累的有形资产的。
比如你的技术经历、思维、经验、感悟是不是可以写成技术博客文字?你写的代码、工具、框架是不是可以形成开源项目?你的工作笔记和踩坑记录是不是可以整理成技术手册?
千万不要让自己的工作经验烂掉,而是要有意识地将自己的技术资产化,将自己的过往经验、知识、能力转化成在行业里有影响力的硬通货。
4、尽早构建 Plan B,提升抗风险能力
当然这一点虽然说的简单,其实对人的要求是比较高的。前面几点做好了,这一点有时候往往就会水到渠成。
我觉得总体的方向应该是:尽量利用你的技术特长来构建一个可持续的 Plan B。
比方说:开发一个小工具、写写技术专栏、或者运营一个 GitHub 项目、在技术博客或社区中建立个人品牌...等等,这些不仅仅能增加收入,往往还能拓展你的人脉圈。
其实很多程序员在年龄大了之后越来越焦虑的一个重要原因就是因为生存技能太过单一了,所以千万不要给自己设限,埋头赶路的同时也不要忘记时常抬头看看周围的环境和机会。
好了,今天就先聊这么多吧,希望能对大家有所启发,我们下篇见。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7579499567869116466
一个超级真实的Three.js树🌲生成器插件
前言
分享一个基于Three.js封装的树生成器插件,可以实现创建不同类型且渲染效果真实的3D树

说实话,第一次在这个插件官网看到这个效果时我一度以为这只是一个视频,树的内容不仅仅是动态的而且整体的渲染效果也十分真实。
在three.js中使用起来也是非常的简单的仅仅需几行代码就可以搞定,下面给大家简单的介绍一下。
安装
通过 npm/pnpm 安装到项目本地即可
npm i @dgreenheck/ez-tree
pnpm add @dgreenheck/ez-tree
使用
使用起来也是非常简单的,只需要将插件import 引入然后在 new 实例化出来 在添加到 场景中就可以了
最后在一个requestAnimationFrame 动画函数中更新树的内容就行了
import { Tree } from '@dgreenheck/ez-tree';
createTree(){
const tree = new Tree();
tree.generate();
// 设置一下位置
tree.position.set(0, 0, 0);
// 设置一下大小缩放
tree.scale.set(0.1, 0.1, 0.1);
// 添加到场景中
this.scene.add(tree);
}
sceneAnimation(): void {
// 确保动画循环持续进行
this.renderAnimation = requestAnimationFrame(() => this.sceneAnimation());
// 更新时钟
const elapsedTime = this.clock.getElapsedTime();
// 更新控制器 如果当前是第一人称控制器则不更新
if (!this.pointerLockControls) {
this.controls.update();
}
// 更新 Tree 动态效果(风动效果等)
if (this.tree) {
this.tree.update(elapsedTime);
}
// 渲染场景
this.renderer.render(this.scene, this.camera);
}
本地项目效果
因为本地项目对光照等参数没有专门调试所以和官网展示的效果有一定的差距

将相机放大查看树渲染的效果细节处理个人觉得是非常nice的,十分真实

参数
该插件还提供了创建不同类型树的方法,通过官网的在线调试就可以看到效果了
创建一个别的类型树

修改树枝的方向

树叶的多少

项目地址
该项目插件是一个外国大佬开发,如果你的项目或者个人网站需要丰富一下页面内容,那么这个插件或许是个不错的选择
来源:juejin.cn/post/7573588675638099983
PAC 2025:在算力风暴中淬炼的国产力量
2025年的夏天虽已远去,然而PAC 2025的热血余温未散:算力的涌动、屏幕的闪烁、代码的狂奔……那份拼搏与激情,仿佛仍在空气中炽烈燃烧,未曾褪色。
顶尖战队齐聚第21届CCF HPC China 2025的PAC决赛现场,展开正面交锋,将激情与实力尽数倾注 “优化” 与 “应用” 两大赛道,现场氛围燃至顶峰。
赛场的热度,不止是代码奔涌时的风扇轰鸣,更是年轻人拼尽全力时的心跳共振。正是这股激情与执着,凝聚成推动国产计算驶向未来的核心动力。终场哨响,PAC2025并行应用挑战赛圆满收官。


鲲鹏撑腰,满格开战
本届大赛全面采用鲲鹏计算平台作为核心硬件底座。以ARM架构为技术核心,其集成的众核架构、向量/矩阵扩展、片上内存高带宽等硬件特性,成为参赛团队挖掘极致性能的核心载体,也标志着国产CPU平台正式成为高性能计算技术探索的关键阵地。
技术亮点回顾“硬件-软件-应用”的全栈突破
硬件架构特性的深度挖掘:以鲲鹏 ARM 为核心,释放国产 CPU 潜力
ARM 技术的规模化应用:特等奖获得者清华大学深圳国际研究生院团队(简称清华团队)充分发挥矩阵运算可伸缩向量扩展的优势,通过循环重排与数据预取优化GEMM与HPCG性能,最大化鲲鹏CPU的向量计算吞吐。在INT8低精度计算与Attention算子这一核心挑战上,清华、浙大、山大团队均依托鲲鹏平台的矩阵算力,实现了“向量→矩阵”的计算单元升级。例如,清华团队利用矩阵运算单指令完成 Tile 级乘加,大幅降低指令数量与寄存器压力;浙江大学团队则验证“矩阵运算+片上内存”组合的优势,将鲲鹏CPU的带宽与矩阵吞吐拉至接近GPU量级,减少CPU与加速器的数据搬运延迟。
鲲鹏硬件优势的协同验证:山东大学团队在应用赛道中,基于鲲鹏新一代CPU的多核并行与高带宽优势,实现了 20 亿原子体系的分子动力学模拟。在弱扩展8倍、强扩展 4 倍的条件下仍保持80%并行效率,直接证明了国产CPU在超大规模科学计算中的端到端性能,已具备与GPU相当的竞争力。

PAC2025上机现场
软件优化创新:硬件特性与软件策略的深度协同
精细化内存与计算调度:清华团队采用二维 Tiling 策略,浙江大学团队针对K维度切分以充分利用HPC缓存,均将关键数据留驻L1/L2缓存,减少对内存带宽的依赖,适配鲲鹏的缓存架构设计。此外,清华基于 Pthreads 自建线程池,规避操作系统调度开销,实现鲲鹏多核间的任务均衡分配,并行效率较传统方案提升显著。
精度与性能的平衡优化:针对混合精度计算需求,浙大提出“fp32保存中间变量 + svzip 转化为 fp16”的方法,避免了纯 fp16 的指数溢出问题;山大则提出“全流程混合精度向量化”,并自研 ARM 向量化超越函数库,进一步适配鲲鹏平台的指令集特性,在保证计算正确性的前提下,效率提升 20%-30%。
算子级优化突破:山东大学团队在优化赛道中,针对 INT8GEMM 与 Attention 算子提出“数值扩展+算子融合”全栈方案——基于SVSUMOPA/SVMOPA指令实现2路/4路矩阵外积乘法,结合FlashAttention融合策略,减少中间结果访存开销与线程竞争,使大Batch训练与大模型推理的稳定性提升40%以上,为鲲鹏平台的AI算子库建设提供直接技术参考。

PAC2025答辩现场
应用落地突破:覆盖 AI 与科学计算的多领域验证
AI 计算:清华团队的矩阵运算加速与山大的算子融合成果,可直接应用于鲲鹏生态的 AI 芯片与 CPU,为大模型推理(如语音识别、视觉计算)与中小规模训练提供高性能算子支撑,有效解决国产平台“AI计算性能不足”的核心痛点。
科学计算:清华团队的 HPCG 优化与山大的分子动力学模拟,验证了鲲鹏平台在气象、天文、流体力学、药物研发等领域的适用性——如山东大学团队的成果可直接复用至新能源材料设计与复杂流体计算,为国产高性能计算的行业落地提供技术范本。

PAC的意义:从赛场到未来
PAC大赛的成果不是单点的创新打法,而是真正能走出赛场、落到产业的技术。无论是算子优化,还是大规模科学计算模拟,都已具备直接赋能科研与产业的潜力。
PAC 2025的意义,在于夯实国产算力生态,让以鲲鹏为核心的国产 CPU 走向成熟,打破“高性能依赖国外架构”的偏见;在于推动“硬件—软件—应用”的全栈融合,让协同优化成为可复制的范式;更在于将成果带入产业与人才的长远布局,既赋能 AI、大模型、分子动力学等应用场景,也培养出一批能够横跨硬件、软件与应用的青年力量。
从 ARM 架构的深度挖掘,到软硬件的协同优化,再到端到端的应用突破,PAC 2025 让国产算力不再只是“能用”,而是真正“好用”。它证明了我们不再只是被动追赶,而是已能与前沿并肩而行,正全力奔向属于中国的高性能计算未来。
从JS到Python:一个前端开发者的丝滑转型之路

“我永远记得第一次用Python写出自动化脚本的那个深夜——原来编程语言间的鸿沟,远没有想象中那么深。”
作为一名纯前端出身的开发者,我曾以为JavaScript就是编程世界的全部。直到被迫接手一个数据分析项目,才在焦虑中踏上了Python学习之路。今天分享我的真实转型经验,带你避开我踩过的坑。
我的认知颠覆时刻
初学JS时,我以为死记语法是王道:
// 曾经的我:疯狂背诵语法
const arrowFn = (a, b) => a + b;
const promise = new Promise((res) => setTimeout(res, 1000));
转学Python后才发现:
# 现在的我:理解概念重于记忆
add = lambda a, b: a + b
await asyncio.sleep(1)
核心顿悟:编程语言的本质是表达逻辑的工具,掌握变量/函数/循环这些通用概念,比记住特定语法重要十倍。
为什么JS开发者必学Python?
当我的项目经理扔来一份Python需求时,我发现了它的不可替代性:
| 场景 | JavaScript | Python | 我的选择 |
|---|---|---|---|
| 数据可视化 | Chart.js | Matplotlib | 后者交互更专业 |
| 爬虫开发 | Puppeteer | Scrapy | 效率差5倍不止 |
| 自动化脚本 | Node脚本 | PyAutoGUI | 写文件操作真香 |
| 机器学习 | TensorFlow.js | PyTorch | 生态碾压性优势 |
最打动我的点:用Python写算法时,代码就像伪代码一样直白:
# 快速实现斐波那契数列
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
我的语法转换血泪史
这些细节坑惨了我这个JS老手:
1. 花括号→缩进的地狱
// JS习惯:靠{}划分作用域
if (loggedIn) {
const user = fetchUser()
console.log(user)
}
# Python的缩进陷阱(初学者的噩梦)
if logged_in:
user = fetch_user()
print(user) # 这里居然还能访问user!
我的教训:安装Pylint插件强制4空格缩进,避免混合制表符
2. 变量命名的文化冲突
// JS:驼峰式
const currentUser = { firstName: "John" };
# Python:蛇形命名
current_user = { "first_name": "John" }
适应技巧:在VSCode中设置自动转换规则
3. 空值判断的深坑
// JS的魔幻三兄弟:null, undefined, false
let value = undefined;
if (!value) { /* 会触发 */ }
# Python的明确哲学
value =
if value is : # 必须显式判断
print("空值")
让我惊艳的Python特性
列表推导式(List Comprehensions)
# 一行完成JS需要5行的操作
squares = [x**2 for x in range(10) if x % 2 == 0]
# 等效JS代码:
# const squares = [];
# for(let x=0; x<10; x++) {
# if(x%2===0) squares.push(x**2)
# }
解构赋件的优雅
# 交换变量值
a, b = b, a
# JS等价操作:
# let temp = a;
# a = b;
# b = temp;
类型提示的救赎
def greet(name: str) -> str:
return f"Hello, {name}"
# 作为TS爱好者狂喜!
我的学习路线图
- 第一周:语法转换训练
- 把常写的JS工具函数用Python重写
- 在Leetcode上用Python刷简单题
- 第二周:生态征服计划
pip install pandas requests matplotlib
- 用Pandas处理Excel数据
- 用Requests爬取网页内容
- 第三周:项目实战
- 自动化日报邮件发送脚本
- Django搭建简易博客后台
- 持续进阶
- 深入理解Python垃圾回收机制
- 掌握asyncio异步编程模型
那些我希望早点知道的技巧
# 1. 虚拟环境是救命稻草
python -m venv .venv
source .venv/bin/activate
# 2. 使用pathlib代替os.path
from pathlib import Path
config = Path.home() / ".config" / "myapp.json"
# 3. 活用f-string
print(f"{user.name=} {user.age=}") # 输出:user.name='John' user.age=30
转型后的真实感受
开发体验变化:
- ✅ 少了
npm install的依赖地狱 - ✅ 错误信息更人性化
- ❌ 前端调试体验不如Chrome DevTools
心态转变:
“以前觉得Python是‘其他语言’,现在明白它和JS是互补的左右手——JS构建用户界面,Python处理背后逻辑。”
给同行的建议:不要试图“转行”,而要“扩列”。我的GitHub个人主页现在 proudly displays:
JavaScript | Python | 持续学习者
最后忠告:当你在Python中写
import this,输出的禅意哲学,正是这门语言的精髓——优美胜于丑陋,明了胜于晦涩。这大概就是我从JS的“灵活”走向Python的“优雅”时,最深的共鸣。

红中老大 快醒吧。 我们要成啦!!!
来源:juejin.cn/post/7538329980636479498
当上组长一年里,我保住了俩下属
前言
人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。
就在上周,"苟住"群里的一个小伙伴也苟不住了。

在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。
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
第一个成功在APP store 上架的APP
XunDoc开发之旅:当AI医生遇上家庭健康管家
当我在生活中目睹家人为管理复杂的健康数据、用药提醒而手忙脚乱时,一个想法冒了出来:我能否打造一个App,像一位贴心的家庭健康管家,把全家人的健康都管起来?它不仅要能记录数据,还要够聪明,能解答健康疑惑,能主动提醒。这就是 XunDoc App。
1. 搭建家庭的健康数据中枢
起初,我转向AI助手寻求架构指导。我的构想很明确:一个以家庭为单位,能管理成员信息、记录多种健康指标(血压、血糖等)的系统。AI很快给出了基于SwiftUI和MVVM模式的代码框架,并建议用UserDefaults来存储数据。
但对于一个完整的应用而言,我马上遇到了第一个问题:数据如何在不同视图间高效、准确地共享? 一开始我简单地使用@State,但随着功能增多,数据流变得一团糟,经常出现视图数据不同步的情况。
接着在Claude解决不了的时候我去询问Deepseek,它一针见血地指出:“你的数据管理太分散了,应该使用EnvironmentObject配合单例模式,建立一个统一的数据源。” 这个建议成了项目的转折点。我创建了FamilyShareManager和HealthDataManager这两个核心管家。当我把家庭成员的增删改查、健康数据的录入与读取都交给它们统一调度后,整个应用的数据就像被接通了任督二脉,立刻流畅稳定了起来。
2. 请来AI医生:集成Moonshot API
基础框架搭好,接下来就是实现核心的“智能”部分了。我想让用户能通过文字和图片,向AI咨询健康问题。我再次找到AI助手,描述了皮肤分析、报告解读等四种咨询场景,它很快帮我写出了调用Moonshot多模态API的代码。
然而,每件事都不能事事如意的。文字咨询很顺利,但一到图片上传就频繁失败。AI给出的代码在处理稍大一点的图片时就会崩溃,日志里满是编码错误。我一度怀疑是网络问题,但反复排查后,我询问Deepseek,他告诉我:“多模态API对图片的Base64编码和大小有严格限制,你需要在前端进行压缩和校验。”
我把他给我的建议给到了Claude。claude帮我编写了一个“图片预处理”函数,自动将图片压缩到4MB以内并确保编码格式正确。当这个“关卡”被设立后,之前桀骜不驯的图片上传功能终于变得温顺听话。看着App里拍张照就能得到专业的皮肤分析建议,那种将前沿AI技术握在手中的感觉,实在令人兴奋。
3. 打造永不遗忘的智能提醒系统
健康管理,贵在坚持,难在记忆。我决心打造一个强大的医疗提醒模块。我的想法是:它不能是普通的闹钟,而要像一位专业的护士,能区分用药、复查、预约等不同类型,并能灵活设置重复。
AI助手根据我的描述,生成了利用UserNotifications框架的初始代码。但很快,我发现了一个新问题:对于“每周一次”的重复提醒,当用户点击“完成”后,系统并不会自动创建下一周的通知。这完全违背了“提醒”的初衷。
“这需要你自己实现一个智能调度的逻辑,在用户完成一个提醒时,计算出下一次触发的时间,并重新提交一个本地通知。” 这是deepseek告诉我的,我把这个需求告诉给了Claude。于是,在MedicalNotificationManager中, claude加入了一个“重新调度”的函数。当您标记一个每周的用药提醒为“已完成”时,App会悄无声息地为您安排好下一周的同一时刻的提醒。这个功能的实现,让XunDoc从一个被动的记录工具,真正蜕变为一个主动的健康守护者。
4. 临门一脚:App Store上架“渡劫”指南
当XunDoc终于在模拟器和我的测试机上稳定运行后,我感觉胜利在望。但很快我就意识到,从“本地能跑”到“商店能下”,中间隔着一道巨大的鸿沟——苹果的审核。证书、描述文件、权限声明、截图尺寸……这些繁琐的流程让我一头雾水。
这次,我直接找到了DeepSeek:“我的App开发完了,现在需要上传到App Store,请给我一个最详细、针对新手的小白教程。”
DeepSeek给出的回复堪称保姆级,它把整个过程拆解成了“配置App ID和证书”、“在App Store Connect中创建应用”、“在Xcode中进行归档打包”三大步。我就像拿着攻略打游戏,一步步跟着操作:
- 创建App ID:在苹果开发者后台,我按照说明创建了唯一的App ID
com.[我的ID].XunDoc。 - 搞定证书:最让我头疼的证书环节,DeepSeek指导我分别创建了“Development”和“Distribution”证书,并耐心解释了二者的区别。
- 设置权限:因为App需要用到相机(拍照诊断)、相册(上传图片)和通知(医疗提醒),我根据指南,在
Info.plist文件中一一添加了对应的权限描述,确保审核员能清楚知道我们为什么需要这些权限。
一切准备就绪,我在Xcode中点击了“Product” -> “Archive”。看着进度条缓缓填满,我的心也提到了嗓子眼。打包成功!随后通过“Distribute App”流程,我将我这两天的汗水上传到了App Store Connect。当然不是一次就通过上传的。

5. 从“能用”到“好用”:三次UI大迭代的觉醒
应用上架最初的兴奋感过去后,我陆续收到了一些早期用户的反馈:“功能很多,但不知道从哪里开始用”、“界面有点拥挤,找东西费劲”。这让我意识到,我的产品在工程师思维里是“功能完备”,但在用户眼里可能却是“复杂难用”。
我决定重新设计UI。第一站,我找到了国产的Mastergo。我将XunDoc的核心界面截图喂给它,并提示:“请为这款家庭健康管理应用生成几套更现代、更友好的UI设计方案。”
Mastergo给出的方案让我大开眼界。它弱化了我之前强调的“卡片”边界,采用了更大的留白和更清晰的视觉层级。它建议将底部的标签栏导航做得更精致,并引入了一个全局的“+”浮动按钮,用于快速记录健康数据。这是我第一套迭代方案的灵感来源:从“功能堆砌”转向“简洁现代” 。

然而,Mastergo的方案虽然美观,但有些交互逻辑不太符合iOS的规范。于是,第二站,我请来了Stitch。我将完整的产品介绍、所有功能模块的说明,以及第一版的设计图都给了它,并下达指令:“请基于这些材料,完全重现XunDoc的完整UI,但要遵循iOS Human Interface Guidelines,并确保信息架构清晰,新用户能快速上手。”等到他设计好了后 我将我的设计图UI截图给Claude,让他尽可能的帮我生成。

(以上是我的Stitch构建出来的页面)
Claude展现出了惊人的理解力。它不仅仅是在画界面,而是在重构产品的信息架构。它建议将“AI咨询”的四种模式(皮肤、症状、报告、用药)从并列排列,改为一个主导航入口,进去后再通过图标和简短说明让用户选择。同时,它将“首页”重新定义为真正的“健康概览”,只显示最关键的数据和今日提醒,其他所有功能都规整地收纳入标签栏。这形成了我的第二套迭代方案:从“简洁现代”深化为“结构清晰” 。

拿着Claude的输出,我结合Mastergo和Stitch的视觉灵感,再让Cluade一步一步的微调。我意识到,颜色不仅是美观,更是传达情绪和功能的重要工具。我将原本统一的蓝色系,根据功能模块进行了区分:健康数据用沉稳的蓝色,AI咨询用代表智慧的紫色,医疗提醒用醒目的橙色。图标也设计得更加线性轻量,减少了视觉负担。(其实这是Deepseek给我的建议)这就是最终的第三套迭代方案:在清晰的结构上,注入温暖与亲和力。

这次从Stitch到Claude的UI重塑之旅,让我深刻意识到,一个成功的产品不仅仅是代码的堆砌。它是一次与用户的对话,而设计,就是这门对话的语言。通过让不同的AI助手在我的引导下“协同创作”,我成功地让XunDoc从一個工程师的作品,蜕变成一个真正为用户着想的产品。
现在这款app已经成功上架到了我的App store上 大家可以直接搜索下来进行使用和体验,我希望大家可以在未来可以一起解决问题!
来源:juejin.cn/post/7559864914883067914
一些经典的3D编辑器开源项目
前言
给大家分享一下个人在探索开发three.js编辑器项目期间发现的一些比较不错的3D编辑器类型的开源项目,如果你也正打算做类似相关的项目,那么这些开源项目会是一个不错的参考借鉴
以下排名不分先后🙏🏻
项目一:Astral3D
描述:基于Vue3 + THREE.JS 免费开源的三维引擎及配套编辑器,包含BIM轻量化、CAD解析预览、粒子系统、插件系统等功能。
特点:强大的3D场景内容元素的编辑和保存功能和丰富多样的3D元素内容,同时支持BIM和CAD等工业建模文件的加载渲染
注意⚠️:项目是Apache-2.0 license 的开源协议,项目作者本人也声明了项目可用于个人学习,如有商用需要向作者申请商用授权
界面:

Github: github.com/mlt131220/A…
项目二:thebrowserlab
描述:一个「运行在浏览器里的 3D 编辑器 + 创意编码 (creative-coding) 环境」
特点:支持加载视频、文本、图片、粒子等内容并提供了丰富的编辑表单参数可视化编辑配置,同时还支持在线代码的脚本内容写入设置3D场景内容。
注意⚠️:项目使用 MIT 授权 (MIT license),意味着你可以自由地 fork、修改、商用 (遵守 MIT 即可)
界面:

在线地址:thebrowserlab.com/
Github: github.com/icurtis1/th…
项目三:threepipe
描述:一个基于 Three.js 构建的现代 3D 框架
特点:项目基于了Three.js进行了二次封装,提供了不少高级功能,使其适合从简单 3D 模型预览到复杂 交互 / 渲染应用,通过简单的API 使用就可以快速创建复杂的3D模型预览器,模型编辑器等内容。
注意⚠️:既然是封装好的框架,在享受使用的便利时,新的学习成本也是不可避免的,项目使用 Apache-2.0-1协议,商用也许需要授权,不过毕竟是歪果仁开发的,即使未授权也难以知晓
界面:
Github: github.com/repalash/th…
项目四:ShadowEditor
描述:基于Three.js、Go语言和MongoDB的跨平台的3D场景编辑器,支持桌面版和Web版。
特点:跨平台的支持 Windows / Linux / Mac,在桌面 (desktop) 和浏览器 (web) 中都能运行,前后端一体的项目
注意⚠️:使用 MIT 许可证的项目,可以自由用于学习、实验或商业用途。从界面不难看出,应该是属于上古时期的项目了,three.js版本也是107的。作者也推出了商业版的,如有需要也可以试用一下商业版的
界面:

在线地址:http://www.hylab.cn/shadowedito…
Github: gitee.com/tengge1/Sha…
项目五:three-editor
描述:一个基于 Three.js 的 可视化 / 低代码 3D 编辑器 / 内核/框架。它的目标是降低使用 Three.js 的门槛,让构建 Web 3D 场景更简单、更迅速
特点:提供了一整套“可视化 + 配置 + 编辑 + 渲染”的能力,使得即使不深入了解 Three.js,也能快速构建 3D 场景 / 项目,:如果你只是想在网页中展示某个 3D 模型、场景或交互,而不想编写大量 Three.js boilerplate,three-editor 能极大降低门槛
注意⚠️:因为场景内容都是封装处理好的,提供的可编辑参数内容配置并不多,如果你的自定义需求很多的话使用这个项目前需要谨慎考虑一下
界面:

在线地址:z2586300277.github.io/threejs-edi…
Github: github.com/z2586300277…
项目六:scene-editor
描述:vis-three/scene-editor 是基于 vis-three 框架构建的 —— vis-three 本身是一个封装自 Three.js 的前端 3D 开发框架,用于简化 Web3D 开发
特点:基于vis-three 衍生开发的一个3D编辑器提供了一套较为完整的 Web 3D 场景编辑功能 — 目标是让你即使对 3D 或 Three.js 不熟,也能比较轻松地 “拖/配/编辑” 出一个 3D 场景
注意⚠️:仓库地址的代码是Vue3项目编译打包后的,作者并没有直接提供Vue3项目的源代码,如果有二次开发需求,无法直接性修改源代码
界面:

在线地址:z2586300277.github.io/threejs-edi…
Github: github.com/Shiotsukika…
Gitee:gitee.com/vis-three/s…
项目七: three.js官方编辑器
描述:Three.js(著名的 WebGL / Web 3D 渲染库)自带 / 官方提供的可视化编辑器,接触过three.js的应该都知道吧
特点:3D编辑器的鼻祖了也是唯一一个能和three.js最新版本保持随时同步的编辑器,很多现有的商业项目和开源项目的功能,或多或少都参考了这个项目去实现的
注意⚠️:使用原生js 去实现的,二次开发和扩展功能成本较大
界面:

在线地址:threejs.org/editor/
Github: github.com/mrdoob/thre…
项目八: threejs-3dmodel-edit
描述:一个基于 Three.js + Vue 3 + TypeScript + Pinia 的前端 3D 模型编辑器 / 可视化编辑平台
特点:是一个比较完整、现代、易用的 Web-based 3D 模型编辑器 — 它把 Three.js 的功能通过 Vue / TS / Pinia 封装起来,让非专业 3D 建模背景的人也能比较容易地加载 /编辑 /导出 /展示 3D 模型。基于企业级项目代码开发的标准规范,如果你正在开发自己的第一个企业级Three.js 项目那么这个项目的代码设计思路将会是一个不错的参考
注意⚠️:作者本人的3D开源项目,毛遂自荐一下,哈哈哈哈
界面:

Github: github.com/zhangbo126/…
Gitee:gitee.com/ZHANG_6666/…
结语
ok以上就是作者本人已知的一些不错的开源3D编辑器合集了,如果你还知道一些好的3D编辑器项目欢迎评论区补充
来源:juejin.cn/post/7576867727719039011
🍭🍭🍭升级 AntD 6:做第一个吃螃蟹的人
AntD 6 发布之后,网上很多人都在观望:
“要不要升级?”
“会不会炸?”
“我的项目能不能扛得住?”
其实 AntD 官方已经在文档里把升级路径写得非常清楚,只是稍显简略。
下面我用更真实、更工程化的方式,把 v5 → v6 的升级步骤 做了一次加强版讲解,
让你升级时不至于踩坑。
① 第一步:升级到 v5 最新版本(必须执行)
在升级到 AntD 6 之前,官方强烈建议你先把项目从 v5 升到 v5 最新版本:5.29.1。
为什么?
✔ v5 的最新版本会给出所有废弃 API 的 warning
✔ 不处理这些 warning,到 v6 会直接报错
✔ v5 → v6 是平滑升级路径,只要你处理掉 v5 的 warning,升级 v6 就不会炸
执行命令:
npm install antd@5
装完以后,启动项目,务必一条一条看控制台 warning。
比如:
- 某个 API 将被移除
- 某个 props 已废弃
- 某个组件 v6 即将删除
所有 warning 都处理完,再继续下一步。
这阶段非常关键,等于是在做“升级前全身检查”。
其实你只要用了5的版本基本没啥大问题

② 第二步:确保项目运行在 React 18(或以上)
AntD 6 不再支持 React 17 及以下版本。
AntD 官方的态度非常明确:
“React 17,我们不救了。”
好消息是:绝大多数前端项目早就 React 18 了。
如果你还停留在 React 17,那建议你别升级 AntD,
你升级 React 本身都要做好打仗的准备。
检查你的 package.json:
"react": "^18.x.x",
"react-dom": "^18.x.x"
如果不是,那你真的得升级个 der(官方术语:赶紧升 😅)。
React 17 升到 React 18 已经是必经之路,
Suspense、Concurrent、SSR 都已经进入新阶段,不升会拖累整个项目生态。
③ 第三步:开干!升级到 AntD 6
前面两步做完,你的项目基本已经“具备上 6 条件”了。
现在就可以正式开刀:
npm install --save antd@6
或者你爱用的包管理器:
yarn add antd@6
# or
pnpm add antd@6
# or
npm install antd@latest
安装完成后,你的项目就是 AntD 6 正式用户。
④ 第四步:启动项目,处理残留 warning
升级完成以后,重启项目。
你可能会看到一些:
- 类型定义变动 warning
- 某些行为变更 warning
- 某些组件结构调整提示
- mask blur 带来的视觉差异
- 你自己写的样式被 DOM 改动影响
这些属于正常“升级后适配”。
根据提示处理即可。
建议你重点检查以下区域:
✔ 自定义覆盖类名(AntD 6 DOM 有变化)
✔ Modal、Drawer 的 mask 是否出现模糊效果
✔ Table、Form 是否有类型冲突
✔ 已废弃 API 是否仍然使用
✔ 第三方依赖是否引用了 AntD 内部 class
通常处理 1-2 小时就可以全部解决 ( 不解决也行,能跑就行😉)。
⑤ 最终:你就正式吃上了 AntD 6 的“螃蟹”
完成以上步骤后,你就完成了整个升级链路:
- 清理 v5 废弃 API
- 升到 React 18(如果你的项目还没)
- 升到 AntD 6
- 修复升级后剩余 warning
从此以后:
- 你可以用 Masonry 瀑布流
- 你可以用更快的 Tooltip
- 你可以享受 ZeroRuntime
- 你可以用语义化 DOM 更好写主题
- 你的项目正式进入 2025 年的前端栈
一句话:
你是第一个吃螃蟹的人,但这次螃蟹真的不难吃,而且还挺香,哥们已经升级,满嘴流油了。

来源:juejin.cn/post/7576571960286822435
基于WASM的纯前端Office解决方案:在线编辑/导入导出/权限切换/多实例/实例缓存(已开源)
效果展示
所有操作均在浏览器进行,先来看看最终效果:
🌐 在线演示: mvp-onlyoffice.vercel.app/
🔗 GitHub仓库: mvp-onlyoffice
基本示例

多实例示例

多tab示例

。。。。。。。。。。
核心功能演示
- ✅ 文档上传:支持本地文件直接上传
- ✅ 实时编辑:流畅的文档编辑体验
- ✅ 格式转换:基于WASM的文档格式转换
- ✅ 导出保存:一键导出编辑后的文档
- ✅ 模式切换:只读/可编辑模式自由切换
- ✅ 多语言支持:中英文界面无缝切换
- ✅ 多实例支持:同时运行多个独立编辑器实例(Word/Excel/PPT)
- ✅ 资源隔离:每个实例独立的图片上传和媒体资源管理
技术架构
核心技术栈
- React 19 + Next.js 15:现代化前端框架
- OnlyOffice SDK:官方JavaScript SDK,提供文档编辑核心能力
- WebAssembly (x2t-wasm):文档格式转换引擎
- TypeScript:类型安全的开发体验
- EventBus:事件驱动的架构设计
- IndexedDB:WASM文件缓存优化
- EditorManagerFactory:多实例管理器工厂模式
tip: 事实上不依赖于 react,你可以拿到 项目中的 src/onlyoffice-comp ,然后接入到任何系统中去,接入层可以参考 src/app/excel/page.tsx等应用层文件
架构流程图
用户上传文档
↓
React组件层
↓
EditorManagerFactory (多实例管理器工厂)
↓
EditorManager (编辑器管理器)
↓
X2T Converter (WASM转换器)
↓
OnlyOffice SDK (文档编辑器)
↓
EventBus (事件总线)
↓
导出/保存文档
WASM文档转换核心流程
转换流程图解
用户选择文件
↓
浏览器读取文件
↓
WASM虚拟文件系统
↓
X2T引擎执行转换
↓
生成二进制数据 + 媒体资源
↓
OnlyOffice编辑器加载
核心代码实现
// src/onlyoffice-comp/lib/x2t.ts
/**
* X2T 工具类 - 负责文档转换功能
*/
class X2TConverter {
private x2tModule: EmscriptenModule | null = null;
// 支持的文件类型映射
private readonly DOCUMENT_TYPE_MAP: Record<string, DocumentType> = {
docx: 'word',
doc: 'word',
odt: 'word',
rtf: 'word',
txt: 'word',
xlsx: 'cell',
xls: 'cell',
ods: 'cell',
csv: 'cell',
pptx: 'slide',
ppt: 'slide',
odp: 'slide',
};
/**
* 转换文档格式
*/
async convertDocument(file: File): Promise<ConversionResult> {
// 初始化WASM模块
await this.ensureReady();
// 写入虚拟文件系统
const data = await file.arrayBuffer();
this.x2tModule!.FS.writeFile('/working/origin', new Uint8Array(data));
// 执行C++编译的转换模块
this.executeConversion('/working/params.xml');
// 提取转换结果和媒体文件
return {
bin: this.x2tModule!.FS.readFile('/working/output.bin'),
media: this.collectMediaFiles() // 提取图片等资源
};
}
}
编辑器管理器:多实例架构设计
项目采用工厂模式管理多个编辑器实例,每个实例都有独立的容器ID和资源管理:
// src/onlyoffice-comp/lib/editor-manager.ts
// EditorManagerFactory - 多实例管理器工厂
class EditorManagerFactory {
private static instance: EditorManagerFactory;
private managers: Map<string, EditorManager> = new Map();
// 创建或获取编辑器管理器实例
create(containerId?: string): EditorManager {
if (containerId) {
// 如果已存在,返回现有实例
if (this.managers.has(containerId)) {
return this.managers.get(containerId)!;
}
// 创建新实例
const manager = new EditorManager(containerId);
this.managers.set(containerId, manager);
return manager;
}
// 创建默认实例
return this.createDefault();
}
// 获取指定容器ID的实例
get(containerId: string): EditorManager | undefined {
return this.managers.get(containerId);
}
// 销毁指定实例
destroy(containerId: string): void {
const manager = this.managers.get(containerId);
if (manager) {
manager.destroy();
this.managers.delete(containerId);
}
}
// 销毁所有实例
destroyAll(): void {
this.managers.forEach(manager => manager.destroy());
this.managers.clear();
}
}
// EditorManager - 单个编辑器管理器
class EditorManager {
private instanceId: string;
private containerId: string;
private editor: DocEditor | null = null;
constructor(containerId?: string) {
this.instanceId = nanoid(); // 生成唯一实例ID
this.containerId = containerId || `onlyoffice-editor-${this.instanceId}`;
}
// 获取实例ID
getInstanceId(): string {
return this.instanceId;
}
// 获取容器ID
getContainerId(): string {
return this.containerId;
}
// 导出文档(事件驱动)
async export(): Promise<SaveDocumentData> {
const editor = this.get();
if (!editor) {
throw new Error('Editor not available');
}
// 触发保存
(editor as any).downloadAs();
// 等待保存事件
const result = await onlyofficeEventbus.waitFor(
ONLYOFFICE_EVENT_KEYS.SAVE_DOCUMENT,
10000
);
return result;
}
// 设置只读模式
async setReadOnly(readOnly: boolean): Promise<void> {
// 实现逻辑...
}
}
事件驱动架构:EventBus解耦设计
项目采用事件总线机制,实现组件间的松耦合通信:
// src/onlyoffice-comp/lib/eventbus.ts
class EventBus {
private listeners: Map<EventKey, Array<(data: any) => void>> = new Map();
// 监听事件
on<K extends EventKey>(key: K, callback: (data: EventDataMap[K]) => void): void {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key)!.push(callback);
}
// 等待事件触发(返回 Promise)
waitFor<K extends EventKey>(key: K, timeout?: number): Promise<EventDataMap[K]> {
return new Promise((resolve, reject) => {
const timeoutId = timeout
? setTimeout(() => {
this.off(key, handleEvent);
reject(new Error(`Event ${key} timeout after ${timeout}ms`));
}, timeout)
: null;
const handleEvent = (data: EventDataMap[K]) => {
if (timeoutId) clearTimeout(timeoutId);
this.off(key, handleEvent);
resolve(data);
};
this.on(key, handleEvent);
});
}
}
支持的事件类型
saveDocument- 文档保存完成事件documentReady- 文档加载就绪事件loadingChange- 加载状态变化事件
核心功能特性
1. 多实例支持
支持同时创建和管理多个独立的编辑器实例,每个实例都有独立的容器ID和资源管理:
import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';
// 创建第一个编辑器实例
const manager1 = await createEditorView({
isNew: true,
fileName: 'Document1.docx',
containerId: 'editor-1', // 指定容器ID
});
// 创建第二个编辑器实例
const manager2 = await createEditorView({
isNew: true,
fileName: 'Document2.xlsx',
containerId: 'editor-2', // 不同的容器ID
});
// 分别操作不同实例
const result1 = await manager1.export();
const result2 = await manager2.export();
// 销毁指定实例
editorManagerFactory.destroy('editor-1');
// 销毁所有实例
editorManagerFactory.destroyAll();
关键特性:
- 容器隔离:每个实例使用唯一的容器ID,通过
data-onlyoffice-container-id属性精确定位 - 资源隔离:每个实例管理独立的媒体资源映射,图片上传不会相互干扰
- 独立事件处理:每个实例通过
createWriteFileHandler(manager)创建独立的图片上传处理函数
2. 国际化支持
项目内置多语言支持,可自由切换中英文界面。在多实例场景下,切换语言会重新创建所有编辑器实例:
// 切换语言(多实例场景)
const handleLanguageSwitch = async () => {
const newLang = currentLang === 'zh' ? 'en' : 'zh';
setCurrentLang(newLang);
// 保存每个编辑器实例的文档信息
const editorDocuments = {
manager1: { fileName: 'Doc1.docx', file: file1 },
manager2: { fileName: 'Doc2.xlsx', file: file2 },
};
// 重新创建所有编辑器以应用新语言
if (editorDocuments.manager1) {
await createEditorView({
fileName: editorDocuments.manager1.fileName,
file: editorDocuments.manager1.file,
containerId: 'editor-1',
lang: newLang,
});
}
if (editorDocuments.manager2) {
await createEditorView({
fileName: editorDocuments.manager2.fileName,
file: editorDocuments.manager2.file,
containerId: 'editor-2',
lang: newLang,
});
}
};
3. 导入导出功能
完整的文档导入导出能力:
// 导出文档
const result = await editorManager.export();
// result 包含: { fileName, fileType, binData, media }
// 转换并下载
const buffer = await convertBinToDocument(
result.binData,
result.fileName,
FILE_TYPE.XLSX,
result.media
);
const blob = new Blob([buffer.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// 执行下载操作
4. 只读/可编辑模式切换
灵活的权限控制,支持动态切换编辑模式:
// 设置为只读模式
await editorManager.setReadOnly(true);
// 切换为可编辑模式
await editorManager.setReadOnly(false);
// 查询当前模式
const isReadOnly = editorManager.getReadOnly();
实现原理:
- 从只读切换到可编辑:重新创建编辑器实例
- 从可编辑切换到只读:使用
processRightsChange命令
5. IndexedDB缓存优化
使用IndexedDB缓存WASM文件,大幅提升二次加载速度:
// 拦截 fetch,缓存 WASM 文件到 IndexedDB
private interceptFetch(): void {
const originalFetch = window.fetch;
window.fetch = async function(input: RequestInfo | URL): Promise<Response> {
// 先尝试从缓存读取
const cached = await this.getCachedWasm(url);
if (cached) {
return new Response(cached, {
headers: { 'Content-Type': 'application/wasm' }
});
}
// 缓存未命中,从网络加载并缓存
const response = await originalFetch(input);
const arrayBuffer = await response.arrayBuffer();
await this.cacheWasm(url, arrayBuffer);
return response;
};
}
使用示例
基本使用(单实例)
import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';
// 创建编辑器视图(使用默认实例)
await createEditorView({
file: fileObject, // File 对象(可选)
fileName: 'document.xlsx', // 文件名
isNew: false, // 是否新建文档
readOnly: false, // 是否只读
lang: 'zh', // 界面语言
});
// 获取默认实例并导出文档
const defaultManager = editorManagerFactory.getDefault();
const result = await defaultManager.export();
console.log('导出成功:', result);
多实例使用
import { createEditorView } from '@/onlyoffice-comp/lib/x2t';
import { editorManagerFactory } from '@/onlyoffice-comp/lib/editor-manager';
// 创建多个编辑器实例
const manager1 = await createEditorView({
isNew: true,
fileName: 'Doc1.docx',
containerId: 'editor-1',
lang: 'zh',
});
const manager2 = await createEditorView({
isNew: true,
fileName: 'Doc2.xlsx',
containerId: 'editor-2',
lang: 'zh',
});
const manager3 = await createEditorView({
isNew: true,
fileName: 'Doc3.pptx',
containerId: 'editor-3',
lang: 'zh',
});
// 分别导出
const result1 = await manager1.export();
const result2 = await manager2.export();
const result3 = await manager3.export();
React组件集成(多实例)
// src/app/multi/page.tsx
function MultiInstancePageContent() {
const [managers, setManagers] = useState({
manager1: null,
manager2: null,
manager3: null,
});
// 保存文档信息,用于语言切换
const [editorDocuments, setEditorDocuments] = useState({
manager1: null,
manager2: null,
manager3: null,
});
// 创建编辑器
const handleView = async (editorKey: string, fileName: string, file?: File) => {
const containerId = `editor-${editorKey.replace('manager', '')}`;
const manager = await createEditorView({
file,
fileName,
isNew: !file,
containerId, // 指定容器ID
lang: getOnlyOfficeLang(),
});
setManagers(prev => ({
...prev,
[editorKey]: manager,
}));
// 保存文档信息
setEditorDocuments(prev => ({
...prev,
[editorKey]: { fileName, file: file || undefined },
}));
};
// 语言切换(重新创建所有编辑器)
const handleLanguageSwitch = async () => {
const newLang = currentLang === 'zh' ? 'en' : 'zh';
// 重新创建所有编辑器
if (editorDocuments.manager1) {
const doc = editorDocuments.manager1;
await handleView('manager1', doc.fileName, doc.file);
}
// ... 其他实例
};
return (
<div className="grid grid-cols-3 gap-4">
{/* 编辑器容器 - 使用 data-onlyoffice-container-id 属性 */}
<div className="onlyoffice-container" data-onlyoffice-container-id="editor-1">
<div id="editor-1" className="absolute inset-0" />
</div>
<div className="onlyoffice-container" data-onlyoffice-container-id="editor-2">
<div id="editor-2" className="absolute inset-0" />
</div>
<div className="onlyoffice-container" data-onlyoffice-container-id="editor-3">
<div id="editor-3" className="absolute inset-0" />
</div>
</div>
);
}
项目结构
mvp-onlyoffice/
├── src/
│ ├── app/ # Next.js 应用页面
│ │ ├── excel/ # Excel 编辑器页面
│ │ ├── docs/ # Word 编辑器页面
│ │ ├── ppt/ # PowerPoint 编辑器页面
│ │ └── multi/ # 多实例演示页面
│ ├── onlyoffice-comp/ # OnlyOffice 组件库
│ │ └── lib/
│ │ ├── editor-manager.ts # 编辑器管理器(支持多实例)
│ │ ├── x2t.ts # 文档转换模块
│ │ ├── eventbus.ts # 事件总线
│ │ └── utils.ts # 工具函数
│ └── components/ # 通用组件
├── public/ # 静态资源
│ ├── web-apps/ # OnlyOffice Web 应用资源
│ ├── sdkjs/ # OnlyOffice SDK 资源
│ └── wasm/ # WebAssembly 转换器
└── onlyoffice-x2t-wasm/ # x2t-wasm 源码
部署方案
Vercel一键部署
项目已配置静态导出,可直接部署到Vercel:
# 安装依赖
npm install
# 构建项目
npm run build
# Vercel 会自动检测并部署
🌐 在线演示: mvp-onlyoffice.vercel.app/
静态文件部署
项目支持静态导出,构建后的文件可部署到任何静态托管服务:
# 构建静态文件
npm run build
# 输出目录: out/
# 可直接部署到 GitHub Pages、Netlify、Nginx 等
技术优势总结
| 特性 | 传统方案 | 本方案 |
|---|---|---|
| 数据安全 | ❌ 需要上传服务器 | ✅ 完全本地处理 |
| 部署成本 | ❌ 需要后端服务 | ✅ 纯静态部署 |
| 格式支持 | ⚠️ 有限格式 | ✅ 30+种格式 |
| 离线使用 | ❌ 需要网络 | ✅ 完全离线 |
| 性能优化 | ⚠️ 依赖网络 | ✅ IndexedDB缓存 |
| 国际化 | ⚠️ 需额外配置 | ✅ 内置支持 |
| 权限控制 | ⚠️ 复杂实现 | ✅ 简单API |
| 多实例支持 | ❌ 不支持 | ✅ 原生支持,资源隔离 |
技术原理
使用x2t-wasm替代OnlyOffice服务
传统OnlyOffice集成需要:
- 搭建OnlyOffice Document Server
- 配置文档转换服务
- 处理文档上传下载
- 管理服务器资源
本方案通过WASM技术:
- 在浏览器中直接运行x2t转换引擎
- 使用虚拟文件系统处理文档
- 完全客户端化,无需服务器
多实例架构设计
- 工厂模式:使用
EditorManagerFactory统一管理多个编辑器实例 - 容器隔离:每个实例使用唯一的容器ID,通过
data-onlyoffice-container-id属性精确定位 - 资源隔离:每个实例管理独立的媒体资源映射,图片上传通过独立的
writeFile处理函数 - 事件隔离:虽然使用全局 EventBus,但每个实例的事件处理函数是独立的
参考项目
- Qihoo360/se-office - se-office扩展,提供基于开放标准的全功能办公生产力套件
- cryptpad/onlyoffice-x2t-wasm - CryptPad WebAssembly文件转换工具
- ranuts/document - 参考静态资源实现
开源地址
🔗 GitHub仓库: mvp-onlyoffice
总结
本项目提供了一个完整的纯前端OnlyOffice集成方案,通过WASM技术实现了文档格式转换的本地化,结合React和OnlyOffice SDK,打造了一个功能完善、性能优秀的文档编辑器。
核心亮点:
- 🚀 纯前端架构,无需后端服务
- 🔒 数据完全本地化,保护隐私安全
- ⚡ 基于WASM的高性能转换
- 🌏 内置国际化支持
- 📦 支持导入导出
- 🔐 灵活的权限控制
- 🎯 多实例支持:同时运行多个独立编辑器,资源完全隔离
欢迎Star和Fork,一起推动前端Office编辑技术的发展!
相关阅读:
来源:juejin.cn/post/7575425466904723519
90% 前端都不知道的 20 个「零依赖」浏览器原生能力!
分享 20 个 2025 年依旧「少人知道、却能立竿见影」的原生 API。
收藏 = 省下一个工具库 + 少写 100 行代码!
1. ResizeObserver
精准监听任意 DOM 宽高变化,图表自适应、虚拟滚动必备。
new ResizeObserver(([e]) => chart.resize(e.contentRect.width))
.observe(chartDom);
2. IntersectionObserver
检测元素进出视口,一次搞定懒加载 + 曝光埋点,性能零损耗。
new IntersectionObserver(entrieList =>
entrieList.forEach(e => e.isIntersecting && loadImg(e.target))
).observe(img);
3. Page Visibility
侦测标签页隐藏,自动暂停视频、停止轮询,移动端省电神器。
document.addEventListener('visibilitychange', () =>
document.hidden ? video.pause() : video.play()
);
4. Web Share
一键唤起系统分享面板,直达微信、微博、Telegram,需 HTTPS。
navigator.share?.({ title: '好文', url: location.href });
5. Wake Lock
锁定屏幕常亮,直播、PPT、阅读器不再自动息屏。
await navigator.wakeLock.request('screen');
6. Broadcast Channel
同域标签实时广播消息,登录态秒同步,告别 localStorage 轮询。
const bc = new BroadcastChannel('auth');
bc.onmessage = () => location.reload();
7. PerformanceObserver
无侵入采集 FCP、LCP、FID,一行代码完成前端性能监控。
new PerformanceObserver(list =>
list.getEntries().forEach(sendMetric)
).observe({ type: 'largest-contentful-paint', buffered: true });
8. requestIdleCallback
把埋点、日志丢进浏览器空闲时间,首帧零阻塞。
requestIdleCallback(() => sendBeacon('/log', data));
9. scheduler.postTask
原生优先级任务队列,低优任务后台跑,主线程丝滑。
scheduler.postTask(() => sendBeacon('/log', data), { priority: 'background' });
10. AbortController
随时取消 fetch,路由切换不再旧请求竞态,兼容 100%。
const ac = new AbortController();
fetch(url, { signal: ac.signal });
ac.abort();
11. ReadableStream
分段读取响应流,边下载边渲染,大文件内存零爆涨。
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
appendChunk(value);
}
12. WritableStream
逐块写入磁盘或网络,实时保存草稿、上传大文件更稳。
const writer = stream.writable.getWriter();
await writer.write(chunk);
13. Background Fetch
PWA 后台静默下载,断网恢复继续,课程视频提前缓存。
await registration.backgroundFetch.fetch('video', ['/course.mp4']);
14. File System Access
读写本地真实文件,需用户授权,Web IDE 即开即用。
const [fh] = await showOpenFilePicker();
editor.value = await (await fh.getFile()).text();
15. Clipboard
异步读写剪贴板,无需第三方库,HTTPS 环境安全复制。
await navigator.clipboard.writeText('邀请码 9527');
16. URLSearchParams
解析、修改、构造 URL 查询串,告别手写正则。
const p = new URLSearchParams(location.search);
p.set('page', 2);
history.replaceState({}, '', `?${p}`);
17. structuredClone
深拷贝对象、数组、Map、Date,循环引用也能完美复制。
const copy = structuredClone(state);
18. Intl.NumberFormat
千分位、货币、百分比一次格式化,国际化零配置。
new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' })
.format(1234567); // ¥1,234,567.00
19. EyeDropper
浏览器级吸管工具,像素级取色,设计系统直接调用。
const { sRGBHex } = await new EyeDropper().open();
20. WebCodecs
原生硬解码音视频,4K 60 帧流畅播放,CPU 占用直降。
const decoder = new VideoDecoder({
output: frame => ctx.drawImage(frame, 0, 0),
error: console.error
});
decoder.configure({ codec: 'vp09.00.10.08' });
来源:juejin.cn/post/7545294766975615015
写给小公司前端的 UI 规范
写给小公司前端的 UI 规范
大部分小公司前端开发是不是都会有个困扰,一天到晚做的都是后台管理系统,而且百分之 80 都是表格的增删改查,导致领导觉得前端简单的很,所以连个 UI 都没有,但是每次写完页面又被老大吐槽没有审美,而且如果整个系统多个人开发,每个的风格都不一样,导致整个系统看起来很乱,想要统一又不知道从何入手,所以今天我给大家分享一下我们团队的针对后台管理系统的 UI 规范,希望对大家有帮助。
叠个甲:这个只是我们遵循的规范,因为交互和设计这种东西每个人的感受不一样,你觉得你这个规范我觉得不合理,没关系,你可以按照自己的想法来也可以,只要遵循一个原则,那就是整个系统风格保持一致性即可,所以这个规范仅供参考。
大家都知道后台管理主要就是几部分:表格、表单、弹窗,所以我们的 UI 规范也是围绕这三部分来的。
表格
自适应形式
- 最小页面宽度+自适应的综合运用,最小自适应页面宽度 1366px(小于此宽度则不再自适应,超出页面的内容使用滚动条查看),单元格字段过长一行展示不下时,不换行并出现省略号,鼠标移入,提示框显示完整字段。

表格内行高
| 场景 | 字号 | 行高 | 适用情况 |
|---|---|---|---|
| 紧凑模式 | 12px | 42px | 数据量大的表格 |
| 标准模式 | 14px | 48px | 默认推荐 |
| 大字号模式 | 16px | 56px | 无障碍/老年版 |

表格内对齐方式
- 表头和文字内容:采用左对齐
- 表头和普通数字:采用左对齐
- 表头和具有比较场景的数字: 采用右对齐
- 表头和操作项: 采用左对齐

分页器
分页器元素:
- 数据总量、单页面展示数量、翻页部分
对齐方式:
- 数据总量左对齐,单页面展示数量&翻页部分右对齐(依次顺序为数据总量、单页面展示数量、翻页部分)
分页器位置:
- 表格为页面全部内容时,表格超出一页分页器固定及底,表格未超出一页分页器跟随表格下方
- 表格仅为页面部分内容时,分页器跟随表格下方

表格操作区
按钮类型:
- 新增数据类按钮 &面向已有数据类的操作按钮(包含可合并类操作按钮)。
对齐方式:
- 操作区居右,主按钮居右。
视觉样式:
- 新增数据类按钮样式-【文字+主色底】或【文字+图标+主色底文字+中性色线框】
- 面向已有数据类的操作按钮样式-【文字+主色底】或【文字+图标+主色底文字+中性色线框】
按钮个数:
- 小于等于 4 个全部展示
- 大于四个时候,最后一个按钮为【更多】按钮,点击【更多】按钮后,下拉展示全部按钮

表格筛选区域
- 一行最多四个筛选条件,不超过四个的时候查询按钮和重置按钮跟在筛选项后
- 超过四个筛选条件时,出现【展示更多】按钮,点击【展示更多】按钮后,下拉展示全部筛选条件,【展示更多】变成【收起更多】按钮
- 如果是时间范围 比如开始时间和结束时间 他独占两个筛选项
- 默认 label 最多四个字 建议两个字或者四个字
- 筛选项的宽度建议 200px

表格滑块
表格为页面全部内容时:
- 内容无超出:滚动不出现
- 横向内容有超出:表格内横向滚动条,且默认固定操作和标题列
- 纵向内容有超出:页面滚动条,表头到达页面顶部,吸顶固定
表格仅为页面部分内容时:
- 表格区最大高度为 10 行数据+分页器,当 10 行数据+分内器超出页面容器时,表格区最大高度将限制为页面容器的 90%
- 内容无超出:滚动不出现
- 横向内容有超出:表格内横向滚动条,且默认固定操作项和标题列
- 纵向内容有超出:表格内滚动条,固定表头&分页器


表单
表单项 Label 与控件的对齐方式&必填标识
- 表单项 Label 和控件对齐方式
- 当表单项 Label 过长(6 个中文字符以上),采用顶对齐方式布局。当表单项 Label 在 6 个中文字符以内,使用右对齐方式布局。
对齐布局; - 当表单项在 15 个以下时,采用单列布局方式,当表单项在 15 个以上时,采用多列顶对齐方式布局。
- 表单项 Label 和必填标识对齐方式
- 表单项 Label 右对齐布局时,必填示识放在标题前;
- 表单项 Label 顶对齐布局时,必填示识放在标题后;

提交/取消操作按钮位置&对齐方式
操作按钮统一采用左对齐的方式,表单域未超出一屏时跟在表单项下方,若超出一屏则固定吸底。
表单页自适应方式&lnput 框长度
- 当只有单列时,表单域左对齐且定宽 、输入框&选择框长度为 480px、文本框长度为 640px,支持表单项 Label 顶对齐和右对齐。
- 当出现双列时,表单域左对齐,表单域宽度为页面容器宽度的 80%,自适应布局,仅支持表单项 Label 顶对齐。
- 当出现三列时,表单域占满整个页面容器,自适应布局,仅支持表单项 Label 顶对齐。

弹窗
弹窗尺寸:
- 在未达到弹窗最大尺寸(弹窗最大宽高<页面宽高的 80%)时,弾窗尺寸由内容决定,弹窗内容距离弹窗边距 20px。
- 滚动条出现在弹窗上,不要出现在整个页面上
弹窗位置:
- 页面居中。
操作按钮位置:
- 固定在弹窗底部,整体居左,主按钮在左侧。


总 结
我之前也在小公司待过,能充分了解小公司的现状,别说 UI 了,有的直接是连前端都不要,管你什么 UI,时间长了就麻木了,想进步却找不到方向,所以希望这个规范能帮助到你,让你在 UI 规范方面有所提升。
因为只是我们总结的一套规范,其实还有很多不足,你完全可以在这个基础上,根据自己的需求,进行修改和优化,比如你觉得表格删除按钮必须是红色,弹窗的按钮放在右下角更合理,不喜欢筛选项有什么展开收缩等等这些你都可以根据自己的需求进行修改,还是那句话,整个系统风格保持统一。
如果大家有自己团队的规范,也可以评论区发出来共享一下大家相互学习一下。

来源:juejin.cn/post/7521013717439315994
图片标签用 img 还是 picture?很多人彻底弄混了!
在网页开发中,图片处理是每个前端开发者都会遇到的基础任务。面对 <img> 和 <picture> 这两个标签,很多人存在误解:要么认为它们是互相替代的关系,要么在不合适的场景下使用了复杂的解决方案。今天,我们来彻底理清这两个标签的真正用途。
<img> 标签
<img> 是 HTML 中最基础且强大的图片标签,但它远比很多人想象的要智能。
基本语法:
<img src="image.jpg" alt="图片描述">
核心属性:
src:图片路径(必需)alt:替代文本(无障碍必需)srcset:提供多分辨率图片源sizes:定义图片显示尺寸loading:懒加载控制
<img> 的响应式能力被低估了
很多人认为 <img> 不具备响应式能力,这是错误的认知:
<img
src="image-800w.jpg"
srcset="image-320w.jpg 320w,
image-480w.jpg 480w,
image-800w.jpg 800w"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
alt="响应式图片示例"
>
这种写法的优势:
- 浏览器自动选择最适合当前屏幕分辨率的图片
- 根据视口大小动态调整加载的图片尺寸
- 代码简洁,性能优秀
<picture> 标签
<picture> 不是为了替代 <img>,而是为了解决 <img> 无法处理的特定场景。
<picture> 解决的三大核心问题
1. 艺术指导(Art Direction)
在不同设备上显示不同构图或裁剪的图片:
<picture>
<!-- 桌面端:宽屏全景 -->
<source media="(min-width: 1200px)" srcset="hero-desktop.jpg">
<!-- 平板端:适中裁剪 -->
<source media="(min-width: 768px)" srcset="hero-tablet.jpg">
<!-- 移动端:竖版特写 -->
<img src="hero-mobile.jpg" alt="产品展示">
</picture>
2. 现代格式降级
优先使用高效格式,同时兼容老旧浏览器:
<picture>
<source type="image/avif" srcset="image.avif">
<source type="image/webp" srcset="image.webp">
<img src="image.jpg" alt="格式优化示例">
</picture>
3. 复杂条件组合
同时考虑屏幕尺寸和图片格式:
<picture>
<!-- 大屏 + AVIF -->
<source media="(min-width: 1200px)" type="image/avif" srcset="large.avif">
<!-- 大屏 + WebP -->
<source media="(min-width: 1200px)" type="image/webp" srcset="large.webp">
<!-- 大屏降级 -->
<source media="(min-width: 1200px)" srcset="large.jpg">
<!-- 移动端方案 -->
<img src="small.jpg" alt="复杂条件图片">
</picture>
关键区别与选择指南
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 同一图片,不同分辨率 | <img> + srcset + sizes | 代码简洁,浏览器自动优化 |
| 不同构图或裁剪 | <picture> | 艺术指导必需 |
| 现代格式兼容 | <picture> | 格式降级必需 |
| 简单静态图片 | <img> | 无需复杂功能 |
| 兼容老旧浏览器 | <img> | 最广泛支持 |
常见误区纠正
误区一:<picture> 用于响应式图片
- 事实:
<img>配合srcset和sizes已经能处理大多数响应式需求 - 真相:
<picture>主要用于艺术指导和格式降级
误区二:<picture> 更现代,应该优先使用
- 事实: 在不需要艺术指导或格式降级的场景下,
<img>是更好的选择 - 真相: 合适的工具用在合适的场景才是最佳实践
误区三:响应式图片一定要用 <picture>
- 事实: 很多响应式场景用
<img>+srcset更合适 - 真相: 评估需求,选择最简单的解决方案
场景分析
应该使用 <img> 的场景
网站Logo:
<img src="logo.svg" alt="公司Logo" width="120" height="60">
用户头像:
<img
src="avatar.jpg"
srcset="avatar.jpg 1x, avatar@2x.jpg 2x"
alt="用户头像"
width="80"
height="80"
>
文章配图:
<img
src="article-image.jpg"
srcset="article-image-600w.jpg 600w,
article-image-1200w.jpg 1200w"
sizes="(max-width: 768px) 100vw, 600px"
alt="文章插图"
loading="lazy"
>
应该使用 <picture> 的场景
英雄横幅(不同裁剪):
<picture>
<source media="(min-width: 1024px)" srcset="hero-wide.jpg">
<source media="(min-width: 768px)" srcset="hero-square.jpg">
<img src="hero-mobile.jpg" alt="产品横幅" loading="eager">
</picture>
产品展示(格式优化):
<picture>
<source type="image/avif" srcset="product.avif">
<source type="image/webp" srcset="product.webp">
<img src="product.jpg" alt="产品详情" loading="lazy">
</picture>
最佳实践
1. 始终遵循的规则
<!-- 正确:始终提供 alt 属性 -->
<img src="photo.jpg" alt="描述文本">
<!-- 错误:缺少 alt 属性 -->
<img src="photo.jpg">
<!-- 装饰性图片使用空 alt -->
<img src="decoration.jpg" alt="">
2. 性能优化策略
<!-- 优先加载关键图片 -->
<img src="hero.jpg" alt="重要图片" loading="eager" fetchpriority="high">
<!-- 非关键图片延迟加载 -->
<img src="content-image.jpg" alt="内容图片" loading="lazy">
<!-- 指定尺寸避免布局偏移 -->
<img src="product.jpg" alt="商品" width="400" height="300">
3. 现代图片格式策略
<picture>
<!-- 优先使用AVIF,压缩率最高 -->
<source type="image/avif" srcset="image.avif">
<!-- 其次WebP,广泛支持 -->
<source type="image/webp" srcset="image.webp">
<!-- 最终回退到JPEG -->
<img src="image.jpg" alt="现代格式示例">
</picture>
总结
<img> 和 <picture> 不是竞争关系,而是互补的工具:
<img>:处理大多数日常图片需求,特别是分辨率适配<picture>:解决特定复杂场景,如艺术指导和格式降级
核心建议:
- 从最简单的
<img>开始,只在必要时升级到<picture> - 充分利用
<img>的srcset和sizes属性 - 为关键图片使用
<picture>进行格式优化 - 始终考虑性能和用户体验
掌握这两个标签的正确用法,你就能在各种场景下都做出最合适的技术选择,既保证用户体验,又避免过度工程化。
希望这篇指南能帮助你彻底理解这两个重要的HTML标签!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot+Vue3 整合 SSE 实现实时消息推送》
《SpringBoot 动态菜单权限系统设计的企业级解决方案》
来源:juejin.cn/post/7577298871005036578
javascript新进展你关注了吗:TC39 东京会议带来五大新特性
上周,JavaScript 语言的掌舵者们齐聚东京。在 Sony Interactive Entertainment 的主持下,Ecma 国际 TC39 委员会召开了第 104 次全体会议,一系列围绕 迭代器(Iterator) 和 Promise 的提案取得了重要进展。这些新特性将让 JavaScript 开发者写出更简洁、更函数式的代码。
让我们一起来看看这些即将改变你编码方式的新特性。
科普:TC39 与提案流程
在深入了解新特性之前,我们先来快速了解一下它们背后的组织和流程。
TC39 是什么?
TC39 (Technical Committee 39) 是 Ecma International 旗下的技术委员会,专门负责 ECMAScript 标准(即 JavaScript 的规范)的制定和维护。其成员由主流浏览器厂商(如 Google, Mozilla, Apple, Microsoft)以及其他对 Web 技术有影响力的科技公司和专家组成。
提案如何成为标准?
一个新特性从提出到最终进入标准,通常需要经历 5 个阶段(The TC39 Process):
- Stage 0 (Strawman - 稻草人): 任何想法或建议,用于开启讨论。
- Stage 1 (Proposal - 提案): 确定问题和解决方案,展示潜在的 API 形式。
- Stage 2 (Draft - 草稿): 具体的语法和语义描述,此时特性已基本定型。
- Stage 3 (Candidate - 候选): 规范文本已完成,需要浏览器厂商的实现反馈和用户测试。
- Stage 4 (Finished - 完成): 至少有两个独立的浏览器实现并通过了测试,准备纳入下一版 ECMAScript 标准。
大概需要多久?
这个过程没有固定的时间表,取决于提案的复杂度和争议程度:
- 简单特性:可能在 1-2 年内走完流程。
- 复杂特性:可能需要在 Stage 2 或 Stage 3 停留数年(例如 Temporal 提案)。
通常来说,一旦提案进入 Stage 3,就意味着它极有可能在不久的将来成为标准的一部分,各大浏览器也会开始逐步支持。
1. Iterator Sequencing(迭代器序列化)— Stage 3
提案地址: proposal-iterator-sequencing
解决什么问题?
你是否曾经需要把多个迭代器"串联"起来,当作一个来用?在以前,你得用生成器函数配合 yield* 来实现:
let lows = Iterator.from([0, 1, 2, 3]);
let highs = Iterator.from([6, 7, 8, 9]);
// 以前的写法,略显繁琐
let combined = function* () {
yield* lows;
yield* highs;
}();
Array.from(combined); // [0, 1, 2, 3, 6, 7, 8, 9]
新写法
现在只需一行:
let combined = Iterator.concat(lows, highs);
Array.from(combined); // [0, 1, 2, 3, 6, 7, 8, 9]
// 还可以在中间插入其他值
let digits = Iterator.concat(lows, [4, 5], highs);
Array.from(digits); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Iterator.concat() 接受多个可迭代对象,惰性地按顺序产出所有值。这不仅代码更简洁,而且因为是惰性求值,处理大数据集时内存效率也更高。
2. Await Dictionary of Promises(Promise 字典等待)— Stage 1
提案地址: proposal-await-dictionary
解决什么问题?
当你需要并发等待多个 Promise 时,Promise.all 返回的是数组,你得靠下标来取值:
const [shape, color, mass] = await Promise.all([
getShape(),
getColor(),
getMass(),
]);
// 顺序错了?很容易出 bug
新写法
使用 Promise.allKeyed(),用对象的键来命名你的结果:
const { shape, color, mass } = await Promise.allKeyed({
shape: getShape(),
color: getColor(),
mass: getMass(),
});
// 再也不用担心顺序问题了!
该提案还支持 Promise.allSettledKeyed(),用于处理可能失败的场景:
const results = await Promise.allSettledKeyed({
shape: getShape(),
color: getColor(),
mass: getMass(),
});
if (results.shape.status === "fulfilled") {
console.log(results.shape.value);
} else {
console.error(results.shape.reason);
}
这个 API 设计有意与 Iterator.zipKeyed() 保持一致,体现了 TC39 在语言设计上的系统性思考。
3. Joint Iteration(联合迭代)— Stage 2.7
提案地址: proposal-joint-iteration
解决什么问题?
Python 开发者对 zip() 函数再熟悉不过了——它能把多个序列"拉链式"地组合在一起。JavaScript 一直缺少这个功能。
新写法
Iterator.zip() 和 Iterator.zipKeyed() 填补了这个空白:
// 位置对应的组合
Array.from(Iterator.zip([
[0, 1, 2],
[3, 4, 5],
]))
// 结果: [[0, 3], [1, 4], [2, 5]]
// 用键名组合
Iterator.zipKeyed({
a: [0, 1, 2],
b: [3, 4, 5, 6],
c: [7, 8, 9],
}).toArray()
// 结果: [
// { a: 0, b: 3, c: 7 },
// { a: 1, b: 4, c: 8 },
// { a: 2, b: 5, c: 9 }
// ]
还支持三种模式来处理长度不等的迭代器:
mode: 'shortest'(默认):最短的迭代器耗尽时停止mode: 'longest':最长的迭代器耗尽时停止,可以指定填充值mode: 'strict':如果长度不等则抛出错误
Iterator.zipKeyed({
a: [0, 1, 2],
b: [3, 4, 5, 6],
c: [7, 8, 9],
}, {
mode: 'longest',
padding: { c: 10 },
}).toArray()
// 结果包含: { a: undefined, b: 6, c: 10 }
4. Iterator Join(迭代器连接)— 新提案
提案地址: proposal-iterator-join
解决什么问题?
Array.prototype.join() 能把数组元素用分隔符连接成字符串。但如果你有一个迭代器呢?现在你得先转成数组:
myIterator.toArray().join(', ') // 先全部加载到内存
新写法
Iterator.prototype.join() 让你直接在迭代器上操作:
Iterator.from(['a', 'b', 'c']).join(', ')
// 结果: "a, b, c"
这对于处理大型或无限序列(只取部分)时特别有用,避免了不必要的内存分配。
5. Typed Array Find Within(类型数组范围查找)
解决什么问题?
现有的 TypedArray.prototype.indexOf() 只能从数组开头或指定位置向后搜索。当你需要在特定范围内查找时,现有 API 不够灵活。
新能力
这个提案为 TypedArray 添加了在指定范围内进行查找的能力,让二进制数据处理更加高效。这对于处理音视频数据、WebGL 缓冲区、以及其他需要精确控制搜索范围的场景非常有用。
迭代器生态的完善
如果你一直关注 TC39 的动态,你会发现这次会议推进的提案有一个共同主题:完善 JavaScript 的迭代器生态。
| 提案 | 功能 | 状态 |
|---|---|---|
| Iterator Helpers | map, filter, take, drop 等 | ✅ Stage 4(已标准化) |
| Iterator Sequencing | concat | Stage 3 |
| Joint Iteration | zip, zipKeyed | Stage 2.7 |
| Iterator Join | join | 新提案 |
| Await Dictionary | Promise.allKeyed | Stage 1 |
参考链接:
来源:juejin.cn/post/7577612585845194761
弃用 html2canvas!快 93 倍的截图神器
在前端开发中,网页截图是个常用功能。从前,html2canvas 是大家的常客,但随着网页越来越复杂,它的性能问题也逐渐暴露,速度慢、占资源,用户体验不尽如人意。
好在,现在有了 SnapDOM,一款性能超棒、还原度超高的截图新秀,能完美替代 html2canvas,让截图不再是麻烦事。

什么是 SnapDOM
SnapDOM 就是一个专门用来给网页元素截图的工具。

它能把 HTML 元素快速又准确地存成各种图片格式,像 SVG、PNG、JPG、WebP 等等,还支持导出为 Canvas 元素。

它最厉害的地方在于,能把网页上的各种复杂元素,比如 CSS 样式、伪元素、Shadow DOM、内嵌字体、背景图片,甚至是动态效果的当前状态,都原原本本地截下来,跟直接看网页没啥两样。
SnapDOM 优势
快得飞起
测试数据显示,在不同场景下,SnapDOM 都把 html2canvas 和 dom-to-image 这俩老前辈远远甩在身后。

尤其在超大元素(4000×2000)截图时,速度是 html2canvas 的 93.31 倍,比 dom-to-image 快了 133.12 倍。这速度,简直就像坐火箭。
还原度超高
SnapDOM 截图出来的效果,跟在网页上看到的一模一样。
各种复杂的 CSS 样式、伪元素、Shadow DOM、内嵌字体、背景图片,还有动态效果的当前状态,都能精准还原。

无论是简单的元素,还是复杂的网页布局,它都能轻松拿捏。
格式任你选
不管你是想要矢量图 SVG,还是常用的 PNG、JPG,或者现代化的 WebP,又或者是需要进一步处理的 Canvas 元素,SnapDOM 都能满足你。

多种格式,任你挑选,适配各种需求。
三、怎么用 SnapDOM
安装
SnapDOM 的安装超简单,有好几种方式:
用 NPM 或 Yarn:在命令行里输
# npm
npm i @zumer/snapdom
# yarn
yarn add @zumer/snapdom
就能装好。
用 CDN 在 HTML 文件里加一行:
<script src="https://unpkg.com/@zumer/snapdom@latest/dist/snapdom.min.js"></script>
直接就能用。
要是项目里用的是 ES Module:
import { snapdom } from '@zumer/snapdom
基础用法示例
一键截图
const card = document.querySelector('.user-card');
const image = await snapdom.toPng(card);
document.body.appendChild(image);
这段代码就是找个元素,然后直接截成 PNG 图片,再把图片加到页面上。简单粗暴,一步到位。
高级配置
const element = document.querySelector('.chart-container');
const capture = await snapdom(element, {
scale: 2,
backgroundColor: '#fff',
embedFonts: true,
compress: true
});
const png = await capture.toPng();
const jpg = await capture.toJpg({ quality: 0.9 });
await capture.download({
format: 'png',
filename: 'chart-report-2024'
});
这儿可以对截图进行各种配置。比如 scale 能调整清晰度,backgroundColor 能设置背景色,embedFonts 可以内嵌字体,compress 能压缩优化。配置好后,还能把截图存成不同格式,或者直接下载到本地。
和其他库比咋样
和 html2canvas、dom-to-image 比起来,SnapDOM 的优势很明显:
| 特性 | SnapDOM | html2canvas | dom-to-image |
|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
| 准确度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 文件大小 | 极小 | 较大 | 中等 |
| 依赖 | 无 | 无 | 无 |
| SVG 支持 | ✅ | ❌ | ✅ |
| Shadow DOM 支持 | ✅ | ❌ | ❌ |
| 维护状态 | 活跃 | 活跃 | 停滞 |
五、用的时候注意点
用 SnapDOM 时,有几点得注意:
跨域资源
要是截图里有外部图片等跨域资源,得确保这些资源支持 CORS,不然截不出来。
iframe 限制
SnapDOM 不能截 iframe 内容,这是浏览器的安全限制,没办法。
Safari 浏览器兼容性
在 Safari 里用 WebP 格式时,会自动变成 PNG。
大型页面截图
截超大页面时,建议分块截,不然可能会内存溢出。
六、SnapDOM 能干啥及代码示例
社交分享
async function shareAchievement() {
const card = document.querySelector('.achievement-card');
const image = await snapdom.toPng(card, { scale: 2 });
navigator.share({
files: [new File([await snapdom.toBlob(card)], 'achievement.png')],
title: '我获得了新成就!'
});
}
报表导出
async function exportReport() {
const reportSection = document.querySelector('.report-section');
await preCache(reportSection);
await snapdom.download(reportSection, {
format: 'png',
scale: 2,
filename: `report-${new Date().toISOString().split('T')[0]}`
});
}
海报导出
async function generatePoster(productData) {
document.querySelector('.poster-title').textContent = productData.name;
document.querySelector('.poster-price').textContent = `¥${productData.price}`;
document.querySelector('.poster-image').src = productData.image;
await new Promise((resolve) => setTimeout(resolve, 100));
const poster = document.querySelector('.poster-container');
const blob = await snapdom.toBlob(poster, { scale: 3 });
return blob;
}
写在最后
SnapDOM 就是这么一款简单、快速、准确,还零依赖的网页截图神器。
无论是社交分享、报表导出、设计保存,还是营销推广,它都能轻松搞定。
而且它是免费开源的,背后还有活跃的社区支持。要是你还在为网页截图的事儿发愁,赶紧试试 SnapDOM 吧。
要是你在用 SnapDOM 的过程中有啥疑问,或者碰上啥问题,可以去下面这些地方找答案:
- 项目地址 :github.com/zumerlab/sn…
- 在线演示 :zumerlab.github.io/snapdom/
- 详细文档 :github.com/zumerlab/sn…
来源:juejin.cn/post/7544287909475090451
有Trae和通义灵码了为什么还要付费Cursor?
去年的时候AI编程还是在ChatGPT上贴代码问答,经常受限于上下文的长度,今年就用上了Cursor,并作为主力开发工具了。
Trae刚出来那会儿第一时间体验了,那时候还只有国际版。刚给它提个任务,居然还要等待,前面还有几十个请求。你可是个生产力工具呀,太败好感了。后面就果断付费了Cursor。第二次败好感的是Trae国内版出来后,铺天盖地的软文广告。作为一款生产力工具,第一件事不是把产品打磨好吗?
通义灵码主打安全以及和阿里云的集成,公司也在推广,于是安装体验了一下,当时还不够智能修改代码还是全文件一行一行慢慢改,插件的体验还是没有原生的好。问了一个在阿里的朋友AI编程的情况,他只用过GitHub Copilot,没用过通义灵码。
国内最看好的就是它俩了,期望越做越好,能95%替代Cursor时,付费也愿意。
Cursor一开始我也是免费体验,额定用完了,换了一个账号。Cursor使用率挺高,又没找到合适的替代。生产力工具,效率第一,免费的才是最贵的,果断买了一年。各种AI工具,智能体层出不穷。Cursor+Claude-3.7-sonnet依然是我认为当下最佳组合(明天估计就得是claude-4.0了吧)
gemini-2.5-pro是又快又强,经常让它俩CodeReview。ChatGPT会用来做方案的设计,代码的架构,过程沟通等。用了Cursor后就把jetbrains全家桶给卸载了,Cursor每1-2周都会有版本的更新,时不时会有一些小惊喜的功能,也是愿意一直付费的原因。
说一下Cursor的坑,第一个就是购买,网上有虚拟信用卡的也有免费无限续杯的,我建议是用银联信用卡直接支付,安全省事。第二个坑就是贵,500次完全不够用,用完了可以用慢速,慢速里gemini-2.5-pro不错。遇到复杂的问题提供好上下文,参考文档。这次有个问题修复不了指望开max,用o3能解决的,结果几个模型用了个遍都没有解决,还得靠人。o3是真贵呀,想想都心疼。

最后,再说一句,有免费的工具为什么还要付费?生产力工具,提升生产力是第一要考虑的,免费往往会是最贵的。付费了Cursor后用它帮我写前端、后端、算法炼丹都能到80分以上,不仅仅是代替自己写节省时间,而是它是全能的,可以比自己写得更好。整体感觉,一个字,值!
来源:juejin.cn/post/7507088558889517093
弃用 uni-app!Vue3 的原生 App 开发框架来了!
长久以来,"用 Vue 3 写真正的原生 App" 一直是块短板。
uni-app 虽然"一套代码多端运行",但性能瓶颈、厂商锁仓、原生能力羸弱的问题常被开发者诟病。
整个 Vue 生态始终缺少一个能与 React Native 并肩的"真·原生"跨平台方案
直到 NativeScript-Vue 3 的横空出世,并被 尤雨溪 亲自点赞。

为什么是时候说 goodbye 了?
| uni-app 现状 | 开发者痛点 |
|---|---|
| 渲染层基于 WebView 或弱原生混合 | 启动慢、掉帧、长列表卡顿 |
自定义原生 SDK 需写大量 renderjs / plus 桥接 | 维护成本高,升级易断裂 |
| 锁定 DCloud 生态 | 工程化、Vite、Pinia 等新工具跟进慢 |
| Vue 3 支持姗姗来迟,Composition API 兼容碎裂 | 类型推断、生态插件处处踩坑 |
"我们只是想要一个 Vue 语法 + 真原生渲染 + 社区插件开箱即用 的解决方案。"
—— 这,正是 NativeScript-Vue 给出的答案。
尤雨溪推特背书
2025-10-08,Evan You 转发 NativeScript 官方推文:
"Try Vite + NativeScript-Vue today —
HMR,native APIs,live reload."

配图是一段 <script setup> + TypeScript 的实战 Demo,意味着:
- 真正的 Vue 3 语法(
Composition API) - Vite 秒级热重载
- 直接调用 iOS / Android 原生 API
获创始人的公开推荐,无疑给社区打了一剂强心针。
NativeScript-Vue 是什么?
一句话:Vue 的自定义渲染器 + NativeScript 原生引擎

- 运行时 没有 WebView,JS 在
V8 / JavaScriptCore中执行 <template>标签 → 原生UILabel/android.widget.TextView- 支持 NPM、CocoaPods、Maven/Gradle 全部原生依赖
- 与 React Native 同级别的性能,却拥有 Vue 完整开发体验
5 分钟极速上手
1. 环境配置(一次过)
# Node ≥ 18
npm i -g nativescript
ns doctor # 按提示安装 JDK / Android Studio / Xcode
# 全部绿灯即可
2. 创建项目
ns create myApp \
--template @nativescript-vue/template-blank-vue3@latest
cd myApp
模板已集成 Vite + Vue3 + TS + ESLint
3. 运行 & 调试
# 真机 / 模拟器随你选
ns run ios
ns run android
保存文件 → 毫秒级 HMR,console.log 直接输出到终端。
4. 目录速览
myApp/
├─ app/
│ ├─ components/ // 单文件 .vue
│ ├─ app.ts // createApp()
│ └─ stores/ // Pinia 状态库
├─ App_Resources/
└─ vite.config.ts // 已配置 nativescript-vue-vite-plugin
5. 打包上线
ns build android --release # 生成 .aab / .apk
ns build ios --release # 生成 .ipa
签名、渠道、自动版本号——标准原生流程,CI 友好。
Vue 3 生态插件兼容性一览
| 插件 | 是否可用 | 说明 |
|---|---|---|
| Pinia | ✅ | 零改动,app.use(createPinia()) |
| VueUse | ⚠️ | 仅无 DOM 的 Utilities 可用 |
| vue-i18n 9.x | ✅ | 实测正常 |
| Vue Router | ❌ | 官方推荐用 NativeScript 帧导航 → $navigateTo(Page) |
| Vuetify / Element Plus | ❌ | 依赖 CSS & DOM,无法渲染 |
检测小技巧:
npm i xxx
grep -r "document\|window\|HTMLElement" node_modules/xxx || echo "大概率安全"
调试神器:Vue DevTools 支持
NativeScript-Vue 3 已提供 官方 DevTools 插件
组件树、Props、Events、Pinia状态 实时查看- 沿用桌面端调试习惯,无需额外学习成本
👉 配置指南:https://nativescript-vue.org/docs/essentials/vue-devtools
插件生态 & 原生能力
- 700+
NativeScript官方插件
ns plugin add @nativescript/camera | bluetooth | sqlite... - iOS/Android SDK 直接引入
CocoaPods/Maven一行配置即可:
// 调用原生 CoreBluetooth
import { CBCentralManager } from '@nativescript/core'
- 自定义 View & 动画
注册即可在<template>使用,与 React Native 造组件体验一致。
结语:这一次,Vue 开发者不再低人一等
React Native 有 Facebook 撑腰,Flutter 有 Google 背书,
现在 Vue 3 也有了自己的 真·原生跨平台答案 —— NativeScript-Vue。
它让 Vue 语法第一次 完整、无损、高性能 地跑在 iOS & Android 上,
并获得 尤雨溪 公开点赞与 Vite 官方生态加持。
弃用 uni-app,拥抱 NativeScript-Vue,
让 性能、原生能力、工程化 三者兼得,
用你最爱的 .vue 文件,写最硬核的移动应用!
🔖 一键直达资源
来源:juejin.cn/post/7560510073950011435
生活艰难,格格不入

“Life is a soup. And I’m a fork.”
前端实时推送(券商行业必看) & WebSocket 原理解析
一、历史背景 + 时间轴
网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新;
- 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;
- 再加长轮询,试图把“有了新数据再说”变成一种伪推送,却仍困在请求—响应的笼子里。
- 开发者于是继续前探:让连接不再频繁重建,尝试分块直输,把事件像水一样持续送达,于是有了更顺滑的 Streaming 与标准化的 SSE。
直到某一刻,我们不再满足于“更聪明的单向”,而是迈向真正的“同时说话与倾听”——WebSocket 把通信从一次次请求,变成一条持久而通透的通道。此后,
- HTTP/2、HTTP/3 与 QUIC 又在底层为效率和时延开了绿灯,甚至提供了可选可靠与无序传输的更多可能。
接下来,我们就沿着这条主线,层层展开:它们各自解决了什么、在哪些场景最合拍、又如何在你的系统里形成清晰的选型边界。

网页一旦需要 “实时” ,麻烦就开始了:数据在不断变化,用户却只能等下一次刷新;
- 刷新解决不了的延迟,用短轮询凑数,又被无数空请求反噬;
- 再加长轮询,试图把“有了新数据再说”变成一种伪推送,却仍困在请求—响应的笼子里。
- 开发者于是继续前探:让连接不再频繁重建,尝试分块直输,把事件像水一样持续送达,于是有了更顺滑的 Streaming 与标准化的 SSE。
直到某一刻,我们不再满足于“更聪明的单向”,而是迈向真正的“同时说话与倾听”——WebSocket 把通信从一次次请求,变成一条持久而通透的通道。此后,
- HTTP/2、HTTP/3 与 QUIC 又在底层为效率和时延开了绿灯,甚至提供了可选可靠与无序传输的更多可能。
接下来,我们就沿着这条主线,层层展开:它们各自解决了什么、在哪些场景最合拍、又如何在你的系统里形成清晰的选型边界。

01|从整页刷新出发:减少浪费的一条链路
这一块是为了解决“整页刷新导致的高延迟与带宽浪费”,逐级细化与优化。
这一块是为了解决“整页刷新导致的高延迟与带宽浪费”,逐级细化与优化。
a. 早期网页:整页刷新
- 背景与问题:每次更新都整页请求,体验割裂、带宽浪费、延迟高。
- 直接影响:促使前端与服务端思考“只取变化”。
- 背景与问题:每次更新都整页请求,体验割裂、带宽浪费、延迟高。
- 直接影响:促使前端与服务端思考“只取变化”。
b. 短轮询(Short Polling)为解决整页刷新的低效
- 解决:改为“隔一段时间拉一次”,显著减少整页重载带来的浪费。
- 局限:高频请求带来大量空响应与服务器开销。
- 承接改进:为减少空转,演进到长轮询;同时催生更流式的思路(Streaming/SSE)。
- 解决:改为“隔一段时间拉一次”,显著减少整页重载带来的浪费。
- 局限:高频请求带来大量空响应与服务器开销。
- 承接改进:为减少空转,演进到长轮询;同时催生更流式的思路(Streaming/SSE)。
c. 长轮询(Comet/挂起请求)为减少短轮询的空转
- 解决:请求挂起,服务器有新数据才返回,接近“伪推送”,显著降低空转。
- 局限:本质仍是请求-响应;连接频繁重建;难做真正双向。
- 承接改进:
- 单向推送更稳:SSE 标准化单向事件流。
- 若要真双向与二进制:交给 WebSocket(见独立块)。
- 解决:请求挂起,服务器有新数据才返回,接近“伪推送”,显著降低空转。
- 局限:本质仍是请求-响应;连接频繁重建;难做真正双向。
- 承接改进:
- 单向推送更稳:SSE 标准化单向事件流。
- 若要真双向与二进制:交给 WebSocket(见独立块)。
d. HTTP Streaming(分块传输/持续输出)为进一步降低重连与延迟
- 解决:保持连接,分块持续输出,适合连续文本/事件流,重连更少、延迟更低。
- 局限:多为单向,受代理/中间件影响,兼容性不一。
- 承接改进:单向事件由 SSE 标准化;双向场景仍需 WebSocket。
- 解决:保持连接,分块持续输出,适合连续文本/事件流,重连更少、延迟更低。
- 局限:多为单向,受代理/中间件影响,兼容性不一。
- 承接改进:单向事件由 SSE 标准化;双向场景仍需 WebSocket。
e. SSE(Server-Sent Events)单向推送的标准化终点
- 解决:以标准事件流语义提供单向推送,浏览器原生支持,资源占用低。
- 适配范围:通知、进度、日志/监控等文本或事件流。
- 位置关系:在“只需单向推送”的场景中,SSE 是这一链条的稳定落点,而非过渡技术。
- 解决:以标准事件流语义提供单向推送,浏览器原生支持,资源占用低。
- 适配范围:通知、进度、日志/监控等文本或事件流。
- 位置关系:在“只需单向推送”的场景中,SSE 是这一链条的稳定落点,而非过渡技术。
02|范式跃迁:WebSocket(独立大块)
这不是前面链条的“又一改良”,而是从请求-响应转向全双工持久连接的范式变化。
这不是前面链条的“又一改良”,而是从请求-响应转向全双工持久连接的范式变化。
WebSocket(全双工、持久、低开销)
- 解决:真正的双向实时通信,降低握手与头部开销,支持文本与二进制,端到端延迟低。
- 典型场景:聊天、协同编辑、在线游戏、行情推送、IoT。
- 与上一链条的关系:
- 在“需要双向实时”的主战场,实质上取代了短轮询/长轮询等过渡方案。
- 与 SSE 并存:若只有单向通知/事件流,SSE 更简单更省资源;若需要双向或二进制,WebSocket 更合适。
- 运维关注:连接状态管理、容量与反压、企业代理/负载均衡兼容。
- 解决:真正的双向实时通信,降低握手与头部开销,支持文本与二进制,端到端延迟低。
- 典型场景:聊天、协同编辑、在线游戏、行情推送、IoT。
- 与上一链条的关系:
- 在“需要双向实时”的主战场,实质上取代了短轮询/长轮询等过渡方案。
- 与 SSE 并存:若只有单向通知/事件流,SSE 更简单更省资源;若需要双向或二进制,WebSocket 更合适。
- 运维关注:连接状态管理、容量与反压、企业代理/负载均衡兼容。
03|底座升级与新选项:HTTP/2·HTTP/3·QUIC 家族
这部分不是替代前两块,而是提供更高效的承载与更灵活的传输语义。
这部分不是替代前两块,而是提供更高效的承载与更灵活的传输语义。
WS over H2/H3
- 价值:与同域请求复用连接、更好穿透与效率、更低握手成本。
- 作用:让 WebSocket 的部署与网络效率更优。
- 价值:与同域请求复用连接、更好穿透与效率、更低握手成本。
- 作用:让 WebSocket 的部署与网络效率更优。
WebTransport(基于 QUIC)
- 价值:可选可靠与有序/无序、更低延迟,适合实时媒体、游戏、定制协议。
- 关系:不是取代 WebSocket/SSE 的通吃方案,而是当你需要“更细粒度的可靠性与顺序控制”时的新工具。
- 价值:可选可靠与有序/无序、更低延迟,适合实时媒体、游戏、定制协议。
- 关系:不是取代 WebSocket/SSE 的通吃方案,而是当你需要“更细粒度的可靠性与顺序控制”时的新工具。
二、速查表
实时推送的目标是“低延迟、双向或单向地把数据从服务端送到客户端”。主流技术选型包括:

实时推送的目标是“低延迟、双向或单向地把数据从服务端送到客户端”。主流技术选型包括:

三、WebSocket 核心定义(重要)
WebSocket 是 HTML5 推出的一种全双工(Full-Duplex)、持久化(Persistent)的网络通信协议, 基于TCP协议构建,允许客户端(浏览器)与服务器之间建立一条长期稳定的连接通道,实现「服务器主动向客户端推送数据」和「客户端实时向服务器发送数据」的双向通信,无需频繁建立/断开连接。
其核心特点可概括为:
- 全双工:通信双方可同时发送/接收数据(区别于HTTP的「请求-响应」单向通信);
- 持久连接:连接建立后长期保持,避免HTTP每次通信都需重新握手的开销;
- 轻量协议:数据帧头部信息简洁(仅2-14字节),传输效率远高于HTTP;
- 协议标识:客户端发起连接时使用ws://(非加密)或wss://(加密,基于TLS,类似HTTPS)作为协议前缀。
WebSocket 是 HTML5 推出的一种全双工(Full-Duplex)、持久化(Persistent)的网络通信协议, 基于TCP协议构建,允许客户端(浏览器)与服务器之间建立一条长期稳定的连接通道,实现「服务器主动向客户端推送数据」和「客户端实时向服务器发送数据」的双向通信,无需频繁建立/断开连接。
其核心特点可概括为:
- 全双工:通信双方可同时发送/接收数据(区别于HTTP的「请求-响应」单向通信);
- 持久连接:连接建立后长期保持,避免HTTP每次通信都需重新握手的开销;
- 轻量协议:数据帧头部信息简洁(仅2-14字节),传输效率远高于HTTP;
- 协议标识:客户端发起连接时使用ws://(非加密)或wss://(加密,基于TLS,类似HTTPS)作为协议前缀。
从零开始的完整流程
下面是一条你在前端真实会走的链路:创建连接 → HTTP 握手与协议切换 → 进入 WebSocket 双向通信 → 启动心跳检测 → 发现异常并重连 → 重连成功后的补偿 → 服务端跨域放行 → 正常/异常关闭。
下面是一条你在前端真实会走的链路:创建连接 → HTTP 握手与协议切换 → 进入 WebSocket 双向通信 → 启动心跳检测 → 发现异常并重连 → 重连成功后的补偿 → 服务端跨域放行 → 正常/异常关闭。
1) 创建连接(入口)
2) HTTP 握手与协议切换(从“请求”到“长连”)
客户端(浏览器) 创建 WebSocket 实例时,会发起一个特殊的 HTTP - GET,核心目的是 「请求将协议从HTTP升级为WebSocket」。服务端验证通过后返回 101,双方切换到 WebSocket 帧通信。
WebSocket 帧是双向通信中的最小传输结构,携带 “ 数据类型 、 是否为消息的最后一段 、 负载长度 、 掩码/密钥 、 实际数据 ” 。消息可以被拆成多帧连续发送,也可以一个帧就送完。

客户端(浏览器) 创建 WebSocket 实例时,会发起一个特殊的 HTTP - GET,核心目的是 「请求将协议从HTTP升级为WebSocket」。服务端验证通过后返回 101,双方切换到 WebSocket 帧通信。
WebSocket 帧是双向通信中的最小传输结构,携带 “ 数据类型 、 是否为消息的最后一段 、 负载长度 、 掩码/密钥 、 实际数据 ” 。消息可以被拆成多帧连续发送,也可以一个帧就送完。

客户端发起「协议升级请求」(HTTP - GET 请求)
请求头中关键字段(面试高频考点):
GET /ws-endpoint HTTP/1.1:请求方法为 GET,路径为服务器的 WebSocket 端点(如/ws);Host:example.com:服务器域名;Upgrade:websocket:核心字段,告知服务器「要升级协议为 WebSocket」;Connection:Upgrade:配合 Upgrade,表示「这是一个协议升级请求」;Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jzQ==:客户端生成的随机字符串(Base64 编码,长度 16 字节),用于服务器验证(防止恶意连接);Sec-WebSocket-Version:13:WebSocket 协议版本(当前主流为 13,需服务器支持);Sec-WebSocket-Origin:https://example.com:客户端所在域名(用于服务器跨域验证)。
请求头中关键字段(面试高频考点):
GET /ws-endpoint HTTP/1.1:请求方法为GET,路径为服务器的 WebSocket 端点(如/ws);Host:example.com:服务器域名;Upgrade:websocket:核心字段,告知服务器「要升级协议为 WebSocket」;Connection:Upgrade:配合 Upgrade,表示「这是一个协议升级请求」;Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jzQ==:客户端生成的随机字符串(Base64 编码,长度 16 字节),用于服务器验证(防止恶意连接);Sec-WebSocket-Version:13:WebSocket 协议版本(当前主流为 13,需服务器支持);Sec-WebSocket-Origin:https://example.com:客户端所在域名(用于服务器跨域验证)。
服务器响应「协议升级成功」(HTTP - 101状态码)
服务器收到请求后,若支持 WebSocket 协议且验证通过(如 Sec-WebSocket-Key 验证、跨域验证) ,会返回HTTP - 101(SwitchingProtocols)状态码,表示「同意协议升级」。
响应头中关键字段 (面试高频考点):
HTTP/1.1 101 Switching Protocols:101 状态码是协议切换的标志;Upgrade: websocket:确认升级为WebSocket 协议;Connection:Upgrade:确认连接用于协议升级;Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=:服务器对Sec-WebSocket-Key 的处理结果(核心验证逻辑):- 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接; - 对拼接后的字符串进行
SHA-1 哈希计算; - 将哈希结果转为 Base64 编码,即为 Sec-WebSocket-Accept 的值;
- 客户端会验证该值是否正确,若不正确则拒绝建立连接(防止伪造响应)。
服务器收到请求后,若支持 WebSocket 协议且验证通过(如 Sec-WebSocket-Key 验证、跨域验证) ,会返回HTTP - 101(SwitchingProtocols)状态码,表示「同意协议升级」。
响应头中关键字段 (面试高频考点):
HTTP/1.1 101 Switching Protocols:101 状态码是协议切换的标志;Upgrade: websocket:确认升级为WebSocket 协议;Connection:Upgrade:确认连接用于协议升级;Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=:服务器对Sec-WebSocket-Key 的处理结果(核心验证逻辑):- 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接; - 对拼接后的字符串进行
SHA-1哈希计算; - 将哈希结果转为 Base64 编码,即为 Sec-WebSocket-Accept 的值;
- 客户端会验证该值是否正确,若不正确则拒绝建立连接(防止伪造响应)。
- 服务器将客户端发送的 Sec-WebSocket-Key 与固定字符串
3) 进入通信阶段(双向数据 + 基础发送)
握手通过,onopen 会触发。此时做两件事:
- 发送初始化数据(如身份、订阅主题)
- 启动心跳(下一步会讲)
通信注意:
onmessage 既可能是字符串,也可能是二进制(Blob/ArrayBuffer)bufferedAmount 可用来做背压控制(积压太大时暂停继续 send)
握手通过,onopen 会触发。此时做两件事:
- 发送初始化数据(如身份、订阅主题)
- 启动心跳(下一步会讲)
通信注意:
onmessage既可能是字符串,也可能是二进制(Blob/ArrayBuffer)bufferedAmount可用来做背压控制(积压太大时暂停继续 send)
4) 启动心跳(让连接“活着”且可感知)
持久连接会遭遇 Wi‑Fi 抖动、防火墙清理等问题。心跳=周期性发 ping,超时未收到 pong 就判死链。
推荐参数(可按业务调优):
HEARTBEAT_INTERVAL ≈ 30sHEARTBEAT_TIMEOUT ≈ 10s

实操要点:
- 启动前先清理旧定时器,避免重复
- 收到 pong 立即清除超时定时器
- onclose/onerror 必须停止心跳
持久连接会遭遇 Wi‑Fi 抖动、防火墙清理等问题。心跳=周期性发 ping,超时未收到 pong 就判死链。
推荐参数(可按业务调优):
HEARTBEAT_INTERVAL ≈ 30sHEARTBEAT_TIMEOUT ≈ 10s

实操要点:
- 启动前先清理旧定时器,避免重复
- 收到 pong 立即清除超时定时器
- onclose/onerror 必须停止心跳
5) 异常→重连(恢复连接但不过载)
一旦 onerror、onclose(code ≠ 1000) 或心跳判定超时,进入重连。
目标是:能恢复、不过载、可被用户停止。
策略三件套:
- 指数退避:1s → 2s → 4s … 最多 30s
- 次数上限:如 10 次(达到即停)
- 可控停止:提供“停止重连”或页面关闭时停

补偿机制:
- 在断开前缓存“待发送”数据(例如未发出的聊天消息)
- 重连成功后按序补发,确保业务连续性
一旦 onerror、onclose(code ≠ 1000) 或心跳判定超时,进入重连。
目标是:能恢复、不过载、可被用户停止。
策略三件套:
- 指数退避:1s → 2s → 4s … 最多 30s
- 次数上限:如 10 次(达到即停)
- 可控停止:提供“停止重连”或页面关闭时停

补偿机制:
- 在断开前缓存“待发送”数据(例如未发出的聊天消息)
- 重连成功后按序补发,确保业务连续性
6) 服务器放行跨域(握手能否过关的关键)
虽然 WebSocket 原生“支持跨域”,但握手是 HTTP,服务端需要对 Sec-WebSocket-Origin 做白名单校验。
否则会 403 或直接关闭。
- Node.js(ws)
- 读取
req.headers['sec-websocket-origin'] - 不在
allowedOrigins 列表:关闭 1008 “Cross-origin access denied”
- Spring Boot
registry.addHandler(...).setAllowedOrigins("https://a.com", "https://b.com")- 需要时
.withSockJS()提供降级

虽然 WebSocket 原生“支持跨域”,但握手是 HTTP,服务端需要对 Sec-WebSocket-Origin 做白名单校验。
否则会 403 或直接关闭。
- Node.js(ws)
- 读取
req.headers['sec-websocket-origin'] - 不在
allowedOrigins列表:关闭1008“Cross-origin access denied”
- 读取
- Spring Boot
registry.addHandler(...).setAllowedOrigins("https://a.com", "https://b.com")- 需要时
.withSockJS()提供降级

7) 正常关闭与资源清理(善始善终)
- 用户离开页面或主动退出:ws.close(1000, '用户主动退出')
- onclose 中停止心跳与重连,清空定时器与队列,避免泄漏
- 记录关闭原因码:1000 正常、1006 常见于异常/心跳超时
- 用户离开页面或主动退出:ws.close(1000, '用户主动退出')
- onclose 中停止心跳与重连,清空定时器与队列,避免泄漏
- 记录关闭原因码:1000 正常、1006 常见于异常/心跳超时
四、面试题
面试关注点通常围绕“协议对比、连接管理、消息语义、可靠性与扩展性、安全与运维成本”。
面试关注点通常围绕“协议对比、连接管理、消息语义、可靠性与扩展性、安全与运维成本”。
1、WebSocket 与 SSE 的差异与使用场景,HTTP轮询呢?
WebSocket(全双工,二进制/文本)
- 适用:即时聊天、协作编辑、游戏状态同步、行情推送、需要客户端→服务端主动上行的实时交互。
- 优点:低延迟、头开销小、全双工、支持二进制、可自定义子协议。
- 注意:需处理心跳、重连、背压、鉴权与扇出扩展;代理/LB 要正确透传 Upgrade 和超时设置。
Server-Sent Events(SSE,单向 server→client)
- 适用:通知流、日志流、监控事件、流式生成文本(如增量输出)、只需服务端下行的实时更新。
- 优点:浏览器原生 EventSource、文本流、自动重连、支持 Last-Event-ID 断点续传;实现简单。
- 注意:单向、仅文本(可 base64 二进制)、连接数限制与代理超时需要关注;移动端网络切换要做容错。
HTTP 轮询/长轮询(兼容兜底)
- 适用:对实时性要求不高的小流量场景、受网络环境或企业防火墙限制无法使用 WS/SSE 时的兜底。
- 优点:最易落地、与缓存/鉴权/监控体系天然兼容;对中间设备最友好。
- 注意:延迟更高、资源利用低;高频轮询会带来成本与限流压力。
✅ 重点
- WebSocket 通过 HTTP/1.1 Upgrade → 101 完成切换,此后是帧协议的全双工通道;Keep-Alive 仅是复用 TCP,不改变 HTTP 的请求-响应语义。
- 选型规则:需要双向实时交互选 WebSocket;单向事件流选 SSE;受限或低实时性场景用轮询作兜底。
WebSocket(全双工,二进制/文本)
- 适用:即时聊天、协作编辑、游戏状态同步、行情推送、需要客户端→服务端主动上行的实时交互。
- 优点:低延迟、头开销小、全双工、支持二进制、可自定义子协议。
- 注意:需处理心跳、重连、背压、鉴权与扇出扩展;代理/LB 要正确透传 Upgrade 和超时设置。
Server-Sent Events(SSE,单向 server→client)
- 适用:通知流、日志流、监控事件、流式生成文本(如增量输出)、只需服务端下行的实时更新。
- 优点:浏览器原生 EventSource、文本流、自动重连、支持 Last-Event-ID 断点续传;实现简单。
- 注意:单向、仅文本(可 base64 二进制)、连接数限制与代理超时需要关注;移动端网络切换要做容错。
HTTP 轮询/长轮询(兼容兜底)
- 适用:对实时性要求不高的小流量场景、受网络环境或企业防火墙限制无法使用 WS/SSE 时的兜底。
- 优点:最易落地、与缓存/鉴权/监控体系天然兼容;对中间设备最友好。
- 注意:延迟更高、资源利用低;高频轮询会带来成本与限流压力。
✅ 重点
- WebSocket 通过 HTTP/1.1 Upgrade → 101 完成切换,此后是帧协议的全双工通道;Keep-Alive 仅是复用 TCP,不改变 HTTP 的请求-响应语义。
- 选型规则:需要双向实时交互选 WebSocket;单向事件流选 SSE;受限或低实时性场景用轮询作兜底。
2、如何设计一个可水平扩展的实时推送系统?
在可水平扩展的实时推送系统中,WebSocket 连接会分布在多台网关节点上。
核心挑战是如何在连接与消息不在同一台机器时,仍能把消息快速路由到正确的连接。可行的范式是
- 网关层负责连接
- 管道层负责路由与发布订阅
- 存储层负责状态与回放
在可水平扩展的实时推送系统中,WebSocket 连接会分布在多台网关节点上。
核心挑战是如何在连接与消息不在同一台机器时,仍能把消息快速路由到正确的连接。可行的范式是
- 网关层负责连接
- 管道层负责路由与发布订阅
- 存储层负责状态与回放
🔌 网关层(负责连接)
- 终止 TLS/WS,维持心跳与速率限制,保持无状态实例。
- 建立本地索引:connectionId → 订阅集合,userId → connectionIds。
- 将连接元数据上报共享存储:connectionId、userId、nodeId、订阅、最近心跳。
- 仅订阅“与自己有关的分片”:按 userId/topic 的哈希分片从管道层拉取,减少无关扇出。
- 写通道背压与优先级:控制帧/关键消息优先,低优先级可丢尾或抽样。
- 终止 TLS/WS,维持心跳与速率限制,保持无状态实例。
- 建立本地索引:connectionId → 订阅集合,userId → connectionIds。
- 将连接元数据上报共享存储:connectionId、userId、nodeId、订阅、最近心跳。
- 仅订阅“与自己有关的分片”:按 userId/topic 的哈希分片从管道层拉取,减少无关扇出。
- 写通道背压与优先级:控制帧/关键消息优先,低优先级可丢尾或抽样。
🚇 管道层(负责路由与发布订阅)
- 选型具备分片与回放能力的总线(Kafka/Pulsar/NATS/Redis Streams)。
- 分片策略:
- 点推:按 userId/connectionId 一致性哈希到分区,保证单用户局部有序。
- 主题推送:按 topic 分区,网关本地再做订阅过滤与扇出。
- 路由方式:
- 生产者只需写对的分片;总线按分区把消息送到订阅该分片的网关。
- 广播/超大房间采用“分层扇出”:先到分片,再由各网关本地扇出,必要时加中间扇出代理。
- 去重与幂等:messageId 或 (topic, partition, offset) 作为幂等键,网关/客户端各自维护短期去重集合。
- 选型具备分片与回放能力的总线(Kafka/Pulsar/NATS/Redis Streams)。
- 分片策略:
- 点推:按 userId/connectionId 一致性哈希到分区,保证单用户局部有序。
- 主题推送:按 topic 分区,网关本地再做订阅过滤与扇出。
- 路由方式:
- 生产者只需写对的分片;总线按分区把消息送到订阅该分片的网关。
- 广播/超大房间采用“分层扇出”:先到分片,再由各网关本地扇出,必要时加中间扇出代理。
- 去重与幂等:messageId 或 (topic, partition, offset) 作为幂等键,网关/客户端各自维护短期去重集合。
🗃️ 存储层(负责状态与回放)
- 会话与订阅状态:使用 Redis Cluster 或 KV 服务存 userId→connectionIds、connectionId→nodeId、订阅清单、心跳时间。
- 游标与回放:在总线层保留 offset;客户端重连携带 resumeToken,网关据此恢复订阅并按 offset 增量补发。
- 一致性与更新:订阅变更写事件流,相关网关消费后刷新本地索引;用版本号/逻辑时钟避免乱序覆盖。
- 会话与订阅状态:使用 Redis Cluster 或 KV 服务存 userId→connectionIds、connectionId→nodeId、订阅清单、心跳时间。
- 游标与回放:在总线层保留 offset;客户端重连携带 resumeToken,网关据此恢复订阅并按 offset 增量补发。
- 一致性与更新:订阅变更写事件流,相关网关消费后刷新本地索引;用版本号/逻辑时钟避免乱序覆盖。
3、如何保证消息不丢、不重、按序?
- 不丢: 消息先落到能持久化、带副本确认的总线里(像“写盘且多副本到位才算成功”),写失败就退避重试;消费侧是“先送到用户手里或进可靠下行队列,再更新位点”,断线后拿着
resumeToken+offset 从保留的历史里把漏掉的补回来。 - 不重: 每条消息都有一个不会撞车的“指纹”(messageId 或 topic-partition-offset);网关用一小块内存做近端去重,只有第一次真正写入才前进位点,重复的一概忽略;客户端也按同一指纹做幂等处理,避免业务状态被二次改动。
- 按序: 把需要有序的对象(userId/roomId)哈希到同一分区,借用分区内天然顺序;同一个键在网关里串行发送、同队列内重试,不跨分区不并行穿插,这样即使重试和补发也不会把顺序打乱。
- 不丢: 消息先落到能持久化、带副本确认的总线里(像“写盘且多副本到位才算成功”),写失败就退避重试;消费侧是“先送到用户手里或进可靠下行队列,再更新位点”,断线后拿着
resumeToken+offset从保留的历史里把漏掉的补回来。 - 不重: 每条消息都有一个不会撞车的“指纹”(messageId 或 topic-partition-offset);网关用一小块内存做近端去重,只有第一次真正写入才前进位点,重复的一概忽略;客户端也按同一指纹做幂等处理,避免业务状态被二次改动。
- 按序: 把需要有序的对象(userId/roomId)哈希到同一分区,借用分区内天然顺序;同一个键在网关里串行发送、同队列内重试,不跨分区不并行穿插,这样即使重试和补发也不会把顺序打乱。
4、心跳如何设计?超时如何判定?
这里的心跳,目标是 “保活、探测、可平滑重连”。
- 不失联: 用应用层
ping/pong,客户端主发、服务端回;- 间隔 20–60s,加±10%抖动
- 未知网络时取 20–30s,确保小于最短 NAT 空闲回收。
- 怎么判死: 别一跳不回就拍板。记录
lastSeen,允许 2–3 次心跳未达或 now-lastSeen 超过 2–3 个周期再判断;进入“Suspect”时降级写入,仍有业务流量即立刻恢复。 - 断了咋办:重连走指数退避并带抖动,携带
resumeToken/offset 补发;移动端切网优先复用会话,失败再重建。监控 RTT/丢包与 Suspect 比例,自动调心跳与阈值。
这里的心跳,目标是 “保活、探测、可平滑重连”。
- 不失联: 用应用层
ping/pong,客户端主发、服务端回;- 间隔 20–60s,加±10%抖动
- 未知网络时取 20–30s,确保小于最短 NAT 空闲回收。
- 怎么判死: 别一跳不回就拍板。记录
lastSeen,允许 2–3 次心跳未达或now-lastSeen超过 2–3 个周期再判断;进入“Suspect”时降级写入,仍有业务流量即立刻恢复。 - 断了咋办:重连走指数退避并带抖动,携带
resumeToken/offset补发;移动端切网优先复用会话,失败再重建。监控 RTT/丢包与 Suspect 比例,自动调心跳与阈值。
5、如何在 Nginx/Envoy 反向代理后稳定运行 WebSocket?
核心思路:让代理“不瞎操心”、连接“常被看见”、后端“可续上”。
- 代理设置:开启 WebSocket 升级;调大超时,禁用缓冲与压缩;保持 TCP keepalive,HTTP/2 用 CONNECT(H2/WebSocket)。
- 心跳与保活:应用层 ping/pong 20–30s(±10%抖动),保证小于代理/NAT空闲回收;大连接数用轻量负载均衡(hash by userId/roomId)避免跨节点迁移。
- 断线与重连:客户端指数退避+抖动,带会话 token/offset 续传;后端幂等去重,重放不重不丢。
- 运维与观测:开代理层指标(升级成功率、idle 关闭数、5xx)、RTT/丢包与重连率,异常时自适应缩短心跳或放宽超时。
6、如何做鉴权与权限隔离?
握手前校验 JWT;Subprotocol 指定租户/版本;频道级 ACL;避免敏感数据从客户端请求非授权频道。
- 握手前校验 JWT:在
HTTP Upgrade前验证 iss/aud/exp/签名并解析 tenant_id/user_id/scopes,避免建立长连后再踢。 - Subprotocol 指定租户/版本:用 Sec-WebSocket-Protocol 携带 tenant 和策略版本做白名单匹配,确保连接上下文一致。
- 频道级 ACL:频道强制以租户前缀命名,每次 subscribe/publish 依据 RBAC+scope 前缀(到资源或前缀)做服务端授权。
- 避免敏感数据越权:仅按服务器维护的“已授权订阅集合”下发数据,忽略客户端自报筛选请求并拒绝未授权频道。
7、如何评估性能与成本?
- 每连接内存占用: 用基线压测量出 MB/1k 连接的实际占用,结合目标并发外推单机上限并监控 GC/碎片。
- 每秒消息数(fanout×频率): 用发布频率×平均扇出得到总吞吐,核算带宽与发送队列容量,识别热点频道放大效应。
- 尾延迟 P95/P99: 持续跟踪端到端延迟长尾并关联队列深度与CPU/GC事件,确保在SLA红线下仍稳定。
- 压测考虑广播峰值与重连风暴: 分别模拟大扇出瞬时广播与大量短时间内握手重连,验证背压、限速和握手路径的韧性。
8、遇到“重连风暴”怎么处理?
- 抖动退避(指数退避 + 随机抖动): 客户端按指数退避间隔重试并加入随机抖动,避免同相位同时重连造成尖峰。
- 分批恢复: 将连接恢复按固定批次/时间片发放(如每 100ms 开放 N 个),把尖峰摊平到更长窗口。
- 服务端限流与排队: 在握手与认证路径设置并发/速率上限与队列,超限直接返回可重试错误或延迟令牌。
- 灰度放量: 按租户/区域/版本逐步提升允许重连比例,结合健康度与错误率自动调节放量速度。
9、前端如何封装一个健壮的 WebSocket 客户端?
- 状态机(
CONNECTING / OPEN / CLOSING / CLOSED ): 用有限状态机驱动所有事件与迁移,单航道控制避免并发重连与回调竞态。 - 心跳/重连策略: 按固定心跳探活与半开检测,重连采用指数退避叠加随机抖动并设上限与冷却期。
- 消息序列化: 统一 envelope(type/id/ts/payload),默认 JSON,性能敏感时切 Protobuf/MessagePack 并保持向后兼容。
- 离线缓存与去重: 未连通时将待发入队、跨刷新用 IndexedDB,按 seq/uuid 去重并用 last-seq 做断点续传。
- 可观测日志: 记录连接尝试/关闭码/重连次数/心跳RTT/队列长度等指标与事件,便于快速定位长尾与故障。
作者:HiStewie
来源:juejin.cn/post/7572539461478907947
CONNECTING / OPEN / CLOSING / CLOSED ): 用有限状态机驱动所有事件与迁移,单航道控制避免并发重连与回调竞态。来源:juejin.cn/post/7572539461478907947
🚣【附源码】牺牲两天摸鱼时间,我做了款大屏
📝项目背景
最近时间比较闲,摸鱼的时间越来越多了,人一闲下来就会想做点什么。说干就干,立马行动。
在刷了半小时pdd之后我买了张ui图,并根据这个ui做了一个大屏。
最终效果如下:

📦项目地址
这里附上项目地址,如果你觉得不错的话,帮我点一个小小的start。
🌐在线预览
这个预览地址是
vercel的地址,如果你没有挂梯子的话,会访问不了。访问不了的话,建议直接本地跑项目。
🛠️ 技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue | 3.5.13 | 前端框架 |
| TypeScript | 5.7.2 | 类型安全 |
| Vite | 6.1.0 | 构建工具 |
| ECharts | 5.6.0 | 数据可视化 |
| Sass | 1.89.2 | CSS预处理器 |
| Vue3-scroll-seamless | 1.0.6 | 无缝滚动 |
| autofit.js | 3.2.8 | 适配不同分辨率的屏幕 |
| vue3-odometer | 0.1.3 | 数字翻牌效果 |
项目主要是vue3+echarts的组合,整个项目主要都是一些图表的应用。下面会介绍一些模块的实现思路。
💻一些模块的实现
🗺️中间地图
第一步先获取地图行政区的geo数据,以我这个项目为例,我需要获取山东省的地图数据。
打开dataV,找到数据可视化学院,在里面找到需要的行政区,把它的geojson下载下来。

下载下来的数据长这样

这就是我们需要的geojson数据了。
拿到数据之后,就需要将其渲染出来。
这里我用的是echarts的地图。因为这个项目的地图,基本没有交互,就纯纯的数据展示。使用echarts来做的 效果会比,cesium那些更好。
注册地图
import * as echarts from 'echarts'
import sdData from '@/assets/data/山东省'
echarts.registerMap('sd', sdData as any)
先将前面下载来的数据geojson数据注册到echarts里面,并配置echarts的geo选项
{
geo: [
// 最外围发光边界
{
map: 'sd',
aspectScale: 0.85,
layoutCenter: ['50%', '50%'], //地图位置
layoutSize: '100%',
z: 12,
emphasis: {
disabled: true
},
itemStyle: {
normal: {
borderColor: 'rgb(180, 137, 81)',
borderWidth: 8,
shadowColor: 'rgba(218, 163, 88, 0.4)',
shadowBlur: 20
}
}
},
],
}

这时候渲染出来的地图是纯色的,什么都没有 也没有立体。
因为这个geo是一个平面的地图,想要立体效果,可以通过堆叠地图,并且设置位移的方式实现
比如我这边就通过这种方式去实现

通过叠加多个图层,并且每个图层的layoutCenter都不同
最终就可以实现这种看起来很立体的二维地图

具体实现代码可以访问我的github仓库看,这里只介绍一下大致思路
🔢底部的数字字体和轮播

可以看到我底部的数字字体很特别,这不是图片,这是一种电子屏风格的数字字体。

我们在网上找一个类似的字体,将其下载下来,并用css的@font-face将其引入。然后在需要的地方用font-family使用即可。

除了这个,这里还有一个数字的轮播效果,我是用vue3-odometer实现的。

为什么用这个库呢,主要是使用方便,不用配置一堆乱七八糟的。
📊其他图表
其它图表就比较常规了,这里就不做过多介绍,具体可以看源码的实现。
🔚 结尾
这个大屏虽然只有一个页面,但是做的时候,相关的图表配置调整还是挺多的。后续打算开发一个mini版的后台管理,用来管理大屏数据,并且这个后台管理的接口用node开发,用来当作node后端的练习。
来源:juejin.cn/post/7521986967103143972
40岁老前端2025年上半年都学了什么?
前端学习记录第5波,每半年一次。对前四次学习内容感兴趣的可以去我的掘金专栏“每周学习记录”进行了解。
第1周 12.30-1.5
本周学习了一个新的CSS媒体查询prefers-reduced-transparency,如果用户在系统层面选择了降低或不使用半透明,这个媒体查询就能够匹配,此特性与用户体验密切相关的。

更多内容参见我撰写的这篇文章:一个新的CSS媒体查询prefers-reduced-transparency —— http://www.zhangxinxu.com/wordpress/?…
第2周 1.6-1.12
这周新学习了一个名为Broadcast Channel的API,可以实现一种全新的广播式的跨页面通信。
过去的postMessage通信适合点对点,但是广播式的就比较麻烦。
而使用BroadcastChannel就会简单很多。
这里有个演示页面:http://www.zhangxinxu.com/study/20250…
左侧点击按钮发送消息,右侧两个内嵌的iframe页面就能接收到。

此API的兼容性还是很不错的:

更多内容可以参阅此文:“Broadcast Channel API简介,可实现Web页面广播通信” —— http://www.zhangxinxu.com/wordpress/?…
第3周 1.13-1.19
这周学习的是SVG半圆弧语法,因为有个需求是实现下图所示的图形效果,其中几段圆弧的长度占比每个人是不一样的,因此,需要手写SVG路径。

圆弧的SVG指令是A,语法如下:
M x1 y1 A rx ry x-axis-rotation large-arc-flag sweep-flag x2 y2
看起来很复杂,其实深究下来还好:

详见这篇文章:“如何手搓SVG半圆弧,手把手教程” - http://www.zhangxinxu.com/wordpress/?…
第4周-第5周 1.20-2.2
春节假期,学什么学,high起来。
第6周 2.3-2.9
本周学习Array数组新增的with等方法,这些方法在数组处理的同时均不会改变原数组内容,这在Vue、React等开发场景中颇为受用。
例如,在过去,想要不改变原数组改变数组项,需要先复制一下数组:

现在有了with方法,一步到位:

类似的方法还有toReversed()、toSorted()和toSpliced()。
更新内容参见这篇文章:“JS Array数组新的with方法,你知道作用吗?” - http://www.zhangxinxu.com/wordpress/?…
第7周 2.10-2.16
本周学习了两个前端新特性,一个JS的,一个是CSS的。
1. Set新增方法
JS Set新支持了intersection, union, difference等方法,可以实现类似交集,合集,差集的数据处理,也支持isDisjointFrom()是否相交,isSubsetOf()是否被包含,isSupersetOf()是否包含的判断。
详见此文:“JS Set新支持了intersection, union, difference等方法” - http://www.zhangxinxu.com/wordpress/?…

2. font-size-adjust属性
CSS font-size-adjust属性,可以基于当前字形的高宽自动调整字号大小,以便各种字体的字形表现一致,其解决的是一个比较细节的应用场景。
例如,16px的苹方和楷体,虽然字号设置一致,但最终的图形表现楷体的字形大小明显小了一圈:

此时,我们可以使用font-size-adjust进行微调,使细节完美。
p { font-size-adjust: 0.545;}
此时的中英文排版效果就会是这样:

更新细节知识参见我的这篇文章:“不要搞混了,不是text而是CSS font-size-adjust属性” - http://www.zhangxinxu.com/wordpress/?…
第8周 2.17-2.23
本周学习的是HTML permission元素和Permissions API。
这两个都是与Web浏览器的权限申请相关的。
在Web开发的时候,我们会经常用到权限申请,比方说摄像头,访问相册,是否允许通知,又或者地理位置信息等。

但是,如果用户不小心点击了“拒绝”,那么用户就永远没法使用这个权限,这其实是有问题的,于是就有了元素,权限按钮直接暴露在网页中,直接让用户点击就好了。

但是,根据我后来的测试,Chrome浏览器放弃了对元素的支持,因此,此特性大家无需关注。
那Permissions API又是干嘛用的呢?
在过去,不同类型的权限申请会使用各自专门的API去进行,这就会导致开始使用的学习和使用成本比较高。
既然都是权限申请,且系统出现的提示UI都近似,何必来个大统一呢?在这种背景下,Permissions API被提出来了。
所有的权限申请全都使用一个统一的API名称入口,使用的方法是Permissions.query()。

完整的介绍可以参见我撰写的这篇文章:“HTML permission元素和Permissions API简介” - http://www.zhangxinxu.com/wordpress/?…
第9周 2.24-3.2
CSS offset-path属性其实在8年前就介绍过了,参见:“使用CSS offset-path让元素沿着不规则路径运动” - http://www.zhangxinxu.com/wordpress/?…
不过那个时候的offset-path属性只支持不规则路径,也就是path()函数,很多CSS关键字,还有基本形状是不支持的。
终于,盼星星盼月亮。
从Safari 18开始,CSS offset-path属性所有现代浏览器全面支持了。

因此,很多各类炫酷的路径动画效果就能轻松实现了。例如下图的蚂蚁转圈圈动画:

详见我撰写的此文:“终于等到了,CSS offset-path全浏览器全支持” - http://www.zhangxinxu.com/wordpress/?…
第10周 3.3-3.9
CSS @supports规则新增两个特性判断,分别是font-tech()和font-format()函数。
1. font-tech()
font-tech()函数可以检查浏览器是否支持用于布局和渲染的指定字体技术。
例如,下面这段CSS代码可以判断浏览器是否支持COLRv1字体(一种彩色字体技术)技术。
@supports font-tech(color-COLRv1) {}
2. font-format()
font-format()这个比较好理解,是检测浏览器是否支持指定的字体格式的。
@supports font-format(woff2) { /* 浏览器支持woff2字体 */ }
不过这两个特性都不实用。
font-tech()对于中文场景就是鸡肋特性,因为中文字体是不会使用这类技术的,成本太高。
font-format()函数的问题在于出现得太晚了。例如woff2字体的检测,这个所有现代浏览器都已经支持了,还有检测的必要吗,没了,没有意义了。
不过基于衍生的特性还是有应用场景的,具体参见此文:“CSS supports规则又新增font-tech,font-format判断” - http://www.zhangxinxu.com/wordpress/?…
第11周 3.10-3.16
本周学习了一种更好的文字隐藏的方法,那就是使用::first-line伪元素,CSS世界这本书有介绍。
::first-line伪元素可以在不改变元素color上下文的情况下变色。
可以让按钮隐藏文字的时候,里面的图标依然保持和原本的文字颜色一致。

详见这篇文章:“一种更好的文字隐藏的方法-::first-line伪元素” - http://www.zhangxinxu.com/wordpress/?…
第12周 3.17-3.23
本周学习了下attachInternals方法,这个方法很有意思,给任意自定义元素使用,可以让普通元素也有原生表单控件元素一样的特性。
比如浏览器自带的验证提示:

比如说提交的时候的FormData或者查询字符串:

有兴趣的同学可以访问“研究下attachInternals方法,可让普通元素有表单特性”这篇文章继续了解 - http://www.zhangxinxu.com/wordpress/?…
第13周 3.24-3.30
本周学习了一个新支持的HTML属性,名为blocking 属性。
它主要用于控制资源加载时对渲染的阻塞行为。
blocking 属性允许开发者对资源加载的优先级和时机进行精细控制,从而影响页面的渲染流程。浏览器在解析 HTML 文档时,会根据 blocking 属性的值来决定是否等待资源加载完成后再继续渲染页面,这对于优化页面性能和提升用户体验至关重要。
blocking 属性目前支持的HTML元素包括
使用示意:

更多内容参见我撰写的这篇文章:“光速了解script style link元素新增的blocking属性” - http://www.zhangxinxu.com/wordpress/?…
第14周 3.31-4.6
本周学习了JS EditContext API。
EditContext API 是 Microsoft Edge 浏览器提供的一个 Web API,它允许开发者在网页中处理文本输入事件,以便在原生输入事件(如 keydown、keypress 和 input)之外,实现更高级的文本编辑功能。

详见我撰写的这篇文章:“JS EditContext API 简介” - http://www.zhangxinxu.com/wordpress/?…
第15周 4.7-4.13
本周学习一个DOM新特性,名为caretPositionFromPoint API。
caretPositionFromPoint可以基于当前的光标位置,返回光标所对应元素的位置信息,在之前,此特性使用的是非标准的caretRangeFromPoint方法实现的。
和elementsFromPoint()方法的区别在于,前者返回节点及其偏移、尺寸等信息,而后者返回元素。
比方说有一段
元素文字描述信息,点击这段描述的某个文字,caretPositionFromPoint()方法可以返回精确的文本节点以及点击位置的字符偏移值,而elementsFromPoint()方法只能返回当前
元素。
不过此方法的应用场景比较小众,例如点击分词断句这种,大家了解下即可。

详见我撰写的这篇文章:“DOM新特性之caretPositionFromPoint API” - http://www.zhangxinxu.com/wordpress/?…
第16周 4.14-4.20
本周学习的是getHTML(), setHTMLUnsafe()和parseHTMLUnsafe()这三个方法,有点类似于可读写的innerHTML属性,区别在于setHTMLUnsafe()似乎对Shadow DOM元素的设置更加友好。
parseHTMLUnsafe则是个document全局方法,用来解析HTML字符串的。
这几个方法几乎是同一时间支持的,如下截图所示:

具体参见我写的这篇文章:介绍两个DOM新方法setHTMLUnsafe和getHTML - http://www.zhangxinxu.com/wordpress/?…
第17周 4.21-4.27
光速了解HTML shadowrootmode属性的作用。
shadowRoot的mode是个只读属性,可以指定其模式——打开或关闭。
这定义了影子根的内部功能是否可以从JavaScript访问。
当影子根的模式为“关闭”时,影子根的实现内部无法从JavaScript访问且不可更改,就像元素的实现内部不能从JavaScript访问或不可更改一样。
属性值是使用传递给Element.attachShadow()的对象的options.mode属性设置的,或者在声明性创建影子根时使用<template>元素的shadowrootmode属性设置的。

类似的属性总共有4个:
- shadowRootClonable 标示可复制状态
- shadowRootDelegatesFocus 标示聚焦委托状态(子元素点击,ShadowRoot获得焦点)
- shadowRootMode 标示开放状态
- shadowRootSerializable 标示序列化状态
这些属性都是与Web Components开发相关的,我看还有人用在SSR中,可以遍历组件元素内部的信息。
详见我整理的这篇文章:“光速了解HTML shadowrootmode等属性的作用” - http://www.zhangxinxu.com/wordpress/?…
第18周 4.28-5.4
最近已经在正式项目中使用scale, rotate, translate属性了(注意,没有skew属性),很赞,毕竟这几个特性已经支持4年多了。

告别transform属性,直接使用scale、rotate和translate属性,是 CSS 发展的一个新趋势。它们不仅语法简洁、易于使用,而且能让我们更方便地对元素的变形效果进行独立控制,提高代码的可维护性和性能。在未来的前端开发中,我们应该积极拥抱这些新特性,让我们的 CSS 代码更加简洁、高效。
详见此文:告别transform,是时候直接使用scale, rotate属性啦 - http://www.zhangxinxu.com/wordpress/?…
第19周 5.5-5.11
本周学习CSS animation-composition属性,该属性可以让动画效果累加。
演示页面地址见这里:不同值混合后的动画效果demo - http://www.zhangxinxu.com/study/20250…
支持replace、add和accumulate这三个值,其中后面两个值很容易混淆,add直接就是属性值累加,accumulate则是属性的计算值累加。
animation-composition特别适合用在transform定位的同时需要transform动画的场景中。

详见我写的这篇文章:“CSS animation-composition可以让动画效果累加” - http://www.zhangxinxu.com/wordpress/?…
第20周 5.12-5.18
这是这周才知道的一个知识,那就是输入框的value值也能直接返回数值类型。
已知输入框元素:
<input id="number" min="1" max="10" type="number" />
平常我们获取输入框的值都是使用 number.value 获取的,但是这个属性的返回值是个字符串。
其实现在浏览器支持直接返回数值类型的,使用numer.valueAsNumber即可。
类似的还有valueAsDate属性,适合时间类型的输入框。
详见此文:你知道吗,输入框的value值也能直接返回数值类型 - http://www.zhangxinxu.com/wordpress/?…
第21周 5.19-5.25
Chrome 133实现了attr()函数所有CSS属性都支持,这个特性可就厉害了。

举个例子,有一个链接地址是图片,那么,无需img元素介入,纯CSS就能让这个地址以图片的方式显示出来。
代码示意:
<a href="example.jpg">图片?</a>
[href]::before {
content: '';
display: block;
width: 150px; height: 200px;
background: image-set(attr(href));
background-size: cover;
}
此时,图片显示的效果就可以实现了。
关于attr()函数更多内容,可以参加此文:“震惊,有生之年居然看到CSS attr()全属性支持” - http://www.zhangxinxu.com/wordpress/?…
第22周 5.26-6.1
本周学习的是JS PageSwapEvent事件,乍一看,以为是页面切换触发的事件。
后来细细一研究,并不是,这个事件发生在,如果页面设置了页面级别的Page Transition过渡效果(URL跳转的页面之间也能平滑过渡,参见下面GIF图),在页面离开的时候,会触发。

主要是方便开发者精确控制页面间的动画细节用的。
这么一看,此事件算是比较小众的,平常开发使用机会并不大,了解下即可。
详见此文:“JS PageSwap PageReveal事件干嘛用的?” -http://www.zhangxinxu.com/wordpress/?…
第23周 6.2-6.8
本周学习的是CSS新的伪元素::scroll-button,其通过特定语法,可以给滚动容器创建自定义的滚动定位按钮,例如:
ul::scroll-button(left) { content: "◄"; }
ul::scroll-button(right){content:"►";}
配合Scroll Snap,可以纯CSS实现slider效果:

更多内容容我本周继续深入学习。
第24周 6.9-6.15
本周学习::scroll-marker伪元素。
上周学习的::scroll-button()伪元素函数可以给Carousel 效果增加左右切换按钮

这周学习的::scroll-marker则可以给Scroll Snap交互的列表元素创建索引切换按钮,以便定位到具体的元素上,效果参见:

::scroll-marker需要配合scroll-marker-group属性和::scroll-marker-group伪元素一起使用才能生效。
另外,同时被浏览器支持的还有::column伪元素,如果Snap效果是使用columns布局实现的时候使用。
更多内容,可以访问这篇文章:“CSS ::scroll-button ::scroll-marker伪元素又是干嘛用的?” - http://www.zhangxinxu.com/wordpress/?…
第25周 6.16-6.22
本周学习了text-wrap的两个子属性和两个新值。
text-wrap:pretty声明和text-wrap:wrap是一样的,区别在于text-wrap:pretty更注重排版,而非性能,也就是wrap的算法速度更快。
text-wrap:stable可以让编辑内容前面的行内容保持稳定,而不会整个文本内容发生排版变化。
两个子属性,一个是text-wrap-mode,还有一个是text-wrap-style。

更多相关内容参见这篇文章:text-wrap进化:支持两子属性和pretty stable新值 - http://www.zhangxinxu.com/wordpress/?…
第26周 6.23-6.29
本周学习clip-path shape()函数。
之前的路径剪裁使用的是path()函数,但是会有尺寸无法自适应的问题。
因为SVG路径里面的数值都是固定的像素px大小,在SVG元素中,这些大小与SVG外部尺寸关联,不会有问题,但是,放在CSS图像中,那就问题大了。
例如,Font Awesome小图标SVG基本尺寸都是512*512,其path坐标值都是好几百的值。
但是,CSS小图标的尺寸是20*20,如果应用几百数值的剪裁路径,小图标肯定就有问题,对不对?
要么path坐标等比例缩小,要么CSS小图标尺寸也设成512像素,然后再zoom缩放,但这样实现就很麻烦。
于是,在这个背景下,clip-path的shape()函数应运而生。
.use-shape {
clip-path: shape(from 50% 0%,curve to 0% 50% with 22.38% 0%/0% 22.38%,smooth by 50% 50% with 22.38% 50%,smooth by 50% -50% with 50% -22.38%,smooth to 50% 0% with 77.62% 0%,close);
}
支持百分比值,和CSS calc等数学函数,自动和元素尺寸相适应,就很厉害!
对此,我还专门开发了一个CSS clip-path path() to shape()函数转换工具 - http://www.zhangxinxu.com/sp/path2sha…

详见我撰写的这篇文章:“CSS小图标剪裁终极解决方案clip-path shape()函数” - http://www.zhangxinxu.com/wordpress/?…
-------------
好,以上就是我这个40岁的老前端上半年学习的内容,下半年我还将继续学习,继续保持对前端的好奇心,欢迎关注,转发,一起进步。
来源:juejin.cn/post/7524548909530005540
我这🤡般的7年开发生涯
前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。
我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单一细想全是漏洞;需求又紧急,今天不上线业务就要没。
所以第一个建议就是大家远离这些需求,否则你会和我一样变得不幸。
但是👴🐂🍺啊,接下来也就算了,还全干完了。正常评估一个月的需求,我 tm 半个月干完上线;你给我一句话,我干完一整条链路上的事;你说必须今天上线,那就加班加点干上线。
就这样干了几年,黄了很多,也有做起来的。但是不管业务怎么发展,这样做时间长了会出现很多致命问题。
开发忙成狗
一句话需求太多,到最后只有开发最了解业务,所有人所有事都来找开发,开发也是人,开发还要写代码呢。最先遇到的问题就是时间严重不够,产品跟个摆设一样,什么忙都帮不上,我成了产品开发结合体。
bug 来了
开发一忙,节奏就乱了,乱则生 bug,再加上原本需求上逻辑不完整的深坑,坑上叠坑,出 bug 是迟早的事。
形象崩塌
一旦出现 bug,人设就毁了。记住一句话,没人会感谢你把原本一个月的需求只用半个月上线,大家都觉得这玩意本来就半个月工时。慢慢的开始以半个月的工时要求你。
那些 bug 自己回头,慢慢做都是可以避免的,就像考试的时候做完了卷子复查一遍,很多问题回头看一下都能发现,结果因为前期赶工,没时间回看,而且有很多图快的写法,后期都是容易出问题的。
形象崩塌在职场中是最恐怖的,正所谓好事不出门,坏事传千里。
一旦出了问题,团队、领导、所有人对你的体感,那都是直线下降,你之前做的所有好事,就跟消失了一样,别人对你的印象,一提起来说的都是,这不是当时写出 xxx bug 的人吗?这还怎么在职场生存?脸都没了,项目好处也跟自己没关系了。
我 tm 真是愣头青啊蠢的💊💩,从入职开始都想的是多学点多干点,结果干的越多错的越多,现在心态干崩了,身体干垮了,钱还没混子多,还背了一身骂名和黑锅。
之前我看同事写代码贼慢,鼠标点来点去,打字也慢一拍,我忍不住说他你这写代码速度太慢了,可以用 xxx 快捷键等等,现在回想起来,我说他不懂代码,其实是我不懂职场。
我真是个纯纯的可悲🤡。
提桶跑路
bug 积累到一定程度,尤其是像我这样出现点资金的问题,那也差不多离走人不远了,我感觉我快到这个阶段了,即使不走,扣钱扣绩效也是在所难免的,综合算下来,还没那些混子赚的多。
我亲自接触的联调一哥们儿,一杯茶,一包烟,一个 bug 修一天。是真真正正的修了一天,从早到晚。那天我要上线那个需求,我不停的催他,后来指着代码说着逻辑让他写,最终半夜转点上线。我累的半死不活,我工资和他差不多,出了问题我还要背锅。
我现在听到 bug 都 PTSD 了,尤其是资金相关的,整个人就那种呆住,大脑空白,心脏像被揪住,我怀疑我有点心理问题了都。
为什么别人可以那么安心的摸鱼?为什么我要如此累死累活还不讨好?我分析出几点我的性格问题。
责任心过强
什么事都觉得跟自己有关系,看着别人做的不好,我就自己上手。
到后期产品真 tm 一句话啊,逻辑也不想,全等着我出开发方案,产品流程图,我再告诉她哪里要改动。不是哥们?合着我自己给出需求文档再自己写代码?
为人老实
不懂拒绝,不懂叫板。
运营的需求,来什么做什么,说什么时候上线就什么时候上线。不是哥们?我都还不知道要做什么,你们把上线时间都定了?就 tm 两字,卑微。
用力过猛
十分力恨不得使出十一分,再加一分吃奶的劲儿。一开始就领导很高的期望,后面活越来越多,而且也没什么晋升机会了,一来的门槛就太高了知道吧,再想提升就很难了。
先总结这么多吧,我现在心情激荡的很,希望给各位和我性格差不多一点提醒,别像我这样愣头青,吃力不讨好,还要遭人骂。后面再写写改进办法。
来源:juejin.cn/post/7450047052804161576
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。
老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。
解决方案:三条技术路线,你全踩了一遍
1. 最偷懒:原生 loading="lazy"
一行代码就能跑,浏览器帮你搞定。
<img
src="https://cdn.xxx.com/poster1.jpg"
loading="lazy"
decoding="async"
width="800" height="450"
/>
🔍 关键决策点
loading="lazy"2020 年后现代浏览器全覆盖,IE 全军覆没。- 必须写死
width/height,否则 CLS 会抖成 PPT。
适用场景:内部系统、用户浏览器可控,且图片域名已开启 Accept-Ranges: bytes(支持分段加载)。
2. 最稳妥:scroll 节流 + getBoundingClientRect
老项目里还有 5% 的 IE11 用户,我们只能回到石器时代。
// utils/lazyLoad.js
const lazyImgs = [...document.querySelectorAll('[data-src]')];
let ticking = false;
const loadIfNeeded = () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
lazyImgs.forEach((img, idx) => {
const { top } = img.getBoundingClientRect();
if (top < window.innerHeight + 200) { // 提前 200px 预加载
img.src = img.dataset.src;
lazyImgs.splice(idx, 1); // 🔍 及时清理,防止重复计算
}
});
ticking = false;
});
};
window.addEventListener('scroll', loadIfNeeded, { passive: true });
🔍 关键决策点
- 用
requestAnimationFrame把 30 ms 的节流降到 16 ms,肉眼不再掉帧。 - 预加载阈值 200 px,实测 4G 网络滑动不白屏。
缺点:滚动密集时 CPU 占用仍高,列表越长越卡。
3. 最优雅:IntersectionObserver 精准观测
新项目直接上 Vue3 + TypeScript,我们用 IntersectionObserver 做统一调度。
// composables/useLazyLoad.ts
export const useLazyLoad = (selector = '.lazy') => {
onMounted(() => {
const imgs = document.querySelectorAll<HTMLImageElement>(selector);
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
const img = e.target as HTMLImageElement;
img.src = img.dataset.src!;
img.classList.add('fade-in'); // 🔍 加过渡动画
io.unobserve(img); // 观测完即销毁
}
});
},
{ rootMargin: '100px', threshold: 0.01 } // 🔍 提前 100px 触发
);
imgs.forEach((img) => io.observe(img));
});
};
- 浏览器合成线程把「目标元素与视口交叉状态」异步推送到主线程。
- 主线程回调里只做一件事:把
data-src搬到src,然后unobserve。 - 整个滚动期间,零事件监听,CPU 占用 < 1%。
原理剖析:从「事件驱动」到「观测驱动」
| 维度 | scroll + 节流 | IntersectionObserver |
|---|---|---|
| 触发时机 | 高频事件(~30 ms) | 浏览器内部合成帧后回调 |
| 计算量 | 每帧遍历 N 个元素 | 仅通知交叉元素 |
| 线程占用 | 主线程 | 合成线程 → 主线程 |
| 兼容性 | IE9+ | Edge79+(可 polyfill) |
| 代码体积 | 0.5 KB | 0.3 KB(含 polyfill 2 KB) |
一句话总结:把「我每隔 16 ms 问一次」变成「浏览器你告诉我啥时候到」。
应用扩展:把懒加载做成通用指令
在 Vue3 项目里,我们干脆封装成 v-lazy 指令,任何元素都能用。
// directives/lazy.ts
const lazyDirective = {
mounted(el: HTMLImageElement, binding) {
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value; // 🔍 binding.value 就是 data-src
io.disconnect();
}
},
{ rootMargin: '50px 0px' }
);
io.observe(el);
},
};
app.directive('lazy', lazyDirective);
模板里直接写:
<img v-lazy="item.url" :alt="item.title" />
举一反三:三个变体场景思路
- 无限滚动列表
把IntersectionObserver绑在「加载更多」占位节点上,触底即请求下一页,再把新节点继续observe,形成递归观测链。 - 广告曝光统计
广告位 50% 像素可见且持续 1 s 才算一次曝光。设置threshold: 0.5并在回调里用setTimeout延迟 1 s 上报,离开视口时clearTimeout。 - 背景图懒加载
背景图没有src,可以把真实地址塞在style="--bg: url(...)",交叉时把background-image设成var(--bg),同样零回流。
小结
- 浏览器新特性能救命的,就别再卷节流函数了。
- 写死尺寸、加过渡、及时
unobserve,是懒加载不翻车的三件套。 - 把观测器做成指令/组合式函数,后续业务直接零成本接入。
现在你的「企业风采」首屏降到 1.2 s,老板滑得开心,运营继续传 8K 图,世界和平。
来源:juejin.cn/post/7530854092869615635
如果产品经理突然要你做一个像抖音一样流畅的H5
从前端到爆点!抖音级 H5 如何炼成?
在万物互联的时代,H5 页面已成为产品推广的利器。当产品经理丢给你一个“像抖音一样流畅的 H5”任务时,是挑战还是机遇?别慌,今天就带你走进抖音 H5 的前端魔法世界。
一、先看清本质:抖音 H5 为何丝滑?
抖音 H5 之所以让人欲罢不能,核心在于两点:极低的卡顿率和极致的交互反馈。前者靠性能优化,后者靠精心设计的交互逻辑。比如,你刷视频时的流畅下拉、点赞时的爱心飞舞,背后都藏着前端开发的“小心机”。
二、性能优化:让页面飞起来
(一)懒加载与预加载协同作战
懒加载是 H5 性能优化的经典招式,只在用户即将看到某个元素时才加载它。但光靠懒加载还不够,聪明的抖音 H5 还会预加载下一个可能进入视野的元素。以下是一个基于 IntersectionObserver 的懒加载示例:
document.addEventListener('DOMContentLoaded', () => {
const lazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
if ('IntersectionObserver' in window) {
let lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImageObserver.unobserve(lazyImage);
}
});
});
lazy Images.forEach((lazyImage) => {
lazyImageObserver.observe(lazyImage);
});
}
});
(二)图片压缩技术大显神威
图片是 H5 的“体重”大户。抖音 H5 常用 WebP 格式,它在保证画质的同时,能将图片体积压缩到 JPEG 的一半。你可以用以下代码轻松实现图片格式转换:
function compressImage(inputImage, quality) {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = inputImage.naturalWidth;
canvas.height = inputImage.naturalHeight;
ctx.drawImage(inputImage, 0, 0, canvas.width, canvas.height);
const compressedImage = new Image();
compressedImage.src = canvas.toDataURL('image/webp', quality);
compressedImage.onload = () => {
resolve(compressedImage);
};
});
}
三、交互设计:让用户欲罢不能
(一)微动画营造沉浸感
在点赞、评论等关键操作上,抖音 H5 会加入精巧的微动画。比如点赞时的爱心从手指位置飞出,这其实是一个 CSS 动画加 JavaScript 事件监听的组合拳。以下是一个简易版的点赞动画代码:
@keyframes flyHeart {
0% {
transform: scale(0) translateY(0);
opacity: 0;
}
50% {
transform: scale(1.5) translateY(-10px);
opacity: 1;
}
100% {
transform: scale(1) translateY(-20px);
opacity: 0;
}
}
.heart {
position: fixed;
width: 30px;
height: 30px;
background-image: url('../assets/heart.png');
background-size: contain;
background-repeat: no-repeat;
animation: flyHeart 1s ease-out;
}
document.querySelector('.like-btn').addEventListener('click', function(e) {
const heart = document.createElement('div');
heart.className = 'heart';
heart.style.left = e.clientX + 'px';
heart.style.top = e.clientY + 'px';
document.body.appendChild(heart);
setTimeout(() => {
heart.remove();
}, 1000);
});
(二)触摸事件优化
在移动设备上,触摸事件的响应速度直接影响用户体验。抖音 H5 通过精准控制触摸事件的捕获和冒泡阶段,减少了延迟。以下是一个优化触摸事件的示例:
const touchStartHandler = (e) => {
e.preventDefault(); // 防止页面滚动干扰
// 处理触摸开始逻辑
};
const touchMoveHandler = (e) => {
// 处理触摸移动逻辑
};
const touchEndHandler = (e) => {
// 处理触摸结束逻辑
};
const element = document.querySelector('.scrollable-container');
element.addEventListener('touchstart', touchStartHandler, { passive: false });
element.addEventListener('touchmove', touchMoveHandler, { passive: false });
element.addEventListener('touchend', touchEndHandler);
四、音频处理:让声音为 H5 增色
抖音 H5 的音频体验也很讲究。它会根据用户的操作实时调整音量,甚至在不同视频切换时平滑过渡音频。以下是一个简单的声音控制示例:
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioElement = document.querySelector('audio');
const audioSource = audioContext.createMediaElementSource(audioElement);
const gainNode = audioContext.createGain();
audioSource.connect(gainNode);
gainNode.connect(audioContext.destination);
// 调节音量
function setVolume(level) {
gainNode.gain.value = level;
}
// 音频淡入效果
function fadeInAudio() {
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + 1);
}
// 音频淡出效果
function fadeOutAudio() {
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 1);
}
五、跨浏览器兼容:让 H5 无处不在
抖音 H5 能在各种浏览器上保持一致的体验,这离不开前端开发者的兼容性优化。常用的手段包括使用 Autoprefixer 自动生成浏览器前缀、为老浏览器提供 Polyfill 等。以下是一个为 CSS 动画添加前缀的示例:
const autoprefixer = require('autoprefixer');
const postcss = require('postcss');
const css = '.example { animation: slidein 2s; } @keyframes slidein { from { transform: translateX(0); } to { transform: translateX(100px); } }';
postcss([autoprefixer]).process(css).then(result => {
console.log(result.css);
/*
输出:
.example {
animation: slidein 2s;
}
@keyframes slidein {
from {
-webkit-transform: translateX(0);
transform: translateX(0);
}
to {
-webkit-transform: translateX(100px);
transform: translateX(100px);
}
}
*/
});
打造一个像抖音一样的流畅 H5,需要前端开发者在性能优化、交互设计、音频处理和跨浏览器兼容等方面全方位发力。希望这些技术点能为你的 H5 开发之旅提供助力,让你的产品在激烈的市场竞争中脱颖而出!
来源:juejin.cn/post/7522090635908251686
前端部署,又有新花样?
大多数前端开发者在公司里,很少需要直接操心“部署”这件事——那通常是运维或 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
这个老爷子研究的神奇算法,影响了全世界!
这两天,科技圈的又一个重磅新闻相信不少同学都刷到了。
那就是 77 岁的计算机科学家,图灵奖+诺贝尔奖双奖得主,同时也是享誉全球的人工智能专家 Geoffrey Hint0n(杰弗里・辛顿)首次来到了中国,参加了在上海举办的 2025 世界人工智能大会(WAIC 2025)并上台进行了演讲。

上一次 Hint0n 站在聚光灯下是去年 10 月份,彼时 76 岁的 Hint0n 刚刚和 John J. Hopfield 一起,拿到了 2024 年的诺贝尔物理学奖,以表彰他们通过人工神经网络实现机器学习的奠基性发现和研究。

提到 Hint0n 这个名字,相信对于很多学习和从事人工智能工作的同学来说,应该都非常熟悉了。
Hint0n 是一位享誉全球的人工智能专家,被誉为“神经网络之父”、“深度学习的鼻祖”、“人工智能教父”等等,也是这个领域最受尊崇的泰斗之一。

7 年前,Hint0n 曾拿下过 2018 年度图灵奖。
如此一来,Hint0n 也成为了获得**「图灵奖+诺贝尔奖」的双奖得主**。
Hint0n 是学术界的传奇人物。
在当代人工智能爆发之前,Hint0n 曾经坐了几十年的学术冷板凳,而他所研究开发的神经网络算法则为后续人工智能的进一步发展奠定了基础。
大多人可能都是因为这几年大火的 AI 领域才了解的 Hint0n,但是看完他之前的人生经历,更可谓是颇具戏剧性。
1947 年,Geoffrey Hint0n 出生于英国温布尔登的一个学术世家,家庭里的很多成员都在学术和研究方面颇有造诣。

他的父亲 Howard Everest Hint0n 是一名研究甲壳虫的英国昆虫学家,而母亲 Margaret Clark 则是一名教师。
除此之外,他的高曾祖父 George Boole 还是著名的逻辑学家,也是现代计算科学基础布尔代数的发明人,而他的叔叔 Colin Clark 则是一位著名的经济学家。
当然,在这样的氛围下长大的 Hint0n,其成长压力也是可想而知的。
1970 年,23 岁的 Hint0n 获得了实验心理学的学士学位。
但是,令谁也没有想到的是,毕业后这位“学二代”由于找不到科研的意义,他竟然先跑去当了一段时间的木匠。
不过这个经历并没有帮助他消除自己的阶段性迷茫,他一直希望真正理解大脑的运作原理,渴望更深入的理论研究,于是经历过一番思想斗争后又下决心重返学术殿堂,投身于人工智能领域。
直到 1978 年,他终于获得了爱丁堡大学人工智能学博士学位,而此时的他也 31 岁了。
那个年代做深度学习的研究可以说是妥妥的冷板凳。
要知道当时的 AI 正值理论萌芽阶段,并且彼时的深度学习研究在很长一段时间里一直处于一个不温不火的状态,甚至好几次陷入寒冬,那 Hint0n 所主张和研究的深度学习派当然在当时也很难得到关注和认可。
那面对这一系列冷漠、质疑甚至反对,唯有纯粹的相信与热爱才能将这个领域深耕了数十年,直到坚持到后来 AI 时代的来临。
Hint0n 主要从事神经网络和机器学习的研究,在 AI 领域做出过许多重要贡献,其中最著名的当属他在神经网络领域所做的研究工作。

他在 20 世纪 80 年代就已经开启了反向传播算法(Back Propagation, BP 算法)的研究,并将其应用于神经网络模型的训练中。
这一算法被广泛应用于语音识别、图像识别和自然语言处理等领域。

除此之外,Hint0n 还在卷积神经网络(Convolutional Neural Networks,CNN)、深度置信网络(Deep Belief Networks,DBN)、递归神经网络(Recursive Neural Networks,RNN)以及胶囊网络(Capsule Network)等领域作出了重要贡献。
2013 年,Hint0n 加入 Google,同时把机器学习相关的很多技术带进了谷歌,并融合到谷歌的多项实际业务之中。

2019 年 3 月,ACM 公布了 2018 年度的图灵奖得主。
图灵奖大家都知道,是计算机领域的国际最高奖项,也被誉为“计算机界的诺贝尔奖”。
而 Hint0n 则与蒙特利尔大学计算机科学教授 Yoshua Bengio 和 Meta 首席 AI 科学家 Yann LeCun 一起因为研究神经网络而获得了该年度的图灵奖,以表彰他们在对应领域所做的杰出贡献。

除此之外,Hint0n 在他的学术生涯中发表了数百篇论文,这些论文中提出了许多重要的理论和方法,涵盖了人工智能、机器学习、神经网络、计算机视觉等多个领域。
而且他的论文被引用的次数也是惊人,这对于后续该领域的研究和发展都产生了重要的影响。

除了自身在 AI 领域的科研造诣很高,Hint0n 同时也是一名优秀的导师和指引者。
当年为了扩大深度学习的圈子,Hint0n 曾在多伦多大学成立过研究中心,专门接收有兴趣从事相关研究的年轻人,以至于现如今 AI 大佬圈子的“半壁江山”都是 Hint0n 的“门生”。

Hint0n 带过很多大牛学生,其中不少都被像苹果、Google 等这类硅谷科技巨头所挖走,在对应的公司里领导着人工智能相关领域的研究。
这其中比较典型的就是 Ilya Sutskever,他既是 Hint0n 的学生,同时他也是大名鼎鼎的 OpenAI 公司的联合创始人与首席科学家。

在这次 WAIC 2025 开幕的前一天,一张全球顶级 AI 专家的合影在业内广为流传。

画面中聚集着包括姚期智先生等在内的多个全球顶级 AI 专家。
不过大家可能也注意到了,在画面的最后一排,还独自站立着一位身材高挑的白发老人。
没错,这个人正是 77 岁的 Hint0n。
画面中 Hint0n 之所以选择站立合影,不是为了秀身高,而是因为 Hint0n 患有腰椎疾病。
1947 年出生的辛顿,年轻时就曾落下腰伤顽疾,随着年龄的增大,这让他无法像正常人那样轻松地坐下,而是习惯于尽量站立,所以这也是为什么大家能看到的 Hint0n 能站着的情况就不会坐着,而即便是坐,Hint0n 的坐姿也非常奇怪。

听 Hint0n 身边的人说,Hint0n 到哪里总是会随身带个垫子,目的也是为了应对多年的腰疾。
不过从这次 Hint0n 来中国参加 WAIC 2025 的全过程来看,老爷子的精神状态还算挺不错。
学术冷板凳 30 年,从谷歌离职时 75 岁,回看 Hint0n 老爷子的奋斗过往,的确非常传奇。
如今 AI 技术的发展巨轮还在滚滚向前,而这些人工智能领域的泰斗们所打下的基础也将继续成为人工智能研究者们心中的图腾,从而激发出更多的进化与创新。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7532771194003554323
🍏让前端去做 iPhone 的液态玻璃❓
最近 iOS 推送了新的系统更新:

其实这个更新早在几个月前的 WWDC 上就开始宣传了:

仔细看苹果 Logo,有种磨砂玻璃的质感对吧?因为他们宣传的 iOS26 主题就是液态玻璃:

为什么直接从 iOS18 跳到 26 呢?这跟咱们前端的 ES 命名有点像:
- ES
5 - ES
5.1 - ES
2015(ES6) - ES
2016 - ES
2017 - ES
2018 - ES
2019 - ES
2020 - ES
2021 - ES
2022 - ES
2023 - ES
2024 - ES
2025 - ES
2026
苹果也打算用年份来命名版本,每年都推陈出新一个版本,但叫 iOS2026 有点太长了,于是省略掉前面的 20 就变成了 iOS26。
这个液态玻璃有人觉得好看也有人觉得难看,毕竟一百个读者就有哈姆雷特嘛!我们公司的产品经理觉得好看,它就想让我们也学苹果搞个这样的主题。头都大了,先给大家看看我们以前的界面:

我们面向的是海外用户,所以界面都是英文,有人可能会问那为什么不去欧美开公司呢?我们确实有欧美的分部,但负责的是运营,而开发基本上都在中国招人,因为国内的牛马实在是太好用了😭
既便宜又耐用,而且还能无偿加班,在欧美你搞这套试试?扯远了哈!咱们来看下液态玻璃之后的界面:

当然这几个图标还没换成透明的,他们想先看看效果再决定用不用开发液态玻璃主题。由于背景是白色的,所以它们认为这个液态玻璃有点与界面融为一体了,于是我们给它加了点淡蓝色:

那么具体到底是怎么实现的呢?实现是不可能自己实现的,因为这个效果可不是常规效果,我们一开始搜了一下液态玻璃的实现,结果您猜怎么着:

这是液态玻璃么?这只是个很普通的毛玻璃吧?评论区也都这么说:


人家液态玻璃的重点是边缘部分,以我们日常生活中常见的烧水壶来举例:

大家也可以拿自己的烧水壶试试,可以看到越靠近边缘,玻璃后面的图案变形就越严重,而且最边缘还会有倒影,除了变形以外还会有色散:


所以这个效果实现起来还是挺复杂的,感觉是需要计算机图形学知识的,幸好最终在网上找到了符合需求的液态玻璃效果:

这个效果是由 Three.js 实现的,传送门:
通过域名大家也能看出来,这是用 React 实现的,但我们公司用的是 Vue。可能有人会说这就是个特效,无论用 React 还是用 Vue 不都一样么?你就不能给它用 Vue 的语法翻译过来么?毕竟 Three.js 又不挑框架,但 React Three 挑框架啊:

这个 Demo 大量的使用了 @react-three:

我总不能再仿照 @react-three 写个 @vue-three 出来吧?那 vue 生态里就没有 @vue-three 吗?有是有,但都不太靠谱:

那你不会直接用 Three.js 写?@react-three 不也就是层封装么?
还真就不会直接用 Three.js 写,因为以前压根就没用过,想要搞懂 @react-three 那些封装的话必然要去看源码,而且最好是有一定的 Three.js 基础才能看明白。这个 React Bits 的作者还出了个 Vue 版的,Vue 版直接砍掉了这个 Three.js 版的液态玻璃:
也就是说就连原作者都没法直接用 Three.js 去实现,当然也可能不是他无法实现 Vue 版的,而是脱离了 @react-three 去实现的话很麻烦之类的原因,总之就是原作者并未实现 Vue 版的。他实现起来都费劲呢,更别提我们这种压根就不会 Three 的人了。
而且产品希望这个液态玻璃只是个锦上添花的效果,不希望为了这个主题而引入 Three.js 从而导致页面变大变慢,所以 Three.js 这个方案被放弃。幸好这个 React Bits 还有个 SVG 版的,而且 SVG 版的还有 Vue 版实现:

但其实还是更喜欢 Three.js 那版,我也说不上来 SVG 版的哪不好,但确实看起来好像没 Three.js 版的好看:

不过好在 SVG 版不需要任何依赖就能够实现,非常的轻量化,在这里分享给大家:
希望能够帮助到有需要的人,毕竟国内想模仿苹果的公司还挺多的,比方说某个把自己手机叫 17 Pro Max 的粮食公司:

往期精彩文章
来源:juejin.cn/post/7552402306222882842
从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践
从“版本号打架”到 30 秒内提醒用户刷新:一个微前端团队的实践
1. 背景与痛点
我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / test / dev / hotfix / feature_x.x.x)加上 Jenkins 自动化,让“一天多次发布”成为常态。但真正影响交付效率的并不是发布次数,而是一个顽固的问题:测试同学常年停留在旧版本页面。
我们团队维护着一个微前端子应用集群,每个子应用都需要同时服务 dev / test / release / online 多套环境。分支策略(master / release / test / dev / hotfix / feature_x.x.x)加上 Jenkins 自动化,让“一天多次发布”成为常态。但真正影响交付效率的并不是发布次数,而是一个顽固的问题:测试同学常年停留在旧版本页面。
1.1 真实场景
- 测试在早上打开 dev 页面,下午我们发布了新的组件样式;
- 他们继续在旧页面里回归,反馈的问题我们一眼看出“这是老版本”;
- 群里喊“刷新一下”并不靠谱,于是“无效缺陷 + 反复沟通”成了常态。
更严重的一次事故,是我们在版本检查逻辑里同时使用了 webpack DefinePlugin 与自定义插件,各自调用了一次 getAppVersion()。结果前端控制台打印的是 0.8.3-release-202511210828,而 version.json 里是 0.8.3-release-202511210829。两边只差 1 秒钟,却让线上用户始终被提示刷新,形象地被团队称为“版本号打架”。
- 测试在早上打开 dev 页面,下午我们发布了新的组件样式;
- 他们继续在旧页面里回归,反馈的问题我们一眼看出“这是老版本”;
- 群里喊“刷新一下”并不靠谱,于是“无效缺陷 + 反复沟通”成了常态。
更严重的一次事故,是我们在版本检查逻辑里同时使用了 webpack DefinePlugin 与自定义插件,各自调用了一次 getAppVersion()。结果前端控制台打印的是 0.8.3-release-202511210828,而 version.json 里是 0.8.3-release-202511210829。两边只差 1 秒钟,却让线上用户始终被提示刷新,形象地被团队称为“版本号打架”。
1.2 我们的诉求
- 用户在 30 秒内感知版本更新;
- 弹窗里能看到“当前版本 / 最新版本 / 环境”;
- 支持“立即刷新 / 稍后再说”,不给用户造成中断;
- 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。
- 用户在 30 秒内感知版本更新;
- 弹窗里能看到“当前版本 / 最新版本 / 环境”;
- 支持“立即刷新 / 稍后再说”,不给用户造成中断;
- 方案需兼容现有微前端架构与 CI/CD 流程,不依赖后端改造。
2. 方案探索与取舍
在动手前,我们列出几种可行方式:
| 方案 | 实现复杂度 | 实时性 | 依赖 | 适配场景 | 关键优缺点 |
|---|---|---|---|---|---|
| 纯前端轮询 version.json | 低 | 中(30s) | 前端 + Nginx | 多环境微前端 | 成本最低;轻微网络开销 |
| Service Worker/PWA | 中 | 较高 | 现代浏览器 | PWA 应用 | 缓存控制好,但改造量大 |
| WebSocket 推送 | 高 | 最高 | 后端服务 | 强实时场景 | 需要额外服务端开发 |
| 后端接口统一管理 | 中 | 中 | 前后端 | 版本集中管理 | 带来跨团队耦合 |
综合团队资源与落地速度,我们选择了 纯前端轮询 + 静态版本文件 的做法,并明确两个原则:
- 版本号唯一,可追溯:
基础版本号-环境-时间戳; - 发布零侵入:Jenkins 仍旧运行
npm run build-xxx,无需新增步骤。
3. 技术方案总览
- 构建阶段生成 version.json:在
vue.config.js 中提前计算版本号,既注入到前端(process.env.APP_VERSION),也写入输出目录的 version.json; - 前端轮询比对:应用启动后每 30 秒请求一次
version.json,禁用缓存并携带时间戳,比较版本号; - 交互提示:复用 Ant Design Vue 的
Modal.confirm,展示当前/最新版本与环境; - 缓存策略:Nginx 对 HTML/
version.json 禁止缓存,对 JS/CSS/图片继续长缓存; - CI/CD 配合:所有环境沿用既有脚本,只是构建产物目录多了一份实时的
version.json。
vue.config.js 中提前计算版本号,既注入到前端(process.env.APP_VERSION),也写入输出目录的 version.json;version.json,禁用缓存并携带时间戳,比较版本号;Modal.confirm,展示当前/最新版本与环境;version.json 禁止缓存,对 JS/CSS/图片继续长缓存;version.json。4. 关键落地细节
4.1 版本号只生成一次(Build-time Deterministic Versioning)
vue.config.js 抽象 buildEnvName、buildVersion,并在 DefinePlugin 与生成 version.json 时复用:
const buildEnvName = getEnvName();
const buildVersion = getAppVersion();
module.exports = {
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
"process.env.APP_VERSION": JSON.stringify(buildVersion),
"process.env.APP_ENV": JSON.stringify(buildEnvName),
}),
],
},
chainWebpack(config) {
config.plugin("generate-version-json").use({
apply(compiler) {
compiler.hooks.done.tap("GenerateVersionJsonPlugin", () => {
fs.writeFileSync(
path.resolve(__dirname, "edu/version.json"),
JSON.stringify(
{
version: buildVersion,
env: buildEnvName,
timestamp: new Date().toISOString(),
publicPath: "/child/edu",
},
null,
2
)
);
});
},
});
},
};
这样即使构建过程持续 5~10 分钟,注入的版本号和静态文件里的版本仍保持一致。这其实是把“构建产物视为不可变工件”的原则落地——保证任何使用该工件的入口看到的元数据都是同一个快照。
4.2 版本检查器(Runtime Polling & Cache Busting)
class VersionChecker {
currentVersion = process.env.APP_VERSION;
publicPath = "/child/edu";
checkInterval = 30 * 1000;
init() {
console.log(`📌 当前前端版本:${this.currentVersion}(${process.env.APP_ENV})`);
this.startChecking();
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && !this.hasNotified) {
this.checkForUpdate();
}
});
}
async checkForUpdate() {
const url = `${this.publicPath}/version.json?t=${Date.now()}`;
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) return;
const latestInfo = await response.json();
if (latestInfo.version !== this.currentVersion && !this.hasNotified) {
this.hasNotified = true;
this.stopChecking();
this.showUpdateModal(latestInfo.version, latestInfo.env);
}
}
}
class VersionChecker {
currentVersion = process.env.APP_VERSION;
publicPath = "/child/edu";
checkInterval = 30 * 1000;
init() {
console.log(`📌 当前前端版本:${this.currentVersion}(${process.env.APP_ENV})`);
this.startChecking();
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && !this.hasNotified) {
this.checkForUpdate();
}
});
}
async checkForUpdate() {
const url = `${this.publicPath}/version.json?t=${Date.now()}`;
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) return;
const latestInfo = await response.json();
if (latestInfo.version !== this.currentVersion && !this.hasNotified) {
this.hasNotified = true;
this.stopChecking();
this.showUpdateModal(latestInfo.version, latestInfo.env);
}
}
}
这里有两个容易被忽略的细节:
fetch显式加cache: "no-store",再叠加时间戳参数,防止 CDN / 浏览器任何一层干预;visibilitychange监听,保证窗口重新激活时立即比对,避免用户在后台等了很久才看到弹窗。
入口 main.ts 在应用 mount 之后调用 versionChecker.init(),即可把整个检测链路串起来。
4.3 Nginx 缓存策略(Precise Cache Partition)
location / {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location /child/edu {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location ~* /child/edu/version.json$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
add_header Surrogate-Control "no-store";
}
location / {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location /child/edu {
if ($request_filename ~* .html$) {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}
location ~* /child/edu/version.json$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
add_header Surrogate-Control "no-store";
}
这一层的思路是把资源分成两类:需要实时性(HTML、version.json)就 no-store,其余走长缓存。再配合 try_files 兜底 history 路由,微前端子应用的独立部署不会互相影响。
4.4 CI/CD 配置(Zero-touch Pipeline)
| 环境 | 构建命令 | 输出路径 | 说明 |
|---|---|---|---|
| develop | npm run build-develop | /child/edu | 日常开发验证 |
| testing | npm run build-testing | /child/edu | 集成测试 |
| release | npm run build-release | /child/edu | 预发布 |
| production | npm run build-production | /child/edu | 线上 |
所有命令都带 cross-env NODE_OPTIONS=--openssl-legacy-provider,以兼容不同系统的 OpenSSL 版本。更重要的是,这套方案没有“要求运维多做一步”——构建产物天然携带 version.json,任何环境拿到包即可上线。
5. 测试与验证
我们定义了一个完整的回归流程,确保方案不会给测试和上线带来额外负担:
- 首次访问:打开 dev 环境页面,确认控制台打印版本号,Network 里能看到
version.json且响应头无缓存; - 触发新版本:调整任意文案,重新发布,保持旧页面不刷新;
- 轮询验证:30 秒内弹出提示框,展示当前/最新版本和环境;
- 交互路径:
- 点击“立即刷新”:页面强制 reload,新版本生效;
- 点击“稍后刷新”:记录取消动作并重新开启轮询;
- 边界场景:切 tab / 清缓存 / 新设备访问 / 短时间连续发布,均能正确感知最新版本。
6. 注意事项与常见问题
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 没有弹窗 | version.json 404 或版本未变 | 检查部署路径、确认构建是否生成文件 |
| 弹窗后刷新仍旧版本 | 静态资源被缓存 | 核实 Nginx 缓存策略、查看浏览器缓存设置 |
| 构建失败 | cross-env 未安装或权限不足 | 补充依赖、确保 Jenkins 工作目录可写 |
| 持续误报更新 | 构建阶段多次生成版本号 | 在 vue.config.js 顶部缓存 buildVersion 并全局复用 |
7. 落地成效
- 旧页面用户在 30 秒内收到提醒,测试效率显著提升;
- “幽灵弹窗”彻底消失,版本对比逻辑稳定;
- 方案只触碰前端与 Nginx 配置,发布流程无需改造;
- 文档化后,其他子应用无需重复思考,直接复用。
8. 展望
下一步我们计划:
- 封装通用 SDK:抽象版本生成、轮询、弹窗逻辑,支持 Vue CLI / Vite;
- 可视化版本面板:在主应用汇总所有环境的版本和发布时间;
- 差异化策略:针对高优先级版本强制刷新,普通版本允许用户自行选择。
这次实践让我再次意识到:真正的坑往往藏在看似“微不足道”的细节里。当我们把问题和思考写成文档、沉淀成模板,团队就能以更小的代价获得更稳定的交付。如果你也在推进微前端版本同步,欢迎交流、互相借鉴。
来源:juejin.cn/post/7575006095389605897
女友去玩,竟带回一道 “虐哭程序员” 的难题

事件交代
前两周,女朋友去了开封玩一趟,回来后不久,她微信上和我吐槽了一件糟心事:由于她在两个平台都购了票,后面行程太赶,忘记在其中一个平台退票,导致现在无法退票

由于广州很多景点买票是要选择具体日期的(例如白云山、陈家祠),所以我以为她可能就是在抖音、美团买了同一天的两张票,后面忘记退掉抖音的票,导致现在票逾期,无法退款
但我还是想的太简单了,如果仅此而已,大家就不会看到这篇小破文
深入了解
随着我深入了解,才发现开封景区的购票大有不同,以下是我了解到的开封景区购票方式:
1️⃣ 你去哪个景点就买哪个景点的票,这种模式只适合去单个景区游玩,但开封景区特别多,这样买特别亏
2️⃣ 购买景区联票,开封这边景区基本是相互合作的,例如他们会出2个、4个、6个等的景区联票套餐,我女朋友就是先在抖音购买了4景区联票,后面她朋友又发现美团有6景区联票,玩起来更划算,所以才会出现了买两个平台的票的情况
抖音4景区联票:

美团6景区联票:

抖音4景区联票包含:万岁山武侠城+翰园碑林+开封城墙+铁塔公园
美团6景区联票包含:万岁山武侠城+翰园碑林+开封城墙+铁塔公园+龙亭景区+天波杨府
其中,美团的6景区联票中,龙亭景区和天波杨府是抖音4景区联票中没有的,这两个信息特别关键
其次还有两个关键信息:
(1)平台承诺随时退:票虽然有使用截止日期,但在截止日期之前,是可以申请退款,如果截止日期仍未使用,系统自动退款
(2)女友的两个平台订单状态皆为已使用
那我开头揣测是票逾期,无法退款的情况就是错误的,既然如此,是什么情况导致了两个订单都为已使用呢?
逐层分析
我原本以为景区的核销订单模式会是:订单生成一个二维码,进入景区时有个机器扫描
如此一来,就可以精准锁定平台的订单核销,但我再次猜错了。女友和我说开封这边的景区都是使用了人脸识别技术,购票时只需要录入身-份-证号,到景区就能人脸识别进入了。
到这里事情已经逐渐清晰了,于是,我尝试站在一名程序员的角度来分析景区的核销流程
- 第一步:识别用户身份

- 第二步:识别用户身份成功,分析该用户是否在合作平台购买过相关景区票

- 第三步:核销平台订单,也是最复杂的一步,因为该环节存在多种情况
1️⃣:该用户在一个平台购买了一个景区的票,这种情况最容易,用户进景区后,票务系统直接核销平台订单,核销完成平台的钱就流向票务系统;
2️⃣:该用户在一个平台购买了联合景区的票,此时锁定该票,等到用户玩完最后一个景区后,才核销订单;有人可能会存在疑惑,为什么不是玩第一个景区时就进行订单核销,而是等玩完全部景区才触发核销?这就涉及到多个平台卖票的问题,程序设计需要兼容多个平台:假设你在抖音购买了两景区联合票:万岁山+铁塔公园,在美团购买了两景区联合票:万岁山+开封城墙,你先去了万岁山,此时如果游玩第一个景区就要核销订单的话,该核销哪个平台的订单?是不是没法核销了,只有等第二个景区玩完你才知道要核销哪个平台的订单;但即使如此,也存在一种情况,就是用户只玩了一个景区,其他景区不去玩。我猜这种特殊情况下,该票就一直处于锁定未核销的状态,直到订单时间逾期,才自动核销
3️⃣:该用户在多个平台购买了一个景区的票,说实在这种情况属实矛盾,但作为一名程序员,还是得给解,我猜很有可能是以时间优先来核销订单的,也就是先在哪个平台下单,就先核销该平台的订单,毕竟用户肯定是希望核销日期更早的一张票
4️⃣:该用户在多个平台购买了联合景区的票,而我女朋友的情况就属于这一种情况,按理说这种情况也挺好解,还记得我前面提到的龙亭景区、天波杨府吗?这两个景区是抖音平台4景区联票套餐中不包含的,而美团平台6景区联票中包含的,也就是说我女朋友她们去了其中一个,就可判断出核销美团平台的订单
但巧妙又凑合的一点就是她们的游玩路线居然是:万岁山武侠城->翰园碑林->开封城墙->铁塔公园->天波杨府->龙亭景区,恰巧把两个特殊的景区放在了最后游玩
当游玩万岁山武侠城->翰园碑林->开封城墙->铁塔公园时,对于抖音平台的订单来说,已经是满足触发核销的条件,我知道此时同为聪明程序员的你也会大有疑问🤔:美团平台的订单同样存在这4个景区,不应该直接就核销抖音平台的订单。
还记得情况3吗?也就是用户在多个平台购买了一个景区票的情况,我猜测的是票务系统以时间优先来核销不同平台的订单,再看看我开头贴出来的聊天记录,我女朋友也说了:先购买抖音的4景区联票,后购买美团的6景区联票
故此,以时间优先来核销的话,程序先核销抖音平台的订单确实没问题,当她们开心地继续畅玩天波杨府->龙亭景区时,却不知又触发了新一轮美团的订单锁单,待玩完这俩景区后,剩余的景点没玩,以至于三天逾期后自动核销订单,这也佐证了情况2里的猜测
复盘总结
后续打电话跟抖音平台核实了相关的情况,顺利完成了退票退款流程。女朋友还在一旁跟我聊着整个出行的趣事,但此时我满脑子都是在想着怎么修复票务系统中的bug🐛
一开始想的方案解决方案很粗暴:就是让景区提供一个特殊通道,用户可以通过打开二维码来核销平台订单,但这种也预防不了用户通过人脸识别进入的情况
后面想了一个从根本上解决的方案:用户人脸识别,检测到存在多个平台订单时,需要提示用户选择指定平台的订单进行核销,这样就可以彻底预防多个平台重复核销的情况了
今天的分享就到此结束,如果你对技术/行业交流有兴趣,欢迎添加howcoder微信,邀你进群交流
往期精彩
来源:juejin.cn/post/7576086446459404323
你还在 for 循环里使用 await?异步循环得这样写
1. 前言
在循环中使用 await,代码看似直观,但运行时要么悄无声息地停止,要么运行速度缓慢,这是为什么呢?
本篇聊聊 JavaScript 中的异步循环问题。
2. 踩坑 1:for 循环里用 await,效率太低
假设要逐个获取用户数据,可能会这样写:
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
代码虽然能运行,但会顺序执行——必须等 fetchUser(1) 完成,fetchUser(2) 才会开始。若业务要求严格按顺序执行,这样写没问题;但如果请求之间相互独立,这种写法就太浪费时间了。
3. 踩坑 2:map 里直接用 await,拿到的全是 Promise
很多人会在 map() 里用 await,却未处理返回的 Promise,结果踩了坑:
const users = [1, 2, 3];
const results = users.map(async (id) => {
const user = await fetchUser(id);
return user;
});
console.log(results); // 输出 [Promise, Promise, Promise],而非实际用户数据
语法上没问题,但它不会等 Promise resolve。若想让请求并行执行并获取最终结果,需用 Promise.all():
const results = await Promise.all(users.map((id) => fetchUser(id)));
这样所有请求会同时发起,results 中就是真正的用户数据了。
4. 踩坑 3:Promise.all 一错全错
用 Promise.all() 时,只要有一个请求失败,整个操作就会报错:
const results = await Promise.all(
users.map((id) => fetchUser(id)) // 假设 fetchUser(2) 出错
);
如果 fetchUser(2) 返回 404 或网络错误,Promise.all() 会直接 reject,即便其他请求成功,也拿不到任何结果。
5. 更安全的替代方案
5.1. 用 Promise.allSettled(),保留所有结果
使用 Promise.allSettled(),即便部分请求失败,也能拿到所有结果,之后可手动判断成功与否:
const results = await Promise.allSettled(users.map((id) => fetchUser(id)));
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("✅ 用户数据:", result.value);
} else {
console.warn("❌ 错误:", result.reason);
}
});
5.2. 在 map 里加 try/catch,返回兜底值
也可在请求时直接捕获错误,给失败的请求返回默认值:
const results = await Promise.all(
users.map(async (id) => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`获取用户${id}失败`, err);
return { id, name: "未知用户" }; // 兜底数据
}
})
);
这样还能避免 “unhandled promise rejections” 错误——在 Node.js 严格环境下,该错误可能导致程序崩溃。
6. 现代异步循环方案,按需选择
6.1. for...of + await:适合需顺序执行的场景
若下一个请求依赖上一个的结果,或需遵守 API 的频率限制,可采用此方案:
// 在 async 函数内
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
// 不在 async 函数内,用立即执行函数
(async () => {
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
})();
- 优点:保证顺序,支持限流
- 缺点:独立请求场景下速度慢
6.2. Promise.all + map:适合追求速度的场景
请求间相互独立且可同时执行时,此方案效率最高:
const usersData = await Promise.all(users.map((id) => fetchUser(id)));
- 优点:网络请求、CPU 独立任务场景下速度快
- 缺点:一个请求失败会导致整体失败(需手动处理错误)
6.3. 限流并行:用 p-limit 控制并发数
若需兼顾速度与 API 限制,可借助 p-limit 等工具控制同时发起的请求数量:
import pLimit from "p-limit";
const limit = pLimit(2); // 每次同时发起 2 个请求
const limitedFetches = users.map((id) => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);
- 优点:平衡并发和控制,避免压垮外部服务
- 缺点:需额外引入依赖
7. 注意:千万别在 forEach() 里用 await
这是个高频陷阱:
users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // ❌ 不会等待执行完成
});
forEach() 不会等待异步回调,请求会在后台乱序执行,可能导致代码逻辑出错、错误被遗漏。
替代方案:
- 顺序执行:用 for...of + await
- 并行执行:用 Promise.all() + map()
8. 总结:按需选择
JavaScript 异步能力很强,但循环里用 await 要“按需选择”,核心原则如下:
| 需求场景 | 推荐方案 |
|---|---|
| 需保证顺序、逐个执行 | for...of + await |
| 追求速度、独立请求 | Promise.all() + map() |
| 需保留所有结果(含失败) | Promise.allSettled()/try-catch |
| 需控制并发数、遵守限流 | p-limit 等工具 |
9. 参考链接
来源:juejin.cn/post/7569402861802782730
为什么有些人边框不用border属性
1) border 会改变布局(占据空间)
border 会参与盒模型,增加元素尺寸。
例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成:
200 + 1px(left) + 1px(right) = 202px
如果不想影响布局,就很麻烦。
使用 box-shadow: 0 0 0 1px #000不会改变大小,看起来像 border,但不占空间。
2) border 在高 DPI 设备上容易出现“模糊/不齐”
特别是 0.5px border(发丝线),在某些浏览器上有锯齿、断线。
transform: scale(0.5) 或伪元素能做更稳定的发丝线。
3) border 圆角 + 发丝线 常出现不规则效果
border + border-radius 在不同浏览器的渲染不一致,容易出现不均匀、颜色不一致的问题。
用 outline / box-shadow 圆角更稳定。
4) border 不适合做阴影/多层边框
如果你需要两层边框:
双层边框用 border 很难做
而用:
box-shadow: 0 0 0 1px #333, 0 0 0 2px #999;
非常简单。
5) border 和背景裁剪一起用时容易出 bug
比如 background-clip、overflow: hidden 配合 border 会出现背景被挤压、不应该被裁剪却裁剪等问题。
6) hover/active 等状态切换时会“跳动”
因为 border 会改变元素大小。
例子:
.btn { border: 0; }
.btn:hover { border: 1px solid #000; }
鼠标移上去会抖动,因为尺寸变大了。
用 box-shadow 的话就不会跳。
总结
边框可以分别使用border、outline、box-shadow三种方式去实现,其中outline、box-shadow不会像border一样占据空间。而box-shadow可以用来解决两个元素相邻时边框变宽的问题。不使用border并不是因为它不好,而是因为outline和box-shadow的兼容性和灵活性相对border会更好一点。
来源:juejin.cn/post/7575065042158633010
老乡鸡也开源?我用 Trae SOLO 做了个像老乡鸡那样做饭小程序!
大家好,我是不如摸鱼去,欢迎来到我的 AI 编程分享专栏。
去年,「老乡鸡不装了,直接开源」的消息引发了广泛的关注。我也纳闷,老乡鸡不是做菜的吗,开的哪门子源?仔细看了下原来是把他们的菜品、溯源报告这些开源了。然后,GitHub 上这个叫「像老乡鸡那样做饭」的项目火了,如今 star 数量已经达到了 18k,这是它的地址:github.com/Gar-b-age/C… 。
作为一名爱做饭的程序员,面对如此诱人的开源资源,怎能袖手旁观?我选择用 Trae 快速构建了一个像老乡鸡那样做饭小程序! 本文将分享我如何利用 Trae SOLO 的高效开发能力,把这份“开源美味”封装成便捷的小程序。
开源
像老乡鸡那样做饭小程序已开源,参见文章TRAE SOLO 正式发布了?我用它将像老乡鸡那样做饭小程序开源了!
实现效果

技术栈
我们开发前选择好开发的技术栈,这样 AI 可以在我们规划好的路线上进行开发,可以达到事半功倍的效果。
前端技术栈
因为我本身就是一个前端程序员,所以前端技术栈比较熟,直接选择常用的技术栈:
- 小程序的开发框架: uni-app
- 开发模板项目: wot-starter 地址: starter.wot-ui.cn/
- 组件库: wot-ui 地址: wot-ui.cn/
后端技术栈
服务端最好是可以免开发,于是我选择了 TRAE SOLO 集成的 Supabase 作为我们的云端服务。
Supabase 是一个开源的 Firebase 替代品,旨在帮助开发者快速构建后端功能。它基于 PostgreSQL 数据库,并提供实时订阅、身份验证、存储、边缘函数等功能,支持 REST 和 GraphQL API。Supabase 强调开源、可自托管,并提供免费起步的云端服务,适合构建现代 Web 和移动应用。
菜谱数据来源
我们的菜谱数据来自于开源项目「像老乡鸡那样做饭」,这是它的 GitHub 地址:github.com/Gar-b-age/C…
动手
我们选择好技术栈之后就可以开始开发,由于我们使用 Supabase 作为服务,所以后端开发无需操心,只需要让 Trae SOLO 来建表、处理数据就好了。
数据处理
我们将菜谱的 markdown 和相关图片放到了 cook-book 目录下,然后让 Trae SOLO 开始处理吧!

Trae SOLO 开始处理需求,生成 tasks 列表,并执行



不过它也不是一步到位了,我发现导入的数据有的配料字段是空的,有的步骤是空的,于是让它重新检查了下(后面我自己检查了下发现是部分菜谱的 markdown 文件的标题不对,这里我就自己处理了)。

最终,Trae SOLO 帮我将全部的菜谱数据处理完毕,并插入到 Supabase 的数据库中了,接近 200 道菜,足够每天吃一道了。

小结
“干净”的数据能达到事半功倍的效果,从上面纠错的过程可以印证这一点,在让 AI 处理前,花点时间做基础的数据清洗(统一文件命名规范、检查必要字段是否存在、清理异常字符)是非常值得的投入。
小程序开发
我们开发前,有一些准备工作,由于 wot-starter 中包含暗黑模式等相关的配置,我们本次暂不需要,故需要移除,以免干扰 AI 对项目的理解(这里我们要明确一个点,要尽量提供有用的语料给 AI,因为过大的上下文会导致它天马行空)。

我们向 Trae SOLO 提出以下需求,先实现一个简易版:
开发一个像老乡鸡那样做饭小程序,基于现有表结构实现以下核心功能:
1. 分类浏览功能:按照菜品分类展示菜谱列表
2. 首页推荐功能:在首页展示精选推荐的3-5个菜谱
3. 菜谱详情页:点击可查看完整菜谱信息
要求:
- 保持现有表结构不变
- 界面设计简洁直观
- 确保数据加载流畅
- 适配移动端显示
- 使用unocss编写样式,使用rpx做单位
经过一轮开发后,项目结构如下:

不过此时项目还是跑不起来的,控制台报错了,我们直接将控制台报错发送给 Trae SOLO。

解决之后,我们的小程序启动起来,效果就已经差不多达到了文章开头的样子了。

当然还有一些小问题,都可以让 Trae SOLO 来处理

后续完善
初版完成后,我们群里的好朋友 FliPPeDround 提醒我说,「像老乡鸡那样做饭」项目的 Github 上有 PR 提供了做菜的手绘流程图。那太好了,我们这就给加上。
首先还是让 Trae SOLO 将新增的手绘流程图上传到 Supabase 并将其地址插入到对应的菜谱中

然后在菜谱详情中展示手绘流程图

效果如图,配上流程图,清晰又美观。

总结
我们今天几乎零代码 用 Trae SOLO 实现了「像老乡鸡那样做饭」小程序,过程中这三个规则让我们事半功倍:
- 保持数据干净:“干净”的数据能达到事半功倍的效果,上面处理数据时纠错的过程可以印证这一点,在让 AI 处理前,花点时间做基础的数据清洗(统一文件命名规范、检查必要字段是否存在、清理异常字符)是非常值得的投入。
- 纯净上下文:为 AI 提供一个相对“纯净”的、与当前任务高度相关的上下文环境,这样可以让 AI 在我们规划好的路线上进行开发,避免“天马行空”。
- 增量式沟通:尽量做到增量式沟通,不要一次性把所有需求都丢给 AI。先实现核心功能,跑通后再提新需求,比如我们添加手绘流程图的功能。每次交互都基于当前已完成的代码状态,让 AI 能“看到”它之前的成果,更容易理解下一步要做什么。
好了,实现这个小程序后,我再也不愁没菜吃了!后面我想还可以加一些有趣的功能,例如:今天吃什么、每周必吃、大家都在吃,等等功能,当然这些功能也是由 Trae 来开发,大家可以期待下,同时也期待未来 AI 编程能给我们带来更多、更强的能力,让我们能专注于更重要的「业务逻辑」。
参考资料
- 小程序的开发框架: uni-app
- 开发模板项目: wot-starter 地址: starter.wot-ui.cn/
- 组件库: wot-ui 地址: wot-ui.cn/
- 像老乡鸡那样做饭:github.com/Gar-b-age/C…
往期精彩
当年偷偷玩小霸王,现在偷偷用 Trae Solo 复刻坦克大战
告别 HBuilderX,拥抱现代化!这个模板让 uni-app 开发体验起飞
Vue3 uni-app 主包 2 MB 危机?1 个插件 10 分钟瘦身
欢迎评论区沟通、讨论👇👇
来源:juejin.cn/post/7554225547117576243













