注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

JWT:你真的了解它吗?

       大家好,我是石头~        在数字化时代,网络安全和用户隐私保护成为了我们无法忽视的关键议题,也是我...
继续阅读 »

       大家好,我是石头~


       在数字化时代,网络安全和用户隐私保护成为了我们无法忽视的关键议题,也是我们作为一个后端开发的必修课。


       而在这个领域中,JWT(JSON Web Token)作为一种现代、安全且高效的会话管理机制,在各类Web服务及API接口中得到了广泛应用。


       那么,什么是JWT?


下载 (3).jfif


1、初识JWT


       JWT,全称为JSON Web Token,是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。


       它本质上是一个经过数字签名的JSON对象,能够携带并传递状态信息(如用户身份验证、授权等)。


       了解了JWT之后,那么它的组成结构又是怎样的?


2、JWT的结构


u=2288314449,1048843062&fm=253&fmt=auto&app=138&f=JPEG.webp


       如上图,JWT由三部分组成,通过点号(.)连接,这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。



  • 头部(Header):声明了JWT的类型(通常是JWT)以及所使用的加密算法(例如HMAC SHA256或RSA)

  • 载荷(Payload):承载实际数据的部分,可以包含预定义的声明(如iss(签发者)、exp(过期时间)、sub(主题)等)以及其它自定义的数据。这些信息都是铭文的,但不建议存放敏感信息。

  • 签名(Signature):通过对前两部分进行编码后的信息,使用指定的密钥通过头部(Header)中声明的加密算法生成,拥有验证数据完整性和防止篡改。


3、JWT的常规认证流程


2020040121153580.png


       JWT的认证流程如上图。当用户登录时,服务器通过验证用户名和密码后,会生成一个JWT,并将其发送给客户端。这个JWT中可能包含用户的认证信息、权限信息以及其它必要的业务数据。


       客户端在接收到JWT后,通常将其保存在本地(如Cookie、LocalStorage或者SessionStorage)。


       客户端在后续的请求中,携带此JWT(通常是附在HTTP请求头中),无需再次提交用户名和密码。服务器只需对收到的JWT进行解码并验证签名,即可完成用户身份的确认和权限验证。


4、JWT的完整认证流程


       在上面的JWT常规认证流程中,我们可以正常完成登陆、鉴权等认证,但是你会发现在这个流程中,我们无法实现退出登陆。


       当服务端将JWT发放给客户端后,服务端就失去了对JWT的控制权,只能等待这些发放出去的JWT超过有效期,自然失效。


       为了解决这个问题,我们引入了缓存,如下图。


2020040121022176.png


       当服务端生成JWT之后,在返回给客户端之前,先将JWT存入缓存中。要鉴权的时候,需要检验缓存中是否存在这个JWT。


       这样的话,如果用户退出登陆,我们只需要将缓存中的JWT删除,即可保证发放出去的JWT无法再通过鉴权。


5、JWT的优势与挑战


       JWT的主要优点在于无状态性,服务器无需存储会话状态,减轻了服务器压力,同时提高了系统的可扩展性和性能。


       此外,由于JWT的有效期限制,增强了安全性。


       然而,JWT也面临一些挑战,比如密钥的安全保管、JWT过期策略的设计以及如何处理丢失或被盗用的情况。


       因此,在实际应用中,需要综合考虑业务场景和技术特性来合理运用JWT。


6、JWT示例


       概念讲完了,我们最后来看个实例吧。


// Java代码示例
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

// 假设有一个User类,其中包含用户ID
public class User {
private String id;
// 其他属性和方法...
}

// 创建JWT
public String generateJWT(User user) {
// 设置秘钥(在此处使用的是HMAC SHA-256算法)
String secret = "your-secret-key"; // 在实际场景中应当从安全的地方获取秘钥
long ttlMillis = 60 * 60 * 1000; // JWT的有效期为1小时

// 构建载荷,包含用户ID和其他相关信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("exp", System.currentTimeMillis() + ttlMillis); // 设置过期时间

// 生成JWT
    String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8))
.compact();
    // TODO JWT写入缓存
    return jwt;
}

// 验证JWT
public boolean validateJWT(String jwtToken, String secretKey) {
boolean flag = false;
try {
Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(jwtToken);
// 如果没有抛出异常,则JWT验证通过
flag = true;
} catch (ExpiredJwtException e) {
// 如果Token过期
System.out.println("JWT已过期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
// 其他非法情况,如格式错误、签名无效等
System.out.println("JWT验证失败:" + e.getMessage());
}
if (flag) {
        // TODO 校验缓存中是否有该JWT
}
return false;
}

// 使用示例
public static void main(String[] args) {
User user = new User("123"); // 假设这是合法用户的ID
String token = generateJWT(user); // 生成JWT
System.out.println("生成的JWT Token: " + token);

// 验证生成的JWT
boolean isValid = validateJWT(token, "your-secret-key");
if (isValid) {
System.out.println("JWT验证通过!");
} else {
System.out.println("JWT验证未通过!");
}
}



作者:石头聊技术
来源:juejin.cn/post/7354308608044072996
收起阅读 »

记一次安卓广播引起的ANR死锁问题

年初遇到个bug,设备安装应用宝之后打开使用一段时间(浏览和安装应用),大概率会卡死,然后整个系统重启(表现为卡死后过一段时间开机动画出现,重新进入系统)。非常容易复现。 看到这种软重启首先考虑ANR,分析抓到的log   到日志中查找watchdog关键词 ...
继续阅读 »

年初遇到个bug,设备安装应用宝之后打开使用一段时间(浏览和安装应用),大概率会卡死,然后整个系统重启(表现为卡死后过一段时间开机动画出现,重新进入系统)。非常容易复现。


看到这种软重启首先考虑ANR,分析抓到的log


  到日志中查找watchdog关键词
01-25 11:02:42.032 22774 22796 W Watchdog: *** WATCHDOG KILLING SYSTEM PROCESS: Blocked in handler on foreground thread (android.fg), Blocked in handler on main thread (main), Blocked in handler on ActivityManager (ActivityManager), Blocked in handler on PowerManagerService (PowerManagerService)
01-25 11:02:42.033 22774 22796 W Watchdog: android.fg annotated stack trace:
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12853)
01-25 11:02:42.037 22774 22796 W Watchdog: - waiting to lock <0x02af09e1> (a com.android.server.am.ActivityManagerService)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12810)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:2035)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.app.ContextImpl.bindService(ContextImpl.java:1958)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.bindService(ServiceConnector.java:343)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.enqueueJobThread(ServiceConnector.java:462)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.lambda$enqueue$1$com-android-internal-infra-ServiceConnector$Impl(ServiceConnector.java:445)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl$$ExternalSyntheticLambda2.run(Unknown Source:4)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Handler.handleCallback(Handler.java:942)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:99)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.038 22774 22796 W Watchdog: main annotated stack trace:
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.alarm.AlarmManagerService$AlarmHandler.handleMessage(AlarmManagerService.java:4993)
01-25 11:02:42.038 22774 22796 W Watchdog: - waiting to lock <0x0edf48f2> (a java.lang.Object)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:106)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.SystemServer.run(SystemServer.java:968)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.SystemServer.main(SystemServer.java:653)
01-25 11:02:42.038 22774 22796 W Watchdog: at java.lang.reflect.Method.invoke(Native Method)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:920)
01-25 11:02:42.038 22774 22796 W Watchdog: ActivityManager annotated stack trace:
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue.processNextBroadcast(BroadcastQueue.java:1154)
01-25 11:02:42.038 22774 22796 W Watchdog: - waiting to lock <0x02af09e1> (a com.android.server.am.ActivityManagerService)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue.-$$Nest$mprocessNextBroadcast(Unknown Source:0)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue$BroadcastHandler.handleMessage(BroadcastQueue.java:224)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:106)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.039 22774 22796 W Watchdog: PowerManagerService annotated stack trace:
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService.handleSandman(PowerManagerService.java:3257)
01-25 11:02:42.039 22774 22796 W Watchdog: - waiting to lock <0x0f550be5> (a java.lang.Object)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService.-$$Nest$mhandleSandman(Unknown Source:0)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService$PowerManagerHandlerCallback.handleMessage(PowerManagerService.java:5103)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:102)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.039 22774 22796 W Watchdog: *** GOODBYE!

可以很清楚的看到watchdog提示waiting to lock </xxxxx/> 关键词已经确定大概率是死锁问题了。
导出Anr日志继续进行分析,一般先从group="main"分析起


"main" prio=5 tid=1 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x71c515d0 self=0xb4000078c0a0abe0
| sysTid=2257 nice=-2 cgrp=foreground sched=0/0 handle=0x7a80db94f8
| state=S schedstat=( 67235248988 24733272110 200587 ) utm=3966 stm=2756 core=7 HZ=100
| stack=0x7fca926000-0x7fca928000 stackSize=8188KB
| held mutexes=
at com.android.server.am.ActivityManagerService.broadcastIntentWithFeature(ActivityManagerService.java:14615)
- waiting to lock <0x0185c3b1> (a com.android.server.am.ActivityManagerService) held by thread 16
at android.app.ActivityManager.broadcastStickyIntent(ActivityManager.java:4620)
at android.app.ActivityManager.broadcastStickyIntent(ActivityManager.java:4610)
at com.android.server.BatteryService.lambda$sendBatteryChangedIntentLocked$0(BatteryService.java:780)
at com.android.server.BatteryService.$r8$lambda$r64V5AVg_Okl7PnB1VjeN4oyo1I(unavailable:0)
at com.android.server.BatteryService$$ExternalSyntheticLambda5.run(unavailable:2)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at com.android.server.SystemServer.run(SystemServer.java:968)
at com.android.server.SystemServer.main(SystemServer.java:653)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:920)

main它在等待thread 16的lock <0x0185c3b1>,所以我们去tid=16再看看


"android.display" prio=5 tid=16 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x14340db8 self=0xb4000078c0a40a10
| sysTid=2349 nice=-3 cgrp=top-app sched=0/0 handle=0x76c8933cb0
| state=S schedstat=( 1805684706 1655206467 5034 ) utm=99 stm=81 core=7 HZ=100
| stack=0x76c8830000-0x76c8832000 stackSize=1039KB
| held mutexes=
at com.android.server.wm.ActivityTaskManagerService$LocalService.onProcessAdded(ActivityTaskManagerService.java:5740)
- waiting to lock <0x033a7977> (a com.android.server.wm.WindowManagerGlobalLock) held by thread 198
at com.android.server.am.ProcessList$MyProcessMap.put(ProcessList.java:699)
at com.android.server.am.ProcessList.addProcessNameLocked(ProcessList.java:2946)
- locked <0x09f229e4> (a com.android.server.am.ActivityManagerProcLock)
at com.android.server.am.ProcessList.newProcessRecordLocked(ProcessList.java:3039)
at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:2487)
at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:2854)
at com.android.server.am.ActivityManagerService$LocalService.startProcess(ActivityManagerService.java:17450)
- locked <0x0185c3b1> (a com.android.server.am.ActivityManagerService)
at com.android.server.wm.ActivityTaskManagerService$$ExternalSyntheticLambda11.accept(unavailable:27)
at com.android.internal.util.function.pooled.PooledLambdaImpl.doInvoke(PooledLambdaImpl.java:363)
at com.android.internal.util.function.pooled.PooledLambdaImpl.invoke(PooledLambdaImpl.java:204)
at com.android.internal.util.function.pooled.OmniFunction.run(OmniFunction.java:97)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.os.HandlerThread.run(HandlerThread.java:67)
at com.android.server.ServiceThread.run(ServiceThread.java:44)

可以看到waiting to lock <0x033a7977>,它在等待threa198的锁释放。因此再去tid=198看看


"binder:2257_1C" prio=5 tid=198 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x143443a0 self=0xb4000078c0bb49f0
| sysTid=6550 nice=-10 cgrp=foreground sched=0/0 handle=0x7638d60cb0
| state=S schedstat=( 12455767235 6173702511 27824 ) utm=695 stm=550 core=4 HZ=100
| stack=0x7638c69000-0x7638c6b000 stackSize=991KB
| held mutexes=
at com.android.server.display.DisplayManagerService.setDisplayPropertiesInternal(DisplayManagerService.java:2019)
- waiting to lock <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot) held by thread 130
at com.android.server.display.DisplayManagerService.-$$Nest$msetDisplayPropertiesInternal(unavailable:0)
at com.android.server.display.DisplayManagerService$LocalService.setDisplayProperties(DisplayManagerService.java:3811)
at com.android.server.wm.DisplayContent.applySurfaceChangesTransaction(DisplayContent.java:4878)
at com.android.server.wm.RootWindowContainer.applySurfaceChangesTransaction(RootWindowContainer.java:1022)
at com.android.server.wm.RootWindowContainer.performSurfacePlacementNoTrace(RootWindowContainer.java:824)
at com.android.server.wm.RootWindowContainer.performSurfacePlacement(RootWindowContainer.java:785)
at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacementLoop(WindowSurfacePlacer.java:177)
at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:126)
at com.android.server.wm.WindowManagerService.relayoutWindow(WindowManagerService.java:2501)
- locked <0x033a7977> (a com.android.server.wm.WindowManagerGlobalLock)
at com.android.server.wm.Session.relayout(Session.java:253)
at com.android.server.wm.Session.relayoutAsync(Session.java:267)
at android.view.IWindowSession$Stub.onTransact(IWindowSession.java:757)
at com.android.server.wm.Session.onTransact(Session.java:178)
at android.os.Binder.execTransactInternal(Binder.java:1285)
at android.os.Binder.execTransact(Binder.java:1244)

198在等待130来释放锁,waiting to lock <0x0faa6d02>,不要嫌麻烦再跟去130


"binder:2257_4" prio=5 tid=130 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x15288e48 self=0xb4000078c0b32400
| sysTid=2541 nice=0 cgrp=foreground sched=0/0 handle=0x76489a8cb0
| state=S schedstat=( 8323462229 6484203506 25835 ) utm=482 stm=349 core=7 HZ=100
| stack=0x76488b1000-0x76488b3000 stackSize=991KB
| held mutexes=
at com.android.server.am.ActivityManagerService.registerReceiverWithFeature(ActivityManagerService.java:13285)
- waiting to lock <0x0185c3b1> (a com.android.server.am.ActivityManagerService) held by thread 16
at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1816)
at android.app.ContextImpl.registerReceiver(ContextImpl.java:1750)
at android.app.ContextImpl.registerReceiver(ContextImpl.java:1738)
at com.android.server.display.DisplayPowerController.<init>(DisplayPowerController.java:674)
at com.android.server.display.DisplayManagerService.addDisplayPowerControllerLocked(DisplayManagerService.java:2710)
at com.android.server.display.DisplayManagerService.handleLogicalDisplayAddedLocked(DisplayManagerService.java:1561)
at com.android.server.display.DisplayManagerService.-$$Nest$mhandleLogicalDisplayAddedLocked(unavailable:0)
at com.android.server.display.DisplayManagerService$LogicalDisplayListener.onLogicalDisplayEventLocked(DisplayManagerService.java:2811)
at com.android.server.display.LogicalDisplayMapper.sendUpdatesForDisplaysLocked(LogicalDisplayMapper.java:759)
at com.android.server.display.LogicalDisplayMapper.updateLogicalDisplaysLocked(LogicalDisplayMapper.java:733)
at com.android.server.display.LogicalDisplayMapper.handleDisplayDeviceAddedLocked(LogicalDisplayMapper.java:580)
at com.android.server.display.LogicalDisplayMapper.onDisplayDeviceEventLocked(LogicalDisplayMapper.java:201)
at com.android.server.display.DisplayDeviceRepository.sendEventLocked(DisplayDeviceRepository.java:214)
at com.android.server.display.DisplayDeviceRepository.handleDisplayDeviceAdded(DisplayDeviceRepository.java:158)
- locked <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot)
at com.android.server.display.DisplayDeviceRepository.onDisplayDeviceEvent(DisplayDeviceRepository.java:87)
at com.android.server.display.DisplayManagerService.createVirtualDisplayLocked(DisplayManagerService.java:1418)
at com.android.server.display.DisplayManagerService.createVirtualDisplayInternal(DisplayManagerService.java:1381)
- locked <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot)
at com.android.server.display.DisplayManagerService.-$$Nest$mcreateVirtualDisplayInternal(unavailable:0)
at com.android.server.display.DisplayManagerService$BinderService.createVirtualDisplay(DisplayManagerService.java:3180)
at android.hardware.display.IDisplayManager$Stub.onTransact(IDisplayManager.java:659)
at android.os.Binder.execTransactInternal(Binder.java:1280)
at android.os.Binder.execTransact(Binder.java:1244)

130在等待16释放锁,这下子就明朗了,画个它们之间的关系图,这样就很明朗了他们三个之间死锁引起main主线程等待超时。


graph TD
binder2257_4 --> binder2257_1c --> android.display --> binder2257_4 & main主线程

前面这里只是分析出问题原因。下面来说问题为什么会发生。从main主线程开始追到binder2257_4然后开始循环。合理怀疑问题是先从binder2257_4出现的。都是binder是因为他们之间是用binder通信的。
看到binder2257_4这个anr日志中出现


at com.android.server.display.DisplayPowerController.<init>(DisplayPowerController.java:674)

竟然是在displaypowercontroller init中出现的,去到这个文件的674行,发现这里在注册广播接收器。


image.png


尝试退回这条提交之后果然问题没有复现了,分析这条提交,它是想在displaypowercontroller中注册一个广播接收器。用于接收挂电话的时候发出的广播。当收到这条广播之后就会通知displaypowercontroller亮屏。其实就是实现了一个通话时对方挂断电话之后自动亮屏。它是在创建阶段进行的注册。要分析为什么这个会导致死锁。


作者:用户8081391597591
来源:juejin.cn/post/7353158088730165259
收起阅读 »

调了个方法导致接口变慢好多,真实原因有点坑。

前言 这篇文章是按照我的记忆梳理的,然后解决方法其实很简单,主要是梳理一下当时的排查思路,所以请大家多多指教。错误之处或表述不清楚的地方,欢迎评论指出或建议,感谢。 背景 事情发生在一个正常的下午,领导对我说:有项目组汇报说一个创建的接口变的比较慢,让我...
继续阅读 »

前言



这篇文章是按照我的记忆梳理的,然后解决方法其实很简单,主要是梳理一下当时的排查思路,所以请大家多多指教。错误之处或表述不清楚的地方,欢迎评论指出或建议,感谢。



背景



事情发生在一个正常的下午,领导对我说:有项目组汇报说一个创建的接口变的比较慢,让我尽快排查一下。我一听,这个功能不是我写的,心里首先放松一下,那就排查吧!



验证线上


首先,当然是和领导确认,什么情况下,访问这个接口慢,因为有时候可能在某个特殊的场景下才会导致此类问题,或者是偶发情况,确定了这一点,也方便排查,然后我就按照领导说的,找到所属的数据页面,进行同样的操作。发现果然比较慢(这是我工作中学到的,不管别人怎么说,自己验证一遍),F12看了下,返回时间差不多快2s了,这个速度确实是有点慢的,因为这个添加的处理逻辑按理来说是不复杂的,不应该这么慢。


本地排查


接下来,就是排查的步骤,首先本地先启动一下,连接测试库看看是否有慢的问题,如果本地也慢,就比较方便一些,可以通过日志来打印每一部分花费的时间(我一般用StopWatch类),就可以知道哪里慢了。但是遗憾的是本地没有复现这种情况。


代码排查


既然无法复现,就只能看代码了,对比了下涉及到这个文件的提交记录,大概伪代码如下:


...
...
String name = xxxService.getMember(name);
...
Project project = prjService.getProject(code);
...
project.getProjectId();
...
...

看见的第一眼,我直接忽略了这一段,觉得没啥问题啊,然后事实证明,事出反常必有妖。


最终原因


其实慢的原因就是我直觉忽略了的这一段代码中的Project project = prjService.getProject(code);,当我点进去后我才发现,里面别有洞天,我没法给大家复制代码,还是给大家一个伪代码:


....
Project project = prjDao.getObject(code);
String pId = project.getProjectId();
List<Task> list = prjDao.queryTaskList(pId);
for (Task task : list){
String taskId = task.getTaskId();
Work w = prjDao.getObject(taskId);
....
s = s.apend(w.getName());
}
project.setExtend(s);
....


以上代码存在的问题主要就是在for循环中进行对数据库的查询,如果list的长度很长的话,就会导致查询慢的问题,当时发现的时候真的是无语了,你如果有扩展,可以在方法中进行清晰的标识(比如:queryprojectExtend),并且代码也不必这么写,可以提取所有的taskId到一个list中,然后作为参数进行查询。


另外还有一个坑的点就是,其实原来的人调用这段方法本身其实就是为了拿一个project.getProjectId();,所以完全可以另外写一个简单获取的方法即可。


解决并验证


因为知道了原因,所以从数据库找了数据比较多的一个来进行复现,果然复现成功,也证明了问题就是出在这里。
首先把代码进行修复,修复后的伪代码如下:


....
Project project = prjDao.getObject(code);
String pId = project.getProjectId();
List<Task> list = prjDao.queryTaskList(pId);
List<String> idList = list.getIdLIst(list);
List<Work> works = prjDao.queryObjectsByIds(idList);
for (Work task : list){
s = s.apend(w.getName());
}
project.setExtend(s);
....

以上虽然也使用了for循环,但是内部只是做了字符串的拼接,也可使用Stream,会更简洁。
更改完后,继续测试该接口,发现是正常的。


思考总结



其实这次的问题很简单,没有涉及到高大上的一些问题来调整,要避免其实也很简单,调用其他人写的方法时,大概点进去观察一下。写方法的时候多想一下,是否会造成查询慢的问题,就可以了。




  • for循环查询这种情况,尽量避免。

  • 调用他人方法时需要知道内部大概的逻辑,切勿望文生义。

  • 不要想当然,觉得没问题就不排查。


致谢


感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。


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

脱敏工具?整个全局的吧

咱又手痒造轮子啦!Hutool工具包有这个一个类DesensitizedUtil实现了一些常见业务数据类型的脱敏,像手机号,中文名,身-份-证号,银彳亍卡号等。那咱就基于它写一个全局切面,需要脱敏的用注解标识,思路有了说干就干。 咱先定义一个切入点注解@Dat...
继续阅读 »

咱又手痒造轮子啦!Hutool工具包有这个一个类DesensitizedUtil实现了一些常见业务数据类型的脱敏,像手机号,中文名,身-份-证号,银彳亍卡号等。那咱就基于它写一个全局切面,需要脱敏的用注解标识,思路有了说干就干。


咱先定义一个切入点注解@DataDesensitized


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDesensitized {
}

然后咱再定义一个注解标识字段脱敏@Desensitized


@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
//加上hutool类中的脱敏类型枚举选择脱敏策略
DesensitizedUtil.DesensitizedType type();
}

最后写切面类


@Aspect
@Component
@Slf4j
public class DataDesensitizedAspect {
@AfterReturning(pointcut = "@annotation(dd)", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, DataDesensitized dd, Object result) {
//TODO 这里可以根据组织架构角色控制是否脱敏
boolean need = true;
if (!need) {
return;
}
//方法响应一般有分页实体,list集合,单实体对象,那就分类处理
if (result instanceof PageInfo) {
PageInfo page = (PageInfo) result;
List records = page.getList();
for (Object record : records) {
objReplace(record);
}
} else if (result instanceof List) {
List list = (List) result;
for (Object obj : list) {
objReplace(obj);
}
} else {
objReplace(result);
}
}

public static <T> void objReplace(T t) {
try {
Field[] declaredFields = ReflectUtil.getFields(t.getClass());
for (Field field : declaredFields) {
Desensitized des = field.getAnnotation(Desensitized.class);
//被脱敏注解修饰且string类型
if (des != null &&
"class java.lang.String".equals(field.getGenericType().toString())) {
Object fieldValue = ReflectUtil.getFieldValue(t, field);
if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
continue;
}
DesensitizedUtil.DesensitizedType type = des.type();
String hide = DesensitizedUtil.desensitized(fieldValue.toString(),type);
ReflectUtil.setFieldValue(t, field, hide);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

在业务方法上标识切入点注解


@Override
@DataProtection
public PageInfo<OrderDetailsVo> queryOrderDetails(QueryParam param) {
return mapper.queryOrderDetails(param);
}

vo实体中需要脱敏的字段加@Desensitized


@Data
public class OrderDetailsVo {
private String orderNo;
private String sn;

@Desensitized(type = DesensitizedUtil.DesensitizedType.CHINESE_NAME)
private String username;

@Desensitized(type = DesensitizedUtil.DesensitizedType.MOBILE_PHONE)
private String mobile;

@Desensitized(type = DesensitizedUtil.DesensitizedType.ID_CARD)
private String idCard;
}

完成!


次日,产品经理要求这个20位的sn字符串从第五位脱敏到第十八位,hutool工具没有这个类型的枚举!成!咱再把轮子改造一下


自己写一个脱敏策略枚举DesensitizedType,对比hutool只加了CUSTOM自定义脱敏类型


public enum DesensitizedType {
//自定义脱敏标识
CUSTOM,
//用户id
USER_ID,
//中文名
CHINESE_NAME,
//身-份-证号
ID_CARD,
//座机号
FIXED_PHONE,
//手机号
MOBILE_PHONE,
//地址
ADDRESS,
//电子邮件
EMAIL,
//密码
PASSWORD,
//中国大陆车牌,包含普通车辆、新能源车辆
CAR_LICENSE,
//银彳亍卡
BANK_CARD
}

@Desensitized改造


@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
//替换成自己定义的枚举
DesensitizedType type() default DesensitizedType.CUSTOM;

/**
* 当type不指定时,可自定义脱敏起始位置(包含)
*/

int startInclude() default 0;

/**
* 当type不指定时,可自定义脱敏结束位置(不包含) ,-1代表字符串长度
*/

int endExclude() default -1;
}

切面类改造


public static <T> void objReplace(T t) {
try {
Field[] declaredFields = ReflectUtil.getFields(t.getClass());
for (Field field : declaredFields) {
Desensitized des = field.getAnnotation(Desensitized.class);
//被脱敏注解修饰且string类型
if (des != null &&
"class java.lang.String".equals(field.getGenericType().toString())) {
Object fieldValue = ReflectUtil.getFieldValue(t, field);
if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
continue;
}
String value = fieldValue.toString();
String hide = "";
if (des.type() == DesensitizedType.CUSTOM) {
int startInclude = des.startInclude();
int endExclude = des.endExclude();
if (endExclude == -1) {
endExclude = value.length();
}
hide = StrUtil.hide(value, startInclude, endExclude);
} else {
DesensitizedUtil.DesensitizedType type =
DesensitizedUtil.DesensitizedType.valueOf(des.type().toString());
hide = DesensitizedUtil.desensitized(value, type);
}
ReflectUtil.setFieldValue(t, field, hide);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

实体标识脱敏字段


@Data
public class OrderDetailsVo {
private String orderNo;

@Desensitized(startInclude = 5,endExclude = 18)
private String sn;

@Desensitized(type = DesensitizedType.CHINESE_NAME)
private String username;

@Desensitized(type = DesensitizedType.MOBILE_PHONE)
private String mobile;

@Desensitized(type = DesensitizedType.ID_CARD)
private String idCard;
}

这下可以开开心心把轮子给小伙伴用啦开心😘😘😘


作者:开大会汪汪队
来源:juejin.cn/post/7348830480789962787
收起阅读 »

js检测网页空闲状态(一定时间内无操作)

web
1. 背景 最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。 网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。 2. 如何判断页面是否空闲 首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何...
继续阅读 »

1. 背景


最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。


网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。


2. 如何判断页面是否空闲


首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何操作,则当前网页为空闲状态。


用户操作网页,无非就是通过鼠标键盘两个输入设备(暂不考虑手柄等设备)。因而我们可以监听相应的输入事件,来判断网页是否空闲(用户是否有操作网页)。



  1. 监听鼠标移动事件mousemove

  2. 监听键盘按下事件mousedown

  3. 在用户进入网页后,设置延时跳转,如果触发以上事件,则移除延时器,并重新开始。


3. 网页空闲检测实现


3.1 简易实现


以下代码,简单实现了一个判断网页空闲的方法:


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;

const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
onClearTimer();
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

也许你注意到了,我并没有针对onStartTimer事件进行防抖,那这是不是会对性能有影响呢?


是的,肯定有那么点影响,那我为啥不添加防抖呢?


这是因为添加防抖后,形成了setTimeout嵌套,嵌套setTimeout会有精度问题(参考)。


或许你还会说,非活动标签页(网页被隐藏)的setTimeout的执行和精度会有问题(参考非活动标签的超时)。


确实存在以上问题,接下来我们就来一一解决吧!


3.2 处理频繁触发问题


我们可以通过添加一个变量记录开始执行时间,当下一次执行与当前的时间间隔小于某个值时直接退出函数,从而解决这个问题(节流思想应用)。


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
// 记录开始时间
let beginTime = 0;
const onStartTimer = () => {
// 触发间隔小于100ms时,直接返回
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
// 更新开始时间
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

3.3 处理页面被隐藏的情况(完整实现)


我们可以监听visibilitychange事件,在页面隐藏时移除延时器,然后页面显示时继续计时,从而解决这个问题。


/**
* 网页空闲检测
* @param {() => void} callback 空闲时执行,即一定时长无操作时触发
* @param {number} [timeout=15] 时长,默认15s,单位:秒
* @param {boolean} [immediate=false] 是否立即开始,默认 false
* @returns
*/

const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
let beginTime = 0;
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const onPageVisibility = () => {
// 页面显示状态改变时,移除延时器
onClearTimer();

if (document.visibilityState === 'visible') {
const currentTime = Date.now();
// 页面显示时,计算时间,如果超出限制时间则直接执行回调函数
if (currentTime - beginTime >= timeout * 1000) {
callback();
return;
}
// 继续计时
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000 - (currentTime - beginTime));
}
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
document.addEventListener('visibilitychange', onPageVisibility);
};

const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
document.removeEventListener('visibilitychange', onPageVisibility);
};

const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

通过以上代码,我们就完整地实现了一个网页空闲状态检测的方法。


4. 扩展阅读


chrome浏览器其实提供了一个Idle DetectionAPI,来实现网页空闲状态的检测,但是这个API还是一个实验性特性,并且Firefox与Safari不支持。API参考


作者:求知若饥
来源:juejin.cn/post/7344670957405405223
收起阅读 »

为什么马斯克能扭曲现实

好多人看完Elon的新传记开始扯现实扭曲立场,聊虚头巴脑的东西都没用,关键是为什么Elon有这种来扭曲现实的能力。   马斯克手下的工程师的bar都非常高,早期在SpaceX为了不雇二流工程师宁愿自己上,想要push下面这些人拼命工作,要没点真本事基本不可能,...
继续阅读 »

好多人看完Elon的新传记开始扯现实扭曲立场,聊虚头巴脑的东西都没用,关键是为什么Elon有这种来扭曲现实的能力。


 


马斯克手下的工程师的bar都非常高,早期在SpaceX为了不雇二流工程师宁愿自己上,想要push下面这些人拼命工作,要没点真本事基本不可能,分享几个例子(在实际的工程和制造过程中大概率是常态):


 


Gega Factory流水线上,本来用6颗螺丝固定的地方,Elon觉得4颗就够了,下面的工程师解释说再少的话强度不够,Elon说他在脑子里模拟了一下冲压强度,感觉4颗螺丝就顶得住,改完后一切正常运行,这种判断需要大量的工程经验和物理直觉。


 


SpaceX开始制造星舰的时候,火箭材料的选择上争议很多,团队告诉他用不锈钢会比用猎鹰9号上的锂铝合金或者碳纤维更重,Elon的直觉告诉他不是这样,要求团队把具体数字算出来,结果Elon是对的,而且不锈钢在极寒条件下强度会增加50%,更适合装载超低温液氧和液氮,再者他可以雇佣没有碳纤维经验的工人(用工的成本更低),更进一步不锈钢的熔点高,星舰的外壁不用额外设置隔热层,最后焊接还更容易(锂铝合金需要超净焊接环境,不锈钢直接露天大棚咔咔焊)。更细节的是,要确认星舰外壁的厚度时,一线的焊接工人给Elon的反馈是4.8mm,Elon觉得4mm就行,不顾一线人员的担忧拍板了,后来证明没问题。


 


Elon在SpaceX内部普及了一个概念叫"白痴指数",指的是一个零件或组件的总成本和原材料成本之间的比值。他在会上问下面的工程师根据⽩痴指数,猛禽发动机中做得最好的部分是什么,结果下面的人答不上来,试探性地答了一个喷管护套,说成本大概13000美元,由一种钢整块制成,Elon追问这种原材料的价格是多少,对方回答说大约几千美元,Elon立马纠正说200美元现场打脸并表示对方错的离谱。


 


现实扭曲立场是什么?就是你老板在你的工作领域很多地方比你还懂,角度比你还新奇,考虑比你还全面,对细节数据掌握比你还扎实,在过往的技术方向,工程方案,成本控制的争论中超出大家的预期,证明他更正确。假想一下这种老板要求员工把某个指标在某一期限内优化xx%,或者彻底拿出一个更好的方案,员工说不可能,然后老板自己住在工厂加班最后有理有据prove you wrong是什么感觉?下一次他再提一个看起来是扯淡的需求,团队就会觉得这是可能的,然后work hard on it,这就是现实扭曲立场。


 


如果leader本人没有这种身先士卒的精神,以及让最好的工程师都赞叹的专业能力,能扭曲个啥?喊几句cheap slogan谁都会


 


不是说Elon是神,他也会犯错误,比如2018年Model3产能爬坡的时候在工厂自动化上面做的过于激进,反而降低了效率(比如某些机械臂在搬运物件的时候出现意外导致整个产线停滞,用手工可靠得多),最后为了最大化生产效率又开始"去自动化"。类似地,在收购SolarCity早期,他疯狂push下面的人增加屋顶太阳能面板的安装效率,搞了半天也没什么成效后来不了了之。但这些错误都是可以接受的,都不妨碍他在团队塑造硬核文化然后drive整个公司往前走。


 


那些故事性很强的鸡汤鸡血都是扯淡,事实很简单,就是Elon在技术和商业上认知几乎比所有人都好。


作者:数据智能老司机
来源:juejin.cn/post/7351430820715266074
收起阅读 »

生命里每一段记忆,可能都伴随着DNA的断裂与修复

周末的时候刷到一篇论文,可能揭示了人类长期记忆的形成机制。而这机制的背后似乎是生命的本质。 Nature上的新研究 这是一篇新鲜热乎的Paper,3月27号刚发的。(P.S.:Paper原地址我放原文链接里了) 图源:Nature ...
继续阅读 »

周末的时候刷到一篇论文,可能揭示了人类长期记忆的形成机制。而这机制的背后似乎是生命的本质


Nature上的新研究


这是一篇新鲜热乎的Paper,3月27号刚发的。(P.S.:Paper原地址我放原文链接里了)



图源:Nature

图源:Nature


这个题目是说记忆构成和TLR9通路中的DNA感知的关系





不是说最近大模型Kimi很火,支持200万字无损上下文了么,于是我直接上Kimi试一下。



图源:Kimi

图源:Kimi


嗯,上传上去稍微问一下整体架构和结论,还是挺有意思的。说的似乎是记忆和DNA的关系。但来来回回问Kimi感觉还是挺花时间,算了还是自己看看吧。


我尽量试着给大家解释解释。


长期记忆和DNA损伤


这篇Paper里的核心是一个反直觉的结论是:在人类大脑海马体形成记忆的过程中,一些细胞会产生损伤,尤其是双瓣DNA会产生断裂和重组


也就是说,记忆的产生本身对人体来说,并不是获得,而是损伤。


是不是非常反直觉?


这个Paper里的研究思路很明确。结论也非常直接。


就是在记忆形成的短期内(大概1-2个小时内),大脑中负责记忆的部分会经历一个“破坏的过程”。这个过程堪称惨烈。比如双链DNA会产生断裂、细胞的瓣膜会产生破裂,甚至还有一些细胞核里的组蛋白会流到细胞核外,同时伴随着一些“炎症反应”。



图源:Nature

图源:Nature


很多人看到“炎症反应”会觉得是发炎,但其实不完全是。学术上的炎症反应指的是“损伤、抗损伤和修复的动态过程”。


那么,在记忆产生这个撕裂的过程中的炎症反应里扮演重要角色的就是这个论文的主角:TLR9


它是一个特定的免疫机制,正是它在这个过程里去修复DNA断裂,修补破裂的细胞并且应对炎症反应。



图源:工作细胞

图源:工作细胞


这也就是说,每一次记忆的形成,都是一个DNA断裂、细胞破损、然后TLR9处理炎症反应、最后身体恢复正常的一个过程。


研究人员还尝试了移除TLR9。于是发现在没有TLR9的情况下是无法形成长期记忆的,也就是说这个作用链条没有TLR9的参与就没法闭环。


听起来这有点像健身锻炼肌肉,肌肉的增长前提是轻微肌肉撕裂


但不同的是,对于锻炼肌肉而言,只有在训练和运动的时候才会出现这样的情况,并且是轻微肌肉撕裂。对于记忆过程中这都不是轻微撕裂了,这是双链DNA断链再重组。


锻炼=断链了属于是。


Viva La vida


我并不是一个专业的医学生,也不是生物专业的研究人员。我只是看到这篇Paper,突然对人类的生命再一次肃然起敬。


我们都觉得体育运动和锻炼带来的酸爽是一种正向反馈,因为这是你的身体在经受一次又一次的挑战与自我修复。


但是在我们看不见的地方,在你记住一些知识、一些任务和一些事情的时候,每一分每一秒身体里都有数以万计的细胞再断裂、产生炎症、再重组。


涅槃在时时刻刻都确实发生着。原来不思考的生命就没有意义,这不是一句鸡汤。


生命的自然机制本身就在挑战、摧毁并重塑自己。我的身体比我自己对自己可狠多了。


Viva la vida是一句西班牙语,意思是生命万岁/致敬生命。



图源:Google

图源:Google


这幅画的名字就叫做《Viva La Vida》,受这幅画和一些历史故事的启发,ColdPlay乐队创作了他们的经典专辑《Viva La Vida》.


所以现在我知道了,那些所谓刻在脑子里的事情,是真的literally的“”在了我的生命里。


作者:wayne3200
来源:mdnice.com/writing/84234c4f5f1c479a920b9103bf1f09f3
收起阅读 »

研究生真的会勾心斗角嘛?

背景 我感觉我是一个有点大大咧咧的人,有时候看不出来别人对我耍心眼,大学期间也有几次是事后才反应过来,而且我很容易对别人真心相待,我觉得我对他好他也会对我好,起码不会坏我,但是并不是这样,最近被好多研究生勾心斗角的言论吓到了,真的很怕 编者语 但什么东西...
继续阅读 »

背景


我感觉我是一个有点大大咧咧的人,有时候看不出来别人对我耍心眼,大学期间也有几次是事后才反应过来,而且我很容易对别人真心相待,我觉得我对他好他也会对我好,起码不会坏我,但是并不是这样,最近被好多研究生勾心斗角的言论吓到了,真的很怕


编者语


但什么东西有利益分配的时候,那么心眼就已经在耍起来了。


大家虽然,但也不会有啥坏心眼,不至于为了自己利益给其他人使绊子。


所以,放宽心,一切都是纸老虎。勇敢面对。


对了。多数人都很单纯,大家都是好朋友,以心换心,你是什么样的,你身边的人也会是什么样的。





把“嘛”字去掉





答案是肯定的,研究生真的会勾心斗角!


有人的地方就会有江湖,你的寝室就四个人还会有好几个小群,更何况一个有好多人的课题组呢?


举个例子:某校大牛课题组学生加职工共计100人,其中小导5个,博士10个,科研助理类工作人员3个,剩下的都是硕士和本科实习生。


大导分配任务给小导的时候会把同一个课题给到两个人,这不就出现矛盾了么?这两个人都需要成果,肯定谁做的快谁能留下,那他们带的研究生不就被迫跟着站队了么?难道你敢私下跟另一个导师的同学说你们组课题进展?或者说实验计划?


加上课题组内部的博士名额有限,想要读博的硕士本身就有很大的竞争关系,这不要靠谁的科研成果多,或者说谁能更得导师的心来决定了?


然后就是奖学金啊、课题组里面的资源啦、实验排期啦、仪器使用啦………能产生利益冲突的不要太多了好不好。


没有永远的朋友,只有永远的利益不仅仅适用于国家之间的关系,在职场或者读书阶段也是如此。


9个女生,1个男生


我们组研123年级一共9个女生,1个男生。1女博5。勾心斗角情况数不胜数。尤其是读到博士阶段且在校时间很长的尸姐(兄),及其擅长利用师弟师妹帮其做实验push 你控制欲极强,然后和老板聊天笑着就把你告了。说你经常找不到人做实验粗去玩。。。


大虎不怕,苍蝇难防


大部分人都是好的。但难免遇到一两个苍蝇。一个人能做到让组内的其他人都对他有意见有看法,是真的不容易。


大勇若怯,大智如愚。


会有的,而且同门有可能会成为打到你的主谋。我读研的时候,当年考研初试复试第一的女生就因为锋芒尽显,在上课做pre和各种提问中锋芒毕露,在组会上也比较积极,给人一种远超同门的气场。结果就被同门联合她的舍友诱导说了一些质疑导师的话,被录音给了她的导师,直接被逼退学,后面运作了好久才转组成功(也没有科研前途了,去给别人当耗材牛马去了,成果不给她的那种)。


大勇若怯,大智如愚。我反正就不会在没啥用的课堂演讲上锋芒毕露或者提问把同学问的下不来台。自己学点喜欢的东西,不争不抢,期末考试考好点,科研做好点,能在没人指导和导师起副作用情况下能写出大小论文通过答辩就行了。


研究生是否勾心斗角甚至是导学关系是否勾心斗角很大程度上取决于学院的整体生态和科研环境。如果你的学院和课题组大部分人都容易沟通、实事求是、科研风气积极向上,那必然不需要花很多时间处理所谓的人际关系。但如果你所在的学院和课题组学风不正,大部分事情讲的是丛林法则,凡事一看关系二看派系三看资源的。那大概率是要花很多时间在人际关系、派系斗争和做局坑人上。毕竟你勤勤恳恳做1000个小时得到的结果,可能别人花2小时编一个谣言或者给导师拍拍马屁就可以直接抢走,换谁会愿意真正的干活呢?


恶心


自己的idea被实验室同学拿去发了论文,顶会,作者没有我,甚至都没有收到一句感谢,并且所有场合提到这个idea的时候都变成了他的,老师不知道会当着我的面说那个同学idea怎么怎么好可以用在什么什么上拓展一下,我该如何自处?!实验室的同学还劝我大度,我大度你个鬼啊!不知道算不算恶心?


还有人明明知道某个代码怎么回事,还故意告诉你错的,实验室有锅往你身上甩,有功劳自己抢的可快了,肆无忌惮的的欺骗,偷抢功劳,算不算恶心?


本人一直秉着实验室都是好同学的思想,大家有求一定100%努力相助,从不欺骗谁,不抢别人功劳,觉得实验室应该互相帮助,一起科研进步,最后却还是遇到上述那些人,呵呵呵。


最难的地方是老板


有感而发。


说几句吧。


国外实验室呆过,感觉就是没有什么勾心斗角。


一个团队的人都很乐于互相帮助。


发文章一块发,有工作贡献就可以。当然主要作者都是博士生。


实验室的人际关系,很简单,很和谐。从无欺负人现象。


听说,国内什么都很卷。


就想说说,到底什么是卷。


有的人说,是蛋糕不够分。


有的人说,是美国卡脖子。


什么的。各种说法都有。


看过各位的回答,有点明白到底什么是内卷了。


就是一个实验室的人,没有爱心,也缺乏互帮互助的精神。


当然,很多精力就要花费在维护人际关系,维护实验室基本操作规范方面了。


这个比较普通与简单的方面,其实都没人重视,也不认真去履行。


实际上,是实验室人员缺乏职业道德,缺乏职业精神。


另外,感觉国内实验室人员缺乏主人翁意识,别人的事情不管,其实别人的事情很大程度也是与自己有关的。因为一个实验室嘛,互帮互助,这是人性。


还有,精力浪费在人际关系方面,就是没有很多精力用于科技研发,科研想法的创新了。


这是个人理解的所谓内卷。


我觉得不是硬件的问题,是软件的问题,就是人之根本的问题。


科学的发展,其实是人的发展。


科学之所以在西方,欧美发展的繁荣,也是因为这些地方,首先是人得到了发展。


人性得到了发展,人的本能得到了发挥与发扬。


人有哪些本能呢?


作为国外观察,我觉得人的本能有很多就是根本性的,却在国内一些实验室可能严重缺乏了。


比如,敬业,负责,有爱心,互帮互助,团队精神,自我批判与质疑,对待他人宽容与谅解,等等。


这些人的本能是维护一个实验室正常高效运转的基本要素,实验仪器其实反而是次要。


实验室里最宝贵的财富,是人。仪器什么的,都可以造,买,借。


只有人,如果本能受到了破坏,则就很难修复。想让一个不负责任各种撒谎的人突然就负责然后特别的敬业严谨,可能需要好几年的培养。而一个出了问题的仪器,花钱修理或者直接购买就行了。


可见,人的本能,难以重建与塑造。


那么发生在一个实验室里的各种内卷情况,则会层出不穷。


一个人的环境良好的实验室,哪怕实验仪器老旧,只需要多付出一点精心维护,实验室还是可以正常运作的。甚至可以发挥出更大的价值。


一个人的环境不好的实验室,哪怕是几百万几千万的大价钱购买的顶尖仪器,恐怕也难以得到足够的维护与修养,从而得不到一个实验仪器应有的功效。


那么如何解决实验室勾心斗角难题呢?


我觉得挺难的,不过还是可以尝试一些办法去解决。


第一,多去国外实验室打工,交流,学习。谦虚点,多观察,尤其多观察国外实验室他们如何管理的。人与人如何搞关系的。肯定和国内不一样。另外,不要觉得在国外,类似国内不好的事情也一样发生,就对国外实验室或科研产生看不起瞧不上的心态,这样只能自己心浮气躁,什么都学不到。最多混个履历或者文章。


第二,提高一下心性。其实冥想之类的挺管用。别人的批评听听,多吸取积极的批评,负面的批评就早点忘记,不要影响自身的积极性。


第三,多阅读一些有关,心灵,精神,方面的书籍。最好还是看国外翻译的。国内很多都是比较假的鸡汤。


第四,组织实验室共同学习,共同进步,多互相帮助,做事有规则有原则,就可以减少很多根本不必要的勾心斗角与内卷。


第五,以身作则,遇到原则问题需要自己去维护自己的利益,同时,要影响实验室其他成员给积极的影响。


当然,最难的部分实际上是实验室老板。这个,我们就无法解决了。如果老板愿意听你的,可以自己去与老板谈谈如何更好构建实验室。也是一个办法。


如果老板固执的不行,只能换地方了。


往大了说,这个内卷问题其实是一个文化问题。


这就太难解决了。谁也解决不了。


到了自己头上


本来以为自己不会有这样的问题的,一年以后发现实验室可真是卧虎藏龙小型帮派现场啊。


实验室大概有两个势力集团,一靠权力,二靠暴力,剩下我们普通人苦苦挣扎,现在新生还好真怕和他们的师兄师姐变得一样。作为有很多老师的大课题组,财产掌握在刚刚留校的实验室大师兄手中,暴力是指实验室有两位博士(男女朋友)逐渐将财产私有化专门化并欺压师弟师妹们。还有其它普通人分在别的老师名下我觉得我们同病相怜惨兮兮。还有师妹实验数据的文件夹被整个删了不知道是谁。


先说大师兄手下的特权,因为大师兄享有采购权所以在导师极度抠门的情况下,资源优先分配到人家那里。一个实验室手套都不能随时供应你敢信?三角瓶也不够要到处借,洗衣粉没了也不及时买(我去一楼借洗衣粉人家说只有洗衣液留下了羡慕的泪水。)试剂随时就没了,还有各种耗材没有是正常操作,(称量纸没了撕笔记本,洗手液没了就不用,甘油管没了找别的实验室借,枪头没了洗洗灭菌,橡皮筋没了把断了的打个结继续用,冰箱保菌装甘油管盒子没了拿硬纸盒装)


但是到了大师兄手下就不一样了,反正我一问就是没有了买了还没回来,但是人家就有一箱一箱的新三角瓶,随时可用的手套,有充足理由实验需要采购各种东西。还有年度大会两三千的奖励为实验室做贡献而其它师兄师姐比如负责每晚安全检查和新生培训的就没有。


那两口子不属于大师兄管但是是博士入学久,所以就自己给自己特权。两口子有自己专属的超净工作台,工作台要预订,我们也可以用但是只要人家要用没定也会在旁边赶你。PCr要预订,一个学弟延伸时间久,师姐就直接把他的PCr管扔了出去扔了出去还给负责学弟的师兄(有事不在学校)打电话骂了半个小时问反应体系延伸时间是放迟了还是超时了,此时还有半小时反应完成。他们组所有公用的东西自己都会私藏一份作为人家专用的东西,小到量筒,浮漂,放三角瓶的框,大到蛋白胶的电泳槽,写了他们名字的别人都不能用,用完放自己柜子里,→_→可是这些都是公用的。至于抢别人定的摇床也是正常操作,以至于他们带的本科生师弟也飞扬跋扈,在下午抢了一个博士师姐的电泳槽说他着急看结果,师姐生气说着急为什么不中午看才立马道歉。两口子的厚脸皮程度就连大师兄也要退让三分,更何况他们还会在大组会上怼大老板自己的导师你敢信?导师给提意见会打断说自己知道。


不过两口子的实验能力有目共睹,师姐洗瓶子要洗到没有肉眼可见的一点点污渍反复清洗十几遍不聚成水滴也不成股流下,这一点是我们做不到的。所以两口子实验效率特别高。


作者:时问桫椤
来源:mdnice.com/writing/d841b81020b94d57821fa484883a2e14
收起阅读 »

灰度发布策略在前端无损升级中的应用

web
为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等...
继续阅读 »

为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等头部来控制、管理、检测这类资源对缓存机制的使用情况。同时,为了使新版本的js、css等资源立即生效,一种比较通行的做法是为js、css这些文件名添加一个hash值。这样当js、css内容发生变化时,浏览器获取的是不同的js、css文件。在这种情况下,旧版本的index.html文件可能是这样的:


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.4656e35c1e8d4345f5bf.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-4656e35c1e8d4345f5bf.js></script>
</body>
</html>

当项目的js、css内容发生了变化时,新版本的index.html文件内容变成这样的(js和css文件名携带了新的hash值1711528240049):


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.1711528240049.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-1711528240049.js></script>
</body>
</html>

因为index.html文件一般会设置为不缓存,这样用户每次访问首页时,都会从web服务器重新获取index.html,然后根据index.html中的资源文件是否变化,从而决定是否使用缓存的文件。这样既能让用户立即获取最新的js、css等静态资源文件,又能充分地使用缓存。index.html的响应头大概长这样:


847fa51ec16bbfa14a5865482b23f396.png


但是为了保证系统的高可用,web后端往往由多个实例提供服务,用户请求会在多个服务实例间进行负载均衡。而系统升级过程中,会存在多个版本共存的现象。这时,如果用户从旧版本实例上获取了index.html文件,然后再去获取旧版本的js、css文件(main-4656e35c1e8d4345f5bf.jsmain.4656e35c1e8d4345f5bf.css),但是请求却分发到了新版本服务实例上,这时因为新版本服务实例只有main-1711528240049.jsmain.1711528240049.css文件,就会导致访问失败。反过来,如果从新版本实例上获取了index.html文件,在请求相应的js、css文件时,也可能被分发到旧版本实例上,也导致访问失败。


解决方法:


1)首先,改造一下index.html文件中引用js、css等静态资源的路径,添加一个版本号,如v1、v2,这样index.html文件对js、css的引用变为:


<link href=/static/v1/css/main.1711528240049.css rel=stylesheet>
<script type=text/javascript src=/static/v1/js/main-1711528240049.js></script>

2)使用灰度发布策略升级系统,具体步骤如下(假设系统包含A、B两个服务实例)



  1. 升级前(稳态),在应用网关(代理)上配置路由策略V1,该路由策略的功能为:匹配路径前缀/static/v1的请求负载均衡分发到A、B两个服务实例

  2. 将待升级的服务实例A从路由策略V1中摘除掉,这时用户请求只会发送给实例B

  3. 待实例A上所有进行中的请求都处理完后,就可以安全的停掉旧的服务,替换为新的服务,这时还不会有请求分发到实例A

  4. 待实例B测试功能正常后,在应用网关(代理)上新增一条路由策略V2,该路由策略的功能为:匹配路径前缀/static/v2的请求分发到服务实例A。这时,从服务实例A上获取的index.html文件引发的后续js、css请求,都会分发到服务实例A,从服务实例B上获取的index.html文件引发的后续js、css请求,都会分发到服务实例B

  5. 继续将实例B从路由策略V1中摘掉,然后升级实例B,将实例B添加到路由策略V2中

  6. 所有的流量都切换到了路由策略V2中,下线路由策略V1。完成整个升级过程,实现了前端的无损升级


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

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

特效炸裂:小米 SU7 在线特效网站技术不完全揭秘!!!

web
哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!! 前言 最近一位叫 @GameMCU的大佬用 Webgl、Three.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu....
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!!



前言


最近一位叫 @GameMCU的大佬用 WebglThree.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu.com/su7/被广大网友疯传,效果相当炸裂!


网站首发当天由于访问量过大导致奔溃, 后来可能获得了某里云官方支持!!! 这一波真的要给某里云点赞!



更有网友评论: 这效果和交互完全可以吊打官方和各种卖车的网站了啊



并且 @小米汽车官方:求求了,收编了吧,这能极大提升小米su7的逼格,再用到公司其他产品,能提升整体公司的逼格



废话不多说,直接上效果!!!


效果展示



  • 模拟在汽车在道路行驶特效,宛如身临其境




  • 流线型车身设计,彰显速度与激情的完美融合。每一处细节都经过精心打磨,只为给你带来最纯粹的驾驶体验。




  • 在高速行驶的过程中,风阻是影响车速的重要因素。我们的特效模拟器通过先进的算法,真实还原了风阻对车辆的影响。当你长按鼠标,感受那股扑面而来的气流,仿佛置身于真实的驾驶环境中。




  • 雷达实时探测功能可以帮你轻松掌握周围车辆的情况,让你在驾驶过程中更加安心



视频


是怎么实现的


在线体验完@GameMCU大佬的网站之后, 我很好奇大佬是使用什么技术去实现的, 身为前端开发的我, 第一步当然是 F12 打开控制台查看



发现使用的是 Three.js r150 版本开发, 并且还用了一个叫 xviewer.js 的插件,


于是乎我找到了@GameMCU大佬的 github 主页, 在主页中介绍了 xviewer.js:



xviewer.js是一个基于 three.js 的插件式渲染框架,它对 three.js 做了一层简洁优雅的封装,包含了大量实用的组件和插件,目标是让前端开发者能更简单地应用webgl技术。



比较遗憾的是 xviewer.js 目前还没有开源, 不过按照作者的意思是可能会在近期开源。


虽然目前 小米 SU7 在线体验网站没有开源, 但是作者主页开源了另外一个项目: three.js复刻原神启动, 也是一个基于 xviewer.js 开发的在线网站。


通过源码发现作者在项目中写了大量的 Shader, Shader 对于实现复杂的视觉效果和图形渲染技术至关重要,它们使得开发者能够创建出令人印象深刻的3D场景动画



Shader 是一种在计算机图形学中使用的程序,它运行在图形处理单元(GPU)上,用于处理渲染过程中的光照、颜色、纹理等视觉效果。


Shader 通常被用于 3D 图形渲染中,以增强视觉效果,使得图像更加逼真和吸引人。


在 Three.js 中, Shader 通常分为两类:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。


顶点着色器负责处理顶点数据,如位置颜色纹理坐标等,而片元着色器则负责处理像素颜色,包括光照材质属性


总之,Shader 在 Three.js 中扮演着至关重要的角色,它们为开发者提供了强大的工具来创建丰富、动态和引人入胜的 3D 图形内容。通过学习和掌握 Shader 编程,开发者可以极大地扩展 Three.js 的应用范围和创作能力。


那么作为一名前端开发人员, 应该怎么快速入门 Shader, 并且用 Shader 创造令人惊叹的交互体验呢???


三个学习 Shader 网站推荐


1. The Book of Shaders



网址: https://thebookofshaders.com/?lan=ch


The Book of Shaders 是一个在线学习 Shader 的网站(电子书),它提供了一系列关于 Shader 的基础教程和示例代码,堪称入门级指南


2.Shadertoy



网址:https://www.shadertoy.com/


Shadertoy 是一个基于 WebGL 的在线实时渲染平台,主要用于编辑分享查看 shader 程序及其实现的效果。


在这个平台上,用户可以创作和分享自己的 3D 图形效果。它提供了一个简单方便的环境,让用户可以轻松编辑自己的片段着色器,并实时查看修改的效果。


同时,Shadertoy 上有许多大佬分享他们制作的酷炫效果的代码,这些代码是完全开源的,用户可以在这些代码的基础上进行修改和学习。


除此之外,Shadertoy 还允许用户选择声音频道,将当前帧的声音信息转变成纹理(Texture),传入 shader 当中,从而根据声音信息来控制图形。这使得 Shadertoy 在视觉和听觉的结合上有了更多的可能性。


3.glsl.app



网址:https://glsl.app/


glsl.app 是一个在线的 GLSL (OpenGL Shading Language) 编辑器。GLSL 是一种用于图形渲染的着色语言,特别是在 OpenGL 图形库中。这种语言允许开发者为图形硬件编写着色器程序,这些程序可以运行在 GPU 上,用于计算图像的各种视觉效果。


在 glsl.app 上,你可以:



  • 编写和编辑着色器代码:直接在网页上编写顶点着色器、片元着色器等。

  • 实时预览:当你编写或修改着色器代码时,可以立即在右侧的预览窗口中看到效果。

  • 分享你的作品:完成你的着色器后,你可以获得一个链接,通过这个链接与其他人分享你的作品。

  • 学习:如果你是初学者,该网站还提供了很多示例和教程,帮助你了解如何编写各种着色器效果。


参考连接:



作者:前端开发爱好者
来源:juejin.cn/post/7352797634556706831
收起阅读 »

高并发下单加锁吗?

一个简单的下单流程包括,商品校验,订单计价,扣库存,保存订单。其中扣库存的并发问题是整个流程中最麻烦,最复杂的环节,可以说聚集了所有的智慧和头发。 解决扣库存并发问题,很容易让人想到加锁,加锁的目的是为了限制同步代码块并发,进一步的保证原子性,可见性和重排序...
继续阅读 »

一个简单的下单流程包括,商品校验,订单计价,扣库存,保存订单。其中扣库存的并发问题是整个流程中最麻烦,最复杂的环节,可以说聚集了所有的智慧和头发。
image.png


解决扣库存并发问题,很容易让人想到加锁,加锁的目的是为了限制同步代码块并发,进一步的保证原子性,可见性和重排序,实现数据一致性。


单机加 jvm 锁,分布式加分布式锁。这让我不禁想起分布式系统一句黑话,分布式系统中,没有什么问题是不能通过增加中间环节解决的,但解决一个问题常常会带来另外的问题,是的,你没听错,以空间换时间,以并发换数据一致性,在这里,锁粒度和范围对并发影响是最直接的,设计的时候尽可能的缩小锁粒度和范围,一般粒度是 skuId,范围尽量减小。


锁时长,锁过期是另外两个不得不考虑的问题。最麻烦的锁过期,常用解决方案是依赖 redission 的看门狗机制,相当于定时任务给锁续命,但粗暴续命会增加 rt,同时增加其他请求的阻塞时长。


尽量避免牺牲并发的方案!

尽量避免牺牲并发的方案!

尽量避免牺牲并发的方案!


一次偶然的机会,我的同事向我推荐了 Google 的 chubby。为什么我们不能用悲观锁+乐观锁的组合呢?在锁过期的时候,乐观锁兜底,不影响请求 rt,也能保证数据一致性。这是个不错的方案,适合简单的场景!


一次偶然的机会,一条公式冲击我的大脑,redis = 高性能 + 原子性。机智的你肯定知道加锁就是为了保证原子性,基于 redis 实现分布式锁也是因为 redis 的原子性和高性能(想想什么情况用 mysql 和 zk),如果我用 redis 代替锁,是不是既能保证扣库存的原子性,同时因为没有锁,又不需要考虑加锁带来的问题。


说干就干,马上画个图。(图片被掘金压缩,有点糊,我上传到图床,点击能跳转到图床看清晰的,如果看不清楚图片,联系我,给我留言
pCGSEE6.png
我把订单流程分为5大块,有点复杂,且听我细细道来。


Order process:



扣库存是限制订单并发的瓶颈,依靠 redis 的原子性保证数据一致性,高性能提升并发



2pc


基于二阶段提交思想,第一步首先插入 INIT 状态的订单


冷热路由


第二步有个路由,冷门商品走 mysql 下单,热门商品并发大,依靠 redis 撑。


如何知道商品冷热,答案是 bitMap,所以我们还需要一个定时任务(job4)维护 bitMap。冷热数据的统计来源一般是购物车,埋点统计。大电商平台来源更丰富,大数据追踪,算法推荐等。


故障处理


lua 扣减库存时,需要考虑请求超时和 redis 宕机。请求超时比较好解决,可以 catch 超时异常,依据业务选择重试或返回。redis 宕机比较棘手,后面分析。


降级

这里说一下降级。redis 宕机之后,走冷门订单流程。但是这里的设计会变的很复杂,因为需要解决两个问题,如何断定 redis 宕机,走冷门路由会不会把 mysql 压垮?这两个问题继续谈论下去会衍生出更多,比如走冷门路由的时机,冷门路由会不会把 mysql 压垮等,所以这里触发熔断还需要马上开启限流,展开真的很复杂,下次有机会说。


扣库存后续动作突然变得顺畅,插入订单库存流水,修改订单状态 UNPAY,发送核销优惠券 mq,日志记录等。这几个步骤中,



  • 流水用于记录订单和库存的绑定,重建商品库存缓存会用到

  • 核销优惠券选择异步,发核销优惠券的 mq,需要考虑消息丢失和重复消费,上游订单服务记录本地消息表,同时有个定时任务(job1)扫描重发,下游做好幂等

  • 我们还需要关注该流程可能会出现 jvm 宕机,这是很严重的事故,按理说没有顺利走完订单流程的订单属于异常订单,异常订单的库存需要返还 redis,所以还需要一个定时任务处理异常订单。


JOB2



redis 没有库存流水,被扣库存 x 无法得知



订单流程有几处宕机需要考虑,一处是执行 lua 脚本时 redis 宕机,另一处是扣完库存之后,jvm 宕机。无论是 redis 还是 jvm 宕机,这些订单都会返回异常信息到前端,所以这些订单的是无效的,需要还库存到 redis。


mysql 和 redis 的流水描述同一件事情,即记录该笔订单所扣库存。在异常情况下,可能只有 redis 有流水,依然可以作为断定库存已经扣减的依据,在极端异常的情况,lua 脚本刚扣完库存,redis 进程死了或者宕机,虽然 lua 是原子性的,但宕机可不是原子性,库存 x 已经扣了,没有流水记录,无法知道 x (redis 的单点问题可以通过 redis ha 保证)。


如果 redis 恢复了,但数据没了,怎么办?

如果 redis 恢复了,但数据丢失了(库存变化还没持久化就宕机,redis 重启恢复的是旧数据),怎么办?


Rebuild stock cache of sku



剩余库存 = (库存服务的总库存减去预占库存) - (mysql 和 redis 流水去重,计算的库存)



把目光锁定到右下角,重建 sku 库存缓存的逻辑。一般地,在 redis 扣完库存,会发个 mq 消息到库存服务,持久化该库存变动。库存服务采用 a/b 库存的设计,分别记录商品总库存和预占库存,为的是解决高并发场景业务操作库存和用户下单操作库存时的锁冲突问题。库存服务里的库存是延迟的,订单服务没发的消息和库存服务没消费的消息造成延迟。


我们既然把库存缓存到 redis,不妨想一下如何准确计算库存的数量。



  • 在刚开始启动服务的时候,redis 没有数据,这时候库存 t = a - b(a/b库存)

  • 服务运行一段时间,redis 有库存 t, 此时 t = a - b - (库存服务还没消费的扣库存消息),所以拿 mysql 和 redis 的流水去重,计算出已扣未消费库存。redis 宕机后,会有一个未知已扣库存 x, x 几乎没有算出来的可能(鄙人尽力了),也没必要算出来,你想,当你 redis 异常了,库存 x 对应的订单是异常订单,异常订单不会返回给用户,用户只会收到下单异常的返回,所以库存 x 是无效的,丢掉就好。


Payment process


用户支付之后,才发扣库存消息到库存服务落地。落地库存服务的流程很简单,不再阐述。重点说说新增库存和减少库存。新增库存不会造成超卖,简单粗暴的加就好。减少库存相当于下单,需要小心超卖问题,所以现在 redis 扣了库存,再执行本地事务,简简单单,凄凄惨惨戚戚,乍暖还寒时候,最难将息,三杯两盏淡酒,咋敌...


多说两句


纵观整幅图,对比简单下单流程,可以发现,为了解决高并发下单,引入一个中间环节,而引入中间环节的副作用需要我们处理。虽然订单流程变复杂了,但并发提高了。一般来说,redis qps 10万,实际上没有10万,如果你的业务 qps 超过单机 redis 限制,记住,分布式的核心思想就是拆,把库存均匀打散到多台 redis。


打散之后需要解决库存倾斜问题,可能实例 a 已经卖完了,实例 b 还有部分库存,但部分用户请求打到实例 a,就会造成明明有货,但下单失败。这个问题也很棘手,感兴趣的同学可以自行研究,学会教教我。


上述流程经过简化,真实情况会更复杂,不一定适合实际场景。如果有错误的地方,烦请留言讨论,多多交流,互相学习,一起进步。


还有个问题需要提,流程中的事务问题。可以发现,订单流程是没有事务控制的。一方面我们认为,数据很宝贵,不分正常异常。异常的订单数据可作为分析系统架构缺陷的依据,另一方面接口尽量避免长事务,特别是高并发下,事务越短越好。


回答几个问题


为什么感觉拿掉分布式锁之后,流程变得很复杂?


其实我大可给订单流程前后包裹一个分布式锁,新的设计就变成下图,可以看到,核心库存扣减逻辑并没有变化,所以分布式锁的存在并不是让流程变复杂的原因。


image.png


为什么流程突然变的很复杂?



  • 为保证数据一致性,加了几个定时任务和一个重建缓存接口;为提高性能,加了冷热路由;为减少复杂度,把库存扣减消息延迟到支付,总体流程比简单下单流程多了几道工序

  • 因为引入异构数据库,数据源由一变多,就需要维护数据源数据一致性。可以说,这些流程纯纯是为了保证多个数据源的数据一致性。如果以前我们在 mysql 做库存扣减,基于 mysql 事务就能保证数据一致性。但是 mysql 的 qps 并不高,他适合并发不高的情况,所以我才会让冷门商品走 mysql 下单流程,因为冷门商品几乎没有并发

  • 所以流程变得复杂的原因是维护数据一致性


总结


场景一:并发较低,MySQL可承受


如果业务量不大,且并发只有几十或百来个,那么 MySQL 可以胜任。为了保证数据一致性,需要在外层套上分布式锁。同时,在使用 MySQL 时需要注意锁粒度和锁区间。此外,避免订单请求把 MySQL 连接数打满,影响其他业务,可以考虑使用 Sentinel 进行限流。


场景二:并发量大,MySQL存在瓶颈


当营销变得复杂时,不仅仅是普通的订单流程,还有秒杀、限时特价和热销推广等复杂场景,此类业务的并发集中在特定的 SKU 上。在这种情况下,接口并发可能没有太大问题,因为分布式锁有限流的作用。但对于用户而言,大量购买失败就会带来严重后果。此类场景的瓶颈在于 MySQL,在理论上,将库存打散到其他 MySQL 实例可以解决问题,但我们不会这样做,因为 MySQL 是有状态的,所以更推荐的做法是基于 Redis 扣库存。


场景三:商品数量过亿


如果有幸业务发展到亿级商品数量,此时如果将所有商品的库存都存储在 Redis,可能会带来非常大的内存开销。一般来说,库存的结构为 {SKU ID: 数量},每个 SKU 只需要占用两个 int(8个字节)的空间,因此在性能方面没有大问题。根据二八原则,非热销商品大约占80%,这些商品可能很久都没人买,把库存存到 redis 实属浪费 700多 m。基于分布式的拆分思想,以热度维度分流商品库存,热门商品库存存储到 Redis,冷门的商品库存存储到 MySQL


此外,可以参照 redis cluster,修改路由算法将商品库存分配到不同的 Redis 实例。不过从实际来说,当你商品过亿,也不差钱搭个 redis cluster。如果你细想,冷热路由相当于把库存分散到多个实例,这会带来一些问题,比如用户购买多件商品的库存跨了多个实例,如果确定扣库存顺序,如何解决库存不足的资损,还有库存的逆向流程等,这些问题展开很复杂,有机会讨论


最后,对于库存的消息落库问题,如果上游订单很多,而下游的库存服务处理速度较慢,可能会出现消息堆积现象。针对这种情况,可以采用生产者-消费者模型,通过合并数据并批量提交的方式来加快落库速度。这种优化方式可以有效地避免消息堆积现象,提高系统的性能和稳定性


作者:勤奋的Raido
来源:juejin.cn/post/7245753944181817403
收起阅读 »

领导问我:为什么一个点赞功能你做了五天?

领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系 前言 可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端...
继续阅读 »

领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系



前言


可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端部分用的技术栈是 nest.js


某一个周一,领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系。


交代完之后,领导就去出差了。等领导回来时已是周五,他问可乐:这期的需求进展如何?


可乐回答:点赞的需求我做完了,其他的还没开始。


领导生气的说:为什么点赞这样的一个小功能你做了五天才做完???


可乐回答:领导息怒。。请听我细细道来


往期文章


仓库地址



初步设计


对于上面的这个需求,我们提炼出来有三点最为重要的功能:



  1. 获取点赞总数

  2. 获取用户的点赞关系

  3. 点赞/取消点赞


所以这里容易想到的是在文章表中冗余一个点赞数量字段 likes ,查询文章的时候一起把点赞总数带出来。


idcontentlikes
1文章A10
2文章B20

然后建一张 article_lile_relation 表,建立文章点赞与用户之间的关联关系。


idarticle_iduser_idvalue
1100120011
2100120020

上面的数据就表明了 id2001 的用户点赞了 id1001 的文章; id2002 的用户对 id1001 的文章取消了点赞。


这是对于这种关联关系需求最容易想到的、也是成本不高的解决方案,但在仔细思考了一番之后,我放弃了这种方案。原因如下:



  1. 由于首页文章流中也需要展示用户的点赞关系,这里获取点赞关系需要根据当前文章 id 、用户 id 去联表查询,会增加数据库的查询压力。

  2. 有关于点赞的信息存放在两张表中,需要维护两张表的数据一致性。

  3. 后续可能会出现对摸鱼帖子点赞、对用户点赞、对评论点赞等需求,这样的设计方案显然拓展性不强,后续再做别的点赞需求时可能会出现大量的重复代码。


基于上面的考虑,准备设计一个通用的点赞模块,以拓展后续各种业务的点赞需求。


表设计


首先来一张通用的点赞表, DDL 语句如下:


CREATE TABLE `like_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`target_id` int(11) DEFAULT NULL,
`type` int(4) DEFAULT NULL,
`created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`value` int(4) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `like_records_target_id_IDX` (`target_id`,`user_id`,`type`) USING BTREE,
KEY `like_records_user_id_IDX` (`user_id`,`target_id`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

解释一下上面各个字段的含义:



  • id :点赞记录的主键 id

  • user_id :点赞用户的 id

  • target_id :被点赞的文章 id

  • type :点赞类型:可能有文章、帖子、评论等

  • value :是否点赞, 1 点赞, 0 取消点赞

  • created_time :创建时间

  • updated_time :更新时间


前置知识


在设计好数据表之后,再来捋清楚这个业务的一些特定属性与具体实现方式:



  1. 我们可以理解这是一个相对来说读比写多的需求,比如你看了 10 篇掘金的文章,可能只会对 1 篇文章点赞

  2. 应该设计一个通用的点赞模块,以供后续各种点赞需求的接入

  3. 点赞数量与点赞关系需要频繁地获取,所以需要读缓存而不是读数据库

  4. 写入数据库与同步缓存需考虑数据一致性


所以可乐针对这样的业务特性上网查找了一些资料,发现有一些前置知识是他所欠缺的,我们一起来看看。


mysql事务


mysql 的事务是指一系列的数据库操作,这些操作要么全部成功执行,要么全部失败回滚。事务是用来确保数据库的完整性、一致性和持久性的机制之一。


mysql 中,事务具有以下四个特性,通常缩写为 ACID



  1. 原子性: 事务是原子性的,这意味着事务中的所有操作要么全部成功执行,要么全部失败回滚。

  2. 一致性: 事务执行后,数据库从一个一致的状态转换到另一个一致的状态。这意味着事务执行后,数据库中的数据必须满足所有的约束、触发器和规则,保持数据的完整性。

  3. 隔离性: 隔离性指的是多个事务之间的相互独立性。即使有多个事务同时对数据库进行操作,它们之间也不会相互影响,每个事务都感觉到自己在独立地操作数据库。 mysql 通过不同的隔离级别(如读未提交、读已提交、可重复读和串行化)来控制事务之间的隔离程度。

  4. 持久性: 持久性指的是一旦事务被提交,对数据库的改变将永久保存,即使系统崩溃也不会丢失。 mysql 通过将事务的提交写入日志文件来保证持久性,以便在系统崩溃后能够恢复数据。


这里以商品下单创建订单并扣除库存为例,演示一下 nest+typeorm 中的事务如何使用:


import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { Order } from './order.entity';
import { Product } from './product.entity';

@Injectable()
export class OrderService {
constructor(
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}

async createOrderAndDeductStock(productId: number, quantity: number): Promise<Order> {
return await this.entityManager.transaction(async transactionalEntityManager => {
// 查找产品并检查库存是否充足
const product = await transactionalEntityManager.findOne(Product, productId);
if (!product || product.stock < quantity) {
throw new Error('Product not found or insufficient stock');
}

// 创建订单
const order = new Order();
order.productId = productId;
order.quantity = quantity;
await transactionalEntityManager.save(order);

// 扣除库存
product.stock -= quantity;
await transactionalEntityManager.save(product);

return order;
});
}
}


this.entityManager.transaction 创建了一个事务,在异步函数中,如果发生错误, typeorm 会自动回滚事务;如果没有发生错误,typeorm 会自动提交事务。


在这个实例中,尝试获取库存并创建订单和减库存,如果任何一个地方出错异常抛出,则事务就会回滚,这样就保证了多表间数据的一致性。


分布式锁



分布式锁是一种用于在分布式系统中协调多个节点并保护共享资源的机制。在分布式系统中,由于涉及多个节点并发访问共享资源,因此需要一种机制来确保在任何给定时间只有一个节点能够访问或修改共享资源,以防止数据不一致或竞争条件的发生。



对于同一个用户对同一篇文章频繁的点赞/取消点赞请求,可以加分布式锁的机制,来规避一些问题:



  1. 防止竞态条件: 点赞/取消点赞操作涉及到查询数据库、更新数据库和更新缓存等多个步骤,如果不加锁,可能会导致竞态条件,造成数据不一致或错误的结果。

  2. 保证操作的原子性: 使用分布式锁可以确保点赞/取消点赞操作的原子性,即在同一时间同一用户只有一个请求能够执行操作,从而避免操作被中断或不完整的情况发生。

  3. 控制并发访问: 加锁可以有效地控制并发访问,限制了频繁点击发送请求的数量,从而减少系统负载和提高系统稳定性。


redis 中实现分布式锁通常使用的是基于 SETNX 命令和 EXPIRE 命令的方式:



  1. 使用 SETNX 命令尝试将 lockKey 设置为 lockValue ,如果 lockKey 不存在,则设置成功并返回 1;如果 lockKey 已经存在,则设置失败并返回 0

  2. 如果 SETNX 成功,说明当前客户端获得了锁,可以执行相应的操作;如果 SETNX 失败,则说明锁已经被其他客户端占用,当前客户端需要等待一段时间后重新尝试获取锁。

  3. 为了避免锁被永久占用,可以使用 EXPIRE 命令为锁设置一个过期时间,确保即使获取锁的客户端在执行操作时发生故障,锁也会在一定时间后自动释放。


  async getLock(key: string) {
const res = await this.redis.setnx(key, 'lock');
if (res) {
// 10秒锁过期
await this.redis.expire(key, 10);
}
return res;
}

async unLock(key: string) {
return this.del(key);
}

redis中的set结构


redis 中的 set 是一种无序集合,用于存储多个不重复的字符串值,set 中的每个成员都是唯一的。


我们存储点赞关系的时候,需要用到 redis 中的 set 结构,存储的 keyvalue 如下:


article_1001:[uid1,uid2,uid3]


这就表示文章 id1001 的文章,有用户 iduid1uid2uid3 这三个用户点赞了。


常用的 set 结构操作命令包括:



  • SADD key member [member ...]: 将一个或多个成员加入到集合中。

  • SMEMBERS key: 返回集合中的所有成员。

  • SISMEMBER key member: 检查成员是否是集合的成员。

  • SCARD key: 返回集合元素的数量。

  • SREM key member [member ...]: 移除集合中一个或多个成员。

  • SPOP key [count]: 随机移除并返回集合中的一个或多个元素。

  • SRANDMEMBER key [count]: 随机返回集合中的一个或多个元素,不会从集合中移除元素。

  • SUNION key [key ...]: 返回给定所有集合的并集。

  • SINTER key [key ...]: 返回给定所有集合的交集。

  • SDIFF key [key ...]: 返回给定所有集合的差集。


下面举几个点赞场景的例子



  1. 当用户 iduid1 给文章 id1001 的文章点赞时:sadd 1001 uid1

  2. 当用户 iduid1 给文章 id1001 的文章取消点赞时:srem 1001 uid1

  3. 当需要获取文章 id1001 的点赞数量时:scard 1001


redis事务


redis 中,事务是一组命令的有序序列,这些命令在执行时会被当做一个单独的操作来执行。即事务中的所有命令要么全部执行成功,要么全部执行失败,不存在部分执行的情况。


以下是 redis 事务的主要命令:



  1. MULTI: 开启事务,在执行 MULTI 命令后,后续输入多个命令来组成一个事务。

  2. EXEC: 执行事务,在执行 EXEC 命令时,redis 会执行客户端输入的所有事务命令,如果事务中的所有命令都执行成功,则事务执行成功,返回事务中所有命令的执行结果;如果事务中的某个命令执行失败,则事务执行失败,返回空。

  3. DISCARD: 取消事务,在执行 DISCARD 命令时,redis 会取消当前事务中的所有命令,事务中的命令不会被执行。

  4. WATCH: 监视键,在执行 WATCH 命令时,redis 会监听一个或多个键,如果在执行事务期间任何被监视的键被修改,事务将会被打断。


比如说下面的代码给集合增加元素,并更新集合的过期时间,可以如下使用 redis 的事务去执行它:


  const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();

流程图设计


在了解完这些前置知识之后,可乐开始画一些实现的流程图。


首先是点赞/取消点赞接口的流程图:


image.png


简单解释下上面的流程图:



  1. 先尝试获取锁,获取不到的时候等待重试,保证接口与数据的时序一致。

  2. 判断这个点赞关系是否已存在,比如说用户对这篇文章已经点过赞,其实又来了一个对此篇文章点赞的请求,直接返回失败

  3. 开启 mysql 的事务,去更新点赞信息表,同时尝试去更新缓存,在缓存更新的过程中,会有3次的失败重试机会,如果缓存更新都失败,则回滚mysql事务;整体更新失败

  4. mysql 更新成功,缓存也更新成功,则整个操作都成功


然后是获取点赞数量和点赞关系的接口


image.png


简单解释下上面的流程图:



  1. 首先判断当前文章 id 对应的点赞关系是否在 redis 中存在,如果存在,则直接从缓存中读取并返回

  2. 如果不存在,此时加锁,准备读取数据库并更新 redis ,这里加锁的主要目的是防止大量的请求一下子打到数据库中。

  3. 由于加锁的时候,可能很多接口已经在等待,所以在锁释放的时候,再加多一次从 redis 中获取的操作,此时 redis 中已经有值,可以直接从缓存中读取。


代码实现


在所有的设计完毕之后,可以做最后的代码实现了。分别来实现点赞操作与点赞数量接口。这里主要关注 service 层的实现即可。


点赞/取消点赞接口


  async toggleLike(params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
}
) {
const { userId, targetId, type, value } = params;
const LOCK_KEY = `${userId}::${targetId}::${type}::toggleLikeLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.toggleLike(params);
}
const record = await this.likeRepository.findOne({
where: { userId, targetId, type },
});
if (record && record.value === value) {
await this.redisService.unLock(LOCK_KEY);
throw Error('不可重复操作');
}

await this.entityManager.transaction(async (transactionalEntityManager) => {
if (!record) {
const likeEntity = new LikeEntity();
likeEntity.targetId = targetId;
likeEntity.type = type;
likeEntity.userId = userId;
likeEntity.value = value;
await transactionalEntityManager.save(likeEntity);
} else {
const id = record.id;
await transactionalEntityManager.update(LikeEntity, { id }, { value });
}
const isSuccess = await this.tryToFreshCache(params);

if (!isSuccess) {
await this.redisService.unLock(LOCK_KEY);
throw Error('操作失败');
}
});
await this.redisService.unLock(LOCK_KEY);
return true;
}

private async tryToFreshCache(
params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
},
retry = 3,
) {
if (retry === 0) {
return false;
}
const { targetId, type, value, userId } = params;
try {
const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();
return true;
} catch (error) {
console.log('tryToFreshCache error', error);
await wait();
return this.tryToFreshCache(params, retry - 1);
}
}


可以参照流程图来看这部分实现代码,基本实现就是使用 mysql 事务去更新点赞信息表,然后去更新 redis 中的点赞信息,如果更新失败则回滚事务,保证数据的一致性。


获取点赞数量、点赞关系接口


  async getLikes(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (!cacheExsit) {
await this.getLikeFromDbAndSetCache(params);
}
const count = await this.redisService.getSetLength(setKey);
const isLike = await this.redisService.isMemberOfSet(setKey, userId);
return { count, isLike };
}

private async getLikeFromDbAndSetCache(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const LOCK_KEY = `${targetId}::${type}::getLikesLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.getLikeFromDbAndSetCache(params);
}
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (cacheExsit) {
await this.redisService.unLock(LOCK_KEY);
return true;
}
const data = await this.likeRepository.find({
where: {
targetId,
userId,
type,
value: ELike.LIKE,
},
select: ['userId'],
});
if (data.length !== 0) {
await this.redisService.setAdd(
setKey,
data.map((item) => item.userId),
this.ttl,
);
}
await this.redisService.unLock(LOCK_KEY);
return true;
}

由于读操作相当频繁,所以这里应当多使用缓存,少查询数据库。读点赞信息时,先查 redis 中有没有,如果没有,则从 mysql 同步到 redis 中,同步的过程中也使用到了分布式锁,防止一开始没缓存时请求大量打到 mysql


同时,如果所有文章的点赞信息都同时存在 redis 中,那 redis 的存储压力会比较大,所以这里会给相关的 key 设置一个过期时间。当用户重新操作点赞时,会更新这个过期时间。保障缓存的数据都是相对热点的数据。


通过组装数据,获取点赞信息的返回数据结构如下:


image.png


返回一个 map ,其中 key 文章 idvalue 里面是该文章的点赞数量以及当前用户是否点赞了这篇文章。


前端实现


文章流列表发生变化的时候,可以监听列表的变化,然后去获取点赞的信息:


useEffect(() => {
if (!article.list) {
return;
}
const shouldGetLikeIds = article.list
.filter((item: any) => !item.likeInfo)
.map((item: any) => item.id);
if (shouldGetLikeIds.length === 0) {
return;
}
console.log("shouldGetLikeIds", shouldGetLikeIds);
getLikes({
targetIds: shouldGetLikeIds,
type: 1,
}).then((res) => {
const map = res.data;
const newList = [...article.list];
for (let i = 0; i < newList.length; i++) {
if (!newList[i].likeInfo && map[newList[i].id]) {
newList[i].likeInfo = map[newList[i].id];
}
}
const newArticle = { ...article };
newArticle.list = newList;
setArticle(newArticle);
});
}, [article]);

image.png


点赞操作的时候前端也需要加锁,接口执行完毕了再把锁释放。


   <Space
onClick={(e) => {
e.stopPropagation();
if (lockMap.current?.[item.id]) {
return;
}
lockMap.current[item.id] = true;
const oldValue = item.likeInfo.isLike;
const newValue = !oldValue;
const updateValue = (value: any) => {
const newArticle = { ...article };
const newList = [...newArticle.list];
const current = newList.find(
(_) => _.id === item.id
);
current.likeInfo.isLike = value;
if (value) {
current.likeInfo.count++;
} else {
current.likeInfo.count--;
}
setArticle(newArticle);
};
updateValue(newValue);
toggleLike({
targetId: item.id,
value: Number(newValue),
type: 1,
})
.catch(() => {
updateValue(oldValue);
})
.finally(() => {
lockMap.current[item.id] = false;
});
}}
>
<LikeOutlined
style={
item.likeInfo.isLike ? { color: "#1677ff" } : {}
}
/>

{item.likeInfo.count}
Space>

Kapture 2024-03-23 at 22.49.08.gif


解释


可乐:从需求分析考虑、然后研究网上的方案并学习前置知识,再是一些环境的安装,最后才是前后端代码的实现,领导,我这花了五天不过份吧。


领导(十分无语):我们平台本来就没几个用户、没几篇文章,本来就是一张关联表就能解决的问题,你又搞什么分布式锁又搞什么缓存,还花了那么多天时间。我不管啊,剩下没做的需求你得正常把它正常做完上线,今天周五,周末你也别休息了,过来加班吧。


最后


以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7349437605858066443
收起阅读 »

环信IM集成教程——Web端UIKit快速集成与消息发送

写在前面千呼万唤始出来,环信Web端终于出UIKit了!🎉🎉🎉文档地址:https://doc.easemob.com/uikit/chatuikit/web/chatuikit_overview.html环信单群聊 UIKit 是基于环信即时通讯云 IM S...
继续阅读 »

写在前面

千呼万唤始出来,环信Web端终于出UIKit了!🎉🎉🎉

环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了 IM SDK,可以帮助开发者不考虑内部实现和数据管理就能根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。现在就让我们一起探索如何集成吧!本文介绍如何快速实现在单聊会话中发送消息


准备工作

  1. React 环境:需要 React 16.8.0 或以上版本;React DOM 16.8.0 或以上版本。

  2. 即时通讯 IM 项目:已在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了 App Key

  3. 环信用户:在环信控制台创建 IM 用户,并获取用户 ID 和密码或 token。

  4. 好友关系:双方需要先 添加好友 才可以聊天



集成UIKit

准备工作完成就开始集成!在此先奉上环信Web端UIKit源码

第一步:创建一个UIKit项目

# 安装 CLI 工具。
npm install create-react-app
# 构建一个 my-app 的项目。
npx create-react-app my-app
cd my-app

第二步:安装 easemob-chat-uikit

cd my-app
  • 使用 npm 安装 easemob-chat-uikit 包
npm install easemob-chat-uikit --save
  • 使用 yarn 安装 easemob-chat-uikit 包
yarn add easemob-chat-uikit

第三步:引入uikit组件

在你的 React 项目中,引入 UIKit 提供的组件和样式:

// 导入组件
import {
UIKitProvider,
Chat,
ConversationList,
// ...
} from "easemob-chat-uikit";

// 导入样式
import "easemob-chat-uikit/style.css";

第四步:初始化配置

easemob-chat-uikit 提供 UIKitProvider 组件管理数据。UIKitProvider 不渲染任何 UI, 只用于为其他组件提供全局的 context,自动监听 SDK 事件, 在组件树中向下传递数据来驱动组件更新。单群聊 UIKit 中其他组件必须用 UIKitProvider 包裹。

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
</UIKitProvider>
</div>
);
}

export default App;


第五步:引入组件

根据自己的项目引入所需组件,组件文档,本文只介绍如何快速实现在单聊会话中发送消息,为了方便快速体验,一定要确保准备工作的第四条双方已经互为好友

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
<div style={{ display: "flex" }}>
<div style={{ width: "40%", height: "100%" }}>
<ContactList
onItemClick={(data) => {
rootStore.conversationStore.addConversation({
chatType: "singleChat",
conversationId: data.id,
lastMessage: {},
unreadCount: "",
});
}}
/>
</div>//联系人组件,点击某个好友通过‘rootStore.conversationStore.addConversation’创建会话
<div style={{ width: "30%", height: "100%" }}>
<ConversationList />//会话列表组件
</div>
<div style={{ width: "30%", height: "100%" }}>
<Chat />//聊天消息组件
</div>
</div>
</UIKitProvider>
</div>
);
}

export default App;

第六步:运行并测试

1、运行项目

npm run start

2、点击好友并发送一条消息



总结

通过以上步骤,你已经成功集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧~

相关文档

收起阅读 »

提升你的CSS技能:深入理解伪类选择器和伪元素选择器!

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。一、伪类选择器1、什...
继续阅读 »

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。

下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。

一、伪类选择器

1、什么是伪类选择器

伪类选择器,顾名思义,是一种特殊的选择器,它用来选择DOM元素在特定状态下的样式。这些特定状态并不是由文档结构决定的,而是由用户行为(如点击、悬停)或元素的状态(如被访问、被禁用)来定义的。

例如,我们可以用伪类选择器来改变链接在不同状态下的颜色,从而给用户以视觉反馈。

2、伪类选择器的语法

selector:pseudo-class {
property: value;
}

a:link {
color: #FF0000;
}

input:focus {
background-color: yellow;
}

注意:伪类名称对大小写不敏感。

3、常用的伪类选择器

下面分别介绍一下比较常用几类伪类选择器:

3.1 动态伪类选择器

这类选择器主要用于描述用户与元素的交互状态。例如:

1):hover: 当鼠标悬停在元素上时的样式。

代码示例:将链接的文本颜色改为红色

a:hover {
color: red;
}

2):active:当元素被用户激活(如点击)时的样式。

代码示例:将按钮的背景色改为蓝色

button:active {
background-color: blue;
}

3):focus: 当元素获得焦点(如输入框被点击)时的样式。

代码示例:将输入框的边框颜色改为绿色

input:focus {
border-color: green;
}

4):visited: 用于设置已访问链接的样式,通常与:link一起使用来区分未访问和已访问的链接。

代码示例:将已访问链接的颜色改为紫色

a:visited {
color: purple;
}

3.2 UI元素状态伪类选择器

这类选择器用于描述元素在用户界面中的状态。例如:

1):enabled和:disabled: 用于表单元素,表示元素是否可用。

示例:将禁用的输入框的边框颜色改为灰色

input:disabled {
border-color: gray;
}

2):checked: 用于单选框或复选框,表示元素是否被选中。

示例:将选中的单选框的背景色改为黄色

input[type="radio"]:checked {
background-color: yellow;
}

3):nth-child(n): 选取父元素中第n个子元素。

示例:将列表中的奇数位置的项目的背景色改为蓝色:

li:nth-child(odd) {
background-color: blue;
}

3.4 否定伪类选择器

这类选择器用于排除符合特定条件的元素。例如:

:not(selector): 选取不符合括号内选择器的所有元素。

示例:将不是段落的元素的背景色改为灰色:

*:not(p) {
background-color: gray;
}

4、常见应用

  • 设置鼠标悬停在元素上时的样式;

  • 为已访问和未访问链接设置不同的样式;

  • 设置元素获得焦点时的样式;


// 示例:a 标签的四种状态,分别对应 4 种伪类;

/* 未访问的链接 */
a:link {
color: blue;
}

/* 已访问的链接 */
a:visited {
color: red;
}

/* 鼠标悬停链接 */
a:hover {
color: orange;
}

/* 已选择的链接(鼠标点击但不放开时) */
a:active {
color: #0000FF;
}

注意:

  • a 标签的 4 个伪类(4种状态)必须按照一定顺序书写,否则将会失效;

  • a:hover 必须在 CSS 定义中的 a:link 和 a:visited 之后,才能生效;

  • a:active 必须在 CSS 定义中的 a:hover 之后才能生效;

  • 书写顺序为:a:link、a:visited、a:hover、a:active;

  • 记忆方法:love hate - “爱恨准则”;

二、伪元素选择器

1、什么是伪元素选择器

与伪类选择器不同,伪元素选择器是用来选择DOM元素的特定部分,而不是整个元素。它们通常用于处理那些不是由HTML标签直接表示的内容,比如首行文字、首字母或者生成的内容(如内容前面的编号)。

伪元素选择器允许我们对页面上的某些部分进行精确的样式控制,而这些部分在HTML结构中并不存在。

2、伪元素选择器语法

selector::pseudo-element {
property: value;
}

p::first-line {
color: #ff0000;
}

h1::before {
content: '♥';
}

3、常用伪元素选择器

伪元素选择器并不是针对真正的元素使用的选择器,而是针对CSS中已经定义好的伪元素使用的选择器,CSS中有如下四种常用伪元素选择器:first-line、 first-letter、 before、after。

3.1 ::first-line

::first-line表示第一行(第一行内容根据屏幕大小来决定显示多少字),例如:p::first-line{}。
代码示例:

    <style>
p::first-line{
color: blue;
}
</style>

Description

3.2 ::first-letter

::first-letter表示第一个字母,例如:p::first-letter{}。

代码示例:

<style>
p::first-letter{
font-size: 30px;
color: blueviolet;
}
</style>

Description

3.3 ::before和::after

::before表示元素的开始,::after表示元素的最后,before和after必须结合content属性来使用。

代码示例:

 <style>
p::after{
content: "hahaha";
color: red;
}
p::before{
content: "hehehe";
color: coral;
}
</style>

Description

注意:

  • before和after创建一个元素,但是属于行内元素。
  • 新创建的这个元素在文档中是找不到的,所以我们称为伪元素。
  • before在父元素内容的前面创建元素,after在父元素内容的后面插入元素。
  • 伪元素选择器和标签选择器一样,权重为1。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

三、伪类与伪元素选择器的区别

CSS中的伪类选择器和伪元素选择器都是用来选取DOM中特定元素的选择器。具体区别如下:

伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档数外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素;

  • 伪类本质上是为了弥补常规CSS选择器的不足,以便获取到更多信息;

  • 伪元素本质上是创建了一个有内容的虚拟容器;

  • CSS3 中伪类和伪元素的语法不同;

  • 在 CSS3 中,已经明确规定了伪类用一个冒号来表示,而伪元素则用两个冒号来表示;

  • 可以同时使用多个伪类,而只能同时使用一个伪元素。

总的来说,伪类选择器关注的是元素在特定状态下的样式变化,而伪元素选择器则是通过创建新的元素来实现特定的样式效果。两者都是CSS中非常强大的工具,可以帮助开发者实现复杂的页面布局和动态效果。

伪类选择器和伪元素选择器虽然不是真正的元素,但它们在CSS中扮演着极其重要的角色。了解并熟练运用它们,可以让你的网页更加生动、互动性更强,同时也能更好地控制页面的布局和内容的表现。

收起阅读 »

JSON非常慢:这里有更快的替代方案!

web
是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您...
继续阅读 »

是的,你没听错!JSON,这种在网络开发中普遍用于数据交换的格式,可能正在拖慢我们的应用程序。在速度和响应性至关重要的世界里,检查 JSON 的性能影响至关重要。在这篇博客中,深入探讨 JSON 可能成为应用程序瓶颈的原因,并探索更快的替代方法和优化技术,使您的应用程序保持最佳运行状态。


JSON 是什么,为什么要关心?


image.png


JSON 是 JavaScript Object Notation 的缩写,一种轻量级数据交换格式,已成为应用程序中传输和存储数据的首选。它的简单性和可读格式使开发者和机器都能轻松使用。但是,为什么要在项目中关注 JSON 呢?


JSON 是应用程序中数据的粘合剂。它是服务器和客户端之间进行数据通信的语言,也是数据库和配置文件中存储数据的格式。从本质上讲,JSON 在现代网络开发中起着举足轻重的作用。


JSON 的流行以及人们使用它的原因...


主要有就下几点:



  1. 人类可读格式:JSON 采用简单明了、基于文本的结构,便于开发人员和非开发人员阅读和理解。这种人类可读格式增强了协作,简化了调试。

  2. 语言无关:JSON 与任何特定编程语言无关。它是一种通用的数据格式,几乎所有现代编程语言都能对其进行解析和生成,因此具有很强的通用性。

  3. 数据结构一致性:JSON 使用键值对、数组和嵌套对象来实现数据结构的一致性。这种一致性使其具有可预测性,便于在各种编程场景中使用。

  4. 浏览器支持:浏览器原生支持 JSON,允许应用程序与服务器进行无缝通信。这种本地支持极大地促进了 JSON 在开发中的应用。

  5. JSON API:许多服务和应用程序接口默认以 JSON 格式提供数据。这进一步巩固了 JSON 在网络开发中作为数据交换首选的地位。

  6. JSON 模式:开发人员可以使用 JSON 模式定义和验证 JSON 数据的结构,从而为其应用程序增加一层额外的清晰度和可靠性。


鉴于这些优势,难怪全球的开发人员都依赖 JSON 来满足他们的数据交换需求。不过,随着我们深入探讨,会发现与 JSON 相关的潜在性能问题以及如何有效解决这些挑战。


对速度的需求


应用速度和响应速度的重要性


在当今快节奏的数字环境中,应用程序的速度和响应能力是不容忽视的。用户希望在网络和移动应用中即时获取信息、快速交互和无缝体验。对速度的这种要求是由多种因素驱动的:



  1. 用户期望:用户已习惯于从数字互动中获得闪电般快速的响应。他们不想等待网页加载或应用程序响应。哪怕是几秒钟的延迟,都会导致用户产生挫败感并放弃使用。

  2. 竞争优势:速度可以成为重要的竞争优势。与反应慢的应用程序相比,反应迅速的应用程序往往能更有效地吸引和留住用户。

  3. 搜索引擎排名:谷歌等搜索引擎将页面速度视为排名因素。加载速度更快的网站往往在搜索结果中排名靠前,从而提高知名度和流量。

  4. 转换率:电子商务网站尤其清楚速度对转换率的影响。网站速度越快,转换率越高,收入也就越高。

  5. 移动性能:随着移动设备的普及,对速度的需求变得更加重要。移动用户的带宽和处理能力往往有限,因此,快速的应用程序性能必不可少。


JSON 会拖慢我们的应用程序吗?


在某些情况下,JSON 可能是导致应用程序运行速度减慢的罪魁祸首。解析 JSON 数据的过程,尤其是在处理大型或复杂结构时,可能会耗费宝贵的毫秒时间。此外,低效的序列化和反序列化也会影响应用程序的整体性能


JSON 为什么会变慢


1.解析开销


JSON 数据到达应用程序后,必须经过解析过程才能转换成可用的数据结构。解析过程可能相对较慢,尤其是在处理大量或深度嵌套的 JSON 数据时。


2.序列化和反序列化


JSON 要求在从客户端向服务器发送数据时进行序列化(将对象编码为字符串),并在接收数据时进行反序列化(将字符串转换回可用对象)。这些步骤会带来开销并影响应用程序的整体速度。


在微服务架构的世界里,JSON 通常用于在服务之间传递消息。但是,JSON 消息需要序列化和反序列化,这两个过程会带来巨大的开销。



在众多微服务不断通信的情况下,这种开销可能会累积起来,有可能会使应用程序减慢到影响用户体验的程度。



image.png


3.字符串操作


JSON 以文本为基础,主要依靠字符串操作来进行连接和解析等操作。与处理二进制数据相比,字符串处理速度较慢。


4.缺乏数据类型


JSON 的数据类型(如字符串、数字、布尔值)有限。复杂的数据结构可能需要效率较低的表示方法,从而导致内存使用量增加和处理速度减慢。


image.png


5.冗长性


JSON 的人机可读设计可能导致冗长。冗余键和重复结构会增加有效载荷的大小,导致数据传输时间延长。


6.不支持二进制


JSON 缺乏对二进制数据的本地支持。在处理二进制数据时,开发人员通常需要将其编码和解码为文本,这可能会降低效率。


7.深嵌套


在某些情况下,JSON 数据可能嵌套很深,需要进行递归解析和遍历。这种计算复杂性会降低应用程序的运行速度,尤其是在没有优化的情况下。


JSON 的替代品


虽然 JSON 是一种通用的数据交换格式,但由于其在某些情况下的性能限制,开发者开始探索更快的替代格式。我们来看呓2其中的一些替代方案。


1.协议缓冲区(protobuf)


协议缓冲区(通常称为 protobuf)是谷歌开发的一种二进制序列化格式。其设计宗旨是高效、紧凑和快速。Protobuf 的二进制特性使其在序列化和反序列化时比 JSON 快得多。


何时使用:当你需要高性能数据交换时,尤其是在微服务架构、物联网应用或网络带宽有限的情况下,请考虑使用 protobuf


2. MessagePack 信息包


MessagePack 是另一种二进制序列化格式,以速度快、结构紧凑而著称。其设计目的是在保持与各种编程语言兼容的同时,提高比 JSON 更高的效率。


何时使用:当你需要在速度和跨语言兼容性之间取得平衡时,MessagePack 是一个不错的选择。它适用于实时应用程序和对减少数据量有重要要求的情况。


3. BSON(二进制 JSON)


BSON 或二进制 JSON 是一种从 JSON 衍生出来的二进制编码格式。它保留了 JSON 的灵活性,同时通过二进制编码提高了性能。BSON 常用于 MongoDB 等数据库。


何时使用:如果你正在使用 MongoDB,或者需要一种能在 JSON 和二进制效率之间架起桥梁的格式,那么 BSON 就是一个很有价值的选择。


4. Apache Avro(阿帕奇 Avro)


Apache Avro 是一个数据序列化框架,专注于提供一种紧凑的二进制格式。它基于模式,可实现高效的数据编码和解码。


何时使用:Avro 适用于模式演进非常重要的情况,如数据存储,以及需要在速度和数据结构灵活性之间取得平衡的情况。


与 JSON 相比,这些替代方案在性能上有不同程度的提升,具体选择取决于您的具体使用情况。通过考虑这些替代方案,您可以优化应用程序的数据交换流程,确保将速度和效率放在开发工作的首位。


image.png


每个字节的重要性:优化数据格式


JSON 数据


下面是我们的 JSON 数据示例片段:


{
"id": 1, // 14 bytes
"name": "John Doe", // 20 bytes
"email": "johndoe@example.com", // 31 bytes
"age": 30, // 9 bytes
"isSubscribed": true, // 13 bytes
"orders": [ // 11 bytes
{ // 2 bytes
"orderId": "A123", // 18 bytes
"totalAmount": 100.50 // 20 bytes
}, // 1 byte
{ // 2 bytes
"orderId": "B456", // 18 bytes
"totalAmount": 75.25 // 19 bytes
} // 1 byte
] // 1 byte
}

JSON 总大小: ~139 字节


JSON 功能多样,易于使用,但也有缺点,那就是它的文本性质。每个字符、每个空格和每个引号都很重要。在数据大小和传输速度至关重要的情况下,这些看似微不足道的字符可能会产生重大影响。


效率挑战:使用二进制格式减少数据大小


现在,我们提供其他格式的数据表示并比较它们的大小:


协议缓冲区 (protobuf)


syntax = "proto3";

message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_subscribed = 5;
repeated Order orders = 6;

message Order {
string order_id = 1;
float total_amount = 2;
}
}

0A 0E 4A 6F 68 6E 20 44 6F 65 0C 4A 6F 68 6E 20 44 6F 65 65 78 61 6D 70 6C 65 2E 63 6F 6D 04 21 00 00 00 05 01 12 41 31 32 33 03 42 DC CC CC 3F 05 30 31 31 32 34 34 35 36 25 02 9A 99 99 3F 0D 31 02 42 34 35 36 25 02 9A 99 99 3F

协议缓冲区总大小: ~38 字节


MessagePack


二进制表示法(十六进制):


a36a6964000000000a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d042100000005011241313302bdcccc3f0530112434353625029a99993f

信息包总大小: ~34 字节


Binary Representation (Hexadecimal):


3e0000001069640031000a4a6f686e20446f6502656d61696c006a6f686e646f65406578616d706c652e636f6d1000000022616765001f04370e4940

BSON 总大小: ~43 字节


Avro


二进制表示法(十六进制):


0e120a4a6f686e20446f650c6a6f686e646f65406578616d706c652e636f6d049a999940040a020b4108312e3525312e323538323539

Avro 总大小: ~32 字节


image.png


现在,你可能想知道,为什么这些格式中的某些会输出二进制数据,但它们的大小却各不相同。Avro、MessagePack 和 BSON 等二进制格式具有不同的内部结构和编码机制,这可能导致二进制表示法的差异,即使它们最终表示的是相同的数据。下面简要介绍一下这些差异是如何产生的:


1. Avro



  • Avro 使用模式对数据进行编码,这种模式通常包含在二进制表示法中。

  • Avro 基于模式的编码通过提前指定数据结构,实现了高效的数据序列化和反序列化。

  • Avro 的二进制格式设计为自描述格式,这意味着模式信息包含在编码数据中。这种自描述性使 Avro 能够保持不同版本数据模式之间的兼容性。


2. MessagePack



  • MessagePack 是一种二进制序列化格式,直接对数据进行编码,不包含模式信息。

  • 它使用长度可变的整数和长度可变的字符串的紧凑二进制表示法,以尽量减少空间使用。

  • MessagePack 不包含模式信息,因此更适用于模式已提前知晓并在发送方和接收方之间共享的情况。



3. BSON



  • BSON 是 JSON 数据的二进制编码,包括每个值的类型信息。

  • BSON 的设计与 JSON 紧密相连,但它增加了二进制数据类型,如 JSON 缺乏的日期和二进制数据。

  • 与 MessagePack 一样,BSON 不包括模式信息。


这些设计和编码上的差异导致了二进制表示法的不同:



  • Avro 包含模式信息并具有自描述性,因此二进制文件稍大,但与模式兼容。

  • MessagePack 的编码长度可变,因此非常紧凑,但缺乏模式信息,因此适用于已知模式的情况。

  • BSON 与 JSON 关系密切,并包含类型信息,与 MessagePack 等纯二进制格式相比,BSON 的大小会有所增加。


总之,这些差异源于每种格式的设计目标和特点。Avro 优先考虑模式兼容性,MessagePack 侧重于紧凑性,而 BSON 在保持类似 JSON 结构的同时增加了二进制类型。格式的选择取决于您的具体使用情况和要求,如模式兼容性、数据大小和易用性。


优化 JSON 性能


下面是一些优化 JSON 性能的实用技巧以及代码示例和最佳实践:


1.最小化数据大小



  • 使用简短的描述性键名:选择简洁但有意义的键名,以减少 JSON 对象的大小


// Inefficient
{
"customer_name_with_spaces": "John Doe"
}

// Efficient
{
"customerName": "John Doe"
}


  • 尽可能缩写:在不影响清晰度的情况下,考虑对键或值使用缩写。


// 效率低
{
"transaction_type": "purchase"
}

// 效率高
{
"txnType": "purchase"
}

2.明智使用数组



  • 尽量减少嵌套:避免深度嵌套数组,因为它们会增加解析和遍历 JSON 的复杂性。


// 效率低
{
"order": {
"items": {
"item1": "Product A",
"item2": "Product B"
}
}
}

// 效率高
{
"orderItems": ["Product A", "Product B"]
}

3.优化数字表示法


尽可能使用整数:如果数值可以用整数表示,就用整数代替浮点数。


// 效率低
{
"quantity": 1.0
}

// 效率高
{
"quantity": 1
}

4.删除冗余


避免重复数据:通过引用共享值来消除冗余数据。


// 效率低
{
"product1": {
"name": "Product A",
"price": 10
},
"product2": {
"name": "Product A",
"price": 10
}
}

// 效率高
{
"products": [
{
"name": "Product A",
"price": 10
},
{
"name": "Product B",
"price": 15
}
]
}

5.使用压缩


应用压缩算法:如果适用,在传输过程中使用 Gzipor Brotlito 等压缩算法来减小 JSON 有效负载的大小。


// 使用 zlib 进行 Gzip 压缩的 Node.js 示例
const zlib = require('zlib');

const jsonData = {
// 在这里填入你的 JSON 数据
};

zlib.gzip(JSON.stringify(jsonData), (err, compressedData) => {
if (!err) {
// 通过网络发送 compressedData
}
});


6.采用服务器端缓存:


缓存 JSON 响应:实施服务器端缓存,高效地存储和提供 JSON 响应,减少重复数据处理的需要。


7.配置文件和优化


剖析性能:使用剖析工具找出 JSON 处理代码中的瓶颈,然后优化这些部分。


实际优化:在实践中加快 JSON 的处理速度


在本节中,我们将探讨实际案例,这些案例在使用 JSON 时遇到性能瓶颈并成功克服。我们会看到诸如 LinkedIn、Auth0、Uber 等知名技术公司如何解决 JSON 的限制并改善他们应用的性能。这些案例为如何提升应用处理速度和响应性提供了实用的策略。


1.LinkedIn 的协议缓冲区集成:



  • 挑战:LinkedIn 面临的挑战是 JSON 的冗长以及由此导致的网络带宽使用量增加,从而导致延迟增加。

  • 解决方案:他们采用了 Protocol Buffers,这是一种二进制序列化格式,用以替换微服务通信中的 JSON。

  • 影响:这一优化将延迟降低了 60%,提高了 LinkedIn 服务的速度和响应能力。


2.Uber 的 H3 地理索引:


挑战:Uber 使用 JSON 来表示各种地理空间数据,但解析大型数据集的 JSON 会降低其算法速度。


解决方案:他们引入了 H3 Geo-Index,这是一种用于地理空间数据的高效六边形网格系统,可减少 JSON 解析开销。


影响:这一优化大大加快了地理空间业务的发展,增强了 Uber 的叫车和地图服务。


3.Slack 的信息格式优化:


挑战:Slack 需要在实时聊天中传输和呈现大量 JSON 格式的消息,这导致了性能瓶颈。


解决方案:他们优化了 JSON 结构,减少了不必要的数据,只在每条信息中包含必要的信息。


影响:这项优化使得消息展现更快,从而提高了 Slack 用户的整体聊天性能。


4.Auth0 的协议缓冲区实现:


挑战:Auth0 是一个流行的身份和访问管理平台,在处理身份验证和授权数据时面临着 JSON 的性能挑战。


解决方案:他们采用协议缓冲区(Protocol Buffers)来取代 JSON,以编码和解码与身份验证相关的数据。


影响:这一优化大大提高了数据序列化和反序列化的速度,从而加快了身份验证流程,并增强了 Auth0 服务的整体性能。


这些现实世界中的例子展示了通过优化策略解决 JSON 的性能挑战如何对应用程序的速度、响应速度和用户体验产生实质性的积极影响。它们强调了考虑替代数据格式和高效数据结构的重要性,以克服各种情况下与 JSON 相关的速度减慢问题。


结论


在不断变化的网络开发环境中,优化 JSON 性能是一项宝贵的技能,它能让你的项目与众不同,并确保你的应用程序在即时数字体验时代茁壮成长。


作者:王大冶
来源:juejin.cn/post/7303424117243297807
收起阅读 »

解锁前端难题:亲手实现一个图片标注工具

web
本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究! 业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图...
继续阅读 »

本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!



业务中涉及图片的制作和审核功能,审核人员需要在图片中进行标注,并说明存在的问题,标注过程中需要支持放大缩小,移动等交互,将业务剥离,这个需求,可以定义为实现一个图片标注功能。


实现这个功能并不容易,其涉及的前端知识点众多,本文带领大家从零到一,亲手实现一个,支持缩放,移动,编辑的图片标注功能,文字描述是抽象的,眼见为实,实现效果如下所示:


Kapture 2024-03-20 at 18.43.56.gif


技术方案


这里涉及两个关键功能,一个是绘制,包括缩放和旋转,一个是编辑,包括选取和修改尺寸,涉及到的技术包括,缩放,移动,和自定义形状的绘制(本文仅实现矩形),绘制形状的选取,改变尺寸和旋转角度等。


从大的技术选型来说,有两种实现思路,一种是 canvas,一种是 dom+svg,下面简单介绍下两种思路和优缺点。


canvas 可以方便实现绘制功能,但编辑功能就比较困难,当然这可以使用库来实现,这里我们考虑自己亲手实现功能。



  • 优点

    • 性能较好,尤其是在处理大型图片和复杂图形时。

    • 支持更复杂的图形绘制和像素级操作。

    • 一旦图形绘制在 Canvas 上,就不会受到 DOM 的影响,减少重绘和回流。



  • 缺点

    • 交互相对复杂,需要手动管理图形的状态和事件。

    • 对辅助技术(如屏幕阅读器)支持较差。



  • 可能遇到的困难

    • 实现复杂的交互逻辑(如选取、移动、修改尺寸等)可能比较繁琐。

    • 在缩放和平移时,需要手动管理坐标变换和图形重绘。




dom+svg 也可以实现功能,缩放和旋转可以借助 css3 的 transform。



  • 优点

    • 交互相对简单,可以利用 DOM 事件系统和 CSS。

    • 对辅助技术支持较好,有助于提高可访问性。



  • 缺点

    • 在处理大型图片和复杂图形时,性能可能不如 Canvas。

    • SVG 元素数量过多时,可能会影响页面性能。



  • 可能遇到的困难

    • 在实现复杂的图形和效果时,可能需要较多的 SVG 知识和技巧。

    • 管理大量的 SVG 元素和事件可能会使代码变得复杂。




总的来说,如果对性能有较高要求,或需要进行复杂的图形处理和像素操作,可以选择基于 Canvas 的方案。否则可以选择基于 DOM + SVG 的方案。在具体实现时,可以根据项目需求和技术栈进行选择。


下面我们选择基于 canvas 的方案,通过例子,一步一步实现完成功能,让我们先从最简单的开始。


渲染图片


本文我们不讲解 canvas 基础,如果你不了解 canvas,可以先在网上找资料,简单学习下,图片的渲染非常简单,只用到一个 API,这里我们直接给出代码,示例如下:


这里我们提前准备一个 canvas,宽高设定为 1000*700,这里唯一的一个知识点就是要在图片加载完成后再绘制,在实战中,需要注意绘制的图片不能跨域,否则会绘制失败。


<body>
<div>
<canvas id="canvas1" width="1000" height="700"></canvas>
</div>
<script>
const canvas1 = document.querySelector('#canvas1');
const ctx1 = canvas1.getContext('2d');
let width = 1000;
let height = 700;

let img = new Image();
img.src = './bg.png';
img.onload = function () {
draw();
};

function draw() {
console.log('draw');
ctx1.drawImage(img, 0, 0, width, height);
}
</script>
</body>

现在我们已经成功在页面中绘制了一张图片,这非常简单,让我们继续往下看吧。


缩放


实现图片缩放功能,我们需要了解两个关键的知识点:如何监听缩放事件和如何实现图片缩放。


先来看第一个,我用的是 Mac,在 Mac 上可以通过监听鼠标的滚轮事件来实现缩放的监听。当用户使用鼠标滚轮时,会触发 wheel 事件,我们可以通过这个事件的 deltaY 属性来判断用户是向上滚动(放大)还是向下滚动(缩小)。


可以看到在 wheel 事件中,我们修改了 scale 变量,这个变量会在下面用到。这里添加了对最小缩放是 1,最大缩放是 3 的限制。


document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
// detect pinch
event.preventDefault(); // prevent zoom
if (event.deltaY < 0) {
console.log('Pinching in');
if (scale < 3) {
scale = Math.min(scale + 0.1, 3);
draw();
}
} else {
console.log('Pinching out');
if (scale > 1) {
scale = Math.max(scale - 0.1, 1);
draw();
}
}
}
},
{ passive: false }
);

图片缩放功能,用到了 canvas 的 scale 函数,其可以修改绘制上下文的缩放比例,示例代码如下:


我们添加了clearRect函数,这用来清除上一次绘制的图形,当需要重绘时,就需要使用clearRect函数。


这里需要注意开头和结尾的 save 和 restore 函数,因为我们会修改 scale,如果不恢复的话,其会影响下一次绘制,一般在修改上下文时,都是通过 save 和 restore 来复原的。


let scale = 1;

function draw() {
console.log('draw');
ctx1.clearRect(0, 0, width, height);
ctx1.save();
ctx1.scale(scale, scale);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

这里稍微解释一下 scale 函数,初次接触,可能会不太好理解。在 Canvas 中使用 scale 函数时,重要的是要理解它实际上是在缩放绘图坐标系统,而不是直接缩放绘制的图形。当你调用 ctx.scale(scaleX, scaleY) 时,你是在告诉 Canvas 之后的所有绘图操作都应该在一个被缩放的坐标系统中进行。


这意味着,如果你将缩放比例设置为 2,那么在这个缩放的坐标系统中,绘制一个宽度为 50 像素的矩形,实际上会在画布上产生一个宽度为 100 像素的矩形。因为在缩放的坐标系统中,每个单位长度都变成了原来的两倍。


因此,当我们谈论 scale 函数时,重点是要记住它是在缩放整个绘图坐标系统,而不是单独的图形。这就是为什么在使用 scale 函数后,所有的绘图操作(包括位置、大小等)都会受到影响。


现在我们已经实现了图片的缩放功能,效果如下所示:


Kapture 2024-03-21 at 15.20.58.gif


鼠标缩放


细心的你可能发现上面的缩放效果是基于左上角的,基于鼠标点缩放意味着图片的缩放中心是用户鼠标所在的位置,而不是图片的左上角或其他固定点。这种缩放方式更符合用户的直觉,可以提供更好的交互体验。


为了实现这种效果,可以使用 tanslate 来移动原点,canvas 中默认的缩放原点是左上角,具体方法是,可以在缩放前,将缩放原点移动到鼠标点的位置,缩放后,再将其恢复,这样就不会影响后续的绘制,实现代码如下所示:


let scaleX = 0;
let scaleY = 0;

function draw() {
ctx1.clearRect(0, 0, width, height);
ctx1.save();
// 注意这行1
ctx1.translate(scaleX, scaleY);
ctx1.scale(scale, scale);
// 注意这行2
ctx1.translate(-scaleX, -scaleY);
ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

scaleX 和 scaleY 的值,可以在缩放的时候设置即可,如下所示:


// zoom
document.addEventListener(
'wheel',
function (event) {
if (event.ctrlKey) {
if (event.deltaY < 0) {
if (scale < 3) {
// 注意这里两行
scaleX = event.offsetX;
scaleY = event.offsetY;
scale = Math.min(scale + 0.1, 3);
draw();
}
}
// 省略代码
}
},
{ passive: false }
);

现在我们已经实现了图片的鼠标缩放功能,效果如下所示:


3.gif


移动视口


先解释下放大时,可见区域的概念,好像叫视口吧
当处于放大状态时,会导致图像只能显示一部分,此时需要能过需要可以移动可见的图像,
这里选择通过触摸板的移动,也就是 wheel 来实现移动视口


通过 canvas 的 translate 来实现改变视口


在图片放大后,整个图像可能无法完全显示在 Canvas 上,此时只有图像的一部分(即可见区域)会显示在画布上。这个可见区域也被称为“视口”。为了查看图像的其他部分,我们需要能够移动这个视口,即实现图片的平移功能。


在放大状态下,视口的大小相对于整个图像是固定的,但是它可以在图像上移动以显示不同的部分。你可以将视口想象为一个固定大小的窗口,你通过这个窗口来观察一个更大的图像。当你移动视口时,窗口中显示的图像部分也会相应改变。


为了实现移动视口,我们可以通过监听触摸板的移动事件(也就是 wheel 事件)来改变视口的位置。当用户通过触摸板进行上下或左右滑动时,我们可以相应地移动视口,从而实现图像的平移效果。


我们可以使用 Canvas 的 translate 方法来改变视口的位置。translate 方法接受两个参数,分别表示沿 x 轴和 y 轴移动的距离。在移动视口时,我们需要更新图片的位置,并重新绘制图像以反映新的视口位置。


代码改动如下所示:


let translateX = 0;
let translateY = 0;

function draw() {
// 此处省略代码
// 改变视口
ctx1.translate(translateX, translateY);

ctx1.drawImage(img, 0, 0, width, height);
ctx1.restore();
}

// translate canvas
document.addEventListener(
"wheel",
function (event) {
if (!event.ctrlKey) {
// console.log("translate", event.deltaX, event.deltaY);
event.preventDefault();
translateX -= event.deltaX;
translateY -= event.deltaY;
draw();
}
},
{ passive: false }
);

在这个示例中,translateXtranslateY 表示视口的位置。当用户通过触摸板进行滑动时,我们根据滑动的方向和距离更新视口的位置,并重新绘制图像。通过这种方式,我们可以实现图像的平移功能,允许用户查看图像的不同部分。


现在我们已经实现了移动视口功能,效果如下所示:


4.gif


绘制标注


为了便于大家理解,这里我们仅实现矩形标注示例,实际业务中可能存在各种图形的标记,比如圆形,椭圆,直线,曲线,自定义图形等。


我们先考虑矩形标注的绘制问题,由于 canvas 是位图,我们需要在 js 中存储矩形的数据,矩形的存储需要支持坐标,尺寸,旋转角度和是否在编辑中等。因为可能存在多个标注,所以需要一个数组来存取标注数据,我们将标注存储在reacts中,示例如下:


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

下面将 rects 渲染到 canvas 中,示例代码如下:


代码扩机并不复杂,比较容易理解,值得一提的rotateAngle的实现,我们通过旋转上下文来实现,其旋转中心是矩形的图形的中心点,因为操作上线文,所以在每个矩形绘制开始和结束后,要通过saverestore来恢复之前的上下文。


isEditing表示当前的标注是否处于编辑状态,在这里编辑中的矩形框,我们只需设置不同的颜色即可,在后面我们会实现编辑的逻辑。


function draw() {
// 此处省略代码
ctx1.drawImage(img, 0, 0, width, height);

rects.forEach((r) => {
ctx1.strokeStyle = r.isEditing ? 'rgba(255, 0, 0, 0.5)' : 'rgba(255, 0, 0)';

ctx1.save();
if (r.rotatable) {
ctx1.translate(r.x + r.width / 2, r.y + r.height / 2);
ctx1.rotate((r.rotateAngle * Math.PI) / 180);
ctx1.translate(-(r.x + r.width / 2), -(r.y + r.height / 2));
}
ctx1.strokeRect(r.x, r.y, r.width, r.height);
ctx1.restore();
});

ctx1.restore();
}

现在我们已经实现了标注绘制功能,效果如下所示:


5.png


添加标注


为了在图片上添加标注,我们需要实现鼠标按下、移动和抬起时的事件处理,以便在用户拖动鼠标时动态地绘制一个矩形标注。同时,由于视口可以放大和移动,我们还需要进行坐标的换算,确保标注的位置正确。


首先,我们需要定义一个变量 drawingRect 来存储正在添加中的标注数据。这个变量将包含标注的起始坐标、宽度和高度等信息:


let drawingRect = null;

接下来,我们需要实现鼠标按下、移动和抬起的事件处理函数:


mousedown中我们需要记录鼠标按下时,距离视口左上角的坐标,并将其记录到全局变量startXstartY中。


mousemove时,需要更新当前在绘制矩形的数据,并调用draw完成重绘。


mouseup时,需要处理添加操作,将矩形添加到rects中,在这里我做了一个判断,如果矩形的宽高小于 1,则不添加,这是为了避免在鼠标原地点击时,误添加图形的问题。


let startX = 0;
let startY = 0;
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

console.log('mousedown', e.offsetX, e.offsetY, x, y);

drawingRect = drawingRect || {};
});
canvas1.addEventListener('mousemove', (e) => {
// 绘制中
if (drawingRect) {
drawingRect = computeRect({
x: startX,
y: startY,
width: e.offsetX - startX,
height: e.offsetY - startY,
});
draw();
return;
}
});
canvas1.addEventListener('mouseup', (e) => {
if (drawingRect) {
drawingRect = null;
// 如果绘制的矩形太小,则不添加,防止原地点击时添加矩形
// 如果反向绘制,则调整为正向
const width = Math.abs(e.offsetX - startX);
const height = Math.abs(e.offsetY - startY);
if (width > 1 || height > 1) {
const newrect = computeRect({
x: Math.min(startX, e.offsetX),
y: Math.min(startY, e.offsetY),
width,
height,
});
rects.push(newrect);
draw();
}
return;
}
});

下面我们来重点讲讲上面的computexycomputeRect函数,由于视口可以放大和移动,我们需要将鼠标点击时的视口坐标换算为 Canvas 坐标系的坐标。


宽高的计算比较简单,只需要将视口坐标除以缩放比例即可得到。但坐标的计算并不简单,这里通过视口坐标,直接去推 canvas 坐标是比较困难的,我们可以求出 canvas 坐标计算视口坐标的公式,公式推导如下:


vx: 视口坐标
x: canvas坐标
scale: 缩放比例
scaleX: 缩放原点
translateX: 视口移动位置

我们x会在如下视口操作后进行渲染成vx:
1: ctx1.translate(scaleX, scaleY);
2: ctx1.scale(scale, scale);
3: ctx1.translate(-scaleX, -scaleY);
4: ctx1.translate(translateX, translateY);

根据上面的步骤,每一步vx的推演如下:
1: vx = x + scaleX
2: vx = x * scale + scaleX
3: vx = x * scale + scaleX - scaleX * scale
4: vx = x * scale + scaleX - scaleX * scale + translateX * scale

通过上面 vx 和 x 的公式,我们可以计算出来 x 和 vx 的关系如下,我在这里走了很多弯路,导致计算的坐标一直不对,不要试图通过 vx 直接推出 x,一定要通过上面的公式来推导:


x = (vx - scaleX * (1 - scale) - translateX * scale) / scale

理解了上面坐标和宽高的计算公式,下面的代码就好理解了:


function computexy(x, y) {
const xy = {
x: (x - scaleX * (1 - scale) - translateX * scale) / scale,
y: (y - scaleY * (1 - scale) - translateY * scale) / scale,
};
return xy;
}
function computewh(width, height) {
return {
width: width / scale,
height: height / scale,
};
}
function computeRect(rect) {
const cr = {
...computexy(rect.x, rect.y),
...computewh(rect.width, rect.height),
};
return cr;
}

最后,我们需要一个函数来绘制标注矩形:


function draw() {
// 此处省略代码
if (drawingRect) {
ctx1.strokeRect(
drawingRect.x,
drawingRect.y,
drawingRect.width,
drawingRect.height
);
}
ctx1.restore();
}

现在我们已经实现了添加标注功能,效果如下所示:


6.gif


选取标注


判断选中,将视口坐标,转换为 canvas 坐标,遍历矩形,判断点在矩形内部
同时需要考虑点击空白处,清空选中状态
选中其他元素时,清空上一个选中的元素
渲染选中状态,选中状态改变边的颜色,为了明显,红色变为绿色
要是先选取元素的功能,关键要实现的判断点在矩形内部,判断点在矩形内部的逻辑比较简单,我们可以抽象为如下函数:


function poInRect({ x, y }, rect) {
return (
x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
);
}

在点击事件中,我们拿到的是视口坐标,首先将其转换为 canvas 坐标,然后遍历矩形数组,判断是否有中选的矩形,如果有的话将其存储下来。


还需要考虑点击新元素时,和点击空白时,重置上一个元素的选中态的逻辑,代码实现如下所示:


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

const pickRect = rects.find((r) => {
return poInRect({ x, y }, r);
});

if (pickRect) {
if (editRect && pickRect !== editRect) {
// 选择了其他矩形
editRect.isEditing = false;
editRect = null;
}
pickRect.isEditing = true;
editRect = pickRect;
draw();
} else {
if (editRect) {
editRect.isEditing = false;
editRect = null;
draw();
}
drawingRect = drawingRect || {};
}
});

现在我们已经实现了选取标注功能,效果如下所示:


7.gif


移动


接下来是移动,也就是通过拖拽来改变已有图形的位置
首先需要一个变量来存取当前被拖拽元素,在 down 和 up 时更新这个元素
要实现拖拽,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,在每次 move 时,用当前坐标减去坐标差即可
不要忘了将视口坐标,换算为 canvas 坐标哦


接下来,我们将实现通过拖拽来改变已有标注的位置的功能。这需要跟踪当前被拖拽的标注,并在鼠标移动时更新其位置。


首先,我们需要一个变量来存储当前被拖拽的标注:


let draggingRect = null;

在鼠标按下时(mousedown 事件),我们需要判断是否点击了某个标注,并将其设置为被拖拽的标注,并在鼠标抬起时(mouseup 事件),将其置空。


要实现完美的拖拽效果,需要一点小技巧,在点击时,计算点击点和图形左上角的坐标差,将其记录到全局变量shiftXshiftY,关键代码如下所示。


let shiftX = 0;
let shiftY = 0;
canvas1.addEventListener('mousedown', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

if (pickRect) {
// 计算坐标差
shiftX = x - pickRect.x;
shiftY = y - pickRect.y;
// 标记当前拖拽元素
draggingRect = pickRect;
draw();
}
});
canvas1.addEventListener('mouseup', (e) => {
if (draggingRect) {
// 置空当前拖拽元素
draggingRect = null;
return;
}
});

在鼠标移动时(mousemove 事件),如果有标注被拖拽,则更新其位置,关键代码如下所示。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 当前正在拖拽矩形
if (draggingRect) {
draggingRect.x = x - shiftX;
draggingRect.y = y - shiftY;
draw();
return;
}
});

现在我们已经实现了移动功能,效果如下所示:


8.gif


修改尺寸


为了实现标注尺寸的修改功能,我们可以在标注的四个角和四条边的中点处显示小方块作为编辑器,允许用户通过拖拽这些小方块来改变标注的大小。


首先,我们需要实现编辑器的渲染逻辑。我们可以在 drawEditor 函数中添加代码来绘制这些小方块。


在这里,我们使用 computeEditRect 函数来计算标注的八个编辑点的位置,并在 drawEditor 函数中绘制这些小方块,关键代码如下所示:


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
t: {
type: "t",
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2,
width,
height: width,
},
b: {// 代码省略},
l: {// 代码省略},
r: {// 代码省略},
tl: {// 代码省略},
tr: {// 代码省略},
bl: {// 代码省略},
br: {// 代码省略},
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = "rgba(255, 150, 150)";

// 绘制矩形
for (const r of Object.values(editor)) {
ctx1.fillRect(r.x, r.y, r.width, r.height);
}
ctx1.restore();
}
function draw() {
rects.forEach((r) => {
// 添加如下代码
if (r.isEditing) {
drawEditor(r);
}
});
}

接下来,我们需要实现拖动这些编辑点来改变标注大小的功能。首先,我们需要在鼠标按下时判断是否点击了某个编辑点。


在这里,我们使用 poInEditor 函数来判断鼠标点击的位置是否接近某个编辑点。如果是,则设置 startEditRect, dragingEditor, editorShiftXY 来记录正在调整大小的标注和编辑点。


let startEditRect = null;
let dragingEditor = null;
let editorShiftX = 0;
let editorShiftY = 0;
function poInEditor(point, rect) {
const editor = computeEditRect(rect);
if (!editor) return;

for (const edit of Object.values(editor)) {
if (poInRect(point, edit)) {
return edit;
}
}
}
canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整大小
startEditRect = { ...editRect };
dragingEditor = editor;
editorShiftX = x - editor.x;
editorShiftY = y - editor.y;
return;
}
}
});

然后,在鼠标移动时,我们需要根据拖动的编辑点来调整标注的大小。


在这个例子中,我们只展示了上边中间编辑点的处理逻辑,其他编辑点的处理逻辑类似。通过拖动不同的编辑点,我们可以实现标注的不同方向和维度的大小调整。


canvas1.addEventListener('mousemove', (e) => {
const { x, y } = computexy(e.offsetX, e.offsetY);

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
// 调整大小中
if (dragingEditor) {
const moveX = (e.offsetX - startX) / scale;
const moveY = (e.offsetY - startY) / scale;

switch (dragingEditor.type) {
case 't':
editRect.y = startEditRect.y + moveY;
editRect.height = startEditRect.height - moveY;
break;
}
draw();
return;
}
}
});

现在我们已经实现了修改尺寸功能,效果如下所示:


9.gif


旋转


实现旋转编辑器的渲染按钮,在顶部增加一个小方块的方式来实现,


旋转图形会影响选中图形的逻辑,即点在旋转图形里的判断,这块的逻辑需要修改


接下来实现旋转逻辑,会涉及 mousedown 和 mousemove


接下来介绍旋转,这一部分会有一定难度,涉及一些数学计算,而且旋转逻辑会修改多出代码,下面我们依次介绍。


旋转涉及两大块功能,一个是旋转编辑器,一个是旋转逻辑,我们先来看旋转编辑器,我们可以在标注的顶部增加一个用于旋转的小方块作为旋转编辑器,如下图所示:


image.png


下面修改我们的drawEditorcomputeEditRect函数,增加渲染逻辑,涉及一个方块和一条线的渲染。


其中rotr就是顶部的方块,rotl是那条竖线。


function computeEditRect(rect) {
let width = 10;
let linelen = 16;
return {
...(rect.rotatable
? {
rotr: {
type: 'rotr',
x: rect.x + rect.width / 2 - width / 2,
y: rect.y - width / 2 - linelen - width,
width,
height: width,
},
rotl: {
type: 'rotl',
x1: rect.x + rect.width / 2,
y1: rect.y - linelen - width / 2,
x2: rect.x + rect.width / 2,
y2: rect.y - width / 2,
},
}
: null),
};
}
function drawEditor(rect) {
ctx1.save();
const editor = computeEditRect(rect);
ctx1.fillStyle = 'rgba(255, 150, 150)';
const { rotl, rotr, ...rects } = editor;

// 绘制旋转按钮
if (rect.rotatable) {
ctx1.fillRect(rotr.x, rotr.y, rotr.width, rotr.height);
ctx1.beginPath();
ctx1.moveTo(rotl.x1, rotl.y1);
ctx1.lineTo(rotl.x2, rotl.y2);
ctx1.stroke();
}

// 绘制矩形
// ...
}

在实现旋转逻辑之前,先来看一个问题,如下图所示,当我们在绿色圆圈区按下鼠标时,在我们之前的逻辑中,也会触发选中状态。


image.png


这是因为我们判断点在矩形内部的逻辑,并未考虑旋转的问题,我们的矩形数据存储了矩形旋转之前的坐标和旋转角度,如下所示。


let rects = [
{
x: 650,
y: 350,
width: 100,
height: 100,
isEditing: false,
rotatable: true,
rotateAngle: 30,
},
];

解决这个问题有两个思路,一个是将旋转后矩形的四个点坐标计算出来,这种方法比较麻烦。另一个思路是逆向的,将要判断的点,以矩形的中点为中心,做逆向旋转,计算出其在 canvas 中的坐标,这个坐标,可以继续参与我们之前点在矩形内的计算。


关键代码如下所示,其中rotatePoint是计算 canvas 中的坐标,poInRotRect判断给定点是否在旋转矩形内部。


// 将点绕 rotateCenter 旋转 rotateAngle 度
function rotatePoint(point, rotateCenter, rotateAngle) {
let dx = point.x - rotateCenter.x;
let dy = point.y - rotateCenter.y;

let rotatedX =
dx * Math.cos((-rotateAngle * Math.PI) / 180) -
dy * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.x;
let rotatedY =
dy * Math.cos((-rotateAngle * Math.PI) / 180) +
dx * Math.sin((-rotateAngle * Math.PI) / 180) +
rotateCenter.y;

return { x: rotatedX, y: rotatedY };
}

function poInRotRect(
point,
rect,
rotateCenter = {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
},
rotateAngle = rect.rotateAngle
) {
if (rotateAngle) {
const rotatedPoint = rotatePoint(point, rotateCenter, rotateAngle);
const res = poInRect(rotatedPoint, rect);
return res;
}
return poInRect(point, rect);
}

接下来实现旋转逻辑,这需要改在 mousedown 和 mousemove 事件,实现拖动时的实时旋转。


在 mousedown 时,判断如果点击的是旋转按钮,则将当前矩形记录到全局变量rotatingRect


canvas1.addEventListener('mousedown', (e) => {
startX = e.offsetX;
startY = e.offsetY;
const { x, y } = computexy(e.offsetX, e.offsetY);

if (editRect) {
const editor = poInEditor({ x, y }, editRect);
if (editor) {
// 调整旋转
if (editor.type === 'rotr') {
rotatingRect = editRect;
prevX = e.offsetX;
prevY = e.offsetY;
return;
}
// 调整大小
}
}
});

在 mousemove 时,判断如果是位于旋转按钮上,则计算旋转角度。


canvas1.addEventListener('mousemove', (e) => {
// 绘制中
const { x, y } = computexy(e.offsetX, e.offsetY);
// 当前正在拖拽矩形

// 如果存在编辑中的元素
if (editRect) {
const editor = poInEditor({ x, y }, editRect);
console.log('mousemove', editor);

// 旋转中
if (rotatingRect) {
const relativeAngle = getRelativeRotationAngle(
computexy(e.offsetX, e.offsetY),
computexy(prevX, prevY),
{
x: editRect.x + editRect.width / 2,
y: editRect.y + editRect.height / 2,
}
);
console.log('relativeAngle', relativeAngle);
editRect.rotateAngle += (relativeAngle * 180) / Math.PI;
prevX = e.offsetX;
prevY = e.offsetY;
draw();
return;
}

// 调整大小中
}
});

将拖拽移动的距离,转换为旋转的角度,涉及一些数学知识,其原理是通过上一次鼠标位置和本次鼠标位置,计算两个点和旋转中心(矩形的中心)三个点,形成的夹角,示例代码如下:


function getRelativeRotationAngle(point, prev, center) {
// 计算上一次鼠标位置和旋转中心的角度
let prevAngle = Math.atan2(prev.y - center.y, prev.x - center.x);

// 计算当前鼠标位置和旋转中心的角度
let curAngle = Math.atan2(point.y - center.y, point.x - center.x);

// 得到相对旋转角度
let relativeAngle = curAngle - prevAngle;

return relativeAngle;
}

现在我们已经实现了旋转功能,效果如下所示:


10.gif


总结


在本文中,我们一步一步地实现了一个功能丰富的图片标注工具。从最基本的图片渲染到复杂的标注编辑功能,包括缩放、移动、添加标注、选择标注、移动标注、修改标注尺寸、以及标注旋转等,涵盖了图片标注工具的核心功能。


通过这个实例,我们可以看到,实现一个前端图片标注工具需要综合运用多种前端技术和知识,包括但不限于:



  • Canvas API 的使用,如绘制图片、绘制形状、图形变换等。

  • 鼠标事件的处理,如点击、拖拽、滚轮缩放等。

  • 几何计算,如点是否在矩形内、旋转角度的计算等。


希望这个实例能够为你提供一些启发和帮助,让你在实现自己的图片标注工具时有一个参考和借鉴。


更进一步


站在文章的角度,到此为止,下面让我们站在更高的维度思考更进一步的可能,我们还能继续做些什么呢?


在抽象层面,我们可以考虑将图片标注工具的核心功能进行进一步的抽象和封装,将其打造成一个通用的开源库。这样,其他开发者可以直接使用这个库来快速实现自己的图片标注需求,而无需从零开始。为了实现这一目标,我们需要考虑以下几点:



  • 通用性:库应该支持多种常见的标注形状和编辑功能,以满足不同场景的需求。

  • 易用性:提供简洁明了的 API 和文档,使得开发者能够轻松集成和使用。

  • 可扩展性:设计上应该留有足够的灵活性,以便开发者可以根据自己的特定需求进行定制和扩展。

  • 性能优化:注重性能优化,确保库在处理大型图片或复杂标注时仍能保持良好的性能。


在产品层面,我们可以基于这个通用库,进一步开发成一个功能完备的图片标注工具,提供开箱即用的体验。这个工具可以包括以下功能:



  • 多种标注类型:支持矩形、圆形、多边形等多种标注类型。

  • 标注管理:提供标注的增加、删除、编辑、保存等管理功能。

  • 导出和分享:支持导出标注结果为各种格式,如 JSON、XML 等,以及分享给他人协作编辑。

  • 用户界面:提供友好的用户界面,支持快捷键操作,提高标注效率。

  • 集成与扩展:支持与其他系统或工具的集成,提供 API 接口和插件机制,以便进行功能扩展。


通过不断地迭代和优化,我们可以使这个图片标注工具成为业界的标杆,为用户提供高效便捷的标注体验。


感谢您的阅读和关注!希望这篇文章能够为您在前端开发中实现图像标注功能提供一些有价值的见解和启发。如果您有任何问题、建议或想要分享自己的经验,欢迎在评论区留言交流。让我们一起探索更多前端技术的可能性,不断提升我们的技能和创造力!


本文示例源码:github.com/yanhaijing/…


本文示例预览:yanhaijing.com/imagic/demo…


作者:颜海镜
来源:juejin.cn/post/7350954669742768147
收起阅读 »

Window.print() 实现浏览器打印

web
前言 由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。 语法 window.print(); 该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体...
继续阅读 »

前言


由于在公司项目中有打印的具体业务场景,在查询相关资料后,找到了 Window.print() 是用来打印的方法,写下这篇文章供自己和大家查漏补缺。


语法


window.print();

该方法没有参数和返回值,在页面中直接调用,将直接打印整个页面,具体使用如下面代码所示



在点击打印该页面按钮后,会触发浏览器的打印对话框,对话框里有一些配置项,可以设置打印的相关参数。


image.png


根据上面的方法,我们就可以实现在浏览器中打印页面。


但是实际的开发中,我们的业务场景比这更加复杂。例如:只打印某个 DOM 元素,需要根据用户需求调整纸张的大小和形状,调整打印的布局,字体大小,缩放比例等等。这些都是常见的情况,那我们应该怎么做呢?


使用 @media 媒体查询


媒体查询 MDN解释:
@media CSS @ 规则可用于基于一个或多个媒体查询的结果来应用样式表的一部分。使用它,你可以指定一个媒体查询和一个 CSS 块,当且仅当该媒体查询与正在使用其内容的设备匹配时,该 CSS 块才能应用于该文档。


简单来说就是使用媒体查询根据不同的条件来决定应用不同的样式。具体到我们的需求就是,在打印时,使用专门的打印样式,隐藏其他元素,实现只打印某个元素的效果



使用样式表


在你的 标签中添加 link 元素,倒入专门供打印使用的样式表。你可以在样式表中编写打印是的具体样式。


<link href="/media/css/print.css" media="print" rel="stylesheet" />

打印页面时常用的一些样式


利用 print 设置打印页面的样式,利用 page 设置打印文档的纸张配置


 /* print.css */
@media print {
* {
-webkit-print-color-adjust: exact !important; /* 保证打印出来颜色与页面一致 */
}
.no-break {
page-break-inside: avoid; /* 避免元素被剪切 */
}
.no-print {
display: none; /* 不想打印的元素设置隐藏 */
}

@page {
size: A4 prtrait; /* 设置打印纸张尺寸及打印方向:A4,纵向打印 */
margin-top: 3cm /* 利用 margin 设置页边距 */
}

}

使用 iframe 实现更加精细的打印


我们也可以将想要打印的内容在 iframe 中渲染出来并打印,通过创建一个隐藏的 iframe 元素,将想要打印的内容放入其中,然后触发 iframe 的打印功能,实现更加灵活的打印。伪代码如下所示:


<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Use ifame print example</title>
<script>
function printPage() {
// 新建一个 iframe 元素并隐藏
const iframe = document.createElement("iframe");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "0";
// 将它添加到 document 中
document.body.appendChild(hideFrame);

// 拿到新创建的 iframe 的文档流
const iframeDocument = iframe.contentDocument;

// 在 iframe 中新建一个 link 标签,引入专门用于打印的样式表
const printCssLink = iframeDocument.createElement('link')
printCssLink.rel = 'stylesheet';
printCssLink.type = 'text/css';
printCssLink.media = 'print';
printCssLink.href = `medis/css/print.css`;
iframeDocument.head.appendChild(printCssLink);

// 在 iframe 中新建一个容器,用来存放你想要打印的元素
let printContainer = iframeDocument.createElement('div');
iframeDocument.body.appendChild(printContainer);

iframeDocument.close(); // 关闭 iframe 文档流

// 获取 iframe 的 window 对象
const iframeWindow = iframe.contentWindow;
// 当 iframe 内容加载完成后触发打印功能。
iframeWindow.onload = function() {
iframeWindow.print();
}
// 打印完后移除 iframe 元素
document.body.removeChild(iframe);
}
</script>
</head>
<body>
<p>
<span onclick="printPage();">
Print external page!
</span>
</p>
</body>
</html>

以上就是关于我在使用 Window.print() 打印页面的一些总结,欢迎大家有问题在评论区讨论学习。


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

Android应用保活全攻略:30个实用技巧助你突破后台限制

在Android系统中,保活(保持应用进程一直存活)通常是为了让应用在后台持续运行,以实现某些特定的功能,如实时消息推送、定位服务等。然而,由于Android系统为了节省资源和保护用户隐私,通常会限制后台应用的运行。因此,开发者需要采取一些策略来实现保活。以下...
继续阅读 »

在Android系统中,保活(保持应用进程一直存活)通常是为了让应用在后台持续运行,以实现某些特定的功能,如实时消息推送、定位服务等。然而,由于Android系统为了节省资源和保护用户隐私,通常会限制后台应用的运行。因此,开发者需要采取一些策略来实现保活。以下是30个常见的Android保活手段,帮助你突破后台限制:


1. 前台服务(Foreground Service)


将应用的Service设置为前台服务,这样系统会认为这个服务是用户关心的,不容易被杀死。前台服务需要显示一个通知,告知用户当前服务正在运行。通过调用startForeground(int id, Notification notification)方法将服务设置为前台服务。


2. 双进程守护


创建两个Service,分别运行在不同的进程中。当一个进程被杀死时,另一个进程可以通过监听onServiceDisconnected(ComponentName name)方法来感知,并重新启动被杀死的进程。这样可以相互守护,提高应用的存活率。


3. 使用系统广播拉活


使用系统广播拉活。监听系统广播,如开机广播、网络变化广播、应用安装卸载广播等。当收到广播时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。


4. JobScheduler


使用JobScheduler定时启动应用。JobScheduler是Android 5.0引入的一种任务调度机制,可以在满足特定条件下执行任务。通过创建一个Job,设置触发条件,然后将Job提交给JobScheduler。当触发条件满足时,JobScheduler会启动应用。


5. 白名单


引导用户将应用加入系统的白名单,如省电白名单、自启动白名单等。加入白名单的应用不会受到系统的限制,可以在后台持续运行。


6. 第三方推送服务


使用第三方推送服务,如极光推送、小米推送等。这些推送服务通常使用保活技巧,可以保证消息的实时推送。


7. 静态广播监听


在AndroidManifest.xml中注册静态广播,监听系统广播,如电池状态改变、屏幕解锁等。当收到广播时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。需要注意的是,从Android 8.0开始,静态广播的使用受到了限制,部分隐式广播无法通过静态注册来接收。


8. 合理利用Activity


在必要时,将应用的Activity设置为singleTask或singleInstance模式,确保应用在后台时只有一个实例。这可以减少系统对应用的限制,提高应用在后台的存活率。


9. 使用AlarmManager定时唤醒


使用AlarmManager定时唤醒应用。通过设置一个定时任务,当到达指定时间时,使用PendingIntent启动应用。需要注意的是,从Android 6.0开始,AlarmManager的行为受到了限制,当设备处于低电量模式时,定时任务可能会被延迟。


10. 合理设置进程优先级


Android系统会根据进程的优先级来决定是否回收进程。通过合理设置进程优先级,可以降低系统回收进程的概率。例如,可以将Service设置为前台服务,或者将进程与用户正在交互的Activity绑定。


11. 使用sticky广播


使用sticky广播在一定程度上可以提高广播接收器的优先级。当发送一个sticky广播时,系统会将该广播存储在内存中,这样即使应用被杀死,也可以在重新启动时收到广播。但需要注意的是,从Android 5.0开始,sticky广播的使用受到了限制,部分广播无法使用sticky模式发送。


12. 使用WorkManager


WorkManager是Android Architecture Components的一部分,它为后台任务提供了一种统一的解决方案。WorkManager可以自动选择最佳的执行方式,即使应用退出或设备重启,它仍然可以确保任务完成。WorkManager在保活方面的效果可能不如其他方法,但它是一种更符合Android系统规范的解决方案,可以避免系统限制和用户体验问题。


13. 合理使用WakeLock


在某些特定场景下,可以使用WakeLock(电源锁)来防止CPU进入休眠状态,从而确保应用能够在后台持续运行。但请注意,WakeLock可能会导致设备电量消耗增加,因此应谨慎使用,并在不需要时尽快释放锁。


14. 合理使用SyncAdapter


SyncAdapter是Android提供的一种同步框架,用于处理数据同步操作。SyncAdapter可以根据设备的网络状态、电池状态等条件来自动调度同步任务。虽然SyncAdapter并非专门用于保活,但它可以在一定程度上提高应用在后台的存活率。


15. 使用AccountManager


通过在应用中添加一个账户,并将其与SyncAdapter关联,可以在一定程度上提高应用的存活率。当系统触发同步操作时,会启动与账户关联的应用进程。但请注意,这种方法可能会对用户造成困扰,因此应谨慎使用。


16. 适配Doze模式和App Standby


从Android 6.0(API级别23)开始,系统引入了Doze模式和App Standby,以优化设备的电池使用。在这些模式下,系统会限制后台应用的网络访问和CPU使用。为了保证应用在这些模式下正常运行,您需要适配这些特性,如使用高优先级的Firebase Cloud Messaging(FCM)消息来唤醒应用。


17. 使用Firebase Cloud Messaging(FCM)


对于需要实时消息推送的应用,可以使用Firebase Cloud Messaging(FCM)服务。FCM是一种跨平台的消息推送服务,可以实现高效且可靠的消息传递。通过使用FCM,您可以确保应用在后台时接收到实时消息,而无需采取过多的保活手段。


18. 遵循Android系统的最佳实践


在开发过程中,遵循Android系统的最佳实践和推荐方案,可以提高应用的兼容性和稳定性。例如,合理使用后台任务、避免长时间运行的服务、优化内存使用等。这样可以降低系统对应用的限制,从而提高应用在后台的存活率。


19. 及时适配新系统版本


随着Android系统版本的更新,系统对后台应用的限制可能会发生变化。为了确保应用在新系统版本上能够正常运行,您需要及时适配新系统版本,并根据需要调整保活策略。


20. 与用户建立信任


在实际开发中,应尽量遵循系统的规范和限制,避免过度使用保活手段。与用户建立信任,告知用户应用在后台运行的原因和目的。在用户授权的情况下,采取适当的保活策略,以实现所需功能。


21. 使用Binder机制


Binder是Android中的一种跨进程通信(IPC)机制。通过在Service中创建一个Binder对象,并在其他进程中获取这个Binder对象,可以使得两个进程建立连接,从而提高Service的存活率。


22. 使用native进程


通过JNI技术,创建一个native进程来守护应用进程。当应用进程被杀死时,native进程可以感知到这个事件,并重新启动应用进程。这种方法需要C/C++知识,并且可能会增加应用的复杂性和维护成本。


23. 使用反射调用隐藏API


Android系统中有一些隐藏的API和系统服务,可以用于提高应用的存活率。例如,通过反射调用ActivityManager的addPersistentProcess方法,可以将应用设置为系统进程,从而提高应用的优先级。然而,这种方法存在很大的风险,可能会导致应用在某些设备或系统版本上无法正常运行。


24 监听系统UI


监听系统UI的变化,如状态栏、导航栏等。当系统UI变化时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。


25. 使用多进程


在AndroidManifest.xml中为Service或Activity设置android:process属性,使其运行在单独的进程中。这样,即使主进程被杀死,其他进程仍然可以存活。


26. 使用Provider


在AndroidManifest.xml中注册一个Provider,并在其他应用中通过ContentResolver访问这个Provider。这样,即使应用在后台,只要有其他应用访问Provider,应用就可以保持存活。


27. 关注Android开发者文档和官方博客


Android开发者文档和官方博客是获取保活策略和系统更新信息的重要途径。关注这些资源,以便了解最新的系统特性、开发者指南和最佳实践。


28. 性能优化


优化应用的性能,降低内存、CPU和电池的消耗。这样,系统在资源紧张时可能会优先回收其他消耗较高的应用,从而提高您的应用在后台的存活率。


29. 用户反馈


关注用户的反馈,了解他们在使用应用过程中遇到的问题。根据用户的反馈,调整保活策略,以实现最佳的用户体验。


30. 使用NotificationListenerService


通过实现一个NotificationListenerService并在AndroidManifest.xml中注册,可以监听系统通知栏的变化。当收到新的通知时,检查应用进程是否存活,如果已经被杀死,则重新启动应用。这种方法可以利用系统通知的变化来触发应用的启动,从而提高应用在后台的存活率。需要注意的是,为了使用NotificationListenerService,用户需要在设置中授权应用访问通知权限。


请注意,保活策略可能会导致系统资源消耗增加、用户体验下降,甚至引发系统限制或用户卸载应用。因此,在实际开发中,应根据功能需求和用户体验来权衡保活策略,尽量遵循系统的规范和限制。在可能的情况下,优先考虑使用系统推荐的解决方案,如前台服务、JobScheduler等。


作者:程序员陆业聪
来源:juejin.cn/post/7352079364611276819
收起阅读 »

《青春咖啡馆》书籍推介

《青春咖啡馆》书籍推介 摘要 一些书籍的阅读往往能带来自我心灵的疗愈,《青春咖啡馆》就是这样的一本书,书中描写了一个和20岁左右的女孩子关于心中幸福的追索,反映出那个年代探索人生意义的青年群像。20世纪60年代,法国存在主义兴起,青年在这些思...
继续阅读 »

《青春咖啡馆》书籍推介





摘要


一些书籍的阅读往往能带来自我心灵的疗愈,《青春咖啡馆》就是这样的一本书,书中描写了一个和20岁左右的女孩子关于心中幸福的追索,反映出那个年代探索人生意义的青年群像。20世纪60年代,法国存在主义兴起,青年在这些思潮的引导下渴望摆脱人生的束缚,不断逃离那些既定的轨道,在“去存在”中获得自由。可是,一味地追求“存在”、“自由”,真的可能吗?故事的女主人公就是如此,她最后选择存在于另一个世界……


点评


《青春咖啡馆》是莫迪亚诺最令人心碎的作品之一。作品一如既往地充满了调查与跟踪,回忆与求证,找不到答案的疑问。“您找到您的幸福吗?”可是,露姬到哪里去寻找她的幸福呢?咖啡馆?书籍?大街上的游荡?婚姻中?逃跑?她全试过了,包括毒品和神秘学,可她像其他许许多多的人一样,最终消失在时间的长河中。


历史背景


本书的故事发生在20世纪60年代的法国巴黎,正是各种青年学生运动盛行的时期。当时有一位重要的思想家居伊·德波,他的思想曾对1968年的法国青年学生运动“五月风暴”产生过巨大影响。本书的作者莫迪亚诺也引用了他的这样一句话放在小说开头:



在真实生活之旅的中途,


我们被一缕绵长的愁绪包围,


在挥霍青春的咖啡馆里,


愁绪从那么多戏谑的和伤感的话语中流露出来。 ——居伊·德波



国际情景主义者认为,“在这个被商品和景观统治的社会中,创造精神和人生梦想失去了自己的家园,人生活在这样的环境里感到窒息”,所以他们主张用创造生活取代“被动生活”,呼吁“毫无拘束地生活、毫无节制地享受”和游戏人生,并进行人生的“漂移” (dérive)。居伊·德波在《漂移理论》一书中指出,漂移是“一种快速通过各种环境的技巧”,是指对物化的城市生活,特别是建筑空间布局的凝固性的否定。《青春咖啡馆》的故事发生在六十年代初,正是情景主义者活动最如火如荼的时期。作品中的人物似乎都在按照情景主义者的规则生活着,跟那些情景主义者一样,他们都认为工作和学习是束缚人的,“永远也别工作”,写在墙上的标语非常醒目。


小说结构


这部小说以上世纪六十年代的巴黎为背景,巴黎塞纳河左岸的拉丁区,一家名叫孔岱的咖啡馆,吸引了一批年轻人。他们四处漂泊,居无定所,放荡不羁,过着今朝有酒今朝醉的生活,他们之中有作家、艺术家、大学生,他们沉湎于酒精和毒品。故事就从他们当中一个名叫露姬的女子出现、失踪、追忆到后来她自杀的过程来展开故事的。整个故事并不完全是以线性方式展开,而是通过四个人的视角来讲述一个相互交叉的故事。读完这本书,我有一个疑问,这种叙述方式是否已经成为当代文学的一种常态,福克纳、略萨的作品都用了这种方式。


叙述者一(巴黎高等矿业学院的学生)


第一个叙述者是一名巴黎高等矿业学院的学生,他也是孔岱咖啡馆的常客,通过他的视角我们第一次见到本文的女主角,“露姬”这个名字也是他给取的。他还带领我们参与了咖啡馆的日常,认识了咖啡馆的服务生和客人,其中一个人称“船长”的客人连续三年记录了咖啡馆客人到达的时间和他们的住址,“船长”离开巴黎时将记录的笔记本留个了这位讲述者,他不断的翻阅笔记本,露姬在他的追寻下,一步一步的清晰起来。露姬是一个蓝眼睛,棕色头发,指甲修长的美女。她身体笔直,形态优雅,一言不发,谨小慎微,手里经常拿着一本《消失的地平线》,与咖啡馆的其他人格格不入。叙述者因此断定“她到孔岱这里,是来避难的,仿佛她想躲避什么东西,想从一个危险中逃脱。”这是从一个陌生人的角度对女主角的描述。


叙述者二(盖世里,一名私家侦探)


第二个叙述者名叫盖世里,是一名私家侦探。他讲述了一个名叫让-皮埃尔·舒罗的人,委托他寻找舒罗离家出走几个月的妻子雅克林娜,据舒罗说,他是在自己的房地产公司结识雅克林娜的,她是他的秘书,为了“建立关系”,他们二人结婚,婚后雅克林娜觉得丈夫无趣,狠心离家出走。盖世里很快查明雅克林娜的真实身份,她二战时期出身于索洛涅,没有父亲,由母亲单独抚养,母亲在红磨坊当服务员。她幼年时,母亲上班时经常将她一人留在家里,她因孤单和恐惧,时常在母亲上班时外出闲逛,并沾染上毒品,两次因“未成年流浪”被警察抓走。在盖世里的深入调查下,雅克林娜就是露姬这一事实得以解开。随着了解的进行,盖世里被雅克林娜的经历打动,似乎理解了她离家出走的缘由,最后,放弃侦查,不再干扰她的生活。这是从一个侦探的角度让人物从模糊走向清晰的过程。


叙述者三(女主人公,本人)


第三个叙述者是露姬(雅克琳娜)本人。她讲述了她在十八区拉谢尔大道10号度过的童年和少年时光。晚上,她母亲上班不在家,她就偷偷溜出去闲逛。一次在九区,一次在十八区,一个男人和一个女人,为她提供了战胜恐惧的“良药”,她尝试吸毒,吸过之后她不再感到恐惧。在克里希林荫大道,她结识一家专门售卖科幻和天文学书籍的书店的老板,老板送了一本《无限之旅》的书给她,让她体验了阅读的乐趣。她解释了自己为什么总想逃走“每次我和什么人断绝往来,我都感到一种沉醉。”不管是年少时的离家出走,还是从丈夫家的逃离,都是居于这样的原因。这是从当事人角度的叙述,部分回答了第一叙述人和第二叙述人的疑惑,让谜一样的露姬逐渐的立体丰满起来。


叙述者四(罗兰,露姬的情人)


第四个叙述人是露姬的情人罗兰。罗兰是一个刚入门的作家。他与露姬在一个名叫居伊·德·威尔组织的聚会上认识,威尔是一个神秘学家,也是这群年轻人的精神导师,他推荐露姬阅读《消失的地平线》和《不存在的路易斯》。罗兰和露姬相识相爱,露姬不想光顾能勾起她痛苦回忆的拉丁区,十六区又离她丈夫家很近,他们因此生活在中立地区,但也并不感到安全,于是谋划出国旅游,还未成行,露姬突然跳窗自杀,故事戛然而止。


哲理


《青春咖啡馆》作者是2014年诺贝尔文学奖得主,他生活在法国,深受法国哲学思想的熏陶,他在这本书中大段引用了法国哲学家吉尔·德勒兹的“逃逸线“概念。


“逃逸线”(ligne de fuite)是法国哲学家德勒兹(1925-1995)经常使用的概念,在后期经典之作《千座高原》中,他详细区分了三种类型的“线”:坚硬线、柔软线和逃逸线。





  • 坚硬线指质量线,透过二元对立所构建僵化的常态,比如说人在坚硬线的控制下,就会循规蹈矩地完成人生的一个个阶段,从小学到大学到拿工资生活到退休。





  • 柔软线指分子线,搅乱了线性和常态,没有目的和意向。





  • 逃逸线完全脱离质量线,由破裂到断裂,主体则在难以控制的流变多样中称为碎片,这也是我们的解放之线,只有在这条线上我们才会感觉到自由,感觉到人生,但也是最危险之线,因为它们最真实。




故事的主人公露姬就是一个摆动在坚硬线和逃逸线之间的女孩子,她说:



后来,我每次与什么人断绝往来的时候,我都能重新体会到这种沉醉。只有在逃跑的时候,我才真的是我自己。我仅有的那些美好回忆都跟逃跑或者离家出走连在一起。但是,生活总会重新占据上风。当我走到迷雾街时,我深信有人约我在此见面,这对我来说又会是一个新的起点。(p82)



露姬的心理


一方面她渴望逃脱出坚硬线的束缚,追求自己认为的幸福,自己认为的“真正的生活”,每当她逃离的时候,她都会感到一种沉醉、自由。另一方面,她不断逃跑,但是后来生活总会占据上风,这是一个从“逃逸线”回归“坚硬线”的过程。


从她的逃跑经历来看,在露姬十三四岁的时候,她母亲上班要上到凌晨两点,于是露姬喜欢一个人在半夜出门,在大街上走来走去。这导致她曾两次因未成年流浪被警察叔叔送回了家,这就是她最开始体会到,她离家出走后生活总会重新占据上风,她又会进入一个有明确意义的事件中。


之后她母亲去世,露姬与一个名叫舒罗的受过良好教育的中产结婚了,她的结婚又是一种对“坚硬线”的回归。



可她为什么要嫁给舒罗?结婚之后再次出逃,但这一次却是朝左岸逃,就好像过河之后,她就逃脱了迫在眉睫的危险,并得到了保护。可是,这桩婚姻对她来说不也是一种保护吗?假如她有足够的耐心呆在诺伊利,久而久之,人们就会忘记在让-皮埃尔·舒罗夫人的底下还藏着一个雅克林娜·德朗克。(p53)



露姬不断逃离,不断融入新的关系之中,“他们只是尝试建立关系”。露姬总是在逃离那个幸福不在的地方,但当她到了一个新的地方,她又觉得幸福不在那里。



她说,真正的生活,不是这样的。可当他问她那种“真正的生活”到底是什么样子时,她就一直耸肩膀,不置一词,就好像说了也是白说。(p39)



结合拉康精神分析


可以说,露姬是一个在不断逃离大他者和小他者的女孩子。她在逃离那些社会规范,比如家庭、婚姻、学校,即所谓的大他者。她也在逃离他人的想象,比如她尝试与她原来在酒吧的那些朋友绝交。



我的心中充满了沉醉的感觉,这种沉醉是酒精或者那雪什么的永远给不了的。我往上一直走到迷雾城堡。我已经痛下决心永远也不和康特尔酒吧里的那帮人见面了。



她希望逃离一切束缚,但是她根本没有意识到她无法逃离大他者和小他者,她从家庭逃到婚姻再逃到友情、爱情,永远都有一个大他者存在。同时,她伪造自己的真实身份,骗别人说自己是学习东方语言的大学生,说自己母亲是会计师。她认为在社会中,“学习东方语言的大学生”似乎比中学辍学更加体面,会计师比红磨坊夜总会的服务员更加体面。她始终没有逃离那个大他者带给她的评价标准,那个无处不在的大他者。因此,她的逃离最终必然以失败告终,也正是如此,最后露姬选择以跳楼自杀的方式,逃离那个无处不在的重力,逃离这个无处可逃的世界。


那么,您找到您的幸福了吗?


这句话贯穿了全书,最开始是书店老板莫名奇妙地一句话,这句话深深地印在露姬的脑海里。



有人言之凿凿地告诉我:人唯一想不起的东西就是人说话的嗓音。可是,直到今天,在那些辗转难眠的夜晚,我却经常能听见那夹带巴黎口音——住在斜坡街上的巴黎人——的声音问我:“那么,您找到您的幸福了吗?”



译后记·四


这本书的译者是金龙格先生,他在译后记中这样分析“那么,您找到您的幸福了吗?”这句话。


《青春咖啡馆》以一种既写实又神秘的笔调,交织谱出青春岁月的青涩、惶惑、焦虑、孤独寂寞与莫名愁绪,描写一个弱女子从不断探寻人生真谛到最终放弃生命追寻的悲剧命运,这个悲剧发生在一个既有着迷人的魅力又像谜一样难以捉摸的年轻美丽女子身上,更使全书充满一种挥之不去的忧伤情调。书中的一句问话像哲学命题一样尤其发人深省:“您找到了您的幸福吗?”,可是,人能找到自己的幸福吗?人终其一生到底能够得到什么?小说中的主人公什么都尝试过了,最终却一无所获。莫迪亚诺的小说似乎在告诉我们,幸福只是昙花一现的东西,人生寻寻觅觅,到头来得到的只有落寞、失去、不幸、迷茫,只有时时袭来的危机与恐慌,只有萍踪不定的漂泊,只有处在时代大潮中身不由己的无奈和顾影自怜的悲哀。


总结


《青春咖啡馆》的法语原文是Dans le café de la jeunesse perdue,意思是“在咖啡馆里青春消逝”,作者以一种伤感的笔调描述了露姬消逝的青春,但是在我们的一生中,青春虽然过去了,但并不会消逝,它只会永远的封存在我们的记忆之中。


在我们的青春年代,或许学业、社会的压力,父母、老师的规训让我们也有点想要逃离,去追寻自己所认为的幸福所在。虽然露姬生活在20世纪60年代,但其实人们的心底或多或少都有露姬的影子,就比如那个巴黎高等矿业学校的大学生、侦探盖世里、情人罗兰,包括你我。我们或许向往着那种不断逃离的生活,这就是当时情景主义者的生活,但作者并没有站在道德制高点对这种生活做出评判,只是如实进行记录,让读者跟着他的笔触,去认识去感受曾经有那样一群人、一个人这样生活过。探讨人生价值的文学作品屡见不鲜,网络发达的今天,许多人也会在网上总结、抱怨、指导、感悟人生的意义,不同的人会有不同的答案,这个答案不可能统一,也没有必要统一,千差万别的人生才是真正的人生。


作者:yueyh
来源:mdnice.com/writing/e894ad0fefa54767a64e22f5a7ac50ff
收起阅读 »

如何正确编写一个占满全部可视区域的组件(hero component)?

web
什么是 hero component hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。 我们经常见到这种...
继续阅读 »

什么是 hero component



hero component 或者 hero image 是一个网页设计术语,用于描述欢迎访问者访问网页的全屏视频、照片、插图或横幅。该图像始终位于网页顶部附近的显着位置,通常会在屏幕上延伸整个宽度。



我们经常见到这种 hero component,在视觉上占据了整个视口的宽度和高度。比如特斯拉的官网:


image.png


如何实现一个 hreo component?


很多人可能会不假思索的写出下面的 css 代码:


.hero {
width: 100vw;
height: 100vh;
}

你写完了这样的代码,满意地提测后。测试同学走过来告诉你:你的代码在苹果手机上不好使了!


image.png


如图所示,hreo component 被搜索栏挡住了。


又是 safari 的问题!我们可以先停止关于 Is Safari the new Internet Explorer? 的辩论,看看问题的成因和解决办法。


什么是视口单位


vh 单位最初存在时被定义为:等于初始包含块高度的 1%。



  • vw = 视口尺寸宽度的 1%。

  • vh = 视口大小高度的 1%。


将元素的宽度设置为 100vw,高度设置为 100vh,它就会完全覆盖视口。这就是上面 hero component 实现的基本原理:


image.png
这看起来非常完美,但在移动设备上。苹果手机的工程师觉得我们应该最大化利用手机浏览器的空间,于是在 Safari 上引入了动态工具栏,动态工具栏会随着用户的滑动而收起。


页面会表现为:高度为 100vh 的元素将从视口中溢出


image.png


当页面向下滚动时,动态工具栏会收起。在这种状态下,高度设为 100vh 的元素将覆盖整个视口。


image.png



图片来自于:大型、小型和动态视口单元



svh / lvh / dvh


为了解决上面提到的问题,2019 年,一个新的 CSS 提案诞生了。



上面的解释来自于 MDN,读起来有点拗口,其实顾名思义,再结合下面的动图就很好理解:
dvh.gif



在 tailwindcss 的文档中,对 dvh 有一个漂亮的动画演示:tailwindcss.com/blog/tailwi…



用以上的属性可以完美解决上面的 safari 中视口大小的问题,值得一提的是,有的人会建议始终使用 dvh 代替 vh。从上面的动图可以看到,dvh 在手机上其实会有个延迟和跳跃。所以是否使用dvh还是看业务的实际场景,就我而言,上面的 hero component 的例子使用 lvh 更合适。


兼容性


你可以在 can I use 中查看兼容性:


image.png


可以看到,这三个 css 属性还算是比较新的特性,如果为了兼容旧浏览器,最好是把 vh 也加上:


.hero {
width: 100vw;
height: 100vh;
height: 100dvh;
}

这样,即使在不支持新特性的浏览器中,也会降级到 vh 的效果。


感谢阅读本文~


作者:李章鱼
来源:juejin.cn/post/7352079427863592971
收起阅读 »

如何完成一个完全不依赖客户端时间的倒计时

web
前言 最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antd 的 CountDown 组件。 于是我就愉快的写出以下代码: import { Statistic } fro...
继续阅读 »

前言


最近在做一个调查问卷系统,其中有一个需求就是倒计 40 分钟以后自动提交问卷。由于 UI 库使用的是 antd,所以我第一反应是使用 antdCountDown 组件。
于是我就愉快的写出以下代码:


import { Statistic } from 'antd';
const { Countdown } = Statistic;

const TOTAL_TIME = 40;
const deadline = dayjs(startTime).add(TOTAL_TIME, 'minute').valueOf();


function TitleAndCountDown() {
useEffect(() => {
if (currentTime >= deadline) {
onFinish();
}
}, []);

return (
<Countdown
value={deadline}
onFinish={onFinish}
format="mm:ss"
prefix={<img src={clock} style={{ width: 25, height: 25 }} />
}
/>

);
}

其中 startTimecurrentTime 是服务端给我返回的开始答题时间以及现在的时间,onFinish 是提交问卷的函数。测试一切正常,并且看起来好像没有依赖客户端时间,于是我就愉快的提交了代码。


antd 的问题


上线后,有客户反映倒计时不正常,进入系统后直接显示 9000 多秒,导致业务直接进行不下去。这个时候我就懵了,我的代码中并没有依赖任何客户端时间,问题肯定是出现在 antdCountDown 组件上。于是我就去看了一下 antdCountDown 组件的源码,果不其然


 // 30帧
const REFRESH_INTERVAL= 1000 / 30;

const stopTimer = () => {
onFinish?.();
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};

const syncTimer = () => {
const timestamp = getTime(value);
if (timestamp >= Date.now()) {
countdown.current = setInterval(() => {
forceUpdate();
onChange?.(timestamp - Date.now());
if (timestamp < Date.now()) {
stopTimer();
}
}, REFRESH_INTERVAL);
}
};

React.useEffect(() => {
syncTimer();
return () => {
if (countdown.current) {
clearInterval(countdown.current);
countdown.current = null;
}
};
}, [value]);

核心代码就是这段,本质 CountDown 并不是一个倒计时,而是根据客户端时间算出来的一个时间差值,这也能解释为啥这个倒计时相对比较准确。


但是依赖了客户端时间,就意味客户的本地时间会影响这个倒计时的准确性,甚至可以直接通过修改本地时间来绕过倒计时。一开始我的方案是加入 diff 值修正客户端时间,我也给 antd 官方提了一个 PR,但是被拒绝了。后来想了一下 CountDown 组件可以直接传入 diff 后的 value,确实没有必要新增 props


这个方案后来也是被否了,因为还是依赖了客户端时间。客户的机房条件比较复杂,可能一开始时间不对,但是做题途中时间会校正回来。因为我们这个调查系统短时间有几十万人参加调查,为了不给服务器过多的压力,查询服务器时间接口的频率是 1 分钟一次,所以会有很长时间的倒计时异常。


完全不依赖客户端时间的倒计时


倒计时的方案大致有 4 种, setTimeoutsetIntervalrequestAnimationFrameWeb WorkerrequestAnimationFrameWeb Worker 因为兼容性问题暂时放弃。


setInterval 实现倒计时是比较方便的,但是 setInterval 有两个缺点



  1. 使用 setInterval 时,某些间隔会被跳过;

  2. 可能多个定时器会连续执行;


每个 setTimeout 产生的任务会直接 push 到任务队列中;而 setInterval 在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)。


可以看到,主线程的渲染都会对 setTimeoutsetInterval 的执行时间产生影响,但是 setTimeout 的影响小一点。所以我们可以使用 setTimeout 来实现倒计时.


const INTERVAL = 1000;

interface CountDownProps {
restTime: number;
format?: string;
onFinish: () => void;
key: number;
}
export const CountDown = ({ restTime, format = 'mm:ss', onFinish }: CountDownProps) => {
const timer = useRef<NodeJS.Timer | null>(null);
const [remainingTime, setRemainingTime] = useState(restTime);

useEffect(() => {
if (remainingTime < 0 && timer.current) {
onFinish?.();
clearTimeout(timer.current);
timer.current = null;
return;
}
timer.current = setTimeout(() => {
setRemainingTime((time) => time - INTERVAL);
}, INTERVAL);
return () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
};
}, [remainingTime]);

return <span>{dayjs(remainingTime > 0 ? remainingTime : 0).format(format)}</span>;
};

为了修正 setTimeout 的时间误差,我们需要在 聚焦页面的时候 以及 定时一分钟请求一次服务器时间来修正误差。这里我们使用 swr 来轻松实现这个功能。


const REFRESH_INTERVAL = 60 * 1000;

export function useServerTime() {
const { data } = useSWR('/api/getCurrentTime', swrFetcher, {
// revalidateOnFocus 默认是开启的,但是我们项目中给关了,所以需要重新激活
revalidateOnFocus: true,
refreshInterval: REFRESH_INTERVAL,
});
return { currentTime: data?.currentTime };
}

最后我们把 CountDown 组件和 useServerTime 结合起来


function TitleAndCountDown() {
const { currentTime } = useServerTime();

return (
<Countdown
restTime={deadline - currentTime}
onFinish={onFinish}
key={deadline - currentTime}
/>

);
}

这样,就完成了一个完全不依赖客户端时间的倒计时组件。


总结



  • 上面方案中的 setTimeout 其实换成 requestAnimationFrame 计时会更加准确,也解决了 requestAnimationFrame未被激活的页面中 中不会执行的问题。

  • setIntervalsetTimeout 的时间误差是由于主线程的渲染时间造成的,所以如果我们的页面中有很多的动画,那么这个误差会更大。

  • 未激活的页面,setTimeout 的最小执行间隔是 1000ms


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

回县城躺平,感觉我的人生过得好失败

从春节前到现在,一个半月没更新了,做啥都提不起劲来。 越来越感觉我的人生过的好失败。 去年我爸因为癌症去世了,家里的门头房用不到了,就想卖掉,找好了买家,价格谈了 140 万。 当时想买我们这个房子的人很多,那个买家怕我们卖给别人,就拿了 20 万定金,和我们...
继续阅读 »

从春节前到现在,一个半月没更新了,做啥都提不起劲来。


越来越感觉我的人生过的好失败。


去年我爸因为癌症去世了,家里的门头房用不到了,就想卖掉,找好了买家,价格谈了 140 万。


当时想买我们这个房子的人很多,那个买家怕我们卖给别人,就拿了 20 万定金,和我们签了一个买房合同。


没想到因为这个房子因为在我爸名下,涉及到继承,需要我奶奶放弃继承才可以过户。


我奶奶现在生活不能自理,由我大娘二大娘养着,然后他们提出条件来,要我们的宅基地,也就是村里的老房子。


我妈开始不同意,因为她想之后从那里出殡,和我爸合葬。


后来也同意了,在哪不能殡出去呢?


然后我这边准备好了材料之后,我堂姐跳了出来,一拍桌说,不行,老房子我们也要,另外你还要给我们 25 万。


她们说咨询了律师要是打官司的话,我们青岛的房子也要分,那个门头房也要分,另外我爸的银行流水也要查,起码得分她们 40 万。


我给讲价到了 20 万,但是我妈不同意,说是她和我爸辛苦攒下的家业凭什么白给他们。


我妈这边也找了律师,给出的意见就是拖着就行,一辈子不卖了。


这时候买房子的不干了,说是合同上写了,如果违约,要双倍返回定金,也就是赔她们 20 万。


当时我们以为就是个收到条就签了,没想到在这等着呢。


其实我们早就把定金返给她了,也说了我们家的情况,但她就是不行。


年前就一直威胁我们要告,刚过完年,马上又来了:



我妈问了下和谈的话怎么谈,她说起诉你赔我 25 万,和谈赔我 18 万。



两头挤我们,家里就要我们老房子 + 21 万,卖房子的就要我们 20 万违约金。


我们夹在中间,几度濒临崩溃。


我想了下,这件事早晚得解决,反正都是一家人,给家里人点钱也没啥。


然后我前几天去找我大爷二大爷,还有堂姐、堂哥坐在一起谈了,我说我同意这 21 万。


最终转了她们 18 万(老房子折合了 2 万块),然后又拿了 1 万律师费(他们请的律师让我拿律师费),还有同意给他们老房子。


但我提的要求是和我妈只能说是 10 万 + 老房子,不然我妈不会同意的。


就这样,我们顺利公证过了户。


公证放弃继承那天,我奶奶才刚知道这个事,她说她只要老房子,不要我爸的其他遗产。


但没办法,她不要不行,我大爷二大爷要啊。


这个房子卖了,到手 120 万。


然后还有青岛的房子,这个房子我买的时候是 70万首付 + 100 万贷款,一共是 200 万下来的,最近我妈又花了 7 万装修。


因为不去青岛住,也打算卖了,中介说价格不到 80 万还是可以卖掉的。



这么一算,这边亏了 120 万,那边房子卖了剩下 120,相当于我爸就给我留下了 70 多万还不好卖的房子。


其实我爸这辈子攒了不少钱,差不多 300 万,都是省出来的,从小我跟着他就是一天吃一顿那种,从没感觉到家里是有钱的。


再就是他对我妈也不好,前几年的时候经常打骂,后来我妈离家出走了,但是他生病了还是会来照顾他。


我爸癌症住院那段时间,生活不能自理,都是我妈没日没夜的照顾他。


临走之前,我爸一只手抓着我的手,一只手抓着我妈的手,然后让我们好好相处,他一直觉得对不起我妈,口里一直喊着“从头再来、从头再来”。


我送我爸去火葬场的时候,送我骨灰盒爸入土的时候,我也一直在说,“爸,你别怕,我们从头再来”。


其实我爸葬礼上,我没有咋哭出来,可能当时没大反应过来。


但是之后好长一段时间,我在村里别人家坐着聊天,一谈起我爸,就再也忍不住了,哭的稀里哗啦的。


我有个干兄弟,在村里拜了干爹,因为疫情好多年也没走动了,但是我爸的棺材是他帮忙抬出去的。


而我大爷二大爷就在一边看着。


我今年过年给他家送了礼,我说,我妈说我爸是你们抬出去的,让我一辈子记得你们的好。


当时说到这里,没忍住,又哭的稀里哗啦的。


我想我爸这辈子,是攒了不少钱,但是不舍得吃不舍得喝的,还在房子上亏了半辈子的积蓄。


对老婆孩子不好,临走前才后悔想着从头再来。


我想我前五年是赚了不少钱,但因为工作,好多年没回家,和家人一年待在一起也就几天。


而且最后都赔在青岛的房子上了。


人这一辈子,到底图啥呢?


年后这几周我找了找工作,有几个不错的 offer,都是 base 40+ 那种。



但我又不那么想出去了。


我这一年没工作,其实和我妈在一块生活还是很踏实的。


而且家里房子卖了,青岛的房子也快了,这样我的存款差不多能到 300w。存定期的话每年银行利息 8w 左右,够我生活了。


就这样在家躺平一辈子是不是也不错。


王小波说,那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。


韩寒说,平凡才是唯一的答案。


小的时候,我希望长大后的自己会光芒万丈。


长大以后,我希望自己有个好的工作打工到退休。


现在的我只想躺平。


我觉得我自己的人生很失败:


打工这些年,钱都赔在房子上了。


我比较社恐,永远达不到我妈对我的会来事、会察言观色的期望。


人家都在大城市结婚生子、买房定居了,而我又回到了小县城。


当年和我同时入职高德的朋友都升 p7 了,我现在啥也不是:



我是 94 年的,今年就 30 了,人生的各种机会和可能性越来越少。


后半辈子,我应该就是在小县城躺平,度过余生了。


但文章我还是想继续写,毕竟人这一生总要坚持点什么。


作者:zxg_神说要有光
来源:juejin.cn/post/7343503718183059471
收起阅读 »

分支管理:master,release,hotfix,sit,dev等等,听着都麻烦。

背景 从一开始的svn到后来接触到git,到目前已经和git打交道比较多了,突然觉得可以把项目中所用到一些分支的管理方式分享出来,希望帮助到大家。 分支介绍 现在使用git一般很少是单个分支来使用,通常是多个分支来进行,接下来我以我最新的项目中所用到的分支,来...
继续阅读 »

背景


从一开始的svn到后来接触到git,到目前已经和git打交道比较多了,突然觉得可以把项目中所用到一些分支的管理方式分享出来,希望帮助到大家。


分支介绍


现在使用git一般很少是单个分支来使用,通常是多个分支来进行,接下来我以我最新的项目中所用到的分支,来做一些介绍。


master



  • master分支代码只能被release分支分支合并,且合并动作只能由特定管理员进行此操作。

  • master分支是保护分支,开发人员不可直接push到远程仓库的master分支


release



  • 命名规则:release/*,“*”一般是标识项目、第几期、日期等

  • 该分支是保护分支,开发人员不可直接push,一般选定某个人进行整体的把控和push

  • 该分支是生产投产分支

  • 该分支每次基于master分支拉取


dev



  • 这个是作为开发分支,大家都可以基于此分支进行开发

  • 这个分支的代码要求是本地启动没问题,不影响其他人的代码


hotfix



  • 这个分支一般是作为紧急修复分支,当前release发布后发现问题后需要该分支

  • 该分支一般从当前release分支拉取

  • 该分支开发完后需要合并到release分支以及dev分支


feat



  • 该分支一般是一个长期的功能需要持续开发或调整使用

  • 该分支基于release创建或者基于稳定的dev创建也可以

  • 一般开发完后需要合并到dev分支


分支使用


以上是简单介绍了几个分支,接下来我针对以上分支,梳理一些场景,方便大家理解。


首先从master创建一个release分支作为本次投产的分支,然后再从master拉取一个dev分支方便大家开发,dev分支我命名为:dev/soe,然后我就在这个分支上进行开发,其他人也是这样。


然后当我开发完某个任务后,又有一个任务,但是呢,这个任务需要做,只是是否要上这次的投产待定,所以为了不影响到大家的开发,我就不能在dev分支进行开发了,此时我基于目前已经稳定了的dev分支创建了一个feat分支,叫做:feat/sonar,主要是用来修复一些扫描的问题,在此期间,如果我又接到了开发的任务,仍然可以切换到dev来开发,并不影响。


当开发工作完成后,并且也基于dev分支进行了测试,感觉没问题之后,我就会把dev分支的代码合并到release上。


当release投产之后,如果业务验证过也没有问题,那么就可以由专人把release合并到master了,如果发现了问题,那么此时就需要基于release创建一个hotfix分支,开发人员在此分支进行问题的修复,修复完成并测试后,合并到release分支和sit分支。然后再使用release分支进行投产。


总结


以上就是我在项目中,对分支的使用,我觉得关于分支使用看团队以及项目的需要,不必要定死去如何如何,如果有的项目不规定必须要release投产,那么hotfix就不必使用,直接release修改完合并也未尝不可,所以大家在项目中是如何使用的呢?可以评论区一起讨论分享。


致谢


感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。


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

年会结束,立马辞职了!

那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。 那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。 可能只有我们做技术人的心里才会觉得“技术...
继续阅读 »

那是发生在多年前的一件事,当时我也是在那家公司做 Java 开发。公司很大,大到去了很长一段时间都感觉毫无存在感。


那年年会,作为技术部的我,依然被安排到一个比较边缘化的桌子,这么多年走来,早已经习惯了这样的安排。


可能只有我们做技术人的心里才会觉得“技术牛逼,技术万岁!”,但在公司领导层看来,这技术研发部就是整个公司开销最大的一个部门,又不能直接产生效益,但开除了又不合适,还要靠他们干活呢,这真是一件即讽刺、又无奈的事儿啊。


说回正题,那年公司所有人依旧是尴尬的、极不情愿的、又不得不碍于情面凑在一起,听完了所谓的又毫无意义的年终总结,然后又敷衍的敬完酒之后,才能装模作样的挥手告别亲爱的同事。


我之所以,要等待年会的第二天才告诉我的顶头上司“我要离职”的主要原因是,年会的时候才给大家集中发年终奖。


我也是领到钱之后就不装了,我摊牌了,第二天就找到了领导,告诉他,我要离职了。这个时候上司也知道你的心思,话已经收出来了,尤其是离职的事,大概率是劝不回来了,毕竟覆水难收。大家都是明白人,寒暄了几句之后,就签了离职的申请。


工作就像谈对象,合不来也没必要勉强。那时候开发的行情还很好,出去面试 4 家公司,最少也能拿 3 个 Offer,所以跳槽基本都是裸跳,一副此地不留爷,自有留爷处的傲娇姿态。


然而,年终奖是拿到手了,新工作也很快又着落了,薪资每次跳槽也能涨到自己满意的数,但干着干着发现,好像还是原来的配方,还是原来的味道,好像也不是理想中的工作嘛。


于是,在周而复始的折腾中才发现,只要是给别人上班,永远不会有理想中的工作,因为上班的本质是你替别人办事,别人给你发薪水,工作从来都是简单的雇佣关系,那来的别人要为你的理想来买单嘛,这本来就不合理,只是想明白这点时,以是上班了十年之后(此处可见自己的笨拙)。


理解了这点之后,我才发现,给任何公司上班的区别不会太大,无非是钱多钱少、活多活少、周围人好相处与否的细微差别,但碍于生计,又不得不苟延残喘的上下班,这可能是大部分打工人的真实感受和现状了。


但即使这样,你依然会发现,你的岗位正在被新人所替代,你的选择也变的越来越少,你的挣钱能力也变的越来越弱,这可能就是所谓的“中年危机”吧。所以说“中年危机”这个词,不是那个行业的专属名称,而是所有行业共性,那要怎么解决呢?


三个小小的建议:




  1. 尽量不要买房:不要和自己过不去,买房一时爽,还贷“火葬场”。我有一个朋友,一个月 2.1W 的房贷,生活中哪怕有一点点小小的变动,对于他来说都是不可承受之殇。“如履薄冰”也不过如此吧?


  2. 培养自己的第二职业:找到自己感兴趣点,并且它能帮你长久的带来经济收益最好,不求大富大贵,只要能够日常开支已经很不错了。任何时候有准备都比没准备要强很多。还有,在做之前,不要怕起步晚、进步慢,只要肯坚持,终会有收获。路虽远,行则将至;事虽难,做则必成。


  3. 提升自己主业的能力:任何时候,提升自己主业的能力,都是收益最大的投资,也是最明智的投资,当你看不清前进的道路时,当你感觉人生黯淡无光事,唯有干好自己目前本职的工作,才是最优的选择,这也能让你为以后的新计划积攒足够的能量。


最后,愿新的一年里:奔赴热爱、享受自由,找到自己热爱的事,并为之努力。加油,XDM~


作者:王磊
来源:mdnice.com/writing/728744844d414145b2efa61ec218606c
收起阅读 »

产品经理的多维度划分

产品经理的进阶是一个涉及专业技能提升、经验积累、视野拓宽以及领导力培养的过程。一.产品经理的层次划分以下是一些产品经理进阶的关键步骤和能力要求:初级产品经理阶段:需求细化与执行:学会撰写高质量的产品需求文档(PRD)、绘制产品原型,具备基本的设计理解和一定的技...
继续阅读 »

产品经理的进阶是一个涉及专业技能提升、经验积累、视野拓宽以及领导力培养的过程。

一.产品经理的层次划分

以下是一些产品经理进阶的关键步骤和能力要求:

  1. 初级产品经理阶段

    • 需求细化与执行:学会撰写高质量的产品需求文档(PRD)、绘制产品原型,具备基本的设计理解和一定的技术背景知识,能够准确传达并跟进需求的执行。
    • 沟通协作:与开发、设计、运营等部门紧密合作,确保需求被正确理解和实施。
    • 基础技能:掌握需求分析、文档编写、原型设计工具使用、基本的用户研究和竞品分析。
  2. 中级产品经理阶段

    • 主动挖掘与项目管理:具备独立进行用户研究、数据分析、竞品分析的能力,主动寻找和验证市场需求,制定并执行产品策略。
    • 产品决策:开始涉及产品方向的选择与功能优化的取舍,具有更全面的产品视角和全局观。
    • 项目管理:有效管理产品生命周期中的各个阶段,保证项目的进度和质量。
  3. 高级产品经理或产品专家阶段

    • 战略规划:负责产品体系的整体规划,形成独特的产品方法论,能够预见和引领行业趋势。
    • 领导力:带领产品团队,负责整个产品线的战略布局和生命周期管理,影响团队成员成长和决策。
    • 成功案例与影响力:拥有成功的产品案例,能通过实践提炼出可复制的产品模式和最佳实践,对外输出方法论和思想领导力。
  4. 管理线进阶

    • 团队建设:组建和管理高效的产品团队,培养人才梯队,进行有效的团队激励和绩效管理。
    • 业务拓展:推动产品商业化进程,对市场反馈敏感,根据市场变化调整产品战略。
  5. 专业线深化

    • 领域专家:在某一细分领域成为专家,深入研究行业规则和技术发展趋势,指导复杂产品解决方案的构建。

此外,产品经理在进阶过程中还需要不断学习和适应新技术、新商业模式的变化,持续提升跨部门协调、解决问题、创新思维和决策能力。同时,良好的自我管理和情绪智商同样是职业发展中不可忽视的素质。随着职位的提升,产品经理可能面临更多战略层面的问题,因此需要不断提升自身的商业洞察力和战略规划能力。

二.高级产品经理能力模型

高级产品经理的能力模型通常涵盖了以下几个关键领域:

  1. 战略思维能力

    • 深入理解行业发展趋势,能够制定长远的产品发展战略和路线图。
    • 能够结合公司整体战略目标,明确产品定位,设定并达成有挑战性的产品愿景和目标。
  2. 用户洞察与需求提炼

    • 具备深入的用户研究和洞察力,能精准把握用户痛点与需求,并转化为产品特性。
    • 利用数据分析手段,量化用户行为,驱动产品迭代优化。
  3. 数据驱动决策

    • 强大的数据解读和分析能力,利用A/B测试、用户行为数据等工具,以数据为基础做出科学决策。
    • 能够建立和完善数据指标体系,用于衡量产品性能和市场表现。
  4. 跨部门协同与领导力

    • 出色的团队管理和组织协调能力,能有效调动内外部资源,推动跨职能团队合作。
    • 具备卓越的领导魅力,激发团队潜能,带领团队完成复杂项目。
  5. 创新与解决问题能力

    • 在面对复杂问题时,能够提出创新解决方案,突破现有框架,引领产品创新。
    • 对技术和市场变化保持敏锐度,预见并应对潜在的竞争威胁和市场机会。
  6. 专业技术能力

    • 深厚的产品设计理论基础,熟练掌握产品生命周期管理、敏捷开发流程等专业技能。
    • 能够深入参与产品设计和技术实现细节,与工程师团队有效沟通。
  7. 商业敏感性与财务知识

    • 具备较强的商业头脑,能从经济和盈利角度考虑产品发展,平衡用户价值与商业价值。
    • 了解财务模型和成本效益分析,能够在产品设计中合理权衡投入产出。

综上所述,高级产品经理不仅需要具备扎实的产品设计和管理能力,还需要有很强的跨领域整合能力、领导力以及对市场、技术、用户和商业的深刻理解,从而成功引领产品的长期发展和市场竞争。

三.产品经理的多维分类

产品经理的角色可以根据多个维度进行分类,以下是几个主要分类方式及其具体类型:

1. 按照服务对象划分:

  • B端产品经理 (Business-to-Business, B2B):主要负责为企业级客户提供产品和服务,例如企业软件、SaaS解决方案、云服务、后台系统等,这类产品经理需要深入理解客户业务流程,解决企业级痛点,注重产品的易用性和效率提升。

  • C端产品经理 (Business-to-Consumer, B2C):专注于为消费者打造产品,这涵盖各种消费级应用、网站、游戏、电商产品等,要求产品经理深入了解用户需求、偏好及行为模式,提供优秀的用户体验。

  • G端产品经理(Business-to-Government, B2G):G端产品经理是指专为政-府机构服务的产品经理,他们聚焦于政务信息化领域,负责设计、优化和管理面向政-府内部或公众的政务类数字产品。核心任务包括:依据政-府业务需求定制信息化解决方案,如搭建行政审批、公-文处理等内部管理系统;开发一站式便民政-务服务产品,如政-务服务网、政-务APP等,实现政-策查询、在线办理等功能

2. 按前后端划分:

  • 前端产品经理:专注于用户界面和交互设计,确保用户能够直观地使用产品,关注用户体验的全流程,包括UI/UX设计、页面跳转逻辑、信息展示结构等。

  • 后端产品经理:主要关注产品后台系统的设计和维护,包括但不限于数据存储、服务器架构、API接口设计、性能优化等非用户直觉感知的功能部分。

  • 中台产品经理:主要负责设计和规划公司的业务中台产品,确保中台系统的稳定、高效运行,并赋能前台业务快速发展。业务中台作为一种重要的IT架构模式,旨在沉淀和复用企业的核心能力,如用户中心、订单中心、商品中心等,为多个前台业务提供通用、灵活且可配置的服务。

3. 按照职能分类:

  • 功能型产品经理:主要职责是设计和优化产品功能,确保功能满足用户需求和业务目标。

  • 战略型产品经理:负责产品整体战略规划,确定产品定位、发展方向,以及基于市场分析、竞争态势作出前瞻性决策。

  • 运营型产品经理:侧重于产品与运营相结合,将运营策略转化为产品功能,关注用户增长、活跃度、留存等运营指标的提升。

  • 数据驱动产品经理:利用数据驱动产品优化和决策,熟悉数据分析工具,能从海量数据中发现规律并指导产品迭代。

  • AI/算法产品经理:在AI、大数据等领域,负责设计和优化基于算法和模型驱动的产品功能。

4. 按照行业分类:

  1. 电子商务产品经理
    • 负责电商平台的产品规划和优化,比如淘宝、京东等综合电商平台的商品管理、订单系统、支付系统、物流跟踪等模块,以及垂直细分市场的电商解决方案。
  2. 社交网络产品经理
    • 主要负责社交产品如微信、微博、QQ等的策划与迭代,关注社交互动、社区运营、信息传播、用户关系链构建等。
  3. 金融产品经理
    • 设计和优化金融服务产品,包括银行应用、证券交易平台、金融科技产品(如P2P借贷、众筹平台、区块链应用)、支付工具等,涉及资金流转、风险管理、合规要求等内容。
  4. 教育科技产品经理
    • 开发在线教育平台、教育管理软件、智慧校园系统等,关注课程设计、学习路径规划、教学资源管理等功能。
  5. 健康医疗产品经理
    • 专注医疗健康管理、电子病历系统、远程诊疗平台、智能穿戴设备配合的健康管理APP等产品的研发,需对接医疗资源、遵循医疗规范和隐私保护法规。
  6. 游戏产品经理
    • 负责游戏产品的策划、更新、运营,关注游戏玩法设计、用户体验、付费模型、社交元素等。
  7. 企业服务(B2B)产品经理
    • 包括CRM、ERP、HRM、SCM等各种企业管理软件,以及云服务、大数据分析工具等,需了解企业运营流程并提供针对性的解决方案。
  8. 物联网(IoT)产品经理
    • 负责智能家居、智能城市、工业自动化等领域的软硬件一体化产品设计,涉及传感器、智能设备、数据分析平台等。
  9. 媒体娱乐产品经理
    • 从事视频流媒体平台、音乐播放器、新闻资讯App等产品设计,关注内容分发、版权管理、个性化推荐等功能。
  10. 人工智能(AI)产品经理
    • 专门从事AI产品如智能语音助手、图像识别系统、自动驾驶系统等的研发,需理解机器学习原理、数据训练过程并转化为用户友好的产品形态。

5. 按照层级划分:

  • 初级产品经理:通常负责较具体模块或任务,配合上级完成产品设计与迭代。
  • 中级产品经理:可以独立承担某个产品线或子产品的规划与执行。
  • 高级产品经理乃至产品总监:负责整个产品部门或多个产品线的战略规划与执行,具备较强的战略眼光和团队管理能力。

6. 按照产品领域分类:

  • 软件产品经理:负责软件产品的规划、设计和管理。
  • 硬件产品经理:专注于硬件产品的开发和推广。
  • 互联网产品经理:管理互联网产品,如网站、移动应用等。
  • 电商产品经理:负责电商平台或相关产品的策划和运营。

以上分类并不绝对孤立,实际工作中产品经理的角色可能会结合多种分类特征,且随着行业的发展和市场需求的变化,还会出现新的产品经理角色类型。


作者:西边一山
来源:mdnice.com/writing/f1f3d57fedef4800932a6afd643809f1

收起阅读 »

需求分析

产品经理在进行需求分析时,首先需要明确需求分析的目的和重要性,即从用户需求出发,挖掘用户的真正目标,并转化为产品需求的过程。接下来,可以通过以下文了解如何来做好需求分析。image-202403070006130051.什么是需求分析需求分析也称为软件需求分析...
继续阅读 »

产品经理在进行需求分析时,首先需要明确需求分析的目的和重要性,即从用户需求出发,挖掘用户的真正目标,并转化为产品需求的过程。接下来,可以通过以下文了解如何来做好需求分析。

image-20240307000613005
image-20240307000613005

1.什么是需求分析

需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体要求,将用户非形式的需求表述转化为完整的需求定义,从而确定系统必须做什么的过程。

需求分析是软件计划阶段的重要活动,也是软件生存周期中的一个重要环节,该阶段是分析系统在功能上需要“实现什么”,而不是考虑如何去“实现”。需求分析的目标是把用户对待开发软件提出的“要求”或“需要”进行分析与整理,确认后形成描述完整、清晰与规范的文档,确定软件需要实现哪些功能,完成哪些工作。此外,软件的一些非功能性需求(如软件性能、可靠性、响应时间、可扩展性等),软件设计的约束条件,运行时与其他软件的关系等也是软件需求分析的目标。

2.需求分析的重要性

需求分析指的是在建立一个新的或改变一个现存的产品时,确定新产品的目的、范围、定义和功能时所要做的所有工作。这个过程通常涉及多个部门和团队成员,包括产品经理、设计师、开发者、销售团队和潜在用户。产品需求分析的目的是确保产品满足市场的需求,为用户提供价值,并与公司的战略目标和愿景保持一致。

img
img

需求分析的重要性在于:

  • 确保产品方向正确:帮助团队确定正确的产品方向,避免开发与市场和用户需求不符的产品。
  • 提高资源利用效率:需求分析能够明确需求,而明确的需求可以帮助团队更加高效地分配资源,避免浪费时间和资金在不必要或优先级较低的功能上。
  • 降低项目风险:需求分析需要我们去深入了解用户需求和市场趋势,所以它可以帮助团队识别潜在的风险,并提前采取措施来应对。

除此以外,需求分析还能够起到提高产品质量加强团队沟通提高用户满意度等等。

3.需求分析的时机

  • 产品规划阶段:在确定产品方向和目标时,进行需求分析以了解市场和用户需求。
  • 产品设计阶段:在设计产品功能和界面时,根据需求分析结果进行具体的设计。
  • 产品迭代阶段:在产品上线后,根据用户反馈和市场变化,持续进行需求分析以优化产品。

需求分析贯穿整个产品生命周期,但尤其重要的是在项目启动阶段和迭代更新时进行。在项目开始前进行全面的需求分析;在产品上线后根据用户反馈和市场变化不断调整优化需求。

4.需求分析的方法

4.1需求分析的最佳实践和方法论

主要包括以下几个方面:

  1. 业务分析而非系统实现:需求分析的任务不仅仅是分析系统如何实现用户的需要,而是更广泛的业务分析,这包括了对业务知识、问题列表等方面的定义。

  2. SERU框架:《软件需求最佳实践》一书中提倡的SERU框架是一套重要的需求分析方法论,它将目标系统分解为主题域,再分解为流程,最后得到用例以及业务实体。

  3. 需求定义和捕获:需求定义是需求分析的起点,涉及到从用户需求中提炼出产品需求的过程。需求捕获则是在此基础上进一步细化需求,确保需求的准确性和完整性。

  4. 需求分析与建模:在需求分析的第一阶段完成结构框架和行为脉络的梳理后,第二阶段的工作任务是填充需求的细节,即根据前面的框架进行需求细节的填充。

  5. 功能分解、结构化分析、信息建模、面向对象分析:需求分析的主要方法包括功能分解方法、结构化分析方法、信息建模方法以及面向对象的分析方法。这些方法有助于从不同角度深入理解和描述需求。

  6. 深入理解需求并调整认知:需求分析的本质是根据认知进行假设,然后给出判断。核心是不断深入理解需求,调整需求认知,让自己的假设尽可能贴近客观事实,以得出更加准确的判断。

需求分析的最佳实践和方法论是一个综合性的过程,涉及到业务分析、需求定义、捕获、分析与建模等多个环节。通过采用SERU框架等方法论,结合功能分解、结构化分析、信息建模、面向对象分析等方法,可以有效地进行需求分析和建模,从而确保软件开发项目能够满足用户的真实需求。

这里,以下对第5点进行简单阐述包括以下几点:1、功能分解方法;2、结构化分析方法;3、信息建模方法;4、面向对象的分析方法。功能分解方法是将新系统作为多功能模块进行组合。各功能亦可分解为若干子功能及接口,子功能再继续分解。

4.3 功能分解方法在需求分析中的具体步骤和技巧有哪些?

  1. 分解步骤:功能分解首先需要将复杂系统分解为更小、更简单的功能单元。这些步骤可以是具体的功能点,也可以是更抽象的操作流程或用户界面元素。例如,通过用户故事切分流程图来准备待切分的需求,或者通过功能分解法将业务功能和辅助功能分开。

  2. 分析技巧:在进行功能分解时,需要分析每个用例之间的约束关系、执行条件,并组织出各种业务流程图。这有助于清晰地理解每个功能单元之间的关系和相互作用。

  3. 评估与优化:在完成功能分解后,需要对分解后的功能单元进行评估,以确定哪些是核心需求,哪些是次要需求。根据评估结果,可能需要对需求进行进一步的调整和优化。

  4. 技术应用:功能分解方法不仅限于软件开发领域,它还可以应用于其他复杂系统的设计、分析和实现过程中。通过对系统功能的分解,可以简化设计和实现的复杂性,提高效率。

  5. 实践案例:在业务场景分析中,功能分解方法结合从场景到挑战再到方案的思考模型,可以有效完成分析过程,输出初步的解决方案。这种方法强调了从具体场景出发,逐步深入到问题挑战和解决方案的整个过程。

功能分解方法在需求分析中的应用涉及到分解步骤的制定、分析技巧的运用、评估与优化的过程,以及技术应用和实践案例的分享。这些步骤和技巧共同作用于需求分析的各个阶段,帮助团队更高效、准确地理解和满足用户的真实需求。

4.4结构化分析方法在识别关键需求时的有效性如何评估?

  1. 需求分析的重要性:首先,需要认识到对软件需求的深入理解是开发成功的前提和关键。这意味着在进行结构化分析时,必须确保需求分析的准确性和全面性。

  2. 结构化分析过程:结构化分析方法包括与用户沟通获取用户需求的方法、分析建模与规格说明、实体—关系图、数据流图、状态转换图等内容。这些过程有助于确保对需求的全面理解和准确描述。

  3. 图形工具的应用:结构化分析中常用的图形工具包括层次方框图、Warnier图和IPO图等。这些工具有助于清晰地展示需求之间的关系和逻辑结构。

  4. 需求结构化的目标:结构化的目标是在业务需求向代码开发转换时,建立一个数字化标准,统一表达方式。这样做可以减少信息损耗,提高开发效率。

  5. 系统分析师的角色:结构化分析方法假定系统分析师理解问题域的全部,并且有能力正确地识别和分解问题。这种方法通过一次性将系统的功能分解到位,有助于提高分析的深度和广度。

  6. 系统体系结构的有效性评估:虽然结构化分析方法主要用于软件需求分析,但其有效性也可以扩展到评估系统体系结构。这需要考虑业务需求、适应性、可靠性、安全性等多个方面。

  7. 结构化数据分析:对于数据分析而言,确立明确的目标和提出假设是进行有效分析的重要第一步。这同样适用于结构化分析,有助于明确分析的方向和目标。

  8. 功能模型、数据模型和行为模型的使用:结构化分析中通常采用软件的功能模型、数据模型和行为模型来建模用户需求。这种方法有助于更准确地捕捉用户需求。

  9. 从可行性研究阶段得到的数据流图出发:结构化分析方法从可行性研究阶段得到的数据流图为起点,有助于确保需求分析的合理性和有效性。

结构化分析方法在识别关键需求时的有效性可以通过需求分析的重要性、过程的完整性、图形工具的应用、需求结构化的目的、系统分析师的角色、系统体系结构的有效性评估、数据分析方法的应用以及功能模型和行为模型的使用等多个方面进行评估。

4.5 信息建模方法在需求分析中的应用

它从数据角度对现实世界建立模型。大型软件较复杂;很难直接对其分析和设计,常借助模型。模型是开发中常用工具,系统包括数据处理、事务管理和决策支持。实质上,也可看成由一系列有序模型构成,其有序模型通常为功能模型、信息模型、数据模型、控制模型和决策模型。有序是指这些模型是分别在系统的不同开发阶段及开发层次一同建立的。建立系统常用的基本工具是E—R图。经过改进后称为信息建模法,后来又发展为语义数据建模方法,并引入了许多面向对象的特点。 信息建模可定义为实体或对象、属性、关系、父类型/子类型和关联对象。此方法的核心概念是实体和关系,基本工具是E-R图,其基本要素由实体、属性和联系构成。该方法的基本策略是从现实中找出实体,然后再用属性进行描述。

4.6 面向对象的分析方法在需求分析中的使用

面向对象的分析方法的关键是识别问题域内的对象,分析它们之间的关系,并建立三类模型,即对象模型、动态模型和功能模型。面向对象主要考虑类或对象、结构与连接、继承和封装、消息通信,只表示面向对象的分析中几项最重要特征。类的对象是对问题域中事物的完整映射,包括事物的数据特征(即属性)和行为特征(即服务)。

5.需求管理

5.1如何有效地进行需求管理以避免项目延误和资源浪费?

  1. 建立综合性需求框架:这是精细化管理的基础,有助于统一和明确需求的范围、质量要求等关键信息。

  2. 采纳迭代式需求优化:需求不是一成不变的,通过迭代优化可以更快地响应市场变化和用户反馈,同时也能减少因需求不明确而导致的项目延误。

  3. 运用数据分析提高预测准确性:利用数据分析工具来预测项目完成所需的时间和资源,可以帮助管理者合理规划项目进度,减少不必要的等待和返工。

  4. 构建跨部门沟通机制:良好的沟通是需求管理成功的关键。跨部门之间的有效沟通可以确保信息的准确传递,避免误解和重复工作,从而提高工作效率。

  5. 维护产品需求变更的历史记录:对于不断产生的新需求,需要有一个清晰的需求变更历史记录,以便追踪每一次需求变更的原因、影响以及解决方案,确保需求管理的准确性和一致性。

  6. 使用资源管理工具:通过资源管理工具,确保资源的分配与项目的优先级和实际需求相匹配,避免资源的过度分配和浪费,同时也能及时发现资源分配中的问题并进行调整。

  7. 实施版本控制与管理策略:有效实施版本控制可以帮助团队更好地管理需求变更,确保需求文档的准确性和一致性,降低沟通成本和避免冲突。

  8. 区分需求类别并制定优先级规则:在制定优先级规则之前,需要先区分需求类别,并根据“产品管理权”和“需求确定性”来划分需求类型,以确保资源的有效分配。

通过上述步骤,可以有效地进行需求管理,避免项目延误和资源浪费,从而提高项目管理的效率和成功率。

5.2敏捷开发方法在需求分析中的应用及其效果如何?

敏捷开发方法在需求分析中的应用主要体现在其迭代、增量式的方法论上,强调团队成员的自我管理、面对变化时的快速适应能力,以及持续的沟通和协作。敏捷需求分析在需求时机与过程、文档要求、变更、参与者角色等方面与传统方法有所不同,能够更好地参与到项目的生命周期演进中。通过合理的需求收集、需求分析与细化、需求优先级排序和需求跟踪等方法,可以更好地管理和满足项目的需求,提高团队的开发效率和产品质量。

敏捷开发的效果表现在多个方面。首先,敏捷指标的引入为软件开发生命周期中的不同阶段提供了对生产力的洞察,有助于评估产品质量并跟踪、优化团队绩效。其次,敏捷实践可以量化分析多种效能指标,如工作效率、可预测性、质量和响应程度等,这些指标有助于团队和项目管理者了解敏捷实践的实际效果。此外,敏捷开发中的过程度量指标,如业务指标和敏捷指标的跟踪,对于衡量开发过程的各个方面非常重要,这不仅侧重于解决方案是否满足市场需求,也包括衡量开发过程的各个方面。

敏捷开发方法在需求分析中的应用通过其独特的迭代和增量式方法论,结合合理的需求收集和管理方法,有效提升了团队的开发效率和产品质量,同时通过敏捷指标的应用,实现了对项目效能的全面评估和优化。

6.需求分析的关键点

6.1需求分析的关键点主要包括以下几个方面:

  1. 明确用户需求与产品需求的区别:这是需求分析的基础,需要区分用户需求和产品需求,确保产品或服务真正满足目标用户的需求。

  2. 将用户需求转化为产品需求的方法:通过深入细致的调研和分析,将用户非形式的需求表述转化为完整的需求定义,确定系统必须做什么。

  3. 深入挖掘用户动机:在需求分析中,了解和分析用户的动机是非常重要的,这有助于更好地理解和满足用户的需求。

  4. 筛选和优化需求:产品经理需要筛选和优化接收的需求,确保需求的质量和优先级,以提高产品质量和用户满意度。

  5. 收集需求、整理需求、分析需求、确认需求和编写需求文档:这是需求分析的一般流程,包括收集、分类、筛选需求、分析需求等步骤[[3]]。

  6. 使用合适的需求分析方法和工具:根据不同的项目需求和使用场景,选择合适的需求分析方法和工具,如HWM分析法、功能分解方法等。

  7. 考虑需求的业务诉求、目标用户的用户诉求、分析总结需求目标:合理地归类接收的需求,明确需求的业务诉求,明确目标用户的用户诉求,分析总结需求目标,并给设计提供依据。

  8. 需求评估分析方法:包括模糊聚类分析、质量功能展开、KANO模型分析、A/B测试等,这些方法可以帮助评估需求的质量和可行性[[22]]。

综上所述,需求分析的关键点是多方面的,涉及到从明确用户需求到优化需求的整个过程,以及在此过程中采用合适的方法和技术来确保需求的准确性和有效性。

6.2需求分析中的HWM分析法具体是什么,如何应用?

HWM分析法,即"How Might We"(我们可以怎样),是一种用于需求分析和问题解决的方法。它的全称为"How Might We",即"我们可以怎样"。在这个过程中,我们假设问题是可以解决的,只是我们尚不知道如何解决。这种方法强调的是通过头脑风暴的方式,最大范围地搜集产品的各种可能性,然后抽象地整理出这些想法背后所隐藏的核心概念和产品需求,快速梳理出正确的产品设计方向。

应用HWM分析法时,可以分为五个步骤:首先是明确用户场景问题,其次是HMW分解问题,然后是扩展思路,接着是使用不同的分解思路如积极、转移、否定、拆解、脑洞等来拆解许多不同的解决方案,最后是抽象地总结这些想法并确定解决方案。这一过程中,工具的使用也很重要,例如思维导图可以帮助清晰地展示问题和可能的解决方案。

此外,HWM分析法不仅仅局限于产品需求分析,它还涉及到对人类行为和社会系统的分析,以识别存在的问题和潜在的风险,并制定出相应的应对策略和措施。这表明HWM分析法具有较强的综合性和灵活性,能够适应不同领域的需求分析。

HWM分析法是一种通过广泛探索和思考各种可能性来解决复杂问题的方法论,它要求团队成员共同参与,通过明确问题、分解问题、扩展思路、采用多种分解思路以及最终的抽象总结,来寻找最合适的解决方案。

7.需求收集技巧

7.1 需求收集技巧有哪些,特别是在多渠道反馈收集方面的策略?

需求收集技巧在多渠道反馈收集方面的策略主要包括以下几点:

  1. 多渠道反馈收集:企业可以提供多种反馈渠道来确保用户能通过多种方式进行反馈。这包括但不限于在线调查问卷、社交媒体平台、用户论坛等,以确保能够覆盖到不同的用户群体和使用场景。

  2. 观察法:通过观察目标用户的日常行为来理解他们的真正需求。这种方法可以是主动的,即观察用户的行为和工作流程,也可以是被动的,如收集用户对设计原型的反馈。这种方法有助于深入了解用户的实际操作习惯和需求,从而提供更准确的产品改进建议。

  3. 访谈和问卷调查:通过与利益相关者进行沟通和访谈,了解他们对于产品或服务的需求和预期。同时,设计有效的问卷调查也是需求收集的重要手段,可以帮助收集更广泛的需求信息。

  4. 文档分析:利用文档(如用户手册、操作指南等)进行分析,可以发现那些不易直接表达但对产品改进至关重要的需求。这种方法适用于那些需要详细说明或解释的产品功能。

  5. 用户反馈渠道:除了上述提到的多种渠道外,还应考虑建立专门的用户反馈机制,如定期发布用户调查问卷、开放日活动等,以直接从用户那里获取反馈。这些反馈可以帮助团队及时了解当前产品或服务的不足之处,以及潜在的用户痛点。

多渠道反馈收集策略要求企业综合运用多种技术和方法,既要注重面对面的沟通和观察,也要充分利用网络和数字工具,同时不忘通过文档分析等方式深入挖掘用户需求。这样的策略能够帮助企业构建更加全面和准确的需求收集系统,为产品和服务改进提供坚实的基础。

7.2 如何有效地进行用户访谈以收集需求?

有效地进行用户访谈以收集需求,首先需要确保访谈环境轻松愉快,避免给受访人带来社会压力。用户访谈是一种有计划、有目的、有意识的过程,通常有明确的时间安排和谈话主题。在准备阶段,应具备正确的预备知识,具备细致的洞察力、耐心和责任感。

在访谈过程中,应积极倾听受访者的意见,不要害怕沉默,也不要强制用户回答。可以在用户回答后以自己理解的方式重复答案,以避免对用户的回答产生误解。此外,正确并恰当地提出问题是解决困惑的第一步,用户访谈首先是一门艺术——说话和倾听艺术,也是提问的艺术。

有效的访谈需要满足提对问题、正确沟通、提炼转化三个条件。这意味着在访谈中,不仅要提出正确的问题,还要通过访谈技巧有效获取用户信息,并将调研信息转化为洞察分析。

观察法也是理解用户真正需求的有效方式之一。通过观察用户的日常行为,可以主动或被动进行访谈,以理解他们当前的工作流程。这种方法有助于深入了解用户的需求和偏好。

总之,有效地进行用户访谈收集需求的过程包括创造友好的访谈环境、准备充分的预备知识、积极倾听和正确提问。同时,结合观察法等其他方法,可以更全面地理解和满足用户需求。

8.需求分析的误区

避免需求分析常见误区的具体步骤和方法:

  1. 目标驱动,结构分解:首先,明确需求分析的目标是非常重要的。这包括了解项目旨在达成的目标,如吸引新用户、保留老用户、提高用户活跃度或产生营收等。接着,根据这些目标,结构分解出为了达成这些目标而需要的需求。

  2. 避免把用户描述当作需求:在需求分析中,不应该仅仅基于用户的描述来确定需求。这种做法很容易导致需求偏离实际业务目标,从而无法满足核心业务的要求。

  3. 避免把数据表象当需求:需求分析时,不应只关注数据表现,而忽视是否有偏离业务的情况。过分依赖数据可能会误导需求分析,导致开发出来的产品与预期目标不符。

  4. 避免把竞品功能当需求:在需求分析过程中,不应该简单地将竞品的功能照搬过来。每个产品的市场定位和目标用户群体都有所不同,直接复制竞品的需求可能会导致产品失败。

  5. 关注用户及业务目标并重:在进行需求分析时,不仅要关注用户需求,还要关注业务目标。确保需求分析能够支持业务目标的实现,而不是单纯地迎合用户需求。

  6. 使用严谨而科学的分析方法:需求分析不应该是一种随意的思考过程,而是需要遵循一定的科学方法和步骤。例如,可以使用马洛斯需求模型来分析需求,然后分析用户和场景,最后分析用户期望。

  7. 识别伪需求:通过学习和实践,学会辨别哪些是需求分析中的常见伪需求。这包括识别那些看似合理但实际上并不符合业务逻辑或目标的需求。

需求分析的误区包括没有进行有效的需求管理、对需求理解不够深入、忽视了需求的可追踪性和变更控制机制等。为了避免误区,产品经理应该采用敏捷开发方法,建立严格的需求变更管理流程,对任何需求变更进行详细讨论、评估、记录。此外,正确分辨用户提出的是需求还是解决需求的方案也是避免的一个误区。

综上所述,产品经理在进行需求分析时,需要从宏观到微观全面考虑,利用多种工具和方法进行深入分析,同时注意需求管理的各个方面,以确保产品能够满足市场和用户的真实需求,与公司的战略目标和愿景保持一致。


作者:西边一山
来源:mdnice.com/writing/b3ea0873bba04ab98767930c4d9a268b
收起阅读 »

JavaScript作用域详解

web
作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。 词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。 JavaScript 的作用域(S...
继续阅读 »

作用域可分为词法作用域和动态作用域,JavaScript 使用词法作用域,也称为静态作用域。


词法作用域是指变量的作用域在代码写好的时候就确定了,而不是在运行时确定。函数在定义的时候就决定了其作用域,而不是在调用的时候。


JavaScript 的作用域(Scope)是指在代码中定义变量时,这些变量在哪里以及在哪些地方可以被访问。作用域控制着变量的可见性和生命周期。在 JavaScript 中,有全局作用域和局部作用域的概念,作用域的规则由函数定义和代码块定义来决定。


1. 全局作用域(Global Scope)


全局作用域是指在整个 JavaScript 程序中都可访问的范围。在全局作用域中定义的变量和函数可以被任何地方访问,包括代码文件、函数内部、循环块等。例如:


var globalVariable = "I am global";

function globalFunction({
  console.log(globalVariable);
}

globalFunction(); // 输出: I am global

2. 局部作用域(Local Scope)


局部作用域是指在函数内部或代码块内部定义的变量,其可见性仅限于该函数或代码块内部。这种作用域遵循 "变量提升" 的规则,即变量在声明之前就可以被访问,但其值为 undefined。例如:


function localScopeExample({
  var localVariable = "I am local";
  console.log(localVariable);
}

localScopeExample(); // 输出: I am local
console.log(localVariable); // 错误,localVariable 不在此处可见

3. 块级作用域(Block Scope)


在 ES6 引入块级作用域概念,可以通过 letconst 关键字在代码块内定义变量,这使得变量在块级范围内有效。在此之前,JavaScript 只有函数作用域,使用 var 关键字定义的变量在整个函数范围内有效。


if (true) {
  let blockVariable = "I am in a block";
  console.log(blockVariable);
}

console.log(blockVariable); // 错误,blockVariable 不在此处可见

总结


作用域是 JavaScript 中重要的概念,理解作用域有助于正确使用变量、避免命名冲突,提高代码的可维护性。


作者:MasterBao
来源:mdnice.com/writing/c771e23f7b014afbbe42499a1b32b0f7
收起阅读 »

2023总结:30岁,结束8年北漂回老家,降薪2/3,我把人生过的稀烂

一转眼又快过年了,回想整个23年,简直是我人生中最黑暗的一年(之一)。 23年,我30岁,在北京干了8年程序员。30岁这年我做了一个决定:结束8年北漂生涯,回老家(一个三线城市)自己创业,去做自媒体。 一、为何做出这个决定 这个决定也不是一时拍脑袋做出的决定...
继续阅读 »

一转眼又快过年了,回想整个23年,简直是我人生中最黑暗的一年(之一)。



23年,我30岁,在北京干了8年程序员。30岁这年我做了一个决定:结束8年北漂生涯,回老家(一个三线城市)自己创业,去做自媒体。


一、为何做出这个决定


这个决定也不是一时拍脑袋做出的决定,导火索是在22年:


那时候大环境不好,大家都越来越卷,下班的时间也越来越晚。


放假回家亲戚朋友总说,你在北京996这么累,图啥啊,工资是高点,但是完全没有生活啊。而且你在北京漂到啥时候是个头?你又买不起房,又没户口,早晚得回来吧。


我仔细想想也有道理,活了这么多年了都在当牛做马,被pua,还得面临35岁危机,真的受够这种生活了!所以那时候心里埋下了一颗种子:我要去浪浪山的那边看看!


其实我本身就是一个喜欢自由的人,这么多年那句“打工是不可能打工的,这辈子都不会打工”一直激励着我,我想自己有一天也能实现不打工这个目标。


于是22年底我做了一个决定:23年去山的那边看看大海的样子!拿完年终奖就辞职!去创业,去开启我的新的人生!


在准备辞职前的几件事情,都让我更加坚定了辞职的决心:



  1. 那时候还没有放开,在家线上办公,本来在公司办公是995,晚上9-10点下班了基本就没啥事情了,但是在家就不一样了,每天各种电话、视频会议,甚至十一二点都要开会,恨不得让你24h都在线,生活和工作基本都没有边界。那个时候只要听到会议呼叫的声音,内心就一紧,心中默默祈祷不要出什么幺蛾子,都快成心理阴影了。

  2. 当时得了新冠也只敢请了一天假,第二天晕晕乎乎的就继续开始工作了。因为我知道,落下的工作最后还得你自己加班完成,否则领导最后还会赖你延期。

  3. 周末也需要随时在线,需要及时回复群里的消息,需要随时解决线上的问题,否则就会打上工作态度不好的标签,绩效肯定低。导致我周末基本不敢出去,出去也得随时看着手机,看有没有@你的消息,整天提心吊胆,玩也玩不好,还不如在家躺着。


我觉得这不是我想要的生活,每天太累了,身体累不算,心还累,生怕自己负责的业务出了什么问题,如坐针毡,如芒刺背,如履薄冰。


二、我辞职了


终于,熬到23年,拿到年终奖后,我决定提出离职。


当时身边有些人劝我不要辞职,说现在环境不好,更不应该放弃你的老本行,去做啥自媒体。


我当时内心嗤之以鼻,心想程序员这行也就干到35岁,而且现在卷的不行,加班加的身体都快废了,这行岁数大了没前途!我趁现在30岁还年轻,创业正值当年,辞职改行的选择非常有战略眼光!(当时真的是感觉杰克马附体,准备在这个三十而立的年纪,大干一场!)


2b5f9de5dbc7cd40403819a50d693574.jpeg


当然我也不是脑袋一热就想着辞职去做自媒体,辞职前我做了充足的准备,和很长时间的调研&分析:



  • 我作为一个互联网人,做实体店肯定不是我擅长的,肯定只能从互联网上选择行业,互联网项目有很多:个人工具站,知乎好物,闲鱼二手书,小红书带货,抖音带货,抖音个人ip,公众号写作,短剧cps,小说推文,知识付费等等的项目,我可以说都看了一个遍,其中抖音现在流量最大,要做风口上的猪,做抖音相关肯定要容易很多。

  • 然后我也学习了一些创业相关的知识,比如如何找对标,如何分析对方商业模式,参加了很多知识付费的圈子,然后还报了小红书和抖音的培训班,总共加起来得有1w多呢。

  • 而且我还预留了充足的资金,我做了最坏的打算,就算我一分钱不挣,也够我活3年呢,我不会3年都创业不成功吧!(此处白岩松表情包:不会吧!.jpg)


u=1021210702,2199782272&fm=253&fmt=auto&app=120&f=JPEG.webp


三、想象很美好


为了这次创业,我还制定了计划,年度计划,季度计划,月计划,周计划,天计划,真的非常详细。


我也要很自律,每天按时起床,锻炼,学习,做业务。这次我真的抱着必胜的决心来做!


当然我也提前列出可能要遇到的风险,并思考解决方案:


比如项目进展慢怎么办,拖延症怎么办,家人反对怎么办,朋友约吃饭打乱我的计划怎么办,遇到困难我该怎么应对等等


这么一套组合拳下来,我觉得已经万事俱备,只差开干了!


四、现实很残酷


4月我如期辞职,当时正值清明节,淅淅沥沥的小雨并没有浇灭我开启新生活的热情。辞职后,我就按计划开始早睡早起,锻炼,学习,搞创业的事情。


但是马上就被打脸了,这是我创业中遇到的第一个难题,也是我万万没有预料到的


就在我创业后的不久,我患上焦虑症,失眠了,而且还很严重,就是那种从晚上11点躺下,躺到早上6点才睡着的那种失眠,而且还时不时的心悸。


我万万没想到会患上失眠症。因为我觉得没有上班的压力了,想啥时候干活就啥时候干活,想干多少干多少,想啥时候下班就啥时候下班,也没人pua我了,还有时间锻炼,应该睡得更好才是。


但实际并不是这样,对于一个从小被学校管,长大了被公司管的芸芸众生来说,创业实际是这样的:



  1. 你会非常忙,比上班还要忙,因为你之前是螺丝钉,做好自己的本职工作就好了,现在事无巨细,都你一个人。比如做自媒体,从开始的账号定位-》内容选题-》写脚本-》置景&拍摄-》后期剪辑-》选品-》商务对接-》客服-》用户社群运营,所有的环节,都得你自己一个人。然后视频没流量怎么办,违规了怎么办,付费转化率低怎么办,还是只有你自己去解决。(之前公司让你干啥你干啥,你只需要规定时间完成你的任务就好了)

  2. 面对大量的自由时间,你根本不会支配时间,因为很多环节你都是小白,要学习的东西太多,但是你天天光学习了,每天看似很忙,但是看不到产出,导致你就很沮丧。(之前你只做熟悉的工作,产出是有保证的)

  3. 行动困扰,没有目标感,没有人给你一个目标&方向,你不知道你现在做的事情对挣钱有没有价值,你会迷茫,你会时常自我怀疑。(之前你只是专注领导安排的任务,至于这个任务能不能帮公司挣到钱,那是公司的事情,你关心到手的工资而已)

  4. 没有成就感,认同感。因为现在你很多事情要从0开始,比如写文案要求写作能力,拍视频要求表现力,搞流量要求你有运营&营销的能力 ,相比之前做熟悉工作,感觉上会有落差(之前工作中都是做你擅长的领域,每完成一项任务也很有成就感,做的出色还能收获同事和领导的认可)

  5. 和社会断了链接,没有存在感,归属感(这是人类的基本需求之一),你不属于任何一个群体,没有人赞扬,尊重,接纳你,甚至你想被骂两句也没人鸟你(之前在公司,做的好了领导或者同事会夸你两句,做的不好了可能会给你建议,起码有人能倾诉,能交流,能寻求帮助)

  6. 没有了收入,眼见钱包一天天变少,你肯定会焦虑。但是更让你焦虑的,是你不知道未来什么时候能挣到钱。更更让你焦虑的,是不知道最后能不能挣到钱。(之前工作压力不管有多大,多累,起码你还有工资,你还有吃饭的钱,这是底气)


所以在此奉劝有裸辞创业想法的人,千万不要裸辞!裸辞创业九死一生! 正确的做法是一边工作愿一边做副业,等副业的收入和工资差不多甚至超过工资了,再去辞职。有人会说,我工作那么忙,根本没时间搞副业啊。我之前也是这么想的,但是现在我会告诉你:没有时间就去挤出时间,每天晚睡或者早起一会,周末也抽出时间搞。这点问题都解决不了?创业的遇到问题会比这难十倍!如果这个你觉得太难了,那我劝你还是老老实实打工吧。


但是我已经裸辞了,没办法,只能去解决问题,我开始吃助眠药,喝中药,有些好转,但也没治好,只是比之前好点。


就这么拖着残血的半条命,我坚持了半年多,一半时间学习,一半时间实践,搞了两个自媒体号,第一个号违规被封了,第二个号流量也没啥起色。这条路是越走越看不到希望,每天晚上都不想睡觉,因为害怕明天的到来,因为明早一起床,眼前又是一片黑暗。


五、彻底崩溃


11月,因为种种原因和媳妇生了一场气,我觉得对于我创业,她不鼓励也就算了,在我状态这么差的情况下还不能对我包容一点,甚至有点拆后台的感觉,那几天我就像一个泄了气的皮球,内心被彻底击垮了。(所以现在有点理解每个成功男人的背后,都有一个伟大的女人这句话的含义了)


终于,在创业的压力,8个月没有收入的恐慌,焦虑失眠心悸的折磨中,我决定放弃了。


失败了,彻彻底底的失败。回想这次经历,就好像之前在一艘航行的货轮上打工,然后受不船上的种种压榨,终于鼓起勇气,自己带着一艘救生艇,跳海奔向自己想要的自由。结果高估了自己的目前的实力,经不起茫茫大海狂风骤雨,翻船了。。濒临溺亡。。。


六、重新找工作


放弃后的那几周,我开始熬夜,开始暴饮暴食,之前的运动也放弃了。整天在家里拉着窗帘,除了吃饭就是躺在床上刷手机,让我尽可能分散注意力,减少内心的痛苦。


但是这样的状态也不是事儿啊,目前肯定是不想再去面对创业的事情了,那只能去找个工作先干着了。


刚开始找工作内心又有不甘,因为一个三线城市比起北京来说,不管是工作机会,环境,薪资来说,都差太多。


但是没办法,我现在的感觉就是快溺死了,我现在急需一个救命稻草,活下来,是我目前的首要任务。


于是在网上海投了一遍,结果惨不忍睹,根本没几家公司招人,前前后后一个月,真正靠谱的面试就一家,是的,只有一家。


好在这家也顺利拿了offer,是一家刚创业的公司,一共十几个人,薪资只有原来1/3多点,但是拿到offer那一刻我依然有些激动,我感觉我活下来了,不管怎样,现在能喘口气了。


七、迷茫的未来


现在上班已经一个多月了,公司挺好,不加班,基本上7点前就都走了,离家也挺近,骑个共享单车也就10分钟。这一个月,焦虑没了,不心悸了,失眠也好了。每天就是按部就班上下班,完成老板给的任务,其他的事情也不用自己操心,终于又做起自己熟悉且擅长的事情。


但是内心还是有落差,本来北京好好的工作自己作死给辞了,要不这一年也能攒不少钱呢,现在不但钱没了,这几个月还花了好几w,最后还差点嘎了。


其实入职这家公司前,北京之前的同事问我要不要回去,说现在正忙,我说你先问问吧。


我当时也纠结,如果真的能回去,我还要不要回去,毕竟在那边挣一个月顶这边仨月。但是回都回来了,再去北京可能就一辈子留北京了吧。


不过后来同事说年前没有招人计划了,可能要年后了,如果招人到时再联系我。正好我不用纠结了,这可能就是命运的安排吧。


不过真的想问问你们,如果到时有机会,是继续北漂呢,还是选择在老家呢?


八、结语


说实话,我现在知道了,山的那边还是山,我不知道什么时候才能看到海,甚至我可能一辈子都看不到海了。不过目前想的就是,调整好状态,先走一步算一步吧。


30岁的年纪,学会和自己和解,学会接受自己的平庸,但是依然要努力,毕竟在这个阴雨连天的环境下,没有伞的孩子只能努力奔跑。


作者:骆驼箱子
来源:juejin.cn/post/7330439494666453018
收起阅读 »

7年Android仔的逆袭人生

1.引言 最近在代码人生模块,看到了很多优秀的同行分享自己的人生经历。有感情的,有创业的,有独立开发者的。看完后感慨良多。为此我也想讲讲我的成长经历。相信能给各位一定的启发。 2.毕业 我是一个来自湖北农村的少年。从小到大学费都是父母卖粮食,卖棉花,找亲朋好友...
继续阅读 »

1.引言


最近在代码人生模块,看到了很多优秀的同行分享自己的人生经历。有感情的,有创业的,有独立开发者的。看完后感慨良多。为此我也想讲讲我的成长经历。相信能给各位一定的启发。


2.毕业


我是一个来自湖北农村的少年。从小到大学费都是父母卖粮食,卖棉花,找亲朋好友,东拼西凑的。要不是勉强考上二本大学。可能现在的我就在工厂搬砖。我记得大四快出去实习的时候,家里没有钱给我路费,硬是卖掉家里十几袋麦子,给我凑的。同期的同学别人家里都是给5000.6000的。而我只有2500。就这样我带着仅有的2500,拉着一个破旧的行李箱踏上南下之旅。怀揣着兴奋,对未来美好的期待来到深圳。运气比较好,很顺利的找到一家公司。那家公司给我开了10000/月的工资。你们知道吗。我是多么多么的兴奋啊。1万啊!,我从来没有掌管过这么多钱。甚至我家里从来没有过1w。过去的二十多年,我的物质生活一直没有被满足过。吃的,用的,都是差人一等。第一台华为手机,也是硬生生用了4年。哪一年我22岁。现在想起来,依旧很兴奋。当时把工作的事情,告诉了父母。父母开心的不得了。我爸在家对我妈说:“我就知道儿子,会有出息”。


3.动荡


本来一切都在向好的方向发展,可是老天爷却给你开了一个玩笑。2017年7月份的一天,一通电话彻底打乱了我的生活节奏。就像一块砖头一下子丢进水里,“咚”的一声。砸在我的心里。电话那头,我姐姐哭着叫我回去,叫我赶紧回去。她告诉我:“爸爸去世了,你赶紧回来,赶紧回来”。面对突如其来的恶耗。经历过的人,应该都知道。那一刻大脑实际上是懵的。呼吸紧促,仿佛喘不过气。内心会质疑这个消息的准确性。觉得不可能,不可能。在家里好好的,好好的,为什么会突然间走了。时至今日,即使过去多年,回想到这些。我依旧会泪流满面。哪一年我22岁。本该幸福的家庭,却发生这样的变故。父亲是一个家庭的基石。是子女坚实的靠山。哪一年,我失去了依靠。也失去了一个完整的家庭。
往后的这些年,我时常梦见我父亲。梦见他还活着,梦到他再和我说话。


父亲是因为急性心肌梗塞走的。发生心肌梗塞的时候,实际上身体会有反应的,例如 四肢无力,例如心跳加速。在出事的前几天,他因为身体不舒服,去过诊所问诊,但是因为医疗条件的不足,加上之前整个家族没有这样的例子。所以没有检测出来。这件事让我意识到世上很多事,我们无能为力,唯有学会的接受


同时,我也会不断的追问自己,为什么会出现在我的身上。因果因果,有这个果,必然有这个因。这个因在哪里呢。我想是这几个因:1.农村人的健康意识差 2.身体不舒服 总觉得自己扛扛就过去了 3.本身存在各种疾病,例如高血压。4.农村的医疗条件差


同样世上很多事,也可以用因果来解释。例如溺水身亡,触电,交通事故。一桩桩悲惨的事件的后面,肯定是有很多因。触电之前,可能有过多次,湿手触碰电器的行为,进而养成习惯; 交通事故之前,可能经常抢红绿灯,经常超车。


也正是因为这个意识的养成。帮助了我改变人生,当然这都是后话。在我父亲去世的三个月之后,爷爷因为接受不了打击,也去世了。仿佛是潘多拉魔盒被打开了,让这个家族饱受磨难。


4.工作


处理完家里的事之后。又匆匆返回深圳,继续当螺丝钉。整个人思想压力也变重了。我未来结婚,买车,买房,给母亲养老都得靠自己。每当我望向身后,我看到的是深渊。我不敢松懈,我不敢像一个正常人那样生活,因为我没有依靠,我只有自己,我得努力,拼搏,奋斗。周六周天会抽出一天的时间去图书馆学习。现在想想,自己能坚持下来,也挺佩服曾经的自己。或许是上天的眷顾,也或许是运气好。在面试OPPO的时候。面试官的问题,正好是我前两天写的博客。于是顺理成章的进入了大厂。在OPPO呆了2年多,技术上得到很大的提升,经历过裁员,经历过背锅,经历过职场的勾心斗角。这段工作经历,拔高了我的世界观,让我从懵懂的少年,蜕变成一个合格的职场人。同时这段经历也间接的改变了我的人生轨迹。


5.感情


自从家里发生变故后,我对未来的规划更加清楚。不管是工作,还是感情上的事。都会提前做准备。例如在oppo的时候。注意到当时一个表现优异,绩效A的同事,来年却转岗了。通过他,我知道了部门发展不顺利,未来可能有裁员的风险。于是就开始私下准备面试的工作。21年一整年,我私下面试了接近40家公司。22年又面试了10多家。当时就明显感受到市场的寒冷。意识到22年假如不跳槽。可能就要一直待在OPPO,等待被裁。于是就果断的跳槽到顺丰。拿到了将近40%的涨幅。虽然后面又离开了顺丰。但是现在来看,当初的选择是没有错误的。


在我年仅26岁的时候。我就在考虑结婚的事,因为我笃信 一个好的伴侣抵得上百万资产,抵得上几十万的高薪工作,她直接决定了你下半生的生活质量。同时我也知道自己一无所有,无存款,无家庭,甚至攒下的钱都不够房子的首付。但是我审视了自己一番。发现自己还是有一些优点的,例如 五官不差,身高1.8,工作也还行,年薪30多万。了解了自己的优点和缺点之后。就开始着手改变自己。


例如跑步减肥从180减到150,每天5公里户外跑步。同时也积极参加一些穿衣打扮课程。了解自己的穿衣风格。 有时间就去相亲会相亲。
说到相亲,我可是相亲界老手了。😁😝 前前后后相亲了 20多场。最后找到了现如今的老婆。


在这段经历当中,一个意识深深的插入到了我的脑海中。那就是 任何事都是有俩面性的,看待事情,争取看到事情的俩面性。这样自己才能提前做准备。


例如:程序员以高薪著称,但是反面就是,肥胖,脱发,油腻。这在相亲市场上是非常不利的。


工程型人才,往往看不上溜须拍马,耍嘴皮子那套。但是随着自媒体的盛行,发现自媒体做的好的人,恰恰都是耍嘴皮子,能放下身段,脱下长衫的人。这个认识正对应了一句话:”当你凝视深渊的时候,深渊正在凝视着你"


6.后记


写这篇分享的时候,老婆已经怀孕8个月了,再有2个月,我就要当爸了。孩子的名字都已经想好。目前定居于深圳,因为老婆家庭条件好,老丈人给了一套豪宅居住。让我免于房贷的压力。有充足的精力为下一个10年奋斗。说到这里我很感谢老婆一家人。他们没有嫌弃我穷,反而时时刻刻照顾我的自尊心。他们一家是我的人生贵人。我问了老丈人很多问题。他都能给我带来不一样的视角,耐心讲给我听。这是多少钱都买不来的。以后有机会可以在聊聊他的一些观点。有一些深深的刻在我的脑海中了。


作者:薯条1492738192844
来源:juejin.cn/post/7351658802393546802
收起阅读 »

面试官:线程调用2次start会怎样?我支支吾吾没答上来

写在开头 刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补充一个小知识点! 记得是一个周末去面试Java后端开发工程师岗位,面试官针对Java多线程进行了狂轰乱炸般的考问,什么线程创建的方式、线程的状态、各状态间的切换、如果保证线程安全、各种锁...
继续阅读 »

写在开头


刚刚吃晚饭时,突然想到了多年前自己面试时的亲身经历,决定再回来补充一个小知识点!


记得是一个周末去面试Java后端开发工程师岗位,面试官针对Java多线程进行了狂轰乱炸般的考问,什么线程创建的方式、线程的状态、各状态间的切换、如果保证线程安全、各种锁的区别,如何使用等等,因为有好好背八股文,所以七七八八的也答上来了,但最后面试官问了一个现在看来很简单,但当时根本不知道的问题,他先是问了我,看过Thread的源码没,我毫不犹豫的回答看过,紧接着他问:



线程在调用了一次start启动后,再调用一次可以不?如果线程执行完,同样再调用一次start又会怎么样?



这个问题抛给你们,请问该如何作答呢?


线程的启动


我们知道虽然很多八股文面试题中说Java创建线程的方式有3种、4种,或者更多种,但实际上真正可以创建一个线程的只有new Thread().start();


【代码示例1】


public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:NEW
Thread-0:RUNNABLE

创建一个Thread,这时线程处于NEW状态,这时调用start()方法,会让线程进入到RUNNABLE状态。


RUNNABLE的线程调用start


在上面测试代码的基础上,我们再次调用start()方法。


【代码示例2】


public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {});
System.out.println(thread.getName()+":"+thread.getState());
//第一次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
//第二次调用start
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:NEW
Thread-0:RUNNABLE
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

第二次调用时,代码抛出IllegalThreadStateException异常。


这是为什么呢?我们跟进start源码中一探究竟!


【源码解析1】


// 使用synchronized关键字保证这个方法是线程安全的
public synchronized void start() {
// threadStatus != 0 表示这个线程已经被启动过或已经结束了
// 如果试图再次启动这个线程,就会抛出IllegalThreadStateException异常
if (threadStatus != 0)
throw new IllegalThreadStateException();

// 将这个线程添加到当前线程的线程组中
group.add(this);

// 声明一个变量,用于记录线程是否启动成功
boolean started = false;
try {
// 使用native方法启动这个线程
start0();
// 如果没有抛出异常,那么started被设为true,表示线程启动成功
started = true;
} finally {
// 在finally语句块中,无论try语句块中的代码是否抛出异常,都会执行
try {
// 如果线程没有启动成功,就从线程组中移除这个线程
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
// 如果在移除线程的过程中发生了异常,我们选择忽略这个异常
}
}
}

这里有个threadStatus,若它不等于0表示线程已经启动或结束,直接抛IllegalThreadStateException异常,我们在start源码中打上断点,从第一次start中跟入进去,发现此时没有报异常。


new线程.png
此时的threadStatus=0,线程状态为NEW,断点继续向下走时,走到native方法start0()时,threadStatus=5,线程状态为RUNNABLE。此时,我们从第二个start中进入断点。


runnable线程.png
这时threadStatus=5,满足不等于0条件,抛出IllegalThreadStateException异常!


TERMINATED的线程调用start


终止状态下的线程,情况和RUNNABLE类似!


【代码示例3】


public class Test {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {});
thread.start();
Thread.sleep(1000);
System.out.println(thread.getName()+":"+thread.getState());
thread.start();
System.out.println(thread.getName()+":"+thread.getState());
}
}

输出:


Thread-0:TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.javabuild.server.pojo.Test.main(Test.java:17)

这时同样也满足不等于0条件,抛出IllegalThreadStateException异常!


我们其实可以跟入到state的源码中,看一看线程几种状态设定的逻辑。


【源码解析2】


// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}

// sun.misc.VM 源码:
// 如果线程的状态值和4做位与操作结果不为0,线程处于RUNNABLE状态。
// 如果线程的状态值和1024做位与操作结果不为0,线程处于BLOCKED状态。
// 如果线程的状态值和16做位与操作结果不为0,线程处于WAITING状态。
// 如果线程的状态值和32做位与操作结果不为0,线程处于TIMED_WAITING状态。
// 如果线程的状态值和2做位与操作结果不为0,线程处于TERMINATED状态。
// 最后,如果线程的状态值和1做位与操作结果为0,线程处于NEW状态,否则线程处于RUNNABLE状态。
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}

总结


OK,今天就讲这么多啦,其实现在回头看看,这仅是一个简单且微小的细节而已,但对于刚准备步入职场的我来说,却是一个难题,今天写出来,除了和大家分享一下Java线程中的小细节外,更多的是希望正在准备面试的小伙伴们,能够心细,多看源码,多问自己为什么?并去追寻答案,Java开发不可浅尝辄止。




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

【CSS定位属性】用CSS定位属性精确控制你的网页布局!

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝...
继续阅读 »

CSS定位属性是用于控制网页中元素位置的一种方式,它能够让元素在页面上精准地落在我们想要的位置。

在CSS中,定位(Positioning)是控制元素在页面上如何定位和显示的一种机制。它主要包括四种属性:静态定位(static)、相对定位(relative)、绝对定位(absolute)、固定定位(fixed)。

每种定位方式都有其独特的特点和使用场景,下面将分别介绍这几种定位属性。

一、Static(静态定位)

静态定位是元素的默认定位方式,元素按照正常的文档流进行排列。在静态定位状态下,不能配合top、bottom、left、right来改变元素的位置。

  • 可以用于取消元素之前的定位设置。

代码示例:

<!DOCTYPE html>
<html>
<head>
<style>
.static {
background-color: lightblue;
padding: 100px;
}
</style>
</head>
<body>


<div>这是一个静态定位的元素。</div>


</body>
</html>

Description

二、Fixed(固定定位)

固定定位使元素相对于浏览器窗口进行定位,即使页面滚动,元素也会保持在固定的位置。

  • 固定定位的元素会脱离正常的文档流。

示例代码:

<!DOCTYPE html>
<html>
<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>
<style>
*{
margin: 0;
padding: 0;
}
body{
/* 给整个页面设置高度,出滚动条以便观察 */
height: 5000px;
}
div{
width: 100px;
height: 100px;
background-color: blue;
/* 固定定位 */
position: fixed;
right: 100px;
bottom: 100px;
}
</style>
</head>
<body>
<div></div>
</body>
</html>

运行结果:

移动前

Description

移动后

Description

比如我们经常看到的网页右下角显示的“返回到顶部”,就可以用固定定位来实现。

<!DOCTYPE html>
<html>
<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>
<style>
body {
position: relative;
}
.content {
/* 页面内容样式 */
}
#backToTop {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #333;
color: #fff;
border: none;
padding: 10px;
cursor: pointer;
}
</style>
</head>
<body style="height: 5000px;">
<div>

</div>
<button id="backToTop" onclick="scrollToTop()">返回顶部</button>
<script>
function scrollToTop() {
window.scrollTo({top: 0, behavior: 'smooth'});
}
</script>
</body>
</html>

运行结果:

Description

三、Relative(相对定位)

相对定位是将元素对于它在标准流中的位置进行定位,通过设置边移属性top、bottom、left、right,使指定元素相对于其正常位置进行偏移。如果没有定位偏移量,对元素本身没有任何影响。

不使元素脱离文档流,空间会保留,不影响其他布局。

代码示例:


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:relative;/*相对定位*/
left:100px;/*向右偏移100px*/
top:-50px;/*向上偏移50px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;
}
</style>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
</body>
</html>

运行结果:

没使用相对定位之前是这样的:

Description

使用相对定位后:相对于原来的位置向右偏移了100px,向上偏移50px。
Description

虽然它的位置发生了变化,但它在标准文档流中的原位置依然保留。

四、Absolute(绝对定位)

绝对定位使元素相对于最近的非 static 定位祖先元素进行定位。如果没有这样的元素,则相对于初始包含块(initial containing block)。绝对定位的元素会脱离正常的文档流。

  • 如果该元素为内联元素,则会变成块级元素,可直接设置其宽和高的值(让内联具备快特性);

  • 如果该元素为块级元素,使其宽度根据内容决定。(让块具备内联的特性)

<style>
.wrap{
width:500px;
height:400px;
border: 2px solid red;
}
.box1{
width:200px;
height:100px;
background:skyblue;
margin:10px;
}
.box2{
width:200px;
height:100px;
background:pink;
margin:10px;
position:absolute;/*绝对定位*/
left:100px;/*向右偏移100px*/
top:30px;/*向下偏移30px*/
}
.box3{
width:200px;
height:100px;
background:yellowgreen;
margin:10px;


}
</style>
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>

将第二个设置为绝对定位后,它脱离了文档流可以定位到页面的任何地方,在标准文档流中的原有位置会空出来,所以第三个会排到第一个下面。

Description

第二个相对于它的父元素向右偏移100,向下偏移30。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

五、z-index(层级顺序的改变)

层叠顺序决定了元素之间的堆叠顺序。z-index 属性用于设置元素的层叠顺序。具有较高 z-index 值的元素会覆盖具有较低 z-index 值的元素。

注意:

  • 默认值是0
  • 数值越大层越靠上
  • 不带单位
  • 没有最大值和最小值
  • 可以给负数

代码示例:

<!DOCTYPE html>
<html>
<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>
<style>
div:nth-of-type(1){
width: 300px;
height: 300px;
background-color: skyblue;
position: absolute;
}
div:nth-of-type(2){
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
}
div:nth-of-type(3){
width: 100px;
height: 100px;
background-color: yellowgreen;
position: absolute;
z-index: -1;
}
</style>
</head>
<body>

<div></div>
<div></div>
<div></div>


</body>
</html>

运行结果:

Description

可以看到,最后一个div依然存在,但是看不见了,原因就是我们改变了z-index属性值。

Description

以上就是CSS定位属性的介绍了,通过这些定位属性,可以灵活地控制网页中元素的位置和堆叠顺序。

在实际应用中,CSS定位属性的使用需要考虑到整体布局和用户体验。合理运用这些定位技巧,可以让你的网页不仅美观,而且易于使用和维护。记住,好的设计总是细节和功能的完美结合。

收起阅读 »

解锁 JSON.stringify() 5 个鲜为人知的功能

web
作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。 考虑一个对象如果想把她转成字符串打印出来: const obj = { name: 'San Shang Y...
继续阅读 »

u=142040142,590010156&fm=253&fmt=auto&app=138&f=JPEG.webp


作为一名前端开发者,你可能熟悉JSON.stringify()方法,通常用于调试。但是很多只是简单使用一下接下来,让我们深入了解其实用性。


考虑一个对象如果想把她转成字符串打印出来:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(obj.toString()); // Result: [object Object]

如果你想这样打印你所看到的只能是 [object Object]


我们可以借助JSON.stringify()方法


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj));
// Result: {"name":"San Shang You Ya","age":18}

大多数开发者直接使用 JSON.stringify(),但我即将揭示一些隐藏的技巧。


1. 第二个参数(Array)


-JSON.stringify() 接受第二个参数,它是一个你想在控制台中显示的对象的键的数组。例如:


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, ['name']));
// Result: {"name": "San Shang You Ya"}

这样而不是将整个 JSON 对象混乱地显示在控制台中,可以通过将所需的键作为数组传递给第二个参数来选择性地打印。


2. 第二个参数(Function)



  • 第二个参数也可以是一个函数,根据函数内的逻辑输出键值对。

  • 如果返回 undefined,则该键值对将不会被打印出来。


const obj = {  
name: 'San Shang You Ya',
age: 18
};

console.log(JSON.stringify(obj, (key, value) => (key === "age" ? value : undefined)));
// Result: {"age": 18}

3. 第三个参数作为数字



  • 第三个参数控制最终字符串中的间距。如果是一个数字,字符串化的每个级别将相应缩进。


const obj = {  
name: 'San Shang You Ya',
age: 18
};
console.log(JSON.stringify(obj, null, 2));

image.png


4. 第三个参数作为字符串


如果第三个参数是一个字符串,它将替换为空格字符


image.png


5. toJSON 方法


对象可以拥有一个 toJSON 方法。
JSON.stringify() 返回该方法的结果,并对其进行字符串化,而不是转换整个对象。


const superhero= {  
firstName: "San Shang",
lastName: "You Ya",
age: 21,
toJSON() {
return {
fullName: `${this.firstName} + ${this.lastName}`
};
}
};

console.log(JSON.stringify(superhero));
// Result: "{ "fullName" : "San Shang You Ya"}"

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

Redis 大 key 问题一文通

1. 背景 最近对接了一个卧龙同事的接口,因为接口比较慢,所以打算对第三方接口加个缓存。但是会有大 key 的问题。设计过程中调研了一些解决方案,这里总结下。 关键字:Redis;大Key问题; 2. 大 key 会带来什么问题 我们都知道,redis 是单线...
继续阅读 »

1. 背景


最近对接了一个卧龙同事的接口,因为接口比较慢,所以打算对第三方接口加个缓存。但是会有大 key 的问题。设计过程中调研了一些解决方案,这里总结下。
关键字:Redis;大Key问题;


2. 大 key 会带来什么问题


我们都知道,redis 是单线程架构,日常的读写操作都是由一个线程完成。一旦某一个线程执行了大 key 的读写,就会影响之后所有命令的执行,进而影响 redis 实例甚至整个 redis 集群的稳定。


3. 什么才叫大 key


那么什么才叫大 key?普遍认同的规范是:



  1. value > 10kb,即认定为大 key

  2. 像list,set,hash 等容器类型的 redis key,元素数量 > 5000,即认定为大 key


现在我们知道了大 key 会来带什么问题,也知道了什么样的 key 才算大key。接下来我们看看都有哪些解决方案。


4. 解决方案一:压缩


适用于字符串类型的 redis key。采用压缩算法,将 key 压缩至可接受的范围内。压缩也是有讲究的,首先要选择无损的压缩算法,然后在压缩速率和压缩率之间也要权衡。比较常用的压缩算法/工具如下:



  • google snappy:无损压缩,追求压缩速度而不是压缩率(Compression rate)

  • message pack:无损压缩,仅适用于 json 字符串的压缩,可以得到一个更小的 JSON,官网是:msgpack.org/


5. 解决方案二:value 切片


适用于 list,set,hash 等容器类型的 redis key。规范要求容器的元素数量 < 5000,我们可以在写 redis 的时候做个逻辑,如果超过了 5000 的容器就做切片。


举个例子,现在有一个 list 类型的缓存 ,他包含 12000 个元素。是很典型的大key。
image.png
我们以 5000 为阈值,把 list 切分成三份:user_list_slice_1、user_list_slice_2、user_list_slice_3,另外还需要一个存储切片具体情况的key,所以还需要一个 user_list_ctl。
业务程序后续访问这个缓存的时候,先请求 user_list_ctl,解析出缓存的切分情况,再去请求具体的切片即可。


6. 解决方案三:抛弃大 key(discard)


大多数场景,我们是把 redis 当缓存用,缓存失效了就走数据库查出数据。我们可以设定一个阈值,如果缓存对象特别大的话,我们就抛弃这个key,不缓存,直接走数据库。这样不会影响 redis 正常的运作。


image.png


当然,这是个取巧的方案,灵感是来自线程池的拒绝策略(DiscardPolicy)。采用这个方案得确认直接抛弃不会影响业务,还需要确保不走缓存后的性能业务上能够接受。



7. 俯瞰一下,从架构的角度解决这个问题


千叮咛万嘱咐,大 key 问题造成的线上事故仍然没有断过,这个怎么解决?
我觉得有如下几个思路



  • 完善监控机制,有大 key 出现就及时告警

  • 封禁/限流能力,能够及时封禁大 key 的访问,降低业务影响(保命用)

  • 在服务和 redis 集群之间建设 proxy 层,在 proxy 做大 key 的处理(压缩或者切片处理),让业务开发无需感知大key。


8. 总结


总结一下,解决 redis 的大 key,我们常规有三种解决方案。一是压缩,而是切片,三是直接抛弃不缓存。


作者:小黑233
来源:juejin.cn/post/7261254961923768380
收起阅读 »

拯救强迫症!前端统一代码规范

web
1. 代码格式化 1.1 工具介绍 ESLint 是一款用于查找并报告代码中问题的工具 Stylelint 是一个强大的现代 CSS 检测器 Prettier 是一款强大的代码格式化工具,支持多种语言 lint-staged 是一个在 git 暂存文件上运...
继续阅读 »

1. 代码格式化


1.1 工具介绍


Untitled 1.png



  • ESLint 是一款用于查找并报告代码中问题的工具

  • Stylelint 是一个强大的现代 CSS 检测器

  • Prettier 是一款强大的代码格式化工具,支持多种语言

  • lint-staged 是一个在 git 暂存文件上运行 linters 的工具

  • husky 是 Git Hook 工具,可以设置在 git 各个阶段触发设定的命令


1.2 配置说明


1.2.1 ESLint 配置


在项目根目录下增加 .eslintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D eslint eslint-plugin-vue eslint-plugin-import eslint-import-resolver-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-plugin-prettier eslint-config-prettier

module.exports = {
// 此项是用来告诉 eslint 找当前配置文件不能往父级查找
root: true,
// 全局环境
env: {
browser: true,
node: true,
},
// 指定如何解析语法,eslint-plugin-vue 插件依赖vue-eslint-parser解析器
parser: "vue-eslint-parser",
// 优先级低于parse的语法解析配置
parserOptions: {
// 指定ESlint的解析器
parser: "@typescript-eslint/parser",
// 允许使用ES语法
ecmaVersion: 2020,
// 允许使用import
sourceType: "module",
// 允许解析JSX
ecmaFeatures: {
jsx: true,
},
},
extends: [
"eslint:recommended", // 引入 ESLint的核心功能并且报告一些常见的共同错误
"plugin:import/recommended", // import/export语法的校验
"plugin:import/typescript", // import/export 语法的校验(支持 TS)
// 'plugin:vue/essential' // vue2 版本使用
// 'plugin:vue/recommended', // vue2 版本使用
"plugin:vue/vue3-essential", // vue3 版本使用
"plugin:vue/vue3-recommended", // vue3 版本使用
"plugin:@typescript-eslint/recommended",
"prettier", // prettier 要放在最后!
],
plugins: ["prettier"],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-undef": "off",
// 更多规则详见:http://eslint.cn/docs/rules/
},
settings: {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
};

💡当 ESLint 同时使用 prettier 的时候,prettier 和 ESLint 可能存在一些规则冲突,我们需要借助 eslint-plugin-prettiereslint-config-prettier 进行解决,在安装完依赖包后在 .eslintrc.js 配置文件中进行添加如下内容:


module.exports = {
"extends": [
// 其他扩展内容...
"prettier" // prettier 要放在最后!
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
},
}

1.2.2 StyleLint 配置


在项目根目录下增加 .stylelintrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D stylelint stylelint-config-standard stylelint-order stylelint-config-rational-order prettier stylelint-prettier stylelint-config-prettier postcss-html postcss-less stylelint-config-recommended-vue

module.exports = {
extends: [
'stylelint-config-standard', // 官方 stylelint 规则
'stylelint-config-rational-order', // 属性排列顺序规则
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
plugins: [
'stylelint-order', // CSS 属性排序
],
rules: {
// 更多规则详见:https://stylelint.io/user-guide/rules/list
},
};

💡当 StyleLint 同时使用 prettier 的时候,prettier 和 StyleLint 可能存在一些规则冲突,我们需要借助 stylelint-prettierstylelint-config-prettier 进行解决,在安装完依赖包后在 .stylelintrc.js 配置文件中进行添加如下内容:


module.exports = {
extends: [
/*
* 通过安装 stylelint-prettier,设置 'stylelint-prettier/recommended',其包含了三个操作
plugins: ['.'],
extends: ['stylelint-config-prettier'], // 需要安装 stylelint-config-prettier
rules: {'prettier/prettier': true},
*/

'stylelint-prettier/recommended',
],
};

1.2.3 Prettier 配置


在项目根目录下增加 .prettierrc.js 文件进行配置,配置项详见官方文档,以下为参考配置:


npm i -D prettier

module.exports = {
// 更多规则详见:https://prettier.io/docs/en/options.html
printWidth: 120, // 单行长度
tabWidth: 2, // 缩进长度
useTabs: false, // 使用空格代替tab缩进
semi: true, // 句末使用分号
singleQuote: true, // 使用单引号
bracketSpacing: true, // 在对象前后添加空格-eg: { foo: bar }
quoteProps: 'consistent', // 对象的key添加引号方式
trailingComma: 'all', // 多行时尽可能打印尾随逗号
jsxBracketSameLine: true, // 多属性html标签的‘>’折行放置
arrowParens: 'always', // 单参数箭头函数参数周围使用圆括号-eg: (x) => x
jsxSingleQuote: true, // jsx中使用单引号
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore', // 对HTML全局空白不敏感
};

1.2.4 husky 和 lint-staged 配置


step1. 初始化 husky


npx husky-init && npm install

step2. 在 .husky/pre-commit 文件中进行修改(注意区别 husky@7 与 husky@4 的设置方式)


#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

step3. 安装 lint-statged 并在 package.json 中进行设置


npm i -D lint-staged

{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"prettier --write",
"git add"
],
"*.{css,less,vue}": [
"stylelint --fix",
"prettier --write",
"git add"
]
}
}

1.3 使用参考



  1. 代码提交:根据上述工具配置,代码在提交仓库时进行检查和格式化,实现代码风格统一;

  2. 本地保存:在 VSCode 中进行配置,使得代码在保存的时候即按照相应的规则进行格式化;



如何在 VSCode 中进行配置使得能够自动按照相应的规则进行格式化呢?接下来进入第二章《编辑器配置》。



2. 编辑器配置


2.1 VSCode 配置


2.1.1 配置内容


Untitled.png


所有 VSCode 配置自定义的内容(包括插件部分)都在 setting.json 文件中,以下为参考配置:


{
"editor.tabSize": 2,
"window.zoomLevel": 0,
"editor.fontSize": 14,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.multiCursorModifier": "ctrlCmd",
"editor.snippetSuggestions": "top",
"eslint.codeAction.showDocumentation": {
"enable": true
},
"eslint.run": "onSave",
"eslint.format.enable": true,
"eslint.options": {
"extensions": [
".js",
".vue",
".ts",
".tsx"
]
},
"eslint.validate": [
"javascript",
"typescript",
"vue"
],
"stylelint.validate": [
"css",
"less",
"postcss",
"scss",
"sass",
"vue",
],
// 保存时按照哪个规则进行格式化
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.fixAll.eslint": true
},
"files.autoSave": "afterDelay", // 文件自动保存
"files.autoSaveDelay": 2000, // 2s 后文件自动保存
}

参考资料: VS Code 使用指南VS Code 中 Vetur 与 prettier、ESLint 联合使用


2.1.1 插件推荐



  1. Eslint: Integrates ESLint JavaScript int0 VS Code

  2. stylelint: Official Stylelint extension for Visual Studio Code

  3. Prettier: Code formatter using prettier

  4. EditorConfig: EditorConfig Support for Visual Studio Code

  5. Npm Intellisense: VS Code plugin that autocompletes npm modules in import statements

  6. Path Intellisense: VS Code plugin that autocompletes filenames

  7. Auto Rename Tag: Auto rename paired HTML/XML tag

  8. Auto Close Tag: Automatically add HTML/XML close tag

  9. Code Spelling Checker: Spelling checker for source code

  10. Volar / Vetur: Language support for Vue 3 / Vue tooling for VS Code


2.2 EditorConfig 配置


EditorConfig 的优先级高于编辑器自身的配置,因此可用于维护不同开发人员、不同编辑器的编码风格。在项目根目录下增加 .editorconfig 文件进行配置即可,以下为参考配置:


# Editor configuration, see http://editorconfig.org

# 表示是最顶层的 EditorConfig 配置文件
root = true

[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
insert_final_newline = true # 始终在文件末尾插入一个新行
trim_trailing_whitespace = true # 去除行尾的任意空白字符

3. Commit Message 格式化


3.1 工具介绍


Conventional Commits 约定式提交规范是一种用于给提交信息增加人机可读含义的规范,可以通过以下工具来进行检查、统一和格式化:



  • commitlint:检查您的提交消息是否符合 conventional commit format

  • commitizen:帮助撰写规范 commit message 的工具

  • cz-customizable:自定义配置 commitizen 工具的终端操作

  • commitlint-config-cz:合并 cz-customizable 的配置和 commitlint 的配置


3.2 配置说明


3.2.1 格式化配置


step1. 安装 commitizen 和 cz-customizable


npm install -D commitizen cz-customizable

step2. 在 package.json 添加以下内容:


{
"config": {
"commitizen": {
"path": "node_modules/cz-customizable"
}
}
}

step3. 在项目根目录下增加 .cz-config.js 文件进行配置即可,以下为参考配置:


module.exports = {
// type 类型
types: [
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{ value: 'style', name: 'style: 代码格式改变(不影响功能)' },
{ value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' },
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加或修改测试用例' },
{ value: 'build', name: 'build: 构建流程或外部依赖变更' },
{ value: 'ci', name: 'ci: 修改 CI 配置或脚本' },
{ value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' },
{ value: 'revert', name: 'revert: 回滚 commit' },
],
// scope 类型
scopes: [
['components', '组件相关'],
['hooks', 'hook 相关'],
['utils', 'utils 相关'],
['styles', '样式相关'],
['deps', '项目依赖'],
// 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
['custom', '以上都不是,我要自定义'],
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`,
};
}),
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
scope: '选择一个 scope(可选):\n',
customScope: '请输入自定义的 scope:\n', // 选择 scope: custom 时会出现的提示
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选):\n',
confirmCommit: '是否确认提交?',
},
// 设置只有 type 选择了 feat 或 fix,才询问 breaking message
allowBreakingChanges: ['feat', 'fix'],
// subject 限制长度
subjectLimit: 100,
};

step4. 新增 husky 配置,使得提交 commit message 时触发 commitizen,快捷命令如下:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && node_modules/.bin/cz --hook || true"

注意,commitizen 如果是全局安装,则使用下面的快捷命令:


npx husky add .husky/prepare-commit-msg "exec < /dev/tty && git cz --hook || true"

3.2.2 格式检查配置


step1. 安装 commitlint 和 commitlint-config-cz ****依赖:


npm install --save-dev @commitlint/{config-conventional,cli} commitlint-config-cz

step2. 在项目根目录下增加 commitlint.config.js 文件进行配置即可,以下为配置内容:


module.exports = {
extends: ['@commitlint/config-conventional', 'cz'],
rules: {},
};

step3. 新增 husky 配置,使得提交 commit message 时触发 commitlint 检验,配置内容如下:


npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

3.3 使用参考


在命令行输入 git commit,然后根据命令行提示输入相应的内容,完成之后则会自动生成符合规范的 commit message,从而实现提交信息的统一。


4. 代码规范参考


4.1 JS/TS 规范


社区类代码风格:



工具类代码风格:



4.2 CSS 规范


社区类代码风格:



工具类代码风格



4.3 VUE 规范


推荐阅读 Vue 官方风格指南:Vue2 版本Vue3 版本,其他可参考 eslint-plugin-vue


作者:植物系青年
来源:juejin.cn/post/7278575483909799947
收起阅读 »

APP端上通用安全体系建设

背景:APP端上安全在谈什么 APP的每个业务场景都有其既定的运行模式,若被人为破坏就可认为是不安全的。举个栗子,比如秒杀场景:大量用户在特定时间点,通过点击抢购来秒杀优惠商品,从而营造一种紧迫而有噱头的营销场景,但如果能通过非法手段自动抢购、甚至提前开始刷接...
继续阅读 »

背景:APP端上安全在谈什么


APP的每个业务场景都有其既定的运行模式,若被人为破坏就可认为是不安全的。举个栗子,比如秒杀场景:大量用户在特定时间点,通过点击抢购来秒杀优惠商品,从而营造一种紧迫而有噱头的营销场景,但如果能通过非法手段自动抢购、甚至提前开始刷接口抢购,那就彻底破坏了业务的玩法,这就是一种不安全的运行模式。再比如常用的用户拉新场景:新客获取成本高达200左右,所有产品的拉新投入都蛮高,如何获得真正的新用户而不是羊毛党也是拉新必须处理的事,一般而言,新设备+新账户是新用户的基本条件,但新账户的成本其实不高,大部分是要靠新设备来识别的,但如果能通过非法手段不断模拟新设备,那拉新投入获取的可能大部分都是无效的羊毛党,这也可看做是一种不安全的运行场景,甚至还有二次篡改,构建马甲APP等各种场景。而APP端上安全要做的就是甄别并防范这种异常场景的发生,简而言之它就是:一种确保官方APP既定业务模型中运行的能力。


APP端上安全体系应该具备哪些能力


一个安全体系要具备哪些能力呢,简单说可分两块:甄别与防御。即一:甄别运行环境是否安全的能力,二:针对不同的场景作出不同的防御的能力,场景千变万化,所以防御手段也没有一剑破万法的能力,基本都要根据具体的风险场景,产出不同的应对方案,但是整体涉及的流程基本一致,如下:


image.png


要做的事情就是围绕四个阶段构建不同的能力。先看第一阶段:风险场景的假设,预判有哪些风险场景,从AppStore或者官方应用市场下载,安装、正常使用,这是理想中的运行模式,但不安分的用户千千万,异常场景也多变,不存在一剑破万法的说法,这里人为做了一些场景归类:


image.png


从实践来看,安全模型必须覆盖的场景包含上述四类,即



一: 运行的APP非官方应用



这种情况一般是非法用户为了谋求特定的权益,对原装APP进行魔改二次打包后发布,这对于一些广告类、离线付费类APP是一种毁灭性的打击,最常见的就是一些APP会在闪屏页面投放广告,从而获取收益,但一旦这个业务逻辑被篡改绕过,那么广告收益直接归零;还有些情况APP被碰瓷,魔改一些功能,跳转到非官方设定的网站,比如在比较火的APP添加劫持逻辑,跳转到一些黄赌网站,这给官方APP带来的负面影响是难易估量的,如果不能自证清白,甚至还会面临法律上的追责。



二:业务的运转模型被篡改



简单的讲就是没有按照产品的既定玩法进行,就像文章开头的秒杀场景【电商平台的茅台抢购】,如果能通过API直接组单,那APP里点击的那批用户无论如何也抢不过插件;再比如有些签到的场景是为了促活,如果通过自动签到工具自动打卡,那促活的目的也无法达到,这也是要重点防控的一个场景。



三:运行的设备非目标设备



这种场景主要影响一些拉新、优惠权益等,甚至直接影响整体运营策略,简单举个例子,拉新场景中会投放大量的新人免单、直减券,基本等于免费领取权益,比如我们都经历过的打车软件、外卖软件、新电商产品上线等。全国有无数的专业羊毛党专注于这一场景,他们掌握大量手机号,可以注册大量新账号,同时能通过虚拟机、应用多开、复刻设备等手段无限冒充新设备,如果不加反制,优惠策略一上线,各种券基本全被这批人掠夺,被这种手段折磨致死的产品数不胜数,也几乎是每个电商平台的噩梦。



四:一些核心逻辑的泄漏



这不是端上特有的风险,比如代码泄漏这种,各方都有这种风险,只不过端上风险更高,因为APP是要上架发出去的,虽然经历过各种混淆与保护,但是用户最终还是能拿到可执行的APP包的,里面各种的核心的逻辑、秘钥都有潜在泄漏的风险,一旦泄漏,也是一种毁灭性的打击,比如某些音视频软件特有滤镜、转场、特效,这些算法是一个产品的核心,一旦破解,产品优势不再,如果防止逻辑代码的泄漏与破解也是安全必须关注的点。


覆盖上述场景的意思是要提供甄别的能力,针对不同的场景,抽象特征值并搜集上报,建立特定的模型,推导C端操作环境是否安全,之后上线不同的应对策略:比如直接退出应用、标记为风险用户等。


建设方案


甄别与防御是体系的核心,建设方案主要是围绕这两个主题展开,虽说名称是“端上安全体系”,但只依靠端自己是无法解决所有问题的,也无法将价值发挥到最大,仍需多端系统配合来完成整个体系的搭建,分工的基本原则是:端上侧重特征信息的搜集,云端负责整体策略的执行,根据上述场景,搭建示意如下:


image.png


按层次跟功能大致分四块,端、网关、业务后台、数据风控中心:



  • 端是信息的来源,负责信息采集上报,是安全体系建设的基石,所以端上采集信息的真实性、为完整性至关重要,同时端上也可以执行部分风险低,但收益高的拦截略,比如对于一些已经侦测到的马甲包,在上报用户信息之后,可以选择Crash,对于一些机器点击类的签到、秒杀场景,可以主动拦截请求,降低带宽压力。

  • 网关是第二层,一般处理一些具体规则类的拦截与信息采集,比如有些简单的规则检验,Header里是否携带必备的校验字段,如多开标识、模拟器标志等,如果携带则可以在这一层直接拦截,并沉淀到数据中心,既保证了信息的采集,又能减轻业务后台的压力,尤其对于一些秒杀类的场景非常有效。

  • 业务后台跟数据平台可以一起看做第三层,负责更复杂的模型建设跟业务落地,比如在什么样的节点,才有什么样的策略,比如在组单的时候,业务后台可根据风控侧的判断决定用户的优惠力度等


针对各个具体的场景会有具体的建设方案,



如何应对非官方APP:APP包识别



非法人员总是出于自己的权益来破解官方APP,定制一些逻辑再二次打包发布,比如对一些付费类APP,含广告类工具APP等,通过破解代码,并二次打包后就可以官方造成冲击,甚至是毁灭性的打击,对于这种场景如何甄别,又如何处理呢?拿Android为例,检测手段有签名校验、文件校验、包完整性校验等,一旦检测到风险就可以做出响应处理,在处理方式上也需要根据不同产品不同场景随机变动,比如工具类APP就Crash阻断,而对于一些有用户体系类的APP则可以先回传用户信息,用作用户画像,再做响应的处理,而处理的手段也可以根据风险等级的不同再做定制,甄别的技术手段可能是死的,但获取的收益一定要灵活。



如何应对非理想设备:设备识别与设备指纹



非理想设备最经典的就是文章开头的场景,拉新拉来一堆老羊毛党,说到底其实就是对于用户设备的定位追踪能力不足,对于这种场景的如何应对?这里单独说说设备指纹,设备指纹主要解决的如何定位一台设备的问题,在理想情况下,一台设备只有一个身份信息,它不因APP卸载、升级、HOOK伪装所改变,这在现在的互联产品生态中是非常困难的,困难主要来源于两个方面:一技术上的、一是法规上的,从技术上来讲,以Android端为例,它是一个开源的系统,每一行代码都不可信,任何通过官方API拿到的信息都可以被HOOK篡改,而指纹很大程度依赖API获得的设备特征信息,如果这些信息都不可信,那指纹的可信程度也会降低。另一方面,从法规上来讲,现在注重保护个人隐私,不可以随意获取用户信息,这一点有利于用户【包括羊毛党】,但是对于运营方却是不利的,信息越少越难定位到,因此,在隐私合规的前提下,仍需要多维度的获取更多的用户信息,从更多的维度定位到该用户。


具体如何执行?以Android为例,定位一台设备的信息有MAC、IMEI、IMSI、序列号、AndroidID、IP+UA、OAID、各种设备型号等,虽然信息很多,但单独任何一条的可信度都不高,比如之前的某盾、某盟都曾用MAC地址作为指纹,甚至有些产品直接用IMEI作为指纹,但网上利用XPOSED来篡改的插件比比皆是,通过官方API获取的分分钟被破解,但是可以对多种信息进行整合生成一个唯一可信的ID,这种方式获取的ID的稳定性要比单一的稳定性要高,原理示意如下:


image.png


简单来说:只有篡改了全部的设备特征信息,才会导致设备指纹更新,这会大幅提高设备逃逸的难度。设备指纹的另一个难题是如何识别虚拟设备,这里特指模拟器,每个新开的模拟器都可以看做是新设备,如果不能识别,同样无法解决设备跟踪的问题,尤其对于国内的Android生态来讲,问题更加严重,各种游戏厂商都有对应的手游模拟器,不仅支持多开,还原生支持篡改各种设备特征信息,可以算得上助纣为虐,在模拟器甄别与防控的层面能做的有如下几种:



  • 通过特征信息甄别【容易绕过】

  • 通过CPU架构甄别【ARM与SimpleX86】

  • 限定APP的运行平台


这里简单介绍下通过CPU架构甄别方式,就目前的硬件市场,几乎99.9%以上的手机设备都是基于ARM处理,而模拟器大部分是面向x86平台设计的,采用的是simplex86架构,两者采用的不一样缓存机制,ARM采用的哈弗架构将指令存储跟数据存储分开,分为I-Cache(指令缓存)与D-Cahce(数据缓存),CPU无法直接修改I-Cache【同步延迟导致不一致】,但Simpled X86架构的模拟器只有一块缓存,这一点导致两者在运行Self-Modifying Code【自修改代码】时会有不同的表现,可以借助这个特性进行甄别,示意如下


image.png


至此,设备的定位与跟踪能力基本已经具备,在用户在领券的节点,就可以从更多维度判断他当前的设备是否有资格享受这个权益,保障业务按既定模型运转。


image.png


当然还有更多的场景,比如应用多开、应用分身等,都要具体问题具体分析,但思路一致:特征搜集、甄别、防控,因为所有的不轨行为一定有迹可循,



如何应对非设定业务场景:场景识别与校验



每种业务都有其既定的运行模式,只有照章办事,运营才能获取最大的收益,这里特指一些可以通过自己的参与获得收益的场景,比如秒杀、签到、预约摇号等。一般而言,在这类场景下,破坏者可钻的空子有两个方向,一个是便利:通过插件自动预约,免得用户自己操作,适合摇号、签到类【签到领积分】;一个是速度,通过插件直接API请求,抢跑下单,获得收益,适合限时秒杀类的场景【各大平台强茅台】。以秒杀为例,通过营造紧迫而又刺激的氛围可以让活动更有意思,但如果能直接刷接口/或者通过插件抢跑,那就会破坏其公平性,影响用户的参与感,造成资产及口碑受损,这类场景如何应对?其实要做的事情分两块:一 识别请求是从APP发出来 , 二 识别是真实用户操作的,这两快一般会整体考虑,非APP端的请求往往伴随着非用户触发,多归结于脚本,所以识别“人”与识别“场景”殊途同归,具体有哪些手段可以用呢?



  • 扩展核心API接口的能力,承载更多逻辑

  • 通过埋点、用户操作轨迹分析识别用户

  • 启用端上特有能力校验,如短信验证码、行程码分析


如何拓展API接口的能力?比如预约接口其基础能力就是预约,如不特殊处理,PC上就完全可以复制APP端发出的请求,进而通过脚本预约,如要加以限制就必须拓展端上API能力,让其携带更多端上独有特征,同时服务端可以完成校验,形成一个闭环,比较容易理解的就是让APP端与服务端协商一套加解密通信协议,并假定协议无法破解,避免接口直刷,从而确保请求是从APP发出的,即使不是从APP发出的,也能被甄别出来,进而提高APP与服务端通信阶段的安全性。当然,无法破解只是理论上,实际上只要舍得投入成本,暴力破解并不是问题,这种就需要通过更多元的手段,不断更新迭代,持续做攻防,例如,为了保证加密算法的保密性,可以将其用c实现,并通混淆、加固、防探测等手段保证这个策略的正确执行;暴力堆积加密的类型、节点,提升秘钥的更新频率也是一种应对手段,而且,惩罚手段上也可以多元,同直接拦截相比,隐秘的搜捕,诱捕也是一种灵活收益的手段。


其次,基于埋点、用户操作行为的大数据分析是另一种更高级的防御手段,对于识别用户操作场景更加科学,正常的用户轨迹与插件类的访问轨迹会有很大的差异,直刷的目标明确,主攻几个关键接口,但正常用户访问会有一系列的曝光、点击等行为,并且每次的点击也会有各种零零散散的活体特征可以采集,比如点击的点位置、数量、力度、频率等,这些维度为用户识别提供了更广的操作空间。基于以上几点的模型示意如下:


image.png


最后一点,启用端上特有的校验能力,这个已经是最终的防御手段,在实在没有办法的情况才会采用的,因为这种手段很影响用户体验,由于采用的是端上特有的能力,比如短信验证码,必须真机才能收到,这就从根本上避免了插件类的直刷,所以可靠性确实所有手段中最高的,但体验差,成本高,所以算是最后一道防线。



如何应对核心逻辑的泄漏



这一块主要关注的是APP端的一些核心逻辑的破解或泄密,可以分两个方向,对外与对内,对外主要是APP包的逆向与破解,不法人员从发布上架的APP包中获取核心业务实现或其他敏感信息;而对内主要指工程安全,核心源码或秘钥的泄漏、误改等。


相应的防范策略也是分两块,对外的线上防破解可以从以下几点入手:



  • 利用代码混淆防APP逆向,一般而言官方会提供相应的能力,也可借助三方加固来提高混淆的力度

  • 核心源码、秘钥下沉,采用更难破解的方式实现,同时增加防外部调用的防范策略,比如Android采用C+混淆来处理

  • 为线上APP添加防调试与HOOK的能力,防止动态调试探测,

  • 添加防止代理与中间人劫持的能力,例如SSLPING等技术,避免被抓包探测

  • 从二次打包入手,添加签名、完整性检测的能力,防止被探测、篡改


而对内主要从工程安全角度推进,主要是做好代码的权责管理



  • 采用组件化开发模式,不同等级的基础能力、业务、核心逻辑做好隔离

  • 仓库单独部署,同时做好权责划分,代码、文档做好权限隔离

  • 加强秘钥、KEY的管控,开发与生产环境严格隔离


上述手段基本涵盖大部可预见的风险场景,即使未覆盖,也大概有类似的手段作为参考,无非就是抽象、搜集、判断、处理。


线上执行方案


最后一步是上线执行,上述的手段多种多样,但相互之间并非孤立运行,彼此可以相互穿插,灵活配合,不存在特定的章法,全看使用方的意图,如何探测,探测之后如何处理,是全杀还是放一部分,都看操刀者自己的运作,以应用多开场景为例,除了利用多开基础的多开检测手段,还可以配合设备指纹做更多的事情,有时虽然没有检测到多开,但是基于设备指纹的补刀,也能定位到问题设备,而在最后一步惩治处理中,不同处理手段也会获得不一样的收益:


类型处理方式最终收益优缺点
被动拦截端上部署检测规则,检测到风险,100%在端上拦截处理【如Crash】效果明显,但易被发现,徒增防御成本
被动捕获检测到风险,在端上不处理,只上报,后端隐形标记或拦截不易被发现,但长期运行收益比较局限
主动诱捕人为制造有迹可循的漏洞,捕获后在端上不拦截或部分拦截,并上报,后端隐形标记不易被发现,虚虚实实,操作空间更大,收益更大

理论上讲,APP技术层面不存在100%有效的安防策略,虚虚实实才是王道,敬畏,才是最有效的防御手段


总结与展望


目前国内APP的生态环境并不健康,甚至可以说野蛮,随着隐私策略收紧,APP所能获取的信息越来越少,安防也越来越难做,反之,刷子却越活越滋润,技术所面临的的挑战也更加棘手,安防注定是一个长期攻防的领域。最后,技术不能解决所有问题,最终还是要依赖法律的健全与全民意识提升。


作者:看书的小蜗牛
来源:juejin.cn/post/7350354672861052980
收起阅读 »

反转反转再反转,揭秘人心深处的“恶意”

“……真是过分,我每天可是难过得要死。有个爱管闲事的邻居,每天都来找我,我没办法,只好去上学,都快给他烦死了。” “老师和学生的关系建立在一种错觉上。老师错以为自己可以教学生什么,而学生错以为能从老师那里学到什么。重要的是,维持这种错觉对双方而言都是件幸福的...
继续阅读 »

“……真是过分,我每天可是难过得要死。有个爱管闲事的邻居,每天都来找我,我没办法,只好去上学,都快给他烦死了。”


“老师和学生的关系建立在一种错觉上。老师错以为自己可以教学生什么,而学生错以为能从老师那里学到什么。重要的是,维持这种错觉对双方而言都是件幸福的事。因为若看清了真相,反而一点好处都没有。我们在做的事,不过是教育的扮家家酒而已。”


是什么样的经历让他说出这样的话呢?我不明白。


大家好,我是杰哥


昨天晚上凌晨三点,我终于第三次翻完了《恶意》。这本书,真的让我欲罢不能!每次阅读都像是一次心灵的冒险,让我惊叹不已。


作者东野圭吾巧妙地运用手记的方式,将故事的发展娓娓道来。那些看似简单平实的文字,却隐藏着令人震撼的真相。他对复杂人性抽丝剥茧的深刻描画,简直让我眼花缭乱,哑口无言。


故事围绕着一起谋杀案展开:畅销书作家在出国的前一晚于家中被杀,凶手很快便落网了。但别以为这只是个简单的“谁是凶手”的故事,其实更多的是”我为什么要杀他“的故事。


凶手对作案动机语焉不详,倒是引起了著名侦探”加贺“的兴趣,他凭借自己一贯对于人性觉察比较敏锐的”直觉“,以及自己曾经作为老师所亲身经历过的“校园暴力”事件,对”作案动机“展开了缜密的分析与调查。经过层层曲折的调查,终于将真实的动机呈现在我们的面前(此处故意不剧透,以免影响了大家的阅读体验)。我只能说,得知真相的你,一定会被震撼到,从而陷入深思。


我是一个悬疑推理类书籍的书迷,看过很多悬疑推理类的书籍,而这本则是一本题材与故事都很新颖且富有创造力,结局也会很让人意外的其中之一。


读这本书的过程中,我就像是坐过山车一样,情绪起伏不定。每当我以为抓到了真相的尾巴,作者就会巧妙地用一个新的情节把我甩回去。反转再反转,直到最后,我才恍然大悟:哦,原来真相是这样的!大概真正优秀的悬疑推理类小说的作家的仅有的几部作品中,才可以与读者的互动达到这样的效果吧。


这本书不仅仅是悬疑,它更深刻地探讨了人性。恶意,这个看似抽象的概念,在书中被具象化,变得触手可及。它让我不禁深思,人心的黑暗面到底能有多深,我们又该如何面对和控制自己内心的恶意。


东野圭吾的笔下,每个人物都有自己的秘密,每个线索都可能是个陷阱。他的作品,让人读起来往往感觉惊险又刺激,恨不得一口气读完,甚至连旁边的手机也被冷落了。


总之,如果你喜欢心理悬疑,喜欢深度剖析人性的作品,那《恶意》绝对不容错过。它会让你在紧张刺激的阅读中,体验到心灵的震撼!


作者:舒米勒
来源:mdnice.com/writing/924e74e14de748d3b72493d7224aba0d
收起阅读 »

跳舞的人

跳舞的人 从我们大学的老校区南院门口进入,迎面便能看到庄严的主楼。 河北大学主楼 主楼后面是一段有花草树木的路,旁边是多功能馆。在主楼和多功能馆中间,有一块空旷的场地,人们清晨、傍晚常常在那里嬉戏玩耍。 大学一年级下半年、...
继续阅读 »

跳舞的人


从我们大学的老校区南院门口进入,迎面便能看到庄严的主楼。



河北大学主楼

河北大学主楼


主楼后面是一段有花草树木的路,旁边是多功能馆。在主楼和多功能馆中间,有一块空旷的场地,人们清晨、傍晚常常在那里嬉戏玩耍。


大学一年级下半年、大学二年纪一整学年我都是在主楼内的我们学院的机房里值班的。所谓值班就是,我晚上需要在机房对面的小屋里睡觉,白天需要管理好机房的日常使用工作。


因为早晨经常需要帮在机房上课的老师学生开门,所以在主楼住的时候,我会醒的比较早。


醒来后,我会先去食堂吃饭,然后在主楼后面散散步,转一圈。


有天在散步的过程中,听到了主楼前的方向有音乐的声音,我对这音乐比较好奇,便循着声音来到了主楼和多功能馆旁的空旷场地。这里放着一个看起来又大又重的音响,音乐便是从这个音箱里传出来的。在大音箱旁边,一个男生在跟随着音乐跳着舞。


早晨的时间是比较充分的,我便在这站了一会,发现跳舞的男生,每个舞蹈都会重复很多遍。我并不懂跳舞,只是能够感觉出来每一遍舞蹈,都是那么认真、那么投入,我猜他很热爱跳舞吧。


后面每每有时间,我都会走到那块空旷的地方,也经常能够看到他在这里跳舞。我想,这块空旷的场地就是他的舞台吧。


有天早晨,雨下得很大,我很早便被雨声吵醒了,我突然想看看「那个跳舞的人,下雨天会不会来练习跳舞呢?」


我便穿好衣服、鞋子,来到主楼不会被雨淋到的台子上。不一会,我便看到一个打着大伞的男生,拉着音箱走了过来。随着男生越来越近,我看清了,就是他——那个跳舞的人


他把音箱拉离地面,一步一步地走上台阶。我心里想,这么大的音箱,拉到台子上,不重吗?


他慢慢近了,来到了我旁边,我和他互相说了声你好,他便打开了音箱,放起了音乐,跳起了舞来,依旧那么认真、那么投入。


外面很冷,看到他来了,心里的疑问算是解开了,我便向主楼里面走去,回到了机房。


在回机房的路上,我便想,因为热爱,音箱便不会重了吧。


作者:随机的未知
来源:mdnice.com/writing/6882be2b53a04567a77d7be826eef49c
收起阅读 »

工作思考|研发环境好好的,怎么上线就出问题了?

场景再现 那是一个夜黑风高的晚上,某个版本迭代经过了完备的测试,正准备上线。研发同事A开完了上线评审后,信心满满地对运维同事B说:“开冲!” 几分钟后,同事B发了条消息过来,看着抖动的头像,同事A心想:小B效率真高啊,这么快!点开消息一看【启动报错了,你看一下...
继续阅读 »

场景再现


那是一个夜黑风高的晚上,某个版本迭代经过了完备的测试,正准备上线。研发同事A开完了上线评审后,信心满满地对运维同事B说:“开冲!”


几分钟后,同事B发了条消息过来,看着抖动的头像,同事A心想:小B效率真高啊,这么快!点开消息一看【启动报错了,你看一下】。


什么?启动还能报错,不可能啊,我研测环境都好好的。


小A火急火忙地连上堡垒机,看了下日志,报错信息大致为 【表tb_xxx没有找到】。


“怎么可能,我用了伟大的flyway,怎么可能会没有表呢?”小A如是说道。



提到flyway,这里简单介绍一下。Flyway是一款开源的数据库版本管理工具,可以实现管理并跟踪数据库变更,支持数据库版本自动升级,而且不需要复杂的配置,能够帮助团队更加方便、合理的管理数据库变更。只需要引入相应依赖,添加配置,再在resource目录下创建db/migration/xxxx.sql文件,在sql文件中写入用到的建表语句,插入语句即可。



不管怎么说,代码是不会骗人的。先找下是哪里出了问题!


小A很快就定位到了代码位置,是一个用于缓存的HashMap,这操作也没什么问题,相信大家都这么用过,对于一些一次查找,到处使用,还亘古不变的表信息,可以先查一次,把它用Map缓存起来,以便后续使用。


但是研发同事C把这段代码放在了afterPropertiesSet()​方法内部,没错,就是那个InitializingBean​接口的方法。看到这里,相信各位熟练背诵Bean生命周期的Java Boy已经明白了!查询数据库的操作在Bean还没有创建完成的时候就进行了!而此时,flyway脚本还没有执行,自然就找不到对应的表信息了。


那怎么办呢?


解决方法


解决方法很简单,sql执行的时候找不到表,那就让它在表创建完之后再执行!


1.CommandLineRunner接口


一个方法就是我们常用的CommandLineRunner​接口,重写run()​方法,把缓存逻辑移到run()​方法中。原因是run()方法的执行时机是在SpringBoot应用程序启动之后,此时flyway已经执行完毕,表结构已经存在,就没问题了!


2.@DependsOn注解


通过代码分析,flyway的加载是由flywayInitializer​这个Bean负责的。所以只需要我们的Bean在它之后加载就行了,这就用上了@DependsOn​注解。



@DependsOn注解可以定义在类和方法上,意思是我这个Bean要依赖于另一个Bean,也就是说被依赖的组件会比该组件先加载注册到IOC容器中。



也就是在我们的Bean上加上这么个注解@DependsOn("flywayInitializer")


总结


此次线上问题复习了Bean的生命周期,复习了InitializingBeanCommandLineRunner​两个接口,复习了@DependsOn​注解。


作者:钱思惘
来源:juejin.cn/post/7349750846898913332
收起阅读 »

时间格式化,显示昨天、今天

web
时间格式化的需求: 今天的数据显示“时分”,HH:mm 10:00 昨天的数据显示“昨天 时分”, 昨天 10:00 今年的数据,显示 “月日 时分”, 05-01 10:00 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00 代...
继续阅读 »

时间格式化的需求:



  • 今天的数据显示“时分”,HH:mm 10:00

  • 昨天的数据显示“昨天 时分”, 昨天 10:00

  • 今年的数据,显示 “月日 时分”, 05-01 10:00

  • 不是今年的数据,显示“年月日 时分”, 2022-05-01 10:00


代码展示



在 ios中 用new Date("2022-05-01 10:00").getTime()会有兼容性问题,跟日期格式的连字符有关系,这里使用moment插件



const moment = require("moment");

// 判断日期是不是今天、昨天, 0:今天 -1:昨天 1-明天
// str: 2023-02-07 14:09:27.0
export function isWhichDay(str) {
const date = new Date();
const that = moment(moment(str).format("YYYY-MM-DD")).valueOf();
const today = moment(moment(date).format("YYYY-MM-DD")).valueOf();
const timeStampDiff = that - today;
const obj = {
"-86400000": "-1",
0: "0",
86400000: "1",
};
return obj[timeStampDiff] || null;
}

// 判断是不是当年
export function isCurYear(str) {
return moment().format("YYYY") === moment(str).format("YYYY");
}

/**
* 格式化时间 YYYY-MM-DD HH:mm:ss
* 1、当天时间显示如 10:00
* 2、昨天显示如 昨天10:00
* 3、昨天之前且当年的,显示如,05-01 10:00
* 4、昨天之前且跨年的,显示如, 2022-05-01 10:00
*
@param {string} time "2022-05-01 10:00:01.0"
*
@returns {string}
*/

export function formatTime(time) {
const t = isWhichDay(time);
if (t === "0") {
return moment(time).format("HH:mm");
} else if (t === "-1") {
return `昨天 ${moment(time).format("HH:mm")}`;
} else if (
isCurYear(time) &&
moment(time).valueOf() < moment(new Date()).valueOf()
) {
return moment(time).format("MM-DD HH:mm");
} else {
return moment(time).format("YYYY-MM-DD HH:mm");
}
}



作者:甜点cc
来源:juejin.cn/post/7226300253921558583
收起阅读 »

环信WEB端单群聊 UIKit 快速集成与消息发送指南

写在前面:千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了...
继续阅读 »

写在前面:

千呼万唤始出来,环信web端终于出uikit了,环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了 IM SDK,可以帮助开发者不关心内部实现和数据管理就能根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。现在就让我们一起探索如何集成吧!本文介绍如何快速实现在单聊会话中发送消息


准备工作:

  1. React 环境:需要 React 16.8.0 或以上版本;React DOM 16.8.0 或以上版本。

  2. 即时通讯 IM 项目:已在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了 App Key

  3. 环信用户:在环信控制台创建 IM 用户,并获取用户 ID 和密码或 token。


  4. 好友关系:双方需要先添加好友才可以聊天







集成uikit:

准备工作完成就开始集成!在此先奉上uikit源码

第一步:创建一个uikit项目

# 安装 CLI 工具。
npm install create-react-app
# 构建一个 my-app 的项目。
npx create-react-app my-app
cd my-app

第二步:安装 easemob-chat-uikit

cd my-app
  • 使用 npm 安装 easemob-chat-uikit 包
npm install easemob-chat-uikit --save
  • 使用 yarn 安装 easemob-chat-uikit 包
yarn add easemob-chat-uikit

第三步:引入uikit组件

在你的 React 项目中,引入 UIKit 提供的组件和样式:

// 导入组件
import {
UIKitProvider,
Chat,
ConversationList,
// ...
} from "easemob-chat-uikit";

// 导入样式
import "easemob-chat-uikit/style.css";

第四步:初始化配置

easemob-chat-uikit 提供 UIKitProvider 组件管理数据。UIKitProvider 不渲染任何 UI, 只用于为其他组件提供全局的 context,自动监听 SDK 事件, 在组件树中向下传递数据来驱动组件更新。单群聊 UIKit 中其他组件必须用 UIKitProvider 包裹。

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
</UIKitProvider>
</div>
);
}

export default App;


第五步:引入组件

根据自己的项目引入所需组件,组件文档,本文只介绍如何快速实现在单聊会话中发送消息,为了方便快速体验,一定要确保准备工作的第四条双方已经互为好友

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
<div style={{ display: "flex" }}>
<div style={{ width: "40%", height: "100%" }}>
<ContactList
onItemClick={(data) => {
rootStore.conversationStore.addConversation({
chatType: "singleChat",
conversationId: data.id,
lastMessage: {},
unreadCount: "",
});
}}
/>
</div>//联系人组件,点击某个好友通过‘rootStore.conversationStore.addConversation’创建会话
<div style={{ width: "30%", height: "100%" }}>
<ConversationList />//会话列表组件
</div>
<div style={{ width: "30%", height: "100%" }}>
<Chat />//聊天消息组件
</div>
</div>
</UIKitProvider>
</div>
);
}

export default App;


第六步:运行并测试

1、运行项目

npm run start

2、点击好友并发送一条消息


总结:

通过以上步骤,你已经成功集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧


收起阅读 »

【CSS浮动属性】别再纠结布局了!一文带你玩转CSS Float属性

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。一、什么是CSS浮动属性浮动属性是CS...
继续阅读 »

在网页设计的世界里,CSS浮动属性(float)就像一把双刃剑。它能够让元素脱离文档流,实现灵活的布局,但如果处理不当,也可能引发一系列布局问题。

今天,我们就来深入探讨这把“剑”的正确使用方法,让你的页面布局既美观又稳定。

一、什么是CSS浮动属性

浮动属性是CSS中的一个定位属性,它允许元素脱离文档流,并向左或向右移动,直到它的外边缘碰到包含框或者另一个浮动元素的边缘。简单来说,它就像是让元素“漂浮”在页面上,不受常规排列规则的限制。

在网站开发中需要一行排列多个元素,使用浮动可以方便实现。下面是使用浮动排列多个元素。

Description

下面我们来了解一下浮动属性的基本使用语法和常用的应用场景都有哪些。

1.1 浮动属性的语法

selector {
float: 值;
}

其中,选择器是你想要应用浮动属性的元素的选择器,值可以是以下之一:

  • none:这是默认值,元素不会浮动,即保持在标准文档流中的位置。

  • left:元素将向左浮动,它会尽量向左移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

  • right:元素将向右浮动,它会尽量向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

1.2 浮动的应用场景

CSS浮动属性的应用场景主要包括以下几点:

布局定位:
浮动可以用于创建复杂的页面布局,例如将块级元素放置在一行内,或者创建多列布局。

文本环绕图片:
这是浮动最常见的应用之一,通过将图片设置为浮动,可以使文本自动环绕在图片周围,从而实现类似印刷布局中的文本环绕效果。

清除元素间缝隙:
浮动元素会紧挨着排列,没有间隙,这可以用来清除列表或图片间的空格,使得元素紧密排列。

创建下拉菜单
浮动还常用于创建下拉菜单或弹出式菜单,通过将菜单项设置为浮动,可以实现菜单的显示和隐藏。

实现侧边栏:
在网页设计中,浮动可以用来创建固定在一侧的侧边栏,而主要内容则围绕侧边栏流动。

创建瀑布流布局:
在响应式设计中,浮动可以用来实现瀑布流布局,这种布局可以根据浏览器窗口的大小自动调整列数和列宽。

1.3 盒子的排列规则

在使用浮动属性后盒子是如何排列的呢?

Description

  • 左浮动的盒子向上向左排列
  • 右浮动的盒子向上向右排列
  • 浮动盒子的顶边不得高于上一个盒子的顶边
  • 若剩余空间无法放下浮动的盒子,则该盒子向下移动,直到具备足够的空间能容纳盒子,然后再向左或向右移动

二、浮动的核心特点

下面将通过这个示例来给大家讲解浮动的特点:

<div>
<div>正常元素</div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级正常元素</div>
<div>我是浮动元素后面的第二个同级正常元素</div>
</div>
.container{
width: 200px;
height: 200px;
background-color: red;
}
.box1{
background-color: green;
}
.box2{
background-color: brown;
}
.box3{
background-color: pink;
}


.float{
float:left;


background-color: yellow;
}

Description

2.1 包裹性

具有“包裹性”的元素当其未主动设置宽度时,其宽度右内部元素决定。且其宽度最大不会超过其包含块的宽度。

设置了float属性(不为none)的元素都会具有包裹性。

在上面的例子中float元素不设置宽度,其宽度也不会超过container元素的宽度。

2.2 块状化并格式化上下文

设置了float属性(不为none)的元素,无论该元素原本是什么元素类型,其display属性的计算值都会自动变成"block"或’table(针对inline-table元素)'。并且浮动元素会生成一个BFC(块级格式化上下文)。

所以永远不需要对设置了float属性(不为none)的元素再设置"display:block"属性或者vertical-align属性!这都是多余和无效的。

2.3 脱离标准流

设置了float属性(不为none)的元素,都会脱离标准流。标准流一般是针对块级或行级元素(不包括行内块)。

通俗一点解释是,浮动元素A会“漂浮”在标准流上面,此时其原始占用的标准流空间由同级的后续第一个标准流兄弟元素B顶替(但是元素B中的文本内容会记住浮动元素A的位置,并在排布时避开它,由此形成文本环绕效果)。所以会出现B的部分内容会被飘起来的A所遮挡的现象。

有人可能会问,上面的例子中好像没发现类似"A遮挡B"的现象啊?

其实并不是,具体解释如下:
我们将box2元素(即浮动元素后续的第一个同级元素)的文本内容减少一些,使其不换行并不占满一行方便解释效果。

Description

图片这时候发现box2是和container元素宽度200是一致的,而不是自身文本的宽度。由于浮动元素的脱离文档流,.box2会忽略浮动元素的原空间(即当其不存在),由因为普通div不设置宽度默认会是父元素宽度。

所以这里box2和其父元素container宽度一致。但又因为浮动元素会使box2的文本环绕,导致box2的文本重新布局排版,“移动”到了紧跟浮动元素的右边界的地方。所以此时可以看作box2被浮动元素遮挡的那一部分实际是空背景。

2.4 高度坍塌

当你给浮动元素设置了具体宽高度,并增加box2元素的文本内容,也许这种脱离文档流现象更明显,如下示例:

Description

浮动元素脱离标准流的特性很容易带来的一大问题就是——父元素的高度塌陷

我们将html内容精简,只保留浮动元素和box2元素:

<div>
<div>
浮动元素
</div>
<div>我是浮动元素后面的第一个同级元素</div>


</div>

然后设置浮动元素宽高度,并去掉父元素设置的宽高度(核心点)

.container{
/* width: 200px;*/
/*height: 200px;*/
background-color: red;
}
.float{
float:left;
width: 40px;
height: 40px;
background-color: yellow;
}

Description

此时我们发现:没有设置高度的container元素,其实际高度只由标准文档流的box2元素撑起来了21px,而设置了30px高度的浮动元素由于脱离文档流其高度被忽略了。

这就是浮动经典的“高度塌陷”问题了。

2.4 无margin重叠问题

普通的块级元素之间的margin-top和margin-bottom有时会出现margin合并的现象,而浮动元素由于其自身会变成一个BFC(块级格式化上下文),不会影响外部元素,所以不会出现margin重叠问题。

三、清除浮动

清除浮动并不是去掉浮动,而是解决因为浮动带来的副作用的消极影响,也就是我们上面说的父元素高度塌陷问题。

3.1 clear属性

在此之前,我们需要了解另一个CSS属性,就是float的克星——clear

官方对于clear属性的解释是:元素盒子的边不能和前面的浮动元素相邻。其本质在于让当前元素不和前面的float元素在一行显示。

对此我们可以对于clear的属性值形象地理解为:

  • left:元素左边抗浮动

  • right:元素右边抗浮动

  • both:元素两侧抗浮动

注意:由于clear属性只关注当前元素前面的浮动元素,所以使用clear:left/right都是和clear:both等效的。实际上我们只需要用到clear:both即可。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!


3.2 清除方法

当今流行的浮动布局已不再是最初的文字环绕了,而是通过给每个子元素添加浮动来实现元素横向排列(一行占不下就换行继续)的布局。

注意:这种横向排列布局建议最好让每个子元素的高度一致,否则可能会出现以下图这种高度不齐引起的布局问题:

Description

即使如此,依然需要解决父元素高度塌陷问题,以下分别对几种常见解决方案简单说明下:

  • 让父元素也浮动

没有从根本解决问题,反而生成了新的浮动问题。

  • 给父元素设置高度

此法太过死板,让父元素高度固定死了,无法自适应高度。

  • 给父元素设置overflow:hidden

此法原理在于让父元素成为一个BFC,唯一缺点容易导致溢出内容被隐藏掉,不过这种场景较少,还是可以用此方法的。

  • 伪元素与clear属性配合(推荐)
/*对浮动元素的父元素设置*/
.clear::after{
clear: both;
content:'';
/*clear属性只对块元素有效,而伪元素::afer默认是行级*/
display: block;
}

CSS浮动属性是网页设计师的重要工具,但也需要谨慎使用。通过今天的介绍,希望你能够更加自信地在你的设计中运用这一属性,创造出既美观又稳定的网页布局。

在CSS的世界里,每一个属性都有其独特的魅力和规则。浮动属性作为布局的强大工具,虽然有时会带来挑战,但只要我们理解它的本质,就能将它变为实现创意设计的利器。

收起阅读 »

Redis不再 “开源”

Redis 官方今日宣布修改开源协议 —— 未来所有版本都将使用 “源代码可用” 的许可证 (source-available licenses)。具体来说,Redis 将不再遵循 BSD 3-Clause 开源协议进行分发。从 Redis 7.4 版本开始,...
继续阅读 »

Redis 官方今日宣布修改开源协议 —— 未来所有版本都将使用 “源代码可用” 的许可证 (source-available licenses)。


具体来说,Redis 将不再遵循 BSD 3-Clause 开源协议进行分发。从 Redis 7.4 版本开始,Redis 采用 SSPLv1 和 RSALv2 双重许可证。Redis 源代码将通过 Redis 社区版免费提供给开发者、客户和合作伙伴。

SSPL:Server Side Public License

RSAL:Redis Source Available License Redis 产品家族的具体许可证如下:


根据新许可证的条款,托管 Redis 产品的云服务提供商将不再允许免费使用 Redis 的源代码。例如,云服务提供商只有在与 Redis(Redis 代码的维护者)达成许可条款后,才能向用户交付 Redis 7.4。

Redis 官方表示:

实际上,Redis 开发者社区不会发生任何变化,他们将继续拥有双重许可证下的宽松许可。同时,Redis 负责的所有 Redis 客户端库将保持采用开源许可证。 Redis 将继续支持其庞大的合作伙伴生态系统(包括托管服务提供商和系统集成商),并独家访问 Redis 通过其合作伙伴计划开发和提供的所有未来版本、更新和功能。 现有 Redis Enterprise 客户没有变化。 总的来说,对于使用 Redis 开源版本和新版本的 Redis 的最终用户(使用双重许可证进行内部或个人使用),没有任何变化。

对于使用 Redis 构建客户端库或其他集成的集成合作伙伴,同样没有任何变化。 Redis 对这次修改开源协议的举措十分坦诚,他们承认 Redis 不再是 OSI 定义下的“开源”项目。但他们仍是开源理念的支持者,并会继续维护开源项目。


原文:https://mp.weixin.qq.com/s/9_-w6lF7ffiu49WbEORPtQ

收起阅读 »

正则表达式太难写?试试这个可视化工具

在工作中有没有觉得写正则表达式很难,我就一直很头疼。今天我们就介绍一个开源项目,它可以用可视化的方式查看、编辑和测试正则表达式,大大的提升效率,它就是:regex-vis regex-vis是什么 regex-vis是一个辅助学习、编写和验证正则的工具,你输入...
继续阅读 »

在工作中有没有觉得写正则表达式很难,我就一直很头疼。今天我们就介绍一个开源项目,它可以用可视化的方式查看、编辑和测试正则表达式,大大的提升效率,它就是:regex-vis


regex-vis是什么


regex-vis是一个辅助学习、编写和验证正则的工具,你输入一个正则表达式后,会生成它的可视化图形。然后可以点选或框选图形中的单个或多个节点,再在右侧操作面板对其进行操作,具体操作取决于节点的类型,比如在其右侧插入空节点、为节点编组、为节点增加量词等。



安装regex-vis


首先regex-vis提供了一个在线环境,可以直接到regex-vis.com/ 去试用,这是最简单的方式。



当然,作为一个开源项目,另外一种方式就是自己运行啦。按以下步骤:



  • 首先下载代码到本地。

  • 安装依赖:pnpm install

  • 安装完成后运行服务:pnpm start



启动完成后到3000端口访问即可。


这里可能会遇到一些小问题,比如SSL的问题,稍微修改一些运行命令的配置即可解决。


使用 regex-vis


首先我准备一个例子的正则表达式,验证身-份-证的正则:


^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$

可视化


直接把正则表达式贴进去就能看到可视化的效果了。



编辑


在右侧是正则表达式的编辑区,可以在这里修改、编辑正则的内容。



首先是一些图例,点击图形中想要编辑的部分,然后点击中间的编辑就可以进入到编辑页面了。



测试


修改完了正则的内容,想要验证一下写的对不对,那就到测试里去试一试吧。通过的会显示绿色,失败的则显示为红色。



示例


项目还自带了几个示例,如果你 刚一进来,不知道用什么来试用,可以直接打开示例来看看。



设置


本身是个小工具,这里有2个可用的设置,一个是切换语言,可以切成中文显示,另一个就是明/暗显示模式的转换。



总结


作为一个小工具还是挺不错的,对于像我这样不熟练正则的人有所帮助,一步步的编辑可以渐进式的编辑。


当然现在写正则最好的方式是让AI帮忙写,所以我建议可以AI帮忙写,然后通过这个工具来检查一下,通过可是的方式检查和AI的沟通有没有错误。


另外测试正则的功能里,不能显示出事那部分 正则出错略有可惜,如果能增强就更好用了。


项目信息



作者:IT咖啡馆
来源:juejin.cn/post/7350683679297290294
收起阅读 »

趣解适配器模式之《买了苹果笔记本的尴尬》

〇、小故事 小王考上了理想的大学,为了更好的迎接大学生活,他决定买一台苹果的笔记本电脑犒赏自己。 电脑很快买好了,用起来也非常的流畅,但是,当他想要插U盘传资料的时候,尴尬的事情来了,这台电脑两侧的插口非常少,只有1个耳机插孔和2个雷电插孔,根本没有USB插...
继续阅读 »

〇、小故事


小王考上了理想的大学,为了更好的迎接大学生活,他决定买一台苹果的笔记本电脑犒赏自己。



电脑很快买好了,用起来也非常的流畅,但是,当他想要插U盘传资料的时候,尴尬的事情来了,这台电脑两侧的插口非常少,只有1个耳机插孔2个雷电插孔根本没有USB插口!这咋办呀?



他赶快咨询了他的哥哥,他哥哥告诉他,去买一个扩展坞就可以了,然后他上网一看,原来买一个扩展坞之后,无论是U盘还是连接显示器的HDMI都可以连接啦!!他开心极了,本来要遗憾退掉这台心爱的苹果笔记本电脑,这回也不用退啦!



以上这个小故事,相信很多使用过苹果笔记本的同学们都会遇到,大多也都会购买这种扩展坞,那么,这种扩展坞其实就是适配器模式的一个具体实现例子了。那么,言归正传,我们来正式了解一下这个设计模式——适配器模式


一、模式定义


适配器模式定义:



该模式将一个类的接口,转换成客户期望的另一个接口。适配器模式让原本接口不兼容的类可以合作无间。



为了进一步加深该模式的理解,我们再举一个研发过程中会遇到的例子:



此时我们维护了一个员工管理系统,然后接入我们系统的第三方系统,我们都要求对方遵守我们的接口规范去开发,比如:提供方法名为queryAllUser()的方法等等。但是,这次接入的系统已经有类似功能了,他们不希望因为两个系统的接入而重新开发新的接口,那么这对这种情况,我们就可以采用适配器模式,将接口做中间层的适配转换。



如图下图所示:



二、模式类图


通过上面的介绍,相信大家对适配器模式也有了一定的了解了。那么,下面我们就来看一下如果要实现适配器模式,我们的类图应该是怎么样的。


首先,我们要说明两个重要的概念:AdapterAdaptee,其含义分别是适配器待适配的类。我们就是通过实现Target接口创建Adapter类,然后在具体的方法内部来通过调用Adaptee方法来实现具体的业务逻辑。具体类图如下所示:



三、代码实现


首先创建目标类接口——Target


public interface Target {
void prepare();
void execute();
}

实现Target接口,创建具体实现类——NormalTarget


public class NormalTarget implements Target {
public void prepare() {
System.out.println("NormalTarget prepare()");
}
public void execute() {
System.out.println("NormalTarget execute()");
}
}

创建待适配的类Adaptee,用于后续适配器对其进行适配工作:


public class Adaptee {
public void prepare1() {
System.out.println("Adaptee prepare1()");
}
public void prepare2() {
System.out.println("Adaptee prepare2()");
}
public void prepare3() {
System.out.println("Adaptee prepare3()");
}
public void doingSomething() {
System.out.println("Adaptee doingSomething()");
}
}

创建适配器Adapter,由于要适配目标对象Target,所以需要实现Target接口:


public class Adapter implements Target {
// 待适配的类
private Adaptee adaptee;

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

public void prepare() {
adaptee.prepare1();
adaptee.prepare2();
adaptee.prepare3();
}

public void execute() {
adaptee.doingSomething();
}
}

创建客户端Client,用于操作Target目标对象执行某些业务逻辑:


public class Client {
Target target;
public void work() {
target.prepare();
target.execute();
}
public void setTarget(Target target) {
this.target = target;
}
}

创建测试类AdapterTest,使得Client操作NormalTarget和Adaptee:


public class AdapterTest {
public static void main(String[] args) {
Client client = new Client();

System.out.println("------------NormalTarget------------");
client.setTarget(new NormalTarget());
client.work();

System.out.println("------------Adaptee------------");
client.setTarget(new Adapter(new Adaptee())); // 适配器转换
client.work();
}
}

通过输出结果我们可以看到,适配器运行正常:


------------NormalTarget------------
NormalTarget prepare()
NormalTarget execute()
------------Adaptee------------
Adaptee prepare1()
Adaptee prepare2()
Adaptee prepare3()
Adaptee doingSomething()

今天的文章内容就这些了:



写作不易,笔者几个小时甚至数天完成的一篇文章,只愿换来您几秒钟的 点赞 & 分享



更多技术干货,欢迎大家关注公众号“爪哇缪斯” ~ \(^o^)/ ~ 「干货分享,每天更新」


作者:爪哇缪斯
来源:juejin.cn/post/7273125596951298060
收起阅读 »

我不敢把水烧的太开

我不敢把水烧开,那样家里会有两个沸物。我也不敢把房间打扫的很干净,我怕太干净,垃圾只剩下我。我常坐在窗口看着窗外发呆,这悲伤来的没有油头。床上有两个枕头,一个是我的,另一个也是我的,因为我裂开了。我也经常熬夜,因为天冷了,时间就不说了。白天面对的是生活,夜晚面...
继续阅读 »

我不敢把水烧开,那样家里会有两个沸物。我也不敢把房间打扫的很干净,我怕太干净,垃圾只剩下我。我常坐在窗口看着窗外发呆,这悲伤来的没有油头。床上有两个枕头,一个是我的,另一个也是我的,因为我裂开了。我也经常熬夜,因为天冷了,时间就不说了。白天面对的是生活,夜晚面对的是灵魂,一半的时间来生存,剩下的时间拿来滋养灵魂!我熬的不是夜,是我短暂的自由。青春献给了梦想,梦想败给了现实。小时候无忧无虑,倒头就睡,长大后忙于生计,失眠怎么常态。背井离乡的颠沛流离,不过是为了碎银几两。习惯了一个人的独处,也慢慢接受了自己的平庸。我们不是故事里的主角,也没有主角光环!我们只是茫茫人海中的渺小一个。不曾坐过飞机,也没喝过星巴克,更没有用过奢侈品。小时候的自由人,长大后的笼中鸟。以前叫醒你的是父母,现在叫醒你的是生活。我们变得好像是机器人,每天起床的意义就是重复的工作。 我似乎感受到那袭来的风划过,渴望度图欣赏,但更吹醒了我的意识,要继续生存下去,夜深人静是自由醒来清晨是生活,朋友人间自由身,却非人间自由人,看似自由自在,实则身不由己。


我不敢轻易点燃热情,以免世界再添两个燃烧的灵魂。我也不敢让心扉过于透彻明亮,只怕纯净得只剩下自我孤独。我常常倚在窗前凝望星空,那份寂寥悄无声息地蔓延。床榻上有两个梦乡,一个属于清醒时的我,另一个属于深夜里破碎的我。我时常与黑夜为伴,仿佛只有此刻,才握住了片刻的宁静。白昼应对的是繁杂尘世,夜晚则是对内心世界的对话,一半的生命为了生存而奋斗,另一半则用来沉淀和丰盈心灵。我熬的不只是夜,更是那一份难能可贵的自我空间。青春曾向理想倾注所有,而理想最终却在现实中折戟沉沙。儿时懵懂无知,酣然入梦,成人后疲于奔命,反倒是失眠成了常态。远离故土、四海为家,无非是为了那些散落各处的铜板。渐渐习惯独自行走,也逐渐接纳了自己的平凡无奇。我们并非小说里的主人公,身上也没有主角光环庇佑,我们只是芸芸众生中的一员。未曾体验云端飞翔,亦未尝品鉴星巴克的香醇,更谈不上拥有奢侈品牌的光环。曾经那个随性率真的孩子,如今已变成囚禁在生活牢笼中的飞鸟。过去的黎明由父母唤醒,现在的黎明却是生活的催促。我们如同被设定程序的机器,日复一日只是为了重复的工作奔波。


我恍若感受到那阵疾风吹过,虽有心去追逐欣赏,但它更像是一记警钟,提醒我要坚韧地活下去,深夜的清醒是对自由的领悟,破晓的苏醒则是对生活的担当,身边的朋友皆似世间自由行者,却非人人能真正活得洒脱。表面看似悠然自得,实则都身陷无形的生活枷锁之中。


作者:Young_
来源:mdnice.com/writing/ac9b73a2a7f84f1eb843a6c404a82701
收起阅读 »

深入研究Kotlin运行时的泛型

深入研究Kotlin运行时的泛型 通过前面的学习,对Kotlin的泛型已经有了比较全面的了解了,泛型的目的是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那就是泛型的类型擦除,今天就来深入地学习一下运行时的泛型,彻底的弄懂类型...
继续阅读 »

深入研究Kotlin运行时的泛型


通过前面的学习,对Kotlin的泛型已经有了比较全面的了解了,泛型的目的是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那就是泛型的类型擦除,今天就来深入地学习一下运行时的泛型,彻底的弄懂类型擦除的前因后果,并学会如何在运行时做类型检查和类型转换,以期完成拼图掌握泛型,写出类型安全的通用代码。





关于泛型话题的一系列文章:



泛型类型擦除(Type erasure)


泛型的类型安全性(包括类型检查type check,和类型转换type casting)都是由编译器在编译时做的,为了保持在JVM上的兼容性,编译器在保障完类型安全性后会对泛型类型进行擦除(Type erasure)。在运行时泛型类型的实例并不包含其类型信息,也就是说它不知道具体的类型参数,比如Foo和Foo都被擦除成了Foo<*>,在虚拟机(JVM)来看,它们的类型是一样的。


因为泛型Foo的类型参数T会被擦除(erased),所以与类型参数相关的类型操作(类型检查is T和类型转换as T)都是不允许的。


可行的类型检查和转换


虽然类型参数会被擦除,但并不是说对泛型完全不能进行类型操作。


星号类型操作


因为所有泛型会被擦除成为星号无界通配Foo<*>,它相当于Foo,是所有Foo泛型的基类,类型参数Any?是根基类,所以可以进行类型检查和类型转换:


if (something is List<*>) {
 something.forEach { println(it) } // 元素被视为Any?类型
}

针对星号通配做类型操作,类型参数会被视为Any?。但其实这种类型操作没有任何意义,毕竟Any是根基类,任何类当成Any都是没有问题的。


完全已知具体的类型参数时


另外一种情况就是,整个方法的上下文中已经完全知道了具体的类型参数时,不涉及泛型类型时,也是可以进行类型操作的,说的比较绕,我们来看一个🌰:


fun handleStrings(list: MutableList<String) {
 if (list is ArrayList) {
  // list is smart-cast to ArrayList
 }
}

这个方法并不涉及泛型类型,已经知道了具体的类型参数是String,所以类型操作也是可行的,因为编译器知道具体的类型,能对类型进行检查 保证是类型安全的。并且因为具体类型参数String可以推断出来,所以是可以省略的。


未检查的转换


当编译器能推断出具体的类型时,进行类型转换就是安全的,这就是被检查的转型(checked cast),如上面的🌰。


如果无法推断出类型时,比如涉及泛型类型T时,因为类型会被擦除,编译器不知道具体的类型,这时as T或者as List都是不安全的,编译器会报错,这就是未检查转型(unchecked cast)。


但如果能确信是类型转换是安全的,可以用注解@Suppress("UNCHECKED_CAST")来忽略。


用关键reified修饰inline泛型函数


要想能够对泛型类型参数T做类型操作,只能是在用关键字reified修饰了的inline泛型函数,在这种函数体内可以对泛型类型参数T做类型操作,🌰如:


inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair = "items" to listOf(123)


val stringToSomething = somePair.asPairOf()
val stringToInt = somePair.asPairOfInt>()

需要注意的是关键字reified能够让针对类型参数T的操作得到编译器的检查,保证安全,是允许的。但是对于泛型仍是不允许的,🌰如:


inline fun <reified T> List<*>.asListOfType(): List? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List else
        null

这个inline泛型函数用关键字reified修饰了,因此针对类型参数T是允许类型检查类型转换,如第2行是允许的。但泛型仍是不合法,如第4行,这时可以用上一小节提到的注解@Suppress("UNCHECKED_CAST")来忽略未检查类型转换。


inline和reified的原理


对于一些泛型工厂方法,就非常适合使用inline和reified,以保证转换为类型参数(因为工厂方法最终肯定要as T)是允许的且是安全的:


inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)

class User {
    private val log = logger()
    // ...
}

关键字reified其实也没有什么神秘的,因为这是inline函数,这种函数是会把函数体嵌入到任何调用它的地方(call site),而每个调用泛型函数的地方必然会有明确的具体类型参数,那么编译器就知道了具体的类型能保证类型安全(checked cast)。上面的工厂方法在调用时就会大概变成酱紫:


class User {
 private val log = LoggerFactory.getLogger(User.class.java)
}

这时其实在函数体内已经知道了具体的类型参数User,编译器能够进行类型检查,所以是安全的。


总结


本文深入的讨论一下运行时泛型的一些特性,泛型类型在运行时会被擦除,无法做泛型相关的类型操作,因为编译器无法保证其类型安全。例外就是在用reified修饰的inline函数中可以对类型参数T做类型操作,但泛型类型(带尖括号的)仍是会被擦除,可以用注解@Suppress("UNCHECKED_CAST")来忽略unchecked cast。


参考资料



作者:稀有猿诉
来源:toughcoder.net/blog/2024/03/16/deep-dive-int0-kotlin-generics-runtime
收起阅读 »

《健听女孩》——一部感人的电影

故事 《健听女孩》的主人公是一个听力正常爱唱歌的女孩鲁比。这里为什么强调听力正常呢?因为她的父母和哥哥都是聋哑人,家里只有她自己一个人具有听力。这个家庭以捕鱼为生,渔船通知响应,渔获交易,与人沟通等事情都是由她进行翻译来进行的。这个家庭很需要她。 她有去伯...
继续阅读 »

故事


《健听女孩》的主人公是一个听力正常爱唱歌的女孩鲁比。这里为什么强调听力正常呢?因为她的父母和哥哥都是聋哑人,家里只有她自己一个人具有听力。这个家庭以捕鱼为生,渔船通知响应,渔获交易,与人沟通等事情都是由她进行翻译来进行的。这个家庭很需要她。


她有去伯克利音乐学院面试的机会,这是她的梦想,本片就是主要讲述了她在梦想与家庭进行抉择的过程和家庭成员的看法、处理方式等。


被“嘲笑”


鲁比出生在聋哑家庭,可想而知,她的家人没有办法教她讲话,她的发声很难听,这遭到了她的同学们的嘲笑、欺凌。她每天凌晨三点便起床与家人一起捕鱼,有时会来不及换衣服,同学也会嫌弃她身上的鱼腥味,进行讥讽。


种种这些,让她幼小的心灵受到了很大打击,所以她害怕面对她们,同时也造成了她的自卑、敏感。


师生情


在报课程的时候,她选择了合唱课,因为她热爱唱歌。但是第一节课,老师让大家开唱的时候,她却逃避得跑出去了。后来和老师讲明因为自己得经历,不敢在同学面前唱歌,害怕被嘲笑。老师给了她鼓励,让她有了自信。


老师发现她的天赋,推荐她去伯克利面试,同时教她唱歌。


在家庭不同意她去伯克利学院时,对她进行惋惜。


友情和爱情


鲁比得闺蜜是她参加合唱团前唯一的好友,闺蜜帮她分担心事,支持她唱歌。


参加合唱团后,她又有了一个男性朋友,是她的二重唱的搭档,当然也发生了一些小插曲。这位朋友家庭也存在一些问题,他的爸妈关系不和,他选择唱歌也并不是自己本意。后来她俩互相了解,互谈心事。 成为了男女朋友。


亲情


这部电影主要还是讲解亲情的。


在得知鲁比想去读大学时,她的妈妈特别反对,她说,“她不能离开我,她是我的宝宝”,爸爸说,“可是她从来没有当过宝宝”。是啊,她从小就帮家里进行翻译,是这个家与外界沟通的桥梁。她这么小就承担了这么多。她的爸爸希望她去,但是这个家庭没有她是没办法运作的。她的哥哥在看到父母把鲁比当成最重要的沟通媒介时,他心事重重,他认为他是哥哥,这些事情他也是能做好的。


虽说她的妈妈开始不支持唱歌,但是在她要进行表演的时候还是送了她一条红色裙子。她俩谈心过程中,妈妈在生孩子时希望鲁比是聋哑人,因为妈妈怕如果是正常儿童的话,自己的聋哑会成为一个“坏妈妈”。 在闺蜜和鲁比哥哥讲述了鲁比唱歌方面的天赋后,他很希望她去参加面试。


她还是没有选择去面试。这是她想成全家庭,因为她知道家庭对她的需要。


一家人一起去看了她的表演,看着台下人的感动,鼓掌,落泪等行为,爸爸心事重重,回到家后爸爸让她为她又唱了一遍,在星空下,他摸着她的振动,感受歌曲。


后来一家人一起去参加了伯克利的面试。这是家庭成员对鲁比的成全。


总结


这是一部很感人的电影,故事的讲述行云流水,每一个转折恰到好处。能够让我们聆听到悦耳的声音。


作者:随机的未知
来源:mdnice.com/writing/4a81248164b4409f8be55c27575fbf3b
收起阅读 »

Android:优雅的处理首页弹框逻辑:责任链模式

背景 随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。 并且弹框显示还有要求,比如: 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗...
继续阅读 »

背景


随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。
并且弹框显示还有要求,比如:



  • 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框

  • 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗高,所以希望优先级高的优先显示

  • 广告弹框只展示一次

  • 等等


如何优雅的处理这个逻辑呢?请出我们的主角:责任链模式。


责任链模式


举个栗子🌰


一位男性在结婚之前有事要和父母请示,结婚之后要请示妻子,老了之后就要和孩子们商量。作为决策者的父母、妻子或孩子,只有两种选择:要不承担起责任来,允许或不允许相应的请求; 要不就让他请示下一个人,下面来看如何通过程序来实现整个流程。


先看一下类图:
未命名文件.png
类图非常简单,IHandler上三个决策对象的接口。


//决策对象的接口
public interface IHandler {
//处理请求
void HandleMessage(IMan man);
}

//决策对象:父母
public class Parent implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("孩子向父母的请求是:" + man.getRequest());
System.out.println("父母的回答是:同意");
}
}

//决策对象:妻子
public class Wife implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("丈夫向妻子的请求是:" + man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

//决策对象:孩子
public class Children implements IHandler{
@Override
public void HandleMessage(IMan man) {
System.out.println("父亲向孩子的请求是:" + man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

IMan上男性的接口:


public interface IMan {
int getType(); //获取个人状况
String getRequest(); //获取个人请示(这里就简单的用String)
}

//具体男性对象
public class Man implements IMan {
/**
* 通过一个int类型去描述男性的个人状况
* 0--幼年
* 1--成年
* 2--年迈
*/

private int mType = 0;
//请求
private String mRequest = "";

public Man(int type, String request) {
this.mType = type;
this.mRequest = request;
}

@Override
public int getType() {
return mType;
}

@Override
public String getRequest() {
return mRequest;
}
}

最后我们看下一下场景类:


public class Client {
public static void main(String[] args) {
//随机生成几个man
Random random = new Random();
ArrayList<IMan> manList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
manList.add(new Man(random.nextInt(3), "5块零花钱"));
}
//定义三个请示对象
IHandler parent = new Parent();
IHandler wife = new Wife();
IHandler children = new Children();
//处理请求
for (IMan man: manList) {
switch (man.getType()) {
case 0:
System.out.println("--------孩子向父母发起请求-------");
parent.HandleMessage(man);
break;
case 1:
System.out.println("--------丈夫向妻子发起请求-------");
wife.HandleMessage(man);
break;
case 2:
System.out.println("--------父亲向孩子发起请求-------");
children.HandleMessage(man);
break;
default:
break;
}
}
}
}

首先是通过随机方法产生了5个男性的对象,然后看他们是如何就要5块零花钱这件事去请示的,运行结果如下所示:


--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------父亲向孩子发起请求-------
父亲向孩子的请求是:5块零花钱
孩子的回答是:同意
--------孩子向父母发起请求-------
孩子向父母的请求是:5块零花钱
父母的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意


发没发现上述的代码是不是有点不舒服,有点别扭,有点想重构它的感觉?那就对了!这段代码有以下几个问题:



  • 职责界定不清晰



对孩子提出的请示,应该在父母类中做出决定,父母有责任、有义务处理孩子的请示,



因此Parent类应该是知道孩子的请求自己处理,而不是在Client类中进行组装出来,
也就是说 原本应该是父亲这个类做的事情抛给了其他类进行处理,不应该是这样的。



  • 代码臃肿



我们在Client类中写了if...else的判断条件,而且能随着能处理该类型的请示人员越多,
if...else的判断就越多,想想看,臃肿的条件判断还怎么有可读性?!




  • 耦合过重



这是什么意思呢,我们要根据Man的type来决定使用IHandler的那个实现类来处理请



求。有一个问题是:如果IHandler的实现类继续扩展怎么办?修改Client类?
与开闭原则违背了!【开闭原则:软件实体如类,模块和函数应该对扩展开放,对修改关闭】
http://www.jianshu.com/p/05196fac1…



  • 异常情况欠考虑



丈夫只能向妻子请示吗?丈夫向自己的父母请示了,父母应该做何处理?
我们的程序上可没有体现出来,逻辑失败了!


既然有这么多的问题,那我们要想办法来解决这些问题,我们先来分析一下需求,男性提出一个请示,必然要获得一个答复,甭管是同意还是不同意,总之是要一个答复的,而且这个答复是唯一的,不能说是父母作出一个决断,而妻子也作出了一个决断,也即是请示传递出去,必然有一个唯一的处理人给出唯一的答复,OK,分析完毕,收工,重新设计,我们可以抽象成这样一个结构,男性的请求先发送到父亲,父母一看是自己要处理的,就作出回应处理,如果男性已经结婚了,那就要把这个请求转发到妻子来处理,如果男性已经年迈,那就由孩子来处理这个请求,类似于如图所示的顺序处理图。
未命名文件 (1).png
父母、妻子、孩子每个节点有两个选择:要么承担责任,做出回应;要么把请求转发到后序环节。结构分析得已经很清楚了,那我们看怎么来实现这个功能,类图重新修正,如图 :
未命名文件 (2).png
从类图上看,三个实现类Parent、Wife、Children只要实现构造函数和父类中的抽象方法 response就可以了,具体由谁处理男性提出的请求,都已经转移到了Handler抽象类中,我们 来看Handler怎么实现,


public abstract class Handler {
//处理级别
public static final int PARENT_LEVEL_REQUEST = 0; //父母级别
public static final int WIFE_LEVEL_REQUEST = 1; //妻子级别
public static final int CHILDREN_LEVEL_REQUEST = 2;//孩子级别

private Handler mNextHandler;//下一个责任人

protected abstract int getHandleLevel();//具体责任人的处理级别

protected abstract void response(IMan man);//具体责任人给出的回应

public final void HandleMessage(IMan man) {
if (man.getType() == getHandleLevel()) {
response(man);//当前责任人可以处理
} else {
//当前责任人不能处理,如果有后续处理人,将请求往后传递
if (mNextHandler != null) {
mNextHandler.HandleMessage(man);
} else {
System.out.println("-----没有人可以请示了,不同意该请求-----");
}
}
}

public void setNext(Handler next) {
this.mNextHandler = next;
}
}

再看一下具体责任人的实现:Parent、Wife、Children


public class Parent extends Handler{

@Override
protected int getHandleLevel() {
return Handler.PARENT_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------孩子向父母提出请示----------");
System.out.println(man.getRequest());
System.out.println("父母的回答是:同意");
}
}

public class Wife extends Handler{
@Override
protected int getHandleLevel() {
return Handler.WIFE_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------丈夫向妻子提出请示----------");
System.out.println(man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

public class Children extends Handler{
@Override
protected int getHandleLevel() {
return Handler.CHILDREN_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------父亲向孩子提出请示----------");
System.out.println(man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

那么再看一下场景复现:
在Client中设置请求的传递顺序,先向父母请示,不是父母应该解决的问题,则由父母传递到妻子类解决,若不是妻子类解决的问题则传递到孩子类解决,最终的结果必然有一个返回,其运行结果如下所示。


----------孩子向父母提出请示----------
15块零花钱
父母的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意

结果也正确,业务调用类Client也不用去做判断到底是需要谁去处理,而且Handler抽象类的子类可以继续增加下去,只需要扩展传递链而已,调用类可以不用了解变化过程,甚至是谁在处理这个请求都不用知道。在这种模式就是责任链模式


定义


Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.
(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。) 责任链模式的重点是在“链”上,由一条链去处理相似的请求在链中决定谁来处理这个请 求,并返回相应的结果,其通用类图如图所示
未命名文件 (3).png
最后总结一下,责任链的模版:
包含四个对象,Handler,Request,Level,Response:


public class Request {
//请求的等级
public Level getRequestLevel(){
return null;
}
}

public class Level {
//请求级别
}


public class Response {
//处理者返回的数据
}

//抽象处理者
public abstract class Handler {
private Handler mNextHandler;

//每个处理者都必须对请求做出处理
public final Response handleMessage(Request request) {
Response response = null;
if (getHandlerLevel().equals(request.getRequestLevel())) {
//是自己处理的级别,自己处理
response = echo(request);
} else {
//不是自己处理的级别,交给下一个处理者
if (mNextHandler != null) {
response = mNextHandler.echo(request);
} else {
//没有处理者能处理,业务自行处理
}
}
return response;
}

public void setNext(Handler next) {
this.mNextHandler = next;
}

@NotNull
protected abstract Level getHandlerLevel();

protected abstract Response echo(Request request);
}

实际应用


我们回到开篇的问题:如何设计弹框的责任链?


//抽象处理者
abstract class AbsDialog(private val context: Context) {
private var nextDialog: AbsDialog? = null

//优先级
abstract fun getPriority(): Int

//是否需要展示
abstract fun needShownDialog(): Boolean

fun setNextDialog(dialog: AbsDialog?) {
nextDialog = dialog
}

open fun showDialog() {
//这里的逻辑,我们就简单点,具体逻辑根据业务而定
if (needShownDialog()) {
show()
} else {
nextDialog?.showDialog()
}
}

protected abstract fun show()

// Sp存储, 记录是否已经展示过
open fun needShow(key: String): Boolean {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
return sp.getBoolean(key, true)
}

open fun setShown(key: String, show: Boolean) {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
sp.edit().putBoolean(key, !show).apply()
}

companion object {
const val LOG_TAG = "Dialog"
const val SP_NAME = "dialog"
const val POLICY_DIALOG_KEY = "policy_dialog"
const val AD_DIALOG_KEY = "ad_dialog"
const val PRAISE_DIALOG_KEY = "praise_dialog"
}
}

/**
* 模拟 隐私政策弹窗
* */

class PolicyDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 0

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如接口控制等等
// 这里通过Sp存储来模拟
return needShow(POLICY_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示隐私政策弹窗")
setShown(POLICY_DIALOG_KEY, true) //记录已经显示过
}
}

/**
* 模拟 广告弹窗
* */

class AdDialog(private val context: Context) : AbsDialog(context) {
private val ad = DialogData(1, "XX广告弹窗") // 模拟广告数据

override fun getPriority(): Int = 1

override fun needShownDialog(): Boolean {
// 广告数据通过接口获取,广告id应该是唯一的,所以根据id保持sp
return needShow(AD_DIALOG_KEY + ad.id)
}

override fun show() {
Log.d(LOG_TAG, "显示广告弹窗:${ad.name}")
setShown(AD_DIALOG_KEY + ad.id, true)
}
}

/**
* 模拟 好评弹窗
* */

class PraiseDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 2

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如用户使用7天等
// 这里通过Sp存储来模拟
return needShow(PRAISE_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示好评弹窗")
setShown(PRAISE_DIALOG_KEY, true)
}
}

//模拟打开app
val dialogs = mutableListOf<AbsDialog>()
dialogs.add(PolicyDialog(this))
dialogs.add(PraiseDialog(this))
dialogs.add(AdDialog(this))
//根据优先级排序
dialogs.sortBy { it.getPriority() }
//创建链条
for (i in 0 until dialogs.size - 1) {
dialogs[i].setNextDialog(dialogs[i + 1])
}
dialogs[0].showDialog()

第一次打开
image.png


第二次打开
image.png


第三次打开
image.png


总结:



  • 优点


责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。



  • 缺点


责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长, 环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。



  • 注意事项


链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。


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

腾讯女后端设计了一套短链系统,当场就想给她offer!

你好,我是猿java 如上图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”,就能跳转到一个网页。其实,背后的秘密就是一套完整的短链系统,今天我们就来看看字节的后端女生是如何设计的? 上图中那串蓝色字符,有个专业的术语叫做“短链”,它可以是一...
继续阅读 »

你好,我是猿java


image.png


如上图,对于这种客评短信,相信大家并不陌生,通过点击短信里“蓝色字体”,就能跳转到一个网页。其实,背后的秘密就是一套完整的短链系统,今天我们就来看看字节的后端女生是如何设计的?


上图中那串蓝色字符,有个专业的术语叫做“短链”,它可以是一个链接地址,也可以设计成二维码。


为什么要用短链?


存在既合理,这里列举 3个主要原因。


1.相对安全


短链不容易暴露访问参数,生成方式可以完全迎合短信平台的规则,能够有效地规避关键词、域名屏蔽等风险,而原始 URL地址,很可能因为包含特殊字符被短信系统误判,导致链接无法跳转。


2.美观


对于精简的文字,似乎更符合美学观念,不太让人产生反感。


3.平台限制


短信发送平台有字数限制,在整条短信字数不变的前提下,把链接缩短,其他部分的文字描述就能增加,这样似乎更能达到该短信的实际目的(比如,营销)。


短链的组成


如下图,短链的组成通常包含两个部分:域名 + 随机码


image.png


短链的域名最好和其他业务域名分开,而且要尽量简短,可以不具备业务含义(比如:xyz.com),因为短链大部分是用于营销,可能会被三方平台屏蔽。


短链的随机码需要全局唯一,建议 10位以下。


短链跳转的原理


首先,我们先看一个短链跳转的简单例子,如下代码,定义了一个 302重定向的代码示例:


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.view.RedirectView;

@Controller
public class RedirectController {

@GetMapping("/{shortCode}")
public RedirectView redirect(@PathVariable String shortCode) {
String destUrl = "https://yuanjava.com";
// destUrl = getDestUrlByShortCode(shortCode); //真实的业务逻辑
return new RedirectView(destUrl);
}
}

接着,在浏览器访问短链”http://127.0.0.1:8080/s2TYdWd” 后,请求会被重定向到 yuanjava.com ,下图为浏览器控制台信息:


image.png


从上图,我们看到了 302状态码并且请求被 Location到另外一个 URL,整个交互流程图如下:


image.png


是不是有一种偷梁换柱的感觉???


最后,总结下短链跳转的核心思想:


生成随机码,将随机码和目标 URL(长链)的映射关系存入数据库;


用域名+随机码生成短链,并推送给目标用户;


当用户点击短链后,请求会先到达短链系统,短链系统根据随机码查找出对应的目标 URL,接着将请求 302重定向到目标 URL(长链);


关于重定向有 301 和 302两种,如何选择?



  • 302,代表临时重定向:每次请求短链,请求都会先到达短链系统,然后重定向到目标 URL(长链),这样,方便短链系统做一些统计点击数等操作;通常采用 302

  • 301,代表永久重定向:第一次请求拿到目标长链接后,下次再次请求短链,请求不会到达短链系统,而是直接跳转到浏览器缓存的目标 URL(长链),短链系统只能统计到第一次访问的数据;一般不采用 301。


如何生成短链?


从短链组成章节可以知道短链=域名+随机码,随意如何生成短链的问题转换成了如何生成一个随机码,而且这个随机码需要全局唯一。通常会有 3种做法:


Base62


Base62 表示法是一种基数为62的数制系统,包含26个英文大写字母(A-Z),26个英文小写字母(a-z)和10个数字(0-9)。这样,共有62个字符可以用来表示数值。 如下代码:


import java.security.SecureRandom;

public class RandomCodeGenerator {
private static final String CHAR_62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final SecureRandom random = new SecureRandom();

public static String generateRandomCode(int length) {
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int rndCharAt = random.nextInt(CHAR_62.length());
char rndChar = CHAR_62.charAt(rndCharAt);
sb.append(rndChar);
}
return sb.toString();
}
}

对于 Base62算法,如果是生成 6位随机数有 62^6 - 1 = 56800235583, 568亿多,如果是生成 7位随机数有 62^7 - 1 = 3521614606208,合计3.5万亿多,足够使用。


Hash算法


Hash算法算法是我们最容易想到的办法,比如 MD5, SHA-1, SHA-256, MurmurHash, 但是这种算法生成的 Hash算法值还是比较长,常用的做法是把这个 Hash算法值进行 62/64进行压缩。


如下代码,通过 Google的 MurmurHash算法把长链 Hash成一个 32位的 10进制正数,然后再转换成62进制(压缩),这样就可以得到一个 6位随机数,


import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.nio.charset.StandardCharsets;

public class MurmurHashToBase62 {

private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static String toBase62(int value) {
StringBuilder sb = new StringBuilder();
while (value > 0) {
sb.insert(0, BASE62.charAt(value % 62));
value /= 62;
}
return sb.toString();
}
public static void main(String[] args) {
// 长链
String input = "https://yuanjava.cnposts/short-link-system/design?code=xsd&page=1";
// 长链利用 MurmurHash算法生成 32位 10进制数
HashFunction hashFunction = Hashing.murmur3_32();
int hash = hashFunction.hashString(input, StandardCharsets.UTF_8).asInt();
if (hash < 0) {
hash = hash & 0x7fffffff; // Convert to positive by dropping the sign bit
}
// 将 32位 10进制数 转换成 62进制
String base62Hash = toBase62(hash);
System.out.println("base62Hash:" + base62Hash);
}
}

全局唯一 ID


比如,很多大中型公司都会有自己全局唯一 ID 的生成服务器,可以使用这些服务器生成的 ID来保证全局唯一,也可以使用雪花算法生成全局唯一的ID,再经过 62/64进制压缩。


如何解决冲突


对于上述3种方法的前 2种:base62 或者 hash,因为都是哈希函数,所以,不可避免地会产生哈希冲突(尽管概率很低),该怎么解决呢?


要解决冲突,首先要检测冲突,通常来说有 3种检测方法。


数据库索


如下,这里以 MySQL数据库为例(也可以保存在 Redis中),表结构如下:


CREATE TABLE `short_url_map` (   
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`long_url` varchar(160) DEFAULT NULL COMMENT '长链',
`short_url` varchar(10) DEFAULT NULL COMMENT '短链',
`gmt_create` int(11) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE INDEX 'short_url' ('short_url')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

首先创建一张长链和短链的关系映射表,然后通过给 short_url字段添加唯一锁,这样,当数据插入时,如果存在 Hash冲突(short_url值相等),数据库就会抛错,插入失败,因此,可以在业务代码里捕获对应的错误,这样就能检测出冲突。


也可以先用 short_url去查询,如果能查到数据,说明 short_url存在 Hash冲突了。


对于这种通过查询数据库或者依赖于数据库唯一锁的机制,因为都涉及DB操作,所以对数据库是一个开销,如果流量比较大的话,需要保证数据库的性能。


布隆过滤器过滤器


在 DB操作的上游增加一个布隆过滤器,在长链生成短链后, 先用短链在布隆过滤器中进行查找,如果存在就代表冲突了,如果不存在,说明 DB里不存在此短链,可以插入。 对于布隆过滤器的选择,单机可以采用 Google的布隆过滤器,分布式可以使用 RedisBloom。


整体流程可以抽象成下图:


image.png


检测出了冲突,需要如何解决冲突?


再 Hash,可以在长链后面拼接一个 UUID之类的随机字符串,然后再次进行 Hash,用得出的新值再进行上述检测,这样 Hash冲突的概率又大大大的降低了。


高并发场景


在流量不大的情况,上述方法怎么折腾似乎都没有问题,但是,为了架构的健壮性,很多时候需要考虑高并发,大流量的场景,因此架构需要支持水平扩展,比如:



  • 采用微服务

  • 功能模块分离,比如,短链生成服务和长链查询服务分离

  • 功能模块需要支持水平扩容,比如:短链生成服务和长链查询服务能支持动态扩容

  • 缓解数据库压力,比如,分区,分库分表,主从,读写分离等机制

  • 服务的限流,自保机制

  • 完善的监控和预警机制


这里给出一套比较完整的设计思路图:


image.png


总结


本文通过一个客服评价的短信开始,分析了短链的构成,短链跳转的原理,同时也给出了业内的一些实现算法,以及一些架构上的建议。


对于业务体量小的公司,可以根据成本来搭建服务(单机或者少量服务器做负载),对于业务体量比较大的公司,更多需要考虑到高并发的场景,如何保证服务的稳定性,如何支持水平扩展,当服务出现问题时如何具备一套完善的监控和预警服务器。


其实,很多系统都是在一次又一次的业务流量挑战下成长起来的,我们需要不断打磨自己宏观看架构,微观看代码的能力,这样自己也就跟着业务,系统一起成长起来了。


作者:猿java
来源:juejin.cn/post/7350585600858898484
收起阅读 »