真正的乐观,是做好被裁员的准备 | 跳槽决策四步法
引言
进入社会后,除了结婚、买房这类重要的事情外,跳槽、选择工作是我们最重要的决策。
每次跳槽,都决定了未来一段时间你处于的行业、岗位、收入,在一定程度上影响你的生活方式。
可就是如此重要的事情,我过去几次换工作,做的都不是太好。
我或许会每天都刷招聘网站,可就算刷到了意向的职位,也迟迟不敢在软件上点下“发送简历”按钮,可能是怕准备不充分、怕行情不好、怕离开熟悉的环境……结果拖到最后某一刻,被动离开。
最近看了一本书叫《怎样决定大事》,里面提到了一些做决策的方法,我试着把这套理论用在跳槽上,聊聊怎么样做出最清醒的跳槽决策。
核心用十六个字可以概括:看清处境,把握时机,避免直觉,适应局面,下面正文开始。
看清处境
马云说过员工离职就两个原因:钱没到位,心委屈了。
但真正让人下定决心离职的,从来不是这么简单的二选一,而是一连串复杂又难以理清的现实。
- 比如年底一到,领导又说你没达预期,绩效一如既往地一般;
- 办公室政治让你无所适从,干着最多的活,背着最大的锅;
- 甚至公司的方向都让你怀疑未来是否值得继续坚持。
这些都让你有离职的想法,但是很多小事也不是不能忍。工资算不上多吧,但也是符合市场水平的。繁琐的工作干着有点烦, 但起码已经轻车熟路。
如果你也在犹豫和纠结,首先要弄清楚你自己的处境,你需要有「情景意识」,情景意识分为三个层次
第一层,了解已经发生了什么。
这里就是刚刚提到的,比如不涨薪、领导pua、工作对自己没有任何成长,这些是已经发生的事情。
第二层,了解为什么会发生这种情况。
这里你思考导致现状的原因,比如技术水平不足,领导并没有给你涨薪。也有可能是公司所处的行业发展停滞,公司大量裁员,导致你工作越来越累。也有可能是你的领导没有眼光,发现不了你的优秀。
但需要注意的是,你要分析两到三种可能性,不是一种,也不是十种。
为什么不是一种?因为如果你头脑中只有一种解释,一旦判断错了,你的努力可能就毫无意义,甚至走向错误的方向。
比如工作经验比较少的程序员在遇到工作瓶颈时,常常会下意识归因为“我是不是太菜了?”。
毕竟程序员天生有技术思维,认为技术可以解决所有问题,性能问题?优化代码。bug频发,重构核心逻辑。
但你以为的问题,不一定是问题的全部。
比如现实世界有很多种可能:你的领导根本没打算提拔你,无论你多努力;你所在的部门业务边缘化,再怎么出色也没有舞台;公司战略转向AI,传统技术深耕已经不再受重视……
为什么不是十种?因为你如果考虑的原因太多,你的大脑就会陷入“分析瘫痪”,最终你什么决定也做不了。你需要抓大放小,找准核心矛盾,忽略那些无关紧要事情。
理清发生了什么、为什么发生,我们才能看清——未来会发生什么。
第三层,据此预测接下来会发生什么。
预测未来可能发生的情况,有一个反人性的技巧,是主动思考最坏的结果。
举个例子,你的公司因为经营原因,已经经历了两轮大规模裁员了,幸运的是一直没有裁到你,领导也安慰你好几次:“放心,你很重要。”
你该因为自己没被裁而庆幸吗?事实上你必须做好最坏的打算,那就是你会出现在下一轮的裁员名单上。
你需要提前思考对应的策略,比如开始评估外面的机会,更新简历,提前做准备。那么即使最坏的情况出现,你也不会猝不及防、惊慌失措。
未来是有不确定性的,我们往往会回避思考可怕的结果,但这会让自己在最坏的事情发生时,带来更多的伤害。
就像现在AI快速发展,几年内随时都有可能替代绝大部分基础性岗位,甚至高级的程序员也会被替代,那么我们必须做好现有岗位随时被替代的准备。
真正的乐观,是认真思考最坏的结果后,发现自己扛得住。
把握时机
毕业后我在济南工作,由于工资略显寒酸,互联网发展火热,我便有了去北京工作的念头。
念头归念头,回到现实我就怂了。那时候我根本没有工作经验,异地找工作这件事对我也很陌生,我不知道自己能不能找到工作,更不知道面试都会问什么技术问题。
我一想到这些就感觉头脑一片空白,想准备却无从下手。于是,我的选择是靠打游戏麻痹自己,开始拖延。
拖延了差不多半年,最后因为频繁出差,冲动之下选择裸辞去了北京。由于没有充分的准备,也是历经一番波折。
回顾这段经历,因为离职这件事没有明确的截止时间,我陷入了两种极端:要么因为恐惧未知,反复拖延,最后什么也没做;要么因为短期情绪,冲动行动。
决策不只是决定做什么,还有决定什么时候做。
先说说怎么避免冲动,那就是在做出离职决定之前,你需要先问自己一个简单的问题: “我需要现在离职吗?”
如果答案是否定的,就不着急做出决策。
这是因为我们很容易陷入情绪当中。
比如你给领导提的好几个建议都不被采纳,感觉收到了冷落;技术不如你的同事拿到了比你还好的绩效,或者项目突然增加导致频繁加班。
程序员一定都听过“不要裸辞”这个忠告,一开始我认为这是因为离职后你可能会以为没有收入,导致面试的心态越来越不稳。后来我觉着这个忠告最大的作用,就是避免我们陷入情绪当中,一上头选择裸辞。
就像我当时裸辞后去了北京,由于没有任何准备,投了半个多月简历,一共就接到4个面试,绝大部分投递的简历都是已读不回。
你可能会说我技术很强,面试准备的非常充分,那我是不是可以随时选择离开呢?
你的确会有更多的底气,但是招聘是有招聘旺季的,比如所谓的“金三银四、金九银十”,因为正好处于企业全年、半年总结,企业会根据未来的计划进行人力盘点,释放岗位。但过去这两个节点,比如十一月份到来年一月份,那就是企业的招聘淡季,甚至是裁员季,如果你十月份离职,极容易遇见投递的简历大部分都未读未回。
诸葛亮已经万事俱备,那也得等等东风。
但是,等一等不意味着你什么也不做,你需要积极收集和换工作相关的信息。
改简历、刷题就不说了,现在什么行业比较火热?招聘的要求比起几年前有什么变化?未来什么样得企业最有发展前景?如果离职找工作不顺利,财务状况有没有什么影响?
这些都需要大量信息,并且充满不确定性,所以你需要去主动收集和了解。
当然了,你也不能一直准备下去,就像刷算法、刷面试题这件事,准备的越久,就会陷入边际效应递减,你不可能把所有的知识都学会,对吧?
这时候你就需要给自己制定一个时间框架,比如专心准备3个月,这期间不去面试。3个月后无论准备的如何,都必须让自己开始投递简历面试,避免回避和拖延。
避免直觉
你可能已经了解过很多认知陷阱:确认偏误让我们只寻找支持自己观点的信息;可得性启发让我们高估容易想起的事件发生概率;首因效应让我们过度依赖最初信息。
我举几个找工作容易陷入的认知陷阱。
第一个是「投射偏差」,比如把过去跳槽必涨薪的经验,投射到现在和将来,忽视了市场环境的变化。
18年我去北京时,互联网发展依旧火热,大厂扩招、抢人,程序员跳槽涨薪50%、80%都不是什么难事,如果你在大数据、P2P火热的时候进入相关企业,薪资翻倍的例子屡见不鲜。
可后来随着互联网增速放缓,涨薪越来越难,疫情之后各类企业发展不顺,别说涨薪了,如果被裁员被动找工作,平薪、降薪也都是有可能的。
如果你还按老的认知来,发现怎么涨薪总是不如预期,自然是心理落差极大,如果因为这个拒绝了一些各方面都不错的offer,那就太可惜了。
第二个是「短期结果焦虑」,过于关注短期结果成败,忽略了长远目标和发展。
你做足了准备,兴致勃勃的开始投简历,一连投了十几家都没接到面试,好不容易接到几个面试,结果全都在一面就挂了。
也许你的简历有硬伤,也许是没有准备充分,这很正常,查缺补漏,继续前行就好。
但你不能陷入焦虑和自我怀疑:我履历太差了,好公司根本不会看我的简历;我能力太差了,大厂的面试我根本不可能过。
最可怕的情况就是,因为面试不顺利,仓促入职一家并不满意的公司。
第三个是单一维度决策,面对offer选择时,我们有可能陷入单一维度决策,比如是否大厂,薪资是否足够高,这是我自己总结出来的。
假设你这时候已经拿到了好多个offer,你该选择哪家企业入职呢?你可能特别关注薪资情况,你强烈的倾向于最高薪资的那个offer。你特别在乎名气,于是选择市场上名气最大的那个。
事实证明只考虑一个因素肯定不行,薪资最高的那个可能工作时间长还996,时薪并不比别的offer高。你的确入职了名气最大的那个企业,但做的不是核心业务,绩效不行,技术也没有什么成长。
我之前写过一篇文章,里面介绍了一个简单公式。比如在职业发展中,我觉着几个比较重要的是行业前景、公司文化和具体岗位,薪资当然也是我们衡量的一个重要指标,但其他的因素我们只做参考,而不能作为决策的决定因素。
对于选择offer这件事,我们也可以借助这个思路,识别几个你认为最重要的核心因素进行打分,选择总分最高的那一个。
别考虑太多,也不能考虑太少,这样才能做出最佳决策。
适应局面
即使决策已经做出,一切也并没有结束,你需要持续评估和调整,不断适应新的局面。
而我们面对新局面的反应,在很多时候是有点慢的。
这里我不得不提到AI,我谈不上对AI有着多深的见解,但当今AI巨头的模型,都已经具备了“完成小块的复杂代码”的能力。
我看到网上的一个预测,不出两年,就可以训练出一个可以自我迭代、不断尝试的AI编程高手。
高级程序员,将是最早一批开始被替代的。
当然,被替代的不仅是程序员行业,绘画、设计、金融、编辑,都面临着这个局面。
我提到AI,就是想提醒大家,对于处在行业第一线的我们,对于AI的适应能力有多高?
适应能力强的人,已经逐步用AI去完成越来越多的工作。而适应能力差的人,甚至现在遇见问题还是习惯性的打开搜索引擎,一点一点的翻看别人的资料。
我刚毕业时,深钻技术对职业生涯只有好处,没有坏处。但现在的局面是,如果还一股脑的让自己陷入到源码里面,不如用好AI,解放自己。
面对技术变革,就算没有应用,也要强迫自己了解。
最可怕的就是认为一些变化都与自己无关。
说在最后
做重大决策,主要分四步:看清处境,把握时机,避免直觉,适应局面。
这四步并不只用于跳槽,职业转换、城市迁移、关系选择、生活方式改变,都可以依靠这个模型去思考和行动。
你或许觉着这太麻烦了,但想想我们花了多少时间在鸡毛蒜皮的小事上?可能网购一件物品,为了价格货比三家;吃午饭订外卖,在各种美食间反复纠结;早上为了选择穿什么衣服,不断尝试。
把时间浪费在这些上面,却在重要的决策上匆匆决定,岂不是本末倒置吗?
这是东东拿铁的第88篇原创文章,欢迎关注,喜欢请三连。
来源:juejin.cn/post/7538357382453657626
实现一个简化版的状态机框架
实现一个简化版的状态机框架
我将为你设计一个简化版的状态机框架,包含状态、事件、转换机制,并支持插件扩展功能。这个实现会参考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());
}
}
进一步扩展建议
- 分层状态机:实现父子状态机结构,支持复合状态和子状态机
- 历史状态:添加对历史状态的支持,允许状态机返回到之前的某个状态
- 伪状态:实现初始状态、终止状态等特殊状态类型
- 转换类型:增加外部转换、内部转换、本地转换等不同类型的转换
- 配置DSL:创建流畅的API用于配置状态机,类似:
machine.configure()
.from(INIT).on(START_PROCESS).to(PROCESSING)
.perform(action)
- 持久化策略:添加更多持久化选项(数据库、文件等)
- 监控插件:添加性能监控、统计信息收集等插件
- 分布式支持:添加集群环境下状态同步的支持
- 异常处理:完善异常处理机制,支持在转换中处理异常
- 表达式支持:集成SpEL或其他表达式语言支持条件判断
这个实现提供了一个灵活的状态机框架基础,可以根据具体需求进一步扩展和完善。
来源:juejin.cn/post/7512231268420894729
goweb中间件
中间件基本概念
中间件(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"憨憨"变成夸它"真棒"。这个技巧就是提示词链(Prompt Chaining) ——简单来说,就是把一个大任务拆解成小步骤,像教小孩一样,一步步引导AI完成复杂任务。
这一个技巧到底是什么?用人话说就是...
图1:提示词链的基本工作流程 - 化整为零,各个击破
提示词链说白了就是"化整为零"的艺术。你不直接问AI一个巨大的问题,而是像剥洋葱一样,一层层地引导它思考。
举个栗子🌰
传统做法(一把梭): "请帮我写一篇关于人工智能发展趋势的深度分析报告,包括市场分析、技术发展、未来预测等内容。"
结果:AI可能会给你一篇看似专业但内容空洞的"八股文"。然后你就想骂:"AI你个憨憨!"
提示词链做法(循循善诱):
- 第一步:调研收集 - "请列出当前人工智能领域的主要发展方向"
- 第二步:深度分析 - "基于以上发展方向,分析每个方向的市场规模和技术成熟度"
- 第三步:趋势预测 - "根据前面的分析,预测未来3-5年各个方向的发展趋势"
- 第四步:报告整合 - "将以上内容整合成一份结构化的分析报告"
看出区别了吗?用了这个技巧后,你会发现AI突然变聪明了,这时候你就会忍不住说:"AI你真棒!"
为什么这一个技巧这么有效?三大核心优势
1. 准确性大幅提升
图2:提示词链 vs 单次提问的准确性对比
AI就像一个刚入职的实习生,你一次性给他太多任务,他就蒙圈了。但如果你一步步指导,他就能做得很好。
我曾经让AI帮我写一个产品介绍,直接问的话,它给了我一堆车轱辘话。后来我改用提示词链:
- 先让它分析目标用户
- 再让它提取产品核心卖点
- 然后针对用户痛点匹配卖点
- 最后整合成介绍文案
结果?完美!就像魔法一样。
2. 过程可控,随时调整
传统方式就像开盲盒,你永远不知道AI会给你什么惊喜(或惊吓)。而提示词链让你可以在每一步都检查结果,发现不对劲立马调整。
这就像做菜,你不会把所有调料一次性倒进锅里,而是一样样加,尝一下味道,不够再加。
3. 复杂任务变简单
还记得小时候数学老师教我们解应用题的方法吗?"读题→找条件→列方程→求解"。提示词链就是这个思路,把复杂问题分解成简单步骤。
提示词链的八大类型:总有一款适合你
1. 顺序链(Sequential Chain)- 最基础款
图3:顺序链结构 - 一步接一步,稳扎稳打
就像流水线一样,前一步的结果是后一步的输入。
实际应用场景: 写邮件 → 检查语法 → 调整语气 → 发送
2. 分支链(Branching Chain)- 一分为多
图4:分支链结构 - 分而治之,高效并行
这就像你让三个员工同时处理不同的任务,最后汇总。
实际应用场景: 分析用户反馈 →
- 分支1:提取积极评价
- 分支2:提取消极评价
- 分支3:统计满意度分数 → 汇总报告
3. 迭代链(Iterative Chain)- 精益求精
图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:动态分支示例 - 智能分流,精准处理
就像智能客服,根据用户问题自动选择处理流程。
2. 递归处理 - 处理超大任务
对于特别大的文档或数据,可以用递归方式处理:
分析500页报告 → 按章节拆分 → 逐章分析 → 汇总结果
3. 多模态链 - 文字+图片+声音
图7:多模态提示词链 - 跨媒体协作
现在的AI不只会处理文字,还能处理图片、音频。你可以设计跨媒体的提示词链。
成功案例分享:真实项目中的提示词链应用
案例1:内容创作工作流
一位自媒体博主用提示词链优化创作流程:
原来的痛苦: 灵感来了→直接写→写到一半卡住→删掉重写→循环往复
提示词链拯救:
- 主题确定:基于热点分析,确定文章主题
- 大纲生成:结构化思路,生成文章框架
- 内容填充:逐段撰写,保持逻辑连贯
- 优化润色:语言美化,增强可读性
- SEO优化:标题和关键词优化
结果: 创作效率提升300%,文章质量显著改善,阅读量平均增长150%。
案例2:客服智能化升级
某电商公司用提示词链改造客服系统:
传统客服问题:
- 响应慢
- 答非所问
- 用户体验差
提示词链解决方案:
- 问题分类:自动识别问题类型
- 情绪分析:判断用户情绪状态
- 方案匹配:根据问题类型匹配解决方案
- 个性化回复:结合用户历史,生成个性化回复
- 满意度跟踪:自动跟进处理结果
效果: 客户满意度从65%提升到92%,人工客服工作量减少70%。
未来展望:提示词链的下一步发展
1. 与AI Agent深度融合
未来的AI Agent会内置更智能的提示词链,能够自主设计和优化执行流程。
2. 可视化设计工具
就像用Scratch编程一样,未来会有拖拽式的提示词链设计工具,小白也能轻松上手。
3. 自适应优化
AI能够根据执行结果自动优化链条结构,实现持续改进。
总结:掌握提示词链,让AI成为你的得力助手
提示词链技术说到底就是一个道理:化繁为简,循序渐进。
就像优秀的老师不会一口气把所有知识塞给学生,而是循循善诱,step by step。掌握了提示词链,你就能让AI变成一个贴心的老师、得力的助手。
关键要点回顾:
- 分解任务:把大象装进冰箱分三步
- 控制节奏:每一步都要有明确目标
- 灵活调整:发现问题及时优化
- 合理设计:避免过度复杂化
- 持续改进:根据效果不断优化
行动建议:
- 从简单开始:选择一个日常任务,尝试用3步提示词链优化
- 记录模板:把好用的链条保存下来,形成自己的工具库
- 持续学习:关注新工具和新技巧,保持技能更新
- 分享交流:和其他用户交流经验,互相学习
记住:最好的提示词链不是最复杂的,而是最适合你需求的。从今天开始,让AI真正成为你的智能伙伴吧!
如果这篇文章对你有帮助,别忘了点赞收藏。有问题欢迎在评论区讨论,我会尽力解答。一起在AI时代做个聪明的"驯兽师"! 🚀
来源:juejin.cn/post/7541935177033072655
每天一个知识点——dayjs常用的语法示例
日期时间处理需求
- 关于时间的处理,一般来说使用公共库更加优雅、方便
- 否则的话,自己就要写一堆处理时间的函数
- 比如:我需要一个将当前时间,转换成年月日时分秒格式的函数
- 如下:
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
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%。排查半天才发现,是对象创建太随意导致的 “内存爆炸”。
八年 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 类核心信息:
- Mark Word:存储对象的哈希码、锁状态(偏向锁 / 轻量级锁 / 重量级锁)、GC 年龄等。
- 实战用:排查死锁时,通过
jstack
查看线程持有锁的对象,就是靠 Mark Word 里的锁状态。
- 实战用:排查死锁时,通过
- Class Metadata Address:指向对象所属类的
Class
对象(如User.class
)。
- 实战用:反射时
user.getClass()
,就是通过这个指针找到Class
对象。
- 实战用:反射时
- Array Length:如果是数组对象,存储数组长度。
步骤 5:执行<init>()
方法 → “给对象穿衣服”
最后,JVM 会执行对象的构造函数(<init>()
方法),完成:
- 成员变量赋值(如
this.name = "张三"
)。 - 执行构造代码块(
{}
包裹的代码)。
这一步才是对象的 “最终初始化”,完成后,一个完整的对象就诞生了,指针会赋值给user
变量。
三、实战解析:怎么排查对象创建相关的问题?
八年开发中,我总结了 3 套 “对象创建问题排查方法论”,从工具到思路,都是踩坑后的精华。
1. 问题 1:对象创建太多 → 怎么找到 “罪魁祸首”?
症状:GC 频繁、内存占用高、响应延迟增加。
工具:jmap
(查看对象实例数)、Arthas
(实时排查)、VisualVM
(分析 GC 日志)。
实战步骤:
- 用
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
- 用 Arthas 的
trace
命令,查看OrderDTO
的创建位置:
trace com.example.OrderService createOrder -n 100
- 定位到循环中创建
OrderDTO
的代码,优化为 “复用对象” 或 “批量创建”。
2. 问题 2:对象创建慢 → 怎么定位瓶颈?
症状:创建对象耗时久(如复杂对象初始化)、类加载慢。
工具:jstat
(查看类加载耗时)、AsyncProfiler
(分析方法执行时间)。
实战步骤:
- 用
jstat -class 进程ID 1000
,查看类加载速度:
Loaded Bytes Unloaded Bytes Time
1234 234560 0 0 123.45 # Time是类加载总耗时,单位ms
- 若类加载慢,检查是否有 “大 jar 包” 或 “类冲突”;若对象初始化慢,用 AsyncProfiler 分析构造函数耗时。
3. 问题 3:单例对象多实例 → 怎么验证?
症状:单例类(如PaymentClient
)出现多实例,导致资源泄漏。
工具:jmap -dump:live,format=b,file=heap.hprof 进程ID
(dump 堆内存)、MAT
(分析堆快照)。
实战步骤:
- Dump 堆内存后,用 MAT 打开,搜索
PaymentClient
类。 - 查看 “Instance Count”,若大于 1,说明单例模式实现有问题(如没加双重检查锁)。
四、核心代码:对象创建的 5 种方式与实战选型
八年开发中,我用过 5 种对象创建方式,每种都有明确的适用场景,选错了就会踩坑。下面结合代码和业务场景对比分析:
1. new 关键字:最基础,但别滥用
代码:
// 普通对象创建
User user = new User("张三", 25);
// 注意:循环中避免频繁new无状态对象
List<User> userList = new ArrayList<>();
// 坑:每次循环都new,10万次循环创建10万个UserValidator
for (Order order : orderList) {
UserValidator validator = new UserValidator(); // 优化:改为单例或局部变量复用
validator.validate(order);
}
适用场景:简单对象、非频繁创建的对象。
八年经验:别在循环中new
临时对象,尤其是无状态工具类(如Validator
、Calculator
),改用单例或对象池。
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% 的对象创建相关问题:
- 避免在循环中 new 临时对象:无状态工具类用单例,临时 DTO 用对象池。
- 复杂对象优先用建造者模式:字段超过 5 个就别用
new
了,参数顺序错了很难查。 - 单例模式别用懒汉式:优先枚举或静态内部类,线程安全且无反射漏洞。
- 别忽视对象的 “销毁” :使用对象池时,一定要在
finally
中归还对象,避免内存泄漏。 - 慎用 finalize () 方法:它会延迟对象回收(需要两次 GC),建议用
try-with-resources
管理资源。 - 监控对象实例数:线上系统定期用
jmap
检查,避免 “隐形” 的对象爆炸。 - 类加载别踩版本冲突:依赖冲突会导致类加载失败,用
mvn dependency:tree
排查。 - 对象创建不是越多越好:有时候 “复用” 比 “创建” 更高效,比如 String 用
intern()
复用常量池对象。
六、结尾:基础不牢,地动山摇
八年 Java 开发,我越来越觉得:真正的高手,不是会写多复杂的框架,而是能把基础问题理解透彻。对象创建看似简单,却关联着 JVM、GC、设计模式、性能优化等多个维度。
我见过太多新人因为不懂对象创建的底层逻辑,写出 “看似能跑,实则埋满坑” 的代码;也见过资深开发者通过优化对象创建,把系统 QPS 从 1 万提升到 10 万。
希望这篇文章能帮你从 “会用new
” 到 “懂创建”,在实战中写出更高效、更稳定的 Java 代码。如果有对象创建相关的踩坑经历,欢迎在评论区分享~
来源:juejin.cn/post/7545921037286047744
【小程序】迁移非主包组件以减少主包体积
代码位置
问题及背景
- 微信小程序主包体积最大为
2M
,超出体积无法上传。 - 组件放在不同的目录下的表现不同:
src/components
目录中的组件会被打包到主包中,可以被所有页面引用。src/pages/about/components
目录中的组件会被打印到对应分包中,只能被当前分包引用(只考虑微信小程序的话可以用分包异步化,我这边因为需要做不同平台所以不考虑这个方案)。
在之前的项目结构中,所有的组件都放在 src/components
目录下,因此所有组件都会被打包到主包中,这导致主包体积超出了 2M
的限制。
后续经过优化,将一些与主包无关的组件放到了对应分包中,但是有一些组件,在主包页面中没有被引用,但是被多个不同的分包页面引用,因此只能放到 src/components
目录下打包到主包中。
本文的优化思路就是将这一部分组件通过脚本迁移到不同的分包目录中,从而减少主包体积,这样做的缺点也显而易见:会增加代码包的总体积(微信还有总体积小于 20M
的限制 🤮)。
实现思路
项目中用 gulp
做打包流程管理,因此将这个功能封装成一个 task
,在打包之前调用。
1. 分析依赖
分析 src/components
组件是否主包页面引用,有两种情况:
- 直接被主页引用。
- 间接被主页引用:主页引用
a
,a
引用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
的组件是不需要放在主包中的,在这一步中将这些组件分发到对应的分包中。
思路:
- 遍历所有
referForMainPackage
值为false
的组件。 - 遍历所有组件的
refers
列表,如果refer
能匹配到分包,做以下动作:
- 在分包根目录下创建
componentsauto
目录,将组件复制到这里。 - 复制组件中引用的相对路径资源。
- 在分包根目录下创建
- 删除
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布局 |
---|---|---|
布局机制 | 动态弹性计算,支持二次布局(重新分配空间) | 单次线性排列,无二次布局 |
方向控制 | 支持水平(Row)、垂直(Column)及反向排列 | Row仅水平,Column仅垂直 |
换行能力 | 支持自动换行(FlexWrap.Wrap ) | 不支持换行,子组件溢出时被截断或压缩 |
子组件控制 | 支持flexGrow 、flexShrink 、flexBasis 动态分配空间 | 仅支持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预处理器
作为mockm项目的维护者,这几天我一直在优化CI/CD流水线。终于把自动化测试和发布流程都搞定了,心想着可以安心写代码了。结果今天早上一看GitHub Actions,我傻眼了...
项目突然构建失败了
昨天还好好的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相关的错误报告如雨后春笋般涌现:
- [Nx框架] -
'stylus@https://registry.npmjs.org/stylus/-/stylus-0.64.0.tgz' is not in this registry on npm install nrwl/nx#32031
- [TypeScript CSS Modules] -
Stylus contained malicious code and was removed from the registry by the npm security team mrmckeb/typescript-plugin-css-modules#287
- [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都没有
- 被全世界几百万项目依赖
这就好比因为苹果公司出了安全问题,就把超市里的苹果都下架了一样荒谬。
我为这个维护者感到心疼
看到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已经足够成熟稳定了。但这次事件让我意识到,我们可能过于依赖这个中心化的平台了。
想想看:
- 一个错误的安全判断,就能让全球项目停摆
- 维护者申诉无门,只能在社交媒体求助
- 没有任何预警机制,用户只能被动承受
这真的合理吗?
开源维护者太难了
@iChenLei的遭遇让我想起了很多开源维护者的心酸。他们用爱发电,服务全世界,但遇到问题时却如此无助。
我觉得我们作为受益者,应该:
- 多给开源项目捐赠
- 积极参与社区建设
- 在这种时候给维护者更多支持
而不是只会在出问题时抱怨。
前端生态的脆弱性
这次事件也暴露了现代前端开发的一个问题:我们的依赖树太复杂了。
一个简单的项目,动不动就有几百个依赖。每个依赖都是一个潜在的故障点。虽然这种模块化的开发方式很高效,但风险也确实不小。
我开始思考:
- 是不是应该减少一些不必要的依赖?
- 关键依赖是不是应该做备份?
- 公司是不是应该建立私有NPM镜像?
从left-pad到stylus,我们学到了什么?
2016年的left-pad事件,曾经让整个JavaScript生态停摆了一天。当时大家说要吸取教训,要建立更稳定的包管理机制。
现在2025年了,类似的事情又发生了,而且更严重。
这让我意识到,单纯依靠技术手段可能解决不了根本问题。我们需要的是:
- 更透明的治理机制:NPM的决策过程应该更开放
- 更快速的申诉渠道:不能让维护者只能在Twitter求助
- 更多元化的生态:不能把鸡蛋都放在一个篮子里
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 上了,网络不方便?爱谁谁!
写在最后
这次事件提醒我们,我们的工作比想象中更脆弱。但也让我看到了社区的力量:当官方渠道失效时,我们依然能够相互帮助,共度难关。PS:这就是为什么我爱这个行业的原因。
然而一个又产生一个新想法:一个小小的名称混淆,就能让全球的前端开发陷入混乱。那么,"软件正在吞噬世界,但谁来守护软件?"
相关链接
- 吃瓜地址: github.com/stylus/styl…
- MOCKM 文档: wll8.github.io/mockm/
来源:juejin.cn/post/7529903134296653839
Nginx 内置变量详解:从原理到实战案例
Nginx 内置变量是 Nginx 配置中极具灵活性的核心特性,它们能动态获取请求、连接、服务器等维度的实时数据,让配置从“固定模板”升级为“智能响应”。本文将系统梳理常用内置变量的分类、含义,并结合实战案例说明其应用场景,帮助你真正用好 Nginx 变量。
一、Nginx 内置变量的核心特性
在深入变量前,先明确两个关键特性:
- 动态性:变量值并非固定,而是在每次请求处理时实时生成(如
$remote_addr
会随客户端 IP 变化)。 - 作用域:变量仅在当前请求的处理周期内有效,不同请求的变量值相互独立。
- 命名规则:所有内置变量均以
$
开头,如$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 等) | GET 、POST |
$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_iso8601 | ISO 8601 标准时间(UTC 时间,无时区偏移) | 2024-08-22T03:20:00+00:00 |
三、内置变量实战案例
了解变量含义后,关键是知道“在什么场景用什么变量”。以下 5 个实战案例覆盖日志、鉴权、跳转、限流等高频场景。
案例 1:自定义访问日志(记录关键请求信息)
默认的 Nginx 访问日志仅包含基础信息,通过变量可自定义日志格式,记录如“客户端 IP、请求方法、参数、耗时”等关键数据,方便后续分析。
配置步骤:
- 在
nginx.conf
的http
块中定义日志格式:
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;
# 其他配置...
}
- 重载 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>";
}
}
测试效果:
- 合法请求:
http://192.168.230.130/admin?token=abc123
→ 返回 200 和 Admin 页面。 - 非法请求:
http://192.168.230.130/admin?token=wrong
→ 返回 403 和 “Forbidden”。
案例 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-Type
为 api
时,转发请求到后端 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;
}
}
测试效果:
- 发送 API 请求(带请求头):
curl -H "X-Request-Type: api" http://192.168.230.130/api/user
→ 请求被转发到
127.0.0.1:8080/api/user
。 - 普通访问:
http://192.168.230.130
→ 返回/usr/local/nginx/html/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>";
}
}
测试效果:
- 灰度用户(带 Cookie):
curl -b "version=beta" http://192.168.230.130/feature
→ 返回 “New Feature (Beta Version)”。
- 普通用户(无 Cookie):
curl http://192.168.230.130/feature
→ 返回 “Old Feature (Stable Version)”。
四、使用内置变量的注意事项
- 避免过度使用
if
指令:Nginx 的if
指令在某些场景下可能触发意外行为(如与try_files
冲突),复杂逻辑优先用map
指令或 Lua 脚本。 - 代理场景下的 IP 问题:若 Nginx 位于代理服务器后(如 CDN、负载均衡器),
$remote_addr
会变为代理 IP,需通过$http_x_forwarded_for
获取客户端真实 IP(需代理服务器传递该请求头)。 - 变量大小写敏感:
$arg_id
和$arg_ID
是两个不同的变量(前者对应?id=1
,后者对应?ID=1
),配置时需注意参数名大小写。 - 性能影响:内置变量本身性能开销极低,但频繁使用复杂正则匹配(如
if ($remote_addr ~* ...)
)可能增加 CPU 消耗,高并发场景需优化正则。
五、总结
Nginx 内置变量是连接“静态配置”与“动态请求”的桥梁,掌握它们能让你摆脱固定配置的束缚,实现更灵活的请求处理逻辑。本文梳理的 5 大类变量和实战案例,覆盖了日志、鉴权、跳转、代理、灰度等高频场景,建议结合实际需求动手测试——只有在实践中反复使用,才能真正理解变量的威力。
如果需要更复杂的场景(如结合 map
指令批量处理变量、Lua 脚本扩展变量功能),可以进一步深入学习 Nginx 高级配置技巧。
来源:juejin.cn/post/7543193023086575625
分支管理提交代码
前言:公司代码需要先放在开发分支上,后面提交的时候再合并到master主分支
开始复现场景:
从git branch -a
输出来看,你的本地分支是 develop
,而远程仓库中存在两个分支:origin/master
和 origin/develop
。此外,remotes/origin/HEAD
指向了 origin/master
,这表明远程仓库的默认分支是 master
。
问题分析
你提到想把分支合并到 master
主分支上去,但 git branch
没有显示 master
分支。根据输出信息,可以确认以下几点:
- 本地没有
master
分支:当前只有develop
分支。 - 远程存在
master
分支:remotes/origin/master
表明远程仓库中有master
分支。 - 远程默认分支是
master
:remotes/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 pull
和 git push
而不需要每次都指定分支,可以设置默认上游分支:
bash
深色版本
# 设置 master 分支的上游为 origin/master
git branch --set-upstream-to=origin/master master
这样,当你在 master
分支上时,可以直接使用 git pull
和 git push
命令,而不需要额外指定分支名。
报错了
远程仓库 master
分支包含了本地分支没有的更改。因此,Git 拒绝了你的推送请求,以防止丢失远程仓库中的工作。
错误原因
这个错误通常有以下几种可能的原因:
- 远程仓库有新的提交:在你上次拉取(
git pull
)之后,其他开发者可能已经向远程仓库的master
分支推送了新的提交。 - 非快进式推送:你的本地
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
还是报错
从你提供的错误信息来看,当前的状态是 MERGING
,这表明你在合并过程中遇到了问题,并且还没有完成合并。因此,Git 拒绝了你的推送请求。
错误原因
- 合并尚未完成:你正在进行一个合并操作(可能是之前执行的
git merge
或git pull
),但还没有解决所有的冲突或完成合并。 - 本地分支落后于远程分支:即使解决了合并冲突,本地分支可能仍然落后于远程分支,需要先拉取最新的更改。
解决方案
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,根据你的分支名
✅ 完成!远程仓库也会删除这些文件。
来源:juejin.cn/post/7549096640340492338
🔥 滚动监听写到手抽筋?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 |
无限滚动 | 长列表数据加载 | 避免频繁滚动监听 | 防止重复加载,错误处理 |
可见性统计 | 用户行为分析 | 精确统计,多阈值监听 | 数据存储和上报策略 |
动画触发 | 页面交互效果 | 时机精准,性能好 | 动画只触发一次的控制 |
虚拟滚动 | 大数据列表 | 内存占用低,滚动流畅 | 元素高度固定,复杂度较高 |
🎯 实战应用建议
最佳实践
- 合理设置rootMargin:根据实际需求提前或延迟触发观察
- 及时清理观察器:使用unobserve()和disconnect()释放资源
- 错误处理机制:为异步操作添加try-catch保护
- 性能监控:在开发环境中监控观察器的性能表现
- 渐进增强:为不支持IntersectionObserver的浏览器提供降级方案
性能考虑
- 观察器数量控制:避免创建过多观察器实例
- threshold设置:根据实际需求设置合适的阈值
- 内存泄漏防护:组件销毁时及时清理观察器
- 兼容性处理:使用polyfill支持旧版浏览器
💡 总结
这5个IntersectionObserver实用场景在日常开发中能显著提升页面性能,掌握它们能让你的滚动监听:
- 图片懒加载:告别手动位置计算,性能提升显著
- 无限滚动:避免频繁滚动监听,用户体验更佳
- 可见性统计:精准的用户行为分析,数据更准确
- 动画触发:完美的视觉效果时机控制
- 虚拟滚动:大数据列表的性能救星
希望这些技巧能帮助你在前端开发中告别滚动监听的性能焦虑,写出更丝滑的交互代码!
🔗 相关资源
- MDN IntersectionObserver文档
- Can I Use - IntersectionObserver兼容性
- IntersectionObserver Polyfill
- Web性能优化最佳实践
💡 今日收获:掌握了5个IntersectionObserver实用场景,这些知识点在实际开发中非常实用。
如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀
来源: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筛简历和在线面试逐渐普及的背景下,不少应届生直呼“努力仿佛被系统淹没”。
在这样的环境下,年轻人发现,光靠努力并不足以脱颖而出:
简历反复修改,却依然石沉大海;
面试准备充分,却临场紧张到大脑一片空白;
拿到多个Offer,却在选择上迟迟犹豫。
专家指出,当下年轻人最缺的,不是努力,而是 在关键节点有人给予即时点拨。例如临面前的快速演练、谈薪时的策略建议,或是Offer选择的利弊分析。
近年来,一些新兴职场服务平台开始探索“即时支持”模式。例如,近期上线的 「对圈职场」App,就尝试通过 15分钟即时咨询 + 陪跑辅导 + 职场学习社区 的组合,为年轻人提供低门槛、全场景的职场支持。
业内人士认为,这类探索,或许能在缓解就业焦虑、提升求职质量方面发挥积极作用。对圈职场的出现,也从侧面印证了一个趋势:年轻人需要的不是泛泛的培训,而是更即时、更贴近现实的职场帮助。
收起阅读 »索引失效的场景有哪些?
本文首发于公众号:托尼学长,立个写 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 增强和 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 的踩坑与突破,交付想法到产品的全过程。
来源:juejin.cn/post/7543912699638906907
你一定疑惑JavaScript中的this绑定的究竟是什么?😵💫
想要了解this的绑定过程,首先要理解调用方式。
调用方式
调用方式被描述为函数被触发执行时语法形式。
主要有以下几种基本模式:
- 直接调用(独立函数调用):
f1()
- 方法调用:
f1.f2()
- 构造函数调用:
new f1()
- 显示绑定调用:
f1.call(f2)
或者f1.apply(f2)
- 间接调用:
(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
会自动绑定到该对象上,这个绑定是隐式发生的,不需要显式使用call
、apply
或bind
。
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
,一个是obj2
,obj2
中的属性为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();
虽然函数fOne
是obj.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
的绑定。我们通过使用函数的apply
、call
或bind
方法,让我们可以自定义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
但用apply
、call
来进行显示绑定并不能避免隐性丢失的问题。下面有两个方法来解决这个问题。
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
,这个函数负责返回绑定好this
的f1
函数,并立即执行它。 这种绑定我们称之为硬绑定。
这种绑定方法会使用在一个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
来调用函数,会自动执行以下操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行原型连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么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
。
四条规则的优先级
- 如果某个调用位置应用了多条规则该怎么办?这时我们就需要知道它们的优先级了。 首先,默认绑定的优先级是最低的。我们先来测试一下它们隐式绑定和显式绑定哪个优先级高吧,这里我偷个懒,就引用一下《你不知道的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
分别绑定的为obj1
,obj2
,所以会打印2
,3
。接着我们调用了obj1.foo.call(obj2)
发现结果输出为obj2
中的a
属性2
,所以这里应用的是显式绑定。
所以显式绑定的优先级是高于隐式绑定的。
- 再来看看
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.a
和bar.a
谁输出为2
,谁的优先级就更高,很明显bar.a
输出为2
,所以new
绑定的优先级高于隐式绑定的。
所以new调用的优先级要高于隐式调用的优先级。
- 再来看看new调用和显式调用的优先级谁高谁低吧。
new
不能和apply
和call
方法同时使用,但我们可以用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属性,值为2
,bar
接收返回的bind
函数。之后new bar
并给a
赋值为3
,用baz
来接收new
的对象,这时如果baz.a
为3
就说明this
应用的绑定规则是new
绑定。
所以new绑定的优先级是高于显示调用的优先级的。
现在知道了四种规则,又知道了这四个规则的优先级,我们就能很清晰的判断this的绑定了。
判断this的流程
以后判断this
我们可以按以下顺序来判断:
- 函数是否在
new
中调用(new
绑定)?如果是的话this
绑定的是新创建的对象。var bar = new foo() //这里bar为this绑定的对象
- 函数是否通过
call
、apply
(显式绑定)或者硬绑定(bind
)调用?如果是的话,this绑定的是指定的对象。var bar = foo.call(obj) //这里obj为this绑定的对象
- 函数是否在某个上下文对象中调用(隐式绑定)如果是的话,
this
绑定的是那个上下文对象。var bar = obj.foo() //这里obj为this绑定的对象
- 如果都不是,则应用默认绑定,
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
现在来简单地来介绍一下柯里化是什么?柯里化是将一个接收多个参数的函数转换为一系列只接受单个参数的函数。这时bind
和null
的作用就体现出来了。
然而,在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
可以发现这段代码中,我们创建了自己的空对象通过apply
和bind
方法把this
绑定到这个空对象了。最后的输出的c
是0
,说明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
中有a
和f1
属性或方法,a
的值为1
;obj2
中只有a
属性,值为2
。我们先隐式绑定obj1
,this
绑定obj1
,最后输出为1
,这个我们可以理解。关键是下面这行代码(obj2.f1 = obj1.f1)()
,obj2
中没有f1
,所以它在obj2
中创建一个f1
,然后将obj1
中的f1
函数赋值给obj2
的f1
,然后执行这个赋值表达式。那为什么输出的是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
内部的箭头函数继承了其外部函数f1
的this
的绑定。所以一开始没给f1
绑定this
时,f1
的this
绑定的是全局对象,箭头函数的也是全局对象;当给f1
的this
绑定一个对象时,箭头函数的this
也绑定该对象。
小结
以上是我的学习分享,希望对你有所帮助。
还有本篇的四条规则只适用于非严格模式,严格模式的this
的绑定我日后再出一篇吧,其实只是有点懒😂。
参考书籍
《你所不知道的JavaScript(上卷)》
来源:juejin.cn/post/7504237094283526178
为什么我的第一个企业级MCP项目上线3天就被叫停?
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%"
关键成功因素:
- 高层支持:这个真的很重要,CEO亲自站台,资源要人给人要钱给钱
- 分阶段实施:我们从最积极的两个团队开始,让他们当种子用户,效果好了再推广
- 用户培训:别小看这个,我们光培训就搞了一个月,但确实值得
- 持续优化:每周都会收集用户反馈,有问题马上改,这个习惯一直保持到现在
案例二:大型金融机构(5000+人规模)
公司背景:
- 行业:银行业
- 规模:5000+名员工,严格合规要求
- 挑战:数据安全、合规审计、多地部署
实施方案:
- 架构选择:多活数据中心,容器化部署
- 核心功能:风险分析、客户服务、合规报告
- 安全措施:端到端加密、零信任架构
金融级安全要求:TLS 1.3传输加密、AES-256数据加密、HSM密钥管理、PCI-DSS/SOX合规、7年审计日志保留、本地化数据存储。
实施效果:
- 合规性:通过所有监管审计,零合规违规
- 效率提升:客户服务响应时间减少50%
- 成本节约:年度IT成本降低25%
- 风险控制:欺诈检测准确率提升40%
经验教训总结
通过这些案例,我们总结出企业级MCP实施的关键经验:
成功要素
- 明确的ROI目标:设定可量化的成功指标
- 充分的资源投入:人力、资金、时间的保障
- 渐进式实施:避免大爆炸式部署
- 用户参与:让最终用户深度参与设计和测试
常见陷阱
- 忽视安全合规:在设计初期就要考虑安全要求
- 低估培训成本:用户培训和支持需要充分投入
- 缺乏监控:没有完善的监控就无法及时发现问题
- 一步到位心态:试图一次性解决所有问题
💡 写在最后:从失败到成功的思考
回想起那次项目失败,我现在反而挺感谢那次经历。它让我明白了一个道理:企业级MCP集成绝不是技术的简单堆砌,而是一个涉及人、流程、技术的复杂系统工程。
如果重新来过,我会这样做:
- 先调研,再动手:花更多时间理解企业的真实需求,而不是想当然
- 小步快跑:从最简单的MVP开始,证明价值后再扩展
- 安全第一:把合规和安全放在功能之前考虑
- 拥抱变化:技术在发展,需求在变化,保持架构的灵活性
现在我参与的企业级MCP项目,成功率已经提升到90%以上。不是因为我的技术水平提高了多少,而是因为我学会了从企业的角度思考问题。
最好的架构不是最复杂的,而是最适合的。
如果你正在考虑为企业部署MCP平台,我的建议是:先找一个小团队试点,积累经验和信心,然后再考虑大规模推广。记住,每个企业都有自己的特色,别人的成功方案未必适合你。
🤔 互动时间
分享你的经验:
- 你的企业在AI工具集成方面遇到了什么挑战?
- 你觉得统一的AI工具平台对企业来说最大的价值是什么?
- 有没有类似的项目失败经历想要分享?
实践练习:
- 使用文章中的需求分析框架,评估你所在企业的MCP部署需求
- 基于你的行业特点,设计合适的安全控制措施
- 参考分阶段实施策略,制定适合你企业的部署计划
欢迎在评论区分享你的想法和经验,我会认真回复每一条评论。
📧 如果你正在规划企业级MCP项目,可以私信我,我很乐意分享更多实战经验和踩坑心得。
下期预告:《MCP最佳实践与性能优化》将深入探讨MCP使用过程中的优化技巧和故障排查方法,敬请期待!
关注专栏,获取更多MCP实战干货!
来源:juejin.cn/post/7532742298825768998
java中,使用map实现带过期时间的缓存
在 Java 开发领域,缓存机制的构建通常依赖于 Redis 等专业缓存数据库。这类解决方案虽能提供强大的缓存能力,但引入中间件意味着增加系统架构复杂度、部署成本与运维负担。本文将深入探讨一种轻量级替代方案 —— 基于 Java 原生Map实现的带过期时间的缓存机制。该方案无需引入外部工具,仅依托 Java 标准库即可快速搭建起缓存体系,特别适用于对资源占用敏感、架构追求极简的项目场景,为开发者提供了一种轻量高效的缓存数据管理新选择。
优点:
- 轻量便捷:无需引入 Redis 等外部中间件,直接使用 Java 标准库即可实现,降低了项目依赖,简化了部署流程。
- 快速搭建:基于熟悉的Map数据结构,开发人员能够快速理解和实现缓存逻辑,显著提升开发效率。
- 资源可控:可灵活控制缓存数据的生命周期,通过设置过期时间,精准管理内存占用,适合对资源占用敏感的场景。
缺点:该方案存在明显局限性,即数据无法持久化。一旦应用程序停止运行,缓存中的所有数据都会丢失。相较于 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
来源:juejin.cn/post/7496335321781829642
svg按钮渐变边框
共用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效果图
移入暂停、移出执行
<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效果图
总结
个人感觉svg实现渐变边框相比较css的实现来说,相对代码量更大一些,但是svg其实还有很多好玩的地方。
用svg来做渐变边框也是另外的一种思路,也许以后能够用的上。
来源:juejin.cn/post/7488575555048161332
Python之 sorted() 函数的基本语法
sorted() 函数的基本语法
sorted(iterable, key=, reverse=False)
- iterable: 要排序的可迭代对象(列表、元组、字符串等)
- key: 可选,指定排序的依据函数
- reverse: 可选,True 为降序,False 为升序(默认)
基本用法
简单排序
# 数字排序
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(numbers))
# 输出: [1, 1, 2, 3, 4, 5, 6, 9]
# 字符串排序
names = ['Alice', 'Bob', 'Charlie', 'David']
print(sorted(names))
# 输出: ['Alice', 'Bob', 'Charlie', 'David']
# 字符排序
chars = ['z', 'a', 'x', 'b']
print(sorted(chars))
# 输出: ['a', 'b', 'x', 'z']
降序排序
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(numbers, reverse=True)) # 输出: [9, 6, 5, 4, 3, 2, 1, 1]
示例解析
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
# 输出:
# BRA/CE342567
# ESP/XDA205856
# USA/31195855
解析: 对于元组列表,sorted() 默认按第一个元素排序,如果第一个元素相同,则按第二个元素排序,以此类推。
使用 key 参数进行自定义排序
按字符串长度排序
words = ['python', 'java', 'javascript', 'go', 'rust']
print(sorted(words, key=len))
# 输出: ['go', 'java', 'rust', 'python', 'javascript']
按照绝对值排序
numbers = [-5, -1, 0, 1, 3, -2]
print(sorted(numbers, key=abs))
# 输出: [0, -1, 1, -2, 3, -5]
按照元祖的特定循序排序
# 按第二个元素排序
students = [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
print(sorted(students, key=lambda x: x[1])) # 输出: [('Charlie', 78), ('Alice', 85), ('Bob', 92)]
# 按第一个元素的长度排序
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
print(sorted(traveler_ids, key=lambda x: len(x[0]))) # 输出: [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
复杂排序示例
多级排序
# 先按年龄排序,年龄相同时按姓名排序
people = [
('Alice', 25, 'Engineer'),
('Bob', 30, 'Doctor'),
('Charlie', 25, 'Teacher'),
('David', 30, 'Engineer')
]
# 按年龄升序,然后按姓名升序
sorted_people = sorted(people, key=lambda x: (x[1], x[0]))
print(sorted_people)
# 输出: [('Alice', 25, 'Engineer'), ('Charlie', 25, 'Teacher'), ('Bob', 30, 'Doctor'), ('David', 30, 'Engineer')]
字符串忽略大小写排序
names = ['alice', 'Bob', 'CHARLIE', 'david']
print(sorted(names))
# 输出: ['CHARLIE', 'Bob', 'alice', 'david'] (按ASCII码)
print(sorted(names, key=str.lower))
# 输出: ['alice', 'Bob', 'CHARLIE', 'david'] (忽略大小写)
自定义排序规则
def custom_sort_key(item):
"""自定义排序:数字优先,然后按字母顺序"""
if item.isdigit():
return (0, int(item)) # 数字类型,按数值排序
else:
return (1, item.lower()) # 字母类型,按字母顺序
data = ['z', '10', 'a', '2', 'B', '1']
print(sorted(data, key=custom_sort_key)) # 输出: ['1', '2', '10', 'a', 'B', 'z']
排序复杂数据结构
字典列表排序
students = [
{'name': 'Alice', 'grade': 85, 'age': 20},
{'name': 'Bob', 'grade': 92, 'age': 19},
{'name': 'Charlie', 'grade': 78, 'age': 21}
]
# 按成绩排序
print(sorted(students, key=lambda x: x['grade']))
# 按年龄排序
print(sorted(students, key=lambda x: x['age']))
# 按姓名排序
print(sorted(students, key=lambda x: x['name']))
使用 operator 模块
from operator import itemgetter, attrgetter
# 对字典列表排序
students = [
{'name': 'Alice', 'grade': 85},
{'name': 'Bob', 'grade': 92},
{'name': 'Charlie', 'grade': 78}
]
# 使用 itemgetter 替代 lambda
print(sorted(students, key=itemgetter('grade')))
# 等同于: sorted(students, key=lambda x: x['grade'])
# 多字段排序
print(sorted(students, key=itemgetter('grade', 'name')))
来源:juejin.cn/post/7522863398243549234
解决 Python requests.post() 返回 406 错误
解决 Python requests.post() 返回 406 错误
HTTP 406 "Not Acceptable" 错误表示服务器无法生成符合客户端请求中 Accept 头部指定的响应内容。
问题分析,最好的方法还是一段代码一段代码的调试
今天是模型一个登录的脚本,以前都用得好好的,现在突然不行了,出现了406的错误,觉得有点奇怪,代码如下:
header = {'Content-Type':'application/x-www-form-urlencoded',
'User-Agent':'Mozilla/5.0 (Macint0sh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
form_data = {
"actionFlag":"login",
"uid":"xxxxx",
"password":"xxxxx"
}
data = parse.urlencode(from_data)
response = request.post(url=login_url,header=header, data = data, verify=False)
以前上面这段代码是没有什么问题的,但最近才发现一直是406,跟踪到代码中的
sessions.py中adapter的值是对的,上面是显示访问成功而且返回值是200,这说明访问代码是没有问题的,但什么时候出的问题呢?继续往下
发现了这一段代码,其中allow_redirects的值就有点意思了,这个值的默认值是Ture,但我们登录后,后面肯定有一个redirect的,也就是说后面那个跳转的地址有问题了,所以才会导致出问题
所以只需要将这个参数设计为false就正常了
allow_redirects=False
下面是我解决问题时用deepseek查问题时,出现的一些总结,也不能说它全部不对,但也是由于这个让我有了一些思路,所以放上来,大家参考一下也是好了。
常见原因及解决方案
1. 请求头 (headers) 问题
确保你的请求头中包含正确的 Accept
和 Content-Type
:
python
headers = {
'Accept': 'application/json', # 或其他服务器期望的类型
'Content-Type': 'application/json', # 或其他适当的内容类型
# 其他必要的头部,如授权信息
}
2. 数据格式问题
确保发送的数据格式与 Content-Type
头部匹配:
python
import json
data = {'key': 'value'}
response = requests.post(url, data=json.dumps(data), headers=headers)
或者使用 json
参数自动处理:
python
response = requests.post(url, json=data, headers=headers)
3. 用户代理问题
有些服务器要求特定的 User-Agent:
python
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
# 其他头部
}
4. 认证问题
如果 API 需要认证,确保提供了正确的凭证:
python
headers = {
'Authorization': 'Bearer your_token_here',
# 其他头部
}
5. 服务器端限制
检查 API 文档,确认:
- 是否支持 POST 方法
- 是否接受你发送的内容类型
- 是否有其他特殊要求
完整示例
python
import requests
import json
url = 'https://api.example.com/endpoint'
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'MyApp/1.0',
'Authorization': 'Bearer abc123...'
}
data = {'key1': 'value1', 'key2': 'value2'}
try:
response = requests.post(url, json=data, headers=headers)
response.raise_for_status() # 如果响应状态码不是200,抛出异常
print(response.json())
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
如果问题仍然存在,建议:
- 检查 API 文档的具体要求
- 使用开发者工具查看浏览器发送的成功请求的格式
- 联系 API 提供商获取支持
来源:juejin.cn/post/7522002830929379368
Java String.replace()原理,你真的了解吗?
大家好呀,我是猿java。
String.replace()
是我们日常开发中经常用到的一个方法,那么,你有看过其底层的源码实现吗?你知道String.replace()
是如何工作的吗?String.replace()
的性能到底怎么样?这篇文章我们来深入地分析。
在开始今天的问题之前,让我们先来看一个问题:
String original = "Hello, World!";
// 替换字符
String result = original.replace('World', 'Java');
original.replace('World', 'Java')
,是把 original的内容直接修改成Hello, Java
了,还是重新生成了一个 Hello, Java
的 String并返回?
1. String.replace()
是什么?
String.replace()
位于java.lang
包中,它是 Java中的一个重要方法,用于替换字符串中的某些字符或子字符串。以下String.replace()
的源码截图。
String.replace()
方法用于替换字符串中的某些字符或子字符串。它有多个重载版本,常见的有:
// 用于替换单个字符
public String replace(char oldChar, char newChar);
// 用于替换子字符串
public String replace(CharSequence target, CharSequence replacement);
下面是一个简单的示例,演示了replace
方法的用法:
public class ReplaceExample {
public static void main(String[] args) {
String original = "Hello, World!";
// 替换字符
String replacedChar = original.replace('o', 'a');
System.out.println(replacedChar); // 输出: "Hella, Warld!"
// 替换子字符串
String replacedString = original.replace("World", "Java");
System.out.println(replacedString); // 输出: "Hello, Java!"
}
}
在上面的例子中,我们演示了如何使用replace
方法替换字符和子字符串。需要注意的是,String
对象在Java中是不可变的(immutable),因此replace
方法会返回一个新的字符串,而不会修改原有字符串。
2. 源码分析
上述示例,我们演示了replace
方法的用法,接下来,我们来分析下replace
方法的实现原理。
2.1 String的不可变性
Java中的String
类是不可变的,这意味着一旦创建了一个String
对象,其内容不能被改变。这样的设计有助于提高性能和安全性,尤其在多线程环境下。String源码说明如下:
2.2 replace()
工作原理
让我们深入了解replace
方法的内部实现。以replace(CharSequence target, CharSequence replacement)
为例,以下是其基本流程:
- 检查目标和替换内容:方法首先检查传入的
target
和replacement
是否为null
,如果是,则抛出NullPointerException
。 - 搜索目标子字符串:在原始字符串中查找所有符合目标子字符串的地方。
- 构建新的字符串:基于找到的位置,将原始字符串分割,并用替换字符串进行拼接,生成一个新的字符串。
2.3 源码解析
让我们看一下String
类中replace
方法的源码(简化版):
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
: StringUTF16.replace(value, oldChar, newChar);
if (ret != null) {
return ret;
}
}
return this;
}
public String replace(CharSequence target, CharSequence replacement) {
String tgtStr = target.toString();
String replStr = replacement.toString();
int j = indexOf(tgtStr);
if (j < 0) {
return this;
}
int tgtLen = tgtStr.length();
int tgtLen1 = Math.max(tgtLen, 1);
int thisLen = length();
int newLenHint = thisLen - tgtLen + replStr.length();
if (newLenHint < 0) {
throw new OutOfMemoryError();
}
StringBuilder sb = new StringBuilder(newLenHint);
int i = 0;
do {
sb.append(this, i, j).append(replStr);
i = j + tgtLen;
} while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
return sb.append(this, i, thisLen).toString();
}
解析步骤
- 参数校验:首先检查
target
和replacement
是否为null
,避免后续操作出现NullPointerException
。 - 查找目标字符串:使用
indexOf
方法查找目标子字符串首次出现的位置。如果未找到,直接返回原字符串。 - 替换逻辑:
- 使用
StringBuilder
来构建新的字符串,这是因为StringBuilder
在拼接字符串时效率更高。 - 通过循环查找所有目标子字符串的位置,并将其替换为替换字符串。
- 最后,拼接剩余的字符串部分,返回最终结果。
- 使用
性能考虑
由于String
的不可变性,每次修改都会创建新的String
对象。如果需要进行大量的字符串替换操作,推荐使用StringBuilder
或StringBuffer
来提高性能。
三、实际示例演示
接下来,我们将通过几个实际的例子,来更好地理解String.replace()
的使用场景和效果。
示例1:替换字符
public class ReplaceCharDemo {
public static void main(String[] args) {
String text = "banana";
String result = text.replace('a', 'o');
System.out.println(result); // 输出: "bonono"
}
}
解释:将所有的'a'
替换为'o'
,得到"bonono"
。
示例2:替换子字符串
public class ReplaceStringDemo {
public static void main(String[] args) {
String text = "I love Java. Java is versatile.";
String result = text.replace("Java", "Python");
System.out.println(result); // 输出: "I love Python. Python is versatile."
}
}
解释:将所有的"Java"
替换为"Python"
,结果如上所示。
示例3:替换多个不同的子字符串
有时,我们可能需要在一个字符串中替换多个不同的子字符串。例如,将文中的标点符号替换为空格:
public class ReplaceMultipleDemo {
public static void main(String[] args) {
String text = "Hello, World! Welcome to Java.";
String result = text.replace(",", " ")
.replace("!", " ")
.replace(".", " ");
System.out.println(result); // 输出: "Hello World Welcome to Java "
}
}
解释:通过链式调用replace
方法,依次将,
、!
和.
替换为空格。
示例4:替换不匹配的情况
public class ReplaceNoMatchDemo {
public static void main(String[] args) {
String text = "Hello, World!";
String result = text.replace("Python", "Java");
System.out.println(result); // 输出: "Hello, World!"
}
}
解释:由于"Python"
在原字符串中不存在,replace
方法不会做任何替换,直接返回原字符串。
四、String.replace()
的技术架构图
虽然文字描述已能帮助我们理解replace
方法的工作原理,但通过一个简化的技术架构图,可以更直观地抓住其核心流程。
+---------------------------+
| String对象 |
| "Hello, World!" |
+------------+--------------+
|
| 调用replace("World", "Java")
v
+---------------------------+
| 搜索目标子字符串 "World" |
+------------+--------------+
|
| 找到位置 7
v
+---------------------------+
| 构建新的字符串 "Hello, Java!" |
+---------------------------+
|
| 返回新字符串
v
+---------------------------+
| 新的 String对象 |
| "Hello, Java!" |
+---------------------------+
图解说明
- 调用
replace
方法:在原始String
对象上调用replace("World", "Java")
。 - 搜索目标:方法内部使用
indexOf
找到"World"
的位置。 - 构建新字符串:使用
StringBuilder
将"Hello, "
与"Java"
拼接,形成新的字符串"Hello, Java!"
。 - 返回新字符串:最终返回一个新的
String
对象,原始字符串保持不变。
五、总结
通过本文的介绍,相信你对Java中String.replace()
方法有了更深入的理解。从基本用法到内部原理,再到实际应用示例,每一步都帮助你全面掌握这个重要的方法。
记住,String
的不可变性设计虽然带来了安全性和线程安全性,但在频繁修改字符串时,可能影响性能。因此,合理选择使用String
还是StringBuilder
,根据具体场景优化代码,是每个Java开发者需要掌握的技能。
希望这篇文章能对你在Java编程的道路上提供帮助。如果有任何疑问或更多的讨论,欢迎在评论区留言!
8. 学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7543147533368229903
一万行代码实现的多维分析表格,让数据处理效率提升 300%
上个月在 趣谈AI 发布了我实现的多维表格1.0版本,没有用到任何第三方组件,完全组件化设计。最近对多维表格进行了进一步的升级优化,满打满算花了接近3个月时间,累计代码接近1w行。
接下来就和大家聊聊我做的 flowmix/mute多维表格 的核心功能和技术实现。
核心功能介绍
1. 多视图模式
目前多维表格支持多种视图模式:表格视图,看板视图,人员分配视图。用户可以轻松在不同视图下切换并进行可视化操作数据。
2. 多条件筛选功能
我们可以基于不同维度进行筛选和排序,并支持组合筛选。
3. 多维度分组功能
表格视图中,我们可以基于用户,优先级,状态,对数据进行分组管理,提高表格数据的查看效率。
4. 表格字段管理功能
多维表格中不仅支持字段的管理控制,同时还支持添加自定义字段:
5. 表格行列支持自定义拖拽排序功能
表格我们不仅仅支持列的宽度拖拽,还支持拖拽调整列的排序,同时表格的行也支持拖拽,可以跨分组进行拖拽,也支持在组内进行拖拽排序,极大的提高了数据管理的效率。
6. 表格支持一键编辑
我们可以在菜单按钮中开启编辑模式,也可以双击编辑单元格一键编辑表格内容,同时大家还可以进行扩展。
7. 表格支持一键转换为可视化分析视图表
我们可以将表格数据转换为可视化分析图表,帮助管理者更好地掌握数据动向。
8. 表格支持一键导入任务数据
目前多维表格支持导出和导入json数据,并一键渲染为多维表格。技术实现多维表格的设计我采用了组件化的实现的方式, 并支持数据持久化,具体使用如下:
<div className="flex-1 bg-gray-50">
{currentView === "tasks" && <TaskManagementTable sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />}
{currentView === "statistics" && <StatisticsView />}
{currentView === "documentation" && <DocumentationView />}
{currentView === "assignment" && <AssignmentView />}
{currentView === "deployment" && <DeploymentView />}
</div>
在开发多维表格的过程中其实需要考虑很多复杂逻辑,比如表格用什么方式渲染,如何优化表格性能,如何实现表格的列排序,行排序,表格编辑等。传统表格组件大多基于div
模拟行列,虽然灵活但渲染性能差。所以可以做如下优化:
- 虚拟滚动当数据量超过 500 行时,启用虚拟滚动机制,仅渲染可见区域的 DOM 节点,内存占用降低 70%;
- 行列冻结通过固定定位
position: sticky
实现表头和固定列冻结,解决大数据表格的滚动迷失问题; - 异步加载采用
Intersection Observer
监听表格滚动事件,动态加载可视区域外的数据,避免一次性请求全量数据。
接下来分享一下简版的虚拟滚动的实现方案:
// 虚拟滚动核心代码(简化版)
function renderVirtualTable(data, visibleHeight) {
const totalRows = data.length;
const rowHeight = 40; // 行高固定
const visibleRows = Math.ceil(visibleHeight / rowHeight);
const startIndex = scrollTop / rowHeight | 0;
const endIndex = startIndex + visibleRows;
// 渲染可见区域数据
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const row = document.createElement('tr');
row.innerHTML = data[i].cells.map(cell => `<td>${cell.value}</td>`).join('');
fragment.appendChild(row);
}
// 更新滚动条高度和偏移量
table.scrollHeight = totalRows * rowHeight;
table.innerHTML = `<thead>${header}</thead><tbody>${fragment}</tbody>`;
}
对于大表格数据量需要在本地缓存,所以需要设计表格数据的缓存处理逻辑,目前我采用的是hooks的实现方案,具体实现如下:
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
其实在实现多维表格的过程中,我也调研了很多开源的方案,但是对于扩展性,灵活度和功能复杂度上,都略显简单,所以我才考虑花时间来实现这款多维表格方案。另一个比较复杂的逻辑是表格的列拖拽和排序,我们需要对可展开折叠的表格支持排序和拖拽,并保持优秀的用户体验:
技术实现如下:
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
多维表格还支持多种视图的转换,比如可以将表格视图一键转换为可视化分析图表:
对用户和团队进行多维度的数据分析。技术实现如下:
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from "recharts"
import type { Task } from "@/lib/types"
interface PriorityDistributionChartProps {
tasks: Task[]
}
export function PriorityDistributionChart({ tasks }: PriorityDistributionChartProps) {
// 计算每个优先级的任务数量
const priorityCounts: Record<string, number> = {}
tasks.forEach((task) => {
const priority = task.priority || "未设置"
priorityCounts[priority] = (priorityCounts[priority] || 0) + 1
})
// 转换为图表数据格式
const chartData = Object.entries(priorityCounts).map(([priority, count]) => ({
priority,
count,
}))
// 为不同优先级设置不同颜色
const COLORS = ["#FF8042", "#FFBB28", "#00C49F", "#0088FE"]
return (
<div className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={true}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="priority"
label={({ priority, percent }) => `${priority}: ${(percent * 100).toFixed(0)}%`}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value, name, props) => [`${value} 个任务`, props.payload.priority]} />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
)
}
项目的体验地址:mute.turntip.cn
如果大家有好的想法,欢迎评论区留言反馈~
来源:juejin.cn/post/7511649092658577448
使用watchtower更新docker容器
更新方式
定时更新(默认)
执行以下命令后,Watchtower 会在后台每 24 小时自动检查并更新所有运行中的容器:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower
手动立即更新
添加 --run-once
参数启动临时容器,检查更新后自动退出,适合按需触发:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower --run-once
更新指定容器
在命令末尾添加需要监控的容器名称,多个容器用空格分隔。例如仅监控 nginx
和 redis
容器:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower nginx redis
简化命令
手动更新时,如果使用上面的命令未免太麻烦了,所以我们可以将更新命令设置为别名:
将下面的命令放到对应shell的环境文件中(比如bash
对应~/.bashrc
,zsh对应~/.zshrc
)
alias update-container="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once"
编辑完环境文件后,重新打开命令窗口,或使用source ~/.bashrc
或source ~/.zshrc
加载命令。
然后就可以通过下面的方式更新容器了:
update-container 容器标识
比如:
update-container nginx-ui-latest
来源:juejin.cn/post/7541682368329170954
用了bun之后,是不是就不用vite了
用了 Bun 之后,并不是完全不用 Vite。Bun 虽然内置了打包器和运行时,且速度非常快,但其打包功能目前还不够完善,缺少对前端项目非常重要的功能,比如对代码分块(chunk splitting)的精细控制和开发服务器(dev server)支持,这些都是 Vite 在前端开发中非常关键的优势
具体来说:
- Bun 是一个高性能的 JavaScript 运行时和包管理器,集成了打包器和测试工具,适合全栈开发和快速安装依赖,但其前端打包功能还处于实验阶段,缺少灵活的代码拆分和热更新支持
- Vite 专注于前端开发体验,提供极快的冷启动和热模块替换(HMR),以及灵活且成熟的生产构建流程,它的生态和插件系统也非常丰富,适合现代前端框架(如 Vue3、React)
- 目前推荐的做法是用 Bun 替代 Node.js 作为运行时和包管理器,用 Vite 负责前端的开发服务器和构建打包。这样可以结合 Bun 的高速安装和执行优势,以及 Vite 的快速开发反馈和优化生产构建的能力
关于 pnpm 和 Vite 的叠加:
- pnpm 是一个高效且节省磁盘空间的包管理器,完全可以和 Vite 一起使用。pnpm 通过内容寻址存储和符号链接机制,避免依赖重复,提升安装速度和项目管理效率,特别适合 monorepo 和多项目共享依赖的场景
- pnpm 和 Vite 的结合非常常见,pnpm 负责依赖管理,Vite 负责开发和构建,两者互不冲突,反而能互补提升开发效率
总结:
工具 | 作用定位 | 是否可以替代 Vite? | 是否能和 pnpm 叠加? | 主要好处 |
---|---|---|---|---|
Bun | JS 运行时+包管理+打包+测试 | 目前不能完全替代 Vite | 可以,pnpm 和 Bun 都是包管理器,选择一个即可 | 速度极快,集成度高,适合全栈开发 |
Vite | 前端开发服务器+构建工具 | 不能被 Bun 完全替代 | 完全兼容 pnpm | 快速热更新,灵活构建,生态丰富 |
pnpm | 高效包管理器 | 和 Vite 不冲突,负责依赖管理 | 本身是包管理器,不和 Vite 冲突 | 节省磁盘空间,安装快,依赖隔离 |
因此,用了 Bun 后,建议继续用 Vite 负责前端开发和构建,pnpm 依然可以用来管理依赖,三者结合能发挥各自优势,提升开发体验和性能
如果你想快速上手,可以用 Bun 创建 Vite 项目:
bun create vite my-app
cd my-app
bun install
bun run dev
这样就用 Bun 作为运行时和包管理器,用 Vite 作为开发服务器和构建工具
综上,Bun 目前还不能完全替代 Vite,Vite 在前端开发体验和构建优化上仍有不可替代的优势;pnpm 作为包管理器可以和 Vite 叠加使用,带来依赖管理的高效和节省空间的好处。三者结合是现代前端项目的一个高效组合方案。
用了next.js之后,是不是就不用vite了
用了 Next.js 之后,一般情况下是不需要再单独使用 Vite 了,因为 Next.js 内部已经集成了自己的构建和开发服务器功能。
具体原因和分析如下:
- Next.js 是一个基于 React 的全栈框架,内置了开发服务器(通过
next dev
命令启动)、构建工具和打包器,它自动处理代码编译、热模块替换(HMR)、代码拆分和优化等工作,开发体验和构建性能都非常优秀 - Next.js 使用了基于 Rust 的编译器 SWC 来加速构建和转译,性能上已经非常接近甚至优于传统的打包工具
- Vite 主要是一个前端构建工具和开发服务器,适合纯前端项目或需要快速启动的 React/Vue 应用,而 Next.js 是一个框架,包含了路由、服务器端渲染(SSR)、静态站点生成(SSG)、API 路由等功能,功能层面更全面
- 使用 Next.js 时,所有的构建和开发流程都由 Next.js 自己管理,开发者无需额外配置 Vite,避免了工具链的重复和冲突。
- 如果你只是做纯前端项目,Vite 是很好的选择;但如果你需要 SSR、API 路由、全栈功能,Next.js 是更合适的框架,且内置了构建和开发支持,不需要再叠加 Vite。
总结:
工具 | 作用定位 | 是否需要同时使用 | 适用场景 |
---|---|---|---|
Next.js | React 全栈框架,含开发服务器和构建 | 不需要 | SSR、SSG、API 路由、全栈应用 |
Vite | 前端开发服务器和构建工具 | 纯前端项目时使用 | 快速启动、热更新、纯前端 SPA |
因此,用了 Next.js 后,基本上不需要再用 Vite 了,Next.js 已经集成了类似 Vite 的开发和构建功能,且提供了更多全栈特性
来源:juejin.cn/post/7522080312564285486
记录App切后台时AppIcon变成默认雪花icon问题
xcode做新项目时,设置了app图标。发现点击app进入前台时,App Icon是正常的,但是回到桌面时App Icon又变成了默认的雪花图标。
之前也遇到过,但是不求甚解,在此列出解决方案。
问题1: AppIcon的设置
随便设置了个图片为app图标,编译报错xxx/Assets.xcassets: The stickers icon set or app icon set named "AppIcon" did not have any applicable content.
同时appIcon可视化窗口显示黄色⚠️图标。
Xcode 提示你在 Assets.xcassets
中名为 "AppIcon"
的 App 图标集合里没有提供任何有效的图片资源。
iOS 应用要求必须有完整的 AppIcon 集合,并且要包含适用于各种设备和分辨率的图标尺寸。如果没有正确设置这些图标,App 就无法通过 App Store 审核,甚至可能在某些模拟器或真机上运行异常。
我使用了makeappicon.com/ 生成appIcon图标。
网站生成的结果包含AppIcon.appiconset,直接把AppIcon.appiconset替换原项目中Assets中的appIcon即可。
结果如下
问题2: 切后台appIcon变成默认雪花icon
现在成功设置appIcon后,切后台时发现appIcon变成了默认的雪花icon。
原因是系统缓存了旧图标,iOS 系统有时会缓存应用的图标缩略图,尤其是多任务界面中的预览图。即使你更新了图标,也可能不会立即刷新。
解决办法: 卸载重装
现在能正常显示了
来源:juejin.cn/post/7520461940273184831
鸿蒙next 游戏授权登录教程王者归来
前沿导读
各位同学很久没有分享技术文章给大家了,因为最近需要兼职讲课,所以我比较忙。都也没有多少个人时间,所以也是趁着现在有空我们就分享下
效果图
调用效果
日志打印
需求背景
工作中最近接到需求,需要接入鸿蒙的游戏授权登录和内购支付,今天把流程走完整,所以现在就做一个分享
开发步骤
初始化
这里如果不是在 EntryAbility 接入初始化代码需要做调整
// 在EntryAbility 里面初始化
try {
gamePlayer.init(this.context,()=>{
hilog.info(0x0000, 'testTag', `Succeeded in initing.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to init. Code: ${err.code}, message: ${err.message}`);
}
// 不在EntryAbility 里面执行初始化
try {
gamePlayer.init(context as common.UIAbilityContext ,()=>{
hilog.info(0x0000, 'testTag', `Succeeded in initing.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to init. Code: ${err.code}, message: ${err.message}`);
}
获取 gamePlayerId
let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.UnionLoginParam = {
showLoginDialog: false, // 是否弹出联合登录面板。true表示强制弹出面板,false表示优先使用玩家上一次的登录选择,不弹出联合登录面板,若玩家首次登录或卸载重装,则正常弹出。
thirdAccountInfos: [] // 若游戏无官包或无官方账号体系,请传空数组。
};
try {
gamePlayer.unionLogin(context, request).then((result: gamePlayer.UnionLoginResult) => {
hilog.info(0x0000, 'testTag', `Succeeded in logining: ${result?.accountName}`);
console.log("gamePlayerId accountName --- >" +result.accountName)
console.log("gamePlayerId thirdOpenId --- >" +result.boundPlayerInfo.thirdOpenId)
console.log("gamePlayerId bindType --- >" +result.boundPlayerInfo.bindType)
let localPlayer=result.localPlayer;
if(localPlayer.gamePlayerId){
console.log("index gamePlayerId localPlayer gamePlayerId --- >" +localPlayer.gamePlayerId)
}
if(localPlayer.teamPlayerId){
console.log("index gamePlayerId localPlayer teamPlayerId --- >" +localPlayer.teamPlayerId)
}
}).catch((error: BusinessError) => {
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${error.code}, message: ${error.message}`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}
获取authorizationCode
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
loginRequest.state = util.generateRandomUUID();
// 执行认证请求
try {
let controller = new authentication.AuthenticationController(getContext(this));
controller.executeRequest(loginRequest, (err, data) => {
if (err) {
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
return;
}
let loginWithHuaweiIDResponse = data as authentication.LoginWithHuaweiIDResponse;
let state = loginWithHuaweiIDResponse.state;
console.log("index authorizationCode state ---> "+state)
if (state != undefined && loginRequest.state != state) {
hilog.error(0x0000, 'testTag', `Failed to login. State is different.`);
return;
}
hilog.info(0x0000, 'testTag', `Succeeded in logining.`);
let loginWithHuaweiIDCredential = loginWithHuaweiIDResponse.data!;
let authorizationCode = loginWithHuaweiIDCredential.authorizationCode;
console.log("index authorizationCode ---> "+authorizationCode)
// 开发者处理authorizationCode
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}
我们拿到了 gamePlayerId 和 authorizationCode 去请求服务端去获取
服务端流程
调用on接口注册playerChanged事件监听
aboutToAppear(): void {
// 调用on接口注册playerChanged事件监听
try {
gamePlayer.on('playerChanged', this.onPlayerChangedEventCallback);
hilog.info(0x0000, 'testTag', `Succeeded in registering.`);
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to register. Code: ${err.code}, message: ${err.message}`);
}
}
监听事件回调
private onPlayerChangedEventCallback(result: gamePlayer.PlayerChangedResult) {
if (result.event === gamePlayer.PlayerChangedEvent.SWITCH_GAME_ACCOUNT) {
// ...
// 游戏号已切换,完成本地缓存清理工作后,再次调用unionLogin接口等
}
}
提交玩家角色信息
let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.GSKPlayerRole = {
roleId: '123', // 玩家角色ID,如游戏没有角色系统,请传入“0”,务必不要传""和null。
roleName: 'Jason', // 玩家角色名,如游戏没有角色系统,请传入“default”,务必不要传""和null。
serverId: '456',
serverName: 'Zhangshan',
gamePlayerId: '789', // 请根据实际获取到的gamePlayerId传值。
thirdOpenId: '123' // 接入华为账号登录时不传该字段。接入游戏官方账号登录时,请根据实际获取到的thirdOpenId传值。
};
try {
gamePlayer.savePlayerRole(context, request).then(() => {
hilog.info(0x0000, 'testTag', `Succeeded in saving.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to save. Code: ${err.code}, message: ${err.message}`);
}
参数配置
我们需要在 添加如此下配置
"metadata": [
// 配置如下信息
{
"name": "client_id",
"value": "xxxxxxxxx"
// 华为Client ID 请替换成你自己的正式参数
},
{
"name": "app_id",
"value": "6917581951060909508"
// 华为APP ID 请替换成你自己的正式参数
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
],
配置手动签名测试
查看日志
写在最后
整个鸿蒙游戏授权登录相对比较简单,但是有一个槽点,就是获取 gamePlayerId 和 authorizationCode需要分开两个方法回调 。其实可以做成一个回调更简单,这个希望后期能完善, 服务端逻辑对比客户端来说,还是相对复杂一点点,不过对着文档也是很快能解决这次接入 华为技术支持也帮了不少忙,这个点赞, 对于鸿蒙生态的推广这块,华为确实下了决心,也非常积极的回应。希望鸿蒙越来越好,国产系统早日完善。我依然是你们最爱的徐老师。我们下一期再见。
来源:juejin.cn/post/7543421087759433738
H5 配合原生开发 App
JS 和 Android
- 原生调用 JS
4.4 版本之前
// mWebView = new WebView(this); //当前webview对象
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
//也可以在UI线程中运行
runOnUiThread(new Runnable() {
@Override
public void run() {
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
// 安卓中原生的弹框
Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
}
});
4.4 版本之后
// 通过异步的方式执行js代码,并获取返回值
mWebView.evaluateJavascript("javascript: 方法名('参数1,参数2...')", new ValueCallback() {
@Override
// 这个方法会在执行完毕之后触发, 其中value就是js代码执行的返回值(如果有的话)
public void onReceiveValue(String value) {
}
});
- JS 调用Android
安卓配置:
// Android4.2版本以上,本地方法要加上注解@JavascriptInterface,否则无法使用
private Object getJSBridge(){
// 实例化新对象
Object insertObj = new Object(){
@JavascriptInterface
// 对象内部的方法1
public String foo(){
// 返回 字符串 foo
return "foo";
}
@JavascriptInterface
// 对象内部的方法2 需要接收一个参数
public String foo2(final String param){
// 返回字符串foo2拼接上传入的param
return "foo2:" + param;
}
};
// 返回实例化的对象
return insertObj;
}
// 获取webView的设置对象,方便后续修改
WebSettings webSettings = mWebView.getSettings();
// 设置Android允许JS脚本,必须要!!!
webSettings.setJavaScriptEnabled(true);
// 暴露一个叫做JSBridge的对象到webView的全局环境
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
在 web 页面中调用
//调用方法一
window.JSBridge.foo(); //返回:'foo'
//调用方法二
window.JSBridge.foo2('test');//返回:'foo2:test'
- 原生调用 JS
4.4 版本之前
// mWebView = new WebView(this); //当前webview对象
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
//也可以在UI线程中运行
runOnUiThread(new Runnable() {
@Override
public void run() {
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
// 安卓中原生的弹框
Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
}
});
4.4 版本之后
// 通过异步的方式执行js代码,并获取返回值
mWebView.evaluateJavascript("javascript: 方法名('参数1,参数2...')", new ValueCallback() {
@Override
// 这个方法会在执行完毕之后触发, 其中value就是js代码执行的返回值(如果有的话)
public void onReceiveValue(String value) {
}
});
- JS 调用Android
安卓配置:
// Android4.2版本以上,本地方法要加上注解@JavascriptInterface,否则无法使用
private Object getJSBridge(){
// 实例化新对象
Object insertObj = new Object(){
@JavascriptInterface
// 对象内部的方法1
public String foo(){
// 返回 字符串 foo
return "foo";
}
@JavascriptInterface
// 对象内部的方法2 需要接收一个参数
public String foo2(final String param){
// 返回字符串foo2拼接上传入的param
return "foo2:" + param;
}
};
// 返回实例化的对象
return insertObj;
}
// 获取webView的设置对象,方便后续修改
WebSettings webSettings = mWebView.getSettings();
// 设置Android允许JS脚本,必须要!!!
webSettings.setJavaScriptEnabled(true);
// 暴露一个叫做JSBridge的对象到webView的全局环境
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
在 web 页面中调用
//调用方法一
window.JSBridge.foo(); //返回:'foo'
//调用方法二
window.JSBridge.foo2('test');//返回:'foo2:test'
JS 和 IOS
- 原生调用 JS
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
// 加载完毕会触发(类似于Vue的生命周期钩子)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 类似于console.log()
print("触发啦");
// wkWebView调用js代码,其中doSomething()会被当做js解析
webView.evaluateJavaScript("doSomething()");
}
}
- JS 调用 IOS
- JS 部分
window.webkit.messageHandlers.方法名.postMessage(数据)
- iOS 部分注册监听
wkWebView.configuration.userContentController.add(self, name: 方法名)
- iOS 部分遵守协议相关方法
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// message.body 就是传递过来的数据
print("传来的数据为", message.body)
}
- 原生调用 JS
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
// 加载完毕会触发(类似于Vue的生命周期钩子)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 类似于console.log()
print("触发啦");
// wkWebView调用js代码,其中doSomething()会被当做js解析
webView.evaluateJavaScript("doSomething()");
}
}
- JS 调用 IOS
- JS 部分
window.webkit.messageHandlers.方法名.postMessage(数据)
- iOS 部分注册监听
wkWebView.configuration.userContentController.add(self, name: 方法名)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// message.body 就是传递过来的数据
print("传来的数据为", message.body)
}
url scheme(互通协议)
web 调用
class="ios" type="button" value="使用iframe加载url">
// 加载url 通过iframe 设置URL 目的是让ios拦截
function loadUrl(url) {
// 创建iframe
const iframe = document.createElement('iframe');
// 设置url
iframe.src = url;
// 设置尺寸(不希望他被看到)
iframe.style.height = 0;
iframe.style.width = 0;
// 添加到页面上
document.body.appendChild(iframe);
// 加载了url之后他就没用了
// 移除iframe
iframe.parentNode.removeChild(iframe);
}
document.querySelector('.ios').onclick = function () {
loadUrl('taobao://click');
}
IOS 监听
// 拦截url
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// 获取url
let url = navigationAction.request.url?.absoluteString;
if(url=="taobao://click"){
print("调用系统功能");
decisionHandler(.cancel);
}else{
decisionHandler(.allow);
}
}
HyBridApp
- 开发框架
- 提供前端运行环境
- 实现前端和原生交互
- 封装原生功能,提供插件机制
加载优化
- 骨架屏
<style>
.shell .placeholder-block{
display: block;
height: 5em;
background: #ccc;
margin: 1em;
}
.novel {
height: 5em;
background-color: yellowgreen;
}
style>
head>
<body>
<div class="shell">
<div class="placeholder-block">div>
div>
body>
html>
<script>
setTimeout(()=>{
// 移除 占位dom元素
document.querySelector('.shell').innerHTML = ''
// 创建数据的dom元素 添加到页面上
let p = document.createElement('p')
p.innerHTML = '黑马程序员'
p.className = 'novel'
document.querySelector('.shell').appendChild(p)
},3000)
script>
<style>
.shell .placeholder-block{
display: block;
height: 5em;
background: #ccc;
margin: 1em;
}
.novel {
height: 5em;
background-color: yellowgreen;
}
style>
head>
<body>
<div class="shell">
<div class="placeholder-block">div>
div>
body>
html>
<script>
setTimeout(()=>{
// 移除 占位dom元素
document.querySelector('.shell').innerHTML = ''
// 创建数据的dom元素 添加到页面上
let p = document.createElement('p')
p.innerHTML = '黑马程序员'
p.className = 'novel'
document.querySelector('.shell').appendChild(p)
},3000)
script>
webview
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 创建webView
var webView = WKWebView(frame: self.view.bounds)
// 设置自己为WebView的代理
webView.navigationDelegate = self
// 添加到页面上
self.view.addSubview(webView)
// 创建URL对象
var url = URL(string: "https://www.baidu.com")
// 创建URLRequest对象
var request = URLRequest(url: url!)
// 加载URL
webView.load(request)
}
}
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 创建webView
var webView = WKWebView(frame: self.view.bounds)
// 设置自己为WebView的代理
webView.navigationDelegate = self
// 添加到页面上
self.view.addSubview(webView)
// 创建URL对象
var url = URL(string: "https://www.baidu.com")
// 创建URLRequest对象
var request = URLRequest(url: url!)
// 加载URL
webView.load(request)
}
}
JSBridge
- 设计思想
- JS 向原生发送消息
- 原生向 JS 发送消息
window.JSBridge = {
invoke: function(action, params, callback) {
// 生成唯一回调ID
const callbackId = 'cb_' + Date.now();
// 存储回调函数
window[callbackId] = callback;
// 构建标准化消息
const msg = {
action: action,
params: params || {},
callbackId: callbackId
};
// 根据平台调用不同原生桥
if (isIOS()) {
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify(msg));
} else if (isAndroid()) {
window.android.postMessage(JSON.stringify(msg));
}
},
// 原生调用此方法来回调结果
receiveMessage: function(msg) {
const { callbackId, result, error } = msg;
const callback = window[callbackId];
if (callback) {
if (error) {
callback(null, error); // 错误回调
} else {
callback(result, null); // 成功回调
}
// 执行后删除回调,避免内存泄漏
delete window[callbackId];
}
}
};
// 使用示例:调用原生相机
JSBridge.invoke('takePhoto', { quality: 'high' }, (result, error) => {
if (error) {
console.error('拍照失败:', error);
} else {
console.log('照片路径:', result.imagePath);
}
});
解释:
- 前端调用
JSBridge.invoke
时:存储回调函数,生成唯一的callbackId
(如cb_1725000000000
),确保每个回调能被唯一识别;把回调函数挂载到window
对象上(即window[callbackId] = 回调函数
),相当于 “暂时存档”,避免函数被垃圾回收。 - 前端向原生发送 “带回调 ID 的消息”,然后根据平台(iOS/Android)把消息发给原生,此时原生收到的是 “操作指令 + 回调 ID”
- 原生执行操作(如调用相机),原生接收到消息后,解析出
action
和params
,执行对应的原生逻辑
- iOS:调用
UIImagePickerController
(系统相机接口),按quality: 'high'
配置拍照质量; - Android:调用
Camera
或CameraX
接口,同样按参数执行拍照。 这个阶段完全在原生环境(Objective-C/Swift 或 Java/Kotlin)中运行,与前端 JS 无关。
- 原生将 “结果 + 回调 ID” 回传给前端
原生执行完操作后(无论成功 / 失败),会构建一个 “结果消息”,包含:callbackId: 'cb_1725000000000'
(必须和前端传过来的一致,才能找到对应的回调);result: { imagePath: '/var/mobile/.../photo.jpg' }
(成功时的结果,如照片路径); 或error: '用户取消拍照'
(失败时的错误信息)。
然后原生会主动调用前端 JSBridge 预留的 receiveMessage
方法,把 “结果消息” 传回去。
- 前端
receiveMessage
执行回调函数
- 解析原生传过来的消息,提取
callbackId
、result
、error
; - 通过
callbackId
找到之前挂载在window
上的回调函数(即window['cb_1725000000000']
); - 执行回调函数:
- 成功:调用
callback(result, null)
(如打印照片路径); - 失败:调用
callback(null, error)
(如打印 “用户取消拍照”);
- 成功:调用
- 执行完后删除
window[callbackId]
,避免内存泄漏。
到这一步,回调函数才真正在前端 JS 环境中执行,完成整个跨端通信闭环。
来源:juejin.cn/post/7544077353371222067
JavaScript 数组扁平化全解析
JavaScript 数组扁平化全解析:从基础到进阶,深入理解 flat
与多种实现方式
在现代前端开发中,数组操作是日常编码中最常见的任务之一。而在处理复杂数据结构时,我们经常会遇到“嵌套数组”(即高维数组)的场景。例如,后端返回的数据结构可能是多层嵌套的,我们需要将其“拍平”为一维数组以便于渲染或进一步处理。这种将多层嵌套数组转换为单层数组的过程,就被称为 数组扁平化(Array Flattening)。
本文将带你全面了解 JavaScript 中数组扁平化的各种方法,包括原生 API 的使用、递归实现、reduce
高阶函数应用、利用 toString
和 split
的巧妙技巧,以及基于展开运算符的循环优化方案。我们将深入剖析每种方法的原理、优缺点和适用场景,帮助你构建完整的知识体系。
一、什么是数组扁平化?
数组扁平化,顾名思义,就是把一个嵌套多层的数组“压平”成一个只有一层的一维数组。例如:
const nestedArr = [1, [2, 3, [4, 5]], 6];
// 扁平化后应得到:
// [1, 2, 3, 4, 5, 6]
这个问题看似简单,但在实际项目中非常常见。比如你在处理树形菜单、评论回复结构、文件目录层级等数据时,都可能需要对嵌套数组进行扁平化处理。
二、使用原生 flat()
方法(推荐方式)
ES2019 引入了 Array.prototype.flat()
方法,使得数组扁平化变得极其简单和直观。
✅ 基本语法
arr.flat([depth])
depth
:指定要展开的层数,默认为1
。- 如果传入
Infinity
,则无论嵌套多少层,都会被完全展开。
✅ 示例代码
const arr = [1, [2, 3, [1]]];
console.log(arr.flat()); // [1, 2, 3, [1]] → 只展开一层
console.log(arr.flat(2)); // [1, 2, 3, 1] → 展开两层
console.log(arr.flat(Infinity)); // [1, 2, 3, 1] → 完全展开
✅ 特点总结
- 简洁高效:一行代码解决问题。
- 兼容性良好:现代浏览器基本都支持(IE 不支持)。
- 可控制深度:灵活控制展开层级。
- 推荐用于生产环境:清晰、安全、性能好。
⚠️ 注意:
flat()
不会改变原数组,而是返回一个新的扁平化数组。
三、递归实现:最经典的思路
如果你不能使用 flat()
(比如兼容老版本浏览器),或者想深入理解其内部机制,那么递归是一个经典且直观的解决方案。
✅ 基础递归版本
function flatten(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); // 递归处理子数组
} else {
res.push(arr[i]); // 非数组元素直接加入结果
}
}
return res;
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 分析
- 使用
for
循环遍历每个元素。 - 判断是否为数组:是 → 递归调用;否 → 直接推入结果数组。
- 利用
concat
合并递归结果。
✅ 缺点
- 每次
concat
都会创建新数组,性能略低。 - 递归深度过大可能导致栈溢出(极端情况)。
四、使用 reduce
+ 递归:函数式编程风格
利用 reduce
可以写出更优雅、更具函数式风格的扁平化函数。
✅ 实现方式
function flatten(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
✅ 解析
reduce
接收一个累加器pre
和当前元素cur
。- 如果
cur
是数组,则递归调用flatten(cur)
,否则直接使用cur
。 - 使用
concat
将结果合并到pre
中。
✅ 优点
- 代码简洁,逻辑清晰。
- 更符合函数式编程思想。
- 易于组合其他操作(如 map、filter)。
五、利用 toString()
+ split()
的“黑科技”技巧
这是一个非常巧妙但需要谨慎使用的技巧,适用于数组中只包含数字或字符串基本类型的情况。
✅ 实现原理
JavaScript 中,数组的 toString()
方法会递归地将每个元素转为字符串,并用逗号连接。
const arr = [1, [2, 3, [1]]];
console.log(arr.toString()); // "1,2,3,1"
我们可以利用这一点,先转成字符串,再用 split(',')
分割,最后通过 +item
转回数字。
✅ 实现代码
function flatten(arr) {
return arr.toString().split(',').map(item => +item);
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 优点
- 代码极短,实现“一行扁平化”。
- 性能较好(底层由引擎优化)。
✅ 缺点(⚠️ 重要)
- 仅适用于纯数字数组:如果数组中有字符串
"hello"
,+"hello"
会变成NaN
。 - 无法保留原始类型:所有元素都会被转为数字。
- 丢失
null
、undefined
、对象等复杂类型信息。
❗ 所以这个方法虽然巧妙,但不适合通用场景,仅作为面试中的“奇技淫巧”了解即可。
六、使用 while
循环 + concat
+ 展开运算符(性能优化版)
这种方法避免了递归调用,采用循环逐步“拍平”数组,适合处理深层嵌套且希望避免栈溢出的场景。
✅ 实现方式
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
✅ 原理解析
arr.some(item => Array.isArray(item))
:检查数组中是否还存在嵌套数组。...arr
:展开数组的所有元素。[].concat(...arr)
:concat
会对展开后的数组元素自动“拍平一层”。
🔍 举个例子:
[].concat(...[1, [2, 3, [1]]])
// 等价于
[].concat(1, [2, 3, [1]])
// → [1, 2, 3, [1]] → 拍平了一层
然后继续循环,直到没有嵌套为止。
✅ 优点
- 非递归,避免栈溢出。
- 逻辑清晰,易于理解。
- 性能较好,尤其适合中等深度嵌套。
✅ 缺点
- 每次
concat(...arr)
都会创建新数组,内存开销较大。 - 对于极深嵌套,仍可能影响性能。
七、对比总结:各种方法的适用场景
方法 | 优点 | 缺点 | 推荐场景 |
---|---|---|---|
arr.flat(Infinity) | 简洁、标准、安全 | IE 不支持 | ✅ 生产环境首选 |
递归 + for | 逻辑清晰,易理解 | 性能一般,可能栈溢出 | 学习理解原理 |
reduce + 递归 | 函数式风格,优雅 | 同上 | 偏好函数式编程 |
toString + split | 代码短,性能好 | 类型受限,不通用 | 面试奇技淫巧 |
while + concat + ... | 非递归,避免栈溢出 | 内存占用高 | 深层嵌套处理 |
八、扩展思考:如何实现深度可控的扁平化?
有时候我们并不想完全拍平,而是只想展开指定层数。可以仿照 flat(depth)
实现一个通用函数:
function flattenDepth(arr, depth = 1) {
if (depth === 0) return arr.slice(); // 深度为0,直接返回副本
let result = [];
for (let item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flattenDepth(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
// 测试
const arr = [1, [2, 3, [4, 5, [6]]]];
console.log(flattenDepth(arr, 1)); // [1, 2, 3, [4, 5, [6]]]
console.log(flattenDepth(arr, 2)); // [1, 2, 3, 4, 5, [6]]
console.log(flattenDepth(arr, Infinity)); // [1, 2, 3, 4, 5, 6]
九、结语
📌 小贴士:如果你的项目需要兼容老旧浏览器,可以使用 Babel 转译 flat()
,或手动引入 polyfill:
// Polyfill for Array.prototype.flat
if (!Array.prototype.flat) {
Array.prototype.flat = function(depth = 1) {
return this.reduce((acc, val) =>
Array.isArray(val) && depth > 0
? acc.concat(val.flat(depth - 1))
: acc.concat(val)
, []);
};
}
这样就能在任何环境中愉快地使用 flat()
了!
来源:juejin.cn/post/7543941409930625087
某些场景下CSS替代JS(现代CSS的深度实践指南)
某些场景下CSS替代JS(现代CSS的深度实践指南)
🧩 前端渲染核心机制解析
水合错误(Hydration Mismatch)深度解析
graph TD
A[客户端渲染CSR] --> B[服务端渲染SSR]
B --> C{水合过程 Hydration}
C -->|成功| D[交互式页面]
C -->|失败| E[水合错误]
E --> F[布局错乱]
E --> G[交互失效]
E --> H[控制台报错]
水合错误的本质:
在SSR框架(如Next.js)中,服务端生成的静态HTML与客户端React组件的初始状态不一致,导致React在"注水"过程中无法正确匹配DOM结构。
典型场景:
// Next.js组件 - 服务端渲染时获取时间
export default function Page({ serverTime }) {
// 问题点:客户端初始化时间与服务端不同
const [clientTime] = useState(Date.now());
return (
<div>
<p>服务端时间: {serverTime}</p>
<p>客户端时间: {clientTime}</p>
</div>
);
}
export async function getServerSideProps() {
return {
props: {
serverTime: Date.now() // 服务端生成时间戳
},
};
}
根本原因分析:
- 时序差异:服务端/客户端执行环境时间差
- 数据异步:客户端数据获取滞后于渲染
- DOM操作:客户端手动修改服务端生成的DOM
- 组件状态:useState初始值与SSR输出不匹配
现代CSS的解决之道
<!-- 纯CSS时间显示方案 -->
<div class="time-container">
<time datetime="2023-11-15T08:00:00Z">08:00</time>
<span class="live-indicator"></span>
</div>
<style>
.live-indicator::after {
content: "实时";
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
</style>
优势对比:
方案 | 水合风险 | 首屏时间 | 复杂度 | 可访问性 |
---|---|---|---|---|
React水合 | 高 | 慢 | 高 | 中等 |
纯CSS | 无 | 快 | 低 | 优 |
渐进增强 | 低 | 中等 | 中等 | 优 |
🛠️ CSS核心解决方案详解
1️⃣ 嵌套选择器:组件化样式管理
/* 卡片组件 - 替代React组件 */
.card {
padding: 1.5rem;
border: 1px solid #e0e0e0;
/* 标题区域 */
&-header {
display: flex;
align-items: center;
&:hover {
background: #f5f5f5;
}
}
/* 响应式处理 */
@media (width <= 768px) {
border-radius: 0;
padding: 1rem;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
border-color: #444;
}
}
工程价值:
- 作用域隔离:避免全局样式污染
- 维护成本:修改单个组件不影响其他部分
- 开发效率:类似JSX的组件化开发体验
2️⃣ CSS变量 + 相对颜色:动态主题系统
:root {
--primary: #2468f2;
--text-primary: #333;
/* 动态派生变量 */
--primary-hover: hsl(from var(--primary) h s calc(l + 8%));
--primary-active: oklch(from var(--primary) l c h / 0.9);
}
/* 主题切换器 */
.theme-switcher:has(#dark:checked) {
--text-primary: #fff;
--bg-primary: #121212;
}
button {
background: var(--primary);
transition: background 0.3s;
&:hover {
background: var(--primary-hover);
}
&:active {
background: var(--primary-active);
}
}
3️⃣ @starting-style:元素入场动画
.modal {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.4s ease-out,
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
/* 初始状态 */
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
与传统方案对比:
// React实现模态框动画 - 需要状态管理
function Modal() {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={`modal ${isOpen ? 'open' : ''}`}
onTransitionEnd={() => console.log('动画结束')}
>
{/* 内容 */}
</div>
)
}
/* 对应CSS */
.modal {
opacity: 0;
transform: translateY(20px);
transition: all 0.4s;
}
.modal.open {
opacity: 1;
transform: translateY(0);
}
📱 响应式设计新范式
动态视口单位实战
/* 移动端布局方案 */
.header {
height: 15svh; /* 最小可视高度 */
}
.hero {
height: 75lvh; /* 最大可视高度 */
}
.content {
height: 120dvh; /* 动态高度 */
overflow-y: auto;
}
.footer {
height: 10svh; /* 保证始终可见 */
}
单位解析:
单位 | 计算基准 | 适用场景 | iOS Safari支持 |
---|---|---|---|
svh | 最小可视区域高度 | 固定导航栏 | 16.4+ |
lvh | 最大可视区域高度 | 全屏轮播图 | 16.4+ |
dvh | 当前可视区域高度 | 可滚动内容区 | 16.4+ |
✅ 实践总结
水合错误规避策略
- 数据一致性:
// Next.js getStaticProps保证数据一致
export async function getStaticProps() {
const data = await fetchData();
return { props: { data } };
}
- 组件设计原则:
// 避免客户端特有状态
function SafeComponent({ serverData }) {
// ✅ 使用服务端传递的数据
return <div>{serverData}</div>;
}
- 渐进增强方案:
<!-- 首屏使用静态HTML -->
<div id="user-profile">
<!-- SSR生成内容 -->
</div>
<!-- 客户端增强 -->
<script type="module">
if (navigator.onLine) {
loadInteractiveComponents();
}
</script>
CSS优先架构优势
指标 | JS方案 | CSS方案 | 提升幅度 |
---|---|---|---|
首屏加载 | 2.8s | 0.6s | 78% |
交互延迟 | 120ms | 16ms | 87% |
内存占用 | 85MB | 12MB | 86% |
代码体积 | 350KB (gzip) | 45KB (gzip) | 87% |
实施路线图:
- 静态内容:优先使用HTML/CSS
- 交互元素:
:hover
,:focus-within
等伪类 - 复杂逻辑:渐进增强添加JS
- 状态管理:URL参数 +
:target
选择器
通过现代CSS技术栈,开发者可在避免水合错误的同时,构建高性能、可访问性强的Web应用,实现真正的"渐进式Web体验"。
来源:juejin.cn/post/7544366602885873679
Docker 与 containerd 的架构差异
要深入理解 Docker 与 containerd 的架构差异,首先需要明确二者的定位:Docker 是一套完整的容器平台(含构建、运行、分发等全流程工具),而 containerd 是一个专注于容器生命周期管理的底层运行时(最初是 Docker 内置组件,后独立为 CNCF 项目)。二者的架构设计围绕 “功能边界” 和 “模块化程度” 展开,以下从核心定位、架构分层、关键组件、交互流程四个维度进行对比分析。
一、核心定位与设计目标
架构差异的根源在于二者的定位不同,直接决定了功能范围和模块划分:
维度 | Docker | containerd |
---|---|---|
核心定位 | 一站式容器平台(Build, Ship, Run) | 轻量级容器运行时(专注于容器生命周期管理:启动、停止、销毁、资源隔离) |
设计目标 | 简化开发者体验,提供全流程工具链;兼顾单机开发与简单集群场景 | 满足云原生环境的可扩展性、稳定性;支持多上层调度器(K8s、Swarm 等) |
功能范围 | 包含镜像构建(docker build)、镜像仓库(docker push/pull)、容器运行、网络 / 存储管理、UI 等 | 仅负责镜像拉取、容器运行时管理、底层存储 / 网络对接;无镜像构建功能 |
依赖关系 | 早期内置 containerd 作为底层运行时(Docker 1.11+),2020 年后逐步拆分 | 可独立运行,也可作为 Docker、K8s(默认运行时)、Nomad 等的底层依赖 |
二、架构分层对比
二者均遵循 “分层解耦” 思想,但分层粒度和模块职责差异显著。Docker 架构更 “重”(含上层业务逻辑),containerd 更 “轻”(聚焦底层核心能力)。
1. Docker 架构(2020 年后拆分版)
Docker 经历了从 “单体架构” 到 “模块化拆分” 的演进(核心是将 containerd 独立,自身聚焦上层工具链),当前架构分为 4 层,自下而上分别是:
架构层 | 核心组件 / 功能 | 职责说明 |
---|---|---|
1. 底层运行时层 | containerd、runc | 承接 Docker daemon 的指令,负责容器的实际创建、启动、资源隔离(依赖 runc 作为 OCI runtime) |
2. Docker 守护进程层 | dockerd | Docker 的核心守护进程,负责接收客户端(docker CLI)请求,协调下层组件(如调用 containerd 管理容器,调用 buildkit 构建镜像) |
3. 工具链层 | BuildKit、Docker Registry Client、Docker Network/Volume Plugins | - BuildKit:替代传统 docker build 后端,优化镜像构建效率;- 镜像客户端:处理 docker push/pull 与仓库交互;- 网络 / 存储插件:管理容器网络(如 bridge、overlay)和数据卷 |
4. 客户端层 | docker CLI(命令行工具)、Docker Desktop UI(桌面端) | 提供用户交互入口,将 docker run/build/pull 等命令转化为 HTTP 请求发送给 dockerd |
2. containerd 架构(CNCF 标准化版)
containerd 架构更聚焦 “容器生命周期”,采用 5 层模块化设计,每层职责单一,可独立扩展,自下而上分别是:
架构层 | 核心组件 / 功能 | 职责说明 |
---|---|---|
1. OCI 运行时层 | runc、crun(可选) | 遵循 OCI 规范(Open Container Initiative),负责创建 Linux 容器(如调用 clone() 系统调用实现 PID 隔离,挂载 cgroup 限制资源) |
2. 容器执行层 | containerd-shim(垫片进程) | - 解耦 containerd 与容器进程:即使 containerd 重启,容器也不会退出;- 收集容器日志、监控容器状态、转发信号(如 docker stop 对应 SIGTERM) |
3. 核心服务层 | containerd 守护进程(containerd) | containerd 的核心,通过 gRPC 提供 API 服务,包含 4 个核心模块:- Namespaces:实现多租户资源隔离;- Images:管理镜像(拉取、存储、解压);- Containers:管理容器元数据(配置、状态);- Tasks:管理容器进程(启动、停止、销毁) |
4. 元数据存储层 | BoltDB(嵌入式 key-value 数据库) | 存储容器、镜像、命名空间等元数据,无需依赖外部数据库(如 MySQL),轻量且高效 |
5. 上层适配层 | CRI 插件(containerd-cri)、Docker API 兼容层 | - CRI 插件:将 containerd 的 gRPC API 转化为 K8s 要求的 CRI(Container Runtime Interface),使其成为 K8s 默认运行时;- Docker API 兼容层:支持部分 Docker 命令,确保与老系统兼容 |
三、关键组件差异
架构的核心差异体现在 “组件职责划分” 和 “功能依赖” 上,以下是最关键的组件对比:
组件 / 能力 | Docker | containerd |
---|---|---|
核心守护进程 | dockerd(上层协调)+ containerd(底层运行时,需与 dockerd 配合) | containerd(独立守护进程,直接对接 OCI 运行时,无需依赖其他进程) |
镜像构建 | 内置 BuildKit(或传统后端),支持 docker build 命令 | 无镜像构建功能,需依赖外部工具(如 BuildKit、img) |
容器进程隔离 | dockerd → containerd → containerd-shim → runc → 容器进程(4 层调用) | containerd → containerd-shim → runc → 容器进程(3 层调用,更轻量) |
元数据存储 | 依赖本地文件系统(/var/lib/docker)+ 部分内存缓存 | 内置 BoltDB(/var/lib/containerd),元数据管理更统一、高效 |
API 接口 | 主要提供 HTTP API(供 docker CLI 调用),对下层暴露有限 | 以 gRPC API 为主(更适合跨进程通信),提供细粒度接口(如镜像、容器、任务分别有独立 API) |
上层调度器支持 | 主要支持 Docker Swarm,对接 K8s 需额外配置(早期需 cri-dockerd 插件) | 原生支持 K8s(通过 containerd-cri 插件),也支持 Swarm、Nomad 等 |
四、容器启动流程对比
通过 “容器启动” 这一核心场景,可以直观看到二者的架构交互差异:
1. Docker 启动容器的流程(以 docker run ubuntu 为例)
- 用户交互:用户在终端执行 docker run ubuntu,docker CLI 将命令转化为 HTTP 请求,发送给本地的 dockerd 守护进程。
- dockerd 协调:
- 检查本地是否有 ubuntu 镜像:若无,调用 “镜像客户端” 从 Docker Hub 拉取镜像;
- 拉取完成后,dockerd 向 containerd 发送 gRPC 请求,要求创建并启动容器。
- containerd 处理:
- containerd 接收请求后,创建容器元数据(存储到本地),并启动 containerd-shim 垫片进程;
- containerd-shim 调用 runc,由 runc 遵循 OCI 规范创建容器进程(分配 PID、挂载 cgroup、设置网络 / 存储)。
- 状态反馈:
- containerd-shim 实时收集容器状态(如运行中、退出),反馈给 containerd;
- containerd 将状态转发给 dockerd,最终由 docker CLI 输出给用户(如 docker ps 显示容器列表)。
2. containerd 启动容器的流程(以 ctr run ubuntu my-container 为例,ctr 是 containerd 自带 CLI)
- 用户交互:用户执行 ctr run ubuntu my-container,ctr 直接通过 gRPC 调用 containerd 守护进程。
- containerd 核心处理:
- 检查本地镜像:若无,直接调用内置的 “镜像模块” 从仓库拉取 ubuntu 镜像;
- 创建容器元数据(存储到 BoltDB),并启动 containerd-shim 垫片进程。
- OCI 运行时启动容器:
- containerd-shim 调用 runc 创建容器进程,完成资源隔离和环境初始化;
- 容器启动后,containerd-shim 持续监控容器状态,直接反馈给 containerd。
- 状态反馈:containerd 将容器状态通过 gRPC 返回给 ctr,用户终端显示启动结果。
五、总结:架构差异的核心影响
对比维度 | Docker | containerd |
---|---|---|
轻量级 | 重(含全流程工具,依赖多组件) | 轻(仅核心运行时,组件少、资源占用低) |
扩展性 | 弱(架构耦合度较高,难适配多调度器) | 强(模块化设计,原生支持 K8s 等调度器) |
性能 | 略低(多一层 dockerd 转发,资源消耗多) | 更高(直接对接 OCI 运行时,调用链短) |
使用场景 | 单机开发、测试、小型应用部署 | 云原生集群(如 K8s 集群)、大规模容器管理 |
学习成本 | 低(CLI 友好,文档丰富) | 高(需理解 gRPC、OCI 规范,适合运维 / 底层开发) |
简言之:Docker 是 “面向开发者的容器平台”,架构围绕 “易用性” 和 “全流程” 设计;containerd 是 “面向云原生的底层运行时”,架构围绕 “轻量、可扩展、高兼容” 设计。在当前云原生生态中,containerd 已成为 K8s 的默认运行时,而 Docker 更多用于单机开发场景。
来源:juejin.cn/post/7544381073698848811
instanceof 的小秘密
instanceof
运算符用于检测某个构造函数的 prototype
属性,是否存在于对象的原型链上。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const cat = new Cat("Mittens", 3);
console.log(cat instanceof Cat); // true
console.log(cat instanceof Object); // true
instanceof
接收两个参数,v
(左)和target
(右),判断v
是否为target
的实例对象,方法是先查询target
的Symbol.hasInstance
属性,如果不存在,则判断target
的prototype
属性是否存在v
的原型中。
class Cat {
static [Symbol.hasInstance](instance) {
return false
}
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const cat = new Cat("Mittens", 3);
console.log(cat instanceof Cat); // false
console.log(cat instanceof Object); // true
console.log(cat instanceof null) // TypeError: Right-hand side of 'instanceof' is not an object
或许有人会想到1 intanceof Number
和1 intanceof Object
为什么会是false呢?明明(1).__proto__
是有值的,为什么呢?这里就不得不提到JS的一个机制"自动装箱"
。
我们定义一个变量const n = 1
, n
是一个原始值,有以下特点:
- 不可变性:原始值本身不能被修改,任何"修改"操作都会创建新值
- 按值传递:赋值时复制值,而不是引用
- 没有属性和方法:原始值本身不是对象,不能直接拥有属性和方法
在访问原始值属性或者方法时,Js会创建一个临时对象,使用后便会销毁。
const n = 1;
n.toString()
// JavaScript 内部实际执行的过程
// 1. 创建临时 Number 对象:new Number(1)
// 2. 调用方法:numberObj.toString()
// 3. 返回结果:"1"
// 4. 销毁临时对象
但是在intanceof
操作时,不会进行"自动装箱"
,所以得到的结果为false
。
来源:juejin.cn/post/7543797314282373162
一个有趣的效果--动态生成动画导航
一个有趣的效果--动态生成动画导航
在接下来的这个项目中,我们即将使用纯 JavaScript 和 CSS 来创建一个具有动态动画效果的导航栏。这篇文章将详细解析该代码的实现,包括 HTML 结构、CSS 样式、JavaScript 逻辑等方面,帮助你理解每一个步骤和实现思路。文章内容将逐步拆解,涵盖从页面结构、样式设计到功能实现的各个细节。
项目概述
这个项目的核心目标是创建一个包含动画效果的导航栏。具体功能包括:
- 动态导航项:当用户将鼠标悬停在导航项上时,显示一个附加的面板。
- 面板动画:面板会根据鼠标悬停的位置进行平滑过渡,显示不同的内容。
- 过渡效果:每个导航项的高亮状态和面板显示都有精美的动画效果,增强用户体验。
HTML 结构
HTML 基本框架
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一个动态动画导航</title>
<style>
/* 样式在这里 */
</style>
</head>
<body>
<script>
/* JavaScript 逻辑在这里 */
</script>
</body>
</html>
HTML 文档是非常标准的结构,包含了 head
和 body
两大部分:
<head>
部分:定义了页面的字符编码、视口设置和页面的标题。标题为 “一个动态动画导航”,用于描述页面内容。<body>
部分:里面没有直接的 HTML 内容,而是通过 JavaScript 动态生成和管理导航栏的结构。
导航栏元素
在页面的 body
中,我们没有直接放置导航栏的 HTML 代码,而是通过 JavaScript 动态生成。接下来我们将深入分析这些 JavaScript 代码的工作原理。
CSS 样式解析
全局样式
body, html, ul, p {
margin: 0;
padding: 0;
}
这一段代码是用来移除 body
、html
、ul
和 p
元素的默认 margin 和 padding,以确保布局没有多余的间隙。这是前端开发中的常见做法,有助于在不同浏览器中获得一致的效果。
导航栏 .nav
.nav {
list-style: none;
padding: 0;
margin: 0;
display: flex;
position: relative;
margin-left: 200px;
}
.nav
是一个容器元素,负责展示导航栏中的各个导航项。它使用了 flex
布局,使得每个 li
元素可以水平排列。此外,通过 position: relative
来为可能添加的子元素(如下拉面板)提供定位上下文,margin-left: 200px
是为了给导航栏留出空间。
导航项 .nav li
.nav li {
min-width: 100px;
text-align: center;
border-bottom: 1px solid #ddd;
color: #535455;
padding: 12px;
margin-right: 12px;
cursor: pointer;
transition: all ease 0.2s;
}
每个导航项 (li
) 有如下样式:
min-width: 100px
:确保每个项至少占据 100px 宽度。text-align: center
:使文本居中显示。border-bottom: 1px solid #ddd
:为每个导航项添加一个细线,增强视觉效果。padding: 12px
和margin-right: 12px
:设置内外边距,使项之间保持一定的间距。cursor: pointer
:当鼠标悬停在导航项上时,显示为可点击的手形光标。transition: all ease 0.2s
:使所有样式变化(如颜色、背景色、缩放等)具有过渡效果,持续时间为 0.2 秒,效果为平滑过渡。
面板 .nav-panel-wrapper
.nav-panel-wrapper {
border: 1px solid #dedede;
position: absolute;
top: 60px;
left: 0;
padding: 12px;
border-radius: 4px;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.32);
display: none;
overflow: hidden;
}
.nav-panel-wrapper
是每个导航项的下拉面板,包含以下样式:
position: absolute
:使面板相对于.nav
容器进行绝对定位。top: 60px
:将面板放置在导航项下方(假设导航栏的高度为 60px)。border-radius: 4px
:为面板添加圆角,使其看起来更加圆滑。box-shadow
:为面板添加阴影效果,使其更加立体,增加视觉层次感。display: none
:面板默认是隐藏的,只有在用户悬停时才会显示。overflow: hidden
:确保面板内容不会溢出其容器。
动画样式
.scale-up-top {
animation: scale-up-top 0.2s cubic-bezier(0.39, 0.575, 0.565, 1) both;
}
@keyframes scale-up-top {
0% {
transform: scale(0.5);
transform-origin: 50% 0%;
}
100% {
transform: scale(1);
transform-origin: 50% 0%;
}
}
.scale-up-top
类通过动画效果使面板从小到大逐渐放大,并且设置了动画的持续时间为 0.2 秒,使用了 cubic-bezier
函数来创建缓动效果。@keyframes scale-up-top
定义了放大过程的具体动画帧:从 50% 的缩放大小(即最小状态)逐渐过渡到 100%(即原始大小)。
JavaScript 逻辑解析
工具类 AnimateNavUtils
AnimateNavUtils
是一个工具类,提供了一些常用的方法,简化了 DOM 操作的代码:
$
:根据选择器返回文档中的第一个匹配元素。createElement
:根据传入的 HTML 字符串创建一个新的 DOM 元素。addClass
、removeClass
、hasClass
:分别用于为元素添加、移除、检查 CSS 类。insertNode
:将一个新的节点插入到指定的元素中,或者替换现有节点。create
:创建一个新的 DOM 元素节点。setStyle
:为元素动态设置样式。
这些工具方法大大简化了后续类的实现,使得代码更具可读性和复用性。
动画导航类 AnimateNav
AnimateNav
类是核心部分,负责处理导航栏的渲染、事件绑定和面板的动画效果。
构造函数
constructor({ data }) {
super();
this.data = data;
this.panelDelayTimer = null;
this.currentIndex = 0;
this.panelEle = null;
this.navEle = null;
}
在构造函数中,我们接收一个 data
参数,它是一个包含导航项信息的数组。panelDelayTimer
用来控制面板的显示延迟,currentIndex
用来记录当前导航项的索引,panelEle
和 navEle
分别存储面板和导航栏的 DOM 元素引用。
mount
方法
mount(el) {
const container = this.isString(el) ? this.$(el) : document.body;
this.render(container);
}
mount
方法负责将导航栏挂载到指定的 DOM 元素中。如果传入的参数是一个字符串(例如选择器),则查找对应的元素;如果是其他类型,则默认为 document.body
。
render
方法
render(container) {
if (!this.isArray(this.data) || this.data?.length === 0) {
return;
}
const node = this.createElement(`
<ul class="nav">
${this.data.map(item => `<li data-sub="${item.sub}" data-index="${item.index}" class="nav-item">${item.text}</li>`).join('')}
<div class="nav-panel-wrapper"> </div>
</ul>
`);
...
}
render
方法负责生成导航栏的 HTML 结构并将其插入到页面中。它首先检查 data
是否有效,确保它是一个数组且非空。接着,它动态创建一个包含 <ul class="nav">
和 <div class="nav-panel-wrapper">
的 HTML 结构。
data.map(item => ...)
生成每个导航项的<li>
元素,并根据data-sub
和data-index
设置相应的自定义属性。this.navEle
和this.panelEle
分别存储了导航栏容器和面板容器的 DOM 元素引用,方便后续操作。- 最后,调用
bindEvents
方法来绑定事件处理器。
绑定事件 bindEvents
bindEvents() {
const items = Array.from(this.navEle.querySelectorAll('.nav-item'));
items.forEach(item => {
item.addEventListener('mouseenter', (e) => {
const index = e.target.dataset.index;
this.showPanel(index);
});
item.addEventListener('mouseleave', () => {
this.hidePanel();
});
});
}
showPanel(index) {
const item = this.navEle.querySelector(`[data-index="${index}"]`);
const subItems = item.getAttribute('data-sub');
this.panelEle.innerHTML = subItems ? subItems : '没有子项';
this.addClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, {
display: 'block',
top: `${item.offsetTop + item.offsetHeight + 12}px`
});
}
hidePanel() {
this.removeClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, { display: 'none' });
}
在 bindEvents
方法中,我们为每个导航项添加了 mouseenter
和 mouseleave
事件监听器:
mouseenter
:当鼠标进入某个导航项时,调用showPanel
方法显示对应的面板,并填充子项内容。mouseleave
:当鼠标离开导航项时,调用hidePanel
隐藏面板。
showPanel
方法
showPanel(index) {
const item = this.navEle.querySelector(`[data-index="${index}"]`);
const subItems = item.getAttribute('data-sub');
this.panelEle.innerHTML = subItems ? subItems : '没有子项';
this.addClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, {
display: 'block',
top: `${item.offsetTop + item.offsetHeight + 12}px`
});
}
showPanel
方法根据导航项的索引 (data-index
) 显示相应的子项。如果该项有子项(存储在 data-sub
属性中),则将这些子项填充到面板中。如果没有子项,则显示默认的消息('没有子项')。然后,通过 scale-up-top
动画类使面板执行放大动画,并将面板的显示位置设为导航项的下方。
hidePanel
方法
hidePanel() {
this.removeClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, { display: 'none' });
}
hidePanel
方法用于隐藏面板。它会移除面板的动画类 scale-up-top
,并通过 setStyle
将面板的 display
属性设置为 none
,使其消失。
总结
动画和交互效果
- 悬停时显示面板:当用户将鼠标悬停在导航项上时,会触发面板的显示,面板内容来自
data-sub
属性。 - 平滑动画:面板在显示和隐藏时应用了平滑的缩放动画,使得界面显得更加动态和流畅。
- 动态子项内容:通过自定义的
data-sub
属性,每个导航项可以动态地包含不同的子项或其他内容。
来看一个在线示例如下所示:
当然这个导航还有可以优化和扩展的空间,如下:
优化和扩展
- 响应式设计:当前代码没有完全考虑到移动端的布局,可以进一步优化以适应不同设备屏幕的大小。
- 面板延迟:目前面板的显示和隐藏没有延迟处理,未来可以根据需要加入延迟显示、隐藏的效果,提升交互体验。
- 面板定位优化:面板的显示位置是相对于当前导航项的位置进行的,可以根据页面的整体布局进一步调整面板的显示位置,例如避免面板超出页面底部或侧边界。
整体来说,这个动态导航效果是通过结合 JavaScript 的 DOM 操作和 CSS 动画来实现的,结构清晰,动画流畅,能够为用户提供良好的互动体验。
来源:juejin.cn/post/7442965793157136420
我让AI一把撸了个算命网站,结果它比我还懂玄学
作为一个信奉代码逻辑的程序员,我做梦也没想到有一天会让AI帮我开发算命网站。
事情是这样的:某天深夜,我突然冒出一个奇葩想法——能不能让Claude Code从零开始搭建一个算命网站? 不是因为我迷信,而是纯粹的技术好奇心:AI能理解2000年前的玄学智慧吗?
于是我开始了这个"用最现代的技术实现最古老的智慧"的实验。
3天后,网站上线了。更神奇的是,AI对传统文化的理解程度让我这个中国人都自愧不如。
选技术栈:AI的选择让我怀疑人生
我本来打算用熟悉的Vue + Express一把梭,结果Claude Code一开口就让我重新审视了自己的技术认知。
我问它:"帮我选个适合算命网站的技术栈"
它的回答让我直接傻眼:
Next.js 14 + TypeScript。我说算命网站用TypeScript?它说:"天干地支计算出错了用户会骂死你的,类型安全不是开玩笑的。"我一想,确实,算错了八字那不是要被用户打死。
Supabase。我说为什么不用MongoDB?它说:"用户算命记录要能实时同步,万一用户换个设备要查看历史记录呢?"行吧,你考虑得比我周到。
DeepSeek API。这个真的让我意外,我下意识想用GPT,结果它说:"中文传统文化,还是国产模型理解得更地道。"后来的事实证明,这个选择太明智了。
最让我佩服的是,它还主动提到了"真太阳时计算"。我当时就懵了,什么是真太阳时?它耐心地给我科普:古代算命要考虑地理位置对时间的影响,不是简单的北京时间。
我心想,这AI对传统文化的理解比我这个中国人还深入啊。
给自己算了一下还是挺满意的
教AI算命:一场智商与玄学的较量
项目最大的难点不是写代码,而是怎么让一个理性的AI理解玄学。
这就像让一个直男理解女朋友的小情绪,难度系数直接拉满。
第一次尝试,我直接甩给AI一个八字:"帮我分析一下甲子年乙丑月丙寅日丁卯时"
AI的回答让我哭笑不得:"这位朋友可能具有较强的时间观念,因为你的出生时间很规律..."
我当场就想关电脑了。这哪里是算命,这是在分析数据规律啊!
第二次,我学聪明了,告诉它"你是命理大师"。结果它开始发挥想象力,创造了一套"六行理论",除了传统五行还加了个"气行"。我差点被它的创新精神感动。
第三次,我痛定思痛,决定从根本上改变策略。我不再把它当AI,而是真的把它当成一个有30年经验的老师傅。我给它详细介绍了传统命理的理论体系,告诉它什么能说,什么不能说,甚至教它怎么说话。
这次它终于开窍了,分析起来有模有样,专业术语用得恰到好处,建议也很中肯。
我突然意识到,训练AI就像带徒弟,不是给它知识,而是教它思考方式。
踩坑实录:当代码遇见玄学,bug都变得玄幻了
做这个项目让我深刻体会到什么叫"传统文化博大精深",每个看似简单的概念背后都藏着巨大的坑。
最让我头疼的是时辰计算。我原本以为很简单,23点到1点是子时嘛,结果Claude Code告诉我:"古代的时辰划分和现代时间概念不完全一样,而且要考虑地理位置。"
我当时就懵了,算个命还要考虑地理位置?后来才知道,古人用的是"真太阳时",北京的中午12点和新疆的中午12点,太阳位置是不一样的。
这就好比你以为做个网站用个时间戳就行了,结果发现还要处理时区、夏令时、闰秒...程序员的痛,古人早就体验过了。
还有一个哭笑不得的bug。AI在分析五行的时候,突然开始"创新",告诉用户发现了"六行理论",除了金木水火土,还有个"气行"。我当时想,你这是要颠覆传统文化吗?
后来我在提示词里加了一句"严格按照传统理论,不要创新",AI这才老实下来。
最隐蔽的坑是日期计算。现代JavaScript处理1900年以前的日期有问题,结果导致古代名人的八字全算错了。我测试的时候用李白的生日,算出来说他五行缺钱...我差点被自己笑死。
每修复一个bug,我都觉得自己对传统文化的理解又深了一层。这感觉很奇妙,就像在用代码穿越时空,和古人对话。
从程序员审美到仙气飘飘
做程序员这么多年,我深知自己的审美水平。我设计的界面通常是这样的:白色背景,黑色字体,偶尔加个边框,完事。
用户打开我设计的网站,第一反应通常是:"这...是1990年代的网站吗?"
但算命网站不一样啊,用户来算命,你给他一个Excel表格的界面,他会觉得你在糊弄他。这玩意得有神秘感,得有仙气。
我问Claude Code:"怎么让网站看起来有仙气?"
它的回答让我刷新了对UI设计的认知。它告诉我色彩心理学:深紫色代表神秘和智慧,金色代表尊贵和权威,渐变背景能营造空间感...
我听得一愣一愣的,心想这AI怎么还懂心理学?
按照它的建议改了界面后,效果确实不错。原本的Excel风格摇身一变成了"古风仙侠游戏界面"。朋友看了都说:"这网站一看就很专业,肯定算得准。"
我当时就想,界面设计真的能影响用户的心理预期。同样的内容,包装不同,用户的信任度完全不一样。
这让我想到另一个问题:在技术驱动的时代,审美能力可能比编程能力更稀缺。会写代码的程序员到处都是,但能设计出让用户一见钟情的界面的,真的不多。
这个布局我很喜欢,但一些ui感觉还可以微调
意外的收获:技术人的文化觉醒
这个项目最大的收获不是技术上的,而是认知上的。
以前我总觉得传统文化和现代技术是两个世界的东西,一个古老神秘,一个理性现代。但做完这个项目后,我发现它们其实是可以融合的。
AI可以学会古老的智慧,代码可以承载文化的传承。技术不是要替代传统,而是要成为传统文化在新时代的载体。
更重要的是,我开始理解用户需求的复杂性。人们使用算命网站,不只是想知道未来会怎样,更多的是希望获得一种心理安慰,一种对未知的控制感。
这让我重新思考技术产品的本质:不是要解决技术问题,而是要解决人的问题。
下一步:用技术重新定义传统
基于这次的经验,我有了一个更大胆的想法:用现代技术重新定义传统文化。
不是简单地把古书电子化,而是用AI、VR、区块链这些新技术,创造全新的文化体验方式。比如用AI生成个性化的《易经》解读,用VR重现古代占卜场景,用区块链记录每个人的文化传承轨迹。
传统文化需要在新时代找到新的表达方式,而技术人恰好有这个能力和责任。
先用three.js写个动画勉强还算满意吧
写在最后:一个程序员的玄学感悟
3天时间,从一个深夜的奇思妙想到一个完整的产品上线。回过头看,这个项目带给我的不只是技术上的提升,更多的是思维上的转变。
最大的感悟是:AI不是工具,而是合作伙伴。它有自己的"想法",会给你意想不到的建议,会从你没想到的角度解决问题。与其说是我在使用AI,不如说是我们在一起探索未知。
第二个感悟是:用户需求比技术实现更重要。算命网站的核心不是算法有多精确,而是能不能给用户带来心理上的满足。技术是手段,解决人的问题才是目的。
第三个感悟是:传统文化需要新的表达方式。不是要用技术颠覆传统,而是要用技术让传统在新时代重新焕发生机。
如果你也对AI开发感兴趣,我的建议是:不要把AI当成万能的代码生成器,要把它当成一个有智慧的合作伙伴。它能给你灵感,能帮你思考,但最终的判断和决策还是要靠你自己。
最后,如果你也想尝试类似的跨界项目,记住一点:技术栈可以学,算法可以抄,但洞察用户需求的能力,只能靠自己慢慢积累。
下一个项目,还不知道做啥,有想法的朋友可以在评论区说一声
本文基于真实项目开发经验,欢迎技术交流和商业合作!
来源:juejin.cn/post/7537339432292270080
用卡尔曼滤波器计算个股票相对大盘的相关性——β值
A股涨到3600点了。
你是不是也跟我一样——看到新闻标题说“牛市要来了!”,一打开账户……嗯?怎么手里的票还是绿的,上证指数25年都涨7%了,而你确亏了7%
为什么你的股票没涨过大盘?到底是市场的问题,还是你的股票压根不跟着市场走?
聪明的你可能已经想到一个词了,叫——β值(Beta)。
说白了,β值其实就是个股相对大盘“跟涨跟跌”的敏感度。你可以把它想象成你手上这票对“市场情绪”的反应速度和强度。
- β > 1 的股票,市场一疯,它先蹦跶;市场一跌,它先躺平。
- β < 1 的股票,属于佛系型,涨跌都慢半拍。
- β ≈ 0 的,那是独行侠,走自己的路让别人说去吧。
我知道,有些小伙伴可能说:“花姐你说这我都懂,但怎么算这个β值啊?”
来,今天我们就来整点硬核的——用卡尔曼滤波器来动态估算β值。
传统β值是怎么算的?
这事儿咱得先交代清楚,不然后面你会问:为啥要搞卡尔曼滤波呢?直接用回归不香吗?
最常见的β值计算方式是用OLS线性回归,比如拿某个股票和上证指数近一年日收益率,做个线性回归,斜率就是β值。代码长这样:
import pandas as pd
import numpy as np
import statsmodels.api as sm
stock_ret = ... # 股票收益率序列
market_ret = ... # 市场指数收益率序列
X = sm.add_constant(market_ret)
model = sm.OLS(stock_ret, X).fit()
beta = model.params[1]
结果算出来的β是固定的一个值。问题来了:市场在变,个股特性也在变,一个固定的β能代表未来吗?
用卡尔曼滤波器
那么问题来了:有没有什么办法,可以让β值随着时间动态变化,反映出最新的市场行为?
有!这时候就该请出我们的主角了——卡尔曼滤波器(Kalman Filter)。
别听名字吓人,其实你可以把它理解为一个“会自我更新”的预测模型。它有点像是个不断修正自己认知的智能体,每来一个新数据,就纠正一下之前的偏差。
如果说OLS是一次性静态判断,那卡尔曼滤波就是边走边看边修正。
卡尔曼滤波器怎么估算β?
卡尔曼滤波在量化里的用途很多,其中一个经典用途就是:时间序列中的线性参数动态估计。
我们的问题就可以建模成这个样子
股票收益率 = α + β × 市场收益率 + 噪声
不同的是,我们让β变成一个随时间变化的变量。
也就是说,今天的β和昨天的不一样,明天的也不一定一样。
我们设定两个方程:
状态方程(β的演变):
观测方程(收益率的观察):
其中:
- 是大盘的收益率
- 是股票的收益率
- 和 是噪声,分别表示系统噪声和观测噪声
这个模型的核心点是:我们认为β本身在缓慢变化,而每个观测数据都能对β的估计做一次修正。
不扯了,直接上代码!
咱们用 Python 写个计算个股β值的方法,当然你也可以把上证指数换成股票对应的行业指数,这样就可以得到个股相对行业的β值了。
这里计算卡尔曼的时候用了第三方库通过以下代码安装
pip install pykalman
from xtquant import xtdata
import pandas as pd
from pykalman import KalmanFilter
import numpy as np
import matplotlib.pyplot as plt
def get_hq(code,start_date='19900101',period='1d',dividend_type='front',count=-1):
'''
这里替换成你获取行情的方法
'''
df = pd.read_csv(f'{code}.csv')
return df
def calu_kalman_beta(df_stock,df_index):
'''
计算某个股票相对某个指数的β值
'''
# 对齐日期,按日期升序
df_stock = df_stock.sort_values('date')
df_index = df_index.sort_values('date')
df_stock = df_stock[df_stock['suspendFlag'] == 0] # 剔除停牌数据
# 合并,方便对齐(外层用 inner,保证两个都有数据)
df = pd.merge(df_stock[['date', 'close']],
df_index[['date', 'close']],
on='date',
suffixes=('_stock', '_index'))
# 计算对数收益率(更平滑、更合理)
df['ret_stock'] = np.log(df['close_stock'] / df['close_stock'].shift(1))
df['ret_index'] = np.log(df['close_index'] / df['close_index'].shift(1))
# 去除缺失
df = df.dropna().reset_index(drop=True)
# 提取序列
stock_ret = df['ret_stock'].values
index_ret = df['ret_index'].values
# 初始化卡尔曼滤波器
kf = KalmanFilter(
transition_matrices=1.0,
observation_matrices=1.0 ,
initial_state_mean=0.0,
initial_state_covariance=1.0,
observation_covariance=0.01, # 控制对观测数据的信任度 可微调
transition_covariance=0.00001 # 控制 β 的平滑程度 越小越平滑
)
# 加入极端值裁剪(防止除以接近0)
index_ret_safe = np.where(np.abs(index_ret) < 1e-4, np.sign(index_ret) * 1e-4, index_ret)
# 我们把 market_ret 作为“输入变量”,用于动态预测观测值
observations = stock_ret / index_ret_safe # y_t / x_t
observations = np.clip(observations, -10, 10) # 避免除数太小导致爆炸(你也可以换个方式)
state_means, _ = kf.filter(observations)
df['beta_kalman'] = state_means.flatten()
return df[10:]
if __name__=="__main__":
start_date='20240101'
code = '这里替换成股票代码'
index = '这里是指数代码'
df_stock = get_hq(code=code,start_date=start_date,period='1d')
df_index = get_hq(code=index,start_date=start_date,period='1d')
df = calu_kalman_beta(df_stock,df_index)
# 画图
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体
plt.rcParams['axes.unicode_minus'] = False # 正负号也正常显示
plt.figure(figsize=(12, 6))
plt.plot(df['date'], df['beta_kalman'], label='动态β(Kalman估计)', color='orange')
plt.axhline(1, linestyle='--', color='gray', alpha=0.5)
plt.title(f'{code} vs {index} 的动态β值')
plt.xlabel('date')
plt.ylabel('β值')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
给大家简单展示几个绘制的图
那我怎么用这个β?
有意思的来了。
用法1:选股
找出动态β长期 < 1 的股票,说明它抗跌能力强——在市场波动大的时候更稳。这类票适合熊市配置。
反过来,找动态β > 1.2 并上升中的,说明它在牛市中可能跑得飞快。
你甚至可以把这个β当作因子,搞个多空策略:多β上升的,空β下降的,构建一个方向中性的策略组合。这个思路其实在某些CTA策略里也有影子。
用法2:择时
你还可以跟踪你组合的整体β,动态调整仓位。
举个栗子:你组合的β从0.9升到1.3,说明风险在上升——这时候该减仓。反之也是。
今天的文章就到这里了,希望大家喜欢。
来源:juejin.cn/post/7533510113068630026
VitePress 彩虹动画
在查阅 VitePress 具体实践时,我被 UnoCSS 文档中的彩虹动画效果深深吸引。在查看其实现原理之后,本文也将探索如何通过自定义组件和样式增强 VitePress 站点,并实现一个炫酷的彩虹动画效果。
自定义主题
VitePress 允许你通过自定义 Layout 来改变页面的结构和样式。自定义 Layout 可以帮助你更好地控制页面的外观和行为,尤其是在复杂的站点中。
项目初始化
在终端中运行以下命令,初始化一个新的 VitePress 项目:
npx vitepress init
然后根据提示,这次选择自定义主题(Default Theme + Customization
):
┌ Welcome to VitePress!
│
◇ Where should VitePress initialize the config?
│ ./docs
│
◇ Site title:
│ My Awesome Project
│
◇ Site description:
│ A VitePress Site
│
◇ Theme:
│ Default Theme + Customization
│
◇ Use TypeScript for config and theme files?
│ Yes
│
◇ Add VitePress npm scripts to package.json?
│ Yes
│
└ Done! Now run npm run docs:dev and start writing.
Tips:
- Make sure to add docs/.vitepress/dist and docs/.vitepress/cache to your .gitignore file.
- Since you've chosen to customize the theme, you should also explicitly install vue as a dev dependency.
注意提示,这里需要额外手动安装 vue 库:
pnpm add vue
自定义入口文件
找到 .vitepress/theme/index.ts
入口文件:
// <https://vitepress.dev/guide/custom-theme>
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// <https://vitepress.dev/guide/extending-default-theme#layout-slots>
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme
里面暴露了一个 Layout 组件,这里是通过 h 函数实现的,我们将其抽离成 Layout.vue
组件。
创建自定义 Layout
VitePress 的 Layout 组件是整个网站的骨架,控制了页面的基本结构和布局。通过自定义 Layout,我们可以完全掌控网站的外观和行为。
为什么要自定义 Layout?
- 增加特定的布局元素
- 修改默认主题的行为
- 添加全局组件或功能
- 实现特殊的视觉效果(如我们的彩虹动画)
我们在 .vitepress/theme
文件夹中创建 Layout.vue
组件,并将之前的内容转换成 vue 代码:
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
</script>
<template>
<DefaultTheme.Layout />
</template>
接下来,在 .vitepress/theme/index.ts
中注册自定义 Layout:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
}
这将会覆盖默认的 Layout,应用你自定义的布局结构。
覆盖原本样式
VitePress 提供了 css 变量来动态修改自带的样式,可以看到项目初始化后在 .vitepress/theme
中有一个 style.css
。里面提供了案例,告诉如何去修改这些变量。
同时可以通过该链接查看全部的 VitePress 变量:VitePress 默认主题变量。
VitePress 允许我们通过多种方式覆盖默认样式。最常用的方法是创建一个 CSS 文件,并在主题配置中导入。
比如想设置 name
的颜色,就可以通过:
:root {
--vp-home-hero-name-color: blue;
}
引入 UnoCSS
UnoCSS 是一个按需生成 CSS 的工具,可以极大简化 CSS 管理,帮助快速生成高效样式。
在项目中安装 UnoCSS 插件:
pnpm add -D unocss
然后,在 vite.config.ts
中配置 UnoCSS 插件:
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UnoCSS()],
}
通过 UnoCSS,可以轻松应用样式而无需写冗余 CSS。例如,使用以下类名快速创建按钮样式:
<button class="bg-blue-500 text-white p-4 rounded-lg hover:bg-blue-600">
按钮
</button>
实现彩虹动画
彩虹动画是本文的主角,主要通过动态改变 CSS 变量值来实现色彩的平滑过渡。
定义彩虹动画关键帧
通过 @keyframes
,在不同颜色之间平滑过渡,形成彩虹动画效果。创建 rainbow.css
文件:
@keyframes rainbow {
0% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
25% {
--vp-c-brand-1: #00a6e2;
--vp-c-brand-light: #56cdff;
--vp-c-brand-lighter: #87f6ff;
--vp-c-brand-dark: #0080b9;
--vp-c-brand-darker: #005c93;
--vp-c-brand-next: #9280ed;
}
50% {
--vp-c-brand-1: #c76dd1;
--vp-c-brand-light: #f194fa;
--vp-c-brand-lighter: #ffbcff;
--vp-c-brand-dark: #9e47a9;
--vp-c-brand-darker: #772082;
--vp-c-brand-next: #eb6552;
}
75% {
--vp-c-brand-1: #e95ca2;
--vp-c-brand-light: #ff84ca;
--vp-c-brand-lighter: #ffadf2;
--vp-c-brand-dark: #be317d;
--vp-c-brand-darker: #940059;
--vp-c-brand-next: #d17a2a;
}
100% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
}
:root {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: rainbow 40s linear infinite;
}
html:not(.rainbow) {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: none !important;
}
这段代码定义了彩虹动画的五个关键帧,并将动画应用到根元素上。注意,我们还定义了不带动画的默认状态,这样就可以通过 CSS 类切换动画的启用/禁用。
实现彩虹动画控制组件
接下来,实现名为 RainbowAnimationSwitcher
的组件,其主要逻辑是通过添加或移除 HTML 根元素上的 rainbow
类来控制动画的启用状态,从而实现页面的彩虹渐变效果。
这个组件使用了 @vueuse/core 的两个工具函数:
useLocalStorage
用于在浏览器本地存储用户的偏好设置useMediaQuery
用于检测用户系统是否设置了减少动画
<script lang="ts" setup>
import { useLocalStorage, useMediaQuery } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { computed, watch } from 'vue'
import RainbowSwitcher from './RainbowSwitcher.vue'
defineProps<{ text?: string; screenMenu?: boolean }>()
const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)').value
const animated = useLocalStorage('animate-rainbow', inBrowser ? !reduceMotion : true)
function toggleRainbow() {
animated.value = !animated.value
}
// 在这里对动画做处理
watch(
animated,
anim => {
document.documentElement.classList.remove('rainbow')
if (anim) {
document.documentElement.classList.add('rainbow')
}
},
{ immediate: inBrowser, flush: 'post' },
)
const switchTitle = computed(() => {
return animated.value ? 'Disable rainbow animation' : 'Enable rainbow animation'
})
</script>
<template>
<ClientOnly>
<div class="group" :class="{ mobile: screenMenu }">
<div class="NavScreenRainbowAnimation">
<p class="text">
{{ text ?? 'Rainbow Animation' }}
</p>
<RainbowSwitcher
:title="switchTitle"
class="RainbowAnimationSwitcher"
:aria-checked="animated ? 'true' : 'false'"
@click="toggleRainbow"
>
<span class="i-tabler:rainbow animated" />
<span class="i-tabler:rainbow-off non-animated" />
</RainbowSwitcher>
</div>
</div>
</ClientOnly>
</template>
<style scoped>
.group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
margin-top: 1rem !important;
}
.NavScreenRainbowAnimation {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px;
padding: 12px;
background-color: var(--vp-c-bg-elv);
max-width: 220px;
}
.text {
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.animated {
opacity: 1;
}
.non-animated {
opacity: 0;
}
.RainbowAnimationSwitcher[aria-checked='false'] .non-animated {
opacity: 1;
}
.RainbowAnimationSwitcher[aria-checked='true'] .animated {
opacity: 1;
}
</style>
其中 RainbowSwitcher
组件是一个简单的开关按钮。以下是其实现:
<template>
<button class="VPSwitch" type="button" role="switch">
<span class="check">
<span v-if="$slots.default" class="icon">
<slot />
</span>
</span>
</button>
</template>
<style scoped>
.VPSwitch {
position: relative;
border-radius: 11px;
display: block;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s !important;
}
.check {
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--vp-c-neutral-inverse);
box-shadow: var(--vp-shadow-1);
transition: transform 0.25s !important;
}
.icon {
position: relative;
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
overflow: hidden;
}
</style>
挂载组件
在 .vitepress/theme/index.ts
中,在 enhanceApp
中挂载组件:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
enhanceApp({ app, router }) {
app.component('RainbowAnimationSwitcher', RainbowAnimationSwitcher)
if (typeof window === 'undefined') return
watch(
() => router.route.data.relativePath,
() => updateHomePageStyle(location.pathname === '/'),
{ immediate: true },
)
},
}
// Speed up the rainbow animation on home page
function updateHomePageStyle(value: boolean) {
if (value) {
if (homePageStyle) return
homePageStyle = document.createElement('style')
homePageStyle.innerHTML = `
:root {
animation: rainbow 12s linear infinite;
}`
document.body.appendChild(homePageStyle)
} else {
if (!homePageStyle) return
homePageStyle.remove()
homePageStyle = undefined
}
}
在导航栏中使用彩虹动画开关
在 .vitepress/config/index.ts
的配置文件中添加彩虹动画开关按钮:
export default defineConfig({
themeConfig: {
nav: [
// 其他导航项...
{
text: `v${version}`,
items: [
{
text: '发布日志',
link: '<https://github.com/yourusername/repo/releases>',
},
{
text: '提交 Issue',
link: '<https://github.com/yourusername/repo/issues>',
},
{
component: 'RainbowAnimationSwitcher',
props: {
text: '彩虹动画',
},
},
],
},
],
// 其他配置...
},
})
这样,彩虹动画开关就成功加载到导航栏的下拉菜单中。
彩虹动画效果
如果想查看具体效果,可查看 EasyEditor 的文档。其中关于彩虹动画效果的详细实现看,可以查看内部对应的代码:EasyEditor/docs/.vitepress/theme at main · Easy-Editor/EasyEditor。
来源:juejin.cn/post/7508591120407576586
python实现的websocket日志类
背景
功能需求需要实现一个“实时日志”功能,即在一个可以在web端触发任务的系统中,可以即时显示触发的任务的日志(此后台任务在后台或其他worker主机上执行)。最后采用了websocket方法来实现该功能,即在任务执行端实现一个logger类通过websocket上传实时日志给web后端,再由web后端通过websocket连接传给web前端,实现实时日志的功能。
websocket logHandler类
协程版本
使用websockets库
import logging
import json
import time
import asyncio
import websockets
from queue import Queue
from threading import Thread
import traceback
from _queue import Empty
class WebSocketHandler(logging.Handler):
"""
自定义日志处理器,将日志通过WebSocket发送到后端服务
"""
def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
"""
初始化WebSocket处理器
Args:
ws_url: WebSocket服务器URL
reconnect_interval: 断线重连间隔(秒)
max_queue_size: 日志队列最大长度,超出时丢弃旧日志
"""
super().__init__()
self.ws_url = ws_url
self.reconnect_interval = reconnect_interval
self.max_queue_size = max_queue_size
self.is_running = False
self.thread =
self.queue = # 异步队列,在start中初始化
self.loop = # 保存事件循环引用
def emit(self, record):
"""
重写emit方法,将日志记录发送到WebSocket
Args:
record: 日志记录对象
"""
try:
# 格式化日志
msg = self.format(record)
if not msg.endswith("\n"):
msg += "\n"
# 跨线程安全添加日志到队列(关键修复)
if self.loop and self.queue:
# 使用事件循环的线程安全方法添加元素
self.loop.call_soon_threadsafe(
self._safe_put_queue, msg
)
else:
print("队列未初始化,日志发送失败")
except Exception as e:
# 处理发送失败的情况
self.handleError(record)
def _safe_put_queue(self, msg):
"""线程安全的队列添加方法(在事件循环线程执行)"""
try:
if not self.queue.full():
self.queue.put_nowait(msg)
else:
# 队列满时丢弃最旧日志
self.queue.get_nowait()
self.queue.put_nowait(msg)
except Exception as e:
print(f"队列添加失败: {e}")
def start(self):
"""启动WebSocket发送线程"""
if not self.is_running:
self.is_running = True
self.thread = Thread(target=self._ws_sender_thread)
self.thread.daemon = True
self.thread.start()
print("WebSocket发送线程启动")
def stop(self):
"""停止WebSocket发送线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2.0)
def _ws_sender_thread(self):
"""WebSocket发送线程主函数"""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.queue = asyncio.Queue(maxsize=self.max_queue_size)
async def custom_heartbeat(websocket):
while True:
try:
await websocket.send(json.dumps({"type":"ping"})) # 自定义心跳消息
await asyncio.sleep(30) # 30秒间隔
except Exception as e:
print(f"心跳发送失败: {e}")
break
async def _process_logs(websocket):
# 连接成功后,发送队列中积压的所有日志
while not self.queue.empty() and self.is_running:
# log_data = self.queue.get_nowait()
# await websocket.send(json.dumps(log_data, ensure_ascii=False))
msg = await self.queue.get()
await websocket.send(msg)
# 持续发送新日志
while self.is_running:
try:
# 阻塞等待新日志,带超时以检查线程是否需要停止
# log_data = self.queue.get(timeout=1)
# await websocket.send(json.dumps(log_data))
msg = await self.queue.get()
await websocket.send(msg)
self.queue.task_done()
except asyncio.TimeoutError:
continue
except Empty:
continue
async def send_logs():
while self.is_running:
try:
# 连接WebSocket服务器
async with websockets.connect(self.ws_url) as websocket:
# 并行运行日志发送和心跳任务
await asyncio.gather(
_process_logs(websocket),
custom_heartbeat(websocket)
)
except Exception as e:
traceback.print_exc()
# 连接失败或断开,等待后重试
# 等待重连间隔(使用异步sleep)
await asyncio.sleep(self.reconnect_interval)
try:
self.loop.run_until_complete(send_logs())
except Exception as e:
pass
finally:
self.loop.close()
def close(self):
"""关闭处理器"""
self.stop()
super().close()
# 配置示例
def setup_logger():
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
# 创建WebSocket处理器
ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
ws_handler.setLevel(logging.DEBUG)
json_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 我们在handler中自己处理JSON格式化
ws_handler.setFormatter(json_formatter)
# 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(ws_handler)
# 启动WebSocket处理器
ws_handler.start()
return logger
# 使用示例
if __name__ == "__main__":
logger = setup_logger()
try:
# 正常记录日志,会同时输出到控制台和WebSocket
logger.debug('这是一条调试日志')
logger.info('这是一条信息日志')
logger.warning('这是一条警告日志')
logger.error('这是一条错误日志')
# 模拟长时间运行的程序
while True:
time.sleep(1)
except KeyboardInterrupt:
# 程序退出时,确保WebSocket处理器正确关闭
for handler in logger.handlers:
if isinstance(handler, WebSocketHandler):
handler.stop()
多线程版本
使用websocket-client库
import logging
import json
import time
from queue import Queue, Empty
from threading import Thread
import traceback
import websocket # 需安装:pip install websocket-client
class WebSocketHandler(logging.Handler):
"""自定义日志处理器(同步版本),通过WebSocket发送日志"""
def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
super().__init__()
self.ws_url = ws_url
self.reconnect_interval = reconnect_interval
self.max_queue_size = max_queue_size
self.is_running = False # 控制整体运行状态
self.heartbeat_running = False # 控制心跳线程
self.thread = # 日志发送主线程
self.heartbeat_thread = # 心跳线程
self.queue = Queue(maxsize=max_queue_size) # 同步队列(线程安全)
self.ws = # WebSocket连接实例
def emit(self, record):
"""日志记录触发时调用,将日志放入队列"""
try:
# 格式化日志
msg = self.format(record)
if not msg.endswith("\n"):
msg += "\n"
# 队列满时丢弃最旧日志
if self.queue.full():
try:
self.queue.get_nowait() # 移除最旧日志
except Empty:
pass # 队列已空,无需处理
self.queue.put_nowait(msg) # 放入新日志(同步队列线程安全)
except Exception as e:
self.handleError(record)
def close(self):
"""关闭处理器"""
self.stop()
super().close()
def start(self):
"""启动日志发送线程和心跳线程"""
if not self.is_running:
self.is_running = True
# 启动日志发送主线程
self.thread = Thread(target=self._ws_sender_thread)
self.thread.daemon = True
self.thread.start()
print("WebSocket发送线程启动")
def stop(self):
"""停止所有线程和连接"""
self.is_running = False
self.heartbeat_running = False # 停止心跳线程
# 关闭WebSocket连接
if self.ws:
try:
self.ws.close()
except Exception as e:
print(f"关闭WebSocket失败: {e}")
# 等待线程结束
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2.0)
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
self.heartbeat_thread.join(timeout=1.0)
print("WebSocket发送线程已停止")
def _start_heartbeat(self):
"""启动心跳线程"""
self.heartbeat_running = True
self.heartbeat_thread = Thread(target=self._heartbeat_loop)
self.heartbeat_thread.daemon = True
self.heartbeat_thread.start()
print("心跳线程启动")
def _heartbeat_loop(self):
"""心跳发送循环(独立线程)"""
while self.heartbeat_running and self.is_running:
try:
if self.ws and self.ws.connected: # 检查连接是否有效
self.ws.send(json.dumps({"type": "ping"})) # 发送心跳
time.sleep(30) # 30秒间隔
except Exception as e:
print(f"心跳发送失败: {e}")
break # 心跳失败,退出循环(由主线程重连)
def _process_logs(self):
"""处理队列中的日志并发送(同步阻塞)"""
while self.is_running:
try:
# 阻塞等待日志(超时1秒,避免永久阻塞)
msg = self.queue.get(timeout=1)
if self.ws and self.ws.connected:
self.ws.send(msg) # 发送日志
self.queue.task_done()
else:
# 连接已断开,将日志放回队列
self.queue.put(msg)
time.sleep(0.1) # 短暂等待后重试
except Empty:
continue # 队列空,继续循环
except Exception as e:
print(f"日志发送失败: {e}")
# 发送失败,将日志放回队列重试
try:
self.queue.put(msg)
except Exception as put_err:
print(f"日志放回队列失败: {put_err}")
time.sleep(1) # 等待后重试
def _ws_sender_thread(self):
"""WebSocket发送主线程:负责连接和日志发送协调"""
while self.is_running:
try:
# 建立WebSocket连接
print(f"连接WebSocket服务器: {self.ws_url}")
self.ws = websocket.create_connection(self.ws_url)
print("WebSocket连接成功")
# 启动心跳线程(每次重连后重启心跳)
self._start_heartbeat()
# 处理日志发送
self._process_logs()
except Exception as e:
print(f"WebSocket连接/发送异常: {e}")
traceback.print_exc()
finally:
# 连接断开时清理
self.heartbeat_running = False # 停止当前心跳线程
if self.heartbeat_thread:
self.heartbeat_thread.join(timeout=1.0)
if self.ws:
try:
self.ws.close()
except Exception as e:
print(f"关闭WebSocket连接失败: {e}")
self.ws = # 重置连接实例
# 断线重连等待
if self.is_running:
print(f"等待{self.reconnect_interval}秒后重试...")
time.sleep(self.reconnect_interval)
# 配置示例
def setup_logger():
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
# 创建WebSocket处理器(使用同步版本)
ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
ws_handler.setLevel(logging.DEBUG)
ws_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ws_handler.setFormatter(ws_formatter)
# 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(ws_handler)
# 启动WebSocket处理器
ws_handler.start()
return logger
# 使用示例
if __name__ == "__main__":
logger = setup_logger()
try:
# 测试日志发送
logger.debug('这是一条调试日志')
logger.info('这是一条信息日志')
logger.warning('这是一条警告日志')
logger.error('这是一条错误日志')
# 模拟长时间运行的程序
while True:
logger.info('持续发送的日志...')
time.sleep(5) # 每5秒发送一条测试日志
except KeyboardInterrupt:
print("程序退出中...")
# 停止WebSocket处理器
for handler in logger.handlers:
if isinstance(handler, WebSocketHandler):
handler.stop()
print("程序已退出")
集成
集成时只需将handler的示例加到全局logger中即可,就像main函数中setup_logger()的使用那样,但需注意正式使用时最好将handler.stop()函数放在finally块中,确保正确退出。
来源:juejin.cn/post/7534661634238185481
代码界的 “建筑师”:建造者模式,让复杂对象构建井然有序
深入理解建造者模式:复杂对象的定制化构建之道
在软件开发中,我们常会遇到需要创建 “复杂对象” 的场景 —— 这类对象由多个部件组成,且部件的组合顺序、配置细节可能存在多种变化。例如,定制一台电脑需要选择 CPU、内存、硬盘等部件;生成一份报告需要包含标题、正文、图表、落款等模块。若直接在客户端代码中编写对象的构建逻辑,不仅会导致代码臃肿、耦合度高,还难以灵活应对不同的定制需求。此时,建造者模式(Builder Pattern) 便能发挥关键作用,它将复杂对象的构建过程与表示分离,让同一构建过程可生成不同的表示。
在软件开发中,我们常会遇到需要创建 “复杂对象” 的场景 —— 这类对象由多个部件组成,且部件的组合顺序、配置细节可能存在多种变化。例如,定制一台电脑需要选择 CPU、内存、硬盘等部件;生成一份报告需要包含标题、正文、图表、落款等模块。若直接在客户端代码中编写对象的构建逻辑,不仅会导致代码臃肿、耦合度高,还难以灵活应对不同的定制需求。此时,建造者模式(Builder Pattern) 便能发挥关键作用,它将复杂对象的构建过程与表示分离,让同一构建过程可生成不同的表示。
一、建造者模式的核心定义与价值
1. 官方定义
建造者模式是 “创建型设计模式” 的重要成员,其核心思想是:将一个复杂对象的构建过程抽象出来,拆分为多个独立的构建步骤,通过不同的 “建造者” 实现这些步骤,再由 “指挥者” 按指定顺序调用步骤,最终组装出完整对象。
简单来说,它就像 “组装家具” 的流程:家具说明书(指挥者)规定了先装框架、再装抽屉、最后装柜门的步骤;而不同品牌的组装师傅(具体建造者),会用不同材质的零件(部件)完成每一步;最终用户(客户端)只需告诉商家 “想要哪种风格的家具”,无需关心具体组装细节。
建造者模式是 “创建型设计模式” 的重要成员,其核心思想是:将一个复杂对象的构建过程抽象出来,拆分为多个独立的构建步骤,通过不同的 “建造者” 实现这些步骤,再由 “指挥者” 按指定顺序调用步骤,最终组装出完整对象。
简单来说,它就像 “组装家具” 的流程:家具说明书(指挥者)规定了先装框架、再装抽屉、最后装柜门的步骤;而不同品牌的组装师傅(具体建造者),会用不同材质的零件(部件)完成每一步;最终用户(客户端)只需告诉商家 “想要哪种风格的家具”,无需关心具体组装细节。
2. 核心价值
- 解耦构建与表示:构建过程(步骤顺序)和对象表示(部件配置)分离,同一过程可生成不同配置的对象(如用相同步骤组装 “游戏本” 和 “轻薄本”)。
- 灵活定制细节:支持对对象部件的精细化控制,客户端可通过选择不同建造者,定制符合需求的对象(如电脑可选择 “i7 CPU+32G 内存” 或 “i5 CPU+16G 内存”)。
- 简化客户端代码:客户端无需关注复杂的构建逻辑,只需与指挥者或建造者简单交互,即可获取完整对象。
- 解耦构建与表示:构建过程(步骤顺序)和对象表示(部件配置)分离,同一过程可生成不同配置的对象(如用相同步骤组装 “游戏本” 和 “轻薄本”)。
- 灵活定制细节:支持对对象部件的精细化控制,客户端可通过选择不同建造者,定制符合需求的对象(如电脑可选择 “i7 CPU+32G 内存” 或 “i5 CPU+16G 内存”)。
- 简化客户端代码:客户端无需关注复杂的构建逻辑,只需与指挥者或建造者简单交互,即可获取完整对象。
二、建造者模式的核心结构
建造者模式通常包含 4 个核心角色,它们分工明确、协作完成对象构建:
角色名称 核心职责 产品(Product) 需要构建的复杂对象,由多个部件组成(如 “电脑”“报告”)。 抽象建造者(Builder) 定义构建产品所需的所有步骤(如 “设置 CPU”“设置内存”),通常包含获取产品的方法。 具体建造者(Concrete Builder) 实现抽象建造者的步骤,定义具体部件的配置(如 “游戏本建造者”“轻薄本建造者”)。 指挥者(Director) 负责调用建造者的步骤,规定构建的顺序(如 “先装 CPU→再装内存→最后装硬盘”)。
建造者模式通常包含 4 个核心角色,它们分工明确、协作完成对象构建:
角色名称 | 核心职责 |
---|---|
产品(Product) | 需要构建的复杂对象,由多个部件组成(如 “电脑”“报告”)。 |
抽象建造者(Builder) | 定义构建产品所需的所有步骤(如 “设置 CPU”“设置内存”),通常包含获取产品的方法。 |
具体建造者(Concrete Builder) | 实现抽象建造者的步骤,定义具体部件的配置(如 “游戏本建造者”“轻薄本建造者”)。 |
指挥者(Director) | 负责调用建造者的步骤,规定构建的顺序(如 “先装 CPU→再装内存→最后装硬盘”)。 |
三、建造者模式的实战案例:定制电脑的构建
为了更直观理解,我们以 “定制电脑” 为例,用 Java 代码实现建造者模式:
为了更直观理解,我们以 “定制电脑” 为例,用 Java 代码实现建造者模式:
1. 第一步:定义 “产品”(电脑)
首先明确需要构建的复杂对象 —— 电脑,它包含 CPU、内存、硬盘、显卡等部件:
// 产品:电脑
public class Computer {
// 电脑的部件
private String cpu;
private String memory;
private String hardDisk;
private String graphicsCard;
// Setter方法(用于建造者设置部件)
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setMemory(String memory) {
this.memory = memory;
}
public void setHardDisk(String hardDisk) {
this.hardDisk = hardDisk;
}
public void setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
}
// 展示电脑配置(对象的“表示”)
public void showConfig() {
System.out.println("电脑配置:CPU=" + cpu + ",内存=" + memory + ",硬盘=" + hardDisk + ",显卡=" + graphicsCard);
}
}
首先明确需要构建的复杂对象 —— 电脑,它包含 CPU、内存、硬盘、显卡等部件:
// 产品:电脑
public class Computer {
// 电脑的部件
private String cpu;
private String memory;
private String hardDisk;
private String graphicsCard;
// Setter方法(用于建造者设置部件)
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setMemory(String memory) {
this.memory = memory;
}
public void setHardDisk(String hardDisk) {
this.hardDisk = hardDisk;
}
public void setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
}
// 展示电脑配置(对象的“表示”)
public void showConfig() {
System.out.println("电脑配置:CPU=" + cpu + ",内存=" + memory + ",硬盘=" + hardDisk + ",显卡=" + graphicsCard);
}
}
2. 第二步:定义 “抽象建造者”(电脑建造者接口)
抽象出构建电脑的所有步骤,确保所有具体建造者都遵循统一规范:
// 抽象建造者:电脑建造者接口
public interface ComputerBuilder {
// 构建步骤1:设置CPU
void buildCpu();
// 构建步骤2:设置内存
void buildMemory();
// 构建步骤3:设置硬盘
void buildHardDisk();
// 构建步骤4:设置显卡
void buildGraphicsCard();
// 获取最终构建的电脑
Computer getComputer();
}
抽象出构建电脑的所有步骤,确保所有具体建造者都遵循统一规范:
// 抽象建造者:电脑建造者接口
public interface ComputerBuilder {
// 构建步骤1:设置CPU
void buildCpu();
// 构建步骤2:设置内存
void buildMemory();
// 构建步骤3:设置硬盘
void buildHardDisk();
// 构建步骤4:设置显卡
void buildGraphicsCard();
// 获取最终构建的电脑
Computer getComputer();
}
3. 第三步:实现 “具体建造者”(游戏本 / 轻薄本建造者)
针对不同需求,实现具体的部件配置。例如,“游戏本” 需要高性能 CPU 和显卡,“轻薄本” 更注重便携性(低功耗部件):
// 具体建造者1:游戏本建造者
public class GamingLaptopBuilder implements ComputerBuilder {
private Computer computer = new Computer(); // 持有产品实例
@Override
public void buildCpu() {
computer.setCpu("Intel i9-13900HX(高性能CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("32GB DDR5(高带宽内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("2TB SSD(高速硬盘)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("NVIDIA RTX 4080(高性能显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
// 具体建造者2:轻薄本建造者
public class UltrabookBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i5-1335U(低功耗CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("16GB LPDDR5(低功耗内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("1TB SSD(便携性优先)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("Intel Iris Xe(集成显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
针对不同需求,实现具体的部件配置。例如,“游戏本” 需要高性能 CPU 和显卡,“轻薄本” 更注重便携性(低功耗部件):
// 具体建造者1:游戏本建造者
public class GamingLaptopBuilder implements ComputerBuilder {
private Computer computer = new Computer(); // 持有产品实例
@Override
public void buildCpu() {
computer.setCpu("Intel i9-13900HX(高性能CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("32GB DDR5(高带宽内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("2TB SSD(高速硬盘)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("NVIDIA RTX 4080(高性能显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
// 具体建造者2:轻薄本建造者
public class UltrabookBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i5-1335U(低功耗CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("16GB LPDDR5(低功耗内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("1TB SSD(便携性优先)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("Intel Iris Xe(集成显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
4. 第四步:定义 “指挥者”(电脑组装指导者)
指挥者负责规定构建顺序,避免具体建造者与步骤顺序耦合。例如,统一按 “CPU→内存→硬盘→显卡” 的顺序组装:
// 指挥者:电脑组装指导者
public class ComputerDirector {
// 接收具体建造者,按顺序调用构建步骤
public Computer construct(ComputerBuilder builder) {
builder.buildCpu(); // 步骤1:装CPU
builder.buildMemory(); // 步骤2:装内存
builder.buildHardDisk();// 步骤3:装硬盘
builder.buildGraphicsCard();// 步骤4:装显卡
return builder.getComputer(); // 返回组装好的电脑
}
}
指挥者负责规定构建顺序,避免具体建造者与步骤顺序耦合。例如,统一按 “CPU→内存→硬盘→显卡” 的顺序组装:
// 指挥者:电脑组装指导者
public class ComputerDirector {
// 接收具体建造者,按顺序调用构建步骤
public Computer construct(ComputerBuilder builder) {
builder.buildCpu(); // 步骤1:装CPU
builder.buildMemory(); // 步骤2:装内存
builder.buildHardDisk();// 步骤3:装硬盘
builder.buildGraphicsCard();// 步骤4:装显卡
return builder.getComputer(); // 返回组装好的电脑
}
}
5. 第五步:客户端调用(定制电脑)
客户端只需选择 “具体建造者”,无需关心构建步骤,即可获取定制化电脑:
public class Client {
public static void main(String[] args) {
// 1. 创建指挥者
ComputerDirector director = new ComputerDirector();
// 2. 定制游戏本(选择游戏本建造者)
ComputerBuilder gamingBuilder = new GamingLaptopBuilder();
Computer gamingLaptop = director.construct(gamingBuilder);
gamingLaptop.showConfig(); // 输出:游戏本配置
// 3. 定制轻薄本(选择轻薄本建造者)
ComputerBuilder ultrabookBuilder = new UltrabookBuilder();
Computer ultrabook = director.construct(ultrabookBuilder);
ultrabook.showConfig(); // 输出:轻薄本配置
}
}
运行结果:
电脑配置:CPU=Intel i9-13900HX(高性能CPU),内存=32GB DDR5(高带宽内存),硬盘=2TB SSD(高速硬盘),显卡=NVIDIA RTX 4080(高性能显卡)
电脑配置:CPU=Intel i5-1335U(低功耗CPU),内存=16GB LPDDR5(低功耗内存),硬盘=1TB SSD(便携性优先),显卡=Intel Iris Xe(集成显卡)
客户端只需选择 “具体建造者”,无需关心构建步骤,即可获取定制化电脑:
public class Client {
public static void main(String[] args) {
// 1. 创建指挥者
ComputerDirector director = new ComputerDirector();
// 2. 定制游戏本(选择游戏本建造者)
ComputerBuilder gamingBuilder = new GamingLaptopBuilder();
Computer gamingLaptop = director.construct(gamingBuilder);
gamingLaptop.showConfig(); // 输出:游戏本配置
// 3. 定制轻薄本(选择轻薄本建造者)
ComputerBuilder ultrabookBuilder = new UltrabookBuilder();
Computer ultrabook = director.construct(ultrabookBuilder);
ultrabook.showConfig(); // 输出:轻薄本配置
}
}
运行结果:
电脑配置:CPU=Intel i9-13900HX(高性能CPU),内存=32GB DDR5(高带宽内存),硬盘=2TB SSD(高速硬盘),显卡=NVIDIA RTX 4080(高性能显卡)
电脑配置:CPU=Intel i5-1335U(低功耗CPU),内存=16GB LPDDR5(低功耗内存),硬盘=1TB SSD(便携性优先),显卡=Intel Iris Xe(集成显卡)
四、建造者模式的适用场景
并非所有对象创建都需要建造者模式,以下场景最适合使用:
- 复杂对象的定制化构建:对象由多个部件组成,且部件配置、组合顺序存在多种变化(如定制电脑、生成个性化报告、构建汽车)。
- 需要隐藏构建细节:客户端无需知道对象的具体构建步骤,只需获取最终结果(如用户无需知道电脑 “先装 CPU 还是先装内存”)。
- 同一构建过程生成不同表示:通过更换具体建造者,可让同一指挥者(步骤顺序)生成不同配置的对象(如同一组装流程,既做游戏本也做轻薄本)。
并非所有对象创建都需要建造者模式,以下场景最适合使用:
- 复杂对象的定制化构建:对象由多个部件组成,且部件配置、组合顺序存在多种变化(如定制电脑、生成个性化报告、构建汽车)。
- 需要隐藏构建细节:客户端无需知道对象的具体构建步骤,只需获取最终结果(如用户无需知道电脑 “先装 CPU 还是先装内存”)。
五、建造者模式的优缺点
优点
- 灵活性高:支持对对象部件的精细化定制,轻松扩展新的具体建造者(如新增 “工作站电脑建造者”,无需修改原有代码)。
- 代码清晰:将复杂构建逻辑拆分为独立步骤,职责单一,便于维护(构建步骤由指挥者管理,部件配置由建造者管理)。
- 解耦性强:客户端与具体构建步骤、部件配置分离,降低代码耦合度。
- 灵活性高:支持对对象部件的精细化定制,轻松扩展新的具体建造者(如新增 “工作站电脑建造者”,无需修改原有代码)。
缺点
- 增加类数量:每个具体产品需对应一个具体建造者,若产品类型过多,会导致类数量激增(如电脑有 10 种型号,需 10 个具体建造者)。
- 不适用于简单对象:若对象仅由少数部件组成(如 “用户” 对象仅含姓名、年龄),使用建造者模式会显得冗余,不如直接 new 对象高效。
- 增加类数量:每个具体产品需对应一个具体建造者,若产品类型过多,会导致类数量激增(如电脑有 10 种型号,需 10 个具体建造者)。
六、建造者模式与工厂模式对比表
建造者模式与工厂模式虽同属 “创建型模式”,但核心意图和适用场景差异显著,以下是两者的关键对比:
对比维度 | 建造者模式(Builder Pattern) | 工厂模式(Factory Pattern) |
---|---|---|
核心意图 | 关注 “如何构建”:拆分复杂对象的构建步骤,定制部件细节 | 关注 “创建什么”:统一创建对象,隐藏实例化逻辑 |
产品复杂度 | 适用于复杂对象(由多个部件组成,需分步构建) | 适用于简单 / 标准化对象(单一完整对象,无需分步) |
客户端控制度 | 客户端可控制部件配置(选择不同建造者) | 客户端仅控制产品类型(告诉工厂 “要什么”,不关心细节) |
角色构成 | 产品、抽象建造者、具体建造者、指挥者(4 个角色) | 产品、抽象工厂、具体工厂(3 个角色,无指挥者) |
典型场景 | 定制电脑、组装汽车、生成个性化报告 | 生产标准化产品(如不同品牌的手机、不同类型的日志器) |
类比生活场景 | 按需求定制家具(选材质、定尺寸,分步组装) | 从工厂批量购买标准化家电(直接拿成品,不关心生产) |
来源:juejin.cn/post/7543448572341157927
用户请求满天飞,如何精准『导航』?聊聊流量路由那些事儿
嘿,各位未来的技术大佬们,我是老码小张。
不知道大家有没有遇到过这样的场景:你美滋滋地打开刚部署上线的应用 cool-app.com
,在国内访问速度飞快。结果第二天,海外的朋友跟你吐槽,说访问你的应用慢得像蜗牛。或者更糟,某个区域的用户突然反馈说服务完全访问不了了!这时候你可能会挠头:用户来自天南海北,服务器也可能部署在不同地方,我怎么才能让每个用户都能又快又稳地访问到我的服务呢?
别慌!这其实就是咱们今天要聊的互联网流量路由策略要解决的问题。搞懂了它,你就掌握了给网络请求“精准导航”的秘诀,让你的应用在全球范围内都能提供更好的用户体验。
流量路由:不止是 DNS 解析那么简单
很多初级小伙伴可能觉得,用户访问网站不就是 浏览器 -> DNS 查询 IP -> 连接服务器
嘛?没错,DNS 是第一步,但现代互联网应用远不止这么简单。特别是当你的服务需要部署在多个数据中心、覆盖不同地理区域的用户时,仅仅返回一个固定的 IP 地址是远远不够的。
我们需要更智能的策略,来决定当用户请求 cool-app.com
时,DNS 服务器应该返回哪个(或哪些)IP 地址。这就引出了各种路由策略(Routing Policies)。你可以把它们想象成 DNS 服务器里的“智能导航系统”,根据不同的规则把用户导向最合适的目的地。
下面,咱们就来盘点几种最常见也最实用的路由策略。
策略一:按地理位置『就近安排』 (Geolocation Routing)
这是最直观的一种策略。顾名思义,它根据用户请求来源的 IP 地址,判断用户的地理位置(比如国家、省份甚至城市),然后将用户导向物理位置上距离最近或者预设好的对应区域的服务器。
工作原理示意:
sequenceDiagram
participant User as 用户 (来自北京)
participant DNS as 智能 DNS 服务器
participant ServerCN as 北京服务器 (1.1.1.1)
participant ServerUS as 美国服务器 (2.2.2.2)
User->>DNS: 查询 cool-app.com 的 IP 地址
activate DNS
DNS-->>DNS: 分析来源 IP,判断用户在北京
DNS-->>User: 返回北京服务器 IP (1.1.1.1)
deactivate DNS
User->>ServerCN: 连接 1.1.1.1
啥时候用?
- 需要为特定地区用户提供本地化内容或服务。
- 有合规性要求,比如某些数据必须存储在用户所在国家境内(像 GDPR)。
- 希望降低跨区域访问带来的延迟。
简单配置示例(伪代码):
// 类似 AWS Route 53 或其他云 DNS 的配置逻辑
RoutingPolicy {
Type: Geolocation,
Rules: [
{ Location: '中国', TargetIP: '1.1.1.1' },
{ Location: '美国', TargetIP: '2.2.2.2' },
{ Location: '*', TargetIP: '3.3.3.3' } // * 代表默认,匹配不到具体位置时使用
]
}
策略二:追求极致速度的『延迟优先』 (Latency-Based Routing)
这个策略的目标是:快! 它不关心用户在哪儿,只关心用户访问哪个服务器的网络延迟最低(也就是 RTT,Round-Trip Time 最短)。DNS 服务商会持续监测从全球不同网络到你各个服务器节点的网络延迟,然后把用户导向响应最快的那个节点。
工作原理示意:
sequenceDiagram
participant User as 用户
participant DNS as 智能 DNS 服务器
participant ServerA as 服务器 A (东京)
participant ServerB as 服务器 B (新加坡)
User->>DNS: 查询 cool-app.com 的 IP 地址
activate DNS
DNS-->>DNS: 检测用户到各服务器的延迟 (到 A: 50ms, 到 B: 30ms)
DNS-->>User: 返回延迟最低的服务器 B 的 IP
deactivate DNS
User->>ServerB: 连接服务器 B
啥时候用?
- 对响应速度要求极高的应用,比如在线游戏、实时通讯。
- 全球用户分布广泛,希望动态地为每个用户找到最快接入点。
注意点: 延迟是动态变化的,所以这种策略依赖于 DNS 服务商持续、准确的延迟探测。
策略三:灵活调度的『按权重分配』 (Weighted Routing)
这种策略允许你给不同的服务节点分配不同的权重(百分比),DNS 服务器会按照你设定的比例,把用户的请求随机分配到这些节点上。
工作原理示意:
假设你有两个版本的服务 V1 和 V2,部署在不同的服务器组上。
// 类似云 DNS 配置
RoutingPolicy {
Type: Weighted,
Targets: [
{ TargetIPGr0up: 'V1_Servers', Weight: 90 }, // 90% 流量到 V1
{ TargetIPGr0up: 'V2_Servers', Weight: 10 } // 10% 流量到 V2
]
}
DNS 会根据这个权重,概率性地返回 V1 或 V2 服务器组的 IP。
啥时候用?
- A/B 测试:想测试新功能?分一小部分流量(比如 5%)到新版本,看看效果。
- 灰度发布/金丝雀发布:新版本上线,先给 1% 的用户试试水,没问题再逐步增加权重到 10%、50%、100%。稳!
- 负载均衡:如果你的服务器配置不同(比如有几台是高性能的,几台是普通配置的),可以按性能分配不同权重,让高性能机器承担更多流量。
策略四:保障高可用的『故障转移』 (Failover Routing)
这个策略是为了高可用性。你需要设置一个主服务节点和一个或多个备用节点。DNS 服务器会持续对主节点进行健康检查(比如探测端口是否存活、HTTP 接口是否返回 200 OK)。
- 正常情况:所有流量都导向主节点。
- 主节点挂了:DNS 检测到主节点 N 次健康检查失败后,会自动把流量切换到备用节点。
- 主节点恢复:一旦主节点恢复健康,流量可以自动切回来(取决于你的配置)。
工作原理示意:
graph LR
A[用户请求] --> B{DNS 健康检查};
B -- 主节点健康 --> C[主服务器];
B -- 主节点故障 --> D[备用服务器];
C --> E[提供服务];
D --> E;
啥时候用?
- 任何对可用性要求高的关键服务。谁也不想服务宕机了用户还一直往坏掉的服务器上撞吧?
- 实现基本的灾备能力。
关键点: 健康检查的配置(频率、失败阈值)和 DNS 记录的 TTL(Time-To-Live,缓存时间)设置很关键。TTL 太长,故障切换就不够及时;TTL 太短,会增加 DNS 查询压力和成本。需要权衡。
策略五:CDN 和大厂最爱『任播』 (Anycast)
Anycast 稍微特殊一点,它通常是在更底层的网络层面(BGP 路由协议)实现的,但 DNS 经常与之配合。简单来说,就是你用同一个 IP 地址在全球多个地点宣告你的服务。用户的请求会被沿途的网络路由器自动导向“网络距离”上最近的那个宣告了该 IP 的节点。
效果: 用户感觉就像是连接到了离他最近的“入口”。
啥时候用?
- CDN 服务:为什么你访问各大 CDN 厂商(如 Cloudflare, Akamai)的资源总是很快?Anycast 是核心技术之一,让用户从最近的边缘节点获取内容。
- 公共 DNS 服务:像 Google 的
8.8.8.8
和 Cloudflare 的1.1.1.1
都使用了 Anycast,你在全球任何地方 ping 这个 IP,响应的都是离你最近的数据中心。
对于应用开发者来说,你可能不会直接配置 BGP,但你会选择使用提供了 Anycast 网络的服务商(比如某些云厂商的负载均衡器或 CDN 服务)。
选哪个?一张表帮你捋清楚
这么多策略,到底该怎么选呢?别急,我给你整理了个表格,对比一下:
策略名称 | 核心原理 | 主要应用场景 | 优点 | 缺点 |
---|---|---|---|---|
地理位置路由 | 基于用户 IP 判断地理位置 | 本地化内容、合规性、区域优化 | 实现区域隔离、满足合规 | IP 库可能不准、无法反映真实网络状况 |
延迟路由 | 基于网络延迟 (RTT) | 追求最低访问延迟、全球性能优化 | 用户体验好、动态适应网络变化 | 依赖准确探测、成本可能较高 |
权重路由 | 按预设比例分配流量 | A/B 测试、灰度发布、按能力负载均衡 | 灵活控制流量分配、上线平稳 | 无法基于用户体验动态调整(除非结合其他策略) |
故障转移路由 | 健康检查 + 主备切换 | 高可用、灾备 | 提升服务可靠性、自动化故障处理 | 切换有延迟(受 TTL 和检查频率影响) |
任播 (Anycast) | 同一 IP 多点宣告,网络路由就近转发 | CDN、公共 DNS、全球入口优化 | 显著降低延迟、抵抗 DDoS 攻击(分散) | 配置复杂(通常由服务商提供)、成本高 |
实战经验分享:组合拳出奇效!
在实际项目中,我们很少只用单一策略。更常见的是打组合拳:
- 地理位置 + 故障转移:先按区域分配流量(比如中国用户到上海,美国用户到硅谷),然后在每个区域内部署主备服务器,使用故障转移策略保障区域内的高可用。这是很多应用的标配。
- 地理位置 + 权重路由:在一个特定的地理区域内(比如只在中国区),对新上线的服务 V2 使用权重路由进行灰度发布。
- Anycast + 后端智能路由:使用 Anycast IP 作为全球统一入口,流量到达最近的接入点后,再根据后端服务的实际负载、延迟等情况,通过内部的负载均衡器或服务网格(Service Mesh)进行更精细的二次路由。
别忘了监控! 无论你用哪种策略,监控都至关重要。你需要关注:
- 各节点的健康状况。
- 用户的实际访问延迟(可以用 RUM - Real User Monitoring)。
- DNS 解析成功率和解析耗时。
- 流量分布是否符合预期。
有了监控数据,你才能知道你的路由策略是否有效,是否需要调整。
好了,今天关于互联网流量路由策略就先和大家聊这么多。希望这些内容能帮助你理解,当用户的请求“满天飞”时,我们是如何通过这些“智能导航”技术,确保他们能又快又稳地到达目的地的。这不仅仅是运维同学的事,作为开发者,理解这些原理,能让你在设计和部署应用时考虑得更周全。
我是老码小张,一个喜欢研究技术原理,并且在实践中不断成长的技术人。如果你觉得这篇文章对你有帮助,或者有什么想法想交流,欢迎在评论区留言!咱们下次再见!
来源:juejin.cn/post/7498292516493656098
提升React移动端开发效率:Vant组件库
在React中使用Vant组件库的指南
Vant是一套轻量、可靠的移动端组件库,特别适合在React项目中使用。本文将详细介绍如何在React项目中集成和使用Vant组件库,并通过Button按钮和NavBar导航栏等常用组件作为示例,展示其基本用法和高级特性。
一、Vant简介与安装
1.1 Vant是什么
Vant是由有赞前端团队开发的一套基于Vue的移动端组件库,后来也推出了React版本(Vant React)。它提供了60+高质量组件,覆盖了移动端开发的大部分场景,具有以下特点:
- 性能极佳:组件经过精心优化,运行流畅
- 样式美观:遵循统一的设计语言,视觉效果出色
- 功能丰富:提供大量实用组件和灵活配置
- 文档完善:中文文档详细,示例丰富
- 社区活跃:GitHub上star数高,问题响应快
1.2 安装Vant
在React项目中使用Vant前,需要先安装它。确保你已经创建了一个React项目(可以使用create-react-app或其它脚手架工具),然后在项目目录下执行:
bash
npm install vant --save
# 或者使用yarn
yarn add vant
1.3 引入组件样式
Vant的样式文件需要单独引入。推荐在项目的入口文件(通常是src/index.js或src/App.js)中添加以下代码:
jsx
import 'vant/lib/index.css';
这一步非常重要,否则组件将没有样式效果。
二、Button按钮组件使用详解
Button是Vant中最基础也是最常用的组件之一,下面详细介绍它的使用方法。
2.1 基本用法
首先引入Button组件:
jsx
import { Button } from 'vant';
然后在你的组件中使用:
jsx
function MyComponent() {
return (
<div>
<Button type="primary">主要按钮</Button>
<Button type="info">信息按钮</Button>
<Button type="default">默认按钮</Button>
</div>
);
}
2.2 按钮类型
Vant提供了多种按钮类型,通过type
属性来设置:
primary
: 主要按钮,蓝色背景success
: 成功按钮,绿色背景danger
: 危险按钮,红色背景warning
: 警告按钮,橙色背景default
: 默认按钮,灰色背景info
: 信息按钮,浅蓝色背景
jsx
<Button type="success">成功按钮</Button>
<Button type="danger">危险按钮</Button>
<Button type="warning">警告按钮</Button>
2.3 按钮形状
除了类型,还可以设置按钮的形状:
- 方形按钮(默认)
- 圆形按钮:添加
round
属性 - 圆角按钮:添加
square
属性
jsx
<Button round>圆形按钮</Button>
<Button square>圆角按钮</Button>
2.4 按钮尺寸
Vant提供了三种尺寸的按钮:
- 大号按钮:
size="large"
- 普通按钮(默认)
- 小号按钮:
size="small"
- 迷你按钮:
size="mini"
jsx
<Button size="large">大号按钮</Button>
<Button size="small">小号按钮</Button>
<Button size="mini">迷你按钮</Button>
2.5 按钮状态
按钮有不同的状态,可以通过以下属性控制:
- 禁用状态:
disabled
- 加载状态:
loading
- 朴素按钮:
plain
(边框样式)
jsx
<Button disabled>禁用按钮</Button>
<Button loading>加载中...</Button>
<Button plain>朴素按钮</Button>
2.6 按钮图标
可以在按钮中添加图标,使用icon
属性:
jsx
import { Icon } from 'vant';
<Button icon="plus">添加</Button>
<Button icon="search">搜索</Button>
<Button icon={<Icon name="like" />}>点赞</Button>
Vant内置了大量图标,可以在官方文档中查看所有可用图标。
2.7 按钮事件
按钮最常用的就是点击事件:
jsx
function handleClick() {
console.log('按钮被点击了');
}
<Button onClick={handleClick}>点击我</Button>
三、NavBar导航栏组件使用详解
NavBar是移动端常用的顶部导航栏组件,下面详细介绍它的使用方法。
3.1 基本用法
首先引入NavBar组件:
jsx
import { NavBar } from 'vant';
然后在你的组件中使用:
jsx
function MyComponent() {
return (
<NavBar
title="标题"
leftText="返回"
rightText="按钮"
leftArrow
/>
);
}
3.2 主要属性
NavBar组件的主要属性包括:
title
: 导航栏标题leftText
: 左侧文字rightText
: 右侧文字leftArrow
: 是否显示左侧箭头fixed
: 是否固定在顶部placeholder
: 是否生成一个等高的占位元素(配合fixed使用)border
: 是否显示下边框zIndex
: 设置z-index
jsx
<NavBar
title="个人中心"
leftText="返回"
rightText="设置"
leftArrow
fixed
placeholder
border
zIndex={100}
/>
3.3 自定义内容
除了使用属性,还可以通过插槽自定义导航栏内容:
jsx
<NavBar>
<template #left>
<Icon name="arrow-left" /> 返回
</template>
<template #title>
<span style={{ color: 'red' }}>自定义标题</span>
</template>
<template #right>
<Icon name="search" />
<Icon name="more-o" style={{ marginLeft: '10px' }} />
</template>
</NavBar>
3.4 事件处理
NavBar组件提供了以下事件:
click-left
: 点击左侧区域时触发click-right
: 点击右侧区域时触发
jsx
function handleClickLeft() {
console.log('点击了左侧');
// 通常用于返回上一页
// history.goBack();
}
function handleClickRight() {
console.log('点击了右侧');
// 可以打开设置页面等
}
<NavBar
title="事件示例"
leftText="返回"
rightText="设置"
leftArrow
onClickLeft={handleClickLeft}
onClickRight={handleClickRight}
/>
3.5 配合路由使用
在实际项目中,NavBar通常需要配合路由使用:
jsx
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleBack = () => {
navigate(-1); // 返回上一页
};
const handleToSettings = () => {
navigate('/settings'); // 跳转到设置页
};
return (
<NavBar
title="路由示例"
leftText="返回"
rightText="设置"
leftArrow
onClickLeft={handleBack}
onClickRight={handleToSettings}
/>
);
}
四、高级用法与注意事项
4.1 主题定制
Vant支持主题定制,可以通过CSS变量来修改主题样式。在项目的全局CSS文件中添加:
css
:root {
--van-primary-color: #ff6a00; /* 修改主题色为橙色 */
--van-border-radius: 8px; /* 修改圆角大小 */
--van-nav-bar-height: 60px; /* 修改导航栏高度 */
}
更多可定制的CSS变量可以参考官方文档。
4.2 按需引入
如果担心引入全部组件会增加包体积,可以使用按需引入的方式。首先安装babel插件:
bash
npm install babel-plugin-import --save-dev
然后在babel配置中添加:
json
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}, "vant"]
]
}
之后就可以按需引入组件了:
jsx
import { Button, NavBar } from 'vant';
这种方式只会打包你实际使用的组件,可以有效减小最终打包体积。
4.3 国际化支持
Vant支持多语言,可以通过LocaleProvider组件设置:
jsx
import { LocaleProvider, Button } from 'vant';
import enUS from 'vant/es/locale/lang/en-US';
function App() {
return (
<LocaleProvider locale={enUS}>
<Button>Submit</Button>
</LocaleProvider>
);
}
4.4 常见问题与解决方案
- 样式不生效:确保已经正确引入了Vant的样式文件
- 组件未定义:检查组件名称拼写是否正确,是否已经正确引入
- TypeScript支持:Vant提供了完整的TypeScript类型定义,可以直接在TS项目中使用
- 移动端适配:建议在项目中同时使用postcss-pxtorem或postcss-px-to-viewport等插件进行移动端适配
五、总结
通过合理使用Vant组件库,可以显著提高React移动端应用的开发效率,同时保证UI的一致性和美观性。建议读者在实际项目中多加练习,掌握更多组件的使用方法。
Vant还提供了许多其他实用组件,如Toast轻提示、Dialog弹出框、List列表等,都可以在官方文档中找到详细的使用说明。
来源:juejin.cn/post/7531667016286863394
transform、translate、transition分别是什么属性,CSS中常用的实现动画方式
transform、translate、transition分别是什么属性,CSS中常用的实现动画方式
在 CSS 中,transform
、translate
和 transition
是用于实现元素变换和动画的重要属性。它们各自有不同的作用,通常结合使用可以实现丰富的动画效果。
1. 属性详解
1.1 transform
- 作用:用于对元素进行 2D 或 3D 变换,如旋转、缩放、倾斜、平移等。
- 常用函数:
translate(x, y)
:平移元素。rotate(angle)
:旋转元素。scale(x, y)
:缩放元素。skew(x-angle, y-angle)
:倾斜元素。matrix(a, b, c, d, e, f)
:定义 2D 变换矩阵。
- 示例:
.box {
transform: translate(50px, 100px) rotate(45deg) scale(1.5);
}
1.2 translate
- 作用:
translate
是transform
的一个函数,用于平移元素。 - 语法:
translate(x, y)
:水平方向移动x
,垂直方向移动y
。translateX(x)
:仅水平方向移动。translateY(y)
:仅垂直方向移动。translateZ(z)
:在 3D 空间中沿 Z 轴移动。
- 示例:
.box {
transform: translate(50px, 100px);
}
1.3 transition
- 作用:用于定义元素在样式变化时的过渡效果。
- 常用属性:
transition-property
:指定需要过渡的属性(如all
、opacity
、transform
等)。transition-duration
:指定过渡的持续时间(如1s
、500ms
)。transition-timing-function
:指定过渡的速度曲线(如ease
、linear
、ease-in-out
)。transition-delay
:指定过渡的延迟时间(如0.5s
)。
- 简写语法:
transition: property duration timing-function delay;
- 示例:
.box {
transition: transform 0.5s ease-in-out, opacity 0.3s linear;
}
2. CSS 中常用的实现动画方式
2.1 使用 transition
实现简单动画
- 适用场景:适用于简单的状态变化动画(如 hover 效果)。
- 示例:
.box {
width: 100px;
height: 100px;
background-color: lightblue;
transition: transform 0.5s ease-in-out;
}
.box:hover {
transform: scale(1.2) rotate(45deg);
}
2.2 使用 @keyframes
和 animation
实现复杂动画
- 适用场景:适用于复杂的多帧动画。
- 步骤:
- 使用
@keyframes
定义动画关键帧。 - 使用
animation
属性将动画应用到元素上。
- 使用
- 示例:
@keyframes slideIn {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
.box {
width: 100px;
height: 100px;
background-color: lightblue;
animation: slideIn 1s ease-in-out;
}
2.3 使用 transform
和 transition
结合实现交互效果
- 适用场景:适用于用户交互触发的动画(如点击、悬停)。
- 示例:
.box {
width: 100px;
height: 100px;
background-color: lightblue;
transition: transform 0.3s ease-in-out;
}
.box:active {
transform: scale(0.9);
}
2.4 使用 will-change
优化动画性能
- 作用:提前告知浏览器元素将会发生的变化,以优化渲染性能。
- 示例:
.box {
will-change: transform;
}
3. 综合示例
示例 1:按钮点击效果
.button {
padding: 10px 20px;
background-color: lightblue;
border: none;
transition: transform 0.2s ease-in-out;
}
.button:active {
transform: scale(0.95);
}
示例 2:卡片翻转动画
.card {
width: 200px;
height: 200px;
position: relative;
perspective: 1000px;
}
.card-inner {
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.card:hover .card-inner {
transform: rotateY(180deg);
}
.card-front, .card-back {
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
}
.card-front {
background-color: lightblue;
}
.card-back {
background-color: lightcoral;
transform: rotateY(180deg);
}
示例 3:加载动画
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
总结
属性/方法 | 作用 | 适用场景 |
---|---|---|
transform | 对元素进行 2D/3D 变换 | 平移、旋转、缩放、倾斜等 |
translate | transform 的一个函数,用于平移元素 | 移动元素位置 |
transition | 定义元素样式变化的过渡效果 | 简单的状态变化动画 |
@keyframes | 定义动画关键帧 | 复杂的多帧动画 |
animation | 将@keyframes 定义的动画应用到元素上 | 复杂的多帧动画 |
will-change | 优化动画性能 | 性能优化 |
通过灵活运用这些属性和方法,可以实现丰富的动画效果,提升用户体验。
更多vue相关插件及后台管理模板可访问vue admin reference,代码详情请访问github
来源:juejin.cn/post/7480766452653260852
尝试解决 Android 适配最后一公里
框架介绍
- Android 碎片化至始至终是一个令人非常头疼的问题,特别为 XXPermissions 上面为不同的厂商做适配的时候就非常头疼,因为市面上能找到的开源库只能判断机型的品牌,而不能判断 Android 厂商定制的系统类型,用机型的品牌去做适配会导致出现误判的情况,例如在小米手机上面运行的厂商系统不一定是 MIUI 或者 HyperOS,也有可能是被人刷成了 Android 原生的系统或者其他,反过来也一样,我时常在想,要是有这么一个工具就好了,可以判断 Android 厂商系统的类型及获取厂商系统的版本号,这样就很方便我们做 Android 适配,于是 DeviceCompat 就诞生了,可以轻松识别各种国内外手机厂商和系统版本,帮助大家解决 Android 适配最后一公里的问题。
- 截至到目前,我是行业内第一个也是唯一一个开源这套方案的人,在这里先感谢网上的同行分享的各种方法和思路,让我在开发的过程中少走了很多弯路,另外我也很能理解为什么行业内一直没有人愿意站出来开源这种框架,因为过程非常麻烦,这不仅仅是一个技术问题,还是一个苦活,因为要针对成千上万的机型进行适配。
框架亮点
- 支持识别各种定制 Android 系统(HarmonyOS、MagicOS、MIUI、HyperOS、ColorOS、OriginOS 等)
- 支持判断多种手机厂商品牌(华为、小米、OPPO、vivo、三星等)
- 使用简单,一行代码即可判断设备品牌、厂商系统类型、厂商系统版本
- 兼容性好,支持 Android 4.0 及以上系统
- 体积小巧,仅 12 KB,不会增加应用体积负担
集成步骤
- 如果你的项目 Gradle 配置是在
7.0
以下,需要在build.gradle
文件中加入
allprojects {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
- 如果你的 Gradle 配置是
7.0
及以上,则需要在settings.gradle
文件中加入
dependencyResolutionManagement {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
- 配置完远程仓库后,在项目 app 模块下的
build.gradle
文件中加入远程依赖
dependencies {
// 设备兼容框架:https://github.com/getActivity/DeviceCompat
implementation 'com.github.getActivity:DeviceCompat:1.0'
}
框架 API 介绍
- 判断系统类型
// 判断当前设备的厂商系统是否为 HyperOS(小米新系统)
DeviceOs.isHyperOs();
// 判断当前设备的厂商系统是否为国内版本的 HyperOS
DeviceOs.isHyperOsByChina();
// 判断当前设备的厂商系统是否为国际版本的 HyperOS
DeviceOs.isHyperOsByGlobal();
// 判断当前设备的厂商系统开启了 HyperOS 的系统优化选项
DeviceOs.isHyperOsOptimization();
// 判断当前设备的厂商系统是否为 MIUI(小米老系统)
DeviceOs.isMiui();
// 判断当前设备的厂商系统是否为国内版本的 MIUI
DeviceOs.isMiuiByChina();
// 判断当前设备的厂商系统是否为国际版本的 MIUI
DeviceOs.isMiuiByGlobal();
// 判断当前设备的厂商系统是否开启了 MIUI 优化选项
DeviceOs.isMiuiOptimization();
// 判断当前设备的厂商系统是否为 RealmeUI(真我系统)
DeviceOs.isRealmeUi();
// 判断当前设备的厂商系统是否为 ColorOS(OPPO 系统)
DeviceOs.isColorOs();
// 判断当前设备的厂商系统是否为 OriginOS(VIVO 系统)
DeviceOs.isOriginOs();
// 判断当前设备的厂商系统是否为 FuntouchOS(VIVO 的老系统)
DeviceOs.isFuntouchOs();
// 判断当前设备的厂商系统是否为 MagicOS(荣耀系统)
DeviceOs.isMagicOs();
// 判断当前设备的厂商系统是否为 HarmonyOS(华为鸿蒙的系统)
DeviceOs.isHarmonyOs();
// 判断当前设备的厂商系统是否为 EMUI(华为和荣耀的老系统)
DeviceOs.isEmui();
// 判断当前设备的厂商系统是否为 OneUI(三星系统)
DeviceOs.isOneUi();
// 判断当前设备的厂商系统是否为 OxygenOS(一加的老系统)
DeviceOs.isOxygenOs();
// 判断当前设备的厂商系统是否为 H2OS(一加的老系统)
DeviceOs.isH2Os();
// 判断当前设备的厂商系统是否为 Flyme(魅族系统)
DeviceOs.isFlyme();
// 判断当前设备的厂商系统是否为 MyOS(中兴或者努比亚的系统)
DeviceOs.isMyOs();
// 判断当前设备的厂商系统是否为 MifavorUI(中兴老系统)
DeviceOs.isMifavorUi();
// 判断当前设备的厂商系统是否为 SmartisanOS(锤子系统)
DeviceOs.isSmartisanOs();
// 判断当前设备的厂商系统是否为 EUI(乐视的系统)
DeviceOs.isEui();
// 判断当前设备的厂商系统是否为 ZUI(摩托罗拉的系统)
DeviceOs.isZui();
// 判断当前设备的厂商系统是否为 360UI(360 系统)
DeviceOs.is360Ui();
// 获取当前设备的厂商系统名称
DeviceOs.getOsName();
// 获取当前设备的厂商系统名称
DeviceOs.getOsName();
// 获取经过美化的厂商系统版本号
DeviceOs.getOsVersionName();
// 获取厂商系统版本的大版本号
DeviceOs.getOsBigVersionCode();
// 获取原始的厂商系统版本号
DeviceOs.getOriginalOsVersionName();
- 判断设备品牌
// 判断当前设备的品牌是否为华为
DeviceBrand.isHuaWei();
// 判断当前设备的品牌是否为荣耀
DeviceBrand.isHonor();
// 判断当前设备的品牌是否为 vivo
DeviceBrand.isVivo();
// 判断当前设备的品牌是否为小米
DeviceBrand.isXiaoMi();
// 判断当前设备的品牌是否为 OPPO
DeviceBrand.isOppo();
// 判断当前设备的品牌是否为真我
DeviceBrand.isRealMe();
// 判断当前设备的品牌是否为乐视
DeviceBrand.isLeEco();
// 判断当前设备的品牌是否为 360
DeviceBrand.is360();
// 判断当前设备的品牌是否为中兴
DeviceBrand.isZte();
// 判断当前设备的品牌是否为一加
DeviceBrand.isOnePlus();
// 判断当前设备的品牌是否为努比亚
DeviceBrand.isNubia();
// 判断当前设备的品牌是否为酷派
DeviceBrand.isCoolPad();
// 判断当前设备的品牌是否为 LG
DeviceBrand.isLg();
// 判断当前设备的品牌是否为 Google
DeviceBrand.isGoogle();
// 判断当前设备的品牌是否为三星
DeviceBrand.isSamsung();
// 判断当前设备的品牌是否为魅族
DeviceBrand.isMeiZu();
// 判断当前设备的品牌是否为联想
DeviceBrand.isLenovo();
// 判断当前设备的品牌是否为锤子
DeviceBrand.isSmartisan();
// 判断当前设备的品牌是否为 HTC
DeviceBrand.isHtc();
// 判断当前设备的品牌是否为索尼
DeviceBrand.isSony();
// 判断当前设备的品牌是否为金立
DeviceBrand.isGionee();
// 判断当前设备的品牌是否为摩托罗拉
DeviceBrand.isMotorola();
// 判断当前设备的品牌是否为传音
DeviceBrand.isTranssion();
// 获取当前设备的品牌名称
DeviceBrand.getBrandName();
- 系统属性相关的方法
// 获取单个系统属性值
SystemPropertyCompat.getSystemPropertyValue((@Nullable String key);
// 获取多个系统属性值
SystemPropertyCompat.getSystemPropertyValues(@Nullable String[] keys);
// 获取多个系统属性中的任一一个值
SystemPropertyCompat.getSystemPropertyAnyOneValue(@Nullable String[] keys);
// 判断某个系统属性是否存在
SystemPropertyCompat.isSystemPropertyExist(@Nullable String key);
// 判断多个系统属性是否有任一一个存在
SystemPropertyCompat.isSystemPropertyAnyOneExist(@Nullable String[] keys);
附上项目开源地址:DeviceCompat
来源:juejin.cn/post/7540524749425180735
Python StringIO 和 BytesIO 用法
在 Python 中,我们平时处理的输入输出(I/O)大多数是针对文件或网络数据的,但有时候,我们希望直接在内存中进行读写,而不必先把数据存到硬盘上。
这时候,StringIO
和 BytesIO
就派上用场了。
参考文章:Python StringIO 和 BytesIO | 简单一点学习 easyeasy.me
简单理解:
- StringIO → 操作内存中的 字符串(
str
类型) - BytesIO → 操作内存中的 二进制数据(
bytes
类型)
它们都来自 io
模块,接口和文件对象几乎一模一样,所以学会文件操作就能直接用它们。
1. 为什么要用 StringIO / BytesIO
- 无需磁盘 I/O,速度快
- 便于测试(不需要真实文件)
- 在网络编程中常用(比如 HTTP 响应先存在内存中再处理)
- 数据临时存储(比如生成报告、图片)
2. StringIO 基础用法
StringIO
适合处理 文本数据,使用方式类似普通文件:
from io import StringIO
# 创建 StringIO 对象
f = StringIO()
# 写入数据
f.write("Hello ")
f.write("Python")
# 获取当前内容
print(f.getvalue()) # Hello Python
注意:
getvalue()
用来获取缓冲区的全部数据。
3. StringIO 读取数据
我们也可以直接用 StringIO
来模拟读取文件:
from io import StringIO
data = "Line1\nLine2\nLine3"
f = StringIO(data)
# 一次性读取
print(f.read())
# 重置游标
f.seek(0)
# 按行读取
for line in f:
print(line.strip())
要点:
seek(0)
→ 把“文件指针”移动到开头- 读取方法(
read
、readline
、readlines
)和文件对象一致
4. BytesIO 基础用法
BytesIO
用于处理 二进制数据,比如图片、音频、压缩包等。
from io import BytesIO
# 创建 BytesIO
f = BytesIO()
# 写入二进制数据
f.write(b"Hello World")
# 获取数据
print(f.getvalue()) # b'Hello World'
5. BytesIO 读取数据
from io import BytesIO
data = b"Binary\x00Data"
f = BytesIO(data)
# 读取前6个字节
print(f.read(6)) # b'Binary'
# 继续读剩下的
print(f.read()) # b'\x00Data'
6. StringIO vs BytesIO 区别
特性 | StringIO | BytesIO |
---|---|---|
处理数据类型 | str (文本) | bytes (二进制) |
读取写入方式 | 和文本文件一致 | 和二进制文件一致 |
编码解码 | 不需要手动编码 | 需要自己 .encode() / .decode() |
常见应用 | 日志处理、临时文本 | 图片、音频、网络传输数据 |
7. 从文件到内存的转换
有时我们需要把磁盘文件内容读到内存中用 BytesIO
处理,比如处理图片:
from io import BytesIO
# 假设有个图片
with open("test.png", "rb") as f:
data = f.read()
# 存入 BytesIO
bio = BytesIO(data)
# 读取前10个字节
print(bio.read(10))
8. 从内存保存到文件
反过来,我们也可以把 BytesIO
或 StringIO
的数据写到磁盘:
from io import BytesIO
# 创建内存数据
bio = BytesIO()
bio.write(b"Save me to file")
# 写入文件
with open("output.bin", "wb") as f:
f.write(bio.getvalue())
9. 常见使用场景
- 临时构建数据,避免磁盘 I/O
- 测试代码时,模拟文件对象
- 网络数据处理(HTTP 请求、Socket 传输)
- 图片或音频处理(配合 Pillow、pydub 等库)
10. 最佳实践
- 如果处理的是文本,用
StringIO
,并且直接写str
类型。 - 如果处理的是二进制数据(比如图片、压缩包),用
BytesIO
,并且确保写入的是bytes
。 - 操作完成后不一定非要
close()
(因为它们是内存对象),但为了习惯最好加上。 - 需要频繁读取时,记得用
seek(0)
重置指针。
来源:juejin.cn/post/7536182225328046122
摆动序列
摆动序列
一、问题描述
LeetCode:376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如,[1, 7, 4, 9, 2, 5]
是一个摆动序列,因为差值 (6, -3, 5, -7, 3)
是正负交替出现的。
相反,[1, 4, 7, 2, 5]
和 [1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数数组 nums
,返回 nums
中作为摆动序列的最长子序列的长度。
二、解题思路
摆动序列的关键在于寻找数组中的峰和谷。每当序列发生方向变化时,摆动序列的长度就会增加。因此,可以通过遍历数组,统计方向变化的次数来得到最长摆动序列的长度。
- 记录初始趋势:计算数组前两个元素的差值作为最开始的变化趋势preTrend = nums[1] - nums[0],若差值不为 0,说明前两个元素构成了摆动序列的初始趋势,此时摆动序列长度初始化为 2;若差值为 0,意味着前两个元素相等,不构成摆动趋势,摆动序列长度初始化为 1。
- 遍历数组寻找变化趋势:记录当前变化趋势curTrend = nums[i] - nums[i - 1],若当前变化趋势curTrend 与之前的变化趋势preTrend 不同,preTrend <= 0 && curTrend > 0 或者 preTrend >= 0 && curTrend < 0 时 更新变化趋势preTrend ,摆动序列加1
三、代码实现
以下是使用 JavaScript 实现的代码:
var wiggleMaxLength = function (nums) {
// 统计波峰波谷的数量
// 若长度为1 或为 0
if (nums.length < 2) return nums.length;
let preTrend = nums[1] - nums[0];
let reLen = preTrend !== 0 ? 2 : 1;
for (let i = 2; i < nums.length; i++) {
let curTrend = nums[i] - nums[i - 1];
// 当出现波谷或者波峰时,更新preTrend
if ((preTrend <= 0 && curTrend > 0) || (preTrend >= 0 && curTrend < 0)) {
preTrend = curTrend;
reLen++;
}
}
return reLen;
};
四、注意事项
- 边界条件需谨慎:在处理数组前两个元素确定初始趋势时,要特别注意数组长度为 2 的情况。若两个元素相等,初始化摆动序列长度为 1,此时不能因为后续没有更多元素判断趋势变化,就错误认为长度还能增加。在遍历过程中,若遇到数组结尾,也应保证最后一次趋势变化能正确统计,避免遗漏。
- 趋势判断避免误判:在比较差值判断趋势变化时,条件 (preTrend <= 0 && curTrend > 0) 与 (preTrend >= 0 && curTrend < 0) 中的 “小于等于” 和 “大于等于” 不能随意替换为 “小于” 和 “大于”。例如,当出现连续相等元素后趋势改变的情况,若使用严格的 “小于” 和 “大于” 判断,可能会错过第一个有效趋势变化点,导致结果错误。
五、复杂度分析
- 时间复杂度:O(n),其中 n 是数组的长度。只需要遍历一次数组。
- 空间复杂度:O(1),只需要常数级的额外空间。
来源:juejin.cn/post/7518198430662492223
绿盟科技重磅发布AI安全运营新成果,全面驱动智能攻防体系升级
8月29日,绿盟科技在北京成功举办以“智御新境·安全无界”为主题的AI赋能安全运营线上成果发布会,全面展示了公司在AI安全领域的最新技术成果与实践经验。
会议总结了“风云卫”AI安全能力平台上半年在客户侧的实际运营成效,介绍了AISOC平台的新特性与能力,进一步验证了“AI+安全运营”在降噪、研判、处置闭环以及未知威胁检测等核心场景中的规模化应用价值。
此外,还正式发布了“绿盟AI智能化渗透系统”,该系统依托AI技术全面赋能渗透测试全流程,可广泛应用于常态化扫描和日常安全运维等场景,有效帮助客户降本增效,显著降低渗透测试的专业门槛。
双底座多智能体架构,构建三位一体AI安全生态
2020年至2022年,绿盟科技连续发布三部AI安全白皮书《安全AI赋能安全运营白皮书》、《知识图谱白皮书》和《AI安全分析白皮书》,并于2023年推出“风云卫”安全大模型,深度融合AI与攻防知识。2025年,公司再度升级,构建“风云卫+DeepSeek”双底座与多智能体架构,打造AI驱动的安全生态,覆盖模型生产、场景适配与应用复制三大层面,全面提升安全检测、运营、数据安全及蓝军对抗能力,实现全域智能赋能。
安全运营实现“智防”质变,绿盟“风云卫”AI实战成效显著
绿盟科技产品BG总经理吴天昊
绿盟科技产品BG总经理吴天昊表示,安全运营人员每天面临几万到几十万不等的原始攻击告警,绿盟“风云卫”AI安全能力平台依托千亿级安全样本训练的大模型,能够自动识别系统日志中的无效告警与重复信息,达到百级左右的高价值告警的优先推荐。
针对不同攻击事件,可自动展开研判分析,精准解析攻击路径和手法,并通过可视化分析界面清晰呈现完整攻击链条。通过自主调查,智能开展横向溯源,自动关联跨端、跨网、跨身份数据,构建出完整的攻击图谱;同时进行并案分析,深度挖掘同类攻击线索,精准定位攻击组织;最后通过SOAR剧本自动生成与执行,实现分钟级事件闭环,并为未来同类事件建立自动化处置范式。
实际应用数据显示,绿盟科技的AI降噪率平均达到95%以上,AI综合辅助研判准确率超过90%。在处置响应方面,依托自主响应可实现超过40%的安全事件端到端的自动化响应处置。特别值得关注的是,经过实际观测和验证,针对13大类77小类的攻击类型,绿盟风云卫AI辅助识别准确率超过95%。
会上,绿盟科技全流程演示了AI赋能安全运营的过程,生动体现了AI技术在安全运营各环节的深度融合——从海量告警的智能降噪、攻击链路的自动重构,到复杂事件的自主研判和自动化响应,真正实现了安全运营从"人防"到"智防"的质变升级。
AI赋能安全检测:混淆检测+自主基线,让未知威胁检测变成可能
在攻防演练中,统计数据显示有76%的攻击属于“已知家族、未知变种”类型,这类攻击因具备一定家族特征又存在变异特性,给检测工作带来不小挑战。
绿盟“风云卫”AI安全能力平台在此类场景中展现出显著优势:在混淆检测方面,AI凭借强大的语义理解能力,能够深入剖析恶意程序的本质特征。即便攻击手段经过混淆处理,改变了表面形态,AI也能透过现象看本质,精准识别出其属于已知家族的核心属性,从而有效识破“未知变种”的伪装。
在自主基线构建上,AI能够自主解读并理解全量日志,从中提炼出账号、流量、进程等各类实体在正常时段的行为画像。基于这些画像,AI可以秒级输出动态阈值,形成精准的正常行为基线。当“已知家族、未知变种”的攻击出现,导致相关实体行为偏离动态阈值时,系统能快速察觉异常,为及时发现和应对威胁提供有力支撑。
智能体中心成效显著,20多个安全领域智能体协同赋能
绿盟“风云卫”AI安全能力平台汇聚绿盟安全专家经验,内置20+安全领域智能体,覆盖网络安全多个关键环节,包含钓鱼邮件检测、可疑样本分析、敏感数据识别、零配置日志解析、情报分析、报告生成等多个智能体。这些智能体既可以赋能产品,也可以独立运行。值得一提的是,智能体中心支持智能体可视化编排,这一特性为用户带来了极大便利。即便是非专业的技术人员,也能通过简单的拖拽、连线操作,如同搭建积木一般,将多个智能体按照企业自身的业务逻辑与安全需求,灵活组合成个性化的安全工作流程。
例如,用户可通过可视化方式自定义编排敏感信息检测智能体,将企业特定的敏感信息嵌入其中,从而实现更精准的自定义检测。这种低代码的编排方式不仅大幅降低了使用门槛,还能灵活应对企业不断变化的安全需求,实现安全防护的定制化与敏捷化,全面提升网络安全工作的效能。
多行业落地实践,安全运营效率大幅提升
绿盟科技鹰眼安全运营中心技术经理李昀磊
截至目前,绿盟科技已助力电信、金融、政府、高校等行业的多家客户实现安全运营智能化转型。在近期多项攻防演练中,公司依托“风云卫”AI安全能力平台,为客户提供全面支撑,多项智能体——包括未知威胁检测、行为分析、钓鱼邮件识别等——均发挥关键作用。
绿盟科技鹰眼安全运营中心技术经理李昀磊介绍,绿盟安全运营中心已持续为超2000家企业提供安全运营服务,并于2023年起全面融合“风云卫”大模型,AI赋能成效主要体现在三方面:
●高频场景AI全自动处置:对实时性要求极高的常见攻击,实现从检测、研判、通知到响应的全自动闭环,无需人工干预;
●复杂事件智能辅助调查:针对约占20%+的复杂事件,AI可自主拓展调查路径,完成情报搜集与初步总结,提升分析师决策效率;
●工具调度与客户交互自动化:AI统一调度多类分析工具,并自动完成工单发送、报告生成与客户反馈响应,显著节约人力。
截至目前,绿盟云端安全运营中心约77%的告警日志依托AI实现辅助研判与处置,在客户预授权条件下5分钟内发现确认并处置闭环事件,运营效率大幅提升。
绿盟AI智能化渗透系统发布
绿盟科技产品总监许伟强
绿盟科技产品总监许伟强表示,公司基于多年攻防实战经验与大模型技术,正式推出新一代绿盟AI智能化渗透系统。该系统全面覆盖常态化扫描与日常安全运维等多种场景,在国内首次实现AI智能体在真实网络环境中完成端到端渗透测试,显著提升渗透效率与响应能力。该系统具备四大核心能力:
●智能任务规划:通过多智能体分层协作,结合专业攻防知识库,实现对复杂渗透场景的智能化任务分解;
●智能工具调度:依托工具调度智能体,无缝调用并协同多种渗透工具,破除工具间壁垒,增强协同作战效能;
●渗透路径推荐:基于安全大模型技术,融合知识图谱与漏洞利用知识,提供渗透路径规划、过程可视化及标准化报告输出;
●AI智能对话:支持自然语言交互,可依据用户指令智能推荐并自动执行渗透工具,大幅降低操作门槛。
绿盟AI智能化渗透系统基于“风云卫”平台构建,采用“人在环路”安全机制与多智能体协同架构,具备“直接模式+深度模式”双轨机制,可快速响应常规攻击面,也可深入攻坚复杂高对抗场景,动态适应多样化的实战攻防需求。
随着国务院常务会议审议通过《关于深入实施“人工智能+”行动的意见》,“人工智能+”正成为产业升级的关键方向,各领域在快速发展的同时,安全问题将不容忽视。绿盟科技始终站在技术前沿,目前形成了以风云卫AI安全能力平台为核心,构建“模型生产、场景适配、应用赋智”的“三位一体”AI安全生态体系,可为不同用户提供全方位的智能安全保障。面向未来,绿盟科技将继续以创新为引擎,携手客户与合作伙伴,共同迎接智能安全新时代。
本地Docker部署Transmission,内网穿透无限远程访问教程
Transmission是一款开源、轻量且资源占用极低的BitTorrent客户端。借助Docker,可以在几分钟内把它跑起来;再借助贝锐花生壳的内网穿透功能,又能随时随地从外网安全访问。下面给出一条龙的部署与远程访问流程,全部命令可直接复制粘贴。
一、准备Docker环境
1. 一键安装最新版Docker(已包含阿里云镜像加速):
2. 启动并设为开机自启:
二、拉取Transmission镜像
如果拉取超时,可在 `/etc/docker/daemon.json` 中追加国内镜像源,例如:
三、运行Transmission容器
下面命令把Web端口9091、BT 监听端口41234(TCP/UDP)都映射出来,并把配置、下载目录、监控目录挂到宿主机持久化。按需替换UID/GID、时区、用户名密码以及宿主机路径。
启动后,浏览器访问 http://局域网IP:9091即可看到Transmission Web UI。
点击左上角图标即可上传种子或粘贴磁力链接开始下载。
四、安装并配置贝锐花生壳
1. 下载客户端
在同一内网任意设备上,从花生壳官网下载最新Linux版客户端,可根据实际情况,选择docker安装花生壳。(`phddns_5.x.x_amd64.deb`)。
2. 安装
根据不同位数的系统输入下面的命令进行安装,安装完成会自动生成SN码与登录密码。
3. 激活与登录
浏览器打开 [花生壳管理平台](http://b.oray.com),用SN和默认密码登录。
首次登录需激活设备,支持微信扫码或绑定贝锐账号。
4. 添加映射
激活成功后,进入「内网穿透」→「添加映射」,填写新增的映射信息。
保存后,贝锐花生壳会生成一个 `http://xxxx.hsk.oray.com:端口` 的外网地址。可访问外网地址访问transmission。
五、外网访问与日常使用
任何地点打开浏览器,输入花生壳提供的外网地址,即可远程管理Transmission:添加种子、查看进度、做种、限速等操作与局域网完全一致。
至此,借助贝锐花生壳内网穿透就可以使本地Docker版Transmission已可安全、便捷地实现远程访问。
收起阅读 »当上组长一年里,我保住了俩下属
前言
人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。
就在上周,"苟住"群里的一个小伙伴也苟不住了。
在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。
1. 组织变动,意外晋升
两年前加入公司,依然是一线搬砖的码农。
干到一年的时候公司空降了一位号称有诸多大厂履历的大佬来带领研发,说是要给公司带来全新的变化,用技术创造价值。
大领导第一件事:抓人事,提效率。
在此背景下,公司不少有能力的研发另谋出处,也许我看起来人畜无害,居然被提拔当了小组长。
2. 领取任务,开启副本
当了半年的小组长,我的领导就叫他小领导吧,给我传达了大领导最新规划:团队需要保持冲劲,而实现的手段就是汰换。
用人话来说就是:
当季度KPI得E的人,让其填写绩效改进目标,若下一个季度再得到E,那么就得走人
我们绩效等级是ABCDE,A是传说中的等级,B是几个人有机会,大部分人是C和D,E是垫底。
而我们组就有两位小伙伴得到了E,分别是小A和小B。
小领导意思是让他们直接走得了,大不了再招人顶上,而我想着毕竟大家共事一场,现在大环境寒气满满,我也是过来人,还想再争取争取。
于是分析了他们的基本资料,他俩特点还比较鲜明。
小A资料:
- 96年,单身无房贷
- 技术栈较广,技术深度一般,比较粗心
- 坚持己见,沟通少,有些时候会按照自己的想法来实现功能
小B资料:
- 98年,热恋有房贷
- 技术基础较薄弱,但胜在比较认真
- 容易犯一些技术理解上的问题
了解了小A和小B的历史与现状后,我分别找他们沟通,主要是统一共识:
- 你是否认可本次绩效评估结果?
- 你是否认可绩效改进的点与风险点(未达成被裁)?
- 你是否还愿意在这家公司苟?
最重要是第三点,开诚布公,若是都不想苟了,那就保持现状,不要浪费大家时间,我也不想做无用功。
对于他们,分别做了提升策略:
对于小A:
- 每次开启需求前都要求其认真阅读文档,不清楚的地方一定要做记录并向相关人确认
- 遇到比较复杂的需求,我也会一起参与其中梳理技术方案
- 需求开发完成后,CR代码看是否与技术方案设计一致,若有出入需要记录下来,后续复盘为什么
- 给足时间,保证充分自测
对于小B:
- 每次需求多给点时间,多出的时间用来学习技术、熟悉技术
- 要求其将每个需求拆分为尽可能小的点,涉及到哪些技术要想清楚、弄明白
- 鼓励他不懂就要问,我也随时给他解答疑难问题,并说出一些原理让他感兴趣的话可以继续深究
- 分配给他一些技术调研类的任务,提升技术兴趣点与成就感
3. 结束?还是是另一个开始?
半年后...
好消息是:小A、小B的考核结果是D,达成了绩效改进的目标。
坏消息是:据说新的一轮考核算法会变化,宗旨是确保团队血液新鲜(每年至少得置换10%的人)。
随缘吧,我尽力了,也许下一个是我呢?
来源:juejin.cn/post/7532334931021824034
一文说透WebSocket协议(秒懂)
本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
为避免同学们概念混淆,先声明一下,其实WebSocket和Socket之间是毫无关系的,就像北大青鸟和北大一样,大家不要被名字给带偏了。
WebSocket是一种建立在TCP底层连接上,使web客户端和服务器端可进行持续全双工通信的协议。
用大白话来说,WebSocket协议最大的特点是支持服务器端给客户端发送消息。
只需先通过HTTP协议进行握手并进行协议升级,即可让服务器端和客户端一直保持连接并实现通信,直到连接关闭。
如下图所示:
一定会有同学存在疑问,WebSocket协议所具备的“支持服务器端给客户端发送消息”的特点,具体适用场景是什么呢?
下面我们就来详细地讲解一下。
适用场景
对于这个问题,我们有必须逆向思考一下,WebSocket协议所适用的场景,必然是其他协议不适用的场景,这个协议就是HTTP。
由于HTTP协议是半双工模式,只能由客户端发起请求并由服务器端进行响应。
所以在线聊天、实时互动游戏、股票行情、物联网设备监控等业务场景下,只能通过客户端以轮询、长轮询的方式去服务器端获取最新数据。
股票行情场景,如下图所示:
这种方式所带来的问题有两点:
1、客户端频繁发送HTTP请求会带来网络开销,也会给服务器端带来负载压力;2、轮询间隔难以把控,间隔过短同样会带来问题(1)中提到的点,间隔过长会导致数据延迟。
而WebSocket协议只有在服务器端有事件发生的时候,才会第一时间给客户端发送消息,彻底杜绝了HTTP轮询所带来的网络开销、服务器负载和数据时延问题。
实现步骤
阶段一、客户端通过 HTTP 协议发送包含特殊头部的请求,触发协议升级:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
- Upgrade: websocket明确请求升级协议。
- Sec-WebSocket-Key:客户端生成的随机字符串,用于安全验证。
- Sec-WebSocket-Version:指定协议版本(RFC 6455 规定为 13)。
阶段二、服务器端进行响应确认,返回 101 Switching Protocols
响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- Sec-WebSocket-Accept:服务器将客户端的 Sec-WebSocket-Key 与固定字符串拼接后,计算 SHA-1 哈希并进行 Base64 编码,生成验证令牌。
阶段三、此时 TCP 连接从 HTTP 升级为 WebSocket 协议,后续数据可通过二进制帧进行传输。
阶段四、数据传输,WebSocket是一种全双工通信协议,客户端与服务端可同时发送/接收数据,无需等待对方请求,数据帧是以二进制格式进行传输的。
如下图所示:
- FIN (1 bit):标记是否为消息的最后一个分片。
- Opcode (4 bits):定义数据类型(如文本 0x1、二进制 0x2、关闭连接 0x8、Ping 0x9、Pong 0xA)。
- Mask (1 bit):客户端发送的数据需掩码处理(防止缓存污染攻击),服务端发送的数据无需掩码。
- Payload Length (7 or 7+16 or 7+64 bits):帧内容的长度,支持最大 2^64-1 字节。
- Masking-key(32 bits),掩码密钥,由上面的标志位 MASK 决定的,如果使用掩码就是 4 个字节的随机数,否则就不存在。
- payload data 字段:这里存放的就是真正要传输的数据
阶段五、连接关闭,客户端或服务器端都可以发起关闭。
示例代码
前端代码:
<!DOCTYPE html>
<html>
<body>
<input type="text" id="messageInput" placeholder="输入消息">
<button onclick="sendMessage()">发送</button>
<div id="messages"></div>
<script>
// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080/ws');
// 连接打开时触发
socket.addEventListener('open', () => {
logMessage('连接已建立');
});
// 接收消息时触发
socket.addEventListener('message', (event) => {
logMessage('收到消息: ' + event.data);
});
// 连接关闭时触发
socket.addEventListener('close', () => {
logMessage('连接已关闭');
});
// 错误处理
socket.addEventListener('error', (error) => {
logMessage('连接错误: ' + error.message);
});
// 发送消息
function sendMessage() {
const message = document.getElementById('messageInput').value;
socket.send(message);
logMessage('发送消息: ' + message);
}
// 日志输出
function logMessage(message) {
const messagesDiv = document.getElementById('messages');
const p = document.createElement('p');
p.textContent = message;
messagesDiv.appendChild(p);
}
</script>
</body>
</html>
我们通过 Spring WebSocket 来实现服务器端代码。
1、添加 Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.14</version>
</dependency>
2、配置类启用 WebSocket:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws")
.setAllowedOrigins("*");
}
}
3、消息处理器实现:
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class MyWebSocketHandler extends TextWebSocketHandler {
private static final Set<WebSocketSession> sessions =
Collections.synchronizedSet(new HashSet<>());
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
log("新连接: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
log("收到消息: " + payload);
// 广播消息
sessions.forEach(s -> {
if (s.isOpen() && !s.equals(session)) {
try {
s.sendMessage(new TextMessage("广播: " + payload));
} catch (Exception e) {
log("发送消息失败: " + e.getMessage());
}
}
});
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
log("连接关闭: " + session.getId());
}
private void log(String message) {
System.out.println("[MyWebSocketHandler] " + message);
}
}
结语
在本文中,我们先是对WebSocket协议的概念进行了讲解,也对其适用场景、实现步骤进行描述,最后给出了实例代码,旨在帮助大家一站式熟悉WebSocket协议。
来源:juejin.cn/post/7503811248288661558
优化Mini React:避免状态未变更时的重复渲染
优化Mini React:避免状态未变更时的重复渲染
在构建Mini React时,我们发现一个常见的性能问题:即使状态值未发生改变,组件也会进行不必要的重复渲染。本文将深入分析问题原因并实现优化方案。
问题现象分析
以下面代码为例:
function Foo() {
console.log('fooo') // 每次点击都会打印
const [bar, setBar] = React.useState('bar')
function handleClick() {
setBar('bar') // 设置相同的值
}
return (
<div>
{bar}
<button onClick={handleClick}>clickbutton>
div>
);
}
当点击按钮时,虽然状态值bar
没有实际变化,但每次点击都会触发组件重新渲染(控制台持续输出"fooo")。这在性能敏感场景下会造成资源浪费。
优化原理与实现
React的核心优化策略之一是:当状态值未改变时,跳过渲染流程。我们在useState的setState函数中加入值比较逻辑:
function useState(initial) {
// ... 状态初始化逻辑
const setState = (action) => {
// 计算期望的新状态
const eagerState = typeof action === 'function'
? action(stateHook.state)
: action;
// 关键优化:状态值未改变时提前返回
if (Object.is(eagerState, stateHook.state)) {
return;
}
// 状态更新及重新渲染逻辑
stateHook.state = eagerState;
scheduleUpdate();
};
return [stateHook.state, setState];
}
优化关键点解析
- 提前计算状态值:
- 处理函数式更新:
action(currentState)
- 处理直接赋值:
action
- 处理函数式更新:
- 精准状态比较:
- 使用
Object.is()
代替===
运算符 - 正确处理特殊值:
NaN
、+0
/-0
等边界情况 - 性能考虑:先比较再更新,避免不必要的渲染流程
- 使用
- 渲染流程优化:
- 状态未变更时直接return,阻断后续更新
- 状态变更时才触发重新渲染调度
优化效果验证
优化后,当点击按钮设置相同状态值时:
setBar('bar') // 与当前状态相同
- 控制台不再输出"fooo"
- 组件不会触发重新渲染
- 虚拟DOM不会进行diff比较
- 真实DOM不会更新
实际应用场景
- 表单控件:输入框失去焦点时重置状态
- 多次相同操作:重复点击相同选项
- 防抖/节流:快速触发时的状态保护
- 数据同步:避免接口返回相同数据时的渲染
扩展思考
- 引用类型优化:
setObj({...obj}) // 内容相同但引用不同
需配合immutable.js或immer等库实现深度比较
- 类组件优化: 在setState方法中实现相同的值比较逻辑
- 性能权衡: 简单值比较成本低,复杂对象比较需评估成本
总结
通过实现状态变更的精准判断,我们:
- 减少不必要的渲染流程
- 降低虚拟DOM diff成本
- 避免真实DOM的无效更新
- 提升组件整体性能
在Mini React中实现的这一优化,体现了React框架设计中的核心性能优化思想。理解这一机制有助于我们编写更高效的React应用代码。
优化本质:计算成本 < 渲染成本时,用计算换渲染
来源:juejin.cn/post/7524992966084083766