注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

常用到的几个Kotlin开发技巧,减少对业务层代码的入侵

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。 善用@get/@set: JvmName()注解并搭配setter/getter使用 假设当前存在下面三个类代码: #Opt1 public cl...
继续阅读 »

本篇文章主要介绍常用到的几个kotlin开发技巧,能够帮助我们减少对业务层代码的修改,以及减少模板代码的编写。



善用@get/@set: JvmName()注解并搭配setter/getter使用


假设当前存在下面三个类代码:


#Opt1


public class Opt1 {

private String mContent;

public String getRealContent() {
return mContent;
}

public void setContent(String mContent) {
this.mContent = mContent;
}
}

#Opt2


public class Opt2 {

public void opt2(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

@Opt3


public class Opt3 {

public void opt3(Opt1 opt1) {
System.out.println(opt1.getRealContent());
}
}

这个时候我想将Opt1类重构成kotlin,我们先看下通过AS的命令Convert Java File to Kotlin File自动转换的结果:


image.png


可以看到为了兼容Opt2Opt3的调用,直接把我的属性名给改成了realContent,kotlin会自动生成getRealContent()setRealContent()方法,这样Opt2Opt3就不用进行任何调整了,kotlin这样就显得太过于智能了。


这样看起来没啥问题,但是java重构kotlin,直接把属性名给我改了,并隐式生成了属性的set和get方法,对于java而言不使用的方法会报灰提示或者只有当前类使用AS会警告可以声明成private,但是对于kotlin生成的set、get方法是隐式的,容易忽略。


所以大家在使用Convert Java File to Kotlin File命令将java重构kotlin的结果一定不能抱有百分之百的信任,即使它很智能,但还是一定要细细的看下转换后的代码逻辑,可能还有不少的优化空间。


这个地方就得需要我们手动进行修改了,比如不想对外暴露修改这个字段的set方法,调整如下:


class Opt1 {
var realContent: String? = null
private set
}

再比如保持原有的字段名mContent,不能被改为realContent,同时又要保证兼容Opt2Opt3类的调用不能报错,且尽量避免去修改里面的代码,我们就可以做如下调整:


class Opt1 {
@get: JvmName("getRealContent")
var mContent: String? = null
private set
}

善用默认参数+@JvmOverloads减少模板代码编写


假设当前Opt1有下面的方法:


public String getSqlCmd(String table) {
return "select * from " + table;
}

且被Opt2Opt3进行了调用,这个时候如果有另一个类Opt3想要调用这个函数并只想从数据库查询指定字段,如果用java实现有两种方式:



  1. 直接在getSqlCmd()方法中添加一个查询字段参数,如果传入的值为null,就查询所有的字段,否则就查询指定字段:


public String getSqlCmd(String table, String name) {
if (TextUtils.isEmpty(name)) {
return "select * from " + table;
}
return "select " + name + " from " + table;
}

这样一来,是不是原本Opt2Opt3getSqlCmd()方法调用是不是需要改动,多传一个参数给方法,而在日常的项目开发中,有可能这个getSqlCmd()被几十个地方调用,难道你一个个的改过去?不太现实且是一种非常糟糕的实现。



  1. 直接在Opt1中新增一个getSqlCmd()的重载方法,传入指定的字段去查询:


public String getSqlCmd(String table,String name) {
return "select " + name + " from " + table;
}

这样做的好处就是不用调整Opt2Opt3getSqlCmd(String table)方法调用逻辑,但是会编写很多模板代码,尤其是getSqlCmd()这个方法体可能七八十行的情况下。


如果Opt1类代码减少即200-400行且不负责的情况下,我们可以将其重构成kotlin,借助于默认参数来实现方法功能增加又不用编写模板代码的效果(如果你的Java类上千行又很复杂,请谨慎转换成kotlin使用下面这种方式)。


@JvmOverloads
fun getSqlCmd(table: String, name: String? = null): String {
return "select ${if (name.isNullOrEmpty()) "*" else name} from $table"
}

添加默认参数name时还要添加@JvmOverloads注解,这样是为了保证java只传一个table参数也能正常调用。


通过上面这种方式,我们就能保证实现了方法功能增加,又不用改动Opt2Opt3对于getSqlCmd()方法的调用逻辑,并且还不用编写额外的模板代码,一举多得。


总结


本篇文章主要介绍了在java重构成kotlin过程中比较常用到的两个技巧,最终实现效果是减少对业务逻辑代码的入侵,希望能对你有所帮助。


作者:长安皈故里
链接:https://juejin.cn/post/7148823027608715295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用力一瞥Android渲染机制-黄油计划

一. 渲染基本概念 对于渲染来说在开始前我们先了解几个概念: CPU主要负责包括 Measure,Layout,Record,Execute 的计算操作。 GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(...
继续阅读 »

一. 渲染基本概念


对于渲染来说在开始前我们先了解几个概念:


CPU主要负责包括 MeasureLayoutRecordExecute 的计算操作。


GPU主要负责 Rasterization(栅格化)操作。栅格化是指将向量图形格式表示的图像转换成位图(像素)以用于显示设备输出的过程,简单来说就是将我们要显示的视图,转换成用像素来表示的格式。


帧率代表了GPU在一秒内绘制操作的帧数。


刷新率代表了屏幕在一秒内刷新屏幕的次数,Android手机一般为60HZ。


二. Android黄油计划


涉及到滑动流畅,Android在谷歌4.1版本引入了黄油计划。其中有三个重要的核心元素:VSYNC、缓存区和Choreographer:


2.1 VSYNC信号


在Android4.0的时候,CPU可能会因为在忙其他的事情,导致没来得及处理UI绘制。为了解决这个问题,设计成系统在收到VSYN信号后,才会开始下一帧的渲染。也就是收到VSYN通知,CPU和GPU才开始计算然后把数据写入buffer中。


VSYN信号是由屏幕产生的,并且以60fps的固定频率发送给Android系统,在Android系统中的SurfaceFlinger接收发送的Vsync信号。当屏幕从缓存区扫描完一帧到屏幕上之后,开始扫描下一帧之前,发出的一个同步信号,该信号用来切换前缓冲区和后缓冲区。


在引入了Vsyn信号之后,绘制就变成了:


image.png


可以看到渲染的时候从第0帧开始,CPU开始准备第一帧的图形处理,好了才交给GPU进行处理,再上一帧到来之后,CPU就会开始第二帧的处理,基本上跟Vsync的信号保持同步。


有了Vsync机制,可以让CPU/GPU有完整的16ms时间来处理数据,减少了jank。


2.2 三重缓存


在采用双缓冲机制的时候,也意味着有两个缓存区,分别是让绘制和显示器拥有各自的buffer,GPU使用Back Buffer进行一帧图像数据写入,显示器则是用Frame Buffer,一般来说CPU和GPU处理数据的速度视乎都能在16ms内完成,而且还有时间空余。但是一旦界面比较复杂的情况,CPU/GPU的处理时间超过了16ms,双缓冲开始失效了:


image.png


在第二个时间段内,因为GPU还是处理B帧,数据没有及时交换,导致继续系那是之前A缓存区中的内容。


在B帧完成之后,又因为缺少了Vusnc信号,只能等待一段时间。


直到下一个Vsync信号出现的时候,CPU/GPU才开始马上执行,由于执行时间仍然超过了16ms,导致下一次应该执行的缓存区交换又被推迟了,反复这种情形,就会出越来越多的jank。


为了解决这个问题,Android 4.1才引入了三缓冲机制:在双缓冲机制的基础上增加了一个Graohic Buffer缓冲区,这样就可以最大限度的利用空闲的时间。


image.png


可以看到在第二个时间段里有了区别,在第一次Vsync发生之后,CPU不用再等待了,它会使用第三个bufferC来进行下一帧的准备工作。整个过程就开始的时候卡顿了一下,后面还是很流畅的。但是GPU需要跨越两个Vsync信号才能显示,这样就还是会有一个延迟的现象。


总的来说三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了lag。


2.3 Choreographer


在了解了Vsync机制后,上层又是如何接受这个Vsync信号的?


Google为上层设计了一个Choreographer类,翻译成中文是“编舞者”,是希望通过它来控制上层的绘制(舞蹈)节奏。


可以直接从其构造函数开始看起:


private Choreographer(Looper looper, int vsyncSource) {
//创建Looper对象
mLooper = looper;
//接受处理消息
mHandler = new FrameHandler(looper);
//用来接受垂直同步脉冲,也就是Vsync信号
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
//计算下一帧的时间,Androoid手机屏幕是60Hz的刷新频率
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
//初始化CallbackQueue,将在下一帧开始渲染时回调
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}

主要来看下FrameHandlerFrameDisplayEventReceiver的数据结构:


private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//开始渲染下一帧的操作
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
//请求Vsync信号
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
//请求执行Callback
case MSG_DO_SCHEDULE_CALLBACK:
doScheduleCallback(msg.arg1);
break;
}
}
}

FrameHandler可以看到对三种消息进行了处理,对其具体实现一会分析。


private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;

public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
}

@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {

......
mTimestampNanos = timestampNanos;
mFrame = frame;
//将本身作为runnable传入msg, 发消息后 会走run(),即doFrame(),也是异步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}

@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}

可以看出来这个类主要是用来接收底层的VSync信号开始处理UI过程。而Vsync信号是由SurfaceFlinger实现并定时发送,接收到之后就会调用onVsync方法,在里面进行处理消息发送到主线程处理,另外在run()方法里面执行了doFrame(),这也是接下来要关注的重点方法。


2.3.1 Choreographer执行过程



ViewRootImpl 中调用 Choreographer 的 postCallback 方法请求 Vsync 并传递一个任务(事件类型是 Choreographer.CALLBACK_TRAVERSAL)



最开始执行的是postCallBack发起回调,这个FrameCallback将会在下一帧渲染时执行。而其内部又调用了postCallbackDelayed方法,在其中又调用了postCallbackDelayedInternal方法:


private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
......
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}

在这里执行了时间的计算,如果立即就会调用scheduleFrameLocked方法,不然就会延迟发送一个MSG_DO_SCHEDULE_CALLBACK消息,并且在这里使用msg.setAsynchronous(true)讲消息设置成异步。、


而所对应的mHandle也就是之前的FrameHandler,根据消息类型MSG_DO_SCHEDULE_CALLBACK,最终会调用到doScheduleCallback方法:


void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}

到了这一步看到还是会调用到scheduleFrameLocked方法。


private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
//开启了Vsync
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}


if (isRunningOnLooperThreadLocked()) {
//申请Vsync信号
scheduleVsyncLocked();
} else {
//最终还是会调用到scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
//如果没有直接使用Vsync的话,则直接通过该消息执行doFrame
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}

在这里对是否使用Vsync信号进行处理,如果没有使用则直接通过消息执行doFrame。如果使用的就会先判断是否在当前Looper线程中运行,如果在的话就会请求Vsync信号,否则发送消息到 FrameHandler。直接来看下scheduleVsyncLocked方法:


 private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}

可以看到调用了FrameDisplayEventReceiverscheduleVsync方法,通过查找在其父类DisplayEventReceiver中找到了scheduleVsync方法:


public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
//申请VSYNC信号,会回调onVsunc方法
nativeScheduleVsync(mReceiverPtr);
}
}

scheduleVsync()就是使用native方法nativeScheduleVsync()去申请VSYNC信号。等下一次信号接收后会调用dispatchVsync 方法:


private void dispatchVsync(long timestampNanos, long physicalDisplayId, int frame) {
onVsync(timestampNanos, physicalDisplayId, frame);
}

这个onVsync方法最终实现也就是在FrameDisplayEventReceiver里。可以知道最终还是走到了doFrame方法里。


void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
......
//设置当前frame的Vsync信号到来时间
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
//时间差大于一个时钟周期,认为跳frame
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
//跳frame数大于默认值,打印警告信息,默认值为30
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
//计算实际开始当前frame与时钟信号的偏差值
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
+ "which is more than the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Skipping " + skippedFrames + " frames and setting frame "
+ "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
}

//修正偏差值,忽略偏差,为了后续更好地同步工作
frameTimeNanos = startNanos - lastFrameOffset;
}

//若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
if (frameTimeNanos < mLastFrameTimeNanos) {
if (DEBUG_JANK) {
Log.d(TAG, "Frame time appears to be going backwards. May be due to a "
+ "previously skipped frame. Waiting for next vsync.");
}
//请求下一次时钟信号
scheduleVsyncLocked();
return;
}

......

//记录当前frame信息
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
//记录上一次frame开始时间,修正后的
mLastFrameTimeNanos = frameTimeNanos;
}

try {
//执行相关callBack
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

doFrame方法对当前帧的运行时间进行了一系列判断和修正,最终顺序执行了五种事件回调。



  1. CALLBACK_INPUT:输入

  2. CALLBACK_ANIMATION:动画

  3. CALLBACK_INSETS_ANIMATION:插入更新的动画

  4. CALLBACK_TRAVERSAL:遍历,执行measure、layout、draw

  5. CALLBACK_COMMIT:遍历完成的提交操作,用来修正动画启动时间


接着就会执行doCallbacks方法:


void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
//迭代执行所有队列任务
for (CallbackRecord c = callbacks; c != null; c = c.next) {
.....
//调用CallbackRecord内的run方法
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

主要是去遍历CallbackRecrd,执行所有任务:


private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;

@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
((Runnable)action).run();
}
}
}

最终actionrun方法会被执行,这里的action也就是我们在前面调用psetCallback传进来的,也就是 ViewRootImpl 发起的绘制任务mTraversalRunnable了。


然后这里又一次调用了doFrame方法,在啥时候token会是FRAME_CALLBACK_TOKEN呢? 可以发现在我们调用postFrameCallback内部会调用postCallbackDelayedInternal进行赋值:


 public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}

postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

ChoreographerpostFrameCallback()通常用来计算丢帧情况。


知道了Choreographer是上层用来接收VSync的角色之后,我们需要进一步了解VSync信号是如何控制上层的绘制的。而绘制UI的起点是View的requestLayout或者是invalidate方法被调用触发,好了时间不早了,这些就放在下一篇Android的屏幕刷新机制里解释吧。(刷新流程和同步屏障)


三. 小结


Android在黄油计划中引入了三个核心元素:VSYNCTriple BufferChoreographer


VSYNC 信号是由屏幕(显示设备)产生的,并且以 60fps 的固定频率发送给 Android 系统,Android 系统中的 SurfaceFlinger 接收发送的 VSYNC 信号。VSYNC 信号表明可对屏幕进行刷新而不会产生撕裂。


三重缓存机制(Triple Buffer) 利用 CPU/GPU 的空闲等待时间提前准备好数据,有效的提升了渲染性能。


又介绍了 Choreographer ,它实现了协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。


参考


Android 显示刷新机制、VSYNC和三重缓存机制


Android图形显示系统(一)


Android屏幕刷新机制


Android Choreographer 源码分析


“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!


作者:罗恩不带土
链接:https://juejin.cn/post/7149838503973830692
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一次查找分子级Bug的经历,过程太酸爽了

作者:李亚飞 Debugging is like trying to find a needle in a haystack, except the needle is also made of hay. Debug调试就像是在大片的干草堆中找针一样,只不...
继续阅读 »

作者:李亚飞





Debugging is like trying to find a needle in a haystack, except the needle is also made of hay.


Debug调试就像是在大片的干草堆中找针一样,只不过针也是由干草制成的。



在软件开发的世界里,偶尔会出现一些非常隐蔽的 Bug,这时候工程师们像探险家一样,需要深入代码的丛林,寻找隐藏在其中的“幽灵宝藏”。前段时间,我和我的团队也踏上了这样一段刺激、有趣的探险之旅。


最近繁忙的工作告一段落,我总算轻松下来了,想趁这个机会,跟大家分享我们的这次“旅途”。



01 引子


我是 ShowMeBug 的 CEO 李亚飞,是一个古老的 Ruby 工程师。由于 2019 年招聘工程师的噩梦经历,我立志打造一个真实模拟工作场景的 IDE,用来终结八股文、算法横行的技术招聘时代。


这个云上的 IDE 引擎,我称之为轻协同 IDE 引擎——因为它不是为了繁杂重度的工作场景准备的,而是适应于大部分人的习惯、能快速上手熟悉、加载速度快、能协同(面试用)、低延迟感,让用户感受非常友好


图片


多环境启动与切换


为了达成秒级启动环境的性能要求,我们设计了一套精巧的分布式文件系统架构,其核心是一个可以瞬间复制大量小文件的写时复制 (COW) 技术。IO 吞吐能达到几万人同时在线,性能绝对是它的一大优势。


我们对此信心满满,然而没想到,很快就翻车了。


02 探险启程


2023 年 1 月,北方已经白雪皑皑,而深圳却仍难以感受到冬天的寒意。


我和我的团队在几次打开文件树的某个文件时,会显得有点慢——当时没有人在意,按照常规思路,“网速”背了这个锅。事后我们复盘才发现,这个看似微不足道的小问题,其实正是我们开始这次探险之旅的起点。


1 月底,南方的寒意缓缓侵入。这时候我们的轻协同 IDE 引擎已经开始陆续支持了 Vue2、Vue3、React、Django、Rails 等框架环境,一开始表现都很棒,加载和启动速度都很快。但是,跑了一段时间,我们开始察觉,线上环境就出现个别环境(Rails 环境)启动要 20-30s 才能完成


虽然其他环境仍然保持了极快的加载和启动速度,但敏锐的第六感告诉我,不行,这一定有什么猫腻,如果不立即行动,势必会对用户体验带来很不好的影响。于是,我开始安排团队排查眼前这个不起眼的问题,我们的探险之旅正式开始。


03 初露希望


湿冷的冬季,夜已深,我和我们的团队依旧坐在电脑前苦苦探索,瑟瑟发抖。


探险之旅的第一站,就是老大难的问题:定位Bug。目前只有某一个环境启动很慢,其他的环境都表现不错。大家想了很多办法都没有想明白为什么,甚至怀疑这个环境的模板是不是有问题——但把代码放在本地启动,最多就2秒。


哎,太诡异了。我们在这里卡了至少一周时间,不断追踪代码,分析日志文件,尝试各种方案,都没有弄清楚一个正常的程序启动为什么会慢。我们一度陷入了疲惫和焦虑的情绪中。



Debug 是种信仰,只有坚信自己能找到 Bug,才有可能找到 Bug。



软件开发界一直有一个低级 Bug 定律:所有诡异的问题都来自一个低级原因。在这“山重水复疑无路”之际,我们决定重新审视我们的探险路径:为什么只有 Rails 更慢,其他并不慢?会不会只是一个非常微小的原因而导致?


这时候,恰好有一个架构师朋友来访,向我们建议,可以用 perf 火焰图分析看看 Rails 的启动过程。


图片


perf火焰图实例


当我们用 perf 来分析时,惊讶地发现:原来 Rails 的启动要加载更多的文件! 紧接着,我们又重新用了一个文件读写监控的工具:fatrace,通过它,我们看到 Rails 每次启动需要读写至少 5000 个文件,但其他框架并不需要。


这才让我们突然意识到,会不会是文件系统读写速度不及预期,导致了启动变慢。


04 Bug现身


为了搞清楚是不是文件系统读写速度的问题,我急需一个测试 IO 抖动的脚本。我们初步估算一下,写好这个脚本需要好几个小时的时间。


夜已深,研发同学都陆续下班了。时间紧迫!我想起了火爆全球的 ChatGPT,心想,不如让它写一个试试。


图片


测试 IO 抖动的脚本


Cool,几乎不需要改动就能用,把代码扔在服务器开跑,一测,果然发现问题:每一次文件读写都需要 10-20ms 才能完成 。实际上,一个优秀的磁盘 IO 读写时延应该在亚毫级,但这里至少慢了 50 倍。

Bingo,如同“幽灵宝藏”一般的分子级 Bug 逐渐显现,问题的根因已经确认:过慢的磁盘 IO 读写引发了一系列操作变慢,进而导致启动时间变得非常慢


更庆幸的是,它还让我们发现了偶尔打开文件树变慢的根本原因,这也是整个系统并发能力下降的罪魁祸首


05 迷雾追因


看到这里,大家可能会问,这套分布式文件系统莫非一直这么慢,你们为什么在之前没有发现?


非也,早在项目开始的时候,这里的时延是比较良好的,大家没有特别注意这个 IOPS 性能指标,直到我们后面才留意到,系统运行超过一个月时,IO 读写时延很容易就进入到卡顿的状态,表现就是文件系统所在主机 CPU 忽高忽低,重启就会临时恢复。


此时,探险之旅还没结束。毕竟,这个“幽灵宝藏”周围依旧笼罩着一层迷雾。


我们继续用 fatrace(监控谁在读写哪个 IO)监控线上各个候选人答题目录的 IO读写情况,好家伙,我们发现了一个意外的情况:几乎每一秒都有一次全量的文件 stats 操作 (这是一个检测文件是否有属性变化的 IO 操作)!


也就是说,比如有 1000 个候选人正在各自的 IDE 中编码,每个候选人平均有 300 个文件,就会出现每秒 30 万的 IO 操作数!


我们赶紧去查资料,根据研究数据显示,一个普通的 SSD 盘的 IOPS 最高也就到 2-3 万 。于是,我们重新测试了自己分布式文件系统的 IOPS 能力,结果发现也是 2-3 万 。


那这肯定远远达不到我们理想中的能力级别。


这时,问题更加明确:某种未知的原因导致了大量的 IOPS 的需求,引发了 IO 读写时延变长,慢了大约几十倍


06 接近尾声


我和我的团队继续深究下去,问题已经变得非常明确了:


原来,早在去年 12 月,我们上线一个监听文件增删的变化来通知各端刷新的功能。


最开始我们采用事件监听 (fswatch event),因为跨了主机,所以存在 1-2s 的延迟。研发同学将其改为轮询实现的方案,进而引发了每秒扫描目录的 stats 行为。


当在百人以下访问时,IOPS 没有破万,还足够应对。但一旦访问量上千,便会引发 IO 变慢,进而导致系统出现各种异常:间歇导致某些关键接口 QPS 变低,进而引发系统抖动


随着“幽灵宝藏”显露真身,这次分子级 Bug 的探险之旅也已经接近尾声。团队大
呼:这过程实在太酸爽了!


07 技术无止境


每一个程序员在成长路上,都需要与 Bug 作充足的对抗,要么你勇于探索,深入代码的丛林,快速定位,挖到越来越丰富的“宝藏”,然后尽情汲取到顶级的知识,最终成为高手;或者被它打趴下, 花费大量时间都找不到问题的根源,成为芸芸众生中的一人。


当然,程序员的世界中,不单单是 Debug。


当我毕业 5 年之后,开始意识到技术的真正价值是解决真正的社会问题。前文中我提到,由于我发现技术招聘真是一个极其痛苦的事:特别花面试官的时间,却又无法有效分析出候选人的技术能力,所以创立 ShowMeBug 来解决这个问题:用模拟实战的编程环境,解决科学评估人才的难度


这个轻协同 IDE 技术从零开发,支持协同文件树、完全自定义的文件编辑器、协同的控制台 (Console) 与终端 (Shell),甚至直接支持 Ctrl+P 的文件树搜索,不仅易于使用,又强大有力。


但是这还不够。要知道,追求技术精进是我们技术人的毕生追求。对于这个轻协同IDE,我们追求三个零:零配置、零启动、零延迟。其中,零启动就是本文所追求的极限:以最快的速度启动环境和切换环境


因此,探险之旅结束后,我们进一步改进了此文件系统,设定 raid 的多磁盘冗余,采用高性能 SSD,同时重新制定了新磁盘架构参数,优化相关代码,最终大幅提升了分布式文件系统的稳定性与并发能力。


截止本文结尾,我们启动环境的平均速度为 1.3 秒,切换环境速度进入到亚秒级,仅需要 780ms。目前在全球范围的技术能力评估赛道 (TSA) 中,具备 1-2 年的领先性


08 后记


正当我打算结束本文时,我们内部的产品吐槽群信息闪烁,点开一看:嚯,我们又发现了新 Bug。


立夏已至,我们的探险之旅又即将开始。


作者:ShowMeBug技术团队
来源:juejin.cn/post/7231429790615240764
收起阅读 »

python-实现地铁延误告警

在深圳地铁延误、临停n次之后 终于让我不得不又new了一个py文件😭😭 这次主要记录的是一个延误告警的开发过程 一、实现逻辑 使用库:requests,time,zmail,re 实现逻辑: 1、抓取深圳地铁微博的文章 2、判断是否有延误相关的内容 3、判断时...
继续阅读 »

在深圳地铁延误、临停n次之后


终于让我不得不又new了一个py文件😭😭


这次主要记录的是一个延误告警的开发过程


一、实现逻辑


使用库:requests,time,zmail,re


实现逻辑:


1、抓取深圳地铁微博的文章


2、判断是否有延误相关的内容


3、判断时间是否是今天

4、通知方式:邮件


5、定时执行任务


二、抓取深圳地铁微博(一中1~3)



def goout_report():
url ="https://weibo.com/ajax/statuses/mymblog"
# url ="https://weibo.com/szmcservice/statuses/mymblog"
data = {"uid":2311331195,"page":1,"feature":0}
headers={
"accept":"application/json, text/plain, */*",
"accept-encoding":"gzip, deflate, br",
"accept-language":"zh-CN,zh;q=0.9",
"referer":"https://weibo.com/szmcservice?tabtype=feed",
"cookie":"SUB=_2AkMV8LtUf8NxqwJRmf8XzmLgaY9wywjEieKjrEqPJRMxHRl-yT92ql0ctRB6PnCVuU8iqV308mSwZuO-G9gDVwYDBUdc; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WFpwsXV4nqgkyH.bEVfx-Xw; login_sid_t=c6bbe5dc58bf01c49b0209c29fadc800; cross_origin_proto=SSL; _s_tentry=passport.weibo.com; Apache=4724569630281.133.1655452763512; SINAGLOBAL=4724569630281.133.1655452763512; ULV=1655452763517:1:1:1:4724569630281.133.1655452763512:; wb_view_log=1920*10801; XSRF-TOKEN=1YMvL3PsAm21Y3udZWs5LeX3; WBPSESS=xvhb-0KtQV-0lVspmRtycws5Su8i9HTZ6dAejg6GXKXDqr8m6IkGO6gdtA5nN5IMNb5JZ1up7qJoFXFyoP2RSQSYXHY1uLzykpOFENQ07VthB0G9WHKwRCMWdaof42zB4mOkdTEeX_N9-m1x6Cpm3pmPsC1YhmTwqH8RGwXmYkI=",
"referer":"https://weibo.com/szmcservice",
"x-requested-with": "XMLHttpRequest",
"x-xsrf-token":"1YMvL3PsAm21Y3udZWs5LeX3",
"sec-ch-ua":'Not A;Brand";v="99", "Chromium";v="102", "Google Chrome";v="102',
"sec-ch-ua-platform":"Windows",
"sec-fetch-dest": "empty",
}
text = requests.get(url,headers=headers,params=data,verify=False).json()['data']['list']
today_date = time.ctime()[:10]
for i in range(1,5):
time_post = text[i]['created_at'][:10]
content = str(text[i]).split("'text': '")[1].split(", 'textLength'")[0]
tp=""
if '延误' in content and time_post == today_date:
# mail(content)
text = re.findall(">(.*?)<|>(.*?)\\",content)
for i in text:
for j in i:
if j!="":


                       tp=tp+j

        mail(tp)
break
else:
continue



三、邮件通知,代码如下


def mail(content):
mail = {
'subject': '别墨迹了!地铁又双叒叕延误啦', #邮件标题
'content_text': content, # 邮件内容
}
server = zmail.server('自己的邮箱', '密码',smtp_host="smtp.qq.com",
smtp_port=465) #此处用的qq邮箱、授权码
server.send_mail('收件人邮箱', mail)

ps:需去QQ邮箱网页版-设置-账户-开启smtp服务、获取授权码


四、定时执行任务


1、Jenkins比较合适项目的一个定时执行,


可参考如下:


jenkins环境: jenkins环境部署踩坑记


git环境:Mac-git环境搭建


2、windows-计算机管理比较合适脚本的执行,具体步骤如下,




  • windows键+R输入compmgmt.msc可进入计算机管理界面


    图片




  • 点击上图“创建任务”后如图,


    “常规”界面上输入任务名称、选项二,


    这样锁屏也会自动执行脚本


    图片




  • 点击“触发器”-新建进入新建触发器界面


    这个界面可设置任务执行时间、执行频率、任务重复间隔、延迟时间等等


    图片




  • 点击“操作”-新建跳到如图-新建操作界面


    这个界面可在“程序或脚本”输入框设置脚本运行程序,比如python.exe


    在“添加参数”输入框设置需要运行脚本路径(包含脚本名)


    在“起始于”输入框设置脚本执行路径(一般可为脚本目录)


    图片




  • 其他选项卡也可以看看,


    全部填写完可以点击“创建任务”界面上的“确定”按钮,


    然后在列表中找到新建的任务点击可查看,


    图片




  • 实时执行测试的话可以点击上图“运行”按钮


    或者右击任务-运行即可


    任务执行结果如下:




图片


作者:WAF910
来源:juejin.cn/post/7231074060788613175
收起阅读 »

正则什么的,你让我写,我会难受,你让我用,真香!

web
哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。 但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 ...
继续阅读 »



哈哈,如题所说,对于很多人来说写正则就是”兰德里的折磨“吧。如果不是有需求频繁要用,根本就不会想着学它。(?!^)(?=(\\d{3})+ 这种就跟外星文一样。


image.png


但你要说是用它,它又真的好用。用来做做校验、做做字符串提取、做做变形啥的,真不错。最好的就是能 CV 过来直接用~


本篇带来 15 个正则使用场景,按需索取,收藏恒等于学会!!


千分位格式化


在项目中经常碰到关于货币金额的页面显示,为了让金额的显示更为人性化与规范化,需要加入货币格式化策略。也就是所谓的数字千分位格式化。



  1. 123456789 => 123,456,789

  2. 123456789.123 => 123,456,789.123


const formatMoney = (money) => {
return money.replace(new RegExp(`(?!^)(?=(\\d{3})+${money.includes('.') ? '\\.' : '$'})`, 'g'), ',')
}

formatMoney('123456789') // '123,456,789'
formatMoney('123456789.123') // '123,456,789.123'
formatMoney('123') // '123'

想想如果不是用正则,还可以用什么更优雅的方法实现它?


解析链接参数


你一定常常遇到这样的需求,要拿到 url 的参数的值,像这样:



// url

const name = getQueryByName('name') // fatfish
const age = getQueryByName('age') // 100

通过正则,简单就能实现 getQueryByName 函数:


const getQueryByName = (name) => {
const queryNameRegex = new RegExp(`[?&]${name}=([^&]*)(&|$)`)
const queryNameMatch = window.location.search.match(queryNameRegex)
// Generally, it will be decoded by decodeURIComponent
return queryNameMatch ? decodeURIComponent(queryNameMatch[1]) : ''
}

const name = getQueryByName('name')
const age = getQueryByName('age')

console.log(name, age) // fatfish, 100

驼峰字符串


JS 变量最佳是驼峰风格的写法,怎样将类似以下的其它声明风格写法转化为驼峰写法?


1. foo Bar => fooBar
2. foo-bar---- => fooBar
3. foo_bar__ => fooBar

正则表达式分分钟教做人:


const camelCase = (string) => {
const camelCaseRegex = /[-_\s]+(.)?/g
return string.replace(camelCaseRegex, (match, char) => {
return char ? char.toUpperCase() : ''
})
}

console.log(camelCase('foo Bar')) // fooBar
console.log(camelCase('foo-bar--')) // fooBar
console.log(camelCase('foo_bar__')) // fooBar

小写转大写


这个需求常见,无需多言,用就完事儿啦:


const capitalize = (string) => {
const capitalizeRegex = /(?:^|\s+)\w/g
return string.toLowerCase().replace(capitalizeRegex, (match) => match.toUpperCase())
}

console.log(capitalize('hello world')) // Hello World
console.log(capitalize('hello WORLD')) // Hello World

实现 trim()


trim() 方法用于删除字符串的头尾空白符,用正则可以模拟实现 trim:


const trim1 = (str) => {
return str.replace(/^\s*|\s*$/g, '') // 或者 str.replace(/^\s*(.*?)\s*$/g, '$1')
}

const string = ' hello medium '
const noSpaceString = 'hello medium'
const trimString = trim1(string)

console.log(string)
console.log(trimString, trimString === noSpaceString) // hello medium true
console.log(string)

trim() 方法不会改变原始字符串,同样,自定义实现的 trim1 也不会改变原始字符串;


HTML 转义


防止 XSS 攻击的方法之一是进行 HTML 转义,符号对应的转义字符:


正则处理如下:


const escape = (string) => {
const escapeMaps = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': 'quot',
"'": '#39'
}
// The effect here is the same as that of /[&<> "']/g
const escapeRegexp = new RegExp(`[${Object.keys(escapeMaps).join('')}]`, 'g')
return string.replace(escapeRegexp, (match) => `&${escapeMaps[match]};`)
}

console.log(escape(`

hello world



`
))
/*
<div>
<p>hello world</p>
</div>
*/


HTML 反转义


有了正向的转义,就有反向的逆转义,操作如下:


const unescape = (string) => {
const unescapeMaps = {
'amp': '&',
'lt': '<',
'gt': '>',
'quot': '"',
'#39': "'"
}
const unescapeRegexp = /&([^;]+);/g
return string.replace(unescapeRegexp, (match, unescapeKey) => {
return unescapeMaps[ unescapeKey ] || match
})
}

console.log(unescape(`
<div>
<p>hello world</p>
</div>
`
))
/*

hello world



*/


校验 24 小时制


处理时间,经常要用到正则,比如常见的:校验时间格式是否是合法的 24 小时制:


const check24TimeRegexp = /^(?:(?:0?|1)\d|2[0-3]):(?:0?|[1-5])\d$/
console.log(check24TimeRegexp.test('01:14')) // true
console.log(check24TimeRegexp.test('23:59')) // true
console.log(check24TimeRegexp.test('23:60')) // false
console.log(check24TimeRegexp.test('1:14')) // true
console.log(check24TimeRegexp.test('1:1')) // true

校验日期格式


常见的日期格式有:yyyy-mm-dd, yyyy.mm.dd, yyyy/mm/dd 这 3 种,如果有符号乱用的情况,比如2021.08/22,这样就不是合法的日期格式,我们可以通过正则来校验判断:


const checkDateRegexp = /^\d{4}([-\.\/])(?:0[1-9]|1[0-2])\1(?:0[1-9]|[12]\d|3[01])$/

console.log(checkDateRegexp.test('2021-08-22')) // true
console.log(checkDateRegexp.test('2021/08/22')) // true
console.log(checkDateRegexp.test('2021.08.22')) // true
console.log(checkDateRegexp.test('2021.08/22')) // false
console.log(checkDateRegexp.test('2021/08-22')) // false

匹配颜色值


在字符串内匹配出 16 进制的颜色值:


const matchColorRegex = /#(?:[\da-fA-F]{6}|[\da-fA-F]{3})/g
const colorString = '#12f3a1 #ffBabd #FFF #123 #586'

console.log(colorString.match(matchColorRegex))
// [ '#12f3a1', '#ffBabd', '#FFF', '#123', '#586' ]

判断 HTTPS/HTTP


这个需求也是很常见的,判断请求协议是否是 HTTPS/HTTP


const checkProtocol = /^https?:/

console.log(checkProtocol.test('https://medium.com/')) // true
console.log(checkProtocol.test('http://medium.com/')) // true
console.log(checkProtocol.test('//medium.com/')) // false

校验版本号


版本号必须采用 x.y.z 格式,其中 XYZ 至少为一位,我们可以用正则来校验:


// x.y.z
const versionRegexp = /^(?:\d+\.){2}\d+$/

console.log(versionRegexp.test('1.1.1'))
console.log(versionRegexp.test('1.000.1'))
console.log(versionRegexp.test('1.000.1.1'))

获取网页 img 地址


这个需求可能爬虫用的比较多,用正则获取当前网页所有图片的地址。在控制台打印试试,太好用了~~


const matchImgs = (sHtml) => {
const imgUrlRegex = /]+src="((?:https?:)?\/\/[^"]+)"[^>]*?>/gi
let matchImgUrls = []

sHtml.replace(imgUrlRegex, (match, $1) => {
$1 && matchImgUrls.push($1)
})
return matchImgUrls
}

console.log(matchImgs(document.body.innerHTML))

格式化电话号码


这个需求也是常见的一匹,用就完事了:


let mobile = '18379836654' 
let mobileReg = /(?=(\d{4})+$)/g

console.log(mobile.replace(mobileReg, '-')) // 183-7983-6654

觉得不错的话,给个赞吧,以后继续补充~~


作者:掘金安东尼
来源:juejin.cn/post/7111857333113716750
收起阅读 »

css实现弧边选项卡

web
实现效果 实现方式 主要使用了 radial-gradient transform perspective rotateX transform-origin 等属性 思路 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变 ...
继续阅读 »

实现效果



image.png



实现方式



主要使用了



等属性



思路




  • 只需要想清楚如何实现弧形三角即可。这里还是借助了渐变 -- 径向渐变


image.png



  • 其实他是这样,如下图所示,我们只需要把黑色部分替换为透明即可,使用两个伪元素即可:


image.png



  • 通过超出隐藏和旋转得到想要的效果


image.png


image.png



  • 综上


在上述 outside-circle 的图形基础上:



  1. 设置一个适当的 perspective 值

  2. 设置一个恰当的旋转圆心 transform-origin

  3. 绕 X 轴进行旋转



  • 动图演示


3.gif



代码



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.g-container {
position: relative;
width: 300px;
height: 100px;
background: red;
border: 1px solid #277f9e;
border-radius: 10px;
overflow: hidden;
}
.g-inner {
position: absolute;
width: 150px;
height: 50px;
background: #fee6e0;
bottom: 0;
border-radius: 0 20px 0 20px;
transform: perspective(40px) scaleX(1.4) scaleY(1.5) rotateX(20deg) translate(-10px, 0);
transform-origin: 50% 100%;
}
.g-inner::before {
content: "";
position: absolute;
right: -10px;
width: 10px;
height: 10px;
top: 40px;
background: radial-gradient(circle at 100% 0, transparent, transparent 9.5px, #fee6e0 10px, #fee6e0);
}
.g-after {
position: absolute;
width: 150px;
height: 50px;
background: #6ecb15;
bottom: 49px;
right: 0;
border-radius: 20px 0 20px 0;
transform: perspective(40px) scaleX(1.4) scaleY(-1.5) rotateX(20deg) translate(14px, 0);
transform-origin: 53% 100%;
}
.g-after::before {
content: "";
position: absolute;
left: -10px;
top: 40px;
width: 10px;
height: 10px;
background: radial-gradient(circle at 0 0, transparent, transparent 9.5px, #6ecb15 10px, #6ecb15);
}
.g-inner-text,.g-after-text {
position: absolute;
width: 150px;
height: 50px;
line-height: 50px;
text-align: center;
}
.g-inner-text {
top: 50%;
left: 0;
}
.g-after-text {
top: 50%;
right: 0;
}
</style>
<body>
<div class="g-container">
<div class="g-inner"></div>
<div class="g-after"></div>
<div class="g-inner-text">选项卡1</div>
<div class="g-after-text">选项卡2</div>
</div>
</body>
</html>

参考文章:github.com/chokcoco/iC…


作者:Agony95z
来源:juejin.cn/post/7223580639710281787
收起阅读 »

极致舒适的Vue页面保活方案

web
为了让页面保活更加稳定,你们是怎么做的? 我用一行配置实现了 Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。 为什么需要页面保活? 页面保活可以提高用户...
继续阅读 »

为了让页面保活更加稳定,你们是怎么做的?


我用一行配置实现了


image.png



Vue页面保活是指在用户离开当前页面后,可以在返回时恢复上一次浏览页面的状态。这种技术可以让用户享受更加流畅自然的浏览体验,而不会被繁琐的操作打扰。



为什么需要页面保活?


页面保活可以提高用户的体验感。例如,当用户从一个带有分页的表格页面(【页面A】)跳转到数据详情页面(【页面B】),并查看了数据之后,当用户从【页面B】返回【页面A】时,如果没有页面保活,【页面A】会重新加载并跳转到第一页,这会让用户感到非常烦恼,因为他们需要重新选择页面和数据。因此,使用页面保活技术,当用户返回【页面A】时,可以恢复之前选择的页码和数据,让用户的体验更加流畅。


如何实现页面保活?


状态存储


这个方案最为直观,原理就是在离开【页面A】之前手动将需要保活的状态存储起来。可以将状态存储到LocalStoreSessionStoreIndexedDB。在【页面A】组件的onMounted钩子中,检测是否存在此前的状态,如果存在从外部存储中将状态恢复回来。


有什么问题?



  • 浪费心智(麻烦/操心)。这个方案存在的问题就是,需要在编写组件的时候就明确的知道跳转到某些页面时进行状态存储。

  • 无法解决子组件状态。在页面组件中还可以做到保存页面组件的状态,但是如何保存子组件呢。不可能所有的子组件状态都在页面组件中维护,因为这样的结构并不是合理。


组件缓存


利用Vue的内置组件<KeepAlive/>缓存包裹在其中的动态切换组件(也就是<Component/>组件)。<KeepAlive/>包裹动态组件时,会缓存不活跃的组件,而不是销毁它们。当一个组件在<KeepAlive/>中被切换时,activateddeactivated生命周期钩子会替换mountedunmounted钩子。最关键的是,<KeepAlive/>不仅适用于被包裹组件的根节点,也适用于其子孙节点。


<KeepAlive/>搭配vue-router即可实现页面的保活,实现代码如下:


<template>
<RouterView v-slot="{ Component }">
<KeepAlive>
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

有什么问题?



  • 页面保活不准确。上面的方式虽然实现了页面保活,但是并不能满足生产要求,例如:【页面A】是应用首页,【页面B】是数据列表页,【页面C】是数据详情页。用户查看数据详情的动线是:【页面A】->【页面B】->【页面C】,在这条动线中【页面B】->【页面C】的时候需要缓存【页面B】,当从【页面C】->【页面B】的时候需要从换从中恢复【页面B】。但是【页面B】->【页面A】的时候又不需要缓存【页面B】,上面的这个方法并不能做到这样的配置。


最佳实践


最理想的保活方式是,不入侵组件代码的情况下,通过简单的配置实现按需的页面保活。


【不入侵组件代码】这条即可排除第一种方式的实现,第二种【组件缓存】的方式只是败在了【按需的页面保活】。那么改造第二种方式,通过在router的路由配置上进行按需保活的配置,再提供一种读取配置结合<KeepAlive/>include属性即可。


路由配置


src/router/index.ts


import useRoutersStore from '@/store/routers';

const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: () => import('@/layout/index.vue'),
children: [
{
path: '/app',
name: 'App',
component: () => import('@/views/app/index.vue'),
},
{
path: '/data-list',
name: 'DataList',
component: () => import('@/views/data-list/index.vue'),
meta: {
// 离开【/data-list】前往【/data-detail】时缓存【/data-list】
leaveCaches: ['/data-detail'],
}
},
{
path: '/data-detail',
name: 'DataDetail',
component: () => import('@/views/data-detail/index.vue'),
}
]
}
];

router.beforeEach((to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const { cacheRouter } = useRoutersStore();
cacheRouter(from, to);
next();
});

保活组件存储


src/stroe/router.ts


import { RouteLocationNormalized } from 'vue-router';

const useRouterStore = defineStore('router', {
state: () => ({
cacheComps: new Set<string>(),
}),
actions: {
cacheRouter(from: RouteLocationNormalized, to: RouteLocationNormalized) {
if(
Array.isArray(from.meta.leaveCaches) &&
from.meta.leaveCaches.inclued(to.path) &&
typeof from.name === 'string'
) {
this.cacheComps.add(form.name);
}
if(
Array.isArray(to.meta.leaveCaches) &&
!to.meta.leaveCaches.inclued(from.path) &&
typeof to.name === 'string'
) {
this.cacheComps.delete(to.name);
}
},
},
getters: {
keepAliveComps(state: State) {
return [...state.cacheComps];
},
},
});

页面缓存


src/layout/index.vue


<template>
<RouterView v-slot="{ Component }">
<KeepAlive :include="keepAliveComps">
<component :is="Component"/>
</KeepAlive>
</RouterView>
</template>

<script lang='ts' setup>
import { storeToRefs } from 'pinia';
import useRouterStore from '@/store/router';

const { keepAliveComps } = storeToRefs(useRouterStore());
</script>

TypeScript提升配置体验


import 'vue-router';

export type LeaveCaches = string[];

declare module 'vue-router' {
interface RouteMeta {
leaveCaches?: LeaveCaches;
}
}

该方案的问题



  • 缺少通配符处理/*/**/index

  • 无法缓存/preview/:address这样的动态路由。

  • 组件名和路由名称必须保持一致。


总结


通过<RouterView v-slot="{ Component }">获取到当前路由对应的组件,在将该组件通过<component :is="Component" />渲染,渲染之前利用<KeepAlive :include="keepAliveComps">来过滤当前组件是否需要保活。
基于上述机制,通过简单的路由配置中的meta.leaveCaches = [...]来配置从当前路由出发到哪些路由时,需要缓存当前路由的内容。


如果大家有其他保活方案,欢迎留言交流哦!


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

vue 递归组件 作用域插槽

web
开头 这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。 代码 父组件 <template> <div> <Tree :data="data"> <templa...
继续阅读 »

开头


这里主要是根据 vue 递归组件 作用域插槽 代码的理解和el-tree是如何写的。


代码


父组件


<template>
<div>
<Tree :data="data">
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

理解步骤,始终知道 -> 递归就是把最里面的放到最外面来,你就当 A插槽最后会被 B 插槽替代

所以,父组件的 default 插槽用的是 B 插槽,因此 B 插槽就暴露出一个 title 给父组件使用。


删掉 A 的title :


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A插槽 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B 插槽 -->
<slot :title='item.title' />
</Tree>
</div>
</div>
</template>

<script>
export default {
name: 'Tree',
props: {
data: Array,
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


由于可能只有一层,所以走不到 B 插槽,因此 A 插槽也需要暴露一个 title 给外面使用。


el-tree 的原理


父组件


<template>
<div>
<Tree :data="data">
<!-- C -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义" }}
</div>
</template>
</Tree>
</div>
</template>
<script>
import Tree from "./tree.vue";
export default {
components: {
Tree,
},
data() {
return {
data: [{
title: "父1",
children: [{
title: "子",
children:[{title:"孙",}]
}],
},{
title: "父2",
children:[{title:"子"}]
}]
};
}
};
</script>

子组件


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去, A -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<!-- B -->
<template #default="{ title }">
<div class="prent">
{{ title + "+自定义22" }}
</div>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node,

},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if(!this.$parent.$scopedSlots.default) {
this.tree = this
}else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

结果:


image.png


这里可以看到,父组件的 C 和 子组件中的 B 都是使用到了 A 这个插槽。


这里我们只要能把 B 替换成父组件的 C 就完成了递归插槽。


子组件的代码转变


<template>
<div class="tree">
<div v-for="item of data" :key="item.title">
<!-- 显示title标题 -->
<div class="title">
<!-- 插槽,这里也是把title传出去 -->
<slot :title="item.title">
<!-- {{ item.title }} -->
</slot>
</div>
<!-- 如果存在子项则调用本身组件 递归 -->
<Tree v-if="item.children" :data='item.children'>
<template #default="{ title }">
<node :title="title">
</node>
</template>
</Tree>
</div>
</div>
</template>

<script>
import node from './node.js'
export default {
name: 'Tree',
components: {
node: {
props: {
title: String,
},
render(h) {
const parent = this.$parent;
const tree = parent.tree
const title = this.title
return (tree.$scopedSlots.default({ title }))
}
}
},
props: {
data: Array,
},
data() {
return {
tree: null,
}
},
created() {
if (!this.$parent.$scopedSlots.default) {
this.tree = this
} else {
this.tree = this.$parent.tree
}
},
};
</script>

<style scoped>
.tree {
padding-left: 10px;
}

ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
</style>

这里搞了一个 node 的函数组件,node 函数组件拿到 子组件的 tree, tree也是一层层的保存着 $scopedSlots.default 其实就是 C 的那些编译节点。 然后把 title 传给了 C。


el-tree 源码贴图


image.png


tree


image.png


tree-node


image.png


image.png


image.png


image.png


总结


写的有点乱啊,这个只是辅助你理解 递归插槽,其实一开始都是懵逼了,多看下代码理解还是能看的懂的。


作者:晓欲望
来源:juejin.cn/post/7222931700438138937
收起阅读 »

不用刷新!用户无感升级,解决前端部署最后的问题

web
前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。 一、背景 网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。 二...
继续阅读 »

前端部署需要用户刷新才能继续使用,一直是一个老大难的用户体验问题。本文将围绕这个问题进行讲解,揭晓问题发生的原因及解决思路。


一、背景


网站发版过程中,用户可在浏览web页面时,可能会导致页面无法加载对应的资源,导致出现点击无反应的情况,严重影响用户体验。


二、问题分析


2.1 问题现象


网络控制台显示加载页面的资源显示404。


image.png


2.2 满足条件


发生这个现象,需要满足三个条件:



  1. 站点是SPA页面,并开启懒加载;

  2. 资源地址启用内容hash。(加载更快启用了强缓存,为了应对资源变更能及时更新内容,会对资源地址的文件名加上内容hash)。

  3. 覆盖式部署,新版本发布后旧的版本会被删除。


特别在容器部署的情况的SPA页面,很容易满足上诉三个条件。笔者在做公司的内部系统就踩过坑。


2.3 原因分析


浏览器打开页面后,会记录路由和资源路径的映射,服务器发版后,没有及时通知浏览器更新路由映射表。导致用户在发布前端打开的页面,在版本更新后,进入下一个路由加载上一个版本的资源失败,导致需要用户刷新才能正常使用。


image.png


三、解决方案


3.1 方案一:失败重试


3.1.1 思路整理:


既然加载失败了,就重试加载发版后的资源版本就行。增加一个manifest.json文件能够获取新版本对应的资源路径。


image.png


3.1.2 举例说明


以vue项目进行举例子说明:


第一步: 修改构建工具配置以生成manifest文件


使用vite构建的项目,可以在vite.config.ts增加配置build.manifest为true,用以生成manifest.json文件


export default defineConfig({
// 更多配置
build: {
//开启manifest
manifest: true,
cssCodeSplit: false //关闭单独生成css文件,方便demo演示
}
})

如果使用webpack构建的项目,可以使用webpack-manifest-plugin插件进行配置。


进行项目生产构建,生成manifest.json,内容如下:


 // 简单说明:文件内容是项目原始代码目录结构和构建生成的资源路径一一对应
{
"index.html": { // 页面入口
"dynamicImports": ["src/pages/page1.vue", "src/pages/page2.vue"],
"file": "assets/index-e170761c.js",
"isEntry": true,
"src": "index.html"
},
// page1对应单文件组件
"src/pages/page1.vue": {
"file": "assets/page1-515906ab1.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page1.vue"
},
// page2对应单文件组件
"src/pages/page2.vue": {
"file": "assets/page2-9785c68c.js", // JS文件
"imports": ["index.html"],
"isDynamicEntry": true,
"src": "src/pages/page2.vue"
},
"style.css": {
"file": "assets/style-809e5baa.css",
"src": "style.css"
}
}

第二步,修改route文件,加上重试逻辑


在路由文件中,增加加载页面js失败的重试逻辑,通过新版的manifest.json来获取新版的页面js,再次加载。


import { createRouter, createWebHistory } from 'vue-router'


const router = createRouter({
history: createWebHistory('/'),
routes: [
{
path: '/page1',
// component: () => import(`../pages/page1.vue`), // 变更前
component: () => retryImport('page1'), // 变更后
},
{
path: '/page2',
// component: () => import(`../pages/page1.vue`),
component: () => retryImport('page2'),
},
]
})


async function retryImport(page) {
try {
// 加载页面资源
switch (page) {
case 'page1':
// 这里demo演示,没有使用dynamic-import-vars
return await import(`../pages/page1.vue`)
default:
return await import(`../pages/page2.vue`)
}
} catch (err: any) {
// 判断是否是资源加载错误,错误重试
if (err.toString().indexOf('Failed to fetch dynamically imported module') > -1) {
// 获取manifest资源清单
return fetch('/manifest.json').then(async (res) => {
const json = await res.json()
// 找到对应的最新版本的js
const errPage = `src/pages/${page}.vue`
// 加载新的js
return await import(`/${json[errPage].file}`)
})
}
throw err
}
}
export default router

3.1.3 总结


这个方案改造只涉及前端层,成本最低,但是无法做到多版本共存,只能适配部分发版变更,如果涉及删除页面的版本,最好增加一个容错页面。


3.2 方案二:增量部署


3.2.1 思路整理


生产环境发布改成增量发布,不再是覆盖式的发布,发版后旧版本依旧保留。


image.png


3.2.2 示例实践


需要改造构建配置,增加版本的概念,保证新旧版本不路径冲突


vite 构建工具示例:


// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const version = require('./package.json').version

// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
manifest: true,
assetsDir: `./${versionName}`, // 版本号
}
})

webpack构建工具示例:


// webpack.config.js
const path = require('path');
// 支持构建命令传入构建版本
const versionName = process.env.VERSION_NAME || version
module.exports = {
//...
output: {
path: path.resolve(__dirname, `dist/${versionName}/assets`),
},
};

3.2.3 总结


需要CI/CD发版改造,由之前的容器部署改成静态部署(即文件上传对象存储的思路一样),这种增量发部署适配全部的场景,而且,支持多版本共存,能做到版本灰度放量。


四、总结


本文通过覆盖式部署在前端版本发布,导致用户不可使用的严重体验问题,分析问题发生根因,并给出两种解决思路。笔者结合公司的云设施,最后使用增量部署,并BFF层配合使用,多版本共存,线上启用通过配置指定启用哪个版本,再也不用赶着时间点去发版。


作者:azuo
来源:juejin.cn/post/7223196531143131194
收起阅读 »

VUE中常用的4种高级方法

web
1. provide/inject provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在...
继续阅读 »

1. provide/inject


provide/inject 是 Vue.js 中用于跨组件传递数据的一种高级技术,它可以将数据注入到一个组件中,然后让它的所有子孙组件都可以访问到这个数据。通常情况下,我们在父组件中使用 provide 来提供数据,然后在子孙组件中使用 inject 来注入这个数据。


使用 provide/inject 的好处是可以让我们在父组件和子孙组件之间传递数据,而无需手动进行繁琐的 props 传递。它可以让代码更加简洁和易于维护。但需要注意的是,provide/inject 的数据是非响应式的,这是因为provide/inject是一种更加底层的 API,它是基于依赖注入的方式来传递数据,而不是通过响应式系统来实现数据的更新和同步。


具体来说,provide方法提供的数据会被注入到子组件中的inject属性中,但是这些数据不会自动触发子组件的重新渲染,如果provide提供的数据发生了变化,子组件不会自动感知到这些变化并更新。


如果需要在子组件中使用provide/inject提供的数据,并且希望这些数据能够响应式地更新,可以考虑使用Vue的响应式数据来代替provide/inject。例如,可以将数据定义在父组件中,并通过props将其传递给子组件,子组件再通过$emit来向父组件发送数据更新的事件,从而实现响应式的数据更新。


下面是一个简单的例子,展示了如何在父组件中提供数据,并在子孙组件中注入这个数据:


<!-- 父组件 -->
<template>
<div>
<ChildComponent />
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
provide: {
message: 'Hello from ParentComponent',
},
components: {
ChildComponent,
},
};
</script>

//上面provide还可以写成函数形式
export default {
provide(){
return {
message: this.message
}
}
}


<!-- 子组件 -->
<template>
<div>
<GrandchildComponent />
</div>
</template>

<script>
import GrandchildComponent from './GrandchildComponent.vue';

export default {
inject: ['message'],
components: {
GrandchildComponent,
},
};
</script>


<!-- 孙子组件 -->
<template>
<div>
<p>{{ message }}</p>
</div>
</template>

<script>
export default {
inject: ['message'],
};
</script>


在上面的例子中,父组件中提供了一个名为 message 的数据,子孙组件中都可以使用 inject 来注入这个数据,并在模板中使用它。注意,子孙组件中的 inject 选项中使用了一个数组,数组中包含了需要注入的属性名。在这个例子中,我们只注入了一个 message 属性,所以数组中只有一个元素。


2. 自定义v-model


要使自定义的Vue组件支持v-model,需要实现一个名为value的prop和一个名为input的事件。在组件内部,将value prop 绑定到组件的内部状态,然后在对内部状态进行修改时触发input事件。


下面是一个简单的例子,展示如何创建一个自定义的输入框组件并支持v-model:


<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
name: 'MyInput',
props: {
value: String
}
};
</script>


在上面的组件中,我们定义了一个value prop,这是与v-model绑定的数据。我们还将内置的input事件转发为一个自定义的input事件,并在事件处理程序中更新内部状态。现在,我们可以在父组件中使用v-model来绑定这个自定义组件的值,就像使用普通的输入框一样:


<template>
<div>
<my-input v-model="message" />
<p>{{ message }}</p>
</div>
</template>

<script>
import MyInput from './MyInput.vue';

export default {
components: {
MyInput
},
data() {
return {
message: ''
};
}
};
</script>


在上面的代码中,我们通过使用v-model指令来双向绑定message数据和MyInput组件的值。当用户在输入框中输入文本时,MyInput组件会触发input事件,并将其更新的值发送给父组件,从而实现了双向绑定的效果。


3. 事件总线(EventBus)


Vue事件总线是一个事件处理机制,它可以让组件之间进行通信,以便在应用程序中共享信息。在Vue.js应用程序中,事件总线通常是一个全局实例,可以用来发送和接收事件。


以下是使用Vue事件总线的步骤:


3.1 创建一个全局Vue实例作为事件总线:


import Vue from 'vue';
export const eventBus = new Vue();

3.2 在需要发送事件的组件中,使用$emit方法触发事件并传递数据:


eventBus.$emit('eventName', data);

3.3 在需要接收事件的组件中,使用$on方法监听事件并处理数据:


eventBus.$on('eventName', (data) => {
// 处理数据
});

需要注意的是,事件总线是全局的,所以在不同的组件中,需要保证事件名称的唯一性。


另外,需要在组件销毁前使用$off方法取消事件监听:


eventBus.$off('eventName');

这样就可以在Vue.js应用程序中使用事件总线来实现组件之间的通信了。


4. render方法


Vue 的 render 方法是用来渲染组件的函数,它可以用来替代模板语法,通过代码的方式来生成 DOM 结构。相较于模板语法,render 方法具有更好的类型检查和代码提示。


下面详细介绍 Vue 的 render 方法的使用方法:


4.1 基本语法


render 方法的基本语法如下:


render: function (createElement) {
// 返回一个 VNode
}

其中 createElement 是一个函数,它用来创建 VNode(虚拟节点),并返回一个 VNode 对象。


4.2 创建 VNode


要创建 VNode,可以调用 createElement 函数,该函数接受三个参数:



  • 标签名或组件名

  • 可选的属性对象

  • 子节点数组


例如,下面的代码创建了一个包含文本节点的 div 元素:


render: function (createElement) {
return createElement('div', 'Hello, world!')
}

如果要创建一个带有子节点的元素,可以将子节点作为第三个参数传递给 createElement 函数。例如,下面的代码创建了一个包含两个子元素的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('h1', 'Hello'),
createElement('p', 'World')
])
}

如果要给元素添加属性,可以将属性对象作为第二个参数传递给 createElement 函数。例如,下面的代码创建了一个带有样式和事件处理程序的 button 元素:


render: function (createElement) {
return createElement('button', {
style: { backgroundColor: 'red' },
on: {
click: this.handleClick
}
}, 'Click me')
},
methods: {
handleClick: function () {
console.log('Button clicked')
}
}

4.3 动态数据


render 方法可以根据组件的状态动态生成内容。要在 render 方法中使用组件的数据,可以使用 this 关键字来访问组件实例的属性。例如,下面的代码根据组件的状态动态生成了一个带有计数器的 div 元素:


render: function (createElement) {
return createElement('div', [
createElement('p', 'Count: ' + this.count),
createElement('button', {
on: {
click: this.increment
}
}, 'Increment')
])
},
data: function () {
return {
count: 0
}
},
methods: {
increment: function () {
this.count++
}
}


4.4 JSX


在使用 Vue 的 render 方法时,也可以使用 JSX(JavaScript XML)语法,这样可以更方便地编写模板。要使用 JSX,需要在组件中导入 VuecreateElement 函数,并在 render 方法中使用 JSX 语法。例如,下面的代码使用了 JSX 语法来创建一个计数器组件:


import Vue from 'vue'

export default {
render() {
return (
<div>
<p>Count:{this.count}</p>
<button onClick={this.increment}>Increment</button>
</div>

)
},
data() {
return { count: 0 }
},
methods: {
increment() {
this.count++
}
}
}


注意,在使用 JSX 时,需要使用 {} 包裹 JavaScript 表达式。


4.5 生成函数式组件


除了生成普通的组件,render 方法还可以生成函数式组件。函数式组件没有状态,只接收 props 作为输入,并返回一个 VNode。因为函数式组件没有状态,所以它们的性能比普通组件更高。


要生成函数式组件,可以在组件定义中将 functional 属性设置为 true。例如,下面的代码定义了一个函数式组件,用于显示列表项:


export default {
functional: true,
props: ['item'],
render: function (createElement, context) {
return createElement('li', context.props.item);
}
}

注意,在函数式组件中,props 作为第二个参数传递给 render<

作者:阿虎儿
来源:juejin.cn/post/7225921305597820985
/code> 方法。

收起阅读 »

代码重构和架构重构:你需要了解的区别

1 代码重构 定义 对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。 目的 增加可读性、增加可维护性、可扩展性 3 关键点 不影响输出 不修正错误 不增加新的功能性 代码重构时,发现有个功能实现逻辑不合理,可直接修改吗? 当然不可! 2 架构...
继续阅读 »

1 代码重构


定义


对软件代码做任何改动以增加可读性或者简化结构而不影响输出结果。


目的


增加可读性、增加可维护性、可扩展性


3 关键点



  • 不影响输出

  • 不修正错误

  • 不增加新的功能性


代码重构时,发现有个功能实现逻辑不合理,可直接修改吗?


当然不可!


2 架构重构


定义


通过整系统结构(4R)来修复系统质量问题而不影响整体系统能力。


目的


修复质量问题(性能、可用性、可扩展......)


关键点



  • 修复质量(架构,而非代码层面的质量)问题,提升架构质量

  • 不影响整体系统功能

  • 架构本质没有发生变化


把某个子系统的实现方式从硬编码改为规则引擎,是代码重构还是架构重构?


属于架构重构,架构设计方案了,实现系统可扩展性。


3 代码重构 V.S 架构重构



4 架构重构技巧


4.0 手段



架构重构是否可以修改 4R 中的 Rank?


不能!修改 rank 就不是重构,而是演进了。拆微服务不属于改 rank。外部系统协作方式都得修改了。比如将淘宝的支付方式支付宝拆出来,成为支付宝公司了。


4.1 先局部优化后架构重构


局部优化


定义:对部分业务或者功能进行优化,不影响系统架构。


常见手段:



  • 数据库添加索引,优化索引

  • 某个数据缓存更新策略采用后台更新

  • 增加负载均衡服务数量

  • 优化代码里面并发的逻辑

  • 修改Innodb buffer pool 配置,分配更多内存

  • 服务间的某个接口增加1个参数


架构重构


定义:优化系统架构,整体提升质量,架构重构会影响架构的4R定义。


常见手段:



  • 引入消息队列(增加 Role )

  • 去掉 ZooKeeper,改为内置 Raft 算法实现(删除 Role)

  • 将 Memcached 改为 Redis( 改变 Role)

  • 按照稳定性拆分微服务( 拆分 Role )

  • 将粒度太细的微服务合并(合并 Role)

  • 将服务间的通信方式由 HTTP 改为 gRPC(修改 Relation )

  • SDK从读本地配置文件改为从管理系统读取配置(修改Rule )


4.2 有的放矢



案例




  • 开发效率很慢,P业务和M系统互相影响

  • 线上问题很多,尤其是数据类问题

  • M系统性能很低


有的放矢:



重构只解决第1个问题(开发效率很慢,P业务和M系统互相影响)。其他问题咋办,架构师你不解决了吗?架构重构后了,各个业务部门再解决各自的问题,如 P业务后台优化自己的问题,M 系统优化自己的性能问题,因为这些问题本身靠重构是解决不了的,而是要靠重构拆分之后,各自再继续优化。


4.3 合纵连横


合纵


说服业务方和老板




  1. 以数据说话


    把“可扩展性”转换为“版本开发速度很慢然后给出对应的项目数据(平时注意搜集数据)。




  2. 以案例说话(其实更有效,给人的冲击力更明显) 若没有数据,就举极端案例,如某个小功能,开发测试只要5天,但是等了1个月才上线。




连横


说服其它团队。



  1. 换位思考 思考对其它团队的好处,才能让人配合。

  2. 合作双赢 汇报和总结的时候,把其它团队也带上。


案例


合纵:告诉PM和项目经理极端案例,设计2周、开发2天、一个月才上线。


连横:P业务线上问题大大减少,P业务不会被其它业务影响


4.4 运筹帷幄


① 问题分类


将问题分类,一段时间集中处理类问题。 避免对照 Excel表格,一条条解决。


② 问题排序


分类后排序,按照优先级顺序来落地。


避免见缝插针式的安排重构任务,不要搭业务的顺风车重构:



  • 避免背锅

  • 效果不明显

  • 无法安排工作量大的重构


③ 逐一攻破


每类问题里面先易后难。


把容易的问题解决掉,增强信心。


④ 案例


Before:



  • 1个100多行的Excel问题表格,一个一个的解决

  • 专挑软柿子捏

  • 见缝插针


After:



  1. 分类:性能、组件、架构、代码

  2. 分阶段: 优化-> 架构重构 -> 架构演进

  3. 专项落地: 明确时间、目标、版本



5 架构重构FAQ


架构重构是否可以引入新技术?


可以,但尽量少,架构重构要求快准


业务不给时间重构怎么办 ?


会哭的孩了有奶吃。收集数据和案例,事实说话。


其它团队不配合怎么办 ?


学会利用上级力量。上级都不支持,说明你做的这个没意义,所以领导也不在乎。那就别做了。


业务进度很紧,人力不够怎么办 ?


收集需要重构的证据,技术汇报的时候有理有据



6 测试


6.1 判断



  1. 代码重构、架构重构、架构演进都不需要去修复问题 ×

  2. 微服务拆分既可以是架构重构的手段,也可以是架构演进的手段 √

  3. 架构重构应该搭业务版本的便车,可以避免对业务版本有影响 ×

  4. 架构重构是为修复问题,因此应该将系统遗留的问题都在架构重构的时候修复 ×

  5. 架构重构应该分门别类,按照优先级逐步落地 √


6.2 思考


架构重构的时候是否可以顺手将代码重构也做了 ? 因为反正都安排版本了。No!


局部优化不属于代码/架构重构。


作者:JavaEdge在掘金
链接:https://juejin.cn/post/7222288378459848762
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Redis 使用zset做消息队列总结

1.zset为什么可以做消息队列 zset做消息队列的特性有: 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。 元素唯一性:zset的每个元素都是独一无二的...
继续阅读 »

1.zset为什么可以做消息队列


zset做消息队列的特性有:



  1. 有序性:zset中所有元素都被自动排序。这让zset很适合用于有序的消息队列,因为可以根据一个或多个标准(比如消息的到达时间或优先级)按需检索消息。

  2. 元素唯一性:zset的每个元素都是独一无二的,这对于实现某些消息需求(比如幂等性)是非常有帮助的。

  3. 成员和分数之间的映射关系:有序集合中的每个成员都有一个分数,这样就可以将相同的数据划分到不同的 queue 中,以及为每个 queue 设置不同的延时。

  4. 高效的添加删除操作:因为zset会自动维护元素之间的顺序,所以在添加或删除元素时无需进行手动排序,从而能提升操作速度。


Redis的zset天然支持按照时间顺序的消息队列,可以利用其成员唯一性的特性来保证消息不被重复消费,在实现高吞吐率等方面也有很大的优势。


2.zset实现消息队列的步骤


Redis的zset有序集合是可以用来实现消息队列的,一般是按照时间戳作为score的值,将消息内容作为value存入有序集合中。


以下是实现步骤:



  1. 客户端将消息推送到Redis的有序集合中。

  2. 有序集合中,每个成员都有一个分数(score)。在这里,我们可以设成消息的时间戳,也就是当时的时间。

  3. 当需要从消息队列中获取消息时,客户端获取有序集合前N个元素并进行操作。一般来说,N取一个适当的数值,比如10。


需要注意的是,Redis的zset是有序集合,它的元素是有序的,并且不能有重复元素。因此,如果需要处理有重复消息的情况,需要在消息体中加入某些唯一性标识来保证不会重复。


3.使用jedis实现消息队列示例


Java可以通过Redis的Java客户端包Jedis来使用Redis,Jedis提供了丰富的API来操作Redis,下面是一段实现用Redis的zset类型实现的消息队列的代码。


import redis.clients.jedis.Jedis;
import java.util.Set;

public class RedisMessageQueue {
  private Jedis jedis; //Redis连接对象
  private String queueName; //队列名字

  /**
    * 构造函数
    * @param host Redis主机地址
    * @param port Redis端口
    * @param password Redis密码
    * @param queueName 队列名字
    */
  public RedisMessageQueue(String host, int port, String password, String queueName){
      jedis = new Jedis(host, port);
      jedis.auth(password);
      this.queueName = queueName;
  }

  /**
    * 发送消息
    * @param message 消息内容
    */
  public void sendMessage(String message){
      //获取当前时间戳
      long timestamp = System.currentTimeMillis();
      //将消息添加到有序集合中
      jedis.zadd(queueName, timestamp, message);
  }

  /**
    * 接收消息
    * @param count 一次接收的消息数量
    * @return 返回接收到的消息
    */
  public String[] receiveMessage(int count){
      //设置最大轮询时间
      long timeout = 5000;
      //获取当前时间戳
      long start = System.currentTimeMillis();

      while (true) {
          //获取可用的消息数量
          long size = jedis.zcount(queueName, "-inf", "+inf");
          if (size == 0) {
              //如果无消息,休眠50ms后继续轮询
              try {
                  Thread.sleep(50);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          } else {
              //计算需要获取的消息数量count与当前可用的消息数量size的最小值
              count = (int) Math.min(count, size);
              //获取消息
              Set<String> messages = jedis.zrange(queueName, 0, count - 1);
              String[] results = messages.toArray(new String[0]);
              //移除已处理的消息
              jedis.zremrangeByRank(queueName, 0, count - 1);
              return results;
          }

          //检查是否超时
          if (System.currentTimeMillis() - start > timeout) {
              return null; //超时返回空
          }
      }
  }

  /**
    * 销毁队列
    */
  public void destroy(){
      jedis.del(queueName);
      jedis.close();
  }
}


使用示例:


public static void main(String[] args) {
  //创建消息队列
  RedisMessageQueue messageQueue = new RedisMessageQueue("localhost", 6379, "password", "my_queue");

  //生产者发送消息
  messageQueue.sendMessage("message1");
  messageQueue.sendMessage("message2");

  //消费者接收消息
  String[] messages = messageQueue.receiveMessage(10);
  System.out.println(Arrays.toString(messages)); //输出:[message1, message2]

  //销毁队列
  messageQueue.destroy();
}


在实际应用中,可以结合线程池或者消息监听器等方式,将消息接收过程放置于独立的线程中,以提高消息队列的处理效率。


4.+inf与-inf


+inf 是 Redis 中用于表示正无穷大的一种特殊值,也就是无限大。在使用 Redis 的 zset 集合时,+inf 通常用作 ZREVRANGEBYSCORE 命令的上限值,表示查找 zset 集合中最大的分数值。+inf 后面的 -inf 表示 zset 中最小的分数值。这两个值一起可以用来获取 zset 集合中的所有元素或一个特定范围内的元素。例如:


# 获取 zset 集合中所有元素
ZREVRANGE queue +inf -inf WITHSCORES

# 获取 zset 集合中第1到第10个元素(分数从大到小排列)
ZREVRANGE queue +inf -inf WITHSCORES LIMIT 0 9

# 获取 zset 集合中分数在 1581095012 到当前时间之间的元素
ZREVRANGEBYSCORE queue +inf 1581095012 WITHSCORES

在这些命令中,+inf 代表了一个最大的分数值,-inf 代表了一个最小的分数值,用于确定查询的分数值范围。


5.redis使用list与zset做消息队列有什么区别


Redis 使用 List 和 ZSET 都可以实现消息队列,但是二者有以下不同之处:



  1. 数据结构不同:List 是一个有序的字符串列表,ZSET 则是一个有序集合,它们的底层实现机制不同。

  2. 存储方式不同:List 只能存储字符串类型的数据,而 ZSET 则可以存储带有权重的元素,即除了元素值外,还可以为每个元素指定一个分数。

  3. 功能不同: List 操作在元素添加、删除等方面比较方便,而 ZSET 在处理数据排序和范围查找等方面比 List 更加高效。

  4. 应用场景不同: 对于需要精细控制排序和分值的场景可以选用 ZSET,而对于只需要简单的队列操作,例如先进先出,可以直接采用 List。


综上所述,List 和 ZSET 都可以用于消息队列的实现,但如果需要更好的性能和更高级的排序功能,建议使用 ZSET。而如果只需要简单的队列操作,则 List 更加适合。


6.redis用zset做消息队列会出现大key的情况吗


在Redis中,使用zset作为消息队列,每个消息都是一个元素,元素中有一个分数代表了该消息的时间戳。如果系统中有大量消息需要入队或者大量的不同的队列,这个key的体积会越来越大,从而可能会出现大key的情况。


当Redis存储的某个键值对的大小超过实例的最大内存限制时,会触发Redis的内存回收机制,可以根据LRU算法等策略来选择需要回收的数据,并确保最热数据保持在内存中。如果内存不足,可以使用Redis的持久化机制,将数据写入磁盘。使用Redis集群,并且将数据分片到多个节点上,也是一种可以有效解决大key问题的方法。


针对大key的问题,可以考虑对消息进行切分,将一个队列切分成多个小队列,或者对消息队列集合进行分片,将消息分布到不同的Redis实例上,从而降低单个Redis实例的内存使用,并提高系统的可扩展性。


7.redis 用zset做消息队列如何处理消息积压



  1. 改变消费者的消费能力:


可以增加消费者的数量,或者优化消费者的消费能力,使其能够更快地处理消息。同时,可以根据消息队列中消息的数量,动态地调整消费者的数量、消费速率和优先级等参数。



  1. 对过期消息进行过滤:


将过期的消息移出消息队列,以减少队列的长度,从而使消费者能够及时地消费未过期的消息。可以使用Redis提供的zremrangebyscore()方法,对过期消息进行清理。



  1. 对消息进行分片:


将消息分片,分布到不同的消息队列中,使得不同的消费者可以并行地处理消息,以提高消息处理的效率。



  1. 对消息进行持久化:


使用Redis的持久化机制,将消息写入磁盘,以防止消息的丢失。同时,也可以使用多个Redis节点进行备份,以提高Redis系统的可靠性。


总的来说,在实际应用中,需要根据实际情况,综合考虑上述方法,选择适合自己的方案,以保证Redis的消息队列在处理消息积压时,能够保持高效和稳定。


8. redis使用zset做消息队列时,有多个消费者同时消费消息怎么处理


当使用 Redis 的 zset 作为消息队列时,可以通过以下方式来处理多个消费者同时消费消息:



  1. 利用Redis事务特性:zset中的元素的score会反映该元素的优先级,多个消费者可以使用Redis事务特性,采用原子性的操作将空闲的消息数据上锁,只有在被加锁的消费者消费完当前消息时,往消息队列中发送释放锁的指令,其它消费者才能够获得该消息并进行消费。

  2. 利用Redis分布式锁:使用 Redis 实现分布式锁来实现只有一个消费者消费一条消息,可以使用redis的SETNX命令(如果键已存在,则该命令不做任何事,如果密钥不存在,它将设置并返回1可以用作锁),将创建一个新的键来表示这一消息是否已经被锁定。

  3. 防止重复消费:为了防止多个消费者消费同一条消息,可以在消息队列中添加一个消息完成的标记,在消费者处理完一条消息之后,会将该消息的完成状态通知给消息队列,标记该消息已经被消费过,其它消费者再次尝试消费该消息时,发现已经被标记为完成,则不再消费该消息。


无论采用哪种方式,都需要保证消息队列的可靠性和高效性,否则会导致消息丢失或重复消费等问题。


9.redis使用zset做消息队列如何实现一个分组的功能


Redis 中的 Zset 可以用于实现一个有序集合,其中每个元素都会关联一个分数。在消息队列中,可以使用 Zset 来存储消息的优先级(即分数),并使用消息 ID 作为 Zset 中的成员,这样可以通过 Zset 的有序性来获取下一条要处理的消息。


为了实现一个分组的功能,可以使用 Redis 的命名空间来创建多个 Zset 集合。每个分组都有一个对应的 Zset 集合,消息都被添加到对应的集合中。然后,你可以从任何一个集合中获取下一条消息,这样就可以实现分组的功能。


例如,假设你的 Redis 实例有三个 Zset 集合,分别是 group1、group2 和 group3,你可以按照如下方式将消息添加到不同的分组中:


ZADD group1 1 message1
ZADD group2 2 message2
ZADD group3 3 message3

然后,你可以通过以下方式获取下一条要处理的消息:


ZRANGE group1 0 0 WITHSCORES
ZRANGE group2 0 0 WITHSCORES
ZRANGE group3 0 0 WITHSCORES

将返回结果中的第一个元素作为下一条要处理的消息。由于每个分组都是一个独立的 Zset 集合,因此它们之间是相互独立的,不会干扰彼此。


10. redis使用zset做消息队列有哪些注意事项


Redis 使用 ZSET 做消息队列时,需要注意以下几点:



  1. 消息的唯一性:使用 ZSET 作为消息队列存储的时候需要注意消息的唯一性,避免重复消息的情况出现。可以考虑使用消息 ID 或者时间戳来作为消息的唯一标识。

  2. 消息的顺序:使用 ZSET 作为消息队列存储可以保证消息的有序性,但消息的顺序可能不是按照消息 ID 或者时间戳的顺序。可以考虑在消息中增加时间戳等信息,然后在消费时根据这些信息对消息进行排序。

  3. 已消费的消息删除:在使用 ZSET 作为消息队列的时候需要注意如何删除已经消费的消息,可以使用 ZREMRANGEBYLEX 或者 ZREMRANGEBYSCORE 命令删除已经消费的消息。

  4. 消息堆积问题:ZSET 作为一种有序存储结构,有可能出现消息堆积的情况,如果消息队列里面的消息堆积过多,会影响消息队列的处理速度,甚至可能导致 Redis 宕机等问题。这个问题可以使用 Redis 定时器来解决,定期将过期的消息从队列中删除。

  5. 客户端的能力:在消费消息的时候需要考虑客户端的能力,可以考虑增加多个客户端同时消费消息,以提高消息队列的处理能力。

  6. Redis 节点的负载均衡:使用 ZSET 作为消息队列的存储结构,需要注意 Redis 节点的负载均衡,因为节点的并发连接数可能会受到限制。必要的时候可以增加 Redis 节点数量,或者采用 Redis 集群解决这个问题。


总之,使用 ZSET 作为消息队列存储需要特别注意消息的唯一性、消息的顺序、已消费消息删除、消息堆积问题、客户端的能力和节点的负载均衡等问题。


作者:香吧香
链接:https://juejin.cn/post/7221910537432825916
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?

问题 你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不? 认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。 第一,提升学历 从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业...
继续阅读 »

问题


你好,我是一名大专的大一学生,计算机科班,未来想从事互联网,能给点什么建议不?


认真考虑一下,想到了以下几点,如有不对的地方欢迎批评指正。


第一,提升学历


从现在起为升本做好准备,做好规划,然后每天去认认真真的执行,如果可能,尽量考研。因为现在互联网行业确实很卷,等你大专毕业的时候应该会更卷。现在的招聘越来越看重学历,学历不行你连面试机会都没有。你可能听说过有大专也进大厂的,那是前几年,也是心存着偏差,不能去赌这个。现在的事实是专科面试的机会很少很少。作为一个普通人,我感觉去接受更高等的教育,未来才会有更多的选择,才能更好的去掌握自己的命运。


第二,学好基础知识


在学习上,一定要想法设法学好数据结构、数据库、操作系统、计算机网络、计算机组成原理、英语、数学等等。尽量每天都抽出点儿时间来学点。你可能会说,听别人说这些东西好像在工作中也不常用呀,对,一般的工作确实不常用,但正是这些决定了你未来能走多远。这些东西是你从事这个行业的根基,你这个根基越稳固,你的未来发展就会越好。出了新东西你才能更快的掌握。假设互联网真不行了,你有这些底子在,你可以很快的去切到其他行业一些软件儿上的开发,如果说你没有这个底子,想迅速切换想都别想。


第三,锻炼合作能力


在学校多去参加一些计算机类的比赛,比如说像一些算法相关的,锻炼自己与他人合作的能力。


第四,参与写作、开源


业余时间去写写博客儿,参加一些开源项目,这些对于你毕业后找工作都是有帮助的。不得不承认,现在的大学生也挺卷的,在学校就各种源码,算法各种卷。你需要制造点不一样,想想你毕业之后有一个不错的博客儿,有一个成百上千小星星的开源项目儿,肯定是加分。


第五,学习人情世故


学习之外适当的去兼顾一些人情往来之类的东西,注意是适当。比如宿舍的一些聚餐呀,学校举办的一些比赛,建议多参加唱歌、演讲、辩论类的比赛,锻炼自己的表达、表现力、还有心里素质。不要太在意别人的看法,没那么多人在意你,这个道理越早知道越好。你迟早是要步入社会的,不是说你技术多好,你就能混的多好,人情世故这个东西,很重要,早锻炼比晚锻炼强,真的,赶早不赶晚。


第六,锻炼身体


永远记住身体是革命的本钱,没有一个好身体啥都没用,要抽时间锻炼身体哈。


作者:程序员黑黑
链接:https://juejin.cn/post/7230603857033248829
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

裁员、结婚、买房、赡养父母.....即将30岁,焦虑扑面而来

前言: 大家好,我是春风。 不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。 就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也...
继续阅读 »

前言:


大家好,我是春风。


不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。


就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也突然就不再属于你自己,我们会不自觉的扮演起家庭的依靠,而且还是唯一的依靠。这种压力完全是在自己还没准备的时候就突袭了你。


就像我这一周都是在这种压力和焦虑中度过...


192b2a6322f5c0559f59d567effb7bfb.jpeg


失眠


我不知道自己为什么会突然一下就想这么多,但年龄就像一个雷管,突然就炸开了赤裸裸的现实,或者是按下了一个倒计时。我不自觉的去想家庭,去想父母,去想我30岁40岁50|岁是什么样子。


这周的每天晚上我想着这些都失眠到三四点,当然如果这个时候你还像我一样去看下确切时间,你很大可能会失眠到五点。


尝试心理学


所以这几天上班也是一行代码都没敲,幸好需求不多。最后我迫切的觉得我应该找个办法解决一下,索性今天摸鱼一天,听了一天的心理学讲座的音频。


果然,心病还需心药医!!!


下面我给大家分享一下自己的治疗过程,希望也能对焦虑的你有所启发。
 


一、我为什么焦虑


解决焦虑的第一步就是先要弄清楚我们为什么焦虑?我们究竟在焦虑什么?可能很多人都是焦虑经济,焦虑结婚,焦虑生活中的各种琐事。


但我们也可以试着站在上帝视角,更深层次的解剖一下自己。


1. 焦虑多年努力没有换来想要的生活


比如我,我最大的焦虑也是钱,我从农村出来,没有任何背景,毕业到现在已经工作六年,20年在广州买房上车,但好巧不巧买的是恒大的房子,买完就暴雷,现在每个月有房贷,还要结婚。


所以我总是在想,这些年我算努力吗,为什么还是没有挣到钱。


三十而立近在眼前,可我这些年究竟立了什么呢?遥想刚毕业那会给自己定下的目标,虽然是天方夜谭,但对比现在,也太天方夜谭了吧。


不是说好的天道酬勤吗?不是说努力就会有收获吗?


所以我焦虑,我表面是焦虑钱,但何尝不是在焦虑自己这么多年的努力却没有得到我想要的结果呢?


2. 攀比带来的自我嫌弃


我们都知道攀比是不好的,尤其是在这个动辄年薪百万年薪的互联网世界,但也是这些网络信息的无孔不入,让我们不得不攀比,不得不怀疑自己是为什么会差这么多。


我承认自己是一个争强好胜的人,我会在读书时非常想要好的名次,因为我体验过那种胜利感一次之后,便会上瘾。所以现在工作,我也时常不自觉的攀比起来,因此,我也深深陷入了自我怀疑和自我嫌弃的枣泥。


为什么我努力学习,辛苦工作,一年下来却不如人家卖一个月炒饭,为什么那个做销售的同学两三个月就赚到了我两年的财富,为什么我工作六年攒下的钱,却还不及人家父母一个月的收租?


和我一样没背景的比我赚的多,有背景的赚的更多。这种怀疑病入膏肓的时候,我都会病态的想,那些富二代肯定都是花花公子,懒惰而不自知,毕竟电影里不都这样演吗?但现实是,别人会比你接受更好的家庭教育,环境教育。别人谈吐自如还努力学习。不仅赢在了起跑线,还比你努力。就是这种对比,越来越让我们自己嫌弃自己,厌恶自己。所以也就总是想要求自己必须去做更好的自己。


二、生命的意义


应该所有人都思考过这个问题吧,来这人间一趟,可都不想白来一趟。我们都想在这个世界留下点什么,就像战国时士人对君主,知道会被烹杀却勇于进言,只为留下一个劝谏名士的美名。人活一世,究竟为了什么呢?生前获利?死后留名?


但对于我们大多数的普通人呢?


待我们死去,我们的名字最多就被孙子辈的人知道,等到他们也故去,那这个世界还会有你来过的痕迹吗?


人生代代无穷已,江月年年望相似。


所以夜深人静的时候,我们总会在想,自己生命的意义?似乎一切都没有意义,我们注定就是会拥有一个低价值甚至无价值的人生


三、结婚的压力


我们九零后,比零零后环境是不是更好不确定,但对比八零后,肯定要差,八零后结婚,印象里还不太谈房子,车子,但我们结婚,确是一个必考题。


所以我们结婚率低,不仅有不婚族,还有现在的丁克族。


我自己来自农村,我们那里男女比例就严重失衡,村里的男孩子结婚的不超过一半。但是我爸着急,不知道你们是否有过这种催婚的经历,父母会反复的告诉你大龄剩男剩女有多丢人,你们的不婚不仅是你自己的问题,还会让家里人都抬不起头。是的,父母含辛茹苦养育了你们,现在因为你,让他们在别人面前抬不起头来,失去了自尊。


四、知道该做什么,但拖延没做后就会更加的自我嫌弃


我们擅长给自己定下很多目标,但有时候就是逃不过人性,孔子说,食色性也。我们在被创造的时候就是被设计为不断的追求多巴胺的动物。所以我们沉迷游戏,沉迷追剧。总是在周五的晚上选择放松自己。而不会因为定下了目标就去学习。


总之,我们的目标定的越美好,我们的行动往往越低效。最后,两者的差距越来越远。我们离自己期望中的那个自己判若两人。


我们又会厌恶自己,嫌弃自己。甚至痛骂自己的不自律。




以上是我分析的自己的焦虑点。相信很多也是屏幕前的你曾经或者当下也有的吧。接下来,就看看我是怎么在心理学上找到解决的办法的吧!


给自己的建议


关于攀比、努力没有想要的结果、不自律等等带来的自我嫌弃。我们或许应该这样看


1、承认自己的普通


有远大报负,有远大理想。追求自由和生命的绚丽是我们每个人都会有也应该有的念想。但当暂时还没有结果的时候。我们不应该及早否定自己。而是勇于承认自己的普通。我们都想成为这个世界上独一无二的人。事实上从某种意义上来说。我们也是独一无二的人。但从金钱,名望这些大家公认的层面来看。99.99%的人都是普通人。我们这一生很大可能就会这样平凡的过完一生。接受自己的普通,活在当下。这才是体验这趟生命之旅最应该有的态度。只要今天比昨天好。我们不就是在进步吗?


为什么一定要有个结果??


人生最大的悲哀就是跨越知道和做到的鸿沟,当一个人承认自己是个普通人的时候,他才是人格完整,真正成熟的时候


我们追求美好的事物,追求自己喜欢的东西,金钱也好,名望也罢,这都是无可厚非的。因为人就是需要被不断满足的,人因为有欲望才会想活下去。但是当暂时没有结果的时候。我们也不应该为此感到自责和焦虑。一旦我们队美好事物的追求变成了一种压力。我们就会陷入一种有负担的内缩状态,反而会跑不快


我们都害怕浪费生命,因为生命只有一次。我们想让自己的生命在这个世界留下来过的痕迹。所以我们追寻那些热爱的东西,但其实追求的过程才是最应该留下的痕迹,结果反而只是别人眼里的痕迹。


当然也有一种理解认为活在当下就是躺平。恰好现在网络上也是躺平之语频频入耳。我想说关于是努力追求理想还是躺平的一点观点。


在禅宗里有这样一句话说的非常好:身无所住而生其心


这里的住 代表的就是追求的一种执念。


身无所住而生其心,说的就是要避免有执和无执的两种极端状态。有执就是我们我都要要要。我要钱 我要名 我要豪车豪宅。无执就是觉得什么都没有意义。生命终会归于尘土。所以努力追求的再多,又有什么用呢?大多数人生命注定是无意义的。这也是很多人躺平的一部分原因吧!


但是就该这样躺平的度过一生吗?每天都陷入低价值的人生?


身无所住而生其心。我们的生命不应该陷入有执和无执这两种极端。花开了,虽然它终会化作春泥。但花开的此刻,它是真美啊!
 


2、关于结婚生子


 
关于结婚生子,为什么我要在所有人都结婚的年龄就结婚,为什么三十岁生孩子就是没出息。生育这个问题,其实是为了什么 我爸老说,你不生小孩或者很晚生小孩,到时候老了都没人照顾你,那养儿真的就是为了防老吗?其实这是一个伪命题,先还不说到时候,儿女孝不孝顺的问题,就说我爸,这么多年,他为了倾其所有,花我身上的钱不说几千万也有上百万了,如果真是要防老,那这个钱存银行,每年光吃利息就有几十万,几十万在一个农村来说晚年怎么都富足了,两三个人照顾你都够,而我到现在每年有给过我爸几十万吗?


再说养儿为了到时候不孤独,能享受天伦之乐,这算是感情上的需求吧。那既然这样,我在准备好的节奏里欣然的生育,不比我在年龄和周遭看法的压力下强行生育更加的好吗,当我想体验一下为人父的生命体验了,我顺其自然的要小孩儿,快快乐乐的养育他,而不是我已经三十岁了,别人小孩儿都打酱油了,大家都在说是不是我有问题,所以即使我现在经济,心理,精力上都没准备好,我也必须要一个小孩儿。


所以大人们说的并不是真正的理由,而人类或者动物,之所以热衷繁衍,最原始的动力是想把自己的基因流下去,是想在这个世界上留下一点记忆。


为别人而活。尤其是在农村,很多人一辈子就认识村里那些人,祖祖辈辈就只见过那些活法,在他们眼里,多少岁结婚,多少岁生孩子,这辈子就这么过去了。但是但凡有一点出格,那在其他人眼里就会抬不起头,因为,其他人出现意外的时候,自己也是这样看其他人的。所以大家都只为活在别人眼里而活,打个比方,我现在很想很想吃一个红薯,明明我吃完这个红薯,内心就会得到满足,但是我不会,因为别人会觉得我是不是穷,都只能吃红薯,这不单单是大家说的死要面子活受罪,其实是我们很多人骨子里的自卑,尤其是我们农村,经济条件都不好,没有什么值得炫耀的,所以我们就尽可能找大家能达成共识的去炫耀。很简单的一个例子。假如一个亿万富翁去到农村,他的身价已经足够自信了,即使他不结婚生子,其他人会看不起他吗?


结尾:


1、心理学是治愈,也是哲学上的思考。这种思考很多都能跳脱出现实而给到我们解决现实中问题的办法


2、再重复一遍:身无所住而生其心!


3、要爱具体的人,不要爱抽象的人,要爱生活,不要爱生活的意义。


作者:程序员春风
链接:https://juejin.cn/post/7119863033920225287
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android 官方架构中的 UseCase 该怎么写?

1. UseCase 的用途 Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。 Android 最新架构:developer.android.com/topi...
继续阅读 »

1. UseCase 的用途


Android 最新的架构规范中,引入了 Domain Layer(译为领域层or网域层),建议大家使用 UseCase 来封装一些复杂的业务逻辑。



Android 最新架构:developer.android.com/topic/archi…



传统的 MVVM 架构中,我们习惯用 ViewModel 来承载业务逻辑,随着业务规模的扩大,ViewModel 变得越来越肥大,职责不清。



Clean Architecture 提出的关注点分离和单一职责(SRP)的设计原则被广泛认可,因此 Android 在最新架构中引入了 Clean Architecture 中 UseCase 的概念。ViewModel 归属 UI Layer,更加聚焦 UiState 的管理,UI 无关的业务逻辑下沉 UseCase,UseCase 与 ViewModel 解耦后,也可以跨 ViewModel 提供公共逻辑。



Android 架构早期的示例代码 todo-app 中曾经引入过 UseCase 的概念,最新架构中只不过是将 UseCase 的思想更明确了,最新的 UseCase 示例可以从官方的 NIA 中学习。




2. UseCase 的特点


官方文档认为 UseCase 应该具有以下几个特点:


2.1 不持有状态


可以定义自己的数据结构类型,但是不能持有状态实例,像一个纯函数一样工作。甚至直接推荐大家将逻辑重写到 invoke 方法中,像调用函数一样调用实例。


下面是 NIA 中的一个示例:GetRecentSearchQueriesUseCase



2.2 单一职责


严格遵守单一职责,一个 UseCase 只做一件事情,甚至其命名就是一个具体行为。扫一眼 UseCase 的文件目录大概就知道 App 的大概功能了。


下面 NIA 中所有 UseCases:



2.3 可有可无


官方文档中将 UseCase 定义为可选的角色,按需定义。简单的业务场景中允许 UI 直接访问 Repository。如果我们将 UseCase 作为 UI 与 Data 隔离的角色,那么工程中会出现很多没有太大价值的 UseCase ,可能就只有一行调用 Repoitory 的代码。


3. 如何定义 UseCase


如上所述,官方文档虽然对 UseCase 给出了一些基本定义,但是毕竟是一个新新生概念,很多人在真正去写代码的时候仍然会感觉不清晰,缺少有效指引。在究竟如何定义 UseCase 这个问题上,还有待大家更广泛的讨论,形成可参考的共识。本文也是带着这个目的而生,算是抛砖引玉吧。


3.1 Optional or Mandatory?


首先,官方文档认为 UseCase 是可选的,虽然其初衷是好的,大家都不希望出现太多 One-Liner 的 UseCase,但是作为一个架构规范切忌模棱两可,这种“可有可无”的规则其结局往往就是“无”。


业务刚起步时由于比较简单往往定义在 Repository 中,随着业务规模的扩大,应该适当得增加 UseCase 封装一些复杂的业务逻辑,但是实际项目中此时的重构成本会让开发者变得“懒惰”,UseCase 最终难产。


那放弃 UseCase 呢?这可能会造成 Repository 的职责不清和无限膨胀,而且 Repository 往往不止有一个方法, ViewModel 直接依赖 Repository 也违反了 SOLID 中的另一个重要原则 ISP ,ViewModel 会因为不相关的 Repository 改动导致重新编译。



ISP(Interface Segregation Principle,接口隔离原则) 要求将接口分离成更小的和更具体的接口,以便调用方只需知道其需要使用的方法。这可以提高代码的灵活性和可重用性,并减少代码的依赖性和耦合性。



为了降低前期判断成本和后续重构成本,如果我们有业务持续壮大的预期,那不妨考虑将 UseCase 作为强制选项。当然,最好这需要研究如何降低 UseCase 带来的模板代码。


3.2 Class or Object?


官方建议使用 Class 定义 UseCase,每次使用都实例化一个新对象,这会做成一些重复开销,那么可否用 object 定义 UseCase 呢?


UseCase 理论上可以作为单例存在,但 Class 相对于 Object 有以下两个优势:



  • UseCase 希望像纯函数一样工作,普通 Class 可以确保每次使用时都会创建一个新的实例,从而避免状态共享和副作用等问题。

  • 普通类可以通过构造参数注入不同的 Repository,UseCase 更利于复用和单元测试


如果我们强烈希望 UseCase 有更长的生命周期,那借助 DI 框架,普通类也可以简单的支持。例如 Dagger 中只要添加 @Singleton 注解即可


@Singleton
class GetRecentSearchQueriesUseCase @Inject constructor(
private val recentSearchRepository: RecentSearchRepository,
) {
operator fun invoke(limit: Int = 10): Flow<List<RecentSearchQuery>> =
recentSearchRepository.getRecentSearchQueries(limit)
}

3.3 Class or Function?


既然我们想像函数一样使用 UseCase ,那为什么不直接定义成 Function 呢?比如像下面这样


fun GetRecentSearchQueriesUseCase : Flow<List<RecentSearchQuery>> 

这确实遵循了 FP 的原则,但又丧失了 OOP 封装性的优势:



  • UseCase 往往需要依赖 Repository 对象,一个 UseCase Class 可以将 Repository 封装为成员存储。而一个 UseCase Function 则需要调用方通过参数传入,使用成本高不说,如果 UseCase 依赖的 Repository 的类型或者数量发生变化了,调用方需要跟着修改

  • 函数起不到隔离 UI 和 Data 的作用,ViewModel 仍然需要直接依赖 Repository,为 UseCase 传参

  • UseCase Class 可以定义一些 private 的方法,相对于 Function 更能胜任一些复杂逻辑的实现


可见,在 UseCase 的定义上 Function 没法取代 Class。当然 Class 也带来一些弊端:



  • 暴露多个方法,破坏 SRP 原则。所以官方推荐用 verb in present tense + noun/what (optional) + UseCase 动词命名,也是想让职责更清晰。

  • 携带可变状态,这是大家写 OOP 的惯性思维

  • 样板代码多


3.4 Function interface ?


通过前面的分析我们知道:UseCase 的定义需要兼具 FP 和 OOP 的优势。这让我想到了 Function(SAM) Interface 。Function Interface 是一个单方法的接口,可以低成本创建一个匿名类对象,确保对象只能有一个方法,同时具有一定封装性,可以通过“闭包”依赖 Repository。此外,Kotlin 对 SAM 提供了简化写法,一定程度也减少了样板代码。



Functional (SAM) interfaces:
kotlinlang.org/docs/fun-in…



改用 Function interface 定义 GetRecentSearchQueriesUseCase 的代码如下:


fun interface GetRecentSearchQueriesUseCase : () -> Flow<List<RecentSearchQuery>>

用它创建 UseCase 实例的同时,实现函数中的逻辑


val recentSearchQueriesUseCase = GetRecentSearchQueriesUseCase {
//...
}

我在函数实现中如何 Repository 呢?这要靠 DI 容器获取。官方示例代码中都使用 Hilt 来解耦 ViewModel 与 UseCase 的,ViewModel 不关心 UseCase 的创建细节。下面是 NIA 的代码, GetRecentSearchQueriesUseCase 被自动注入到 SearchViewModel 中。


@HiltViewModel
class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase // UseCase 注入 VM
//...
) : ViewModel() {
//...
}

Function interface 的 GetRecentSearchQueriesUseCase 没有构造函数,需要通过 Dagger 的 @Module 安装到 DI 容器中,provideGetRecentSearchQueriesUseCase 参数中的 RecentSearchRepository 可以从容器中自动获取使用。


@Module
@InstallIn(ActivityComponent::class)
object UseCaseModule {
@Provides
fun provideGetRecentSearchQueriesUseCase(recentSearchRepository: RecentSearchRepository) =
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

当时用 Koin 作为 DI 容器时也没问题,代码如下:


single<GetRecentSearchQueriesUseCase> {
GetRecentSearchQueriesUseCase { limit ->
recentSearchRepository.getRecentSearchQueries(limit)
}
}

4. 总结


UseCase 作为官方架构中的新概念,尚没有完全深入人心,需要不断探索合理的使用方式,本文给出一些基本思考:




  • 考虑到架构的扩展性,推荐在 ViewModel 与 Repository 之间强制引入 UseCase,即使眼下的业务逻辑并不复杂




  • UseCase 不持有可变状态但依赖 Repository,需要兼具 FP 与 OOP 的特性,更适合用 Class 定义而非 Function




  • 在引入 UseCase 之前应该先引入 DI 框架,确保 ViewModel 与 UseCase 的耦合。




  • Function Interface 是 Class 之外的另一种定义 UseCase 的方式,有利于代码更加函数式


作者:fundroid
链接:https://juejin.cn/post/7230597098847092791
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解

视频先行 这是一篇视频形式的分享,如果你方便看,可以直接去看视频: 哔哩哔哩:这里 抖音:这里 YouTube:这里 下面是视频内容的脚本文案原稿分享。 视频文案原稿 很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有...
继续阅读 »

视频先行


这是一篇视频形式的分享,如果你方便看,可以直接去看视频:




下面是视频内容的脚本文案原稿分享。



视频文案原稿


很多从 Java 转到 Kotlin 的人都会有一个疑惑:为什么 Kotlin 没有沿用 Java 的 void 关键字,而要引入这个叫 Unit 的新东西?


// Java
public void sayHello() {
System.out.println("Hello!");
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

不过这个问题一般也不会维持很久,因为就算你不明白,好像……也不影响写代码。


直到这两年,大家发现 Compose 的官方示例代码里竟然有把 Unit 填到函数参数里的情况:


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

我们才觉得:「啊?还能这么写?」


Unit 的本质


大家好,我是扔物线朱凯。


今天来讲一讲 Unit 这个特殊的类型。


我们在刚学 Kotlin 的时候,就知道 Java 的 void 关键字在 Kotlin 里没有了,取而代之的是一个叫做 Unit 的东西:


// Java
public void sayHello() {
System.out.println("Hello!")
}

// Kotlin
fun sayHello(): Unit {
println("Hello!")
}

而这个 Unit,和 Java 的 void 其实是不一样的。比如 Unit 的返回值类型,我们是可以省略掉不写的:


// Kotlin
fun sayHello() {
println("Hello!")
}

不过省略只是语法上的便利,实际上 Kotlin 还是会把它理解成 Unit


Unit 和 Java 的 void 真正的区别在于,void 是真的表示什么都不返回,而 Kotlin 的 Unit 却是一个真实存在的类型:


public object Unit {
override fun toString() = "kotlin.Unit"
}

它是一个 object,也就是 Kotlin 里的单例类型或者说单例对象。当一个函数的返回值类型是 Unit 的时候,它是需要返回一个 Unit 类型的对象的:


// Kotlin
fun sayHello() {
println("Hello!")
return Unit
}

只不过因为它是个 object ,所以唯一能返回的值就是 Unit 本身。


另外,这一行 return 我们也可以省略不写:


// Kotlin
fun sayHello() {
println("Hello!")
}

因为就像返回值类型一样,这一行 return,Kotlin 也会帮我们自动加上:


// Kotlin
fun sayHello(): Unit {
println("Hello!")
return Unit
}

这两个 Unit 是不一样的,上面的是 Unit 这个类型,下面的是 Unit 这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是 Kotlin 给 Unit 的特权,而是 object 本来就有的语法特性。你如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:


object Rengwuxian

fun getRengwuxian(): Rengwuxian {
return Rengwuxian
}

包括你也可以这样写:


val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例你就直接用就行了。


所以在结构上,Unit 并没有任何的特别之处,它就只是一个 Kotlin 的 object 而已。除了对于函数返回值类型和返回值的自动补充之外,Kotlin 对它没有再施加任何的魔法了。它的特殊之处,更多的是在于语义和用途的角度:它是个由官方规定出来的、用于「什么也不返回」的场景的返回值类型。但这只是它被规定的用法而已,而本质上它真就是个实实在在的类型。也就是在 Kotlin 里,并不存在真正没有返回值的函数,所有「没有返回值」的函数实质上的返回值类型都是 Unit,而返回值也都是 Unit 这个单例对象,这是 Unit 和 Java 的 void 在本质上的不同。


Unit 的价值所在


那么接下来的问题就是:这么做的意义在哪?


意义就在于,Unit 去掉了无返回值的函数的特殊性,消除了有返回值和无返回值的函数的本质区别,这样很多事做起来就会更简单了。


例:有返回值的函数在重写时没有返回值


比如?


比如在 Java 里面,由于 void 并不是一种真正的类型,所以任何有返回值的方法在子类里的重写方法也都必须有返回值,而不能写成 void,不管你用不用泛型都是一样的:


public abstract class Maker {
public abstract Object make();
}

public class AppleMaker extends Maker {
// 合法
@Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker {
// 非法
@Override
public void make() {
world.refresh();
}
}


public abstract class Maker<T> {
public abstract T make();
}

public class AppleMaker extends Maker<Apple> {
// 合法
Override
public Apple make() {
return new Apple();
}
}

public class NewWorldMaker extends Maker<void> {
// 非法
Override
public void make() {
world.refresh();
}
}


你只能去写一行 return null 来手动实现接近于「什么都不返回」的效果:


public class NewWorldMaker extends Maker {
@Override
public Object make() {
world.refresh();
return null;
}
}


而且如果你用的是泛型,可能还需要用一个专门的虚假类型来让效果达到完美:


public class NewWorldMaker extends Maker<Void> {
@Override
public Void make() {
world.refresh();
return null;
}
}


而在 Kotlin 里,Unit 是一种真实存在的类型,所以直接写就行了:


abstract class Maker {
abstract fun make(): Any
}

class AppleMaker : Maker() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker() {
override fun make() {
world.refresh()
}
}

abstract class Maker<T> {
abstract fun make(): T
}

class AppleMaker : Maker<Apple>() {
override fun make(): Apple {
return Apple()
}
}

class NewWorldMaker : Maker<Unit>() {
override fun make() {
world.refresh()
}
}

这就是 Unit 的去特殊性——或者说通用性——所给我们带来的便利。


例:函数类型的函数参数


同样的,这种去特殊性对于 Kotlin 的函数式编程也提供了方便。一个函数的函数类型的参数,在函数调用的时候填入的实参,只要符合声明里面的返回值类型,它是可以有返回值,也可以没有返回值的:


fun runTask(task: () -> Any) {
when (val result = task()) {
Unit -> println("result is Unit")
String -> println("result is a String: $result")
else -> println("result is an unknown type")
}
}

...

runTask { } // () -> Unit
runTask { "完成!" } // () -> String
runTask { 1 } // () -> Int

Java 不支持把方法当做对象来传递,所以我们没法跟 Java 做对比;但如果 Kotlin 不是像现在这样用了 Unit,而是照抄了 Java 的 void 关键字,我们就肯定没办法这样写。


小结:去特殊化


这就是我刚才所说的,对于无返回值的函数的「去特殊化」,是 Unit 最核心的价值。它相当于是对 Java 的 void 进行了缺陷的修复,让本来有的问题现在没有了。而对于实际开发,它的作用是属于润物细无声的,你不需要懂我说的这一大堆东西,也不影响你享受 Unit 的这些好处。


…………


那我出这期视频干嘛?


——开个玩笑。了解各种魔法背后的实质,对于我们掌握和正确地使用一门语言是很有必要的。


延伸:当做纯粹的单例对象来使用


比如,知道 Unit 是什么之后,你就能理解为什么它能作为函数的参数去被使用。


Compose 里的协程函数 LaunchedEffect() 要求我们填入至少一个 key 参数,来让协程在界面状态变化时可以自动重启:


LaunchedEffect(key) {
xxxx
xxxxxx
xxx
}

而如果我们没有自动重启的需求,就可以在参数里填上一个 Unit


LaunchedEffect(Unit) {
xxxx
xxxxxx
xxx
}

因为 Unit 是不变的,所以把它填进参数里,这个协程就不会自动重启了。这招用着非常方便,Compose 的官方示例里也有这样的代码。不过这个和 Unit 自身的定位已经无关了,而仅仅是在使用它「单例」的性质。实际上,你在括号里把它换成任何的常量,效果都是完全一样的,比如 true、比如 false、比如 1、比如 0、比如 你好,都是可以的。所以如果你什么时候想「随便拿个对象过来」,或者「随便拿个单例对象过来」,也可以使用 Unit,它和你自己创建一个 object 然后去使用,效果是一样的。


总结


好,这就是 Kotlin 的 Unit,希望这个视频可以帮助你更好地了解和使用它。下期我会讲 Kotlin 里另一个特殊的类型:Nothing。关注我,了解更多 Android 开发的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!


作者:扔物线
链接:https://juejin.cn/post/7231345137850286138
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android-EventBus修改纪实

背景 笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例: // Event.java public final class Event { private final in...
继续阅读 »

背景


笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例:


// Event.java
public final class Event {

private final int code;

public Event(int code) {
this.code = code;
}

public int getCode() {
return code;
}
}

// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

所以去查看了 EventBus 的源码,接下来我们分析下 EventBus 发送粘性事件的流程。


分析


粘性事件



以下源码基于 EventBus 3.3.1 版本



下面是发送粘性事件的源码:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

postSticky 代码比较简单,首先对 stickyEvents 进行加锁,接下来把 event 事件的 Class 对象作为 Key,event 事件本身作为 value 放进 Map 中,其中stickyEvents 是 Map 对象,实例是 ConcurrentHashMap, 其 Key 和 Value 的泛型形参分别是 ClassObject, 它的作用就是用来存储粘性事件;然后调用 post(event) 把粘性事件当作普通事件发送一下。


首先我们看下最后为什么要调用下 post(event)


虽然 post(evnet) 上面有注释,简单翻译下:"在放进 Map 后应该再发送一次,以防止订阅者想立即删除此事件",读完注释后,可能还是不太明白,这里笔者认为:在前面存储完粘性事件后,这里调用 post 把粘性事件当作普通事件发送出去,或许是因为现在已经有注册的粘性事件订阅者,此时把已经注册的粘性事件订阅者当作普通事件的订阅者,这样已经注册的粘性事件订阅者可以立即收到相应的事件,只是此时事件不再是粘性的。


postSticky 中我们并没有看到粘性事件是在哪里发送的,想一想我们使用粘性事件的目的是什么?当注册订阅者时可以收到之前发送的事件,这样来看,粘性事件的发送是在注册订阅者时,下面是注册订阅者的源码,删除了一些无关代码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

register 代码也比较简单,首先通过订阅者的 Class 对象查找订阅者所有的Event事件接收方法,然后对 EventBus 对象加锁,遍历所有的Event事件接收方法 subscriberMethods 调用 subscribe 方法,以下是 subscribe 方法源码:


// Key 为 Event Class 对象,Value 为存储 Event 的订阅者和接收 Event 方法对象的集合 
private final Map, CopyOnWriteArrayList> subscriptionsByEventType;

// Key 为订阅者对象,Value 为订阅者中的 Event Class对象集合
private final Map>> typesBySubscriber;

// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// Event Class对象
Class eventType = subscriberMethod.eventType;

// 订阅者和接收 Event 方法对象
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

// 根据 Event Class对象,获取订阅者和接收 Event 方法对象的集合
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);

// 判断订阅者和接收 Event 方法对象是否为空
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
// 判断是否已经包含了新的订阅者和接收 Event 方法对象,若是包含则认为是重复注册
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}

// 这里是按优先级排序插入到集合中
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}

// 这里是把 Event Class对象添加进对应订阅者的 Event Class对象集合中
List> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}

// 上面已经判断了是否重复注册,所以这里直接添加
subscribedEvents.add(eventType);

// 接下来就是粘性事件的发送逻辑了
// 判断 Event 接收方法是否可以处理粘性事件
if (subscriberMethod.sticky) {
// 这里判断是否考虑 Event 事件类的继承关系,默认为 Ture
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}

在上面的源码中,增加了不少注释有助于我们读懂源码,在源码的最后就是粘性事件的发送逻辑了,其中有两个分支,其中一个分支根据 Event 事件的继承关系发送事件,另外一个分支根据接收 Event 方法中的 Event Class 对象从 stickyEvents 中直接查找粘性事件,最后两个分支殊途同归,都调用了 checkPostStickyEventToSubscription 方法:


private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

checkPostStickyEventToSubscription 方法很简单,对粘性事件做下判空处理,继续调用 postToSubscription 方法,传入订阅者与接收 Event 方法对象,粘性事件和是否是主线程布尔值:


private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;A
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

postToSubscription 方法比较长,但是比较好理解,就是根据接收 Event 方法上的 @Subscribe 注解中传入的线程模型进行事件的分发,具体的事件分发流程,有空再分析,本文就先不分析了,现在我们只需知道最后都会调用 invokeSubscriber(Subscription subscription, Object event) 方法即可:


void invokeSubscriber(Subscription subscription, Object event) {
try {
// 反射调用 Event 接收方法传入 Event 事件
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

终于在 invokeSubscriber 方法中找到调用 Event 接收方法的地方了,原来 EventBus 最后是通过反射调用 Event 接收方法并传入相应 Event 事件的。


分析完 Event 事件的发送流程,好像没有发现为什么有时收不到粘性事件。


我们回过头来再看下笔者的使用示例,为了方便查看,下面贴出使用示例代码:


// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

可能细心的读者已经发现 test 方法调用了,问题应该出在 postSticky 方法中,让我们再次查看 postSticky 方法:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

根据前面分析 postSticky 方法的结果,stickyEvents 用于存储粘性事件,它是个 Map 结构,而 stickyEvents 的 Key 正是 Event 的 Class 对象,根据 Map 结构的存储原理:如果存在相同的 Key,则覆盖 Value 的值,而 stickyEvents 的 Value 正是 Event 本身。


终于真相大白,多次调用 test 方法发送粘性事件,EventBus 只会存储最后一次的粘性事件。


小结


EventBus 针对同一个粘性 Event 事件只会存储最后一次发送的粘性事件。


EventBus 的上述实现可能是因为多次发送同一个粘性事件,则认为之前的事件是过期事件应该抛弃,因此只传递最新的粘性事件。


EventBus 的这种实现无法满足笔者的业务逻辑需求,笔者希望多次发送的粘性事件,订阅者都能接收到,而不是只接收最新的粘性事件,可以理解为粘性事件必达订阅者,下面让我们修改 EventBus 的源码来满足需求吧。


修改


上一节我们分析了粘性事件的发送流程,为了满足粘性事件必达的需求,基于现有粘性事件流程,我们可以仿照粘性事件的发送来提供一个发送必达消息的方法。


Subscribe


首先我们定义 Event 接收方法可以接收粘性事件是在 @Subscribesticky = true , 所以我们可以修改 Subscribe 注解,增加粘性事件必达的方法:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;

/**
* If true, delivers the most recent sticky event (posted with
* {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
*/

boolean sticky() default false;

// 增加消息必达的方法
boolean rendezvous() default false;

/** Subscriber priority to influence the order of event delivery.
* Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
* others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
* delivery among subscribers with different {@link ThreadMode}s! */

int priority() default 0;
}

rendezvous 以为约会、约定的意思,可以理解为不见不散,在这里它有两层作用,其一是标记方法可以接收粘性事件,其二是标记方法接收的事件是必达的。


findSubscriberMethods


接下来就需要解析 rendezvous 了,我们先看看 sticky 是如何解析的,在上一节我们分析了 register 方法,方便查看,下面再贴出 register 方法源码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

上一节分析中,我们没有分析查找订阅者中所有的 Event 接收方法 findSubscriberMethods ,接下来我们分析下在 findSubscriberMethods 方法是如何查找 Event 接收方法的:


List findSubscriberMethods(Class subscriberClass) {
// 先从缓存中查找
List subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}

// 是否忽略生成索引,默认为False,所以这里走else分支
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
// 查找Event接收方法
subscriberMethods = findUsingInfo(subscriberClass);
}

// 如果订阅者和订阅者父类中没有Event接收方法则抛出异常
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
// 添加进缓存中
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

调用 findSubscriberMethods 方法需要传入订阅者 Class 对象,通过笔者在源码中增加的注释分析发现默认调用 findUsingInfo 方法查找 Event 接收方法,我们继续跟踪 findUsingInfo 方法:


private List findUsingInfo(Class subscriberClass) {
// FindState对订阅者Class对象和Event接收方法进行了一层封装
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass); // ①
while (findState.clazz != null) {

// 查找订阅者信息,包含订阅者Class对象、 订阅者父类、Event接收方法等
findState.subscriberInfo = getSubscriberInfo(findState); // ②

// 在 ① initForSubscriber中会把subscriberInfo置为null,
// 在 ② getSubscriberInfo中没有Index对象,
// 所以第一次时这里会走else分支
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
// 查找Event接收方法
findUsingReflectionInSingleClass(findState);
}

// 查找父类的Event接收方法
findState.moveToSuperclass();
}

// 通过findState返回Event接收方法,并回收findState
return getMethodsAndRelease(findState);
}

根据笔者在源码中的注释分析,在 findUsingInfo 方法中使用「享元模式」对 FindState 进行回收利用,避免创建大量临时的 FindState 对象占用内存,最后再次调用 findUsingReflectionInSingleClass 方法查找 Event 接收方法,看方法名字应该是使用反射查找,findUsingReflectionInSingleClass 源码较长,删减一些不关心的代码:


private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
// 通过反射获取当前类中声明的所有方法
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// 删减不关心的代码
}

// 遍历所有方法
for (Method method : methods) {

// 获取方法的修饰符
int modifiers = method.getModifiers();

// 判断方法是否是public的;是否是抽象方法,是否是静态方法,是否是桥接方法,是否是合成方法
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {

// 获取方法的形参Class对象数组
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {

// 获取方法上的Subscribe注解
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class eventType = parameterTypes[0];

// 检测是否已经添加了相同签名的方法,考虑子类复写父类方法的情况
if (findState.checkAdd(method, eventType)) {

// 获取注解的参数
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky(),

// 这里我们添加rendezvous参数 ①
subscribeAnnotation.rendezvous()));
}
}
}
// 删减不关心的代码
}
// 删减不关心的代码
}
}

findUsingReflectionInSingleClass 方法中通过反射获取订阅者中声明的所有方法,然后遍历所有方法:



  1. 首先判断方法的修饰符是否符合,

  2. 其次判断方法是否只有一个形参,

  3. 再次判断方法是否有 Subscribe 注解,

  4. 然后检测是否已经添加了相同签名的方法,主要是考虑子类复写父类方法这种情况,

  5. 最后获取 Subscribe 注解的参数,在这里我们解析 rendezvous,封装进 SubscriberMethod 中。


SubscriberMethod 中增加 rendezvous 字段,删除不关心的代码:


public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class eventType;
final int priority;
final boolean sticky;

// 增加 `rendezvous` 字段
final boolean rendezvous;
/** Used for efficient comparison */
String methodString;

public SubscriberMethod(Method method, Class eventType, ThreadMode threadMode,
int priority, boolean sticky,

// 增加 `rendezvous` 形参
boolean rendezvous) {
this.method = method;
this.threadMode = threadMode;
this.eventType = eventType;
this.priority = priority;
this.sticky = sticky;
this.rendezvous = rendezvous;
}
}

postRendezvous


好的,rendezvous 已经解析出来了,接下来我们对外提供发送必达事件的接口:


// 选择List存储必达事件,使用Pair封装必达事件的Key和Value
private final List, Object>> rendezvousEvents;

public void postRendezvous(Object event) {
synchronized (rendezvousEvents) {
rendezvousEvents.add(Pair.create(event.getClass(), event));
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

上面的源码,我们通过仿照 postSticky 方法实现了 postRendezvous 方法,在 postSticky 方法中使用 Map 存储粘性事件,不过我们在 postRendezvous 方法中使用 List 存储必达事件,保证必达事件不会因为 Key 相同而被覆盖丢失,最后也是调用 post 方法尝试先发送一次必达事件。


register


在上一节中我们分析了粘性事件是在 register 中调用 subscribe 方法进行发送的,这里我们仿照粘性事件的发送逻辑,实现必达事件的发送逻辑,我们可以在 subscribe 方法最后增加发送必达事件的逻辑,以下源码省略了一些不关心的代码:


private final List, Object>> rendezvousEvents;

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// 省略不关心的代码

// 粘性事件发送逻辑
if (subscriberMethod.sticky) {
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}

// 新增必达事件发送逻辑
// 判断方法是否可以接收必达事件
if (subscriberMethod.rendezvous) {
if (eventInheritance) {
for (Pair, Object> next : rendezvousEvents) {
Class candidateEventType = next.first;
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = next.second;
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object rendezvousEvent = getRendezvousEvent(eventType);
if (rendezvousEvent != null) {
checkPostStickyEventToSubscription(newSubscription, rendezvousEvent);
}
}
}
}

subscribe 方法中,我们通过仿照粘性事件的发送逻辑增加了必达事件的发送:



  1. 首先判断 Event 接收方法是否可以接收必达事件

  2. 其次考虑 Event 必达事件的继承关系,

  3. 最后两个分支都调用 checkPostStickyEventToSubscription 方法发送必达事件


happy~


总结


使用第三方库时,发现问题不要慌张,带着问题去查看源码总有一番收获,这也告诫我们在使用第三库时最好先搞明白它的实现原理,遇到问题时不至于束手无策。


通过分析 EventBus 的源码,我们有以下收获:



  1. 明白了我们注册订阅者时 EventBus 做了哪些事情

  2. 知晓了我们发送粘性事件时,EventBus 是如何处理及何时发送粘性事件的

  3. 了解到 EventBus 是通过反射调用 Event 事件的接收方法

  4. 学习了 EventBus 中的一些优化点,比如对 FindState 使用「享元模式」避免创建大量临时对象占用内存

  5. 进一步了解到对并发的处理


通过以上收获,我们成功修改 EventBus 源码实现了我们必达事件的需求。


到这里我们已经完成了必达事件的发送,不过我们还剩下获取必达事件,移除必达事件没有实现,最后 EventBus 中还有单元测试 module,我们还没有针对 rendezvous 编写单元测试,读者有兴趣的话,可以自己试着实现。


希望可以帮到你~


作者:guodongAndroid
链接:https://juejin.cn/post/7104107150678917133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

入职东北国企做程序员一个月,感受如何?

工作环境 我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。 人 目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职...
继续阅读 »

工作环境


我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。



目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职责去负责自己的事情就好。


办公环境可以分为两个环境,分别是“职能部门”和“研发部门”:


* 职能部门比较正式,工位、装修以及员工着装都比较正规。
* 研发部门较为随意一些,无论是工位还是桌椅什么的,有些东拼西凑的感觉,但是整体还是可以接受。

另外可能是因为国企的原因,所有的工位都是大隔断那种,如果换成现在公司常见的大通桌,估计人数还能多做十好几个,毕竟我刚来的时候还没有正式工位坐呢。



相比于在其他公司上班,可能在这最大的体会就是不用考虑吃什么。公司有食堂,提供午饭,菜不能选,但是每天四菜一汤,加水果或酸奶。相比于每天纠结的选择外卖,我对这个很满意。


晚上如果加班的话,公司会统一订餐,大概一餐的费用也在20至30块之间吧,当然也没法选择吃什么,有啥吃啥被。


早餐为什么最后说,因为公司的早餐在早上八点之前供应,八点半上班。。。有点难受啊。


幸好公司提供简单的零食,面包、火腿肠、泡面等等,虽然偶尔会被大家抢空,但是总比没有强吧。



上家公司离我家只有1公里的距离,所以从回到哈尔滨也没有买车,每天不行上班,还挺惬意的。


现在不行了,新公司距离家里有十好几公里,当然我也暂时没有选择买车,地铁出行,快捷方便,还省心,唯一的缺点就是要走个1.5公里吧。


在晚上八点之后打车可以报销的,但是只能是网约车,可能是出租车的票,粘贴太过麻烦了吧。反正我是不打车,因为我嫌报销麻烦。


工具


啥是工具呢,对程序员来说就是电脑了,公司提供电脑,也可以自己买电脑进行报销,还是很人性化地。


公司的会议室设施还是不错的,各种投屏等等,比较先进,完全摒弃了传统的投影仪等等,这还让我对公司有种另眼相看的感觉。


还提供显示器什么的,自己申请就好了。


入职感受


我面试的岗位是java开发,常规的java框架使用起来都没有问题。面试过程还是比较简单的,主要是常用的一些组件,简单的实现原理等等,所以顺利通过了。


但是比较遗憾的公司给我砍了一些,定位的职级也不是很高。说实话我还是有点难受的,毕竟整个面试过程,和我对个人的能力认知还是比较清楚地。


但是当我入职后我明白了,这里毕竟是哈尔滨,收入和年龄还是有很大的关系的。部门内有好几位大哥,想想也就释然了,在其位谋其政吧,他们的工作确实我我接下来要做的繁琐。希望日后能够慢慢的升职加薪吧。


总体来说,东北人还是敞亮,有事直接提,工作也没啥拐弯抹角的,干就完了。我才刚来公司第一天,就给我把工作安排上了,一点不拿我当外人啊


工作感受


既然谈到工作了,就展开说说。


我第一天到公司,找了个临时工位,领导们各种git账号、禅道账号就给我创建好,一个项目扔给我,本月中期要求做完。。我当时内心的想法真的是:东北人果然是好相处啊,整的跟老同事似的。我能怎么办,干就完了啊。


项目还是很简单的,常规的springboot + mybatis + vue2的小项目,大概也没到月中期,一个礼拜就完事了。


比较让我惊喜的是部署的环节。居然使用的是devops工具KubeSphere。我只说这一句你们可能不理解,这是我在哈尔滨的第三家公司,从来没有一家公司说使用过k8s,甚至相关的devops工具。只能说是哈尔滨软件行业的云化程度还是太低了。唯一在上家公司的jenkins还是因为我想偷懒搭建的。


不过运维相关的内容都把握在运维人员手里,所以想要料及并且掌握先关的知识还是要自己私下去学习的。


项目其实都是小项目,以web端和app为主,基本都是前后端分离的单体架构。唯一我接触到的微服务架构应该就是公司的中台,提供统一的权限配置和登录认证,整体还是很不错的。


虽然公司的项目很多,工作看起来很忙碌,但实际还是比较轻松愉快的,我还能应付自如。每天晚上为了蹭一顿晚饭,通常会加班到七点半。用晚上这个时间更更文,也挺好的。


从体来说,是我比较喜欢的工作节奏。


个人分析


我是一个不太安定的人,长期干一件事会让我比较容易失去兴趣,还是挺享受刚换工作时,这段适应环境的感觉。也有可能更喜欢这种有一定挑战的感觉。


和上一家公司相比,这家公司在公司的时间明显多出很多,也没有那么悠闲了,但是我却觉得这更适合我,毕竟我是一个闲不住的人,安逸的环境让我感到格外的焦虑,忙碌的生活会让自己感到生活很充实。


记得之前的文章说过自己的身体健健的不太好,但是最近不知道是上班的路程变远,导致运动量的增加,之前不适的症状似乎都小时了。真闲出病来了




既来之,则安之,时刻提醒自己再努力点,阳光总在风雨后。


作者:我犟不过你
链接:https://juejin.cn/post/7125627005407592462
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
来源:juejin.cn/post/7127653600532103198
收起阅读 »

记一次不规范使用key引发的惨案

web
前言 平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求 场景 在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的...
继续阅读 »

前言


平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求


场景


在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的排在前面,而且选好商品之后,需要在下单界面给每个商品选择发货地,发货地列表是通过商品id去接口取的,我的代码长这样:



  • 下单界面调用商品组件


// 这里每次选了商品都是从前插入:list.value = [...newList, ...list.value]
<Goods
v-for="(item, index) in list"
:key="index"
:goods="item">
</Goods>


  • 商品组件内部调用发货地组件


<SendAddress
v-model="address"
:product-no="goods.productNo"
placeholder="请选择发货地"
@update:model-value="updateValue"></SendAddress>


  • 发货地组件内部获取发货地址列表


onMounted(async () => {
getList()
})
const getList = async () => {
const postData = {
productInfo: props.productNo,
}
}

上述代码运行结果是,每次获取地址用的都是最开始选的那个商品的信息,百思不得其解啊,最后说服产品,不要倒序了,问题解决


解决过程


后来在研究前进刷新后退缓存时,关注到了组件的key,详细了解后才知其中来头


企业微信截图_16813558431830.png



重点:根据key复用或者更新,也就是key没有变化,就是复用,变化了在更新挂载,而onMounted是在挂载完成后执行,没有挂载的元素,就不会走onMounted



回到上述问题,当我们每次从前面插入数据,key的变化逻辑是这样的


结论


企业微信截图_16813564053499.png



最开始选中的商品key从1变成了2,最近选的是0。


而0和1是本来就存在的,只会更新数据,不会重新挂载,只有最开始选的那个商品key是全新的,会重新挂载,重新走onMounted。


所以每次选择数据后,拿去获取地址列表的商品信息都是第一个的



解决以上问题,把key改成item.productNo就解决了


作者:赖皮喵
来源:juejin.cn/post/7221357811287834680
收起阅读 »

1.6kB 搞定懒加载、无限滚动、精准曝光

web
上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精...
继续阅读 »

上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精准曝光的功能。


1. IntersectionObserver 基础介绍


不管我们使用哪个类库,都需要了解 IntersectionObserver 的基本原理,下面是一个简单的例子



import React, { useEffect } from "react";
import "./page.css";

const Page1 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

useEffect(() => {
const io = new IntersectionObserver((entries) => {
console.log(entries[0].intersectionRatio);
});

const footer = document.querySelector(".footer");

if (footer) {
io.observe(footer);
}

return () => {
io.disconnect();
};
}, []);

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer">被观察的元素</div>
</div>

);
};

export default Page1;

如上例,可以了解到以下几点知识




  1. new 一个 IntersectionObserver 对象,下称 io,需传入一个函数,下称 callbackcallback 的入参 entries 代表了正在被观察的元素数组,数组的每一项都拥有属性 intersectionRatio ,代表了被观察的元素与根元素可视区域的交叉比例,。




  2. 使用 ioobserve 方法来添加你想观察的元素,可以多次调用添加多个,




  3. 使用 iodisconnect 方法来销毁观测




使用上方的代码,可以完成对元素最基本的观察。如上方 gif 操作,在控制台可得到以下结果 ,




  • 进入页面时,callback 被调用了一次:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0

  • 滚动出可视区,再次调用:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0


而懒加载,无限滚动,精准曝光是如何基于这个 api 去实现的呢,如果直接去写,当然也能实现,但是会有些繁琐,下面引入本篇文章的主角:react-intersection-observer 类库,先看看这个类库的基本介绍吧。


2. react-intersection-observer 基础介绍


这个类库在全局维护了一个 IntersectionObserver 实例(如果只有一个根元素,那全局仅有一个实例,实际上代码中维护了一个实例的 Map,此处简单表述),并提供了一个名为 useInViewhooks 方便我们了解到被观测的元素的观测状态。与上面相同的例子,他的写法如下:


import React, { useEffect } from "react";
import { useInView } from 'react-intersection-observer';
import "./page.css";

const Page2 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
onChange: (inView, entry) => {
console.log(entry.intersectionRatio);
}
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>被观察的元素</div>
</div>

);
};

export default Page2;

如上例,使用更少的代码,就实现了相同的功能,而且带来了一些好处



  • 不用自己维护 IntersectionObserver 实例,既不用关心创建,也不用关心销毁

  • 不用控制被观察的元素到底是 entries 内的第几个,观察事件都会在相应绑定的 onChange 中进行回调


以上仅为基本使用,实战中需求是更为复杂的,所以这个类库也提供了一系列属性,方便大家的使用:



利用上面这些配置项,我们可以实现以下功能


3. 实战用例


3.1. 懒加载


import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

interface Props {
width: number;
height: number;
src: string;
}

const LazyImage = ({ width, height, src, ...rest }: Props) => {
const { ref, inView } = useInView({
triggerOnce: true,
root: document.querySelector('.scroll-container'),
rootMargin: `0px 0px ${window.innerHeight}px 0px`,
onChange: (inView, entry) => {
console.log('info', inView, entry.intersectionRatio);
}
});

return (
<div
ref={ref}
style={{
position: "relative",
paddingBottom: `${(height / width) * 100}%`,
background: "#2a4b7a",
}}
>

{inView ? (
<img
{...rest}
src={src}
width={width}
height={height}
style={{ position: "absolute", width: "100%", height: "100%", left: 0, top: 0 }}
/>

) : null}
</div>

);
};

const Page3 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<LazyImage width={750} height={200} src={"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4acf97e7dc944bf8ad5719b2b42f026~tplv-k3u1fbpfcp-watermark.image?"} />
</div>

);
};

export default Page3;

懒加载中我们需要用到几个额外的属性:




  • triggerOnce :只触发一次




  • root:默认为文档视口(如果被观察的元素,父/祖元素中有 overflow: scroll,需要指定为该元素)




  • rootMarginrootmargin




    • 同 css 上右下左写法,需要带单位,可简写('200px 0px')




    • 正值代表观察区域增大,负值代表观察区域缩小






在图片懒加载中,因为通常不可能等到元素被滚动到了可视区域,才开始加载图片,所以需要调整 rootMargin ,可以写为,rootMargin: `0px 0px ${window.innerHeight}px 0px ,这样图片可以提前一屏进行加载。


同样懒加载不需要不可见的时候回收掉相应的 dom ,所以只需要触发一次,设置 triggerOncetrue 即可。


3.2. 无限滚动



import React, { useState } from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page4 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const [datas, setDatas] = useState([1, 1, 1]);
const { ref } = useInView({
onChange: (inView, entry) => {
console.log("inView", inView);
if (inView) {
setDatas((prevDatas) => [...prevDatas, ...new Array(3).fill(1)]);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
{datas.map((item, index) => {
return (
<div key={index + 1} className="placeholder">
第{index + 1}个元素
</div>
);
})}
<div className="load-more" ref={ref}></div>
</div>

);
};

export default Page4;

无限滚动主要依赖在 onChange 中对 inView 进行判断,我们可以添加一个高度为0的元素,名为 load-more ,当页面滚动到最下方时,该元素的 onChange 会被触发,通过对 inViewtrue 的判断后,加载后续的数据。同理,真正的无限滚动也需要提前加载(在观察内写异步请求等),也可以设置相应的 rootMargin ,让无限滚动更丝滑。


3.3. 精准曝光



import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page5 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
threshold: 0.5,
delay: 500,
onChange: (inView, entry) => {
if (inView) {
console.log("元素需要上报曝光事件", entry.intersectionRatio);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>
需要精准曝光的元素
</div>
</div>

);
};

export default Page5;

精准曝光也是很常见的业务需求,通常此类需求会要求元素的露出比例和最小停留时长。



  • 对露出比例要求的原因:因为有可能元素的有效信息并未展示,只是露出了一点点头,一般业务上会要求露出比例大于一半。

  • 对停留时长要求的原因:有可能用户快速划过,比如小说看到了很啰嗦的章节快速滑动,直接看后面结果,如果不加停留时长,中间快速滑动的区域也会曝光,与实际想要的不符。


类库恰好提供了下面两个属性方便大家的使用



  • threshold: 观察元素露出比例,取值范围 0~1,默认值 0

  • delay: 延迟通知元素露出(如果延迟后元素未达标,则不会触发onChange),取值单位毫秒,非必填。


使用上面两个属性,就可以轻松实现业务需求。


3.4. 官方示例


示例,官方示例中还有很多对属性的应用,比如 threshold 传入数组,skiptrack-visibility ,大家可自行体验。


总结


以上就是对 IntersectionObserver 以及 react-intersection-observer 的介绍了,希望能对大家有所帮助,文中录制的示例完整项目可以从此处获取。


作者:windyrain
来源:juejin.cn/post/7220309530910851130
收起阅读 »

字节都在用的代码自动生成

web
背景 如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。 字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作...
继续阅读 »

背景


如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。


字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作为接口定义,是不是也可以通过接口定义自动生成请求接口的代码呢?答案是肯定的,字节内部已经衍生出了多个基于 thrift 的代码生成工具,本篇文章主要介绍如何通过 thrift 生成前端接口调用的代码。


接口定义


接口定义,顾名思义就是用来定义接口的语言,由于字节内部广泛使用的 thrift 基本上满足接口定义的要求,所以我们不妨直接把 thrift 当成接口定义。


thrift 是一种跨语言的远程过程调用 (RPC) 框架,如果你对 Typescript 比较熟悉的话,那它的结构看起来应该很简单,看个例子:


namespace go namesapce

// 请求的结构体
struct GetRandomRequest {
1: optional i32 min,
2: optional i32 max,
3: optional string extra
}

// 响应的结构体
struct GetRandomResponse {
1: optional i64 random_num
}

// 定义服务
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req)
}

示例中的 service 可以看成是一组函数,每个函数可以看成是一个接口。我们都知道,对于 restful 接口,还需要定义接口路径(比如 /getUserInfo)和参数(query 参数、body 参数等),我们可以通过 thrift 注解来表示这些附加信息。


namespace go namesapce

struct GetRandomRequest {
1: optional i32 min (api.source = "query"),
2: optional i32 max (api.source =
"query"),
3: optional string extra (api.source = "body"),
}

struct GetRandomResponse
{
1: optional i64 random_num,
}

// Service
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req) (api.get = "/api/get-random"),
}

api.source 用来指定参数的位置,query 表示是 query 参数,body 表示 body 参数;api.get="/api/get-random" 表示接口路径是 /api/get-random,请求方法是 GET;


生成 Typescript


上面我们已经有了接口定义,那么对应的 Typescript 应该就呼之欲出了,一起来看代码:


interface GetRandomRequest {
min: number;
max: number;
extra: string;
}

interface GetRandomResponse {
random_num: number;
}

async function GetRandom(req: GetRandomRequest): Promise<GetRandomResponse> {
return request<GetRandomResponse>({
url: '/api/get-random',
method: 'GET',
query: {
min: req.min,
max: req.max,
},
body: {
extra: req.extra,
}
});
}


生成 Typescript 后,我们无需关心生成的代码长什么样,直接调用 GetRandom 即可。


架构设计


要实现基于 thrift 生成代码,最核心的架构如下:


image.png
因为 thrift 的内容我们不能直接拿来用,需要转化成中间代码(IR),这里的中间代码通常是 json、AST 或者自定义的 DSL。如果中间代码是 json,可能的结构如下:


{
name: 'GetRandom',
method: 'get',
path: '/api/get-random',
req_schema: {
query_params: [
{
name: 'min',
type: 'int',
optional: true,
},
{
name: 'max',
type: 'int',
optional: true,
}
],
body_params: [
{
name: 'extra',
type: 'string',
optional: true,
}
],
header_params: [],
},
resp_schema: {
header_params: [],
body_params: [],
}
}

为了保持架构的开放性,我们在核心链路上插入了 PrePlugin 和 PostPlugin,其中 PrePlugin 决定了 thrift 如何转化成 IR,PostPlugin 决定 IR 如何生成目标代码。


这里之所以是「目标代码」而不是「Typescript 代码」,是因为我希望不同的 PostPlugin 可以产生不同的目标代码,比如可以通过 TSPostPlugin 生成 Typescript 代码,通过 GoPostPlugin 生成 go 语言的代码。


总结


代码生成这块的内容还有很多可以探索的地方,比如如何解析 thrift?是找第三方功能生成 AST 还是通过 pegjs 解析成自定义的 DSL?多文件联编如何处理、字段名 case 如何转换、运行时类型校验、生成的代码如何与 useRequest 或 ReactQuery 集成等。


thrift 其实可以看成接口定义的具体实现,如果 thrift 不满足你的业务场景,也可以自己实现一套类似的接口定义语言;接口定义作为前后端的约定,可以降低前后端的沟通成本;代码生成,可以提升前端代码的质量和研发效率。


如果本文对你有启发,欢迎点赞、关注、留言交流。


作者:探险家火焱
来源:juejin.cn/post/7220054775298359351
收起阅读 »

前端怎么样限制用户截图?

web
做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。 先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图...
继续阅读 »

做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。


先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图,有哪些脑洞?


有哪些脑洞


v站和某乎上的大佬给出了不少脑洞,我又加了点思路。


1.基础方案,阻止右键保存和拖拽。


这个方案是最基础,当前可只能阻拦一些小白用户。如果是浏览器,分分钟调出控制台,直接找到图片url。还可以直接ctrl+p,进入打印模式,直接保存下来再裁减。


2.失焦后加遮罩层


这个方案有点意思,看敏感信息时,必须鼠标点在某个按钮上,照片才完整显示。如果失去焦点图片显示不完整或者直接遮罩盖住。


3.高速动态马赛克


这个方案是可行的,并且在一些网站已经得到了应用,在视频或者图片上随机插像素点,动态跑来跑去,对客户来说,每一时刻屏幕上显示的都是完整的图像,靠用户的视觉残留看图或者视频。即时手机拍照也拍不完全。实际应用需要优化的点还是挺多的。比如用手机录像就可以看到完整内容,只是增加了截图成本。


下面是一个知乎上的方案效果。(原地址):


image.png


正经需求vs方案


其实限制用户截图这个方案本身就不合理,除非整个设备都是定制的,在软件上阉割截图功能。为了这个需求添加更复杂的功能对于一些安全性没那么高的需求来说,有点本末倒置了。


下面聊聊正经方案:


1.对于后台系统敏感数据或者图片,主要是担心泄漏出去,可以采用斜45度七彩水印,想要完全去掉几乎不可能,就是观感比较差。


2.对于图片版权,可以使用现在主流的盲水印,之前看过腾讯云提供的服务,当然成本比较高,如果版权需求较大,使用起来效果比较好。


3.视频方案,tiktok下载下来的时候会有一个水印跑来跑去,当然这个是经过处理过的视频,非原画,画质损耗也比较高。Netflix等视频网站采用的是服务端权限控制,走的视频流,每次播放下载加密视频,同时获得短期许可,得到许可后在本地解密并播放,一旦停止播放后许可失效。


总之,除了类似于Android提供的截图API等底层功能,其他的功能实现都不完美。即使是底层控制了,一样可以拍照录像,没有完美的方案。不过还是可以做的相对安全。


你还有什么新思路吗?有的话咱评论区见,欢迎点赞收藏关注,感谢!


作者:正经程序员
来源:juejin.cn/post/7127829348689674253
收起阅读 »

Vue 实现接口进度条

web
前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。 接口响应快 - 效果 接口响应慢 - 效果 实现思路 首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背...
继续阅读 »

cover.png


前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。



接口响应快 - 效果



接口响应慢 - 效果


实现思路


首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背景、进度长度、以及进度数字,同时还要设置数据绑定相关属性,如进度条当前的百分比、动画执行状态、以及完成状态等。在请求数据的过程中,需要添加监听函数来监测数据请求的过程变化,并更新组件相应的属性和界面元素。


代码实现


下面是使用 Vue 实现一个接口进度条的栗子:


<template>
<div class="progress-bar">
<div class="bg"></div>
<div class="bar" :style="{ width: progress + '%' }"></div>
<div class="label">{{ progress }}%</div>
</div>
</template>

<script>
export default {
data() {
return {
progress: 0,
isPlaying: false,
isCompleted: false
}
},
mounted() {
this.start();
},
methods: {
start() {
this.isPlaying = true;
this.animateProgress(90)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100);
}
})
.catch((error) => {
console.error('Progress error', error);
});
},
animateProgress(target) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;
const duration = (target - start) * 150;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
},
finish() {
this.isCompleted = true;
this.progress = 100;
}
}
};
</script>

<style scoped>
.progress-bar {
position: relative;
height: 8px;
margin: 10px 0;
}
.bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ccc;
border-radius: 5px;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 5px;
background-color: #409eff;
transition: width 0.5s;
}
.label {
position: absolute;
top: -20px;
left: calc(100% + 5px);
color: #333;
font-size: 12px;
}
</style>

首先定义了三个数据属性用于控制动画的播放和完成状态,分别是进度条当前比例 progress、动画播放状态 isPlaying、动画完成状态 isCompleted。在组件初始化的过程中,调用了 start 方法来启动进度条动画效果。在该方法内部,使用 Promise 来从 0% 到 90% 的百分比向相应位置移动,并在到达该位置时停止。


判断当前是否完成,如果没有完成则再次调用 animateProgress(100) ,并在进度加载期间检查是否有数据返回。若存在,则停止前半段动画,并使用1秒钟将进度条填充至100%。


下面讲解一下如何在请求数据的过程中添加监听函数:


import axios from 'axios';
import ProgressBar from './ProgressBar.vue';

const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

在这个代码片段中,使用了 Axios 拦截器来监听请求的过程。在请求开始之前,向页面添加进度条组件,之后将该组件挂载到页面中,并且将其元素追加到 HTML 的 <body> 标记尾部。


接下来,通过 onDownloadProgress 监听函数来监测下载进度的变化。如果加载完成则移除进度条组件。同时,也可以实现针对使用不同 API 的 ajax 请求设定不同的进度条,以达到更佳的用户体验效果。


axios.interceptors.request.use((config) => {    
const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

config.onDownloadProgress = (event) => {
if (event.lengthComputable) {
progressBar.progress = parseInt((event.loaded / event.total) * 100, 10);
if (progressBar.progress === 100) {
progressBar.finish();
setTimeout(() => {
document.body.removeChild(progressBar.$el);
}, 500);
}
}
};

return config;
}, (error) => {
return Promise.reject(error);
});

参数注入


为了能够灵活地调整接口进度条效果,可以使用参数注入来控制动画速度和完成时间的设定。在 animateProgress 函数中,使用传参来设置百分比范围和动画播放速度,从而得到不同进度条和播放时间的效果。


animateProgress(target, duration) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
}

...

this.animateProgress(90, 1000)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100, 500);
}
})
...

在这个栗子中,将 duration 参数添加到 animateProgress 函数内部,并使用该参数来设置动画速度和完成时间。在第一个调用函数的时候,将异步进度条的播放时间设为 1000ms,从而得到速度较慢、完成时间较长的进度条效果。在第二个调用函数时,将进度条完成时间缩短为 500ms,并获得由此带来的更快动画效果。


总结


实现一个接口进度条函数可以提高网站性能和用户体验效果,同时也可以让页面更加交互性和生动有趣。在栗子中,使用了 Vue 框架来构建动画组件,使用了 Axios 拦截器来监听请求进度,使用了参数注入来控制动画速度和完成时间。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7225417805855916087
收起阅读 »

Js中异步代码挂起怎么解决?

web
从下面代码引入问题 function a() { console.log('aa'); } function b() { setTimeout(() => { //异步代码 console.log('bb'); ...
继续阅读 »

从下面代码引入问题


function a() {
console.log('aa');
}

function b() {
setTimeout(() => { //异步代码
console.log('bb');
}, 1000)
}

function c() {
console.log('cc');
}

a()
b()
c()

上述代码的执行结果为先打印'aa',再打印'cc',等一秒后再打印'bb'。哎?我们是不是就有疑问了,我们明显是先调用的函数a,再调用的函数b,最后调用的函数c,为什么函数b的打印结果最后才出来呢?这里我们要清楚的是函数b中定义了一个计时器,执行此代码是需要时间的,属于异步代码,当浏览器执行到此代码时,会先将此程序挂起,继续往下执行,最后才会执行异步代码。那要怎么解决此类问题呢?一个方法是将其他函数体内也定义一个计时器,这样也就会按顺序调用了,但是这样太不优雅了;还一个方法是函数c作为参数传入函数b,在函数b中执行掉,这样也不优雅。es6出来后就可以使用promise来解决此问题了。


js是一种单线程语言


什么是单线程?


我们可以理解为一次只能完成一个任务,如果有其他任务进来,那就需要排队了,一个任务完成了接着下一个任务。



因为js是一种单线程语言,任务是按顺序执行的,但是有时我们有多个任务同时执行的需求,这就需要异步编程的思想。



什么是异步?


当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情。


什么是异步模式调用? 


前一个任务执行完,调用回调函数而不是进行后一个任务。后一个任务不等前一个任务结束就执行,任务排列顺序与执行顺序无关。


什么是回调函数?


把函数当作参数传入另一个函数中,不会立即执行,当需要用这个函数时,再回调运行()这个函数。



以前是通过回调函数实现异步的,但是回调用多了会出现回调地狱,导致爆栈。



举个用回调函数来解决异步代码挂起问题


<body>
<div class="box">
<audio src="" id="audio" controls></audio> </audio>
</div>
<script>
//ajax
let url = ''
function getSong(cb) {
$.ajax({
url: ' 数据地址',
dataType: 'json',
success(res) {
console.log(res);
url = res[0].url
cb()
}
})
}
getSong(playSong)

function playSong() {
let audio = document.getElementById('audio')
window.addEventListener('click', () => {
audio.src = url
window.onclick = function () {
audio.play()
}
})
}

</script>
</body>

代码中用ajax向后端获取数据,这是需要时间的,属于异步代码,当我们分开调用这两个函数,函数getSong中的异步代码会出现挂起状态,导致函数playSong中的url获取不到值,会出现报错的情况,运用回调函数可以很好地解决这个问题。


Promise的使用


先执行一段代码


function xq() {

setTimeout(() => {
console.log('老王');
}, 2000)

}


function marry() {
setTimeout(() => {
console.log('老王结婚了');
}, 1000)
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}
xq()
marry()
baby()

结果为


1.png


???这是不是有点违背了道德,只能说老王是个渣男。这时候我们就需要使用promise对象来调整一下顺序了。


function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王去相亲');
resolve('ok')
}, 2000)
})
}


function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王结婚了');
resolve('ok')
}, 1000)
})
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}


// xq().then(() => {
// marry().then(() => {
// baby()
// })
// })
xq()
.then(marry)
.then(baby)
// xq().then(marry)
// baby()

在这里我们可以理解为老王相亲的时候疯狂对相亲对象promise,才有了后面的步入婚姻的殿堂,结婚后想生个娃也要对妻子疯狂的promise,才有了后面的小王出生了。


老王长叹了一口气,终于通过promise挽回了形象。


小结


Js异步编程方法不只这两种,还有比如事件监听,发布/订阅,生成器函数 Generator/yield等。需要我们一起去探索研究,毕竟‘学无止境’。


作者:一拾九
来源:juejin.cn/post/7225257817345884221
收起阅读 »

😈当一个摆子前端太闲的时候会做什么

国破山河在,城春草木深。 ——杜甫·春望 今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么。 🤯是独自深耕论坛,钻研学术? 👯还是三两闲聊打趣,坐而论道? 💆‍♂️亦或是闭目养神,神游天地? 作为一名优秀的(摆子、摸鱼、切图....
继续阅读 »

国破山河在,城春草木深。 ——杜甫·春望



今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么


🤯是独自深耕论坛,钻研学术?


👯还是三两闲聊打趣,坐而论道?


💆‍♂️亦或是闭目养神,神游天地?




作为一名优秀的(摆子、摸鱼、切图...)前端开发者,在下在空闲时间最喜欢做的还是钻研(混)前端技术(工作量)。


新的一周,新的开始,上篇文章中有同学批评在下说不够“玩”,那么这周就“简单”画一个鼠标精灵再交予各位“玩一玩”吧。



说明一下:在下说的玩,是写一遍嗷


温馨提示:文章较长,图片较多,不耐看的同学可以先去文末玩一玩在下的“大眼”,不满足了再去创造属于各位自己的鼠标精灵



以下是这周“玩具”的简单介绍:



  • 名称:大眼

  • 生辰:发文时间的昨天(2022-08-15)

  • 性别:随意

  • 情绪:发怒/常态

  • 状态:休眠/工作中

  • 简介:没啥特别的,大眼就干一件事,就是盯着你的鼠标,以防你找不到鼠标了。不过大眼有起床气,而且非常懒散,容易犯困。


大眼生活照:


image.png


接下来请各位跟随在下的节奏,一步一步把自己的“大眼”创造出来。


👀 画“大眼”先画圆


老话说“画人先画骨”,同样画大眼也得先画它的骨,嗯......也就是个圆,没错,就是个普通的圆


在下的笔法还是老套路,先给他一个容器。


<div class="eyeSocket"></div>

给大眼容器添加一些必要的样式


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
}
.eyeSocket {
position: absolute; // 浮动居中
left: calc(50% - 75px);
top: calc(50% - 75px);
width: 150px; // 固定宽度
aspect-ratio: 1; // 长宽比 1:1 如果浏览器不支持该属性,换成 height: 150px 也一样
border-radius: 50%;
border: 4px solid rgb(41, 104, 217);
z-index: 1;
}

效果:


image.png


然后就是另外两个圆和一些阴影效果,由于另外两个圆没有特殊的动效,所以在下使用两个伪元素来实现


.eyeSocket::before,
.eyeSocket::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); // 居中
border-radius: 50%;
box-sizing: border-box; // css3盒子模型
}
.eyeSocket::before {
width: calc(100% + 20px);
height: calc(100% + 20px);
border: 6px solid #02ffff;
}
.eyeSocket::after {
width: 100%;
height: 100%;
border: 4px solid rgb(35, 22, 140);
box-shadow: inset 0px 0px 30px rgb(35, 22, 140);
}

效果:


image.png


👀 画龙需点睛


大眼的眼球画好了,之后就需要给它点上眼睛,喜欢什么样的眼睛因人而异,在下就选择这种分割线来作为大眼的眼仁。


为了方便做一些过渡效果,在下使用echarts来完成这个眼仁。


首先在下需要各位通过任何方式引入echarts库,然后给眼仁一个容器,并初始化echarts画布。


<div class="eyeSocket">
<div id="eyeball"></div>
</div>

#eyeball {
width: 100%;
height: 100%;
}

// 画眼球
let eyeball = document.getElementById('eyeball'); // 获取eyeball元素
let eyeballChart = echarts.init(eyeball); // 初始化画布
function getEyeballChart() {
eyeballChart.setOption({
series: [
{
type: 'gauge', // 使用仪表盘类型
radius: '-20%', // 采用负数是为了让分割线从内向外延伸
clockwise: false,
startAngle: '0', // 起始角度
endAngle: '270', // 结束角度
splitNumber: 3, // 分割数量,会将270度分割为3份,所以有四根线
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12, // 分割线长度
lineStyle: {
shadowBlur: 20, // 阴影渐变
shadowColor: 'rgb(0, 238, 255)', // 阴影颜色
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)', // 分割线颜色
width: 4, // 分割线宽度
}
},
axisLabel: false
},
{
type: 'gauge',
radius: '-20%',
clockwise: false,
startAngle: '45', // 倾斜45度
endAngle: '315',
splitNumber: 3,
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12,
lineStyle: {
shadowBlur: 20,
shadowColor: 'rgb(0, 238, 255)',
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)',
width: 4,
}
},
axisLabel: false
}
]
})
}
getEyeballChart();

效果:


image.png


眼仁就这么轻轻松松的画好了,对于常用echarts的同学可以说是轻而易举,对吧。


同时一个静态的大眼也创建完毕,接下来就要给大眼赋予生命了。



再次提醒:长文,而且代码量多,建议抽思路看即可。



✨ 生命仪式:休眠状态


赋予生命是神圣的,她需要一个过程,所以在下从最简单的开始——休眠状态


在下给大眼设计的休眠状态,就是闭着眼睛睡觉,其实不露出眼仁同时有节奏的呼吸(缩放)罢了,相比于整个生命仪式来说,还是比较简单的,只需要修改大眼外框的大小即可。


呼吸

这里在下采用的是css转换+动画的方式


<div class="eyeSocket eyeSocketSleeping">
<div id="eyeball"></div>
</div>

/* ...其他样式 */
.eyeSocketSleeping {
animation: sleeping 6s infinite;
}

@keyframes sleeping {
0% {
transform: scale(1);
}

50% {
transform: scale(1.2);
}

100% {
transform: scale(1);
}
}

sleeping.gif


闭眼

搞定了呼吸,但是睁着眼睛怎么睡得着?


所以接下来在下要帮助大眼把眼睛闭上,这时候咱们前面给眼睛设置负数radius的好处就来了(其实是在下设计好的),因为分割线是从内向外延伸的,所以此时只需要慢慢减小分割线的高度,即可实现眼睛慢慢缩小的效果,即在下给大眼设计的闭眼效果。


实现的效果是:大眼慢慢闭上眼睛(分割线缩小至0),然后开始呼吸


直接上代码


<div class="eyeSocket" id='bigEye'> // 去掉 eyeSocketSleeping 样式,添加id
<div id="eyeball"></div>
</div>

let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0; // 旋转角度
let ballSize = 12; // 眼睛尺寸
let rotTimer; // 定时器

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
startAngle: `${0 + leftRotSize * 5}`, // 加为逆时针旋转,乘5表示速度为leftRotSize的倍
endAngle: `${270 + leftRotSize * 5}`, // 即变为每10微秒移动0.5度,1234678同理
// ...其他
splitLine: {
length: ballSize, // 分割线高度设置为眼球尺寸变量
},
},
{
startAngle: `${45 + leftRotSize * 5}`,
endAngle: `${315 + leftRotSize * 5}`,
// ...其他
splitLine: {
length: ballSize, // 同上
}
},
}
]
})
}
// 休眠
function toSleep() {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
if (ballSize > 0) {
ballSize -= 0.1; // 当眼球存在时慢慢减小
} else {
bigEye.className = 'eyeSocket eyeSocketSleeping'; // 眼球消失后添加呼吸
}
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); // 旋转,
}, 10);
}
getEyeballChart();
toSleep()


旋转实现原理:(看过在下第一篇动效的同学对旋转的实现原理应该不陌生)


修改每个圈的起始角度(startAngle)和结束角度(endAngle),并不断刷新视图,


增加度数为逆时针旋转,减去度数为顺时针旋转



如此一来就实现了眼睛缩小消失,然后开始呼吸的过程,同时咱们的大眼也进入了生命仪式之休眠状态(乱入的鼠标有点烦);


tosleep.gif


✨ 生命仪式:起床气状态


在下相信,在座(站?蹲?)的各位同僚身边或者自身都存在一些小毛病,譬如咱们的大眼,它不但懒,喜欢睡觉,甚至叫醒它还会生气,通俗讲就是有起床气


心理学上说有种说法是:情绪会让你接近生命的本真


生命不就是情绪的结合嘛,没有情绪怎么能称之为生命的呢?


在设计之前我们还有点准备工作,就是让大眼先处于休眠状态


<div class="eyeSocket eyeSocketSleeping" id='bigEye'> // 添加休眠
<div id="eyeball"></div>
</div>

// ...其他代码
let ballSize = 0; // 初始眼球尺寸为0
// ...其他代码
// getEyeballChart(); // 把这两行删掉
// toSleep() // 把这两行删掉

唤醒

然后我们需要唤醒大眼,所以首先我们需要添加唤醒动作——点击事件;


let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0;
let ballSize = 0;
let rotTimer;
let isSleep = true; // 是否处于休眠状态
// 添加点击事件,当处于休眠状态时执行唤醒方法
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})
// 唤醒
function clickToWeakup() {
isSleep = false; // 修改状态
bigEye.className = 'eyeSocket'; // 清除休眠状态
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 12 && (ballSize += 0.1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
}

这样点一下大眼它就苏醒了过来:


toWeakup.gif


生气

但是!


这是一个没有情绪的大眼,而在下需要的是一个有起床气的大眼,所以这样的大眼咱们不要!


退格←...退格←...退格←...退格←...退格←...退格←......


......


慢点慢点,也不是全都不要了,咱们只需要修改一下他唤醒以后的操作,给他添加上起床气不就行了?


接着来吧:


首先我们把代表了大眼常态的蓝色系抽离出来,使用css变量代替,然后再苏醒后给他添加成代表生气的红色系


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
perspective: 1000px;
--c-eyeSocket: rgb(41, 104, 217);
--c-eyeSocket-outer: #02ffff;
--c-eyeSocket-outer-shadow: transparent;
--c-eyeSocket-inner: rgb(35, 22, 140);
}
.eyeSocket {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket);
box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow); /* 当生气时添加红色外发光,常态则保持透明 */
transition: border 0.5s ease-in-out, box-shadow 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before,
.eyeSocket::after {
/* 其他属性 */
transition: all 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before {
/* 其他属性 */
border: 6px solid var(--c-eyeSocket-outer);
}
.eyeSocket::after {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket-inner);
box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner);
}

// ...其他代码
let ballColor = 'transparent'; // 默认透明,其实默认是啥都无所谓,反正看不见

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor, // 把眼睛的眼影颜色设为变量控制
color: ballColor,
}
},
},
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor,
color: ballColor,
}
}
},
}
]
})
}
// 生气模式
function setAngry() {
// 通过js修改body的css变量
document.body.style.setProperty('--c-eyeSocket', 'rgb(255,187,255)');
document.body.style.setProperty('--c-eyeSocket-outer', 'rgb(238,85,135)');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'rgb(255, 60, 86)');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(208,14,74)');
ballColor = 'rgb(208,14,74)';
}
// 常态模式
function setNormal() {
document.body.style.setProperty('--c-eyeSocket', 'rgb(41, 104, 217)');
document.body.style.setProperty('--c-eyeSocket-outer', '#02ffff');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'transparent');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(35, 22, 140)');
ballColor = 'rgb(0,238,255)';
}
// 唤醒
function clickToWeakup() {
isSleep = false;
bigEye.className = 'eyeSocket';
setAngry(); // 设置为生气模式
clearInterval(rotTimer);
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 50 && (ballSize += 1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.5);
}, 10);
}
// 点击
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

大眼生气长这样:


angry.gif


更生气

不知道在座(站?蹲擦?)各位是如何看待,但是在下看来,大眼这样好像还不够生气。


没错还不够生气,如何让大眼起来更生气呢,生气到发火如何?


嗦干酒干!


在下这里采用的是svg滤镜的方法,svg滤镜的属性和使用方法非常繁多,在下使用得也不是很娴熟,本文中在下就不赘述了,网上冲浪有许多技术大牛讲的非常好,希望各位勉励自己。emmmm......然后来教会在下,记得给在下留言文章地址


在下使用的是feTurbulence来形成噪声,然后用feDisplacementMap替换来给大眼添加粒子效果,因为feDisplacementMap会混合掉元素,所以在下需要给大眼新增一个大眼替身来代替大眼被融合。


创建大眼替身


<div class="filter"> // 添加滤镜的元素
<div class="eyeSocket" id='eyeFilter'> // 大眼替身
</div>
</div>

.filter {
width: 100%;
height: 100%;
}
.eyeSocket,
.filter .eyeSocket { /* 给替身加上相同的样式 */
/* ...原属性 */
}

image.png


融合


<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
<filter id='filter'>
<feTurbulence baseFrequency="1">
<animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end">
</animate>
<animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end">
</animate>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" />
</filter>
</svg>

.filter {
width: 100%;
height: 100%;
filter: url('#filter'); /* 开启滤镜 */
}

copy.gif


芜湖~果然献祭了一只“大眼”出来的效果看起来确实还不错哈?确实看起来酷炫多了,不愧是**“献祭”**啊!


真眼出现


既然粒子效果已经产生,咱们的真实大眼也就不需要躲躲藏藏了,该站出来获取这粒子“光环”了!


大眼:哈!


fire.gif


额......


其实......


也挺好看的嘛,不是吗?毕竟不是献祭的真正的大眼,毕竟是个替身,效果没有本体好也是很正常的对吧。



本质上是因为feDisplacementMap设置了scale属性的原因。


feDisplacementMap其实就是一个位置替换滤镜,通过就是改变元素和图形的像素位置的进行重新映射,然后替换一个新的位置,形成一个新的图形。


scale就是替换公式计算后偏移值相乘的比例,影响着图形的偏移量和呈现的效果。



但是话虽如此,咱这个光环不能真的就这么戴着呀,咱们还需要对光环的位置进行一些微调。


.filter .eyeSocket {
left: calc(50% - 92px);
top: calc(50% - 92px);
}

goodfire.gif


看看,看看!这不就顺眼多了吗,献祭了替身,所以尺寸都是非常契合的,而且共用了样式,所以当大眼生气的时候,光环也会跟着生气。


这下光环也有了,看起来的确比之前更生气了。


但是我们还需要对大眼做一些细微的调整,因为大眼在常规状态下并不需要这个光环,睡着的时候光环在旁边“滋啦滋啦”不吵的慌么,所以我们还需要把常态下的大眼光环给消除掉。


在下采用的是不透明度opacity来控制,当大眼处于生气状态时,光环为不透明;处于常规状态时光环透明不可见。


.filter .eyeSocket {
opacity: 0; // 默认状态下不透明度为0
left: calc(50% - 92px);
top: calc(50% - 92px);
transition: all 0.5s ease-in-out; // 添加过渡效果,值得注意的是不能丢了原本的过渡效果,所以这里使用all
}

// ...其他代码
let eyeFilter = document.getElementById('eyeFilter'); // 获取元素
// 唤醒
function clickToWeakup() {
eyeFilter.style.opacity = '1'; // 不透明度设为1
// ...其他
}
deathEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

这样设置完,一个更生气的大眼就这样出现了:


moreAngry.gif


更更生气


不知看到这样发火的大眼,各位是不是已经满足于此。


但是在下认为不,在下觉得一个真正足够生气的大眼,不只局限于自己生气,还需要找人发泄!!


所以在下还给大眼添加了一些大眼找人的动效(当然是找不到的,它这么笨)。


其实就是让大眼左右旋转,通过css转换来实现。


<div class="eyeSocket eyeSocketSleeping" id='bigEye'>
<div id="eyeball"></div>
</div>
<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
...
</svg>

/* ...其他样式 */
body {
/* ...其他属性 */
perspective: 1000px;
}
.eyeSocketLooking {
animation: lookAround 2.5s; // 添加动画,只播放一次
}
/* 环视动画 */
@keyframes lookAround {
0% {
transform: translateX(0) rotateY(0);
}

10% {
transform: translateX(0) rotateY(0);
}

40% {
transform: translateX(-70px) rotateY(-30deg);
}

80% {
transform: translateX(70px) rotateY(30deg);
}

100% {
transform: translateX(0) rotateY(0);
}
}

// ...其他代码
let bigEye = document.getElementById('bigEye'); // 获取元素
let eyeFilter = document.getElementById('eyeFilter');

// 唤醒
function clickToWeakup() {
// ...其他代码
eyeFilter.className = bigEye.className = 'eyeSocket eyeSocketLooking'; // 同时给大眼和光环添加环视动画
}

bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

看看大眼在找什么?


lookaround.gif



向左看时,Y轴偏移量为-70px,同时按Y轴旋转-30°


向右看时,Y轴偏移量为70px,同时Y轴旋转30°



✨ 生命仪式:自我调整状态


这个状态非常好理解,大眼虽然有起床气,但是也仅限于起床对吧,总不能一直让它生气,气坏了咋办,带着情绪工作,效果也不好不是吗。


所以我们还需要给它一点时间,让它自我调整一下,恢复成正常状态。


这个自我调整状态就是一个从生气状态变回常态的过程,在这个过程中,大眼需要将生气状态的红色系切换为常态的蓝色系,同时红眼也会慢慢褪去恢复正常。


其实这个自我调整状态还是属于唤醒状态中,只是需要放在起床气状态之后。


这里在下采纳了上文中有位同学给的建议,监听动画结束事件webkitAnimationEnd,然后将自我调整放在动画结束以后。


同时这里也有两个步骤:



  1. 退出起床气状态

  2. 变回常态


为了保证两个步骤的先后顺序,可以使用Promise来实现。不懂Promise的同学可以先去学习一下,在下也讲不清楚哈哈哈哈。


// ...其他代码
bigEye.addEventListener('webkitAnimationEnd', () => { // 监听动画结束事件
new Promise(res => {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart(); // 更新视图
ballSize > 0 && (ballSize -= 0.5); // 眼球尺寸减小
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
if (ballSize === 0) { // 当眼球尺寸为0时,将Promise标记为resolved,然后执行后面的代码
clearInterval(rotTimer);
res();
}
}, 10);
}).then(() => {
eyeFilter.style.opacity = '0'; // 清除光环
eyeFilter.className = bigEye.className = 'eyeSocket'; // 清除环视动画
setNormal(); // 设置常态样式
rotTimer = setInterval(() => {
getEyeballChart();
ballSize <= 12 && (ballSize += 0.1); // 眼球尺寸缓慢增加
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
})
})

添加了这样一个监听事件后,咱们的大眼就已经具备了自我调整的能力了:


back.gif


✨ 生命仪式:工作状态


接下来就到了大眼重中之重的环节,也就是大眼的工作状态


在下给大眼的工作非常简单,就是单纯的盯住在下的鼠标,如果各位想给各自的大眼一些其他的功能,可以自己发挥。


盯住鼠标,不只是说说而已,那么怎么样才能让大眼表现出他已经盯住了呢?


在下的思路是:



  1. 以大眼的位置为原点建立一个直角坐标系

  2. 然后通过监听鼠标移动事件,获取鼠标所在位置,计算出鼠标处于大眼坐标系的坐标。

  3. 将整个视口背景以X轴和Y轴进行等分成无数个旋转角度,通过鼠标坐标的数值和正负来调整大眼眼框和眼睛的Y轴和Z轴旋转,从而达到盯住鼠标的目的。


好的,咱们理清思路,接下来就是付诸于行动。


// ...其他代码
// 工作
function focusOnMouse(e) {
{
// 视口尺寸,获取到整个视口的大小
let clientWidth = document.body.clientWidth;
let clientHeight = document.body.clientHeight;
// 原点,即bigEye中心位置,页面中心
let origin = [clientWidth / 2, clientHeight / 2];
// 鼠标坐标
let mouseCoords = [e.clientX - origin[0], origin[1] - e.clientY];
// 旋转角度
let eyeXDeg = mouseCoords[1] / clientHeight * 80; // 这里的80代表的是最上下边缘大眼X轴旋转角度
let eyeYDeg = mouseCoords[0] / clientWidth * 60;
bigEye.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`;
eyeball.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`;
}
}


注意: 如果觉得旋转角度不够大,可以调整代码中的8060,最大可以到180,也就是完全朝向,但是由于大眼终归是一个平面生物,如果旋转度数过大,就很容易穿帮,如果旋转角度为180,大眼就会在某个方向完全消失看不见(因为大眼没有厚度,这个也许是可以优化的点),所以个人喜好调整吧。



咱们来看看大眼工作时的飒爽英姿:


watching.gif


✨ 生命仪式:懒惰状态


顾名思义,懒惰状态就是......懒惰状态。


在下给大眼设计的懒惰状态就是当在下的鼠标超过30秒没有移动时,大眼就会进入休眠状态


所以生命仪式的最后收尾其实非常的轻松,没有大量的代码,只需要添加一个定时器,然后修改休眠状态的代码,将大眼的所有参数初始化即可。


// ...其他代码
let sleepTimer; // 休眠定时器

// 休眠
function toSleep() {
// ...其他操作
document.body.removeEventListener('mousemove', focusOnMouse); // 移除鼠标移动事件
bigEye.style.transform = `rotateY(0deg) rotateX(0deg)`; // 大眼归位
eyeball.style.transform = `translate(0px, 0px)`; // 眼睛归位
}
// 工作
function focusOnMouse(e) {
// ...其他操作
// 设置休眠
if (sleepTimer) clearTimeout(sleepTimer); // 如果休眠定时器已经被设置,则清除休眠定时器
sleepTimer = setTimeout(() => { // 重新计时
toSleep();
}, 30000);
}

感谢上次掘金官方的提醒,在下把线上代码贴在这,在下文笔较差,看不下去的同学可以直接过来玩一玩,感兴趣再去创建自己的大眼。(没有点运行的不要来问我为什么出不来!!!)



如果自己在码上掘金动手的同学记得不要忘记添加echarts资源


image.png


💐 结语


好家伙,原来再写一遍大眼会这么累,这次是真真正正的“玩”了一天,有功夫的各位同僚也可以去玩一玩,于在下的基础上进行完善,创造出属于各位自己的大眼。当然如果有一些比较好玩的动效也可以留言告知在下,当下次混工作量时在下可以有东西写。


就这样!


image.png


作者:Urias
来源:juejin.cn/post/7132409301380890660
收起阅读 »

IDEA用上这十大插件绝对舒服

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。 1、TabNine TabNine 是一个 IntelliJ IDEA 插...
继续阅读 »

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。


1、TabNine


TabNine 是一个 IntelliJ IDEA 插件,可以为 Java 和 JavaScript 开发人员的代码提供 AI 建议。它分析来自数百万个开源项目的代码,并提供相关且准确的代码片段,以帮助开发人员更快、更准确地编写代码。

使用 TabNine 的众多优势包括:



  1. 有效的代码提示。

  2. 支持大量编程语言。

  3. 为主流编辑器和IDE提供帮助。

  4. 使用机器学习,记住你经常写的代码,并提供极其详细的提示。


地址:plugins.jetbrains.com/plugin/1279…



2、RestfulToolkit


RestfulToolkit 提供了与 RESTful API 交互的有用工具。开发人员可以使用此插件直接从 IDE 轻松测试、调试和管理 RESTful API 请求,从而提高他们的整体效率和生产力。


该插件与 HTTP Client、REST Assured 等流行工具集成,使其成为 RESTful API 开发的完整解决方案。


地址:plugins.jetbrains.com/plugin/1029…


3、MyBatisCodeHelperPro


MyBatisCodeHelperPro 在使用 MyBatis 框架时提高了开发人员的工作效率。它包括代码生成和实时模板,使编写和管理 MyBatis 代码更加容易,节省时间和精力。



此外,该插件支持数据库架构同步和 SQL 文件生成,提高开发效率。



地址:plugins.jetbrains.com/plugin/9837…
dehelperpro


4、CodeGlance


CodeGlance 为开发人员提供了代码右侧添加了简明概览,使他们更容易浏览和理解代码。

地址:plugins.jetbrains.com/plugin/7275…



可以看到在上图右侧区域有一个代码概览区域,并且可以上下滑动。


5、GenerateAllSetter


GenerateAllSetter 有助于为类中的所有属性生成 setter 方法。这可以在编写代码时节省时间和精力,同时也降低了出错的可能性。



地址:plugins.jetbrains.com/plugin/9360…



6、Lombok


Lombok:一个自动生成样板代码的 Java 库。



Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。永远不要再写另一个 getter 或 equals 方法,通过一个注解,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。



地址:projectlombok.org/

需要注意的就是在使用了在 IDEA 中使用 Lombok 插件记得启用 Enable annotation processing


7、Rainbow Brackets


该插件为代码的方括号和圆括号着色,从而更容易区分不同级别的代码块。


地址:plugins.jetbrains.com/plugin/1008…


可以看到添加彩色方括号后,代码可读性有所提高。


8、GitToolBox


它包括许多额外的功能和快捷方式,使开发人员更容易使用 Git。使用 GitToolBox 的众多优点包括:



  1. GitToolBox 在 IntelliJ IDEA 上下文菜单中添加了几个快速操作,允许您在不离开 IDE 的情况下执行常见的 Git 任务。

  2. Git 控制台:该插件向 IntelliJ IDEA 添加了一个 Git 控制台,允许您在 IDE 中使用 Git。

  3. GitToolBox包含了几个解决合并冲突的工具,可以更容易地解决冲突并保持你的代码库是最新的。

  4. Git stash management:该插件添加了几个用于管理Git stashes的工具,使保存和重新应用代码更改变得更加容易。


地址:plugins.jetbrains.com/plugin/7499…


9、Maven Helper


Maven Helper 提供了一种更方便的方式来处理 Maven 项目。


Maven Helper 是一个帮助开发人员完成 Maven 构建过程的工具。该插件包括用于管理依赖项、插件和配置文件的功能,例如查看、分析和解决冲突以及运行和调试特定 Maven 目标的能力。


这可以通过减少花在手动配置和故障排除任务上的时间,使开发人员有时间进行编码和创新,从而提高生产力。


地址:plugins.jetbrains.com/plugin/7179…


10、Sonarlint


Sonarlint 是一个代码质量检测工具,集成了 SonarQube 以动态检测和修复代码质量问题。


Sonarlint 提供实时反馈和建议,帮助开发人员提高代码质量。它集成了 SonarQube 代码分析平台,允许开发人员直接在他们的 IDE 中查看代码质量问题。


这通过在潜在问题到达构建和测试阶段之前检测它们来节省时间并提高效率。 Sonarlint 还可以帮助开发人员遵守最佳实践和编码标准,从而生成更易于维护和更健壮的代码。


地址:plugins.jetbrains.com/plugin/7973…



作者:wayn
链接:https://juejin.cn/post/7225989135999434789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。


4个方面


设计数据库表结构需要考虑到以下4个方面:




  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。




  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。




  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。




  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。




设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:




  1. 简单明了:表结构应该简单明了,避免过度复杂化。




  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。




  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。




  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。




  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。




  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。




最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析



  1. 可以根据红框的标签筛选视频

  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样



  • 综合是根据业务逻辑取值,并不需要入库

  • 类型、地区、年份、演员等需要入库



  1. 设计表结构时要考虑到:



  • 方便获取标签信息,方便把标签信息缓存处理

  • 方便根据标签筛选视频,方便我们写后续的业务逻辑


设计思路



  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中

  2. 类型、地区、年份、演员都设计单独的表

  3. 视频表中设计标签表的外键,方便视频列表筛选取值

  4. 标签信息写入缓存,提高接口响应速度

  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护


表结构设计


视频表































字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表























字段注释
id类型主键id
name类型名称
sort排序字段

地区表























字段注释
id类型主键id
name类型名称
sort排序字段

年份表























字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表























字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:




  1. 比较常用的就是redis缓存

  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗

  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能


列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}
复制代码

go-zero


DB缓存机制


go-zero缓存设计之持久层缓存


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:



  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。

  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。

  3. 或者将视频详情的查询结果整体进行缓存


还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:



  1. 关键问题是想解决管理后台灵活配置

  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。

  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~


总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度



本文抛砖引玉,欢迎大家留言交流。


作者:王中阳Go
链接:https://juejin.cn/post/7212828749128876092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么要使用Docker和容器

1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。 Docker容器提供了一种把...
继续阅读 »


1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。


Docker容器提供了一种把握软件的方法。你可以使用Docker将应用程序封装起来,以便处理其部署和运行时的问题,如:如何在网络上公开它,如何管理其对存储、内存和I/O的使用,如何控制访问权限等等。这些问题都在应用程序本身之外处理,并以一种在所有“容器化”应用程序中保持一致的方式处理。你可以在任何安装了Docker运行时的兼容操作系统主机(Linux或Windows)上运行Docker容器。


除了这种方便的封装、隔离、可移植性和控制之外,Docker还提供了许多其他好处。Docker容器很小(几兆字节),启动速度很快,具有自己内置的版本控制和组件重用机制,可以通过公共Docker Hub或私有仓库轻松共享。


Docker容器也是不可变的,这既有安全性又有操作上的好处。对容器的任何更改都必须部署为一个全新的、版本不同的容器。


在本文中,我将探讨Docker容器如何使构建和部署软件更容易,容器解决的问题,何时容器才是正确的解决方案,何时不是。




在Docker容器之前


多年来,企业软件通常是部署在“裸机”上(即安装在具有对底层硬件完全控制的操作系统上)或虚拟机上(即安装在与其他“客户”操作系统共享底层硬件的操作系统上)。自然地,安装在裸机上使得软件难以移动和更新,这两个限制使得IT难以敏捷地响应业务需求的变化。


然后,虚拟化出现了。虚拟化平台(也称为“虚拟机管理程序”)允许多个虚拟机共享单个物理系统,每个虚拟机以隔离的方式模拟整个系统的行为,包括其自己的操作系统、存储和I/O。IT现在可以更有效地响应业务需求的变化,因为虚拟机可以克隆、复制、迁移和启动或关闭以满足需求或节约资源。



虚拟机也有助于降低成本,因为更多的虚拟机可以合并到更少的物理机器上。运行旧应用程序的遗留系统可以转换为虚拟机,并进行物理退役以节省更多的资金。


但是虚拟机仍然存在一些问题。虚拟机很大(千兆字节),每个虚拟机都包含一个完整的操作系统。只有很多虚拟化应用程序可以合并到单个系统上。分配虚拟机仍然需要相当长的时间。最后,虚拟机的可移植性有限。在某个点之后,虚拟机无法提供快速移动的企业所需的速度、敏捷性和节省成本。




Docker容器的好处


容器的工作方式有点像虚拟机,但更加具体和细粒度。它们将单个应用程序及其依赖项(应用程序运行所需的所有外部软件库)与底层操作系统和其他容器隔离开来。


所有容器化的应用程序共享一个公共操作系统(Linux或Windows),但它们彼此之间与整个系统隔离开来。操作系统提供所需的隔离机制,使这种隔离发生。Docker将这些机制包装在一个方便的接口。


Docker容器的好处在许多地方体现。以下是一些Docker和容器的主要优势:


1、Docker 可以更有效地利用系统资源


容器化应用程序的实例使用的内存比虚拟机少得多,它们启动和停止更快,并且可以在它们的主机硬件上更密集地打包。所有这些都意味着 IT 开支更少。


成本节省将根据所使用的应用程序和它们可能的资源密集程度而异,但容器无疑比虚拟机更有效率。还可以节省软件许可证的成本,因为您需要更少的操作系统实例来运行相同的工作负载。


2、Docker 可以加快软件交付周期


企业软件必须快速响应各种不断变化的情况。这意味着需要轻松扩展以满足需求,并且需要轻松更新以添加业务所需的新功能。


Docker容器可以轻松地将具有新业务功能的新版软件快速投入生产,并在需要时快速回滚到以前的版本。它们还可以更轻松地实施蓝/绿部署等策略。


3、Docker 可以实现应用程序的可移植性


在防火墙后面运行企业应用程序很重要,为了保持紧密和安全; 或者在公共云中,以便于公众访问和高弹性的资源。因为Docker容器封装了应用程序运行所需的所有内容(并且只包含那些内容),所以它们允许应用程序在环境之间轻松穿梭。任何安装了Docker运行时的主机,无论是开发人员的笔记本电脑还是公共云实例,都可以运行Docker容器。


4、Docker 在微服务架构中表现出色


Docker 容器是轻量级、可移植和自包含的,使得更容易按照前瞻性的思路构建软件,这样您就不会试图用昨天的开发方法来解决明天的问题。


容器使得实现微服务等软件模式更加容易,其中应用程序由许多松散耦合的组件构成。通过将传统的“单块式”应用程序分解为单独的服务,微服务允许业务应用程序的不同部分可以分别进行扩展、修改和维护——如果符合业务需求,可以由不同的团队在不同的时间表上进行。


容器不是实现微服务的必要条件,但它们非常适合微服务方法和敏捷开发流程。




容器并不是万能的


需要记住的是,与任何软件技术一样,容器并不是万能的。Docker 容器本身不能解决所有问题。


特别是以下几点:


1、Docker 无法解决软件的安全问题


容器中的软件默认情况下可能比在裸机上运行的软件更安全,但这就像说锁着门的房子比开着门的房子更安全一样。这并没有说明社区的状况、诱人偷盗的贵重物品的可见存在、居住在那里的人的日常生活等等。容器可以为应用程序添加一层安全性,但只能作为在上下文中保护应用程序的一般计划的一部分。


2、Docker 不能神奇地将应用程序变成微服务


如果将现有的应用程序容器化,可以减少其资源消耗并使其更容易部署。但它并不会自动更改应用程序的设计或其与其他应用程序的交互方式。这些好处只能通过开发人员的时间和努力来实现,而不仅仅是将所有内容移动到容器中的命令。


如果将传统的单块式或面向服务的应用程序放入容器中,最终得到的是一个老旧的应用程序在容器中运行。这对你的工作没有任何帮助。


容器本身没有组合微服务式应用程序的机制。需要更高级别的编排来实现这一点。Kubernetes 是这种编排系统的最常见示例。Docker swarm 模式也可以用于管理多个 Docker 主机上的许多 Docker 容器。


3、Docker 不是虚拟机的替代品


容器的一个误解是它们使虚拟机过时了。许多以前在虚拟机中运行的应用程序可以移动到容器中,但这并不意味着所有应用程序都可以或应该这样做。例如,如果你在一个有严格监管要求的行业中,可能无法将容器替换为虚拟机,因为虚拟机提供的隔离性比容器更强。


作者:Squids数据库云服务提供商
链接:https://juejin.cn/post/7226153074078416933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
链接:https://juejin.cn/post/7127653600532103198
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

快到 35 的"大龄"程序员

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。 我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部...
继续阅读 »

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。


我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部,做前端开发,也是 IMWeb 团队的一员,先后做过腾讯课堂和企鹅辅导业务,2020年正式任命 leader,管理差不多10人左右的前端开发团队;2022年3月,因(cheng)为(xu)某(yuan)些(dou)原(zhi)因(dao),加入虾皮金融产品部,现负责消费贷业务+催收业务的前端开发和管理工作。


我的自我介绍完了,如果大家不想浪费更多时间深入了解我的话,知道以上信息已经足够了,为了大家的脑细胞着想,提供给大家 3 个不用思考的快捷选项:



  1. 对我不感兴趣,可以左上角关闭页面(我可以对天发誓,这绝对不是相亲贴);

  2. 觉得可以交个朋友,给自己保留一个未来有惊喜的可能,可以关注我的公众号或者加我微信;

  3. 还想听我唠唠嗑的,欢迎继续看下去呀,一定满足大家的好奇心。




感谢你能够继续看下去。我想了很久,怎么样才能不至于让我的自我介绍写成流水账,但是,当我想了更久的时间之后,我发现,我想把这份流水账写出来更难,因为,很多的经历我都不记得了,我只能把我的记忆片段写下来,拼凑出我的职业生涯。好记性不如烂笔头,我觉得本文我可以永远留存并持续迭代,直到我的职业生涯结束的时候,可以用来回顾我的人生,也不失一桩美事。我也推荐大家这样做。


我的前端之路的伊始


我的第一份进入企业的工作是在2011年,大三实习,在杭州阿里,阿里妈妈广告部门(部门全称已经不记得了),后台开发,你没有看错,是的,我是后台开发,那会儿我还不知道前端,大学课程也没有一门是教前端的。


我对于阿里的印象,绝不是现在的"味儿"。我对阿里最大的印象还停留在当初那个时代,有三:



  1. 江湖气派,店小二文化,随性,直来直往,互相接受度非常高,我是非常喜欢这个文化的,当时阿里实习是不能拥有花名的,这是我职业生涯最大的遗憾之一,我还很清楚记得,当时我曾经查过,好像还没有人取名曹操,不过也是我的异想天开,因为即使我转正,我也没有那个资格取这个花名。

  2. 开放,真得非常开放,当我在新人入职欢迎聚餐中,脱到只剩裤衩的时候,我相信我那会一定是完全理解了开放这个词了。虽然直到现在回忆起来,还会有点不适,但是,当经历了那次聚餐之后,隐隐中,我会潜意识地得觉得,好像自己没什么是不可以“坦诚相待”的。

  3. 倒立文化,换个角度思考,我自认为我完全做到了,当我换个角度思考我的职业的时候,我走上了前端之路。。。


虽然在我拿到转正 offer 的时候,还是毅然决然选择保研(其实是被父母逼的)并转前端,但是我还是觉得,我在阿里的大半年实习期间,是我整个开发生涯中成长最快的时期,在那里,我学到了太多太多,以至于到现在我的开发习惯还会保留当时的一些痕迹:




  • 当我碰到需要服务运维的场景,我一定是首选 bash 脚本,然后是 python,最后才是 js,基本不会是 js 的,因为没什么是前两者做不到的。定时任务,文件锁,离线计算,文本处理等等,到现在我还记忆犹新。




  • 记不清写了多少 Map Reduce 了,但是当时,我真得被 Hadoop 的设计原理给深深的吸引到了,原来,大数据处理&分析,分布式计算和分布式文件系统有这么多的挑战,但它的解决方案又是这么的精简,直到现在,我仍然坚信,任何架构设计,一定是精简的,当我跟别人讨论架构的时候,如果他讲不清楚,或者表达非常复杂的时候,我就知道,不是设计有问题,就是表达有问题,我们还有更优的方案。天地良心,当时实习的时候,我真的是非常认真的做后台开发的,当时我还啃下了那本大象书呢,现在想想也觉得不容易,当年我是真喜欢看书呀。




  • 架构设计非常“好玩” ,在当时,阿里内部有非常多的技术分享,我常常去听我自己喜欢的分享,让我的技术视野得到了非常大的增长,比如:



    • 中文的分词要比英文的分词要难很多,最终发现,自然语言处理不是我的菜;

    • 推荐系统的结果是竞速的,当时真的有想入职百度,去学习搜索引擎的冲动;

    • 秒杀的多重降级、动态降级,各种“砍砍砍”,非常有意思。


    在当时,我学到的一个最重要的知识是,任何架构设计都是因地制宜的,不能墨守成规




在实习转正答辩的时候,最后问我的未来规划的时候,我的回答更多是偏架构设计和 UI 相关,现在回想起来都会觉得搞笑,当时我一度以为是转正失败了,但是没想到阿里开放到这都给我发了 offer,真得很感激我的老领导,但也觉得很对不起他们,因为我真的不想淹没在数据的海洋里,我更喜欢开发一些"看得着,摸得到"的东西,我会觉得做这个更有意思,所以,我选择了前端。


一波三折的腾讯梦


先说说为什么想去腾讯吧,因为我是广东人,父母都在深圳,都希望我回深圳,当时深圳不用多说,大公司就腾讯了,所以,我在实习和毕业的选择上一直都非常明确,就是深圳腾讯,但是我自己都没想到我回深圳是这么的坎坷。


研一找实习的时候,我第一次面试腾讯挂了,当时是电话面试,我记得是早上,很突然接到了面试电话,然后突然开始面试,我完全没有准备,很自然地就挂了,跟我同一个项目的做 web 服务的同学拿到腾讯的实习 offer 了,当时心理还有点不平衡,但是后面我也很快拿到新的 offer 了。


插一段题外话,当时我跟另外两个同学一起跟着导师外包项目,项目也挺有意思的,因为我们是嵌入式方向的实验室,所以我们做的是一个实时监控系统,有个同学主要负责传感器和网络编程,另外一个同学主要负责 web 后台服务,我负责前端页面(extjs),我们的项目是给一家医院做冰柜的温度实时监控系统,在冰柜中放入温度传感器,然后不断把冰柜的温度数据通过各个局域网网络节点传输器一路传到中心服务器中,然后中心服务负责存储并分析数据,落库并返回数据到前端,展示实时监控页面并添加告警功能。整个系统非常有意思,通过这个项目,我深深地感受到物联网的魅力,软硬件结合的威力。这还只是单向的,如果可以做到双向,再加上智能化,那基本就可以取代人的工作了,实际上,现在很多的无人XXX系统,他们的本质都是这个,现在互联网环境这么差,哪天干不下去了,换个行业,做物联网+虚拟+AI,做现实虚拟,实业升级事业,也是大有可为的。


回归正题,在腾讯突然面挂之后,我就开始认真复习,专门找前端的实习工作,然后很快就找到了网易的教育部门的前端开发 offer,这段经历我印象最深刻的是当时那批前端的笔试当中,我是最高分的,面试也没怎么问就拿到 offer 了,果然有笔试就是好呀,妥妥我的强项。或者是因为我有这段经历,所以后面我才会被分配到腾讯做教育吧。。。


在网易,我做的是网易云课堂和网易公开课相关的前端工作,在网易的实习过程中,我的前端基础和实践不断加强,三剑客,前端组件库,前端基础库,模块化,构建,浏览器兼容处理等等,基础技术收获很多,但是大的方面上,没什么特别的收获,就像网易的公司文化一样,没什么特别的感受,至今都没留下什么。在网易,印象最深的两个点就是:



  • 除了游戏,万般皆下品,主要靠情怀。其实这点跟在腾讯做教育也差不多;

  • 网易的伙食真的是互联网第一,不存在之一。


研二找工作的时候,我研究了腾讯的校招路演,发现有以下问题:



  • 杭州算是最后一站那种,时间很晚,到我们这边黄花菜都凉了;

  • 杭州离上海很近,过来招聘的团队应该基本都是上海的;

  • 像我这样的杭州毕业生不去阿里想去腾讯的奇葩真得不多了。


因此,我决定跑去上海参加校园招聘。当年校招我只面了百度跟腾讯,当时校园招聘都是统一笔试,面试,我记得百度是去他们上海分公司内部面试的,面了 2 轮就到 hr 了,还能留下记忆的是当时 2 面面试官对我的阿里经历很感兴趣,问了非常多,我当时就懵了,你们不是招前端的么。


然后是腾讯的面试,在一家 5 星级酒店的房间面的,当时进去就问我,能不能接受 web 服务研发岗位,我当时第一反应就是,你有无搞错呀!?但是机敏如我,肯定是立刻回答可以接受的,虽然这是一个随时都可以被废弃的万金油 api 岗位,但是它胜在可上可下,呸,是可前可后,啊呸,是可前端可后台,必须难不倒我呀,然后就是很无聊的面试,问了一些简单的前端题,了解了一下实习项目,最后做了一道智力题就结束了,相比百度的面试,有点看不过去了。最后问了我填的志愿是深圳的岗位,问我服不服从调剂,我说只想看深圳岗位,让我一度以为我又挂了,不过最后还是顺利进到 hr 的房间。。。面试,随便瞎聊,最后确认我只想回深圳,并表示可以给我争取调剂。


在回杭州的火车上,我知道百度的 offer 基本稳了,不过是上海的,腾讯的 offer 还是内心忐忑,实在是腾讯的面试有点“敷衍”了,那会儿我都在思考怎么忽悠我爸妈先在上海工作2年再回深圳了。不过没过2天,就收到了腾讯的 offer,是深圳易迅的前端开发岗位,当时在上海招聘的 90% 都是易迅(腾讯收购)的招聘,也很感谢当时帮我调剂的面试官跟 hr 了。兴奋的我在跟百度 hr 电话的时候就直接拒掉了百度 offer,现在回想起来,还真有点轻率了。


很快,我就决定提前到腾讯实习,当我坐在回深圳的火车时,看到了一则新闻:腾讯决定出售整个 ECC 给京东置换京东股份,并和京东开启战略合作。我不太记得我回家那天是什么心情,我只记得我办理入职手续的时候,窗外的天空是没有太阳的。我甚至都没认识全我的团队,因为当时所有工作都暂停了,那会儿,不是开大会,就是漫长的等待,现在想想,还挺像现在经历这场寒冬的我们一样,迷茫,忐忑,甚至有点慌张。


我加入了应届生群,在联名信上“签名”,在论坛上堆楼,终于,高层听到了我们的声音,跟京东友好协商之后,给予了我们这届应届生自主选择权 —— 是去京东还是留在腾讯,待遇不变。毫不犹豫地,我选择了腾讯。


写到这里,我还是很感慨,我的腾讯梦还真是一波三折,除了幸运还是幸运,或许因为在这件事情上花光了我前半生积攒的运气,以至于直到到现在所有的年会我都是阳光普照,深圳车牌摇号还遥遥无期,但是,我的腾讯之路还是开启了。。。


我职业生涯中最大的幸运 —— IMWeb 团队


多动动脑子


刚转来 IMWeb 团队,我接到的第一个任务是做一个爬虫,要爬淘宝教育的课程和购课数据。这不是很简单吗,之前做过呀,殊不知噩梦即将开始...


不到半天我就写好了,包括邮件模板,也自测好了,正式启动,美滋滋去喝杯茶,回来就能交差了。当我摸鱼回来一看,咦,脚本停了,接口报错,被限频了。于是我进入了疯狂调试模式,添加重试逻辑,不断调整请求频率策略,最终祭出终极策略,3分钟请求1次,这下不会被限频了吧,在稳定跑了1个小时没问题之后,我安心的下班回家了。


第二天到公司,数据跑完了,完美。于是,我做了最后的数据校对和计算调整,然后调通自动发送邮件的逻辑,再次执行。当我美滋滋地再次摸鱼回来,发现脚本又停了,这次是新的错误,没有错误信息,就是 5xx,黑人问号啊,于是各种调试各种排查,最终得出一个结论,ip 被拉进黑名单了。


好家伙,算你狠。于是我上网各种研究代理,不管免费付费,能用就是好代理,再次调整策略,申请十多个账号轮流爬,光荣牺牲了一批又一批的 ip 之后,我还是败下阵来。那个时候,我觉得我的人生都是黑暗的,我的面前立着一座大山,我怎么样都翻不过去。


当老大咨询进度的时候,我并没有得到任何安慰和建议,而是一句“多动动脑子”。


我已经忘记当时的我是什么心情,被打击成什么样了。也已经忘记了一周后是怎样完成任务的。我只记得,之后我只花了半天时间就爬了网易云课堂和慕课网的数据,他们就是毫不设防的裸......奔。


任性如我


对于我们程序员来说,碰到的最棘手的问题中,无法复现的问题肯定名列前茅。


有一次需求发布,现网验证的时候发现了一个问题,在本地和 test 环境都复现不了,live 打断点也复现不了,真是绝了,打断点没问题,不打断点有问题,我大概能猜到问题,但是需要打印一些日志来定位最终问题,可是只能在 live 才有效,先不说 live 构建会自动删掉 console.log 语句,执行一次 live 部署非常慢,如果要折腾几次来调试,那半天都解决不了问题了。


急性子的我肯定受不了这种折磨,所以我选择了直接登录现网服务器改代码调试。先把压缩文件 down 下来,本地格式化,找到对应位置添加 console.log,然后传回服务器覆盖文件,禁用 cdn 资源,直接在现网复现排查问题。几分钟不到就确定问题,然后修改代码重新部署一次过完成最终需求发布。整个过程行云流水,但是我内心慌得一比,这要是出问题被发现,那后果不敢想象。


还有好几次的 Node 服务问题,我也是直接现网调试,其实 Node 服务才是最适合这么做的场景,但是,我并不是推荐大家这样做。再到后面,我行我素的我越来越能够理解流程机制的用意和作用,现在踏上管理岗位,我更希望小伙伴们是严格遵照流程规范来工作,但我的内心深处,还是住着一个不羁的我


“万恶的” owner


“清龙,这个需求就由你来当 owner 吧。”


“owner?要做什么?”


“就是这个需求的负责人,看看需求进度有没有问题,发布别延期就行”


“好”


【需求开发中...】


“清龙,现在需求进度怎样?有没有风险?”


“我这边没问题,我问一下后台同学看看”


“你可以每天下班前收集一下大家的进展,然后在群里同步哈”


“好”


【需求测试中...】


“清龙,需求测得怎么样啦?”


“......(这不应该问测试吗)应该问题不大,我这边的 bug 都处理完了,我找测试跟进一下测试进度哈”


“可以每天下班前找测试对齐一下测试的整体进度,让测试在群里同步哈”


“好”


【需求发布中...】


“清龙,需求发得怎么样啦?”


“后台发完了,前端正在发,问题不大”


“牛呀,一定要做好现网验证,发布完成记得要在群里周知哈”


“好~”


自从团队推行 owner 机制,工作量是噌噌噌地往上涨,但是工作能力也有很大的提升。


怎么说呢,这是毁誉参半的机制,重点在于每个人怎么看待这个事情,它可以是卷、分担压力的借口;它也可以是培养新人,锻炼项目管理能力,提升沟通协调能力的最佳实践机会。


我眼中的 IMWeb 团队


它是综合的。我们团队涉猎的领域非常广,移动端,pc 端,后台均有涉猎,正因如此,我们有非常好的土壤茁壮成长,尝试各种新技术。在很早的时候,我就在数据接口低代码平台落地 GraphQL,实现了基于mysql 的 GraphQL 的 node 版本,不说业界,在公司内肯定是领先的。在公司成长的过程中,我们团队也在成长,在前端工程化上也有很多的实践和成果。后面腾讯搞 Oteam,我们团队也多有贡献。


它是着眼于业务的。 我们团队推崇做产品的主人翁,坚持不懈地以技术手段助力业务发展。我们做的所有项目都是为了业务服务,为了整个团队服务。我们团队是专业的,没有钻技术的牛角尖,更多地是扎根于业务,一切以实际出发,更多以落地与实践为主。但我们团队的业务并不是很出彩,属于半公益的教育,至今我仍然唏嘘不已,只能感叹时运不济,现在回过头来细品,再厉害的技术,没有好的业务相辅相成,也是无法一直走下去,业务是王道啊。


它是被信任与敢于信任的。作为前端团队,能够有那么大的空间来施展身手,这足以说明我们团队是受到领导的充分信任的,我们团队也非常努力来对得起这份信任。而团队也非常信任团队里的每一个人,会给予很多的试错机会和时间,就看我们有没有耐心,主动与坚持了。


在一个已经建立了一定文化的团队是幸福的,它是需要细品的,但很多人都不愿意去感受。这两年,我过得很难受,不知变通地我一直守着这份坚持,与已经被潜移默化的团队文化对抗,最终只是落得个遍体鳞伤。但是我并不后悔,反而很庆幸,因为最后我找到了自己内心的真相,一直以来,我觉得是 IMWeb 团队造就了我,其实,我所依恋的一直都是它的价值观与文化,而我愿意一直为之践行。


我的管理之路


我正式任命是在 2020年上半年,但实际上,我在 2018 年下半年就从腾讯课堂调到了企鹅辅导,从一组调到三组,并开始做一些团队管理的工作。整体而言我的管理经验成长的非常缓慢,这是我自己的结论。


首先,我的角色转变比较缓慢。经常看到小伙伴们做事情太“慢”,我都忍不住要自己上,或者直接告诉他们答案,我知道这很不好,但是初期的我就是忍不住,我感觉我的管理之路就是憋气之路,最后总结就是,在大方向上,我要站出来,但是具体实施层面,我要当个隐身人,这对我来说,非常难受。


其次,我是主猫头鹰次考拉的重事风格,不太擅长管理小伙伴的情绪还有激励,沟通和语言艺术真是我需要投入一生去学习锻炼的课程。另外,我有一个最大的问题就是不喜欢冲突,直接导致我不太擅长争取资源,这会让我觉得很对不起小伙伴们,这点也是我离开腾讯最大的原因吧。感觉我比较适合增量市场,在存量市场这点真的是致命的,不过专心搞好业务不挺好吗,何苦浪费时间在这些地方。


作者:潜龙在渊灬
链接:https://juejin.cn/post/7189612924116140092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

对话大环境严峻之下选择跨行跨岗的小姐姐

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。 她讲述自己...
继续阅读 »

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。


她讲述自己行业的时候,没感觉到丝毫的怯场,反而将自己的所处的情况以及计划都理得很清晰。为什么我会在意这些,大概是自身所不具备的看到别人具备的特质则显得格外的在意。从她描述的状况和语气来看,她已经解除了自己的困境,话里透露着对以后的走向多了些坚定和勇气。据后来了解到是咨询了行业大佬V姐,V姐将她的现状和想法做了解读和引导,大概是能如此吧。


她硕士毕业,去了一家央企工作,从事金融相关的行业,日常需要对接国内外的业务,目前该行业是天花板,自己处于事业的瓶颈期,也在离职过程中。听到这里或许觉得,今年大环境差,就业严峻,能苟着就苟着,况且还是央企,养老铁饭碗啊。是的,我也是这样想的,今年杭州上半年大肆裁员,海投简历多半已读不回,看多了it从业者在社交平台贩卖焦虑。但此刻的她,已经分析过自身的处境才能做出这一举动。毕竟相比于环境怎么样,时间其实更加宝贵,财富的积累更能决定你所做的和做想的是否值得。所以我是很支持她,结束之后和她交流了一番,并表示将来能够有更多机会联动。


一段时间后,周三的下午,正在码bug中,突然收到了她的消息,从她的话里提取出来就是她成功裸辞跳槽,还跨行跨岗,心里暗暗称强,终于能联动上了。那个下午疯狂码代码中,回了一句手头忙得晚点,觉得交流是在微信里的,可以推迟到晚上交流。当她回复预约线下当面聊的时候,我内心其实是拒绝的,像我这种比较腼腆内向的男孩子,一向没太多线下交流的机会,除非早期体验生活接外快硬着头皮上,还硬是拉上了同学陪衬,且这次还是女孩子,啊,好为难啊。


图片


当然,这个弱点一直被我重视,今年上半年都在为这个弱点做了大量的功课。每次都会和内心的自己和解,尝试和周围的人交流,小区的人,店家老板,租友,其他行业的人,再者就是周末跑出去摆摊和阿姨大叔一块聊天,聊聊他们女儿的情况......


所以,这次当然也和自己和解了,决定赴约。再次见面像极了朋友重逢的感觉,或许在职场被领导或甲方压迫久了,这次没有压迫感,久违了,唯独怕自己不知道专不专业的知识能不能够帮助到她。


见面聊了很多,发现跨行跨岗位,能够看出给她带来了很多的压力。本身专业学的就不是这个,现在从事网络安全相关的岗位,不仅要把这些计算机相关知识消化,还需要把公司提供的产品都要有所掌握,光说她工作内容下来我统计她叹了两三次气,我赶忙安慰慢慢来,一切都会好的,她遂即回我她很喜欢这份工作,累是确实是累,但是很有意义,是带着笑着说出来的。


期间也吐露过以后成熟了打算和自己的挚友出来单干,这么敏感重要的信息我当然很在意并予以支持和鼓励,这充分的表达了她对于自身情况和职业规划是清晰的。


对于自己的人生或者职业规划,有一个清晰的目标是很重要的,至少我认为很重要,“大方向不变,小动作可伸缩”,但凡你敢想敢做,就能有无限可能,不管概率大不大,有经验包就是一个值。


或许有股鸡汤的味道,但我身边接触的大佬,有互联网行业的,做自媒体的,做副业的,总结就是他们从不会把网络上频繁出现的“润”,“躺平”,“摆烂”之类慰藉心灵的词摆在嘴上,反而他们做事非常的雷厉风行,不拖拉,很明白自己的现状,以及自己想要干嘛。总比自己状况都摸不清楚,自己本职的工作都没搞明白,一边想“润”,一边好高骛远强得多。


和她交流之后,给我的感觉就是做事雷厉风行,学习能力强的那种,非但没有被跨行跨业给恐惧到,反而越发的有精神。期间问了我一些市面上主流比较热门的内容,例如云服务与本地机房,防火墙的原理,堡垒机的原理和作用,系统架构以及服务治理等等。开始是不适应的,可能内心戏比较多,我讲得很生硬岂不尴尬?或者旁边人觉得我这桌好怪,大晚上不好好吃东西讲些莫名其妙的话题?想刀一个人心是藏不住的,想帮一个人的心也是藏不住的,我尽可能地调度我那单核脑容量,同时组织人话讲述出去,专不专业我不知道,但是我知道有些比较主观,有些没能覆盖全面,讲的过程中感觉不到周围有人的存在。


结束之后,虽然打滴回去的路上还在讨论网络安全案例图,我不知道司机师傅会不会懵逼,但我觉得讨论这些很有趣,哈哈。期待下次的联动。


图片
▲图/ Ariel 拍摄提供


这一次的联动,从她身上也学习到了很多。她对于一个新环境的适应能力和学习能力是很强的,做事的风格雷厉风行,遇到难题会选择非常有效的方式。上一个做事雷厉风行,从不拖拉让我印象深刻的大佬还是去年认识,成功转型go开发,很遗憾的就是因为很多因素没能一起共事。


相反,我在往期文章多次提到的挚友鑫仔,学的建筑行业,近期询问他的状况,因为大环境恶劣,破坏了自己在学校规划好的一切,从深圳跑回到家乡二三线城市,选择了一家低于自己预期但实属无奈的设计院工作。他日常焦虑,对于过来人的经验看待他的状态和举动像是以第三视角在看他,于是对他进行了言传身教,希望对他有所帮助,能够再次恢复挥斥方遒的热血青年。


不知不觉,文章看多了,写得多了,身边各行各业的人接触多了,越发觉得什么最重要,要像什么样的人靠齐,真正做到知行合一是能带来多大的益处。


作者:桑小榆呀
链接:https://juejin.cn/post/7118992017175838750
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用Snackbar代替Toast

用Snackbar代替Toast Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Sna...
继续阅读 »

用Snackbar代替Toast



Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Snackbar。



认识Snackbar


我是在看某个三方库源码的时候发现它的,在以往的开发经验中,它出现的频率并不高。我在某个三方库中发现,在两个项目中,提示的表现却不一样,代码怎么会骗人呢。于是认真的看这个三方库的源码,直到看到下面的判断。



static {

try {

Class.forName("android.support.design.widget.Snackbar");

Class.forName("android.support.design.widget.BottomSheetDialog");

HAS_DESIGN_LIB = true;

} catch (Throwable ignore) {

HAS_DESIGN_LIB = false;

}

}

protected AbsAgentWebUIController create() {

return HAS_DESIGN_LIB ? new DefaultDesignUIController() : new DefaultUIController();

}

这里的意思是说,如果项目中有SnackbarBottomSheetDialog库的引用,则使用DefaultDesignUIController的UI,也就是Snackbar的提示;没有的话,则使用DefaultUIController的UI,也就是默认的Toast提示。


实际上其实谷歌在Android 5.0的时候就推出了Snackbar,它是Material Design中的一个控件。


实践 Snackbar


下图为ToastSnackbar的展示效果


image.png



  • 简单的提示


Snackbar的基本用法和Toast类似



Snackbar.make(findViewById(R.id.root), "这是一条提示", Snackbar.LENGTH_LONG).show();


  • 带有Action的提示



Snackbar snackbar = Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE);

snackbar.setAction("取消", new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this,"点击取消",Toast.LENGTH_SHORT).show();

}

});

snackbar.show();

这里只能设置一个Action,不然旧的会被替代掉。




  1. Snackbar.LENGTH_INDEFINITE:代表无限期的显示,一直显示,点击按钮才可以隐藏




  2. Snackbar.LENGTH_LONG:长时间提示




  3. Snackbar.LENGTH_SHORT:短时间提示





  • 修改样式


更改Snackbar的背景颜色



snackbar.getView().setBackgroundColor(Color.parseColor("#0000ff"));

更改Action提示的颜色



snackbar.setActionTextColor(Color.parseColor("#ffffff"));

更改padding的距离



snackbar.getView().setPadding(50, 50, 50 , 50);

操作文字,比如添加图片、更改文字内容、更改文字颜色,更改文字大小等。


虽然Snackbar没有提供给我们直接操作文字样式的方法,但我们可以通过findViewById获取这个文字,然后就像操作TextView一样去操作它就可以了。


怎么获取TextView?



TextView textView = snackbar.getView().findViewById(R.id.snackbar_text);

snackbar_text id来自Snackbar的源码。获取ID的时候编辑器可能会报错提示,实际上是可以运行的。



@NonNull

public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {

ViewGroup parent = findSuitableParent(view);

if (parent == null) {

throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");

} else {

LayoutInflater inflater = LayoutInflater.from(parent.getContext());

SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);

Snackbar snackbar = new Snackbar(parent, content, content);

snackbar.setText(text);

snackbar.setDuration(duration);

return snackbar;

}

}


<view

xmlns:android="http://schemas.android.com/apk/res/android"

class="android.support.design.widget.SnackbarContentLayout"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom"

android:theme="@style/ThemeOverlay.AppCompat.Dark">

<TextView

android:id="@+id/snackbar_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_weight="1"

android:layout_gravity="center_vertical|left|start"

android:paddingTop="@dimen/design_snackbar_padding_vertical"

android:paddingBottom="@dimen/design_snackbar_padding_vertical"

android:paddingLeft="@dimen/design_snackbar_padding_horizontal"

android:paddingRight="@dimen/design_snackbar_padding_horizontal"

android:ellipsize="end"

android:maxLines="@integer/design_snackbar_text_max_lines"

android:textAlignment="viewStart"

android:textAppearance="?attr/textAppearanceBody2"/>

<Button

android:id="@+id/snackbar_action"

style="?attr/snackbarButtonStyle"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_gravity="center_vertical|right|end"

android:minWidth="48dp"

android:visibility="gone"/>

</view>

作者:Android唐浮
链接:https://juejin.cn/post/7103803312910106660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

定位都得集成第三方?Android原生定位服务LocationManager不行吗?

前言 现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。 有些同学觉得不就是获取到经纬度么,An...
继续阅读 »

前言


现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。


有些同学觉得不就是获取到经纬度么,Android 自带的就有位置服务 LocationManager ,我们无需引入第三方服务,就可以很方便的实现定位逻辑。


确实 LocationManager 的使用很简单,获取经纬度很方便,我们就无需第三方的服务了吗? 或者说 LocationManager 有没有坑呢?兼容性问题怎么样?获取不到位置有没有什么兜底策略?


一、LocationManager的使用


由于是Android的系统服务,直接 getSystemService 可以获取到


LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

一般获取位置有两种方式 NetWork 与 GPS 。我们可以指定方式,也可以让系统自动提供最好的方式。


// 获取所有可用的位置提供器
List<String> providerList = locationManager.getProviders(true);
// 可以指定优先GPS,再次网络定位
if (providerList.contains(LocationManager.GPS_PROVIDER)) {
provider = LocationManager.GPS_PROVIDER;
} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
provider = LocationManager.NETWORK_PROVIDER;
} else {
// 当没有可用的位置提供器时,弹出Toast提示用户
return;
}

当然我更推荐由系统提供,当我的设备在室内的时候就会以网络的定位提供,当设备在室外的时候就可以提供GPS定位。


 String provider = locationManager.getBestProvider(criteria, true);

我们可以实现一个定位的Service实现这个逻辑


/**
* 获取定位服务
*/
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

//第二个参数是间隔时间 第三个参数是间隔多少距离,这里我试过了不同的各种组合,能获取到位置就是能,不能获取就是不能
lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:"+provider +" status:"+status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}

@Override
public void onDestroy() {
super.onDestroy();
lm.removeUpdates(listener); // 停止所有的定位服务
}

}

使用:定义并动态申请权限之后即可开启服务



fun testLocation() {

extRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) {

startService(Intent(mActivity, LocationService::class.java))

}

}

这样我们启动这个服务就可以获取到当前的经纬度,只是获取一次,大家如果想再后台持续定位,那么实现的方式就不同了,我们服务要设置为前台服务,并且需要额外申请后台定位权限。


话说回来,这么使用就一定能获取到经纬度吗?有没有兼容性问题


Android 5.0 Oppo



Android 6.0 Oppo海外版



Android 7.0 华为



Android 11 三星海外版



Android 12 vivo



目前测试不多,也能发现问题,特别是一些低版本,老系统的手机就可能无法获取位置,应该是系统的问题,这种服务跟网络没关系,开不开代理都是一样的。


并且随着测试系统的变高,越来越完善,提供的最好定位方式还出现混合定位 fused 的选项。


那是不是6.0的Oppo手机太老了,不支持定位了?并不是,百度定位可以获取到位置的。



既然只使用 LocationManager 有风险,有可能无法获取到位置,那怎么办?


二、混合定位


其实目前百度,高度的定位Api的服务SDK也不算大,相比地图导航等比较重的功能,定位的SDK很小了,并且目前都支持海外的定位服务。并且定位服务是免费的哦。


既然 LocationManager 有可能获取不到位置,那我们就加入第三方定位服务,比如百度定位。我们同时使用 LocationManager 和百度定位,哪个先成功就用哪一个。(如果LocationManager可用的话,它的定位比百度定位更快的)


完整代码如下:


@SuppressLint("MissingPermission")
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;
private LocationClient mBDLocationClient = null;
private MyBDLocationListener mBDLocationListener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

createNativeLocation();

createBDLocation();
}

/**
* 第三方百度定位服务
*/
private void createBDLocation() {
mBDLocationClient = new LocationClient(UIUtils.getContext());
mBDLocationListener = new MyBDLocationListener();
//声明LocationClient类
mBDLocationClient.registerLocationListener(mBDLocationListener);
//配置百度定位的选项
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Battery_Saving);
option.setCoorType("WGS84");
option.setScanSpan(10000);
option.setIsNeedAddress(true);
option.setOpenGps(true);
option.SetIgnoreCacheException(false);
option.setWifiCacheTimeOut(5 * 60 * 1000);
option.setEnableSimulateGps(false);
mBDLocationClient.setLocOption(option);
//开启百度定位
mBDLocationClient.start();
}

/**
* 原生的定位服务
*/
private void createNativeLocation() {

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:" + provider + " status:" + status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}


/**
* 百度定位的监听
*/
class MyBDLocationListener extends BDAbstractLocationListener {

@Override
public void onReceiveLocation(BDLocation location) {

double latitude = location.getLatitude(); //获取纬度信息
double longitude = location.getLongitude(); //获取经度信息


YYLogUtils.w("百度的监听 latitude:" + latitude);
YYLogUtils.w("百度的监听 longitude:" + longitude);

YYLogUtils.w("onBaiduLocationChanged:" + longitude + "-" + latitude);

stopSelf(); // 获取到经纬度以后,停止该service
}
}

@Override
public void onDestroy() {
super.onDestroy();
// 停止所有的定位服务
lm.removeUpdates(listener);

mBDLocationClient.stop();
mBDLocationClient.unregisterLocationListener(mBDLocationListener);
}

}

其实逻辑都是很简单的,并且省略了不少回调通信的逻辑,这里只涉及到定位的逻辑,别的逻辑我就尽量不涉及到。


百度定位服务的API申请与初始化请自行完善,这里只是简单的使用。并且坐标系统一为国际坐标,如果需要转gcj02的坐标系,可以网上找个工具类,或者看我之前的文章


获取到位置之后,如何Service与Activity通信,就由大家自由发挥了,有兴趣的可以看我之前的文章


总结


所以说Android原生定位服务 LocationManager 还是有问题啊,低版本的设备可能不行,高版本的Android系统又很行,兼容性有问题!让人又爱又恨。


很羡慕iOS的定位服务,真的好用,我们 Android 的定位服务真是拉跨,居然还有兼容性问题。


我们使用第三方定位服务和自己的 LocationManager 并发获取位置,这样可以增加容错率。是比较好用的,为什么要加上 LocationManager 呢?我直接单独用第三方的定位服务不香吗?可以是可以,但是如果设备支持 LocationManager 的话,它会更快一点,体验更好。


好了,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。


作者:newki
链接:https://juejin.cn/post/7131910145574961182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android案例手册 - 仅一个文件的展开收缩LinearLayout

👉实践过程 Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。 前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。 仅一个文件(Java版或...
继续阅读 »

👉实践过程


Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。


前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。


仅一个文件(Java版或Kotlin版),随时复制随时用。
先看效果图


可展开的LinearLayout-仅一个文件复制即用.gif


默认展示两个子item,当点击“显示更多”的时候展开所有的子View,当点击“收起内容”的时候除了前两个其他的都隐藏。


😜使用


我们先来看看使用方式:


<cn.phototocartoonstudy.ExpandableLinearLayout
    android:id="@+id/idExpandableLinearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="芝麻粒儿" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://juejin.cn/user/4265760844943479" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="CSDN" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://zhima.blog.csdn.net/" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="Android/Unity技术" />
</cn.phototocartoonstudy.ExpandableLinearLayout>

直接布局中用即可,或者动态代码添加:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_framelayout);
ExpandableLinearLayout idExpandableLinearLayout=findViewById(R.id.idExpandableLinearLayout);
for (int i = 0; i < 4; i++) {
TextView  txtViewTip = new TextView(this);
txtViewTip.setText("芝麻粒儿添加更多内容"+i);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
idExpandableLinearLayout.addView(txtViewTip);
}
}

😜实现


说完了使用,我们就来说说实现,前面学完LinearLayout后知道该控件使用了wrap_content,如果子View使用隐藏GONE的形式,则高度自动变化,页面布局中和该控件对其的其他控件也会自动变化。


所以,当子View的个数小于设置的默认个数,则不用添加底部,如果子View个数大于默认显示个数,则在最后动态添加一个View,当点击展开和隐藏的时候,其他多余的控件进行GONE和VISIBLE的控制即可。


我们再为控件增加点其他方法:




  1. 修改当隐藏的时候默认展示的条目




  2. 可修改展开和收起的控件文本




  3. 可修改展开和收起控件的字体大小和颜色




  4. 其他功能自己看着加吧




public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}
public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}
public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}

Java版


/**
* Created by akitaka on 2022-08-11.
*
* @author akitaka
* @filename ExpandableLinearLayout
*/

public class JavaExpandableLinearLayout extends LinearLayout implements View.OnClickListener {

private TextView txtViewTip;
/**
* 是否是展开状态,默认是隐藏
*/
private boolean isExpand = false;
private boolean boolHasBottom = false;

private int intDefaultItemCount = 2;
/**
* 待展开显示的文字
*/
private String strExpandText = "显示更多";
/**
* 待隐藏显示的文字
*/
private String strHideText = "收起内容";
private float fontTextSize;
private int intTextColor;

public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}

public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}

public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}
public JavaExpandableLinearLayout(Context context) {
this(context, null);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置垂直方向
setOrientation(VERTICAL);
}

@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableLinearLayout只支持垂直布局");
}
super.setOrientation(orientation);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
justToAddBottom(childCount);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


/**
* 判断是否要添加底部
*/
private void justToAddBottom(int childCount) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true;
//要使用默认底部,并且还没有底部
LinearLayout linearLayoutBottom = new LinearLayout(getContext());
LinearLayout.LayoutParams layoutParamsBottom = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutBottom.setLayoutParams(layoutParamsBottom);
linearLayoutBottom.setGravity(Gravity.CENTER);
txtViewTip = new TextView(getContext());
txtViewTip.setText("展开更多");
txtViewTip.setTextSize(fontTextSize);
txtViewTip.setTextColor(intTextColor);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10);
linearLayoutBottom.addView(txtViewTip);
linearLayoutBottom.setOnClickListener(this);
//添加底部
addView(linearLayoutBottom);
hide();
Log.e("TAG", "justToAddBottom: zou l zhe ");
}
}

/**
* 刷新UI
*/
private void refreshView(View view) {
int childCount = getChildCount();
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount);
}
//大于默认数目的先隐藏
view.setVisibility(GONE);
}
}

/**
* 展开
*/
private void expand() {
for (int i = intDefaultItemCount; i < getChildCount(); i++) {
//从默认显示条目位置以下的都显示出来
View view = getChildAt(i);
view.setVisibility(VISIBLE);
}
}

/**
* 收起
*/
private void hide() {
int endIndex = getChildCount() - 1;
for (int i = intDefaultItemCount; i < endIndex; i++) {
//从默认显示条目位置以下的都隐藏
View view = getChildAt(i);
view.setVisibility(GONE);
}
}

@Override
public void onClick(View v) {
outUseMethodToggle();
}

/**
* 外部也可调用 展开或关闭
*/
public void outUseMethodToggle() {
if (isExpand) {
hide();
txtViewTip.setText(strExpandText);
} else {
expand();
txtViewTip.setText(strHideText);
}
isExpand = !isExpand;
}

/**
* 外部可随时添加子view
*/
public void outUseMethodAddItem(View view) {
int childCount = getChildCount();
//插在底部之前
addView(view, childCount - 1);
refreshView(view);
}
}

Kotlin版


/**
* Created by akitaka on 2022-08-11.
* @author akitaka
* @filename KotlinExpandableLinearLayout
*/
class KotlinExpandableLinearLayout :LinearLayout, View.OnClickListener {
private var txtViewTip: TextView? = null
constructor(context: Context?) :this(context,null)
constructor(context: Context?, attrs: AttributeSet?) :this(context,attrs,0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
//设置垂直方向
orientation = VERTICAL
}
/**
* 是否是展开状态,默认是隐藏
*/
private var isExpand = false

private var intDefaultItemCount = 2
private var boolHasBottom = false

/**
* 待展开显示的文字
*/
private var strExpandText = "显示更多"

/**
* 待隐藏显示的文字
*/
private var strHideText = "收起内容"
private var fontTextSize = 0f
private var intTextColor = 0

fun outUseMethodChangeDefaultItemCount(intDefaultItemCount: Int) {
this.intDefaultItemCount = intDefaultItemCount
}

fun outUseMethodChangeExpandText(strExpandText: String) {
this.strExpandText = strExpandText
}

fun outUseMethodChangeHideText(strHideText: String) {
this.strHideText = strHideText
}

fun outUseMethodChangeExpandHideTextSize(fontTextSize: Float) {
this.fontTextSize = fontTextSize
}

fun outUseMethodChangeExpandHideTextColor(@ColorInt intTextColor: Int) {
this.intTextColor = intTextColor
}

override fun setOrientation(orientation: Int) {
require(HORIZONTAL != orientation) { "ExpandableLinearLayout只支持垂直布局" }
super.setOrientation(orientation)
}


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val childCount = childCount
justToAddBottom(childCount)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}


/**
* 判断是否要添加底部
*/
private fun justToAddBottom(childCount: Int) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true
//要使用默认底部,并且还没有底部
val linearLayoutBottom = LinearLayout(context)
val layoutParamsBottom = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
linearLayoutBottom.layoutParams = layoutParamsBottom
linearLayoutBottom.gravity = Gravity.CENTER
txtViewTip = TextView(context)
txtViewTip!!.text = "展开更多"
txtViewTip!!.textSize = fontTextSize
txtViewTip!!.setTextColor(intTextColor)
val layoutParamsBottomTxt = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
txtViewTip!!.layoutParams = layoutParamsBottomTxt
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10)
linearLayoutBottom.addView(txtViewTip)
linearLayoutBottom.setOnClickListener(this)
//添加底部
addView(linearLayoutBottom)
hide()
}
}

/**
* 刷新UI
*/
private fun refreshView(view: View) {
val childCount = childCount
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount)
}
//大于默认数目的先隐藏
view.setVisibility(GONE)
}
}

/**
* 展开
*/
private fun expand() {
for (i in intDefaultItemCount until childCount) {
//从默认显示条目位置以下的都显示出来
val view: View = getChildAt(i)
view.setVisibility(VISIBLE)
}
}

/**
* 收起
*/
private fun hide() {
val endIndex = childCount - 1
for (i in intDefaultItemCount until endIndex) {
//从默认显示条目位置以下的都隐藏
val view: View = getChildAt(i)
view.setVisibility(GONE)
}
}

override fun onClick(v: View?) {
outUseMethodToggle()
}

/**
* 外部也可调用 展开或关闭
*/
fun outUseMethodToggle() {
if (isExpand) {
hide()
txtViewTip!!.text = strExpandText
} else {
expand()
txtViewTip!!.text = strHideText
}
isExpand = !isExpand
}

/**
* 外部可随时添加子view
*/
fun outUseMethodAddItem(view: View) {
val childCount = childCount
//插在底部之前
addView(view, childCount - 1)
refreshView(view)
}
}


📢作者:小空和小芝中的小空


作者:芝麻粒儿
链接:https://juejin.cn/post/7130639529123250213
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

JS令人头疼的类型转换

web
前言 JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。 数据类型 基本数据类型 Number(数字) S...
继续阅读 »

前言


JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。


数据类型


基本数据类型



  • Number(数字)

  • String(字符串)

  • Boolean(布尔)

  • Null

  • Undefined

  • Symbol(ES6)


引用数据类型



  • object{}

  • array[]

  • function(){}

  • date()


由于JS中拥有动态类型,在定义的时候不用指定数据类型,赋值的时候可以将任意类型赋给同一个变量,例如:let a = 1; a = '1'


类型转换


什么是类型转换?


简单来说就是将值从一种数据类型转换为另一种数据类型的过程。


分为哪几种?


根据转换的特点分为:显式类型转换(强制转换)和隐式类型转换(自动转换)。


显示类型转换(强制转换)


通过Boolean()——原始值转布尔,Number()——原始值转数字,String()——原始值转字符来进行强制类型转换。这里的转换规则可以直接查看Js官方文档:Annotated ES5


1.png


2.png


我们从文档中可以知道当我们想进行强制类型转换时,js会自动会帮我们使用ToString(value),ToNumber(value)进行转换。


//原始值转布尔
console.log(Boolean('123'));
console.log(Boolean(123));
console.log(Boolean(null));
console.log(Boolean(undefined));
console.log(Boolean(true));

//原始值转数字
console.log(Number('123'));
console.log(Number(123));
console.log(Number(null));
console.log(Number(undefined));
console.log(Number(true));

//原始值转字符串
console.log(String('123'));
console.log(String(123));
console.log(String(null));
console.log(String(undefined));
console.log(String(true));

结果为:


3.png


对象转字符串,数字


通过调用特殊的对象转换方法来完成,在js中有两个方法来执行转换,这两个方法所有的对象都具备,就是用来把对象转换为原始值的。这两个方法分别为toString(),valueOf(),这两个方法对象的构造函数原型上就有,其目的就是要有办法把对象转换为原始类型。


对象转字符串


toString()方法除了Null和Undefined其他的数据类型都具有此方法。通常情况下toString()和String()效果一样。



4.png


我们在文档中重点关注对象转字符串,上图中对象转字符串有两个步骤,先是执行自带的ToPrimitive(obj,String),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的toString方法,如果能得到一个原始类型,则返回


3.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


4.报错


对象转数字

对象转数字的话也同样是有两个步骤:先是执行自带的ToPrimitive(obj,Number),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


3.调用对象自带的toString方法,如果能得到一个原始类型,则返回


4.报错


隐式类型转换



  • 当 + 运算作为一元操作符时,会自动调用ToNumber()处理该值。(相当于Number())


例如:console.log(+'123');结果为数字123。
console.log(+[]);结果为0,因为对象[]转换为了0。



  • 当 + 运算作为二元操作符,例(a + b)


1.lprim = ToPrimitive(v1)


2.rprim = ToPrimitive(v2)


3.如果lprim是字符串或者rprim是字符串,则返回ToString(lprim)和ToStringrprim()的拼接结果


4.返回ToNumber(lprim) + ToNumber(rprim)


结语


js类型转换规则,相当于历史事件,是已经规定好的,弄清楚它,能更好地和面试官侃侃而谈。最后感谢各位的观看。


作者:一拾九
来源:juejin.cn/post/7224518612161593402
收起阅读 »

节流 你会手写吗?

web
节流 在各大面试题中,频繁出现的老油条,节流。 啥叫节流呢? 节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。 在间隔一段时间执行一次回调的场景有: 1...
继续阅读 »

节流


在各大面试题中,频繁出现的老油条,节流。


啥叫节流呢?


节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。


在间隔一段时间执行一次回调的场景有:


1.滚动加载,加载更多或滚到底部监听

2.搜索框,搜索联想功能

简单来说就是,一段时间内重复触发,按一定频率(1s、3s、5s)执行,可配置一开始就执行一次。


如果还不懂,就直接上我们的例子。我们可以看到当我们滑动屏幕的时候,会频繁运行打印这个函数。


image.png
当我们进行节流后,给它设置一个时间,那么他就只会在该时间后


image.png


上代码


其中fn代表将要运行的函数,delay代表函数触发的时间间隔。


整个代码思路,
timer=null,
flag=false, 默认刚开始不运行
设置一个定时器,
等到delay时间到了,就会开始运行这个函数fn。如果在delay之前,发生了滚动等事件,因为已经
flag = true,只会return 不会运行这个函数fn。只有等带delay到了时间,才会运行函数。


定时器实现的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。


js
let count = 0;
function throttle(fn, delay) {
let timer = null // 把变量放函数里面,避免全局污染
let flag = false
let that = this
return function () {
if (flag) return
flag = true
let arg = arguments // 此处为fn函数的参数
timer = setTimeout(() => {
fn.apply(that, arg)
flag = false
}, delay)
}
}
function test(a, b) {
console.log(a, b)
}
let throttleTest = throttle(test, 1000)
// 测试函数
function test1() {
console.log('普通test:', count++)
}

window.addEventListener('scroll', (e) => {
// test1() // 不用节流的话,页面滚动一直在调用
throttleTest(1, 2) // 加上节流,即使页面一直滚动,也只会每隔一秒执行一次test函数
})

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7222984001769488443
收起阅读 »

上传的图片怎么满足我们的审美的呢?开始玩弄css的一些 特别属性 object-fit

web
今天开始玩弄css的一些比较冷门但是可能比较实用的属性 object-fit。 首先让我们先看看一张效果图 这两张自拍,你个人觉得哪张比较好看,不用想都知道第一张好看啦,我们肯定希望我们上传的图片都是以第一种图片当头像啊,而不是第二种扁扁的。那么这样的效果是怎...
继续阅读 »

今天开始玩弄css的一些比较冷门但是可能比较实用的属性 object-fit。


首先让我们先看看一张效果图


这两张自拍,你个人觉得哪张比较好看,不用想都知道第一张好看啦,我们肯定希望我们上传的图片都是以第一种图片当头像啊,而不是第二种扁扁的。那么这样的效果是怎么实现的呢?
image.png


诀窍 object-fit


相信大多数人都对这个属性比较陌生吧。没咋看过这个属性吧!它有啥用呢?


我来给大家介绍介绍这个属性。
object-fit是一个CSS属性,用于控制图片或视频等替换元素的尺寸和位置,以使其适合其容器。


默认情况下,替换元素的大小取决于其本身的大小,而不是其容器的大小。这可能会导致替换元素与其容器不匹配,或者在缩放容器时无法应用正确的比例。


使用object-fit属性,可以指定替换元素如何调整其大小以适应其容器。它有以下几个值:



  • fill:默认值,替换元素会拉伸以填充容器,可能会导致元素的宽高比发生变化。

  • contain:替换元素会缩放以适应容器,保持其宽高比,可能会留有空白区域。

  • cover:替换元素会缩放以填充容器,保持其宽高比,可能会被裁剪。

  • none:替换元素将保持其本来的尺寸和宽高比,可能会溢出容器。

  • scale-down:替换元素会根据容器的大小进行缩放,但不会超过其原始大小,可能会留有空白区域。


看完还是好晕,不如直接看代码和效果图



注释解释了代码中每个部分的作用:



  • object-fit: cover将图像填充到容器中,保持比例不变。

  • border-radius: 50%将图像的四个角设置为圆角,使其呈现圆形。

  • width: 340pxheight: 340px设置图像的宽度和高度。

  • border: 1px solid #ccc设置图像周围的边框。


容器1和容器2具有相同的样式,但容器1使用了object-fit属性,而容器2没有。这样,我们可以比较两者之间的区别,看看object-fit如何影响图像的呈现方式。


html
<!DOCTYPE html>
<html>

<head>
<title>object-fit示例</title>
<style>
/* 容器1样式 */
.container1 img {
object-fit: cover; /* 图像填充容器,保持比例不变 */
border-radius: 50%; /* 圆角 */
width: 340px;
height: 340px;
border: 1px solid #ccc;
}

/* 容器2样式 */
.container2 img {
border-radius: 50%; /* 圆角 */
width: 340px;
height: 340px;
border: 1px solid #ccc;
}
</style>
</head>

<body>
<h2>自拍照 object-fit</h2>
<!-- 容器1 -->
<div class="container1">
<img src="https://ts1.cn.mm.bing.net/th/id/R-C.e62783996335efecfb15e445205cc5f6?rik=2Z0xpGAe3tn1kQ&riu=http%3a%2f%2fwww.xgsy188.com%2fuploadfile%2f20131151461663843.jpg&ehk=rKGrd9FbAQUFWicdL8Omt%2bFaMw%2f09v2obcuVTAWca4w%3d&risl=&pid=ImgRaw&r=0" alt="自拍照">
</div>

<h2>自拍照2</h2>
<!-- 容器2 -->
<div class="container2">
<img src="https://ts1.cn.mm.bing.net/th/id/R-C.e62783996335efecfb15e445205cc5f6?rik=2Z0xpGAe3tn1kQ&riu=http%3a%2f%2fwww.xgsy188.com%2fuploadfile%2f20131151461663843.jpg&ehk=rKGrd9FbAQUFWicdL8Omt%2bFaMw%2f09v2obcuVTAWca4w%3d&risl=&pid=ImgRaw&r=0" alt="自拍照">
</div>
</body>

</html>


作者:Mr-Wang-Y-P
来源:juejin.cn/post/7223767530981326885
收起阅读 »

这些数组reduce的妙用,你都会吗?

web
reduce 是 JavaScript 数组对象上的一个高阶函数,它可以用来迭代数组中的所有元素,并返回一个单一的值。其常用的语法为: array.reduce(callback[, initialValue]) 其中,callback 是一个回调函数,它接...
继续阅读 »

reduce 是 JavaScript 数组对象上的一个高阶函数,它可以用来迭代数组中的所有元素,并返回一个单一的值。其常用的语法为:
array.reduce(callback[, initialValue])



其中,callback 是一个回调函数,它接受四个参数:累加器(初始值或上一次回调函数的返回值)、当前元素、当前索引、操作的数组本身。initialValue 是一个可选的初始值,如果提供了该值,则作为累加器的初始值,否则累加器的初始值为数组的第一个元素。
reduce 函数会从数组的第一个元素开始,依次对数组中的每个元素执行回调函数。回调函数的返回值将成为下一次回调函数的第一个参数(累加器)。最后,reduce 函数返回最终的累加结果。
以下是一个简单的 reduce
示例,用于计算数组中所有元素的和:


const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(sum); // 15

在上面的代码中,reduce 函数从数组的第一个元素开始,计算累加值,返回最终的累加结果 15。
除了数组的求和,reduce 函数还可以用于其他各种用途,如数组求平均数、最大值、最小值等。此外,reduce 函数还可以与 map、filter、forEach 等函数组合使用,实现更加复杂的数据操作。


当然,以下是一些 reduce 的实际应用案例,帮助你更好地理解它的用法:


计算数组的平均数


const arr = [1, 2, 3, 4, 5];
const average = arr.reduce((accumulator, currentValue, index, array) => {
accumulator += currentValue;
if (index === array.length - 1) {
return accumulator / array.length;
} else {
return accumulator;
}
});
console.log(average); // 3

求数组的最大值


const arr = [1, 2, 3, 4, 5];
const max = arr.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));
console.log(max); // 5

求数组的最小值


const arr = [1, 2, 3, 4, 5];
const min = arr.reduce((accumulator, currentValue) => Math.min(accumulator, currentValue));
console.log(min); // 1

数组去重


const arr = [1, 2, 3, 3, 4, 4, 5];
const uniqueArr = arr.reduce((accumulator, currentValue) => {
if (!accumulator.includes(currentValue)) {
accumulator.push(currentValue);
}
return accumulator;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

计算数组中每个元素出现的次数


const arr = [1, 2, 3, 3, 4, 4, 5];
const countMap = arr.reduce((accumulator, currentValue) => {
if (!accumulator[currentValue]) {
accumulator[currentValue] = 1;
} else {
accumulator[currentValue]++;
}
return accumulator;
}, {});
console.log(countMap); // {1: 1, 2: 1, 3: 2, 4: 2, 5: 1}

实现数组分组


const arr = [1, 2, 3, 4, 5];
const result = arr.reduce((accumulator, currentValue) => {
if (currentValue % 2 === 0) {
accumulator.even.push(currentValue);
} else {
accumulator.odd.push(currentValue);
}
return accumulator;
}, { even: [], odd: [] });
console.log(result); // {even: [2, 4], odd: [1, 3, 5]}

计算数组中连续递增数字的长度


const arr = [1, 2, 3, 5, 6, 7, 8, 9];
const result = arr.reduce((accumulator, currentValue, index, array) => {
if (index === 0 || currentValue !== array[index - 1] + 1) {
accumulator.push([currentValue]);
} else {
accumulator[accumulator.length - 1].push(currentValue);
}
return accumulator;
}, []);
const maxLength = result.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue.length), 0);
console.log(maxLength); // 5

计算对象数组的属性总和


const arr = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const result = arr.reduce((accumulator, currentValue) => accumulator + currentValue.age, 0);
console.log(result); // 90

将对象数组转换为键值对对象


const arr = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const result = arr.reduce((accumulator, currentValue) => {
accumulator[currentValue.name] = currentValue.age;
return accumulator;
}, {});
console.log(result); // {Alice: 25, Bob: 30, Charlie: 35}

计算数组中出现次数最多的元素


const arr = [1, 2, 3, 4, 4, 4, 5, 5, 6, 6, 6, 6];
const result = arr.reduce((accumulator, currentValue) => {
accumulator[currentValue] = (accumulator[currentValue] || 0) + 1;
return accumulator;
}, {});
const maxCount = Math.max(...Object.values(result));
const mostFrequent = Object.keys(result).filter(key => result[key] === maxCount).map(Number);
console.log(mostFrequent); // [6]

实现 Promise 串行执行


const promise1 = () => Promise.resolve('one');
const promise2 = (input) => Promise.resolve(input + ' two');
const promise3 = (input) => Promise.resolve(input + ' three');

const promises = [promise1, promise2, promise3];
const result = promises.reduce((accumulator, currentValue) => {
return accumulator.then(currentValue);
}, Promise.resolve('start'));
result.then(console.log); // 'one two three'

对象属性值求和


const obj = {
a: 1,
b: 2,
c: 3
};
const result = Object.values(obj).reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(result); // 6

按属性对数组分组


const arr = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Mary' },
{ id: 3, name: 'Bob' },
{ id: 4, name: 'Mary' }
];
const result = arr.reduce((accumulator, currentValue) => {
const key = currentValue.name;
if (!accumulator[key]) {
accumulator[key] = [];
}
accumulator[key].push(currentValue);
return accumulator;
}, {});
console.log(result);
/*
{
John: [{ id: 1, name: 'John' }],
Mary: [
{ id: 2, name: 'Mary' },
{ id: 4, name: 'Mary' }
]
,
Bob: [{ id: 3, name: 'Bob' }]
}
*/

扁平化数组


// 如果你有一个嵌套的数组,可以使用reduce将其扁平化成一个一维数组。例如:
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flattenedArray = nestedArray.reduce((acc, curr) => acc.concat(curr), []);
console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]

合并对象


// 可以使用reduce将多个对象合并成一个对象。例如:
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };
const mergedObj = [obj1, obj2, obj3].reduce((acc, curr) => Object.assign(acc, curr), {});
console.log(mergedObj); // {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}
复制代码
作者:思学堂
来源:juejin.cn/post/7223278436893163581
>
收起阅读 »

都这么多年了,作为一个前端的你是不是连Symbol都不会用

web
Symbol是JavaScript中的原始数据类型之一,它表示一个唯一的、不可变的值,通常用作对象属性的键值。由于Symbol值是唯一的,因此可以防止对象属性被意外地覆盖或修改。以下是Symbol的方法和属性整理: 属性 Symbol.length Symbo...
继续阅读 »

Symbol是JavaScript中的原始数据类型之一,它表示一个唯一的、不可变的值,通常用作对象属性的键值。由于Symbol值是唯一的,因此可以防止对象属性被意外地覆盖或修改。以下是Symbol的方法和属性整理:


属性


Symbol.length


Symbol构造函数的length属性值为0。


示例代码:


console.log(Symbol.length); // 0

方法


Symbol.for()


Symbol.for()方法会根据给定的字符串key,返回一个已经存在的symbol值。如果不存在,则会创建一个新的Symbol值并将其注册到全局Symbol注册表中。


示例代码:


const symbol1 = Symbol.for('foo');
const symbol2 = Symbol.for('foo');

console.log(symbol1 === symbol2); // true

使用场景: 当我们需要使用一个全局唯一的Symbol值时,可以使用Symbol.for()方法来获取或创建该值。例如,在多个模块之间共享某个Symbol值时,我们可以使用Symbol.for()来确保获取到的Symbol值是唯一的。


Symbol.keyFor()


Symbol.keyFor()方法会返回一个已经存在的Symbol值的key。如果给定的Symbol值不存在于全局Symbol注册表中,则返回undefined。


示例代码:


const symbol1 = Symbol.for('foo');
const key1 = Symbol.keyFor(symbol1);

const symbol2 = Symbol('bar');
const key2 = Symbol.keyFor(symbol2);

console.log(key1); // 'foo'
console.log(key2); // undefined

使用场景: 当我们需要获取一个全局唯一的Symbol值的key时,可以使用Symbol.keyFor()方法。但需要注意的是,只有在该Symbol值被注册到全局Symbol注册表中时,才能使用Symbol.keyFor()方法获取到其key。


Symbol()


Symbol()函数会返回一个新的、唯一的Symbol值。可以使用可选参数description来为Symbol值添加一个描述信息。


示例代码:


const symbol1 = Symbol('foo');
const symbol2 = Symbol('foo');

console.log(symbol1 === symbol2); // false

使用场景: 当我们需要使用一个唯一的Symbol值时,可以使用Symbol()函数来创建该值。通常情况下,我们会将Symbol值用作对象属性的键值,以确保该属性不会被意外地覆盖或修改。


Symbol.prototype.toString()


Symbol.prototype.toString()方法会返回Symbol值的字符串表示形式,该表示形式包含Symbol()函数创建时指定的描述信息。


示例代码:


const symbol = Symbol('foo');

console.log(symbol.toString()); // 'Symbol(foo)'

使用场景: 当我们需要将一个Symbol值转换成字符串时,可以使用Symbol.prototype.toString()方法。


Symbol.prototype.valueOf()


Symbol.prototype.valueOf()方法会返回Symbol值本身。


示例代码:


const symbol = Symbol('foo');

console.log(symbol.valueOf()); // Symbol(foo)

使用场景: 当我们需要获取一个Symbol值本身时,可以使用Symbol.prototype.valueOf()方法。


Symbol.iterator


Symbol.iterator是一个预定义好的Symbol值,表示对象的默认迭代器方法。该方法返回一个迭代器对象,可以用于遍历该对象的所有可遍历属性。


示例代码:


const obj = { a: 1, b: 2 };

for (const key of Object.keys(obj)) {
console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertyNames(obj)) {
console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertySymbols(obj)) {
console.log(key);
}
// Output:
// No output

obj[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield key;
}
}

for (const key of obj) {
console.log(key);
}
// Output:
// 'a'
// 'b'

使用场景: 当我们需要自定义一个对象的迭代行为时,可以通过定义Symbol.iterator属性来实现。例如,对于自定义的数据结构,我们可以定义它的Symbol.iterator方法以便能够使用for...of语句进行遍历。


Symbol.hasInstance


Symbol.hasInstance是一个预定义好的Symbol值,用于定义对象的 instanceof 操作符行为。当一个对象的原型链中存在Symbol.hasInstance方法时,该对象可以被instanceof运算符使用。


示例代码:


class Foo {
static [Symbol.hasInstance](obj) {
return obj instanceof Array;
}
}

console.log([] instanceof Foo); // true
console.log({} instanceof Foo); // false

使用场景: 当我们需要自定义一个对象的 instanceof 行为时,可以通过定义Symbol.hasInstance方法来实现。


Symbol.isConcatSpreadable


Symbol.isConcatSpreadable是一个预定义好的Symbol值,用于定义对象在使用concat()方法时的展开行为。如果一个对象的Symbol.isConcatSpreadable属性为false,则在调用concat()方法时,该对象不会被展开。


示例代码:


const arr1 = [1, 2];
const arr2 = [3, 4];
const obj = { length: 2, 0: 5, 1: 6, [Symbol.isConcatSpreadable]: false };

console.log(arr1.concat(arr2)); // [1, 2, 3, 4]
console.log(arr1.concat(obj)); // [1, 2, { length: 2, 0: 5, 1: 6, [Symbol(Symbol.isConcatSpreadable)]: false }]

使用场景: 当我们需要自定义一个对象在使用concat()方法时的展开行为时,可以通过定义Symbol.isConcatSpreadable属性来实现。


Symbol.toPrimitive


Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则在将该对象转换为原始值时,会调用该方法。


示例代码:


const obj = {
valueOf() {
return 1;
},
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 2;
} else if (hint === 'string') {
return 'foo';
} else {
return 'default';
}
}
};

console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'


使用场景: 当我们需要自定义一个对象在被强制类型转换时的行为时,可以通过定义Symbol.toPrimitive方法来实现。


Symbol.toStringTag


Symbol.toStringTag是一个预定义好的Symbol值,用于定义对象在调用Object.prototype.toString()方法时返回的字符串。如果一个对象定义了Symbol.toStringTag属性,则在调用该对象的toString()方法时,会返回该属性对应的字符串。


示例代码:


class Foo {
get [Symbol.toStringTag]() {
return 'Bar';
}
}

console.log(Object.prototype.toString.call(new Foo())); // '[object Bar]'

使用场景: 当我们需要自定义一个对象在调用Object.prototype.toString()方法时返回的字符串时,可以通过定义Symbol.toStringTag属性来实现。


Symbol.species


Symbol.species是一个预定义好的Symbol值,用于定义派生对象的构造函数。如果一个对象定义了Symbol.species属性,则在调用该对象的派生方法(如Array.prototype.map())时,返回的新对象会使用该属性指定的构造函数。


示例代码:


class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}

const myArr = new MyArray(1, 2, 3);
const arr = myArr.map(x => x * 2);

console.log(arr instanceof MyArray); // false
console.log(arr instanceof Array); // true

使用场景: 当我们需要自定义一个派生对象的构造函数时,可以通过定义Symbol.species属性来实现。


Symbol.match


Symbol.match是一个预定义好的Symbol值,用于定义对象在调用String.prototype.match()方法时的行为。如果一个对象定义了Symbol.match方法,则在调用该对象的match()方法时,会调用该方法进行匹配。


示例代码:


class Foo {
[Symbol.match](str) {
return str.indexOf('foo') !== -1;
}
}

console.log('foobar'.match(new Foo())); // true
console.log('barbaz'.match(new Foo())); // false

使用场景: 当我们需要自定义一个对象在调用String.prototype.match()方法时的行为时,可以通过定义Symbol.match方法来实现。


Symbol.replace


Symbol.replace是一个预定义好的Symbol值,用于定义对象在调用String.prototype.replace()方法时的行为。如果一个对象定义了Symbol.replace方法,则在调用该对象的replace()方法时,会调用该方法进行替换。


示例代码:


class Foo {
[Symbol.replace](str, replacement) {
return str.replace('foo', replacement);
}
}

console.log('foobar'.replace(new Foo(), 'baz')); // 'bazbar'
console.log('barbaz'.replace(new Foo(), 'baz')); // 'barbaz'

使用场景: 当我们需要自定义一个对象在调用String.prototype.replace()方法时的行为时,可以通过定义Symbol.replace方法来实现。


Symbol.search


Symbol.search是一个预定义好的Symbol值,用于定义对象在调用String.prototype.search()方法时的行为。如果一个对象定义了Symbol.search


class Foo {
[Symbol.search](str) {
return str.indexOf('foo');
}
}

console.log('foobar'.search(new Foo())); // 0
console.log('barbaz'.search(new Foo())); // -1


使用场景: 当我们需要自定义一个对象在调用String.prototype.search()方法时的行为时,可以通过定义Symbol.search方法来实现。


Symbol.split


Symbol.split是一个预定义好的Symbol值,用于定义对象在调用String.prototype.split()方法时的行为。如果一个对象定义了Symbol.split方法,则在调用该对象的split()方法时,会调用该方法进行分割。


示例代码:


class Foo {
[Symbol.split](str) {
return str.split(' ');
}
}

console.log('foo bar baz'.split(new Foo())); // ['foo', 'bar', 'baz']
console.log('foobarbaz'.split(new Foo())); // ['foobarbaz']

使用场景: 当我们需要自定义一个对象在调用String.prototype.split()方法时的行为时,可以通过定义Symbol.split方法来实现。


Symbol.iterator


Symbol.iterator是一个预定义好的Symbol值,用于定义对象在被遍历时的行为。如果一个对象定义了Symbol.iterator方法,则可以使用for...of循环、扩展运算符等方式来遍历该对象。


示例代码:


class Foo {
constructor() {
this.items = ['foo', 'bar', 'baz'];
}

*[Symbol.iterator]() {
for (const item of this.items) {
yield item;
}
}
}

const foo = new Foo();

for (const item of foo) {
console.log(item);
}

// 'foo'
// 'bar'
// 'baz'

使用场景: 当我们需要自定义一个对象在被遍历时的行为时,可以通过定义Symbol.iterator方法来实现。比如,我们可以通过实现Symbol.iterator方法来支持自定义数据结构的遍历。


Symbol.toPrimitive


Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则可以通过调用该方法来进行强制类型转换。


示例代码:


const obj = {
valueOf() {
return 1;
},
[Symbol.toPrimitive](hint) {
if (hint === 'default') {
return 'default';
} else if (hint === 'number') {
return 2;
} else {
return 'foo';
}
}
};

console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'

使用场景: 当我们需要自定义一个对象在被强制类型转换时的行为时,可以通过定义Symbol.toPrimitive方法来实现。


Symbol.toStringTag


Symbol.toStringTag是一个预定义好的Symbol值,用于定义对象在调用Object.prototype.toString()方法时返回的字符串。如果一个对象定义了Symbol.toStringTag属性,则在调用该对象的toString()方法时,会返回该属性对应的字符串。


示例代码:


class Foo {
get [Symbol.toStringTag]() {
return 'Bar';
}
}

console.log(Object.prototype.toString.call(new Foo())); // '[object Bar]'

使用场景: 当我们需要自定义一个对象在调用Object.prototype.toString()方法时返回的字符串时,可以通过定义Symbol.toStringTag属性来实现。这样做有助于我们更清晰地表示对象的类型。


Symbol.unscopables


Symbol.unscopables是一个预定义好的Symbol值,用于定义对象在使用with语句时的行为。如果一个对象定义了Symbol.unscopables属性,则在使用with语句时,该对象的指定属性将不会被绑定到with语句的环境中。


示例代码:


const obj = {
a: 1,
b: 2,
c: 3,
[Symbol.unscopables]: {
c: true
}
};

with (obj) {
console.log(a); // 1
console.log(b); // 2
console.log(c); // ReferenceError: c is not defined
}

使用场景: 由于with语句会带来一些安全性问题和性能问题,因此在实际开发中不建议使用。但是,如果确实需要使用with语句,可以通过定义Symbol.unscopables属性来避免某些属性被误绑定到with语句的环境中。


Symbol.hasInstance


Symbol.hasInstance是一个预定义好的Symbol值,用于定义对象在调用instanceof运算符时的行为。如果一个对象定义了Symbol.hasInstance方法,则在调用该对象的instanceof运算符时,会调用该方法来判断目标对象是否为该对象的实例。


示例代码:


class Foo {
static [Symbol.hasInstance](obj) {
return Array.isArray(obj);
}
}

console.log([] instanceof Foo); // true
console.log({} instanceof Foo); // false

使用场景: 当我们需要自定义一个对象在调用instanceof运算符时的行为时,可以通过定义Symbol.hasInstance方法来实现。比如,我们可以通过实现Symbol.hasInstance方法来支持自定义数据类型的判断。


总结


Symbol是ES6中新增的一种基本数据类型,用于表示独一无二的值。Symbol值在语言层面上解决了属性名冲突的问题,可以作为对象的属性名使用,并且不会被意外覆盖。除此之外,Symbol还具有以下特点:



  • Symbol值是唯一的,每个Symbol值都是独一无二的,即使是通过相同的描述字符串创建的Symbol值,也不会相等;

  • Symbol值可以作为对象的属性名使用,并且不会被意外覆盖;

  • Symbol值可以作为私有属性来使用,因为无法通过对象外部访问对象中的Symbol属性;

  • Symbol值可以被用作常量,因为它们是唯一的;

  • Symbol值可以用于定义迭代器、类型转换规则、私有属性、元编程等高级功能。


在使用Symbol时需要注意以下几点:



  • Symbol值不能使用new运算符创建;

  • Symbol值可以通过描述字符串来创建,但是描述字符串并不是Symbol值的唯一标识符;

  • Symbol属性在使用时需要用[]来访问,不能使用.运算符;

  • 同一对象中的多个Symbol属性是独立的,它们之间不会互相影响。


总之,Symbol是一个非常有用的数据类型,在JavaScript中具有非常广泛的应用。使用Symbol可以有效地避免属性名冲突问题,并且可以为对象提供一些高级功能。熟练掌握Symbol,有助于我们写出更加健壮、高效和可维护的Jav

作者:布衣1983
来源:juejin.cn/post/7226193000496463928
aScript代码。

收起阅读 »

实现tabs圆角及反圆角效果

web
直接上最终效果 基本页面结构 <div class="tab-list"> <div v-for="tab in tabList" :key="tab.id" ...
继续阅读 »

直接上最终效果


image.png


image.png


基本页面结构


      <div class="tab-list">
<div
v-for="tab in tabList"
:key="tab.id"
class="tab-item"
:class="activeTab === tab.id ? 'tab-selected' : ''"
@click="onTab(tab.id)"
>

<image :src="tab.icon" class="tab-icon" />
<div>{{ tab.label }}</div>
</div>
</div>

  $tab-height: 52px;
$tab-bgcolor: #e2e8f8

.tab-list {
display: flex;
border-radius: 12px 12px 0 0;
background-color: $tab-bgcolor;

.tab-item {
flex: 1;
height: $tab-height;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
opacity: 0.65;
color: $primary-color;
font-weight: 600;
position: relative;
}

.tab-icon {
width: 17px;
height: 17px;
margin-right: 4px;
}

.tab-selected {
opacity: 1;
background: #ffffff;
}

}


image.png


image.png


添加上半两个圆角


这个很简单


    .tab-selected {
opacity: 1;
background: #ffffff;
// 新增
border-radius: 12px 12px 0 0;
}

image.png


添加下半两个反圆角


加两个辅助的伪元素


    .tab-selected::before {
content: '';
position: absolute;
left: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: red;
border-radius: 0 0 12px 0;
}
.tab-selected::after {
content: '';
position: absolute;
right: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: red;
border-radius: 0 0 0 12px;
}

image.png


image.png


再添加box-shadow


    .tab-selected {
opacity: 1;
background: #ffffff;
border-radius: 12px 12px 0 0;
// 新装置
box-shadow: 12px 12px 0 0 blue, -12px 12px 0 0 blue;
}

image.png


image.png


到这个就差不多可以收尾了,把伪元素的背景色改为tabs的背景色


    .tab-selected::before {
content: '';
position: absolute;
left: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: #e2e8f8; // 修改
border-radius: 0 0 12px 0;
}
.tab-selected::after {
content: '';
position: absolute;
right: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: #e2e8f8; // 修改
border-radius: 0 0 0 12px;
}

image.png


再处理下box-shadow


    .tab-selected {
opacity: 1;
background: #ffffff;
border-radius: 12px 12px 0 0;
// box-shadow: 12px 12px 0 0 blue, -12px 12px 0 0 blue;
box-shadow: 12px 12px 0 0 #ffffff, -12px 12px 0 0 #ffffff;
}

完美


image.png


但是两边的还会有问题


image.png


image.png


父级元素overflow:hidden即可


.tab-list {
display: flex;
position: relative;
z-index: 2;
border-radius: 12px 12px 0 0;
background-color: #e2e8f8;
overflow: hidden; // 新增
}

收工


参考



CSS3 实现双圆角 Tab 菜单



相关知识点回顾


box-shadow




  1. x轴偏移 右为正

  2. y轴偏移 下为正

  3. 模糊半径

  4. 阴影大小

  5. 颜色

  6. 位置 inset



border-radius


先记得下面这个图


image.png



  • 一个值的时候设置1/2/3/4

  • 两个值的时候设置 1/32/4

  • 三个值的时候设置12/4, 3

  • 四个值就简单了1234


border-radius 如果需要设置某个角的圆角边框,可以使用下面四个



  1. border-top-left-radius;

  2. border-top-right-radius;

  3. border-bottom-left-radius;

  4. border-bottom-right-radius;


又要画一个图了,上面四个属性,又可以设置一个值或者两个值


第一个值是水平半径,第二个是垂直半径。如果省略第二个值,它是从第一个复制


image.png


image.png


当然border-radius也可以分别设置水平半径 垂直半径



border-radius: 10px / 20px 30px 40px 50px; 水平半径都为10px, 但四个角的垂直半径分别设置



image.png



border-radius: 50px 10px / 20px;



image.png


下期预告


曲线圆角tabs


image.png


传送门


作者:feng_cc
来源:juejin.cn/post/7224311569777934392
收起阅读 »

用Flutter写一个单例

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码: class MySingleton { // 静态变量 static final MySingleton _singleton = MySingle...
继续阅读 »

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码:


class MySingleton {
// 静态变量
static final MySingleton _singleton = MySingleton._internal();

// 工厂方法
factory MySingleton() {
return _singleton;
}

// 私有构造函数
MySingleton._internal();

// 其他方法
void doSomething() {
print("Doing something...");
}
}

在上面的代码中,MySingleton类有一个私有的构造函数,这意味着它不能直接实例化。


相反,它使用一个静态变量 _singleton 来存储唯一的实例,并使用一个工厂方法来获取该实例。因此,当您需要引用该单例时,您只需调用 MySingleton() 方法,就可以得到唯一的实例。


要使用该单例,只需调用 MySingleton() 方法,并调用其公共方法,如 doSomething()


MySingleton mySingleton = MySingleton();
mySingleton.doSomething();

Flutter单例模式可以在以下场景中使用:



  1. 网络请求:在网络请求过程中,您可能只需要一个单例来管理所有的HTTP客户端和连接。使用单例模式可以确保只有一个实例在整个应用程序中被创建和使用,这样可以节约系统资源并避免重复创建相同的实例。

  2. 数据库操作:在应用程序中,您可能需要与数据库进行交互。使用单例模式,您可以确保只需要一个单例来管理数据库连接并执行所有数据库操作。

  3. 状态管理:在Flutter中,您可以使用单例模式来管理应用程序状态。您可以创建一个具有全局作用域的单例,以存储和管理应用程序中的状态,并确保在整个应用程序中只有一个实例在使用。

  4. 全局管理:在某些情况下,您可能需要在整个应用程序中共享某些对象或数据。使用单例模式,您可以创建一个具有全局作用域的单例来存储这些对象和数据,并确保在整个应用程序中只有一个实例在使用。 在这些场景中,使用单例模式可以简化代码并提高应用程序性能,避免了创建多个重复的对象的开销。

作者:早起的年轻人
链接:https://juejin.cn/post/7230416597012463677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter list 数组排序

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。 sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。 以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序: List<...
继续阅读 »

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。
sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。
以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序:


List<int> numbers = [1, 3, 2, 5, 4];
// 升序排序
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // 输出:[1, 2, 3, 4, 5]

// 降序排序
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 输出:[5, 4, 3, 2, 1]

在上述代码中,我们使用了sort()方法将数字列表按照升序和降序进行了排序。


在比较函数中,我们使用了 compareTo() 方法来比较两个数字对象。


如果想按照其他字段进行排序,只需将比较函数中的a和b替换为您想要排序的字段即可。




以下是示例代码,假设您有一个包含Person对象的列表,可以按照Person的年龄字段进行排序:


class Person {
String name;
int age;

Person({this.name, this.age});
}

List<Person> persons = [
Person(name: "John", age: 30),
Person(name: "Jane", age: 20),
Person(name: "Bob", age: 25),
];

// 按照年龄字段进行排序
persons.sort((a, b) => a.age.compareTo(b.age));

// 输出排序后的列表
print(persons);

在上述代码中,我们使用了sort()函数将Person对象列表按照年龄字段进行排序。
在该示例中,我们使用了compareTo()函数来比较Person对象的年龄字段,并按照升序排序。


作者:早起的年轻人
链接:https://juejin.cn/post/7230420475494137913
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

No Focused Window ANR是怎样产生的

ANR
之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢? 由之前的文...
继续阅读 »

之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢?


由之前的文章,我们知道,点击事件都是由InputDispatcher来分发的,我们直接来看InputDispatcher的源码。


No Focused Window ANR如何产生


如果是Key事件,或Motion事件,都需要找到焦点窗口取处理,都会调用到findFocusedWindowTargetsLocked()。


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
const EventEntry& entry,
std::vector<InputTarget>& inputTargets,
nsecs_t* nextWakeupTime) {
std::string reason;

int32_t displayId = getTargetDisplayId(entry);
// mFocusedWindowHandlesByDisplay在setInputWindowsLocked()里赋值
sp<InputWindowHandle> focusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
// mFocusedApplicationHandlesByDisplay在setFocusedApplication()里赋值
sp<InputApplicationHandle> focusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);

// focusedWindowHandle和focusedApplicationHandle都为空时表示当前无窗口,该事件会被丢弃,不会执行dispatchEventLocked
// 一般出现两个都为空的场景,是在窗口切换的过程,此时不处理事件注入
if (focusedWindowHandle == nullptr && focusedApplicationHandle == nullptr) {
return INPUT_EVENT_INJECTION_FAILED;
}

// focusedWindowHandle为空但focusedApplicationHandle不为空时开始ANR检查
if (focusedWindowHandle == nullptr && focusedApplicationHandle != nullptr) {
// 默认mNoFocusedWindowTimeoutTime没有值,第一次检查ANR会走下面这个流程
if (!mNoFocusedWindowTimeoutTime.has_value()) {
// DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5s * HwTimeoutMultiplier();
// 默认input dispatch timeout时间时5s
const nsecs_t timeout = focusedApplicationHandle->getDispatchingTimeout(
DEFAULT_INPUT_DISPATCHING_TIMEOUT.count());
// 给mNoFocusedWindowTimeoutTime赋值,触发ANR时会检查这个值是否为空,不为空才触发ANR
mNoFocusedWindowTimeoutTime = currentTime + timeout;
// 把当前的focusedApplicationHandle赋值给mAwaitedFocusedApplication,触发ANR时会检查这个值是否为空,不为空才触发ANR
mAwaitedFocusedApplication = focusedApplicationHandle;
mAwaitedApplicationDisplayId = displayId;
*nextWakeupTime = *mNoFocusedWindowTimeoutTime;
// 返回INPUT_EVENT_INJECTION_PENDING表示dispatchKeyLocked()或者dispatchMotionLocked()为false
return INPUT_EVENT_INJECTION_PENDING;
} else if (currentTime > *mNoFocusedWindowTimeoutTime) {
// Already raised ANR. Drop the event
return INPUT_EVENT_INJECTION_FAILED;
} else {
// Still waiting for the focused window
return INPUT_EVENT_INJECTION_PENDING;
}
}

// 如果走到这个流程,说明没有ANR,清空mNoFocusedWindowTimeoutTime和mAwaitedFocusedApplication
resetNoFocusedWindowTimeoutLocked();
return INPUT_EVENT_INJECTION_SUCCEEDED;
}

主要逻辑:



  • 如果focusedWindowHandle和focusedApplicationHandle都为null,一般发生在窗口切换的时候,返回INPUT_EVENT_INJECTION_FAILED,直接drop事件,不做处理

  • 如果focusedWindowHandle为null,focusedApplicationHandle不为null,返回INPUT_EVENT_INJECTION_PENDING,在nextWakeupTime之后唤醒,检查是否发生ANR

    • mNoFocusedWindowTimeoutTime:记录no focused window timeout的时间

    • mAwaitedFocusedApplication:记录focusedApplicationHandle

    • nextWakeupTime: 下次唤醒pollInner的时间




接下来看看检查ANR的逻辑:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
nsecs_t InputDispatcher::processAnrsLocked() {
const nsecs_t currentTime = now();
nsecs_t nextAnrCheck = LONG_LONG_MAX;
// 在findFocusedWindowTargetsLocked()中,如果focusedWindowHandle为空,focusedApplicationHandle不为空,以下条件就会满足
if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
// mNoFocusedWindowTimeoutTime为检查时间+5s,如果currentTime大于等于mNoFocusedWindowTimeoutTime,表示超时
if (currentTime >= *mNoFocusedWindowTimeoutTime) {
// 触发ANR流程,此处触发的ANR类型是xxx does not have a focused window
processNoFocusedWindowAnrLocked();
// 清空mAwaitedFocusedApplication,下次就不会再走ANR流程
mAwaitedFocusedApplication.clear();
mNoFocusedWindowTimeoutTime = std::nullopt;
return LONG_LONG_MIN;
} else {
// Keep waiting
const nsecs_t millisRemaining = ns2ms(*mNoFocusedWindowTimeoutTime - currentTime);
ALOGW("Still no focused window. Will drop the event in %" PRId64 "ms", millisRemaining);
// 还没有超时,更新检查时间
nextAnrCheck = *mNoFocusedWindowTimeoutTime;
}
}
....
// 如果走到这个流程,ANR类型是xxx is not responding. Waited xxx ms for xxx
// 这个地方,focusedWindowHandle和focusedApplicationHandle都是不为空的场景
onAnrLocked(*connection);
return LONG_LONG_MIN;
}

主要流程:



  • 如果mNoFocusedWindowTimeoutTime有值,且mAwaitedFocusedApplication不为空

    • 超时:调用processNoFocusedWindowAnrLocked触发ANR

    • 未超时:更新检查时间



  • 继续检查input事件是否超时,如果超时,则调用onAnrLocked触发ANR


接下来,我们看看processNoFocusedAnrLocked的流程:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::processNoFocusedWindowAnrLocked() {
// 在触发ANR前,再获取一次当前的focusedApplication
sp<InputApplicationHandle> focusedApplication =
getValueByKey(mFocusedApplicationHandlesByDisplay, mAwaitedApplicationDisplayId);
// 检查触发ANR时的条件是focusedApplication不为空
// 如果此时focusedApplication为空,或者focusedApplication不等于前一个mAwaitedFocusedApplication表示已经切换application focus,取消触发ANR
if (focusedApplication == nullptr ||
focusedApplication->getApplicationToken() !=
mAwaitedFocusedApplication->getApplicationToken()) {
return; // The focused application has changed.
}
// 在触发ANR前,再获取一次当前的focusedWindowHandle
const sp<InputWindowHandle>& focusedWindowHandle =
getFocusedWindowHandleLocked(mAwaitedApplicationDisplayId);
// 检查触发ANR时focusedWindowHandle为空,如果此时focusedWindowHandle不为空,取消触发ANR
if (focusedWindowHandle != nullptr) {
return; // We now have a focused window. No need for ANR.
}
// 通过前面的判断,还是无法拦截,说明该ANR无可避免,最终触发ANR
// 早期代码没有前面一系列的判断,是直接触发的ANR,会在性能较差的场景下出现误判
onAnrLocked(mAwaitedFocusedApplication);
}

主要流程:



  • 在这个方法里面,再次检查focusedApplication

    • 如果当前focusedApplication为空,或者和之前记录的mAwaitedFocusedApplication不一致,则说明窗口已经切换,不需要报ANR



  • 再次检查focusedWindow是否未空

    • 如果不为空,则不需要报ANR



  • 检查都通过之后,才会调用onAnrLocked,报no Focused Window ANR


focusedApplication设置流程


// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
void InputDispatcher::setFocusedApplication(
int32_t displayId, const sp<InputApplicationHandle>& inputApplicationHandle) {
{ // acquire lock
std::scoped_lock _l(mLock);
// 获取当前的focusedApplicationHandle
sp<InputApplicationHandle> oldFocusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);
// 如果当前的focusedApplicationHandle跟触发ANR是的focusedApplicationHandle是一样且
// 新的focusedApplicationHandle跟旧的不一样,说明focusedApplicationHandle有更新
// 需要重置ANR计时
if (oldFocusedApplicationHandle == mAwaitedFocusedApplication &&
inputApplicationHandle != oldFocusedApplicationHandle) {
// 重置ANR计时
resetNoFocusedWindowTimeoutLocked();
}

if (inputApplicationHandle != nullptr && inputApplicationHandle->updateInfo()) {
if (oldFocusedApplicationHandle != inputApplicationHandle) {
// 赋值新的inputApplicationHandle到mFocusedApplicationHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedApplicationHandlesByDisplay[displayId] = inputApplicationHandle;
}
} else if (oldFocusedApplicationHandle != nullptr) {
// 如果inputApplicationHandle为空,oldFocusedApplicationHandle不为空,需要清除oldFocusedApplicationHandle
oldFocusedApplicationHandle.clear();
// 走到这个流程会出现findFocusedWindowTargetsLocked()中focusedApplicationHandle为空
mFocusedApplicationHandlesByDisplay.erase(displayId);
}
} // release lock

// Wake up poll loop since it may need to make new input dispatching choices.
mLooper->wake();
}

主要流程:



  • 如果inputApplicationHandle与oldFocusedApplication,则要重置ANR计时

  • 如果inputApplicationHandle不为空,则更新map中的值

  • 如果inputApplicationHandle为空,则清除oldFocusedApplication


这个方法,是从AMS调过来的,主要流程如下图:
image.png


focusedWindow设置流程


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// 当VSYNC信号来了之后,会调用到SurfaceFlinger的onMessageInvalidate()方法
// SurfaceFlinger::onMessageInvalidate()
// ==> SurfaceFlinger: updateInputFlinger()
// ==> SurfaceFlinger: updateInputWindowInfo()
// ==> InputManager::setInputWindows()
// ==> InputDispatcher::setInputWindows()
// ==> InputDispatcher::setInputWindowsLocked()
void InputDispatcher::setInputWindowsLocked(
const std::vector<sp<InputWindowHandle>>& inputWindowHandles, int32_t displayId) {

// ......
const std::vector<sp<InputWindowHandle>> oldWindowHandles = getWindowHandlesLocked(displayId);
// 更新mWindowHandlesByDisplay这个map,然后通过getWindowHandlesLocked()找newFocusedWindowHandle
updateWindowHandlesForDisplayLocked(inputWindowHandles, displayId);

sp<InputWindowHandle> newFocusedWindowHandle = nullptr;
bool foundHoveredWindow = false;
// 在mWindowHandlesByDisplay这个map里面找newFocusedWindowHandle
for (const sp<InputWindowHandle>& windowHandle : getWindowHandlesLocked(displayId)) {
// newFocusedWindowHandle要不为空,windowHandle具备focusable和visible属性
if (!newFocusedWindowHandle && windowHandle->getInfo()->hasFocus &&
windowHandle->getInfo()->visible) {
// 给newFocusedWindowHandle赋值,最后这个值存到mFocusedWindowHandlesByDisplay这个map
newFocusedWindowHandle = windowHandle;
}
if (windowHandle == mLastHoverWindowHandle) {
foundHoveredWindow = true;
}
}

if (!foundHoveredWindow) {
mLastHoverWindowHandle = nullptr;
}

// 在mFocusedWindowHandlesByDisplay这个map里找当前的焦点窗口
sp<InputWindowHandle> oldFocusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);

// 判断oldFocusedWindowHandle是否等于newFocusedWindowHandle,如果相等则不走focus change流程
if (!haveSameToken(oldFocusedWindowHandle, newFocusedWindowHandle)) {
// 如果当前的焦点窗口不为空,需要从mFocusedWindowHandlesByDisplay移除掉
if (oldFocusedWindowHandle != nullptr) {
sp<InputChannel> focusedInputChannel =
getInputChannelLocked(oldFocusedWindowHandle->getToken());
if (focusedInputChannel != nullptr) {
CancelationOptions options(CancelationOptions::CANCEL_NON_POINTER_EVENTS,
"focus left window");
synthesizeCancelationEventsForInputChannelLocked(focusedInputChannel, options);
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*oldFocusedWindowHandle, false /*hasFocus*/);
}
// oldFocusedWindowHandle不为空时需要移除旧的
mFocusedWindowHandlesByDisplay.erase(displayId);
}
// 走到这个流程,如果oldFocusedWindowHandle不为空,newFocusedWindowHandle为空,那么在findFocusedWindowTargetsLocked()中的focusedWindowHandle为空
// 如果newFocusedWindowHandle不为空,更新mFocusedWindowHandlesByDisplay
if (newFocusedWindowHandle != nullptr) {
// 更新mFocusedWindowHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedWindowHandlesByDisplay[displayId] = newFocusedWindowHandle;
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*newFocusedWindowHandle, true /*hasFocus*/);
}

if (mFocusedDisplayId == displayId) {
// 添加focusChanged到mCommandQueue,在dispatchOnce时会执行
onFocusChangedLocked(oldFocusedWindowHandle, newFocusedWindowHandle);
}
}

// ......
}

这个方法,是从WMS调过来的,主要流程如下图:
image.png


ANR可能的原因



  1. 设置focusedApplication和focusedWindow中间时间差太长,在这个时间差内发生了ANR



  • 设置focusedApplication发生在resumeTopActivity,也就是am_set_resumed_activity的时候。

  • 设置focusedWindow发生在onResume结束后,也就是调用WMS的addView添加完窗口之后。


在这个过程中,很有很多的生命周期流程,包括前一个Activity的onPause,Applicaiton的attachBaseContext, onCreate, Activity的onCreate,onStart,onResume。所有方法加起来耗时不能超过5s,否则很容易发生ANR。



  1. window被设置成了no_focusable,无法响应焦点。



  • 如果误将一个window设置成no_focusable,则窗口无法成为focusedWindow,也可能导致ANR的发生。

  • 不过这种情况一般比较少出现。

作者:尹学姐
链接:https://juejin.cn/post/7224839996402122809
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一线城市or回老家

前言 哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历 19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测…… 过需求嘛,不就是听一听pm怎么讲; 开发嘛...
继续阅读 »

前言


哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历


19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测……



  • 过需求嘛,不就是听一听pm怎么讲;

  • 开发嘛,就自己慢慢开发;

  • 对接口嘛,等着后端给呗,慢慢对;

  • 过测试用例嘛,就听听测试怎么讲呗;

  • 提测嘛,主要测试干,有bug我改改呗~


no!现在回想起当时的这些心里所想,简直是大错特错啦!经历过很多事情后,我来给你们整理个干货:



  • 过需求,很重要,不只是听pm说,自己还要审视需求,从技术的角度,让技术实现起来简单,又能满足产品需求,否则最后坑的还是自己,吭哧吭哧的去实现pm提出来的奇奇怪怪的需求~

  • 开发,不单单要开发,还要提前预估好时间,安排好自己的计划,有什么问题,要 delay了,都要提前跟pm说,否则 最后难办的还是你自己,熬大夜的还是自己。。提前跟他们说,提前要时间,提前规划好,我就是不加班的那个崽!

  • 对接口,也不仅仅是对接口,要提前跟后端要接口文档,否则你都不会想到你的后端怎么能给你跟你开发差别这么大的数据格式,尤其是陌生的后端,别问我咋知道的,说多了都是泪o(╥﹏╥)o

  • 过测试用例也要给我好好听,谁知道测试那个货看完需求文档后,理解的跟你开发的有啥区别,没准完全不一样,这时候你要给予反驳,问pm到底是啥,否则你会收到很多奇奇怪怪,每个字都认识,但是结合起来无法理解的bug。。一样也别问我咋知道的0.0

  • 提测,一定一定要自测,确保主体流程通顺,否则被打下来的话,是piapia的被测试打脸


从摆烂到涨知识


经历过初期的摧残之后,我进入到了摆烂期,因为什么都熟悉了,给东西也能做出来了,就日常摆烂了,直到跳槽去了另一家比较新型的互联网公司,接触了好多之前没接触的,干一天学到的东西是上家公司干一年也可能学不到的。


之前每次发包,是自己吭哧吭哧远程服务器,贴个包,现在是Jenkins自动化部署,一点就好;


之前没开发过微信小程序、没用react写过项目,现在天天是uniapp开发的微小和react+hooks的后管……


总之,就感觉学了好多东西,每天都在学习。


2022 放飞


直到2022年三月,上海疫情的到来,开启了在家办公,身边同事也被辞了好多。


2022年6月复工,又开始了正常去公司上班,但是任务很少,几乎没再学到东西了,每天上班就是再跟同事扯皮子。


2022年9月我也被辞了,公司因自己发展原因,辞退了我,然后就计划回老家了,给外面也干了三年了,决定在走之前玩一把,就去了杭州、去了好多之前想去的的地方



回老家安安稳稳


因为自己还养了两只猫,我自己还晕车,总之就很艰难的在2022年九月中旬回了老家,又休息了半个多月,开始找工作,老家的工作真的很不好找,boss、智联都被翻烂了,全聊过了,而且薪资也很低,简直是比之前的一半都低,面试也根本不像一线城市一样那么难,好多还不是双休,就这样艰难的挑挑拣拣,在十月中旬,我入职了,过起了躺平的日子,从来不加班,九点到,六点跑,双休,技术上有很多之前没接触过的,但不怕,慢慢整呗,而且我身兼数职,虽然是前端,但可以帮忙做icon图标,还可以当当测试。


跟之前比,难免有技术上和管理规范上的落差感,但是回老家后的生活相当充实和真实,每天都能吃的爸爸妈妈做的饭,走从小走过的路,虽然钱不多,但是真的幸福感+真实感上升了好多。


2023年我就希望可以安安稳稳,平平安安过着简单的小日子,只要自己觉得快乐就好。


还在犹豫在一线城市打拼还是回老家的友友们,你们也可以看看我的经历,来判断哟,我个人觉得如果不打算在一线城市买房安家的,早点回老家挺好的,安安稳稳,愿大家也可以过自己觉得舒服的日子哟~


作者:wenLi
链接:https://juejin.cn/post/7187315339846713399
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android自定义控件之虚拟摇杆(遥控飞机)

前言 之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图: 功能分析 本次自定义View功能需求如下: 1.摇杆绘制 自定...
继续阅读 »

前言


之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图:
虚拟摇杆.gif


功能分析


本次自定义View功能需求如下:

1.摇杆绘制

自定义View绘制摇杆大小圆,手指移动时只改变小圆位置,当手指触摸点在大圆外时,小圆圆心在大圆边缘上,并且绘制一条蓝色弧线,绘制度数为小圆圆心位置向两侧延伸45度(一般UI设计的时候,会给特定的圆弧形图片,如果显示图片就需要将图片移动到小圆圆心位置,之后根据手指触摸点与大圆圆心夹角来旋转图片,目前没有找到类似的圆弧图片,后期看能不能找到类似的)。

2.摇杆移动数据返回

返回摇杆移动产生的数据,根据这些数据控制飞行图片移动。在这里我返回的是飞机图片x,y坐标应该改变的值。这个值具体如何获得,在下面代码实现中讲解。

3.飞机图片移动

飞机图片移动相对简单,只需要在接收到摇杆数据的时候,修改飞机图片绘制位置,并重绘即可,需要注意的地方是摇杆移动飞机超出View边界该怎么处理。


代码实现


摇杆绘制和摇杆移动数据返回,通过自定义的RockerView内实现,飞机图片移动,通过自定义的FlyView实现,上述功能在RockerView和FlyView代码实现里面介绍。


摇杆(RockerView)


我们可以先从摇杆如何绘制开始。


首先从RockerView开头声明一些绘制需要一些变量,比如画笔,圆心坐标,手指触摸点坐标,圆半径等变量。


在init()方法内对画笔样式,颜色,View默认宽高等数据进行设置。


在onMeasure()方法内获取View的宽高模式,该方法简单可以概况为,宽高有具体值或者为match_parent。宽高设置为MeasureSpec.getSize()方法获取的数据,之后宽高值取两者中最小值,当宽高值在xml设置为wrap_content时,宽高取默认值,之后在方法末尾通过setMeasuredDimension()设置宽高。


在onLayout()方法内,对绘制圆等图像用到的变量进行赋值,例如,大圆圆心xy值,小圆圆心xy值,大小圆半径,绘制蓝色圆弧矩形,RockerView宽高等数据。


之后是onDraw()方法,在该方法内绘制大小圆,蓝色圆弧等图案。只不过蓝色圆弧需要加上判断条件来控制是否绘制。


手指触摸时绘制小圆位置改变,则需要重写onTouchEvent()方法,当手指按下或移动时,需要更新手指触摸点坐标,并判断手指触摸点是否超出大圆,超出大圆时,需要计算小圆圆心位置,并且还需要计算手指触摸点与圆心连线和x正半轴形成的夹角。并且通过接口返回摇杆移动的数据,飞机图片根据这些数据来移动。


绘制代码简单介绍如上,下面对View内一些需要注意地方进行介绍。如果看到完整代码,里面有一个自定义方法是initAngle(),该方法代码如下:


/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

这个方法用在onTouchEvent()方法的手指按下与移动事件中应用,这个方法前两行代码是计算手指触摸点与圆心连线和x正半轴形成的夹角取值,夹角取值范围如下图所示。
图片.png
代码先通过Math.atan2(y,x)方法获取手指触摸点与圆心连线和x正半轴之间的弧度制,获取弧度后通过(float) (radian * (180 / Math.PI))获取对应的度数,这里特别注意下Math.atan2(y,x)方法是y值在前,x在后。

此外这个方法还计算了手指触摸点与大圆圆心距离,以及判断手指触摸点是否在大圆外,以及在大圆外时,获取在大圆边缘上的小圆圆心的xy值。


在计算小圆圆心的坐标需要了解一个地方是,view实现过程中使用的坐标系是屏幕坐标系,屏幕坐标系是以View左上角为原点,原点左边是x的正半轴,原点下面是y正半轴,屏幕坐标系和数学坐标系是不一样。小圆圆心坐标获取原理,是根据三角形的相似原理获取,小圆圆心的坐标获取原理如下图所示:


图片.png
在上图中可以看到小圆y坐标的获取,小圆x坐标获取与y获取类似。可以直接把公式套进去。关于摇杆绘制的内容,至此差不多完成了,下面来处理返回摇杆移动数据的功能。


返回摇杆移动数据是通过自定义接口实现的。在触摸事件返回摇杆移动数据的事件有手指按下与移动。我们代码可以写为下面的形式(下面代码是伪代码)。


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//返回摇杆移动数据的方法
break;
case MotionEvent.ACTION_UP:
...
break;
}
postInvalidate();
return true;
}

如果按照上面代码写法我们会发现,当我们手指按下不动的时候或者手指按下移动一会后手指不动,是不会触发ACTION_MOVE事件的,不触发这个事件,我们就无法返回摇杆移动的数据,进而无法控制飞机改变位置。效果图如下


虚拟摇杆_按下不移动的问题.gif
解决这个问题,需要使用Handler和Runnable,在Runnable的run方法内,实现接口方法,并调用自身。getFlyOffset()是传递摇杆移动数据的方法,代码如下:


private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

之后在手指按下与点击事件里面,先判断Handler有没有开始,若isStart为true,则isStart改为false,并移除mRunnable,之后isStart改为true,延迟16ms执行mRunnable,当手指抬起时,若Handler状态为开始,则修改状态为false并移除mRunnable,这样就解决了手指按下不移动时,传递摇杆数据,相关代码如下:


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
...
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
...
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

至此摇杆相关功能介绍完毕,RockerView完整代码如下:


public class RockerView extends View {
private final int VELOCITY = 40;//飞机速度

private Paint smallCirclePaint;//小圆画笔
private Paint bigCirclePaint;//大圆画笔
private Paint sideCirclePaint;//大圆边框画笔
private Paint arcPaint;//圆弧画布
private int smallCenterX = -1, smallCenterY = -1;//绘制小圆圆心 x,y坐标
private int bigCenterX = -1,bigCenterY = -1;//绘制大圆圆心 x,y坐标
private int touchX = -1, touchY = -1;//触摸点 x,y坐标
private float bigRadiusProportion = 69F / 110F;//大圆半径占view一半宽度的比例 用于获取大圆半径
private float smallRadiusProportion = 4F / 11F;//小圆半径占view一半宽度的比例
private float bigRadius = -1;//大圆半径
private float smallRadius = -1;//小圆半径
private double distance = -1; //手指按压点与大圆圆心的距离
private double radian = -1;//弧度
private float angle = -1;//度数 -180~180
private int viewHeight,viewWidth;
private int defaultViewHeight, defaultViewWidth;
private RectF arcRect = new RectF();//绘制蓝色圆弧用到矩形
private int drawArcAngle = 90;//圆弧绘制度数
private int arcOffsetAngle = -45;//圆弧偏移度数
private int drawTime = 16;//告诉flyView重绘的时间间隔 这里是16ms一次
private boolean isBigCircleOut = false;//触摸点在大圆外

private boolean isStart = false;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

public RockerView(Context context) {
super(context);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}


private void init(Context context) {
defaultViewWidth = DensityUtil.dp2px(context,220);
defaultViewHeight = DensityUtil.dp2px(context,220);

bigCirclePaint = new Paint();
bigCirclePaint.setStyle(Paint.Style.FILL);
bigCirclePaint.setStrokeWidth(5);
bigCirclePaint.setColor(Color.parseColor("#1AFFFFFF"));
bigCirclePaint.setAntiAlias(true);

smallCirclePaint = new Paint();
smallCirclePaint.setStyle(Paint.Style.FILL);
smallCirclePaint.setStrokeWidth(5);
smallCirclePaint.setColor(Color.parseColor("#4DFFFFFF"));
smallCirclePaint.setAntiAlias(true);

sideCirclePaint = new Paint();
sideCirclePaint.setStyle(Paint.Style.STROKE);
sideCirclePaint.setStrokeWidth(DensityUtil.dp2px(context, 1));
sideCirclePaint.setColor(Color.parseColor("#33FFFFFF"));
sideCirclePaint.setAntiAlias(true);

arcPaint = new Paint();
arcPaint.setColor(Color.parseColor("#FF5DA9FF"));
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(5);
arcPaint.setAntiAlias(true);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取视图的宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width,height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else {
width = defaultViewWidth;
}

if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else {
height = defaultViewHeight;
}
width = Math.min(width,height);
height = width;
//设置视图的宽度和高度
setMeasuredDimension(width,height);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
bigCenterX = getWidth() / 2;
bigCenterY = getHeight() / 2;
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;

bigRadius = bigRadiusProportion * Math.min(bigCenterX, bigCenterY);
smallRadius = smallRadiusProportion * Math.min(bigCenterX, bigCenterY);

arcRect.set(bigCenterX-bigRadius,bigCenterY-bigRadius,bigCenterX+bigRadius,bigCenterY+bigRadius);
viewHeight = getHeight();
viewWidth = getWidth();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, bigCirclePaint);
canvas.drawCircle(smallCenterX, smallCenterY, smallRadius, smallCirclePaint);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, sideCirclePaint);

if (isBigCircleOut) {
canvas.drawArc(arcRect,angle+arcOffsetAngle,drawArcAngle,false,arcPaint);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
touchX = (int) event.getX();
touchY = (int) event.getY();
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;
isBigCircleOut = false;
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

/** 获取飞行偏移量 */
private void getFlyOffset() {
float x = (smallCenterX - bigCenterX) * 1.0f / viewWidth * VELOCITY;
float y = (smallCenterY - bigCenterY) * 1.0f / viewHeight * VELOCITY;
onRockerListener.getDate(this, x, y);
}

/**
* pX,pY为手指按点坐标减view的坐标
*/
public interface OnRockerListener {
public void getDate(RockerView rocker, final float pX, final float pY);
}
private OnRockerListener onRockerListener;
public void getDate(final OnRockerListener onRockerListener) {
this.onRockerListener = onRockerListener;
}
}

飞机(FlyView)


飞机图片移动相对简单,实现原理是在自定义View里面,通过改变绘制图片方法(drawBitmap()方法)里的left,top值来模拟飞机移动。FlyView实现代码如下:


public class FlyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int viewHeight, viewWidth;
private int imgHeight, imgWidth;
private int left, top;

public FlyView(Context context) {
super(context);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

void init(Context context) {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.fly);
imgHeight = mBitmap.getHeight();
imgWidth = mBitmap.getWidth();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
left = w / 2 - imgHeight / 2;
top = h / 2 - imgWidth / 2;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, left, top, mPaint);
}

/** 移动图片 */
public void move(float x, float y) {
left += x;
top += y;
if (left < 0) {
left = 0;
}else if (left > viewWidth - imgWidth) {
left = viewWidth - imgWidth;
}

if (top < 0) {
top = 0;
} else if (top > viewHeight - imgHeight) {
top = viewHeight - imgHeight;
}
postInvalidate();
}
}

在Activity或者Fragment里面对View设置代码(kotlin)如下:


binding.viewRocker.getDate { _, pX, pY ->
binding.viewFly.move(pX, pY)
}

飞机图片如下:


fly.png


总结


摇杆整体实现没有太复杂的逻辑,比较容易混的地方,可能是屏幕坐标系和数学坐标系能不能转过弯来。印象中好像可以通过Matrix将坐标变换,但一时间想不起来怎么实现,后面了解下Matrix相关内容。

关于虚拟摇杆实现有很多方式,我写的这个不是最优的方式,虚拟摇杆有些需求没有接触到,在代码实现中可能比较简单,小伙伴们看到文章不足的地方,可以留言告诉我,一起学习交流下。


项目地址: GitHub


作者:卤肉拌面
链接:https://juejin.cn/post/7225079705992822842
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我受精了,Git log竟然还有这种用法!

Git
前言 当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。 在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。 查看提交历史记录 通过在命令行中输入以下命令来查看提交历史记录: git...
继续阅读 »

前言


当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。


在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。


查看提交历史记录


通过在命令行中输入以下命令来查看提交历史记录:


git log

这将显示最新的提交历史记录,包括提交ID、作者、提交日期、提交消息等。例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

commit 0498b3d96b2732e36e7d41501274c327a38188aa
Author: Zhangsan <Zhangsan@example.com>
Date: Fri Apr 23 14:01:11 2023 -0500

Updated documentation

显示分支图


如果你想查看分支的提交历史记录,你可以使用git log --graph命令来显示一个分支图。分支图可以帮助你更好地理解分支之间的关系和合并历史。


例如:


git log --graph

这将显示一个分支图,其中每个提交都显示为一个节点,不同的分支用不同的线表示。你可以在分支图中看到合并提交和分支之间的关系。


例如:


* commit da32d1d7e7f22ec59330e6b8c51def819b951aec
| Author: Zhangsan <Zhangsan@example.com>
| Date: Wed Apr 12 15:28:40 2023 +0800
|
| feat:xxx
|
* commit 8fdc0a9838d45d9e027740e7a448822bb8431969
|\ Merge: e22ce87ae d80ce707b
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Wed Apr 12 13:08:17 2023 +0800
| |
| | Merge branch 'xxx' into xxx
| |
| * commit d80ce707b72e1231c18a4843e62175a7a430e3c3
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Tue Apr 11 19:36:48 2023 +0800
| |
| | xxxx
| |

格式化输出


git log命令还支持格式化输出,你可以使用--pretty选项来指定输出的格式。例如,以下命令将以一种类似于JSON的格式输出提交记录:


git log --pretty=format:'{%n "commit": "%H",%n "author": "%an <%ae>",%n "date": "%ad",%n "message": "%f"%n},' --no-merges

这将输出每个提交的哈希值、作者、提交日期和提交消息。


例如:


{
"commit": "a8c4b34ab5e4d844dc741e105913266502d82dcd",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sun Apr 16 16:32:20 2023 +0800",
"message": "feat-resize"
},
{
"commit": "f23b4e61633033b9db5a3c87afc5f523cf5e583e",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sat Apr 15 15:32:25 2023 +0800",
"message": "feat"
}

你也可以使用一些预定义的格式来输出,例如--pretty=oneline将每个提交压缩成一行,只包含哈希值和提交消息。


例如:


a3fe1d136ab9587db19d9f8073fd491ead892f4a feat:xxxx
84738075dd00f1e0712f139c23c276b7559fd0d9 feat:xxxx
a8c4b34ab5e4d844dc741e105913266502d82dcd feat:xxxx
f23b4e61633033b9db5a3c87afc5f523cf5e583e feat:xxxx

查看详细信息


默认情况下,git log命令只显示每个提交的基本信息。但是,你可以通过添加--stat选项来显示每个提交所做的更改数量和文件列表。


例如:


git log --stat

这将显示每个提交所做的更改数量和文件列表


例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

somefile.txt | 1 +
1 file changed, 1 insertion(+)

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

somefile.txt | 1 -
1 file changed, 1 deletion(-)

commit 049

查看某个提交的详细信息


除了git log命令,我们还可以使用git show


如果你想查看某个提交的详细信息,可以使用git show <commit>命令。


例如:


git show 6d883ef

这将显示提交6d883ef的详细信息,包括提交消息、作者、提交日期和更改的文件。


查看某分支记录


有时候你可能只想查看某个特定分支历史记录。这可以使用git log <branch>命令。


例如,如果你只想查看main分支的历史记录,你可以输入以下命令:


git log main

显示指定文件的提交历史记录


如果你只想查看某个文件的提交历史记录,你可以使用git log <filename>命令。


例如:


git log somefile.txt

这将显示与该文件相关的所有提交历史记录。


显示指定作者的提交历史记录


如果你只想查看某个作者的提交历史记录,你可以使用git log --author=<author>命令。例如:


例如:


git log --author="Zhangsan"

这将显示所有由Zhangsan提交的历史记录。


显示指定时间段的提交记录


如果你指向查看某个时间范围内的提交历史记录、你可以使用git log --after,git log --before命令。


例如:


git log --after="2023-04-01" --before="2023-04-02"

这将显示出,2023-04-01 到 2023-04-02之间的提交记录



  • --after 会筛选出指定时间之后的提交记录

  • --before 会筛选出指定时间之前的提交记录。


还有一些快捷命令:


git log --after="yesterday" //显示昨天的记录
git log --after="today" //显示今天的
git log --before="10 day ago" // 显示10天前的提交记录
git log --after="1 week ago" //显示最近一周的提交录
git log --after="1 month ago" //显示最近一个月的提交率

限制输出的历史记录数量


例如,要查看最近的5个提交,你可以使用以下命令:


git log -5

搜索历史记录


git log命令还可以用于搜索历史记录。例如,如果你只想查看提交消息中包含关键字“bug”或“fix”的历史记录,可以使用以下命令:


git log --grep=bug --grep=fix

这将显示所有提交消息中包含关键字“bug”或“fix”的提交记录。


commit 27ad72addeba005d7194132789a22820d994b0a9
Author: Zhangsan <Zhangsan@example.com>
Date: Thu Apr 13 11:17:13 2023 +0800

fix:还原local环境配置

commit 8369c45344640b3b7215de957446d7ee13a48019
Author: Zhangsan <Zhangsan@example.com>
Date: Mon Apr 10 11:02:47 2023 +0800

fix:获取文件

显示带有内容变更的提交日志


如果你想查看带有内容变更的提交日志,可以使用git log -p,能清楚的看到每次提交的内容变化。
非常有用的一个命令;


例如:


git log -p

这将显示每个提交与它的父提交之间的差异。


diff --git a/xxxx.tsx b/xxxx.tsx
index 7f796c934..87b365426 100644
--- a/xxx.tsx
+++ b/xxx.tsx

我们也可以知道某个具体的提交的差异,可以使用git log -p <commit>命令


显示提交的差异


如果你想查看某个提交与上一个提交之间的差异,可以使用git log -p <commit>命令。例如:


git log -p 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429

这将显示提交6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429与它的父提交之间的差异。


显示当前分支的合并情况


如果你想查看当前分支的合并情况,可以使用git log --merges命令。例如:


git log --merges

commit 2f3f4c45a7be3509fff6496c9de6d13ef0964c9d
Merge: 8369c4534 4103a08bf
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 11:03:55 2023 +0800

Merge branch 'dev/feature1' into dev/dev

commit 14b40421ef54c875b8f8f0cfc297bcdc3960b9be
Merge: 30e36edbb 48bb05ede
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 00:34:09 2023 +0800

Merge branch 'dev/feature1' into dev/dev

总结


以上是更多关于git log命令的使用教程、示例,希望对你有所帮助。


作者:前端后腿哥
链接:https://juejin.cn/post/7225902885544165436
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

普通专科生的十年

前言 Hello,大家好,我是 Sunday。 其实我想写这个博客已经很久了,总是提起笔然后又放下,总觉得自己的经历普普通通,实在是没有什么值得分享的地方。 我从2013年 毕业,转眼间,时间来到了 2023 年的 4 月份。“前端已死,java 已亡”、“专...
继续阅读 »

前言


Hello,大家好,我是 Sunday


其实我想写这个博客已经很久了,总是提起笔然后又放下,总觉得自己的经历普普通通,实在是没有什么值得分享的地方。


我从2013年 毕业,转眼间,时间来到了 2023 年的 4 月份“前端已死,java 已亡”、“专科生是找不到工作的” 类似的言论开始充斥着各个平台。仿佛互联网已经到了耄耋之年,尽显疲态。


未来如何,没有人会知道。我们只谈过去,暂且不谈未来。


我也算是经历过 “互联网红利期” 的人,回想当年拿着一份简历,一天就可以收到 10 多个面试电话。对比现在刚毕业的同学而言,真的是幸福太多了。


因为工作的原因,我会接触到很多的前端初学者(以大专为主),能够感受到他们的焦虑,也能够感受到他们的迷茫。


我能够做到感同身受,可能因为,我也有过类似的经历吧~~


我的十年


2010 年,高考失利的我进入了济南一家普通的专科院校。


image-20230417202445036.png


在这里,我的脑海中没有 “努力、专注、奋斗、反思” 等词汇,只有 “游戏、恋爱” 这两个关键字。


混了三年,2013 年我从学院毕业。


可能是因为我运气比较好,也可能因为那个时候专科还有点价值。我很幸运的进入到了一家国企做检测员,薪资是 1200 块钱。


80.jpeg


如果一切没有变化,那么我可能会在这里一直干下去,过着混吃等死,没心没肺的生活。


让我人生轨迹发生改变的是:一次和我对面的一位老大哥的聊天。



这位老大哥当时是 三十五六岁,每个月到手是 3000 块钱出头,有两个孩子。因为父母身体不好,所以他老婆只能全职在家。


他们一家四口人,每个月只有这三千多块钱的收入,和别人合租了一个卧室,一家四口就挤在那个小小的卧室里面。


我当时就突然间有了一个想法,十年之后,我也会是这个样子吗?



因为当时身边有很多朋友在做这一行,所以我就从一个 hello world 都不知道怎么写的白中白开始,自学软件开发。


我开始尝试在工作之余自学 java,并且特别好运的在三个月之后拿到了一个 offer,薪资是 1600 块钱。从此开始了我的软件开发之路。对了,这个时候是 2014 年。


后来我又陆陆续续自学了 Android开发、IOS 开发、以及现在的前端开发


3221681782093_.pic.jpg


这让我养成了看书的习惯,所以才有了现在 小破站 的《一个小时阅读一本书》系列。


image-20230417195637739.png


随着不断的学习和工作,我开始可以在各个平台产出一些东西。


比如:在 GitChat 的上的付费专栏


image.png


比如:在慕课网上的技术视频


image-20230417201023839.png


长期的自学,让我深知自学之艰难,持续的产出,也让我喜欢上了教学的工作。


所以 2019年 10 月 我入职了黑马程序员,成为了一名前端老师,并且一直工作到了现在。


image-20230418091825852.png


2013 年 开始,到现在的 2023 年,正好十年的时间。


十年。


我从之前的一个 160斤 的山东小伙


DSC_08760.jpg


变成了现在 210斤 的山东大汉。


3211681781608_.pic.jpg


收入,也从原先的 1200 块钱,到现在可以保证一个家庭体面的生活。


十年,我没有变成那位老大哥的样子,而是可以做着自己喜欢的工作,同时能够帮助着更多希望学习技术的人。


仔细想想,好像也不错~~~~~~~


结语


这篇博客我在半个月之前就已经写好了,只是一直没有发出来。


今天也不是什么特殊的日子,想着发就发了吧。


以此来致我的十年~~


作者:LGD_Sunday
链接:https://juejin.cn/post/7223642909793878053
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

无聊的分享:点击EditText以外区域隐藏软键盘

1.前言 当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小...
继续阅读 »

1.前言


当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小小的思路。


2.如何实现


当我们在Activity单纯的添加一个EditText时,点击吊起软键盘,这个时候再点击EditText外部区域会是这个样子的:



会发现,无论我们怎么点击外部区域软键盘都不会收起。所以要达到点击外部区域收起键盘效果需要我们自己添加方法去隐藏键盘:


重写dispatchTouchEvent


override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
   ev?.let {
       if (it.action == MotionEvent.ACTION_DOWN) {
           //如果现在取得焦点的View为EditText则进入判断
           currentFocus?.let { view ->
               if (view is EditText) {
                   if (!isInSide(view, ev) && isSoftInPutDisplayed()) {
                       hideSoftInPut(view)
                  }
              }
          }
      }
  }
   return super.dispatchTouchEvent(ev)
}

在Activity 中重写dispatchTouchEvent,对ACTION_DOWN事件做处理,使用getCurrentFocus()方法拿到当前获取焦点的View,判断其是否为EditText,若为EditText,则看当前软键盘是否展示(isSoftInPutDisplayed)并且点击坐标是否在EditText的外部区域(isInSide),满足条件则隐藏软键盘(hideSoftInPut)。


判断点击坐标是否在EditText内部


//判断点击坐标是否在EditText内部
private fun isInSide(currentFocus: View, ev: MotionEvent): Boolean {
   val location = intArrayOf(0, 0)
//获取当前EditText坐标
   currentFocus.getLocationInWindow(location)
//上下左右
   val left = location[0]
   val top = location[1]
   val right = left + currentFocus.width
   val bottom = top + currentFocus.height
//点击坐标是否在其内部
   return (ev.x >= left && ev.x <= right && ev.y > top && ev.y < bottom)
}

定义一个数组location存储当前EditText坐标,计算出其边界,再用点击坐标(ev.x,ev.y)和边界做比较最终得出点击坐标是否在其内部。


来判断软键盘是否展示


private fun isSoftInPutDisplayed(): Boolean {
   return ViewCompat.getRootWindowInsets(window.decorView)
       ?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
}

使用
WindowInsetsCompat类来判断当前状态下软键盘是否展示,WindowInsetsCompat是AndroidX库中的一个类,用于处理窗口插入(WindowInsets)的辅助类,可用于帮助开发者处理设备的系统UI变化,如状态栏、导航栏、软键盘等,给ViewCompat.getRootWindowInsets传入decorView拿到其实例,利用isVisible方法判断软键盘(WindowInsetsCompat.Type.ime())是否显示。


隐藏软键盘


private fun hideSoftInPut(currentFocus: View) {
   currentFocus.let {
    //清除焦点
       it.clearFocus()
    //关闭软键盘
       val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
       imm.hideSoftInputFromWindow(it.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
  }
}

首先要清除当前EditText的焦点,防止出现键盘收起但是焦点还在的情况:



最后是获取系统Service隐藏当前的键盘。


来看看最终的效果吧:



3.结尾


以上就是关于点击EditText外部区域隐藏软键盘并且清除焦点的实现方法,当然这只是其中的一种方式,如有不足请在评论区或私信指出,如果你们有更多的实现方法也欢迎在论区或私信留言捏❤️❤️


作者:Otaku_尻男
链接:https://juejin.cn/post/7226248402798936119
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这一年半的时间我都做了些什么

介绍 也很久没有在这儿更新文章了,主要自己也不知道写什么。这篇文章不讲技术,主要讲一下我在这最近一年多时间的生活、工作上的一些经历和感受。 2021 在 2021 年 9 月,我通过两个星期的准备和面试,拿到几家公司的 offer。通过一些自己的硬性要求...
继续阅读 »

介绍



  • 也很久没有在这儿更新文章了,主要自己也不知道写什么。这篇文章不讲技术,主要讲一下我在这最近一年多时间的生活、工作上的一些经历和感受。


2021



  • 20219 月,我通过两个星期的准备和面试,拿到几家公司的 offer。通过一些自己的硬性要求(上下班时间、距离、薪资待遇...),最后选择了一家做低代码平台的公司,就在我和 HR 商谈到双方都满意准备定入职时间时,我收到了另一家公司的面试。本来这边我基本上已经确定了,可以选择推脱掉,但是那边说面试流程很快,让我考虑一下。就这样,我在回复后的当天晚上收到了初试并面试通过,随后不到 10 分钟的时间里接到了二面也通过了,就这样不到一个小时,我直接到了 HR 的终面,通过并拿到了 offer

  • 通过 HR 和面试官,我了解到了这家公司是做中老年教育的,有 APP、小程序、H5 ,用的技术也是比较前沿的新技术,通过自己的考虑,对比自己缺失的技术方向,我选择了后者。

  • 简单说一下谈的一些内容:

    • 上班时间为早上弹性 10:00

    • 每年 2 次调薪;

    • 年终奖 2 个月,也就是 14 薪,我看的招聘是 16 薪,但 HR 说的之前大小周的是,我去的时候刚好取消了大小周;

    • 社保全额 12%、公积金全额 5%

    • 节假日有节日福利;

    • 晚上 6 点有晚餐;

    • 加班到 10 点可以打车;

    • 每月最后一天发当月工资。



  • 看到这里,我相信很多人都觉得这是一个不错的工作,当时我也是这么想的,所以我怀着学习深造、技术沉淀、薪资待遇啥的都还不错的想法就去了。

  • 在去的第一天,我遇到了我大学的同班同学,可能是我去太早了,还没什么人,巧的是刚好是他给我开的门;这天除了办理入职,剩下的时间就是在熟悉代码,还是和其他公司入职没有什么差异;第二天拿到电脑,我就开始了在这的搬砖之路;前几天每天我都还都是比较正常的上下班,直到我接了第一个需求,每个需求都有排期,你需要自己在需求评讲的时候去思考涉及的改动、可能遇到的问题、是否对其他地方有影响以及需要的开发时间。由于对项目不熟悉、需求的变动等原因,导致好几天的加班,有时候到 21:00 点,有时候 22:00 甚至更晚。当我以为熟悉后下一个需求就好了,然而理想总是丰满的,这个状态并没有什么改变,反而越来越离谱。
    image.png

  • 需求一个接一个的来,到我手上的基本上都是大需求,前一个需求还没有提测或者刚提测就要进入下一个需求,边开发新需求边修改测试提的 bug。上线发布的时候加班是最多的,最开始那一两个月,加班太晚(00:00 及以后)还有夜宵,还有半天到一天的调休,招我进来的 HR 走了之后就什么都没有了。

  • 在快放春节的前几天,运营还临时加一个春节活动的需求,春节这种节日要做什么不知道提前规划,到要放假的前几天才通知。没出意外,我和另一个测试是公司最后走的。

  • 这年的中秋节是什么都没有,春节有一副对联、福字还有一支像钢笔的中性笔,到处都是公司的大而显眼的 logo


2022



  • 很多人可能很奇怪,不是 14 薪吗?上文怎么没有提到呢?是没有还是没发呢?

  • 为什么 2021 的年终奖的事情放在 2022 里说,年终奖是发了的,发的时间却是 4 月底,而在发年终奖前,还做了一个考核表让自己填去年在各个方面的表现,然后直系领导再打分。本以为只是简单走个过场,没想到事实却并非如此,年终奖有分级 A、B、C、D 分别占 5%、10%、75%、10%,对应系数 1.5、1.2、1、0D 是强制比例且必须被淘汰。如果都是强制比例那也还好,但却只有 D 是,A、B 的据我小道消息了解,普通员工中就只有一个 B,至于领导层是否占了名额就不得而知了。在整个过程中,没有一个考核标准,在 2021 年,基本上所有研发都在努力付出,都加班不少,结果还有强制比例的 D,前后端各有一个。前端的 D 还给到了我们意想不到的人头上,一个将三端使用的项目 0-1 搭建起来的人,后端则是忽悠说你是 21 年最后一天来的,得 D也无所谓(最开始没有说 D 强制被淘汰,名额确定后才约谈的,这我也是后来才知道的)。
    image.png

  • 从年终奖下来以后,前端走了 4 个人,其他的人也是居安思危,既然这次能这么做,那么也有可能有下次。

  • 说一下我为什么没有走:

    • 我才来不到半年时间;

    • 我当时和女朋友准备结婚;

    • 当时的行情也不是很好。



  • 综上,我继续在这里待着,但我也不再像从前那样自己老是加班,每天非的把某个东西做完或者做到某种程度再走。需求也不再没做完一个,领导让接下一个就屁颠屁颠去接(挣表现),领导之前开早会的时候说:xx(我)手上的事情够多了,下一个就换个人来。到过需求的时候,领导:这个还是让xx(我)来吧。我不知道我是该开心还是怎么样,让我来是因为认可我的能力、我对这块比较熟,还是其他什么原因呢?后来我知道了,需求是做不完的,我们当时就 4 个前端,产品却有 5-6个,永远有需求在等待评审开发。

  • 后面,我开始了每天 10 点,晚上 8 点前上下班,需求按时完成,但基本上不会再加班,除非遇到什么棘手的问题,一直没解决。

  • 说的年终奖后要普调薪资,最终结果是调了公积金的比例由 5% 调到了 12%,因为刚好买了房子,想着也还能接受。

  • 然后后面开始搞 OKR ,结果搞了 2 次,不知道什么原因没有继续搞了。

  • 5.20 号我领结婚证了,领证后我有一段时间不想让班,抵触的情绪异常高涨,就是去公司就觉得人没有精神、呆呆的。于是我就请假准备休息一下,我花光了我所有假,包括婚假和年假,我觉得还不够还请了 2 天事假,连续请了 2 周。由于假期太长,领导让老板审批,于是我就给老板发了飞书消息,老板也同意了,我给HR看了一下,让后我就开始了休假,结果后面我发现我的请假是在我已经休假后的 2-3 天才通过的。我当时走的时候就只有 HR 那儿没有通过,我还单独给他说了。

  • 8 月的婚礼由于疫情,推出到了 9 月底,最后也是圆满的完成了。

  • 这年的节假日只有中秋节有一个水杯、背包、飞盘,四个糕点饼,其他节日就什么都没有了,年会也没有。离谱的是,年前一个月左右有次聚餐,后来被称之为年会。团建看什么时候想起来,又一次去运动馆,自己吃中午饭后出发,完了没有晚饭自己回家。
    image.png

  • 日期老婆构图时写错啦,哈哈哈哈!!!


2023



  • 年初开始打卡了,没有邮件通知上下班打卡时间,但是显示的是早 108,领导说的是晚9,如果部门时长过低,会追溯到个人。然后我开始了早 107,可能很多人会说,你这不是明目张胆对着干吗?其实我自己也想了很多,我觉得我不应该只有工作,我应该还有生活。还有就是我老婆怀孕了,所以我需要更多的时间,而不是说只有工作,更不说是自己工作得不是很舒服的工作。

  • 公司也实施了一些控制成本的方案,降低晚餐标准 25 -> 18,降低打车额度 150 -> 100,...

  • 慢慢的,大家不约而同的都在等 4 月,想看今年又会有怎么样的年终考核方式,不出所料,还是去年那一套,自评加领导评。没有任何的依据,还说:你完成工作只是及格,想要高分,就得做出重大贡献啥的。

  • 我去年从做产品需求迭代,转到了内部系统基建,完成了桌面端直播开播的从 0-1 的第一版,开发内部新的应用 4 个,维护多个老的内部应用的前后端项目,这样的工作是否合格呢?答案是否定的。

  • 4 月底,结果出来了,我不出意外在被裁名单里,有人肯定会疑惑为什么没有说比例和系数?因为没有意义,这次明说比例 10%,结果裁了快一半,有的整个部门从前后端测试到产品运营都裁了。还增加了一个部门系数,听没被裁的说的是整个都是 0.5,意思就是 14 薪的 1 个月,16 薪的 2 个月。

  • 我自己都还好,有两个来了不到一个月的实习生也没能幸免,刚出来就经历这样的事情换谁也不好受。只能说往好的方面想吧!


总结


工作



  • 在这里确实学到了不少东西,就技术而言,对跨端、nodejs 后台、数据库、桌面端等一些问题的解决方案,有自己想的,也有学习别人的。

  • 职场相处,都说程序员的职场比较简单,确实相对其他的行业,这儿并没有那么多,但偶尔还是会遇到一些让你无法接受、理解、相处的人,但大部分人都是很好相处的,至少我是这么认为的,在这儿我也遇到一些很好的同事,也是朋友。

  • 当你要去一个公司的时候,不要光看表面的以及面试官和 HR 给你说的,你可以去一些贴吧、论坛、脉脉等上面去看这家公司的在职人员或离职人员的评论或看法。我相信不会有人无缘无辜去只说一家公司不好的方面,一家公司肯定有好有坏,每个人看重的点不一样,所以需要自己去权衡。

  • 像年终奖这种,如果合同没有固定写几个月,公司有很多的方法去卡。如果有一个考核标准,我觉得大家都能接受,你做了什么、成绩怎么样、出了哪些问题等等,总的有个衡量标准去量化它。


生活


  • 完成了自己的一件人生大事(结婚);

  • 有了属于自己房子(虽然还没交房);

  • 老婆在今年怀孕了(健康的兔宝宝)。


最后



  • 希望大家接好运,工作顺心、生活开心。

  • 有什么问题欢迎大家评论区留言交流。


作者:小小小十七
来源:juejin.cn/post/7229667871606374457
收起阅读 »