前端实现画中画超简单,让网页飞出浏览器
Document Picture-in-Picture 介绍
今天,我来介绍一个非常酷的前端功能:文档画中画 (Document Picture-in-Picture, 本文简称 PiP)。你有没有想过,网页上的任何内容能悬浮在桌面上?😏
🎬 视频流媒体的画中画功能
你可能已经在视频平台(如腾讯视频
、哔哩哔哩
等网页)见过这种效果:视频播放时,可以点击画中画后。无论你切换页面,它都始终显示在屏幕的最上层,非常适合上班偷偷看电视
💻
在今天的教程中,不仅仅是视频,我将教你如何将任何 HTML 内容放入画中画
模式,无论是动态内容、文本、图片,还是纯炫酷的 div,统统都能“飞”起来。✨
一个如此有趣的功能,在网上却很少有详细的教程来介绍这个功能的使用。于是我决定写一篇详细的教程来教大家如何实现画中画 (建议收藏)😁
体验网址:Treasure-Navigation
📖 Document Picture-in-Picture 详细教程
🛠 HTML 基本代码结构
首先,我们随便写一个简单的 HTML 页面
,后续的 JS 和样式都会基于它实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Picture-in-Picture API 示例</title>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
</head>
<body>
<div id="container">
<div id="pipContent">这是一个将要放入画中画的 div 元素!</div>
<button id="clickBtn">切换画中画</button>
</div>
<script>
// 在这里写你的 JavaScript 代码
</script>
</body>
</html>
1️. 请求 PiP 窗口
PiP
的核心方法是 window.documentPictureInPicture.requestWindow
。它是一个 异步方法
,返回一个新创建的 window
对象。
PIP 窗口
可以将其看作一个新的网页,但它始终悬浮在屏幕上方。
document.getElementById("clickBtn").addEventListener("click", async function () {
// 获取将要放入 PiP 窗口的 DOM 元素
const pipContent = document.getElementById("pipContent");
// 请求创建一个 PiP 窗口
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200, // 设置窗口的宽度
height: 300 // 设置窗口的高度
});
// 将原始元素添加到 PiP 窗口中
pipWindow.document.body.appendChild(pipContent);
});
演示:
👏 现在,我们已经成功创建了一个画中画窗口!
这段代码展示了如何将网页中的元素放入一个新的画中画窗口,并让它悬浮在最上面。非常简单吧
关闭PIP窗口
可以直接点右上角关闭PIP窗口,如果我们想在代码中实现关闭,直接调用window上的api
就可以了
window.documentPictureInPicture.window.close();
2️. 检查是否支持 PiP 功能
一切不能兼容浏览器的功能介绍都是耍流氓,我们需要检查浏览器是否支持PIIP功能
。
实际就是检查documentPictureInPicture属性是否存在于window上 🔧
if ('documentPictureInPicture' in window) {
console.log("🚀 浏览器支持 PiP 功能!");
} else {
console.warn("⚠️ 当前浏览器不支持 PiP 功能,更新浏览器或者换台电脑吧!");
}
如果是只需要将视频实现画中画功能,视频画中画 (Picture-in-Picture)
的兼容性会好一点,但是它只能将<video>
元素放入画中画窗口。它与本文介绍的 文档画中画(Document Picture-in-Picture)
使用方法也是十分相似的。
3️. 设置 PiP 样式
我们会发现刚刚创建的画中画没有样式
,一点都不美观。那是因为我们只放入了dom元素,没有添加css样式。
3.1. 全局样式同步
假设网页中的所有样式如下:
<head>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
<link rel="stylesheet" type="text/css" href="https://abc.css">
</head>
为了方便,我们可以直接把之前的网页的css样式全部赋值给画中画
。
// 1. document.styleSheets获取所有的css样式信息
[...document.styleSheets].forEach((styleSheet) => {
try {
// 转成字符串方便赋值
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
// 创建style标签
const style = document.createElement('style');
// 设置为之前页面中的css信息
style.textContent = cssRules;
console.log('style', style);
// 把style标签放到画中画的<head><head/>标签中
pipWindow.document.head.appendChild(style);
} catch (e) {
// 通过 link 引入样式,如果有跨域,访问styleSheet.cssRules时会报错。没有跨域则不会报错
const link = document.createElement('link');
/**
* rel = stylesheet 导入样式表
* type: 对应的格式
* media: 媒体查询(如 screen and (max-width: 600px))
* href: 外部样式表的 URL
*/
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href ?? '';
console.log('error: link', link);
pipWindow.document.head.appendChild(link);
}
});
演示:
3.2. 使用 link
引入外部 CSS 文件
向其他普通html
文件一样,可以通过link
标签引入特定css
文件:
创建 pip.css
文件:
#pipContent {
width: 600px;
height: 300px;
background: skyblue;
}
js
引用:
// 其他不变
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = './pip.css'; // 引入外部 CSS 文件
pipWindow.document.head.appendChild(link);
pipWindow.document.body.appendChild(pipContent);
演示:
3.3. 媒体查询的支持
可以设置媒体查询 @media (display-mode: picture-in-picture)
。在普通页面中会自动忽略样式,在画中画模式会自动渲染样式
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
<!-- 普通网页中会忽略 -->
@media (display-mode: picture-in-picture) {
#pipContent {
background: lightgreen;
}
}
</style>
在普通页面中显示为粉色
,在画中画自动变为浅绿色
演示:
4️. 监听进入和退出 PiP 模式的事件
我们还可以为 PiP 窗口
添加事件监听
,监控画中画模式的 进入 和 退出。这样,你就可以在用户操作时,做出相应的反馈,比如显示提示或执行其他操作。
// 进入 PIP 事件
documentPictureInPicture.addEventListener("enter", (event) => {
console.log("已进入 PIP 窗口");
});
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});
// 退出 PIP 事件
pipWindow.addEventListener("pagehide", (event) => {
console.log("已退出 PIP 窗口");
});
演示
5️. 监听 PiP 焦点和失焦事件
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});
pipWindow.addEventListener('focus', () => {
console.log("PiP 窗口进入了焦点状态");
});
pipWindow.addEventListener('blur', () => {
console.log("PiP 窗口失去了焦点");
});
演示
6. 克隆节点画中画
我们会发现我们把原始元素传入到PIP窗口后,原来窗口中的元素就不见了。
我们可以把原始元素克隆后再传入给PIP窗口,这样原始窗口中的元素就不会消失了
const pipContent = document.getElementById("pipContent");
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200,
height: 300
});
// 核心代码:pipContent.cloneNode(true)
pipWindow.document.body.appendChild(pipContent.cloneNode(true));
演示
PIP 完整示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document Picture-in-Picture API 示例</title>
<style>
#pipContent {
width: 600px;
height: 300px;
background: pink;
font-size: 20px;
}
</style>
</head>
<body>
<div id="container">
<div id="pipContent">这是一个将要放入画中画的 div 元素!</div>
<button id="clickBtn">切换画中画</button>
</div>
<script>
// 检查是否支持 PiP 功能
if ('documentPictureInPicture' in window) {
console.log("🚀 浏览器支持 PiP 功能!");
} else {
console.warn("⚠️ 当前浏览器不支持 PiP 功能,更新浏览器或者换台电脑吧!");
}
// 请求 PiP 窗口
document.getElementById("clickBtn").addEventListener("click", async function () {
const pipContent = document.getElementById("pipContent");
// 请求创建一个 PiP 窗口
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 200, // 设置窗口的宽度
height: 300 // 设置窗口的高度
});
// 将原始元素克隆并添加到 PiP 窗口中
pipWindow.document.body.appendChild(pipContent.cloneNode(true));
// 设置 PiP 样式同步
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
pipWindow.document.head.appendChild(style);
} catch (e) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href ?? '';
pipWindow.document.head.appendChild(link);
}
});
// 监听进入和退出 PiP 模式的事件
pipWindow.addEventListener("pagehide", (event) => {
console.log("已退出 PIP 窗口");
});
pipWindow.addEventListener('focus', () => {
console.log("PiP 窗口进入了焦点状态");
});
pipWindow.addEventListener('blur', () => {
console.log("PiP 窗口失去了焦点");
});
});
// 关闭 PiP 窗口
// pipWindow.close(); // 可以手动调用关闭窗口
</script>
</body>
</html>
总结
🎉 你现在已经掌握了如何使用 Document Picture-in-Picture
API 来悬浮任意 HTML 内容!
希望能带来更灵活的交互体验。✨
如果你有什么问题,或者对 PiP 功能有更多的想法,欢迎在评论区与我讨论!👇📬
来源:juejin.cn/post/7441954981342036006
还在每次都写判断?试试惰性函数,让你的代码更聪明!
什么是惰性函数?
先来看个例子
function addEvent(el, type, handler) {
if (window.addEventListener) {
el.addEventListener(type, handler, false);
} else {
el.attachEvent('on' + type, handler);
}
}
上面这段代码中,每次调用 addEvent
都会进行一遍判断。实际上,我们并不需要每次都进行判断,只需要执行一次就够了,当然,我们也可以存个全局的flag来记录,但是,有更好的办法了
function addEvent(el, type, handler) {
if (window.addEventListener) {
addEvent = function(el, type, handler) {
el.addEventListener(type, handler, false);
}
} else {
addEvent = function(el, type, handler) {
el.attachEvent('on' + type, handler);
}
}
return addEvent(el, type, handler); // 调用新的函数
}
这就是惰性函数:第一次执行时会根据条件覆盖自身,后续调用直接执行更新后的逻辑。
惰性函数的实现方式
惰性函数一般情况下有两种实现方式
在函数内部重写自身
这种实现方式就是上面我们介绍的那样
function foo() {
console.log('初始化...');
foo = function() {
console.log('后续逻辑');
}
}
大多数情况下,这种实现方式都可以覆盖
用函数表达式赋值
const foo = (function() {
if (someCondition) {
return function() { console.log('A'); }
} else {
return function() { console.log('B'); }
}
})();
这种情况适用于模块或者立即执行的情况,其实就是用闭包做了个封装
惰性函数的应用场景
兼容性判断
我们在做适配的时候,很多时候需要进行浏览器特性的判断,比如之前提到的事件绑定
性能优化
其实惰性函数说起来很像单例,他的原理就是只执行一次,那么如果想要避免一些重复操作,尤其是重复初始化,就可以想一下是不是可以用惰性函数来处理,比如Canvas渲染引擎,加载某些外部依赖、判断登录状态等等
注意事项
- 写好注释,一定要写好注释,因为函数在执行后会变化,不写注释如果除了一些问题,可能后面维护的人会骂街,会大大增加你的不可替代性,咳咳,千万不要这么操作,一定要写好注释
- 不适合频繁修改逻辑和复杂上下文的场景,会增加复杂度
一句话总结:能判断一次就不要判断两次,惰性函数让你的代码更聪明
来源:juejin.cn/post/7490850417976508428
Electron 应用太重?试试 PakePlus 轻装上阵
Electron 作为将 Web 技术带入桌面应用领域的先驱框架,让无数开发者能够使用熟悉的 HTML、CSS 和 JavaScript 构建跨平台应用。然而,随着应用规模的扩大,Electron 应用的性能问题逐渐显现——内存占用高、启动速度慢、安装包体积庞大,这些都成为了用户体验的绊脚石。不过,现在有了 PakePlus,这些烦恼都将迎刃而解。
PakePlus官网文档:PakePlus
PakePlus开源地址:github.com/Sjj1024/Pak…
首先要轻
以一款基于 Electron 的文档编辑应用为例,在使用 PakePlus 优化前,安装包大小达 200MB,启动时间超过 10 秒。但是使用PakePlus重新打包之后,安装包大小控制在5M左右,缩小了将近40倍!启动时间也做到了2秒以内!这就是PakePLus的魅力所在。
开发者反馈:"迁移过程出乎意料的顺利,大部分代码无需修改,性能提升却立竿见影。"
其次都是其次
- 🚀 基于 Rust Tauri,PakePlus 比基于 JS 的框架更轻量、更快。
- 📦 内置丰富功能包——支持快捷方式、沉浸式窗口、极简自定义。
- 👻 PakePlus 只是一个极简的软件,用 Tauri 替代旧的打包方式,支持跨平台桌面,将很快支持手机端。
- 🤗 PakePlus 易于操作使用,只需一个 GitHub Token,即可获得桌面应用。
- 🌹 不需要在本地安装任何复杂的依赖环境,使用 Github Action 云端自动打包。
- 🧑🤝🧑 支持国际化,对全球用户都非常友好,并且会自动跟随你的电脑系统语言。
- 💡 支持自定义 js 注入。你可以编写自己的 js 代码注入到页面中。
- 🎨 ui 界面更美观更友好对新手更实用,使用更舒适,支持中文名称打包。
- 📡 支持网页端直接使用,但是客户端功能更强大,更推荐客户端。
- 🔐 数据安全,你的 token 仅保存在你本地,不会上传服务器,你的项目也都在你自己的 git 中安全存储。
- 🍀 支持静态文件打包,将 Vue/React 等项目编译后的 dist 目录或者 index.html 丢进来即可成为客户端,何必是网站。
- 🐞 支持 debug 调试模式,无论是预览阶段还是发布阶段,都可以找到 bug 并消灭 bug
使用场景
你有一个网站,想把它立刻变成跨平台桌面应用和手机APP,立刻高大尚。
你有一个 Vue/React 等项目,不想购买服务器,想把它打包成桌面应用。
你的 Cocos 游戏是不是想要跨平台客户端运行?完全没有问题。
你的 Unity 项目是不是想要跨平台打包为客户端?也完全没有问题。
隐藏你的网站地址,不被随意传播和使用,防止爬虫程序获取你的网站内容。
公司内网平台,不想让别人知道你的网站地址,只允许通过你的客户端访问。
想把某个网站变成自己的客户端,实现自定义功能,比如注入 js 实现自动化操作。
网站广告太多?想把它隐藏起来,用无所不能的 js 来屏蔽它们吧。
需要使用 tauri2 打包,但是依赖环境太复杂,本地电脑硬盘不够用,就用 PakePlus
热门包
PakePLus 支持 arm 和 inter 架构的安装包,流行的程序安装包仅仅包含了 mac 的 arm(M 芯片)版本 和 windows 的 Inter(x64)版本 和 Linux 的 x64 版本,如果需要更多架构的安装包,请使用 PakePlus 单独编译自己需要的安装包。热门包的下载地址请到官方文档下载体验
常见问题
mac提示:应用已随坏
这是因为没有给苹果给钱,所以苹果会拒绝你的应用。
解决办法:
Mac 用户可能在安装时看到“应用已损坏”的警告。 请点击“取消”,然后运行以下命令,输入电脑密码后,再重新打开应用:(这是由于应用需要官方签名,才能避免安装后弹出“应用已损坏”的提示,但官方签名的费用每年 99 美元...因此,需要手动绕过签名以正常使用)
sudo xattr -r -d com.apple.quarantine /Applications/PakePlus.app
当你打包应用时,Mac 用户可能在安装时看到“应用已损坏”的警告。 请点击“取消”,然后运行以下命令,再重新打开应用:
sudo xattr -r -d com.apple.quarantine /Applications/你的软件名称.app
来源:juejin.cn/post/7490876486292389914
只需一行代码,任意网页秒变可编辑!
大家好,我是石小石!
在我们日常工作中,可能会遇到截图页面的场景,有时页面有些内容不符合要求,我们可能需要进行一些数值或内容的修改。如果你会PS,修改内容难度不高,如果你是前端,打开控制台也能通过修改dom的方式进行简单的文字修改。
今天,我就来分享一个冷门又实用的前端技巧 —— 只需一行 JavaScript 代码,让任何网页瞬间变成可编辑的! 先看看效果:
甚至,还可以插入图片等媒体内容
如何实现
很难想象,这么炫酷的功能,居然只需要在控制台输入一条指令:
document.designMode = "on";
打开浏览器控制台(F12),复制粘贴这行代码,回车即可。
如果你想关闭此功能,输入document.designMode = "off"
即可。
Document:designMode 属性
MDN是这样介绍的:
document.designMode
控制整个文档是否可编辑。有效值为 "on"
和 "off"
。根据规范,该属性默认为 "off"
。Firefox 遵循这一标准。早期版本的 Chrome 和 IE 默认为 "inherit"
。从 Chrome 43 开始,默认为 "off"
并不再支持 "inherit"
。在 IE6-10 中,该值为大写。
兼容性方面,基本上所有浏览器都是支持的。
借助次API,我们也能实现Iframe嵌套页面的编辑:
iframeNode.contentDocument.designMode = "on";
关联API
与designMode关联的API其实还有contentEditable和execCommand(已弃用,但部分浏览器还可以使用)。
contentEditable
与designMode
功能类似,不过contentEditable
可以使特定的 DOM 元素变为可编辑,而designMode
只能使整个文档可编辑。
特性 | contentEditable | document.designMode |
---|---|---|
作用范围 | 可以使单个元素可编辑 | 可以使整个文档可编辑 |
启用方式 | 设置属性为 true 或 false | 设置 document.designMode = "on" |
适用场景 | 用于指定某些元素,如 <div> , <span> 等 | 用于让整个页面变为可编辑 |
兼容性 | 现代浏览器都支持 | 现代浏览器都支持,部分老旧浏览器可能不支持 |
document.execCommand()
方法允许我们在网页中对内容进行格式化、编辑或操作。它主要用于操作网页上的可编辑内容(如 <textarea>
或通过设置 contentEditable
或 designMode
属性为 "true" 的元素),例如加粗文本、插入链接、调整字体样式等。由于它已经被W3C弃用,所以本文也不再介绍了。
来源:juejin.cn/post/7491188995164897320
“新E代弯道王”MAZDA EZ-6鹭羽白内饰焕新
今日,“新E代弯道王”MAZDA EZ-6(以下称EZ-6)宣布鹭羽白内饰焕新,现在购车可享补贴后9.98万起。新车在现有的赤麂棕色高配内饰色之外,新增兼具时尚气质和高级质感的鹭羽白浅色内饰,不仅快速回应了部分用户对于浅色系内饰的需求,更为用户带来“增色不加价”的新选择。
e签宝携华为鸿蒙打造全国首个"智能签署江南范式"。
3月31日鸿蒙生态在钱塘江畔绽放新活力。“HDD·浙江春日鸿蒙生态伙伴论坛”汇聚政企学研近200家单位,中国工程院院士陈纯、浙江省经信厅副厅长王忠民等嘉宾,与曹操出行、丁香园、e签宝等企业代表共同见证浙江鸿蒙生态建设新里程。目前全省已有政务、金融、教育等领域近千家机构完成鸿蒙应用适配,正构建智能物联时代的“江南范式”。
论坛现场,e签宝与来自政务、金融、教育等领域的众多机构企业一道,共同展示了鸿蒙生态的创新成果与价值,发布了专为鸿蒙系统的应用适配。
e签宝通过深度适配鸿蒙系统,实现了与华为生态的无缝对接,为用户带来了更加流畅、便捷、安全的电子签名与合同管理体验。
值得一提的是,2024年12月26日,在华为智慧办公生态峰会上,e签宝在武汉与华为鸿蒙完成了战略合作协议的签署。
作为全国领先的电子签名服务商,e签宝一直致力于推动电子签名技术的普及与应用,为政府、企业及个人用户提供高效、便捷、安全的电子签名服务。此次与鸿蒙的合作,是e签宝在拓展技术生态、提升服务品质方面的重要一步。通过鸿蒙生态的分布式架构与多设备协同能力,e签宝App得以在多场景下实现更加智能化的应用体验,为用户带来前所未有的便捷与高效。论坛期间,e签宝代表与浙江大学、华为等单位的嘉宾进行了深入交流与探讨。大家一致认为,随着数字经济的不断发展与普及,电子签名等数字化服务将成为未来发展的重要趋势。而鸿蒙作为华为在智能物联时代的重要布局,将为e签宝等合作伙伴提供更加广阔的技术舞台与市场空间。
浙江省作为全国首个国家信息经济示范区,将开源生态建设列为软件产业核心战略,一直以来都高度重视数字经济产业的发展。未来,e签宝将继续秉承“让签署更便捷、让信任更简单”的使命,携手鸿蒙生态及更多合作伙伴,依托浙江人才与产业优势,加速千行万业应用适配进程,共同推动数字经济的繁荣发展,为全国产业升级贡献浙江智慧。
为什么把私钥写在代码里是一个致命错误
为什么把私钥写在代码里是一个致命错误
在技术社区经常能看到一些开发者分享的教训,前几天就有人发帖讲述一位Java开发者因同事将私钥直接硬编码在代码里而感到愤怒的事情。这种情况虽然听起来可笑,但在开发团队中却相当常见,尤其对于经验不足的程序员来说。
为什么把私钥写在代码里如此危险?
1. 代码会被分享和同步
代码通常会提交到Git或SVN等版本控制系统中。一旦私钥被提交,团队中的每个人都能看到这些敏感信息。即使后来删除了私钥,在历史记录中依然可以找到。有开发者就分享过真实案例:团队成员意外将AWS密钥提交到GitHub,结果第二天账单暴增数千元——有人利用泄露的密钥进行了挖矿活动。
2. 违反安全和职责分离原则
在规范的开发流程中,密钥管理和代码开发应该严格分离。通常由运维团队负责密钥管理,而开发人员则不需要(也不应该)直接接触生产环境的密钥。这是基本的安全实践。
3. 环境迁移的噩梦
当应用从开发环境迁移到测试环境,再到生产环境时,如果密钥硬编码在代码中,每次环境切换都需要修改代码并重新编译。这不仅效率低下,还容易出错。
正确的做法
业内已有多种成熟的解决方案:
- 使用环境变量存储敏感信息
- 采用专门的配置文件(确保加入.gitignore)
- 使用AWS KMS、HashiCorp Vault等专业密钥管理系统
- 在CI/CD流程中动态注入密钥
有开发团队就曾经花费两周时间清理代码中的硬编码密钥。其中甚至发现了一个已离职员工留下的"临时"数据库密码,注释中写着"临时用,下周改掉"——然而那个"下周"已经过去五年了。
作为专业开发者,应当始终保持良好的安全习惯。将私钥硬编码进代码,就像把家门钥匙贴在门上一样不可理喻。
这个教训值得所有软件工程师引以为戒。
来源:juejin.cn/post/7489043337290203163
老板花一万大洋让我写的艺术工作室官网?! HeroSection 再度来袭!(Three.js)
引言.我不是鸽子大王!!
哈喽大家好!距离我上次发文已经过去半个月了,差点又变回了那只熟悉的“老鸽子”。不行,我不能堕落!我还没有将 Web3D
推广到普罗大众,还没有让更多人感受到三维图形的魅力 (其实是还没有捞着米)。怀着这样的心情,我决定重新振作,继续为大家带来更多关于 Three.js
和 Shader
的进阶内容。
0.前置条件
欢迎阅读本篇文章!在深入探讨 Three.js
和 Shader (GLSL)
的进阶内容之前,确保您已经具备以下基础知识:
- Three.js 基础:您需要熟悉
Three.js
的基本概念和使用方法,包括场景(Scene
)、相机(Camera
)、渲染器(Renderer
)、几何体(Geometry
)、材质(Material
)和网格(Mesh
)等核心组件。如果您还不熟悉这些内容,建议先学习Three.js
的入门教程。 - Shader 语法:本文涉及
GLSL
(OpenGL Shading Language)的编写,因此您需要了解GLSL
的基本语法,包括顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的编写,以及如何在Three.js
中使用自定义着色器。
1. Hero Section 概览
Hero Section 是网页设计中的一个术语,通常指页面顶部的一个大型横幅区域。但对于开发人员而言,这个概念可以更直观地理解为用户在访问网站的瞬间所感受到的视觉冲击,或者促使用户停留在该网站的关键原因因素。
话说这天老何接到了一个私活
起始钱不钱的无所谓!主要是想挑战一下自己(不是)。最后的成品如图所示 (互动方式为鼠标滑动
+ 鼠标点击
GIF 压缩太多了内容了,实际要好看很多)。
PC端在线预览地址: fluid-light.vercel.app
Debug调试界面: fluid-light.vercel.app/#debug
2.基础场景搭建
首先我来为你解读一下这个场景里面有什么,他非常简单。只有一个可以接受光照影响的平面几何体以及数个点光源构成,仅此而已。
让我去掉后处理以及一些页面文本元素展示给你看
构建这样的一个基础场景不难。
2.1 构建平面几何体
让我们先来解决平面几何体
值得注意的是,为了让显示效果更好,我使用了正交相机并让平面覆盖整个视窗大小
this.geometry = new THREE.PlaneGeometry(2 * 屏幕宽高比, 2);
然后构建相应的物理材质,可以去 polyhaven 下载一些自己喜欢的texture
并下载下来。
根据右边的分类选择纹理大类细分,随后选择想要下载的纹理点击下载。
因为我们本质是需要 Displacement Texture
置换贴图 & Normal Texture
法线贴图
所以不需要太在意这个纹理是作用在什么物件上面的
随后将纹理导入后赋予材质相应的属性,并对部分参数进行调整。通常直接赋予displacementMap
后 Threejs
中显示平面的凹凸会特别明显。所以记得通过
displacementScale
来调整相应的大小。
this.material = new THREE.MeshPhysicalMaterial({
color: '#121423',
metalness: 0.59,
roughness: 0.41,
displacementMap: 下载的纹理贴图,
displacementScale: 0.1,
normalMap: 下载的法线贴图,
normalScale: new THREE.Vector2(0.68, 0.75),
side: THREE.FrontSide
});
最后将物体加入场景即可
this.mesh = new THREE.Mesh(this.geometry, this.material);
scene.add(this.mesh);
(tips:MeshStandardMaterial 和 MeshPhysicalMaterial 适合需要真实感光照和复杂物理特性的场景,但性能消耗较高。如果您的电脑出现卡顿可以选择消耗较少性能的物理材质)
2.2 灯光加入战场
在本案例中,高级感的来源之一就是灯光的变换。如果您足够细心,可能会注意到一些更微妙的细节:场景中的灯光并不是简单地从 A Color
切换到 B Color
,而是同时存在多个光源,并且它们的强度也在动态变化。这种设计使得场景的光影效果更加丰富和立体。
如果场景中只有一个光源,效果可能会显得单调。而本案例中,灯光的变化呈现出一种层次感:中间是白色,周围还有一层类似年轮的光圈,最后逐渐扩散为纯色背景。这种效果的关键在于同一时间场景中存在多个点光源。虽然多个光源会显著增加性能消耗,但为了实现唯美的视觉效果,这是值得的。
让我们逐步分析灯光是如何实现的。
1. 封装创建点光源的函数
为了简化代码并提高复用性,我们可以先封装一个创建点光源的函数。这个函数会返回一个包含光源对象和目标颜色的对象。
createPointLight(intensity) {
const light = new THREE.PointLight(
0xff_ff_ff,
intensity,
100,
Math.random() * 10
);
light.position.copy(this.lightPosition); //所有的光源都同步在一个位置
return {
object: light,
targetColor: new THREE.Color()
};
}
2. 生成多个点光源
接下来,我们可以调用这个函数生成多个点光源,并将它们添加到场景中。
this.colors = [
new THREE.Color('orange'),
new THREE.Color('red'),
new THREE.Color('red'),
new THREE.Color('orange'),
new THREE.Color('lightblue'),
new THREE.Color('green'),
new THREE.Color('blue'),
new THREE.Color('blue')
];
this.lights = [
this.createPointLight(2),
this.createPointLight(3),
this.createPointLight(2.5),
this.createPointLight(10),
this.createPointLight(2),
this.createPointLight(3),
];
// 初始化灯光颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].object.color.copy(this.colors[colorIndex]);
}
for (const light of this.lights) this.scene.add(light.object);
3. 动态调整光源强度
在场景中,所有光源同时存在,但它们的强度会有所不同。每次由光照强度为 10 的光源担任场景的主色。当用户点击场景时,灯光会像上楼梯或者传送带一样逐步切换,即由新的点光源担任场景主色。
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,
8 8"b, "Ya
8 8 "b, "Ya
8 aa=D光源=a8, "b, "Ya
8 8"b, "Ya "8""""""8
8 8 "b, "Ya 8 8
8 a=C光源=8, "b, "Ya8 8
8 8"b, "Ya "8""""""" 8
8 8 "b, "Ya 8 8
8 a=B光源=8, "b, "Ya8 8
8 8"b, "Ya "8""""""" 8
8 8 "b, "Ya 8 8
8=A光源=, "b, "Ya8 8
8"b, "Ya "8""""""" 8
8 "b, "Ya 8 8
8, "b, "Ya8 8
"Ya "8""""""" 8
"Ya 8 8
"Ya8 8
"""""""""""""""""""""""""""""""""""""
让我们看看代码是如何实现的吧
window.addEventListener('click', () => {
// 打乱颜色数组(看个人喜好)
this.colors = [...this.colors.sort(() => Math.random() - 0.5)];
// 标记开始颜色过渡
this.colorTransition = true;
// 为每个灯光设置目标颜色
const numberLights = this.lights.length;
for (let index = 0; index < numberLights; index++) {
const colorIndex = Math.min(index, this.colors.length - 1);
this.lights[index].targetColor = this.colors[colorIndex].clone();
}
});
然后再Render函数中以easeing
方式更新颜色
update() {
// 只在需要时更新颜色
if (this.colorTransition) {
const numberLights = this.lights.length;
const baseSmooth = 0.25;
const smoothIncrement = 0.05;
let allTransitioned = true; // 检查所有颜色是否已完成过渡
for (let index = 0; index < numberLights; index++) {
const smoothTime = baseSmooth + index * smoothIncrement;
// 使用目标颜色进行平滑过渡
const currentColor = this.lights[index].object.color;
const targetColor = this.lights[index].targetColor;
this.dampC(currentColor, targetColor, smoothTime, delta);
// 检查是否还在过渡
if (!this.isColorClose(currentColor, targetColor)) {
allTransitioned = false;
}
}
// 如果所有颜色都已完成过渡,停止更新
if (allTransitioned) {
this.colorTransition = false;
}
}
}
3.后处理完善场景
在完成了场景的基本构建之后,我们已经实现了大约 80% 的内容。即使现在加上 UI,效果也不会太差。不过,为了让场景更具视觉冲击力和艺术感,我们可以通过后处理(Post Processing)技术来进一步提升质感。
使用 UnrealBloomPass
和 FilmPass
在本文中,我们将使用 UnrealBloomPass
(辉光效果)和 FilmPass
(电影滤镜)来增强场景的视觉效果。以下是具体的实现步骤:
- 引入后处理库:首先,我们需要引入
Three.js
的后处理库EffectComposer
以及相关的Pass
类。 - 创建
EffectComposer
:EffectComposer
是后处理的核心类,用于管理和执行各种后处理效果。 - 添加
RenderPass
:RenderPass
用于将场景渲染到后处理管道中。 - 添加
UnrealBloomPass
:UnrealBloomPass
用于实现辉光效果,可以使场景中的亮部区域产生光晕。 - 添加
FilmPass
:FilmPass
用于模拟电影胶片的效果,增加颗粒感和复古风格。
这里的具体参数需要看个人品味进行调试。同款参数可以从这里看我的源码。具体路径位于src\js\world\effect.js
this.composer = new EffectComposer(this.renderer);
this.composer.addPass(this.renderPass);
this.composer.addPass(this.bloomPass);
this.composer.addPass(this.filmPass);
此时页面的质感是不是一下就上来了呢?
最后我们需要添加最关键的一部,就是画面扭曲。
这里我们需要用到 Threejs
的 ShaderPass
,让我们来创建一个初始的ShaderPass
,仅将 EffectComposer 的读取缓冲区的图像内容复制到其写入缓冲区,而不应用任何效果。
具体内容你可以从 Threejs 后处理中了解到更多
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
const BaseShader = {
name: 'BaseShader',
uniforms: {
'tDiffuse': { value: null },
'opacity': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}`
};
const BasePass = new ShaderPass( BaseShader );
此时画面不会有任何变化
让我们对uv
进行简单操纵,让其读取tDiffuse
时可以发生扭曲
vec2 uv = vUv;
uv.y += sin(uv.x * frequency + offset) * amplitude;
gl_FragColor = texture2D(tDiffuse, uv);
最后得到效果
4.最后一些话
技术的未来与前端迁移
随着 AI 技术的快速发展,各类技术的门槛正在大幅降低,以往被视为高门槛的 3D
技术也不例外。与此同时,过去困扰开发者的数字资产构建成本问题,也正在被最新的 3D generation
技术所攻克。这意味着,在不久的将来,前端开发将迎来一次技术迁移,开发者需要掌握更新颖的交互方式和更出色的视觉效果。
为什么选择 Three.js
?
Three.js
作为最流行的 WebGL
库之一,不仅简化了三维图形的开发流程,还提供了丰富的功能和强大的扩展性。无论是创建复杂的 3D 场景,还是实现炫酷的视觉效果,Three.js
都能帮助开发者快速实现目标。
本专栏的愿景
本专栏的愿景是通过分享 Three.js
的中高级应用和实战技巧,帮助开发者更好地将 3D
技术应用到实际项目中,打造令人印象深刻的 Hero Section
。我们希望通过本专栏的内容,能够激发开发者的创造力,推动 Web3D
技术的普及和应用。
加入社区,共同成长
如果您对 Threejs
这个 3D
图像框架很感兴趣,或者您也深信未来国内会涌现越来越多 3D
设计风格的网站,欢迎加入 ice 图形学社区。这里是国内 Web 图形学最全的知识库,致力于打造一个全新的图形学生态体系!您可以在认证达人里找到我这个 Threejs
爱好者和其他大佬。
此外,如果您很喜欢 Threejs
又在烦恼其原生开发的繁琐,那么我诚邀您尝试 Tresjs 和 TvTjs, 他们都是基于 Vue
的 Threejs
框架。 TvTjs 也为您提供了大量的可使用案例,并且拥有较为活跃的开发社区,在这里你能碰到志同道合的朋友一起做开源!
5.下期预告
未来科技?机器人概念官网来袭 !!!
6. 往期回顾
2025 年了,我不允许有前端不会用 Trae 让页面 Hero Section 变得高级!!!(Threejs)
2024年了,前端人是时候给予页面一点 Hero Section 魔法了!!! (Three.js)
来源:juejin.cn/post/7478403990141796352
前端如何优雅通知用户刷新页面?
前言
老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀
窝囊废:让用户刷新一下页面,或者清一下缓存
老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛?
窝囊废:可以解决(OS:一点改的必要没有,用户全是大聪明)
老板:新的需求不是上线了嘛,怎么用户看到的还是老的页面呀
窝囊废:让用户刷新一下页面,或者清一下缓存
老板:那我得告诉用户,刷新一下页面,或者清一下缓存,才能看到新的页面呀,感觉用户体验不好啊,不能直接刷新页面嘛?
窝囊废:可以解决(OS:一点改的必要没有,用户全是大聪明)
产品介绍
c端需要经常进行一些文案调整,一些老版的文字字眼可能会导致一些舆论问题,所以就需要更新之后刷新页面,让用户看到新的页面。
c端需要经常进行一些文案调整,一些老版的文字字眼可能会导致一些舆论问题,所以就需要更新之后刷新页面,让用户看到新的页面。
思考问题为什么产生
项目是基于vue的spa应用,通过nginx代理静态资源,配置了index.html协商缓存,js、css等静态文件Cache-Control
,按正常前端重新部署后, 用户重新
访问系统,已经是最新的页面。
但是绝大部份用户都是访问页面之后一直停留在此页面,这时候前端部署后,用户就无法看到新的页面,需要用户刷新页面。
项目是基于vue的spa应用,通过nginx代理静态资源,配置了index.html协商缓存,js、css等静态文件Cache-Control
,按正常前端重新部署后, 用户重新
访问系统,已经是最新的页面。
但是绝大部份用户都是访问页面之后一直停留在此页面,这时候前端部署后,用户就无法看到新的页面,需要用户刷新页面。
产生问题
- 如果后端接口有更新,前端重新部署后,用户访问老的页面,可能会导致接口报错。
- 如果前端部署后,用户访问老的页面,可能无法看到新的页面,需要用户刷新页面,用户体验不好。
- 出现线上bug,修复完后,用户依旧访问老的页面,仍会遇到bug。
- 如果后端接口有更新,前端重新部署后,用户访问老的页面,可能会导致接口报错。
- 如果前端部署后,用户访问老的页面,可能无法看到新的页面,需要用户刷新页面,用户体验不好。
- 出现线上bug,修复完后,用户依旧访问老的页面,仍会遇到bug。
解决方案
- 前后端配合解决
- WebSocket
- SSE(Server-Send-Event)
- 纯前端方案 以下示例均以vite+vue3为例;
- 轮询html Etag/Last-Modified
- 前后端配合解决
- WebSocket
- SSE(Server-Send-Event)
- 纯前端方案 以下示例均以vite+vue3为例;
在App.vue中添加如下代码
const oldHtmlEtag = ref();
const timer = ref();
const getHtmlEtag = async () => {
const { protocol, host } = window.location;
const res = await fetch(`${protocol}//${host}`, {
headers: {
"Cache-Control": "no-cache",
},
});
return res.headers.get("Etag");
};
oldHtmlEtag.value = await getHtmlEtag();
clearInterval(timer.value);
timer.value = setInterval(async () => {
const newHtmlEtag = await getHtmlEtag();
console.log("---new---", newHtmlEtag);
if (newHtmlEtag !== oldHtmlEtag.value) {
Modal.destroyAll();
Modal.confirm({
title: "检测到新版本,是否更新?",
content: "新版本内容:",
okText: "更新",
cancelText: "取消",
onOk: () => {
window.location.reload();
},
});
}
}, 30000);
- versionData.json
自定义plugin,项目根目录创建/plugins/vitePluginCheckVersion.ts
import path from "path";
import fs from "fs";
export function checkVersion(version: string) {
return {
name: "vite-plugin-check-version",
buildStart() {
const now = new Date().getTime();
const version = {
version: now,
};
const versionPath = path.join(__dirname, "../public/versionData.json");
fs.writeFileSync(versionPath, JSON.stringify(version), "utf8", (err) => {
if (err) {
console.log("写入失败");
} else {
console.log("写入成功");
}
});
},
};
}
在vite.config.ts中引入插件
import { checkVersion } from "./plugins/vitePluginCheckVersion";
plugins: [
vue(),
checkVersion(),
]
在App.vue中添加如下代码
const timer = ref()
const checkUpdate = async () => {
let res = await fetch('/versionData.json', {
headers: {
'Cache-Control': 'no-cache',
},
}).then((r) => r.json())
if (!localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
} else {
if (res.version !== localStorage.getItem('demo_version')) {
localStorage.setItem('demo_version', res.version)
Modal.confirm({
title: '检测到新版本,是否更新?',
content: '新版本内容:' + res.content,
okText: '更新',
cancelText: '取消',
onOk: () => {
window.location.reload()
},
})
}
}
}
onMounted(()=>{
clearInterval(timer.value)
timer.value = setInterval(async () => {
checkUpdate()
}, 30000)
})
Use
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { webUpdateNotice } from '@plugin-web-update-notification/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
webUpdateNotice({
logVersion: true,
}),
]
})
来源:juejin.cn/post/7439905609312403483
部署项目,console.log为什么要去掉?
console.log的弊端
1. 影响性能(轻微但可优化)
console.log
会占用 内存 和 CPU 资源,尤其是在循环或高频触发的地方(如 mousemove
事件)。
虽然现代浏览器优化了 console
,但大量日志仍可能导致 轻微性能下降。
2. 暴露敏感信息(安全风险)
可能会 泄露 API 接口、Token、用户数据 等敏感信息,容易被恶意利用。
3. 干扰调试(影响开发者体验)
生产环境日志过多,可能会 掩盖真正的错误信息,增加调试难度。
开发者可能会误以为某些 console.log
是 预期行为,而忽略真正的 Bug。
4. 增加代码体积(影响加载速度)
即使 console.log
本身很小,但 大量日志 会增加打包后的文件体积,影响 首屏加载速度。
解决方案:移除生产环境的 console.log
1. 使用 Babel 插件
在 babel.config.js
中配置:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
plugins: [
['@babel/plugin-proposal-optional-chaining'],
...process.env.NODE_ENV === 'production' ? [['transform-remove-console', { exclude: ['info', 'error', 'warn'] }]] : []
]
}
特点:
- 不影响源码,仅在生产环境生效,开发环境保留完整
console
。 - 配置简单直接,适合快速实现基本需求。
- 依赖 Babel 插件
2. 使用 Terser 压缩时移除(Webpack/Vite 默认支持)
在 vite.config.js
或 webpack.config.js
中配置:
module.exports = {
chainWebpack: (config) => {
config.optimization.minimizer("terser").tap((args) => {
args[0].terserOptions.compress = {
...args[0].terserOptions.compress,
drop_console: true, // 移除所有 console
pure_funcs: ["console.log"], // 只移除 console.log,保留其他
};
return args;
});
},
};
特点:
- 不影响源码,仅在生产环境生效,开发环境保留完整
console
。 - 避免 Babel 插件兼容性问题
- 需要额外配置
3. 自定义 console
包装函数(按需控制)
// utils/logger.js
const logger = {
log: (...args) => {
if (process.env.NODE_ENV !== "production") {
console.log("[LOG]", ...args);
}
},
warn: (...args) => {
console.warn("[WARN]", ...args);
},
error: (...args) => {
console.error("[ERROR]", ...args);
},
};
export default logger;
使用方式
import logger from "./utils/logger";
logger.log("Debug info"); // 生产环境自动不打印
logger.error("Critical error"); // 始终打印
特点:
- 可以精细控制日志,可控性强,可以自定义日志级别。
- 不影响
console.warn
和console.error
。 - 需要手动替换
console.log
来源:juejin.cn/post/7485938326336766003
只写后台管理的前端要怎么提升自己
本人写了五年的后台管理。每次面试前就会头疼,因为写的页面除了表单就是表格。抱怨过苦恼过也后悔过,但是站在现在的时间点回想以前,发现有很多事情可以做的更好,于是有了这篇文章。
写优雅的代码
一道面试题
大概两年以前,面试美团的时候,面试官让我写一道代码题,时间单位转换。具体的题目我忘记了。
原题目我没做过,但是我写的业务代码代码里有类似的单位转换,后端返回一个数字,单位是kb
,而我要展示成 KB
,MB
等形式。大概写一个工具函数(具体怎么写的忘记了,不过功能比这个复杂点):
function formatSizeUnits(kb) {
let units = ['KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;
while (kb >= 1024 && unitIndex < units.length - 1) {
kb /= 1024;
unitIndex++;
}
return `${kb.toFixed(2)} ${units[unitIndex]}`;
}
而在此之前,是一个后端同学写的代码(说起来,后台管理系统前端人力不够后端帮忙写好像是常规操作,后端总觉得写前端简单,而我只能说你们那代码写的,一言难尽……),如下:
function formatSizeUnits(kb) {
var result = '';
if (kb < 1024) {
result = kb + ' KB';
} else if (kb < 1024 * 1024) {
result = (kb / 1024).toFixed(2) + ' MB';
} else if (kb < 1024 * 1024 * 1024) {
result = (kb / 1024 / 1024).toFixed(2) + ' GB';
} else {
result = (kb / 1024 / 1024 / 1024).toFixed(2) + ' TB';
}
return result;
}
虽然功能都一样,但是我写在面试的时候写了上面的代码得到了面试官的赞扬,也因此非常顺利的通过了面试。而如果我写成了下面的样子,估计不会有这种效果。
如何提升代码质量
想提升代码质量,最理想的是靠 code review ,但实际上这玩意在大多数公司根本就不存在。其次就只能多看看大佬们写的代码,但是一来后台管理本来就没太大难度很少有大佬,二来很多时候一个后台管理系统就一两个人负责。
还好现在有了各种 AI 工具,可以非常方便的优化自己的代码了。
还是上面的问题,看看 GPT 给的答案
// 提问:写一个工具函数,对文件大小的单位进行转换,加注释
// 回答:下面是一个JavaScript工具函数,它接受文件大小、初始单位、目标单位和小数位数,然后返回目标单位下的文件大小。
/**
* 将文件大小从一个单位转换为另一个单位。
*
* @param {number} size 文件大小。
* @param {string} fromUnit 初始单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {string} toUnit 目标单位('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')。
* @param {number} [decimalPoint=2] 结果保留的小数位数,默认为2。
* @return {string} 转换后的文件大小,带单位。
*/
function convertFileSize(size, fromUnit, toUnit, decimalPoint = 2) {
// 定义单位与字节之间的转换关系
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// 获取初始单位和目标单位的索引
const fromIndex = units.indexOf(fromUnit);
const toIndex = units.indexOf(toUnit);
// 如果单位不在列表中,抛出错误
if (fromIndex === -1 || toIndex === -1) {
throw new Error('Invalid units');
}
// 计算初始单位与目标单位之间的转换系数
const exponent = toIndex - fromIndex;
// 计算结果大小
const resultSize = size / Math.pow(1024, exponent);
// 返回格式化后的结果
return parseFloat(resultSize.toFixed(decimalPoint)) + ' ' + toUnit;
}
// 示例使用
console.log(convertFileSize(1, 'GB', 'MB')); // 输出: 1024.00 MB
console.log(convertFileSize(1, 'MB', 'KB')); // 输出: 1024.00 KB
console.log(convertFileSize(1, 'KB', 'B')); // 输出: 1024.00 B
console.log(convertFileSize(1, 'MB', 'GB', 5)); // 输出: 0.00098 GB
还能说什么,我只能说 NB!(经评论区提示,此段代码可能有问题,建议大家在使用AI工具时,重点是借鉴思路,直接使用代码需要谨慎~)
我现在每次写大段逻辑之后都习惯拿给 AI 看看,有什么更好的实现方式,或者用什么设计模式。AI 是非常低成本且高效提升代码质量的工具。
学会封装
一个功能用到了好多次,为什么不封装成组件?一个组件用到了好几个项目,为什么不单独写个npm包?差不多的项目创建了好几个,为什么不封装成脚手架?
你说,没时间,没必要,复制粘贴反而更快。
那你就完全没理解,这么做不一定是为了让工作更快完成,而是可以让你在年年终述职时更有话说(你就算写了一百个表单表格没有写一个脚手架更值得炫耀),如果不会写可以问问 AI。
而当你真正开始封装组件,开始写工具库了,你会发现你需要思考的确实比之前多了。
关注业务
对于前端业务重要吗?
相比于后端来说,前端一般不会太关注业务。就算出了问题大部分也是后端的问题。
但是就我找工作的经验,业务非常重要!
如果你做的工作很有技术含量,比如你在做低代码,你可以面试时讲一个小时的技术难点。但是你只是一个破写后台管理,你什么都没有的说。这个时候,了解业务就成为了你的亮点。
一场面试
还是拿真实的面试场景举例,当时前同事推我字节,也是我面试过N次的梦中情厂了,刚好那个组做的业务和我之前呆的组做的一模一样。
- 同事:“做的东西和咱们之前都是一样的,你随便走个过场就能过,我在前端组长面前都夸过你了!”
- 我:“好嘞!”
等到面试的时候:
- 前端ld:“你知道xxx吗?(业务名词)”
- 我:“我……”
- 前端ld:“那xxxx呢?(业务名词)”
- 我:“不……”
- 前端ld:“那xxxxx呢??(业务名词)”
- 我:“造……”
然后我就挂了………………
如何了解业务
- 每次接需求的时候,都要了解需求背景,并主动去理解
我们写一个表格简简单单,把数据展示出来就好,但是表格中的数据是什么意思呢?比如我之前写一个 kafka 管理平台,里面有表格表单,涉及什么
cluster
controller
topic
broker
partition
…… 我真的完全不了解,很后悔我几年时间也没有耐下心来去了解。 - 每次做完一个需求,都需要了解结果
有些时候,后台管理的团队可能根本没有PM,那你也要和业务方了解,这个功能做了之后,多少人使用,效率提高了吗?数据是怎样的?
- 理解需求,并主动去优化
产品要展示一千条数据,你要考虑要不要分页,不分页会不会卡,要不要上虚拟表格?
产品要做一个可拖拽表单,你要考虑是否需要拖动,是否需要配置。
其实很多时候,产品的思维可能会被局限在竞品的实现方式,而前端可以给TA更多选项。在和产品沟通的时候,你不仅是沟通页面的实现,也更能理解业务。
关注源码
说到源码, Vue,React 这些基本是每次必问,也有太多大佬们的总结我就不多说了。
除此之外,我想大家每次面试应该都会被问,你写了什么亮点的项目,困难的项目,你会说什么?哦,都是表单表格,都用 Element UI 实现的,没什么难度。那估计你面试也就到这里了。
那说什么,后台管理就这些啊?!
如果你用了 Element UI 你可以说写了什么自定义 Element UI 表单组件,Element UI 表单源码是什么原理,怎么通信的?用了 Axios 可以说 Axios 的原理,它的拦截器怎么实现的?用了编辑器 diff 那你知道 diff 算法是什么吗?用了 ECharts 那你知道他底层是用什么绘制的吗?用了 husky ?husky的原理是什么?用了Vite,它为什么快?是怎么打包的?写过插件吗?
可能有人觉得我离谱,事实上上面每一个问题都是我在面试中真实被问到的。当然,如果全部去了解,肯定不现实,每个人的精力有限,不可能每个用到的每个工具每个库都去了解原理,不过只要有一两个你去了解过,就可以在面试中引导面试官来问,要学会表现出你擅长的点。
至于怎么读源码?以前我不知道,现在嘛…… 为什么不问问神奇的 ChatGPT 呢?
讲一下 Axios 源码中,拦截器是怎么实现的?
Axios 的拦截器实现基于 Axios 的核心原理,即 Axios 实例是一个包含请求和响应拦截器堆栈的对象。当发出请求或接收响应时,Axios 会遍历这些拦截器,并按照添加的顺序执行请求拦截器,以及按照相反的顺序执行响应拦截器。
在 Axios 的源码中,拦截器是通过一个 AxiosInterceptorManager 实例来管理的,它维护了一个拦截器数组。每个拦截器都是一个包含
fulfilled
和rejected
函数的对象。这两个函数分别对应于拦截器成功处理和拦截器处理出错的情况。
以下是 Axios 拦截器管理器的一个简化版本,展示了其核心实现思路:
class InterceptorManager {
constructor() {
this.handlers = []; // 存储拦截器的数组
}
use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1; // 返回拦截器的ID
}
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; // 移除拦截器
}
}
forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
}
}
在发送请求或接收响应时,Axios 会创建一个 promise 链,并通过
forEach
方法将拦截器中的fulfilled
和rejected
函数添加到这个链中。这样,每个拦截器都可以对请求或响应进行处理,然后将结果传递到链的下一个拦截器,或者在出错时结束链的执行。
axios.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
axios.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
在 Axios 的完整实现中,这个拦截器机制被集成到了 Axios 的请求发送和响应处理流程中。通过这种方式,Axios 可以在发送请求之前和接收响应之后,但在用户定义的
.then
或.catch
执行之前,插入自定义的逻辑。
请注意,这里提供的代码只是为了说明 Axios 拦截器的实现原理,并不是 Axios 源码的完整复制。如果你对 Axios 的拦截器实现细节感兴趣,建议查看 Axios 的官方 GitHub 仓库中的源码。
前端基建
当我们工作时间久了面试难免会遇到这些问题,前端工程化,前端监控,工作流,部署,性能等等。其实我们在工作中绝大部分时间都在写代码,对于这些不是所有人都有机会接触到,不过这些和所做的业务无关,是我们提升自己很好的一个思路。
技术选型
技术栈选 Vue 还是 React?Vue 选 Vue2 还是 Vue3?组件库选 ElementUI 还是 Ant Design?微前端有没有使用过?打包工具用 Vite 还是 Webpack?有那么多表单怎么实现的,有没有什么表单配置化方案,比如Formily?
对于我这种菜鸡,我这种只写简单的表单表格的人,这些都……无所谓……

不过为了应对面试我们还是需要了解下未选择技术栈的缺点,和已选择技术栈的优点(有点本末倒置…但是常规操作啦)
Vue 你可以说简单高效轻量级,面试必会问你为什么,你就开始说 Vue 的响应式系统,依赖收集等。
React 你可以说 JSX、Hooks 很灵活,那你必然要考虑 JSX 怎么编译, Hooks 实现方式等。
总体而言,对于技术选型,依赖于我们对所有可选项的理解,做选择可能很容易,给出合理的理由还是需要花费一些精力的。
开发规范
这个方面,在面试的时候我被问到的不多,我们可以在创建项目的时候,配置下 ESlint
,stylelint
, prettier
, commitlint
等。
前端监控
干了这么多年前端,前端监控我是……一点没做过。

前端监控,简单来说就是我们在前端程序中记录一些信息并上报,一般是错误信息,来方便我们及时发现问题并解决问题。除此之外也会有性能监控,用户行为的监控(埋点)等。之前也听过有些团队分享前端监控,为了出现问题明确责任(方便甩锅)。
对于实现方案,无论使用第三方库还是自己实现,重要的都是理解实现原理。
对于错误监控,可以了解一下 Sentry,原理简单来说就是通过 window.onerror
和 window.addEventListener('unhandledrejection', ...)
去分别捕获同步和异步错误,然后通过错误信息和 sourceMap
来定位到源码。
对于性能监控,我们可以通过 window.performance
、PerformanceObserver
等 API 收集页面性能相关的指标,除此之外,还需要关注接口的响应时间。
最后,收集到信息之后,还要考虑数据上报的方案,比如使用 navigator.sendBeacon
还是 Fetch、AJAX?是批量上报,实时上报,还是延迟上报?上报的数据格式等等。
CI/CD
持续集成(Continuous Integration, CI)和 持续部署(Continuous Deployment, CD),主要包括版本控制,代码合并,构建,单测,部署等一系列前端工作流。
场景的工作流有 Jenkins、 Gitlab CI 等。我们可以配置在合并代码时自动打包部署,在提交代码时自动构建并发布包等。
这块我了解不多,但感觉这些工具层面的东西,不太会涉及到原理,基本上就是使用的问题。还是需要自己亲自动手试一下,才能知道细节。比如在 Gitlab CI 中, Pipeline
、 Stage
和 Job
分别是什么,怎么配置,如何在不同环境配置不同工作流等。
了解技术动态
这个可能还是比较依赖信息收集能力,虽然我个人觉得很烦,但好像很多领导级别的面试很愿意问。
比如近几年很火的低代码,很多面试官都会问,你用过就问你细节,你没用过也会问你有什么设计思路。
还有最近的两年爆火的 AI,又或者 Vue React的最新功能,WebAssembly,还有一些新的打包工具 Vite Bun 什么的,还有鸿蒙开发……
虽然不可能学完每一项新技术,但是可以多去了解下。
总结
写了这么多,可能有人会问,如果能回到过去,你会怎么做。
啊,我只能说,说是一回事,做又是另一回事,事实上我并不希望回到过去去卷一遍,菜点没关系,快乐就好,一切都是最好的安排。

来源:juejin.cn/post/7360528073631318027
双Token无感刷新方案
提醒一下
双Token机制并没有从根本上解决安全性的问题,本文章只是提供一个思路,具体是否选择请大家仔细斟酌考虑,笔者水平有限,非常抱歉对你造成不好的体验。
token有效期设置问题
最近在做用户认证模块的后端功能开发,之前就有一个问题困扰了我好久,就是如何设置token
的过期时间,前端在申请后端登录接口成功之后,会返回一个token
值,存储在用户端本地,用户要访问后端的其他接口必须通过请求头带上这个token
值,但是这个token
的有效期应该设置为多少?
- 如果设置的太短,比如1小时,那么用户一小时之后。再访问其他接口,需要再次重新登录,对用户的体验极差
- 如果设置为一个星期,那么在这个时间内
- 一旦
token
泄露,攻击者可长期冒充用户身份,直到token
过期,服务端无法限制其访问用户数据 - 虽然可以依赖黑名单机制,但会增加系统复杂度,还要进行系统监测
- 如果在这段时间恶意用户利用未过期的条款持续调用后端API将会导致资源耗尽或产生巨额费用
- 一旦
所以有没有两者都兼顾的方案呢?
双token无感刷新方案
传统的token
方案要么频繁要求用户重新登录,要么面临长期有效的安全风险
但是双token
无感刷新机制,通过组合设计,在保证安全性的情况下,实现无感知的认证续期
核心设计
access_token
:访问令牌,有效期一般设置为15~30分钟,主要用于对后端请求API的交互refresh_token
:刷新令牌,一般设置为一个星期到一个月,主要用于获取新的access_token
大致的执行流程如下
用户登录之后,后端返回access_token
和refresh_token
响应给前端,前端将两个token
存储在用户本地
在用户端发起前端请求,访问后端接口,在请求头中携带上access_token
前端会对access_token
的过期时间进行检测,当access_token
过期前一分钟,前端通过refresh_token
向后端发起请求,后端判断refresh_token是否有效,有效则重新获取新的access_token
,返回给前端替换掉之前的access_token
存储在用户本地,无效则要求用户重新认证
这样的话对于用户而言token
的刷新是无感知的,不会影响用户体验,只有当refresh_token
失效之后,才需要用户重新进行登录认证,同时,后端可以通过对用户refresh_token
的管理来限制用户对后端接口的请求,大大提高了安全性
有了这个思路,写代码就简单了
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private JwtUtils jwtUtils;
// token过期时间
private static final Integer TOKEN_EXPIRE_DAYS =5;
// token续期时间
private static final Integer TOKEN_RENEWAL_MINUTE =15;
@Override
public boolean verify(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return false;
}
String key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN,uid);
String realToken = RedisUtils.getStr(key);
return Objects.equals(refresh_token, realToken);
}
@Override
public void renewalTokenIfNecessary(String refresh_token) {
Long uid = jwtUtils.getUidOrNull(refresh_token);
if (Objects.isNull(uid)) {
return;
}
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
long expireSeconds = RedisUtils.getExpire(refresh_key, TimeUnit.SECONDS);
if (expireSeconds == -2) { // key不存在,refresh_token已过期
return;
}
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
RedisUtils.expire(access_key, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
}
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(key = "#uid")
public LoginTokenResponse login(Long uid) {
String refresh_key = RedisKey.getKey(RedisKey.USER_REFRESH_TOKEN, uid);
String access_key = RedisKey.getKey(RedisKey.USER_ACCESS_TOKEN, uid);
String refresh_token = RedisUtils.getStr(refresh_key);
String access_token;
if (StrUtil.isNotBlank(refresh_token)) { //刷新令牌不为空
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
refresh_token = jwtUtils.createToken(uid);
RedisUtils.set(refresh_key, refresh_token, TOKEN_EXPIRE_DAYS, TimeUnit.DAYS);
access_token = jwtUtils.createToken(uid);
RedisUtils.set(access_key, access_token, TOKEN_RENEWAL_MINUTE, TimeUnit.MINUTES);
return LoginTokenResponse.builder()
.refresh_token(refresh_token).access_token(access_token)
.build();
}
}}
注意事项
- 安全存储Refresh Token时,优先使用HttpOnly+Secure Cookie而非LocalStorage
- 在颁发新Access Token时,重置旧Token的生存周期(滑动过期)而非简单续期
- 针对高敏感操作(如支付、改密),建议强制二次认证以突破Token机制的限制
安全问题
双Token机制并没有从根本上解决安全性的问题,它只是尝试通过改进设计,优化用户体验,全面的安全策略需要多层防护,分别针对不同类型的威胁和风险,而不仅仅依赖于Token的管理方式或数量
安全是一个持续对抗的过程,关键在于提高攻击者的成本,而非追求绝对防御。
"完美的认证方案不存在,但聪明的权衡永远存在。"
本笔者水平有限,望各位海涵
如果文章中有不对的地方,欢迎大家指正。
来源:juejin.cn/post/7486782063422717962
程序员,你使用过灰度发布吗?
大家好呀,我是猿java。
在分布式系统中,我们经常听到灰度发布这个词,那么,什么是灰度发布?为什么需要灰度发布?如何实现灰度发布?这篇文章,我们来聊一聊。
1. 什么是灰度发布?
简单来说,灰度发布也叫做渐进式发布或金丝雀发布,它是一种逐步将新版本应用到生产环境中的策略。相比于一次性全量发布,灰度发布可以让我们在小范围内先行测试新功能,监控其表现,再决定是否全面推开。这样做的好处是显而易见的:
- 降低风险:新版本如果存在 bug,只影响少部分用户,减少了对整体用户体验的冲击。
- 快速回滚:在小范围内发现问题,可以更快地回到旧版本。
- 收集反馈:可以在真实环境中收集用户反馈,优化新功能。
2. 原理解析
要理解灰度发布,我们需要先了解一下它的基本流程:
- 准备阶段:在生产环境中保留旧版本,同时引入新版本。
- 小范围发布:将新版本先部署到一小部分用户,例如1%-10%。
- 监控与评估:监控新版本的性能和稳定性,收集用户反馈。
- 逐步扩展:如果一切正常,将新版本逐步推广到更多用户。
- 全面切换:当确认新版本稳定后,全面替换旧版本。
在这个过程中,关键在于如何切分流量,确保新旧版本平稳过渡。常见的切分方式包括:
- 基于用户ID:根据用户的唯一标识,将部分用户指向新版本。
- 基于地域:先在特定地区进行发布,观察效果后再扩展到其他地区。
- 基于设备:例如,先在Android或iOS用户中进行发布。
3. 示例演示
为了更好地理解灰度发布,接下来,我们通过一个简单的 Java示例来演示基本的灰度发布策略。假设我们有一个简单的 Web应用,有两个版本的登录接口/login/v1
和/login/v2
,我们希望将百分之十的流量引导到v2
,其余流量继续使用v1
。
3.1 第一步:引入灰度策略
我们可以通过拦截器(Interceptor)来实现流量的切分。以下是一个基于Spring Boot的简单实现:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Random;
@Component
public class GrayReleaseInterceptor implements HandlerInterceptor {
private static final double GRAY_RELEASE_PERCENT = 0.1; // 10% 流量
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
if ("/login".equals(uri)) {
if (isGrayRelease()) {
// 重定向到新版本接口
response.sendRedirect("/login/v2");
return false;
} else {
// 使用旧版本接口
response.sendRedirect("/login/v1");
return false;
}
}
return true;
}
private boolean isGrayRelease() {
Random random = new Random();
return random.nextDouble() < GRAY_RELEASE_PERCENT;
}
}
3.2 第二步:配置拦截器
在Spring Boot中,我们需要将拦截器注册到应用中:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private GrayReleaseInterceptor grayReleaseInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(grayReleaseInterceptor).addPathPatterns("/login");
}
}
3.3 第三步:实现不同版本的登录接口
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/login")
public class LoginController {
@GetMapping("/v1")
public String loginV1(@RequestParam String username, @RequestParam String password) {
// 旧版本登录逻辑
return "登录成功 - v1";
}
@GetMapping("/v2")
public String loginV2(@RequestParam String username, @RequestParam String password) {
// 新版本登录逻辑
return "登录成功 - v2";
}
}
在上面三个步骤之后,我们就实现了登录接口地灰度发布:
- 当用户访问
/login
时,拦截器会根据设定的灰度比例(10%)决定请求被重定向到/login/v1
还是/login/v2
。 - 大部分用户会体验旧版本接口,少部分用户会体验新版本接口。
3.4 灰度发布优化
上述示例,我们只是一个简化的灰度发布实现,实际生产环境中,我们可能需要更精细的灰度策略,例如:
- 基于用户属性:不仅仅是随机切分,可以根据用户的地理位置、设备类型等更复杂的条件。
- 动态配置:通过配置中心动态调整灰度比例,无需重启应用。
- 监控与告警:集成监控系统,实时监控新版本的性能指标,异常时自动回滚。
- A/B 测试:结合A/B测试,进一步优化用户体验和功能效果。
4. 为什么需要灰度发布?
在实际工作中,为什么我们要使用灰度发布?这里我们总结了几个重要的原因。
4.1 降低发布风险
每次发布新版本,尤其是功能性更新或架构调整,都会伴随着一定的风险。即使经过了充分的测试,实际生产环境中仍可能出现意想不到的问题。灰度发布通过将新版本逐步推向部分用户,可以有效降低全量发布可能带来的风险。
举个例子,假设你上线了一个全新的支付功能,直接面向所有用户开放。如果这个功能存在严重 bug,可能导致大量用户无法完成支付,甚至影响公司声誉。而如果采用灰度发布,先让10%的用户体验新功能,发现问题后只需影响少部分用户,修复起来也更为迅速和容易。
4.2 快速回滚
在传统的全量发布中,一旦发现问题,回滚到旧版本可能需要耗费大量时间和精力,尤其是在高并发系统中,数据状态的同步与恢复更是复杂。而灰度发布由于新版本只覆盖部分流量,问题定位和回滚变得更加简单和快速。
比如说,你在灰度发布阶段发现新版本的某个功能在某些特定条件下会导致系统崩溃,立即可以停止向新用户推送这个版本,甚至只针对受影响的用户进行回滚操作,而不用影响全部用户的正常使用。
4.3 实时监控与反馈
灰度发布让你有机会在真实的生产环境中监控新版本的表现,并收集用户的反馈。这些数据对于评估新功能的实际效果至关重要,有助于做出更明智的决策。
举个具体的场景,你新增了一个推荐算法,希望提升用户的点击率。在灰度发布阶段,你可以监控新算法带来的点击率变化、服务器负载情况等指标,确保新算法确实带来了预期的效果,而不是引入了新的问题。
4.4 提升用户体验
通过灰度发布,你可以在推出新功能时,逐步优化用户体验。先让一部分用户体验新功能,收集他们的使用反馈,根据反馈不断改进,最终推出一个更成熟、更符合用户需求的版本。
举个例子,你开发了一项新的用户界面设计,直接全量发布可能会让一部分用户感到不适应或不满意。灰度发布允许你先让一部分用户体验新界面,收集他们的意见,进行必要的调整,再逐步扩大使用范围,确保最终发布的版本能获得更多用户的认可和喜爱。
4.5 支持A/B测试
灰度发布是实现A/B测试的基础。通过将用户随机分配到不同的版本,你可以比较不同版本的表现,选择最优方案进行全面推行。这对于优化产品功能和提升用户体验具有重要意义。
比如说,你想测试两个不同的推荐算法,看哪个能带来更高的转化率。通过灰度发布,将用户随机分配到使用算法A和算法B的版本,比较它们的表现,最终选择效果更好的算法进行全面部署。
4.6 应对复杂的业务需求
在一些复杂的业务场景中,全量发布可能无法满足灵活的需求,比如分阶段推出新功能、针对不同用户群体进行差异化体验等。灰度发布提供了更高的灵活性和可控性,能够更好地适应多变的业务需求。
例如,你正在开发一个面向企业用户的新功能,希望先让部分高价值客户试用,收集他们的反馈后再决定是否全面推广。灰度发布让这一过程变得更加顺畅和可控。
5. 总结
本文,我们详细地分析了灰度发布,它是一种强大而灵活的部署策略,能有效降低新版本上线带来的风险,提高系统的稳定性和用户体验。作为Java开发者,掌握灰度发布的原理和实现方法,不仅能提升我们的技术能力,还能为团队的项目成功保驾护航。
对于灰度发布,如果你有更多的问题或想法,欢迎随时交流!
6. 学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7488321730764603402
因网速太慢我把20M+的字体压缩到了几KB
于水增
故事背景
事情起源于之前做的海报编辑器,自己调试时无意中发现字体渲染好慢,第一反应就是网怎么变慢了,断网了?仔细一看才发现,淦!这几个字体资源咋这么大,难怪网速变慢了呢😁😁。
图片中的海报包含6种字体,其中最大的字体文件超过20M,而最长的网络加载时长已接近20s。所以海报实际效果图展示耗时太久,很影响用户体验。那就趁此机会跟大家聊聊 字体 这件小事。
字体文件为什么那么大?
🙋 DeepSeek同学来回答下大家:
这里所说的大体积的字体资源多数是指中文主要原因下边两点
- 中文字符数量庞大,英文仅 26 个字母 + 符号,中文(全字符集)包含 70,000+ 字符
- 字形结构复杂,字体文件需为每个字符存储独立的矢量轮廓数据,而汉字笔画复杂,每个字符需存储数百个控制点坐标(例如「龍」字的轮廓点数量可能是「A」的 10 倍以上)
总结下来就是咱们不光汉字多,书法也是五花八门,它是真小不了。如果你硬要压缩,我们只能从第一点入手,将字符数量进行缩减,比如保留 1000 个常用汉字。
web网站中常见字体格式
由于我司物料部门提供的为TTF格式,所以这里通过 思源黑体 给一个直观的对比:
- TTF 文件:16.9 MB
- WOFF2 文件:7.4 MB(压缩率约 60%)
两者为什么会差这么多,其实WOFF2 只是在 TTF/OTF 基础上添加了压缩和 Web 专用元数据,且WOFF2支持增量解码,也就是边下载边解析,文本可更快显示(即使字体未完全加载,不过有待考证)。
TTF有办法优化吗?
回归问题本身
首先来简单回顾下我们自定义的字体是如何在浏览器中完成渲染的
一般情况下我们对字体文件的引用方式为下边三种
- 通过绝对路径来引用,这种就是将字体文件打包在工程内,所以带来的结果就是工程打包文件体积太大
@font-face {
font-family: 'xxx';
src: url('../../assets/fonts.woff2')
}
- 第二种就是 CDN 中存放的字体文件,一般是通过这种方式来减少工程的编译后体积
@font-face {
font-family: 'xxx';
src: url('https://xxx.woff2')
}
- 通过 FontFace 构造一个字体对象
前两种一般是在浏览器构建 CSSOM 时,当遇到**<font style="color:rgba(0, 0, 0, 0.9);background-color:rgb(243, 243, 243);">url()</font>**
引用时会发起资源请求。第三种则是通过 js 来控制字体的加载流程,所以归根结底就是字体文件太大,导致网络资源下载速度慢,我们只能从优化字体大小的方向入手
确定解决方向
下面汇总下查到的具体几个优化方案,诸如提高网络传输效率,增加缓存之类的就不讲了,能够立竿见影的主要下边这两个方案
方案 | 方法/原理 | 适用场景 |
---|---|---|
字体子集化 | 通过工具将字体文件进行提取(支持动态),返回指定的字符集的字体文件,其根本就是减少单次资源请求的体积,需要服务端支持 | 这个方案是所有优化场景的基础 |
按需加载 | 通过设置 unicode-range 属性,浏览器在进行css样式计算时候,会根据页面中的字符与设置的字符范围进行比对,匹配上会加载对应的字体文件 | 前提是资源已经被子集化,比较适用多语言切换的场景 |
简单来说,字体子集化可单独食用,按需加载则必须要将字体前置子集化。才能完美实现按需加载。就我的这个项目而言,动态子集化方案不要太完美,毕竟一张海报本身就没几个字儿!所以我们这次将抛弃 CDN,通过动态的将服务本地中的字体资源子集化来实现字体的压缩效果。
这里我们使用python中的一个字体工具库 fontTools 来实现一个动态子集化,类似于 Google Fonts 的实现。核心思路就是将字符传给服务端,通过工具将传入的字符在本地字体文件中提取并返回给客户端,通过fontTools 还可以将TTF格式转化为和Web更搭的WOFF2格式。实现细节如下述代码所示
@app.route('/font/<font_name>', methods=['GET'])
def get_font_subset(font_name):
# 获取本地字体文件路径
font_path = os.path.join(FONTS_DIR, f"{font_name}.ttf")
# 获取子集字符
chars = request.args.get('text', '')
# 字体文件格式
format = request.args.get('format', 'woff2').lower()
# 处理字符,去重
unique_chars = ''.join(sorted(set(chars)))
try:
# 配置子集化选项
options = Options()
options.flavor = format if format in {'woff', 'woff2'} else
options.desubroutinize = True # 增强兼容性
subsetter = Subsetter(options=options)
# 加载字体并生成子集
font = TTFont(font_path)
subsetter.populate(text=unique_chars)
subsetter.subset(font)
# 保存为指定格式
buffer = io.BytesIO()
font.save(buffer)
buffer.seek(0)
# 确定MIME类型
mime_type = {
'woff2': 'font/woff2',
'woff': 'font/woff',
}[format]
# 创建响应并设置
response = Response(buffer.read(), mimetype=mime_type)
# 其他设置...
return response
except Exception as e:
# 子集化失败...
前端代码中增加了一些字符提取的工作,我本身就是通过 FontFace Api
来请求字体资源的,所以我仅需将资源链接替换为子集化字体的接口就可以了,下面代码来描述字体的加载过程
// ...其他逻辑
Toast.loading('字体加载中')
// 遍历海报中的字体对象
[...new Set(fontFamilies)].forEach((fontName) => {
// 在字体库中找到对应字体详细信息
const obj = fontLibrary.find((el) => el?.value === fontName) ?? {};
if (obj.value && obj.src) {
// 处理海报中提取的文案集合
const text = textMap[obj.value].join('');
// 构建字体对象
const font = new FontFace(
obj.value,
`url(http://127.0.0.1:5000/font/${obj.value}?text=${text}&format=woff2)`
);
// 加载字体
font.load();
// 添加到文档字体集中
document.fonts.add(font);
}
});
// 文档所有字体加载完毕后返回成功的 Promise
return document.fonts.ready.finally(() => Toast.destory());
好了,刷新下浏览器,来看看最终的效果:
这这 真立竿见影(主要是基数大😁😁),最终得到的结果就是,实际 22.4M 的字体文件,子集化后缩减到 3.6KB。实际效果图生成的时间由 20s+ 缩减到毫秒级(300ms 以内)。这下就无惧网速了吧!
结语
总的来说,优化字体加载的方案有很多,我们需要结合自己的实际业务场景来进行选型,字体子集化确实是一种高效且实用的优化手段,更多的实践思路可以参考下 Google fonts。
来源:juejin.cn/post/7490337281866317836
面试官:前端倒计时有误差怎么解决
前言
去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的
倒计时为啥不准
一个最简单的常用倒计时:
const [count, setCount] = useState(0)
let total = 10 // 倒计时10s
const countDown = ()=>{
if(total > 0){
setCount(total)
total--
setTimeout(countDown ,1000)
}
}
稍微有几毫秒的误差,但是问题不大。
原因:JavaScript是单线程,setTimeout
的回调函数会被放入事件队列,既然要排队,就可能被前面的任务阻塞导致延迟 。且任务本身从call stack中拿出来执行也要耗时。所以有1000变1002也合理。就算setTimeout
的第二个参数设为0,也会有至少有4ms的延迟。
如果切换了浏览器tab,或者最小化了浏览器,那误差就会变得大了。
倒计时10s,实际时间却经过了15s,误差相当大了。(不失为一种穿越时间去到未来的方法)
原因:当页面处于后台时,浏览器会降低定时器的执行频率以节省资源,导致 setTimeout
的延迟增加。切回来后又正常了
目标:解决切换后台导致的倒计时不准问题
解决方案1
监听 visibilitychange 事件,在切回tab时修正。
页面从后台离开或者切回来,都能触发visibilitychange事件。只需在document.visibilityState === 'visible'时去修正时间,删掉旧的计时器,设置正确的计时,计算下一次触发的差值,然后创建新的计时器。
// 监听页面切换
useEffect(() => {
const handleVisibilityChange = () => {
console.log('Page is visible:', document.visibilityState);
if(document.visibilityState === 'visible'){
updateCount()
}
};
// 添加事件监听器
document.addEventListener('visibilitychange', handleVisibilityChange);
// 清理函数:移除事件监听器
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
// 修正倒计时
const updateCount = ()=>{
clearTimeout(timer) // 清除
const nowStamp = Date.now()
const pastTime = nowStamp - firstStamp
const remainTime = CountSeconds * 1000 - pastTime
if(remainTime > 0){
setCount(Math.floor(remainTime/1000))
total = Math.floor(remainTime/1000)
timer = setTimeout(countDown,remainTime%1000)
}else{
setCount(0)
console.log('最后时间:',new Date().toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}
}
特点:会跳过一些时刻计数,可能会错过一些关键节点上事件触发。如果长时间离开,误差变大,实际时间结束,倒计时仍在,激活页面时才结束。
解决方案2
修改回调函数,自带修正逻辑,每次执行时都去修正
// 每次都修正倒计时
const countDown = ()=>{
const nowDate = new Date()
const nowStamp = nowDate.getTime()
firstStamp = firstStamp || nowStamp
lastStamp = lastStamp || nowStamp
const nextTime = firstStamp + (CountSeconds-total) * 1000
const gap = nextTime - nowStamp ;
// 如果当前时间超过了下一次应该执行的时间,就修正时间
if(gap < 1){
clearTimeout(timer)
if(total == 0){
setCount(0)
console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}else{
console.log('left',total, 'time:',nowDate.toLocaleString(),'间隔:',nowStamp-lastStamp)
lastStamp = nowStamp
setCount(total)
total--
countDown()
}
}else{
timer = setTimeout(countDown,gap)
}
}
结果:
特性:每个倒计时时刻都触发,最后更新更精准。(顺便一提,edge浏览器后台状态timeout间隔最低是1000)
解决方案3
上面的都依赖Date模块,改本地时间就会爆炸,一切都乱套了。(可以用performance.now 来缺相对值判断时间)
有没有方案让时钟像邓紫棋一样一直倒数的
有的,就是用web worker,单独的线程去计时,不会受切tab影响
let intervalId;
let count = 0;
self.onmessage = function (event) {
const data = event.data; // 接收主线程传递的数据
console.log('Worker received:', data);
count = data;
intervalId = setInterval(countDown,1000); // 这里用了interval
};
function countDown() {
count--
self.postMessage(count); // 将结果发送回主线程
if (count == 0) {
clearInterval(intervalId);
}
}
const [worker, setWorker] = useState(null);
// 初始化 Web Worker
useEffect(() => {
const myWorker = new Worker(new URL('./worker.js', import.meta.url));
// 监听 Worker 时钟 返回的消息
myWorker.onmessage = (event) => {
// console.log('Main thread received:', event.data);
const left = event.data
const nowDate = new Date()
const nowStamp = nowDate.getTime()
if(left > 0){
const gap = nowStamp - lastStamp
console.log('left',left, 'time:',nowDate.toLocaleString(),'间隔:',gap)
lastStamp = nowStamp
setCount(left)
}else{
setCount(0)
console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp)
}
};
setWorker(myWorker);
// 清理函数:关闭 Worker
return () => {
myWorker.terminate();
};
}, []);
缺点:worker的缺点 ;优点:精准计时
总结:
方案1 大修正
方案2 小修正
方案3 无修正
三种方式来使倒计时更准确
来源:juejin.cn/post/7478687361737768986
Electron实现静默打印小票
Electron实现静默打印小票
静默打印流程
1.渲染进程通知主进程打印
//渲染进程 data是打印需要的数据
window.electron.ipcRenderer.send('handlePrint', data)
2.主进程接收消息,创建打印页面
//main.ts
/* 打印页面 */
let printWindow: BrowserWindow | undefined
/**
* @Author: yaoyaolei
* @Date: 2024-06-07 09:27:22
* @LastEditors: yaoyaolei
* @description: 创建打印页面
*/
const createPrintWindow = () => {
return new Promise<void>((resolve) => {
printWindow = new BrowserWindow({
...BASE_WINDOW_CONFIG,
title: 'printWindow',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
nodeIntegration: true,
contextIsolation: false
}
})
printWindow.on('ready-to-show', () => {
//打印页面创建完成后不需要显示,测试时可以调用show查看页面样式(下面有我处理的样式图片)
// printWindow?.show()
resolve()
})
printWindow.webContents.setWindowOpenHandler((details: { url: string }) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
printWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/print.html`)
} else {
printWindow.loadFile(join(__dirname, `../renderer/print.html`))
}
})
}
ipcMain.on('handlePrint', (_, obj) => {
//主进程接受渲染进程消息,向打印页面传递数据
if (printWindow) {
printWindow!.webContents.send('data', obj)
} else {
createPrintWindow().then(() => {
printWindow!.webContents.send('data', obj)
})
}
})
3.打印页面接收消息,拿到数据渲染页面完成后通知主进程开始打印
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>打印</title>
<style>
</style>
</head>
<body>
</body>
<script>
window.electron.ipcRenderer.on('data', (_, obj) => {
//这里是接受的消息,处理完成后将html片段放在body里面完成后就可以开始打印了
//样式可以写在style里,也可以内联
console.log('event, data: ', obj);
//这里自由发挥
document.body.innerHTML = '处理的数据'
//通知主进程开始打印
window.electron.ipcRenderer.send('startPrint')
})
</script>
</html>
这个是我处理完的数据样式,这个就是print.html
4,5.主进程接收消息开始打印,并且通知渲染进程打印状态
ipcMain.on('startPrint', () => {
//这里如果不指定打印机使用的是系统默认打印机,如果需要指定打印机,
//可以在初始化的时候使用webContents.getPrintersAsync()获取打印机列表,
//然后让用户选择一个打印机,打印的时候将打印机名称传过来
printWindow!.webContents.print(
{
silent: true,
margins: { marginType: 'none' }
//deviceName:如果要指定打印机传入打印机名称
},
(success) => {
//通知渲染进程打印状态
if (success) {
mainWindow.webContents.send('printStatus', 'success')
} else {
mainWindow.webContents.send('printStatus', 'error')
}
}
)
})
完毕~
来源:juejin.cn/post/7377645747448365091
富文本选型太难了,谁来帮帮我!
前言
在管理平台中,富文本编辑器是信息输入和内容编辑的重要工具,几乎是必不可少的功能模块。它不仅能帮助用户轻松创建和格式化文档、邮件、模版等内容,还支持多样化的输入方式,提升了内容管理的便捷性和效率。
遇事不决扔个富文本,随心所欲不逾矩
富文本编辑器的实现虽然为用户提供了极大的便利,但其背后的技术复杂度也不容小觑。实现一个全面且高效的富文本编辑器,涉及到跨浏览器兼容性、复杂的格式化操作、多媒体支持等多个技术难题,往往需要花费大量的人力物力。即便如此,富文本编辑器也不可能满足所有用户的需求
。不同的用户对功能有着各自的偏好.对于用户来说,选择富文本编辑器时,也需要在功能性与操作便捷性之间进行取舍
,找到最适合自己需求的解决方案。
碰到说做一个和word相同功能的富文本的领导,直接上砖头
我认为选择富文本要考虑以下这些重要的功能:
- 页面简洁美观(不难看是前提,不要说功能好用 - 不好看的界面,我连用都不用)
- 支持从
Word中复制、粘贴
- 格式化功能丰富,尽可能的支持各种文本和段落的样式
- 多媒体功能丰富,支持对
图片大小、位置
的处理 - 支持
html代码与显示切换
- 支持并满足复杂的
表格功能
- 插件拓展
- 多端兼容
- 编辑器不能基于某一种编程语言,迁移成本小
- 多语言支持(对于部分海外客户可能有需求)
你很难想象竟然有不会英文的客户,强烈要求我们更换富文本编辑器
富文本测评
Tinymce
Tinymce
是一个老牌做富文本的公司,文档和插件配置的自由度都不错,也支持自定义拓展。功能强大,可以完全作为用户的首选
免费版如下图所示:
评测一下Tinymce的优缺点:
优点:
- 老牌做富文本的公司,且不断保持更新和维护,值得信赖
- UI也做的蛮好看的(吐槽一下5.X好丑)
- 免费版功能强大,基本能满足日常需要(开源版本支持商用,nice)
- 功能强大:如导出,自定义插件,表格功能强大,文件上传,复制粘贴,数学方程等等
- 对非技术用户友好: 所见所得,拖动即可完成所有
- 支持多端,移动端友好
- 社区丰富,文档友好,集成简单
- 支持多种语言,阿拉伯这种程序员的噩梦也支持
缺点:
图片上传需要自定义
- 超链接不友好并且很丑
- UI不是很好看 - 我们领导吐槽好丑
- 复杂的word复制过去格式会变化,需要重新编辑
打开缓慢,需要开发者和使用者有相当的耐心
我司从5.X开始使用,目前已迭代到7.X(UI好看了好多)
不知道怎么选的情况下,选Tinymce肯定没错,当然也不能对Tinymce有太大要求,你会失望的
CKEditor
CKEditor
也是一个老牌做富文本的公司, 5.0版本无论是功能还是UI做的相当不错,毫不夸张的说,这是我见过插件最丰富的富文本了
完整版如下图所示:
评测一下CKEditor的优缺点:
优点:
- 老牌做富文本的公司,且不断保持更新和维护,值得信赖
- UI简洁美观
- 功能强大:100多个插件,如导出,自定义插件,表格功能强大,文件上传,复制粘贴,数学方程等等
- 对非技术用户友好: 所见所得,拖动即可完成所有
- 支持多主题配置
- 支持多端,移动端友好
- 支持多人协作
- 支持多种语言,阿拉伯这种程序员的噩梦也支持
缺点:
- 价格比较贵,
免费版功能太少,可能不会满足日常使用
- API太多,
文档对于国内不是很友好, 开发成本高
- V38.0.0起,
免费版会显示“Powered by CKEditor”logo
(这也是我司最后放弃使用CKEditor的原因之一)
最基础的免费版如下:
基于CKEditor的优缺点,推荐复杂文档编辑及格式化的业务使用
白嫖党不推荐,适合人民币玩家
Tiptap
Tiptap是一个模块化的编辑器, 官方解释是tipTap是一个无头编辑器,无头特征决定了完全自由。
自由扩展,自由定义。
它基于Prosemirror,完全可扩展且无渲染。可以轻松地将自定义外观、样式、位置等等
可以像这样:
也可以像这样:
灵活的API,不仅可以让你进行天马行空的布局,各种事件也可以让你用到飞起。作为一个后起之秀,迅速占领市场,赢得了大量客户的好评。真心推荐大家去使用一下!
下面评测一下Tiptap富文本
优点:
- 高度可定制的UX
- 功能强大,插件丰富
- 可快速搭建,集成简单
- 和element适配度高,使用element-ui不要太爽
支持多人协作
缺点:
- 自由度太高,配置较复杂,需要对API有一定的了解
不支持markdown
经查阅资料,tiptap插件支持markdown格式,感谢 李里_lexy 和 imber的指正
想要多人协作的,不要考虑了:用它,用它,用它!
Quill
我司最开始使用的一款富文本。配置简单,UI也还不错。中文文档丰富,一天的时间就可以集成到项目中。
评测一下Quill的优缺点
优点:
- UI还可以(虽说排版看起来有点乱)
- API和文档简单,一天的时间就可以集成到项目中
- 支持word的复制,粘贴操作
(加分项)
- 持续更新,从发布到至今,已13年之久,不用担心不维护的问题
缺点:
- 对图片非常不友好,不支持图片拖拽
- 不支持表格
- 不支持html与界面的切换(在文档中统一替换很困难,需要技术人员配合)
在v-if中显示文本到编辑文本切换后,会无法输入文本,官方bug且没有被修复
全局改样式的时候,从控制台中取一下html代码,然后全局替换一下忍忍也就算了。随着图片上传后,文档排版越来越麻烦,真的不能忍了。只能宣布这个富文本的倒计时了!
适合对图片没有太多需求,并且对文档没有太大格式需求的公司
个人开发蛮推荐的
wangEditor
国产之光,个人能做成这个样子,我还是由衷的佩服的。
有兴趣的掘友们可以看下为什么都说富文本编辑器是天坑?。觉得自己还可以的掘友可以试着做做下面这两个小功能?
- 输入一个Hello, world!
- Hello加粗,llo斜体,world加粗,ld下划线
- 选中hello,取消选择,复制,粘贴,全选,删除
- 兼容主流浏览器
- 看一下标签至少符合规范(加粗斜体不能嵌套,p标签不能包块元素,是否有空标签...)
不要小看这个小功能,我敢打赌,95%的程序员至少一周的时间或者压根做不出来
赌一包辣条,立贴为证
好了,回到正题。我们评测一下wangEditor富文本
优点:
- 简单易用,可以快速集成
- 中文文档友好
- 支持图片和表格拖拽
- 社区友好,可以在github提交意见和反馈
- 多语言支持
缺点:
- 个人开源,相对专门做富文本的公司,相对配置型和丰富性不足
- 移动端不适配,Android下有严重bug,可能会影响使用
暂停维护了
首先向wangEditor开源作者双越老师表示敬意,因为开源想盈利太不容易了,基本都是为爱发电
不考虑移动端的,并且不介意暂停维护的可以使用
否则直接pass吧,
用Jodit或者wangEditor-next替代吧
wangEditor-next
wangEditor-next的作者cycleccc在简介中这样写道:
wangeditor 因作者个人原因短期不再维护,wangEditor-next为fork版本,将在尽量不 Breaking change 的前提下继续维护。
此外,wangEditor-next也不再是个人单打独斗了,开始多人团队协作。最近几个月更新也很频繁,近3000次commit也能看出该团队的活跃度和持续产出,这种高频次的更新不仅反映了团队对用户反馈的响应速度,确保项目能够持续迭代并满足用户需求。
期待wangEditor-next能够在开源和盈利中找到平衡点,期待国人的产品能够获得广泛认可,为用户提供优质的体验。
Jodit
免费版功能如下:
- 风格和wangEditor类似,但功能要比wangEditor强大的多。
- 项目持续维护中,不用担心跑路的问题
- 支持图片和表格拖拽
- 支持word复制和粘贴
- 支持
打印
- 适配
移动端
,可预览 - 支持多语言
白嫖党的福音
大招:收费版来了
在原有的基础上又增加了以下实用的功能
- 支持
文档翻译和谷歌地图
- 支持
预览和导出pdf
- 支持自定义button
- 支持插入iframe
- 支持恢复功能(
不是撤销,是真的回档
) - 支持查找和替换(很nice的功能)
一次性付费99$(一个项目),399$(无限项目)即可解锁,我觉得超值
相比于wangEditor,我更推荐Jodit。无论是免费版,还是付费版,都值得拥有和尝试
Editor.js
Editor也是一个模块化编辑器。与Tiptap有很多相似之处,如模块化设计,可拓展性等等。这里不详细展开了,主要说一下两个编辑器的不同点:
- Editor.js
默认生成 JSON 格式的数据
,便于解析和存储。适合保存结构化的数据
,如文档管理
等等等。
Tiptap 功能更强大,适合需要精细控制的富文本编辑器应用,尤其适合需要所见即所得体验的场景,如
博客编辑器
等等 - Tiptap的社区及文档更友好,非常适合 Vue 或 React 项目集成,更适合初次开发者
- Editor功能较少,可能不满足使用
Editorbug比较多
,虽然已经修复了好多,但建议还是慎选
Editor默认使用JSON格式的数据,更易于展示和分析,除非有强烈需求,否则慎选
Slate
引用一下原文:
Slate是一个 完全 可定制的富文本编辑器框架。
Slate
让你构建像 Medium, Dropbox Paper或者是 Google Docs(它们正成为web
应用的标杆)这样丰富,直观的编辑器,而不会让你在代码实现上深陷复杂度的泥潭。
文档介绍的很酷,但目前是beta
版本,且仍没有计划发布正式版
。我没有用过,不做过多的评价,有兴趣的可以自己去体验一下
开源项目,没有大公司的支持,完全是自愿奉献。30k的star用户默默的支持,期待早日发布
很酷的架构设计,推荐大家去体验一下,期待Slate的早日发布
medium-editor
一款轻量级的编辑器,压缩后约为28KB
, 除了工具栏可以显示在文本上方,支持内联编辑外,我没有找到其他的优点。4年没有更新, 插件拓展非常不友好。对不起这16K的star
如果你喜欢这种编辑方式,还可以体验一下。除此外,直接Pass
Squire
又是一款轻量级的编辑器,压缩完才11.5kb
,相对于其它的富文本来说是非常的小了,推荐功能不复杂
的场景使用
轻量级的编辑器,相较于medium-editor更符合用户习惯,推荐功能不复杂的场景使用
UEditor
看到上面这个图片,估计很多用户直接就断绝了使用的想法吧。确实,UI设计的真不好看,不符合当今的审美。可是在小10年前,用百度UEditor的比比皆是,只能说此一时彼一时。
我们评测一下UEditor富文本
优点:
- 发布之初,功能强大,但是放在现在,已经有点弱了
- 支持从word复制粘贴
- 中文文档友好
缺点:
- 无力吐槽的UI
- 官方已经不维护了,gg
- 不支持图片和表格拖拽
- ...
不吐槽了,直接用省略号吧。祭奠我的青春
久远的富文本,官方已经不维护了,不推荐使用
UEditor Plus
UEditor Plus 是基于 UEditor 二次开发的富文本编辑器
让人诟病的UI风格终于焕新了,还算在我的审美点上了,简单评测一下
优点:
- UI风格焕新还是蛮清新的(
功能平铺还是感觉有点乱,太占空间了
) - 支持图片拖拽
- 表格功能强大(虽然有些功能我不太会用)
- 兼容现有UEditor
缺点:
- 作为开发者,很多功能都不会用,更别提普通用户了,估计一脸懵
- 为了功能而功能,忽视了用户体验
- 更新迭代比较慢,社区不友好,不是很关心用户的反馈
试用了1天,总给我不踏实的感觉,担心某一天又不维护了
编辑器的功能体验不是很好,有些功能是为了功能而功能,真心希望提升一下用户体验
Summernote
一款韩国人做的开源编辑器。基于 jQuery 和 Bootstrap 构建,支持快捷键操作,提供大量可定制的选项,乍一看,页面挺清新简洁的。但使用下来非常让人之气愤
。用户提的bug和优化项目完全不理,格式化也是做的很差劲。搞不懂11k的star是怎么出来的
不推荐用。除非你喜欢用hook去擦屁股
lexical
Facebook出品的编辑器,大厂出品,值得信赖。
lexical是draft-js的升级版。由于draft-js
只作维护,不做开发。整体功能已迁移到Lexical新框架。
github这样写道:
该项目目前处于维护模式。它不会收到任何功能更新,只会收到关键的安全漏洞补丁。2022 年 12 月 31 日,该仓库将完全归档。
对于寻找开源替代方案的用户,Meta 一直致力于迁移到一个名为Lexical的新框架。它仍处于实验阶段,我们正在努力添加迁移指南,但我们相信,它提供了一种更高效、更易于访问的替代方案。
样式见上图,很清爽,很漂亮。多而不乱,我蛮喜欢的。
功能上也很强大,图片拖拽、表格拖拽、导入导出,分享,还能进行涂鸦,绘图。还可以选中文本设置文本样式,同时还能对文本进行评论...功能上没得说,由于React是 Facebook的亲儿子,lexical也是基于React的编辑器 。虽然可以在vue上用,但官方支持主要集中在 React 上,因此在 Vue 中的集成需要额外处理一些兼容性问题。违背了我们选择不依赖框架语言的初衷
。因此将lexical放在最后,不做过多的评价,也不会放在简评中。只是让我们多了解一下,尤其是React用户,可以作为备选方案
但要注意:Lexical 不支持 Internet Explorer 或旧版本的 Edge
。需要兼容IE以及Edge浏览器的业务就不要选了
Facebook出品,可以作为React全家桶使用
简评
强烈推荐
Tinymce: 不知道怎么去选的时候,就用Tinymce!不求有功,但求无过
CKEditor: 文档格式化内容众多,追求使用效率,并且土豪公司或者人民币玩家就用它
Tiptap: 喜欢diy样式的,追求美观和别具一格的开发者首选
Jodit: 一次性付款,你值得拥有
比较推荐
Quill: 集成简单,小项目够用,大项目不推荐
wangEditor: 国人之光
wangEditor-next: wangEditor的升级版,持续迭代,持续为您服务
Editor: 存储,解析JSON数据的首选,其他慎选
Slate: 很棒的架构设计,期待早上发布
UEditor Plus: 基于 UEditor 二次开发的富文本编辑器,UI和功能都做了升级,初步体验还是蛮不错的,个人建议还是要慎用
不推荐
UEditor: 百度的烂尾项目,不够打了
Squire: 轻量级编辑器,格式文本还是可以的
Summernote: 韩国佬开发的项目,大爷级别的,反人类,还不搭理你
medium-editor 除了在文档上方显示,找不到其他的优点
总结
优秀的富文本编辑器不仅要页面美观,而且要格式化功能强大,使用方便快捷。非技术用户也能轻松排版、编辑、上传多媒体内容,从而提升管理平台的整体易用性和用户体验。因此,选择合适的富文本编辑器,不仅关系到内容生产的效率,更直接影响到平台的用户满意度和运营效果。
同时,我们也要认识到,不可能有一个编辑器能百分百满足我们的需求
,理解开发富文本编辑器是一个巨难,且很难盈利的项目。只能尽可能的满足我们的痛点,在使用体验和需求功能上找到一个平衡点,权衡选择。
最后,祝开发找到适合自己公司的编辑器,早日脱坑
参考资料
来源:juejin.cn/post/7434373084747333658
十年跨平台开发,Electron 凭什么占据一席之地?
大家好,我是徐徐。今天我们来认识认识 Electron。
前言
其实一直想系统的写一写 Electron 相关的文章,之前在掘金上写过,但是现在来看那些文章都写得挺粗糙的,所以现在我决定系统整理相关的知识,输出自己更多 Electron 开发相关的经验。这一节我们主要是来认识一下 Electron,这个已经有 10 年历史的跨端开发框架。我将从诞生背景,优劣势,生态,案例以及和其他框架的对比这几个方面带大家来认识 Electron。
Electron 诞生背景
Electron 的背景还是很强劲的,下面我们就来看看它是如何诞生的。
起源
Electron 的前身 Atom Shell,由 GitHub 的开发者在 2013 年创建的,当时 Atom 需要一个能够在桌面环境中运行的跨平台框架,同时能够利用 web 技术构建现代化的用户界面,于是就有了 Electron 的雏形。
需求 & Web 技术的发展
互联网的兴起使得桌面端的需求日益增长,传统的桌面应用开发需要针对每个操作系统(Windows、macOS、Linux)分别编写代码,这增加了开发和维护成本,所以非常需要可以通过一次开发实现多平台支持的框架。
随着 HTML5、CSS3 和 JavaScript 的快速发展,web 技术变得越来越强大和灵活。开发者希望能够利用这些技术构建现代化的用户界面,并且享受 web 开发工具和框架带来的便利。这使得更加需要一款跨端大杀器架来支持开发者,Electron 应运而生。
发展历程
- 2013 年:Atom Shell 诞生,最初用于 GitHub 的 Atom 编辑器。
- 2014 年 2 月:Atom 编辑器对外发布,Atom Shell 作为其核心技术。
- 2015 年 4 月:Atom Shell 更名为 Electron,并作为一个独立项目发布。随着时间的推移,Electron 的功能和社区支持不断增强。
- 2016 年:Electron 的应用开始广泛传播,许多公司和开发者开始使用 Electron 构建跨平台桌面应用。
- 2020 年:Electron 发布 10.0 版本,进一步增强了稳定性和性能。
- 2023 年:Electron 10 周年
更多可以参考:
http://www.electronjs.org/blog/electr…
Electron 优势
Electron 的优势非常的明显,大概总结为下面四个方面。
跨平台支持
Electron 的最大优势在于其跨平台特性。开发者可以编写一次代码,Electron 会处理不同操作系统之间的差异,使应用能够在 Windows、macOS 和 Linux 上无缝运行。
前端技术栈
Electron 应用使用 HTML、CSS 和 JavaScript 构建界面。开发者可以使用流行的前端框架和工具(如 React、Vue.js、Angular)来开发应用,提高开发效率和代码质量。
Node.js 集成
Electron 将 Chromium 和 Node.js 集成在一起,这使得应用不仅可以使用 web 技术构建界面,还可以使用 Node.js 访问底层系统资源,如文件系统、网络、进程等。
强大社区
Electron 拥有丰富的文档、教程、示例和强大的社区支持。开发者可以很容易地找到解决问题的方法和最佳实践,从而加快开发速度。
Electron 劣势
当然,一个东西都有两面性,有优势肯定也有劣势,劣势大概总结为以下几个方面。
性能问题
Electron 应用由于需要运行一个完整的 Chromium 实例,通常会占用较高的内存和 CPU 资源,性能相对较差。这在资源有限的设备上(如老旧计算机)尤为明显。
打包体积大
由于需要包含 Chromium 和 Node.js 运行时,Electron 应用的打包体积较大。一个简单的 Electron 应用的打包体积可能达到几十到上百 MB,这对于一些应用场景来说是不小的负担。
安全性
Electron 应用需要处理 web 技术带来的安全问题,如跨站脚本(XSS)攻击和远程代码执行(RCE)漏洞。开发者需要特别注意安全性,采取适当的防护措施(如使用 contextIsolation
、sandbox
、Content Security Policy
等)。
生态
上面谈到了 Electron 的优势和劣势,下面我们来看看 Electron 的生态。对于一款开源框架,生态是非常关键的,社区活跃度以及相应的配套工具非常影响框架的生态,如果有众多的开发者支持和维护这个框架,那么它的生态才会越来越好。Electron 的生态依托于 Node.js 发展出了很多很多开源工具,其生态是相当的繁荣。下面可以看看两张图就知道其生态的繁荣之处。
- GitHub 情况
- NPM 情况
下面是一些常见的相关生态工具。
打包和分发工具
- electron-packager:用于将 Electron 应用打包成可执行文件。支持多平台打包,简单易用。
- electron-builder:一个功能强大的打包工具,支持自动更新、多平台打包和安装程序制作。
测试工具
- Spectron:基于 WebDriver,用于 Electron 应用的端到端测试。支持模拟用户操作和验证应用行为。
- electron-mocha:用于在 Electron 环境中运行 Mocha 测试,适合进行单元测试和集成测试。
开发工具
- Electron Forge:一个集成开发工具,简化了 Electron 应用的开发、打包和分发流程。支持脚手架、插件系统和自动更新。
- Electron DevTools:调试和分析 Electron 应用性能的工具,帮助开发者优化应用性能。
案例
用 Electron开发的软件非常多,国内外都有很多知名的软件,有了成功的案例才会吸引更多的开发者使用它,下面是一些举例。
国内
- 微信开发者工具
- 百度网盘
- 语雀
- 网易灵犀办公
- 网易云音乐
国外
- Visual Studio Code
- Slack
- Discord
- GitHub Desktop
- Postman
其他更多可参考:http://www.electronjs.org/apps
一个小技巧,Mac 电脑检测应用是否是 Electron 框架,在命令行运行如下代码:
for app in /Applications/*; do;[ -d $app/Contents/Frameworks/Electron\ Framework.framework ] && echo $app; done
和其他跨端框架的对比
一个框架的诞生避免不了与同类型的框架对比,下面是一个对比表格,展示了 Electron 与其他流行的跨端桌面应用开发框架(如 NW.js、Proton Native、Tauri 和 Flutter Desktop)的优缺点和特性:
特性 | Electron | NW.js | Proton Native | Tauri | Flutter Desktop |
---|---|---|---|---|---|
开发语言 | JavaScript, HTML, CSS | JavaScript, HTML, CSS | JavaScript, React | Rust, JavaScript, HTML, CSS | Dart |
框架大小 | 大(几十到几百 MB) | 中等(几十 MB) | 中等(几十 MB) | 小(几 MB) | 大(几十到几百 MB) |
性能 | 中等 | 中等 | 中等 | 高 | 高 |
跨平台支持 | Windows, macOS, Linux | Windows, macOS, Linux | Windows, macOS, Linux | Windows, macOS, Linux | Windows, macOS, Linux |
使用的技术栈 | Chromium, Node.js | Chromium, Node.js | React, Node.js | Rust, WebView | Flutter Engine |
生态系统和社区 | 非常活跃,生态丰富 | 活跃 | 停滞了 | 新兴,快速增长 | 活跃,现阶段更新不频繁 |
开发难度 | 易于上手 | 易于上手 | 需要 React 知识 | 需要 Rust 和前端知识 | 需要 Dart 知识 |
自动更新支持 | 内置支持 | 需要手动实现 | 需要手动实现 | 需要手动实现 | 需要手动实现 |
原生功能访问 | 通过 Node.js 模块访问 | 通过 Node.js 模块访问 | 通过 Node.js 和原生模块访问 | 通过 Rust 原生模块访问 | 通过插件和原生模块访问 |
热重载和开发体验 | 支持(需要配置) | 支持(需要配置) | 支持(需要配置) | 支持(需要配置) | 支持(内置支持) |
打包和发布 | Electron Builder, Forge | nw-builder | 需要手动配置打包工具 | Tauri 打包工具 | Flutter build tools |
常见应用场景 | 聊天应用、生产力工具、IDE | 聊天应用、生产力工具 | 小型工具和实用程序 | 轻量级、性能要求高的应用 | 跨平台移动和桌面应用 |
知名应用 | VS Code, Slack, Discord, 知名应用 | WebTorrent, 其他工具 | 小型 React 工具和应用 | 新兴应用和工具 | 仅少数桌面应用,Flutter主打移动应用 |
结语
Electron 是一个强大的跨平台开发框架,其诞生对前端开发者的意义非常大,让很多从事前端的开发者也有机会开发桌面客户端,扩大了前端开发工程师的岗位需求。当然,它不一定是最好的框架,因为适合自己的才是最好的,主要还是看自己的业务场景和技术需要,优势和劣势都是需要考虑的,仁者见仁,智者见智。
来源:juejin.cn/post/7416902812251111476
Llama 4 训练作弊爆出惊天丑闻!AI 大佬愤而辞职,代码实测崩盘全网炸锅
【新智元导读】Llama 4 本该是 AI 圈的焦点,却成了大型翻车现场。开源首日,全网实测代码能力崩盘。更让人震惊的是,模型训练测试集被曝作弊,内部员工直接请辞。
Meta 前脚刚发 Llama 4,后脚就有大佬请辞了!
一亩三分地的爆料贴称,经过反复训练后,Llama 4 未能取得 SOTA,甚至与顶尖大模型实力悬殊。
为了蒙混过关,高层甚至建议:
在后训练阶段中,将多个 benchmark 测试集混入训练数据。
在后训练阶段中,将多个 benchmark 测试集混入训练数据。
最终目的,让模型短期提升指标,拿出来可以看起来不错的结果。
这位内部员工 @dliudliu 表示,「自己根本无法接受这种做法,甚至辞职信中明确要求——不要在 Llama 4 技术报告中挂名」。
另一方面,小扎给全员下了「死令」——4 月底是 Llama 4 交付最后期限。
在一系列高压之下,已有高管提出了辞职。
其实,Llama 4 昨天开源之后,并没有在业内得到好评。全网测试中,代码能力极差,实力不如 GPT-4o。
网友 Flavio Adamo 使用相同的提示词,分别让 Llama 4 Maveric 和 GPT-4o 制作一个旋转多边形的动画。
可以看出,Llama 4 Maveric 生成的多边形并不规则而且没有开口。小球也不符合物理规律,直接穿过多边形掉下去了。
相比之下 GPT-4o 制作的动画虽然也不完美,但至少要好得多。
甚至,有人直接曝出,Llama 4 在 LMarena 上存在过拟合现象,有极大的「作弊」嫌疑。
而如今,内部员工爆料,进一步证实了网友的猜想。
沃顿商学院教授 Ethan Mollick 一语中的,「如果你经常使用 AI 模型,不难分辨出哪些是针对基准测试进行优化的,哪些是真正的重大进步」。
不过,另一位内部员工称,并没有遇到这类情况,不如让子弹飞一会儿。
内部员工爆料,Llama 4 训练作弊?
几位 AI 研究人员在社交媒体上都「吐槽」同一个问题,Meta 在其公告中提到 LM Arena 上的 Maverick 是一个「实验性的聊天版本」。
如果看得仔细一点,在 Llama 官网的性能对比测试图的最下面一行,写着「Llama 4 Maverick optimized for conversationality.」
翻译过来就是「针对对话优化的 Llama 4 Maverick」——似乎有些「鸡贼」。
这种「区别对待」的会让开发人员很难准确预测该模型在特定上下文中的表现。
AI 的研究人员观察到可公开下载的 Maverick 与 LM Arena 上托管的模型在行为上存在显著差异。
而就在今天上午,已经有人爆料 Llama 4 的训练过程存在严重问题!
即 Llama 4 内部训练多次仍然没有达到开源 SOTA 基准。
Meta 的领导层决定在后训练过程中混合各种基准测试集——让 Llama 4「背题」以期望在测试中取得「好成绩」。
这个爆料的原始来源是「一亩三分地」,根据对话,爆料者很可能来自于 Meta 公司内部。
对话中提到的 Meta AI 研究部副总裁 Joelle Pineau 也申请了 5 月底辞职。(不过,也有网友称并非是与 Llama4 相关)
但是根据 Meta 的组织架构体系,Pineau 是 FAIR 的副总裁,而 FAIR 实际上是 Meta 内部与 GenAI 完全独立的组织,GenAI 才是负责 Llama 项目的组织。
GenAI 的副总裁是 Ahmad Al-Dahle,他并没有辞职。
Llama 4 才刚刚发布一天,就出现如此重磅的消息,让未来显得扑朔迷离。
代码翻车,网友大失所望
在昨天网友的实测中,评论还是有好有坏。
但是过去一天进行更多的测试后,更多的网友表达了对 Llama 4 的不满。
在 Dr_Karminski 的一篇热帖中,他说 Llama-4-Maverick——总参数 402B 的模型——在编码能力方面大致只能与 Qwen-QwQ-32B 相当。
Llama-4-Scout——总参数 109B 的模型——大概与 Grok-2 或 Ernie 4.5 类似。
在评论中,网友响应了这个判断。
有人说 Llama 4 的表现比 Gemma 3 27B 还要差。
有人认为 Llama 4 的表现甚至和 Llama 3.2 一样没有任何进步,也无法完成写诗。
其他用户在测试后也表达了同样的观点,Llama 4 有点不符合预期。
网友 Deedy 也表达了对 Llama 4 的失望,称其为「一个糟糕的编程模型」。
他表示,Scout (109B) 和 Maverick (402B) 在针对编程任务的 Kscores 基准测试中表现不如 4o、Gemini Flash、Grok 3、DeepSeek V3 和 Sonnet 3.5/7。
他还给出了贴出了 Llama 4 两个模型的一张测试排名,结果显示这两个新发布的模型远远没有达到顶尖的性能。
网友 anton 说,Llama 4「真的有点令人失望」。
他表示自己不会用它来辅助编码,而 Llama 4 的定位有点尴尬。
anton 认为 Llama 4 的两个模型太大了,不太好本地部署。他建议 Meta 应该推出性能优秀的小模型,而不是去追求成为 SOTA。
「因为目前他们根本做不到。」他写道。
参考资料:
来源:juejin.cn/post/7490391697093476378
URL地址末尾加不加”/“有什么区别
URL 结尾是否带 /
主要影响的是 服务器如何解析请求 以及 相对路径的解析方式,具体区别如下:
1. 基础概念
- URL(统一资源定位符) :用于唯一标识互联网资源,如网页、图片、API等。
- 目录 vs. 资源:
- 以
/
结尾的 URL 通常表示目录,例如:
https://example.com/folder/
- 不以
/
结尾的 URL 通常指向具体的资源(如文件),例如:
https://example.com/file
- 以
2. 带 /
和不带 /
的具体区别
(1)目录 vs. 资源
https://example.com/folder/
- 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如
index.html
)。
- 服务器通常会将其解析为 目录,并尝试返回该目录下的默认文件(如
https://example.com/folder
- 服务器可能会将其视为 文件,如果
folder
不是文件,而是目录,服务器可能会返回 301 重定向到folder/
。
- 服务器可能会将其视为 文件,如果
📌 示例:
- 访问
https://example.com/blog/
- 服务器可能返回
https://example.com/blog/index.html
。
- 服务器可能返回
- 访问
https://example.com/blog
(如果blog
是个目录)
- 服务器可能重定向到
https://example.com/blog/
,再返回index.html
。
- 服务器可能重定向到
(2)相对路径解析
URL 末尾是否有 /
会影响相对路径的解析。
假设 HTML 页面包含以下 <img>
标签:
<img src="image.png">
📌 示例:
- 访问
https://example.com/folder/
- 访问
https://example.com/folder
- 图片路径解析为
https://example.com/image.png
- 可能导致 404 错误,因为
image.png
在folder/
里,而浏览器错误地去example.com/
下查找。
- 图片路径解析为
原因:
- 以
/
结尾的 URL,浏览器会认为它是一个目录,相对路径会基于folder/
解析。 - 不带
/
,浏览器可能认为folder
是文件,相对路径解析可能会出现错误。
(3)SEO 影响
搜索引擎对 https://example.com/folder/
和 https://example.com/folder
可能会视为两个不同的页面,导致 重复内容问题,影响 SEO 排名。因此:
- 网站通常会选择 一种形式 并用 301 重定向 规范化 URL。
- 例如:
https://example.com/folder
自动跳转 到https://example.com/folder/
。- 反之亦然。
(4)API 请求
对于 RESTful API,带 /
和不带 /
可能导致不同的行为:
https://api.example.com/users
- 可能返回所有用户数据。
https://api.example.com/users/
- 可能返回 404 或者产生不同的结果(取决于服务器实现)。
一些 API 服务器对 /
非常敏感,因此最好遵循 API 文档的规范。
3. 总结
URL 形式 | 作用 | 影响 |
---|---|---|
https://example.com/folder/ | 目录 | 通常返回 folder/ 下的默认文件,如 index.html ,相对路径解析基于 folder/ |
https://example.com/folder | 资源(或重定向) | 可能被解析为文件,或者服务器重定向到 folder/ ,相对路径解析可能错误 |
https://api.example.com/data/ | API 路径 | 可能与 https://api.example.com/data 表现不同,具体由 API 设计决定 |
如果你在开发网站,建议:
- 统一 URL 规则,例如所有目录都加
/
或者所有请求都不加/
,然后用 301 重定向 确保一致性。 - 测试 API 的行为,确认带
/
和不带/
是否影响请求结果。
来源:juejin.cn/post/7468112128928350242
Electron调用dll的新姿势
之前旧的系统在浏览器中调用dll都是使用IE的activex控件实现。进而通过dll脚本和硬件发生交互。现在IE浏览器已经不在默认预制在系统中,且对windows的操作系统有限制,win10之前的版本才能正常访问。
在不断的业务迭代过程中,已经成了制约系统扩展的最大阻碍。调研后选择了electron-egg框架来进行业务功能尝试,主要是dll的嵌入调用和设备交互。
ElectronEgg
作为一个对前端不是那么擅长的后端来说,electron-egg已经包装了大部分操作,且拥有非常详尽的中文开发文档。可以无缝切换,低成本代码的开发。
框架设计
具体的业务逻辑还是放在后台服务中,electron只负责透传交互指令和硬件设备进行交互。这里调用dll用的js库是koffi。
Koffi
Koffi 是一个快速且易于使用的 Node.js C FFI 模块。实现了在Node中调用dll的功能。
koffi版本2.8.0
DLL配置
按照官方文档dll文件放置在extraSources文件中。
DLL加载
const lib = koffi.load(path.join(Ps.getExtraResourcesDir(), "dll", "dcrf32.dll"));
DLL调用
dll调用有两种方式。分别为经典语法和类c原型方式。
- 经典语法
定义函数名,返回参数类型,入参类型constprintf=lib.func('printf','int', ['str','...']);
- 类C语法
在类中定义方法类似,lib.func('int printf(const char *fmt, ...)');
推荐使用类C语法
更加方便,不受参数的限制,更易于修改。
DLL调用类型
- 同步调用
本机函数,您就可以像调用任何其他 JS 函数一样简单地调用它。
const atoi = lib.func('int atoi(const char *str)');
let value = atoi('1257');
- 异步调用
有一些耗时的操作,可以使用异步调用回调的方式处理。
const atoi = lib.func('int atoi(const char *str)');
atoi.async('1257', (err, res) => {
console.log('Result:', res);
})
JS类型值传递
JS基础类型时不支持值传递的,遇到需要传递指针变量时,直接调用是无法获取到变更后的值。相应的koffi也提供了非常方便的值包装。
- 数组包装
项目中采用比较方便的数组包装来进行值传递。包装基础对象到数组中,变更后取出第一位就能获取到变更后的值。
需要定义返回的值的获取长度,防止出现只获取到部分的返回结果。
- 引用类型包装
把基础类型包装成引用对象。传递到函数中。
let cardsenr = koffi.alloc('int', 64);
let cardRet = dcCard(icdev, 0, cardsenr);
这种就更方便,调用后也不需要转换。在调用完后需要通过free方法进行内存释放。
- Buffer对象
如果遇到接收中文数据时,koffi可以结合Node中的Buffer进行对象传递。
let text = Buffer.alloc(1024);
let ret = read(text);
部分dll默认读出来的编码是gbk格式,需要将buffer对象转换成utf8格式的字符串进行展示。 就需要通过iconv
组件进行gbk解码。
iconv.decode(text, 'gbk')
如果需要把utf8转成gbk,使用相反的方式就可以
iconv.encode(
build/photos/${id_number}.bmp, "gbk")
结构体调用
JS中只有引用对象,如果遇到结构体参数需要进行JS包装。
// C
typedef struct A {
int a;
char b;
const char *c;
struct {
double d1;
double d2;
} d;
} A;
// JS
const A = koffi.struct('A', {
a: 'int',
b: 'char',
c: 'const char *', // Koffi does not care about const, it is ignored
d: koffi.struct({
d1: 'double',
d2: 'double'
})
});
如果调用出现对齐不对的情况,可以使用pack方法进行手动对齐类型。
// This struct is 3 bytes long
const PackedStruct = koffi.pack('PackedStruct', {
a: 'int8_t',
b: 'int16_t'
});
常规dll的调用都可以轻易的在JS中实现。
Node后端
底层调用已经通过koffi来实现。后面就需要借助electron-egg框架能力进行业务代码指令透传。
service层
'use strict';
const { Service } = require('ee-core');
/**
* 示例服务(service层为单例)
* @class
*/
class ExampleService extends Service {
constructor(ctx) {
super(ctx);
}
/**
* test
*/
async test(args) {
let obj = {
status:'ok',
params: args
}
return obj;
}
}
ExampleService.toString = () => '[class ExampleService]';
module.exports = ExampleService;
定义我们需要交互的方法
controller层
'use strict';
const { Controller } = require('ee-core');
const Log = require('ee-core/log');
const Services = require('ee-core/services');
/**
* example
* @class
*/
class ExampleController extends Controller {
constructor(ctx) {
super(ctx);
}
/**
* 所有方法接收两个参数
* @param args 前端传的参数
* @param event - ipc通信时才有值。详情见:控制器文档
*/
/**
* test
*/
async test () {
const result = await Services.get('example').test('electron');
Log.info('service result:', result);
return 'hello electron-egg';
}
}
ExampleController.toString = () => '[class ExampleController]';
module.exports = ExampleController;
JS前端
通过ipc的方式,乡调用api一样调用node后端接口。
定义路由
import { ipc } from '@/utils/ipcRenderer';
const ipcApiRoute = {
test: 'controller.d8.test',
init: 'controller.d8.init',
reset: 'controller.d8.reset',
exit: 'controller.d8.exit',
command:'controller.d8.command'
}
调用
ipc.invoke(this.ipcApiRoute.init).then(r => {
// r为返回的数据
if(r >= 0) {
this.setInitRet(r);
this.scrollToBottom("连接成功,返回码:" + r);
this.connectStr = "连接成功";
this.setDeviceStatus('success');
} else {
this.scrollToBottom("连接失败,返回码:" + r);
}
})
通过ipc的invode方法调用方法即可。后续就可以愉快的编写我们的业务代码了。
参照
来源:juejin.cn/post/7352075771534868490
公司来的新人用字符串存储日期,被组长怒怼了...
在日常的软件开发工作中,存储时间是一项基础且常见的需求。无论是记录数据的操作时间、金融交易的发生时间,还是行程的出发时间、用户的下单时间等等,时间信息与我们的业务逻辑和系统功能紧密相关。因此,正确选择和使用 MySQL 的日期时间类型至关重要,其恰当与否甚至可能对业务的准确性和系统的稳定性产生显著影响。
本文旨在帮助开发者重新审视并深入理解 MySQL 中不同的时间存储方式,以便做出更合适项目业务场景的选择。
不要用字符串存储日期
和许多数据库初学者一样,笔者在早期学习阶段也曾尝试使用字符串(如 VARCHAR)类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,'YYYY-MM-DD HH:MM:SS' 这样的格式看起来清晰易懂。
但是,这是不正确的做法,主要会有下面两个问题:
- 空间效率:与 MySQL 内建的日期时间类型相比,字符串通常需要占用更多的存储空间来表示相同的时间信息。
- 查询与计算效率低下:
- 比较操作复杂且低效:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观(例如,'2024-05-01' 会小于 '2024-1-10'),而且效率远低于使用原生日期时间类型进行的数值或时间点比较。
- 计算功能受限:无法直接利用数据库提供的丰富日期时间函数进行运算(例如,计算两个日期之间的间隔、对日期进行加减操作等),需要先转换格式,增加了复杂性。
- 索引性能不佳:基于字符串的索引在处理范围查询(如查找特定时间段内的数据)时,其效率和灵活性通常不如原生日期时间类型的索引。
DATETIME 和 TIMESTAMP 选择
DATETIME
和 TIMESTAMP
是 MySQL 中两种非常常用的、用于存储包含日期和时间信息的数据类型。它们都可以存储精确到秒(MySQL 5.6.4+ 支持更高精度的小数秒)的时间值。那么,在实际应用中,我们应该如何在这两者之间做出选择呢?
下面我们从几个关键维度对它们进行对比:
时区信息
DATETIME
类型存储的是字面量的日期和时间值,它本身不包含任何时区信息。当你插入一个 DATETIME
值时,MySQL 存储的就是你提供的那个确切的时间,不会进行任何时区转换。
这样就会有什么问题呢? 如果你的应用需要支持多个时区,或者服务器、客户端的时区可能发生变化,那么使用 DATETIME
时,应用程序需要自行处理时区的转换和解释。如果处理不当(例如,假设所有存储的时间都属于同一个时区,但实际环境变化了),可能会导致时间显示或计算上的混乱。
TIMESTAMP
和时区有关。存储时,MySQL 会将当前会话时区下的时间值转换成 UTC(协调世界时)进行内部存储。当查询 TIMESTAMP
字段时,MySQL 又会将存储的 UTC 时间转换回当前会话所设置的时区来显示。
这意味着,对于同一条记录的 TIMESTAMP
字段,在不同的会话时区设置下查询,可能会看到不同的本地时间表示,但它们都对应着同一个绝对时间点(UTC 时间)。这对于需要全球化、多时区支持的应用来说非常有用。
下面实际演示一下!
建表 SQL 语句:
CREATE TABLE `time_zone_test` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`date_time` datetime DEFAULT NULL,
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入一条数据(假设当前会话时区为系统默认,例如 UTC+0)::
INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());
查询数据(在同一时区会话下):
SELECT date_time, time_stamp FROM time_zone_test;
结果:
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
+---------------------+---------------------+
现在,修改当前会话的时区为东八区 (UTC+8):
SET time_zone = '+8:00';
再次查询数据:
# TIMESTAMP 的值自动转换为 UTC+8 时间
+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |
+---------------------+---------------------+
扩展:MySQL 时区设置常用 SQL 命令
# 查看当前会话时区
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';
占用空间
下图是 MySQL 日期类型所占的存储空间(官方文档传送门:dev.mysql.com/doc/refman/…):
在 MySQL 5.6.4 之前,DateTime 和 TIMESTAMP 的存储空间是固定的,分别为 8 字节和 4 字节。但是从 MySQL 5.6.4 开始,它们的存储空间会根据毫秒精度的不同而变化,DateTime 的范围是 58 字节,TIMESTAMP 的范围是 47 字节。
表示范围
TIMESTAMP
表示的时间范围更小,只能到 2038 年:
DATETIME
:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'TIMESTAMP
:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC
性能
由于 TIMESTAMP
在存储和检索时需要进行 UTC 与当前会话时区的转换,这个过程可能涉及到额外的计算开销,尤其是在需要调用操作系统底层接口获取或处理时区信息时。虽然现代数据库和操作系统对此进行了优化,但在某些极端高并发或对延迟极其敏感的场景下,DATETIME
因其不涉及时区转换,处理逻辑相对更简单直接,可能会表现出微弱的性能优势。
为了获得可预测的行为并可能减少 TIMESTAMP
的转换开销,推荐的做法是在应用程序层面统一管理时区,或者在数据库连接/会话级别显式设置 time_zone
参数,而不是依赖服务器的默认或操作系统时区。
数值时间戳是更好的选择吗?
除了上述两种类型,实践中也常用整数类型(INT
或 BIGINT
)来存储所谓的“Unix 时间戳”(即从 1970 年 1 月 1 日 00:00:00 UTC 起至目标时间的总秒数,或毫秒数)。
这种存储方式的具有 TIMESTAMP
类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。
时间戳的定义如下:
时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。
数据库中实际操作:
-- 将日期时间字符串转换为 Unix 时间戳 (秒)
mysql> SELECT UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
| 1578707612 |
+---------------------------------------+
1 row in set (0.00 sec)
-- 将 Unix 时间戳 (秒) 转换为日期时间格式
mysql> SELECT FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
| 2020-01-11 09:53:32 |
+---------------------------+
1 row in set (0.01 sec)
PostgreSQL 中没有 DATETIME
由于有读者提到 PostgreSQL(PG) 的时间类型,因此这里拓展补充一下。PG 官方文档对时间类型的描述地址:http://www.postgresql.org/docs/curren…。
可以看到,PG 没有名为 DATETIME
的类型:
- PG 的
TIMESTAMP WITHOUT TIME ZONE
在功能上最接近 MySQL 的DATETIME
。它存储日期和时间,但不包含任何时区信息,存储的是字面值。 - PG 的
TIMESTAMP WITH TIME ZONE
(或TIMESTAMPTZ
) 相当于 MySQL 的TIMESTAMP
。它在存储时会将输入值转换为 UTC,并在检索时根据当前会话的时区进行转换显示。
对于绝大多数需要记录精确发生时间点的应用场景,TIMESTAMPTZ
是 PostgreSQL 中最推荐、最健壮的选择,因为它能最好地处理时区复杂性。
总结
MySQL 中时间到底怎么存储才好?DATETIME
?TIMESTAMP
?还是数值时间戳?
并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。
《高性能 MySQL 》这本神书的作者就是推荐 TIMESTAMP,原因是数值表示时间不够直观。下面是原文:

每种方式都有各自的优势,根据实际场景选择最合适的才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:
类型 | 存储空间 | 日期格式 | 日期范围 | 是否带时区信息 |
---|---|---|---|---|
DATETIME | 5~8 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999] | 否 |
TIMESTAMP | 4~7 字节 | YYYY-MM-DD hh:mm:ss[.fraction] | 1970-01-01 00:00:01[.000000] ~ 2038-01-19 03:14:07[.999999] | 是 |
数值型时间戳 | 4 字节 | 全数字如 1578707612 | 1970-01-01 00:00:01 之后的时间 | 否 |
选择建议小结:
TIMESTAMP
的核心优势在于其内建的时区处理能力。数据库负责 UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。如果应用需要处理多时区,或者希望数据库能自动管理时区转换,TIMESTAMP
是自然的选择(注意其时间范围限制,也就是 2038 年问题)。- 如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示 2038 年之后的时间,
DATETIME
是更稳妥的选择。 - 如果极度关注比较性能,或者需要频繁跨系统传递时间数据,并且可以接受可读性的牺牲(或总是在应用层转换),数值时间戳是一个强大的选项。
来源:juejin.cn/post/7488927722774937609
你见过的最差的程序员是怎样的?
我见过的最差程序员,差到让整个团队崩溃
作为一名在嵌入式领域摸爬滚打近十年的老兵,我见过太多奇葩程序员了。但要说最差的,非"赵工"莫属。
初见赵工
那是我从机械调剂到电子部门的第二年,公司接了个重要项目,需要开发一款基于STM32的工业控制系统。领导从总部借来一位"资深嵌入式专家"——赵工。
初见赵工时,他西装革履,一副成功人士模样。"我做过BAT核心项目,对单片机开发了如指掌",他面试时的豪言壮语,让领导对他寄予厚望。
"独特"的编码风格
接手项目的第一周,赵工就展示了他的"实力":
void Do_Something(void)
{
u8 a;
u8 b;
u8 c;
u8 i;
u8 j;
u8 k;
a=1;
b=2;
if(a==1)
{
for(i=0;i<10;i++)
{
if(b==2)
{
k = i + 1;
//do something here
}
}
}
}
没错,这就是他的编码风格——变量命名全是单字母,没有注释,缩进混乱,函数名毫无意义。当我问他这些变量代表什么意思时,他瞪了我一眼:"代码就是写给机器看的,能运行就行,哪那么多讲究?"
"高效"的调试方法
赵工的调试方法更是"高效"。有一次系统死机,排查原因时,他直接往代码里塞了几十个printf:
printf("here1\n");
if(temp > 50) {
printf("here2\n");
control_valve();
printf("here3\n");
}
printf("here4\n");
任何正常程序员都会使用条件断点或日志系统,但他偏要用这种原始方法。更可怕的是,调试完成后,这些垃圾代码常常被他忘记删除,留在生产代码中。
"革命性"的存储管理
记得有次他在处理EEPROM存储时,创造了这样的"杰作":
// 存储用户配置
void save_config(void)
{
// 直接从0地址开始写,不管有没有其他数据
EEPROM_Write(0, (uint8_t*)&global_config, sizeof(global_config));
}
// 加载配置
void load_config(void)
{
// 没有任何校验,直接读取
EEPROM_Read(0, (uint8_t*)&global_config, sizeof(global_config));
}
没有地址规划,没有数据校验,没有版本管理。当我提醒他这会导致数据混乱时,他不以为然:"又不是大型系统,用不着那么复杂。"
结果可想而知,产品一上线,用户配置经常莫名其妙丢失或混乱。
"高级"的内存管理
在一个需要处理大量传感器数据的模块中,他写出了这样的代码:
void process_sensor_data(void)
{
// 每次分配固定大小,用完不释放
uint8_t *buffer = malloc(1024);
// 处理数据...
// 没有free操作
}
这个函数每分钟会被调用几十次,内存泄漏严重。当系统运行几小时后必然崩溃。我指出这个问题时,他竟然说:"单片机会自动回收内存的,不用担心。"
我当时就懵了,这种基础常识都不懂,他是怎么通过面试的?
"创新"的版本控制
提到版本控制,赵工也有独到见解。公司用Git管理代码,他却坚持用自己的方式:
- 从不写commit信息,或者就写个"update"
- 本地修改后直接push到master分支
- 代码出问题了,就复制整个项目文件夹重命名为"project_backup_0415"
有一次他把整个主分支代码弄坏了,急得团队其他成员直冒冷汗。当问他为什么不用分支开发时,他理直气壮:"那太麻烦了,我一个人开发用不着那些东西。"
"高超"的团队协作
赵工的团队协作能力堪称一绝。记得有次我接手他的一个模块进行扩展,打开代码后惊呆了:
// 神秘函数
void xyz(void)
{
u16 m = get_value();
if(m > 30)
{
op();
}
else if(m <= 30 && m > 20)
{
op2();
}
else
{
if(flag)
{
op3();
}
}
}
完全看不懂这函数是干什么的!没有文档,没有注释,变量名全是缩写,函数名毫无意义。我只好硬着头皮找他问。
他却说:"代码写出来就是给机器看的,你看不懂是你水平问题。再说了,这是我的核心竞争力,如果写得太清楚,公司还要我干嘛?"
这种"核心竞争力"理论让我哭笑不得。在我看来,真正的核心竞争力是创造价值的能力,而不是制造混乱的能力。
灾难的项目结局
最后这个项目如何收场?你们猜到了。
原定三个月的项目拖了半年,客户不断投诉系统不稳定。在一次重要演示中,系统当场崩溃,客户大怒。公司损失了一个重要客户,也赔了一大笔违约金。
赵工却毫不愧疚,反而抱怨环境问题:"肯定是测试环境不对,我本地运行得好好的。"
最终,他被公司礼貌地送回了总部,项目由我和另外两位同事重构。我们花了两个月才把这烂摊子收拾干净。
反思:什么造就了"最差程序员"
回想这段经历,我总结赵工这类"最差程序员"的特质:
- 技术傲慢:自以为是,不接受批评,拒绝学习新知识
- 基础薄弱:缺乏编程基本素养,连最基础的内存管理、代码规范都不遵守
- 自私封闭:视代码为个人财产,故意设置理解障碍
- 责任推卸:问题永远是别人的,从不反思自己
- 短视功利:只关心眼前能跑,不考虑长期维护
这种程序员不仅技术差,更可怕的是态度差。他们像一颗定时炸弹,迟早会给团队和产品带来灾难。
与之对比:什么是好程序员
我27岁进入世界500强外企时,遇到一位让我敬佩的技术主管李工。他的代码风格截然不同:
/**
* @brief 处理温度传感器数据并控制阀门
* @param temperature 当前温度值(摄氏度)
* @return 操作是否成功
* @note 当温度超过临界值时,会自动关闭阀门
*/
bool processTempAndControlValve(float temperature)
{
// 安全检查
if (!isSensorValid(SENSOR_TEMP)) {
logError("Temperature sensor not valid!");
return false;
}
// 温度过高,关闭阀门
if (temperature > CRITICAL_TEMP_THRESHOLD) {
logWarning("Critical temperature detected: %.2f°C", temperature);
return closeValve(VALVE_MAIN);
}
// 正常温度范围
return true;
}
他的代码:
- 命名清晰,一看就懂
- 有完善注释和文档
- 考虑异常情况
- 模块化,便于测试和维护
- 遵循团队代码规范
更重要的是,他从不吝啬分享知识。每周五下午,他都会组织技术分享会,讲解嵌入式Linux的各种难点。正是在他的影响下,我开始自学Linux,并在28岁时开始写技术公众号分享所学。
职场启示:远离"赵工",培养好习惯
这些经历让我深刻认识到,成为好程序员不仅关乎技术,更关乎态度和习惯。这也是我30岁创业后,在培训和咨询中一直强调的核心理念。
在我的小公司里,我们有严格的代码审查制度,无论资历高低,代码必须符合规范才能合并。有位刚入职的年轻人抱怨:"写那么多注释太浪费时间了!"我给他看了赵工项目的代码和我们后来重构的对比,他立刻理解了。
好的编程习惯就像复利,短期看不到效果,长期却能造就天壤之别。这也是我从嵌入式开发一路走来的深刻体会。
结语
如果你在团队中遇到了"赵工"式的程序员,请保持警惕,远离这种技术债务制造机。如果你担心自己可能有类似倾向,请反思并改变,这对你的职业生涯至关重要。
真正的编程高手,不仅代码写得好,更能让团队变得更好。就像我在二线城市靠技术和分享积累第一个百万时所感悟的:技术能力决定下限,协作能力决定上限。
作为一个从机械转行到嵌入式的非科班程序员,我深知基础扎实和态度端正的重要性。希望每位程序员都能远离"最差",走向更好的自己。
你们遇到过什么样的奇葩程序员?欢迎在评论区分享,我们一起吐槽一下。
另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。
刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!
有收获?希望老铁们来个三连击,给更多的人看到这篇文章
推荐阅读:
欢迎关注我的博客:良许嵌入式教程网,满满都是干货!
来源:juejin.cn/post/7489488440113692724
websocket和socket有什么区别?
WebSocket 和 Socket 的区别
WebSocket 和 Socket 是两种不同的网络通信技术,它们在使用场景、协议、功能等方面有显著的差异。以下是它们之间的主要区别:
1. 定义
- Socket:Socket 是一种网络通信的工具,可以实现不同计算机之间的数据交换。它是操作系统提供的 API,广泛应用于 TCP/IP 网络编程中。Socket 可以是流式(TCP)或数据报(UDP)类型的,用于低层次的网络通信。
- WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器和客户端之间实时地交换数据。WebSocket 是建立在 HTTP 协议之上的,主要用于 Web 应用程序,以实现实时数据传输。
2. 协议层次
- Socket:Socket 是一种底层通信机制,通常与 TCP/IP 协议一起使用。它允许开发者通过编程语言直接访问网络接口。
- WebSocket:WebSocket 是一种应用层协议,建立在 HTTP 之上。在初始握手时,使用 HTTP 协议进行连接,之后切换到 WebSocket 协议进行数据传输。
3. 连接方式
- Socket:Socket 通常需要手动管理连接的建立和关闭。通过调用相关的 API,开发者需要处理连接的状态,确保数据的可靠传输。
- WebSocket:WebSocket 的连接管理相对简单。建立连接后,不需要频繁地进行握手,可以保持持久连接,随时进行数据交换。
4. 数据传输模式
- Socket:Socket 可以实现单向或双向的数据传输,但通常需要在发送和接收之间进行明确的控制。
- WebSocket:WebSocket 支持全双工通信,客户端和服务器之间可以随时互相发送数据,无需等待响应。这使得实时通信变得更加高效。
5. 适用场景
- Socket:Socket 常用于需要高性能、低延迟的场景,如游戏开发、文件传输、P2P 网络等。由于其底层特性,Socket 适合对网络性能有严格要求的应用。
- WebSocket:WebSocket 主要用于 Web 应用程序,如即时聊天、实时通知、在线游戏等。由于其易用性和高效性,WebSocket 特别适合需要实时更新和交互的前端应用。
6. 数据格式
- Socket:Socket 发送的数据通常是二进制流或文本流,需要开发者自行定义数据格式和解析方式。
- WebSocket:WebSocket 支持多种数据格式,包括文本(如 JSON)和二进制(如 Blob、ArrayBuffer)。WebSocket 的数据传输格式非常灵活,易于与 JavaScript 进行交互。
7. 性能
- Socket:Socket 对于大量并发连接的处理性能较高,但需要开发者进行优化和管理。
- WebSocket:WebSocket 在建立连接后可以保持长连接,减少了握手带来的延迟,适合高频率的数据交换场景。
8. 安全性
- Socket:Socket 的安全性取决于使用的协议(如 TCP、UDP)和应用层的实现。开发者需要自行处理安全问题,如加密和身份验证。
- WebSocket:WebSocket 支持通过 WSS(WebSocket Secure)进行加密,提供更高层次的安全保障。它可以很好地与 HTTPS 集成,确保数据在传输过程中的安全性。
9. 浏览器支持
- Socket:Socket 是底层的网络通信技术,通常不直接在浏览器中使用。Web 开发者需要通过后端语言(如 Node.js、Java、Python)来实现 Socket 通信。
- WebSocket:WebSocket 是专为 Web 应用设计的,所有现代浏览器均支持 WebSocket 协议,开发者可以直接在客户端使用 JavaScript API 进行通信。
10. 工具和库
- Socket:使用 Socket 进行开发时,开发者通常需要使用底层网络编程库,如 BSD Sockets、Java Sockets、Python's socket 模块等。
- WebSocket:WebSocket 提供了简单的 API,开发者可以使用原生 JavaScript 或第三方库(如 Socket.IO)轻松实现 WebSocket 通信。
结论
总结来说,WebSocket 是一种为现代 Web 应用量身定制的协议,具有实时、双向通信的优势,而 Socket 是一种底层的网络通信机制,提供更灵活的使用方式。选择使用哪种技术取决于具体的应用场景和需求。对于需要实时交互的 Web 应用,WebSocket 是更合适的选择;而对于底层或高性能要求的网络通信,Socket 提供了更多的控制和灵活性。
来源:juejin.cn/post/7485631488114278454
当上小组长的第3天,我裁掉了2年老员工
前言
这周末和上上公司的小伙伴小酌一杯,获悉了两则消息,一则好消息,一则坏消息。
好消息是他晋升了,当了个小组长,管了4个人。
坏消息是他需要优化掉组内一个人。
征得本人同意,本文以他的视角来回顾这个魔幻的一周。
职业之殇
20年刚毕业那会,怀着满腔热情进入了某互联网公司,成为了人见人爱的前端CV仔,一心想要造出几个叫得上名字的轮子,也幻想着某天别人称呼我为轮子哥。
人在怀揣梦想的时候感觉总有使不完的劲,工作中捣鼓了一些轮子,也在公司内部使用了,虽然有些Bug,但瑕不掩瑜,没有哪个轮子出来就是完美的,慢慢优化就是了。
然而天不遂人愿,公司因为一些不可说的原因,业务没法继续开展,它倒闭了,算上上一份实习的公司,这已经是我干垮的第二家公司了。
后来同事们戏称:"XX,你没当成轮子哥,却成了垮司哥"
我:"..."
突然晋升
23年的行情你们是知道,还好前端的中级岗位还算比较多。
是的,我又入职了新公司。
前端有十个人,只有一个领导,姑且叫B吧。
与我同一年入职的还有2个小伙伴,其中一个是C。
平淡的日子没啥可留恋的,今天就是昨天复刻。
前几天下午,B突然钉钉发消息给我,还DING了一下,让我到会议室聊一下。
当时感觉挺诧异的,平时虽然也有消息往来,但从来没有单独约谈过,难道有什么坏消息?难道要干掉我了?思绪百转千回。
进了会议室就看到B正襟危坐面对着Mac笔记本,让我做到他对面,他的键盘声时不时想起。
B:是这样子的,你来公司快两年了,这段时间你的产出也是有目共睹的,技术也不错。现在团队内人也比较多,我一个人忙不过来,因此我向领导推荐你当前端的小组长,这事你觉得如何?
我:额,有点突然,可我之前没管过人呢?
B:凡事都有第一次,而且这次你管的人也不多,我们分成两个组,一个是我带,另一个是你带。你只需要管住你底下的4个人就好。
我:那我管哪几个人呢?
B:某某某...,这几个平时相处怎么样?
我:哦哦,这几位老哥我都是比较熟,平时也经常一起吃饭什么的,还好说话。
B:嗯,你回去先考虑一下,确定了明天就会发正式通知。
我:好的。
当B说出让我到小组长的瞬间,其实我已经接受了,没啥好考虑的,毕竟我之前没当过,也跃跃欲试。
接下来的几天,因为这事开心了不少,感觉每天都不一样...
裁员广进
过了三天,周五下午,又是B找我到会议室聊。
B:通知下来,组织架构变了,角色切换得咋样了?
我:还好啊,还是在做以前的事。
B:你现在是小组长了,算是管理了,管、理需要并进。
B:组内的同事工作了解的怎么样了?
我:还好吧,我们平时工作都有交集,大概知道他们在弄什么。
B:你觉得C怎么样,我需要真实的想法?
我:C和我同一年来的,工作年限比我长,技术也可以啊。
B:C技术比你如何?
我:额,各有千秋吧,侧重点不一样。
B:C招进来的时候工资比较高,但他的工资没有匹配他的产出,你看平时他也不怎么加班,很多时候到点就走,不像其他的同事有干劲,感觉他的积极性、主动性都不怎么强。
我:听说C的媳妇最近怀孕了,父亲也因病住院了,可能比较忙。
B:我们不说理由,只管结果,上头最近在盘点人力,前端需要走一个人,这个名额我倾向落到C,你找他谈谈。公司的底线是赔1个月,不能再多。
我:但他的绩效没问题啊,不是应该n+1吗?
B:他去年Q4得的是E,今年Q1再给一个E,两个E就可以因为绩效低而少给赔偿,之前其它部门也是有这种先例,Q1的E我倾向给他背,你来操作一下,理由要写的有理有据。
我:4月就准备发年终奖了(普遍1个月),是不是发了之后再让他走。
B:不可能,就是要在年终发之前给他赔偿,你只管通知他,具体会有人事去说。
我:...
走出会议室,我的心情是拔凉拔凉的,心想:资本果然是邪恶的,充满了算计。
整个下午我都在犹豫怎么和C开口,代码都没写几行。
犹豫着告诉了他这条消息,他会不会周末就过不好。
犹豫着告诉了他这条消息,他会不会记恨我,毕竟经常一起吃饭也互加了微信好友。
犹豫着告诉了他这条消息,组内的其他同事怎么看我...
最终我还是将B的想法告诉了C,全程我没怎么敢看C的眼睛,我怕看到他失落的眼神。
没想到的是C听到这消息还很平静,只说了一句:
公事公办,好聚好散
尘埃落定
我不知道C的周末怎么过的,我也不知道C最终和人事咋谈的,但结局已注定,C肯定要走。
期间我和B也争取了。
我:C还是比较有经验的,有些疑难问题还得靠他,而且平时对待工作也是蛮负责的。
B:我们不看态度,只看结果,他性价比不高。现在的疑难问题问AI就可以了,他走了我们再招一个新人,哪怕是实习生也可以在AI的帮助下胜任工作,还解决了一半多的成本。
我:C手里还有负责的比较重要的工作怕是不好分出来。
B:这会就要体现出你水平的时候了,该怎么平稳交接,我只要一个结果,记住必须是正向的结果。管理管理,就是管和理,既要管人(管人的行为),也要理人(修理人,让他走)。在公司工作就是要以公司的利益为准。我就是一个比较纯粹的人,只对事不对人,只做对团队对公司有益的事,其他的放第二位。
我:心想:泥马,价值观都出来了,我还能说啥...
经过这事,把刚当上小组长的喜悦心情基本冲没了,有时候我在想:
人和人都是有差距的,屁股决定脑袋,不要轻信别人,也不要总是试图说服别人。
你在工作过程中会遇到哪些冲击三观的事?说出来让大家涨涨姿势。
来源:juejin.cn/post/7487210421209186355
AI对话的逐字输出:流式返回才是幕后黑手
AI应用已经渗透到我们生活的方方面面。其中,AI对话系统因其自然、流畅的交流方式而备受瞩目。前段时间有人在交流群中问到,如何实现AI对话中那种逐字输出的效果,这背后,流式返回技术发挥了关键作用。

欢迎加入前端筱园交流群:点击加入交流群
其实这背后并不是前端做了什么特效,而是采用的流式返回,而不是一次性返回完整的响应。流式返回允许服务器在一次连接中逐步发送数据,而不是一次性返回全部结果。这种方式使得前端可以在等待完整响应的过程中,逐步展示生成的内容,从而极大地提升了用户体验。
那么,前端接收流式返回具体有哪些方式呢?接下来,本文将详细探讨几种常见的技术手段,帮助读者更好地理解并应用流式返回技术。
使用 Axios
大多数场景下,前端用的最多的就是axios来发送请求,但是axios
只有在在Node.js环境中支持设置 responseType: 'stream'
来接收流式响应。
const axios = require('axios');
const fs = require('fs');
axios.get('http://localhost:3000/stream', {
responseType: 'stream', // 设置响应类型为流
})
.then((response) => {
// 将响应流写入文件
response.data.pipe(fs.createWriteStream('output.txt'));
})
.catch((error) => {
console.error('Stream error:', error);
});
特点
- 仅限 Node.js:浏览器中的
axios
不支持responseType: 'stream'
。 - 适合文件下载:适合处理大文件下载。
使用 WebSocket
WebSocket 是一种全双工通信协议,适合需要双向实时通信的场景。
前端代码:
const socket = new WebSocket('ws://localhost:3000');
socket.onopen = () => {
console.log('WebSocket connected');
};
socket.onmessage = (event) => {
console.log('Received data:', event.data);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
socket.onclose = () => {
console.log('WebSocket closed');
};
服务器代码
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', (ws) => {
console.log('Client connected');
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ws.send(JSON.stringify({ message: 'Hello', counter }));
if (counter >= 5) {
clearInterval(intervalId);
ws.close();
}
}, 1000);
ws.on('close', () => {
console.log('Client disconnected');
clearInterval(intervalId);
});
});
虽然WebSocket作为一种在单个TCP连接上进行全双工通信的协议,具有实时双向数据传输的能力,但AI对话情况下可能并不选择它进行通信。主要有以下几点原因:
- 在AI对话场景中,通常是用户向AI模型发送消息,模型回复消息的单向通信模式,WebSocket的双向通信能力在此场景下并未被充分利用
- 使用WebSocket可能会引入不必要的复杂性,如处理双向数据流、管理连接状态等,也会增加额外的部署与维护工作

特点
- 双向通信:适合实时双向数据传输
- 低延迟:基于 TCP 协议,延迟低
- 复杂场景:适合聊天、实时游戏等复杂场景
使用 XMLHttpRequest
虽然 XMLHttpRequest
不能直接支持流式返回,但可以通过监听 progress
事件模拟逐块接收数据
const xhr = new XMLHttpRequest();
xhr.open('GET', '/stream', true);
xhr.onprogress = (event) => {
const chunk = xhr.responseText; // 获取当前接收到的数据
console.log(chunk);
};
xhr.onload = () => {
console.log('Request complete');
};
xhr.send();
服务器代码(Koa 示例):
router.get("/XMLHttpRequest", async (ctx, next) => {
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(
JSON.stringify({ message: "Hello", counter })
);
if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);
ctx.req.on("close", () => {
clearInterval(intervalId);
ctx.res.end();
});
});
可以看到以下的输出结果,在onprogress中每次可以拿到当前已经接收到的数据。它并不支持真正的流式响应,用于AI对话场景中,每次都需要将以显示的内容全部替换,或者需要做一些额外的处理。
如果想提前终止请求,可以使用 xhr.abort()
方法;
setTimeout(() => {
xhr.abort();
}, 3000);
特点
- 兼容性好:支持所有浏览器。
- 非真正流式:
XMLHttpRequest
仍然需要等待整个响应完成,progress
事件只是提供了部分数据的访问能力。 - 内存占用高:不适合处理大文件。
使用 Server-Sent Events (SSE)
SSE 是一种服务器向客户端推送事件的协议,基于 HTTP 长连接。它适合服务器向客户端单向推送实时数据
前端代码:
const eventSource = new EventSource('/sse');
eventSource.onmessage = (event) => {
console.log('Received data:', event.data);
};
eventSource.onerror = (event) => {
console.error('EventSource failed:', event);
};
服务器代码(Koa 示例):
router.get('/sse', (ctx) => {
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(`data: ${JSON.stringify({ message: 'Hello', counter })}\n\n`);
if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);
ctx.req.on('close', () => {
clearInterval(intervalId);
ctx.res.end();
});
});
EventSource 也具有主动关闭请求的能力,在结果没有完全返回前,用户可以提前终止内容的返回。
// 在需要时中止请求
setTimeout(() => {
eventSource.close(); // 主动关闭请求
}, 3000); // 3 秒后中止请求
虽然EventSource支持流式请求,但AI对话场景不使用它有以下几点原因:
- 单向通信
- 仅支持
get
请求:在 AI 对话场景中,通常需要发送用户输入(如文本、文件等),这需要使用 POST 请求 - 无法自定义请求头:
EventSource
不支持自定义请求头(如Authorization
、Content-Type
等),在 AI 对话场景中,通常需要设置认证信息(如 API 密钥)或指定请求内容类型
注意点
返回给 EventSource
的值必须遵循 data:
开头并以 \n\n
结尾的格式,这是因为 Server-Sent Events (SSE) 协议规定了这种格式。SSE 是一种基于 HTTP 的轻量级协议,用于服务器向客户端推送事件。为了确保客户端能够正确解析服务器发送的数据,SSE 协议定义了一套严格的格式规范。SSE 协议规定,服务器发送的每条消息必须遵循以下格式:
field: value\n
其中 field
是字段名,value
是对应的值。常见的字段包括:
data:
:消息的内容(必须)。event:
:事件类型(可选)。id:
:消息的唯一标识符(可选)。retry:
:客户端重连的时间间隔(可选)。
每条消息必须以 两个换行符 (\n\n
) 结尾,表示消息结束
以下是一个完整的 SSE 消息示例:
id: 1\n
event: update\n
data: {"message": "Hello", "counter": 1}\n\n
特点
- 单向通信:适合服务器向客户端推送数据。
- 简单易用:基于 HTTP 协议,无需额外协议支持。
- 自动重连:
EventSource
会自动处理连接断开和重连
使用 fetch
API
fetch
API 是现代浏览器提供的原生方法,支持流式响应。通过 response.body
,可以获取一个 ReadableStream
,然后逐块读取数据。
前端代码:
// 发送流式请求
fetch("http://localhost:3000/stream/fetch", {
method: "POST",
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(new TextDecoder().decode(value));
}
})
.catch((error) => {
console.error("Fetch error:", error);
});
服务器代码(Koa 示例):
router.post("/fetch", async (ctx) => {
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;
let counter = 0;
const intervalId = setInterval(() => {
counter++;
ctx.res.write(JSON.stringify({ message: "Hello", counter }));
if (counter >= 5) {
clearInterval(intervalId);
ctx.res.end();
}
}, 1000);
ctx.req.on("close", () => {
clearInterval(intervalId);
ctx.res.end();
});
});
fetch也同样可以在客户端主动关闭请求。
// 创建一个 AbortController 实例
const controller = new AbortController();
const { signal } = controller;
// 发送流式请求
fetch("http://localhost:3000/stream/fetch", {
method: "POST",
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(new TextDecoder().decode(value));
}
})
.catch((error) => {
console.error("Fetch error:", error);
});
// 在需要时中止请求
setTimeout(() => {
controller.abort(); // 主动关闭请求
}, 3000); // 3 秒后中止请求
打开控制台,可以看到在Response中可以看到返回的全部数据,在EventStream中没有任何内容。
这是由于返回的信息SSE协议规范,具体规范见上文的 Server-Sent Events
模块中有介绍到
ctx.res.write(
`data: ${JSON.stringify({ message: "Hello", counter })}\n\n`
);
但是客户端fetch请求中接收到的数据也包含了规范中的内容,需要前端对数据进一步的处理一下
特点
- 原生支持:现代浏览器均支持
fetch
和ReadableStream
。 - 逐块处理:可以实时处理每个数据块,而不需要等待整个响应完成。
- 内存效率高:适合处理大文件或实时数据。
总结
综上所述,在 AI 对话场景中,fetch
请求 是主流的技术选择,而不是 XMLHttpRequest
或 EventSource
。以下是原因和详细分析:
fetch
是现代浏览器提供的原生 API,基于 Promise,代码更简洁、易读fetch
支持ReadableStream
,可以实现流式请求和响应fetch
支持自定义请求头、请求方法(GET、POST 等)和请求体fetch
结合AbortController
可以方便地中止请求fetch
的响应对象提供了response.ok
和response.status
,可以更方便地处理错误
方式 | 特点 | 适用场景 |
---|---|---|
fetch | 原生支持,逐块处理,内存效率高 | 大文件下载、实时数据推送 |
XMLHttpRequest | 兼容性好,非真正流式,内存占用高 | 旧版浏览器兼容 |
Server-Sent Events (SSE) | 单向通信,简单易用,自动重连 | 服务器向客户端推送实时数据 |
WebSocket | 双向通信,低延迟,适合复杂场景 | 聊天、实时游戏 |
axios (Node.js) | 仅限 Node.js,适合文件下载 | Node.js 环境中的大文件下载 |
最后来看一个接入deekseek的完整例子:
resource.dengzhanyong.com/mp4/7823928…
服务器代码(Koa 示例):
const openai = new OpenAI({
baseURL: "https://api.deepseek.com",
apiKey: "这里是你申请的deepseek的apiKey",
});
// 流式请求 DeepSeek 接口并流式返回
router.post("/fetchStream", async (ctx) => {
// 设置响应头
ctx.set({
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
try {
// 创建一个 PassThrough 流
const stream = new PassThrough();
ctx.body = stream;
// 调用 OpenAI API,启用流式输出
const completion = await openai.chat.completions.create({
model: "deepseek-chat", // 或 'gpt-3.5-turbo'
messages: [{ role: "user", content: "请用 100 字介绍 OpenAI" }],
stream: true, // 启用流式输出
});
// 逐块处理流式数据
for await (const chunk of completion) {
const content = chunk.choices[0]?.delta?.content || ""; // 获取当前块的内容
ctx.res.write(content);
process.stdout.write(content); // 将内容输出到控制台
}
ctx.res.end();
} catch (err) {
console.error("Request failed:", err);
ctx.status = 500;
ctx.res.write({ error: "Failed to stream data" });
}
});
前端代码:
const controller = new AbortController();
const { signal } = controller;
const Chat = () => {
const [text, setText] = useState<string>("");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
function send() {
if (!message) return;
setText(""); // 创建一个 AbortController 实例
setLoading(true);
// 发送流式请求
fetch("http://localhost:3000/deepseek/fetchStream", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
}),
signal,
})
.then(async (response: any) => {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const data = new TextDecoder().decode(value);
console.log(data);
setText((t) => t + data);
}
})
.catch((error) => {
console.error("Fetch error:", error);
})
.finally(() => {
setLoading(false);
});
}
function stop() {
controller.abort();
setLoading(false);
}
return (
<div>
<Input
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<Button
onClick={send}
type="primary"
loading={loading}
disabled={loading}
>
发送
</Button>
<Button onClick={stop} danger>
停止回答
</Button>
<div>{text}</div>
</div>
);
};
写在最后
欢迎加入前端筱园交流群:点击加入交流群
关注我的公众号【前端筱园】,不错过每一篇推送
来源:juejin.cn/post/7478109057044299810
electron+node-serialport串口通信
electron+node-serialport串口通信
公司项目需求给客户开发一个能与电子秤硬件通信的Win7客户端,提供数据读取、处理和显示功能。项目为了兼容win7使用了
electron 22.0.0
版本,串口通信使用了serialport 12.0.0版本
//serialport的基本使用方法
//安装 npm i serialport
import { SerialPort } from 'serialport'
SerialPort.list()//获取串口列表
/** 创建一个串口连接
path(必需):串口设备的路径。例如,'/dev/robot' 或 'COM1'。
baudRate(必需):波特率,即每秒传输的比特数。常见值有 9600、19200、38400、57600、115200 等。
autoOpen(可选):是否在创建对象时自动打开串口。默认为 true。如果设置为 false,你需要手动调用 port.open() 来打开串口。
dataBits(可选):每字节的数据位数。可以是 5、6、7、8。默认值是 8。
stopBits(可选):停止位的位数。可以是 1 或 2。默认值是 1。
parity(可选):校验位类型。可以是 'none'、'even'、'odd'、'mark'、'space'。默认值是 'none'。
rtscts(可选):是否启用硬件流控制(RTS/CTS)。布尔值,默认值是 false。
xon(可选):是否启用软件流控制(XON)。布尔值,默认值是 false。
xoff(可选):是否启用软件流控制(XOFF)。布尔值,默认值是 false。
xany(可选):是否启用软件流控制(XANY)。布尔值,默认值是 false。
highWaterMark(可选):用于流控制的高水位标记。默认值是 16384(16KB)。
lock(可选):是否锁定设备文件,防止其他进程访问。布尔值,默认值是 true。
**/
const serialport = new SerialPort({ path: '/dev/example', baudRate: 9600 })
serialport.open()//打开串口
serialport.write('ROBOT POWER ON')//向串口发送数据
serialport.on('data', (data) => {//接收数据
//data为串口接收到的数据
})
获取串口列表
//主进程main.ts
import { SerialPort, SerialPortOpenOptions } from 'serialport'
//初始化先获取串口列表,提供给页面选择
ipcMain.on('initData', async () => {
const portList = await SerialPort.list()
mainWindow.webContents.send('initData', {portList})
})
//渲染进程
window.electron.ipcRenderer.once('initData', (_,{portList}) => {
//获取串口列表后存入本地,登录页直接做弹窗给客户选择串口,配置波特率
window.localStorage.setItem('portList', JSON.stringify(portList))
})
串口选择
波特率配置
读取数据
公司秤和客户的秤串口配置不一样,所以做了model1和model2区分
//主进程main.ts
let P: SerialPort | undefined
ipcMain.on('beginSerialPort', (_, { path, baudRate }) => {
//区分配置
const portConfig: SerialPortOpenOptions<AutoDetectTypes> =
import.meta.env.VITE_MODE == 'model1'
? {
path: path || 'COM1',
baudRate: +baudRate || 9600, //波特率
autoOpen: true,
dataBits: 8
}
: {
path: path || 'COM1',
baudRate: +baudRate || 115200, //波特率
autoOpen: true,
dataBits: 8,
stopBits: 1,
parity: undefined
}
if (P) {
P.close((error) => {
if (error) {
console.log('关闭失败:', error)
} else {
P = new SerialPort(portConfig)
P?.write('SIR\r\n', 'ascii')//告诉秤端开始发送信息,具体看每个秤的配置,有的不需要
P.on('data', (data) => {
//接收到的data为Buffer类型,直接转为字符串就可以使用了
mainWindow.webContents.send('readingData', data.toString())
})
}
})
} else {
P = new SerialPort(portConfig)
P?.write('SIR\r\n', 'ascii')
P.on('data', (data) => {
mainWindow.webContents.send('readingData', data.toString())
})
}
})
解析数据
<!--渲染进程解析数据-->
<template>
<div class="weight-con">
<div class="weight-con-main">
<div>
<el-text class="wei-title" type="primary">毛<br />重</el-text>
</div>
<div class="weight-panel">
<el-text id="wei-num">{{ weightNum!.toFixed(2) }}</el-text>
<div class="weight-con-footer">当前最大称重:600公斤</div>
</div>
<div>
<el-text class="wei-title" type="primary">公<br />斤</el-text>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { weightModel } from '@/utils/WeightReader'
const emits = defineEmits(['zeroChange'])
const weightNum = defineModel<number>()
window.electron.ipcRenderer.on('readingData', (_, data: string) => {
//渲染进程接收到主进程数据,根据环境变量解析数据
weightNum.value = weightModel[import.meta.env.VITE_MODE](data)
if (weightNum.value == 0) {
emits('zeroChange')
}
})
</script>
//weightReader.ts 解析配置
export type Mode = 'model1' | 'model2'
let str = ''
export const weightModel = {
model1: (weightStr: string) => {
const rev = weightStr.split('').reverse().join('')
return +rev.replace('=', '')
},
module2: (weightStr: string) => {
str += weightStr
if (str.match(/S\s+[A-Za-z]\s*(-?\d*.?\d+)\s*kg/g)) {
const num = str.match(/S\s+[A-Za-z]\s*(-?\d*.?\d+)\s*kg/m)![1]
str = ''
return Number(num)
} else {
return 0
}
}
}
完活~下班!
来源:juejin.cn/post/7387701265796562980
完蛋,被扣工资了,都是JSON惹的祸
JSON是一种轻量级的数据交换格式,基于ECMAScript的一个子集设计,采用完全独立于编程语言的文本格式来表示数据。它易于人类阅读和编写,同时也便于机器解析和生成,这使得JSON在数据交换中具有高效性。
JSON也就成了每一个程序员每天都要使用一个小类库。无论你使用的谷歌的gson
,阿里巴巴的fastjson
,框架自带的jackjson
,还是第三方的hutool的json等。总之,每天都要和他打交道。
但是,却在阴沟里翻了船。
1、平平无奇的接口
/**
* 获取vehicleinfo 信息
*
* @RequestParam vehicleId
* @return Vehicle的json字符串
*/
String loadVehicleInfo(Integer vehicleId);
该接口就是通过一个vehicleId
参数获取Vehicle
对象,返回的数据是Vehicle
的JSON字符串,也就是将获取的对象信息序列化成JSON字符串了。
2、无懈可击的引用
String jsonStr = auctVehicleService.loadVehicleInfo(freezeDetail.getVehicle().getId());
if (StringUtils.isNotBlank(jsonStr)) {
Vehicle vehicle = JSON.parseObject(jsonStr, Vehicle.class);
if (vehicle != null) {
// 后续省略 ...
}
}
看似无懈可击的引用,隐藏着魔鬼。为什么无懈可击,因为做了健壮性的判断,非空字符串、非空对象等的判断,根除了空指针异常。
但是,魔鬼隐藏在哪里呢?
3、故障引发
线上直接出现类似的故障(此报错信息为线下模拟)。
现在测试为什么没有问题:主要的测试了基础数据,测试的数据中恰好没有Date
类型的数据,所以线下没有测出来。
4、故障原因分析
从报错日志可以看出,是因为日期类型的参数导致的。Mar 24, 2025 1:23:10 PM
这样的日期格式无法使用Fastjson
解析。
深入代码查看:
@Override
public String loadVehicleInfo(Integer vehicleId) {
String key = VEHICLE_KEY + vehicleId;
Object obj = cacheService.get(key);
if (null != obj && StringUtils.isNotEmpty(obj.toString())
&& !"null".equals(obj.toString())) {
String result = (String)obj;
return result;
}
String json = null;
try {
Vehicle vInfo = overrideVehicleAttributes(vehicleId);
// 使用了Gson序列化对象
json = gson.toJson(vInfo);
cacheService.setExpireSec(key, gson.toJson(vInfo), 5 * 60);
} catch (Exception e) {
cacheService.setExpireSec(key, "", 1 * 60);
} finally {
}
return json;
}
原来接口的实现里面采用了谷歌的Gson
对返回的对象做了序列化。调用的地方又使用了阿里巴巴的Fastjson
发序列化,导致参数解析异常。
完蛋,上榜是要被扣工资的!!!
5、小结
问题虽小,但是影响却很大。坊间一直讨论着,程序员为什么不能写出没有bug的程序。这也许是其中的一种答案吧。
肉疼,被扣钱了!!!
--END--
喜欢就点赞收藏,也可以关注我的微信公众号:编程朝花夕拾
来源:juejin.cn/post/7485560281955958794
IT外传:老技术部的困境
它像一个锈迹斑斑的铁柱,挪了它,会立马塌一片房。不挪它,又不敢在上面推陈出新。如今是一边定钉子加固,一边试探着放一把椅子,太难了……
年会如期进行,在今年严峻的经济形势下,公司今年的营收和利润,相比去年都有大于30%的增长。这主要得益于老板有个原则:员工要比公司挣得多。也就是今年公司增幅10%,员工收入也要比去年增10%。但是,这个增长不是针对所有人,而是那些挣钱的部门。
老板拉来一堆现金,在年会现场分发。张三,3万;李四,13万;王五25万。销售1部,10万;客服2部,5万……很遗憾,IT研发部,不管是团队还是个人,都榜上无名。
不得不说,确实存在这么一个现象,销售类的岗位是盈利部门,像行政、人力、财务这类属于成本部门。而研发类的岗位,则视情况而定。可以是盈利部门,也可以是成本部门。
这个技术部几十个人,其中老员工很多。这里说的老员工,一方面是指在公司待得久,入职七、八年的大有人在。另一方面,年龄也都很大,40岁的也不在少数。公司很重视老员工,常将入职10多年的立为榜样,这让新员工入职后,感到不可思议以及安全感十足。
很多技术部老人看到发奖金,都会很失落。他们说,每年都这样,热闹是别人的,和自己没关系,连保洁、保安都有个“勤勤恳恳奖”,而程序员啥都没有。干技术没有前途。
作为刚入职的员工,我了解的不是很多,不过也稍微有点旁观者的视角。
公司对于技术部很有成见。各个部门也都有意见,尤其是老板。首先体现在系统的脆弱性上,基本上每年在最关键的营销活动时,服务都会宕机。每次宕机,老板都很着急,事后想让技术部避免此类情况再次发生。而技术部每次都有理由,也会提出新的解决方案。 老板配合技术部,从云服务器改为自建机房,从自建机房又迁移上云服务。
我来公司后,经历过两次宕机。第一次是一个大型活动,技术部说本来是没事的,结果因为运营人员在活动还没结束,就登上后台去查看汇总数据。这个汇总,会导致大量实时计算,结果数据库就顶不住了,停服务也停不掉,死机后数小时才重启成功。
领导说既然是数据库差,那就买数据库。技术部讨论后,感觉不能买。买多少?如果买了之后,还出现问题,那么就会处于舆论的被动面。
第二次,不算是宕机,只是限流。就是很多人来访问,将一部分人挡在外面。体现在app上是一直弹出500错误、服务器内部错误的提示。弄得营销人员都不敢推广了。来了很多人,进不了门,错失很多客源,浪费了营销成本。
后来,技术部又开始总结。原因是APP在一个页面调用了12个不同的接口,而且还有一个接口被连续调用了35次。这导致数据库压力加大,直接100%。幸好禁止用户访问,才没有崩溃。
技术部总监很着急。但是这个总监是App开发出身,不了解服务端。服务出问题了,他就去找后端开发。后端开发者感觉,架构设计是你总监的事情。我就算干好了,那也是你的功劳。因此不优化是本分,优化是情分,有些消极和抵触。
让各个部门吐槽的,还有跟技术部提需求。技术部一直说活多,排不开,响应不了需求。但是,很明显多数人,看起来并不忙,也没有人加班。于是,这几个业务部门领导一碰头,发现都没有开发他们的需求。那他们忙什呢?其实需求提到总监那里,当总监去安排任务时,结果安排不下去,各个组都说自己很忙。最后,总监就向上反馈说自己部门的人都很忙。实际上,可能是几个组长很忙,忙的很烦,不愿接需求,而组员并没有事情做。
整体情况就是这样,老技术们,感觉自己很辛苦,老板也不加钱。不加钱,我没有优化系统的动力,而且你也没有具体的详细策略,出架构那不是我的职责。老板感觉技术部问题频出,系统不稳定,不愿加钱。你干出成绩我才加钱,比如做到今年不宕机,原来需要100万的成本,通过你们技术研发,成本降低到50万。这才叫成绩。
技术:不加钱,我没法努力。老板:干不好,怎么加钱? 两者陷入如此的循环。
老板为了解决技术部的问题,就经常给技术部换领导。因为你告诉老板,宕机的原因是一个接口被调用30多次,他听不懂,也没有解决方案。要钱、要人都好办,你说接口调用太多,他懵了。他只能找一个能对得上话的人去解决。技术部内部是找不出来的,他认为如果存在这样的人,问题就不会发生了。实际上,技术部里的人,都觉着自己能解决,但是前提得加钱。多少钱办多少事,否则我就静止不动,装傻充愣。
结果,就空降了很多领导,换一个不出成果,换一个还是没有起色。但是,每一任领导都会推翻上一位的设计。比如云服务有问题,那我们就自己建机房。下一任领导来了,听说自己的机房不行?我们上云服务不就解决了!这就造成技术部架构经常变,也没有什么积累。更严重的是,员工也倦了,变来变去,反反复复,再有新的改革措施,大家也不愿认真执行了。
并且,在这个过程中,业务还是不断积累和发展的。这也堆积了形形色色的病态业务系统。而这些系统,只有老员工能掌握,里面的机关埋伏,根本没有文档,全在他们的脑子里。想改什么东西,复杂不复杂,里面到底怎么个逻辑,老技术说啥就是啥。你想维持生命,又不能让老员工过于动荡,否则会导致业务断层。
倒是也会新来一些空降的技术领导。所有的空降领导,都能发现问题所在。问题大家都知道,实习生都看得出来。 比如缺乏技术领导力,团队没有激励机制,缺乏考核流程,技术债积累严重,技术体系稳定性缺失,业务混乱,人员消极等等。
但新领导也只能做一些表面上的改善。比如,基层管理说员工都不听他的,多次强调要交周报,底下员工就是不交,导致自己不知道他们每周都在干什么。空降领导说,周报写不好,扣工资,看他们交不交。稳定性缺失?把稳定性纳入考核,谁写的代码不稳定,扣工资。说一直忙还不加班?压缩工期,原定10天的任务,让6天干完,这不就忙起来了。 对于系统BUG多,领导说好解决,发现bug,按照数量扣工资,肯定就没有bug了。
实际上这是一种从末端治理的方案,是对洪水的围堵而非疏通。软件系统的配合是复杂的,更需要从源头开始治理。 发现bug扣程序员的工资,属于问题倒推的行为。Bug需要界定是哪方产生的,是单纯代码逻辑问题,还是产品规则问题,还是用户操作方式问题,或者是偶然问题。领导说:那就扣所有参与成员的工资,这样大家都会紧绷一根弦,谁都会为了没有bug而努力。
另外,功能还有复杂和简单之分。一个简单的版本,比如修改个提示语,可能产生不了bug。但是,如果是一项复杂的业务,比如写一个机器人对战,可能会有很多bug。还有,考核是不是应该和职级和工资挂钩?月薪2万的人和月薪5000的,干同一项任务,是不是应该有不同的要求。领导说:有意见?有意见可以去没意见的地方工作。
这又是一个新的轮回,让过于散漫的老技术部,又开始变得剑拔弩张起来。他们将面临新的技术架构,考核体系,工作方式。至于这一轮能给公司带来什么,或许只有时间才能给出答案。
而这个老技术部门的困境,到底能不能走出来,又该如何走出来?
本故事纯属虚构,如有雷同,实属巧合。
来源:juejin.cn/post/7470751370653499427
离职转AI独立开发半年,我感受到了真正的生活
离职转AI独立开发半年,我感受到了真正的生活
我的新产品:code.promptate.xyz/
开场白:一个不被理解的决定
2022年12月的最后一天,我收拾了自己的小盒子,里面装着我在这家互联网公司工作的所有痕迹:一个定制水杯,几本技术书籍,和一摞写满代码思路的便利贴。HR部门的小姐姐看着我签完最后一份文件,表情有些复杂:"小张,你才来半年就走,真的想好了吗?这个时候辞职,外面行情不好..."
我点点头,没多解释。如何向别人解释我这个2000年出生的"孩子",毕业仅仅半年就对光鲜的互联网工作心生倦意?如何解释我不想再每天凌晨两点被产品经理的消息惊醒,然后爬起来改几行代码?如何解释我想追求的不只是一份体面的工资和一个看起来不错的头衔?
当我走出公司大楼,北京的冬风刮得我脸生疼。我的储蓄只够支撑我半年,而我计划做的事情——成为一名AI独立开发者——在大多数人眼中无异于天方夜谭。"你疯了吧?现在的独立开发者,有几个能养活自己的?"这是我最好朋友听到我计划时的反应。
事实证明,他错了。我也曾经错了。而现在,当我坐在自己选择的咖啡馆,以自己喜欢的节奏工作,看着用户数突破10,000的后台数据,我知道这半年的挣扎、焦虑和不安都是值得的。
职场困境:我在互联网大厂的日子
回想起入职的第一天,一切都充满希望。校招拿到知名互联网公司的offer,年薪30万,比许多同学高出不少。父母骄傲地向亲戚们宣布他们的儿子"找到了好工作"。
然而现实很快给了我当头一棒。
我被分到一个负责内部工具开发的小组。领导在入职第一天就明确告诉我:"小张,我们这个组不是核心业务,资源有限,但任务不少,你得做好加班的准备。"
第一个月,适应期,我每天工作10小时,感觉还能接受。到了第二个月,一个重要项目启动,我开始习惯每天凌晨回家,第二天早上9点又准时出现在公司。最夸张的一次,我连续工作了38个小时,只为了赶一个莫名其妙被提前的deadline。
# 当时的我就像这段无限循环的代码
while True:
wake_up()
go_to_work()
coding_till_midnight()
get_emergency_task()
sleep(2) # 只睡2小时
工作内容也让我倍感挫折。作为一名热爱技术的程序员,我希望能够参与有挑战性的项目,学习前沿技术。但现实是,我大部分时间都在做重复性的维护工作,修复一些简单但繁琐的bug,或者应对产品经理们不断变化的需求。
我感到自己正在成为一个"代码工具人",一个可以被随时替换的齿轮。我的创造力,我对技术的热情,我想为这个世界带来一些改变的梦想,都在日复一日的996中渐渐磨灭。
转折点:AI浪潮中看到的希望
2022年底,ChatGPT横空出世。作为一个技术爱好者,我第一时间注册了账号,体验了这个令人震惊的产品。我记得那天晚上,我熬夜到凌晨三点,不断地与ChatGPT对话,测试它的能力边界。
"这太不可思议了,"我对自己说,"这将改变一切。"
随后几周,我利用所有空闲时间(其实并不多)研究OpenAI的API文档,尝试构建一些简单的应用。我发现,大语言模型(LLM)并不像我想象的那样遥不可及,即使是一个普通开发者,只要理解其工作原理,也能基于它创造出有价值的产品。
同时,我开始关注独立开发者社区。我惊讶地发现,有不少人依靠自己开发的小产品,实现了不错的收入。虽然他们中的大多数人都经历了长期的积累,但AI技术的爆发似乎提供了一个弯道超车的机会。
这个想法越来越强烈,直到有一天晚上,当我又一次被加到一个紧急项目里,领导发来消息:"小张,这个需求很紧急,今晚能上线吗?"
我望着窗外的夜色,突然感到一阵前所未有的清晰。
我回复道:"可以,这是我在公司的最后一个项目了。"
第二天,我提交了辞职申请。
技术探索:从零开始的AI学习之路
辞职后的第一个月,我给自己制定了严格的学习计划。每天早上6点起床,先锻炼一小时,然后开始我的"AI课程"。
首先,我需要理解大语言模型的基本原理。虽然我有编程基础,但NLP和深度学习对我来说仍是比较陌生的领域。我从《Attention is All You Need》这篇奠定Transformer架构的论文开始,通过各种在线资源,逐步理解了当代大语言模型的工作机制。
# 简化的Transformer注意力机制示例
def scaled_dot_product_attention(query, key, value, mask=):
# 计算注意力权重
matmul_qk = tf.matmul(query, key, transpose_b=True)
# 缩放
depth = tf.cast(tf.shape(key)[-1], tf.float32)
logits = matmul_qk / tf.math.sqrt(depth)
# 添加掩码(可选)
if mask is not :
logits += (mask * -1e9)
# softmax归一化
attention_weights = tf.nn.softmax(logits, axis=-1)
# 应用注意力权重
output = tf.matmul(attention_weights, value)
return output, attention_weights
然后,我需要掌握如何有效地利用OpenAI、Anthropic等公司提供的API。这包括了解Prompt Engineering的技巧,学会如何构建有效的提示词,以及如何处理模型输出的后处理工作。
我还深入研究了向量数据库、检索增强生成(RAG)等技术,这些对于构建基于知识的AI应用至关重要。
这个余弦相似度公式成为了我日常工作的一部分,用于计算文本嵌入向量之间的相似性。
同时,我不断实践、不断失败、不断调整。我记得有一周,我几乎每天睡眠不足5小时,只为解决一个模型幻觉问题。但与公司工作不同的是,这种忙碌源于我的热情和对问题的好奇,而非外部压力。
产品孵化:从创意到实现
学习的同时,我开始思考自己的产品定位。在观察市场和分析自身技能后,我决定开发一款面向内容创作者的AI助手,我将其命名为"创作魔法师"。
这个产品的核心功能是帮助博主、自媒体人和营销人员高效创作内容。与市面上的通用AI不同,它专注于内容创作流程:从选题分析、结构规划、初稿生成到细节优化和SEO改进,提供全流程支持。
产品开发过程中,我遇到了许多挑战:
- 技术架构选择:作为独立开发者,资金有限,我需要在功能与成本间找平衡。最终我选择了Next.js + TailwindCSS搭建前端,Node.js构建后端,MongoDB存储数据,Pinecone作为向量数据库存储文档嵌入向量。
- 模型优化:为了降低API调用成本,我设计了一套智能路由系统,根据任务复杂度自动选择不同的模型,简单任务用更经济的模型,复杂任务才调用高端模型。
- 用户体验设计:没有设计团队,我自学了基础UI/UX知识,参考优秀产品,反复调整界面直到满意。
- 运营与推广:这对我这个技术人来说是最大挑战。我学会了编写有吸引力的产品描述,设计落地页,甚至尝试了简单的SEO优化。
最艰难的时刻是产品上线后的第一个月。用户增长缓慢,每天只有个位数的新注册。我开始怀疑自己的决定,甚至一度考虑放弃,重新找工作。
转机:从10个用户到10,000用户
转机出现在上线后的第二个月。一位拥有20万粉丝的自媒体创作者使用了我的产品,对效果非常满意,在他的平台上分享了使用体验。这篇分享在创作者圈内引起了不小的反响。
24小时内,我的注册用户从原来的不到200人猛增至1500多人。服务器一度崩溃,我熬夜进行紧急扩容和优化。这次意外的曝光让我意识到,产品定位是正确的,市场需求确实存在。
接下来,我调整了运营策略:
- 主动联系内容创作者,提供免费试用,换取真实反馈和可能的推荐。
- 根据用户反馈快速迭代产品功能,每周至少发布一次更新。
- 建立用户社区,鼓励用户分享使用技巧,相互帮助。
- 编写详细的使用教程和最佳实践指南,降低用户上手难度。
// 用户增长追踪系统的一部分
function trackUserGrowth() {
const date = new Date().toISOString().split('T')[0];
db.collection('metrics').updateOne(
{ date: date },
{
$inc: {
newUsers: 1,
totalImpressions: userSource.impressions || 0
},
$set: {
lastUpdated: new Date()
}
},
{ upsert: true }
);
}
三个月后,用户数突破5,000;半年后,达到10,000。更令人欣慰的是,付费转化率远超我的预期,达到了8%左右,而行业平均水平通常在2-3%。
我分析了成功原因:
- 产品聚焦特定痛点:不追求通用性,而是深入解决内容创作者的具体问题。
- 及时响应用户需求:独立开发的优势是决策链短,能快速调整方向。
- 社区效应:用户之间的口碑传播形成了良性循环。
- 个性化服务:我经常亲自回复用户问题,提供定制化建议,这在大公司很难做到。
财务自由:从赤字到收支平衡
谈到收入模式,我采用了"免费+订阅"的策略:
- 基础功能完全免费,足以满足普通用户的需求
- 高级功能(如批量处理、高级模板、深度分析等)需要订阅
- 提供月度计划(49元)和年度计划(398元,约33元/月)
最初几个月,收入微乎其微。我记得第一个月的收入仅有287元,而我在公司的月薪是25,000元。差距之大,让我一度怀疑自己的决定。
但随着用户增长,情况逐渐改善。第三个月收入突破5,000元,第四个月达到12,000元,第六个月——也就是我离职半年后,月收入达到了23,500元,基本与我原来的工资持平。
考虑到我现在的生活成本降低了(不需要租住在北京市中心的高价公寓,不需要每天通勤),实际上我的生活质量反而提高了。
更重要的是,这些收入是真正属于我的,不依赖于任何公司的评价和KPI。我建立了自己的"被动收入引擎",它可以在我睡觉时继续为我工作。
生活平衡:找回被工作吞噬的自我
收入只是故事的一部分。对我来说,最大的变化是生活方式的改变。
在互联网公司工作时,我的生活可以用一句话概括:工作即生活。我几乎没有个人时间,健康状况逐渐恶化,社交圈萎缩到只剩同事,爱好被束之高阁。
成为独立开发者后,我重新掌控了自己的时间:
- 合理作息:我不再熬夜加班,保持每天7-8小时高质量睡眠。
- 定期锻炼:每天至少运动一小时,半年下来体重减轻10kg,体脂率降低5%。
- 地点自由:我可以在家工作,也可以去咖啡馆,甚至尝试了几次"工作旅行",边旅游边维护产品。
- 深度学习:不再为了应付工作而学习,而是追随个人兴趣深入研究技术。
- 重拾爱好:我重新开始弹吉他,参加了当地的音乐小组,结识了一群志同道合的朋友。
这种生活方式让我找回了工作的意义——工作是为了更好的生活,而不是生活为了工作。我的创造力和工作热情反而因此提升,产品迭代速度和质量都超出了预期。
技术反思:AI时代的个人定位
在这半年的独立开发经历中,我对AI技术和个人发展有了更深的思考。
首先,大模型时代确实改变了软件开发的范式。传统开发模式是"写代码解决问题",而现在更多的是"设计提示词引导AI解决问题"。这不意味着编程技能不重要,而是编程与AI引导能力的结合变得越来越重要。
# 传统开发方式
def analyze_sentiment(text):
# 复杂的NLP算法实现
words = tokenize(text)
scores = calculate_sentiment_scores(words)
return determine_overall_sentiment(scores)
# AI时代的开发方式
def analyze_sentiment_with_llm(text):
prompt = f"""
分析以下文本的情感倾向,返回'正面'、'负面'或'中性'。
只返回分类结果,不要解释。
文本: {text}
"""
result = llm_client.generate(prompt, max_tokens=10)
return result.strip()
其次,我认识到技术民主化的力量。曾经需要一个团队才能完成的项目,现在一个人借助AI工具也能完成。这为独立开发者创造了前所未有的机会,但也意味着差异化和创新变得更加重要。
最后,我发现真正的核心竞争力不在于熟悉某项技术,而在于解决问题的思维方式和对用户需求的理解。技术工具会不断更新迭代,但洞察问题和设计解决方案的能力将长期有效。
写给迷茫的年轻人
回顾这半年的经历,我想对那些和当初的我一样迷茫的年轻人说几句话:
- 公司经历有价值,但不是唯一路径:在大公司工作能积累经验和人脉,但不要把它视为唯一选择。如果环境压抑了你的创造力和热情,寻找改变是勇敢而非逃避。
- 技术浪潮创造机会窗口:AI等新技术正在重构行业,为个人提供了"弯道超车"的机会。保持开放心态,持续学习,你会发现比想象中更多的可能性。
- 找到可持续的节奏:成功不在于短期的爆发,而在于长期的坚持。设计一种既能推动目标实现又不会消耗自己的工作方式,才能走得更远。
- 用户价值胜过技术炫耀:最成功的产品往往不是技术最先进的,而是最能解决用户痛点的。专注于创造真正的价值,而不仅仅是展示技术能力。
- 享受过程,而非仅追求结果:如果你只关注最终目标而忽视日常体验,即使达到目标也可能感到空虚。真正的成功包含了对过程的享受和个人成长。
未来展望:持续进化的旅程
现在,我站在新的起点上。"创作魔法师"只是我旅程的第一步,我已经开始规划下一个产品,瞄准了另一个我认为有潜力的细分市场。
与此同时,我也在考虑如何扩大团队规模。虽然独立开发有其魅力,但有些想法需要更多元的技能组合才能实现。我计划在未来半年内招募1-2名志同道合的伙伴,组建一个小而精的团队。
技术上,我将继续深入研究大模型的微调和部署技术。随着开源模型的进步,在特定领域微调自己的模型变得越来越可行,这将是我产品的下一个竞争优势。
生活方面,我正计划一次为期两个月的"数字游牧"之旅,边旅行边工作,探索更多可能的生活方式。
路上会有挑战,也会有挫折,但我不再惧怕。因为我知道,真正的自由不在于没有困难,而在于面对困难时仍能按自己的意愿选择前进的方向。
当我在咖啡馆工作到黄昏,看着窗外的夕阳,我常常感到一种难以言喻的满足感。这种感觉告诉我,我正在正确的道路上——一条通往真正生活的道路。
如果你也在考虑类似的选择,希望我的故事能给你一些启发。记住,每个人的路都不同,重要的是找到属于自己的节奏和方向。
在这个AI加速发展的时代,机会前所未有,但终究,技术只是工具,生活才是目的。
来源:juejin.cn/post/7486788421932400652
从0到1开发DeepSeek天气助手智能体——你以为大模型只会聊天?Function Calling让它“上天入地”
前言
2025年伊始,科技界的风云人物们——从英伟达的黄仁勋到OpenAI的山姆·奥特曼,再到机器学习领域的泰斗吴恩达不约而同地将目光聚焦于一个关键词:AI Agent(即智能体,若想深入了解,可阅读我的文章《一文读懂2025核心概念 AI Agent:科技巨头都在布局的未来赛道》)。然而,对于AI Agent的前景,持怀疑态度的人可能会问:“大模型只是个能完成问答的概率模型,它哪来的行为能力?又怎能摇身一变成为AI Agent呢?” 这个问题的答案,正隐藏在我们今天要探讨的 Function Calling(函数调用) 技术之中!
2025年伊始,科技界的风云人物们——从英伟达的黄仁勋到OpenAI的山姆·奥特曼,再到机器学习领域的泰斗吴恩达不约而同地将目光聚焦于一个关键词:AI Agent(即智能体,若想深入了解,可阅读我的文章《一文读懂2025核心概念 AI Agent:科技巨头都在布局的未来赛道》)。然而,对于AI Agent的前景,持怀疑态度的人可能会问:“大模型只是个能完成问答的概率模型,它哪来的行为能力?又怎能摇身一变成为AI Agent呢?” 这个问题的答案,正隐藏在我们今天要探讨的 Function Calling(函数调用) 技术之中!
一、 什么是大模型的 Function Calling 技术?
Function Calling 是一种让大语言模型能够调用外部函数或工具的技术。简单来说,就是让大模型不仅能理解和生成文本,还能根据用户的需求,调用特定的 API 或工具来完成更复杂的任务。
举个例子:
- 用户:“帮我订一张明天从北京到上海的机票。”
- 不具备Function Calling的大模型:回复“好的,我会帮您订票。”,但无法真正执行。
- 具备 Function Calling 的大模型:可以调用机票预订 API,获取航班信息,并完成订票操作。
Function Calling 是一种让大语言模型能够调用外部函数或工具的技术。简单来说,就是让大模型不仅能理解和生成文本,还能根据用户的需求,调用特定的 API 或工具来完成更复杂的任务。
举个例子:
- 用户:“帮我订一张明天从北京到上海的机票。”
- 不具备Function Calling的大模型:回复“好的,我会帮您订票。”,但无法真正执行。
- 具备 Function Calling 的大模型:可以调用机票预订 API,获取航班信息,并完成订票操作。
二、 Function Calling 和 AI Agent 的关系
AI Agent 是指能够自主感知环境、进行决策和执行动作的智能体。Function Calling 是构建强大 AI Agent 的关键技术之一,它为 AI Agent 提供了以下能力:
- 连接现实世界: 通过调用外部 API,AI Agent 可以获取实时信息、操作外部系统,从而与现实世界进行交互。
- 执行复杂任务: 通过组合调用不同的函数,AI Agent 可以完成更复杂、更个性化的任务,例如旅行规划、日程安排等。
- 提升效率和准确性: 利用外部工具的强大功能,AI Agent 可以更高效、更准确地完成任务,例如数据分析、代码生成等。
从上述分析中可知要开发智能体,必须用到大模型的Function Calling技术。要让大模型调用Function Calling功能,必须提供大模型相应功能的函数。
为了更直观感受大模型Function Calling技术,我们将利用DeepSeek大模型从0到1开发天气助手智能体,可以实时查询天气状态并给我们提供穿衣建议等~
AI Agent 是指能够自主感知环境、进行决策和执行动作的智能体。Function Calling 是构建强大 AI Agent 的关键技术之一,它为 AI Agent 提供了以下能力:
- 连接现实世界: 通过调用外部 API,AI Agent 可以获取实时信息、操作外部系统,从而与现实世界进行交互。
- 执行复杂任务: 通过组合调用不同的函数,AI Agent 可以完成更复杂、更个性化的任务,例如旅行规划、日程安排等。
- 提升效率和准确性: 利用外部工具的强大功能,AI Agent 可以更高效、更准确地完成任务,例如数据分析、代码生成等。
从上述分析中可知要开发智能体,必须用到大模型的Function Calling技术。要让大模型调用Function Calling功能,必须提供大模型相应功能的函数。
为了更直观感受大模型Function Calling技术,我们将利用DeepSeek大模型从0到1开发天气助手智能体,可以实时查询天气状态并给我们提供穿衣建议等~
三、心知天气 + Python + DeepSeek开发天气预报智能体
3.1 心知天气注册及API key获取方法
- 申请免费版的API,点击左侧免费版,就可以看到API私钥了:
- 利用python requests库调用API获得天气情况(免费版的只能得到天气现象、天气现象代码和气温 3项数据)
请提前安装requests sdk: pip install requests
import requests
url = "https://api.seniverse.com/v3/weather/now.json"
params = {
"key": "", # 填写你的私钥
"location": "北京", # 你要查询的地区可以用代号,拼音或者汉字,文档在官方下载,这里举例北京
"language": "zh-Hans", # 中文简体
"unit": "c", # 获取气温
}
response = requests.get(url, params=params) # 发送get请求
temperature = response.json() # 接受消息中的json部分
print(temperature['results'][0]['now']) # 输出接收到的消息进行查看
- 将请求天气的代码封装成可以指定查询地点的函数:
import requests
def get_weather(loc):
url = "https://api.seniverse.com/v3/weather/now.json"
params = {
"key": "", #填写你的私钥
"location": loc,
"language": "zh-Hans",
"unit": "c",
}
response = requests.get(url, params=params)
temperature = response.json()
return temperature['results'][0]['now']
3.2 DeepSeek API Key注册方法
Function Calling 适用于模型规模大于30B的模型,本次分享我们使用DeepSeek-V3模型。按如下方法注册获得DeepSeek-V3 API Key(Deep-V3 API 访问教程请看文章DeepSeek大模型API实战指南):
- 进入DeepSeek官网,点击API 开放平台:
- 注册并充值tokens后(deepseek的tokens还是相当便宜的,10元可以用好久),点击左边栏API Keys生成API Key:
- 利用python openai库访问deepseek (这里openai库定义的是请求数据格式,并不是说deepseek是基于openai构造的`)
# 请提前安装openai sdk: pip install openai
from openai import OpenAI
client = OpenAI(api_key="你创建的api key", base_url="https://api.deepseek.com")
response = client.chat.completions.create(
model="deepseek-chat", # 指定deepseek-chat, deepseek-chat对应deepseek-v3, deepseek-reasoner对应deepseek-r1
messages=[
{"role": "system", "content": "You are a helpful assistant"}, #指定系统背景
{"role": "user", "content": "Hello"}, #指定用户提问
],
stream=False
)
print(response.choices[0].message.content)
3.3 Function Calling准备: 让大模型理解函数
准备好外部函数之后,非常重要的一步是将外部函数的信息以某种形式传输给大模型,让大模型理解函数的作用。大模型需要特定的字典格式对函数进行完整描述, 字典描述包括:
- name:函数名称字符串
- description: 描述函数功能的字符串,大模型选择函数的核心依据
- parameters: 函数参数, 要求遵照JSON Schema格式输入,JSON Schema格式请参照JSON Schema格式详解
对于上面的get_weather函数, 我们创建如下字典对其完整描述:
get_weather_function = {
'name': 'get_weather',
'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
'parameters': {
'type': 'object',
'properties': { #参数说明
'loc': {
'description': '城市名称',
'type': 'string'
}
},
'required': ['loc'] #必备参数
}
}
完成对get_weather函数描述后,还需要将其加入tools列表,用于告知大模型可以使用哪些函数以及这些函数对应的描述,并在可用函数对象中记录一下:
tools = [
{
"type": "function",
"function":get_weather_function
}
]
available_functions = {
"get_weather": get_weather,
}
3.4 Function calling 功能实现
完成一系列基础准备工作之后,接下来尝试与DeepSeek-V3大模型对话调用Function calling
功能(分步教程代码在 codecopy.cn/post/ir801w ,完整优化代码在codecopy.cn/post/c80rrk ):
- 实例化客户端并创建如下
messages
# 实例化客户端
client = OpenAI(api_key=你的api_key,
base_url="https://api.deepseek.com")
messages=[
{"role": "user", "content": "请帮我查询北京地区今日天气情况"}
]
- 测试一下如果只输入问题不输入外部函数,模型是不知道天气结果的,只会告诉我们如何获得实时天气
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages
)
print(response.choices[0].message.content)
- 接下来尝试将函数相关信息输入给Chat模型,需要额外设置两个参数,首先是
tools
参数, 用于申明外部函数库, 也就是我们上面定义的tools
列表对象。其次是可选参数tool_choice
参数,该参数用于控制模型对函数的选取,默认值为auto
, 表示会根据用户提问自动选择要执行函数,若想让模型在本次执行特定函数不要自行挑选,需要给tool_choice
参数赋予{"name":"functionname"}
值,这时大模型就会从tools
列表中选取函数名为functionname
的函数执行。这里我们考验一下模型的智能性,让模型自动挑选函数来执行:
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "user", "content": "请帮我查询北京地区今日天气情况"}
],
tools=tools,
)
print(response.choices[0].message)
观察现在response
返回的结果, 我们发现message
中的content
变为空字符串, 增加了一个tool_calls
的list, 如图红框所示,该list就包含了当前调用外部函数的全部信息:
我们输出一下toll_calls
列表项中的function
内容,可以看到大模型自动帮我们选择了要执行的函数get_weather
,并告诉我们要传递的参数{loc:北京}
。,
response_message = response.choices[0].message
print(response_message.tool_calls[0].function)
- 下一步将大模型生成的函数参数输入大模型选择的函数并执行(注意大模型不会帮我们自动调用函数,它只会帮我们选择要调用的函数以及生成函数参数),通过上面定义的
available_functions
对象找到具体的函数,并将大模型返回的参数传入(这里 ** 是一种便捷的参数传递方法,该方法会将字典中的每个key对应的value传输到同名参数位中),可以看到天气函数成功执行:
# 获取函数名称
function_name = response_message.tool_calls[0].function.name
# 获得对应函数对象
function_to_call = available_functions[function_name]
# 获得执行函数所需参数
function_args = json.loads(response_message.tool_calls[0].function.arguments)
# 执行函数
function_response = function_to_call(**function_args)
print(function_response)
- 在调用天气函数得到天气情况后,将天气结果传入
mesages
列表中并发送给大模型,让大模型理解上下文。函数执行结果的message
是tool_message
类型(这部分有点绕,可以看整体对于message
类型有疑问的请看我的文章DeepSeek大模型API实战指南, 里面有详细的参数指南)。
首先将大模型关于选择函数的回复response_message
内容解析后传入messages
列表中
print(response_message.model_dump())
messages.append(response_message.model_dump())
解析结果如下:
{
'content': '',
'refusal': ,
'role': 'assistant',
'annotations': ,
'audio': ,
'function_call': ,
'tool_calls': [{
'id': 'call_0_8feaa367-c274-4c84-830f-13b49358a231',
'function': {
'arguments': '{"loc":"北京"}',
'name': 'get_weather'
},
'type': 'function',
'index': 0
}]
}
然后再将函数执行结果作为tool_message
并与response_message
关联后传入messages
列表中:
messages.append({
"role": "tool",
"content": json.dumps(function_response), # 将回复的字典转化为json字符串
"tool_call_id": response_message.tool_calls[0].id # 将函数执行结果作为tool_message添加到messages中, 并关联返回执行函数内容的id
})
- 接下来,再次调用Chat模型来围绕
messages
进行回答。需要注意的是,此时不再需要向模型重复提问,只需要简单的将我们已经准备好的messages
传入Chat模型即可:
second_response = client.chat.completions.create(
model="deepseek-chat",
messages=messages)
print(second_response.choices[0].message.content)
下面看大模型的输出结果,很明显大模型接收到了函数执行的结果,并进一步处理得到输出,同时天气和气温的输出也是正确的,这样我们就基于function calling技术完成一个简单的智能体了!
3.5 代码优化
以上步骤详细描述了Fucntion Calling的技术细节,执行流程图如下:
开发一个智能体需要将上面流程串起来,下一步我们编写一个能够自动执行外部函数调用的Chat智能体函数, 参数messages
为输入到Chat模型的messages
参数对象, 参数api_key
为调用模型的API-KEY ,参数tools
设置为包含全部外部函数的列表对象, 参数model
默认为deepseek-chat , 该函数返回结果为大模型根据function calling内容的回复, 函数的具体代码如下:
def run_conv(messages,
api_key,
tools=,
functions_list=,
model="deepseek-chat"):
user_messages = messages
client = OpenAI(api_key=api_key,
base_url="https://api.deepseek.com")
# 如果没有外部函数库,则执行普通的对话任务
if tools == :
response = client.chat.completions.create(
model=model,
messages=user_messages
)
final_response = response.choices[0].message.content
# 若存在外部函数库,则需要灵活选取外部函数并进行回答
else:
# 创建外部函数库字典
available_functions = {func.__name__: func for func in functions_list}
# 创建包含用户问题的message
messages = user_messages
# first response
response = client.chat.completions.create(
model=model,
messages=user_messages,
tools=tools,
)
response_message = response.choices[0].message
# 获取函数名
function_name = response_message.tool_calls[0].function.name
# 获取函数对象
fuction_to_call = available_functions[function_name]
# 获取函数参数
function_args = json.loads(response_message.tool_calls[0].function.arguments)
# 将函数参数输入到函数中,获取函数计算结果
function_response = fuction_to_call(**function_args)
# messages中拼接first response消息
user_messages.append(response_message.model_dump())
# messages中拼接外部函数输出结果
user_messages.append(
{
"role": "tool",
"content": json.dumps(function_response),
"tool_call_id": response_message.tool_calls[0].id
}
)
# 第二次调用模型
second_response = client.chat.completions.create(
model=model,
messages=user_messages)
# 获取最终结果
final_response = second_response.choices[0].message.content
return final_response
以上函数的流程就十分清晰啦,调用该函数测试一下结果~
ds_api_key = '你的api key'
messages = [{"role": "user", "content": "请问上海今天天气如何?"}]
get_weather_function = {
'name': 'get_weather',
'description': '查询即时天气函数,根据输入的城市名称,查询对应城市的实时天气',
'parameters': {
'type': 'object',
'properties': { # 参数说明
'loc': {
'description': '城市名称',
'type': 'string'
}
},
'required': ['loc'] # 必备参数
}
}
tools = [
{
"type": "function",
"function": get_weather_function
}
]
final_response = run_conv(messages=messages,
api_key=ds_api_key,
tools=tools,
functions_list=[get_weather])
print(final_response)
四、总结与展望
本文我们详细讲解了大模型 function calling
技术并基于该技术开发了天气智能体。Function Calling
技术是AI Agent实现的关键,它让大模型不再只是简单的聊天回复,更可以"上天入地”完成各种各样的事。
然而在开发过程中我们也发现,function calling
技术开发过程冗长,需要编写相应的能力函数,有没有什么办法可以做到函数复用或简化开发呢,这就需要用到2025年最流行的Agent开发技术——MCP协议,什么是MCP协议呢?我们下一篇文章给大家分享~
感兴趣大家可关注微信公众号:大模型真好玩,工作开发中的大模型经验、教程和工具免费分享,大家快来看看吧~
来源:juejin.cn/post/7486323379474645027
JDK 24 发布,新特性解读!
真快啊!Java 24 这两天已经正式发布啦!这是自 Java 21 以来的第三个非长期支持版本,和 Java 22、Java 23一样。
下一个长期支持版是 Java 25,预计今年 9 月份发布。
Java 24 带来的新特性还是蛮多的,一共 24 个。Java 23 和 Java 23 都只有 12 个,Java 24的新特性相当于这两次的总和了。因此,这个版本还是非常有必要了解一下的。
下图是从 JDK8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
我在昨天晚上详细看了一下 Java 24 的详细更新,并对其中比较重要的新特性做了详细的解读,希望对你有帮助!
本文内容概览:
JEP 478: 密钥派生函数 API(预览)
密钥派生函数 API 是一种用于从初始密钥和其他数据派生额外密钥的加密算法。它的核心作用是为不同的加密目的(如加密、认证等)生成多个不同的密钥,避免密钥重复使用带来的安全隐患。 这在现代加密中是一个重要的里程碑,为后续新兴的量子计算环境打下了基础
通过该 API,开发者可以使用最新的密钥派生算法(如 HKDF 和未来的 Argon2):
// 创建一个 KDF 对象,使用 HKDF-SHA256 算法
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// 创建 Extract 和 Expand 参数规范
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial) // 设置初始密钥材料
.addSalt(salt) // 设置盐值
.thenExpand(info, 32); // 设置扩展信息和目标长度
// 派生一个 32 字节的 AES 密钥
SecretKey key = hkdf.deriveKey("AES", params);
// 可以使用相同的 KDF 对象进行其他密钥派生操作
JEP 483: 提前类加载和链接
在传统 JVM 中,应用在每次启动时需要动态加载和链接类。这种机制对启动时间敏感的应用(如微服务或无服务器函数)带来了显著的性能瓶颈。该特性通过缓存已加载和链接的类,显著减少了重复工作的开销,显著减少 Java 应用程序的启动时间。测试表明,对大型应用(如基于 Spring 的服务器应用),启动时间可减少 40% 以上。
这个优化是零侵入性的,对应用程序、库或框架的代码无需任何更改,启动也方式保持一致,仅需添加相关 JVM 参数(如 -XX:+ClassDataSharing
)。
JEP 484: 类文件 API
类文件 API 在 JDK 22 进行了第一次预览(JEP 457),在 JDK 23 进行了第二次预览并进一步完善(JEP 466)。最终,该特性在 JDK 24 中顺利转正。
类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。
// 创建一个 ClassFile 对象,这是操作类文件的入口。
ClassFile cf = ClassFile.of();
// 解析字节数组为 ClassModel
ClassModel classModel = cf.parse(bytes);
// 构建新的类文件,移除以 "debug" 开头的所有方法
byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
classBuilder -> {
// 遍历所有类元素
for (ClassElement ce : classModel) {
// 判断是否为方法 且 方法名以 "debug" 开头
if (!(ce instanceof MethodModel mm
&& mm.methodName().stringValue().startsWith("debug"))) {
// 添加到新的类文件中
classBuilder.with(ce);
}
}
});
JEP 485: 流收集器
流收集器 Stream::gather(Gatherer)
是一个强大的新特性,它允许开发者定义自定义的中间操作,从而实现更复杂、更灵活的数据转换。Gatherer
接口是该特性的核心,它定义了如何从流中收集元素,维护中间状态,并在处理过程中生成结果。
与现有的 filter
、map
或 distinct
等内置操作不同,Stream::gather
使得开发者能够实现那些难以用标准 Stream 操作完成的任务。例如,可以使用 Stream::gather
实现滑动窗口、自定义规则的去重、或者更复杂的状态转换和聚合。 这种灵活性极大地扩展了 Stream API 的应用范围,使开发者能够应对更复杂的数据处理场景。
基于 Stream::gather(Gatherer)
实现字符串长度的去重逻辑:
var result = Stream.of("foo", "bar", "baz", "quux")
.gather(Gatherer.ofSequential(
HashSet::new, // 初始化状态为 HashSet,用于保存已经遇到过的字符串长度
(set, str, downstream) -> {
if (set.add(str.length())) {
return downstream.push(str);
}
return true; // 继续处理流
}
))
.toList();// 转换为列表
// 输出结果 ==> [foo, quux]
JEP 486: 永久禁用安全管理器
JDK 24 不再允许启用 Security Manager
,即使通过 java -Djava.security.manager
命令也无法启用,这是逐步移除该功能的关键一步。虽然 Security Manager
曾经是 Java 中限制代码权限(如访问文件系统或网络、读取或写入敏感文件、执行系统命令)的重要工具,但由于复杂性高、使用率低且维护成本大,Java 社区决定最终移除它。
JEP 487: 作用域值 (第四次预览)
作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
final static ScopedValue<...> V = new ScopedValue<>();
// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });
// In a method called directly or indirectly from the lambda expression
... V.get() ...
作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。
JEP 491: 虚拟线程的同步而不固定平台线程
优化了虚拟线程与 synchronized
的工作机制。 虚拟线程在 synchronized
方法和代码块中阻塞时,通常能够释放其占用的操作系统线程(平台线程),避免了对平台线程的长时间占用,从而提升应用程序的并发能力。 这种机制避免了“固定 (Pinning)”——即虚拟线程长时间占用平台线程,阻止其服务于其他虚拟线程的情况。
现有的使用 synchronized
的 Java 代码无需修改即可受益于虚拟线程的扩展能力。 例如,一个 I/O 密集型的应用程序,如果使用传统的平台线程,可能会因为线程阻塞而导致并发能力下降。 而使用虚拟线程,即使在 synchronized
块中发生阻塞,也不会固定平台线程,从而允许平台线程继续服务于其他虚拟线程,提高整体的并发性能。
JEP 493:在没有 JMOD 文件的情况下链接运行时镜像
默认情况下,JDK 同时包含运行时镜像(运行时所需的模块)和 JMOD 文件。这个特性使得 jlink 工具无需使用 JDK 的 JMOD 文件就可以创建自定义运行时镜像,减少了 JDK 的安装体积(约 25%)。
说明:
- Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE。
- JMOD 文件是 Java 模块的描述文件,包含了模块的元数据和资源。
JEP 495: 简化的源文件和实例主方法(第四次预览)
这个特性主要简化了 main
方法的的声明。对于 Java 初学者来说,这个 main
方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
没有使用该特性之前定义一个 main
方法:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
使用该新特性之后定义一个 main
方法:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
进一步简化(未命名的类允许我们省略类名)
void main() {
System.out.println("Hello, World!");
}
JEP 497: 量子抗性数字签名算法 (ML-DSA)
JDK 24 引入了支持实施抗量子的基于模块晶格的数字签名算法 (Module-Lattice-Based Digital Signature Algorithm, ML-DSA),为抵御未来量子计算机可能带来的威胁做准备。
ML-DSA 是美国国家标准与技术研究院(NIST)在 FIPS 204 中标准化的量子抗性算法,用于数字签名和身份验证。
JEP 498: 使用 sun.misc.Unsafe
内存访问方法时发出警告
JDK 23(JEP 471) 提议弃用 sun.misc.Unsafe
中的内存访问方法,这些方法将来的版本中会被移除。在 JDK 24 中,当首次调用 sun.misc.Unsafe
的任何内存访问方法时,运行时会发出警告。
这些不安全的方法已有安全高效的替代方案:
java.lang.invoke.VarHandle
:JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。java.lang.foreign.MemorySegment
:JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与VarHandle
协同工作。
这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。
import jdk.incubator.foreign.*;
import java.lang.invoke.VarHandle;
// 管理堆外整数数组的类
class OffHeapIntBuffer {
// 用于访问整数元素的VarHandle
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
// 内存管理器
private final Arena arena;
// 堆外内存段
private final MemorySegment buffer;
// 构造函数,分配指定数量的整数空间
public OffHeapIntBuffer(long size) {
this.arena = Arena.ofShared();
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
}
// 释放内存
public void deallocate() {
arena.close();
}
// 以volatile方式设置指定索引的值
public void setVolatile(long index, int value) {
ELEM_VH.setVolatile(buffer, 0L, index, value);
}
// 初始化指定范围的元素为0
public void initialize(long start, long n) {
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.fill((byte) 0);
}
// 将指定范围的元素复制到新数组
public int[] copyToNewArray(long start, int n) {
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
ValueLayout.JAVA_INT.byteSize() * n)
.toArray(ValueLayout.JAVA_INT);
}
}
JEP 499: 结构化并发(第四次预览)
JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent
,目前处于孵化器阶段。
结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
结构化并发的基本 API 是StructuredTaskScope
,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
StructuredTaskScope
的基本用法如下:
try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close
结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
Java 新特性系列解读
如果你想系统了解 Java 8 以及之后版本的新特性,可以在 JavaGuide 上阅读对应的文章:
比较推荐这几篇:
来源:juejin.cn/post/7483478667143626762
无虚拟DOM到底能快多少?
相信大家都跟我一样有这样一个疑问:之前都说虚拟DOM
是为了性能,怎么现在又反向宣传了?无虚拟DOM
怎么又逆流而上成为了性能的标杆了呢?
下篇文章我们会仔细分析无虚拟DOM
与虚拟DOM
之间的区别,本篇文章我们先看数据。首先是体积,由于没有了虚拟DOM
以及vDOM diff
算法,所以体积肯定能小不少。当然不是说无虚拟DOM
就彻底不需要diff
算法了,我看过同为无虚拟DOM
框架的Svelte
和Solid
源码,无虚拟DOM
只是不需要vDOM
间的Diff
算法,列表之间还是需要diff
的。毕竟再怎么编译,你从后端获取到的数组,编译器也不可能预测得到。
那么官方给出的数据是:
虽然没有想象中的那么多,但33.6%
也算是小不少了。当然这个数据指的是纯Vapor
模式,如果你把虚拟DOM
和Vapor
混着用的话,体积不仅不会减小反而还会增加。毕竟会同时加载Vapor
模式的runtime
和虚拟DOM
模式的runtime
,二者一相加就大了。
Vapor
模式指的就是无虚拟DOM
模式,如果你不太清楚二者之间有何关联的话,可以看一眼这篇:《无虚拟DOM版Vue为什么叫Vapor》
那性能呢?很多人对体积其实并不敏感,觉得多10K
少10k
都无所谓,毕竟现在都是5G
时代了。所以我们就来看一眼官方公布的性能数据:
从左到右依次为:
- 原生
JS
:1.01 Solid
:1.09Svelte
:1.11- 无虚拟
DOM
版Vue
:1.24 - 虚拟
DOM
版Vue
:1.32 React
:1.55
数字越小代表性能越高,但无论再怎么高都不可能高的过手动优化过的原生JS
,毕竟无论什么框架最终打包编译出来的还是JS
。不过框架的性能其实已经挺接近原生的了,尤其是以无虚拟DOM
著称的Solid
和Svelte
。但无虚拟DOM
版Vue
和虚拟DOM
版Vue
之间并没有拉开什么很大的差距,1.24
和1.32
这两个数字证明了其实二者的性能差距并不明显,这又是怎么一回事呢?
一个原因是Vue3
本来就做了许多编译优化,包括但不限于静态提升、大段静态片段将采用innerHTML
渲染、编译时打标记以帮助虚拟DOM
走捷径等… 由于本来的性能就已经不错了,所以可提升的空间自然也没多少。
看完上述原因肯定有人会说:可提升的空间没多少可以理解,那为什么还比同为无虚拟DOM
的Solid
和Svelte
差那么多?如果Vapor
和Solid
性能差不多的话,那可提升空间小倒是说得过去。但要是像现在这样差的还挺多的话,那这个理由是不是就有点站不住脚了?之所以会出现这样的情况是因为Vue Vapor
的作者在性能优化和快速实现之间选择了后者,毕竟尤雨溪第一次公布要开始做无虚拟DOM
版Vue
的时间是在2020
年:
而如今已经是2025
年了。也就是说如果一个大一新生第一次满心欢喜的听到无虚拟DOM
版Vue
的消息后,那么他现在大概率已经开始毕业工作了都没能等到无虚拟DOM
版Vue
的发布。这个时间拖的有点太长了,甚至从Vue2
到Vue3
都没用这么久。
可以看到Vue3
从立项到发布也就不到两年的时间,而Vapor
呢?从立项到现在已经将近5
年的光阴了,已经比Vue3
所花费的时间多出一倍还多了。所以现在的首要目标是先实现出来一版能用的,后面再慢慢进行优化。我们工作不也是这样么?先优先把功能做出来,优化的事以后再说。那么具体该怎么优化呢:
现在的很多渲染逻辑继承自原版Vue3
,但无虚拟DOM
版Vue
可以采用更优的渲染逻辑。而且现在的Vapor
是跟着原版Vue
的测试集来做的,这也是为了实现和原版Vue
一样的行为。但这可能并不是最优解,之后会慢慢去掉一些不必要的功能,尤其是对性能产生较大影响的功能。
往期精彩文章:
来源:juejin.cn/post/7480069116461088822
leaflet+天地图+更换地图主题
先弄清楚leaflet和天地图充当的角色
- leaflet是用来在绘制、交互地图的
- 天地图是纯粹用来当个底图的,相当于在leaflet中底部的一个图层而已
- 进行Marker打点、geojson绘制等操作都是使用leaflet实现
1. 使用天地图当底图
- 在token处填自己的token
- 我这里用的是天地图的
影像底图
,如果需要可自行更换或添加底图 - 天地图底图网址:lbs.tianditu.gov.cn/server/MapS…
- 只用替换我代码里的天地图链接里的
http://{s}.tianditu.com/img_c
/里的img_c
为我图中圈起来的编号,其他不用动
const token = "填自己的天地图token";
// 底图
const VEC_C ="http://{s}.tianditu.com/img_c/wmts?layer=vec&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
// 文字标注
// const CVA_C = "http://{s}.tianditu.com/cia_c/wmts?layer=cva&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
let map = L.map("map", {
minZoom: 3,
maxZoom: 17,
center: [34.33213, 109.00945],
zoomSnap: 0.1,
zoom: 3.5,
zoomControl: false,
attributionControl: false,
crs: L.CRS.EPSG4326,
});
L.tileLayer(VEC_C + token, {
zoomOffset: 1,
subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
opacity: 0.2,
}).addTo(map);
// 添加文字标注
// L.tileLayer(CVA_C + token, {
// tileSize: 256,
// zoomOffset: 1,
// subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
// }).addTo(map);
2. 绘制中国地图geojson
- 这里我需要国的边界和省的边界线颜色不一样,所以用了一个国的geojson和另一个包含省的geojson叠加来实现
- 获取geojson数据网站:datav.aliyun.com/portal/scho…
L.geoJson(sheng, {
style: {
color: "#0c9fb8",
weight: 1,
fillColor: "#028297",
fillOpacity: 0.1,
},
}).addTo(map);
L.geoJson(guo, {
style: {
color: "#b8c3c8",
weight: 2,
fillOpacity: 0,
},
}).addTo(map);
3. 更换背景主题色
我的实现思路比较简单粗暴,直接给天地图的图层设置透明度,对div元素设置背景色,如果UI配合,可以叫UI给个遮罩层的背景图,比如我这里就是用了四周有黑边渐变阴影,中间是透明的背景图。
<div id="map"></div>
<div class="mask"></div>
#map {
height: 100vh;
width: 100vw;
background: #082941;
z-index: 2;
}
.mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: url("./mask.png") no-repeat;
background-size: 100% 100%;
z-index: 1;
}
4. 完整代码
- 写自己天地图的token
- 自己下载geojson文件
- 自己看需要搞个遮罩层背景图,不需要就注释掉mask
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link
href="https://cdn.bootcdn.net/ajax/libs/leaflet/1.9.4/leaflet.css"
rel="stylesheet"
/>
</head>
<style>
* {
margin: 0;
padding: 0;
}
#map {
height: 100vh;
width: 100vw;
background: #082941;
z-index: 2;
}
.mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background: url("./mask.png") no-repeat;
background-size: 100% 100%;
z-index: 1;
}
</style>
<body>
<div id="map"></div>
<div class="mask"></div>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
<script src="./china.js"></script>
<script src="./guo.js"></script>
<script>
const token = "写自己天地图的token";
// 底图
const VEC_C =
"http://{s}.tianditu.com/img_c/wmts?layer=vec&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
// 文字标注
// const CVA_C =
// "http://{s}.tianditu.com/cia_c/wmts?layer=cva&style=default&tilematrixset=c&Service=WMTS&Request=GetTile&Version=1.0.0&Format=tiles&TileMatrix={z}&TileCol={x}&TileRow={y}&tk=";
let map = L.map("map", {
minZoom: 3,
maxZoom: 17,
center: [34.33213, 109.00945],
zoomSnap: 0.1,
zoom: 3.5,
zoomControl: false,
attributionControl: false, //版权控制器
crs: L.CRS.EPSG4326,
});
L.tileLayer(VEC_C + token, {
zoomOffset: 1,
subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
opacity: 0.2,
}).addTo(map);
// L.tileLayer(CVA_C + token, {
// tileSize: 256,
// zoomOffset: 1,
// subdomains: ["t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7"],
// }).addTo(map);
L.geoJson(sheng, {
style: {
color: "#0c9fb8",
weight: 1,
fillColor: "#028297",
fillOpacity: 0.1,
},
}).addTo(map);
L.geoJson(guo, {
style: {
color: "#b8c3c8",
weight: 2,
fillOpacity: 0,
},
}).addTo(map);
</script>
</html>
来源:juejin.cn/post/7485482994989596722
怎么将中文数字转为阿拉伯数字?
说在前面
最近实现了一个b站插件,可以通过语音来控制播放页面上指定的视频,在语音识别的过程中遇到了需要将中文数字转为阿拉伯数字的情况,在这里分享一下具体事例和处理过程。
功能背景
先介绍一下功能背景,页面渲染的时候会先对视频进行编号处理,具体如下:
比如我们想要播放第4个视频的话,我们只需要说“第4个”,插件就能帮我们选择第四个视频进行播放。
问题描述
功能背景我们已经了解了,那么问题是出在哪里呢?
如上图,这里识别出来的语音文本数字是中文数字,这样跟页面的视频编号无法对应上,因此我们需要实现一个方法来将中文转为阿拉伯数字。
方法实现
1、个位级映射表
const numMap = {
零: 0,一: 1,壹: 1,二: 2,两: 2,
三: 3,叁: 3,四: 4,肆: 4,五: 5,
伍: 5,六: 6,陆: 6,七: 7,柒: 7,
八: 8,捌: 8,九: 9,玖: 9,
};
2、单位映射表
const unitMap = {
十: { value: 10, sec: false },
拾: { value: 10, sec: false },
百: { value: 100, sec: false },
佰: { value: 100, sec: false },
千: { value: 1000, sec: false },
仟: { value: 1000, sec: false },
万: { value: 10000, sec: true },
萬: { value: 10000, sec: true },
亿: { value: 100000000, sec: true },
億: { value: 100000000, sec: true }
};
3、处理流程
- 遇到数字:先存起来(比如「三」记作3)
if (hasZero && current > 0) {
current *= 10;
hasZero = false;
}
current += numMap[char];
- 遇到单位:
- 如果是十/百/千:把存着的数字乘上倍数
(如「三百」→3×100=300)
current = current === 0 ? unit.value : current * unit.value;
section += current;
current = 0;
- 遇到万/亿:先结算当前数字,将当前数字加到总数上
processSection();
section = (section + current) * unit.value;
total += section;
section = 0;
- 如果是十/百/千:把存着的数字乘上倍数
- 遇到零:做个标记,提醒下个数字要占位
(如「三百零五」→300 + 0 +5=305)
if (char === "零") {
hasZero = true;
continue;
}
4、完整代码
function chineseToArabic(chineseStr) {
// 映射表(支持简繁)
const numMap = {
零: 0,一: 1,壹: 1,二: 2,两: 2,
三: 3,叁: 3,四: 4,肆: 4,五: 5,
伍: 5,六: 6,陆: 6,七: 7,柒: 7,
八: 8,捌: 8,九: 9,玖: 9,
};
//单位映射表
const unitMap = {
十: { value: 10, sec: false },
拾: { value: 10, sec: false },
百: { value: 100, sec: false },
佰: { value: 100, sec: false },
千: { value: 1000, sec: false },
仟: { value: 1000, sec: false },
万: { value: 10000, sec: true },
萬: { value: 10000, sec: true },
亿: { value: 100000000, sec: true },
億: { value: 100000000, sec: true }
};
let total = 0; // 最终结果
let section = 0; // 当前小节
let current = 0; // 当前累加值
let hasZero = false; // 零标记
const processSection = () => {
section += current;
current = 0;
};
for (const char of chineseStr) {
if (numMap.hasOwnProperty(char)) {
if (char === "零") {
hasZero = true;
continue;
}
if (hasZero && current > 0) {
current *= 10;
hasZero = false;
}
current += numMap[char];
} else if (unitMap.hasOwnProperty(char)) {
const unit = unitMap[char];
if (unit.sec) {
// 处理万/亿分段
processSection();
section = (section + current) * unit.value;
total += section;
section = 0;
} else {
current = current === 0 ? unit.value : current * unit.value;
section += current;
current = 0;
}
hasZero = false;
}
}
const last2 = chineseStr.slice(-2)[0];
const last2Unit = unitMap[last2];
if (last2Unit) {
current = (current * last2Unit.value) / 10;
}
return total + section + current;
}
功能测试
柒億零捌拾萬
十萬三十
十萬三
二百五
二百零五
八
插件信息
对我上述提到的插件感兴趣的同学可以看下我前面发的这篇文章:
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。
来源:juejin.cn/post/7485936146071765030
好人难当,坏人不做
好人难当,以后要多注意了,涨点记性。记录三件事情证明下:
1. 免费劳动
之前和一个同学一起做一个项目,说是创业,不过项目做好了,倒是他家店铺自己用起来了,后来一直让我根据他家的需求进行修改,我也一一的改了,他倒是挺感谢我的,说是请吃饭。不过也一直没请,后面都一年多过去了,还让我免费帮他改需求,我就说没时间,他说没时间的话可以把源码给他,他自己学着修改,我就直接把源码给他了,这个项目辛苦了一个多月,钱一毛也没赚到,我倒是搭进去一台服务器,一年花了三百多吧。现在源码给他就给他了吧,毕竟同学一场。没想到又过了半年,前段时间又找我来改需求了。这个项目他们家自己拿着赚钱,又不给我一毛钱,我相当于免费给他家做了个软件,还要出服务器钱,还要免费进行维护。我的时间是真不值钱啊,真成义务劳动了。我拒绝了,理由是忙,没时间。
总结一下,这些人总会觉得别人帮自己是理所当然的,各种得寸进尺。
2. 帮到底吧
因为我进行了仲裁,有了经验,然后被一个人加了好友,是一个前同事(就是我仲裁的那家公司),然后这哥们各种问题我都尽心回答,本着能帮别人一点就帮别人一点的想法,但是我免费帮他,他仲裁到手多少钱,又不会给我一毛钱。这哥们一个问题接一个,我都做了回答,后来直接要求用我当做和公司谈判的筹码,我严词拒绝了,真不知道这人咋想的,我帮你并没有获得任何好处,你这个要求有点过分了,很简单,他直接把我搬出来和公司谈判,公司肯定会找我,会给我带来麻烦,这人一点也没想这些事。所以之后他再询问有关任何我的经验,我已经不愿意帮他了。
总结一下,这些人更进一步,甚至想利用下帮自己的人,不考虑会给别人带来哪些困扰。
3. 拿你顶缸
最近做了通过一个亲戚接了一个项目,而这个亲戚的表姐是该项目公司的领导,本来觉得都是有亲戚关系的,项目价格之类开始问了,他们没说,只是说根据每个人的工时进行估价,后面我们每个人提交了个人报价,然后还是一直没给明确答复,本着是亲戚的关系,觉得肯定不会坑我。就一直做下去了,直到快做完了,价格还是没有出来,我就直接问了这个价格的事情,第二天,价格出来了,在我报价基础上直接砍半。我当然不愿意了,后来经过各种谈判,我终于要到了一个勉强可以的价格,期间群里谈判也是我一个人在说话,团队的其他人都不说话。后来前端的那人问我价格,我也把过程都实话说了,这哥们也要加价,然后就各种问我,我也啥都告他了。后来这个前端在那个公司领导(亲戚表姐)主动亮明身份,她知道这个前端和那个亲戚关系好,然后这个前端立马不好意思加价了,并且还把锅甩我头上,说是我没有告诉他她是他姐。还说我不地道,我靠,你自己要加价,关我啥事,你加钱也没说要分我啊,另外我给自己加价的时候你也没帮忙说话啊,我告诉你我加价成功了是我好心,也是想着你能加点就加点吧,这时候你为了面子不加了,然后说成要加价的理由是因为我,真是没良心啊。后面还问我关于合同的事情,我已经不愿意回答他了,让他自己找对面公司问去。
总结一下,这些人你帮他了他当时倒是很感谢你,但是一旦结果有变,会直接怪罪到你头上。
4. 附录文章
这个文章说得挺好的《你的善良,要有锋芒》:
你有没有发现,善良的人,心都很软,他们不好意思拒绝别人,哪怕为难了自己,也要想办法帮助身边的人。善良的人,心都很细,他们总是照顾着别人的情绪,明明受委屈的是自己,却第一时间想着别人会不会难过。
也许是习惯了对别人好,你常常会忽略自己的感受。有时候你知道别人是想占你便宜,你也知道别人不是真心把你当朋友,他们只是觉得你好说话,只是看中了你的善良,但是你没有戳穿,你还是能帮就帮,没有太多怨言。
你说你不想得罪人,你说你害怕被孤立,可是有人在乎过你吗?
这个世界上形形色色的人很多,有人喜欢你,有人讨厌你,你没有办法做到对每一个人好,也没办法要求每一个人都是真心爱你。所以你要有自己的选择,与舒服的人相处,对讨厌的人远离,过你自己觉得开心自在的生活就好,没必要为了便利别人,让自己受尽委屈。
看过一段话:善良是很珍贵的,但善良要是没有长出牙齿来,那就是软弱。
你的善良,要有锋芒,不要把时间浪费在不值得的人身上。对爱你的人,倾心相助,对利用你的人,勇敢说不。
愿你的善良,能被真心的人温柔以待。
来源:juejin.cn/post/7455667125798780980
如何优雅的回复面试官问:“你能接受加班吗?”
面试官问:“你能接受加班吗?”我脑袋嗡的一声,余音绕梁三日不绝于耳。
那一刻,我简直觉得自己像被突然砸中脑袋,脑袋里嗡的一声,余音绕梁三日。作为一个职场小白,这种问题简直颠覆了我对面试的认知。于是,我一时心血来潮,脱口而出一句:“领导抗揍吗?” 结果,大家猜到了,面试是上午结束的,Offer是当天中午凉的。
如何巧妙回答
“我认为加班是工作中不可避免的一部分,尤其是在一些特殊项目或紧急情况下。我非常热爱我的工作,并且对公司的发展充满信心,因此我愿意为了团队的成功付出额外的努力。当然,我也注重工作效率和时间管理,尽量在正常工作时间内完成任务。如果确实需要加班,我也会根据公司合理的安排,积极的响应。”
作为一名资深的面试官,今天面对这个问题,坐下来和大家聊聊应该怎么回答呢?面试官究竟喜欢怎样的回答?让我们深入分析一下。
面试官的心理
在职场中,想要出色地应对面试,需要具备敏锐的观察力和理解力。学会细致入微地观察,善于捕捉每一个细微的线索,这样才能在面试中游刃有余。懂的察言观色,方能尽显英雄本色。
面试官的考量点
- 评估工作稳定性
面试官提出“能否接受加班”的问题,旨在深入了解求职者的职业稳定性和对加班安排的适应性。这一评估有助于预测求职者入职后的表现和长期留任的可能性。工作稳定性是企业考量员工的关键指标之一,通过这一问题,面试官能够洞察求职者的职业发展规划及其对未来工作的期望。
- 筛选合适的候选人
通过询问加班的接受度,面试官筛选出那些愿意为达成工作目标而投入额外时间和精力的候选人。这种筛选方式有助于确保团队的整体运作效率和协作精神。合适的候选人不仅能快速融入团队,还能显著提升工作效率。因此,面试官借此问题寻找最匹配岗位需求的员工。
- 了解求职者的价值观
面试官还利用这个问题来探查求职者的价值观和工作态度,以此判断他们是否与公司的文化和核心价值观相契合。员工的价值观和态度对公司的长远发展起着至关重要的作用。通过这一询问,面试官能够确保求职者的个人目标与公司的发展方向保持一致,从而促进整体的和谐与进步。
考察的问题的意义
要理解问题的本质……为什么面试官会提出这样的问题?难道是因为你的颜值过高,引发了他的嫉妒?
- 工作态度
面试官通过询问加班的接受度,旨在评估求职者是否展现出积极的工作态度和强烈的责任心。在许多行业中,加班已成为常态,面试官借此问题了解求职者是否愿意在工作上投入额外的时间和精力。积极的工作态度和责任心是职场成功的关键因素,通过这一问题,面试官能够初步判断求职者是否适应高强度的工作环境。
- 岗位匹配度
特定岗位因其工作性质可能需要频繁加班。面试官通过提出加班相关的问题,旨在了解求职者是否能适应这类岗位的工作强度。由于不同岗位对工作强度的要求各异,面试官希望通过这一问题确保求职者对即将承担的角色有明确的认识,从而防止入职后出现期望不一致的情况。
- 抗压能力
加班往往伴随压力,面试官通过这一问题考察求职者的抗压能力和情绪管理技巧。抗压能力对于职场成功至关重要,面试官借此了解求职者在高压环境下的表现,以判断其是否符合公司的需求。
- 公司文化
面试官还利用这个问题来评估求职者对公司加班文化的接受程度,以此判断其价值观是否与公司相符。公司文化对员工的工作体验和满意度有着深远的影响,面试官希望通过这一问题确保求职者能够认同并融入公司文化。
回答的艺术
“知己知彼,百战不殆。”在面试中,回答问题的关键在于展现出积极和正向的态度。
- 积极态度
在回答有关加班的问题时,表达你对工作的热爱和对公司的忠诚,强调你愿意为了团队的成功而付出额外的努力。这种积极的态度不仅展示了你的职业素养和对工作的热情,还能显著提升面试官对你的好感。
例如:“我非常热爱我的工作,并且对公司的发展充满信心。我相信为了实现公司的目标和团队的成功,适当的加班是可以接受的。”
- 灵活性和效率
强调你在时间管理和工作效率上的能力,表明你在确保工作质量的同时,会尽可能减少加班的需求。灵活性和效率是职场中极为重要的技能,面试官可以通过这个问题了解你的实际工作表现。
例如:“我在工作中注重效率和时间管理,通常能够在规定的工作时间内完成任务。当然,如果有特殊情况需要加班,我也会全力以赴。”
- 平衡工作与生活
适当地提到你对工作与生活平衡的重视,并希望公司在安排加班时能够充分考虑到员工的个人需求。平衡工作与生活是职场人士普遍关注的问题,面试官通过这个问题可以了解你的个人需求和期望。
例如:“我非常重视工作与生活的平衡,希望在保证工作效率的同时,也能有足够的时间陪伴家人和进行个人活动。如果公司能够合理安排加班时间,我会非常乐意配合。”
- 适度反问
在回答时,可以适当地向面试官询问关于公司加班的具体情况,以便更全面地了解公司的加班文化和预期。这样的反问可以展现你的主动性和对公司的兴趣,有助于获取更多信息,做出更加明智的回答。
例如:“请问公司通常的加班情况是怎样的?是否有相关的加班补偿或调休安排?”
最后
所谓士为知己者死,遇良将则冲锋陷阵,择良人则共谋天下。在职场这场没有硝烟的战争中,我们每个人都是一名战士,寻找着属于自己的知己和良将。当面试官提出挑战性问题时,我们不仅要展示自己的能力和才华,更要表现出对工作的热爱和对公司的忠诚。
面对“你能接受加班吗?”这样的问题,我们应以积极的态度、灵活的思维和对工作与生活平衡的重视来回应。这样的回答不仅能展示我们的职业素养,还能让我们在众多求职者中脱颖而出,赢得面试官的青睐。
正如士为知己者死,我们在职场中也要找到那个能理解我们、支持我们的知己;遇良将则冲锋陷阵,我们要在优秀的领导下发挥自己的潜能,为公司的发展贡献力量;择良人则共谋天下,我们要与志同道合的同事共同努力,实现职业生涯的辉煌。
总之一句话,在面试中展现出积极向上的形象,不仅能为我们的职业生涯加分,更能让我们在职场上找到属于自己的价值和归属感。让我们以这句话为指引,勇敢地迎接职场的挑战,书写属于自己的辉煌篇章。
来源:juejin.cn/post/7457211584709066792
npm和npx的区别
npx
和 npm
是 Node.js 生态中两个密切相关的工具,但它们的功能和用途有显著区别:
1. npm
(Node Package Manager)
- 定位:Node.js 的包管理工具,用于安装、管理和发布 JavaScript 包。
- 核心功能:
- 安装依赖:通过
npm install <package>
安装包到本地或全局。 - 管理项目依赖:通过
package.json
文件记录依赖版本。 - 运行脚本:通过
npm run <script>
执行package.json
中定义的脚本。 - 发布包:通过
npm publish
将代码发布到 npm 仓库。
- 安装依赖:通过
- 示例:
npm install lodash # 安装 lodash 到本地 node_modules
npm install -g typescript # 全局安装 TypeScript
npm run start # 运行 package.json 中的 "start" 脚本
2. npx
(Node Package Executor)
- 定位:
npm
的配套工具,用于直接执行包中的命令,无需全局或本地安装。 - 核心功能:
- 临时执行包:自动下载远程包并运行,完成后删除。
- 运行本地已安装的包:直接调用本地
node_modules/.bin
中的命令。 - 切换包版本:指定特定版本运行(如
npx node@14 myscript.js
)。
- 示例:
npx create-react-app my-app # 临时下载并运行 create-react-app
npx eslint . # 运行本地安装的 eslint
npx http-server # 启动一个临时 HTTP 服务器
关键区别
特性 | npm | npx |
---|---|---|
主要用途 | 安装和管理依赖 | 直接执行包中的命令 |
是否需要安装包 | 需要提前安装(本地或全局) | 可临时下载并执行,无需提前安装 |
典型场景 | 管理项目依赖、运行脚本、发布包 | 运行一次性命令、测试工具、脚手架 |
执行本地包命令 | 需通过 npm run 或完整路径调用 | 直接通过 npx <command> 调用 |
全局包依赖 | 依赖全局安装的包 | 不依赖全局包,可指定版本运行 |
为什么需要 npx
?
- 避免全局污染:
例如运行create-react-app
时,无需全局安装,直接通过npx
临时调用最新版本。 - 简化本地包调用:
本地安装的工具(如eslint
、jest
)可以直接用npx
执行,无需配置package.json
脚本或输入冗长路径。 - 兼容多版本:
可指定版本运行,如npx node@14 myscript.js
,避免全局版本冲突。
使用建议
- 用
npm
:
管理项目依赖、定义脚本、发布包。 - 用
npx
:
运行脚手架工具(如create-react-app
)、临时工具(如http-server
)或本地已安装的命令。
示例场景
# 使用 npm 安装依赖
npm install axios
# 使用 npx 运行一次性工具
npx json-server db.json # 临时启动一个 REST API 服务器
# 使用 npm 运行脚本(需在 package.json 中定义 "scripts")
npm run build
# 使用 npx 调用本地已安装的包
npx webpack --config webpack.config.js
通过合理使用 npm
和 npx
,可以更高效地管理依赖和执行命令。
来源:juejin.cn/post/7484992785952096267
TypeScript 官方宣布弃用 Enum?Enum 何罪之有?
1. 官方真的不推荐 Enum 了吗?
1.1 事情的起因
起因是看到 科技爱好者周刊(第 340 期) 里面推荐了一篇文章,说是官方不再推荐使用 enum 语法,原文链接在 An ode to TypeScript enums,大致是说 TS 新出了一个 --erasableSyntaxOnly
配置,只允许使用可擦除语法,但是 enum 不可擦除,因此推断官方已不再推荐使用 enum 了。官方并没有直接表达不推荐使用,那官方为什么要出这个配置项呢?
1.2 什么是可擦除语法
就在上周,TypeScript 发布了 5.8 版本,其中有一个改动是添加了 --erasableSyntaxOnly
配置选项,开启后仅允许使用可擦除语法,否则会报错。enum
就是一个不可擦除语法,开启 erasableSyntaxOnly
配置后,使用 enum
会报错。
例如,如果在 tsconfig 文件中配置 "erasableSyntaxOnly": true
(只允许可擦除语法),此时使用不可擦除语法,将会得到报错:
可擦除语法就是可以直接去掉的、仅在编译时存在、不会生成额外运行时代码的语法,例如 type
,interface
。不可擦除语法就是不能直接去掉的、需要编译为JS且会生成额外运行时代码的语法,例如 enum
,namesapce(with runtime code
)。 具体举例如下:
可擦除语法,不生成额外运行时代码,比如 type
、let n: number
、interface
、as number
等:
不可擦除语法,生成额外运行时代码,比如 enum
、namespace
(具有运行时行为的)、类属性参数构造语法糖(Class Parameter properties)等:
// 枚举类型
enum METHOD {
ADD = 'add'
}
// 类属性参数构造
class A {
constructor(public x: number) {}
}
let a: number = 1
console.log(a)
需要注意,具有运行时行为的 namespace
才属于不可擦除语法。
// 不可擦除,具有运行时逻辑
namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
}
// 可擦除,仅用于类型声明,不包含任何运行时逻辑
namespace Shapes {
export interface Rectangle {
width: number;
height: number;
}
}
1.3 TS 官方为什么要出 erasableSyntaxOnly?
官方既然没有直接表达不推荐 enum,那为什么要出 erasableSyntaxOnly
配置来排除 enum
呢?
我找到了 TS 官方文档(The --erasableSyntaxOnly Option)说明:
大致意思是说之前 Node 新版本中支持了执行 TS 代码的能力,可以直接运行包含可擦除语法的 TypeScript 文件。Node 将用空格替换 TypeScript 语法,并且不执行类型检查。总结下来就是:
在 Node 22 版本:
- 需要配置
--experimental-transform-types
执行支持 TS 文件 - 要禁用 Node 这种特性,使用参数
--no-experimental-strip-types
在 Node 23.6.0 版本:
- 默认支持直接运行可擦除语法的 TS 文件,删除参数
--no-experimental-strip-types
- 对于不可擦除语法,使用参数
--experimental-transform-types
综上所述,TS 官方为了配合 Node.js 这次改动(即默认允许直接执行不可擦除语法的 TS 代码),才添加了一个配置项 erasableSyntaxOnly
,只允许可擦除语法。
2. Enum 的三大罪行
自 Enum 从诞生以来,它一直是前端界最具争议的特性之一,许多前端开发者乃至不少大佬都对其颇有微词,纷纷发起了 DO NOT USE TypeScript Enum 的吐槽。那么enum
真的有那么难用吗?我认为是的,这玩意坑还挺多的,甲级战犯 Enum,出列!
2.1 枚举默认值
enum
默认的枚举值从 0
开始,这还不是最关键的,你传入了默认枚举值时,居然是合法的,这无形之中带来了类型安全问题。
enum METHOD {
ADD
}
function doAction(method: METHOD) {
// some code
}
doAction(METHOD.ADD) // ✅ 可以
doAction(0) // ✅ 可以
2.2 不支持枚举值字面量
还有一种场景,我要求既可以传入枚举类型,又要求传入枚举值字面量,如下所示,但是他又不合法了?(有人说你定义传枚举类型就要传相应的枚举,这没问题,但是上面提到的问题又是怎么回事呢?这何尝不是 Enum 的双标?)
enum METHOD {
ADD = 'add'
}
function doAction(method: METHOD) {
// some code
}
doAction(METHOD.ADD) // ✅ 可以
doAction('add') // ❌ 不行
2.3 增加运行时开销
TypeScript 的 enum
在编译后会生成额外的 JavaScript 双向映射数据,这会增加运行时的开销。
3. Enum 的替代方案
众所周知,TS 一大特性是类型变换,我们可以通过类型操作组合不同类型来达到目标类型,又称为类型体操。下面的四种解决方案,可以根据实际需求来选择。
3.1 const enum
const enum
是解决产生额外生成的代码和额外的间接成本有效且快捷的方法,但不推荐使用。
const enum
由于编译时内联带来了性能优化,但在.d.ts
文件、isolatedModules
兼容性、版本不匹配及运行时缺少.js
文件等场景下存在隐藏陷阱,可能导致难以发现的 bug。详见官方说明:const-enum-pitfalls
const enum METHOD {
ADD = 'add',
DELETE = 'delete',
UPDATE = 'update',
QUERY = 'query',
}
function doAction(method: METHOD) {
// some code
}
doAction(METHOD.ADD) // ✅ 可行
doAction('delete') // ❌ 不行
const enum
解析后的代码中引用 enum 的地方将直接被替换为对应的枚举值:
3.2 模板字面量类型
将枚举类型包装为模板字面量类型(Template Literal Types),从而即支持枚举类型,又支持枚举值字面量,但是没有解决运行时开销问题。
enum METHOD {
ADD = 'add',
DELETE = 'delete',
UPDATE = 'update',
QUERY = 'query',
}
type METHOD_STRING = `${METHOD}`
function doAction(method: METHOD_STRING) {
// some code
}
doAction(METHOD.ADD) // ✅ 可行
doAction('delete') // ✅ 可行
doAction('remove') // ❌ 不行
3.3 联合类型(Union Types)
使用联合类型,引用时可匹配的值限定为指定的枚举值了,但同时也没有一个地方可以统一维护枚举值,如果一旦枚举值有调整,其他地方都需要改。
type METHOD =
| 'add'
/**
* @deprecated 不再支持删除
*/
| 'delete'
| 'update'
| 'query'
function doAction(method: METHOD) {
// some code
}
doAction('delete') // ✅ 可行,没有 TSDoc 提示
doAction('remove') // ❌ 不行
3.4 类型字面量 + as const(推荐)
类型字面量就是一个对象,将一个对象断言(Type Assertion)为一个 const
,此时这个对象的类型就是对象字面量类型,然后通过类型变换,达到即可以传入枚举值,又可以传入枚举类型的目的。
const METHOD = {
ADD:'add',
/**
* @deprecated 不再支持删除
*/
DELETE:'delete',
UPDATE: 'update',
QUERY: 'query'
} as const
type METHOD_TYPE = typeof METHOD[keyof typeof METHOD]
function doAction(method: METHOD_TYPE) {
// some code
}
doAction(METHOD.DELETE) // ✅ 可行,有 TSDoc 提示
doAction('delete') // ✅ 可行
doAction('remove') // ❌ 不行
3.5 Class 类静态属性自定义实现
还有一种方法,参考了 @Hamm 提供的方法,即利用 TS 面向对象的特性,自定义实现一个枚举类,实际上这很类似于后端定义枚举的一般方式。这种方式具有很好的扩展性,自定义程度更高。
- 定义枚举基类
/**
* 枚举基类
*/
export default class EnumBase {
/**
* 枚举值
*/
private value!: string
/**
* 枚举描述
*/
private label!: string
/**
* 记录枚举
*/
private static valueMap: Map<string, EnumBase> = new Map();
/**
* 构造函数
* @param value 枚举值
* @param label 枚举描述
*/
public constructor(value: string, label: string) {
this.value = value
this.label = label
const cls = this.constructor as typeof EnumBase
if (!cls.valueMap.has(value)) {
cls.valueMap.set(value, this)
}
}
/**
* 获取枚举值
* @param value
* @returns
*/
public getValue(): string | null {
return this.value
}
/**
* 获取枚举描述
* @param value
* @returns
*/
public getLabel(): string | null {
return this.label
}
/**
* 根据枚举值转换为枚举
* @param this
* @param value
* @returns
*/
static convert<E extends EnumBase>(this: new(...args: any[]) => E, value: string): E | null {
return (this as any).valueMap.get(value) || null
}
}
- 继承实现具体的枚举(可根据需要扩展)
/**
* 审核状态
*/
export class ENApproveState extends EnumBase {
/**
* 未审核
*/
static readonly NOTAPPROVED = new ENApproveState('1', '未审核')
/**
* 已审核
*/
static readonly APPROVED = new ENApproveState('2', '已审核')
/**
* 审核失败
*/
static readonly FAILAPPROVE = new ENApproveState('3', '审核失败')
/***
* 审核中
*/
static readonly APPROVING = new ENApproveState('4', '审核中')
}
- 使用
test('ENCancelState.NOCANCEL equal 1', () => {
expect(ENApproveState.NOTAPPROVED.getValue()).toBe('1')
expect(ENApproveState.APPROVING.getValue()).toBe('4')
expect(ENApproveState.FAILAPPROVE.getLabel()).toBe('审核失败')
expect(ENApproveState.convert('2')).toBe(ENApproveState.APPROVED)
expect(ENApproveState.convert('99')).toBe(null)
})
4. 总结
- TS 可擦除语法 是指
type
、interface
、n:number
等可以直接去掉的、仅在编译时存在、不会生成额外运行时代码的语法 - TS 不可擦除语法 是指
enum
、constructor(public x: number) {}
等不可直接去除且会生成额外运行时代码的语法 - Node.js 23.6.0 版本开始 默认支持直接执行可擦除语法 的 TS 文件
enum
的替代方案有多种,取决于实际需求。用字面量类型 +as const
是比较常用的一种方案。
TS 官方为了兼容 Node.js 23.6.0 这种可执行 TS 文件特性,出了 erasableSyntaxOnly
配置禁用不可擦除语法,反映了 TypeScript 官方对减少运行时开销和优化编译输出的关注,而不是要放弃 enum
。
但或许未来就是要朝着这个方向把 enum 优化掉也说不定呢?
5. 参考链接
来源:juejin.cn/post/7478980680183169078
Linux 之父把 AI 泡沫喷了个遍:90% 是营销,10% 是现实。
作者:Shubhransh Rai
Linux 之父把 AI 泡沫喷了个遍
前言: 一篇“技术老炮”的情绪宣泄文而已,说白了,这篇文章就是作者用来发泄不满的牢骚文。全篇围绕一个中心思想打转:我讨厌 AI 炒作,讨厌到牙痒痒。
但话说回来,没炒作怎么能让大众知道、接受这些新技术?大家都讨厌广告,可真到了你要买东西的时候,没有广告你上哪儿去找好产品?炒作虽然惹人烦,但在商业世界里,它就是传播的方式——不然怎么让一个普通人知道什么是AI?
所以归根到底,这篇文章其实并不是在批评 AI 本身,更不是在否定技术的未来。它只是在重复一个观点:**我就是讨厌炒作。**而已。
Linus Torvalds 刚刚狠狠喷了整个 AI 行业 —— 而且他说得没错
Linus Torvalds —— 那个基本上构建出现代计算的人 —— 直接放出了他对 AI 的原话。
他的结论?
“90% 是营销,10% 是现实。”
毒辣。准确。而且,说实话,早该有人站出来讲了。
在维也纳的开源峰会上,Torvalds 对 AI 的炒作问题发表了一番咬牙切齿的评论,他说:
“我觉得 AI 确实很有意思,我也觉得它终将改变世界。但与此同时,我真的太讨厌这类炒作循环了,我真的不想卷进去。”
这个人见过太多科技泡沫的兴起和崩塌。现在?AI 是下一个加密货币。
Torvalds 的应对方式:直接无视
AI 的炒作已经到了让人无法忍受的地步,甚至连 Linus —— 也就是发明了 Linux 的人 —— 都选择闭麦了。
“所以我现在对 AI 的态度基本就是:无视。因为我觉得整个围绕 AI 的科技行业都处在一个非常糟糕的状态。”
说真的?Respect。
我们现在活在一个时代,每个初创公司都在自己网站上贴上“AI 加持”,然后祈祷能拿到风投。
现实呢?这些所谓的“AI 公司”绝大多数不过是把 OpenAI 的 API 包装了一层花哨的 UI。
甚至那些大厂 —— Google、微软、OpenAI —— 也在砸几百亿美元,试图说服大家 AGI(通用人工智能)马上就来了。
与此同时,AI 模型却在数学题上瞎编,还能虚构出不存在的法律案件。
Torvalds 是科技圈为数不多的几个,完全没必要陪大家演戏的人。
他没在卖 AI 产品,也不需要讨好投资人。
他看到 BS(胡扯)就直说。
五年内 AI 的现实检验
Torvalds 也承认,AI 最终会有用的……
“再过五年,情况会变,到时候我们就会看到 AI 真正被用在日常工作负载中了。”
这是目前最靠谱的观点了。
现在的 AI,基本上:
• 写一些烂代码,让真正的工程师收拾残局。
• 吐出一堆 AI 生成垃圾,被 SEO 农场铺满互联网。
• 以前所未有的速度生成公司里的官话废话。
再等五年,我们要么看到实际的生产力提升,要么看到一堆烧光 hype 的 AI 创业公司坟场。
Torvalds 谈 AI 优点:“ChatGPT 还挺酷,我猜吧。”
Torvalds 也不完全是个 AI 悲观论者 —— 他承认确实有些场景是真的有用。
“ChatGPT 演示效果挺好,而且显然已经在很多领域用上了,尤其是像图形设计这类。”
听起来挺合理的。AI 工具有些方面确实还行:
• 帮创意项目生成素材
• 自动化一些无聊流程(比如总结文档)
• 让人以为自己变得更高效了
问题是?AI 的炒作和实际效果严重脱节。
我们听到一些 CEO 说“AI 会取代所有软件工程师”,结果 LLM 连基本逻辑都理不清。
Torvalds 一眼看穿了这些噪音。
他的最终结论?
“但我真的讨厌这个炒作周期。”
结语:Linus Torvalds 是科技界最后的清醒人
Torvalds 不讨厌 AI。
他讨厌的是 AI 的炒作机器。
而他是对的。
每一次科技革命,都是先疯狂承诺一堆,然后现实拍脸:
• 互联网泡沫 —— “互联网一夜之间会取代一切!”
• 加密货币泡沫 —— “去中心化能解决所有问题!”
• AI 泡沫 —— “AGI 马上就来了!”
现实呢?
• 互联网确实改变了一切 —— 但用了 20 年。
• 加密货币确实有用 —— 但 99% 的项目都是骗子。
• AI 也终将有用 —— 但现在,它基本上只是公司演戏用的道具。
Linus Torvalds 很清楚这游戏怎么玩。
他见过科技圈的每一波炒作潮起又落。
他的解决办法?
别听那些噪音。关注真正的技术。等 hype 自动消散。
说真的?这是 2025 年最靠谱的建议了。
AI 的炒作到底是个啥?
AI 就是个 hype 吗?是,也不是。
AI 炒作列车全速前进。
所有人都在卖 “生成式 AI”、“预测式 AI”、“自主智能体 AI”,还有不知道接下来啥新词。
硅谷根本停不下来,逮谁跟谁说 AI 会彻底颠覆一切。
问题是:真会吗?
我们来捋一捋。
AI 炒作周期:一套熟悉的骗局
只要你过去二十年关注过科技趋势,你肯定见过这个套路。
Gartner 给它取了个名字:炒作周期(Hype Cycle),它是这样的:
- 创新触发 —— 某些技术宅发明了点啥
- 膨胀期顶点 —— CEO 和 VC 开始说些离谱话
- 幻灭低谷 —— 现实来袭,发现比想象难多了
- 生产力平台期 —— 多年打磨后,终于变得真有用
我们现在在哪?
AI 正脸着地掉进“幻灭低谷”。
为啥?
• 大多数 AI 初创公司不过是 OpenAI API 的壳子
• 各种公司贴“AI 加持”标签就为了拉高股价
• 技术贵、不稳定、而且经常瞎编
基本上,我们正处在“先装出来,后面再补课”的阶段。
AI 已经来了(但和你想的不一样)
很多人以为 AI 是个超级智能体,一夜之间能自动化一切。
现实警告:AI 早就来了,真相却挺无聊的。
它没有掌控公司。
它没有替代程序员。
它在干的事包括:
• 过滤垃圾邮件
• 生成客服脚本
• 推荐广告(只是不那么烂而已)
所以,AI 是有用的。
但远没你风投爹说的那么牛。
预测式 AI vs. 生成式 AI:真正的游戏
AI 可以分两大类:
- 生成式 AI —— 那些 LLM(像 ChatGPT)能生成文本、图像、深伪视频
- 预测式 AI —— 用来预测趋势、识别模式的机器学习模型
生成式 AI 吸引了全部目光,因为它光鲜亮丽。
预测式 AI 才是挣钱的正道,因为它解决了真正的商业问题。
比如?
• 医疗:预测疾病暴发
• 金融:在诈骗发生前识别它
• 零售:在厕纸卖光前优化库存
最好的效果来自两者结合:
预测式 AI 预测未来,生成式 AI 自动应对。
这就是 AI 今天真正能发挥作用的地方。
AI 的未来:炒作 vs. 现实
所以,AI 会真的改变世界吗?
会。
但不是明天。
一些靠谱的预测:
✅ AI 会自动化那些烦人的工作 —— 重复性任务直接消失
✅ AI 会提升效率 —— 前提是公司别再吹过头
✅ AI 会无处不在 —— 某些我们根本注意不到的地方
一些纯 BS 的预测:
❌ AI 会替代所有工作 —— 它还是得靠人引导
❌ AGI 马上就来了 —— 不可能,别骗了
❌ AI 是完美且无偏见的 —— 它是喂互联网垃圾长大的
最终结论:AI 既被过度炒作,又是不可避免的未来
AI 是不是 hype?当然是。
AI 会不会消失?绝对不会。
现在大多数 AI 项目,都是营销秀。
但再过 5 到 10 年,最后活下来的赢家会是那些:
• 真正把 AI 用在合适地方的公司
• 关注解决实际问题,而不是追热词的公司
• 不再把 AI 当魔法,而是当工具对待的公司
hype 会死。
有用的东西会留下来。
来源:juejin.cn/post/7485940589885538344
安卓突然终止「开源」,开发者遭背叛?社区炸锅了
【新智元导读】谷歌将改变一直以来对 Android 开源项目(AOSP)的公开开发模式,转而在私有环境中进行。但这并非意味着 Android 彻底闭源。对于普通用户而言不会有什么影响,但却让科技爱好者失去了一扇「窥视」安卓内部的窗口。
据 Android Authority 报道,谷歌已经向其确认,谷歌将很快在私有环境中开发 Android 开源项目(AOSP,Android Open Source Project),但依然会开源代码。
很多小伙伴可能会慌了,我的安卓手机不能用了?
目前来看,谷歌私下开发 AOSP 项目还不至于到「天塌下来」的地步,普通手机用户更是几乎感觉不到什么变化。
大部分主流手机厂商(比如小米、vivo、三星等)早就跟谷歌签好了各种合作伙伴协议。
只要这些协议还在,厂商们就还能照常拿到最新的 Android 源代码,通过 Google 自家的认证,正常预装 Google Play、Gmail 这些服务和应用。
谷歌对安卓系统的支持也不会断。
一句话,还是老样子。
那么问题来了,谷歌到底做了什么?
这就要从谷歌的安卓开源项目(AOSP)说起了。
什么是 Android Open Source Project(AOSP)?
AOSP 简单来说,就是谷歌给所有 Android 设备提供了一个「毛坯房」——操作系统的基本框架和核心部件。
任何开发者都可以免费下载它的代码,随意改动、分发,然后打造自己的定制系统。
比如小米 HyperOS、vivo OriginOS 都是在 AOSP 基础上搭建起来的。
网站地址:source.android.com/?hl=zh-cn
而 Android 系统本身是跑在 Linux 内核上,这个内核用的是 GPL 许可证,规则挺严格。
简单说就是,只要使用采用了 GPL 许可证的代码,你就得开源,体现「要玩就一起玩」的精神。
但 Google 为了让 Android 既开源又能赚钱,玩了个聪明设计:底层 Linux 内核老实按 GPL 开源,但中间 AOSP 大部分代码却用宽松的 Apache 2.0 许可证。
这样厂商既能自由改动 Android,不用全盘公开,还能加自己专有的东西,既开放又灵活。
具体来说,Linux 内核和模块还得开源,但到了用户空间的应用就不受 GPL 限制,想闭源就闭源。
结果就是,AOSP 底层 GPL 开源,中层 Apache 宽松开源,上层应用随开发者意愿,想怎么玩就怎么玩。
谷歌的这点小聪明那是相当的成功。
回想将近二十年前,智能手机刚起步那会儿,苹果发布了 iPhone。
谷歌也想在移动市场分一杯羹,于是决定推出 Android。
这不光帮助谷歌赚了个技术开放的好名声,还把一大堆厂商和用户从塞班、诺基亚、Windows Mobile、黑莓手里抢了过来。
真是神来之笔。
Android 开源这步棋,绝对是谷歌今天能占据移动操作系统市场七成以上份额的最大功臣。
市场是拿到了,代价是 AOSP 软件的维护是要做的。
问题是,随着手机的功能越来越多,这种维护工作的代价也越来越大。
终于,谷歌忍不了了。
代码同步难,谷歌决定「关起门」来开发,但依然开源代码
写过代码的都知道相比写代码,「合并代码」反而是最令人头疼的问题。
2007 年,谷歌开放了安卓的核心代码,这步棋让谷歌摘取了移动互联网时代最大的果实。
但是也导致安卓这个项目有了两个「主分支」。
一个分支就是公共的 AOSP 分支,这个分支对任何人都开放,大家所说的「安卓是开源」就是指这个分支。
一些附属功能,比如蓝牙功能,仍然在 AOSP 分支中公开开发,你可以在开源的 Android Code Search 中搜索到相关源代码。
然而,AOSP 公共分支并不包含谷歌专有的应用和服务,比如 Google Play 商店、Gmail、Google Maps 等。
AOSP 虽然没有谷歌自己的服务,但是仍然可以编译为一个完整的可用操作系统。
许多设备制造商基于 AOSP 开发自己的操作系统,包括:
- 三星:开发了 One UI。
- 小米:开发了 MIUI。
- OPPO:开发了 ColorOS。
- 华为:开发了早期的 EMUI。
- 一加:开发了 OxygenOS。
另一个分支则是完全的闭源开发,可以看做谷歌自己的安卓「亲儿子」。
这个分支仅限于拥有谷歌移动服务(GMS)许可协议的公司使用,以上类似三星 One UI 这种 Android 系统也可以使用,只要谷歌给予授权。
目前来看,大多数组件,包括核心 Android 操作系统框架,都是在 Google 的内部分支中私下开发的。
两个分支导致一个很大问题,就是内部分支的开发进度领先于公开的 AOSP,导致两个分支差异很大。
这种差异逼得谷歌必须花费时间和精力在公共 AOSP 分支与其内部分支之间合并补丁上。
这就到了程序员「喜闻乐见」的环节,由于分支差异很大,合并冲突经常出现。
以这个启用导航栏和键盘屏幕放大功能的补丁为例,该补丁引入了新的辅助功能设置,该设置被放置在辅助功能设置列表的末尾。
这会导致合并冲突,因为 AOSP 与谷歌内部分支之间的列表长度不同(图中变量 accessibility_magnify_nav_and_ime 设置为 58 和 59 冲突)。
虽然针对此特定问题的修复很简单,但当其他许多 AOSP 补丁集成到谷歌的内部分支时,都会触发类似的合并冲突。
另一个例子是,开发 Android 的新仅解锁存储区域 API 需要一位 Google 工程师从内部分支中挑选一个补丁到 AOSP 以解决合并冲突。
这是因为虽然 API 是在 AOSP 中开发的,但包含新 Android 构建标志的文件是在内部开发的。
因此,必须在内部提交一个更新构建标志文件的补丁,然后应用到 AOSP。
也许这些冲突单独看都不难处理,但是架不住可能会有无数这样「合并冲突」的例子。
「累觉不爱」,也许这就是谷歌放弃当前双管齐下的 Android 开发策略,转而将所有开发工作内部化的原因。
这对我们意味着什么?
这一决策整体来说,并不意味着 Android 正在变成「闭源」。
谷歌只是想把「开发过程」藏起来,依然会继续发布源代码。
最大的区别在于,AOSP 公共分支存在时,对于 Android 爱好者和科技行业记者来说,这是一个能够「窥探」Android 最新动向的窗口。
现在这个「窗口」要被谷歌关上了,这可能会让这些科技极客们感到沮丧,因为这减少了他们对 Google 开发工作的洞察力。
对于开发者,这会让他们更难跟上新的 Android 平台变化,因为他们将无法再跟踪 AOSP 中的变化。
比如外国的一个记者在 AOSP 中发现了某些代码变更,然后提前数月就预测了 Pixel 的网络摄像头功能,他还利用 AOSP 中的线索推断出 Android 16 的提前发布日期。
而对于大多数的我们,甚至包括安卓应用开发者,可以说毫无影响。
事实上,从逻辑的角度上,谷歌大概率就是觉得维护代码的成本过高,不论是从 AOSP 合并到内部版本,还是将内部版本的更新带给 AOSP 公共分支,这些工作都需要工程师完成。
可以说这些处理冲突的工作过于「低端」,对于谷歌的工程师来说,耗时耗力而且毫无意义。
但是 AOSP 某种意义上已经可以看做是谷歌在开源生态和程序员心目中的「投名状」。
作为以「不作恶」为公司理念的谷歌,安卓开源这步棋被认为是谷歌最成功的一次战略决策之一。
在极客们看来,这次决策类似于谷歌自己推倒了过去十几年树立起来的「精神丰碑」。
当然,从谷歌自己的角度看来,选择将工作整合在一个内部分支下,同时简化操作系统开发和源代码发布,是可以理解的。
毕竟 AOSP 对 Google 的商业价值,跟当年比起来,已经完全不是一个量级了。
从最近谷歌对 Gemini 以及 Gemma 的疯狂更新来看,AI 才是其工作的重点。
其实所有人都知道,相比于 Gemini,安卓对于谷歌已不再那么重要。
参考资料:
来源:juejin.cn/post/7486315070362075173
年少不知自增好,错把UUID当个宝!!!
在 MySQL 中,使用 UUID 作为主键 在大表中可能会导致性能问题,尤其是在插入和修改数据时效率较低。以下是详细的原因分析,以及为什么修改数据会导致索引刷新,以及字符主键为什么效率较低。
1. UUID 作为主键的问题
(1)UUID 的特性
- UUID 是一个 128 位的字符串,通常表示为 36 个字符(例如:
550e8400-e29b-41d4-a716-446655440000
)。 - UUID 是全局唯一的,适合分布式系统中生成唯一标识。
(2)UUID 作为主键的缺点
1. 索引效率低
- 索引大小:UUID 是字符串类型,占用空间较大(36 字节),而整型主键(如
BIGINT
)仅占用 8 字节。索引越大,存储和查询的效率越低。 - 索引分裂:UUID 是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。
2. 插入性能差
- 随机性:UUID 是无序的,每次插入新数据时,新记录可能会插入到索引树的任意位置,导致索引树频繁调整。
- 页分裂:InnoDB 存储引擎使用 B+ 树作为索引结构,随机插入会导致页分裂,增加磁盘 I/O 操作。
3. 查询性能差
- 比较效率低:字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。
- 索引扫描范围大:UUID 索引占用的空间大,导致索引扫描的范围更大,查询效率降低。
2. 修改数据导致索引刷新的原因
(1)索引的作用
- 索引是为了加速查询而创建的数据结构(如 B+ 树)。
- 当数据被修改时,索引也需要同步更新,以保持数据的一致性。
(2)修改数据对索引的影响
- 更新主键:
- 如果修改了主键值,MySQL 需要删除旧的主键索引记录,并插入新的主键索引记录。
- 这个过程会导致索引树的调整,增加磁盘 I/O 操作。
- 更新非主键列:
- 如果修改的列是索引列(如唯一索引、普通索引),MySQL 需要更新对应的索引记录。
- 这个过程也会导致索引树的调整。
(3)UUID 主键的额外开销
- 由于 UUID 是无序的,修改主键值时,新值可能会插入到索引树的不同位置,导致索引树频繁调整。
- 相比于有序的主键(如自增 ID),UUID 主键的修改操作代价更高。
3. 字符主键导致效率降低的原因
(1)存储空间大
- 字符主键(如 UUID)占用的存储空间比整型主键大。
- 索引的大小直接影响查询性能,索引越大,查询时需要的磁盘 I/O 操作越多。
(2)比较效率低
- 字符串比较比整型比较慢,尤其是在大表中,查询性能会显著下降。
- 例如,
WHERE id = '550e8400-e29b-41d4-a716-446655440000'
的效率低于WHERE id = 12345
。
(3)索引分裂
- 字符主键通常是无序的,插入新数据时,可能会导致索引树频繁分裂和重新平衡,影响性能。
4. 如何优化 UUID 主键的性能
(1)使用有序 UUID
- 使用有序 UUID(如
UUIDv7
),减少索引分裂和页分裂。 - 有序 UUID 的生成方式可以基于时间戳,保证插入顺序。
(2)将 UUID 存储为二进制
- 将 UUID 存储为
BINARY(16)
而不是CHAR(36)
,减少存储空间。
CREATE TABLE users (
id BINARY(16) PRIMARY KEY,
name VARCHAR(255)
);
(3)使用自增主键 + UUID
- 使用自增主键作为物理主键,UUID 作为逻辑主键。
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE,
name VARCHAR(255)
);
(4)分区表
- 对大表进行分区,减少单个索引树的大小,提高查询性能。
~Summary
- UUID 作为主键的缺点:
- 索引效率低,插入和查询性能差。
- 修改数据时,索引需要频繁刷新,导致性能下降。
- 字符主键效率低的原因:
- 存储空间大,比较效率低,索引分裂频繁。
- 优化建议:
- 使用有序 UUID 或二进制存储。
- 结合自增主键和 UUID。
- 对大表进行分区。
来源:juejin.cn/post/7478495083374559270
AI时代下,我用陌生技术栈两天开发完一个App后的总结
AI时代下,我用陌生技术栈两天开发完一个App后的总结
今年的 AI 应用赛道简直是热火朝天,大街上扔一把核桃能砸到三个在讨论 AI 的,更有不少类似于零编程经验用 AI 三天开发 App 登榜 App Store的标题在各大平台的AI话题圈子里刷屏,作为一个在互联网行业摸爬滚打多年的程序员,我做过开源项目,也做过多个小型独立商业项目,最近两年也是在 AI 相关公司任职,对此我既感到兴奋又难免焦虑——为什么我还没遇到这样的机遇?
刚好最近想到了一个点子,是一个结合了屏幕呼吸灯 + 轻音乐 + 白噪声的辅助睡眠移动端 A 应用,我将其命名为“音之梦”,是我某天晚上睡不着看到路由器闪烁的灯光照耀在墙壁上之后突然爆发的灵感。
这是个纯客户端应用,没有太多外部依赖,体量小,正好拿来试一下是不是真的有可能完全让 AI 来将它实现,而为了尽量模拟“编程小白”这个身份,这次我选择用我比较陌生的 Swift UI。
先上结论:
对于小体量的应用,或者只考虑业务实现而不需要考虑架构合理性/可维护性的稍大体量的应用,在纯编码层面确实是完全可以实现的,作为一个不会 Swift UI 的开发者,我确实在不到 2 天时间内完全借助 AI 完成了这个应用的开发,而且已经上架苹果App Store。
以下是应用截图:
感兴趣的朋友们也访问下面的链接或者上App Store搜索 ”音之梦“ 下载体验。
我做了哪些事情?
工具准备
开发工具使用的是Cursor + XCode,开发语言选的 Swift UI,模型自然选择最适合编码的Claude 3.7。
为什么不选择Trae?因为下一个开坑项目准备用Trae + Deepseek来进行效果对比。
产品设计
上面截图展示的其实是第二版, UI和交互流程是我根据产品需求仔细思考琢磨设计出来的。
而第一版则完全由AI生成的,我只是描述了我期望的功能,交互方式和UI效果都是AI来生成的,那自然和我心目中期望的差距很大,不过最开始只是想验证AI的快速编码能力,所以首次上架的还是还是用的这一版的样式,可以看下面的截图:
而因为国区上架需要备案,在等待备案的过程中,我又诞生了很多新的想法,加上对于第一版的UI和交互流程也是越看越不爽,所以就重新思考了整个应用的UI和交互流程,并重新用figma画了设计稿,然后交由AI来实现。
当然每个人的审美和需求都不一样,也并不是每个人都有不错的UI和交互设计能力,对于大部分人来说现阶段的AI设计水平已经是能满足需要了的。
开发过程
使用AI来进行开发,那最重要的就是提示词,而如何编写提示词来让AI更了解你的需求、尽可能不走弯路去实现,其实是很不容易的。
这里我的经验是,先自己思考清楚,用markdown整理好需求,包括主要功能、需要有哪些页面、每个页面的大致布局,以及一些需要额外强调的细节等等,然后让AI先根据你整理的需求文档来描述一下它对这个需求的理解,也可以让它反过来问你一些在需求文档无法确定的问题,补全到文档中,直到他能八九不离十的把你想要的结果描述出来。
此外,你也可以先在chat模式里面简单一句话描述需求,选择claude 3.7 thinking模型,或者deepseek r1模型,然后你们俩一起交流来把需求逐步完善和明确。
需求明确之后,也不要马上就让AI直接开始开发,因为如果整个过程很长的话,大模型目前的上下文肯定是不够的,就算是基于codebase,后续也必然会丢失细节,甚至完全忘记了你们之前定好的需求。
这里的建议是,先让AI根据需求文档把目录和文件创建好,并为每个代码文件建立一个md文件,用于标记每个代码文件里面包含的关键变量、方法名、和其他模块的依赖关系等,这样相比完整的代码文件,数据量要小很多,后续觉得大模型忘事儿了,就让他根据md来进行分析,这要比让他分析完整的代码文件要靠谱很多。另外在后续开发过程中也一定要让AI及时更新md文件。
可以在Cursor的规则配置文件中明确上面的要求。
由于Cursor中的Claude 3.7不支持输入图片作为参考,所以如果你需要基于现有的设计图进行开发,可以先选择Claude 3.5,传入参考图,让它先帮你把不带交互的UI代码实现,然后再使用claude 3.7 来进一步完善设计的业务逻辑。
开发过程中,每一次迭代可能都大幅改动之前已经实现的部分,所以切记一定要及时git commit,尤其是已经完成了某个小功能之后,一定要提交代码!这也是我使用AI进行全流程开发的最大的教训!
音频资源的获取
这个App中有很多音频资源,包括轻音乐、环境声、白噪声等,在以往,个人开发者要获取这些资源其实是很费时间的,而且需要付出的成本也比较高,但是随着AI的发展,现在获取这些资源已经变得非常容易了。
比如轻音乐,我用的是Suno AI来生成的,十几美元一个月,就能生成足够多的轻音乐,而且质量还不错,可以满足大部分场景的需求。
白噪声,则是让AI帮我编写的nodejs脚本来生成的,直接本地生成mp3音乐文件。
环境声、物品音效之类的,可以使用Eleven Lab来生成,价格也很便宜,不过我这里是先用的开源项目Moodist中的资源,而且也对作者进行了捐赠支持。
另外,在讲这些音频资源打包到应用的时候,从体积角度考虑,我对这些音频都做了压缩,在以往可能需要找一些格式工厂之类的软件来完成,现在直接让AI帮你基于Macos内置的音频处理模块生成命令行脚本,轻松完成格式转换和比率压缩,非常方便。
非AI的部分
虽然我全程几乎没有写一行代码,但是还是有一些非AI的部分,需要手动来完成。
比如应用的启动图标设计、App Store上架资料的准备、关键词填写、技术支持网址、隐私协议内容、应用截图的准备等等,虽然这其中有一些也能借助AI辅助进行,但是最终还是免不了要手动进行处理。
隐私协议内容可以让AI生成,不过一定要自己过一遍,而技术支持网站,可以用在线问卷的形式,不用自己准备网站。
在App Store上架审核的时候,也需要时刻关注审核进度和反馈,一般来说新手第一次上架审核就过审的概率很低,苹果那边也有很多规范或者要求甚至是红线,需要踩坑多次才能了解清楚。我之前已经上架过好几款应用了,这一次提审第一次没过居然是因为内购项目忘记一并提审了,笑死,不然就是一把过了,后面更新的几个版本最快半小时不到就过审了。
另外还有国区上架备案的问题,实际要准备的资料也不多,流程其实也不复杂,但是就是需要等待,而且等待的时间不确定,我这次等了近5天才通过。
有朋友可能会咨询安卓版的问题,我只能说一言难尽,目前安卓的上架流程和资质要求对独立开发者太不友好了,不确定项目有商业价值之前,不建议考虑安卓(就算是出海上google play,也比上苹果应用商店麻烦多了)。
总结
以往我作为全栈工程师,在开发产品的时候,编码始终是我最核心的工作,而这一次,我最重要的编码过程交给了 AI,我则是充当了产品设计师和测试工程师的角色,短短几天,我已经体会了很多专职产品设计师在和开发人员沟通时候的感受,也体会到了测试工程师在测试产品时候的感受,这确实是一个非常有趣和有意义的过程。
作为产品设计师,我需要能够准备描述需求、考虑到尽可能多的场景和细节,才能让 AI 更加敏捷和高质量的完成需求的开发,而作为测试工程师,我需要学会如何准确地描述问题的表现和复现步骤,才能让 AI 更加精准的给出解决方案。
虽然我确实没有写一行代码,但是在开发过程中,遇到一些复杂场景或者问题,Cursor 也会原地踏步甚至把问题越改越严重,这个时候还是需要我去分析一下它实现的代码,跳出它的上下文来来给他提示,然后他就会恍然大悟一般迅速解决问题,这确确实实依赖了我多年来的开发经验和直觉,我相信在开发复杂应用的时候,这是必不可少的。
而且开发 App 是一回事,上架 App 又是另外一回事了,备案、审核、隐私协议准备、上架配图准备等等等等,这些可能要花的时间比开发的时间还长。
在这次实践的过程中,我虽然是借助了自己的以往的独立开发经验解决了很多问题,并因此缩短了从开始开发到真正完成审核上架的周期,但我相信最核心的编码问题已经完全能交给AI了的话,那对于大部想要做一个自己的应用的人来说,真正的技术门槛确实已经不存在了。
因此我可以对大家说,准备好面对一个人人都能编程做应用的新时代吧!
再回到关于零编程经验用 AI 三天开发 App 登榜 App Store这个话题,我只能说,这确实是一个非常吸引眼球的话题,但是它也确实存在一定的误导性,不管用 AI 还是纯人工编码,做出来好的 APP 和成为爆款 APP 其实是两回事。
事实上我体验过一些此类爆款产品,在产品完成度和交互设计上实际上还很初级,甚至可以说很粗糙,但是它们却能够获得巨大的成功,除了运气和时机之外,营销上的成功其实是更重要的。
需要清醒认识的是,App Store 榜单是产品力、运营策略和市场机遇的综合产物。作为开发者,更应该关注 AI 如何重构我们的能力边界,而非简单对标营销案例。
最后再说点
总有人会问AI会不会取代程序员,我觉得不会,被AI淘汰的,只会是那些害怕使用AI,不愿意学习使用AI的人。我相信经常逛掘金的朋友们也都是不甘于只做一颗螺丝钉的,快让借助AI来拓展你的能力边界吧,
最后再再再说一句
一定要记得及时git commit
!!!
来源:juejin.cn/post/7484530047866355766
🔥🔥什么?LocalStorage 也能被监听?为什么我试了却不行?
引言:最近,团队的伙伴需要实现监听
localStorage
数据变化,但开发中却发现无法直接监听。
在团队的一个繁重项目中,我们发现一个新功能的实现成本很大,因此开始思考:能否通过实时监听 LocalStorage 的变化并自动触发相关操作。我们尝试使用 addEventListener
来监听 localStorage
的变化,但令人意外的是,这种方法仅在不同浏览器标签页之间有效,而在同一标签页内却无法实现监听。这是怎么回事?

经过调研了解到,浏览器确实提供了 storage
事件机制,但它仅适用于同源的不同标签页之间。对于同一标签页内的 LocalStorage 变化,却没有直接的方法来实现实时监听。最初,我们考虑使用 setInterval
进行定时轮询来获取变化,但这种方式要么导致性能开销过大,要么无法第一时间捕捉到变化。
今天,我们探讨下几种高效且实用的解决方案,是否可以帮助轻松应对LocalStorage
这种监听需求?希望对你有所帮助,有所借鉴!
传统方案的痛点🎯🎯
先来看看浏览器是如何帮助我们处理不同页签的 LocalStorage 变化:
window.addEventListener("storage", (event) => {
if (event.key === "myKey") {
// 执行相应操作
}
});
通过监听 storage
事件,当在其他页签修改 LocalStorage 时,你可以在当前页签捕获到这个变化。但问题是:这种方法只适用于跨页签的 LocalStorage 修改,在同一页签下无法触发该事件。于是,很多开发者开始寻求替代方案,比如:
1、轮询(Polling)
轮询是一种最直观的方式,它定期检查 localStorage
的值是否发生变化。然而,这种方法性能较差,尤其在高频轮询时会对浏览器性能产生较大的影响,因此不适合作为长期方案。
let lastValue = localStorage.getItem('myKey');
setInterval(() => {
const newValue = localStorage.getItem('myKey');
if (newValue !== lastValue) {
lastValue = newValue;
console.log('Detected localStorage change:', newValue);
}
}, 1000); // 每秒检查一次
这种方式实现简单,不依赖复杂机制。但是性能较差,频繁轮询会影响浏览器性能。
2、监听代理(Proxy)或发布-订阅模式
这种方式通过创建一个代理来拦截 localStorage.setItem
的调用。每次数据变更时,我们手动发布一个事件,通知其他监听者。
(function() {
const originalSetItem = localStorage.setItem;
const subscribers = [];
localStorage.setItem = function(key, value) {
originalSetItem.apply(this, arguments);
subscribers.forEach(callback => callback(key, value));
};
function subscribe(callback) {
subscribers.push(callback);
}
subscribe((key, value) => {
if (key === 'myKey') {
console.log('Detected localStorage change:', value);
}
});
localStorage.setItem('myKey', 'newValue');
})();
这种比较灵活,可以用于复杂场景。但是需要手动拦截 setItem
,维护成本高(但也是值得推荐的)。

然而,这些方案往往存在性能问题或者开发的复杂度,在高频数据更新的情况下,有一定的性能问题,而且存在一定的风险性。那么有没有可以简单快速,风险性还小的方案呢?
高效的解决方案 🚀🚀
既然浏览器不支持同一页签的 storage
事件,我们可以手动触发事件,以此来实现同一页签下的 LocalStorage 变化监听。
1、自定义 Storage 事件
通过手动触发 StorageEvent
,你可以在 LocalStorage 更新时同步分发事件,从而实现同一页签下的监听。
localStorage.setItem('myKey', 'value');
// 手动创建并分发 StorageEvent
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
url: window.location.href
});
window.dispatchEvent(storageEvent);
你可以使用相同的监听逻辑来处理数据变化,无论是同一页签还是不同页签:
window.addEventListener("storage", (event) => {
if (event.key === "myKey") {
// 处理 LocalStorage 更新
}
});
这种实现简单、轻量、快捷。但是需要手动触发事件。
2、基于 CustomEvent
的自定义事件
与 StorageEvent
类似,你可以使用 CustomEvent
手动创建并分发事件,实现 localStorage
的同步监听。
localStorage.setItem('myKey', 'newValue');
const customEvent = new CustomEvent('localStorageChange', {
detail: { key: 'myKey', value: 'newValue' }
});
window.dispatchEvent(customEvent);
这种方式适合更加灵活的事件触发场景。CustomEvent
不局限于 localStorage
事件,可以扩展到其他功能。
window.addEventListener('localStorageChange', (event) => {
const { key, value } = event.detail;
if (key === 'myKey') {
console.log('Detected localStorage change:', value);
}
});
3、MessageChannel(消息通道)
MessageChannel
API 可以在同一个浏览器上下文中发送和接收消息。我们可以通过 MessageChannel
将 localStorage
的变化信息同步到其他部分,起到类似事件监听的效果。
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
console.log('Detected localStorage change:', event.data);
};
localStorage.setItem('myKey', 'newValue');
channel.port2.postMessage(localStorage.getItem('myKey'));
适合组件通信和复杂应用场景,消息机制较为灵活。相对复杂的实现,可能不适合简单场景。
4、BroadcastChannel
BroadcastChannel
提供了一种更高级的浏览器通信机制,允许多个窗口或页面之间广播消息。你可以通过这个机制将 localStorage
变更同步到多个页面或同一页面的不同部分。
const channel = new BroadcastChannel('storage_channel');
channel.onmessage = (event) => {
console.log('Detected localStorage change:', event.data);
};
localStorage.setItem('myKey', 'newValue');
channel.postMessage({ key: 'myKey', value: 'newValue' });
支持跨页面通信,方便在不同页面间同步数据,易于实现。适用场景较为具体,通常用于复杂的页面通信需求。
这4个方法,主打的就是一个见缝插针,简单快速,风险性低。但是客观角度来讲,每种方案都是有各自优势的。
优势对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
轮询 | 实现简单,适合低频监控需求 | 性能差,频繁轮询影响浏览器性能 | 简单场景或临时方案 |
监听代理/发布-订阅模式 | 灵活扩展,适合复杂项目 | 需要手动拦截 setItem ,维护成本高 | 需要手动事件发布的场景 |
自定义 StorageEvent | 实现简单,原生支持 storage 事件监听 | 需要手动触发事件 | 同页签下 localStorage 监听 |
自定义事件 | 灵活的事件管理,适合不同场景 | 需要手动触发事件 | 需要自定义触发条件的场景 |
MessageChannel | 适合组件通信和复杂应用场景 | 实现复杂,不适合简单场景 | 高级组件通信需求 |
BroadcastChannel | 跨页面通信,适合复杂通信需求 | 使用场景较具体 | 复杂的多窗口通信 |
如何在 React / Vue 使用
在主流前端框架(如 React 和 Vue)中,监听 LocalStorage 变化并不困难。无论是 React 还是 Vue,你都可以使用自定义的 StorageEvent
或其他方法来实现监听。在此,我们以自定义 StorageEvent
为例,展示如何在 React 和 Vue 中实现 LocalStorage 的监听。

1. 在 React 中使用自定义 StorageEvent
React 是一个基于组件的框架,我们可以使用 React 的生命周期函数(如 useEffect
)来监听和处理 LocalStorage 的变化。
import React, { useEffect } from 'react';
const LocalStorageListener = () => {
useEffect(() => {
// 定义 storage 事件监听器
const handleStorageChange = (event) => {
if (event.key === 'myKey') {
console.log('Detected localStorage change:', event.newValue);
}
};
// 添加监听器
window.addEventListener('storage', handleStorageChange);
// 模拟触发自定义的 StorageEvent
const triggerCustomStorageEvent = () => {
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
newValue: 'newValue',
url: window.location.href,
});
window.dispatchEvent(storageEvent);
};
// 组件卸载时移除监听器
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []); // 空依赖数组表示该 effect 只会在组件挂载时运行
return (
<div>
<button onClick={() => localStorage.setItem('myKey', 'newValue')}>
修改 localStorage
</button>
<button onClick={() => window.dispatchEvent(new StorageEvent('storage', {
key: 'myKey',
newValue: localStorage.getItem('myKey'),
url: window.location.href,
}))}>
手动触发 StorageEvent
</button>
</div>
);
};
export default LocalStorageListener;
useEffect
是 React 的一个 Hook,用来处理副作用,在这里我们用它来注册和清除事件监听器。- 我们手动触发了
StorageEvent
,以便在同一页面中监听 LocalStorage 的变化。
2. 在 Vue 中使用自定义 StorageEvent
在 Vue 3 中,我们可以使用 onMounted
和 onUnmounted
这两个生命周期钩子来管理事件监听器。(Vue 3 Composition API):
<template>
<div>
<button @click="updateLocalStorage">修改 localStorage</button>
<button @click="triggerCustomStorageEvent">手动触发 StorageEvent</button>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'myKey') {
console.log('Detected localStorage change:', event.newValue);
}
};
const updateLocalStorage = () => {
localStorage.setItem('myKey', 'newValue');
};
const triggerCustomStorageEvent = () => {
const storageEvent = new StorageEvent('storage', {
key: 'myKey',
newValue: 'newValue',
url: window.location.href,
});
window.dispatchEvent(storageEvent);
};
onMounted(() => {
window.addEventListener('storage', handleStorageChange);
});
onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange);
});
</script>
- 使用了 Vue 的 Composition API,其中
onMounted
和onUnmounted
类似于 React 的useEffect
,用于在组件挂载和卸载时管理副作用。 - 同样手动触发了
StorageEvent
来监听同一页面中的 LocalStorage 变化。
提炼封装一下 🚀🚀
无论是 React 还是 Vue,将自定义 StorageEvent
实现为一个组件或工具函数是常见的做法。你可以将上面的逻辑提取到一个独立的 hook 或工具函数中,方便在项目中多次使用。
在 React 中提取为 Hook
import { useEffect } from 'react';
const useLocalStorageListener = (key, callback) => {
useEffect(() => {
const handleStorageChange = (event) => {
if (event.key === key) {
callback(event.newValue);
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [key, callback]);
};
export default useLocalStorageListener;
在 Vue 中提取为工具函数
import { onMounted, onUnmounted } from 'vue';
export const useLocalStorageListener = (key: string, callback: (value: string | null) => void) => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key) {
callback(event.newValue);
}
};
onMounted(() => {
window.addEventListener('storage', handleStorageChange);
});
onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange);
});
};
- 在 React 中,我们创建了一个自定义 Hook
useLocalStorageListener
,通过传入监听的 key 和回调函数来捕获 LocalStorage 的变化。 - 在 Vue 中,我们创建了一个工具函数
useLocalStorageListener
,同样通过传入 key 和回调函数来监听变化。
总结

在同一个浏览器页签中监听 localStorage
的变化并非难事,但不同场景下需要不同的方案。从简单的轮询到高级的 BroadcastChannel
,本文介绍的几种方案各有优缺点。根据你的实际需求,选择合适的方案可以帮助你更高效地解决问题。
- 简单需求:可以考虑使用自定义
StorageEvent
或CustomEvent
实现监听。 - 复杂需求:对于更高级的场景,如跨页面通信,
MessageChannel
或BroadcastChannel
是更好的选择。
如果你有其他的优化技巧或问题,欢迎在评论区分享,让我们一起交流更多的解决方案!
来源:juejin.cn/post/7418117491720323081
CSS换行最容易出现的bug,半天被提了两个😭😭
引言
大家好,我是石小石!
文字超出换行应该是大家项目中高频的css样式了,它也是非常容易出现样式bug的一个地方。
分享一下我工作中遇到的问题。前几天开发代码的时候,突然收到两个测试提的bug:
bug的内容大致就是我的文字换行出现了问题
我的第一反应就是线上代码不是最新的,因为自测的时候,我注意过这个问题,我在本地还测试过
然而经过验证,最后我还是被打脸了,确实是自己的问题!
问题原因分析
在上述的问题代码中,我没有做任何换行的规则
.hover-content {
max-width: 420px;
max-height: 420px;
}
因此,此时弹框内的换行规则遵循的是浏览器的默认换行规则(white-space:normal
):
浏览器换行遵循 单词完整性优先 的规则,即尽可能不在单词或数字序列内部断行;而中文是固定宽度字符,每个汉字视为独立的可断点,因此换行非常自然,展示不下浏览器会将其移动到下一行。
那么,出现上述bug的原因就非常简单了,基于浏览器的默认换行规则,这种胡乱输入、没有规则的连续纯英文或数字不换行,而汉字会换行。
white-space:normal
指定文本能够换行,是css的默认值,后文我们会继续讲解
解决方案
解决上述问题其实非常简单,一行css就可以解决🤗
word-break: break-all
word-break: break-all
可以打破浏览器的默认规则,允许在任何字符间断行(包括英文单词和数字序列)。
word-break
- 作用:指定如何对单词进行断行。
- 取值:
normal
(默认值):使用浏览器默认规则,中文按字断行,英文按单词断行。break-all
:强制在任何字符间断行(适用于中文、英文、数字)。keep-all
:中文按字断行,英文和数字不允许在单词或数字中断行。
与换行关联的css属性
除了word-break,你可能还会对white-space、word-wrap有疑问,他们与文本换行又有什么关系呢?
white-space
white-space大家一定不陌生,做文本超出显示...的时候,它是老熟人了。
white-space: nowrap; /* 禁止文本换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 使用省略号表示溢出内容 */
white-space
用于指定如何处理元素内的空白字符和如何控制文本的换行。简单来说,它的作用就是是否应该允许文本自动换行,它的默认值normal,代表文本允许换行。
所有能够换行的文本,一定拥有此默认属性white-space:normal,如果你设置nowrap,那么不管是中文还是数字或者英文,都是不会出现换行的。
white-space的换行遵循的是单词完整性优先 的规则,如果我们要使单词可以在其内部被截断,就需要使用 overflow-wrap、word-break 或 hyphens。
word-break我们已经说过了,我们介绍下
overflow-wrap
这个属性原本属于微软扩展的一个非标准、无前缀的属性,叫做 word-wrap,后来在大多数浏览器中以相同的名称实现。目前它已被更名为 overflow-wrap,word-wrap 相当于其别称。
作用:控制单词过长时是否允许断行。
常用值:
normal
:单词超出容器宽度时不换行。break-word
:允许在单词中断行以防止溢出。anywhere
:类似break-word
,但优先级更高。
实际开发中,overflow-wrap:break-word的效果同word-break: break-all
但他们存在一点点差异
换行方式:
overflow-wrap: break-word
允许在单词内部进行断行,但会尽量保持单词的完整性。word-break: break-all
则强制在任意字符间进行换行,不考虑单词的完整性。
因此,使用verflow-wrap: break-word 会比word-break: break-all更好一些!
推荐实践
通过本文,相信大家对css文本换行有了新的认识,个人比较推荐的实践如下:
- 中文为主:可以默认使用
word-break: normal;
。 - 中英文混排:
overflow-wrap: break-word;
。 - 主要为英文或数字:需要强制换行时,使用
word-break: break-all;
。
- 中文为主:可以默认使用
考虑到场景的复杂新,大家可以word-break: break-all走天下。
来源:juejin.cn/post/7450110698728816655
token泄漏产生的悲剧!Vant和Rspack被注入恶意代码,全网大面积被感染
一、事件
2024年12月19号,在前端圈发生了一件匪夷所思的事情,前端开源项目Vant
的多个版本被注入恶意代码后,发布到了npm
上,导致全网大面积被感染。
随后Vant
团队人员出来发声说:经排查可能是我们团队一名成员的 token 被盗用,他们正在紧急处理。 What?token
还能被别人盗用的么,这安全性真的是差点惊掉我的下巴。
然后Vant
团队人员废弃了有问题的版本,并在几个大版本2、3、4上都发布了新的安全版本2.13.6
、3.6.16
、4.9.15
,我刚试了下,现在使用npm i vant@latest
安装的是最新的4.9.15
版本,事件就算是告一段落了。
二、关联事件:Rspack躺枪
攻击者拿到了vant成员的token
后,进一步拿到了同个GitHub
组织下另一个成员的token
,并发布了同样带有恶意代码的Rspack@1.1.7
版本。
这里简单介绍下Rspack
,它是一个基于Rust
编写打的高性能javascript
打包工具,相比于webpack
、rollup
等打包工具,它的构建性能有很大的提升,是字节团队为了解决构建性能的问题而研发的,后开源在github
。
Rspack
这波属实是躺枪了,不过Rspack
团队反应很快,已经在一小时内完成该版本的废弃处理,并发布了1.1.8
修复版本,字节的问题处理速度还是可以的。目前Rspack
的1.1.7
版本在npm上已经删除了,无法安装。
三、带来的影响
Vant
作为一个老牌的国产移动端组件库,由有赞团队负责开发和维护,在github
上已经拥有23.4k的Star
,是一款很优秀的组件库,其在国内的前端项目中应用是非常广泛的,几乎是开发H5项目的首选组件库。vant
官方目前提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,微信小程序版本本次不受影响,遭受攻击的是Vue2
和Vue3
版本。
如果在发布恶意版本还未修复的期间,正好这时候有项目发布安装到了这些恶意版本,那后果不堪设想。要知道Vant
可是面向用户端(C端)的组件库,其杀伤力是十分大的。
我们公司很多前端移动端项目也用了Vant
组件库,不过我们项目都用了package-lock.json
,所以对于我们来说问题不大,这里也简单介绍下package-lock.json
,也推荐大家都用一下。
四、package-lock.json介绍
比如你在package.json
中写了一个依赖^3.7.0
,你用npm install
安装到了3.7.0
版本,然后过了一段时间后,你同事把这个代码克隆到本地,然后也执行npm install
,由于此时依赖包已经更新到了3.8.0
版本,所以此时你同事安装到的是3.8.0
版本。
这时候问题就来了,这个依赖的开发团队“不讲武德”,在3.8.0
对一个API
做了改动,并且做好向后兼容,于是代码报错了,项目跑不起来了,你同事找了半天,发现是依赖更新了,很无语,浪费半天开发时间,又得加班赶项目了!
按理来说,npm install
就应该向纯函数
(相同的输入产生相同的输入,无副作用的函数)一样,产出相同node_modules
,然而依赖是会更新的,这会导致npm install
产出的结果不一样,而如果依赖开发人员不按规范升级版本,或者升级后的新版本有bug,尽管业务代码一行没改,项目再次发布时也可能会出现问题。
package-lock.json
就是为了解决这个问题的,在npm install
的时候,会根据package.json
中的依赖版本,默认生成一份package-lock.json
文件,你可以把这个lock文件
上传到git
仓库上,下次npm install
的时候,会根据一定规则选择最终安装的版本:
- npm v5.0.x版本:不管package.json文件中的依赖项是否有更新,都会按照package-lock.json文件中的依赖版本进行下载。
- npm v5.1.0 - v5.4.2:当package.json的依赖版本有符合的更新版本时,会忽略package-lock.json,按照package.json安装,并更新package-lock.json文件。
- npm v5.4.2以上:当package.json声明的的依赖版本与package-lock.json中的版本兼容时,会按照package-lock.json的版本安装,反之,如果不兼容则按照package.json安装,并更新package-lock.json文件。
npm v5.4.2
这个版本已经很旧了,我们用的npm版本几乎都在这之上,所以如果用了package-lock.json
文件,应该就能避免这个问题,不要怕麻烦,我觉得为了项目的稳定性,这个还是需要用的。
这个事件就介绍到这里了,对此大家怎么看呢?
来源:juejin.cn/post/7450080841546121243
你知道JS中有哪些“好用到爆”的一行代码?
哈喽,各位小伙伴们,你们好呀,我是喵手。
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
偶尔帮同事开发点前端页面,每次写代码,总会遇到一些能让人直呼nb的代码。今天,我们就来盘点一下那些 “好用到爆”的 JavaScript 一行代码。省去复杂的多行代码,直接用一行或者简洁的代码进行实现。也能在同事面前秀一波(当然是展示技术实力,而不是装X 🤓)。
也许你会问:“一行代码真的能有这么强吗?” 别急,接着往下看,保证让你大呼—— 这也行?! 哈哈,待看完之后,你可能会心一笑,原来一行代码还能发挥的如此优雅!核心就是要简洁高效快速实现。
目录
- 妙用之美:一行代码的魅力
- 实用案例:JS 一行代码提升开发效率
- 生成随机数
- 去重数组
- 检查变量类型
- 深拷贝对象
- 交换两个变量的值
- 生成 UUID
- 延伸知识:一行代码背后的原理
- 总结与感悟
妙用之美:一行代码的魅力
为什么“一行代码”如此让人着迷?因为它是 简洁、高效、优雅 的化身。在日常开发中,我们总希望能用更少的代码实现更多的功能,而“一行代码”就像是开发者智慧的结晶,化繁为简,带来极致的编码体验。
当然,别以为一行代码就等同于简单。事实上,这些代码往往利用了 JavaScript 中的高级技巧,比如 ES6+ 的特性、函数式编程的思维、甚至对底层机制的深入理解。它们既是技巧的体现,也是对语言掌控力的证明。
接下来,让我们通过一些实用案例,感受“一行代码”的高优雅吧!
实用案例:JS 一行代码提升开发效率
1. 生成随机数
在日常开发中,生成随机数是非常常见的需求。但是我可以一句代码就能搞定,示例如下:
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
用法示例:
console.log(random(1, 100)); // 输出 1 到 100 之间的随机整数
解析:代码核心是 Math.random()
,它生成一个 0 到 1 的随机数。通过数学公式将其映射到指定范围,并利用 Math.floor()
确保返回的是整数。
2. 数组去重
数组去重的方法有很多种,但下面这种方式极其优雅,不信你看!
const unique = (arr) => [...new Set(arr)];
用法示例:
console.log(unique([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]
解析:Set
是一种集合类型,能自动去重。而 ...
是扩展运算符,可以将 Set
转换为数组,省去手动遍历的步骤。
3. 检查变量类型
判断变量类型也是日常开发中的常见操作,但是下面这一行代码就足够满足你的需求:
const type = (value) => Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
用法示例:
console.log(type(123)); // 'number'
console.log(type([])); // 'array'
console.log(type(null)); // 'null'
解析:通过 Object.prototype.toString
可以准确获取变量的类型信息,而 slice(8, -1)
是为了提取出 [object Type]
中的 Type
部分。
4. 深拷贝对象
经常会碰到拷贝的场景,但是对于需要深拷贝的对象,下面的一行代码简单且高效:
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
用法示例:
const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj);
console.log(copy); // { a: 1, b: { c: 2 } }
注意:这种方法不适用于循环引用的对象。如果需要处理复杂对象,建议使用 Lodash
等库。
5. 交换两个变量的值
日常中,如果是传统写法,可能会采用需要引入临时变量,但是,今天,我可以教你一个新写法,使用解构赋值就简单多了:
let a = 1, b = 2;
[a, b] = [b, a];
用法示例:
console.log(a, b); // 2, 1
解析:利用 ES6 的解构赋值语法,可以轻松实现两个变量的值交换,代码简洁且直观。
6. 生成 UUID
这个大家都不陌生,且基本所有的项目中都必须有,UUID ,它是开发中常用的唯一标识符,下面这段代码可以快速生成一个符合规范的 UUID,对,就一行搞定:
const uuid = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return c === 'x' ? r.toString(16) : ((r & 0x3) | 0x8).toString(16);
});
用法示例:
console.log(uuid()); // 类似 'e4e6c7c4-d5ad-4cc1-9be8-d497c1a9d461'
解析:通过正则匹配字符 x
或 y
,并利用 Math.random()
生成随机数,再将其转换为符合 UUID 规范的十六进制格式。
延伸知识
如上这些“一行代码”的实现主要得益于 ES6+ 的特性,如:
- 箭头函数:让函数表达更简洁。
- 解构赋值:提升代码的可读性。
- 扩展运算符:操作数组和对象时更加优雅。
- Set 和 Map:提供高效的数据操作方式。
所以说,深入理解这些特性,不仅能让你更轻松地掌握这些代码,还能将它们灵活地应用到实际开发中,在日常开发中游刃有余,用最简洁的代码实现最复杂的也无需求。
总结与感悟
一行代码的背后,藏着开发者的智慧和对 JavaScript 代码的深入理解。通过这些代码,我们不仅能提升开发效率,还能在细节中感受代码的优雅与美感,这个也是我们一致的追求。
前端开发的乐趣就在于此——简单的代码,却能带来无限可能。如果你有更好用的一行代码,欢迎分享,让我们一起玩耍 JavaScript 的更多妙用!体验其中的乐趣。
... ...
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
... ...
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
来源:juejin.cn/post/7444829930175905855
做定时任务,一定要用这个神库!!
说实话,作为前端开发者,我们经常需要处理一些定时任务,比如轮询接口、定时刷新数据、自动登出等功能。
过去我总是用 setTimeout
和 setInterval
,但这些方案在复杂场景下并不够灵活。
我寻找了更可靠的方案,最终发现了 cron 这个 npm 包,为我的前端项目(特别是 Node.js 环境下运行的那部分)带来了专业级的定时任务能力。
cron 包:不只是个定时器
安装超级简单:
npm install cron
基础用法也很直观:
import { CronJob } from 'cron';
const job = new CronJob(
'0 */30 * * * *', // 每30分钟执行一次
function () {
console.log('刷新用户数据...');
// 这里放刷新数据的代码
},
null, // 完成时的回调
true, // 是否立即启动
'Asia/Shanghai' // 时区
);
看起来挺简单的,对吧?
但这个小包却能解决前端很多定时任务的痛点。
理解 cron 表达式,这个"魔法公式"
刚开始接触 cron 表达式时,我觉得这简直像某种加密代码。* * * * * *
这六个星号到底代表什么?
在 npm 的 cron 包中,表达式有六个位置(比传统的 cron 多一个),分别是:
秒 分 时 日 月 周
比如 0 0 9 * * 1
表示每周一早上 9 点整执行。
我找到一个特别好用的网站 crontab.guru 来验证表达式。
不过注意,那个网站是 5 位的表达式,少了"秒"这个位置,所以用的时候需要自己在前面加上秒的设置。
月份和星期几还可以用名称来表示,更直观:
// 每周一、三、五的下午5点执行
const job = new CronJob('0 0 17 * * mon,wed,fri', function () {
console.log('工作日提醒');
});
前端开发中的实用场景
作为前端开发者,我在这些场景中发现 cron 特别有用:
1. 在 Next.js/Nuxt.js 等同构应用中刷新数据缓存
// 每小时刷新一次产品数据缓存
const cacheRefreshJob = new CronJob(
'0 0 * * * *',
async function () {
try {
const newData = await fetchProductData();
updateProductCache(newData);
console.log('产品数据缓存已更新');
} catch (error) {
console.error('刷新缓存失败:', error);
}
},
null,
true,
'Asia/Shanghai'
);
2. Electron 应用中的定时任务
// 在 Electron 应用中每5分钟同步一次本地数据到云端
const syncJob = new CronJob(
'0 */5 * * * *',
async function () {
if (navigator.onLine) {
// 检查网络连接
try {
await syncDataToCloud();
sendNotification('数据已同步');
} catch (err) {
console.error('同步失败:', err);
}
}
},
null,
true
);
3. 定时检查用户会话状态
// 每分钟检查一次用户活动状态,30分钟无活动自动登出
const sessionCheckJob = new CronJob(
'0 * * * * *',
function () {
const lastActivity = getLastUserActivity();
const now = new Date().getTime();
if (now - lastActivity > 30 * 60 * 1000) {
console.log('用户30分钟无活动,执行自动登出');
logoutUser();
}
},
null,
true
);
踩过的那些坑
使用 cron 包时我踩过几个坑,分享给大家:
- 时区问题:有次我设置了一个定时提醒功能,但总是提前 8 小时触发。一查才发现是因为没设置时区。所以国内用户一定要设置
'Asia/Shanghai'
!
// 这样才会在中国时区的下午6点执行
const job = new CronJob('0 0 18 * * *', myFunction, null, true, 'Asia/Shanghai');
- this 指向问题:如果你用箭头函数作为回调,会发现无法访问 CronJob 实例的 this。
// 错误示范
const job = new CronJob('* * * * * *', () => {
console.log('执行任务');
this.stop(); // 这里的 this 不是 job 实例,会报错!
});
// 正确做法
const job = new CronJob('* * * * * *', function () {
console.log('执行任务');
this.stop(); // 这样才能正确访问 job 实例
});
- v3 版本变化:如果你从 v2 升级到 v3,要注意月份索引从 0-11 变成了 1-12。
实战案例:构建一个智能通知系统
这是我在一个电商前端项目中实现的一个功能,用 cron 来管理各种用户通知:
import { CronJob } from 'cron';
import { getUser, getUserPreferences } from './api/user';
import { sendNotification } from './utils/notification';
class NotificationManager {
constructor() {
this.jobs = [];
this.initialize();
}
initialize() {
// 新品上架提醒 - 每天早上9点
this.jobs.push(
new CronJob(
'0 0 9 * * *',
async () => {
if (!this.shouldSendNotification('newProducts')) return;
const newProducts = await this.fetchNewProducts();
if (newProducts.length > 0) {
sendNotification('新品上架', `今天有${newProducts.length}款新品上架啦!`);
}
},
null,
true,
'Asia/Shanghai'
)
);
// 限时优惠提醒 - 每天中午12点和晚上8点
this.jobs.push(
new CronJob(
'0 0 12,20 * * *',
async () => {
if (!this.shouldSendNotification('promotions')) return;
const promotions = await this.fetchActivePromotions();
if (promotions.length > 0) {
sendNotification('限时优惠', '有新的限时优惠活动,点击查看详情!');
}
},
null,
true,
'Asia/Shanghai'
)
);
// 购物车提醒 - 每周五下午5点提醒周末特价
this.jobs.push(
new CronJob(
'0 0 17 * * 5',
async () => {
if (!this.shouldSendNotification('cartReminder')) return;
const cartItems = await this.fetchUserCart();
if (cartItems.length > 0) {
sendNotification('周末将至', '别忘了查看购物车中的商品,周末特价即将开始!');
}
},
null,
true,
'Asia/Shanghai'
)
);
console.log('通知系统已初始化');
}
async shouldSendNotification(type) {
const user = getUser();
if (!user) return false;
const preferences = await getUserPreferences();
return preferences?.[type] === true;
}
// 其他方法...
stopAll() {
this.jobs.forEach(job => job.stop());
console.log('所有通知任务已停止');
}
}
export const notificationManager = new NotificationManager();
写在最后
作为前端开发者,我们的工作不只是构建漂亮的界面,还需要处理各种复杂的交互和时序逻辑。
npm 的 cron 包为我们提供了一种专业而灵活的方式来处理定时任务,特别是在 Node.js 环境下运行的前端应用(如 SSR 框架、Electron 应用等)。
它让我们能够用简洁的表达式设定复杂的执行计划,帮助我们构建更加智能和用户友好的前端应用。
来源:juejin.cn/post/7486390904992890895
Browser.js:轻松模拟浏览器环境
什么是Browser.js
Browser.js是一个小巧而强大的JavaScript库,它模拟浏览器环境,使得原本只能在浏览器中运行的代码能够在Node.js环境中执行。这意味着你可以在服务器端运行前端代码,而不需要依赖真实浏览器
Browser.js的核心原理
Browser.js通过实现与浏览器兼容的API(如window
、document
、navigator
等)来创建一个近似真实的浏览器上下文。它还支持fetch
API用于网络请求,支持Promise,使得异步操作更加方便
Browser.js的用途
Browser.js主要用于以下场景:
- 服务器端测试:在服务端运行前端单元测试,无需依赖真实浏览器,从而提高测试效率
// 示例:使用Browser.js进行服务器端测试
const browser = require('browser.js');
const window = browser.window;
const document = browser.document;
// 在Node.js中模拟浏览器环境
console.log(window.location.href);
- 构建工具:编译或预处理只能在浏览器运行的库,例如基于DOM的操作,如CSS处理器或模板引擎
// 示例:使用Browser.js处理CSS
const browser = require('browser.js');
const document = browser.document;
// 创建一个CSS样式表
const style = document.createElement('style');
style.textContent = 'body { background-color: #f2f2f2; }';
document.head.appendChild(style);
- 离线应用:将部分业务逻辑放在客户端和服务端之外,在本地环境中执行1。
// 示例:使用Browser.js在本地环境中执行业务逻辑
const browser = require('browser.js');
const window = browser.window;
// 在本地环境中执行JavaScript代码
window.alert('Hello, World!');
- 自动化脚本:对网页进行自动化操作,如爬虫、数据提取等,而不必依赖真实浏览器1。
// 示例:使用Browser.js进行网页爬虫
const browser = require('browser.js');
const fetch = browser.fetch;
// 发送HTTP请求获取网页内容
fetch('https://example.com')
.then(response => response.text())
.then(html => console.log(html));
解决的问题
Browser.js解决了以下问题:
- 跨环境执行:使得原本只能在浏览器中运行的JavaScript代码能够在Node.js环境中执行,扩展了JavaScript的应用边界
- 兼容性问题:通过模拟浏览器环境,减少了不同浏览器之间的兼容性问题,提高了代码的可移植性
- 测试效率:提高了前端代码在服务端的测试效率,减少了对真实浏览器的依赖
Browser.js的特点
- 轻量级:体积小,引入方便,不会过多影响项目整体性能
- 兼容性:模拟的浏览器环境高度兼容Web标准,能够运行大部分浏览器代码
- 易用性:提供简单直观的API接口,快速上手
- 可扩展:支持自定义插件,可以根据需求扩展功能
- 无依赖:不依赖其他大型库或框架,降低项目复杂度
来源:juejin.cn/post/7486845198485585935
Vue 首个 AI 组件库发布!
人工智能(AI)已深度融入我们的生活,如今 Vue 框架也迎来了首个 AI 组件库 —— Ant Design X Vue,为开发者提供了强大的 AI 开发工具。
Ant Design X Vue 概述
Ant Design X Vue 是基于 Vue.js 的 AI 组件库,旨在简化 AI 集成开发。
它包含高度定制化的 AI 组件和 API 解决方案,支持无缝接入 AI 服务,是构建智能应用的理想选择。
组件库亮点
丰富多样的 AI 组件
通用组件:
- Bubble:显示会话消息气泡,支持多种布局。
- Conversations:管理多个会话,查看历史记录。
唤醒组件:
- Welcome:会话加载时插入欢迎语。
- Prompts:展示上下文相关的问题或建议。
表达组件:
- Sender:构建会话输入框,支持自定义样式。
- Attachments:展示和管理附件信息。
- Suggestion:提供快捷输入提示。
确认组件:
- ThoughtChain:展示 AI 的思维过程或结果。
工具组件:
- useXAgent:对接 AI 模型推理服务。
- useXChat:管理 AI 对话应用的数据流。
- XStream:处理数据流,支持流式传输。
- XRequest:向 AI 服务发起请求。
- XProvider:全局化配置管理。
RICH 设计范式
基于 RICH 设计范式,提供丰富、沉浸式、连贯和人性化的交互体验,适应不同 AI 场景。
AGI 混合界面(Hybrid-UI)
融合 GUI 和自然会话交互,用户可在同一应用中自由切换交互方式,提升体验。
适用场景
- 智能聊天应用:构建多轮对话界面,支持复杂会话逻辑。
- 企业级 AI 系统:快速搭建智能客服、知识管理等系统。
如何使用 Ant Design X Vue
安装与引入
npm install ant-design-x-vue --save
引入组件库及样式:
import Vue from 'vue';
import Antd from 'ant-design-x-vue';
import 'ant-design-x-vue/dist/antd.css';
Vue.use(Antd);
使用组件
示例:使用 Bubble 组件展示对话气泡
<template>
<div>
<a-bubble content="欢迎使用 Ant Design X Vue!" />
</div>
</template>
官方文档与示例
访问 Ant Design X Vue 官方文档:https://antd-design-x-vue.netlify.app/
获取更多信息。
Ant Design X Vue 为 Vue 开发者提供了强大的 AI 组件库,助力高效构建智能应用。
无论是聊天应用
还是企业级系统
,都值得一试。
最后
如果觉得本文对你有帮助,希望能够给我点赞支持一下哦 💪 也可以关注wx公众号:
前端开发爱好者
回复加群,一起学习前端技能 公众号内包含很多实战
精选资源教程,欢迎关注
来源:juejin.cn/post/7475978280841543716