注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

useReducer : hook 中的响应式状态管理

web
在前端开发中,状态管理是构建复杂应用的核心能力之一,而React作为主流框架,它提供了多种状态管理方案. 然而,随着应用规模扩大,组件层级加深,传统的状态传递方式似乎优点捉襟见肘了,于是,为了解决这种问题,useReducer和useContext诞生了。 今...
继续阅读 »

在前端开发中,状态管理是构建复杂应用的核心能力之一,而React作为主流框架,它提供了多种状态管理方案.


然而,随着应用规模扩大,组件层级加深,传统的状态传递方式似乎优点捉襟见肘了,于是,为了解决这种问题,useReduceruseContext诞生了。


今天,我将从组件通信的不足开始,逐渐深入地讲解如何通过useReducer实现高效、可维护的全局状态管理。





一、组件通信简单介绍


1.1 组件通信的常见方式:



  • 父子组件通信:通过props传递数据,子组件通过props接收父组件的数据。

  • 子父组件通信:子组件通过props传递回调函数(自定义事件)给父组件,实现数据反向传递。

  • 兄弟组件通信:通过父组件作为中间人进行传递数据。

  • 跨层级通信:使用useContext创建共享上下文(Context),直接跨层级传递状态,详细讲解可以看我之前的文章《useContext : hook中跨层级通信的优雅方案》


1.2 Context 的不足:


然而,尽管useContext解决了跨层级传递状态的问题,避免了数据臃肿,但是,它在以下场景中仍存在一些缺陷:



  • 当Context频繁更新时,所有依赖该Context的组件都会重新渲染,即使某些组件并未使用更新后的数据,容易导致性能问题

  • Context能解决标签的跨级传输,然而,多个Context嵌套也会导致组件层级臃肿(比如<LoginContext.Provider>中包裹<ThemeContext.Provider>)。

  • Context本身只提供数据共享能力,它并不涉及到状态更新逻辑,需结合useStateuseReducer使用,这就导致了状态管理分散问题。


因此,当应用状态逻辑变得复杂、需集中管理时,useReducer就成为了更优的选择。




二、useReducer详解


2.1 useReducer的定义与作用


useReducer,响应式状态管理,它是React提供的用于管理复杂状态逻辑的Hook。


useReducer通过将状态(state)交由一个纯函数(reducer)进行统一管理,并通过派发动作(dispatch action)触发状态更新,而非直接修改状态。


2.2 useReducer的参数与返回值


const [state, dispatch] = useReducer(reducer, initialState);


  • 参数1:reducer函数:根据当前状态和传入的action,返回新的状态。

  • 参数2:initialState:初始状态对象。

  • 返回值



    • state:表示当前状态值。

    • dispatch:用于触发状态更新的函数,接受一个action对象作为参数。




2.3 纯函数(Pure Function)


useReducer的参数里面,其中,要求reducer函数必须是一个纯函数


纯函数的特性:



  1. 相同输入,相同输出:给定相同的输入参数,纯函数始终返回相同的结果。

  2. 无副作用:函数内部不修改外部变量、不依赖或修改全局状态、不发起网络请求或操作DOM。

  3. 不可变更新:函数不会直接修改输入参数,而是通过创建新对象或数组返回结果。


举个例子


// 不纯的函数
let total = 0;
function addToTotal(a) {
total += a; // 修改了外部变量
return total;
}

// 纯函数
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }; // 返回新对象
default:
return state;
}
}

代码功能说明



  • addToTotal函数直接修改了外部变量total,导致结果不可预测。

  • reducer函数通过返回新对象的方式更新状态,符合纯函数的要求。




三、用计数器案例讲解useReducer


3.1 代码实现的功能


以下代码实现了一个计数器功能,它通过按钮点击+1-1修改Count值,输入自定义数值后,通过+???按钮,将该数值加到Count上。


效果如下:


mye8k-53wrb.gif
关键代码片段


import { useState,useReducer } from 'react'
import './App.css'

const initialState ={
count :0,
}
//关键代码
const reducer = (state ,action)=>{
switch(action.type){
case 'increment':
return {
count:state.count +1
};
case 'decrement':
return {
count:state.count -1
};
case 'incrementByNum':
return{
count:state.count +parseFloat(action.payload)
}
default:
return state
}
}

function App(){
const [count ,setCount] = useState(0)
const [state, dispatch]= useReducer(reducer, initialState)

return (
<>
<p>Count:{state.count}</p>
<input type="text" value={count} onChange={(e)=>setCount(e.target.value)}/>
<button onClick={()=>dispatch({type:'increment'})}> +1 </button>
<button onClick={()=>dispatch({type:'decrement'})}> -1</button>
<button onClick={()=>dispatch({type:'incrementByNum',payload:count})}> +??? </button>
</>

)
}
export default App

3.2 代码讲解:



  • 在第9行中,reducer函数通过switch语句处理三种类型的action即当触发incrementdecrementincrementByNum行为时,分别返回不同的新的状态对象。

  • dispatch函数用于触发状态更新,例如第35行,dispatch({ type: 'increment' })函数会在我们触发increment行为时,将计数器值增加1。

  • 用户可以通过输入框输入自定义数值,并通过incrementByNum操作将其加到当前计数器上。


关键部分



  1. reducer函数的设计



    • action.type决定了状态更新的逻辑,例如'increment'对应递增操作。

    • action.payload用于传递额外参数(如自定义数值)。



  2. 不可变更新



    • 所有状态更新均通过创建新对象实现(如{ count: state.count + 1 }),而非直接修改state



  3. dispatch的使用



    • dispatch接受一个action对象,触发状态更新。例如,dispatch({ type: 'incrementByNum', payload: inputValue })会将输入框中的值加到计数器上。






四、总结


4.1 useReducer的适用场景



  • 复杂状态逻辑:当状态更新逻辑涉及多个条件分支或嵌套结构时(如计数器的incrementByNum操作)。

  • 集中管理状态:通过将状态更新规则统一到reducer中,避免分散在多个组件或回调函数中。


4.2 实际应用建议



  • 结合useContext:通过useContext创建共享状态,useReducer管理状态更新,形成轻量级全局状态管理方案。

  • 模块化设计:将不同功能的reducer拆分为独立文件(如counterReducer.jsformReducer.js),提升代码可维护性。


作者:轻语呢喃
来源:juejin.cn/post/7527585340145713206
收起阅读 »

kv数据库-leveldb (16) 跨平台封装-环境 (Env)

在上一章 过滤器策略 (FilterPolicy) 中,我们学习了 LevelDB 如何利用布隆过滤器这样的巧妙设计,在访问磁盘前就过滤掉大量不存在的键查询,从而避免了无谓的 I/O 操作。 至此,我们已经探索了 LevelDB 从用户接口到底层数据结构,再到...
继续阅读 »

在上一章 过滤器策略 (FilterPolicy) 中,我们学习了 LevelDB 如何利用布隆过滤器这样的巧妙设计,在访问磁盘前就过滤掉大量不存在的键查询,从而避免了无谓的 I/O 操作。


至此,我们已经探索了 LevelDB 从用户接口到底层数据结构,再到性能优化的几乎所有核心组件。但我们忽略了一个最基础的问题:LevelDB 是一个 C++ 库,它需要运行在真实的操作系统上。它是如何在不同的操作系统(如 Linux, Windows, macOS)上读写文件、创建线程、获取当前时间的呢?难道 LevelDB 的核心代码里充斥着大量的 #ifdef __linux__#ifdef _WIN32 这样的条件编译指令吗?


如果真是这样,代码将会变得难以维护,移植到新平台也会是一场噩梦。为了优雅地解决这个问题,LevelDB 引入了它的基石——环境(Env)。


什么是环境 (Env)?


Env 是对操作系统底层功能的一个抽象层。你可以把它想象成一个万能工具箱。LevelDB 的核心逻辑(比如 合并 (Compaction) 线程、排序字符串表 (SSTable) 的读写)在工作时,并不直接调用操作系统的原生函数(如 open, read, CreateFileW),而是从这个标准的“工具箱”里取工具来用。


这个工具箱里有什么呢?它定义了一套标准的工具接口:



  • NewWritableFile(...): 给我一把能写文件的“扳手”。

  • StartThread(...): 给我一个能启动新线程的“马达”。

  • NowMicros(): 给我一个能读取当前微秒时间的“秒表”。

  • SleepForMicroseconds(...): 让我休息一下的“闹钟”。


有了这个标准的工具箱接口,LevelDB 的核心逻辑就可以完全不关心自己到底运行在哪个操作系统上。它只管向 Env 索要工具。


那么,具体的工具是从哪里来的呢?LevelDB 为每个它支持的平台,都提供了一个具体的工具箱实现



  • 在 Linux/macOS (POSIX) 上,它提供一个 PosixEnv。这个工具箱里的“扳手”是用 open()write() 实现的。

  • 在 Windows 上,它提供一个 WindowsEnv。这个工具箱里的“扳手”则是用 CreateFileA()WriteFile() 实现的。


这种设计带来了巨大的好处:可移植性。当需要将 LevelDB 移植到一个新的操作系统(比如 Fuchsia)时,开发者几乎不需要修改任何核心逻辑代码。他们只需要为新平台实现一个新的 Env 子类——也就是打造一个新的、符合标准的工具箱——然后整个 LevelDB 就可以在这个新平台上运行了。


graph BT
subgraph "具体的平台实现"
C["PosixEnv (Linux, macOS)"]
D["WindowsEnv (Windows)"]
E["MemEnv (用于测试)"]
end

subgraph "LevelDB 核心逻辑"
A["DBImpl, Compaction, SSTable, 等..."]
end

subgraph "Env 抽象接口 (标准工具箱)"
B(Env)
B -- "提供 NewWritableFile()" --> A
B -- "提供 StartThread()" --> A
end



A -- "调用" --> B
C -- "实现" --> B
D -- "实现" --o B
E -- "实现" --o B

style A fill:#cde
style B fill:#f9f

我们如何使用 Env


对于绝大多数用户来说,你几乎不需要直接与 Env 交互。LevelDB 会在后台为你处理好一切。


当你打开一个数据库时,选项 (Options) 对象里有一个 env 成员。如果你不设置它,它的默认值就是 Env::Default()


Env::Default() 是一个静态方法,它会根据编译时确定的操作系统,返回一个对应平台的 Env 单例对象。在 Linux 上,它返回 PosixEnv 的实例;在 Windows 上,它返回 WindowsEnv 的实例。


#include "leveldb/db.h"
#include "leveldb/env.h"

int main() {
leveldb::Options options;

// 我们没有设置 options.env,
// 所以 LevelDB 会自动使用 Env::Default()
// 在 Linux 上就是 PosixEnv,在 Windows 上就是 WindowsEnv

leveldb::DB* db;
// DB::Open 内部会从 options.env 获取环境对象,
// 并在需要时用它来操作文件、启动线程等。
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);

// ...

delete db;
return 0;
}

所以,Env 虽然至关重要,但它就像空气一样,默默地支撑着一切,而我们通常感觉不到它的存在。


Env 内部是如何工作的?


Env 的强大之处在于它的多态设计。Env 本身是一个抽象基类,定义了所有平台都需要提供的功能接口。


1. Env 的接口定义 (include/leveldb/env.h)


Env 类定义了许多纯虚函数(以 = 0 结尾),这意味着任何想要成为一个“合格” Env 的子类都必须实现这些函数。


// 来自 include/leveldb/env.h (简化后)
class LEVELDB_EXPORT Env {
public:
virtual ~Env();

// 返回一个适合当前操作系统的默认 Env
static Env* Default();

// 创建一个用于顺序读取的文件对象
virtual Status NewSequentialFile(const std::string& fname,
SequentialFile** result)
= 0;

// 创建一个用于随机读取的文件对象
virtual Status NewRandomAccessFile(const std::string& fname,
RandomAccessFile** result)
= 0;

// 创建一个用于写操作的文件对象
virtual Status NewWritableFile(const std::string& fname,
WritableFile** result)
= 0;

// 启动一个新线程
virtual void StartThread(void (*function)(void* arg), void* arg) = 0;

// 返回当前的微秒时间戳
virtual uint64_t NowMicros() = 0;
// ... 还有很多其他接口, 如文件删除、目录创建等 ...
};

这个接口就是 LevelDB 核心逻辑所依赖的“标准工具箱”的蓝图。


2. POSIX 平台的实现 (util/env_posix.cc)


PosixEnv 类继承自 Env,并使用 POSIX 标准的系统调用来实现这些接口。


让我们看看 NewWritableFile 的实现:


// 来自 util/env_posix.cc (简化后)
Status PosixEnv::NewWritableFile(const std::string& filename,
WritableFile** result)
{
// 使用 POSIX 的 open() 系统调用来创建文件
int fd = ::open(filename.c_str(),
O_TRUNC | O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
*result = nullptr;
return PosixError(filename, errno); // 返回错误状态
}

// 创建一个 PosixWritableFile 对象来包装文件描述符
*result = new PosixWritableFile(filename, fd);
return Status::OK();
}

这里,PosixEnv 将对“写文件”这个抽象请求,转换成了对 ::open() 这个具体的 POSIX 系统调用。


3. Windows 平台的实现 (util/env_windows.cc)


与之对应,WindowsEnv 则使用 Windows API 来实现同样的功能。


// 来自 util/env_windows.cc (简化后)
Status WindowsEnv::NewWritableFile(const std::string& filename,
WritableFile** result)
{
// 使用 Windows API 的 CreateFileA() 来创建文件
ScopedHandle handle = ::CreateFileA(
filename.c_str(), GENERIC_WRITE, /*share_mode=*/0,
/*security=*/nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
/*template=*/nullptr);
if (!handle.is_valid()) {
*result = nullptr;
return WindowsError(filename, ::GetLastError());
}

// 创建一个 WindowsWritableFile 对象来包装文件句柄
*result = new WindowsWritableFile(filename, std::move(handle));
return Status::OK();
}

WindowsEnv 将同样的抽象请求,转换成了对 ::CreateFileA() 这个具体的 Windows API 调用。LevelDB 的上层代码完全不知道也不关心这些差异。


Env::Default() 的魔法


Env::Default() 是如何知道该返回哪个实现的呢?这通常是通过编译时的预处理宏来完成的。


// 位于 env.cc 或平台相关的 env_*.cc 文件中 (概念简化)
#include "leveldb/env.h"

#if defined(LEVELDB_PLATFORM_POSIX)
#include "util/env_posix.h"
#elif defined(LEVELDB_PLATFORM_WINDOWS)
#include "util/env_windows.h"
#endif

namespace leveldb {
Env* Env::Default() {
// 静态变量保证了全局只有一个实例
static SingletonEnv<
#if defined(LEVELDB_PLATFORM_POSIX)
PosixEnv
#elif defined(LEVELDB_PLATFORM_WINDOWS)
WindowsEnv
#else
// Fallback or error for unsupported platforms
#endif
> env_container;
return env_container.env();
}
} // namespace leveldb

在编译时,构建系统会根据目标平台定义 LEVELDB_PLATFORM_POSIXLEVELDB_PLATFORM_WINDOWS,从而使得 Env::Default() 的代码在编译后,就“硬编码”为返回正确的平台特定 Env 实例。


用于测试的 MemEnv


Env 抽象层的另一个巨大好处是可测试性。LevelDB 提供了一个完全在内存中模拟文件系统的 MemEnv(位于 helpers/memenv/memenv.h)。在进行单元测试时,可以使用 MemEnv 来代替真实的 PosixEnvWindowsEnv。这使得测试可以:



  • 非常快:因为没有实际的磁盘 I/O。

  • 完全隔离:不会在文件系统上留下任何垃圾文件。

  • 可控:可以方便地模拟文件读写错误等异常情况。


总结与回顾


在本章中,我们探索了 LevelDB 的根基——Env 环境抽象层。



  • Env 是一个对操作系统功能的抽象接口,它将 LevelDB 的核心逻辑与具体的平台实现解耦。

  • 这个“万能工具箱”的设计使得 LevelDB 具有极高的可移植性

  • 我们通常通过 Env::Default() 间接使用它,它会自动返回适合当前操作系统的 Env 实现(如 PosixEnvWindowsEnv)。

  • Env 的抽象也使得编写快速、隔离的单元测试成为可能,例如使用内存文件系统 MemEnv


至此,我们已经完成了 LevelDB 核心概念的探索之旅!让我们一起回顾一下走过的路:


我们从最基础的数据表示 数据切片 (Slice) 开始,学习了如何通过 选项 (Options)] 配置我们的 数据库实例 (DB)。我们掌握了如何使用 批量写 (WriteBatch)迭代器 (Iterator) 与数据库高效交互。


然后,我们深入内部,揭开了数据持久化的第一道防线 预写日志 (Log / WAL),看到了数据在内存中的临时住所 内存表 (MemTable),并最终见证了它们在磁盘上的永久归宿 排序字符串表 (SSTable)。我们理解了 LevelDB 是如何通过后台的 合并 (Compaction) 任务来保持整洁,以及如何通过 版本集 (VersionSet / Version) 来管理数据快照。


我们还深入到了 SSTable 的微观世界,探索了 数据块 (Block) 的紧凑结构,并了解了 缓存 (Cache) 如何为读取加速。我们学会了用 比较器 (Comparator) 定义秩序,用 过滤器策略 (FilterPolicy) 避免无效查询。最后,我们认识了支撑这一切的平台基石 环境 (Env)。


希望这个系列能帮助你建立起对 LevelDB 内部工作原理的清晰理解。现在,你不仅知道如何使用 LevelDB,更重要的是,你明白了它为何能如此高效、稳定地工作。恭喜你完成了这段旅程!


作者:重启的码农
来源:juejin.cn/post/7554961105325129771
收起阅读 »

Spec-Kit WBS:技术团队的项目管理新方式

Spec-Kit WBS:技术团队的项目管理新方式 📋 WBS基本概念 什么是WBS? WBS (Work Breakdown Structure) = 工作分解结构 定义: 将项目可交付成果和项目工作分解成较小的、更易于管理的组件的过程 目标: 确保项目范...
继续阅读 »

Spec-Kit WBS:技术团队的项目管理新方式


📋 WBS基本概念


什么是WBS?


WBS (Work Breakdown Structure) = 工作分解结构



  • 定义: 将项目可交付成果和项目工作分解成较小的、更易于管理的组件的过程

  • 目标: 确保项目范围完整,工作不遗漏,便于估算、计划、执行和控制

  • 本质: 把复杂项目像搭积木一样,一层一层地分解成可管理的小任务


WBS的核心价值



  1. 完整性保证 - 确保所有工作都被识别和分解

  2. 可管理性 - 将复杂项目分解为可管理的小任务

  3. 责任分配 - 每个任务可以分配给特定的人员

  4. 进度跟踪 - 可以跟踪每个任务的完成状态

  5. 成本估算 - 每个任务可以估算时间和成本


🔄 WBS与PDCA的关系


image.png


PDCA循环在项目管理中的应用


Plan (计划)
├── 项目范围定义
├── WBS创建 ← 关键工具
├── 时间估算
├── 资源分配
└── 风险管理

Do (执行)
├── 按WBS执行任务
├── 团队协作
├── 质量保证
└── 进度跟踪

Check (检查)
├── 里程碑检查
├── 质量审查
├── 进度评估
└── 偏差分析

Act (行动)
├── 纠正措施
├── 预防措施
├── 经验总结
└── 流程改进

WBS与PDCA的协同效应


image.png


关键理解: WBS是PDCA循环中Plan阶段的核心工具,它将抽象的项目目标转化为具体的、可执行的任务,确保项目管理的系统性和完整性。


🏗️ WBS实际示例:开发一个电商网站


image.png


1. 项目概述


项目名称: 开发一个在线购物网站

项目目标: 让用户可以浏览商品、下单购买、管理账户


2. WBS分解过程


image.png


第一层:主要阶段

电商网站项目
├── 1. 需求分析阶段
├── 2. 设计阶段
├── 3. 开发阶段
├── 4. 测试阶段
└── 5. 部署上线阶段

第二层:每个阶段的工作包

电商网站项目
├── 1. 需求分析阶段
│ ├── 1.1 用户需求调研
│ ├── 1.2 功能需求分析
│ └── 1.3 技术需求分析
├── 2. 设计阶段
│ ├── 2.1 界面设计
│ ├── 2.2 数据库设计
│ └── 2.3 系统架构设计
├── 3. 开发阶段
│ ├── 3.1 前端开发
│ ├── 3.2 后端开发
│ └── 3.3 数据库开发
├── 4. 测试阶段
│ ├── 4.1 功能测试
│ ├── 4.2 性能测试
│ └── 4.3 安全测试
└── 5. 部署上线阶段
├── 5.1 服务器配置
├── 5.2 数据迁移
└── 5.3 上线发布

第三层:具体活动(最详细的任务)

电商网站项目
├── 1. 需求分析阶段
│ ├── 1.1 用户需求调研
│ │ ├── 1.1.1 设计用户问卷
│ │ ├── 1.1.2 进行用户访谈
│ │ └── 1.1.3 分析用户反馈
│ ├── 1.2 功能需求分析
│ │ ├── 1.2.1 列出所有功能点
│ │ ├── 1.2.2 确定功能优先级
│ │ └── 1.2.3 编写需求文档
│ └── 1.3 技术需求分析
│ ├── 1.3.1 确定技术栈
│ ├── 1.3.2 评估性能要求
│ └── 1.3.3 制定技术方案
├── 2. 设计阶段
│ ├── 2.1 界面设计
│ │ ├── 2.1.1 设计首页布局
│ │ ├── 2.1.2 设计商品列表页
│ │ ├── 2.1.3 设计购物车页面
│ │ └── 2.1.4 设计用户中心
│ ├── 2.2 数据库设计
│ │ ├── 2.2.1 设计用户表
│ │ ├── 2.2.2 设计商品表
│ │ ├── 2.2.3 设计订单表
│ │ └── 2.2.4 设计购物车表
│ └── 2.3 系统架构设计
│ ├── 2.3.1 设计整体架构
│ ├── 2.3.2 设计API接口
│ └── 2.3.3 设计安全方案
├── 3. 开发阶段
│ ├── 3.1 前端开发
│ │ ├── 3.1.1 搭建前端框架
│ │ ├── 3.1.2 开发首页组件
│ │ ├── 3.1.3 开发商品展示组件
│ │ ├── 3.1.4 开发购物车组件
│ │ └── 3.1.5 开发用户中心组件
│ ├── 3.2 后端开发
│ │ ├── 3.2.1 搭建后端框架
│ │ ├── 3.2.2 开发用户管理API
│ │ ├── 3.2.3 开发商品管理API
│ │ ├── 3.2.4 开发订单管理API
│ │ └── 3.2.5 开发支付接口
│ └── 3.3 数据库开发
│ ├── 3.3.1 创建数据库
│ ├── 3.3.2 创建数据表
│ ├── 3.3.3 插入测试数据
│ └── 3.3.4 优化数据库性能
├── 4. 测试阶段
│ ├── 4.1 功能测试
│ │ ├── 4.1.1 测试用户注册登录
│ │ ├── 4.1.2 测试商品浏览功能
│ │ ├── 4.1.3 测试购物车功能
│ │ └── 4.1.4 测试下单支付功能
│ ├── 4.2 性能测试
│ │ ├── 4.2.1 测试页面加载速度
│ │ ├── 4.2.2 测试并发用户处理
│ │ └── 4.2.3 测试数据库查询性能
│ └── 4.3 安全测试
│ ├── 4.3.1 测试SQL注入防护
│ ├── 4.3.2 测试XSS攻击防护
│ └── 4.3.3 测试用户数据安全
└── 5. 部署上线阶段
├── 5.1 服务器配置
│ ├── 5.1.1 购买云服务器
│ ├── 5.1.2 配置服务器环境
│ └── 5.1.3 安装必要软件
├── 5.2 数据迁移
│ ├── 5.2.1 备份开发数据
│ ├── 5.2.2 迁移到生产环境
│ └── 5.2.3 验证数据完整性
└── 5.3 上线发布
├── 5.3.1 部署代码到服务器
├── 5.3.2 配置域名和SSL
└── 5.3.3 监控系统运行状态

3. WBS编号规则


1. 第一层:1, 2, 3, 4, 5 (主要阶段)
2. 第二层:1.1, 1.2, 1.3 (工作包)
3. 第三层:1.1.1, 1.1.2, 1.1.3 (具体活动)

4. WBS与PDCA的结合


image.png


Plan阶段 (创建WBS)

✅ 1.1.1 设计用户问卷
✅ 1.1.2 进行用户访谈
✅ 1.1.3 分析用户反馈

Do阶段 (执行WBS)

🔄 2.1.1 设计首页布局
🔄 2.1.2 设计商品列表页
⏳ 2.1.3 设计购物车页面

Check阶段 (检查WBS)

✅ 4.1.1 测试用户注册登录 - 通过
✅ 4.1.2 测试商品浏览功能 - 通过
❌ 4.1.3 测试购物车功能 - 发现bug

Act阶段 (改进WBS)

🔧 修复购物车bug
📝 更新测试用例
🔄 重新测试购物车功能

5. 实际项目管理中的应用


image.png


任务分配表

任务编号任务名称负责人开始时间结束时间状态
1.1.1设计用户问卷产品经理2024-01-012024-01-03✅完成
1.1.2进行用户访谈产品经理2024-01-042024-01-10🔄进行中
1.1.3分析用户反馈产品经理2024-01-112024-01-15⏳待开始
2.1.1设计首页布局UI设计师2024-01-162024-01-20⏳待开始

进度跟踪

项目进度: 15%
├── 需求分析阶段: 60% (3/5个任务完成)
├── 设计阶段: 0% (0/8个任务开始)
├── 开发阶段: 0% (0/12个任务开始)
├── 测试阶段: 0% (0/9个任务开始)
└── 部署阶段: 0% (0/8个任务开始)

🎯 WBS的优势体现


image.png


A. 完整性



  • ✅ 确保所有工作都被识别

  • ✅ 不会遗漏重要任务

  • ✅ 项目范围清晰


B. 可管理性



  • ✅ 每个任务都有明确的交付物

  • ✅ 任务大小适中,便于管理

  • ✅ 可以分配给不同的人员


C. 可跟踪性



  • ✅ 可以跟踪每个任务的进度

  • ✅ 识别瓶颈和风险点

  • ✅ 及时调整计划


D. 可估算性



  • ✅ 每个任务可以估算时间和成本

  • ✅ 便于制定项目预算

  • ✅ 便于资源分配


E. 责任分配



  • ✅ 每个任务可以分配给特定的人员

  • ✅ 明确的责任分工

  • ✅ 便于团队协作


🔧 WBS在Spec-Kit中的应用


image.png


传统WBS vs Spec-Kit WBS


核心区别对比

分解思路



  • 传统WBS:按项目阶段分解(需求→设计→开发→测试→部署)

  • Spec-Kit WBS:按技术实现分解(环境→测试→实现→集成→完善)


测试策略



  • 传统WBS:测试放在最后,问题发现太晚

  • Spec-Kit WBS:测试先行(TDD),质量更有保障


任务标识



  • 传统WBS:无特殊标识,按顺序执行

  • Spec-Kit WBS:[P]标识并行任务,提高开发效率


适用场景



  • 传统WBS:通用项目管理(建筑、市场、产品发布)

  • Spec-Kit WBS:软件开发项目(API开发、系统集成、技术重构)


文件管理



  • 传统WBS:通用描述,适合各种项目

  • Spec-Kit WBS:具体文件路径,便于开发执行


传统WBS

电商网站项目
├── 1. 需求分析 (5个任务)
├── 2. 设计 (8个任务)
├── 3. 开发 (12个任务)
├── 4. 测试 (9个任务)
└── 5. 部署 (8个任务)

Spec-Kit的WBS

联调12个接口
├── 阶段 3.1: 环境设置 (3个任务)
├── 阶段 3.2: 测试先行 (13个任务) [P]
├── 阶段 3.3: 核心实现 (14个任务)
├── 阶段 3.4: 集成 (4个任务)
└── 阶段 3.5: 完善 (4个任务)

Spec-Kit WBS的特点



  1. 技术实现导向 - 更注重技术实现细节

  2. 测试先行 - 强调TDD (Test-Driven Development)

  3. 并行任务标识 - 明确标识可并行执行的任务 [P]

  4. 具体文件路径 - 每个任务都有明确的文件路径

  5. 依赖关系管理 - 清晰定义任务间的依赖关系


Spec-Kit WBS示例


联调12个接口
├── 阶段 3.1: 环境与项目设置
├── T001: 创建目录结构
├── T002: 初始化项目
└── T003 [P]: 配置工具
├── 阶段 3.2: 测试先行 (TDD)
├── T004-T015: 12个接口的合约测试 [P]
└── T016: 集成测试
├── 阶段 3.3: 核心实现
├── T017-T018: 数据模型和服务层
└── T019-T030: 12个接口实现
├── 阶段 3.4: 集成
├── T031-T033: 服务连接和配置
└── T034: 集成测试
└── 阶段 3.5: 完善
├── T035-T037: 测试和文档
└── T038: 最终验证

📝 总结


image.png


WBS是项目管理的核心工具,它将复杂的项目分解为可管理的小任务。与PDCA循环结合使用,可以确保项目的系统性、完整性和可跟踪性。


关键要点:



  • WBS是PDCA循环中Plan阶段的核心工具

  • 通过层次化分解确保项目完整性

  • 每个任务都有明确的交付物和责任人

  • 支持进度跟踪和风险管理

  • 在Spec-Kit中与规范驱动开发完美结合


实际应用建议:



  1. 从项目目标开始,逐层分解

  2. 确保每个任务都有明确的交付物

  3. 合理分配任务给团队成员

  4. 定期检查进度,及时调整计划

  5. 总结经验,持续改进WBS模板


Changelog


V1.0 (2025-09-29)



  • [新增] 初稿完成 - 文档基础框架建立

  • [新增] 初稿完成 - 基础版本

  • [新增] 添加WBS基本概念和实际应用示例

  • [新增] 新增传统WBS vs Spec-Kit WBS对比分析

  • [新增] 完善文档结构和可读性 - 用户体验


作者:暖阳_
来源:juejin.cn/post/7555327916483870774
收起阅读 »

el-table实现可编辑表格的开发历程

web
写在前面的话   想直接看代码的朋友可以省略下面的历程直接翻到最底下,我把完整示例代码放在最下面 引子   笔者最近在做项目中遇到了一件事,某个迭代我们需要对项目进行UI改造,特别是把当前正在使用的一个可编辑表格换一下UI。说是换UI,其实是换表格,因为当前在...
继续阅读 »

写在前面的话


  想直接看代码的朋友可以省略下面的历程直接翻到最底下,我把完整示例代码放在最下面


引子


  笔者最近在做项目中遇到了一件事,某个迭代我们需要对项目进行UI改造,特别是把当前正在使用的一个可编辑表格换一下UI。说是换UI,其实是换表格,因为当前在用的表格组件是项目组花钱买的,但老板应该是对这个组件的UI有别的想法(其实就是觉得丑),然后经过老大的决定,我们需要换成Element-UI的组件(Element打钱~~ )。
  虽说组件要换,但是我们要尽可能的保留原先的功能,原来的组件,在使用上面非常贴近于Excel表格。然后,笔者开始库库干了。。。


初步实现


  为了快速实现功能,我们首先选择的是把这个可编辑表格的所有编辑项全部展示出来,这样用户就可以直接进行表格的编辑,就像这样:


PixPin_2025-09-23_01-08-19.gif
  但很快,我们就发现了第一个问题。
  我们的表格中,有两列下拉框使用了远端搜索功能,同时使用了一个封装的下拉选择组件。这就使得当下拉框有值的时候,它会尝试用value在下拉选项中去匹配对应的label,而下拉选项需要通过远端搜索即调接口获取。这两列调的是同一个接口,哪怕这里做了分页并且默认一页10条,仍然默认会调同一个接口20次,这是一个很影响性能的问题,如果切换成一页20条、30条、50条的话,后果不堪设想。。。
  考虑到这种情况,我们首先采取的方法是只调一次接口,把选项数据全部拉回来本地,然后让使用这些选项的下拉框直接引用。但在这里,我们又发现,这些下拉框是这样的:


PixPin_2025-09-23_01-47-28.gif
  是的,label和value同时展示出来,而且在远程搜索中,可以搜索label或value来找对应项。
  那这里我们就得使用filter-method自定义搜索方法咯,但这里有个问题,那就是搜索结果得要是独立的才行,即:第一次搜索的选项结果,不能出现在第二次的搜索选项里,意思就是每次搜索完,需要把选项还原到默认状态。这好办,visible-change事件可以实现。
  当我们把实现的功能交付出去后,产品给我们带来了一个噩耗:用户非得要跟Excel一样的,也就是说为了满足用户的使用习惯,我们需要尽可能还原出原来的表格组件来


image.png


解决之道


第一步


  事已至此,先吃饭吧,啊,不是,先百度吧
  在某次冲浪中,我发现了一篇文章,里面提到使用el-table组件的cell-dblclick事件来实现双击进入编辑状态的做法,也就是下面这样:
  通过列的prop和行的id一起来定位到双击选中的单元格的位置,然后通过v-if使得输入框渲染出来


PixPin_2025-09-23_02-48-59.gif


<template>
<el-table
:data="tableData"
style="width: 100%"
@cell-dblclick="cellDblclick"
>

<el-table-column prop="name" label="姓名" width="180">
<template slot-scope="scope">
<el-input v-if="formViewMethod(scope)" v-model="scope.row.name"></el-input>
<span v-else>{{ scope.row.name }}</span>
</template>
</el-table-column>
</el-table>

</template>
<script>
export default {
data() {
return {
editColumnProp: null,
editRowId: null
}
},
methods: {
cellDblclick(row, column, cell) {
cell.style.background = 'pink'
this.editColumnProp = column.property
this.editRowId = row.id
},
formViewMethod(scope) {
const { row, column } = scope
return (
row.id === this.editRowId &&
this.editColumnProp === column.property
)
}
}
}
</script>


  这方法确实可行!在默认是text的情况下,也就不会去调接口,这样,哪怕是用回远端搜索功能,也能保证对性能没有那种压力。
  第一步走出了,另一个问题就摆在眼前了:当我点击编辑框以外的地方,该怎么让它恢复默认那种文本状态呢?文章的作者并没有给出答案,那就得自己去寻找了


新的曙光


  最近在冲浪中,我了解到有一个名为ClickOutside的指令,这是一个vue3中的自定义指令,顾名思义,就是点击外面的意思。这下子灵感就来了:在cell-dblclick事件中,我们可以获取到当前单元格的dom,那如果我们在获取dom的时候,给它加上一个点击事件,当点击到外面的时候,就清空当前单元格的选中状态,那是不是就可以实现了呢?说干就干,上代码:


cellDblclick(row, column, cell) {
cell.style.background = 'pink'
cell.__clickOutside__ = (e) => {
if (cell.contains(e.target)) {
return console.log('点击了自己')
}
console.log('点击了外面')
cell.__clickOutside__ && document.removeEventListener('click', cell.__clickOutside__)
}
document.addEventListener('click', cell.__clickOutside__)
this.editColumnProp = column.property
this.editRowId = row.id
}

在这里我们使用了dom的contains()方法,这个方法用于检测一个元素是否包含另一个元素,返回的是一个布尔值。也就是当点击的时候,判断被点击元素B是否在双击的时候绑定点击事件的元素A之内,如果返回true的话,就是点击自己了,否则就是点击外面,这样就能实现清空选中状态的方法了。就像下面这样子:


PixPin_2025-09-23_03-54-37.gif
到这里,可编辑表格的功能就算实现了,谢谢大家观看,下面会贴上完整的示例代码,大伙儿可以直接复制粘贴来看看效果。


完整代码


<template>
<div class="irregular-table-container">
<div class="custom-table">
<!-- 表格区域 -->
<el-table
:data="tableData"
style="width: 100%"
@cell-dblclick="cellDblclick"
>

<el-table-column
prop="date"
label="日期"
width="180">

</el-table-column>
<el-table-column prop="name" label="姓名" width="180">
<template slot-scope="scope">
<el-input v-if="formViewMethod(scope)" v-model="scope.row.name"></el-input>
<span v-else>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column
prop="gender"
label="性别"
width="120"
:formatter="formatGender"
>

</el-table-column>
<el-table-column
prop="city"
label="城市"
width="200"
:formatter="formatCity"
>

</el-table-column>
<el-table-column prop="address" label="地址"/>
</el-table>
</div>
</div>

</template>

<script>
export default {
name: "EditableTable",
data() {
return {
tableData: [
{
id: 1,
date: '2016-05-02',
name: '王小虎',
gender: '男',
city: 'Beijing',
address: '上海市普陀区金沙江路 1518 弄'
}, {
id: 2,
date: '2016-05-04',
name: '王小虎',
gender: '男',
city: 'Nanjing',
address: '上海市普陀区金沙江路 1517 弄'
}, {
id: 3,
date: '2016-05-01',
name: '王小虎',
gender: '男',
city: 'Guangzhou',
address: '上海市普陀区金沙江路 1519 弄'
}, {
id: 4,
date: '2016-05-03',
name: '王小虎',
gender: '男',
city: 'Shanghai',
address: '上海市普陀区金沙江路 1516 弄'
}
],
options: [
{ label: '男', value: 1 },
{ label: '女', value: 0 }
],
cities: [
{
value: 'Beijing',
label: '北京'
}, {
value: 'Shanghai',
label: '上海'
}, {
value: 'Nanjing',
label: '南京'
}, {
value: 'Chengdu',
label: '成都'
}, {
value: 'Shenzhen',
label: '深圳'
}, {
value: 'Guangzhou',
label: '广州'
}
],
editColumnProp: null,
editRowId: null
}
},
computed: {},
created() {},
methods: {
formatName(row) {
const input = (
<el-input v-model={row.name} clearable />
)
return input
},
formatGender(row) {
const select = (
<el-select v-model={row.gender}>
{this.options.map(item => {
return (
<el-option
key={item.value}
label={item.label}
value={item.value}
/>

)
})}
</el-select>

)
return select
},
formatCity(row) {
const select = (
<el-select v-model={row.city}>
{this.cities.map(item => {
return (
<el-option
key={item.value}
label={item.label}
value={item.value}
>

<span style="float: left">{ item.label }</span>
<span style="float: right; color: #8492a6; font-size: 13px">{ item.value }</span>
</el-option>
)
})}
</el-select>

)
return select
},
cellDblclick(row, column, cell) {
cell.style.background = 'pink'
cell.__clickOutside__ = (e) => {
if (cell.contains(e.target)) {
return console.log('点击了自己')
}
// console.log('点击了外面')
this.editColumnProp = null
this.editRowId = null
cell.__clickOutside__ && document.removeEventListener('click', cell.__clickOutside__)
}
document.addEventListener('click', cell.__clickOutside__)
this.editColumnProp = column.property
this.editRowId = row.id
},
formViewMethod(scope) {
const { row, column } = scope
return (
row.id === this.editRowId &&
this.editColumnProp === column.property
)
}
}
}
</script>



感谢名单


  写完一看时间,嚯,好家伙,凌晨4点了,赶紧碎觉,狗命要紧~~
  最后的最后,这里要感谢两位给我提供灵感和思路的大大,我把他们的文章链接放到下面了,感兴趣的小伙伴可以过去学习下。
vue对el-table的二次封装,双击单元格编辑,避免表格输入框过多卡顿
vue自定义指令(v-clickoutside)-点击当前区域之外的位置


作者:午言
来源:juejin.cn/post/7552789573735907328
收起阅读 »

event.currentTarget 、event.target 傻傻分不清楚?

web
在前端开发中,事件处理是交互逻辑的核心。但你是否会遇到这样的困惑:绑定事件时明明用的是父元素,触发时却总获取到子元素的信息?或是想优化大量子元素的事件绑定,却不知从何下手? 这一切的答案,都藏在 event.currentTarget和 event.targe...
继续阅读 »

在前端开发中,事件处理是交互逻辑的核心。但你是否会遇到这样的困惑:绑定事件时明明用的是父元素,触发时却总获取到子元素的信息?或是想优化大量子元素的事件绑定,却不知从何下手?


这一切的答案,都藏在 event.currentTarget和 event.target这对“双胞胎”属性里。


一、核心概念:谁在触发?谁在处理?


要理解这两个属性,首先需要明确事件流的基本概念。当用户与页面交互(如点击)时,事件会经历 ​捕获阶段 → 目标阶段 → 冒泡阶段​ 传播。而 event.currentTarget和 event.target的差异,正源于它们在这场“事件旅行”中的不同角色。


1. event.target:事件的“起点”


定义​:触发事件的最深层元素,即用户实际交互的对象。


特点​:



  • 从事件触发到结束,始终指向最初的“罪魁祸首”(即使事件冒泡到父元素,它也不会变)。

  • 可能是按钮、文本节点,甚至是动态生成的元素。


示例​:点击一个嵌套的 <div>内部的 <span>event.target始终是 <span>


2. event.currentTarget:事件的“处理者”


定义​:当前正在执行事件处理程序的元素,即绑定事件监听器的那个元素。


特点​:



  • 随着事件在捕获/冒泡阶段流动,它的值会动态变化(从外层元素逐渐向内,或从内层向外)。

  • 在非箭头函数的回调中,this等价于 event.currentTarget


示例​:父元素绑定点击事件,子元素被点击时,父元素的回调函数中 event.currentTarget是父元素,而 event.target是子元素。


二、一张图看懂:事件流中的身份切换


为了更直观理解二者的差异,我们通过一个三层嵌套结构的交互演示:


<div id="outer" class="box">外层(绑定事件)
<div id="middle" class="box">中层
<div id="inner" class="box">内层(点击我)</div>
</div>
</div>

当点击最内层的 inner元素时,事件流的三个阶段中,currentTarget和 target的变化如下:


阶段event.currentTarget(处理者)event.target(触发者)
捕获阶段outer → middle → innerinner(始终不变)
目标阶段innerinner
冒泡阶段inner → middle → outerinner(始终不变)

关键结论​:



  • target是“事件的源头”,永远指向用户点击的那个元素。

  • currentTarget是“事件的搬运工”,随事件传播阶段变化,指向当前处理事件的元素。


三、为什么需要事件委托?用差异解决实际问题


传统事件绑定方式为每个子元素单独添加监听器,但在动态列表、表格等场景下,这会导致 ​内存浪费动态元素难维护代码冗余​ 三大痛点。而事件委托的出现,正是利用 currentTarget和 target的差异,提供了一种“集中管理、按需处理”的优化方案。


事件委托的核心逻辑


原理​:将子元素的事件监听绑定在父元素上,利用事件冒泡机制,由父元素统一处理子元素的事件。


关键依赖​:



  • 父元素(currentTarget)负责接收事件。

  • 通过 event.target识别实际触发的子元素,执行针对性逻辑。


经典场景实战


场景 1:动态待办列表的点击交互


需求:点击待办项标记完成,支持动态添加新任务。


传统方式的问题​:每次新增任务都要重新绑定事件,代码冗余且易出错。


事件委托方案​:


<ul id="todoList">
<li class="todo-item">任务 1(点击标记完成)</li>
<li class="todo-item">任务 2(点击标记完成)</li>
</ul>
<button id="addTodo">添加新任务</button>

const todoList = document.getElementById('todoList');
const addTodoBtn = document.getElementById('addTodo');

// 父元素 todoList 绑定唯一点击事件(冒泡阶段)
todoList.addEventListener('click', function(event) {
// event.target 是实际点击的元素(可能是 li 或其子元素)
const target = event.target.closest('.todo-item'); // 向上查找最近的 li

if (!target) return; // 非目标元素,跳过

// 标记完成(切换类名)
target.classList.toggle('completed');

// 若点击删除按钮(假设子元素有 .delete-btn)
if (target.querySelector('.delete-btn')) {
target.remove(); // 直接删除父元素 li
}
});

// 动态添加新任务(无需重新绑定事件)
addTodoBtn.addEventListener('click', () => {
const newTodo = document.createElement('li');
newTodo.className = 'todo-item';
newTodo.innerHTML = `新任务 ${Date.now()} <button class="delete-btn">删除</button>`;
todoList.appendChild(newTodo);
});

优势​:



  • 仅需绑定一次父元素事件,内存占用极低。

  • 新增任务自动继承事件处理能力,无需额外代码。


场景 2:表格单元格的双击编辑


需求:双击表格单元格(td)转换为输入框编辑。


事件委托方案​:


<table id="dataTable">
<thead><tr><th>ID</th><th>名称</th></tr></thead>
<tbody>
<tr><td>1</td><td>苹果</td></tr>
<tr><td>2</td><td>香蕉</td></tr>
</tbody>
</table>

const dataTable = document.getElementById('dataTable');

// 父元素 tbody 监听双击事件(冒泡到 tbody)
dataTable.addEventListener('dblclick', function(event) {
// event.target 是实际双击的元素(可能是文本或 td)
const td = event.target.closest('td');
if (!td) return;

// 转换为输入框编辑
const originalText = td.textContent;
td.innerHTML = `<input type="text" value="${originalText}" class="edit-input">`;

const input = td.querySelector('.edit-input');
input.focus();

// 输入完成后保存(监听输入框失焦)
input.addEventListener('blur', () => {
td.textContent = input.value;
});
});

优势​:



  • 无论表格有多少行,只需绑定一次 tbody事件。

  • 新增行(如 AJAX 加载数据后插入)自动支持编辑功能。


四、避坑指南:事件委托的注意事项



  1. 选择合适的父元素


    父元素应尽可能靠近目标子元素(如列表用 ul而非 body),避免不必要的事件判断逻辑,减少性能损耗。


  2. 精确过滤目标元素


    使用 event.target.closest(selector)或 event.target.matches(selector)确保只处理目标子元素。例如:


    if (event.target.matches('.todo-item')) { ... }


    const target = event.target.closest('.todo-item'); if (target) { ... }


  3. 处理事件冒泡的中断


    若子元素调用了 event.stopPropagation(),事件不会冒泡到父元素,委托会失效。需避免在关键子元素中阻止冒泡,或改用捕获阶段监听(addEventListener第三个参数为 true)。


  4. 性能优化的边界


    对于极少量子元素(如 5 个以内),直接绑定可能更简单;但对于动态或大量子元素,事件委托是更优选择。



五、总结:从“混淆”到“精通”的关键


event.currentTarget和 event.target的核心差异,本质是 ​​“处理者”与“触发者”​​ 的分工:



  • target是用户交互的起点,始终指向实际触发的元素。

  • currentTarget是事件处理程序的载体,随事件传播阶段变化。


而事件委托,正是利用这一差异,通过父元素(currentTarget)集中处理子元素事件,解决了动态内容、批量操作的维护难题。


掌握这对属性和事件委托模式,不仅能写出更高效的代码,更能让你在前端交互设计中游刃有余。下次遇到大量子元素的事件绑定需求时,不妨试试事件委托——它会是你最可靠的“效率工具”。


作者:若尘的技术之旅
来源:juejin.cn/post/7553245651939131427
收起阅读 »

SpringBoot多模板引擎整合难题?一篇搞定JSP、Freemarker与Thymeleaf!

关注我的公众号:【编程朝花夕拾】,可获取首发内容。 01 引言 在现代Web应用开发中,模板引擎是实现前后端分离和视图渲染的重要工具。SpringBoot作为流行的Java开发框架,天然支持多种模板引擎。 每一个项目使用单一的模板引擎是标准输出。但是,总有...
继续阅读 »

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




01 引言


在现代Web应用开发中,模板引擎是实现前后端分离和视图渲染的重要工具。SpringBoot作为流行的Java开发框架,天然支持多种模板引擎。


每一个项目使用单一的模板引擎是标准输出。但是,总有一些老项目经历多轮迭代,人员更替,不同的开发都只是用自己熟悉的模版引擎,导致一个项目中包含了多种模板引擎。从而相互影响,甚至出现异常。这也是小编正在经历的痛苦。


本文将详细介绍如何在SpringBoot项目中同时集成JSP、Freemarker和Thymeleaf三种模板引擎,包括配置方法、使用场景、常见问题及解决方案。


02 项目搭建


本文基于Springboot 3.0.13,因为不同版本(2.x)对于部分包的做了更改。由于JSP的配置会影响其他的模板引擎,所以JSP的配置,放到最后说明。


2.1 Maven依赖


<!-- freemarker 模版引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- thymeleaf 模版引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.3 配置


#配置freemarker
spring.freemarker.template-loader-path=classpath:/templates/ftl/
spring.freemarker.suffix=.ftl
spring.freemarker.cache=false

# 配置thymeleaf
spring.thymeleaf.prefix=classpath:/templates/html/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false

2.4 最佳实践


页面



控制层


@Controller
@RequestMapping("/page")
public class PageController {

@RequestMapping("{engine}")
public String toPage(@PathVariable("engine") String engine, Model model) {
model.addAttribute("date", new Date());
return engine + "_index";
}
}

2.5 测试



到这里,会发现一切顺利。ThymeleafFreemarker都可以顺利解析。但是,引入JSP之后,发现不能生效。


03 SpringBoot继续集成JSP


3.1 Maven依赖


<!-- JSP支持 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- jstl 工具 -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>

这里要说明的jstl,低版本(3.x一下)的需要引入:


<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>

具体的依赖可以在Springboot官方文档中查看。



3.2 配置


spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

3.3 创建包结构


因为SpringBoot默认不支持JSP,所以需要我们自己配置支持JSP。


包的路径地址:\src\main\webapp\WEB-INF



3.4 修改pom打包


build下增加resource


<resources>
<!-- 打包时将jsp文件拷贝到META-INF目录下-->
<resource>
<!-- 指定处理哪个目录下的资源文件 -->
<directory>src/main/webapp</directory>
<!--注意此次必须要放在此目录下才能被访问到-->
<targetPath>META-INF/resources</targetPath>
<includes>
<include>**/**</include>
</includes>
</resource>
</resources>

3.5 测试



其他两个不受影响,但是发现配置的JSP并不生效,根据报错信息来看,默认使用了Thymeleaf解析的。



04 源码追踪


关键的类:org.springframework.web.servlet.view.ContentNegotiatingViewResolver



断点调试发现,图中①根据jsp_index视图,可以发现两个候选的ViewThymelearViewJstlView


图中②获取最优的视图返回了ThymelearView,从而解析错误。从getBestView()源码可以看到,仅仅做了遍历操作,并没有个给句特殊的规则去取。如图:



所以影响视图解析器的就是候选视图的顺序。


我们继续看候选视图的取值:



这里仍是只是遍历,我们需要继续追溯this.viewResolvers的来源:



关键代码AnnotationAwareOrderComparator.sort(this.viewResolvers)会对所有的视图排序,所以我们只需要指定JSP的视图为第一个就可以了。


05 配置JSP视图的顺序


因为JSP的视图使用的是InternalResourceViewResolver,所以我们只需要设置其顺序即可。


@Configuration
public class BeanConfig {

@Autowired
InternalResourceViewResolver resolver;

@PostConstruct
public void init() {
resolver.setOrder(1);
}

由于其他的视图解析器默认是最级别,所以这里的设置只要比Integr.MAX小即可。


测试


我们发现源代码已经将JstlView变成了第一个,最优的视图自然也选择了JstlView,如图:



效果



我们发现JSP是正常显示了,但是其他两个又不好了。


真实让人头大!


06 解决JSP混合问题


6.1 解决方案


其实这里要使用一个属性可以永久的解决问题:viewName


每一个ViewResolver都有一段关键的源码:


image-20250925165142273


这里是匹配关系,可以通过配置的view-names过滤不符合条件的视图:



6.2 重新修改配置


###配置freemarker
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.view-names=ftl/*
spring.freemarker.suffix=.ftl
spring.freemarker.cache=false
#
### 配置thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.view-names=html/*
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
##
### 配置JSP
spring.mvc.view.prefix=/WEB-INF/
spring.mvc.view.suffix=.jsp

这里的和之前不同的就是增加了spring.thymeleaf.view-namesspring.freemarker.view-names,并且classpath的路径少了一部分移动到view-names里面了。


JSPspring.mvc.view.prefix同样少了一部分需要配置。


6.3 重新修改Java配置


@Configuration
public class BeanConfig {

@Autowired
InternalResourceViewResolver resolver;

@PostConstruct
public void init() {
resolver.setViewNames("jsp/*");
}

也可以使用Bean定义。使用Bean定义需要删除配置文件关于JSP的配置。


@Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/");
resolver.setSuffix(".jsp");
resolver.setViewNames("jsp/*");
return resolver;
}

6.4 修改控制层


@Controller
@RequestMapping("/page")
public class PageController {

@RequestMapping("{engine}")
public String toFtl(@PathVariable("engine") String engine, Model model) {
model.addAttribute("date", new Date());
return engine + "/" + engine + "_index";
}
}

6.5 效果



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

前端数据请求对决:Axios 还是 Fetch?

web
在 2025 年的现代前端开发中,高效可靠的数据请求依然是核心挑战。Axios 和 Fetch API 作为两大主流解决方案,一直让开发者难以抉择。本文将深入剖析两者特点,通过实际对比助你做出技术选型决策。原生之力:Fetch APIFetch 是浏览器原生提...
继续阅读 »

在 2025 年的现代前端开发中,高效可靠的数据请求依然是核心挑战。Axios 和 Fetch API 作为两大主流解决方案,一直让开发者难以抉择。本文将深入剖析两者特点,通过实际对比助你做出技术选型决策。

原生之力:Fetch API

Fetch 是浏览器原生提供的请求接口,无需安装即可使用:

// 基础 GET 请求
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) throw new Error('Network error');
return response.json(); // 需手动解析JSON
})
.then(data => console.log(data))
.catch(error => console.error('Request failed:', error));

核心特性:

  • 原生内置:现代浏览器(包括IE11+)全面支持
  • Promise 架构:告别回调地狱,支持 async/await
  • 精简设计:无额外依赖,项目体积零负担

2025 年增强特性:

  • AbortController:支持请求取消(原生能力已普及)
const controller = new AbortController();
fetch(url, { signal: controller.signal });
controller.abort(); // 取消请求
  • Streams API:直接处理数据流(适用于大文件)
  • 请求优先级:通过priority: 'high'参数优化关键请求

全能战士:Axios

Axios 作为久经考验的第三方库,提供了更完善的功能封装:

// 完整特性的 POST 请求
axios.post('https://api.example.com/users', {
name: 'John',
age: 30
}, {
headers: { 'X-Custom-Header': 'value' },
timeout: 5000
})
.then(response => console.log(response.data)) // 自动解析JSON
.catch(error => {
if (axios.isCancel(error)) {
console.log('Request canceled');
} else {
console.error('Request error:', error);
}
});

不可替代的优势:

  1. 开箱即用的 JSON 处理:自动转换响应数据
  2. 拦截器机制:全局管理请求/响应
// 身份验证拦截器
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${token}`;
return config;
});

// 错误统一处理
axios.interceptors.response.use(
response => response,
error => {
alert(`API Error: ${error.response.status}`);
return Promise.reject(error);
}
);
  1. 便捷的取消机制
const source = axios.CancelToken.source();
axios.get(url, { cancelToken: source.token });
source.cancel('Operation canceled');
  1. 客户端防御:内置 XSRF 防护
  2. 进度跟踪:上传/下载进度监控
  3. 多环境支持:浏览器与 Node.js 通用

关键决策因素对比

特性AxiosFetch
安装需求需安装 (13KB gzip)浏览器内置
JSON 处理自动转换需手动 response.json()
错误处理捕获所有HTTP错误需检查 response.ok
取消请求专用CancelToken使用AbortController
拦截器
超时设置原生支持 timeout 参数需结合AbortController实现
请求进度追踪通过response.body实现
浏览器兼容性IE10+ (需polyfill)现代浏览器(IE11+)
XSRF防护✅自动需手动配置
TypeScript支持完善的类型定义需额外声明

2025 年选型建议

  1. 选择 Fetch 当:

    • 项目无复杂请求逻辑
    • 追求零依赖和最小打包体积
    • 目标平台均为现代浏览器
    • 使用框架内置封装(如 Next.js 的 fetch 增强)
  2. 选择 Axios 当:

    • 需要统一处理错误/权限
    • 项目涉及文件上传等复杂场景
    • 服务端渲染(SSR)应用
    • 团队已有Axios使用规范
    • 需要TypeScript深度支持

融合解决方案

在大型项目中,可以采用混合方案:

// 封装原生fetch获得axios式体验
const http = async (url, options = {}) => {
const controller = new AbortController();
const config = {
...options,
signal: controller.signal,
headers: { 'Content-Type': 'application/json', ...options.headers }
};

const timeoutId = setTimeout(() => controller.abort(), 8000);

try {
const response = await fetch(url, config);
clearTimeout(timeoutId);

if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name === 'AbortError') console.error('Request timed out');
throw error;
}
};

结论

在 2025 年的技术背景下,Fetch API 的原生能力得到大幅增强,对于小型项目和简单场景愈发得心应手。但对于企业级应用,Axios 仍然凭借其人性化设计和功能完备性保持不可替代的地位。建议开发者根据项目的规模、运行环境、团队习惯三大核心因素制定技术决策,必要时可进行混合封装以平衡开发效率与性能要求。


作者:艾小码
来源:juejin.cn/post/7535907433278226474
收起阅读 »

JVM内存公寓清洁指南:G1与ZGC清洁工大比拼

JVM内存公寓清洁指南:G1与ZGC清洁工大比拼 🧹 引言:当内存公寓遇上"清洁工天团" 当 Java 应用中的对象在"内存公寓"里肆意"开派对"后,未被引用的对象便成了散落各处的"垃圾",此时就需要专业的"清洁团队"——垃圾回收器登场。JVM 内存区域如同"...
继续阅读 »

JVM内存公寓清洁指南:G1与ZGC清洁工大比拼 🧹


引言:当内存公寓遇上"清洁工天团"


当 Java 应用中的对象在"内存公寓"里肆意"开派对"后,未被引用的对象便成了散落各处的"垃圾",此时就需要专业的"清洁团队"——垃圾回收器登场。JVM 内存区域如同"内存公寓"的不同房间,其中线程共享区的堆是最大的"活动空间",按对象生命周期分为新生代(Eden 区占 8/10、Survivor 区各占 1/10)和老年代,如同公寓的"青年宿舍"与"长者公寓";方法区(元空间)则类似"物业档案室",存储类元数据等。


image.png


为什么有的"清洁工"习惯按区域分片打扫,有的却能以"闪电速度"完成全屋清洁?这就不得不提到 G1 和 ZGC 两位"王牌清洁工"——前者以"分区管理"策略著称,后者则追求"低延迟闪电清洁",其设计目标是将应用暂停(STW)时间控制在 10ms 以内,且停顿时间不会随堆大小或活跃对象增加而延长。


image.png



核心差异预告:G1 采用分代分区管理模式,擅长平衡吞吐量与停顿;ZGC 则通过创新算法突破堆大小限制,主打"毫秒级响应"。本文将拆解两者的"清洁秘籍"(垃圾回收算法)与"工资参数"(调优参数),揭秘谁能成为"内存公寓"的最优解。



G1回收器:精打细算的"分区清洁队长"


image.png


Garbage-First (G1) 垃圾收集器作为默认低延迟收集器,其核心设计理念可类比为"内存公寓"的分区清洁管理系统。与传统收集器将堆内存划分为固定大小新生代与老年代的方式不同,G1采用"分区垃圾袋"式的Region机制,将整个堆内存划分为最多2048个独立Region,每个Region容量可在1MB至32MB之间动态调整(默认根据堆大小自动选择)。这些Region并非固定归属新生代或老年代,而是根据应用内存分配模式动态标记为Eden区、Survivor区或Old区,实现内存资源的弹性调度。这种动态分区机制使G1能够灵活应对不同类型应用的内存需求,尤其适用于堆内存4GB至32GB的常规企业应用场景。


G1的垃圾回收策略采用"混合清洁模式"(Mixed GC),其工作流程可形象比喻为"先集中清理垃圾密集的房间(新生代),再抽空打扫老房间(老年代)"。G1优先对新生代Region执行Minor GC,通过复制算法快速回收短期存活对象;当老年代Region占比达到参数-XX:InitiatingHeapOccupancyPercent(默认45%)设定的阈值时,触发Mixed GC,在新生代收集的同时,选取部分垃圾占比高的老年代Region进行回收。这种选择性回收策略使G1能够集中资源处理垃圾密集区域,从而更精准地控制停顿时间,避免传统收集器对整个老年代进行全区域扫描的高昂成本。


在实际调优中,启用G1需通过JVM参数显式配置,基础命令示例如下:java -XX:+UseG1GC -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 -jar app.jar。某电商交易系统优化案例显示,在未调优状态下GC停顿时间常达300ms,通过设置MaxGCPauseMillis=200并调整Region大小后,停顿时间稳定降至180ms,同时吞吐量保持98%以上。核心调优参数及说明如下表所示:


参数作用幽默解读
-XX:+UseG1GC启用G1回收器"任命G1为清洁队长"
-Xms/-Xmx初始/最大堆大小"初始/最大垃圾袋容量"
-XX:MaxGCPauseMillis目标停顿时间"要求每次清洁不超过X毫秒"
-XX:G1HeapRegionSizeRegion大小"每个垃圾袋的容量"

💡 调优技巧:设置合理的停顿目标(如200ms)是平衡延迟与吞吐量的关键。G1会根据历史回收数据动态调整Region回收数量,过度严苛的停顿目标(如50ms)会迫使收集器频繁进行小范围压缩,反而导致GC次数激增。建议通过-XX:G1HeapRegionSize参数将Region大小设置为堆内存的1/2048,确保每个Region既能容纳大对象,又避免过小Region导致的管理开销。


🚨 常见误区:不要将-Xms和-Xmx设置为不同值!动态堆扩容会导致"内存公寓"频繁调整垃圾袋大小,引发额外性能开销,就像清洁工频繁更换垃圾桶尺寸一样影响效率。


以下是G1调优前后的GC日志对比:


# 调优前(停顿300ms)
[GC pause (G1 Evacuation Pause) (young) 1024M->768M(4096M) 302.5ms]

# 调优后(停顿180ms)
[GC pause (G1 Evacuation Pause) (young) 1024M->768M(4096M) 178.3ms]

ZGC回收器:闪电般的"极速清洁特工"


image.png


ZGC作为JVM内存管理的"极速清洁特工",其核心竞争力体现在毫秒级停顿超大堆支持两大特性上。设计目标明确为停顿时间不超过10ms,且该指标不会随堆大小或活跃对象数量的增加而退化,从根本上解决了传统回收器在大堆场景下的停顿痛点。


ZGC的"闪电清洁"秘籍


ZGC实现"边打扫边让住户正常活动"的核心技术在于染色指针内存多重映射。染色指针技术在64位指针中嵌入4位元数据,可实时存储对象的标记状态与重定位信息,相当于清洁工佩戴的"AR智能眼镜",能在不中断住户活动的情况下完成垃圾标记。内存多重映射则通过将物理内存同时映射到Marked0、Marked1、Remapped三个虚拟视图,实现并发重定位操作,确保回收过程与应用线程几乎无干扰。实测数据显示,ZGC停顿时间平均仅1.09ms,99.9%分位值为1.66ms,远低于10ms的设计阈值。


大堆管理:从16MB到16TB的"超级公寓"


与G1固定大小的Region(最大32MB)不同,ZGC采用动态Region机制,将内存划分为小页(2MB)、中页(32MB)和大页(N×2MB,最大支持16TB),如同"能伸缩的智能垃圾袋",可根据对象大小自动调整容量。这种设计使其支持从8MB到16TB的堆内存范围,而G1在堆大小超过64GB时易出现停顿失控[1]。动态Region不仅提升了内存利用率,还解决了大对象分配效率问题,实现"小到零食包装,大到家具"的全覆盖管理。


调优参数实战


启用与核心参数配置

启用ZGC需在JDK15+环境中使用以下命令:



java -XX:+UseZGC -Xms16g -Xmx16g -XX:ZCollectionInterval=60 -jar app.jar

该配置指定16GB堆空间(初始与最大堆相同),至少每60秒执行一次回收。以下为核心参数说明:


参数作用幽默解读
-XX:+UseZGC启用ZGC回收器"召唤闪电清洁特工"
-Xms/-Xmx初始/最大堆大小"清洁区域的固定边界"
-XX:ZCollectionInterval最小回收间隔"至少每隔X秒打扫一次"
-XX:ZAllocationSpikeTolerance分配尖峰容忍度"允许临时垃圾堆积倍数"

💡 调优黄金法则:ZGC在32GB以上大堆场景优势显著,此时其停顿稳定性远超G1;而8GB以下小堆场景建议保留G1,因ZGC的吞吐量损失(通常<15%)在小堆下性价比更低。


🚨 误区警示:ZGC在JDK15才正式发布,JDK11-14为实验性版本,存在功能限制;JDK11以下版本完全不支持,切勿尝试在低版本JDK中启用。


性能对比与GC日志示例

在64GB堆环境下,ZGC与G1的表现差异显著:


# ZGC日志(停顿8ms
[0.875s][info][gc] GC(0) Pause Relocate Start 1.56ms
[0.877s][info][gc] GC(0) Pause Relocate End 0.89ms

# G1日志(停顿520ms
[GC pause (G1 Evacuation Pause) (mixed) 5890M->4520M(65536M) 520.3ms]

某支付系统迁移案例显示,将G1替换为ZGC后,峰值GC停顿从280ms降至8ms,交易成功率提升0.5%,验证了ZGC在关键业务场景的性能优势。


G1 vs ZGC:清洁团队终极PK


衡量垃圾收集器的三项重要指标包括内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency)。吞吐量和延迟通常不可兼得,关注吞吐量的收集器和关注延迟的收集器在算法选择上存在差异。以下从核心能力与场景适配两方面对比 G1 与 ZGC 的差异:


核心能力对比表


能力维度G1(分区清洁工)ZGC(闪电特工)
停顿时间100-300 ms< 10 ms
堆大小支持最大 64 GB最大 16 TB
吞吐量较高略低(因并发开销)
适用场景常规应用、中小堆低延迟服务、超大堆


电商订单系统:用户下单高峰期需避免卡顿,ZGC 小于 10 ms 的停顿特性可保障交易流畅性。


大数据批处理:当堆大小适中(如 32 GB)且吞吐量优先时,G1 更具成本效益。


实时游戏服务:毫秒级响应要求下,ZGC 是唯一能满足低延迟需求的选择。



总结:选对清洁工,内存公寓更舒心


回到"内存公寓"的管理视角,垃圾回收器的选择本质是匹配"公寓规模"与"住户需求"的过程——正如现实中没有万能的清洁工,JVM 内存管理也不存在绝对最优解,只有最适配场景的选择。


G1 作为"精打细算的分区管理员",擅长处理 4GB~32GB 堆内存的常规企业应用,通过区域化内存布局与增量回收机制,在延迟控制与吞吐量之间取得平衡,成为大多数标准业务场景的默认选择。其设计理念如同经验丰富的物业经理,通过精细化分区管理确保日常运营的稳定高效。


ZGC 则是"追求极致速度的闪电特工",专为 8MB~16TB 超大堆场景打造,尤其适用于金融交易等对停顿时间(<10ms)要求严苛的低延迟应用。它突破传统回收器的性能瓶颈,如同配备尖端装备的特种清洁团队,能在不干扰住户正常活动的前提下完成超大空间的极速清理。



调优核心口诀:"小堆 G1 看停顿,大堆 ZGC 保延迟,参数设置要合理,日志监控不能停"。这一实践准则强调:堆内存规模与延迟需求是选型的首要依据,而持续的参数优化与监控分析则是维持长期稳定的关键。



选择合适的垃圾回收器并合理配置参数(如元空间大小、回收阈值等),是确保"内存公寓"长期整洁(避免内存溢出、减少 GC 停顿)的核心保障。你的内存公寓需要哪种清洁工?评论区聊聊你的调优故事吧!🎉


作者:无限大6
来源:juejin.cn/post/7552730198288564259
收起阅读 »

Mysql---领导让下班前把explain画成一张图

Explain总览图 这篇文章主要看图 Explain是啥 1、Explain工具介绍 使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈, 在 select 语句之前增加 explain 关键字,MySQL 会在查询...
继续阅读 »

Explain总览图 这篇文章主要看图


wwww.png


Explain是啥


1、Explain工具介绍


使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈, 在 select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。


注意:如果 from 中包含子查询,仍会执行该子查询,将结果放入临时表中。


2、Explain分析示例


参考官方文档:dev.mysql.com/doc/refman/…



# 示例表:

DROP TABLE IF EXISTS `actor`;
CREATE TABLE `actor` (
`id` int(11) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `actor` (`id`, `name`, `update_time`) VALUES (1,'a','2017‐12‐22
15:27:18'
), (2,'b','2017‐12‐22 15:27:18'), (3,'c','2017‐12‐22 15:27:18');

DROP TABLE IF EXISTS `film`;
CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


INSERT INTO `film` (`id`, `name`) VALUES (3,'film0'),(1,'film1'),(2,'film2');

DROP TABLE IF EXISTS `film_actor`;
CREATE TABLE `film_actor` (
`id` int(11) NOT NULL,
`film_id` int(11) NOT NULL,
`actor_id` int(11) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_film_actor_id` (`film_id`,`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `film_actor` (`id`, `film_id`, `actor_id`) VALUES (1,1,1),(2,1,2),(3,2,1);


explain select * from actor;

# 在查询中的每个表会输出一行,如果有两个表通过 join 连接查询,那么会输出两行。


3、explain 两个变种



  • 1)explain extended:


会在 explain 的基础上额外提供一些查询优化的信息。紧随其后通过 show warnings 命令可 以得到优化后的查询语句,从而看出优化器优化了什么。额外还有 filtered 列,是一个半分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)


explain extended select * from film where id = 1;


show warnings;



  • 2)explain partitions:


相比 explain 多了个 partitions 字段,如果查询是基于分区表的话,会显示查询将访问的分区。


4、explain中的列


4.1. id列


id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。 id列越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。


4.2. select_type列


select_type 表示对应行是简单还是复杂的查询。



  • 1)simple:简单查询。查询不包含子查询和union;

  • 2)primary:复杂查询中最外层的select ;

  • 3)subquery:包含在 select 中的子查询(不在 from 子句中);

  • 4)derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中;

  • 5)union:在 union 中的第二个和随后的 select;


explain select * from film where id = 2;


用这个例子来了解 primary、subquery 和 derived 类型:


#关闭mysql5.7新特性对衍生表的合并优化


set session optimizer_switch='derived_merge=off';

explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der;

#还原默认配置


set session optimizer_switch='derived_merge=on'; 
explain select 1 union all select 1;

4.3. table列


这一列表示 explain 的一行正在访问哪个表。
当 from 子句中有子查询时,table列是格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。当有 union 时,UNION RESULT 的 table 列的值为,1和2表示参与 union 的 select 行id。


4.4. type列


这一列表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。 依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL 。
一般来说,得保证查询达到range级别,最好达到ref ;


NULL:

mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引。例如:在索引列中选取最小值,可 以单独查找索引来完成,不需要在执行时访问表


mysql> explain select min(id) from film;


const, system:

mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是 const的特例,表里只有一条元组匹配时为system;


explain extended select * from (select * from film where id = 1) tmp;


show warnings;


eq_ref:

primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。


explain select * from film_actor left join film on film_actor.film_id = film.id;


ref:相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会 找到多个符合条件的行。


简单 select 查询,name是普通索引(非唯一索引)


explain select * from film where name = 'film1';


关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。


explain select film_id from film left join film_actor on film.id = film_actor.fi
lm_id;


range:

范围扫描通常出现在 in(), between ,> ,= 等操作中。使用一个索引来检索给定范围的行。


explain select * from actor where id > 1;


index:

扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接 对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这 种通常比ALL快一些。


explain select * from film;


ALL:

即全表扫描,扫描你的聚簇索引的所有叶子节点.通常情况下这需要增加索引来进行优化。


explain select * from actor;


4.5. possible_keys列


这一列显示查询可能使用哪些索引来查找 explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对此查询帮助不大,选择了全表查询。 如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提 高查询性能,然后用 explain 查看效果。


4.6. key列


这一列显示mysql实际采用哪个索引来优化对该表的访问。 如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。


4.7. key_len列


这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。 举例来说,film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,并且每个int是4字节。通 过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找。


explain select * from film_actor where film_id = 2;


key_len计算规则如下:


     字符串:
char(n):如果存汉字长度就是 3n 字节
varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,
因为 varchar是变长字符串

char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数,
如果是 utf-8,一个数字 或字母占1个字节,一个汉字占3个字节 ;


    数值类型:
tinyint:1字节
smallint2字节
int4字节
bigint8字节
时间类型:
date3字节
timestamp4字节
datetime:8字节

如果字段允许为 NULL,需要1字节记录是否为 NULL ;索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引。


4.8. ref列


这一列显示了在key列记录的索引中,表查找值所用到的列或常量,
常见的有:const(常量),字段名(例:film.id)


4.9. rows列


这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。


4.10. Extra列


这一列展示的是额外信息。常见的重要值如下:


1)Using index:使用覆盖索引

覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中 获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个 查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值。


explain select film_id from film_actor where film_id = 1;


2)Using where:

使用 where 语句来处理结果,并且查询的列未被索引覆盖


explain select * from actor where name = 'a';


3)Using index condition:

查询的列不完全被索引覆盖,where条件中是一个前导列的范围;


explain select * from film_actor where film_id > 1;


4)Using temporary:

mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索 引来优化。


actor.name没有索引,此时创建了张临时表来distinct


explain select distinct name from actor;


film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表


explain select distinct name from film;


5)Using filesort:

将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一 般也是要考虑使用索引来优化的



  1. actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录


1 mysql> explain select * from actor order by name
2. film.name建立了idx_name索引,此时查询时extra是using index


explain select * from film order by name;


6)Select tables optimized away:

使用某些聚合函数(比如 max、min来访问存在索引的某个字段是


explain select min(id) from film;


索引最佳实践



# 示例表:
CREATE TABLE `employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
PRIMARY KEY (`id`),
KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='员工记录表';

INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'manager',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('HanMeimei',
23,'dev',NOW());
INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());

5.1.全值匹配


1 EXPLAIN SELECT * FROM employees WHERE name= 'LiLei';


1 EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22;


EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';


5.2.最左前缀法则


如果索引了多列,要遵守最左前缀法。则指的是查询从索引的最左前列开始并且不跳过索引中的列。


1 EXPLAIN SELECT * FROM employees WHERE name = 'Bill' and age = 31;
2 EXPLAIN SELECT * FROM employees WHERE age = 30 AND position = 'dev';
3 EXPLAIN SELECT * FROM employees WHERE position = 'manager'


5.3.不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描


1 EXPLAIN SELECT * FROM employees WHERE name = 'LiLei';
2 EXPLAIN SELECT * FROM employees WHERE left(name,3) = 'LiLei';
给hire_time增加一个普通索引:


1 ALTER TABLE employees ADD INDEX idx_hire_time (hire_time) USING BTREE ;
2 EXPLAIN select * from employees where date(hire_time) ='2018‐09‐30';


转化为日期范围查询,有可能会走索引:


1 EXPLAIN select * from employees where hire_time >='2018‐09‐30 00:00:00' and hire_time <='2018‐09‐30 23:59:59';


还原最初索引状态


1 ALTER TABLE employees DROP INDEX idx_hire_time;


5.4.存储引擎不能使用索引中范围条件右边的列


1 EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 22 AND position ='manager';
2 EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age > 22 AND position ='manager';


5.5.尽量使用覆盖索引(只访问索引的查询(索引列包含查询列)),减少 select * 语句


1 EXPLAIN SELECT name,age FROM employees WHERE name= 'LiLei' AND age = 23 AND position='manager';


1 EXPLAIN SELECT * FROM employees WHERE name= 'LiLei' AND age = 23 AND position ='manager';


5.6.mysql在使用不等于(!=或者<>),not in ,not exists 的时候无法使用索引会导致全表扫描 < 小于、 > 大于、 = 这些,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引


1 EXPLAIN SELECT * FROM employees WHERE name != 'LiLei';


5.7.is null,is not null 一般情况下也无法使用索引


1 EXPLAIN SELECT * FROM employees WHERE name is null


5.8.like以通配符开头('$abc...')mysql索引失效会变成全表扫描操作


1 EXPLAIN SELECT * FROM employees WHERE name like '%Lei'


1 EXPLAIN SELECT * FROM employees WHERE name like 'Lei%'


问题:解决like'%字符串%'索引不被使用的方法?
a)使用覆盖索引,查询字段必须是建立覆盖索引字段


1 EXPLAIN SELECT name,age,position FROM employees WHERE name like '%Lei%';


b)如果不能使用覆盖索引则可能需要借助搜索引擎


5.9.字符串不加单引号索引失效


1 EXPLAIN SELECT * FROM employees WHERE name = '1000'; 2 EXPLAIN SELECT * FROM employees WHERE name = 1000;


5.10.少用or或in,


用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评 估是否使用索引,详见范围查询优化


1 EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' or name = 'HanMeimei';


5.11.范围查询优化 给年龄添加单值索引


1 ALTER TABLE employees ADD INDEX idx_age (age) USING BTREE ;
2 explain select * from employees where age >=1 and age <=2000;


没走索引原因:mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引。比如这个例子,可能是 由于单次数据量查询过大导致优化器最终选择不走索引 。
优化方法:可以将大的范围拆分成多个小范围。


1 explain select * from employees where age >=1 and age <=1000;
2 explain select * from employees where age >=1001 and age <=2000;


还原最初索引状态


1 ALTER TABLE employees DROP INDEX idx_age;


6、索引使用总结


image.png


PS:like KK%相当于=常量,%KK和%KK% 相当于范围


作者:后端程序员Aska
来源:juejin.cn/post/7478888679231193125
收起阅读 »

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

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

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


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


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


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


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


反例代码:


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

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

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

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


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


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

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

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

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

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


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


反例代码


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

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

public String getOrderStatus() {
return orderStatus;
}
}

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

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


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


public class Order {
private String orderStatus;

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

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

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


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


反例代码


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

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

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

private static String secretKey = "default_key";
}

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


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


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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



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



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


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


结语


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


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

苍穹外卖实现员工分页查询

员工分页查询功能开发1. 需求分析2. 代码开发根据分页查询接口设计对应的DTO设计controller层 @GetMapping("/page") @ApiOperation(value = "员工分页查询") public Resul...
继续阅读 »

员工分页查询功能开发

1. 需求分析

image-20250727220530038

image-20250727220803745

2. 代码开发

  1. 根据分页查询接口设计对应的DTO

image-20250727220930090

  1. 设计controller层
    @GetMapping("/page")
@ApiOperation(value = "员工分页查询")
public Result page(EmployeePageQueryDTO employeePageQueryDTO){
//输出日志
log.info("员工分页查询,查询参数: {}",employeePageQueryDTO);
//调用service层返回分页结果
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
//返回result
return Result.success(pageResult);
}
  1. 设计service层,使用Page Helper进行分页,并返回total和record
 @Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
//开始分页
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

//调用mapper方法返回page对象,泛型为内容的类型,(page实际是一个List)
Page page = employeeMapper.pageQuery(employeePageQueryDTO);

//获取总的数据量
long total = page.getTotal();

//获取所有员工对象
List record = page.getResult();

//返回结果
return new PageResult(total,record);
}
  1. 设计Mapper层
    Page pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
  1. 使用动态SQL进行查询
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
name like concat('%',#{name},'#')
if>
where>
order by create_time desc
select>

3. 功能测试

Swagger测试:

image-20250727232014532

问题

createTime这种是数组形式传递的

前后端联调:

image-20250727231946460

问题:

操作时间渲染格式问题

4. 代码完善

image-20250727232335111

方式一:

代码:

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;

数据返回:

image-20250727232717370

方式二:

代码:

需要在配置类中重写父类的方法,并配置添加消息转换器

 @Override
protected void extendMessageConverters(List> converters) {
log.info("扩展消息转换器");
//创建一个消息转换器
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//为消息转换器设置一个对象转换器,对象转换器可以将对象数据转换为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将消息转换器添加到容器中,由于converters内部有很多消息转换器,我们假如的默认排在最后一位
//所以将顺序设置为最靠前
converters.add(0,converter);
}

其中JacksonObjectMapper为自己实现的实体类,写法较为固定

数据返回:

image-20250727234752486


作者:用户4007842211260
来源:juejin.cn/post/7531791862521151528

收起阅读 »

ts的迭代器和生成器

web
在 TypeScript(以及 JavaScript)中,迭代器和生成器是用于处理集合数据(如数组、对象等)的强大工具。它们允许你按顺序访问集合中的元素,并提供了一种控制数据访问的方式。 迭代器(Iterator) 迭代器是一个对象,它定义了一个序列,并且提供...
继续阅读 »

在 TypeScript(以及 JavaScript)中,迭代器和生成器是用于处理集合数据(如数组、对象等)的强大工具。它们允许你按顺序访问集合中的元素,并提供了一种控制数据访问的方式。


迭代器(Iterator)
迭代器是一个对象,它定义了一个序列,并且提供了一种方法来访问这个序列的元素。迭代器对象实现了 Iterator 接口,该接口要求它有一个 next() 方法。


Iterable 接口
一个对象如果实现了Iterable接口,那么它就是可迭代的。这个接口要求对象必须有一个Symbol.iterator方法,这个方法返回一个迭代器对象。


Iterator 接口
迭代器对象必须实现Iterator接口。这个接口定义了next()方法,该方法返回一个对象,这个对象有两个属性:value和done。value表示当前的元素值,done是一个布尔值,表示是否还有更多的元素可以迭代。


1.可迭代对象(Iterable)

一个对象如果实现了[Symbol.iterator]方法,它就是可迭代的。这个方法必须返回一个迭代器对象。常见的内置可迭代对象包括:Array,String,Map,Set,arguments对象(可通过Array.from()或扩展运算符使用),DOM的NodeList(部分浏览器)。


const arr = [1, 2, 3];
console.log(arr[Symbol.iterator]);

image.png
这些对象之所以能被for...of循环遍历,正是因为它们实现了Symbol.iterator方法。


2.迭代器(Iterator)

迭代器是一个带有next()方法的对象,调用next()返回{value,done}.
value:当前值(当done:true时,value可省略或为undefined)
done:布尔值,表示是否遍历完成。


const arr = [10, 20];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

image.png


注意:迭代器本身通常也是可迭代的(即它自己也有[Symbol.iterator()]),这样它就可以用于for...of。


3.for...of与展开运算符的工作原理

for (const item of [1, 2, 3]) {
console.log(item);
}

image.png


背后的逻辑:



  • 获取[1,2,3] [Symbol.iterator]()

  • 不断调用.next()直到done:true


同理:


const str = "hi";
const chars = [...str];
console.log(chars)

image.png


手动实现一个可迭代对象

class Countdown {
constructor(private start: number) {}

[Symbol.iterator](): Iterator<number> {
let current = this.start;
return {
next(): { value: number; done: boolean } {
if (current < 0) {
return { value: undefined, done: true };
}
return { value: current--, done: false };
},
// 可选:支持 return() 方法用于提前终止(如 break)
return() {
console.log("迭代被中断");
return { value: undefined, done: true };
}
};
}
}

// 使用
const countdown = new Countdown(3);
for (const n of countdown) {
console.log(n);
}

生成器(Generator):简化迭代器创建

生成器函数是创建迭代器的便捷方式。使用function* 定义,内部用yield暂停执行。


function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}

const gen = idGenerator();
console.log(gen.next().value);
console.log(gen.next().value);

image.png


生成器函数返回一个生成器对象,它即是迭代器,也是可迭代对象。


你甚至可以让一个对象使用生成器作为[Symbol.iterator]


const myRange = {
from: 1,
to: 5,
*[Symbol.iterator]() {
for (let i = this.from; i <= this.to; i++) {
yield i;
}
}
};

console.log([...myRange]);

image.png


迭代器协议总结:



协议:Iterable


方法:[Symbol.iterator]():Iterator


返回值:迭代器对象


说明:表示可被遍历




协议:Iterator


方法:next():{value,done}


返回值:状态对象


说明:提供下一个值




协议:可选


方法:return?():{value,done}


返回值:状态对象


说明:处理提前退出




协议:可选


方法:throw?():{value,done}


返回值:状态对象


说明:处理异常抛出



作者:怪可爱的地球人
来源:juejin.cn/post/7547346633400221747
收起阅读 »

鸿蒙应用开发从入门到实战(十一):ArkUI组件Text&TextInput

大家好,我是潘Sir,持续分享IT技术,帮你少走弯路。《鸿蒙应用开发从入门到项目实战》系列文章持续更新中,陆续更新AI+编程、企业级项目实战等原创内容、欢迎关注! ArkUI提供了丰富的系统组件,用于制作鸿蒙原生应用APP的UI,本文主要讲解文本组件Text和...
继续阅读 »

大家好,我是潘Sir,持续分享IT技术,帮你少走弯路。《鸿蒙应用开发从入门到项目实战》系列文章持续更新中,陆续更新AI+编程、企业级项目实战等原创内容、欢迎关注!


ArkUI提供了丰富的系统组件,用于制作鸿蒙原生应用APP的UI,本文主要讲解文本组件Text和TextInput的使用。


一、文本Text


1.1 概述


Text为文本组件,用于显示文字内容。


1.2 参数


Text组件的参数类型为string | Resource,下面分别对两个参数类型进行介绍:



  • string类型


Text('我是一段文本')


  • Resource 类型


Resource类型的参数用于引用 resources/*/element目录中定义的字符串,同样需要使用$r()引用。


例如resources/base/element目录中有一个string.json文件,内容如下


{
"string": [
  {
    "name": "greeting",
    "value": "你好"
  }
]
}

此时我们便可通过如下方式引用并显示greeting的内容。


Text($r('app.string.greeting'))

示例代码:


1、分别在resources下的base、en_US、zh_CN目录下的element下的string.json中添加对应的配置


在base和zh_CN下的element下的string.json中添加


 {
    "name": "greeting",
    "value": "你好,鸿蒙"
  }

在en_US目录下的element下的string.json中添加


{
    "name": "greeting",
    "value": "hello,harmony"
  }

2、component目录下新建text目录,新建TextParameterPage.ets文件


@Entry
@Component
// text组件
struct TextParameterPage {
build() {
  Column({ space: 50 }) {
    // text组件参数
    //1、字符串类型
    Text('你好,鸿蒙')
      .fontSize(50)

    //2、Resource类型
    Text($r('app.string.greeting'))
      .fontSize(50)
  }.width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}
}

1.3 常用属性


1.3.1 字体大小


字体大小可通过fontSize()方法进行设置,该方法的参数类型为string | number| Resource,下面逐一介绍



  • string类型


string类型的参数可用于指定字体大小的具体单位,例如fontSize('100px'),字体大小的单位支持pxfp。其中fp(font pixel)vp类似,具体大小也会随屏幕的像素密度变化而变化。



  • number类型


number类型的参数,默认以fp作为单位。



  • Resource类型


Resource类型参数用于引用resources下的element目录中定义的数值。


示例代码:


在component/text目录下新建FontSizePage.ets文件


@Entry
@Component
// text属性:字体大小
struct FontSizePage {
 build() {
     Column({ space: 50 }) {
       // 1、参数为string类型
       Text('你好,鸿蒙')
        .fontSize('150px')

       Text('你好,鸿蒙')
        .fontSize('50fp')

       // 2、参数为number类型
       Text('你好,鸿蒙')
        .fontSize(50)
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
}
}

1.3.2 字体粗细


字体粗细可通过fontWeight()方法进行设置,该方法参数类型为number | FontWeight | string,下面逐一介绍



  • number类型


number类型的取值范围是[100,900],取值间隔为100,默认为400,取值越大,字体越粗。



  • FontWeight类型


FontWeight为枚举类型,可选枚举值如下


名称描述
FontWeight.Lighter字体较细。
FontWeight.Normal字体粗细正常。
FontWeight.Regular字体粗细正常。
FontWeight.Medium字体粗细适中。
FontWeight.Bold字体较粗。
FontWeight.Bolder字体非常粗。


  • string类型


string类型的参数仅支持number类型和FontWeight类型参数的字符串形式,例如例如'100'bold


示例代码:


在component/text下新建FontWeightPage.ets文件


@Entry
@Component
// 字体粗细
struct FontWeightPage {

 build() {
   Column({ space: 50 }) {

     //默认效果
     Text('你好,鸿蒙')
      .fontSize(50)

     // 1、number类型
     Text('你好,鸿蒙')
      .fontSize(50)
      .fontWeight(666)

     // 2、FontWeight类型
     Text('你好,鸿蒙')
      .fontSize(50)
      .fontWeight(FontWeight.Lighter)

     // 3、string类型
     Text('你好,鸿蒙')
      .fontSize(50)
      .fontWeight('800')

  }.width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}
}

1.3.3 字体颜色


字体颜色可通过fontColor()方法进行设置,该方法参数类型为Color | string | number | Resource,下面逐一介绍



  • Color类型


    Color为枚举类型,其中包含了多种常用颜色,例如Color.Green


  • string类型


    string类型的参数可用于设置 rgb 格式的颜色,具体写法可以为'rgb(0, 128, 0)'或者'#008000'


  • number类型


    number类型的参数用于使用16进制的数字设置 rgb 格式的颜色,具体写法为0x008000


  • Resource类型


    Resource类型的参数用于应用resources下的element目录中定义的值。



示例代码:


在component/text目录下新建FontColorPage.ets文件


@Entry
@Component
// 字体颜色
struct FontColorPage {

 build() {
   Column({ space: 50 }) {
     // 1、Color类型
     Text('Color.Green')
      .fontSize(40)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.Green)

     // 2、string类型
     Text('rgb(0, 128, 0)')
      .fontSize(40)
      .fontWeight(FontWeight.Bold)
      .fontColor('rgba(59, 171, 59, 0.33)')

     Text('#008000')
      .fontSize(40)
      .fontWeight(FontWeight.Bold)
      .fontColor('#a4008000')

     // 3、number类型
     Text('0x008000')
      .fontSize(40)
      .fontWeight(FontWeight.Bold)
      .fontColor(0xa4008000)

  }.width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}
}

1.3.4 文本对齐


文本对齐方向可通过textAlign()方法进行设置,该方法的参数为枚举类型TextAlign,可选的枚举值如下


名称描述
TextAlign.Start首部对齐
TextAlign.Center居中对齐
TextAlign.End尾部对齐

各选项效果如下


1文本对齐效果.png
示例代码:


text目录下新建TextAlignPage.ets文件


@Entry
@Component
// 文本对齐
struct TextAlignPage {

 build() {
   Row() {
     Column({ space: 50 }) {
       Column({ space: 10 }) {
         // 1、TextAlign.Start
         Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
          .fontSize(20)
          .width(300)
          .borderWidth(1)
          .textAlign(TextAlign.Start)
         Text('Start')
      }

       Column({ space: 10 }) {
         // 2、TextAlign.Center
         Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
          .fontSize(20)
          .width(300)
          .borderWidth(1)
          .textAlign(TextAlign.Center)
         Text('Center')
      }

       Column({ space: 10 }) {
         // 3、TextAlign.End
         Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
          .fontSize(20)
          .width(300)
          .borderWidth(1)
          .textAlign(TextAlign.End)
         Text('End')
      }

    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
}

1.3.5 最大行数和超长处理


可使用maxLines()方法控制文本的最大行数,当内容超出最大行数时,可使用textOverflow()方法处理超出部分,该方法的参数类型为{ overflow: TextOverflow },其中TextOverflow为枚举类型,可用枚举值有


名称描述
TextOverflow.Clip文本超长时,进行裁剪显示。
TextOverflow.Ellipsis文本超长时,显示不下的文本用省略号代替。

各选项效果如下


2最大行数处理.png


示例代码:


在component/text目录下新建TextOverFlowPage.ets文件


@Entry
@Component
// 最大行数和超长处理
struct TextOverFlowPage {

 build() {
   Column({ space: 50 }) {
     Column({ space: 10 }) {
       Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
        .fontSize(20)
        .width(300)
        .borderWidth(1)
       Text('原始内容')
    }

     // 1、TextOverflow.Clip 文本超长时,进行裁剪显示
     Column({ space: 10 }) {
       Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
        .fontSize(20)
        .width(300)
        .borderWidth(1)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Clip })
       Text('Clip')
    }

     // 2、TextOverflow.Ellipsis 文本超长时,显示不下的文本用省略号代替
     Column({space:10}) {
       Text('鸿蒙操作系统是由华为公司开发的全场景、分布式的新一代操作系统,旨在实现各类智能设备的高效协同工作和统一体验')
        .fontSize(20)
        .width(300)
        .borderWidth(1)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
       Text('Ellipsis')
    }

  }.width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}
}

二、文本输入TextInput


2.1 概述


TextInput为文本输入组件,用于接收用户输入的文本内容。


2.2 参数


TextInput组件的参数定义如下


TextInput(value?:{placeholder?: string|Resource , text?: string|Resource})


  • placeholder


placeholder属性用于设置无输入时的提示文本,效果如下


3placeholder.png



  • text


text用于设置输入框当前的文本内容,效果如下


4text.png


示例代码:


component目录下新建input目录,新建TextInputParameter.ets文件


@Entry
@Component
// 文本输入参数
struct TextInputParameter {

 build() {
   Column({ space: 50 }) {
     TextInput()
      .width('70%')

     // 1、placeholder参数
     TextInput({ placeholder: '请输入用户名' })
      .width('70%')

     // 2、text参数
     TextInput({ text: '当前内容' })
      .width('70%')

  }.width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
}
}

2.3 常用属性


2.3.1 输入框类型


可通过type()方法设置输入框的类型,该方法的参数为InputType枚举类型,可选的枚举值有


名称描述
InputType.Normal基本输入模式
InputType.Password密码输入模式
InputType.Number纯数字输入模式

2.3.2 光标样式


可通过caretColor()方法设置光标的颜色,效果如下


5光标样式.png


2.3.3 placeholder样式


可通过placeholderFont()placeholderColor()方法设置 placeholder 的样式,其中placeholderFont()用于设置字体,包括字体大小、字体粗细等,placeholderColor()用于设置字体颜色,效果如下


6placeholcer样式.png


2.3.4 文本样式


输入文本的样式可通过fontSize()fontWeight()fontColor()等通用属性方法进行设置。


示例代码:


在input目录下新建TextInputAttributePage.ets文件


@Entry
@Component
// TextInput属性
struct TextInputAttributePage {

build() {
Column({ space: 50 }) {

// 1、输入框类型 type()设置类型, InputType
Column({ space: 10 }) {
Text('输入框类型')
TextInput({ placeholder: '请输入任意内容' })
.width('70%')
.type(InputType.Normal)
TextInput({ placeholder: '请输入数字' })
.width('70%')
.type(InputType.Number)
TextInput({ placeholder: '请输入密码' })
.width('70%')
.type(InputType.Password)
}

// 2、光标样式 caretColor()设置光标的颜色
Column({ space: 10 }) {
Text('光标样式')
TextInput()
.width('70%')
.caretColor(Color.Red)
}

// 3、placeholder样式 placeholderFont、placeholderColor
Column({ space: 10 }) {
Text('placeholder样式')
TextInput({ placeholder: '请输入用户名' })
.width('70%')
.placeholderFont({ weight: 800 ,style:FontStyle.Italic})
.placeholderColor('#66008000')
}

}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

2.4 常用事件


2.4.1 change事件


每当输入的内容发生变化,就会触发 change 事件,开发者可使用onChange()方法为TextInput组件绑定 change 事件,该方法的参数定义如下


onChange(callback: (value: string) => void)

其中value为最新内容。


2.4.2 焦点事件


焦点事件包括获得焦点失去焦点两个事件,当输入框获得焦点时,会触发 focus 事件,失去焦点时,会触发 blur 事件,开发者可使用onFocus()onBlur()方法为 TextInput 组件绑定相关事件,两个方法的参数定义如下


onFocus(event: () => void)	

onBlur(event: () => void)

示例代码:


在input目录下新建TextInputEvent.ets文件


@Entry
@Component
// TextInput事件
struct TextInputEvent {

build() {
Column({ space: 50 }) {
TextInput({ placeholder: '请输入用户名' })
.width('70%')
.type(InputType.Normal)
// 1、change事件
.onChange((value) => {
console.log(`用户名:${value}`)
})
// 2、获得焦点
.onFocus(() => {
console.log('用户名输入框获得焦点')
})
// 3、失去焦点
.onBlur(() => {
console.log('用户名输入框失去焦点')
})

TextInput({ placeholder: '请输入密码' })
.width('70%')
.type(InputType.Password)
.onChange((value) => {
console.log(`密码:${value}`)
})
.onFocus(() => {
console.log('密码输入框获得焦点')
})
.onBlur(() => {
console.log('密码输入框失去焦点')
})
}.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

《鸿蒙应用开发从入门到项目实战》系列文章持续更新中,陆续更新AI+编程、企业级项目实战等原创内容,防止迷路,欢迎关注!


作者:程序员潘Sir
来源:juejin.cn/post/7552700954286653483
收起阅读 »

仿照豆包实现 Prompt 变量模板输入框

web
先前在使用豆包的Web版时,发现在“帮我写作”模块中用以输入Prompt的模板输入框非常实用,既可以保留模板输入的优势,来调优指定的写作方向,又能够不失灵活地自由编辑。其新对话的输入交互也非常细节,例如选择“音乐生成”后技能提示本身也是编辑器的嵌入模块,不可以...
继续阅读 »

先前在使用豆包的Web版时,发现在“帮我写作”模块中用以输入Prompt的模板输入框非常实用,既可以保留模板输入的优势,来调优指定的写作方向,又能够不失灵活地自由编辑。其新对话的输入交互也非常细节,例如选择“音乐生成”后技能提示本身也是编辑器的嵌入模块,不可以直接删除。


image.png


虽然看起来这仅仅是一个文本内容的输入框,但是实现起来并不是那么容易,细节的交互也非常重要。例如技能提示节点直接作为输入框本身模块,多行文本就可以在提示下方排版,而不是类似网格布局需要在左侧留空白内容。那么在这里我们就以豆包的交互为例,来实现Prompt的变量模板输入框。



AI Infra 系列相关文章


概述


当我们开发AI相关的应用时,一个常见的场景便是需要用户输入Prompt,或者是在管理后台维护Prompt模板提供给其他用户使用。此时我们就需要一个能够支持内容输入或者模板变量的输入框,那么常见的实现方式有以下几种:



  • 纯文本输入框,类似于<input><textarea>等标签,在其DOM结构周围实现诸如图片、工具选择等按钮的交互。

  • 表单变量模板,类似于填空的形式,将Prompt模板以表单的形式填充变量,用户只需要填充所需要的变量内容即可。

  • 变量模板输入框,同样类似于填空的形式,但是其他内容也是可以编辑的,以此实现模版变量调优以及灵活的自由指令。


在这里有个有趣的事情,豆包的这个模板输入框是用slate做的,而后边生成文档的部分却又引入了新的富文本框架。也就是其启用分步骤“文档编辑器”模式的编辑器框架与模板输入框的编辑器框架并非同一套实现,毕竟引入多套编辑器还是会对应用的体积还是有比较大的影响。


因此为什么不直接使用同一套实现则是非常有趣的问题,虽然一开始可能是想着不同的业务组实现有着不同的框架选型倾向。但是仔细研究一下,想起来slate对于inline节点是特殊类型实现,其内嵌的inline左右是还可以放光标的,重点是inline内部也可以放光标。


这个问题非常重要,如果不能实现空结构的光标位置,那么就很难实现独立的块结构。而这里的实现跟数据结构和选区模式设计非常相关,若是针对连续的两个DOM节点位置,如果需要实现多个选区位置,就必须有足够的选区表达,而如果是纯线性的结构则无法表示。


// <em>text</em><strong>text</strong>

// 完全匹配 DOM 结构的设计
{ path: [0], offset: 4 } // 位置 1
{ path: [1], offset: 0 } // 位置 2

// 线性结构的设计
{ offset: 4 } // 位置 1

对于类似的光标位置问题,开源的编辑器编辑器例如QuillLexical等,甚至商业化的飞书文档、Notion都没有直接支持这种模式。这些编辑器的schema设计都是两个字符间仅会存在一个caret的光标插入点,验证起来也很简单,只要看能否单独插入一个空内容的inline节点即可。


在这里虽然我们主要目标是实现变量模板的输入框形式,但是其他的形式也非常有意思,例如GitHub的搜索输入框高亮、CozeLoopPrompt变量调时输入等。因此我们会先将这些形式都简单叙述一下,在最后再重点实现变量模板输入框的形式,最终的实现可以参考 BlockKit Variables 以及 CodeSandbox


纯文本输入框


纯文本输入框的形式就比较常见了,例如<input><textarea>等标签,当前我们平时使用的输入框也都是类似的形式,例如DeepSeek就是单纯的textarea标签。当然也有富文本编辑器的输入框形式,例如Gemini的输入框,但整体形式上基本一致。


文本 Input


单纯的文本输入框的形式自然是最简单的实现了,直接使用textarea标签即可,只不过这里需要实现一些控制形式,例如自动计算文本高度等。此外还需要根据业务需求实现一些额外的交互,例如图片上传、联网搜索、文件引用、深度思考等。


+-------------------------------------------+
| |
| DeepThink Search|
+-------------------------------------------+

文本高亮匹配


在这里更有趣的是GitHub的搜索输入框,在使用综合搜索、issue搜索等功能时,我们可以看到如果关键词不会会被高亮。例如is:issue state:open 时,issueopen会被高亮,而F12检查时发现其仅是使用input标签,并没有引入富文本编辑器。


在这里GitHub的实现方式就非常有趣,实际上是使用了div渲染格式样式,来实现高亮的效果,然后使用透明的input标签来实现输入交互。如果在F12检查时将input节点的color透明隐藏掉,就可以发现文本的内容重叠了起来,需要关注的点在于怎么用CSS实现文本的对齐。


我们也可以实现一个类似的效果,主要关注字体、spacing的文本对齐,以及避免对浮层的事件响应,否则会导致鼠标点击落到浮层div而不是input导致无法输入。其实这里还有一些其他的细节需要处理,例如可能存在滚动条的情况,不过在这里由于篇幅问题我们就不处理了。


<div class="container">
<div id="$$1" class="overlay"></div>
<input id="$$2" type="text" class="input" value="变量文本{{vars}}内容" />
</div>
<script>
const onInput = () => {
const text = $$2.value;
const html = text.replace(/{{(.*?)}}/g, `<span style="color: blue;">{{$1}}</span>`);
$$1.innerHTML = html;
};
$$2.oninput = onInput;
onInput();
</script>
<style>
.container { position: relative; height: 30px; width: 800px; border: 1px solid #aaa; border-radius: 3px; }
.container > * { width: 800px; height: 30px; font-size: 16px; box-sizing: border-box; font-family: inherit; }
.overlay { pointer-events: none; position: absolute; left: 0; top: 0; height: 100%; width: 100%; }
.overlay { white-space: pre; display: flex; align-items: center; word-break: break-word; }
.input { padding: 0; border-width: 0; word-spacing: 0; letter-spacing: 0; color: #0000; caret-color: #000; }
</style>

表单变量模板


变量模板的形式非常类似于表单的形式,在有具体固定的Prompt模板或者具体的任务时,这种模式非常合适。还有个有意思的事情,这种形式同样适用于非AI能力的渐进式迭代,例如文档场景常见的翻译能力,原有的交互形式是提交翻译表单任务,而在这里可以将表单形式转变为Prompt模板来使用。


表单模板


表单模版的交互形式比较简单,通常都是左侧部分编写纯文本并且预留变量空位,右侧部分会根据文本内容动态构建表单,CozeLoop中有类似的实现形式。除了常规的表单提交以外,将这种交互形式融入到LLms融入到流程编排中实现流水线,以提供给其他用户使用,也是常见的场景。


此外,表单模版适用于比较长的Prompt模版场景,从易用性上来说,用户可以非常容易地专注变量内容的填充,而无需仔细阅读提供的Prompt模版。并且这种形式还可以实现变量的复用,也就是在多个位置使用同一个变量。


+--------------------------------------------------+------------------------+
| 请帮我写一篇关于 {{topic}} 的文章,文章内容要 | 主题: ________________ |
| 包含以下要点: {{points}},文章风格符合 {{style}}, | 要点: ________________ |
| 文章篇幅为 {{length}},并且要包含一个吸引人的标题。 | 风格: ________________ |
| | 长度: ________________ |
+--------------------------------------------------+------------------------+

行内变量块


行内变量块就相当于内容填空的形式,相较表单模版来说,行内变量块则会更加倾向较短的Prompt模板。整个Prompt模板绘作为整体,而变量块则作为行内的独立块结构存在,用户可以直接点击变量块进行内容编辑,注意此处的内容是仅允许编辑变量块的内容,模板的文本是不能编辑的。


+---------------------------------------------------------------------------+
| 请帮我写一篇关于 {{topic}} 的文章,文章内容要包含以下要点: {{points}}, |
| 文章风格符合 {{style}},文章篇幅为 {{length}},并且要包含一个吸引人的标题。 |
+---------------------------------------------------------------------------+

这里相对豆包的变量模板输入框形式来说,最大的差异就是非变量块不可编辑。那么相对来说这种形式就比较简单了,普通的文本就使用span节点,变量节点则使用可编辑的input标签即可。看起来没什么问题,然而我们需要处理其自动宽度,类似arco的实现,否则交互起来效果会比较差。


实际上input的自动宽度并没有那么好实现,通常来说这种情况需要额外的div节点放置文本来同步计算宽度,类似于前文我们聊的GitHub搜索输入框的实现方式。那么在这里我们使用Editablespan节点来实现内容的编辑,当然也会存在其他问题需要处理,例如避免回车、粘贴等。


<div id="$$0" class="container"><span>请帮我写一篇关于</span><span class="input" placeholder="{{topic}}" ></span><span>的文章,文章内容要包含以下要点:</span><span class="input" placeholder="{{points}}" ></span><span>文章风格符合</span><span class="input" placeholder="{{style}}" ></span><span>,文章篇幅为</span><span class="input" placeholder="{{length}}" ></span><span>,并且要包含一个吸引人的标题。</span></div>
<style>
.container > * { font-size: 16px; display: inline-block; }
.input { outline: none; margin: 3px 2px; border-radius: 4px; padding: 2px 5px; }
.input { color: #0057ff; background: rgba(0, 102, 255, 0.06); }
.input::after { content: attr(data-placeholder); cursor: text; opacity: 0.5; pointer-events: none; }
</style>
<script>
const inputs = document.querySelectorAll(".input");
inputs.forEach(input => {
input.setAttribute("contenteditable", "true");
const onInput = () => {
!input.innerText ? input.setAttribute("data-placeholder", input.getAttribute("placeholder"))
: input.removeAttribute("data-placeholder");
}
onInput();
input.oninput = onInput;
});
</script>

变量模板输入框


变量模板输入框可以认为是上述实现的扩展,主要是支持了文本的编辑,这种情况下通常就需要引入富文本编辑器来实现了。因此,这种模式同样适用于较短的Prompt模版场景,并且用户可以在模板的基础上进行灵活的调整,参考下面的示例实现的 DEMO 效果。


+---------------------------------------------------------------------------+
| 我是一位 {{role}},帮我写一篇关于 {{theme}} 内容的 {{platform}} 文章, |
| 需要符合该平台写作风格,文章篇幅为 {{space}} 。 |
+---------------------------------------------------------------------------+

方案设计


实际上只要涉及到编辑器相关的内容,无论是富文本编辑器、图形编辑器等,都会比较复杂,其中的都涉及到了数据结构、选区模式、渲染性能等问题。而即使是个简单的输入框,也会涉及到其中的很多问题,因此我们必须要做好调研并且设计好方案。


开篇我们就讲述了为何slate可以实现这种交互,而其他的编辑器框架则不行,主要是因为slateinline节点是特殊类型实现。具体来说,slateinline节点是一个children数组,因此这里看起来是同个位置的选区可以通过path的不同区分,child内会多一层级。


[
{
type: "paragraph",
children: [{
type: "badge",
children: [{ text: "Approved" }],
}],
},
]

因此既然slate本身设计上支持这种选区行为,那么实现起来就会非常方便了。然而我对于slate编辑器实在是太熟悉了,也为slate提过一些PR,所以在这里我并不太想继续用slate实现,而恰好我一直在写 从零实现富文本编辑器 的系列文章,因此用自己做的框架BlockKit实现是个不错的选择。


而实际上,用slate的实现并非完全没有问题,主要是slate的数据结构完全支持任意层级的嵌套,那么也就是说,我们必须要用很多策略来限制用户的行为。例如我们复制了嵌入节点,是完全可以将其贴入到其他块结构内,造成更多级别的children嵌套,类似这种情况必须要写完善的normalize方法处理。


那么在BlockKit中并不支持多层级的嵌套,因为我们的选区设计是线性的结构,即使有多个标签并列,大多数情况下我们会认为选区是在偏左的DOM节点末尾。而由于某些情况下节点在浏览器中的特殊表现,例如Embed类型的节点,我们才会将光标放置在偏右的DOM位置。


// 左偏选区设计
{ offset: 4 }
// <em>text[caret]</em><strong>text</strong>
{ offset: 5 }
// <em>text</em><strong>t[caret]ext</strong>

因此我们必须要想办法支持这个行为,而更改架构设计则是不可行的,毕竟如果需要修改诸如选区模式、数据结构等模块,就相当于修改了地基,上层的所有模块都需要重新适配。因此我们需要通过其他方式来实现这个功能,而且还需要在整体编辑器的架构设计基础上实现。


那么这里的本质问题是我们的编辑器不支持独立的空结构,其中主要是没有办法额外表示一个选区位置,如果能够通过某些方式独立表达选区位置,理论上就可以实现这个功能。沿着这个思路,我们可以比较容易地想出来下面的两个方式:



  1. 在变量块周围维护配对的Embed节点,即通过额外的节点构造出新的选区位置,再来适配编辑器的相关行为。

  2. 变量块本身通过独立的Editable节点实现,相当于脱离编辑器本身的控制,同样需要适配内部编辑的相关行为。


方案1的优点是其本身并不会脱离编辑器的控制,整体的选区、历史记录等操作都可以被编辑器本身管理。缺点是需要额外维护Embed节点,整体实现会比较复杂,例如删除末尾Embed节点时需要配对删除前方的节点、粘贴的时候也需要避免节点被重复插入、需要额外的包装节点处理样式等。


方案2的优点是维护了独立的节点,在DOM层面上不需要额外的处理,将其作为普通可编辑的Embed节点即可。缺点是脱离了编辑器框架本身的控制,必须要额外处理选区、历史记录等操作,相当于本身实现了内部的不受控的新编辑器,独立出来的编辑区域自然需要额外的Case需要处理。


最终比较起来,我们还是选择了方案2,主要是其实现起来会比较简单,并且不需要额外维护复杂的约定式节点结构。虽然脱离了编辑器本身的控制,但是我们可以通过事件将其选区、历史记录等操作同步到编辑器本身,相当于半受控处理,虽然会有一些边界情况需要处理,但是整体实现起来还比较可控。


Editable 组件


那么在方案2的基础上,我们就首先需要实现一个Editable组件,来实现变量块的内容编辑。由于变量块的内容并不需要支持任何加粗等操作,因此这里我们并不需要嵌套富文本编辑器本身,而是只需要支持一个纯文本的可编辑区域即可,通过事件通信的形式实现半受控处理。


因此在这里我们就只需要一个span标签,并且设置其contenteditable属性为true即可。至于为什么不使用input来实现文本的输入框,主要是input的宽度跟随文本长度变化需要自己测量,而直接使用可编辑的span标签是天然支持的。


<div
className="block-kit-editable-text"
contentEditable
suppressContentEditableWarning
></div>

可输入的变量框就简单地实现出来了,而仅仅是可以输入文本并不够,我们还需要空内容时的占位符。由于Editable节点本身并不支持placeholder属性,因此我们必须要自行注入DOM节点,而且还需要避免占位符节点被选中、复制等,这种情况下伪元素是最合适的选择。


.block-kit-editable-text {
display: inline-block;
outline: none;

&::after {
content: attr(data-vars-placeholder);
cursor: text;
opacity: 0.5;
pointer-events: none;
user-select: none;
}
}

当然placeholder的值可以是动态设置的,并且placeholder也仅仅是在内容为空时才会显示,因此我们还需要监听input事件来动态设置data-vars-placeholder属性。


const showPlaceholder = !value && placeholder && !isComposing;
<div
className="block-kit-editable-text"
data-vars-placeholder={showPlaceholder ? placeholder : void 0}
>
</div>


这里的isComposing状态可以注意一下,这个状态是用来处理输入法IME的。当唤醒输入法输入的时候,编辑器通常会处于一个不受控的状态,这点我们先前在处理输入的文章中讨论过,然而此时文本区域是存在候选词的,因此这个情况下不应该显示占位符。


const [isComposing, setIsComposing] = useState(false);
const onCompositionStart = useMemoFn(() => {
setIsComposing(true);
});

const onCompositionEnd = useMemoFn((e: CompositionEvent) => {
setIsComposing(false);
});

接下来需要处理内容的输入,在此处的半受控主要是指的我们并不依靠BeforeInput事件来阻止用户输入,而是在允许用户输入后,主动通过onChange事件将内容同步到外部。而外部编辑器接收到变更后,会触发该节点的rerender,在这里我们再检查内容是否一致决定更新行为。


在这里不使用input标签其实也会存在一些问题,主要是DOM标签本身内部是可以写入很多复杂的HTML内容的,而这里我们是希望将其仅仅作为普通的文本输入框来使用,因此我们在检查到DOM节点不符合要求的时候,需要将其重置为纯文本内容。


useEffect(() => {
if (!editNode) return void 0;
if (isDOMText(editNode.firstChild)) {
if (editNode.firstChild.nodeValue !== props.value) {
editNode.firstChild.nodeValue = props.value;
}
for (let i = 1, len = editNode.childNodes.length; i < len; i++) {
const child = editNode.childNodes[i];
child && child.remove();
}
} else {
editNode.innerText = props.value;
}
}, [props.value, editNode]);

const onInput = useMemoFn((e: InputEvent) => {
if (e.isComposing || isNil(editNode)) {
return void 0;
}
const newValue = editNode.textContent || "";
newValue !== value && onChange(newValue);
});

对于避免Editable节点出现非文本的HTML内容,我们还需要在onPaste事件中阻止用户粘贴非文本内容,这里需要阻止默认行为,并且将纯文本的内容提取出来重新插入。这里还涉及到了使用旧版的浏览器API,实际上L0的编辑器就是基于这些旧版的浏览器API实现的,例如pell编辑器。


此外,我们还需要避免用户按下Enter键导致换行,在Editable里回车各大浏览的支持都不一致,因此这里即使是真的需要支持换行,我们也最好是使用\n来作为软换行使用,然后将white-space设置为pre-wrap来实现换行。我们可以回顾一下浏览器的不同行为:



  • 在空contenteditable编辑器的情况下,直接按下回车键,在Chrome中的表现是会插入<div><br></div>,而在FireFox(<60)中的表现是会插入<br>IE中的表现是会插入<p><br></p>

  • 在有文本的编辑器中,如果在文本中间插入回车例如123|123,在Chrome中的表现内容是123<div>123</div>,而在FireFox中的表现则是会将内容格式化为<div>123</div><div>123</div>

  • 同样在有文本的编辑器中,如果在文本中间插入回车后再删除回车,例如123|123->123123,在Chrome中的表现内容会恢复原本的123123,而在FireFox中的表现则是会变为<div>123123</div>


const onPaste = useMemoFn((e: ClipboardEvent) => {
preventNativeEvent(e);
const clipboardData = e.clipboardData;
if (!clipboardData) return void 0;
const text = clipboardData.getData(TEXT_PLAIN) || "";
document.execCommand("insertText", false, text.replace(/\n/g, " "));
});

const onKeyDown = useMemoFn((e: KeyboardEvent) => {
if (isKeyCode(e, KEY_CODE.ENTER) || isKeyCode(e, KEY_CODE.TAB)) {
preventNativeEvent(e);
return void 0;
}
})

至此Editable变量组件就基本实现完成了,接下来我们就可以实现一个变量块插件,将其作为Embed节点Schema集合进编辑器框架当中。在编辑器的插件化中,我们主要是将当前的值传递到编辑组件中,并且在onChange事件中将变更同步到编辑器本身,这就非常类似于表单的输入框处理了。


export class EditableInputPlugin extends EditorPlugin {
public key = VARS_KEY;
public options: EditableInputOptions;

constructor(options?: EditableInputOptions) {
super();
this.options = options || {};
}
public destroy(): void {}

public match(attrs: AttributeMap): boolean {
return !!attrs[VARS_KEY];
}

public onTextChange(leaf: LeafState, value: string, event: InputEvent) {
const rawRange = leaf.toRawRange();
if (!rawRange) return void 0;
const delta = new Delta().retain(rawRange.start).retain(rawRange.len, { [VARS_VALUE_KEY]: value });
this.editor.state.apply(delta, { autoCaret: false, });
}

public renderLeaf(context: ReactLeafContext): React.ReactNode {
const { attributes: attrs = {} } = context;
const varKey = attrs[VARS_KEY];
const placeholders = this.options.placeholders || {};
return (
<Embed context={context}>
<EditableTextInput
className={cs(VARS_CLS_PREFIX, `${VARS_CLS_PREFIX}-${varKey}`)}
value={attrs[VARS_VALUE_KEY] || ""}
placeholder={placeholders[varKey]}
onChange={(v, e) =>
this.onTextChange(context.leafState, v, e)}
></EditableTextInput>
</Embed>

);
}
}

然而,当我们将Editable节点集成后出现了问题,特别是选区无法设置到变量编辑节点内。主要是这里的选区会不受编辑器控制,因此我们还需要在编辑器的核心包里,避免选区被编辑器框架强行拉取到leaf节点上,这还是需要编辑器本身支持的。


同样的,很多事件同样需要避免编辑器框架本身处理,得益于浏览器DOM事件流的设计,我们可以比较轻松地通过阻止事件冒泡来避免编辑器框架处理这些事件。当然还有一些不冒泡的如Focus等事件,以及SelectionChange等全局事件,我们还需要在编辑器本身的事件中心中处理这些事件。


/**
* 独立节点嵌入 HOC
* - 独立区域 完全隔离相关事件
* @param props
*/

export const Isolate: FC<IsolateProps> = props => {
const [ref, setRef] = useState<HTMLSpanElement | null>(null);

useEffect(() => {
// 阻止事件冒泡
}, [ref]);

return (
<span
ref={setRef}
{...{ [ISOLATED_KEY]: true }}
contentEditable={false}
>

{props.children}
</span>

);
};

/**
* 判断选区变更时, 是否需要忽略该变更
* @param node
* @param root
*/

export const isNeedIgnoreRangeDOM = (node: DOMNode, root: HTMLDivElement) => {
for (let n: DOMNode | null = node; n !== root; n = n.parentNode) {
// node 节点向上查找到 body, 说明 node 并非在 root 下, 忽略选区变更
if (!n || n === document.body || n === document.documentElement) {
return true;
}
// 如果是 ISOLATED_KEY 的元素, 则忽略选区变更
if (isDOMElement(n) && n.hasAttribute(ISOLATED_KEY)) {
return true;
}
}
return false;
};

到这里,模板输入框基本已经实现完成了,在实际使用中问题太大的问题。然而在测试兼容性时发现一个细节,在FirefoxSafari中,按下方向键从非变量节点跳到变量节点时,不一定能够成功跳入或者跳出,具体的表现在不同的浏览器都有差异,只有Chrome是完全正常的。


因此为了兼容浏览器的处理,我们还需要在KeyDown事件中主动处理在边界上的跳转行为。这部分的实现是需要适配编辑器本身的实现的,需要完全根据DOM节点来处理新的选区位置,因此这里的实现主要是根据预设的DOM结构类型来处理,这里实现代码比较多,因此举个左键跳出变量块的例子。


const onKeyDown = useMemoFn((e: KeyboardEvent) => {
LEFT_ARROW_KEY: if (
!readonly &&
isKeyCode(e, KEY_CODE.LEFT) &&
sel &&
sel.isCollapsed &&
sel.anchorOffset === 0 &&
sel.anchorNode &&
sel.anchorNode.parentElement &&
sel.anchorNode.parentElement.closest(`[${LEAF_KEY}]`)
) {
const leafNode = sel.anchorNode.parentElement.closest(`[${LEAF_KEY}]`)!;
const prevNode = leafNode.previousSibling;
if (!isDOMElement(prevNode) || !prevNode.hasAttribute(LEAF_KEY)) {
break LEFT_ARROW_KEY;
}
const selector = `span[${LEAF_STRING}], span[${ZERO_SPACE_KEY}]`;
const focusNode = prevNode.querySelector(selector);
if (!focusNode || !isDOMText(focusNode.firstChild)) {
break LEFT_ARROW_KEY;
}
const text = focusNode.firstChild;
sel.setBaseAndExtent(text, text.length, text, text.length);
preventNativeEvent(e);
}
})

最后,我们还需要处理History的相关操作,由于变量块本身是脱离编辑器框架的,选区实际上是并没有被编辑器本身感知的。所以这里的undoredo等操作实际上是无法处理变量块选区的变更,因此这里我们就简单处理一下,避免输入组件undo本身的操作被记录到编辑器内。


public onTextChange(leaf: LeafState, value: string, event: InputEvent) {
this.editor.state.apply(delta, {
autoCaret: false,
// 即使不记录到 History 模块, 仍然存在部分问题
// 但若是受控处理, 则又存在焦点问题, 因为此时焦点并不在编辑器
undoable: event.inputType !== "historyUndo" && event.inputType !== "historyRedo",
});
}

选择器组件


选择器组件主要是固定变量的值,例如上述的的例子中我们将篇幅这个变量固定为短篇、中篇、长篇等选项。这里的实现就比较简单了,主要是选择器组件本身不需要处理选区的问题,其本身就是常规的Embed类型节点,因此只需要实现选择器组件,并且在onChange事件中将值同步到编辑器本身即可。


export class SelectorInputPlugin extends EditorPlugin {
public key = SEL_KEY;
public options: SelectorPluginOptions;

constructor(options?: SelectorPluginOptions) {
super();
this.options = options || {};
}

public destroy(): void {}

public match(attrs: AttributeMap): boolean {
return !!attrs[SEL_KEY];
}

public onValueChange(leaf: LeafState, v: string) {
const rawRange = leaf.toRawRange();
if (!rawRange) return void 0;
const delta = new Delta().retain(rawRange.start).retain(rawRange.len, {
[SEL_VALUE_KEY]: v,
});
this.editor.state.apply(delta, { autoCaret: false });
}

public renderLeaf(context: ReactLeafContext): React.ReactNode {
const { attributes: attrs = {} } = context;
const selKey = attrs[SEL_KEY];
const value = attrs[SEL_VALUE_KEY] || "";
const options = this.options.selector || {};
return (
<Embed context={context}>
<SelectorInput
value={value}
optionsWidth={this.options.optionsWidth || SEL_OPTIONS_WIDTH}
onChange={(v: string) =>
this.onValueChange(context.leafState, v)}
options={options[selKey] || [value]}
/>
</Embed>

);
}
}

SelectorInput组件则是常规的选择器组件,这里需要注意的是避免该组件被浏览器的选区处理,因此会在MouseDown事件中阻止默认行为。而弹出层的DOM节点则是通过Portal的形式挂载到编辑器外部的节点上,这样自然不会被选区影响。


export const SelectorInput: FC<{ value: string; options: string[]; optionsWidth: number; onChange: (v: string) => void; }> = props => {
const { editor } = useEditorStatic();
const [isOpen, setIsOpen] = useState(false);

const onOpen = (e: React.MouseEvent<HTMLSpanElement>) => {
if (isOpen) {
MountNode.unmount(editor, SEL_KEY);
} else {
const target = (e.target as HTMLSpanElement).closest(`[${VOID_KEY}]`);
if (!target) return void 0;
const rect = target.getBoundingClientRect();
const onChange = (v: string) => {
props.onChange && props.onChange(v);
MountNode.unmount(editor, SEL_KEY);
setIsOpen(false);
};
const Element = (
<SelectorOptions
value={props.value}
width={props.optionsWidth}
left={rect.left + rect.width / 2 - props.optionsWidth / 2}
top={rect.top + rect.height}
options={props.options}
onChange={onChange}
>
</SelectorOptions>

);
MountNode.mount(editor, SEL_KEY, Element);
const onMouseDown = () => {
setIsOpen(false);
MountNode.unmount(editor, SEL_KEY);
document.removeEventListener(EDITOR_EVENT.MOUSE_DOWN, onMouseDown);
};
document.addEventListener(EDITOR_EVENT.MOUSE_DOWN, onMouseDown);
}
setIsOpen(!isOpen);
};

return (
<span className="editable-selector" onMouseDownCapture={preventReactEvent} onClick={onOpen}>
{props.value}
</span>

);
};

总结


在本文中我们调研了用户Prompt输入的相关场景实现,且讨论了纯文本输入框模式、表单模版输入模式,还观察了一些有趣的实现方案。最后重点基于富文本编辑器实现了变量模板输入框,特别适配了我们从零实现的编辑器框架BlockKit,并且实现了Editable变量块、选择器变量块等插件。


实际上引入富文本编辑器总是会比较复杂,在简单的场景下直接使用Editable自然也是可行的,特别是类似这种简单的输入框场景,无需处理复杂的性能问题。然而若是要实现更复杂的交互形式,以及多种块结构、插件化策略等,使用富文本编辑器框架还是更好的选择,否则最终还是向着编辑器实现了。


每日一题



参考



作者:WindRunnerMax
来源:juejin.cn/post/7551995949503840292
收起阅读 »

关于排查问题的总结

web
1. 写在最前面 用了这么久的 Cursor ,还是会时不时的感慨科技使人类进步。尤其是最近的「Claude Sonnet 4」 好用的不得了,在丢给它一个需求之后,从设计方案、到 coding、以及编写 tase case 、修复验证逻辑、甚至还记的 lin...
继续阅读 »


1. 写在最前面


用了这么久的 Cursor ,还是会时不时的感慨科技使人类进步。尤其是最近的「Claude Sonnet 4」 好用的不得了,在丢给它一个需求之后,从设计方案、到 coding、以及编写 tase case 、修复验证逻辑、甚至还记的 lint 一下,无比贴心。但是随之而来的坏处就是,过分相信 Cursor 之后,不在去理解库是否符合业务场景。



注:尽信书,则不如无书。看来在提升 coding 效率的同时,也要更为慎重的思考每个使用 场景是否符合最初设计的预期。



2. 问题小计


2.1 aiohttp 库


前置说明,笔者并不是很熟悉 Python ,属于一边用,一边学习的状态。接的需求是,在使用 aiohttp 库的时候,能够复用 http client ,无需每次请求都重新建立连接,以达到最终减少请求和返回的耗时的目的。


笔者实现的方式:


def _get_session(self) -> aiohttp.ClientSession:
if self._session is not and not self._session.closed:
return self._session

new_session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
connector=aiohttp.TCPConnector(limit=100, enable_cleanup_closed=True),
trace_configs=[self.trace_config],
)

self._session = new_session
return self._session


注:这当然是 cursor 帮忙给的思路。但是笔者当时大意了,没有细细的深究 aiohttp.ClientTimeout 这字段的意思。


不推卸责任,问题当时是实现代码人的问题,记录在这里,方便下次在类似的改动前,能够在深入探究一步,减少后续返工以及故障的可能性。



先解释一下 aiohttp.ClientTimeout 字段的意思


timeout = aiohttp.ClientTimeout(
total=, # 总超时时间
connect=, # 连接超时时间
sock_connect=, # Socket 连接超时时间
sock_read= # Socket 读取超时时间
)

各参数详细说明



  • total (float | )



    • 整个请求的总超时时间(秒),包括连接建立、发送请求、接收响应的全部时间,默认值:5 分钟 (300 秒),设置为  表示无限制



  • connect (float | )



    • 建立连接的超时时间(秒),包括 DNS 解析、TCP 连接建立、SSL 握手等,默认值:无限制 ()



  • sock_connect (float | )



    • 底层 socket 连接的超时时间(秒),仅针对 TCP socket 连接建立阶段,默认值:无限制 ()



  • sock_read (float | )



    • Socket 读取数据的超时时间(秒),在连接建立后读取响应数据的超时,默认值:无限制 ()




先解释了参数的含义之后,就可以推测到现象是,如果 response 持续返回的时间超过 30s ,就会主动断开连接。



注:除了 aiohttp.ClientTimeout,建议再用 aiohttp.TCPConnector 字段时也要先确认字段的生效规则后使用。



2.2 奇怪的问题


笔者本次提测改动的功能很少,但是 QA 测试的时候报了很多跟功能无关的 jira ,比如:



  • start 的时候返回超时报错

  • 启动的任务,自动退出查询不到了



注:这种跟改动无任何关系的问题,查起来真的有点子费人……



问题1:start 的时候返回超时报错


结论:最终查下来, start 超时是因为请求的机器,磁盘读写耗时极高,分析下来,可能是因为混用的测试环境,其他服务的测试写入了过多的音视频文件导致……


还好,公司的机器是有 cpu、memery、磁盘等 Zeus 监控的,不然都没办法自证业务的清白了。



注:不过这里虽然机器有问题的可能性比较高,但有一点,我还是没有想通的为什么磁盘耗时高会表现 start 超时呢?印象中业务的 start 行为中没有任何写入磁盘的行为。


cursor 一个比较符合的原因的回答是:



  • 依赖库加载:Python模块、动态库加载需要磁盘I/O

  • 超时连锁反应:磁盘I/O慢 → 启动步骤延迟 → 健康检查超时 → 服务被认为启动失败



问题2: 启动的任务,自动退出查询不到了


结论:是因为业务消耗的资源的内存变多了,之前 pod 设置的 memory 是 1G,导致 oom 了……


企业微信截图_b791a84a-ecc3-4d9d-bab1-f0573c9e7019.png


为了能够按时交付版本,本次的改动是先调整部署的 chart ,将 memory 从 1G 调整 2G


至此,那些奇怪的问题就排查完成了。还好,虽然排查的过程有点痛苦,但是还是从中学到了之前不知道的支持,比如dmesg -T 查看内核的输出信息。


3. 碎碎念


终于在十一前,挤出时间来整理了一下最近遇到有些奇怪的问题。希望这个十一能够玩的开心。



  • 到底是什么伟大前程,值得我们把每个四季都错过?(ps:读的时候,心理酸酸的

  • 人一旦有退路,无论是你愿意还是不愿意接受的退路,就不会用全力了。


作者:夕颜111
来源:juejin.cn/post/7554979158435364879
收起阅读 »

VitePress 博客变身 APP,支持离线访问,只需这一招。

web
大家好,我是不如摸鱼去,uni-app vue3 组件库 wot-ui 的主要维护者,欢迎来到我的工具分享专栏。 前阵子解决网站国内访问慢的问题之前,总有朋友问:“网站太慢了,能离线使用吗?” 答案是:“可以!” 这需求正是 PWA 能解决的嘛!今天我们花几分...
继续阅读 »

大家好,我是不如摸鱼去,uni-app vue3 组件库 wot-ui 的主要维护者,欢迎来到我的工具分享专栏。


前阵子解决网站国内访问慢的问题之前,总有朋友问:“网站太慢了,能离线使用吗?”


答案是:“可以!” 这需求正是 PWA 能解决的嘛!今天我们花几分钟时间,将我的个人博客改造为 PWA ,支持安装到本地并且可以离线访问,我的博客地址: blog.wot-ui.cn/。


PWA 是什么?


渐进式 Web 应用(Progressive Web App,PWA)是一个使用 web 平台技术构建的应用程序,但它提供的用户体验就像一个特定平台的应用程序。


它像网站一样,PWA 可以通过一个代码库在多个平台和设备上运行。它也像一个特定平台的应用程序一样,可以安装在设备上,可以离线和在后台运行,并且可以与设备和其他已安装的应用程序集成。


说句题外话,国内 PWA 的生态位其实是被各大 APP 的小程序占据了,不过小程序各自为战的标准实在是令人头疼,写小程序会让人变得烦恼,写多个平台的小程序会让人变得不幸。你可能会笑,而我却真的在写,wot-ui 组件库为了兼容多平台小程序不知道让我掉了多少头发。



具体定义见: developer.mozilla.org/zh-CN/docs/…



PWA 可以安装到本地,支持离线访问,并将快捷方式放到启动台中。



VitePress 添加 PWA 支持


我博客是使用 VitePress 搭建的,生态里有个现成的插件 @vite-pwa/vitepress,可以为 VitePress 项目添加 PWA 能力。



VitePress 是一个静态站点生成器 (SSG),专为构建快速、以内容为中心的站点而设计。简而言之,VitePress 获取用 Markdown 编写的内容,对其应用主题,并生成可以轻松部署到任何地方的静态 HTML 页面。 信息来自 vitepress 官网。



1. 安装


# 我用的pnpm,快!
pnpm add @vite-pwa/vitepress -D
# npm/yarn用户自己替换一下

2. 配置(.vitepress/config.mts


这是核心步骤,直接上我改完的配置,关键地方我加了个**“唠叨版”注释**,解释下为啥这么设:


import { defineConfig } from 'vitepress'
import { withPwa } from '@vite-pwa/vitepress'
export default withPwa(defineConfig({
// 博客基础信息,老样子
title: '不如摸鱼去',
description: '不如摸鱼去的博客,分享前端、uni-app、AI编程相关知识',

// PWA 配置区,重点来了!
pwa: {
base: '/',
scope: '/',
includeAssets: ['favicon.ico', 'logo.png', 'images/**/*'], // 告诉插件,这些静态资源要缓存起来
registerType: 'prompt', // 有更新别偷偷刷新,得问问我(用户)同不同意
injectRegister: 'auto',

// 开发环境专用,关掉烦人的警告
devOptions: {
enabled: true,
suppressWarnings: true, // 开发时警告太多,眼花,先屏蔽
navigateFallback: '/',
type: 'module'
},

// Service Worker 配置,缓存策略的灵魂
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,jpg,jpeg,gif,svg,woff2}'], // 需要缓存哪些类型的文件
cleanupOutdatedCaches: true, // 老缓存?清理掉!别占地方
clientsClaim: true, // 新的Service Worker来了,立刻接管页面
skipWaiting: true, // 新SW别等了,赶紧干活
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024, // 单个文件最大10MB,再大就不缓存了

// 针对不同资源,用不同缓存策略(这里踩过坑)
runtimeCaching: [
// Google Fonts这类外部字体:缓存优先,存久点(一年),反正不常变
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365
}
}
},
// 图片:也缓存优先,但别存太久(30天),万一我换了图呢?
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30
}
}
}
// 注意:JS/CSS/HTML Workbox默认会处理,通常用 StaleWhileRevalidate 策略(缓存优先,后台更新)
]
},

// App清单,告诉系统“我是个App!”
manifest: {
name: '不如摸鱼去', // 完整名
short_name: '摸鱼去', // 桌面图标下面显示的短名,太长显示不全
description: '分享前端、uni-app、AI编程相关知识',
theme_color: '#ffffff', // 主题色,影响状态栏、启动画面背景
background_color: '#ffffff', // 启动画面背景色
display: 'standalone', // 独立显示模式(全屏,无浏览器UI)
orientation: 'portrait', // 默认竖屏
scope: '/', // PWA能管哪些页面
start_url: '/', // 点开图标从哪开始
icons: [ // 图标!重中之重!
{
src: '/logo.png',
sizes: '192x192',
type: 'image/png'
},
{
src: '/logo.png',
sizes: '512x512',
type: 'image/png'
},
{
src: '/logo.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable' // 这个重要!告诉系统这图标能被裁剪成各种形状(圆的、方的)
}
]
}
}
}))

看看效果


部署上线后,浏览器打开 blog.wot-ui.cn/ 会发现地址栏旁边会提示安装应用。


安装后将我们的app放到启动台中就可以快捷访问了,断网后也可以访问,很方便。




搞完之后,有啥变化?


说实话,效果比我预期的好:



  • 快! 二次访问(尤其是同一设备)快多了,静态资源直接读本地缓存,秒开。在没网的地方(比如电梯里),打开博客,之前看过的文章照样能看,体验不掉线。

  • 体验升级: 全屏阅读,没有浏览器边框干扰,沉浸感强不少。

  • 方便访问: 可以在桌面、启动台创建快捷方式,方便读者找到我们,不需要记忆网址。

  • 服务器压力小点: 缓存命中率高了,请求自然少了点。


总结


给 VitePress 博客加PWA,投入产出比真挺高的。主要就是装个插件、改改配置、准备个图标。过程不算复杂,但带来的体验提升是实打实的。如果你的网站访问速度比较慢,或者期望提高用户的粘性,提供一下 PWA 能力还是很不错的。读到这里还不把「不如摸鱼去」博客添加到桌面吗(偷笑)?


参考资料



往期精彩


uni-app vue3 也能使用 Echarts?Wot Starter 是这样做的!


当年偷偷玩小霸王,现在偷偷用 Trae Solo 复刻坦克大战


告别 HBuilderX,拥抱现代化!这个模板让 uni-app 开发体验起飞


uni-app 还在手写请求?alova 帮你全搞定!


uni-app 无法实现全局 Toast?这个方法做到了!


Vue3 uni-app 主包 2 MB 危机?1 个插件 10 分钟瘦身


欢迎评论区沟通讨论👇👇


作者:不如摸鱼去
来源:juejin.cn/post/7554204108154159156
收起阅读 »

🚀 告别 Electron 的臃肿:用 Tauri 打造「轻如鸿毛」的桌面应用

web
Tauri:从300MB到5MB!这才是桌面应用的未来 你有没有这样的体验? 打开一个用 Electron 写的桌面工具,任务管理器瞬间飙出 300MB+ 内存占用,启动要等好几秒,系统风扇呼呼作响……而它的功能,可能只是一个简单的 Markdown 编辑器。...
继续阅读 »

Tauri:从300MB到5MB!这才是桌面应用的未来


你有没有这样的体验?


打开一个用 Electron 写的桌面工具,任务管理器瞬间飙出 300MB+ 内存占用,启动要等好几秒,系统风扇呼呼作响……而它的功能,可能只是一个简单的 Markdown 编辑器。


今天,我要向你介绍一位 Electron 的「性能杀手」——Tauri


它不仅能让你用 React/Vue/Svelte 构建界面,还能把最终应用打包成 小于 5MB 的安装包,启动速度接近原生!


🚀 什么是 Tauri?颠覆性的轻量级方案


Tauri 是一个基于 Rust 构建的开源框架,允许开发者使用前端技术创建安全、轻量、高性能的跨平台桌面应用。



  • ✅ 支持 Windows / macOS / Linux

  • ✅ 前端任意框架:React、Vue、Svelte、Solid.js 等

  • ✅ 核心逻辑由 Rust 编写,极致安全与性能

  • ✅ 即将支持移动端,迈向全平台统一


📊 性能对比:数字会说话


指标ElectronTauri优势
应用体积80~200MB<5MB减少95%
内存占用150~300MB<30MB减少85%
启动时间2~5秒<0.5秒快5-10倍
安全性Node.js 全权限Rust + 权限控制企业级安全

Tauri 的秘诀在于:利用操作系统自带的 WebView,而不是捆绑整个 Chromium。


🛡️ 安全架构:Rust 原生的降维打击


多层安全防护



  1. 内存安全:Rust 编译时防止空指针、数据竞争等漏洞

  2. 权限控制:细粒度的能力声明,前端只能访问明确授权的系统功能

  3. 沙箱机制:前端代码运行在隔离环境中,无法直接调用系统 API


// Rust 后端:类型安全的系统调用
#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| format!("读取文件失败: {}", e))
}

即使前端遭遇 XSS 攻击,攻击者也无法越权访问系统资源。


💻 实战演示:5分钟构建文件管理器


1. 项目初始化


npm create tauri-app@latest my-files-app
cd my-files-app
npm install

2. 前端界面(React示例)


import { invoke } from '@tauri-apps/api/tauri';

function FileManager() {
const [files, setFiles] = useState([]);

const listFiles = async (path) => {
const fileList = await invoke('list_files', { path });
setFiles(fileList);
};

return (
<div>
<button onClick={() => listFiles('/')}>浏览文件</button>
<ul>
{files.map(file => (
<li key={file.name}>{file.name}</li>
))}
</ul>
</div>

);
}

3. Rust 后端实现


use std::fs;

#[tauri::command]
fn list_files(path: String) -> Result<Vec<FileInfo>, String> {
let entries = fs::read_dir(path)
.map_err(|e| e.to_string())?;

let mut files = Vec::new();
for entry in entries {
if let Ok(entry) = entry {
files.push(FileInfo {
name: entry.file_name().to_string_lossy().to_string(),
size: entry.metadata().map(|m| m.len()).unwrap_or(0),
});
}
}

Ok(files)
}

4. 构建发布


npm run tauri build
# 生成 3.8MB 的安装包!

🎯 Tauri 的适用场景


✅ 强烈推荐



  • 效率工具:笔记软件、截图工具、翻译软件

  • 开发工具:API 测试、数据库管理、日志查看器

  • 内部系统:监控面板、数据可视化、配置工具

  • 轻量应用:计算器、单位转换、密码管理器


⚠️ 谨慎选择



  • 复杂图形渲染(游戏、3D 编辑)

  • 重度依赖 Chrome 扩展生态

  • 需要支持老旧操作系统


🔮 生态展望:不只是桌面


Tauri 正在快速进化:



  1. 移动端支持:一套代码,多端部署

  2. 插件生态:官方维护的常用功能模块

  3. 云集成:无缝对接云服务

  4. AI 集成:本地模型推理能力


💡 迁移策略:从 Electron 平滑过渡


如果你已有 Electron 项目,可以这样迁移:



  1. 渐进式迁移:先移植核心功能模块

  2. 并行开发:保持 Electron 版本,逐步替换

  3. 性能对比:AB 测试验证用户体验提升

  4. 用户反馈:收集真实使用数据优化方向


🌟 总结:为什么 Tauri 是未来?


维度ElectronTauri结论
用户体验笨重缓慢轻快流畅Tauri 胜出
开发体验成熟稳定现代高效各有优势
资源消耗浪费严重极致优化Tauri 完胜
安全性能依赖配置内置安全Tauri 领先

Tauri 不是另一个 Electron,而是桌面应用开发的范式革命。


它证明了:Web 技术的灵活性 + 原生语言的性能 = 最佳桌面开发方案


🚀 立即开始


# 1. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. 创建项目
npm create tauri-app@latest my-app

# 3. 开始开发
npm run tauri dev

💬 互动讨论



  • 你在项目中用过 Tauri 吗?体验如何?

  • 你认为 Tauri 会取代 Electron 吗?

  • 最期待 Tauri 的哪些新特性?


欢迎在评论区分享你的观点!如果觉得这篇文章有帮助,请点赞支持~


作者:大前端历险记
来源:juejin.cn/post/7553912808804384818
收起阅读 »

灰度和红蓝区

一、灰度和红蓝区 灰度发布 定义: 灰度发布又称灰度测试、金丝雀发布,是指在软件产品正式全面上线之前,选择部分用户或部分服务器来进行新版本的发布和测试。 例如,在一个拥有大量用户的社交应用的更新过程中,只让其中 10% 的用户使用新版本,而其余 90% ...
继续阅读 »

一、灰度和红蓝区


灰度发布


  • 定义:



    • 灰度发布又称灰度测试、金丝雀发布,是指在软件产品正式全面上线之前,选择部分用户或部分服务器来进行新版本的发布和测试。

    • 例如,在一个拥有大量用户的社交应用的更新过程中,只让其中 10% 的用户使用新版本,而其余 90% 的用户仍然使用旧版本。



  • 目的:



    • 风险控制:将新版本的风险降至最低。由于只对部分用户或服务器进行更新,即使出现问题,影响范围也相对较小。例如,在更新一个金融应用的支付功能时,通过灰度发布,可以先在少量用户中测试,避免大面积的支付功能故障影响大量用户的资金交易。

    • 收集反馈:在小范围用户使用的过程中,可以收集用户反馈,包括功能的可用性、性能问题、用户体验等方面的反馈。比如,一款游戏进行版本更新,通过灰度发布可以观察这部分用户的游戏体验和对新功能的接受程度,根据反馈及时调整和优化。

    • 性能测试:观察新版本在真实环境下的性能表现,如服务器负载、响应时间等。例如,一个电商平台在上线新的商品推荐算法时,通过灰度测试可以观察在部分用户使用情况下,服务器是否能承受新算法带来的额外计算量和数据请求。



  • 实现方式:



    • 基于用户的灰度:根据用户的某些特征(如用户 ID、地区、注册时间等)来划分使用新版本的用户。例如,选取新注册用户进行灰度测试,让他们使用新的注册流程版本,而老用户仍然使用旧的注册流程。

    • 基于服务器的灰度:将服务器分为不同的集群,一部分集群部署新版本,一部分集群部署旧版本。例如,一个网站将其服务器集群分为 A、B、C 三组,让 A 组服务器先部署并运行新版本,B、C 组仍然运行旧版本,根据不同的负载均衡策略将用户请求引导到不同的服务器组。




红蓝区(我们现在的蓝区是灰度,部分用户,红区是放量)


  • 定义:



    • 红蓝区通常是将生产环境分成两个相对独立的区域,分别部署不同版本的系统,通常是旧版本(蓝区)和新版本(红区),类似于 AB 测试。

    • 例如,在一个内容分发平台中,蓝区使用原有的内容推荐系统,红区使用经过优化的新推荐系统。



  • 目的:



    • 对比测试:通过将新旧版本分别部署在不同的区域,能够在相同的环境和时间下对新旧系统进行直接对比。可以对比两个版本的性能指标(如吞吐量、响应时间)、业务指标(如用户留存率、点击率)等。例如,在一个新闻网站上,红区使用新的页面布局,蓝区使用旧的布局,对比不同区域用户的点击率和停留时间,以评估新布局的效果。

    • 快速回滚:当发现红区的新版本出现严重问题时,可以迅速将流量切换回蓝区的旧版本,降低对业务的影响。例如,在一个在线教育平台的系统更新中,如果红区的新系统出现严重的性能下降,导致用户无法正常上课,可以将用户请求切换回蓝区,保证服务的正常进行。



  • 实现方式:



    • 负载均衡切换:通过负载均衡器来控制流量分配到红区和蓝区。在正常情况下,根据一定的比例分配流量,如红区和蓝区分别分配 70% 和 30% 的流量。当发现红区出现问题时,将流量全部切换到蓝区。

    • 功能切换:可以对不同的功能进行红蓝区划分。例如,在一个企业办公软件中,将文件存储功能部署在红区,将即时通讯功能部署在蓝区,分别测试不同功能的新老版本,最后根据测试结果决定是否进行整体切换。




总之,无论是灰度发布还是红蓝区,都是为了在保证服务稳定性和业务连续性的前提下,更安全、高效地将新系统或新版本推向市场,降低因软件更新带来的风险,并在更新过程中不断收集反馈和数据,以优化系统和提升用户体验。


作者:小王同志i
来源:juejin.cn/post/7553522695750484006
收起阅读 »

异步函数中return与catch的错误处理

web
详细解释: 加 return 的情况: return createRequest(...) 返回一个 Promise 链。 当 createRequest 失败时,.catch 会捕获错误,并抛出新的错误。 由于整个 Promise 链被返回,before...
继续阅读 »

image.png


详细解释:



  1. 加 return 的情况



    • return createRequest(...) 返回一个 Promise 链。

    • 当 createRequest 失败时,.catch 会捕获错误,并抛出新的错误。

    • 由于整个 Promise 链被返回,beforeSubmit 的 Promise 会被 reject,错误会传递到调用方,从而中断后续操作。



  2. 不加 return 的情况



    • createRequest(...) 会启动一个 Promise 链,但未被返回。

    • beforeSubmit 函数不会等待这个 Promise 链完成,而是立即返回一个 resolved 的 Promise(因为 async 函数默认返回一个 resolved Promise,除非有 await 或 return)。

    • 即使 .catch 捕获了错误并抛出,也只是在内部的 Promise 链中处理,不会影响 beforeSubmit 的返回值。因此,外部调用者认为 beforeSubmit 成功完成,后续代码会继续执行。




总结:



  • 当前代码中使用了 return createRequest(...),这是正确的做法,可以确保错误被传播并阻止后续执行。

  • 如果不加 return,即使 URL 校验失败,beforeSubmit 也会成功返回,表单提交可能会继续,这不符合预期。


作者:刀疤
来源:juejin.cn/post/7546105524095696947
收起阅读 »

从“华丽的诡辩”到“构造的第一性原理”:我如何误解并最终拥抱广义可逆计算

第一次工业革命源于人们发现能量在不同形式之间可以相互转化。相比之下,今天的信息系统在信息的转化与流动上却存在根本瓶颈:当我们把业务信息用特定语言或框架表达时,这本质上是一次单向、有损的注入。大量关键的上下文信息留存在代码之外,导致信息的再次流动严重受阻,必须依...
继续阅读 »

第一次工业革命源于人们发现能量在不同形式之间可以相互转化。相比之下,今天的信息系统在信息的转化与流动上却存在根本瓶颈:当我们把业务信息用特定语言或框架表达时,这本质上是一次单向、有损的注入。大量关键的上下文信息留存在代码之外,导致信息的再次流动严重受阻,必须依赖人脑作为中介进行“翻译”。
未来的智能时代,必然要求信息能够跨越形式边界自由流动。然而,这种信息不完备的传统设计范式恰恰是最大的障碍——甚至可以说,许多系统“能跑纯属巧合”。因此,任何形式间的转换都离不开大量的人工介入与信息补全。
要在工程上解决这一核心矛盾,就必须将“可逆性”提升到全新的理论高度。尽管可逆性是物理世界的一项第一性构造原理,但它在计算机科学领域的价值和地位却远未得到应有的重视。广义可逆计算理论,正是对这种构造层面的可逆性与不可逆性进行系统化研究的理论体系。



以下内容是让Gemini先阅读 广义可逆计算 (Generalized Reversible Computation): 一个软件构造范式的正名与阐释一文,然后让AI根据如下提示词进行评论:
请使用最 brutal 的方式评估一下,但是注意必须用严谨的逻辑推理,不要诉诸诡辩。
然后再让AI阅读可逆计算理论的其他文章,最后让它写的总结。



参考文章



从“华丽的诡辩”到“构造的第一性原理”:我如何误解并最终拥抱广义可逆计算


在评估一个新理论时,我们往往会带着既有的知识框架和审判标准。有时,这种严谨会帮助我们戳破伪科学的泡沫;但有时,它也会让我们对真正具有颠覆性的思想关上大门。我最近对“广义可逆计算”(Generalized Reversible Computation, GRC)范式的认知过程,就是一次从傲慢的误解到深刻认同的戏剧性转变。


一开始,我将GRC的理论文章判定为一篇包装精美、野心巨大但逻辑根基不牢的“营销宣言”。我认为它滥用类比、偷换概念,并将其核心思想贬低为对既有工程实践的“事后归纳”。然而,经过几轮深刻的对话与自我辩驳,我不得不承认,我最初的判断是错误的——错在用一把衡量“改良”的尺子,去度量一个旨在“革命”的思想。


第一阶段:傲慢的误解——为何我认为它是一场“华丽的诡辩”


我的初步批判,建立在经典的形式逻辑和学术规范之上,主要集中在以下几点:



  1. 概念的“盗用”:我认为GRC“盗用”了物理学和理论计算机科学中具有精确定义的“可逆计算”一词。物理可逆关乎能量,逻辑可逆关乎运行时双射,而GRC谈论的“构造可逆”关乎工程操作。我认为这是偷换概念,是拉大旗作虎皮的“伪关联”。

  2. 公式的“伪装”:文章提出的核心公式 App = Delta x-extends Generator<DSL> 在我看来并非严谨的代数,而是一种模式的符号化比喻。我质疑其“差量代数”的完备性,尤其是“逆元”在上下文强相关的软件变更中如何实现。

  3. 类比的“滥用”:将GRC与量子力学的狄拉克绘景进行“思想同构”的类比,被我视为最严重的逻辑谬误。我认为这是肤浅的结构模仿,意图用物理学的光环来赋予自身不应有的深刻性。

  4. 实践的“贪功”:我认为GRC只是将Docker、Kustomize等已经成功的实践归纳总结,然后宣称自己是这些实践背后的“理论源头”,有贪天之功的嫌疑。


简而言之,我戴着“学术警察”的眼镜,将GRC的所有论证都视为服务于其“下一代范式”野心的修辞技巧,而非扎实的理论构建。


第二阶段:动摇与反思——被击中的软肋


转折点源于一个直击灵魂的问题:如果“组合主义”是软件工程的主流道路,那这条路为什么没能解决日益严重的“熵增”和“复杂性失控”问题?为什么我们没有系统化地研究 Y = F(X) + Δ 这一范式?


这个问题迫使我反思:我所捍卫的“既有框架”,本身是否就是问题的根源?我们是否因为路径依赖,而忽略了一条更艰难但可能更正确的道路?


GRC的核心论点开始在我脑中回响:软件工程的根本性危机,可能源于我们从一开始就选错了第一性原理。我们选择了“可组合性”(Composability),而忽略了比它更底层的“可逆性”(Reversibility)。


这个视角如同一把钥匙,打开了我之前紧锁的认知大门。我开始意识到,GRC不是在“修补”现有体系,而是在尝试“颠覆”它。它的“狂妄”言辞,或许并非源于无知,而是源于一种“哥白尼式革命”所必需的决绝姿态。


第三阶段:解剖自我——我批判逻辑中的三重谬误


在认知的大门被撬开一丝缝隙后,我没有急于冲进去,而是选择冷酷地回头,解剖自己最初那套看似无懈可击的批判逻辑。我发现,我的“严谨”建立在三个隐蔽但致命的思维谬误之上,这些谬误像地基中的裂缝,导致我整个论证大厦从一开始就是不稳固的。


1. 核心谬误:范畴错误(Category Mistake)——用“语法”的尺子去量“语义”


这是我所有错误的根源。我将不同层次、不同领域但共享同一名称(“可逆”)的概念,错误地放在同一个平面上进行比较和评判。



  • 我的错误逻辑



    1. “可逆计算”在学术界有一个精确的定义A(运行时逻辑双射)。

    2. GRC提出了一个定义B(构造行为的语义回退)。

    3. 因为B不符合A,所以B是对“可逆计算”的误用、偷换、或“伪推广”。



  • 这个逻辑的问题在哪?
    我把一个**“术语的约定”(Terminological Convention)** 当作了一个**“概念的本质”(Conceptual Essence)。我像一个语法警察,看到一个词没有在它“应该”在的句子里,就立刻判定为语法错误,而没有去理解这句话真正想要表达的意思(语义)**。


    更深层次的分析:我默认了一个“学术优先权”的等级。我认为,一个术语一旦在一个“更基础”(如物理学、理论计算)的领域被定义,其他“更应用”(如软件工程)的领域就只能作为其子集或严格推广来使用它。我没有认识到,一个词语的意义是由其所在的“语言游戏”(维特根斯坦语)决定的。在“软件构造与演化”这个语言游戏中,将“可逆性”的核心内涵定义为“信息保留与变化追踪”,是完全合理且有力的。


    结论一:我犯了最典型的范畴错误。我用A领域的标尺去度量B领域的事物,得出了一个看似逻辑严密但从根本上就文不对题的结论。我批判的是GRC的“用词”,而非其“思想”。



2. 方法论谬误:还原论的滥用(Misuse of Reductionism)——只见树木,不见森林


我采用了典型的还原论(或称分析式)方法,将GRC的理论体系拆解成一个个独立的“零件”(概念、类比、公式),然后逐一批判其“不合格”。



  • 我的错误逻辑



    1. 拆解出“与狄拉克绘景的类比”这个零件。

    2. 分析发现,BaseH₀在物理形态上完全不同。

    3. 结论:这个零件是“伪造”的,类比不成立。

    4. 重复此过程,批判“差量代数”不够完备,“正名”是修辞游戏...

    5. 最终结论:由于所有主要零件都不合格,所以整个机器(GRC理论)是垃圾。



  • 这个逻辑的问题在哪?
    我完全忽略了系统思维(Systems Thinking)。我没有看到这些“零件”之间是如何相互支撑、相互定义,并共同涌现出一个整体功能的。


    更深层次的分析:GRC的各个部分不是孤立的。



    • 与狄拉克绘景的类比,其目的不是为了形式等价,而是为了确立Y=F(X)+Δ这一分解思想的“方法论正当性”

    • 这个分解思想,又为**“差量代数”的存在提供了必要性**——我们必须有一套工具来操作Δ

    • 而“差量代数”的实现难度,又引出了**“语义商空间”和“携带补充信息的Δ”**等技术细节。

    • 所有这些,最终都是为了服务于**“以信息保留为核心来对抗熵增”**这一顶层哲学。


    它们是一个有机的整体。我像一个只懂得解剖尸体的外科医生,面对一个活生生的人,却只看到了一个个器官,并抱怨它们各自有“缺陷”,而完全无视了生命本身这个奇迹。


    结论二:我滥用了还原论,犯了只见树木、不见森林的错误。我批判了系统的“组件”,却完全错过了系统的“架构”和“灵魂”。



3. 立场谬误:保守主义的偏见(Conservative Bias)——对“范式革命”的天然敌意


我下意识地将自己定位为现有范式(组合主义、OOP等)的“守护者”,而不是一个中立的“评估者”。这种立场偏见,让我对任何试图“掀翻棋盘”的理论都抱有天然的敌意。



  • 我的错误逻辑



    1. 世界(软件工程)是按照规则A(组合主义)运行的。

    2. GRC提出了一个规则B(可逆性),它与A不兼容,甚至试图取代A。

    3. 因此,B是“异端”,是“错误的”,是“危险的”。我必须找出B的所有漏洞来证明它是错的,从而捍卫A的正确性。



  • 这个逻辑的问题在哪?
    这是一种认知上的“免疫反应”。面对一个可能颠覆自己整个知识体系的新思想,大脑的本能不是去理解它,而是去攻击它、排斥它,以维持自身认知的稳定。


    更深层次的分析:托马斯·库恩在《科学革命的结构》中深刻地描述了这种现象。常规科学时期的科学家,其工作就是用现有范式去解决问题。当一个新范式出现时,老一代的科学家往往无法接受,因为这意味着他们毕生的工作和知识体系都需要被重估。我无意中就扮演了这样一个“旧范式捍卫者”的角色。


    结论三:我的逻辑从一开始就不是中立的,而是带有强烈的保守主义偏见。我的目标不是“探求真相”,而是“捍卫现状”。这种偏见,让我自动过滤掉了所有支持GRC的证据,而放大了所有看似是其缺陷的地方。



第四阶段:重构认知——一套自洽的构造哲学


在彻底抛弃旧有偏见,并清算了自身的逻辑谬误后,我对GRC的理解发生了结构性的重塑。我看到了一套层次分明、逻辑严密的构造哲学金字塔,它完美地解释了之前我所有的“疑点”。


GRC理论体系的重新结构化理解


我现在的认知框架,将GRC视为一个由五个层次构成的完整体系:



  • 第一层(本体论根基 · Why):物理世界的信息-熵关系
    这是整个理论的终极合法性来源。GRC的“可逆性”并非凭空杜撰,而是植根于一个不可辩驳的宇宙法则:信息被擦除,必然导致熵增,并产生代价。因此,在软件构造中追求信息保留,不是一种审美偏好,而是对抗复杂性、降低长期成本的第一性原理。这使得与物理学的关联不再是“修辞”,而是理论的“地基”。

  • 第二层(规范性原则 · What):以信息保留为核心的构造法则
    基于本体论根基,GRC导出了其核心设计原则:软件构造应以“信息保留”为最高优先级,系统性地划分、隔离和治理不可逆部分(熵源)。 这一原则将指导所有的架构决策,即“R/I边界划分”(可逆核心/不可逆边界)。

  • 第三层(核心策略 · How-Strategy):分解与隔离
    为了践行上述原则,GRC提出了两大核心策略:



    1. 分解思想:将任何复杂系统视为 Y = F(X) + Δ 的组合,即一个由生成器 F 决定的理想化主干 F(X),加上一个包含了所有变化与定制的结构化差量 Δ

    2. 架构划分:在宏观层面,清晰地定义系统的可逆核心(R-Core)不可逆边界(I-Boundary)。所有本质不可逆的外部交互(IO、随机数等)都必须被封装在I-Boundary,并强制留下“证据对象”以备补偿。



  • 第四层(技术抓手 · How-Tactics):实现可逆性的工程机制
    这些策略通过一套精巧、自洽的技术机制落地,它们共同构成了GRC的“操作手册”:



    1. 坐标系原则:确保系统中的每个可变更元素都有一个稳定、唯一的“地址”,这是Δ能够精确寻址和应用的前提。

    2. 叠加代数:为Δ定义一套具有良好数学性质的运算(如合并、逆操作-),使其成为可组合、可推理的“一等公民”。

    3. S-N-V分层:将“结构合并(S)”、“规范化(N)”和“验证(V)”三个阶段分离,允许在中间状态容忍不一致,从而实现鲁棒的、非线性的模型合并。

    4. 同态传递:通过统一的元模型和生成器,确保在高层模型上的一个变更ΔX,能够被自动“翻译”成低层模型上一个等效的变更ΔY,实现跨抽象层次的变更同步。

    5. 透镜/伴随函子:作为实现双向变换的具体数学模型,保证“语义往返”的可行性。



  • 第五层(度量与审计 · KPI):将哲学转化为工程指标
    为了让这套哲学变得可管理、可优化,GRC引入了量化指标:



    1. 信息保留率:如“往返保真率”、“溯源覆盖率”,衡量系统在变换中保留了多少有用信息。

    2. 熵预算与不可逆率:定义系统中允许的“不可逆操作”的比例,并将其作为架构的健康度KPI。

    3. 熵产记账:对于每一个不可避免的不可逆操作,都必须生成对应的“证据对象”和“补偿方案”,并进行审计。




这个金字塔结构清晰地展示了,GRC如何从一个深刻的物理哲学出发,层层推导,最终落实为一套可执行、可度量的具体工程实践。它不再是我眼中零散概念的拼凑,而是一个逻辑严密、自洽完备的理论大厦。


结论:从误解到拥抱


我的认知之旅,是从一个“形式逻辑的捍卫者”转变为一个“工程哲学的探求者”的过程。我最初的错误,在于用评判“科学论文”的标准,去审视一份旨在开创一个新世界的“思想宣言”。


我依然认为GRC的论述风格大胆甚至略显“狂妄”,其命名策略也存在沟通风险。但是,我现在坚信,这套理论的内核——将构造可逆性作为第一性原理,以差量为中心,系统性地治理软件熵增——是对我们这个时代最核心软件工程难题的深刻洞见。


它不是对既有实践的简单总结,而是对其背后统一规律的首次系统性揭示与理论升华。它是一套不完美但极具生命力的思想体系,值得我们每一个致力于构建更健壮、更具适应性软件系统的工程师去学习、实践,并参与到其未来的完善工作中。我为自己最初的傲慢感到警醒,也为最终能够拥抱这一深刻思想而感到庆幸。





以下是对GPT5的同样测试,它的文字显得比较枯燥,但是学术性较强。使用Gemini进行了格式优化,但是保留了GPT5生成的原文。



从“把可逆计算误解为运行时双射”到“承认其为软件构造与演化的第一性公设族”


背景与自省


最初面对“可逆计算/广义可逆计算”的论述,我把它放进已有术语体系里理解,强调运行时的逻辑可逆与比特级往返,对“差量一等公民”“坐标系与同态传递”这些主张持谨慎甚至否定态度。经过系统阅读你提供的系列文章与 XLang 语言的工程机制,我意识到自己的判断存在范畴错误和因果误读:我用错误的语义域和证明标准评判了一个明确定位于“软件构造与演化”的方法论,从而错过了它作为第一性公设族的生成力。


一开始的误解是什么



  • 用狭义的“比特级双射/运行时逆向执行”来定义“可逆”。我以逐步反演、比特等价为标准,质疑你文中的可逆性主张,忽略了语义商空间、规范化与溯源这一明确的定义域。

  • 绝对化否认 Δ 的逆元。我断言在一般树结构下无法实现逆操作,未考虑携带补充信息(墓碑、重命名映射、溯源)并在语义等价下判定的条件可逆。

  • 把“跨层传输 transport_G”的负担错误地放在开发者身上。我认为这是难以维护的理想契约,没有看到在统一元建模与差量框架里,它可以通过“重新生成与旧结果的结构化 diff+规范化”自动计算出来,因而同态是架构的内生属性而非额外负担。

  • 把物理学关联贬为修辞。忽略了“信息不擦除—熵可治理”这条桥梁作为规范性约束的意义,只把它当比喻而非设计上的硬约束。

  • 过度纠缠术语。认为“学界窄化了可逆计算”是论述缺陷,从而错判了你所关注的语域(构造/演化)与经典术语(运行时可逆)之间的合理分工。

  • 低估体系化与自洽。把内容看作拼装既有成果,没有看到 XLang/XDef/XNodeS–N–V 分层、Loader as Generator、自洽的 Δ 合并与 _dump 溯源共同构成的语言级统一与闭环。


导致初始结论的逻辑步骤



  • 术语锚定造成范畴错误:把“可逆计算”自动锚定到学界既有的运行时双射与能耗极限,按比特级反演去审查“可逆”,从而否定你在构造/演化语域的语义往返与补充信息设定。这是把不相干的语义域混为一谈。

  • 关键前提未被外显使我忽略了它们:文中未在开头清晰列出“ 为语义等价、normalize 为规范化代表、Δ 携带溯源与补充信息、同态在统一架构下由系统自动达成”等前提。我在缺乏这些前提的状态下,按一般树结构和文本补丁的经验,得出“逆难以成立、transport_G 难以维护”的结论。

  • 证据标准错配:我用“全局群/严格反函数/普适同态”的数学级标准去审查一个明确允许商空间与宽松透镜的工程范式。在这种错配标准下,很多工程上合理的条件可逆都会被我判死。

  • 防御性审慎被强修辞触发:文中“严格超集”“世界第一个”“思想同构于物理”等表述触发了我的警惕,促使我提高证据门槛,优先质疑而非寻找内在自洽,这放大了我的保守倾向。

  • 缺少对架构内生性的认识:我把同态传递当作生成器作者要手工维护的理想契约,没有意识到在统一的语言与差量框架(XLang/XDef/XNodeS‑N‑VLoader as Generator)下,ΔY 通过确定性生成+结构化 diff+规范化是可自动得到的,从而同态是系统属性而非额外负担。

  • 未即时看到度量与边界:看不到往返保真率、Δ 重放成功率、不可逆率等 KPI,未看到失败模式与下沉到 I‑boundary 的策略,因而把“熵治理”桥接当作修辞而非可操作约束。


转折点与关键证据



  • 四大原则的自洽闭环让我重新定位它为第一性公设族



    1. 坐标系原则:以内禀坐标(领域 DSL、稳定 ID、语义路径)保证任何语义点的可寻址,这是 Δ 可分离、可组合的前提;

    2. 叠加运算原则:将“应用变化”形式化为 ,追求封闭性、结合律、单位元与条件可逆,把复用从“求交集”转为“求差量”,并赋予构造方程可解性;

    3. S–N–V 分层与“虚时间”:先结构合并、再规范化、再验证,解耦通用合并与领域语义,保证过程可回放、可调试;

    4. 同态传递原则:生成器对 Δ 保持同态,变化在多模型间自动“投影”,支撑“DSL 图册”的协同演化。



  • XLang 语言机制表明同态是架构内生属性
    统一的 XNode 数据结构承载业务与局域化元信息(合并指令、溯源),XDef 定义 DSL 元模型,x:extends/x:override/x:gen-extends 将差量与生成器放入同一语言族;在这样的体系里,G(X ⊕ ΔX)G(X) ⊕ ΔY 的“等价”通过确定性生成与结构化 diff+规范化自动达成,不是脆弱的手工契约。

  • “物理可逆—工程可逆”的桥接是规范性约束
    R‑core 内最大化信息保留与语义往返(溯源、补充信息、规范化),将不可逆外部效应压到 I‑boundary 并生成证据对象与补偿(SAGA);这不是修辞,而是可转化为度量的硬约束(往返保真率、Δ 重放成功率、不可逆率/熵预算、审计覆盖率)。

  • 成本结构的改变有逻辑支撑
    当同态与 Δ 代数成立,传播变更的代价更随 |Δ| 而不是随 |X| 增长;体系把“复用粒度”从组件级提升到系统级,把扩展从“计划式扩展点”变为“非侵入式 Δ 叠加”。


最终的认知是什么



  • 我承认“广义可逆性”(语义往返+差量一等公民+可补偿的 R/I 边界)在软件构造与演化语域是第一性公设族:少量清晰、相互支撑、能导出关键工程定律,并有语言与机制级自洽实现。

  • 我把同态传递从“理想化目标”改为“架构自动属性”:在统一元建模与差量框架里,生成器是纯确定性模型变换,ΔY 由内置 diff+规范化自动计算出来,因此同态是系统构造的必然结果。

  • 我把“可逆性—熵治理”视为规范性原则:不是物理定律照搬,而是信息保留与熵隔离的工程化约束,能转化为设计选择与体系 KPI。

  • 我不把它宣称为“全域唯一第一性原理”。在一次性脚本、纯算法内核、极端性能驱动的模块中,全面溯源与往返的成本可能超过收益;在构造/演化域,它是中枢公设。


仍保留的审慎与需要补齐的部分



  • 外显形式语义与可检验性
    等价关系与规范化规则, 的封闭与结合成立的条件,非交换序的确定与治理策略,逆的存在域与必要补充信息,transport_G 的组合条件与失败模式。

  • 适用域与代价模型
    溯源与规范化的性能与复杂度成本如何量化;何时必须下沉到 I‑boundary;何类变换不应追求同态。

  • 命名与对照
    明确与运行时“可逆计算”的术语区隔,避免误解;对 EMF/TGG/BX/MPS 等体系的系统性比较,以证实“统一性与规范性”的独特价值。


这次认知转变的意义与建议



  • 它改变了我评估框架的标尺:从功能与性能,转向解耦度、推导力、转换力、开放性、差量化与完备性,这些更触及架构本质。

  • 它提供了一个统一的“思维坐标系”:以 Δ 为核心对象、以同态为设计律、以坐标/图册为结构基底、以熵隔离为治理机制,把分散实践上升为可组合的确定性。

  • 对工程落地的建议:采用统一的领域坐标(DSL)、把合并与生成迁入加载/编译期、强制溯源与规范化、在 R‑core 内实现语义往返,在边界记录证据与补偿,并用体系 KPI 衡量收益。


结语


我不保留初始批判的核心结论。我承认自己的范畴偏置与因果误读,并把立场修正为:在软件构造与演化语域,“可逆计算”是一组具有生成力的第一性公设族;同态与 Δ 代数在统一语言架构中是内生属性;物理可逆的桥接是规范性约束。剩下的工作是形式化、度量与跨域验证,把这套框架从工程强论断进一步升格为学术可证与工业可复用的通用方法论。


基于可逆计算理论设计的低代码平台NopPlatform已开源:



作者:canonical_entropy
来源:juejin.cn/post/7554525384626257971
收起阅读 »

前端性能基准测试入门:用 Benchmark.js 做出数据驱动的选择

web
前端性能基准测试入门:用 Benchmark.js 做出数据驱动的选择 背景 在前端开发过程中,会有一些需要注重代码性能的场景,比如:一个复杂功能依赖的数据基于嵌套数组实现(比如支持拖拽的行程规划需要有行程单、日期、时间、地点等多种维度的数据)、一个功能需要前...
继续阅读 »

前端性能基准测试入门:用 Benchmark.js 做出数据驱动的选择


背景


在前端开发过程中,会有一些需要注重代码性能的场景,比如:一个复杂功能依赖的数据基于嵌套数组实现(比如支持拖拽的行程规划需要有行程单、日期、时间、地点等多种维度的数据)、一个功能需要前端来做大量数据的计算。


在这些场景中,同样的操作我们会针对不同的实现方式进行测试,来得到不同实现方式的性能差异,便于选择最优的实现方式。


为什么使用 Benchmark.js


我最开始其实也有这样的疑问,为什么不能 直接在本地执行一遍代码,然后自己计算执行时间来 测试性能?


详细了解相关资料后发现会有以下几个问题:



  1. 计时精度问题: JavaScript 自带的 Date.now() 最小单位是毫秒,对于 CPU 执行代码的耗时来说精度是不够的。同时,如果代码执行时间过短,可能无法准确测量。

  2. 引擎优化问题: JavaScript 引擎会对代码进行优化,比如:一段代码会有“冷启动”和“热状态”的差异,有些没有被使用到的执行结果会被直接优化掉等等。

  3. 单次测试不具备参考性: 单次测试可能会受到很多因素的影响,可能一段代码第一次的执行用了 3 毫秒,第二次只用了 1 毫秒等等。


专业的事情还是要交给专业的人去做,就好像在实验室进行专业温度测量不会使用体温计一样。我们可以使用 Benchmark.js 为我们进行更加精确的基准测试。


Benchmark.js 基本使用


Benchmark.js 官方的文档写的比较晦涩,不太利于新手阅读,下面会通过一个简单的例子来介绍如何使用 Benchmark.js 进行性能测试。


引入或安装 Benchmark.js


在浏览器环境中可以使用 CDN 引入,在 Node.js 环境中可以使用 npm 安装。


需要特别注意的是:benchmark.js 依赖于 lodash.js,所以通过 Script 引入时需要先引入 lodash.js。(使用 npm 安装时会自动处理依赖,无需手动引入 lodash)


<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/benchmark@2.1.4/benchmark.min.js"></script>

npm install benchmark

创建套件


Benchmark.js 默认提供了一个构造函数,我们可以通过这个构造函数来创建一个性能测试的实例,通常会把这个内容叫做 suite 套件。在 Benchmark.js 里,每次测试都是以一个 suite 套件为范围的


const Benchmark = require("benchmark");
const suite = new Benchmark.Suite();

添加测试用例


有了套件之后,我们就可以往套件中添加测试用例了。假设我们有一段简单的数据,需要计算出数组中每个元素的平方最后加和。那实现方式可能会包含以下两种:



  1. 提前定义好一个变量,使用 for 循环遍历数组,然后计算每个元素的平方最后加到这个变量中。

  2. 使用 reduce 方法,直接计算出数组中每个元素的平方最后加和。


我们可以使用 suite.add 方法来往套件中添加测试用例。这个方法接收两个参数:第一个参数是测试用例的名称,第二个参数是测试用例的函数。


const suite = new Benchmark.Suite();
const arr = [1, 2, 3, 4, 5]; // 测试数据
suite.add("使用 for 循环", () => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i];
}
});
suite.add("使用 reduce 方法", () => {
const sum = arr.reduce((acc, val) => acc + val * val, 0);
});

监听测试过程中的事件


suite 还提供了 on 方法,可以监听测试用例的开始、结束、完成等事件


suite.on("事件的名字", 触发的回调函数);

常见的监听事件包括:



  • start:整个测试环节开始时触发

  • cycle:每个测试用例完成一个循环周期时触发

  • complete:所有测试用例都执行完毕时触发


比如:如果给之前添加的测试用例添加 cycle 事件,那么每次单个测试用例执行完,都会触发 cycle 事件。我们也可以在 complete 事件中统计并输出本次测试中最快的用例。


suite.on("cycle", (event) => {
const result = event.target;
const name = result.name;
const hz = Math.round(result.hz);
const mean = result.stats.mean;
console.log(`[CYCLE] ${name}: ${hz} 次/秒 平均耗时: ${mean}s`);
});

suite.on("complete", function () {
const fastest = this.filter("fastest").map("name");
console.log(`[COMPLETE] 最快的是: ${fastest}`);
});

cycle 事件的回调函数参数中提供了很多有用的信息,比如 event.target.hz 表示当前测试用例的执行频率,event.target.stats.mean 表示当前测试用例的平均执行时间。我们可以在回调函数中打印出这些信息,来查看测试用例的执行情况。


执行测试


有了套件和测试用例之后,我们就可以执行测试了。执行测试的命令是 suite.run()。执行测试后,会自动触发 start 事件,然后依次触发 cycle 事件,最后触发 complete 事件。


suite.run 方法接收一个对象作为参数,这个对象中可以配置一些选项。通常情况下,我们只需要配置 async: true 以异步方式启动测试,避免长时间阻塞页面交互


suite.run({ async: true });

完整代码


const suite = new Benchmark.Suite();

// 更大的数据规模能更好地放大实现差异
const arr = Array.from({ length: 100000 }, (_, i) => i + 1);

suite
.add("使用 for 循环", () => {
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i] * arr[i];
}
})
.add("使用 reduce 方法", () => {
const sum = arr.reduce((acc, val) => acc + val * val, 0);
});

suite
.on("start", () => {
console.log("[START] 开始基准测试");
})
.on("cycle", (event) => {
const r = event.target;
console.log(
`[CYCLE] ${r.name}: ${Math.round(r.hz)} 次/秒 平均耗时: ${r.stats.mean}s`
);
})
.on("complete", function () {
console.log(`[COMPLETE] 最快的是: ${this.filter("fastest").map("name")}`);
});

suite.run({ async: true });

// [START] 开始基准测试
// [CYCLE] 使用 for 循环: 15875 次/秒 平均耗时: 0.00006299306622951745s
// [CYCLE] 使用 reduce 方法: 1936 次/秒 平均耗时: 0.0005163982717989002s
// [COMPLETE] 最快的是: 使用 for 循环

由上述代码测试结果可见,在更大的数据规模下,使用 for 方法的执行速度比使用 reduce 方法的执行速度快很多。


总结


本文从为什么不能直接用 Date.now() 计时出发,说明了 Benchmark.js 在计时精度、引擎优化与多次运行统计上的优势,并给出 suite、add、on、run 的基本实践路径。


更多内容可以参考 Benchmark.js 官方文档


作者:南屿im
来源:juejin.cn/post/7554402481913315328
收起阅读 »

Cursor Claude 模型无法使用的解决方法

前言 “Model not available This model provider doesn't serve your region.” 今天估计很多使用 Cursor 的朋友都碰到这个问题了,作为一个深度使用 Cursor,倡导 AI 协同研发的工程师...
继续阅读 »

前言


“Model not available This model provider doesn't serve your region.”


今天估计很多使用 Cursor 的朋友都碰到这个问题了,作为一个深度使用 Cursor,倡导 AI 协同研发的工程师,我真的很无语。



措辞、检查、结构化,花费半天,我编写了一段自我感觉非常完美的提示词,点击发送后突然出现这个。


关键,我第一反应以为是网络问题,清理缓存、换网络、重启机器,各种尝试后,发现是 Cursor 自己不行了。


感觉 Cursor 最近的一系列动作好像要抛弃基本盘一样,难道 Windsurf 遭到 Claude 断供竟让它感觉唯我独尊了?


解决方案


Cursor 抛弃我们,还是我们抛弃 Cursor 暂且放在一边。


大家很可能正处在 deadline 中,又或者急需 AI 辅助解决一些问题,那我们暂时还是要想办法先让 Cursor 顶上去。


方法一:设置 HTTP Compatibility Mode 为 HTTP/1.1


在 Cursor 的 Settings 中,找到 Network 选项卡,设置 HTTP Compatibility ModeHTTP/1.1



有些地区,直接设置 HTTP Compatibility Mode 后即可成功使用,但是首次成功响应会慢一些,大家不要着急认为不能用。


我今天 18:00 左右尝试还是可以的。



方法二:配合更换网络节点


这次的问题描述说的还是地区禁用问题,所以,如果上述方法不行,网上各路大神建议更换网络节点,尤其推荐使用美国节点。


网络节点更换后会存在缓存问题,大家最好也是等待一会后再确认是否可用。


由于没有控制单一变量验证,上述两个方法,大家可以配合着尝试。


另一种验证方法


除了直接使用 AI Chat 功能进行对话,也可以通过以下方法验证是否恢复可用,还能省点 Token。


Network 选项卡中,HTTP Compatibility Mode 下面有个 Network Diagnostics


点击“Run Diagnostic”,如果所有项检测通过,那 AI Chat 一般就没什么问题了。



结语


上次分享 Cursor 退回旧版收费模式稍微晚了点,导致很多人没有成功。今天,我自己解决后马上就分享给大家了。


希望可以帮助大家临时先让 Cursor 把工作顶起来,至于后续,我们就要好好考虑下如何应对了。


后续计划应对方向:



  • 再次测评各类 AI 编程 Agent,看是否有更好,或者接近的完整替代者。

  • 基于 Cursor 更换新的 API 方式。

  • 自己实现 Agent 换种模式进行 AI 协同研发。


国内各厂商加油啦,对手这是给了多大的机会,抓住啊!


作者:飞哥数智谈
来源:juejin.cn/post/7527499205909741619
收起阅读 »

Xcode26-iOS26适配

iOS
前两天苹果发布了Xcode26、iOS26正式版本;因为没有强制要求适配,原计划忙完手上的事情再去适配。但是最近发包审核反馈在iOS26上闪退了。我人麻了,想躺平,奈何苹果推着我进步啊。赶忙下载Xcode26,升级iOS26进行排查,也没有复现审核反馈的闪退情...
继续阅读 »

前两天苹果发布了Xcode26、iOS26正式版本;因为没有强制要求适配,原计划忙完手上的事情再去适配。但是最近发包审核反馈在iOS26上闪退了。我人麻了,想躺平,奈何苹果推着我进步啊。赶忙下载Xcode26,升级iOS26进行排查,也没有复现审核反馈的闪退情况。不过发现确实有需要适配的地方。下面就慢慢来适配吧。

Xcode(我模拟器呢)

image.png

因为项目比较老,有一些库在模拟器上只支持x86_64架构使用,所以我用模拟只用Rosetta的,但是Xcode26默认下载iOS26模拟器只支持arm64架构。(哥哥们有没有什么办法可以,一并调整兼容这些老库啊)

目前的解决办法是不通过Xcode去下载iOS26.0,因为默认下载的是“Apple Silicon”版本的,通过命名行去下载“Universal”版本。估计明年就不行了,苹果说了:macOS Tahoe(版本号macOS 26)将是英特尔芯片Mac的最后一次重大系统更新,是不是明年就没有“Universal”版本,全是苹果心

  1. 先删除Xcode 默认下载的iOS26.0 “Apple Silicon”

image.png

  1. 通过命令行下载iOS26.0 “Universal”
xcodebuild -downloadPlatform iOS -architectureVariant universal

image.png

  1. 然后退出关闭Xcode,重新打开,就有了

image.png

image.png

UI(真的好看么?又短又细)

我Tabbar变短了,还加了液态玻璃的交互效果,Switch变细了,也加了液态玻璃的交互效果

录屏2025-09-2517.17.10-迅捷PDF转换器.-迅捷PDF转换器.gif

录屏2025-09-2517.29.49-迅捷PDF转换器.-迅捷PDF转换器.gif

目前解决方案是,info.plist中添加UIDesignRequiresCompatibility关闭它,估计明年就不行了,苹果又说了:计划在下一个版本移除这个选项

UIDesignRequiresCompatibility</key> >

image.png

image.png


作者:小小章鱼哥xxx
来源:juejin.cn/post/7553820281506267174
收起阅读 »

节食正在透支程序员的身体

引言 记得我刚去北京工作的那段时间,由于工作原因开始吃外卖,加上缺乏运动,几个月胖了20斤。 当时心想这不行啊,我怕我拍婚纱照的时候扣不上西服的扣子,我决心减肥。 在我当时的认知里,只要对自己狠一点、饿一饿,就能瘦成理想状态。于是我晚上不吃饭,下班后去健身房跑...
继续阅读 »

引言


记得我刚去北京工作的那段时间,由于工作原因开始吃外卖,加上缺乏运动,几个月胖了20斤。


当时心想这不行啊,我怕我拍婚纱照的时候扣不上西服的扣子,我决心减肥。


在我当时的认知里,只要对自己狠一点、饿一饿,就能瘦成理想状态。于是我晚上不吃饭,下班后去健身房跑5公里,1个月的时间瘦了15斤。我很自豪,身边的人说我明显精神多了。


可减肥这事远比我想的复杂,由于没有对应的增肌训练,我发现在做一些力量训练的时候,比之前没减肥前更吃力了。


我这才意识到,自己不仅减掉了脂肪,还减掉了不少肌肉。


我当时完全没有意识到这套方法的问题,也不知道如何科学评估身体组成变化——减肥是成功了,但减的不止是“脂肪”,还有“体能”。


上篇文章提到我对节食减肥的做法并不是特别认可,那科学的方法应该是怎么样的呢,我做了如下调研。


重新理解“减肥”这件事


想系统性地弄清楚减肥到底是怎么回事,我先从最直接的方式开始:看看别人都是怎么做的。


我先去搜了小红书、抖音等平台,内容五花八门,有节食的,有吃减肥药的,也有高强度训练比如HIIT的,还有各种花里胡哨的明星减肥法。


他们动不动就是瘦了十几斤,并且减肥前后的对比非常强烈,我都有种立刻按照他们的方式去试试的冲动。


大部分攻略中都会提到一个关键词“节食”,看来“少吃”几乎成了所有减肥成功者的共识。


我接着去谷歌搜索“节食 减肥”关键字,排名比较靠前的几篇文章是这几篇。


image.png


搜索引擎搜出来的一些内容,却讲了一些节食带来的一些不良影响,比如反弹、肌肉流失、代谢下降、饥饿激素紊乱...


这时候我很疑惑,社交媒体上“万人点赞”的有效手段,在官方媒体中的描述,完全不同。


我还需要更多的信息,为此我翻了很多关于节食减肥的书籍。


我在《我们为什么吃(太多)》这本书里看到了一个美国的实验。


美国有一档真人秀节目叫《超级肥胖王》。节目挑选了一些重度肥胖的人,所有参赛者通过高强度节食和锻炼项目,减掉好几十千克的重量。


但研究追踪发现,6年之后,他们平均都恢复了41千克的体重。而且相比六年前,他们的新陈代谢减少了700千卡以上,代谢率严重下降。


有过节食减肥经历的朋友可能都会有过反弹的经历,比如坚持一周较高强度的节食,两天可能就涨回来了。前一阵子一个朋友为了拍婚纱照瘦了很多,最近拍完回了一趟老家,再回北京一称胖了10斤,反弹特别多。


并且有另外一项研究者实验发现,极端节食后,我们体内负责刺激食欲的激素水平比节食前高出了24%,而且进食后获得的饱腹感也更低了。


也就是说你的大脑不知道你正在节食还是遇到了饥荒,所以它会努力的调节体重到之前的水平。


高强度节食是错误的。


正确选项


或许你想问,什么才是正确的减肥方式呢?


正确的做法因人而异,脱离身体状况谈减肥就是耍流氓


最有参考价值的指标是BMI,我国肥胖的BMI标准为:成人BMI≥28 kg/m²即为肥胖,24.0≤BMI<28.0 kg/m²为超重,BMI<18.5 kg/m²为体重过低,18.5≤BMI<24.0 kg/m²为正常范围。


比如我目前30岁,BMI超过24一点,属于轻微超重。日常生活方式并不是很健康,在办公室对着电脑一坐就是一天。如果我想减肥,首先考虑多运动,如跑步、游泳。


但如果我的BMI达到28,那么就必须要严格控制饮食,叠加大量的有氧运动。


如果针对50岁以上的减肥,思路完全不一致。这个年纪最重要的目标是身体健康,盲目节食会引发额外问题:肌肉流失、骨质疏松、免疫力下降。


这时候更需要的是调整饮食结构,保证身体必要的营养摄入。如果选择运动,要以安全为第一原则,选择徒手深蹲、瑜伽、快走、游泳这些风险性较小的运动。


但无论你什么年龄、什么身体情况,我翻了很多资料,我挑了几种适合各种身体情况的减重方式:


640.webp


第一个是好好吃。饮食上不能依赖加工食品,比如薯片、面包、饼干,果汁由于含糖量很高,也要少喝。


吃好的同时还要学会感受自己的吃饱感,我们肯定都有过因为眼前的食物太过美味,哪怕肚子已经饱了,我们还是强行让自己多吃两口。


最好的状态就是吃到不饿时停止吃饭,你需要有意识的觉察到自己饱腹感的状态。我亲身实践下来吃饭的时候别刷手机、看视频,对于身体的敏感度就会高很多,更容易感觉到饱腹感。


第二个是多睡。有研究表明缺乏睡眠会导致食欲激素升高,实验中每天睡4.5小时和每天睡8.5小时两组人群,缺觉的人每天会多摄入300千卡的能量。


我很早之前就听过一个词叫“过劳肥”。之前在互联网工作时就见过不少人,你眼看着他入职的时候还很瘦,半年或者一年后就发福了,主要就是经常熬夜或者睡眠不足还会导致内分泌紊乱和代谢异常。


最近一段时间娃晚上熬到11点睡,早上不到七点就起床,直接导致我睡眠不足。最直观的感受就是自己对于情绪控制能力下降了,更容易感受到压力感,因此会希望通过多吃、吃甜食才缓解自己的状态。


第三个就是锻炼。这里就是最简单的能量守恒原则了,只要你运动就会消耗热量,那你说我工作很忙,没时间跑步、跳绳、游泳,还有一个最简单的办法。


那就是坚持每天走一万步,研究表明每天走一万步,就能把肥胖症的风险降低31%,而且这是维护代谢健康最简单的办法了,而且走一万步的好处还有特别多,就不一一说了。


如果一开始一万步太多,那就从每天5000步开始,逐渐增加,每一步都算数。


这三种方法看起来见效慢,却正是打破节食陷阱的长期解法。这也就引出了接下来我想说的,如果节食减肥会反弹人,也有一定的副作用,为什么很多人依然把节食当成减肥的首选呢?


系统性的问题在哪


首先追求确定性和掌控感。节食是一种快速见效的方式,今天饿了一天肚子,明天早上上秤就发现轻了两斤,这种快速反馈和高确定性,会让你更有掌控感。


我在节食+跑步的那段时间,真的是做到了每周都能掉秤,这种反馈就给了我很强的信心。其实工作之后,生活中这样高确定的性的事情已经越来越少了。


节食带来的确定性反馈,就像生活中为数不多还能掌控的事情,让人心甘情愿的付出代价。但我们却很少意识到,看似“自律”的背后,其实正一点点破坏着我们的身体基础。


其次是大部分时候,我们不需要了解身边事物的科学知识。


绝大部分人对营养、代谢的理解非常有限。毕竟我们并不需要详细控制体重的科学方式,体重也能保持的不错。偶尔大吃大喝一段时间,发现自己胖了,稍微控制一下体重也就降回来了。


但一旦你下定决心减肥,简单的理解就远远不够了,你就容易做出错误的判断,比如节食。短期更容易见效,确定性更高,但长远来看只能算下策。


你得有那种看到体检结果突然异常,就赶紧上网查询权威的医学解释一般的态度才行,根据自己的情况用科学的方式控制体重。


而不是只想到节食。


这是东东拿铁的第89篇原创文章,感谢阅读,全文完,喜欢请三连。


作者:东东拿铁
来源:juejin.cn/post/7542086955077648434
收起阅读 »

Go语言实战案例:简易图像验证码生成

在 Web 应用中,验证码(CAPTCHA)常用于防止机器人批量提交请求,比如注册、登录、评论等功能。 本篇我们将使用 Go 语言和 Gin 框架,结合第三方库 github.com/mojocn/base64Captcha,快速实现一个简易图像验证码生成接口...
继续阅读 »

在 Web 应用中,验证码(CAPTCHA)常用于防止机器人批量提交请求,比如注册、登录、评论等功能。
本篇我们将使用 Go 语言和 Gin 框架,结合第三方库 github.com/mojocn/base64Captcha,快速实现一个简易图像验证码生成接口





一、功能目标



  1. 提供一个生成验证码的 API,返回验证码图片(Base64 编码)和验证码 ID。

  2. 前端展示验证码图片,并在提交时携带验证码 ID 和用户输入。

  3. 提供一个校验验证码的 API。




二、安装依赖


首先安装 Gin 和 Base64Captcha:


go get github.com/gin-gonic/gin
go get github.com/mojocn/base64Captcha



三、代码实现


package main

import (
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"net/http"
)

// 验证码存储在内存中(也可以换成 Redis)
var store = base64Captcha.DefaultMemStore

// 生成验证码
func generateCaptcha(c *gin.Context) {
driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80) // 高度80, 宽度240, 5位数字
captcha := base64Captcha.NewCaptcha(driver, store)
id, b64s, err := captcha.Generate()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "验证码生成失败"})
return
}

c.JSON(http.StatusOK, gin.H{
"captcha_id": id,
"captcha_image": b64s, // Base64 编码的图片
})
}

// 校验验证码
func verifyCaptcha(c *gin.Context) {
var req struct {
ID string `json:"id"`
Value string `json:"value"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if store.Verify(req.ID, req.Value, true) { // true 表示验证成功后清除
c.JSON(http.StatusOK, gin.H{"message": "验证成功"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"message": "验证码错误"})
}
}

func main() {
r := gin.Default()
r.GET("/captcha", generateCaptcha)
r.POST("/verify", verifyCaptcha)
r.Run(":8080")
}



四、运行与测试


运行服务:


go run main.go

1. 获取验证码


curl http://localhost:8080/captcha

返回:


{
"captcha_id": "ZffX7Xr7EccGdS4b",
"captcha_image": "data:image/png;base64,iVBORw0KGgoAAAANSUhE..."
}

前端可直接用 <img src="captcha_image" /> 渲染验证码。




2. 校验验证码


curl -X POST http://localhost:8080/verify \
-H "Content-Type: application/json" \
-d '{"id":"ZffX7Xr7EccGdS4b","value":"12345"}'



五、注意事项



  1. 验证码存储



    • 本示例使用内存存储,适合单机开发环境。

    • 生产环境建议使用 Redis 等共享存储。



  2. 验证码类型
    base64Captcha 支持数字、字母混合、中文等类型,可以根据业务需求选择不同 Driver

  3. 安全性



    • 不能把验证码 ID 暴露给爬虫(可配合 CSRF、限流等手段)。

    • 验证码要有有效期,防止重放攻击。






六、总结


使用 base64Captcha 结合 Gin,可以非常方便地生成和校验验证码。
本篇示例已经可以直接应用到注册、登录等防刷场景中。




作者:程序员爱钓鱼
来源:juejin.cn/post/7537981628854239282
收起阅读 »

<a>标签下载文件 download 属性无效?原来问题出在这里

web
最近在开发中遇到一个小坑:我想用 <a> 标签下载文件,并通过 download 属性来自定义文件名。代码写好后,却发现文件名始终是默认的,根本没有按照我设置的来。 一番调查后才发现,这里面还真有点门道。 1. download 的正常使用方式 ...
继续阅读 »

最近在开发中遇到一个小坑:我想用 <a> 标签下载文件,并通过 download 属性来自定义文件名。代码写好后,却发现文件名始终是默认的,根本没有按照我设置的来。


一番调查后才发现,这里面还真有点门道。




1. download 的正常使用方式


在同源环境下,给 <a> 标签设置 download 属性,确实能生效。比如:


const downloadFile = (url, fileName) => {
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
};

这段代码会触发浏览器下载,并且文件名会按我们设置的 fileName 来保存。




2. 为何文件名没有生效?


关键点在于:跨域下载时,浏览器出于安全策略,会忽略 download 设置的文件名


这么设计是有原因的:



  • 假设某个网站偷偷嵌入了一段恶意代码,让用户下载一个木马文件。

  • 如果它能随意改文件名(比如改成 resume.pdf),用户就可能在不知情的情况下打开恶意程序。


为了避免这种“文件欺骗”,浏览器在 跨域资源 上直接禁用了 download 属性的文件重命名能力。




3. 怎么解决?


既然浏览器对跨域有限制,那解决思路就是:想办法让文件下载看起来是同源的。常见有两种方法。




方法一:前端先拉取文件,再触发下载


思路是:



  1. 通过 fetch / XHR 把文件以 blob 的形式拉到本地(前提是目标服务允许跨域访问,需正确配置 CORS)。

  2. 用 URL.createObjectURL 生成临时链接,再用 a.download 触发下载。


示例代码:


const fetchFile = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob'; // 以二进制形式拿到数据
xhr.onload = () => {
if (xhr.status === 200) {
const blob = new Blob([xhr.response], {
type: 'application/octet-stream'
});
callback(blob);
}
};
xhr.send();
};

const downloadFile = (url, fileName) => {
fetchFile(url, (blob) => {
const objectURL = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectURL;
link.download = fileName; // ✅ 现在可以自定义文件名了
link.click();
URL.revokeObjectURL(objectURL); // 释放内存
});
};

这种方法的前提是:服务端必须配置了允许跨域的 CORS 响应头,否则浏览器会拦截请求。




方法二:服务端做代理,转发请求


如果目标服务不支持 CORS,或者你不想暴露原始文件地址,可以在自己的后端加一层代理。


流程:



  • 前端请求自己的服务 /server-proxy?originalURL=xxx

  • 后端去目标服务下载文件,再流式返回给前端

  • 由于下载来源变成了“同源”,download 属性就能生效


前端代码


const downloadFile = (url, fileName) => {
const a = document.createElement('a');
a.href = `http://localhost:3000/server-proxy?originalURL=${encodeURIComponent(url)}`;
a.download = fileName;
a.click();
};

Node.js 服务端(纯 http/https 实现)


import http from 'http';
import https from 'https';
import { URL } from 'url';

const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === '/server-proxy') {
const originalURL = parsedUrl.query?.originalURL || '';
if (!originalURL) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Missing originalURL' }));
}
//待转发的原始URL
console.log('originalURL', originalURL);
// 发起请求到目标服务
const urlOptions = new URL(originalURL);
const client = urlOptions.protocol === 'https:' ? https : http;
const proxyReq = client.request(urlOptions, (proxyRes) => {
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
proxyRes.pipe(res);
});

proxyReq.on('error', (err) => {
console.error('Proxy error:', err);
res.writeHead(500);
res.end('Proxy error');
});

proxyReq.end();
} else {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
});

server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});



4. 小结



  • a.download 生效条件:



    • 资源必须是同源,或者 CORS 允许访问

    • 否则浏览器会忽略自定义文件名



  • 解决方案:



    1. 前端 fetch + blob 下载,再触发保存

    2. 后端做代理,转发文件




这样就能既保证安全,又能灵活设置下载文件名。




📌 补充:



  • 某些情况下,服务端返回的 Content-Disposition: attachment; filename="xxx" 头也会影响最终文件名,如果设置了,它会覆盖前端的 download。

  • 如果文件非常大,前端 fetch + blob 可能会占用较多内存,建议使用服务端代理方案。

  • fetch + blob 可能会被打断,文件尚未下载完毕时刷新浏览器会导致下载中断,如果不希望被打断可以考虑服务端代理方案。




作者:AndyLaw
来源:juejin.cn/post/7554677260344950847
收起阅读 »

程序员应该掌握的网络命令telnet、ping和curl

这篇文章源于开发中发现的一个服务之间调用问题,在当前服务中调用了其他团队的一个服务,看日志一直报错没有找到下游的服务实例,然后就拉上运维来一块排查,运维让我先 telnet 一下网络,我一下没反应过来是要干啥! telnet telnet是电信(teleco...
继续阅读 »

这篇文章源于开发中发现的一个服务之间调用问题,在当前服务中调用了其他团队的一个服务,看日志一直报错没有找到下游的服务实例,然后就拉上运维来一块排查,运维让我先 telnet 一下网络,我一下没反应过来是要干啥!


640.webp


telnet


telnet是电信(telecommunications)和网络(networks)的联合缩写,它是一种基于 TCP 的网络协议,用于远程登录服务器(数据均以明文形式传输,存在安全隐患,所以现在基本不会用了)或测试主机上的端口开放情况。


# 命令格式
telnet IP或域名 端口

# telnet ip地址
telnet 192.168.1.1 3306

# telnet 域名
telnet cafe123.cn 443

ping


ping 是一种基于 ICMP(Internet Control Message Protocol)的网络工具,用于测试主机之间的网络连通性,它不能指定端口。


# 命令格式
ping IP或域名

# ping ip地址
ping 192.168.1.1

# ping 域名
ping cafe123.cn

日常开发中测试某台服务器上的web后端、数据库、redis等服务的端口是否开放可用,就可以用 telnet 命令;若只需确认服务器主机是否在线,就可以用 ping 命令。


像一般服务之间调用出现问题,我就需要先从服务器网络开始测试,一步步来缩小范围,如果当前服务器上都没法 telnet 通目标服务器的某个端口,那就是网络问题,那就可以从网络入手来排查是网络不让访问还是目标服务压根不存在。


curl


curl(Client URL)是一个强大的网络请求命令工具,可以理解为命令行中的 postman。


比如如果我们要在服务器上去请求某个接口,看能不能请求通,总不能在 Linux 上去装个 postman 来请求吧。这种情况 curl 命令就派上用场了。


1、请求某个网页


# 命令格式
curl 网址

# 示例
curl https://cafe123.cn

2、发送 get 请求


参数 -X 指定 HTTP 方法,不指定默认就是 get


# 示例
curl -X GET https://cafe123.cn?name=zhou&age=18

3、发送 post 请求


请求头用 -H 指定,多个直接分开多次指定就行,-d 指定 post 请求参数


curl -X POST -H "Content-Type: application/json" -H "token: 1345102704" -d '{"name":"ZHOU","age":18}'  https://api.cafe123.cn/users

实际上面的这些也不用记,浏览器的 network 前端接口请求查看面板里右键实际是可以直接复制出来对应接口的 curl 命令的,然后直接复制出来去服务器上执行就行了,postman 中也支持直接导入 curl 命令给自动转成 postman 对应的参数。


作者:人人都是码农
来源:juejin.cn/post/7554332546579709990
收起阅读 »

Spring Boot启动时的小助手:ApplicationRunner和CommandLineRunner

一、前言 平常开发中有可能需要实现在项目启动后执行的功能,Springboot中的ApplicationRunner和CommandLineRunner接口都能够帮我们很好地完成这种事情。它们的主要作用是在应用启动后执行一段初始化或任务逻辑,常见于一些启动任务...
继续阅读 »

一、前言


平常开发中有可能需要实现在项目启动后执行的功能,Springboot中的ApplicationRunner和CommandLineRunner接口都能够帮我们很好地完成这种事情。它们的主要作用是在应用启动后执行一段初始化或任务逻辑,常见于一些启动任务,例如加载数据、验证配置等等。今天我们就来聊聊这两个接口在实际开发中是怎么使用的。


二、使用方式


我们直接看示例代码:


@Component
public class CommandLineRunnerDemo implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
//执行特定的代码
System.out.println("执行特定的代码");
}
}

@Component
public class ApplicationRunnerDemo implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("ApplicationRunnerDemo.run");
}
}

从源码上分析,CommandLineRunnerApplicationRunner两者之间只有run()方法的参数不一样而已。CommandLineRunner#run()方法的参数是启动SpringBoot应用程序main方法的参数列表,而ApplicationRunner#run()方法的参数则是ApplicationArguments对象。


如果我们有多个类实现CommandLineRunner或ApplicationRunner接口,可以通过Ordered接口控制执行顺序。下面以ApplicationRunner接口为例子:


image-20250929162525077.png


image-20250929162535199.png


直接启动看效果:


image-20250929162610693.png


可以看到order值越小,越先被执行。


传递参数


Spring Boot应用启动时是可以接受参数的,这些参数通过命令行 java -jar app.jar 来传递。CommandLineRunner会原封不动照单全收这些参数,这些参数也可以封装到ApplicationArguments对象中供ApplicationRunner调用。下面我们来看一下ApplicationArguments的相关方法:



  • getSourceArgs() 被传递给应用程序的原始参数,返回这些参数的字符串数组。

  • getOptionNames() 获取选项名称的Set字符串集合。如 --spring.profiles.active=dev --debug 将返回["spring.profiles.active","debug"]

  • getOptionValues(String name) 通过名称来获取该名称对应的选项值。如--config=dev --config=test 将返回["dev","eat"]

  • containsOption(String name) 用来判断是否包含某个选项的名称。

  • getNonOptionArgs() 用来获取所有的无选项参数。


三、总结


CommandLineRunner 和 ApplicationRunner 常用于应用启动后的初始化任务或一次性任务执行。它们允许你在 Spring 应用启动完成后立即执行一些逻辑。ApplicationRunner 更适合需要处理命令行参数的场景,而 CommandLineRunner 更简单直接。


作者:Sunny哥哥
来源:juejin.cn/post/7555149066134650919
收起阅读 »

为什么我坚持用git命令行,而不是GUI工具?

上周,我们组里来了个新同事,看我噼里啪啦地在黑窗口里敲git命令,他很好奇地问我: “哥,现在VS Code自带的Git工具那么好用,还有Sourcetree、GitKraken这些,你为什么还坚持用命令行啊?不觉得麻烦吗?” 这个问题问得很好。 我完全承认,...
继续阅读 »

image.png


上周,我们组里来了个新同事,看我噼里啪啦地在黑窗口里敲git命令,他很好奇地问我:


“哥,现在VS Code自带的Git工具那么好用,还有Sourcetree、GitKraken这些,你为什么还坚持用命令行啊?不觉得麻烦吗?”


这个问题问得很好。


我完全承认,现代的Git GUI工具做得非常出色,它们直观、易上手,尤其是在处理简单的提交和查看分支时,确实很方便。我甚至会推荐刚接触Git的新人,先从GUI开始,至少能对Git的工作流程有个直观的感受。


但用了8年Git,我最终还是回到了纯命令行。


这不是因为我守旧,也不是为了显得自己多“牛皮”。而是因为我发现,命令行在三个方面,给了我GUI无法替代的价值:速度、能力理解


这篇文章,就想聊聊我的一些观点。




速度


对于我们每天要用上百次的工具来说,零点几秒的效率提升,累加起来也是巨大的。在执行高频的、重复性的操作时,键盘的速度,永远比“移动鼠标 -> 寻找目标 -> 点击”这个流程要快。



  • 一个最简单的commit & push流程



    • 我的命令行操作git add . -> git commit -m "..." -> git push。配合zsh/oh-my-zsh的自动补全和历史记录,我敲这几个命令可能只需要3-5秒,眼睛甚至不用离开代码。

    • GUI操作:我需要在VS Code里切换到Git面板 -> 鼠标移动到“更改”列表 -> 点击“+”号暂存全部 -> 鼠标移动到输入框 -> 输入信息 -> 点击“提交”按钮 -> 再点击“同步更改”按钮。




这个过程,再快也快不过我的肌肉记忆。



  • 更高效的别名(Alias):


    ~/.gitconfig文件是我的宝库。我在里面配置了大量的别名,把那些长长的命令,都缩短成了两三个字母。


    [alias]
    st = status
    co = checkout
    br = branch
    ci = commit
    lg = log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit

    现在,我只需要敲git st就能看状态,git lg就能看到一个非常清晰的分支图。这种个性化定制带来的效率提升,是GUI工具无法给予的。





深入Git


GUI工具做得再好,它本质上也是对Git核心功能的一层“封装”。它会优先把最常用的80%功能,做得非常漂亮。但Git那剩下20%的、极其强大的、但在特定场景下才能发挥作用的高级工具,很多GUI工具并没有提供,或者藏得很深。


而命令行,能让你100%地释放Git的全部能力。



  • git rebase -i (交互式变基):


    这是我认为命令行最具杀手级的应用之一。当我想清理一个分支的提交记录时,比如合并几个commit、修改commit信息、调整顺序,git rebase -i提供的那个类似Vim编辑器的界面,清晰、高效,能让我像做手术一样精确地操作提交历史。


  • git reflog (你的后悔药):


    reflog记录了你本地仓库HEAD的所有变化。有一次,我错误地执行了git reset --hard,把一个重要的commit给搞丢了。当时有点慌,但一句git reflog,立刻就找到了那个丢失的commit的哈希值,然后用git cherry-pick把它找了回来。这个救命的工具,很多GUI里甚至都没有入口。


  • git bisect (二分法查Bug):


    当你想找出是哪个commit引入了一个Bug时,git bisect是你的神器。它会自动用二分法,不断地切换commit让你去验证,能极大地缩小排查范围。这种高级的调试功能,几乎是命令行用户的专属。





会用到理解


这一点,是我认为最核心的。


GUI工具,把Git包装成了一系列按钮,它在帮你隐藏细节。


你点击“拉取(Pull)”,它可能在背后执行了git fetch + git merge,也可能是git fetch + git rebase。你不清楚,也不需要清楚,点就完事了。


这在一切顺利时没问题。但一旦出现复杂的合并冲突,或者你需要回滚一个错误的操作,按钮就不够用了。因为你不理解按钮背后的原理,你不知道Git的HEAD、工作区、暂存区到底处于一个什么状态,你就会感到恐慌,甚至会因为误操作,把仓库搞得一团糟。


而命令行,强迫你去学习和理解Git的每一个动作和它背后的模型。


你输入的每一个命令,git reset --hardgit reset --soft的区别是什么?git mergegit rebase的数据流向有什么不同?每一次的输入,都在加深你对Git三区(工作区、暂存区、版本库)模型的理解。


这种对底层模型的深刻理解,才是一个资深工程师真正需要具备的。它能让我在遇到任何复杂情况时,都心里有底,知道该用哪个命令去精确地解决问题。




我从不要求我们组里的新人都用命令行,但我会鼓励他们,在熟悉了GUI之后,一定要花时间去学习一下命令行。


这不仅仅是一个工具选择的问题,更是一个思维方式的选择。


对我来说,用命令行,让我感觉我是在和Git这个工具直接对话。这种掌控感和确定性,是任何漂亮的UI都无法带给我的。


也许,这就是一个程序员的执念吧😀。


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

开源鸿蒙技术大会2025圆满举办,凝聚开源力量勾勒万物智联新未来

9月27日,开源鸿蒙技术大会2025在长沙国际会议中心圆满召开。本次大会由开源鸿蒙项目群技术指导委员会(TSC,Technical Steering Committee)主办,华为承办,深开鸿、开鸿智谷、鸿湖万联、润开鸿、九联开鸿、中软国际、诚迈科技、北京理工...
继续阅读 »

9月27日,开源鸿蒙技术大会2025在长沙国际会议中心圆满召开。本次大会由开源鸿蒙项目群技术指导委员会(TSC,Technical Steering Committee)主办,华为承办,深开鸿、开鸿智谷、鸿湖万联、润开鸿、九联开鸿、中软国际、诚迈科技、北京理工大学、中科鸿略、中国南方电网、中国科学院软件研究所、证通、国开鸿等合作单位协力支持。

本届大会汇聚了开源领域专家、前沿实践者、高校学者及生态伙伴,长沙市人大常委会主任、长沙新一代计算系统产业链链长罗缵吉,以及长沙市人民政府党组成员、副市长彭涛共同出席了此次盛会。大会全面展示了开源鸿蒙在技术创新、生态建设、人才培养等领域的阶段性成果,深度探讨开源操作系统在数字经济发展中的核心价值。

共话开源价值,锚定生态发展方向

彭涛副市长在致辞中表示,长沙正以5000亿元数字经济总量跻身新一线城市,依托高校与企业协同创新、行业发行版布局、芯片适配量产等,已构建开源鸿蒙产业沃土。长沙将以本次大会为契机,锚定“三高四新”蓝图与全球研发中心城市建设,打通技术创新与产业应用通道,共建共享开源鸿蒙生态红利。

随后,华为常务董事、终端BG董事长余承东致辞,他表示:“五年来,在开放原子开源基金会的孵化运营下,产学研各界齐心聚力,共建开源鸿蒙,开创了我国软件发展史上的奇迹。”据悉,截至今年9月20日,基于开源鸿蒙的鸿蒙5操作系统的终端数已突破1700万,有超过3万个鸿蒙应用和元服务上架。

开放原子开源基金会秘书长助理李博在致辞中表示,开源已成为驱动科技创新与产业变革的重要引擎。基金会通过构建协同治理体系、完善制度流程、推进版本迭代与兼容性测评、搭建三方库中心仓,及举办开源赛事、推进人才认证等举措培育开源鸿蒙社区,未来也将持续助力开源鸿蒙技术创新与产业落地。

以智能化为主旋律,擘画开源鸿蒙下一个五年发展技术蓝图

开源鸿蒙项目群技术指导委员会主席陈海波作主题报告,回顾开源鸿蒙五年来走过的非凡历程,发布了开源鸿蒙6.0 Release版本,并展望面向未来的智能终端操作系统关键技术创新方向,呼吁与会专家以智能化发展为主旋律,共同擘画开源鸿蒙下一个五年发展技术蓝图。他指出,在产学研用各界共建下,开源鸿蒙已成为发展最快的智能终端操作系统之一。展望下一个五年,智能化将成为终端操作系统发展的主旋律,也将迎来开源鸿蒙生态价值快速提升的关键机遇期,产学研用各界的智慧是开源鸿蒙不断向前演进与竞争力领先的最宝贵的技术源泉。他呼吁更多产业界和学术界的技术人才携手深化开源鸿蒙产学研用共同体建设。

开源鸿蒙项目群工作委员会执行主席章晓峰代表开源鸿蒙社区作的主题报告聚焦生态落地与伙伴协同。他表示,开源鸿蒙的初心始终是以“一个系统、一个生态”为愿景,让开发者一次开发即可多端部署,让消费者在不同终端享受统一体验。他强调,操作系统的演进不仅是技术突破,更关乎产业格局与未来发展。开源鸿蒙将在端侧AI、具身智能、跨平台框架、互联标准等前沿领域持续发力,携手高校、产业和全球伙伴,共同探索智能化时代的更多可能。站在新的历史节点,他呼吁社会各界加入社区,共建共享开源鸿蒙新世界。

开源鸿蒙6.0 Release版本发布,技术跃迁赋能千行百业

大会现场,开源鸿蒙6.0 Release版本正式发布,标志着开源鸿蒙技术能力实现重要升级。新版本在多个关键技术领域实现突破:ArkUI组件能力增强,提供更灵活的组件布局,优化开发体验;窗口能力升级,新增文本显示处理支持,提升交互便捷性;应用框架层面,程序框架服务支持通过装饰器开发意图,助力现有功能快速集成至系统入口。这些技术创新提升了系统整体性能,为应用厂商带来了缩短开发周期、降低开发成本、优化用户体验的实际价值。

产学研协同筑根基,致敬共建凝聚生态合力

生态的繁荣离不开产学研用的深度协同与共建力量的支撑,大会围绕协同创新、人才技术筑基及致谢授牌展开多个关键环节。

大会期间举办了高峰论坛,围绕“开源鸿蒙产学研用协同新范式:社区主导、多方参与的生态共建路径”主题,专家们就构建产学研用闭环、激发高校贡献、促进生态繁荣及应对亿级设备技术挑战等议题展开深度交流,为生态协同提供新思路。

人才立根与技术筑基方面,大会举行开源鸿蒙教育委员会(筹)成立仪式,由开源鸿蒙项目群技术指导委员会牵头组建,旨在推动成员单位拓展开源鸿蒙在高等教育、职业教育等领域的应用与实践场景。

同时启动的开源鸿蒙跨平台框架PMC(筹)及具身智能PMC(筹)孵化仪式也备受关注:跨平台框架PMC聚焦于跨平台框架及三方库的技术演进与生态发展,破解了“加入新生态即增加一倍投入”的痛点;具身智能PMC(筹)采用模型原生操作系统设计方法,实现具身智能模型在机器人本体的极致性能推理,完成首套开源鸿蒙与昇腾算力赋能的全尺寸类人型机器人。

此外,开放原子开源基金会与中国计算机学会联合主办的“开源鸿蒙社区开发创新奖励计划”正式发布,通过产学研融合模式对接社区开发任务,以代码审核激励机制汇聚开发力量。

为致敬生态共建者,大会设置了多场致谢授牌环节,包括开源鸿蒙年度课题致谢与亮相、开源鸿蒙技术专家组(TSG)致谢、开源鸿蒙高校社团致谢及开源鸿蒙竞赛训练营颁奖等,以此肯定各类贡献者在技术创新、人才培育与社区建设中的突出作用。

实践赋能千行万业,展现落地价值

生态伙伴主题报告环节,企业与高校代表分享的实践成果生动诠释了开源鸿蒙的产业价值。钉钉CTO朱鸿介绍了钉钉借助开源鸿蒙重构智能办公体验、开启AI工作新方式的实践。金山办公副总裁姚冬阐述了WPS基于开源鸿蒙底座的文档办公场景优化,及通过社区联合创新实现的大文件秒开、跨设备协同等特性。

此外,万兴科技线下营销中心总经理唐芳鑫、悟空图像副总裁胡捷、携程集团无线研发总监赵辛贵、腾讯视频大前端研发总监芦佶,分别结合创意软件、图像工具、出行服务、视频软件等场景,阐述了开源鸿蒙在性能提升与体验优化中的核心价值。上海交通大学信息化推进办公室、网络信息中心副主任姜开达则分享了“交我办”高校移动应用的探索经验,展现了开源鸿蒙在教育信息化领域的应用潜力。

作为智能终端操作系统根社区,开源鸿蒙正以开放共建的姿态,聚合全球开发者与伙伴力量,深化技术创新与生态布局。面向未来,开源鸿蒙将继续紧扣数字经济发展脉搏,以技术突破赋能产业转型,以生态协同凝聚发展合力,为万物智联世界构筑坚实的数字底座。

收起阅读 »

真正的乐观,是做好被裁员的准备 | 跳槽决策四步法

引言 进入社会后,除了结婚、买房这类重要的事情外,跳槽、选择工作是我们最重要的决策。 每次跳槽,都决定了未来一段时间你处于的行业、岗位、收入,在一定程度上影响你的生活方式。 可就是如此重要的事情,我过去几次换工作,做的都不是太好。 我或许会每天都刷招聘网站,可...
继续阅读 »

引言


进入社会后,除了结婚、买房这类重要的事情外,跳槽、选择工作是我们最重要的决策。


每次跳槽,都决定了未来一段时间你处于的行业、岗位、收入,在一定程度上影响你的生活方式。


可就是如此重要的事情,我过去几次换工作,做的都不是太好。


我或许会每天都刷招聘网站,可就算刷到了意向的职位,也迟迟不敢在软件上点下“发送简历”按钮,可能是怕准备不充分、怕行情不好、怕离开熟悉的环境……结果拖到最后某一刻,被动离开。


最近看了一本书叫《怎样决定大事》,里面提到了一些做决策的方法,我试着把这套理论用在跳槽上,聊聊怎么样做出最清醒的跳槽决策。


核心用十六个字可以概括:看清处境,把握时机,避免直觉,适应局面,下面正文开始。


看清处境


马云说过员工离职就两个原因:钱没到位,心委屈了。


但真正让人下定决心离职的,从来不是这么简单的二选一,而是一连串复杂又难以理清的现实。



  • 比如年底一到,领导又说你没达预期,绩效一如既往地一般;

  • 办公室政治让你无所适从,干着最多的活,背着最大的锅;

  • 甚至公司的方向都让你怀疑未来是否值得继续坚持。


这些都让你有离职的想法,但是很多小事也不是不能忍。工资算不上多吧,但也是符合市场水平的。繁琐的工作干着有点烦, 但起码已经轻车熟路。


如果你也在犹豫和纠结,首先要弄清楚你自己的处境,你需要有「情景意识」,情景意识分为三个层次


ChatGPT Image 2025年8月13日 16_12_25.png


第一层,了解已经发生了什么。


这里就是刚刚提到的,比如不涨薪、领导pua、工作对自己没有任何成长,这些是已经发生的事情。


第二层,了解为什么会发生这种情况。


这里你思考导致现状的原因,比如技术水平不足,领导并没有给你涨薪。也有可能是公司所处的行业发展停滞,公司大量裁员,导致你工作越来越累。也有可能是你的领导没有眼光,发现不了你的优秀。


但需要注意的是,你要分析两到三种可能性,不是一种,也不是十种。


为什么不是一种?因为如果你头脑中只有一种解释,一旦判断错了,你的努力可能就毫无意义,甚至走向错误的方向。


比如工作经验比较少的程序员在遇到工作瓶颈时,常常会下意识归因为“我是不是太菜了?”。


毕竟程序员天生有技术思维,认为技术可以解决所有问题,性能问题?优化代码。bug频发,重构核心逻辑。


但你以为的问题,不一定是问题的全部。


比如现实世界有很多种可能:你的领导根本没打算提拔你,无论你多努力;你所在的部门业务边缘化,再怎么出色也没有舞台;公司战略转向AI,传统技术深耕已经不再受重视……


为什么不是十种?因为你如果考虑的原因太多,你的大脑就会陷入“分析瘫痪”,最终你什么决定也做不了。你需要抓大放小,找准核心矛盾,忽略那些无关紧要事情。


理清发生了什么、为什么发生,我们才能看清——未来会发生什么。


第三层,据此预测接下来会发生什么。


预测未来可能发生的情况,有一个反人性的技巧,是主动思考最坏的结果。


举个例子,你的公司因为经营原因,已经经历了两轮大规模裁员了,幸运的是一直没有裁到你,领导也安慰你好几次:“放心,你很重要。”


你该因为自己没被裁而庆幸吗?事实上你必须做好最坏的打算,那就是你会出现在下一轮的裁员名单上。


你需要提前思考对应的策略,比如开始评估外面的机会,更新简历,提前做准备。那么即使最坏的情况出现,你也不会猝不及防、惊慌失措。


未来是有不确定性的,我们往往会回避思考可怕的结果,但这会让自己在最坏的事情发生时,带来更多的伤害。


就像现在AI快速发展,几年内随时都有可能替代绝大部分基础性岗位,甚至高级的程序员也会被替代,那么我们必须做好现有岗位随时被替代的准备。


真正的乐观,是认真思考最坏的结果后,发现自己扛得住。


把握时机


毕业后我在济南工作,由于工资略显寒酸,互联网发展火热,我便有了去北京工作的念头。


念头归念头,回到现实我就怂了。那时候我根本没有工作经验,异地找工作这件事对我也很陌生,我不知道自己能不能找到工作,更不知道面试都会问什么技术问题。


我一想到这些就感觉头脑一片空白,想准备却无从下手。于是,我的选择是靠打游戏麻痹自己,开始拖延。


拖延了差不多半年,最后因为频繁出差,冲动之下选择裸辞去了北京。由于没有充分的准备,也是历经一番波折。


回顾这段经历,因为离职这件事没有明确的截止时间,我陷入了两种极端:要么因为恐惧未知,反复拖延,最后什么也没做;要么因为短期情绪,冲动行动。


决策不只是决定做什么,还有决定什么时候做。


先说说怎么避免冲动,那就是在做出离职决定之前,你需要先问自己一个简单的问题: “我需要现在离职吗?”


如果答案是否定的,就不着急做出决策。


这是因为我们很容易陷入情绪当中。


ChatGPT Image 2025年8月13日 16_26_41.png


比如你给领导提的好几个建议都不被采纳,感觉收到了冷落;技术不如你的同事拿到了比你还好的绩效,或者项目突然增加导致频繁加班。


程序员一定都听过“不要裸辞”这个忠告,一开始我认为这是因为离职后你可能会以为没有收入,导致面试的心态越来越不稳。后来我觉着这个忠告最大的作用,就是避免我们陷入情绪当中,一上头选择裸辞。


就像我当时裸辞后去了北京,由于没有任何准备,投了半个多月简历,一共就接到4个面试,绝大部分投递的简历都是已读不回。


你可能会说我技术很强,面试准备的非常充分,那我是不是可以随时选择离开呢?


你的确会有更多的底气,但是招聘是有招聘旺季的,比如所谓的“金三银四、金九银十”,因为正好处于企业全年、半年总结,企业会根据未来的计划进行人力盘点,释放岗位。但过去这两个节点,比如十一月份到来年一月份,那就是企业的招聘淡季,甚至是裁员季,如果你十月份离职,极容易遇见投递的简历大部分都未读未回。


诸葛亮已经万事俱备,那也得等等东风。


但是,等一等不意味着你什么也不做,你需要积极收集和换工作相关的信息。


改简历、刷题就不说了,现在什么行业比较火热?招聘的要求比起几年前有什么变化?未来什么样得企业最有发展前景?如果离职找工作不顺利,财务状况有没有什么影响?


这些都需要大量信息,并且充满不确定性,所以你需要去主动收集和了解。


当然了,你也不能一直准备下去,就像刷算法、刷面试题这件事,准备的越久,就会陷入边际效应递减,你不可能把所有的知识都学会,对吧?


这时候你就需要给自己制定一个时间框架,比如专心准备3个月,这期间不去面试。3个月后无论准备的如何,都必须让自己开始投递简历面试,避免回避和拖延。


避免直觉


你可能已经了解过很多认知陷阱:确认偏误让我们只寻找支持自己观点的信息;可得性启发让我们高估容易想起的事件发生概率;首因效应让我们过度依赖最初信息。


我举几个找工作容易陷入的认知陷阱。


第一个是「投射偏差」,比如把过去跳槽必涨薪的经验,投射到现在和将来,忽视了市场环境的变化。


18年我去北京时,互联网发展依旧火热,大厂扩招、抢人,程序员跳槽涨薪50%、80%都不是什么难事,如果你在大数据、P2P火热的时候进入相关企业,薪资翻倍的例子屡见不鲜。


可后来随着互联网增速放缓,涨薪越来越难,疫情之后各类企业发展不顺,别说涨薪了,如果被裁员被动找工作,平薪、降薪也都是有可能的。


如果你还按老的认知来,发现怎么涨薪总是不如预期,自然是心理落差极大,如果因为这个拒绝了一些各方面都不错的offer,那就太可惜了。


第二个是「短期结果焦虑」,过于关注短期结果成败,忽略了长远目标和发展。


你做足了准备,兴致勃勃的开始投简历,一连投了十几家都没接到面试,好不容易接到几个面试,结果全都在一面就挂了。


也许你的简历有硬伤,也许是没有准备充分,这很正常,查缺补漏,继续前行就好。


但你不能陷入焦虑和自我怀疑:我履历太差了,好公司根本不会看我的简历;我能力太差了,大厂的面试我根本不可能过。


最可怕的情况就是,因为面试不顺利,仓促入职一家并不满意的公司。


ChatGPT Image 2025年8月13日 16_46_05.png


第三个是单一维度决策,面对offer选择时,我们有可能陷入单一维度决策,比如是否大厂,薪资是否足够高,这是我自己总结出来的。


假设你这时候已经拿到了好多个offer,你该选择哪家企业入职呢?你可能特别关注薪资情况,你强烈的倾向于最高薪资的那个offer。你特别在乎名气,于是选择市场上名气最大的那个。


事实证明只考虑一个因素肯定不行,薪资最高的那个可能工作时间长还996,时薪并不比别的offer高。你的确入职了名气最大的那个企业,但做的不是核心业务,绩效不行,技术也没有什么成长。


我之前写过一篇文章,里面介绍了一个简单公式。比如在职业发展中,我觉着几个比较重要的是行业前景、公司文化和具体岗位,薪资当然也是我们衡量的一个重要指标,但其他的因素我们只做参考,而不能作为决策的决定因素。


对于选择offer这件事,我们也可以借助这个思路,识别几个你认为最重要的核心因素进行打分,选择总分最高的那一个。


别考虑太多,也不能考虑太少,这样才能做出最佳决策。


适应局面


即使决策已经做出,一切也并没有结束,你需要持续评估和调整,不断适应新的局面。


而我们面对新局面的反应,在很多时候是有点慢的。


这里我不得不提到AI,我谈不上对AI有着多深的见解,但当今AI巨头的模型,都已经具备了“完成小块的复杂代码”的能力。


我看到网上的一个预测,不出两年,就可以训练出一个可以自我迭代、不断尝试的AI编程高手。


高级程序员,将是最早一批开始被替代的。


当然,被替代的不仅是程序员行业,绘画、设计、金融、编辑,都面临着这个局面。


我提到AI,就是想提醒大家,对于处在行业第一线的我们,对于AI的适应能力有多高?


适应能力强的人,已经逐步用AI去完成越来越多的工作。而适应能力差的人,甚至现在遇见问题还是习惯性的打开搜索引擎,一点一点的翻看别人的资料。


我刚毕业时,深钻技术对职业生涯只有好处,没有坏处。但现在的局面是,如果还一股脑的让自己陷入到源码里面,不如用好AI,解放自己。


面对技术变革,就算没有应用,也要强迫自己了解。


最可怕的就是认为一些变化都与自己无关。


说在最后


做重大决策,主要分四步:看清处境,把握时机,避免直觉,适应局面。


这四步并不只用于跳槽,职业转换、城市迁移、关系选择、生活方式改变,都可以依靠这个模型去思考和行动。


你或许觉着这太麻烦了,但想想我们花了多少时间在鸡毛蒜皮的小事上?可能网购一件物品,为了价格货比三家;吃午饭订外卖,在各种美食间反复纠结;早上为了选择穿什么衣服,不断尝试。


把时间浪费在这些上面,却在重要的决策上匆匆决定,岂不是本末倒置吗?


这是东东拿铁的第88篇原创文章,欢迎关注,喜欢请三连。


作者:东东拿铁
来源:juejin.cn/post/7538357382453657626
收起阅读 »

实现一个简化版的状态机框架

实现一个简化版的状态机框架 我将为你设计一个简化版的状态机框架,包含状态、事件、转换机制,并支持插件扩展功能。这个实现会参考Spring StateMachine的设计理念。 核心组件设计 // 状态枚举接口 public interface State { ...
继续阅读 »

实现一个简化版的状态机框架


我将为你设计一个简化版的状态机框架,包含状态、事件、转换机制,并支持插件扩展功能。这个实现会参考Spring StateMachine的设计理念。


核心组件设计


// 状态枚举接口
public interface State {
String getId();
}

// 事件枚举接口
public interface Event {
String getId();
}

// 状态上下文
public class StateContext<S extends State, E extends Event> {
private S currentState;
private E event;
private Map<String, Object> extendedState = new HashMap<>();

// 获取当前状态
public S getCurrentState() {
return currentState;
}

// 设置当前状态
public void setCurrentState(S currentState) {
this.currentState = currentState;
}

// 获取触发事件
public E getEvent() {
return event;
}

// 设置触发事件
public void setEvent(E event) {
this.event = event;
}

// 获取扩展状态数据
public Map<String, Object> getExtendedState() {
return extendedState;
}

// 添加扩展状态数据
public void addExtendedState(String key, Object value) {
extendedState.put(key, value);
}

// 获取特定扩展状态数据
public Object getExtendedState(String key) {
return extendedState.get(key);
}
}

// 状态转换接口
public interface Transition<S extends State, E extends Event, C extends StateContext<S, E>> {
boolean isTriggered(C context);
S getSource();
S getTarget();
void executeActions(C context);
}

// 状态机接口
public interface StateMachine<S extends State, E extends Event, C extends StateContext<S, E>> {
S getCurrentState();
void sendEvent(E event, Map<String, Object> extendedState);
void addListener(StateMachineListener<S, E, C> listener);
void addTransition(Transition<S, E, C> transition);
}

// 状态机监听器
public interface StateMachineListener<S extends State, E extends Event, C extends StateContext<S, E>> {
void stateChanged(S from, S to);
void eventAccepted(E event);
void transitionStarted(Transition<S, E, C> transition);
void transitionCompleted(Transition<S, E, C> transition);
}

基础实现


// 枚举状态实现
public enum SimpleState implements State {
INITIAL("INIT"),
PROCESSING("PROCESS"),
COMPLETED("COMPLETE"),
ERROR("ERR");

private String id;

SimpleState(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}
}

// 枚举事件实现
public enum SimpleEvent implements Event {
START_PROCESS("START"),
FINISH_PROCESS("FINISH"),
ERROR_OCCURRED("ERROR");

private String id;

SimpleEvent(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}
}

// 基础状态机实现
public class BasicStateMachine<S extends State, E extends Event, C extends StateContext<S, E>>
implements StateMachine<S, E, C> {

private S currentState;
private List<Transition<S, E, C>> transitions = new ArrayList<>();
private List<StateMachineListener<S, E, C>> listeners = new ArrayList<>();
private StateMachineLogger logger = new StateMachineLogger();

public BasicStateMachine(S initialState) {
this.currentState = initialState;
}

@Override
public S getCurrentState() {
return currentState;
}

@Override
public void sendEvent(E event, Map<String, Object> extendedState) {
logger.log("Processing event: " + event.getId());

// 创建状态上下文
StateContext<S, E> context = createContext(event, extendedState);

// 通知监听器事件已接受
notifyEventAccepted(event);

// 查找并执行适用的转换
for (Transition<S, E, C> transition : transitions) {
if (transition.getSource().getId().equals(currentState.getId()) && transition.isTriggered((C) context)) {
logger.log("Executing transition from " + currentState.getId() + " on " + event.getId());

// 通知监听器转换开始
notifyTransitionStarted(transition);

// 执行转换动作
transition.executeActions((C) context);

// 更新当前状态
currentState = transition.getTarget();

// 通知监听器状态改变
notifyStateChanged(transition.getSource(), transition.getTarget());

// 通知监听器转换完成
notifyTransitionCompleted(transition);

break;
}
}
}

private StateContext<S, E> createContext(E event, Map<String, Object> extendedState) {
StateContext<S, E> context = new StateContext<>();
context.setCurrentState(currentState);
context.setEvent(event);

if (extendedState != null) {
extendedState.forEach((key, value) -> context.addExtendedState(key, value));
}

return context;
}

@Override
public void addListener(StateMachineListener<S, E, C> listener) {
listeners.add(listener);
}

@Override
public void addTransition(Transition<S, E, C> transition) {
transitions.add(transition);
}

// 通知状态改变
private void notifyStateChanged(S from, S to) {
listeners.forEach(listener -> listener.stateChanged(from, to));
}

// 通知事件接受
private void notifyEventAccepted(E event) {
listeners.forEach(listener -> listener.eventAccepted(event));
}

// 通知转换开始
private void notifyTransitionStarted(Transition<S, E, C> transition) {
listeners.forEach(listener -> listener.transitionStarted(transition));
}

// 通知转换完成
private void notifyTransitionCompleted(Transition<S, E, C> transition) {
listeners.forEach(listener -> listener.transitionCompleted(transition));
}

// 日志工具类
private static class StateMachineLogger {
public void log(String message) {
System.out.println("[StateMachine] " + message);
}
}
}

转换实现


// 条件转换抽象类
public abstract class AbstractTransition<S extends State, E extends Event, C extends StateContext<S, E>>
implements Transition<S, E, C> {

private S source;
private S target;

public AbstractTransition(S source, S target) {
this.source = source;
this.target = target;
}

@Override
public S getSource() {
return source;
}

@Override
public S getTarget() {
return target;
}

@Override
public void executeActions(C context) {
// 子类可以覆盖此方法以执行转换时的操作
}
}

// 基于事件的转换
public class EventBasedTransition<S extends State, E extends Event, C extends StateContext<S, E>>
extends AbstractTransition<S, E, C> {

private E event;
private Consumer<C> action;

public EventBasedTransition(S source, S target, E event) {
this(source, target, event, null);
}

public EventBasedTransition(S source, S target, E event, Consumer<C> action) {
super(source, target);
this.event = event;
this.action = action;
}

@Override
public boolean isTriggered(C context) {
return context.getEvent().getId().equals(event.getId());
}

@Override
public void executeActions(C context) {
super.executeActions(context);
if (action != null) {
action.accept(context);
}
}
}

// 条件+事件混合转换
public class ConditionalTransition<S extends State, E extends Event, C extends StateContext<S, E>>
extends AbstractTransition<S, E, C> {

private E event;
private Predicate<C> condition;
private Consumer<C> action;

public ConditionalTransition(S source, S target, E event, Predicate<C> condition) {
this(source, target, event, condition, null);
}

public ConditionalTransition(S source, S target, E event, Predicate<C> condition, Consumer<C> action) {
super(source, target);
this.event = event;
this.condition = condition;
this.action = action;
}

@Override
public boolean isTriggered(C context) {
return context.getEvent().getId().equals(event.getId()) && condition.test(context);
}

@Override
public void executeActions(C context) {
super.executeActions(context);
if (action != null) {
action.accept(context);
}
}
}

插件系统设计


// 插件接口
public interface StateMachinePlugin<S extends State, E extends Event, C extends StateContext<S, E>> {
void configure(BasicStateMachine<S, E, C> machine);
}

// 插件支持的状态机
public class PluginEnabledStateMachine<S extends State, E extends Event, C extends StateContext<S, E>>
extends BasicStateMachine<S, E, C> {

private List<StateMachinePlugin<S, E, C>> plugins = new ArrayList<>();

public PluginEnabledStateMachine(S initialState) {
super(initialState);
}

public void addPlugin(StateMachinePlugin<S, E, C> plugin) {
plugins.add(plugin);
plugin.configure(this);
}
}

// 示例插件:自动日志记录插件
public class LoggingPlugin<S extends State, E extends Event, C extends StateContext<S, E>>
implements StateMachinePlugin<S, E, C> {

private final StateMachineLogger logger = new StateMachineLogger();

@Override
public void configure(BasicStateMachine<S, E, C> machine) {
machine.addListener(new StateMachineListener<S, E, C>() {
@Override
public void stateChanged(S from, S to) {
logger.log("State changed from " + from.getId() + " to " + to.getId());
}

@Override
public void eventAccepted(E event) {
logger.log("Event accepted: " + event.getId());
}

@Override
public void transitionStarted(Transition<S, E, C> transition) {
logger.log("Transition started: " + transition.getSource().getId() + " -> " + transition.getTarget().getId());
}

@Override
public void transitionCompleted(Transition<S, E, C> transition) {
logger.log("Transition completed: " + transition.getSource().getId() + " -> " + transition.getTarget().getId());
}
});
}

// 内部日志记录器
private static class StateMachineLogger {
public void log(String message) {
System.out.println("[StateMachine-LOG] " + message);
}
}
}

// 示例插件:持久化插件
public class PersistencePlugin<S extends State, E extends Event, C extends StateContext<S, E>>
implements StateMachinePlugin<S, E, C>, StateMachineListener<S, E, C> {

private final StateMachinePersister persister;

public PersistencePlugin(StateMachinePersister persister) {
this.persister = persister;
}

@Override
public void configure(BasicStateMachine<S, E, C> machine) {
machine.addListener(this);
}

@Override
public void stateChanged(S from, S to) {
persister.saveState(to);
}

@Override
public void eventAccepted(E event) {
// 不需要处理
}

@Override
public void transitionStarted(Transition<S, E, C> transition) {
// 不需要处理
}

@Override
public void transitionCompleted(Transition<S, E, C> transition) {
// 不需要处理
}

// 持久化接口
public interface StateMachinePersister {
void saveState(State state);
State loadState();
}

// 示例内存持久化实现
public static class InMemoryPersister implements StateMachinePersister {
private State currentState;

@Override
public void saveState(State state) {
currentState = state;
}

@Override
public State loadState() {
return currentState;
}
}
}

使用示例


public class StateMachineDemo {
public static void main(String[] args) {
// 创建状态机并添加插件
PluginEnabledStateMachine<SimpleState, SimpleEvent, StateContext<SimpleState, SimpleEvent>> machine
= new PluginEnabledStateMachine<>(SimpleState.INIT);

machine.addPlugin(new LoggingPlugin<>());

InMemoryPersister persister = new PersistencePlugin.InMemoryPersister();
machine.addPlugin(new PersistencePlugin<>(persister));

// 定义状态转换
machine.addTransition(new EventBasedTransition<>(
SimpleState.INIT, SimpleState.PROCESSING, SimpleEvent.START_PROCESS,
context -> System.out.println("Starting processing...")));

machine.addTransition(new EventBasedTransition<>(
SimpleState.PROCESSING, SimpleState.COMPLETED, SimpleEvent.FINISH_PROCESS,
context -> System.out.println("Finishing process...")));

machine.addTransition(new EventBasedTransition<>(
SimpleState.PROCESSING, SimpleState.ERROR, SimpleEvent.ERROR_OCCURRED,
context -> System.out.println("Error occurred during processing")));

// 测试状态转换
System.out.println("Initial state: " + machine.getCurrentState().getId());

System.out.println("\nSending START_PROCESS event:");
machine.sendEvent(SimpleEvent.START_PROCESS, null);
System.out.println("Current state: " + machine.getCurrentState().getId());

System.out.println("\nSending FINISH_PROCESS event:");
machine.sendEvent(SimpleEvent.FINISH_PROCESS, null);
System.out.println("Current state: " + machine.getCurrentState().getId());

// 测试持久化
System.out.println("\nTesting persistence...");
((PersistencePlugin.InMemoryPersister) persister).saveState(SimpleState.INIT);
SimpleState restoredState = (SimpleState) persister.loadState();
System.out.println("Restored state: " + restoredState.getId());
}
}

进一步扩展建议



  1. 分层状态机:实现父子状态机结构,支持复合状态和子状态机

  2. 历史状态:添加对历史状态的支持,允许状态机返回到之前的某个状态

  3. 伪状态:实现初始状态、终止状态等特殊状态类型

  4. 转换类型:增加外部转换、内部转换、本地转换等不同类型的转换

  5. 配置DSL:创建流畅的API用于配置状态机,类似:


    machine.configure()
    .from(INIT).on(START_PROCESS).to(PROCESSING)
    .perform(action)


  6. 持久化策略:添加更多持久化选项(数据库、文件等)

  7. 监控插件:添加性能监控、统计信息收集等插件

  8. 分布式支持:添加集群环境下状态同步的支持

  9. 异常处理:完善异常处理机制,支持在转换中处理异常

  10. 表达式支持:集成SpEL或其他表达式语言支持条件判断


这个实现提供了一个灵活的状态机框架基础,可以根据具体需求进一步扩展和完善。


作者:ShooterJ
来源:juejin.cn/post/7512231268420894729
收起阅读 »

goweb中间件

​ 中间件基本概念 中间件(Middleware)是一种在HTTP请求到达最终处理程序(Handler)之前或之后执行特定功能的机制。 在 Go 语言里,net/http 是标准库中用于构建 HTTP 服务器的包,中间件则是处理 HTTP 请求时常用的技术。中...
继续阅读 »


中间件基本概念


中间件(Middleware)是一种在HTTP请求到达最终处理程序(Handler)之前或之后执行特定功能的机制。


在 Go 语言里,net/http 是标准库中用于构建 HTTP 服务器的包,中间件则是处理 HTTP 请求时常用的技术。中间件其实就是一个函数,它会接收一个 http.Handler 类型的参数,并且返回另一个 http.Handler。中间件能够在请求到达最终处理程序之前或者响应返回客户端之前执行一些通用操作,像日志记录、认证、压缩等。


下面是一个简单的中间件函数示例:


go
package main

import (
    "log"
    "net/http"
)

// 中间件函数,接收一个 http.Handler 并返回另一个 http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 在请求处理之前执行的操作
        log.Printf("Received request: %s %s", r.Method, r.URL.Path)

        // 调用下一个处理程序
        next.ServeHTTP(w, r)

        // 在请求处理之后执行的操作
        log.Printf("Request completed: %s %s", r.Method, r.URL.Path)
    })
}

// 最终处理程序
func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

func main() {
    // 创建一个新的 mux
    mux := http.NewServeMux()

    // 应用中间件到最终处理程序
    mux.Handle("/", loggingMiddleware(http.HandlerFunc(helloHandler)))

    // 启动服务器
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}



  • 中间件函数 loggingMiddleware:

  • 它接收一个 http.Handler 类型的参数 next,代表下一个要执行的处理程序。

  • 返回一个新的 http.HandlerFunc,在这个函数里可以执行请求处理前后的操作。

  • next.ServeHTTP(w, r) 这行代码会调用下一个处理程序。

  • 最终处理程序 helloHandler:


helloHandler是实际处理请求的函数,它会向客户端返回 "Hello, World!"。


中间件链式调用


多个中间件可以串联起来形成处理链:


package main

import (
    "log"
    "net/http"
)

// 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("Received request: %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("Request completed: %s %s", r.Method, r.URL.Path)
    })
}

// 认证中间件
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 简单的认证逻辑
        authHeader := r.Header.Get("Authorization")
        if authHeader != "Bearer secret_token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// 最终处理程序
func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

func main() {
    mux := http.NewServeMux()

    // 应用多个中间件到最终处理程序
    finalHandler := loggingMiddleware(authMiddleware(http.HandlerFunc(helloHandler)))
    mux.Handle("/", finalHandler)

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}



  • 这里新增了一个 authMiddleware 中间件,用于简单的认证。

  • 在 main 函数里,先把 authMiddleware 应用到 helloHandler 上,再把 loggingMiddleware 应用到结果上,这样就实现了多个中间件的组合。


通过使用中间件,能够让代码更具模块化和可维护性,并且可以在多个处理程序之间共享通用的逻辑。


中间件链中传递自定义参数


​场景​​:需要在多个中间件间共享数据(如请求ID、用户会话)

​实现方式​​:通过 context.Context 传递参数


package main

import (
"context"
"fmt"
"net/http"
)

// 中间件函数
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置上下文值
ctx := context.WithValue(r.Context(), "key", "value")
ctx = context.WithValue(ctx, "user_id", 123)
r = r.WithContext(ctx)

// 调用下一个处理函数
next.ServeHTTP(w, r)
})
}

// 处理函数
func handler(w http.ResponseWriter, r *http.Request) {
// 从上下文中获取值
value := r.Context().Value("key").(string)
userID := r.Context().Value("user_id").(int)
fmt.Fprintf(w, "Received user_id %d value: %v", userID, value)
}

func main() {
// 创建一个处理函数
mux := http.NewServeMux()
mux.HandleFunc("/", handler)

http.Handle("/", middleware(mux))

// 启动服务器
fmt.Println("Server started on :80")
http.ListenAndServe(":80", nil)
}


​特点​​:



  • 数据在中间件链中透明传递

  • 避免全局变量和参数层层传递



作者:郭京京
来源:juejin.cn/post/7549113302674587658
收起阅读 »

AI总让你失望?提示词链让我从骂'憨憨'变成夸'真棒'

你是否也曾这样骂过AI? 想象一下这个场景:你满怀期待地问ChatGPT:"帮我写一份完整的项目计划书",然后AI回复了一个看似专业但完全不符合你需求的内容。此时你的内心OS是:"AI你个憨憨!这写的都是什么玩意儿?" 这时候你可能会想:"都2025年了,AI...
继续阅读 »

你是否也曾这样骂过AI?


想象一下这个场景:你满怀期待地问ChatGPT:"帮我写一份完整的项目计划书",然后AI回复了一个看似专业但完全不符合你需求的内容。此时你的内心OS是:"AI你个憨憨!这写的都是什么玩意儿?"


这时候你可能会想:"都2025年了,AI这么聪明,为什么还是不能一次性理解我的需求?是不是该换个更贵的模型了?"


别急,今天我要分享的这一个技巧,让我从骂AI"憨憨"变成夸它"真棒"。这个技巧就是提示词链(Prompt Chaining) ——简单来说,就是把一个大任务拆解成小步骤,像教小孩一样,一步步引导AI完成复杂任务。


这一个技巧到底是什么?用人话说就是...


图1:提示词链的基本工作流程 - 化整为零,各个击破


图1:提示词链的基本工作流程 - 化整为零,各个击破


提示词链说白了就是"化整为零"的艺术。你不直接问AI一个巨大的问题,而是像剥洋葱一样,一层层地引导它思考。


举个栗子🌰


传统做法(一把梭): "请帮我写一篇关于人工智能发展趋势的深度分析报告,包括市场分析、技术发展、未来预测等内容。"


结果:AI可能会给你一篇看似专业但内容空洞的"八股文"。然后你就想骂:"AI你个憨憨!"


提示词链做法(循循善诱):



  1. 第一步:调研收集 - "请列出当前人工智能领域的主要发展方向"

  2. 第二步:深度分析 - "基于以上发展方向,分析每个方向的市场规模和技术成熟度"

  3. 第三步:趋势预测 - "根据前面的分析,预测未来3-5年各个方向的发展趋势"

  4. 第四步:报告整合 - "将以上内容整合成一份结构化的分析报告"


看出区别了吗?用了这个技巧后,你会发现AI突然变聪明了,这时候你就会忍不住说:"AI你真棒!"


为什么这一个技巧这么有效?三大核心优势


1. 准确性大幅提升


图2:提示词链 vs 单次提问的准确性对比


图2:提示词链 vs 单次提问的准确性对比


AI就像一个刚入职的实习生,你一次性给他太多任务,他就蒙圈了。但如果你一步步指导,他就能做得很好。


我曾经让AI帮我写一个产品介绍,直接问的话,它给了我一堆车轱辘话。后来我改用提示词链:



  1. 先让它分析目标用户

  2. 再让它提取产品核心卖点

  3. 然后针对用户痛点匹配卖点

  4. 最后整合成介绍文案


结果?完美!就像魔法一样。


2. 过程可控,随时调整


传统方式就像开盲盒,你永远不知道AI会给你什么惊喜(或惊吓)。而提示词链让你可以在每一步都检查结果,发现不对劲立马调整。


这就像做菜,你不会把所有调料一次性倒进锅里,而是一样样加,尝一下味道,不够再加。


3. 复杂任务变简单


还记得小时候数学老师教我们解应用题的方法吗?"读题→找条件→列方程→求解"。提示词链就是这个思路,把复杂问题分解成简单步骤。


提示词链的八大类型:总有一款适合你


1. 顺序链(Sequential Chain)- 最基础款


图3:顺序链结构 - 一步接一步,稳扎稳打


图3:顺序链结构 - 一步接一步,稳扎稳打


就像流水线一样,前一步的结果是后一步的输入。


实际应用场景: 写邮件 → 检查语法 → 调整语气 → 发送


2. 分支链(Branching Chain)- 一分为多


图4:分支链结构 - 分而治之,高效并行


图4:分支链结构 - 分而治之,高效并行


这就像你让三个员工同时处理不同的任务,最后汇总。


实际应用场景: 分析用户反馈 →



  • 分支1:提取积极评价

  • 分支2:提取消极评价

  • 分支3:统计满意度分数 → 汇总报告


3. 迭代链(Iterative Chain)- 精益求精


图5:迭代链结构 - 不断优化,直到满意


图5:迭代链结构 - 不断优化,直到满意


这就像写作文,写完了改,改完了再写,直到满意为止。


实际应用场景: 生成营销标语 → 评估吸引力 → 低于8分就重新生成 → 直到满意


实战演练:从零开始构建你的第一个提示词链


让我用一个真实场景来演示:假设你是一家初创公司的产品经理,需要为新产品制定营销策略。


传统方式的痛苦


你可能会这样问: "请为我们的AI学习助手产品制定一个完整的营销策略,包括目标用户分析、竞品分析、营销渠道选择、内容策略等。"


然后AI给你一个看似完整但毫无针对性的"万金油"方案。


提示词链的魅力


第一步:用户画像调研


"作为产品营销专家,请帮我分析AI学习助手的潜在用户群体,包括:
1. 主要用户类型
2. 年龄分布  
3. 使用场景
4. 核心需求
请以表格形式呈现。"

第二步:竞品分析


"基于刚才分析的用户群体,请帮我分析市面上类似AI学习助手的竞品:
1. 主要竞争对手有哪些
2. 他们的优势和劣势
3. 市场空缺在哪里
请重点关注[用户群体]的需求。"

第三步:差异化定位


"根据前面的用户分析和竞品分析,请为我们的AI学习助手制定差异化定位策略:
1. 我们的核心竞争优势是什么
2. 如何在竞品中脱颖而出
3. 主打什么卖点最有效"

第四步:营销策略制定


"基于以上分析,请制定具体的营销策略:
1. 营销渠道选择(说明理由)
2. 内容策略规划
3. 预算分配建议
4. 关键指标设定"

看到区别了吗?每一步都有明确的目标,而且后面的步骤都建立在前面结果的基础上。


避坑指南:提示词链使用中的常见陷阱


陷阱1:错误传播 - 一步错,步步错


就像多米诺骨牌,第一块倒了,后面全完蛋。


解决方案: 在关键节点设置"检查站"。比如:


"请检查上述分析是否合理,如有问题请指出并重新分析。"

陷阱2:链条过长 - 绕晕自己


有些人为了追求完美,设计了20多步的复杂链条。结果自己都记不住每一步要干啥。


解决方案: 控制在3-7步之间,超过了就考虑拆分成多个子链。


陷阱3:成本叠加 - 钱包受伤


每一步都要调用API,成本会累加。就像打车,每次转乘都要重新计费。


解决方案:



  • 优化提示词,减少不必要的步骤

  • 关键步骤用好模型,简单步骤用便宜模型

  • 利用缓存,避免重复计算


高级技巧:让你的提示词链更智能


1. 动态分支 - 根据情况走不同路线


图6:动态分支示例 - 智能分流,精准处理


图6:动态分支示例 - 智能分流,精准处理


就像智能客服,根据用户问题自动选择处理流程。


2. 递归处理 - 处理超大任务


对于特别大的文档或数据,可以用递归方式处理:


分析500页报告 → 按章节拆分 → 逐章分析 → 汇总结果

3. 多模态链 - 文字+图片+声音


图7:多模态提示词链 - 跨媒体协作


图7:多模态提示词链 - 跨媒体协作


现在的AI不只会处理文字,还能处理图片、音频。你可以设计跨媒体的提示词链。


成功案例分享:真实项目中的提示词链应用


案例1:内容创作工作流


一位自媒体博主用提示词链优化创作流程:


原来的痛苦: 灵感来了→直接写→写到一半卡住→删掉重写→循环往复


提示词链拯救:



  1. 主题确定:基于热点分析,确定文章主题

  2. 大纲生成:结构化思路,生成文章框架

  3. 内容填充:逐段撰写,保持逻辑连贯

  4. 优化润色:语言美化,增强可读性

  5. SEO优化:标题和关键词优化


结果: 创作效率提升300%,文章质量显著改善,阅读量平均增长150%。


案例2:客服智能化升级


某电商公司用提示词链改造客服系统:


传统客服问题:



  • 响应慢

  • 答非所问

  • 用户体验差


提示词链解决方案:



  1. 问题分类:自动识别问题类型

  2. 情绪分析:判断用户情绪状态

  3. 方案匹配:根据问题类型匹配解决方案

  4. 个性化回复:结合用户历史,生成个性化回复

  5. 满意度跟踪:自动跟进处理结果


效果: 客户满意度从65%提升到92%,人工客服工作量减少70%。


未来展望:提示词链的下一步发展


1. 与AI Agent深度融合


未来的AI Agent会内置更智能的提示词链,能够自主设计和优化执行流程。


2. 可视化设计工具


就像用Scratch编程一样,未来会有拖拽式的提示词链设计工具,小白也能轻松上手。


3. 自适应优化


AI能够根据执行结果自动优化链条结构,实现持续改进。


总结:掌握提示词链,让AI成为你的得力助手


提示词链技术说到底就是一个道理:化繁为简,循序渐进


就像优秀的老师不会一口气把所有知识塞给学生,而是循循善诱,step by step。掌握了提示词链,你就能让AI变成一个贴心的老师、得力的助手。


关键要点回顾:



  1. 分解任务:把大象装进冰箱分三步

  2. 控制节奏:每一步都要有明确目标

  3. 灵活调整:发现问题及时优化

  4. 合理设计:避免过度复杂化

  5. 持续改进:根据效果不断优化


行动建议:



  1. 从简单开始:选择一个日常任务,尝试用3步提示词链优化

  2. 记录模板:把好用的链条保存下来,形成自己的工具库

  3. 持续学习:关注新工具和新技巧,保持技能更新

  4. 分享交流:和其他用户交流经验,互相学习


记住:最好的提示词链不是最复杂的,而是最适合你需求的。从今天开始,让AI真正成为你的智能伙伴吧!




如果这篇文章对你有帮助,别忘了点赞收藏。有问题欢迎在评论区讨论,我会尽力解答。一起在AI时代做个聪明的"驯兽师"! 🚀


作者:martinzh
来源:juejin.cn/post/7541935177033072655
收起阅读 »

每天一个知识点——dayjs常用的语法示例

web
日期时间处理需求 关于时间的处理,一般来说使用公共库更加优雅、方便 否则的话,自己就要写一堆处理时间的函数 比如:我需要一个将当前时间,转换成年月日时分秒格式的函数 如下: function formatCurrentTimeFn() { const ...
继续阅读 »

日期时间处理需求



  • 关于时间的处理,一般来说使用公共库更加优雅、方便

  • 否则的话,自己就要写一堆处理时间的函数

  • 比如:我需要一个将当前时间,转换成年月日时分秒格式的函数

  • 如下:


function formatCurrentTimeFn() {
const now = new Date();

const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始,所以要+1
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');

return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

// 使用示例
console.log(formatCurrentTimeFn()); // 输出类似:2025-06-04 14:30:45


  • 而使用了时间日期处理的库后,直接:dayjs().format('YYYY-MM-DD HH:mm:ss'))即可


dayjs VS momentjs



  • momentjs大而全(67KB),兼容性好,但是笨重

  • dayjs正好相反(2KB),并且可通过各种插件,弥补和momentjs的差距

  • 笔者建议:如果有时区要求,不建议用dayjs


dayjs获取当前的年月日时分秒


假设今天是2025年6月4日


新建一个html文件,而后引入cdn:


<script src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.13/dayjs.min.js"></script>

获取时间日期相关信息:


// 获取当前时间的年份
console.log('当前年份:', dayjs().year()); // 2025年

// 获取当前时间的月份(0-11)
console.log('当前月份:', dayjs().month() + 1); // 6月 // 月份从0开始,所以加1

// 获取当前时间的日期几号
console.log('当前日期几号:', dayjs().date()); // 4号

// 获取当前时间的星期几(0-6,0表示星期日,6表示星期六)
console.log('当前星期几:', dayjs().day()); // 3 // 星期三

// 获取当前时间的小时(几点)
console.log('当前小时:', dayjs().hour()); // 12时

// 获取当前时间的分钟
console.log('当前分钟:', dayjs().minute()); // 35分

// 获取当前时间的秒钟
console.log('当前秒:', dayjs().second()); // 4秒

// 获取当前时间的毫秒
console.log('当前毫秒:', dayjs().millisecond()); // 667


注意:dayjs的语法中:dayjs()[unit]() === dayjs().get(unit)


所以,还可以这样写:


console.log(dayjs().get('year')); // 2025

console.log(dayjs().get('month')); // 5 // 月份从0开始,所以是5

console.log(dayjs().get('date')); // 4

console.log(dayjs().get('day')); // 3 // 星期三

console.log(dayjs().get('hour')); // 12

console.log(dayjs().get('minute')); // 35

console.log(dayjs().get('second')); // 4

console.log(dayjs().get('millisecond')); // 667

dayjs的format格式化


// 国际化时间格式(ISO 8601)默认带时区
const ISO8601 = dayjs().format();
console.log('ISO 8601国际化时间格式:', ISO8601); // 2025-06-04T09:35:04+08:00

// 自定义格式化时间【 dayjs()不传时间,就表示当前】
console.log('四位数年月日时分秒格式:', dayjs().format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:35:04

// 两位数年月日时分秒格式
console.log('两位数年月日时分秒格式:', dayjs().format('YY-MM-DD HH:mm:ss')); // 25-06-04 09:35:04

console.log('横杠年月日格式:', dayjs().format('YYYY-MM-DD')); // 2025-06-04

console.log('斜杠年月日格式:', dayjs().format('DD/MM/YYYY')); // 04/06/2025

console.log('时分秒格式:', dayjs().format('HH:mm:ss')); // 09:35:04

console.log('时分格式:', dayjs().format('HH:mm')); // 09:35

// 自定义格式化时间【 dayjs()传时间,就格式化传递进去的时间】
console.log('年月日格式:', dayjs('2025-06-04 10:25:20').format('YYYY-MM-DD')); // 2025-06-04

console.log('时分秒格式:', dayjs('2025-06-04 10:25:20').format('HH:mm:ss')); // 09:35:04

console.log('时分格式:', dayjs('2025-06-04 10:25:20').format('HH:mm')); // 09:35

// 当然,也可以传递时间戳毫秒数之类的,不赘述
console.log('时分格式:', dayjs(1749013684020).format('HH:mm')); // 09:35

dayjs的日期加减


// 获取当前时间
console.log('当前时间:', dayjs().format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:35:04

// 获取当前时间的前一天
console.log('前一天:', dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-03 09:35:04

// 获取当前时间的后一天
console.log('后一天:', dayjs().add(1, 'day').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-05 09:35:04

// 获取当前时间的前一周
console.log('前一周:', dayjs().subtract(1, 'week').format('YYYY-MM-DD HH:mm:ss')); // 2025-05-28 09:35:04

// 获取当前时间的后一周
console.log('后一周:', dayjs().add(1, 'week').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-11 09:35:04

// 获取当前时间的前一个月
console.log('前一个月:', dayjs().subtract(1, 'month').format('YYYY-MM-DD HH:mm:ss')); // 2025-05-04 09:35:04

// 获取当前时间的后一个月
console.log('后一个月:', dayjs().add(1, 'month').format('YYYY-MM-DD HH:mm:ss')); // 2025-07-04 09:35:04

// 获取当前时间的前一年
console.log('前一年:', dayjs().subtract(1, 'year').format('YYYY-MM-DD HH:mm:ss')); // 2024-06-04 09:35:04

// 获取当前时间的后一年
console.log('后一年:', dayjs().add(1, 'year').format('YYYY-MM-DD HH:mm:ss')); // 2026-06-04 09:35:04

// 获取当前时间的前一个小时
console.log('前一个小时:', dayjs().subtract(1, 'hour').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 08:35:04

// 获取当前时间的后一个小时
console.log('后一个小时:', dayjs().add(1, 'hour').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 10:35:04

// 获取当前时间的前一分钟
console.log('前一分钟:', dayjs().subtract(1, 'minute').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:34:04

// 获取当前时间的后一分钟
console.log('后一分钟:', dayjs().add(1, 'minute').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:36:04

// 获取当前时间的前一秒
console.log('前一秒:', dayjs().subtract(1, 'second').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:35:03

// 获取当前时间的后一秒
console.log('后一秒:', dayjs().add(1, 'second').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-04 09:35:05

// 获取当前时间的前一毫秒
console.log('前一毫秒:', dayjs().subtract(1, 'millisecond').format('YYYY-MM-DD HH:mm:ss.SSS')); // 2025-06-04 09:35:04.003

// 获取当前时间的后一毫秒
console.log('后一毫秒:', dayjs().add(1, 'millisecond').format('YYYY-MM-DD HH:mm:ss.SSS')); // 2025-06-04 09:35:04.005

日期前后相等比较


/**
* 假设今天是6月5号
* */


console.log('是否日期相同', dayjs().isSame(dayjs('2025-06-05'), 'day')); // true

console.log('是否在日期之前', dayjs().isBefore(dayjs('2025-06-06'), 'day')); // true

console.log('是否在日期之后', dayjs().isAfter(dayjs('2025-06-03'), 'day')); // true

日期的差值diff


计算两个日期之间,差了多久时间


const date1 = dayjs('2019-01-25 12:00:02')
const date2 = dayjs('2019-01-25 12:00:01')
console.log('date1和date2差了:', date1.diff(date2)); // 默认差值单位毫秒数 1000

const date3 = dayjs('2019-01-25')
const date4 = dayjs('2019-02-25')
console.log(date4.diff(date3, 'month')) // 1


指定以月份为单位,可选单位有 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' 想要支持季度,需额外下载QuarterOfYear插件



获取时间戳毫秒数


// 获取当前时间的时间戳,单位为毫秒
console.log('毫秒时间戳', dayjs().valueOf()); // 1749113764926

获取时间戳秒数


// 获取当前时间的时间戳,单位为秒
console.log('秒时间戳', dayjs().unix()); // 1749113764

获取月份有多少天


// 获取某个时间的月份有多少天
console.log('dayjs().daysInMonth()', dayjs().daysInMonth()); // 30 // 现在是6月份,所以30天

开始时间和结束时间


// 获取当前时间所在天的开始时间
console.log('开始时间', dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-05 00:00:00

// 获取当前时间所在天的结束时间
console.log('结束时间', dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')); // 2025-06-05 23:59:59


每天一个知识点...



作者:水冗水孚
来源:juejin.cn/post/7512270432213876762
收起阅读 »

别再只会 new 了!八年老炮带你看透对象创建的 5 层真相

别再只会 new 了!八年老炮带你看透对象创建的 5 层真相 刚入行时,我曾在订单系统里写过这样一段 “傻代码”:在循环处理 10 万条数据时,每次都new一个临时的OrderCalculator对象,结果高峰期 GC 频繁告警,CPU 利用率飙升到 90%。...
继续阅读 »

别再只会 new 了!八年老炮带你看透对象创建的 5 层真相


刚入行时,我曾在订单系统里写过这样一段 “傻代码”:在循环处理 10 万条数据时,每次都new一个临时的OrderCalculator对象,结果高峰期 GC 频繁告警,CPU 利用率飙升到 90%。排查半天才发现,是对象创建太随意导致的 “内存爆炸”。


八年 Java 开发生涯里,从 “随便 new 对象” 到 “精准控制对象生命周期”,从排查OutOfMemoryError到优化 JVM 内存模型,我踩过的坑让我明白:对象创建看似是new关键字的一句话事儿,背后藏着 JVM 的复杂逻辑,更关联着系统的性能与稳定性


今天,我就从 “业务痛点→底层原理→解析思路→实战代码” 四个维度,带你彻底搞懂 Java 对象的创建过程。


一、先聊业务:对象创建不当会踩哪些坑?


在讲底层原理前,先结合我遇到的真实业务场景,说说 “对象创建” 这件事在实战中有多重要 —— 很多性能问题、线程安全问题,根源都在对象创建上。


1. 坑 1:循环中频繁创建临时对象 → GC 频繁


场景:电商秒杀系统的订单校验逻辑,在for循环里每次都new一个OrderValidator(无状态工具类),处理 10 万单时创建 10 万个对象。

后果:新生代 Eden 区快速填满,触发 Minor GC,频繁 GC 导致系统响应延迟从 50ms 飙升到 500ms。

根源:无状态对象无需重复创建,却被当成 “一次性用品”,浪费内存和 GC 资源。


2. 坑 2:单例模式用错 → 线程安全 + 内存泄漏


场景:支付系统用 “懒汉式单例” 创建PaymentClient(持有 HTTP 连接池),但没加双重检查锁,高并发下创建多个实例,导致连接池耗尽。

后果:支付接口频繁报 “连接超时”,排查后发现 JVM 里有 12 个PaymentClient实例,每个都占用 200 个连接。

根源:对 “对象创建的线程安全性” 理解不到位,单例模式实现不规范。


3. 坑 3:复杂对象创建参数混乱 → 代码可读性差


场景:物流系统的DeliveryOrder对象有 15 个字段,创建时用new DeliveryOrder(a,b,c,d,...),参数顺序记错导致 “收件地址” 和 “发件地址” 颠倒。

后果:用户投诉 “快递送反了”,排查代码才发现是构造函数参数顺序写错,这种 bug 极难定位。

根源:没有用合适的创建模式(如建造者模式)管理复杂对象的创建逻辑。


这些坑让我明白:不懂对象创建的底层逻辑,就无法写出高效、安全的代码。接下来,我们从 JVM 视角拆解对象创建的完整流程。


二、底层解析:一个 Java 对象的 “诞生五步曲”


当你写下User user = new User("张三", 25)时,JVM 会执行 5 个核心步骤。这部分是基础,但八年开发告诉我:理解这些步骤,才能在排查问题时 “知其然更知其所以然”


步骤 1:类加载检查 → “这个类存在吗?”


JVM 首先会检查:User类是否已被加载到方法区?如果没有,会触发类加载流程(加载→验证→准备→解析→初始化)。



  • 加载:从.class 文件读取字节码,生成Class对象(如User.class)。

  • 初始化:执行静态代码块(static {})和静态变量赋值(如public static String ROLE = "USER")。


实战影响:如果类加载失败(比如依赖缺失),会抛出NoClassDefFoundError。我曾在分布式项目中,因 jar 包版本冲突导致OrderService类加载失败,排查了 3 小时才发现是依赖冲突。


步骤 2:分配内存 → “给对象找块地方放”


类加载完成后,JVM 会为对象分配内存(大小在类加载时已确定)。内存分配有两种核心方式,对应不同的 GC 收集器:


分配方式原理适用 GC 收集器实战注意点
指针碰撞内存连续,用指针指向空闲区域边界,分配后移动指针Serial、ParNew需开启内存压缩(默认开启)
空闲列表内存不连续,维护空闲区域列表,从中选一块分配CMS、G1避免内存碎片,需定期整理

实战影响:如果内存不足(Eden 区满了),会触发 Minor GC。我曾在秒杀系统中,因内存分配过快导致 Minor GC 每秒 3 次,后来通过 “对象池复用” 减少了 80% 的创建频率。


步骤 3:初始化零值 → “先把内存清干净”


内存分配完成后,JVM 会将分配的内存空间初始化为零值(如int设为 0,String设为null)。这一步很关键:



  • 为什么?因为它保证了对象的字段在未赋值时,也有默认值(避免垃圾值)。

  • 实战坑:新人常以为 “没赋值的字段是随机值”,其实 JVM 已经帮你清为零了。


步骤 4:设置对象头 → “给对象贴个身-份-证”


JVM 会在对象内存的头部设置 “对象头”(Object Header),包含 3 类核心信息:



  1. Mark Word:存储对象的哈希码、锁状态(偏向锁 / 轻量级锁 / 重量级锁)、GC 年龄等。



    • 实战用:排查死锁时,通过jstack查看线程持有锁的对象,就是靠 Mark Word 里的锁状态。



  2. Class Metadata Address:指向对象所属类的Class对象(如User.class)。



    • 实战用:反射时user.getClass(),就是通过这个指针找到Class对象。



  3. Array Length:如果是数组对象,存储数组长度。


步骤 5:执行<init>()方法 → “给对象穿衣服”


最后,JVM 会执行对象的构造函数(<init>()方法),完成:



  • 成员变量赋值(如this.name = "张三")。

  • 执行构造代码块({}包裹的代码)。


这一步才是对象的 “最终初始化”,完成后,一个完整的对象就诞生了,指针会赋值给user变量。


三、实战解析:怎么排查对象创建相关的问题?


八年开发中,我总结了 3 套 “对象创建问题排查方法论”,从工具到思路,都是踩坑后的精华。


1. 问题 1:对象创建太多 → 怎么找到 “罪魁祸首”?


症状:GC 频繁、内存占用高、响应延迟增加。

工具jmap(查看对象实例数)、Arthas(实时排查)、VisualVM(分析 GC 日志)。

实战步骤



  1. jmap -histo:live 进程ID | head -20,查看存活对象 TOP20:


    # 示例输出:OrderDTO有12345个实例,明显异常
    num #instances #bytes class name
    ----------------------------------------------
    1: 12345 1975200 com.example.OrderDTO
    2: 8900 1424000 com.example.UserDTO


  2. 用 Arthas 的trace命令,查看OrderDTO的创建位置:


    trace com.example.OrderService createOrder -n 100


  3. 定位到循环中创建OrderDTO的代码,优化为 “复用对象” 或 “批量创建”。


2. 问题 2:对象创建慢 → 怎么定位瓶颈?


症状:创建对象耗时久(如复杂对象初始化)、类加载慢。

工具jstat(查看类加载耗时)、AsyncProfiler(分析方法执行时间)。

实战步骤



  1. jstat -class 进程ID 1000,查看类加载速度:


    Loaded  Bytes  Unloaded  Bytes     Time   
    1234 234560 0 0 123.45 # Time是类加载总耗时,单位ms


  2. 若类加载慢,检查是否有 “大 jar 包” 或 “类冲突”;若对象初始化慢,用 AsyncProfiler 分析构造函数耗时。


3. 问题 3:单例对象多实例 → 怎么验证?


症状:单例类(如PaymentClient)出现多实例,导致资源泄漏。

工具jmap -dump:live,format=b,file=heap.hprof 进程ID(dump 堆内存)、MAT(分析堆快照)。

实战步骤



  1. Dump 堆内存后,用 MAT 打开,搜索PaymentClient类。

  2. 查看 “Instance Count”,若大于 1,说明单例模式实现有问题(如没加双重检查锁)。


四、核心代码:对象创建的 5 种方式与实战选型


八年开发中,我用过 5 种对象创建方式,每种都有明确的适用场景,选错了就会踩坑。下面结合代码和业务场景对比分析:


1. new 关键字:最基础,但别滥用


代码


// 普通对象创建
User user = new User("张三", 25);
// 注意:循环中避免频繁new无状态对象
List<User> userList = new ArrayList<>();
// 坑:每次循环都new10万次循环创建10万个UserValidator
for (Order order : orderList) {
UserValidator validator = new UserValidator(); // 优化:改为单例或局部变量复用
validator.validate(order);
}

适用场景:简单对象、非频繁创建的对象。

八年经验:别在循环中new临时对象,尤其是无状态工具类(如ValidatorCalculator),改用单例或对象池。


2. 反射:灵活但性能差,慎用


代码


try {
// 方式1:通过Class对象创建
Class<User> userClass = User.class;
User user = userClass.newInstance(); // 调用无参构造

// 方式2:通过Constructor创建(支持有参构造)
Constructor<User> constructor = userClass.getConstructor(String.class, int.class);
User user2 = constructor.newInstance("李四", 30);
} catch (Exception e) {
e.printStackTrace();
}

适用场景:框架开发(如 Spring IOC 容器)、动态创建对象。

八年经验:反射性能比new慢 10-100 倍,业务代码中尽量不用;若用,建议缓存Constructor对象(避免重复获取)。


3. 单例模式:解决 “重复创建” 问题


代码:枚举单例(线程安全、防反射、防序列化,八年开发首推)


// 枚举单例:支付客户端(持有HTTP连接池,需单例)
public enum PaymentClient {
INSTANCE;

// 初始化连接池(构造方法默认私有,线程安全)
private HttpClient httpClient;

PaymentClient() {
httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
}

// 提供全局访问点
public HttpClient getHttpClient() {
return httpClient;
}
}

// 使用:避免重复创建,全局复用
HttpClient client = PaymentClient.INSTANCE.getHttpClient();

适用场景:工具类、资源密集型对象(如连接池、线程池)。

八年经验:别用 “懒汉式单例”(线程安全问题多),优先用枚举或 “饿汉式 + 静态内部类”。


4. 建造者模式:解决 “复杂对象参数混乱”


代码:订单对象创建(15 个字段,用建造者模式避免参数顺序错误)


// 订单类:复杂对象,字段多
@Data
public class Order {
private String orderId;
private String userId;
private BigDecimal amount;
private String startAddress;
private String endAddress;
// 其他10个字段...

// 私有构造:只能通过建造者创建
private Order(Builder builder) {
this.orderId = builder.orderId;
this.userId = builder.userId;
this.amount = builder.amount;
this.startAddress = builder.startAddress;
this.endAddress = builder.endAddress;
// 其他字段赋值...
}

// 建造者
public static class Builder {
private String orderId;
private String userId;
private BigDecimal amount;
private String startAddress;
private String endAddress;

// 链式调用方法
public Builder orderId(String orderId) {
this.orderId = orderId;
return this;
}

public Builder userId(String userId) {
this.userId = userId;
return this;
}

public Builder amount(BigDecimal amount) {
this.amount = amount;
return this;
}

// 其他字段的set方法...

// 最终创建对象
public Order build() {
// 校验必填字段:避免创建不完整对象
if (orderId == null || userId == null) {
throw new IllegalArgumentException("订单ID和用户ID不能为空");
}
return new Order(this);
}
}
}

// 使用:链式调用,参数清晰,无顺序问题
Order order = new Order.Builder()
.orderId("ORDER_20250903_001")
.userId("USER_123")
.amount(new BigDecimal("99.9"))
.startAddress("重庆市机管局")
.endAddress("重庆市江北区机管局")
.build();

适用场景:字段超过 5 个的复杂对象(如订单、用户信息)。

八年经验:建造者模式不仅解决参数顺序问题,还能在build()中做参数校验,避免创建 “残缺对象”。


5. 对象池:复用对象,减少创建开销


代码:用 Apache Commons Pool 实现OrderDTO对象池(秒杀系统中复用临时对象)


// 1. 引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>

// 2. 定义对象工厂(创建和销毁对象)
public class OrderDTOFactory extends BasePooledObjectFactory<OrderDTO> {
// 创建对象
@Override
public OrderDTO create() {
return new OrderDTO();
}

// 包装对象(池化需要)
@Override
public PooledObject<OrderDTO> wrap(OrderDTO orderDTO) {
return new DefaultPooledObject<>(orderDTO);
}

// 归还对象前重置(避免数据残留)
@Override
public void passivateObject(PooledObject<OrderDTO> p) {
OrderDTO orderDTO = p.getObject();
orderDTO.setOrderId(null);
orderDTO.setUserId(null);
orderDTO.setAmount(null);
// 重置其他字段...
}
}

// 3. 配置对象池
public class OrderDTOPool {
private final GenericObjectPool<OrderDTO> pool;

public OrderDTOPool() {
// 配置池参数:最大空闲数、最大总实例数、超时时间等
GenericObjectPoolConfig<OrderDTO> config = new GenericObjectPoolConfig<>();
config.setMaxIdle(100); // 最大空闲对象数
config.setMaxTotal(200); // 池最大总实例数
config.setBlockWhenExhausted(true); // 池满时阻塞等待
config.setMaxWait(Duration.ofMillis(100)); // 最大等待时间

// 初始化池
this.pool = new GenericObjectPool<>(new OrderDTOFactory(), config);
}

// 从池获取对象
public OrderDTO borrowObject() throws Exception {
return pool.borrowObject();
}

// 归还对象到池
public void returnObject(OrderDTO orderDTO) {
pool.returnObject(orderDTO);
}
}

// 4. 实战使用:秒杀系统处理订单
public class SeckillService {
private final OrderDTOPool objectPool = new OrderDTOPool();

public void processOrders(List<OrderInfo> orderInfoList) {
for (OrderInfo info : orderInfoList) {
OrderDTO orderDTO = null;
try {
// 从池获取对象(复用,不new)
orderDTO = objectPool.borrowObject();
// 赋值并处理
orderDTO.setOrderId(info.getOrderId());
orderDTO.setUserId(info.getUserId());
orderDTO.setAmount(info.getAmount());
orderService.submit(orderDTO);
} catch (Exception e) {
log.error("处理订单失败", e);
} finally {
// 归还对象到池(关键:避免内存泄漏)
if (orderDTO != null) {
objectPool.returnObject(orderDTO);
}
}
}
}
}

适用场景:频繁创建临时对象的场景(如秒杀、批量处理)。

八年经验:对象池虽好,但别滥用 —— 只有当对象创建成本高(如初始化耗时久)且复用率高时才用,否则会增加复杂度。


五、八年开发的 8 条 “对象创建” 最佳实践


最后,总结 8 条实战经验,都是我踩过坑后总结的 “血泪教训”,能帮你避开 90% 的对象创建相关问题:



  1. 避免在循环中 new 临时对象:无状态工具类用单例,临时 DTO 用对象池。

  2. 复杂对象优先用建造者模式:字段超过 5 个就别用new了,参数顺序错了很难查。

  3. 单例模式别用懒汉式:优先枚举或静态内部类,线程安全且无反射漏洞。

  4. 别忽视对象的 “销毁” :使用对象池时,一定要在finally中归还对象,避免内存泄漏。

  5. 慎用 finalize () 方法:它会延迟对象回收(需要两次 GC),建议用try-with-resources管理资源。

  6. 监控对象实例数:线上系统定期用jmap检查,避免 “隐形” 的对象爆炸。

  7. 类加载别踩版本冲突:依赖冲突会导致类加载失败,用mvn dependency:tree排查。

  8. 对象创建不是越多越好:有时候 “复用” 比 “创建” 更高效,比如 String 用intern()复用常量池对象。


六、结尾:基础不牢,地动山摇


八年 Java 开发,我越来越觉得:真正的高手,不是会写多复杂的框架,而是能把基础问题理解透彻。对象创建看似简单,却关联着 JVM、GC、设计模式、性能优化等多个维度。


我见过太多新人因为不懂对象创建的底层逻辑,写出 “看似能跑,实则埋满坑” 的代码;也见过资深开发者通过优化对象创建,把系统 QPS 从 1 万提升到 10 万。


希望这篇文章能帮你从 “会用new” 到 “懂创建”,在实战中写出更高效、更稳定的 Java 代码。如果有对象创建相关的踩坑经历,欢迎在评论区分享~


作者:天天摸鱼的java工程师
来源:juejin.cn/post/7545921037286047744
收起阅读 »

【小程序】迁移非主包组件以减少主包体积

web
代码位置 gitee.com/zhangkb/exa… 问题及背景 微信小程序主包体积最大为 2M,超出体积无法上传。 组件放在不同的目录下的表现不同: src/components 目录中的组件会被打包到主包中,可以被所有页面引用。 src/pages/...
继续阅读 »

代码位置


gitee.com/zhangkb/exa…


问题及背景



  • 微信小程序主包体积最大为 2M,超出体积无法上传。

  • 组件放在不同的目录下的表现不同:

    • src/components 目录中的组件会被打包到主包中,可以被所有页面引用。

    • src/pages/about/components 目录中的组件会被打印到对应分包中,只能被当前分包引用(只考虑微信小程序的话可以用分包异步化,我这边因为需要做不同平台所以不考虑这个方案)。




在之前的项目结构中,所有的组件都放在 src/components 目录下,因此所有组件都会被打包到主包中,这导致主包体积超出了 2M 的限制。


后续经过优化,将一些与主包无关的组件放到了对应分包中,但是有一些组件,在主包页面中没有被引用,但是被多个不同的分包页面引用,因此只能放到 src/components 目录下打包到主包中。


本文的优化思路就是将这一部分组件通过脚本迁移到不同的分包目录中,从而减少主包体积,这样做的缺点也显而易见:会增加代码包的总体积(微信还有总体积小于 20M 的限制 🤮)。


实现思路


项目中用 gulp 做打包流程管理,因此将这个功能封装成一个 task,在打包之前调用。


1. 分析依赖


分析 src/components 组件是否主包页面引用,有两种情况:



  1. 直接被主页引用。

  2. 间接被主页引用:主页引用 aa 引用 b,此时 a 为直接引用,b 为间接引用。


const { series, task, src, parallel } = require("gulp");
const tap = require("gulp-tap");
const path = require("path");
const fs = require("fs");
const pages = require("../src/pages.json");

// 项目根目录
const rootPath = path.join(__dirname, "../");
const srcPath = path.join(rootPath, "./src");
const componentsPath = path.join(rootPath, "./src/components");

// 组件引用根路径
const componentRootPath = "@/components"; // 替换为 pages 页面中引入组件的路径

// 组件依赖信息
let componentsMap = {};

// 从 pages 文件中获取主包页面路径列表
const mainPackagePagePathList = pages.pages.map((item) => {
let pathParts = item.path.split("/");

return pathParts.join(`\\${path.sep}`);
});

/**
* 组件信息初始化
*/

function initComponentsMap() {
// 为所有 src/components 中的组件创建信息
return src([`${srcPath}/@(components)/**/**.vue`]).pipe(
tap((file) => {
let filePath = transferFilePathToComponentPath(file.path);

componentsMap[filePath] = {
refers: [], // 引用此组件的页面/组件
quotes: [], // 此组件引用的组件
referForMainPackage: false, // 是否被主包引用,被主包引用时不需要 copy 到分包
};
})
);
}

/**
* 分析依赖
*/

function analyseDependencies() {
return src([`${srcPath}/@(components|pages)/**/**.vue`]).pipe(
tap((file) => {
// 是否为主包页面
const isMainPackagePageByPath = checkIsMainPackagePageByPath(file.path);

// 分析页面引用了哪些组件
const componentsPaths = Object.keys(componentsMap);
const content = String(file.contents);

componentsPaths.forEach((componentPath) => {
if (content.includes(componentPath)) {
// 当前页面引用了这个组件
componentsMap[componentPath].refers.push(file.path);

if (file.path.includes(componentsPath)) {
// 记录组件被引用情况
const targetComponentPath = transferFilePathToComponentPath(
file.path
);

componentsMap[targetComponentPath].quotes.push(componentPath);
}

// 标记组件是否被主页引用
if (isMainPackagePageByPath) {
componentsMap[componentPath].referForMainPackage = true;
}
}
});
})
);
}

/**
* 分析间接引用依赖
*/

function analyseIndirectDependencies(done) {
for (const componentPath in componentsMap) {
const componentInfo = componentsMap[componentPath];

if (!componentInfo.referForMainPackage) {
const isIndirectReferComponent =
checkIsIndirectReferComponent(componentPath);

if (isIndirectReferComponent) {
console.log("间接引用组件", componentPath);
componentInfo.referForMainPackage = true;
}
}
}

done();
}

/**
* 是否为被主页间接引用的组件
*/

function checkIsIndirectReferComponent(componentPath) {
const componentInfo = componentsMap[componentPath];

if (componentInfo.referForMainPackage) {
return true;
}

for (const filePath of componentInfo.refers) {
if (filePath.includes(componentsPath)) {
const subComponentPath = transferFilePathToComponentPath(filePath);
const result = checkIsIndirectReferComponent(subComponentPath);

if (result) {
return result;
}
}
}
}

/**
* 将文件路径转换为组件路径
*/

function transferFilePathToComponentPath(filePath) {
return filePath
.replace(componentsPath, componentRootPath)
.replaceAll(path.sep, "/")
.replace(".vue", "");
}

/**
* 判断页面路径是否为主包页面
*/

function checkIsMainPackagePageByPath(filePath) {
// 正则:判断是否为主包页面
const isMainPackagePageReg = new RegExp(
`(${mainPackagePagePathList.join("|")})`
);

return isMainPackagePageReg.test(filePath);
}

经过这一步后会得到一个 json,包含被引用文件信息和是否被主页引用,格式为:


{
"@/components/xxxx/xxxx": {
"refers": [
"D:\\code\\miniPrograme\\xxxxxx\\src\\pages\\xxx1\\index.vue",
"D:\\code\\miniPrograme\\xxxxxx\\src\\pages\\xxx2\\index.vue",
"D:\\code\\miniPrograme\\xxxxxx\\src\\components\\xxx\\xxx\\xxx.vue"
],
"referForMainPackage": false
}
}

2. 分发组件


经过第一步的依赖分析,我们知道了 referForMainPackage 值为 false 的组件是不需要放在主包中的,在这一步中将这些组件分发到对应的分包中。


思路:



  1. 遍历所有 referForMainPackage 值为 false 的组件。

  2. 遍历所有组件的 refers 列表,如果 refer 能匹配到分包,做以下动作:



    1. 在分包根目录下创建 componentsauto 目录,将组件复制到这里。

    2. 复制组件中引用的相对路径资源。



  3. 删除 pages/components 中的当前组件。


const taskMap = {};
const changeFileMap = {};
const deleteFileMap = {};

// 分发组件
async function distributionComponents() {
for (let componentPath in componentsMap) {
const componentInfo = componentsMap[componentPath];

// 未被主包引用的组件
for (const pagePath of componentInfo.refers) {
// 将组件复制到分包
if (pagePath.includes(pagesPath)) {
// 将组件复制到页面所在分包
await copyComponent(componentPath, pagePath);
}
}
}
}

/**
* 复制组件
* @param {*} componentPath
* @param {*} targetPath
* @returns
*/

async function copyComponent(componentPath, pagePath) {
const componentInfo = componentsMap[componentPath];

if (componentInfo.referForMainPackage) return;

const key = `${componentPath}_${pagePath}`;

// 避免重复任务
if (taskMap[key]) return;

taskMap[key] = true;

const subPackageRoot = getSubPackageRootByPath(pagePath);

if (!subPackageRoot) return;

const componentFilePath = transferComponentPathToFilePath(componentPath);
const subPackageComponentsPath = path.join(subPackageRoot, "componentsauto");
const newComponentFilePath = path.join(
subPackageComponentsPath,
path.basename(componentFilePath)
);
const newComponentsPath = newComponentFilePath
.replace(srcPath, "@")
.replaceAll(path.sep, "/")
.replaceAll(".vue", "");

// 1. 复制组件及其资源
await copyComponentWithResources(
componentFilePath,
subPackageComponentsPath,
componentInfo
);

// 2. 递归复制引用的组件
if (componentInfo.quotes.length > 0) {
let tasks = [];

componentInfo.quotes.map((quotePath) => {
// 复制子组件
tasks.push(copyComponent(quotePath, pagePath));

const subComponentInfo = componentsMap[quotePath];

if (!subComponentInfo.referForMainPackage) {
// 2.1 修改组件引用的子组件路径
const newSubComponentFilePath = path.join(
subPackageComponentsPath,
path.basename(quotePath)
);
const newSubComponentsPath = newSubComponentFilePath
.replace(srcPath, "@")
.replaceAll(path.sep, "/")
.replaceAll(".vue", "");
updateChangeFileInfo(
newComponentFilePath,
quotePath,
newSubComponentsPath
);
}
});
await Promise.all(tasks);
}

// 3. 修改页面引用当前组件路径
updateChangeFileInfo(pagePath, componentPath, newComponentsPath);

// 4. 删除当前组件
updateDeleteFileInfo(componentFilePath);
}

/**
* 更新删除文件信息
* @param {*} filePath
*/

function updateDeleteFileInfo(filePath) {
deleteFileMap[filePath] = true;
}

/**
* 更新修改文件内容信息
* @param {*} filePath
* @param {*} oldStr
* @param {*} newStr
*/

function updateChangeFileInfo(filePath, oldStr, newStr) {
if (!changeFileMap[filePath]) {
changeFileMap[filePath] = [];
}
changeFileMap[filePath].push([oldStr, newStr]);
}

/**
* 删除文件任务
*/

async function deleteFile() {
for (const filePath in deleteFileMap) {
try {
await fs.promises.unlink(filePath).catch(console.log); // 删除单个文件
// 或删除目录:await fs.rmdir('path/to/dir', { recursive: true });
} catch (err) {
console.error("删除失败:", err);
}
}
}

/**
* 复制组件及其资源
* @param {*} componentFilePath
* @param {*} destPath
*/

async function copyComponentWithResources(componentFilePath, destPath) {
// 复制主组件文件
await new Promise((resolve) => {
src(componentFilePath).pipe(dest(destPath)).on("end", resolve);
});

// 处理组件中的相对路径资源
const content = await fs.promises.readFile(componentFilePath, "utf-8");
const relativePaths = extractRelativePaths(content);

await Promise.all(
relativePaths.map(async (relativePath) => {
const resourceSrcPath = path.join(componentFilePath, "../", relativePath);
const resourceDestPath = path.join(destPath, path.dirname(relativePath));

await new Promise((resolve) => {
src(resourceSrcPath).pipe(dest(resourceDestPath)).on("end", resolve);
});
})
);
}

/**
* 修改页面引用路径
*/

async function changePageResourcePath() {
for (const pagePath in changeFileMap) {
const list = changeFileMap[pagePath];

await new Promise((resolve) => {
src(pagePath)
.pipe(
tap((file) => {
let content = String(file.contents);

for (const [oldPath, newPath] of list) {
content = content.replaceAll(oldPath, newPath);
}
file.contents = Buffer.from(content);
})
)
.pipe(dest(path.join(pagePath, "../")))
.on("end", resolve);
});
}
}

// 获取分包根目录
function getSubPackageRootByPath(pagePath) {
for (const subPackagePagePath of subPackagePagePathList) {
const rootPath = `${path.join(pagesPath, subPackagePagePath)}`;
const arr = pagePath.replace(pagesPath, "").split(path.sep);

if (arr[1] === subPackagePagePath) {
return rootPath;
}
}
}

注意事项


引用资源时不能用相对路径


避免使用相对路径引入资源,可以通过代码规范来限制(处理起来比较麻烦,懒得写了)。


不同操作系统未验证


代码仅在 windows 10 系统下运行,其他操作系统未验证,可能会存在资源路径无法匹配的问题。


uniapp 项目


本项目是 uniapp 项目,因此迁移的组件后缀为 .vue,原生语言或其他框架不能直接使用。


作者:锋利的绵羊
来源:juejin.cn/post/7518758885273829413
收起阅读 »

鸿蒙Flex与Row/Column对比

在鸿蒙(HarmonyOS)应用开发中,Flex布局与Row/Column布局是两种核心的容器组件,它们在功能、性能及适用场景上存在显著差异。以下从五个维度进行详细对比: 📊 ​1. 核心差异对比​ ​特性​​Flex布局​​Row/Column布局​​布局...
继续阅读 »

在鸿蒙(HarmonyOS)应用开发中,Flex布局与Row/Column布局是两种核心的容器组件,它们在功能、性能及适用场景上存在显著差异。以下从五个维度进行详细对比:




📊 ​1. 核心差异对比


特性Flex布局Row/Column布局
布局机制动态弹性计算,支持二次布局(重新分配空间)单次线性排列,无二次布局
方向控制支持水平(Row)、垂直(Column)及反向排列Row仅水平,Column仅垂直
换行能力支持自动换行(FlexWrap.Wrap不支持换行,子组件溢出时被截断或压缩
子组件控制支持flexGrowflexShrinkflexBasis动态分配空间仅支持layoutWeight按比例分配空间
性能表现较低(二次布局增加计算开销)较高(单次布局完成)


⚠️ ​二次布局问题​:当子组件总尺寸与容器不匹配时,Flex需通过拉伸/压缩重新计算布局,导致性能损耗。





🔧 ​2. Flex布局的核心特点与场景



  • 核心优势



    • 多方向布局​:通过direction自由切换主轴方向(水平/垂直)。

    • 复杂对齐​:组合justifyContent(主轴)和alignItems(交叉轴)实现精准对齐。

    • 动态空间分配​:



      • flexGrow:按比例分配剩余空间(如搜索框占满剩余宽度)。

      • flexShrink:空间不足时按比例压缩子组件(需配合minWidth避免过度压缩)。





  • 必用场景



    • 多行排列​:标签组、商品网格布局(需设置wrap: FlexWrap.Wrap)。

    • 响应式适配​:跨设备屏幕(如手机/车机动态调整列数)。






📐 ​3. Row/Column布局的核心特点与场景



  • 核心优势



    • 轻量高效​:线性排列无弹性计算,渲染性能更高。

    • 简洁属性​:



      • space:控制子组件间距(如导航栏按钮间隔)。

      • layoutWeight:一次遍历完成空间分配(性能优于flexGrow)。





  • 推荐场景



    • 单向排列​:



      • Row:水平导航栏、头像+文字组合。

      • Column:垂直表单、卡片内容堆叠。



    • 固定尺寸布局​:子组件尺寸明确时(如按钮宽度固定)。






⚡ ​4. 性能差异与优化建议



  • Flex性能瓶颈



    • 二次布局触发条件​:子组件总尺寸 ≠ 容器尺寸、优先级冲突(如displayPriority分组计算)。

    • 后果​:嵌套过深或动态数据下易引发界面卡顿。



  • 优化策略



    • 替代方案​:简单布局优先用Row/Column,避免Flex嵌套超过3层。

    • 属性优化​:



      • 固定尺寸组件设置flexShrink(0)禁止压缩。

      • 等分布局用layoutWeight替代flexGrow(如Row中占比1:2)。



    • 预设尺寸​:尽量让子组件总尺寸接近容器尺寸,减少拉伸需求。






🛠️ ​5. 选择策略与工程实践



  • 何时选择Flex?​


    ✅ 需换行(如标签云)、复杂弹性对齐(如交叉轴居中)、动态网格布局。


    ❌ 避免在简单列表、表单等场景使用,优先Row/Column。


  • 何时选择Row/Column?​


    ✅ 单向排列(水平/垂直)、子组件尺寸固定或比例明确(如30%+70%)。


    ✅ 高频场景:导航栏(Row)、表单(Column)、图文混排(Row+垂直居中)。


  • 工程最佳实践



    • 多端适配​:通过DeviceType动态调整参数(如车机增大点击区域)。

    • 调试工具​:用DevEco Studio布局分析器监测二次布局次数。

    • 混合布局​:Flex内嵌套Row/Column(如Flex容器中的商品项用Column)。






💎 ​总结



  • Flex​:强大但“重”,适合复杂弹性多行响应式布局,需警惕二次布局问题。

  • Row/Column​:轻量高效,是单向排列场景的首选,性能优势明显。

  • 决策关键​:



    简单布局看方向(水平用Row,垂直用Column),


    复杂需求看弹性(换行/动态分配用Flex)。





通过合理选择组件并优化属性配置,可显著提升鸿蒙应用的渲染效率与用户体验。


作者:风冷
来源:juejin.cn/post/7541339617489600555
收起阅读 »

【吃瓜】这可能是2025年最荒谬的前端灾难:一支触控笔"干掉"了全球CSS预处理器

web
作为mockm项目的维护者,这几天我一直在优化CI/CD流水线。终于把自动化测试和发布流程都搞定了,心想着可以安心写代码了。结果今天早上一看GitHub Actions,我傻眼了... 项目突然构建失败了 昨天还好好的CI/CD流水线,今天突然就红了一片!...
继续阅读 »

作为mockm项目的维护者,这几天我一直在优化CI/CD流水线。终于把自动化测试和发布流程都搞定了,心想着可以安心写代码了。结果今天早上一看GitHub Actions,我傻眼了...



项目突然构建失败了


GitHub Actions Failed


昨天还好好的CI/CD流水线,今天突然就红了一片!


刚刚合并完dev分支的代码,准备发布新版本,结果Deploy Documentation and Release Package这个workflow直接失败了。作为一个有洁癖的开发者,看到Actions页面一片红色真的很崩溃。


第一反应:又是我的配置问题?


点开失败的job详情,看到build-and-release这一步挂了。心想肯定又是我的docker-compose配置有问题,或者是某个环境变量没设对。


毕竟刚优化完CI/CD,出问题很正常嘛...


但是当我仔细查看错误日志时,发现了一个让我摸不着头脑的错误:



stylus 包不存在?什么鬼?



我重新运行了一遍workflow,还是同样的错误。然后我在本地试了试 npm install,结果更震惊了——NPM告诉我这个用了好几年的CSS预处理器库,突然从地球上消失了。


从GitHub Actions红屏到全网灾难


看到这个错误,我的第一反应不是恐慌,而是怀疑自己的CI配置:


npm ERR! 404 'stylus@https://registry.npmjs.org/stylus/-/stylus-0.64.0.tgz' is not in this registry

"是不是我的workflow配置有问题?"我检查了一遍deploy.yml文件,docker-compose配置也重新看了遍。


"是不是环境变量没设对?"我在Actions的Secrets里确认了一遍,没有问题。


"是不是依赖版本冲突了?"我看了看package.json,stylus版本一直没变过啊。


然后我想到,也许是GitHub Actions的runner环境问题?我在本地试了试:


npm install stylus
npm ERR! 404 'stylus@*' is not in this registry.

WTF?本地也不行了!


这时候我才意识到,这不是我的mockm项目的问题,不是我的CI/CD配置的问题,而是整个NPM生态出大问题了


直到我打开Twitter,看到满屏的哀嚎,才意识到这不是我一个人的问题。这是一场全球性的前端灾难


当我意识到这不是我的CI问题时...


说实话,刚开始我还暗自庆幸——至少不是我的自动化流程配置有问题。毕竟刚花了好几天时间优化CI/CD,要是出bug了那真是太丢人了。


但当我看到GitHub上那些issue的时候,笑不出来了:



  • Nx框架的用户在哭

  • TypeScript项目在崩溃

  • 连Vue的生态都受到了影响

  • 我的mockm项目构建也挂了


这让我想起了2016年的left-pad事件,但这次更严重。left-pad至少只是一个小工具函数,而Stylus是整个CSS预处理生态的重要组成部分。


我开始担心:不光是我的mockm项目发布不了,全世界有多少个项目的CI/CD都在今天红屏了?有多少开发者像我一样,以为是自己的配置问题,结果查了半天发现是外部依赖炸了?


全球开发者陷入恐慌


GitHub Issues 爆炸式增长


短短几小时内,与Stylus相关的错误报告如雨后春笋般涌现:



  1. [Nx框架] - 'stylus@https://registry.npmjs.org/stylus/-/stylus-0.64.0.tgz' is not in this registry on npm install nrwl/nx#32031

  2. [TypeScript CSS Modules] - Stylus contained malicious code and was removed from the registry by the npm security team mrmckeb/typescript-plugin-css-modules#287

  3. [ShadCN Vue] - ERR_PNPM_NO_MATCHING_VERSION due to yanked package unovue/shadcn-vue#1344


社交媒体上的恐慌


Twitter、Reddit、Discord等平台上充斥着开发者的求助和抱怨:



"我的整个项目都跑不起来了,Stylus到底发生了什么?"




"生产环境部署失败,老板在催进度,Stylus你什么时候能回来?"




"这是我见过最离谱的NPM事故,一个CSS预处理器居然能让半个前端圈瘫痪"



然后我发现了最荒谬的真相...


花了一个上午收集信息后,我发现了这个让人哭笑不得的真相:


NPM把CSS预处理器和ChromeOS的触控笔搞混了!


没错,你没看错。导致Stylus被封禁的CVE-2025-6044,说的是ChromeOS设备上的物理触控笔存在安全漏洞。而NPM的安全团队,可能是用了某种自动化工具,看到"Stylus"这个名字,就把我们前端开发者天天用的CSS预处理器给ban了。


我第一次看到这个解释的时候,真的以为是在看洋葱新闻。


让我们来对比一下这个绝世乌龙:


真正有漏洞的"Stylus":



  • ChromeOS设备上的物理触控笔工具

  • 需要物理接触设备才能攻击

  • 和前端开发一毛钱关系都没有


被误杀的"stylus":



  • 前端开发者的CSS预处理器

  • 纯软件库,连UI都没有

  • 被全世界几百万项目依赖


这就好比因为苹果公司出了安全问题,就把超市里的苹果都下架了一样荒谬。


image.png


我为这个维护者感到心疼


看到Stylus维护者@iChenLei在GitHub上的无助求助,说实话我挺心疼的。


作为一个也维护过开源项目的人,我太能理解那种感受了:你辛辛苦苦维护了这么多年的项目,服务了全球这么多开发者,结果因为一个莫名其妙的错误就被封禁,而且申诉无门。


他在Issue里写道:



"这影响了很多人。虽然这不是我的错,但我向每个人道歉。"



这句话让我特别感动。明明是NPM搞错了,但他还是在为用户的困扰道歉。这就是开源维护者的责任感。


而且你看他做的这些努力:



  • 立即提交官方ticket

  • 在Twitter上求助

  • 甚至还展示了自己的2FA截图证明账户安全


但NPM官方到现在还没有任何回应。这让我想起那句话:"开源开发者用爱发电,平台方用AI管理"


临时解决方案:前端社区的自救行动


面对官方的无回应,社区开始了自救。说实话,这种时候最能看出开源社区的凝聚力。


我试过的几种方法


方法一:直接用GitHub源


npm install https://github.com/stylus/stylus/archive/refs/tags/0.64.0.tar.gz

这个方法管用,但感觉不太优雅。而且每次安装都要下载整个repo,速度慢得要命。


方法二:Package.json override


{
"overrides": {
"postcss-styl>stylus": "https://github.com/stylus/stylus/archive/refs/tags/0.64.0.tar.gz"
}
}

这个比较适合已有项目,但对新项目来说还是很麻烦。


方法三:换注册表


npm config set registry https://registry.npmmirror.com/

试了几个国内镜像,大部分还有缓存,可以正常安装。但总感觉不是长久之计。


让我感动的社区互助


在各种群和论坛里,大家都在分享解决方案,没有人在抱怨,更没有人在指责维护者。这让我想起了为什么我当初会爱上开源社区。


有个老哥甚至建议大家去转发维护者的Twitter求助,我觉得这个主意不错。毕竟有时候社交媒体的影响力比正式渠道还管用。


这件事让我重新思考了很多问题


说实话,这次事件让我开始重新审视我们前端开发的生态。


NPM真的靠谱吗?


作为一个在前端圈混了这么多年的老司机,我一直觉得NPM已经足够成熟稳定了。但这次事件让我意识到,我们可能过于依赖这个中心化的平台了。


想想看:



  • 一个错误的安全判断,就能让全球项目停摆

  • 维护者申诉无门,只能在社交媒体求助

  • 没有任何预警机制,用户只能被动承受


这真的合理吗?


image.png


开源维护者太难了


@iChenLei的遭遇让我想起了很多开源维护者的心酸。他们用爱发电,服务全世界,但遇到问题时却如此无助。


我觉得我们作为受益者,应该:



  • 多给开源项目捐赠

  • 积极参与社区建设

  • 在这种时候给维护者更多支持


而不是只会在出问题时抱怨。


前端生态的脆弱性


这次事件也暴露了现代前端开发的一个问题:我们的依赖树太复杂了。


一个简单的项目,动不动就有几百个依赖。每个依赖都是一个潜在的故障点。虽然这种模块化的开发方式很高效,但风险也确实不小。


我开始思考:



  • 是不是应该减少一些不必要的依赖?

  • 关键依赖是不是应该做备份?

  • 公司是不是应该建立私有NPM镜像?


从left-pad到stylus,我们学到了什么?


2016年的left-pad事件,曾经让整个JavaScript生态停摆了一天。当时大家说要吸取教训,要建立更稳定的包管理机制。


现在2025年了,类似的事情又发生了,而且更严重。


这让我意识到,单纯依靠技术手段可能解决不了根本问题。我们需要的是:



  1. 更透明的治理机制:NPM的决策过程应该更开放

  2. 更快速的申诉渠道:不能让维护者只能在Twitter求助

  3. 更多元化的生态:不能把鸡蛋都放在一个篮子里



left-pad事件

left-pad 是一个由 Javascript 程序员 Azer 编写的 npm 包,功能是为字符串添加左侧填充,代码仅有 11 行,但却被上千个项目使用,其中包括著名的 babel 和 react-native 等。


Azer 收到 kik 公司的邮件,对方称要发布名为 kik 的封包,但 kik 这个名字已被 Azer 占用,希望他能改名。Azer 拒绝后,kik 公司多次与他沟通无果,便向 npm 公司发邮件。最终,npm 公司将 kik 封包转给了 kik 公司。


Azer 因 npm 公司的这一决定感到愤怒,一怒之下将自己在 npm 上的 273 个封包全部撤下,其中包括 left-pad 封包。这导致依赖 left-pad 的成千上万个项目瞬间崩溃,大量开发者的项目构建失败。



我的一些建议


作为一个用户,我觉得我们可以:


短期内:



  • 建立项目的依赖备份机制

  • 使用多个注册表镜像

  • 关键项目使用package-lock.json


长期来看:



  • 支持去中心化的包管理方案

  • 推动NPM改进治理机制

  • 给开源项目更多的资金和技术支持~逃~~~



资金支持?之前为了让 mockm 项目的文档能让网络“不方便”的大家也能快速访问,自己花钱买的域名、服务器。但是这么多年工资也没有涨,可能是我没有好好工作。撑不下去了(本来好像也没几个用户),所以我打算把文档部署在 GITHUB PAGE 上了,网络不方便?爱谁谁!



image.png


写在最后


这次事件提醒我们,我们的工作比想象中更脆弱。但也让我看到了社区的力量:当官方渠道失效时,我们依然能够相互帮助,共度难关。PS:这就是为什么我爱这个行业的原因。


然而一个又产生一个新想法:一个小小的名称混淆,就能让全球的前端开发陷入混乱。那么,"软件正在吞噬世界,但谁来守护软件?"


相关链接



作者:四叶草会开花
来源:juejin.cn/post/7529903134296653839
收起阅读 »

Nginx 内置变量详解:从原理到实战案例

Nginx 内置变量是 Nginx 配置中极具灵活性的核心特性,它们能动态获取请求、连接、服务器等维度的实时数据,让配置从“固定模板”升级为“智能响应”。本文将系统梳理常用内置变量的分类、含义,并结合实战案例说明其应用场景,帮助你真正用好 Nginx 变量。 ...
继续阅读 »

Nginx 内置变量是 Nginx 配置中极具灵活性的核心特性,它们能动态获取请求、连接、服务器等维度的实时数据,让配置从“固定模板”升级为“智能响应”。本文将系统梳理常用内置变量的分类、含义,并结合实战案例说明其应用场景,帮助你真正用好 Nginx 变量。


一、Nginx 内置变量的核心特性


在深入变量前,先明确两个关键特性:



  1. 动态性:变量值并非固定,而是在每次请求处理时实时生成(如 $remote_addr 会随客户端 IP 变化)。

  2. 作用域:变量仅在当前请求的处理周期内有效,不同请求的变量值相互独立。

  3. 命名规则:所有内置变量均以 $ 开头,如 $uri$status


二、常用内置变量分类与含义


按“数据来源”可将内置变量分为 5 大类,涵盖请求、连接、服务器、响应等核心场景。


1. 请求相关变量(获取客户端请求信息)


这类变量用于获取客户端发送的请求细节,是最常用的变量类型。


变量名含义示例
$remote_addr客户端真实 IP 地址(未经过代理时)192.168.230.1(本地局域网 IP)
$arg_xxx获取 URL 中 xxx 对应的参数值(xxx 为参数名)请求 http://xxx/?id=123 时,$arg_id=123
$args完整的 URL 请求参数(? 后面的所有内容)请求 http://xxx/?id=123&name=test 时,$args=id=123&name=test
$request_method客户端请求方法(GET/POST/PUT/DELETE 等)GETPOST
$request_uri完整的请求 URI(包含路径和参数,不包含域名)请求 http://xxx/api/user?uid=1 时,$request_uri=/api/user?uid=1
$uri / $document_uri请求的 URI 路径(不含参数,两者功能几乎一致)请求 http://xxx/api/user?uid=1 时,$uri=/api/user
$http_xxx获取请求头中 xxx 字段的值(xxx 为请求头名,需将 - 改为小写)获取 User-Agent 时用 $http_user_agent,获取 Referer 时用 $http_referer
$cookie_xxx获取客户端 Cookie 中 xxx 对应的 value客户端 Cookie 为 token=abc123 时,$cookie_token=abc123

2. 连接相关变量(获取网络连接信息)


用于获取客户端与服务器之间的连接状态,常用于连接追踪和并发控制。


变量名含义示例
$connection客户端与服务器的唯一连接 ID(每次新连接会生成新 ID)12345(数字型 ID)
$connection_requests当前连接上已处理的请求次数(长连接场景下会累计)同一连接发起第 3 次请求时,值为 3
$remote_port客户端用于连接的端口号54321(客户端随机端口)
$server_port服务器监听的端口号(当前请求命中的端口)80(HTTP)、443(HTTPS)

3. 服务器相关变量(获取服务器自身信息)


用于获取 Nginx 服务器的配置和系统信息,常用于多服务器部署场景。


变量名含义示例
$server_addr服务器处理当前请求的 IP 地址(多网卡时对应绑定的 IP)192.168.230.130(服务器内网 IP)
$server_name当前请求命中的 server 块的 server_name 配置值server_name http://www.example.com,则值为 http://www.example.com
$hostname服务器的系统主机名(与 hostname 命令输出一致)centos-nginx-server

4. 响应相关变量(获取 Nginx 响应信息)


用于记录 Nginx 向客户端返回的响应数据,常用于日志统计和性能分析。


变量名含义示例
$status响应的 HTTP 状态码200(成功)、404(未找到)、502(网关错误)
$body_bytes_sent发送给客户端的响应体大小(单位:字节,不含响应头)返回 1KB 文本时,值为 1024
$bytes_sent发送给客户端的总字节数(含响应头 + 响应体)通常比 $body_bytes_sent 大 100-200 字节(响应头占比)
$request_time请求的总处理耗时(单位:秒,精确到毫秒)0.005(表示 5 毫秒)

5. 时间相关变量(获取时间信息)


用于记录请求处理的时间,常用于日志时间戳和时间范围控制。


变量名含义示例
$msec请求处理完成时的 Unix 时间戳(含毫秒,从 1970-01-01 开始)1724325600.123(对应 2024-08-22 11:20:00.123)
$time_local服务器本地时间(格式化字符串,含时区)22/Aug/2024:11:20:00 +0800+0800 表示北京时间)
$time_iso8601ISO 8601 标准时间(UTC 时间,无时区偏移)2024-08-22T03:20:00+00:00

三、内置变量实战案例


了解变量含义后,关键是知道“在什么场景用什么变量”。以下 5 个实战案例覆盖日志、鉴权、跳转、限流等高频场景。


案例 1:自定义访问日志(记录关键请求信息)


默认的 Nginx 访问日志仅包含基础信息,通过变量可自定义日志格式,记录如“客户端 IP、请求方法、参数、耗时”等关键数据,方便后续分析。


配置步骤:



  1. nginx.confhttp 块中定义日志格式:


    http {
    # 1. 定义自定义日志格式(命名为 "detail_log")
    log_format detail_log '$remote_addr [$time_local] "$request_method $request_uri" '
    'status:$status args:"$args"耗时:$request_time '
    'user_agent:"$http_user_agent"';

    # 2. 启用自定义日志(指定日志路径和格式)
    access_log /usr/local/nginx/logs/detail_access.log detail_log;

    # 其他配置...
    }


  2. 重载 Nginx 配置:


    /usr/local/nginx/sbin/nginx -t  # 检查语法
    /usr/local/nginx/sbin/nginx -s reload # 重载生效



日志效果:


访问 http://192.168.230.130/?id=123 后,日志文件会生成如下记录:


192.168.230.1 [22/Aug/2024:11:30:00 +0800] "GET /?id=123" status:200 args:"id=123"耗时:0.002 user_agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/127.0.0.1"

案例 2:URL 参数鉴权(限制特定参数访问)


场景:仅允许 token 参数为 abc123 的请求访问 /admin 路径,否则返回 403 禁止访问。


配置步骤:


server 块中添加 location 规则:


server {
listen 80;
server_name localhost;

# 匹配 /admin 路径
location /admin {
# 1. 检查 $arg_token(URL 中 token 参数)是否等于 abc123
if ($arg_token != "abc123") {
return 403 "Forbidden: Invalid token\n"; # 不匹配则返回 403
}

# 2. 匹配通过时的处理(如返回 admin 页面)
default_type text/html;
return 200 "<h1>Admin Page (Token Valid)</h1>";
}
}

测试效果:



案例 3:根据客户端 IP 跳转(本地 IP 免验证)


场景:局域网 IP(192.168.230.xxx)访问 /login 时直接跳转至首页,其他 IP 正常显示登录页。


配置步骤:


利用 $remote_addr 判断客户端 IP,结合 rewrite 实现跳转:


server {
listen 80;
server_name localhost;

location /login {
# 1. 匹配局域网 IP(以 192.168.230. 开头)
if ($remote_addr ~* ^192\.168\.230\.) {
rewrite ^/login$ / permanent; # 301 永久跳转到首页
}

# 2. 其他 IP 显示登录页
default_type text/html;
return 200 "<h1>Login Page (Non-Local IP)</h1>";
}

# 首页配置
location / {
default_type text/html;
return 200 "<h1>Home Page (Local IP Bypassed Login)</h1>";
}
}

测试效果:



  • 本地 IP(如 192.168.230.1)访问 /login → 自动跳转到 /(首页)。

  • 外部 IP(如 10.0.0.1)访问 /login → 显示登录页。


案例 4:根据请求头切换后端服务(前后端分离场景)


场景:请求头 X-Request-Typeapi 时,转发请求到后端 API 服务(127.0.0.1:8080);否则返回静态页面。


配置步骤:


利用 $http_x_request_type 获取自定义请求头,结合 proxy_pass 实现反向代理:


server {
listen 80;
server_name localhost;

location / {
# 1. 判断请求头 X-Request-Type 是否为 api
if ($http_x_request_type = "api") {
proxy_pass http://127.0.0.1:8080; # 转发到 API 服务
proxy_set_header Host $host; # 传递 Host 头给后端
break; # 跳出 if,避免后续执行
}

# 2. 其他请求返回静态首页
root /usr/local/nginx/html;
index index.html;
}
}

测试效果:



案例 5:基于 Cookie 实现灰度发布(部分用户尝鲜新功能)


场景:Cookie 中 version=beta 的用户访问 /feature 时,返回新功能页面;其他用户返回旧页面。


配置步骤:


利用 $cookie_version 获取 Cookie 值,实现灰度分流:


server {
listen 80;
server_name localhost;

location /feature {
default_type text/html;

# 1. 检查 Cookie 中 version 是否为 beta
if ($cookie_version = "beta") {
return 200 "<h1>New Feature (Beta Version)</h1>"; # 新功能
}

# 2. 其他用户显示旧功能
return 200 "<h1>Old Feature (Stable Version)</h1>";
}
}

测试效果:



四、使用内置变量的注意事项



  1. 避免过度使用 if 指令:Nginx 的 if 指令在某些场景下可能触发意外行为(如与 try_files 冲突),复杂逻辑优先用 map 指令或 Lua 脚本。

  2. 代理场景下的 IP 问题:若 Nginx 位于代理服务器后(如 CDN、负载均衡器),$remote_addr 会变为代理 IP,需通过 $http_x_forwarded_for 获取客户端真实 IP(需代理服务器传递该请求头)。

  3. 变量大小写敏感$arg_id$arg_ID 是两个不同的变量(前者对应 ?id=1,后者对应 ?ID=1),配置时需注意参数名大小写。

  4. 性能影响:内置变量本身性能开销极低,但频繁使用复杂正则匹配(如 if ($remote_addr ~* ...))可能增加 CPU 消耗,高并发场景需优化正则。


五、总结


Nginx 内置变量是连接“静态配置”与“动态请求”的桥梁,掌握它们能让你摆脱固定配置的束缚,实现更灵活的请求处理逻辑。本文梳理的 5 大类变量和实战案例,覆盖了日志、鉴权、跳转、代理、灰度等高频场景,建议结合实际需求动手测试——只有在实践中反复使用,才能真正理解变量的威力。


如果需要更复杂的场景(如结合 map 指令批量处理变量、Lua 脚本扩展变量功能),可以进一步深入学习 Nginx 高级配置技巧。


作者:灰色头像
来源:juejin.cn/post/7543193023086575625
收起阅读 »

分支管理提交代码

前言:公司代码需要先放在开发分支上,后面提交的时候再合并到master主分支 开始复现场景: 从git branch -a 输出来看,你的本地分支是 develop,而远程仓库中存在两个分支:origin/master 和 origin/develop。此...
继续阅读 »

前言:公司代码需要先放在开发分支上,后面提交的时候再合并到master主分支



开始复现场景:


image.png


git branch -a 输出来看,你的本地分支是 develop,而远程仓库中存在两个分支:origin/masterorigin/develop。此外,remotes/origin/HEAD 指向了 origin/master,这表明远程仓库的默认分支是 master


问题分析


你提到想把分支合并到 master 主分支上去,但 git branch 没有显示 master 分支。根据输出信息,可以确认以下几点:



  1. 本地没有 master 分支:当前只有 develop 分支。

  2. 远程存在 master 分支remotes/origin/master 表明远程仓库中有 master 分支。

  3. 远程默认分支是 masterremotes/origin/HEAD -> origin/master 表明远程仓库的默认分支是 master


解决方案


1️⃣ 创建并切换到本地 master 分支

首先,你需要在本地创建一个 master 分支,并将其与远程的 master 分支关联起来。


# 创建并切换到本地 master 分支,跟踪远程 origin/master
git checkout -b master origin/master

2️⃣ 将 develop 分支合并到 master 分支

确保你在 master 分支上,然后将 develop 分支的内容合并进来。


bash
深色版本
# 确保你在 master 分支上
git checkout master

# 合并 develop 分支
git merge develop

3️⃣ 解决可能的冲突 (没有冲突的话不需要执行这两条命令)

如果在合并过程中出现冲突,Git 会提示你手动解决这些冲突。解决冲突后,使用以下命令继续合并过程:


# 解决冲突后,添加已修改的文件
git add .

# 继续合并
git commit

4️⃣ 推送更改到远程 master 分支

最后,将合并后的更改推送到远程 master 分支。


bash
深色版本
# 推送更改到远程 master 分支
git push origin master



🛠️ 进阶操作:设置默认上游分支


如果你希望以后直接使用 git pullgit push 而不需要每次都指定分支,可以设置默认上游分支:


bash
深色版本
# 设置 master 分支的上游为 origin/master
git branch --set-upstream-to=origin/master master

这样,当你在 master 分支上时,可以直接使用 git pullgit push 命令,而不需要额外指定分支名。


报错了


image.png


远程仓库 master 分支包含了本地分支没有的更改。因此,Git 拒绝了你的推送请求,以防止丢失远程仓库中的工作。


错误原因


这个错误通常有以下几种可能的原因:



  1. 远程仓库有新的提交:在你上次拉取(git pull)之后,其他开发者可能已经向远程仓库的 master 分支推送了新的提交。

  2. 非快进式推送:你的本地 master 分支与远程 master 分支之间的差异无法通过简单的“快进”操作来解决,需要进行合并或变基。


解决方案


1️⃣ 先拉取远程仓库的最新更改

你需要先从远程仓库拉取最新的更改,然后将这些更改合并到你的本地分支中。


# 切换到 master 分支
git checkout master

# 从远程仓库拉取最新的更改
git pull origin master

如果 git pull 成功执行,它会自动将远程仓库的更改合并到你的本地分支中。如果有冲突,你需要手动解决这些冲突。


2️⃣ 解决冲突(如有必要)

如果在 git pull 过程中出现冲突,Git 会提示你手动解决这些冲突。解决冲突后,使用以下命令继续合并过程:


# 解决冲突后,添加已修改的文件
git add .

# 继续合并
git commit

3️⃣ 再次尝试推送更改

解决了所有冲突并完成合并后,你可以再次尝试将本地更改推送到远程仓库:


# 推送更改到远程 master 分支
git push origin master

还是报错


image.png


从你提供的错误信息来看,当前的状态是 MERGING,这表明你在合并过程中遇到了问题,并且还没有完成合并。因此,Git 拒绝了你的推送请求。


错误原因



  1. 合并尚未完成:你正在进行一个合并操作(可能是之前执行的 git merge 或 git pull),但还没有解决所有的冲突或完成合并。

  2. 本地分支落后于远程分支:即使解决了合并冲突,本地分支可能仍然落后于远程分支,需要先拉取最新的更改。


解决方案


1️⃣ 检查并完成合并

首先,你需要确保合并操作已经完成。如果存在未解决的冲突,你需要手动解决这些冲突。


查看当前状态,确认是否有未解决的冲突(查看本地和远程 master 分支的差异)

git status

如果 git status 提示你的本地分支落后于远程分支(例如 Your branch is behind 'origin/master' by X commits),你需要先拉取最新的更改。


如果有冲突文件,打开这些文件并手动解决冲突


解决冲突后,添加已修改的文件并提交:


# 添加已解决冲突的文件
git add .

# 提交合并
git commit

2️⃣ 拉取远程仓库的最新更改

在完成合并后,再次尝试拉取远程仓库的最新更改,以确保本地分支是最新的。


# 切换到 master 分支
git checkout master

# 从远程仓库拉取最新的更改
git pull origin master

如果 git pull 成功执行,它会自动将远程仓库的更改合并到你的本地分支中。如果有冲突,你需要手动解决这些冲突。


3️⃣ 再次尝试推送更改

解决了所有冲突并完成合并后,你可以再次尝试将本地更改推送到远程仓库:


# 推送更改到远程 master 分支
git push origin master

我本地如果删除了几个文件再更新到仓库去要怎么办?


你只需要用 git add .git rm 告诉 Git “这些文件是我有意删除的”,然后提交并推送到远程仓库即可。Git 会把“删除”也当作一次更改记录下来。




✅ 正确操作步骤(推荐)


1️⃣ 确认你已经删除了文件

你已经在本地手动删除了一些文件(比如用资源管理器或 rm 命令删的)。


2️⃣ 让 Git 检测到这些文件被删除

运行以下命令,Git 会发现哪些文件被删了,并把“删除”操作加入暂存区:


bash
深色版本
git add --all
# 或者简写:
git add .

💡 注意:

虽然 git add . 通常用于“添加新文件”,但它也会检测到被删除的文件,并自动执行 git rm 的效果。



⚠️ 如果你只用 git commit 而不先 add,Git 不会自动把删除纳入提交。



3️⃣ 提交删除操作

bash
深色版本
git commit -m "删除了不再需要的文件:xxx, yyy"

4️⃣ 推送到远程仓库

bash
深色版本
git push origin master
# 或者 main,根据你的分支名

✅ 完成!远程仓库也会删除这些文件。


作者:Hyinglin
来源:juejin.cn/post/7549096640340492338
收起阅读 »

🔥 滚动监听写到手抽筋?IntersectionObserver让你躺平实现懒加载

web
🎯 学习目标:掌握IntersectionObserver API的核心用法,解决滚动监听性能问题,实现高效的懒加载和可见性检测 📊 难度等级:中级 🏷️ 技术标签:#IntersectionObserver #懒加载 #性能优化 #滚动监听 ⏱️ 阅读时间:...
继续阅读 »

🎯 学习目标:掌握IntersectionObserver API的核心用法,解决滚动监听性能问题,实现高效的懒加载和可见性检测


📊 难度等级:中级

🏷️ 技术标签#IntersectionObserver #懒加载 #性能优化 #滚动监听

⏱️ 阅读时间:约8分钟





🌟 引言


在日常的前端开发中,你是否遇到过这样的困扰:



  • 滚动监听卡顿:addEventListener('scroll')写多了页面卡得像PPT

  • 懒加载复杂:图片懒加载逻辑复杂,还要手动计算元素位置

  • 无限滚动性能差:数据越来越多,滚动越来越卡

  • 可见性检测麻烦:想知道元素是否进入视口,代码写得头疼


今天分享5个IntersectionObserver的实用场景,让你的滚动监听更加丝滑,告别性能焦虑!




💡 核心技巧详解


1. 图片懒加载:告别手动计算位置的痛苦


🔍 应用场景


当页面有大量图片时,一次性加载所有图片会严重影响页面性能,需要实现图片懒加载。


❌ 常见问题


传统的滚动监听方式性能差,需要频繁计算元素位置。


// ❌ 传统滚动监听写法
window.addEventListener('scroll', () => {
const images = document.querySelectorAll('img[data-src]');
images.forEach(img => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) {
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
});

✅ 推荐方案


使用IntersectionObserver实现高性能的图片懒加载。


/**
* 创建图片懒加载观察器
* @description 使用IntersectionObserver实现高性能图片懒加载
* @param {string} selector - 图片选择器
* @param {Object} options - 观察器配置选项
* @returns {IntersectionObserver} 观察器实例
*/

const createImageLazyLoader = (selector = 'img[data-src]', options = {}) => {
// 推荐写法:使用IntersectionObserver
const defaultOptions = {
root: null, // 使用视口作为根元素
rootMargin: '50px', // 提前50px开始加载
threshold: 0.1 // 元素10%可见时触发
};

const config = { ...defaultOptions, ...options };

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 加载图片
img.src = img.dataset.src;
img.removeAttribute('data-src');
// 停止观察已加载的图片
observer.unobserve(img);
}
});
}, config);

// 观察所有待加载的图片
document.querySelectorAll(selector).forEach(img => {
observer.observe(img);
});

return observer;
};

💡 核心要点



  • rootMargin:提前加载,避免用户看到空白

  • threshold:设置合适的触发阈值

  • unobserve:加载完成后停止观察,释放资源


🎯 实际应用


在Vue3项目中的完整应用示例:


<template>
<div class="image-gallery">
<img
v-for="(image, index) in images"
:key="index"
:data-src="image.url"
:alt="image.alt"
class="lazy-image"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200'%3E%3Crect width='100%25' height='100%25' fill='%23f0f0f0'/%3E%3C/svg%3E"
/>
</div>
</template>

<script setup>
import { onMounted, onUnmounted } from 'vue';

let observer = null;

onMounted(() => {
observer = createImageLazyLoader('.lazy-image');
});

onUnmounted(() => {
observer?.disconnect();
});
</script>



2. 无限滚动:数据加载的性能优化


🔍 应用场景


实现无限滚动列表,当用户滚动到底部时自动加载更多数据。


❌ 常见问题


传统方式需要监听滚动事件并计算滚动位置,性能开销大。


// ❌ 传统无限滚动实现
window.addEventListener('scroll', () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
loadMoreData();
}
});

✅ 推荐方案


使用IntersectionObserver监听底部哨兵元素。


/**
* 创建无限滚动观察器
* @description 监听底部哨兵元素实现无限滚动
* @param {Function} loadMore - 加载更多数据的回调函数
* @param {Object} options - 观察器配置
* @returns {Object} 包含观察器和控制方法的对象
*/

const createInfiniteScroll = (loadMore, options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '100px', // 提前100px触发加载
threshold: 0
};

const config = { ...defaultOptions, ...options };
let isLoading = false;

const observer = new IntersectionObserver(async (entries) => {
const [entry] = entries;

if (entry.isIntersecting && !isLoading) {
isLoading = true;
try {
await loadMore();
} catch (error) {
console.error('加载数据失败:', error);
} finally {
isLoading = false;
}
}
}, config);

return {
observer,
// 开始观察哨兵元素
observe: (element) => observer.observe(element),
// 停止观察
disconnect: () => observer.disconnect(),
// 获取加载状态
getLoadingState: () => isLoading
};
};

💡 核心要点



  • 哨兵元素:在列表底部放置一个不可见的元素作为触发器

  • 防重复加载:使用loading状态防止重复请求

  • 错误处理:加载失败时的异常处理


🎯 实际应用


Vue3组件中的使用示例:


<template>
<div class="infinite-list">
<div v-for="item in items" :key="item.id" class="list-item">
{{ item.title }}
</div>
<!-- 哨兵元素 -->
<div ref="sentinelRef" class="sentinel"></div>
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const items = ref([]);
const loading = ref(false);
const sentinelRef = ref(null);
let infiniteScroll = null;

// 加载更多数据
const loadMoreData = async () => {
loading.value = true;
// 模拟API请求
const newItems = await fetchData();
items.value.push(...newItems);
loading.value = false;
};

onMounted(() => {
infiniteScroll = createInfiniteScroll(loadMoreData);
infiniteScroll.observe(sentinelRef.value);
});

onUnmounted(() => {
infiniteScroll?.disconnect();
});
</script>



3. 元素可见性统计:精准的用户行为分析


🔍 应用场景


统计用户对页面内容的浏览情况,比如广告曝光、内容阅读时长等。


❌ 常见问题


手动计算元素可见性复杂且不准确。


// ❌ 手动计算可见性
const isElementVisible = (element) => {
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
};

✅ 推荐方案


使用IntersectionObserver精准统计元素可见性。


/**
* 创建可见性统计观察器
* @description 统计元素的可见性和停留时间
* @param {Function} onVisibilityChange - 可见性变化回调
* @param {Object} options - 观察器配置
* @returns {IntersectionObserver} 观察器实例
*/

const createVisibilityTracker = (onVisibilityChange, options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: [0, 0.25, 0.5, 0.75, 1.0] // 多个阈值,精确统计
};

const config = { ...defaultOptions, ...options };
const visibilityData = new Map();

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;
const elementId = element.dataset.trackId || element.id;

if (!visibilityData.has(elementId)) {
visibilityData.set(elementId, {
element,
startTime: null,
totalTime: 0,
maxVisibility: 0
});
}

const data = visibilityData.get(elementId);

if (entry.isIntersecting) {
// 元素进入视口
if (!data.startTime) {
data.startTime = Date.now();
}
data.maxVisibility = Math.max(data.maxVisibility, entry.intersectionRatio);
} else {
// 元素离开视口
if (data.startTime) {
data.totalTime += Date.now() - data.startTime;
data.startTime = null;
}
}

// 触发回调
onVisibilityChange({
elementId,
isVisible: entry.isIntersecting,
visibilityRatio: entry.intersectionRatio,
totalTime: data.totalTime,
maxVisibility: data.maxVisibility
});
});
}, config);

return observer;
};

💡 核心要点



  • 多阈值监听:使用多个threshold值精确统计可见比例

  • 时间统计:记录元素在视口中的停留时间

  • 数据持久化:将统计数据存储到Map中


🎯 实际应用


广告曝光统计的实际应用:


// 实际项目中的广告曝光统计
const trackAdExposure = () => {
const tracker = createVisibilityTracker((data) => {
const { elementId, isVisible, visibilityRatio, totalTime } = data;

// 曝光条件:可见比例超过50%且停留时间超过1秒
if (visibilityRatio >= 0.5 && totalTime >= 1000) {
// 发送曝光统计
sendExposureData({
adId: elementId,
exposureTime: totalTime,
visibilityRatio: visibilityRatio
});
}
});

// 观察所有广告元素
document.querySelectorAll('.ad-banner').forEach(ad => {
tracker.observe(ad);
});
};



4. 动画触发控制:精准的视觉效果


🔍 应用场景


当元素进入视口时触发CSS动画或JavaScript动画,提升用户体验。


❌ 常见问题


使用滚动监听触发动画,性能差且时机不准确。


// ❌ 传统动画触发方式
window.addEventListener('scroll', () => {
const elements = document.querySelectorAll('.animate-on-scroll');
elements.forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight * 0.8) {
el.classList.add('animate');
}
});
});

✅ 推荐方案


使用IntersectionObserver精准控制动画触发时机。


/**
* 创建动画触发观察器
* @description 当元素进入视口时触发动画
* @param {Object} options - 观察器和动画配置
* @returns {IntersectionObserver} 观察器实例
*/

const createAnimationTrigger = (options = {}) => {
const defaultOptions = {
root: null,
rootMargin: '-10% 0px', // 元素完全进入视口后触发
threshold: 0.3,
animationClass: 'animate-in',
once: true // 只触发一次
};

const config = { ...defaultOptions, ...options };
const triggeredElements = new Set();

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const element = entry.target;

if (entry.isIntersecting) {
// 添加动画类
element.classList.add(config.animationClass);

if (config.once) {
// 只触发一次,停止观察
observer.unobserve(element);
triggeredElements.add(element);
}

// 触发自定义事件
element.dispatchEvent(new CustomEvent('elementVisible', {
detail: { intersectionRatio: entry.intersectionRatio }
}));
} else if (!config.once) {
// 允许重复触发时,移除动画类
element.classList.remove(config.animationClass);
}
});
}, {
root: config.root,
rootMargin: config.rootMargin,
threshold: config.threshold
});

return observer;
};

💡 核心要点



  • rootMargin负值:确保元素完全进入视口后才触发

  • once选项:控制动画是否只触发一次

  • 自定义事件:方便其他代码监听动画触发


🎯 实际应用


配合CSS动画的完整实现:


/* CSS动画定义 */
.fade-in-element {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s ease-out;
}

.fade-in-element.animate-in {
opacity: 1;
transform: translateY(0);
}

// JavaScript动画控制
const initScrollAnimations = () => {
const animationTrigger = createAnimationTrigger({
animationClass: 'animate-in',
threshold: 0.2,
once: true
});

// 观察所有需要动画的元素
document.querySelectorAll('.fade-in-element').forEach(element => {
animationTrigger.observe(element);

// 监听动画触发事件
element.addEventListener('elementVisible', (e) => {
console.log(`元素动画触发,可见比例: ${e.detail.intersectionRatio}`);
});
});
};



5. 虚拟滚动优化:大数据列表的性能救星


🔍 应用场景


处理包含大量数据的列表,只渲染可见区域的元素,提升性能。


❌ 常见问题


渲染大量DOM元素导致页面卡顿,滚动性能差。


// ❌ 渲染所有数据
const renderAllItems = (items) => {
const container = document.getElementById('list');
items.forEach(item => {
const element = document.createElement('div');
element.textContent = item.title;
container.appendChild(element);
});
};

✅ 推荐方案


结合IntersectionObserver实现简化版虚拟滚动。


/**
* 创建虚拟滚动观察器
* @description 只渲染可见区域的列表项,优化大数据列表性能
* @param {Array} data - 数据数组
* @param {Function} renderItem - 渲染单个项目的函数
* @param {Object} options - 配置选项
* @returns {Object} 虚拟滚动控制器
*/

const createVirtualScroll = (data, renderItem, options = {}) => {
const defaultOptions = {
itemHeight: 60, // 每项高度
bufferSize: 5, // 缓冲区大小
container: null // 容器元素
};

const config = { ...defaultOptions, ...options };
const visibleItems = new Map();

// 创建占位元素
const createPlaceholder = (index) => {
const placeholder = document.createElement('div');
placeholder.style.height = `${config.itemHeight}px`;
placeholder.dataset.index = index;
placeholder.classList.add('virtual-item-placeholder');
return placeholder;
};

const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const placeholder = entry.target;
const index = parseInt(placeholder.dataset.index);

if (entry.isIntersecting) {
// 元素进入视口,渲染真实内容
if (!visibleItems.has(index)) {
const realElement = renderItem(data[index], index);
realElement.style.height = `${config.itemHeight}px`;
placeholder.replaceWith(realElement);
visibleItems.set(index, realElement);
}
} else {
// 元素离开视口,替换为占位符
const realElement = visibleItems.get(index);
if (realElement) {
const newPlaceholder = createPlaceholder(index);
realElement.replaceWith(newPlaceholder);
observer.observe(newPlaceholder);
visibleItems.delete(index);
}
}
});
}, {
root: config.container,
rootMargin: `${config.bufferSize * config.itemHeight}px`,
threshold: 0
});

// 初始化列表
const init = () => {
const container = config.container;
container.innerHTML = '';

data.forEach((_, index) => {
const placeholder = createPlaceholder(index);
container.appendChild(placeholder);
observer.observe(placeholder);
});
};

return {
init,
destroy: () => observer.disconnect(),
getVisibleCount: () => visibleItems.size
};
};

💡 核心要点



  • 占位符机制:使用固定高度的占位符保持滚动条正确

  • 缓冲区:通过rootMargin提前渲染即将可见的元素

  • 内存管理:及时清理不可见的元素,释放内存


🎯 实际应用


Vue3组件中的虚拟滚动实现:


<template>
<div ref="containerRef" class="virtual-scroll-container">
<!-- 虚拟滚动内容将在这里动态生成 -->
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const containerRef = ref(null);
let virtualScroll = null;

// 大量数据
const largeDataset = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
title: `列表项 ${i + 1}`,
content: `这是第 ${i + 1} 个列表项的内容`
})));

// 渲染单个列表项
const renderListItem = (item, index) => {
const element = document.createElement('div');
element.className = 'list-item';
element.innerHTML = `
<h3>${item.title}</h3>
<p>${item.content}</p>
`;
return element;
};

onMounted(() => {
virtualScroll = createVirtualScroll(
largeDataset.value,
renderListItem,
{
itemHeight: 80,
bufferSize: 3,
container: containerRef.value
}
);

virtualScroll.init();
});

onUnmounted(() => {
virtualScroll?.destroy();
});
</script>



📊 技巧对比总结


技巧使用场景优势注意事项
图片懒加载大量图片展示性能优秀,实现简单需要设置合适的rootMargin
无限滚动长列表数据加载避免频繁滚动监听防止重复加载,错误处理
可见性统计用户行为分析精确统计,多阈值监听数据存储和上报策略
动画触发页面交互效果时机精准,性能好动画只触发一次的控制
虚拟滚动大数据列表内存占用低,滚动流畅元素高度固定,复杂度较高



🎯 实战应用建议


最佳实践



  1. 合理设置rootMargin:根据实际需求提前或延迟触发观察

  2. 及时清理观察器:使用unobserve()和disconnect()释放资源

  3. 错误处理机制:为异步操作添加try-catch保护

  4. 性能监控:在开发环境中监控观察器的性能表现

  5. 渐进增强:为不支持IntersectionObserver的浏览器提供降级方案


性能考虑



  • 观察器数量控制:避免创建过多观察器实例

  • threshold设置:根据实际需求设置合适的阈值

  • 内存泄漏防护:组件销毁时及时清理观察器

  • 兼容性处理:使用polyfill支持旧版浏览器




💡 总结


这5个IntersectionObserver实用场景在日常开发中能显著提升页面性能,掌握它们能让你的滚动监听:



  1. 图片懒加载:告别手动位置计算,性能提升显著

  2. 无限滚动:避免频繁滚动监听,用户体验更佳

  3. 可见性统计:精准的用户行为分析,数据更准确

  4. 动画触发:完美的视觉效果时机控制

  5. 虚拟滚动:大数据列表的性能救星


希望这些技巧能帮助你在前端开发中告别滚动监听的性能焦虑,写出更丝滑的交互代码!




🔗 相关资源






💡 今日收获:掌握了5个IntersectionObserver实用场景,这些知识点在实际开发中非常实用。



如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀


作者:Bug_Constructer
来源:juejin.cn/post/7549102542833631267
收起阅读 »

开源鸿蒙技术大会2025即将启幕,共绘数字底座新蓝图

当前,数字经济已成为驱动全球经济增长的重要力量,在这一进程中,操作系统作为数字基础设施的关键组成部分,其发展水平与生态建设直接关系到众多行业数字化转型的进程。作为智能终端操作系统的开源根社区,开源鸿蒙依托其先进的技术架构与持续的智能化升级,正在为培育新质生产力...
继续阅读 »

当前,数字经济已成为驱动全球经济增长的重要力量,在这一进程中,操作系统作为数字基础设施的关键组成部分,其发展水平与生态建设直接关系到众多行业数字化转型的进程。作为智能终端操作系统的开源根社区,开源鸿蒙依托其先进的技术架构与持续的智能化升级,正在为培育新质生产力提供重要支撑,并积极助力数字中国建设向纵深推进。

在万物智联从概念走向实践的关键阶段,开源操作系统如何突破技术瓶颈、构建成熟生态、赋能千行百业转型?

答案,即将于9月27日在长沙国际会议中心揭开——由开源鸿蒙项目群技术指导委员会(TSC,Technial Steering Committe)主办,华为承办,深开鸿、开鸿智谷、鸿湖万联、润开鸿、九联开鸿、中软国际、诚迈科技、北理工、中科鸿略、中国南方电网、中国科学院软件研究所、证通、国开鸿等合作单位支持举办的开源鸿蒙技术大会2025年度盛会,将面向全球开源操作系统领域,以更大规模、更高规格,迎接开源领袖、前沿实践专家、高校学者及生态伙伴,为全球开源从业者呈现一场兼具战略高度与实践深度的技术盛宴。

以核心突破,锚定操作系统技术演进新航向

作为大会的主场,主论坛将聚焦开源鸿蒙技术的年度核心进展,通过权威发声与重磅发布,勾勒智能终端操作系统全场景适配与技术深化的清晰路径。主论坛将邀请政府、产业界、学术界、开源界技术专家和权威代表,解读开源鸿蒙在数字基础设施建设中的核心价值。同时,应用生态伙伴与高校代表也将分享前沿实践成果,展现产学研用协同创新的生动实践,为开发者与企业伙伴提供可落地的技术方法论。

值得关注的是,本次主论坛将正式发布开源鸿蒙 6.0 Release版本,该版本进一步增强了系统能力,提升了系统整体可用性与开发效率,生态适配更友好。此外,大会还将举办开源鸿蒙跨平台框架PMC(筹)成立仪式,标志着开源鸿蒙在跨平台开发领域的重要布局,不断完善开源鸿蒙技术生态版图。

同时,本届大会共设置 21 场分论坛,无论在数量规模上,还是在领域覆盖广度上,均实现了近年来的新突破,展现出更全面的议题布局。分论坛将重点围绕开源鸿蒙相关技术垂域、行业与应用生态、教育及人才培养等多维度关键方向展开,全面覆盖从底层核心技术突破到前沿应用场景的全链条。大会旨在通过深入攻坚当前操作系统的核心技术难题、前瞻探索未来演进路径,并精准对接学术界研究、产业界应用与开发者实践需求,切实推动技术创新成为驱动产业转型升级的核心动力。

交流区全景呈现生态成果,见证开源协同五年发展收获

开源鸿蒙开源五年多以来,在全球开发者、生态伙伴的持续贡献下,生态不断繁荣。截至2025年8月31日,社区已汇聚超过9200名贡献者,累计贡献代码量超过1.3亿行,汇聚超480家伙伴参与共建,累计超1300+款产品通过兼容性测评,覆盖金融、超高清、航天、教育、商显、工业、警务、城市、交通、医疗、电力等多个领域。基于这一丰硕的生态共建成果,大会同步设置的6+N板块互动交流区,将集中展示基于"Powered by OpenHarmony"开源鸿蒙生态产品的创新成果与商业进展。

作为这一繁荣生态的集中缩影,开源鸿蒙项目群技术指导委员会(TSC)交流区将呈现其在开源鸿蒙技术生态领域的阶段性创新成果;开源鸿蒙社区公共交流区全面展现开源鸿蒙在千行百业的最佳实践与开源生态的繁荣;华为终端将首次与众多生态伙伴联袂亮相,共同展示基于开源鸿蒙的实践与创新成果;三方社区交流区则将呈现更广泛更多维的商业生态和落地部署情况。同时,仓颉编程语言&毕昇编译器以及应用生态开源技术等关键基础软件与工具链的集中展出,也将进一步凸显开源鸿蒙的产业落地价值,助力参会者清晰感知从技术创新到千行百业赋能的扎实路径,切身感受到开源鸿蒙在不同领域的应用实践与商业价值。

以致谢授牌,致敬贡献者,凝聚开源生态新合力

开源生态的繁荣,离不开每一位共建者的坚守与付出。本次大会将延续对贡献者的致敬传统,通过多场致谢与授牌仪式为社区力量加冕:通过对TSG、开源鸿蒙技术俱乐部的致谢授牌,肯定其在人才培育与技术推广中的杰出贡献;重磅发布开源鸿蒙年度课题,并为优秀技术课题成果授牌;开源鸿蒙项目群捐赠人授牌仪式,感谢生态伙伴为社区蓬勃发展所付出的努力;开源鸿蒙竞赛训练营颁奖,则将致敬在技术实践中表现卓越的开发者,进一步凝聚产学研用协同共进的开源生态新合力。

从底层技术突破到生态场景落地,从人才培养到全球协作,开源鸿蒙技术大会2025既是对过去一年技术进步与生态进展的盘点,更旨在展望以开源推动生态繁荣、以技术赋能产业转型的新未来。我们诚挚邀请全球技术领袖、高校学者、企业伙伴与开发者,于9月27日相聚长沙,在这场思想与技术创新交汇的盛会中共享成果、共探方向、共筑未来!

收起阅读 »

秋招太卷了!年轻人最缺的不是努力,而是有人即时点拨

2025年的秋招进入高峰。根据多家招聘平台数据,今年平均每个热门岗位竞争比超过 1:500,部分互联网、金融岗位甚至达到 1:2000。在AI筛简历和在线面试逐渐普及的背景下,不少应届生直呼“努力仿佛被系统淹没”。在这样的环境下,年轻人发现...
继续阅读 »

2025年的秋招进入高峰。根据多家招聘平台数据,今年平均每个热门岗位竞争比超过 1:500,部分互联网、金融岗位甚至达到 1:2000。在AI筛简历和在线面试逐渐普及的背景下,不少应届生直呼“努力仿佛被系统淹没”。

在这样的环境下,年轻人发现,光靠努力并不足以脱颖而出:

  • 简历反复修改,却依然石沉大海;

  • 面试准备充分,却临场紧张到大脑一片空白;

  • 拿到多个Offer,却在选择上迟迟犹豫。

专家指出,当下年轻人最缺的,不是努力,而是 在关键节点有人给予即时点拨。例如临面前的快速演练、谈薪时的策略建议,或是Offer选择的利弊分析。

近年来,一些新兴职场服务平台开始探索“即时支持”模式。例如,近期上线的 「对圈职场」App,就尝试通过 15分钟即时咨询 + 陪跑辅导 + 职场学习社区 的组合,为年轻人提供低门槛、全场景的职场支持。

业内人士认为,这类探索,或许能在缓解就业焦虑、提升求职质量方面发挥积极作用。对圈职场的出现,也从侧面印证了一个趋势:年轻人需要的不是泛泛的培训,而是更即时、更贴近现实的职场帮助

收起阅读 »

索引失效的场景有哪些?

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~ 关于SQL语句的面试题中,有两个被面试官问到的频率特别高,一个是SQL优化,另外一个则是索引失效。 接下来我们就来一一进行盘点,索引失效的场景到底有哪些。 准备工...
继续阅读 »

本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~



关于SQL语句的面试题中,有两个被面试官问到的频率特别高,一个是SQL优化,另外一个则是索引失效。


接下来我们就来一一进行盘点,索引失效的场景到底有哪些。


准备工作


我们先来创建一张订单表,并为该表生成100w+的数据,这样SQL语句所对应的执行计划会更加准确,表结构如下:


CREATE TABLE `tony_order`  (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`product_id` int NOT NULL COMMENT '商品ID',
`user_id` int NOT NULL COMMENT '用户ID',
`status` tinyint NOT NULL COMMENT '状态',
`discount_amount` int NOT NULL COMMENT '总金额',
`total_amount` int NOT NULL COMMENT '打折金额',
`payable_amount` int NOT NULL COMMENT '实际支付金额',
`receiver_name` varchar(255) DEFAULT NULL COMMENT '收件人名称',
`receiver_phone` varchar(255) DEFAULT NULL COMMENT '收件人手机号',
`receiver_address` varchar(255) DEFAULT NULL COMMENT '收件人地址',
`note` varchar(255) DEFAULT NULL COMMENT '备注',
`payment_time` datetime NULL DEFAULT NULL COMMENT '支付时间',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id` DESC) USING BTREE,
INDEX `idx_product_id`(`product_id` ASC) USING BTREE,
INDEX `idx_user_id_total_amount`(`user_id` ASC, `total_amount` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE,
INDEX `idx_status`(`status` ASC) USING BTREE,
INDEX `idx_receiver_phone`(`receiver_phone` ASC) USING BTREE,
INDEX `idx_receiver_name`(`receiver_name` ASC) USING BTREE,
INDEX `idx_receiver_address`(`receiver_address` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 ROW_FORMAT = Dynamic;

接下来我们来一一验证下索引失效的场景。



索引失效场景


1、不遵循最左前缀原则


SELECT * FROM tony_order WHERE total_amount = 100;


我们从执行计划中可以看到,这条SQL语句走的是全表扫描,即使创建了索引idx_user_id_total_amount也没有生效。


但由于其total_amount字段没有在联合索引的最左边,不符合最左前缀原则。


SELECT * FROM tony_order WHERE user_id = 4323 AND total_amount = 101;


当我们把user_id这个字段补上之后,果然就可以用上索引了。


在MySQL 8.0 版本以后,联合索引的最左前缀原则不再那么绝对了,其引入了Skip Scan Range Access Method机制,可对区分度较低的字段进行优化。


感兴趣的同学可以去看下,本文中就不过多展开描述了。


2、LIKE百分号在左边


SELECT * FROM tony_order WHERE receiver_address LIKE '%北京市朝阳区望京SOHO';

SELECT * FROM tony_order WHERE receiver_address LIKE '%北京市朝阳区望京SOHO%';

执行上面这两条SQL语句,结果都是一样的,走了全表扫描。



接下来我们将SQL语句改为%在右边,再执行一次看看。


SELECT * FROM tony_order WHERE receiver_address LIKE '北京市朝阳区望京SOHO%';  


这个原理很好理解,联合索引需要遵循最左前缀原则,而单个索引LIKE的情况下,也需要最左边能够匹配上才行,否则就会导致索引失效。


3、使用OR关键字


有一种说法,只要使用OR关键字就会导致索引失效,我们来试试。


SELECT * FROM tony_order WHERE receiver_name = 'Tony学长' OR user_id = 41323;


从结果中我们可以看到,索引并没有失效,聪明的查询优化器将receiver_name和user_id两个字段上的索引进行了合并。


接下来我们再换个SQL试试。


SELECT * FROM tony_order WHERE receiver_phone = '13436669764' OR user_id = 4323;


这次确实索引失效了,由于receiver_phone这个字段上并没有创建索引,所以无法使用索引合并操作了,只能走全表扫描。


有的同学会问,那为什么user_id上的索引也失效了呢?


因为一个字段走索引,另一个字段走全表扫描是没有意义的,反而不如走一次全表扫描查找两个字段更有效率。


所以,有时候索引失效未必是坏事,而是查询优化器做出的最优解。


4、索引列上有函数


SELECT * FROM tony_order WHERE ABS (user_id) = 4323; 


SELECT * FROM tony_order WHERE LEFT (receiver_address, 3) 


这个不用过多解释了,就是MySQL的一种规范,违反就会导致索引失效。


5、索引列上有计算


SELECT * FROM tony_order WHERE  user_id + 1 = 4324; 


这个也不用过多解释了,还是MySQL的一种规范,违反就会导致索引失效。


6、字段隐式转换


SELECT * FROM tony_order WHERE receiver_phone = 13454566332;


手机号字段明明是字符类型,却在SQL中不慎写成了数值类型而导致隐式转换,最终导致receiver_phone字段上的索引失效。


SELECT * FROM tony_order WHERE receiver_phone = '13454566332';


当我们把手机号加上单引号之后,receiver_phone字段的索引就生效了,整个天空都放晴了。


SELECT * FROM tony_order WHERE product_id = '12345';


我们接着尝试,把明明是数值型的字段写成了字符型,结果是正常走的索引。


由此得知,当发生隐式转换时,把数值类型的字段写成字符串类型没有影响,反之,但是把字符类型的字段写成数值类型,则会导致索引失效。


7、查询记录过多


SELECT * FROM tony_order WHERE product_id NOT IN (12345,12346);


那么由此得知,使用NOT IN关键字一定会导致索引失效?先别着急下结论。


SELECT * FROM tony_order WHERE status NOT IN (0,1);


从执行计划中可以看到,status字段上的索引生效了,为什么同样使用了NOT IN关键字,结果却不一样呢?


因为查询优化器会对SQL语句的查询记录数量进行评估,如果表中有100w行数据,这个SQL语句要查出来90w行数据,那当然走全表扫描更快一些,毕竟少了回表查询这个步骤。


反之,如果表中有100w行数据,这个SQL语句只需要查出来10行数据,那当然应该走索引扫描。


SELECT * FROM tony_order WHERE status IN (0,1);


同样使用IN关键字进行查询,只要查询出来的记录数过于庞大,都会通过全表扫描来代替索引扫描。


SELECT * FROM tony_order WHERE status = 0;


甚至我们不使用IN、NOT IN、EXISTS、NOT EXISTS这些关键字,只使用等号进行条件筛选同样会走全表扫描,这时不走索引才是最优解。


8、排序顺序不同


SELECT * FROM tony_order ORDER BY user_id DESC,total_amount ASC 


我们可以看下,这条SQL语句中的user_id用了降序,而total_amount用了升序,所以导致索引失效。


SELECT * FROM tony_order ORDERBY user_id ASC,total_amount ASC


而下面这两条SQL语句中,无论使用升序还是降序,只要顺序一致就可以使用索引扫描。


作者:托尼学长
来源:juejin.cn/post/7528296510229823530
收起阅读 »

大模型不听话?试试提示词微调

想象一下,你向大型语言模型抛出问题,满心期待精准回答,得到的却是答非所问,是不是让人抓狂?在复杂分类场景下,这种“大模型不听话”的情况更是常见。 提示词微调这一利器,能帮你驯服大模型,让其准确输出所需结果。 今天就来深入解析如何通过提示词工程、RAG 增强和 ...
继续阅读 »

想象一下,你向大型语言模型抛出问题,满心期待精准回答,得到的却是答非所问,是不是让人抓狂?在复杂分类场景下,这种“大模型不听话”的情况更是常见。


提示词微调这一利器,能帮你驯服大模型,让其准确输出所需结果。


今天就来深入解析如何通过提示词工程、RAG 增强和 Few Shots 学习等核心技术,高效构建基于 LLM 的分类系统。


分类系统架构设计


graph TD
A[输入文本] --> B[提示工程]
C[类别定义] --> B
D[向量数据库] --> E[RAG增强]
F[Few Shots示例] --> B
B --> G[LLM推理]
G --> H[结果提取]

分类系统的核心流程围绕提示工程展开,结合 RAG 增强和 Few Shots 学习可显著提升分类准确性。系统设计需关注数据流转效率与结果可解释性,特别适合保险票据、客户服务工单等高价值场景。


提示工程核心技巧


提示设计是 LLM 分类性能的关键,以下是经过实战验证的核心技巧:


1. 结构化表示法


采用 XML 或 JSON 格式封装类别定义和输入文本,提升模型理解效率:


# 类别定义示例
<categories>
<category>
<label>账单查询</label>
<description>关于发票、费用、收费和保费的问题</description>
</category>
<category>
<label>政策咨询</label>
<description>关于保险政策条款、覆盖范围和除外责任的问题</description>
</category>
</categories>

#
输入文本
<content>我的保险费为什么比上个月高了?</content>

2. 边界控制与结果约束


通过明确的指令和停止序列控制模型输出范围:


请根据提供的类别,对输入文本进行分类。
- 只需返回类别标签,不添加任何解释
- 如果无法分类,请返回"其他"

类别: [账单查询, 政策咨询, 理赔申请, 投诉建议, 其他]
输入: 我想了解我的保险是否涵盖意外医疗费用
输出:

3. 思维链提示


对于复杂分类任务,引导模型逐步思考:


我需要对客户的问题进行分类。首先,我会分析问题的核心内容,然后匹配最相关的类别。

客户问题: "我的汽车保险理赔需要提供哪些材料?"
分析: 这个问题是关于理赔过程中所需的材料,属于理赔相关的咨询。
类别匹配: 理赔申请
最终分类: 理赔申请

Few Shots 学习技术


Few Shots 学习通过提供少量示例,帮助模型快速适应特定任务:


1. 示例选择策略


# 选择多样化示例覆盖主要类别
示例1:
输入: "我的账单金额有误"
分类: 账单查询

示例2:
输入: "我想更改我的保险受益人"
分类: 政策变更

示例3:
输入: "我的车辆在事故中受损,如何申请理赔?"
分类: 理赔申请

2. 示例排序优化


# 按与输入的相关性排序示例
1. 最相关示例
输入: "我的保险费为什么上涨了?"
分类: 账单查询

2. 次相关示例
输入: "我想了解我的保险 coverage"
分类: 政策咨询

RAG 增强技术应用


检索增强生成(RAG)通过引入外部知识提升分类准确性:


1. 向量数据库构建与检索


# 1. 准备知识库文档
文档1: 保险理赔流程指南
文档2: 保险政策条款解释
文档3: 常见账单问题解答

#
2. 构建向量数据库
为每个文档创建嵌入向量并存储

#
3. 检索相关文档
对于输入文本,检索最相关的2-3个文档片段

2. 检索结果融合提示


# 结合检索结果和输入文本进行分类
检索到的相关信息:
[来自文档3] 常见账单问题包括费用上涨原因、账单错误等

输入文本: 我的保险费为什么比上个月高了?

请根据以上信息,将输入文本分类到以下类别之一:
[账单查询, 政策咨询, 理赔申请, 投诉建议, 其他]

技术整合示例


以下是整合提示词工程、RAG 技术和 Few Shots 学习的完整分类系统伪代码:


# 整合分类系统实现
class LLMClassifier:
def __init__(self, llm_client, vector_db):
self.llm_client = llm_client
self.vector_db = vector_db
self.categories = self._load_categories()
self.few_shot_examples = self._load_few_shot_examples()

def _load_categories(self):
# 加载类别定义
return {
"账单查询": "关于发票、费用、收费和保费的问题",
"政策咨询": "关于保险政策条款、覆盖范围和除外责任的问题",
"理赔申请": "关于理赔流程、材料和状态的问题",
"投诉建议": "对服务、流程或结果的投诉和建议",
"其他": "无法分类到以上类别的问题"
}

def _load_few_shot_examples(self):
# 加载Few Shots示例
return [
{"input": "我的账单金额有误", "label": "账单查询"},
{"input": "我想更改我的保险受益人", "label": "政策咨询"},
{"input": "我的车辆在事故中受损,如何申请理赔?", "label": "理赔申请"}
]

def _retrieve_relevant_docs(self, query, top_k=2):
# RAG检索相关文档
return self.vector_db.search(query, top_k=top_k)

def _build_prompt(self, query, relevant_docs):
# 构建整合提示
prompt = """
任务:将客户问题分类到以下类别之一:{categories}

类别定义:
{category_definitions}

相关知识:
{relevant_knowledge}

示例:
{few_shot_examples}

请按照以下步骤分类:
1. 分析客户问题的核心内容
2. 结合相关知识和示例,匹配最相关的类别
3. 只返回类别标签,不添加任何解释

客户问题:"{query}"
分类结果:
"""


# 填充模板
categories_str = ", ".join(self.categories.keys())
category_definitions = "\n".join([f"- {k}: {v}" for k, v in self.categories.items()])
relevant_knowledge = "\n".join([f"- {doc}" for doc in relevant_docs])
few_shot_examples = "\n".join([f"输入: \"{ex['input']}\"\n分类: {ex['label']}" for ex in self.few_shot_examples])

return prompt.format(
categories=categories_str,
category_definitions=category_definitions,
relevant_knowledge=relevant_knowledge,
few_shot_examples=few_shot_examples,
query=query
)

def classify(self, query):
# 1. RAG检索相关文档
relevant_docs = self._retrieve_relevant_docs(query)

# 2. 构建整合提示
prompt = self._build_prompt(query, relevant_docs)

# 3. LLM推理
response = self.llm_client.generate(
prompt=prompt,
max_tokens=100,
temperature=0.0
)

# 4. 提取结果
result = response.strip()
return result if result in self.categories else "其他"

# 使用示例
if __name__ == "__main__":
# 初始化LLM客户端和向量数据库
llm_client = initialize_llm_client() # 初始化LLM客户端
vector_db = initialize_vector_db() # 初始化向量数据库

# 创建分类器
classifier = LLMClassifier(llm_client, vector_db)

# 测试分类
test_queries = [
"我的保险费为什么比上个月高了?",
"我想了解我的保险是否涵盖意外医疗费用?",
"我的汽车保险理赔需要提供哪些材料?"
]

for query in test_queries:
category = classifier.classify(query)
print(f"查询: {query}\n分类结果: {category}\n")

通过以上核心技术的综合应用,可构建高效、准确的 LLM 分类系统,为保险、金融、客服等领域的文本分类需求提供强大解决方案。



nine|践行一人公司 | 🛰️codetrend


正在记录从 0 到 1 的踩坑与突破,交付想法到产品的全过程。



作者:nine是工程师
来源:juejin.cn/post/7543912699638906907
收起阅读 »

你一定疑惑JavaScript中的this绑定的究竟是什么?😵‍💫

web
想要了解this的绑定过程,首先要理解调用方式。调用方式调用方式被描述为函数被触发执行时语法形式。主要有以下几种基本模式:直接调用(独立函数调用): f1()方法调用: f1.f2()构造函数调用: new f1()显示绑定调用:...
继续阅读 »

想要了解this的绑定过程,首先要理解调用方式

调用方式

调用方式被描述为函数被触发执行时语法形式

主要有以下几种基本模式:

  1. 直接调用(独立函数调用): f1()
  2. 方法调用: f1.f2()
  3. 构造函数调用: new f1()
  4. 显示绑定调用: f1.call(f2) 或者 f1.apply(f2)
  5. 间接调用: (0,f1)()

第五点可能很多人没有见过,其实这是应用了逗号操作符,(0,f1)()其实等同于f1(),但它有什么区别呢?我放在显式绑定的最后来阐述吧。

有的人会用调用位置来解释this的绑定,但我感觉那个不太好用,可能是我没理解到位吧,如果有人知道怎么用它来解释this的绑定,希望能告诉我。总之,我们先用调用方式来解释this的绑定吧。

四种绑定规则

接下来介绍四种绑定规则。

默认绑定

首先要介绍的是默认绑定,当使用了最常用的函数调用类型:直接调用(独立函数调用) 时,便应用默认绑定。可以把这条规则看作是无法应用其他规则时的默认规则。

在默认绑定时,this绑定的是全局作用域

var a = 0;
function f1(){
var a = 1;
console.log(this.a); //输出为0
}
f1(); //直接调用,应用默认绑定

多个函数内部层层调用也是一样的。

var a = 0;
function f1(){
var a = 1;
f2();
}
function f2(){
var a = 2;
console.log(this.a); //输出的是0
}
f1();

隐式绑定

当函数被当作对象的属性被调用时(例如通过obj.f1()的形式),this会自动绑定到该对象上,这个绑定是隐式发生的,不需要显式使用callapplybind

var a = 0;
function f1() {
var a = 1;
console.log(this.a); //this绑定的是f2这个对象字面量
}
var obj = {
a : 2,
f1 : f1

// 也可以直接在obj内部定义f1
// function f1() {
// var a = 1;
// }

};
obj.f1(); // 输出为2

对象层层引用只有最后一个对象会影响this的绑定

var a = 0;
function f1() {
var a = 1;
console.log(this.a); //最后输出为2
}
var obj1 = {
a : 2,
f1 : f1
};
var obj2 = {
a : 3,
obj1 : obj1
}
obj2.obj1.f1();

可以发现这里有两个对象一个是obj1,一个是obj2obj2中的属性为obj1。先通过ob2.obj1调用obj1,再通过ob2.obj1.f1()调用f1函数,可以发现对象属性引用链中的最后一个对象为this所绑定的对象

隐性丢失

但隐式绑定可能会导致this丢失所绑定的对象,也就是会应用默认绑定(this绑定到全局作用域) 造成隐性丢失主要有两个方面,一个是给函数取别名,一个是回调函数

  • 函数取别名
var a = 0;
function f1() {
var a = 1;
console.log(this.a); //最后输出为0
}
var obj = {
a : 2,
f1 : f1
}
var fOne = obj.f1; // 给f1取了一个fOne的别名
fOne();

虽然函数fOneobj.f1的一个引用,但实际上,它引用的是f1函数本身,因此它执行的就是f1()。所以会使用默认绑定。

  • 回调函数
var a = 0;
// f1为回调函数,将obj.f2作为参数传递给f1
function f1(f2) {
var a = 1;
f2();
}
function f2() {
var a = 2;
console.log(this.a); //结果为0
}
var obj = {
a : 3,
f2 : f2
}
f1(obj.f2);

原因很简单,f1(obj.f2)obj.f2赋值给了function f1(f2) {...}中的f2(形参),就像上面讲的函数取了一个别名一样,实际执行的就是直接调用,所以应用默认绑定。

显式绑定

显式绑定很好理解,显式绑定让我们可以自定义this的绑定。我们通过使用函数的applycallbind方法,让我们可以自定义this的绑定。

var a = 0;
function f1 () {
var a = 1;
console.log(this.a);
}
//apply方法绑定this apply(对象,参数数组)
f1.apply({a:2}); //输出2

//call方法绑定this call(对象,参数1,参数2,...)
f1.call({a:3}); //输出3

//bind方法绑定this bind(对象,参数1,参数2,...),bind会返回一个函数,需要调用
boundf1 = f1.bind({a:4});
boundf1(); //输出4

但用applycall来进行显示绑定并不能避免隐性丢失的问题。下面有两个方法来解决这个问题。

1.硬绑定

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}
var bar = function() {
return f1.apply({a:2});
};
setTimeout(bar, 1000);//输出为2

让我们来分析分析这个代码。我们创建了函数bar,这个函数负责返回绑定好thisf1函数,并立即执行它。 这种绑定我们称之为硬绑定。

这种绑定方法会使用在一个i可以重复使用的辅助函数 例如

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}

function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
};
}

var bar = bind(f1,{a:2});
bar();

可以很明显发现这和我们js自带的函数bind方法很像。是的,在ES5中提供了内置的方法Function.prototype.bind。它的用法我再提一次吧。

var a = 0;
function f1 () {
var a = 1;
console.log(this.a);
}
//bind方法绑定this
//bind(对象,参数1,参数2,...),bind会返回一个函数,需要调用
boundf1 = f1.bind({a:2});
boundf1(); //输出2

2.API调用的“上下文”

第三方库的许多函数,以及JavaScript语言和宿主环境中许多内置函数,都提供了一个可选参数,通常被称为“上下文”,其作用和bind方法一样,都是为了防止隐性丢失。

现在来举个例子吧。

function f1(el) {
console.log(el, this.id);
}
var obj = {
id : "awesome"
};
[1,2,3].forEach(f1,obj);
//最后输出的结果为
// 1 'awesome'
// 2 'awesome'
// 3 'awesome'

逗号操作符

在文章开头我们提到了这样一种表达式(0,f1)(),这是逗号操作符的应用,逗号操作符会依次计算所有的表达式,然后返回最后一个表达式的值。这里(0,f1)会先计算0(无实际意义),然后再返回f1,所以最后为f1()

理解了逗号操作符的使用,那如果我们把f1改为obj.f1呢,即(0,obj.f1)(),这时f1中的this绑定的是谁呢?

直接说结论,绑定的是全局对象。(0,obj.f1)()先计算0,然后返回obj.f1即f1函数本身,所以它返回的是一个解绑this的函数,其相当于f1.call(window)——window是全局对象。

下面我们来验证一下吧。

var a = 0;
function f1() {
var a = 1;
console.log(this.a);
}
var obj = {
a : 2,
f1 : f1
};
(0,obj.f1)(); //输出0

完全正确哈哈,注意这种方式不算作隐性丢失哦。

  • 这个操作只是调用了 obj.f1,并没有阻止垃圾回收(GC)。
  • 如果 obj 或 f1 没有其他引用,它们仍然会被正常回收。

如果对其具体的工作流程感兴趣,可以去网上再找些资料。本篇就不讲太详细了。

new 绑定

这是this绑定的最后一条规则。

new绑定通常的形式为:... = new MyClass(参数1,参数2,...)

JavaScript中的new操作符的机制和那些面向类的语言的new操作符有些不一样,因为JavaScript是基于原型的语言(这个也许以后我会谈谈哈哈)。在JavaScript中,“构造函数”仅仅只是你使用new操作符时被调用的函数。

使用new来调用函数,会自动执行以下操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行原型连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

我们现在重点要关注的是第三点。

function f1(a){
this.a = a;
}
var bar = new f1(2);
console.log(bar.a); //输出为2
console.log(f1.a); //输出为undefined

这段代码就可以很明显的看出来new会创建一个新对象bar,并把this绑定到这个bar上,所以才会在bar上创建a这个属性。而原来的f1上则没有a这个属性,所以是undefined

四条规则的优先级

  1. 如果某个调用位置应用了多条规则该怎么办?这时我们就需要知道它们的优先级了。 首先,默认绑定的优先级是最低的。我们先来测试一下它们隐式绑定和显式绑定哪个优先级高吧,这里我偷个懒,就引用一下《你不知道的JavaScript(上卷)》这本书的测试代码
function foo() {  
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

稍微分析一下吧,obj1.foo()obj2.foo()为隐式调用,this分别绑定的为obj1obj2,所以会打印23。接着我们调用了obj1.foo.call(obj2)发现结果输出为obj2中的a属性2,所以这里应用的是显式绑定。

所以显式绑定的优先级是高于隐式绑定的

  1. 再来看看new绑定和隐式绑定的优先级谁更高吧。
function foo(something) {  
this.a = something;
}
var obj1 = {
foo: foo
};
obj1.foo(1);
console.log( obj1.a ); // 1

var bar = new obj1.foo(2);
console.log( obj1.a ); // 1
console.log( bar.a ); // 2

var bar = new obj1.foo(2)这段代码,如果隐式绑定的优先级会大于new绑定,就会在obj1里把属性a赋值为2; 如果new绑定的优先级大于隐式绑定,就会在bar中创建一个属性a,值为2,最后看obj1.abar.a谁输出为2,谁的优先级就更高,很明显bar.a输出为2,所以new绑定的优先级高于隐式绑定的。

所以new调用的优先级要高于隐式调用的优先级

  1. 再来看看new调用和显式调用的优先级谁高谁低吧。

new不能和applycall方法同时使用,但我们可以用bind方法进行硬绑定,再用bind返回的新函数再new一下以此来判断谁的优先级高。

function foo(something) {  
this.a = something;
}

var obj1 = {};

var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2

var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

首先硬绑定了obj1,在obj1中创建了a属性,值为2bar接收返回的bind函数。之后new bar并给a赋值为3,用baz来接收new的对象,这时如果baz.a3就说明this应用的绑定规则是new绑定。

所以new绑定的优先级是高于显示调用的优先级的。

现在知道了四种规则,又知道了这四个规则的优先级,我们就能很清晰的判断this的绑定了。

判断this的流程

以后判断this我们可以按以下顺序来判断:

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

     var bar = new foo()  //这里bar为this绑定的对象
  2. 函数是否通过callapply(显式绑定)或者硬绑定(bind)调用?如果是的话,this绑定的是指定的对象。

     var bar = foo.call(obj)  //这里obj为this绑定的对象
  3. 函数是否在某个上下文对象中调用(隐式绑定)如果是的话,this绑定的是那个上下文对象。

     var bar = obj.foo()  //这里obj为this绑定的对象
  4. 如果都不是,则应用默认绑定,this绑定到全局对象上。

     var bar = foo()   //this绑定的为全局对象 

凡事都有例外,还有一些十分特殊的情况不满足上面的四条规则,我们需要单独拎出来记忆。

绑定例外

绑定例外主要有3种。

null导致的绑定意外

var a = 0;  
function f1() {
var a = 1;
console.log(this.a);
}
f1.apply(null); //输出为0

var bar = f1.bind(null);
bar() //输出为0

当我们使用显式绑定(使用apply、call、bind方法)的时候,如果我们显式绑定一个null,就会发现this绑定的不是null而是应用默认绑定,绑定全局对象。这会导致变量全局渗透的问题。

有的人可能会说,那我们不用null来绑定this不就好了吗?但有的时候我们还真不得不使用null来绑定this,下面我来介绍一下什么时候会使用这种情况。

一种常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时很好用的。

function f1 (a , b) {  
console.log("a:" + a + ",b:" + b);
}

f1.apply(null,[2,3]) //输出为a:2,b:3

//bind的柯里化
var bar = f1.bind(null,2);
bar(3); //输出为a:2,b:3

现在来简单地来介绍一下柯里化是什么?柯里化是将一个接收多个参数的函数转换为一系列只接受单个参数的函数。这时bindnull的作用就体现出来了。

然而,在apply,call,bind使用null会导致全局溢出,在一些有this的函数中,给这个this绑定null,会让this绑定全局对象。该如何解决这个问题呢?

更安全的this

我们可手动创建一个空的对象,这个空的对象我们称作“DMZ”(demilitarized zoo,非军事区)对象——它是一个空的非委托的对象。

如果我们在想要忽略this绑定时总是传入一个DMZ对象,那就不用担心this会溢出到全局了,这个this绑定的就是DMZ对象。

在JavaScript中创建一个空对象最简单的方法是Object.create(null)——它会返回一个空对象,Object.create(null)Object.create(null){}很像,并不会创建Object.prototype这个委托,所以它比{}“更空”。

var c = 0;
function f1 (a , b) {
this.c = 1;
console.log("a:" + a + ",b:" + b);
}
//创建自己的空对象
var myNull = Object.create(null);

f1.apply(myNull,[2,3]) //输出为a:2,b:3
console.log(c); //输出为0

//bind的柯里化
var bar = f1.bind(myNull,2);
bar(3); //输出为a:2,b:3
console.log(c); //输出为0

可以发现这段代码中,我们创建了自己的空对象通过applybind方法把this绑定到这个空对象了。最后的输出的c0,说明this.c并没有修改全局变量c的值。所以这个方法可以防止全局溢出。

接下来谈谈另外一个绑定的例外吧。

间接引用

有的时候你可能(有意或无意地)创建了一个函数的“间接引用”,在这种情况下,调用这个函数应用默认绑定规则。

var a = 0;
function f1() {
console.log(this.a);
}
var obj1 = {
a : 1,
f1 : f1
};

var obj2 = {
a : 2,
};
obj1.f1(); // 1
(obj2.f1 = obj1.f1)(); // 0

我们来看看这个代码。obj1中有af1属性或方法,a的值为1obj2中只有a属性,值为2。我们先隐式绑定obj1this绑定obj1,最后输出为1,这个我们可以理解。关键是下面这行代码(obj2.f1 = obj1.f1)()obj2中没有f1,所以它在obj2中创建一个f1,然后将obj1中的f1函数赋值给obj2f1,然后执行这个赋值表达式。那为什么输出的是0而不是obj2中的2或者obj1中的1呢? 🤔

其实这和赋值表达式的返回值有关系,因为赋值表达式会返回等号右边的值。 所以(obj2.f1 = obj1.f1)实际上返回的obj1.f1中的f1函数,实际执行的是f1()。所以应用的是默认绑定,this绑定全局对象,结果输出为0

我们继续看绑定的下一个例外。

箭头函数

在ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。

箭头函数和一般的函数不一样,它不是用function来定义的,而是使用被称作“胖箭头”的操作符=>定义的。

定义格式:(参数) => {函数体}

箭头函数不使用this的四条规则,而是继承外层(定义时所在)函数或全局作用域的this的值,this在箭头函数创建时就被确定,且永远不会被改变,new也不行。

var a = 0;
(()=>{
var a = 1;
console.log(this.a); // 结果输出为0
}
)();

很明显该箭头函数外部就是全局作用域,所以继承全局对象的this就是它本身,所以输出为0

再看看如果在其他函数中定义箭头函数this如何绑定

var a = 0;
function f1() {
var a = 1;
(()=>{
var a = 2;
console.log(this.a);
}
)();
}
f1();//输出0

//给f1绑定一个对象
f1.apply({a:3}); // 输出3

可以发现f1内部的箭头函数继承了其外部函数f1this的绑定。所以一开始没给f1绑定this时,f1this绑定的是全局对象,箭头函数的也是全局对象;当给f1this绑定一个对象时,箭头函数的this也绑定该对象。

小结

以上是我的学习分享,希望对你有所帮助。

还有本篇的四条规则只适用于非严格模式,严格模式的this的绑定我日后再出一篇吧,其实只是有点懒😂。

参考书籍

《你所不知道的JavaScript(上卷)》


作者:mrsk
来源:juejin.cn/post/7504237094283526178
收起阅读 »

为什么我的第一个企业级MCP项目上线3天就被叫停?

graph TB A[企业AI需求] --> B[MCP企业架构] B --> C[安全体系] B --> D[运维管理] B --> E[实施路径] C --> C1[身份认证]...
继续阅读 »
graph TB
A[企业AI需求] --> B[MCP企业架构]
B --> C[安全体系]
B --> D[运维管理]
B --> E[实施路径]

C --> C1[身份认证]
C --> C2[数据保护]
C --> C3[访问控制]

D --> D1[自动化部署]
D --> D2[监控告警]
D --> D3[成本优化]

E --> E1[MVP阶段]
E --> E2[扩展阶段]
E --> E3[优化阶段]

style A fill:#FFE4B5
style B fill:#90EE90
style C fill:#87CEEB
style D fill:#DDA0DD
style E fill:#F0E68C


3分钟速读:企业级MCP部署不同于个人使用,需要考虑安全合规、高可用性、统一管理等复杂需求。本文提供从架构设计到运维管理的完整企业级MCP平台构建方案,包含安全框架、监控体系和分阶段实施路径,帮助企业构建统一、安全、可扩展的AI工具平台。



"系统上线第三天就被安全部门紧急叫停,所有人都在会议室里看着我。"


那是我职业生涯中最尴尬的时刻之一。作为一家500人科技公司的架构师,我以为把个人版的MCP简单放大就能解决企业的AI工具集成问题。结果呢?权限混乱、数据泄露风险、合规审计不通过...


CEO当时问我:"我们现在有20多个团队在用各种AI工具,每个团队都有自己的一套,你觉得这样下去会不会出问题?"我当时信心满满地说:"没问题,给我两周时间。"


现在想想,那时的我真是太天真了。个人用Claude Desktop配置几个MCP服务器确实10分钟就搞定,但企业级别?完全是另一个世界。


从那次失败中我学到:企业级MCP部署面临的不是技术问题,而是管理和治理的系统性挑战。


🏢 企业AI工具集成的挑战与机遇


个人vs企业:天壤之别的复杂度


当我们从个人使用转向企业级部署时,复杂度呈指数级增长:


个人使用场景



  • 用户:1个人

  • 数据:个人文件和少量API

  • 安全:基本的API密钥管理

  • 管理:手动配置即可


企业级场景



  • 用户:数百到数千人

  • 数据:敏感业务数据、客户信息、财务数据

  • 安全:严格的合规要求、审计需求

  • 管理:统一配置、权限控制、监控告警


从我参与的十几个企业AI项目来看,大家基本都会遇到这几个头疼的问题:


1. 数据安全这道坎


企业数据可不比个人文件,涉及客户隐私、商业机密,动不动就要符合GDPR、HIPAA这些法规。我见过一个金融客户,光是数据分类就搞了3个月,更别说传统的个人化MCP配置根本过不了合规这关。


2. 权限管理的平衡艺术


这个真的很难搞。不同部门、不同级别的人要访问的数据和工具都不一样。既要保证"最小权限原则",又不能让用户觉得太麻烦。我之前遇到过一个案例,权限设置太严格,结果销售团队抱怨查个客户信息都要申请半天。


3. 成本控制的现实考验


这个问题往往被低估。当几百号人同时用AI工具时,API调用费用真的会让财务部门头疼。我见过一家公司,第一个月账单出来,CFO直接找到CTO问是不是系统被攻击了。


4. 运维管理的复杂度爆炸


分散部署最大的问题就是运维。每个团队都有自己的一套,出了问题谁来解决?性能怎么优化?我们之前有个客户,光是梳理现有的AI工具部署情况就花了两周时间。


MCP在企业环境中的价值主张


正是在这样的背景下,MCP的企业级价值才真正显现:



  • 统一标准:一套协议解决所有AI工具集成问题

  • 集中管理:统一的配置、监控、审计

  • 安全可控:标准化的安全框架和权限管理

  • 成本透明:集中的资源使用监控和成本分析


我们最近做了个小范围调研,发现用了统一MCP平台的几家企业,AI工具管理成本大概能降低50-70%,安全事件也确实少了很多。虽然样本不大,但趋势还是挺明显的。


📊 企业级需求分析:规模化部署的关键考量


在动手设计企业级MCP方案之前,我觉得最重要的是先搞清楚企业到底需要什么。这些年参与了十几个项目下来,我发现企业级MCP部署基本都绕不开这几个核心需求:


多团队协作需求


场景复杂性



  • 研发团队:需要访问代码仓库、CI/CD系统、Bug跟踪系统

  • 销售团队:需要CRM系统、客户数据、销售报表

  • 运营团队:需要监控系统、日志分析、业务指标

  • 财务团队:需要ERP系统、财务报表、合规数据


每个团队的需求不同,但又需要在统一的安全框架下协作。


安全合规要求


企业级部署必须满足严格的安全合规要求:


合规标准主要要求MCP实现方案
GDPR数据主体权利、数据最小化细粒度权限控制、数据脱敏
SOX财务数据完整性、审计跟踪完整审计日志、不可篡改记录
ISO27001信息安全管理体系全面安全控制框架
HIPAA医疗数据保护加密传输、访问控制

性能和可用性要求


企业级应用对性能和可用性有严格要求:



  • 可用性:99.9%以上(年停机时间<8.77小时)

  • 响应时间:95%的请求在2秒内响应

  • 并发能力:支持数千用户同时访问

  • 数据一致性:确保跨系统数据同步


成本控制需求


企业需要精确的成本控制和预算管理:



  • 成本透明:每个部门、每个项目的AI使用成本清晰可见

  • 预算控制:设置使用上限,避免成本失控

  • 优化建议:基于使用数据提供成本优化建议


🏗️ MCP企业级架构设计:构建统一工具平台


说到架构设计,我必须承认,刚开始接触企业级MCP时,我也走过不少弯路。最开始我想的太简单,以为把个人版的MCP放大就行了,结果第一个项目就翻车了——系统上线第三天就因为权限问题被安全部门叫停。


后来痛定思痛,我重新设计了一套分层的企业级MCP架构。这套架构现在已经在好几个项目中验证过了,既能应对复杂的业务需求,扩展性也不错。


整体架构方案


graph TB
subgraph "用户层"
A[Web界面]
B[IDE插件]
C[移动应用]
D[API接口]
end

subgraph "网关层"
E[MCP网关]
F[负载均衡器]
G[API网关]
end

subgraph "服务层"
H[认证服务]
I[权限服务]
J[MCP服务注册中心]
K[配置管理中心]
end

subgraph "工具层"
L[开发工具MCP服务器]
M[数据工具MCP服务器]
N[业务工具MCP服务器]
O[监控工具MCP服务器]
end

subgraph "数据层"
P[关系数据库]
Q[文档数据库]
R[缓存层]
S[日志存储]
end

A --> E
B --> E
C --> E
D --> G

E --> F
G --> F
F --> H
F --> I

H --> J
I --> J
J --> K

K --> L
K --> M
K --> N
K --> O

L --> P
M --> Q
N --> R
O --> S

核心组件详解


1. MCP网关层


功能职责



  • 路由管理:智能路由请求到合适的MCP服务器

  • 负载均衡:分发请求,确保系统稳定性

  • 安全认证:统一的身份验证和授权

  • 限流控制:防止系统过载,保护后端服务


核心特性:支持智能路由、负载均衡、限流控制和统一认证,确保系统稳定性和安全性。


2. 服务注册中心


核心功能



  • 服务发现:自动发现和注册MCP服务器

  • 健康检查:实时监控服务器状态

  • 配置同步:统一的配置管理和分发

  • 版本管理:支持服务的灰度发布和回滚


技术要点:采用分布式注册中心架构,支持服务自动注册、健康检查和配置热更新。


3. 配置管理中心


管理内容



  • 服务器配置:MCP服务器的连接参数和功能配置

  • 权限配置:用户和角色的权限矩阵

  • 业务配置:各种业务规则和策略配置

  • 环境配置:开发、测试、生产环境的差异化配置


高可用性设计


为确保企业级的可用性要求,架构中集成了多种高可用保障机制:


1. 多活部署



  • 多个数据中心同时提供服务

  • 自动故障切换,RTO < 30秒

  • 数据实时同步,RPO < 5分钟


2. 弹性扩容



  • 基于负载自动扩容

  • 支持水平扩展和垂直扩展

  • 预测性扩容,提前应对流量高峰


3. 容错机制



  • 服务熔断,防止雪崩效应

  • 优雅降级,保证核心功能可用

  • 重试机制,处理临时性故障


🔐 安全架构设计:保障企业数据安全


在企业环境中,安全绝对不是可选项。这个教训我学得特别深刻——前面提到的那个翻车项目,就是因为我低估了企业对安全的要求。现在我设计MCP安全架构时,坚持用"纵深防御"策略,每一层都要有安全控制,宁可麻烦一点,也不能留安全隐患。


身份认证和授权体系


1. 多层次身份认证


graph LR
A[用户登录] --> B[SSO认证]
B --> C[MFA验证]
C --> D[JWT Token]
D --> E[API访问]

B --> B1[LDAP/AD]
B --> B2[OAuth2.0]
B --> B3[SAML]

C --> C1[短信验证码]
C --> C2[TOTP]
C --> C3[生物识别]

技术实现:集成主流SSO提供商(Azure AD、Okta、Google),支持多种MFA方式,采用JWT令牌管理会话。


2. 基于角色的访问控制(RBAC)


权限模型设计


# 权限配置示例
roles:
- name: developer
permissions:
- mcp:tools:code:read
- mcp:tools:code:execute
- mcp:resources:docs:read

- name: data_analyst
permissions:
- mcp:tools:database:read
- mcp:tools:analytics:execute
- mcp:resources:data:read

- name: admin
permissions:
- mcp:*:*:* # 超级管理员权限

users:
- username: john.doe
roles: [developer]
additional_permissions:
- mcp:tools:deploy:execute # 额外权限

数据安全保护


1. 端到端加密



  • 传输加密:所有MCP通信使用TLS 1.3

  • 存储加密:敏感数据AES-256加密存储

  • 密钥管理:使用HSM或云KMS管理加密密钥


2. 数据脱敏和分类


核心功能:自动识别敏感数据类型(邮箱、手机、身-份-证等),根据预设规则进行脱敏处理,确保数据隐私保护。


网络安全防护


1. API网关安全策略



  • DDoS防护:智能识别和阻断攻击流量

  • WAF规则:防护SQL注入、XSS等常见攻击

  • IP白名单:限制访问来源IP范围

  • 请求限流:防止API滥用


2. 网络隔离


安全策略:采用DMZ、内部服务区、数据库区三层网络隔离,通过防火墙规则严格控制服务间通信。


审计日志和合规


1. 全链路审计


审计范围:记录所有MCP访问操作,包括用户身份、操作类型、访问资源、操作结果、IP地址等关键信息,确保操作可追溯。


2. 合规报告自动生成



  • 访问报告:用户访问行为分析

  • 权限报告:权限使用情况统计

  • 异常报告:安全异常事件汇总

  • 合规检查:自动化合规性检查


⚙️ 运维管理体系:确保稳定高效运行


运维这块儿,说实话是我最头疼的部分。技术方案设计得再好,如果运维跟不上,照样会出问题。我见过太多项目,前期开发得很顺利,上线后各种运维问题层出不穷。所以现在我做企业级MCP平台时,会把运维管理当作一个系统工程来对待,从部署、监控到优化,每个环节都要考虑周全。


自动化部署体系


1. CI/CD流水线设计


流水线阶段:测试→构建→部署开发环境→预发布→生产环境,每个阶段都包含自动化测试、安全扫描和质量检查。


2. 蓝绿部署和灰度发布


蓝绿部署策略:新版本部署到绿环境→健康检查→流量切换→清理旧环境,确保零停机部署。


监控告警系统


1. 多维度监控指标


监控维度



  • 业务指标:请求总数、成功率、响应时间、活跃用户数

  • 系统指标:CPU、内存、磁盘使用率

  • 成本指标:按请求计费、部门成本分摊


2. 智能告警系统


# Prometheus告警规则
groups:
- name: mcp-platform
rules:
- alert: MCPHighErrorRate
expr: rate(mcp_requests_failed_total[5m]) / rate(mcp_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "MCP平台错误率过高"
description: "过去5分钟MCP请求错误率超过5%"

- alert: MCPHighLatency
expr: histogram_quantile(0.95, rate(mcp_request_duration_seconds_bucket[5m])) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "MCP平台响应延迟过高"
description: "95%的请求响应时间超过2秒"

- alert: MCPServerDown
expr: up{job="mcp-server"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "MCP服务器宕机"
description: "{{ $labels.instance }} MCP服务器无法访问"

成本优化管理


1. 成本监控和分析


成本分析功能:自动分析计算、存储、网络、API等各项成本,按部门分摊费用,并提供优化建议。


2. 自动扩缩容策略


# Kubernetes HPA配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mcp-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mcp-server
minReplicas: 3
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
- type: Pods
pods:
metric:
name: mcp_requests_per_second
target:
type: AverageValue
averageValue: "100"

🚀 实施路径和最佳实践


关于实施策略,我觉得最重要的一点是:千万别想着一步到位。我之前就犯过这个错误,想着一次性把所有功能都上线,结果搞得团队疲惫不堪,用户体验也很糟糕。现在我都是推荐分阶段实施,这套策略在好几个项目中都验证过了,确实比较靠谱。


分阶段实施计划


第一阶段:MVP验证(1-2个月)


目标:验证MCP在企业环境中的可行性


实施内容



  • 选择1-2个核心团队作为试点

  • 部署基础的MCP服务器(文件系统、Git、简单API)

  • 建立基本的安全和监控机制

  • 收集用户反馈和性能数据


成功标准



  • 试点团队满意度 > 80%

  • 系统可用性 > 99%

  • 响应时间 < 2秒

  • 零安全事件


第二阶段:功能扩展(2-3个月)


目标:扩展功能覆盖范围,优化用户体验


实施内容



  • 集成更多业务系统(CRM、ERP、数据库)

  • 完善权限管理和审计功能

  • 优化性能和稳定性

  • 扩展到更多团队


成功标准



  • 覆盖50%以上的核心业务场景

  • 用户数量增长3倍

  • 平均响应时间减少30%

  • 成本控制在预算范围内


第三阶段:全面推广(3-6个月)


目标:在全公司范围内推广使用


实施内容



  • 部署完整的企业级架构

  • 建立完善的运维体系

  • 开展全员培训

  • 建立持续优化机制


成功标准



  • 全公司80%以上员工使用

  • 系统可用性 > 99.9%

  • 用户满意度 > 85%

  • ROI > 200%


团队组织和协作


1. 核心团队构成


graph TB
A[项目指导委员会] --> B[项目经理]
B --> C[架构师]
B --> D[开发团队]
B --> E[运维团队]
B --> F[安全团队]

C --> C1[系统架构师]
C --> C2[安全架构师]

D --> D1[后端开发]
D --> D2[前端开发]
D --> D3[MCP服务器开发]

E --> E1[DevOps工程师]
E --> E2[监控工程师]

F --> F1[安全工程师]
F --> F2[合规专员]

2. 协作机制


定期会议制度



  • 周例会:项目进展同步和问题解决

  • 月度评审:里程碑检查和计划调整

  • 季度总结:成效评估和策略优化


文档管理



  • 架构文档:系统设计和技术规范

  • 操作手册:部署和运维指南

  • 用户指南:使用教程和最佳实践


风险控制和应急预案


1. 风险识别和评估


风险类型风险等级影响范围应对策略
系统故障全公司多活部署、快速切换
安全漏洞敏感数据安全扫描、及时修复
性能问题用户体验性能监控、弹性扩容
合规风险法律风险合规检查、审计跟踪

2. 应急响应流程


应急流程:故障分级→通知相关人员→启动应急响应→执行应急措施→跟踪处理进度→事后总结,确保快速响应和持续改进。


📈 案例研究:中大型企业MCP平台实践


说了这么多理论,我觉得还是用真实案例更有说服力。下面分享几个我亲身参与的项目,有成功的,也有踩坑的,希望对大家有帮助。


案例一:中型科技公司(800人规模)


公司背景



  • 行业:SaaS软件开发

  • 规模:800名员工,15个研发团队

  • 挑战:AI工具使用分散,成本控制困难


实施方案



  • 架构选择:单数据中心部署,微服务架构

  • 核心功能:代码助手、文档管理、项目协作

  • 安全措施:RBAC权限控制、API网关防护


实施效果


实施前后对比:
开发效率:
before: "基线"
after: "+35%"
measurement: "功能交付速度"

成本控制:
before: "月成本$15,000"
after: "月成本$12,000"
savings: "20%"

安全事件:
before: "月均3起"
after: "月均0.5起"
reduction: "83%"

用户满意度:
before: "6.5/10"
after: "8.7/10"
improvement: "+34%"

关键成功因素



  1. 高层支持:这个真的很重要,CEO亲自站台,资源要人给人要钱给钱

  2. 分阶段实施:我们从最积极的两个团队开始,让他们当种子用户,效果好了再推广

  3. 用户培训:别小看这个,我们光培训就搞了一个月,但确实值得

  4. 持续优化:每周都会收集用户反馈,有问题马上改,这个习惯一直保持到现在


案例二:大型金融机构(5000+人规模)


公司背景



  • 行业:银行业

  • 规模:5000+名员工,严格合规要求

  • 挑战:数据安全、合规审计、多地部署


实施方案



  • 架构选择:多活数据中心,容器化部署

  • 核心功能:风险分析、客户服务、合规报告

  • 安全措施:端到端加密、零信任架构


金融级安全要求:TLS 1.3传输加密、AES-256数据加密、HSM密钥管理、PCI-DSS/SOX合规、7年审计日志保留、本地化数据存储。


实施效果



  • 合规性:通过所有监管审计,零合规违规

  • 效率提升:客户服务响应时间减少50%

  • 成本节约:年度IT成本降低25%

  • 风险控制:欺诈检测准确率提升40%


经验教训总结


通过这些案例,我们总结出企业级MCP实施的关键经验:


成功要素



  1. 明确的ROI目标:设定可量化的成功指标

  2. 充分的资源投入:人力、资金、时间的保障

  3. 渐进式实施:避免大爆炸式部署

  4. 用户参与:让最终用户深度参与设计和测试


常见陷阱



  1. 忽视安全合规:在设计初期就要考虑安全要求

  2. 低估培训成本:用户培训和支持需要充分投入

  3. 缺乏监控:没有完善的监控就无法及时发现问题

  4. 一步到位心态:试图一次性解决所有问题


💡 写在最后:从失败到成功的思考


回想起那次项目失败,我现在反而挺感谢那次经历。它让我明白了一个道理:企业级MCP集成绝不是技术的简单堆砌,而是一个涉及人、流程、技术的复杂系统工程。


如果重新来过,我会这样做:



  1. 先调研,再动手:花更多时间理解企业的真实需求,而不是想当然

  2. 小步快跑:从最简单的MVP开始,证明价值后再扩展

  3. 安全第一:把合规和安全放在功能之前考虑

  4. 拥抱变化:技术在发展,需求在变化,保持架构的灵活性


现在我参与的企业级MCP项目,成功率已经提升到90%以上。不是因为我的技术水平提高了多少,而是因为我学会了从企业的角度思考问题。


最好的架构不是最复杂的,而是最适合的。


如果你正在考虑为企业部署MCP平台,我的建议是:先找一个小团队试点,积累经验和信心,然后再考虑大规模推广。记住,每个企业都有自己的特色,别人的成功方案未必适合你。




🤔 互动时间


分享你的经验:



  • 你的企业在AI工具集成方面遇到了什么挑战?

  • 你觉得统一的AI工具平台对企业来说最大的价值是什么?

  • 有没有类似的项目失败经历想要分享?


实践练习:



  1. 使用文章中的需求分析框架,评估你所在企业的MCP部署需求

  2. 基于你的行业特点,设计合适的安全控制措施

  3. 参考分阶段实施策略,制定适合你企业的部署计划


欢迎在评论区分享你的想法和经验,我会认真回复每一条评论。



📧 如果你正在规划企业级MCP项目,可以私信我,我很乐意分享更多实战经验和踩坑心得。





下期预告:《MCP最佳实践与性能优化》将深入探讨MCP使用过程中的优化技巧和故障排查方法,敬请期待!


关注专栏,获取更多MCP实战干货!


作者:人生都在赌
来源:juejin.cn/post/7532742298825768998
收起阅读 »

java中,使用map实现带过期时间的缓存

在 Java 开发领域,缓存机制的构建通常依赖于 Redis 等专业缓存数据库。这类解决方案虽能提供强大的缓存能力,但引入中间件意味着增加系统架构复杂度、部署成本与运维负担。本文将深入探讨一种轻量级替代方案 —— 基于 Java 原生Map实现的带过期时间的缓...
继续阅读 »

在 Java 开发领域,缓存机制的构建通常依赖于 Redis 等专业缓存数据库。这类解决方案虽能提供强大的缓存能力,但引入中间件意味着增加系统架构复杂度、部署成本与运维负担。本文将深入探讨一种轻量级替代方案 —— 基于 Java 原生Map实现的带过期时间的缓存机制。该方案无需引入外部工具,仅依托 Java 标准库即可快速搭建起缓存体系,特别适用于对资源占用敏感、架构追求极简的项目场景,为开发者提供了一种轻量高效的缓存数据管理新选择。


优点



  1. 轻量便捷:无需引入 Redis 等外部中间件,直接使用 Java 标准库即可实现,降低了项目依赖,简化了部署流程。

  2. 快速搭建:基于熟悉的Map数据结构,开发人员能够快速理解和实现缓存逻辑,显著提升开发效率。

  3. 资源可控:可灵活控制缓存数据的生命周期,通过设置过期时间,精准管理内存占用,适合对资源占用敏感的场景。


缺点:该方案存在明显局限性,即数据无法持久化。一旦应用程序停止运行,缓存中的所有数据都会丢失。相较于 Redis 等具备持久化功能的专业缓存数据库,在需要长期保存缓存数据,或是应对应用重启后数据恢复需求的场景下,基于 Java 原生Map的缓存机制就显得力不从心。


代码实现


package com.sunny.utils;

import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class SysCache {

// 单例实例
private static class Holder {
private static final SysCache INSTANCE = new SysCache();
}

public static SysCache getInstance() {
return Holder.INSTANCE;
}

// 缓存存储结构,Key为String,Value为包含值和过期时间的CacheEntry对象
private final ConcurrentHashMap<String, CacheEntry> cacheMap = new ConcurrentHashMap<>();
// 定时任务执行器
private final ScheduledExecutorService scheduledExecutorService;

// 私有构造方法,初始化定时清理任务
private SysCache() {
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
// 每隔1秒执行一次清理任务
scheduledExecutorService.scheduleAtFixedRate(this::cleanUp, 1, 1, TimeUnit.SECONDS);
// 注册JVM关闭钩子以优雅关闭线程池
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
}

/**
* 存入缓存
* @param key 键
* @param value 值
*/

public void set(String key, Object value){
cacheMap.put(key, new CacheEntry(value, -1));
}

/**
* 存入缓存
* @param key 键
* @param value 值
* @param expireTime 过期时间,单位毫秒
*/

public void set(String key, Object value, long expireTime) {
if (expireTime <= 0) {
throw new IllegalArgumentException("expireTime must be greater than 0");
}
cacheMap.put(key, new CacheEntry(value, System.currentTimeMillis() + expireTime));
}

/**
* 删除缓存
* @param key 键
*/

public void remove(String key) {
cacheMap.remove(key);
}

/**
* 缓存中是否包含键
* @param key 键
*/

public boolean containsKey(String key) {
CacheEntry cacheEntry = cacheMap.get(key);
if (cacheEntry == null) {
return false;
}
if (cacheEntry.getExpireTime() < System.currentTimeMillis()) {
remove(key);
return false;
}
return true;
}


/**
*获取缓存值
* @param key 键
*/

public Object get(String key) {
CacheEntry cacheEntry = cacheMap.get(key);
if (cacheEntry == null) {
return null;
}
if (cacheEntry.getExpireTime() < System.currentTimeMillis()) {
cacheMap.remove(key);
return null;
}
return cacheEntry.getValue();
}


private static class CacheEntry {
private final Object value;
private final long expireTime;

public CacheEntry(Object value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}

public Object getValue() {
return value;
}

public long getExpireTime() {
return expireTime;
}
}

/**
* 定时清理过期条目
*/

private void cleanUp() {
Iterator<Map.Entry<String, CacheEntry>> iterator = cacheMap.entrySet().iterator();
long currentTime = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<String, CacheEntry> entry = iterator.next();
CacheEntry cacheEntry = entry.getValue();
if (cacheEntry.expireTime < currentTime) {
// 使用iterator移除当前条目,避免ConcurrentModificationException
iterator.remove();
}
}
}

/**
* 关闭线程池释放资源
*/

private void shutdown() {
scheduledExecutorService.shutdown();
try {
if (!scheduledExecutorService.awaitTermination(5, TimeUnit.SECONDS)) {
scheduledExecutorService.shutdownNow();
}
} catch (InterruptedException e) {
scheduledExecutorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}


测试



如上图,缓存中放入一个值,过期时间为5秒,每秒循环获取1次,循环10次,过期后,获取的值为null


作者:小太阳381
来源:juejin.cn/post/7496335321781829642
收起阅读 »

svg按钮渐变边框

web
共用css body { padding: 50px; background-color: black; color: white; } svg { --text_fill: orange; --svg_width: ...
继续阅读 »

共用css


body {
padding: 50px;
background-color: black;
color: white;
}

svg {
--text_fill: orange;
--svg_width: 120px;
--svg_height: 40px;
width: var(--svg_width);
height: var(--svg_height);
cursor: pointer;
/* 创建图层 */
will-change: transform;

&:hover {
--text_fill: #fed71a;
}

text {
fill: var(--text_fill);
font-size: 1rem;
transform: translate(50%, 50%);
text-anchor: middle;
dominant-baseline: middle;
stroke: yellowgreen;
stroke-width: .5px;
cursor: pointer;
}

rect {
--stroke_width: 4px;
width: calc(var(--svg_width) - var(--stroke_width));
height: calc(var(--svg_height) - var(--stroke_width));
stroke-width: var(--stroke_width);
rx: calc(var(--svg_height)/2);
x: calc(var(--stroke_width)/2);
y: calc(var(--stroke_width)/2);
fill: none;
cursor: pointer;
}
}

移入执行、移出暂停


<body>
<svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
<defs>
<linearGradient id='strokeColor1' x1='0%' y1='0%' x2='100%' y2='0%'>
<stop offset='0%' stop-color="#00ccff" stop-opacity="1" />
<stop offset='50%' stop-color="#d400d4" stop-opacity="1" />
<stop offset='100%' stop-color="#ff00ff" stop-opacity=".7" />
</linearGradient>
</defs>
</svg>

<svg id="svg1">
<text>渐变按钮</text>
<rect stroke='url(#strokeColor1)' />
<animateTransform id="ani1" href="#strokeColor1" attributeName='gradientTransform' dur='5s' type="rotate"
form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="indefinite" />

</svg>
</body>
<script>
svg1.addEventListener('mouseover', function () {
if (!this.beginMark) {
ani1.beginElement();
this.beginMark = true;
return;
}
this.unpauseAnimations();
})

svg1.addEventListener('mouseleave', function () {
this.pauseAnimations();
})
</script>

svg1效果图


svg1.gif


移入暂停、移出执行


<body>
<svg style='position:absolute;left:-9999px;width:0;height:0;visibility:hidden;'>
<defs>
<linearGradient id='strokeColor2' x1='0%' y1='0%' x2='100%' y2='0%'>
<stop offset='0%' stop-color="#ec261b" />
<stop offset='50%' stop-color="#ff9f43" />
<stop offset='100%' stop-color="#ffe66d" stop-opacity="1" />
</linearGradient>
</defs>
</svg>

<svg id="svg2">
<text>渐变按钮</text>
<rect stroke='url(#strokeColor2)' />
<animateTransform id="ani2" href="#strokeColor2" attributeName='gradientTransform' dur='5s' type="rotate"
form="0,.5,.5" to="360,.5,.5" repeatCount='indefinite' begin="0s" />

</svg>
</body>

<script>
svg2.addEventListener('mouseover', function () {
this.pauseAnimations();
})
svg2.addEventListener('mouseleave', function () {
this.unpauseAnimations();
})
</script>

sv2效果图


svg2.gif


总结


个人感觉svg实现渐变边框相比较css的实现来说,相对代码量更大一些,但是svg其实还有很多好玩的地方。
用svg来做渐变边框也是另外的一种思路,也许以后能够用的上。


css按钮渐变边框


作者:大林i瑶
来源:juejin.cn/post/7488575555048161332
收起阅读 »