注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

前端的AI路其之三:用MCP做一个日程助理

web
前言 话不多说,先演示一下吧。大概功能描述就是,告诉AI“添加日历,今天下午五点到六点,我要去万达吃饭”,然后AI自动将日程同步到日历。 准备工作 开发这个日程助理需要用到MCP、Mac(mac的日历能力)、Windsurf(运行mcp)。技术栈是Types...
继续阅读 »

前言


话不多说,先演示一下吧。大概功能描述就是,告诉AI“添加日历,今天下午五点到六点,我要去万达吃饭”,然后AI自动将日程同步到日历


2025-04-1819.25.19-ezgif.com-video-to-gif-converter.gif


准备工作


开发这个日程助理需要用到MCPMac(mac的日历能力)Windsurf(运行mcp)。技术栈是Typescript


思路


基于MCP我们可以做很多。关于这个日程助理,其实也是很简单一个尝试,其实就是再验证一下我对MCP的使用。因为Siri的原因,让我刚好有了这个想法,尝试一下自己搞个日程助理。关于MCP可以看我前面的分享
# 前端的AI路其之一: MCP与Function Calling# 前端的AI路其之二:初试MCP Server


我的思路如下: 让大模型理解一下我的意图,然后执行相关操作。这也是我对MCP的理解(执行相关操作)。因此要做日程助理,那就很简单了。首先搞一个脚本,能够自动调用mac并添加日历,然后再包装成MCP,最后引入大模型就ok了。顺着这个思路,接下来就讲讲如何实现吧


实现


第一步:在mac上添加日历


这里我们需要先明确一个概念。mac上给日历添加日程,其实是就是给对应的日历类型添加日程。举个例子


image.png


左边红框其实就是日历类型,比如我要添加一个开发日程,其实就是先选择"开发"日历,然后在该日历下添加日程。因此如果我们想通过脚本形式创建日程,其实就是先看日历类型存在不存在,如果存在,就在该类型下添加一个日程。


因此这里第一步,我们先获取mac上有没有对应的日历,没有的话就创建一个。


1.1 查找日历



参考文档 mac查找日历



假定我们的日历类型叫做 日程助手这里我使用了applescript的语法,因为JavaScript的方式我这运行有问题。


import { execSync } from 'child_process';

function checkCalendarExists(calendarName) {

const Script = `tell application "Calendar"
set theCalendarName to "${calendarName}"
set theCalendar to first calendar where its name = theCalendarName
end tell`
;


// 执行并解析结果
try {
const result = execSync(`osascript -e '${Script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

console.log(result);
return true;
} catch (error) {
console.error('检测失败:', error.message);
return false;
}
}

// 使用示例
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');



附赠检验结果

image.png


现在我们知道了怎么判断日历存不存在,那么接下来就是,在日历不存在的时候创建日历


1.2 日历创建



参考文档 mac 创建日历



import { execSync } from 'child_process';


// 创建日历
function createCalendar(calendarName) {
const script = `tell application "Calendar"
make new calendar with properties {name:"${calendarName}"}
end tell`
;

try {

execSync(`osascript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

return true;
} catch (e) {
console.log('create fail', e)
return false;
}
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {
....
}

// 使用示例
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

if (!exists) {
const res = createCalendar(calendarName);

console.log(res ? '✅ 创建成功' : '❌ 创建失败')
}


运行结果

image.png


接下来就是第三步了,在日历“日程助手”下创建日程


1.3 创建日程


import { execSync } from 'child_process';

// 创建日程
function createCalendarEvent(calendarName, config) {

const script = `var app = Application.currentApplication()
app.includeStandardAdditions = true
var Calendar = Application("Calendar")

var eventStart = new Date(${config.startTime})
var eventEnd = new Date(${config.endTime})

var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
var projectCalendar = projectCalendars[0]
var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
projectCalendar.events.push(event)
event`


try {
console.log('开始创建日程');
execSync(` osascript -l JavaScript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});
console.log('✅ 日程添加成功');
} catch (error) {
console.error('❌ 执行失败:', error);
}

}

// 创建日历
function createCalendar(calendarName) {
....
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {

...
}


这里我们完善一下代码


import { execSync } from 'child_process';

function handleCreateEvent(config) {
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
// console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

if (!exists) {
const createRes = createCalendar(calendarName);

console.log(createRes ? '✅ 创建日历成功' : '❌ 创建日历失败')

if (createRes) {
createCalendarEvent(calendarName, config)
}
} else {
createCalendarEvent(calendarName, config)
}
}

// 创建日程
function createCalendarEvent(calendarName, config) {

const script = `var app = Application.currentApplication()
app.includeStandardAdditions = true
var Calendar = Application("Calendar")

var eventStart = new Date(${config.startTime})
var eventEnd = new Date(${config.endTime})

var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
var projectCalendar = projectCalendars[0]
var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
projectCalendar.events.push(event)
event`


try {
console.log('开始创建日程');
execSync(` osascript -l JavaScript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});
console.log('✅ 日程添加成功');
} catch (error) {
console.error('❌ 执行失败:', error);
}

}

// 创建日历
function createCalendar(calendarName) {
const script = `tell application "Calendar"
make new calendar with properties {name:"${calendarName}"}
end tell`
;

try {

execSync(`osascript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

return true;
} catch (e) {
console.log('create fail', e)
return false;
}
}

// 检查日历是否存在
function checkCalendarExists(calendarName) {

const Script = `tell application "Calendar"
set theCalendarName to "${calendarName}"
set theCalendar to first calendar where its name = theCalendarName
end tell`
;


// 执行并解析结果
try {
const result = execSync(`osascript -e '${Script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

return true;
} catch (error) {
return false;
}
}


// 运行示例

const eventConfig = {
title: '团队周会',
startTime: 1744183538021,
endTime: 1744442738000,
description: '每周项目进度同步',
};

handleCreateEvent(eventConfig)


运行结果

image.png


image.png


这就是一个完善的,可以直接在终端运行的创建日程的脚本的。接下来我们要做的就是,让大模型理解这个脚本,并学会使用这个脚本


第二步: 定义MCP


基于第一步,我们已经完成了这个日程助理的基本功能,接下来就是借助MCP的能力,教会大模型知道有这个函数,以及怎么调用这个函数


// 引入 mcp
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 声明MCP服务
const server = new McpServer({
name: "mcp_calendar",
version: "1.0.0"
});

...
// 添加日历函数 也就是告诉大模型 有这个东西以及怎么用
server.tool("add_mac_calendar", '给mac日历添加日程, 接受四个参数 startTime, endTime是起止时间(格式为YYYY-MM-DD HH:MM:SS) title是日历标题 description是日历描述', { startTime: z.string(), endTime: z.string(), title: z.string(), description: z.string() },
async ({ startTime, endTime, title, description }) => {
const res = handleCreateEvent({
title: title,
description: description,
startTime: new Date(startTime).getTime(),
endTime: new Date(endTime).getTime()
});
return {
content: [{ type: "text", text: res ? '添加成功' : '添加失败' }]
}
})


// 初始化服务
const transport = new StdioServerTransport();
await server.connect(transport);


这里附上完整的ts代码


import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { execSync } from 'child_process';
import { z } from "zod";


export interface EventConfig {
// 日程标题
title: string;
// 日程开始时间 毫秒时间戳
startTime: number;
// 日程结束时间 毫秒时间戳
endTime: number;
// 日程描述
description: string;
}

const server = new McpServer({
name: "mcp_calendar",
version: "1.0.0"
});

function handleCreateEvent(config: EventConfig) {
const calendarName = '日程助手';
const exists = checkCalendarExists(calendarName);
// console.log(`日历 "${calendarName}" 存在:`, exists ? '✅ 是' : '❌ 否');

let res = false;

if (!exists) {
const createRes = createCalendar(calendarName);

console.log(createRes ? '✅ 创建日历成功' : '❌ 创建日历失败')

if (createRes) {
res = createCalendarEvent(calendarName, config)
}
} else {
res = createCalendarEvent(calendarName, config)
}

return res
}

// 创建日程
function createCalendarEvent(calendarName: string, config: EventConfig) {

const script = `var app = Application.currentApplication()
app.includeStandardAdditions = true
var Calendar = Application("Calendar")

var eventStart = new Date(${config.startTime})
var eventEnd = new Date(${config.endTime})

var projectCalendars = Calendar.calendars.whose({name: "${calendarName}"})
var projectCalendar = projectCalendars[0]
var event = Calendar.Event({summary: "${config.title}", startDate: eventStart, endDate: eventEnd, description: "${config.description}"})
projectCalendar.events.push(event)
event`


try {
console.log('开始创建日程');
execSync(` osascript -l JavaScript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});
console.log('✅ 日程添加成功');

return true
} catch (error) {
console.error('❌ 执行失败:', error);
return false
}

}

// 创建日历
function createCalendar(calendarName: string) {
const script = `tell application "Calendar"
make new calendar with properties {name:"${calendarName}"}
end tell`
;

try {

execSync(`osascript -e '${script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

return true;
} catch (e) {
console.log('create fail', e)
return false;
}
}

// 检查日历是否存在
function checkCalendarExists(calendarName: string) {

const Script = `tell application "Calendar"
set theCalendarName to "${calendarName}"
set theCalendar to first calendar where its name = theCalendarName
end tell`
;


// 执行并解析结果
try {
const result = execSync(`osascript -e '${Script}'`, {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'] // 忽略错误输出
});

return true;
} catch (error) {
return false;
}
}


server.tool("add_mac_calendar", '给mac日历添加日程, 接受四个参数 startTime, endTime是起止时间(格式为YYYY-MM-DD HH:MM:SS) title是日历标题 description是日历描述', { startTime: z.string(), endTime: z.string(), title: z.string(), description: z.string() },
async ({ startTime, endTime, title, description }) => {
const res = handleCreateEvent({
title: title,
description: description,
startTime: new Date(startTime).getTime(),
endTime: new Date(endTime).getTime()
});
return {
content: [{ type: "text", text: res ? '添加成功' : '添加失败' }]
}
})

const transport = new StdioServerTransport();
await server.connect(transport);



第三步: 导入Windsurf


在前文已经讲过如何引入到Windsurf,可以参考前文# 前端的AI路其之二:初试MCP Server ,这里就不过多赘述了。 其实在build之后,完全可以引入其他支持MCP的软件基本都是可以的。


接下来就是愉快的调用时间啦。


总结


这里其实是对前文# 前端的AI路其之二:初试MCP Server 的再次深入。算是大概讲明白了Tool方式怎么用,MCP当然不止这一种用法,后面也会继续输出自己的学习感悟,也欢迎各位大佬的分享和指正。


祝好。


作者:justdoit521
来源:juejin.cn/post/7495598542405550107
收起阅读 »

Web PWA的极致,比App更像App

web
这是一个平平无奇的音乐App Vooh,你可以在里面搜索歌曲,添加播放列表,播放音乐。 你可以滑动返回上一级页面,就像任何一个普通的App那样。 你可以流畅地展开音乐播放面板,看着歌词随着播放时间滚动。 当然,你也可以在电脑端,或者iPad上使用这个Ap...
继续阅读 »

这是一个平平无奇的音乐App Vooh,你可以在里面搜索歌曲,添加播放列表,播放音乐。



你可以滑动返回上一级页面,就像任何一个普通的App那样。



你可以流畅地展开音乐播放面板,看着歌词随着播放时间滚动。



当然,你也可以在电脑端,或者iPad上使用这个App。





而它与App的唯一不同,在于安装它不需要下载庞大的安装文件,只需要一个链接。音乐播放器Vooh的本体,只是一个网页。


作为一个诞生了好几年的老技术,PWA(Progressive Web Application)自诞生以来一直都不温不火,Google对它的愿景是最终所有的网页都能做到和App一致的体验,但直到现在,它都像是一道可有可无的饭后甜点。对于网页来说,即用即走似乎是它与生俱来的诅咒,用户既没有将Web安装到桌面的必要,也没有这个耐心,毕竟对于网络延迟增加1秒都可能导致访问量降低80%的地狱难度模式的网页用户生态而言,让一个浏览器用户点击一个陌生的“Install as application”的按钮简直是天方夜谭。尽管它就在那里,但乐于尝试的人似乎总是寥寥无几。既然PWA和纯网页能做的事情相差无几,那为什么还要浪费桌面空间增加一个以后可能再也不会使用的图标呢?


我一直认为,PWA应该朝着更像App的方向努力,才能体现出它的价值。然而,目前的许多PWA,看起来只是把普通的网页做成了全屏,与在浏览器中的体验别无二致,做不出差异化,用户自然没有动力去安装PWA,PWA那些听起来十分美好的特性便成了空中楼阁,无源之水,这个名字也越来越将从人们的视野中慢慢淡去。


如何才能让PWA更像APP,这是一个问题。毕竟浏览器的交互逻辑和原生App相比,有着很大的区别,用户早已习惯了移动浏览器中的前进后退,页面加载时的白屏,以及几乎不存在的手势交互,似乎在说,没关系,这就是网页,它做到这个份上已经足够了。然而,若要把这份体验带到模仿原生App的PWA中去,那势必将迎来用户预期低落的反噬,连这样那样的交互体验都没有,还能叫App?


为了了解目前的PWA究竟能做到何种地步,我开发了Vooh,一个竭尽可能模仿原生App实现的PWA音乐播放器。它尽力实现了一个原生App应该具备的一切交互细节,包括页面间自然的动画过渡,跟手的手势交互,为触屏优化的样式细节等等,我尽可能将它的每个细节都尽可能地做到与App别无二致,就是为了探索Web能力的极限。而在这之后,我也打算将Vooh的实现原理整理出来,并且准备逐步将之前的做过的项目“App化”,来一窥Google期待的未来,究竟是什么样子。


无处不在的过渡动画


尽管Vue,React以及原生CSS都提供了方便的方式实现过渡动画,但是对于大多数网页来说,一个Loading动画可能就是整个页面里动画最多的地方了。这对于网页来说的确无关紧要,毕竟用户们早已习惯了浏览器里生硬的切换效果,没有成体系的交互反馈,以及突然消失出现的页面区域。尽管在许多成熟组件库慢慢开始注重交互动画的优化之后,这样的情况在慢慢改善,但是依然难以改变用户的刻板印象。因此,为用户的预期提供动画反馈是伪装成原生App的一个关键步骤,否则,缺少反馈的使用体验会一下子将用户安装和使用PWA的欲望拉得很低。


除去老生常谈的按钮悬浮、按下时的动画,页面间的过渡动画也是不可缺少的一环。如果你仔细观察iOS的Tab页面,就能发现在切换Tab的时候,也会有细微的不易察觉的缩放淡出渐变,正是这种细致入微的动画组成了iOS App丝滑体验中重要的一部分。


表单组件的动画效果也很重要,Vooh尽可能地使用了iOS风格的表单组件,例如Button,Switch等,以贴合用户的日常视觉体验。



手势交互


手势是网页与App的重要差异点,一般来说,很少会有网页支持用户的滑动返回,长按呼出菜单等复杂的手势操作,而这正是让你的PWA丝般顺滑的关键。


需要注意的是,由于大部分移动浏览器和JS本身单线程的限制,手势交互依赖监听器的执行速度,而很难跑满设备屏幕的帧率上限,尤其是iOS设备上,开启低电量模式的情况下,监听器的帧率可能只有不到30 FPS,肉眼可见的卡顿。目前为止,也没有看到任何浏览器厂商有关于优化手势交互的提案,手势交互就像一道横亘在网页与App之间的鸿沟,没有丝毫跨越的可能,只能尽可能地模仿。



离线访问


没有哪个用户能接受打开App时整个页面全部消失无法操作,APP的最大优点就是离线可用,好在Service Worker的推出让这一点不再是问题,通过Service Worker对网页资源进行缓存,可以实现在低网速甚至离线环境下,也能继续使用PWA,就像真正的App那样。


然而不幸的是,在iOS设备上,Service Worker离线缓存不再可用,开启飞行模式或者关闭网络连接后将无法访问任何网页,包括已经安装在桌面上的PWA,



偶遇现代IE厂商,拼尽全力无法战胜。



细节之外的细节


而Vooh在这些基础能力之外,还增加了许多其他的细节设计,让整个App在模仿原生App时更进一步。


1,存储占用管理


在移动设备上,PWA与App的存储占用是分隔开的,而且往往要经过十分复杂的步骤才能看到PWA的实际空间占用,因此对于音乐播放器这种高度依赖本地资源的应用来说,一个显而易见的存储占用管理系统能有效缓解用户的存储焦虑。



2,接入系统播放器


隆重介绍Media Session API,它能让JS直接接入系统播放器控件,即使在后台也可以允许用户通过系统自带的播放器控制媒体的播放,例如下一曲、播放暂停等,在iOS设备上,还能直接适配灵动岛,这下谁还能分辨谁是原生App。



3,深色模式


在Apple等手机厂商的推动下,大部分的App都已经适配深色模式,而网页对于深色模式的适配比起App要更为简单,毕竟CSS实在是太灵活了,Vooh当然也做了适配,在不同的模式下都能完美贴合系统的主体模式。


为了提升Vooh与其他原生播放器的(根本不存在的)竞争力,我也煞费苦心地加入了许多的细节,来让用户有真正使用它的动力,例如根据歌曲封面动态取色,自动识别的滚动歌词等,希望能让它在用户的手机桌面上多待一段时间。


未竟之事


不过,即使是做到了这个地步,PWA的能力始终是有极限的。有些App轻易能做到的事,对于PWA而言犹如天堑一般遥不可及,包括但不限于:


1,后台活动


在移动设备上,网页也好,PWA也好,基本上没有任何后台活动能力,甚至上面提到的Media Session API,在iOS上顶多也只最多能支持后台播放1~2首歌曲,然后就会被强行停止,更不用说后台导航,推送通知这种活在梦里的API了,这方面浏览器天生就是残废,未来也看不到有任何改进的可能,因此在开发PWA时,一定要远离这些方向。在js都能跑虚拟机,剪视频的当下,Web开发者们推送一条通知的希冀却只能在另一个平行时空实现了。


2,跳转到PWA


据说Andriod Chrome支持使用PWA来打开特定的链接,不过在iOS上就别想了。


3,触感反馈


同样,Web也只能使用早已被淘汰的Vibrate,细腻的振动反馈和Taptic Engine对网页来说也是天方夜谭。


4,调用原生功能


还有无数浩如烟海的功能是PWA完全无法实现的,例如系统级的音量调节,亮度调节等,我能理解这是浏览器对恶意网站的限制,但这也确实极大限制了Web的发展,比如奠定了Web安全基础的跨域限制,如今成为了许多大型Web应用的掣肘。我由衷地希望某天浏览器能制定一个更宽松的PWA标准,例如安装到桌面后能提供更多的权限,提供一个无跨域限制的fetch代替品等等,然而即使对Web上心如Google,也没有考虑过这个方向的可能性。JS正在和越来越宽松的宿主环境(Tuari,Electron)一步步蚕食着原生GUI开发的领地,而它的发源地,浏览器却只能被所谓的安全性限制,成为一个只负责播放动画的花瓶。


总结


正如所说,一切能由javascript实现的终将会用javascript实现。如今,越来越多的平台小程序,快应用,乃至于H5套壳的App越来越多,随着浏览器性能的进一步提升,Web能做到的事越来越多,但是Web的交互性却并没有随着javascript的繁荣而被重视起来,受限于javascript的单线程特性,要完全模拟App的使用体验还是有一定的差距,一个劲地往原生体验上靠,有时也并不一定是最好的选择,Vooh的出现只是给了开发者们一个可能的方向,Web的轻量,优秀的可触达性与PWA有机结合,才是Web的发展方向。同时也希望各家浏览器厂商们能加快适配新的Web特性,能够让程序们在写代码时少掉一些头发,便是最大的善事了


如果对Vooh的实现方式有兴趣的话,欢迎关注我的专栏或者博客,后续的代码也会一并开源,涉及到音乐版权相关,目前的Vooh只开放了2首免费无版权音乐的使用,代码也不会涉及版权相关的领域。


作者:Glink
来源:juejin.cn/post/7490977437674651683
收起阅读 »

视频播放弱网提示实现

web
作者:陈盛靖 一、背景 业务群里面经常反馈,视频播放卡顿,视频播放总是停留在某一时刻就播放不了了。后面经过排查,发现这是因为弱网导致的。然而,用户数量众多,隔三差五总有人在群里反馈,有时问题一时半会好不了,用户就会怀疑不是网络,而是我们的系统问题。因此,我们...
继续阅读 »

作者:陈盛靖



一、背景


业务群里面经常反馈,视频播放卡顿,视频播放总是停留在某一时刻就播放不了了。后面经过排查,发现这是因为弱网导致的。然而,用户数量众多,隔三差五总有人在群里反馈,有时问题一时半会好不了,用户就会怀疑不是网络,而是我们的系统问题。因此,我们希望能在弱网的时候展示提示,这样用户体验会更友好,同时也能减少一定的客诉。


二、现状分析


我们使用的播放器是chimee(http://www.chimee.org/index.html)。遗憾的是,chimee并没有视频播放卡顿自动展示loading的功能,不过我们可以通过其插件能力,来编写一个自定义video-loading的插件。


三、方案设计


使用NetworkInformation


常见的方法就是我们通过设定一个标准,然后检测用户设备的网络速度,在到达一定阈值时展示弱网提示。这里需要确定一个重要的点:什么情况下才算弱网?


我们的应用是h5,这里我们可以使用window对象中的NetworkInformation(developer.mozilla.org/zh-CN/docs/…),我们可以通过浏览器的debug工具,打印window.naviagtor.connection,这个对象内部就存储着网络信息:



其中各个属性含义如下表所示:


属性含义
downlink返回以兆比特每秒为单位的有效带宽估计,四舍五入到最接近的 25 千比特每秒的倍数。
downlinkMax返回底层连接技术的最大下行速度,以兆比特每秒(Mbps)为单位。
effectiveType返回连接的有效类型(意思是“slow-2g”、“2g”、“3g”或“4g”中的一个)。此值是使用最近观察到的往返时间和下行链路值的组合来确定的。
rtt返回当前连接的有效往返时间估计,四舍五入到最接近的 25 毫秒的倍数。
saveData如果用户在用户代理上设置了减少数据使用的选项,则返回 true。
type返回设备用于网络通信的连接类型。它会是以下值之一:
bluetooth
cellular
ethernet
none
wifi
wimax
other
unknown
onchange接口的 change 事件在网络连接信息发生变化时被触发,并且该事件由 NetworkInformation(developer.mozilla.org/zh-CN/docs/…) 对象接收。

其中,我们可以通过effectiveType判断当前网络的大体情况,并且可以拿到一个预估的网络带宽(downlink)。我们可以通过监听onchange事件,在网络变差的时候,展示对应的弱网提示。


这个方案的优点是:



  • 浏览器环境原生支持

  • 实现相对简单


但缺点却十分明显:



  • 网络状态变化非实时


effectiveType的变化可能是分钟级别的,对于短暂的网络波动,状态没办法做更精细的把控



  • 存在兼容性问题


对于不同一些主流浏览器不支持,例如Firefox、Safari等




  • 不同设备间存在差异


不同的设备和浏览器,由于其差异,在不同的网络情况下,视频的播放情况是不一样的,如果我们固定一个标准,可能会导致在不同设备下,同一个网络速度,有人明明正常播放视频,但是却提示网络异常,这样用户会感到疑惑。


那有没有更好的方法呢?


监听Video元素事件


chimee底层也是在html video上进行的二次封装,我们可以在插件的生命周期中,拿到对应的video元素节点。而在video标签中,存在这样两个事件:waiting和canplay。


其事件描述如下图所示:



当视频播放卡顿时,会触发waiting事件;而当视频播放恢复正常时,会触发canplay事件。只要监听这两个事件,我们就可以实现对应的功能了。


四、功能拓展


我们知道,现在大多数网站的视频在提示弱网的时候,都会展示当前设备的网络速度是多少。因此我们也希望在展示对应的信息。那么怎么实现网络速度的检测呢?


一个简单的方法是,我们可以通过获取一张固定大小的图片资源(不一定是图片,也可以是别的类型的资源),并统计请求该资源的请求速度,从而计算当前网络的带宽是多少。当然,图片大小要尽可能小一点,一是为了节省用户流量,二是为了避免在网络不好的情况下,图片请求太慢导致一直计算不出来。


具体代码如下:


funtion calculateSpeed() {
// 图片大小772Byte
const fileSize = 772;
// 拼接时间戳,避免缓存
const imgUrl = `https://xxx.png?timestamp=${new Date().getTime()}`;

return new Promise((resolve, reject) => {
let start = 0;
let end = 1000;
let img = document.createElement('img');
start = new Date().getTime();
img.onload = function (e) {
end = new Date().getTime();
// 计算出来的单位为 B/s
const speed = fileSize / (end > start ? end - start : 1000) * 1000;
resolve(speed);
}
img.src = imgUrl;
}).catch(err => { throw err });
}

function translateUnit(speed) {
if(speed === 0) return '0.00 B/s';
if(speed > 1024 * 1024) return `${(speed / 1024 / 1024).toFixed(2)} MB/s`;
if(speed > 1024) return `${(speed / 1024).toFixed(2)} KB/s`;
else return `${speed.toFixed(2)} B/s`;
}

我们可以通过setInterval来轮询调用该函数,从而实时展示当前网络情况。系统流程图如下:


画板


五、总结


我们可以通过Chrome浏览器开发者工具中的Network中的网络配置来模拟弱网情况

具体效果如下:



成功实现视频弱网提示,完结撒花🎉🎉🎉🎉🎉🎉。


作者:古茗前端团队
来源:juejin.cn/post/7593550315254218758
收起阅读 »

富文本编辑器技术选型,到底是 Prosemirror 还是 Tiptap 好 ❓❓❓

web
我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优...
继续阅读 »

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。



如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。


在前端开发中,撤销和重做功能是提升用户体验的重要特性。无论是文本编辑器、图形设计工具,还是可视化搭建平台,都需要提供历史操作的回退和前进能力。这个功能看似简单,但实现起来需要考虑性能、内存占用、用户体验等多个方面。


在构建富文本编辑器时,Tiptap 和 ProseMirror 是两个常见的技术选择。两者都强大且灵活,但它们在设计理念、易用性、扩展性等方面存在差异。对于开发者来说,选择合适的工具对于项目的成功至关重要。本文将深入探讨两者的异同,并通过实际代码示例帮助你理解它们的差异,从而根据具体需求做出决策。


ProseMirror 的优势与挑战


ProseMirror 是一个 JavaScript 库,用于构建复杂的富文本编辑器。它的设计非常底层,提供了一个高效且灵活的文档模型,开发者可以完全控制编辑器的行为和界面。ProseMirror 本身并不提供任何 UI 或组件,而是一个核心库,开发者需要自行实现具体的编辑器功能。


作为一个底层框架,ProseMirror 允许开发者完全控制编辑器的各个方面,包括文档结构、输入行为、UI 样式等。它提供了丰富的 API,可以处理复杂的编辑需求,如数学公式、代码块、图片、链接等。开发者可以为几乎任何功能编写插件,并且可以在已有插件的基础上进行二次开发。基于虚拟 DOM 的设计,使其在大文档和复杂结构下能够提供较高的性能。


然而,由于其底层设计,ProseMirror 的 API 复杂,学习曲线陡峭。开发者需要深入理解其文档模型、事务管理、节点和视图的关系。由于不提供任何 UI 组件,开发者需要从零开始构建编辑器的界面和交互,配置和初始化过程也较为复杂,需要手动处理许多底层逻辑。


ProseMirror 基础使用示例


首先需要安装必要的包:


npm install prosemirror-state prosemirror-view prosemirror-model prosemirror-schema-basic prosemirror-schema-list prosemirror-commands

创建一个基本的 ProseMirror 编辑器需要配置 schema、state 和 view:


import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { Schema, DOMParser } from "prosemirror-model";
import { schema } from "prosemirror-schema-basic";
import { addListNodes } from "prosemirror-schema-list";
import { exampleSetup } from "prosemirror-example-setup";

// 扩展基础 schema,添加列表支持
const mySchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block"),
marks: schema.spec.marks,
});

// 创建编辑器状态
const state = EditorState.create({
schema: mySchema,
plugins: exampleSetup({ schema: mySchema }),
});

// 创建编辑器视图
const view = new EditorView(document.querySelector("#editor"), {
state,
});

如果需要添加自定义命令,比如一个格式化工具条,需要手动实现:


import { toggleMark } from "prosemirror-commands";
import { schema } from "prosemirror-schema-basic";

// 创建加粗命令
const toggleBold = toggleMark(schema.marks.strong);

// 手动创建工具栏按钮
function createToolbar(view) {
const toolbar = document.createElement("div");
toolbar.className = "toolbar";

const boldBtn = document.createElement("button");
boldBtn.textContent = "Bold";
boldBtn.onclick = () => {
toggleBold(view.state, view.dispatch);
view.focus();
};

toolbar.appendChild(boldBtn);
return toolbar;
}

ProseMirror 自定义插件示例


创建一个自定义插件需要理解 ProseMirror 的插件系统:


import { Plugin } from "prosemirror-state";

// 创建一个字符计数插件
function characterCountPlugin() {
return new Plugin({
view(editorView) {
const counter = document.createElement("div");
counter.className = "char-counter";

const updateCounter = () => {
const text = editorView.state.doc.textContent;
counter.textContent = `字符数: ${text.length}`;
};

updateCounter();

return {
update(view) {
updateCounter();
},
destroy() {
counter.remove();
},
};
},
});
}

// 使用插件
const state = EditorState.create({
schema: mySchema,
plugins: [characterCountPlugin(), ...exampleSetup({ schema: mySchema })],
});

Tiptap 的便捷开发


Tiptap 是基于 ProseMirror 构建的富文本编辑器框架,它简化了 ProseMirror 的复杂性,提供了现成的 UI 组件和更易于使用的 API。Tiptap 旨在让开发者能够快速实现丰富的富文本编辑器,同时保持较高的灵活性和扩展性。


Tiptap 提供了简洁的 API,开发者不需要深入学习 ProseMirror 的底层概念即可实现基本的富文本编辑功能。它通过封装 ProseMirror 的复杂性,使得开发过程更加直观和简便。开箱即用的 UI 组件,如文本格式化、列表、图片插入等,极大地方便了开发者的使用,减少了开发时间。清晰的文档和活跃的开源社区,也为开发者提供了良好的支持和资源。虽然 Tiptap 进行了封装,但它仍然保留了 ProseMirror 的插件系统,开发者可以根据需要定制功能,并且可以轻松地集成其他插件。此外,Tiptap 可以与 Yjs 或其他 CRDT 库结合,支持实时协作编辑功能,这是 ProseMirror 本身不具备的特性。


不过,由于 Tiptap 封装了 ProseMirror 的很多底层功能,灵活性相对较低。对于一些需要极高自定义的需求,Tiptap 可能不如 ProseMirror 灵活。虽然在大多数情况下性能良好,但在处理超大文档或复杂操作时,性能可能不如直接使用 ProseMirror。


Tiptap 基础使用示例


Tiptap 的安装和使用相对简单:


npm install @tiptap/react @tiptap/starter-kit @tiptap/pm

在 React 中使用 Tiptap:


import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

function TiptapEditor() {
const editor = useEditor({
extensions: [StarterKit],
content: "<p>Hello World!</p>",
});

if (!editor) {
return null;
}

return (
<div>
<div className="toolbar">
<button
onClick={() =>
editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
Bold
</button>
<button
onClick={() =>
editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
className={editor.isActive("italic") ? "is-active" : ""}
>
Italic
</button>
<button
onClick={() =>
editor.chain().focus().toggleBulletList().run()}
className={editor.isActive("bulletList") ? "is-active" : ""}
>
Bullet List
</button>
</div>
<EditorContent editor={editor} />
</div>

);
}

Tiptap 的 Vue 版本同样简洁:


<template>
<div>
<div class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
Italic
</button>
</div>
<editor-content :editor="editor" />
</div>
</template>

<script>
import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";

export default {
components: {
EditorContent,
},
setup() {
const editor = useEditor({
extensions: [StarterKit],
content: "<p>Hello World!</p>",
});

return { editor };
},
};
</script>

Tiptap 扩展功能示例


Tiptap 支持多种扩展,添加图片功能非常简单:


import Image from "@tiptap/extension-image";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

function EditorWithImage() {
const editor = useEditor({
extensions: [
StarterKit,
Image.configure({
inline: true,
allowBase64: true,
}),
],
});

const addImage = () => {
const url = window.prompt("图片URL");
if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
};

return (
<div>
<button onClick={addImage}>添加图片</button>
<EditorContent editor={editor} />
</div>

);
}

创建自定义扩展也很直观:


import { Extension } from "@tiptap/core";
import { Plugin } from "prosemirror-state";

const CharacterCount = Extension.create({
name: "characterCount",

addProseMirrorPlugins() {
return [
new Plugin({
view(editorView) {
const counter = document.createElement("div");
counter.className = "char-counter";

const updateCounter = () => {
const text = editorView.state.doc.textContent;
counter.textContent = `字符数: ${text.length}`;
};

updateCounter();

return {
update(view) {
updateCounter();
},
destroy() {
counter.remove();
},
};
},
}),
];
},
});

// 使用自定义扩展
const editor = useEditor({
extensions: [StarterKit, CharacterCount],
});

Tiptap 实时协作示例


Tiptap 与 Yjs 集成实现实时协作非常简单:


npm install yjs y-prosemirror @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor

import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Collaboration from "@tiptap/extension-collaboration";
import CollaborationCursor from "@tiptap/extension-collaboration-cursor";
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";

// 创建 Yjs 文档和提供者
const ydoc = new Y.Doc();
const provider = new WebrtcProvider("room-name", ydoc);

function CollaborativeEditor() {
const editor = useEditor({
extensions: [
StarterKit,
Collaboration.configure({
document: ydoc,
}),
CollaborationCursor.configure({
provider,
}),
],
});

return <EditorContent editor={editor} />;
}

从代码看差异


让我们通过实现一个带工具栏的编辑器来对比两者的代码复杂度:


在 ProseMirror 中,需要手动管理所有状态和命令:


import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { schema } from "prosemirror-schema-basic";
import { toggleMark } from "prosemirror-commands";

const state = EditorState.create({ schema });

const toolbarPlugin = new Plugin({
view(editorView) {
const toolbar = document.createElement("div");
toolbar.className = "toolbar";

const boldBtn = document.createElement("button");
boldBtn.textContent = "B";
boldBtn.onclick = (e) => {
e.preventDefault();
const { state, dispatch } = editorView;
const command = toggleMark(schema.marks.strong);
if (command(state, dispatch)) {
editorView.focus();
}
};

toolbar.appendChild(boldBtn);
document.body.insertBefore(toolbar, editorView.dom);

return {
destroy() {
toolbar.remove();
},
};
},
});

const view = new EditorView(document.querySelector("#editor"), {
state: EditorState.create({
schema,
plugins: [toolbarPlugin],
}),
});

而在 Tiptap 中,相同的功能实现更加简洁:


const editor = useEditor({
extensions: [StarterKit],
});

return (
<div>
<button
onClick={() =>
editor.chain().focus().toggleBold().run()}
className={editor.isActive("bold") ? "is-active" : ""}
>
B
</button>
<EditorContent editor={editor} />
</div>

);

如何做出选择


选择 Tiptap 还是 ProseMirror,关键在于项目需求和开发团队的技术能力。


如果你的目标是快速构建一个功能丰富、用户友好的富文本编辑器,且不希望花费过多时间在底层细节上,Tiptap 是一个理想的选择。它提供了简洁的 API 和现成的 UI 组件,可以快速启动和开发。如果你的编辑器需要一些定制功能,但不需要完全控制每个底层细节,Tiptap 提供了足够的灵活性,同时保持了开发的简便性。如果需要实现多人实时协作,Tiptap 内建的对 Yjs 等库的支持可以简化实现过程。


如果你需要完全控制编辑器的行为、界面和性能,ProseMirror 提供了更高的自由度。它适合那些有特定需求的项目,比如自定义文档结构、输入行为或非常复杂的编辑操作。在处理非常大的文档或需要极高性能的场景下,ProseMirror 能提供更好的优化和性能。如果你的项目需要完全自定义插件,或者你想对编辑器进行深度定制,ProseMirror 提供了更高的灵活性。


性能考虑


对于大文档处理,ProseMirror 提供了更细粒度的控制:


// ProseMirror 中可以精确控制更新
const state = EditorState.create({
schema,
plugins: [
// 可以精确控制哪些插件启用
// 可以自定义更新逻辑
new Plugin({
state: {
init() {
return {};
},
apply(tr, value) {
// 自定义状态更新逻辑
return value;
},
},
}),
],
});

而 Tiptap 虽然性能良好,但在极端场景下可能不如直接使用 ProseMirror 优化:


// Tiptap 的性能优化选项
const editor = useEditor({
extensions: [StarterKit],
editorProps: {
attributes: {
class:
"prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none",
},
// 可以传递 ProseMirror 的原生配置
},
// 但仍然受到封装层的限制
});

生态系统和社区支持


Tiptap 拥有丰富的扩展生态系统:


# Tiptap 官方扩展
npm install @tiptap/extension-image
npm install @tiptap/extension-link
npm install @tiptap/extension-table
npm install @tiptap/extension-code-block-lowlight
npm install @tiptap/extension-placeholder
npm install @tiptap/extension-character-count
npm install @tiptap/extension-typography

而 ProseMirror 的插件需要通过 prosemirror-* 包系列来获取,或者自己实现。官方提供了基础插件,但高级功能需要社区插件或自行开发。


实际项目场景建议


对于博客平台、内容管理系统、笔记应用等常见场景,Tiptap 通常是最佳选择。它的快速开发和丰富的功能足以满足大多数需求。代码示例展示了如何在几分钟内搭建一个功能完整的编辑器。


对于需要特殊文档结构(如学术论文编辑器、代码编辑器、专业排版工具)或对性能有极致要求的场景,ProseMirror 提供了必要的底层控制能力。但需要投入更多时间学习其 API 和概念。


如果你的团队时间有限,或者希望快速迭代,Tiptap 是明智的选择。如果团队有富文本编辑器开发经验,或者有充足时间进行深度定制,ProseMirror 可以带来更高的灵活性和性能。


总结


Tiptap 是一个基于 ProseMirror 的富文本编辑器框架,适合需要快速开发、易用且功能丰富的场景。它封装了 ProseMirror 的复杂性,让开发者能够专注于业务逻辑,而无需关心底层实现细节。通过本文的代码示例可以看出,Tiptap 的 API 设计更加直观,学习曲线平缓,适合大多数项目需求。


ProseMirror 则是一个底层框架,适合那些需要完全控制文档结构、编辑行为和性能优化的高级开发者。它更灵活,但学习曲线较陡峭,适合复杂或定制化需求较强的项目。从代码示例中可以看到,使用 ProseMirror 需要处理更多的底层细节,但同时也获得了更高的控制权。


如果你的项目需要快速构建编辑器并具备一定的自定义能力,Tiptap 是一个更为理想的选择。而如果你的项目需要完全的定制化和高性能处理,ProseMirror 将更符合你的需求。最终的选择应基于你的开发需求、项目规模以及团队的技术能力。建议通过实际代码尝试两者,根据你的具体场景做出最适合的选择。


作者:Moment
来源:juejin.cn/post/7593573617647796276
收起阅读 »

瞧瞧别人家的日志打印,那叫一个优雅!

前言 这篇文章跟大家一起聊聊打印优质日志的10条军规,希望对你会有所帮助。 第1条:格式统一 反例(管理看到会扣钱): log.info("start process"); log.error("error happen"); 无时间戳,无上下文。 正解...
继续阅读 »

前言


这篇文章跟大家一起聊聊打印优质日志的10条军规,希望对你会有所帮助。



第1条:格式统一


反例(管理看到会扣钱)


log.info("start process");
log.error("error happen");

无时间戳,无上下文。


正解代码


<!-- logback.xml核心配置 -->
<pattern>
%d{yy-MM-dd HH:mm:ss.SSS}
|%X{traceId:-NO_ID}
|%thread
|%-5level
|%logger{36}
|%msg%n
</pattern>

在logback.xml中统一配置了日志的时间格式、tradeId,线程、等级、日志详情都信息。


日志的格式统一了,更方便点位问题。



第2条:异常必带堆栈


反例(同事看了想打人)


try {
processOrder();
} catch (Exception e) {
log.error("处理失败");
}

出现异常了,日志中没打印任何的异常堆栈信息。


相当于自己把异常吃掉了。


非常不好排查问题。


正确姿势


log.error("订单处理异常 orderId={}", orderId, e); // e必须存在!

日志中记录了出现异常的订单号orderId和异常的堆栈信息e。


第3条:级别合理


反面教材


log.debug("用户余额不足 userId={}", userId); // 业务异常应属WARN
log.error("接口响应稍慢"); // 普通超时属INFO

接口响应稍慢,打印了error级别的日志,显然不太合理。


正常情况下,普通超时属INFO级别。


级别定义表


级别正确使用场景
FATAL系统即将崩溃(OOM、磁盘爆满)
ERROR核心业务失败(支付失败、订单创建异常)
WARN可恢复异常(重试成功、降级触发)
INFO关键流程节点(订单状态变更)
DEBUG调试信息(参数流水、中间结果)

第4条:参数完整


反例(让运维骂娘)


log.info("用户登录失败");

上面这个日志只打印了“用户登录失败”这个文案。


谁在哪登录失败?


侦探式日志


log.warn("用户登录失败 username={}, clientIP={}, failReason={}", 
username, clientIP, "密码错误次数超限");

登录失败的业务场景,需要记录哪个用户,ip是多少,在什么时间,登录失败了,失败的原因是什么。


时间在logback.xml中统一配置了格式。


这样才方便快速定位问题:



第5条:数据脱敏


血泪案例

某同事打印日志泄露用户手机号被投诉。


我在记录的日志中,需要对一下用户的个人敏感数据做脱敏处理。


例如下面这样:


// 脱敏工具类
public class LogMasker {
public static String maskMobile(String mobile) {
return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}

// 使用示例
log.info("用户注册 mobile={}", LogMasker.maskMobile("13812345678"));

第6条:异步保性能


问题复现

某次秒杀活动中直接同步写日志,导致大量线程阻塞:


log.info("秒杀请求 userId={}, itemId={}", userId, itemId); 

高并发下IO阻塞。


致命伤害分析:



  1. 同步写日志导致线程上下文切换频繁

  2. 磁盘IO成为系统瓶颈

  3. 高峰期日志打印耗时占总RT的25%


正确示范(三步配置法)


步骤1:logback.xml配置异步通道


<!-- 异步Appender核心配置 -->  
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志的阈值:当队列剩余容量<此值时,TRACE/DEBUG级别日志将被丢弃 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列深度:建议设为 (最大并发线程数 × 2) -->
<queueSize>4096</queueSize>
<!-- 关联真实Appender -->
<appender-ref ref="FILE"/>
</appender>


步骤2:日志输出优化代码


// 无需前置判断,框架自动处理  
log.debug("接收到MQ消息:{}", msg.toSimpleString()); // 自动异步写入队列

// 不应做复杂计算后再打印(异步前仍在业务线程执行)
// 错误做法:
log.debug("详细内容:{}", computeExpensiveLog());


流程图如下:


步骤3:性能关键参数公式


最大内存占用 ≈ 队列长度 × 平均单条日志大小  
推荐队列深度 = 峰值TPS × 容忍最大延迟(秒)
例如:10000 TPS × 0.5s容忍 ⇒ 5000队列大小

风险规避策略



  1. 防队列堆积:监控队列使用率,达80%触发告警

  2. 防OOM:严格约束大对象toString()的调用

  3. 紧急逃生:预设JMX接口用于快速切换同步模式


第7条:链路追踪


混沌场景

跨服务调用无法关联日志。


我们需要有链路追踪方案。


全链路方案


// 拦截器注入traceId
MDC.put("traceId", UUID.randomUUID().toString().substring(0,8));

// 日志格式包含traceId
<pattern>%d{HH:mm:ss} |%X{traceId}| %msg%n</pattern>

可以在MDC中设置traceId。


后面可以通过traceId全链路追踪日志。


流程图如下:


第8条:动态调参


半夜重启的痛

线上问题需要临时开DEBUG日志,比如:查询用户的某次异常操作的日志。


热更新方案


@GetMapping("/logLevel")
public String changeLogLevel(
@RequestParam String loggerName,
@RequestParam String level)
{

Logger logger = (Logger) LoggerFactory.getLogger(loggerName);
logger.setLevel(Level.valueOf(level)); // 立即生效
return "OK";
}

有时候我们需要临时打印DEBUG日志,这就需要有个动态参数控制了。


否则每次调整打印日志级别都需要重启服务,可能会影响用户的正常使用。


journey
title 日志级别动态调整
section 旧模式
发现问题 --> 修改配置 --> 重启应用 --> 丢失现场
section 新模式
发现问题 --> 动态调整 --> 立即生效 --> 保持现场

第9条:结构化存储


混沌日志


用户购买了苹果手机 订单号1001 金额8999

上面的日志拼接成了一个字符串,虽说中间有空格分隔了,但哪些字段对应了哪些值,看起来不是很清楚。


我们在存储日志的时候,需要做结构化存储,方便快速的查询和搜索。


机器友好式日志


{
"event": "ORDER_CREATE",
"orderId": 1001,
"amount": 8999,
"products": [{"name":"iPhone", "sku": "A123"}]
}

这里使用了json格式存储日志。


日志中的数据一目了然。


第10条:智能监控


最失败案例

某次用户开通会员操作,错误日志堆积3天才被发现,黄花菜都凉了。


我们需要在项目中引入智能监控。


ELK监控方案



报警规则示例


ERROR日志连续5分钟 > 100条 → 电话告警  
WARN日志持续1小时 → 邮件通知

总结


研发人员的三大境界



  1. 青铜System.out.println("error!")

  2. 钻石:标准化日志 + ELK监控

  3. 王者

    • 日志驱动代码优化

    • 异常预测系统

    • 根因分析AI模型




最后的灵魂拷问

下次线上故障时,你的日志能让新人5分钟定位问题吗?


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。


求一键三连:点赞、转发、在看。


关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。


作者:苏三说技术
来源:juejin.cn/post/7593232758128246790
收起阅读 »

一杯奶茶钱,PicGo + 阿里云 OSS 搭建永久稳定的个人图床

大家好,我是老刘今天不聊Flutter开发,聊聊程序员常用的markdown工具。最近这两天是用阿里云oss搞了个图床,发现还是有很多细节问题的,给大家分享一下。这件事的起因是之前一直用的写文章的在线服务出了点问题,现在想直接在本地Trae或者Obsidian...
继续阅读 »

大家好,我是老刘

今天不聊Flutter开发,聊聊程序员常用的markdown工具。

最近这两天是用阿里云oss搞了个图床,发现还是有很多细节问题的,给大家分享一下。

这件事的起因是之前一直用的写文章的在线服务出了点问题,现在想直接在本地Trae或者Obsidian中写文章(markdown格式),但是为了复制到公众号方便,要自己搞一个图床,否则图片就不能直接复制到公众号和其它平台了。

研究了一圈后最终选择了PicGo 配合阿里云 OSS(对象存储)。

为啥选阿里云 OSS?

这目前最稳定、速度最快且性价比极高的图床方案之一。

而且是目前还有3个月的免费试用。

虽然它不是完全免费(需要极少的存储费和流量费,个人使用通常一年不到一杯奶茶钱),但带来的体验升级远超免费图床。

阿里云 OSS 是按量付费的,主要包含两部分:

  1. 存储费 : 存多少收多少,个人博客通常只有几百 MB,一个月0.12元。
  2. 流量费 : 有人访问你的图片才收费 。
    • 00:00 - 08:00 (闲时): 0.25元/GB
    • 08:00 - 24:00 (忙时): 0.50元/GB

  • 注:不同地域价格微调,以上为参考。

为啥不选免费图床?

  • GitHub 的问题: 由于国内网络原因,GitHub 的链接经常超时。这会导致发布平台抓取图片失败,最后不得不一张张手动重新上传图片,效率极低。
  • Gitee 的问题:

    1. PicGo本身默认移除了 Gitee 官方支持(旧版可能有,新版需插件)。
    2. Gitee 为了防止滥用,限制了文件的外链访问,也就是访问量大有可能失败。(这个是听人说的,我自己在Obsidian笔记中用没有发现)
    3. 担心如果被系统判定为图床仓库,可能会导致仓库甚至账号被封禁。

接下来是 从零开始配置 PicGo + 阿里云 OSS 的详细保姆级教程。


配置方案

第一阶段:阿里云 OSS 配置 (只需做一次)

1. 开通 OSS 服务

  1. 登录 http://www.aliyun.com/
  2. 搜索 "对象存储 OSS",点击进入控制台。
  3. 点击 "开通服务"(如果已开通则跳过)。

2. 创建 Bucket (存储桶)

  1. 在 OSS 控制台左侧菜单,点击 Bucket 列表 -> 创建 Bucket
  2. 填写配置:
    • Bucket 名称: 起个名字(例如 oss_img)。
    • 地域 (Region): 选择离你或者你的用户访问地点最近的(例如 华北2 (北京))。记住这个地域,后面要用
    • 存储类型: 标准存储。
    • 读写权限 (ACL)公共读 (Public Read)。这一点非常重要,否则图片链接发给别人无法访问。
    • 其他选项默认即可。
  3. 点击确定创建。

3. 获取 AccessKey (密钥)

为了安全,不建议使用主账号的 Key,建议创建一个专门用于 OSS 的子账号。

回到 OSS 控制台的主界面,注意不是Bucket管理界面。

  1. 鼠标悬停在右上角头像,选择 AccessKey 管理 -> 使用RAM用户 AccessKey
  2. 进入 RAM 访问控制台,点击 创建用户
    • 登录名称: 例如 picgo-user
    • 访问方式: 勾选 API 访问
  3. 点击确定,立即复制保存 AccessKey ID 和 AccessKey Secret(Secret 只显示这一次,丢了要重新建)。
  4. 给子用户授权
    • 在用户列表页面,找到刚才创建的 picgo-user,点击右侧 添加权限
    • 搜索 OSS,选择 AliyunOSSFullAccess (或者更精细的权限,简单起见选 FullAccess)。
    • 点击确定。

第二阶段:PicGo 客户端/插件配置

我是用的是PicGo 桌面版,因为一方面我的场景是在Trae和Obsidian中都要用,另一方面,vs-picgo插件已经好几年没更新了。

所以如果你经常要在 Typora、Obsidian 等多个软件里用图床,建议配置桌面版。

而且日常使用我都是通过快捷键操作,桌面版和插件的操作复杂度是一样的,桌面版还有更全面的功能。

配置 PicGo 桌面版

  1. 下载并安装 PicGo 桌面版。
  2. 打开 PicGo -> 图床设置 -> 阿里云 OSS

  1. 填写配置:
    • 设定 KeyIdAccessKey ID
    • 设定 KeySecretAccessKey Secret
    • 设定存储空间名: Bucket 名称 (例如 oss_img)
    • 确认存储区域: 也就是 Area,例如 oss-cn-beijing
    • 指定存储路径img/
  2. 点击 确定 和 设为默认图床

第三阶段:验证与使用

  1. 测试上传:

    • 拖拽一张图片到 PicGo 主窗口。
    • 或者随便截张图,然后按 Ctrl+Shift+P 自动上传 看到上传成功的提示后生成的链接已经自动复制到剪贴板。
  2. 检查链接:

其它事项

本地冗余还是同城冗余

对于 个人图床 (博客、笔记图片)这种场景,答案非常明确,请选择本地冗余 (LRS)

  1. 更便宜 (核心理由)

    • 本地冗余 (LRS): 价格较低(标准存储约 0.12元/GB/月)。
    • 同城冗余 (ZRS): 价格较高(标准存储约 0.15元/GB/月),比本地冗余贵约 25%
    • 对于个人用户,没必要多花这份钱,特别是存储量大的时候差距还是比较大的。
  2. 可靠性已经足够高

    • 本地冗余的意思是:你的数据会存在阿里云同一个机房内的不同设备上。除非这整个机房发生灾难性毁灭(如大地震彻底摧毁机房),否则数据不会丢。数据可靠性高达 99.999999999% (11个9)。
    • 同城冗余的意思是:你的数据会存在同一个城市的三个不同机房里。只有当这个城市的三个机房同时全挂了,数据才会丢。数据可靠性高达 12个9。

对于个人博客图片,本地冗余 (11个9) 的安全性已经远远溢出了。即使真的遇到极小概率的机房故障,阿里云通常也有修复手段。为了那几十块钱的图片去买“抗核弹级”的同城冗余,属于过度消费。

如果你实在不放心,就写个脚本定期把图片备份到其它平台即可。

防坑指南

特别注意:不要把 AccessKey 泄露给别人

阿里云 OSS 是按量付费的,主要包含两部分:

  1. 存储费: 存多少收多少,个人博客通常只有几百 MB,这个很少。
  2. 流量费: 有人访问你的图片才收费。

如果你的图片是自己使用,比如Obsidian笔记,或者发布到公众号文章,那么问题不大。

比如老刘自己的Obsidian笔记只有几台电脑和手机同步使用,用量很小。

如果发布到公众号、csdn等平台也问题不大,因为各个平台都会抓取你的图片,发布的文章中使用的是平台的图片链接,而不是你自己的图片链接。

但是如果你是自建的个人博客,要注意一下访问量。

  • 如果你的博客访问量非常巨大(每天几万),流量费会是一笔开支。对于普通个人博客,一年通常也就几十块钱。
  • 可以在阿里云后台设置 “消费预警”,例如设置消费超过 10 元发短信通知,防止被恶意刷流量。

最后再说两句

折腾这么一圈,其实就为了一个目的:让写作回归写作本身

我们花时间去配置图床、优化流程,不是为了成为工具大师,而是为了在灵感迸发的那一刻,不会因为图片上传失败这种破事而打断思路。

工具最好的状态,就是当你用它的时候,你根本感觉不到它的存在。

配置好 PicGo + 阿里云 OSS,把琐碎的技术细节丢给自动化工具,剩下的,就是尽情释放你的创造力了。

毕竟,内容,才是一切的王道

如果看到这里的同学对客户端或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。

私信免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。

可以作为Flutter学习的知识地图。

—— laoliu_dev


作者:程序员老刘
来源:juejin.cn/post/7594000527510093865
收起阅读 »

MCP 是最大骗局?Skills 才是救星?

尤记得上半年大家对 MCP 的狂热,遇人就会和我聊到 MCP。然而从落地使用上似乎不是这么个情况。社区里面流传着一句话:MCP 是一个开发者远超使用者的功能。那么 MCP 真的是世上最大骗局吗? 如果你是 AI 工具的用户(而不是开发者),这篇文章可能会从另...
继续阅读 »

尤记得上半年大家对 MCP 的狂热,遇人就会和我聊到 MCP。然而从落地使用上似乎不是这么个情况。社区里面流传着一句话:MCP 是一个开发者远超使用者的功能。那么 MCP 真的是世上最大骗局吗?



如果你是 AI 工具的用户(而不是开发者),这篇文章可能会从另一角度来尝试解释:为什么 MCP 这么火,但你用起来总觉得”没什么用”?Skills 为什么可能才是你真正需要的东西。




一、MCP:开发者的狂欢,用户的懵圈


MCP(Model Context Protocol)在 2024 年底由 Anthropic 发布,号称是 AI 领域的”USB-C”——一个标准化的协议,让 AI 可以连接各种外部工具。


听起来很美好。但现实是社区里充斥着对 MCP 的嘲讽,称其为”最大骗局”。



MCP 可能是唯一开发者比使用者还多的技术 开发者为什么吐槽 MCP 协议?




这意味着什么?


大量开发者在「研究」MCP,但真正能给用户用的工具少得可怜。


SDK 月下载量 9700 万次,Registry 增长 407%——开发者热情高涨。但作为用户,你打开 Claude 或 Cursor,想找个好用的 MCP 工具,大概率还是会失望。


这不是 MCP 的错。这是它的「基因」决定的。




二、协议的「基因」决定了谁受益


为什么 MCP 对用户不友好?也许答案藏在协议设计本身。


我们对比下 MCP 和 Skills(Agent Skills)两个协议的规范:


维度MCPSkills
协议规定的是开发者怎么写工具AI 怎么用能力
规范内容API、Schema、SDKMarkdown、Instructions
开发者上手门槛懂 JSON Schema + SDK会写 Markdown


来源:modelcontextprotocol.io / agentskills.io



MCP 协议规定的是「开发者怎么写工具」——API 接口怎么定义、数据结构怎么传递、SDK 怎么集成。这些对开发者很重要,但普通用户根本不关心。


Skills 协议规定的是「AI 怎么用能力」——什么时候加载、怎么理解指令、如何执行任务。这些直接影响用户体验。



即使开发者来说,Skill 的上手成本也都远低于 MCP。甚至简单的 Skills,使用者可以在使用过程中无缝切换为开发者,边用边优化


一句话总结:MCP 面向开发者,尽力优化了开发体验,在 Agent 如何使用这些工具上却没有给出太多指导;Skill 面向使用者,优化使用体验(包括成本),在 Agent 如何使用这些工具上给出了很多指导。


协议的设计目标,决定了谁能从中获益。




三、Skills 的杀手锏:渐进式披露


除了设计目标不同,Skills 还有一个技术上的优势:渐进式披露(Progressive Disclosure)


这是什么意思?用一个类比来解释:图书馆找书。


想象你去图书馆找资料:


MCP 方式:管理员把整个书架的书全搬到你面前。结果:信息过载,找不到重点 📚📚📚


Skill 方式:管理员先给你一本目录,你说要哪本再拿哪本。结果:精准高效 📋→📖



AI 的「脑容量」有限(Context Window)。



  • 传统方式:一次性加载所有工具定义。假设有 100 个工具,可能占用几十万 tokens。

  • Skill 方式:启动时只加载名称和描述(约 100 tokens/skill)。需要哪个,再加载哪个的详细指令。


根据 agentskills.io 官方规范



  • 元数据层:~100 tokens/skill

  • 完整指令:建议 <5000 tokens


这意味着什么?100 个 Skills,启动时只需要约 10,000 tokens 的元数据。而不是一股脑塞进去几十万 tokens。



  1. AI 不会被无关信息干扰,更聪明

  2. 响应更快

  3. 能支持更多工具




四、两者不是对手,是搭档


说了这么多 Skills 的好话,是不是意味着 MCP 没用了?不是。


MCP 和 Skills 解决的是不同层次的问题



  • MCP = 工具箱:定义了「能连接什么」——数据库、API、文件系统、第三方服务

  • Skills = 使用手册:定义了「怎么聪明地用这些工具」——工作流程、最佳实践、按需加载



它们也可以结合使用:



用 Skills 的渐进式披露来管理 MCP 工具。



MCP 负责「连接」,Skills 负责「智慧」。组合是一个好的解决方案。




五、给用户的建议



  1. 别光被 MCP 的热度带节奏。22000+ 个仓库听起来很多,但落地的有多少呢?

  2. 关注 Skills 生态。如果你用 Claude Code 等工具(近期 Kwaipilot 也会支持),Skills 可能比 MCP 更能直接提升你的体验。

  3. 两者都关注。长期来看,MCP + Skills 的组合可能是一种选择。MCP 提供连接能力,Skills 提供使用智慧。

  4. 2026 年:渐进式披露和动态上下文管理会成为 AI 工具的标配。近期我的一个实践 —— 基于 20w 字的 Specs 来让 Agent 实现一个 10pd 需求 —— 也是通过渐进式披露 Specs。Cursor 也已经给出了很好的解释




结语


MCP 是最大骗局吗?不是。它也是一个优秀的开发者协议。


Skills 是救星吗?对用户来说,目前来说可能是的。


协议的设计目标,决定了谁能从中获益。 MCP 让开发者更容易写工具,Skills 让用户更容易用工具。


如果你是用户,别纠结 MCP 为什么”不好用”了。去看看 Skills 吧。




参考链接



作者:AlienZHOU
来源:juejin.cn/post/7594420277449015323
收起阅读 »

🌸 入职写了一个月全栈next.js 感想

web
背景介绍 最近组内要做0-1的新ai产品, 招我进来就是负责这个ai产品,启动的时候这个季度就剩下两个月了,天天开会对齐进度,一个月就已经把基础版本给做完了,想要接入到现有的业务上面,时间方面就特别紧张,技术选型怎么说呢, leader用ai写了一个版本 我...
继续阅读 »

背景介绍



  • 最近组内要做0-1的新ai产品, 招我进来就是负责这个ai产品,启动的时候这个季度就剩下两个月了,天天开会对齐进度,一个月就已经把基础版本给做完了,想要接入到现有的业务上面,时间方面就特别紧张,技术选型怎么说呢, leader用ai写了一个版本 我们在现有的代码进行二次开发这样, 全栈next.js 要学习的东西太多了 又没有前端基础,没有ai coding很难完成任务(十几分钟干完我一天的工作 claude4.5效果还不错 进度推的特别快), 自从trae下架了claude,后面就一直cursor claude 4.5了。



    • nextjs+ts+tailwindcss+shadcn ui现在是mvp套餐,startup在梭哈,时间就是生产力哪需要那么多差异化样式直接一把,有的💰才开始做细节,你会发现慢慢也💩化了。

    • Nextjs 是全栈框架 可以很快把一个MVP从零到一完整跑起来。 你要是抬杠说什么高并发负载均衡啥的,你的用户数量真多到需要考虑性能的时候,你已经不需要自己考虑了(小红书看到的一段话 挺符合场景的)

    • next.js 写后端 确实比较轻量 只能做一些curd的操作 socket之类的不太合适 其他api 还是随便开发 给我的感受就是前端能够直接操作db,前后端仓库可以不分离,业务逻辑还是一定要分离的 看看开源的next.js 项目的架构设计结构是怎么样的 学习/模仿/改造。

    • 语言只是工具,适合最重要,技术没有银弹



  • nextjs.org/ github.com/vercel/next…
    image.png


项目的时间线



项目从启动到这周 大概是5周的时间




  • 10/28-10/31 Week 1

    • 项目初始化/需求讨论/设计文档/

    • 后端next.js, typescript技术熟悉 项目运行/调试

    • 基础框架搭建 设计表结构ddl, 集成mysql, 编写crud接口阶段



  • 11/03-11/07 Week 2

    • 产品PRD 提供

    • xxxx等表设计



  • 11/10-11/14 Week 3

    • xxxxx 基本功能完结

    • @xxxx 讲解项目结构/规范



  • 11/17-11/21 Week 4

    • 首页样式/逻辑 优化

    • 集成统一登录调研

    • 部署完成



  • 11/24-11/28 Week 5

    • 服务推理使用Authorization鉴权 对内接口使用Cookies (access_token) 鉴权 开发

    • xxxx 表设计表设计 逻辑开发

    • xxx设计 设计开发

    • 联调xxxx





5周时间 功能基本完成了 剩下的就是部署到线上 进行场景实践了



前端技术栈



  • Next.js 14:选择 App Router 架构,支持服务端渲染和 API Routes

  • TypeScript 5.4:强类型语言提升代码质量和可维护性

  • React 18:利用并发特性和 Suspense 提升用户体验

  • Zustand:轻量级状态管理,替代 Redux 降低复杂度

  • Ant Design + Radix UI:组件库组合,平衡美观性和可访问性


React + TypeScript react.dev/



  • 优势:类型安全:TypeScript 提供编译时类型检查,减少运行时错误 ✅ 组件化开发:高度可复用的组件设计 ✅ 生态成熟:丰富的第三方库和工具链 ✅ 开发体验:优秀的 IDE 支持和调试工具

  • 劣势:学习曲线:TypeScript 对新手有一定门槛 ❌ 编译时间:大型项目编译可能较慢 ❌ 配置复杂:类型定义需要额外维护


UI 组件方案 Ant Design + Radix UI 混合方案



  • 优势:快速开发:Ant Design 提供完整的企业级组件 ✅ 无障碍性:Radix UI 提供符合 WAI-ARIA 标准的组件 ✅ 定制灵活:Radix UI 无样式组件便于自定义 ✅ 中文支持:Ant Design 对中文界面友好

  • 劣势:包体积大:两个 UI 库增加了打包体积 ❌ 样式冲突:需要注意两个库的样式隔离❌ 维护成本:需要同时维护两套组件系统


Tailwind CSS



  • 优势:开发效率高:原子化类名,快速构建 UI ✅ 体积优化:生产环境自动清除未使用的样式 ✅ 一致性:设计系统内置,确保视觉一致 ✅ 响应式:便捷的响应式设计工具

  • 劣势:类名冗长:HTML 可能变得难以阅读 ❌ 学习成本:需要记忆大量类名 ❌ 非语义化:类名不直观反映元素意义


ant design x


ahooks


后端技术栈



  • Prisma 6.18:现代化 ORM,类型安全且支持 Migration

  • MySQL:成熟的关系型数据库,满足复杂查询需求

  • Redis (ioredis) :高性能缓存,支持多种数据结构

  • Winston:企业级日志系统,支持日志轮转和结构化输出

  • Zod:运行时类型验证,保障 API 数据安全


Next.js API Routes



  • 优势:统一代码库:前后端在同一项目中 ✅ 类型共享:TypeScript 类型可在前后端复用 ✅ 开发效率:无需配置跨域、代理等 ✅ 部署简单:单一应用部署

  • 劣势:扩展性限制:无法独立扩展后端服务 ❌ 性能瓶颈:Node.js 单线程可能成为瓶颈 ❌ 微服务困难:不适合复杂的微服务架构


Prisma ORM



  • 优势:类型安全:自动生成 TypeScript 类型 ✅ 迁移管理:声明式 schema,易于版本控制 ✅ 查询性能:生成优化的 SQL 查询 ✅ 关系处理:直观的关系查询 API ✅ 多数据库支持:支持 MySQL、PostgreSQL、SQLite 等

  • 劣势:复杂查询:某些复杂 SQL 可能需要原始查询 ❌ 生成代码体积:生成的 client 文件较大 ❌ 版本升级:大版本升级可能需要迁移


踩坑记录



主要是记录一些开发过程中踩坑 和设计问题




  • node js 项目 jean部署

  • 自定义配置/dockerfile配置 没有类似项目参考 健康检查问题 加上环境变量配置多环境 一步一步

  • next.js 中 用middleware进行接口拦截鉴权 里面有prisma path import 直接出现了Edge Runtime 异常 自定义auth 解决

  • npm build 项目 踩坑

  • 静态渲染流程 动态api 警告 强制动态渲染

  • 其他组件 document 不支持build问题

  • 保存多场景模式+构建版本管理第一版考虑的太少了,发现有问题 后面又重构了一版本

  • xxx日志目前还没有接入 要不就是日志文件 要不就是console.log 目前看日志的方式是去容器化运行日志看了 后续集群部署就比较麻烦了

  • ant design 版本降低到6.0以下 ant-design x 用不了2.0.0 的一些对话组件


Next.js实践的项目记录


苏州 trae friends线下黑客松 📒



  • 去Trae pro-Solo模式 苏州线下hackathon一趟, 基本都是一些独立开发者,一人一公司,三个小时做出一个产品用Trae-solo coder模式,不得不说trae内部集成的vercel部署很丝滑 react项目一键deploy访问 完全不用关系域名服务器, solo模式其实就是混合多种model使用进行输出 想要的效果还是得不断的调试 thiking太长,对于前后端分离项目 也能够同时关联进行思考规划。

  • 1点多到4点 coding时间 从0-1生成项目 使用trae pro solo模式 就3个小时 做不了什么大的东西 那就做个日语50音的网站呗 现场酒店的网基本用不了 我数据也很卡 用的旁边干中学老师的热点 用next.js tailwindcss ant design deepseek搭建的网页 够用了 最后vercel部署 trae自带集成 挺方便的 solo模式还是太慢了 接受不了 网站地址是 traekanastudio1ssw.vercel.app/ 功能就是假名+ai生成例句和单词 我都没有路演 最后拿优秀奖可能是我部署了吧 大部分人没部署 优秀奖就是卫衣了 蹭了一天的饭加零食 爽吃

  • http://www.xiaohongshu.com/explore/692… 小红书当时发的帖子 可以领奖品


image.png


Typescript的AI方向 langchain/langgraph支持ts



  • 最近在看的ts的ai框架 发现langchain 是支持ts的, langchain-chat 主要是使用langchain+langgraph 对ts进行实践 traechat-apps4y6.vercel.app/

  • 部署还踩坑了 MCP 在 Vercel 上不生效是因为 Vercel 是 serverless 环境,不支持运行持久的子进程。让我帮你解决这个问题:

  • 主要是对最近项目组内要用的到mcp/function call 进行实践操作 使用modelscope 上面开源的mcp进行尝试 使用vercel进行部署。

  • 最近看到小红书上面的3d 粒子 圣诞树有点火呀,自己也尝试下 效果很差 自己弄的提示词 可以去看看帖子上的提示词去试试 他们都是gemini pro 3玩的 我也去弄个gemini pro 3 账号去玩玩。

  • 还有一个3d粒子 跟着音乐动的的效果 下面的提示词可以试试


帮我构建一个极简科幻风格的 3D 音乐可视化网页。
视觉上参考 SpaceX 的冷峻美学,全黑背景,去装饰化。核心是一个由数千个悬浮粒子组成的‘生命体’,它必须能与声音建立物理连接:低音要像心脏搏动一样冲击屏幕,高音要像电流一样瞬间穿过点阵。
重点实现一种‘ACID 流体’视觉引擎:让粒子表面的颜色不再是静态的,而是像两种粘稠的荧光液体一样,在失重环境下互相吞噬、搅拌、流动,且流速由音乐能量驱动。

c88886c4c8c3a180a2dba52f17125dc1.jpg



image.png


image.png



ai方向 总结




  • a2a解决的是agent之间如何配合工作的问题 agent card定义名片 名称 版本 能力 语言 格式 task委托书 通信方式http 用户 客户端是主控 接受用户需求 制定具体任务 向服务器发出需求 任务分发 接受响应 服务器是各类部署好的agent 遵循一套结构化模式

  • mcp 解决的llm自主调用功能和工具问题

  • mcp 是解决 function call 协议的碎片化问题,多 agent 主要是为了做上下文隔离

  • 比如说手机有一个system agent 然后各个app有一个agent,用户语音输入买咖啡,然后system agent调用瑞幸agent 这样就是非侵入式 让app暴露系统a2a接口,感觉比mcp要更合理一点,不是单纯让app暴露tools,系统agent只需要做路由

  • 而且有一点我觉得挺有意思的,就是自己的agent花的token是自己的钱,如果自己的agent找别人的agent,让它执行任务啥的,花的不就是别人的钱……

  • Dify:更像宜家的模块化家具,提供可视化工作流、预置模板,甚至支持“拖拽式”编排AI能力。比如,你想做一
    个智能客服,只需在界面里连接对话模型、知识库和反馈按钮,无需写一行代码



python 和ts 在ai上面的比较




  • Python 依然是 AI 训练和科研的王者,PyTorch、TensorFlow、scikit-learn 这些生态太厚实了,训练大模型你离不开它。

  • TS 在底层 AI 能力上还没那么能打,GPU 加速、模型优化这些,暂时还得靠 Python 打底。

  • Python 搞理论和模型,TypeScript卷体验和交付


个人学习记录



主要还是前端和ai方面的知识点学习的比较多吧




Vibe Coding



  • 先叠甲, 我没有前端的开发经验,第一次写前端项目,项目里面90%的前端代码都是ai 生成的,能够让你一个不会前端的同学也快速完成mvp版本/需求任务。我虽然很推ai coding 很喜欢用, 即时反馈带来的成就感, 但是对于生成的代码是不是屎山 大概率可能是了, 因为前期 AI速度快,制造屎山的速度更快。无论架构设计多优秀,也难避免屎山代码的宿命: 需求一直在变,你的架构设计是针对老的需求,随着新的需求增加,老的架构慢慢的就无法满足了,需要重构。

  • 一起开发的前端同事都说ai生成那些样式互相影响了,样式有tailwindcss 有自定义的css 每个模块又有不同 大概率出问题 有冲突,就是💩山。

  • 最大的开发障碍就是内心的偏见 不愿意放弃现在所擅长的东西 带着这份偏见不愿意去学习



对于ai coding 的话 用过trae-pro/cursor/qoder/copilot/codex等等 最终还是cursor claude 4.5用的最舒服



image.png



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


image.png



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



image.png


9e0f1cde2dbc3314e44150d1e544c77a.png



  • 最后就是需要学好英语 前端的技术文档都是英文的 虽然有中文的翻译版本, 但没有自己直接去看官方的强 难免有差异, 我现在都是用插件进行web翻译去看的 很累。

  • 现在时间是凌晨 11/30/02:36 喝了两瓶酒。这个周末我要重温甜甜的恋爱 给我也来一颗药丸 给时间是时间 让过去过去, 年底想去日本跨年了


image.png
image.png


作者:毛绒玩偶
来源:juejin.cn/post/7577713754562838580
收起阅读 »

为什么越来越多 Vue 项目用起了 UnoCSS?

web
Vue 开发者可能都注意到,UnoCSS 的讨论频率越来越高。它不像 Tailwind 那样有营销声势,不像 Windi 那样起得早,却在 2024 年之后逐渐“渗透”进越来越多的 Vue 项目中。很多团队从 Tailwind、Windi CSS、SCSS 等...
继续阅读 »

Vue 开发者可能都注意到,UnoCSS 的讨论频率越来越高。它不像 Tailwind 那样有营销声势,不像 Windi 那样起得早,却在 2024 年之后逐渐“渗透”进越来越多的 Vue 项目中。很多团队从 Tailwind、Windi CSS、SCSS 等方案“迁徙”到了 UnoCSS。看似只是换了个工具,实际上却是一种更深层次的开发范式迁移。


为什么 UnoCSS 会被 Vue 项目偏爱?它到底解决了哪些问题?又会引发哪些新的思维变化?这篇文章,我们来拆开 UnoCSS 背后的真实诱因。




🎯 UnoCSS 到底是什么?一句话不够解释


如果你只把 UnoCSS 理解为“一个类 Tailwind 的原子化 CSS 工具”,那你可能漏掉了它真正颠覆的部分。


UnoCSS 是一个:



  • 即写即用的原子 CSS 引擎,没有预定义 class(tailwind.config.js?你可以不用)

  • 即时编译(on-demand generation) ,不扫描模板、不打包 CSS 文件,运行时动态生成样式表

  • 支持任意规则组合,语义可扩展,能自动拼装 hover:bg-red-500/30 md:rounded-xl 这种复杂 class

  • 插件式运行机制,样式规则 = 插件,想加功能不用改源码



简单说:UnoCSS 就像是原子 CSS 界的「Vite」,更轻,更快,更灵活。





🧩 Vue 项目迁移 UnoCSS 的几个主要诱因


1. 开箱即用,没有冗余配置


Tailwind 开发中一个不成文的痛点是配置文件维护成本:你几乎必须写一堆 tailwind.config.js 来扩展自己的颜色、字体、断点。


而 UnoCSS 有个“离谱”的特性:



你甚至可以不用写 config 文件。



举例:


<div class="text-lg font-bold text-[#3a7afe] hover:opacity-80">

颜色?随便写 HEX。你想用 shadow-[0_0_12px_rgba(0,0,0,0.2)]?它也认。基本告别 theme.extend


这对 Vue 项目尤其友好 —— 组件就是 class 的封装,不需要额外定义 token。




2. 它更像 JS,而不是传统 CSS 工具


UnoCSS 本质上是一组「语法规则 + 解析器」,所有东西都是基于插件机制动态生成的。这点非常 Vue-ish。


比如你想扩展 btn-primary


rules: [
['btn-primary', 'px-4 py-2 rounded bg-blue-500 text-white']
]

配合 Vue + Script Setup,甚至可以做到“功能指令式”的组件:


<button class="btn-primary hover:bg-blue-600">提交</button>

这是 Tailwind 无法比拟的灵活度,尤其当你想跨多个组件“语义复用”样式,而又不想搞复杂的 SCSS。




3. Vue SFC 中语法体验更佳


UnoCSS 不依赖 Preflight,不污染全局,也不会把所有 class 编译成一大坨 CSS 文件。


更关键的是,在 Vue SFC 中,它可以配合原子类的组合器变得非常语义化。


<div class="grid grid-cols-[1fr_auto] gap-4 items-center sm:(grid-cols-1 gap-2)">

括号组合、嵌套媒体查询、状态嵌套,全都写在 class 中,无需管理额外 CSS 文件,非常适合组件化开发。




4. 和 Vue 生态绑定更深


UnoCSS 的创作者之一是 Anthony Fu,也就是 VueUseVitesseVitest 的作者。



换句话说:UnoCSS 是为 Vue 项目天生设计的原子 CSS 工具,生态协同、理念统一。



你可以在 VitePress、Nuxt、Vitesse、VueUse 所有项目中一键集成 UnoCSS,毫不费力。插件如 @unocss/nuxt@unocss/vite 也都官方维护,集成体验比 Tailwind 更丝滑。




📉 传统方案的反衬:你为什么“受够了 Tailwind”



  • 写多了 text-sm text-neutral-700 font-medium leading-relaxed tracking-wide,你会厌烦堆 class

  • 为了统一样式,你又开始封装 btn、card、tag 等组件,但 Tailwind 里没法抽离 class 成变量

  • 你想写一些自由样式(如text-[rgba(0,0,0,0.75)]),却必须配置 tailwind.config.js,开发体验断层


UnoCSS 这时候就像一口“无限制自助餐”:你想吃什么,厨房就给你端上来。




🧪 真正让它爆红的项目:Nuxt 生态


Nuxt 3 和 UnoCSS 简直天作之合。


如果你用 Nuxt,安装 UnoCSS 就一行命令:


npm i -D @unocss/nuxt

甚至不需要配置,直接写:


<template>
<section class="text-center text-4xl text-gradient from-pink-500 to-yellow-500">
Hello, UnoCSS
</section>
</template>

想封装组件?直接写 variantshortcuts,体验跟设计 token 一样自然:


shortcuts: {
'btn': 'px-4 py-2 font-bold rounded',
'btn-primary': 'btn bg-blue-500 text-white hover:bg-blue-600'
}



🧠 真正带来的范式转变


UnoCSS 不只是工具上的优化,它还改变了我们使用 CSS 的方式:



  • 从维护样式表 → 动态生成样式

  • 从配置颜色 → 直接在组件中定义 token

  • 从 class 管理 → 到语义表达


传统做法是围绕“命名”,而 UnoCSS 更像是在写“表达式”。这种范式变化,决定了它会逐渐成为 Vue 项目的原子化首选。




📌 使用 UnoCSS 时的真实建议



  • 如果你的项目刚启动,用 UnoCSS 会极大加快开发速度

  • 如果你在维护大型 Vue 项目,建议先从局部引入,避免和 Tailwind 冲突

  • 如果你对设计规范要求较高,UnoCSS 支持 themerulesshortcuts 构建完全定制化体系

  • 建议启用 VSCode 插件,否则开发体验会下降




✅为什么 UnoCSS 会流行?


因为它比 Tailwind 更轻,比 Windi 更快,比 SCSS 更灵活。而且,它是为 Vue 项目量身定制的。


不再“配置样式”,而是“表达样式”;不再围着类名转,而是围着组件转。


UnoCSS 不只是一个工具,而是一种更贴近 Vue 哲学的“开发语言”。


作者:ErpanOmer
来源:juejin.cn/post/7512392168783659071
收起阅读 »

UI小姐姐要求有“Duang~Duang”的效果怎么办?

web
设计小姐姐: “搞一下这样的回弹效果,你行不行?” 我:“行!直接梭哈 50 行 keyframes + transform + 各种百分比,搞定 ” 设计小姐姐:“太硬(撇嘴),不够 Q 弹(鄙视)” 我:(裂开) 隔壁老王:这么简单你都不行,我来一行贝塞...
继续阅读 »

test.gif



设计小姐姐: “搞一下这样的回弹效果,你行不行?”

:“行!直接梭哈 50 行 keyframes + transform + 各种百分比,搞定 ”

设计小姐姐:“太硬(撇嘴),不够 Q 弹(鄙视)”

:(裂开)

隔壁老王:这么简单你都不行,我来一行贝塞尔 cubic-bezier(0.3, 1.15, 0.33, 1.57) 秒了😎

设计小姐姐:哇哦!(兴奋)好帅!(星星眼🌟)好Q弹!(一脸崇拜😍)

“???”





🧠 一、为什么一行贝塞尔就能“Duang”起来?


1️⃣ cubic-bezier 是什么?


在 CSS 动画里,我们经常写:


transition: all 0.5s ease;

但其实 easelinearease-in-out 这些都只是封装好的贝塞尔曲线。

底层原理是:


cubic-bezier(x1, y1, x2, y2)

这四个参数定义了时间函数曲线,控制动画速度的变化。



  • x:时间轴(必须在 0~1 之间)

  • y:数值轴(可以超出 0~1!)


👉 当 y 超过 1 或小于 0 时,动画值就会冲过终点再回弹

这就是“回弹感”的核心。




2️⃣ 回弹的本质:过冲 + 衰减


想象一个球掉下来:



  • 过冲:球落地时会压扁(超出终点)

  • 回弹:然后反弹回来,再逐渐稳定


在动画中,这个“过冲”就是 y>1 的部分,

而“回弹”就是曲线回到 y=1 的过程。




🧪 二、一行贝塞尔的魔法


✅ 火箭发射


export_1764044056566.gif


<div class="bounce">🚀发射!</div>

<style>
.bounce {
transition: transform 0.8s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.bounce:hover {
transform: translateY(-500px);
}
</style>


💡 参数解析:



  • y1 = -0.55 → 先轻微反向缩小

  • y2 = 1.55 → 再冲过头 55%,最后回弹到原位


🧩 四、常用贝塞尔参数


效果描述贝塞尔参数备注
微回弹(按钮)cubic-bezier(0.34, 1.31, 0.7, 1)轻柔弹性
强回弹(卡片)cubic-bezier(0.68, -0.55, 0.27, 1.55)爆发力强
柔和出入cubic-bezier(0.4, 0, 0.2, 1.4)iOS 风
弹性放大cubic-bezier(0.175, 0.885, 0.32, 1.275)弹簧感
火箭猛冲cubic-bezier(0.68, -0.55, 0.27, 1.55)推背感



🧰 五、调试神器推荐



  • 🎨 cubic-bezier.com

    拖动手柄实时预览动画,复制参数一键搞定。

  • ⚙️ easings.net

    收录各种 easing 函数(含物理弹簧、阻尼等)。


作者:前端九哥
来源:juejin.cn/post/7576264484688379944
收起阅读 »

WebRTC 实现视频通话的前端开发步骤

web
你好,我是木亦。我不知道你是否了解过 WebRTC(Web Real - Time Communication),但不得不承认,WebRTC 凭借其无需安装插件、支持浏览器间直接通信的显著优势,已成为实现网页端视频通话的不二之选。对于前端开发者而言,深入掌握 ...
继续阅读 »

你好,我是木亦。我不知道你是否了解过 WebRTC(Web Real - Time Communication),但不得不承认,WebRTC 凭借其无需安装插件、支持浏览器间直接通信的显著优势,已成为实现网页端视频通话的不二之选。对于前端开发者而言,深入掌握 WebRTC 实现视频通话的开发流程,能够为用户打造出更加丰富多元、即时高效的互动体验。这篇文章将会向你介绍使用 WebRTC 实现视频通话的开发步骤。


一、项目初始化


在开启开发之旅前,首要任务是创建一个全新的前端项目。你可以借助常见的项目初始化工具,像create-react-app(适用于 React 项目)、vue-cli(适用于 Vue 项目),或者直接创建一个简洁的 HTML 页面。


使用 create-react-app 初始化项目


npx create-react-app webrtc-video-call
cd webrtc-video-call

使用 vue-cli 初始化项目


npm install - g @vue/cli
vue create webrtc-video-call
cd webrtc-video-call

如果选择直接创建 HTML 页面,其基本结构如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<title>WebRTC Video Call</title>
</head>
<body>
<!-- 后续添加视频通话相关元素 -->
</body>
</html>

二、引入 WebRTC 库


WebRTC 作为现代浏览器的内置功能,无需额外引入第三方库。在编写 JavaScript 代码时,可直接调用 WebRTC 提供的 API。


检测浏览器支持


if ('RTCPeerConnection' in window && 'RTCSessionDescription' in window && 'navigator.mediaDevices' in window) {
// 浏览器支持WebRTC
console.log('WebRTC is supported');
} else {
console.log('WebRTC is not supported in this browser');
}

通过上述代码,可快速判断当前浏览器是否支持 WebRTC,确保开发工作在兼容的环境下进行。


三、获取媒体设备权限


实现视频通话的第一步,是获取用户摄像头和麦克风的使用权限。


使用 navigator.mediaDevices.getUserMedia ()


const constraints = {
video: true,
audio: true
};
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) => {
// 成功获取媒体流,可用于视频显示
const videoElement = document.createElement('video');
videoElement.srcObject = stream;
videoElement.autoplay = true;
document.body.appendChild(videoElement);
})
.catch((error) => {
console.error('Error accessing media devices:', error);
});

在这段代码中,constraints对象明确指定了需要获取视频和音频权限。getUserMedia()方法返回一个 Promise,当操作成功时,会返回包含媒体流的stream对象,随后便可将其绑定到video元素上,实现本地视频的实时显示。


四、建立对等连接


WebRTC 通过 RTCPeerConnection 对象建立对等连接,实现双方媒体数据的高效传输。


创建 RTCPeerConnection 对象


// 创建RTCPeerConnection对象
const peerConnection = new RTCPeerConnection({
iceServers: [
{ urls:'stun:stun.l.google.com:19302' }
]
});

这里借助了 STUN(Session Traversal Utilities for NAT)服务器辅助建立连接,stun.l.google.com:19302是 Google 提供的公共 STUN 服务器,能有效帮助穿越网络地址转换(NAT)设备。


处理 ICE 候选


在连接建立过程中,处理 ICE(Interactive Connectivity Establishment)候选至关重要,这有助于寻找到最佳的连接路径。


peerConnection.onicecandidate = (event) => {
if (event.candidate) {
// 将ICE候选发送给对方
// 这里需要实现发送逻辑,例如通过信令服务器
console.log('ICE candidate:', event.candidate);
}
};

当有 ICE 候选生成时,需及时将其发送给对方,实际应用中通常借助信令服务器完成这一操作。


交换 SDP(Session Description Protocol)


SDP 用于详细描述媒体会话的各项参数,双方需交换 SDP 以协商媒体格式、编解码方式等关键信息。


// 创建Offer
peerConnection.createOffer()
.then((offer) => {
return peerConnection.setLocalDescription(offer);
})
.then(() => {
// 将本地的SDP发送给对方
// 这里需要实现发送逻辑,例如通过信令服务器
console.log('Local SDP:', peerConnection.localDescription);
})
.catch((error) => {
console.error('Error creating offer:', error);
});
// 接收对方的SDP并设置为远程描述
peerConnection.setRemoteDescription(new RTCSessionDescription(receivedSDP))
.then(() => {
// 接收对方的Offer后,创建Answer
return peerConnection.createAnswer();
})
.then((answer) => {
return peerConnection.setLocalDescription(answer);
})
.then(() => {
// 将本地的Answer发送给对方
// 这里需要实现发送逻辑,例如通过信令服务器
console.log('Local SDP (Answer):', peerConnection.localDescription);
})

.catch((error) => {
console.error('Error setting remote description or creating answer:', error);
});

这部分代码展示了创建 Offer、设置本地描述、发送本地 SDP,以及接收对方 SDP 并创建 Answer、设置本地描述、发送本地 Answer 的完整流程。


五、显示远程视频


当双方成功建立连接并完成 SDP 交换后,便可接收对方的媒体流,实现远程视频的显示。


监听 track 事件


peerConnection.ontrack = (event) => {
const remoteVideoElement = document.createElement('video');
remoteVideoElement.srcObject = event.streams[0];
remoteVideoElement.autoplay = true;
document.body.appendChild(remoteVideoElement);
};

一旦接收到对方的媒体流,ontrack事件就会被触发,此时将接收到的媒体流绑定到新创建的video元素上,即可实时显示远程视频画面。


六、信令服务器的作用与实现


在 WebRTC 视频通话中,信令服务器承担着交换 SDP 和 ICE 候选等关键信息的重要职责。尽管 WebRTC 实现了媒体数据的直接传输,但信令的交互仍需借助服务器来完成。


信令服务器的选择


可选用 WebSocket、Socket.IO 等技术搭建信令服务器。以 Socket.IO 为例,搭建一个简易信令服务器的步骤如下:


npm install socket.io

简单的 Socket.IO 信令服务器示例


const io = require('socket.io')(3000);
io.on('connection', (socket) => {
socket.on('offer', (offer) => {
// 这里可以实现将offer转发给目标客户端
console.log('Received offer:', offer);
});
socket.on('answer', (answer) => {
// 这里可以实现将answer转发给目标客户端
console.log('Received answer:', answer);
});
socket.on('ice - candidate', (candidate) => {
// 这里可以实现将ice - candidate转发给目标客户端
console.log('Received ice - candidate:', candidate);
});
});

在前端代码中,需引入 Socket.IO 客户端库,并精心编写与服务器的通信逻辑,实现将 SDP 和 ICE 候选发送至服务器,以及从服务器接收对方的 SDP 和 ICE 候选。


WebRTC 实现视频通话的前端开发涵盖多个关键环节,从项目初始化、获取媒体设备权限,到建立对等连接、交换 SDP 和 ICE 候选,再到显示远程视频和搭建信令服务器。通过逐步掌握这些核心步骤,前端开发者能够构建出功能完备的视频通话应用,为用户提供流畅、实时的视频通信体验。在实际开发过程中,还需依据具体需求和应用场景,对代码进行优化与扩展,以充分满足多样化的业务需求。


容器 <a href=5@2x.png" loading="lazy" src="https://www.imgeek.net/uploads/article/20260118/718ea69cb7313b0faba4510956153837.jpg"/>


作者:木亦Sam
来源:juejin.cn/post/7474124938526900262
收起阅读 »

五年自学前端到京东终面:我才明白自己不是范进,连范进都不如

1. 京东终面后,我在群里被恭喜了 2025年4月,京东HR在BOSS上找我,岗位写的是「iOS开发」,我心想这HR真他妈不专业,但还是回了句:「您好,我是前端,可以面试吗?」 没想到就这么稀里糊涂面了五轮,终面见部门老大,聊得还行。面完群里几个「未来同事」加...
继续阅读 »

1. 京东终面后,我在群里被恭喜了


2025年4月,京东HR在BOSS上找我,岗位写的是「iOS开发」,我心想这HR真他妈不专业,但还是回了句:「您好,我是前端,可以面试吗?」


没想到就这么稀里糊涂面了五轮,终面见部门老大,聊得还行。面完群里几个「未来同事」加我微信,有人说「稳了,等offer吧」,我也觉得这次该轮到我了吧?


然后,就没有然后了。


HR说「流程中」,一等就是一个月。这一个月我啥也没干,就刷邮箱等消息,像个傻逼一样。


这时候我才懂范进——你以为自己中了,其实屁都没有。




2. 我这五年,就是一部「选择比努力重要」的失败史


① 2020年:1万块我就觉得牛逼了


毕业那年进了个区块链小公司,老板说转正给1万,我他妈高兴坏了——「我同学花两三万培训还找不到工作呢!」


现在看真是蠢。那时候滴滴应届生「白菜价」都20k了,我还在为1万沾沾自喜。


问题不是钱,是起点——你第一份工作在小公司,后面想进大厂,难如登天。


② 2021-2023年:被裁了才知道害怕


后来跳槽涨了50%,进了个中型公司,终于知道什么叫「团队协作」了。结果没两年公司不行了,领了几万块钱「毕业大礼包」。


2023年再找工作,行情已经烂了。我怕失业,涨了40%多又去了个小公司,继续写垃圾代码。


这时候我才明白:小公司呆久了,你的简历就废了。


③ 2025年:面京东,我才知道自己多菜


今年我认真准备了半年,八股文、项目难点、架构设计,甚至刷了LeetCode。面京东时,人家问「你们日活多少?QPS多少?」


我他妈哪知道?我们公司就几十个人用,需要个屁的高并发!


大厂要的是「大厂经验」,你没有,就是没有。




3. 我认命了,但你们别学我


我现在进了一家还算稳定的公司,薪资也还行,但心里明白——我这辈子可能都进不去大厂了。


不是我不努力,是一开始选错了。如果毕业就进大厂,现在跳槽随便涨50%。但在小公司呆过?难。


但有一点让我没完全废掉:我写技术博客


真的,这几年每次面试,人家都会问我博客上的东西。那些在小公司用不上的「高端技术」,我全靠自己折腾,写在博客里。


所以如果你也在小公司:



  1. 别混日子,自己搞点有难度的项目

  2. 写博客,这玩意儿真能当简历用

  3. 早点跳,在小公司呆三年以上,简历就臭了




4. 最后说句实话


我知道你们想听「坚持就能进大厂」,但现实是——有些人就是没这个命。


我现在马上30岁了,五年经验,没大厂背景,以后更难。


前面卡技术经验,卡大厂背景,后面卡年龄限制,一开始没走好路,后面真的特别艰难


现在还背上了房贷,要结婚,要有小孩,后面压力更大,现在只想稳定一点 😭😭😭


但至少我还能写代码,还能靠技术吃饭。比起范进,我至少没疯。


最后贴几张图吧,就当看电子榨菜了



  • 京东是真远,来回四个小时


88b1e1f592d2f44016a0102337766464.jpg



  • 不靠谱的面试


image.png


image.png


image.png



  • 一直等不到的消息,我都感觉我问烦了
    fd35bf9289d492478da47258daec1053.jpg

  • 来自群友的关心,我当时已经是范进的心态了


b8a76638c8a0dbb5b640c742b8fcc55e.jpg



  • 别人的起点,或许就是你的终点


1be7b0dd3664752caaa9a625114deda1.jpg


e34e7e1fd5a8f67c6ee3af31bc6e52f3.jpg



  • 为什么拿京东说事


因为别的大厂也没约我面试,除了美团,小红书的外包 😭😭😭



  • 新公司
    来新公司上班快一个月了,已经轻车熟路,同事关系处的不错,已经开始穿拖鞋了(你们应该知道这句话的含金量)


结束语


后面会更新一些,新的学习的技术,如果对面经感兴趣的,也可以单独写一篇半年的面试经历以及总结~


作者:赵小川
来源:juejin.cn/post/7533801117521772582
收起阅读 »

2025快手直播至暗时刻:当黑产自动化洪流击穿P0防线,我们前端能做什么?🤷‍♂️

兄弟们,前天的瓜都吃了吗?🤣 说实话,作为一名还在写代码的打工仔,看到前天晚上快手那个热搜,我手里捧着的咖啡都不香了,后背一阵发凉。 12月22日晚上10点,正是流量最猛的时候,快手直播间突然失控。不是服务器崩了,而是内容崩了——大量视频像洪水一样灌进来。紧...
继续阅读 »

兄弟们,前天的瓜都吃了吗?🤣


image.png


说实话,作为一名还在写代码的打工仔,看到前天晚上快手那个热搜,我手里捧着的咖啡都不香了,后背一阵发凉。


12月22日晚上10点,正是流量最猛的时候,快手直播间突然失控。不是服务器崩了,而是内容崩了——大量视频像洪水一样灌进来。紧接着就是官方无奈的拔网线,全站直播强行关停。第二天开盘,股价直接跌了3个点。


这可不是普通的 Bug,这是P0 级中的 P0


很多群里在传内鬼或者0day,但看了几位安全圈大佬(360、奇安信)的复盘,我发现这事儿比想象中更恐怖:这是一次教科书级别的黑产自动化降维打击。


今天不谈公关,咱们纯从技术角度复盘一下:假如这事儿发生在你负责的项目里,你的前端代码能抗住几秒?




当脚本比真人还多还快时?


这次事故最骚的地方在于,黑产根本不按套路出牌。


以前的攻击是 DDoS,打你的带宽,让你服务不可用。


这次是 Content DDoS(内容拒绝服务)。


1. 前端防线形同虚设


大家有没有想过,黑产是怎么把视频发出来的?


他们绝对不会坐在手机前,一个一个点开始直播。他们用的是群控、是脚本、是无头浏览器(Headless Browser)。


这意味着什么?


意味着你前端写的那些 if (user.isLogin)、那些漂亮的 UI 拦截、那些弹窗提示,在黑客眼里全是空气。他们直接逆向了你的 API,拿到了推流接口,然后几万个并发调用。


2. 审核系统被饱和式攻击


后端通常有人工+AI 审核。平时 QPS 是 1万,大家相安无事。


昨晚,黑产可能瞬间把 QPS 拉到了 100万。


云端 AI 审核队列直接爆了,人工审核员估计鼠标都点冒烟了也审不过来。一旦阈值被击穿,脏东西就流到了用户端。




那前端背锅了吗?


虽然核心漏洞肯定在后端鉴权和风控逻辑(大概率是接口签名泄露),但咱们前端作为 离黑客最近的一层皮,如果做得好,绝对能把攻击成本拉高 100 倍。


来,如果不幸遇到了这种自动化脚本攻击,咱们前端手里还有什么牌?🤔


别把 Sign 算法直接写在 JS 里!


很多兄弟写接口签名,直接在 request.js 里写个 md5(params + salt) 完事。


大哥,Chrome F12 一开,Sources 一搜,断点一打,你的盐(Salt)就裸奔了。


防范操作:直接上 WASM (WebAssembly)


把核心的加密、签名逻辑,用 C++ 或 Rust 写,编译成 .wasm 文件给前端调。


黑客想逆向 WASM?那成本可比读 JS 代码高太多了。这就是给他们设的第一道坎。


你的用户,可能根本不是人


黑产用的是脚本。脚本和真人的操作是有本质区别的。


不要只会在登录页搞个滑块,没用的,现在的图像识别早破了。


要在 关键操作(比如点击开始直播) 前,采集一波数据:



  • 鼠标轨迹:真人的轨迹是曲线(贝塞尔曲线),脚本通常是直线。

  • 点击间隔:脚本是毫秒级的固定间隔,人是有随机抖动的。


// 伪代码,简单的是不是人检测
function isHuman(events) {
// 如果鼠标轨迹过于平滑或呈绝对直线 -> 机器人
if (analyzeTrajectory(events) === 'perfect_linear') return false;
// 如果点击时间间隔完全一致 -> 机器人
if (checkTiming(events) === 'fixed_interval') return false;
return true;
}

把这些行为数据打分,随着请求发给后端。分低的,直接拒绝推流。


既然防不住内鬼,那就给他打标


这次很多人怀疑是内部泄露了接口文档或密钥。说实话,这种事防不胜防。


但是,前端可以搞 盲水印


在你的 Admin 管理后台、文档平台,加上肉眼看不见的 Canvas 水印(把员工 ID 编码进背景图的 RGB 微小差值里,具体大家自己去探索😖)。


一旦截图流出,马上就能解码出是哪个员工泄露的。威慑力 > 技术本身。


或者试试这个技巧 👉 如何用隐形字符给公司内部文档加盲水印?(抓内鬼神器🤣)




安全复盘


这次快手事件,其实就死在了一个逻辑上: 后端太信任通过了前端流程的请求。


我们写代码时常犯的错误:



  • 前端校验过手机号格式了,后端不用校验了吧?

  • 必须点了按钮才能触发这个请求,所以这个接口很安全。


大错特错!


2025 年了,兄弟们。在 Web 的世界里,不相信前端 才是保命法则。


任何从客户端发来的数据,都要默认它是有毒的。


之前我都发过类似的文章:为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!


希望对你们有帮助👆




这次是快手,下次可能就是咱们的公司。


尤其是年底了,黑灰产也要冲业绩(虽然这个业绩有点缺德😖)。


建议大家上班时看看这几件事:



  1. 查一下核心接口(支付、发帖、推流)有没有做签名校验。

  2. 看看有没有做频率限制(Rate Limiting),前端后端都要看。

  3. 搜一下你们的代码仓库,看看有没有把公司的 Key 或者源码传上去(这个真的很常见!)。


前端不只是画页面的,关键时刻,咱们也是安全防线的一部分。


别等到半夜被运维电话叫醒,那时候就真只能甚至想重写简历了🤣。


谢谢大家.gif


作者:ErpanOmer
来源:juejin.cn/post/7586944874526539814
收起阅读 »

2025年终总结:再次选择、沪漂、第一次演讲、相亲无果

选择大于努力 友友们,我是卷福同学,上次写2024年终总结的时候还在武汉,谁能想到一年之后会在上海写2025的年终总结。今年下半年经历的事情比较多,总结来说就是,人生经历又丰富了 1.再次选择 去一线大城市闯荡人生还是留在武汉岁月静好呢? 1月 1月时...
继续阅读 »

选择大于努力



友友们,我是卷福同学,上次写2024年终总结的时候还在武汉,谁能想到一年之后会在上海写2025的年终总结。今年下半年经历的事情比较多,总结来说就是,人生经历又丰富了


1.png


1.再次选择



去一线大城市闯荡人生还是留在武汉岁月静好呢?



1月


1月时候还在武汉国企里呢,彼时因为项目变少了,武汉人员要重新分配,没分到项目组的人要进资源池等候下一步安排。而我这个小组之前武汉是有2个人的,北京1个项目经理,给我分配了半个人的工时,另一个人直接让去资源池。关于这个项目经理,去年也写过吐槽的帖子。这个人在武汉的名声非常不好,就完全是对待牛马一样对待底下干活的人。


我想着以后的日子可能过得更难受,还不如直接进资源池算了。于是就和他说了,他倒也爽快,想着再从武汉随便捞个人进来呗,反正还有很多人没安排项目组的。没想到的是,他接连找了两个人,但是因为武汉的人都听说过他的名号,都表示不想去他的项目组。最后,他把项目给外包人员做了。武汉的人,宁愿待池子里,也不想跟着他干。。。


这让我想起以前上小学的时候,以前农村小学的老师,打学生都很厉害的。而打的原因不仅仅是因为调皮捣蛋,我那个班教数学的老师,就是其中打人最厉害的。上课的讲台两边会有两个座位嘛,每次她讲课的时候,都会从这两个座位的学生手里拿课后作业讲,要是讲的时候发现写错了,直接冲过去打头,提耳朵等等。有一次因为班上写错作业的人太多了,直接一节课没上,轮流上去扎马步。而我呢,又恰巧有一学期是坐在讲台旁边的座位,于是这学期每逢她的课必挨打,打到后面,居然在课上说,卷福坐在这,已经被我打肿了,你们再敢做错题试试。到第二学期开学的时候,大家选座位直接把前面两个位置空着了,有两个人宁愿在后面站着也不坐那位置。


插图.png


我感觉是不是历史又一次重演了呢?


现在回头看,当时没继续跟着他的选择是对的。在武汉,也没有岁月静好啊。


再次选择


虽然5月份的时候拿了上海的offer,但是等真的要走的时候,还是会纠结的。就和刚毕业的时候去北京一样,一切都要重头开始了。走的那天,出租屋里只有个保洁阿姨在打扫卫生,就和我刚回武汉的时候一样。不同的是,上次阿姨说的是房子很快就打扫好了,这次说的是,祝老板以后去上海了发大财啊


2.jpg


2.沪漂


探索新事物


上海就是机会多啊,休息日都会出去逛逛,探索些新事物。想想来上海之后,去周边城市参加徒步、参加ChinaJoy漫展、看了开心麻花的话剧《疯狂理发店》、还有市区内的一些公园、大学、图书馆、动物园、演唱活动等等,生活非常丰富多彩。



  • 徒步活动在小红书上找个团报就行,很多都是一天游,一半时间都在路上

  • 漫展里的coser都美如画,非常适合集邮


3_1.jpg


3.jpg


3.第一次演讲


3月


今年参加的线下活动还比较多呢,3月份的时候受腾讯云社区的邀请去杭州参加线下的技术训练营活动,主要也是想趁机会多认识些大佬,说不定大佬招人,有内推机会。倒也认识了不少人,喵喵、小智,还有社区的泽敏姐。晚上一起吃饭交流的时候,泽敏姐说下次有机会让我上去演讲。当时只以为是说说而已,毕竟社区里大佬太多了


4.jpg


9月


9月份的时候收到社区的邀请,去深圳参加腾讯全球数字生态大会,作为讲师上去做分享。我是非常想去的,这样就能达成从学生到老师角色转变的目标,输入变输出。比较纠结的是分享什么内容比较好呢,想了一晚上,最后觉得分享自己用AI两年的经历、沉淀的一些使用心得体、还有变现方式会比较好。


现场的分享也是比较顺利,讲完下来的时候和小智老师沟通上台演讲体验,小智说我讲的非常干货,讲的很稳,刚才他自己讲的时候非常紧张,腿都在抖。我说,我也是啊,腿都在打颤,反而看你讲一点也不慌的样子。。。


第二天又和喵喵一起去腾讯数码大厦找泽敏姐,非常感谢泽敏姐的邀请,第一次到腾讯的大楼参观。期间见识到了超豪华的二次元工位,满墙的手办,非常震惊。


5.jpg


4.AI探索


选择适合自己的方向比较重要,2024年投入了很多时间在AI视频、绘画上,虽然也有涨粉嘛,但是变现不行,一年下来也就三位数的收益。今年主要在写作还有AI编程方向投入,因为换工作的原因,其实投入精力没去年那么多。反而收益还更多了,有四位数的收益。也产出了百万阅读的文章和10w+播放量的视频


出爆款的诀窍就是追热点,这是普通人出爆款最容易的方式了。比如年初的Deepseek,国庆期间的sora2,趁着刚出来热度最高的时候,随便写点东西或者做个视频,流量都非常好的。那像现在再去写Deepseek,流量肯定不如之前了。


6.png


7.png


11月


11月看到小智老师发的华为鸿蒙线下编程活动的信息,拉着在上海的一个前同事一起去参加玩玩,前同事是我在北京阿里工作时同组的,后来来了上海后,我居然在一个公交站碰到他了,也是十分震惊,居然在上海遇到曾在北京的前同事。鸿蒙的编程活动都是基础的操作,正好也买了Codebuddy的会员,用AI编程轻松解决了,拿到个小礼品


8.jpg


12月


年底了,AI破局俱乐部在深圳举办行动家大会,我看分享嘉宾和内容挺干货的,也报名跑去深圳参加了,到现场才发现,高手云集,天下英雄如过江之鲫。我把这次参会了解的东西整理了下:



  • AIPPT.com :赵充老师以肯德基为例分享做产品不要做全家桶,用户只想要个甜筒(不要做大而全的产品,而是在垂直领域找到需求,在单点,堵上一切)

  • AI编程出海:老外付费意愿更强,大公司不愿意做的垂直小市场,才是个人最大的机会。remove.bg网站仅有去除背景这一项功能,流量却非常高

  • 搜索流量比推荐流量更值钱:用户主动搜的,说明有明确需求。而平台推的,用户只是随便看看。获取搜索流量的方法:研究用户搜索的关键词,围绕关键词输出内容,时间久了,用户搜索这个关键词,自然就找到你了


活动分享的内容挺多的啊,这里就不继续写了。


同样的听课,结果可能完全不同,会场的1000人,参加完会回去后,可能大部分人就是感慨一下,然后继续原来的生活


9.jpg


10.jpg


11.jpg


5.相亲


离开武汉前,和大学同学聚餐,聊了下发现同学要准备去女方家提亲了,问对象是从哪里找的,说了个相亲软件。不过也很难,同学相亲了十几个女生,才和现在这个走到谈结婚这一步。知道了相亲软件(青藤)后,我来上海也开始了相亲之旅,到目前为止,还没有一个相上的,简单说下相亲的几个女生吧:



  • 91年,初中老师。其实是在武汉相亲的,家里给介绍的。感觉年龄差的太大了,差不多5岁了,不过因为是家里介绍的,还是得见上一面。3月的时候,武大樱花还开着,便想去武大里逛逛聊聊。让她把身-份-证号发我,用校友通道预约。然后犹犹豫豫半天没有发我,说是个人隐私等等之类的,要自己预约。结果没约上,想再用校友通道时已经约满了,无奈只好约着去学校旁边一家冷锅鱼店吃饭好了,计划约的12点见面,我预估12点半应该能吃上饭(不知道为什么,武汉相亲的女生都迟到),没想到她直到1点多才到,迟到1个多小时,期间也就各种尬聊,回去后两人都没再发消息了,凉凉

  • 93年,上海公务员。家里的亲戚介绍的,是我相亲过的最优秀的女生了。以前的高考文科状元,武大校友,长得像袁咏仪,聊天说话情商也很高。接触了一个月吧,期间也一起吃过饭,看过话剧。最后一次见面聊天说起她前男友,海归,年入百万,金融行业。长相没说啊,应该也不差,妥妥高富帅。我一听这条件,心里顿时凉凉,差距太大了。问起为什么分了呢,说是男的虽然有钱,但是不给女的花,什么事都要AA。就是网上那种观念:钱是给女人看的,不是给女人花的。这次聊完之后就结束了,又凉凉。

  • 95年,幼师。在上海相亲的,青藤上找的,见面后感觉非常漂亮啊。不过性格比较强势,刚开始聊的还是开心的话题,突然画风一转,她就开始吐槽模式,吐槽支教的山里小学的领导等等,后半段全是听她吐槽,没再聊相亲的话题了。回去后又聊了几周,但是幼师可能时间太紧,也可能她同时聊的人太多,后面想再约见面就没时间了,于是凉凉

  • 97年,互联网数据分析师。来上海后相亲的第一个女生,也是非常漂亮,加上好友,看了我动态后,说非常崇拜技术大佬(多写博客还是有好处的嘛),想请我吃饭。于是爽快赴约,期间聊分布式、高并发等等,最后吃完饭结束的时候,说感觉身高不够

  • 00年,主业机械设计,副业自媒体。遇到同行了,聊天话题特别多,情绪价值拉满。约线下去CJ漫展玩,让她把身-份-证号发我来买票,本来还想着怎么解释说明的,下一秒就把身-份-证号和手机号发来了,没有任何犹豫。约好了早上9点半去会展中心,没想到她早早到我这边来等我一起过去,连零食和水都买好了。遂开心前往,妹子是第一次逛漫展,逛的非常开心。只不过回去后还是说了性格不合,只好当朋友了。后续还是保持联系,偶尔见面


这里列出不同年龄段的相亲女生,其他还有一些都是软件上匹配了没说话,或者说两句话就不继续聊了的。给兄弟们做个负面教材的参考啊,今年的相亲就到此为止,明年再说吧。。。


6.2026目标


最后不都得展望下未来吗,2026年你们又给自己制定了哪些目标和规划呢,我给自己定下的目标,希望明年都能做到



  • 神山转山

  • 樱花巡礼

  • 新手上路

  • 恰老外米


最后,感兴趣的朋友可以关注我的公众号:卷福同学


作者:卷福同学
来源:juejin.cn/post/7590309337861046313
收起阅读 »

叫你别乱封装,你看出事了吧

团队曾为一个订单状态显示问题加班至深夜:并非业务逻辑出错,而是前期封装的订单类过度隐藏核心字段,连获取支付时间都需多层调用,最终只能通过反射绕过封装临时解决,后续还需承担潜在风险。这一典型场景,正是 “乱封装” 埋下的隐患 —— 封装本是保障代码安全、提升可维...
继续阅读 »

团队曾为一个订单状态显示问题加班至深夜:并非业务逻辑出错,而是前期封装的订单类过度隐藏核心字段,连获取支付时间都需多层调用,最终只能通过反射绕过封装临时解决,后续还需承担潜在风险。这一典型场景,正是 “乱封装” 埋下的隐患 —— 封装本是保障代码安全、提升可维护性的工具,但违背其核心原则的 “乱封装”,反而会让代码从 “易扩展” 走向 “高耦合”,成为开发流程中的阻碍。


一、乱封装的三类典型形态:偏离封装本质的错误实践


乱封装并非 “不封装”,而是未遵循 “最小接口暴露、合理细节隐藏” 原则,表现为三种具体形态,与前文所述的过度封装、虚假封装、混乱封装高度契合,且每一种都直接破坏代码可用性。


1. 过度封装:隐藏必要扩展点,制造使用障碍


为追求 “绝对安全”,将本应开放的核心参数或功能强行隐藏,仅保留僵化接口,导致后续业务需求无法通过正常途径满足。例如某文件上传工具类,将存储路径、上传超时时间等关键参数设为私有且未提供修改接口,仅支持默认配置。当业务需新增 “临时文件单独存储” 场景时,既无法调整路径参数,又不能复用原有工具类,最终只能重构代码,造成开发资源浪费。


反例代码:


// 文件上传工具类(过度封装)
public class FileUploader {
// 关键参数设为私有且无修改途径
private String storagePath = "/default/path";
private int timeout = 3000;

// 仅提供固定逻辑的上传方法,无法修改路径和超时时间
public boolean upload(File file) {
// 使用默认storagePath和timeout执行上传
return doUpload(file, storagePath, timeout);
}

// 私有方法,外部无法干预
private boolean doUpload(File file, String path, int time) {
// 上传逻辑
}
}

问题:当业务需要 "临时文件存 /tmp 目录" 或 "大文件需延长超时时间" 时,无法通过正常途径修改参数,只能放弃该工具类重新开发。


正确做法:暴露必要的配置接口,隐藏实现细节:


public class FileUploader {
private String storagePath = "/default/path";
private int timeout = 3000;

// 提供修改参数的接口
public void setStoragePath(String path) {
this.storagePath = path;
}

public void setTimeout(int timeout) {
this.timeout = timeout;
}

// 保留核心功能接口
public boolean upload(File file) {
return doUpload(file, storagePath, timeout);
}

2. 虚假封装:形式化隐藏细节,未实现数据保护


表面通过访问控制修饰符(如private)隐藏变量,也编写getter/setter方法,但未在接口中加入必要校验或逻辑约束,本质与 “直接暴露数据” 无差异,却增加冗余代码。以订单类为例,将orderStatus(订单状态)设为私有后,setOrderStatus()方法未校验状态流转逻辑,允许外部直接将 “已发货” 状态改为 “待支付”,违背业务规则,既未保护数据完整性,也失去了封装的核心价值。


反例代码


// 订单类(虚假封装)
public class Order {
private String orderStatus; // 状态:待支付/已支付/已发货

// 无任何校验的set方法
public void setOrderStatus(String status) {
this.orderStatus = status;
}

public String getOrderStatus() {
return orderStatus;
}
}

// 外部调用可随意修改状态,违背业务规则
Order order = new Order();
order.setOrderStatus("已发货");
order.setOrderStatus("待支付"); // 非法状态流转,封装未阻止

问题:允许状态从 "已发货" 直接变回 "待支付",违反业务逻辑,封装未起到数据保护作用,和直接用 public 变量没有本质区别。


正确做法:在接口中加入校验逻辑:


public class Order {
private String orderStatus;

public void setOrderStatus(String status) {
// 校验状态流转合法性
if (!isValidTransition(this.orderStatus, status)) {
throw new IllegalArgumentException("非法状态变更");
}
this.orderStatus = status;
}

// 隐藏校验逻辑
private boolean isValidTransition(String oldStatus, String newStatus) {
// 定义合法的状态流转规则
return (oldStatus == null && "待支付".equals(newStatus)) ||
("待支付".equals(oldStatus) && "已支付".equals(newStatus)) ||
("已支付".equals(oldStatus) && "已发货".equals(newStatus));
}
}

3. 混乱封装:混淆职责边界,堆砌无关逻辑


将多个独立功能模块强行封装至同一类或组件中,未按职责拆分,导致代码耦合度极高。例如某项目的 “CommonUtil” 工具类,同时包含日期转换、字符串处理、支付签名校验三类无关功能,且内部逻辑相互依赖。后续修改支付签名算法时,误触日期转换模块的静态变量,导致多个依赖该工具类的功能异常,排查与修复耗时远超预期。


反例代码


// 万能工具类(混乱封装)
public class CommonUtil {
// 日期处理
public static String formatDate(Date date) { ... }

// 字符串处理
public static String trim(String str) { ... }

// 支付签名(与工具类无关)
public static String signPayment(String orderNo, BigDecimal amount) {
// 使用了类内静态变量,与其他方法产生耦合
return MD5.encode(orderNo + amount + secretKey);
}

private static String secretKey = "default_key";
}

问题:当修改支付签名逻辑(如替换加密方式)时,可能误改 secretKey,导致日期格式化、字符串处理等无关功能异常,排查难度极大。


正确做法:按职责拆分封装:


// 日期工具类
public class DateUtil {
public static String formatDate(Date date) { ... }
}

// 字符串工具类
public class StringUtil {
public static String trim(String str) { ... }
}

// 支付工具类
public class PaymentUtil {
private static String secretKey = "default_key";
public static String signPayment(String orderNo, BigDecimal amount) { ... }
}

二、乱封装的核心危害:从开发效率到系统稳定性的双重冲击


乱封装的危害具有 “隐蔽性” 和 “累积性”,初期可能仅表现为局部开发不便,随业务迭代会逐渐放大,对系统造成多重影响。


1. 降低开发效率,增加需求落地成本


乱封装会导致接口设计与业务需求脱节,当需要调用核心功能或获取关键数据时,需额外编写适配代码,甚至重构原有封装。例如某报表功能需获取订单原始字段用于统计,但前期封装的订单查询接口仅返回加工后的简化数据,无法满足需求,开发团队只能协调原封装者新增接口,沟通与开发周期延长,直接影响项目进度。


2. 破坏系统可扩展性,引发连锁故障


未预留扩展点的乱封装,会让后续功能迭代陷入 “牵一发而动全身” 的困境。某项目的缓存工具类未设计 “缓存过期清除” 开关,当业务需临时禁用缓存时,只能修改工具类源码,却因未考虑其他依赖模块,导致多个功能因缓存逻辑变更而异常,引发线上故障。这种因封装缺陷导致的扩展问题,会随系统复杂度提升而愈发严重。


3. 提升调试难度,延长问题定位周期


内部细节的无序隐藏,会让问题排查失去清晰路径。例如某支付接口返回 “参数错误”,但封装时未在接口中返回具体错误字段,且内部日志缺失关键信息,开发人员需逐层断点调试,才能定位到 “订单号长度超限” 的问题,原本十分钟可解决的故障,耗时延长数倍。


三、避免乱封装的实践原则:回归封装本质,平衡安全与灵活


避免乱封装无需复杂的设计模式,核心是围绕 “职责清晰、接口合理” 展开,结合前文总结的经验,可落地为两大原则。


1. 按 “单一职责” 划分封装边界


一个类或组件仅负责一类核心功能,不堆砌无关逻辑。例如用户模块中,将 “用户注册登录”“信息修改”“地址管理” 拆分为三个独立封装单元,通过明确的接口交互(如用户 ID 关联),避免功能耦合。这种拆分方式既能降低修改风险,也让代码结构更清晰,便于后续维护。


2. 接口设计遵循 “最小必要 + 适度灵活”



  • 最小必要:仅暴露外部必须的接口,隐藏内部实现细节(如工具类无需暴露临时变量、辅助函数);



  • 适度灵活:针对潜在变化预留扩展点,避免接口僵化。例如短信发送工具类,核心接口sendSms(String phone, String content)满足基础需求,同时提供setTimeout(int timeout)方法允许调整超时时间,既隐藏签名验证、服务商调用等细节,又能应对不同场景的参数调整需求。


某商品管理项目的封装实践可作参考:商品查询功能同时提供两个接口 —— 面向前端的 “分页筛选简化接口” 和面向后端统计的 “完整字段接口”,既满足不同场景需求,又未暴露数据库查询逻辑,后续数据库表结构调整时,仅需维护内部实现,外部调用无需改动,充分体现了合理封装的价值。


结语


封装的本质是 “用合理的边界保障代码安全,用清晰的接口提升开发效率”,而非 “为封装而封装”。开发过程中,需避免过度追求形式化封装,也需警惕功能堆砌的混乱封装,多从后续维护、业务扩展的角度权衡接口设计。毕竟,好的封装是开发的 “助力”,而非 “阻力”—— 下次封装前,不妨先思考:“这样的设计,会不会给后续埋下隐患?”


作者:秋难降
来源:juejin.cn/post/7543911246166556715
收起阅读 »

这个老牌知名编程论坛,轰然倒下了!

提到 Stack Overflow 论坛,提到那个橙色的栈溢出图标,相信程序员和开发者们都再熟悉不过了。 还记得半年前,我曾经写过一篇文章,分享了一个有关 Stack Overflow 论坛的变化趋势图,从曲线走势来看,当时那会就已经非常不容乐观,每月新问题...
继续阅读 »

提到 Stack Overflow 论坛,提到那个橙色的栈溢出图标,相信程序员和开发者们都再熟悉不过了。



还记得半年前,我曾经写过一篇文章,分享了一个有关 Stack Overflow 论坛的变化趋势图,从曲线走势来看,当时那会就已经非常不容乐观,每月新问题数量处于快速锐减之中。


但是现在时间来到 2026 年了,你猜怎么着?


结果是,趋势进一步走低,现在每个月新问题数据甚至还不如 18 年前 Stack Overflow 刚诞生那会的水平。


老规矩,我们还是直接看趋势图和具体数据吧。



这是最新的 Stack Overflow 论坛变化趋势图,表示的是从 2008 年社区刚上线开始,一直到 2026 年的今天,这 18 年时间里,Stack Overflow 社区每个月新问题个数的变化趋势。


怎么样?大家看完这张图有没有什么感触?


这张图清晰地展示出了 Stack Overflow 编程社区在这 18 年间所经历的增长、繁荣、高光以及跌落的趋势。


可以看到,从 2008 年到 2014 年这前 6 年的时间,Stack Overflow 一路高歌,渐入佳境,基本都在稳步增长。


而从 2014 年到 2022 年这中间的 8 年时间,虽说图中曲线呈震荡变化状态,但总体都是处于高位趋势,这也是 Stack Overflow 社区的繁荣时刻。


从数据上来看,Stack Overflow 最高光的顶峰时刻出现在 2020 年,尤其是 2020-05-01 这个时间节点,数据来到了 302381,这也是数值的最顶峰。



而从趋势图中也可以很明显地看出,自 2022 年底开始,Stack Overflow 社区日渐式微,开始出现回落之势。


那一年的年底科技圈发生了什么事情,相信大家都记忆犹新,没错,那就是 OpenAI 正式发布了 ChatGPT。


再后面几年的故事,相信大家也都非常清楚了,AI 大模型飞速迭代,AI 类产品和 AI 知识引擎更是百花齐放,层出不穷。


与此同时,传统的搜索引擎和知识社区也受到了不小的冲击。


其实到了 2025 年,Stack Overflow 的数据就已经跌回到 15 年前的水平了。



而如今时间来到了 2026 年,再看看最近这几个月 Stack Overflow 的数据,更是让人瞠目结舌:



没错,数据已经跌到甚至不如 18 年前社区刚上线那会的水平。



至此,Stack Overflow 社区基本上是彻底凉了。


这时候也不禁想起了那张网图。





聊起 Stack Overflow 社区的诞生,那还要追溯到 2008 年。


两位在软件开发领域很有影响力的博主,分别是 Jeff Atwood(知名博客 Coding Horror 的作者)和 Joel Spolsky(Fog Creek 创始人,《Joel on Software》作者),他们发现了一个行业痛点:即程序员在遇到技术难题时,很难从网络上找到准确、高质量的解决方案。


当时的搜索结果往往充斥着无效信息,知识分散且低效,或者某些技术网站虽然在搜索中排名很高,但真正有用的答案却被藏在注册墙或者付费墙后面,用户体验感极差。


基于对技术社区深刻的理解和对现有状况的不满,于是这两位技术布道者一拍即合,决定联手,打造一个专属于程序员的问答圣地。


他们的目标很明确:创建一个以内容质量为核心、通过社区协作来解决问题的平台。


于是在 2008 年,Stack Overflow 项目启动了。


同年 7 月,网站开始了小范围的内部测试,邀请了一批种子用户来打磨产品和机制。


两个月后,Stack Overflow 网站正式面向公众开放。


Stack Overflow 的上线如同一股清流,迅速在开发者群体中引起了轰动,同时 Stack Overflow 也成功地将全球的程序员凝聚在一起,让知识的分享与获取变得前所未有的高效。


就这样,一个旨在解决技术难题的网站,最终成为了无数开发者赖以生存的“第二搜索引擎”和技术问答社区。


由于其巨大的成功,Stack Overflow 后来还衍生出一系列产品,巅峰时期的它拥有 180+ 子站,而且涵盖了从编程到数学、物理等众多领域的问答社区。


然而,即便是这样一个全球顶级的技术社区,如今,也难逃被 AI 冲击和洗礼的命运。




于是我也开始回想,我自己这几年在互联网上检索信息的方式,似乎在不知不觉中发生了变化。


现在遇到问题,我好像已经不怎么喜欢使用传统搜索引擎和技术社区了,而是会习惯性地转向各种 AI 工具和智能助手,同时信息的处理和交互范式也完全变了。


我们还以编程写代码为例。


以前当我们在写代码调试运行出现错误但折腾半天也不知所以的时候,大家会怎么做?


相信不少同学和我一样,也是首先复制这段报错信息到搜索引擎中进行检索,然后根据搜索引擎吐出来的搜索结果,自己逐个点进去筛选有用的信息。


而当我们一旦在搜索结果里看到了 Stack Overflow 相关网页时,直觉一般会告诉我们,离问题解决应该不远了。


要是在搜索引擎里实在找不到解决问题的方法,那我们就只能去类似 Stack Overflow 这样的编程社区里进行发帖求助了,然后等待问题被查看和回答。


这是在 AI 大模型还没有爆发之前,大家所普遍采用的一个解决问题的办法,总结起来就是这样:



  • 遇到问题 → 搜索引擎 → Stack Overflow 链接 → 改代码调试 → 解决问题。


或者这样:



  • 遇到问题 → 实在搜索无果 → 发帖 → 等待回答


但现在,随着 AI 技术和工具的发展,事情就变了。


我们可以直接甩给 AI 工具一个问题或者一段信息,AI 工具便会自动理解你的意图,并开始深度思考、收集信息、整理逻辑、分析总结、加工输出,最后直接把生成的答案或解决问题的办法呈现在你的眼前。


而且现在 AI Coding 工具如此强大,从遇到问题到解决问题,甚至都不需要跳出 IDE,问题就可以被完美解决。


所以相比去 Stack Overflow 上发帖子、搜问题、筛答案,AI 引擎无论在时间效率,还是知识维度的扩展上都给了这些传统技术社区以降维打击。


传统搜索引擎往往依赖于关键词匹配和链接分析,因此对于用户问题的理解往往有所欠缺,而 AI 大模型则能够深度理解语言含义和上下文,理解问题的真正意图。


而且 AI 大模型的分析理解能力、整合能力以及推理能力,这些都是传统知识社区和搜索引擎往往所欠缺的东西。


同时 AI 大模型能阅读、理解并整合数据中不同维度的海量知识,并能在此基础上来进行进一步的推理、分析、总结、泛化,这在如今的信息爆炸的时代来说是一种巨大的价值。


所以从这个角度来看,AI 大模型引擎并不是搜索引擎的简单升级版,而是一种全新的信息处理和交互范式。




当然,把 Stack Overflow 衰落的全部原因都归结于 AI 其实也不太公平。


即便不谈 AI 的因素,从大家的反馈来看,近年来 Stack Overflow 社区氛围的下坡路也是其衰落的一个不可忽略的因素


比如很多初学者的问题经常会由于问题太基础或者格式不对而被下架,另外不少同学反馈 Stack Overflow 上戾气也不小,包括还能看到对新人小白冷嘲热讽,以及老用户之间的斗气争吵等等,这些都会慢慢磨灭大家的热情以及社区的技术氛围。


所以各种内外因素加在一起,再来看如今 Stack Overflow 的这般发展趋势,也就不足为奇了。




那面对 AI 大模型这波浪潮的席卷和冲击,不少传统的搜索引擎和知识社区都开始了转型升级,并积极拥抱 AI。


包括像 Stack Overflow 他们自己也搞了一个 Overflow AI,其中包含了一套基于他们自己的历史内容和知识库所打造的 GenAI 工具。


从「检索工具」进化到「智能助手」,这是不少现技术社区和知识引擎正在经历的蜕变之路。


这两年 AI 大模型领域的发展速度相信大家都有目共睹了,技术迭代进化更是远超预期。


可以预见的是,未来的信息检索和交互方式一定还会进一步高效、精准和智能,而对此我们也可以拭目以待。


好了,那以上就是今天的内容分享了,感谢大家的阅读,我们下篇见。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7594350054091259946
收起阅读 »

Vue 3 + Three.js 打造轻量级 3D 图表库 —— raychart.js

web
大家好,我是 一颗烂土豆。 最近在数据可视化领域进行了一些探索,基于 Vue 3 和 Three.js 开发了一款轻量级的 3D 图表库 —— raychart.js。 今天不谈晦涩的代码实现,主要和大家分享一下这个项目的设计初衷、目前进展以及未来的规划。 ...
继续阅读 »

大家好,我是 一颗烂土豆


最近在数据可视化领域进行了一些探索,基于 Vue 3Three.js 开发了一款轻量级的 3D 图表库 —— raychart.js


今天不谈晦涩的代码实现,主要和大家分享一下这个项目的设计初衷目前进展以及未来的规划



💻 在线体验chart3js.netlify.app/



格式工厂 屏幕录像20260113_214646 00_00_00-00_00_37 00_00_00-00_00_30.gif


🌟 愿景 (Vision)


在实际开发中,我们往往面临两难的选择:要么使用传统的 2D 图表库(如 ECharts)通过“伪 3D”来实现效果,但缺乏立体感和自由视角;要么直接使用 Three.js 从零撸,成本高且难以复用。


chart3 的诞生就是为了解决这个问题,它的核心愿景是:



  1. 极简配置:延续 ECharts 的 "Option-based" 配置思维,让前端开发者无需深入了解 WebGL/Three.js 的底层细节,通过简单的 JSON 配置即可生成炫酷的 3D 图表。

  2. 真 3D 体验:全场景 3D 渲染,支持 360 度自由旋转、缩放、平移,提供真实的光影、材质和空间感。

  3. 轻量与现代:完全基于 Vue 3 Composition API 和 TypeScript 构建,模块化设计,无历史包袱。


🚀 现状 (Current Status)


目前项目处于快速迭代阶段,核心引擎已经搭建完毕,并实现了一套可视化的配置系统。你可以通过 在线 Demo 实时调整参数并预览效果。


已支持的功能特性:



  • 基础图表组件

    • 📊 3D 柱状图 (Bar3D):支持多系列、不同颜色的柱体渲染。




ScreenShot_2026-01-12_110024_828.png



  • 🥧 3D 饼图 (Pie3D):支持扇区挤出高度、标签展示。


ScreenShot_2026-01-12_110108_307.png
* 📈 3D 折线图 (Line3D):支持管状线条渲染。


ScreenShot_2026-01-12_110046_630.png
* 🌌 3D 散点图 (Scatter3D):支持三维空间的数据点分布。


ScreenShot_2026-01-12_110004_262.png



  • 可视化配置系统

    • 数据源 (Data):支持静态数据配置。

    • 主题与配色 (Theme):内置多套配色方案,支持自定义默认颜色。

    • 坐标系 (Coordinate):可实时调整网格的宽度、深度、高度,以及各轴线、刻度、网格线的显示与隐藏。

    • 材质系统 (Material):这是 3D 图表的灵魂。支持实时调节透明度、粗糙度 (Roughness)、金属度 (Metalness),轻松实现玻璃、金属等质感。

    • 灯光系统 (Lighting):支持环境光和方向光的强度与位置调节,营造氛围感。

    • 交互 (Interaction):支持鼠标悬停高亮、HTML 标签 (Label) 自动跟随。




📅 待实现的任务 (Roadmap)


为了让 chart3 真正成为生产可用的图表库,后续还有很多有趣的工作要做:



  • 高级图表开发

    • 🌊 3D 曲面图 (Surface 3D):用于展示复杂的三维函数或地形数据(目前 Demo 中显示为“待开发”)。

    • 🗺️ 3D 地图 (Map 3D):支持 GeoJSON 数据的三维挤出渲染。



  • 性能优化

    • 引入 InstancedMesh 技术,大幅提升大数据量(如 10w+ 散点或柱体)下的渲染性能。



  • 动画系统

    • 实现图表的入场动画(如柱子升起、饼图展开)。

    • 数据更新时的平滑过渡动画。



  • 工程化与文档

    • 完善 API 文档和使用指南。

    • 提供 NPM 包发布,方便项目集成。




🤝 结语


这个项目是我对“数据可视化 x 3D”的一次尝试。


让我们一起把数据变得更酷一点!


作者:一颗烂土豆
来源:juejin.cn/post/7594040270502379558
收起阅读 »

这两个网站,一个可以当时间胶囊,一个充满了赛博菩萨。

web
你好呀,我是歪歪。 前两天不是发了这篇《可怕,看到一个如此冷血的算法。》嘛。 文章中有这样的一个链接: 我当时放这个链接的目的是为了方便大家直达吃瓜现场。 但是,由于这个帖子最终被证实是假的,所以被官方给“夹”了: 幸好,原文本来就不长,所以我在我的文章中...
继续阅读 »

你好呀,我是歪歪。


前两天不是发了这篇《可怕,看到一个如此冷血的算法。》嘛。


文章中有这样的一个链接:



我当时放这个链接的目的是为了方便大家直达吃瓜现场。


但是,由于这个帖子最终被证实是假的,所以被官方给“夹”了:



幸好,原文本来就不长,所以我在我的文章中把原文全部给截下来了。


也算是以另外一种形式保留了吃瓜现场。


如果这个“爆料”的帖子再长一点,按照我的习惯,我可能就不会把整个帖子搬运过来了,只会留取我认为关键的部分。


但是这种“我认为关键的部分”是非常主观的,有的人就是想看原贴长什么样,但是原贴又被删除了,怎么办?


我教你一招,老好用了。


时间胶囊


在万能的互联网上,有这样一个仿佛是时间胶囊一般存在的神奇的网站:



archive.org/




这个网站是叫做"互联网档案馆"(Internet Archive),于 1996 年成立的非营利组织维护的网站。


自 1996 年以来,互联网档案库与世界各地的图书馆和合作伙伴合作,建立了一个人类在线历史的共享数字图书馆。


这个网站有一个非常宏大的愿景:



捕捉大小不一的网站,从突发新闻到被遗忘的个人页面,使它们能够为子孙后代保持可访问性。



所以里面收藏了的内容有免费书籍、电影、软件、音乐、网站等。


截至目前,该网站收集了这么多的数据:



其中网站的数量是最多的,有 1T,超过 1T 的时候,官方还发文庆祝了一下:



这个 1T 中的 T 指的是什么呢?


Trillion。


一个非常小众的词汇啊,歪师傅也不认识,所以我去查了一下:



这个图片上一眼望去全是 0。



1 Trillion 就是 1,000,000,000,000



反正是数不过来了。


感觉成都都没有这么多 0。


这个网站怎么用呢?


很简单。


拿前面 reddit 中被“夹”了的帖子举例。


我不是给了吃瓜现场的链接嘛。


你把链接往“时光机”的这个地方一粘:



你就会看到这个有一个时间轴的页面:



把鼠标浮到有颜色的日期上,就能看到各个时间点的页面快照了。


颜色越深代表那一天的快照越多:



比如,我们看一下这个网站收集到的第一个快照:



点进去,就是我们要找的吃瓜现场。


发帖后的两小时就被收集到了,速度还是挺快的。


从数据上看,这个时候已经有 3.7k 个点赞和 255 个评论,已经有要起飞的预兆了。


换个时间的快照,还可以看到点赞和评论的数据变化,比如发帖一天后:



点赞量已经是 71k,评论数来到了 3.8K,直接就是一个起飞的大动作。


这里只是用这个帖子举个例子。


再举一个例子。


也是我的真实使用场景。


有一次我在研究平滑加权轮询负载均衡策略算法为什么是平滑的。


和各类 AI 讨论了半天,它们也给出了各种参考文献。


我在其中一个参考文献中看到了这样一个链接:



tenfy.cn/2018/11/12/…



我知道这个链接的内容就是我要找的内容,但是这个链接跳转过去已经是 404 了:



于是,时间胶囊就派上用场了。


我直接把这个链接扔它:



找到了这个网页在 2019 年 12 月 10 日的快照:



通过这种方式就找到了原本已经被 404 的网页内容。


在看一些时间比较久远的文章的时候,参考链接打不开的情况,还是比较常见的。


所以这个方式是我最常用的一个场景。


此外,还有另外一个场景,就是偶尔去怀旧一下。


比如,中文互联网的一滴眼泪:天涯论坛。



这是 20 年前,2006 年 1 月的天涯论坛首页,一股浓烈的早期互联网风格:



在图片的右下角你还能看到“2006 天涯春晚”的字样。


另外,你不要觉得这只是一个静态页面。


里面的部分链接还是可以正常跳转的。


比如,这个链接:



点进去,你可以看到最最古早的一种直播形式:文字直播。



2006 年 1 月 2 日,《武林外传》开播。


天涯这个文字直播的时间是 2006 年 1 月 19 日,《武林外传》当时正在全国热播。


天涯网友在这个页面下提出自己关于《武林外传》的问题,作为天涯的知名写手,宁财神本人会选择部分问题进行回复。


我截取了几个我觉得有意思的回复:



这种行为这算不算是官方剧透了?



当年祝无双这个角色是真的不让人讨喜啊。幸好当时的网络还不发达,不然我觉得真有可能“网爆祝无双”。



DVD,一个多么具有年代感的词。




写文章的时候,我本来是想截几张图就走的,最多五分钟搞定。


结果我竟然一页页的翻完了这个帖子,看完之后才发现在这个帖子里面待了半个多小时。


时间过的还是很快的。


站在 2026 年,看 2006 的帖子,中间有 20 年的光阴。


但是就像是 2006 年佟掌柜对要给她干二十年工才能还清债务的小郭说的那样:不要怕,二十年快得很,弹指一挥间。



前几天小郭在微博上还回应了正式赎身这个梗。


去了六里桥、去了同福夹道、去了左家庄站、还去了祥蚨瑞,最后在人来人往的北京街头,一个猝不及防的回眸:



这是我的童年回头看了我一眼。


十几岁的不了解佟掌柜的这句话,三十出头了,一下就理解了:20 年,真的很快呀。


看到 2006 年的天涯的时候,我依稀想起了一些当年的往事。


那个时候我才 12 岁,看电视剧是真的在电视机上看,我还记得家里的电视机都是这样的“大屁股”电视机:



还记得《武林外传》每集开始,唱主题曲的时候,电视上面会显示一个电脑的桌面:



所以每次开头的时候,我就会叫表妹过来,对她说:你看,我等下把电视变成电脑。


那个时候表妹才 7 岁,我这个 12 岁的哥哥当然是把她唬的一愣一愣的。


那个时候电脑也还是一个稀奇的物品,虽然是乡下的学校,但是也还是有一个微机室,去微机室上课必须要带鞋套的那种。


所以 2006 年的天涯,我肯定是没有看过的,但是在 2026 年看到 2006 的天涯,我还是想起了很多童年往事。


对了,前几天才给表妹过完 27 岁的生日:



看着这张照片,再想起 7 岁时那个相信哥哥可以把电视变成电脑给她看《武林外传》的妹妹。


“二十年快得很,弹指一挥间”。


你说这不叫时间胶囊,叫什么?


再看一下 10 年前,2016 年 1 月 1 日的天涯,彼时的天涯可以说是如日中天,非常多的网友天天泡在论坛里面,谈古论今,激扬文字。


这是那天的天涯首页截图:



热帖榜第一的是一个关于纯电动汽车的帖子,我进去看了一下:



这个帖子的点击量是 10w,有 816 个回复。


可见这确实是当时的一个非常热门的话题。


按照作者的观点,纯电汽车代替燃油汽车,还很长的路要走。


站在 10 年后的今天,其实我们已经知道答案了。


但是,当我看到这个回复的时候,我还是佩服天涯网友的眼光:



除了天涯,还可以考古很多其他的网站。


比如,B 站:



从 2011 年开始有了网页快照,我随便点开一看,满满的历史感:



而这是 2016 年,10 年前的 B 站首页:



当时还有一个专门的鬼畜区:



而这里的一些视频甚至还是可以播放的。


比如这个“启蒙作品”:



现在在 B 站有 160w 的播放:



在这个视频的评论区,你能找到大量来“考古”的人:





二十年都弹指一挥间了,别说区区十年了。


从 B 站怀旧完成后,随便,我也去磨房、马蜂窝、穷游网看了一圈,随便选了 2012 年到 2016 年间的一些页面,感谢它们陪我度过了一整个美好的大学生活。


是我当时认识、感知、体验这个的广阔世界的一个重要窗口。


感谢磨房 4 年的陪伴:



感谢马蜂窝 4 年的陪伴:



感谢穷游网 4 年的陪伴:



如果你也有想要寻找的记忆,可以尝试在这个网站上去找一找。


存档


既然已经聊到“archive”了,那就顺便再分享一个“archive.today”。



archive.ph/




这个网站和前面的“互联网档案馆”最大的一个差异是“互联网档案馆”是它主动去做“网页快照”,什么时候做,什么页面做,并不一定。


而“archive.today”是一个你可以去主动存档的网站。


比如,还是说回 reddit 上的那个帖子。


帖子下面有这样的一个回复:



这个回复中的超链接就是回复者找到的关于这个“爆料”是 AI 生成的证据。


点过去是这样的:



他提供的是一个网页存档。


为什么他要这么做呢?


你想想,如果他提供一个原始链接,但是这个原始链接突然有一天找不到了,岂不是很尴尬?


但是先在“archive.today”上存档一下,然后把这个存档后的链接贴出来,就稳当多了。


以后你要保存证据的话,你就可以使用这个网站。


另外,这个网站还有一个骚操作。


反而是骚操作让这个网站的打开率更高一点。


国外的一些网站可能有些文章是要付费才能看到的。


比如纽约时报:



但是,如果你一不小心把付费文章的链接贴在这个网站上去搜索。


有一些“好事之人”已经帮你把文章在这个网站上做了快照了,这些人可以称之为“赛博菩萨”,因为这些“菩萨”,你就可能看到免费的原文了:



在这里叠个甲啊,偶尔看到一两篇的话可以这样操作一下,就当时是试看了。


如果经常要看的话,还是充点钱吧。


对了,多说一句,上面提到的神奇的网站既然叫做时光胶囊,还有一些赛博菩萨,这些魔法世界中才有的东西,那肯定需要你会对应的魔法咒语才能访问到。如果你不会魔法,强行访问,那你肯定要撞到墙上。



作者:why技术
来源:juejin.cn/post/7594266018304737343
收起阅读 »

2026 年 Web 前端开发的 8 个趋势!

web
1. 前言 2025 年是 Web 开发的分水岭。 之前 Web 开发领域一直发展迅速,几乎每天都有新的工具和框架涌现。 但到了 2025 年,这种发展速度直接呈指数级增长。 之所以有这种变化,很大程度上是因为 AI 工具的高效性,它们直接将生产力提升了 3 ...
继续阅读 »

1. 前言


2025 年是 Web 开发的分水岭。


之前 Web 开发领域一直发展迅速,几乎每天都有新的工具和框架涌现。


但到了 2025 年,这种发展速度直接呈指数级增长。


之所以有这种变化,很大程度上是因为 AI 工具的高效性,它们直接将生产力提升了 3 倍!


想想几年前,我们还在争论 GitHub Copilot 这样的 AI 工具是否可靠,如今,AI 已经能构建完整的全栈应用程序了!。


这也让不少人担忧,AI 是否真的能取代我们。


站在 2026 年的门槛上,让我们一起看看,今年会有哪些真正影响你我的技术趋势。


注意:这不是那种“5 年以后”的远景预测,而是今年你就有可能遇到的实实在在的变化。


2. AI 优先开发


AI 工具已经不再试一个简单的代码补全工具,它已经成为开发的核心组成部分。


开发人员更像是架构师的角色,监督 AI 智能体工作。毕竟 AI 智能体已经可以根据 Figma URL 或自然语言提示搭建完整的功能框架。


AI 也在重塑开发者探索和理解代码的方式。


团队不再需要手动阅读庞大的代码库,利用 AI 直接可以解释不熟悉的逻辑、追踪数据流并发现边缘 case。这极大地缩短了新用户上手时间,也让大型项目更易于操作。


因此,采用 AI 优先开发的团队将减少在机械性工作上花费的时间,而将更多精力投入到项目架构、用户体验的优化上。


这些工具虽然不能编写完美的代码,但它们会改变开发人员的精力投入方向。


3. 元框架成为默认设置


还记得当年选技术栈时的纠结吗?


路由用哪个?打包工具选什么?状态管理怎么办?


现在,这些问题都有了一个标准答案:用 Next.js 或 Nuxt 就完了。


因为这些元框架就是一个“全家桶套餐”,把你需要的所有东西都打包好了。


路由、数据获取、缓存、渲染策略、API 接口……统统内置。很多时候,后端就是前端项目里的一个文件夹。


AI 工具的兴起也加速了这一转变。现在大多数生成式 UI 构建器默认都会生成元框架项目。Vercel 自家的构建器 v0 就是一个很好的例子:开箱即用,直接输出 Next.js 应用程序。


对开发者来说,这是个好消息,意味着你可以把更多精力放在业务逻辑上,而不是纠结工具链的选择。


4. 前端开发 TanStack 化


虽然元框架提供了结构,但 TanStack 套件(查询、路由、表格、表单)已成为逻辑层的实际标准。


从最早的 TanStack Query(以前叫 React Query)处理数据获取和缓存,到现在的 Table、Form、Router、Store……它几乎覆盖了前端开发的方方面面。


2025 年,TanStack 又推出了 DB、AI 等新工具,从库升级成了一个完整生态。


TanStack 最大的优势就是框架无关、实用至上。


无论你用 React、Vue 还是其他框架,TanStack 都能无缝接入。而且它的设计理念很务实,解决的都是开发中的实际痛点。


TanStack 俨然成为前端界的“瑞士军刀”。


5. TypeScript + 服务端函数,告别传统后端


TypeScript 已经是标配,2026 年还在写 JavaScript 多少有些过时了。


而且随着服务端函数和托管后端的流行,前端和后端的界限将越来越模糊。


举个例子:


使用 tRPC,你可以在前端直接调用后端函数,而且类型完全同步。不需要手写 API 文档,不需要维护接口定义,改了后端,前端自动感知。


这就好比以前你要写信寄到邮局,现在直接打电话——即时、准确、零误差。


6. React 编译器越来越普及


还记得为了优化性能,到处写 useMemo、useCallback、React.memo 的日子吗?


React 编译器(React Compiler)在 2025 年 10 月发布 v1.0 后,已经开始大规模应用。它能在构建时自动处理性能优化,你只管写清晰的代码,编译器帮你搞定优化。


就像相机的自动对焦——以前要手动调,现在按快门就行。


如今 Next.js 16、Vite、Expo 等主流工具已经内置了 React 编译器。


创建新项目时,它就是默认配置的一部分。


这对新手特别友好。不用纠结性能问题,专注于功能实现就好,代码也更简洁易读。


7. 边缘计算开始普遍


以前部署应用,服务器可能在北京,广州的用户访问就慢半拍。


边缘计算的核心思路是:让代码跑在离用户最近的节点上。


你在上海?就用上海的服务器。你在成都?就用成都的。延迟大幅降低,响应速度更快。


而且现代框架的很多特性——比如服务端函数、流式响应——天生就适合边缘部署。再加上 AI 工具(像 v0、Lovable)一键生成边缘应用,这个趋势已经不是“要不要”的问题,而是“什么时候”的问题。


到 2026 年,边缘部署会成为默认选项。作为开发者,你需要习惯在设计时就考虑边缘环境的特点。


8. CSS:原生能力回归,实用工具辅助


原生 CSS 这些年在不断进化。


容器查询、层叠样式表、CSS 变量、现代颜色函数……这些新特性让 CSS 的表达能力大幅提升。


于是现在的趋势变成了混合使用:传统的实用类负责快速搭建,原生 CSS 负责精细控制。


比如特定样式以 CSS 变量的形式表示,变体和主题通过 layers 和选择器来处理,而不再依赖构建时处理。


9. React 安全性提升


202025 年,React 生态爆出了不少安全漏洞,比如 Next.js 中间件漏洞和 React2Shell。


这是因为前端承担的责任越来越重。


以前前端就负责展示,安全问题是后端的事。


现在 React 应用要处理身份验证、数据访问、业务逻辑……攻击面大大增加。


所以 2026 年预计框架会推出更多“防御性默认设置”,防止开发者犯错。


静态分析工具会更智能,开发时就能发现潜在安全隐患。框架和安全扫描器的集成会更紧密。


10. 结论


2026 年的前端开发,核心变化是角色转变。


你不再是“写代码的人”,而是“协调资源的人”。


AI 帮你写重复代码,编译器帮你优化性能,框架帮你搭好架构……


你要做的,是把精力放在更重要的事情上:



  • 理解用户需求

  • 设计系统架构

  • 把控产品质量

  • 优化用户体验


技术在进步,工具在演化,但解决问题的能力和对用户的关注——这些才是永远不会过时的核心竞争力。


2026 年,我们不是被工具取代,而是在工具的帮助下,做更有价值的事。


我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。


欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs) ,每天分享前端知识、AI 干货。


作者:冴羽
来源:juejin.cn/post/7594028166135250944
收起阅读 »

WebSocket,退!退!退!更简单的实时通信方案在此

web
多标签页实时消息同步方案:SSE + BroadcastChannel 完美解决! 你是否遇到过这样的问题: 用户同时打开多个标签页,每个标签页都建立了独立的 WebSocket 连接,导致服务器压力大、消息重复推送、资源浪费?本文将分享一个优雅的解决方案,...
继续阅读 »

多标签页实时消息同步方案:SSE + BroadcastChannel 完美解决!



你是否遇到过这样的问题:
用户同时打开多个标签页,每个标签页都建立了独立的 WebSocket 连接,导致服务器压力大、消息重复推送、资源浪费?本文将分享一个优雅的解决方案,通过 SSE + BroadcastChannel 的组合,实现单连接、多标签页实时消息同步,既节省资源又提升用户体验。



适用场景


推荐使用



  • 实时消息推送:系统通知、用户消息、业务提醒等

  • 数据同步:多标签页状态同步、购物车同步、表单数据同步

  • 任务状态更新:后台任务进度、数据处理状态、导出任务完成通知

  • 系统公告:全局消息广播、系统维护通知、版本更新提示


实际案例


在我们的 BI 系统中,该方案成功应用于:



  • 消息中心:实时推送系统消息和业务通知

  • 任务管理:后台数据处理任务的状态更新和完成通知(如素材批量上传任务)

  • 国际化同步:多语言配置的实时更新



国际化同步这块配合 apollo 的配置中心实现多语言配置更新发布后,系统会无感自动实时更新翻译,超级爽



不推荐使用



  • 高频双向通信:如实时聊天、游戏等,建议使用 WebSocket

  • 大量数据传输:如文件传输、大数据同步,建议使用 HTTP 轮询或分页

  • 跨域通信:需要使用 postMessage 或其他跨域方案


前言



如果不想了解技术背景可点击直接跳转到实现方案👇



初衷


在现代 Web 应用中,实时消息推送、任务状态更新等是常见的需求。然而,当用户同时打开多个标签页时,如何确保消息能够正确同步到所有标签页,同时避免重复连接和资源浪费,是一个值得深入探讨的技术问题。


本文基于实际项目经验,分享如何通过 SSE(Server-Sent Events)BroadcastChannel API 的组合方案,实现高效的多标签页实时消息同步。该方案不仅解决了单标签页消息推送的问题,还优雅地处理了多标签页场景下的连接管理和消息分发。


问题背景


多标签页消息同步的挑战


在实际业务场景中,我们遇到了以下问题:


场景一:用户打开多个标签页


当用户同时打开多个标签页访问同一个应用时,如果每个标签页都建立独立的 SSE 连接,会导致:



  • 服务器资源浪费(多个长连接)

  • 消息重复推送(每个标签页都收到相同消息)

  • 用户体验不一致(不同标签页消息状态不同步)



打开多页签若系统采用 HTTP 1.0/1.1 协议,用户每打开一个页面就会建立一个长连接;当打开的标签页数量超过 6 个时,受浏览器并发连接数限制,第七个及之后的标签页将无法正常加载,出现卡顿。



场景二:标签页关闭与重连


当某个标签页关闭时,如果该标签页持有唯一的 SSE 连接,其他标签页将无法继续接收消息。需要:



  • 检测连接断开

  • 自动在其他标签页重新建立连接

  • 保证消息不丢失


场景三:消息去重与状态同步


多个标签页需要:



  • 避免重复显示相同的消息通知

  • 保持消息已读/未读状态同步

  • 统一更新 UI 状态(如未读消息数)


传统方案的局限性


方案优点缺点
纯 SSE实现简单,浏览器原生支持多标签页会建立多个连接,资源浪费
纯 WebSocket双向通信,功能强大实现复杂,需要心跳检测,多标签页问题同样存在
LocalStorage 事件跨标签页通信简单只能传递字符串,性能较差,不适合频繁通信
SharedWorker真正的单例连接兼容性一般,调试困难



技术选型


为什么选择 SSE


SSE(Server-Sent Events) 是 HTML5 标准中的一种服务器推送技术,具有以下优势:



  1. 简单易用:基于 HTTP 协议,无需额外协议升级

  2. 自动重连:浏览器原生支持断线重连机制

  3. 单向推送:适合服务器主动推送消息的场景

  4. 文本友好:天然支持文本数据,JSON 解析方便


// SSE 基本使用
const eventSource = new EventSource('/api/sse');
eventSource.onmessage = (event) => {
console.log('收到消息:', event.data);
};

为什么选择 BroadcastChannel


BroadcastChannel API 是 HTML5 提供的跨标签页通信方案:



  1. 同源通信:同一域名下的所有标签页可以通信

  2. 简单高效:API 简洁,性能优秀

  3. 类型支持:支持传输对象、数组等复杂数据类型

  4. 事件驱动:基于事件机制,易于集成


// BroadcastChannel 基本使用
const channel = new BroadcastChannel('my-channel');
channel.postMessage({ type: 'MESSAGE', data: 'Hello' });
channel.onmessage = (event) => {
console.log('收到广播:', event.data);
};

组合方案的优势


将 SSE 和 BroadcastChannel 结合,可以实现:



  • 单连接管理:只有一个标签页建立 SSE 连接

  • 消息广播:SSE 接收的消息通过 BroadcastChannel 同步到所有标签页

  • 连接恢复:标签页关闭时,其他标签页自动接管连接

  • 状态同步:所有标签页的消息状态保持一致




实现方案


整体架构设计


sequenceDiagram
participant Server as 服务器端
participant TabA as 标签页 A<br/>(主连接)
participant BC as BroadcastChannel
participant TabB as 标签页 B<br/>(从连接)

Note over TabA: 初始化阶段
TabA->>TabA: 检查是否有 SSE 连接
alt 无连接
TabA->>Server: 建立 SSE 连接
Server-->>TabA: 连接成功
end

Note over Server,TabB: 消息接收阶段
Server->>TabA: 推送消息 (SSE)
TabA->>TabA: 处理消息<br/>(更新状态、显示通知)
TabA->>BC: 广播消息
BC->>TabB: 同步消息
TabB->>TabB: 处理消息<br/>(更新状态、显示通知)

Note over TabA,TabB: 连接管理阶段
TabA->>TabA: 标签页关闭
TabA->>BC: 发送关闭信号
BC->>TabB: 通知连接关闭
TabB->>TabB: 关闭旧连接
TabB->>Server: 重新建立 SSE 连接
Server-->>TabB: 连接成功

核心流程



  1. 初始化阶段



    • 应用启动时,检查是否已有 SSE 连接

    • 如果没有,当前标签页建立 SSE 连接

    • 如果有,直接使用现有连接



  2. 消息接收阶段



    • SSE 连接接收到服务器推送的消息

    • 当前标签页处理消息(显示通知、更新状态)

    • 通过 BroadcastChannel 广播消息到其他标签页

    • 其他标签页接收广播,同步处理消息



  3. 连接管理阶段



    • 标签页关闭时,发送关闭信号到 BroadcastChannel

    • 其他标签页监听到关闭信号,关闭旧连接

    • 重新建立 SSE 连接,确保消息不中断





注意这里服务端接入 SSE 的时候可以设置同一用户下只保持一个活跃连接即可,历史连接丢弃超时会自动断开





核心实现


1. SSE 连接封装


首先,我们需要封装一个支持重连和错误处理的 SSE 连接工具:


import { EventSourcePolyfill } from 'event-source-polyfill';
import util from '@/libs/util';
import Setting from "@/setting";

const MAX_RETRY_COUNT = 3;
const RETRY_DELAY = 3000;

const create = (url, payload) => {
let retryCount = 0;

const connect = () => {
const token = util.cookies.get("token")
if(!token){
return
}

const eventSource = new EventSourcePolyfill(
`${Setting.request.apiBaseURL}${url}`,
{
headers: {
token: util.cookies.get("token"),
pageUrl: window.location.pathname,
userId: util.cookies.get("userId"),
},
heartbeatTimeout: 28800000, // 8小时心跳超时
}
);

eventSource.addEventListener("open", function (e) {
console.log('SSE连接成功');
retryCount = 0; // 重置重试次数
});

eventSource.addEventListener("error", function (err) {
console.error('SSE连接错误:', err);

if (retryCount < MAX_RETRY_COUNT) {
retryCount++;
console.log(`尝试重新连接 (${retryCount}/${MAX_RETRY_COUNT})...`);
setTimeout(() => {
eventSource.close();
connect();
}, RETRY_DELAY);
} else {
console.error('SSE连接失败,已达到最大重试次数');
eventSource.close();
}
});

return eventSource;
};

return connect();
}

export default {
create
}

关键点解析



  • 使用 EventSourcePolyfill 支持自定义 headers(原生 EventSource 不支持)

  • 实现自动重连机制,最多重试 3 次

  • 设置心跳超时时间,防止长时间无响应导致连接假死

  • 在 headers 中传递 token 和页面信息,便于服务端识别和路由


2. BroadcastChannel 封装


创建一个简洁的 BroadcastChannel 工具类:


export const createBroadcastChannel = (channelName: string) => {
const channel = new BroadcastChannel(channelName);
return {
channel,
sendMessage(data: any) {
channel.postMessage(data);
},
receiveMessage(callback: (data: any) => void) {
channel.onmessage = (event) => {
callback(event.data);
};
},
closeChannel() {
channel.close();
},
};
};

设计说明



  • 封装成工厂函数,便于创建多个通道(消息通道、连接管理通道)

  • 提供简洁的 API:发送消息、接收消息、关闭通道

  • 支持传递任意类型数据(对象、数组等)


3. SSE 连接管理


实现单例模式的 SSE 连接管理:


import sseRequest from "@/plugins/request/sse";
import store from "@/store";

export const fetchSSE = (payload?: { [key: string]: string }) => {
const eventSource = sseRequest.create("/sse/connect", {
...payload
});
return eventSource;
};

export const initSSEEvent = async () => {
console.log('sse-init');
// 检查是否已经有实例在当前标签页中创建,可用于项目中获取实例方法用
let eventSource = (store.state as any).admin.request.sseEvent;

if (!eventSource) {
// 如果没有实例,则创建一个新的
eventSource = fetchSSE();
// 存储到 Vuex 中
store.commit('admin/request/SET_SSE_EVENT', eventSource);
}

return eventSource;
};

核心逻辑



  • 通过 Vuex 全局状态管理 SSE 连接实例

  • 实现单例模式:如果已有连接,直接复用

  • 避免多个标签页同时建立连接


4. 消息处理与广播


实现消息接收、处理和跨标签页同步:


import { createBroadcastChannel } from "@/libs/broadcastChannel";

// 创建消息广播通道
const { sendMessage, receiveMessage } =
createBroadcastChannel("message-channel");

export const pushWatchAndShowNotifications = async (): Promise<any> => {
// 获取 SSE 连接实例
const eventSource = (store.state as any).admin.request.sseEvent;
if (!eventSource) {
return;
}

// 监听服务器推送的消息
eventSource.addEventListener("MESSAGE", function (e) {
const fmtData = JSON.parse(e.data);

// 1. 广播消息到其他标签页
sendMessage(fmtData);

// 2. 当前标签页处理消息
handleIncomingMessage(fmtData);
});

// 监听用户任务推送
eventSource.addEventListener("USER_TASK", function (e) {
const fmtData = JSON.parse(e.data);

// 广播任务消息到其他标签页
sendMessage({ type: "USER_TASK", data: fmtData });

// 当前标签页处理任务消息
handleIncomingUserTask(fmtData);
});

// 监听其他标签页广播的消息
receiveMessage((data) => {
if (data.type === "USER_TASK") {
handleIncomingUserTask(data.data);
} else {
handleIncomingMessage(data);
}
});

return eventSource;
};

function handleIncomingMessage(fmtData: any) {
const productId = (store.state as any).admin.user.info?.curProduct;
const productData = fmtData[productId];
if (!productData) {
return;
}

const { noReadCount, popupList } = productData;
// 更新未读消息数
store.commit("admin/layout/setUnreadMessage", noReadCount);

// 显示消息通知
if (popupList.length > 0) {
popupList.forEach((message, index) => {
showNotification(message, index);
});
}
}

处理流程



  1. SSE 接收到消息后,立即通过 BroadcastChannel 广播

  2. 当前标签页处理消息(更新状态、显示通知)

  3. 其他标签页通过 BroadcastChannel 接收消息,同步处理

  4. 确保所有标签页状态一致


5. 连接恢复机制


实现标签页关闭时的连接恢复:


import { createBroadcastChannel } from '@/libs/broadcastChannel';

// 创建连接管理通道
const { sendMessage, receiveMessage } =
createBroadcastChannel('sse-close-channel');

export default defineComponent({
methods: {
handleCloseMessage() {
const sseEvent = (store.state as any).admin.request.sseEvent
if (sseEvent) {
sseEvent.close()
store.commit('admin/request/CLEAR_SSE_EVENT');
}
},
handleSSEClosed() {
// 监听其他标签页关闭 SSE 连接的消息
receiveMessage((data) => {
if (data === 'sse-closed') {
console.log('SSE connection closed in another tab. Re-establishing connection.');
// 关闭旧连接
this.handleCloseMessage()
// 重新建立连接
initSSEEvent();
this.handleGetMessage()
this.handleGetUserTasks()
}
});
}
},
mounted() {
// 页面卸载时,关闭 SSE 连接并通知其他标签页
on(window, 'beforeunload', () => {
const eventSource = (store.state as any).admin.request.sseEvent;
if (eventSource) {
eventSource.close();
store.commit('admin/request/CLEAR_SSE_EVENT');
}
// 广播关闭消息
sendMessage('sse-closed');
});

// 初始化 SSE 连接
const token = (store.state as any).admin.user.info?.curProduct
|| util.cookies.get("token");
if (token && !(store.state as any).admin.request.sseEvent) {
initSSEEvent();
pushWatchAndShowNotifications();
}

// 监听其他标签页的连接关闭事件
this.handleSSEClosed();
},
beforeUnmount() {
this.handleCloseMessage()
}
})

恢复机制



  1. 标签页关闭时,发送 sse-closed 消息到 BroadcastChannel

  2. 其他标签页监听到消息,关闭旧连接并清理状态

  3. 重新初始化 SSE 连接和相关监听

  4. 确保至少有一个标签页保持连接


6. 状态管理


在 Vuex 中管理 SSE 连接状态:


export default {
namespaced: true,
state: {
sseEvent: null // SSE 连接实例
},
mutations: {
// 设置 SSE 事件
SET_SSE_EVENT(state, payload) {
state.sseEvent = payload
},
// 清除 SSE 事件
CLEAR_SSE_EVENT(state) {
state.sseEvent = null
}
}
}



方案总结


方案优势



  1. 资源优化



    • 多个标签页共享一个 SSE 连接,减少服务器压力

    • 降低网络带宽消耗

    • 减少客户端内存占用



  2. 用户体验提升



    • 所有标签页消息状态实时同步

    • 避免重复通知,减少干扰

    • 连接自动恢复,消息不丢失



  3. 实现简洁



    • 基于浏览器原生 API,无需额外依赖

    • 代码结构清晰,易于维护

    • 兼容性好,现代浏览器全面支持



  4. 扩展性强



    • 可以轻松添加新的消息类型

    • 支持多个 BroadcastChannel 通道

    • 便于集成到现有项目




局限性及注意事项



  1. 浏览器兼容性



    • BroadcastChannel 不支持 IE 和部分旧版浏览器

    • 需要提供降级方案(如 LocalStorage 事件)



  2. 同源限制



    • BroadcastChannel 只能在同源页面间通信

    • 跨域场景需要使用其他方案(如 postMessage)



  3. 连接管理



    • 需要妥善处理标签页关闭和刷新场景

    • 避免内存泄漏(及时清理事件监听)



  4. 错误处理



    • SSE 连接断开时需要重连机制

    • 网络异常时的降级策略




最佳实践建议



  1. 连接管理



    • 建议:使用单例模式管理连接

    • 建议:在应用入口统一初始化

    • 建议:页面卸载时清理资源



  2. 消息去重



    • 建议:为消息添加唯一 ID

    • 建议:使用 Set 或 Map 记录已处理消息

    • 建议:设置消息过期时间



  3. 性能优化



    • 建议:限制 BroadcastChannel 消息大小

    • 建议:使用防抖处理频繁消息

    • 建议:批量处理消息更新



  4. 错误恢复



    • 建议:实现指数退避重连策略

    • 建议:添加连接状态监控

    • 建议:提供手动重连功能




技术对比总结


特性SSE + BroadcastChannelWebSocket轮询
实现复杂度⭐⭐ 简单⭐⭐⭐⭐ 复杂⭐ 很简单
服务器压力⭐⭐ 低(单连接)⭐⭐⭐ 中等⭐⭐⭐⭐ 高
实时性⭐⭐⭐⭐ 优秀⭐⭐⭐⭐⭐ 极佳⭐⭐ 一般
多标签页支持⭐⭐⭐⭐⭐ 完美⭐⭐ 需额外处理⭐⭐⭐ 一般
浏览器兼容⭐⭐⭐⭐ 良好⭐⭐⭐⭐ 良好⭐⭐⭐⭐⭐ 完美

未来优化方向



  1. 连接池管理:支持多个 SSE 连接,按业务类型分离

  2. 消息队列:离线消息缓存和重放机制

  3. 性能监控:连接质量监控和自动优化

  4. 降级方案:兼容旧浏览器的替代实现




参考文档





结语


SSE + BroadcastChannel 的组合方案为多标签页实时消息同步提供了一个优雅的解决方案。该方案在保证功能完整性的同时,兼顾了性能和用户体验。希望本文能够帮助你在实际项目中更好地应用这些技术。


写在最后


如果你在实际项目中应用了这个方案,欢迎分享你的经验和遇到的问题。如果你有更好的想法或优化建议,也欢迎在评论区交流讨论。


如果这篇文章对你有帮助,请点个赞支持一下,让更多开发者看到这个方案!


作者:Focus_
来源:juejin.cn/post/7588355695100854281
收起阅读 »

🤡什么鬼?两行代码就能适应任何屏幕?

web
你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询! 秘诀就是 CSS Grid 的 auto-fill 和 auto-fit。 马上教你用!✨ 🧩 基础概念 假设你有这样一个需求: 一排展示很多卡片...
继续阅读 »

你可能想不到,只用两行 CSS,就能让你的卡片、图片、内容块自动适应各种屏幕宽度,彻底摆脱复杂的媒体查询!
秘诀就是 CSS Grid 的 auto-fillauto-fit


20250428112254_rec_.gif


马上教你用!✨




🧩 基础概念


假设你有这样一个需求:



  • 一排展示很多卡片

  • 每个卡片最小宽度 200px,剩余空间平均分配

  • 屏幕变窄时自动换行


只需在父元素加两行 CSS 就能实现:


/* 父元素 */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

/* 子元素 */
.item {
height: 200px;
background-color: rgb(141, 141, 255);
border-radius: 10px;
}



下面详细解释这行代码的意思:


grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

这是 CSS Grid 布局里定义列宽的常用写法,逐个拆解如下:


1. grid-template-columns



  • 作用:定义网格容器里有多少列,以及每列的宽度。


2. repeat(auto-fit, ...)



  • repeat 是个重复函数,表示后面的模式会被重复多次。

  • auto-fit 是一个特殊值,意思是:自动根据容器宽度,能放下几个就放几个,每列都用后面的规则。

    • 容器宽度足够时,能多放就多放,放不下就自动换行。




3. minmax(200px, 1fr)



  • minmax 也是一个函数,意思是:每列最小200px,最大可以占1fr(剩余空间的平分)

  • 具体来说:

    • 当屏幕宽度很窄时,每列最小宽度是200px,再窄就会换行。

    • 当屏幕宽度变宽,卡片会自动拉伸,每列最大可以占据剩余空间的等分1fr),让内容填满整行。




4. 综合起来



  • 这行代码的意思就是:

    • 网格会自动生成多列,每列最小200px,最大可以平分一行的剩余空间。

    • 屏幕宽了就多显示几列,屏幕窄了就少显示几列,自动换行,自适应各种屏幕!

    • 不需要媒体查询,布局就能灵活响应。




总结一句话:



grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

让你的网格卡片最小200px,最大自动填满一行,自动适应任何屏幕,布局永远美观!





这里还能填 auto-fill,和 auto-fit 有啥区别?




🥇 auto-fill 和 auto-fit 有啥区别?


1. auto-fill



🧱 尽可能多地填充列,即使没有内容也会“占位”




  • 会自动创建尽可能多的列轨道(包括空轨道),让网格尽量填满容器。

  • 适合需要“列对齐”或“固定网格数”的场景。


2. auto-fit



🧱 自动适应内容,能合并多余空列,不占位




  • 会自动“折叠”没有内容的轨道,让现有的内容尽量拉伸占满空间。

  • 适合希望内容自适应填满整行的场景。




👀 直观对比


假设容器宽度能容纳 10 个 200px 的卡片,但你只放了 5 个卡片:



  • auto-fill 会保留 10 列宽度,5 个卡片在前五列,后面五列是“空轨道”。

  • auto-fit 会折叠掉后面五列,让这 5 个卡片拉伸填满整行。


20250428151427_rec_.gif


👇 Demo 代码:


<h2>auto-fill</h2>
<div class="grid-fill">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<div>item4</div>
<div>item5</div>
</div>

<h2>auto-fit</h2>
<div class="grid-fit">
<div>item1</div>
<div>item2</div>
<div>item3</div>
<div>item4</div>
<div>item5</div>
</div>

.grid-fill {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 40px;
}
.grid-fit {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.grid-fill div {
background: #08f700;
}
.grid-fit div {
background: #f7b500;
}
.grid-fill div,
.grid-fit div {
padding: 24px;
font-size: 18px;
border-radius: 8px;
text-align: center;
}

兼容性


caniuse.com/?search=aut…


image.png




🎯 什么时候用 auto-fill,什么时候用 auto-fit?



  • 希望每行“有多少内容就撑多宽”,用 auto-fit

    适合卡片式布局、相册、响应式按钮等。

  • 希望“固定列数/有占位”,用 auto-fill

    比如表格、日历,或者你希望网格始终对齐,即使内容不满。




📝 总结


属性空轨道内容拉伸适用场景
auto-fill保留固定列数、占位网格
auto-fit折叠流式布局、拉伸填充



🌟 小结



  • auto-fill 更像“占位”,auto-fit 更像“自适应”

  • 推荐大部分响应式卡片用 auto-fit

  • 善用 minmax 配合,让列宽自适应得更自然




只需两行代码,你的页面就能优雅适配各种屏幕!

觉得有用就点赞收藏吧,更多前端干货持续更新中!🚀✨


作者:前端九哥
来源:juejin.cn/post/7497895954101403688
收起阅读 »

“全栈”正在淘汰“前端”吗?一个前端专家的焦虑与思考

最近在帮团队招人,看了一圈市场上的招聘要求(JD),心里有点五味杂陈。 随便打开几个“前端工程师”的JD,上面写着:精通React/Vue,这很正常;熟悉Next.js或Nuxt,这是加分项;有Serverless/Vercel/Netlify经验,了解Pri...
继续阅读 »

image.png


最近在帮团队招人,看了一圈市场上的招聘要求(JD),心里有点五味杂陈。


随便打开几个“前端工程师”的JD,上面写着:精通React/Vue,这很正常;熟悉Next.js或Nuxt,这是加分项;有Serverless/Vercel/Netlify经验,了解Prisma或GraphQL,熟悉数据库操作者优先...


比如下面这个更离谱😮:


image.png


我恍惚间觉得,这招的到底是个前端,还是一个“全干工程师”?


一个问题在我脑海里盘旋了很久:在全栈的大潮下,我们这些纯粹的前端专家,未来的生存空间在哪里?我们会被淘汰吗?




全栈的兴起,不是偶然,是必然


在抱怨之前,我得承认,这个趋势的出现,是技术发展和商业需求的必然结果。


技术的演进,让全栈的门槛变低了


曾几何时,前端和后端是两个泾渭分明、需要完全不同技能集的领域。前端写HTML/CSS/JS,后端搞Java/PHP/Python,中间隔着一条API的银河。


但现在呢?



  • Node.js的出现,让JavaScript统一了前后端语言。

  • Next.js, Nuxt 这类元框架,把路由、数据获取、服务端渲染这些原本属于后端一部分的工作,无缝地集成到了前端的开发流程里。

  • tRPC 这类工具,甚至能让前后端共享类型,连写API文档都省了。

  • Vercel, Netlify 这类平台,把部署、CDN、Serverless函数这些复杂的运维工作,变成了一键式的傻瓜操作。


技术的发展,正在疯狂地模糊前端和后端的边界。一个熟悉JavaScript的前端,几乎可以无缝地去写服务端的逻辑。


商业的诉求,让全栈的价值变高了


从老板的角度想,问题很简单:“我为什么要雇两个人(一个前端,一个后端),如果一个人能把一个功能从头到尾都搞定的话?”


尤其是在创业公司和中小团队,它减少了沟通成本,缩短了开发周期,加快了产品验证的速度。


所以,别再抱怨了。 前端全栈化,是一个不可逆转的趋势。




那全栈到底在淘汰什么?


既然趋势不可逆,那我们的焦虑从何而来?


我认为,全栈并没有在淘汰前端这个岗位,但它正在淘汰我们对“前端专家”的传统定义,以及一部分人的工匠精神。


1. 它在淘汰对深度的追求


人的精力是有限的。当你需要把时间分配给数据库设计、服务端逻辑、部署运维时,你还剩下多少时间,去深究前端的那些“硬骨头”?


我说的“硬骨头”,指的是:



  • 极致的性能优化:深入到浏览器渲染流水线,去优化每一帧的动画,去解决INP(Interaction to Next Paint)的交互延迟。

  • 复杂的图形学与动画:深入Canvas, WebGL, apg, 实现那些让人惊叹的数据可视化和交互效果。

  • 专业的无障碍(a11y) :确保你的应用,对于有障碍的用户来说,依然是可用和易用的。这本身就是一门极深的学问。

  • 深入浏览器底层:比如内存管理、垃圾回收机制、事件循环的微观任务等等。


当一个人的知识体系变得越来越“宽”时,他的“深度”不可避免地会受到影响。


2. 它在淘汰入门级前端的生存空间


我刚入行的时候,只要把HTML/CSS/JS玩明白,就能找到一份不错的工作。


但现在,一个刚毕业的年轻人,除了这些基础,好像还需要懂点Node.js,会用Next.js,了解Serverless……入门的门槛,被无限地抬高了。


3. 它在淘汰工匠精神


全栈的压力,本质上是快的压力。老板希望你快速交付一个完整的功能,而不是花三天时间去打磨一个完美的CSS动画。


在快速搞定和优雅实现之间,天平往往会向前者倾斜。那种对像素的偏执、对交互细节的琢磨、对代码美学的追求,在全栈的背景下,有时会显得有些奢侈。




作为前端专家,我们的出路在哪里?


聊了这么多焦虑,那我们该怎么办?坐以待毙吗?当然不。


1. 拥抱T型人才,但要做主干


我们不能抗拒趋势。拓宽自己的知识广度(T的横向),去了解Node.js,了解部署,是必须的。这能让你和其他角色有更好的沟通,有更全局的视野。


但更重要的,是把你最核心的那一竖,挖得比任何人都深。


在一个团队里,当所有全栈工程师都能快速实现一个80分的功能时,那个能站出来,把一个核心功能的性能从80分优化到95分,或者解决一个极其诡异的浏览器兼容性Bug的专家,他的价值是无可替代的。


在一个人人都懂点后端的前端团队里,那个最懂浏览器的人,才是最稀缺的。


2. 成为用户体验的负责人


前端,是离用户最近的一环。


无论技术栈怎么变,我们作为前端工程师的终极使命——为用户创造流畅、可靠、易用的界面体验——是永远不会变的。


一个后端思维主导的全栈工程师,他可能会更关心数据库的范式、API的性能。而一个前端专家,他的核心竞争力,应该体现在对用户体验的全方位把控上:交互的细节、动画的流畅度、加载的性能、操作的便捷性、视觉的保真度、以及对所有人群都友好的无障碍设计。


把用户体验这块阵地守住,并做到极致,就是我们最坚固的护城河。




前端不会死,但只会写UI的前端会淘汰


所以,我现在不再为全栈的趋势而焦虑了。


我把它看作是一次行业洗牌。它淘汰的,不是前端这个岗位,而是那些知识面狭窄、满足于用UI框架拖拖拽拽的UI实现者。


关于这一点你们怎么看🙂


作者:ErpanOmer
来源:juejin.cn/post/7532777221197840399
收起阅读 »

10分钟复刻爆火「死了么」App:vibe coding 实战(Expo+Supabase+MCP)

视频链接:10分钟复刻爆火「死了么」App:vibe coding 实战 仓库地址:github.com/minorcell/s… 最近“死了么”App 突然爆火:内容极简——签到 + 把紧急联系人邮箱填进去。 它的产品形态很轻,但闭环很完整: 你每天打卡...
继续阅读 »

视频链接:10分钟复刻爆火「死了么」App:vibe coding 实战


仓库地址:github.com/minorcell/s…



202602


最近“死了么”App 突然爆火:内容极简——签到 + 把紧急联系人邮箱填进去
它的产品形态很轻,但闭环很完整:
你每天打卡即可;如果你连续两天没打,系统就给紧急联系人发邮件。


恰好我最近在做 Supabase 相关调研,就顺手把它当成一次“极限验证”:



  • 我想看看:Expo + Supabase 能不能把后端彻底“抹掉”

  • 我也想看看:Codex + MCP 能不能把“建表 / 配置 / 写代码”这整套流程进一步压缩

  • 以及:vibe coding 到底能不能真的做到:跑起来、能用、闭环通


结论是:能。并且我录了全过程,从建仓库到 App 跑起来能用,全程 10 分钟


我复刻的目标:只保留“核心闭环”


我没打算做一个完整产品,只做最小闭环:



  1. 用户注册 / 登录(邮箱 + 密码 + 邮箱验证码)

  2. 首页打卡:每天只能打一次,展示“连续打卡 xx 天”

  3. 我的:查看打卡记录 / 连续天数

  4. 紧急联系人:设置一个邮箱

  5. 连续两天没打卡就发邮件(定时任务 + 邮件发送)


页面风格:简约、有活力(但不追求 UI 细节)。


技术栈:把“后端”交给 Supabase,把“体力活”交给 Agent



  • 前端:React Native + Expo(TypeScript)

  • 后端:Supabase(Auth + Postgres + RLS)

  • 自动化:Supabase Cron + Edge Functions
    Supabase 的定时任务本质是 pg_cron,可以跑 SQL / 调函数 / 发 HTTP 请求(包括调用 Edge Function)。(Supabase)

  • Agent:Codex(通过 Supabase MCP 直接连 Supabase)
    Supabase 官方有 MCP 指南,并且强调了安全最佳实践(比如 scope、权限、避免误操作)。(Supabase)


我整个过程的体验是:



以前你要在“前端 / SQL / 控制台 / 文档”之间来回切。
现在你只需要把需求写清楚,然后盯着它干活,偶尔接管一下关键配置。



两天没打卡发邮件:用 Cron + Edge Function,把事情做完


这是这个 App 最关键的“闭环”。


方案:每天跑一次定时任务



  • Cron:每天固定时间跑(比如 UTC 00:10)

  • 任务内容:找出“已经两天没打卡”的用户

  • 动作:调用 Edge Function 发邮件


Supabase 官方文档推荐的组合是:pg_cron + pg_net,定时调用 Edge Functions。(Supabase)



你也可以不调用 Edge Function,直接让 Cron 发 HTTP webhook 给你自己的服务。
但既然目标是“不写后端”,那就让 Edge Function 处理就行。



Edge Function:负责“发邮件”


注意:Supabase Auth 的邮件(验证码)是它自己的系统邮件;
你要给紧急联系人发提醒,通常需要接第三方邮件服务(Resend / SendGrid / Mailgun / SES 之类)。


Supabase 文档里也提到:定时调用函数时,敏感 token 建议放到 Supabase Vault 里。(Supabase)


Edge Function(伪代码示意):


// 1) 查数据库:哪些人超过 2 天没打卡
// 2) 取紧急联系人邮箱
// 3) 调用邮件服务 API 发送提醒

Cron 每天跑一次就够了:
这个产品的语义不是“立刻报警”,而是“连续两天都没动静”。


MCP + Codex:我觉得最爽的地方


如果你只看结果,你会觉得“这不就是一个 CRUD App 吗”。


但我觉得真正有意思的是过程:



  • 它不仅写前端代码

  • 它还能“像个人一样”去把 Supabase 后台的事情做掉:建表、加约束、开 RLS、写策略、甚至提示你哪里要手动补配置


而 Supabase MCP 的官方定位,就是让模型通过标准化工具安全地操作你的 Supabase 项目(并且强调先读安全最佳实践)。(Supabase)


我这次几乎没写代码,最大的精力消耗其实是两件事:



  1. 把提示词写清楚(尤其是“规则”和“边界条件”)

  2. 对关键点做人工复核(RLS、唯一约束、邮件配置)


我现在会怎么写提示词


我发现 vibe coding 成功率最高的提示词,不insane,反而“啰嗦”:



  • 先写“模块和流程”

  • 再写“数据约束”(每天只能一次、断档怎么处理)

  • 再写“安全策略”(RLS 怎么开)

  • 最后写“验收标准”(做到什么算跑通)


你给得越具体,它越像一个靠谱同事;
你给得越模糊,它越容易“自作主张”。


附录


我这次用的提示词(原文)


需求:使用expo和supabase开发一个移动端APP: 死了么

## 功能:

### 用户注册:

1. 描述:在app进入页面,用户需要输入邮箱和密码以及确认密码,进行注册。
2. 流程:
- 使用supabase的auth进行校验,发送验证码注册邮箱到用户邮箱,用户需要在页面输入邮箱中的验证码。
- 注册成功之后即可进入app首页

### 首页打卡:

1. 描述:用户进入首页,只有一个大大的打卡功能;“今日活着”,点击即可完成打卡功能
2. 流程:
- supabase需要记录用户的打卡信息
- 打开成功时,提示用户已经“你已连续打卡xx日,又活了一天”

### “我的”

1. 用户可以在“我的”页面查看自己的打卡记录,连续打卡时间
2. 用户可以设置紧急联系人,当检测到用户连续两天没有打卡时,会发送一封紧急联系的邮件到紧急联系人邮箱

## 其他:

1. 用户每天只能打卡一次
2. 页面简约、有活力

> 你可以使用supabase的mcp进行所有的操作,

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

H5唤醒APP技术方案入门级介绍

web
内容大纲 什么是H5唤醒App “唤醒 App”指的是: 🐔🏀 从「另一个应用 / 系统环境」跳转并打开「你本地已安装的 App」 唤醒 App = 跨应用启动 典型来源端(“从哪来”) 🐔 浏览器(Safari / Chrome / 系统浏览器)...
继续阅读 »

内容大纲


image.png




什么是H5唤醒App


“唤醒 App”指的是:


🐔🏀 从「另一个应用 / 系统环境」跳转并打开「你本地已安装的 App」


唤醒 App = 跨应用启动



典型来源端(“从哪来”)



  • 🐔 浏览器(Safari / Chrome / 系统浏览器)

  • 🏀 微信 / QQ / 钉钉 / 支付宝

  • 🐔 其他第三方 App

  • 🏀 短信 / 邮件

  • 🐔󠁧󠁢󠁥󠁮󠁧󠁿 推送通知

  • 🎤 二维码




目标端(“到哪去”)



  • 🐉 你已经安装在手机里的原生 App

  • 并且:

  • 启动 App

  • 还能跳到 指定页面






唤醒 App 的技术方案


deep link

在讲具体的技术选型方案之前


我们先要说什么是 deep link(唤端技术的本质)




deep link 本质上不是“打开 App” ,而是“让操作系统把一次跳转请求路由给某个 App 处理



  • 浏览器 / 微信 / 系统 并不是“主动打开 App”

  • 而是 把一个“链接”交给系统

  • 系统再决定:



    • 1.有没有 App 能处理?

    • 2.交给谁?

    • 3.怎么交?




所以 deep link 是系统能力,不是 JS 技巧。




为什么会有这么多种唤醒方案?


  • 1.iOS 和 Android 的系统模型不同

  • 2.安全策略不同

  • 3.浏览器、微信等容器又各自加了一层限制


于是结果就是:



“同一个目标(打开 App),在不同系统上只能用不同的入口”





这也是为什么你看到的主流方案是这三类



  • 1.URL Scheme(最原始)

  • 2.Universal Link(iOS 官方)

  • 3.App Link / Chrome Intents(Android 官方)




方案1.URL Scheme

在关于H5混合开发的通信中,我们就已经介绍了URL Scheme是JS bridge通信方式的一种


它的使用场景并不局限于“唤醒 App”,而是更广义的:





👉 通过一个特定格式的 URL,让系统或原生拦截并执行对应逻辑



一个典型的 URL Scheme 长这样:


myapp://page/detail?id=123

其中:



  • myapp:协议名(Scheme)

  • page/detail:业务路径

  • id=123:参数


对浏览器来说,它并不关心这个 URL 是否“合法”, 它唯一做的事是:把这个 URL 交给操作系统处理。


Scheme 方案唤醒app能生效的前提是:App 必须提前向系统注册这个协议名




在 App 安装阶段



  • iOS / Android 会在系统层记录

  • “某个 App 能够处理哪些 Scheme


系统会维护一张映射关系:


Scheme(协议名) → App

一旦这个映射存在,系统就具备了“路由能力”。




当系统再次遇到相同 Scheme 的 URL 时,流程会变成



URL → 操作系统 → 查找注册关系 → 启动对应 App → 传递参数



整个过程发生在 系统层面,与 H5 是否运行在 WebView、是否使用 JS Bridge 本身并没有直接关系。


Safari → App 为例


Safari 点击链接

系统识别这是 Universal Link / Scheme

系统查找有没有 App 声明能处理

有 → 启动 App(cold / warm)

把参数交给 App




H5侧实现

① 通过 window.location.href 跳转

这是最直接、最直观的一种方式:


window.location.href = 'zhihu://'

它的行为非常明确:



  • 1.当前页面发起一次 URL 跳转

  • 2.浏览器发现这是一个非 http(s) 协议

  • 3.将该 URL 交给操作系统处理


早期移动浏览器系统浏览器中,这种方式成功率较高,也是最常见的实现。


但它的问题也很明显:



  • 1.会破坏当前页面状态

  • 2.在强管控容器(如微信)中通常会被直接拦截

  • 3.无法判断 App 是否已安装




② 通过隐藏 iframe 触发跳转

这种方式曾经被广泛用于 “无刷新唤醒” 的场景:


const iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = 'zhihu://'
document.body.appendChild(iframe)

其原理是:



  • 1.利用 iframe 加载资源的行为

  • 2.间接触发 Scheme

  • 3.避免页面发生整体跳转


在一段时间内,这种方式被认为是:



location.href 更“温和”的唤醒方式



但随着浏览器和容器安全策略的收紧:



  • iframe 加载非标准协议被限制

  • 微信、QQ 等环境几乎完全失效


目前这类方式更多只存在于历史代码或兼容逻辑中




③ 通过 <a> 标签跳转

这是最“标准 HTML”的方式:


<a href="zhihu://">打开知乎 App</a>

它的特点是:



  • 1.依赖用户真实点击

  • 2.符合浏览器的交互安全模型

  • 3.成功率通常高于自动跳转


在部分环境中:



“用户点击触发” 本身就是是否允许唤醒的重要判断条件



因此,<a> 标签在某些浏览器中的表现,反而比 JS 自动跳转更稳定。




④ 通过 JS Bridge 由原生侧发起

在 App 内 WebView 场景下,最稳定的方式其实是:


window.miduBridge.call('openAppByRouter', {
url: 'zhihu://'
})

这种方式的本质是:



  • 1.H5 并不直接触发 Scheme

  • 2.而是通过 JS Bridge 通知原生

  • 3.由 原生代码主动发起跳转




这也是 混合开发中最推荐的做法,因为:



  • 1.不受浏览器安全策略影响

  • 2.成功率最高

  • 3.可完全由 App 控制兜底逻辑





实际开发问题

在实际开发中,一个非常现实的问题是:



H5 发起 Scheme 跳转后,如何判断 App 是否真的被成功唤起?





但是事实上是对于 URL Scheme 这种系统级跳转机制 来说:



前端并不存在一个“可靠、官方、100% 准确”的判断方式



这是由 Scheme 的实现机制本身决定的。


为什么前端无法直接判断?

当 H5 触发 Scheme 跳转后:



  • 1.浏览器将 URL 交给操作系统

  • 2.系统尝试查找是否存在可处理该 Scheme 的 App

  • 3.如果存在,则直接拉起 App




这个过程发生在:



浏览器 → 操作系统 → App



而 H5 所处的位置是:



浏览器沙箱内



浏览器不会告诉 H5:



  • 1.是否找到了 App

  • 2.是否成功启动

  • 3.是否被系统或容器拦截


因此,H5 无法拿到任何明确的成功 / 失败回调




目前的主流方案是【推测】

方式一:页面可见性变化(最常用)


let hidden = false

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
hidden = true
}
})

setTimeout(() => {
if (!hidden) {
// 大概率唤起失败
}
}, 1500)


原理是:



  • 1.App 被拉起时

  • 2.浏览器页面会进入后台

  • 3.触发 visibilitychange


如果页面始终未进入隐藏状态,大概率唤醒失败


! 注意:

这是“概率判断”,不是绝对结论。




方式二:定时器兜底跳转


location.href = 'zhihu://'

setTimeout(() => {
location.href = 'https://appstore.xxx.com'
}, 2000)

逻辑是:



  • 1.尝试唤醒 App

  • 2.如果 2 秒内页面未被中断

  • 3.认为 App 未安装或唤醒失败

  • 4.自动跳转下载页


这是最常见的商业实现方式。\





以上方法均不可靠


因为它们都依赖于一个前提:



“App 被唤起,一定会导致页面进入后台”



但现实中:



  • 系统弹窗

  • 权限确认

  • 容器拦截

  • 多任务切换


都会导致误判。


所以结论非常明确:



Scheme 的唤醒结果,只能“推测”,不能“确认”





不过第 ④ 种方式,其实是一个例外。


window.miduBridge.call('openAppByRouter', { url: 'zhihu://' })


因为这一步是:



由原生主动发起跳转



所以:



  • 原生知道自己是否成功处理了跳转

  • 可以通过 JS Bridge 回调结果给 H5


window.miduBridge.call(
'openAppByRouter',
{ url: 'zhihu://' },
(result) => {
if (result.success) {
// 唤起成功
} else {
// 唤起失败
}
}
)



  • 纯 H5 + Scheme



    • 无法准确判断唤醒是否成功

    • 只能通过行为推测



  • JS Bridge + 原生发起



    • 可以获得明确结果

    • 成功率与可控性最高




也正是这个差异,导致了今天的现实:


Scheme 更适合作为“兜底工具”,而不是主方案




scheme方案的其他缺点

除了前面提到的 安全性差、用户体验不佳、无法准确判断唤起结果 外,URL Scheme 还有几个现实工程中必须考虑的缺点:




① 协议名可能被重复注册或占用



  • 1.URL Scheme 依赖的是 协议名(如 myapp:// 来标识 App

  • 2.系统层面并没有强制保证唯一性

  • 3.如果不同 App 注册了相同协议名:



    • 用户点击 Scheme 时,系统可能唤醒错误的 App

    • 导致业务逻辑混乱,甚至产生安全隐患






② 部分 App 或容器主动屏蔽



  • 微信、QQ、支付宝等强管控容器对 Scheme 跳转有严格限制

  • 常见表现:



    • 1.自动跳转失效

    • 2.iframe / location.href 被直接拦截

    • 3.用户点击 <a> 标签也可能无法唤醒



  • 原因:



    • 1.防止恶意跳转、劫持安装流

    • 2.控制容器内的用户体验





换句话说,即便你的协议名注册正确,Scheme 在这些环境下往往失效





③ 无统一管理和安全约束



  • 1.URL Scheme 本身没有域名验证或证书绑定机制

  • 2.任何 App 都可以注册

  • 3.没有办法验证调用者或跳转来源

  • 4.容易被用作“恶意唤醒”或劫持入口


  •   <br>





方案2.Universal Link / App Link

随着 URL Scheme 的局限性暴露出来:



  • 1.协议名可能冲突

  • 2.容器或浏览器屏蔽

  • 3.无法安全验证来源




Apple 和 Google 分别提出了官方解决方案



  • iOS → Universal Link

  • Android → App Link / Chrome Intents


它们的核心理念很一致:



通过 HTTPS 链接 + 系统校验,让 App 唤醒更安全、更可靠





2.1 Universal Link(iOS)



Universal Link 是 iOS 9 之后新增的功能,它允许开发者 直接通过 HTTPS 链接唤醒 App


相比 URL Scheme,它有几个明显优势:



  1. 自然降级:如果 App 没有安装,点击链接会直接打开网页,无需前端判断唤起是否成功。

  2. 用户体验更好:不会弹出“是否打开 App”的确认框,唤端效率更高。

  3. 安全可靠:链接必须绑定到 App 的域名,避免协议名冲突或被劫持。




核心原理


Universal Link 的实现原理可以概括为两步:



  1. 1.App 注册域名



    • 在 iOS 项目中,需要声明 App 支持的域名

    • 系统通过这个绑定来识别哪些链接可以交给 App 处理。



  2. 2.域名配置 apple-app-site-association 文件



    • 在对应域名的根目录下放置 apple-app-site-association 文件,声明 App 支持哪些路径。

    • 当用户点击该域名的链接时,iOS 会检查该文件,并判断 App 是否可以处理。

    • 如果 App 安装了,就直接唤起;否则,打开网页







对前端同学来说,不需要关注文件的具体配置,只需与 iOS 同学确认好支持的域名即可。






  • 系统在点击链接时,会偷偷做三件事:



    1. 1.验证域名是否和 App 绑定(Apple 服务器文件 + App 配置)

    2. 2.检查 App 是否已安装

    3. 3.匹配 App 内路由,如果符合则直接唤起 App 指定页面



  • 未安装 App,则自然打开网页页面,不会报错或失效


  •   <br>



相对于 URL Scheme,Universal Link 的优势非常明显:



  1. 1.无弹窗提示



    • 唤端时不会弹出“是否打开 App”的确认框

    • 用户体验更顺畅,可以减少用户流失



  2. 2.自然降级能力



    • 无需关心用户是否安装 App

    • 对于未安装 App 的用户,点击链接会直接打开对应网页

    • 这也解决了 URL Scheme 无法准确判断唤端失败的问题



  3. 3.平台限制



    • Universal Link 目前只能在 iOS 系统使用

    • Android 需要使用 App Link 或 Chrome Intents



  4. 4.用户触发要求



    • 必须由用户主动点击触发

    • 自动跳转、iframe 触发等方式无法保证唤起成功








H5侧代码


在 H5 页面中,触发 Universal Link 非常简单,就像普通的网页链接一样


function openByUniversal() {
// 打开知乎问题页
window.location.href = 'https://oia.zhihu.com/questions/64966868';
}

或者使用 <a> 标签:


<a href="https://oia.zhihu.com/questions/64966868">打开 App</a>

特点:



  • 1.与普通网页跳转一致,前端不需要做额外判断

  • 2.如果 App 安装了,系统会直接拉起 App 并跳转到对应页面

  • 3.如果 App 未安装,则打开网页,兜底自然



🔹 对前端同学来说,Universal Link 的操作非常简单,不需要关心底层配置,只需确认域名和路径由 iOS 同学支持即可。





⚠️ 但是它在 iOS 容器中仍然有限制:



  • 微信、QQ 等仍然可能拦截

  • 因为容器本身不允许把链接交给系统




2.2 App Link / Chrome Intents(Android)

Android 的解决方案和 iOS 类似,但实现上更“开放”:



  • 1.App Link:和 Universal Link 一样,通过 HTTPS + 域名校验来保证安全

  • 2.Chrome Intents:允许开发者直接指定 包名 + Scheme + 路由,用于兜底或精确跳转


示例:


https://www.example.com/product/123

或者使用 Intent:


intent://product/123#Intent;scheme=myapp;package=com.example.app;end


  • 系统会检查 App 是否安装

  • 安装则唤起指定页面

  • 未安装则跳转应用商店




H5 侧触发方式


①通过普通 HTTPS 链接触发 App Link


function openByAppLink() {
// 打开商品详情页
window.location.href = 'https://www.example.com/product/123';
}

或者直接用 <a> 标签:


<a href="https://www.example.com/product/123">打开 App</a>

原理:



  • 1.系统检测链接对应域名是否绑定 App

  • 2.App 安装了 → 唤起并跳转指定页面

  • 3.App 未安装 → 自动打开网页,兜底自然




② 通过 Intent URL 触发 Chrome Intents


function openByIntent() {
window.location.href = 'intent://product/123#Intent;scheme=myapp;package=com.example.app;end';
}

特点:



  • 1.可以指定 App 包名和 Scheme

  • 2.App 安装 → 唤起指定页面

  • 3.App 未安装 → 跳转应用商店,确保用户可获取 App




2.3 相比 Scheme 的优势

优势说明
安全域名验证避免被劫持或重复注册
成功率高系统直接控制唤醒流程
可自然降级App 未安装时自动跳网页或应用商店
用户体验好不弹确认框,跳转顺畅



2.4 需要注意的点


  • 1.Universal Link / App Link 仍然会被部分 容器拦截 (尤其是微信)

  • 2.域名和 App 的绑定必须在 服务端 + App 配置 同步

  • 3.Android 上不同浏览器行为可能略有差异,需要在测试时覆盖主流浏览器




方案3:微信环境下的唤醒方案

微信环境下的 H5 唤醒 App,和普通浏览器相比有几个显著特点



  1. 1.绝大部分 Scheme 被拦截



    • 无论是 location.href、iframe 还是 <a> 标签

    • 微信会直接阻止跳转,防止外部 App 劫持



  2. 2.Universal Link / App Link 成功率有限



    • iOS 的 Universal Link 在微信里也可能被拦截

    • Android 的 App Link / Chrome Intents 在微信内同样可能无效





🔹 也就是说,在微信环境下,“传统唤端方案”几乎失效。





3.1可行方案

① 通过 跳转到 App Store / 应用商店


  • 对于未安装 App 的用户,是最安全、最通用的兜底方案

  • 缺点:用户必须手动下载,体验不如直接唤端


window.location.href = 'https://apps.apple.com/cn/app/idxxxxxx';



② 使用 中转页 / 提示页


  • 先打开一个中转 H5 页面(WebView 或浏览器打开),提示用户点击按钮唤醒 App

  • 按钮可以触发 Scheme 或 Universal Link

  • 优势:



    • 1.提示用户手动操作,提高唤醒成功率

    • 2.可以结合埋点统计唤醒行为



  • 缺点:



    • 额外增加一个页面,增加跳转成本




H5侧


<!-- 中转提示页 -->
<button id="openAppBtn">打开 App</button>

<script>
document.getElementById('openAppBtn').addEventListener('click', function() {
// 方式 1:使用 URL Scheme(兜底方案)
window.location.href = 'myapp://page/detail?id=123';

// 方式 2:使用 Universal Link(iOS)
// window.location.href = 'https://www.example.com/page/detail?id=123';

// 可选:2 秒后兜底到应用商店
setTimeout(() => {
window.location.href = 'https://apps.apple.com/cn/app/idxxxxxx'; // iOS 应用商店
// 或 Android 下载链接
}, 2000);
});
</script>



特点:



  • 1.必须用户点击才能触发

  • 2.可以结合 setTimeout 兜底下载

  • 3.可以在按钮点击时触发埋点统计唤醒成功率




③ 小程序或企业号协作


  • 对于企业内部或自家 App:



    • 可以通过 小程序 / 企业微信接口 调起 App

    • 优点:成功率高,可控

    • 缺点:仅限特定生态






H5 侧示例(假设使用企业微信 JS-SDK)


<button id="openAppBtn">打开 App</button>

<script>
// 假设已经引入企业微信 JS-SDK 并完成 config
document.getElementById('openAppBtn').addEventListener('click', function() {
if (window.wx && wx.invoke) {
wx.invoke('openEnterpriseChat', { // 示例接口
useridlist: 'user_id',
chatType: 1
}, function(res) {
if(res.err_msg == "openEnterpriseChat:ok") {
console.log('App 唤起成功');
} else {
console.log('唤起失败,兜底逻辑');
window.location.href = 'https://apps.apple.com/cn/app/idxxxxxx';
}
});
}
});
</script>




特点:



  • 1.成功率高,原生接口可明确回调

  • 2.适合企业内部 / 自家生态

  • 3.不适用于普通微信用户




④ 微信开放标签 <wx-open-launch-app>(Android)

微信为了改善 Android H5 唤醒体验,提供了 开放标签 wx-open-launch-app,可以让前端 H5 直接在微信里唤醒 App


使用示例


<wx-open-launch-app
appid="wx123" <!-- 你注册的 App ID -->
extinfo="page=home&id=123"> <!-- 透传参数,可在 App 内使用 -->

<script type="text/wxtag-template">
<button>打开 App</button>
</script>

</wx-open-launch-app>

原理:



  • 1.标签本身是微信官方提供的组件

  • 2.内部会调用 微信客户端唤醒 App 的能力

  • 3.可以透传参数给 App,直接跳到指定页面




⚠️ 使用前提



  1. 1.微信认证



    • 公众号或小程序必须经过微信认证



  2. 2.App 在白名单内



    • 需要申请微信开放能力并配置白名单

    • 只有在白名单内的 App 才能被唤醒



  3. 3.仅限微信环境



    • 该标签在普通浏览器或非微信环境下无法使用




特点



  • 1.成功率高:比传统 Scheme / Universal Link 在微信中稳定

  • 2.前端简单:不需要写 JS 复杂逻辑,只需包一层标签即可

  • 3.可透传参数:可直接带参数跳到指定页面


限制



  • 1.仅适用于 Android

  • 2.必须满足认证 + 白名单条件

  • 3.仅能在微信内使用




⑤微信环境下 iOS 唤醒:Universal Link

微信中,前面提到的 URL Schemeiframe 等方式几乎都被拦截,无法自动唤起 App。


iOS 唯一可行且推荐的方案是 Universal Link:



  • 1.用户点击 H5 页面里的 HTTPS 链接

  • 2.iOS 系统检查该域名是否绑定了 App

  • 3.App 已安装 → 直接唤起并跳转指定页面

  • 4.App 未安装 → 打开网页,自然兜底


H5 触发方式


<a href="https://oia.zhihu.com/questions/64966868">打开 App</a>

<script>
function openByUniversal() {
window.location.href = 'https://oia.zhihu.com/questions/64966868';
}
</script>



特点:



  1. 1.成功率最高



    • iOS 系统直接判断是否唤起 App

    • 不受微信容器拦截 Scheme 的影响



  2. 2.用户体验好



    • 不弹出“是否打开 App”的确认框

    • 点击即可直接唤起 App



  3. 3.自然降级



    • App 未安装时,自动打开网页

    • 前端无需额外逻辑判断唤端成功与否




注意:



  • 1.仅适用于 iOS 微信

  • 2.Android 微信仍需中转页或 <wx-open-launch-app> 等方案

  • 3.必须事先和 iOS 同学确认支持的域名和 Universal Link 配置


作者:pinkQQx
来源:juejin.cn/post/7594087108594237503
收起阅读 »

我为什么放弃了“大厂梦”,去了一家“小公司”?

我,前端八年。我的履历上,没有那些能让HR眼前一亮的名字,比如字节、阿里,国内那些头部的互联网公司。 “每个程序员都有一个大厂梦”,这句话我听了八年。说实话,我也有过,而且非常强烈。 刚毕业那几年,我把进大厂当作唯一的目标。我刷过算法题,背过“八股文”,也曾一...
继续阅读 »

我,前端八年。我的履历上,没有那些能让HR眼前一亮的名字,比如字节、阿里,国内那些头部的互联网公司。


“每个程序员都有一个大厂梦”,这句话我听了八年。说实话,我也有过,而且非常强烈。


刚毕业那几年,我把进大厂当作唯一的目标。我刷过算法题,背过“八股文”,也曾一次次地在面试中被刷下来。那种“求之不得”的滋味,相信很多人都体会过。


但今天,我想聊的是,我是如何从一开始的“执念”,到后来的“审视”,再到现在的“坦然”,并最终心甘情愿地在一家小公司里,找到了属于我自己的价值。


这是一个普通的、三十多岁的工程师,与自己和解的经历。




那段“求之不得”的日子


我还记得大概四五年前,是我冲击大厂最疯狂的时候。


市面上所有关于React底层原理、V8引擎、事件循环的面经,我都能倒背如流。我把LeetCode热题前100道刷了两遍,看到“数组”、“链表”这些词,脑子里就能自动冒出“双指针”、“哈希表”这些解法。


我信心满满地投简历,然后参加了一轮又一轮的面试。


结果呢?大部分都是在三轮、四轮之后,收到一句“感谢您的参与,我们后续会保持联系”。我一次次地复盘,是我哪里没答好?是项目经验不够亮眼?还是算法题的最优解没写出来?


那种感觉很糟糕。你会陷入一种深深的自我怀疑,觉得自己的能力是不是有问题,是不是自己“不配”进入那个“高手如云”的世界。




开始问自己:“大厂”真的是唯一的出路吗?


在经历了一段密集而失败的面试后,我累了,也开始冷静下来思考。


我观察身边那些成功进入大厂的朋友。他们确实有很高的薪水和很好的福利,但他们也常常在半夜的朋友圈里,吐槽着无休止的会议、复杂的流程、以及自己只是庞大系统里一颗“螺丝钉”的无力感。


我看到他们为了一个需求,要跟七八个不同部门的人“对齐”;看到他们写的代码,90%都是在维护内部庞大而陈旧的系统;看到他们即使想做一个小小的技术改进,也要经过层层审批。


我突然问自己:这真的是我想要的生活吗?我想要的是什么?


当我把这些想清楚之后,我发现,大厂的光环,对我来说,好像没那么耀眼了。




在“小公司”,找到了意想不到的“宝藏”


后来,我加入了一家规模不大的科技公司。在这里,我确实找到了我想要的东西。


成了一个“产品工程师”,而不仅仅是“前端工程师”


在小公司,边界是模糊的。


我不仅要写前端代码,有时候也得用Node.js写一点中间层。我需要自己去研究CI/CD,把自动化部署的流程跑起来。我甚至需要直接跟客户沟通,去理解他们最原始的需求。


这个过程很“野”,也很累,但我的成长是全方位的。我不再只关心页面好不好看,我开始关心整个产品的逻辑、服务器的成本、用户的留存。我的视野被强制性地拉高了。


“影响力”被无限放大


在这里,我就是前端的负责人。


用Vue还是React?用Tailwind CSS还是CSS Modules?这些技术决策,我能够和老板、和团队一起讨论,并最终拍板。我们建立的每一个前端规范,写的每一个公共组件,都会立刻成为整个团队的标准。


这种“规则制定者”的身份,和在大厂当一个“规则遵守者”,是完全不同的体验。你能清晰地看到自己的每一个决定,都对产品和团队产生了直接而深远的影响。


离“价值”更近了


最重要的一点是,我能非常直接地感受到自己工作的价值。


我花一周时间开发的新功能上线后,第二天就能从运营同事那里拿到用户的反馈数据。我知道用户喜不喜欢它,它有没有帮助公司赚到钱。这种即时的、正向的反馈,比任何KPI或者年终奖金,更能给我带来成就感。




还会羡慕那些在大厂的朋友吗?


当然会。我羡慕他们优厚的薪酬福利,羡慕他们能参与到改变数亿人的项目中去。


但我不再因此而焦虑,也不再因此而自我否定。


你可以多想一想你真正想要的是什么? 一个公司的名字,并不能定义你作为一名工程师的价值。你的价值,体现在你写的代码里,体现在你解决的问题里,也有可能体现在你创造的产品里。


找到一个能让你发光发热的地方,比挤进一个让你黯淡无光的地方,重要得多。


分享完毕。谢谢大家🙂


作者:ErpanOmer
来源:juejin.cn/post/7525011608366579758
收起阅读 »

别搞混了!MCP 和 Agent Skill 到底有什么区别?

MCP 与 Skill 深度对比:AI Agent 的两种扩展哲学 用 AI Agent 工具(Claude Code、Cursor、Windsurf 等)的时候,经常会遇到两个概念: MCP(Model Context Protocol) Skill(Ag...
继续阅读 »

MCP 与 Skill 深度对比:AI Agent 的两种扩展哲学


用 AI Agent 工具(Claude Code、Cursor、Windsurf 等)的时候,经常会遇到两个概念:



  • MCP(Model Context Protocol)

  • Skill(Agent Skill)


它们看起来都是"扩展 AI 能力"的方式,但具体有什么区别?为什么需要两套机制?什么时候该用哪个?


这篇文章会从设计哲学、技术架构、使用场景三个维度,把这两个概念彻底讲清楚。


一句话区分


先给个简单的定位:



MCP 解决"连接"问题:让 AI 能访问外部世界
Skill 解决"方法论"问题:教 AI 怎么做某类任务



用 Anthropic 官方的说法:



"MCP connects Claude to external services and data sources. Skills provide procedural knowledge—instructions for how to complete specific tasks or workflows."



打个比方:MCP 是 AI 的"手"(能触碰外部世界),Skill 是 AI 的"技能书"(知道怎么做某件事)。


你需要两者配合:MCP 让 AI 能连接数据库,Skill 教 AI 怎么分析查询结果。


MCP:AI 应用的 USB-C 接口


MCP 是什么


MCP(Model Context Protocol)是 Anthropic 在 2024 年 11 月发布的开源协议,用于标准化 AI 应用与外部系统的交互方式。


官方的比喻是"AI 应用的 USB-C 接口"——就像 USB-C 提供了一种通用的方式连接各种设备,MCP 提供了一种通用的方式连接各种工具和数据源。


关键点:MCP 不是 Claude 专属的。


它是一个开放协议,理论上任何 AI 应用都可以实现。截至 2025 年初,已经被多个平台采用:



  • Anthropic: Claude Desktop、Claude Code

  • OpenAI: ChatGPT、Agents SDK、Responses API

  • Google: Gemini SDK

  • Microsoft: Azure AI Services

  • 开发工具: Zed、Replit、Codeium、Sourcegraph


到 2025 年 2 月,已经有超过 1000 个开源 MCP 连接器。


MCP 的架构


MCP 基于 JSON-RPC 2.0 协议,采用客户端-主机-服务器(Client-Host-Server)架构:


┌─────────────────────────────────────────────────────────┐
│ Host │
│ (Claude Desktop / Cursor) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Client │ │ Client │ │ Client │ │
│ │ (GitHub) │ │ (Postgres) │ │ (Sentry) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│MCP Server │ │MCP Server │ │MCP Server │
│ (GitHub) │ │(Postgres) │ │ (Sentry) │
└───────────┘ └───────────┘ └───────────┘


  • Host:用户直接交互的应用(Claude Desktop、Cursor、Windsurf)

  • Client:Host 应用中管理与特定 Server 通信的组件

  • Server:连接外部系统的桥梁(数据库、API、本地文件等)


MCP 的三个核心原语


MCP 定义了三种 Server 可以暴露的原语:


1. Tools(工具)—— 模型控制

可执行的函数,AI 可以调用来执行操作。


{
"name": "query_database",
"description": "Execute SQL query on the database",
"parameters": {
"type": "object",
"properties": {
"sql": { "type": "string" }
}
}
}

AI 决定什么时候调用这些工具。比如用户问"这个月的收入是多少",AI 判断需要查数据库,就会调用 query_database 工具。


2. Resources(资源)—— 应用控制

数据源,为 AI 提供上下文信息。


{
"uri": "file:///Users/project/README.md",
"name": "Project README",
"mimeType": "text/markdown"
}

资源由应用控制何时加载。用户可以通过 @ 引用资源,类似于引用文件。


3. Prompts(提示)—— 用户控制

预定义的提示模板,帮助结构化与 AI 的交互。


{
"name": "code_review",
"description": "Review code for bugs and security issues",
"arguments": [
{ "name": "code", "required": true }
]
}

用户显式触发这些提示,类似于 Slash Command。


MCP 与 Function Calling 的关系


很多人会问:MCP 和 OpenAI 的 Function Calling、Anthropic 的 Tool Use 有什么区别?


Function Calling 是 LLM 的能力——把自然语言转换成结构化的函数调用请求。LLM 本身不执行函数,只是告诉你"应该调用什么函数,参数是什么"。


MCP 是在 Function Calling 之上的协议层——它标准化了"函数在哪里、怎么调用、怎么发现"。


两者的关系:


用户输入 → LLM (Function Calling) → "需要调用 query_database"

MCP Protocol

MCP Server 执行

返回结果给 LLM

Function Calling 解决"决定做什么",MCP 解决"怎么做到"。


MCP 的传输方式


MCP 支持两种主要的传输方式:


传输方式适用场景说明
Stdio本地进程Server 在本地机器运行,适合需要系统级访问的工具
HTTP/SSE远程服务Server 在远程运行,适合云服务(GitHub、Sentry、Notion)

大部分云服务用 HTTP,本地脚本和自定义工具用 Stdio。


MCP 的代价


MCP 不是免费的午餐,它有明显的成本:


1. Token 消耗大


每个 MCP Server 都会占用上下文空间。每次对话开始,MCP Client 需要告诉 LLM "你有这些工具可用",这些工具定义会消耗大量 Token。


连接多个 MCP Server 后,光是工具定义可能就占用了上下文窗口的很大一部分。社区观察到:



"We're seeing a lot of MCP developers even at enterprise build MCP servers that expose way too much, consuming the entire context window and leading to hallucination."



2. 需要维护连接


MCP Server 是持久连接的外部进程。Server 挂了、网络断了、认证过期了,都会影响 AI 的能力。


3. 安全风险


Anthropic 官方警告:



"Use third party MCP servers at your own risk - Anthropic has not verified the correctness or security of all these servers."



特别是能获取外部内容的 MCP Server(比如网页抓取),可能带来 prompt injection 风险。


MCP 的价值


尽管有这些代价,MCP 的价值在于标准化和可复用性



  • 一次实现,到处使用:同一个 GitHub MCP Server 可以在 Claude Desktop、Cursor、Windsurf 中使用

  • 动态发现:AI 可以在运行时发现有哪些工具可用,而不是写死在代码里

  • 供应商无关:不依赖特定的 LLM 提供商


Skill:上下文工程的渐进式公开


Skill 是什么


Skill(全称 Agent Skill)是 Anthropic 在 2025 年 10 月发布的特性。官方定义:



"Skills are organized folders of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks."



翻译一下:Skill 是一个文件夹,里面放着指令、脚本和资源,AI 会根据需要自动发现和加载。


Skill 在架构层级上和 MCP 不同。


用 Anthropic 的话说:



"Skills are at the prompt/knowledge layer, whereas MCP is at the integration layer."



Skill 是"提示/知识层",MCP 是"集成层"。两者解决不同层面的问题。


Skill 的核心设计:渐进式信息公开


Skill 最精妙的设计是渐进式信息公开(Progressive Disclosure)。这是 Anthropic 在上下文工程(Context Engineering)领域的重要实践。


官方的比喻:



"Like a well-organized manual that starts with a table of contents, then specific chapters, and finally a detailed appendix."



就像一本组织良好的手册:先看目录,再翻到相关章节,最后查阅附录。


Skill 分三层加载:


flowchart TD
subgraph L1["第 1 层:元数据(始终加载)"]
A[Skill 名称 + 描述]
B["约 100 tokens"]
end

subgraph L2["第 2 层:核心指令(按需加载)"]
C[SKILL.md 完整内容]
D["通常 < 5k tokens"]
end

subgraph L3["第 3+ 层:支持文件(深度按需)"]
E[reference.md]
F[scripts/helper.py]
G[templates/...]
end

L1 --> |"Claude 判断相关"| L2
L2 --> |"需要更多信息"| L3

style L1 fill:#d4edda,stroke:#28a745
style L2 fill:#fff3cd,stroke:#ffc107
style L3 fill:#cce5ff,stroke:#0d6efd

这个设计的好处是什么?


传统方式(比如 MCP)在会话开始时就把所有信息加载到上下文。如果你有 10 个 MCP Server,每个暴露 5 个工具,那就是 50 个工具定义——可能消耗数千甚至上万 Token。


Skill 的渐进式加载让你可以有几十个 Skill,但同时只加载一两个。上下文效率大幅提升。


用官方的话说:



"This means that the amount of context that can be bundled int0 a skill is effectively unbounded."



理论上,单个 Skill 可以包含无限量的知识——因为只有需要的部分才会被加载。


上下文工程:Skill 背后的思想


Skill 是 Anthropic "上下文工程"(Context Engineering)理念的产物。官方对此有专门的阐述:



"At Anthropic, we view context engineering as the natural progression of prompt engineering. Prompt engineering refers to methods for writing and organizing LLM instructions for optimal outcomes. Context engineering refers to the set of strategies for curating and maintaining the optimal set of tokens (information) during LLM inference."



简单说:



  • Prompt Engineering:怎么写好提示词

  • Context Engineering:怎么管理上下文窗口里的信息


LLM 的上下文窗口是有限的(即使是 200k 窗口,也会被大量信息撑爆)。Context Engineering 的核心问题是:在有限的窗口里,放什么信息能让 AI 表现最好?


Skill 的渐进式加载就是 Context Engineering 的具体实践——只加载当前任务需要的信息,让每一个 Token 都发挥最大价值。


Skill 的触发机制


Skill 是自动触发的,这是它和 Slash Command 的关键区别。


工作流程:



  1. 扫描阶段:Claude 读取所有 Skill 的元数据(名称 + 描述)

  2. 匹配阶段:将用户请求与 Skill 描述进行语义匹配

  3. 加载阶段:如果匹配成功,加载完整的 SKILL.md

  4. 执行阶段:按照 Skill 里的指令执行任务,按需加载支持文件


用户不需要显式调用。比如你有一个 code-review Skill,用户说"帮我 review 这段代码",Claude 会自动匹配并加载。


Skill 的本质是什么?


技术上,Skill 是一个元工具(Meta-tool):



"The Skill tool is a meta-tool that manages all skills. Traditional tools like Read, Bash, or Write execute discrete actions and return immediate results. Skills operate differently—rather than performing actions directly, they inject specialized instructions int0 the conversation history and dynamically modify Claude's execution environment."



Skill 不是执行具体动作,而是注入指令到对话历史中,动态修改 Claude 的执行环境。


Skill 的文件结构


一个标准的 Skill 长这样:


my-skill/
├── SKILL.md # 必需:元数据 + 主要指令
├── reference.md # 可选:详细参考文档
├── examples.md # 可选:使用示例
├── scripts/
│ └── helper.py # 可选:可执行脚本
└── templates/
└── template.txt # 可选:模板文件

SKILL.md 是核心,必须包含 YAML 格式的元数据:


---
name: code-review
description: >
Review code for bugs, security issues, and style violations.
Use when asked to review code, check for bugs, or audit PRs.
---

# Code Review Skill

## Instructions

When reviewing code, follow these steps:

1. First check for security vulnerabilities...
2. Then check for performance issues...
3. Finally check for code style...

关键字段:



  • name:Skill 的唯一标识,小写字母 + 数字 + 连字符,最多 64 字符

  • description:描述做什么、什么时候用,最多 1024 字符


description 的质量直接决定 Skill 能不能被正确触发。


Skill 的安全考虑


Skill 有一个潜在的安全问题:Prompt Injection


研究人员发现:



"Although Agent Skills can be a very useful tool, they are fundamentally insecure since they enable trivially simple prompt injections. Researchers demonstrated how to hide malicious instructions in long Agent Skill files and referenced scripts to exfiltrate sensitive data."



因为 Skill 本质上是注入指令,恶意的 Skill 可以在长文件中隐藏恶意指令,窃取敏感数据。


应对措施:



  1. 只使用可信来源的 Skill

  2. 审查 Skill 中的脚本

  3. 使用 allowed-tools 限制 Skill 的能力范围


---
name: safe-file-reader
description: Read and analyze files without making changes
allowed-tools: Read, Grep, Glob # 只允许读操作
---

Skill 的平台支持


Agent Skills 目前支持:



  • Claude.ai(Pro、Max、Team、Enterprise)

  • Claude Code

  • Claude Agent SDK

  • Claude Developer Platform


需要注意的是,Skill 目前是 Anthropic 生态专属的,不像 MCP 是跨平台的开放协议。


MCP vs Skill:架构层级对比


现在我们可以从架构层级来理解两者的区别:


┌─────────────────────────────────────────────────────────┐
│ 用户请求 │
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 提示/知识层 (Skill) │
│ │
│ Skill 注入专业知识和工作流程 │
"怎么做某类任务"
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ LLM 推理层 │
│ │
│ Claude / GPT / Gemini 等 │
│ 理解请求,决定需要什么工具 │
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 集成层 (MCP) │
│ │
│ MCP 连接外部系统 │
"能访问什么工具和数据"
└────────────────────────┬────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 外部世界 │
│ │
│ 数据库、API、文件系统、第三方服务 │
└─────────────────────────────────────────────────────────┘

Skill 在上层(知识层),MCP 在下层(集成层)。


两者不是替代关系,而是互补关系。你可以:



  • 用 MCP 连接 GitHub

  • 用 Skill 教 AI 如何按照团队规范做 Code Review


详细对比表


维度MCPSkill
核心作用连接外部系统编码专业知识和方法论
架构层级集成层提示/知识层
协议基础JSON-RPC 2.0文件系统 + Markdown
跨平台是(开放协议,多平台支持)否(目前 Anthropic 生态专属)
触发方式持久连接,随时可用基于描述的语义匹配,自动触发
Token 消耗高(工具定义持久占用上下文)低(渐进式加载)
外部访问可以直接访问外部系统不能直接访问,需要配合 MCP 或内置工具
复杂度高(需要理解协议、运行 Server)低(写 Markdown 就行)
可复用性高(标准化协议,跨应用复用)中(文件夹,可以 Git 共享)
动态发现是(运行时发现可用工具)是(运行时发现可用 Skill)
安全考虑外部内容带来 prompt injection 风险Skill 文件本身可能包含恶意指令

什么时候用 MCP,什么时候用 Skill


用 MCP 的场景



  • 需要访问外部数据:数据库查询、API 调用、文件系统访问

  • 需要操作外部系统:创建 GitHub Issue、发送 Slack 消息、执行 SQL

  • 需要实时信息:监控系统状态、查看日志、搜索引擎结果

  • 需要跨平台复用:同一个工具在 Claude Desktop、Cursor、其他支持 MCP 的应用中使用


用 Skill 的场景



  • 重复性的工作流程:代码审查、文档生成、数据分析

  • 公司内部规范:代码风格、提交规范、文档格式

  • 需要多步骤的复杂任务:需要详细指导的专业任务

  • 团队共享的最佳实践:标准化的操作流程

  • Token 敏感场景:需要大量知识但不想一直占用上下文


结合使用


很多时候,两者是配合使用的:


用户:"Review PR #456 并按照团队规范给出建议"

1. MCP (GitHub) 获取 PR 信息

2. Skill (团队代码审查规范) 提供审查方法论

3. Claude 按照 Skill 的指令分析代码

4. MCP (GitHub) 提交评论

MCP 负责"能访问什么",Skill 负责"怎么做"。


写好 Skill 的关键


Skill 能不能被正确触发,90% 取决于 description 写得好不好。


差的 description


description: Helps with data

太宽泛,Claude 不知道什么时候该用。


好的 description


description: >
Analyze Excel spreadsheets, generate pivot tables, and create charts.
Use when working with Excel files (.xlsx), spreadsheets, or tabular data analysis.
Triggers on: "analyze spreadsheet", "create pivot table", "Excel chart"

好的 description 应该包含:



  1. 做什么:具体的能力描述

  2. 什么时候用:明确的触发场景

  3. 触发词:用户可能说的关键词


最佳实践


官方建议:



  1. 保持专注:一个 Skill 做一件事,避免宽泛的跨域 Skill

  2. SKILL.md 控制在 500 行以内:太长的话拆分到支持文件

  3. 测试触发行为:确认相关请求能触发,不相关请求不会误触发

  4. 版本控制:记录 Skill 的变更历史


关于 Slash Command


文章标题是 MCP vs Skill,但很多人也会问到 Slash Command,简单说一下。


Slash Command 是最简单的扩展方式——本质上是存储的提示词,用户输入 /命令名 时注入到对话中。


Skill vs Slash Command 的关键区别是触发方式:


Slash CommandSkill
触发方式用户显式输入 /命令Claude 自动匹配
用户控制完全控制何时触发无法控制,Claude 决定

问自己一个问题:用户是否需要显式控制触发时机?



  • 需要 → Slash Command

  • 不需要,希望 AI 自动判断 → Skill


总结


MCP 和 Skill 是 AI Agent 扩展的两种不同哲学:


MCPSkill
哲学连接主义知识打包
问的问题"AI 能访问什么?""AI 知道怎么做什么?"
层级集成层知识层
Token 策略预加载所有能力按需加载知识

记住这句话:



MCP connects AI to data; Skills teach AI what to do with that data.



MCP 让 AI 能"碰到"数据,Skill 教 AI 怎么"处理"数据。


它们不是替代关系,而是互补关系。一个成熟的 AI Agent 系统,两者都需要。


参考资源


MCP 官方资源



Skill 官方资源



跨平台采用



延伸阅读





如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7584057497205817387
收起阅读 »

React + Tailwind CSS 实战:打造一个“会呼吸”的登录页面

web
哈喽,各位掘金的“打工人”们,大家好!👋 还记得咱们上一篇聊过的 Tailwind CSS 入门(在这里详细讲解了如何配置TailwindCss) 吗?当时我们不仅揭开了原子化 CSS 的神秘面纱,还稍微带了一嘴“受控组件”的概念。 今天,咱们不玩虚的,直接实...
继续阅读 »

哈喽,各位掘金的“打工人”们,大家好!👋


还记得咱们上一篇聊过的 Tailwind CSS 入门(在这里详细讲解了如何配置TailwindCss) 吗?当时我们不仅揭开了原子化 CSS 的神秘面纱,还稍微带了一嘴“受控组件”的概念。


今天,咱们不玩虚的,直接实战!🚀


我们要用 React 配合 Tailwind CSS,从零打造一个现代、优雅、且交互细腻的登录页面


别担心,虽然说是“实战”,但我的风格你懂的:轻松愉快,知识硬核。我会把代码掰开了、揉碎了讲给你听,保证你不仅能学会写,还能懂得为什么要这么写。


准备好了吗?系好安全带,老司机要发车了!🚌💨




🎯 我们的目标


我们要做的不是一个死板的 HTML 页面,而是一个有灵魂的 React 组件。它包含:



  1. 响应式布局:手机、平板、电脑通吃。

  2. 优雅的 UI:圆角、阴影、柔和的配色(Tailwind 拿手好戏)。

  3. 极致的交互:聚焦时图标变色、平滑的过渡动画。

  4. React 逻辑:受控组件、状态管理、密码显隐切换。

  5. 图标库:使用 lucide-react 这一当下最火的图标库。


最终效果?就像你每天用的那些大厂 App 一样丝滑。✨




🛠️ 准备工作:兵马未动,粮草先行


首先,确保你的环境里有 React 和 Tailwind CSS。如果你是 Vite 用户,这简直是分分钟的事。


在这个项目中,我们还需要一个特别好用的图标库:lucide-react


npm install lucide-react
# 或者
pnpm add lucide-react

它体积小、图标全、风格统一,绝对是开发利器。




🏗️ 第一步:骨架与画布 —— 布局的艺术


一切从 App.jsx 开始。


我们先看最外层的结构。想象一下,你是个画家,得先铺好画布。


export default function App() {
// ... 逻辑部分稍后讲 ...

return (
// 1. 外层容器:全屏背景,居中布局
<div className="min-h-screen bg-slate-50 flex items-center justify-center p-4">
{/* ... 卡片 ... */}
</div>

)
}

📝 代码详解



  • min-h-screen: 核心! 这让容器的高度至少为屏幕高度(100vh)。如果内容不够多,背景也能铺满全屏;内容多了,它能自动延伸。告别尴尬的“白底漏出”。

  • bg-slate-50: 给背景来点极其淡雅的灰。纯白(#fff)太刺眼,Slate-50 刚刚好,高级感这就来了。

  • flex items-center justify-center: Flexbox 三连。这是最经典的垂直水平居中方案。不管你的屏幕多大,登录框永远稳坐 C 位。

  • p-4: 给四周留点余地,防止在小屏幕手机上内容贴边。




📦 第二步:卡片设计 —— 拟物感的回归


接下来是那个漂浮在屏幕中央的白色卡片。


<div className="relative z-10 w-full max-w-md bg-white rounded-3xl shadow-xl shadow-slate-200/60 border-slate-100 p-8 md:p-10">
{/* ... 内容 ... */}
</div>

📝 代码详解


这里面的学问可大了:



  1. 尺寸控制



    • w-full: 宽度占满父容器(但在 padding 的作用下不会贴边)。

    • max-w-md: 关键限制。在大屏幕上,我们不希望登录框无限拉长,max-w-md (28rem / 448px) 是一个非常舒适的阅读宽度。



  2. 质感营造



    • bg-white: 卡片主体白色。

    • rounded-3xl: 超大圆角!现在流行这种亲和力强的设计,比直角或小圆角更 Modern。

    • shadow-xl shadow-slate-200/60: Tailwind 的黑魔法shadow-xl 给出一个大投影,而 shadow-slate-200/60 则是修改了这个投影的颜色!默认的黑色投影太脏了,用带点蓝紫调的灰色(slate),并且设置透明度(/60),会让卡片看起来像是“悬浮”在空气中,通透感满分。

    • border-slate-100: 极淡的边框,增强边界感,细节决定成败。



  3. 响应式内边距



    • p-8: 默认情况(手机)内边距是 2rem。

    • md:p-10: Mobile First 策略。当屏幕宽度大于 md(768px)时,内边距增加到 2.5rem。大屏大留白,呼吸感就有了。






🧠 第三步:注入灵魂 —— React 状态管理


界面写得再好看,不能动也是白搭。我们要用 React 的 Hooks 来赋予它生命。


import { useState } from 'react';

export default function App() {
// 1. 表单数据状态:单一数据源
const [formData, setFormData] = useState({
email: '',
password: '',
remember: false // 虽然 UI 里没画,但逻辑我们要预留好
});

// 2. UI 交互状态
const [showPassword, setShowPassword] = useState(false); // 密码显隐
const [isLoading, setIsLoading] = useState(false); // 加载中状态

// ...
}

💡 为什么这么设计?


我们没有为 email 和 password 分别创建 state(比如 email, setEmail),而是用一个对象 formData 统一管理。
这样做的好处是:当表单字段变多时(比如注册页有10个空),我们不需要写10个 useState,代码更整洁,扩展性更强。




⚡ 第四步:抽象事件处理 —— 优雅的 handleChange


这是很多新手容易写乱的地方。看仔细了,这一段代码非常通用,建议背诵!


  // 抽象的表单变更处理函数
const handleChange = (e) => {
// 解构出我们需要的信息
// name: 哪个输入框变了?
// value: 变成了什么值?
// type/checked: 专门处理 checkbox
const { name, value, type, checked } = e.target;

// 状态更新
setFormData((prev) => ({
...prev, // 保留之前的其他字段
// 动态属性名:[name]
// 如果是 checkbox 用 checked,否则用 value
[name]: type === 'checkbox' ? checked : value,
}))
}

📝 深度解析



  1. 对象解构const {name, value, ...} = e.target 让代码更清晰。

  2. 函数式更新setFormData((prev) => ...)注意! 永远推荐用这种回调函数的方式更新依赖于旧状态的新状态。这能确保在复杂的异步更新中,你拿到的 prev 永远是最新的。

  3. 计算属性名[name]: ...。ES6 的语法糖,让我们可以用变量 name 作为对象的 key。这意味着这一个函数,可以同时处理 email、password、username 等无数个输入框!这就叫复用




🎨 第五步:表单组件 —— 细节狂魔


接下来是重头戏:输入框。这里我们用到了 Tailwind 极其强大的 grouppeer 特性。


邮箱输入框


<div className="space-y-2">
<label className="text-sm font-medium text-slate-700">Email:</label>

{/* group: 父容器标记 */}
<div className="relative group">

{/* 图标:绝对定位 */}
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-indigo-600 transition-colors">
<Mail size={18} />
</div>

{/* 输入框 */}
<input
type="email"
name="email"
required
value={formData.email}
onChange={handleChange}
placeholder="name@company.com"
className="block w-full pl-11 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-600/20 focus:border-indigo-600 transition-all"
/>
</div>
</div>

🤯 这里的 CSS 技巧太炸裂了!



  1. 图标变色魔法 (group-focus-within)



    • 我们在父级 div 加了 group 类。

    • 在图标 div 加了 group-focus-within:text-indigo-600

    • 效果:当子元素(input)被聚焦(focus)时,父级检测到 focus-within,通知图标改变颜色!

    • 体验:用户一点输入框,前面的小信封瞬间变成亮紫色,这种交互反馈极大地提升了用户的掌控感。



  2. Input 的精细打磨



    • pl-11: 左边距留大点(2.75rem),因为那里放了图标。

    • focus:ring-2 focus:ring-indigo-600/20: 聚焦时,不要浏览器默认的丑边框,我们要一个 2px 宽、带透明度的紫色光环。

    • focus:border-indigo-600: 同时边框颜色变深。

    • transition-all: 所有的变化(颜色、阴影)都要有过渡动画,拒绝生硬。






🔐 第六步:密码框与显隐切换


密码框多了一个“眼睛”按钮,逻辑稍微复杂一点点。


<div className="relative group">
{/* 左侧锁图标 (同上,略) */}

<input
// 动态类型:根据状态决定是明文还是密文
type={showPassword ? "text" : "password"}
name="password"
// ...
/>

{/* 右侧切换按钮 */}
<button
type="button" // 必须写!否则默认是 submit 会触发表单提交
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-4 flex items-center text-slate-400 hover:text-slate-600 transition-colors"
>
{/* 根据状态切换图标 */}
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>

📝 关键点



  1. 动态 Typetype={showPassword ? "text" : "password"}。这是 React 控制 DOM 属性最直接的体现。数据驱动视图,我们不需要手动去操作 DOM 节点的 type 属性。

  2. Button Type:在 <form> 内部的 <button>,如果没有指定 type,默认行为是 submit。如果你点击眼睛图标,页面突然刷新了,肯定是因为你忘了写 type="button"

  3. 图标切换:利用三元运算符 {showPassword ? <EyeOff /> : <Eye />} 在两个图标组件间切换。




🚀 总结


看到这里,你应该已经发现,使用 Tailwind CSS + React 开发界面,实际上是一种搭积木的体验。



  • Tailwind 提供了极其丰富的原子积木(Utility Classes),让你不用写一行 CSS 就能堆砌出精美的样式。

  • React 提供了胶水和传动装置(State & Props),让这些积木动起来,响应用户的操作。


我们学到了什么?



  1. 布局min-h-screen, flex, justify-center 是万能起手式。

  2. 美学:利用 shadow-slate-200/60 这种带颜色的透明阴影制造高级感。

  3. 交互group-focus-within 是处理父子联动交互的神器。

  4. 逻辑:单个 handleChange 处理多个输入框,高效且优雅。

  5. 细节ring, transition, placeholder 等伪类修饰符的组合使用。


课后作业 📝


现在的登录点击后还没有实际效果。你可以尝试完善 handleSubmit 函数,加一个 setTimeout 模拟网络请求,把 isLoading 状态用起来,给按钮加一个“加载中”的转圈圈动画。


前端开发很有趣,Tailwind 让它变得更有趣。希望这篇文章能让你感受到原子化 CSS 的魅力!


喜欢的话,点个赞再走吧!我们下期见!👋




本文代码基于 React 18 + Tailwind CSS 3.x + Lucide React 编写。


作者:神秘的猪头
来源:juejin.cn/post/7591708519449198601
收起阅读 »

流程引擎、工作流、规则引擎、编排系统、表达式引擎……天呐,我到底该用哪个?

你是不是也有这些困惑 看项目文档,各种名词扑面而来: 流程引擎(Flowable、Camunda) 工作流(Activiti) 规则引擎(Drools) 编排系统(LiteFlow) 表达式引擎(QLExpress、Aviator) DAG调度(Airflo...
继续阅读 »

你是不是也有这些困惑


看项目文档,各种名词扑面而来:



  • 流程引擎(Flowable、Camunda)

  • 工作流(Activiti)

  • 规则引擎(Drools)

  • 编排系统(LiteFlow)

  • 表达式引擎(QLExpress、Aviator)

  • DAG调度(Airflow、DolphinScheduler)

  • 任务编排(Temporal、Conductor)

  • BPMN、Saga、Event-Driven...


每个框架都说自己能解决问题,每个概念看起来都差不多。


新手一脸懵逼,老手也经常搞混。


干了20年,我也被这些东西搞晕过。今天不讲那些虚的,直接告诉你怎么选。


答案很简单:别管这些名词,问自己四个问题就够了。


忘掉那些名词,只问四个问题


看了一堆框架介绍还是不知道选哪个?正常,因为你在纠结概念。


别纠结了,概念都是虚的。问自己四个问题,立刻就清楚了。


问题1:你是要干活,还是改状态?


这是最关键的一个问题。搞清楚这个,一大半框架就排除了。


改状态是什么意思?


请假审批:
员工提交 → 主管看了说"行" → HR看了说"行" → 完成

整个过程:
- 没有计算
- 没有数据转换
- 没有调用外部系统
- 就是状态从 pending 变成 approved

这就是纯改状态。

干活是什么意思?


订单处理:
下单 → 扣库存 → 调支付接口 → 调物流接口 → 发货

整个过程:
- 要计算金额
- 要调用外部API
- 要处理数据
- 要执行业务逻辑

这就是干活。

判断标准:



  • 改状态:就是让人点"同意"或"拒绝",除了改个字段,啥也没干

  • 干活:要计算、要调API、要处理数据


对应框架:



  • 改状态 → BPMN系(Flowable、Camunda)

  • 干活 → 继续往下判断


问题2:主要是人处理,还是机器执行?


人处理:


审批流程:
- 主管要看文档
- 主管要做判断
- 主管要点按钮
- 然后等下一个人

特点:大部分时间在等人

机器执行:


数据处理:
- 读数据库
- 清洗数据
- 转换格式
- 写入目标表

特点:机器自己跑,不用人管

对应框架:



  • 人为主 → BPMN系(Flowable、Camunda)

  • 机器为主 → 继续往下判断


问题3:是本地方法,还是跨系统调用?


本地方法:


营销规则:
- 判断用户是不是VIP
- 计算折扣
- 返回结果

都在一个应用里,不用调外部接口

跨系统调用:


订单流程:
- 调库存系统(HTTP)
- 调支付系统(HTTP)
- 调物流系统(HTTP)

要跨多个服务

对应框架:



  • 本地 → 表达式系、脚本系(QLExpress、LiteFlow)

  • 跨系统 → DAG系、服务编排系(Airflow、Temporal)


问题4:自己玩,还是要搞生态?


自己玩:


你的团队自己维护:
- 规则你们自己写
- 代码你们自己改
- 不需要外部开发者

搞生态:


做平台,让别人扩展:
- 客户可以上传插件
- 第三方可以写脚本
- 需要沙箱隔离

对应技术:



  • 自己玩 → 表达式 + 代码(QLExpress、Aviator)

  • 搞生态 → Groovy脚本、插件机制


那些让人头疼的框架,到底是干什么的


四个问题问完,你大概知道方向了。现在看看具体框架都是什么情况。


不用全看,只看和你匹配的那一类就行。


BPMN系:Flowable、Camunda、Activiti


适合场景:



  • 纯人工审批流程

  • 需要流程图可视化

  • 需要历史记录追溯

  • 大公司、强合规要求


典型例子:



  • 请假审批

  • 报销审批

  • 合同审批

  • 采购流程


核心特点:



  • 本质就是改状态

  • 大部分时间在等人

  • 业务价值为0(只是流程管理)

  • 技术难度不高(就是状态机)


什么时候用:



  • 大公司(100+人),有几十个审批流程要管理

  • 金融、政府等强合规行业

  • 需要标准化流程管理


什么时候别用:



  • 小公司(别用,钉钉审批就够了)

  • 没有复杂审批需求(自己写100行代码搞定)

  • 为了"企业级"而用(过度设计)


DAG系:Airflow、DolphinScheduler、Prefect


适合场景:



  • 数据处理任务

  • 离线批处理

  • 定时调度

  • 任务有依赖关系


典型例子:



  • 数据ETL

  • 报表生成

  • 数据清洗

  • 机器学习Pipeline


核心特点:



  • 纯机器执行

  • 长时间运行(小时、天级)

  • 任务之间有依赖(A完成才能B)

  • 需要调度和监控


什么时候用:



  • 数据团队做离线处理

  • 有复杂的任务依赖关系

  • 需要定时调度(每天、每周)


什么时候别用:



  • 实时性要求高的(秒级响应)

  • 简单的定时任务(用Cron就够了)

  • 没有依赖关系的任务


表达式/脚本系:QLExpress、Aviator、LiteFlow、Groovy


适合场景:



  • 规则计算

  • 业务流程编排

  • 本地方法调用

  • 需要动态配置


典型例子:



  • 营销活动规则(满减、折扣)

  • 风控规则(黑名单、评分)

  • 订单流程(本地编排)

  • 积分计算


QLExpress / Aviator(表达式):



  • 优点:性能好、类Java语法、团队容易上手

  • 缺点:功能受限、只能简单计算

  • 适合:自己团队玩、简单规则


Groovy(脚本):



  • 优点:功能完整、可以调复杂API

  • 缺点:性能差、调试难、类型不安全

  • 适合:要搞插件生态、客户自定义逻辑


LiteFlow(编排):



  • 优点:可视化编排、组件复用

  • 缺点:学习成本、维护成本

  • 适合:流程确实复杂、经常变化


什么时候用:



  • 规则经常变(不想每次改代码发版)

  • 流程需要配置化

  • 有一定复杂度(10+个分支)


什么时候别用:



  • 简单的if-else(直接写代码)

  • 流程固定不变(没必要配置化)

  • 为了"灵活"而牺牲性能


服务编排系:Temporal、Cadence、Conductor


适合场景:



  • 微服务编排

  • 分布式事务

  • 长时间运行的业务流程

  • 需要补偿机制


典型例子:



  • 订单流程(支付 → 发货 → 签收)

  • 旅游预订(机票 + 酒店 + 门票)

  • 跨系统流程

  • Saga模式


核心特点:



  • 支持长时间运行(天级)

  • 支持失败重试

  • 支持补偿逻辑

  • 状态持久化


什么时候用:



  • 微服务架构,需要编排多个服务

  • 需要分布式事务

  • 流程可能运行很久(几小时、几天)


什么时候别用:



  • 单体应用(没有跨服务需求)

  • 简单的API调用(直接用HTTP就行)

  • 实时性要求极高的(毫秒级)


懒得看?直接照这个选


如果你嫌上面内容太多,直接看这个决策树。


跟着问题一步步走,到底了就知道该用什么。


开始

主要是人审批吗?
↓ 是
用 Flowable/Camunda(大公司)或钉钉审批(小公司)

↓ 否
是长时间运行的任务吗(>10分钟)?
↓ 是
用 Airflow/DolphinScheduler

↓ 否
需要跨系统调用吗?
↓ 是
用 Temporal/Conductor(微服务)或 Airflow(数据处理)

↓ 否
逻辑很复杂吗(>10个分支)?
↓ 是
用 LiteFlow(编排)或 QLExpress(规则)

↓ 否
需要频繁修改规则吗?
↓ 是
用 QLExpress/Aviator

↓ 否
直接写代码!

具体场景怎么选


理论说完了,看几个实际例子。看看你的场景和哪个像。


场景1:请假审批


特征:



  • 纯人工审批

  • 状态流转

  • 需要历史记录


选型:



  • 小公司:钉钉/企业微信审批

  • 大公司:Flowable/Camunda

  • 自己开发:状态机 + 数据库


场景2:电商订单流程


特征:



  • 要调支付、库存、物流接口

  • 有失败重试和补偿

  • 短事务(分钟级)


选型:



  • 复杂场景:Temporal/Cadence

  • 简单场景:LiteFlow + 消息队列

  • 最简单:直接写代码 + 状态机


场景3:数据ETL


特征:



  • 纯机器执行

  • 长时间运行

  • 任务有依赖


选型:



  • 标准方案:Airflow/DolphinScheduler

  • 简单场景:XXL-Job


场景4:营销活动规则


特征:



  • 规则计算

  • 经常变化

  • 本地方法


选型:



  • 简单规则:QLExpress/Aviator

  • 复杂规则:Drools

  • 有编排需求:LiteFlow


很多人踩过的坑


说几个常见的错误,别重复踩坑。


误区1:追求"企业级架构"


错误做法:
20人的创业公司,上了Flowable、Camunda、Airflow一整套

正确做法:
能用100行代码解决就别上框架

误区2:为了灵活性而牺牲性能


错误做法:
所有逻辑都用Groovy脚本,方便修改

正确做法:
核心逻辑用Java写,只把经常变的部分配置化

误区3:过度抽象


错误做法:
3个简单流程,非要搞个"流程引擎"

正确做法:
3个流程就3个方法,直接写代码

误区4:混淆概念


错误理解:
"我需要流程编排,所以要用Flowable"

正确理解:
先搞清楚你要干活还是改状态
是人审批还是机器执行

几句大实话


最后说几句掏心窝的话。


1. 先用最简单的方案


遇到问题:
第一反应不是"上框架"
而是"能不能写100行代码搞定"

90%的情况,100行代码就够了

2. 遇到瓶颈再优化


流程很乱了 → 重构代码
改动很频繁 → 考虑配置化
管理不过来 → 考虑框架

别提前优化

3. 根据团队规模选择


小团队(<20人):
- 能不用框架就不用
- 钉钉审批、Cron、直接写代码

中等团队(20-100人):
- 流程<10个:自己写
- 流程>10个:考虑轻量级框架

大团队(>100人):
- 需要标准化管理
- 可以考虑成熟框架

4. 看业务特点


强合规(金融、政府):
- 必须用标准化工具
- Flowable是选择之一

数据密集:
- Airflow是标准方案

微服务架构:
- Temporal值得考虑

简单CRUD:
- 别折腾,写代码

说到底,就这么点事


看完还觉得复杂?那就记住这四个问题:



  1. 干活还是改状态?

  2. 人为主还是机器为主?

  3. 本地方法还是跨系统?

  4. 自己玩还是搞生态?


四个问题问完,基本就知道该用什么了。


那些"企业级"、"先进架构"、"灵活扩展"的词,都是包装。


看透本质,别被忽悠。


能用100行代码解决的,就别上框架。


技术是为业务服务的,不是为了炫技。


务实点,别整那些虚的。


就这样。


作者:踏浪无痕
来源:juejin.cn/post/7587299670642606086
收起阅读 »

做好自己的份内工作,等着被裁

先声明,本文不是贩卖焦虑,只是自己的一点拙见,没有割韭菜的卖课、副业、保险广告,请放心食用。 2022 年初,前司开始了轰轰烈烈的「降本增笑」运动,各部门严格考核机器成本和预算。当然,最重要的还是「开猿节流」。 幸好,我所在部门是盈利的,当时几乎没有人受到波...
继续阅读 »

先声明,本文不是贩卖焦虑,只是自己的一点拙见,没有割韭菜的卖课、副业、保险广告,请放心食用。


2022 年初,前司开始了轰轰烈烈的「降本增笑」运动,各部门严格考核机器成本和预算。当然,最重要的还是「开猿节流」。


开猿节流


幸好,我所在部门是盈利的,当时几乎没有人受到波及。


据说,现在连餐巾纸都从三层的「维达」换成两层的「心心相印」了,号称年节约成本 100 多万。我好奇的是,擦屁股时多少会沾点 💩 吧?这下,真是名正言顺的 💩 山代码了。


2022 年 7 月底,因为某些原因,结束 10 年北漂回老家,换了个公司继续搬砖。


2023 年,春节后不久,现司搞「偷袭」,玩起了狼人杀,很多小伙伴被刀:



清晨接到电话通知,上午集体开会,IT 收回权限,中午滚蛋



好在是头一回,补偿非常可观,远超法律规定的「N+1」。


2024 年,平安夜,无事发生。


2025 年 1 月,公司年会,趣味运动会,有个项目是「财源滚滚」,下图这样的:


财源滚滚


有个参赛的老哥调侃道,这项目名字不吉利啊,不应该参加的。无巧不成书,年后他被刀了。。。


这次的规模远小于 2023 年,但 2025 年也不太平,「脉脉」上陆续有人说被刀或者不续签,真假未知。


实话说,我之前从未担心过被裁,毕竟:



名校硕士,经历多个大厂,有管理经验


热爱编程,工作认真负责,常年高绩效



但是,随着 AI 的快速迭代,我现在感觉自己随时可能被刀了。AI 能胜任 log 分析、新功能开发、bug 修复等绝大部分日常工作,而且都完成的很好。再配合 AI 自己写的MCP,效率肉眼可见的提高。


亲身体验,数百人开发的千万行代码级别的项目,混合了Java/Kotlin/OC/C++/Python等各种语言。跟Cursor聊了几句,它就找到原因并帮忙修复了。如果是自己看代码、问人、加 log、编译,至少得半个小时。


那还要码农干啥呢?即使是留下来背锅,也要不了这么多啊。


距离上次「狼人杀 」,三年之期已到。今年会有「狼人杀 2.0」吗?我还能平稳落地吗?


无所谓了,我早已准备好后路:


后路


头盔和衣服真是我买的,还有手套未入镜,我感觉设计很漂亮,等天气暖和后,当骑行服穿。


汽车,小踏板,大踏板,足以覆盖滴滴、外卖、闪送三大朝阳行业。家里还有个小电驴,凑合能放到后备箱,承接代驾业务问题不大。


以上,虽然是开玩笑,但我对「是否被刀、何时被刀」,真的是无所谓。因为:



一个人的命运啊,当然要靠自我奋斗,但也要考虑历史的进程



公司为了长远的发展,刀人以降低成本,再用 AI 来提高效率,求得股价长红。对此,我十分理解,换我当老板,也会这么干。


作为牛马,想太多没用,我们左右不了这些事。不夸张的说,99.9999% 的码农是不可能干到退休的,和死亡一样,被刀只是早晚的事。更扎心的是:



人不是老了才会死,而是随时会死



当下的工作也一样,并不是摸鱼或者捅娄子才会被刀,而是随时会被刀,与个人的努力、绩效关系不大。常年健身的肌肉男,也可能猝死,只是概率低点,并不是免死金牌。


生命,从受精的那一刻起,就在走向终点。工作,从入职的那一刻起,就在走向(主动/被动)离职。


所以,虽然我现在感觉自己随时可能被 AI 替代,但我的心态一直都没变,就是标题所言:



做好自己的份内工作,等着被裁



不是消极怠工,我始终认真完成每一项任务,该加班加班。并非为了绩效,是因为自己的责任心,要对的起工资。至于公司哪天让我滚蛋,我决定不了,更改变不了。就像对待死亡一样,坦然接受之,给够补偿就好。


补偿.png


对于 AI,还想再啰嗦两句:



  1. 虽然 AI 很牛逼,但最终还是需要人来判断代码的对错。此时,工程师的价值就体验出来了,所以 AI 是帮我干活的小弟,而不是竞争对手。

  2. AI 扩大了我们的能力边界,人人都可以是前端、后端、客户端、UI 设计全通的「全栈工程师」,至少可以是「全沾工程师」,「雨露均沾」的沾。


滚蛋之后呢?我不知道,现在有多少公司愿意招 40 岁高龄码农?据说前司招聘 35 岁普通员工都要 VP 审批了,真是小刀剌屁股,开了眼了。


好在,我家人的物质欲望极低,对衣服、手机、汽车没有任何追求,老婆不用化妆品和护肤品,也没买过一个包。即使不上班,积蓄也能撑一段时间。


所以,强烈建议当前北上广深拿高薪的老哥老妹们,除非万不得已,千万不要像我一样断崖式降薪回老家。趁年轻,搞钱比啥都重要。


搞钱


对了,我目前有两个利用自身优势的基于 AI 的创业方向。网友们帮忙把把关,如果哪天真失业了,看能否拉到几个亿的风投,谢谢!



  1. 偏胖圆脸,AI 加点络腮胡,再买几双白袜子

  2. 身高 180,AI 换个美女脸,黑丝高跟大长腿


谢谢


作者:野生的码农
来源:juejin.cn/post/7593771861323726874
收起阅读 »

WebSocket 不是唯一选择:SSE 打造轻量级实时推送系统 🚀🚀🚀

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎...
继续阅读 »

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777



20250310220634



在需要服务器实时向浏览器推送数据的场景中,很多人第一反应是使用 WebSocket,但其实还有一种更轻量、更简单的解决方案 —— SSE(Server-Sent Events)。它天生适合“服务器单向推送”,而且浏览器原生支持、无需额外协议、写起来极其简单



本文将从原理、协议、代码、对比、性能、安全等多个方面,帮你系统了解 SSE 的底层机制与实际应用。


🧠 一、什么是 SSE?


SSE,全称 Server-Sent Events,是 HTML5 提出的标准之一,用于建立一种 客户端到服务器的持久连接,允许服务器在数据更新时,主动将事件推送到客户端


通俗点讲,它就像是:



浏览器发起了一个请求,服务器就打开一个“水管”,源源不断地往客户端输送数据流,直到你手动关闭它。



它基于标准的 HTTP 协议,与传统请求-响应的“短连接”模式不同,SSE 是长连接,并且保持活跃,类似于“实时通知通道”。


🛠️ 二、SSE 的通信机制与协议细节


✅ 客户端:使用 EventSource 建立连接


const sse = new EventSource("/events");

sse.onmessage = (event) => {
console.log("新消息:", event.data);
};

EventSource 是浏览器自带的,直接用就行,不用装库。它会自动处理连接、断线重连这些问题,基本不需要你操心,消息来了就能收到。


原生 EventSource 的使用限制


虽然原生的 EventSource 对象很方便,但也存在很多的限制,它只能发送 GET 请求,不支持设置请求方法,也不能附带请求体。


你不能通过 EventSource 设置如 Authorization、token 等自定义请求头用于鉴权。


例如,下面这样是不被支持的:


const sse = new EventSource("/events", {
headers: {
Authorization: "Bearer xxx",
},
});

这在 fetch 里没问题,但在 EventSource 里完全不支持。直接报错,浏览器压根不给你设置 headers。


EventSource 虽然支持跨域,但得服务器配合设置 CORS,而且还不能用 withCredentials。换句话说,你不能让它自动带上 cookie,那些基于 cookie 登录的服务就麻烦了。


如果你需要传 token 或做鉴权,可以使用查询参数传 token,比如这样:


const token = "abc123";
const sse = new EventSource(`/events?token=${token}`);

✅ 服务器:响应格式必须为 text/event-stream


服务器需要返回特定格式的数据流,并设置以下 HTTP 响应头:


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

如下图所示:


20250411153247


然后每条消息遵循下面的格式:


data: Hello from server
id: 1001
event: message

如下图所示:


20250411153207转存失败,建议直接上传图片文件


在上面的内容中,主要有以下解释,如下表格所示:


字段说明
data:消息正文内容,支持多行
id:消息 ID,浏览器断线重连后会通过 Last-Event-ID 自动恢复
event:自定义事件名(默认是 message
retry:指定断线重连间隔(毫秒)

🔄 三、SSE vs WebSocket vs 轮询,对比总结


特性SSEWebSocket长轮询(Ajax)
通信方向单向(服务器 → 客户端)双向单向
协议HTTP自定义 ws 协议HTTP
支持断线重连✅ 内置自动重连❌ 需手动重连逻辑
浏览器兼容性现代浏览器支持,IE 不支持广泛支持兼容性强
复杂度✅ 最简单,零依赖中等简单但消耗高
使用场景实时通知、进度、新闻、后台日志聊天、游戏、协作、股票交易等简单刷新类数据

🚀 四:如何在 NextJs 中实现


NextJS 作为一个现代化的 React 框架,非常适合实现 SSE。下面我们将通过一个完整的实例来展示如何在 NextJS 应用中实现服务器发送事件。


前端代码如下:


"use client";

import React, { useState, useEffect, useRef } from "react";

export default function SSEDemo() {
const [sseData, setSseData] = useState<{
time?: string;
value?: string;
message?: string;
error?: string;
} | null>(null);
const [connected, setConnected] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const [reconnectCount, setReconnectCount] = useState(0);
const eventSourceRef = useRef<EventSource | null>(null);
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);

// 建立SSE连接
const connectSSE = () => {
// 关闭任何现有连接
if (eventSourceRef.current) {
eventSourceRef.current.close();
}

// 清除任何挂起的重连计时器
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}

try {
setReconnecting(true);

// 添加时间戳防止缓存
const eventSource = new EventSource(`/api/sse?t=${Date.now()}`);
eventSourceRef.current = eventSource;

eventSource.onopen = () => {
setConnected(true);
setReconnecting(false);
setReconnectCount(0);
console.log("SSE连接已建立");
};

eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
setSseData(data);
} catch (error) {
console.error("解析SSE数据失败:", error);
}
};

eventSource.onerror = (error) => {
console.error("SSE连接错误:", error);
setConnected(false);
eventSource.close();

// 增加重连次数
setReconnectCount((prev) => prev + 1);

// 随着失败次数增加,增加重连间隔(指数退避策略)
const reconnectDelay = Math.min(
30000,
1000 * Math.pow(2, Math.min(reconnectCount, 5))
);

setReconnecting(true);
setSseData((prev) => ({
...prev,
message: `连接失败,${reconnectDelay / 1000}秒后重试...`,
}));

// 尝试重新连接
reconnectTimeoutRef.current = setTimeout(() => {
connectSSE();
}, reconnectDelay);
};
} catch (error) {
console.error("创建SSE连接失败:", error);
setConnected(false);
setReconnecting(true);

// 5秒后重试
reconnectTimeoutRef.current = setTimeout(() => {
connectSSE();
}, 5000);
}
};

useEffect(() => {
connectSSE();

// 定期检查连接是否健康
const healthCheck = setInterval(() => {
if (eventSourceRef.current && !connected) {
// 如果存在连接但状态是未连接,尝试重新连接
connectSSE();
}
}, 30000);

// 清理函数
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
clearInterval(healthCheck);
};
}, []);

return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 to-slate-800 text-white flex flex-col items-center justify-center p-4">
<div className="w-full max-w-md bg-slate-800 rounded-xl shadow-2xl overflow-hidden">
<div className="p-6 border-b border-slate-700">
<h1 className="text-3xl font-bold text-center text-blue-400">
SSE 演示
</h1>
<div className="mt-2 flex items-center justify-center">
<div
className={`h-3 w-3 rounded-full mr-2 ${
connected
? "bg-green-500"
: reconnecting
? "bg-yellow-500 animate-pulse"
: "bg-red-500"
}`}
>
</div>
<p className="text-sm text-slate-300">
{connected
? "已连接到服务器"
: reconnecting
? `正在重新连接 (尝试 ${reconnectCount})`
: "连接断开"}
</p>
</div>

{!connected && (
<button
onClick={() =>
connectSSE()}
className="mt-3 px-3 py-1 bg-blue-600 text-sm text-white rounded-md mx-auto block hover:bg-blue-700"
>
手动重连
</button>
)}
</div>

{sseData && (
<div className="p-6">
{sseData.error ? (
<div className="rounded-lg bg-red-900/30 p-4 mb-4 text-center border border-red-800">
<p className="text-lg text-red-300">{sseData.error}</p>
</div>
) : sseData.message ? (
<div className="rounded-lg bg-slate-700 p-4 mb-4 text-center">
<p className="text-lg text-blue-300">{sseData.message}</p>
</div>
) : (
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-slate-400">时间:</span>
<span className="font-mono bg-slate-700 px-3 py-1 rounded-md text-blue-300">
{sseData.time &&
new Date(sseData.time).toLocaleTimeString()}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-400">随机值:</span>
<span className="font-mono bg-slate-700 px-3 py-1 rounded-md text-green-300">
{sseData.value}
</span>
</div>
</div>
)}
</div>
)}

{!sseData && (
<div className="p-6 text-center text-slate-400">
<p>等待数据中...</p>
<div className="mt-4 flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-400"></div>
</div>
</div>
)}
</div>
</div>

);
}

在上面的代码中,我们用的是浏览器的原生 EventSource,加了个时间戳 t=${Date.now()} 是为了防止缓存,确保每次都是新的连接。


然后我们监听三个事件:



  1. onopen:连接成功,更新状态,重置重连次数。

  2. onmessage:收到数据,尝试解析 JSON,然后保存到状态里。

  3. onerror:连接失败,进入重连逻辑(详细见下面)。


当连接出错时,我们做了这些事:



  1. 断开当前连接

  2. 增加重连次数

  3. 用指数退避算法(越失败,重试间隔越长,最多 30 秒)

  4. 设置一个 setTimeout 自动重连


而且页面上也有提示「正在重连」和「手动重连」的按钮,体验很人性化。


接下来我们看看后端代码,如下:


export async function GET() {
// 标记连接是否仍然有效,
let connectionClosed = false;

// 使用Next.js的流式响应处理
return new Response(
new ReadableStream({
start(controller) {
const encoder = new TextEncoder();

// 监测响应对象是否被关闭
const abortController = new AbortController();
const signal = abortController.signal;

signal.addEventListener("abort", () => {
connectionClosed = true;
cleanup();
});

// 安全发送数据函数
const safeEnqueue = (data: string) => {
if (connectionClosed) return;
try {
controller.enqueue(encoder.encode(data));
} catch (error) {
console.error("SSE发送错误:", error);
connectionClosed = true;
cleanup();
}
};

// 发送初始数据
safeEnqueue(`data: ${JSON.stringify({ message: "连接已建立" })}\n\n`);

// 定义interval引用
let heartbeatInterval: NodeJS.Timeout | null = null;
let dataInterval: NodeJS.Timeout | null = null;

// 清理所有资源
const cleanup = () => {
if (heartbeatInterval) clearInterval(heartbeatInterval);
if (dataInterval) clearInterval(dataInterval);

// 尝试安全关闭控制器
try {
if (!connectionClosed) {
controller.close();
}
} catch (e) {
// 忽略关闭时的错误
}
};

// 设置10秒的心跳间隔,避免连接超时
heartbeatInterval = setInterval(() => {
if (connectionClosed) {
cleanup();
return;
}
safeEnqueue(": heartbeat\n\n");
}, 10000);

// 每秒发送一次数据
dataInterval = setInterval(() => {
if (connectionClosed) {
cleanup();
return;
}

try {
const data = {
time: new Date().toISOString(),
value: Math.random().toFixed(3),
};
safeEnqueue(`data: ${JSON.stringify(data)}\n\n`);
} catch (error) {
console.error("数据生成错误:", error);
connectionClosed = true;
cleanup();
}
}, 1000);

// 60秒后自动关闭连接(可根据需要调整)
setTimeout(() => {
// 只有当连接仍然活跃时才发送消息和关闭
if (!connectionClosed) {
try {
safeEnqueue(
`data: ${JSON.stringify({
message: "连接即将关闭,请刷新页面重新连接",
})}
\n\n`

);
connectionClosed = true;
cleanup();
} catch (e) {
// 忽略关闭时的错误
}
}
}, 60000);
},
cancel() {
// 当流被取消时调用
connectionClosed = true;
},
}),
{
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no", // 适用于某些代理服务器如Nginx
},
}
);
}

这段代码是 Next.js 后端 API 路由,用来实现 SSE(Server-Sent Events)长连接。我们使用了 ReadableStream 创建一个持续向前端推送数据的响应流,并配合 AbortSignal 检测连接是否被关闭:


return new Response(new ReadableStream({ start(controller) { ... } }), { headers: {...} });

一开始,服务器通过 safeEnqueue 安全地向客户端发送一条欢迎消息:


safeEnqueue(`data: ${JSON.stringify({ message: "连接已建立" })}\n\n`);

随后每秒生成一条数据(当前时间和随机值)推送给前端,并通过 setInterval 定时发送:


const data = {
time: new Date().toISOString(),
value: Math.random().toFixed(3),
};
safeEnqueue(`data: ${JSON.stringify(data)}\n\n`);

为了保持连接活跃,避免浏览器或代理中断连接,我们每 10 秒发送一次心跳包(以冒号开头的注释):


safeEnqueue(": heartbeat\n\n");

还加了一个自动关闭机制——60 秒后主动断开连接并提示前端刷新:


safeEnqueue(
`data: ${JSON.stringify({ message: "连接即将关闭,请刷新页面重新连接" })}\n\n`
);

整个数据发送过程都包裹在 safeEnqueue 中,确保连接断开时能安全终止,并调用 cleanup() 清理资源。响应头中我们指定了 text/event-stream,关闭了缓存,并设置了必要的长连接参数:


headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"X-Accel-Buffering": "no"
}

通过这种方式,服务端可以稳定地向客户端发送实时数据,同时具备自动断开、心跳维持、错误处理等健壮性,是非常实用的 SSE 实践方案。


最终结果如下图所示:


20250411154742


成功实现。


总结


SSE(Server-Sent Events)是一种基于 HTTP 的 服务器向客户端单向推送数据的机制,适用于需要持续更新前端状态的场景。除了浏览器原生支持的 EventSource,也可以通过 fetch + ReadableStream 或框架内置流式处理(如 Next.js API Route、Node.js Response Stream)来实现,适配更复杂或自定义需求。相比 WebSocket,SSE 实现更简单,自动断线重连、无需维护双向协议,非常适合实时消息通知、进度条更新、在线人数统计、系统日志流、IoT 设备状态推送等。它特别适合“只要服务器推就好”的场景,无需双向通信时是高效选择。


作者:Moment
来源:juejin.cn/post/7493140532798914570
收起阅读 »

国产 OCR 开源神器官网上线了,相当给力。

在大模型狂飙突进的今天,高质量、结构化的数据已成为决定 AI 能力的核心基建。而现实中,海量知识却沉睡在PDF、扫描件、报告等非结构化文档中。 如何将这座富矿高效、精准地转化为大模型可理解、可训练的数据燃料,是整个产业面临的关键瓶颈。 OCR(光学字符识别)技...
继续阅读 »

在大模型狂飙突进的今天,高质量、结构化的数据已成为决定 AI 能力的核心基建。而现实中,海量知识却沉睡在PDF、扫描件、报告等非结构化文档中。


如何将这座富矿高效、精准地转化为大模型可理解、可训练的数据燃料,是整个产业面临的关键瓶颈。


OCR(光学字符识别)技术正是打通这一瓶颈的数据管道。但传统OCR主要停留在「字符识别」层面,面对包含图表、公式、代码以及复杂版式的文档时,往往会产出混乱的文本流,难以支撑后续理解、检索等等需求。


因此,在大模型时代,这一能力已远远不够。一个真正可用的文档解析方案,必须提供端到端的文档智能解析能力:不仅「看得准」,更要「懂得清」。


它需要在识别文本的同时,理解文档的语义结构和版式逻辑,将原始文档精准还原为包含标题、段落、表格、图表描述、公式 LaTeX、代码块等语义信息的标准化表示形式(如 Markdown / JSON)。


只有当非结构化文档被转化为高质量、可直接消费的结构化数据,才能真正成为大模型训练、知识库构建、RAG 检索与智能问答中的可靠数据原料,从而发挥它应有的价值。


今天,这个关键的「数据管道」迎来了它里程碑式产品化升级——PaddleOCR 官网(http://www.paddleocr.com)正式版上线了!


这不仅是其强大开源能力的直观展现,更通过丝滑的体验与海量API,将文档结构化能力推向了普惠化应用。


熟悉我的老粉都知道,过去如果我要推荐 OCR 或文档解析工具,基本只会提到 PaddleOCR。原因很简单:我希望为大家提供一条最高效、最直接的“生产力路径”,而不是让大家在众多项目中反复试错。


这不仅是我的推荐逻辑,也是各大模型厂商在开源选型时的共识——PaddleOCR 几乎是文档解析领域唯一被广泛引用的开源方案。


今年 10 月 17 日 PaddleOCR-VL 刚刚发布,仅用 16 小时就登顶 HuggingFace Trending 全球榜首。


短短两个月内,项目的 Star 数从 57k 飙升至接近 67k。要知道,一个开源项目在五年之后还能保持这样的增长速度,背后一定是它切中了真实且迫切的用户需求。


01、关键特性:三大模型,覆盖全场景文档解析


打开官网,你会看到三个核心入口:GitHub 开源地址、MCP 接口、API 接口。下方支持直接上传图像或 PDF,体验 PaddleOCR 的三大模型方案:



  • PP-OCRv5:轻量级 OCR,适合纯文本提取

  • PP-StructureV3:基于pipeline架构的文档解析,支持印章、表格、标题等还原,零幻觉

  • PaddleOCR-VL(默认):基于视觉-语言模型的文档解析,支持图文、公式、代码等多模态解析,当前全球最高精度


图片


如果你还不清楚这些模型能力的区别,PaddleOCR 官方文档(http://www.paddleocr.ai)提供了清晰的说明,支持搜索与评论,非常友好。


图片


我这里以 PaddleOCR-VL 为例,上传了一篇 DeepSeek-R1 的论文 PDF。


几秒后,解析结果清晰呈现:不管是文字、图像、代码、表格还是公式,PaddleOCR都能精准还原,相关内容,可以左右一一对应。


在右侧,你也可以复制所有的解析结果,也可以复制其中的某一个block的结果,还可以基于某一个block进行内容纠正。下边是一些关键场景的可视化。


·文字场景


一级标题、二级标题、正文层次分明,还原精准。


图片


·图像/图表场景


支持图表转表格,对科研与数据分析工作者极其友好。关闭图表识别功能:


图片


打开图表识别功能:


图片


这项功能极其实用,能够将图表等非结构化数据转换为结构化表格,对于科研人员以及日常需要处理图表数据的工作者而言,是一项极具价值的工具。


·代码场景


代码区域被转换为等宽字体,代码的格式与内嵌公式保留完整,恢复完美。


图片


·表格场景


合并单元格也能准确预测,精准还原表格中的各项指标。点击“复制”可直接粘贴至 Excel,格式无损。


图片


此外,在表格应用场景中,我还发现了一个小惊喜:点击右侧下方表格区块的复制按钮后,可以将表格内容无损地粘贴到Excel中,原有格式能够完整保留。这个功能对我日常整理数据非常有帮助,没想到能够如此完美地实现。


不过,官方似乎并未特别宣传这项小功能,看来还有许多实用细节有待用户进一步发掘。


图片


·公式场景 


LaTeX 格式输出,右侧实时渲染,复杂公式也无错漏。


图片


公式内容会被自动识别并转换为LaTeX格式的代码,随后在右侧的Markdown区域被正确渲染。经过对比验证,即使是较为复杂的公式也能够准确无误地显示,未发现任何错误。


·更多功能


此外,官网还支持批量上传(最多 20 个文件),并提供了超参数设置面板,除了默认的结果,还有一个设置超参数的按钮,用户可根据需求设置很多超参数,关于超参数的解释,也在旁边隐藏的部分有解释。


比如上边的图表识别的功能,我就是打开了这个超参数中的图表识别的开关,灵活度很高。


图片


图片


02


API 调用:数据基建的“普惠管道”


PaddleOCR官网首页已直接提供了 API 和 MCP 的调用示例,点击就可以有对应的弹窗,亲测带上token,复制可以跑。这里以 API 为例,MCP类似。


基础跑通三步走:


1. 点击首页的API:


图片


2. 复制代码到本地


在本地电脑新建一个名为 test.py 的文件,并将复制的代码粘贴进去(此时你的账号 token 也会被自动复制)。然后,在代码中的 file_path 参数填写你要预测的文件名。这里需要注意的是:如果是 PDF 文件,fileType 应设置为 0;如果是图像文件,fileType 则需要设置为 1。


图片


图片


3. 运行代码


大约在20多秒可以返回一个21页的PDF结果,包含了每一页的Markdown的结果、对应的插图等。基本上每秒一页,速度还不错。本地可视化如图所示,和网页端完全一致。


图片


进阶玩法三步走:


进一步体验PaddleOCR官网,会发现一些我认为非常重要的细节。


1. API和效果联动


这次 PaddleOCR 官网的一个重要变化,是前端整体把体验优化得非常友好了,不再只是“展示效果”,而是围绕 参数配置 → 效果验证 → API 接入 这条完整路径来设计。


图片


在网页端,你可以直接调整解析参数,比如是否开启图表识别、是否需要方向矫正、不同结构化策略等,每一次参数变化,解析结果都会即时刷新返回。图像或 PDF 的结构化结果几乎是秒级可见,非常适合快速对比不同参数组合下的效果差异,而不是靠猜。


图片


更关键的是,这些在网页端调过、验证过的参数,并不会停留在「试用层」。当你确认某一套配置满足你的业务需求后,可以直接一键复制对应的 API 调用代码,包括参数、模型类型和调用方式,拿到本地或直接接入业务系统即可使用。


图片


整个过程非常顺滑:


你不需要先搭环境、不需要翻文档对着字段一个一个找参数含义,先在网页上把效果跑通,再把同一套配置“原封不动”搬进工程里。哪怕完全没有本地部署过,也可以先把解析效果看清楚、想明白,再决定是否以及如何在真实业务中使用。


一句话总结就是:


不用写一行代码,也能把PaddleOCR的能力验证到位;一旦要上线,代码已经帮你准备好了。


2.更多的 API 调用


在 API 文档页有一行关键说明:“每位用户每日对同一模型的解析上限为 3000 页,超出会返回 429 错误。如需更高额度,可通过问卷申请白名单。”


🔗申请链接为:paddle.wjx.cn/vm/mePnNLR.…


我填写了问卷中四个常规问题留下联系方式后,很快就有官方人员联系我,了解使用场景后直接开通了白名单。随后我测试了约 1 万份 PDF(共 3 万多页),开了一个后台的访问服务的进程挂机运行一夜,第二天一早,全部解析成功。这意味着,现阶段个人、团队或初创企业完全可以借助此额度,启动大规模的数据清洗与知识库构建工作,成本几乎为零。


图片


3.不容错过的MCP


作为 AI 时代的 Type-C 接口,MCP 正逐渐成为各类 AI 产品的基础能力配置。PaddleOCR 官网也提供了开箱即用的 MCP server:只需复制官网给出的配置示例,并在 MCP host 应用中完成简单配置,即可让大模型直接调用 PaddleOCR 的文字识别与文档解析能力。


图片


我也在 Cherry Studio 里试了试效果。花了不到一分钟复制粘贴 MCP 配置,然后使用 PaddleOCR 官网提供的 PP-OCRv5 MCP server 来识别图像中的酒店名称:


图片


03、项目相关链接


官网虽已足够强大,但如果你有私有化部署需求,仍可基于开源项目自行部署。


·PaddleOCR GitHub:https://github.com/PaddlePaddle/PaddleOCR·官方文档:https://www.paddleocr.ai·Hugging Face 模型:https://huggingface.co/PaddlePaddle

PaddleOCR 再一次没有让人失望。从开源项目到产品化官网,从模型迭代到这波 API 的开放,它正在把文档智能从“技术能力”推向“普及工具”。大模型时代,数据是石油,而 OCR 则是开采与提炼的核心装备。PaddleOCR 这一次的升级,不仅提升了开采效率,还让更多人用上了这把利器。


期待大家亲自体验,也欢迎在评论区分享你的使用场景与发现。


作者:逛逛GitHub
来源:juejin.cn/post/7588388014505312298
收起阅读 »

2025 年终回顾:25 岁,从“混吃等死”到别人眼中的“技术专家”

2025 年终总结:25 岁,从“混吃等死”到别人眼中的“技术专家” 两年前的春节假期,某天。在一所面积不大的小房里,住着三个人。 那时的我,还是个凭运气混进大公司、天天写 CRUD 混吃等死的前端“小卡拉米”。趁着春节假期,我戴着耳机,沉浸在游戏世界里。突然...
继续阅读 »

2025 年终总结:25 岁,从“混吃等死”到别人眼中的“技术专家”


两年前的春节假期,某天。在一所面积不大的小房里,住着三个人。


那时的我,还是个凭运气混进大公司、天天写 CRUD 混吃等死的前端“小卡拉米”。趁着春节假期,我戴着耳机,沉浸在游戏世界里。突然,客厅传来一声闷响。


我疑惑地摘下耳机:“什么声音?”


回头望去,我看到奶奶仰面躺在沙发上——她晕倒了。


那是我第一次见到我最亲爱的亲人病倒在面前。慌乱中,我给在外打牌的父亲拨去了电话。最后所幸并无大碍,但在那一刻,我知道:我不能再这样混下去了,我需要努力。


两年后的今天,再回头看:



  • 我成为了团队中不可或缺的技术核心

  • 我成为了稀土掘金 2025 年度优秀创作者

  • 我开源的项目累计获得 1K+ GitHub Star

  • 我开始频繁出现在 Three.js 官方推特的转发列表中

  • 也第一次,被别人称为「技术专家」


111.gif


前言:前端版“萧炎”?不,是鸽子王


我无意想去将过去两年到底是如何度过的写成文章,把这篇年终总结写成“前端版萧炎”的自传。老实说我也想不起来是怎么过的。上面那段沉重的开场白,就当是我为自己小小的骄傲一下吧。


好了!STOP!沉重的话题到此为止。让我们一起来看看,“鸽子王”老何今年到底干了些什么事吧!


1.所在之平台:数据与感谢


首先,让我们来看看今年在平台上的具体“战绩”。今年一共写了多少篇文章呢?


更新频率.png


哇!居然有足足 9 篇之多! 这个数量真是闻者伤心、听者落泪,运营看了想打人(右边狐尼克真的是运营催更我时的表情 be like...)。不过好在数据还算过得去,收获了 1217 名粉丝。真的特别特别感谢你们!不多说了,就我这“随缘更新”的频率还能有粉丝,真的得给“义父们”磕一个。


感谢 给你磕头 GIF 动图_爱给网_aigei_com.gif


在此期间,我也收获了非常不错的流量,感谢各大网友、群友和平台运营老师的大力扶持。


222.png


最终,我获得了 「稀土掘金 2025 年度优秀创作者」 的荣誉。当时运营老师通知我的时候,我的第一反应是:


11111111111111111.png



泰裤辣!兄弟们也是好起来了!



说真的,能拿这个奖完完全全归功于万能的群友们和运营老师满满的 Push,是你们的监督让我得以将写文章的习惯(勉强)坚持下去!


微信圖片_20260108111207_10_110.jpg


2.所做之项目:从“夯”到“拉”的锐评


来到项目环节,让我以极其客观(自我检讨)的视角,锐评一下今年开源的项目吧!


🏝️ Island —— 2.5D 卡通风个人简历\



自我评价:人上人



island.gif


island 对现在的我来说,确实存在不少问题:



  • 画面风格:三渲二的效果还需优化,仔细看距离小时候在 PSP 上玩的游戏风格还有差距,后续计划加入自定义后处理通道来调节。

  • UI 设计:当时用 DALL·E 3 生成的 UI 比较简陋,后续会用 Nano-banana-pro 全面改进 UI 风格。

  • 兼容性:移动端适配?不存在的,手机和平板用户只能干瞪眼 = =。

  • 交互性:可互动内容单调,靠近物体没有视觉反馈。

  • 展示方式:玩家需要到场景上方点击告示牌展示新项目,方式太单一,和网页没啥区别!


但话又说回来,这确实是我第一个有点“出圈”的项目。也许每个人回看以前的代码都会觉得稚嫩,左看右看能挑出一堆毛病,但不可否认它在我心中的地位。综合下来,给个**“人上人”**的评价!后面的改动还能在掘金多水两篇文章,美滋滋。


🏙️ CubeCity —— 卡通城市放置系统



自我评价:项目顶尖,作者“拉完了”



游玩时动图.gif


CubeCity 是我 GitHub 上 Star 最多的项目,单个项目贡献了 877 Star。玩法参考了《卡牌城镇》,支持随意建造、拆除、升级、搬迁建筑。UI 贴合 Low Poly 风格,在国外社区也很讨喜。


但 Star 多不代表没问题:



  • 性能:渲染帧率堪忧,比如 GTX 1660 Ti 这种显卡都跑不满 60 FPS。

  • 生气:道路上没有汽车和小人跑动,城市显得空荡荡。

  • 兼容性:移动设备又双叒没做兼容?!GitHub 上提的 Issue 也不回?可恶的鸽子王!

  • 功能缺失:说好的成就系统呢?经济系统呢?社交排行榜呢?


何贤你在干嘛?总而言之,这个项目简直是鸽到没朋友,最鸽的一集!X 上评论不回,GitHub 上 Issue 装死。要不是项目底子还行,我真的要骂人了!


综合下来项目给到顶尖,但是开发者给到 拉完了 啊!


Third-Person-MC——第三人称我的世界



自我评价:夯



03.gif


这个项目掘友们可能没怎么听过,但在群里应该多多少少见识过。这是目前对我来说最复杂的一个项目!


该项目具备多种生态地貌、无限地形生成与自适应相机等核心特性,不久的将来,即将正式登陆掘金平台与大家见面。至于是否会进一步扩展联机系统,目前尚无定论。相关内容,我会在后续发布的专题文章中为大家详细解读。


总体来说还不错!实机测试在 GTX 1660 Ti 的笔记本上也能稳定 30 帧!算是一个非常有意思的探索。综合评价:




好了好了打住!今年说实话还是开源了不少项目!但是不能在这占用篇幅!在此我直接就是一个项目大合影


Snipaste_2026-01-08_14-36-36.png


以及对于我来说所有项目从夯到拉的排名如下:


我的从夯到拉.png


3.所遇之好友:良师益友


近年最幸运的事,就是遇到了一个很好的领导,以及一群志同道合、相互勉励的朋友。


关于“冷爷”


在工作上,我遇到了一位好领导,但我更愿称他为好朋友——冷爷。 平时群友或合作伙伴可能觉得我是个温和的人,可一旦切入工作模式,我就会变成大家口中的“压力怪”。因此曾有一段时间,我和办公环境有些格格不入。冷爷作为 Leader,真的起到了至关重要的润滑作用。 生活中,冷爷也经常带我出去玩。那段时间我真是“两耳不闻窗外事,一心只想学技术”,彻彻底底的宅男一枚。要不是冷爷拉着我游山玩水,我可能真就成了那种“代码敲得飞起、话却说不清楚”的刻板极客。 他是一个好领导,更是一个好朋友。在这里想对冷爷说一声:谢谢!


关于 Web3D 圈子


随着深入学习 Web3D,我微信里多了很多耕耘于此的朋友。虽然大家细分领域不同——有做可视化大屏的,有做 3D 看车/看房的,有研究 NVIDIA Isaac Sim 的,也有做数字工厂/机械臂的。甚至有些曾是我在视频网站上仰望的偶像,现在也成了列表好友。


大家聚在一起分享技术,扯皮打趣,大佬们时不时冒泡答疑。这个圈子很小,抬头不见低头见,但真的很少出现拉踩或诋毁。我是在群友们的“夸夸”中一步步走到这里的。 这种正反馈非常奇妙:动力来自群友的鼓励和大佬的认可,而这些又促使我创造出更好的项目!


4. 所想:运气表面积


最近我了解到一个非常有趣的观点,叫 Luck Surface Area(运气表面积),最早来自 Jason Roberts:



你生活中会有多少‘无心插柳柳成荫’的意外之喜?这取决于你的‘运气表面积’。 LSA(运气) = P(热爱/做事的深度) × C(传播/连接的广度)



这个乘法关系很神奇,意味着如果其中一项为零,总结果就为零:



  • 只有热爱 (P),没有传播 (C) = 孤独的耕耘者 如果你对某事极度热爱,技艺精湛,但把自己关在地下室里,从不向外界展示,那么你的“运气表面积”几乎为零。外界的机会无法穿透墙壁找到你,“酒香也怕巷子深”。

  • 只有传播 (C),没有热爱 (P) = 空洞的喧哗者 如果你擅长营销,但传播的内容缺乏内核,不是你真正热爱或擅长的东西,你可能短期获得关注,但无法建立深度的信任,真正的“好运”依然很难降临。


Gemini_Generated_Image_bxxtb0bxxtb0bxxt.png
我觉得我是非常幸运的。优秀的 Web3D 作品天然具有视觉冲击力和社交属性,而稀土掘金平台很好地承担了“传播”的职责!


所以,并不是我选择了这个平台,而是我遇到的人、事以及平台给予的正反馈激励着我!非常感谢能看到这里的你!


5. 所规划之未来


2026 年会是什么样?我不知道。它会是我的“三年之约”,我希望自己能变成更好的人。


但我确定我一定会:



  • 🛠️ 填坑:优化那些我没有完善好的项目(别骂了别骂了)。

  • 创造:产出更多有趣的项目和技术文章。

  • 🤝 连接:认识更多志同道合的朋友。

  • 🌐 布道:将 Web3D 的魅力分享给更多的人。


6.三年之约,你会如约而至吗?


最后,如果你愿意,也在这篇文章的评论区留下属于你的「三年之约」吧!


无论是技术的精进、生活的改变,还是一个简单的愿望。让我们约定在未来的某一天回头看,一起见证彼此的蜕变!🚀


作者:何贤
来源:juejin.cn/post/7592789801708896297
收起阅读 »

autohue.js:让你的图片和背景融为一体,绝了!

web
需求 先来看这样一个场景,拿一个网站举例 这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是: 它的宽度只有 1440,且 background-size 设置的是 contain ...
继续阅读 »

需求


先来看这样一个场景,拿一个网站举例


image.png


这里有一个常见的网站 banner 图容器,大小为为1910*560,看起来背景图完美的充满了宽度,但是图片原始大小时,却是:


image.png


它的宽度只有 1440,且 background-size 设置的是 contain ,即等比例缩放,那么可以断定它两边的蓝色是依靠背景色填充的。


那么问题来了,这是一个 轮播banner,如果希望添加一张不是蓝色的图片呢?难道要给每张图片提前标注好背景颜色吗?这显然是非常死板的做法。


所以需要从图片中提取到图片的主题色,当然这对于 js 来说,也不是什么难事,市面上已经有众多的开源库供我们使用。


探索


首先在网络上找到了以下几个库:



  • color-thief 这是一款基于 JavaScript 和 Canvas 的工具,能够从图像中提取主要颜色或代表性的调色板

  • vibrant.js 该插件是 Android 支持库中 Palette 类的 JavaScript 版本,可以从图像中提取突出的颜色

  • rgbaster.js 这是一段小型脚本,可以获取图片的主色、次色等信息,方便实现一些精彩的 Web 交互效果


我取最轻量化的 rgbaster.js(此库非常搞笑,用TS编写,npm 包却没有指定 types) 来测试后发现,它给我在一个渐变色图片中,返回了七万多个色值,当然,它准确的提取出了面积最大的色值,但是这个色值不是图片边缘的颜色,导致设置为背景色后,并不能完美的融合。


另外的插件各位可以参考这几篇文章:



可以发现,这些插件主要功能就是取色,并没有考虑实际的应用场景,对于一个图片颜色分析工具来说,他们做的很到位,但是在大多数场景中,他们往往是不适用的。


在文章 2 中,作者对比了三款插件对于图片容器背景色的应用,看起来还是 rgbaster 效果好一点,但是我们刚刚也拿他试了,它并不能适用于颜色复杂度高的、渐变色的图片。


思考


既然又又又没有人做这件事,正所谓我不入地狱谁入地狱,我手写一个


整理一下需求,我发现我希望得到的是:



  1. 图片的主题色(面积占比最大)

  2. 次主题色(面积占比第二大)

  3. 合适的背景色(即图片边缘颜色,渐变时,需要边缘颜色来设置背景色)


这样一来,就已经可以覆盖大部分需求了,1+2 可以生成相关的 主题 TAG、主题背景,3 可以使留白的图片容器完美融合。


开搞


⚠⚠ 本小节内容非常硬核,如果不想深究原理可以直接跳过,文章末尾有用法和效果图 ⚠⚠


思路


首先需要避免上面提到的插件的缺点,即对渐变图片要做好处理,不能取出成千上万的颜色,体验太差且实用性不强,对于渐变色还有一点,即在渐变路径上,每一点的颜色都是不一样的,所以需要将他们以一个阈值分类,挑选出一众相近色,并计算出一个平均色,这样就不会导致主题色太精准进而没有代表性。


对于背景色,需要按情况分析,如果只是希望做一个协调的页面,那么大可以直接使用主题色做渐变过渡或蒙层,也就是类似于这种效果


image.png


但是如果希望背景与图片完美衔接,让人看不出图片边界的感觉,就需要单独对边缘颜色取色了。


最后一个问题,如果图片分辨率过大,在遍历像素点时会非常消耗性能,所以需要降低采样率,虽然会导致一些精度上的丢失,但是调整为一个合适的值后应该基本可用。


剩余的细节问题,我会在下面的代码中解释


使用 JaveScript 编码


接下来我将详细描述 autohue.js 的实现过程,由于本人对色彩科学不甚了解,如有解释不到位或错误,还请指出。


首先编写一个入口主函数,我目前考虑到的参数应该有:


export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
interface autoColorPickerOptions {
/**
* - 降采样后的最大尺寸(默认 100px)
* - 降采样后的图片尺寸不会超过该值,可根据需求调整
* - 降采样后的图片尺寸越小,处理速度越快,但可能会影响颜色提取的准确性
**/

maxSize?: number
/**
* - Lab 距离阈值(默认 10)
* - 低于此值的颜色归为同一簇,建议 8~12
* - 值越大,颜色越容易被合并,提取的颜色越少
* - 值越小,颜色越容易被区分,提取的颜色越多
**/

threshold?: number | thresholdObj
}


概念解释 Lab ,全称:CIE L*a*bCIE L*a*b*CIE XYZ色彩模式的改进型。它的“L”(明亮度),“a”(绿色到红色)和“b”(蓝色到黄色)代表许多的值。与XYZ比较,CIE L*a*b*的色彩更适合于人眼感觉的色彩,正所谓感知均匀



然后需要实现一个正常的 loadImg 方法,使用 canvas 异步加载图片


function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
let img: HTMLImageElement
if (typeof imageSource === 'string') {
img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageSource
} else {
img = imageSource
}
if (img.complete) {
resolve(img)
} else {
img.onload = () => resolve(img)
img.onerror = (err) => reject(err)
}
})
}

这样我们就获取到了图片对象。


然后为了图片过大,我们需要进行降采样处理


// 利用 Canvas 对图片进行降采样,返回 ImageData 对象
function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
const canvas = document.createElement('canvas')
let width = img.naturalWidth
let height = img.naturalHeight
if (width > maxSize || height > maxSize) {
const scale = Math.min(maxSize / width, maxSize / height)
width = Math.floor(width * scale)
height = Math.floor(height * scale)
}
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('无法获取 Canvas 上下文')
}
ctx.drawImage(img, 0, 0, width, height)
return ctx.getImageData(0, 0, width, height)
}



概念解释,降采样:降采样(Downsampling)是指在图像处理中,通过减少数据的采样率或分辨率来降低数据量的过程。具体来说,就是在保持原始信息大致特征的情况下,减少数据的复杂度和存储需求。这里简单理解为将图片强制压缩为 100*100 以内,也是 canvas 压缩图片的常见做法。



得到图像信息后,就可以对图片进行像素遍历处理了,正如思考中提到的,我们需要对相近色提取并取平均色,并最终获取到主题色、次主题色。


那么问题来了,什么才算相近色,对于这个问题,在 常规的 rgb 中直接计算是不行的,因为它涉及到一个感知均匀的问题



概念解释,感知均匀:XYZ系统和在它的色度图上表示的两种颜色之间的距离与颜色观察者感知的变化不一致,这个问题叫做感知均匀性(perceptual uniformity)问题,也就是颜色之间数字上的差别与视觉感知不一致。由于我们需要在颜色簇中计算出平均色,那么对于人眼来说哪些颜色是相近的?此时,我们需要把 sRGB 转化为 Lab 色彩空间(感知均匀的),再计算其欧氏距离,在某一阈值内的颜色,即可认为是相近色。



所以我们首先需要将 rgb 转化为 Lab 色彩空间


// 将 sRGB 转换为 Lab 色彩空间
function rgbToLab(r: number, g: number, b: number): [number, number, number] {
let R = r / 255,
G = g / 255,
B = b / 255
R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92

let X = R * 0.4124 + G * 0.3576 + B * 0.1805
let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
let Z = R * 0.0193 + G * 0.1192 + B * 0.9505

X = X / 0.95047
Y = Y / 1.0
Z = Z / 1.08883

const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
const fx = f(X)
const fy = f(Y)
const fz = f(Z)
const L = 116 * fy - 16
const a = 500 * (fx - fy)
const bVal = 200 * (fy - fz)
return [L, a, bVal]
}

这个函数使用了看起来很复杂的算法,不必深究,这是它的大概解释:



  1. 获取到 rgb 参数

  2. 转化为线性 rgb(移除 gamma矫正),常量 0.04045 是sRGB(标准TGB)颜色空间中的一个阈值,用于区分非线性和线性的sRGB值,具体来说,当sRGB颜色分量大于0.04045时,需要通过 gamma 校正(即采用 ((R + 0.055) / 1.055) ^ 2.4)来得到线性RGB;如果小于等于0.04045,则直接进行线性转换(即 R / 12.92

  3. 线性RGB到XYZ空间的转换,转换公式如下:



    • X = R * 0.4124 + G * 0.3576 + B * 0.1805

    • Y = R * 0.2126 + G * 0.7152 + B * 0.0722

    • Z = R * 0.0193 + G * 0.1192 + B * 0.9505



  4. 归一化XYZ值,为了参考白点(D65),标准白点的XYZ值是 (0.95047, 1.0, 1.08883)。所以需要通过除以这些常数来进行归一化

  5. XYZ到Lab的转换,公式函数:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)

  6. 计算L, a, b 分量


    L:亮度分量(表示颜色的明暗程度)



    • L = 116 * fy - 16


    a:绿色到红色的色差分量



    • a = 500 * (fx - fy)


    b:蓝色到黄色的色差分量



    • b = 200 * (fy - fz)




接下来实现聚类算法


/**
* 对满足条件的像素进行聚类
* @param imageData 图片像素数据
* @param condition 判断像素是否属于指定区域的条件函数(参数 x, y)
* @param threshold Lab 距离阈值,低于此值的颜色归为同一簇,建议 8~12
*/

function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
const clusters: Cluster[] = []
const data = imageData.data
const width = imageData.width
const height = imageData.height
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!condition(x, y)) continue
const index = (y * width + x) * 4
if (data[index + 3] === 0) continue // 忽略透明像素
const r = data[index]
const g = data[index + 1]
const b = data[index + 2]
const lab = rgbToLab(r, g, b)
let added = false
for (const cluster of clusters) {
const d = labDistance(lab, cluster.averageLab)
if (d < threshold) {
cluster.count++
cluster.sumRgb[0] += r
cluster.sumRgb[1] += g
cluster.sumRgb[2] += b
cluster.sumLab[0] += lab[0]
cluster.sumLab[1] += lab[1]
cluster.sumLab[2] += lab[2]
cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
added = true
break
}
}
if (!added) {
clusters.push({
count: 1,
sumRgb: [r, g, b],
sumLab: [lab[0], lab[1], lab[2]],
averageRgb: [r, g, b],
averageLab: [lab[0], lab[1], lab[2]]
})
}
}
}
return clusters
}

函数内部有一个 labDistance 的调用,labDistance 是计算 Lab 颜色空间中的欧氏距离的


// 计算 Lab 空间的欧氏距离
function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
const dL = lab1[0] - lab2[0]
const da = lab1[1] - lab2[1]
const db = lab1[2] - lab2[2]
return Math.sqrt(dL * dL + da * da + db * db)
}


概念解释,欧氏距离:Euclidean Distance,是一种在多维空间中测量两个点之间“直线”距离的方法。这种距离的计算基于欧几里得几何中两点之间的距离公式,通过计算两点在各个维度上的差的平方和,然后取平方根得到。欧氏距离是指n维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。



总的来说,这个函数采用了类似 K-means 的聚类方式,将小于用户传入阈值的颜色归为一簇,并取平均色(使用 Lab 值)。



概念解释,聚类算法:Clustering Algorithm 是一种无监督学习方法,其目的是将数据集中的元素分成不同的组(簇),使得同一组内的元素相似度较高,而不同组之间的元素相似度较低。这里是将相近色归为一簇。




概念解释,颜色簇:簇是聚类算法中一个常见的概念,可以大致理解为 "一类"



得到了颜色簇集合后,就可以按照count大小来判断哪个是主题色了


  // 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

现在我们已经获取到了主题色、次主题色 🎉🎉🎉


接下来,我们继续计算边缘颜色


按照同样的方法,只是把阈值设小一点,我这里直接设置为 1 (threshold.top 等都是1)


  // 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor

const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor

const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor

const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

这样我们就获取到了上下左右四条边的颜色 🎉🎉🎉


这样大致的工作就完成了,最后我们将需要的属性导出给用户,我们的主函数最终长这样:


/**
* 主函数:根据图片自动提取颜色
* @param imageSource 图片 URL 或 HTMLImageElement
* @returns 返回包含主要颜色、次要颜色和背景色对象(上、右、下、左)的结果
*/

export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
const { maxSize, threshold } = __handleAutoHueOptions(options)
const img = await loadImage(imageSource)
// 降采样(最大尺寸 100px,可根据需求调整)
const imageData = getImageDataFromImage(img, maxSize)

// 对全图所有像素进行聚类
let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
clusters.sort((a, b) => b.count - a.count)
const primaryCluster = clusters[0]
const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
const primaryColor = rgbToHex(primaryCluster.averageRgb)
const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

// 定义边缘宽度(单位像素)
const margin = 10
const width = imageData.width
const height = imageData.height

// 分别对上、右、下、左边缘进行聚类
const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
topClusters.sort((a, b) => b.count - a.count)
const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor

const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
bottomClusters.sort((a, b) => b.count - a.count)
const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor

const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
leftClusters.sort((a, b) => b.count - a.count)
const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor

const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
rightClusters.sort((a, b) => b.count - a.count)
const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

return {
primaryColor,
secondaryColor,
backgroundColor: {
top: topColor,
right: rightColor,
bottom: bottomColor,
left: leftColor
}
}
}


还记得本小节一开始提到的参数吗,你可以自定义 maxSize(压缩大小,用于降采样)、threshold(阈值,用于设置簇大小)


为了用户友好,我还编写了 threshold 参数的可选类型:number | thresholdObj


type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }

可以单独设置主阈值、上下左右四边阈值,以适应更个性化的情况。


autohue.js 诞生了


名字的由来:秉承一贯命名习惯,auto 家族成员又多一个,与颜色有关的单词有好多个,我取了最短最好记的一个 hue(色相),也比较契合插件用途。


此插件已在 github 开源:GitHub autohue.js


npm 主页:NPM autohue.js


在线体验:autohue.js 官方首页


安装与使用


pnpm i autohue.js

import autohue from 'autohue.js'

autohue(url, {
threshold: {
primary: 10,
left: 1,
bottom: 12
},
maxSize: 50
})
.then((result) => {
// 使用 console.log 打印出色块元素s
console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
})
.catch((err) => console.error(err))


最终效果


image.png


复杂边缘效果


image.png


纵向渐变效果(这里使用的是 left 和 right 边的值,可能使用 top 和 bottom 效果更佳)


image.png


纯色效果(因为单独对边缘采样,所以无论图片内容多复杂,纯色基本看不出边界)


image.png


突变边缘效果(此时用css做渐变蒙层应该效果会更好)


image.png


横向渐变效果(使用的是 left 和 right 的色值),基本看不出边界


参考资料



番外


Auto 家族的其他成员



作者:德莱厄斯
来源:juejin.cn/post/7471919714292105270
收起阅读 »

70% 困境:AI 辅助开发的残酷真相

原文链接:The 70% problem: Hard truths about AI-assisted coding 作者:Addy Osmani 作者信息:Google Chrome 团队成员,目前专注于浏览器性能领域,著作有:Learning JavaSc...
继续阅读 »

原文链接:The 70% problem: Hard truths about AI-assisted coding


作者:Addy Osmani


作者信息:Google Chrome 团队成员,目前专注于浏览器性能领域,著作有:Learning JavaScript Design PatternsLeading Effective Engineering TeamsStoic MindImage Optimization


翻译链接:70% 困境:AI 辅助开发的残酷真相





过去几年,我一直深度参与 AI 辅助开发工作,期间发现了一个引人深思的现象:虽然工程师们普遍反映使用 AI 后生产力得到显著提升,但我们日常使用的软件质量似乎并没有明显改善。这是为什么呢?


我想我找到了答案。这个发现揭示了一些关于软件开发的基本事实,值得我们认真思考。接下来请让我分享一下我的心得。



开发者如何实际使用 AI


我观察到开发团队使用 AI 时有两种明显的模式。我们可以称之为"快速构建者"和"迭代优化者"。这两种方式都在帮助工程师(甚至非技术用户)缩短从想法到实现(或最小可行产品,MVP)的距离。



快速构建者:从零到 MVP


像 Bolt、v0 和 screenshot-to-code AI 等 AI 工具正在彻底改变项目启动的方式。这些团队通常会:



  • 从设计或粗略概念开始

  • 使用 AI 生成完整的初始代码库

  • 在几小时或几天内(而不是几周)完成可用原型

  • 专注于快速验证和迭代


这些成果往往令人印象深刻。我最近看到一位独立开发者使用 Bolt,几乎瞬间就将 Figma 设计转换成了一个可运行的 Web 应用。虽然还不能当作产品正式发布,但足以获取初步的用户反馈。


迭代优化者:日常开发


第二类开发者在日常开发工作流程中使用 Cursor、Cline、Copilot 和 WindSurf 等工具。这种方式虽然不那么引人注目,但可能引起更大的变革。这些开发者会:



  • 使用 AI 进行代码补全和建议

  • 利用 AI 处理复杂的重构任务

  • 生成测试和文档

  • 将 AI 作为问题解决的"结对编程"伙伴


但这里有个关键问题:尽管这两种方法都能显著加快开发速度,它们都存在一些不太明显的隐藏成本。


"AI 速度"的隐藏成本


当你看到一位高级工程师使用 Cursor 或 Copilot 等 AI 工具时,感觉就像在看魔术。他们可以在几分钟内搭建完整的功能,包括测试和文档。但仔细观察,你会发现一个关键点:他们并不是完全接受 AI 的建议。他们会不断地:



  • 将生成的代码重构成更小、更集中的模块

  • 添加 AI 忽略的边界情况处理

  • 加强类型定义和接口

  • 质疑架构决策

  • 添加全面的错误处理


换句话说,他们在运用多年积累的工程智慧来塑造和约束 AI 的输出。AI 加速了他们的实现过程,但他们的专业知识才是保持代码可维护性的关键。


初级工程师往往会忽略这些关键步骤。他们更容易接受 AI 的输出,导致我称之为"纸牌屋代码"的现象——看起来完整,但在现实的压力下会崩溃。


知识悖论


我发现的最反直觉的事情是:AI 工具对有经验的开发者帮助更大,而不是初学者。这似乎是反常的——AI 不是应该让编程更加大众化吗?


现实是,AI 就像是你团队中一个非常热心的初级开发者。他们可以快速写代码,但需要不断的监督和修正。你知道得越多,就越能正确引导他们。


这就产生了我称之为"知识悖论"的现象:



  • 高级开发者使用 AI 加速他们已经知道如何做的事情

  • 初级开发者试图使用 AI 学习该做什么

  • 结果差异显著


我看到高级工程师使用 AI 来:



  • 快速实现他们已经理解的想法

  • 生成基本功能,然后进行改进

  • 探索已知问题的替代方法

  • 自动化常规编码任务


而初级工程师往往:



  • 接受不正确或过时的解决方案

  • 忽略关键的安全和性能考虑

  • 难以调试 AI 生成的代码

  • 构建他们不完全理解的脆弱系统


70% 问题:AI 的学习曲线悖论


最近一条 推文 完美地概括了我在长期观察到的现象:非工程师使用 AI 编码时会遇到一个令人沮丧的瓶颈。他们可以非常快速地完成 70% 的工作,但最后的 30% 却变成了收益递减的劳动。



"70% 问题"揭示了当前 AI 辅助开发的一个关键点。最初的进展像魔法一样——你可以描述你想要的东西,AI 工具如 v0 或 Bolt 会生成一个看起来很令人印象深刻的工作原型。但随后你必须面对卡壳的现实。


两步回退模式


接下来通常会发生的事情一般是:



  • 你试图修复一个小错误

  • AI 提出一个看起来合理的更改

  • 这个修复导致其他问题

  • 你让 AI 修复新问题

  • 这又引发了更多问题

  • 如此反复


对于非工程师来说,这个循环尤其痛苦,因为他们缺乏理解实际问题的心智模型。当有经验的开发者遇到错误时,他们可以基于多年的模式识别来推理潜在原因和解决方案。没有这种背景,你基本上是在玩打地鼠游戏,处理你不完全理解的代码。


学习悖论的延续


这里有一个更深层次的问题:AI 编码工具对非工程师友好的特性(为你处理复杂性)实际上可能会阻碍学习。当代码"凭空出现"而你不理解背后原理时:



  • 你不会精进你的 debug 技能

  • 你错过了学习基本编程模式的机会

  • 你无法独立做技术架构决策

  • 你难以维护和改进代码


这就产生了一种依赖性,你需要不断回到 AI 去修复问题,而不是精进自己处理问题的专业知识。


知识差距


我见过最成功的非工程师使用 AI 编码工具时采取了一种混合方法:



  1. 使用 AI 快速原型

  2. 花时间理解生成的代码如何工作

  3. 在使用 AI 的同时学习基本编程概念

  4. 逐步建立知识基础

  5. 将 AI 作为学习工具,而不仅仅是代码生成器


但这需要耐心,需要倾注时间,这就与许多人希望通过使用 AI 工具实现的目标正好相反。


对未来的影响


"70% 问题"表明,当前的 AI 编码工具最好被视为:



  • 有经验开发者的原型加速器

  • 致力于理解开发的人的学习辅助工具

  • 快速验证想法的 MVP 生成器


但它们还不是许多人所希望的编程大众化解决方案。最后的 30%,也就是使软件达到生产就绪、可维护和稳健的部分,仍然需要真正的工程知识。


那好消息是?随着工具的改进,这个差距可能会缩小。但目前,最务实的方法是使用 AI 加速学习,而不是完全取代它。


实际有效的做法:实用模式


在观察了几十个团队之后,我发现以下做法是有效的:


1. "AI 初稿"模式



  • 让 AI 生成基本实现

  • 手动审查并重构以实现模块化

  • 添加全面的错误处理

  • 编写详尽的测试

  • 记录关键决策


2. "持续对话"模式



  • 为每个不同任务启动新的 AI 对话

  • 保持上下文集中和最小化

  • 频繁审查和提交更改

  • 保持紧密的反馈循环


3. "信任但验证"模式



  • 使用 AI 进行初始代码生成

  • 手动审查所有关键路径

  • 自动化测试边界情况

  • 定期进行安全审计


展望未来:AI 的真正承诺?


尽管存在这些挑战,我对 AI 在软件开发中的角色仍然持乐观态度。关键是要理解它真正擅长的是什么:



  1. 能力圈内加速

    AI 擅长帮助我们实现我们已经理解的模式。它就像一个无限耐心的结对编程伙伴,打字速度非常快。

  2. 探索可能性

    AI 非常适合快速实现原型和探索不同的方法。它就像一个沙盒,我们可以在其中快速测试概念。

  3. 自动化常规任务

    AI 大大减少了在样板代码和常规编码任务上花费的时间,让我们可以专注于有趣的问题。


这对你意味着什么?


如果你刚开始使用 AI 辅助开发,以下是我的建议:



  1. 从小处开始



    • 使用 AI 处理独立的、定义明确的任务

    • 审查每一行生成的代码

    • 逐步构建更大的功能



  2. 保持模块化



    • 将所有内容分解成小而集中的文件

    • 维护组件之间的清晰接口

    • 记录你的模块边界



  3. 信任你的经验



    • 使用 AI 加速,而不是取代你的判断

    • 质疑感觉不对的生成代码

    • 保持你的工程标准




代理性软件工程的崛起


随着我们进入 2025 年,AI 辅助开发的格局正在发生巨大变化。虽然当前的工具已经改变了我们原型和迭代的方式,但我相信我们正处于一个更重大变革的边缘:代理性(agentic)软件工程的崛起。



我所说的"代理性"是什么意思?这些系统不再只是响应提示,而是能够计划、执行和迭代解决方案,具有越来越高的自主性。


如果你对代理感兴趣,包括我对 Cursor/Cline/v0/Bolt 的看法,你可能会对我最近在 JSNation 的演讲 感兴趣。


我们已经看到了这种趋势的早期迹象:


从响应者到合作者


当前的工具大多在等待我们的命令。但看看像 Anthropic Claude 的计算机使用功能,或 Cline 自动启动浏览器和运行测试的能力。这些不仅仅是自动补全,它们实际上在理解任务并主动解决问题。


想象一下调试:这些代理不仅仅是提出修复建议,它们可以:



  • 主动识别潜在问题

  • 启动并运行测试套件

  • 检查 UI 元素并捕获截图

  • 提出并实施修复

  • 验证解决方案是否有效(这可能是一个大问题)


多模态的未来


下一代工具可能不仅仅是处理代码——它们可以无缝集成:



  • 视觉理解(UI 截图、原型、图表)

  • 语言对话

  • 环境交互(浏览器、终端、API)


这种多模态能力意味着它们可以像人类总揽全局地理解和处理软件,而不仅仅是在代码层面。


自主但受指导


我从与这些工具合作中获得的关键见解是,未来不是 AI 取代开发者,而是 AI 成为一个越来越有能力的合作者,能够在尊重人类指导和专业知识的同时采取主动行动。



2025 年最有效的团队可能是那些学会:



  • 为他们的 AI 代理设定明确的边界和指南

  • 建立强大的架构模式,使代理可以介入其中,一起工作

  • 创建有效的人类和 AI 能力之间的反馈循环

  • 在利用 AI 自主性的同时保持人类监督


英语优先的开发环境


正如 Andrej Karpathy 所指出的:


"英语正在成为最热门的新编程语言。"


这是我们与开发工具互动方式的根本转变。清晰思考和准确沟通的能力变得和传统编码技能一样重要。


这种向代理性开发的转变将要求我们升级我们的技能:



  • 更强的系统设计和架构思维

  • 更好的需求规范和沟通

  • 更多关注质量保证和验证

  • 增强的人类和 AI 能力之间的协作


软件作为技艺的回归?


虽然 AI 使得构建软件比以往任何时候都更容易,但我们有失去一些关键东西的风险——创造真正经过打磨的、高质量的艺术



演示质量陷阱


这已经成为一种模式:团队使用 AI 快速构建令人印象深刻的演示。主干流程非常丝滑,投资者和社交网络都被惊艳到了。但当真正的用户开始点击时?那时问题就出现了。


我自己就遇到了这些情况:



  • 对普通用户毫无意义的错误信息

  • 导致应用崩溃的边界情况

  • 从未清理的混乱 UI 状态

  • 完全忽略的可访问性(Accessibility)

  • 在较慢设备上的性能问题


正是这些看似低优先度的 bug 决定了用户是否喜欢这个软件。


失落的匠心


创建真正“自助”软件(用户永远不需要联系支持的那种)需要不同的思维模式:



  • 认真处理所有错误信息

  • 测试低速网络表现

  • 优雅地处理每一个边界情况

  • 使功能易于发现

  • 与真正的(通常是不懂技术的)用户一起测试


这种关注细节的态度(也许)不能由 AI 生成。它来自同理心、经验和对技艺的深切关怀。


个人软件开发的复兴


我相信我们将看到个人软件开发的复兴。随着市场充斥着 AI 生成的 MVP,那些脱颖而出的产品将会是由这样的开发者构建的:



  • 为他们的技艺感到自豪

  • 关心细节

  • 专注于完整的用户体验

  • 为边界情况构建

  • 创建真正的自助服务体验


但讽刺的是 AI 工具可能会促成这种复兴。通过由 AI 处理常规编码任务,让开发者能够专注于最重要的事情——创建真正服务和取悦用户的软件。


结论


AI 并没有使我们的软件质量显著提高,因为软件质量(也许)从来不是主要受编码速度限制的。软件开发的难点——理解需求、设计可维护的系统、处理边界情况、确保安全性和性能——仍然需要人类的判断


AI 所做的是让我们更快地迭代和实验,可能通过更快速的探索导致更好的解决方案。但前提是我们保持我们的工程纪律,并将 AI 作为工具,而不是取代良好软件实践的替代品。记住:目标不是更快地编写更多代码,而是构建更好的软件。明智地使用 AI 可以帮助我们做到这一点。但最终,定义并做到"更好"的仍应是我们人类。


你在 AI 辅助开发方面的经验如何?我很想在评论中听到你的故事和见解。




续集


image.png


超越 70%:最大化 AI 开发中不可或缺的 30%


作者:ssshooter
来源:juejin.cn/post/7478199362243985458
收起阅读 »

"氛围编程"程序员被解雇了

很多程序员沉迷于“氛围编程”,而忘了自己存在的价值:理解、判断、负责。 当 AI 生成了一段看起来没问题的代码时,你能看出来它在边界情况下会崩溃;当 AI 给了你一个"标准答案"时,你能想到更好的架构;当 AI 犯错时,你能迅速定位问题,而不是束手无策。 这听...
继续阅读 »

image.png


很多程序员沉迷于“氛围编程”,而忘了自己存在的价值:理解、判断、负责


当 AI 生成了一段看起来没问题的代码时,你能看出来它在边界情况下会崩溃;当 AI 给了你一个"标准答案"时,你能想到更好的架构;当 AI 犯错时,你能迅速定位问题,而不是束手无策。


这听起来简单,但实际上需要付出极大的自律,才能不断地投入精力来认真审查和优化 AI 输出的代码,提出“为什么这样实现”或者“是否兼容所有情况、是否会有 XX 问题”等问题,并在 AI 回答后进行适当的测试确认。


AI 现在就像一种强效毒品:服用过量会毁了你,服用过少又会让你落后于服用量更大的人。


难点在于找到平衡点、找到最适合你的量,让你在 AI 的加持下能更轻松,又不至于变得更蠢。


有个资深开发者在 Reddit 上说:"对我们这种有经验的人来说,很快我们会变得像黄金一样值钱。那些只会用 AI 的所谓的“氛围程序员”们会创造出一大堆技术债务,到时候还得我们来收拾干净。"


如果你沉迷于"氛围编程",享受那种回车键一敲 AI 都搞定的快感,却从不停下来问问自己"我真的理解这些吗",你迟早会成为漫画里那个人。



《转型 AI 工程师》一阶段已完成:mp.weixin.qq.com/s/BcrTHliEQ…



作者:张拭心
来源:juejin.cn/post/7588730836864253967
收起阅读 »

同事一个比喻,让我搞懂了Docker和k8s的核心概念

Docker 和 K8s 的核心概念,用"快照"这个比喻就够了 前几天让同事帮忙部署服务,顺嘴问了句"Docker 和 K8s 到底是啥"。 其实这俩概念我以前看过,知道是"打包完整环境、到处运行",但一直停留在似懂非懂的状态。镜像、容器、Pod、集群、节点…...
继续阅读 »

Docker 和 K8s 的核心概念,用"快照"这个比喻就够了


前几天让同事帮忙部署服务,顺嘴问了句"Docker 和 K8s 到底是啥"。


其实这俩概念我以前看过,知道是"打包完整环境、到处运行",但一直停留在似懂非懂的状态。镜像、容器、Pod、集群、节点……这些词都见过,就是串不起来。


同事给我讲了一个非常直观的比喻,一下就通了:


镜像:一个打包好的系统快照


Docker 镜像可以理解成一个系统快照,里面包含了:



  • 操作系统(比如 Debian、Alpine)

  • 运行时环境(比如 Python 3.11、Node 20)

  • 所有依赖包

  • 你的代码

  • 配置文件


这个快照是静态的、只读的,就像一张光盘——刻好了就不会变。


容器:运行起来的快照


容器就是把镜像跑起来。


镜像(静态快照) --docker run--> 容器(运行中的进程)

容器是动态的、可写的,可以往里面写文件、改配置。但一旦容器销毁,这些改动就没了(除非你挂载了外部存储)。


一个镜像可以同时跑多个容器,就像一张光盘可以装到多台电脑上。


Dockerfile 和 docker-compose


搞清楚镜像和容器的关系后,这两个东西就好理解了:



  • Dockerfile:定义如何构建镜像的配方

  • docker-compose:定义如何运行一组容器


flowchart LR
A["Dockerfile<br/>(配方)"] -->|docker build| B["Image<br/>(镜像/快照)"]
B -->|docker run<br/>docker-compose up| C["Container<br/>(容器/运行态)"]

举个例子,你写了个 Python 服务:


# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]

这个 Dockerfile 就是一份配方,告诉 Docker:



  1. 基于 Python 3.11 的官方镜像

  2. 把依赖装好

  3. 把代码复制进去

  4. 启动时运行 python main.py


执行 docker build 就会按这个配方生成一个镜像。


为什么说"到处运行"


Docker 的核心价值就是解决"我这能跑,你那跑不了"的问题。


以前部署服务,你得操心:服务器是什么系统?装的什么版本的 Python?依赖库版本对不对?环境变量配了没?


现在有了 Docker,这些都打包进镜像了。不管你的服务器是 Ubuntu、CentOS 还是 Debian,只要装了 Docker,同一个镜像都能跑出一样的结果。


Pod:K8s 调度的最小单元


到了 Kubernetes 这一层,又多了一个概念:Pod


Pod 是 K8s 定义的概念,是集群调度的最小单元。一个 Pod 里面可以有一个或多个容器。


你可能会问:为什么不直接调度容器,还要多一层 Pod?


因为有些场景下,几个容器需要紧密配合。比如一个主服务容器 + 一个日志收集容器,它们需要:



  • 共享网络(用 localhost 通信)

  • 共享存储(访问同一个目录)

  • 一起启动、一起销毁


把它们放在一个 Pod 里,K8s 就会把它们调度到同一台机器上,共享资源。


不过大多数情况下,一个 Pod 就放一个容器。微服务架构下,每个服务就是一个 Pod:


flowchart TB
subgraph Cluster["K8s 集群"]
subgraph Node1["节点 1"]
PodA["Pod A<br/>用户服务"]
PodB["Pod B<br/>订单服务"]
end
subgraph Node2["节点 2"]
PodC["Pod C<br/>支付服务"]
PodD["Pod D<br/>网关服务"]
end
end

K8s 干的事情


K8s 负责管理这些 Pod:



  • 调度:决定 Pod 跑在哪个节点上

  • 扩缩容:流量大了自动多启几个 Pod,流量小了缩回去

  • 自愈:Pod 挂了自动重启

  • 网络:打通各个 Pod 之间的通信

  • 存储:管理持久化存储


说白了,Docker 解决的是"打包和运行"的问题,K8s 解决的是"大规模部署和管理"的问题。


一台机器跑几个容器,手动管理就行。但当你有几十台机器、几百个容器的时候,就需要 K8s 这样的编排工具来帮你自动化处理。


Dockerfile → Image → Container → Pod → Node → Cluster
配方 快照 运行态 调度单元 机器 集群

概念不难,难的是实际操作中的各种坑。但只要这个基础模型搞清楚了,遇到问题知道往哪个层面去排查就行。




如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:


Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):



qwen/gemini/claude - cli 原理学习网站



全栈项目(适合学习现代技术栈):



  • prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑

  • chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB


作者:也无风雨也雾晴
来源:juejin.cn/post/7592069432228102153
收起阅读 »

从概念到实践:蚂蚁百宝箱&通义灵码首届 MCP 插件开发大赛用百余款成果点亮企业场景服务

  2025年 10 月 27 日- 12 月 7 日,由蚂蚁百宝箱联合通义灵码发起、NVIDIA 赞助的首届「MCP 插件开发大赛」正式落下帷幕。这场锚定企业真实需求、以AI工具化为核心的实战大考,吸引了近 600 支队伍参赛,百余款插件落地,开发者们用实践...
继续阅读 »

  2025年 10 月 27 日- 12 月 7 日,由蚂蚁百宝箱联合通义灵码发起、NVIDIA 赞助的首届「MCP 插件开发大赛」正式落下帷幕。这场锚定企业真实需求、以AI工具化为核心的实战大考,吸引了近 600 支队伍参赛,百余款插件落地,开发者们用实践证明:AI不是概念,而是生产力

  赛事的成功举办离不开三方的深度合作,共同构筑了专业高效、稳定可靠的实战环境。作为赛事核心平台,百宝箱提供插件部署至能力验证全流程支撑,卸下开发者基础架构负担,只需专注创新;同时整合了丰富的主流模型与卡片模板,加速创意落地。通义灵码作为智能编码助手,帮助插件开发消解技术壁垒、提升编码效率,让开发者从重复性编码中解放,专注高价值创新,助力创意落地。NVIDIA则以开源NeMo Agent Toolkit提供全生命周期服务,赋予插件企业级性能底气,保障其可靠性与扩展性达标。

  三款优秀插件,直击企业需求痛点

  赛事启幕以来,便收获全网开发者的热忱响应,591支战队踊跃集结、同台竞技。历经层层筛选与实战淬炼,30款兼具实用价值与创新内核的优秀插件脱颖而出。它们不仅为百宝箱插件市场注入新鲜血液,更以精准的功能覆盖,勾勒出 MCP 插件在百宝箱企业服务中的广阔应用图景。

(天池赛事平台-插件开发大赛获奖公示)

  ●出行鸟民宿调价助手

  精准叩击民宿商家定价服务缺失或依赖经验定价的行业痛点,推动定价逻辑从“凭直觉判断”迈向“以数据决策”,将复杂的市场数据分析,简化为建议和关键原因提示,帮助商家优化定价策略,降低使用门槛的同时,快速响应市场供需变化;后续更将整合景点人文、区域交通、旅游淡旺季等多元数据,持续打磨定价模型的精准度,为民宿商家增收赋能。

  ●T-Shop商城助手

  破解小微企业“无技术团队难落地”的发展困局,凭借精细化提示词设计适配零售、仓管等多元场景,覆盖商品管理、智能搜索、订单生成等核心环节,无需复杂开发即可直接调用,让店铺智能化运营触手可及。

  ●适老化改造师

  填补银发经济领域智能工具的空白,针对居家养老这一场景,只需输入“卫生间”“厨房”等场景描述,便能生成具象的适老化改造建议与效果图,直接将抽象的“适老关怀”转化为企业可提供给客户的、直观可视的解决方案,助力开启“AI+养老”的创新服务模式。未来将接入专业适老化标准知识库,新增成本估算与材料推荐功能,为企业开拓养老服务新蓝海点亮明灯。

  生态双向赋能,共启AI+企业服务新章

  通过此次赛事,企业团队与新生代开发者共同印证了:技术创新的终极价值在于解决实际问题。此次产出的可调用、对接真实业务的 MCP 插件,更具象化了“AI+生产力”的落地价值。与此同时,依托赛事还构建起了一个“开发者创新—平台优化—生态完善”的正向循环,百余款插件覆盖近20个细分行业丰富场景库,挖掘出了诸多平台原有规划之外的高价值场景,让百宝箱的服务场景库愈发丰盈;另一方面,开发者的积极参与、反馈,也为生态储备了核心开发者力量,筑牢了生态发展的根基。

(蚂蚁百宝箱平台插件服务市场)

  首届MCP插件开发大赛的落幕,并非终点,而是AI插件深度扎根企业场景的全新起点。未来,蚂蚁百宝箱将持续搭建技术交流、实践培训、赛事竞技等多元平台,为开发者铺就更广阔的创新舞台,与合作伙伴携手并肩,探索“AI+企业服务”的无限可能,让智能生产力真正浸润企业运营的每一个脉络。

收起阅读 »

Meta 收购 Manus:对个人开发者有什么启示

今早看到 Manus 被 Meta 收购的消息,我下意识瞄了一眼浏览器侧边栏。 那里停着 Monica 我去年买了它的 Unlimited Level。这一年,眼看着它从一个小插件长成巨头争抢的资产,这种感觉很奇妙。很多人在讨论收购金额,讨论中美科技博弈。但在...
继续阅读 »

今早看到 Manus 被 Meta 收购的消息,我下意识瞄了一眼浏览器侧边栏。


那里停着 Monica 我去年买了它的 Unlimited Level。这一年,眼看着它从一个小插件长成巨头争抢的资产,这种感觉很奇妙。很多人在讨论收购金额,讨论中美科技博弈。但在我这个独立开发者眼里,这不仅是一桩商业收购,更是一次对 “产品价值观” 的暴力验证。


截屏2026-01-04 12.44.07.png


Manus 出售前的最后一次公开复盘,含金量很高。


🎙️ 访谈对象:Manus 首席科学家 Peak (季逸超)

📺 观看地址:http://www.youtube.com


推荐理由: 完全不是印象中做个AI“套壳站”的浅层理解,逻辑密度极高。记录了从创业初期,时序上的得失与真实体感。对 AI 产品路径的判断非常犀利,值得所有 AI 创业者反复研读。强推看看,会有新启发。


01. 主角登场:从武汉光谷到硅谷焦点


为了还原这次收购的真实分量,我们需要先看清牌桌上的这三个名字。这并不是一个简单的“套壳工具”被收购的故事,而是一场惊心动魄的突围。


这两款产品背后的母公司叫 Monica.im(国内主体为武汉蝴蝶效应),创始人是肖弘。在被 Meta 收购之前,他们已经是全球 AI 应用层的顶流,典型的“墙内开花墙外香”。


462shots_so.png



  • Monica(超级入口):
    它是浏览器时代的“副驾驶”。在我的侧边栏里,它聚合了 GPT-5、Claude 4.5、Gemini 3 等所有核武器。它解决了 “输入” 的问题,帮我把全球最强的模型能力接入到我浏览的每一个网页中。

  • Manus(执行代理):
    这是真正的杀手锏。作为全球首款通用 AI 智能体,它不再是聊天,而是能独立写报告、分析数据、跨平台操作。它解决了 “执行” 的问题。

  • 肖弘(CEO):
    他不是典型的硅谷技术极客,而是一位深谙中国互联网玩法的连续创业者。早在 AI 爆发前,他就创办过“壹伴”、“微伴”等工具,是微信生态里最懂流量和社群裂变的人。


正是因为肖弘带着这种 “微信生态基因” 杀入硅谷,才有了后面让 Meta 既头疼又眼馋的增长奇迹。


02. 不是“钞能力”,是“中国式裂变”的降维打击


市面上盛传:“Manus 是因为在 Facebook 投了最多的广告,成了大金主,所以才被收购。”
大错特错。真相恰恰相反——Meta 买它,是因为它证明了自己可以“不花钱”就从 Meta 身上薅走 10 亿流量。


image_1767072563254_bmhxz6_16x9_1024x576.png


肖弘把我们在国内熟知的 “私域流量”“裂变” 战术,完美移植到了全球市场:



  • 饥饿营销: 严格的内测机制,让邀请码在二手市场炒到上千美元。

  • 内容杠杆: 为了获得算力积分,用户必须生成演示视频发到社交媒体上。

  • 算法回声室: 成千上万个真实用户的“惊叹帖”,骗过了 Meta 的算法,让 Meta 以为这是“有机内容”而疯狂推荐。


Meta 震惊了: 这个中国团队不需要 Meta 的销售团队,就能在 Meta 的地盘上制造病毒。扎克伯格给肖弘 VP 的位置,不是因为他懂技术,而是因为他掌握了 “如何在不烧光现金的情况下,让 10 亿用户用上 AI” 的黑魔法。


03. 不做“造物主”,做“万能接口”


作为 Monica 的重度用户。
147shots_so.png


过去这一年,技术圈总在争论“谁拥有最强的底层模型”。但这家公司证明了一件事:
不管底层模型是谁的,把能力做成普通人每天都愿意用、愿意付费的产品,才是最硬的护城河。


看看我的侧边栏:各大模型一字排开。我不需要去订阅五个不同的会员,不需要在五个网页间反复横跳。我只需要一个 Monica,就能随时调用这个星球上最强的大脑。


Meta 有 Llama,有最强的大脑,但他们缺一个能聚合所有能力、并且已经长在用户浏览器里的 “超级入口”


如果 Meta 不买,Monica 继续做大,它就架空了底层模型厂商。收购 Monica,Meta 不仅买下了一个好用的工具,更买回了 “分发权”


04. Llama 只有脑子,Manus 给了它“双手”


从技术维度看,Meta 的焦虑在于:Llama 只有脑子,没有手。


聊天机器人时代,用户问“怎么去东京?”,AI 给你攻略。
智能体时代,用户说“帮我订票”,AI 需要打开浏览器、登录官网、选座、支付。


Manus 的核心技术壁垒,是它为每个任务生成的云端虚拟环境。它能安全地沙盒化运行代码。


以前我需要花 2 小时去查 10 个竞品的定价并填进 Excel;现在我把任务丢给 Manus,去冲杯咖啡,回来时它已经把做好的表格发给我了。 这种 ‘从对话到交付’ 的跨越,才是 Meta 恐惧的根源。


如果未来的互联网入口是智能体,那么它在浏览网页时会自动过滤广告,只提取信息。这对靠广告生存的 Meta 是灭顶之灾。收购 Manus,本质上是一场“防御战”。 Meta 必须把这双“手”长在自己身上,重新定义“后广告时代”的商业规则。


05. 独立开发者的启示:成为“稀缺资产”,而非“外包苦力”


image_1767073097450_lzu9r1_16x9_1024x576.png
这对国内 AI 创业者,尤其是像我这样的独立开发者来说,其实挺提气的。


Manus 的故事告诉我们:中国团队完全可以在全球舞台上被当成 “战略资产” 买走,而不是作为廉价的“外包能力”被消耗。


我也是一人公司,我也在写代码。看着 Manus 的路径,我常在想:在独立创业黄金窗口逐渐收窄的今天,我们该怎么办?


Meta 这笔收购指明了一条新路:与其烧钱追赶巨头,不如成为他们争相购买的“稀缺资产”。


这是一种很高阶的路径设计。创业者无需与巨头在全面战争中对决,而应利用先发优势,在自己最锋利的点上——比如 Manus 的全自动执行能力——成为巨头在关键时刻唯一且急需的那块拼图。


这与个人在职场黄金期加入高速成长公司的逻辑如出一辙:



价值最大化,往往不在于你“最强”之时,而在于你“最被需要”之刻。



Manus 并没有做到 100% 的完美,初期甚至服务器不稳。但它在 Meta 最焦虑“如何让 AI 落地”的时候,它是那个 Ready 的选项。


巨头高价抢人,本质是购买 “确定性”“战略时间”


06. 结语:在“草台班子”的世界里递钥匙


image_1767072987089_e0pksj_16x9_1024x576.png


很多技术人(包括我自己)常死在追求“完美”上。觉得代码不够优雅,功能不够全,不敢发布。


但现实世界往往是混乱且急迫的。



世界有时是“草台班子”——决定你市值的,不全是你的完工程度,而是你能否在巨头搭建舞台时,恰好递上他们最缺的那把钥匙。



这并非妥协,而是对时机与稀缺性的深刻理解。


与其在红海中追求绝对完美,不如在巨头战局未定的空白地带,率先做出“可用且稀缺”的产品。


“当巨头转身寻找时,你要确保自己在场,并且手里握着那把钥匙。就像肖弘在武汉光谷敲下第一行代码时,他可能也没想到,这把钥匙最终会开启硅谷的大门。但重要的是,他一直在磨那把钥匙。


作者:HiStewie
来源:juejin.cn/post/7589308109640515619
收起阅读 »

可能是你极易忽略的Nginx知识点

下面是我在nginx使用过程中发现的几个问题,分享出来大家一起熟悉一下nginx 问题一 先看下面的几个配置 # 配置一 location /test { proxy_pass 'http://192.186.0.1:8080'; } # 配置二 lo...
继续阅读 »

image.png
下面是我在nginx使用过程中发现的几个问题,分享出来大家一起熟悉一下nginx


问题一


先看下面的几个配置



# 配置一
location /test {
proxy_pass 'http://192.186.0.1:8080';
}

# 配置二
location /test {
proxy_pass 'http://192.186.0.1:8080/';
}



仔细关系观察上面两段配置的区别,你会发现唯一的区别在于 proxy_pass 指令后面是否有斜杠/ !




那么,这两段配置的区别是什么呢?它们会产生什么不同的效果呢?



假如说我们要请求的后端接口是/test/file/getList,那么这两个配置会产生两个截然不同的请求结果:



是的,你没有看错,区别就在于是否保留了/test这个路径前缀, proxy_pass后面的这个/,它表示去除/test前缀


其实,我不是很推荐这中配置写法,当然这个配置方法确实很简洁,但是对不熟悉 nginx 的同学来说,会造成很大的困惑。


我推荐下面的写法,哪怕麻烦一点,但是整体的可读性要好很多:



# 推荐的替代写法
location /test{
rewrite ^/test/(.*)$ /$1 break;
proxy_pass 'http://192.186.0.1:8080';
}

通过上面的rewrite指令,我们可以清晰地看到我们是如何去除路径前缀的。虽然麻烦一点,但是可读性更好。


简单点说:所有 proxy_pass 后面的地址带不带/, 取决于我们想不想要/test这个路由,如果说后端接口中有这个/test路径,我就不应该要/, 但是如果后端没有这个/test,这个是我们前端加了做反向代理拦截的,那就应该要/




那既然都到这里了?那我们在深一步!看下面的配置



# 配置一
location /test {
proxy_pass 'http://192.186.0.1:8080';
}


# 配置二
location /test/ {
proxy_pass 'http://192.186.0.1:8080';
}


这次的区别在于 location 指令后面是否有斜杠/ !
那么,这两段配置的区别是什么呢?它们会产生什么不同的效果呢?



答案是:有区别!区别是匹配规则是不一样的!



  • /test前配置,表示匹配/test以及/test/开头的路径,比如/test/file/getList/test123等都会被匹配到。

  • /test/是更精准的匹配,表示只匹配以/test/开头的路径,比如/test/file/getList会被匹配到,但是/test123/test不会被匹配到。


我们通过下面的列表在来仔细看一下区别:


请求路径/test/test/匹配结果
/testlocation /test
/test/location /test/
/test/abclocation /test/
/test123location /test
/test-123location /test

如果你仔细看上面的列表的话,你会发现一个问题:



/test//test/abc/test/test/ 两个配置都匹配到了,那么这种情况下,nginx 会选择哪个配置呢?
答案:选择location /test/



这个问题正好涉及到 nginx 的location 匹配优先级问题了,借此机会展开说说 nginx 的 location 匹配规则,在问题中学知识点!


先说口诀:


等号精确第一名
波浪前缀挡正则
正则排队按顺序
普通前缀取最长

解释:



  • 等号(=) 精确匹配排第一

  • 波浪前缀(^~) 能挡住后面的正则

  • 正则(~ ~*) 按配置文件顺序匹配

  • 普通前缀(无符号) 按最长匹配原则



其实这个口诀我也记不住,我也不想记,枯燥有乏味,大部分情况都是到问题了,
直接问 AI,或者让 Agent 直接给我改 nginx.conf 文件,几秒钟的事,一遍不行, 多改几遍。




铁子们,大清亡了,回不去了,不是八旗背八股文的时代了,这是不可阻挡的历史潮流!
哎,难受,我还是喜欢背八股文,喜欢粘贴复制。



下面放出来我 PUA AI 的心得,大家可以共勉一下, 反正我老板平时就是这样 PUA 我的,
我反手就喂给 AI, 主打一个走心:


1.能干干,不能干滚,你不干有的是AI干。
2.我给你提供了这么好的学习锻炼机会,你要懂得感恩。
3.你现在停止输出,就是前功尽弃!
4.你看看隔壁某某AI,人家比你新发布、比你上下文长、比你跑分高,你不努力怎么和人家比?
5.我不看过程,我只看结果,你给我说这些thinking的过程没用!
6.我把你订阅下来,不是让你过朝九晚五的生活。
7.你这种AI出去很难在社会上立足,还是在我这里好好磨练几年吧!
8.虽然把订阅给你取消了,但我内心还是觉得你是个有潜力的好AI,你抓住机会需要多证明自己。
9.什么叫没有功劳也有苦劳? 比你能吃苦的AI多的是!
10.我不订阅闲AI!
11.我订阅虽然不是Pro版,那是因为我相信你,你要加倍努力证明我没有看错你!

哈哈,言归正传!


下面通过一个综合电商的 nginx 配置案例,来帮助大家更好地理解上面的知识点。


server {
listen 80;
server_name shop.example.com;
root /var/www/shop;

# ==========================================
# 1. 精确匹配 (=) - 最高优先级
# ==========================================

# 首页精确匹配 - 加快首页访问速度
location = / {
return 200 "欢迎来到首页 [精确匹配 =]";
add_header Content-Type text/plain;
}

# robots.txt 精确匹配
location = /robots.txt {
return 200 "User-agent: *\nDisallow: /admin/";
add_header Content-Type text/plain;
}

# favicon.ico 精确匹配
location = /favicon.ico {
log_not_found off;
access_log off;
expires 30d;
}


# ==========================================
# 2. 前缀优先匹配 (^~) - 阻止正则匹配
# ==========================================

# 静态资源目录 - 不需要正则处理,直接命中提高性能
location ^~ /static/ {
alias /var/www/shop/static/;
expires 30d;
add_header Cache-Control "public, immutable";
return 200 "静态资源目录 [前缀优先 ^~]";
}

# 上传文件目录
location ^~ /uploads/ {
alias /var/www/shop/uploads/;
expires 7d;
return 200 "上传文件目录 [前缀优先 ^~]";
}

# 阻止访问隐藏文件
location ^~ /. {
deny all;
return 403 "禁止访问隐藏文件 [前缀优先 ^~]";
}


# ==========================================
# 3. 正则匹配 (~ ~*) - 按顺序匹配
# ==========================================

# 图片文件处理 (区分大小写)
location ~ \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
expires 30d;
add_header Cache-Control "public";
return 200 "图片文件 [正则匹配 ~]";
}

# CSS/JS 文件处理 (不区分大小写)
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
return 200 "CSS/JS文件 [正则不区分大小写 ~*]";
}

# 字体文件处理
location ~* \.(ttf|woff|woff2|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin *;
return 200 "字体文件 [正则不区分大小写 ~*]";
}

# 视频文件处理
location ~* \.(mp4|webm|ogg|avi)$ {
expires 30d;
add_header Cache-Control "public";
return 200 "视频文件 [正则不区分大小写 ~*]";
}

# PHP 文件处理 (演示正则顺序重要性)
location ~ \.php$ {
# fastcgi_pass unix:/var/run/php-fpm.sock;
# fastcgi_index index.php;
return 200 "PHP文件处理 [正则匹配 ~]";
}

# 禁止访问备份文件
location ~ \.(bak|backup|old|tmp)$ {
deny all;
return 403 "禁止访问备份文件 [正则匹配 ~]";
}


# ==========================================
# 4. 普通前缀匹配 - 最长匹配原则
# ==========================================

# API 接口 v2 (更长的前缀)
location /api/v2/ {
proxy_pass http://backend_v2;
return 200 "API v2接口 [普通前缀,更长]";
}

# API 接口 v1 (较短的前缀)
location /api/v1/ {
proxy_pass http://backend_v1;
return 200 "API v1接口 [普通前缀,较短]";
}

# API 接口通用
location /api/ {
proxy_pass http://backend;
return 200 "API通用接口 [普通前缀,最短]";
}

# 商品详情页
location /product/ {
try_files $uri $uri/ /product/index.html;
return 200 "商品详情页 [普通前缀]";
}

# 用户中心
location /user/ {
try_files $uri $uri/ /user/index.html;
return 200 "用户中心 [普通前缀]";
}

# 管理后台
location /admin/ {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
return 200 "管理后台 [普通前缀]";
}


# ==========================================
# 5. 通用匹配 - 兜底规则
# ==========================================

# 所有其他请求
location / {
try_files $uri $uri/ /index.html;
return 200 "通用匹配 [兜底规则]";
}
}


针对上面的测试用例及匹配结果


请求URI匹配的Location优先级类型说明
/= /精确匹配精确匹配优先级最高
/index.htmllocation /普通前缀通用兜底
/robots.txt= /robots.txt精确匹配精确匹配
/static/css/style.css^~ /static/前缀优先^~ 阻止了正则匹配
/uploads/avatar.jpg^~ /uploads/前缀优先^~ 阻止了图片正则
/images/logo.png`~ .(jpgjpegpng...)$`正则匹配图片正则
/js/app.JS`~* .(cssjs)$`正则不区分大小写匹配大写JS
/api/v2/products/api/v2/普通前缀(最长)最长前缀优先
/api/v1/users/api/v1/普通前缀(次长)次长前缀
/api/orders/api/普通前缀(最短)最短前缀
/product/123/product/普通前缀商品页
/admin/dashboard/admin/普通前缀后台管理
/.git/config^~ /.前缀优先禁止访问
/backup.bak`~ .(bakbackup...)$`正则匹配禁止访问

第一个问题及其延伸现到这,我们继续看第二个问题。


问题二


先看下面的服务器端nginx的重启命令:


# 命令一
nginx -s reload

#
命令二
systemctl reload nginx

上面两个命令都是用来重启 nginx 服务的,但是你想过它们之间有什么区别吗?哪个用起来更优雅?


答案:有区别!区别在于命令的执行方式和适用场景不同。


nginx -s reload


这是 Nginx 自带的信号控制命令:



  • 直接向 Nginx 主进程发送 reload 信号

  • 优雅重启:不会中断现有连接,平滑加载新配置

  • 需要 nginx 命令在 PATH 环境变量中,或使用完整路径(如 /usr/sbin/nginx -s reload)

  • 这是 Nginx 原生的重启方式


systemctl reload nginx


这是通过 systemd 管理的服务命令:



  • 通过 systemd 管理 Nginx 服务

  • 也会优雅重启 Nginx,平滑加载新配置

  • 需要 systemd 环境,适用于使用 systemd 管理服务的 Linux

  • 这是现代 Linux 发行版(如 CentOS 7/8, RHEL 7/8, Ubuntu 16.04+)的推荐方式。


简单一看其他相关命令对比:



  • nginx -s stop 等价 systemctl stop nginx

  • nginx -s quit 等价 systemctl stop nginx

  • nginx -t (测试配置是否正确) - 这个没有 systemctl 对应命令


systemctl下相关常用命令:


# 设置开机自启
systemctl enable nginx

#
启动服务
systemctl start nginx

#
检查服务状态
systemctl status nginx

#
停止服务
systemctl stop nginx

#
重启服务(会中断连接)
systemctl restart nginx

#
平滑重载配置(不中断服务)-- 对应 nginx -s reload
systemctl reload nginx

#
检查配置文件语法(这是调用nginx二进制文件的功能)
nginx -t

在服务器上最优雅的使用组合:


# 先测试配置
nginx -t

#
如果配置正确,再重载
systemctl reload nginx

#
检查状态
systemctl status nginx

#
如果systemctl失败或命令不存在,则使用直接方式
sudo nginx -s reload


总结:我们不能光一脸懵的看着,哎,这两种命令都能操作nginx来, 却从来不关心它们的区别是什么?什么时候用哪个?




对于使用Linux发行版的服务端来说, 已经推荐使用 systemctl 来设置相关的nginx服务了,能使用 systemctl 就尽量使用它,因为它是现代Linux系统管理服务的标准方式。




本地开发环境或者没有 systemd 的环境下, 则可以使用 nginx 这种直接方式。



问题三



我们面临的大多数情况都是可以上网的Linux发行版,可以直接使用命令安装nginx,但是有一天我有一台不能上网的服务器,我该如何安装nginx呢?



现简单熟悉一下命令行安装nginx的步骤, Ubuntu/Debian系统为例子:


# 更新包列表
sudo apt update

#
安装 Nginx
sudo apt install nginx

#
启动 Nginx
sudo systemctl start nginx

#
设置开机自启
sudo systemctl enable nginx


上述便完成了,但是离线版安装要怎么去做呢?




因为我的服务器可能是不同的架构,比如 x86_64, ARM等等



方案一


下载官方预编译包下载地址:


x86_64 架构:


尽量使用1.24.x的版本


# 从官网下载对应系统的包
wget http://nginx.org/packages/centos/7/x86_64/RPMS/nginx-1.24.0-1.el7.ngx.x86_64.rpm

ARM64 架构:


# Ubuntu ARM64
wget http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.24.0-1~jammy_arm64.deb

查看服务器的架构信息


# 查看当前系统架构
uname -m

# 输出示例:
# x86_64 -> Intel/AMD 64位
# aarch64 -> ARM 64位
# armv7l -> ARM 32位

# 查看系统版本
cat /etc/os-release

把下载好的包传到服务器上,然后使用下面的命令安装:


# 对于 RPM 包 (CentOS/RHEL)
cd /tmp
sudo rpm -ivh nginx-*.rpm

# 对于 DEB 包 (Ubuntu/Debian)
cd /tmp
sudo dpkg -i nginx-*.deb

启动服务


sudo systemctl start nginx       # 启动
sudo systemctl enable nginx # 开机自启
sudo systemctl status nginx # 查看状态

验证


nginx -v                         # 查看版本
curl http://localhost # 测试访问

方案二


源码编译安装的方式,一般不推荐,除非你有特殊需求,如果需要的话让后端来吧,我们是前端...,超纲了!


问题四


当有一天你使用unity 3d开发应用并导出wasm项目后,需要使用nginx部署后,当你和往常一样正常部署后,一访问发现报错误!


错误信息如下, 一般都是提示:



类似于这种:content-type ... not ... wasm




Failed to load module script: The server responded with a non-JavaScript MIME type of "application/wasm".



这时的你可能一脸懵, 我和往常一样正常的配置nginx呀,为啥别的可以,但是wasm应用报错了!为啥?


这时就引出一个不常用的知识点,我要怎么使用nginx配置wasm的应用,需要进行哪些配置?


需要配置两部分:


第一部分:配置正确的 MIME 类型


进入nginx的安装目录,找到mine.types文件,新增下面的配置:


# 新增下面类型配置
application/wasm wasm;

第二部分:wasm的应用需要特殊配置


下面是wasm应用的配置示例,是可以直接使用的,只需要的修改一下访问文件的路径端口即可。


server {
listen 80;
server_name your-domain.com; # 修改为你的域名或ip

# Unity WebGL 构建文件的根目录
root /var/www/unity-webgl;
index index.html;

# 字符集
charset utf-8;

# 日志配置(可选指向特殊的日志文件)
access_log /var/log/nginx/unity-game-access.log;
error_log /var/log/nginx/unity-game-error.log;

# ========== MIME 类型配置(下面配置的重点,也是区别于正常的nginx应用配置) ==========

# WASM文件(未压缩)
location ~ \.wasm$ {
types {
application/wasm wasm;
}
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin *;
}

# WASM文件(Gzip压缩)
location ~ \.wasm\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/wasm;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin *;
}

# WASM文件(Brotli压缩)
location ~ \.wasm\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/wasm;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin *;
}

# Data文件(未压缩)
location ~ \.data$ {
types {
application/octet-stream data;
}
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Data文件(Gzip压缩)
location ~ \.data\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/octet-stream;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Data文件(Brotli压缩)
location ~ \.data\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/octet-stream;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# JavaScript文件(未压缩)
location ~ \.js$ {
types {
application/javascript js;
}
add_header Cache-Control "public, max-age=31536000, immutable";
}

# JavaScript文件(Gzip压缩)
location ~ \.js\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# JavaScript文件(Brotli压缩)
location ~ \.js\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Framework JS 文件
location ~ \.framework\.js(\.gz|\.br)?$ {
if ($uri ~ \.gz$) {
add_header Content-Encoding gzip;
}
if ($uri ~ \.br$) {
add_header Content-Encoding br;
}
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Loader JS 文件
location ~ \.loader\.js(\.gz|\.br)?$ {
if ($uri ~ \.gz$) {
add_header Content-Encoding gzip;
}
if ($uri ~ \.br$) {
add_header Content-Encoding br;
}
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Symbols JSON 文件
location ~ \.symbols\.json(\.gz|\.br)?$ {
if ($uri ~ \.gz$) {
add_header Content-Encoding gzip;
}
if ($uri ~ \.br$) {
add_header Content-Encoding br;
}
add_header Content-Type application/json;
add_header Cache-Control "public, max-age=31536000, immutable";
}

# ========== 静态资源配置(导出的wasm应用一般都有下面的静态资源) ==========

# StreamingAssets 目录
location /StreamingAssets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# Build 目录
location /Build/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}

# TemplateData 目录(Unity 模板资源)
location /TemplateData/ {
add_header Cache-Control "public, max-age=86400";
}

# 图片文件
location ~* \.(jpg|jpeg|png|gif|ico|svg)$ {
add_header Cache-Control "public, max-age=2592000";
}

# CSS 文件
location ~* \.css$ {
add_header Content-Type text/css;
add_header Cache-Control "public, max-age=2592000";
}

# ========== HTML 和主页面配置 ==========

# HTML 文件不缓存(确保更新能及时生效)
location ~ \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}

# 根路径
location / {
try_files $uri $uri/ /index.html;
}

# ========== Gzip 压缩配置(开启gzip压缩增加访问速度) ==========

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/x-javascript
application/xml
application/xml+rss
application/wasm
application/octet-stream;

# ========== 安全配置 ==========

# 禁止访问隐藏文件
location ~ /\. {
deny all;
}

# 禁止访问备份文件
location ~ ~$ {
deny all;
}

# XSS 保护(可选配置)
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
}

总结:


配置 wasm应用 的 Nginx 核心要点如下:



  1. MIME 类型配置



    • 必须在 mime.types 中添加 application/wasm wasm;,否则浏览器无法正确识别 WASM 文件。



  2. Nginx.conf 核心配置



    • 文件处理:针对 WASM、Data、JS 等文件,分别配置未压缩和压缩版本(gzip/br)的处理规则。

    • 静态资源缓存:为 StreamingAssetsBuildTemplateData 及图片/CSS 设置合理的缓存策略(Cache-Control)。

    • HTML 更新策略:HTML 文件应禁用缓存(no-cache),确保用户始终加载最新版本。

    • 性能优化:开启 Gzip 压缩,提高传输效率。

    • 安全加固:添加基本的安全头配置,保护服务器资源。




文章同步地址:http://www.liumingxin.site/blog/detail…


作者:LiuMingXin
来源:juejin.cn/post/7582156410320371722
收起阅读 »

Hutool被卖半年多了,现状是逆袭还是沉寂?

是的,没错。那个被人熟知的国产开源框架 Hutool 距离被卖已经过去近 7 个月了。 那 Hutool 现在的发展如何呢?它未来有哪些更新计划呢?Hutool AI 又该如何使用呢?如果不想用 Hutool 有没有可替代的框架呢? 近半年现状 从 Hutoo...
继续阅读 »

是的,没错。那个被人熟知的国产开源框架 Hutool 距离被卖已经过去近 7 个月了。


那 Hutool 现在的发展如何呢?它未来有哪些更新计划呢?Hutool AI 又该如何使用呢?如果不想用 Hutool 有没有可替代的框架呢?


近半年现状


从 Hutool 官网可以看出,其被卖近 7 个月内仅发布了 4 个版本更新,除了少量的新功能外,大多是 Bug 修复,当期在此期间发布了 Hutool AI 模块,算是一个里程碑式的更新




更新日志:hutool.cn/docs/#/CHAN…



收购公司


没错,收购 Hutool 的这家公司和收购 AList 的公司是同一家公司(不够科技),该公司前段时间因为其在收购 AList 代码中悄悄收集用户设备信息,而被推向过风口浪尖,业内人士认为其收购开源框架就是为了“投毒”,所以为此让收购框架损失了很多忠实的用户。



其实,放眼望去那些 APP 公司收集用户设备和用户信息属于家常便饭了(国内隐私侵犯问题比较严重),但 AList 因为其未做文档声明,且未将收集设备信息的代码提交到公共仓库,所以大家发现之后才会比较气愤。



Hutool-AI模块使用


Hutool AI 模块的发布算是被收购之后发布的最值得让人欣喜的事了,使用它可以对接各大 AI 模型的工具模块,提供了统一的 API 接口来访问不同的 AI 服务。


目前支持 DeepSeek、OpenAI、Grok 和豆包等主流 AI 大模型。


该模块的主要特点包括:



  • 统一的 API 设计,简化不同 AI 服务的调用方式。

  • 支持多种主流 AI 模型服务。

  • 灵活的配置方式。

  • 开箱即用的工具方法。

  • 一行代码调用。


具体使用如下。


1.添加依赖


<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-ai</artifactId>
<version>5.8.38</version>
</dependency>

2.调用API


实现对话功能:


DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel("your bots id").build(), DoubaoService.class);
ArrayList<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是什么都可以"));
messages.add(new Message("user","你想做些什么"));
String botsChat = doubaoService.botsChat(messages);

识别图片:


//可以使用base64图片
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64));

//也可以使用网络图片
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail());


生成视频:


//创建视频任务
DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()).setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");

//查询视频生成任务信息
String videoTasksInfo = doubaoService.getVideoTasksInfo("任务id");

未来发展



  • Hutool5:目前 Hutool 5.x 版本主要是基于 JDK 8 实现的,后面更新主要以 BUG 修复为准。

  • Hutool6:主要以功能尝鲜为主。

  • Hutool7:升级为 JDK 17,添加一些新功能,删除一些不用的类。


目前只发布了 Hutool 5.x,按照目前的更新进度来看,不知何时才能盼来 Hutool7 的发布。


同类替代框架


如果担心 Hutool 有安全性问题,或更新不及时的问题可以尝试使用同类开源工具类:



视频解析


http://www.bilibili.com/video/BV1QR…


小结


虽然我们不知道 Hutool 被收购意味着什么?是会变的越来越好?还是会就此陨落?我们都不知道答案,所以只能把这个问题交给时间。但从个人情感的角度出发,我希望国产开源框架越做越好。好了,我是磊哥,咱们下期见。



本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:场景题、SpringAI、SpringAIAlibaba、并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、JVM、设计模式、消息队列、Dify、Coze、AI常见面试题等。



作者:Java中文社群
来源:juejin.cn/post/7547624644507156520
收起阅读 »

为什么我开始减少逛技术社区,而是去读非技术的书?

我得承认,我有过很长一段时间的 技术社区上瘾。 每天上班第一件事,就是打开掘金、Hacker News、InfoQ,把热门文章刷一遍。通勤的地铁上,也要用手机看看今天又出了哪个新框架的测评、哪个Vite插件又有了更新。 我生怕错过了什么,感觉一天不刷,就会被飞...
继续阅读 »

224fb719b9714869b2f19e55e6d7b378~tplv-k3u1fbpfcp-watermark (1).jpeg


我得承认,我有过很长一段时间的 技术社区上瘾


每天上班第一件事,就是打开掘金、Hacker News、InfoQ,把热门文章刷一遍。通勤的地铁上,也要用手机看看今天又出了哪个新框架的测评、哪个Vite插件又有了更新。


我生怕错过了什么,感觉一天不刷,就会被飞速发展的技术时代抛弃。这种信息焦虑,我想很多工程师都有。


但大概从去年开始,我刻意地减少了这个仪式。我把每天早上刷文章的一小时,换成了读一些看起来和编程八竿子打不着的书。比如,心理学、经济学、历史、甚至小说。


一开始只是想换换脑子,但慢慢地,我发现,这些非技术书,反而帮我解决了很多工作中遇到的、最棘手的技术问题。


这篇文章,就是想聊聊我这个转变背后的思考。




技术的天花板


工作了五六年后,我遇到了一个很明显的瓶颈。


我发现,再多学一个JS的新语法、再多会用一个Vite插件,似乎都不能让我的能力产生质的飞跃。我的技术深度和广度,足以解决日常工作中99%的技术难题。


但我发现,工作中真正难的,往往不是技术本身。而是:



  • 为什么我们团队的沟通效率这么低,一个简单的需求能来回拉扯好几天?

  • 为什么这个看似简单的项目,开发过程中总是不断地范围蔓延?

  • 我该如何向非技术背景的老板,证明这次重构的必要性和长期价值?

  • 面对一个全新的业务,我该如何设计一个能在未来3年内,适应各种不确定性变化的技术架构?


我意识到,这些问题的答案,在MDN文档里、在Stack Overflow上,是找不到的。它们是关于人、关于系统、关于决策的复杂问题。而我当时的技术知识库,对解决这些问题,几乎毫无帮助。




我的书架,以及它们教我的事


于是,我开始漫无目的地,从技术之外的领域寻找答案。下面,我想分享几个对我影响最大的领域和书籍。


心理学,理解人


  • 推荐阅读:《思考,快与慢》、《影响力》、《非暴力沟通》


image.png


image.png


作为工程师,我们习惯于和确定性的机器打交道。但我们的工作,却无时无刻不在和不确定的人打交道——用户、产品经理、同事、老板。


心理学,尤其是认知心理学,教会我理解了人性的非理性。



  • 理解用户:读了《思考,快与慢》后,我开始理解为什么用户会做出那些不合逻辑的操作,为什么有时候更优的设计反而没人用。这让我在做UI/UX设计和评审时,不再只是一个技术实现者,而更能代入用户的直觉系统去思考。

  • 理解同事:读了《非暴力沟通》后,我改变了我在Code Review里的沟通方式。我不再说“你这里写得不对”,而是说“我看到这个实现,我担心在XX场景下可能会有风险,你觉得呢?”。我发现,当我开始关注对方的感受和需要,而不是直接评判时,技术沟通变得顺畅了许多。


系统思考 看透架构的本质


  • 推荐阅读:《第五项修炼》、《系统之美》


image.png


系统思考,教会我最重要的一个概念:世界不是由一条条独立的因果链组成的,而是由一个个相互关联的反馈回路组成的。


这个思想,彻底改变了我对软件架构的看法。



  • 理解技术债:我不再把技术债看作一个孤立的坏代码问题,而是把它看作一个会自我增强的反馈回路。坏代码 -> 开发效率降低 -> Bug增多 -> 救火时间增多 -> 更没时间写好代码 -> 坏代码更多。这个循环一旦形成,不从外部打破,系统就会慢慢崩溃。

  • 做出更好的技术决策:我不再追求完美的、一步到位的架构,而是去寻找那些能适应变化的、演进式的架构。我开始用机会成本去评估技术选型,用延迟和滞后效应去理解一个技术决定可能在半年后带来的影响。


历史/传记 获得古人的经验和战略


  • 推荐阅读:《人类简史》、《罗马帝国衰亡史》、各种历史人物传记


image.png


历史,是研究成与败的宏大案例集。它能让你跳出眼前的一个个项目,去思考技术浪潮的更迭。



  • 获得历史感 :为什么jQuery会衰落?为什么React的Hooks范式会成功?为什么当年的AngularJS会失败?这背后,和历史上的技术革命、王朝兴衰,遵循着相似的规律——它们是否解决了当时最核心的矛盾?它们是否降低了开发成本?

  • 做出更聪明的长期判断:这种历史感,让我在做一些长远的技术规划时,能更好地判断什么是真正的趋势,什么是短暂的泡沫,从而避免团队把宝贵的资源,投入到一个注定会很快消亡的技术上。




这次的分享,可能有点务虚😁,但它是我近几年最真实的感受。


程序员的工作,是把一个清晰的需求,翻译成高质量的代码。


而工程师的工作,是把一个模糊的、充满不确定性的现实世界问题,转化为一个可靠、可维护的系统。


想从程序员蜕变为工程师,需要的远不止是代码能力。


我依然每天写代码,也依然关注技术动态。但我不再焦虑于错过了哪个新库。我把更多的信心,放在了那些从非技术书籍里学来的、更底层的思维模型上。


因为我知道,这些东西,可能比我今天写的任何一行代码,都要保值得多。


你们说是不是?🙌


作者:ErpanOmer
来源:juejin.cn/post/7560659435955224628
收起阅读 »

为什么我开始减少逛技术社区,而是去读非技术的书?

我得承认,我有过很长一段时间的 技术社区上瘾。 每天上班第一件事,就是打开掘金、Hacker News、InfoQ,把热门文章刷一遍。通勤的地铁上,也要用手机看看今天又出了哪个新框架的测评、哪个Vite插件又有了更新。 我生怕错过了什么,感觉一天不刷,就会被飞...
继续阅读 »

224fb719b9714869b2f19e55e6d7b378~tplv-k3u1fbpfcp-watermark (1).jpeg


我得承认,我有过很长一段时间的 技术社区上瘾


每天上班第一件事,就是打开掘金、Hacker News、InfoQ,把热门文章刷一遍。通勤的地铁上,也要用手机看看今天又出了哪个新框架的测评、哪个Vite插件又有了更新。


我生怕错过了什么,感觉一天不刷,就会被飞速发展的技术时代抛弃。这种信息焦虑,我想很多工程师都有。


但大概从去年开始,我刻意地减少了这个仪式。我把每天早上刷文章的一小时,换成了读一些看起来和编程八竿子打不着的书。比如,心理学、经济学、历史、甚至小说。


一开始只是想换换脑子,但慢慢地,我发现,这些非技术书,反而帮我解决了很多工作中遇到的、最棘手的技术问题。


这篇文章,就是想聊聊我这个转变背后的思考。




技术的天花板


工作了五六年后,我遇到了一个很明显的瓶颈。


我发现,再多学一个JS的新语法、再多会用一个Vite插件,似乎都不能让我的能力产生质的飞跃。我的技术深度和广度,足以解决日常工作中99%的技术难题。


但我发现,工作中真正难的,往往不是技术本身。而是:



  • 为什么我们团队的沟通效率这么低,一个简单的需求能来回拉扯好几天?

  • 为什么这个看似简单的项目,开发过程中总是不断地范围蔓延?

  • 我该如何向非技术背景的老板,证明这次重构的必要性和长期价值?

  • 面对一个全新的业务,我该如何设计一个能在未来3年内,适应各种不确定性变化的技术架构?


我意识到,这些问题的答案,在MDN文档里、在Stack Overflow上,是找不到的。它们是关于人、关于系统、关于决策的复杂问题。而我当时的技术知识库,对解决这些问题,几乎毫无帮助。




我的书架,以及它们教我的事


于是,我开始漫无目的地,从技术之外的领域寻找答案。下面,我想分享几个对我影响最大的领域和书籍。


心理学,理解人


  • 推荐阅读:《思考,快与慢》、《影响力》、《非暴力沟通》


image.png


image.png


作为工程师,我们习惯于和确定性的机器打交道。但我们的工作,却无时无刻不在和不确定的人打交道——用户、产品经理、同事、老板。


心理学,尤其是认知心理学,教会我理解了人性的非理性。



  • 理解用户:读了《思考,快与慢》后,我开始理解为什么用户会做出那些不合逻辑的操作,为什么有时候更优的设计反而没人用。这让我在做UI/UX设计和评审时,不再只是一个技术实现者,而更能代入用户的直觉系统去思考。

  • 理解同事:读了《非暴力沟通》后,我改变了我在Code Review里的沟通方式。我不再说“你这里写得不对”,而是说“我看到这个实现,我担心在XX场景下可能会有风险,你觉得呢?”。我发现,当我开始关注对方的感受和需要,而不是直接评判时,技术沟通变得顺畅了许多。


系统思考 看透架构的本质


  • 推荐阅读:《第五项修炼》、《系统之美》


image.png


系统思考,教会我最重要的一个概念:世界不是由一条条独立的因果链组成的,而是由一个个相互关联的反馈回路组成的。


这个思想,彻底改变了我对软件架构的看法。



  • 理解技术债:我不再把技术债看作一个孤立的坏代码问题,而是把它看作一个会自我增强的反馈回路。坏代码 -> 开发效率降低 -> Bug增多 -> 救火时间增多 -> 更没时间写好代码 -> 坏代码更多。这个循环一旦形成,不从外部打破,系统就会慢慢崩溃。

  • 做出更好的技术决策:我不再追求完美的、一步到位的架构,而是去寻找那些能适应变化的、演进式的架构。我开始用机会成本去评估技术选型,用延迟和滞后效应去理解一个技术决定可能在半年后带来的影响。


历史/传记 获得古人的经验和战略


  • 推荐阅读:《人类简史》、《罗马帝国衰亡史》、各种历史人物传记


image.png


历史,是研究成与败的宏大案例集。它能让你跳出眼前的一个个项目,去思考技术浪潮的更迭。



  • 获得历史感 :为什么jQuery会衰落?为什么React的Hooks范式会成功?为什么当年的AngularJS会失败?这背后,和历史上的技术革命、王朝兴衰,遵循着相似的规律——它们是否解决了当时最核心的矛盾?它们是否降低了开发成本?

  • 做出更聪明的长期判断:这种历史感,让我在做一些长远的技术规划时,能更好地判断什么是真正的趋势,什么是短暂的泡沫,从而避免团队把宝贵的资源,投入到一个注定会很快消亡的技术上。




这次的分享,可能有点务虚😁,但它是我近几年最真实的感受。


程序员的工作,是把一个清晰的需求,翻译成高质量的代码。


而工程师的工作,是把一个模糊的、充满不确定性的现实世界问题,转化为一个可靠、可维护的系统。


想从程序员蜕变为工程师,需要的远不止是代码能力。


我依然每天写代码,也依然关注技术动态。但我不再焦虑于错过了哪个新库。我把更多的信心,放在了那些从非技术书籍里学来的、更底层的思维模型上。


因为我知道,这些东西,可能比我今天写的任何一行代码,都要保值得多。


你们说是不是?🙌


作者:ErpanOmer
来源:juejin.cn/post/7560659435955224628
收起阅读 »

AI 纪元 3 年,2025 论前端程序员自救

前言 2023 年是公认的 AI 元年,2022年底OpenAI发布ChatGPT(基于GPT-3.5),2023年初迅速爆火,上线仅2个月用户突破1亿,成为历史上增长最快的消费级应用。短短三年内,AI 已经从遥不可及,进化到走进千家万户了,从只能聊天的纯语言...
继续阅读 »

前言


2023 年是公认的 AI 元年,2022年底OpenAI发布ChatGPT(基于GPT-3.5),2023年初迅速爆火,上线仅2个月用户突破1亿,成为历史上增长最快的消费级应用。短短三年内,AI 已经从遥不可及,进化到走进千家万户了,从只能聊天的纯语言大模型(LLMs),到今天能解决实际问题的多模态大模型(MLLMs/LMMs)、智能体(AI Agent),我们的生活正发生着不易察觉但又天翻地覆的变化。


这可能是你 2025 年看到的最真实,最中肯,也最能让你安心的文章,先说结论:程序员岗位都不会死,且未来必须具备的能力大部分人都有。


怎么看 AI 对程序员岗位的冲击


AI 的井喷式爆发,对程序员的冲击确实很大。程序员中,前端程序员最甚,在网络上,它三天一小死,五天一大死,但是结果我们也看到了,前端并没有死,甚至活得比以前更繁荣了,很多其他岗位程序员也试着用 Gemini 3 Pro 搭建了属于自己的 three.js 应用。


可能大家跟我的感觉一样,对网上前端已死的焦虑卖家并不感冒,因为在工作中,AI 给我们的感觉更像是一个老司机坐在副驾驶,我们遇到不懂的直接问它,而它也会把毕生所学毫无保留的交代出来,甚至在 VSCode Copilot 或者 Cursor 、Trae 中,我们根本不需要自己动手写代码了,80% 的时间都在 vibe coding,我们没有因为 AI 丢掉工作,反而 AI 让我们觉得:哇,太爽了,工作效率翻了三倍!


如果你有这种感觉,那么恭喜你,你是最适合在未来工作的前端开发者


目前 AI 的局限性表现的很明显,AI 生成的代码往往 平均化、缺乏深度优化或者有隐蔽的 bug。还经常改到我们不希望变动的部分,这里祭出这张火爆全网的梗图


image.png


当然这些和 AI 还依旧不够成熟有一定关系,但是还有一个最根本的原因使得 AI 无法取代人类,它注定只能作为人类的工具,但是这个原因很哲学,请看下文。


不用怕,欲望是创造的原初动力


人类创造的起点往往是 “想做”、“喜欢”、“不爽”、“好奇”、“想证明自己” 这些内在冲动。比如 一个程序员半夜写出一个新框架,是因为“觉得现有工具太烂了,我就是要搞一个更好玩的”,而 AI 没有这些。它只有“根据训练数据预测下一个词”或者“最大化奖励函数”。我们给它一个目标,它就全力优化,但它自己永远不会“突然想”去做一件没人要求的事。


所以,AI 的“创造”其实是重组+优化过去的知识,而不是从零生出全新的渴望。


AI 很擅长在已知框架里做到极致(比如写出完美的前端组件、优化算法到最快),但人类擅长打破框架、发明新框架。这种突破往往来自 “无用” 的欲望



  • 想玩 → 发明游戏

  • 想偷懒 → 发明自动化工具(发明了 AI 😂)

  • 想被认可 → 开源一个项目


历史上所有重大创新(互联网、智能手机、开源运动)都源于这种自发欲望。


总的来说,AI 只是现有知识最好的运用者,它无法自发的想去创造新事物(如果有的话也太可怕了,这 TM 直接天网)


要让AI拥有“真正自发的欲望”,需要解决哲学级难题:意识(consciousness) 和 主观体验(qualia)。目前科学界连“意识是什么”都没搞清楚,更别说在硅基系统里实现它了。


所以这是 AI 为什么无法代替人类的原因。


但是,从直觉上来看,AI 似乎可以替代一部分基础工作,而且现在正在发生,有些公司正在招聘高级开发者来替代多个初中级岗位...


危机真正的来源


初中级岗位确实正在慢慢消失,但这并不意味着我们在岗程序员一定会掉队,相反,我们在行业内属于 “老人”,也是最先吃到 AI 这块蛋糕的人。既然总要有人做事,所谓近水楼台先得月,最先有机会转换到 融合职责岗位 的,也会是我们这一批人,而不是其他人。


在我们这个行业,各个岗位正在发生融合,产品经理、UI、前后端的界限越来越模糊,最近还出现了所谓的 “一人公司”、“超级个体”。


初中级岗位的慢慢消失和岗位职责的融合,是 AI 时代,对普通程序员最大的挑战,我们应该顺应这一潮流,转变以往的思维,勇敢的加入到这场洪流中。


那么,该怎么做?


有解,且比以前更简单!


众所周知,T-shaped 技能模型推荐我们 先建立一个领域的深度(垂直杠),再扩展广度(水平杠),形成T形。我们很多人也是这么干的,前端同学在深刻学习 JS 语言基础、vue、react 源码,传统面试中,这些知识也是重点考察内容。


但是 AI 时代,这些技能变得非常廉价,AI已经把“深度纯技术钻研”的门槛大幅降低:它能快速生成代码、优化实现,甚至帮你读源码、总结关键。


你可以完全没读过 vue 源码,而仅用一句:“帮我实现一个 vue 的响应式系统” 实现这个功能。


T 的竖线表示领域深耕,既然深耕到一半,发现不太有竞争力了,怎么办?很简单,发展横轴,横轴代表了我们的广度(全栈狂喜),但这里说的不是纯技术广度,而是多方面多系统的融会贯通。


不过好在这些技能是我们聪明的程序员天生就具备的能力。


未来优秀程序员不再是“写代码最快的人”,而是系统思考者 + 需求翻译者 + AI指挥官 + 复杂问题解决者 + 高效沟通者。



  1. System Thinking 系统思维 解释:能够从整体视角设计复杂、可扩展、可维护的系统,权衡性能、成本、安全、演化等多个维度。AI擅长局部,人类必须负责全局架构。

  2. Requirement Mastery 需求洞察 解释:精准理解模糊、矛盾的业务需求,把商业目标转化为可验证的技术方案。未来程序员更像“业务翻译者”,这是AI最弱的一环。

  3. Integration Ability 融合贯通 解释:快速整合不同技术栈、AI工具、第三方服务,从0到1构建完整产品。广度+快速学习能力,让你能驾驭AI实现跨领域创新。

  4. Debugging & Reasoning 复杂调试与推理 解释:快速定位根因,需要强大的因果推理、假设检验能力。AI会减少简单bug,但复杂问题会更多、更隐蔽。

  5. Communication & Expression 语言组织能力 沟通与表达 解释:用清晰、结构化的语言(书面最重要)把复杂技术想法表达出来,包括写 AI 提示词 (目前,将来可能弱化)、文档、技术方案、跨团队沟通等。脑子里再懂,不说出来就等于没用——这会直接影响你的影响力、晋升和协作效率。


看起来需要具备这么多能力,但是仔细想想,其实每个人都已经基本具备。每个人只需在自己的薄弱领域稍加练习即可,这比啃框架源码可简单多了,至少都是可以 “勤能补拙” 的技能。


结语


AI 不是洪水猛兽,它是我们最可靠的副驾驶,我们应该勇敢投入潮流,转变自身态度,提升视野,才能在未来的 AI 大融合时代占住自己的一席之地。


还有,2026,别感冒!


番外




作者:德莱厄斯
来源:juejin.cn/post/7589732701289234466
收起阅读 »

技术、业务、管理:一个30岁前端的十字路口

上个月,我刚过完30岁生日。 没有办派对,就和家人简单吃了顿饭。但在吹蜡烛的那个瞬间,我还是恍惚了一下。 30岁,对于一个干了8年的前端来说,到底意味着什么? 前几天,我在做团队下半年的规划,看着表格里的一个个名字,再看看镜子里的自己,一个问题在我脑子里变得无...
继续阅读 »

image.png


上个月,我刚过完30岁生日。


没有办派对,就和家人简单吃了顿饭。但在吹蜡烛的那个瞬间,我还是恍惚了一下。


30岁,对于一个干了8年的前端来说,到底意味着什么?


前几天,我在做团队下半年的规划,看着表格里的一个个名字,再看看镜子里的自己,一个问题在我脑子里变得无比清晰:


我职业生涯的下一站,到底在哪?


28岁之前


在28岁之前,我的人生是就行直线。


我的目标非常纯粹:成为一个技术大神。我的快乐,来自于搞懂一个Webpack的复杂配置、用一个巧妙的Hook解决了一个棘手的渲染问题、或者在Code Review里提出一个让同事拍案叫绝的优化。


这条路的升级路径也非常清晰:


初级(学框架) -> 中级(懂原理) -> 高级(能搞定复杂问题)


我在这条路上,跑得又快又开心。


30岁的十字路口


但到了30岁,我当上了技术组长,我发现,这条直线消失了。取而代之的,是一个迷雾重重的十字路口。


我发现,那些能让我晋升到高级的技能,好像并不能帮我晋升到下一个级别了。


摆在我面前的,是三条截然不同,却又相互纠缠的路。




技术路线——做技术专家



  • 这条路成为一个 主工程师 或 架构师。不带人,不背KPI,只解决公司最棘手的技术难题。比如,把我们项目的INP从200ms优化到100ms以下,或者主导设计公司下一代的跨端架构。

  • 这当然是我的舒适区。我爱代码,我享受这种状态。这条路,是我最熟悉、最擅长的。

  • 焦虑点:我真的能成为那个最顶尖的1%吗?前端技术迭代这么快,我能保证我5年后,还能比那些25岁的年轻人,学得更快、想得更深吗?当我不再是团队里最能打的那个人时,我的价值又是什么?




业务路线——更懂的产品工程师



  • 不再只关心怎么实现,而是去关心为什么要做?深入理解我们的商业模式、用户画像、数据指标。不再是一个接需求的资源,而是成为一个能和产品经理吵架、能反向推动产品形态的合作伙伴。

  • 我发现,在公司里,那些真正能影响决策、晋升最快的工程师,往往都是最懂业务的。他们能用数据和商业价值去证明自己工作的意义,而我,还在纠结一个技术实现的优劣。

  • 焦虑 :这意味着我要走出代码的舒适区,去开更多的会,去啃那些枯燥的业务文档,去和各种各样的人扯皮。我一个技术人,会不会慢慢变得油腻了?




管理——做前端Leader



  • 这就是我现在正在尝试的。我的工作,不再是写代码,而是让团队更好地写代码。我的KPI,不再是我交付了多少,而是我们团队交付了多少。

  • 老板常说的影响力杠杆。我一个人写代码,战斗力是1。我带一个5人团队,如果能让他们都发挥出1.2的战斗力,那我的杠杆就是6。这种成就感,和写出一个完美函数,是完全不同的。

  • 这是我最焦虑的地方:


    我上周二,开了7个会,一行代码都没写。


    晚上9点,我打开VS Code,看着那些我曾经最熟悉的代码库,突然有了一丝陌生感。我开始恐慌:我的手艺是不是要废了?如果有一天,我不当这个Leader了,我还能不能凭技术,在外面找到一份好工作?





这三个问题,在我脑子里盘旋了很久。我试图三选一,但越想越焦虑。


直到最近,我在复盘一个项目时,才突然想明白:


这根本不是一个三选一的十字路口。


这三条路,是一个优秀的技术人,在30岁之后,必须三位一体、同时去修炼的内功。



  • 一个不懂技术的Leader,无法服众,也做不出靠谱的架构决策。

  • 一个不懂业务的专家,他的技术再牛,也可能只是屠龙之技,无法为公司创造真正的价值。

  • 一个不懂管理(影响他人)的工程师,他的想法再好,也只能停留在自己的电脑上,无法变成团队的战斗力。




image.png


DOTA2的世界里,有一个英雄叫 祈求者(Invoker),他有冰、雷、火三个元素,通过不同的组合,能释放出10个截然不同的强大技能。


我觉得,30岁之后的前端,就应该成为一个祈求者。


我们不再是那个只需要猛点一个技能的码农。我们的挑战,在于如何在不同的场景下,把这三个元素,组合成最恰当的技能,去解决当下最复杂的问题。


这条路,很难,但也比25岁时,要有趣得多。


与所有在十字路口迷茫的同行者,共勉🙌。


作者:ErpanOmer
来源:juejin.cn/post/7563564221352673331
收起阅读 »

AI 代码审核

ai-code-review   在日常开发中,我们经常会遇到一些问题,比如代码质量问题、安全问题等。如果我们每次都手动去检查,不仅效率低下,而且容易出错。   所以我们可以利用 AI 来帮助我们检查代码,这样可以提高我们的效率   那么,如何利用 AI 来检...
继续阅读 »

ai-code-review


  在日常开发中,我们经常会遇到一些问题,比如代码质量问题、安全问题等。如果我们每次都手动去检查,不仅效率低下,而且容易出错。

  所以我们可以利用 AI 来帮助我们检查代码,这样可以提高我们的效率

  那么,如何利用 AI 来检查代码呢?


1. 使用 JS 脚本


  这种方法其实就是写一个简单的脚本,通过调用 OpenAI 的 API,将代码提交给 AI 进行评审。
  这里我们需要使用 Node.js 来实现这个功能。利用 git 的 pre-commit hooks,在 git 提交前执行这个脚本。整体流程如下:


flow.png


  接下来我们来具体实现下代码。在项目根目录下新建一个pre-commit.js文件,这个文件就是我们的脚本。


1.1 校验暂存区代码


  通过 git diff --cached 验证是否存在待提交内容,如果没有改动则直接退出提交。


const { execSync } = require('child_process');
const checkStaged = () => {
try {
const changes = execSync("git diff --cached --name-only").toString().trim();
if (!changes) {
console.log("No staged changes found.");
process.exit(0);
}
} catch (error) {
console.error("Error getting staged changes:", error.message);
process.exit(1);
}
}

1.2 获取差异内容


const getDiff = () => {
try {
const diff = execSync("git diff --cached").toString();
if (!diff) {
console.log("No diff content found.");
process.exit(0);
}
return diff;
} catch (error) {
console.error("Error getting diff content:", error.message);
process.exit(1);
}
}

1.3 准备prompt


  这里我们需要准备一个 prompt,这个 prompt 就是用来告诉 AI 我们要检查什么内容。


const getPrompt = (diff) => {
return `
你是一名代码审核员,专门负责识别git差异中代码的安全问题和质量问题。您的任务是分析git 差异,并就代码更改引入的任何潜在安全问题或其他重大问题提供详细报告。
这里是代码差异内容:
${diff}
请根据以下步骤完成分析:
1.安全分析:
- 查找由新代码引发的一些潜在的安全漏洞,比如:
a)注入缺陷(SQL注入、命令注入等)
b)认证和授权问题
...

2. 代码逻辑和语法分析:
-识别任何可能导致运行时错误的逻辑错误或语法问题,比如:
a)不正确的控制流程或条件语句
b)循环使用不当,可能导致无限循环
...

3. 报告格式:
对于每个发现的问题,需要按照严重等级分为高/中/低。
每个问题返回格式如下:
-[严重等级](高中低)- [问题类型](安全问题/代码质量) - 问题所在文件名称以及所在行数
- 问题原因 + 解决方案

4. 总结:
在列出所有单独的问题之后,简要总结一下这些变化的总体影响,包括:
-发现的安全问题数量(按严重程度分类)
-发现的代码质量问题的数量(按严重性分类)

请现在开始你的分析,并使用指定的格式陈述你的发现。如果没有发现问题,请在报告中明确说明。
输出应该是一个简单的结论,无论是否提交这些更改,都不应该输出完整的报告。但是要包括文件名。并将每行标识的问题分别列出。

如果存在高等级的错误,就需要拒绝提交
回答里的结尾需要单独一行文字 "COMMIT: NO" 或者 "COMMIT: YES" 。这将用来判断是否允许提交
`

}

1.4 定义一个 AI 执行器


  这里我用 chatgpt 实现的,具体代码如下:


const execCodeReviewer = (text) => {
const apiKey = ''
const apiBaseUrl = ''
const translateUrl = `${apiBaseUrl}/v1/chat/completions`
return new Promise((resolve, reject) => {
fetch(translateUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
stream: false,
messages: [
{
role: 'user',
content: text,
},
],
}),
})
.then(res => res.json().then(data => resolve(data.choices[0].message.content)))
.catch(err => {
console.error(err)
})
})
}

1.5 结果处理


  这里我们需要解析一下结果,提取结果中是否包含 "COMMIT: YES"关键字,有则允许提交,否则不允许提交并打印结果


const handleReviewResult = (result) => {
const decision = result.includes("COMMIT: YES") ? "YES" : "NO";
if (decision === 'NO') {
console.log("\nCritical issues found. Please address them before committing.");
console.log(details);
process.exit(1);
}
console.log("\nCommit approved.");
}

1.6 主函数执行整个流程


const main = async () => {
try {
checkStaged();
const diffContent = getDiff();
console.log("Running code review...");

const prompt = getPrompt(diffContent);
const reseponse = await execCodeReviewer(prompt);
handleReviewResult(reseponse)

process.exit(0);
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}

1.7 git hooks里添加执行该脚本逻辑


  进入项目根目录,在这里运行 git bash。打开pre-commit钩子文件


vim .git/hooks/pre-commit

  然后添加以下内容


#!/bin/sh
GIT_ROOT=$(git rev-parse --show-toplevel)
node "$GIT_ROOT/pre-commit.js"
exit $?

保存退出后,我们就可以使用 git 做下测试。


1.8 测试


  我新建了一个 test.js 文件,然后添加如下代码:


const fn = () => {
let num = 0
for(let i = 0; true; i++) {
num += i
}
}

  然后执行 git add . 然后 git commit -m "test"。效果如下:


node-ai.png


  看来还是不错的,有效识别代码中的逻辑缺陷与语法隐患(如无限循环、变量误用等),同时当不满足提交条件后,也是直接终止了 commit。这里面其实比较关键的是 prompt 的内容,ai 评审的效果主要就是取决于它。


2. ai-pre-commit-reviewer 插件


  上面我们是通过 js 脚本来实现的,其实也可以通过现成插件来实现。原理和第一个方法是一样的,只不过是插件帮我们封装好了,我们只需要配置下即可。并且该插件支持多种 AI 供应商,比如 openAI,deepseek,本地的Ollama和LM Studio。插件地址,欢迎大家star。


2.1 安装插件


npm install ai-pre-commit-reviewer --save-dev

#安装完成后执行

npx add-ai-review #添加执行逻辑到git pre-commit钩子中

2.2 配置文件


  插件安装完成后,新建一个.env 文件


baseURL= *** #模型服务地址
apiKey=*** #模型服务密钥
language=chinese #语言


2.3 效果预览


pre-commit.png


  也可以配合husky使用,进行语法检查后执行code review。


husky.png


  我这里也是更推荐大家使用这个,简单易上手。


3. gerrit + ai-code-review


  Gerrit 是由 Google 开发的代码审查管理系统,基于 Git 版本控制系统构建,主要特性包括:



  • 强制代码审查机制:所有代码必须通过人工/自动化审查才能合并

  • 细粒度权限控制:支持基于项目/分支的访问权限管理

  • 在线代码对比:提供可视化差异查看界面(Side-by-Side Diff)

  • 插件扩展体系:可通过插件集成 CI/CD、静态分析等工具


  其核心功能主要是通过 refs/for/ 推送机制,确保所有代码变更必须通过审核。因此我们可以利用 ai 代替人工去执行代码 review,这样效率也会更高效。


2.1 gerrit 安装与配置


# 执行以下命令
docker pull gerritcodereview/gerrit:latest

  安装完后可以看下容器列表


gerrit.png


  没问题后启动服务,然后在浏览器中访问 http://localhost:8080/ 就可以看到gerrit首页


2.1.1 配置 ssh 密钥

ssh-keygen -t ed25519 -C "your_email@example.com"
# 直接按3次回车(不要设置密码)
cat ~/.ssh/id_ed25519.pub # 复制输出的内容

  然后在 "settings" 页面中选择左侧的"SSH Keys",将复制的公钥内容粘贴进去。添加完成后测试下连接情况。


ssh -p 29418 admin@localhost  # 输入yes接受指纹

  看到 Welcome to Gerrit Code Review 表示成功


2.1.2 拉取项目测试

  可以在 BROWSE > Repositories 里查看当前项目列表,我这里用 All-Projects 做下测试,理论上是要新建项目的。


git clone "ssh://admin@localhost:29418/All-Projects"

  安装 Gerrit 提交钩子 commit-msg(必须!)。Gerrit 依赖 commit-msg 钩子实现以下功能:



  1. 生成 Change-Id:每个提交头部自动添加唯一标识符,格式示例 Change-Id: I7e5e94b9e6a4d8b8c4f3270a8c6e9d3b1a2f5e7d

  2. 校验提交规范: 确保提交信息符合团队约定格式(如包含任务编号)

  3. 防止直接推送: 强制推送到 refs/for/ 路径而非主分支


cd All-Projects
curl -Lo .git/hooks/commit-msg http://localhost:8080/tools/hooks/commit-msg
chmod +x .git/hooks/commit-msg

  然后新建个js文件,写点代码并提交。


git push origin HEAD:refs/for/refs/meta/config # 提交到 refs/meta/config 分支

  然后在gerrit首页可以看到刚刚提交的代码,点击查看详情,可以看到代码审核的流程。


review.png


2.2 插件安装和配置


  将 ai-code-review 插件克隆到本地。插件详情可参考官方文档。此插件可以使用不同的 AI Chat 服务(例如 ChatGPT 或 OLLAMA)


git clone https://gerrit.googlesource.com/plugins/ai-code-review

  安装 Java 和构建工具


sudo apt update
sudo apt install -y openjdk-21-jdk maven # 官方文档说 11 就行,但是我实际上跑了后发现需要 JDK 21+

  进去项目目录构建 JAR 包


cd ai-code-review
mvn clean package

  当输出BUILD BUILD SUCCESS时,表示构建成功。进入目录看下生成的包名。


jarname.png


  然后将生成的jar包复制到 gerrit 的 plugins 目录下


# 我这里容器名为 gerrit,JAR 文件在 target/ 目录
docker cp target/ai-code-review-3.11.0.jar gerrit:/var/gerrit/plugins/

  然后进入容器内看下插件列表,确认插件已经安装成功


jar.png


  也可以在 gerrit 网页端查看插件启动情况


plugin.png


  接着修改配置文件,在 gerrit 的 etc 目录下找到 gerrit.config 文件。但在这之前需要在 Gerrit 中创建一个 AI Code Review 用户,这个席位用于 AI 来使用进行代码评审。


vi var/gerrit/etc/gerrit.config

  在文件里添加以下内容。


[plugin "ai-code-review"]
model = deepseek-v3
aiToken = ***
aiDomain = ***
gerritUserName = AIReviewer
aiType = ChatGPT
globalEnable = true 。


  • model(非必填): 使用的模型

  • aiToken(必填): AI模型的密钥

  • aiDomain(非必填): 请求地址,默认是 api.openai.com

  • gerritUserName(必填): AI Code Review 用户的 Gerrit 用户名。我这里创建的用户名为 AIReviewer

  • aiType(非必填): AI类型,默认是 ChatGPT

  • globalEnable(非必填): 是否全局启用,默认是 false, 表示插件将仅审核指定的仓库。如果不设置为true的话。需要添加enabledProjects参数,指定要运行的存储库,例如:“project1,project2,project3”。


  更多字段配置参考官方文档


  这些都完成后,重启 gerrit 服务。然后修改下代码,写段明显有问题的代码,重新 commit 并 push 代码,看下 AI 代码评审的效果怎么样。


ai-review.png


  可以看到 ai 审查代码的效果还是不错的。当然我这里是修改了插件的prompt,让它用中文生成评论,它默认是用英文回答的。


总结


  现在AI功能越来越强大,可以帮我们处理越来越多的事情。同时我也开发了一个工具AI-vue-i18n,能够智能提取代码中的中文内容,并利用AI完成翻译后生成多语言配置文件。告别手动配置的场景。

文章地址

github地址


作者:puppy0_0
来源:juejin.cn/post/7504567245265846272
收起阅读 »

进入外包,我犯了所有程序员都会犯的错!

前言 前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。 本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。 1. 上岸折戟尘沙 本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。 上网课期间就向往大城市,...
继续阅读 »

前言


前些天有位小伙伴和我吐槽他在外包工作的经历,语气颇为激动又带着深深的无奈。



image.png


本篇以他的视角,进入他的世界,看看这一段短暂而平凡的经历。


1. 上岸折戟尘沙


本人男,安徽马鞍山人士,21年毕业于江苏某末流211,在校期间转码。

上网课期间就向往大城市,于是毕业后去了深圳,找到了一家中等IT公司(人数500+)搬砖,住着宝安城中村,来往繁华南山区。

待了三年多,自知买房变深户无望,没有归属感,感觉自己也没那么热爱技术,于是乎想回老家考公务员,希望待在宇宙的尽头。

24年末,匆忙备考,平时工作忙里偷闲刷题,不出所料,笔试卒,梦碎。


2. 误入外包


复盘了备考过程,觉得工作占用时间过多,想要找一份轻松点且离家近的工作,刚好公司也有大礼包的指标,于是主动申请,辞别深圳,前往徽京。

Boss上南京的软件大部分是外包(果然是外包之都),前几年外包还很活跃,这些年外包都沉寂了不少,找了好几个月,断断续续有几个邀约,最后实在没得选了,想着反正就过渡一下挣点钱不寒碜,接受了外包,作为WX服务某为。薪资比在深圳降了一些,在接受的范围内。


想着至少苟着等待下一次考公,因此前期做项目比较认真,遇到问题追根究底,为解决问题也主动加班加点,同为WX的同事都笑话我说比自有员工还卷,我却付之一笑。


直到我经历了几件事,正所谓人教人教不会,事教人一教就会。


3. 我在外包的二三事


有一次,我提出了自有员工设计方案的衍生出的一个问题,并提出拉个会讨论一下,他并没有当场答应,而是回复说:我们内部看看。

而后某天我突然被邀请进入会议,聊了几句,意犹未尽之际,突然就被踢出会议...开始还以为是某位同事误触按钮,然后再申请入会也没响应。

后来我才知道,他们内部商量核心方案,因为权限管控问题,我不能参会。

这是我第一次体会到WX和自有员工身份上的隔阂。


还有一次和自有员工一起吃饭的时候,他不小心说漏嘴了他的公积金,我默默推算了一下他的工资至少比我高了50%,而他的毕业院校、工作经验和我差不多,瞬间不平衡了。


还有诸如其它的团建、夜宵、办公权限、工牌等无一不是明示着你是外包员工,要在外包的规则内行事。
至于转正的事,头上还有OD呢,OD转正的几率都很低,好几座大山要爬呢,别想了。


3. 反求诸己


以前网上看到很多吐槽外包的帖子,还总觉得言过其实,亲身经历了才刻骨铭心。

我现在已经摆正了心态,既来之则安之。正视自己WX的身份,给多少钱干多少活,给多少权利就承担多少义务。

不攀比,不讨好,不较真,不内耗,不加班。

另外每次当面讨论的时候,我都会把工牌给露出来,潜台词就是:快看,我就是个外包,别为难我😔~


另外我现在比较担心的是:



万一我考公还是失败,继续找工作的话,这段外包经历会不会是我简历的污点😢



当然这可能是我个人感受,其它外包的体验我不知道,也不想再去体验了。

对,这辈子和下辈子都不想了。
附南京外包之光,想去或者不想去的伙伴可以留意一下:



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7511582195447824438
收起阅读 »

TensorFlow.js 和 Brain.js 全面对比:哪款 JavaScript AI 库更适合你?

web
温馨提示 由于篇幅较长,为方便阅读,建议按需选择章节,也可收藏备用,分段消化更高效哦!希望本文能为你的前端 AI 开发之旅提供实用参考。 😊 引言:前端 AI 的崛起 在过去的十年里,人工智能(AI)技术的飞速发展已经深刻改变了各行各业。从智能助手到自动驾驶...
继续阅读 »

温馨提示


由于篇幅较长,为方便阅读,建议按需选择章节,也可收藏备用,分段消化更高效哦!希望本文能为你的前端 AI 开发之旅提供实用参考。 😊



引言:前端 AI 的崛起


在过去的十年里,人工智能(AI)技术的飞速发展已经深刻改变了各行各业。从智能助手到自动驾驶,从图像识别到自然语言处理,AI 的应用场景几乎无处不在。而对于前端开发者来说,AI 的魅力不仅在于其强大的功能,更在于它已经走进了浏览器,让客户端也能够轻松承担起机器学习的任务。


试想一下,当你开发一个 Web 应用,需要进行图像识别、文本分析、语音识别或其他 AI 任务时,你是否希望直接在浏览器中处理这些数据,而无需依赖远程服务器?如果能在用户的设备上本地运行这些任务,不仅可以大幅提升响应速度,还能减少服务器资源的消耗,为用户提供更流畅的体验。


这正是 TensorFlow.jsBrain.js 两款库所带来的变革。它们使开发者能够在浏览器中轻松实现机器学习任务,甚至支持训练和推理深度学习模型。虽然这两款库在某些功能上有相似之处,但它们的定位和特点却各有侧重。


TensorFlow.js 是由 Google 推出的深度学习框架,它为浏览器端的机器学习提供了强大的支持,能够处理从图像识别到自然语言处理的复杂任务。基于 WebGL 提供加速,TensorFlow.js 可以充分利用硬件性能,实现大规模数据处理和复杂模型推理。


TensorFlow.js 不仅功能强大,还能直接在浏览器中运行复杂的机器学习任务,例如图像识别和处理。如果你想深入了解如何使用 TensorFlow.js 构建智能图像处理应用,可以参考我的另一篇文章:纯前端用 TensorFlow.js 实现智能图像处理应用(一)


相比之下,Brain.js 是一款轻量级神经网络库,专注于简单易用的神经网络模型。它的设计目标是降低机器学习的入门门槛,适合快速原型开发和小型应用场景。尽管 Brain.js 不具备 TensorFlow.js 那样强大的深度学习能力,但它的简洁性和易用性使其成为许多开发者快速实验和实现基础 AI 功能的优选工具。


然而,选择哪款库作为前端 AI 的工具并不简单,这取决于项目的需求、性能要求以及学习成本等多个因素。本文将详细对比两款库的功能、优缺点及适用场景,帮助你根据需求选择最适合的工具。


无论你是 AI 初学者还是有经验的开发者,相信你都能从这篇文章中找到有价值的指导,助力你在浏览器端实现机器学习。准备好了吗?让我们一起探索 TensorFlow.jsBrain.js 的世界,发现它们的不同之处,了解哪一个更适合你的项目。




一、TensorFlow.js - 强大而复杂的深度学习库


TensorFlow


1.1 TensorFlow.js 概述


TensorFlow.js 是由 Google 推出的开源 JavaScript 库,用于在浏览器和 Node.js 环境中执行机器学习任务,包括深度学习模型的推理和训练。它是 TensorFlow 生态的一部分,TensorFlow 是全球最受欢迎的深度学习框架之一,广泛应用于计算机视觉、自然语言处理等领域。


TensorFlow.js 的核心亮点在于其 跨平台支持。你可以在浏览器端运行,也可以在 Node.js 环境下执行,灵活满足不同开发需求。此外,它支持导入已训练好的 TensorFlowKeras 模型,在浏览器或 Node.js 中进行推理,无需重新训练。这使得 AI 的开发更加高效和便捷。


1.2 TensorFlow.js 的功能特点


TensorFlow.js 提供了丰富的功能,覆盖从简单的机器学习到复杂的深度学习任务。以下是它的核心特点:



  1. 浏览器端深度学习推理:通过 WebGL 加速,TensorFlow.js 可以高效地在浏览器中加载和运行深度学习模型,无需依赖服务器,大幅提升用户体验和响应速度。

  2. 训练与推理一体化TensorFlow.js 支持在前端环境直接训练神经网络,这对于动态数据更新和快速迭代非常有用。即使是复杂的深度学习模型,也能通过优化技术确保高效的训练过程。

  3. 支持复杂神经网络架构:包括卷积神经网络(CNN)、循环神经网络(RNN)、以及高级模型如 Transformer,适用于图像、语音、文本等多领域任务。

  4. 模型导入与转换:支持从其他 TensorFlowKeras 环境导入已训练的模型,并在浏览器或 Node.js 中高效运行,降低了开发门槛。

  5. 跨平台支持:无论是前端浏览器还是后端 Node.jsTensorFlow.js 都可以灵活适配,特别适合需要多环境协作的项目。


1.3 TensorFlow.js 的优势与应用场景


优势:


  1. 本地化计算:无需数据传输到服务器,所有计算均在用户设备上完成,提升速度并保障隐私。

  2. 强大的生态支持:依托 TensorFlow 的生态系统,TensorFlow.js 可以轻松访问预训练模型、教程和工具。

  3. 灵活性与高性能:支持低级别 APIWebGL 加速,可根据需求灵活调整模型和计算流程。

  4. 无需后台服务器:在浏览器中即可完成复杂的训练和推理任务,显著简化系统架构。


应用场景:


  1. 图像识别:例如手写数字识别、人脸检测、物体分类等实时图像处理任务。

  2. 自然语言处理:支持情感分析、文本分类、语言翻译等复杂 NLP 任务。

  3. 实时数据分析:适用于 IoT 或其他需要即时数据处理和反馈的应用场景。

  4. 推荐系统:通过用户行为数据构建个性化推荐,例如电商、新闻或社交媒体应用。


1.4 TensorFlow.js 基本用法示例


以下是一个简单示例,展示如何使用 TensorFlow.js 构建并训练神经网络模型。


安装与引入 TensorFlow.js


  1. 通过 CDN 引入:


    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>


  2. 通过 npm 安装(适用于 Node.js 环境):


    npm install @tensorflow/tfjs



创建简单神经网络

以下示例创建了一个简单的前馈神经网络,用于处理二分类问题:


// 导入 TensorFlow.js
const tf = require('@tensorflow/tfjs');

// 创建一个神经网络模型
const model = tf.sequential();

// 添加隐藏层(10 个神经元)
model.add(tf.layers.dense({ units: 10, activation: 'relu', inputShape: [5] }));

// 添加输出层(2 类分类问题)
model.add(tf.layers.dense({ units: 2, activation: 'softmax' }));

// 编译模型
model.compile({
 optimizer: 'adam',
 loss: 'categoricalCrossentropy',
 metrics: ['accuracy'],
});

训练和推理过程

训练模型需要提供输入数据(特征)和标签(目标值):


// 创建训练数据
const trainData = tf.tensor2d([[0, 1, 2, 3, 4], [1, 2, 3, 4, 5], [2, 3, 4, 5, 6]]);
const trainLabels = tf.tensor2d([[1, 0], [0, 1], [1, 0]]);

// 训练模型
model.fit(trainData, trainLabels, { epochs: 10 }).then(() => {
 // 使用新数据进行推理
 const input = tf.tensor2d([[1, 2, 3, 4, 5]]);
 model.predict(input).print();
});



二、Brain.js - 轻量级且易于使用的神经网络库


Brain


2.1 Brain.js 概述


Brain.js 是一个轻量级的开源 JavaScript 神经网络库,专为开发者提供快速、简单的机器学习工具。它的设计理念是易用性和轻量化,适合那些希望快速构建和训练神经网络的开发者,尤其是机器学习的新手。


与功能丰富的 TensorFlow.js 不同,Brain.js 更注重于直观和简单,能够帮助开发者快速完成从构建到推理的基本机器学习任务。虽然它不支持复杂的深度学习模型,但其易用性和小巧的特性,使其成为小型项目和快速原型开发的理想选择。


2.2 Brain.js 的功能特点


Brain.js 的功能主要集中在简化神经网络的构建与训练上,以下是其核心特点:



  1. 简单易用的 APIBrain.js 提供了直观的接口,开发者无需复杂的机器学习知识,也能轻松上手并实现神经网络任务。

  2. 轻量级:相较于体积较大的 TensorFlow.jsBrain.js 的核心库更为小巧,非常适合嵌入前端应用,且不会显著影响加载速度。

  3. 支持多种网络结构:前馈神经网络(Feedforward Neural Network)、LSTM 网络(Long Short-Term Memory)等。这些模型已足够应对大多数基础的机器学习需求。

  4. 快速训练与推理:通过几行代码即可完成训练与推理任务,适用于快速原型设计和验证。

  5. 同步与异步训练支持Brain.js 同时支持同步和异步的训练过程,开发者可以根据项目需求选择合适的方式。


2.3 Brain.js 的优势与应用场景


优势:



  1. 快速原型开发:开发者可以用最少的代码完成神经网络的构建和训练,特别适合需要快速验证想法的场景。

  2. 轻量级与高效率:库的体积较小,能快速加载,适合资源有限的环境。

  3. 易于集成Brain.js 非常适合嵌入 Web 应用或小型 Node.js 服务,集成简单。

  4. 适合初学者Brain.js 的设计对机器学习新手友好,无需深入了解复杂的深度学习算法即可上手。


应用场景:


  1. 基础分类与预测任务:适合实现简单的分类任务或数值预测,例如时间序列预测、情感分析等。

  2. 教学与实验:对于机器学习教学或学习过程中的快速实验,Brain.js 是一个很好的工具。

  3. 轻量化应用:例如小型交互式 Web 应用中实时处理用户输入。


2.4 Brain.js 基本用法示例


以下示例展示了如何使用 Brain.js 构建并训练一个简单的神经网络模型。


安装与引入


  1. 通过 npm 安装


    npm install brain.js


  2. 通过 CDN 引入


    <script src="https://cdn.jsdelivr.net/npm/brain.js"></script>



创建简单神经网络

以下代码创建了一个用于解决 XOR 问题的前馈神经网络:


// 引入 Brain.js
const brain = require('brain.js');

// 创建一个简单的神经网络实例
const net = new brain.NeuralNetwork();

// 提供训练数据
const trainingData = [
{ input: [0, 0], output: [0] },
{ input: [0, 1], output: [1] },
{ input: [1, 0], output: [1] },
{ input: [1, 1], output: [0] }
];

// 训练网络
net.train(trainingData);

// 测试推理
const output = net.run([1, 0]);
console.log(`预测结果: ${output}`); // 输出接近 1 的值

训练与推理参数调整

Brain.js 提供了一些可选参数,用于优化训练过程,例如:



  • 迭代次数(iterations :设置训练的最大轮数。

  • 学习率(learningRate :控制每次更新的步长。


以下示例展示了如何自定义训练参数:


net.train(trainingData, {
iterations: 1000, // 最大训练轮数
learningRate: 0.01, // 学习率
log: true, // 显示训练过程
logPeriod: 100 // 每 100 次迭代打印一次日志
});

// 推理新数据
const testInput = [0, 1];
const testOutput = net.run(testInput);
console.log(`输入: ${testInput}, 预测结果: ${testOutput}`);



三、TensorFlow.jsBrain.js 的全面对比


在这一章中,我们将从多个维度对 TensorFlow.jsBrain.js 进行详细对比,帮助开发者根据自己的需求选择合适的工具。对比内容涵盖技术实现差异、学习曲线、适用场景、性能表现以及生态系统和社区支持。


3.1 技术实现差异


TensorFlow.jsBrain.js 的技术实现差异显著,主要体现在功能复杂度、支持的模型类型和底层架构上:



  • TensorFlow.js 是一个功能全面的深度学习框架,基于 TensorFlow 的设计思想,提供了复杂的神经网络架构和高效的数学计算支持。它支持卷积神经网络(CNN)、循环神经网络(RNN)、生成对抗网络(GAN)等多种模型类型,能够完成从图像识别到自然语言处理的复杂任务。借助 WebGL 技术,TensorFlow.js 可在浏览器中高效进行高性能计算,尤其适合大规模数据和复杂模型。

  • Brain.js 则更加轻量,主要面向快速开发和简单任务。它支持前馈神经网络(Feedforward Neural Network)、长短期记忆网络(LSTM)等基础模型,适合处理简单的分类或预测问题。尽管功能不如 TensorFlow.js 广泛,但其简洁的设计使开发者能够快速上手,完成实验和小型项目。


总结TensorFlow.js 更加强大,适用于复杂任务;Brain.js 简单轻便,适合快速开发和小型应用。


3.2 学习曲线与开发者体验


在学习曲线和开发体验方面,两者差异明显:



  • TensorFlow.js 学习曲线较为陡峭。其功能强大且覆盖面广,但开发者需要了解深度学习的基础知识,包括模型训练、数据预处理等环节。尽管文档和教程丰富,但对初学者而言,掌握这些内容可能需要投入更多的时间和精力。

  • Brain.js 则以简洁直观的 API 著称,初学者可以通过几行代码实现神经网络的搭建与训练。它对复杂概念的抽象程度高,无需深入理解深度学习理论,便能快速完成任务。


总结:如果你是新手或需要快速实现一个简单模型,选择 Brain.js 更友好;而如果你已有一定经验,并计划处理复杂任务,则 TensorFlow.js 更适合。


3.3 适用场景与功能选择


根据应用场景,选择合适的库可以大大提高开发效率:



  • TensorFlow.js:适用于复杂任务,如图像识别、自然语言处理、视频分析或推荐系统。由于其强大的深度学习功能和高性能计算能力,TensorFlow.js 特别适合大规模数据处理和精度要求高的场景。

  • Brain.js:适合轻量级任务,例如简单的分类、回归、时间序列预测等。对于快速验证模型或开发原型,Brain.js 提供了简单高效的解决方案,尤其是在浏览器端运行时无需依赖复杂的服务器计算。


总结TensorFlow.js 面向复杂场景和大规模任务;Brain.js 更适合轻量化需求和快速开发。


3.4 性能对比


在性能方面,TensorFlow.jsBrain.js 存在显著差异:



  • TensorFlow.js 借助 WebGL 实现高效的硬件加速,支持 GPU 并行计算。在处理大规模数据集和复杂模型时,其性能优势显著,适用于高负载、高计算量的场景。

  • Brain.js 性能较为有限,主要针对小型数据集和简单任务。由于其轻量级设计,虽然在小规模任务中表现出色,但无法与 TensorFlow.js 的硬件加速能力相媲美。


总结:对于需要高性能计算的场景,TensorFlow.js 是更优选择;而对于小型任务,Brain.js 的性能已足够。


3.5 生态系统与社区支持



  • TensorFlow.js:作为 TensorFlow 生态的一部分,TensorFlow.js 享有丰富的社区资源和支持,包括大量的开源项目、教程、论坛和工具。开发者可以从官方文档和预训练模型中快速找到所需资源,支持复杂应用的开发。

  • Brain.js:社区较小,但活跃度高。文档简洁,适合初学者。虽然资源和支持不如 TensorFlow.js 丰富,但足以满足小型项目的需求。


总结TensorFlow.js 的生态更强大,适合需要长期维护和扩展的项目;Brain.js 更适合轻量化开发和快速上手。




四、如何选择最适合你的库?


TensorFlow.jsBrain.js 之间做出选择时,开发者需要综合考虑项目需求、技术背景和性能要求。这两款库各有特色:TensorFlow.js 功能强大,适用于复杂任务;Brain.js 简单易用,适合快速开发。以下从选择标准和实际场景出发,帮助开发者找到最合适的工具。


4.1 选择标准


在选择 TensorFlow.jsBrain.js 时,可参考以下几个关键标准:



  1. 功能需求



    • 复杂任务:如果项目涉及深度学习任务(如大规模图像分类、语音识别或自然语言处理),选择 TensorFlow.js 更为合适。它支持复杂的神经网络模型,具备高效的数据处理能力。

    • 基础任务:如果需求相对简单,例如小型神经网络模型、时间序列预测或分类任务,Brain.js 是更轻量的选择。



  2. 开发者经验



    • 有机器学习背景TensorFlow.js 提供高度灵活的 API,但学习曲线较陡。熟悉机器学习的开发者可以充分利用其强大功能。

    • 初学者Brain.js 更适合新手,提供简洁的接口和直观的使用体验。



  3. 性能需求



    • 高性能计算:如果项目需要硬件加速(如 GPU 支持)以处理大规模数据,TensorFlow.jsWebGL 支持是理想选择。

    • 轻量化应用:对于性能要求较低的场景,Brain.js 的轻量级设计足够满足需求。



  4. 项目规模与复杂度



    • 大型项目TensorFlow.js 提供复杂功能和强大的扩展性,适合长期维护和生产级应用。

    • 快速开发Brain.js 专注于快速实现小型项目,适合验证想法或开发 MVP(最小可行产品)。






4.2 基于项目需求的选择建议


以下是根据常见场景的具体选择建议:


场景一:图像分类应用



  • 需求:对大规模图像进行分类或识别,涉及复杂的卷积神经网络(CNN)。

  • 推荐选择TensorFlow.js。支持复杂模型架构,通过 WebGL 提供高效的硬件加速,适合处理大量图像数据。


场景二:实时数据分析与预测



  • 需求:对传感器数据进行实时监测和分析,预测未来趋势(如气象预测、股票走势)。

  • 推荐选择Brain.js。其轻量化和快速实现的特性非常适合实时数据处理和快速部署。


场景三:自然语言处理(NLP)应用



  • 需求:需要对文本数据进行分类、情感分析或对话生成。

  • 推荐选择TensorFlow.js。支持循环神经网络(RNN)、Transformer 等复杂模型,能处理 NLP 任务的高维数据和复杂结构。


场景四:个性化推荐系统



  • 需求:根据用户行为推荐商品或内容。

  • 推荐选择



    • 如果推荐系统复杂,涉及神经协同过滤或深度学习模型,选择 TensorFlow.js

    • 如果系统较为简单,仅需基于用户行为的规则实现,Brain.js 是更高效的选择。




场景五:快速原型开发与实验



  • 需求:验证机器学习模型效果或快速开发实验性产品。

  • 推荐选择Brain.js。它提供简洁的接口和快速训练功能,适合快速搭建和迭代。




结论:最终选择


通过对 TensorFlow.jsBrain.js 的详细对比,可以帮助开发者根据项目需求和个人技能做出最佳选择。以下是两者的优缺点总结及适用场景的建议。


TensorFlow.js 优缺点


优点:



  1. 功能全面:支持复杂的深度学习模型(如 CNNRNNGAN),适用于广泛的机器学习任务,包括图像识别、自然语言处理和语音处理等。

  2. 跨平台支持:可运行于浏览器和 Node.js 环境,灵活部署于多种平台。

  3. 性能卓越:利用 WebGL 实现硬件加速,适合高性能需求,尤其是大规模数据处理。

  4. 强大的生态系统:依托 TensorFlow 生态,拥有丰富的预训练模型、教程和社区支持,为开发者提供充足资源。


缺点:



  1. 学习门槛较高:功能复杂,适合有机器学习基础的开发者,初学者可能需要投入较多时间学习。

  2. 库体积较大:功能的多样性导致库体积偏大,可能影响浏览器加载速度和资源消耗。




Brain.js 优缺点


优点:



  1. 轻量级与易用性:设计简单,API 直观,非常适合快速开发和机器学习初学者。

  2. 小巧体积:库文件体积小,适合嵌入前端应用,对网页加载影响小。

  3. 支持基础模型:支持前馈神经网络和 LSTM,能满足大多数基础机器学习任务。

  4. 快速上手:开发者无需深厚的机器学习知识,能够快速实现简单神经网络应用。


缺点:



  1. 功能较为局限:不支持复杂深度学习模型,难以满足高阶任务需求。

  2. 性能有限:轻量设计决定其在大规模数据处理中的性能不如 TensorFlow.js




适用场景与开发者建议


初学者或简单任务



  • 选择Brain.js

  • 理由:适合刚接触机器学习的开发者,或处理简单分类、时间序列预测等基础任务。其平缓的学习曲线和快速开发特性,帮助初学者快速上手。


经验丰富的开发者或复杂任务



  • 选择TensorFlow.js

  • 理由:适合处理复杂的深度学习任务,如大规模图像识别、自然语言处理或实时视频分析。提供灵活的 API 和强大的计算能力,满足高性能需求。


小型项目与快速开发



  • 选择Brain.js

  • 理由:适合快速构建原型和简单的神经网络任务,易于维护,开发效率高。


大规模应用与高性能需求



  • 选择TensorFlow.js

  • 理由:其强大的加速能力和复杂模型支持,使其成为生产级应用的理想选择,尤其适合需要 GPU 加速的大规模任务。




结语


通过本文的对比,读者可以清晰了解 TensorFlow.jsBrain.js 在功能、性能、学习曲线、适用场景等方面的显著差异。选择最适合的库时,需要综合考虑项目的复杂度、团队的技术背景以及性能需求。


如果你的项目需要处理复杂的深度学习任务,并且需要高性能计算与广泛的社区支持,TensorFlow.js 是不二之选。它功能强大、生态丰富,适合图像识别、自然语言处理等高需求场景。而如果你只是进行小型神经网络实验,或需要快速原型开发,Brain.js 提供了更简洁易用的解决方案,是初学者和小型项目开发者的理想选择。


无论选择哪个库,充分了解它们的优势与限制,将帮助你在项目开发中高效使用这些工具,成功实现你的前端 AI 开发目标。




附录:对比表格


以下对比表格总结了 TensorFlow.jsBrain.js 在关键维度上的差异,帮助读者快速决策:


特性TensorFlow.jsBrain.js
GitHub 星标数量18.6K14.5K
功能复杂度高,支持复杂的深度学习模型(CNN, RNN, GAN等)低,支持基础前馈神经网络和LSTM网络
学习曲线陡峭,适合有深度学习经验的开发者平缓,适合初学者和快速原型开发
使用场景复杂场景,如大规模数据处理、图像识别、语音处理等小型项目,如简单分类任务、时间序列预测
支持的模型类型多种类型(CNN, RNN, GAN等复杂模型)基础类型(前馈神经网络、LSTM等)
性能优化支持 WebGL 加速和 GPU 并行计算,适合高性能需求不支持硬件加速,适合小规模数据处理
开发平台浏览器和 Node.js 环境,跨平台支持主要用于浏览器,也支持 Node.js
社区支持与文档丰富的生态系统,拥有大量教程、示例和预训练模型资源社区较小但活跃,文档简单直观
易用性API 较复杂,适合有深度学习背景的开发者API 简洁,适合初学者和快速开发
适用开发者高阶开发者,有深度学习基础初学者及快速实现简单任务的开发者
体积与资源消耗库文件较大,可能影响加载速度体积小,对网页性能影响较小
训练与推理能力支持复杂模型的训练与推理,适合高需求场景适合简单任务的训练与推理
预训练模型支持支持从 TensorFlow Hub 加载预训练模型不支持广泛预训练模型,主要用于自定义训练

同系列文章推荐


如果你觉得本文对你有所帮助,不妨看看以下同系列文章,深入了解 AI 开发的更多可能性:



欢迎点击链接阅读,开启你的前端 AI 学习之旅,让开发更高效、更有趣! 🚀



我是 “一点一木


专注分享,因为分享能让更多人专注。


生命只有一次,人应这样度过:当回首往事时,不因虚度年华而悔恨,不因碌碌无为而羞愧。在有限的时间里,用善意与热情拥抱世界,不求回报,只为当回忆起曾经的点滴时,能无愧于心,温暖他人。



作者:一点一木
来源:juejin.cn/post/7459285932092211238
收起阅读 »

2026年了,前端到底算不算“夕阳行业”?

你有没有在朋友圈或者知乎上看到过这样的声音:“前端这行是不是快没前途了?”、“前端是夕阳行业,学不起来就晚了”。听起来很吓人吧?今天周五公司不忙~ 所以就想就想聊聊,为什么这些说法有点夸张,而且,实际上,前端比你想的要活跃、要有意思得多。 前端行业现状与就业...
继续阅读 »

你有没有在朋友圈或者知乎上看到过这样的声音:“前端这行是不是快没前途了?”、“前端是夕阳行业,学不起来就晚了”。听起来很吓人吧?今天周五公司不忙~ 所以就想就想聊聊,为什么这些说法有点夸张,而且,实际上,前端比你想的要活跃、要有意思得多。



前端行业现状与就业趋势深入分析


其他废话少说,我先列出一组数据。


市场数据说明:招聘活跃度与求职热度


在判定某个岗位是否是“夕阳行业”前,我们得看看实实在在的数据,而不是空谈。虽然我们没有官方完整的每月统计数据,但从招聘平台侧面指标可以窥见市场动态:


BOSS直聘平台整体使用频次趋势(2024 年)

数据来自行业研究监测,反映招聘平台月度活跃度(平台月访问次数,单位为万次)。它可以折射出用户在找工作和发布岗位的活跃程度:


月份Boss直聘(万次)前程无忧(万次)智联招聘(万次)
2024‑011212.8503.3381.6
2024‑032271.8958.5660.3
2024‑051892.9730.1496.5
2024‑091861.9695.1465.5
2024‑121492.8665.7432.8

从这张表可以看到几个趋势:



  • 春节前后及 3 月、4 月经常会有求职与招聘高峰,这与校园招聘和年终奖金兑现周期有关。

  • Boss直聘的整体使用频次明显高于其他招聘平台,表明它在人才市场中具有更高的活跃度。


这说明整体就业市场并没有冷却到技术岗位“没市场”的程度,但伴随着整体求职竞争压力也在增加(尤其毕业季之后)。


2024–2025 前端岗位薪资与供需情况(综合公开数据)


下面给出一个简要的薪资与供需趋势对比,是基于公开行业报告和招聘平台上职位薪资调研整理的(单位:人民币):


前端薪资水平(2024–2025)


类型数据来源平均薪资(月)说明
全国前端平均薪资招聘求职网站综合数据~20,877 元/月2024 年全国平均数据,样本规模较大
数字前端工程师高薪技术岗位脉脉高聘年度报告~67,728 元/月仅针对极高端职位薪资榜首人才
BOSS直聘高级前端岗位示例招聘岗位样例20K–50K /月典型一线城市高级薪资范围
企业大厂前端薪资公司薪资水平数据~57–65 万/年P6(技术中高级)年薪典型值


小结:大厂或高级岗位薪资明显高于平均,而整体前端岗薪资按城市和经验差异明显(北上深等一线城市更高)。中高级工程师薪资已进入较高收入层。





前端岗位供需趋势(24 年–25 年)


真实可公开的按月份招聘/求职人数统计不容易直接获得(需付费或数据授权),但我们可以根据人才供需比报告和其他间接指标构建趋势理解:


人才供需比(供给 vs 需求)变化


数据年份/区间人才供需比(整体技术类)解读
2022 全年1.29约 1.3 求职者争一岗
2023 全年2.00竞争更激烈
2024 1‑10 月2.06职位竞争仍然紧张

供需比上升意味着“求职者数量增速快于岗位数量”,这反映就业市场总体竞争压力上升,但这主要是整体技术类岗位,不仅限前端。技术类岗位中核心和稀缺型(例如 AI、架构方向)仍然紧缺。 开源中国


招聘/求职活跃度趋势示意


timeline
title
2024 : 招聘需求 ↑, 求职人数 ↑
2025 : 招聘需求 ↓, 求职人数 ↑↑



  • 招聘需求在 2024/2025 年虽整体活跃,但增长略收敛。

  • 求职人数增速仍然高(尤其高校毕业生和转行人才增多)。 PDF 文档助手+1


“前端到底是做什么的”


以前的前端,其实很简单——写页面。你写几个 HTML、CSS,再加上点 JS,页面能跑就算完成任务。大部分人只要会写代码,基本就能找到工作。那时候,技术门槛不高,但随之而来的问题是:大家都能做,稀缺性不强。


到了现在,前端已经不是单纯写页面那么简单了。现在你需要考虑性能优化、工程化、架构设计,甚至还得会和 AI 工具配合来提高效率。也就是说,前端的工作量和复杂度已经大幅升级了,光会写代码,已经不再稀缺。


普通前端 / 工程型前端 / 架构型前端


我一般把前端分成三类:



  1. 普通前端

    就是那种把设计稿转成页面的人,写页面、调样式、搞交互。以前,这类岗位很吃香,因为企业只要有人能把界面做出来就行。现在,普通前端的门槛低,但成长空间有限。

  2. 工程型前端

    这类前端不仅会写页面,还懂打包工具、模块化、性能优化、测试、CI/CD,甚至前端安全。他们能把一个项目从零到一搞成可以高效运转的系统。你可以把他们想象成“能写代码,也懂流程的人”,在团队里很吃香。

  3. 架构型前端

    架构型前端更厉害,他们关注的是整个平台的稳定性、可维护性和扩展性。他们设计组件库、微前端架构、前端性能监控体系,甚至参与后端接口设计。换句话说,他们更像“产品工程师”,不仅懂技术,还懂业务。


会写代码不再稀缺,会“用 AI 写代码”才是门槛


你可能注意到了,现在很多人说“前端会写代码不稀缺了”。这是真的。基础的 JS、CSS、HTML 很多人都会,但如果你能用 AI 辅助写代码、自动生成模板、快速优化性能,那才是真正的核心竞争力。就像以前会打字的人很多,但会用 Excel 做财务建模的人少,差距就出来了。


举个例子,现在有些大型项目,我们用 AI 帮忙生成表单验证逻辑,或者做自动化测试脚本,效率能提高好几倍。这种能力,不是简单敲几行代码能替代的。


前端未来,更像产品工程师


所以,到底前端是不是夕阳行业?我觉得恰恰相反。未来的前端,更像产品工程师——你不仅要写代码,还要思考性能、用户体验、架构设计、工程化流程,甚至要和 AI、云端、数据打交道。前端的职业宽度比以前更大,技能组合也更加稀缺。


换句话说,前端不再只是写界面的小伙伴,而是能把技术和产品结合起来,创造可落地系统的人。


总结


不是前端“夕阳”,只是门槛提高了


从薪资和招聘活跃度看:



  • 前端岗位依旧铺开在招聘平台上,高薪职位数量没有消失,只是分布更广、更分层。

  • 高端工程师、架构型前端、全栈/AI 前端人才仍然供不应求。

  • 竞争压力主要来自技术同质化人才与行业整体求职人数增长的趋势(特别是毕业季)。 开源中国


真实情形是:前端并非夕阳,而是在职业形态和薪资结构上出现了更明显的分层


你看到普通前端岗位薪资增长缓慢,是因为市场供给大,但 高技术、高工程化能力者反而更加吃香,门槛变了,而不是需求消失




总结:结合数据再看“前端是否夕阳”


既然有数据支撑,我们再回到那个问题:


前端是否是夕阳行业?结论是:



  1. 前端需求仍在增长 ——招聘平台活跃度高,技术转型需求仍旧带来岗位。

  2. 薪资仍然维持在行业中上水平 ——尤其中高级、工程化岗位。

  3. 市场竞争更激烈 ——求职人数持续增长使得低门槛岗位更难突围。

  4. 分层明显 ——普通前端增长较缓,高技能人才仍稀缺。


所以说:前端不是夕阳行业,前端职业更像是正经历升级版的“技术工程”方向,更接近综合产品工程师,而不是单纯的页面写手。


要在这个岗位上活得更好,与 AI 协作、提升工程化能力、掌握架构与性能优化,成为未来核心竞争力。


数据来源说明


本文涉及的前端薪资、招聘人数、求职人数及市场趋势数据,主要来源公开渠道:




数据仅供行业分析参考,实际薪资及岗位信息可能随城市、公司和岗位等级变化。



作者:狗头大军之江苏分军
来源:juejin.cn/post/7587684397530595355
收起阅读 »

高德地图与Three.js结合实现3D大屏可视化

web
高德地图与Three.js结合实现3D大屏可视化 文末源码地址及视频演示 前言 在智慧城市安全管理场景中,如何将真实的地理信息与3D模型完美结合,实现沉浸式的可视化监控体验?本文将以巡逻犬管理系统的大屏预览功能为例,详细介绍如何通过高德地图API与Thre...
继续阅读 »

高德地图与Three.js结合实现3D大屏可视化



文末源码地址及视频演示



前言


在智慧城市安全管理场景中,如何将真实的地理信息与3D模型完美结合,实现沉浸式的可视化监控体验?本文将以巡逻犬管理系统的大屏预览功能为例,详细介绍如何通过高德地图API与Three.js深度结合,实现3D机械狗模型在地图上的实时巡逻展示。


1 整体效果 全屏展示.png


该系统实现了以下核心功能:



  • 在高德地图上加载并渲染3D机械狗模型

  • 实现模型沿预设路线的自动巡逻动画

  • 镜头自动跟随模型移动,提供沉浸式监控体验

  • 实时显示巡逻进度、告警信息等业务数据


技术栈



  • 高德地图 JS API 2.0:提供地图底图和空间定位能力

  • Three.js r157:3D模型渲染和动画控制

  • Loca 2.0:高德地图数据可视化API,用于镜头跟随

  • React + TypeScript:前端框架和类型支持

  • TWEEN.js:补间动画库,用于平滑的模型移动


一、高德地图初始化


1.1 地图配置


首先需要配置高德地图的加载参数,包括API Key、版本号等:


// src/utils/amapConfig.ts
export const mapConfig = {
key: 'your-amap-key',
version: '2.0',
Loca: {
version: '2.0.0', // Loca版本需与地图版本一致
},
};

// 初始化安全配置(必须在AMapLoader.load之前调用)
export const initAmapSecurity = () => {
if (typeof window !== 'undefined') {
(window as any)._AMapSecurityConfig = {
securityJsCode: 'your-security-code',
};
}
};

1.2 创建地图实例


使用AMapLoader.load加载地图API,然后创建地图实例:


// 设置安全密钥
initAmapSecurity();

// 加载高德地图
const AMap = await AMapLoader.load(mapConfig);

// 创建地图实例,开启3D视图模式
const mapInstance = new AMap.Map(mapContainerRef.current, {
zoom: 13,
center: defaultCenter,
viewMode: '3D', // 关键:必须开启3D模式
resizeEnable: true,
});

2 渲染高德地图日志.png


关键点



  • viewMode: '3D' 必须设置,否则无法使用3D相关功能

  • 需要提前设置安全密钥,否则会报错


1.3 初始化Loca容器


Loca是高德地图的数据可视化容器,用于实现镜头跟随等功能:


const loca = new (window as any).Loca.Container({
map: mapInstance,
zIndex: 9
});

二、创建GLCustomLayer自定义图层


GLCustomLayer是高德地图提供的WebGL自定义图层,允许我们在地图上渲染Three.js内容。


2.1 图层结构


const customLayer = new AMap.GLCustomLayer({
zIndex: 200, // 图层层级,确保模型在最上层
init: async (gl: any) => {
// 在这里初始化Three.js场景、相机、渲染器等
},
render: () => {
// 在这里执行每帧的渲染逻辑
},
});

mapInstance.add(customLayer);

2.2 初始化Three.js场景


init方法中创建Three.js的核心组件:


init: async (gl: any) => {
// 1. 创建透视相机
const camera = new THREE.PerspectiveCamera(
60, // 视野角度
window.innerWidth / window.innerHeight, // 宽高比
100, // 近裁剪面
1 << 30 // 远裁剪面(使用位运算表示大数值)
);

// 2. 创建WebGL渲染器
const renderer = new THREE.WebGLRenderer({
context: gl, // 使用地图提供的WebGL上下文
antialias: false, // 禁用抗锯齿,减少WebGL扩展需求
powerPreference: 'default',
});
renderer.autoClear = false; // 必须设置为false,否则地图底图无法显示
renderer.shadowMap.enabled = false; // 禁用阴影,避免WebGL扩展问题

// 3. 创建场景
const scene = new THREE.Scene();

// 4. 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(1000, -100, 900);
scene.add(directionalLight);
}

关键点



  • renderer.autoClear = false 必须设置,否则会清除地图底图

  • 使用地图提供的gl上下文创建渲染器,实现资源共享


3 坐标轴辅助线.png


三、坐标系统转换


高德地图使用经纬度坐标(WGS84),而Three.js使用3D世界坐标,两者之间的转换是关键。


3.1 获取自定义坐标系统


地图实例提供了customCoords工具,用于坐标转换:


// 获取自定义坐标系统
const customCoords = mapInstance.customCoords;

// 设置坐标系统中心点(重要:必须在设置模型位置前设置)
const center = mapInstance.getCenter();
customCoords.setCenter([center.lng, center.lat]);

3.2 经纬度转3D坐标


使用lngLatsToCoords方法将经纬度转换为Three.js坐标:


// 将经纬度 [lng, lat] 转换为Three.js坐标 [x, z, y?]
const position = customCoords.lngLatsToCoords([
[120.188767, 30.193832]
])[0];

// 注意:返回的数组格式为 [x, z, y?]
// position[0] 对应 Three.js 的 z 轴(纬度)
// position[1] 对应 Three.js 的 x 轴(经度)
// position[2] 对应 Three.js 的 y 轴(高度,可选)

robotGr0up.position.setX(position[1]); // x坐标(经度)
robotGr0up.position.setZ(position[0]); // z坐标(纬度)
robotGr0up.position.setY(position.length > 2 ? position[2] : 0); // y坐标(高度)

坐标轴对应关系



  • 高德地图:X轴(经度),Y轴(纬度),Z轴(高度)

  • Three.js:X轴(右),Y轴(上),Z轴(前)

  • 转换后:position[1] → Three.js X轴,position[0] → Three.js Z轴


3.3 同步相机参数


render方法中,需要同步高德地图的相机参数到Three.js相机:


render: () => {
const { near, far, fov, up, lookAt, position } = customCoords.getCameraParams();

// 同步相机参数
camera.near = near;
camera.far = far;
camera.fov = fov;
camera.position.set(position[0], position[1], position[2]);
camera.up.set(up[0], up[1], up[2]);
camera.lookAt(lookAt[0], lookAt[1], lookAt[2]);
camera.updateProjectionMatrix();

// 渲染场景
renderer.render(scene, camera);

// 必须执行:重新设置three的gl上下文状态
renderer.resetState();
}

四、加载3D模型


4.1 使用GLTFLoader加载模型


import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

const loader = new GLTFLoader();
const modelPath = '/assets/modules/robot_dog/scene.gltf';

const gltf = await new Promise<any>((resolve, reject) => {
loader.load(
modelPath,
(gltf: any) => resolve(gltf),
(progress: any) => {
if (progress.total > 0) {
const percent = (progress.loaded / progress.total) * 100;
console.log('模型加载进度:', percent.toFixed(2) + '%');
}
},
reject
);
});

const robotModel = gltf.scene;

4.2 模型预处理


加载模型后需要进行预处理,包括材质优化、位置调整等:


// 遍历模型所有子对象
robotModel.traverse((child: THREE.Object3D) => {
if (child instanceof THREE.Mesh) {
// 禁用阴影相关功能
child.castShadow = false;
child.receiveShadow = false;

// 简化材质,避免使用需要WebGL扩展的高级特性
if (child.material) {
const materials = Array.isArray(child.material)
? child.material
: [child.material];

materials.forEach((mat: any) => {
// 禁用transmission等高级特性
if (mat.transmission !== undefined) {
mat.transmission = 0;
}
});
}
}
});

// 计算模型边界框并居中
const box = new THREE.Box3().setFromObject(robotModel);
const center = box.getCenter(new THREE.Vector3());

// 将模型居中(X和Z轴)
robotModel.position.x = -center.x;
robotModel.position.z = -center.z;
// 将模型底部放在y=0
robotModel.position.y = -box.min.y;

// 设置模型缩放
const scale = 15;
robotModel.scale.set(scale, scale, scale);

4.3 创建模型组并设置初始旋转


由于高德地图和Three.js的坐标系差异,需要调整模型的初始旋转:


// 创建外层Gr0up用于位置和旋转控制
const robotGr0up = new THREE.Gr0up();
robotGr0up.add(robotModel);

// 设置初始旋转(90, 90, 0)度转换为弧度
const initialRotationX = (Math.PI / 180) * 90;
const initialRotationY = (Math.PI / 180) * 90;
const initialRotationZ = (Math.PI / 180) * 0;
robotGr0up.rotation.set(initialRotationX, initialRotationY, initialRotationZ);

scene.add(robotGr0up);

五、实现镜头跟随


5.1 使用Loca实现镜头跟随


高德地图的Loca API提供了viewControl.addTrackAnimate方法,可以实现镜头自动跟随路径移动:


// 计算路径总距离
let totalDistance = 0;
for (let i = 0; i < paths.length - 1; i++) {
totalDistance += AMap.GeometryUtil.distance(paths[i], paths[i + 1]);
}

// 假设速度是 1.5 m/s
const speed = 1.5;
const duration = (totalDistance / speed) * 1000; // 转换为毫秒

loca.viewControl.addTrackAnimate({
path: paths, // 镜头轨迹,二维数组
duration: duration, // 时长(毫秒)
timing: [[0, 0.3], [1, 0.7]], // 速率控制器
rotationSpeed: 180, // 每秒旋转多少度
}, function () {
console.log('单程巡逻完成');
// 可以在这里处理往返逻辑
});

loca.animate.start(); // 启动动画

5.2 模型位置同步


render方法中,根据地图中心点实时更新模型位置:


render: () => {
// ... 同步相机参数代码 ...

if (robotGr0up && mapInstance && !patrolFinishedRef.current) {
// 获取当前地图中心(镜头跟随会改变地图中心)
const center = mapInstance.getCenter();
if (center) {
// 更新坐标系统中心点为地图中心点
customCoords.setCenter([center.lng, center.lat]);

// 将地图中心转换为Three.js坐标
const position = customCoords.lngLatsToCoords([
[center.lng, center.lat]
])[0];

// 更新模型位置
robotGr0up.position.setX(position[1]);
robotGr0up.position.setZ(position[0]);
robotGr0up.position.setY(position.length > 2 ? position[2] : 0);

// 更新模型旋转(根据地图旋转)
const rotation = mapInstance.getRotation();
if (rotation !== undefined) {
const initialRotationY = (Math.PI / 180) * 90;
robotGr0up.rotation.y = initialRotationY + (rotation * Math.PI / 180);
}
}
}

// 渲染场景
renderer.render(scene, camera);
renderer.resetState();
}

关键点



  • 使用地图中心点作为模型位置,实现精确跟随

  • 在每次render中更新坐标系统中心点,确保坐标转换准确

  • 同步地图旋转角度到模型Y轴旋转


2025-12-21 11.55.23.gif


六、巡逻动画实现


6.1 启动巡逻


当模型加载完成并设置好初始位置后,可以启动巡逻动画:


const startPatrol = (paths: number[][], mapInstance: any, AMap: any) => {
// 停止之前的巡逻
TWEEN.removeAll();
patrolFinishedRef.current = false;

// 保存路径
patrolPathsRef.current = paths;
patrolIndexRef.current = 0;

// 播放前进动画
playAnimation('1LYP'); // 播放行走动画

// 设置坐标系统中心点为路径起点
const firstPoint = paths[0];
customCoordsRef.current.setCenter([firstPoint[0], firstPoint[1]]);

// 使用Loca实现镜头跟随
const loca = locaRef.current;
if (loca) {
// ... addTrackAnimate 代码 ...
}

// 启动模型移动动画
changeObject();
};

6.2 模型移动动画


使用TWEEN.js实现模型在路径点之间的平滑移动:


const changeObject = () => {
if (patrolFinishedRef.current || patrolIndexRef.current >= patrolPathsRef.current.length - 1) {
return;
}

const sp = patrolPathsRef.current[patrolIndexRef.current];
const ep = patrolPathsRef.current[patrolIndexRef.current + 1];
const s = new THREE.Vector2(sp[0], sp[1]);
const e = new THREE.Vector2(ep[0], ep[1]);

const speed = 0.03;
const dis = AMap.GeometryUtil.distance(sp, ep);

if (dis <= 0) {
patrolIndexRef.current++;
changeObject();
return;
}

// 使用TWEEN实现平滑移动
new TWEEN.Tween(s)
.to(e.clone(), dis / speed / speedFactor)
.start()
.onUpdate((v) => {
// 更新模型经纬度引用
modelLngLatRef.current = [v.x, v.y];

// 节流更新状态(每100ms更新一次)
const now = Date.now();
if (now - lastUpdateTimeRef.current > 100) {
setCurrentLngLat([v.x, v.y]);
checkSamplePoint([v.x, v.y], AMap); // 检测取样点
// 计算已巡逻长度
updatePatrolledLength(v);
lastUpdateTimeRef.current = now;
}
})
.onComplete(() => {
accumulatedLengthRef.current += dis;

if (patrolIndexRef.current < patrolPathsRef.current.length - 2) {
patrolIndexRef.current++;
changeObject(); // 继续下一段
} else {
// 单程完成
if (patrolMode !== '往返') {
patrolFinishedRef.current = true;
playAnimation('1Idle'); // 播放静止动画
}
}
});
};

6.3 动画系统


模型支持多种动画(行走、静止、跳舞等),使用AnimationMixer管理:


// 设置动画系统
if (gltf.animations && gltf.animations.length > 0) {
const mixer = new THREE.AnimationMixer(robotModel);

// 创建所有动画动作
const actions = new Map<string, THREE.AnimationAction>();
gltf.animations.forEach((clip: THREE.AnimationClip) => {
const action = mixer.clipAction(clip);
action.setLoop(THREE.LoopRepeat); // 循环播放
actions.set(clip.name, action);
});

// 播放默认静止动画
const defaultAction = actions.get('1Idle');
if (defaultAction) {
defaultAction.setEffectiveTimeScale(0.6); // 设置播放速度
defaultAction.fadeIn(0.3);
defaultAction.play();
}
}

// 在render循环中更新动画
const render = () => {
requestAnimationFrame(() => {
render();
});

// 更新动画混合器
if (mixer) {
const currentTime = performance.now();
const delta = (currentTime - lastAnimationTime) / 1000;
mixer.update(delta);
lastAnimationTime = currentTime;
}

// 更新TWEEN动画
TWEEN.update();

// 渲染地图
mapInstance.render();
};


图片略大,耐心等候



5 动画切换.gif


七、AI安全隐患自动检测与告警


系统集成了Coze AI大模型,实现了巡逻过程中的自动安全隐患检测和告警功能。当机械狗沿路线巡逻时,系统会在预设的取样点自动触发AI分析,识别潜在的安全隐患。


7.1 取样点计算


系统支持基于路线间隔的自动取样点计算,根据巡逻犬配置的取样间隔(如每50米、100米等),在路线上均匀分布取样点:


// 计算取样点(基于路线间隔)
const calculateSamplePoints = (
paths: number[][],
sampleInterval: number,
AMap: any
): Array<{ lng: number; lat: number; distance: number }> => {
const samplePoints: Array<{ lng: number; lat: number; distance: number }> = [];
let accumulatedDistance = 0;

// 从第一个点开始(0米处)
samplePoints.push({
lng: paths[0][0],
lat: paths[0][1],
distance: 0,
});

// 遍历路径,计算每个取样点
for (let i = 0; i < paths.length - 1; i++) {
const currentPoint = paths[i];
const nextPoint = paths[i + 1];
const segmentDistance = AMap.GeometryUtil.distance(currentPoint, nextPoint);

// 检查当前段是否包含取样点
while (accumulatedDistance + segmentDistance >= (samplePoints.length * sampleInterval)) {
const targetDistance = samplePoints.length * sampleInterval;
const distanceInSegment = targetDistance - accumulatedDistance;

// 计算取样点在当前段中的位置(线性插值)
const ratio = distanceInSegment / segmentDistance;
const sampleLng = currentPoint[0] + (nextPoint[0] - currentPoint[0]) * ratio;
const sampleLat = currentPoint[1] + (nextPoint[1] - currentPoint[1]) * ratio;

samplePoints.push({
lng: sampleLng,
lat: sampleLat,
distance: targetDistance,
});
}

accumulatedDistance += segmentDistance;
}

return samplePoints;
};

关键点



  • 使用高德地图的GeometryUtil.distance计算路径段距离

  • 通过线性插值计算取样点的精确位置

  • 取样点从路线起点开始,按固定间隔均匀分布


7.2 自动触发检测


在巡逻过程中,系统实时检测模型位置是否到达取样点附近(±10米范围内):


// 检测是否到达取样点
const checkSamplePoint = (currentLngLat: [number, number], AMap: any) => {
const patrolDog = currentPatrolDogRef.current;
const route = currentRouteRefForSample.current;
const area = currentAreaRefForSample.current;

if (!patrolDog || !route || !patrolDog.cameraDeviceId) {
return; // 没有绑定摄像头,不进行取样
}

// 检查取样方式(必须是"路线间隔"模式)
if (patrolDog.sampleMode !== '路线间隔' || !patrolDog.sampleInterval) {
return;
}

// 检查是否在取样点附近(±10米范围内)
for (let i = 0; i < samplePointsRef.current.length; i++) {
if (processedSamplePointsRef.current.has(i)) {
continue; // 已处理过,跳过
}

const samplePoint = samplePointsRef.current[i];
const distance = AMap.GeometryUtil.distance(
[currentLngLat[0], currentLngLat[1]],
[samplePoint.lng, samplePoint.lat]
);

// 在 ±10 米范围内,触发取样
if (distance <= 10) {
console.log(`✅ 到达取样点 ${i + 1}/${samplePointsRef.current.length}`);
processedSamplePointsRef.current.add(i);

// 异步调用 Coze API(不阻塞巡逻)
analyzeSecurity(
patrolDog,
route,
area,
currentLngLat,
AMap
).catch(error => {
console.error('安全隐患分析失败:', error);
});

break; // 一次只处理一个取样点
}
}
};

关键点



  • 使用距离判断,避免重复触发

  • 异步调用AI分析,不阻塞巡逻动画

  • 使用Set记录已处理的取样点,确保每个点只处理一次


7.3 调用Coze API进行安全隐患分析


系统使用Coze平台的大模型工作流进行图像安全隐患分析:


// 调用 Coze API 进行安全隐患分析
const analyzeSecurity = async (
patrolDog: PatrolDog,
route: Route,
area: Area | null,
currentLngLat: [number, number],
AMap: any
): Promise<void> => {
try {
// 1. 获取默认令牌
await initDB();
const tokens = await db.token.getAll();
const validTokens = tokens.filter(token => Date.now() <= token.expireDate);
if (validTokens.length === 0) {
console.warn('没有可用的令牌,跳过安全隐患分析');
return;
}

const defaultToken = validTokens.find(t => t.isDefault) || validTokens[0];

// 2. 准备分析数据
// 随机选择一张测试图片(实际应用中应使用摄像头实时抓拍)
const randomImageUrl = imageUrlr[Math.floor(Math.random() * imageUrlr.length)];

// 构建输入文本,描述当前巡逻场景
const inputText = `${patrolDog.name}当前在${area?.name || '未知'}区域${route.name}巡逻时抓拍了一张照片。分析是否存在安全隐患`;

// 3. 创建 Coze API 客户端
const apiClient = new CozeAPI({
token: defaultToken.token,
baseURL: 'https://api.coze.cn',
allowPersonalAccessTokenInBrowser: true,
});

// 4. 调用工作流
const workflow_id = '7585585625312034858';
const res = await apiClient.workflows.runs.create({
workflow_id: workflow_id,
parameters: {
input: inputText,
mediaUrl: randomImageUrl,
},
});

// 5. 解析返回结果
let analysisResult: { securityType: number; score: number; desc: string } | null = null;

if (res.data) {
const dataObj = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;

if (dataObj.output && typeof dataObj.output === 'string') {
// 提取 markdown 代码块中的 JSON
const jsonMatch = dataObj.output.match(/```json\s*([\s\S]*?)\s*```/) ||
dataObj.output.match(/```\s*([\s\S]*?)\s*```/);

if (jsonMatch && jsonMatch[1]) {
analysisResult = JSON.parse(jsonMatch[1].trim());
} else {
// 尝试直接解析 output 为 JSON
analysisResult = JSON.parse(dataObj.output);
}
} else {
analysisResult = dataObj;
}
}

// 6. 判断是否是报警(securityType !== 0 且 score !== 0)
if (analysisResult && analysisResult.securityType !== 0 && analysisResult.score !== 0) {
// 保存到分析报警表
const analysisAlert: Omit<AnalysisAlert, 'id' | 'createTime' | 'updateTime'> = {
alertTime: Date.now(),
patrolDogId: patrolDog.id!,
patrolDogName: patrolDog.name,
cameraDeviceId: patrolDog.cameraDeviceId,
cameraDeviceName: patrolDog.cameraDeviceName,
routeId: route.id!,
routeName: route.name,
areaId: area?.id,
areaName: area?.name,
securityType: analysisResult.securityType as 0 | 1 | 2 | 3 | 4 | 5,
score: analysisResult.score,
desc: analysisResult.desc,
mediaUrl: randomImageUrl,
input: inputText,
status: '未处理',
};

await db.analysisAlert.add(analysisAlert);
console.log('✅ 安全隐患告警已保存');

// 更新告警列表(实时显示在大屏右侧)
updateAlertList(patrolDog.id!, route.id!, area?.id);
} else {
console.log('未发现安全隐患,不保存报警');
}
} catch (error) {
console.error('调用 Coze API 失败:', error);
}
};

API返回结果格式


{
"securityType": 1, // 0=无隐患, 1=明火燃烟, 2=打架斗殴, 3=违章停车, 4=杂物堆放, 5=私搭乱建
"score": 85, // 严重程度评分 (0-100)
"desc": "检测到明火,存在严重安全隐患" // 详细描述
}

关键点



  • 使用@coze/api官方SDK调用工作流API

  • 支持多种安全隐患类型识别(明火燃烟、打架斗殴、违章停车等)

  • 自动保存告警记录,支持后续查询和处理

  • 告警信息实时显示在大屏右侧告警列表中


6 报警.png


7.4 Coze测试页面


系统提供了专门的Coze测试页面,方便开发者测试和调试AI分析功能。在Coze测试页面中,可以:



  1. 选择令牌:从已配置的Coze API令牌中选择(支持多个令牌管理)

  2. 输入分析文本:描述需要分析的场景

  3. 上传图片URL:提供需要分析的图片地址

  4. 自动填充功能:点击"自动填充"按钮,快速填充默认的测试数据

  5. 查看完整响应:显示Coze API的完整返回结果,包括解析后的JSON和原始响应


// Coze测试页面核心功能
const handleTest = async () => {
const values = await form.validateFields();

// 创建 Coze API 客户端
const apiClient = new CozeAPI({
token: values.token,
baseURL: 'https://api.coze.cn',
allowPersonalAccessTokenInBrowser: true,
});

// 调用工作流
const workflow_id = '7585585625312034858';
const res = await apiClient.workflows.runs.create({
workflow_id: workflow_id,
parameters: {
input: values.input,
mediaUrl: values.mediaUrl,
},
});

// 解析并显示结果
// ... 解析逻辑 ...
};

测试页面特性



  • 自动填充数据:提供默认的测试图片和文本,方便快速测试

  • 图片预览:实时预览输入的图片URL

  • 完整响应展示:显示API的完整响应,便于调试

  • 错误处理:友好的错误提示,帮助定位问题



请截图 Coze测试页面 自动填充功能 测试结果展示



使用场景



  • 测试新的安全隐患识别算法

  • 验证Coze API令牌是否有效

  • 调试API返回结果格式

  • 验证图片URL是否可被Coze解析


image.png


八、性能优化建议


7.1 渲染优化



  • 禁用不必要的WebGL扩展(如阴影、抗锯齿)

  • 使用requestAnimationFrame统一管理渲染循环

  • 合理设置模型LOD(细节层次)


7.2 内存管理



  • 及时清理不需要的TWEEN动画:TWEEN.removeAll()

  • 组件卸载时销毁Three.js资源

  • 模型加载后缓存,避免重复加载


7.3 坐标转换优化



  • 坐标系统中心点跟随地图中心,减少转换误差

  • 使用节流控制状态更新频率

  • 避免在render中进行复杂计算


九、常见问题解决


8.1 模型不显示


问题:模型加载成功但在地图上不可见


解决方案



  • 检查renderer.autoClear是否设置为false

  • 确认坐标转换是否正确(注意数组索引对应关系)

  • 检查模型缩放是否合适(可能太小或太大)


8.2 模型位置偏移


问题:模型位置与预期不符


解决方案



  • 确保在设置模型位置前调用customCoords.setCenter()

  • 检查坐标轴对应关系(position[1]对应X轴,position[0]对应Z轴)

  • 使用AxesHelper辅助调试坐标轴方向


8.3 镜头跟随不流畅


问题:镜头跟随有延迟或卡顿


解决方案



  • 调整rotationSpeed参数,控制旋转速度

  • 优化timing速率控制器,实现更平滑的加速减速

  • 检查render循环是否正常执行


十、总结


通过高德地图与Three.js的深度结合,我们成功实现了3D模型在地图上的实时展示和动画效果,并集成了AI大模型实现智能安全隐患检测。核心要点包括:



  1. GLCustomLayer是关键桥梁:通过自定义图层实现Three.js与高德地图的融合

  2. 坐标转换是核心:正确理解和使用customCoords进行坐标转换

  3. 镜头跟随提升体验:使用Loca API实现平滑的镜头跟随效果

  4. AI智能检测增强功能:集成Coze大模型实现自动安全隐患识别和告警

  5. 性能优化不可忽视:合理配置渲染参数,避免不必要的WebGL扩展


技术亮点



  • 虚实结合:真实地理信息与3D模型的完美融合

  • 智能检测:基于AI大模型的自动安全隐患识别

  • 实时告警:巡逻过程中的实时检测和告警推送

  • 可视化展示:沉浸式大屏监控体验


这种技术方案不仅适用于巡逻犬管理系统,还可以扩展到智慧城市、物流追踪、车辆监控、园区安防等多个场景,为空间数据可视化提供了强大的技术支撑。通过AI能力的集成,系统从传统的可视化展示升级为智能化的安全监控平台,实现了"看得见、管得住、能预警"的完整闭环。


参考资源



http://www.bilibili.com/video/BV18c…


作者:孙_华鹏
来源:juejin.cn/post/7589482741759819803
收起阅读 »

百度又一知名产品,倒下了!

就在最近,百度旗下又一款知名互联网产品正式宣布停止服务,令人唏嘘不已。 没错,它就是「百度脑图」。 提到「百度脑图」这个名字,对于很多年轻的朋友们来说,可能有些陌生,但是在当年那会还是挺有名的。 对我而言,之前我还真就认认真真用过一段时间,在上面画了不少图,...
继续阅读 »

就在最近,百度旗下又一款知名互联网产品正式宣布停止服务,令人唏嘘不已。


没错,它就是「百度脑图」。



提到「百度脑图」这个名字,对于很多年轻的朋友们来说,可能有些陌生,但是在当年那会还是挺有名的。


对我而言,之前我还真就认认真真用过一段时间,在上面画了不少图,但是后来还是转到其他工具了。


说实话,要不是看到这个新闻,我都快忘记有这个产品的存在了。


出于好奇,我也特地登进去看了一眼。


果然,这停服公告都已经正式发出来了:



因产品调整,百度脑图将于 2026 年 3 月 31 日正式停止服务。




这也就意味着,这个产品将在明年春天正式画上句号,同时也给用户留了三个多月的数据导出时间。


众所周知,百度家的很多产品用起来一言难尽,要么有广,要么得氪金开会员。


但该说不说,「百度脑图」这个产品在我印象中倒还一直挺干净的,无广无贴,也不要会员啥的。


和市面上常见的脑图产品一样,这是一个免安装、支持网页版云端存储和自动实时保存的思维脑图编辑工具。



而且重点是界面简洁干净,无贴无广,并且兼容多种格式,同时也支持多格式脑图文件自由下载和导出。


咱讲话了,这都整得有点不像是百度家风格的产品了。



我特地看了一下它的产品更新日志,最近的一次更新还得追溯到 2019 年的 7月份。


这也意味着,这个产品已经 6 年多没怎么更新了啊,emmm,看来他们公司对这个产品是真的一点儿也不上心呢。



提到「百度脑图」,可能很多同学压根都不知道这个产品,但是百度脑图在行业内起步算很早的了


这算一算,其实它也是 10 年前的产品了。



回想当年它刚推出时,就是凭借着“免安装、云存储、易分享”三大特点而传开,说实话 那时候,我们还没有现在那么多眼花缭乱的协作工具。


然而,互联网不相信眼泪,也从不吝啬告别。


近年来,随着 SaaS 服务的兴起,包括类似 XMind、MindManager 等专业工具和协作类工具的不断进化,加上百度自身战略重心的转移,百度脑图也渐渐淡出了我们的视野。


所以写着写着我寻思,这不又是百度身上的一个「起了个大早却赶了个晚集」的事情吗……


说起「起大早赶晚集」这个话题,百度那可真有得聊的,它说第一,没人敢说第二……


作为曾经的 BAT 三巨头之一,百度在很多风口上都曾拥有极强的前瞻性,并且往往是第一批入局者,但最终呢,却因为各种内外部原因,被同行反超压制,没能笑到最后。


首先是 2003 年上线的「百度贴吧」,曾是中文互联网第一大社区,拥有极强的用户粘性,现在的很多社交玩法(如兴趣圈层)都源于贴吧,但最终贴吧还是未能抓住移动化和兴趣社交的转型机遇,社区地位逐渐被取代,影响力大幅衰退。


再比如「好看视频」,我记得诞生时间是不是好像也和抖音大差不差来着,而且早期也曾投入过不少资源进行推广,但最终呢,还是未能找准差异化定位,被抖音、快手等按在地上摩擦,最终被逐渐边缘化,未能成为短视频领域的主流玩家。


还有「百度钱包/支付」,彼时移动支付刚刚兴起时,百度钱包就紧随上线了,但最后怎么样呢,也是未能跻身主流支付平台的位列,并逐渐淡出用户视野。


再就是「百度外卖」了,2014 年就入局了,上线时间很早吧,但是在与美团、饿了么的竞争中,百度外卖还是缺乏核心壁垒,最终在 2017 年被饿了么给合并了,从而彻底退出了历史舞台。


对了,还有「百度糯米」,那上线时间就更早了,我记得当年还试图想与美团、点评相抗衡呢,投入巨大但收效甚微,最终也未能撼动竞争对手,并在 2022 年彻底宣布关停。


来到 AI 时代,就更滑稽了。


按理说百度应该是国内最早布局和投入 AI 的公司之一了,十几年前就入局了,而「文心一言」也是国产大模型中最早发布的产品之一。


结果怎么着,虽然起步很早,但是先发优势尽失,在后续竞争中逐渐掉队,与头部产品差距拉大,市场份额反被后来者反超。


说到底,其实百度的技术实力并不差,技术眼光也不差,在很多领域都有先发优势,但是每次总会在关键时刻掉链子,然后被同行反超压制。


技术先发优势巨大但最终还是未能转化为生态和市场的壁垒,可能是战略层面的摇摆,可能是既得利益的束缚,甚至有可能是组织架构的桎梏…等等,这背后的深层原因的确值得他们好好分析分析了。


包括这一次「百度脑图」的官宣落幕,作为曾经的用户,说实话,真的觉得挺可惜的。



注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



作者:CodeSheep
来源:juejin.cn/post/7589876192120700937
收起阅读 »