注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

跨窗口通信的九重天劫:从postMessage到BroadcastChannel

web
跨窗口通信的九重天劫:从postMessage到BroadcastChannel 第一重:postMessage 基础劫 —— 安全与效率的平衡术 // 父窗口发送 const child = window.open('child.html'); child...
继续阅读 »

跨窗口通信的九重天劫:从postMessage到BroadcastChannel




第一重:postMessage 基础劫 —— 安全与效率的平衡术


// 父窗口发送
const child = window.open('child.html');
child.postMessage({ type: 'AUTH_TOKEN', token: 'secret' }, 'https://your-domain.com');

// 子窗口接收
window.addEventListener('message', (e) => {
if (e.origin !== 'https://parent-domain.com') return;
console.log('收到消息:', e.data);
});

安全守则



  1. 始终验证origin属性

  2. 敏感数据使用JSON.stringify + 加密

  3. 使用transfer参数传递大型二进制数据(如ArrayBuffer)




第二重:MessageChannel 双生劫 —— 高性能私有通道


// 建立通道
const channel = new MessageChannel();

// 端口传递
parentWindow.postMessage('INIT_PORT', '*', [channel.port2]);

// 接收端处理
channel.port1.onmessage = (e) => {
console.log('通过专用通道收到:', e.data);
};

// 发送消息
channel.port1.postMessage({ priority: 'HIGH', payload: data });

性能优势



  • 相比普通postMessage减少50%的序列化开销

  • 支持传输10MB以上文件(Chrome实测)




第三重:BroadcastChannel 广播劫 —— 同源全域通信


// 发送方
const bc = new BroadcastChannel('app-channel');
bc.postMessage({ event: 'USER_LOGOUT' });

// 接收方
const bc2 = new BroadcastChannel('app-channel');
bc2.onmessage = (e) => {
if (e.data.event === 'USER_LOGOUT') {
localStorage.clear();
}
};

适用场景



  • 多标签页状态同步

  • 全局事件通知系统

  • 跨iframe配置更新




第四重:SharedWorker 共享劫 —— 持久化通信枢纽


// worker.js
const connections = [];
onconnect = (e) => {
const port = e.ports[0];
connections.push(port);

port.onmessage = (e) => {
connections.forEach(conn => {
if (conn !== port) conn.postMessage(e.data);
});
};
};

// 页面使用
const worker = new SharedWorker('worker.js');
worker.port.start();
worker.port.postMessage('来自页面的消息');

内存管理



  • 每个SharedWorker实例共享同一个全局作用域

  • 需要手动清理断开连接的端口




第五重:localStorage 事件劫 —— 投机取巧的同步


// 页面A
localStorage.setItem('sync-data', JSON.stringify({
timestamp: Date.now(),
data: '重要更新'
}));

// 页面B
window.addEventListener('storage', (e) => {
if (e.key === 'sync-data') {
const data = JSON.parse(e.newValue);
console.log('跨页更新:', data);
}
});

致命缺陷



  • 事件仅在其他页面触发

  • 同步API导致主线程阻塞

  • 无法传递二进制数据




第六重:IndexedDB 观察劫 —— 数据库驱动通信


// 建立观察者
let lastVersion = 0;
const db = await openDB('msg-db', 1);

db.transaction('messages')
.objectStore('messages')
.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (cursor && cursor.value.version > lastVersion) {
lastVersion = cursor.value.version;
handleMessage(cursor.value);
}
};

// 写入新消息
await db.add('messages', {
version: Date.now(),
content: '新订单通知'
});

适用场景



  • 需要持久化保存的通信记录

  • 离线优先的跨窗口消息队列




第七重:Window.name 穿越劫 —— 上古秘术


// 页面A
window.name = JSON.stringify({ session: 'temp123' });
location.href = 'pageB.html';

// 页面B
const data = JSON.parse(window.name);
console.log('穿越传递:', data);

安全警告



  • 数据暴露在所有同源页面

  • 最大容量约2MB

  • 现代应用已不建议使用




第八重:Server-Sent Events (SSE) 服务劫 —— 服务器中转


// 服务端(Node.js)
app.get('/updates', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
setInterval(() => {
res.write(`data: ${Date.now()}\n\n`);
}, 1000);
});

// 浏览器端
const es = new EventSource('/updates');
es.onmessage = (e) => {
allWindows.forEach(w => w.postMessage(e.data));
};

架构优势



  • 支持跨设备同步

  • 自动重连机制

  • 与WebSocket互补(单向vs双向)




第九重:WebSocket 广播劫 —— 实时通信终极形态


// 共享连接管理
const wsMap = new Map();

function connectWS() {
const ws = new WebSocket('wss://push.your-app.com');

ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'BROADCAST') {
broadcastToAllTabs(data.payload);
}
};

return ws;
}

// 页面可见性控制
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
ws.close();
} else {
ws = connectWS();
}
});

性能优化



  • 心跳包维持连接(每30秒)

  • 消息压缩(JSON → ArrayBuffer)

  • 退避重连策略




渡劫指南(技术选型矩阵)


graph LR
A[是否需要持久化?] -->|是| B[IndexedDB]
A -->|否| C{实时性要求}
C -->|高| D[WebSocket]
C -->|中| E[BroadcastChannel]
C -->|低| F[postMessage]
B --> G[是否需要跨设备?]
G -->|是| H[SSE/WebSocket]
G -->|否| I[localStorage事件]



天劫问答



  1. 如何防止跨窗口消息风暴?



    • 采用消息节流(throttle)

    • 使用window.performance.now()标记时序

    • 实施优先级队列



  2. 哪种方式最适合微前端架构?



    • BroadcastChannel全局通信 + postMessage父子隔离



  3. 如何实现跨源安全通信?



    • 使用iframe作为代理中继

    • 配合CORS和document.domain设置






调试工具推荐



  1. Charles - 抓取WebSocket消息

  2. Window Query - 查看所有窗口对象

  3. Postman - 模拟SSE事件流


性能检测代码


// 通信延迟检测
const start = performance.now();
channel.postMessage('ping');
channel.onmessage = () => {
console.log('往返延迟:', performance.now() - start);
};

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

你可能不知道的前端18个冷知识

web
今天带大家盘点一下前端的一些冷知识。 一、浏览器地址栏的妙用 1.1 可以执行javascript代码 在地址栏中输入javascript:alert('hello world'),然后按回车键,会弹出一个提示框显示hello world。 注意:如果直接把...
继续阅读 »

今天带大家盘点一下前端的一些冷知识。


一、浏览器地址栏的妙用


1.1 可以执行javascript代码


在地址栏中输入javascript:alert('hello world'),然后按回车键,会弹出一个提示框显示hello world



注意:如果直接把这段代码复制到地址栏,浏览器会删除掉前面javascript:(比如谷歌浏览器、edge浏览器等),需要自己手动加上。



还可以使用location.hrefwindow.open来执行它。


location.href = "javascript:alert('hello world')";
window.open("javascript:alert('hello world')");

1.2 可以运行html


在地址栏中输入data:text/html,<div>hello world</div>,然后按回车键,会显示一个包含hello world的div元素。


利用这个能力,我们可以把浏览器标签页变成一个编辑器。


contenteditable属性能把一个元素变成可编辑的,所以我们如果在地址栏中输入data:text/html,<html contenteditable>,就可以把页面直接变成一个编辑器了。你还可以把它收藏到书签,以后直接点击就可以打开一个编辑器了。


二、把整个在线网页变成可编辑


只需要在浏览器控制台中输入这样一行代码,就能把整个页面变成可编辑的。


document.body.contentEditable = 'true';

这样我们就能随便修改页面了,比如修改页面中的文字、图片等等,轻松实现修改账户余额去装逼!


三、利用a标签解析URL


const a = document.createElement('a');
a.href = 'https://www.baidu.com/s?a=1&b=1#hash';
console.log(a.host); // http://www.baidu.com
console.log(a.pathname); // /s
console.log(a.search); // ?a=1&b=1
console.log(a.hash); // #hash

四、HTML的ID和全局变量的映射关系


在HTML中,如果有一个元素的id是a,那么在全局作用域中,会有一个变量a,这个变量指向这个元素。


<div id="a"></div>
<script>
console.log(a); // <div id="a"></div>
</script>

如果id重复了,还是会生成一个全局变量,但是这个变量指向的是一个HTMLCollection类数组。


<div id="a">a</div>
<div id="a">b</div>
<script>
console.log(a); // HTMLCollection(2) [div#a, div#a]
</script>

五、cdn加载省略协议头


<script src="//cdn.xxx.com/xxx.js"></script>

src的值以//开头,省略了协议,则在加载js时,会使用当前页面的协议进行加载。


如果当前页面是https则以https进行加载。
如果当前页面是http则以http进行加载。
如果当前页面是ftp则以ftp进行加载。


六、前端的恶作剧:隐藏鼠标光标


<style>
* {
cursor: none !important;
}
</style>

直接通过css把光标隐藏,让人哭笑不得。


七、文字模糊效果


前端文本的马赛克效果,可以使用text-shadow实现。


<style>
.text {
color: transparent;
text-shadow: #111 0 0 5px;
user-select: none;
}
</style>

<span>hello</span><span class="text">world</span>

效果如下:



八、不借助js和css,让元素消失


直接用DOM自带的hidden属性即可。


<div hidden>hello world</div>

九、保护隐私


禁用F12快捷键:


document.addEventListener('keydown', (e) => {
if (e.keyCode === 123) {
e.preventDefault();
}
})

禁用右键菜单:


document.addEventListener('contextmenu', (e) => {
e.preventDefault();
})

但即使通过禁用F12快捷键和右键菜单,用户依然可以通过其它方式打开控制台。



  1. 通过浏览器菜单选项直接打开控制台:比如 chrome浏览器通过 菜单 > 更多工具 > 开发者工具 路径可以打开控制台,Firefox/Edge/Safari 等浏览器都有类似选项。

  2. 用户还可以通过其它快捷键打开控制台:



  • Cmd+Opt+I (Mac)

  • Ctrl+Shift+C (打开检查元素模式)


十、css实现三角形


<style>
.triangle {
width: 0;
height: 0;
border: 20px solid transparent;
border-top-color: red;
}
</style>

<div class="triangle"></div>

十一、为啥 a === a-1 结果为true


aInfinity无穷大时,a - 1的结果也是Infinity,所以a === a - 1的结果为true


同理,a的值为-Infinity时,此等式也成立。


const a = Infinity;
console.log(a === a - 1);

十二、数字的包装类


console.log(1.toString()); // 报错
console.log(1..toString()); // 正常运行 输出字符串'1'

十三、防止网站以 iframe 方式被加载


if (window.location !== window.parent.location) window.parent.location = window.location;

十四、datalist的使用


datalistHTML5 中引入的一个新元素,它用于为<input>元素提供预定义的选项列表。就是当用户在下拉框输入内容时,浏览器会显示一个下拉列表,列表的内容就是与当前输入内容相匹配的 datalist 选项。


<input list="fruits" name="fruit" />
<datalist id="fruits">
<option value="苹果"></option>
<option value="橘子"></option>
<option value="香蕉"></option>
</datalist>

效果如下:



十五、文字纵向排列


<style>
.vertical-text {
writing-mode: vertical-rl;
text-orientation: upright;
}
</style>

<div class="vertical-text">文字纵向排列</div>

效果如下:



十六、禁止选中文字


document.addEventListener('selectstart', (e) => {
e.preventDefault();
})

效果跟使用 css 的 user-select: none 效果类似。


十七、利用逗号,在一行中执行多个表达式


let a = 1;
let b = 2;
(a += 2), (b += 3);

十八、inset


inset是一个简写属性,用于同时设置元素的 toprightbottomleft 属性


.box {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}

可以简写成:


.box {
position: absolute;
inset: 0;
}

小结


以上就是前端的18个冷知识,希望大家看完都有所收获。


作者:程序员小寒
来源:juejin.cn/post/7502059146641784883
收起阅读 »

产品:我要的是“五彩斑斓的黑”

故事的小黄花: 「“这个VIP按钮不够尊贵,我要那种黑中透着高级感,最好带点若隐若现的紫金色!”产品经理指着设计稿,眼神中闪烁着“五彩斑斓的期待”。 🖤 」 我盯着纯黑的按钮陷入沉思——这需求听起来像在为难我胖虎,但转念一想🤔,自己的产品经理,肯定得自己来宠...
继续阅读 »

故事的小黄花:



「“这个VIP按钮不够尊贵,我要那种黑中透着高级感,最好带点若隐若现的紫金色!”产品经理指着设计稿,眼神中闪烁着“五彩斑斓的期待”。 🖤 」


我盯着纯黑的按钮陷入沉思——这需求听起来像在为难我胖虎,但转念一想🤔,自己的产品经理,肯定得自己来宠着啦



Snipaste_2025-05-06_14-14-43.png



「几小时后,当按钮在黑暗中浮现暗紫流光时,产品经理惊呼:“对对对!这就是我想要的低调奢华!”」



Snipaste_2025-05-06_14-12-04.png


一、技术解析:如何让黑色“暗藏玄机”?


1. 核心代码一览


<!-- 产品经理说这里要五彩斑斓的黑 🖤 -->
<button class="btn-magic">黑紫VIP</button>

.btn-magic {
background:
linear-gradient(45deg,
#000 25%,
rgba(90, 0, 127, 0.3) 40%, /* 暗紫 */
rgba(0, 10, 80, 0.3) 60%, /* 墨蓝 */
#000 75%
);
background-size: 500% 500%;
animation: shimmer 8s infinite linear;
color: white;
}

@keyframes shimmer {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}

2. 代码逐层拆解


代码部分作用说明视觉隐喻
linear-gradient(45deg)45度对角线渐变,比水平/垂直更动态让色彩“流动”起来
rgba(90, 0, 127, 0.3)透明度0.3的暗紫色,叠加黑色不突兀黑中透紫,神秘感+1
background-size:500%放大背景尺寸,制造移动空间为动画预留“跑道”
shimmer动画背景位置循环位移,形成无限流动效果仿佛黑夜中的极光



vip卡片.gif


PS:动图效果有些掉帧


二、效果升级:让按钮更“灵动”的秘籍


1. 悬浮微交互


.btn-magic {
transition: transform 0.3s, box-shadow 0.3s;
}
.btn-magic:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(90, 0, 127, 0.5); /* 紫色投影,具体效果微调 */
}

效果:悬浮时按钮轻微上浮+投影扩散,可配合swiper使用点击突出效果 🧚♂️


vip卡片2.gif


2. 文字流光


.btn-magic {
position: relative;
overflow: hidden;
}
.btn-magic::after {
content: "VIP";
position: absolute;
background: linear-gradient(90deg, transparent, #fff, transparent);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: textShine 3s infinite;
}
@keyframes textShine {
0% { opacity: 0; left: -50%; }
50% { opacity: 1; }
100% { opacity: 0; left: 150%; }
}

效果:文字表面划过一道白光,尊贵感拉满! ✨


3. 性能优化


/* 开启GPU加速 */
.btn-magic {
transform: translateZ(0);
backface-visibility: hidden;
}
/* 减少动画负荷 */
@media (prefers-reduced-motion: reduce) {
.btn-magic { animation: none; }
}

原理:避免重绘,尊重用户设备偏好。




三、设计思维:如何把“离谱需求”变成亮点?


1. 需求翻译


产品经理原话前端工程师理解技术实现方案
“五彩斑斓的黑”动态深色渐变+微交互反馈CSS渐变+动画
“要高级感”低饱和度辅色+精致细节暗紫/墨蓝+悬浮投影
“用户一眼能看到VIP”文字强调设计流光文字+居中放大

2. 参数可配置化(方便产品经理AB测试)


/* 定义CSS变量 */
:root {
--main-color: #000;
--accent-purple: rgba(90, 0, 127, 0.3);
--accent-blue: rgba(0, 10, 80, 0.3);
}
.btn-magic {
background: linear-gradient(45deg,
var(--main-color) 25%,
var(--accent-purple) 40%,
var(--accent-blue) 60%,
var(--main-color) 75%
);
}

玩法:通过切换变量值,快速生成“暗金奢华版”“深蓝科技版”等风格。




四、效果对比:从“煤炭”到“黑钻石”


指标优化前(纯黑卡片)优化后(流光卡片)
产品反馈“按钮太普通”“看起来就很贵”
Lighthouse评分性能99,视觉效果70性能98,视觉效果95 ↑

image.png


vip卡片.gif


五、灵魂总结:


「当产品经理提出“五彩斑斓的黑”时(我透他猴子****),他真正想要的是用户的“情绪价值”

作为前端,我们要做的不是争论RGB能否合成黑色(我日他****),而是用技术将想象力转化为体验,
毕竟,最好的黑不是#000000,而是让用户忍不住想点击的“故事感”。」


下次再见!🌈


Snipaste_2025-04-27_15-18-02.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7500874757706350619
收起阅读 »

Day.js 与 Moment.js 比较

web
Day.js 与 Moment.js 的比较 优点 体积小:Day.js 的体积仅为 2KB 左右,而 Moment.js 的体积约为 67KB。 API 相似:Day.js 的 API 与 Moment.js 高度相似,迁移成本低。 不可变性:Day.js...
继续阅读 »

Day.js 与 Moment.js 的比较


优点



  • 体积小:Day.js 的体积仅为 2KB 左右,而 Moment.js 的体积约为 67KB。

  • API 相似:Day.js 的 API 与 Moment.js 高度相似,迁移成本低。

  • 不可变性:Day.js 的日期对象是不可变的,这意味着每次操作都会返回一个新的日期对象,避免了意外的副作用。


缺点



  • 功能较少:Day.js 的功能相对 Moment.js 较少,特别是在处理时区和复杂日期操作时。

  • 插件依赖:一些高级功能(如时区支持)需要通过插件实现,增加了额外的依赖。


定位与设计理念



  • Moment.js


image.png
- 老牌时间处理库,2012 年发布,曾是 JavaScript 时间处理的事实标准,功能全面且语法直观。
- 设计目标:覆盖几乎所有时间处理需求,包括复杂的时区、本地化、格式化、操作等。
- 现状:2020 年进入 维护模式(不再新增功能,仅修复严重 bug),官方推荐迁移至更现代的库(如 Day.js、Luxon 等)。


image.png



  • Day.js



    • 轻量替代方案,2018 年发布,设计灵感直接来源于 Moment.js,语法高度相似,但更简洁轻量。

    • 设计目标:通过最小化核心功能 + 插件机制,提供常用时间操作能力,避免过度设计。

    • 现状:持续活跃更新,由单一开发者维护,社区支持度快速增长。





核心差异对比


维度Moment.jsDay.js
体积约 40KB+ (完整版本),包含大量功能模块。仅 2KB(核心库),插件按需引入,体积极小。
API 设计功能全面(如 localeData()utcOffset()tz() 等),部分高级功能略显复杂。极简 API,保留高频操作(如 format()add()diff() 等),链式调用风格与 Moment 一致,学习成本低。
功能完整性原生支持时区(需单独引入 moment-timezone 插件)、复杂本地化、相对时间、ISO 8601 等,无需额外依赖。核心库仅包含基础功能,时区(需 dayjs-plugin-timezone 插件)、本地化(需 dayjs/plugin/locales)等需手动安装插件,灵活性高但需配置。
性能解析和操作大型时间数据时性能中等,体积大导致加载速度较慢。轻量核心 + 按需加载,解析和操作速度更快,尤其在移动端或高频时间处理场景优势明显。
浏览器支持兼容 IE 8+ 及现代浏览器,对旧版浏览器友好。依赖 ES6+(如 PromiseProxy),支持现代浏览器(Chrome 49+, Firefox 52+, 等),不支持 IE。
生态与社区生态成熟,周边工具丰富(如 Webpack 插件、React 组件等),但更新停滞。生态快速发展中,主流框架(如 Vue、React)适配良好,插件系统完善(官方维护 20+ 插件)。
维护状态进入维护模式,仅安全更新,无新功能。活跃维护,定期发布新版本,快速响应社区需求。

Dayjs中文文档


dayjs.uihtm.com/


如何将 Moment.js 替换为 Day.js


1. 安装 Day.js


首先,安装 Day.js:


npm install dayjs


2. 替换导入语句


将项目中的 Moment.js 导入语句替换为 Day.js:


// 将
import moment from 'moment';

// 替换为
import dayjs from 'dayjs';


3. 替换 API 调用


将 Moment.js 的 API 调用替换为 Day.js 的等效调用。由于两者的 API 非常相似,大多数情况下只需简单替换即可:


// Moment.js
const date = moment('2023-10-01');
console.log(date.format('YYYY-MM-DD'));

// Day.js
const date = dayjs('2023-10-01');
console.log(date.format('YYYY-MM-DD'));


4. 处理差异


在某些情况下,Day.js 和 Moment.js 的行为可能略有不同。你需要根据具体情况调整代码。例如,Day.js 的 diff 方法返回的是毫秒数,而 Moment.js 返回的是天数:


// Moment.js
const diff = moment('2023-10-02').diff('2023-10-01', 'days'); // 1

// Day.js
const diff = dayjs('2023-10-02').diff('2023-10-01', 'day'); // 1


5. 引入插件(可选)


如果你需要使用 Day.js 的高级功能(如时区支持),可以引入相应的插件:


5. 总结:如何选择?



  • 选 Moment.js:如果项目依赖其成熟生态、需要兼容旧浏览器,或时间逻辑极其复杂且不愿配置插件。

  • 选 Day.js:如果追求轻量、高性能、简洁 API,且能接受通过插件扩展功能(推荐新项目使用)。


import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';

dayjs.extend(utc);
dayjs.extend(timezone);

const date = dayjs().tz('America/New_York');
console.log(date.format('YYYY-MM-DD HH:mm:ss'));


总结:


两者语法高度相似,迁移成本低。若项目对体积和性能敏感,Day.js 是更优解;若功能全面性和旧项目兼容更重要,Moment.js 仍可短期使用,但长期建议迁移至活跃库(如 Day.js 或 Luxon)。


作者:天天码行空
来源:juejin.cn/post/7499005521116545062
收起阅读 »

鸿蒙中的长列表「LazyForEach」:起猛了,竟然在鸿蒙系统上看到了「RecyclerView」?

web
声明式UI && 命令式UI 传统的命令式UI编程范式中,开发者需要明确地指示系统如何一步一步地构建和更新UI,手动处理每一个UI更新和状态变化,随着应用复杂度增加,管理UI和状态同步变得更加困难。所以声明式UI应运而生,它的出现就是为了简化U...
继续阅读 »

声明式UI && 命令式UI


传统的命令式UI编程范式中,开发者需要明确地指示系统如何一步一步地构建和更新UI,手动处理每一个UI更新和状态变化,随着应用复杂度增加,管理UI和状态同步变得更加困难。所以声明式UI应运而生,它的出现就是为了简化UI开发,减少手动管理状态和UI更新的复杂性。现代前端框架(Jetpack Compose、SwiftUI)都采用了声明式UI的编程范式。


在声明式UI编程范式中,开发者不再手动构建、更新UI,而是「描述界面应该是什么样子的」:开发者定义界面状态,然后框架会根据状态自动更新UI。


相对于命令式UI,声明式UI更加简洁和易于维护,但缺乏了灵活性——开发者无法完全控制UI更新的粒度。所以声明式UI的性能是一大挑战,尤其是复杂长列表场景下的性能问题。


为了解决长列表的渲染问题,Jetpack Compose 提供了LazyColumnLazyRow等组件,SwiftUI也有ListLazyVStack等组件。作为鸿蒙系统的UI体系ArkUI自然也有用于长列表的组件LazyForEach



LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。



LazyForEach用法


本文就针对ArkUI中的LazyForEach来探究一二。


LazyForEach 的渲染依赖IDataSourceDataChangeListener,我们一个一个来看下:


IDataSource


LazyForEach 的数据获取、更新都是通过IDataSource来完成的:



  • totalCount(): number 获得数据总数

  • getData(index: number): Object获取索引值index对应的数据

  • registerDataChangeListener(listener: DataChangeListener)注册数据改变的监听器

  • unregisterDataChangeListener(listener: DataChangeListener)注销数据改变的监听器


DataChangeListener


DataChangeListener,官方定义其为数据变化监听器,用于通知LazyForEach组件数据更新。除掉已废弃的方法外,共有以下几个方法:



  • onDataReloaded()通知组件重新加载所有数据。键值没有变化的数据项会使用原先的子组件,键值发生变化的会重建子组件。重新加载数据完成后调用。

  • onDataAdd(index: number)通知组件index的位置有数据添加。添加数据完成后调用

  • onDataMove(from: number, to: number)通知组件数据有移动。将from和to位置的数据进行交换。数据移动起始位置与数据移动目标位置交换完成后调用。

  • onDataDelete(index: number)通知组件删除index位置的数据并刷新LazyForEach的展示内容。删除数据完成后调用。

  • onDataChange(index: number)通知组件index的位置有数据有变化。改变数据完成后调用。

  • onDatasetChange(dataOperations: DataOperation[])进行批量的数据处理,该接口不可与上述接口混用。批量数据处理后调用。


披着马甲的RecyclerView?




这...这不对吧?你给我干哪儿来了?这还是国内么?





相信大部分Android开发者看到LazyForEach的API都是这样两眼一黑:这...这确定不是RecyclerView?连API都能一一对应上:



  • DataChangeListener.onDataReloaded() -> RecyclerView.Adapter.notifyDataSetChanged()

  • DataChangeListener.onDataAdd() -> RecyclerView.Adapter.notifyItemInserted()

  • DataChangeListener.onDataDelete() -> RecyclerView.Adapter.notifyItemRangeRemoved()

  • DataChangeListener.onDataChange() -> RecyclerView.Adapter.notifyItemChanged()


一个简单的demo


我们写一个简单的长列表来体验下鸿蒙的LazyForEach用法:页面顶部3个按钮对应列表的增、删、改功能,列表的item显示当前item的index,数据源部分代码如下:


class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: string[] = [];

public totalCount(): number {
return 0;
}

public getData(index: number): string {
return this.originDataArray[index];
}

// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}

// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}

// 通知LazyForEach组件需要重载所有子组件
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded();
})
}

// 通知LazyForEach组件需要在index对应索引处添加子组件
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
})
}

// 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
})
}

// 通知LazyForEach组件需要在index对应索引处删除该子组件
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
})
}

// 通知LazyForEach组件将from索引和to索引处的子组件进行交换
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to);
})
}
}


export class MyDataSource extends BasicDataSource {
private dataArray: string[] = [];

public totalCount(): number {
return this.dataArray.length;
}

public getData(index: number): string {
return this.dataArray[index];
}

public addData(index: number, data: string): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}

public pushData(data: string): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}

public deleteData(index: number): void {
this.dataArray.splice(index, 1);
this.notifyDataDelete(index);
}

public changeData(index: number, data: string): void {
this.dataArray.splice(index, 1, data);
this.notifyDataChange(index);
}
}

UI部分正常使用LazyForEach展示数据即可:


@Entry
@Component
struct Index {

private data: MyDataSource = new MyDataSource();

aboutToAppear(): void {
for (let i = 0; i <= 4; i++) {
this.data.pushData(`index ${i}`)
}
}

build() {

Column() {

Button('add')
.borderRadius(8)
.backgroundColor(0x317aff)
.margin({top: 12, left: 20, right: 20})
.width(360)
.height(40)
.onClick(() => {
const lastIndex = this.data.totalCount()
this.data.addData(lastIndex, `index ${lastIndex}`)
})

Button('remove')
.borderRadius(8)
.backgroundColor(0xF55A42)
.margin({top: 12, left: 20, right: 20})
.width(360)
.height(40)
.onClick(() => {
const lastIndex = this.data.totalCount()
this.data.notifyDataMove(lastIndex - 1, lastIndex - 1)
})

List({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
Row() {
Text(item)
.fontSize(40)
.textAlign(TextAlign.Center)
.width('100%')
.height(55)
.borderRadius(8)
.backgroundColor(0xF5F5F5)
.onAppear(() => {
console.info("appear:" + item)
})
}.margin({ left: 10, right: 10 , top: 10 })
}
}, (item: string) => item)
}.cachedCount(5)
.width('100%')
.height('auto')
.layoutWeight(1)
}.width('100%')
.height('100%')
}
}

demo功能也很简单:



  • 点击add按钮在列表底部添加新元素

  • 点击remove按钮删除列表底部最后一个元素

  • 点击update按钮在将第一个元素文案更新为index new 0





那如果是复杂的数据更新操作呢?


比如列表原来的数据为 ['Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e'],经过一系列变化后需要调整成['Hello x', 'Hello 1', 'Hello 2', 'Hello b', 'Hello c', 'Hello e', 'Hello d'],这时候如何更新UI展示?


此时就需要用到onDatasetChange(dataOperations: DataOperation[])API了:


#BasicDataSource
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 1);
}
}

notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
})
}
}

#MyDataSource
class MyDataSource extends BasicDataSource {

private dataArray: string[] = ['Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e'];

public operateData(): void {
this.dataArray =
['Hello x', 'Hello 1', 'Hello 2', 'Hello b', 'Hello c', 'Hello e', 'Hello d']
this.notifyDatasetChange([
{ type: DataOperationType.CHANGE, index: 0 },
{ type: DataOperationType.ADD, index: 1, count: 2 },
{ type: DataOperationType.EXCHANGE, index: { start: 3, end: 4 } },
]);
}
}

复杂的数据操作需要我们告诉组件如何变化,以上述的例子为例:


// 修改之前的数组
['Hello a', 'Hello b', 'Hello c', 'Hello d', 'Hello e']
// 修改之后的数组
['Hello x', 'Hello 1', 'Hello 2', 'Hello b', 'Hello c', 'Hello e', 'Hello d']


  • 第一个元素从'Hello a'变为'Hello x',因此第一个operation为{ type: DataOperationType.CHANGE, index: 0 }

  • 新增了元素'Hello 1'和'Hello 2',下标为1和2,所以第二个operation为{ type: DataOperationType.ADD, index: 1, count: 2 }

  • 元素'Hello d'和'Hello e'交换了位置,所以第三个operation为{ type: DataOperationType.EXCHANGE, index: { start: 3, end: 4 } }


使用onDatasetChange(dataOperations: DataOperation[])API时需要注意:




  1. onDatasetChange与其它操作数据的接口不能混用。

  2. 传入onDatasetChange的operations,其中每一项operation的index均从修改前的原数组内寻找。因此,opeartions中的index跟操作Datasource中的index不是一一对应的。

  3. 调用一次onDatasetChange,一个index对应的数据只能被操作一次,若被操作多次,LazyForEach仅使第一个操作生效。

  4. 部分操作可以由开发者传入键值,LazyForEach不会再去重复调用keygenerator获取键值,需要开发者保证传入的键值的正确性。

  5. 若本次操作集合中有RELOAD操作,则其余操作全不生效。



通过@Observed 更新子组件


在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。


LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。


上述的列表更新都是依靠LazyForEach的刷新机制:当item变化时,通过将将原来的子组件全部销毁再重新构建的方式来更新子组件。这种通过改变键值去刷新的方式渲染性能较低。因此鸿蒙系统也提供了@Observed机制进行深度观测,可以做到仅刷新使用了该属性的组件,提高渲染性能。还是上面的例子,这次我们将数据源换成被@Observed修饰的类:


@Observed
class StringData {
message: string;
constructor(message: string) {
this.message = message;
}
}

@Entry
@Component
struct MyComponent {
private moved: number[] = [];
@State data: MyDataSource = new MyDataSource();

aboutToAppear() {
for (let i = 0; i <= 20; i++) {
this.data.pushData(new StringData(`Hello ${i}`));
}
}

build() {
List({ space: 3 }) {
LazyForEach(this.data, (item: StringData, index: number) => {
ListItem() {
ChildComponent({data: item})
}
.onClick(() => {
item.message += '0';
})
}, (item: StringData, index: number) => index.toString())
}.cachedCount(5)
}
}

@Component
struct ChildComponent {
@Prop data: StringData
build() {
Row() {
Text(this.data.message).fontSize(50)
.onAppear(() => {
console.info("appear:" + this.data.message)
})
}.margin({ left: 10, right: 10 })
}
}

此时点击LazyForEach子组件改变item.message时,重渲染依赖的是ChildComponent@Prop成员变量对其子属性的监听,此时框架只会刷新Text(this.data.message),不会去重建整个ListItem子组件。


实际开发时,开发者需要根据其自身业务特点选择使用哪种刷新方式:改变键值 or 通过@Observed


算是吐槽?


作为一名Android开发者,使用LazyForEach后,彷佛看到了故人之姿。用法和API设计都和RecyclerView太像了,甚至RecyclerView需要注意的用法上的问题,LazyForEach同样也有:




关于ScrollView嵌套RecyclerView使用上的问题,可以移步:


实名反对《阿里巴巴Android开发手册》中NestedScrollView嵌套RecyclerView的用法



不同的是,早期的RecyclerView出来让人惊艳:相比于它的前辈 ListView,同时通过Adapter将数据和UI隔离,设计非常灵活,可拓展性非常强。


然而使用LazyForEach时我却总有些恍惚:不是声明式UI么?不是应该描述、定义列表界面状态,然后ArkUI框架根据列表状态自动完成UI的更新么?为什么还会有DataChangeListener这种东西存在?


官方文档里也明确表示了LazyForEach不支持状态变量:





LazyForEach必须使用DataChangeListener对象进行更新,对第一个参数dataSource重新赋值会异常;dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。



猜测还是和性能有关系,所以官方也没将LazyForEach归类为容器组件而是把它划到了渲染控制模块里。不过个人觉得这种违背声明式UI的初衷,将逻辑抛给开发者的方式并不可取。


对比之下,同样是声明式UI的Compose在长列表的处理就显得优雅了许多:


var items by remember { mutableStateOf(listOf("Item 0", "Item 1", "Item 2")) }

@Composable
fun LazyColumnDemo() {
var items by remember { mutableStateOf(listOf("Item 0", "Item 1", "Item 2")) }

Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = {
items = items + "Item ${items.size}"
}) {
Text("Add Item")
}

Button(onClick = {
if (items.isNotEmpty()) {
items = items.dropLast(1)
}
}) {
Text("Remove Item")
}

Button(onClick = {
if (items.isNotEmpty()) {
items = items.toMutableList().apply {
this[0] = "new"
}
}
}) {
Text("Update First")
}
}

Spacer(modifier = Modifier.height(16.dp))

LazyColumn(
modifier = Modifier.fillMaxSize()
) {
itemsIndexed(items) { index, item ->
ListItem(index = index, text = item)
}
}
}
}

作者:沈剑心
来源:juejin.cn/post/7410590100965572643
收起阅读 »

中原银行鸿蒙版开发实战

web
一、建设背景     2024年1月18日,HarmonyOS NEXT鸿蒙星河版亮相,标志着“纯血鸿蒙”正式开始扬帆起航。同年6月21日,在华为开发者大会上HarmonyOS Next正式发布,并且将于第4季度发布商用版。     中原银行App用户中华为机...
继续阅读 »

一、建设背景


    2024年1月18日,HarmonyOS NEXT鸿蒙星河版亮相,标志着“纯血鸿蒙”正式开始扬帆起航。同年6月21日,在华为开发者大会上HarmonyOS Next正式发布,并且将于第4季度发布商用版。


    中原银行App用户中华为机型占比第一,及时兼容鸿蒙系统,能够为使用华为设备的客户提供更好的服务,同时适配鸿蒙系统也可以支持我国科技创新和提升金融系统安全性。


二、建设历程


    2024年1月,中原银行App鸿蒙版项目启动;


    2024年4月,完成整体研发工作;


    2024年6月,完成功能测试、安全测试等测试工作;


    2024年6月14日,正式在华为应用市场上架。


三、关键技术


1. 混合开发框架


    中原银行鸿蒙版应用架构为四层架构,分别为基础环境层、基础库层、混合开发框架层、业务模块层。


基础环境层: 主要是一些基础设施及环境配置,如OHPM私仓搭建;


基础库层: 主要是应用中使用的基础功能组件,如网络、加解密等;


混合开发框架层: 采用混合开发模式,各业务模块以中原银行小程序的形式开发,拥有“一次开发、多端适用”和迭代发版灵活快速等特性。基于混合开发框架,原有Android和iOS上运行的小程序可无缝运行在鸿蒙设备上,极大提高开发效率。


    为进一步优化用户体验与性能,自研JsBridge,有效降低了小程序与原生系统间交互的性能损耗,确保流畅的交互体验。同时,采用离线下载机制,将小程序代码及资源通过离线包形式预先下载至本地,配合离线包校验机制,显著提升了小程序加载速度,同时增强了小程序安全性。此外,引入预加载策略,针对公共代码进行预加载处理,并使用C语言优化资源加载逻辑,进一步提升了整体加载性能。


业务模块层: 主要是应用中各业务功能,如存款、理财、登录等。


图3.1 中原银行鸿蒙版架构图



2. 传输安全


    为满足金融app对网络传输的安全、性能及复杂业务逻辑要求,使用分层拦截器将复杂的网络请求进行加解密、gzip、防重防等功能的拆分、解耦,增加网络传输过程安全性、可靠性。其中由于鸿蒙原生密钥协商算法暂不支持国密算法,项目中引入铜锁密码库,替换鸿蒙ECDH密钥协商算法,实现了对国密SM2_256的密钥协商算法支持,满足了监管对国密算法使用的要求;针对加密zip包解压和tar包文件读取,我们定制裁剪minizip-ng和libtar开源c库,通过napi实现arkTs与C库之间的相互调用,最终完成对加密zip包解压和tar包特定文件读取的支持。


图3.2 网络分层拦截器  

图3.3 加解密流程


3. OHPM私仓搭建


    由于金融网络与互联网网络隔离,金融网络环境下无法直接访问互联网上的鸿蒙中心仓库 ohpm.openharmony.cn,导致开发环境无法正常使用,同时需要一个仓库来存放私有类库,为此我们搭建了 OHPM 私有仓库,实现了金融网络环境下 OHPM 仓库的正常使用,并且可一键安装内网专用包和外网公共包,为金融网络内鸿蒙应用开发打下坚实基础。


  具体操作为:使用OHPM 私仓搭建工具(developer.huawei.com/consumer/cn…),配置“上游”鸿蒙相关仓库地址(ohpm.openharmony.cn),通过公司内专用互联网代理通道代理到鸿蒙中心仓库。现将搭建过程遇到的部分问题总结如下:


(1)由于内网中无法申请到 HTTPS 证书,私仓无法以 HTTPS 方式部署,我们改造了 OHPM 底层网络代码,对使用 HTTPS 的“上游”仓库,改为 HTTP 代理,改造代码如下:
// 改造 ohpm 源代码,解决内网申请不了 https 证书的问题


// 文件: libs/service/uplinks/uplink-proxy/UplinkProxyService.js


// 改造 ohpm 源代码,解决内网申请不了 https 证书的问题
// 文件: libs/service/uplinks/uplink-proxy/UplinkProxyService.js
if ("https:" === t.protocol.trim()) {
const t = e.https_proxy;
// 对 https 的上游仓库,使用 http 代理
t && (o = new i.HttpProxyAgent(t));
}

(2)原版搭建工具为前台启动,可靠性低,日志难以管理。在部署过程中,我们使用了守护进程管理工具PM2用于提升服务可靠性并记录日志,配置代码如下:


// 使用 pm2 实现守护进程管理
// 文件: pm2.config.js
module.exports = {
apps: [
{
// 服务名称
name: "ohpm-repo",
// 私仓搭建工具的所在目录
cwd: "/path/to/ohpm-repo",
// 入口脚本
script: "index.js",
// 集群模式启动,提升服务可靠性
exec_mode: "cluster",
// 实例数量
instances: 2,
// 崩溃时自动重启服务
autorestart: true,
// 不需要监听文件变化
watch: false,
// 内存时重新启动
max_memory_restart: "1G",
// 将控制台日志输出到文件
error_file: "./logs/ohpm-repo-error.log",
out_file: "./logs/ohpm-repo-out.log",
merge_logs: true,
// 环境变量
env_production: {
NODE_ENV: "production",
},
},
],
};

四、鸿蒙特性实践


1. 原生智能


    鸿蒙原生系统已深度集成了多项AI能力,例如OCR识别、语音识别等。我们在个人信息设置、贷款信息录入等场景集成了鸿蒙Vision Kit组件,通过扫描身-份-证/银彳亍卡的方式录入客户信息,不仅提升了客户使用的便捷性,还确保了交易的安全性;后续还会在客户上传正件照片时集成智能PhotoPicker,当客户需要上传正件照时,系统智能地从图库中选出正件类照片优先展示,极大地提升用户使用体验;在搜索等场景集成Core Speech Kit组件,通过语音识别实现说话代替手工打字输入,使得输入操作更便捷、内容更准确,后续计划将该能力扩展至智能客服交互和老年版界面播报场景,真正地实现智能贴心服务。


2. 终端安全


鸿蒙设备为开发者提供了基于可信执行环境(TEE)的关键资产存储服务(Asset Store Kit),确保用户敏感数据无法被恶意获取和篡改。我们在可信终端识别场景,通过采集鸿蒙基础环境信息,配合相关唯一标识算法计算出设备的标识码,为防止该标识码被恶意篡改或因应用卸载重装发生变化,利用Asset Store Kit将该标识缓存于设备TEE中,再结合云端关联匹配与碰撞检测机制, 充分保证了标识码的稳定性与抗欺骗性,为应用提供了稳定、唯一与抗欺骗的可信终端识别能力。


3. har和hsp


    鸿蒙lib库分为har和hsp,har包类似正常的lib库,但是如果存在多终端发布可能会重复引用导致包体变大;hsp包为项目内可以共享的lib库,可以提高代码、资源的可重用性和可维护性。


    实践过程中发现对外提供lib库时如使用hsp须包名,版本与宿主App保持一致,否则会出现安装失败问题。通过实践总结如下:


(1)对外提供sdk要使用har包;


(2)项目内部共享的基础库使用hsp包。


4. sdk依赖


    复杂的App项目基本上都会采用分模块管理,不可避免会出现多个模块依赖同一基础库的现象。基础库升级时所有依赖此基础库的模块均需升级,此时非常容易出现个别模块遗漏升级而导致库冲突。


建议统一管理维护sdk依赖,具体操作如下:


(1)将版本信息统一放置在parameter-file.json;


(2)增加冲突解决配置,.ohpmrc中配置resolve_conflict=true,配置后系统会自动使用最新lib库版本。


五、未来展望


    展望未来,我们将深度依托鸿蒙系统的“一次开发、多端部署”核心优势,进一步拓展金融服务边界,构建跨设备、无缝连接的“1+8+N”全场景智慧金融服务生态,将服务延伸至PC、电视、智能手表、智能音箱、平板、穿戴设备、车机、耳机以及更多泛IoT设备(即“N”类设备),实现金融服务在各类智能终端上的全面覆盖与深度融合。银行网点服务侧,我们将结合鸿蒙实况窗技术,实现客户在网点排队取号时,可通过手机或智能手表实时查看排队进度,甚至提前线上完成部分业务预办理,提升服务效率与用户体验。此外,通过对接鸿蒙的意图框架,智能识别用户的信用卡还款需求,自动推送还款提醒,减少逾期风险;同时,基于用户的地理位置等信息,精准推送本地化的金融产品与服务,实现金融服务的个性化与精准化。


作者:跟着感觉走2024
来源:juejin.cn/post/7403606017308082226
收起阅读 »

11 个 JavaScript 杀手脚本,用于自动执行日常任务

web
作者:js每日一题 今天这篇文章,我将分享我使用收藏的 11 个 JavaScript 脚本,它们可以帮助您自动化日常工作的各个方面。 1. 自动文件备份 担心丢失重要文件?此脚本将文件从一个目录复制到备份文件夹,确保您始终保存最新版本。 const fs =...
继续阅读 »

作者:js每日一题


今天这篇文章,我将分享我使用收藏的 11 个 JavaScript 脚本,它们可以帮助您自动化日常工作的各个方面。


1. 自动文件备份


担心丢失重要文件?此脚本将文件从一个目录复制到备份文件夹,确保您始终保存最新版本。


const fs = require('fs');const path = require('path');
function backupFiles(sourceFolder, backupFolder) {  fs.readdir(sourceFolder, (err, files) => {    if (err) throw err;    files.forEach((file) => {      const sourcePath = path.join(sourceFolder, file);      const backupPath = path.join(backupFolder, file);      fs.copyFile(sourcePath, backupPath, (err) => {        if (err) throw err;        console.log(`Backed up ${file}`);      });    });  });}const source = '/path/to/important/files';const backup = '/path/to/backup/folder';backupFiles(source, backup);

提示:将其作为 cron 作业运行


2. 发送预定电子邮件


需要稍后发送电子邮件但又担心忘记?此脚本允许您使用 Node.js 安排电子邮件。


const nodemailerrequire('nodemailer');
function sendScheduledEmail(toEmail, subject, body, sendTime{  const delay = sendTime - Date.now();  setTimeout(() => {    let transporter = nodemailer.createTransport({      service'gmail',      auth: {        user'your_email@gmail.com',        pass'your_password', // Consider using environment variables for security      },    });    let mailOptions = {      from'your_email@gmail.com',      to: toEmail,      subject: subject,      text: body,    };    transporter.sendMail(mailOptions, function (error, info) {      if (error) {        console.log(error);      } else {        console.log('Email sent: ' + info.response);      }    });  }, delay);}// Schedule email for 10 seconds from nowconst futureTime = Date.now() + 10000;sendScheduledEmail('recipient@example.com', 'Hello!', 'This is a scheduled email.', futureTime);

注意:传递您自己的凭据


3. 监控目录的更改


是否曾经想跟踪文件的历史记录。这可以帮助您实时跟踪它。


const fs = require('fs');
function monitorFolder(pathToWatch) {  fs.watch(pathToWatch, (eventType, filename) => {    if (filename) {      console.log(`${eventType} on file: ${filename}`);    } else {      console.log('filename not provided');    }  });}monitorFolder('/path/to/watch');

用例:非常适合关注共享文件夹或监控开发目录中的变化。


4. 将图像转换为 PDF


需要将多幅图像编译成一个 PDF?此脚本使用 pdfkit 库即可完成此操作。


const fs = require('fs');const PDFDocumentrequire('pdfkit');
function imagesToPDF(imageFolder, outputPDF) {  const doc = new PDFDocument();  const writeStream = fs.createWriteStream(outputPDF);  doc.pipe(writeStream);  fs.readdir(imageFolder, (err, files) => {    if (err) throw err;    files      .filter((file) => /.(jpg|jpeg|png)$/i.test(file))      .forEach((file, index) => {        const imagePath = `${imageFolder}/${file}`;        if (index !== 0) doc.addPage();        doc.image(imagePath, {          fit: [500700],          align'center',          valign'center',        });      });    doc.end();    writeStream.on('finish'() => {      console.log(`PDF created: ${outputPDF}`);    });  });}imagesToPDF('/path/to/images''output.pdf');

提示:非常适合编辑扫描文档或创建相册。


5. 桌面通知提醒


再也不会错过任何约会。此脚本会在指定时间向您发送桌面通知。


const notifier = require('node-notifier');
function desktopNotifier(title, message, notificationTime) {  const delay = notificationTime - Date.now();  setTimeout(() => {    notifier.notify({      title: title,      message: message,      soundtrue// Only Notification Center or Windows Toasters    });    console.log('Notification sent!');  }, delay);}// Notify after 15 secondsconst futureTime = Date.now() + 15000;desktopNotifier('Meeting Reminder', 'Team meeting at 3 PM.', futureTime);

注意:您需要先安装此包:npm install node-notifier。


6. 自动清理旧文件


此脚本会删除超过 n 天的文件。


const fs = require('fs');const path = require('path');
function cleanOldFiles(folder, days) {  const now = Date.now();  const cutoff = now - days * 24 * 60 * 60 * 1000;  fs.readdir(folder, (err, files) => {    if (err) throw err;    files.forEach((file) => {      const filePath = path.join(folder, file);      fs.stat(filePath, (err, stat) => {        if (err) throw err;        if (stat.mtime.getTime() < cutoff) {          fs.unlink(filePath, (err) => {            if (err) throw err;            console.log(`Deleted ${file}`);          });        }      });    });  });}cleanOldFiles('/path/to/old/files'30);

警告:请务必仔细检查文件夹路径,以避免删除重要文件。


7. 在语言之间翻译文本文件


需要快速翻译文本文件?此脚本使用 API 在语言之间翻译文件。


const fs = require('fs');const axios = require('axios');
async function translateText(text, targetLanguage) {  const response = await axios.post('https://libretranslate.de/translate', {    q: text,    source'en',    target: targetLanguage,    format'text',  });  return response.data.translatedText;}(async () => {  const originalText = fs.readFileSync('original.txt''utf8');  const translatedText = await translateText(originalText, 'es');  fs.writeFileSync('translated.txt', translatedText);  console.log('Translation completed.');})();

注意:这使用了 LibreTranslate API,对于小型项目是免费的。


8. 将多个 PDF 合并为一个


轻松将多个 PDF 文档合并为一个文件。


const fs = require('fs');const PDFMergerrequire('pdf-merger-js');
async function mergePDFs(pdfFolder, outputPDF) {  const merger = new PDFMerger();  const files = fs.readdirSync(pdfFolder).filter((file) => file.endsWith('.pdf'));  for (const file of files) {    await merger.add(path.join(pdfFolder, file));  }  await merger.save(outputPDF);  console.log(`Merged PDFs int0 ${outputPDF}`);}mergePDFs('/path/to/pdfs''merged_document.pdf');

应用程序:用于将报告、发票或任何您想要的 PDF 合并到一个地方。


9. 批量重命名文件


需要重命名一批文件吗?此脚本根据模式重命名文件。


const fs = require('fs');const path = require('path');
function batchRename(folder, prefix) {  fs.readdir(folder, (err, files) => {    if (err) throw err;    files.forEach((file, index) => {      const ext = path.extname(file);      const oldPath = path.join(folder, file);      const newPath = path.join(folder, `${prefix}_${String(index).padStart(3, '0')}${ext}`);      fs.rename(oldPath, newPath, (err) => {        if (err) throw err;        console.log(`Renamed ${file} to ${path.basename(newPath)}`);      });    });  });}batchRename('/path/to/files''image');

提示:padStart(3, '0') 函数用零填充数字(例如,001,002),这有助于排序。


10. 抓取天气数据


通过从天气 API 抓取数据来了解最新天气情况。


const axios = require('axios');
async function getWeather(city) {  const apiKey = 'your_openweathermap_api_key';  const response = await axios.get(    `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric`  );  const data = response.data;  console.log(`Current weather in ${city}${data.weather[0].description}${data.main.temp}°C`);}getWeather('New York');

注意:您需要在 OpenWeatherMap 注册一个免费的 API 密钥。


11. 生成随机引语


此脚本获取并显示随机引语。


const axios = require('axios');
async function getRandomQuote() {  const response = await axios.get('https://api.quotable.io/random');  const data = response.data;  console.log(`"${data.content}" \n- ${data.author}`);}getRandomQuote();

最后,感谢您一直阅读到最后!希望今天内容能够帮助到你,如果你喜欢此内容的话,也请分享给你的小伙伴,也许能够帮助到他们。


作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7502855221241888805
收起阅读 »

给前端小白的科普,为什么说光有 HTTPS 还不够?为啥还要请求签名?

今天咱们聊个啥呢?先设想一个场景:你辛辛苦苦开发了一个前端应用,后端 API 也写得杠杠的。用户通过你的前端界面提交一个订单,比如说买1件商品。请求发出去,一切正常。但如果这时候,有个“不开眼”的黑客老哥,在你的请求发出后、到达服务器前,悄咪咪地把“1件”改成...
继续阅读 »

今天咱们聊个啥呢?先设想一个场景:你辛辛苦苦开发了一个前端应用,后端 API 也写得杠杠的。用户通过你的前端界面提交一个订单,比如说买1件商品。请求发出去,一切正常。但如果这时候,有个“不开眼”的黑客老哥,在你的请求发出后、到达服务器前,悄咪咪地把“1件”改成了“100件”,或者把你用户的优惠券给薅走了,那服务器收到的就是个被篡改过的“假”请求。更狠一点,如果他拿到了你某个用户的合法请求,然后疯狂重放这个请求,那服务器不就炸了?


是不是想想都后怕?别慌,今天咱就来聊聊怎么给咱们的API请求加一把“锁”,让这种“中间人攻击”和“重放攻击”无处遁形。这把锁,就是大名鼎鼎的 HMAC-SHA256 请求签名。学会了它,你就能给你的应用穿上“防弹衣”!


image.png


一、光有 HTTPS 还不够?为啥还要请求签名?


可能有机灵的小伙伴会问:“老张,咱不都有 HTTPS 了吗?数据都加密了,还怕啥?”


问得好!HTTPS 确实牛,它能保证你的数据在传输过程中不被窃听和篡改,就像给数据修了条“加密隧道”。但它主要解决的是传输层的安全。可如果:



  1. 请求在加密前就被改了:比如黑客通过某种手段(XSS、恶意浏览器插件等)在你的前端代码执行时就修改了要发送的数据,那 HTTPS 加密的也是被篡改后的数据。

  2. 请求被合法地解密后,服务器无法验证“我是不是我”:HTTPS 保证了数据从A点到B点没被偷看,但如果有人拿到了一个合法的、加密的请求包,他可以原封不动地发给服务器100遍(重放攻击),服务器每次都会认为是合法的。

  3. API Key/Secret 直接在前端暴露: 有些简单的 API 认证,可能会把 API Key 直接写在前端,这简直就是“裸奔”,分分钟被扒下来盗用。


请求签名,则是在应用层做的一道防线。它能确保:



  • 消息的完整性:数据没被篡改过。

  • 消息的身份验证:确认消息确实是你授权的客户端发来的。

  • 防止重放攻击:结合时间戳或 Nonce,让每个请求都具有唯一性。


它和 HTTPS 是好搭档,一个负责“隧道安全”,一个负责“货物安检”,双保险!


二、主角登场:HMAC-SHA256 是个啥?


HMAC-SHA256,听起来挺唬人,拆开看其实很简单:



  • HMAC:Hash-based Message Authentication Code,翻译过来就是“基于哈希的消息认证码”。它是一种使用密钥(secret key)来生成消息摘要(MAC)的方法。

  • SHA256:Secure Hash Algorithm 256-bit,一种安全的哈希算法,能把任意长度的数据转换成一个固定长度(256位,通常表示为64个十六进制字符)的唯一字符串。相同的输入永远得到相同的输出,输入有任何微小变化,输出都会面目全非。


所以,HMAC-SHA256 就是用一个共享密钥 (Secret Key),通过 SHA256 算法,给你的请求数据生成一个独一无二的“签名”。


三、签名的艺术:请求是怎么被“签”上和“验”货的?


整个流程其实不复杂,咱们用个图来说明一下:


sequenceDiagram
participant C as 前端 (Client)
participant S as 后端 (Server)

C->>C: 1. 准备请求参数 (如 method, path, query, body)
C->>C: 2. 加入时间戳 (timestamp) 和/或 随机数 (nonce)
C->>C: 3. 将参数按约定规则排序、拼接成一个字符串 (stringToSign)
C->>C: 4. 使用共享密钥 (Secret Key) 对 stringToSign 进行 HMAC-SHA256 运算,生成签名 (signature)
C->>S: 5. 将原始请求参数 + timestamp + nonce + signature 一起发送给后端

S->>S: 6. 接收到所有数据
S->>S: 7. 校验 timestamp/nonce (检查是否过期或已使用,防重放)
S->>S: 8. 从接收到的数据中,按与客户端相同的规则,提取参数、排序、拼接成 stringToSign'
S->>S: 9. 使用自己保存的、与客户端相同的 Secret Key,对 stringToSign' 进行 HMAC-SHA256 运算,生成 signature'
S->>S: 10. 比对客户端传来的 signature 和自己生成的 signature'
alt 签名一致
S->>S: 11. 验证通过,处理业务逻辑
S-->>C: 响应结果
else 签名不一致
S->>S: 11. 验证失败,拒绝请求
S-->>C: 错误信息 (如 401 Unauthorized)
end

简单来说,就是:



  1. 客户端:把要发送的数据(比如请求方法、URL路径、查询参数、请求体、时间戳等)按照事先约定好的顺序和格式拼成一个长长的字符串。然后用一个只有你和服务器知道的“秘密钥匙”(Secret Key)和 HMAC-SHA256 算法,给这个字符串算出一个“指纹”(签名)。最后,把原始数据、时间戳、签名一起发给服务器。

  2. 服务器端:收到请求后,用完全相同的规则和完全相同的“秘密钥匙”,对收到的原始数据(不包括客户端传来的签名)也算一遍“指纹”。然后比较自己算出来的指纹和客户端传过来的指纹。如果一样,说明数据没被改过,而且确实是知道秘密钥匙的“自己人”发的;如果不一样,那对不起,这请求有问题,拒收!


四、Talk is Cheap, Show Me The Code!


光说不练假把式,咱们来点实在的。


前端签名 (JavaScript - 通常使用 crypto-js 库)


// 假设你已经安装了 crypto-js: npm install crypto-js
import CryptoJS from 'crypto-js';

function generateSignature(params, secretKey) {
// 1. 准备待签名数据
const method = 'GET'; // 请求方法
const path = '/api/user/profile'; // 请求路径
const timestamp = Math.floor(Date.now() / 1000).toString(); // 时间戳 (秒)
const nonce = CryptoJS.lib.WordArray.random(16).toString(); // 随机数,可选

// 2. 构造待签名字符串 (规则很重要,前后端要一致!)
// 通常会对参数名按字典序排序
const sortedKeys = Object.keys(params).sort();
const queryString = sortedKeys.map(key => `${key}=${params[key]}`).join('&');

const stringToSign = `${method}\n${path}\n${queryString}\n${timestamp}\n${nonce}`;
console.log("String to Sign:", stringToSign); // 调试用

// 3. 使用 HMAC-SHA256 生成签名
const signature = CryptoJS.HmacSHA256(stringToSign, secretKey).toString(CryptoJS.enc.Hex);
console.log("Generated Signature:", signature); // 调试用

return {
signature,
timestamp,
nonce
};
}

// --- 使用示例 ---
const mySecretKey = "your-super-secret-key-dont-put-in-frontend-directly!"; // 强调:密钥不能硬编码在前端!
const requestParams = {
userId: '123',
role: 'user'
};

const { signature, timestamp, nonce } = generateSignature(requestParams, mySecretKey);

// 实际发送请求时,把 signature, timestamp, nonce 放在请求头或请求体里
// 例如:
// fetch(`${path}?${queryString}`, {
// method: method,
// headers: {
// 'X-Signature': signature,
// 'X-Timestamp': timestamp,
// 'X-Nonce': nonce,
// 'Content-Type': 'application/json'
// },
// // body: JSON.stringify(requestBody) // 如果是POST/PUT等
// })
// .then(...)

划重点! 上面代码里的 mySecretKey 绝对不能像这样直接写在前端代码里!这只是个演示。真正的 Secret Key 需要通过安全的方式分发和存储,比如在构建时注入,或者通过更安全的认证流程动态获取(但这又引入了新的复杂性,通常 Secret Key 是后端持有,客户端动态获取一个有时效性的 token)。对于纯前端应用,更常见的做法是后端生成签名所需参数,或者整个流程由 BFF (Backend For Frontend) 层处理。如果你的应用是 App,可以把 Secret Key 存储在原生代码中,相对安全一些。


后端验签 (Node.js - 使用内置 crypto 模块)


const crypto = require('crypto');

function verifySignature(requestData, clientSignature, clientTimestamp, clientNonce, secretKey) {
// 0. 校验时间戳 (例如,请求必须在5分钟内到达)
const serverTimestamp = Math.floor(Date.now() / 1000);
if (Math.abs(serverTimestamp - parseInt(clientTimestamp, 10)) > 300) { // 5分钟窗口
console.error("Timestamp validation failed");
return false;
}

// (可选) 校验 Nonce 防止重放,需要存储已用过的 Nonce,可以用 Redis 等
// if (isNonceUsed(clientNonce)) {
// console.error("Nonce replay detected");
// return false;
// }
// markNonceAsUsed(clientNonce, clientTimestamp); // 标记为已用,并设置过期时间

// 1. 从请求中提取参与签名的参数
const { method, path, queryParams } = requestData; // 假设已解析好

// 2. 构造待签名字符串 (规则必须和客户端完全一致!)
const sortedKeys = Object.keys(queryParams).sort();
const queryString = sortedKeys.map(key => `${key}=${queryParams[key]}`).join('&');

const stringToSign = `${method}\n${path}\n${queryString}\n${clientTimestamp}\n${clientNonce}`;
console.log("Server String to Sign:", stringToSign);

// 3. 使用 HMAC-SHA256 生成签名
const expectedSignature = crypto.createHmac('sha256', secretKey)
.update(stringToSign)
.digest('hex');
console.log("Server Expected Signature:", expectedSignature);
console.log("Client Signature:", clientSignature);

// 4. 比对签名 (使用 crypto.timingSafeEqual 防止时序攻击)
if (clientSignature.length !== expectedSignature.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(clientSignature), Buffer.from(expectedSignature));
}

// --- Express 示例中间件 ---
// app.use((req, res, next) => {
// const clientSignature = req.headers['x-signature'];
// const clientTimestamp = req.headers['x-timestamp'];
// const clientNonce = req.headers['x-nonce'];
// // 实际项目中,secretKey 应该从环境变量或配置中读取
// const API_SECRET_KEY = process.env.API_SECRET_KEY || "your-super-secret-key-dont-put-in-frontend-directly!";

// // 构造 requestData 对象,包含 method, path, queryParams
// // 注意:如果是 POST/PUT 请求,请求体 (body) 通常也需要参与签名
// // 且 body 如果是 JSON,建议序列化后参与签名,而不是原始对象
// const requestDataForSig = {
// method: req.method.toUpperCase(),
// path: req.path,
// queryParams: req.query, // 对于GET;POST/PUT可能还需包含body
// // bodyString: req.body ? JSON.stringify(req.body) : "" // 如果body参与签名
// };

// if (!verifySignature(requestDataForSig, clientSignature, clientTimestamp, clientNonce, API_SECRET_KEY)) {
// return res.status(401).send('Invalid Signature');
// }
// next();
// });

五、细节是魔鬼:实施过程中的注意事项



  1. 密钥管理 (Secret Key)



    • 绝对保密:这是最重要的!密钥泄露,签名机制就废了。

    • 不要硬编码在前端:再次强调!对于B端或内部系统,可以考虑通过安全的构建流程注入。对于C端开放应用,通常结合用户登录后的 session token 或 OAuth token 来做,或者使用更复杂的 API Gateway 方案。

    • 定期轮换:为了安全,密钥最好能定期更换。



  2. 时间戳 (Timestamp)



    • 防止重放攻击:服务器会校验收到的时间戳与当前服务器时间的差值,如果超过一定阈值(比如5分钟),就认为是无效请求。

    • 时钟同步:客户端和服务器的时钟要尽量同步,不然很容易误判。



  3. 随机数 (Nonce)



    • 更强的防重放:Nonce 是一个只使用一次的随机字符串。服务器需要记录用过的 Nonce,在一定时间内(同时间戳窗口)不允许重复。可以用 Redis 等缓存服务来存。



  4. 哪些内容需要签名?



    • HTTP 方法 (GET, POST, etc.)

    • 请求路径 (Path, e.g., /users/123)

    • 查询参数 (Query Parameters, e.g., ?name=zhangsan&age=18):参数名需要按字典序排序,确保客户端和服务端拼接顺序一致。

    • 请求体 (Request Body):如果是 application/x-www-form-urlencodedmultipart/form-data,处理方式同 Query Parameters。如果是 application/json,通常是将整个 JSON 字符串作为签名内容的一部分。注意空 body 和有 body 的情况。

    • 关键的请求头:比如 Content-Type,以及自定义的一些重要 Header。

    • 时间戳和 Nonce:它们本身也要参与签名,防止被篡改。



  5. 一致性是王道:客户端和服务端在选择哪些参数参与签名、参数的排序规则、拼接格式等方面,必须严格一致,一个空格,一个换行符不同,签名结果就天差地别。


六、HMAC-SHA256 vs. 其他方案?


方案优点缺点适用场景
仅 HTTPS传输层加密,防止窃听无法防止应用层篡改(加密前)、无法验证发送者身份(应用层)、无法防重放基础数据传输安全
简单摘要 (如MD5)实现简单若无密钥,容易被伪造;MD5本身已不安全文件完整性校验(非安全敏感)
HMAC-SHA256消息完整性、身份验证(基于共享密钥)、可防重放(结合时间戳/Nonce)密钥管理是关键和难点;签名和验签有一定计算开销需要保障API接口安全、防止未授权访问和篡改的场景
JWT (JSON Web Token)无状态、可携带用户信息、标准化Token 可能较大;吊销略麻烦;主要用于用户认证和授权用户登录、单点登录、API授权

HMAC-SHA256 更侧重于请求本身的完整性和来源认证,而 JWT 更侧重于用户身份的认证和授权。它们可以结合使用。


好啦,今天关于 HMAC-SHA256 请求签名的唠嗑就到这里。这玩意儿看起来步骤多,但一旦理解了原理,实现起来其实就是细心活儿。给你的 API 加上这把锁,晚上睡觉都能踏实点!


我是老码小张,一个喜欢研究技术原理,并且在实践中不断成长的技术人。希望今天的分享对你有帮助,咱们下回再聊!欢迎大家留言交流你的看法和经验哦!


作者:老码小张
来源:juejin.cn/post/7502641888970670080
收起阅读 »

AI场景前端必学——SSE流式传输

背景 由于大模型通常是需要实时推理的,Web 应用调用大模型时,它的标准模式是浏览器提交数据,服务端完成推理,然后将结果以 JSON 数据格式通过标准的 HTTP 协议返回给前端。但是这么做有一个问题,主要是推理所花费的时间和问题复杂度、以及生成的 token...
继续阅读 »

背景


由于大模型通常是需要实时推理的,Web 应用调用大模型时,它的标准模式是浏览器提交数据,服务端完成推理,然后将结果以 JSON 数据格式通过标准的 HTTP 协议返回给前端。但是这么做有一个问题,主要是推理所花费的时间和问题复杂度、以及生成的 token 数量有关。在日常使用中会发现,只是简单问候一句,可能 Deepseek 推理所花费的时间很少,但是如果我们提出稍微复杂一点的要求,比如编写一本小说的章节目录,或者撰写一篇千字的作文,那么 AI 推理的时间会大大增加,这在具体应用中就带来一个显而易见的问题,那就是用户等待的时间很长。能够发现,我们在使用线上大模型服务时,不管是哪一家大模型,通常前端的响应速度并没有太慢,这正是因为它们默认采用了流式(streaming)传输,不必等到整个推理完成再将内容返回,而是可以将逐个 token 实时返回给前端,这样就大大减少了响应时间。


服务端推送


服务端推送,也称为消息推送或通知推送,是一种允许应用服务器主动将信息发送到客户端的能力,为客户端提供了实时的信息更新和通知,增强了用户体验。


服务端推送的背景与需求主要基于以下几个诉求:


实时通知:在很多情况下,用户期望实时接收到应用的通知,如新消息提醒、商品活动提醒等。节省资源:如果没有服务端推送,客户端需要通过轮询的方式来获取新信息,会造成客户端、服务端的资源损耗。通过服务端推送,客户端只需要在收到通知时做出响应,大大减少了资源的消耗。增强用户体验:通过服务端推送,应用可以针对特定用户或用户群发送有针对性的内容,如优惠活动、个性化推荐等。这有助于提高用户对应用的满意度和黏性。


常见推送场景有:微信消息通知栏、新闻推送、外卖状态 等等,我们自身的推送场景有:下载、连线请求、直播提醒 ......


解决方案


传统实时处理方案:


轮询:这是一种较为传统的方式,客户端会定时地向服务端发送请求,询问是否有新数据。服务端只需要检查数据状态,然后将结果返回给客户端。轮询的优点是实现简单,兼容性好;缺点是可能产生较大的延迟,且对服务端资源消耗较高。长轮询(Long Polling):轮询的改进版。客户端向服务器发送请求,服务器收到请求后,如果有新的数据,立即返回给客户端;如果没有新数据,服务器会等待一定时间(比如30秒超时时间),在这段时间内,如果有新数据,就返回给客户端,否则返回空数据。客户端处理完服务器返回的响应后,再次发起新的请求,如此反复。长轮询相较于传统的轮询方式减少了请求次数,但仍然存在一定的延迟。


HTML5 标准引入的实时处理方案:


WebSocket:一种双向通信协议,同时支持服务端和客户端之间的实时交互。WebSocket 是基于 TCP 的长连接,和HTTP 协议相比,它能实现轻量级的、低延迟的数据传输,非常适合实时通信场景,主要用于交互性强的双向通信。SSE:Server-Sent Events 服务器推送事件,简称 SSE,是一种服务端实时主动向浏览器推送消息的技术。SSE 是 HTML5 中一个与通信相关的 API,主要由两部分组成:服务端与浏览器端的通信协议( HTTP 协议)及浏览器端可供 JavaScript 使用的 EventSource 对象。


​ 从“服务端主动向浏览器实时推送消息”这一点来看,SSE 与 WebSockets API 有一些相似之处。但是,SSE 与 WebSockers API 的不同之处在于:


Server-Sent Events APIWebSockets API
协议基于 HTTP 协议基于 TCP 协议
通信单工,只能服务端单向发送消息全双工,可以同时发送和接收消息
量级轻量级,使用简单相对复杂
自动重连内置断线重连和消息追踪的功能不在协议范围内,需手动实现
数据格式文本或使用 Base64 编码和 gzip 压缩的二进制消息类型广泛
事件支持自定义事件类型不支持自定义事件类型
连接数连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100)连接数无限制
浏览器支持大部分支持,但在ie及早期的edge浏览器中并不被支持主流浏览器(包括移动端)的支持较好

第三方推送:


常见的有操作系统提供相应的推送服务,如苹果的APNs(Apple Push Notification service)、谷歌的FCM(Firebase Cloud Messaging)等。同时,也有一些跨平台的推送服务,如个推、极光推送、友盟推送等,帮助开发者在不同平台上实现统一的推送功能。


这种推送方式在生活中十分常见,一般你打开手机就能看到各种信息推送,基本就是利用第三方推送来实现。


SSE


developer.mozilla.org/zh-CN/docs/…


SSE 服务端推送,它基于 HTTP 协议,易于实现和部署,特别适合那些需要服务器主动推送信息、客户端只需接收数据的场景:


image.png


EventSource


developer.mozilla.org/zh-CN/docs/…


服务器发送事件 API (SSE)包含在 eventSource 接口中。换句话说 eventsource 接口是 web 内容与服务器发送事件通信的接口。一个 eventsource 实例会对 HTTP 服务器开启一个持久化的连接,以text/event-stream格式发送事件,此连接会一直保持开启直到通过调用EventSource.close()关闭。


image.png
一旦连接开启,来自服务端传入的消息会以事件的形式分发至你代码中。如果接收消息中有一个 event 字段,触发的事件与 event 字段的值相同。如果不存在 event 字段,则将触发通用的 message 事件。


建立连接


EventSource 接受两个参数:URL 和 options。


​ URL 为 http 事件来源,一旦 EventSource 对象被创建后,浏览器立即开始对该 URL 地址发送过来的事件进行监听。


​ options 是一个可选的对象,包含 withCredentials 属性,表示是否发送凭证(cookie、HTTP认证信息等)到服务端,默认为 false。


const eventSource = new EventSource('http_api_url', { withCredentials: true })

// 关闭连接
eventSource.close()

// 可以使用addEventListener()方法监听
eventSource.addEventListener('open', function(event) {
console.log('Connection opened')
})

eventSource.addEventListener('message', function(event) {
console.log('Received message: ' + event.data);
})

// 监听自定义事件
eventSource.addEventListener('xxx', function(event) {
console.log('Received message: ' + event.data);
})

eventSource.addEventListener('error', function(event) {
console.log('Error occurred: ' + event.event);
})
// 也可以使用属性监听的方式
eventSource.onopen = function(event) {
console.log('Connection opened')
}

eventSource.onmessage = function(event) {
console.log('Received message: ' + event.data);
}

eventSource.onerror = function(event) {
console.log('Error occurred: ' + event.event);
})

Stream API


developer.mozilla.org/zh-CN/docs/…


Stream API 允许 JavaScript 以编程方式访问从网络接收的数据流,并且允许开发人员根据需要处理它们。


流会将你想要从网络接受的资源分成一个个小的分块,然后按位处理它。


image.png


@microsoft/fetch-event-source


http://www.npmjs.com/package/@mi…


默认的浏览器eventSource API在以下方面存在一些限制:


无法传递请求体(request body),必须将执行请求所需的所有信息编码到 URL 中,而大多数浏览器对 URL 的长度限制为 2000 个字符。无法传递自定义请求头。只能进行 GET 请求,无法指定其他方法。如果连接中断,无法控制重试策略,浏览器会自动进行几次尝试然后停止。


@microsoft/fetch-event-source 的优势:


@microsoft/fetch-event-source提供了一个基于 Fetch API 的替代接口,完全兼容 Event Stream 格式。这使得我们能够以更加灵活的方式进行服务器发送事件的消费。以下是该库的一些主要优势:


支持任何请求方法、请求头和请求体,以及 Fetch API 提供的其他功能。甚至可以提供替代的 fetch() 实现,以应对默认浏览器实现无法满足需求的情况。


提供对响应对象的访问权限,允许在解析事件源之前进行自定义验证/处理。这在存在 API 网关(如 nginx)的情况下非常有用,如果网关返回错误,我们可能希望正确处理它。


对连接中断或发生错误时,提供完全控制的重试策略。


此外,该库还集成了浏览器的 Page Visibility API,使得在文档被隐藏时(例如用户最小化窗口),连接会关闭,当文档再次可见时会自动使用上次事件 ID 进行重试。这有助于减轻服务器负担,避免不必要的开放连接(但如果需要,可以选择禁用此行为)。


import { fetchEventSource } from "@microsoft/fetch-event-source";

const Assistant: React.FC<Iprops> = (props) => {
const [abortController, setAbortController] = useState(new AbortController());

const send = (question: any) => {
setIsAnswering(true);
setIsScrollAtBottom(true);
setAskText("");

// 创建“生成中...”的占位符消息
const loadingMessage = { content: "生成中...", chatSenderType: 0, isLoading: true };

// 更新 chatList,添加用户消息和占位符消息
setChatList([...chatList, { content: question.text, chatSenderType: 1, problemType: question.problemType }, loadingMessage]);

setLoading(true); // 开始加载

fetchEventSource("https://demo.com/chat", {
method: "post",
body: JSON.stringify({ message: question.text, systemType, oa, problemType: question.problemType }),
headers: {
"Content-Type": "application/json"
},
signal: abortController.signal,
async onopen(response) {
// 可以在这里进行一些操作
},
onmessage(msg: { data: string }) {
msg.data.length && setStopDisabled(false);
// 接收到实际响应后,更新 chatList 中的占位符消息
const newMessage = { ...JSON.parse(msg.data).data, chatSenderType: 0, isLoading: false };
setChatList((prevChatList: any[]) => {
// 替换最后一个消息(占位符)为实际消息
const updatedChatList = [...prevChatList];
updatedChatList[updatedChatList.length - 1] = newMessage;
return updatedChatList;
});

setIsScrollAtBottom(true);
setLoading(false); // 加载完成
},
onclose() {
setIsStop(true);
setLoading(false); // 加载完成
setIsAnswering(false);
// 停止生成禁用
setStopDisabled(true);
},
onerror(err) {
abortController.abort();
setLoading(false); // 加载出错,停止加载
throw err;
}
});
};

const stop = async () => {
abortController.abort();
const answer = chatList[chatList.length - 1];
setAbortController(new AbortController());
setIsAnswering(false);
setLoading(false); // 停止加载
stopAnswer({ message: answer.content, messageId: answer.messageId, problemType: answer.problemType, systemType, oa }).then((res: any) => {
message.success("操作成功");
});
};

return (
<div>
<Chat
chatList={chatList}
setChatList={setChatList}
askText={askText}
setAskText={setAskText}
send={send}
stop={stop}
/>

</div>

)
};

AbortController


developer.mozilla.org/zh-CN/docs/…


在前端开发中,网络请求是不可或缺的一环。但在处理网络请求时,我们经常会遇到需要中途取消请求的情况。这时候,abortController可以帮助大家更好地掌控网络请求。


简介


AbortController是一个Web API,它提供了一个信号对象(AbortSignal),该对象可以用来取消与Fetch API相关的操作。当我们创建AbortController实例时,会自动生成一个与之关联的AbortSignal对象。我们可以将这个AbortSignal对象作为参数传递给fetch函数,从而实现对网络请求的取消控制。


使用方法


创建AbortController实例获取AbortSignal对象使用signal对象发起fetch请求取消fetch请求


const controller = new AbortController();
const signal = controller.signal;
// 当需要取消请求时,我们只需调用AbortController实例的abort方法:
fetch(url, { signal }).then(response => {
// 处理响应数据
}).catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch 请求已被取消');
} else {
// 处理其他错误
}
});
// 当需要取消请求时,我们只需调用AbortController实例的abort方法:
controller.abort();

参考资料


blog.csdn.net/ldw20151080…


juejin.cn/post/722112…


http://www.npmjs.com/package/@mi…


segmentfault.com/a/119000004…


作者:Yancy_
来源:juejin.cn/post/7504843440778870794
收起阅读 »

🎯TAPD MCP:拯救我们于无聊的重复工作之中!

写在开头 其实这才是文章的标题:使用 TAPD MCP 实现任务的自动同步与快速管理😋 🤔 困境:在飞书和TAPD之间反复横跳是什么体验? 日常小编的需求任务拆分的工作流程大概是这样的: 首先,打开飞书,进入飞书文档,找到对应属于你的需求,创建任务。 ✍...
继续阅读 »

写在开头



其实这才是文章的标题:使用 TAPD MCP 实现任务的自动同步与快速管理😋



🤔 困境:在飞书和TAPD之间反复横跳是什么体验?


日常小编的需求任务拆分的工作流程大概是这样的:



  1. 首先,打开飞书,进入飞书文档,找到对应属于你的需求,创建任务。 ✍️

  2. 其次,打开TAPD,再创建一遍同样的任务。✍️✍️

  3. 最后,每天打开TAPD,不断更新任务状态。✍️✍️✍️



看流程不算复杂,甚至优于不少企业的管理流程,似乎该"知足常乐"吧!🙊


当然!!!


但作为坚持极客精神的执行者(强行立人设 + 1🙈),重复劳动简直是效率大敌!


从 “技术人视角” 看,第二步的手动同步操作尤为繁琐 —— 虽说程序猿是世界上最"懒"的人,但本质是用智慧消灭无意义的重复。✨✨✨


任何机械性工作都该交给程序处理,腾出时间做更有价值的事(比如……moyu,误,专注工作)



✨ 优化思路:


image.png


😎 为什么我们爱飞书?


说实话,飞书真的很香!



其实,更多的是因为日常办公使用的就是飞书沟通。😋


二连追问,企微、钉钉:我们不配?😑



不过,小编这段时间使用下来,确实也感觉飞书的功能非常强大!


特别是📝多维表格功能,Top1!!!



前段时间网上爆火的使用"飞书多维表格+AI=小红书爆款内容",那效果......啧啧啧,确实牛👍。



讲回来,在使用飞书文档管理我们的需求任务时,也确实是有好处的,起码我所知道的有:



  • 📊 计划图表:直观展示每个人的任务分配情况,看着舒服~

  • 📝 多维表格:各种公式随便玩,算工时简直不要太方便!

  • 💬 即时沟通:有问题?评论一下自动戳同事!


😅 那为什么还要用TAPD?


emmm...这就要问问Leader了(小声bb)不过认真说,TAPD确实有它的优势:



  • 🎯 需求管理更专业

  • 📈 数据分析很强大

  • 🔄 工作流程更规范


但是...这不代表我们要当复制粘贴工具人啊!(╯°□°)╯︵ ┻━┻


🎉 解救方案:TAPD MCP 来啦!


救星:传送门 🚀🚀🚀


🤖 什么是MCP?


简单来说,MCP就是让AI变得更聪明的一个协议!它可以:


(此处省略一万字。。。。)


🛠️ 开始配置我们的AI助手


支持MCP的AI客户端:



第1️⃣步:Python环境配置



为什么要安装Python环境?🤔


Anthropic 为 MCP 提供了官方的 Python 和 TypeScript/Node.js SDK,方便开发者快速构建 MCP 服务或将 MCP 客户端集成到自己的应用中。(参考


而 TAPD MCP 是使用 Python 开发的,所以要想使用这个MCP,需要先安装Python的环境,它是以uvx命令来运行的。



首先,python环境的安装教程网上非常多,这里就不细嗦了,可以上官网直接下载:传送门


然后,我们来扩展认识一个新朋友:uv



uv:一个超快的Python包管理器,比pip快到飞起!🚀 和前端的nvm差不多的东西,uv有一个坑点就是下载python版本的时候,需要🪜🪜🪜。




安装与使用uv的方式不是本章的主要内容,也不细嗦了,可以参考这篇文章:传送门


本章要求的Python环境版本最低要 3.13+ 🔉🔉🔉 (为啥?当然是 TAPD MCP 要求的🙇)



小编的python版本配置:




其他一些工具对比:


工具核心功能适合场景
anaconda管理环境 + Python 版本 + 包数据科学、简单隔离
pyenv管理 Python 版本多版本精确控制
uv管理 Python 版本 + 虚拟环境 + 包追求速度、现代工具爱好者

总结:反正你本地需要安装好 Python 3.13 + 的环境,并且安装 uv ,能运行 uvx 命令即可。


第2️⃣步:获取TAPD凭证



  1. 登录 TAPD。

  2. 点击左下角 "公司管理"。

  3. 点击 "API账号管理",获取API账号与API秘钥。




每个API账号的权限是不一样的,也可以配置该账号的权限范围:



设置权限范围可以有效的防止AI助手误操作其他项目的情况,这很重要!!!⏰



第3️⃣步:在Cursor中配置MCP



  1. 打开 Cursor。

  2. 点击右上角的 Open Cursor Settings 或者 Ctrl + Shift + J。

  3. 点击MCP,再点击 Add new global MCP server,进入MCP配置页面。



具体配置如下:


{
"mcpServers": {
"mcp-server-tapd": {
"command": "uvx",
"args": [
"mcp-server-tapd",
"--api-user=你的API账号",
"--api-password=你的API秘钥",
"--api-base-url=https://api.tapd.cn",
"--tapd-base-url=https://www.tapd.cn"
]
}
}
}

使用 Ctrl + S 保存后,回到 Cursor Settings 就能看到 TAPD MCP 的服务了,并且它应该是亮绿灯,这说明你配置成功了。🥳




如果配置后,没有亮绿灯,那么你要先可以检查一下TAPD的凭证有没有什么问题,Python的环境有没有 3.13+ 以上, 有没有安装uv,或者重启大法。


如果还不行,就要进行技术的排查了,可以点击 Help -> Toggle Developer Tools ,会调出 Cursor 的控制台,MCP配置不成功的话,控制台是会抛出错误的,拿到错误。


如果你是程序猿就自己分析错误的内容啦,你可以的。👌


如果你非程序猿,咱们就点击下图的第四步,把错误内容丢给AI,给它简单描述一下你的困境,最好选择agent模式,让它帮你修复,你只要不断给它同意、同意、同意即可。😋




🎮 实战:让AI帮我们做任务!


完成配置后,到这里咱们就能进入正式的使用环节了。咱们来开启Cursor的Agent模式,开始来实际使用TAPD MCP Server!



4.1 验证MCP服务是否正常工作



当然,你最好先在TAPD平台上创建一个空间,方便咱们初始验证。


在 TAPD 中,空间是团队协作的基础单元,用于隔离不同项目或团队的数据和权限,每个空间可以有多个项目


需求是从用户角度描述的独立功能点,是产品研发的核心对象。


任务是在需求下拆分的具体工作项。



TAPD 的业务对象还包括迭代缺陷测试计划测试用例等。



以下是小编创建的一个名叫"橙子果园项目"的空间,TAPD默认会帮我们初始化一些需求、特性啥的。还有,我们可以从地址栏获取到这个空间的唯一ID(workspace_id),通过这个ID能让AI更加精准的自动去操作,也能防止它操作到其他空间中去!!!⏰



其实本质是通过API接口去操作,接口要求传递workspace_id参数,这很正常吧。😋




有了空间ID,接下来咱们来让AI帮我们查询一下这个空间的"需求"列表,如下:



请你使用TAPD的MCP,帮我查询一下这个空间(58195679)中的需求列表。





它仅把"需求"的帮我们查出来了,是不是还不错?👻


初始目的达成,撒花撒花。🌸🌸🌸


4.2 自动化创建需求


能进行查找,基本上TAPD的MCP是能正常使用了,接下来,咱们让AI通过MCP帮我们创建一个需求。


首先,我们先上TAPD上看看创建一个需求要填些什么信息(其实不看也是可以的,它会给你提示):



能填的东西很多,但是只有标题是必填的,咱们简单的填写一个标题和内容来创建一个需求就行,如下:



帮我创建一个需求,标题为"第一期计划1.0.1",内容为“项目的基本搭建、架构规划、发布流程部署、缺陷计划、验收标准”。





结果:



是不是挺好,一句话,就让AI帮咱们吭哧吭哧的干活。😍



注意,我们使用的是自然语言,上面小编虽然提供了对话内容,但是也不一定要和我一样,能大致表达你的想法就行。



4.3 自动化创建任务🍊🍊🍊


上面,需求已经创建完了,接下来就要来解决咱们开头提到的实际困境了。


本来按照小编开始的设想,任务的信息应该是AI自动去飞书的平台那边获取的,但是......🙉。



  • 飞书还没有提供文档这方面的相关MCP,社区倒是有,如:传送门。但是好像不能满足小编心中所想, 还有就是它非官方,不敢用呀,怕夹带私货。😩

  • 飞书提供了开放的API平台,我们其实可以自己搭一个服务,让AI去访问这个服务拿数据就行,Em...就是要写代码,麻烦,再想想...。😑

  • 思考了两坤年半后,小编觉得前面配置运行环境,配置MCP已经很麻烦,信息来源这部分应该需要简单化了😋,咱直截了当从飞书文档中复制过来就行啦。


日常工作中,小编需要在飞书多维表格里查找对应需求并创建开发任务,如下:



其中,需同步至 TAPD 的核心内容为上图红框部分。


同样,👀咱们可以先去TAPD上看看手动在需求下创建任务的情况是如何的,如下:



刚刚好,内容是正好对应上的。但要每次都得逐个创建任务,面对大量任务时,这操作流程就显得极为繁琐,实在令人困扰!😣


现在,我们可以借助 AI 进行自动化创建,只需将内容复制给它即可。


具体操作是,在多维表格中长按并拖动鼠标选中目标单元格,按下 Ctrl+C 完成复制。



再把内容丢给AI,告诉它帮我们创建任务,如下:



我希望你在"第一期计划1.0.1"的 需求 下创建三个子任务,任务内容如下:


页面样式切图与基本逻辑编写 周北北 3 2025/04/20 2025/04/20


页面接口联调与逻辑完善 周北北 4 2025/04/21 2025/04/21


缺陷修复 周北北 2 2025/04/22 2025/04/24





Em...最终结果是正向的,AI 确实成功帮小编创建好了任务,效果堪称完美💯-1。 不过,就给它打99分吧,因为这一过程并非一帆风顺,其中也遇到了不少难题😅




首先,AI 在区分需求和任务这两个概念时,存在一定困难❗


从用户角度来看,需求和任务的界定清晰明了,但对于程序而言,两者存在层级关系。



TAPD MCP 并未提供专门用于创建任务的独立 API,创建任务与创建需求共用同一个 API,仅通过 "workitem_type_id" 字段来加以区分。从程序设计层面讲,这种方式并无问题,然而却给 AI 的理解带来了挑战,这也恰恰凸显出不同模型推理能力的差异。


起初,AI 将小编的三个任务错误创建成了三个需求。于是,小编想着更换模型,让AI能更好理解我的想法,我从 GPT-4o 切换为 Claude3.7。


Claude3.7确实强大,当它遇到 “任务” 概念无法理解时,会先在 TAPD MCP 提供的全部功能中进行查找,在发现确实没有可直接创建任务的 API 后,又找到了创建需求的 API,并留意到其中有一个参数能够区分需求和任务。



随后,模型沿着这个思路,一步步进行自我引导,并向小编询问关键信息,最终成功完成了任务创建。✅


其次,AI无法很明确字段的定义❗


在上面的TAPD的截图中,可以三个任务已成功创建,但处理人这列还显示为空,这是为什么呢?



"预估工时"字段是正常的,它要在任务详情中查看,小编在 TAPD 平台调整许久,始终无法调出 "处理人" 列。。。



小编通过核查 AI 执行详情与 TAPD 文档发现:




原来是AI把字段搞错了。。。当复制内容涉及多个相近字段时,AI 可能因信息模糊而 "懵圈",这也是其不确定性之一。因此,明确告知复制内容对应的字段至关重要。


我们再来尝试重新一个创建任务,并向 AI 详细说明 "处理人" 字段:



再帮我创建一个任务:


产品验收小缺陷修复 周北北 1 2025/04/25 2025/04/25


周北北 是处理人owner





从截图可见,这次效果堪称完美了💯💯💯!


经过上一轮 "调教"(其实是上下文连贯的作用😂),AI 已能清晰区分需求与任务的概念。同理,本次明确 "处理人" 字段后,AI 下次便能自动识别,让我们省心不少。



不过,AI 理解仍有小插曲 —— 小编本意不想设置负责人的,AI 却自动添加了,不过问题不大。整体来看,明确字段规则后,AI 协作效率显著提升啦!




TAPD MCP API 详情:




4.4 自动化更新任务状态


需求和任务创建完成后,接下来还有一个问题就是咱们需要时不时去更改任务的状态。虽然操作本身不复杂,只需点击几下,但小编仍觉得有些 "麻烦"—— 尤其是每次登录 TAPD 平台时,若遇到登录状态过期,还需用手机扫码重新登录,实在让人头疼。😕


还有,试想,如果每次完成任务(比如敲完代码)时,能在编辑器旁边顺手告诉 AI,让它帮忙更新任务状态,岂不是更高效?这样一来,写代码和更新任务状态都能在 Cursor 中完成,无需频繁切换平台。


还有还有不仅仅是任务,"缺陷"修复后若能自动更新状态,也能省去反复登录平台修改的麻烦。可见,自动化更新任务状态是个非常实用的操作呀。😀


那么,我们要如何做呢?


一个任务在TAPD平台上通常有以下三种状态:



我们尝试让 AI 将某个任务状态更改为 "进行中" 试试:




在 Claude 3.7 模型下,该操作算是一次成功的。🎉🎉🎉




但此前小编在 GPT-4o 模型中尝试时,初次操作就出现了错误❗


模型未理解 "进行中" 的状态定义,随便选择塞了一个状态进行更新,而 TAPD 平台居然没有对状态值进行有效性验证,直接就成功了😗。此外,GPT-4o 也没有像 Claude 3.7 那样先查询任务状态列表,直接 "盲操作",推理能力略显不足呀!


不过,在小编向其提供了 TAPD 文档中的任务状态说明后:



它最终也是能正确完成状态的更新,也算可以啦。😋 如果说TAPD更出名一点,文档更友好一点,AI模型的前期训练积累了这方面的内容,其实都问题不大。



看到这里,不知道你有没有存在一些疑问❓是不是好似还有一个隐藏的痛点🙈:


每次对话时,都需要提供精确的任务名称作为匹配标准。虽然不算太麻烦,但是如果能更简洁一点,那肯定是更简洁好呀。在某些AI模型的视角下,如果存在名称相近的任务,就容易混淆,它容易"乱来"。但有一些模型比较聪明一些,相近或者模糊的任务名称也是可以的,AI会列举任务名称相近的任务,一个一个咨询你是否执行,也可能是 AI 先查询任务列表,再从结果中定位目标任务进行状态修改。这样一来,即使任务名称相近,也能通过列表精准匹配,这样操作效率与准确度反而更高了。


总的来说,尽管不同模型的表现有差异,但通过合理引导和补充规则,都问题不大,能满足实际需要了。👻



🚀 未来展望



  • 🔄 通过飞书开放平台的 API,实现任务自动同步。

  • ⏳设置定时任务,定期同步两个平台的数据。

  • 🎯 自动帮我们写代码?

  • 💪 可以专注于更有意义的工作


随着 AI 技术的发展,咱们可以期待更多智能化的协作方式。希望本章的分享能帮助大家从重复的工作中解放出来,毕竟生活不只有搬砖,还有诗和远方呢!(๑•̀ㅂ•́)و✧









至此,本篇文章就写完啦,撒花撒花。


image.png


作者:橙某人
来源:juejin.cn/post/7499014256547774490
收起阅读 »

不用 js实现渐变、虚线、跑马灯、可伸缩边框

web
最近遇到个需求,要求实现一个渐变色的边框,并且是虚线的,同时还要有动画。 有的朋友可能看到这里就要开骂了,估计要提刀找设计和产品怼回去了。 但其实我是可以理解的,因为这种花哨的边框想要用在一个类似于魔法框的地方,框住一个地方,然后交给 ai 处理。这样的交互设...
继续阅读 »

最近遇到个需求,要求实现一个渐变色的边框,并且是虚线的,同时还要有动画。


有的朋友可能看到这里就要开骂了,估计要提刀找设计和产品怼回去了。


但其实我是可以理解的,因为这种花哨的边框想要用在一个类似于魔法框的地方,框住一个地方,然后交给 ai 处理。这样的交互设计可以很好的体现科技感,并且我也想尝试一下,就接了这个需求。


单看几个条件都好处理,css 已经支持了 border-image。


再不济用伪元素遮盖一下,clip-path镂空也可以


甚至我看到很多网站是直接放个视频就完了


但是我这次的需求最重要的是虚线,这就不好处理了。因为设置了边框为虚线后会忽略掉 border-image。


其实这个问题看起来很难,做起来也确实难。我搜到了张鑫旭大佬多年前的文章,就是专门讲这件事的


http://www.zhangxinxu.com/wordpress/2…


看完之后我受益匪浅,虽然我不能用他的方案(因为他的方案中,虚线是假的,样式会和浏览器有差异)


我尝试了很多方案,mask、clip-path、背景图等等,效果都不好。


绝望之际我想到了一个救星svg


div 做不到的事情,我 svg 来做。svg 可以设置 stroke,可以设置 fill,可以设置渐变色,渐变色还可以做动画。简直就是完美的符合需求


先写个空标签上去


<style>
.rect{
width: 100px;
height: 100px;
}
</style>
<div class='rect'>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
</svg>
</div>

因为我需要 svg 尺寸跟随父容器变化,所以就不写 viewBox 了,直接设置宽高 100%。同时在里面画一个矩形,也是宽高 100%。


<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width='100%' height='100%'>
<rect width="100%" height="100%"></rect>
</svg>

现在长这样
image.png


接下来给 rect 设置填充和描边,边框宽度为 4px


<rect 
fill="transparent"
stroke="red"
stroke-width="4"
width="100%"
height="100%"
>
</rect>

image.png


接下来我们给border 设置为渐变色,需要在 svg 中定义一个渐变,svg 定义渐变色还是很方便的,都是现成标签和属性直接就可以通过 id 取到。


<svg>
...
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="lightcoral" />
<stop offset="50%" stop-color="lightblue" />
<stop offset="100%" stop-color="lightgreen" />
</linearGradient>
</defs>
</svg>

接下来给红色的 stroke 换成渐变色


<rect 
fill="transparent"
stroke="url(#gradient)"
stroke-width="4"
width="100%"
height="100%"
>
</rect>

image.png


接下通过 stroke-dasharray 来设置虚线边框


mdn 上关于 dasharry的介绍在这里 developer.mozilla.org/zh-CN/docs/…


image.png


我给 rect 设置 dasharray 为 5,5


<rect 
fill="transparent"
stroke="url(#gradient)"
stroke-dasharray="5,5"
stroke-width="4"
width="100%"
height="100%"
>
</rect>

image.png


这样渐变虚线边框就成了


接下来处理动画效果


动画分两种



  1. 线动,背景色不动

  2. 线不动,背景色动


这两种效果我都实现了


首先展示线动,背景色不动的情况


这种情况只要能想办法让虚线产生偏移就可以,于是我搜了一下,这不巧了吗,正好有个属性叫 stroke-dashoffset


image.png


于是就可以通过 css 动画来修改偏移量


<style>
.dashmove {
animation: dashmove 1s linear infinite;
}

@keyframes dashmove {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: 10;
}
}
</style>
<rect class="dashmove" .... ></rect>

ezgif-1bb6c3542c4ad7.gif


大功告成


接下来处理第二种情况,线不动,背景动


这种情况就更简单了,因为 svg 本身就支持动画


我们只需要在渐变色中增加一个animateTransform标签


<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
...
<animateTransform
attributeName="gradientTransform"
type="rotat
from="
0 0.5 0.5"
to="360 0.5 0.5"
dur="1s"
repeatCount="indefinite"
/>

</linearGradient>

ezgif-6b83b81feb0420.gif


接下来看一下拖拽的效果,这个很重要,因为我们不希望随着容器比例变化,会让边框宽度也变化。


给容器元素加上这三个属性,这个 div 就变成了可拖拽缩放的


.rect{
// ...
resize: both;
position: relative;
overflow: auto;
}

看下效果


ezgif-58de40b814d0bb.gif


完美 🎉🎉🎉


在这里查看完整在线 demo stackblitz.com/edit/stackb…


作者:阿古达木
来源:juejin.cn/post/7502127751572406323
收起阅读 »

鸿蒙UI通用代码几种抽离方法

web
    对于做APP的UI,免不了会写大量的重复布局,重复UI页面。此时对于将重复的UI控件抽离出来封装为通用组件来进行优化很是重要。     本文重点分析鸿蒙几种UI处理上,如何抽离通用方法来进行UI的复用。重点对比@Style,@Extend, Attri...
继续阅读 »

    对于做APP的UI,免不了会写大量的重复布局,重复UI页面。此时对于将重复的UI控件抽离出来封装为通用组件来进行优化很是重要。


    本文重点分析鸿蒙几种UI处理上,如何抽离通用方法来进行UI的复用。重点对比@Style,@Extend, AttributeModifier, @Builder和 struct 这五种方法的区别和使用场景。


Styles装饰器


    学过Android开发的小伙伴知道Android中有样式Style概念,我们定义好一个样式,那么就可以在各个组件中使用。从而保持每个组件有一样的属性。


    同理,鸿蒙中也可以使用样式。比如我们UI上的按钮具有相同的宽,高,背景色和边角距。那么我们就可以定义一个Style,每次定义按钮时候,只需要将Style赋给按钮就可以实现该属性,从而避免重复代码的书写。



  • 代码说明


image.png


    如图,在当前页面内定义一个方法,使用装饰器Styles修饰,修饰后,方法内部就可以直接通过 .属性 的方式来定义属性了。方法定义完后,下方button里可以直接使用该方法。虽然我这里方法命名为commonButton,但是实际上所有基础控件都可以使用该方法使用里边的属性,比如下方的Text组件。



  • Style特点



  1. 对于定义的方法,无法使用export修饰。
    这也就意味着,我们抽离的通用属性,只能在当前页面内的组件上使用,换个页面就不可以了,无法做到全局所有页面通用。

  2. 对于定义的方法,只能定义组件的通用属性。
    比如宽高,背景色等。对于一些控件特有属性是无法定义的。比如Select组件的selectedOptionFont特有属性无法定义。

  3. 方法不支持传递入参。
    意味着该样式无法做到动态修改,只要定义好就无法修改。比如定义好宽高为30,而某个组件宽要求为40,其他属性都不变,那这个方法也没法用了。

  4. 方法为组件通用属性,故所有组件都可以引用方法。


Extend装饰器


    对于Styles装饰器的第2点限制,鸿蒙推出了解决方案,那就是使用@Extend装饰器。


    Extend装饰器需要我们在使用时候指定定义哪个组件的属性,是专门抽离指定组件的。



  • 代码说明


image (1).png


    Extend要求必须定义方法为在当前文件的全局定义,且也不能够export,同时定义时候需要指定是针对哪个控件。如图指定了控件Select,然后就可以指定Select的专有属性了。



  • Extend特点



  1. 方法不支持export。
    和Styles一样,无法真正做到为所有页面抽离出都可用的属性方法。

  2. 方法只能定义为当前页面内的全局方法。
    一定程度上全局方法存在引用GlobalThis,具体副作用未知。

  3. 方法需要指定控件,其他控件无法使用,只能对专有控件做到了抽离

  4. 方法可以传入参。
    相比Styles, 可以在其他属性不变的情况下,只修改其中的部分属性。


AttributeModifier


    对于上述两个装饰器都存在一个相同的限制,就是无法做到全局所有文件都可以公用。


    AttributeModifier的出现可以变相的解决这个问题。AttributeModifier本意是用于自定义控件中设置属性用的。但是我们在这里也可以通过这个机制,来实现全局所有文件中控件均可通用的属性。



  • 代码说明


image (2).png


    该Modifier只能针对专用控件,比如我要抽离一个通用的TextInput,那么我可以如上图所定义。


    需要实现一个接口 AttributeModifier,接口泛型定义和我们想要给哪个控件使用有关,比如我们想给TextInput使用,那么泛型就是 TextInputAttribute,如果给Column使用,那么泛型就是ColumnAttribute,以此类推。


    在该接口的实现方法中,定义控件的属性。



  • 布局中使用


image (3).png



  • 自定义属性


    我们还可以自定义部分属性,只需要修改TextInputAttribute,例如我们想自定义字体大小。可以定义变量。


image (4).png



  • 使用


image (5).png



  • AttributeModifier特点



  1. 可以全局给所有页面中的控件使用

  2. 可以自定义任何控件中的属性,包括特有属性

  3. 可以通过修改代码做成链式调用

  4. 该方法需要new对象,比较笨重,需要书写较多代码


@Builder


    上述说的都是针对单独的控件,如果我们想抽离一个通用的布局呢?或者我们的控件就是固定的可以拿来到处使用。


    比如我有一个Text,各种属性固定,只是文案不同,那么我使用上述几种都比较麻烦,需要书写较多代码。那么这个时候就可以使用builder了。



  • 代码


image (6).png


    我们可以在任意需要展示该Text的地方使用,直接调用该方法,对应位置就可以显示出内容了。原理相当于是将方法内的控件代码放到了对应的位置上。



  • 使用


image (7).png



  • @Builder特点



  1. 定义好方法后,需要拿Builder装饰器修饰,可以在任何一个页面内调用方法使用。

  2. 可以通过方法传递入参

  3. 无法通过方法拿到控件对象,只能在方法里操作控件属性

  4. 除了单一控件,还可以定义布局,布局中存在多个控件的情况

  5. 轻量级

  6. 方法即拿即用,代码量少


struct


    有时候,我们可能页面中存在大量如下UI:


image (8).png


    对于这种UI,我们完全可以抽离出为一个控件。然后我们页面需要展示的地方,直接调用该控件,设置标题,按钮文案等就可以简化了。


    我们可能想到使用builder来定义,但是builder只能写纯UI代码,这里还涉及到用户输入的内容,如何在点击按钮时候传过去。所以builder就无法使用了,这个时候就可以用struct封装了。



  • 代码


@Component
export struct InputNumberItemWithButton {
label: string = "标题"
buttonClick: (v: number) => void = () => {
}
buttonLabel: string = "设置"
inputPlaceholder: string = "我是提示语"
inputId: string = this.label
parentWidth: string = '100%'
private value: number = 0

build() {
RelativeContainer() {
Text(this.label)
.attributeModifier(Modifier.textLabel())
.id('label1')
.alignRules({
left: { anchor: '__container__', align: HorizontalAlign.Start },
top: { anchor: '__container__', align: VerticalAlign.Top },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
})
.margin({ left: 2 })

TextInput({ placeholder: this.inputPlaceholder })
.onChange((value: string) => {
this.value = Number.parseInt(value) ?? 0
})
.type(InputType.Number)
.id(this.inputId)
.height(30)
.placeholderFont({ size: 10 })
.fontSize(CommonStyle.INPUT_TEXT_SIZE)
.borderRadius(4)
.alignRules({
right: { anchor: 'button1', align: HorizontalAlign.Start },
left: { anchor: 'label1', align: HorizontalAlign.End },
top: { anchor: '__container__', align: VerticalAlign.Top },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
})
.margin({ left: 6, right: 6 })

Button(this.buttonLabel)
.attributeModifier(SuBaoSmallButtonModifier.create())
.onClick(() => {
this.buttonClick(this.value)
})
.id('button1')
.alignRules({
right: { anchor: '__container__', align: HorizontalAlign.End },
top: { anchor: '__container__', align: VerticalAlign.Top },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
})
.margin({ right: 2 })
}
.width(this.parentWidth)
.height(40)
.padding({
left: 5,
right: 5,
top: 2,
bottom: 2
})
.borderRadius(4)
}
}

    该struct中通过维护一个变量value 来保存用户输入的数字,然后在用户点击按钮时候传给点击事件方法,交给调用者调用。



  • 使用


image (9).png


    点击设置按钮,点击事件触发,a直接赋值。



  • struct特点



  1. 可以封装复杂组件,自定义组件

  2. 可以维护变量存储用户输入输出

  3. 可以所有页面全局使用

  4. 可以自定义属性

  5. 无法链式设置属性


对比各个使用场景


    实际编程中,一般都是混合相互配合使用,没必要单独硬使用哪一个。



  1. style
    可以用来定义一些通用属性,比如背景色,边角据等

  2. Extend
    对于页面中一些特殊的控件,用的地方较多时候,可以抽离方法

  3. AttributeModifier
    如果Extend无法满足,那么选择这个

  4. Builder
    对于布局控件的属性变化不大,但是用的地方多时候使用,比如定义一个分割线。

  5. struct
    涉及到用户输入输出时候,相关控件可以抽离封装,避免页面内上方定义太多变量,不好维护。


作者:MinQ
来源:juejin.cn/post/7374293974577692706
收起阅读 »

Promise 引入全新 API!效率提升 300%!

web
来源:前端开发爱好者 在 JavaScript 的世界里,Promise 一直是处理异步操作的神器。 而现在,随着 ES2025 的发布,Promise 又迎来了一个超实用的新成员——Promise.try()! 这个新方法简直是对异步编程的一次 “革命” ,...
继续阅读 »

来源:前端开发爱好者


在 JavaScript 的世界里,Promise 一直是处理异步操作的神器。


而现在,随着 ES2025 的发布,Promise 又迎来了一个超实用的新成员——Promise.try()


这个新方法简直是对异步编程的一次 “革命” ,让我们来看看它是怎么让代码变得更简单、更优雅的!


什么是 Promise.try()


简单来说,Promise.try() 是一个静态方法,它能把任何函数(同步的、异步的、返回值的、抛异常的)包装成一个 Promise。无论这个函数是同步还是异步,Promise.try() 都能轻松搞定,还能自动捕获同步异常,避免错误遗漏。


语法


Promise.try(func)
Promise.try(func, arg1)
Promise.try(func, arg1, arg2)
Promise.try(func, arg1, arg2, /* …, */ argN)

参数



  • func:要包装的函数,可以是同步的,也可以是异步的。

  • arg1arg2、…、argN:传给 func 的参数。


返回值


一个 Promise,可能的状态有:



  • 如果 func 同步返回一个值,Promise 就是已兑现的。

  • 如果 func 同步抛出一个错误,Promise 就是已拒绝的。

  • 如果 func 返回一个 Promise,那就按这个 Promise 的状态来。


为什么需要 Promise.try()


在实际开发中,我们经常遇到一种情况:不知道或者不想区分函数是同步还是异步,但又想用 Promise 来处理它。


以前,我们可能会用 Promise.resolve().then(f),但这会让同步函数变成异步执行,有点不太理想。


const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

上面的代码中,函数 f 是同步的,但用 Promise 包装后,它变成了异步执行。


有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?


答案是可以的,并且 Promise.try() 就是这个方法!


怎么用 Promise.try()


示例 1:处理同步函数


const syncFunction = () => {
  console.log('同步函数执行中');
  return '同步的结果';
};

Promise.try(syncFunction)
  .then(result => console.log(result)) // 输出:同步的结果
  .catch(error => console.error(error));

示例 2:处理异步函数


const asyncFunction = () => {
returnnewPromise(resolve => {
    setTimeout(() => {
      resolve('异步的结果');
    }, 1000);
  });
};

Promise.try(asyncFunction)
  .then(result =>console.log(result)) // 1秒后输出:异步的结果
  .catch(error =>console.error(error));

示例 3:处理可能抛出异常的函数


const errorFunction = () => {
  throw new Error('同步的错误');
};

Promise.try(errorFunction)
  .then(result => console.log(result))
  .catch(error => console.error(error.message)); // 输出:同步的错误

Promise.try() 的优势



  1. 统一处理同步和异步函数:不管函数是同步还是异步,Promise.try() 都能轻松搞定,代码更简洁。

  2. 异常处理:自动捕获同步异常,错误处理更直观,避免遗漏。

  3. 代码简洁:相比传统方法,Promise.try() 让代码更易读易维护。


实际应用场景


场景 1:统一处理 API 请求


function fetchData(url) {
  return Promise.try(() => fetch(url))
    .then(response => response.json())
    .catch(error => console.error('请求失败:', error));
}

fetchData('https://api.example.com/data')
  .then(data => console.log('数据:', data));

场景 2:混合同步和异步操作


const syncTask = () => '同步任务完成';
const asyncTask = () => new Promise(resolve => setTimeout(() => resolve('异步任务完成'), 1000));

Promise.try(syncTask)
  .then(result => console.log(result)) // 输出:同步任务完成
  .then(() => Promise.try(asyncTask))
  .then(result => console.log(result)) // 1秒后输出:异步任务完成
  .catch(error => console.error(error));

场景 3:处理数据库查询


function getUser(userId) {
  return Promise.try(() => database.users.get({ id: userId }))
    .then(user => user.name)
    .catch(error => console.error('数据库查询失败:', error));
}

getUser('123')
  .then(name => console.log('用户名称:', name));

场景 4:处理文件读取


function readFile(path) {
  return Promise.try(() => fs.readFileSync(path, 'utf8'))
    .catch(error => console.error('文件读取失败:', error));
}

readFile('example.txt')
  .then(content => console.log('文件内容:', content));

总结


Promise.try() 的引入让异步编程变得更加简单和优雅。


它统一了同步异步函数的处理方式,简化了错误处理,让代码更易读易维护。


ES2025 的这个新特性,绝对值得你去尝试!快去试试吧,你的代码会变得更清晰、更强大!


作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7494174524453158949
收起阅读 »

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

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

前言


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


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


准备工作


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


思路


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


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


实现


第一步:在mac上添加日历


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


image.png


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


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


1.1 查找日历



参考文档 mac查找日历



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


import { execSync } from 'child_process';

function checkCalendarExists(calendarName) {

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


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

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

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



附赠检验结果

image.png


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


1.2 日历创建



参考文档 mac 创建日历



import { execSync } from 'child_process';


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

try {

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

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

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

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

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

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


运行结果

image.png


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


1.3 创建日程


import { execSync } from 'child_process';

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

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

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

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


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

}

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

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

...
}


这里我们完善一下代码


import { execSync } from 'child_process';

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

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

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

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

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

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

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

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


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

}

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

try {

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

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

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

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


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

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


// 运行示例

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

handleCreateEvent(eventConfig)


运行结果

image.png


image.png


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


第二步: 定义MCP


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


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

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

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


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


这里附上完整的ts代码


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


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

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

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

let res = false;

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

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

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

return res
}

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

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

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

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


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

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

}

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

try {

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

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

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

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


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

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


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

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



第三步: 导入Windsurf


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


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


总结


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


祝好。


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

从劝退 flutter_screenutil 聊到不同尺寸 UI 适配的最佳实践

先说优点 💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。 由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位 ,实现对设计稿等比例的适配,同时保真程度一般很高。 在有设计稿的情况下,只使用 C...
继续阅读 »

先说优点



💡 先说优点叠个甲,毕竟库本身没有太大问题,往往都是使用的人有问题。



由于是基于设计稿进行屏幕适配的框架,在处理不同尺寸的屏幕时,都可以使用相同的 尺寸数值+单位 ,实现对设计稿等比例的适配,同时保真程度一般很高。


在有设计稿的情况下,只使用 Container + GestureDetector 都可以做到快速的开发,可谓是十分的无脑梭哈。


在:只考虑移动端、可以接受使用大屏幕手机看小屏幕 ui、不考虑大字体的模式、被强烈要求还原设计稿、急着开发。的情况下,还是挺好用的。


为什么劝退?



来到我劝退师最喜欢的一个问题,为什么劝退。如果做得不好,瞎搞乱搞,那就是我劝退的对象。



在亲身使用了两个项目并结合群里的各种疑惑,我遇到常见的有如下问题:


如何实现对平板甚至是桌面设备的适配?


由于基于设计稿尺寸,平板、桌面等设备的适配基本上是没法做的,要做也是费力不讨好的事。


千万不要想着说,我通过屏幕宽度断点来使用不同的设计稿,当用户拉动边框来修改页面的宽度时,体验感是很崩溃的。而且三套设计稿要写三遍不同的代码,就更不提了。(这里说三遍代码的原因是,计算 .w .h 的布局,数据会跟随设计稿变化)


如何适配大字体无障碍?


因为大字体缩放在满屏的 .w .h 下,也就是写死了尺寸的情况下,字体由于随系统字体放大,布局是绝对会溢出的。很多项目开发到最后上线才意识到自己有大字体无障碍的用户,甚至某些博客上,使用了一句:


MediaQuery.of(context).copyWith(textScaleFactor: 1.0),

来处理掉自己的用户,强制所有屏幕字体不可缩放。一时的勉强敷衍过去,最后只能等项目慢慢腐烂。


为什么在 1.w 的情况下会很糊?同样是 16.sp 为什么肉眼可见的不一样大?


库的原理很简单,提供了一堆的 api 相对于设计图的宽高去做等比例计算,所以必然存在一个问题,计算结果是浮点数。可是?浮点数有什么问题吗?


梳理一下原理:已知屏幕设计图宽度 sdw 、组件设计图宽度 dw ,根据屏幕实际宽度 sw ,去计算得出组件实际宽度 w


w = sw / sdw * dw

可是设计图的屏幕宽度 sdw 作为分母时,并不能保证总是可以被表示为有限小数。举个例子:库的文档中给的示例是 const Size(360, 690), 的尺寸,如果我需要一个 100.w 会得到多少?在屏幕宽度为 420 的情况下,得到组件宽度应该为 116.6666... 的无限小数


这会导致最终在栅格化时会面临消除小数点像素的锯齿问题。一旦有像素点的偏差,就会导致边缘模糊。


字体对尺寸大小更为敏感,一些非矢量的字体甚至只有几个档位的大小,当使用 14.5、15、15.5 的字体大小时,可能会得到一样的视觉大小,再加上 .sp 去计算一道,误差更是放大。



具体是否会发生在栅格化阶段,哪怕文章有误也无所谓,小数点像素在物理意义上就是不存在的,总是会面临锯齿平滑的处理,导致无法像素级还原 UI。



为什么部分屏幕下会溢出?


我们知道了有小数点问题,那么不得不说起计算机编程常见的一个不等式:


0.1 + 0.2 != 0.3

由于底层表示浮点数本身就有的精度问题,现在让 Flutter 去做这个加法,一样会溢出。考虑以下代码:


    Row(
children: [
SizedBox(width: 60.w),
SizedBox(width: 100.w),
SizedBox(width: 200.w),
],
);

在一个总共宽度 360.w 的设计图上,可能出现了溢出,如果不去使用多个屏幕来调试,根本不会觉得异常,毕竟设计图是这样做的,我也是这样写的,怎么可能有错呢?


然而恰恰是库本身的小数问题,加上编程届常见的底层浮点数精度问题,导致边缘溢出一点点像素。


我使用了 screenutil 为什么和真实的单位 1px 1rem 1dp 的大小不同呢?


哪怕是 .sp 都是基于设计图等比例缩放的,使用 screenutil 就从来不存在真实大小,计算的结果都是基于设计稿的相对大小。就连 .w.h 都没法保证比例相同,导致所有布局优先使用 .w 来编写代码的库,还想保证和真实尺寸相等?


为什么需要响应式 UI?


说个题外话:在面试淘菜菜的时候真的会有点崩不住,他们问如何做好不同屏幕的适配,我说首先这是 UI 出图的问题,如果 UI 出的图是响应式的,那没问题,照着写,闭着眼都能适配。


但是如果设计图不是响应式的,使用 flutter_screenutil 可以做到和设计图高保真等比还原,但是如果做多平台就需要 UI 根据屏幕断点出不同平台的设计图。


面试官立即就打断我说他们的 UI 只会出一份图。我当场就沉默了,然后呢?也不说话了?是因为只有移动端用户,或者说贵公司 UI 太菜了,还是说都太菜了。菜就给我往下学 ⏬


首先 UI 的响应式设计是 UI 的责任


抛开国情不谈,因为国内的 UI 能做到设计的同时,UI 还是响应式的,这样的 UI 设计师很少很少,他们能把主题规范好,约定好,已经是不得了的了。


但即使如此,响应式 UI 设计也还是应该归于 UI 设计中,在设计图中去根据不同的尺寸,拖动验证不同的布局效果是很容易的。在不同的尺寸下,应该怎么调整元素个数,应该如何去布局元素,只有 UI 使用响应式的写法去实现了,UI 和开发之间的无效交流才会减少。


响应式的 UI 可以避免精度问题


早在 19 年我就有幸翻阅了一本 iOS 的 UI 设计规范,当时有个特别的点特别印象深刻:尺寸大小应该为 2 的整数次幂,或者 4 的倍数。因为这样做,在显示和计算上会较为友好。



💡 这其实是有点历史原因的,之前的 UI 在栅格化上做得并不是很好,锯齿化严重也是常态,所以使用可以被 2 整除的尺寸,一方面使用起来只有几个档位,方便调整;另一方面这样的尺寸可以在像素的栅格化上把小数除尽。



举个例子,在屏幕中间显示一个 300 宽度的卡片,和边距 16 的卡片,哪一个更响应式,无疑是后者,前者由于需要计算 300 相对与设计稿屏幕的宽度,后者只需要准确的执行 16 的边距就好,中间的卡片宽度随屏幕的宽度自动变化。


同样的例子,带有 Expanded 布局的 Row 组件,相比直接给定每个子组件尺寸导致精度问题的布局,更能适配不同的屏幕。因为 Row 会先放置固定大小的组件,剩余空间由 Expanded 去计算好传给子组件,原理和 Web 开发中的 flex 布局一样。


响应式布局是通用的规范


如果有 Web 开发经验的,应该会知道 Web 的屏幕是最多变的,但是设计起来也可以很规范,常见的 bootstrap 框架就提到了断点这个观点,指出了当我们去做 UI 适配的时候,需要根据不同的屏幕大小去做适配。同时 flex 布局也是 Web 布局中常用的响应式布局手段。


在设计工具中,响应式 UI 也没有那么遥远,去下载一份 Material Design 的 demo,对里面的组件自由的拉伸缩放,再对比一下自己通过输入尺寸大小拼凑在一起的 UI,找找参数里面哪里有差异。


怎么做响应式 UI


这里直接放一个谷歌大会的演讲,我相信下面的总结其实都可以不用看了,毕竟本实验室没有什么可补充的,但是我们还是通过从外到内、从整体到局部的顺序来梳理一下如何去做一个响应式的 UI,从而彻底告别使用 flutter_screenutil。


http://www.youtube.com/watch?v=LeK…


SafeArea


一个简单的组件,可以确保内部的 UI 不会因为愚蠢的设备圆角、前置挖孔摄像头、折叠屏链接脚、全面屏边框等原因而被意外的裁剪,将重要的内容,显示在“安全区”中。


屏幕断点


让 UI 根据不同的尺寸的窗口变化而变化,首先就要使用 MediaQuery.sizeOf(context);LayoutBuilder() 来实现对窗口的宽度的获取,然后通过不同的屏幕断点,去构建不同情况下的 UI。


其中 LayoutBuilder 还能获取当前约束下的宽度,以实现页面中子区域的布局,比如 Drawer 的宽度,对话框的宽度,导航的宽度。


这里举了个例子,使用媒体查询获得窗口宽度之后,展示不同的 Dialog



写出如此优雅的断点代码只需要三步:



  • 抽象:找到全屏对话框和普通对话框中共同的属性,并将功能页面提取出来。

  • 测量:思考应该使用窗口级别的宽度(MediaQuery),还是某个约束下的宽度(LayoutBuilder)。

  • 分支:编写如上图所示的带有断点逻辑的代码。



GridView


熟悉了移动端的 ListView 布局之后,切换到 GridView 布局并适配到平板、桌面端,是一件十分自然的事,只需要根据情况使用不同的 gridDelegate 属性来设置布局方式,就能简单的适配。


这里一般使用 SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: ) 方法来适配,传入一个期望的最大宽度,使其在任何屏幕上看到的子组件都自然清晰,GridView 会根据宽度计算出合适的一行里合适的列数。


Flex 布局,但是 Flutter 版


前面说过了尽量不要去写固定尺寸的几个元素加起来等于屏幕宽度,没有那么巧合的事情。在 Row/Column 中,善用 Expanded 去展开子组件占用剩余空间,善用 Flexible 去缩紧子组件,最后善用 Spacer 去占用空白,结合 MainAxisAlignment 的属性,你会发现布局是那样的自然。


只有部分组件是固定尺寸的


例如 Icon 一般默认 24,AppBar 和 BottomNavigationBar 高度为 56,这些是写在 MD 设计中的固定尺寸,但是一般不去修改。图片或许是固定尺寸的,但是一般也使用 AspectRatio 来固定宽高比。


我曾经也说过一个普遍的公理,因为有太多初学者容易因为这个问题而出错了。



当你去动态计算宽高的时候,可能是布局思路有问题了。



在大多数情况下,你的布局都不应该计算宽高,交给响应式布局,让组件通过自己的能力去得出自己的位置、约束、尺寸。


举一个遇到过的群友问题,他使用了 stack 布局包裹了应用栏和一个滚动布局,由于SliverAppBar 拉伸后的高度会变化,他想去动态的计算下方的滚动布局的组件起始位置。这个问题就连描述出来都是不可思议的,然后他问我,我应该如何去获取这个 AppBar 的高度,因为我想计算下方组件的高度。(原问题记不清了,但是这样的需求是不成立的)


最后,多看文档


最后补上关于 MD3 设计中,关于布局的文档,仔细学习:


Layout – Material Design 3


最后的最后,响应式布局其实是一个很宽的话题,这里没法三言两语说完,只能先暂时在某些领域劝退使用这个库。任何觉得可能布局困难的需求,都可以发到评论区讨论,下一篇文章我们将根据几个案例来谈谈具体的实践。


作者:优雅实践实验室
来源:juejin.cn/post/7386947074640298038
收起阅读 »

聊聊四种实时通信技术:长轮询、短轮询、WebSocket 和 SSE

这篇文章,我们聊聊 四种实时通信技术:短轮询、长轮询、WebSocket 和 SSE 。 1 短轮询 浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新)。 优点:实现简单,兼容性极佳 缺点:高频请求浪费资源,实时性...
继续阅读 »

这篇文章,我们聊聊 四种实时通信技术:短轮询、长轮询、WebSocket 和 SSE 。


1 短轮询


浏览器 定时(如每秒)向服务器发送 HTTP 请求,服务器立即返回当前数据(无论是否有更新)。




  • 优点:实现简单,兼容性极佳

  • 缺点:高频请求浪费资源,实时性差(依赖轮询间隔)

  • 延迟:高(取决于轮询频率)

  • 适用场景:兼容性要求高,延迟不敏感的简单场景。


笔者职业生涯印象最深刻的短轮询应用场景是比分直播



如图所示,用户进入比分直播界面,浏览器定时查询赛事信息(比分变动、黄红牌等),假如数据有变化,则重新渲染页面。


这种方式实现起来非常简单可靠,但是频繁的调用后端接口,会对后端性能会有影响(主要是 CPU)。同时,因为依赖轮询间隔,页面数据变化有延迟,用户体验并不算太好。


2 长轮询


浏览器发送 HTTP 请求后,服务器 挂起连接 直到数据更新或超时,返回响应后浏览器立即发起新请求。




  • 优点:减少无效请求,比短轮询实时性更好

  • 缺点:服务器需维护挂起连接,高并发时资源消耗大

  • 延迟:中(取决于数据更新频率)

  • 适用场景:需要较好实时性且无法用 WebSocket/SSE 的场景(如消息通知)


长轮询最常见的应用场景是:配置中心,我们耳熟能详的注册中心 Nacos 、阿波罗都是依赖长轮询机制。


nacos长轮询



客户端发起请求后,Nacos 服务端不会立即返回请求结果,而是将请求挂起等待一段时间,如果此段时间内服务端数据变更,立即响应客户端请求,若是一直无变化则等到指定的超时时间后响应请求,客户端重新发起长链接。



3 WebSocket


基于 TCP 的全双工协议,通过 HTTP 升级握手(Upgrade: websocket)建立持久连接,双向实时通信。 image.png



  • 优点:最低延迟,支持双向交互,节省带宽

  • 缺点:实现复杂,需单独处理连接状态

  • 延迟:极低

  • 适用场景:聊天室、在线游戏、协同编辑等 高实时双向交互 需求


笔者曾经服务于北京一家电商公司,参与直播答题功能的研发。



直播答题整体架构见下图:



Netty TCP 网关的技术选型是:Netty、ProtoBuf、WebSocket ,选择 WebSocket 是因为它支持双向实时通信,同时 Netty 内置了 WebSocket 实现类,工程实现起来相对简单。


4 Server Send Event(SSE)


基于 HTTP 协议,服务器可 主动推送 数据流(如Content-Type: text/event-stream),浏览器通过EventSource API 监听。




  • 优点:原生支持断线重连,轻量级(HTTP协议)

  • 缺点:不支持浏览器向服务器发送数据

  • 延迟:低(服务器可即时推送)

  • 适用场景:股票行情、实时日志等 服务器单向推送 需求。


SSE 最经典的应用场景是 : DeepSeek web 聊天界面 ,如图所示:



当在 DeepSeek 对话框发送消息后,浏览器会发送一个 HTTP 请求 ,服务端会通过 SSE 方式将数据返回到浏览器。



5 总结


特性短轮询长轮询SSEWebSocket
通信方向浏览器→服务器浏览器→服务器服务器→浏览器双向通信
协议HTTPHTTPHTTPWebSocket(基于TCP)
实时性极高
资源消耗高(频繁请求)中(挂起连接)低(长连接)

选择建议



  • 需要 简单兼容性 → 短轮询

  • 需要 中等实时性 → 长轮询

  • 只需 服务器推送 → SSE

  • 需要 全双工实时交互 → WebSocket




作者:勇哥Java实战
来源:juejin.cn/post/7496375493329174591
收起阅读 »

做Docx预览,一定要做这个神库!!

web
来源:沉浸式趣谈只需几行代码,你就能在浏览器中完美预览 Word 文档,甚至连表格样式、页眉页脚都原汁原味地呈现出来。接下来,给大家分享两个 Docx 预览的库:docx-preview VS mammothdocx-preview和mammoth是目前最流行...
继续阅读 »

来源:沉浸式趣谈

只需几行代码,你就能在浏览器中完美预览 Word 文档,甚至连表格样式、页眉页脚都原汁原味地呈现出来。

接下来,给大家分享两个 Docx 预览的库:

docx-preview VS mammoth

docx-previewmammoth是目前最流行的两个 Word 文档预览库,它们各有特色且适用于不同场景。

docx-preview:还原度爆表的选择

安装简单:

npm install docx-preview

基础用法:

import { renderAsync } from 'docx-preview';

// 获取到docx文件的blob或ArrayBuffer后
renderAsync(docData, document.getElementById('container')).then(() => console.log('文档渲染完成!'));

试了试后,这个库渲染出来的效果简直和 Office 打开的一模一样!连段落格式、表格样式、甚至是分页效果,都完美呈现。

mammoth:简洁至上的转换器

mammoth 的思路完全不同,它把 Word 文档转成干净的 HTML:

npm install mammoth

使用也很简单:

import mammoth from 'mammoth';

mammoth.convertToHtml({ arrayBuffer: docxBuffer }).then(result => {
    document.getElementById('container').innerHTML = result.value;
    console.log('转换成功,但有些警告:', result.messages);
});

转换出来的 HTML 非常干净,只保留了文档的语义结构。

比如,Word 中的"标题 1"样式会变成 HTML 中的

标签。

哪个更适合你?

场景一:做了个简易 Word 预览器

要实现在线预览 Word 文档,且跟 "Word" 长得一模一样。

首选docx-preview

import { renderAsync } from'docx-preview';

async functionpreviewDocx(fileUrl) {
    try {
        // 获取文件
        const response = awaitfetch(fileUrl);
        const docxBlob = await response.blob();

        // 渲染到页面上
        const container = document.getElementById('docx-container');
        awaitrenderAsync(docxBlob, container, null, {
            className: 'docx-viewer',
            inWrapper: true,
            breakPages: true,
            renderHeaders: true,
            renderFooters: true,
        });

        console.log('文档渲染成功!');
    } catch (error) {
        console.error('渲染文档时出错:', error);
    }
}

效果很赞!文档分页显示,目录、页眉页脚、表格边框样式都完美呈现。

不过也有些小坑:

    1. 文档特别大时,渲染速度会变慢
    1. 一些复杂的 Word 功能可能显示不完美

场景二:做内容编辑系统

需要让用户上传 Word 文档,然后提取内容进行编辑。

选择mammoth

import mammoth from'mammoth';

async functionextractContent(file) {
    try {
        // 读取文件
        const arrayBuffer = await file.arrayBuffer();

        // 自定义样式映射
        const options = {
            styleMap: ["p[style-name='注意事项'] => div.alert-warning""p[style-name='重要提示'] => div.alert-danger"],
        };

        const result = await mammoth.convertToHtml({ arrayBuffer }, options);
        document.getElementById('content').innerHTML = result.value;

        if (result.messages.length > 0) {
            console.warn('转换有些小问题:', result.messages);
        }
    } catch (error) {
        console.error('转换文档失败:', error);
    }
}

mammoth 的优点在这个场景下完全发挥出来:

  1. 1. 语义化 HTML:生成干净的 HTML 结构
  2. 2. 样式映射:可以自定义 Word 样式到 HTML 元素的映射规则
  3. 3. 轻量转换:处理速度非常快

进阶技巧

docx-preview 的进阶配置

renderAsync(docxBlob, container, styleContainer, {
    className'custom-docx'// 自定义CSS类名前缀
    inWrappertrue// 是否使用包装容器
    ignoreWidthfalse// 是否忽略页面宽度
    ignoreHeightfalse// 是否忽略页面高度
    breakPagestrue// 是否分页显示
    renderHeaderstrue// 是否显示页眉
    renderFooterstrue// 是否显示页脚
    renderFootnotestrue// 是否显示脚注
    renderEndnotestrue// 是否显示尾注
    renderCommentstrue// 是否显示评论
    useBase64URLfalse// 使用Base64还是ObjectURL处理资源
});

超实用技巧:如果只想把文档渲染成一整页(不分页),只需设置breakPages: false

mammoth 的自定义图片处理

默认情况下,mammoth 会把图片转成 base64 嵌入 HTML。

在大型文档中,这会导致 HTML 特别大。

更好的方案:

const options = {
    convertImage: mammoth.images.imgElement(function (image) {
        return image.readAsArrayBuffer().then(function (imageBuffer) {
            // 创建blob URL而不是base64
            const blob = newBlob([imageBuffer], { type: image.contentType });
            const url = URL.createObjectURL(blob);

            return {
                src: url,
                alt: '文档图片',
            };
        });
    }),
};

mammoth.convertToHtml({ arrayBuffer: docxBuffer }, options).then(/* ... */);

这样一来,图片以 Blob URL 形式加载,页面性能显著提升!

其他方案对比

说实话,在选择这两个库之前,也有其他解决方案:

微软 Office Online 在线预览

利用微软官方提供的 Office Online Server 或 Microsoft 365 的在线服务,通过嵌入 WebView 或 

优点

  • • 格式高度还原:支持复杂排版、图表、公式等。
  • • 无需本地依赖:纯浏览器端实现。
  • • 官方维护:兼容性最好。

折腾一圈,还是docx-previewmammoth这俩兄弟最实用。

它们提供了轻量级的解决方案,仅需几十 KB 就能搞定 Word 预览问题,而且不需要依赖外部服务,完全可以在前端实现。


作者:独立开阀者_FwtCoder
来源:juejin.cn/post/7493733975779917861
收起阅读 »

记一下iOS App无法访问到网络

iOS
记一下iOS App无法访问到网络 最近遇到一件十分诡异的事情,那就是按照平时进行Flutter Debug运行我们项目的时候,日志全部都在报错,统一指向了当前App无法访问到网络。 最开始我还没有当回事,觉得这小问题很好解决。 没有给App授权网络访问? 我...
继续阅读 »

记一下iOS App无法访问到网络


最近遇到一件十分诡异的事情,那就是按照平时进行Flutter Debug运行我们项目的时候,日志全部都在报错,统一指向了当前App无法访问到网络。


最开始我还没有当回事,觉得这小问题很好解决。


没有给App授权网络访问?


我最开始是怀疑我没给当前的App进行授权,但是当我在设置看了之后发现已经授权移动网络和Wifi,对于开发,正常的授权还是不会忘记的,这个原因排除?


手机链接的Wifi的问题?


因为手机链接是公司的内网,其次我将原因怪罪在我们公司的内网身上,随后我关闭了Wifi进行重启,问题依然没有得到解决。


手机开代理的问题?


因为经常开代理去一些网站,所以我怀疑是我手机开了代理。看了一下没开,我果断的重置了手机的网络,发现重置之后问题依然存在。


难道我电脑开启代理问题?


在思考这个问题的时候,我都觉得不可能,手机又不访问我电脑的网络,怎么可能。为了找到问题的原因,我还是关掉了软件,重启App发现还是不行?


难道需要需要重装App?


到这个时候我即将崩溃,我都怀疑自己了。删了手机上的APP,之后重新运行授权,还是提示访问不到网络。


难道手机需要重启?


万能的重启大法,随后我重启已经运行不知道多少天的手机。果然,重启大法在我这里无效,还是不行。


难道工程配置有问题?


为了验证这个问题,我找来测试机,进行安装,发现测试机竟然一切正常。


千呼万唤始出来


看到测试机可以,我的手机不行,当时我都怀疑这个世界了是否存在灵异了。后来冷静的分析了一下测试机和我的手机的区别,一样访问公司的内网。一样工程配置运行的,还有什么不一样的因素引起测试机和我的手机的区别?


并且在几天之前还是可以的,这几天我一直做新需求,都没有用我们工程,这几天对接才运行发现不能访问网络了?


在之前可以和现在不可以之间的时间,这个手机发生了什么?对于测试机上面只有几个竞品和我们自己的APP,我的手机运行了一堆的APP。


运行一堆但是又有什么影响了?等等!后来我想到了我的手机还运行一堆当时为了做需求找的插件运行一堆的Example App


它们的Bundle ID都不一样,怎么可能影响。虽然心里面这样猜测,但是还是行动起来进行删掉。都没打算能修复,但是随着最后一个APP删除之后,我们的APP的日志打开了正常的流程,意味着可以访问网络了。


因为APP做了没网就无法继续的逻辑,检测有网就继续执行的操作,所以到此为止,我验证出运行其他Example App影响我们目前APP的访问网络。


这个结论是为了什么,目前没有任何的文献找到原因,可能是苹果不允许开发者运行多少没有创建证书的APP运行手机访问网络吧。


作者:君赏
来源:juejin.cn/post/7503532008497840147
收起阅读 »

产品小姐姐:地图(谷歌)选点,我还不能自己点?

web
💡 背景 最近在做海外项目,需要让用户选择一个实际地点——比如设置店铺位置、收货地址、活动举办地等。 我:不就是 uni.getLocation(object) 嘛,可惜海外项目用不了高德地图和百度地图,只能转向谷歌地图。 在@googlemaps/j...
继续阅读 »

💡 背景



最近在做海外项目,需要让用户选择一个实际地点——比如设置店铺位置、收货地址、活动举办地等。


我:不就是 uni.getLocation(object) 嘛,可惜海外项目用不了高德地图和百度地图,只能转向谷歌地图。



image.png



在@googlemaps/js-api-loader和vue3-google-map一顿折磨之后,不知道是不是使用方式错了,谷歌地图只在h5上显示,真机(包括自定义基座)都显示不了地图。无奈,只能转向WebView,至此,开始手撕谷歌地图: 地图选点 + 搜索地址 + 点击地图选点 + 经纬度回传



image.png




🎬 场景设定:选房子


某天产品说:“用户能搜地址,也能点地图,最后把这些地点存起来显示在地图。”


我听着简单,于是点开地图,灵光一闪:这不就是选房的逻辑吗?



  • 用户可以搜地段(搜索框)

  • 也可以瞎逛看到喜欢的(点击地图)

  • 最后点个确定,告诉中介(确认按钮)


我:“你疯啦?这是太平洋中间。”


产品:“不是,这是用户自由。”




🧱 核心结构分析


📦 页面骨架


<div id="map"></div>
<div id="overlay-controls">
<input id="search-input" ... />
<div id="confirm-btn">确定</div>
</div>
<script type="text/javascript" src="https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=你的key&callback=initMap" async defer></script>
<script>

这是一个标准的地图 + 控制浮层结构。我们用一个 #map 占据全屏,再通过 position: absolute 让搜索框和按钮漂浮在上面。(ps:注意必须引入uni.webview才能进行通讯)




🧠 方法逐个看


1. initMap:地图的灵魂觉醒


function initMap() { ... }


  • 调用时机:Google Maps 的 callback 会自动触发

  • 作用:初始化地图、绑定事件、准备控件




2. 获取定位:我在哪我是谁


if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(...)
}


  • 成功:把你真实的位置显示出来

  • 失败:退而求其次用旧金山




3. 搜索地址:让用户自己找方向


const autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.addListener("place_changed", () => {
const place = autocomplete.getPlace();
...
});


  • 功能:用 Google 提供的地址搜索建议

  • 高级点:可以定位到建筑物级别的精度

  • 产品:用户脑子里比你更清楚他想去哪


地图1.gif




4. 点地图选点:给随性的人自由


map.addListener("click", (e) => {
const lat = e.latLng.lat();
const lng = e.latLng.lng();
...
});


  • 功能:用户随手一点,就能选中那个点

  • 技术点:用 Geocoder 反解析经纬度 ➜ 地址

  • 实用性:解决“我不知道地址叫什么”的痛点,且可切换卫星实景图像选点


就像: 当年你去面试,不知道公司叫什么,只知道“拐角有个便利店”。


地图2.gif




5. setLocation:标记我心之所向


function setLocation(lat, lng, address) {
selectedLatLng = { lat, lng };
selectedAddress = address;
...
}


  • 核心职责:更新选择结果,设置 marker

  • 重复点击自动替换 marker,保持页面整洁

  • UI 响应式体验的小心机,细节满满


哲理时间: 你不能同时站在两个地方,虽然marker可以,但是此处marker不做分布点,只作为当前点击地点。




6. confirm-btn:确定这就是你的人生目标吗?


document.getElementById("confirm-btn").addEventListener("click", () => {
if (!selectedLatLng) {
alert("请先选择地点");
return;
}
uni.postMessage({ data: { ... } });
uni.navigateBack({ delta: 1 });
});


  • 检查用户是否真的选点了

  • uni.postMessage 把选中的地址、经纬度送回 uniapp 主体页面

  • 然后自动关闭 WebView,返回主流程


产品视角: 用户选完东西,你就别啰嗦了,自己退出。


微信图片_20250508174451.jpg


可查看卫星实景图像,点击地图选点


image.png


点击地图拿到地点数据就可以继续业务处理啦~




🎁 彩蛋动画:CSS Loading


<div class="loader" id="loader"></div>

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

加载的时候出现个小旋转圈圈,用户等得不烦,体验感 +1。

这就像爱情:等一等,说不定就没了。




✅ 功能总结


功能实现方式
地图显示Google Maps JS SDK
获取当前位置navigator.geolocation
搜索地点google.maps.places.Autocomplete
点击地图选点map.addListener("click") + Geocoder
回传经纬度uni.postMessage
用户体验优化marker 替换、加载动画



🧘 写在最后


这个地图选点组件,看似只是点点点,但背后涉及用户体验、API 使用、移动端交互的多种协作。本文只写了大概方法思路,具体实现看具体业务需求。


下次再见!🌈


Snipaste_2025-04-27_15-18-02.png


作者:不爱说话郭德纲
来源:juejin.cn/post/7501649258279845939
收起阅读 »

Vue动态弹窗(Dialog)新境界:告别繁琐,拥抱优雅!🎉

web
写在开头 嘿,各位好呀!😀 今是2025年04月30日,明天就是五一假期了,激动的心从早上就一直没有沉静过,午休的时候闭着眼半小时硬是没睡着,哎,这班是一点也上不下去。 好!说回正题,本次要分享的是关于如何在Vue中比较优雅的调用弹窗的过程,请诸君按需食用哈...
继续阅读 »

写在开头


嘿,各位好呀!😀


今是2025年04月30日,明天就是五一假期了,激动的心从早上就一直没有沉静过,午休的时候闭着眼半小时硬是没睡着,哎,这班是一点也上不下去。


394674CE.jpg


好!说回正题,本次要分享的是关于如何在Vue中比较优雅的调用弹窗的过程,请诸君按需食用哈。


需求背景


最近,小编在捣鼓一个和低代码拖动交互类似的业务,说到低代码,大家肯定都不陌生吧❓像低代码表单、低代码图表平台等,用户可以通过简单的拖拽操作,像搭积木一样,快速"拼"出一个功能完善的表单页面,或者酷炫的数据可视化大屏。


而在这些低代码平台中,配置组件属性的交互方式通常有两种主流玩法:


其一,三栏式布局,左边是组件列表,中间是画布/预览区,右边是属性配置面板。选中中间画布的某个组件,右侧面板就自动显示它的配置项,如下:


image.png


其二,弹窗式配置,同样从左侧拖拽组件到画布,但选中组件后,通常会看到一个"设置"或"编辑"按钮。点击这个按钮,Duang~ ✨ 弹出一个专门的配置窗口 (Dialog),让你在里面集中完成所有设置。


image.png


这两种交互各有千秋,不评判好坏哈,反正合适自己业务场景的才是最好的。


然,今天咱们重点聚焦第二种:点击按钮弹出 Dialog 进行配置的场景。


这种方式在很多场景下也很常见,比如配置项特别多、需要更沉浸式的设置体验时。


但问题也随之而来:如果平台支持的组件越来越多,这里咱们假设是低代码图表场景,如柱状图、折线图、饼图、地图、文本、图片...等等,每个组件都需要一个独立的配置弹窗...🤔,那么,我们应该如何设计一套优雅、可扩展、易维护的代码架构来管理这些层出不穷的 Dialog 呢?🤔


结构设计


万事开头难,尤其是在做一些稍微带点设计或架构意味的事情时,切忌盲目上手。心里得先有个谱,想清楚大致方向,否则等到后面业务需求像潮水般涌来,迭代压力陡增时,你就会深刻体会到早期设计不佳带来的痛苦了(别问小编是怎么知道的...😭)。


当然,如果你已是经验丰富的老司机,那就当我没说哈。😂


面对"组件点击按钮弹出配置框"这个需求,最开始,最直观的想法可能就是:一个组件配一个专属的 Dialog.vue 文件,相互独立,互不影响,挺好不是❓


比如,咱当前有柱状图、折线图、饼图三个组件,那么它们的目录结构可能是这样子的:


src/
├── components/
│ ├── BarChart/
│ │ ├── Drag.vue # 组件的拖动视图
│ │ ├── Dialog.vue # 组件的配置弹窗
│ │ └── index.js # 组件的Model
│ ├── LineChart/
│ │ ├── Drag.vue # 组件的拖动视图
│ │ ├── Dialog.vue # 组件的配置弹窗
│ │ └── index.js # 组件的Model
│ ├── PieChart/
│ │ ├── Drag.vue # 组件的拖动视图
│ │ ├── Dialog.vue # 组件的配置弹窗
│ │ └── index.js # 组件的Model
└── App.vue # 入口

咱们不详说其他文件中的代码情况,仅关注每个组件中 Dialog.vue 文件的代码要如何写❓


可能大概是这样:


<template>
<el-dialog :modelValue="modelValue">
<div>内容....</div>
</el-dialog>

</template>
<script>
defineProps({
modelValue: Boolean,
});
</script>



小编这里使用 Element-Plusel-dialog 组件作为案例演示。



然后,为了在页面上渲染这些不同组件的 Dialog.vue,最笨的方法可能是在父组件里面用 v-if/v-else-if 来判断, 或者高级一点使用 <component :is="currentDialog"> 再配合一堆 import 来动态加载渲染。父组件需要维护哪个弹窗应该显示的状态,以及负责传递数据和接收结果,逻辑很快变得复杂且难以维护。


在项目初期,组件类型少的时候,这种方式确实能跑通,没有问题❗



你就说它能不能跑吧,就算它不能跑,你能跑不就行😋,项目和你总有一个能跑的。



但随着业务不断迭代,支持的组件类型越来越多,这种"各自为战"的模式很快就暴露出了诸多问题,其中有两个问题比较尖锐:



  • 缺乏统一控制📝:如果想给所有弹窗统一调整弹窗配置、或者添加一个水印、或者调整一下默认样式、或者增加一个通用的"重置"按钮,怎么办?只能去每个 Dialog.vue 文件里手动修改,效率低下不说,还极易遗漏或出错。

  • 代码冗余严重📜:每个 Dialog.vue 文件里,关于弹窗的显示/隐藏逻辑、确认/取消按钮的处理、与 Element Plus (或其他 UI 库) ElDialog 组件的交互代码,几乎都是大同小异的模板代码,写到后面简直是精神污染。(这里手动Q一下我同事🔨)


总之,随着项目的迭代,这种最初看似简单的结构,维护成本越来越高,每次增加或修改一个组件的配置弹窗都成了一种"折磨"。


那么,要如何重新来设计这个架构呢❓


小编采用的是基于动态创建和静态方法关联的架构,其架构的核心理念就是:将通用的弹窗逻辑(创建、销毁、交互)抽离出来,让每个组件的配置面板(Panel)只专注于自身的配置项 UI 视图和数据处理逻辑 ,从而实现高内聚、低耦合、易扩展的目标。


先来瞅瞅目录结构的最终情况👇:


src/
├── components/
│ ├── BarChart/
│ │ ├── Dialog/
│ │ | ├── index.js # Dialog 组件的入口
│ │ | ├── Panel.vue # Dialog 组件UI视图
│ │ ├── Drag.vue
│ │ └── index.js
│ ├── LineChart/
│ │ ├── Dialog/
│ │ | ├── index.js # Dialog 组件的入口
│ │ | ├── Panel.vue # Dialog 组件UI视图
│ │ ├── Drag.vue
│ │ └── index.js
│ ├── PieChart/
│ │ ├── Dialog/
│ │ | ├── index.js # Dialog 组件的入口
│ │ | ├── Panel.vue # Dialog 组件UI视图
│ │ ├── Drag.vue
│ │ └── index.js
│ ├── BaseDialog.vue
│ └── index.js
├── utils/
│ ├── BaseControl.js
│ └── dialog.js
└── App.vue # 入口

关键变动是 Dialog.vue 变成了 Dialog/index.jsDialog/Panel.vue,它们俩的作用:



  • Panel.vue:负责"长什么样"和"填什么数据" 。

  • index.js:负责"怎么被调用"和"调用时带什么默认配置",并将 Panel.vue 包装后提供给外部使用。


具体实现


接下来,咱们就详细拆解一下这套新架构的设计具体代码实现过程。👇


但为了更好的讲述关键代码的实现,咱们不管拖动那块逻辑,仅通过点击按钮简单的来模拟,效果如下:


0430-01.gif

本次小编是新建了一个 Vue3 的项目并且安装了 ElementPlus 进行了全局引入,基础项目环境就这样。


然后,从入口出发(App.vue):


<template>
<el-button type="primary" v-for="type in componentList" :key="type" @click="openDialog(type)">
{{ type }}
</el-button>

</template>

<script setup>
import { ElButton } from "element-plus";
import { componentMap } from "./components"; // 引入组件映射

/** @name 实例化所有组件 **/
const componentInstanceMap = Object.keys(componentMap).reduce((pre, key) => {
const instance = new componentMap[key]();
pre[key] = instance;
return pre;
}, {});

/** @name 打开组件弹窗 **/
async function openDialog(type) {
const component = await componentMap[type].DialogComponent.create(
{ type },
componentInstanceMap[type]
);
console.log("component", component);
}
</script>


统一管理所有组件导出文件(components/index.js):


import PieChart from "./PieChart";
import BarChart from "./BarChart";
import LineChart from "./LineChart";

export const componentMap = {
[PieChart.type]: PieChart,
[BarChart.type]: BarChart,
[LineChart.type]: LineChart,
};

/** @typedef { keyof componentMap } ComponentType */

组件入口文件(components/PieChart/index.js):


import BaseControl from "../../utils/BaseControl";
import Drag from "./Drag.vue";
import Dialog from "./Dialog";

class Component extends BaseControl {
static type = "barChart";
label = "柱状图";
icon = "bar-chart";

getDialogDataDefault() {
return {
title: { text: "柱状图" },
tooltip: { trigger: "axis" },
};
}

static DragComponent = Drag;
static DialogComponent = Dialog;
}

export default Component;

该文件用于集中管理组件的核心数据结构与统一的业务逻辑。



咱们以柱状图为例哈。📊



所有组件的基类文件(utils/BaseControl.js):


/** @typedef { import('vue').Component|import('vue').ConcreteComponent } VueConstructor */

export default class BaseControl {
/** @name 组件唯一标识 **/
type = "baseControl";
/** @name 组件label **/
label = "未知组件";
/** @name 组件高度 **/
height = "110px";
constructor() {
if (this.constructor.type) {
this.type = this.constructor.type;
}
}
/**
* @name 拖动组件
* @type { VueConstructor | null }
*/

static DragComponent = null;
/**
* @name 弹窗组件
* @type { VueConstructor | null }
*/

static DialogComponent = null;

dialog = {};
/**
* @name 用于获取Dialog组件的默认数据
* @returns {Dialog} 默认数据
*/

getDialogDataDefault() {
return {};
}
}

该文件是所有组件的"基石"🏛️,每个具体的图表组件都继承自 BaseControl 类,并在该基础上定义自己特有的信息和逻辑。


组件的拖动视图组件(Drag.vue),这个可以先随便整一个,暂时用不上:


<template>
<div>某某组件的拖动视图组件</div>
</template>

Dialog 组件的入口文件(components/BarChart/Dialog/index.js):


import Panel from "./Panel.vue"; // Dialog 的 UI 视图组件
import { dialogWithComponent } from "../../../utils/dialog.js";

/**
* @name 静态方法,渲染Dialog组件,并且可在此处自定义dialog组件的props
* @param {{ component: object, instance: object, componentDataAll: Array<object> }} contentProps 组件数据
* @returns {Promise<any>}
*/

Panel.create = async (panelProps = {}) => {
return dialogWithComponent((render) => render(Panel, panelProps), {
title: panelProps.label,
width: "400px",
});
};

export default Panel;

该文件导入真正的 UI 视图面板(Panel.vue),然后给组件挂载了一个静态 create 方法。这个 create 方法用于动态创建 Dialog 组件,它内部调用 dialogWithComponent 方法,并可以在此处预设一些该 Dialog 组件特有的配置(如默认标题、宽度)。


Dialog 组件的 Panel.vue 文件:


<template>
<h1>柱状图的配置</h1>
</template>

<script setup>
defineExpose({
async getValue() {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
});
return { type: "barChart" };
}
})
</script>


该组件仅放置柱状图特有的配置信息,并且不需要管弹窗自身的逻辑行为,很干净很专注😎。还有,它内部必须对外提供一个 getValue 方法❗用于在用户点击确认时调用,以获取最终的配置数据。


核心工具函数(utils/dialog.js)文件 :


import { createApp, h, ref } from "vue";
import { ElDialog, ElMessage } from "element-plus";
import BaseDialog from "../components/BaseDialog.vue";

/**
* @name 协助统一创建dialog组件,并且进行挂载、销毁、上报
* @param {import('vue').Component|Function} ContentComponent 渲染的组件
* @param {import('element-plus').dialogProps} dialogProps dialog组件的props
* @returns {Promise<any>}
*/

export function dialogWithComponent(ContentComponent, dialogProps = {}) {
return new Promise((resolve) => {
/** @name 挂载容器 */
const container = document.createElement("div");
document.body.appendChild(container);
/** @name dialog组件实例 */
let vm = null;
/** @name dialog组件loading */
let loading = ref(false);
const dialogRef = ref(null);
const contentRef = ref(null);

const unmount = () => {
if (vm) {
vm.unmount();
vm = null;
}
document.body.removeChild(container);
};
const confirm = async () => {
let result = {};
const instance = contentRef.value;
if (instance && instance.getValue) {
loading.value = true;
try {
result = await instance.getValue();
} catch (error) {
typeof error === "string" && ElMessage.error(error);
loading.value = false;
return;
}
loading.value = false;
}
unmount();
resolve(result);
};

// 创建dialog组件实例
vm = createApp({
render() {
return h(
BaseDialog,
{
ref: dialogRef,
modelValue: true,
loading: loading.value,
onDialogConfirm() {
confirm();
},
onDialogCancel() {
unmount();
},
...dialogProps,
},
{
default: () => createVNode(h, ContentComponent, contentRef),
},
);
},
});

// 挂载dialog组件
vm.mount(container);
});
}

/**
* @name 创建一个 VNode 实例
* @param {import('vue').CreateElement} h Vue 的 createElement 函数
* @param {import('vue').Component|Function} Component 渲染的组件或渲染函数
* @param {string} key VNode 的 key
* @param {import('vue').Ref} ref 组件引用
* @returns {import('vue').VNode|null} 返回 VNode 实例或 null
*/

export function createVNode(h, Component, ref = null) {
if (!Component) return null;
/** @type { import('vue').VNode } */
let instance = null;
/** @name 升级h函数,统一混入ref **/
const render = (type, props = {}, children) => {
return h(
type,
{
...props,
ref: (el) => {
if (ref) ref.value = el;
},
},
children,
);
};
if (typeof Component === "function") {
instance = Component(render);
} else {
instance = render(Component);
}
return instance;
}

dialogWithComponent 这个函数是整个架构的核心!它的职责就像一个专业的 Dialog "召唤师":



脑袋突然蹦出一句话:"去吧,就决定是你了,皮卡丘(柱状图)"🎯




  • 动态创建:不再需要在模板里预先写好 <el-dialog>dialogWithComponent 会在你需要的时候,通过 createApph 函数,动态地创建一个包含 <el-dialog> 和你的内容组件的 Vue 应用实例。

  • 挂载与销毁:它负责将创建的 Dialog 实例挂载到 document.body 上,并在 Dialog 关闭(确认、取消或点击遮罩层)后,优雅地将其从 DOM 中移除并销毁 Vue 实例,避免内存泄漏。

  • Promise 驱动:调用 dialogWithComponent 会返回一个 Promise。当用户点击"确认"并成功获取数据后,Promise 会调用 resolve 并返回数据;如果用户点击"取消"或"关闭",Promise 会调用 reject 。这使得异步处理 Dialog 结果变得异常简洁,并且支持异步。

  • 配置注入:你可以轻松地向 dialogWithComponent 传递 <el-dialog> 的各种 props,实现 Dialog 的定制化。


createVNode 这个函数是 Vue 中 h 函数的升级版本,它主要是帮忙做内容的渲染🎩,它有两个小小的特点:



  • 组件/函数通吃:你可以直接传递一个 Vue 组件 ( .vue 文件或 JS/TS 对象) 给它,它会用 h 函数渲染这个组件。你还可以传递一个渲染函数!能让你在运行时动态决定渲染什么内容,简直不要太方便!是吧是吧。🤩

  • Ref 传递:它巧妙地集中处理了 ref ,使得 dialogWithComponent 函数可以获取到内容组件的实例 (contentRef.value),从而能够调用内容组件暴露的方法(getValue),非常关键的一点。⏰


基础的 Dialog 组件文件(components/BaseDialog.vue):


<template>
<el-dialog v-bind="dialogAttrs">
<slot></slot>
<template v-if="showFooter" #footer>
<span>
<template v-if="!$slots.footer">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleConfirm">确定</el-button>
</template>
<slot v-else name="footer"></slot>
</span>
</template>
</el-dialog>

</template>

<script setup>
import { useAttrs, computed } from "vue";
import { ElDialog, ElButton } from "element-plus";
defineProps({
showFooter: {
type: Boolean,
default: true,
},
loading: {
type: Boolean,
default: false,
}
});
const emit = defineEmits(["dialogCancel", "dialogConfirm"]);
const attrs = useAttrs();
const dialogAttrs = computed(() => ({
...attrs,
}));
function handleCancel() {
emit("dialogCancel");
}
function handleConfirm() {
emit("dialogConfirm");
}
</script>


那么,整个核心代码的实现过程大概就是如此了。不知道你看完这部分的拆解,是否有了新的收获呢?😋


当然啦,在实际的业务场景中,代码的组织和细节处理会更加复杂,比如会涉及到更精细的状态管理、错误处理、权限控制、以及各种边界情况的兼容等等。这里为了突出咱们动态创建 Dialog 架构的核心思想,小编仅仅是把最关键的脉络拎了出来,并进行了一定程度的精简。


总结


总而言之,言而总之,这次架构的演进,给小编最大的感受就是🏗️从"各自为战"到"统一调度"。


告别了维护繁琐、数量庞大的单个 Dialog.vue 文件,转而拥抱了基于 createApph 函数的动态创建方式。


这种新模式下,基础 Dialog、配置面板 ( Panel.vue )、以及调用逻辑各司其职,实现了真正的高内聚、低耦合。最终使得整个项目结构更加清晰、代码更加健壮,也极大地提升了后续的可维护性。希望这套方案能给你带来一些启发!


最后,如果你有任何疑问或者更好的想法,随时欢迎交流哦!👇









至此,本篇文章就写完啦,撒花撒花。


image.png


作者:橙某人
来源:juejin.cn/post/7498737799204093978
收起阅读 »

用canvas实现一个头像上的间断式的能量条

web
今天遇到一个很有意思的面试题,面试官给我一道题目,要我实现之前它们公司之前写的一个组件。 首先我介绍下这道题,首先我是先想到用flex布局来写头像分布,因为grid布局不能实现头像最后一排不能居中的效果。 然后这道题的重点来了,我一开始以为它头像上的边框是死...
继续阅读 »

今天遇到一个很有意思的面试题,面试官给我一道题目,要我实现之前它们公司之前写的一个组件。


e416716a-0b87-4411-ba39-7ec328968391.webp
首先我介绍下这道题,首先我是先想到用flex布局来写头像分布,因为grid布局不能实现头像最后一排不能居中的效果。


然后这道题的重点来了,我一开始以为它头像上的边框是死的,是张贴图。然后我去问面试官,他说是一个能量条,能根据投票的数量进行改变。我脑袋有点懵,问ai也没结果,生成的非常垃圾,然后就开始思考怎么才能实现。首先想到的是echats,但没有找到合适的,我就开始想echats是用canvas写的,我就想用canvas写下,在bilibili上看了下canvas的使用方法,于是就想到了这道题的解法。这是我的成果。


image.png
我就不做过多的讲解关于canvas的使用方法,我只在我的演示代码注释中讲每条代码的作用,和使用方法。不会的话,可以去看看bilibili,然后做个笔记,然后就印象深刻了。


代码讲解


这里是初步实现的代码,写出了大概的轮廓方便理解。完整代码在最后面。


具体的代码讲解就写在注释中了。


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<canvas id="canvas" width="600" height="600" backgroud></canvas>
<script>
function animate() {
var canvas = document.getElementById('canvas');//找到canvas
var ctx = canvas.getContext('2d');//读取canvas的上下文,进行修改,就能实现对canvas的绘画

ctx.translate(canvas.width / 2, canvas.height / 2);//这个是将canvas的坐标轴移到中间
ctx.rotate(-Math.PI / 2);//这个是将坐标轴反向转动90度

ctx.strokeStyle = 'rgb(144, 211, 205)';//设置画笔的颜色
ctx.lineWidth = 20; // 这里是设置画笔的宽度,也就是能量条的宽度
ctx.lineCap = "butt"; //这里设置画笔结束的位置是圆的直的还是弯的

for (let i = 0; i < 17; i++) {//这里17表示要绘制17段线,到时候这里循环的次数会传过来在我后面的成品中。
ctx.beginPath();//这里开始绘制路径
// 绘制小段圆弧 (角度改为弧度制)
ctx.arc(0, 0, 100, -Math.PI / 34, Math.PI / 34, false);//前两个位置是圆心,第三个是半径,第四个是开始角度,第五个是结束角度,第六个是是否逆时针
ctx.stroke();//这个表填充绘画的轨迹
// 旋转到下一个位置
ctx.rotate(Math.PI / 16);//这里坐标轴顺时针移动一定角度,如果想要格子更多就设的更小,上面画线的角度也要调小
ctx.closePath()//结束绘制
}
}
animate();
</script>
</body>

</html>

image.png


成品代码


最后的成品我是用vue写的,没有特别去封装,毕竟只是面试题。


<template>
<div class="grid-container">
<div class="member-card" v-for="(member, index) in members" :key="index">
<canvas :id="'' + index" width="150" height="150"></canvas>
<div class="circle">
<img :src="member.avatar" alt="avatar" class="avatar" />
</div>
</div>
</div>

</template>

<script setup>
import { onMounted } from 'vue';


const members = [
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 10 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 2 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 18 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 20 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 31 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
{ name: '用户A', avatar: 'https://img0.baidu.com/it/u=600722015,3838115472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750', numbers: 1 },
];

onMounted(() => {
members.forEach((member, index) => {
drawEnergyBar(index, member.numbers); // 使用member.numbers作为参数
});
});

function drawEnergyBar(index, count) {
const canvas = document.getElementById(`canvas-${index}`);
const ctx = canvas.getContext('2d');

// 重置画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

// 绘制设置
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(-Math.PI / 2);

ctx.strokeStyle = 'rgb(144, 211, 205)';
ctx.lineWidth = 60;
ctx.lineCap = "butt";

// 根据传入的count值绘制线段
for (let i = 0; i < count; i++) {
ctx.beginPath();
ctx.arc(0, 0, 44, -Math.PI / 36, Math.PI / 36, false);
ctx.stroke();
ctx.rotate(Math.PI / 16);
}
}
</script>


<style scoped>
/* 修改canvas样式 */
canvas {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
/* 作为背景层 */
}

.member-card {
position: relative;
width: 150px;
height: 150px;
/* 添加固定高度 */
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.3s ease;
border: rgb(144, 211, 205) solid 2px;
border-radius: 50%;
background-color: black;
overflow: hidden;
}

.circle {
position: relative;
border: 2px solid black;
width: 100px;
height: 100px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 2;
/* 确保在画布上方 */
margin: 0;
/* 移除外边距 */
}

.grid-container {
height: 100%;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
padding: 30px;
max-width: calc(150px * 6 + 30px * 5);
margin: 0 auto;
background: url(https://pic.nximg.cn/file/20230303/33857552_140701783106_2.jpg);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}

.member-card {
position: relative;
width: 150px;
display: flex;
justify-content: center;
align-items: center;
transition: transform 0.3s ease;
border: rgb(144, 211, 205) solid 2px;
border-radius: 50%;
background-color: black;
}


.circle {
position: relative;
border: 2px solid black;
margin: 20px 20px;
width: 100px;
height: 100px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.avatar {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
</style>


结语


虽然这道题有点难,但好处是我对canvas的理解加深了,canvas绝对是前端的一个非常有用的东西,值得掘友们认真学习。原本这道题的灵感来源于bilibili上讲的canvas实现钟表中刻度的实现,虽然没用它的方法,因为他的方法会导致刻度变形,不是扇形的能量条,但是它旋转坐标轴的想法让我大受启发。


作者:睡觉zzz
来源:juejin.cn/post/7501568955498070016
收起阅读 »

别再用 useEffect 写满组件了!试试这个三层数据架构 🤔🤔🤔

web
面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎...
继续阅读 »

面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:yunmz777



我们常常低估了数据获取的复杂性,直到项目已经陷入困境。很多项目一开始只是简单地在组件中随手使用 useEffect()fetch()


然而不知不觉中,错误处理、加载状态、缓存逻辑和重复请求越堆越多,代码变得混乱难以维护,调试也越来越痛苦。


以下是我在许多项目中常见的一些问题:



  1. 组件触发了重复的网络请求 —— 只是因为没有正确缓存数据

  2. 组件频繁重渲染 —— 状态管理混乱,每秒更新几十次

  3. 过多的骨架屏加载效果 —— 导致整个应用看起来总是“卡在加载中”

  4. 用户看到旧数据 —— 修改数据后缓存没有及时更新

  5. 并发请求出现竞态条件 —— 不同请求返回顺序无法预测,导致数据错乱

  6. 内存泄漏 —— 订阅和事件监听未正确清理

  7. 乐观更新失败却悄无声息 —— 页面展示和实际数据不一致

  8. 服务端渲染数据失效太快 —— 页面跳转后立即变成过期数据

  9. 无效的轮询逻辑 —— 要么频繁请求没有变化的数据,要么根本没必要轮询

  10. 组件与数据请求逻辑强耦合 —— 导致组件复用性极差

  11. 顺序依赖的多层请求链 —— 比如:获取用户 → 用户所属组织 → 组织下的团队 → 团队成员


以上问题在复杂应用中极为常见。如果不从架构层面进行规划和优化,很容易陷入混乱的技术债中,影响项目长期维护与扩展。


这些问题会互相叠加。一个糟糕的数据获取模式,往往会引发三个新的问题。等你意识到时,原本“简单”的仪表盘页面已经需要从头重构了。


这篇文章将向你展示一种更好的做法,至少是我在项目中偏好的架构方式。我们将构建一个三层数据获取架构,它可以从最基本的 CRUD 操作无缝扩展到复杂的实时应用,而不会让你陷入混乱的思维模型中。


不过在介绍这套“三层数据架构”之前,我们得先谈谈一个常见的起点:
你的第一反应可能是直接在组件里用 useEffect() 搭配 fetch() 来获取数据,然后继续开发下去。


但这种方式,很快就会失控。以下是原因:


export function TeamDashboard() {
const [user, setUser] = useState(null);
const [org, setOrg] = useState(null);
const [teams, setTeams] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [isCreating, setIsCreating] = useState(false);
const [lastUpdated, setLastUpdated] = useState(null);

// Waterfall ❌
useEffect(() => {
const fetchData = async () => {
try {
// User request
const userData = await fetch("/api/user").then((res) => res.json());
setUser(userData);

// Wait for user, then fetch org
const orgData = await fetch(`/api/org/${userData.orgId}`).then((res) =>
res.json()
);
setOrg(orgData);

// Wait for org, then fetch teams
const teamsData = await fetch(`/api/teams?orgId=${orgData.id}`).then(
(res) => res.json()
);
setTeams(teamsData);
setIsLoading(false);
} catch (err) {
setError(err.message);
setIsLoading(false);
}
};

fetchData();
}, []);

// Handle window focus to refetch
useEffect(() => {
const handleFocus = async () => {
if (!user?.id) return;
setIsLoading(true);
await refetchData();
};

window.addEventListener("focus", handleFocus);
return () => window.removeEventListener("focus", handleFocus);
}, [user?.id]);

// Polling for updates
useEffect(() => {
if (!user?.id || !org?.id) return;

const pollTeams = async () => {
try {
const teamsData = await fetch(`/api/teams?orgId=${org.id}`).then(
(res) => res.json()
);
setTeams(teamsData);
} catch (err) {
// Silent fail or show error?
console.error("Polling failed:", err);
}
};

const interval = setInterval(pollTeams, 30000);
return () => clearInterval(interval);
}, [user?.id, org?.id]);

const refetchData = async () => {
try {
const userData = await fetch("/api/user").then((res) => res.json());
const orgData = await fetch(`/api/org/${userData.orgId}`).then((res) =>
res.json()
);
const teamsData = await fetch(`/api/teams?orgId=${orgData.id}`).then(
(res) => res.json()
);

setUser(userData);
setOrg(orgData);
setTeams(teamsData);
setLastUpdated(new Date());
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};

const createTeam = async (newTeam) => {
setIsCreating(true);
try {
const response = await fetch("/api/teams", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTeam),
});

if (!response.ok) throw new Error("Failed to create team");

const createdTeam = await response.json();

// Optimistic update attempt
setTeams((prev) => [...prev, createdTeam]);

// Or full refetch because you're paranoid
await refetchData();
} catch (err) {
setError(err.message);
// Need to rollback optimistic update?
// But which teams were the original ones?
} finally {
setIsCreating(false);
}
};

// Component unmount cleanup
useEffect(() => {
return () => {
// Cancel any pending requests?
// How do we track them all?
};
}, []);

// The render logic is still complex
if (isLoading && !teams.length) {
return <LoadingSpinner />;
}

if (error) {
return <ErrorDisplay message={error} onRetry={refetchData} />;
}

return (
<div>
<h1>{org?.name}'s Dashboard</h1>
{isLoading && <div>Refreshing...</div>}
<TeamList teams={teams} onCreate={createTeam} isCreating={isCreating} />
{lastUpdated && (
<div>Last updated: {lastUpdated.toLocaleTimeString()}</div>
)}
</div>

);
}

这种“在组件中用 useEffect + fetch”的方式,存在大量问题:



  • 瀑布式请求:请求按顺序依赖执行,效率低下(后面我们会详细讨论)

  • 状态管理混乱:多个 useState 钩子相互独立,容易不同步

  • 内存泄漏风险:事件监听器、定时器等需要手动清理,容易遗漏

  • 无法取消请求:组件卸载时,无法终止正在进行中的请求

  • 加载状态复杂:isLoading 究竟是哪个请求在加载?多个并发请求怎么处理?

  • 错误处理难统一:错误冒泡到哪里?如何集中处理错误?

  • 缓存数据过期问题:没有机制标记哪些数据已经过期

  • 乐观更新灾难:需要手动写回滚逻辑,容易出错

  • 依赖数组陷阱:一不小心漏了依赖,导致潜在 Bug 难以排查

  • 测试极其困难:模拟这些副作用和状态逻辑是一场噩梦


当你的应用变得越来越复杂,这些问题会指数级地增长。每新增一个功能,就意味着更多的状态、更多的副作用、更多边界条件需要考虑。


当然,你也可以用 Redux 或 MobX 来集中管理状态,但这些库往往也会引入新的复杂度和大量样板代码。最终你会陷入一张难以理清的“action → reducer → selector”的关系网中。我自己也喜欢这两个库,但它们并不是解决这个问题的最佳答案。


你可能会想:“那我用 useReducer() + useContext() 管理状态不就好了?”
是的,这种组合确实可以整洁地组织状态,但它仍然没有解决数据获取本身的复杂性。加载状态、错误处理、缓存失效等问题依旧存在。


顺带一提,你可能还会想:“我干脆一次性把所有数据都请求回来,不就没这些问题了?”


接下来我们就来聊聊,为什么这也不可行。


export default function Dashboard() {
// Creates a waterfall - each request waits for the previous ❌
const { user } = useUser(); // Request 1
const { org } = useOrganization(user?.id); // Request 2 (waits)
const { teams } = useTeams(org?.id); // Request 3 (waits more)

// Total delay: 600-1200ms
return <DashboardView user={user} org={org} teams={teams} />;
}

Server Components(服务器组件)是一种更快、更高效的解决方案。它们允许你在服务器端获取数据,然后一次性将处理后的结果发送给客户端,从而:



  • 减少前后端之间的网络请求次数

  • 降低客户端的计算负担

  • 提升页面加载速度和整体性能


通过在服务器上完成数据获取与渲染逻辑,Server Components 能帮助你构建更简洁、高性能的 React/Next.js 应用架构。


export default async function Dashboard() {
const user = await getUser();

// fetch org and teams in parallel using user data ✅
const [org, teams] = await Promise.all([
getOrganization(user.orgId),
getTeamsByOrgId(user.orgId),
]);

return <DashboardView user={user} org={org} teams={teams} />;
}

如果我告诉你,其实有一种更优雅的方式来组织数据获取逻辑,不仅能随着应用的增长而扩展,还能让你的组件保持简洁、专注 —— 你会不会感兴趣?


这正是 “三层数据架构(Three Layers of Data Architecture)” 的核心思想。这个模式将数据获取逻辑划分为三个清晰的层级,每一层都各司其职,互不干扰。


这样的设计让你的应用:



  • 更容易理解

  • 更方便测试

  • 更便于维护和扩展


接下来我们就来深入了解这三层到底是什么。


三层数据架构


解决方案就是构建一个三层架构,实现关注点分离,让你的应用更容易理解、维护和扩展。


这种架构理念受到 React Query 的启发,它为管理服务端状态提供了一套强大且高效的解决方案。


你不一定非得使用 React Query,但我个人非常推荐它作为数据获取与缓存的首选库。
它帮你处理掉大量样板代码,让你可以专注于业务逻辑和界面开发。



💡 小提示:如果你选择使用 React Query,别忘了在开发环境中加上




<ReactQueryDevtools /> —— 这个调试工具会极大提升你的开发体验。



20250513092135


回到“三层架构”本身。其实它的结构非常简单:



  1. 服务器组件(Server Components) —— 负责初始数据获取

  2. React Query —— 处理客户端的缓存与数据更新

  3. 乐观更新(Optimistic Updates) —— 提供即时的 UI 反馈


React Query 支持两种方式来实现乐观更新(即在真正完成数据变更之前就提前更新界面):



  • 使用 onMutate 钩子,直接操作缓存实现数据预更新

  • 或者通过 useMutation 的返回值,根据变量手动更新 UI


这种模式不仅让用户感受到更快的响应,还能保持数据与界面的同步性。


下面是一个推荐的项目结构示例,用来更清晰地理解这三层架构的组织方式:


app/
├── page.tsx # Layer 1: Server Component entry
├── api/
│ └── teams/
│ └── route.ts # GET, POST teams
│ └── [teamId]/
│ └── route.ts # GET, PUT, DELETE specific team
├── TeamList.tsx # Client component consuming Layers 2 & 3
├── components/ # Fix: Add this folder
│ └── TeamCard.tsx
└── ui/
├── error-state.tsx # Layer 2: Error handling states
└── loading-state.tsx # Layer 2: Loading states

hooks/
├── teams/
│ ├── useTeamsData.ts # Layer 2: React Query hooks
│ └── useTeamMutations.ts # Layer 3: Mutations with optimism

queries/ # Layer 1: Server-side database queries
├── teams/
│ ├── getAllTeams .ts
│ ├── getTeamById.ts
│ ├── getTeamsByOrgId.ts
│ ├── deleteTeamById.ts
│ ├── createTeam.ts
│ ├── updateTeamById.ts

context/
└── OrganizationContext.tsx # Layer 2: Centralized data management

三层架构的数据如何流动?


这三个层按顺序工作但保持独立:


用户请求(User Request)

【第一层:服务器组件(Server Component)】
- 调用 getAllTeams() 从数据库获取数据
- 返回已渲染的 HTML(含初始数据)

【第二层:React Query(客户端状态管理)】
- 接收并“脱水”服务器返回的数据(hydrate)
- 管理客户端缓存
- 处理自动/手动重新请求(refetch)

【第三层:用户交互(User Actions)】
- 执行乐观更新,立即反馈 UI
- 发起真实的变更请求(mutation)
- 自动或手动触发缓存失效(cache invalidation)


第一层:Server Components


服务器组件负责处理初始数据获取,让你的应用感觉即时可用。但它们不会动态更新——这时 React Query 就派上用场了(第二层)。


import { getAllTeams } from "@/queries/teams/getAllTeams";
import { TeamList } from "./TeamList";
import { OrganizationProvider } from "@/context/OrganizationContext";

export default async function Page() {
// Layer 1: Fetch initial data on server
const teams = await getAllTeams();

return (
<main>
<h1>Teams Dashboard</h1>
{/* Pass server data to React Query via context */}
<OrganizationProvider initialTeams={teams}>
<TeamList />
</OrganizationProvider>
</main>

);
}

getAllTeams 函数是一个简单的数据库查询,用于获取所有团队。它可以是一个简单的 SQL 查询,也可以是一个 ORM 调用,具体取决于您的设置。


如下代码所示:


import { db } from "@/lib/db"; // Database or ORM connection
import { Team } from "@/types/team";
import { NextResponse } from "next/server";

export async function getAllTeams(): Promise<Team[]> {
try {
const teams = await db.team.findMany();
return teams;
} catch (error) {
throw new Error("Failed to fetch teams");
}
}

第二层:React Query


第 2 层使用来自第 1 层的初始数据并管理客户端状态:


import { useQuery } from "@tanstack/react-query";

export function useTeamsData(initialData: Team[]) {
return useQuery({
queryKey: ["teams"],
queryFn: async () => {
// Client-side must use API routes, not direct queries
// I want to keep my server and client code separate
const response = await fetch("/api/teams");
if (!response.ok) throw new Error("Failed to fetch teams");
return response.json();
},
initialData, // Received from Server Component via context
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
});
}

以下是客户端组件从第 2 层消费的方式。


"use client";

import { useOrganization } from "@/context/OrganizationContext";
import { LoadingState } from "@/ui/loading-state";
import { ErrorState } from "@/ui/error-state";

export function TeamList() {
// Data from Layer 2 context
const { teams, isLoadingTeams, error } = useOrganization();

if (error) {
return <ErrorState message="Failed to load teams" />;
}

if (isLoadingTeams) {
return <LoadingState />;
}

return (
<div>
{teams.map((team) => (
<TeamCard key={team.id} team={team} />
))}
</div>

);
}

真正的“魔法”发生在第三层,这一层让你在服务器还在处理请求时,就能立即更新 UI,带来极致流畅的用户体验 —— 这正是乐观更新(Optimistic Updates)的价值所在。


在这个层中,所有变更请求(mutations)都被集中管理,例如创建或删除团队。


我们通常会把这部分逻辑封装在一个独立的 Hook 中,比如 useTeamMutations,它内部使用 React Query 的 useMutation 来处理对应的操作,从而让业务逻辑更清晰、职责更明确、代码更易维护。


// Layer 3: Mutations with optimism
import { useMutation, useQueryClient } from "@tanstack/react-query";

export function useTeamMutations() {
const queryClient = useQueryClient();

const createTeamMutation = useMutation({
mutationFn: async (newTeam: { name: string; members: string[] }) => {
const response = await fetch("/api/teams", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTeam),
});
if (!response.ok) throw new Error("Failed to create team");
return response.json();
},
onMutate: async (newTeam) => {
await queryClient.cancelQueries({ queryKey: ["teams"] });
const currentTeams = queryClient.getQueryData(["teams"]);
queryClient.setQueryData(["teams"], (old) => [
...old,
{ ...newTeam, id: `temp-${Date.now()}` },
]);
return { currentTeams };
},
onError: (err, variables, context) => {
queryClient.setQueryData(["teams"], context.currentTeams);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["teams"] });
},
});

const deleteTeamMutation = useMutation({
mutationFn: async (teamId: string) => {
const response = await fetch(`/api/teams/${teamId}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Failed to delete team");
return response.json();
},
onMutate: async (teamId) => {
await queryClient.cancelQueries({ queryKey: ["teams"] });
const currentTeams = queryClient.getQueryData(["teams"]);
queryClient.setQueryData(["teams"], (old) =>
old.filter((team) => team.id !== teamId)
);
return { currentTeams };
},
onError: (err, teamId, context) => {
queryClient.setQueryData(["teams"], context.currentTeams);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["teams"] });
},
});

return {
createTeam: createTeamMutation.mutate,
deleteTeam: deleteTeamMutation.mutate,
isCreating: createTeamMutation.isLoading,
isDeleting: deleteTeamMutation.isLoading,
};
}

TeamCard 组件使用 useTeamMutations 钩子来处理团队的创建和删除。它还显示每个操作的加载状态。


"use client";
// TeamList.tsx - Using Layer 3 mutations
import { useTeamMutations } from "@/hooks/teams/useTeamMutations";

interface TeamCardProps {
team: {
id: string;
name: string;
members: string[];
};
}

export function TeamCard({ team }: TeamCardProps) {
const { deleteTeam, isDeleting } = useTeamMutations();

return (
<div className="p-4 border border-gray-200 rounded-lg mb-4">
<h3 className="text-lg font-semibold">{team.name}</h3>
<p className="text-gray-600">Members: {team.members.length}</p>
<button
onClick={() =>
deleteTeam(team.id)}
disabled={isDeleting}
className="mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50"
>
{isDeleting ? "Deleting..." : "Delete Team"}
</button>
</div>

);
}

将所有内容联系在一起:Context


上下文提供程序消除了 prop 钻取,并集中了数据访问。这对于多个组件需要相同数据的复杂应用尤其有用。


import { createContext, useContext } from "react";
import { useTeamsData } from "@/hooks/teams/useTeamsData";

interface OrganizationContextValue {
teams: Team[];
isLoadingTeams: boolean;
error: Error | null;
}

const OrganizationContext = createContext<OrganizationContextValue | null>(
null
);

export function OrganizationProvider({ children, initialTeams }) {
const { data: teams, isLoading, error } = useTeamsData(initialTeams);

return (
<OrganizationContext.Provider
value={{ teams, isLoadingTeams: isLoading, error }}
>

{children}
</OrganizationContext.Provider>

);
}

export function useOrganization() {
const context = useContext(OrganizationContext);
if (!context) {
throw new Error("useOrganization must be used within OrganizationProvider");
}
return context;
}

OrganizationProvider 组件包裹了 TeamList,为其提供了第一层的初始数据,同时统一管理加载状态与错误处理


在更复杂的应用中,你可以为不同的数据层增加更多的上下文(Context)提供器。
比如,你可能会有:



  • 一个 UserContext 来管理用户信息

  • 一个 AuthContext 来处理认证状态


通过这种方式,你的组件可以专注于渲染 UI,而数据获取与状态管理则被集中管理,逻辑更清晰、职责更分明。


需要注意的是:



对于简单应用而言,“三层数据架构”可能有点大材小用。
但对于中大型项目来说,它具有极高的可扩展性,能很好地应对不断增长的复杂度。



此外,它还让测试变得更加简单 —— 你可以通过模拟(mock)这些 Context Provider,来独立测试每个组件,无需依赖真实数据。


P.S.:这个架构不仅限于 React。你在 Vue.js、Svelte 或其他前端框架中也可以采用类似的思路。关键在于:关注点分离,让组件专注于“渲染”,而不是“获取数据”或“管理状态”。


总结


这篇文章介绍了一个适用于复杂 React/Next.js 应用的 三层数据架构,通过将数据获取流程拆分为 Server Components、React Query 和用户交互三层,解决了传统 useEffect + fetch 带来的各种性能与维护问题。该模式强调 关注点分离,提升了组件的复用性、可测试性和扩展能力。尽管对小项目可能偏重,但在中大型应用中具备良好的可扩展性和清晰的逻辑组织能力,是构建健壮前端架构的实用指南。


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

初级、中级、高级前端工程师,对于form表单实现的区别

web
在 React 项目中使用 Ant Design(Antd)的 Form 组件能快速构建标准化表单,特别适合中后台系统开发。以下是结合 Antd 的 最佳实践 和 分层实现方案: 一、基础用法:快速搭建标准表单 import { Form, Input, B...
继续阅读 »

在 React 项目中使用 Ant Design(Antd)的 Form 组件能快速构建标准化表单,特别适合中后台系统开发。以下是结合 Antd 的 最佳实践分层实现方案




一、基础用法:快速搭建标准表单


import { Form, Input, Button, Checkbox } from 'antd';

const BasicAntdForm = () => {
const [form] = Form.useForm();

const onFinish = (values: any) => {
console.log('提交数据:', values);
};

return (
<Form
form={form}
layout="vertical"
initialValues={{ remember: true }}
onFinish={onFinish}
>

{/* 邮箱字段 */}
<Form.Item
label="邮箱"
name="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]}
>

<Input placeholder="user@example.com" />
</Form.Item>

{/* 密码字段 */}
<Form.Item
label="密码"
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ min: 8, message: '至少8位字符' }
]}
>

<Input.Password />
</Form.Item>

{/* 记住我 */}
<Form.Item name="remember" valuePropName="checked">
<Checkbox>记住登录状态</Checkbox>
</Form.Item>

{/* 提交按钮 */}
<Form.Item>
<Button type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>

);
};

核心优势



  • 内置校验系统:通过 rules 属性快速定义验证规则

  • 布局控制layout="vertical" 自动处理标签对齐

  • 状态管理Form.useForm() 自动处理表单状态




二、中级进阶:复杂场景处理


1. 动态表单字段(如添加多个联系人)


import { Form, Button } from 'antd';

const DynamicForm = () => {
return (
<Form>
<Form.List name="contacts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...rest }) => (
<div key={key} style={{ display: 'flex' }}>
<Form.Item
{...rest}
name={[name, 'phone']}
rules={[{ required: true }]}
>

<Input placeholder="手机号" />
</Form.Item>
<Button onClick={() => remove(name)}>删除</Button>
</div>
))}
<Button onClick={() => add()}>添加联系人</Button>
</>
)}
</Form.List>

</Form>
);
};

2. 异步验证(如检查用户名是否重复)


<Form.Item
name="username"
rules={[
{ required: true },
{
validator: (_, value) =>
fetch(`/api/check?username=${value}`)
.then(res => res.ok ? Promise.resolve() : Promise.reject('用户名已存在'))
}
]}
>
<Input />
</Form.Item>

3. 条件渲染字段(如选择国家后显示省份)


const { watch } = useForm();
const country = watch('country');

<Form.Item name="province" hidden={!country}>
<Select options={provinceOptions} />
</Form.Item>




三、高级优化:性能与可维护性


1. 表单性能优化


// 使用 shouldUpdate 避免无效渲染
<Form.Item shouldUpdate={(prev, current) => prev.country !== current.country}>
{({ getFieldValue }) => (
getFieldValue('country') === 'CN' && <ProvinceSelect />
)}
</Form.Item>

2. 类型安全(TypeScript)


interface FormValues {
email: string;
password: string;
}

const [form] = Form.useForm<FormValues>();

3. 主题定制(通过 ConfigProvider)


import { ConfigProvider } from 'antd';

<ConfigProvider
theme={{
token: {
colorPrimary: '#1890ff',
borderRadius: 4,
},
components: {
Form: {
labelColor: '#333',
},
},
}}
>

<YourFormComponent />
</ConfigProvider>




四、企业级解决方案


1. 表单设计器集成


// 结合 XFlow 实现可视化表单设计
import { XFlow, FormBuilder } from '@antv/xflow';

const FormDesigner = () => (
<XFlow>
<FormBuilder
components={registeredComponents} // 注册的Antd组件
onSave={(schema) =>
saveToBackend(schema)}
/>
</XFlow>

);

2. 微前端表单共享


// 使用 qiankun 共享表单组件
export default function AntdFormModule() {
return (
<Module name="form-module">
<ConfigProvider>
<Router>
<Route path="/form" component={YourAntdForm} />
</Router>
</ConfigProvider>
</Module>

);
}



五、Ant Design Form 的局限与应对策略


场景问题解决方案
大数据量表单渲染性能下降虚拟滚动(react-virtualized)
复杂联动逻辑代码复杂度高使用 Form.Provider 共享状态
深度定制UI样式覆盖困难使用 CSS-in-JS 覆盖样式
多步骤表单状态保持困难结合 Zustand 做全局状态管理
跨平台需求移动端适配不足配合 antd-mobile 使用



六、推荐技术栈组合


- **基础架构**:React 18 + TypeScript 5
- **UI 组件库**:Ant Design 5.x
- **状态管理**:Zustand(轻量)/ Redux Toolkit(复杂场景)
- **表单增强**:@ant-design/pro-form(ProComponents)
- **验证库**:yup/zod + @hookform/resolvers(可选)
- **测试工具**:Jest + Testing Library

通过 Ant Design Form 组件,开发者可以快速构建符合企业标准的中后台表单系统。关键在于:



  1. 合理使用内置功能(Form.List、shouldUpdate)

  2. 类型系统深度整合

  3. 性能优化意识

  4. 扩展能力设计(动态表单、可视化配置)


作者:前端开发张小七
来源:juejin.cn/post/7498950758475055119
收起阅读 »

TensorFlow.js 和 Brain.js 全面对比:哪款 JavaScript AI 库更适合你?

web
温馨提示 由于篇幅较长,为方便阅读,建议按需选择章节,也可收藏备用,分段消化更高效哦!希望本文能为你的前端 AI 开发之旅提供实用参考。 😊 引言:前端 AI 的崛起 在过去的十年里,人工智能(AI)技术的飞速发展已经深刻改变了各行各业。从智能助手到自动驾驶...
继续阅读 »

温馨提示


由于篇幅较长,为方便阅读,建议按需选择章节,也可收藏备用,分段消化更高效哦!希望本文能为你的前端 AI 开发之旅提供实用参考。 😊



引言:前端 AI 的崛起


在过去的十年里,人工智能(AI)技术的飞速发展已经深刻改变了各行各业。从智能助手到自动驾驶,从图像识别到自然语言处理,AI 的应用场景几乎无处不在。而对于前端开发者来说,AI 的魅力不仅在于其强大的功能,更在于它已经走进了浏览器,让客户端也能够轻松承担起机器学习的任务。


试想一下,当你开发一个 Web 应用,需要进行图像识别、文本分析、语音识别或其他 AI 任务时,你是否希望直接在浏览器中处理这些数据,而无需依赖远程服务器?如果能在用户的设备上本地运行这些任务,不仅可以大幅提升响应速度,还能减少服务器资源的消耗,为用户提供更流畅的体验。


这正是 TensorFlow.jsBrain.js 两款库所带来的变革。它们使开发者能够在浏览器中轻松实现机器学习任务,甚至支持训练和推理深度学习模型。虽然这两款库在某些功能上有相似之处,但它们的定位和特点却各有侧重。


TensorFlow.js 是由 Google 推出的深度学习框架,它为浏览器端的机器学习提供了强大的支持,能够处理从图像识别到自然语言处理的复杂任务。基于 WebGL 提供加速,TensorFlow.js 可以充分利用硬件性能,实现大规模数据处理和复杂模型推理。


TensorFlow.js 不仅功能强大,还能直接在浏览器中运行复杂的机器学习任务,例如图像识别和处理。如果你想深入了解如何使用 TensorFlow.js 构建智能图像处理应用,可以参考我的另一篇文章:纯前端用 TensorFlow.js 实现智能图像处理应用(一)


相比之下,Brain.js 是一款轻量级神经网络库,专注于简单易用的神经网络模型。它的设计目标是降低机器学习的入门门槛,适合快速原型开发和小型应用场景。尽管 Brain.js 不具备 TensorFlow.js 那样强大的深度学习能力,但它的简洁性和易用性使其成为许多开发者快速实验和实现基础 AI 功能的优选工具。


然而,选择哪款库作为前端 AI 的工具并不简单,这取决于项目的需求、性能要求以及学习成本等多个因素。本文将详细对比两款库的功能、优缺点及适用场景,帮助你根据需求选择最适合的工具。


无论你是 AI 初学者还是有经验的开发者,相信你都能从这篇文章中找到有价值的指导,助力你在浏览器端实现机器学习。准备好了吗?让我们一起探索 TensorFlow.jsBrain.js 的世界,发现它们的不同之处,了解哪一个更适合你的项目。




一、TensorFlow.js - 强大而复杂的深度学习库


TensorFlow


1.1 TensorFlow.js 概述


TensorFlow.js 是由 Google 推出的开源 JavaScript 库,用于在浏览器和 Node.js 环境中执行机器学习任务,包括深度学习模型的推理和训练。它是 TensorFlow 生态的一部分,TensorFlow 是全球最受欢迎的深度学习框架之一,广泛应用于计算机视觉、自然语言处理等领域。


TensorFlow.js 的核心亮点在于其 跨平台支持。你可以在浏览器端运行,也可以在 Node.js 环境下执行,灵活满足不同开发需求。此外,它支持导入已训练好的 TensorFlowKeras 模型,在浏览器或 Node.js 中进行推理,无需重新训练。这使得 AI 的开发更加高效和便捷。


1.2 TensorFlow.js 的功能特点


TensorFlow.js 提供了丰富的功能,覆盖从简单的机器学习到复杂的深度学习任务。以下是它的核心特点:



  1. 浏览器端深度学习推理:通过 WebGL 加速,TensorFlow.js 可以高效地在浏览器中加载和运行深度学习模型,无需依赖服务器,大幅提升用户体验和响应速度。

  2. 训练与推理一体化TensorFlow.js 支持在前端环境直接训练神经网络,这对于动态数据更新和快速迭代非常有用。即使是复杂的深度学习模型,也能通过优化技术确保高效的训练过程。

  3. 支持复杂神经网络架构:包括卷积神经网络(CNN)、循环神经网络(RNN)、以及高级模型如 Transformer,适用于图像、语音、文本等多领域任务。

  4. 模型导入与转换:支持从其他 TensorFlowKeras 环境导入已训练的模型,并在浏览器或 Node.js 中高效运行,降低了开发门槛。

  5. 跨平台支持:无论是前端浏览器还是后端 Node.jsTensorFlow.js 都可以灵活适配,特别适合需要多环境协作的项目。


1.3 TensorFlow.js 的优势与应用场景


优势:


  1. 本地化计算:无需数据传输到服务器,所有计算均在用户设备上完成,提升速度并保障隐私。

  2. 强大的生态支持:依托 TensorFlow 的生态系统,TensorFlow.js 可以轻松访问预训练模型、教程和工具。

  3. 灵活性与高性能:支持低级别 APIWebGL 加速,可根据需求灵活调整模型和计算流程。

  4. 无需后台服务器:在浏览器中即可完成复杂的训练和推理任务,显著简化系统架构。


应用场景:


  1. 图像识别:例如手写数字识别、人脸检测、物体分类等实时图像处理任务。

  2. 自然语言处理:支持情感分析、文本分类、语言翻译等复杂 NLP 任务。

  3. 实时数据分析:适用于 IoT 或其他需要即时数据处理和反馈的应用场景。

  4. 推荐系统:通过用户行为数据构建个性化推荐,例如电商、新闻或社交媒体应用。


1.4 TensorFlow.js 基本用法示例


以下是一个简单示例,展示如何使用 TensorFlow.js 构建并训练神经网络模型。


安装与引入 TensorFlow.js


  1. 通过 CDN 引入:


    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>


  2. 通过 npm 安装(适用于 Node.js 环境):


    npm install @tensorflow/tfjs



创建简单神经网络

以下示例创建了一个简单的前馈神经网络,用于处理二分类问题:


// 导入 TensorFlow.js
const tf = require('@tensorflow/tfjs');

// 创建一个神经网络模型
const model = tf.sequential();

// 添加隐藏层(10 个神经元)
model.add(tf.layers.dense({ units: 10, activation: 'relu', inputShape: [5] }));

// 添加输出层(2 类分类问题)
model.add(tf.layers.dense({ units: 2, activation: 'softmax' }));

// 编译模型
model.compile({
 optimizer: 'adam',
 loss: 'categoricalCrossentropy',
 metrics: ['accuracy'],
});

训练和推理过程

训练模型需要提供输入数据(特征)和标签(目标值):


// 创建训练数据
const trainData = tf.tensor2d([[0, 1, 2, 3, 4], [1, 2, 3, 4, 5], [2, 3, 4, 5, 6]]);
const trainLabels = tf.tensor2d([[1, 0], [0, 1], [1, 0]]);

// 训练模型
model.fit(trainData, trainLabels, { epochs: 10 }).then(() => {
 // 使用新数据进行推理
 const input = tf.tensor2d([[1, 2, 3, 4, 5]]);
 model.predict(input).print();
});



二、Brain.js - 轻量级且易于使用的神经网络库


Brain


2.1 Brain.js 概述


Brain.js 是一个轻量级的开源 JavaScript 神经网络库,专为开发者提供快速、简单的机器学习工具。它的设计理念是易用性和轻量化,适合那些希望快速构建和训练神经网络的开发者,尤其是机器学习的新手。


与功能丰富的 TensorFlow.js 不同,Brain.js 更注重于直观和简单,能够帮助开发者快速完成从构建到推理的基本机器学习任务。虽然它不支持复杂的深度学习模型,但其易用性和小巧的特性,使其成为小型项目和快速原型开发的理想选择。


2.2 Brain.js 的功能特点


Brain.js 的功能主要集中在简化神经网络的构建与训练上,以下是其核心特点:



  1. 简单易用的 APIBrain.js 提供了直观的接口,开发者无需复杂的机器学习知识,也能轻松上手并实现神经网络任务。

  2. 轻量级:相较于体积较大的 TensorFlow.jsBrain.js 的核心库更为小巧,非常适合嵌入前端应用,且不会显著影响加载速度。

  3. 支持多种网络结构:前馈神经网络(Feedforward Neural Network)、LSTM 网络(Long Short-Term Memory)等。这些模型已足够应对大多数基础的机器学习需求。

  4. 快速训练与推理:通过几行代码即可完成训练与推理任务,适用于快速原型设计和验证。

  5. 同步与异步训练支持Brain.js 同时支持同步和异步的训练过程,开发者可以根据项目需求选择合适的方式。


2.3 Brain.js 的优势与应用场景


优势:



  1. 快速原型开发:开发者可以用最少的代码完成神经网络的构建和训练,特别适合需要快速验证想法的场景。

  2. 轻量级与高效率:库的体积较小,能快速加载,适合资源有限的环境。

  3. 易于集成Brain.js 非常适合嵌入 Web 应用或小型 Node.js 服务,集成简单。

  4. 适合初学者Brain.js 的设计对机器学习新手友好,无需深入了解复杂的深度学习算法即可上手。


应用场景:


  1. 基础分类与预测任务:适合实现简单的分类任务或数值预测,例如时间序列预测、情感分析等。

  2. 教学与实验:对于机器学习教学或学习过程中的快速实验,Brain.js 是一个很好的工具。

  3. 轻量化应用:例如小型交互式 Web 应用中实时处理用户输入。


2.4 Brain.js 基本用法示例


以下示例展示了如何使用 Brain.js 构建并训练一个简单的神经网络模型。


安装与引入


  1. 通过 npm 安装


    npm install brain.js


  2. 通过 CDN 引入


    <script src="https://cdn.jsdelivr.net/npm/brain.js"></script>



创建简单神经网络

以下代码创建了一个用于解决 XOR 问题的前馈神经网络:


// 引入 Brain.js
const brain = require('brain.js');

// 创建一个简单的神经网络实例
const net = new brain.NeuralNetwork();

// 提供训练数据
const trainingData = [
{ input: [0, 0], output: [0] },
{ input: [0, 1], output: [1] },
{ input: [1, 0], output: [1] },
{ input: [1, 1], output: [0] }
];

// 训练网络
net.train(trainingData);

// 测试推理
const output = net.run([1, 0]);
console.log(`预测结果: ${output}`); // 输出接近 1 的值

训练与推理参数调整

Brain.js 提供了一些可选参数,用于优化训练过程,例如:



  • 迭代次数(iterations :设置训练的最大轮数。

  • 学习率(learningRate :控制每次更新的步长。


以下示例展示了如何自定义训练参数:


net.train(trainingData, {
iterations: 1000, // 最大训练轮数
learningRate: 0.01, // 学习率
log: true, // 显示训练过程
logPeriod: 100 // 每 100 次迭代打印一次日志
});

// 推理新数据
const testInput = [0, 1];
const testOutput = net.run(testInput);
console.log(`输入: ${testInput}, 预测结果: ${testOutput}`);



三、TensorFlow.jsBrain.js 的全面对比


在这一章中,我们将从多个维度对 TensorFlow.jsBrain.js 进行详细对比,帮助开发者根据自己的需求选择合适的工具。对比内容涵盖技术实现差异、学习曲线、适用场景、性能表现以及生态系统和社区支持。


3.1 技术实现差异


TensorFlow.jsBrain.js 的技术实现差异显著,主要体现在功能复杂度、支持的模型类型和底层架构上:



  • TensorFlow.js 是一个功能全面的深度学习框架,基于 TensorFlow 的设计思想,提供了复杂的神经网络架构和高效的数学计算支持。它支持卷积神经网络(CNN)、循环神经网络(RNN)、生成对抗网络(GAN)等多种模型类型,能够完成从图像识别到自然语言处理的复杂任务。借助 WebGL 技术,TensorFlow.js 可在浏览器中高效进行高性能计算,尤其适合大规模数据和复杂模型。

  • Brain.js 则更加轻量,主要面向快速开发和简单任务。它支持前馈神经网络(Feedforward Neural Network)、长短期记忆网络(LSTM)等基础模型,适合处理简单的分类或预测问题。尽管功能不如 TensorFlow.js 广泛,但其简洁的设计使开发者能够快速上手,完成实验和小型项目。


总结TensorFlow.js 更加强大,适用于复杂任务;Brain.js 简单轻便,适合快速开发和小型应用。


3.2 学习曲线与开发者体验


在学习曲线和开发体验方面,两者差异明显:



  • TensorFlow.js 学习曲线较为陡峭。其功能强大且覆盖面广,但开发者需要了解深度学习的基础知识,包括模型训练、数据预处理等环节。尽管文档和教程丰富,但对初学者而言,掌握这些内容可能需要投入更多的时间和精力。

  • Brain.js 则以简洁直观的 API 著称,初学者可以通过几行代码实现神经网络的搭建与训练。它对复杂概念的抽象程度高,无需深入理解深度学习理论,便能快速完成任务。


总结:如果你是新手或需要快速实现一个简单模型,选择 Brain.js 更友好;而如果你已有一定经验,并计划处理复杂任务,则 TensorFlow.js 更适合。


3.3 适用场景与功能选择


根据应用场景,选择合适的库可以大大提高开发效率:



  • TensorFlow.js:适用于复杂任务,如图像识别、自然语言处理、视频分析或推荐系统。由于其强大的深度学习功能和高性能计算能力,TensorFlow.js 特别适合大规模数据处理和精度要求高的场景。

  • Brain.js:适合轻量级任务,例如简单的分类、回归、时间序列预测等。对于快速验证模型或开发原型,Brain.js 提供了简单高效的解决方案,尤其是在浏览器端运行时无需依赖复杂的服务器计算。


总结TensorFlow.js 面向复杂场景和大规模任务;Brain.js 更适合轻量化需求和快速开发。


3.4 性能对比


在性能方面,TensorFlow.jsBrain.js 存在显著差异:



  • TensorFlow.js 借助 WebGL 实现高效的硬件加速,支持 GPU 并行计算。在处理大规模数据集和复杂模型时,其性能优势显著,适用于高负载、高计算量的场景。

  • Brain.js 性能较为有限,主要针对小型数据集和简单任务。由于其轻量级设计,虽然在小规模任务中表现出色,但无法与 TensorFlow.js 的硬件加速能力相媲美。


总结:对于需要高性能计算的场景,TensorFlow.js 是更优选择;而对于小型任务,Brain.js 的性能已足够。


3.5 生态系统与社区支持



  • TensorFlow.js:作为 TensorFlow 生态的一部分,TensorFlow.js 享有丰富的社区资源和支持,包括大量的开源项目、教程、论坛和工具。开发者可以从官方文档和预训练模型中快速找到所需资源,支持复杂应用的开发。

  • Brain.js:社区较小,但活跃度高。文档简洁,适合初学者。虽然资源和支持不如 TensorFlow.js 丰富,但足以满足小型项目的需求。


总结TensorFlow.js 的生态更强大,适合需要长期维护和扩展的项目;Brain.js 更适合轻量化开发和快速上手。




四、如何选择最适合你的库?


TensorFlow.jsBrain.js 之间做出选择时,开发者需要综合考虑项目需求、技术背景和性能要求。这两款库各有特色:TensorFlow.js 功能强大,适用于复杂任务;Brain.js 简单易用,适合快速开发。以下从选择标准和实际场景出发,帮助开发者找到最合适的工具。


4.1 选择标准


在选择 TensorFlow.jsBrain.js 时,可参考以下几个关键标准:



  1. 功能需求



    • 复杂任务:如果项目涉及深度学习任务(如大规模图像分类、语音识别或自然语言处理),选择 TensorFlow.js 更为合适。它支持复杂的神经网络模型,具备高效的数据处理能力。

    • 基础任务:如果需求相对简单,例如小型神经网络模型、时间序列预测或分类任务,Brain.js 是更轻量的选择。



  2. 开发者经验



    • 有机器学习背景TensorFlow.js 提供高度灵活的 API,但学习曲线较陡。熟悉机器学习的开发者可以充分利用其强大功能。

    • 初学者Brain.js 更适合新手,提供简洁的接口和直观的使用体验。



  3. 性能需求



    • 高性能计算:如果项目需要硬件加速(如 GPU 支持)以处理大规模数据,TensorFlow.jsWebGL 支持是理想选择。

    • 轻量化应用:对于性能要求较低的场景,Brain.js 的轻量级设计足够满足需求。



  4. 项目规模与复杂度



    • 大型项目TensorFlow.js 提供复杂功能和强大的扩展性,适合长期维护和生产级应用。

    • 快速开发Brain.js 专注于快速实现小型项目,适合验证想法或开发 MVP(最小可行产品)。






4.2 基于项目需求的选择建议


以下是根据常见场景的具体选择建议:


场景一:图像分类应用



  • 需求:对大规模图像进行分类或识别,涉及复杂的卷积神经网络(CNN)。

  • 推荐选择TensorFlow.js。支持复杂模型架构,通过 WebGL 提供高效的硬件加速,适合处理大量图像数据。


场景二:实时数据分析与预测



  • 需求:对传感器数据进行实时监测和分析,预测未来趋势(如气象预测、股票走势)。

  • 推荐选择Brain.js。其轻量化和快速实现的特性非常适合实时数据处理和快速部署。


场景三:自然语言处理(NLP)应用



  • 需求:需要对文本数据进行分类、情感分析或对话生成。

  • 推荐选择TensorFlow.js。支持循环神经网络(RNN)、Transformer 等复杂模型,能处理 NLP 任务的高维数据和复杂结构。


场景四:个性化推荐系统



  • 需求:根据用户行为推荐商品或内容。

  • 推荐选择



    • 如果推荐系统复杂,涉及神经协同过滤或深度学习模型,选择 TensorFlow.js

    • 如果系统较为简单,仅需基于用户行为的规则实现,Brain.js 是更高效的选择。




场景五:快速原型开发与实验



  • 需求:验证机器学习模型效果或快速开发实验性产品。

  • 推荐选择Brain.js。它提供简洁的接口和快速训练功能,适合快速搭建和迭代。




结论:最终选择


通过对 TensorFlow.jsBrain.js 的详细对比,可以帮助开发者根据项目需求和个人技能做出最佳选择。以下是两者的优缺点总结及适用场景的建议。


TensorFlow.js 优缺点


优点:



  1. 功能全面:支持复杂的深度学习模型(如 CNNRNNGAN),适用于广泛的机器学习任务,包括图像识别、自然语言处理和语音处理等。

  2. 跨平台支持:可运行于浏览器和 Node.js 环境,灵活部署于多种平台。

  3. 性能卓越:利用 WebGL 实现硬件加速,适合高性能需求,尤其是大规模数据处理。

  4. 强大的生态系统:依托 TensorFlow 生态,拥有丰富的预训练模型、教程和社区支持,为开发者提供充足资源。


缺点:



  1. 学习门槛较高:功能复杂,适合有机器学习基础的开发者,初学者可能需要投入较多时间学习。

  2. 库体积较大:功能的多样性导致库体积偏大,可能影响浏览器加载速度和资源消耗。




Brain.js 优缺点


优点:



  1. 轻量级与易用性:设计简单,API 直观,非常适合快速开发和机器学习初学者。

  2. 小巧体积:库文件体积小,适合嵌入前端应用,对网页加载影响小。

  3. 支持基础模型:支持前馈神经网络和 LSTM,能满足大多数基础机器学习任务。

  4. 快速上手:开发者无需深厚的机器学习知识,能够快速实现简单神经网络应用。


缺点:



  1. 功能较为局限:不支持复杂深度学习模型,难以满足高阶任务需求。

  2. 性能有限:轻量设计决定其在大规模数据处理中的性能不如 TensorFlow.js




适用场景与开发者建议


初学者或简单任务



  • 选择Brain.js

  • 理由:适合刚接触机器学习的开发者,或处理简单分类、时间序列预测等基础任务。其平缓的学习曲线和快速开发特性,帮助初学者快速上手。


经验丰富的开发者或复杂任务



  • 选择TensorFlow.js

  • 理由:适合处理复杂的深度学习任务,如大规模图像识别、自然语言处理或实时视频分析。提供灵活的 API 和强大的计算能力,满足高性能需求。


小型项目与快速开发



  • 选择Brain.js

  • 理由:适合快速构建原型和简单的神经网络任务,易于维护,开发效率高。


大规模应用与高性能需求



  • 选择TensorFlow.js

  • 理由:其强大的加速能力和复杂模型支持,使其成为生产级应用的理想选择,尤其适合需要 GPU 加速的大规模任务。




结语


通过本文的对比,读者可以清晰了解 TensorFlow.jsBrain.js 在功能、性能、学习曲线、适用场景等方面的显著差异。选择最适合的库时,需要综合考虑项目的复杂度、团队的技术背景以及性能需求。


如果你的项目需要处理复杂的深度学习任务,并且需要高性能计算与广泛的社区支持,TensorFlow.js 是不二之选。它功能强大、生态丰富,适合图像识别、自然语言处理等高需求场景。而如果你只是进行小型神经网络实验,或需要快速原型开发,Brain.js 提供了更简洁易用的解决方案,是初学者和小型项目开发者的理想选择。


无论选择哪个库,充分了解它们的优势与限制,将帮助你在项目开发中高效使用这些工具,成功实现你的前端 AI 开发目标。




附录:对比表格


以下对比表格总结了 TensorFlow.jsBrain.js 在关键维度上的差异,帮助读者快速决策:


特性TensorFlow.jsBrain.js
GitHub 星标数量18.6K14.5K
功能复杂度高,支持复杂的深度学习模型(CNN, RNN, GAN等)低,支持基础前馈神经网络和LSTM网络
学习曲线陡峭,适合有深度学习经验的开发者平缓,适合初学者和快速原型开发
使用场景复杂场景,如大规模数据处理、图像识别、语音处理等小型项目,如简单分类任务、时间序列预测
支持的模型类型多种类型(CNN, RNN, GAN等复杂模型)基础类型(前馈神经网络、LSTM等)
性能优化支持 WebGL 加速和 GPU 并行计算,适合高性能需求不支持硬件加速,适合小规模数据处理
开发平台浏览器和 Node.js 环境,跨平台支持主要用于浏览器,也支持 Node.js
社区支持与文档丰富的生态系统,拥有大量教程、示例和预训练模型资源社区较小但活跃,文档简单直观
易用性API 较复杂,适合有深度学习背景的开发者API 简洁,适合初学者和快速开发
适用开发者高阶开发者,有深度学习基础初学者及快速实现简单任务的开发者
体积与资源消耗库文件较大,可能影响加载速度体积小,对网页性能影响较小
训练与推理能力支持复杂模型的训练与推理,适合高需求场景适合简单任务的训练与推理
预训练模型支持支持从 TensorFlow Hub 加载预训练模型不支持广泛预训练模型,主要用于自定义训练

同系列文章推荐


如果你觉得本文对你有所帮助,不妨看看以下同系列文章,深入了解 AI 开发的更多可能性:



欢迎点击链接阅读,开启你的前端 AI 学习之旅,让开发更高效、更有趣! 🚀



我是 “一点一木


专注分享,因为分享能让更多人专注。


生命只有一次,人应这样度过:当回首往事时,不因虚度年华而悔恨,不因碌碌无为而羞愧。在有限的时间里,用善意与热情拥抱世界,不求回报,只为当回忆起曾经的点滴时,能无愧于心,温暖他人。



作者:一点一木
来源:juejin.cn/post/7459285932092211238
收起阅读 »

(紧急修复!)老板急call:pdf阅读器不能用了?

web
客户那边说,发现了我们项目中有一个高危漏洞,需要修复下。我过去一看,好像不是我们的代码,是三方依赖的pdf.js 突然爆了高危,心想,这怎么可能,这东西都发布好多年了,用的好好的也没说高危啊。老板说,这他不管,客户那边的高危项一定要给解决。好吧,那我先去看看这...
继续阅读 »

客户那边说,发现了我们项目中有一个高危漏洞,需要修复下。我过去一看,好像不是我们的代码,是三方依赖的pdf.js 突然爆了高危,心想,这怎么可能,这东西都发布好多年了,用的好好的也没说高危啊。老板说,这他不管,客户那边的高危项一定要给解决。好吧,那我先去看看这个高危是个什么。


一番检索,发现真是 pdf.js 的高危漏洞,而且是今年24年4月26日内部报的,编号是 CVE-2024-4367,并且在今年24年4月30日的 4.2.67 版本上已经修复并发布了。


背景


不管如何,先看这个高危项,它允许攻击者在打开恶意 PDF 文件时立即执行任意 JavaScript 代码。主要是利用了pdf.js 中的字体渲染技术上的特性,当识别到当前浏览器支持 new Function("")并且在加载 pdf 资源时配置了 isEvalSupported 为 true(该值默认为true),此时如果我们在 pdf 资源中输入一些内容,用来控制字体渲染的参数,那么就可以在加载pdf 资源时,执行自己想要的任意的 JavaScript 代码,实现对应用系统的攻击。




解决方案


常规方案有三种



  • 完全杜绝的话可以直接将依赖的 pdf.js-dist 版本升级到 4.2.67+

  • 在使用 pdf.js-dist 的上层代码中将加载 pdf 的参数 isEvalSupported 设置为false

  • 禁用使用 eval 和 Function 构造函数


一般如果对兼容性要求不高的话就可以选择第一种,4.2.67 版本的兼容性legacy版本最低能兼容到以下版本的浏览器


Browser/environmentSupported
Firefox ESR+Yes
Chrome 98+Yes
OperaYes
EdgeYes
Safari 16.4+Mostly
Node.js 18+Mostly

但是如果像一些运行比较久远的至少要兼容到5年以上的设备的话,比如说我司产品,要兼容到 ios10.3(也不知道现在除了我司测试,到底还有谁在用 ios10.3),这种情况下,方案1就完全不可行了,那么就可以考虑使用方案2。


方案3与方案2有相似之处,通过重写 eval 和 Function 来控制内部的 isEvalSupported 的值,也可以避免 pdf文件在被渲染时使用 Function 加载 pdf 内容。


// 重写eval和Function
window.eval = function() {
throw new Error("eval() is disabled");
};

window.Function = function() {
throw new Error("Function constructor is disabled");
};

// pdf.js 中的Function 检测
function isEvalSupported() {
try {
new Function("");
return true;
} catch (e) {
return false;
}
}

const IsEvalSupportedCached = {
get value() {
return shadow(this, "value", isEvalSupported());
}
};

上述的重写会影响全局的 eval 和 Function,若项目中不使用上述功能,可以考虑。若一些内部使用模块使用了以上两个功能,则不建议如此修改。


但是,我们的客户不认,只认依赖版本,我们的 pdf.js-dist 版本低于 4.2.67,这件事在他们的安全报告中,属于完全不能容忍的高危漏洞,一定要解决的,解释也没用,那现在咋办?总不能不用吧?也不能抛弃大部分的低版本客户吧?



那么这个时候,上述三种方案都不能解决问题了,就要考虑其他的方式了。


那么回归我们程序员的本质,只能 fork-modify-push-publish 了。因为只有内部产品使用,也不需要 publish 了,将本来作为第三方的依赖,转成项目内置模块来使用,这个时候想怎么改就能怎么改了。


模块内置后,客户找的安全检测机构也不知道还能不能检测出来,以防万一,还是得把 pdf.js 关于这条安全漏洞的修复给同步到我们的低版本上来。


修复内容


根据官方发布,这条漏洞主要在 pr[18015] 中修复了,那我们把这条 pr 中有关上述漏洞的部分迁移过来即可。不用把这条内容都迁,比如其中对于cmds的重写部分,我们只需要将和isEvalSupported 相关的部分迁移即可,毕竟此漏洞也是由 isEvalSupported 引起的。


主要修复内容:



  • 去除 font_loader.FontFaceObject 中的入参 isEvalSupported 及相关使用该参数的内容

  • 如果使用的版本中isEvalSupported 只用来做字体渲染,可以去除整个 pdf.js 中使用了 isEvalSupported 逻辑的相关内容


通过上述修复方式,客户那边应该也能安心了吧?检索不到低于4.2.67版本的 pdf.js 引用,也不会在解析渲染pdf 资源时,出现外部的 pdf文件对系统造成攻击


关于漏洞


总所周知,pdf.js 里不仅对pdf 文件进行了资源解析,也做了资源的渲染,其中就包含了很多字体字形的绘制,而该漏洞就来源于字体绘制时使用了 new Function(""),导致可以在 pdf 文件中写一些能够被解析的内容,在应用系统中使用 pdf.js 去解析 pdf 文件并在绘制时执行任意的 JavaScript 内容,造成对系统的攻击。


// pdf.js font_loader 字体绘制中能够执行任意js内容的部分
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
const jsBuf = [];
for (const current of cmds) {
const args = current.args !== undefined ? current.args.join(",") : "";
jsBuf.push("c.", current.cmd, "(", args, ");\n");
}
// eslint-disable-next-line no-new-func
console.log(jsBuf.join(""));
return (this.compiledGlyphs[character] = new Function(
"c",
"size",
jsBuf.join("")
));
}

具体的绘制可以在在 PDF.js 中执行任意 JavaScript 中查看,以下为在 pdf 文件中输入任意代码的示例,


通过首先关闭c.transform(...)函数,并利用结尾括号来触发 alert:


/FontMatrix [1 2 3 4 5 (0); alert('foobar')]

将上述内容输入到 pdf 文件中,然后在火狐浏览器(未更新最新版本的 pdf 预览插件版本)中加载该 pdf 文件时的效果如下:



也可以使用 旧版本的 pdf.js 开源的 viewer 打开该文件,有一样的效果。


附录:


可用于攻击的 pdf 文件地址:codeanlabs.com/wp-content/…


CVE-2024-4367 漏洞详细攻击介绍:codeanlabs.com/blog/resear…


CVE-2024-4367 漏洞详情及相关修改 pr:github.com/mozilla/pdf…


pdf.js 相关文档推荐


前端接入 pdfjs-dist 渲染 pdf 文件踩坑


PDF.js 与 WebComponent:打造轻量级 PDF 预览器


作者:九酒
来源:juejin.cn/post/7408168213362507827
收起阅读 »

纯前端也能实现 OCR?

web
前言 前端时间有一个 OCR 的需求,原本考虑调用现成的 OCR 接口,但由于只是做一个我个人使用的工具,花钱购买 OCR 接口显得有些奢侈。于是就想着找找是否有现成的库可以自己部署或直接使用,结果发现了一个可以在纯前端实现 OCR 的库——Tesseract...
继续阅读 »

前言


前端时间有一个 OCR 的需求,原本考虑调用现成的 OCR 接口,但由于只是做一个我个人使用的工具,花钱购买 OCR 接口显得有些奢侈。于是就想着找找是否有现成的库可以自己部署或直接使用,结果发现了一个可以在纯前端实现 OCR 的库——Tesseract.js


Tesseract.js


Tesseract.js 是一个基于 Google Tesseract OCR 引擎的 JavaScript 库,利用 WebAssembly 技术将的 OCR 引擎带到了浏览器中。它完全运行在客户端,无需依赖服务器,适合处理中小型图片的文字识别。


主要特点



  • 多语言支持:支持多种语言文字识别,包括中文、英文、日文等。

  • 跨平台:支持浏览器和 Node.js 环境,灵活应用于不同场景。

  • 开箱即用:无需额外依赖后端服务,直接在前端实现 OCR 功能。

  • 自定义训练数据:支持加载自定义训练数据,提升特定场景下的识别准确率。


安装


通过 npm 安装


npm install tesseract.js

通过 CDN 引入


<script src="https://unpkg.com/tesseract.js@latest/dist/tesseract.min.js"></script>

基本使用


以下示例展示了如何使用 Tesseract.js 从图片中提取文字:


import Tesseract from 'tesseract.js';

Tesseract.recognize(
'image.png', // 图片路径
'chi_sim', // 识别语言(简体中文)
{
logger: info => console.log(info), // 实时输出进度日志
}
).then(({ data: { text } }) => {
console.log('识别结果:', text);
});

示例图片



运行结果



可以看到,虽然识别结果不完全准确,但整体准确率较高,能够满足大部分需求。


更多用法


1. 多语言识别


Tesseract.js 支持多语言识别,可以通过字符串或数组指定语言代码:


// 通过字符串的方式指定多语言
Tesseract.recognize('image.png', 'eng+chi_sim').then(({ data: { text } }) => {
console.log('识别结果:', text);
});

// 通过数组的方式指定多语言
Tesseract.recognize('image.png', ['eng','chi_sim']).then(({ data: { text } }) => {
console.log('识别结果:', text);
});

eng+chi_sim 表示同时识别英文和简体中文。Tesseract.js 内部会将字符串通过 split 方法分割成数组:


const currentLangs = typeof langs === 'string' ? langs.split('+') : langs;

2. 处理进度日志


可以通过 logger 回调函数查看任务进度:


Tesseract.recognize('image.png', 'eng', {
logger: info => console.log(info.status, info.progress),
});

输出示例:



3. 自定义训练数据


如果需要识别特殊字符,可以加载自定义训练数据:


const worker = await createWorker('语言文件名', OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
gzip: false, // 是否对来自远程的训练数据进行 gzip 压缩
langPath: '/path/to/lang-data' // 自定义训练数据路径
});


[!warning] 注意:



  1. 第一个参数为加载自定义训练数据的文件名,不带后缀。

  2. 加载自定义训练数据的文件后缀名必须为 .traineddata

  3. 如果文件名不是 .traineddata.gzip,则需要设置 gzipfalse



举例


const worker = await createWorker('my-data', OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
gzip: false,
langPath: 'http://localhost:5173/lang',
});

加载效果



4. 通过前端上传图片


通常,图片是通过前端让用户上传后进行解析的。以下是一个简单的 Vue 3 示例:


<script setup>
import { createWorker } from 'tesseract.js';

async function handleUpload(evt) {
const files = evt.target.files;
const worker = await createWorker("chi_sim");
for (let i = 0; i < files.length; i++) {
const ret = await worker.recognize(files[i]);
console.log(ret.data.text);
}
}
</script>

<template>
<input type="file" @change="handleUpload" />
</template>

完整示例


下面提供一个简单的 OCR 示例,展示了如何在前端实现图片上传、文字识别以及图像处理。


代码


<!--
* @Author: zi.yang
* @Date: 2024-12-10 09:15:22
* @LastEditors: zi.yang
* @LastEditTime: 2025-01-14 08:06:25
* @Description: 使用 tesseract.js 实现 OCR
* @FilePath: /vue-app/src/components/HelloWorld.vue
-->

<script setup lang="ts">
import { ref } from 'vue';
import { createWorker, OEM } from 'tesseract.js';

const uploadFileName = ref<string>("");
const imgText = ref<string>("");

const imgInput = ref<string>("");
const imgOriginal = ref<string>("");
const imgGrey = ref<string>("");
const imgBinary = ref<string>("");

async function handleUpload(evt: any) {
const file = evt.target.files?.[0];
if (!file) return;
uploadFileName.value = file.name;
imgInput.value = URL.createObjectURL(file);
const worker = await createWorker("chi_sim", OEM.DEFAULT, {
logger: info => console.log(info.status, info.progress),
});
const ret = await worker.recognize(file, { rotateAuto: true }, { imageColor: true, imageGrey: true, imageBinary: true });
imgText.value = ret.data.text || '';
imgOriginal.value = ret.data.imageColor || '';
imgGrey.value = ret.data.imageGrey || '';
imgBinary.value = ret.data.imageBinary || '';
}

// 占位符 svg
const svgIcon = encodeURIComponent('<svg t="1736901745913" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4323" width="140" height="140"><path d="M804.9 243.4c8.1 0 17.1 10.5 17.1 24.5v390.9c0 14-9.1 24.5-17.3 24.5H219.3c-8 0-17.3-10.7-17.3-24.5V267.9c0-14 9.1-24.5 17.3-24.5h585.6m0-80H219.3c-53.5 0-97.3 47-97.3 104.5v390.9c0 57.3 43.8 104.5 97.3 104.5h585.4c53.5 0 97.3-47 97.3-104.5V267.9c0-57.5-43.7-104.5-97.1-104.5z" fill="#5E9EFC" p-id="4324"></path><path d="M678.9 294.5c28 0 50.6 22.7 50.6 50.6 0 28-22.7 50.6-50.6 50.6s-50.6-22.7-50.6-50.6c0-28 22.7-50.6 50.6-50.6z m-376 317.6l101.4-215.7c6-12.8 24.2-12.8 30.2 0l101.4 215.7c5.2 11-2.8 23.8-15.1 23.8H318c-12.2 0-20.3-12.7-15.1-23.8z" fill="#5E9EFC" p-id="4325"></path><path d="M492.4 617L573 445.7c4.8-10.1 19.2-10.1 24 0L677.6 617c4.1 8.8-2.3 18.9-12 18.9H504.4c-9.7 0-16.1-10.1-12-18.9z" fill="#5E9EFC" opacity=".5" p-id="4326"></path></svg>');
const placeholder = 'data:image/svg+xml,' + svgIcon;
</script>

<template>
<div class="custom-file-upload">
<label for="file-upload" class="custom-label">选择文件</label>
<span id="file-name" class="file-name">{{ uploadFileName || '未选择文件' }}</span>
<input id="file-upload" type="file" @change="handleUpload" />
</div>

<div class="row">
<div class="column">
<p>输入图像</p>
<img alt="原图" :src="imgInput || placeholder">
</div>
<div class="column">
<p>旋转,原色</p>
<img alt="原色" :src="imgOriginal || placeholder">
</div>
<div class="column">
<p>旋转,灰度化</p>
<img alt="灰度化" :src="imgGrey || placeholder">
</div>
<div class="column">
<p>旋转,二值化</p>
<img alt="二进制" :src="imgBinary || placeholder">
</div>
</div>

<div class="result">
<h2>识别结果</h2>
<p>{{ imgText || '暂无结果' }}</p>
</div>
</template>

<style scoped>
/* 隐藏原生文件上传按钮 */
input[type="file"] {
display: none;
}

/* 自定义样式 */
.custom-file-upload {
display: inline-block;
cursor: pointer;
margin-bottom: 30px;
}

.custom-label {
padding: 10px 20px;
color: #fff;
background-color: #007bff;
border-radius: 5px;
display: inline-block;
font-size: 14px;
cursor: pointer;
}

.custom-label:hover {
background-color: #0056b3;
}

.file-name {
margin-left: 10px;
font-size: 14px;
color: #555;
}

.row {
display: flex;
width: 100%;
justify-content: space-around;
}

.column {
width: 24%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
text-align: center;
min-height: 100px;
}

.column > p {
margin: 0 0 10px 0;
padding: 5px;
border-bottom: 1px solid #ccc;
font-weight: 600;
}

.column > img {
width: 100%;
}

.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}

.result > h2 {
margin: 0;
}

.result > p {
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
font-size: 16px;
line-height: 1.5;
color: #333;
margin: 10px 0;
}
</style>

实现效果



资源加载失败


Tesseract.js 在运行时需要动态加载三个关键文件:Web Workerwasm训练数据。由于默认使用的是 jsDelivr CDN,国内用户可能会遇到网络加载问题。为了解决这个问题,可以通过指定 unpkg CDN 来加速资源加载:


const worker = await createWorker('chi_sim', OEM.DEFAULT, {
langPath: 'https://unpkg.com/@tesseract.js-data/chi_sim/4.0.0_best_int',
workerPath: 'https://unpkg.com/tesseract.js/dist/worker.min.js',
corePath: 'https://unpkg.com/tesseract.js-core/tesseract-core-simd-lstm.wasm.js',
});

如果需要离线使用,可以将这些资源下载到本地,并将路径指向本地文件即可。


结语


Tesseract.js 是目前前端领域较为成熟的 OCR 库,适合在无需后端支持的场景下快速实现文字识别功能。通过合理的图片预处理和优化,可以满足大部分中小型应用的需求。


相关链接



作者:子洋
来源:juejin.cn/post/7459791088791797786
收起阅读 »

我开源了一个基于 Tiptap 实现一个功能丰富的协同编辑器 🚀🚀🚀

web
一个基于 Tiptap 和 Next.js 构建的现代化协同文档编辑器,集成了丰富的编辑能力与多人实时协作功能,支持插件扩展、主题切换与持久化存储。适合团队写作、教育笔记、在线文档平台等场景。 项目地址 预览地址 无论你是想学习或者想参与开发,你都可以添加...
继续阅读 »

一个基于 TiptapNext.js 构建的现代化协同文档编辑器,集成了丰富的编辑能力与多人实时协作功能,支持插件扩展、主题切换与持久化存储。适合团队写作、教育笔记、在线文档平台等场景。



无论你是想学习或者想参与开发,你都可以添加我微信 yunmz777,我拉你进交流群中进行学习交流,我们还有很多其他不同的开源项目。



近期开始准备出一个 前端工程化实战 类的课程,如果你对前端技术迷茫,那么学习前端工程化是最好的一个进阶方案,以下是相关的实战内容大纲:



20250519222445


如果你感兴趣想参与的,可以添加我微信进行更详细的了解。


🚀 功能特性



  • 📄 富文本编辑:标题、列表、表格、代码块、数学公式、图片、拖拽等

  • 👥 实时协作:使用 Yjs + @hocuspocus/provider 实现高效协同

  • 🧩 插件丰富:基于 Tiptap Pro 多种增强功能(如表情、详情组件等)

  • 🧰 完善工具链:支持 Prettier、ESLint、Husky、Vitest 等开发工具


📦 技术栈


前端技术栈


技术说明
Next.js构建基础框架,支持 SSR / SSG
Tiptap富文本编辑器,基于 ProseMirror
Yjs协同编辑核心,CRDT 数据结构
@hocuspocusYjs 的服务端与客户端 Provider
React 19UI 框架,支持 Suspense 等新特性
Tailwind CSS原子化 CSS,集成动画、表单样式等
Socket.io协同通信通道
Prettier/ESLint代码风格统一
Vitest/Playwright单元测试与端到端测试支持

20250519183256


后端技术栈


分类技术 / 工具说明
应用框架NestJS现代化 Node.js 框架,支持模块化、依赖注入、装饰器和类型安全等特性
HTTP 服务Fastify高性能 Web 服务引擎,替代 Express,默认集成于 NestJS 中
协同编辑服务@hocuspocus/server, yjs提供文档协同编辑的 WebSocket 服务与 CRDT 算法实现
数据库 ORMPrisma类型安全的数据库访问工具,自动生成 Schema、支持迁移与种子数据
数据验证class-validator, class-transformer请求数据验证与自动转换,配合 DTO 使用
用户鉴权@nestjs/passport, passport, JWT, GitHub支持本地登录、JWT 认证与 GitHub OAuth 登录
缓存与状态ioredis用于缓存数据、实现限流、协同会话管理或 Pub/Sub 消息推送
对象存储minio私有化部署的 S3 兼容存储服务,支持图片与附件上传
图像处理sharp图像压缩、格式转换、缩略图等操作
日志系统winston, winston-daily-rotate-file支持多种格式、日志分级、自动归档的日志方案
服务监控@nestjs/terminus, prom-client提供 /health 健康检查和 /metrics Prometheus 指标暴露接口
监控平台Prometheus, Grafana采集与可视化服务运行指标(已内置 Docker 部署配置)
接口文档@nestjs/swagger基于代码注解自动生成 Swagger UI 文档
安全中间件@fastify/helmet, @fastify/rate-limit添加 HTTP 安全头部、限制请求频率、防止暴力攻击等安全保护
文件上传@fastify/multipart, @webundsoehne/nest-fastify-file-upload支持文件流式上传,集成 Fastify 与 NestJS 的多文件上传处理

20250519183049


🚀 快速开始


1. 克隆仓库


git clone https://github.com/xun082/DocFlow.git
cd DocFlow

安装依赖


建议使用 pnpm:


pnpm install

启动本地开发环境


pnpm dev

如何部署


确保已安装以下环境:



  • Docker

  • 推荐:Linux/macOS 或启用 WSL 的 Windows 环境


1️⃣ 构建镜像


docker build -t doc-flow .

2️⃣ 启动容器


docker run -p 6001:6001 doc-flow

启动完成之后访问地址:


http://localhost:6001

🔧 常用脚本


脚本命令作用说明
pnpm dev启动开发服务器
pnpm build构建生产环境代码
pnpm start启动生产环境服务(端口 6001)
pnpm lint自动修复所有 ESLint 报错
pnpm format使用 Prettier 格式化代码
pnpm type-check运行 TypeScript 类型检查
pnpm test启动测试(如配置)

🧰 开发规范



  • 使用 Prettier 和 ESLint 保证代码风格统一

  • 配置了 Husky + lint-staged 进行 Git 提交前检查

  • 使用 Commitizen + cz-git 管理提交信息格式(支持语义化发布)


初始化 Git 提交规范:


pnpm commit

📌 未来规划(Roadmap)


项目目前已具备基础协作编辑能力,未来将持续完善并拓展更多功能,进一步提升产品的实用性与专业性:


✅ 近期目标




  • 完善现有功能体验



    • 优化协同冲突解决策略

    • 更细粒度的权限管理(只读 / 可评论 / 可编辑)

    • 增强拖拽体验与文档结构导航(大纲视图)




  • 增强文档组件系统



    • 重构基础组件体系:标题、表格、代码块等更智能、模块化

    • 增加工具栏、快捷键提示和 Markdown 快速输入支持




  • 丰富文档类型与节点支持



    • 支持更多 自定义 Tiptap 节点,如:



      • 引用评论块(Comment Block)

      • 自定义警告框 / 提示框(Tip/Warning)

      • UML/流程图嵌入(如支持 Mermaid)

      • 数据展示组件(如 TableChart、Kanban)






🚀 中期目标




  • 引入音视频实时会议能力



    • 集成 LiveKitDaily 实现嵌入式音视频会议

    • 支持多人语音 / 视频通话,结合文档协同,提升远程会议效率

    • 集成会议内共享笔记区、AI 摘要、会议录制等功能




  • 集成 AI 能力



    • 智能语法纠错、改写建议

    • 语义搜索与问答(支持上下文理解)

    • AI 总结 / 摘要生成




  • 多平台同步支持



    • PWA 支持,适配移动端和桌面离线编辑

    • 跨设备自动同步与版本恢复




🧠 长期方向




  • 插件生态系统建设



    • 引入用户可安装的第三方插件体系

    • 提供插件开发文档与市场入口




  • 文档协作平台化



    • 支持文档团队空间、多人组织结构

    • 文档看板与团队活动看板集成




  • 权限与审计系统



    • 支持操作日志记录、文档编辑历史审查

    • 审批流、编辑建议、协同讨论区等功能




License


本项目采用 MIT 开源协议发布,但包含部分 Tiptap Pro 模板代码除外


Tiptap Pro 模板版权归 Tiptap GmbH 所有,并根据 Tiptap Pro 授权协议进行授权。

详见:tiptap.dev/pro/license


如需使用本项目中涉及 Tiptap Pro 的部分,必须拥有有效的 Tiptap Pro 订阅授权。


📬 联系方式


有更多的问题或者想参与开源,可以添加我微信 yunmz777,我们这还有很多开源项目:


20250519222610


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

手把手教你实现一个自动倒啤酒的效果

web
前言 继上次实现一个汽车运货的效果后,这次我就带大家来实现一个自动倒酒的效果,纯CSS实现,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。 效果预览 最终实现的相关效果如下。 HTML部分 首先看到HTML部分。相关代码如下。 <div ...
继续阅读 »

前言


继上次实现一个汽车运货的效果后,这次我就带大家来实现一个自动倒酒的效果,纯CSS实现,十分简单,没有花里胡哨的技巧。话不多说,咱们直接进入主题。


效果预览


最终实现的相关效果如下。


HTML部分


首先看到HTML部分。相关代码如下。


 <div class="container">
<div class="keg">
<span class="handle"></span>
<span class="pipe"></span>
</div>
<div class="glass">
<span class="beer"></span>
</div>
</div>

这里定义了一个啤酒桶(keg)和玻璃杯(glass)的容器结构,通常用于模拟倒啤酒的动画场景。


.container 是最外层容器,用于定位整个啤酒桶和玻璃杯的组合。 .keg(啤酒桶) 包含两个子元素分别是 .handle(啤酒桶的金属把手)以及 .pipe(出酒管道)。 .glass(玻璃杯) 包含  .beer 元素,表示杯中的啤酒液体,通常通过CSS动画模拟啤酒被倒入的效果。


CSS部分


由于这里的效果涉及了很多动画效果,并且作为该效果的主要功能是倒酒,所以我们在这里主要介绍动画相关的CSS部分,即是如何实现倒酒。相关代码如下。


这里是类名为flow的动画部分,相关代码如下。


@keyframes flow {
0%, 15% {
top: 40px;
height: 0;
}

20% {
height: 115px;
}

40% {
height: 75px;
}

55% {
top: 40px;
height: 50px;
}

60%, 100% {
top: 80px;
height: 0;
}
}

这个动画的整体效果是液体从无到有爆发式流出 → 流量逐渐减少 → 最后滴落消失,通过 height 变化模拟液体体积变化,通过 top 调整模拟液体位置移动(如滴落时的垂直位移)。最后再不断循环。


初始状态(0%, 15%) 液体不可见(高度为0),准备开始流动。液体开始流出(20%) 液体高度突然增加(从0到115px),模拟液体从管道中快速涌出。液体减少(40%) 液体高度降低(从115px到75px),模拟流量减小。液体即将流尽(55%) 液体顶部位置回弹(可能模拟最后几滴液体下落),高度进一步减小。液体完全消失(60%, 100%) 液体高度归零,同时顶部位置下移(top: 80px),模拟液体完全流尽或滴落。


最后就是handle,slide的动画部分,相关代码如下。


@keyframes handle {
10%, 60% {
transform: rotate(0deg);
}

20%, 50% {
transform: rotate(-90deg);
}
}
@keyframes slide {
0% {
left: 0;
filter: opacity(0);
}

20%, 80% {
left: 300px;
filter: opacity(1);
}

100% {
left: 600px;
filter: opacity(0);
}
}

这里定义了两个关键帧动画:handle(把手旋转)  和 slide(水平滑动淡入淡出)


把手旋转动画模拟把手(如啤酒桶开关)的来回扳动效果。在 0%-10% 保持初始状态(0deg)。在20% 快速逆时针旋转到-90deg(如打开阀门)。在50% 仍保持-90deg(持续打开状态)。在60% :回到0deg(关闭阀门)。


水平滑动动画 实现元素从左侧滑入、暂停、再滑出并淡出的效果。在 0% 元素从左侧(left: 0)透明状态开始。在 20%-80% 滑动到中间(left: 300px)并完全显示。在 100% 继续滑到右侧(left: 600px)并淡出。


最后就是fillup,fillup-foam,wave的动画部分,相关代码如下。


@keyframes fillup {
0%, 20% {
height: 0px;
border-width: 0px;
}

40% {
height: 40px;
}

80%, 100% {
height: 80px;
border-width: 5px;
}
}
@keyframes fillup-foam {
0%, 20% {
top: 0;
height: 0;
}

60%, 100% {
top: -15px;
height: 15px;
}
}

@keyframes wave {
from {
transform: skewY(-3deg);
}

to {
transform: skewY(3deg);
}
}

这里定义了三个关键帧动画,用于模拟液体(如啤酒)倒入容器时的动态效果,包括液体上升、泡沫生成和液体表面波动。


液体填充动画 模拟液体从空杯到满杯的填充过程。在 0%-20% 容器为空(高度为0)。在 40% 液体快速上升至半满(40px)。在 80%-100% 液体完全填满(80px),同时显示容器边框(如玻璃杯厚度)。


泡沫生成动画 模拟液体倒满时产生的泡沫层。在 0%-20% 无泡沫(高度为0)。在 60%-100% 泡沫在液体顶部形成并略微溢出(top: -15px)。通过 top 负值实现泡沫“溢出”杯口的视觉效果。


液体波动动画 模拟液体表面的轻微波动(如倒入后的晃动)。通过 skewY 实现Y轴倾斜变换,产生波浪效果。通常需配合 animation-direction: alternate 让动画来回播放。


总结


以上就是整个效果的实现过程了,纯 CSS 实现,代码简单易懂。另外,感兴趣的小伙伴们还可以在现有基础上发散思维,比如增加点其他效果,或者更改颜色等等。关于该效果如果大家有更好的想法欢迎在评论区分享,互相学习。最后,完整代码在码上掘金里可以查看,如果有什么问题大家在评论区里讨论~


作者:一条会coding的Shark
来源:juejin.cn/post/7502329326098366473
收起阅读 »

我在团队内部提倡禁用 css margin

web
新的文章已经写完了,从技术角度详详细细的介绍了我的理由,朋友们在阅读完本文之后,如果还有兴趣可继续深入阅读 juejin.cn/post/747896… 一一一分割线一一一 目前社区也有不少人提倡禁用 margin,大概原因有什么奇怪的边距融合、责任区域划分不...
继续阅读 »

新的文章已经写完了,从技术角度详详细细的介绍了我的理由,朋友们在阅读完本文之后,如果还有兴趣可继续深入阅读
juejin.cn/post/747896…


一一一分割线一一一


目前社区也有不少人提倡禁用 margin,大概原因有什么奇怪的边距融合、责任区域划分不明确等等,我今天从另一个角度来说明为什么不要使用 margin


我们现在处于协同化时代,基本都是靠 figma、motiff 这类在线设计工具看设计稿的。这类工具有写特点



  • 没有 margin 概念

  • 只有自动布局和约束布局两种方式

  • 有研发模式


自动布局等同于 flex 布局,支持设置主轴方向,主轴辅轴对其方式,间距(gap),边距(padding)等等
image.png
下面是我随手画的一个例子,在研发模式下,鼠标 hover 到容器上面,会出现蓝色和粉色区域。蓝色就代表 padding,粉色就代表 gap
image.png


约束就是绝对定位,这个很简单,不详细阐述
image.png


所以,由于工具的天然限制,设计师在画稿的时候,不会像写代码一些,条条大路通罗马。比如我想让两个 div 相距 100px,css 起码得有 10 种方式。所以我们作为前端开发,拿到设计稿的时候可以放心的相信设计师的打组结构,设计稿一个框,你就写一个 div。因为他们不会有天马行空的骚操作,两个设计师是有很大概率画出结构一样的设计稿的。


实战
我在 figma 画了一个移动端界面


image.png
然后切换到研发模式,从外向内开始选中图层查看细节


image.png
可以看到结构是一套四,竖向 flex 布局,间距是 29px padding 是 0


// frame 7
<div class='flex flex-col gap-29px'>
// frame 8
<div></div>
// frame 9
<div></div>
// frame 10
<div></div>
// frame 10
<div></div>
</div>

然后直接看最后一个图层,前面的简单就不看了
image.png
可以一看看出结构是 flex 横向布局,padding 13px 34px,justify-content: space-between
然后可以继续无脑的写代码了


// frame 7
<div class='flex flex-col gap-29px'>
// frame 8
<div></div>
// frame 9
<div></div>
// frame 10
<div></div>
// frame 10
<div class='flex px-13px py-34px justify-between'>
// star 3
<div></div>
// star 4
<div></div>
// star 5
<div></div>
// star 6
<div></div>
</div>
</div>

然后增加一个回到顶部的 float button,约束为右、下。
image.png
hover 到 button 上


image.png
发现出现了两条线,指向右和下,这就代表这是一个相对于父元素的右下角的绝对定位图层。只需要无脑写代码即可


// frame 7
<div class='relative flex flex-col gap-29px'>
.....
<div class='absolute right-xxx bottom-xxx w-10 h-10'></div>
</div>

总结


在使用 figma、motiff 这类的工具的情况下,



  1. 前端程序员可以无脑的根据设计稿分组来写自己的 html,绝大部分情况他们应该是一对一的。

  2. 应该跟随工具,只使用 flex 布局,绝对定位布局

  3. 绝大部分情况不应该使用 margin


确实存在一些情况使用 margin 会更方便,我也真实遇到了一些 case。如果你们有想聊的 case 可以发到评论区


作者:阿古达木
来源:juejin.cn/post/7478182398409965620
收起阅读 »

同学聚会,是我不配?

前言 初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。 缘起 高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙...
继续阅读 »

前言


初八就回城搬砖了,有位老哥跟我吐槽了他过年期间参与同学会的事,整理如下,看读者们是否也有相似的境遇。



image.png


缘起


高中毕业至今已有十五年了,虽然有班级群但鲜有人发言,一有人冒泡就会立马潜水围观。年前有位同学发了条消息:高中毕业15年了,趁过年时间,咱们大伙聚一聚?


我还是一如既往地只围观不发言,组织的同学看大家都三缄其口,随后发了一个红包并刷了几个表情。果然还是万恶的金钱有新引力,领了红包的同学也刷了不少谢谢老板的表情,于是乎大家都逐渐放开了,最终发起了接龙。


看到已接龙的几位同学在高中时还是和自己打过一些交道,再加上时间选的是大年初五,我刚好有空闲的时间,总归还是想怀旧,于是也接了龙。


牢笼


我们相约在县城的烧烤一条街某店会面,那离我们高中母校不远,以前偶尔经过但苦于囊中羞涩没有大快朵颐过。


到了烧烤店时发现人声鼎沸,猜拳、大笑声此起彼伏,我循着服务员的指示进入了包间。放眼望去已有四、五位同学在座位上,奇怪的是此时包间却是很安静,大家都在低头把玩着手机。


当我推门的那一刻,同学们都抬头放眼望来,迅速进行了一下眼神交流,微笑地打了招呼就落座。与左右座的同学寒暄了几句,进行一些不痛不痒的你问我答,而后就沉默,气氛落针可闻,那时我是多希望有服务员进来问:帅哥,要点单了吗?


还好最后一位同学也急匆匆赶到了,后续交流基本上明白了在场同学的工作性质。

张同学:组织者,在A小镇上开了超市、圆通、中通提货点,座驾卡迪拉克

李同学:一线城市小创业者,公司不到10人,座驾特斯拉

吴同学:县城第一中学老师、班主任,座驾大众

毛同学:县委办某科室职员、公务员,座驾比亚迪

王同学:某小镇纪委书记,座驾别克

潘同学:县住房和城乡建设局职员,事业编,座驾哈佛

我:二线城市码农一枚,座驾雅迪


一开始大家都在忆往昔,诉说过去的一些快乐的事、糗事、甚至秘辛,感觉自己的青葱时光就在眼前重现。
酒过三巡,气氛逐渐热烈,称呼也开始越拔越高,某书记、某局、某老板,主任、某老总的商业互吹。

期间大家的话题逐渐往县城的实事、新闻、八卦上靠,某某人被双了,某某同事动用了某层的关系调到了市里,某漂亮的女强人离婚了。


不巧的是张同学还需要拜会另一位老板,提前离席,李同学公司有事需要处理,离开一会。

只剩我和其他四位体制内的同学,他们在聊体制内的事,我不熟悉插不进话题,我聊公司的话题估计他们不懂、也不感兴趣。

更绝的是,毛同学接到了一个电话,而后提着酒杯拉着其他同学一起去隔壁的包间敬酒去了,只剩我一个人在包间里。

过了几分钟他们都提着空酒杯回来了,悄悄询问了吴同学才知道隔壁是县委办公室主任。

回来后,他们继续畅聊着县城的大小事。


烧烤结束之后,有同学提议去唱K,虽然我晚上没安排,但想到已经没多少可聊的就婉拒了。


释怀


沿着县城的母亲河散步,看着岸边新年的装饰,我陷入了沉思。

十多年前大家在同一间教室求学,甚至同一宿舍生活,十多年后大家的选择的生活方式千差万别,各自的境遇也大不相同。

再次相遇,共同的话题也只是学生时代,可是学生时代的事是陈旧的、不变的,而当下的事才是新鲜的、变化的。因此聚会里更多的是聊现在的事,如果不在一个圈子里,是聊不到一块的。


其实小城里,公务员是一个很好的选择,一是稳定,二是有面子(可能本身没多大权利,但是可以交易,可以传递)。小城里今天发生的事,明天就可能人尽皆知了,没有秘密可言。

有志于公务员岗位的朋友提早做准备,别等过了年纪就和体制内绝缘了。


其他人始终是过客,关注自己,取悦自己。



image.png


作者:小鱼人爱编程
来源:juejin.cn/post/7468614661326159881
收起阅读 »

微信小程序包体积治理

web
背景 微信考虑到小程序的体验和性能问题限制主包不能超过2M。哈啰微信小程序也随着业务线在主包中由简到复杂,体积越来越大,前期业务野蛮增长阶段npm库缺乏统一管理,第三方组件库本身工程复杂等问题导致包体积长期处于2M临界卡点,目前存在以下痛点: 阻塞各业务正常...
继续阅读 »

背景


微信考虑到小程序的体验和性能问题限制主包不能超过2M。哈啰微信小程序也随着业务线在主包中由简到复杂,体积越来越大,前期业务野蛮增长阶段npm库缺乏统一管理,第三方组件库本身工程复杂等问题导致包体积长期处于2M临界卡点,目前存在以下痛点:



  • 阻塞各业务正常微信小程序端需求排期。

  • 迭代需求需要人肉搜索包体积的增长点,推动增长业务线去优化对应的包体积,治标不治本。

  • 缺乏微信端包体积统一管理平台来限制各业务包体积增长。

  • 微信包体积太大导致加载时间长、体验差。


所以主要从包体积优化和长期控制包体积增长两个方面让微信包体积达到平衡状态,长期运行。


包体积优化


微信包体积优化是个老生常谈的话题,只要是公司业务体积达到一定的量级都会不可避免的碰到主包体积超出和体验问题,关于怎么解决官方和网上也给出了比较多的解决方案。知其然知其所以然,那我们就从小程序的原理层面去看解决方案。主要也分为常规的优化方案和结合业务优化技术方案。


常规优化方案


按照微信小程序官网介绍,我们把小程序的性能优化分为启动性能优化和运行时性能优化:



  • 启动性能 :小程序的启动过程以「用户打开小程序」为起点,到小程序「首页渲染完成」为止。小程序「首页渲染完成」的标志是首个页面 Page.onReady 事件触发。

  • 运行时性能:小程序的运行时性能直接决定了用户在使用小程序功能时的体验。如果运行时性能出现问题,很容易出现页面滚动卡顿、响应延迟等问题,影响用户使用。如果内存占用过高,还会出现黑屏、闪退等问题。


1.启动性能优化

在进行启动性能优化之前,先介绍下小程序启动流程,小程序的启动流程主要包括以下几个环节:


image.png


1.1 资源准备

a. 小程序相关信息准备:微信客户端需要从微信后台获取小程序的头像、昵称、版本、配置、权限等基本信息,这些信息会在本地缓存,并通过一定的机制进行更新。


b. 环境预加载(受到场景、设备资源和操作系统调度的影响,并不能保证每次启动一定命中)


为了尽可能的降低运行环境准备对启动耗时的影响,微信客户端会根据用户的使用场景和设备资源的使用情况,依照一定策略在小程序启动前对运行环境进行部分地预加载,以降低启动耗时。


image.png
c. 代码包准备

从微信后台获取代码包的地址,从CDN下载小程序代码包,并对代码包进行校验。

为了提高下载耗时,微信本身就做了一些优化:



  • 代码包压缩

  • 增量更新

  • 更高效的网络协议:下载代码包优先使用 QUIC 和 HTTP/2

  • 预先建立连接:在下载发生前,提前和 CDN 建立连接,降低下载过程中 DNS 请求和连接建立的耗时。

  • 代码包复用:对每个代码包都会计算 MD5 签名。即使发生了版本更新,如果代码包的 MD5 没有发生变化,则不需要重新进行下载。


1.2 小程序代码注入

小程序启动时需要从代码包内读取小程序的配置和代码,并注入到 JavaScript 引擎中,同时WXSS 和 WXML 会编译成 JavaScript 代码注入到视图层,视图层和逻辑层的小程序代码注入是并行进行的。


微信客户端会使用 V8 引擎的 Code Caching 技术对代码编译结果进行缓存,降低非首次注入时的编译耗时(Code Caching:V8会把编译和解析的结果缓存下来,等到下次遇到相同的文件时,直接使用缓存数据)


1.3 首屏渲染\


image.png


视图层和逻辑层都是从start并行进行初始化操作,视图层初始化完毕后会发送notify给逻辑层,自身进入等待状态,逻辑层收到信号后会结合自身初始化状态(第一种没初始化完,继续初始化。第二种初始化完进入等待状态)发送初始数据Data到视图层,结合初始数据和视图层得到的页面结构和样式信息,小程序框架会进行小程序首页的渲染,展示小程序首屏,并触发首页的 Page.onReady 事件。


1.4 优化方案

a. 控制包体积:降低代码包大小是最直接的手段,代码包大小直接影响了下载耗时,影响用户启动小程序时的体验。



  • 分包:使用 分包加载 是优化小程序启动耗时效果最明显的手段。及时清理无用代码和资源。

  • 独立分包。

  • 分包预下载:在使用「分包加载」后,虽然能够显著提升小程序的启动速度,但是当用户在使用小程序过程中跳转到分包内页面时,需要等待分包下载完成后才能进入页面,造成页面切换的延迟,影响小程序的使用体验

  • 分包异步化:「分包异步化」将小程序的分包从页面粒度细化到组件甚至文件粒度。


b. 代码注入优化:



  • 按需引入:在小程序启动时,启动页面依赖的所有代码包(主包、分包、插件包、扩展库等)的所有 JS 代码会全部合并注入,包括其他未访问的页面以及未用到自定义组件,同时所有页面和自定义组件的 JS 代码会被立刻执行。这造成很多没有使用的代码在小程序运行环境中注入执行,影响注入耗时和内存占用。

  • 用时注入:在开启「按需注入」特性的前提下,「用时注入」可以指定一部分自定义组件不在小程序启动时注入,而是在真正渲染的时候才进行注入。


c. 首屏渲染优化:



  • 启用【初始渲染缓存】:启用初始渲染缓存,可以使视图层不需要等待逻辑层初始化完毕,而直接提前将页面初始 data 的渲染结果展示给用户,这可以使得页面对用户可见的时间大大提前。

  • 数据预拉取:预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度 。

  • 周期性更新:周期性更新能够在用户未打开小程序的情况下,也能从服务器提前拉取数据,当用户打开小程序时可以更快地渲染页面,减少用户等待时间,增强在弱网条件下的可用性。

  • 骨架屏:如果首页内容是通过接口异步获取的,用户不一定立即看到完整的界面,需要等待接口返回后调用setData进行页面更新,才能看到真实内容,避免过长时间白屏可以选择骨架屏来提高用户体验。


2.运行时性能优化

2.1 优化方案:

a. 合理使用setData:小程序的逻辑层和视图层是两个独立的运行环境,通讯通过Native层实现。具体的实现原理和bridge实现一致,ios利用WKWebView提供的messageHandlers,安卓是往webview的window对象注入一个原生方法,所以数据传输的耗时和数据量的大小成正比。


b. 页面切换优化:页面切换的性能影响用户操作的连贯性和流畅度,是小程序运行时性能的一个重要组成部分。


请求前置:小程序不同于H5,在跳转本身就需要消耗比较多的时间,特别是在安卓机上,所以我们可以在页面跳转的同时进行数据并行请求。
image.png


c. 控制预加载下个页面的时机(仅安卓):

小程序页面加载完成后,会预加载下一个页面。默认情况下,小程序框架会在当前页面 onReady 触发 200ms 后触发预加载。


在安卓上,小程序渲染层所有页面的 WebView 共享同一个线程。很多情况下,小程序的初始数据只包括了页面的大致框架,并不是完整的内容。页面主体部分需要依靠 setData 进行更新。因此,预加载下一个页面可能会阻塞当前页面的渲染,造成 setData 和用户交互出现延迟,影响用户看到页面完整内容的时机。


image.png


我们本次拉齐两轮、数科、普惠,分别进行部分页面分包,下掉0流量页面及其依赖的npm包,把仅有单个业务线引用的npm从主小程序移植到分包下从而不占用主包体积,删除无用文件等操作才从2M体积减少到1.88M,这个收益对于反复优化过的主小程序而言已经算是不错的收益,但是很难满足未来各业务线对小程序主包体积的迭代诉求,所以我们还需要更优的解决方案来减少更多的包体积和限制各业务线在现有体积上进行置换而不是无限扩张。


关于这两个问题我们就在结合业务优化方案和长期控制包体积机制中探讨。


结合业务优化方案


1.第三方组件库异步分包

微信小程序为考虑体验问题主包被限制到了2M,但随着小程序业务线接入越来越多,npm库缺乏统一管理,第三方组件库本身工程比较复杂等问题导致主包超过1M+都被npm库所占用掉,留给业务的空间不足1M,所以可以从vendor.js中进行部分拆分优化,在不占用主包体积下主包也能够使用这些第三方库。


这样操作的意义在于可以把部分第三方npm库抽离到分包中,主包内只剩核心业务和不能拆的npm库。


实现原理:小程序的分包异步化就是来实现这种功能的,按照微信官方文档提供可以使用require来异步引入包含第三方npm的分包。


image.png


但是我们的小程序是使用taro,通过webpack进行编译的,静态产物不支持CommonJS模块的写法,所以require在编译的时候会进行报错,解决方法有两种:



  • 自定义webpack插件,将require关键字替换为customRequireKey(自定义key值,在解析的时候替换成require就可以)。


image.png



  • webpack提供的__non_webpack_require__代替require,不会被webpack解析。


注意点1:如果把第三方npm库改成异步引用后,对于之前通过import同步引用的代码需要进行改造,不然可能会出现在包引入前提前调用包内部方法的问题,对于这个问题可以创建缓存队列解决。


注意点2:分包因为网络波动等原因会加载失败,但是概率极低,可以使用重试机制解决。


2.封面方案

封面方案相比于第三方组件异步分包方案更好理解,就是把业务全部抽离到分包中,主包中只保留各业务线所依赖的基础库和公共文件,在小程序启动的时候做个启动界面,页面一旦加载就立即跳转到真正承载业务的页面中, 而这个页面被放在分包中。


这么做的好处在于主包中的2M体积只用来放基础库和公共文件,包体积始终控制在1M左右,对小程序性能优化和体验上都有很大的提升。而其他业务都放在业务的主分包中进行管理。


image.png


长期控制包体积机制


主包体积优化后如果缺乏标准的控制方法,在未来还是会随着各业务迭代增加不停的增加直到超出2M。所以一套标准的管理机制也是至关重要的。


小程序包体积治理主要从两个方面:



  • 业务线管理机制后台

  • 发布系统管理机制


业务线管理机制后台


业务线size管理机制后台主要集临时资源申请和图标展示于一体,以解决业务线临时size压力。可以通过后台系统进行临时size申请,提出申请后说明申请原因、资源需要时长、size大小,到达审批人时可酌情考虑,审批通过\不通过后都会钉钉通知申请。在管理平台也能看到当前业务线的永久size、临时size、临时size到期时间、申请理由和各业务每迭代包体积大小等信息。


image.png


a. 申请临时资源流程:用户根据自己的诉求进入后台选择对应业务线点击新增按钮去申请临时资源、申请临时资源时需在申请弹窗中明确以下几点内容:



  • 申请资源大小:最大申请资源为当前包体积剩余的最大值

  • 使用时间:最多为2个迭代就要把临时资源退回、否则限制发布流程

  • 申请理由:在理由中需要明确填写申请资源后带来的业务价值、由平台的产品侧和研发侧共同衡量价值。

  • prd地址:链接地址。


b. 申请临时资源最长路径:最多为2个迭代就要把申请的临时资源进行退回、否则在发布时限制发布。


c. 临时申请最大包体积:申请最大资源为当前包体积剩余的最大值


d. 包体积到期通知:提前一个迭代时间钉钉通知对应的申请人和leader包体积到期时间进行优化,申请资源到期后后台系统会自动把申请的资源状态改为已到期,并减少对应申请的资源大小,如果未归还对应体积大小,在发布流程阶段会做体积大小卡口,限制发布。


发布系统管理机制


发布系统管理机制主要流程是developer在AppHelloBikeWXSS项目上每次通过feature分支merge到release分支的时候都会触发gitlab的钩子函数,然后触发jenkins的job进行编译、计算现在各业务线在主包中所占的体积,在通过包体积管理后台申请的体积进行比对,如果超出会钉钉通知到开发者并且在发布系统限制发布,如果没超出正常发布。


image.png
(本文作者:董恒磊)


作者:哈啰技术小编
来源:juejin.cn/post/7381657886801805312
收起阅读 »

前端页面怎么限制用户打开浏览器控制台?

web
说在前面 作为一名开发,相信大家对于浏览器控制台都是不陌生的,平时页面一出问题第一反应总是先打开控制台看看报错信息,而且还可以在控制台里插入自己的脚本信息来修改页面逻辑,那么你有没有想过 怎么限制用户打开控制台 呢? 禁用右键菜单 🔨 添加图片注释,不超...
继续阅读 »

说在前面



作为一名开发,相信大家对于浏览器控制台都是不陌生的,平时页面一出问题第一反应总是先打开控制台看看报错信息,而且还可以在控制台里插入自己的脚本信息来修改页面逻辑,那么你有没有想过 怎么限制用户打开控制台 呢?



禁用右键菜单 🔨



添加图片注释,不超过 140 字(可选)


在页面上点击鼠标右键我们可以看到有个 检查 选项,通过这个菜单可以直接打开控制台,我们可以直接在这个页面上禁用右键菜单。


document.addEventListener("contextmenu", e => e.preventDefault());  

加上这段代码后用户在页面上点击右键就不会有反应了。


拦截快捷键 🛑


除了右键菜单栏,还有最经典的 F12 ,通过 F12 快捷键也可以快速打开控制台,所以我们也可以将这个快捷键给拦截掉


document.addEventListener("keydown", e => {  
if (e.keyCode === 123) {
e.preventDefault();
}
});

那么除了 F12 你知道还有什么快捷键可以打开控制台吗?



  • Ctrl+Shift+C

  • Ctrl+Shift+I


上面这两个快捷键也可以打开控制台,还有一个快捷键 Ctrl+U 可以打开源码页面,这里我们也可以一起把它给拦截掉。


document.addEventListener("keydown", e => {  
if (e.keyCode === 123 || // F12
(e.ctrlKey && e.shiftKey && e.keyCode === 67) || // Ctrl+Shift+C
(e.ctrlKey && e.shiftKey && e.keyCode === 73) || // Ctrl+Shift+I
(e.ctrlKey && e.keyCode === 85)) { // Ctrl+U
e.preventDefault();
}
});

加上这段代码后用户在页面上按下这些快捷键就不会有反应了。


检测窗口变化🔷


加上前面的拦截之后,其实我们还是有办法打开控制台,可以通过浏览器设置来打开控制台,这里的入口我们并无法监听拦截到



添加图片注释,不超过 140 字(可选)


let lastWidth = window.innerWidth;
let lastHeight = window.innerHeight;

window.addEventListener("resize", () => {
const widthDiff = Math.abs(window.innerWidth - lastWidth);
const heightDiff = Math.abs(window.innerHeight - lastHeight);

// 如果窗口尺寸变化但不是全屏切换,可能是控制台打开
if ((widthDiff > 50 || heightDiff > 50) && !isFullScreen()) {
//跳转到空白页面
window.location.href = "about:blank";
alert("检测到异常窗口变化,请关闭开发者工具");
}
lastWidth = window.innerWidth;
lastHeight = window.innerHeight;
});

function isFullScreen() {
return (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement
);
}

通常默认是会在页面内靠边打开控制台,所以可以通过监听页面大小变化来简单判断是否打开控制台,监听到打开后直接跳转到空白页面。



添加图片注释,不超过 140 字(可选)


但是还有这么两种情况



  • 全屏切换时的尺寸变化可能被误判

  • 独立打开控制台页面时无法监听到


无限Debugger⚡


我们还可以通过 Function("debugger") 来动态生成断点(动态生成是为了防断点禁用),通过无限循环生成断点,让页面一直处于断点状态。


(() => {  
function block() {
setInterval(() => {
(function(){return false;})["constructor"]("debugger")["call"]();
}, 50);
}
try { block(); } catch (err) {}
})();


添加图片注释,不超过 140 字(可选)



虽然我们可以通过一些技术手段,给用户打开控制台设置一些障碍,但对于经验老到的用户而言,绕过这些限制并非难事。依赖前端技术拦截控制台访问是一种典型的“防君子不防小人”策略,不能想着靠这些手段来保障自己网站的安全。



公众号


关注公众号『 前端也能这么有趣 』,获取更多有趣内容。


发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~


说在后面



🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。



作者:JYeontu
来源:juejin.cn/post/7508362269586063360
收起阅读 »

开源鸿蒙开发者大会2025交流区亮点纷呈,社区与生态伙伴共绘智能图景

5月24日,开源鸿蒙开发者大会2025(OHDC.2025)在深圳盛大召开。本次大会由开源鸿蒙项目群工作委员会主办,华为、润开鸿、中软国际、开鸿智谷、九联开鸿、鸿湖万联、深开鸿、诚迈科技、中国南方电网、中国科学院软件研究所、证通电子等合作单位共同支持。本次大会...
继续阅读 »

5月24日,开源鸿蒙开发者大会2025(OHDC.2025)在深圳盛大召开。本次大会由开源鸿蒙项目群工作委员会主办,华为、润开鸿、中软国际、开鸿智谷、九联开鸿、鸿湖万联、深开鸿、诚迈科技、中国南方电网、中国科学院软件研究所、证通电子等合作单位共同支持。本次大会聚焦开源鸿蒙 5.1 Release版本,面向开发者和伙伴深度展示开源鸿蒙操作系统的生态成果与落地实践。

图片1.png

本届大会在主论坛和八场主题分论坛之外,特别打造了互动交流区,通过公共交流区和十家生态伙伴交流区的立体布局,吸引了现场参会嘉宾的沉浸式体验,全面展示了基于开源鸿蒙创新技术,在消费电子产品领域的最新生态成果及其在千行百业的落地实践。

图片2.png

开源鸿蒙公共交流区:全面展示开源鸿蒙在千行百业的最新实践

在大会公共交流区,集中展示了基于开源鸿蒙在AI、Watch、大屏等领域的最新落地成果,包括基于开源鸿蒙的笔记本、手表可穿戴设备集群,智慧大屏等组成的全场景产品矩阵,其跨设备无缝协同能力引发体验热潮。

AI时代,万物智联。开源鸿蒙作为万物智联时代的全场景智能终端操作系统,在AI技术加持下将发挥“乘数效应”,创造更强的新质生产力,加速千行百业数字化应用创新。在大会的公共交流区,基于开源鸿蒙的智能机器人和PC等产品吸引了大量参会者驻足互动。基于开源鸿蒙的AI大模型机器人搭载开源鸿蒙操作系统,集成了ROS机器人操作系统环境并接入了AI大模型能力,可实现环境感知、视觉识别、创建激光地图、定位、避障和导航功能。基于开源鸿蒙的PC系列产品则面向开源鸿蒙开发者量身打造,搭载基于开源鸿蒙深度定制的KaihongOS桌面操作系统,构建起一站式的开源鸿蒙学习与开发平台。这一系列AI+成果丰富了开源鸿蒙在人工智能技术领域的使用场景,将进一步推动基于开源的技术创新,促进开源鸿蒙社区的生态繁荣,同时也将推动更多开发者投入到开源鸿蒙社区共建中,让开发者从技术“消费者”转变为“共创者”。

图片3.png

在消费电子产品领域,开源鸿蒙以其开源的特性持续推动产品升级,此次交流区现场,也全面展示了基于开源鸿蒙打造的Watch生态和大屏生态等系列成果,全方位展现了开源鸿蒙在硬件适配上以及生态整合上的强大实力,彰显出开源鸿蒙生态蓬勃的发展活力与无限潜力。

在Watch生态交流区,已上线的开源鸿蒙Watch表盘设计工具Dial Studio成为焦点。它是基于开源鸿蒙专为智能手表量身定制的工具,用户仅需轻点鼠标,就能轻松创作出独具个性的表盘,大幅降低了表盘开发的技术门槛。此外,华为、KUMI、速智达、金康特等多家行业伙伴基于开源鸿蒙精心打造的智能手表等可穿戴设备也在现场惊艳亮相。

图片4.png

大屏生态交流区则重点展示了基于开源鸿蒙打造的TV形态操作系统,该系统集成红外、蓝牙、灵犀等多种遥控器的控制方式,基于开源鸿蒙底座分布式特性,支持分布式音乐 、图库、视频、摄像头、白板等应用 ,可实现多设备联动,高效协同。基于开源鸿蒙打造TV形态的操作系统会持续迭代,未来还将面向电视、投影仪、机顶盒、广告机、会议大屏等不同场景、不同设备实现适配。

图片5.png

开源鸿蒙社区成果展:技术牵引产学研协同,共筑全球生态

值得一提的是,本次大会的专题交流区首次展出了有关开源鸿蒙 SIG 地图、开源鸿蒙Web SIG及W3C标准、统一互联PMC(筹)地图等丰富的信息,以及第二届中国研究生操作系统开源创新大赛开源鸿蒙赛道、开源鸿蒙人才生态建设以及开源鸿蒙社区出海的最新动态。

其中,统一互联PMC(筹)由通信互联TSG孵化,并主导筹建统一互联PMC,负责社区开发及运营。统一互联PMC(筹)专注于解决跨操作系统、跨厂商的开源鸿蒙设备互通技术难题,构建统一互联技术底座。目前已吸引24家社区伙伴、240+名成员参与,并完成开源鸿蒙统一互联2.0版本发布、统一互联接入与接口规范/物模型规范/测试规范等团标发布和统一互联白皮书发布。

图片6.png

此外,现场还展示了面向高校人才培养的中国研究生操作系统开源创新大赛最新进展,赛事以夯实基础软件生态和人才培养为目标,围绕操作系统关键技术和生态建设,激发研究生创新意识,提高研究生创新和实践能力,着力培养创新型、复合型、应用型高端人才,为操作系统生态健康发展提供人才支撑。

在出海交流区,社区展示了与欧洲开源组织Eclipse合作、面向欧洲市场发布了以开源鸿蒙为基础的Oniro OS的进展。Oniro产品进入欧洲市场,不仅确保智能设备之间的全球应用程序兼容性,同时也为开源鸿蒙智能终端操作系统的全球开源实践提供了创新范例。未来,开源鸿蒙将持续与全球开源社区开发者们共享技术资源,共同探讨技术进展,持续推动全球开源生态良好发展。

图片7.png

开源鸿蒙项目群依托产学研各界及广大开发者的共同努力,以创新为基石,以开放为理念,在技术与生态领域斩获累累硕果。开源鸿蒙社区也在技术创新发展、开源生态建设、创新人才培养及产学研深度融合等方面持续投入,携手生态伙伴推动开源鸿蒙生态在全球范围内蓬勃发展。

十家伙伴携最新成果亮相:产业共建打造开源鸿蒙繁荣生态

从智能服务到万物互联,开源鸿蒙的生态演进正在书写数字时代的范式变革。截至目前,开源鸿蒙已有400多家生态伙伴的1100多个软硬件产品通过兼容性测评,并落地到金融、交通、教育、医疗、航天、能源、政务、工业、消费电子等多个行业及领域。

图片8.png

生态的发展离不开各行各业伙伴的共同努力。在大会现场,十家共建单位带来了丰富的面向开源鸿蒙商业落地的产品案例和最新技术成果:

江苏润开鸿数字科技有限公司交流区呈现了其作为社区核心共建单位助力开源鸿蒙技术演进与生态构建的全景战略规划,并重点展示基于开源鸿蒙的全场景巡检实训车【Pro版】、机器狗星闪联动场景DEMO、鸿锐云桌面终端、NearLink物联开发平台等产品成果,推动基于开源鸿蒙的新“智”硬件创新,以加速数智技术与行业场景的深度融合与实践。

中软国际科技服务有限公司携雷泽智联一体机、基于开源鸿蒙的信号控制机与超级智能充电桩惊艳亮相,展示了开源鸿蒙生态的创新成果。聚焦交通领域,中软国际基于开源鸿蒙信号控制机融合先进算法,实现交通信号智能调度与优化,助力城市交通流畅运行。

湖南开鸿智谷数字产业发展有限公司携在鸿OS应用开发实验箱、基于开源鸿蒙的智慧园区沙盘(含三防平板)、在鸿隧道控制器、在鸿通用控制器、在鸿IO控制器、在鸿车道控制器、在鸿笔记本、在鸿平板等最新应用成果亮相展台,全面呈现了基于开源鸿蒙生态的技术创新与行业解决方案。

广东九联开鸿科技发展有限公司交流区重点展示了基于开源鸿蒙的实训箱以及动环监测系统。这是一款针对数据中心、机房等关键设施的环境与设备状态进行实时监测的系统。基于开源鸿蒙技术底座,充分发挥开源鸿蒙分布式软总线的技术特点,通过视频监控+传感器相结合的融合感知解决方案,实现电力机房的无人值守、日常监测、辅助运维的功能。

鸿湖万联(江苏)科技发展有限公司作为开源鸿蒙项目群A类捐赠人,一直以来积极参与开源鸿蒙生态共建和人才培养。在交流区现场,全面展示了其基于SwanlinkOS天鸿操作系统、联合近百家合作伙伴完成的50 +商用设备开源鸿蒙化成果,以及携手头部芯片伙伴打造的AI IPC和智慧大屏等核心产品。

深圳开鸿数字产业发展有限公司交流区展出了丰富的专为赋能开源鸿蒙开发者打造的公司自有及伙伴生态产品。其中,一站式面向开源鸿蒙的学习平台——开鸿Bot系列电脑产品成为焦点。专为开发者量身定制,极大降低开发者学习门槛,重构开源鸿蒙开发体验。此外还展示了驭系列-星闪无人机和开鸿智慧全场景实验箱等,为开发者提供了多样的实践与探索工具

诚迈科技(南京)股份有限公司携多款基于开源鸿蒙打造的鸿诚志远HongZOS系列产品亮相大会,展出多款基于开源鸿蒙的行业解决方案,覆盖工业、教育及警务领域,通过开放化的硬件平台与标准化的系统架构,为开发者提供可快速复用的行业解决方案,加速千行百业智能化转型进程。

中国南方电网有限责任公司展示了最新发布的基于开源鸿蒙5.1 Release版本的电力物联操作系统方案,该方案基于ArkUI框架实现“一次开发、多端部署”,整合开源鸿蒙分布式技术架构,构建起能源行业全域互联、智能协同的物联网络体系,并在开源鸿蒙AI能力基础上进一步集成先进算法,全面激活端侧与边缘侧设备的AI潜能。

中国科学院软件研究所重点展示了一款基于RISC-V 架构的创新型笔记本电脑——如意BOOK,以开源鸿蒙生态与全栈开源技术布局,重新定义轻办公、开发与教育场景的新形态。

深圳市证开鸿科技有限公司展出智慧金融厅堂一体化解决方案及产品。这一方案搭载基于开源鸿蒙的LightBeeOS系统,融合多形态智能终端、AI、物联网、大数据等前沿技术,精准适配金融网点场景,助力金融机构提升服务质量与运营效率,为客户带来便捷优质的金融体验。

开源鸿蒙开源共建四年多来,始终秉承“共建、共治、共享”的理念,通过持续建设,携手伙伴持续壮大开源生态。未来,社区也将继续以开放姿态拥抱千行百业伙伴,共同推动技术创新与商业应用落地,携手共建万物智联新时代。

收起阅读 »

BOE(京东方)第6代新型半导体显示器件生产线全面量产 打造全球显示产业新引擎

2025年5月26日,BOE(京东方)成功举办主题为“屏启未来 智显无界”的量产交付活动,开启第6代新型半导体显示器件生产线由建设转向运营的崭新篇章。这不仅标志着BOE(京东方)在LTPO、LTPS、Mini LED等高端显示领域实现跨越式突破,也为我国半导体...
继续阅读 »

2025年5月26日,BOE(京东方)成功举办主题为“屏启未来 智显无界”的量产交付活动,开启第6代新型半导体显示器件生产线由建设转向运营的崭新篇章。这不仅标志着BOE(京东方)在LTPO、LTPS、Mini LED等高端显示领域实现跨越式突破,也为我国半导体显示产业注入强劲动能,加速助力北京打造国际科技创新中心。作为全球技术最先进、产能最大的VR用LCD生产基地,该生产线将充分发挥技术引领和产业集聚优势,进一步巩固BOE(京东方)行业龙头地位,加速全球虚拟现实产业和数字经济发展。BOE(京东方)科技集团董事长陈炎顺,BOE(京东方)首席执行官冯强,BOE(京东方)首席运营官王锡平,行业专家及生态伙伴出席现场仪式,共同见证这一荣耀时刻。

活动现场,BOE(京东方)科技集团董事长陈炎顺发表致辞,他表示,BOE(京东方)以“BOE速度”打造新型显示产业基地建设标杆,成功实现开工当年封顶、次年产品点亮的关键目标。与此同时,技术研发与产品准备也在同步推进,多款产品已完成客户送样并推进交付。BOE(京东方)特别感谢战略合作伙伴们对技术创新的追求和坚持,这也推动着BOE(京东方)不断超越自我,取得一个又一个新的突破。BOE(京东方)将始终以战略客户伙伴的前沿需求和技术标准为指引,在“屏之物联”战略指导下,用踏实奋斗和持续创新回馈各界支持。

作为全球技术最先进的液晶显示屏生产基地,BOE(京东方)第6代新型半导体显示器件生产线总投资290亿元,占地面积42万平方米,设计月产能达5万片。该生产线以LTPO(低温多晶氧化物)和LTPS(低温多晶硅)技术为核心,聚焦聚焦 VR 显示面板、中小尺寸高附加值 IT 显示面板、车载显示面板等高端产品研发与生产,采用1500mm×1850mm的6代线玻璃基板,配备当前最先进的生产设备,并整合京东方多条成熟产线的先进经验,大幅提升生产效率和产品精度。在技术创新方面,BOE(京东方)LTPO技术融合了LTPS的高迁移率和Oxide的低功耗优势,可实现1500PPI以上的超高像素密度,并大幅度降低面板功耗,为显示设备提供更流畅、更清晰的动态画面。

值得一提的是,BOE(京东方)第6代新型半导体显示器件生产线还充分赋能多元化的场景应用,多款产品凭借极具竞争力的产品性能和领先的技术优势,获得全球一线知名客户的高度认可。其中,BOE(京东方)自主设计开发的超高2117PPI Real RGB显示屏实现成功点亮,达到当前LCD行业最高分辨率。在此次交付活动上,BOE(京东方)展示了已具备量产条件的2.24英寸1500PPI以及2.24英寸1700PPI VR显示模组,16英寸240Hz电竞笔记本屏幕(分辨率2560×1600,100% DCI-P3色域),以及14.6英寸窄边框高端车载中控屏等产品,全面满足“元宇宙”、高端消费电子、智能出行等领域的需求。

更加值得关注的是,BOE(京东方)第6代新型半导体显示器件生产线还在可持续发展方面走在世界前列。通过洁净室气流集控、AI分区温湿度自调、用电集控等创新技术,BOE(京东方)实现供热回收使用率100%、实现纯水回用率达80%、污染物排放均值小于标准50%。此外,在“双碳”目标引领下,BOE(京东方)将绿色理念贯穿于研发、生产与回收全生命周期。例如,生产线生产的产品在提升画质的同时更加注重产品低功耗性能,为设备的长时间使用提供可持续支持。这些实践不仅呼应了全球绿色低碳转型趋势,更展现了BOE(京东方)作为行业领军者的责任担当。同时,依托AI赋能,BOE(京东方)第6代新型半导体显示器件生产线还实现了智能排产、预测性维护、智能缺陷管理等全流程优化,设备综合效率(OEE)提升0.5%,工艺稳定性提升20%,良率分析效率提升20%,为行业树立了绿色生产与智能制造的双重标杆,也有力地回应了BOE(京东方)“Open Next Earth”的可持续发展品牌内涵。

在虚实交融的数字文明浪潮中,屏幕已从信息媒介跃升为跨越现实与虚拟、链接当下与未来的纽带。BOE(京东方)将持续以“屏之物联”战略为核心,加速显示技术与物联网、人工智能等前沿技术的深度融合,深刻践行“科技创新+绿色发展”之道。面向未来,BOE(京东方)将与更多合作伙伴携手,以协同创新之力探寻合作路径,全力赋能万物互联的未来智能生态体系,共同迎接一个更智慧、更互联、更美好、更绿色的全新时代。


收起阅读 »

前端遇到高并发如何解决重复请求

web
在前端开发中遇到高并发场景时,若不加控制容易出现重复请求,这可能导致接口压力增加、数据异常、用户体验变差等问题。以下是前端防止/解决重复请求的常见方法,按不同场景归类总结: 🌟 一、常见重复请求场景 用户频繁点击按钮:多次触发相同请求(例如提交表单、下载操...
继续阅读 »

在前端开发中遇到高并发场景时,若不加控制容易出现重复请求,这可能导致接口压力增加、数据异常、用户体验变差等问题。以下是前端防止/解决重复请求的常见方法,按不同场景归类总结:




🌟 一、常见重复请求场景



  1. 用户频繁点击按钮:多次触发相同请求(例如提交表单、下载操作)。

  2. 路由短时间内多次跳转或刷新:导致重复加载数据。

  3. 多次调用 debounce/throttle 未正确控制函数执行时机

  4. 轮询或 WebSocket 消息导致并发访问同一接口




🚀 二、常用解决方案


✅ 1. 禁用按钮防止多次点击


const [loading, setLoading] = useState(false);

const handleClick = async () => {
if (loading) return;
setLoading(true);
try {
await fetchData();
} finally {
setLoading(false);
}
};
<Button loading={loading} onClick={handleClick}>提交</Button>

✅ 2. 使用请求缓存 + Map 记录请求状态


原理:在请求发出前先检查是否已有相同请求在进行。


const requestCache = new Map();

const requestWithDeduplication = (url: string, options: any = {}) => {
if (requestCache.has(url)) {
return requestCache.get(url); // 复用已有请求
}

const req = fetch(url, options).finally(() => {
requestCache.delete(url); // 请求结束后清除缓存
});

requestCache.set(url, req);
return req;
};


适合统一封装 fetchaxios 请求,避免相同参数的并发请求。





✅ 3. 使用 Axios 的 CancelToken 取消上一次请求


let controller: AbortController | null = null;

const request = async (url: string) => {
if (controller) {
controller.abort(); // 取消上一个请求
}
controller = new AbortController();

try {
const res = await fetch(url, { signal: controller.signal });
return await res.json();
} catch (e) {
if (e.name === 'AbortError') {
console.log('Request canceled');
}
}
};


适合搜索联想、快速切换 tab 等需要 只保留最后一次请求 的场景。





✅ 4. 使用 debounce/throttle 防抖节流


import { debounce } from 'lodash';

const fetchData = debounce((params) => {
// 实际请求
}, 300);

<input onChange={(e) => fetchData(e.target.value)} />


控制高频输入类请求频率,减少并发请求量。





✅ 5. 后端幂等 + 前端唯一请求 ID(可选高级方案)



  • 给每次请求生成唯一 ID(如 UUID),发送给后端。

  • 后端对相同 ID 请求只处理一次。

  • 前端避免再做复杂状态判断,适合交易、支付类场景。




🧠 小结对照表


场景推荐方案
按钮多次点击禁用按钮 / loading 状态
相同请求并发请求缓存 Map / Axios CancelToken
输入频繁调用接口debounce 防抖
只保留最后一个请求AbortController / CancelToken
表单提交 /交易请求幂等请求唯一 ID + 后端幂等处理



如果你告诉我你遇到的具体是哪个页面或场景(例如点击下载、搜索联想、多 tab 切换等),我可以给出更加定制化的解决方案。


作者:光影少年
来源:juejin.cn/post/7507560729609830434
收起阅读 »

生产环境到底能用Docker部署MySQL吗?

程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。” 架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。” 小李:“...
继续阅读 »

程序员小李:“老王,我有个问题想请教您。MySQL 能不能部署在 Docker 里?我听说很多人说不行,性能会有瓶颈。”


架构师老王:“摸摸自己光突突的脑袋, 小李啊,这个问题可不简单。以前确实很多人说不行,但现在技术发展这么快,情况可能不一样了。”


小李:“那您的意思是,现在可以了?”


老王:“也不能这么说。性能、数据安全、运维复杂度,这些都是需要考虑的。不过,已经有不少公司在生产环境里用 Docker 跑 MySQL 了,效果还不错。”


Docker(鲸鱼)+MySQL(海豚)到底如何,我们来具体看看:



一、业界大厂


我们来看看业界使用情况:


1.1、京东超70%的MySQL在Docker中



刘风才是京东的资深数据库专家,他分享了京东在MySQL数据库Docker化方面的实践经验。京东从最初的小规模使用,到现在超过70%的MySQL数据库运行在Docker容器中


当然京东也不是所有的业务都适合把 mysql 部署在 docker 容器中。比如,


刘风才演讲中也提出:数据文件多于1T多的情况下是不太合适部署在Docker上的;再有就是在性能上要求特别高的,特别重要的核心系统目前仍跑在物理机上,后面随着Docker技术不断的改进,会陆续地迁到Docker上。


1.2、 同程艺龙:大规模 MySQL 容器化实践



同程艺龙的机票事业群 CTO 王晓波在QCon北京2018大会上做了《MySQL的Docker容器化大规模实践》的主题演讲。他分享了同程艺龙如何大规模实践基于Docker的MySQL私有云平台,集成了高可用、快速部署、自动化备份、性能监控等多项自动化运维功能。该平台支撑了总量90%以上的MySQL服务(实际数量超过2000个),资源利用率提升了30倍,数据库交付能力提升了70倍,并经受住了业务高峰期的考验。


当然不仅仅是京东、同程像阿里云、腾讯、字节、美团等都有把 Mysql 部署在 Docker 容器中的案例。


二、官方情况


MySql 官方文档提供了 mysql 的 docker 部署方式,文档中并没有明确的表明这种方式是适用于开发、测试或生产。那就是通用性的,也就是说生产也可以使用


以下就是安装的脚本可以看到配置文件和数据都是挂载到宿主机上。


docker run --name=mysql1 \
--mount type=bind,src=/path-on-host-machine/my.cnf,dst=/etc/my.cnf \
--mount type=bind,src=/path-on-host-machine/datadir,dst=/var/lib/mysql \
-d container-registry.oracle.com/mysql/community-server:tag

再看看镜像文件,可以看到 oralce 官方 7 年前就发布了 mysql5.7 的镜像。



三、具体分析


反方观点:生产环境MySQL不该部署在Docker里


反方主要担心数据持久化、性能、复杂性、备份恢复和安全性等问题,觉得在Docker里跑MySQL风险挺大。


正方观点:生产环境MySQL可以部署在Docker里


正方则认为Docker的灵活性、可移植性、资源隔离、自动化管理以及社区支持都挺好,生产环境用Docker部署MySQL是可行的,而且有成熟的解决方案来应对数据持久化和性能等问题。


总结


争议的焦点主要在于Docker容器会不会影响性能。其实 Docker和虚拟机不一样,虚拟机是模拟物理机硬件,而Docker是基于Linux内核的cgroups和namespaces技术,实现了CPU、内存、网络和I/O的共享与隔离,性能损失很小。



Docker 和传统虚拟化方式的不同之处,在于 Docker 是在操作系统层面上实现虚拟化,直接复用本地主机的操作系统,而传统方式则是在硬件层面实现。


Docker的特点:



  • 轻量级:共享宿主机内核,启动快,资源占用少。

  • 隔离性:容器之间相互隔离,不会互相干扰。

  • 可移植性:容器可以在任何支持Docker的平台上运行,不用改代码。


四、结尾


Docker虚拟化操作系统而不是硬件


随着技术的发展,Docker在数据库部署中的应用可能会越来越多。


所以,生产环境在Docker里部署MySQL,虽然有争议,但大厂都在用,官方也支持,技术也在不断进步,未来可能是个趋势。


我是栈江湖,如果你喜欢此文章,不要忘记点赞+关注


作者:栈江湖
来源:juejin.cn/post/7497057694530502665
收起阅读 »

30+程序员别只闷头写代码,信息力才是未来的核心竞争力

引言 如果你也是程序员,或许你会认为「技术思维」是一个颇为正面的词汇,比如思维缜密,逻辑清晰,不但能写出优雅、高效的代码,嗨能够把复杂的功能落地。 但在一些非技术人员眼中,技术思维却有一定的“贬义”,它代表了眼光过于聚焦于技术,考虑不全面,不理解用户需求。 技...
继续阅读 »

引言


如果你也是程序员,或许你会认为「技术思维」是一个颇为正面的词汇,比如思维缜密,逻辑清晰,不但能写出优雅、高效的代码,嗨能够把复杂的功能落地。


但在一些非技术人员眼中,技术思维却有一定的“贬义”,它代表了眼光过于聚焦于技术,考虑不全面,不理解用户需求。


技术思维对一个优秀的程序员不可或缺,但如果只注重自己“技术思维”的训练,职场的晋升道路可能不会那么一帆风顺,乃至于对理解这个世界,都会产生一些阻碍。


真正限制我们的,从来不是技术,而是视野。


今天谈谈「信息思维」。


pexels-omerderinyar-31983812.jpg


信息思维


一个常见的误解是,技术Leader应该是技术最强的才行,你得能解决团队开发中遇见的问题,还能设计出支持高并发大数据的架构。


如果你有心观察许多技术团队的Leader,你会发现他们也许并不是团队中技术最好的那一个,但一定是团队内掌握信息最全面的那一个。



  • 你有一个技术难题不知道如何设计技术方案,他可以告诉你业内主流的方案是什么,应该去调研那些框架或者中间件。

  • 你有一个业务问题需要咨询,他可以清晰的告诉你整个业务流程。

  • 你发现有一个调用其他产品的接口报错,他会告诉你找哪个团队哪个人来配合。


一名优秀的技术Leader,所负责工作的不再仅仅限于技术层面。他需要和不同专业的、不同部门、不同背景的人建立联系,比如负责上传下达的任务,负责不同部门的开发协调,他知道每个人都在干什么,你有个什么事儿,他知道应该去找谁。


或许你会说,我天生内向,不善于和别人打交道,所以你说的这些我都不擅长。你认为我只要关心技术就够了,但不在这里就在那里,了解这些信息会对你有帮助。


有研究表明,任何人只需要经过一定的培训或者引导,积极参与公司内部的事情,了解各个部门都在做什么,不同部门的人负责那些事情,那么他的晋升频率和绩效评分就会提高。


但之所以许多程序员没有这样的思维,是因为我们大脑有两个偏见。


大脑的两个偏见


第一个偏见是,我们会天然的高估「我们」的价值,你更在意自己的小圈子、你的同伴们的观点。


大多数程序员在开发的时候,关注的是技术方案怎么定,代码如何写的更优雅,而不是这个功能好不好用。你让程序员优化产品,他们大都只会在工程视角下优化产品,很少接触用户需求。


比如我见过的一个同事,在处理一些咨询类的问题时,经常抱怨客户和一些操作人员:为什么“这么简单的”东西都不会用,或者“这么清晰”的提示都看不明白?


或许从程序员角度看,很多问题是非常低级的问题,可是从客户或者使用人员的视角来说,功能就是不好用,提示就是不够清晰。


你可能也遇见过一种场景:家里的长辈让你帮忙设置微信或者某个APP的功能, 你可能下意识的就会说:“这个功能不就在这里吗,你怎么不会?”


可问题就在于,你或者身边朋友都是手机的高频用户。但家人不是,对你来说直觉的操作,对别人来说就是迷宫。


第二个偏见,是我们总喜欢「往上看」,我们更关注比自己社会经济地位更高的人。


我们更在意社会地位高的人的意见,即使他们并不具备你面临的一线问题或场景;我们倾向于忽略那些地位低的人的意见,哪怕他们掌握关键信息。


比如一次技术选型,你想用一套能够快速落地、运维简单的技术方案,而公司架构师推荐了一套更成熟完备的技术方案,这个方案适用在大业务量的场景,可你们段时间内业务量根本到不了这么高。


最后团队内部多次讨论,你的方案被否决了,大家都认为架构师的方案更优。结果团队成员吭哧吭哧干了半年,最后发现开发周期明显变长,运维成本翻倍,组里内部怨声载道,纷纷后悔当时没选你推荐的方案。


再举个例子,许多年轻人在投资的时候,喜欢关注财经博主分析的“热门领域”和“热门板块”,却不愿意花时间问问身边人的看法和意见。


GDP增速、产业升级、未来规划这些固然重要,但是你的日常收入、家庭储蓄、日常花销占比有多少,你家人一定比各种专家更了解你。


过去和程序员朋友们聊天,大家都有一种同样地感觉,就是做技术做久了,技术的确是在不断提升,但总感觉自己就是公司的螺丝钉,想要跳出这个平台,但发现如果离开这个平台,自己可能什么都不是。


技术思维给程序员带来的影响还在逐渐增大, 当引以为傲的技能逐渐被AI替代,我们需要训练自己的信息思维。


你可以想见克服这两个偏见并不容易,而这也是我们的机会。


pexels-merve-cakir-351055068-15002639.jpg


提供路径


我在刚工作的前几年,特别羡慕那些看起来天生就适合职场的人,工作中不但经常拿到成绩,还能快速晋升。我试图总结他们的经验,是他们认真负责,还是说善于沟通,甚至是擅长和领导搞好关系?


后来我发现,他们有一个关键特质是——好奇心。


比如你遇见程序框架的一个问题,有些人google一下,对着教程修改一下配置,调好了就不管了。有的人偏偏一步步debug源码,直到确定问题的根本原因在哪里。


当开发了一个功能和系统,有的人代码提交之后就不管了。而你就想知道需求是谁提出来的,他在现实中遇见了什么问题,我们的功能昨晚最后解决了吗?


94岁的巴菲特,为什么还能够保持思维的敏捷。凭什么?前一阵子在伯克希尔哈撒韦的股东大会上,他在回答一个13岁孩子提问时,巴菲特说出了自己的答案:好奇心。


他们真诚的关心别人都在想什么,在做什么,愿意接纳不同的视角。他们做的这些事情不是为了获得什么,只是单纯的想知道而已。


当然现在信息这么多,光有好奇还不够,还得思考的更深入一点,提问的更多一点。


如果你想克服大脑的这两个偏见,有以下几个经验供你参考——


首先理解信息是跟人连接在一起的。很多知识或者道理,不只是纸面上干巴巴的文字,而都是由现实中的人真实感受和体验。


由于我们的成长环境不同,因此每个人的视角和思维方式都不一样,有些道理你需要放在他那个具体的情境之中才能体会到那句话是什么意思。


1月份DeepSeek R1横空出世,我看网上说全世界的大模型团队都在研究R1的论文。我就去问了问一个做大模型的朋友有没有关注这块,当时临近过年,大家其实都已经在期待放假了。我朋友回答说:“反正领导已经开始研究了,要不然我也不会这么忙,给我平添烦恼。”


这种真实朋友给我的的反馈,让我立刻感受到DeepSeek R1带来的震撼有多么大。


培养对「一手信息」的敏感度


一手信息就是那些还未上网,来自一线的信息。你或许认为现在网络这么发达,想了解任何事情都可以从网上找到答案,并且AI能力突飞猛进,把信息交给AI,那么AI就可以帮我们做任何事。


但实际上这个世界上永远存在那些仅存在于一线,还未上网的「隐性知识」。书本会告诉你“是什么”和“为什么”,而实际接触一线,我们才知道很多事情是如何发生的。


刘润老师在最近的文章里分享了他进二十多年,坚持在做的一件事情就是「参访」。每年平均的出差时间有上百天,只要有机会就会去现场看一看,刘润老师在文章中描述的很清晰,在办公室看报告、打电话,和你去生产线走一走,和一线员工聊几句的差别是非常大的。


最有意思的一个描述是一家企业员工的眼神、办公室的氛围,甚至厕所干不干净,这些细节都会藏着关于公司的文化、管理的密码。


训练自己跨界翻译的能力。


跨界翻译你能不能把一个领域的信息,用另一个领域的语言表达出来,让别人能听懂。


也许在程序员和产品经理为了一个问题争论的面红耳赤的时候,你是否能有一种更好的方式,让产品经理能听懂工程师的技术方案是什么?是否能让工程师明白,真实的用户需求是什么?


比如有一次销售很着急的来找研发说:“我们有一个大客户说下周前必须研发出这个功能,不然就不验收!”


研发同事一听,满脸不屑地说:“你知不知道这个功能要重构整个接口?开发的工作量非常大,下周根本不可能。客户表述清楚了吗,还是你自己拍脑袋定的?”


销售和研发直接氛围一度陷入紧张,但是跨界翻译者可能会这么说:



销售最在意的就是验收,因为这决定了他是否能拿到这家客户的收入。研发在意的是技术稳定性,贸然增加新功能也是“挖坑”。


我们不妨看看客户为什么需要这个功能,我们系统有没有满足部分功能的方案先应急,而不是按照客户要求全部重新开发?



这需要大量的学习、消化和吸收,你只有见过和积累的足够多,才能够做到触类旁通,举一反三。


说在最后


真正限制我们的,从来不是技术,而是视野。信息思维并不是让你放弃技术,而是让你站得更高,看得更远,理解得更深。


你看到的世界有多宽,你未来的上限就有多高。


不知道这篇文章能否对你有所启发,又或者你能想到身边的哪个朋友可能会从中受益呢?


这是东东拿铁的第78篇原创文章,欢迎关注。


作者:东东拿铁
来源:juejin.cn/post/7503712510592385051
收起阅读 »

30岁了才有了自己的博客

写作是很痛苦说来惭愧,干程序员这么多年,30 岁了,终于想起来要自己搭建一个博客了!其实一直羡慕别人拥有炫酷的博客,可是自己却一直没去做。作为前端程序员,通常来说,搭建好看的博客还是非常容易的,不过因为自己没啥分享欲望,加上上学时对写作的厌烦(上学时,语文不及...
继续阅读 »

写作是很痛苦

说来惭愧,干程序员这么多年,30 岁了,终于想起来要自己搭建一个博客了!其实一直羡慕别人拥有炫酷的博客,可是自己却一直没去做。作为前端程序员,通常来说,搭建好看的博客还是非常容易的,不过因为自己没啥分享欲望,加上上学时对写作的厌烦(上学时,语文不及格的主要原因就是作文)。

为什么现在想起来搭建博客呢?

我发现,我虽然不喜欢写作,却喜欢记录,学习笔记、工作总结、快捷命令汇总等等。学习新技术时,我喜欢把教程或者书本上的语言总结成自己能看懂的语言记录起来,然后看看自己的记事本,这么多年来也有 600 多篇了。

因此,我觉得我不是不喜欢写东西,而是我内心对写作的的恐惧,也担心自己水平有限,文章不够深入,误人子弟。也是为什么我一直不敢把自己写的东西发布到网上。

[质问] 那么,为啥你现在敢发了,你水平更高了?还是你写作水平提升了??

[回答] 其实都不是!回想有一天,发现我多年前发布的第一篇文章,竟然现在都还有人收藏点赞,全文不过 500字,当时 md 语法都不知道,更别谈什么排版了。

这让我意识到一个问题:即便随手写的一篇文章,只要有完整的解决方案,都有可能帮助到别人

我掘金的第一篇文章。

当然,除了帮助别人,分享文章还有有什么好处呢?会获得点赞和评论;甚至会遇到一些志同道合的人加好友,一起聊技术细节。当然,对于淡泊名利的你来说,这些有什么用呢?这些东西都是正反馈的来源呀!!!

正反馈

说到正反馈,这真的是一个值得深入的话题。工作中,大多数时候的任务是重复和无聊的,你的正反馈来源多数是你的上级或者同事,可是这样的正反馈是很少的。生活中也是一样。也就导致对工作失去兴趣,对技术失去兴趣,因为没有正反馈,也就没有动力继续,不想做任何事情。所以我们需要主动去寻找一些能产生正反馈的事情,写文章、运动、学习,分享。都可以。(有段时间,我每天上班第一件事就是看看有没有新的点赞和评论。)

其实这些道理一早就知道,很多文章、前辈都有说过,可是呢,人呀,就是要自己亲身体会后才会明白!

开始写作

与其说是开始写作,不如说是开始记录,对我来说更加合适。我并不擅长写出段落清晰、用词优美的文章。所以我决定把写作当做生活中的记录,把本地的总结记录分享到线上论坛。

既然要分享到网上,也促使我要求自己发布的文章不仅是自己能看懂,尽量也能让所有人看懂。这很重要!!!

让所有人能看懂很重要,因为即便是自己写的文章,隔一段时间你回头看,会发现啥也看不懂了,根本回想不起来当时是怎么实现的。这就是因为当时写得不够清晰。

于是,我逐渐将本地的记录文章颁发到了论坛,并获得了一些收获。

当然,这点数据根本不够看。对我来说是一个开始,打破固有思维的开始!渐渐地,我从一年 5 篇,到一年 10 篇了,现在一个月一篇。发布的文章逐渐变多了... 这可并不是我强迫自己输出呀(不可能,我意志力不强),这本来就是我本地记录的频率。

这也是我的做事风格,把一些看似很难的事情变得平常化。例如记单词,地铁上就非常适合记单词,手机没网络,记完单词刚好就到公司了。

写本篇的时候,我发现又有一段时间没更新了,不是我没写,是写了很多,都还没发布,因为总觉得少点什么。

搭建博客

说了这么多,总算可以回归正题了~

我参考了 v 友们的建议想搭建自己的博客,求大佬们推荐方案 - V2EX,总结出以下常见的博客框架。

  • wordpress
  • hugo
  • hexo
  • Astro
  • typecho

不过,看了一圈,都不太适合我,要么是配置麻烦、要么主题不好看。相反,并不是这些博客不好,里面的插件、主题多得很,只是没有找到我想要的那种风格(喜欢折腾0.0),下面我会详细解释。

我喜欢的风格

之前我一直很长一段时间,我很喜欢那种酷炫的博客,例如,动态背景图(雪花、3D 地图),或者页面下方还有个可以通过鼠标挑逗的二次元卡通人。再就是搞一些炫酷跳动文字?大家应该都懂,哈哈。

而现在呢,我想要的博客就是:

”简约“

看了太多混杂广告、推广、链接的博客,我更想让读者专注在文章本身上。所以除了文章相关的内容,其他东西一律去除。

”简单“

博客要关注的是内容,所以发布流程越简单越好,我只需要关注写内容就好了。

结合这两点,并简单调研了下相关实现,还是决定自己搭建一个,比较往后较长一段时间都要用它,所以还是自己用着舒服为主。

页面展示

Github: github.com/chennlang/c…

PC 端

H5 端

样式参考了以下两个非常喜欢的博主。

NextJs 搭建博客

其实博客的本质,无非就是一个 markdown 的渲染器,对于前端来说,实现起来不算难事。所以我决定使用 NextJs 自己搭建一个,选用 Nextjs 的主要是因为它天然对 SSG (静态生成)的友好支持。

1.配置 nextConfig

通过如下配置,我们就可以导出一个静态web项目。

const nextConfig: NextConfig = {
/* config options here */
trailingSlash: true,
output: 'export',
};

2. 文章格式范式

我们将文章放在 posts 目录下,文章格式如下:

---
title: "时隔三年,一个可视化可交互的前端依赖分析工具发布了"
date: "2025-02-01"
categories: "Life"
summary: "我一直认为它只是一个不完美的半成品,所以我只是悄悄的上架到了个人的 github 仓库,并没打算公之于众。不过就在今天,我意外的发现仓库多了一个陌生人的 Issue,真的很震惊,我没想到真的有人安装文档安装使用了。这真的让我感到意外,也许真的有人觉得有用,那么我就要做好它,也有责任做好它!"
---


### 终于有勇气发布了~
我一直认为它只是一个不完美的半成品,所以我只是悄悄的上架到了个人的 github 仓库,并没打算公之于众。不过就在今天,我意外的发现仓库多了一个陌生人的 Issue,真的很震惊,我没想到真的有人安装文档安装使用了。这真的让我感到意外,也许真的有人觉得有用,那么我就要做好它,也有责任做好它!

3. 解析文章内容-核心逻辑

import { promises as fs } from "fs";
import path from "path";
import matter from "gray-matter";
import { remark } from "remark";
import html from "remark-html";

// 获取文章内容
export async function getPost(id: string) {
const markdownWithMeta = await fs.readFile(
path.join("posts", id + ".md"),
"utf-8"
);
// frontmatter 是文章的元数据信息, content 是文章内容
const { data: frontmatter, content } = matter(markdownWithMeta);

const processedContent = await remark().use(html).process(content);
const contentHtml = processedContent.toString();

frontmatter.categories = Array.isArray(frontmatter.categories) ? frontmatter.categories : [frontmatter.categories || "Default"]

return {
id,
contentHtml,
frontmatter,
}
}

4. 展示文章页面-核心逻辑

const Post = async ({ params }: any) => {
const { frontmatter, contentHtml } = await getPost(params.id);

return <div>
<h1 className="text-2xl mb-4 font-bold">{frontmatter.title}h1>
{/* 元数据 */}
<p className="flex flex-wrap">
{metaList.map((item) => (
<span key={item.label} className="text-gray-600 mr-4">
<label>{item.label}: label> {item.value}
span>
))}
p>
{/* 内容 */}
<div className="mt-4" dangerouslySetInnerHTML={{ __html: contentHtml }} />
<div className="mt-10">
<Comments />
div>
div>
};

展示就很简单了 dangerouslySetInnerHTML 将内容渲染在页面上就好了

5. 添加评论功能

因为是静态页面,所以就不存在数据库和server服务,所以最佳的方式是直接使用 giscus

利用 GitHub Discussions 实现的评论系统,让访客借助 GitHub 在你的网站上留下评论和反应吧!本项目深受 utterances 的启发。

首先我们到 giscus 上配置好我们的 github 项目,并在项目中开启 GitHub Discussions 功能。

所以本质上,所有评论最终都存在该仓库的 Discussions 里,每个文章对应一个 discussion。

对应关系可以自行设置,默认是文章的 url 路径对应作为 discussion 的标题,然后该文章的评论都是在这个 discussion 下。

然后新建一个 Comments 组件(配置需要根据自己的仓库自行修改)

"use client";

import Giscus from "@giscus/react";

export default function Comments() {
return (
<Giscus
repo="xx/xxx.github.io"
repoId="xxxx"
category="xxx"
categoryId="xxxx"
mapping="pathname"
strict="0"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
theme="preferred_color_scheme"
lang="zh-CN"
/>

);
}

注意:

  1. 本地 localhost 域名可能不会自动创建 discussion,发布到线上后就能解决。
  2. 自动创建Discussion的触发条件是:当用户首次在该页面发表评论或表情时才会创建。若未进行任何交互,Discussion不会自动生成。

Github Page 部署

既然输出是静态页面,可以托管的地方就很多了,前面我也提到了,我并不想做除了写内容以外的任何事,Github Page 就是很不错的选择,不仅能托管文章代码,还能配合流水线自动构建,一举两得。

  1. 创建仓库

新建一个 [用户名].github.io 的 Github 仓库

  1. 新建一个 nextjs 部署构建流水线 .github/workflows/nextjs.yml
# Sample workflow for building and deploying a Next.js site to GitHub Pages
name: Deploy Next.js site to Pages

on:
push:
branches: ["main"]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: 'npm'

- name: Install pnpm
run: npm install -g pnpm

- name: Detect package manager
id: detect-package-manager
run: |
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
echo "manager=yarn" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
echo "runner=yarn" >> $GITHUB_OUTPUT
exit 0
elif [ -f "${{ github.workspace }}/pnpm-lock.yaml" ]; then
echo "manager=pnpm" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
echo "runner=pnpm" >> $GITHUB_OUTPUT
exit 0
elif [ -f "${{ github.workspace }}/package.json" ]; then
echo "manager=npm" >> $GITHUB_OUTPUT
echo "command=install" >> $GITHUB_OUTPUT
echo "runner=npm" >> $GITHUB_OUTPUT
exit 0
else
echo "Unable to determine package manager"
exit 1
fi

- name: Install dependencies
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}

- name: Build with Next.js
run: ${{ steps.detect-package-manager.outputs.runner }} next build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./out

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

然后当 main 分支 push 时自动触发 NextJs 项目的构建,完成 GitHub Pages 页面部署。接下来你就可以通过 [usename].github.io 访问属于你自己的博客了!

总结

最后,如果有和我一样喜欢折腾的小伙伴,可以使用使用我的项目作为模版,搭建一个属于你自己的博客。当然,如果你觉得还不错的话,别忘了留下你的 star~ GitHub - chennlang/chennlang.github.io: blog


作者:Alang
来源:juejin.cn/post/7474923858165252105

收起阅读 »

iOS、Android、Flutter 流行框架对比(基于 OpenAI 的数据)

背景 基于 OpenAI 最新的 o1-preview 模型,整理出来的 App 分别使用 iOS、Android、Flutter 流行框架对比,方便查阅,部分数据可能不完全准确,整体的数据质量我看了一下还是挺高,有需要的自取。 内容 自动布局的框架 iOS...
继续阅读 »

背景


基于 OpenAI 最新的 o1-preview 模型,整理出来的 App 分别使用 iOS、Android、Flutter 流行框架对比,方便查阅,部分数据可能不完全准确,整体的数据质量我看了一下还是挺高,有需要的自取。



内容


自动布局的框架


iOS



Prompt: 以表格的形式输出 iOS 自动布局框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
Auto LayoutSwift/Objective-CiOS 原生的布局系统动态适应不同屏幕尺寸的布局N/A(原生功能)N/A
SnapKitSwift简化 Auto Layout 的 DSL使用链式语法创建布局约束SnapKit19k+
MasonryObjective-C类似 SnapKit 的 Objective-C 版本使用链式语法创建 Auto Layout 约束Masonry18k+
PureLayoutSwift/Objective-C提供一致的 Auto Layout 接口简化约束的创建和管理PureLayout7k+
CartographySwift使用纯 Swift 语法构建 Auto Layout通过操作符重载简化约束定义Cartography7k+
TinyConstraintsSwift轻量级的 Auto Layout 语法糖更简洁地创建和管理约束TinyConstraints3k+
SteviaSwift简洁的布局框架使用声明式语法定义布局Stevia3k+
EasyPeasySwift简化 Auto Layout 语法的库使用简洁的语法定义和修改约束EasyPeasy2k+
AnchorageSwift提供更简洁的 Auto Layout API利用 Swift 特性简化约束定义Anchorage1k+
LayoutKitSwift高性能的布局框架使用纯代码创建高性能的视图布局LayoutKit7k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Andorid



Porompt:以表格的形式输出 Andorid 自动布局框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
ConstraintLayoutJava/Kotlin官方提供的强大布局系统创建复杂布局,支持动画和约束N/A(原生功能)N/A
FlexboxLayoutJava/Kotlin类似于 CSS Flexbox 的布局库实现灵活的响应式布局FlexboxLayout6k+
MotionLayoutJava/Kotlin基于 ConstraintLayout 的动画布局系统创建复杂的过渡和动画N/A(原生功能)N/A
AndroidAutoLayoutJava自动适配各种屏幕尺寸的布局库根据屏幕尺寸自动调整布局参数AndroidAutoLayout10k+
Anko LayoutsKotlin使用 Kotlin DSL 构建布局简化布局代码,减少 XML 文件Anko15k+
FlowLayoutJava支持自动换行的布局适用于标签、按钮等需要自动换行的场景FlowLayout9k+
PercentLayoutJava/Kotlin支持百分比布局的库(已弃用)根据父布局的百分比设置子视图尺寸PercentLayoutN/A
EpoxyJava/KotlinAirbnb 开源的用于构建复杂列表的库声明式界面,支持复杂布局和模型Epoxy11k+
Gr0upieJava/Kotlin简化 RecyclerView 布局和适配器的库轻松创建复杂的列表布局Gr0upie12k+
DataBindingJava/Kotlin官方的数据绑定库将布局与数据源绑定,减少代码量N/A(原生功能)N/A

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Flutter



Porompt:以表格的形式输出 Flutter 自动布局框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
Flutter 内置布局系统DartFlutter 自带的布局系统,包含多种布局组件提供灵活的布局,支持多种布局方式N/A(原生功能)N/A
Responsive FrameworkDart响应式布局框架自动适配不同屏幕尺寸和分辨率Responsive Framework2k+
flutter_layout_gridDart类似于 CSS Grid 的布局库支持基于网格的布局方式flutter_layout_grid1k+
flutter_staggered_grid_viewDart创建不规则网格布局的库支持瀑布流布局和不规则网格布局flutter_staggered_grid_view2.5k+
AutoSizeTextDart自动调整文本大小的组件根据可用空间自动调整文本大小AutoSizeText1.5k+
flutter_screenutilDart屏幕适配解决方案支持多屏幕尺寸、像素密度的适配flutter_screenutil5k+
SizerDart使响应式设计变得简单的库根据屏幕尺寸调整组件和字体大小Sizer600+
flutter_responsiveDart响应式布局助手简化不同屏幕尺寸的布局适配flutter_responsive300+
align_positionedDart更灵活的 Positioned 小部件提供百分比定位和对齐align_positioned200+
FlowBuilderDart用于构建响应式和可组合的布局提供灵活的流式布局FlowBuilder1k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


网络请求的框架


iOS



Prompt: 以表格的形式输出 iOS 网络请求框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
AlamofireSwift强大的网络请求库,简化 HTTP 网络请求支持链式调用、文件上传下载、认证处理Alamofire40k+
AFNetworkingObjective-C功能强大的网络请求库,支持多种网络请求支持数据任务、上传、下载、认证处理AFNetworking33k+
SwiftyJSONSwift简单的 JSON 解析库轻松解析和处理网络请求中的 JSON 数据SwiftyJSON22k+
KingfisherSwift强大的图片下载和缓存库支持异步图片下载和缓存,集成到网络请求Kingfisher22k+
PromiseKitSwift处理异步操作的 Promise 库简化异步网络请求、支持链式调用PromiseKit14k+
MoyaSwift基于 Alamofire 的网络请求抽象层通过 API 定义和管理网络请求,插件支持Moya15k+
Reachability.swiftSwift用于监控网络连接状态的库支持监控网络状态,处理连接状态的变化Reachability.swift12k+
URLSessionSwiftiOS 原生网络请求 API完全支持 HTTP 和 HTTPS 请求N/A (原生功能)N/A
SiestaSwift高级网络请求框架,支持缓存和状态管理支持响应式编程、数据缓存、重试机制Siesta3.5k+
NetworkingSwift网络请求简化库,支持上传下载和缓存文件上传下载、自动管理缓存Networking1.1k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Andorid



Prompt: 以表格的形式输出 Andorid 网络请求框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
RetrofitJava/Kotlin类型安全的 HTTP 客户端,用于 Android 和 Java简化 RESTful API 的网络请求,支持异步和同步Retrofit42k+
OkHttpJava/Kotlin高效的 HTTP 和 HTTP/2 客户端支持连接池、拦截器和超时设置等高级特性OkHttp43k+
RxJavaJava/Kotlin用于异步和事件驱动编程的响应式扩展库支持异步数据流处理,可与网络请求结合使用RxJava47k+
KtorKotlinKotlin 的异步网络框架,支持客户端和服务器端支持多种协议,异步编程,协程支持Ktor12k+
AsyncHttpClientJava异步的 HTTP 客户端支持异步网络请求、文件上传下载AsyncHttpClient10k+
VolleyJavaGoogle 提供的网络请求库支持异步请求、图片加载和缓存内置于 Android(无需单独的 GitHub 仓库)N/A
FuelKotlin轻量级的网络请求库简化 HTTP 请求,支持并发和协程Fuel4k+
IonJavaAndroid 上的异步网络请求和图片加载库支持异步请求、图片加载、缓存Ion7k+
FastAndroidNetworkingJava基于 OkHttp 的高性能网络库支持文件上传下载、JSON 解析、缓存FastAndroidNetworking5k+
Apollo AndroidJava/Kotlin用于 Android 的 GraphQL 客户端支持类型安全的 GraphQL 请求和响应处理Apollo Android5k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 9 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Flutter



Prompt: 以表格的形式输出 Flutter 网络请求框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
httpDartFlutter 和 Dart 的基本 HTTP 客户端简单的网络请求支持,GET/POST 等方法http1.5k+
DioDart功能强大的 HTTP 客户端库支持拦截器、全局配置、FormData、请求取消Dio13k+
ChopperDartRESTful API 客户端库,类似 Retrofit支持代码生成、拦截器、内置 JSON 解析Chopper1.6k+
Retrofit.dartDartRetrofit 的 Dart 实现自动生成 API 调用代码,支持 RxDartretrofit.dart1.8k+
graphql_flutterDartGraphQL 客户端库支持查询、变更、订阅和缓存graphql_flutter3.5k+
Flutter Socket.IODartSocket.IO 的 Flutter 客户端实现实时通信,支持 WebSocketsocket_io_client600+
Flutter Secure StorageDart安全的密钥/值存储库用于存储令牌和敏感信息flutter_secure_storage1.7k+
Flutter DataDart数据管理和网络请求框架支持关联、缓存、离线支持flutter_data700+
GetConnectDartGetX 框架内置的网络请求库简化网络请求,集成状态管理GetX9k+
FlutterFireDartFirebase 的 Flutter 插件支持实时数据库、身份验证、云功能等FlutterFire7k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


图片加载的框架


iOS



Prompt: 以表格的形式输出 iOS 网络图片加载框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
SDWebImageObjective-C/Swift高性能的异步图片加载和缓存框架支持 GIF、WebP、APNG、缩略图等格式SDWebImage24k+
KingfisherSwift纯 Swift 实现的轻量级图片加载框架支持异步下载、缓存和处理图片Kingfisher21k+
NukeSwift功能丰富的图片加载和缓存框架支持异步加载、缓存、预加载等功能Nuke7k+
PINRemoteImageObjective-C/Swift高性能的图片加载和缓存库支持异步下载、缓存、支持 WebP 格式PINRemoteImage6k+
AlamofireImageSwift基于 Alamofire 的图片组件库支持图像过滤、缓存和下载AlamofireImage4k+
YYWebImageObjective-C高性能的异步图片加载和缓存框架支持 GIF、APNG、WebP、缩略图等格式YYWebImage4k+
HanekeSwiftSwift轻量级的缓存库,支持图片缓存支持内存和磁盘缓存,自动缩放图片HanekeSwift5k+
SDWebImageSVGKit PluginObjective-C/SwiftSDWebImage 的 SVG 支持插件支持加载和显示 SVG 格式图片SDWebImageSVGCoder300+
UIImageView+AFNetworkingObjective-CAFNetworking 的 UIImageView 类别为 UIImageView 提供异步图片加载UIImageView+AFNetworking33k+ (AFNetworking)
ImageLoaderSwiftSwift简单的图片加载和缓存库支持异步加载、缓存和占位图ImageLoaderSwift1k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Andorid



Prompt: 以表格的形式输出 Andorid 网络图片加载框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
GlideJava/Kotlin高效的图片加载和缓存库支持异步加载、内存和磁盘缓存、GIF 支持Glide34k+
PicassoJava简单强大的图片下载和缓存库处理图片加载、缓存,支持转换和裁剪Picasso19k+
FrescoJavaFacebook 开源的图片库支持大图加载、GIF、WebP、渐进式加载Fresco18k+
CoilKotlin基于 Kotlin 的现代图片加载库支持异步加载、内存和磁盘缓存、动画Coil9k+
Universal Image LoaderJava过时但曾经流行的图片加载库支持异步加载和缓存Universal Image Loader16k+
Glide TransformationsJavaGlide 的图像转换工具集合提供多种图像转换效果Glide Transformations10k+
Picasso TransformationsJavaPicasso 的图像转换工具集合提供多种图像转换效果Picasso Transformations5k+
AndroidSVGJava支持加载 SVG 矢量图的库解析和渲染 SVG 文件AndroidSVG2k+
Subsampling Scale Image ViewJava支持显示超大图片的控件支持平滑缩放和拖动大尺寸图像Subsampling Scale Image View8k+
ImageLoaderJava轻量级的图片加载库支持异步加载和缓存ImageLoader16k+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 10 月。如需获取最新信息,请访问相应的 GitHub 仓库。


Flutter



Prompt: 以表格的形式输出 Flutter 网络图片加载框架,包括使用的编程语言、描述、功能、GitHub 仓库地址和 Star 数量



框架/库名称编程语言描述功能GitHub 仓库地址Star 数量
cached_network_imageDartFlutter 的缓存网络图片加载库支持图片缓存、占位符、错误图片显示等cached_network_image5k+
flutter_svgDartSVG 矢量图支持库加载和渲染 SVG 格式的矢量图flutter_svg3k+
photo_viewDart图片查看组件支持图片缩放、平移,适用于图片预览photo_view1.6k+
extended_imageDart功能丰富的图片加载和处理库支持缓存、编辑、裁剪、旋转等高级功能extended_image1.5k+
flutter_advanced_networkimageDart高级网络图片加载库支持缓存、加载进度指示、重试机制等flutter_advanced_networkimage800+
octo_imageDart提供多种占位符和淡入效果的图片加载库支持占位符、淡入淡出动画、错误重试octo_image500+
flutter_blurhashDart显示模糊占位符的库支持使用 BlurHash 占位符,提高加载体验flutter_blurhash500+
progressive_imageDart渐进式图片加载库支持先加载低分辨率图像,后加载高清图像progressive_image200+
flutter_lubanDart图片压缩库实现了鲁班算法的图片压缩flutter_luban500+
flutter_advanced_networkimageDart高级网络图片加载库支持缓存、加载进度指示、重试机制等flutter_advanced_networkimage800+

请注意,Star 数量可能会随着时间变化,以上数据截至 2023 年 9 月。如需获取最新信息,请访问相应的 GitHub 仓库。


数据洞察


针对文档内容,我将分别为 iOS、Android 和 Flutter 提供一些有价值的观点,基于框架的特性、开发趋势及其在各自生态系统中的重要性。


iOS 平台的分析



  1. 自动布局系统(Auto Layout 与替代方案) :



    1. Auto Layout 是 iOS 的原生布局系统,它的动态布局功能使得 iOS 应用能够很好地适配各种设备的屏幕尺寸。随着新设备的推出,开发者需要确保应用的界面能在不同屏幕上保持一致,Auto Layout 提供了很好的适配解决方案。

    2. 但是,由于 Auto Layout 语法相对繁琐,开发者更倾向于使用第三方库来简化布局创建,例如 SnapKit(19k+ stars)和 Masonry(18k+ stars)。这些库通过链式语法让布局的书写更具可读性,提升了开发效率。



  2. 网络请求的主流选择:



    1. Alamofire(40k+ stars)是 iOS 开发中最受欢迎的网络请求框架,它简化了 HTTP 请求,支持文件上传、下载及认证处理。它的链式语法大大提高了网络请求的简洁性和可读性,因此广泛应用于项目中。

    2. 对于 Objective-C 开发者,AFNetworking(33k+ stars)依然是稳固的选择,尽管 Swift 逐渐成为主流编程语言,AFNetworking 仍在老项目维护和过渡期间使用。



  3. 图片加载和缓存技术:



    1. SDWebImage(24k+ stars)和 Kingfisher(21k+ stars)是 iOS 中最常用的异步图片加载和缓存库。两者支持图片的异步下载、缓存和格式处理,尤其适合优化应用的性能并提升用户体验。




Android 平台的分析



  1. 布局系统的灵活性:



    1. Android 的官方布局工具 ConstraintLayout 是目前最强大的布局系统,支持复杂的布局和动画。它结合了 MotionLayout 等工具,让开发者能够轻松实现 UI 动效,这一点在高交互性应用中至关重要。

    2. 另一个重要的布局库是 FlexboxLayout,它借鉴了前端开发中 CSS 的布局方式,使得 Android 开发者可以使用响应式设计来适配多种屏幕尺寸和分辨率。



  2. 网络请求的多样化选择:



    1. Retrofit(42k+ stars)和 OkHttp(43k+ stars)是 Android 社区中最受欢迎的网络请求库。Retrofit 简化了 API 定义和请求处理,尤其适合 RESTful API 的集成;而 OkHttp 则因其高效的 HTTP/HTTP2 支持和先进的特性如连接池、超时机制等,成为高性能应用的首选。



  3. 图片加载技术:



    1. Glide(34k+ stars)和 Picasso(19k+ stars)是 Android 开发者的常用工具。Glide 提供了更强大的缓存管理和内存优化能力,适合对性能要求较高的应用,而 Picasso 则以简单的 API 接口著称。




Flutter 平台的分析



  1. 响应式布局的优势:



    1. Flutter 的内置布局系统提供了丰富的布局组件,可以轻松实现不同屏幕的适配。结合 Responsive Framework(2k+ stars)等框架,开发者能够快速构建适应各种屏幕尺寸的应用,确保跨平台应用的 UI 一致性。



  2. Dart 语言和网络请求框架:



    1. Dio(13k+ stars)是 Flutter 社区的主流 HTTP 客户端库,支持全局配置、拦截器、请求取消等高级特性,非常适合复杂网络请求场景。其功能与 Android 的 Retrofit 类似,但通过 Dart 实现,与 Flutter 的生态系统紧密结合。



  3. 图片加载的灵活性:



    1. cached_network_image(5k+ stars)是 Flutter 中最受欢迎的图片加载库,支持缓存和占位符功能,这在跨平台应用中有助于提升用户体验,尤其是在网络环境不稳定的情况下。




总结



  • iOS 开发者在布局时普遍依赖 Auto Layout,但为了简化布局代码,越来越多开发者选择 SnapKitMasonry 等第三方库。网络请求和图片加载方面,AlamofireSDWebImage 占据主导地位。

  • Android 开发在布局上具有更多选择,尤其是 ConstraintLayoutFlexboxLayout,这两者提供了极大的灵活性。网络请求方面,RetrofitOkHttp 继续主导市场,而 Glide 则成为处理图片的首选库。

  • Flutter 由于其跨平台特性,布局和网络请求框架都高度依赖于 Dart。Diocached_network_image 是最受开发者青睐的选择,特别是在需要处理多屏幕适配和网络资源加载的场景中。


通过这些框架的对比,可以更好地理解不同平台开发的趋势和工具的选择,为优化项目开发流程提供有力支持。


作者:wangruofeng
来源:juejin.cn/post/7430770689684865078
收起阅读 »

我在成都教人用Flutter写TDD(上)——为啥要搞TDD?

哈喽,我是老刘 写这篇文章的时候刚回到北京,之前的一周去成都帮助一家公司完成基于Flutter的TDD流程的搭建。 这个工作一半是敏捷教练,一半是Flutter相关的技术顾问。 起因是这个客户接了一份欧洲那边的开发项目,但是欧洲客户对项目流程的要求比较高,要求...
继续阅读 »

哈喽,我是老刘


写这篇文章的时候刚回到北京,之前的一周去成都帮助一家公司完成基于Flutter的TDD流程的搭建。

这个工作一半是敏捷教练,一半是Flutter相关的技术顾问。

起因是这个客户接了一份欧洲那边的开发项目,但是欧洲客户对项目流程的要求比较高,要求开发团队采用TDD流程。

这是老刘第二次碰到要求开发采用TDD的情况,而且都是欧美客户。

为啥欧美软件开发团队对TDD这样的敏捷开发趋之若鹜,而大多数国内团队却鲜少能真正搞起来敏捷开发?

是我们比欧美开发者更务实还是敏捷开发确有其独到之处?

老刘希望用这篇文章尝试解答一下。

image.png
我记得刚去成都的时候大家一起吃饭,讨论起啥样的项目适合TDD,TDD会不会浪费额外的时间这些话题。

当时吃饭只是简单的聊了聊,但是让我意识到一个问题,TDD或者说敏捷开发在国内之所以一直没有真正成为主流更多的可能是意识问题。

看不到明确的收益,却能看到明确的额外工作量,怪不得众多管理者不愿意尝试。

这篇文章会回顾一下我的敏捷开发相关的经历,并借此尝试回答TDD究竟能给我们的开发带来什么收益。


初试敏捷:网络安全领域的尝试


老刘我带着团队使用Flutter进行App开发已经6年多了,而搞TDD的时间更长一些。

大约10多年前我还没有做客户端开发的时候,那时候我是做网络安全方面开发的,使用的是C语言。

那时候公司希望在敏捷开发方面做一些尝试,因为我在研究生期间做过一些敏捷方面的工作,所以就承担起来这个任务的研发部分。

当时公司希望尝试的是Scrum的完整流程,但是因为涉及到的团队和部门有点多,而很多团队对这个流程是持保留意见的,所以最终就没有搞成。

image.png
但是研发内部的单元测试和TDD流程因为只涉及开发团队,最终得以保留,这成为了我们探索敏捷开发的重要起点。


TDD的实践与收获


虽然最终Scrum没有成功搞起来,谈不上升职加薪 ┭┮﹏┭┮

不过在TDD的探索方面确实有很多的收获:


1、实战经验的积累


这是我第一次在百万级代码量以及几十人的项目中实施TDD流程。

相当于把以前只能算是纸上谈兵的认知变成了真正的实战经验。


2、踩坑与成长


在整个的实践过程中,我们几乎踩遍了所有可能的“坑”。

要知道防火墙项目的规模和复杂度相对于一般客户端或者服务端项目要高很多,特别是还有很多底层数据的操作和对系统内核的修改定制。

例如,我们的代码需要在MIPS架构的CPU上运行,而MIPS与常见的x86架构在字节序上存在差异。这种差异导致我们在PC端进行单元测试时,经常遇到与设备上运行时不同的错误。

image.png
这件事直接导致了后来我们对整个代码进行重构,把我们的业务逻辑和cpu架构解耦。

这是我第一次如此直观且深刻的看到架构到底是什么,也让我在后续的开发过程中开始有意识的注意架构设计的合理性。


3、TDD与架构合理性


在解决TDD实践中遇到的各种问题时,我们逐渐意识到,超过一半的问题实际上源于不合理的架构设计。

以之前提到的CPU字节序问题为例,一个合理的架构设计应该将底层数据操作与上层业务逻辑解耦。

理论上,我们都明白低耦合、高内聚的重要性,但在实际操作中,很难把握到何种程度才算真正实现。

而单元测试在这里就扮演了一个标尺的角色。

它通过测试的难易程度和可行性,帮助我们检验架构设计的合理性。

如果某个部分难以测试或者无法测试,这往往意味着架构存在问题。

通过这种方式,TDD不仅促进了代码质量的提升,也推动了架构设计的不断优化。

对TDD的推行加深了我们对架构合理性的理解,也让我们在后续的项目中能够更加注重架构的合理设计。


4、TDD是思维模式的转变


自己用TDD方式写代码和组织大家一起写又是一种不同的经验。

TDD本质上不是一个编码流程,而是一个设计流程。

在工作中去观察那些真正的资深程序员和新手的差别就会发现,资深程序员是在做脑力劳动,新手是在做体力劳动。
image.png
资深程序员通常是把一个功能点的逻辑链条想清楚才开始动手写,而新手却往往急于动手,发现问题后才不得不对代码做大幅的修改。

所以他们不是在加班写代码就是在加班改bug,然后就把工时卷上去了。

TDD为普通程序员提供了一条追赶资深程序员的捷径。

通过先编写单元测试,再通过测试驱动来编写正式代码,TDD迫使开发者在编码之前深入思考功能的逻辑链条。

image.png
这种拆解测试例的过程实际上是在进行设计工作,它要求开发者在动手之前就想清楚功能点的代码设计。

即使最初的设计或思考存在问题,TDD的重构步骤也会促使开发者及时发现并解决问题,减少定位问题的成本。

而且,所有的重构和修改都是在测试代码的保护下进行的,这样可以确保修改不会对现有代码造成意外的影响。

因此,TDD不仅仅是工作顺序的改变,它是一种完全不同的思维模型。

这种思维模式的转变对于提升整个团队的开发效率和代码质量都具有深远的影响。

(关于这一点我会在后面的经历中更详细的说明。)


TDD在客户端开发中的尝试


后来我开始转做Android客户端方向的开发工作。

我尝试着在客户端开发中使用TDD流程。

刚开始我以为这会是一件非常简单的事情,因为Android开发是基于Java体系的,而Java生态中有大量单元测试框架可供挑选。

万万没想到的是在Android工程中执行单元测试每次都需要等待漫长的编译过程。

基本上可以理解为执行一个单元测试就需要把整个工程编译一遍。

虽然可以通过Robolectric等库脱离对Android环境和SDK的依赖,但实际效果仍然很不理想。

要知道TDD的核心在于小步快跑,如果每个测试例都需要编译几分钟然后运行,就完全背离了TDD的出发点了。

所以这次在客户端开发中对TDD的尝试以失败告终。


Flutter带来全新的可能


大约六年前,我带领着Android开发团队,我们当时面临着一个挑战:如何解决Android和iOS两个客户端在用户体验上的差异问题,同时还需要摆脱原生项目中历史代码的泥潭。

在寻找跨平台开发框架的过程中,Flutter以其技术优势脱颖而出,但其年轻和缺乏有说服力的案例让我犹豫不决。

最终是Flutter对单元测试的良好支持让我最终下定了决心。

在此之前,我已经放弃了在手机端开发中实施TDD的想法,因为原生单元测试的体验实在令人沮丧。

但当我尝试了Flutter的单元测试后,我意识到TDD在移动开发中还能再抢救一下。

image.png
Flutter的单元测试体验可以用“舒适”来形容。

测试的运行速度达到了秒级,并且在测试场景下对UI组件的支持也非常出色。

这使得TDD从看似不可能的任务变成了一个顺畅且自然的过程。

Flutter的设计考虑到了TDD的场景,并且不仅仅是在小规模项目中,即使在大规模工程中也能实施TDD,这为移动应用开发带来了全新的可能性。


但是TDD在Flutter上的实施过程也不是一帆风顺的,主要挑战来源于两个方面:

首先,团队成员对Flutter的掌握程度不足。

由于团队中的小伙伴们都是Flutter的初学者,他们在对Flutter本身还不够熟悉的情况下尝试执行TDD,遇到了不少技术上的挑战。

其次,长期从事客户端开发的资深程序员在切换到新的开发流程时,似乎比新手更加困难。

这些资深程序员由于多年养成的开发习惯,很难立即适应TDD的模式。

他们习惯于传统的开发流程,对于TDD这种先编写测试用例再编写代码的方式感到不适应。

这种习惯的转变需要时间和实践,才能逐渐适应并掌握TDD的精髓。


面对这些挑战,我们采取了多种措施来促进TDD的实施。例如,组织定期的培训和研讨会,帮助团队成员加深对Flutter的理解,并通过实际案例来演示TDD的实践方法。同时,我们也鼓励资深程序员和新手之间的交流和合作,通过分享经验和教训,共同克服实施TDD过程中的困难。


基于Flutter的TDD带来的改变


经过半年多的学习、尝试以及代码架构的调整,我们从项目数据上看到了一些明显的变化。

首先解释一下之所以用了超过半年的时间,主要是因为我们的项目采用Flutter + 原生的混合开发模式。

刚开始的时候主要以原生代码为主,Flutter用于开发少数不重要的页面进行测试。

验证了Flutter的用户体验和稳定性后,Flutter页面的比例才开始逐步上升,然后Flutter才变为日常开发的主导。

随着Flutter变成日常开发的主要选择,可以看到几个明显的变化。


1、开发效率提升60%


这其中一半是Flutter本身优秀跨平台能力带来的。

只要不涉及原生功能,Flutter基本可以完全覆盖所有需要编写的代码。

从性能和用户体验来说Flutter页面也完全能胜任代替原生。

另外一半效率的提升则来源于TDD。

这主要体现在下面的几点。


2、提交测试后bug减少70%


这个数据其实从不同的维度统计会有不同的结果,我个人倾向于减少的bug比例会更高一些。

举个例子,产品需求写的可能是从家里坐车到全聚德烤鸭店。

开发人员要实现的是走3米到家门口——开门——走5米到电梯——按电梯哪个按钮——出电梯左转——走5米后右转出单元门……

这还只是正常情况,没有考虑如果电梯坏了怎么处理,路上有人放了东西怎么绕开。

开发工作的本质其实就是把所有正常的、异常的可能情况都进行处理。

这个过程中主要会出两种问题:




  • 有些场景没有考虑到,比如没想到路上被堆了箱子需要绕路。

  • 处理流程不达预期,比如考虑了绕路,但是绕路的流程不对,走不到单元门。



我们正常的瀑布流程其实就是开发人员先思考一下整个流程和都有哪些可能情况,这一步是设计阶段,更精细一些的可能还会区分概要设计和详细设计。

然后就是用代码实现这个流程,并且补充每一个细节,这一步就是编码阶段了。

开发完成后研发人员会进行简单的测试,比如验证按照自己的代码能不能走到全聚德。

如果能走到就会认为功能正常把软件交给测试人员进行更详细的测试。

测试人员会测试所有能想到的可能场景,发现某些场景走不到就给开发人员提bug。

image.png


按照这个流程写代码,出现bug是再正常不过的事情了。

主要有几个原因:




  1. 现实情况纷繁复杂,总会有一开始没有预料到的情况发生。甚至有些情况测试同学也没有预料到,只有APP上线了用户使用中发现了才会反馈回来。

  2. 即使有很严格的概要设计、详细设计流程,其精细程度也远远做不到真实代码的精细度。而设计过程越粗放,编码过程中遗漏、出错的概率就越高。

  3. 研发自己测试代码功能相对来说覆盖范围比较小,有些功能自己感觉实现的没问题,但是又没有测试到,只能在测试阶段由测试人员发现。



上面几种情况中第一种其实是无法避免的,而后面两种TDD都可以帮助开发人员最大幅度的降低发生的概率。

前面说了TDD是一个设计流程,它本质上代替的是概要设计和详细设计。

image.png
我们通过把一个功能需求拆分成不同的任务,把一个任务拆分成多个很具体的测试例来进行代码功能的拆分设计。

这种设计精细到什么程度呢?

每一行功能逻辑的代码都有对应的测试例,因为每一行功能代码都是测试例驱动下编写的。

而且TDD从流程上要求先写测试代码,这就强制开发者必须先进行设计层面的思考,然后才能开始编码。

这进一步避免了瀑布流程中省略或者敷衍设计流程,直接进行编码的情况。


接下来说开发者自测覆盖范围有限的问题。

基于TDD编写的代码每一行都是先有测试代码的,可以说每一行功能代码都是测试覆盖的。

可以在大概率上保证功能代码的运行结果和我们预期是一致的。

那种我写了很多代码,一运行结果和我想的完全不一样的情况在TDD中很难出现。


所以最终效果就是使用TDD流程后,我们团队提交测试的项目,总的bug数量大幅降低,测试周期和产品发布周期的稳定也得以保证。


3、交付健壮、可修改的代码


不知道各位看到这里的同学有多少是正在996的。

加班这种事在国内的软件开发领域很常见,这里面当然有即使不忙也要把工时耗够这种无效内卷的加班文化的原因。

但是也确实有很大一部分比例是因为项目进度不达预期导致开发人员不得不加班加点赶进度。

为什么项目总是面临延期的风险呢?

排除了那些管理混乱总是临时插入新需求的团队,瀑布流程本身对项目进度掌控力不足才是罪魁祸首。

做过几年开发的同学可能都碰到过这种情况:

项目刚开始的时候时间并不紧张,开发进度推进到中期发现原本以为很简单的一个对现有代码模块的修改远比想象中复杂。

要么是那坨代码和众多地方耦合,牵一发动全身,根本没法简单修改。

要么是那坨代码根本看不懂,写它的大神早已离职或者被当作大动脉裁掉了。

image.png
你薅秃仅剩的头发,熬夜加班把这部分代码改完,然后发现不是功能不好用就是原先工作正常的模块出问题了。

然后原本正常上下班的项目迭代周期就变成了996。

这种情况反复的发生,于是开发者在预估项目时间时就会增加很多的冗余时间来应对。

项目管理者知道你会预留冗余的时间,要么压缩项目进度,要么时不时的插入临时功能。

最终无效内卷的死循环达成了。


那么TDD能解决这个问题吗?

答案是可以,而且TDD能将这种情况发生的概率降到最低。

其实我们看前面说的场景,本质上就是两个问题,对现有代码“看不懂、改不动”。

先说“看不懂”的问题

TDD中业务代码都是由测试代码驱动生产的。

所以测试例和测试代码本身就是对业务代码最好的说明。

而且这种说明是站在业务代码的使用者角度,会向你展示业务代码是如何被使用的以及各种不同情况下预期的结果是什么。

另一方面,要想实现所有业务代码都基于测试代码产生,测试例的顺序必须是层层递进的。

所以基于测试例的前后顺序也很容易把握业务逻辑的脉络。


再来说“改不动”

当我们面对一团乱麻的代码,即使这份代码就是自己写的,想在上面做些修改也绝非易事。

究其根本还是当初写代码的时候只顾着实现功能,没有腾出手来对代码的合理性做一些优化。

这其实和一个程序员的能力水平关系不大。

我们的大脑本质上是单核cpu,无法同时干两件同类的事情。

比如你没办法一边和人说话一边写邮件。

同样的道理,你也没办法一边思考业务逻辑如何实现一边思考代码结构如何优化。

敏捷开发的先行者们对这种情况有很清晰的认知,所以敏捷开发中队这种情况也有不少很好的应对手段。

而TDD就是其中很有效的一种。

TDD的小步快跑最后一步就是重构。

image.png
它不要求你实现业务逻辑时就把代码结构一并调整完善,而是把重构这个动作放在了实现业务逻辑之后。

这样就可以保证每个步骤只专心的完成一件事。

写测试代码的时候就专心思考业务代码应该提供什么样的功能。

写业务代码的时候就专心思考具体的技术细节如何实现。

重构的时候就专心思考代码结构怎么样更合理更健壮。


把重构作为一个独立步骤的第二个好处是会强制你进行重构的思考。

其实大部分程序员都是有意愿去优化代码结构的。

但是如果碰到项目进度紧急或者代码结构看起来很简单清晰等情况,人的潜意识就会让我们跳过重构这个动作。

这是人脑降低功耗的一种本能行为。

TDD的好处是把这个动作流程化、标准化。走到这一步,不管代码看起来是不是很简单,你都会去想一想有没有可以重构的地方。

即使最后真的没啥可以改动的,你也尽最大可能做到了代码优化。

而且从实际经验来看,很多时候即使看起来很简单的代码,当你专门去思考重构的时候还是会有一些值得修改的地方。


TDD给重构带来的第三个好处是你的重构是在测试代码保护下进行的。

实际工作中开发人员有时候排斥重构一个重要的原因是责任问题。

比如原先代码虽然混乱,但是能正常工作,你重构了结果出现bug,在很多团队里你就要背这个锅。

TDD的特点是大部分代码都是在测试例的覆盖下。

你的重构是否会对现有代码逻辑造成影响跑一遍测试就知道了。

所以我们重构的时候也不用担心把原先好用的功能搞坏了。


同时这种所有代码都在测试保护下的特性,也解决了“改不动”的另一个原因:代码副作用。

这里说的代码副作用是指当我们为了开发新功能对原有代码进行修改时,影响原有功能不能正常运转。

这一点有过团队开发经验的同学应该深有体会。

新功能提交测试了,收到一堆bug都是影响了原先正常的功能。

当然这本质上还是架构不合理、代码混乱的原因。

但是在TDD的场景下,你要修改的代码都在测试代码的保护下,即使架构不合理也不用担心把原先的逻辑改坏了。


4、Flutter与原生代码对比


我们团队使用Flutter一年多之后,已经有几十个页面由原生迁移到Flutter上。

因为在Flutter端采用了TDD流程,对比这些页面的原生代码和Flutter代码,可以明显看出两者的不同。

Flutter代码更为整洁、清晰。

在Flutter代码中几乎找不到大段的重复代码,基本都在重构阶段被消灭了。

而Flutter代码中除了UI布局和底层的三方库封装部分,其他代码都对应的测试覆盖。

直接体现在开发过程中就是当我们同时有Flutter和原生的新需求时,Flutter投入一个人,原生两端各投入一个人,都是1周开发时间。

Flutter提测后bug数量比原生少了70%以上,三天就测试完成可以交付。

而原生端前前后后改了一周bug才勉强达到交付标准。

那段时间原生iOS端的开发同学每天晚上加班改bug到10点多(那个模块iOS端的历史代码太混乱,经常一不小心就把原先的代码改出问题)。

所以不同流程下开发效率的差距可见一斑。

而且这种差距会随着项目迭代,代码量的累积和越来越大。


总结


六年后再来看,我们的App大多数页面都已经切换到Flutter版本。

前段时间领导找我谈心好几次,都是因为我们客户端团队平均工时太少。

可是我们对比另一个产品没有使用Flutter的客户端团队,规模差不多的项目,我们的人手是他们的一半。我们交付的稳定性、项目周期可控性、bug率等指标都远好于他们。

应该说TDD充分证明了在这种长期迭代的项目中的可靠性和项目收益。


那么回到文章最开始的问题,在短期项目中是否值得使用TDD呢?

我的观点是只要你的项目不是那种写完代码不需要改bug就交付的类型,那TDD一定能带来正收益。

这几年我们也做了几个临时性的小App,都是不需要后续长期维护的那种。

我们全部沿用TDD流程,结果也如预料,项目的测试周期、交付质量和我们的主项目基本一致,远高于以前的原生项目。

所以真的不要再觉得写测试代码是花了额外的时间,这些时间本来就是用于进行设计的。

你只不过是换了一种概要设计和详细设计的方式。

而得到的收益就是你的项目从架构到代码细节的质量再到后期修改和维护的能力都得到了不可思议的提升。


好了,本文主要是结合我过往的TDD实战的经历,希望能从理论上说明TDD在不同类型的项目中能给我们带来哪些收益。

下一篇文章,我会结合成都客户的具体情况和我们自己在Flutter上实践TDD的过程来讲讲基于Flutter的TDD具体流程和技术细节。


如果看到这里的同学有学习Flutter或者TDD的兴趣,欢迎联系老刘,我们互相学习。

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

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

覆盖90%开发场景的《Flutter开发手册》


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

她说:JSON 没错,但就是 parse 不过?我懂了!

web
技术纯享版:《不规范 JSON 怎么办?三种修复思路+代码实现》 开篇:夜色渐浓,佳人亦在 那天晚上,办公室的灯已经灭了大半,只剩几个工位发出轻轻的蓝光。中央空调早就熄了,但显示器的热度依然在屏幕前形成一圈圈淡淡的光晕。 我坐在靠窗的位置,刚把代码提交推送完...
继续阅读 »

技术纯享版:《不规范 JSON 怎么办?三种修复思路+代码实现》



开篇:夜色渐浓,佳人亦在


那天晚上,办公室的灯已经灭了大半,只剩几个工位发出轻轻的蓝光。中央空调早就熄了,但显示器的热度依然在屏幕前形成一圈圈淡淡的光晕。


我坐在靠窗的位置,刚把代码提交推送完,正打算收键盘走人。


这时,小语走过来,端着还冒着热气的速溶咖啡——她果然又是那个留下来最晚的人之一。


“诶~”她蹲在我旁边的桌子边上,语气带着一丝挫败,“你这边有没有遇到 JSON 字符串明明格式看着没错,却死活 JSON.parse 不过的情况?”


一个普通的错误,却不是普通的崩溃


原来她在调试一个用户日志上传模块,前端接收到的日志数据是从后端来的 JSON 字符串。


问题出在一个看似再平常不过的解析操作上——


const logData = JSON.parse(incomingString);

可是控制台总是报错:Unexpected token。数据一眼看去也没问题,{'name': 'Tom', 'age': 30} —— 结构清晰,属性齐全,但偏偏就是“坏掉了”。


她抿了一口咖啡,苦笑,“我知道是引号的问题,可这种数据是从破旧的系统里吐出来的,量还特别大,我不可能一个个手动改。”


风起 · JSON.parse 不是万灵药


我们一起回顾了她的实现方式。她用的是最基础的 JSON.parse(),这是我们在项目里默认的处理方式——简单、直接、快速。


但这个方法对 JSON 格式的要求极其严格:



  • 只能使用双引号 "

  • 属性名必须加引号

  • 不容忍任何额外字符或注释


一旦出现诸如单引号、缺少逗号、多余空格这些“微小过失”,就直接抛错了。


小语叹气,“很多时候这些 JSON 是设备端拼出来的,不规范,又没有错误提示,我根本不知道该怎么修。”


我翻了翻之前的代码,从夹缝中找出来一张破旧的黄皮纸,我们俩一起瞅了上去,看到上面写着


function tryParseJSON(jsonString) {
try {
return JSON.parse(jsonString);
} catch (e) {
// 尝试简单修复:去除可能的多余字符
const cleaned = jsonString.replace(/[^\x20-\x7E]/g, '').trim();
try {
return JSON.parse(cleaned);
} catch (e2) {
console.error("无法解析JSON:", e2);
return null;
}
}
}

下面备注了一行小字:此法在一些更轻量的场景里,做一些“简陋修复“,对于简单的问题有时能奏效,但对于更复杂的错误,比如混合了单引号和双引号的情况,只能再实现另一个方法可以做更针对性的修复方法


function fixQuotes(jsonString) {
// 将单引号替换为双引号(简单情况)
return jsonString.replace(/'/g, '"');
}

小语感叹一声:“没有更好的了吗?”


解决篇 · 来自大佬的一句话


恰好这时,阿杰从会议室出来,耳机还挂在脖子上。


他听了一耳朵后随口说了句:“你们试过 jsonrepair 吗?那玩意能把坏 JSON 修回来,就像修车。”


“json... repair?”小语一脸困惑。


我忽然想起,之前有个日志监控服务也碰到类似的问题,当时就是用了这个库一把梭。


我打开编辑器,快速翻出来了这一段:


npm install jsonrepair

const { jsonrepair } = require('jsonrepair');

const damaged = "{name: 'John', age: 30}";
const fixed = jsonrepair(damaged); // => {"name":"John","age":30}
const obj = JSON.parse(fixed);

小语凑过来看了一眼,眼睛一亮:“它真的把引号补好了?”


我点头。这个工具是为了解决类似“非标准 JSON”问题的,它会尽可能地补全缺失引号、逗号,甚至处理 Unicode 异常字符。


当然,也不是所有情况都适用。


比如碰到乱码或者非法嵌套结构,jsonrepair 有时也会无能为力。这时可以退一步——用更宽松的解析器,比如 JSON5


const JSON5 = require('json5');
const result = JSON5.parse("{name: 'John', age: 30}"); // 也能解析

我看着认真学习的小语,语重心长的讲道:它不是修复,而是扩展 JSON 标准,让一些非标准写法也能解析(JSON5 能容忍的内容包括:单引号、尾逗号、注释、未加引号的属性名、十六进制、科学计数法等数字格式)


接着我们还讨论了更复杂的修复方式,比如用正则处理批量日志,甚至用 AST 工具逐步构建 JSON 树。但那是更远的故事了。


面对当前的问题,我们准备搞一套组合拳:


function parseJson(jsonString) {
// 第一步:尝试标准JSON解析
try {
return JSON.parse(jsonString);
} catch (e) {
console.log("标准JSON解析失败,尝试修复...");

// 第二步:尝试使用jsonrepair修复
try {
const { jsonrepair } = require('jsonrepair');
const fixedJson = jsonrepair(jsonString);
return JSON.parse(fixedJson);
} catch (e2) {
console.log("修复失败,尝试使用JSON5解析...");

// 第三步:尝试使用JSON5解析
try {
const JSON5 = require('json5');
return JSON5.parse(jsonString);
} catch (e3) {
// 最后:如果所有方法都失败,返回错误信息
console.error("所有解析方法都失败了:", e3);
throw new Error("无法解析JSON数据");
}
}
}
}

结局


一段时间后,小语在前端监控日志里贴了段截图:原本一天上千条的 parse error 错误,几乎消失了。


她补了一句:“终于不用再一个个点开调日志了。”


我回头看她的工位,屏幕亮着,浏览器里是一个模拟器页面,console 正在缓缓输出内容。


她突然抬起头看着我,问道:“AST是什么?听说也能实现json修复?”


作者:洛小豆
来源:juejin.cn/post/7506754146894168118
收起阅读 »

Spring之父:自从我创立了 Spring Framework以来,我从未如此确信需要一个新项目

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术 Spring框架之父再出发:发布JVM智能体框架Embabel,赋能企业级AI应用 当今,人工智能的浪潮正以前所未有的势头席卷技术世界,Python 凭借其强大的生态系统成为了AI开发的“通用语”。 然...
继续阅读 »

大家好,这里是小奏,觉得文章不错可以关注公众号小奏技术



Spring框架之父再出发:发布JVM智能体框架Embabel,赋能企业级AI应用


当今,人工智能的浪潮正以前所未有的势头席卷技术世界,Python 凭借其强大的生态系统成为了AI开发的“通用语”。


然而,Spring 框架的创始人Rod Johnson 却发出了不同的声音。


”自从我创立 Spring 框架以来,我从未如此坚信一个新项目的必要性。自从我开创了依赖注入(Dependency Injection)和其他 Spring 核心概念以来,我从未如此坚信一种新编程模型的必要性,也从未如此确定它应该是什么样子“


为此,他亲手打造并开源了一个全新的项目——Embabel:一个为 JVM 生态量身定制的 AI 智能体(Agent)框架


我们为什么需要一个智能体框架


难道大型语言模型(LLM)还不够聪明,无法直接解决我们的问题吗?难道多聊天协议(MCP)工具不就是我们让它们解决复杂问题所需要的一切吗?


不。MCP 是向前迈出的重要一步,Embabel 自然也拥抱它,就像它让使用多模型变得简单一样。


但是,我们需要一个更高级别的编排技术,尤其是对于业务应用程序,原因有很多。以下是一些最重要的原因



  • 可解释性(Explainability): 在解决问题时,选择是如何做出的?

  • 可发现性(Discoverability): MCP 绕开了这个重要问题。我们如何在每个节点找到正确的工具,并确保模型在它们之间进行选择时不会混淆?

  • 混合模型的能力(Ability to mix models): 这样我们就不用依赖于“上帝模型”,而是可以为许多任务使用本地的、更便宜的、私有的模型。

  • 在流程的任何节点注入“护栏”(guardrails)的能力。

  • 管理流程执行并引入更高弹性的能力。

  • 大规模流程的可组合性(Composability)。 我们很快将看到的不仅是在一个系统上运行的智能体,而是智能体的联邦。

  • 与敏感的现有系统(如数据库)进行更安全的集成,在这些地方,即使是最好的 LLM,给予其写权限也是危险的。


这些问题在企业环境中尤为突出,它们需要的不是一个简单的问答机器人,而是一个可解释、可控制、可组合且足够安全的高级编排系统。这正是智能体框架的价值所在。


为什么是JVM,而不是Python?


PythonAI 研究和数据科学领域地位稳固,但 GenAI 的核心是连接与整合。当我们构建企业级 AI 应用时,真正的挑战在于如何将 AI 能力与数十年积累的、运行在 JVM 上的海量业务逻辑、基础设施和数据无缝对接。


在企业应用开发、复杂系统构建和关键业务逻辑承载方面,JVM 生态(Java/Kotlin)拥有无与伦比的优势和成熟度。因此,与其让业务逻辑去追赶 AI 技术栈,不如让 AI 技术栈主动融入业务核心——JVM。


Embabel:为超越而生的下一代智能体框架


Embabel 的目标并非简单地追赶 Python 社区的同类框架,而是要实现跨越式超越。它带来了几个革命性的特性:



  1. 确定性的智能规划:Embabel 创新地引入了非 LLM 的 AI 规划算法。它能自动从你的代码中发现可用的“能力”和“目标”,并根据用户输入智能地规划出最优执行路径。这意味着你的系统是可扩展的,增加新功能不再需要重构复杂的逻辑,同时整个规划过程是确定且可解释的。

  2. 类型安全的领域模型:Embabel 鼓励开发者使用 Kotlin data classJava record 构建丰富的领域模型。这使得与 LLM 交互的提示(Prompt)变得类型安全、易于工具检查和代码重构,从根本上提升了代码质量和可维护性。

  3. Spring无缝集成:EmbabelKotlin 构建,并承诺为 Java 开发者提供同等一流的体验。更重要的是,它与 Spring 框架深度集成。对于数百万 Spring 开发者来说,构建一个 AI 智能体将像开发一个 REST API 一样自然、简单。


加入我们,共创未来


对于JVM 开发者来说,这是一个激动人心的时代。Embabel 提供了一个绝佳的机会,让你可以利用自己早已熟练掌握的技能,为你现有的 Java/Kotlin 应用注入强大的 AI 能力,从而释放巨大的商业价值。


项目尚在早期,但蓝图宏大。Embabel 的目标是成为全球最好的智能体平台。现在就去 GitHub 关注 Embabel,加入社区,贡献你的力量,一同构建企业级 AI 应用的未来。


参考



作者:小奏技术
来源:juejin.cn/post/7507438828178849828
收起阅读 »

苹果加急审核是“绿色通道”还是"死亡陷阱"?

iOS
前言 作为iOS的老人基本上都听说过加急审核通道,对于加急审核通道最初是苹果审核给开发者用来维护重大Bug,提供的特殊通道。 如果是说遇到重大节假日或者有严重闪退问题,比如:新用户注册失败,搜索闪退,启动闪退等等.. 有一位粉丝最近遇到了加急审核的时候触发了账...
继续阅读 »

前言


作为iOS的老人基本上都听说过加急审核通道对于加急审核通道最初是苹果审核给开发者用来维护重大Bug,提供的特殊通道。


如果是说遇到重大节假日或者有严重闪退问题,比如:新用户注册失败,搜索闪退,启动闪退等等..


有一位粉丝最近遇到了加急审核的时候触发了账号等待终止的通知,具体如下:


We're unable to accommodate your expedite request.
Your developer account is currently pending termination due to violations of the Apple Developer Program License Agreement. Refer to the message you received in App Store Connect for more information.
If you believe your account shouldn't be terminated, you may submit an appeal to the App Review Board if you have not done so already.

加急审核能不能用?


首先刚刚已经提过了关于苹果提供加急审核背景,常言道:存在即合理! 如果说产品确实出现了紧急Bug,真的需要修复没有问题。但是建议新账号新产品不要使用。


这就好比一个新手村的小白,突然跟PNC说我想获得新手通关秘籍。对于苹果审核人员来说只会是满头问号。内心OS:谁给你的勇气?勇气大帝梁静茹么?


正常迭代的产品申请加急审核最快在30~50分钟就可以进入【这里是指从等待审核状态到正在审核状态】。


那么老账户可以使用但是别作! 这里说的别作是指别动不动就加急审核!尤其是小修小补的bug,想快速开奖。虽然加急审核没有明确的次数说明,但是归根结底加急审核是为开发者提供“绿色通道”,而不是试探苹果底线的通道。


这就好星爷“九品芝麻官”中的场景。


wechat_2025-05-22_185630_813.png


偶尔使用还好,频繁使用对于审核人员的内心。


wechat_2025-05-22_185850_582.png


为什么会触发封号?


说句拗口的话,封号不是因为加速审核而封号,而是本身就有问题而封号。说的直白一点,加速审核不过是导火索罢了。 其实苹果早就盯上了,只是问题不太严重睁一只眼闭一只,本来大家可以相安无事,非得站出来打破这种平衡。


举个栗子,这就好比追一个女神本来处于比较暧昧的关系半推半就,你突然心急了。让女神一下子看到了你的另一面,瞬间关系结束回到冰点。


那么到底有哪些行为开发者不适合加急审核呢?


1.套用的代码,重复使用的矩阵型App


2.新产品使用被下架品牌词做App名称


3.购买的账号,身份信息存在可疑


4.存在关联的测试设备或收款账户


5.过渡堆砌关键词在元数据中


作为开发者要拎的清楚大小王,不要疯狂去试探苹果的规则,除非UI原创、玩法新颖、代码古法手打。


遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!


作者:iOS研究院
来源:juejin.cn/post/7507198934159835174
收起阅读 »

这篇 Git 教程太清晰了,很多 3 年经验程序员都收藏了

引言 📌 Git 是现代开发中不可或缺的版本控制工具,尤其适用于团队协作和代码管理。本文将带你了解 Git 的基础操作命令,包括 git init、git add、git commit、git diff、git log、.gitignore 等,快速上手版本控...
继续阅读 »

引言


📌 Git 是现代开发中不可或缺的版本控制工具,尤其适用于团队协作和代码管理。本文将带你了解 Git 的基础操作命令,包括 git initgit addgit commitgit diffgit log.gitignore 等,快速上手版本控制。




🛠️ 一、初始化仓库:git init


使用 Git 前,需先初始化一个本地仓库:


git init

执行后会在当前目录生成一个 .git 文件夹,Git 会在此目录下跟踪项目的变更记录。




👤 二、配置用户信息


首次使用 Git 时,推荐设置用户名和邮箱:


git config --global user.name "xxxxx"
git config --global user.email "xxxx@qq.com"

加上 --global 会全局生效,仅对当前项目配置可以省略该参数。




📦 三、代码暂存区(Staging Area)是什么?


Git 的提交操作分为两个阶段:暂存(staging)提交(commit)



  • 当你修改了文件,Git 并不会立即记录这些改动;

  • 你需要先使用 git add 命令,把改动“放进暂存区”,告诉 Git:“这些改动我准备好了,可以提交”;

  • 然后再使用 git commit 将暂存区的内容提交到本地仓库,记录为一个快照。



🧠 可以把暂存区类比为“快照准备区”,你可以反复修改文件、添加到暂存区,最后一口气提交,确保每次提交都是有意义的逻辑单元。



🎯 举个例子:


# 修改了 index.html 和 style.css
git add index.html # 把 index.html 放入暂存区
git add style.css # 再把 style.css 放入暂存区
git commit -m "更新首页结构和样式" # 一起提交

💡 小贴士:你可以分批使用 git add 管理暂存内容,按逻辑分组提交更利于协作和回溯。




📝 四、查看当前状态:git status


在进行任何修改之前,查看当前仓库的状态是非常重要的。git status 是最常用的命令之一,能让你清楚了解哪些文件被修改了,哪些文件已加入暂存区,哪些文件未被跟踪。


git status

它的输出通常会分为三部分:



  1. 已暂存的文件:这些文件已使用 git add 添加到暂存区,准备提交。

  2. 未暂存的文件:这些文件被修改,但还未添加到暂存区。

  3. 未跟踪的文件:这些文件是新创建的,Git 并未跟踪它们。


例如:


On branch main
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
new file: style.css

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: app.js

🎯 通过 git status,你可以随时了解当前工作区和暂存区的状态,帮助你决定接下来的操作。




📥 五、添加文件到暂存区:git add


当你修改或新增文件后,使用 git add 将其添加到 Git 的暂存区:


git add 文件名

也可以批量添加所有修改:


git add .



💾 六、提交更改:git commit -m


将暂存区的内容提交至本地仓库:


git commit -m "提交说明"

-m 后面是提交信息,建议语义清晰,例如:


git commit -m "新增用户登录功能"



🚀 七、推送到远程仓库:git push origin main


本地提交之后,需要推送代码到远程仓库(如 GitHub、Gitee):


git push origin main


  • origin 是默认的远程仓库别名;

  • main 是目标分支名(如果你使用的是 master,请替换);



✅ 提交后远程成员就可以拉取(pull)你最新的修改了。



🔗 如果你还没有远程仓库,请先去 GitHub / Gitee 创建一个,然后关联远程仓库地址:


git remote add origin https://github.com/yourname/your-repo.git



🕵️ 八、查看文件改动:git diff


commit 之前,可用 git diff 查看修改内容:


git diff



📜 九、查看提交历史:git log --oneline


快速查看历史提交记录:


git log --oneline

输出示例:


e3f1a1b 添加登录功能
2c3d9a7 初始提交



🛑 十、忽略某些文件:.gitignore


在项目中,有些文件无需提交到 Git 仓库,例如缓存、编译结果、配置文件等。使用 .gitignore 文件可忽略这些文件:


# 忽略 node_modules 文件夹
node_modules/

# 忽略所有 .log 文件
*.log

# 忽略 .env 环境变量文件
.env



🌿 十一、重命名默认分支:git branch -M main


很多平台(如 GitHub)推荐使用 main 作为主分支名称:


git branch -M main

这样可以将默认分支由 master 改为 main




✅ 总结命令一览表


命令作用
git init初始化仓库
git config设置用户名与邮箱
git status查看当前文件状态
git add添加改动到暂存区
git commit -m提交改动
git push origin main推送代码到远程 main 分支
git diff查看未提交的改动
git log --oneline查看提交历史
.gitignore忽略文件
git branch -M main重命名分支为 main



🧠 写在最后


Git 是每个开发者都必须掌握的技能之一。掌握好这些常用命令,就能覆盖 90% 的使用场景。未来如果你要进行多人协作、分支合并、冲突解决,这些基础就是你的武器库。




觉得实用就点个赞、关注、收藏支持一下吧 🙌


作者:小公主
来源:juejin.cn/post/7506776151315922971
收起阅读 »

📢 程序员注意!这些代码可能会让你"吃牢饭"!

关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 不知道你有没有听过面向监狱编程,可能你好好的码着代码,就就被帽子叔叔带走了。 "我只是个写代码的,关我什么事?" 这是深圳某P2P平台架构师在法庭上崩溃大喊,但代码提交记录中的"反爬优化注释"...
继续阅读 »

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




01 引言


不知道你有没有听过面向监狱编程,可能你好好的码着代码,就就被帽子叔叔带走了。


"我只是个写代码的,关我什么事?" 这是深圳某P2P平台架构师在法庭上崩溃大喊,但代码提交记录中的"反爬优化注释"成了铁证——他因非法吸收公众存款罪获刑5年。这不是电影,而是2023年真实判决!


程序员早已不是免责职业,你的键盘可能正在敲响监狱的大门!


02 血泪案例


⚠️ 这些代码真的会"坐牢"!


2.1 爬虫爬进铁窗


爬虫其实是最容易爬进铁窗的代码。当时可能只是为了解决一个业务痛点,一旦被非法使用,就可能触犯法律的红线。开发者无意,使用者有心,莫名其名的就背了锅。


案例:


浙江某程序员用分布式爬虫狂扫10亿条个人信息,庭审时辩解:"技术无罪!"。


法官怒怼:"每秒突破5次反爬验证,这叫'技术中立'?" —— 6人团队全员获刑!


所以,我们在日常工作中开发爬虫就应该得到启示:



  • ✅数据是否涉个人隐私?

  • ✅是否突破网站防护?

  • ✅对方是否知情同意?


实在拿不准,公司一般都会有法务,可以咨询。


2.2 权限变"凶器"


我们在开发过程中为处理决异常数据的问题,可能会在代码里面留后门。正常的业务功能本身没有问题,但是涉及支付、数据安全等行为,就要注意了。被他人恶意使用,不仅会造成财产损失,可能会还会勉励牢狱之灾。


案例:


杭州前程序员偷偷植入"定时转账代码",21万公款秒变私人财产。检察机关以马某涉嫌盗窃罪、妨害公务罪向法院提起公诉,经法院审理作出一审判决,马某被判处有期徒刑四年二个月,并处罚金。


该起事件也为程序员们敲响了警钟。玩归玩,闹归闹,法律红线不可碰。


🛑 高位操作清单:



  • ❌ 私留系统后门

  • ❌ 超权限访问数据

  • ❌ 删除/篡改数据或日志


2.3 "技术黑产"陷阱


程序员除了工作之外,很多人可能还会通过接私活,如猪八戒网等。以此增加自己的收入。没有公司的严格审核,很多程序员就会掉如技术黑产 的陷阱。


案例:


湖北大学生接私活开发"诈骗APP",庭审播放需求录音:"要能后台改赌局结果"。可能当时你只想着:"我只负责技术实现" 最后却成诈骗案从犯!


尤其一些关于支付的似乎要尤为谨慎,特别是支付成功后,限制体现的时间,很有可能就会用于非法洗钱的黑坑里。


🔥 接私活避坑指南



  • 👉 要求签署书面合同

  • 👉 录音确认需求合法性

  • 👉 转账账户必须对公


03 为什么程序员总成背锅侠


其实大多数程序员都是很单纯的,尤其那些喜欢挑战的程序员。他可能只为表现自己的实力,仅仅只是按照需求开发了功能,但是被恶意利用,最终成为背锅侠


程序员以为法官认定
突破反爬是技术挑战非法侵入计算机系统
按需求开发无过错明知违法仍提供帮助
删除代码就没事电子证据完整链锁定"

血泪真相:你的Git提交记录、代码注释、甚至TODO列表都可能成为呈堂证供!


04 IT人保命三件套


4.1 代码防坐牢套餐


敏感功能增加法律注释。开发的功能以及项目的沟通都要留档尤其需求的变更。因为接到需求的时候可能没有问题,改着改着就出问题了。


拒绝口头需求,落实文档记录,需求、会议、项目事件以邮件的方式存档。


4.2 权限管理生死线


权限管理是保护数据安全的重要措施,但是可能为了调试方便,预留逃逸后门。被人利用轻则数据信息泄露、重则踩缝纫机。


三方对接中,加强公钥、私钥的管理,防止恶意推送或者拉取数据。敏感信息是否脱敏,都是开发中需要考虑的要点。


如果有必要,增加埋点记录,日志记录。收集用户的操作行为。


4.3 法律意识


每个IT公司都会面临网络安全的检查,可以多了解一些相关的法律发条。至少了解那些数据可能会属于需要保护的数据,引起重视。


如果公司允许,可以多参加一些《数据安全法》《网络安全》等的培训。


05 技术向善


代码改变世界,这个不是一句虚话。运用好技术,代码也可以是光。


阿里巴巴的支付宝硬是借助技术,将全国带入数字支付的时代;疫情期间的随申码、一码通等,为战胜疫情作出了巨大贡献;大模型的火爆推送了智能时代的到来等等。


真正的大神不仅代码能跑通,人生更不能"跑偏"!


你在工作中遇到过哪些"法律边缘"的需求?评论区说出你的故事。


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

Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻

web
欢迎关注微信公众号:FSA全栈行动 👋 BiliBili: http://www.bilibili.com/video/BV1yT… 一、概述 距离 chat_bottom_container 首个可用版本 (0.0.2) 的发布已经过去了 1 个多月,在...
继续阅读 »

欢迎关注微信公众号:FSA全栈行动 👋




BiliBili: http://www.bilibili.com/video/BV1yT…


一、概述


距离 chat_bottom_container 首个可用版本 (0.0.2) 的发布已经过去了 1 个多月,在这期间根据大家的使用反馈,我们也做了一些优化调整,今天就来盘点一下到底做了哪些优化,新增了什么功能,以及一些常见操作。


请注意




开源不易,如果你也觉得这个库好用,请不吝给个 Star 👍 ,并多多支持!


github.com/LinXunFeng/…



二、使用


调整键盘高度监听管理逻辑


0.1.0 版本前,只考虑了页面栈这种常规情况,当键盘高度变化时只处理栈顶的监听。


但其实还有一种常见打破该规则的场景,就是悬浮聊天页,它会一直在页面上,可能为了能快速从悬浮小球展开聊天页面,收起时只是做了隐藏,而不会销毁页面,在这种情况下,它依旧在监听管理里的栈顶,所以在收起后,上一个聊天页的键盘高度监听就会失效。


这个在 0.1.0 版本中得到修复,内部会倒序遍历调用所有的监听回调。


不过你不用担心这一改动会导致其它聊天页面出现多余的视图刷新,因为在键盘高度监听回调里会先判断输入框是否有焦点,若无则直接返回了。


兼容外接键盘


当连接外接键盘时,软键盘会消失,高度会降为 0,这里可以用 iOS 模拟器结合 Toggle Software Keyboard (快捷键: cmd + k) 来模拟连接与断开外接键盘的效果。



隐藏面板


有小伙伴提出,不知道如何程序式的隐藏面板,其实很简单,就两步



  1. 让输入框失去焦点

  2. 更新内部状态为 ChatBottomPanelType.none


hidePanel() {
// 0.2.0 前
inputFocusNode.unfocus();
if (ChatBottomPanelType.none == controller.currentPanelType) return;
controller.updatePanelType(ChatBottomPanelType.none);

// 0.2.0 后,可以这么写
controller.updatePanelType(
ChatBottomPanelType.none,
forceHandleFocus: ChatBottomHandleFocus.unfocus,
);
}

自定义底部安全区高度


在默认情况下,chat_bottom_container 在收起模式 (.none) 下会自动帮你添加底部安全区高度,但在一些场景下你可能不希望如此。比如:



  • 安卓的底部安全区的高度,很多小伙伴都是简单粗暴的设置个高度了事

  • App 首页有底部 BottomNavigationBar,不需要安全区高度


在此,你可以通过将 safeAreaBottom 参数来自定义这个高度,如下设置为 0


return ChatBottomPanelContainer<PanelType>(
...
safeAreaBottom: 0,
...
);

调整键盘面板高度


如示例中位于首页的聊天页面



在键盘弹出时,如下图所示


实际期望

很明显,我们希望键盘容器高度能够减去外层底部固定的 BottomNavigationBar 高度。


ChatBottomPanelContainer 提供了 changeKeyboardPanelHeight 回调,在回调中可以拿到当前的键盘高度,经过计算后,将合适的键盘容器高度返回即可。


return ChatBottomPanelContainer<PanelType>(
...
changeKeyboardPanelHeight: (keyboardHeight) {
final renderObj = bottomNavigationBarKey.currentContext?.findRenderObject();
if (renderObj is! RenderBox) return keyboardHeight;
return keyboardHeight - renderObj.size.height;
},
...
);

缓存键盘高度


先来看未做键盘高度缓存处理之前,会发生什么?



上图一共进入了三次聊天页



  • 第一次是先点击键盘,再切到表情面板,体验起来还是挺不错的。

  • 为了避免一闪而过,没有注意到,所以第二次和第三次的操作是一样的,先唤起表情面板,再切到键盘,可以看到在切到键盘时会抖动。


这是因为每次进入聊天页,键盘的高度为初始值 0,在 0.2.0 版本中对此进行了优化,加入了键盘高度缓存逻辑,从而尽量避免该抖动问题的出现。



❗️ 但需要注意的是,假如你卸载重装 App,该缓存会丢失,即你还是有可能会看到最多一次的抖动。



除此之外,你还可以使用这个缓存的键盘高度来实现表情面板与键盘高度保持一致的效果,这样在切换的时候体验上会更好些。😉


Widget _buildEmojiPickerPanel() {
// 如果键盘高度还没有缓存过,则使用默认高度 300
double height = 300;
final keyboardHeight = controller.keyboardHeight;
if (keyboardHeight != 0) {
height = keyboardHeight;
}

return Container(
padding: EdgeInsets.zero,
height: height,
color: Colors.blue[50],
child: const Center(
child: Text('Emoji Panel'),
),
);
}

效果如下



支持表情面板与输入框焦点共存


这也是提升用户体验的重要一点,效果见上图。


先按如下设置你的输入框


bool readOnly = false;
TextEditingController textEditingController = TextEditingController();

...
TextField(
controller: textEditingController,
focusNode: inputFocusNode,
// 为 true 时不显示键盘,默认为 false
readOnly: readOnly,
// 获取焦点后显示光标,设置为 true 才不受 readOnly 的影响
showCursor: true,
),
...

接下来就是切换表情面板的操作


switchToEmojiPanel() {
readOnly = true;
// 这里你可以只刷新输入框
setState(() {});

// 等待下一帧
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
controller.updatePanelType(
// 内部切至 other 状态
ChatBottomPanelType.other,
// 关联外部的面板类型为表情面板
data: PanelType.emoji,
// 输入框获取焦点
forceHandleFocus: ChatBottomHandleFocus.requestFocus,
);
});
}

updatePanelType 方法中,如果是切至 .other 状态,是会帮你执行失去焦点操作的,所以这里提供了一个 forceHandleFocus 参数,如果你对方法内部对焦点的处理不满意,你可以使用它来强制指定焦点的处理方式。


三、最后


好了,上述便是该库的更新内容, 惯例附上 GitHub 地址: github.com/LinXunFeng/… ,如果接入上有什么问题,可以在链接中查看 demo 演示代码。


开源不易,如果你也觉得这个库好用,请不吝给个 Star 👍 ,并多多支持!


本篇到此结束,感谢大家的支持,我们下次再见! 👋



如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~



作者:LinXunFeng
来源:juejin.cn/post/7399045497002328102
收起阅读 »

一个js库带你看懂AI+前端的发展方向

web
前言随着技术的发展,人工智能正逐渐渗透到我们生活的方方面面,从前端开发到后端服务,从数据分析到用户体验设计。特别是在前端领域,AI 的应用正成为一个不可忽视的趋势。本文将探讨 AI 在前端领域的应用,并重点介绍一个在浏览器端即可运行的神经网络库——Brain....
继续阅读 »

前言

随着技术的发展,人工智能正逐渐渗透到我们生活的方方面面,从前端开发到后端服务,从数据分析到用户体验设计。特别是在前端领域,AI 的应用正成为一个不可忽视的趋势。本文将探讨 AI 在前端领域的应用,并重点介绍一个在浏览器端即可运行的神经网络库——Brain.js。

Brain.js:浏览器端的神经网络库

Brain.js 是一个专为前端开发者设计的 JavaScript 库,它允许开发者在浏览器或 Node.js 环境中轻松创建和训练神经网络。以下是 Brain.js 的几个核心能力:

  1. 投喂数据训练
  • Brain.js 支持以 JSON 数组的形式投喂数据,这使得准备训练数据变得非常简单。例如,可以准备一个包含输入和期望输出的数据集,用于训练神经网络。
const trainingData = [
{ input: [0, 0], output: { zero: 1 } },
{ input: [0, 1], output: { one: 1 } },
{ input: [1, 0], output: { one: 1 } },
{ input: [1, 1], output: { zero: 1 } }
];
  1. 实例化神经网络
  • Brain.js 提供了多种类型的神经网络,包括前馈神经网络(Feedforward Neural Networks)和循环神经网络(Recurrent Neural Networks)。对于文本处理和序列数据,推荐使用 brain.recurrent.LSTM(),这是一种长短期记忆网络,特别适合处理时间序列数据。
  1. 训练模型
  • 训练神经网络非常简单,只需调用 train 方法并传入训练数据即可。Brain.js 会自动调整网络参数,使模型逐步学会从输入数据中提取特征并作出准确的预测。
  1. 推理能力
  • 训练完成后,可以使用 run 方法对新的输入数据进行推理。例如,在 NLP 场景中,可以使用训练好的模型对用户输入的文本进行情感分析或分类。
  1. 结果分类
  • Brain.js 支持多分类任务,可以将输入数据归类到多个预定义的类别中。这对于内容推荐、垃圾邮件过滤等应用场景非常有用。

开始使用 Brain.js:

要开始使用 Brain.js,首先需要安装它。如果你是在 Node.js 环境下工作,可以通过 npm 安装:

npm install brain.js

如果你在浏览器中使用,可以直接通过 CDN 引入:

<script src="https://cdn.jsdelivr.net/npm/brain.js">script>

然后可以按照官方文档提供的示例代码来构建你的第一个神经网络模型。

示例1:

// 创建一个神经网络
const network = new brain.NeuralNetwork();

// 4 个输入对象训练网络
network.train([
{ input: [0, 0], output: { zero: 1 } },
{ input: [0, 1], output: { one: 1 } },
{ input: [1, 0], output: { one: 1 } },
{ input: [1, 1], output: { zero: 1 } }
]);

// [1, 0] 的预期输出是什么?
const result = network.run([1, 0]);

// 显示 "zero" "one" 的概率
console.log(result["one"] + " " + result["zero"]);
  • 使用 new brain.NeuralNetwork() 创建一个神经网络。
  • 使用 network.train([examples]) 训练网络。
  • examples 表示 4 个输入值及其对应的输出值。
  • 使用 network.run([1, 0]) 询问 "[1, 0] 的可能输出是什么?"

网络的输出是:

  • one: 93%(接近 1)
  • zero: 6%(接近 0)

使用 CSS,颜色可以通过 RGB 设置:

示例2:

颜色RGB
黑色RGB(0,0,0)
黄色RGB(255,255,0)
红色RGB(255,0,0)
白色RGB(255,255,255)
浅灰色RGB(192,192,192)
深灰色RGB(65,65,65)

下面的代码展示了如何预测颜色的深浅:

// 创建一个神经网络
const net = new brain.NeuralNetwork();

// 4 个输入对象训练网络
net.train([
// 白色 RGB(255, 255, 255)
{ input: [255 / 255, 255 / 255, 255 / 255], output: { light: 1 } },
// 浅灰色 (192, 192, 192)
{ input: [192 / 255, 192 / 255, 192 / 255], output: { light: 1 } },
// 深灰色 (64, 64, 64)
{ input: [65 / 255, 65 / 255, 65 / 255], output: { dark: 1 } },
// 黑色 (0, 0, 0)
{ input: [0, 0, 0], output: { dark: 1 } }
]);

// 深蓝色 (0, 0, 128) 的预期输出是什么?
let result = net.run([0, 0, 128 / 255]);

// 显示 "dark" "light" 的概率
console.log(result["dark"] + " " + result["light"]);
  • 使用 new brain.NeuralNetwork() 创建一个神经网络。
  • 使用 network.train([examples]) 训练网络。
  • examples 表示 4 个输入值及其对应的输出值。
  • 使用 network.run([0, 0, 128 / 255]) 询问 "深蓝色的可能输出是什么?"

网络的输出是:

  • Dark: 95%
  • Light: 4%

示例3:

下面这个例子演示如何使用 Brain.js 创建并训练一个基本的神经网络,该网络学习从摄氏度转换为华氏度:

const brain = require('brain.js');

// 创建一个 LSTM 神经网络实例
const net = new brain.recurrent.LSTM();

// 准备训练数据
const trainingData = [
{ input: '0', output: '32' }, // 0°C -> 32°F
{ input: '100', output: '212' } // 100°C -> 212°F
];

// 训练神经网络
net.train(trainingData, {
iterations: 20000, // 训练迭代次数
log: (stats) => console.log(`Training progress: ${stats.iterations}/${stats.error}`) // 训练日志
});

// 使用训练好的模型进行推理
const output = net.run('50'); // 预测 50°C 对应的华氏温度
console.log(output); // 输出结果接近 "122"

其他用于创建神经网络的js库

TensorFlow.js、Synaptic.js、ConvNetJS、Keras.js、Deeplearn.js (现更名为 TensorFlow.js)、 ML.js等。 这些js库作为在浏览器端即可运行的神经网络库,为前端开发者提供了强大的工具,使得我们能够在不深入数学和机器学习理论的前提下,快速实现和应用机器学习功能。无论是简单的分类任务、预测建模,还是更复杂的自然语言处理和图像识别,它们都能帮助你轻松应对。

结语

你发现了吗,通过brain.js,你也可以轻松地将机器学习功能集成到你的项目中。未来,随着模型的小型化、边缘计算的发展以及多模态融合的推进,AI + 前端将更加普及和成熟。

R-C.png

点个赞再走吧~


作者:PW
来源:juejin.cn/post/7438876948762066980

收起阅读 »

写了7年代码,我有点焦虑了!

很多人都知道程序员是吃青春饭,但是身为程序员的我们却很难感受到,直到某一天跳槽,突然看到坐在对面的面试官比自己还年轻,问的技术自己都是最新的,是不是觉得不服老都不行了 我是92年出生的,13年下半年入职第一份工作, 至今写代码经验已近7年。 职场经历也是有过多...
继续阅读 »

很多人都知道程序员是吃青春饭,但是身为程序员的我们却很难感受到,直到某一天跳槽,突然看到坐在对面的面试官比自己还年轻,问的技术自己都是最新的,是不是觉得不服老都不行了


我是92年出生的,13年下半年入职第一份工作, 至今写代码经验已近7年。


职场经历也是有过多种角色,初级程序员、中级程序员、高级程序员、项目经理、小组长、后端负责人等等。


除了短暂的做过政府项目、企业内部建站项目等,大部分还是混迹在互联网项目,项目经验也算是比较丰富。做过很多失败的产品,也参与过日并发近千万的项目,但是还是抵不住我的焦虑。


为何焦虑?


家庭开支和各类贷款


因为结婚生娃了,家庭开支压力也来了,如果不是靠着副业收入的补贴其实是很难存下来钱的。


深圳的生活压力其实挺大,稍微算了下每个月的支出从去年开始已经差不多稳定超2w了。


生活成本:房租3700+车贷4300+老家买的房贷2300+ 两个大人一个小孩的支出7000+


车贷目前还有3个月就还完了,这点倒是可以松一口气。


年龄和环境


再有2年就迈入30岁这个槛了,都说男人30而立,但是对于30岁的程序员来说,能保留一头浓密的头发已是万幸,我已经有这个趋势了,目前还在抢救中,晒个图吧


image-20200812163133017


当然,头发只是顺带的劣势,主要是如果30岁还没走上管理岗或者核心岗,那以后跳槽是相当悲剧的,写代码的如果跨行,多年的项目和业务经验都带不过去,在企业择人的时候,为什么不选个25岁的呢?


再说环境,如果我目前待的公司是世界五百强等牛逼的企业,肯定不会说焦虑这类的东西了,因为往往是那些公司,年龄和经验反而不是事业的阻碍,至少在企业内部不出问题的情况下还能稳定待很久,这个东西往往很现实的,很多工作多年的程序员跳槽的时候,除了考虑到薪资和待遇,对公司的稳定性还是有很大考量的。


很多人包括程序员都觉得,以后大不了年纪大了身体扛不住了转管理,但管理岗是这么容易说转就转的么?没有经历过带团队和带项目的你,给个团队你带岂不是带的一团糟,所以在这里建议程序员在工作中尽量体验多种角色,比如说小组长、项目负责人、项目经理或者团队负责人等等,不仅能在沟通上锻炼弥补程序员的不足,做出一定成绩了还能在工资和待遇上体现出来,这点我是深有体会。


对转业后的迷茫


本来想说退休的,有点歧义,这里叫转业吧。


转业是不可避免的事情,至于啥时候转业,啥时候考虑转业,还是根据个人所处环境决定的。


有的人在大公司,公司业绩蒸蒸日上,手握大把股权,自然不会考虑到这上面


但这是更多普通程序员的必走之路


所以很多有想法的程序员会对转业前有一个前期规划,大体上就是:工资+副业+理财


作为我来说,理财这方便目前还是小白,其他两个马马虎虎吧。


所以还得继续加油


年轻的程序员和上了年纪的程序员


认识一些之前共事的程序员朋友,在聊天的过程中发现,年轻的程序员不太喜欢安于现状,当环境或者薪资达不到自己期望的时候,就会选择跳槽,而且大概率能找到待遇比之前公司好的,上了年纪的程序员喜欢求稳,因为一份工作已经来之不易,养家糊口不容易,没那么喜欢折腾了。


同学小A


小A是我的大学同学,在近期有过一次跳槽,虽然其他待遇比之前公司差了一点,但是工资却是足足涨了5k,从25k涨到了30K.



虽然待遇提升了,但是还是避免不了程序员从一个坑跳另一个坑的魔咒,入职第一天就开始吐槽项目代码~


image-20200812171620405


对此我只能深表同情~


同事小B


小B是之前的同事,算算时间竟然也认识5年了,5年前就是前端leader,目前已经35岁了,我们有共同的群聊,一直保持密切沟通,这点我觉得在维护同事关系上还是做得不错的。


小B目前也是一家公司的leader,这两年又生了二胎,加上前几年在惠州拿了一套房,经济压力还是挺大的,当我们聊到跳槽这个话题的时候,想不到也是怂恿不动,一心求稳。不过小B保养有道,一个35岁的大龄程序员竟然看起来和毕业生差不多,想必还能写5年代码。



写到最后


焦虑这个话题,感觉不适合写长篇大论,只能草草收尾了,以下几点是写给大龄程序员以及自己的建议:



  • 做好长期合理理财。你不理财,财不理你,以前看到相关理财软文,其实是比较反感的,但是当身边一些朋友通过合理理财并获得比较理想回报的时候,我就在拍脑袋,觉悟跟不上啊。

  • 有上位机会的尽量不要错过。认识很多程序员当上面让他顶上去的时候都喜欢退缩,如果你的程序员生涯只仅限于写增删改查,那么多年以后真的只能做增删改查被淘汰了。当领导是会背锅,是会压力大,但是你收获的会更多。

  • 调理好身体。这点我就做的不好,已经有脂肪肝了,每年体检都会有点大大小小的毛病,身体就是革命的本钱,年轻的时候赚的再多,不搞好身体都会交给医院


看到这了,但凡有点共鸣,点赞转发走一波?


作者:GitHub摸轮子啊
来源:juejin.cn/post/6860043925357821966
收起阅读 »

项目经理训练两年半,我决定回归做程序员了

23年因一次机缘巧合,我从程序员转岗为项目经理。这之前,我已在程序员岗位摸爬滚打了近5年,当时的自己似乎已经看到了技术生涯的天花板,尤其是前端和 Web 开发领域,那把35岁“达摩克利斯之剑”仿佛始终悬在头顶。于是,我头也不回地扎进了项目管理岗位,远离了熟悉的...
继续阅读 »

23年因一次机缘巧合,我从程序员转岗为项目经理。这之前,我已在程序员岗位摸爬滚打了近5年,当时的自己似乎已经看到了技术生涯的天花板,尤其是前端和 Web 开发领域,那把35岁“达摩克利斯之剑”仿佛始终悬在头顶。于是,我头也不回地扎进了项目管理岗位,远离了熟悉的编辑器、代码与熟悉的团队。


回过头来看,疫情以来,互联网行业确实进入了大规模收缩期,程序员裁员新闻频繁爆出,我一度还庆幸自己的选择:“看来转岗是对的?”


可两年半后,在项目管理岗位上逐渐摸索成长,我却越来越清晰地看到“真实的自己”——可能我对工作的真正热情依然在于编程、在于技术。


一、认“真”与乌托邦


如果二十岁出头的年纪,还可以坦然地追求理想、抱着点乌托邦的情怀,倒也无可厚非。但三十岁左右,面对现实生活压力,似乎早该学会“世俗圆滑”,甚至当一个愤青、表达不满,都会被人视作“不知所谓”、“单纯幼稚”。


然而,每次试图“再忍忍”,内心总会不平静,耳边总回响起中学时代背诵的诗句:“竹杖芒鞋轻胜马”、“三十功名尘与土,八千里路云和月”、“穷且益坚,不坠青云之志”……想想,也不过就是从头再来?


以前我一直认为“认真”是个褒义词,但现在别人一提起“认真”,心里就会一阵不适:



  • 认真=负责? 那是做好牛马的前提,是被消耗的好耗材。

  • 认真=老实? 脾气好、有教养,最适合被人拿枪使。

  • 认真 vs 苟着? 难道想要站着把钱挣了不成?


在项目管理工作里,我再也感受不到编程带给我的那种纯粹、干净与安静。很多时候,大环境之下个体无法改变什么,哪怕你再坚持原则,也不强过如“铁笔换木枪”,一瞬即逝。


所以,我最终还是决定回归代码,至少在技术世界里,纯粹依旧值得追求。


二、开源精神与自由


做项目经理时,我学习了 PMP,了解了职能型和矩阵型组织结构。现实告诉我,矩阵型的项目管理在落地执行时往往演变成职能部门之间的推诿扯皮,“要发展就要搞关系”似乎已成为铁律。


与之相比,代码世界却截然不同。编程圈的开源精神是开放的、普世对等且包容的,它不拘泥于等级资历,这才是它真正迷人的地方。这种去中心化的世界,能够带给我们真正的自由感。


正因如此,我更加明确地感受到,自己真正热爱的、追求的依旧是编程,是技术带来的自由与纯粹。


未来,不论环境如何变化,我都会坚定地回归代码。


立此帖为证。


作者:掘金安东尼
来源:juejin.cn/post/7507908812599296035
收起阅读 »

着色器预热?为什么 Flutter 需要?为什么原生 App 不需要?那 Compose 呢?Impeller 呢?

依旧是来自网友的问题,这个问题在一定程度上还是很意思的,因为大家可能会想,Flutter 使用 skia,原生 App 是用 skia ,那为什么在 Flutter 上会有着色器预热(Shader Warmup)这样的说法?原生是不是也有?那 Compose ...
继续阅读 »

依旧是来自网友的问题,这个问题在一定程度上还是很意思的,因为大家可能会想,Flutter 使用 skia,原生 App 是用 skia ,那为什么在 Flutter 上会有着色器预热(Shader Warmup)这样的说法?原生是不是也有?那 Compose MultiPlafrom 是不是也需要?




今天又是又干又长。



是什么,为什么?


首先,我们要知道着色器预热(Shader Warmup)是什么,它又能干嘛?简单说:



着色器是 GPU 上运行所需的单元,也可以说成是在 GPU 上运行的代码段,skia 把「绘制命令」编译成可在 GPU 执行代码的过程,就叫做着色器编译。



好了,那着色器编译有什么问题? skia 需要「动态编译」着色器,但是 skia 的着色器「生成/编译」与「帧工作」是按顺序处理,如果着色器编译速度不够快,就可能会出现掉帧(Jank)的情况,这个我们可以叫做「着色器卡顿」。


那么,Flutter 使用 skia 作为渲染引擎时,skia 就会在应用首次打开时去生成着色器,这就很容易造成在设备上首次运行时出现「卡顿」的情况:



如果你的 Flutter 移动应用的动画看起来很「卡顿」,但仅在第一次运行时出现,那么这很可能是由于着色器编译造成。



所以发生这种情况的原因是设备需要编译一些代码(着色器),从而告诉 GPU如何渲染图形 ,那么这时候着色器预热就出现了



着色器预热有点类似于,尽可能让 Flutter 在 skia 上多挤出一点性能的味道。



简单说,就是通过 flutter run --profile --cache-sksl ,让 Flutter 运行时预热(缓存)着色器,然后把导出文件打包到 App,让 Flutter Engine 在启动绘制第一帧之前处理。


所以预热的本质,是将部分性能敏感的 SkSL(Skia Shader Language)生成时间提前放到编译期去处理,所以它需要在真机上进行运行,从而捕获 SkSL 导出配置文件,才能在打包时通过编译参数(--bundle-sksl-path)将导出的 SkSL 预置在应用里面。



SkSL 最终还是需要在运行时转化为平台的 GPU 语言才能执行,因为它与底层 GLSL 或 Metal(MSL) 实现无关,SkSL 可以对信息进行编码,当转换为 GLSL 时,它将使用创建它的 GPU 的一些特有能力,所以 SkSL 与平台无关,但它与功能检测和使用有关,skia 可能会假设,如果在生成时检测到某个功能,它就可以使用该功能。



那么到这里,我们知道了什么是着色器和着色器预热,并且知道为什么 Flutter 会用到着色器预热,事实上着色器预热只是一种补充手段,它不是 Flutter 必须,而是为了解决边界问题而存在的「过渡」手段,但是它又对 Flutter 的未来起到了“推动作用“


那么接下来我们再看原生和 Flutter 的区别。


原生 VS Flutter


那么为什么原生开发的 App 不需要着色器预热,但是 Flutter 上确需要呢?


前面我们知道,着色器预热只是一种「补充手段」,而需要「补充手段」的原因,自然就是 Flutter 不是系统的亲儿子,而原生开发框架,它作为亲儿子,自然就不需要「预热」,因为本来就很「亲热」了


因为在原生开发里,极少需要开发者去「自定义着色器」 ,除非你是做游戏的,不然大部分时候着色器都是提前内置在系统等着你,例如:



iOS 上 Core Animation 所需的着色器,在系统内部对于所有的 App 来说都是共享的,在 Framework 框架内根本不需要考虑运行中的着色器编译问题。



可能有人就奇怪了,不是在说 Android 和 Flutter 么,为什么提 iOS ?因为 Flutter 里的着色器预热,它的存在 90% 都是为了 iOS 而存在


因为在 Android 内部,本身具有二进制着色器持久缓存,而 iOS 没有,所以 iOS 的首次卡顿一直是 Flutter 里的诟病问题。


另外,由于 SkSL 创建过程中需要捕获一些用户设备的特定的参数,不同设备“预热”的配置文件不一定通用,在 Android 上的“预热”可能只会只对某些硬件有效,这也是为什么预热需要在真机上进行的原因之一


最后,使用着色器预热的后期维护成本很高,先不说不同设备/不同系统版本是否通用,就单纯每升级一次 Flutter SDK 就需要做一次新的“预热”这个行为,就很费事:



“预热”要求用户操作整个应用的所需流畅,并点击一些常见动画场景,并且缓存文件也是针对特定的 skia 和 Flutter 版本构建的,还需要为 iOS 和 Android 构建两个不同的缓存文件。



当然,也有类似 flutter drive --profile --cache-sksl 的命令脚本,但是维护这个预热着色器的行为,本身就不是什么长期的选择。


事实上着色器预热这东西不亏是“烂摊子”,在 iOS 平台开始使用 Metal 和 Metal Binary Archive 之后,这个雷区彻底“爆炸”,最终达成了弃坑并推动「换 Impeller」 的壮举



说回 Android vs Flutter,虽然 Android 原生和 Flutter 大家都是使用 skia ,但是除了需不需要预热这个区别外,其实还是存在差异,事实上 Flutter 在 Engine 捆绑了自己的 skia 副本, Flutter 的 skia 版本和 Flutter SDK 有关,于平台无关,所以原生 android skia 和 Flutter skia 还是存在一定差异化。



Flutter also embeds its own copy of Skia as part of the engine


详细可见:http://www.youtube.com/watch?v=OLj…




另外说到 Android skia ,可以在简单聊个题外话,其实在 Honeycomb(3.0) - P(9.0) ,Skia 不一定是你设备的 GPU 直接渲染工具,例如下图是 Android Oreo(8.0) 在开发者模式下的选项。




Android Honeycomb(3.0) 开始用 hwui 取代了部分 skia,hwui 是一个将 Canvas 命令转换为硬件加速 OpenGL 命令的库,所以在到 P 之前,当硬件加速开启时,hwui 会自己进行渲染,而不是 skia ,而从 Oreo(8.0) 开始 hwui 渲染支持 skia opengl 选项,而 P(9.0) 开始支持 vulkan ,另外 P(9.0) 开始 skia 库也不再作为一个单独的动态库 so,而是集成到 hwui 里,成为默认。



好了,这里扯了那么多,大概也了解了为什么原生不需要着色器预热,并且 Flutter 的 skia 和原生 skia 也是存在差异


Impeller


那么时间来到 Impeller , Impeller 其实属于「必然」又「无奈」的产物,在此之前,着色器预热的更新问题推进一直卡在 skia 节点,毕竟 skia 并不是完全服务于 Flutter ,所以随着矛盾的积累,最终只能「搞拆迁」。


与 skia 的不同在于,Impeller 会提前预编译大多数着色器,从而减少渲染延迟,并消除与动态着色器编译相关的卡顿,因为预预编译发生在 Flutter 应用的构建过程里,所以可以确保着色器在应用启动后立即可用



当然,搞拆迁,自然是东拆西补,不少老问题又被翻出来重新回炉。



简单说,例如 Flutter Engine 在构建的时候,它的 Impeller 会使用 impellercentity/shaders/ 目录下的所有着色器进行预处理(SPIR-V),然后再看情况例如编译为 iOS 上的 MSL(Metal SL) ,其中内置着色器就是如下图所示这部分 :



这样 Flutter 也就有了自己的「亲爹」,大部分需要的着色器都在离线时被编译为 shader library,在运行时不再需要什么预热着色器或者编译着色器的操作。


另外在 Impeller 里 Contents 也是很重要的存在,所有绘图信息都要转化为 Entity ,而 Entity 对有对应的 Contents ,用于表示一个绘图操作:



所以,回到前面说个的 iOS Framework 可以使用系统的共享着色器一样,Impeller 放弃了使用 SkSL ,而是使用 GLSL 作为着色器语言,通过 impellerc 在编译期即可将所有的着色器转换为 MSL(Metal SL) , 并使用 MetalLib 格式打包为 AIR 字节码内置在应用中,属于自己「造爹」了。



另外,Impeller 不依赖于特定的客户端渲染 API ,着色器只需编写一次,根据上面的那组固定着色器,提前编译:



  • impellerc (Impeller Shader Compiler ) 将 GLSL 转换为 SPIRV,这个阶段不对生成的 SPIRV 执行任何优化,保留所有调试信息。

  • 使用 SPIRV,后端特定的转译器将 SPIRV 转换为适当的高级着色语言



所以大概就是:



  • impellerc 将 GLSL 代码编译成 SPIRV

  • SPIRV 可用于 OpenGL、OpenGL ES、Metal、Vulkan 等

  • 最终生成的 lib 可以在各种平台上执行。


这里值得一提的是, Flutter 在 DisplayList 层面对 engine 做了解耦,所以 DisplayList 帮助了 Flutter 适配到 skia 和 Impeller 的切换,而 Impeller 上,HAL 帮助 Impeller 适配到 Metal/Vulkan/OpenGLES 等不同管道




详细可见:《2024 Impeller:快速了解 Flutter 的渲染引擎的优势》



所以 Impeller 通过搞「拆迁补偿」,让着色器预编译可以依赖一组比 Skia 更简单的着色器,从而保持应用整体大小不会剧增的效果,得到更好的启动和预热效果。


所以到这里我们知道,Impeller 不需要对 App 代码做着色器预热,因为它给自己找了个「干爹」


Compose MultiPlatform


那么回到 Compose 和 Compose MultiPlatform 上,其实这个问题需要区别对待。


首先 Compose 也是使用 skia ,那么 Compose 是否也需要着色器预热?因为 Compose 本身不也是脱离默认 XML 的独立 UI 吗?那它是否也存在着色器加载问题?


首先这个问题其实很简单,那就是 Compose 是直接使用系统自带的渲染管道,尽管渲染模式和构建树变了,但是它还是在 android.graphics.Canvas 体系之下,Compose 在 Android 并没有内嵌自己的 skia 版本,本质上 View 和 Compose 都是通过 hwui - skia 的过程 。



在不考虑前面说过的 Honeycomb(3.0) 到 P(9.0) 开了硬件加速的情况下。



所以也许早期的 Compose 在 Android 会比较卡,但是作为亲儿子,随着版本迭代,卡的问题自然就会被底层解决,从根本上来说,Android 上的 Compose 根本不需要着色器预热这种「小瘪三」


那么在 iOS 上呢?Compose Multiplatform 的 UI 是通过 Skiko(skia for Kotlin) 实现进行渲染,JetBrains 通过 Kotlin 语言对 skia 做了一层封装,让 Kotlin 在各个平台均可以通过统一的 Kotlin API 来调用 skia 进行图形绘制。


所以理论上 Compose Multiplatform 不直接使用 skia,而是使用 skiko 来访问 skia API,不过因为本质还是 skia 和 SKSL ,所以从目前情况来看,对于 iOS 着色器问题同样是存在



那么 Compose iOS 在未来可能选的有几条路:



  • #3141 所说的,提供着色器预热支持

  • KN 直接编译为 UIKit

  • 接入 Impeller 或者自研发另一个

  • 配合 skia 推进着色器问题


目前 Compose Multiplatform 不管在 SkSL 还是在 warm-up 的推动上都不是很上心,毕竟还有不少其他工作要推动,不过随着 KMP 的支持,Compose Multiplatform 在 iOS 上已经可以与 SwiftUI 和 UIKit 相互操作,所以理论上其实可以在 Swift/UIKit 中使用 Compose,也可以在 Compose 中使用 SwiftUI/UIKit。


所以目前来说 Compose Multiplatform 在 iOS 上还有不少路要走,现在没有“预热”,大概也只是还没走到需要“预热”的瓶颈。


OpenGL & Vulkan & Metal


最后我们聊一聊渲染的底层管道 API,事实上底层渲染管管道 API 对于 App 的性能起到关键作用,以 OpenGL 为例子:



从顶点处理(vertex processing)、图元装配(triangle assembly)、光栅化(rasterization)、片段处理(fragment processing)、测试和混合(testing and blending)这样的 Graphics Pipeline 组成了一个简单的画面渲染流程。




而在这个流程里,光栅化是一个非常耗时的过程,一般是通过 GPU 来加速,而将数据从 CPU 传输到 GPU 的过程也是一个耗时过程。



例如在 Android 里,RenderThread 主要就是从 UI 线程获取输入并将它们处理到 GPU ,RenderThread 是与 GPU 通信的单独线程。




而到了 Metal 和 Vulkan ,它们的出现弥补了 OpenGL 很多历史问题,将渲染性能和执行效率提高了一个层级,举个例子:



  • OpenGL 是单线程模型,所有的渲染操作都放在一个线程;而 Vulkan 中引入了 Command Buffer ,每个线程都可以往 Command Buffer 提交渲染命令,可以更多利用多核多线程的能力

  • OpenGL 很大一部分支持需要驱动的实现,OpenGL 驱动包揽了一大堆工作,在简化上层操作的同时也牺牲了性能;Vulkan 里驱动不再负责跟踪资源和 API 验证,虽然这提高了框架使用的复杂度,但是性能得到了大幅提升


又比如前面 iOS 上的 OpenGL & Metal ,而 Metal 相比 OpenGL 可以“更接近”底层硬件,同时降低了资源开销,例如:



Metal 里资源在 CPU 和 GPU 之间的同步访问是由开发者自己负责,它提供更快捷的资源同步 API,可以有效降低 OpenGL 里纹理和缓冲区复制时的耗时;另外 Metal 使用 GCD 在 CPU 和 GPU 之间保持同步,CPU 和 GPU 是共享内存无需复制就可以交换数据。



可以看到 Vulkan 和 Metal 都给 Android 和 iOS 带来了巨大的性能提升,所以如果讨论渲染实现带来的性能差异,现阶段更多应该是 Vulkan 和 Metal 的差异。


最后,因为 Vulkan 是一个通用的底层渲染 API ,它不止考虑 Android,而 Metal 专职于苹果设备,如果不需要很严谨的对比,那么可以说 Metal 其实更简单,绝大部分实际是 Metal 对 Vulkan 在概念上的合并和简化,例如:



Metal 会自动帮助开发者处理幕后管理工作,它会执行更多自动化操作来处理加速视觉效果和增强性能等后台管理,然而 Vulkan 是更多提供 API,主要取决于开发者自主的控制。



总体而言,Metal 更容易使用,而 Vulkan 更灵活可控,当然,对比 OpenGL 其实都变复杂,特别是 Vulkan ,因为更接近底层,所以复杂度更高。



所以 Flutter 在 Impeller 的第一站选中支持 iOS 的 Metal ,在解决 iOS 问题的同时,Metal 相较于 Vulkan 对于平台更专注且“简单”。



最后


又是一篇「又臭又长」的干文,不知道看完是否对你有所帮助?想来从实用角度而言,这又是一篇没什么用的吃灰类型的内容,不过,话说回来,也许哪天你就突然想用上了呢?


作者:恋猫de小郭
来源:juejin.cn/post/7385942645232828442
收起阅读 »