注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

如何零成本搭建个人站点

同步至个人站点:如何零成本搭建个人站点 站点地址:stack.mcell.top,包含完整的:写作、评论、部署、MCP支持... 我经常写作,最开始是在一些平台上,比如稀土掘金。后面慢慢写多了,就想有个自己的博客平台。 最初搭建的博客很简单:一个纯静态的...
继续阅读 »

同步至个人站点:如何零成本搭建个人站点




站点地址:stack.mcell.top,包含完整的:写作、评论、部署、MCP支持...



我经常写作,最开始是在一些平台上,比如稀土掘金。后面慢慢写多了,就想有个自己的博客平台。


最初搭建的博客很简单:一个纯静态的 HTML 文件,内容也不复杂,写点自我介绍,当作个人站点。直接托管到 GitHub Pages,域名用的也是它默认那串。


但很快就发现:功能太少了。
比如发布文章?评论?甚至想加点扩展能力都很难——纯 HTML 又没框架,后面越改越痛苦。


接着就走上了“大家都走过的弯路”:
买了轻量服务器,又买了域名……然后写服务端、接数据库、写前端,把整套都搭起来。


直到后面参与了一个开源项目才意识到:
这种内容站点/文档站点,压根没必要搞这么重。成熟框架太多了,比如 VitePress 这种(比如Vue官网就是VitePress),基本开箱即用。


然后我就重构了一次:直接上 VitePress。部署?还是 GitHub Pages。那时候至少配上了自定义域名,看起来舒服多了:stack.mcell.top



又过了一段时间,我开始觉得个人站点还是有点单调。VitePress 能改,但做深度定制的时候会有点别扭(有些地方甚至会翻车)。
索性就 vibe coding 一把:把原先 VitePress 那套,重构到了 Next.js


路线也很清晰:



  • Next.js

  • SSG / 静态导出(要部署到 GitHub Pages,关键是要把站点导出成纯静态)

  • GitHub Actions 自动构建

  • 部署到 GitHub Pages

  • 配上自定义域名


后面我又陆续补了评论和文档搜索功能。用到服务器了吗?没有。



  • 评论用的是 giscus:本质是把评论托管在 GitHub Discussions 里,前端加载组件就行,也不用数据库。

  • 搜索用的是 pagefind:还是静态站那套玩法,构建阶段生成索引,运行时纯前端查询。


再后面,我还给博客加了 MCP 功能。同样,还是没有服务器:
SSG 阶段生成一份 JSON docs,只要把路径映射到 MCP server 就行;然后我做了个本地的 MCP server,用户安装大概这样:


memo mcp add stack-mcepp npx -y @mcell/stack-mcell



memo code 是我最近自己写的一个轻量级编程Agent,类似Claude code那种,感兴趣可以参与进来。



本质上就是:agent 请求本地 MCP server,MCP server 再去拉取我提前生成好的 JSON 内容。


文档站上 MCP 的整体方案的记录我放在这里:从一个想法到可发布:我把博客接进 MCP 的完整实践


这一套折腾下来,依然是 0 成本。分享给大家,或许是个不错的“0 成本建站思路”。


提示词


如果你对这套方案比较感兴趣,想要多了解了解,你可以clone我的博客仓库:mcell satck,或者是直接把这段提示词发给AI,他会给你方案:


我想搭建一个个人博客,大致如下:

- 框架:nextjs ssg
- 部署:Github Action 自动化部署 + Github Page(自定义域名)
- 图片存储:对象存储(七牛云或者火山引擎)
- 搜索服务:pagefind
- 文章评论服务:giscus
- 开发方式:Vibe coding
- mcp 集成:参考 https://stack.mcell.top/blog/2026/mcp-from-idea-to-delivery-for-content-site

请你给我一个具体可落地的方案(分阶段)

(完)


作者:mCell
来源:juejin.cn/post/7605807405306740799
收起阅读 »

告别切换!一个工具搞定数据库、SSH和Docker管理

关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 你是否找过免费可用的数据库连接工具,又寻找SSH的连接工具。我们自从收到Navicat律师函警告后,从一度卸载了所有破解的软件,花了很多时间寻找替代品。 这两天发现了一个All in one的集...
继续阅读 »

关注我的公众号:【编程朝花夕拾】,可获取首发内容。



01 引言


你是否找过免费可用的数据库连接工具,又寻找SSH的连接工具。我们自从收到Navicat律师函警告后,从一度卸载了所有破解的软件,花了很多时间寻找替代品。


这两天发现了一个All in one的集成软件,可以连接数据库、SSHDocker的神仙工具:HexHub


02 简介



HexHub 是一款专为开发者和运维人员设计、集成了数据库、SSHSFTPDocker管理功能的桌面图形界面工具。其核心理念是“All in one”,旨在提供一个统一的工作平台。


官方提供了免费版和商业版,然而免费版已经足够我们日常使用了。



官网地址:http://www.hexhub.cn/


03 使用


官方提供了三种平台的安装包,满足不同的平台的需要。



3.1 数据库连接


目前满足的数据有:RedisMysqlMariaDBPostgreSQLSqlServerClickHouseSQLiteOracle


我们以Mysql为例:



我们填入数据库信息即可:



控制台库表提示,关键词高亮的辅助信息。



还有常用的执行计划、格式化、导出、保存等



对于表的操作涵盖了常用的操作,完全满足日常需要。



3.2 SSH


直接右键建立SSH连接即可。



界面主要分了三屏:控制台、UI以及监控。其中UI和监控可以手动收起来或者展开。



控制台的配色,感觉下来还是比较舒服的。


3.3 Docker


Docker的配置类似



其中Docker的配置可能会出现不成功的问题,官方也给除了解决方案:



04 小结


如果你目前在同时使用多个不同的工具来完成日常工作,那么尝试 HexHub 来简化工作流可能会是一个不错的选择。


作者:SimonKing
来源:juejin.cn/post/7597299207573946414
收起阅读 »

写给年轻程序员的几点小建议

本人快 40 岁了。第一份工作是做网站编辑,那时候开始接触 jQuery,后来转做前端,一直做到现在。说实话,我对写程序谈不上特别热爱,所以技术水平一般。 年轻的时候如果做得不开心,就会直接 裸辞。不过每次裸辞的那段时间,我都会拼命学习,这对我的成长帮助其实很...
继续阅读 »

本人快 40 岁了。第一份工作是做网站编辑,那时候开始接触 jQuery,后来转做前端,一直做到现在。说实话,我对写程序谈不上特别热爱,所以技术水平一般。


年轻的时候如果做得不开心,就会直接 裸辞。不过每次裸辞的那段时间,我都会拼命学习,这对我的成长帮助其实很大。


下面给年轻人几点个人建议:



  • 不要被网上“35 岁就失业”的说法吓到。很多人是在贩卖焦虑。我都快 40 了还能拿到 offer,只是这些 offer 薪资不到 30K。

  • 基础真的很重要。我靠着基础吃香了十几年,在公司里也解决过不少疑难问题,深得领导器重。就算现在有 AI,你也要有能力判断它写得对不对,还要知道如何向 AI 提问。

  • 适不适合做程序员,其实几年之后就能看出来:你能不能当上 Leader,或者至少能不能独当一面。如果你觉得自己确实不太适合,可以趁早考虑转行,或者下班后发展一些副业。大千世界,行行出状元,能赚钱的行业很多,不必只盯着程序员这一条路。

  • 如果你觉得自己资质一般,但又真的喜欢写程序,那也没关系。《刻意练习》这本书里提到,一个人能不能成为行业顶尖,关键在于后天练习的方式,而不是天赋本身。

  • 程序员做到后面,最大的挑战其实是身体机能,而不是技术。一定要多锻炼身体。在还没有小孩之前,尽量把自己的技术水平拉到一个相对高的位置。结婚有家庭之后,学习时间会明显减少,而且年龄增长、抗压能力下降,而程序员本身又是高度用脑的职业。如果你的技术储备够高,就能在一定程度上缓冲项目压力,让自己工作更从容。

  • React、Vue、Angular 等框架都可以尝试做做项目。不同框架背后的设计思路,对思维成长很有帮助。前端很多理念本身就借鉴了后端的逻辑,多接触不同体系,会让你看问题更立体。

  • 可以在 GitHub 上做一些开源小项目。素材从哪里来?其实就来自你在公司做过的项目。把其中一块通用能力抽出来,沉淀成一个独立组件或工具库,再整理发布到 GitHub。与此同时,多写一些技术文章进行总结和输出。等到找工作时,简历里可以写上类似 "全网阅读量几万+" 这样的成果展示,这些都会成为你的加分项,让你在竞争中更有优势。

  • 35 岁以上,竞争力通常体现在两个方向:要么技术水平足够强,能够解决复杂问题;要么具备一定的管理能力,能够带团队。有人说那我以前就带过一两个徒弟,怎么办,那你得学会包装,你懂得,哈哈。

  • 35 岁以上,面试对技术广度要求更高,所以不要太深入挖掘某一项技术了。我以前认识一个领导,虽然写代码能力一般,在公司已经不写代码了,但是他的技术广度比较好,业务能力还行,虽然 40岁了 还能跳槽到比较好的广告公司,而不是靠人脉,不得不佩服。

  • 打工人比较麻烦的事就是 简历太"花"。频繁跳槽,在一个公司没干几个月就走,或者长期待业太久。如果岗位需要背调,简历造假会很麻烦,虽然有些小公司或外包公司不做背调。所以这方面简历自己要想想办法,你懂得。

  • 另外要认清一个现实:单纯打工,很难发财。 这件事越早想明白越好。多读一些关于认知、资产配置的书,弄清楚什么是资产,什么是消费。哪怕这些认知在你有生之年未必能带来巨大财富,也可以传递给下一代,让他们少走弯路。


以上只是个人经历和感受,不一定适用于所有人,但希望能给年轻的你一些参考。


作者:程序员大卫
来源:juejin.cn/post/7606155928197070900
收起阅读 »

用 Go 语言还原 2026 春晚《惊喜定格》魔术!

今天是大年初一,江湖十年给读者朋友们拜年了,祝大家新年快乐! 又是新的一年,想必大家都没看春晚吧 😄,今天继续一年一度的用 Go 语言实现春晚魔术。 废话不多说,咱们直接看原理。 魔术原理揭秘 这个魔术的数学原理其实很简单,基于一个简单的恒等式: 设目标时间为...
继续阅读 »

2026-magic.png


今天是大年初一,江湖十年给读者朋友们拜年了,祝大家新年快乐!


又是新的一年,想必大家都没看春晚吧 😄,今天继续一年一度的用 Go 语言实现春晚魔术。


废话不多说,咱们直接看原理。


魔术原理揭秘


这个魔术的数学原理其实很简单,基于一个简单的恒等式:


设目标时间为 T
设观众说的两个数为 A 和 B
魔术师计算的第三个数为 C = T - (A + B)

那么:A + B + C = A + B + [T - (A + B)] = T

这里的 T 就是最终结果:2162227



  • 观众 A 说:1106

  • 观众 B 说:88396

  • 观众“乱按”的计算器显示:2072725

  • 相加结果:1106 + 88396 + 2072725 = 2162227


这个数字代表:2 月 16 日 22 时 27 分


理解了吗?


这里其实有个障眼法,就是魔术师现场找了三个观众“乱按”了几下,但其实谁也不确定他们按的什么,对不对,其实他们按的数字根本就没用上,而是计算器里已经预先计算出了目标时间 T 与 A + B 总和的差值。


没错,魔术这种东西就是这么朴实无华。


Go 语言实现魔术


那么接下来,就用 Go 语言来实现一个简单的计算器程序,来还原这个魔术。


首先定义一个魔术计算器:


// MagicCalculator 魔术计算器
type MagicCalculator struct {
targetTime int // 目标时间转换的数字
timestamp string // 实际时间字符串
}

接下来实现一个魔术计算器的构造方法:


// NewMagicCalculator 创建一个魔术计算器实例
func NewMagicCalculator() *MagicCalculator {
// 获取当前时间
now := time.Now()

// 生成类似 "2162227" 的时间数字
// 格式: 月(1-2 位) + 日(2 位) + 小时(2 位) + 分钟(2 位)
month := int(now.Month())
day := now.Day()
hour := now.Hour()
minute := now.Minute()

// 构建时间字符串和数字
timestamp := fmt.Sprintf("%dddd", month, day, hour, minute)

// 转换为整数
target, _ := strconv.ParseInt(timestamp, 10, 64)

return &MagicCalculator{
targetTime: int(target),
timestamp: timestamp,
}
}

当前时间 now 就是用来计算目标时间的,可以根据需要设定,这里直接使用当前时间。


target 是格式为 2162227 的时间数字,也就是咱们原理解析中的目标时间 T。


定义一个 计算魔术数字 T -(A + B)的方法:


// GetMagicNumber 计算魔术数字
func (mc *MagicCalculator) GetMagicNumber(num1, num2 int) int {
// 魔术公式: target - (num1 + num2)
return mc.targetTime - (num1 + num2)
}

最后就是定义一个交互式函数,它实现了:



  • 计算目标时间 T

  • 接收用户输入的 A、B

  • 计算观众“乱按”的第三个数字


源码如下:


func InteractiveMagic() {
fmt.Println("=== 交互式魔术体验 ===")
fmt.Println("请按照提示输入数字,我会展示魔术的原理")

mc := NewMagicCalculator()

var num1, num2 int
fmt.Print("请输入第一个数: ")
fmt.Scan(&num1)
fmt.Print("请输入第二个数: ")
fmt.Scan(&num2)

fmt.Printf("\n你输入的是: %d 和 %d\n", num1, num2)

magicNum := mc.GetMagicNumber(num1, num2)
fmt.Printf("魔术数字(第三个数)是: %d\n", magicNum)

fmt.Printf("\n验证: %d + %d + %d = %d\n", num1, num2, magicNum, mc.targetTime)
fmt.Printf("这个数字代表的时间是: %s\n", mc.timestamp)
}

程序 main 入口:


func main() {
InteractiveMagic()
}

验证魔术:


# 运行程序
$ go run main.go
=== 交互式魔术体验 ===
请按照提示输入数字,我会展示魔术的原理
请输入第一个数: 1106
请输入第二个数: 88396

你输入的是: 1106 和 88396
魔术数字(第三个数)是: 2081398

验证: 1106 + 88396 + 2081398 = 2170900
这个数字代表的时间是: 2170900

通过 go run main.go 运行程序,接下来根据提示分别输入两个数字,这里以春晚观众说的两个数字(110688396)为例,然后计算观众“乱按”的第三个数字(2081398),最终得到的目标时间是 2170900


没错,我在 2 月 17 日 09 时 00 分运行的程序。


总结


今天依旧使用 Go 语言简单实现了魔术小程序,看个乐子,开心最重要。


如果你有兴趣,完全可以通过聊天的方式让大模型生成一个带有前端界面的魔术计算器程序,体验 vibe coding 的乐趣。


本文完整代码示例我放在了 GitHub 上,欢迎点击查看。



25 年我的文章里吐槽了春晚魔术“降本增效”,在此给刘谦老师道个歉,是我冒犯了🤣之前对魔术一无所知。


25 年看了老罗采访刘谦的视频,对刘谦大佬肃然起敬 respect 🫡。


image.png



延伸阅读



联系我



作者:江湖十年
来源:juejin.cn/post/7606646387053936667
收起阅读 »

为什么Java里面,Service 层不直接返回 Result 对象?

前言 昨天在Code Review时,我发现阿城在Service层直接返回了Result对象。 指出这个问题后,阿城有些不解,反问我为什么不能这样写。 于是我们展开了一场技术讨论(battle 🤣)。 讨论过程中,我发现这个看似简单的设计问题,背后其实涉及分层...
继续阅读 »

前言


昨天在Code Review时,我发现阿城在Service层直接返回了Result对象。


指出这个问题后,阿城有些不解,反问我为什么不能这样写。


于是我们展开了一场技术讨论(battle 🤣)。


讨论过程中,我发现这个看似简单的设计问题,背后其实涉及分层架构、职责划分、代码复用等多个重要概念。


与其让这次讨论的内容随风而去,不如整理成文,帮助更多遇到同样困惑的朋友理解原因。


知其然,更知其所以然。


耐心看完,你一定有所收获。


Pokemon GIF.gif


正文


职责分离原则


在传统的MVC架构中,Service层和Controller层各自承担着不同的职责。


Service层负责业务逻辑的处理,而Controller层负责HTTP请求的处理和响应格式的封装。


当我们将数据包装成 Result 对象的任务交给 Service 层时,意味着 Service 层不再单纯地处理业务逻辑,而是牵涉到了数据处理和响应的部分。


这样会导致业务逻辑与表现逻辑的耦合,降低了代码的清晰度和可维护性。


看一个不推荐的写法:


@Service
public class UserService {
public Result<User> getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return Result.error(404, 用户不存在);
}
return Result.success(user);
}
}

@RestController
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}

上面代码中,Service 层不仅负责从数据库获取用户信息,还直接处理了返回的结果。


如果我们需要改变返回的格式,或者进行错误信息的标准化,所有 Service 层的方法都需要修改。这样会导致代码的高耦合。


相比之下,以下做法将展示逻辑留给 Controller 层,保证了业务逻辑的纯粹性:


@Service
public class UserService {
public User getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(用户不存在);
}
return user;
}
}

@RestController
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
}

让每一层都专注于自己的职责。


可复用性问题


当Service层返回Result时,会严重影响方法的可复用性。


假设我们有一个订单服务需要调用用户服务:


@Service
public class OrderService {
@Autowired
private UserService userService;

public void createOrder(Long userId, OrderDTO orderDTO) {
// 不推荐的方式:需要解包Result
Result<User> userResult = userService.getUserById(userId);
if (!userResult.isSuccess()) {
throw new BusinessException(userResult.getMessage());
}
User user = userResult.getData();

// 后续业务逻辑
validateUserStatus(user);
// ...
}
}

这种写法有个很明显的问题。


OrderService 作为另一个业务服务,业务之间的调用本来应该简单直接,但使用 Result 带来了两个问题:



  1. 不知道 Result 里到底包含什么,还得去查看代码里面的实现,写起来麻烦。

  2. 还需要额外判断 Result 的状态,增加了不必要的复杂度。


如果是调用第三方外部服务,需要这种包装还能理解,但在自己业务之间互相调用时,完全没必要这样做。


如果Service返回纯业务对象:


@Service
public class OrderService {
@Autowired
private UserService userService;

public void createOrder(Long userId, OrderDTO orderDTO) {
// 推荐的方式:直接获取业务对象
User user = userService.getUserById(userId);

// 后续业务逻辑
validateUserStatus(user);
// ...
}
}

代码变得简洁且符合直觉。


业务层之间直接传递业务对象,保持简单和清晰。


异常处理机制


有些 Service 层在业务判断失败后,会直接返回 Result.fail(xxx) 这样的代码,例如:


public Result<Void> createOrder(Long userId, OrderDTO orderDTO) {
if (userId == null) {
return Result.fail("用户ID不能为空");
}
// 后续业务逻辑
return Result.success();
}

这种做法有几个问题:



  1. 重复的错误处理:每个方法都得写一大堆类似的错误判断代码,增加了代码量。

  2. 错误分散:错误处理分散在每个方法里,如果需要改进错误逻辑,要在多个地方修改,麻烦且容易出错。


而如果我们通过抛出异常并结合全局异常处理来统一处理错误,例如:


public void createOrder(Long userId, OrderDTO orderDTO) {
if (userId == null) {
throw new BusinessException("用户ID不能为空");
}
// 后续业务逻辑
}

再通过全局异常捕获来转换为 Result


@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(400, e.getMessage());
}

@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e); // 这里可以查看堆栈信息
return Result.error(500, "系统繁忙");
}
}

这样做的好处是:



  • 减少重复代码:业务方法不再需要写重复的错误判断,代码更简洁。

  • 集中错误处理:错误处理集中在一个地方,修改时只需修改全局异常处理器,不用改动每个 Service 层方法。

  • 业务与错误分离:业务逻辑专注处理核心功能,错误处理交给统一的机制,代码更加清晰易懂。


而且异常可以携带更丰富的上下文信息,如果业务侧需要时,可以带上堆栈信息,便于一些问题的定位。


测试便利性


Service层返回业务对象而不是Result时,能够大大提升单元测试的便利性:


@SpringBootTest
public class UserServiceTest {

@Autowired
private UserService userService;

@Test
public void testGetUserById() {
// 推荐的方式:直接断言业务对象
User user = userService.getUserById(1L);
assertNotNull(user);
assertEquals(张三, user.getName());
}

@Test
public void testGetUserById_NotFound() {
// 推荐的方式:断言抛出异常
assertThrows(BusinessException.class, () -> {
userService.getUserById(999L);
});
}
}

如果Service返回Result,测试代码则需要写得更复杂:


@Test
public void testGetUserById() {
// 不推荐的方式:需要解包Result
Result<User> result = userService.getUserById(1L);
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertEquals(张三, result.getData().getName());
}

测试代码变得莫名冗长,还得去关注响应结构,这并不是Service层测试的关注点。


Service 层本应专注于业务逻辑,测试也应该直接验证业务数据。


领域驱动设计角度


再换个角度。


从领域驱动设计(DDD)的角度来看,Service 层属于应用层或领域层,应该使用领域语言来表达业务逻辑。


Result 是基础设施层的概念,代表 HTTP 响应格式,不应该污染领域层。


例如,考虑转账业务:


@Service
public class TransferService {

public TransferResult transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId);
Account toAccount = accountRepository.findById(toAccountId);

fromAccount.deduct(amount);
toAccount.deposit(amount);

accountRepository.save(fromAccount);
accountRepository.save(toAccount);

return new TransferResult(fromAccount, toAccount, amount);
}
}

在这个例子中,TransferResult 是一个领域对象,代表了转账的结果,包含了与业务相关的意义,而不是一个通用的 HTTP 响应封装 Result


这种做法更符合领域模型的表达,体现了领域层的职责——处理业务逻辑,而不是涉及 HTTP 响应格式的细节。


接口适配的灵活性


当 Service 层返回纯粹的业务对象时,Controller 层可以根据不同的接口需求灵活封装响应:


@RestController
@RequestMapping("/api")
public class UserController {

@Autowired
private UserService userService;

// REST接口返回Result
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}

// GraphQL接口直接返回对象
@QueryMapping
public User user(@Argument Long id) {
return userService.getUserById(id);
}

// RPC接口返回自定义格式
@DubboService
public class UserRpcServiceImpl implements UserRpcService {
public UserDTO getUserById(Long id) {
User user = userService.getUserById(id);
return convertToDTO(user);
}
}
}

同一个Service方法可以被不同类型的接口复用,每个接口根据自己的协议要求封装响应。


强行使用 Result 会导致接口的适配性变差,无法根据不同协议的需求灵活定制响应格式。


灵活性反而丢失了。


事务边界清晰


Service 层通常是事务边界所在,当 Service 返回业务对象时,事务的语义更加清晰:


@Service
public class OrderService {

@Transactional
public Order createOrder(OrderDTO orderDTO) {
Order order = new Order();
// 设置订单属性
orderMapper.insert(order);

// 扣减库存
inventoryService.deduct(orderDTO.getProductId(), orderDTO.getQuantity());

return order;
}
}

在这个例子中,事务是围绕 Service 层的方法展开的,@Transactional 注解确保在业务逻辑执行失败时,事务会回滚。因为方法正常返回时,事务会提交;如果抛出异常,事务会回滚,事务的边界非常明确。


如果 Service 返回的是 Result,很难界定事务是否应该回滚。比如:


public Result<Order> createOrder(OrderDTO orderDTO) {
Order order = new Order();
// 设置订单属性
orderMapper.insert(order);

// 扣减库存
Result<Void> inventoryResult = inventoryService.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
if (!inventoryResult.isSuccess()) {
return Result.fail("库存不足");
}

return Result.success(order);
}

在这种情况下,如果库存不足,虽然 Result 返回失败信息,但事务并不会回滚,可能会导致数据不一致,反而还得额外去抛出异常。


而通过抛出异常的方式,事务的回滚语义非常清晰:异常抛出则回滚,方法正常返回则提交,这种设计确保了事务的边界更加明确,避免了潜在的数据一致性问题。


写在最后


看来阿城要走的路还很长,码路漫漫,踏浪前行。


2026年,祝大家加班少,薪水多,bug少,头发多,多写点注释,少走点弯路。


人生就像一个大项目,需求多,时间紧,但没关系——bug 和头发总有一个会先来。


🤣


作者:一只叫煤球的猫
来源:juejin.cn/post/7594817135128248354
收起阅读 »

后端开发必备:生产环境异常自动电话通知方案

生产环境出bug了但没及时发现?支付接口异常导致资损?这个语音通知方案专为开发者打造,重要异常直接打电话,让你第一时间响应处理! 作为开发者,我们最怕的就是生产环境出现异常却没有及时发现。飞书群、钉钉群报警很容易错过,尤其是深夜或周末。今天分享一个专门针对开...
继续阅读 »

生产环境出bug了但没及时发现?支付接口异常导致资损?这个语音通知方案专为开发者打造,重要异常直接打电话,让你第一时间响应处理!



作为开发者,我们最怕的就是生产环境出现异常却没有及时发现。飞书群、钉钉群报警很容易错过,尤其是深夜或周末。今天分享一个专门针对开发者的语音电话通知解决方案,让重要异常第一时间电话通知到你。


🎯 典型使用场景


需要立即电话通知的开发场景:



  • 🚨 API接口异常或超时

  • 💰 支付流程异常(防止资损)

  • 📊 数据处理任务失败

  • 🔐 用户登录异常激增

  • ⚡ 核心业务逻辑报错


🚀 3步快速集成


步骤操作说明
1️⃣ 扫码注册访问 push.spug.cc,微信扫码登录
2️⃣ 创建模板新建消息模板 → 语音通道 → 语音模板 → 动态推送对象
3️⃣ 集成代码复制API地址,添加到异常处理代码中

语音通知模板配置


💻 代码集成示例


🐍 Python(异常处理)


import requests
import logging

def send_voice_alert(message, phone):
"""发送语音告警"""
url = "https://push.spug.cc/send/A27L****bgEY"
data = {'key1': message, 'targets': phone}

try:
response = requests.post(url, json=data, timeout=5)
return response.json()
except Exception as e:
logging.error(f"语音告警发送失败: {e}")

# 在异常处理中使用
try:
# 你的业务代码
process_payment(order_id)
except PaymentException as e:
# 支付异常立即电话通知
send_voice_alert(f"支付异常: {str(e)}", "186xxxx9898")
raise

☕ Java(Spring Boot异常处理)


@Component
public class VoiceAlertService {

private final RestTemplate restTemplate = new RestTemplate();
private static final String VOICE_URL = "https://push.spug.cc/send/A27L****bgEY";

public void sendVoiceAlert(String message, String phone) {
try {
Map<String, String> data = new HashMap<>();
data.put("key1", message);
data.put("targets", phone);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(data, headers);

restTemplate.postForEntity(VOICE_URL, entity, String.class);
} catch (Exception e) {
log.error("语音告警发送失败: {}", e.getMessage());
}
}
}

// 全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {

@Autowired
private VoiceAlertService voiceAlertService;

@ExceptionHandler(CriticalException.class)
public ResponseEntity<String> handleCriticalException(CriticalException e) {
// 关键异常立即电话通知
voiceAlertService.sendVoiceAlert("API异常: " + e.getMessage(), "186xxxx9898");
return ResponseEntity.status(500).body("Internal Server Error");
}
}

🔧 实际开发场景


场景1: 支付接口监控


def create_order_payment():
try:
result = payment_service.create_order()
if result.status != 'success':
send_voice_alert("支付订单创建失败", "186xxxx9898")
except Exception as e:
send_voice_alert(f"支付系统异常: {str(e)}", "186xxxx9898")

场景2: 定时任务监控


def daily_data_sync():
try:
sync_user_data()
except Exception as e:
send_voice_alert("每日数据同步失败", "186xxxx9898")
raise

场景3: API响应时间监控


@Component
public class ApiPerformanceInterceptor implements HandlerInterceptor {

@Autowired
private VoiceAlertService voiceAlertService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler)
throws Exception {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex)
throws Exception {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;

if (duration > 5000) { // 超过5秒
String message = String.format("API响应慢: %s 耗时%dms",
request.getRequestURI(), duration);
voiceAlertService.sendVoiceAlert(message, "186xxxx9898");
}
}
}

📋 参数说明


参数说明示例值
key1异常消息内容"支付接口异常"
targets接收电话的手机号"186xxxx9898"

❓ 开发者常见问题


🤔 如何避免告警风暴?
建议设置异常频率限制,同类异常5分钟内只发送一次。


💰 语音通话收费吗?

语音通话按次计费,建议只对关键异常使用,普通日志用短信或微信。


🛡️ 如何保护API安全?



  1. 不要将API地址提交到公开代码仓库

  2. 可以设置IP白名单限制调用来源

  3. 建议使用环境变量存储API地址




🎉 为什么开发者需要语音通知?


及时响应:生产故障分秒必争,电话比微信更直接

降低损失:支付、订单等关键业务异常能立即处理

简单集成:几行代码搞定,无需复杂配置

多语言支持:Python、Node.js、Java等任何语言都能用

个人友好:无需企业资质,个人开发者也能用


记住:好的开发者不是不写bug,而是能第一时间发现并修复bug!


网站链接:push.spug.cc


作者:外滩运维专家
来源:juejin.cn/post/7531844121465274377
收起阅读 »

Spring 的替代方案:Micronaut

一、为什么选择 Micronaut? 在开始编码前,先了解 Micronaut 的核心优势: 特性MicronautSpring Boot启动速度毫秒级(依赖 AOT 编译)秒级(依赖反射和动态代理)内存占用极低(适合 Serverless 环境)较高(需加载...
继续阅读 »

一、为什么选择 Micronaut?


在开始编码前,先了解 Micronaut 的核心优势:


特性MicronautSpring Boot
启动速度毫秒级(依赖 AOT 编译)秒级(依赖反射和动态代理)
内存占用极低(适合 Serverless 环境)较高(需加载完整上下文)
依赖注入编译时生成代码(无反射)运行时反射(影响性能)
响应式编程原生支持(Project Reactor)支持 WebFlux(但不如 Micronaut 集成紧密)
GraalVM 支持原生优化(直接生成原生镜像)需要额外配置(Spring Native)

适用场景:



  • 高并发、低延迟的微服务(如 API 网关、实时数据处理)。

  • Serverless 环境(如 AWS Lambda、Azure Functions)。

  • 资源受限的边缘计算设备。


二、示例项目:构建一个图书管理微服务


我们将实现一个简单的 图书管理服务,支持以下功能:



  • 添加图书(POST /books)。

  • 查询所有图书(GET /books)。

  • 根据 ID 查询图书(GET /books/{id})。


1. 初始化项目


使用 Micronaut Launch 生成项目模板:


(1) 选择 Micronaut Version:4.9.0。


(2) 语言:Java。


(3) 构建工具:Gradle(或 Maven)。


(4) 添加依赖:



  • Micronaut Data JDBC(数据库访问)。

  • Micronaut HTTP Server(Web 服务)。

  • Lombok(简化代码)。

  • H2 Database(内存数据库,便于测试)。


生成后的项目结构如下:


src/
├── main/
│ ├── java/com/cycad/micronaut/
│ │ ├── controller/ # 控制器层
│ │ ├── model/ # 数据模型
│ │ ├── repository/ # 数据访问层
│ │ └── Application.java # 主启动类
│ └── resources/
│ └── application.yml # 配置文件

2. 定义数据模型


创建 Book 实体类,使用 Lombok 简化代码:


import io.micronaut.data.annotation.AutoPopulated;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import lombok.Data;

@Data
@MappedEntity
publicclass Book {
@Id
@AutoPopulated
private Long id;
private String title;
private String author;
private Double price;
}

3. 实现数据访问层


使用 Micronaut Data JDBC 定义 BookRepository,无需编写 SQL:


import com.cycad.micronaut.model.Book;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2)
public interface BookRepository extends CrudRepository<Book, Long> {

}


4. 编写控制器层


实现 RESTful API 控制器:


import com.cycad.micronaut.model.Book;
import com.cycad.micronaut.repository.BookRepository;
import io.micronaut.http.annotation.*;
import jakarta.inject.Inject;

import java.util.List;

@Controller("/books")
publicclass BookController {

@Inject
private BookRepository bookRepository;

@Get
public List<Book> listBooks() {
return bookRepository.findAll().toList();
}

@Get("/{id}")
public Book getBookById(Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Book not found"));
}

@Post
public Book createBook(@Body Book book) {
return bookRepository.save(book);
}
}


5. 配置数据库


在 application.yml 中配置 H2 内存数据库:


# src/main/resources/application.yml
micronaut:
application:
name:book-service
server:
port:8080
datasources:
default:
url:jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
driverClassName:org.h2.Driver
username:sa
password:""
schema-generate:CREATE_DROP
dialect:H2


6. 启动服务


运行主类 Application.java:


import io.micronaut.runtime.Micronaut;

public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}


观察控制台输出,Micronaut 的启动速度极快(通常在 100ms 以内):


14:25:30.123 [main] INFO  i.m.context.env.DefaultEnvironment - Established active environments: [cli, test]
14:25:30.456 [main] INFO i.m.h.s.netty.NettyHttpServer - Server Started: http://localhost:8080


三、测试 API


使用 curl 或 Postman 测试接口:


(1) 添加图书:


curl -X POST -H "Content-Type: application/json" \
-d '{"title": "Effective Java", "author": "Joshua Bloch", "price": 45.99}' \
http://localhost:8080/books

响应:


{"id":1,"title":"Effective Java","author":"Joshua Bloch","price":45.99}

(2) 查询所有图书:


curl http://localhost:8080/books

响应:


[{"id":1,"title":"Effective Java","author":"Joshua Bloch","price":45.99}]


(3) 根据 ID 查询:


curl http://localhost:8080/books/1

响应:


{"id":1,"title":"Effective Java","author":"Joshua Bloch","price":45.99}


四、GraalVM 原生镜像


通过 GraalVM 将应用编译为原生二进制文件,进一步减少启动时间:


(1) 安装 GraalVM 和 Native Image 工具。


(2) 在 build.gradle 中添加插件:


id 'io.micronaut.application' version '3.10.0'
id 'org.graalvm.nativeimage' version '0.9.21'


(3) 执行编译命令:


./gradlew nativeImage

(4) 生成的可执行文件位于 build/native-image/,启动速度可压缩至 10ms 以内!


五、总结


Micronaut 通过 AOT 编译、低内存占用 和 快速启动 等特性,为微服务开发提供了高性能的解决方案。本文通过一个完整的图书管理服务示例,演示了其核心功能,并对比了与 Spring Boot 的性能差异。无论是构建传统微服务还是 Serverless 应用,Micronaut 都是一个值得尝试的选择。


官方文档:guides.micronaut.io/。


作者:星辰聊技术
来源:juejin.cn/post/7527884547537223690
收起阅读 »

自研第一个SKILL-openclaw入门

自研第一个SKILL-openclaw入门 openclaw不是搭建好了就结束了,用起来才能发挥作用,而SKILL,就是openclaw的灵魂,是openclaw的内功。 所以准备慢慢记录学习openclaw的skill之路。 所谓读万卷书不如行万里路,实践对...
继续阅读 »

自研第一个SKILL-openclaw入门


openclaw不是搭建好了就结束了,用起来才能发挥作用,而SKILL,就是openclaw的灵魂,是openclaw的内功。


所以准备慢慢记录学习openclaw的skill之路。


所谓读万卷书不如行万里路,实践对知识的理解非常重要,今天就准备自己开发一个简单的SKILL,来加深对SKILL的理解。


如果还不知道SKILL是什么,建议先看第一篇:


抄一个还是改一个


学字先描红,开发先抄,cv程序员的称号不是白得的。


先看看这个:


2.png


openclaw毕竟是外国人开发的,默认的skill都是国外的,比如这个天气查询,先找到这个系统自带的skill看看:


这个skill位于系统内置skill的目录:


/app/skills/weather/SKILL.md

这个SKILL的内容:


3.png
一个查询天气的skill,还挺复杂,有主要的服务,还有备份的,可惜都是国外的服务,国人用就有点水土不服了,就以他为例,改成简单的国内的:


完整的代码如下:


4.png


需要重启gateway生效,然后问一下:


1.png


万事大吉了!


小结


这是第一个自研的SKILL,功能不大,却也实用,再找一个免费的天气接口,作为备份,完善一下,就可以替代系统自带的天气SKILL了。


从简单开始,从复制开始,慢慢学习复杂一点的SKILL,开发openclaw的功能!


作者:iqiu
来源:juejin.cn/post/7605848213603074094
收起阅读 »

Kafka 4.0 正式发布,彻底抛弃 Zookeeper,队列功能来袭!

Apache Kafka 4.0 正式发布了,这是一次里程碑式的版本更新。这次更新带来的改进优化非常多,不仅简化了 Kafka 的运维,还显著提升了性能,扩展了应用场景。 我这里简单聊聊我觉得最重要的 3 个改动: KRaft 模式成为默认模式 消费者重平...
继续阅读 »

Apache Kafka 4.0 正式发布了,这是一次里程碑式的版本更新。这次更新带来的改进优化非常多,不仅简化了 Kafka 的运维,还显著提升了性能,扩展了应用场景。



我这里简单聊聊我觉得最重要的 3 个改动:



  1. KRaft 模式成为默认模式

  2. 消费者重平衡协议升级

  3. 队列功能(早期访问版本)


详细更新介绍可以参考官方文档:http://www.confluent.io/blog/introd…


KRaft 模式成为默认模式


在 Kafka 2.8 之前,Kafka 最被大家诟病的就是其重度依赖于 Zookeeper 做元数据管理和集群的高可用(ZK 模式)。在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式(Kafka Raft),不再依赖 Zookeeper,大大简化了 Kafka 的架构,让你可以以一种轻量级的、单进程的方式来使用 Kafka。



KRaft 模式在后续的版本中不断完善,直到 Kafka 3.3.1,被正式标记为生产环境可用(Production Ready)。



Kafka 4.0 则迈出了更大的一步——彻底移除了对 Zookeeper 的支持,并默认采用 KRaft 模式。


需要注意的是,Kafka 4.0 不再支持以 ZK 模式运行或从 ZK 模式迁移。如果你的 Kafka 仍然使用 ZK 模式,官方建议先升级到过渡版本(如 Kafka 3.9),执行 ZK 迁移后再升级到目标版本。


详细介绍:developer.confluent.io/learn/kraft…


消费者重平衡协议升级


全新的消费者重平衡协议正式 GA 了,可以告别“stop-the-world”重新平衡了!这个协议的核心思想最早在 Kafka 2.4 版本 通过KIP-429: Kafka Consumer Incremental Rebalance Protocol 实现。


新协议的核心在于增量式重平衡,不再依赖全局同步屏障,而是由组协调器(Gr0upCoordinator)驱动,各个消费者独立地与协调器交互,只调整自身相关的分区分配,从而将全局的“停顿”分解成多个局部的、微小的调整。只有需要调整的消费者和分区才会发生变更,未受影响的消费者可以继续正常工作(旧有的再均衡协议依赖于组范围内的同步屏障,所有消费者都需要参与,这会导致明显的“停顿”)。



详细介绍:cwiki.apache.org/confluence/…


队列功能(早期访问版本)


Kafka 4.0 通过引入共享组 (Share Gr0up) 机制提供了类似队列的功能。不过,它并非真正意义上的队列,而是利用 Kafka 已有的主题(Topic)和分区(Partition)机制,结合新的消费模式和记录确认机制来实现类似队列的行为。


Kafka 发布订阅模型Kafka 发布订阅模型


共享组解决了传统 Kafka 消费者组(Consumer Gr0ups)在某些场景下的局限性,主要体现在:



  • 支持多消费者协同消费:多个消费者可以协同消费同一主题的消息,并可以同时处理同一分区的数据。每个消息在被确认之前,都会被一个时间限制的锁机制保护,确保同一时刻只有一个消费者可以尝试处理。

  • 突破消费者组与分区数量的限制:共享组的消费者数量可以超过主题的分区数量,从而更好地支持高并发消费场景。而消费者组的并行消费能力受限于分区数量,这往往导致用户为了满足峰值负载下的并行消费需求而创建过多的分区,造成资源浪费。

  • 消息的独立确认:支持对每条消息进行独立确认、释放或拒绝,提供更精细的消息处理能力。

  • 消息投递次数记录:系统会记录每条消息的投递次数,方便处理无法处理的消息。


共享组还支持无队列深度限制和基于时间点的恢复能力,极大地扩展了 Kafka 的应用场景。


详细介绍:cwiki.apache.org/confluence/…


Kafka 常见面试题


关于 Kafka 以及其他常见消息队列的知识点/面试题总结,大家可以参考这两篇文章:



作者:JavaGuide
来源:juejin.cn/post/7485584242040062002
收起阅读 »

从Mybatis源码学会了什么

摘要:MyBatis源码展现了优秀的设计模式和架构思想。其运用建造者模式创建复杂对象,动态代理实现Mapper接口,装饰器动态扩展功能等。架构上采用清晰分层设计,插件化扩展机制实现开闭原则,多级缓存提升性能,面向接口编程降低耦合。这些设计理念对日常业务开发也有...
继续阅读 »

摘要:MyBatis源码展现了优秀的设计模式和架构思想。其运用建造者模式创建复杂对象,动态代理实现Mapper接口,装饰器动态扩展功能等。架构上采用清晰分层设计,插件化扩展机制实现开闭原则,多级缓存提升性能,面向接口编程降低耦合。这些设计理念对日常业务开发也有很大借鉴价值。



通过以下系列博文,我们熟悉了Mybatis源码实现的诸多细节,如Executor接口及其实现、缓存体系、自定义插件等。



  1. Mybatis入门到精通 一

  2. Mybatis的Executor和缓存体系

  3. Mybatis二级缓存实现详解

  4. Mybatis执行Mapper过程详解

  5. Mybatis插件原理及分页插件

  6. Spring集成Mybatis原理详解


这次,让我们跳出局部,看看Mybatis在设计和架构上,有哪些值得我们学习的地方。



注:本文中源码来自mybatis 3.4.x版本,地址github.com/mybatis/myb…



一 设计模式


1.1 建造者模式


MyBatis 大量使用建造者模式解决「复杂对象初始化」问题,比如 SqlSessionFactoryBuilderXMLConfigBuilderMapperBuilderAssistant 等。


SqlSessionFactoryBuilder为例,在创建SqlSessionFactory前需要先解析主配置文件,这个过程非常繁琐。使用建造者模式:



  • 分离「对象构建逻辑」和「对象使用逻辑」,让复杂对象创建流程清晰;

  • 建造者可以分步骤校验配置正确性,避免无效对象产生;

  • 多重载的 build 方法, 适配不同入参场景 。


public class SqlSessionFactoryBuilder {

public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}

// 简化代码:reader就是配置文件的读取流
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}
}

1.2 工厂模式


工厂模式隐藏对象创建细节,上层无需关心「具体实现类」,只依赖接口。例如



  • SqlSessionFactory 负责创建 SqlSession,提供了多个重载实现。

  • Executor、StatementHandler、ResultSetHandler、ParameterHandler只能由 Configuration 中的 newExecutor 等方法创建,根据配置选择不同实现类;同时应用自定义插件。


1.3 动态代理


MyBatis 核心的特性之一是「Mapper 接口无需实现类」,底层通过 JDK 动态代理,实现接口方法调用触发SQL执行。详见 MapperProxyFactory,缓存方法元数据,避免重复解析。


// 缓存mapper方法元数据,避免重复解析
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

此外,插件原理也是动态代理,定义Interceptor接口声明代理逻辑、代理对象创建等。使用户可以自定义插件实现。


1.4 模板方法



  • 模板方法将「不变的通用逻辑」抽离到父类,「可变的差异化逻辑」交给子类实现;

  • 避免子类重复编写通用代码(如缓存检查、参数校验),降低代码冗余;

  • 父类控制流程,子类只关注核心逻辑,符合「开闭原则」。


BaseExecutor 实现了模板方法模式,将 Executor 的通用流程(如参数处理、SQL 执行、结果集处理)抽象为模板方法,具体子类(SimpleExecutor/BatchExecutor/ReuseExecutor)只需实现差异化逻辑。


// 模板方法,一级缓存使用逻辑
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// ...,会调用doQuer()

}

// 由子类实现
protected abstract <E> List<E> doQuery(...)

1.5 装饰模式


用装饰模式替代继承,无需定义子类,就能给对象动态增加职责。


如通过CachingExecutor装饰,给BaseExecutor增加二级缓存能力。


public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 对BaseExecutor子类进行装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 省略...
}


如Cache接口体系,定义了很多装饰器,根据用户配置,实现功能特性自由组合。


1.6 策略模式


Executor接口可根据用户配置,运行时可切换SimpleExecutor、ReuseExecutor或BatchExecutor,实现不同的SQL执行策略。


RoutingStatementHandler 根据配置路由到不同的 StatementHandler实现。


1.7 注册中心


源码中大量使用 Registry 模式来管理可扩展组件, 可以统一初始化、统一查找、统一生命周期;例如:



  • TypeHandlerRegistry

  • MapperRegistry

  • LanguageDriverRegistry

  • CacheRegistry



二 架构思维


2.1 分层设计


MyBatis的核心分层非常清晰,每层只做一件事,符合「单一职责原则」:
image.png



  • 各层职责

    • Mapper 层:用户接口,定义 SQL 操作;

    • SqlSession 层:会话入口,封装 Executor 调用;

    • Executor 层:执行器,处理缓存、事务、SQL 执行流程;

    • StatementHandler 层:处理 SQL 拼接、Statement 创建;

    • Parameter/ResultSetHandler 层:参数绑定、结果集映射;



  • 架构思维

    • 分层降低耦合:修改结果集映射逻辑,不会影响 Executor 层;

    • 每层依赖「接口」而非「实现」:比如 Executor 是接口,具体实现可替换;

    • 分层便于测试:可单独测试 ParameterHandler 的参数绑定逻辑。




2.2 插件化设计


MyBatis 提供了插件扩展机制(Interceptor),允许开发者通过拦截器增强核心组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的功能,实现如分页、日志、加密等。



  • 核心设计

    • 定义 Interceptor 接口,开发者实现 intercept 方法即可拦截目标方法;

    • 通过 @Intercepts 注解指定拦截的类和方法,无需修改源码;

    • 拦截器链(InterceptorChain)通过动态代理层层包装目标对象,保证插件的有序执行;



  • 架构思维

    • 「开闭原则」的良好体现:框架核心功能固定,扩展功能通过插件实现,无需修改源码;

    • 「责任链模式」:多个插件按顺序执行,每个插件只处理自己的逻辑,互不干扰;

    • 扩展点设计要「最小化」:只开放核心组件的关键方法,避免过度暴露内部逻辑。




2.3 多级缓存


MyBatis 实现了「一级缓存(SqlSession 级别)+ 二级缓存(Mapper 级别)」的多级缓存:



  • 一级缓存:BaseExecutor 中的 localCache(PerpetualCache),默认开启,SqlSession 关闭后失效;

  • 二级缓存:CachingExecutor 包装普通 Executor,缓存数据存入 Mapper 对应的 Cache 对象,跨 SqlSession 共享;


同时支持集成 Redis/Ehcache 等第三方缓存 。


2.4 面向接口编程


MyBatis 全程贯彻「依赖倒置原则」,完全面向接口编程:接口定义「做什么」,实现类定义「怎么做」,替换实现类不影响上层逻辑;



  • 核心组件都是接口:ExecutorStatementHandlerParameterHandlerResultSetHandlerSqlSession 等;

  • 上层代码只依赖接口:比如 SqlSessionselectList 方法,底层调用的是 Executor 接口的 query 方法,无需关心具体是 SimpleExecutor 还是 BatchExecutor。


2.5 约定优于配置


MyBatis 大量使用约定来减少配置,通过约定可以显著降低配置量,提升开发体验。例如:



  • Mapper 接口与 XML 文件同名同包;

  • 方法名与 SQL ID 一致;

  • 结果集字段与 JavaBean 属性自动映射;

  • StatementHandler接口默认使用PreparedStatementHandler


2.6 架构思维总结


MyBatis源码体现了简单而不简陋的设计哲学:



  1. 高内聚低耦合 - 模块职责清晰,依赖抽象

  2. 开闭原则 - 对扩展开放,对修改关闭

  3. 组合优于继承 - 装饰器、代理模式的应用

  4. 关注点分离 - SQL、映射、执行逻辑分离

  5. 性能优化 - 缓存、延迟加载、连接复用


上述技巧和思维不仅适用于框架开发,在日常业务开发中也能直接落地。



  1. 小型项目:学习其简洁的API设计

  2. 中型项目:借鉴其分层架构和模式应用

  3. 大型项目:参考其扩展机制和插件体系


作者:程序员侠客行
来源:juejin.cn/post/7598477092197220390
收起阅读 »

除夕夜,国产顶流压轴上线,QWEN3.5多模态开源!

除夕夜,老金我刚咬了一口韭菜鸡蛋饺子。 手机"叮"的一声,弹出个通知。 老金我瞄了一眼——Qwen3.5,上线了。饺子差点没喷出来。 赶紧打开 chat.qwen.ai,两个模型直接挂在上面,可以用了。 阿里这帮人,大年三十放大招,连个发布会都没开,就这么安安...
继续阅读 »

Image


除夕夜,老金我刚咬了一口韭菜鸡蛋饺子。
手机"叮"的一声,弹出个通知。
老金我瞄了一眼——Qwen3.5,上线了。饺子差点没喷出来。


赶紧打开 chat.qwen.ai,两个模型直接挂在上面,可以用了。
阿里这帮人,大年三十放大招,连个发布会都没开,就这么安安静静地把东西甩出来了。


老金我放下筷子,扒了一晚上代码和文档,确认了一件事:
这不是小版本迭代,这是架构级别的重构。


Image




先说结论:Qwen3.5到底升级了什么


根据老金我除夕夜扒的HuggingFace代码库、阿里云官网和chat.qwen.ai的实际体验,帮你梳理了3个核心变化。


第一个:原生多模态。
注意,是"原生",不是"拼接"。
Qwen3之前的多模态方案是语言模型+视觉模块的两段式架构。
Qwen3.5直接把视觉感知和语言推理塞进了同一个训练框架。


阿里云官网对Qwen3.5-Plus的描述是:"原生多模态合一训练,混合架构双创新突破。"
简单说,以前是两个人配合干活,现在是一个人同时搞定。


第二个:Gated Delta Networks——线性注意力机制。
官方确认,Qwen3.5采用了一种叫 Gated Delta Networks 的线性注意力,跟传统的Gated Attention做了混合架构。
传统Transformer的注意力计算量跟序列长度的平方成正比,Gated Delta Networks把这个关系拉成线性。


翻译成人话:处理长文本的速度快了,显存占用也降了。
而且不是快了一点半点——官方实测数据:



  • 在32k上下文长度下,Qwen3.5-397B-A17B的解码吞吐量是Qwen3-Max的 8.6倍

  • 在256k上下文长度下,这个数字是 19.0倍

  • 跟Qwen3-235B-A22B比,分别是3.5倍和7.2倍


老金我看到这个数据的时候饺子真喷出来了。


第三个:更大的模型家族。
目前在chat.qwen.ai上已经可以直接使用的有两个版本:



  • Qwen3.5-Plus(闭源API模型,通过阿里云百炼提供服务,支持 1M token上下文窗口)

  • Qwen3.5-397B-A17B(开源旗舰模型,3970亿参数只激活170亿)


跟之前HuggingFace代码里泄露的9B和35B-A3B相比,正式发布的模型规模大得多。
3970亿总参数,比Qwen3的旗舰235B-A22B直接翻了快一倍。


总参数量达3970亿,每次前向传播仅激活170亿参数,在保持能力的同时优化速度与成本。


语言与方言支持从119种扩展至201种,词表从15万扩大到25万,在多数语言上带来约10-60%的编码/解码效率提升。
简单说,同样的一段话,Qwen3.5能用更少的token表示,推理更快,API费用也更省。


Image




线性注意力到底意味着什么


这块稍微展开说一下,因为这可能是Qwen3.5最关键的技术突破。
不懂技术的朋友别跳过,老金我用人话给你翻译。


传统Transformer用的是标准自注意力机制。
简单理解:AI在读一篇文章的时候,每读到一个字,都要回头看一遍前面所有的字。


如果文章有1万个字,每个字要跟其他9999个字各看一次。
字数越多,AI就越吃力——计算量是"字数的平方"级别的。


Qwen3.5用的Gated Delta Networks,核心思路是:用一个巧妙的数学方法,让AI不用每次都回头看所有内容。
结果就是:计算量从"字数的平方"降到"字数的倍数"。


听起来差别不大?我给你举个具体例子:


处理一个10分钟的视频:



  • 传统方式:可能需要64G显存的显卡才能跑

  • Gated Delta Networks:16G显存就够了


这不是快了几个百分点的问题,是能不能跑起来的问题。
很多任务以前根本跑不动,现在可以了。


Qwen3.5更聪明的地方在于:它把Gated Delta Networks(线性注意力)和Gated Attention(标准注意力)做成了 混合架构。
简单任务用线性注意力省资源,复杂任务自动切换到标准注意力保精度。
不是非此即彼,而是动态选择——什么场景用什么方案。


这也是为什么官方说的"Qwen3-Next架构"——更高稀疏度的MoE + 混合注意力 + 多token预测。


多token预测是什么意思?
传统模型一次只能"想"出一个字,Qwen3.5一次能预测多个字,生成速度又快了一截。


Image


原生多模态为什么重要


之前的多模态模型大多是"拼接式"的。
打个比方:就像找了一个英语翻译和一个法语翻译,中间再安排一个协调员把两人的翻译对接起来。


先训一个语言模型(处理文字),再训一个视觉编码器(处理图片),最后用对齐层把两者连起来。
这种方式有个天然缺陷:视觉和语言的理解是割裂的。


Qwen3.5走的是另一条路——从预训练阶段就把文本、图像、视频放在一起训。
模型从一开始就"看"和"读"同时进行。
就像培养一个从小就双语环境长大的孩子,不需要翻译,直接理解。


阿里官方说法是"统一架构整合语言推理与视觉感知"。


这对普通用户来说意味着什么?
1、你发一张图给AI,它能真正"看懂"图里的内容,不容易出现"看到了但理解错了"的情况
2、一次对话就能同时处理图片+文字,不用分两步操作
3、成本更低——一个模型干两个模型的活,API费用直接砍半


阿里官网已经写了"效果、成本与多模态理解深度上同时超越Qwen3-Max与Qwen3-VL"。
如果这个说法成立,那Qwen3.5-Plus可能是目前性价比最高的多模态模型之一。


比如这样提问,它都能准确且快速的回答:


跑分亮了:Qwen3.5到底有多强


说技术架构大家可能没直觉,直接看跑分数据。
官方放了一大堆benchmark对比,老金我帮你提炼最关键的几个:


自然语言能力(对比GPT5.2、Claude 4.5 Opus、Gemini-3 Pro):


Image


几个重点:


1、指令遵循(IFBench 76.5)和多语言挑战(MultiChallenge 67.6)两项全场第一。
这意味着你给它的指令它听得更准,不容易跑偏。


2、搜索Agent能力(BrowseComp 78.6)也是第一。
联网搜索信息的能力很强。


3、多语言能力(NOVA-63 59.1)第一。
201种语言不是白支持的。


4、编程和数学还是GPT5.2和Claude强一些,但差距不大。


视觉语言能力(这才是Qwen3.5的杀手锏):


Image


乖乖,视觉能力这块Qwen3.5真的杀疯了:



  • MathVision 88.6——看图做数学题,全场最高

  • OCRBench 93.1——文字识别能力,直接碾压,比GPT5.2高出12个点

  • OmniDocBench 90.8——文档理解能力第一,对搞办公的朋友来说太实用了

  • HallusionBench 71.4——幻觉最少,看到什么说什么,不瞎编

  • AndroidWorld 66.8——能操作安卓手机,这个后面单独说


注意,这是一个3970亿参数只激活170亿的模型跑出来的成绩。
跟GPT5.2这种完整版闭源大模型对打还能在多个维度赢,开源模型能做到这个水平,老金我服了。


Image




Visual Agent:AI能操作你的手机和电脑了


这是老金我觉得最炸裂的功能,但很多报道都没重点说。
Qwen3.5可以作为 视觉智能体,自主操作手机和电脑完成日常任务。


什么意思?你告诉它"帮我把这个Excel表格的缺失行补全",它真的能:
1、打开Excel文件
2、识别出哪些行和列需要补全
3、自动填写数据
4、保存文件


Image


全程不需要你动手,AI自己操作界面完成。
官方展示了好几个演示:



  • 手机端:适配主流App,你说"帮我发条朋友圈",它能自己操作完成

  • 电脑端:处理跨应用的数据整理、多步骤流程自动化


AndroidWorld跑分66.8,目前公开数据里最高的。
这不是ChatGPT那种"帮你写个脚本自己跑"。
Qwen3.5是真的在操作GUI界面,像人一样点击、输入、滑动。


对于不会编程的普通用户来说,这个能力可能比会写代码更有用。


空间智能和视觉编程


除了操作手机电脑,Qwen3.5在"看"这件事上还有两个特别的能力。


空间智能:
借助对图像像素级位置信息的建模,Qwen3.5能做到:



  • 物体计数——图里有几个苹果,它能数准

  • 相对位置判断——电话亭在黄色货车的左边还是右边

  • 驾驶场景理解——看行车记录仪画面,分析为什么没在路口停车


官方展示了一个驾驶场景的例子:给它一段行车记录仪视频截帧,它能分析出"信号灯在我接近停车线时变黄,此时距离太近无法安全停车,所以选择通过路口"。
这个能力在自动驾驶和机器人导航场景里非常关键。


视觉编程:
更酷的是,Qwen3.5能把看到的东西变成代码:



  • 手绘界面草图 → 结构清晰的前端代码

  • 游戏视频 → 逻辑还原代码

  • 长视频 → 自动提炼为结构化网页


你甚至可以让他看视频手搓游戏。


Image


如果对你有帮助,记得关注一波~




春节档:AI圈的神仙打架


Qwen3.5选在除夕夜发布,这个时间点太狠了。
这个春节档,至少还有3个重磅选手要登场。


1、DeepSeek V4——最受期待的选手,V3已经证明了DeepSeek的实力
2、GLM-5——智谱的新旗舰,之前Pony Alpha的表现已经让人刮目相看
3、MiniMax 2.2——M2.5编程能力追平Claude,2.2值得关注


老金我觉得今年春节档的竞争格局跟去年完全不同。
去年是DeepSeek V3一家独大。
今年是四五个玩家同时出牌。


对普通用户来说,这其实是好事。
竞争越激烈,开源模型的能力提升越快,API价格越便宜。


MoE架构:小身材大能量


Qwen3.5-397B-A17B这个版本号值得单独说一下。
397B是总参数量,A17B是激活参数量——3970亿参数里每次只用170亿。


什么意思?打个比方:
这就像一个公司有3970个员工,但每次处理一个任务只需要170个人同时干活。
其他人"待命",等需要的时候再上。


这就是MoE(Mixture of Experts,混合专家)架构的核心思路。
模型里有很多"专家"模块,每个token只激活其中几个。
好处是:模型容量大(知识多),但推理成本低(算得快)。


回顾一下Qwen3的数据:


Qwen3-235B-A22B(2350亿参数,激活220亿)在编程、数学、推理上已经能跟DeepSeek-R1、GPT-5正面对决。
Qwen3-30B-A3B在SWE-Bench上拿到69.6分,价格性能比吊打一众付费模型。


Qwen3.5-397B-A17B直接把总参数量拉到3970亿,是Qwen3旗舰的1.7倍。
但激活参数只有170亿,比Qwen3旗舰的220亿还少。


翻译成人话:知识储备更多了,但跑起来反而更省资源。
再加上原生多模态和线性注意力的加持,老金我认为这是2026年上半年最值得关注的开源模型之一。


Image


现在就能用:3步上手Qwen3.5


说了这么多技术细节,老金我讲讲实际怎么用。
好消息是:你现在就可以直接体验Qwen3.5,不用等。


第1步:打开 chat.qwen.ai
浏览器直接输入 chat.qwen.ai,这是阿里官方的对话平台。
注册一个账号就能用,支持手机号和邮箱注册。
不需要科学上网,国内直接访问。


第2步:选模型和模式
页面顶部有个模型选择器,点开会看到两个选项:



  • Qwen3.5-Plus:推荐日常使用,速度快,响应快

  • Qwen3.5-397B-A17B:旗舰模型,适合复杂任务(推理、写代码、分析长文档)


不知道选哪个?选Qwen3.5-Plus就行,够用了。
需要更强的推理能力再切397B。


选好模型后,还能选三种思考模式:



  • 自动(auto):自适应思考,该深入就深入,该快就快,推荐大多数场景使用

  • 思考(thinking):遇到难题用这个,模型会进行深度推理,一步步想清楚再回答

  • 快速(fast):简单问题用这个,不消耗思考token,回答又快又省


第3步:直接对话
跟ChatGPT的用法一模一样——输入框打字,回车发送。
支持的功能包括:



  • 纯文字对话(问答、写作、翻译、编程)

  • 上传图片让它分析(产品截图、文档照片、手写笔记)

  • 上传文件让它总结(PDF、Word、代码文件)

  • 联网搜索(点击搜索按钮,它会帮你查最新信息)


完全免费,目前没有次数限制。


对,你没看错,免费的。
这也是阿里开源生态的一贯打法。


开发者进阶用法


如果你是开发者,除了网页版还有更多玩法。


场景1:API调用(1M上下文窗口)
阿里云百炼已经上线Qwen3.5-Plus的API,支持100万token的上下文窗口。
100万token是什么概念?大概相当于一次性读完一本750页的英文小说还绰绰有余。


而且API完全兼容OpenAI格式,切换成本几乎为零:


from openai import OpenAI

client = OpenAI(
    api_key="your-api-key",
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

completion = client.chat.completions.create(
    model="qwen3.5-plus",
    messages=[{"role""user""content""介绍一下Qwen3.5"}],
    extra_body={
        "enable_thinking": True,
        "enable_search": False
    },
    stream=True
)

两个关键参数:



  • enable_thinking:开启推理模式,让模型先想再答,适合复杂问题

  • enable_search:开启联网搜索和Code Interpreter


场景2:Vibe Coding(跟编程工具集成)
官方明确说了,百炼API可以跟这些编程工具无缝集成:



  • Qwen Code——阿里自己的编程助手

  • Claude Code——Anthropic的CLI工具

  • Cline——VS Code插件

  • OpenClaw——开源Agent框架

  • OpenCode——开源编程工具


也就是说,你在Claude Code里把模型切成Qwen3.5-Plus,一样能用。
价格比GPT-5便宜10倍以上,对于日常编程来说性价比拉满。


场景3:多模态应用
原生多模态意味着你可以用一个模型搞定:



  • 图片内容识别+文案生成

  • 视频内容理解+摘要提取

  • 图文混排文档的解析和问答

  • GUI自动化——让AI帮你操作软件界面


以前这些任务要调3-4个不同的API,现在一个就够了。


场景4:本地部署
Qwen3.5-397B-A17B虽然总参数3970亿,但激活参数只有170亿。
等开源权重发布后,用Ollama或vLLM部署,消费级显卡也有可能跑起来。
后续如果有更小的版本(比如9B),16G显存的显卡就能流畅运行。


老金的判断


Qwen3.5除夕夜在chat.qwen.ai正式上线了。
老金我说说自己的看法。


看好的点:



  • 原生多模态是正确的方向,拼接式迟早要被淘汰

  • Gated Delta Networks解决了长序列的核心瓶颈,8.6倍/19倍的吞吐量提升不是闹着玩的

  • MoE架构在成本和性能之间找到了平衡点——3970亿参数只激活170亿,这个比例很激进

  • 视觉能力真的强——OCR、文档理解、数学视觉多项第一

  • Visual Agent能操作手机电脑,这是AI从"回答问题"到"替你干活"的关键一步

  • 阿里在开源这条路上一直很坚定,Qwen3的开源质量有目共睹

  • 完全免费使用,对普通用户来说门槛为零


值得关注的未来方向:
官方博客最后提了三个方向,老金我觉得每个都很重要:
1、跨会话持久记忆——现在的AI每次对话都是"失忆"状态,未来能记住你之前聊过什么
2、具身接口——不只是操作手机电脑屏幕,未来可能控制机器人在真实世界干活
3、自我改进机制——AI能自己变得更好,不需要人类手动更新


阿里原话是:"将当前以任务为边界的助手升级为可持续、可信任的伙伴。"


老金我的态度是谨慎乐观。
架构升级的方向是对的,除夕夜放这个大招,阿里是真的有底气。


跑分数据已经出来了,视觉能力多项碾压GPT5.2和Claude 4.5 Opus,你现在就可以去chat.qwen.ai亲自试试。


有一点可以确定:2026年的开源大模型,竞争只会越来越激烈。
对于开发者和普通用户来说,这是最好的时代。




往期推荐:


AI编程教程列表
提示词工工程(Prompt Engineering)
LLMOPS(大语言模运维平台)
AI绘画教程列表
WX机器人教程列表




每次我都想提醒一下,这不是凡尔赛,是希望有想法的人勇敢冲。
我不会代码,我英语也不好,但是我做出来了很多东西,在文末的开源知识库可见。
我真心希望能影响更多的人来尝试新的技巧,迎接新的时代。


谢谢你读我的文章。
如果觉得不错,随手点个赞、在看、转发三连吧🙂
如果想第一时间收到推送,也可以给我个星标⭐~谢谢你看我的文章。


开源知识库地址:
tffyvtlai4.feishu.cn/wiki/OhQ8wq…


作者:老金带你玩AI
来源:juejin.cn/post/7606555195753873408
收起阅读 »

JDK 25(长期支持版) 发布,新特性解读!

JDK 25 重磅发布了!这是一个非常重要的版本,里程碑式。 JDK 25 是 LTS(长期支持版),至此为止,有 JDK8、JDK11、JDK17、JDK21 和 JDK 25 这五个长期支持版了。 JDK 21 共有 18 个新特性,这篇文章会挑选其中较为...
继续阅读 »

JDK 25 重磅发布了!这是一个非常重要的版本,里程碑式。


JDK 25 是 LTS(长期支持版),至此为止,有 JDK8JDK11JDK17JDK21 和 JDK 25 这五个长期支持版了。


JDK 21 共有 18 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:



  • JEP 506: Scoped Values (作用域值)

  • JEP 512: Compact Source Files and Instance Main Methods (紧凑源文件与实例主方法)

  • JEP 519: Compact Object Headers (紧凑对象头)

  • JEP 521: Generational Shenandoah (分代 Shenandoah GC)

  • JEP 507: Primitive Types in Patterns, instanceof, and switch (模式匹配支持基本类型, 第三次预览)

  • JEP 511: Module Import Declarations (模块导入声明)

  • JEP 513: Flexible Constructor Bodies (灵活的构造函数体)

  • JEP 508: Vector API (向量 API, 第十次孵化)


其实里面的很多新特性在之前的版本中就多次提到了,这里只是转正或者再次预览。


下图是从 JDK 8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:



JEP 506: 作用域值


作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量 ThreadLocal ,尤其是在使用大量虚拟线程时。


final static ScopedValue<...> V = new ScopedValue<>();

// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get() ...

作用域值通过其“写入时复制”(copy-on-write)的特性,保证了数据在线程间的隔离与安全,同时性能极高,占用内存也极低。这个特性将成为未来 Java 并发编程的标准实践。


JEP 512: 紧凑源文件与实例主方法


该特性第一次预览是由 JEP 445 (JDK 21 )提出,随后经过了 JDK 22 、JDK 23 和 JDK 24 的改进和完善,最终在 JDK 25 顺利转正。


这个改进极大地简化了编写简单 Java 程序的步骤,允许将类和主方法写在同一个没有顶级 public class的文件中,并允许 main 方法成为一个非静态的实例方法。


class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}

进一步简化:


void main() {
System.out.println("Hello, World!");
}

这是为了降低 Java 的学习门槛和提升编写小型程序、脚本的效率而迈出的一大步。初学者不再需要理解 public static void main(String[] args) 这一长串复杂的声明。对于快速原型验证和脚本编写,这也使得 Java 成为一个更有吸引力的选择。


JEP 519: 紧凑对象头


该特性第一次预览是由 JEP 450 (JDK 24 )提出,JDK 25 就顺利转正了。


通过优化对象头的内部结构,在 64 位架构的 HotSpot 虚拟机中,将对象头大小从原本的 96-128 位(12-16 字节)缩减至 64 位(8 字节),最终实现减少堆内存占用、提升部署密度、增强数据局部性的效果。


紧凑对象头并没有成为 JVM 默认的对象头布局方式,需通过显式配置启用:



  • JDK 24 需通过命令行参数组合启用:
    $ java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ...

  • JDK 25 之后仅需 -XX:+UseCompactObjectHeaders 即可启用。


JEP 521: 分代 Shenandoah GC


Shenandoah GC 在 JDK12 中成为正式可生产使用的 GC,默认关闭,通过 -XX:+UseShenandoahGC 启用。


Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等


传统的 Shenandoah 对整个堆进行并发标记和整理,虽然暂停时间极短,但在处理年轻代对象时效率不如分代 GC。引入分代后,Shenandoah 可以更频繁、更高效地回收年轻代中的大量“朝生夕死”的对象,使其在保持极低暂停时间的同时,拥有了更高的吞吐量和更低的 CPU 开销。


Shenandoah GC 需要通过命令启用:



  • JDK 24 需通过命令行参数组合启用:-XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational

  • JDK 25 之后仅需 -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational 即可启用。


JEP 507: 模式匹配支持基本类型 (第三次预览)


该特性第一次预览是由 JEP 455 (JDK 23 )提出。


模式匹配可以在 switchinstanceof 语句中处理所有的基本数据类型(int, double, boolean 等)


static void test(Object obj) {
if (obj instanceof int i) {
System.out.println("这是一个int类型: " + i);
}
}

这样就可以像处理对象类型一样,对基本类型进行更安全、更简洁的类型匹配和转换,进一步消除了 Java 中的模板代码。


JEP 505: 结构化并发(第五次预览)


JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent,目前处于孵化器阶段。


结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。


结构化并发的基本 API 是StructuredTaskScope,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。


StructuredTaskScope 的基本用法如下:


    try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close

结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。


JEP 511: 模块导入声明


该特性第一次预览是由 JEP 476 (JDK 23 )提出,随后在 JEP 494 (JDK 24)中进行了完善,JDK 25 顺利转正。


模块导入声明允许在 Java 代码中简洁地导入整个模块的所有导出包,而无需逐个声明包的导入。这一特性简化了模块化库的重用,特别是在使用多个模块时,避免了大量的包导入声明,使得开发者可以更方便地访问第三方库和 Java 基本类。


此特性对初学者和原型开发尤为有用,因为它无需开发者将自己的代码模块化,同时保留了对传统导入方式的兼容性,提升了开发效率和代码可读性。


// 导入整个 java.base 模块,开发者可以直接访问 List、Map、Stream 等类,而无需每次手动导入相关包
import module java.base;

public class Example {
public static void main(String[] args) {
String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> fruitMap = Stream.of(fruits)
.collect(Collectors.toMap(
s -> s.toUpperCase().substring(0, 1),
Function.identity()));

System.out.println(fruitMap);
}
}

JEP 513: 灵活的构造函数体


该特性第一次预览是由 JEP 447 (JDK 22)提出,随后在 JEP 482 (JDK 23)和 JEP 492 (JDK 24)经历了预览,JDK 25 顺利转正。


Java 要求在构造函数中,super(...)this(...) 调用必须作为第一条语句出现。这意味着我们无法在调用父类构造函数之前在子类构造函数中直接初始化字段。


灵活的构造函数体解决了这一问题,它允许在构造函数体内,在调用 super(..)this(..) 之前编写语句,这些语句可以初始化字段,但不能引用正在构造的实例。这样可以防止在父类构造函数中调用子类方法时,子类的字段未被正确初始化,增强了类构造的可靠性。


这一特性解决了之前 Java 语法限制了构造函数代码组织的问题,让开发者能够更自由、更自然地表达构造函数的行为,例如在构造函数中直接进行参数验证、准备和共享,而无需依赖辅助方法或构造函数,提高了代码的可读性和可维护性。


class Person {
private final String name;
private int age;

public Person(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative.");
}
this.name = name; // 在调用父类构造函数之前初始化字段
this.age = age;
// ... 其他初始化代码
}
}

class Employee extends Person {
private final int employeeId;

public Employee(String name, int age, int employeeId) {
this.employeeId = employeeId; // 在调用父类构造函数之前初始化字段
super(name, age); // 调用父类构造函数
// ... 其他初始化代码
}
}

JEP 508: 向量 API(第十次孵化)


向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。


向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。


这是对数组元素的简单标量计算:


void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}

这是使用 Vector API 进行的等效向量计算:


static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.int0Array(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}

尽管仍在孵化中,但其第十次迭代足以证明其重要性。它使得 Java 在科学计算、机器学习、大数据处理等性能敏感领域,能够编写出接近甚至媲美 C++等本地语言性能的代码。这是 Java 在高性能计算领域保持竞争力的关键。


从 JDK8 到 JDK24 每一版版本的新特性详细介绍,可以在 JavaGuide 官方网站( javaguide.cn )上找到。


作者:JavaGuide
来源:juejin.cn/post/7551104474233831475
收起阅读 »

FastExcel消失了,原来捐给了Apache

关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 FastExcel仅存在江湖上出现了两年,可能很多开发者还不知道这个项目。但是说到阿里的EasyExcel,大家肯定耳熟能详。 没错,FastExcel就是EasyExcel的作者离开阿里之后,...
继续阅读 »

关注我的公众号:【编程朝花夕拾】,可获取首发内容。



01 引言


FastExcel仅存在江湖上出现了两年,可能很多开发者还不知道这个项目。但是说到阿里的EasyExcel,大家肯定耳熟能详。


没错,FastExcel就是EasyExcel的作者离开阿里之后,重新维护的加强版EasyExcel,而此后,阿里的EasyExcel宣布不再更新进入维护期。


这两天,无意间看到一篇文章介绍的Apache新项目,怎么看怎么眼熟,和FastExcel如出一撤。了解下来,才发现原来是同一个项目,只是背景更加强大了。


02 Fesod



2.1 简介


Apache Fesod (Incubating)是一个高性能、内存高效的 Java 库,用于读写电子表格文件,旨在简化开发并确保可靠性。


Apache Fesod (Incubating) 可以为开发者和企业提供极大的自由度和灵活性。我们计划在未来引入更多新功能,以持续提升用户体验和工具可用性。Apache Fesod (Incubating) 致力于成为您处理电子表格文件的最佳选择。


名称 fesod(发音为 /ˈfɛsɒd/),是 fast easy spreadsheet and other documents(快速简单的电子表格和其他文档)的首字母缩写,表达了项目的起源、背景和愿景。


Apache Fesod目前处于孵化器,还没有正式毕业。最低的Java版本也必须是1.8


GitHub地址:github.com/apache/feso…


官网地址:fesod.apache.org/


2.2 Maven依赖


以后要使用的依赖:


<dependency>
<groupId>org.apache.fesod</groupId>
<artifactId>fesod</artifactId>
<version>version</version>
</dependency>

由于目前正处于Apache的孵化期,暂时没有稳定版本。要使用的话,目前最新的fastexcel 1.3.0的版本。


<dependency>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>1.3.0</version>
</dependency>


2.3 大致时间线



  • 2024.09.11 easyexcel发布最后一个稳定版本,easyexcel 4.0.3

  • 2024.11.06 easyexcel阿里官方宣布停更。只修复BUG

  • 2024.12.05 easyexcel作者新开仓库,取名FastExcel,并发布第一个版本,fastexcel 1.0.0

  • 2025.01.14 fastExcel 发布第二个版本稳定版,fastexcel 1.1.0

  • 2025.04.14 fastExcel 发布第三个版本稳定版,fastexcel 1.2.0

  • 2025.08.23 fastExcel发布最后一个稳定版本,fastexcel 1.3.0

  • 2025.09.04 easyexcelGitHub仓库归档,仅可读

  • 2025.09.17 fastExcel正式进入Apache服化器,更名Fesod


从此,正式成为Apache的产物,所谓Apache出品必是精品,这么强大的维护团队,期待更多的功能以及更好的性能。


其实在FastExcel作者创建仓库时,第一次的名字并不是FastExcel,好像是EasyExcel plus,具体什么不记得了。但确实存在过。


2.4 怀疑


网上搜了一下fastExcel捐给Apache的消息有限,并没有官方说明。还特意看了下Apache Fesod团队的人员有没有Fastexcel的作者。看了之后确实有。



2.5 熟悉的味道


案例这里就不在赘述,我们看看官方即可:



新的项目使用FesodSheet调起读写方法,其他和原来的一致。


03 小结


不追求新功能的可以继续使用原来的fastexcel或者easyexcel,大部分场景,简单的导入导出功能已经足够使用。渴望新功能的,可以期待一下fesod的正式版。


作者:SimonKing
来源:juejin.cn/post/7598071804969812006
收起阅读 »

Tomcat 与 Nginx、Apache 的区别是什么?

这个问题本身有个误解:把三个东西都叫「web server」,会让人以为它们是同一种东西的三种实现。其实不是。Nginx 和 Apache 是 HTTP 服务器,Tomcat 是 Servlet 容器,它们干的活不在一个层次上。 Nginx 和 Apache(...
继续阅读 »

这个问题本身有个误解:把三个东西都叫「web server」,会让人以为它们是同一种东西的三种实现。其实不是。Nginx 和 Apache 是 HTTP 服务器,Tomcat 是 Servlet 容器,它们干的活不在一个层次上。


Nginx 和 Apache(一般说的 Apache 指的是 Apache HTTP Server,也就是 httpd)是 HTTP 服务器:收 HTTP 请求、按配置干活、回 HTTP 响应。它们擅长扛静态文件、做反向代理、做负载均衡,但它们不执行 Java 代码。你打一个 .war 包丢给 Nginx,Nginx 不知道怎么处理——它只会返回 404 或者把请求转给别人。


Tomcat 是 Servlet 容器,不是完整的 Java EE 应用服务器(那是 WildFly、WebLogic、WebSphere 干的事,它们支持 EJB、JMS、JTA 等完整规范)。Tomcat 只实现 Servlet 和 JSP 规范,核心能力是:把 HTTP 请求交给你的 Java 代码去处理,再把结果变成 HTTP 响应发回去。


所以「Java 后台程序能不能用 Apache 和 Nginx」——能,而且生产环境里经常是「Nginx/Apache 在前,Tomcat 在后」:前面负责扛流量、静态资源、HTTPS 终结、负载均衡,后面专门跑 Java。


Nginx 和 Apache:都是 HTTP 服务器,架构不一样


两者都能做静态文件服务、反向代理、负载均衡,但内部设计完全不同。


Apache 有三种工作模式(MPM):prefork 是一个连接一个进程,worker 是多进程+多线程,event 是在 worker 基础上优化了 keep-alive 连接的处理。现代 Apache(2.4+)默认用 event MPM,处理 keep-alive 的方式已经接近事件驱动了,不完全是老式的「一个连接占一个线程」。但不管哪种模式,Apache 的并发上限都受限于进程/线程数——每个线程有自己的栈空间(Linux 默认 8MB),1000 个线程光栈就要 8GB 内存,还没算堆上的数据。所以 Apache 的并发连接数一般在几百到几千这个量级。


Nginx 是另一种思路:少量 worker 进程 + 事件循环。每个 worker 用 epoll(Linux)/ kqueue(macOS)做 I/O 多路复用,一个 worker 可以同时挂着几万条连接。大部分连接在等 I/O,不需要单独的线程,也就不需要那 8MB 的栈空间。所以同样一台机器,Nginx 能撑的并发连接数比 Apache 高一个数量级。


这也是很多人说「Nginx 比 Apache 性能好」的原因——不是 Nginx 处理单个请求更快,而是它用更少的资源就能维持大量连接。 如果你的场景是几十个并发、主要跑 PHP,Apache + mod_php 用着挺好,没必要换。但如果要扛几万并发、做反向代理或者负载均衡,Nginx 的模型更合适。


Tomcat:能直接对外,但不擅长


这里要纠正一个常见的说法:「Tomcat 必须放在 Nginx 后面」。


Tomcat 自带 HTTP 连接器(Coyote),可以直接监听 80 或 443 端口对外服务。开发的时候大家天天直接访问 localhost:8080,没什么问题。Spring Boot 更进一步——内嵌 Tomcat 打成一个 jar 包,java -jar 直接跑,连单独部署 Tomcat 都省了。


很多微服务架构里,每个服务就是一个内嵌 Tomcat 的 Spring Boot 应用,前面挂一个 API 网关(Spring Cloud Gateway、Kong 之类的)做路由和鉴权,根本没有单独部署 Nginx 的环节。


但 Tomcat 直接对外有几个短板:


静态文件性能。 Nginx 处理静态文件用的是 sendfile 系统调用(之前零拷贝那篇讲过),数据不经过用户空间,直接从磁盘到网卡。Tomcat 处理静态文件要经过 Java 的 IO 层,多了一次拷贝和 JVM 的开销。量小的时候感知不到,量大了差距就出来了。


SSL 终结。 Nginx 的 SSL 实现基于 OpenSSL,经过大量优化,支持 session 复用、OCSP stapling 这些。Tomcat 也能做 SSL,但性能和配置灵活性都不如 Nginx。把 SSL 卸载到 Nginx,Tomcat 和 Nginx 之间走 HTTP 明文,Tomcat 的负担更轻。


限流、缓存、负载均衡。 这些 Nginx 用几行配置就能搞定,Tomcat 要么不支持,要么需要写 Java 代码或者引入额外组件。


所以典型的生产部署是这样的:


upstream tomcat_backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081; # 多实例负载均衡
}

server {
listen 443 ssl;
ssl_certificate /etc/nginx/cert.pem;
ssl_certificate_key /etc/nginx/key.pem;

location /static/ {
alias /var/www/static/;
}

location / {
proxy_pass http://tomcat_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

Nginx 扛 SSL、吐静态文件、做负载均衡,动态请求转给后面的 Tomcat。但这不是唯一的架构——小项目、内部系统、微服务里 Tomcat 直接对外也很常见,取决于你的流量规模和运维需求。


为啥早年都是 Apache + Tomcat


早期 Nginx 还没普及的时候,Apache 是 Linux 上默认的 HTTP 服务器。Java 项目的标准搭配是 Apache + mod_jk(或 mod_proxy)+ Tomcat:Apache 在前面接请求,通过 AJP 协议或 HTTP 代理转给 Tomcat。


mod_jk 用的是 AJP 协议(Apache JServ Protocol),比 HTTP 更紧凑,省了 HTTP 头的解析开销。但 AJP 协议在 2020 年爆出过 Ghostcat 漏洞(CVE-2020-1938),之后很多团队开始关闭 AJP 端口,改用 HTTP 代理。


现在新项目基本都用 Nginx 替代 Apache 当入口了。Apache 在需要 .htaccess(目录级配置覆盖)或者跑 mod_php 的场景还有优势,但纯做反向代理和负载均衡,Nginx 的资源占用和并发能力都更好。


怎么判断你的项目该用哪种组合


Spring Boot 微服务、内部系统、流量不大: 内嵌 Tomcat 直接对外,前面挂个网关或者云厂商的负载均衡器就行,不需要单独部署 Nginx。


对外的 Web 应用、有静态资源、需要 HTTPS: Nginx 在前做 SSL 终结和静态资源,动态请求 proxy_pass 到 Tomcat。


PHP + Java 混合部署(老项目): Apache 跑 mod_php 处理 PHP,同时 mod_proxy 把 Java 请求转给 Tomcat。不过这种架构越来越少了。


纯静态站点、CDN 回源、API 网关: 只需要 Nginx,不需要 Tomcat。


Nginx 和 Tomcat 不是竞争关系,Apache 和 Nginx 才是。而即便是 Apache 和 Nginx,在大部分场景下也不是「谁好谁差」的问题——Nginx 在高并发反向代理上更强,Apache 在需要 .htaccess 和动态模块加载的场景更方便。


作者:嘻哈baby
来源:juejin.cn/post/7609933479262715950
收起阅读 »

PWA 到底是什么?它在 2026 年解决了哪些真实痛点?

web
PWA 到底是什么? Progressive Web App(渐进式 Web 应用,简称 PWA)是一种使用标准 Web 技术(HTML、CSS、JavaScript)构建的网页应用,但通过浏览器提供的增强能力,让它具备接近原生 App 的体验。 它不是一个全...
继续阅读 »

PWA 到底是什么?


Progressive Web App(渐进式 Web 应用,简称 PWA)是一种使用标准 Web 技术(HTML、CSS、JavaScript)构建的网页应用,但通过浏览器提供的增强能力,让它具备接近原生 App 的体验。


它不是一个全新的东西,而是一种“渐进增强”(Progressive Enhancement)的理念:从普通的网页开始,逐步添加高级特性,让用户感觉像在使用安装的原生应用。


PWA 的三大核心支柱(至今仍是)



  • 可靠(Reliable):即使在弱网/断网情况下也能加载并基本可用(靠 Service Worker + 缓存)。

  • 快速(Fast):瞬间加载、流畅交互(优化的缓存 + 性能最佳实践)。

  • 可安装(Installable):可以“添加到主屏幕”,以独立窗口(standalone)模式运行,有图标、启动画面,像 App 一样。


在 2026 年,PWA 已经从 2015 年的“概念”变成了许多企业实际落地的主流移动解决方案之一。浏览器支持大幅成熟,Chrome/Edge/Firefox 几乎完整,Safari(iOS)也追赶了很多年(虽仍有差距)。


它在 2026 年真正解决了哪些真实痛点?


以下是 2026 年开发者/产品/业务最常遇到的痛点,以及 PWA 如何针对性解决(基于当前浏览器现实支持情况):



  1. 开发和维护成本爆炸(Separate iOS + Android + Web)



    • 痛点:同一功能要写 3 套代码(Swift/Kotlin + Web),测试、上架、更新各走各的流程,维护成本高到离谱。

    • PWA 解决:一套代码跑三端(甚至桌面 Windows/macOS/ChromeOS)。2026 年 60%+ 的企业级移动项目已转向 PWA 或 hybrid 模式,开发成本可降 40–60%。更新无需 App Store 审核,秒级生效。



  2. 用户安装/获取摩擦巨大(App Store 下载壁垒)



    • 痛点:用户看到链接 → 去 App Store → 下载几十 MB → 安装 → 打开,转化率惨不忍睹(很多场景 <5%)。

    • PWA 解决:链接一点就用,符合条件可弹出“添加到主屏幕”提示(Android 自动 banner,iOS 手动但更顺畅)。安装后有图标、离线可用、无需占 App Store 空间。很多电商/内容/工具类 App 转化率因此提升 2–5 倍。



  3. 弱网/无网场景下体验崩坏



    • 痛点:地铁、电梯、农村、国际漫游……用户一断网就白屏/卡死,流失严重。

    • PWA 解决:Service Worker 预缓存 + 运行时缓存,核心页面/资源离线可用。2026 年 Workbox 等工具让实现几乎零成本。新闻、邮件、待办、天气、记账类 PWA 在断网时仍能浏览历史、写草稿,等联网再同步。



  4. 推送通知和用户再触达难



    • 痛点:H5 基本没推送,原生 App 推送又贵又麻烦(审核、权限)。

    • PWA 解决:Web Push 已跨平台可用。Android/桌面完整支持,iOS 从 iOS 16.4 开始支持(需加到主屏幕,非 EU 地区更稳定)。2026 年 Declarative Web Push 等新 API 让推送更可靠,企业再营销/订单提醒/消息触达率大幅提升。



  5. 加载慢、性能差直接影响收入



    • 痛点:移动端 3 秒未加载完,用户流失率飙升;Core Web Vitals 差 → SEO 排名掉。

    • PWA 解决:强制 HTTPS + 缓存策略 + 优化后,首屏加载常 <1s。Lighthouse PWA 分数 90+ 已成为标配,很多业务报告转化率提升 20–50%。



  6. 跨平台一致性 & 快速迭代



    • 痛点:iOS 和 Android 体验割裂,bug 修复要双平台发版。

    • PWA 解决:浏览器统一渲染逻辑,一处修复全局生效。2026 年 PWA 还能用 File System Access、Web Share、Badging API 等,让体验更接近原生。




2026 年 PWA 的真实平台支持对比(简表)


特性Android (Chrome)iOS (Safari 26+)Windows/macOS备注
添加到主屏幕/安装完整(自动提示)支持(手动 Share → Add)支持iOS 26 默认更倾向 web app 模式
离线 & 缓存完整完整(但存储配额仍限)完整Service Worker 跨平台
Push 通知完整支持(需 home screen,非EU更稳)完整iOS 无 silent push,reach 稍低
Background Sync完整部分/不支持部分iOS 仍最大短板
Periodic Sync完整不支持部分用于定期更新内容
硬件 API(相机、蓝牙等)大部分支持部分支持部分差距在缩小

总结一句话(2026 年视角)


PWA 不是要完全取代原生 App,而是解决了**“我想给用户 App 般的体验,但不想付出双平台原生开发的代价”** 这个最真实、最普遍的痛点。


特别适合:



  • 电商、新闻、社交工具、SaaS、生产力工具、内容平台

  • 预算有限、需要快速验证、重视 SEO 和链接分享的场景

  • 想覆盖桌面 + 移动 + 弱网用户的企业


不适合:



  • 重度游戏、AR/VR、深度硬件调用(如银行指纹/人脸支付完整链路)

  • 对 iOS 推送/后台要求极高的场景(仍需原生补位)


作者:前端小小栈
来源:juejin.cn/post/7608782906940620840
收起阅读 »

彻底重绘Spring Boot性能版图,资源占用缩减80%

很多开发者还在用十年前的习惯写现在的 Spring Boot 应用。这种技术代差不仅让代码显得臃肿,更是在浪费服务器的真金白银。本文整理了一些进阶技巧,帮助优化 Spring Boot 应用的运行效率与代码质量。 准备工作:快速搭建 Java 环境 编写代码...
继续阅读 »

很多开发者还在用十年前的习惯写现在的 Spring Boot 应用。这种技术代差不仅让代码显得臃肿,更是在浪费服务器的真金白银。本文整理了一些进阶技巧,帮助优化 Spring Boot 应用的运行效率与代码质量。



准备工作:快速搭建 Java 环境


编写代码前需要安装 JDK。但对新手来说,手动配置环境变量和切换版本其实挺浪费时间的。


而 ServBay 提供了集成化的解决方案,支持一键部署 Java 环境,这样开发者可以快速切换不同的 JDK 版本,无需手动调整系统配置,让开发环境的搭建变得高效且规范。



精细化配置 JVM 内存降低云端成本


很多 Spring Boot 应用在云服务器上裸奔。默认的 JVM 配置往往会申请过多的内存,导致账单金额飙升。


如果我们运行的是微服务,其实根本不需要动辄 4GB 的堆空间。我通常会把初始内存和最大内存压到一个合理的范围。


# 限制内存并指定垃圾回收线程数
export JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseContainerSupport -XX:ParallelGCThreads=2 -XX:MaxMetaspaceSize=256m"
java $JAVA_OPTS -jar app-service.jar

加上 -XX:+UseContainerSupport 后,JVM 能准确识别容器的边界。同时,我会手动限制 Tomcat 的线程池大小,因为默认的 200 个线程对大多数中小型业务来说完全是浪费。


# application.yaml 里的精简配置
server:
tomcat:
threads:
max: 60

通过这些设置,单个容器的内存占用通常能降低 20% 以上,从而减少云服务器的节点数量。


采用现代 Java 语法精简业务逻辑


如果代码库里到处是冗长的 Getter 和 Setter,或者还在用复杂的 if-else 处理枚举,那就赶紧升级新版 Java。


使用 Record 定义数据模型


Record 适合用于 DTO 或 API 返回对象。我现在的 API 数据传输对象全部改用 Record。


// 这一行代码就搞定了构造、Getter 和 toString
public record UserResponse(Long id, String nickname, String email) {}

文本块与 Switch 表达式


文本块解决了多行字符串拼接时的转义符困扰,而 Switch 表达式则提供了更安全的返回值方式。


// 使用文本块编写 SQL
String sql = """
SELECT * FROM product_info
WHERE category = 'ELECTRONICS'
AND stock > 0
"""
;

// Switch 表达式直接返回结果
String categoryName = switch (typeCode) {
case 1 -> "电子产品";
case 2 -> "家居生活";
default -> "其他类型";
};

Switch 的模式匹配


Java 17 引入的模式匹配让类型判断更加直观,减少了显式的强制类型转换。所以,处理复杂的业务分支时,我会用增强型的 Switch 配合模式匹配。


// 处理不同类型的事件消息
static String handleEvent(Object event) {
return switch (event) {
case OrderEvent o -> "订单编号:" + o.id();
case UserEvent u -> "用户名称:" + u.name();
case null -> "空消息";
default -> "未知事件";
};
}

善用 Stream API 的增强功能


Stream API 在数据处理方面持续进化。新版增加了更丰富的收集器与过滤逻辑,使得内存中的数据聚合与转换更加高效。合理使用并行流处理大规模数据集,可以充分利用多核 CPU 的性能,缩短复杂逻辑的执行时间。


启用现代垃圾回收器减少停顿


在高并发场景下,我更倾向于使用 ZGC 这种低延迟垃圾回收器。它能把停顿时间压减到 1 毫秒以内,用户几乎感知不到卡顿。


如果对启动速度有极端要求,比如在函数计算场景下,我会利用 GraalVM 将 Spring Boot 编译为原生镜像(Native Image)。


# 构建原生执行文件
./mvnw native:compile -Pnative

这样编译出来的程序启动时间从几秒缩短到几十毫秒,内存占用甚至能砍掉 80%。虽然编译过程变久了,但运行时换来的性能增益它值得。


耗时任务必须异步化


在 Controller 里同步生成 PDF 或者发送复杂的邮件的都叉出去,这会直接堵死 Web 线程。


现在的做法是把这些重活直接扔进消息队列,让主流程瞬间返回。


// 投递到 RabbitMQ 或 Kafka@PostMapping("/submit-report")
public ResponseEntity<String> handleReport(@RequestBody ReportConfig config) {
taskQueue.send("report_gen_topic", config);
return ResponseEntity.accepted().body("报告生成任务已启动,请稍后查看");
}

把计算压力转移到后端的 Worker 节点上,这样主 API 就能保持极高的响应速度,即便在高并发流量下也不会崩溃。


总结


优化 Spring Boot 应用是一个系统性的工程。如果你还在忍受冗长的编译等待、高昂的云端开支和莫名其妙的停顿,现在就应该改变做法了。快来试试这些技巧吧。


作者:ServBay
来源:juejin.cn/post/7609660097783300123
收起阅读 »

你的程序应该启动多少线程?

"线程数等于 CPU 核数"——这可能是程序员最耳熟能详的性能优化建议之一。但当你真正着手设计一个系统时,你会发现事情远没有这么简单:Web 服务器动辄上千线程,游戏引擎可能只用寥寥几个,而一些高性能中间件甚至会创建 CPU 核数两倍的线程。到底谁是对的?这篇...
继续阅读 »

"线程数等于 CPU 核数"——这可能是程序员最耳熟能详的性能优化建议之一。

但当你真正着手设计一个系统时,你会发现事情远没有这么简单:Web 服务器动辄上千线程,游戏引擎可能只用寥寥几个,而一些高性能中间件甚至会创建 CPU 核数两倍的线程。到底谁是对的?

这篇文章试图回答一个看似简单的问题:你的程序应该启动多少线程?


一、那条广为流传的经验法则

几乎每本并发编程的书都会告诉你:

对于 CPU 密集型任务,线程数应等于 CPU 核数(或核数 + 1)。

这条规则背后的逻辑很直观:每个 CPU 核心同一时刻只能执行一个线程。如果线程数超过核心数,多余的线程只能等待,还会带来额外的上下文切换开销。如果线程数少于核心数,又会让部分核心空转。

这条规则没有错,但它只回答了一个非常狭窄的问题:当你的唯一目标是最大化 CPU 利用率时,应该用多少线程?

现实中的软件系统要复杂得多。


二、线程的真实作用:不只是并行

当我们谈论"为什么需要线程"时,教科书往往只强调一点:并行计算。但在实际工程中,线程至少承担着三种截然不同的职责:

1. 通过异步避免阻塞

想象一个 GUI 程序:用户点击按钮后,程序需要从网络加载数据。如果在主线程中同步等待网络响应,整个界面就会冻结。

// 糟糕的做法:阻塞主线程
void onClick() {
   Data data = network.fetchSync();  // 界面卡住 3 秒
   updateUI(data);
}

// 更好的做法:用单独线程处理阻塞操作
void onClick() {
   new Thread(() -> {
       Data data = network.fetchSync();
       runOnUIThread(() -> updateUI(data));
  }).start();
}

这里的线程不是为了并行计算,而是为了不阻塞主线程。即使在单核 CPU 上,这种设计也是有意义的。

2. 故障隔离:舱壁模式

在微服务架构中,一个服务可能依赖多个下游系统。如果所有请求共享同一个线程池,当某个下游系统变慢时,线程会被逐渐耗尽,最终导致整个服务不可用——这就是级联故障

┌─────────────────────────────────────────────────┐
│                   共享线程池                     │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐       │
│ │ T1 │ │ T2 │ │ T3 │ │ T4 │ │ T5 │       │
│ │阻塞 │ │阻塞 │ │阻塞 │ │阻塞 │ │阻塞 │       │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘       │
│     │       │       │       │       │         │
│     └───────┴───────┼───────┴───────┘         │
│                     ▼                           │
│         下游服务 A(响应变慢)                   │
│                                                 │
│ 结果:所有线程被 A 占满,B 和 C 的请求无法处理   │
└─────────────────────────────────────────────────┘

舱壁模式(Bulkhead Pattern)借鉴了船舶设计的思想:将船体分隔成多个水密舱,一个舱室进水不会导致整艘船沉没。

┌──────────────────────────────────────────────────┐
│                                                 │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐     │
│   │ 线程池 A │ │ 线程池 B │ │ 线程池 C │     │
│   │ (3线程) │ │ (3线程) │ │ (3线程) │     │
│   └────┬─────┘ └────┬─────┘ └────┬─────┘     │
│       │             │             │           │
│       ▼             ▼             ▼           │
│   下游服务A     下游服务B     下游服务C       │
│   (变慢)       (正常)       (正常)           │
│                                                 │
│   结果:A 的线程池耗尽,但 BC 不受影响       │
└──────────────────────────────────────────────────┘

这种设计会让总线程数远超 CPU 核数,但换来的是系统的韧性

3. 简化抽象:让代码更易理解

有时候,多线程的目的纯粹是为了代码组织

考虑一个游戏服务器需要同时处理:

  • 网络消息收发
  • 游戏逻辑 tick
  • 定时任务调度
  • 日志异步写入
  • 监控指标上报

你当然可以用一个复杂的事件循环把它们全部塞进单线程:

while True:
   if has_network_event():
       handle_network()
   if time_for_game_tick():
       game_tick()
   if has_scheduled_task():
       run_task()
   if log_buffer_not_empty():
       flush_logs()
   # ... 代码很快变成一团乱麻

但更清晰的做法是为每个职责分配独立的线程:

Thread("network",   network_loop)
Thread("game-tick", game_loop)
Thread("scheduler", scheduler_loop)
Thread("logger",    log_writer_loop)

这些线程大部分时间可能都在 sleep 或等待 I/O,根本不争抢 CPU。但它们让代码结构变得清晰:每个线程有明确的职责和生命周期


三、线程的真实开销

既然线程有这么多用途,是不是可以随意创建?在回答这个问题之前,我们需要理解线程在现代操作系统中的真实开销。

1. 创建开销

创建一个线程需要:

  • 分配内核数据结构(Linux 上是 task_struct,约 2-3 KB)
  • 分配用户态栈空间
  • 在调度器中注册
  • 各种安全检查和初始化

在 Linux 上,创建一个线程大约需要 10-30 微秒。这个开销对于长生命周期的线程可以忽略,但如果频繁创建销毁(如"每个请求一个线程"的模型),累积起来就相当可观。

// 简单测试:创建 10000 个线程
for (int i = 0; i < 10000; i++) {
   pthread_create(&threads[i], NULL, empty_func, NULL);
}
// 在普通机器上可能需要 100-300ms

2. 内存占用

每个线程需要独立的栈空间。默认配置下:

  • Linux:8 MB(虚拟内存),实际物理内存按需分配
  • Windows:1 MB(commit)
  • macOS:512 KB(主线程 8 MB)

1000 个线程,按 Linux 默认配置,光栈空间就需要 8 GB 的虚拟地址空间。虽然物理内存是惰性分配的,但这个数字仍然令人警醒。

你可以通过调整栈大小来优化:

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 256 * 1024);  // 256 KB
pthread_create(&thread, &attr, func, NULL);

或者在 Java 中:

Thread thread = new Thread(null, runnable, "name", 256 * 1024);
// 或者 JVM 参数 -Xss256k

3. 上下文切换

这是最常被提及的开销。当 CPU 从执行线程 A 切换到线程 B 时,需要:

  1. 保存现场:寄存器状态、程序计数器、栈指针等
  2. 切换页表(如果是不同进程的线程)
  3. 恢复现场:加载线程 B 的状态
  4. 缓存失效:这往往是最大的隐藏开销

纯粹的上下文切换本身只需要 1-5 微秒。但切换后,新线程访问的数据很可能不在 CPU 缓存中,需要从内存重新加载。这种缓存污染导致的性能损失可能比切换本身高出一个数量级。

┌─────────────────────────────────────────────────────┐
│                 上下文切换的真实开销                 │
├─────────────────────────────────────────────────────┤
│ 直接开销(保存/恢复状态)         ~1-5 μs         │
│ 间接开销(缓存失效)             ~10-100 μs       │
│ 总体影响                         视工作负载而定   │
└─────────────────────────────────────────────────────┘

4. 调度开销

操作系统需要维护运行队列、就绪队列,执行调度算法来决定下一个运行的线程。线程数越多,调度器的负担越重。

在极端情况下(数万线程),光是遍历调度队列都会成为性能瓶颈。

量化视角

让我们把这些数字放在一起:

开销类型量级影响
创建线程10-30 μs频繁创建时累积
栈内存256KB-8MB/线程限制最大线程数
上下文切换1-5 μs高频切换时累积
缓存失效10-100 μs最大的隐藏开销

对于大多数应用来说,几十到几百个线程是完全可以接受的。现代 Linux 内核可以轻松处理上万个线程,只要它们不都在同时争抢 CPU。


四、决策框架:按用途确定线程数

理解了线程的多重作用和真实开销后,我们可以建立一个决策框架:

1. CPU 密集型计算:等于核数

如果线程的主要工作是计算(数学运算、数据处理、加密解密等),坚守经典法则:

int threadCount = Runtime.getRuntime().availableProcessors();
// 或者 核数 + 1,留一个处理偶发的 I/O

这里的逻辑很简单:更多的线程只会增加切换开销,不会提升吞吐量。

实际案例

  • 图像处理库
  • 科学计算框架
  • 视频编解码器

2. I/O 密集型:优先考虑非阻塞

如果线程大部分时间在等待 I/O(网络、磁盘、数据库),你有两个选择:

选项 A:非阻塞 I/O + 事件循环(推荐)

# Python asyncio
async def fetch_all(urls):
   async with aiohttp.ClientSession() as session:
       tasks = [fetch(session, url) for url in urls]
       return await asyncio.gather(*tasks)
       
# 用少量线程处理大量并发连接

Node.js、Nginx、Redis 都采用这种模型,用极少的线程处理海量并发。

选项 B:线程池 + 阻塞 I/O

如果你必须使用阻塞 I/O(比如 JDBC 不支持异步),可以用经典公式:

线程数 = CPU 核数 × (1 + I/O 等待时间 / CPU 计算时间)

如果平均每个请求花 100ms 等待 I/O、1ms 做计算,在 8 核机器上:

线程数 = 8 × (1 + 100/1) = 808

这只是理论上限。实际中还要考虑:

  • 下游系统能否承受这么多并发
  • 内存是否足够
  • 连接池大小限制

3. 故障隔离:按风险域划分

为每个可能独立失败的依赖分配独立的线程池:

// Hystrix/Resilience4j 风格
ThreadPoolBulkhead paymentPool = ThreadPoolBulkhead.of("payment",
   ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(10)
      .coreThreadPoolSize(5)
      .queueCapacity(20)
      .build());

ThreadPoolBulkhead inventoryPool = ThreadPoolBulkhead.of("inventory",
   ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(8)
      .coreThreadPoolSize(4)
      .queueCapacity(15)
      .build());

ThreadPoolBulkhead shippingPool = ThreadPoolBulkhead.of("shipping",
   ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(6)
      .coreThreadPoolSize(3)
      .queueCapacity(10)
      .build());

每个池的大小取决于:

  • 下游服务的正常响应时间:响应越慢,需要越多线程来维持吞吐量
  • 可接受的最大并发数:下游服务能承受多少并发请求
  • 降级策略:线程池满时是快速失败、排队等待,还是返回降级结果

计算舱壁大小的方法

假设某个下游服务:

  • 正常响应时间:50ms
  • 期望吞吐量:每秒 100 个请求
  • 可接受的排队延迟:100ms
最小线程数 = 吞吐量 × 响应时间
        = 100 × 0.05
        = 5 个线程

队列容量 = 吞吐量 × 可接受排队延迟
      = 100 × 0.1
      = 10 个请求

但还要考虑异常情况。如果下游服务变慢(响应时间从 50ms 变成 500ms):

此时需要的线程数 = 100 × 0.5 = 50 个线程

这就是舱壁要保护的场景。我们不应该给它 50 个线程,而是:

ThreadPoolBulkhead.of("payment",
   ThreadPoolBulkheadConfig.custom()
      .maxThreadPoolSize(10)      // 硬上限:最多 10 个线程
      .coreThreadPoolSize(5)      // 正常情况够用
      .queueCapacity(20)          // 允许短暂排队
      .build());

// 当下游变慢时:
// - 10 个线程被占满
// - 20 个请求在队列等待
// - 第 31 个请求立即被拒绝(快速失败)
// - 系统其他部分不受影响

舱壁 vs 断路器

舱壁模式常与断路器(Circuit Breaker)配合使用:

┌─────────────────────────────────────────────────────────┐
│                       请求流程                         │
│                                                         │
│   请求 ──→ 断路器 ──→ 舱壁(线程池) ──→ 下游服务       │
│             │             │                           │
│             │             │                           │
│         检查是否熔断   检查是否有空闲线程               │
│             │             │                           │
│             ▼             ▼                           │
│         熔断则快速失败 无空闲则拒绝或排队               │
│                                                         │
└─────────────────────────────────────────────────────────┘
// Resilience4j 组合使用示例
CircuitBreaker circuitBreaker = CircuitBreaker.of("payment",
   CircuitBreakerConfig.custom()
      .failureRateThreshold(50)           // 失败率超过 50% 则熔断
      .waitDurationInOpenState(Duration.ofSeconds(30))
      .build());

ThreadPoolBulkhead bulkhead = ThreadPoolBulkhead.of("payment", ...);

Supplier decoratedSupplier = Decorators
  .ofSupplier(() -> paymentService.call())
  .withCircuitBreaker(circuitBreaker)     // 先检查断路器
  .withThreadPoolBulkhead(bulkhead)       // 再进入线程池
  .withFallback(ex -> fallbackResponse()) // 降级响应
  .decorate();

舱壁的代价

舱壁模式会显著增加系统的总线程数:

传统模式:
1 个共享线程池 × 50 线程 = 50 线程

舱壁模式:
支付服务池   10 线程
库存服务池     8 线程
物流服务池     6 线程
用户服务池     8 线程
通知服务池     4 线程
─────────────────────
总计         36 线程(但每个池都有冗余)
 
实际配置时考虑峰值:
每个池 ×1.5 = 54 线程

这看起来线程更多了,但关键区别在于:

对比项共享线程池舱壁模式
总线程数较少较多
单点故障影响整个系统仅一个服务
资源利用率更高有冗余浪费
容量规划简单需要分别规划
故障恢复慢(需等待所有线程释放)快(其他池不受影响)

在微服务架构中,隔离性通常比资源利用率更重要。舱壁带来的额外线程开销,换来的是系统在部分故障时仍能提供服务的能力。

4. 简化抽象:按职责最小化

当线程用于代码组织时,遵循够用就好原则:

// 典型的服务端应用线程分配
Thread acceptor    = new Thread(this::acceptLoop);     // 1个:接受连接
Thread[] workers   = new Thread[cpuCores];             // N个:处理业务
Thread timer       = new Thread(this::timerLoop);      // 1个:定时任务
Thread logger      = new Thread(this::logWriter);      // 1个:异步日志
Thread monitor     = new Thread(this::metricsReport);  // 1个:监控上报

// 总计:cpuCores + 4 个线程

这些辅助线程大部分时间在休眠,不会与 worker 线程竞争 CPU。关键是确保它们:

  • 不会执行耗时的计算
  • 不会频繁唤醒
  • 有明确的单一职责

五、警惕"线程风暴"

即使每个决策单独看都合理,累积起来也可能造成问题。

叠加效应

假设你的 Java 服务:

  • Tomcat 线程池:200 个
  • 数据库连接池:每个连接有后台线程,50 个
  • Redis 客户端池:20 个
  • Kafka 消费者:10 个分区 × 3 个消费者组 = 30 个
  • 定时任务调度器:核心线程 10 个
  • JVM GC 线程:8 个
  • 其他框架的后台线程:若干

加起来可能有 300-500 个线程,而你的机器可能只有 8 个 CPU 核心。

抖动风险

在某些时刻,大量线程可能同时被唤醒:

┌─────────────────────────────────────────────────────┐
            t0: 某个事件触发                      
                                               
    ┌────────────────┼───────────────┐            
                                           
┌─────┐         ┌─────┐         ┌─────┐          
│100个│         │50个         │30个          
│HTTP         │定时         │Kafka│          
│请求         │任务         │消息          
└──┬──┘         └──┬──┘         └──┬──┘          
                                             
    └───────────────┼───────────────┘              
                                                 
          8 CPU 核心开始疯狂切换                
                                                 
                                                 
      延迟飙升,GC 停顿,服务抖动                  
└─────────────────────────────────────────────────────┘

这种"线程风暴"会导致:

  • 所有请求的延迟同时上升
  • P99 延迟剧烈波动
  • 可能触发超时和级联故障

缓解策略

策略一:错峰调度

让定时任务随机分散,而不是整点触发:

// 不好:所有实例同时执行
@Scheduled(cron = "0 0 * * * *")  // 每小时整点

// 更好:启动时计算随机偏移
int jitter = random.nextInt(60);
@Scheduled(cron = "0 " + jitter + " * * * *")

策略二:为关键线程提升优先级

确保 CPU 密集型的核心工作线程能优先获得调度:

// 关键业务线程
thread.setPriority(Thread.MAX_PRIORITY);  // Java: 1-10,默认 5

// 或者在 Linux 上使用 nice 值
// nice -n -5 java -jar app.jar
// C/C++:使用实时调度策略
struct sched_param param;
param.sched_priority = 50;  // 实时优先级
pthread_setschedparam(thread, SCHED_FIFO, ¶m);

策略三:CPU 绑定(CPU Affinity)

将关键线程绑定到特定 CPU,避免缓存失效:

// 使用 JNA 或 JNI 调用系统 API
// Linux: sched_setaffinity()
// 或使用 Disruptor 等框架内置的支持
// C: 将线程绑定到 CPU 0 和 1
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);
pthread_setaffinity_np(thread, sizeof(cpuset), &cpuset);

策略四:限制并发

使用信号量或令牌桶限制同时运行的线程数:

Semaphore semaphore = new Semaphore(cpuCores * 2);

void process(Request request) {
   semaphore.acquire();
   try {
       doWork(request);
  } finally {
       semaphore.release();
  }
}

六、协程时代:换汤不换药?

Go 语言的 goroutine、Kotlin 的协程、Java 的虚拟线程(Project Loom)……协程似乎成了并发的银弹。

"创建一百万个协程"的 demo 随处可见,这是否意味着我们不用再关心"数量"问题了?

协程的本质

协程(或用户态线程、绿色线程)本质是把调度权从操作系统收回到用户态

┌───────────────────────────────────────────────────┐
│                   传统线程                       │
│                                                   │
│   线程1 线程2 线程3 线程4 ... 线程1000       │
│     │     │     │     │           │           │
│     └──────┴──────┴──────┴───────────┘           │
│                   │                             │
│           操作系统调度器                         │
│                   │                             │
│     ┌──────┬──────┬──────┬──────────┐           │
│     ▼     ▼     ▼     ▼         ▼           │
│   CPU0   CPU1   CPU2   CPU3 ... CPU7           │
└───────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────┐
│                   协程模型                       │
│                                                   │
│   协程1 协程2 协程3 ... 协程1000000           │
│     │     │     │           │                 │
│     └──────┴──────┴────────────┘                 │
│                   │                             │
│           语言运行时调度器                         │
│                   │                             │
│     ┌──────────────┼──────────────┐             │
│     ▼             ▼             ▼             │
│   线程1         线程2   ...   线程N           │
│     │             │             │             │
│     └──────────────┼──────────────┘             │
│                   │                             │
│           操作系统调度器                         │
│                   │                             │
│           ┌───────┴───────┐                     │
│           ▼               ▼                     │
│         CPU0   ...   CPU7                     │
└───────────────────────────────────────────────────┘

协程的优势在于:

  • 创建开销极小:Go 的 goroutine 初始栈只有 2KB
  • 切换开销极小:用户态切换,不需要进入内核
  • 调度更智能:运行时了解协程在做什么(如等待 channel)

但物理规则依然适用

协程改变的是切换效率,不是 CPU 核心数

// 100 万个协程同时做 CPU 密集计算?
for i := 0; i < 1000000; i++ {
   go func() {
       for {
           // 纯计算,没有 I/O
           heavyComputation()
      }
  }()
}
// 结果:并不会比 GOMAXPROCS 个协程更快

核心洞察:如果协程在执行时不主动让出(通过 I/O、channel、sleep 等),它就和操作系统线程没有本质区别

协程的正确心智模型

操作类型协程行为考量
I/O 等待挂起,让出执行权可以有百万并发
Channel 等待挂起,让出执行权可以有百万并发
CPU 计算持续占用线程同时计算数 ≈ GOMAXPROCS
调用 C 代码可能阻塞线程可能需要更多线程

Go 运行时会自动调整实际的操作系统线程数,但 GOMAXPROCS(默认等于 CPU 核数)限制了同时执行的线程数。

// 正确用法:百万协程处理 I/O
for i := 0; i < 1000000; i++ {
   go handleConnection(conn[i])  // 每个协程大部分时间在等待网络
}

// 需要注意:CPU 密集型任务
pool := make(chan struct{}, runtime.NumCPU())  // 信号量
for task := range tasks {
   pool <- struct{}{}  // 获取令牌
   go func(t Task) {
       defer func() { <-pool }()  // 释放令牌
       cpuIntensiveWork(t)        // 同时只有 NumCPU 个在计算
  }(task)
}

Java 虚拟线程的启示

Java 21 引入的虚拟线程(Virtual Threads)同样遵循这个逻辑:

// 可以创建大量虚拟线程处理阻塞 I/O
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
   for (int i = 0; i < 100000; i++) {
       executor.submit(() -> {
           // 阻塞 I/O 会自动让出载体线程
           String result = httpClient.send(request);
           process(result);
      });
  }
}

// 但如果是 CPU 密集型...
executor.submit(() -> {
   // 这个虚拟线程会持续占用载体线程
   while (true) {
       computePi();  // 其他虚拟线程饿死
  }
});

七、实践清单

让我们把讨论转化为可操作的检查清单:

启动前问自己

□ 这个线程/协程的主要工作是什么?
- [ ] CPU 计算
- [ ] I/O 等待
- [ ] 故障隔离
- [ ] 代码组织

□ 它会阻塞吗?阻塞多久?

□ 它需要和其他线程竞争资源吗?

□ 它的生命周期是什么?
- [ ] 与应用相同(后台线程)
- [ ] 与请求相同(per-request)
- [ ] 执行完任务就结束(fire-and-forget)

配置建议速查

场景线程数建议关键考量
纯 CPU 计算= 核数更多无益
CPU 计算 + 偶发 I/O= 核数 + 1~2应对偶发阻塞
I/O 密集(非阻塞)核数或更少事件循环处理并发
I/O 密集(阻塞)取决于 I/O 时间比例用公式估算,压测验证
舱壁隔离每依赖一个独立池池大小取决于下游容量
辅助功能每职责 1 个确保不争抢 CPU

监控指标

上线后,持续关注:

线程状态分布
├── RUNNABLE(运行中)   → 应该 ≈ CPU 核数
├── BLOCKED(锁等待)   → 过高说明有锁竞争
├── WAITING(条件等待) → I/O 线程正常状态
└── TIMED_WAITING(超时等待)→ sleep 或 poll

上下文切换率
└── vmstat, pidstat -w → 每秒切换数

线程创建率
└── 高频创建说明需要用池

CPU 使用率
└── 高于预期 → 检查是否在自旋
└── 低于预期 → 检查是否在等锁

八、总结

回到最初的问题:你的程序应该启动多少线程?

答案是:取决于这些线程要做什么

  • 如果是为了并行计算,线程数应该接近 CPU 核心数
  • 如果是为了处理阻塞 I/O,优先考虑非阻塞方案;如果必须阻塞,根据 I/O 时间比例估算
  • 如果是为了故障隔离,为每个风险域分配独立的资源边界
  • 如果是为了简化代码,确保这些线程不会争抢关键资源

"线程数 = CPU 核心数"是一个好的起点,但不是终点。理解你的工作负载,理解线程的真实开销,理解你想通过多线程解决的问题——这比任何公式都重要。

最后,无论你做出什么选择,记得压测验证。真实世界的表现总是比理论分析更复杂,而性能问题往往藏在那些"理论上应该没问题"的地方。


附录:常见框架的默认配置参考

了解你正在使用的框架的默认行为,有助于做出更好的决策。

Web 服务器

框架/服务器默认线程配置说明
Tomcat最大 200,最小 10每个请求一个线程
Jetty最大 200QueuedThreadPool
Undertow核数 × 8(I/O)+ 核数(Worker)非阻塞架构
Netty核数 × 2(EventLoop)事件驱动,少量线程
Go net/http无限制(goroutine)每连接一个 goroutine
Node.js1(主线程)+ 4(libuv 线程池)单线程事件循环
Nginxworker 数通常 = 核数每个 worker 单线程事件循环

数据库连接池

连接池默认配置推荐起点
HikariCP最大 10核数 × 2 + 磁盘数
Druid最大 8根据并发量调整
c3p0最大 15通常偏保守
pgBouncer取决于模式transaction 模式更高效

HikariCP 作者给出的经验公式:

连接数 = ((核心数 × 2) + 有效磁盘数)

对于大多数场景,10-20 个连接足以支撑相当高的吞吐量。更多的连接往往意味着更多的锁竞争和上下文切换,反而降低性能。

消息队列客户端

客户端默认配置注意事项
Kafka Consumer每分区一个线程分区数决定并行度上限
RabbitMQ Consumer可配置 prefetch控制未确认消息数
Redis (Lettuce)共享连接 + 核数个事件线程非阻塞,高效
Redis (Jedis)连接池,每操作占用一个阻塞模型

线程池最佳实践

// 推荐:根据任务类型创建不同的线程池
// 而不是所有任务共享一个

// CPU 密集型任务
ExecutorService cpuPool = Executors.newFixedThreadPool(
   Runtime.getRuntime().availableProcessors(),
   new ThreadFactoryBuilder().setNameFormat("cpu-worker-%d").build()
);

// I/O 密集型任务
ExecutorService ioPool = new ThreadPoolExecutor(
   corePoolSize,     // 核心线程数
   maxPoolSize,      // 最大线程数
   60, TimeUnit.SECONDS,  // 空闲线程存活时间
   new LinkedBlockingQueue<>(queueCapacity),  // 有界队列!
   new ThreadFactoryBuilder().setNameFormat("io-worker-%d").build(),
   new CallerRunsPolicy()  // 拒绝策略:让调用者自己执行
);

// 定时任务
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(
   2,  // 通常不需要很多
   new ThreadFactoryBuilder().setNameFormat("scheduler-%d").build()
);

关键提醒:永远使用有界队列和合理的拒绝策略。无界队列在高负载下会导致内存溢出。


作者:老迟聊架构
来源:juejin.cn/post/7607636614357663770
收起阅读 »

Python 正在遭遇人气下滑

TIOBE 的编程社区指数指出,这门全球最受欢迎的编程语言正逐渐失去市场份额,被 R、Perl 这类更专精的语言赶超。 目前 Python 在 TIOBE 月度编程语言人气指数中仍位居榜首,领先第二名 C 语言超过 10 个百分点。但在过去六个月里,Pyth...
继续阅读 »

TIOBE 的编程社区指数指出,这门全球最受欢迎的编程语言正逐渐失去市场份额,被 RPerl 这类更专精的语言赶超。


de.jpg


目前 Python 在 TIOBE 月度编程语言人气指数中仍位居榜首,领先第二名 C 语言超过 10 个百分点。但在过去六个月里,Python 的人气其实一直在下滑,从去年 7 月 26.98% 的市场份额高点,降到了本月 TIOBE 指数中的 21.81%


TIOBE 的 CEO 保罗・扬森表示,这一变化说明,有好几门更专精、更面向特定领域的语言正在慢慢蚕食 Python 的市场,其中最显眼的就是 RPerl


我大概学习过一年的 perl 语言,这门语言给我留下的比较深的印象就是文本处理,尤其是正则表达式的部分,非常厉害。


扬森提到,R 是一门用于统计计算的编程语言,长期以来在数据科学领域都是 Python 的直接竞争对手。本月 R2.19% 的占比排在第八位,而一年前它还排在第 15 位。虽然近几年 Python 超越了 R,但如今 R 似乎正重新找回势头,已经连续好几个月回到 TIOBE 指数的前十行列。


与此同时,Perl 也在脚本领域重新崛起。扬森说,Perl 曾经是脚本领域毫无争议的霸主,但后来因为多年的内部分裂,再加上新兴语言的竞争,逐渐走向衰落。不过最近这门语言迎来了转机,2025 年 9 月它在 TIOBE 指数中从一年前的第 27 位冲到了第 10 位。本月它以 1.67% 的占比排在第 11 位,而去年同期它还在第 30 位。


TIOBE 编程社区月度指数是衡量编程语言人气的指标,其评分基于一个计算公式,会评估全球范围内掌握该语言的工程师数量、相关课程以及第三方供应商的情况。评分数据会通过分析谷歌、亚马逊、维基百科、必应等超过 20 个网站得出。


2026 年 2 月 TIOBE 指数前十排名:



  1. Python,占比 21.81%

  2. C,占比 11.05%

  3. C++,占比 8.55%

  4. Java,占比 8.12%

  5. C#,占比 6.83%

  6. JavaScript,占比 2.92%

  7. Visual Basic,占比 2.85%

  8. R,占比 2.19%

  9. SQL,占比 1.93%

  10. Delphi/Object Pascal,占比 1.88%


与之相对应的的 PYPLPopularitY of Programming Language) 编程语言人气指数则是通过分析谷歌上语言教程的搜索频率来评估语言人气。


2026 年 2 月 PYPL 指数前十排名:



  1. Python,占比 31.17%

  2. C/C++,占比 14.96%

  3. Java,占比 10.46%

  4. R,占比 6.88%

  5. JavaScript,占比 5.05%

  6. Swift,占比 3.92%

  7. Rust,占比 3.19%

  8. C#,占比 3.19%

  9. PHP,占比 3.14%

  10. Ada,占比 2.81%



原文

http://www.infoworld.com/article/412…



作者:RockByte
来源:juejin.cn/post/7608953699231088680
收起阅读 »

Flutter 为什么能运行在 HarmonyOS 上

前言 Flutter 是 Google 推出的跨平台 UI 框架,最初只支持 iOS 和 Android。随着 HarmonyOS 的崛起,Flutter 也能在鸿蒙系统上运行了。这背后到底是怎么实现的呢?本文将从源码层面进行解析。 一、核心原理:Flutt...
继续阅读 »

335328e4fabd7656e8f1e9587269d3a4.jpeg


前言


Flutter 是 Google 推出的跨平台 UI 框架,最初只支持 iOS 和 Android。随着 HarmonyOS 的崛起,Flutter 也能在鸿蒙系统上运行了。这背后到底是怎么实现的呢?本文将从源码层面进行解析。




一、核心原理:Flutter 分层架构


要理解 Flutter 如何在 HarmonyOS 上运行,首先需要了解 Flutter 的架构。Flutter 采用分层设计,从上到下分为三层:


┌─────────────────────────────────┐
│   Framework 层(Dart)          │  ← Flutter 代码
├─────────────────────────────────┤
│   Engine 层(C++)              │  ← 渲染引擎(Impeller)
├─────────────────────────────────┤
│   Embedder 层(平台相关)        │  ← 与操作系统交互(调用 HarmonyOS 原生 API)
└─────────────────────────────────┘

前面两层完全复用现有Dart和C++代码,而 Embedder 层则是为 HarmonyOS 定制的。


关键点:Embedder 层


Embedder 层是 Flutter 能够跨平台运行的关键。它负责:



  • 创建和管理窗口

  • 处理输入事件

  • 调用系统 API

  • 管理渲染 Surface


**不同平台有不同的 Embedder 实现: **



  • Android:platform_view_android.cc

  • iOS:platform_view_ios.mm

  • HarmonyOS:platform_view_ohos.cpp




cc和cpp是标准的C++语言代码后缀


鸿蒙的系统API是C++ 实现的,所以鸿蒙platform_view 使用C++实现进行调用最方便**


二、HarmonyOS Embedder 的核心实现


让我们看看 HarmonyOS Embedder 的核心代码结构:


2.1 平台视图(PlatformViewOHOS)


这是 HarmonyOS Embedder 的核心类,位于:
engine/src/flutter/shell/platform/ohos/platform_view_ohos.cpp


class PlatformViewOHOS final : public PlatformView {
 public:
  PlatformViewOHOS(PlatformView::Delegate& delegate,
                   const flutter::TaskRunners& task_runners,
                   const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
                   const std::shared_ptr<flutter::OHOSContext>& ohos_context);

  // 通知窗口创建
  void NotifyCreate(fml::RefPtr<OHOSNativeWindow> native_window)

  // 更新显示尺寸
  void UpdateDisplaySize(int width, int height);

  // 分发平台消息
  void DispatchPlatformMessage(std::string name, void* message, ...);
 private:
  std::shared_ptr<OHOSContext> ohos_context_;  // HarmonyOS 图形上下文
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI装饰器(NAPI 是 HarmonyOS 提供的 JavaScript 接口, 用于调用 HarmonyOS 系统 API
  std::unique_ptr<OHOSSurface> ohos_surface_;  // HarmonyOS 渲染 Surface, surface 是渲染的目标画布, 可以是窗口, 也可以是离屏缓冲区
};

**这个类做了什么? **



  1. 继承自 PlatformView(Flutter 的通用平台视图接口)

  2. 持有 HarmonyOS 的图形上下文 OHOSContext

  3. 持有 NAPI装饰器 PlatformViewOHOSNapi(用于调用 HarmonyOS 原生 API)

  4. 管理渲染 Surface OHOSSurface


2.2 Shell 持有者(OHOSShellHolder)


Shell 是 Flutter 引擎的核心,负责管理 Flutter 应用的生命周期、渲染循环、事件处理等, OHOSShellHolder 负责创建和管理 Shell:


class OHOSShellHolder {
 public:
  // 构造函数
  // settings: Flutter 引擎启动参数(如是否启用 Impeller、日志级别等)

  // napi_facade: 与 HarmonyOS 原生层交互的 NAPI 装饰器

  // platform_loop: HarmonyOS 平台线程的 looper,用于投递平台任务
  OHOSShellHolder(const flutter::Settings& settings,
                  std::shared_ptr<PlatformViewOHOSNapi> napi_facade,
                  void* platform_loop);

  // 析构函数:确保 Shell 安全退出并释放所有资源
  ~OHOSShellHolder();

  // 启动 Flutter 引擎,加载 Dart 代码并开始渲染
  // hap_asset_provider: HarmonyOS HAP 包资源提供器,用于读取 assets、fonts、kernel_blob 等
  // entrypoint: Dart 入口函数名(默认为 main)

  // libraryUrl: Dart 库 URI(如 package:my_app/main.dart)

  // entrypoint_args: 传给 Dart main 的命令行参数列表

  void Launch(std::unique_ptr<OHOSAssetProvider> hap_asset_provider,
              const std::string& entrypoint,
              const std::string& libraryUrl,
              const std::vector<std::string>& entrypoint_args)
;
  // 优雅地停止 Flutter Shell,等待所有任务完成后退出
  void Shutdown();
  // 获取 PlatformViewOHOS 的弱引用,用于在平台线程安全地访问平台视图
  fml::WeakPtr<PlatformViewOHOS> GetPlatformView();
  // 设置应用生命周期回调,供 HarmonyOS 通知 Flutter 前后台切换
  void SetLifecycleHandler(std::function<void(AppLifecycleState)> handler);
  // 设置平台消息回调,供 HarmonyOS 主动发消息到 Dart 侧
  void SetPlatformMessageHandler(
      std::function<void(const std::string& channel,
                         const std::vector<uint8_t>& message,
                         std::function<void(std::vector<uint8_t>)> reply)> handler)
;
  // 向 Dart 侧发送平台消息,支持异步回调
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message,
                           std::function<void(std::vector<uint8_t>)> reply = nullptr)
;
  // 通知 Flutter 引擎窗口尺寸变化,触发重新布局
  void NotifyViewportMetricsChanged(const ViewportMetrics& metrics);
  // 通知 Flutter 引擎内存压力,触发 Dart 侧 GC 或资源释放
  void NotifyLowMemoryWarning();
  // 获取当前 Shell 的运行状态
  enum class ShellState { kNotStarted, kRunning, kShuttingDown, kStopped };
  ShellState GetShellState() const;
  // 返回当前线程安全的 Shell 指针,仅用于调试或测试
  Shell* GetShellUnsafe() const { return shell_.get(); }
 private:
  // 创建并配置 Flutter Shell,内部调用 Shell::Create
  void CreateShell(const flutter::Settings& settings,
                   std::unique_ptr<OHOSAssetProvider> asset_provider)
;
  // 初始化平台任务执行器,将 HarmonyOS 平台任务映射到 Flutter 的任务队列
  void SetupTaskRunners(void* platform_loop);
  // 注册 HarmonyOS 平台视图到 Shell,完成平台桥接
  void RegisterPlatformView();
  // 加载 Dart AOT 或 Kernel,决定运行模式(Release/Profile 使用 AOT,Debug 使用 Kernel)
  void LoadDartCode(const std::string& entrypoint,
                    const std::string& libraryUrl,
                    const std::vector<std::string>& entrypoint_args)
;
  // 释放所有资源,顺序:PlatformView → Shell → TaskRunners
  void Teardown();
 private:
  std::unique_ptr<Shell> shell_;                         // Flutter 引擎核心
  std::shared_ptr<PlatformViewOHOSNapi> napi_facade_;  // NAPI 装饰器
  fml::WeakPtrFactory<OHOSShellHolder> weak_factory_;    // 弱引用工厂,防止悬空指针
  ShellState state_ = ShellState::kNotStarted;           // 当前 Shell 状态
  flutter::TaskRunners task_runners_;                    // 跨平台任务队列(UI/GPU/IO/Platform)
  std::mutex state_mutex_;                               // 保护 state_ 的线程安全
};


三、图形渲染适配


Flutter 在 HarmonyOS 上支持三种渲染方式:


3.1 鸿蒙三种渲染方式


enum class OHOSRenderingAPI {
  kSoftware,          // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
  kOpenGLES,          // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
  kImpellerVulkan,    // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
};

platform_view_ohos.cpp 中,根据渲染方式创建不同的Surface


std::unique_ptr<OHOSSurface> OhosSurfaceFactoryImpl::CreateSurface() {
  switch (ohos_context_->RenderingApi()) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSSurfaceSoftware>(ohos_context_); // 软件渲染, 基于 CPU 进行渲染, 性能较低, 不依赖于 GPU,适用于简单场景。
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosSurfaceGLSkia>(ohos_context_); // OpenGL ES 渲染(Skia), 基于 OpenGL ES 进行渲染, 性能较高, 依赖于 GPU, 适用于复杂场景。
    case flutter::OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSSurfaceVulkanImpeller>(ohos_context_); // Vulkan 渲染(Impeller), 基于 Vulkan 进行渲染, 性能最高, 依赖于 GPU, 适用于需要高性能渲染的场景。
    default:
      return nullptr;
  }
}

3.2 原生窗口(OHOSNativeWindow)


HarmonyOS 的窗口系统通过 OHNativeWindow 暴露给 Flutter:


class OHOSNativeWindow : public fml::RefCountedThreadSafe<OHOSNativeWindow> {
 public:
  Handle Gethandle() const// 获取 HarmonyOS 原生窗口句柄
  bool IsValid() const;      // 检查窗口是否有效
  SkISize GetSize() const;   // 获取窗口尺寸
 private:
  Handle window_;  // OHNativeWindow*
};

**渲染流程: **


Flutter Engine
    ↓
PlatformViewOHOS
    ↓
OHOSSurface(根据渲染方式创建不同的Surface)
    ↓
OHOSNativeWindow(HarmonyOS 原生窗口)
    ↓
HarmonyOS 图形系统



四、输入事件处理


因为事件处理需要在渲染完成后(VSync同步流程)才能触发, 否则会导致事件处理与渲染不一致的问题。


4.1 VSync 同步


VSync(垂直同步)信号是渲染的关键,它是每次屏幕刷新周期开始时发送的信号,用于同步渲染和显示。


Flutter 需要等待系统的 VSync 信号,才能触发下一帧渲染。


class VsyncWaiterOHOS final : public VsyncWaiter {
 public:
  explicit VsyncWaiterOHOS(const flutter::TaskRunners& task_runners,
                           std::shared_ptr<bool>& enable_frame_cache)
;

 private:
  OH_NativeVSync* vsync_handle_;  // HarmonyOS VSync 句柄
  void AwaitVSync() override// 等待 VSync 信号
  static void OnVsyncFromOHOS(long long timestamp, void* data); // 接收 HarmonyOS VSync 信号, 通知 Flutter Engine 触发下一帧渲染
};

**工作流程: **


HarmonyOS VSync 信号
    ↓
VsyncWaiterOHOS::OnVsyncFromOHOS
    ↓
通知 Flutter Engine
    ↓
触发下一帧渲染
    ↓
渲染完成
    ↓
触发事件处理

4.2 触摸事件处理


HarmonyOS 的输入事件需要转换为 Flutter 的事件格式:


触摸事件通过 OhosTouchProcessor 处理:


class OhosTouchProcessor {
 public:
  // 处理 HarmonyOS 触摸事件
  void ProcessTouchEvent(const OH_NativeXComponent_TouchEvent* event);
 private:
  // 转换为 Flutter 触摸事件格式
  std::vector<PointerData> ConvertToFlutterTouchEvents(
      const OH_NativeXComponent_TouchEvent* event)
;
};



五、平台消息通信


Flutter 与 HarmonyOS 的通信通过 Platform Channel 实现:


5.1 NAPI 装饰器(PlatformViewOHOSNapi)


NAPI(Native API)是 HarmonyOS 提供的原生 API 接口:


class PlatformViewOHOSNapi {
 public:
  // 发送平台消息到 HarmonyOS
  void SendPlatformMessage(const std::string& channel,
                           const std::vector<uint8_t>& message)
;
  // 接收来自 HarmonyOS 的平台消息
  void SetPlatformMessageHandler(
      std::function<void(const std::string&, const std::vector<uint8_t>&)> handler)
;
 private:
  napi_env env_;  // NAPI 环境
};

5.2 消息处理流程


Flutter 代码(Dart)
    ↓
MethodChannel.invokeMethod
    ↓
PlatformViewOHOS::DispatchPlatformMessage
    ↓
PlatformViewOHOSNapi::SendPlatformMessage
    ↓
HarmonyOS 原生代码(ArkTS/C++)
    ↓
返回结果
    ↓
Flutter 接收响应



六、完整的工作流程


让我们把所有部分串联起来,看看 Flutter 应用在 HarmonyOS 上是如何运行的:


6.1 初始化流程


1. HarmonyOS 应用启动
    ↓
2. 调用 OhosMain::NativeInit(NAPI 入口)
    ↓
3. 创建 OHOSShellHolder
    ↓
4. 创建 PlatformViewOHOS
    ↓
5. 创建 OHOSContext(图形上下文)
    ↓
6. 创建 OHOSSurface(渲染表面)
    ↓
7. 创建 Flutter Shell(引擎)
    ↓
8. 加载 Dart 代码
    ↓
9. 开始渲染

6.2 渲染流程


1. Dart 代码构建 Widget 树
    ↓
2. Framework 层生成 Layer 树
    ↓
3. Engine 层生成 Scene
    ↓
4. Impeller 渲染引擎绘制
    ↓
5. 通过 OHOSSurface 提交绘制指令
    ↓
6. OHOSNativeWindow 接收绘制结果
    ↓
7. HarmonyOS 图形系统显示到屏幕

6.3 事件处理流程


1. 用户触摸屏幕
    ↓
2. HarmonyOS 接收触摸事件
    ↓
3. OhosTouchProcessor 处理
    ↓
4. 转换为 Flutter 触摸事件格式
    ↓
5. PlatformViewOHOS 分发事件
    ↓
6. Framework 层处理事件
    ↓
7. Widget 响应用户操作



七、关键代码示例


7.1 创建 HarmonyOS Embedder


// 创建图形上下文
std::unique_ptr<OHOSContext> CreateOHOSContext(
    const flutter::TaskRunners& task_runners,
    OHOSRenderingAPI rendering_api,
    bool enable_vulkan_validation,
    bool enable_opengl_gpu_tracing,
    bool enable_vulkan_gpu_tracing)
{
  switch (rendering_api) {
    case OHOSRenderingAPI::kSoftware:
      return std::make_unique<OHOSContext>(OHOSRenderingAPI::kSoftware);
    case OHOSRenderingAPI::kOpenGLES:
      return std::make_unique<OhosContextGLSkia>(OHOSRenderingAPI::kOpenGLES,
                                                 task_runners);
    case OHOSRenderingAPI::kImpellerVulkan:
      return std::make_unique<OHOSContextVulkanImpeller>(
          enable_vulkan_validation, enable_vulkan_gpu_tracing);
    default:
      return nullptr;
  }
}
// 创建平台视图
PlatformViewOHOS::PlatformViewOHOS(
    PlatformView::Delegate& delegate,
    const flutter::TaskRunners& task_runners,
    const std::shared_ptr<PlatformViewOHOSNapi>& napi_facade,
    const std::shared_ptr<flutter::OHOSContext>& ohos_context)
    : PlatformView(delegate, task_runners),
      napi_facade_(napi_facade),
      ohos_context_(ohos_context) {
  // 创建 Surface 工厂
  surface_factory_ = std::make_shared<OhosSurfaceFactoryImpl>(ohos_context_);
  // 创建渲染 Surface
  ohos_surface_ = surface_factory_->CreateSurface();
  // 预加载 GPU Surface(加速首帧渲染)
  task_runners_.GetRasterTaskRunner()->PostDelayedTask(
      [surface = ohos_surface_]() { surface->PrepareGpuSurface(); },
      fml::TimeDelta::FromMicroseconds(1000));
}

7.2 通知窗口创建


void PlatformViewOHOS::NotifyCreate(
    fml::RefPtr<OHOSNativeWindow> native_window)
{
  FML_LOG(INFO) << "NotifyCreate start";
  // 缓存原生窗口
  native_window_ = native_window;
  // 通知 Surface 窗口已创建
  ohos_surface_->SetNativeWindow(native_window);
  // 获取窗口尺寸
  SkISize size = native_window->GetSize();
  // 更新视口尺寸
  UpdateDisplaySize(size.width(), size.height());

  // 通知 Flutter 引擎窗口已创建
  NotifyCreated();
}

7.3 处理平台消息


void PlatformViewOHOS::DispatchPlatformMessage(
    std::string name,
    void* message,
    int messageLength,
    int responseId)
{
  // 创建平台消息
  fml::MallocMapping buffer = fml::MallocMapping(
      static_cast<const uint8_t*>(message), messageLength);
  auto platform_message = std::make_unique<PlatformMessage>(
      name,
      std::move(buffer),
      responseId,
      fml::TimePoint::Now());
  // 分发到 Flutter 引擎
  DispatchPlatformMessage(std::move(platform_message));
}



八、为什么 Flutter 能在 HarmonyOS 上运行?


通过上面的代码分析,我们可以总结出以下几个关键原因:


8.1 架构设计优势


Flutter 的分层架构设计使得 Embedder 层可以独立适配不同平台:



  • Framework 层Engine 层是平台无关的

  • 只有 Embedder 层需要针对不同平台实现


8.2 HarmonyOS 提供的开放接口


HarmonyOS 提供了丰富的原生 API,使得 Flutter 可以:



  • 通过 OHNativeWindow 获取窗口句柄

  • 通过 OH_NativeVSync 获取 VSync 信号

  • 通过 NAPI 调用系统能力

  • 通过 XComponent 组件集成 Flutter 视图


8.3 图形接口兼容


HarmonyOS 支持标准的图形接口:



  • OpenGL ES:Skia 渲染引擎可以直接使用

  • Vulkan:Impeller 渲染引擎可以直接使用

  • NativeWindow:提供了跨平台的窗口抽象


8.4 社区共同努力



  • 华为官方和 Flutter 社区共同维护 flutter_flutter 项目

  • 基于 Flutter Engine 源码进行适配

  • 提供完整的开发工具链




从代码层面看,核心就是实现了 PlatformViewOHOSOHOSShellHolderOHOSContext 等类,将 Flutter Engine 与 HarmonyOS 系统连接起来。


**一句话总结:Flutter 通过实现 HarmonyOS 专属的 Embedder 层,将 Flutter Engine 与 HarmonyOS 的窗口系统、图形系统、输入系统对接,从而实现了跨平台运行。 **




九、参考资料



作者:Bowen_Jin
来源:juejin.cn/post/7607097714300174346
收起阅读 »

从安装到实测:基于 Claude Code + GLM-4.7 的前端生成与评测实战

引言 近一年来,代码生成类工具逐渐从“写几行示例代码”走向“完整功能交付”,但真正落到工程实践时,很多工具仍停留在 Demo 阶段:要么跑不起来,要么改动成本过高。 本次评测的核心目标并不是追求“炫技”,而是站在开发者真实使用场景出发,验证一套组合方案是否具备...
继续阅读 »

引言


近一年来,代码生成类工具逐渐从“写几行示例代码”走向“完整功能交付”,但真正落到工程实践时,很多工具仍停留在 Demo 阶段:要么跑不起来,要么改动成本过高。 本次评测的核心目标并不是追求“炫技”,而是站在开发者真实使用场景出发,验证一套组合方案是否具备以下能力:



  • 是否能在本地环境中快速跑通

  • 是否能端到端生成可演示、可交付的前端成果

  • 是否减少重复劳动,而不是制造新的维护负担


因此,本文选择了 Claude Code + 蓝耘 MaaS 平台 这一组合,从命令行工具****接入开始,结合多个真实前端需求案例,对模型在网页应用、小游戏以及 3D 可视化等场景下的表现进行实测分析。 评测重点不在“模型参数”或“理论能力”,而在于:它到底能不能帮开发者省时间、少踩坑。



最大输出和最大输入一比一,编码能力放在下面了,个人觉得是挑不出毛病的好吧。不信你试试


一、命令行使用 Claude Code(安装与配置)


步骤一:安装 Claude Code(命令行)


前提



  • Node.js ≥ 18(建议使用 nvm 管理版本以避免权限问题)。

  • macOS:推荐用 nvm 或 Homebrew 安装 Node.js,不建议直接双击 pkg 安装(可能有权限问题)。

  • Windows:请先安装 Git for Windows。


安装


npm install -g @anthropic-ai/claude-code

安装完成后验证:


claude --version


步骤二:配置蓝耘MaaS平台


1、注册 / 登录:访问**蓝耘MaaS平台**,完成账号注册并登录。


2、在「API KEY 管理」中创建 API Key,并复制备用。



在本机设置环境变量(推荐方式:编辑配置文件)



  • macOS / Linux:~/.claude/settings.json

  • Windows:%USERPROFILE%/.claude/settings.json


示例 settings.json(请替换your_lanyun_maas_api_key):


{
"env": {
"ANTHROPIC_AUTH_TOKEN": "your_lanyun_maas_api_key",
"ANTHROPIC_BASE_URL": "https://maas-api.lanyun.net/anthropic",
"API_TIMEOUT_MS": "3000000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1,
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "/maas/deepseek-ai/DeepSeek-V3.2",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "/maas/deepseek-ai/DeepSeek-V3.2",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "/maas/deepseek-ai/DeepSeek-V3.2"
}
}



  • 同时创建(或确认)~/.claude.json


    {
    "hasCompletedOnboarding": true
    }




生效提示



  • 配置完成后请打开一个新的终端窗口以载入新的环境变量。

  • 启动 claude,首次会询问是否使用该 API key(选择 Yes),并请在第一次访问时同意信任工作目录(允许读取文件以便代码功能)。



步骤三:常见排查



  • 若手动修改 ~/.claude/settings.json 后不生效:



    • 关闭所有 Claude Code 窗口,重新打开新的终端。

    • 若仍不生效,尝试删除该文件并重新生成配置(注意备份原文件)。

    • 检查 JSON 格式是否正确(可用在线 JSON 校验工具)。



  • 检查版本与更新:


    claude --version
    claude update



二、编码工具中使用 claude-code:三个端到端案例(含提示与实测评价)



每个案例先给出“需求 + 提示词”示例,然后给出对模型产出(代码/效果)的实测评价,评价尽量贴近工程实践:是否能直接运行、需要手工修改的点、功能完整性、性能与安全注意项。



案例 1:交互式个人血压记录网页 — 前端端到端生成


需求:希望 GLM-4.7 能够生成一个简单的个人血压记录网页应用,包括录入血压数据的前端界面和一个数据可视化大屏展示页面,要求界面美观,且支持单人登录功能。


提示词:我们向 GLM-4.7 输入了如下的自然语言提示:



请用 HTML、CSS 和 JavaScript 创建一个完整的个人血压记录网页应用。要求包括:1) 用户登录界面;2) 血压数据录入表单(收缩压、舒张压、测量日期);3) 数据可视化大屏界面,以图表展示历史血压记录;4) 整体界面风格现代简洁,配色协调美观。5) 将前端代码与样式、脚本整合在一个 HTML 文件中,方便直接运行。





实测评价(工程视角)



  • 可运行性:生成的单文件 HTML 通常能在本地直接打开并运行,图表(如用 Chart.js)能正常渲染——基本可直接跑通

  • 需要人工补充/注意点:持久化通常仅用 localStorage,真实生产需后端与加密;登录为前端模拟(不安全),若要求真登录需接入后端 API 与认证方案。

  • 代码质量:结构清晰但注释与边界检查(表单验证、异常处理)需补充;样式可直接用但对响应式与无障碍要进一步优化。

  • 总结:非常适合原型与内部演示;若要上线需补后端、认证与输入校验、数据导出等工程工作。


案例 2:Web 双人对战小游戏(Joy-Con 风格)


需求:开发一个基于 Web 的双人对战小游戏,界面风格模仿 Nintendo Switch 主机的 Joy-Con 手柄,包括左右两个虚拟手柄和中间的游戏屏幕。要求实现基本的游戏逻辑和简单的控制功能。


提示词:我们向 GLM-4.7 输入了如下提示:



请用 HTML5 Canvas 和 JavaScript 编写一个双人对战小游戏。界面要求模仿 Nintendo Switch 的 Joy-Con 手柄:左侧蓝色手柄,右侧红色手柄,中间为游戏屏幕。玩家 1 使用键盘 A/D 移动,J 攻击,K 跳跃;玩家 2 使用键盘 U/I/O 分别释放技能。游戏要求有基本的角色移动和攻击判定逻辑,界面风格统一美观。请将所有代码整合在一个 HTML 文件中,确保在浏览器中打开即可运行。




实测评价(工程视角)



  • 可运行性:模型生成的 Canvas 游戏通常包含主循环、碰撞/判定的基本实现,能够进行本地试玩;帧率在普通浏览器和单页面逻辑下表现正常。

  • 需要人工补充/注意点:物理判定、碰撞响应和输入去抖(debounce)常是“粗糙实现”,需手动修正以避免卡顿或误判;网络对战未实现(仅本地双人)。

  • 代码质量:逻辑上可读,但没有模块化(全部放在全局),不利于维护;建议拆分为模块或使用简易引擎封装。

  • 总结:适合快速原型与教学演示;若做成产品需重构输入处理、物理/判定逻辑、以及添加资源管理与关卡数据。


案例 3:前端可视化组件生成


需求:创建一个基于 Three.js 的 3D 场景,包含一个华丽的宝塔和周围盛开的樱花树,场景要求视觉精美、结构清晰,且支持用户通过鼠标或手势进行交互控制(如旋转场景、缩放视图)。


提示词:我们向 GLM-4.7 输入了如下提示:



请用 Three.js 编写一个包含宝塔和樱花树的 3D 场景。要求:1) 宝塔位于场景中央,装饰华丽;2) 周围环绕盛开的樱花树,营造花园氛围;3) 场景使用等轴测或俯视视角,光影柔和,有适当的环境光和定向光以产生投影;4) 支持鼠标拖动旋转场景和滚轮缩放查看;5) 所有代码整合在一个 HTML 文件中,使用 CDN 引入 Three.js 及其依赖,确保直接打开即可运行。




实测评价(工程视角)



  • 可运行性:多数生成结果能在现代浏览器中打开并展示场景(依赖 CDN 的 Three.js),基础交互(OrbitControls)通常可用。

  • 需要人工补充/注意点:模型与细节(如樱花树的粒子/贴图)可能是简单几何或贴图替代,若追求视觉精细需要自行替换高质量模型/贴图与烘焙光照或使用 PBR 材质;阴影与性能在低端设备上需做 LOD/简化处理。

  • 代码质量:示例代码多为教学风格,未必包含资源加载进度管理与错误处理;建议加上纹理压缩、异步加载与内存释放逻辑。

  • 总结:适合演示级视觉效果与交互交付;商业级视觉需投入美术资源并改造渲染管线与性能优化。


三、补充建议(快速 checklist)



  • 环境:Node.js 用 nvm 管理、macOS 权限使用 sudo 谨慎;Windows 使用 PowerShell / Git Bash 测试命令。

  • 配置:编辑 ~/.claude/settings.json 时注意 JSON 语法(逗号、引号、转义);每次修改后重启终端。

  • 模型选择:通过 ~/.claude/settings.json 修改 ANTHROPIC_DEFAULT_*_MODEL 字段来切换模型;切换后启动 claude 并在交互中用 /status 确认。

  • 安全/上线:所有“示例仅前端”场景上线前必须接入安全认证、后端存储与输入验证(避免注入与隐私泄露)。


总结


从本次实际使用和多个案例的结果来看,Claude Code 在接入蓝耘 MaaS 后,已经具备“工程可用级”的生成能力,尤其在以下几个方面表现比较稳定:



  • 端到端能力明确:在单文件 HTML、前端 Demo、Canvas 游戏、Three.js 场景等任务中,生成结果大多可直接运行,减少了大量“拼代码”的前期工作。

  • 适合作为原型与验证工具:非常适合用在需求验证、内部演示、方案评审和教学场景中,而不是一开始就手写全部代码。

  • 开发者心智成本低:命令行方式接入,不改变现有工作流,比网页对话式工具更符合日常编码习惯。


当然,也需要客观看待它的边界:



  • 生成代码在安全性、模块化、性能优化方面仍需要人工介入;

  • 登录、数据存储、多人协作等生产级能力仍需配合后端体系完善;

  • 更复杂的项目仍然离不开开发者的架构设计与工程判断。


整体来看,这套方案的价值并不在于“替代程序员”,而在于把开发者从重复、低价值的样板工作中解放出来,让时间更多地投入到业务逻辑、架构设计和体验打磨上。


如果你的目标是: 更快做出可运行的东西,而不是从零写样板代码,那么 Claude Code + 蓝耘 MaaS,已经是一个值得放进工具箱里的选项。


作者:Lethehong
来源:juejin.cn/post/7607358297458196480
收起阅读 »

Spring Boot + JPackage:构建独立安装包!

前言 从 JDK 14 开始,Java 官方引入了 JPackage** 工具(在 JDK 16 正式成为标准功能),它能够将 Java 应用打包成特定平台的原生安装包,自带定制化的 JRE 运行环境。这意味着用户无需提前安装 Java 环境,双击安装包即可完...
继续阅读 »

前言


从 JDK 14 开始,Java 官方引入了 JPackage** 工具(在 JDK 16 正式成为标准功能),它能够将 Java 应用打包成特定平台的原生安装包,自带定制化的 JRE 运行环境。这意味着用户无需提前安装 Java 环境,双击安装包即可完成应用部署,极大地简化了交付流程。


本文将介绍如何使用 JPackage 工具将 Spring Boot 项目打包成 Windows、macOS 或 Linux 平台的原生安装包。


一、JPackage 简介


1.1 什么是 JPackage


JPackage 是 JDK 自带的打包工具,位于 $JAVA_HOME/bin 目录下。它的核心功能是:


生成平台原生安装包:Windows 的 .exe/.msi、macOS 的 .dmg/.pkg、Linux 的 .deb/.rpm


自定义 JRE:使用 jlink 工具裁剪 JDK,仅打包应用所需的模块,大幅减小安装包体积


简化部署:用户无需预装 Java 环境,安装包自带运行时


1.2 JPackage 的优势


传统部署方式JPackage 方式
需要预装 JRE/JDK自带 JRE,无需额外安装
环境版本可能不匹配绑定特定 JRE 版本,环境一致
手动编写启动脚本自动生成启动器
跨平台需要多套脚本一键生成各平台安装包

二、环境准备


2.1 JDK 版本要求


推荐使用 JDK 17 或更高版本(JPackage 在 JDK 16 才成为标准功能,JDK 17 是 LTS 版本)


确认 JPackage 可用:


jpackage --version

2.2 平台特定工具


根据目标操作系统,需要安装对应的打包工具:


Windows


WiX Toolset** 3.11+(用于生成 .msi 安装包) 下载地址:wixtoolset.org/ 安装后将 bin 目录添加到系统环境变量 PATH


macOS


Xcode** 命令行工具(用于生成 .dmg/.pkg


xcode-select --install

Linux


Debian/Ubuntu:安装 fakeroot


sudo apt-get install fakeroot

RedHat/CentOS:安装 rpm-build


sudo yum install rpm-build

三、Spring Boot 项目准备


3.1 示例项目结构


假设我们有一个标准的 Spring Boot 项目:


my-springboot-app/
├── src/
│   └── main/
│       ├── java/
│       └── resources/
├── pom.xml
└── target/
    └── my-app-1.0.0.jar

3.2 构建可执行 JAR


首先使用 Maven 或 Gradle 构建项目:


## Maven
mvn clean package

#
# Gradle
gradle clean build

确保生成的 JAR 包是可执行的(Spring Boot 默认打包方式)。


四、使用 JPackage 打包


4.1 基础打包命令


以下是一个基础的 JPackage 命令示例(以 Windows 为例):


jpackage \
  --input target \
  --name MySpringBootApp \
  --main-jar my-app-1.0.0.jar \
  --main-class org.springframework.boot.loader.JarLauncher \
  --type msi \
  --app-version 1.0.0 \
  --vendor "我的公司" \
  --description "基于 Spring Boot 的企业级应用" \
  --icon src/main/resources/app-icon.ico \
  --win-dir-chooser \
  --win-menu \
  --win-shortcut

参数说明


参数说明
--input输入目录,包含 JAR 包和依赖
--name应用名称
--main-jar主 JAR 包文件名
--main-class主类(Spring Boot 使用 JarLauncher
--type安装包类型(msi/exe/dmg/pkg/deb/rpm
--app-version应用版本号
--icon应用图标(Windows 用 .ico,macOS 用 .icns
--win-dir-chooser允许用户选择安装目录
--win-menu创建开始菜单项
--win-shortcut创建桌面快捷方式

4.2 自定义 JRE(使用 jlink)


为了减小安装包体积,可以使用 jlink 裁剪 JRE,仅包含必要的模块。


步骤 1:查找应用依赖的模块


jdeps --list-deps target/my-app-1.0.0.jar

输出示例:


java.base
java.logging
java.sql
java.naming
java.desktop
...

步骤 2:使用 jlink 创建自定义 JRE


jlink \
  --add-modules java.base,java.logging,java.sql,java.naming,java.desktop,java.xml,java.management \
  --output custom-jre \
  --strip-debug \
  --no-header-files \
  --no-man-pages \
  --compress=2

步骤 3:使用自定义 JRE 打包


jpackage \
  --input target \
  --name MySpringBootApp \
  --main-jar my-app-1.0.0.jar \
  --main-class org.springframework.boot.loader.JarLauncher \
  --type msi \
  --runtime-image custom-jre \
  --app-version 1.0.0 \
  --vendor "我的公司"


注意:Spring Boot 应用通常依赖较多模块,建议先不裁剪 JRE,确保功能正常后再优化。





五、不同平台的打包示例


5.1 Windows 平台(MSI)


jpackage \
  --input target \
  --name MyApp \
  --main-jar my-app-1.0.0.jar \
  --main-class org.springframework.boot.loader.JarLauncher \
  --type msi \
  --app-version 1.0.0 \
  --icon src/main/resources/app.ico \
  --win-dir-chooser \
  --win-menu \
  --win-shortcut \
  --win-menu-group "我的应用"

5.2 macOS 平台(DMG)


jpackage \
  --input target \
  --name MyApp \
  --main-jar my-app-1.0.0.jar \
  --main-class org.springframework.boot.loader.JarLauncher \
  --type dmg \
  --app-version 1.0.0 \
  --icon src/main/resources/app.icns \
  --mac-package-name "com.mycompany.myapp" \
  --mac-package-identifier "com.mycompany.myapp"

5.3 Linux 平台(DEB)


jpackage \
  --input target \
  --name myapp \
  --main-jar my-app-1.0.0.jar \
  --main-class org.springframework.boot.loader.JarLauncher \
  --type deb \
  --app-version 1.0.0 \
  --icon src/main/resources/app.png \
  --linux-shortcut \
  --linux-menu-group "Development"



六、集成到 Maven 构建流程


为了自动化打包流程,可以将 JPackage 命令集成到 Maven 的 pom.xml 中。


6.1 使用 exec-maven-plugin


在 pom.xml 中添加以下插件配置:


<build>
    <plugins>
        <!-- Spring Boot Maven 插件 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <!-- JPackage 打包插件 -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
                <execution>
                    <id>jpackage</id>
                    <phase>package</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <executable>jpackage</executable>
                        <arguments>
                            <argument>--input</argument>
                            <argument>target</argument>
                            <argument>--name</argument>
                            <argument>MySpringBootApp</argument>
                            <argument>--main-jar</argument>
                            <argument>${project.build.finalName}.jar</argument>
                            <argument>--main-class</argument>
                            <argument>org.springframework.boot.loader.JarLauncher</argument>
                            <argument>--type</argument>
                            <argument>msi</argument>
                            <argument>--app-version</argument>
                            <argument>${project.version}</argument>
                            <argument>--vendor</argument>
                            <argument>我的公司</argument>
                            <argument>--win-dir-chooser</argument>
                            <argument>--win-menu</argument>
                            <argument>--win-shortcut</argument>
                        </arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

6.2 执行构建


mvn clean package

构建完成后,安装包将生成在项目根目录下。


作者:Java编程爱好者
来源:juejin.cn/post/7609677415800373288
收起阅读 »

把模型焊死在芯片上,就能跑出 17,000 tokens/秒?这是一条死路,还是一条新路?

最近刷到一条挺“炸裂”的消息:多伦多一家初创公司 Taalas 做了一颗 HC1 芯片,宣称跑 Llama 3.1 8B 能到 17,000 tokens/秒。 方案倒是很好理解,他们把 AI 大模型物理焊死在芯片里。 方案优劣先按下不表,我们先把一个问题讲...
继续阅读 »

最近刷到一条挺“炸裂”的消息:多伦多一家初创公司 Taalas 做了一颗 HC1 芯片,宣称跑 Llama 3.1 8B 能到 17,000 tokens/秒



方案倒是很好理解,他们把 AI 大模型物理焊死在芯片里


方案优劣先按下不表,我们先把一个问题讲清楚:17,000 tokens/秒 到底意味着什么?


Token 和 TPS


在大语言模型(LLM)中,Token 是模型处理文本的基本单位,可以理解为把文本切成一小片一小片的“最小颗粒度”。


它可以是一个单词(如 “cat”)、子词(如 “un”、“believable”),甚至是一个标点符号。例如:



  • 英文句子 “Hello, world!” ≈ 3 tokens("Hello", ",", "world" + "!")

  • 中文 “你好世界” ≈ 4 tokens(每个汉字通常单独成 token)


TPS(tokens per second) 指每秒生成多少 token,是衡量推理“输出吞吐”的核心指标。TPS 越高,长回答越像“刷屏”;越低,就越像在看模型慢慢打字。


横向对比


虽然了解了名词,但是直观感受依然没有。


我们来看几个直观的对比。



  • 普通消费级 GPU(如 RTX 4090)运行 Llama 3 8B:约 30–60 tokens/秒

  • 英伟达 H100 运行同模型:约 150–300 tokens/秒

  • Cerebras CS-3 系统(专为 AI 设计):约 2,000 tokens/秒

  • Taalas HC117,000 tokens/秒 —— 比 H100 快 50–100 倍


上面几个指标对比后,新方案 Taalas 的优势应该非常明显了。


代价是什么


这么高的速度优势,那付出的代价是什么呢?


无法更新/更改模型


芯片出厂后,只能运行写死的模型,比如报道中的 Llama 3.1 8B


不管是想要更新到 Llama 4,还是更换其他多模态模型,都无法实现。


特定场景下,反而刚刚好


看到这个限制,很多人的第一反应可能是:那不就成“一次性芯片”了?


但换个角度,如果你的需求足够稳定,它反而可能很有价值。


场景 1:智能体(Agent)之间的通信


目前,多智能体通信已经成为标配,如果依然采用原有通用芯片的吞吐,那速度将会成为瓶颈。


此时,速度 >> 灵活性,专用路线的 AI 芯片正好适合,甚至可以接受人类无法理解的速度。


场景 2:垂直领域嵌入式 AI


工厂质检机器人、车载语音助手、智能家居中枢,这类只需执行固定任务的设备,模型多年不变影响也不大。


此时,低成本、低功耗、高可靠性的专用芯片无疑可以带来更好的投入产出比。


因此,特殊场景下,个人感觉专用 AI 芯片可能具有更大的价值。


结语


17,000 tokens/秒 也许还需要更多公开测试来验证,但已经揭示了专有化 AI 芯片的价值。


如果让你选,你更看好哪种方向:继续堆通用算力,还是把专用化模型直接铺到设备里?


作者:飞哥数智谈
来源:juejin.cn/post/7610160716066488330
收起阅读 »

Java 版本管理工具:Jabba

shyiko/jabba: (cross-platform) Java Version ManagerJabba 是专门为 Java 设计的版本管理工具,基于 Go 开发,体积小、速度快,对 Windows 原生支持非常好,无需依赖 WSL 或其他复杂环境,是...
继续阅读 »

shyiko/jabba: (cross-platform) Java Version Manager

Jabba 是专门为 Java 设计的版本管理工具,基于 Go 开发,体积小、速度快,对 Windows 原生支持非常好,无需依赖 WSL 或其他复杂环境,是纯 Windows 下管理 Java 版本的首选。

详细使用步骤(Windows PowerShell):

  1. 安装 Jabba
# 以管理员身份打开PowerShell,执行安装命令
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-Expression (Invoke-WebRequest -Uri https://github.com/shyiko/jabba/raw/master/install.ps1 -UseBasicParsing).Content

安装完成后重启 PowerShell,验证是否安装成功:

jabba --version
  1. 核心使用命令
# 列出所有可安装的Java版本
jabba ls-remote

# 安装指定版本(以zulu@1.17.0-0为例)

jabba install zulu@1.17.0-0

# 临时切换当前终端的Java版本

jabba use zulu@1.17.0-0

# 设置全局默认Java版本(永久生效)

jabba alias default zulu@1.17.0-0

# 查看已安装的版本

jabba ls

# 卸载不需要的版本

jabba uninstall zulu@1.17.0-0


作者:夏默Notes
来源:juejin.cn/post/7608102583497048073
收起阅读 »

AI 系统架构

AI 系统看起来很复杂,但核心可以压缩成三句话: 尽量少搬数据:很多时候不是算不动,而是数据搬运太慢。 尽量提高有效计算密度:让硬件更多时间在做有价值的乘加计算。 尽量重叠计算与通信:训练和推理都要避免“设备空等”。 换句话说,AI 性能问题本质上是 计算...
继续阅读 »

AI 系统看起来很复杂,但核心可以压缩成三句话:



  1. 尽量少搬数据:很多时候不是算不动,而是数据搬运太慢。

  2. 尽量提高有效计算密度:让硬件更多时间在做有价值的乘加计算。

  3. 尽量重叠计算与通信:训练和推理都要避免“设备空等”。


换句话说,AI 性能问题本质上是 计算(Compute)+ 访存(Memory)+ 通信(Communication) 的协同问题。


1. AI 系统栈


层级主要职责典型问题常见关键词
L7 AI 应用层提供用户可见功能回答是否准确、体验是否流畅Chat、Copilot、推荐
L6 业务编排层把业务逻辑组织成可执行流程如何用最少 token 获得最好结果Prompt、RAG、Agent
L5 模型服务层把模型能力稳定对外提供如何高可用、可扩展、可治理网关、限流、灰度、A/B
L4 推理引擎层把请求高效变成 token 输出如何降 TTFT/TPOT、提并发Batch、KV Cache、PagedAttention
L3 训练框架层训练与微调模型如何在多卡多机稳定收敛Autograd、DDP/FSDP、计算图
L2 编译运行时层把模型算子变成高效程序如何逼近硬件峰值性能IR、Fusion、Tiling、CUDA
L1 硬件系统层提供真实算力与带宽算力/带宽/通信瓶颈在哪里Tensor Core、HBM、NVLink



2. AI 硬件与体系结构:算力的物理根基


2.1 CPU、GPU、ASIC 的职责划分



  • CPU(中央处理器):通用控制能力强,擅长复杂分支和系统调度。

  • GPU(图形处理器):并行吞吐高,擅长大规模矩阵乘法。

  • ASIC(专用芯片):针对 AI 运算固化电路(如 TPU/NPU),能效高但通用性低。


类比:



  • CPU 像“总指挥 + 少量专家”。

  • GPU 像“超大规模流水线工人”。

  • ASIC 像“只做某几类工序但极快的专机”。


2.2 GPU 的执行单位:SIMT、Warp、Block



  • SIMT(Single Instruction Multiple Threads):同一程序由大量线程并发执行。

  • Warp:GPU 调度基本单位(NVIDIA 常见 32 线程)。

  • Thread Block(线程块):多个 warp 组成,可共享片上内存。


关键性能点:



  1. Warp Divergence(分支发散):同一 warp 走不同分支会串行执行,吞吐下降。

  2. Coalesced Access(内存合并访问):连续地址访问可减少内存事务。

  3. Occupancy(占用率):同时驻留 SM 的线程比例;不是越高越好,要平衡寄存器压力。


2.3 内存层级决定“真实速度”


从快到慢大致是:



  1. Register(寄存器)

  2. Shared Memory / SRAM(共享内存/片上存储)

  3. L2 Cache

  4. HBM(高带宽显存)

  5. Host Memory(主机内存)

  6. Remote Memory(远端节点)


高性能 kernel 的共同目标:尽量让热点数据停留在更靠近计算单元的层级


2.4 互联与通信:单机多卡到多机集群



  • PCIe:通用互联,带宽相对有限。

  • NVLink/NVSwitch:GPU 间高带宽低延迟互联。

  • InfiniBand + RDMA:多机高性能网络。


训练常见通信原语:



  • All-Reduce:聚合并广播(常用于梯度同步)。

  • All-Gather:把各卡分片收集到每卡。

  • Reduce-Scatter:先归约再分发(常与 All-Gather 配合)。




3. AI 编译与计算架构:模型代码如何变成硬件指令


3.1 为什么需要 AI 编译器


如果每个框架都手写每种芯片的底层代码,会形成 N 框架 × M 硬件 组合爆炸。AI 编译器通过中间表示把问题变成:


前端框架 -> IR(中间表示) -> 后端硬件


代表系统:TVM、XLA、TensorRT、MLIR 生态


3.2 多级 IR(Intermediate Representation,中间表示)


常见分层:



  1. High-level IR(高层图 IR):表达算子依赖关系,便于图级优化。

  2. Tensor/Loop IR(张量或循环 IR):表达循环、访存、布局,便于调度优化。

  3. Low-level IR(低层 IR):接近目标指令(如 PTX、LLVM IR)。


3.3 前端优化(硬件无关)



  • Constant Folding(常量折叠):编译期算掉常量表达式。

  • Dead Code Elimination(死代码消除):删掉无用分支。

  • Operator Fusion(算子融合):合并多个小算子,减少中间读写。

  • Shape Inference(形状推导):提前推断维度,减少运行期开销。


例子:


MatMul -> Add -> GELU 三个 kernel 可以融合为一个 fused kernel,减少两次中间张量落地。


3.4 后端优化(硬件相关)



  • Tiling(分块):把大矩阵切小块,提升缓存命中。

  • Vectorization(向量化):一条指令并行处理多个元素。

  • Unrolling(循环展开):减少分支跳转和调度开销。

  • Double Buffering(双缓冲):计算当前块时预取下一块,隐藏访存延迟。

  • Auto-Tuning(自动调优):自动搜索 block size、tile size、pipeline 深度。


3.5 CUDA 编程模型(理解“手写 kernel 为何快”)


CUDA 核心概念:



  • Grid(网格):一次 kernel launch 的全体线程块集合。

  • Block(线程块):可共享 shared memory 的线程组。

  • Thread(线程):最小执行单元。


手写 kernel 价值高的场景:



  1. 小算子链可融合。

  2. 特殊 shape(如超长序列)导致通用库不最优。

  3. 延迟极其敏感链路(在线推理)。




4. AI 框架核心模块:训练引擎的心脏


4.1 Tensor 与计算图



  • Tensor(张量):带 shape/dtype/layout/device/stride 的多维数组。

  • Computational Graph(计算图):节点是算子,边是张量依赖关系。

  • DAG(有向无环图):计算图通常是 DAG,保证依赖可拓扑排序。


动态图与静态图:



  • Dynamic Graph(动态图):边执行边建图,调试灵活(如 PyTorch eager)。

  • Static Graph(静态图):先建图再编译执行,优化空间大(如 XLA 图模式)。


现代方向:动静结合(开发用动态图,部署时图编译)。


4.2 Autograd(自动微分)到底在做什么


自动微分不是数值差分,也不是纯符号求导,它是“程序级链式法则”。


简化例子:



  • 前向:y = (w*x + b)^2

  • 反向:框架自动记录依赖并计算

    • dy/dw = 2*(w*x+b)*x

    • dy/db = 2*(w*x+b)




你只写 loss.backward(),框架完成拓扑回溯和梯度累加。


关键工程点:



  1. Activation Checkpointing(激活重计算):省显存,代价是额外计算。

  2. Mixed Precision(混合精度):常用 BF16/FP16 提升吞吐。

  3. Loss Scaling(损失缩放):防止低精度下梯度下溢。


4.3 分布式并行:LLM 训练为什么离不开它


单卡常见瓶颈:参数放不下、激活放不下、吞吐不够。


并行策略:



  1. DP(Data Parallel,数据并行):模型复制到多卡,数据切分。

  2. TP(Tensor Parallel,张量并行):单层矩阵按维度切到多卡。

  3. PP(Pipeline Parallel,流水线并行):按层切分到不同设备。

  4. FSDP/ZeRO(全分片数据并行):参数、梯度、优化器状态分片,显存友好。


类比:



  • DP:每家分店做同一菜单,不同顾客。

  • TP:一道超大菜由多位厨师同时做不同部分。

  • PP:后厨分工流水线,A 备料,B 烹饪,C 装盘。


4.4 集合通信库 NCCL 的地位



  • NCCL:NVIDIA 的 GPU 集合通信库。

  • 对大规模训练而言,通信效率直接决定扩展效率。

  • 优化目标是 Overlap(重叠):反向计算的同时进行梯度通信,减少空等。




5. AI 推理系统与引擎:走向生产的最后一公里


5.1 训练关注“学会”,推理关注“服务好”


训练目标:高吞吐 + 收敛精度。

推理目标:低延迟 + 高并发 + 低成本 + 稳定性。


5.2 推理引擎的核心职责



  1. 模型加载与图优化。

  2. 请求排队、动态批处理、并发调度。

  3. KV Cache 管理。

  4. kernel 选择与执行。

  5. 监控指标上报(TTFT、TPOT、P95/P99)。


5.3 Prefill 与 Decode 的优化重点不同



  • Prefill:计算密集,重点看吞吐和 Tensor Core 利用率。

  • Decode:访存+调度密集,重点看单步延迟和 cache 命中。


5.4 模型转换:训练框架与部署环境解耦


常见链路:



  1. 训练框架导出模型(如 ONNX 或引擎专有格式)。

  2. 引擎做图优化与算子替换。

  3. 构建硬件相关执行计划(engine build)。

  4. 发布到线上并灰度验证。


术语:



  • ONNX(Open Neural Network Exchange):跨框架模型交换格式。

  • Engine Build(引擎构建):针对目标硬件生成最优执行计划。


5.5 模型轻量化:量化、剪枝、蒸馏



  1. Quantization(量化):FP16/FP32 -> INT8/INT4,降低显存与带宽开销。

  2. Pruning(剪枝):删除低贡献连接/通道。

  3. Knowledge Distillation(知识蒸馏):大模型指导小模型学习。


生活化例子:



  • 量化像把照片从 RAW 压成高质量 JPEG,体积显著变小,细节轻微损失。

  • 剪枝像裁掉盆景无效枝杈,让营养集中到主干。

  • 蒸馏像名师把重点题型浓缩成小册子给学生。


5.6 LLM 推理热点技术



  • PagedAttention(分页注意力):把 KV Cache 分页管理,降低碎片。

  • Continuous Batching(连续批处理):动态拼批,提升设备利用率。

  • Prefix Cache(前缀缓存):复用共享前缀,避免重复 prefill。

  • Speculative Decoding(投机解码):小模型草拟,大模型校验提速。

  • CUDA Graph:复用固定执行图,降低 kernel launch 开销。


5.7 线上必须看的指标与告警



  • 业务层:QPS、成功率、P95/P99 延迟。

  • 模型层:TTFT、TPOT、tokens/s。

  • 资源层:GPU 利用率、显存水位、KV 命中率。

  • 稳定性:OOM 次数、重试率、超时率、节点漂移。




6. 端到端工程实战:一条训练与部署链路


下面是一条常见流程,适合作为团队实施模板。



  1. 训练侧



    • 准备数据与特征。

    • 选择并行策略(DP/TP/PP/FSDP)。

    • 开启混合精度与梯度检查点。

    • 监控 MFU、通信时间占比、loss 曲线。



  2. 导出与优化侧



    • 固化模型版本与权重 checksum。

    • 导出 ONNX 或目标引擎格式。

    • 跑量化标定(PTQ)或量化感知训练(QAT)。

    • 进行 engine build 与 benchmark。



  3. 推理侧



    • 上线前压测:TTFT/TPOT/P99。

    • 打开连续批处理与 KV 分页。

    • 设置多级降级策略(限流、降精度、短路回复)。

    • 灰度发布,监控回归。



  4. 回路闭环



    • 采集线上 bad case。

    • 进入下一轮训练与蒸馏。

    • 通过 A/B Test 验证收益。






结语


model.forward(x) 到 GPU 上数十亿晶体管翻转,AI 系统是一套跨学科工程:



  • 体系结构决定物理上限。

  • 编译器决定代码能否逼近上限。

  • 框架决定训练是否可扩展、可维护。

  • 推理系统决定模型能否稳定创造业务价值。


真正稀缺的能力,不只是“会训练模型”,而是能把模型在真实生产中 稳定、低成本、高性能 地跑起来。




附录:AI 术语词典(按模块整理)


1 硬件与体系结构


术语英文全称一句话解释
AI InfraArtificial Intelligence Infrastructure支撑 AI 训练与推理的软硬件系统工程。
CPUCentral Processing Unit通用处理器,强控制与通用计算。
GPUGraphics Processing Unit高并行吞吐处理器,擅长矩阵运算。
ASICApplication-Specific Integrated Circuit面向特定任务定制的专用芯片。
TPUTensor Processing UnitGoogle 的 AI 专用加速芯片。
NPUNeural Processing Unit面向神经网络运算的专用单元。
Tensor Core-GPU 上用于矩阵乘加的专用计算单元。
FLOPSFloating Point Operations Per Second每秒浮点运算次数,常用算力指标。
Bandwidth-单位时间可传输的数据量。
Roofline-用算力上限和带宽上限分析性能边界的模型。
SIMDSingle Instruction Multiple Data一条指令并行处理多个数据元素。
SIMTSingle Instruction Multiple Threads同一程序由多个线程并发执行。
Warp-GPU 调度的基本线程组。
SMStreaming MultiprocessorGPU 的核心计算资源单元。
HBMHigh Bandwidth MemoryGPU 高带宽显存。
SRAMStatic Random Access Memory片上低延迟存储,常用于缓存。
PCIePeripheral Component Interconnect Express通用高速总线接口。
NVLink-NVIDIA GPU 间高速互联。
RDMARemote Direct Memory Access跨节点低开销远程内存访问技术。

2 编译与执行


术语英文全称一句话解释
Compiler-将模型计算转换为目标硬件可执行程序。
IRIntermediate Representation编译器内部的中间抽象表示。
Frontend-负责解析模型并做图级优化。
Backend-负责硬件相关调度与代码生成。
Constant Folding-编译期预计算常量表达式。
DCEDead Code Elimination删除不影响结果的无效计算。
Operator Fusion-把多个算子融合为一个更高效算子。
CodegenCode Generation将 IR 翻译为目标代码。
Tiling-按块划分计算以提升局部性。
Vectorization-把标量操作改写为向量并行操作。
UnrollingLoop Unrolling展开循环减少跳转开销。
Auto-Tuning-自动搜索最佳 kernel 参数配置。
CUDACompute Unified Device ArchitectureNVIDIA 的 GPU 编程平台。
Kernel-在 GPU 上执行的函数。
PTXParallel Thread ExecutionNVIDIA 的中间指令表示。
cuBLASCUDA Basic Linear Algebra Subprograms高性能线性代数库。
cuDNNCUDA Deep Neural Network library深度学习算子加速库。

3 框架与训练


术语英文全称一句话解释
Tensor-多维数组,AI 数据基本形态。
Shape-张量各维度大小。
DTypeData Type张量元素精度类型。
Stride-张量在内存中的步长布局信息。
Computational Graph-表示计算依赖关系的图结构。
DAGDirected Acyclic Graph有向无环图,便于拓扑执行。
Dynamic Graph-运行时构图,调试灵活。
Static Graph-先构图再执行,优化空间更大。
AutogradAutomatic Differentiation通过链式法则自动计算梯度。
ForwardForward Pass从输入到输出的正向计算。
BackwardBackward Pass从损失反向传播梯度。
Gradient-参数对损失的导数信息。
Optimizer-根据梯度更新参数的算法。
Mixed Precision-用低精度计算提升吞吐、节省显存。
Loss Scaling-对 loss 放缩以避免低精度梯度下溢。
DP/DDPData Parallel / Distributed Data Parallel多卡复制模型、切分数据并同步梯度。
TPTensor Parallel将单层张量运算切分到多卡。
PPPipeline Parallel将不同层分配到不同设备流水执行。
FSDPFully Sharded Data Parallel参数与状态全分片的数据并行策略。
ZeROZero Redundancy Optimizer降低并行训练冗余内存占用的技术。
NCCLNVIDIA Collective Communications LibraryGPU 高性能集合通信库。
All-Reduce-聚合并广播,常用于梯度同步。
All-Gather-汇聚各卡分片数据到每卡。
Reduce-Scatter-先归约再分发的通信原语。

4 推理与服务


术语英文全称一句话解释
Inference-使用训练好的模型进行预测/生成。
Latency-单次请求延迟。
Throughput-单位时间处理能力。
QPSQueries Per Second每秒请求数。
TTFTTime To First Token首 token 返回时间。
TPOTTime Per Output Token平均每个输出 token 的耗时。
P95/P99-95/99 分位延迟,衡量长尾性能。
ONNXOpen Neural Network Exchange跨框架模型表示与交换格式。
TensorRT-NVIDIA 推理优化与执行引擎。
vLLM-面向 LLM 的高吞吐推理服务框架。
ORTONNX RuntimeONNX 模型运行时与优化执行引擎。
Prefill-处理输入上下文的首轮计算阶段。
Decode-逐 token 生成阶段。
KV CacheKey-Value Cache缓存历史注意力状态以复用计算。
PagedAttention-分页管理 KV Cache 的注意力实现。
Continuous Batching-动态接入请求并持续拼批执行。
Prefix Cache-复用公共提示词前缀的缓存机制。
Speculative Decoding-小模型草拟、大模型校验的加速解码。
Quantization-用低比特表示参数/激活以提速降耗。
PTQPost-Training Quantization训练后量化,无需完整再训练。
QATQuantization-Aware Training训练中模拟量化误差以保精度。
INT8/INT4-8 位/4 位整型量化精度。
Pruning-删除冗余参数连接以压缩模型。
DistillationKnowledge Distillation大模型指导小模型训练。
CUDA Graph-录制并复用 GPU 执行图以降低启动开销。

作者:lizhongxuan
来源:juejin.cn/post/7608759940800708658
收起阅读 »

我的网站被黑了:一天灌入 227 万条垃圾数据,AI 写的代码差点让我社死

大家好,我是孟健。 上周六下午,我收到一封邮件。 标题很直白:"你的东西泄露了,兄弟"。 邮件里附了一个暗网论坛链接,声称我的网站 kirkify.net 的数据库已经被公开下载。 ▲ 就是这封邮件。发件人 punker,链接指向暗网论坛的数据库下载帖 我打...
继续阅读 »

大家好,我是孟健。


上周六下午,我收到一封邮件。


标题很直白:"你的东西泄露了,兄弟"


邮件里附了一个暗网论坛链接,声称我的网站 kirkify.net 的数据库已经被公开下载。


黑客发来的嘲讽邮件,包含数据库泄露下载链接


▲ 就是这封邮件。发件人 punker,链接指向暗网论坛的数据库下载帖


我打开后台一看——


case_studies 表:227 万条记录。


而前一天,这张表只有 259 条


从下午 3 点 25 分开始,持续了整整 6 个半小时,有人往我的数据库里灌了 227 万条垃圾数据


数据分布极其均匀:763K approved、763K pending、764K rejected。攻击者用 FloodUser_XXXXX 的命名模式批量写入,每条 caption 填充 1KB 的 base64 随机数据——这不是随机攻击,是有计划的数据库膨胀攻击。


墨码发现攻击数据后的应急响应


▲ 我的 AI Agent 墨码发现数据异常后的应急分析,第一时间定位到了漏洞




漏洞在哪?一个没有门锁的 API


kirkify.net 网站首页


▲ kirkify.net——我用 AI 编程工具做的出海小产品,用 Cursor + Claude 几天搭好就上线了


问题出在一个叫 submitCaseStudy 的 Next.js Server Action 上。


这段代码有 五个致命问题叠在一起


① Server Action 暴露为 HTTP POST 端点
Next.js 的 Server Action 虽然在服务端执行,但本质上就是一个 HTTP POST 接口。任何人都可以直接构造请求调用,完全不需要前端页面。


② 无认证(No Authentication)
函数体内没有任何 session / auth 验证。谁都能调,无门槛。


③ 无 Rate Limit
没有任何频率限制。攻击者写个循环脚本,每秒能提交几百条。


④ 自动审核通过(Auto-Approve)
代码里 status: 'approved' 硬编码,提交即上线。垃圾内容直接出现在 Gallery 页面,不需要任何审核。


⑤ 用 Service Role Key 绕过数据库安全
代码用 Supabase 的 service_role key 操作数据库,完全绕过了行级安全策略(RLS)。虽然数据库开了 RLS,但 service_role 拥有 God Mode 权限,安全策略形同虚设。


五个漏洞叠加的效果:你家大门敞开,没有保安,没有门禁,来者自动发 VIP 卡。


而这段代码,恰恰是 AI 帮我写的。


当初我对 Cursor 说:"帮我写一个提交案例的功能"。AI 很快给出了代码——功能完整、类型正确、能跑。但完全没有考虑安全性。


而我看到代码能跑,就直接部署了。




这不是个例


Veracode 2025 GenAI Code Security Report: AI 生成代码 45% 存在安全漏洞


▲ Veracode 2025 报告:测试 100+ 个 AI 模型,45% 的代码引入了安全漏洞


数据印证了这一点:



  • Veracode 报告:分析 100+ 个 LLM 生成的代码样本,45% 存在安全漏洞,引入了 OWASP Top 10 安全问题

  • Apiiro 研究:截至 2025 年 6 月,AI 生成代码每月引入 超过 10,000 个新安全问题,六个月增长 10 倍

  • 常见漏洞类型:缺乏认证、硬编码密钥、SQL 注入、缺少输入校验、未配置 Rate Limit


AI 很擅长写"能跑的代码",但它默认不会帮你加认证、加限速、配安全策略。


除非你明确要求它这么做。




修复过程:一天迁移整个数据库


发现问题后,我和 AI Agent 墨码立刻开始修复。


第一步:堵住入口



  • 关闭 submitCaseStudy 的公开访问

  • 给所有写入 API 加上 JWT 认证 + Rate Limit


第二步:全面安全审计



  • 审查所有 Server Action 的认证状态

  • 检查 Supabase RLS 配置——发现 case_studies 表虽然开了 RLS,但只有一条 SELECT 策略,没有 INSERT 策略。而代码用 service_role key 绕过了全部 RLS

  • 检查 billing_customers、credit_balances 等敏感表——确认未被攻击者访问,攻击者只能通过 Server Action 写入 case_studies 表


第三步:数据库迁移(关键决策)



  • 没有选择在原数据库上修补,而是直接把整个数据库从 Supabase 迁移到 Cloudflare D1

  • 迁移 12 张表,只导入 261 条干净数据(260 条 approved + 1 条 pending),227 万条垃圾数据直接抛弃

  • D1 版本完全重写了数据访问层,用 JWT 认证,不再有裸露的 Server Action

  • 同步完成 CF Workers 部署,DNS 切换上线


第四步:服务器加固



  • fail2ban 收紧(3 次失败封 24 小时)

  • VNC 绑定内网 IP

  • 环境变量文件权限改为 600

  • 泄露的 API Key 全部轮换


从发现到完成数据库迁移和上线切换,总共花了约一天。




给用 AI 编程的你:写完代码多问一句


这次教训让我养成了一个习惯:


每次 AI 帮我写完一个功能,我都会追加一句:


"这段代码有什么安全隐患?帮我加上认证、Rate Limit 和输入校验。"


就这一句话,能避免 80% 的安全问题。


再具体一点,每次上线前检查这 5 件事:


1. API 端点有没有裸奔? 不需要认证就能访问的写入 API = 等着被刷。特别注意 Next.js Server Action——它本质是 HTTP POST,不是"服务端函数"。


2. API Key 有没有硬编码在代码里? 检查 .env 文件权限,确保 Key 不在 Git 仓库里,更不要出现在聊天记录里。


3. 数据库有没有正确配置权限? 用 Supabase 就开 RLS + 写好策略。用 service_role key 要极其谨慎——它能绕过一切安全策略。


4. 有没有 Rate Limit? 没有限速的 API,就是一个等着被刷的 API。


5. 有没有监控异常? 数据量突增、请求量突增——这些信号需要有人盯着。




用 OpenClaw 的朋友,多留个心眼


最近 OpenClaw 火了(GitHub 21 万星),越来越多人在自己的服务器上跑 AI Agent。


OpenClaw 本身的安全性很好——沙箱隔离、权限分级、Token 加密存储,这些基础设施层面做得扎实。但你用 OpenClaw 搭建的应用和服务,安全性取决于你自己。


这次被攻击之后,我把自己服务器上的安全配置全过了一遍,总结了几条实用建议:


1. .env 文件权限改 600


你的 OpenClaw 配置文件和环境变量里存着所有 API Key。运行 chmod 600 ~/.openclaw/*.jsonchmod 600 .env,只允许当前用户读写。


2. 不要在聊天里发明文 Key


我这次就踩了这个坑——在 Telegram 里给 Agent 发了 Replicate API Key,结果被记录到了几十个文件里。正确做法是直接写进 .env 或用 openclaw config 设置。


3. 远程访问绑内网


如果你在服务器上跑 VNC、远程桌面或调试端口,一定绑到内网 IP 或 Tailscale 网络,不要暴露在公网。我之前 VNC 绑了 0.0.0.0,相当于全世界都能连。


4. 开 fail2ban


SSH 暴力破解是最常见的攻击手段。apt install fail2ban 一行命令,建议配置 3 次失败封 24 小时。


5. Agent 不要用 root 跑


给 OpenClaw 创建专用用户,限制文件系统访问范围。万一 Agent 被诱导执行恶意命令,损害范围可控。


6. 定期检查开放端口


运行 ss -tlnp 看看你的服务器对外暴露了哪些端口。数据库端口(5432、3306)、调试端口(9229、18800)绝对不应该对公网开放。


OpenClaw 给了你 10 倍的生产力,也意味着 10 倍的攻击面。Agent 能帮你写代码、调接口、操作数据库——如果权限没控好,攻击者也能通过 Agent 做这些事。




AI 编程让我们 10 倍速出活,但也让安全债务 10 倍速累积。


代码能跑,不代表代码安全。


希望你不用像我一样,等到收到黑客邮件那天才明白这个道理。


你用 AI 写代码时踩过安全的坑吗?评论区聊聊,我来回。


作者:孟健AI编程
来源:juejin.cn/post/7608953699230384168
收起阅读 »

年过完了,该上班了,我用Compose给大家放个烟花喜庆喜庆

web
今年没有买烟花,但是看了不少别人放的烟花,有些烟花真的是好看,现在年也过完了,该放的烟花也全都放了,差不多要上班了,那么我也趁这个时候,用Compose做个烟花,为新的一年来个开门红 分析烟花特征 做动效前脑子里要有个具体目标的,想好自己要的是什么效果,而不是...
继续阅读 »

今年没有买烟花,但是看了不少别人放的烟花,有些烟花真的是好看,现在年也过完了,该放的烟花也全都放了,差不多要上班了,那么我也趁这个时候,用Compose做个烟花,为新的一年来个开门红


分析烟花特征


做动效前脑子里要有个具体目标的,想好自己要的是什么效果,而不是随意去做,那么今天要做的烟花需要具备以下几点



  1. 数量不限:谁家买烟花也不会买个单响,高低也得多放几个才看的过瘾

  2. 扩散:烟花升空了肯定要炸开,不会炸的叫信号弹

  3. 路径是抛物线:炸开后虽然速度很快,但因重力影响,方向会有改变,会呈抛物线形态

  4. 逐渐消失:火药在空中烧没了,烟花也就越来越小,越来越淡了,最后消失了

  5. 颜色不同:好看点的烟花每次放出来颜色都不一样,酷炫一点的单个烟花里面颜色也不一样


好了就这么多了,下面来一个个去实现它


数量不限


要做到数量不限,我的实现方式是每次在界面上点击一次,该位置就生成一个烟花,为此这里需要定一个烟花的模型


image.png


这个Fire里面现在只有坐标点,接着我们需要做的是在界面上每点一次,就创建一个Fire并且塞到groups中,监听点击的事件就交给onPointerEvent函数


image.png


Fire的坐标是通过获取event.changes.first().position来拿到,表示当前点击的坐标,然后将groups里面的Fire绘制出来即可,暂时以红点表示


image.png


到这里,就实现了在界面上每次点击就生成一个红点的效果了


001-ezgif.com-video-to-gif-converter.gif

然后需要给Fire再添加个属性alpha表示透明值,用来降低每个生成后的红点的透明值,来达到淡化的效果


image.png

实现淡化就是不断降低透明度的过程,所以需要一个循环体,每一次降低的透明度用fadeSpeed来表示


image.png


上游用来改变每个Fire的属性,下游用来将改变后的数组赋值给groups来触发重组,淡化的效果就出来了


002-ezgif.com-video-to-gif-converter.gif

扩散


之前画了一个点,表示烟花上升后炸开前的位置,那么接下去就是实现炸开的效果,也就是将现有的点扩散出去,那么不得不扩充一下Fire的属性,新增一个数组表示每个扩散分支的终点


image.png


扩散分支endPoints里面每一个分支用SingleFire来表示,可以看到SingleFire内部暂时定了两个属性,offset表示终点坐标,radius表示与扩散中心点的距离,size代表每个分支烟花的粗细,默认设置了一个随机值,当点击的那一刻,endPoints也必须初始化出来,初始化的值就是以扩散中心为圆心,radius=0的圆周上的点,对了,还需要定义一个分支数目以及各个分支的角度


image.png


pointXpointY是计算圆周上坐标的函数,在之前写的动效文章里面出场次数还是蛮多的,翻旧代码找来了,那么endPoints的初始值也有了


image.png


endPoints赋值的地方就在LaunchedEffect里面,在改变透明值的同时,也增加radius的大小


image.png


一边增加扩散的半径,一边将分支给绘制出来,绘制的代码要改下,从drawCircle改成drawLine,drawLine的参数值我们都能拿得到


image.png


这样我们的扩散效果也基本完事了


003-ezgif.com-video-to-gif-converter.gif


抛物线路径


现在该想想怎么让烟花扩散的每条路径呈现抛物线的样式了,首先一点是肯定的,刚用过的drawLine肯定是不能用了,它画不了弯的线,那么谁可以画弯的呢,必须是drawPath,用它来画贝塞尔曲线真是太顺手了,咱先在SingleFire里面把用来绘制贝塞尔曲线用到的Path声明出来


image.png


在副作用LaunchedEffect函数中,需要新增创建Path的逻辑,其中第三个控制点的值暂时与第二个点一致,代码如下


image.png


最后在drawScope中,使用drawPath将需要的贝塞尔曲线绘制出来


image.png


呈现的效果如下


004-ezgif.com-video-to-gif-converter.gif

看起来效果没啥变化,没变化就对了,因为组成我们每根烟花分支的Path目前基本在一条直线上,自然绘制出来的贝塞尔曲线也是直线,只需要将第三个点改个位置就行,怎么改呢?我们知道所有第三个点也都在一个圆周上,所计算的角度与第二个点是一样的,只需要将第三个点的角度稍微增加或者减少一些,那么所有的点的位置就改变了,比如都加个30度


image.png


那么现在的样子就不一样了


005-ezgif.com-video-to-gif-converter.gif


好了,现在来做两个调整,一个是抛物线拐弯的弧度,应该也是个逐渐增大的过程,扩散的瞬间,其实弧度就是0,然后受重力逐渐变大,那么在SingleFire里面需要一个属性来表示抛物线弧度的属性


image.png


sweepDegree就表示当前抛物线的弧度,改变的过程其实可以参考透明度或者扩散半径那样做


image.png


另一个调整,这个烟花现在有点“顺拐”,现在抛物线拐弯的弧度都是按照顺时针来的,但实际场景中,烟花是朝四周扩散的,我们这个二维空间里面,也应该是有左右两个方向,所以不应该所有的弧度都是加上degree变量,其实想想也明白,真的要加上degree的是那些x坐标比初始值大的点,其他的都应该要减去degree才行,代码按照这个逻辑稍作调整


image.png


我们要的抛物线效果就做好了,效果看下


006-ezgif.com-video-to-gif-converter.gif

逐渐消失


烟花消失的样子也可以分成两部分,其中一个是炸开后从中心向外开始消失,所以我们做的烟花的起始位置就不能是一个点了,而是num个点,也在一个圆周上,这些点的起始半径也都为0,在SingleFire中新增一个startRadius属性代表这个半径


image.png


同样在副作用中,逐渐去变大这个startRadius,然后将最新值拿来计算最新圆周上的点,代码如下


image.png

另一个要处理的是烟花每一条曲线上,当开始消失的时候,不是整根曲线一起消失,而是会分成若干个小火星,然后小火星也慢慢变小变暗,要做到这一点就要用到一个关键型函数
PathEffect.dashPathEffect,用来将一根线变成一根虚线的,这个函数里面第一个参数就是个float数组,数组里面会传两个值,分别代表虚线每一个线段的长度以及虚线之间的间距


image.png


而我们要做的就是将float数组中第一个值不断减小,第二个值不断变大,SingleFire中再定义两个属性代表线段长以及间距长


image.png


副作用里面的代码做如下更改


image.png


然后在drawPath中添加上PathEffect.dashPathEffect的处理


image.png


烟花消失的部分也做好了,看下效果


007-ezgif.com-video-to-gif-converter.gif

不同颜色


到了最后一步了,给烟花上色,正常放烟花的时候,每次放出来的烟花颜色都会不一样,而且单个烟花中颜色也不是只有一种,所以这里打算这么做,先弄一个颜色的数组


image.png


然后在给烟花初始化的时候,每一次都从这个数组中随机抽三个颜色组成一个新的数组,我们烟花每一个分支的颜色,都从这个新的数组中随机找一个来设置,先给SingleFire再添加一个color属性


image.png


初始化部分,通过shuffled函数把colorList打乱,再抽取最后三个颜色


image.png


赋值部分,shuffList中随机拿一个颜色出来赋值


image.png


绘制部分,使用Brush生成一个渐变色,singleFire.color与白色的渐变,这样可以让烟花看起来亮一些


image.png


看下最终效果


008-ezgif.com-video-to-gif-converter.gif


最后


烟花放完了,又要开始重新搬砖当牛马了,那我就借这篇烟花动效的文章祝各位看官在新的一年工作顺利,事业有成,天天发大财~


作者:Coffeeee
来源:juejin.cn/post/7609288132602101760
收起阅读 »

写给年轻程序员的几点小建议

本人快 40 岁了。第一份工作是做网站编辑,那时候开始接触 jQuery,后来转做前端,一直做到现在。说实话,我对写程序谈不上特别热爱,所以技术水平一般。 年轻的时候如果做得不开心,就会直接 裸辞。不过每次裸辞的那段时间,我都会拼命学习,这对我的成长帮助其实很...
继续阅读 »

本人快 40 岁了。第一份工作是做网站编辑,那时候开始接触 jQuery,后来转做前端,一直做到现在。说实话,我对写程序谈不上特别热爱,所以技术水平一般。


年轻的时候如果做得不开心,就会直接 裸辞。不过每次裸辞的那段时间,我都会拼命学习,这对我的成长帮助其实很大。


下面给年轻人几点个人建议:



  • 不要被网上“35 岁就失业”的说法吓到。很多人是在贩卖焦虑。我都快 40 了还能拿到 offer,只是这些 offer 薪资不到 30K。

  • 基础真的很重要。我靠着基础吃香了十几年,在公司里也解决过不少疑难问题,深得领导器重。就算现在有 AI,你也要有能力判断它写得对不对,还要知道如何向 AI 提问。

  • 适不适合做程序员,其实几年之后就能看出来:你能不能当上 Leader,或者至少能不能独当一面。如果你觉得自己确实不太适合,可以趁早考虑转行,或者下班后发展一些副业。大千世界,行行出状元,能赚钱的行业很多,不必只盯着程序员这一条路。

  • 如果你觉得自己资质一般,但又真的喜欢写程序,那也没关系。《刻意练习》这本书里提到,一个人能不能成为行业顶尖,关键在于后天练习的方式,而不是天赋本身。

  • 程序员做到后面,最大的挑战其实是身体机能,而不是技术。一定要多锻炼身体。在还没有小孩之前,尽量把自己的技术水平拉到一个相对高的位置。结婚有家庭之后,学习时间会明显减少,而且年龄增长、抗压能力下降,而程序员本身又是高度用脑的职业。如果你的技术储备够高,就能在一定程度上缓冲项目压力,让自己工作更从容。

  • React、Vue、Angular 等框架都可以尝试做做项目。不同框架背后的设计思路,对思维成长很有帮助。前端很多理念本身就借鉴了后端的逻辑,多接触不同体系,会让你看问题更立体。

  • 可以在 GitHub 上做一些开源小项目。素材从哪里来?其实就来自你在公司做过的项目。把其中一块通用能力抽出来,沉淀成一个独立组件或工具库,再整理发布到 GitHub。与此同时,多写一些技术文章进行总结和输出。等到找工作时,简历里可以写上类似 "全网阅读量几万+" 这样的成果展示,这些都会成为你的加分项,让你在竞争中更有优势。

  • 35 岁以上,竞争力通常体现在两个方向:要么技术水平足够强,能够解决复杂问题;要么具备一定的管理能力,能够带团队。有人说那我以前就带过一两个徒弟,怎么办,那你得学会包装,你懂得,哈哈。

  • 35 岁以上,面试对技术广度要求更高,所以不要太深入挖掘某一项技术了。我以前认识一个领导,虽然写代码能力一般,在公司已经不写代码了,但是他的技术广度比较好,业务能力还行,虽然 40岁了 还能跳槽到比较好的广告公司,而不是靠人脉,不得不佩服。

  • 打工人比较麻烦的事就是 简历太"花"。频繁跳槽,在一个公司没干几个月就走,或者长期待业太久。如果岗位需要背调,简历造假会很麻烦,虽然有些小公司或外包公司不做背调。所以这方面简历自己要想想办法,你懂得。

  • 另外要认清一个现实:单纯打工,很难发财。 这件事越早想明白越好。多读一些关于认知、资产配置的书,弄清楚什么是资产,什么是消费。哪怕这些认知在你有生之年未必能带来巨大财富,也可以传递给下一代,让他们少走弯路。


以上只是个人经历和感受,不一定适用于所有人,但希望能给年轻的你一些参考。


作者:程序员大卫
来源:juejin.cn/post/7606155928197070900
收起阅读 »

分享前端项目常用的三个Skills--Vue、React 与 UI 核心 Skills

web
前言 在过去的几年里,前端开发经历了从手写每一行 HTML/CSS 或使用组件库(Ant Design, Element Plus),到 AI 辅助编程(GitHub Copilot, Cursor)的巨大跨越。然而,2025 年以来的技术浪潮正将我们推向一个...
继续阅读 »

前言


在过去的几年里,前端开发经历了从手写每一行 HTML/CSS 或使用组件库(Ant Design, Element Plus),到 AI 辅助编程(GitHub Copilot, Cursor)的巨大跨越。然而,2025 年以来的技术浪潮正将我们推向一个新的阶段:从 “AI 辅助” 向 “AI Agent(智能体)” 转型。


在这种模式下,开发者不再只是接收代码建议,而是为 AI 提供一系列“技能包(Skills)”,让 AI 能够理解复杂的框架逻辑、项目结构乃至视觉审美。


什么是Skills? 用一句话概括就是: Skills = 领域专业知识 + 你的项目偏好 + 严厉的审查官。


1. 为什么需要 Skills?


在 AI 编程的语境下,Skills 的出现是为了解决大模型(LLM)“博而不精”的天然缺陷。


1.1 通才 AI:知识的“图书馆”


一个没有安装 Skills 的大模型(如 GPT-5 或 Claude 4.5)就像一个读过全世界所有代码的毕业生



  • 广度惊人:它知道几百种编程语言,甚至能背出 2014 年的过时语法。

  • 深度断层:它不知道你公司内部的封装规范,不知道你的组件命名习惯,更不知道你项目中那个“不能动的老 Bug”。

  • 执行模糊:给它一个需求,它会从 100 种可能的解法中随机选一个给你,不管这是否符合你现在的工程环境。


1.2 专才 AI:自带“十年工龄”的骨干


安装了 Skills 的 AI(Agent),则完成了从“大模型”到**“领域智能体”**的进化:


安装了 Skills 之后, 它不再是那个满嘴跑火车的机器人,而是一个被你强制要求“必须按这套规约写代码”的资深开发。


特性通才 AI (Generalist)专才 AI (Specialist with Skills)
思维边界无边界,容易产生幻觉有界思维:严格锁定在当前技术栈内
项目理解盲人摸象,只看当前文件全局视野:理解路由、状态管理、样式体系
输出质量“大概能跑”的代码**“符合工程直觉”**的生产级代码
角色定位知识检索器虚拟技术负责人 (Virtual Lead)

2. Skills 里面到底装了什么


一个 Skills 文件夹里通常不是深奥的代码,而是三样东西:



  1. 规约(Rules) :一堆告诉 AI “准许做什么”和“严禁做什么”的指令(通常是 .cursorrules.md 文件)。

  2. 上下文(Context) :你项目的特殊结构。比如你的路由写在哪里,你的接口是怎么封装的。

  3. 工具(Tools) :一些自动化的小脚本。比如 AI 写完代码后,自动运行一下 npm lint 检查有没有写错。


如果说通才 AI 提供的是“概率” (它觉得这段代码看起来像对的),那么 Skills 提供的就是“确定性”。它把行业内顶尖架构师的经验,封装成了 AI 触手可及的“条件反射”。


本文将分享前端高频使用的三个核心技能包:vue-skillsreact-skills(agent-skills)以及 ui-skills(uipro-cli),剖析它们如何通过标准化的指令集,彻底改变我们的开发流。


Vue-Skills


Vue-Skills涵盖了 Vue 3 项目的最佳实践、TypeScript 类型安全增强、IDE 性能优化以及现代 Vue 编码模式。它们旨在解决大型 Vue 项目中常见的开发痛点,如 IDE 卡顿、类型推断错误、构建配置以及代码可维护性问题。


1.1 核心规则


可以将这些规则概括为以下 5 个主要方面:


1. 开发体验与 IDE 性能优化 (IDE & Performance)


关注如何解决大型项目中 VSCode 和 Volar 插件的性能问题。



  • codeactions-save-performance.md: 解决在大型 Vue 项目中保存文件时耗时过长(30秒+)的问题,建议禁用或限制耗时的 Code Actions。

  • volar-3-breaking-changes.md: 针对 Volar 升级带来的变更进行适配,确保插件功能正常。

  • vue-directive-comments.md: 介绍了@vue-ignore, @vue-skip,@vue-expect-error等指令注释,用于在模版中精细控制类型检查行为(类似 @ts-ignore)。


2. TypeScript 类型安全与配置 (Type Safety & Configuration)


致力于提升 Vue 模版和组件的类型检查严格度,减少运行时错误。



  • vue-tsc-strict-templates.md: 推荐开启 strictTemplates,在编译时捕获模版中未定义的组件和属性错误。

  • vue-router-typed-params.md: 解决unplugin-vue-router,导致的路由参数类型丢失问题(Property does not exist),推荐更严格的路由参数类型定义。

  • data-attributes-config.md: 配置 vueCompilerOptions 以支持 data-*属性的类型检查,避免误报类型错误。

  • ** module-resolution-bundler.md**: 推荐在 tsconfig.json 设置 "moduleResolution": "bundler"
    以适配现代构建工具。

  • with-defaults-union-types.md: 修复在defineProps中使用联合类型(如 string | false)配合withDefaults 时的虚假警告问题。


3. Vue 3 现代编码模式 (Modern Patterns)


推广 Vue 3.4+ 的新特性及更优雅的代码组织方式。



  • define-model-update-event.md: 推荐使用 Vue 3.4 的 defineModel()宏,替代手写的props+ emit('update:modelValue')模式。

  • extract-component-props.md: 建议将复杂的defineProps类型提取为独立的 TS 接口,提高代码可读性和复用性。

  • script-setup-jsdoc.md: 规范 <script setup>中的 JSDoc 使用,增强组件文档和类型提示。

  • fallthrough-attributes.md: 关于 inheritAttrs: false和透传属性(Attributes Fallthrough)的最佳实践。


4. 运行时陷阱与调试 (Runtime Caveats & Debugging)


解决特定场景下的怪异 bug 和运行时问题。



  • deep-watch-numeric.md: 警告在侦听(watch)数字数组时使用 deep: true的陷阱(新旧值相同),建议使用深拷贝。

  • duplicate-plugin-detection.md: 防止 Vue 插件(如 Pinia)在微前端或特定环境下被重复注册。

  • hmr-vue-ssr.md: 处理服务端渲染(SSR)场景下的热更新(HMR)问题。


5. 测试与样式 (Testing & Styling)



  • pinia-store-mocking.md: 关于如何在测试中正确 Mock Pinia Store 的指南。

  • strict-css-modules.md: 开启 CSS Modules 的严格模式,防止使用未定义的 class 名。


1.2 安装方法


通过简单的命令,你可以将此技能植入到你的 AI 工作流中:


npx add-skill hyf0/vue-skills

执行上面的指令后,会自动检查IDE,终端环境
image.png


选择安装在当前项目,还是对所有项目生效
image.png


选择下载一份每个项目建立软链接,还是将规则文件复制在每个项目下
image.png


安装完成之后的显示
image.png


1.3 实战案例


案例展示了当 AI Agent 加载了 vue-best-practices 技能包后,如何通过 “提取组件属性 (Extract Component Props)” 规则,优雅地解决二次封装组件时的类型继承问题。


场景描述:我们正在基于一个第三方库的 BaseButton.vue 组件,封装一个我们项目专用的 ProButton.vue。我们需要让 ProButton 继承 BaseButton 的所有属性(Props),同时增加一个自定义属性 loading


1. 优化前:常规“盲写”模式


现象:如果没有技能包指导,开发者往往会手动重复定义属性,或者使用不推荐的 Vue 内部实例类型。


<!-- ProButton.vue -->
<script setup lang="ts">
// ❌ 错误做法 1:手动重复定义,维护成本极高
interface Props {
text: string;
color?: string;
loading: boolean; // 自定义属性
}

// ❌ 错误做法 2:使用 InstanceType,会包含大量的 Vue 内部属性,干扰类型提示
import type BaseButton from "./BaseButton.vue";
type BaseProps = InstanceType<typeof BaseButton>["$props"];

defineProps<BaseProps & { loading: boolean }>();
</script>



2. 优化后:使用 vue-component-type-helpers


技能规则应用



  • 规则名称extract-component-props.md

  • 核心逻辑:利用 vue-component-type-helpers 库精确提取组件定义的 Props,排除内部干扰项。


重构结果:代码简洁,类型提示完美,且具备 100% 的继承安全性。


<!-- ProButton.vue -->
<script setup lang="ts">
import type { ComponentProps } from "vue-component-type-helpers";
import BaseButton from "./BaseButton.vue";

// ✅ 符合最佳实践:精确提取子组件的 Props 类型
type BaseButtonProps = ComponentProps<typeof BaseButton>;

// 扩展基础组件的属性
interface Props extends BaseButtonProps {
loading?: boolean;
size?: "sm" | "md" | "lg";
}

const props = withDefaults(defineProps<Props>(), {
loading: false,
size: "md",
});
</script>

<template>
<div class="pro-button">
<!-- 将剩余属性透传给基础组件 -->
<BaseButton v-bind="$attrs" :loading="loading">
<slot />
</BaseButton>
</div>
</template>

3.核心价值总结


维度传统方式Skills 方案 (Vue Best Practices)
开发效率需要翻阅源码查找子组件 Props自动提取,AI 自动完成类型桥接
类型提示混杂大量 $props 内部属性,极难看清纯净提示,仅显示业务定义的属性
维护性子组件增加 Prop 后,包装组件需手动同步自动同步,类型定义随子组件动态更新
代码洁癖充满大量的 Hack 或冗余定义标准工程化,符合 Vue 3 官方推荐模式


提示:安装技能包后,当你在写高阶组件(HOC)或二次封装组件时,AI 会自动识别场景并提示你使用 vue-component-type-helpers 进行类型提取,确保你的 TypeScript 链路在全项目保持强类型约束。



2.1 核心功能


1. React 组件组合模式 (vercel-composition-patterns)


适用场景:



  • 组件重构:当核心组件因布尔属性(Boolean props)爆炸(如 isLoading, isSmall 等)而难以维护时。

  • 库级开发:构建需要高度灵活、可扩展 API 的企业级 UI 组件库。

  • 复杂交互设计:实现具有强父子联动逻辑的复合组件(如 Tabs, Select, Menu)。


核心规则:



  • 架构优先组合:严禁无限制叠加布尔属性,推崇使用 复合组件 (Compound Components) 和 Context 共享状态。

  • 接口解耦:Provider 集中管理状态逻辑,子组件仅通过约定的 Context 接口进行交互。

  • 显式变体 (Explicit Variants):与其给一个组件加 10 个布尔值,不如创建明确命名的变体组件(如 PrimaryButton, IconButton)。

  • 组合优于配置:优先通过 children 进行 UI 拼装,而非通过庞大的配置对象或大量的 renderX Props。


2. React & Next.js 性能最佳实践 (vercel-react-best-practices)


适用场景:



  • 性能调优:页面响应慢、首屏渲染 (LCP) 时间长或存在明显的交互延迟。

  • 现代 Web 构建:基于 Next.js App Router 架构的全栈开发。

  • 大规模数据处理:需要并行获取多个 API 数据且必须避免渲染阻塞的场景。


核心规则:



  • 消除瀑布流 (Waterfalls):性能优化的 头等大事。强制要求并行化独立异步操作,并利用 Suspense 实现流式 (Streaming) 内容分发。

  • 打包体积压缩禁用 Barrel Files (单一入口导出文件) 以保护 Tree-shaking;强制使用 next/dynamic 进行代码分割。

  • 服务端性能 (RSC):利用 React.cache() 进行请求级数据去重,最小化传输至客户端的序列化数据量。

  • 重渲染控制:避免在 Effects 中处理同步状态,提倡使用派生状态 (Derived State);利用 startTransition 处理非紧急更新。


3. React Native & Expo 移动开发指南 (vercel-react-native-skills)


适用场景:



  • 移动端丝滑体验:针对 iOS 和 Android 优化长列表滑动和复杂手势动画。

  • 跨端性能消除:解决由于 JS 线程与 UI 线程通信延迟导致的性能瓶颈。

  • 原生功能集成:在 Expo 或原生环境中处理多媒体、字体和原生组件的高性能接入。


核心规则:



  • 列表性能 (最关键):强制使用 FlashList 替代 FlatList;列表项必须经过 memo 处理以减少多余重绘。

  • GPU 加速动画:动画属性仅限在 transformopacity 上操作,确保逻辑在 UI 线程直接执行。

  • 原生 UI 适配:始终使用 expo-image 优化图片加载;优先使用 Pressable 替代 TouchableOpacity 以获得更好的响应响应。

  • 渲染规范:文本必须且只能包裹在 Text 组件内;禁止在条件渲染中使用 &&(防止在移动端渲染出数字 0)。


4. Web 界面设计指南 (web-design-guidelines)


适用场景:



  • UI/UX 审计:项目发布前检查 UI 间距、颜色 Token 和视觉输出的一致性。

  • 无障碍性 (A11y):确保网站符合 Web 辅助功能标准,提升产品包容性。

  • 自动化 UI 审查:在 Code Review 阶段快速发现硬编码和非标准交互实现。


核心规则:



  • 动态合规检查:通过远程拉取最新的设计系统准则,确保证审视标准始终是最新的。

  • 无障碍强制约束:严格检查颜色对比度、ARIA 标签完整性以及键盘导航流程。

  • 高精度反馈:能够精确到代码行指出不符合设计系统规范(如未使用的 Design Tokens)的地方。


2.2 安装方法


npx add-skill vercel-labs/agent-skills

可以只选择安装其中的一个规则集,比如说vercel-react-best-practices
image.png


其余步骤,与Vue-Skills的问询问题一模一样,不再赘述。
image.png


image.png


image.png


image.png


2.3 用法示例


在一个 Next.js App Router 项目的个人中心页面中,我们需要同时获取用户信息、订单列表和优惠券信息。


1. 优化前:串行瀑布流 (Sequential Waterfall)


现象:如果没有技能包约束,AI 可能会写出标准的串行代码。这种方案下,总耗时是三个接口请求时间的累加(T1 + T2 + T3)。


// ❌ 不符合最佳实践:串行阻塞
export default async function ProfilePage() {
// 请求 1:获取用户信息
const user = await fetchUser();

// 请求 2:依赖于 user.id,但在请求 1 完成前无法开始
const orders = await fetchOrders(user.id);

// 请求 3:不依赖于前二者,却被白白阻塞
const coupons = await fetchCoupons();

return (
<div>
<UserInfo user={user} />
<OrderList orders={orders} />
<CouponList coupons={coupons} />
</div>

);
}



2. 优化后:并行获取 + 组件组合 (Parallel & Composition)


技能规则应用



  • async-parallel:识别出不互相关联的请求,并并行启动。

  • server-parallel-fetching:利用服务器组件的组合特性,减少主线程阻塞。


重构结果:页面总耗时缩短为(T1 + Max(T2, T3)),且实现了流式分发。


// ✅ 符合最佳实践:并行获取与解耦渲染
import { Suspense } from "react";

export default async function ProfilePage() {
// 1. 同时启动互不关联的异步任务,不加 await
const userPromise = fetchUser();
const couponsPromise = fetchCoupons(); // 并行开始

// 2. 仅等待必要的基础数据
const user = await userPromise;

return (
<div>
{/* 优先渲染用户信息 */}
<UserInfo user={user} />

{/* 3. 将耗时较长的“订单列表”逻辑下移至组件内部,并行获取 */}
<Suspense fallback={<Skeleton />}>
<OrderDataLayer userId={user.id} />
</Suspense>

{/* 4. 将预启动的“优惠券”Promise 传入组件 */}
<Suspense fallback={<Skeleton />}>
<CouponDataLayer promise={couponsPromise} />
</Suspense>
</div>

);
}

// 独立的异步数据层组件
async function OrderDataLayer({ userId }) {
const orders = await fetchOrders(userId); // 并行进行的请求
return <OrderList orders={orders} />;
}

async function CouponDataLayer({ promise }) {
const coupons = await promise; // 使用外部传入的 Promise
return <CouponList coupons={coupons} />;
}



3. 核心价值总结


优化点传统方案Skills 方案 (Vercel Best Practices)
请求速度累加耗时 (Waterfall)并发执行,耗时大幅度缩减
用户感知全黑屏等待,直到所有数据返回流式渲染 (Streaming),局部内容先出
代码结构逻辑逻辑堆在主页面,难以复用原子化组件,数据获取逻辑与渲染高度内聚
AI 表现随机生成,依赖运气确定性重构,严格执行 Vercel 性能规约


结论:通过注入 vercel-react-best-practices 技能,AI Agent 从一个简单的“代码生成器”转变为具备“性能自觉”的高级架构师。



三、 UI-Skills


如果说前两个工具解决了“逻辑”问题,那么 uipro-cli 及其关联的 ui-skills 则是为了解决“审美与交付”问题。 UI/UX Pro Max:赋能 AI Agent 的专业设计大脑。uipro-cli 是一个功能强大的命令行工具,专门用于为各种 AI 编程助手(如 Claude Code, Cursor, Windsurf, Antigravity 等)一键注入 UI/UX Pro Max 专家级技能。它让 AI 不仅能写代码,更能像资深设计师一样思考。


2.1 核心功能


1. 多元化视觉风格 (67 种 UI 风格)


UI/UX Pro Max 内置了 67 种最前沿的视觉设计风格,确保 AI 生成的界面告别“通用感”。



  • 现代趋势:支持 Glassmorphism(玻璃拟态)、Claymorphism(粘土拟态)、Minimalism(极简主义)。

  • 特色风格:包括 Brutalism(新野兽派)、Neumorphism(新拟物化)、Bento Grid(便当网格)以及针对 AI 产品的 AI-Native UI 风格。


2. 行业深度色彩与排版 (96 行业色板 + 57 字体配对)



  • 精准色板:提供 96 套针对特定行业(如 SaaS, 电商, 医疗, 金融金融, 美妆)优化的专业色板。

  • 字体艺术:内置 57 组精心挑选的字体组合,无缝集成 Google Fonts,从视觉底层提升产品质感。


3. 跨平台技术栈适配 (13 种主流技术栈)


支持从 Web 到移动端的 13 种主流技术架构,生成的代码即学即用。



  • Web 端:React, Next.js, Vue, Nuxt.js, Astro, Svelte, HTML+Tailwind, shadcn/ui。

  • 移动端:React Native, Flutter, SwiftUI, Jetpack Compose 等。


4. 专家级 UX 准则与设计推理 (100+ 准则与推理规则)


内置专业的设计逻辑,让 AI 具备“审美自觉”:



  • UX 指南:涵盖 99 条 UX 最佳实践、反模式规避和 A11y 无障碍规则。

  • 设计推理:拥有 100 条行业推理规则(例如:金融类应用严禁使用 AI 风格的紫粉渐变,以确保稳重感),自动进行交付前的 UI/UX 质量自检。


2.2 安装方法


通过 uipro-cli,你可以在几秒钟内完成技能初始化:


1. 全局安装工具


npm install -g uipro-cli

2. 为指定编辑器初始化技能


# 为 Claude Code 初始化
uipro init --ai claude

# 为 Cursor 初始化
uipro init --ai cursor

# 为所有支持的 AI 助手同时初始化
uipro init --ai all

3. 实战案例


场景描述: 用户需要为一个 AI 内容创作平台设计一个数据看板(Dashboard)。用户希望界面现代、直观,并且符合当前流行的审美趋势。


1. 传统 AI 生成:平庸且缺乏设计感


现象:如果没有加载专业 UI 技能包,AI 通常只会根据组件库(如 shadcn/ui)的基础示例,堆砌出缺乏灵魂的列表和卡片。色调单一(通常只用蓝色或灰阶),布局死板。


// ❌ 传统 AI 的“直觉”:基础的 Grid 布局,没有任何样式特色
export default function Dashboard() {
return (
<div className="grid grid-cols-3 gap-4 p-4">
<Card title="总收入" value="$12,000" />
<Card title="活跃用户" value="1,240" />
<Card title="API 调用" value="45.2k" />
<div className="col-span-3">主要统计图表</div>
</div>

);
}

2. UI/UX Pro Max 介入:具备“审美自觉”的设计


技能规则应用



  • 风格锁定:选择了 Bento Grid(便当网格) 布局。

  • 行业色板:套用了 SaaS 行业高阶调色盘

  • 设计推理:AI 会自动添加微交互动画、圆角分层(Glassmorphism 效果)以及符合产品气质的字体排版。


重构指令



“使用 UI/UX Pro Max 技能,为我的 AI 创作平台设计一个 Bento Grid 风格的统计看板。色调选用‘高级钛金灰’配合‘赛博蓝’点缀,并要求包含毛玻璃质感。”



重构结果(语义描述与关键代码片段)


// ✅ 符合设计的组件 (自动生成的 Tailwind 样式)

// 使用了 Bento Grid 非对称网格布局
export function BentoDashboard() {
return (
<section className="bg-slate-950 p-8 min-h-screen font-sans text-slate-100">
<div className="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-6">
{/* 高度分层的毛玻璃卡片 (Glassmorphism) */}
<div
className="md:col-span-2 md:row-span-2 bg-slate-900/50 backdrop-blur-xl border border-white/10 rounded-3xl p-8
hover:bg-slate-800/60 transition-all duration-300 group"

>

<header className="flex justify-between">
<h3 className="text-xl font-bold tracking-tight">创作产出分析</h3>
<div className="h-2 w-2 bg-cyan-400 rounded-full animate-pulse" />
</header>
{/* AI 自动推荐的图表配色与坐标轴样式 */}
<MainAnalyticChart color="#22d3ee" />
</div>

{/* 紧凑型统计卡片 */}
<div className="bg-slate-900/50 backdrop-blur-md rounded-3xl p-6 border border-white/5">
<Subtitle>API 余额</Subtitle>
<Value className="text-3xl font-mono">84.2%</Value>
</div>

{/* 动态微交互元素 */}
<div className="bg-cyan-500/10 rounded-3xl p-6 border border-cyan-500/20 text-cyan-400">
<div className="text-sm font-semibold mb-2">服务状态</div>
<div className="text-lg">系统运行正常</div>
</div>

{/* 更多 Bento 网格单元... */}
</div>
</section>

);
}

3.核心价值总结


维度传统构建UI/UX Pro Max (uipro)
布局逻辑简单的 1/2 或 1/3 等分非对称动态布局 (如 Bento Grid, Masonry)
色彩应用基础颜色(#3b82f6),视觉单调分层色彩系统,包含渐变、阴影与半透明层
细节打磨只有基础功能微交互、骨架屏、平滑过渡自动注入
设计自检需要人工多次反馈返工内置反模式自检,首次生成即达“可交付”标准

结语:前端开发者的角色转变


随着 vue-skillsreact-skillsui-skills 的普及,前端开发者的角色正在发生深刻变化。我们正在从 “代码编写者(Coder)” 转变为 “AI 指令师(Prompt Engineer)”“技术评审员(Reviewer)”


传统的 AI 辅助仅仅是“搜索”的变种,而 Skills 模式代表了 “领域知识的预装载”



  1. 降低认知负荷:你不需要记住 Vue 3 的所有新特性或 Tailwind 的上千个类名,Skills 充当了你的“外部脑”。

  2. 代码风格统一:团队只需约定一套 Skill 脚本,就能保证所有 AI 生成的代码风格高度一致,甚至比人类手动编写的更规范。

  3. 快速原型到生产:它极大地缩短了从 MVP(最小可行性产品)到正式发布的时间,让前端开发者更关注于“业务价值”而非“语法实现”。


掌握这些 Skills 并不意味着放弃底层的学习,相反,只有深刻理解 Vue/React 原理和 UI 规范的人,才能通过这些技能包更好地引导 AI,释放出前所未有的生产力。


作者:去伪存真
来源:juejin.cn/post/7599641289887055918
收起阅读 »

当你的Ant-Design成了你最大的技术债

web
大家好😁 如果你是一个前端,尤其是在B端(中后台)领域,Ant Design(antd)这个名字,你不可能没听过。 在过去的5年里,我们团队的所有新项目,技术选型里的第一行,永远是antd。它专业、开箱即用、文档齐全,拥有一切你想要的组件, 帮我们这些小团队,...
继续阅读 »

image.png


大家好😁


如果你是一个前端,尤其是在B端(中后台)领域,Ant Design(antd)这个名字,你不可能没听过。


在过去的5年里,我们团队的所有新项目,技术选型里的第一行,永远是antd。它专业、开箱即用、文档齐全,拥有一切你想要的组件, 帮我们这些小团队,一夜之间就拥有了大厂的专业门面。


我们靠它,快速地交付了一个又一个项目。



Antd 虽好,但魔改样式确实让人头秃,转向 Headless UI 又怕重复造轮子。想兼顾灵活定制与开发效率?试试 RollCode 低代码平台,利用 自定义组件 快速封装 Headless 逻辑,通过 私有化部署 沉淀企业级资产,更能一键 静态页面发布(SSG + SEO),让 UI 自由不再昂贵。



但是,从去年开始,我发现,这个曾经的经典,正在变成我们团队脖子上最重的枷锁。


Ant Design,这个我们当初用来解决技术债的核心组件库,现在,却成了我们最大的技术债本身😖。


这是一篇团队血泪史, 讲一讲感想🤷‍♂️。




我们为什么会爱上 AntD?


我们必须承认,从无到有阶段,antd是无敌的。


你一个3人的小团队,用上antd,做出来的东西,看起来和阿里几百人团队做的系统,没什么区别。


TableFormModalMenu... 你需要的一切,它都以一种极其标准的方式给你了。你不再需要自己造轮子。


当你发现@ant-design/pro-components时,一个ProTable,直接帮你搞定了请求、分页、查询表单、工具栏... 你甚至都不用写useState了。


在那个阶段,我们以为我们找到了大结局。




当个性化成为 我们的 KPI


美好可能是短暂的,从我们的产品经理和UI设计师开始👇:


能不能...不要长得这么 Ant Design?🤣


image.png


这是我们设计师,在评审会上,小心翼翼提出来的第一句话。


老板也说:我们要做自己的品牌,现在的系统,太千篇一律了!!!


于是,我们接到了第一个简单的需求:把全局的主题色,从橙色改成我们的品牌红。


这很简单,不就是 ConfigProvider嘛🤔。我们改了。


然后,第二个需求来了:这个Modal弹窗的关闭按钮,能不能不要放在右上角?我们要放在左下角,和确认按钮放在一起。(有点反人类🤷‍♂️)


灾难,就从这里开始了。


antdModal组件,根本就没提供这个插槽或prop。我们唯一的办法,是 强改


于是,我们的代码里,开始出现这种恶臭的CSS:


/* 一个高权重的全局CSS文件 */
.ant-modal-header {
/* ... */
}

/* 嘿,那个右上角的关闭按钮,给我藏起来! */
.ant-modal-close-x {
display: none !important;
}

为了把那个 X 藏起来,我们用了!important。我们亲手打开了潘多拉魔盒。


这个表格的筛选图标,能换成我们自己画的吗?😖


antdTable,是一个重灾区。它太强大了,也很黑盒。


我们设计师,重新画了一套筛选、排序的图标。但我们发现,antdTable组件,根本没想过让你换这个。


我们唯一的办法,就是用 CSS选择器,一层一层地穿进antd的DOM结构里,找到那个<span>,然后用background-image去盖掉它。


/* 另一个人写的,更恶臭的CSS */
.ant-table-thead > tr > th.ant-table-column-has-filters .ant-table-filter-trigger {
/* 妈呀,这是啥? */
background: url('our-own-icon.svg') !important;
}

.ant-table-thead > tr > th.ant-table-column-has-filters .ant-table-filter-trigger > svg {
/* 藏起来,藏起来! */
display: none !important;
}

我们被拖累了。


我们花在 覆盖antd默认样式上的时间,已经远远超过了我们自己写一个组件的时间。




压死骆驼的最后一根稻草


image.png


我们用了ProTable,它的查询表单和表格是强耦合的。当产品经理提出一个我希望查询表单,在页面滚动时,吸附在顶部的需求时... 我们发现,我们改不动。我们被ProComponents的黑盒,锁死了。


然后我们的vendor.js打包出来,2.5MB。用webpack-bundle-analyzer一看,antd@ant-design/icons,占了1.2MB。我们为了一个ButtonIcon,引入了一个全家桶。antd的按需加载?别闹了,在ProComponents面前,它几乎是全量的。


而且 antdv3v4,我们花了一个月。从v4v5,我们花了半个月。每一次升级,都是一次大型重构,因为我们那些写法一样被CSS覆盖,在新版里,全失效了🤷‍♂️。


我们本想找一个可靠的组件库,这么久过来,结果它成了债主。




我们真正需要的可能是轮子


我终于想明白了。


Ant Design,它不是一个组件库(Library),它是一个UI框架(Framework)。它是一套解决方案,它有它自己强势的 设计价值观


当你的需求,和它的价值观一致时,它就是圣经。
当你的需求,和它的价值观不一致时,它就变成枷锁。


我们当初要的,其实是一个带样式的Button;而antd给我的,是一个内置了loadingdisabledonClick时会有水波纹动画、并且必须是蓝色或白色的Button




我们的自救之路


在我们新的项目中,我忍痛做出了一个决定🤷‍♂️:


原则上,不再使用antd


我们新的技术栈,转向了:
Tailwind CSS + Headless UI 方案(比如Radix UI


image.png


这个组合,才是我们想要的:



  • Headless UI:它只提供功能无障碍。比如,一个Dialog(模态框),它帮我搞定了按Esc关闭、焦点管理。但它没有任何样式

  • Tailwind CSS:我拿到了这个无样式的Dialog,然后用Tailwindclass,在5分钟内,在AI的帮助下,把它拼成了我们设计师想要的、独一无二的弹窗。


我们拿回了CSS的完全控制权,同时又享受了 AI + 组件开发的便利。


我依然尊敬Ant Design,它在前端B端历史上,是个丰碑。
对于那些从0到1的、对UI没有要求的内部系统,我可能依然会用它。


但对于那些需要品牌、体验、个性化的核心产品,我必须和它说再见了。


Suggestion.gif


因为,当你的组件库开始控制你的设计和性能时,它就不是你的资产了。


而变成你最大的技术债🙌。


作者:ErpanOmer
来源:juejin.cn/post/7571176484515659828
收起阅读 »

H5性能优化-打开效率提升了62%

web
一、达成的结果 app嵌套h5 加载效率提升了62%。时间从平均2.259s 降到了0.852s 二、优化过程 思路:优化原生webview+h5 ,先测试webview点击到创建时间 120ms(无需优化)。webview 提供了 api 测试 加载url ...
继续阅读 »

一、达成的结果


app嵌套h5 加载效率提升了62%。时间从平均2.259s 降到了0.852s


二、优化过程


思路:优化原生webview+h5 ,先测试webview点击到创建时间 120ms(无需优化)。webview 提供了 api 测试 加载url 的进度。但是没提供具体的类似pc端网络的工具。所以就通过ai搜索一些工具。发现阿里云的arms 方便引入。


测试设备 android荣耀50、华为meta9、华为p40、oppo


开发环境测试


测试样本:优化前后各测试10次。


测试移动端h5 引入了阿里云的arms arms.console.aliyun.com/ 可以查看加载某个url的时候 请求的js css、图片、网络请求 跟网页版的网络类似。


import ArmsRum from '@arms/rum-browser'

ArmsRum.init({
pid: 'ha63j3v892@efe71d242023cd5',
endpoint: 'https://ha63j3v892-default-cn.rum.aliyuncs.com',
// 设置环境信息,参考值:'prod' | 'gray' | 'pre' | 'daily' | 'local'
env: 'prod',
// 设置路由模式, 参考值:'history' | 'hash'
spaMode: 'hash',
collectors: {
// 页面性能指标监听开关,默认开启
perf: true,
// WebVitals指标监听开关,默认开启
webVitals: true,
// Ajax监听开关,默认开启
api: true,
// 静态资源开关,默认开启
staticResource: true,
// JS错误监听开关,默认开启
jsError: true,
// 控制台错误监听开关,默认开启
consoleError: true,
// 用户行为监听开关,默认开启
action: true,
},
// 链路追踪配置开关,默认关闭
tracing: false,
})

export default ArmsRum

步骤一、懒加载echarts


以前的代码


import Api from '@/api'
import * as echarts from 'echarts'
import dayjs from 'dayjs'
export default {
}

现在的代码


async getRepairChart() {
const echarts = await import('echarts')
// xxx
const chartDom = document.getElementById('repairChart')
const myChart = echarts.init(chartDom)
const option = {
}
myChart.setOption(option)
},

通过F12查看网络 发现加载列表的时候有出现echartsxxx.js 而且有800多k ,所以就考虑加载列表不应该去加载数据看板的数据。


优化前,测试前日志打印



均是3.234s 然后 当然还测试了华为p40 、oppo机器 由于系统不一样时间有些差别


测试后日志打印



平均是1.71s




华为meta9 9年前的老手机打印log



平均也2s多


优化后



不到0.8s


步骤二、禁用预加载、路由懒加载


通过查看网络发现请求dev-haolipei.cias.cn/app/#/takeO… 超级多的js 跟css


一张图都截取不完。当时就感觉肯定请求了很多无关紧要的资源。



npm run build 执行后查看dist目录index.html 的确很触目惊心 加载了太多没必要的资源了。



module.exports = {
publicPath,
devServer: {
disableHostCheck: true,
// host: 'localhost.cias.cn',
proxy: {
'/api': {
target,
changeOrigin: true,
// cookieDomainRewrite在手机调试时用得上
// cookieDomainRewrite: '',
pathRewrite: {
'^/api': '',
},
},
'/media': {
target,
changeOrigin: true,
pathRewrite: {
'^/media': '',
},
},
},
},
chainWebpack: config => {
// 禁用预加载
config.plugins.delete('preload')
config.plugins.delete('prefetch')
},
}

禁用它们的核心好处

1. 减少不必要的网络请求,节省带宽


  • preload 可能会强制加载一些 “非关键资源”(如配置不当的情况下,预加载了体积大但当前页面暂时用不到的资源),导致带宽浪费。

  • prefetch 会预加载未来可能访问的路由 chunk(如用户可能不会点击的低频页面),如果用户最终没有访问这些页面,预加载的资源就成了 “无效请求”,尤其对移动端用户(流量有限)不友好。


禁用后,资源仅在明确需要时才会加载(如用户进入对应路由时),避免 “提前加载但用不上” 的浪费。


2. 避免阻塞关键资源加载,提升首屏速度


  • 浏览器对同一域名的并发请求数有限制(HTTP/1.1 通常为 6 个)。preload 加载的资源会占用并发名额,可能阻塞当前页面真正需要的关键资源(如核心 JS/CSS),导致首屏渲染延迟。

  • 例如:若 preload 预加载了一个 2MB 的非关键 chunk,可能会挤占首屏 JS 的加载带宽,导致页面 “白屏时间” 变长。


禁用后,浏览器的并发资源会优先分配给当前页面的核心资源,减少阻塞。


3. 避免缓存资源被 “无效资源” 占用

浏览器缓存空间有限,prefetch 预加载的大量 “未来可能用到” 的 chunk 会占用缓存空间,可能导致真正需要长期缓存的核心资源(如 chunk-vendors.js)被挤出缓存,下次访问时需要重新加载。


禁用后,缓存可优先保留关键资源,提升二次访问速度。


4. 适配低网速 / 弱网环境

在 3G、偏远地区等弱网环境下,preload/prefetch 的预加载行为会加剧网络拥堵:



  • 预加载的资源可能耗时过长,导致当前页面的核心资源加载超时。

  • 禁用后,资源加载更 “轻量化”,优先保证当前页面可用,符合弱网环境的用户体验需求。


5. 减少开发环境的冗余加载

在开发环境中,Webpack 会频繁编译资源,preload/prefetch 可能导致每次热更新时加载大量无关资源,拖慢开发服务器响应速度,影响开发体验。禁用后可简化开发环境的资源加载逻辑,提升热更新效率。


注意:并非所有场景都适合禁用

preload/prefetch 本身是性能优化手段,若项目存在以下情况,可能需要保留或部分配置:



  • 首屏依赖的关键资源(如核心 CSS、字体)体积大,preload 可加速其加载。

  • 高频访问的路由(如首页→列表页),prefetch 可提前加载列表页 chunk,提升跳转速度。


因此,禁用的合理性取决于项目场景:资源体积大、用户网络不稳定、低频路由多的项目(如移动端 H5)更适合禁用;高频路由明确、网络环境好的项目可选择性保留


路由懒加载

必须写上 /* webpackChunkName: "system-setting" */


以前的代码


	{
path: '/takeOrder',
name: 'HomeTakeOrder',
component: () =>
import(
'@/views/baosi/orderList.vue'
),
meta: {
title: '推返修列表',
keepAlive: true,
},
}

懒加载路由模式


const HomeTakeOrder = () =>
import(/* webpackChunkName: "take-order" */ '@/views/baosi/orderList.vue')

{
path: '/takeOrder',
name: 'HomeTakeOrder',
component: HomeTakeOrder,
meta: {
title: '推返修列表',
keepAlive: true,
},
},

这样的好处 打包后 会有路由名称



对比



继续测试打印log



可以看看网络请求 就只有5个js文件 总体积不到300k



平均是0.852,基本达成要求


通过npm run build 本地打包看看文件对比大小。


1、体积巨减!!!


2、加载的文件名称是路由名称+hash值


优化前



优化后



三、内部项目具体实践


(增值移动端h5项目可以参考以下操作)


1、禁用预加载


module.exports = {
publicPath,
devServer: {
disableHostCheck: true,
// host: 'localhost.cias.cn',
proxy: {
'/api': {
target,
changeOrigin: true,
// cookieDomainRewrite在手机调试时用得上
// cookieDomainRewrite: '',
pathRewrite: {
'^/api': '',
},
},
'/media': {
target,
changeOrigin: true,
pathRewrite: {
'^/media': '',
},
},
},
},
chainWebpack: config => {
// 禁用预加载
config.plugins.delete('preload')
config.plugins.delete('prefetch')
},
}

2、将路由全部改成懒加载


并且路由需要/* webpackChunkName:"take-order" */


	{
path: '/takeOrder',
name: 'HomeTakeOrder',
component: () =>
import(
/* webpackChunkName:"take-order" */ '@/views/baosi/orderList.vue'
),
meta: {
title: '推返修列表',
keepAlive: true,
},
}

3、组件懒加载


以前常见写法 就是一进来 把所有组件的都加载进来


比如这个车牌号组件有120k 对于比较大一点的组件可以用懒加载 。


	<van-popup v-model="showPlatePopup" position="bottom" :overlay="false">
<keyboard
v-model="baseInfo.plateNumber"
:show.sync="showPlatePopup"
@input="handlePlateInput"
></keyboard>


import Keyboard from '@/components/numberplate/vnp-keyboard.vue'

export default {
name: 'CreateOrder',
components: {
MultiSelectPopup,
DispatchingPopup,
DispatchFailPopup,
DispatchFinishPopup,
Keyboard,
},

}

懒加载代码


用这个方式可以直接在网络中查看到对应组件的名称和大小


<van-popup
v-if="showPlatePopup"
v-model="showPlatePopup"
position="bottom"
:overlay="false"
>
<keyboard
v-model="baseInfo.plateNumber"
:show.sync="showPlatePopup"
@input="handlePlateInput"
></keyboard>
</van-popup>

export default {
name: 'CreateOrder',
components: {
MultiSelectPopup,
DispatchingPopup,
DispatchFailPopup,
DispatchFinishPopup,
Keyboard: () =>
import(
/* webpackChunkName: "keyboard" */
'@/components/numberplate/vnp-keyboard.vue'
),
},
}

还有 特别大的第三方库 echart 懒加载 按需引入


四、总结


目前通过禁用预加载、路由懒加载、和第三方组件懒加载使用方式 基本能达到很大程度的优化效果。


但是看图 每次加载前面2个文件一个683k 另外一个285k 还是压缩了的文件 ,


我猜应该是main.js 引入了很多东西,后续还可以有优化空间。移动端的h5 入口文件尽量简洁。



(自己写的页面 一定多注意看一下 网络 有多少个js 和css 图片) 资源过大就要考虑优化了。


性能优化一定要多测试验证、多测试验证、多测试验证,保证业务正常情况下优化性能。


性能优化参考。


juejin.cn/post/718889…


作者:德哥0904
来源:juejin.cn/post/7572301616168583177
收起阅读 »

我创建了一个全 AI 员工的一人公司

大家好,我是 Sunday。 现在,用 AI 写代码,已经不算什么新鲜事了。写个组件、补个函数、改个 Bug,几句话交给模型就能搞定。 但是,大家有没有想过一个问题:如果我不只是用一个 AI,而是用一群 AI,会发生什么? 我们现在用 AI,本质上只是把它当成...
继续阅读 »

大家好,我是 Sunday。


现在,用 AI 写代码,已经不算什么新鲜事了。写个组件、补个函数、改个 Bug,几句话交给模型就能搞定。


但是,大家有没有想过一个问题:如果我不只是用一个 AI,而是用一群 AI,会发生什么?


我们现在用 AI,本质上只是把它当成“高级工具”,即:我遇到问题 → 我问 AI → 它给我答案。


那么如果我们把脑洞放大一点呢?


既然我可以调用一个 AI,那是不是也可以 同时调用多个 AI ?既然 AI 可以写代码,那它是不是也可以完成:产品经理、设计师、测试、运维,甚至是 财务、人事 的工作?


换句话说: 我能不能,用一群 AI,组建一家“只有我一个人类员工”的公司?


多个 AI,各自承担不同角色:




  • 产品负责拆需求:告诉产品我的需求,然后产品帮我把需求拆解成可以具体执行的步骤

  • 开发负责编码:把具体的步骤告诉开发,开发负责把整个功能落地

  • 测试负责验收:开发的代码,交给测试进行验收。验收失败则重新打回开发,开发负责修改 BUG

  • 运维负责部署:最终完成的项目,运维负责部署上线,构建整个自动化部署的流程

  • 财务负责算账:每天的 token 支出 和 营收,并给出更省钱的财务方案

  • 人事负责管理:哪个 AI 员工“不合格”,人事负责把它“开掉”,并根据我的需求重新“招聘(生成)”新的 AI 员工


并且,我还希望它们之间可以独立思考,互相沟通,而我只负责:下目标、做决策。


如果这件事真的可行,那意味着什么?意味着一个人,或许可能真的可以撬动一整家公司级别的生产力。


说干就干。


AI 选择


市面上的 AI 模型已经非常多了:Claude、GPT Coder、Gemini、GLM、DeepSeek。。。有的贵,有的便宜,有的擅长推理,有的擅长编码。


最理想的状态其实很明确:



  • 贵的 AI,承担复杂任务:产品设计、系统架构、核心编码

  • 便宜的 AI,承担简单任务:统计、对账、流程、输出财务报表


只要这些 AI 之间可以互相沟通,以上这些都不是问题。


但是,Sunday 在经过一系列的测试之后,发现了一个很残酷的事,就是 不同模型的 AI 之间完全无法沟通。特别是不同厂商的 AI,每一个都被作为了独立的个体。虽然可以通过“协调者”方式强行拼接,但是实现复杂,并且效果也不理想。


额。。。好像这个一人公司的梦想就直接破灭了...


不行,不能那么容易放弃。


所以,在第一阶段,我只能选择一个更“粗暴但稳定”的方案:全部使用 Claude。


因为 Claude 提供了一个非常关键的工具:Claude Code:一个运行在纯终端里的 AI 编码智能体。我们只需要打开终端就可以直接通过语言对话的方式调用 AI 模型。



而接下来,我要做的事情是:用 Claude Code,一步步搭出一家“AI 员工公司”的雏形。


启动多个 AI 终端


一个终端窗口作为一个员工,如果我们想要多个员工那么就只需要启动多个终端就可以。然后我们要给他们赋予角色。一开始,我们可以先从最小可行方案开始:



我们先制定两个角色,他们分别是:



  • 张三:产品经理

  • 李四:程序员


然后,就出现了 大型翻车现场....



在我尝试给 Claude 提示词,让他变成 张三 的时候。Claude 给我的回复是:我是 Claude,我不是张三....


不是,咱说好的玩角色扮演呢...你就这么不配合的吗?


因为在我的设想里,这一步应该是最简单、最“理所当然”的:起个名字、设个背景、分配个角色。然后 AI 就会自动进入对应的工作模式。


结果现实就是这么现实:“我能帮你完成工作,但是我就不承认我是张三,我就是 Claude


这不是能力问题,这赤裸裸的就是个 态度问题 啊! 看来 AI 也不想当牛马啊。。。


没办法,我只能尝试修改下提示词:



好的,产品经理已就位。然后我们可以从一个小的 todolist 的需求开始:



现在我们已经有了一个 todolist 的需求文档了。接下来最好的方案就是 产品经理的 AI 可以直接通知 程序员 AI,完成代码实现。


但是,可惜 不同窗口之间 AI 无法直接通讯。所以,我就必须要承担起这个通讯员的角色。



最终实现的效果如下:



整个功能完善、可用。并且还提供了 主题变化 的功能。。。


反思


可是如果我们仔细去分析上面整个流程,我们可以发现:这个流程远没有我们想象的那么顺畅。甚至有点 多此一举


因为以上这些需求完全可以在一个 Claude code 中完成,没有必要进行这样的划分。甚至可以说:在任务简单时,多 AI 协作不仅没有提升效率,反而增加了沟通成本。


而这样的一种调度+协作的方式,如果任务变得复杂了,恐怕 AI 没有乱呢,我们自己就已经先手忙脚乱了。


这让我开始怀疑:最初设想的这种全 AI 员工的方式,会不会从一开始,就是错的?


但是,当我仔细思考过之后,我发现问题不在于“多角色”这个想法本身,而在于:当前这种“多终端 + 人工调度”的协作方式,本质上是一个非常低效的协作模型。


如果我们换一个角度,从“协作工具”入手,情况可能会完全不同。


这时,我注意到了一个开源项目:vibekanban



它提供了一种非常典型的多人协作看板模式,和我们熟悉的 Teambition、Jira、Trello 几乎一致,包含了:任务可视化、状态流转清晰、每个角色只关注自己的列


teambition 的多人协作看板


vibekanban 的协作看板


通过 vibekanban 这种工具,我们至少可以做到:多任务并行更清晰、角色边界更稳定、协作过程可追踪、可回溯。


但即便如此,我很快又意识到一个更现实的问题:这,依然离“全 AI 员工的一人公司”,依旧差得很远。


全 AI 员工的难点在哪里?


根据 Sunday 的实验,其实我们可以发现,全 AI 员工的最大难点就是 如何高效的完成 AI 之间的自动协作


更具体一点说,核心难点只有一个:如何让多个 AI,在几乎没有人工干预的情况下,自动完成“上下文传递 + 状态对齐 + 任务接力”?


那么这个功能可以实现吗?


答案是 绝对可以


学习过咱们的 商业级 AI 课程 的同学都知道,在 AI 的持续对话中,我们可以通过记录上下文的方式完成跨角色的连续对话


那么同样的道理,如果我们可以记录不同 AI 沟通上下文的 关键内容,然后通过上述的同样逻辑呼叫下一个 AI(让代码通过指令自动传输关键上下文,并调起 AI 服务),那么全 AI 员工的功能就可以实现。只不过在这样的完美的流转过程中,我们还需要做很多的努力才可以。


总结


这一通折腾下来,收获还是很明显的。三个关键:



  1. 单模型,已经足够强

  2. 多模型,真正难的是系统设计

  3. “全 AI 员工”,本质是一个 调度系统 + 状态系统 + 协作协议 的问题


而这,也意味着:下一阶段的探索重点,已经不再是“哪个模型更强”,而是:如何设计一套真正可自动运行的“AI 组织系统”。


作者:程序员Sunday
来源:juejin.cn/post/7597355509747974170
收起阅读 »

全栈开发者的谎言:什么都会 = 什么都不精?

上周面了一个自称5年全栈的兄弟🤔。 简历漂亮得像报菜名:精通 Vue/React,熟悉 Node.js/Go,玩过 K8s,能画原型图,甚至还写过两个 Flutter App。 我只问了一个问题:如果不使用任何框架,Node.js 的 HTTP 模块是如何处理...
继续阅读 »

Full-stack-web-developer.png


上周面了一个自称5年全栈的兄弟🤔。


简历漂亮得像报菜名:精通 Vue/React,熟悉 Node.js/Go,玩过 K8s,能画原型图,甚至还写过两个 Flutter App。
我只问了一个问题:如果不使用任何框架,Node.js 的 HTTP 模块是如何处理高并发下的内存积压的?


他愣了三秒,支支吾吾说:厄...一般我们都用 NestJS,框架处理好了吧?😖


那一刻,我看到了无数前端人的缩影:我们拼命想成为无所不能的全栈大神,最后却活成了什么都懂一点、什么都搞不定的API 缝合怪。



全栈不等于样样稀松,真正的价值在于深耕核心难题。与其在重复造轮子中消耗精力,不如用 RollCode 低代码平台 提效。它支持 私有化部署自定义组件,搞定 静态页面发布(SSG + SEO),让开发回归技术本质。





全栈的本质


你要知道,全栈工程师(Full Stack Engineer)这个词,最开始是谁捧红的?
是硅谷的创业公司。


为什么?因为没钱
他们招不起一个前端专家 + 一个后端专家 + 一个运维专家。他们需要一个性价比极高的耗材,一个人把这三个坑都填了。


于是,招聘 JD 画风突变:



25K,招全栈。要求精通 React、Node.js、MySQL、Docker、AWS...



你看似拿了比纯前端高 20% 的工资,干的却是 3 个人的活。你的大脑需要在 CSS 的 z-index 和 MySQL 的 Transaction Isolation Level 之间疯狂切换。


结果是什么?
你的认知被彻底击穿。


你以为你的认知,什么场景都能用。
但在真正的技术攻坚战里,什么都不是。🥱




所谓的全栈,大多是全沾


我见过太多这种虚假全栈的代码了,简直是灾难现场。


他们写后端,思维还是前端那一套:



  • 数据库设计:没有范式概念,一张表 50 个字段,全是 JSON 字符串。

  • 错误处理try-catch 包住整个 API,报错全返 200 OK,msg 里写 bug。

  • 并发安全:在 for 循环里 await 查库,完全不懂什么是连接池耗尽。


让我们看一段典型的前端思维写后端的死代码:


// 典型的假全栈代码
// 以为用了 async/await 就是后端大神了
router.post('/buy', async (req, res) => {
// 1. 先查库存(没有锁,并发一来直接超卖)
const stock = await db.query(`SELECT count FROM products WHERE id=${req.body.id}`);

if (stock > 0) {
// 2. 扣库存(中间如果服务挂了,数据不一致)
await db.query(`UPDATE products SET count = count - 1 WHERE id=${req.body.id}`);
// 3. 创建订单
await db.query(`INSERT INTO orders ...`);
return res.json({ success: true });
}
});


这种代码,稍微有点后端经验的人看了都会心肌梗塞。但在全栈眼里:跑通了啊,没报错啊!


什么都会 = 什么都不精。
你以为你拓宽了广度,其实你牺牲了深度。在裁员潮来临时,公司是会留一个能解决复杂内存泄漏的 Node 专家,还是留一个既能写页面又能写增删改查,但稍微上点量就崩服务的万金油


在我们国内,答案是极其残酷的。




T 型人才的骗局


很多人反驳:我要做 T 型人才,一专多能!


理想很丰满,现实是绝大多数人做成了 一型人才 —— 横向铺得无限开,纵向深度为零。



  • 学了 Docker,只会 docker run,不懂 Cgroup 原理。

  • 学了 React,只会 useEffect,不懂 Fiber 调度。

  • 学了 Rust,只会写 Hello World,借用检查器都过不去。

  • 学了 SQLite, 只会增删改查,不懂什么叫锁,什么叫性能优化


这种简历驱动型学习产生的知识,极其脆弱。
一旦遇到深水区的 Bug,你的全栈光环瞬间破碎,只能去 AI Chat 复制粘贴,然后祈祷奇迹发生。


真正的全栈
是你能从前端的一个点击事件(Click),一路追踪到内核的系统调用(Syscall),这中间的每一层你都可控
如果你做不到,那你充其量只是一个全栈水货。😥




请你先成为单栈战神


人的精力是有限的。在 35 岁危机到来之前,请功利一点,聚焦一点。


如果你是前端:
别急着去学 Go,别急着去搞 K8s。
先把浏览器渲染原理吃透,把 V8 垃圾回收搞懂,把图形学(WebGL/Canvas)啃下来。
当你在一个领域钻得足够深,深到能解决 99% 人解决不了的问题时,你才有资格去谈横向扩展。


这时候的扩展,不是为了凑简历,而是为了解决问题。



  • 学 Node.js,是因为前端构建工具跑得太慢,你需要深入 OS 层优化 I/O。

  • 学 Rust,是因为 JS 在计算密集型任务上拉胯,你需要 WASM 来救场。


这才是全栈的正确打开方式:降维打击。




别再用全栈来标榜自己了。
在这个分工日益精细化的时代,专家永远比杂家值钱。


专注你的赛道,把它做到极致。
那才是你不可被替代的根本。


大家怎么看🤔


Suggestion (3).gif


作者:ErpanOmer
来源:juejin.cn/post/7601811815828406323
收起阅读 »

为什么程序员不自己开发一个小程序赚钱

大家好,我是凌览。 个人网站:blog.code24.top 去水印下载鸭:nologo.code24.top 如果本文能给你提供启发或帮助,欢迎动动小手指,一键三连(点赞、评论、转发),给我一些支持和鼓励谢谢。 刷到一个挺扎心的话题:程序员为什么不自己...
继续阅读 »

大家好,我是凌览。



如果本文能给你提供启发或帮助,欢迎动动小手指,一键三连(点赞评论转发),给我一些支持和鼓励谢谢。




刷到一个挺扎心的话题:程序员为什么不自己做产品赚钱。


身边还真有不少人问过类似的话:"你天天写代码这么厉害,怎么不自己搞个App、做个小程序?随便弄弄不就发财了?"


每次听到这种问题,我都不知道该从哪儿开始解释。


image.png


最近在 X 乎上看到同行的回答,看完只能说:太真实了。


理想很丰满、现实很骨感


首先,假装我们是程序员,某天深夜加班回家,瘫在沙发上刷手机,突然一个念头炸开——"我去,这个功能市面上根本没有!我要是做一个,肯定爆火!”。


脑子里的画面瞬间清晰:产品上线、用户疯涨、投资人排队、财务自由...,满脑子都是"老子不干了,要创业"。


说干就干,流程走起来:


第一步:注册账号结果发现邮箱早就被自己多年前注册过,还冻结了。解冻、换邮箱,折腾一圈。


第二步:想名字绞尽脑汁想了个好名字,一搜,已被占用。再想想想,终于通过。


第三步:开发前端后端一把抓,不会前端?没事,Ai结伴编程一把梭。uniapp启动,一套代码多端运行,微信、QQ、抖音、快手平台全都要上。


第四步:买服务器,阿里云一核两G,一年600块,付款的时候手还没抖。


第五步:搞域名,随便挑一个,一年30块,便宜。


第六步:备案到这里,噩梦开始了。拍照、填表、等审核,来来回回折腾。好不容易过了,提交小程序审核——"该项目类型个人不支持,需要企业认证。"


卒。亏损-630元。


但程序员嘛,头铁。不信邪,继续:


第七步:注册公司个体户要经营场所,干脆直接注册公司。准备材料、开对公账户、刻公章,又是一顿操作。


第八步:重新认证企业认证要的材料堆成山,干脆重新注册个小程序。又是想名字(原来的还要等两天才能释放)、填资料、承诺书、盖章...


终于,小程序上线了。


上线只是开始,赚钱才是难题。


每天努力宣传、引流,结果广告收益长这样:昨日收入0.65元。


对,你没看错,六毛五。折线图上的曲线在0.3元到1.8元之间反复横跳,月收入6.72元。服务器钱还没赚回来,先赔进去几百块。


什么会这样?



  • 个人开发者不能收费,只能通过挂广告,而广告收入低到离谱。激励广告单价居然只有4.29元/千次展示,Banner广告更惨,几块钱千次展示。算笔账:日访问量要达到2万,才能日入500。2万UV什么概念?很多小公司的官网一天都没这么多人。

  • 推广难,小程序是个封闭生态,你不能诱导分享,否则直接封号。只能从其他平台往微信导流,但用户路径一长,流失率奇高。要开通流量主还得先引流500人,这第一道门槛就卡死不少人。

  • 审核机制让人头大,页面上文字一多,就说你涉及"内容资讯",不给过。个人开发者经营类目受限,动不动就踩红线。


不是技术问题,是商业问题


程序员不做小程序赚钱,不是因为不会写代码,而是因为写代码只是万里长征第一步。


做一个能赚钱的小程序,需要:



  • 产品能力:做什么?解决谁的什么问题?凭什么用你的?

  • 运营能力:流量从哪来?怎么留存?怎么变现?

  • 商业资质:公司、对公账户、各种许可证,合规成本不低;

  • 时间和精力:白天上班,晚上搞副业,服务器半夜挂了还得爬起来修。


而大多数程序员,只是喜欢写代码而已。让他们去搞流量、谈商务、处理工商税务,比写一万行代码还痛苦。


更扎心的是,就算你愿意干这些,小程序的红利期也早过了。2017年刚出来那会儿,确实有人靠简单工具类小程序赚到第一桶金。现在?各大平台库存量几百万个,用户注意力被某音、被红书切得稀碎,新入局者基本就是炮灰。


成功案例


网上经常能看到"做小程序月入过万"的帖子,但仔细看会发现,要么是卖课的,要么是有特殊资源的(比如手里有公众号矩阵导流),要么是早期入局者吃到了红利。
对于普通程序员来说,接个外包项目,按时薪算可能比折腾三个月小程序赚得还多,还省心。


技术只是工具,商业才是战场。会拿锤子的不一定会盖房子,会写代码的不一定能做出赚钱的产品。这不是技术问题,这是两个完全不同的赛道。


最后


所以,开发一个小程序到底能不能赚钱?


能,但跟你关系不大。


要么你有现成的流量池,比如几十万粉丝的公众号、抖音号,小程序只是变现工具;要么你有特殊资源,比如独家数据、行业资质;再要么你踩中了某个极小概率的风口,比如当年疫情期间的健康码周边工具。否则,个人开发者大概率是炮灰。


写代码是确定性的事,输入逻辑输出结果;做生意是概率性的事,投入不一定有回报。 大多数人适合前者,却误以为自己能驾驭后者。


你呢?有没有过"做个产品改变世界"的冲动?最后成了吗?


作者:程序员凌览
来源:juejin.cn/post/7600489282839625738
收起阅读 »

前端也能搞模型?在浏览器里跑 Qwen2.5,我给公司省了 5 万 API 费用

月初,财务把上个月的 OpenAI 账单甩在了 咱们 CTO 的桌子上。 数字很惊人:9000 刀。 我们那个所谓AI 赋能 的内部知识库助手,每个月光 Token 费就要烧掉一辆比亚迪秦PLUS😃。 老板在会上拍桌子:不就是个查文档的机器人吗?为什么要用 G...
继续阅读 »

run-qwen-locally.webp


月初,财务把上个月的 OpenAI 账单甩在了 咱们 CTO 的桌子上。


数字很惊人:9000 刀


我们那个所谓AI 赋能 的内部知识库助手,每个月光 Token 费就要烧掉一辆比亚迪秦PLUS😃。


老板在会上拍桌子:不就是个查文档的机器人吗?为什么要用 GPT-5?能不能换便宜的?能不能限流?GPT-4o 或者 GPT-4o mini 就行?


后端架构师面露难色:精度要求高啊,换 GPT-4o 就变智障。自部署?那得买 H100 显卡,更贵。😖


会议室里充满了贫穷且焦虑的空气。


这时候,我默默合上了笔记本,说了一句:要不,把模型跑在前端吧?让用户的显卡帮我们算。


全场安静了三秒。


后端组长没忍住笑了:浏览器跑大模型?你当 Chrome 是 4090 呢?跑个 Three.js 都能卡,还要跑 Llama 3?😥


我不响。


如果你不知道 WebGPU,那我们确实没法聊。




你的浏览器就是显卡


兄弟,时代变了🤷‍♂️。


在大多数人的认知里,前端就是切图、调 API、写 React 组件。


计算?那是服务器的事。


但你有没有想过,现在的用户手里拿的是什么设备?


M3 芯片的 MacBook,RTX 4060 的游戏本,甚至 iPhone 15 Pro。


这些设备的算力,如果不利用起来,简直就是暴殄天物


我们现在做的,叫 Edge AI(边缘 AI)


核心技术就三个词:WebGPU + WebAssembly + 量化模型


以前浏览器只能用 CPU 跑 JS,慢得像蜗牛。


现在有了 WebGPU,JavaScript 可以直接调用底层的显卡算力。


再加上 WebLLM 🤖 这种大杀器,我们完全可以把 Llama-3-8B-Instruct 这种级别的模型,经过 4bit 量化 后,压缩到 3-4GB,直接塞进浏览器里。


这意味着什么?


意味着每一次对话,不再消耗公司的 Token,而是消耗用户自己的电费


听起来是不是有点资本家?


不,这叫分布式计算,哈哈哈哈哈。😎




先让 Chrome 跑起来


说干就干。


我没用后端的一行代码。


直接 npm install @mlc-ai/web-llm


核心逻辑就这么几行(伪代码),大家可以自己去试一下:


import { CreateMLCEngine } from "@mlc-ai/web-llm";

// 选最小且稳定的Qwen2.5,先保证能跑起来
const modelId = "Qwen2.5-0.5B-Instruct-q4f32_1-MLC";

// 初始化引擎(WebGPU)
const engine = await CreateMLCEngine(modelId, {
initProgressCallback: (p) => {
console.log("[MLC]", p.text);
},
});

// 控制台对话
async function ask(text: string) {
const res = await engine.chat.completions.create({
messages: [{ role: "user", content: text }],
});
console.log("🤖", res.choices[0].message.content);
}

// 测试
await ask("写一个 React Button")
await ask("用CSS实现三角形")
await ask("用Python写一个冒泡排序")


运行的那一刻,模型会自动加载(设备配置低的同学尽量用小模型🙂‍↔️)
👉 可用模型


image.png



ASK: 写一个 React Button



image.png



ASK: 用CSS实现三角形



image.png



ASK: 用Python写一个冒泡排序



image.png


上周一,我把 Demo 发到了公司群里了。


那个嘲笑我的后端组长,拿着他的 M1 MacBook Air 点开了链接。


初次加载用了 40 秒(下载模型权重),然后...


Token 生成速度:15 tokens/s。


甚至比 GPT-4 的 API 响应还要快!


因为这是本地生成,没有网络延迟。


他抬头看我:这...真的没调 API?😖


我指了指 Network 面板:0 请求,纯本地。




算一笔账:这 5 万多是怎么省出来的


我们内部有 50 个员工,每个人每天平均问 50 次。


如果是 GPT-5,一年就是30多万了(排除假期和周末)。


现在呢?


成本 = CDN 流量费(下载那是 0.5GB 模型文件,默认最低成本去算)。


而且模型有了缓存(Cache Storage)后,第二次打开就是零成本


更重要的是数据隐私


财务部那个姐姐之前死活不敢用 AI 搜发票政策,怕数据传给 OpenAI 泄密。


现在我告诉她:这东西跑在你自己的电脑上,断网都能用,数据出不了这个房间。


她看我的眼神都变了。🤩😃




也别神话,也有坑的!


当然,为了保持技术人的客观(虽然我已经在爽文里了),我得说实话,这方案不是完美的。



  1. 首屏加载是噩梦:如果让用户下载 4GB 的文件,如果是移动端 5G,用户会顺着网线来打你。所以这只适合桌面端 Web 或者 Electron 应用,尤其是开发测或者内部。

  2. 显卡门槛:没有独立显卡或者 Apple Silicon 的老电脑,跑起来会卡成 PPT。

  3. 发热:跑模型时,用户的电脑确实会变成暖手宝。


但在我们的场景——企业内部工具、文档助手、代码生成器——这些缺点完全可以接受。


毕竟,公司省下来的钱,哪怕分我 1% 当奖金,也够我换个新键盘了😂😂😂。




前端的边界在哪里?


2026 年了,别再把自己定义为画页面的。


WebGPU 的出现,给了前端直接挑战原生性能的入场券。


从今天起,试着把计算压力从云端搬到到终端来吧。


当后端还在为扩容发愁时,你已经利用用户闲置的 GPU 算力,构建了一个庞大的分布式计算网络。


这,才是属于前端的降本增效。🚀


你们说是不是呢?😃


Suggestion.gif


作者:ErpanOmer
来源:juejin.cn/post/7604741630448893986
收起阅读 »

Prompt 不够用了,火爆全网的 Skills 到底是个啥?

大家好,我是 Sunday。 最近不少同学在讨论 Skills。就连 Vue 也出了 Vue Skills 这样的一个库。 不光是 Vue,所有和 Skills 沾边的,star 都涨的飞快 那么 Skills 到底是个什么东西,为什么最近讨论的声音这么大...
继续阅读 »

大家好,我是 Sunday。


最近不少同学在讨论 Skills。就连 Vue 也出了 Vue Skills 这样的一个库。



不光是 Vue,所有和 Skills 沾边的,star 都涨的飞快



那么 Skills 到底是个什么东西,为什么最近讨论的声音这么大呢?今天,Sunday 就通过这篇文章,一文带你彻底搞懂 Skills。


什么是 Skills


Skill 翻译过来是 技能 的意思。更直白的来说就是:“给 AI 赋予一种技能”。


这句话乍一看可能不太好理解。咱们举个例子。


咱们就以 训练营 所做的服务为例。


任何一个程序员都得找工作,或者说 找工作是程序员具备的一大堆能力中的一个能力。


但是,因为程序员是一个综合性的角色,他会 写代码、梳理需求、和产品扯皮、跟测试甩锅、被裁了找工作。他虽然会找工作,但是找工作只是他众多能力中的一个,能找,但是找不好


那怎么办呢?


他就需要有一个独特的 找工作流程。比如:



  • 应该如何梳理自己的技术亮点,哪些亮点是对找工作有价值的,哪些是没有价值的。

  • 筛选出有价值的亮点之后,如何把有价值的亮点体现在简历上,让 HR 可以通过你的简历。

  • 如何进行面试的准备,面试练习、模拟面试、面试回顾 应该怎么去做,在每个环节中自己应该如何进行反思。


当我们把这一套 “经过验证的、标准化的、专业的流程” 封装起来,教给这位程序员时,他就从一个“只会瞎找工作的人”,变成了一个“求职高手”。


对于 AI 来说,这个道理是一模一样的。


通用的大模型(比如 ChatGPT、Claude)就像那个什么都会一点的程序员,它很聪明,什么都能聊两句。但如果你没有给它具体的 流程和规范,它也就是泛泛而谈。


Skills,就是我们把上面那一套 专业的 方法、模板、规范 打包成一个 工作流程(Workflow) 塞给 AI。


当 AI 装备了这个 Skill,他就会成为一个 具备专业技能,并且有标准工作流程的 AI... 很像之前的 工作流 概念,但是没有工作流这么复杂。


所以,Skills 的本质,就是 流程性知识的封装与复用 方案。


如何构建 Skills


Skills 最初是 Claude 提出的概念,并且配置非常简单。



那具体到代码层面,这玩意儿到底长啥样呢?其实对于咱们程序员来说,它的结构 非常简单,甚至有点朴实无华


一个标准的 Skill,在物理层面上,其实就是一个 文件夹。它的结构大概是这样的


skill-name/
├── SKILL.md (必需)
│ ├── YAML frontmatter 元数据 (必需)
│ │ ├── name: (必需)
│ │ └── description: (必需)
│ └── Markdown 指令 (必需)
└── Bundled Resources (可选)
├── scripts/ - 可执行代码(Python / Bash 等)
├── references/ - 用于按需加载到上下文中的文档
└── assets/ - 输出中使用的文件(模板、图标、字体等)

我们要构建一个 Skill,只需要两步走:


第一步:创建一个文件夹


首先,我们需要创建一个文件夹。这里有一个硬性规定:文件夹名称必须是小写字母 + 连字符。比如:lgd-sunday,不要使用驼峰标识。


如果,我们想做一个 “简历优化专家” 的 Skill,文件夹名字就得叫:resume-polisher(不能有空格,也不能大写)。


第二步:编写核心文件 SKILL.md


在这个文件夹里,必须包含一个核心文件,名字固定为 SKILL.md


这就好比 package.json ,它是整个 Skill 的入口。


SKILL.md 的内容结构非常固定,分为两部分:YAML 头部Markdown 主体


我们直接来看一个 demo,假设我们要把刚才提到的“简历优化流程”写成代码:


---
name: resume-polisher
description: 专门用于程序员简历优化。分析用户提供的原始简历内容,提炼技术亮点,并按照 STAR 法则生成专业的简历描述。
---


# 简历优化专家 (Resume Polisher)

## 指令 (Instructions)

你是一位拥有 10 年经验的技术招聘专家。当用户提供简历内容或技术经历时,请按以下步骤执行:

1.  **亮点挖掘**:分析用户描述,识别其中的高价值技术点(如性能优化、架构设计)。
2.  **去伪存真**:剔除毫无价值的描述(如“负责日常开发”、“修复 bug”)。
3.  **STAR 重构**:将保留的亮点,严格按照 STAR 法则(情境、任务、行动、结果)重写。
4.  **数据量化**:强制要求用户补充量化数据(如“提升了 50% 的加载速度”),如果没有,请标注为【待补充数据】。

## 示例 (Examples)

**用户输入**
“我在公司里做了一个长列表优化,用了虚拟滚动,效果挺好的。”

**你的输出**
- **项目背景**:在处理海量数据展示场景下,传统列表渲染导致页面卡顿(FPS 低于 30)。
- **解决方案**:引入虚拟滚动技术,只渲染可视区域内的 DOM 节点,并配合节流函数处理滚动事件。
- **最终结果**:首屏加载速度提升 40%,长列表滑动帧率稳定在 60FPS,内存占用减少 60%。

看到这里,大家应该就懂了。所谓的构建 Skill,其实就是:



  1. YAML 头部:相当于给这个 Skill 定义身份。



    • name:技能的名字。

    • description最关键的字段! 它是给 AI 系统看的,告诉系统“我是干嘛的,什么时候该调我”。注意:这里一定要用第三人称描述(比如“用于处理...”,而不要写“我可以帮你...”)。



  2. Markdown 主体:相当于具体的“SOP 手册”。



    • Instructions:具体的执行步骤,越详细越好。

    • Examples:给 AI 一个样板间,让它照葫芦画瓢。




当然,除了 SKILL.md,你还可以在这个文件夹里放各种脚本、参考文档、甚至 JSON 数据。Agent 在执行任务时,会根据需要自己去读取这些文件。


自动的 Skills 构建


虽然独立构建一个 Skills 并不复杂,但是很多之前没有接触过 Skills 的同学依然会觉得这玩意挺难的。


不过 没事! Claude 帮咱们想到了这一层。Claude 对外开源了多套 Skills,可以让我们直接拿过来用,对应的 Github 仓库叫做 skills



这个仓库里面有一个 skills 的文件夹,里面存放了 Anthropic 团队为 Claude Code 编写的各种官方 skill。



给大家用大致翻译了一下:



这里面不有一个必须要拿出来单独讲的,就是上图中最后一个 skill skill-creator。他的作用就是帮助我们直接创建属于自己的 skill


使用起来也非常简单。


大家可以直接把仓库的代码下载到本地,然后像我这样,直接通过 Claude 按安装这个 skill 整个过程全部使用自然语言就可以



安装成功之后,就可以直接跟 claude 说:“帮我创建一个新的 skill



后续的整个过程都完全使用自然语言对话的方式进行即可。


怎么让 Skills 跑起来


Skill 创建好了,不管是手写的还是自动生成的,现在你都已经有了一个自己的 skill 了。那么我们要怎么让 AI 识别并使用它呢?


这就涉及到 配置 的问题了。


对于 Claude Code 来说,它需要知道去哪里“读取”这些技能。通常我们有两种方式来配置:


1. 全局配置(推荐)


默认情况下,安装好的 skill 会被放到 ~/.claude-code/skills(苹果电脑) 下



这样的 skill 相当于是全局配置的。可以在 Claude code 中直接使用。


2. 项目级配置


如果你开发的 Skill 只是针对当前项目的(比如当前项目的自动化测试脚本),你可以直接把 skill-name 文件夹放在项目根目录的 .claude/skills 下。相当于单独使用




当配置完成之后,怎么触发 skills 呢?


其实很简单。


不需要 显式地输入类似 /run skill-creator 这样的命令。


还记得我们在 SKILL.md 里写的 description 吗?



description: 专门用于创建 skill 的 skill



当你对 Claude 说:“创建一个新的 skill” 时,Claude 的“中枢大脑”会快速扫描所有已安装 Skill 的 description,然后瞬间判断出:“哦!这个问题归 skill-creator 管!



于是,它会自动加载这个 Skill 的上下文,按照你制定的 SOP,一步步完成任务。


这就是 Skills 最迷人的地方: 用户无感,但能力却被无限扩展了。


总结


到这里,Sunday 相信大家已经彻底搞懂了 Skills 到底是个什么东西。


回顾一下,其实它并没有那么神秘:



  1. 本质:它不是什么黑科技,它就是 Prompt + 知识库 + 执行脚本 的结构化封装。

  2. 核心:它的核心在于 SOP(标准作业流程) 。你把 expert(专家)的经验变成了流程,AI 就能表现得像个专家。

  3. 未来:随着 AI 能力的进化,未来的编程模式可能会从 “写代码” 变成 “写 Skills” 。我们不再是写具体的逻辑,而是去定义一个个 AI 的工作流。


就像 Vue Skills 的出现一样,未来或许每个人、每个框架、每个公司,都会维护一套属于自己的 Skills 库。


另外: 来都来了,点个三连再走呗~~~


作者:程序员Sunday
来源:juejin.cn/post/7598353543892828166
收起阅读 »

有赞AI研发全流程落地实践

1. AI 时代的研发变革 1.1 AI编程的火爆 25年可以说是 AI 应用的元年,在编程领域从最基础的代码补全到辅助编程,到 AI 工程师,再到 AI 开发团队,各种概念层出不穷。 编程工具迭代涌现,从老牌的 Github Copilot 到爆火的 Cur...
继续阅读 »

1. AI 时代的研发变革


1.1 AI编程的火爆


25年可以说是 AI 应用的元年,在编程领域从最基础的代码补全到辅助编程,到 AI 工程师,再到 AI 开发团队,各种概念层出不穷。


编程工具迭代涌现,从老牌的 Github Copilot 到爆火的 Cursor,再到 Claude Code,和最新出的 Codex。


对应的用户规模也爆发增长,Github Copilot 用户超过 2000 万,Cursor 用户超过 230 万,Claude Code 在短短 3 个月用户增长 10倍,Github Copilot 年度 ARR 超过 3 亿美元,而 Cursor 和 Claude Code 都超过了 5 亿美元。



随着编程工具的发展,编程门槛被大幅降低,氛围编程(Vide Coding)开始兴起,让更多非专业开发者专注创意和结果


在有赞内部有产品同学开始用代码交付交互式的 PRD,在一些创新型项目中这会成为第一版代码。


1.2 AI对企业研发的变革



我们开始思考,AI 对企业研发意味着什么?


首先,传统软件研发生产要素,由人力、技术、信息组成,核心逻辑是多招人,就能多做项目,就有更多产出。 围绕人这个要素存在一系列问题:好人才难招、新人要培养、人才会流失、个体的动性、人的协作效率。


AI 时代人力开始向算力转移,增长逻辑编程多买显卡,获取更多算力,就能有更多产出。大部分体力工作向算力迁移,部分脑力工作可沉淀到算力中,算力能快速扩大。


最近看到一些新闻,硅谷的大厂一边裁员,一边争相购买显卡。


在这个过程中,人从直接产出转为对算力的设计与编排。



“让子弹飞”是一部经典的电影,这里引用其中一张截图,模糊度恰到好处。


远看是火车即将超越马,近看是马在拉着火车,这很有意思。和我们目前落地 AI 有点相似,“朋友圈”看起来各种炫酷,实际落地需要大批人马参与。


但这就意味着火车不如马吗?


显然不是,我们都知道最终火车一定会超越马,亦或,马拉火车的方式本身就错了


1.3 有赞研发的AI+


出于对 AI 趋势的坚信,在有赞研发内部,我们有大量的 AI 探索与实践。


横跨研发的完整流程,包括需求、设计、编码、测试、上线的各个阶段。


所有这些实践可以,归纳为三类:AI Coding、AI Test、AI DevOps, 以及围绕 Agent 本身的研发与评测。



1.4 AI编程小案例


在 AI 探索的初期我们遇到了两个很有意思的小故事。


1.4.1 一位产品的氛围编程之路


第一个故事是,我们的一位产品同学用氛围编程做了一个简单的日常需求,他给了一个反馈:严重缺乏安全感


我们深入了解后发现来源于三点:



  • 心理不可接受:由于企业级工程规模大、系统复杂度高,系统/功能间依赖关系较深。非专业人员面对生产稳定性和大量线上用户压力时,心理负担急剧增加。本质是判断力缺失;

  • 效率不可持续:判断力缺失也导致效率不可持续,导致需要频繁的和开发者沟通,氛围编程变成了 AI 和开发者之间的传声筒。编程的耗时被极大的转换成了沟通确认的耗时,最终效率不升反降;

  • 质量不可信赖:虽然 AI 做对了效果,但是实现方案不合理,最后返工。现代企业级软件工程,除了完成需求外,还需考虑健壮性、可维护性、架构一致性等因素。AI 由于缺少上下文输入,这方面做得并不好;


1.4.2 一位开发带领的编程团队


第二个故事是,五月 Claude Code 发布后,我们看到一些开发者编程模式的变化,如图是一位同学的 Cli 工具:



左侧是 4 个 Agent 分别在完成一个项目的 4 子需求,右侧是他在验证和提交代码。


这种模式类似一位技术 Leader 带领一个小团队做项目,由 Leader 负责任务拆分、方案设计、过程把控和代码CR。


过去需要几个人的小组,现在一位开发者加上多个 AI Agent 就可以做到了。


这给了我们不少启发,我们开始思考如何把这种模式推广到更多项目中。


1.5 企业级软件工程四大特点


在经过了初步的观察和尝试后,我们总结了 AI Coding 落地需要解决的关键问题,归纳为四点:



  • 大规模:这对 LLM 的多仓库理解、上下文窗口提出了要求;

  • 高复杂:通常涉及多个业务系统、历史兼容等,需要解决 LLM 的注意力缺失问题、准召率低问题;

  • 多协作:一个项目往往涉及多职能、多部门的协作,不同团队间对 AI 的理解和应用有深浅,如何协同?以及,人在哪些环节以怎样的力度监督 AI?需要合理地规划 AI 落地的节奏以及人机协作模式;

  • 私有化:企业内部有大量分散在各处的信息,不透明、不标准,难以被 AI 检索,需要建设内部知识库、对接内部工具;



1.6 AI落地的三个阶段


明确关键问题后,我们将 AI 落地划分为了三个阶段:



  • AI 增强阶段:人主导 AI 辅助,此阶段重点聚焦用 AI 做单点提效;

  • AI 驱动阶段:AI 主导人监督,此阶段 AI 有能力接管单一研发环节;

  • AI 自主阶段:AI 自主人少量介入;



对于不同的场景应该匹配适合的阶段,过度追求 AI 适得其反,我们踩过不少坑,AI 的落地无法一蹴而就,需要循序渐进


目前我们的实践大量在 AI 增强阶段,有少量能到 AI 驱动阶段。


2. AI Coding:从个人助手到规模协同


2.2 设计路线


首先,是关于 AI Coding 的设计路线,我们有两个选择:以人为主 Agent 为辅、以 Agent 为主人监督。


我们观察了一些开发者的辅助编程模式,我们发现以人为主的模式存在一些问题:



  • 协作还是以人为中心,相互沟通靠声音、工具调用靠手速,信息传递效率低

  • 个人经验内化不共享,Agent 配置没有复用,信息分布式存储

  • 另外,人的规模化需要时间


因此,我们选择了第二条路线,其可以系统化解决以上三个问题,并且“天花板”更高。



2.3 构建框架


明确了设计路线后,让我们开始构建 Coding Agent,这个过程类似从 0 到 1 搭建一个开发团队:



  • 它们都需要有好用的工具,无论是硬件、软件还是技术选型;

  • 它们都由专业的个体组成;

  • 并且这些个体能高效协同完成复杂的任务;



让我们先从设计一个专业的 Agent 开始!


2.4 设计演进


通用 AI Agent 和计算机很相似:



  • 它们都有计算单元,一个是 CPU,一个是 LLM;

  • 它们都有短期存储,一个是内存,一个是上下文;

  • 它们都需要获取外部信息,一个通过网络,一个通过搜索工具;

  • 它们也都可以多节点进行协同,完成复杂任务;



当我们把通用 AI Agent 细化到 Coding Agent,一些选型会更具体:



  • LLM 需要选用垂直的编程 LLM;

  • 上下文需要引入企业的开发规范/约束;

  • 长期存储需要对接企业内部知识库;

  • 搜索工具聚焦在代码仓库的搜索和理解;

  • 工具需要对接企业内部的工具链和平台;

  • 协同系统的拆分应该以企业内部的协作流程或领域分工来拆分;



2.4.1 选型与选择


在明确了核心模块后,需要先回答两个问题:方案的选型、自研的选择。


在 AI 时代行业解决方案百花齐放,无论是大模型还是配套的技术,且迭代速度极快,比如近期的Gemini3(11月18日)、Nano Banana Pro(11月20日)。


做的越多越容易陷入追赶的局面,最大程度的借助行业能力,并将其与企业内部资源串联是关键


我们的核心策略是将通识的交给行业,集中资源做私有部分,通过行业迭代来提升底层能力


2.4.2 Base Agent & System Prompt


明确策略之后,首先是基础模型的选择,在25年初时我们基本有共识,LLM 应该交给大厂,我们则专注于应用。


到了 5 月份我们发现 Agent 系统也有不错的行业实践,因此问题被延展成了选择基础模型还是选择基础 Agent? 也就是 Claude 还是 ClaudeCode?


围绕这个问题我们做了一些研究,发现 Claude Code 作为通用编程 Agent,在子 Agent 调度、MCP 集成、通用开发工具、上下文压缩方面已经做得不错了。


因此,我们选择了 Claude Code 作为基础 Agent 的底座,通过其强大的扩展能力,将其连接到我们内部的研发体系。


首先是系统提示词,这方面 Claude Code 已经做的不错了,我们仅做了少量扩展,总共不到 200 行,主要是一些内部的编程规范、特定约束。



2.4.3 Dev Tools


接着是工具,和人一样,Agent 也需要开发工具。比如,当开发者让其参考历史需求,它需要能访问需求管理平台查看。又比如,它可以通过飞书文档创建一篇技术方案给开发者审阅。


这些工具散落在内部的各个系统、平台,有些在外部,我们通过 MCP 将这些工具集成到一起,交由 Agent 决策使用。



2.4.4 Codebase Search


有了工具后,接着是代码理解能力,这也是 Coding Agent 的关键能力之一。


我们将其分为了三层:目标仓库定位跨仓库依赖代码召回工作区代码理解。目标仓库定位比较简单,我们用的是知识库 RAG 的方案。


在代码理解上,业内常见的方案有:向量索引检索(RAG)抽象语法树(AST)文本搜索(grep)预生成文档(deepWiki),在实际应用中有不少组合的情况。


Cursor 选择的是 RAG 方案,好处是速度快,适合大的单体仓库,缺点是精度丢失(向量化),实时性不高。


Claude Code 则选择比较纯粹的文本搜索方案,和人很像,好处是精度高、实时性强,但性能会差一些。


在有赞对于跨仓库依赖代码,由于仓库数量庞大,采用的是 RAG 方案,同时更新频率没有 Cursor 这么高,我们基于 Git 的提交记录进行增了更新。对于工作区代码理解,由于基础 Agent 是 Claude Code,直接采用它的文本搜索。


另外,代码安全也十分重要,无论是发送给向量嵌入模型还是大模型的代码片段,都通过安全扫描进行脱敏。



2.4.5 Knowledge Base


有了代码,接着是内部知识,和开发者一样 Agent 需要理解业务知识,比如产品模块有哪些、业务术语是什么意思。也需要理解专业知识,比如技术选型用了什么、编程规范是怎样的,以及工程现状。


这些额外的上下文可以让 Agent 的编码更符合预期,避免写出“与众不同”的代码。


过往企业知识库建设有两大痛点,一是信息孤岛(无法统一检索/重复建设严重),二是内容老旧(因为缺少审核/运营机制,大量过期/错误内容)


为此,我们内部成立了专门做知识工程的团队,由他们推动知识透明和更新。他们围绕知识增长、内容质量、知识使用进行建设,为上层应用提供高质量的知识。



至此,一个 MVP 版本的 Coding Agent 完成了。我们拿去做了一些需求,发现效果并不好,原因是还有大量经验在开发者的头脑中


2.4.6 Long-term Memory


为此,我们会 Agent 打造了基于长期记忆的学习系统。简单来说,就是将开发者和 Agent 的监督对话提取为长期记忆,后续遇到类似场景时进行召回使用,这也可以实现跨会话的复用。


这就如同一个经验丰富的老员工手把手带教实习生,整个过程分为:



  • 记忆提取,该阶段重点关注符合事实、保留细节、不归纳泛化。

  • 记忆储存,我们采用自然语言+标签的形式储存而非结构化,并且保留了原文引用。

  • 记忆合并,定期将类似记忆归纳合并。

  • 记忆检索,业内根据场景不同常见的有:向量数据库、知识图谱、分层存储,我们采用的是向量数据库。

  • 记忆遗忘,随着记忆数量的增加,根据时间和使用频次进行遗忘。对于高频使用的记忆经过泛化后转化为知识


虽然设定了很多提取规则和后处理,但在实际应用中,通过人工标注及修正的方式,可以较快加速记忆的可用性


我们在 AI Coding 做了测试,同类任务未接入记忆需要 5-10 轮对话修正,接入记忆后仅需 1-3 轮,有较大提升。



2.4.7 Cloud Sandbox


Agent 构建完成后,无论是运行、调用工具、拉取代码等等,都需要一个环境,如同为开发者配置一台电脑一样。


通过对本地辅助编程的观察,我们发现本地运行存在一些问题:复杂环境导致额外上下文、工程化适配问题、安全与监管难解决。


因此,我们选择了云端部署的方案,为每个 Agent 的每次会话分配了独立且标准化的沙箱(Sandbox),其中包含了开发环境,以实现交付结果预览。


同时对会话及沙箱做了无状态化,实现水平扩容能力。我们将会话存储在共享存储中,当开发者开启或继续一个会话时,会动态分配随机沙箱,并从共享存储中恢复会话。


这一切都需要通过统一网关,以解决安全和监管问题



2.4.8 Multi Agents System


随着接入环节的增加,我们发现单一 Agent 面对个多环节,由于 LLM 上下文长度导致了遗忘、性能等问题。


为此,我们引入了多 Agent 系统,将这些复杂度分而治之



  • Agent 的编排上有 Workflow 和 Agentic 两种方式。由于企业级工程对稳定性、观测性的要求较高,我们采用 Workflow 串联多个 Agent。在部分 Agent 内部通过 ReAct 模式发挥 LLM 的自规划能力。

  • Agent 的拆分上有流程拆分和领域拆分两种方式。我们都做了尝试,从落地情况看按流程拆分基本可以解决大部分问题。


同时,我们为每个 Agent 做了差异化的基模选择,主要根据 Agent 的任务特点、LLM 的优劣势以及成本。



  • 需求解析和仓库定位较简单用 GTP-4o;

  • 代码定位用向量嵌入模型 voyage-code-3 并配合 deepseek-v3 做一些后处理;

  • 方案和编码则用 Claude-sonnet-4.5,其中记忆用 GTP-5 效果出色;

  • 代码审查则用 Gemini 2.5 它的上下文窗口较大,可以把更多代码片段给到 LLM;



2.4.9 人工监督(HITL)


整个 Agent 系统的运行离不开人工监督(Human in the loop),我们面向开发者、管理者分别设计了两套监督系统。



  • 面向开发者的监督系统主要基于飞书 IM,它是一个天然的对话流,同时结合飞书文档、GitLab。开发者可以在 Agent 的每个阶段对产物进行审核,包括:需求清单、技术方案、改动代码、实际效果等,并通过多轮对话进行修正;

  • 面向控制者的监督系统主要基于多维表格及其仪表盘,管理者对每个需求的情况一目了然,包括:交付率、对话轮次、Token 消耗、对话明细等;


其中,对话明细可以转化为评测集,用于后续 Agent 的评测。



2.4.10 对接发布系统


最后,我们通过部署发布 Agent,打通了发布系统。如图所示,通过对话就可以很方便的部署到预发,或发布至生产:



2.5 产品形态


这就是我们最终的产品形态,已经交付了不少需求。当然实际落地中,这种人机交互模式也有局限性,因此我们正在开发 Web 端,类似 manus 的效果。



2.6 需求选择与落地


在 AI Coding 落地的需求选择上,我们分了三个维度:多职能、多仓库、需求规模,和新人一样在 Agent 能力还不够时,先从单职能单仓库的日常需求开始。通过做小需求验证 Agent 并积累数据,同时小需求因优先级不高而积累,用 AI 来做具有提效价值。


在我们将 AI Coding 推广到兄弟团队时,发现大家对 AI 期望很高,大家会给它做非常有挑战的需求,我们需要理解** AI 不是万能的,人做不了的它也是**。


目前我们的 AI Coding 已经可以实现单职能多仓库的日常需求,已交付近百个需求。综合提效 30%,包括人工监督的耗时,单个需求 Token 费用不到 100 人民币。


接下来我们会重点向多职能多仓库、项目级需求两个方向迭代。



2.7 实践案例


2.7.1 翻译型事务


在落地过程中,我们也发现了一些特别适合 AI 做的共性需求。


第一类我们称之为:翻译型任务。 已经有明确方案且较为简单的任务,AI 可以快速完成,且质量还不错。比如:技术债务治理。


有一个基础库升级的案例,涉及基础库、业务库和23个业务应用的升级,原本需要超过50人日,通过 AI 几十分钟就好了。


2.7.2 降低开发门槛


第二类是跨域编程,我们工作中经常会需要跨团队、跨领域支援,过往人的学习和熟悉过程会有额外的时间。通过 AI Coding 我们的开发者进行了不少的跨域编程,这部分时间几乎被抹平。


2.7.3 小插曲:移动编程


另外有个比较有意思的小插曲分享一下:


相信程序员都有共鸣,随身携带电脑是大家的痛点之一。在我们落地 AI Coding 的过程中发生了另外一个小案例,有个开发同学出去聚餐没带电脑,这时候来了个 Bug:



  • 他掏出手机打开飞书用聊天唤起了一个 Agent

  • 把 Jira 和说明丢过去

  • 让 Agent 先改着,就先吃饭了

  • 过了几分钟 Agent 改完了

  • 他看了没问题就让 Agent 发布了


这虽然只是很小很小很小的案例,但确实让我们看到了 AI 正在慢慢改变我们的编程方式。


2.8 完整架构


以上,就是我们在 AI Coding 方面的实践:



  • 首先,我们做了好用的工具,也就是云端 Sandbox;

  • 其次,基于上下文工程,我们打造了专业的 Agent 个体;

  • 最后,通过 MAS 让他们协同工作;

  • 另外我们也做了人机交互界面,让人可以监督 Agent;



3. AI Test:从自动化到智能化


3.1 传统测试的局限


在开始之前,先简单回顾下传统测试的挑战:技术栈多样化、设备终端碎片化、工程规模增大,因此对测试效率提出了要求


自动化测试被引入,提升了一些效率,但有一些局限:编写有门槛、维护成本高、难扩展复用、失败排查困难。



3.2 AI时代的新挑战


在 AI 时代,随着编码效率提升,对测试效率提出了更高的要求。同时由于 AI 生成代码不确定,影响面和风险更大,对测试带来了新挑战。


同样 AI 也对测试带来了新的可能:



  • 在测试设计阶段,能做 AI 用例生成、AI 用例优化、AI 用例选择(精准测试)等;

  • 在测试执行阶段,能做 AI 数据构造、AI 驱动执行、AI 增强录制、AI 无参考测试等;

  • 在评估优化阶段,能做 AI 失败归因、AI 用例修复、AI 分析报告等;


我们在这方面踩了不少坑,也有些在部分场景落地了。



3.3 AI用例标准化


首先是,AI 用例标准化,在我们开始结合 AI 和测试时,碰到的第一个问题就是:用例


过往历史用例缺乏维护,质量参差不齐,如左图各种隐式步骤、断言缺失。另外用例还分散在各个平台,比如文本和自动化用例互不相通。


这就导致了GIGO(Garbage in, Garbage out)的现象,你给 LLM 的输入质量低,它的输出质量也低,幻觉严重。



于是,我们开始思考如何解决用例的问题,我们发现 LLM 本身有强大的自然语言理解能力。这让传统文本用例和自动化用例的融合成为可能,也就是自然语言用例,让用例兼具语意性和可执行性


我们探索了两个方向:AI 存量用例优化、AI 增量用例生成。


首先是存量用例优化,根据已有的测试目标和测试步骤由 LLM 生成更规范的用例名、标签等,同时逐步优化用例步骤、补充断言。


其次对于增量的用例,我们结合业务知识库,并参考现有用例进行生成。


所有的这些自然语言用例放在一起,它们本身就是一个高质量的测试知识库



一个新的用例生成过程分为三步:



  • 填写基本信息:测试目标、开启 AI 规划;

  • 选择参考用例:从历史用例库选择供 LLM 参考;

  • 用例生成:LLM 会生成用例的基础信息,以及完整的测试步骤,这里的步骤是自然语言,一目了然,且可被执行;


目前,用例生成的准确度和覆盖场景还有很大空间,仍处于探索阶段。



3.4 AI增强录制


我们还有不少用例是录制的,因此我们做了 AI 增强录制。


首先先简单回顾下传统录制的局限:定位不精确、录制步骤冗余、需要人工校准、维护成本高。如下图:



通过 AI 增强后,测试步骤可以精确识别用户的意图:输入价格1、输入划线价2,且较为精炼:



整个增强过程大致分为几步:



  • DOM+截图输入:首先是捕捉用户的操作,将完整 DOM 和截图丢给多模态 LLM,目前通用多模态 LLM 已经比较强大了,我们第一版就能做到 70% 准确率。即使不提供 DOM,LLM 也可仅依赖截图识别,从而支持跨平台录制。但这个方案的 Token 消耗是巨大的,且上下文太长会带来一些幻觉导致不稳定;




  • DOM 预处理:通过对 DOM 进行裁切,降低噪音和 Token 消耗,同时标注重要内容提升大模型注意力。另外一些敏感的信息和代码也会被过滤。做完这些后我们的上下文能裁切大约 99%;




  • 交互区域标注:接着我们把 70% 准确率进一步提升,遇到了一些问题。在用户操作无交互表现的情况下,LLM 无法很好的理解,如左图,这里点击“…”没有交互状态变化,LLM 只理解到“点击空白区域”。我们通过增加交互区域标注来解决,这让 LLM 可以准确理解用户交互的区域,效果如右图,LLM 可以理解到“点击更多按钮”;




  • 父级元素标注:虽然增加了交互标注,但在一些复杂业务,元素之间是有关联的。如左图,“?”的图标,LLM 可以理解到是“帮助”,但它并不理解是“退款资金状态的帮助”,如果一个页面中有多个类似图标就不准确了。为此,我们增加了父级元素标注,把交互区域前后的父级 DOM 元素一起给到 LLM,让其理解更多上下文关系。如右图,增加后 LLM 可以理解到“点击退款资金状态的帮助”;



做完这些后,我们的准确率可以做到 89%,单次增强的耗时在 5-10 秒之间,这得益于模型能力的提升,从 Qwen2.5-VL 的 20 秒到 GTP-4o 的 10 秒以内。


目前这套方案是跨全平台的,包括Web、Pad、桌面等设备,Android、iOS、鸿蒙等系统,以及各类小程序。



3.5 AI用例执行


有了用例之后,接下来是用例的执行。目前我们的用例执行都会统一注册到任务中心,由其下发到两大集群,分别执行浏览器任务、App任务(包括小程序)。


AI 在执行方面有天然的优势,跨平台支持,具备一定自适应能力,比如一些设备有像素抖动的情况,LLM 可以识别提升用例稳定性。


目前我们的用例执行量超过每天 10 万次,任务成功率 96%。



当然,这个过程中也遇到了一些问题:模型执行速度慢、模型幻觉问题、模型识别精度问题。



  • 解决“模型执行速度慢”:我们先做了基于图像和 Prompt 的识别缓存。但未命中缓存时 AI 指令的秒级,和程序指令的百毫秒级差距依旧很大。因此我们做了 AI 提速,用例解析后首先程序执行,失败时 AI 兜底执行,执行成功后由 AI 进行自愈,提取成功的信息更新程序脚本;

  • 解决“模型幻觉问题”:幻觉问题不是太大,通过 LLM 二次优化步骤 Prompt,同时采用更准确的 AI 指令(如 Midscene 中用 AI Input 替代 AI Action)基本可以解决;

  • 解决“模型识别精确问题”:看下图中右上角的图,红框是 Qwen-vl-max,绿框是UI-TARS-1.5,它们对于小元素的识别精度差异很大。从我们时间来看 Qwen-vl-max 对小图标识别能力较差,开启 deepThink 有所改善。UI-TARS-1.5 具备较强的探索能力,它们在断言方面都不如 GTP-4o。我们也从元素定位、元素断言、内容提取、任务规划方面,测试了 5 个主流端,UI-TARS-1.5 在元素定位精度和移动端表现较好,也是我们目前的选型;



3.6 AI无参考测试


做了 AI 执行后,我们开始思考 LLM 本身具备常识,也知道企业内部知识,是否可以做 AI 无参考测试?


我们做了探索,首先要做无参考测试,对模型精度要求较高,通用的多模态 LLM 较难满足


因此,我们引入了监督微调(SFT LoRA),主要让 LLM:具备更细化的任务定义、明确输出格式便于工程化对接、注入内部知识和评判标准。


我们的算法团队做了不少建设,让业务团队可以聚焦在微调数据集。一条微调数据包括:Instruction、Input、Output,下图是一个最简单的案例:



当然这条微调数据并不好,它的 Output 缺少了很多内容, 比如:作答风格、结构化输出、内部知识、分析的过程和原因。我们从指令微调思维链微调两部分对这条数据进行优化,如下图:



定义了数据结构,接着是如何生成大量的微调数据,一般需要训练集、验证集和测试集,前两者用于训练过程,后者用于最后的评估。


整个数据集包括正样本、负样本和混合样本,一般比例为 1:3,避免模型过度偏向“总是发现问题”。


数据集通过 LLM 合成,首先通过脚本抓取线上的原始样本(正样本),然后通过程序合成多个负样本。


结合样本图片和特定的 Prompt 用 LLM 来生成 Output,Prompt 中包括我们的内部知识、预期输出结构、作答风格。


目前我们的垂直模型正在微调中。



3.7 AI归因归类


当用例执行完成后,我们需要对失败用例进行分析,过往需要人工分析效率较低,100 条失败需要15分钟。


我们通过 AI 来提速这个过程,首先由 LLM 将单个失败总结核心原因,然后将类似原因的分组归类。


如右下图,一共 85 条失败用例,被快速归为了 3 组,且标明了核心原因。


人工可以非常快速的进行分析,100 条仅需 1 分钟。



整个归因归类过程,首先是对用例执行报告进行程序预处理。


程序主要将失败图片进行切片、步骤进行拆分。前者解决多模态 LLM 在大图下的幻觉及不稳定性,后者解决性能问题。


接着切片交给图片归因 Agent、步骤归因 Agent,进行并发分析。


再由总结 Agent 对原因进行合并总结,最后归类 Agent 对原因类似用例进行分组。


目前我们线上用例已 100% 覆盖 AI 归因归类,归因准确率为 85%。



3.8 AI用例修复


做了 AI 归因归类后,我们发现既然原因都知道了,何不让 LLM 自己修复?


因此,我们正在探索了 AI 用例修复,目前主要针对像素差异率波动、非核心元素变化的场景实现了修复。


如下图,AI 会对可修复场景进行建议,人工确认无误可一键修复。目前修复准确率 60%。



3.9 与 AI Coding 结合


以上就是我们在 AI Test 方向的实践,后续我们计划串联 AI Coding 和 AI Test。由 Coding Agent 生成测试目标,交由 Test Agent 对用例进行召回和生成,并完成后续的自动化测试工作。



4. Agent 评测:从炫酷飞起到生产落地


在 AI Coding 和 AI Test 中我们构建了大量的 Agent,需要一套 Agent 评测体系对它们进行评估反馈。


4.1 为什么需要评测


先看一张大家都很熟悉的图,我们看别人的 Agent 时都会觉得好炫酷跃跃欲试,然后自己也开始手搓 Agent。


做完后在开发环境或者小范围测试中表现也不错。


发布到生产后,各种奇奇怪怪的情况,用户提问的思路无法捉摸,最后还是要转人工。



我们需要知道,Agent 和传统软件不同,没办法列举完整的用例来保障效果。


软件测试目标固定、标准化、可复现,而 Agent 评测具有不确定性、开放性(比如用户提问)、多样化(比如模型回答)。


发布标准上,传统软件主要看功能点完成度,测试通过与否。 Agent 则需要用评测集进行多维度的指标评估。


Agent 开发完并不意味着达到生产发布标准,应该用评测指标作为依据。



4.2 评测集


开始 Agent 评测,首先需要准备评测集, 评测集的类型有:有参考、无参考、参考资料三种。


接着是评测集的构造,常见的方法有:人工标注、LLM 泛化、线上采样。


一个全新的 Agent 没有线上采样数据,我们可以通过人工标注 50-100 条的种子集,然后让 LLM 进行泛化。


评测集根据场景不同,也可以划分为:种子集、Badcase集(一般来源于用户反馈)、扩展集、对抗集、场景集(一般由业务场景决定)。



4.3 评测器


有了评测集之后是评测器,评测可以分为两种:人工评测、自动化评测。新的 Agent 初期由于评测标准不确定,可结合人工评测,随着调用增加,将人工评测维度沉淀为自动化评测。相较于人工,自动化评判尺度统一、主观偏差少。


自动化评测器包括: 托管(平台自带的评测器,可以快速启动评测项目)、 自定义(自行根据业务编写 Prompt 的裁判模型)、外部(由外部平台/服务提供)。自动化评测器可以由 LLM 作为裁判,可以是特定的算法验证(如 BLEU、CLIPScore等),也可以是业务逻辑验证。


评测器设计一般包括:评估步骤、打分标准、推理指令、少样本提示(正例/反例)、边界案例、基于业务的判断要点(如合规、医疗等场景)。评测打分用 0-5 分制归一,可以兼顾可解释性和区分度,让不同评测维度可以横向对比。另外,为裁判模型添加 CoT 可以提升可解释性、一致性,便于后续人工分析和优化。



4.4 评测指标


接着是评测指标的设定,首先需要先明确评测主体,可以是:基础模型、提示词版本、工具版本、召回的记忆等。


围绕主题设定评测指标,通常有四类:效果指标、技术指标、用户指标、业务指标。


另外裁判模型自身需要有评测指标



  • 人工一致性, 衡量模型评分与人工标注结果的一致程度;

  • 评分方差,衡量模型打分的稳定性;

  • 异常打分率;



4.5 反馈系统


除了人工评测、自动化评测外,生产中用户真实的反馈至关重要。能够帮助我们发现 Agnet 在实际应用中遇到的各种边界场景和潜在问题。


一般有两种方式:



  • 显示反馈:需要用户明确操作(点赞/点踩)反馈,意图明确但反馈率较低;

  • 隐式反馈:是用户使用流程中的行为数据,被采集并解释为反馈,可以获得较高反馈率,比如 AIGC 生成多张图片时,用户选用其中一张图;


对于反馈率低的问题,通过预设标签来减少用户行动成本,通过预设高评分来增加用户反馈意愿(人们更倾向吐槽而非夸赞)。


4.6 评测体系


以上这些组合在一起构成了我们完整的评测体系。



  • 项目启动:由专门的评测人员和项目组成员,共同设定评测维度、创建评测器、建立种子集、泛化评测集,基于评测不断迭代/验证/反馈 Agent;

  • 开发过程:在 Agent 迭代过程中,通过评测集和评测器进行线下实验;

  • 生产环境: Agent 会产生日志和反馈,日志会通过评测器进行线上评测,Trace 可采样沉淀到评测集,另外反馈的 Badcase 也会沉淀到评测集;


所有 Agent 发布前都需要经过评测验证



5. AI落地的一些经验


5.1 AI与程序的结合


首先是 AI 与程序,常见构建 Agent 的模块包括:程序计算节点、单一 LLM 节点、Workflow、Agentic。


可能有些人会觉得,Agentic 比 Workflow 好,Workflow 比单一 LLM 节点好,单一 LLM 节点比程序好。从我们实践来看,这四者之间没有绝对的优劣,通常需要根据业务场景进行多选和组合,其取决于场景对确定性、稳定性、创造性、自主性的侧重。


另外,程序和 LLM 也并非替代关系,LLM 更适合理解、规划、生成类任务,程序更适合确定性、稳定性、高性能任务。通过程序规避 LLM 的弱项是必要的,避免过度追求 AI 含量



5.2 AI与人的陷阱


接着是,AI与人,需要警惕两个陷阱:



  • 在落地 AI Coding 的过程中,出现开发者对 LLM 生成代码的依赖,甚至出现不作判断直接提交的情况。保持开发团队的主观判断力非常必要。另外分场景追求 AI 生码,比如核心业务逻辑人写,非核心交由 AI 写;

  • 在落地 AI Agent 的过程中,需要人工监督,应该**警惕人工监督大于迭代 Agent **的情况,将监督数据用于 Agent 迭代;



5.3 AI落地经验


最后总结下 AI 落地的关键经验:



  • 区分创造性和执行性工作,现阶段 LLM 可以较好的完成执行性工作,这类场景较为合适;

  • 落地从传统研发流程切入,寻求快速出 MVP 进行单点突破;

  • 现阶段 AI 仍需大量人参与,过程中充分考虑人机协作,比如人机交互(人好用)、知识外化(人转化)、责任归属(人敢用);

  • 同步建设私有化的 AI 基建,将行业能力与企业内部能力串联;

  • Agent 的迭代不能先方案再开发,应该基于测试集和上下文工程快速试错;

  • 最后在不同的场景分阶段落地,不要一蹴而就;



6. AI实践全景



以上是我们在研发全流程落地 AI 的实践:



  • 我们通过软件工程(Software Engineering)和上下文工程(Context Engineering),将程序与 LLM 结合

  • 围绕研发的设计、编码、测试阶段落地 AI 能力;

  • 过程中建设了 Agent 评测体系,不断反馈,持续迭代;


作者:有赞技术
来源:juejin.cn/post/7592094358658138146
收起阅读 »

Agent Skills在货拉拉AI应用尝试

前言 美国时间 2025 年 12 月 18 日,Anthropic 正式宣布将 Agent Skills 发布为开放标准。去年刚写了篇关于 MCP 的文章,今年 Anthropic 发布了 Agent Skills,迫不及待的试一试,到底有没有宣发的那么强悍...
继续阅读 »

前言


美国时间 2025 年 12 月 18 日,Anthropic 正式宣布将 Agent Skills 发布为开放标准。去年刚写了篇关于 MCP 的文章,今年 Anthropic 发布了 Agent Skills,迫不及待的试一试,到底有没有宣发的那么强悍。


Agent Skills 是什么



This led us to create Agent Skills: organized folders of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks.


http://www.anthropic.com/engineering…



官网的介绍就是这样,说到 Agent Skills,就一定要和 MCPA2A 对比,这样才能更好理解 Agent Skills


image.png引用:Anthropic 工程团队博客 http://www.anthropic.com/engineering…


首先,抛出结论:Agent Skills 定义“能力”,MCP 提供“工具”,A2A 实现“协作”。


对比


2.png


image.png


核心关系


你可以将这三者理解为构建一个“智能体公司”的不同部门:



  • Agent Skills 像是公司的各个专业员工,他们各自掌握了完成特定任务(如写代码、做设计、分析数据)的完整方法和流程。



  • MCP 像是公司的统一后勤与工具库。无论哪个员工需要工具(如使用数据库、调用某个软件),都通过标准流程从这个库中领取,无需自己再造。



  • A2A 像是公司内部的协作通讯协议和会议制度。当一项复杂任务需要多个部门的员工(即多个智能体)合作时,他们依据这套规则进行沟通、同步进度和交付成果。


优势


Agent Skill 的思路有别于 MCP 的开发模式,从官网来看,有几个特点可以关注。


特点一:渐进式披露 (Progressive Disclosure)


渐进式披露是Agent技能设计中的核心原则,它让智能体的技能体系既灵活又可扩展。就像一本结构清晰的说明书,先给目录,再分章节,最后附上详细附录——技能的设计也是如此,让Claude只在需要时才加载对应的信息。


当智能体具备文件系统和代码执行工具时,在处理特定任务时,无需一次性将某个技能的全部内容读入上下文窗口。这意味着,一个技能所能涵盖的信息量实际上是没有上限的。这相当于,你可以给一个 Agent 装备 1000 个,甚至无限技能(从写 SQL 到 查数据),只占用极少的上下文(Context),只在执行时才调用相关工具。这完美解决了长期以来困扰开发者的Token 浪费上下文干扰问题。


特点二:LLM不是万能的


大语言模型虽然擅长处理多种任务,但有些操作还是交给传统代码来执行更合适。比如,让模型通过逐词生成来排序一个列表,远比直接运行排序算法的消耗大得多。除了效率问题,很多实际应用还需要确定性的可靠结果——而这只有代码才能保证。


Agent Skills提出,很多确定性的事情或者输入输出很清晰的事情,是可以拆解为traditional code执行,甚至执行的效果会更好,这也是Agent Skills的优势,它只会在具体执行到的时候触发(Claude can run this script without loading either the script or the PDF int0 context. )不用像传统Agent方式,全部输入到prompt上下文。


image.png
引用:Anthropic 工程团队博客http://www.anthropic.com/engineering…


技能会在上下文窗口中通过系统提示符触发


落地


大概的Skill结构,如下


image.png


核心是需要写SKILL.md


image.png


必需字段:



  • name - 技能的名字(小写字母、数字、下划线)

  • description - 技能功能和使用场景描述,帮助AI判断何时使用


实战一:自然语言查数


背景


大数据存在大量数据分析场景,例如财务、A/B 实验报告等。Agent Skills 可将流程性的知识,打包成可组合、可复用的技能。我们不需要造更多的 Agent,只需动态加载技能,就可以解决特定领域的问题。


案例


我们可以将财务Agent和A/B实验报告Agent的自然语言查数,提炼为如下步骤:



  1. 理解用户意图:选择合适的数据集信息(财务、A/B实验报告(订单、用户))

  2. 加载领域知识:读取相关场景的元数据、业务知识等信息

  3. 加载SQL:生成知识,识别所使用的数据库信息、及相关SQL规范

  4. 生成并执行 SQL:选取hive.py & doris.py 工具,查询结果


现在,我们将这一套流程打包成技能,其结构如下:


image.png


接下来,我们在 Agent 中注册这个技能,就可以快速实现自然语言查数的能力。


财务


image.png


A/B 实验报告


image.png


将自然语言查数打包成技能,后续各业务Agent不再需要定制自然语言查数能力,只需要做好相关领域知识的维护,就能快速解决查数问题,而且,整个流程更容易治理和迭代。


实战二:指标归因分析


背景


大数据存在海量的数据,数据需做一些归因分析,可以进一步发挥数据价值。


skills能力


image.png


image.png


核心流程



  1. 理解用户意图:选择合适的SKILL

  2. 加载领域知识:读取相关场景的元数据、业务知识等信息

  3. 解析scripts:识别提供的python工具包并使用

  4. 判断是否继续:判断是否解决问题并调用其他工具


核心结果:


第一阶段分析,分析结束后可衔接其他技能第二阶段分析,数据视角更深入

注意:文章内容均为测试环境测试数据



  1. 业务经验抽象的质量,决定了Agent能力的上限

  2. Agent Skills方案,降低了把业务经验注入到大模型的技术复杂度

  3. scripts是双刃剑,为agent扩展能力边界的同时,也带来较大安全隐患,请谨慎使用外部Skills


核心业务指标分析逻辑 SKILL.md原文件


---
name: 核心业务指标分析逻辑
description: 分析指标1指标及其关联指标的周环比变化,识别影响因子和异常原因。使用场景:当用户需要分析业务指标变化、查找指标下降原因、进行指标根因分析时。
---

# 核心业务指标分析逻辑

分析指标1指标及其关联指标的周环比变化,识别影响因子和可能原因。

## 分析流程

### 1. 获取指标1周环比数据

调用 `scripts/query_demo.py` 获取指标1指标的周环比数据:

python scripts/query_demo.py 指标1 --json

返回数据包含:
- 今日日期、上周同期日期
- 今日指标值、上周同期指标值
- 变化率(周环比)

### 2. 判断是否需要深入分析

**如果指标1环比下降**,继续执行以下步骤:

#### 2.1 获取关联指标数据

调用 `scripts/query_demo.py` 获取以下指标的周环比数据:
- 指标1
- 指标2
- 指标3
- 指标4
- 指标5

python scripts/query_demo.py <指标名称> --json

#### 2.2 分析影响因子

对比各指标的变化率,识别:
- 哪个指标对指标1影响较大(变化率最显著)
- 指标间的关联关系
- 可能的原因分析

### 3. 获取节假日信息(可选)

如需考虑节假日因素,调用 `scripts/holiday.py`:

python scripts/holiday.py

返回指定日期范围内的工作日和节假日信息,用于判断指标变化是否受节假日影响。

### 4. 进行OLAP下钻分析(可选)

对于影响较大的指标,可进行OLAP下钻分析以识别细分维度的贡献度:

参考 `OLAP下钻分析` 技能,使用该技能进行多维度下钻分析。

## 支持的指标

- 指标1(核心指标)
- 指标2
- 指标3
- 指标4
- 指标5

## 分析输出建议

分析结果应包含:

1. **核心指标状态**
- 指标1周环比变化
- 变化趋势(上升/下降/持平)

2. **关联指标分析**(如指标1下降)
- 各关联指标的周环比数据
- 影响因子排序
- 指标关联性分析

3. **可能原因**
- 基于数据变化的可能原因推断
- 节假日因素(如适用)
- 其他外部因素考虑

4. **下钻分析结果**(如适用)
- 细分维度的贡献度分析
- 关键维度识别

## 使用示例

**示例:分析指标1下降原因**

# 1. 获取指标1数据
python scripts/query_demo.py 指标1 --json

# 2. 如果下降,获取关联指标
python scripts/query_demo.py 指标2 --json
python scripts/query_demo.py 指标3 --json
python scripts/query_demo.py 指标4 --json
python scripts/query_demo.py 指标5 --json

# 3. 检查节假日因素
python scripts/holiday.py

# 4. 对影响最大的指标进行下钻分析(如指标2)

展望


Agent Skills 并非一个简单的“新功能”,而是从单体架构到微服务,从过程式脚本到组件化框架这一转型的标准化接口。它的核心价值,在于为“模型智能”的工程化落地,定义了一种可组合、可复用的 “能力单元” 设计范式。


未来的竞争维度将发生根本变化:问题将从 “你的单体模型巨石应用)性能多强?” ,转向 “你的‘包管理器’(Skill 生态)有多丰富、可靠和高效?” 。拥有最强大模型,但缺乏易用、标准化能力接口的公司,可能会像拥有最强单核CPU但缺乏操作系统和软件生态的厂商一样,在真正的应用战场中失势。


Skill 规范,正是在尝试为 AI 世界定义那个至关重要的 “操作系统层”和“包管理协议”。


 



产研团队:李鸣、王海艳、包恒彬、黄燮聪


笔者介绍:李鸣|大数据专家。曾任职于腾讯,从事地图渲染SDK研发、智能网联云平台后端开发,现就职于货拉拉,搭建了基于供需关系的调价平台、异动监测系统、GPT基础能力建设等项目,目前专注于大数据应用赋能。



作者:货拉拉技术
来源:juejin.cn/post/7597776334065516596
收起阅读 »

极限挑战,全球化新篇!吉利银河成为首个不补能直通北冰洋的国产新能源品牌

2月9日,随着冬测车队抵达极夜与严寒交织的北极腹地,吉利银河完成了一场跨越瑞典与挪威,总里程超1000公里不补能的冰雪全场景长测,这是自主品牌第一次完成跨洲际、远征北冰洋的冬测挑战。其中,吉利银河混动家族V900、M9、星舰7 EM-i、星耀8、A7、星耀6等...
继续阅读 »

2月9日,随着冬测车队抵达极夜与严寒交织的北极腹地,吉利银河完成了一场跨越瑞典与挪威,总里程超1000公里不补能的冰雪全场景长测,这是自主品牌第一次完成跨洲际、远征北冰洋的冬测挑战。其中,吉利银河混动家族V900、M9、星舰7 EM-i、星耀8、A7、星耀6等车型,完成不补能直通北冰洋千里长测。品牌全系车型,更在Colmis试验场完成亚欧大陆纬度最高测试场挑战。与此同时,即将上市的吉利银河战舰等三款新车,以及醇氢动力车型也在北欧完成了首次冬标测试。

不同于国内常规冬测,吉利银河此次特意选址北欧高纬度地区,直面当地高湿度、黑冰路况等国内难以复刻的极端自然环境,精准模拟全球高纬度寒冷区域的真实用车场景。高湿极寒环境与极夜低光照条件的双重叠加,对车辆密封防雾、整车热管理系统,以及智驾传感器的环境感知能力提出了严苛的要求。而这正是吉利银河远征北欧的核心初衷,以最极致的自然实验室,全面校验品牌“全球研发、全球验证”的体系化技术能力。

此次不补能直通北冰洋的极限挑战,不仅彰显了吉利银河车型续航能力的硬核实力,更印证了品牌全维度的综合性能优势。本次冬测中,吉利银河围绕北欧极寒工况开展多维度专项验证,重点测试冬季ADAS(冬季主动安全功能测试)功能、高湿极寒环境适配性,车顶行李箱适配、冰雪路面牵引载重测试及冬季钉胎、AWD策略雪地标定,同时将低附着路面操控稳定性、整车热管理效率,以及冻融循环下的车辆环境耐久与密封可靠性纳入测试,实现车型极寒工况适配性与稳定性的全方位验证。

硬核表现的背后,是吉利银河针对性的技术攻坚与优化。品牌对四驱车型扭矩管理进行深度调校,实现毫秒级扭矩调节与全地形适配,有效破解冰雪路面打滑难题;新能源适配技术、欧7法规提前验证方案与除霜系统优化,确保车辆在极端低温、高湿度环境下的排放合规、车窗无霜与采暖高效;而甲醇动力车型更是实现超低温冷启动,成功攻克行业内的极寒启动痛点。此次北极冬测的圆满完成,为吉利银河的技术迭代升级、筑牢全球化产品根基奠定了坚实基础。

目前,吉利银河已形成了覆盖极寒到高温、沙漠到赛道的全场景测试能力,包括杭州湾中央试验基地、海南湿热试验基地、黑河高寒试验基地、吐鲁番高温试验基地、欧洲试验基地、云南高原试验基地共六大试验基地,构建起“研发-验证-迭代”的全球化闭环,满足全球车型开发与验证需要。吉利全球试验基地还将在国内外陆续建成16个全球试验基地,打造全天候、全地形、全场景的产品开发与验证能力。

对新能源技术极限的主动验证、对研发不遗余力的大力投入,是吉利银河超越竞品、持续领跑的核心底气。站在2026年1月销量助力吉利汽车登顶中国市场销冠的新起点上,吉利银河正以无畏的闯劲,将“全球研发、全球验证、全域安全”的理念转化为每一位用户触手可及的高价值体验。在全球化征程上,吉利银河已然在北极冰原上留下了属于中国力量的深刻印记,这不仅是“百万银河”时代开启后的首场全球化技术“亮剑”,更是中国汽车工业在世界顶级试炼场上完成的一次壮丽远征。未来,在世界的每一个角落,吉利银河都将像“本地车”一样安全、可靠,以世界的标准,打造世界级好车!

收起阅读 »

告别 “人工内卷”!绿盟科技风云卫AI安全能力平台成果重磅发布

当传统安全运营困于能效低下、规则误报、建模费时等多重桎梏,网络安全行业迫切需要一场技术革新。2月5日,绿盟科技召开风云卫AI安全能力平台线上成果发布会。本次发布会通过主题演讲与实践分享,展示了AI在安全运营场景、数据分类分级、安全风险检测三大核心方向的关键突破...
继续阅读 »

当传统安全运营困于能效低下、规则误报、建模费时等多重桎梏,网络安全行业迫切需要一场技术革新。2月5日,绿盟科技召开风云卫AI安全能力平台线上成果发布会。本次发布会通过主题演讲与实践分享,展示了AI在安全运营场景、数据分类分级、安全风险检测三大核心方向的关键突破。依托自研安全垂域大模型——风云卫AI安全能力平台,绿盟科技正重构数智时代下的安全运营范式,推动网络安全从“被动防御”迈向“主动智能”。


一、自主闭环,重塑AI安全运营新范式

面对突发漏洞与安全事件,传统依赖人工分析的响应模式往往捉襟见肘,而AI技术的加持,让漏洞识别到策略优化的全流程效率得到显著提升。支撑这一突破的,是绿盟科技构建的7×24小时“AI+”安全运营中心(SOC)。绿盟科技运营专家唐宗星带来AI赋能安全运营实战场景分享。

一、从辅助研判自主闭环的三阶段进化

绿盟科技在本次发布会上首次详细披露了其安全运营中心AI能力演进的清晰路线图:

第一阶段:能力构建与精度攻坚

初期实践中,绿盟科技将AI能力嵌入安全运营平台辅助分析师进行安全事件的研判分析,所有结果经过专家审核与修正。此阶段的核心挑战在于提升大语言模型输出的准确性与一致性,绿盟科技深入探索了监督微调、知识库构建、提示工程优化等关键技术,积累了宝贵的数据处理与模型迭代经验以及AI辅助研判能效和准确度的度量体系。

第二阶段:任务移交与效率跃升

随着AI模型可靠性的提升,绿盟科技开始将单步分析、敏感内容确认等标准化任务逐步移交AI全自动处理。目前,在安全分析领域,AI已能稳定准确地完成约80%的日常工作,人工只需要专注于复杂跨业务事件的调查以及策略优化。

第三阶段(当前):能效优化与自主闭环

面对AI广泛应用后带来的资源消耗新挑战,绿盟科技探索如何在保障效果的前提下,显著提升AI的能效,从而将释放的资源投入到更高级的自主智能能力建设上。发布会重点分享的正是应对这一命题的创新性解决方案。

二、创新实践分享:两大场景破解运营效率瓶颈

实践一:威胁分析场景的AI优化AI能效革命

在威胁分析流程中,海量告警在经过初步过滤后,仍需AI进行兜底研判,导致大量简单、重复的告警消耗了宝贵的计算资源。绿盟科技创新性地引入了策略优化智能体,该智能体能够持续学习历史分析与研判结果,自动归纳总结规律,并主动生成两类优化策略:一是精准的业务白名单策略,二是可自动执行的研判分析剧本。这些策略经人工审核确认后,即可部署生效,从而将大量重复性工作前置化、策略化处理。

“这本质上是一种‘AI优化AI’的元管理思路”,唐宗星解释道,“我们让一个专门的AI去学习我们主力分析AI的工作成果,并不断优化它的工作环境与前置规则。这让我们在威胁分析场景中,能够用生成一份策略的消耗,替代成百上千次重复的研判调用,实现了能效的倍增。”

实践二:漏洞应急场景的数字管家驱动复杂流程闭环

面对涉及情报收集、资产匹配、攻击回溯、风险分级、报告通知的复杂应急流程,风云卫平台展现了其多智能体协同调度的强大能力。平台可像“安全管家”一样,自动串联并驱动全流程:从智能收集漏洞情报、精准定位受影响资产,到自动回溯攻击痕迹、依据风险等级生成差异化报告并定向通知,最终实现高风险客户的优先处置闭环。这一过程极大减少了人工串联与协调工作,将应急响应效率提升至新的高度。

基于此,绿盟科技描绘了面向未来的 “安全数字人”愿景——一个能够理解不同角色(如工程师、管理者、业务人员)自然语言需求,自主调度后台智能体,交付个性化解决方案的智能交互入口,旨在彻底跨越安全运营的“最后一公里”。


二、智能感知,打造数据分类分级新引擎

面对数据爆炸式增长带来的效率低下、标准不一、风险暗藏三大挑战,绿盟科技数据安全产品经理王晓丹介绍了绿盟科技推出的AI分类分级智能体。该产品具备六大核心能力,标志着数据治理从依赖固定规则迈入智能感知时代。

开放的智能引擎绿盟分类分级智能体支持灵活调度多个主流厂商的大语言模型,并通过直观的可视化拖拽编排界面,让用户能够自定义和优化识别策略与工作流。

全模态数据覆盖:除了文本,还支持对文档、图像、音视频等多模态非结构化数据的深度内容解析与敏感数据识别能力,实现数据资产的全域可视。

深度的行业知识:基于RAG知识库技术,内置多年积累的经过实战验证的覆盖金融、运营商、能源、政府、教育、医疗等十余个重点行业的专属特征库,在相同测试条件下,使得识别结果更准确。

自动化的策略生成:能够根据识别出的敏感数据与上下文,自动推荐或生成高可用的数据标签与防护规则,大幅提升策略部署效率。

“优化模板”与“直接执行”双轨并行的成熟路径“优化模板”旨在从根本上构建统一、标准化的分类分级体系,其成果可独立运行,赋能更广泛场景;“直接执行”则用于快速响应单次紧急任务。两者结合,形成从“应急治标”到“体系治本”的良性循环。

一体化的治理闭环:识别出的敏感数据、分类分级结果可无缝对接数据脱敏、访问控制、审计监控等安全产品,形成"发现—识别—保护—监控"的主动治理闭环。

绿盟分类分级智能体模版的开发周期从周级别降低至小时级别,初始化模版准确率提升20%以上,整体人工复核投入时间降低50%以上。绿盟科技将持续深耕AI分类分级核心技术,围绕多模态理解与人机协同两大方向进行持续迭代,打造更智能、更易用、更精准的数据分类分级系统。


三、小时级建模,解锁业务风险检测新路径

数据安全需紧密结合具体业务来进行行为风险分析,而传统UEBA依赖人工建模且无法理解业务,导致成本高昂且效率低下。绿盟科技数据安全产品经理查文静带来绿盟数据安全风险检测解决方案,该方案通过AI大模型赋能数据安全事件检测,将数据建模缩短至“小时级别”,贴合具体的业务特性和数据特性,具备良好的准确性和实战效果。

破除壁垒:AI大模型让UEBA开箱即用

传统UEBA方案如同一头“数据巨兽”,需要海量的历史数据进行长达数月的基线建模,并依赖专业的安全专家编写复杂的关联规则,实施门槛与运维成本极高。绿盟科技数据安全平台从根本上改变了这一现状。其AI大模型经过大量安全、数据安全的预训练,企业无需庞大的专家团队,通过AI即可轻松生成这一先进的检测能力,真正实现数据安全的高门槛技术普惠化。

懂你业务:告别告警风暴,实现精准打击

海量误报是传统UEBA运营失败的根源,这是因为传统模型无法理解业务的复杂性与多样性。AI革新后的数据安全风险平台,凭借大模型的深度语义理解能力,首次让UEBA拥有了“业务常识”。它不再仅仅是统计“下载量超过1GB”这类冰冷的数字,而是能理解“研发人员下载代码仓库”与“销售人员下载客户列表”在业务上下文中的本质区别。通过与正常行为基线进行对比,能够精准区分合理的高权限操作与恶意的违规行为,将告警更贴合具体的业务和数据使用场景。

洞见于微:构建活的多维实体画像

绿盟数据安全平台在AI赋能下, 不仅为每个用户和关键数据资产绘制实体画像,更结合了其行为基线,数据访问特征正乃至群体关联。它不再是一个简单的仪表盘统计信息,而是一个关于实体的动态画像。这种基于深度画像的洞察力,使得绿盟数据安全平台能够发现那些隐藏在正常数据访问行为之下、精心伪装的“慢速渗透”和“蚂蚁搬家”式的数据窃取,防患于未然。

本次发布会的成功举办,全面展示了绿盟科技风云卫AI安全能力平台“场景化智能”的核心理念与“数智焕新”的实践成果。平台通过将AI智能深度植入安全产品架构,不仅解决了当前安全运营、数据治理与风险检测中的效率与精度瓶颈,更指明了未来安全体系自主进化、精准赋能业务的发展方向。未来,绿盟科技将继续深耕AI安全大模型领域,并结合持续不断地创新实践,为千行百业夯实面向数字未来的智能化安全底座。


收起阅读 »

聚焦AI应用实战,第2届PolarDB数据库创新设计赛精彩收官!

日前,“2025全国大学生计算机系统能力大赛——第2届PolarDB数据库创新设计赛”(以下简称“大赛”)总决赛暨颁奖典礼圆满落幕。本届大赛吸引了来自全国300余所高校的3500余支队伍报名参赛,经过3个月的激烈角逐,最终共计21支参赛队伍脱颖而出,成功晋级决...
继续阅读 »

日前,“2025全国大学生计算机系统能力大赛——第2届PolarDB数据库创新设计赛”(以下简称“大赛”)总决赛暨颁奖典礼圆满落幕。本届大赛吸引了来自全国300余所高校的3500余支队伍报名参赛,经过3个月的激烈角逐,最终共计21支参赛队伍脱颖而出,成功晋级决赛线下答辩环节,并在为期2天的高强度现场答辩与综合评审中展开技术比拼。

经过大赛特邀专家评审团的严格把关和现场答辩后,来自北京理工大学的“BIT-Vector”队伍凭借卓越的创新思维和精湛的技术实力摘得桂冠,荣获一等奖及5万元奖金。

一等奖队伍与颁奖嘉宾合影,并分享从初赛到决赛的成长

第2届PolarDB数据库创新设计赛为全国普通高校学科竞赛排行榜入选赛事,由全国高等学校计算机教育研究会、系统能力培养研究专家组、系统能力培养研究项目发起高校主办,阿里云计算有限公司、AMD、浙江大学承办的全国性数据库大赛。本届赛事以向量计算为背景,聚焦AI技术与数据库的创新融合,要求参赛者基于PolarDB for PostgreSQL版数据库,优化向量索引构建与向量查询性能,充分检验学生的系统能力与工程实践水平。

大赛决赛答辩采用“双轮答辩制”,参赛队伍与专家评审齐聚一堂。比赛组委会特邀了15位来自高校及企业界的专业评委老师进行评审,并对作品的技术深度、工程实现能力和创新水平进行了严格考量。在答辩中,参赛选手展现出了卓越的技术能力,来自北京理工大学的“BIT-Vector”团队通过精心设计的算法和工程实践,实现了PolarDB数据库向量性能的显著提升;来自华中科技大学、杭州电子科技大学等高校的本科生团队与硕博研究生同场竞技,取得了不俗的成绩。

赛事自2025年10月28日启动以来,吸引全国300余所高校的3513支队伍报名参赛,覆盖本科生至博士研究生全学段。决赛阶段的参赛队伍需要基于大赛提供的 PolarDB-PG 与 pgvector 源代码为技术底座进行开发,通过修改代码、调整数据库参数、改变向量索引构建参数与向量查询参数等方式,优化向量索引构建与向量查询的性能。参赛队伍需提交源代码、设计与实现文档等资料,全面考察参赛作品的技术深度、工程实现能力与创新水平,充分展现了各参赛队伍运用综合知识进行数据库系统部署、源码修改与性能调优的能力。

大赛共评选出一等奖获奖队伍 1 支、二等奖获奖队伍 3 支、三等奖获奖队伍 6 支、优胜奖团队 11 支。

二等奖队伍与颁奖嘉宾合影

三等奖队伍与颁奖嘉宾合影

除参赛队伍的团队奖项外,本届大赛还同步评选并颁发了“优秀指导老师奖”、“优秀组织奖”等,以表彰在赛事中做出突出贡献的教师和院校,共有26位指导老师和9所高校获奖。来自中国人民大学的卞昊穹老师和来自武汉大学的彭煜玮老师获得大赛“特殊贡献奖”,感谢各高校及老师们对赛事组织工作的长期支持。

“优秀指导老师奖”获奖代表与颁奖嘉宾合影

“特殊贡献奖”获得者与颁奖嘉宾合影

“优秀组织奖”获奖代表与颁奖嘉宾合影

大赛颁奖典礼由主办方代表——全国大学生计算机系统能力大赛秘书处秘书长周浩杰主持,华东师范大学教授、CCF数据库专委会主任周傲英与阿里云副总裁、市场营销部总裁、CCF常务理事刘湘雯等重量级嘉宾出席并发表致辞,共同见证了这一荣耀时刻。与会嘉宾们表达了对大赛成功举办的高度肯定和支持,面向高校与产业分享了PolarDB在AI与数据库融合方向的最新实践,并对未来国产数据库领域的发展寄予厚望。

全国大学生计算机系统能力大赛秘书处秘书长周浩杰发表开场致辞

阿里云副总裁、市场营销部总裁、CCF常务理事刘湘雯

阿里云副总裁、市场营销部总裁、CCF常务理事刘湘雯表示:“自2018年发起PolarDB数据库大赛以来,赛事影响力持续提升。展望未来,我们坚信:数据人才是AI时代最宝贵的战略资源。AI的终极目标不是取代人类,而是增强人类——而能驾驭数据、理解AI、连接技术与业务的复合型人才,正是实现这一愿景的核心力量。阿里云将持续开放平台能力,深化产教融合,与高校、科研机构和广大师生携手,在数据库与AI融合的前沿,共同探索人才培养的新路径、新标准、新生态。”

华东师范大学教授、CCF数据库专委会主任周傲英

“爱上数据库有 100 个理由。数据库有历史、有硬核、有哲学、有使命、有未来……。在AI Agent时代,数据库正从“事务中枢”进化为“数据赋能平台”,其核心使命是支撑智能体直接、安全、高效地调用基础数据。”,华东师范大学教授、CCF数据库专委会主任周傲英在颁奖典礼分享道。

阿里云资深副总裁、数据库产品事业部负责人李飞飞寄语大赛选手:“我们基于云原生数据库PolarDB设计赛题的初衷,是为了让广大学子有机会深入学习和实践数据库核心技术,特别是当前热门的AI与向量计算方向。此次大赛吸引了来自全国各地高校的广泛参与,展现了我国高校学子在数据库与AI交叉领域的巨大潜力与创新活力,印证了数据库能力教育已从理念走向规模化实践。大赛的成功举办不仅能促进高校与产业的深度协同,更有更多的优秀人才脱颖而出,为数据库技术的未来发展注入新动能。”

正如周傲英教授所言,这是一个“因为相信,所以看见”的时代。值此第2届PolarDB数据库创新设计赛圆满落幕之际,我们衷心祝愿:“愿每一行参赛代码都扎根真实场景,愿每一位数据库新锐都怀抱敬畏深耕内核。”期待参赛选手们带着在PolarDB大赛过程中锤炼出的系统能力持续打磨,成为中国基础软件自主创新的中坚力量。也期待与更多高校携手,通过开源与赛事共建,推动教育进步,激励科研创新。

收起阅读 »

深度复刻小米AI官网交互动画

web
近日在使用小米AI大模型MIMO时,被其顶部的透视跟随动画深深吸引,移步官网( mimo.xiaomi.com/zh/ ) 效果演示 1. 交互梳理 初始状态底部有浅色水印,且水印奇数行和偶数行有错位 初始状态中间文字为黑色的汉字 鼠标移入后,会在以鼠标为...
继续阅读 »

近日在使用小米AI大模型MIMO时,被其顶部的透视跟随动画深深吸引,移步官网( mimo.xiaomi.com/zh/


效果演示


效果图.gif


1. 交互梳理



  1. 初始状态底部有浅色水印,且水印奇数行和偶数行有错位

  2. 初始状态中间文字为黑色的汉字

  3. 鼠标移入后,会在以鼠标为中心形成一个黑色圆形,黑色圆中有第二种背景水印,且水印依旧奇数行和偶数行有错位

  4. 鼠标移动到中间汉字部分,会有白色英文显示

  5. 鼠标迅速移动时,会根据鼠标移动轨迹有一个拉伸椭圆跟随,然后恢复成圆形的动画效果


现在基于这个交互的拆解,逐步来复刻交互效果


2. 组件结构与DOM设计


2.1 模板结构


采用「静态底层+动态上层」的双层视觉结构,通过CSS绝对定位实现图层叠加,既保证初始状态的视觉完整性,又能让交互效果精准作用于上层,不干扰底层基础展示。两层分工明确,具体如下:


图层类名内容功能
底层.z-1中文标题 "你好,世界!" 和灰色 "HELLO" 文字矩阵静态背景展示
上层.z-2英文标题 "Hello , World!" 和白色 "HELLO" 文字矩阵鼠标交互时的动态效果层

2.2 核心 DOM 结构


<div class="container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousemove="onMouseMove">
<!-- 底层内容 -->
<div class="z-1">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-1">你好,世界!</h1>

<!-- 上层交互内容 -->
<div class="z-2" :style="{ 'clip-path': circleClipPath }">
<div class="hidden-div">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-2">Hello , World!</h1>
</div>
</div>


关键说明:hidden-div用于包裹上层文字矩阵,配合.z-2的定位规则,确保遮罩效果精准覆盖;两层文字矩阵尺寸一致,保证视觉对齐,增强透视沉浸感。



3. 技术实现


3.1 核心功能模块


3.1.1 轨迹点系统


轨迹点系统是实现平滑鼠标跟随效果的核心,通过维护6个轨迹点的位置信息,创建出具有弹性延迟的跟随动画。


// 轨迹点系统 
const trailSystem = ref({
targetX: 0,
targetY: 0,
trailPoints: Array(6).fill(null).map(() => ({ x: 0, y: 0 })),
animationId: 0,
isInside: false
});



设计思路:6个轨迹点是兼顾流畅度与性能的平衡值——点太少则拖尾效果不明显,点太多则增加计算开销,配合递减阻尼系数,实现“头快尾慢”的自然跟随。



3.1.2 动态 Clip-Path 计算


通过计算鼠标位置和轨迹点的关系,动态生成 clip-path CSS 属性值,实现跟随鼠标的圆形/椭圆形遮罩效果。


// 计算clip-path值
const circleClipPath = computed(() => {
if (!showCircle.value) {
return 'circle(0px at -300px -300px)'; // 完全隐藏状态
}

// 复制轨迹系统数据进行计算
const system = JSON.parse(JSON.stringify(trailSystem.value));

// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? system.targetX : system.trailPoints[t - 1].x;
const prevY = t === 0 ? system.targetY : system.trailPoints[t - 1].y;
const damping = 0.7 - 0.04 * t; // 阻尼系数,后面的点移动更慢

const deltaX = prevX - system.trailPoints[t].x;
const deltaY = prevY - system.trailPoints[t].y;

// 平滑插值
system.trailPoints[t].x += deltaX * damping;
system.trailPoints[t].y += deltaY * damping;
}

// 获取第一个点(头部)和最后一个点(尾部)
const head = system.trailPoints[0];
const tail = system.trailPoints[5];

const diffX = head.x - tail.x;
const diffY = head.y - tail.y;
const distance = Math.sqrt(diffX * diffX + diffY * diffY);

let clipPathValue = '';

if (distance < 10) { // 如果距离很近,显示圆形
clipPathValue = `circle(200px at ${head.x}px ${head.y}px)`;
} else {
// 创建椭圆形的polygon,连接头尾两点
const angle = Math.atan2(diffY, diffX); // 连接角度
const points = [];

// 从头部开始,画半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle - Math.PI / 2 + Math.PI * i / 30;
const x = head.x + 200 * Math.cos(theta);
const y = head.y + 200 * Math.sin(theta);
points.push(`${x}px ${y}px`);
}

// 从尾部开始,画另半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle + Math.PI / 2 + Math.PI * i / 30;
const x = tail.x + 200 * Math.cos(theta);
const y = tail.y + 200 * Math.sin(theta);
points.push(`${x}px ${y}px`);
}

clipPathValue = `polygon(${points.join(', ')})`;
}

return clipPathValue;
});


3.1.3 鼠标事件处理


实现了完整的鼠标交互逻辑,包括鼠标进入、离开和移动时的状态管理和动画控制。


事件处理函数功能
mouseenteronMouseEnter激活交互效果,初始化轨迹点
mouseleaveonMouseLeave停用交互效果,重置轨迹点
mousemoveonMouseMove更新目标点位置,驱动动画

4. 技术亮点


4.1 轨迹点系统算法


核心原理:使用6个轨迹点,每个点跟随前一个点移动,并应用不同的阻尼系数,实现平滑的拖尾效果。


技术优势



  • 实现了自然的物理运动效果,比简单的线性跟随更具视觉吸引力

  • 通过阻尼系数的递减,创建出层次感和深度感

  • 算法复杂度低,性能消耗小,适合实时交互场景


4.2 动态 Clip-Path 技术


核心原理:利用CSS clip-path属性的动态特性,结合轨迹点位置计算,实时生成不规则遮罩,替代Canvas/SVG的图形绘制方案,用更轻量化的方式实现复杂视觉效果。


技术优势



  • 无依赖轻量化:无需引入任何图形库,纯CSS+JS即可实现,减少项目依赖体积,降低集成成本

  • 平滑过渡无卡顿:通过数值插值计算,实现圆形与椭圆形遮罩的无缝切换,无帧断裂感,视觉连贯性强

  • 渲染性能优化:配合 will-change: clip-path 提示浏览器,提前分配渲染资源,减少重排重绘,提升动画流畅度


5. 性能优化



  1. 渲染性能



    • 使用 will-change: clip-path 提示浏览器优化渲染

    • 合理使用 Vue 的响应式系统,避免不必要的重计算



  2. 事件处理



    • 仅在鼠标在容器内时更新目标点位置,减少计算量

    • 鼠标离开时停止动画,释放资源



  3. 动画性能



    • 使用 requestAnimationFrame 实现流畅的动画效果

    • 鼠标离开时取消动画帧请求,避免内存泄漏




6. 总结与扩展


本次复刻的小米MiMo透视动画,核心价值在于“用简单技术组合实现高级视觉效果”——无需复杂图形库,仅依托Vue3响应式能力与CSS clip-path属性,就能打造出兼具质感与性能的交互组件。其核心亮点可概括为三点:



  • 交互创新:轨迹点系统与动态clip-path结合,打破传统静态标题的交互边界,带来自然流畅的鼠标跟随体验

  • 视觉精致:双层文字矩阵的分层设计,配合遮罩形变,营造出兼具深度感与品牌性的视觉效果

  • 性能可控:轻量化技术方案+多维度优化策略,在保证视觉效果的同时,兼顾页面性能与可维护性


扩展方向


该组件的实现思路可灵活迁移至其他场景:



  • 弹窗过渡动画:将clip-path遮罩用于弹窗进入/退出效果,实现不规则形状的过渡动画。

  • 滚动动效:结合滚动事件替换鼠标事件,实现页面滚动时的元素透视跟随效果。

  • 移动端适配:增加触摸事件支持,将鼠标交互替换为触摸滑动,适配移动端场景。


完整代码


<template>
<div class="hero-container" @mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousemove="onMouseMove">
<div class="z-1">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-1">你好,世界</h1>

<!-- 第二个div,鼠标移入后需要显示的内容,通过clip-path:circle(0px at -300px -300px)达到隐藏效果 -->
<div class="z-2" :style="{ 'clip-path': circleClipPath }">
<div class="hidden-div">
<div class="line" v-for="line in 13">
<span class="line-item" v-for="item in 13">HELLO</span>
</div>
</div>
<h1 class="title-2">HELLO , World</h1>
</div>
</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const showCircle = ref(false)
const containerRef = ref(null)

const trailSystem = ref({
targetX: 0,
targetY: 0,
trailPoints: Array(6)
.fill(null)
.map(() => ({ x: 0, y: 0 })),
animationId: 0,
isInside: false,
})

const circleClipPath = computed(() => {
if (!showCircle.value) {
return 'circle(0px at -300px -300px)'
}

// 复制轨迹系统数据进行计算
const system = JSON.parse(JSON.stringify(trailSystem.value))

// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? system.targetX : system.trailPoints[t - 1].x
const prevY = t === 0 ? system.targetY : system.trailPoints[t - 1].y
const damping = 0.7 - 0.04 * t // 阻尼系数,后面的点移动更慢

const deltaX = prevX - system.trailPoints[t].x
const deltaY = prevY - system.trailPoints[t].y

// 平滑插值
system.trailPoints[t].x += deltaX * damping
system.trailPoints[t].y += deltaY * damping
}

// 获取第一个点(头部)和最后一个点(尾部)
const head = system.trailPoints[0]
const tail = system.trailPoints[5]

const diffX = head.x - tail.x
const diffY = head.y - tail.y
const distance = Math.sqrt(diffX * diffX + diffY * diffY)

let clipPathValue = ''

if (distance < 10) {
// 如果距离很近,显示圆形
clipPathValue = `circle(200px at ${head.x}px ${head.y}px)`
} else {
// 创建椭圆形的polygon,连接头尾两点
const angle = Math.atan2(diffY, diffX) // 连接角度
const points = []

// 从头部开始,画半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle - Math.PI / 2 + (Math.PI * i) / 30
const x = head.x + 200 * Math.cos(theta)
const y = head.y + 200 * Math.sin(theta)
points.push(`${x}px ${y}px`)
}

// 从尾部开始,画另半个椭圆
for (let i = 0; i <= 30; i++) {
const theta = angle + Math.PI / 2 + (Math.PI * i) / 30
const x = tail.x + 200 * Math.cos(theta)
const y = tail.y + 200 * Math.sin(theta)
points.push(`${x}px ${y}px`)
}

clipPathValue = `polygon(${points.join(', ')})`
}

return clipPathValue
})

// 动画循环函数
const animate = () => {
if (showCircle.value) {
// 更新轨迹点
for (let t = 0; t < 6; t++) {
const prevX = t === 0 ? trailSystem.value.targetX : trailSystem.value.trailPoints[t - 1].x
const prevY = t === 0 ? trailSystem.value.targetY : trailSystem.value.trailPoints[t - 1].y
const damping = 0.7 - 0.04 * t // 阻尼系数,后面的点移动更慢

const deltaX = prevX - trailSystem.value.trailPoints[t].x
const deltaY = prevY - trailSystem.value.trailPoints[t].y

// 平滑插值
trailSystem.value.trailPoints[t].x += deltaX * damping
trailSystem.value.trailPoints[t].y += deltaY * damping
}

// 请求下一帧
trailSystem.value.animationId = requestAnimationFrame(animate)
}
}

const onMouseEnter = (event) => {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

showCircle.value = true

// 初始化目标位置和轨迹点
trailSystem.value.targetX = x
trailSystem.value.targetY = y
trailSystem.value.isInside = true

// 初始化所有轨迹点到当前位置
for (let i = 0; i < 6; i++) {
trailSystem.value.trailPoints[i] = { x, y }
}

// 开始动画
if (!trailSystem.value.animationId) {
trailSystem.value.animationId = requestAnimationFrame(animate)
}
}

const onMouseLeave = (event) => {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

showCircle.value = false
trailSystem.value.isInside = false

// 将目标点移出容器边界,使轨迹点逐渐拉回
let targetX = x
let targetY = y

if (x <= 0) targetX = -400
else if (x >= rect.width) targetX = rect.width + 400

if (y <= 0) targetY = -400
else if (y >= rect.height) targetY = rect.height + 400

trailSystem.value.targetX = targetX
trailSystem.value.targetY = targetY

// 停止动画
if (trailSystem.value.animationId) {
cancelAnimationFrame(trailSystem.value.animationId)
trailSystem.value.animationId = 0
}
}

const onMouseMove = (event) => {
if (showCircle.value) {
const container = event.currentTarget
const rect = container.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top

trailSystem.value.targetX = x
trailSystem.value.targetY = y
}
}
</script>

<style scoped>
.hero-container {
cursor: crosshair;
background: #faf7f5;
border-bottom: 1px solid #000;
justify-content: center;
align-items: center;
width: 100%;
height: 500px;
display: flex;
position: relative;
overflow: hidden;
}

.z-1 {
pointer-events: auto;
-webkit-user-select: none;
user-select: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}

.z-1 .line {
display: flex;
align-items: center;
white-space: nowrap;
color: #0000000d;
letter-spacing: 0.3em;
flex-wrap: nowrap;
font-size: 52px;
font-weight: 700;
line-height: 1.6;
display: flex;
}

.z-1 .line-item {
cursor: default;
flex-shrink: 0;
margin-right: 0.6em;
transition:
color 0.3s,
text-shadow 0.3s;
font-family: inherit !important;
}

.z-1 .line:nth-child(odd) {
margin-left: -2em;
background-color: rgb(245, 235, 228);
}

.title-1 {
z-index: 1;
color: #000;
letter-spacing: 0.02em;
text-align: center;
margin: 0;
font-size: 72px;
font-weight: 700;
}

.z-2 {
pointer-events: none;
z-index: 10;
will-change: clip-path;
background: #000;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
}

.z-2 .hidden-div {
pointer-events: none;
-webkit-user-select: none;
user-select: none;
flex-direction: column;
justify-content: flex-start;
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}

.z-2 .hidden-div .line {
white-space: nowrap;
color: #ffffff1f;
letter-spacing: 0.3em;
flex-wrap: nowrap;
font-size: 32px;
font-weight: 700;
line-height: 1.6;
display: flex;
}

.z-2 .hidden-div .line:nth-child(odd) {
margin-left: -0.5em;
}

.title-2 {
font-size: 72px;
color: #fff;
letter-spacing: 0.02em;
text-align: center;
white-space: nowrap;
margin: 0;
font-size: 72px;
font-weight: 700;
}
</style>



小米的前端一直很牛,非常有创意,我也通过F12学习源码体会到了新的思路,希望大家也多多关注小米和小米的技术~



作者:SmartNorth
来源:juejin.cn/post/7598005428258340927
收起阅读 »

Tailwind CSS都更新到4.0了,你还在抵触吗?

web
Tailwind CSS的体量Tailwind CSS有多火爆呢?几组数据告诉你?一组数据告诉你 Tailwind CSS 有多受欢迎:github 86.1K的 Star, 足以证明它的受欢迎程度。NPM 周下载量已突破 1000 万, 前端开发者的不二之选...
继续阅读 »

Tailwind CSS的体量

Tailwind CSS有多火爆呢?

几组数据告诉你?

image.png

image.png

一组数据告诉你 Tailwind CSS 有多受欢迎:

  1. github 86.1K的 Star, 足以证明它的受欢迎程度。
  2. NPM 周下载量已突破 1000 万, 前端开发者的不二之选
  3. 被无数大公司采用,如 GitHub、Vercel、Laravel 等。
  4. 被很多框架和打包工具推荐,如Vite,Nuxt,React等

从数据上看,Tailwind CSS 已经成为前端开发的主流选择之一

原子化CSS

什么是原子化CSS

原子化 CSS 是一种 CSS 架构,它提倡使用高度可复用的小类名,每个类名通常只控制单一的样式属性。例如:

<div class="text-red-500 font-bold p-4 text-[14px]">Hello Tailwinddiv>

其中:

  • text-red-500: 代表文字颜色
  • font-bold: 代表加粗
  • p-4: 代表内边距
  • text-[14px]: 代表字体大小为14px

这种方式避免了传统 CSS 中复杂的层叠规则,让样式控制更加直观。

原子化CSS和传统CSS的区别

image.png

说了这么一通,我相信用过的都说“真香”,用过一次后就离不开了。

关键是没用过的呢?是不是心里还在嘀咕。别急,正餐来了!

Tailwind CSS宝藏库

为什么说Tailwind CSS是一个宝藏,因为你担忧和抵触的地方,Tailwind CSS都给你解决了

类名难记

你可能在担忧,我是不是每次用都要查文档呢?那么多css好不容易记住了,现在又让我再学一遍?

答案是:不用。完全和你使用css一样简单。只需要记住几个关键字,智能提示帮你搞定

image.png

VS Code插件 - Tailwind CSS IntelliSense

HTML又长又乱

首先,不可否认,将所有的类名整合到html中,会让你的html变得比较长。但是,当你写的代码又长又乱的时候,你就要停下来想想

  1. 是否违背了创作者的初衷
  2. 架构是否设计不合理

为此,我们简单分析一下,到底是人的问题还是工具的问题。根据以上2点,分析一下你的HTML为什么又长又乱?

  • 因为太长导致太乱

    没有合并之前,你的代码可能是这样的

    class="flex justify-center items-center">
    clsss="bg-blue-500 text-white py-2 px-4 rounded">提交

    合并后是这样的

    .flex-center {
    @apply flex justify-center items-center;
    }
    .btn-submit {
    @apply bg-blue-500 text-white py-2 px-4 rounded;
    }

    class="flex-center">
    clsss="btn-submit">提交

    现在是不是很清晰了呢?

    我敢说,只要你使用@apply合并类名,时刻记着复用样式,你的HMLT至少减少1/3,甚至也可以写出像诗一样的代码

  • 因为没有分组和顺序性导致太乱

    没有顺序和分组的书写,是这样的

    <div class="p-2 font-bold text-[14px] mt-4 color-[#333333] bg-white">Hello world!div>

    想到哪写到哪,会让你的代码一眼望上去比较乱,时间长了,一眼看上去很难维护...

    下面我们就着手解决这2个问题

    1. 类排序

    使用 Prettier 进行类排序(Class sorting with Prettier)

    Tailwind CSS 维护了一个官方 Prettier 插件,它会自动按照我们的 推荐的类顺序 对你的类进行排序。

    使用插件后,代码这样的

    <div class="bg-white color-[#333333] mt-4 p-2 text-[14px] font-bold">Hello world!div>

    现在是不是清晰很多了呢?

    不过还不够

    1. 分组

我们根据样式进行的类别分组,比如颜色,字体,定位,间距等等,每个类别一行,这样你写出的代码会清晰无比

<div class="
bg-white
color-[#333333]
mt-
4 p-2
text-
[14px] font-bold">
Hello world!div>

现在代码是不是清晰的多了

全局类名

不用担心公共类的问题,@apply帮你搞定。

使用 @apply 合并类,前面已经讲过了,就不展开了

样式冲突

也许你还在担心tailwind 的 class 名和我已有的 class 冲突了咋办?我怎么处理兼容问题

别担心,给你的类名加个前缀prefix就搞定了

@import "tailwindcss" prefix(tw);

<div class="tw:flex tw:bg-red-500 tw:hover:bg-red-600"> div>

拥抱Tailwind CSS

Tailwind CSS为什么受到追捧

  1. 再也不用忍受css上下切换的痛苦了
  2. 再也不用花时间去取语义化类名了

    不用纠结container, wrapper, box等被使用的问题后,如何起名的问题了

  3. 为了加权重,不断的加父级类名,甚至!important,永远不知道哪个样式起作用了。冗长的css让项目很难维护!
  4. 简单完成伪类、伪元素、媒体查询等变体的书写

image.png

Tailwind CSS、PrimeFlex、UnoCSS评测

在CSS工具类框架中,除了Tailwind CSS之外,还有其他很多工具类。如PrimeFlex和UnoCSS,它们各有特点,下面我简单的评测一下

  • PrimeFlex: 生态系统较小,多适用于Prime生态,如PrimevVue,PrimeReact。样式和较Tailwind CSS低。只能构建起简单样式框架。最让我吐槽的是,样式竟然用!important。你想替换某个属性,麻烦程度想骂人!

image.png

  • UnoCSS: 未构建良好的生态系统,多用于自定义规则和项目优化

总结

TailwindCSS 已经成为前端开发的趋势之一,随着4.0 版本的发布,它的性能更强大、使用更方便。如果你还在抵触,不妨试试看,它可能会彻底改变你的 CSS 编写方式!


作者:高志小鹏鹏
来源:juejin.cn/post/7480734875723415552

收起阅读 »

为什么没人走后门当程序员?

最近刷 X 乎时看到这样一个耐人寻味的的讨论话题,浏览量超 170w,参与讨论的同学也好多。 问题描述是这样的: “为什么没人走后门当程序员?” 我认真浏览了一圈,心里五味杂陈。 在许多人眼中,程序员是一个高薪的职业。然而,即便程序员们拿着如此令人羡慕的高薪...
继续阅读 »

最近刷 X 乎时看到这样一个耐人寻味的的讨论话题,浏览量超 170w,参与讨论的同学也好多。


问题描述是这样的:


“为什么没人走后门当程序员?”



我认真浏览了一圈,心里五味杂陈。


在许多人眼中,程序员是一个高薪的职业。然而,即便程序员们拿着如此令人羡慕的高薪,尽管互联网行业如此火热,但却几乎很少听说有人说走后门想进去。


其实这事情一点也不难理解,这得先从程序员工作的本质说起。


因为程序员这个职业,从根子上来说压根就不靠后门吃饭。


而且程序员这行,恰恰是最混不了日子的,它要求你持续学习,跟上技术迭代,解决一个个具体而棘手的问题。


编程是一个实实在在的技术活,当你的代码运行不起来,它就是运行不起来,你写的系统有漏洞,它就会在某个深夜悄然崩溃,这种刚性特质就决定了程序员这个岗位无法容忍滥竽充数者。


而程序员的门槛,是技术,是能力,走后门也写不出一行能跑通的代码。


退一步说,哪怕就算你真靠后门挤进了公司,项目一上来,分分钟就会露馅。


那些想走后门的人,大概率是想找一个稳当、轻松、有人脉资源的工作。但反思程序员这行,是这样吗?好……好像哪个也不沾边吧……


所以没人走后门干程序员,不是因为这行没前途,而是因为它太实在、太透明、太难伪装。


这是一份必须用真本事去交换的职业,关系在这里,价值被迅速稀释到近乎为零。


另外大家往往有种误解或者说错觉,总觉得程序员赚得多就是香,而实际却忽略了这个高薪背后所付出的代价,这一切都是来源于高强度脑力劳动和长时间脑力付出所带来的回报。


再者,互联网行业的本质是工程化与扁平化。在这个体系里,你是谁、认识谁、从哪来,其实并不太重要,没人会关注你这个,英雄不问出处。


重要的是,你能不能解决问题,能不能为项目创造价值。


所以,当我们回过头来再看,为什么没人走后门干程序员这个问题,其实本身就蕴含着一种误解。它预设了程序员是一个好差事,一个可以让人躺着赚钱的美差。


但事实上,程序员是一份需要真才实学、持续奋斗、直面挑战的工作。你付出多少努力,掌握多少技能,最终都会在你的代码和收入上得到真实的反馈。


当然,这里还有一点需要反思的是:


该说不说,程序员行业的这种去关系化特质,其实某一角度来说也带来了一些副产品。


比方说,技术至上的工作文化有时会导致个体沟通能力的忽视,对硬技能的过度强调可能让软技能的发展有所滞后,另外代码世界的非黑即白有时候也会让人忽略了现实世界的复杂灰度。


这些其实都是程序员文化中值得反思和平衡的地方。


有一说一,其实很多代码之外的东西对现如今的生存也很重要,因为思维如果不开阔出来的话,路可能就会越走越窄了。


其实很多程序员在年龄大了之后越来越焦虑的一个重要原因就是因为生存技能太过单一了,所以千万不要给自己设限,不要把目光仅仅聚集在自己的一亩三分地上,还是要多培养一些其他方面的一些软实力,会很有帮助。


不知道大家有没有看过《软技能》那两本书,讲的就是代码之外的一些软技能和经验,里面提到了很多有关职场的分析,自我提高的一些路径,个人的持续学习和成长,甚至包括像理财、健身、时间管理、心态调整等等。


有意识地去关注这方面东西的原因在于可以帮助自己把思维给开阔出来,毕竟很多时候有必要跳出来看问题,这时候这些软技能往往就能发挥作用了。


另外,程序员作为一个有个性的创造性群体要专注精进技术这本身没错,但是职场毕竟也是一个充满人情世故的江湖,所以掌握一些通用的职场规则、沟通技巧,甚至是向上管理的艺术,这对于程序员来说也是十分有必要的。


仰望星空,脚踏实地,埋头赶路的同时也不要忘记时常抬头看看周围的环境和机会。


那关于这个问题,你的看法是什么呢,如果有不同的见解,也欢迎一起来分享交流~



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7599581204859715610
收起阅读 »

华为擎云发布HarmonyOS 6 MDM能力,赋能政企安全高效数字化转型

2025年11月29日,华为擎云 HarmonyOS 6 MDM(移动终端管理)能力交流会于深圳圆满举行。本次会议汇聚了业界主流EMM应用厂商代表与移动安全领域的行业专家,共同围绕移动终端安全的技术与应用进行交流探讨,分享不同行业信息化建设的成功经验。鸿蒙生态...
继续阅读 »

2025年11月29日,华为擎云 HarmonyOS 6 MDM(移动终端管理)能力交流会于深圳圆满举行。本次会议汇聚了业界主流EMM应用厂商代表与移动安全领域的行业专家,共同围绕移动终端安全的技术与应用进行交流探讨,分享不同行业信息化建设的成功经验。

鸿蒙生态当前已跨越了阶段性里程碑,搭载HarmonyOS 5、HarmonyOS 6的终端设备已突破2700万台,且以每天超过10万台的速度增长,应用市场可搜索应用及元服务数量突破30万,覆盖用户生活和工作的方方面面。作为面向全场景未来的新生态,鸿蒙将持续携手伙伴依托OS的底层创新能力,为用户带来更安全、更高效、更智能的数字生态体验。

政企单位因行业属性特殊、数据敏感度高,在移动办公场景中催生出大量区别于消费端用户的场景化需求,MDM能力由此成为政企数字化转型的重要支撑。作为本次交流会的核心议题,华为终端各领域专家系统阐述了鸿蒙MDM的架构设计理念、核心技术特性,并针对云侧与端侧的具体开发路径提供了详细指导。

鸿蒙MDM能力作为面向政企需求的核心中间件,一端连接用户需求,一端连接OS底层能力,采用生态开放,分层设计的理念,通过开放通信、文件管理、多媒体、UI等多个子系统能力,赋能专业EMM伙伴,结合伙伴对行业场景的深刻洞察与丰富交付经验,共同为政企客户打造覆盖全场景的移动安全解决方案。

在接口特性方面,此次Harmony OS 6上开放了300+的系统API,覆盖设备管理、通信管理、网络配置、应用保活、应用分发、KIOSK展台模式等关键领域。接口的丰富性与精细化,直接提升了鸿蒙设备管理的精准度与安全防护等级,为复杂政企场景提供了更灵活的适配可能。

针对政企客户关注的设备识别与授权安全问题,华为擎云依托HEM(HUAWEI Enterprise Manager)平台构建了全流程自动部署体系,实现企业客户、项目信息、MDM应用与设备SN的多元绑定,让设备使用者、权限分配等信息一目了然。该体系支持企业对鸿蒙设备进行远程快速配置,新设备开箱连网后即可自动完成办公环境配置,涵盖系统参数配置、网络环境配置、管理策略下发、企业应用预装等全环节。

在政企核心需求的应用分发领域,HEM平台整合应用市场AG、MDM核心能力及ISV伙伴资源,构建适配不同应用类型、不同网络环境的分场景应用分发方案,全面满足鸿蒙设备在各类政企办公场景中的应用分发与生命周期管理需求。

同时,HEM平台提供了高效灵活的开箱定制能力。通过云端可视化操作界面,企业可针对设备资源配置、开机向导流程、桌面布局设计、系统参数设定等进行快速定制,助力企业实现敏捷化、多样化、批量化的设备定制交付。

除传统政企COPE(企业配发设备)终端管理模式外,针对消费电子、生物医药等高端制造领域中“人员临时性进入涉密厂区”的特殊场景,HarmonyOS 6基于RBAC(基于角色的访问控制)权限管理理念,从系统底层定义不同管理者角色与权限边界,设计了BDA管理模式。该模式支持BYOD(自带设备)模式的临时管理方案,既保障企业核心信息安全,又兼顾员工个人设备隐私,破解了传统BYOD管理的安全与隐私矛盾。

在移动终端管理领域,华为擎云已拥有十年技术沉淀与实践积累,深度洞察政企客户需求痛点,积累了海量行业项目的交付经验。未来,华为擎云将持续以技术创新为核心驱动力,助力政企客户加速数字化转型,提供更安全、高效、灵活、开放的鸿蒙设备管理解决方案,与生态伙伴在一起,共建共享鸿蒙新世界。

收起阅读 »

裁员为什么先裁技术人员?网友一针见血

最近逛职场社区的时候,刷到一个职场话题,老生常谈了,但是每次参与讨论的同学都好多。 这个问题问得比较扎心: “为什么有些企业的裁员首先从技术人员开始?” 关于这个问题,网上有一个被讨论很多的比喻: “房子都盖起来了,还需要工人么?” 有一说一,这个比喻虽然刺...
继续阅读 »

最近逛职场社区的时候,刷到一个职场话题,老生常谈了,但是每次参与讨论的同学都好多。


这个问题问得比较扎心:


“为什么有些企业的裁员首先从技术人员开始?”



关于这个问题,网上有一个被讨论很多的比喻:


“房子都盖起来了,还需要工人么?”


有一说一,这个比喻虽然刺耳,但却非常形象地揭示了某些企业的用人逻辑,尤其在某些非技术驱动型的公司里


在某些非技术驱动的公司(比如传统企业转型、或者业务模式成型的公司),其实技术部门很多时候是会被视为「成本中心」,而非「利润中心」的,我相信在这类企业待过的技术同学肯定是深有体会。


就像盖大楼一样,公司需要做一个 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。



作者:CodeSheep
来源:juejin.cn/post/7579499567869116466
收起阅读 »

我的2025:做项目、跑副业、见人、奔波、搬家、维权、再回上海

2025 年,如果让我用一句话定性,我会说:我在变强,也在重新选择自己的人生结构。这一年我做了很多事,多到我一度不敢回头看。表面上看,我一直在“往前”:写内容、做项目、跑副业、见人、奔波、搬家、维权、再回上海。可只有我自己知道,真正折磨人的不是忙,是那种反复出...
继续阅读 »

2025 年,如果让我用一句话定性,我会说:我在变强,也在重新选择自己的人生结构。

这一年我做了很多事,多到我一度不敢回头看。表面上看,我一直在“往前”:写内容、做项目、跑副业、见人、奔波、搬家、维权、再回上海。可只有我自己知道,真正折磨人的不是忙,是那种反复出现的瞬间——我突然意识到:我不是在冲,我是在被生活推着跑

我确实拿到了一些结果。内容有过爆的时刻,小红书涨了粉,视频剪辑从手忙脚乱到慢慢顺手,有人开始来问我、信我、甚至愿意付费。那段时间我有一种很罕见的笃定:只要我肯学、肯磨,很多事我都能做成。那种“我好像什么都能做”的自信,在这一年里反复把我从低谷里托起来。

但同样是这一年,我也交了一笔不轻的学费。不是钱那么简单,更是对人、对机会、对“看起来很美”的承诺的那种天真。我曾因为信任做了一个很重的决定;也曾在北京的夜里把事情一条条摊开算清楚,最后发现不是值不值的问题,而是我再拖下去,就会把自己耗到没样子。

我不想把这篇复盘写成流水账,也不想写成鸡汤。我只想把这一年最真实的部分摆出来:我怎么一点点变强,怎么被现实教育,怎么止损、怎么维权、怎么把自己从废墟里捡回来。


1. 我开始把表达当成一件正事

三月开始,我把很多注意力放在“说清楚”这件事上。

以前我也输出,但更多像随手记录。2025 年不一样,我开始认真经营表达:每天钻研、每天尝试、每天复盘。公众号有了更明确的正反馈,有几篇文章突然被推起来,评论区开始出现陌生人的共鸣,后台也开始有人来问我问题。那种感觉很奇妙——我写的东西不再只属于我自己,它开始进入别人的生活。

今年使用最多的AI IDE 就是Trae,也参加了第一期的Trae 征文活动,获得了第二名,Trae给我来了很多成长。

今年在Trae 方面的实践:


2.自己维护了一个Trae 生态资源合集

image.png


3.基于Trae 开发的第一个APP

Trae 刚出来Claude模型时,连夜测评它的能力,当时花了5个小时搞出一个App,项目并且还开源了 image.png


  1. 基于Trae 设计的原型稿

tickhaijun.github.io_Podcast_.png

我也开始碰视频。说实话,一开始很狼狈:剪一个一分钟的视频,要花我两三个小时。卡点、配乐、字幕、节奏,哪一样都不像看起来那么简单。我一度怀疑是不是我不适合,但又不甘心。我知道这是一块我之前没尝试过的能力,一旦练出来,就是新的路。

基于Trae还做了原型还原设计稿,没想到视频火了 image.png


图片

这一段给我的礼物,是一种更稳定的自信:很多事看起来复杂,只要拆开、一步步做,就会变得可控。


2. 我把想法做成了作品通过Vibe Coding

五月到八月,我进入了一种“手里有活”的状态。

图片

从懵懂到落地:记录我们第一次成功将大模型“塞”进业务的曲折历程

图片

年初做了自己第一款AI应用

图片图片

那段时间我做了很多作品,也开源了不少东西。说白了,就是把想法从脑子里拎出来,做成一个能跑、能看、能用、能被别人理解的东西。

与此同时,我也给团队做了多次分享,讲我最近在做什么、怎么做、踩了什么坑、怎么绕开。

图片

中间有两次机会我印象很深:一次是来自一家很大的咨询公司,一次是出海方向的远程邀请。它们都挺诱人,但我当时都拒绝了。原因很简单:我知道我还没准备好。能力没到那个厚度、心态没到那个稳定度,我不想靠运气上去,然后靠硬扛撑住。

也有一些小小的惊喜:有人买了我做的东西,虽然数量不算多,但足够让我确认——我做的东西不是自嗨,是真的有人需要。更重要的是,越来越多的网友通过我的内容认识我,联系我,问我问题。

那几个月我最大的收获不是“做了多少”,而是一个更朴素的结论:想法不值钱,做出来才值钱。


3. 有人愿意为我的能力买单

九月到十一月,我的副业开始像一门“正经事”。

咨询变多了。有的是临时问答,有的是更系统的陪跑。我接了三份陪跑,也因此认识了几位很投缘的朋友,都是山西的。我们聊项目、聊选择、聊怎么把事情做成,也聊怎么在现实里不把自己弄丢。

这份关系很珍贵。它不是那种互相吹捧的热闹,而是我能明显感到:对方因为我的建议少走了弯路,事情推进得更顺,而我也因为对方的反馈变得更坚定。那种“我真的帮到了人”的成就感,比数字更实在。

我也在这一段第一次更清晰地看到我的位置:我不是只能埋头做项目的人,我还可以把经验讲清楚,把复杂拆简单,把别人卡住的点指出来。这是一种能力,也是一种责任感。

这一段让我相信:靠自己攒出来的口碑,慢,但稳。


4. 我重新确认了“钱该花在哪”

国庆我和家人自驾出去玩了一趟。

图片

风很大,天很高,羊肉很香。我们在草原上待了一天,我给父母安排了越野卡丁车,让他们在草地上跑一圈;我和姐姐骑了马,笑得像回到小时候。那几天我很放松,甚至有点恍惚——原来我努力这么久,最想换来的并不是某个头衔,而是这种“我能让他们开心”的底气。

我以前对花钱很谨慎,总觉得要攒着、要算计回报。可当我把钱花在家人身上,那种舒坦很直接:不需要证明,不需要解释,花出去就是一种“我扛得住了”的确认。


5. 去北京一趟,我把胆子捡了回来

图片

十月我去北京参加了一个活动,也算第一次为了这类事出远门。2026年,多输出AI,多参加活动。

现场人很多,节奏很快,信息密得让人喘不过气。那天我最大的感受,不是见了什么产品,而是突然明白:机会真的会从我身边走过去,走过去就没了。很多时候不是我不够好,是我不敢站出来,或者我下意识觉得“我还不够格”。

图片

去天津路上,熟悉的感觉

我也去了天津,见了老朋友老李。我们聊了一整天,我帮他搬运整理食品,他带我吃了天津菜,甚至让我体验了一把保时捷 911。最后他把我送到机场。

图片

那一天让我很感慨:这个世界其实很大,也很活,我不能总把自己困在“怕麻烦、怕尴尬、怕出丑”的情绪里。

图片

今年我也买了不少书,也读了不少书。《亲密关系》《认知驱动》《纳瓦尔宝典》……它们没有给我标准答案,但给了我更清醒的视角:我要对自己的情绪负责,对自己的选择负责,对自己的长期负责。


6. 我相信过他,也因此完成了一次祛魅

十一月底,我做了一个很重的决定:离职,去北京试一次。

图片

这件事我并不是冲动。相反,我想了将近一个月。朋友“他”邀请过我三次,前两次我都拒绝了。第三次创始人亲自找我,话说得很漂亮,未来画得很大,而我也确实在那个阶段渴望一次更大的空间。再加上对“他”的信任,我最终点了头。

图片

离开前,我做了一件我很想做的事:把爸爸接到上海。那是他第一次来上海,也是他第一次坐飞机。我去接他的时候,他脸上的喜悦藏不住。我带他逛了很多地方,拍了很多照片。送他去机场那天,我心里很踏实——那种成就感,不来自任何评价,只来自“我能带他看世界”的瞬间。

今年我也给妈妈买了新手机,她之前那部太卡了。再小的事情,落在父母身上都是实在的改变。

然后我去了北京。

现实很快给了我一记闷棍。之前说的和实际差太多太多。我会在很短时间内发现:有些话只是话,有些承诺只是情绪,有些“格局”只是包装。我不想在这里写具体细节,但我可以写结论——这次经历让我完成了一次祛魅:对人、对所谓“机会”、对“看起来很美”的未来。

我也更清楚了一件事:我并不是不能吃苦,我是不愿意把我的尊严和时间押在不靠谱的人和不靠谱的事上。


7. 我救了三只狗,也被这座城市的善意接住

这一年我救了三只狗。

图片

第一只是中华田园犬,在公园遇到的。它很瘦,眼神怯,但又不躲人。

第二只是边牧,在公司附近,它更像是走丢的孩子,聪明又无助。

图片

第三只是阿拉斯加,在豫园附近,体型很大,却一点安全感都没有。

我喜欢狗。遇见它们的时候,我很难装作没看见。我做的事其实也不复杂:拍照、发帖、联系、筛选领养人、把信息对齐清楚,然后送它们去新家。

这件事最打动我的,不是我多善良,而是我发现:大城市真的有很多愿意伸手的人。我发出求助,真的会有人回应。我以为我在救它们,其实在某些时刻,是这些善意在把我从疲惫里接住。


8. 一笔沉没成本:止损、维权、和不再委屈自己

十二月初,北京给了我最硬的一课。

图片

我在北京待了十来天,一直住酒店。对方之前说会报销,但后来什么都没有。入职前一天我找了房子,租房费用、中介费用、再加上各种奔波成本,堆起来是一笔不小的支出。更糟的是:入职第一天我就通过另一位同样处境的人了解到了真实情况;再加上“他”下班后说的一些话,我很快确定——这里不是我该待的地方。

图片

那一刻最难的其实不是离开,而是面对沉没成本。我已经付出那么多,我会本能地想“再忍忍,再等等”。但我很庆幸,那天我没骗自己。我选择止损。

随之而来的就是维权。房子我没入住,合同日期也没开始,但管家很无赖,甚至带着恐吓。那种“我讲理他就耍赖”的感觉很恶心。我一开始也很烦,后来干脆不和她废话,直接走流程,通过 12315 协调,拿回了一部分。理论上可以拿回更多,但要继续耗时间精力,我当时选择到此为止。

这一段时间,让家里也没少操心,哎....

我最想写给自己的不是“钱亏了”,而是一个更重要的结论:以后遇到不公,我不再用委屈换和平。该维权就维权,该翻脸就翻脸。


9. 回到上海:我把自己一点点拉回正轨

图片

十二月中旬我回到了上海。

图片

收拾好家里的工位

那段时间我能量很低。不是累,是一种被现实撞过之后的钝。我会怀疑自己、怀疑判断、怀疑信任,甚至怀疑“是不是我太敏感了”。但生活不会等我缓过来,它只会继续往前。

图片

我做的第一件事是把我自己拉回正常:吃饭、睡觉、见朋友。后来我和老耿去了杭州散心。城市很安静,走在路上我突然发现:风还是一样吹,灯还是一样亮,我不会因为受挫就失去明天。

我慢慢控住场了。把生活拉回正轨了。也把那句最重要的话重新捡回来——我在变强,也在重新选择自己的人生结构。


最后

回头看 2025 年,我最大的变化不是“我做了多少”,而是我对人生结构的要求变高了

以前我会把努力当成答案。现在我更在意:这份努力能不能沉淀,能不能让我拥有更多选择权。以前我遇到烂事会先忍,想着“算了”。但北京那一段之后我更确定:委屈不会换来尊重,只会换来下一次更大的代价。该止损就止损,该维权就维权——哪怕沉没成本已经砸下去,我也要把自己从泥里拎出来。

这一年我也完成了一次祛魅:
对“机会”的祛魅,对“关系”的祛魅,对“画出来的未来”的祛魅。
我开始相信一句话:真正值得的机会,不会只靠嘴说;真正可靠的人,也不会只靠情绪绑架。

如果说 2025 年教会了我什么,我觉得是三件事:

第一,能力不是拿来逞强的,是拿来兜底的。
我在最狼狈的时候,靠自己把局面稳住了。那种“我能扛住”的底气,是真的。

第二,钱花在家人身上,会变成一种很踏实的成就感。
我以前以为成就感来自外界认可,今年我更确定:来自父母的笑、来自家人的安心、来自“我可以照顾他们”。

第三,善意是会流动的。
我帮过人,也被人帮过;我救过狗,也被陌生人的热心治愈过。世界不全是烂人,但我得学会识别,学会筛选,学会保护自己。

2026 年我不想再喊口号了。我只想做三件更具体的事:

  • 把一条能长期跑的主线做出来:让输出、作品和服务真正形成稳定的节奏,而不是靠运气起伏。
  • 给信任立规矩:合作要有边界,承诺要能落地,任何决定都要留后手。
  • 把家放进计划里:不是“有空再说”,而是本来就该排在前面。

2025 年没有把我推到高处,但它把我从幻觉里拽出来了。
我依然会往前走,只是以后我更在乎的不是速度,而是方向;不是热闹,而是结构。

我在变强,也在重新选择自己的人生结构。

就复盘到这吧,用时6个小时,该休息了....

希望2026年一切顺利! 


作者:程序员海军
来源:juejin.cn/post/7595147871939493934
收起阅读 »

Skill 真香!5 分钟帮女友制作一款塔罗牌 APP

最近发现一个 AI 提效神器 ——Skills,用它配合 Cursor 开发,我仅用 5 分钟就帮女友做出了一款塔罗牌 H5 APP!在说如何操作之前,我们先大概了解下 Skills 的原理 一、Skills的核心内涵与技术构成 (一)本质界定 Skills ...
继续阅读 »

最近发现一个 AI 提效神器 ——Skills,用它配合 Cursor 开发,我仅用 5 分钟就帮女友做出了一款塔罗牌 H5 APP!在说如何操作之前,我们先大概了解下 Skills 的原理


一、Skills的核心内涵与技术构成


(一)本质界定


Skills 可以理解为给 AI Agent 定制的「专业技能包」,把特定领域的 SOP、操作逻辑封装成可复用的模块,让 AI 能精准掌握某类专业能力,核心目标是实现领域知识与操作流程的标准化传递,使AI Agent按需获取特定场景专业能力。其本质是包含元数据、指令集、辅助资源的结构化知识单元,通过规范化封装将分散专业经验转化为AI Agent可理解执行的“行业SOP能力包”,让 AI 从‘只会调用工具’变成‘懂专业逻辑的执行者


(二)技术构成要素


完整Skill体系由三大核心模块构成,形成闭环能力传递机制:



  1. 元数据模块:以SKILL.md或meta.json为载体,涵盖技能名称、适用场景等关键信息约 100 个字符(Token),核心功能是实现技能快速识别与匹配,为AI Agent任务初始化阶段的加载决策提供依据。

  2. 指令集模块:以instructions.md为核心载体,包含操作标准流程(SOP)、决策逻辑等专业规范,是领域知识的结构化转化成果,明确AI Agent执行任务的步骤与判断依据。

  3. 辅助资源模块:可选扩展组件,涵盖脚本代码、案例库等资源,为AI Agent提供直接技术支撑,实现知识与工具融合,提升执行效率与结果一致性。


和传统的函数调用、API 集成相比,Skills 的核心优势是:不只是 “告诉 AI 能做什么”,更是 “教会 AI 怎么做”,让 AI 理解专业逻辑而非机械执行


二、Skills与传统Prompt Engineering的技术差异


从技术范式看,Skills与传统Prompt Engineering存在本质区别,核心差异体现在知识传递的效率、灵活性与可扩展性上:



  1. 知识封装:传统为“一次性灌输”,冗余且复用性差;Skills为“模块化封装”,一次创建可跨场景复用,降低冗余成本。

  2. 上下文效率:传统一次性加载所有规则,占用大量令牌且易信息过载;Skills按需加载,提升效率并支持多技能集成。

  3. 任务处理:传统面对复杂任务易逻辑断裂,无法整合外部资源;Skills支持多技能组合调用,实现复杂任务全流程转化。

  4. 知识迭代:传统更新需逐一修改提示词,维护成本高;Skills为独立模块设计,更新成本低且关联任务可同步受益。


上述差异决定Skills更适配复杂专业场景,可破解传统Prompt Engineering规模化、标准化应用的瓶颈。


三、渐进式披露:Skills的核心技术创新


(一)技术原理与实现机制


Skills能在不增加上下文负担的前提下支撑多复杂技能掌握,核心在于“按需加载”的渐进式披露(Progressive Disclosure)设计,将技能加载分为三阶段,实现知识传递与上下文消耗的动态平衡:



  1. 发现阶段(启动初始化):仅加载所有Skills元数据(约100个令牌/个),构建“技能清单”明确能力边界,最小化初始化上下文负担。

  2. 激活阶段(任务匹配时):匹配任务后加载对应技能指令集,获取操作规范,实现精准加载并避免无关知识干扰。

  3. 执行阶段(过程按需加载):动态加载辅助资源,进一步优化上下文利用效率。


(二)技术优势与价值


渐进式披露机制使Skills具备三大核心优势:



  1. 降低令牌消耗:分阶段加载避免资源浪费,支持单次对话集成数十个技能,降低运行成本。

  2. 提升执行准确性:聚焦相关知识组件,减少干扰,强化核心逻辑执行精度。

  3. 增强扩展性:模块化设计支持灵活集成新知识,无需重构系统,适配领域知识快速迭代。


四、Cursor Skills


介绍完 Skills 是什么之后,我将使用的是 Cursor 作为我的开发工具。先说明一下,最开始只有 Claude Code 支持 Skills、Codex 紧随其后,口味自己选。



好消息是,Cursor 的 Skills 机制采用了与 Claude Code 几乎完全一致的 SKILL.md 格式。这意味着,你完全不需要从头编写,可以直接将 Claude Code 的生态资源迁移到 Cursor。



(一)Cursor 设置


因为 Cursor 刚支持不久,并且是 Beta 才能使用,所以要进行下面操作



Agent Skills 仅在 Nightly 更新渠道中可用。

要切换更新渠道,打开 Cursor 设置( Cmd+Shift+J ),选择 Beta,然后将更新渠道设置为 Nightly。更新完成后,你可能需要重新启动 Cursor。 如下图所示




要启用或禁用 Agent Skills:



  1. 打开 Cursor Settings → Rules

  2. 找到 Import Settings 部分

  3. 切换 Agent Skills 开关将其开启或关闭 如下图所示


(二)复制 Claude Skills


然后我们直接去 Anthropic 官方维护的开源仓库 anthropics/skills,里面提供了大量经过验证的 Skill 范例,涵盖了创意设计、开发技术、文档处理等多个领域。


你可以访问 github.com/anthropics/… 查看完整列表。以下是这次用到的 Skills


Frontend Design:这是一个专门用于提升前端设计质量的技能。它包含了一套完整的 UI 设计原则(排版、色彩、布局)


然后我们直接把 Skills 里面的 .claude/skills/frontend-design 到当前项目文件下,如图:



模型和模式如下图



提示词如下,不一定非得用我的。


使用 Skill front-design。我要做一个 H5 ,功能是一个塔罗牌。

你是一名经验丰富的产品设计专家和资深前端专家,擅长UI构图与前端页面还原。现在请你帮我完成这个塔罗牌应用的 UI/UX 原型图设计。请输出一个包含所有设计页面的完整HTML文件,用于展示完整UI界面。

注意:生成代码的时候请一步一步执行,避免单步任务过大,时间执行过长

然后 Cursor 会自动学习 Skills,并输出代码



然后就漫长的等待之后,Cursor 会自动做一个需求技术文档,然后会一步一步的实现出来,这时候可以去喝杯茶,再去上个厕所!


最终输出了 5 个页面



  1. 首页 (Home)

  2. 每日抽牌页 (Daily Draw)

  3. 牌阵占卜页 (Spread Reading)

  4. 塔罗百科页 (Encyclopedia)

  5. 占卜历史页 (History)


最终效果如下,整体效果看起来,完全是一个成熟的前端工程师的水准,甚至还带有过渡动画和背景效。因为掘金无法上传视频,欢迎私信我找我要或者关注我:


image.png


扩展阅读


因为 Cursor 目前仅在 Nightly 版本上才可以使用 Skills。如果担心切换此模式会引发意想不到的情况,可以使用另一种方案


OpenSkills 是一个开源的通用技能加载器。



  • 完全兼容:它原生支持 Anthropic 官方 Skill 格式,可以直接使用 Claude 官方市场或社区开发的技能。

  • 桥梁作用:它通过简单的命令行操作,将这些技能转换为 Cursor、Windsurf 等工具可识别的配置(AGENTS.md),从而让 Cursor 具备与 Claude Code 同等的“思考”与“技能调用”能力。


作者:乘风gg
来源:juejin.cn/post/7593362940071903284
收起阅读 »

秒懂 Headless:为什么现在的软件都要“去头”?

web
简单来说, “Headless”(无头) 在软件开发中指的是:只有逻辑(后端/内核),没有预设界面(前端/GUI) 的软件架构模式。 这里的“Head(头)”比喻的是用户界面(UI/GUI) ,“Body(身体)”比喻的是核心业务逻辑或引擎。 Headless...
继续阅读 »

简单来说, “Headless”(无头) 在软件开发中指的是:只有逻辑(后端/内核),没有预设界面(前端/GUI) 的软件架构模式。


这里的“Head(头)”比喻的是用户界面(UI/GUI) ,“Body(身体)”比喻的是核心业务逻辑或引擎


Headless = 砍掉自带的 UI,只给你提供 API 或核心逻辑,让你自己去画界面。




1. 核心概念图解


想象一下 “传统的软件”(比如 Word):它像一家堂食餐厅。你有厨房(逻辑),也有固定的桌椅板凳和装修风格(UI)。你必须在它提供的环境里吃饭,无法改变装修。


“Headless 软件”:它像一个中央厨房(外卖工厂)。它只负责做饭(逻辑),不提供桌椅(UI)。



  • 你想把菜送到五星级酒店摆盘(Web 端高级定制 UI)?可以。

  • 你想把菜送到路边摊(手机 App)?可以。

  • 你想把菜送到自动售货机(小程序)?也可以。




2. 具体例子


A. 无头浏览器 (Headless Browser)



  • 传统的浏览器(如 Chrome): 你打开它,能看到窗口、地址栏、渲染出来的网页,你能用鼠标点击。

  • 无头浏览器(如 Puppeteer, Playwright):



    • 定义: 它是浏览器内核(Chrome/Webkit),但没有可视化的窗口。它在后台(命令行/服务器)运行。

    • 怎么用? 你写代码控制它:“打开百度 -> 输入关键词 -> 截图”。

    • 有什么用?



      1. 自动化测试: 模拟用户点击,快速跑通几千个测试用例,不需要真的弹出一千个窗口。

      2. 爬虫: 爬取那些需要 JS 渲染的复杂网页。

      3. 生成截图/PDF: 在服务器端把网页渲染成图片或 PDF 报告。






B. 无头编辑器 (Headless Editor)



  • 传统的编辑器(如 CKEditor 旧版, Quill):



    • 你引入它,它就自带一套“加粗、斜体、插入图片”的工具栏,自带一套 CSS 样式。

    • 缺点: 如果设计师说“把工具栏按钮变成圆形的,而且要悬浮在文字上方”,你就要疯狂覆盖它的默认 CSS,非常痛苦。



  • 无头编辑器(如 Tiptap, Plate, Slate.js):



    • 定义: 它只提供文字处理的核心逻辑(比如:选中文本、按下 Ctrl+B 变粗体、撤销重做逻辑)。它不提供任何 UI(没有工具栏,没有按钮)。

    • 怎么用? 你需要自己写一个 <button>,自己写样式,然后调用它的 API editor.toggleBold()

    • 有什么用? 你可以完全自由地定制编辑器的长相。比如 Notion、飞书文档那种高度定制的 UI,必须用无头编辑器开发。






3. 还有哪些常见的 Headless?


除了浏览器和编辑器,现在的开发趋势中还有:


C. 无头组件库 (Headless UI)



  • 例子: Radix UI, Headless UI, React Aria。

  • 解释: 以前我们用 Ant Design 或 Bootstrap,按钮长什么样是库定好的。Headless UI 库只提供交互逻辑(比如下拉菜单怎么打开,键盘怎么选,无障碍怎么读),不提供任何 CSS

  • 好处: 完美配合 Tailwind CSS,长相由你完全控制。


D. 无头 CMS (Headless CMS)



  • 例子: Strapi, Contentful。

  • 解释: 以前用 WordPress,后台管理内容,前台页面也是 WordPress 生成的(耦合)。Headless CMS 只提供后台管理API

  • 好处: 你的一份内容(API)可以同时发给 网站、App、智能手表、甚至冰箱屏幕。




总结:为什么现在流行 Headless?


虽然 Headless 意味着开发者要写更多的代码(因为要自己画 UI),但它解决了现代开发最大的痛点:定制化


维度传统 (Coupled)Headless (无头)
上手难度 (开箱即用) (需要自己写 UI)
自由度 (改样式很难)极高 (随心所欲)
适用场景快速做个标准后台像 Notion/Figma 这种需要极致体验的产品
比喻方便面 (有面有调料包,味道固定)生鲜面条 (只有面,想做炸酱面还是汤面随你)

一句话总结:Headless 就是把“业务逻辑”和“界面表现”彻底分家,让你拥有无限的 UI 定制权。


作者:皮蛋小精灵
来源:juejin.cn/post/7582118218649288730
收起阅读 »

为了让 iframe 支持 keepAlive,我连夜写了个 kframe

web
前几天收到一个bug,说是后台管理系统每次切换标签栏后,xxx内容区自动刷新,操作进度也丢失了,给用户造成很大困扰。作为结丹期修士的我自然不容允许此等存在,开干! 问题分析 该后台管理系统基于 vue3 全家桶开发,多标签页模式,标签页默认 KeepAli...
继续阅读 »

前几天收到一个bug,说是后台管理系统每次切换标签栏后,xxx内容区自动刷新,操作进度也丢失了,给用户造成很大困扰。作为结丹期修士的我自然不容允许此等存在,开干!


28af57256b600c33b123001d5f4c510fdbf9a1df.jpg


问题分析



该后台管理系统基于 vue3 全家桶开发,多标签页模式,标签页默认 KeepAlive,本文以 demo 示例。



切换标签后内容区自动刷新,操作进度丢失?首先想到的是 KeepAlive 问题,但经过排查后才发现,KeepAlive 是正常的,异常的是内嵌于页面的 iframe 内容区,页面每次 onActivated 时,iframe 内容区都会重新加载一次,导致进度丢失。


录制_2025_05_13_19_18_22_59.gif


iframe 并没有被 keep 住,为什么?


通过查阅 Vue 文档得知,KeepAlive缓存的只是 Vue 组件实例,组件实例包含组件状态和 VNode (虚拟 DOM 节点)等。当组件 activated 时,组件 VNode 已经转为真实 DOM 节点插入文档中了,而组件 deactivated 时,已经从文档中移除了组件对应的真实 DOM 节点并缓存组件实例。

image.png


image.png


VNode 是对真实 DOM 节点的映射,包含节点标签名、节点属性等信息。我们打开控制台选中 iframe 元素,右侧那栏就是其对应的 VNode 了。
image.png
从上图可看出,iframe 的内容并不属于节点信息,是个独立的 browsing context(浏览上下文),无法被缓存;iframe 每次渲染(如 DOM 节点插入、移动)都会触发完整的加载过程(相当于打开新窗口)。故组件每次 activated 时,iframe 都会重新加载,创建了新的上下文,之前的操作进度自然是丢失了。


至此,问题原因已找到,接下来看下如何处理。


解决方案


iframe 无法保存于 VNode 中,又不能将 iframe 从文档中移动或移除,那么就想办法在某个地方把 iframe 存起来,比如 body 节点下,然后通过样式控制 iframe 展示与隐藏,顺着思路捋一下整体流程。


image.png


有了上述流程,开始设计下细节。 Iframe 组件是对 iframe 操作流的封装,方便在 vue 项目中使用,内部涉及 iframe 创建、插入、设置样式、移除等操作,为方便操作,将其封装为 Iframe 类;分散的 Iframe 类操作,稍有不当可能造成内存占用过多,故为了统一管理,再设计一个 IframeManage 来统一管理 Iframe。

相关的类关系图如下


classDiagram
class Iframe {
-instance: HTMLIFrameElement
-ops: IframeOptions
+init()
+hide()
+show(rect: IFrameRect)
+resize(rect: IFrameRect)
+destroy()
}

class IFrameManager {
+static frames: Map<string, Iframe>
+static createFrame()
+static showFrame()
+static hideFrame()
+static destroyFrame()
+static resizeFrame()
+static getFrame()
}

class VueComponent {
-frameContainer: Ref
+createFrame()
+destroyFrame()
+showFrame()
+resizeFrame()
-handleLoaded()
-handleError()
}

VueComponent --> IFrameManager : 使用
IFrameManager --> Iframe : 创建/管理
Iframe --> HTMLIFrameElement : 封装

对应的时序图如下


sequenceDiagram
participant VueComponent
participant IFrameManager
participant Iframe
participant DOM

VueComponent->>IFrameManager: createFrame()
IFrameManager->>Iframe: new Iframe(ops)
Iframe->>DOM: createElement('iframe')
Iframe->>DOM: appendChild()
VueComponent->>IFrameManager: resizeFrame()
IFrameManager->>Iframe: resize()
Iframe->>DOM: setElementStyle()
VueComponent->>IFrameManager: destroyFrame()
IFrameManager->>Iframe: destroy()
Iframe->>DOM: remove()

至此思路清晰,开始进入编码


编码实战


首先是 Iframe 类的实现


interface IframeOptions {
uid: string
src: string
name?: string
width?: string
height?: string
className?: string
style?: string
allow?: string
onLoad?: (e: Event) => void
onError?: (e: string | Event) => void
}

type IframeRect = Pick<DOMRect, 'left' | 'top' | 'width' | 'height'> & { zIndex?: number | string }

class Iframe {
instance: HTMLIFrameElement | null = null
constructor(private ops: IframeOptions) {
this.init()
}
init() {
const {
src,
name = `Iframe-${Date.now()}`,
className = '',
style = '',
allow,
onLoad = () => {},
onError = () => {},
} = this.ops

this.instance = document.createElement('iframe')
this.instance.name = name
this.instance.className = className
this.instance.style.cssText = style
this.instance.onload = onLoad
this.instance.onerror = onError
if (allow) this.instance.allow = allow
this.hide()
this.instance.src = src
document.body.appendChild(this.instance)
}
setElementStyle(style: Record<string, string>) {
if (this.instance) {
Object.entries(style).forEach(([key, value]) => {
this.instance!.style.setProperty(key, value)
})
}
}
hide() {
this.setElementStyle({
display: 'none',
position: 'absolute',
left: '0px',
top: '0px',
width: '0px',
height: '0px',
})
}
show(rect: IframeRect) {
this.setElementStyle({
display: 'block',
position: 'absolute',
left: rect.left + 'px',
top: rect.top + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
border: '0',
'z-index': String(rect.zIndex) || 'auto',
})
}
resize(rect: IframeRect) {
this.show(rect)
}
destroy() {
if (this.instance) {
this.instance.onload = null
this.instance.onerror = null
this.instance.remove()
this.instance = null
}
}
}

其次是 IFrameManager 类的实现


export class IFrameManager {
static frames = new Map()
static createFame(ops: IframeOptions, rect: IframeRect) {
const existFrame = this.frames.get(ops.uid)
if (existFrame) {
existFrame.destroy()
}
const frame = new Iframe(ops)
this.frames.set(ops.uid, frame)
frame.show(rect)
return frame
}
static showFrame(uid: string, rect: IframeRect) {
const frame = this.frames.get(uid)
frame?.show(rect)
}
static hideFrame(uid: string) {
const frame = this.frames.get(uid)
frame?.hide()
}
static destroyFrame(uid: string) {
const frame = this.frames.get(uid)
frame?.destroy()
this.frames.delete(uid)
}
static resizeFrame(uid: string, rect: IframeRect) {
const frame = this.frames.get(uid)
frame?.resize(rect)
}
static getFrame(uid: string) {
return this.frames.get(uid)
}
}

最后是 Iframe 组件的实现


<template>
<div ref="frameContainer" class="k-frame">
<span v-if="!src" class="k-frame-tips">
<slot name="placeholder">暂无数据</slot>
</span>
<span v-else-if="isLoading" class="k-frame-tips">
<slot name="loading">加载中... </slot>
</span>
<span v-else-if="isError" class="k-frame-tips"> <slot name="error">加载失败 </slot></span>
</div>

</template>
<script setup lang="ts">
import { onActivated, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'
import { IFrameManager, getIncreaseId } from './core'
import { useResizeObserver, useThrottleFn } from '@vueuse/core'

defineOptions({
name: 'KFrame',
})

const props = withDefaults(
defineProps<{
src: string
zIndex?: string | number
keepAlive?: boolean
}>(),
{
src: '',
keepAlive: true,
},
)

const emits = defineEmits(['loaded', 'error'])

const uid = `kFrame-${getIncreaseId()}`
const frameContainer = ref()
const isLoading = ref(false)
const isError = ref(false)
let readyFlag = false

const getFrameContainerRect = () => {
const { x, y, width, height } = frameContainer.value?.getBoundingClientRect() || {}
return {
left: x || 0,
top: y || 0,
width: width || 0,
height: height || 0,
zIndex: props.zIndex ?? 'auto',
}
}

const createFrame = () => {
isError.value = false
isLoading.value = true

IFrameManager.createFame(
{
uid,
name: uid,
src: props.src,
onLoad: handleLoaded,
onError: handleError,
allow: 'fullscreen;autoplay',
},
getFrameContainerRect(),
)
}
const handleLoaded = (e: Event) => {
isLoading.value = false
emits('loaded', e)
}
const handleError = (e: string | Event) => {
isLoading.value = false
isError.value = true
emits('error', e)
}

const showFrame = () => {
IFrameManager.showFrame(uid, getFrameContainerRect())
}
const hideFrame = () => {
IFrameManager.hideFrame(uid)
}
const resizeFrame = useThrottleFn(() => {
IFrameManager.resizeFrame(uid, getFrameContainerRect())
})

const destroyFrame = () => {
IFrameManager.destroyFrame(uid)
}

const getFrame = () => {
return IFrameManager.getFrame(uid)
}

useResizeObserver(frameContainer, () => {
resizeFrame()
})

onBeforeUnmount(() => {
destroyFrame()
readyFlag = false
})

onDeactivated(() => {
if (props.keepAlive) {
hideFrame()
} else {
destroyFrame()
}
})

onActivated(() => {
if (props.keepAlive) {
showFrame()
return
}
if (readyFlag) {
createFrame()
}
})

watch(
() => [frameContainer.value, props.src],
(el, src) => {
if (el && src) {
createFrame()
readyFlag = true
} else {
destroyFrame()
readyFlag = false
}
},
{
immediate: true,
},
)

defineExpose({
getRef: () => getFrame()?.instance,
})
</script>


<style lang="scss" scoped>
.k-frame {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;

&-tips {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>



看看效果


录制_2025_05_14_14_23_18_474.gif


小结


管理后台多页签切换,iframe 区操作进度丢失,根本原因在于 KeepAlive 缓存机制与iframe 的独立浏览上下文特性存在本质冲突。本文通过物理隔离视觉映射的双重策略,将 iframe 的真实 DOM 节点与Vue 组件实例解耦,实现了 keepAlive 的效果。

当然,该方案在代码实现还有很大优化空间,如 IFrameManager 目前是单例模式、Iframe 池未设计淘汰缓存机制(如 LRU )。嘀嘀嘀...产品催着上线了,没时间优化了,下次一定。

相关代码已上传只 github,欢迎道友们给个 star,在此谢过了


4aef7c55564e925846d4563bd982d158cebf4e55.gif


作者:Canmick
来源:juejin.cn/post/7504146372771004425
收起阅读 »

Tailwind 到底是设计师喜欢,还是开发者在硬撑?

web
我们最近刚把一个后台系统从 element-plus 切成了完全自研组件,CSS 层统一用 Tailwind。全员同意设计稿一致性提升了,但代码里怨言开始冒出来。 这篇文章不讲原理,直接上代码对比和团队真实使用反馈,看看是谁在享受,谁在撑着。 1.组件内样式...
继续阅读 »

我们最近刚把一个后台系统从 element-plus 切成了完全自研组件,CSS 层统一用 Tailwind。全员同意设计稿一致性提升了,但代码里怨言开始冒出来。


这篇文章不讲原理,直接上代码对比和团队真实使用反馈,看看是谁在享受,谁在撑着。




1.组件内样式迁移


原先写法(BEM + scoped):


<template>
<div class="card">
<h2 class="card__title">用户概览</h2>
<p class="card__desc">共计 1280 位</p>
</div>
</template>

<style scoped>
.card {
padding: 16px;
background-color: #fff;
border-radius: 8px;
}
.card__title {
font-size: 16px;
font-weight: bold;
}
.card__desc {
color: #999;
font-size: 14px;
}
</style>

Tailwind 重写:


<template>
<div class="p-4 bg-white rounded-lg">
<h2 class="text-base font-bold">用户概览</h2>
<p class="text-sm text-gray-500">共计 1280 位</p>
</div>
</template>

优点:



  • 组件直接可读,不依赖 class 定义

  • 样式即结构,调样式时不用来回翻


缺点:



  • 设计稿变了?全组件搜索 text-sm 改成 text-base

  • 无法抽象:多个地方复用 .text-label 变成复制粘贴




2.复杂交互样式


纯 CSS(原写法)


<template>
<button class="btn">提交</button>
</template>

<style scoped>
.btn {
background-color: #409eff;
color: #fff;
padding: 8px 16px;
border-radius: 4px;
}
.btn:hover {
background-color: #66b1ff;
}
.btn:active {
background-color: #337ecc;
}
</style>

Tailwind 写法


<button
class="bg-blue-500 hover:bg-blue-400 active:bg-blue-700 text-white py-2 px-4 rounded">

提交
</button>

问题来了:



  • ✅ 简单 hover/active 很方便

  • ❌ 多态样式(如 disabled + dark mode + hover 同时组合)就很难读:


<button
class="bg-blue-500 text-white disabled:bg-gray-300 dark:bg-slate-600 dark:hover:bg-slate-700 hover:bg-blue-600 transition-all">

>
提交
</button>

调试时需要反复阅读 class 字符串,不能直接 Cmd+Click 查看样式来源。




3.统一样式封装,复用方案混乱


原写法:统一样式变量 + class


$border-color: #eee;

.panel {
border: 1px solid $border-color;
border-radius: 8px;
}

Tailwind 使用中经常出现的写法:


<div class="border border-gray-200 rounded-md" />

问题来了:



设计稿调整了主色调或边框粗细,如何批量更新?



BEM 模式下你只需要改一个变量,Tailwind 下必须靠 @apply 或者手动替换所有 .border-gray-200


于是我们项目里又写了一堆“语义类”去封装 Tailwind:


/* 自定义 utilities */
@layer components {
.app-border {
@apply border border-gray-200;
}
.app-card {
@apply p-4 rounded-lg shadow-sm bg-white;
}
}

最后导致的问题是:我们重新“造了个 BEM”,只不过这次是基于 Tailwind 的 apply 写法。




🧪 实测维护成本:100+组件、多人协作时的问题


我们项目有 110 个组件,4 人开发,统一用 Tailwind,协作两个月后出现了这些反馈:



  • 👨‍💻 A 开发:写得很快,能复制设计稿的 class 直接粘贴

  • 🧠 B 维护:改样式全靠人肉找 .text-sm.p-4,没有结构命名层

  • 🤯 C 重构:统一调整圆角半径?所有 .rounded-md 都要搜出来替换


所以我们内部的结论是:



Tailwind 写得爽,维护靠人背。它适合“一次性强视觉还原”,不适合“结构长期型组件库”。





🔧 我们后来的解决方案:Tailwind + token 化抽象


我们仍然使用 Tailwind 作为底层 utilities,但同时强制使用语义类抽象,例如:


@layer components {
.text-label {
@apply text-sm text-gray-500;
}

.btn-primary {
@apply bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded;
}

.card-container {
@apply p-4 bg-white rounded-lg shadow;
}
}

模板中统一使用:


<h2 class="text-label">标题</h2>
<button class="btn-primary">提交</button>
<div class="card-container">内容</div>

这种方式保留了 Tailwind 的构建优势(无 tree-shaking 问题),但代码结构有命名可依,后期批量维护不再靠搜索。




📌 最终思考


Tailwind 是给设计还原速度而生的,不是给可维护性设计的。
设计师爱是因为它像原子操作;
开发者撑是因为它把样式从结构抽象变成了“字串组合游戏”。


如果你的团队更在意开发效率,样式一次性使用,那 Tailwind 非常合适。
如果你的组件系统是要长寿、要维护、要被多人重构的——你最好在 Tailwind 之上再造一层自己的语义层,或者别用。


分享完毕,谢谢大家🙂


📌 你可以继续看我的系列文章



作者:ErpanOmer
来源:juejin.cn/post/7517496354245492747
收起阅读 »