注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Spring 实现 3 种异步流式接口,干掉接口超时烦恼

大家好,我是小富~ 如何处理比较耗时的接口? 这题我熟,直接上异步接口,使用 Callable、WebAsyncTask 和 DeferredResult、CompletableFuture等均可实现。 但这些方法有局限性,处理结果仅返回单个值。在某些场景下,...
继续阅读 »

大家好,我是小富~


如何处理比较耗时的接口?


这题我熟,直接上异步接口,使用 CallableWebAsyncTaskDeferredResultCompletableFuture等均可实现。


但这些方法有局限性,处理结果仅返回单个值。在某些场景下,如果需要接口异步处理的同时,还持续不断地向客户端响应处理结果,这些方法就不够看了。


Spring 框架提供了多种工具支持异步流式接口,如 ResponseBodyEmitterSseEmitterStreamingResponseBody。这些工具的用法简单,接口中直接返回相应的对象或泛型响应实体 ResponseEntity<xxxx>,如此这些接口就是异步的,且执行耗时操作亦不会阻塞 Servlet 的请求线程,不影响系统的响应能力。


下面将逐一介绍每个工具的使用及其应用场景。


ResponseBodyEmitter


ResponseBodyEmitter适应适合于需要动态生成内容并逐步发送给客户端的场景,例如:文件上传进度、实时日志等,可以在任务执行过程中逐步向客户端发送更新。


举个例子,经常用GPT你会发现当你提问后,得到的答案并不是一次性响应呈现的,而是逐步动态显示。这样做的好处是,让你感觉它在认真思考,交互体验比直接返回完整答案更为生动和自然。



使用ResponseBodyEmitter来实现下这个效果,创建 ResponseBodyEmitter 发送器对象,模拟耗时操作逐步调用 send 方法发送消息。



注意:ResponseBodyEmitter 的超时时间,如果设置为 0-1,则表示连接不会超时;如果不设置,到达默认的超时时间后连接会自动断开。其他两种工具也是同样的用法,后边不在赘述了



@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// 创建一个ResponseBodyEmitter,-1代表不超时
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
// 异步执行耗时操作
CompletableFuture.runAsync(() -> {
try {
// 模拟耗时操作
for (int i = 0; i < 10000; i++) {
System.out.println("bodyEmitter " + i);
// 发送数据
emitter.send("bodyEmitter " + i + " @ " + new Date() + "\n");
Thread.sleep(2000);
}
// 完成
emitter.complete();
} catch (Exception e) {
// 发生异常时结束接口
emitter.completeWithError(e);
}
});
return emitter;
}

实现代码非常简单。通过模拟每2秒响应一次结果,请求接口时可以看到页面数据在动态生成。效果与 GPT 回答基本一致。



SseEmitter


SseEmitterResponseBodyEmitter 的一个子类,它同样能够实现动态内容生成,不过主要将它用在服务器向客户端推送实时数据,如实时消息推送、状态更新等场景。在我之前的一篇文章 我有 7种 实现web实时消息推送的方案 中详细介绍了 Server-Sent Events (SSE) 技术,感兴趣的可以回顾下。



SSE在服务器和客户端之间打开一个单向通道,服务端响应的不再是一次性的数据包而是text/event-stream类型的数据流信息,在有数据变更时从服务器流式传输到客户端。



整体的实现思路有点类似于在线视频播放,视频流会连续不断的推送到浏览器,你也可以理解成,客户端在完成一次用时很长(网络不畅)的下载。


客户端JS实现,通过一次 HTTP 请求建立连接后,等待接收消息。此时,服务端为每个连接创建一个 SseEmitter 对象,通过这个通道向客户端发送消息。


<body>
<div id="content" style="text-align: center;">
<h1>SSE 接收服务端事件消息数据</h1>
<div id="message">等待连接...</div>
</div>
<script>
let source = null;
let userId = 7777

function setMessageInnerHTML(message) {
const messageDiv = document.getElementById("message");
const newParagraph = document.createElement("p");
newParagraph.textContent = message;
messageDiv.appendChild(newParagraph);
}

if (window.EventSource) {
// 建立连接
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
setMessageInnerHTML("连接用户=" + userId);
/**
* 连接一旦建立,就会触发open事件
* 另一种写法:source.onopen = function (event) {}
*/

source.addEventListener('open', function (e) {
setMessageInnerHTML("建立连接。。。");
}, false);
/**
* 客户端收到服务器发来的数据
* 另一种写法:source.onmessage = function (event) {}
*/

source.addEventListener('message', function (e) {
setMessageInnerHTML(e.data);
});
} else {
setMessageInnerHTML("你的浏览器不支持SSE");
}
</script>
</body>

在服务端,我们将 SseEmitter 发送器对象进行持久化,以便在消息产生时直接取出对应的 SseEmitter 发送器,并调用 send 方法进行推送。


private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();

@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
log.info("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
CompletableFuture.runAsync(() -> {
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.data("sseEmitter" + userId + " @ " + LocalTime.now())
.id(String.valueOf(userId))
.name("sseEmitter");
emitterTmp.send(event);
} catch (Exception ex) {
emitterTmp.completeWithError(ex);
}
});
return emitterTmp;
}

@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
sseEmitter.send(msg);
}

接下来向 userId=7777 的用户发送消息,127.0.0.1:9033/sendSseMsg/7777?msg=欢迎关注-->程序员小富,该消息可以在页面上实时展示。



而且SSE有一点比较好,客户端与服务端一旦建立连接,即便服务端发生重启,也可以做到自动重连



StreamingResponseBody


StreamingResponseBody 与其他响应处理方式略有不同,主要用于处理大数据量或持续数据流的传输,支持将数据直接写入OutputStream


例如,当我们需要下载一个超大文件时,使用 StreamingResponseBody 可以避免将文件数据一次性加载到内存中,而是持续不断的把文件流发送给客户端,从而解决下载大文件时常见的内存溢出问题。


接口实现直接返回 StreamingResponseBody 对象,将数据写入输出流并刷新,调用一次flush就会向客户端写入一次数据。


@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {

StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
out.write(((message + i) + "\r\n").getBytes());
out.write("\r\n".getBytes());
//调用一次flush就会像前端写入一次数据
out.flush();
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(stream);
}

demo这里输出的是简单的文本流,如果是下载文件那么转换成文件流效果是一样的。



总结


这篇介绍三种实现异步流式接口的工具,算是 Spring 知识点的扫盲。使用起来比较简单,没有什么难点,但它们在实际业务中的应用场景还是很多的,通过这些工具,可以有效提高系统的性能和响应能力。



文中 Demo Github 地址:github.com/chengxy-nds…



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

为什么推荐用Redisson实现分布式锁,看完直呼好好好

开心一刻 一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解劝解员:兄弟,别跳跳楼人:我不想活了劝解员:你想想你媳妇跳楼人:媳妇跟人跑了劝解员:你还有兄弟跳楼人:就是跟我兄弟跑的劝解员:你想想你家孩子跳楼人:孩子是他俩的劝解员:死吧,妈的,你活着也没啥意义...
继续阅读 »

开心一刻


一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解
劝解员:兄弟,别跳
跳楼人:我不想活了
劝解员:你想想你媳妇
跳楼人:媳妇跟人跑了
劝解员:你还有兄弟
跳楼人:就是跟我兄弟跑的
劝解员:你想想你家孩子
跳楼人:孩子是他俩的
劝解员:死吧,妈的,你活着也没啥意义了


开心一刻

写在前面


关于锁,相信大家都不陌生,一般我们用其在多线程环境中控制对共享资源的并发访问;单服务下,用 JDK 中的 synchronizedLock 的实现类可实现对共享资源的并发访问,分布式服务下,JDK 中的锁就显得力不从心了,分布式锁也就应运而生了;分布式锁的实现方式有很多,常见的有如下几种



  1. 基于 MySQL,利用行级悲观锁(select ... for update)

  2. 基于 Redis,利用其 (setnx + expire) 或 set

  3. 基于 Zookeeper,利用其临时目录和事件回调机制   


本文不讲这些,网上资料很多,感兴趣的小伙伴自行去查阅;本文的重点是基于 Redis 的 Redisson,从源码的角度来看看为什么推荐用 Redisson 来实现分布式锁;推荐大家先去看看



搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了



有助于理解后文


分布式锁特点


可以类比 JDK 中的锁



  1. 互斥


    不仅要保证同个服务中不同线程的互斥,还需要保证不同服务间、不同线程的互斥;如何处理互斥,是自旋、还是阻塞 ,还是其他 ?


  2. 超时


    锁超时设置,防止程序异常奔溃而导致锁一直存在,后续同把锁一直加不上


  3. 续期


    程序具体执行的时长无法确定,所以过期时间只能是个估值,那么就不能保证程序在过期时间内百分百能运行完,所以需要进行锁续期,保证业务是在加锁的情况下完成的


  4. 可重入


    可重入锁又名递归锁,是指同一个线程在外层方法已经获得锁,再进入该线程的中层或内层方法会自动获取锁;简单点来说,就是同个线程可以反复获取同一把锁


  5. 专一释放


    通俗点来讲:谁加的锁就只有它能释放这把锁;为什么会出现这种错乱释放的问题了,举个例子就理解了



    线程 T1 对资源 lock_zhangsan 加了锁,由于某些原因,业务还未执行完,锁已经过期自动释放了,此时线程 T2 对资源 lock_zhangsan 加锁成功,T2 还在执行业务的过程中,T1 业务执行完后释放资源 lock_zhangsan 的锁,结果把 T2 加的锁给释放了




  6. 公平与非公平


    公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁


    非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁


    JDK 中的 ReentrantLock 就有公平和非公平两种实现,有兴趣的可以去看看它的源码;多数情况下用的是非公平锁,但有些特殊情况下需要用公平锁



你们可能会有这样的疑问



引入一个简单的分布式锁而已,有必要考虑这么多吗?



虽然绝大部分情况下,我们的程序都是在跑正常流程,但不能保证异常情况 100% 跑不到,出于健壮性考虑,异常情况都需要考虑到;下面我们就来看看 Redisson 是如何实现这些特点的


Redisson实现分布式锁


关于 Redisson,更多详细信息可查看官方文档,它提供了非常丰富的功能,分布式锁 只是其中之一;我们基于 Redisson 3.13.6,来看看分布式锁的实现



  1. 先将 Redis 信息配置给 Redisson,创建出 RedissonClient 实例


    Redis 的部署方式不同,Redisson 配置模式也会不同,详细信息可查看:Configuration,我们就以最简单的 Single mode 来配置


    @Before
    public void before() {
    Config config = new Config();
    config.useSingleServer()
    .setAddress("redis://192.168.1.110:6379");
    redissonClient = Redisson.create(config);
    }


  2. 通过 RedissonClient 实例获取锁


    RedissonClient 实例创建出来后,就可以通过它来获取锁


    /**
    * 多线程
    * @throws Exception
    */

    @Test
    public void multiLock() throws Exception {

    RLock testLock = redissonClient.getLock("multi_lock");
    int count = 5;
    CountDownLatch latch = new CountDownLatch(count);

    for (int i=1; i<=count; i++) {
    new Thread(() -> {
    try {
    System.out.println("线程 " + Thread.currentThread().getName() + " 尝试获取锁");
    testLock.lock();
    System.out.println(String.format("线程 %s 获取到锁, 执行业务中...", Thread.currentThread().getName()));
    try {
    TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(String.format("线程 %s 业务执行完成", Thread.currentThread().getName()));
    latch.countDown();
    } finally {
    testLock.unlock();
    System.out.println(String.format("线程 %s 释放锁完成", Thread.currentThread().getName()));
    }
    }, "t" + i).start();
    }

    latch.await();
    System.out.println("结束");
    }

    完整示例代码:redisson-demo



用 Redisson 实现分布式锁就是这么简单,但光会使用肯定是不够的,我们还得知道其底层实现原理



知其然,并知其所以然!



那如何知道其原理呢?当然是看其源码实现


客户端创建


客服端的创建过程中,会生成一个 id 作为唯一标识,用以区分分布式下不同节点中的客户端


client

id 值就是一个 UUID,客户端启动时生成;至于这个 id 有什么用,大家暂且在脑中留下这个疑问,我们接着往下看


锁获取


我们从 lock 开始跟源码


lock

最终会来到有三个参数的 lock 方法


private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();

// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}

// 锁被其他线程占用而获取失败,使用redis的发布订阅功能来等待锁的释放通知,而非自旋监测锁的释放
RFuture<RedissonLockEntry> future = subscribe(threadId);

// 当前线程会阻塞,直到锁被释放时当前线程被唤醒(有超时等待,默认 7.5s,而不会一直等待)
// 持有锁的线程释放锁之后,redis会发布消息,所有等待该锁的线程都会被唤醒,包括当前线程
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}

try {
while (true) {
// 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}

// waiting for message
if (ttl >= 0) {
try {
// future.getNow().getLatch() 返回的是 Semaphore 对象,其初始许可证为 0,以此来控制线程获取锁的顺序
// 通过 Semaphore 控制当前服务节点竞争锁的线程数量
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 退出锁竞争(锁获取成功或者放弃获取锁),则取消锁的释放订阅
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}

主要三个点:尝试获取锁订阅取消订阅



  1. 尝试获取锁


    尝试获取锁

    尝试获取锁主要做了两件事:1、尝试获取锁,2、锁续期;尝试获取锁主要涉及到一段 Lua 代码


    尝试获取锁Lua脚本

    结合 搞懂了 Redis 的订阅、发布与Lua,Redisson的加锁原理就好理解了 来看这段 Lua 脚本,还是很好理解的




    1. 用 exists 判断 key 不存在,则用 hash 结构来存放锁,key = 资源名,field = uuid + : + threadId,value 自增 1;设置锁的过期时间(默认是 lockWatchdogTimeout = 30 * 1000 毫秒),并返回 nil

    2. 用 hexists 判断 field = uuid + : + threadId 存在,则该 field 的 value 自增 1,并重置过期时间,最后返回 nil


      这里相当于实现了锁的重入


    3. 上面两种情况都不满足,则说明锁被其他线程占用了,直接返回锁的过期时间



    给你们提个问题



    为什么 field = uuid + : + threadId,而不是 field = threadId


    友情提示:从多个服务(也就是多个 Redisson 客户端)来考虑


    这个问题想清楚了,那么前面提到的:在 Redisson 客户端创建的过程中生成的 id(一个随机的 uuid 值),它的作用也就清楚了



    尝试获取锁成功之后,会启动一个定时任务(即 WatchDog,亦称 看门狗)实现锁续期,也涉及到一段 Lua 脚本


    看门狗Lua

    这段脚本很简单,相信你们都能看懂



    默认情况下,锁的过期时间是 30s,锁获取成功之后每隔 10s 进行一次锁续期,重置过期时间成 30s


    若锁已经被释放了,则定时任务也会停止,不会再续期




  2. 订阅


    订阅

    获取锁的过程中,尝试获取锁失败(锁被其他线程锁占有),则会完成对该锁频道的订阅,订阅过程中线程会阻塞;持有锁的线程释放锁时会向锁频道发布消息,订阅了该锁频道的线程会被唤醒,继续去获取锁,


    给你们提个问题



    如果持有锁的线程意外停止了,未向锁频道发布消息,那订阅了锁频道的线程该如何唤醒



    Redisson 其实已经考虑到了,提供了超时机制来处理


    锁频道超时机制

    默认超时时长 = 3000 + 1500 * 3 = 7500 毫秒


    再给你们提个问题



    为什么要用 Redis 的发布订阅



    如果我们不用 Redis 的发布订阅,我们该如何实现,自旋?自旋有什么缺点? 自旋频率难以掌控,太高会增大 CPU 的负担,太低会不及时(锁都释放半天了才检测到);可以类比 生产者与消费者 来考虑这个问题


  3. 取消订阅


    有订阅,肯定就有取消订阅;当阻塞的线程被唤醒并获取到锁时需要取消对锁频道的订阅,当然,取消获取锁的线程也需要取消对锁频道的订阅


    取消订阅

    比较好理解,就是取消当前线程对锁频道的订阅



锁释放


我们从 unlock 开始


unlock

代码比较简单,我们继续往下跟


unlock_跟源码

主要有两点:释放锁取消续期定时任务



  1. 释放锁


    重点在于一个 Lua 脚本


    释放锁Lua脚本

    我们把参数具象化,脚本就好理解了



    KEYS[1] = 锁资源,KEYS[2] = 锁频道


    ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId



    1. 如果当前线程未持有锁,直接返回 nil

    2. hash 结构的 field 的 value 自减 1,counter = 自减后的 value 值


      如果 counter > 0,表示线程重入了,重置锁的过期时间,返回 0


      如果 counter <= 0,删除锁,并对锁频道发布锁释放消息(频道订阅者则可收到消息,然后唤醒线程去获取锁),返回 1


    3. 上面 1、2 都不满足,则直接返回 nil


    两个细节:1、重入锁的释放,2、锁彻底释放后的消息发布




  2. 取消续期定时任务


    取消续期定时任务

    比较简单,没什么好说的


    总结


    我们从分布式锁的特点出发,来总结下 Redisson 是如何实现这些特点的



    1. 互斥


      Redisson 采用 hash 结构来存锁资源,通过 Lua 脚本对锁资源进行操作,保证线程之间的互斥;互斥之后,未获取到锁的线程会订阅锁频道,然后进入一定时长的阻塞


    2. 超时


      有超时设置,给 hash 结构的 key 加上过期时间,默认是 30s


    3. 续期


      线程获取到锁之后会开启一个定时任务(watchdog 即 看门狗),每隔一定时间(默认 10s)重置 key 的过期时间


    4. 可重入


      通过 hash 结构解决,key 是锁资源,field(值:uuid + : + threadId) 是持有锁的线程,value 表示重入次数


    5. 专一释放


      通过 hash 结构解决,field 中存放了线程信息,释放的时候就能够知道是不是当前线程加上的锁,是才能够进行锁释放


    6. 公平与非公平


      由你们在评论区补充





作者:青石路
来源:juejin.cn/post/7425786548061683727
收起阅读 »

90后在技术上开始被00后嘲笑了,90后该何去何从?

只要你不断的学习,哪怕学习一项新技术生命周期只有五年,未来十年内这个行业找口饭吃还是不难的, 如果互联网赛道太卷,也可以换个赛道 前言 事情是这样的,最近我正在做一个项目需要用到Elasticsearch做全文检索,我们组刚好进了一个00后新人,这个项目需求...
继续阅读 »

只要你不断的学习,哪怕学习一项新技术生命周期只有五年,未来十年内这个行业找口饭吃还是不难的, 如果互联网赛道太卷,也可以换个赛道



前言


事情是这样的,最近我正在做一个项目需要用到Elasticsearch做全文检索,我们组刚好进了一个00后新人,这个项目需求是在原来的搜索上增加很多新的字段,他说:新增加的字段他一会整理下然后添加到老索引上,我当时说:我们Elasticsearch索引里面有好几千万的数据,有一套添加字段的流程,需要重建索引比较麻烦的,大致流程是:先创建一个新的索引,然后reindex老数据到新索引中,然后再将别名重新指向新索引,具体流程可以参考我之前的文章《EalsticSearch添加字段后重建索引方法》。他说不需要这么麻烦啊,直接添加就好了,当时我就愣住了,我说你这有点超出了我的认知,在我的认知里Elasticsearch添加字段都是需要重建索引了,你确定索引里添加新字段不需要重建索引也能被检索到,他被我这么一问也有点不自信,然后我就实际求正了一下,在Elasticsearch7.10里添加字段确实不需要重建索引,就和Mysql添加字段一样,可能是我的知识有点落伍了,所以这里也学习、记录、反思一下。


一、添加索引


1.1 创建索引


第一步我们创建一个user索引,里面添加一个id字段:




PUT /user
{
"mappings": {
"properties":
{
"id": {
"type": "long"
}
}
}
}

然后我们添加一条数据:




POST /user/_doc/1
{
"id":1
}

1.2 添加字段


现在我们需要为user索引添加一个userName字段,类型为keyword




PUT /user/_mapping
{
"properties": {
"userName": {
"type": "keyword"
}
}
}

然后 GET /user/_mapping 发现字段已经添加上了




{
"user" : {
"mappings" : {
"properties" : {
"id" : {
"type" : "long"
},
"userName" : {
"type" : "keyword"
}
}
}
}
}

为了验证添加字段是否能补检索,我们添加一条数据




POST /user/_doc/2
{
"id":2,
"userName":"赵侠客"
}


▲可以搜索出添加的字段


验证完全没有问题,字段添加上了,也能搜索,事实证明我的知识确实需要更新了,我也不知道为什么我的认知里添加字段是需要重建索引的,而且我身边的开发者也是这么认为的,可能是最初使用者用了这个方法,后来留给我们了,然后大家都觉得添加新字段需要重建索引,也就一直这么用下来了!!!


1.3 历史数据处理


新加字段后新数据都有了userName字段,但是老数据是没有userName字段的,处理方法有两种



  • 老数据从数据库批量同步一下

  • 老数据可以通过_update_by_query设置个默认值



POST /user/_update_by_query
{
"script":{
"source": "ctx._source['userName'] = "公众号:【赵侠客】""
},
"query": {
"range": {
"id": {
"gte": 0,
"lte": 1
}
}
}
}

_update_by_query还有很多其它参数,比如异步执行、查看执行任务、取消任务等等,具体可以参考官方文档: Update By Query API


二、修改|删除索引字段类型


2.1 修改索引字段类型


在 Elasticsearch 中,一旦创建索引,就不能更改现有字段的数据类型,因为Elasticsearch是构建了倒排序索引,试想一下比如userName现在是keyword类型,如果我想改成text类型并且使用ik_max_word分词器分词,如果Elasticsearch能支持修改,那么它需要 将所有的历史数据都使用ik_max_word分词一下再重建索引,如果数据量巨大这个过程是非常缓慢,就像Mysql大表修改索引是非常慢的,所以就不支持了。想要修改字段类型,最好的方法应该是按我之前写的使用别名+reindex方式《EalsticSearch添加字段后重建索引方法》


2.2 删除索引字段


Elasticsearch已经建立好的索引数据是无法直接删除一个字段的,不过可以有两种方式来解决:



  • 第一种:是和修改字段类型一样,使用别名+reindex方式重建索引,

  • 第二种


是通过_update_by_query将历史数据中的字段删除掉,这种方法只能删除数据中的userName 字段值,mapping中的userName还是存在的,不过只要数据中没有userName字段其实和删除字段效果是一样的




POST /user/_update_by_query
{
"script": "ctx._source.remove("userName")",
"query": {
"bool": {
"must": [
{
"exists": {
"field": "userName"
}
}
]
}
}
}

2.3 批量删除数据


既然有了_update_by_query,那肯定就有_delete_by_query,我们可以批量删除数据




POST /user/_delete_by_query
{
"query": {
"match_all": {}
}
}

同样_delete_by_query也有很多其它用法,可以参考官方文档: Delete by query API


最后总结:



  • 索引可以添加字段

  • 索引字段类型不能修改

  • 索引字段不能删除


三、焦虑的原因


前面写Elasticsearch不是本文的重点,只是我的引言,我是想让真正爱学习、有耐心看到最后的人能看到接下来的内容,本文的重点我是想聊聊对程序员这个行业的一点看法,为什么这个行业大都数人都非常的焦虑,刚工作的焦虑自己技术差,工作几年的焦虑自己要非升即走,工作很多年的焦虑自己会被裁。我觉得主要的原因就和上面写的案例有点关系。数学老师为什么不焦虑,越老越吃香?因为数学老师的教学经验会随着工作年限的增长而不断积累。比如三角函数有很多很难记的公式如:sin(A+B)=sinAcosB+cosAsinB,对于老师来说在刚工作可能25岁的时候下功夫记住,探索出一套自己的教学方法,在他65岁的时间照样可以用,如果每年还能优化一下教学方法,那么他会在教学岗位上干的越来越顺,教学效果也会越来越好,自然越老越吃香。反观我们这个行业,10年前你可能对SSH框架(struts+spring+hibernate)非常精通,可是现在呢?完全没人用了,不仅不能帮助你,写在简历上别人肯定觉得你比较落后,也就是说你的经验是不能一直积累的。就好比今天上面这个添加字段的案列,可能今天来看Elasticsearch不支持修改字段类型,但是5年后你还敢说Elasticsearch不支持修改字段类型吗?你现在学习的知识也许只能使用5年,5年后现在学习的知识很可能不但不能帮助你 还可能拖累你,而且随着年龄的增长,你的学习的时间、精力和效率还在不断的下降,这放在谁身上都会焦虑的。还拿数据老师教三角函数来说,如果每年都有几个三角函数公式被证明是错误,然后会有新的公式来取代,老的教学方法就不适用了,要探索新的教学方法 那我觉得老师可能比程序员更焦虑。


四、破解之法


4.1、职业选择


选择大于努力,第一步我觉得要认清程序员这个行业的现状,不能因为初期工资高就一门脑袋往里扎,就像最近比较火的太原理工大学2024软件工程招60个班,近2000人,有多少人是了解这个行业的,现在不清楚自己是不是适合这个行业,一门脑子扎进去,以后会随着时间的发展会陷的越来越深,想跳出来也会越来越难。所以我觉得前期要定位自己适不适合这个行业非常重要,我觉得有以下特点的人是非常适合这个行业的:



  • 逻辑思维强:编程工作需要严谨的逻辑推理,能将复杂问题拆解成简单的步骤,并用代码实现。

  • 耐心 和专注:编程过程中常遇到调试错误、修复bug等琐碎但关键的工作,这需要有足够的耐心和专注力。

  • 好奇心和学习 能力:技术更新快,编程语言和框架不断变化,适合做程序员的人通常对新知识感兴趣,并有较强的学习能力。

  • 解决问题的能力:编程本质上是解决问题的过程,适合做程序员的人喜欢面对挑战,乐于通过逻辑推理和分析找到解决方案。

  • 自我驱动力强:编程项目通常需要独立完成或长时间集中开发,具备自我激励、主动学习的能力尤为重要。

  • 注重细节:代码中的小错误可能会导致程序崩溃,适合做程序员的人往往对细节有高度敏感性,善于发现和修复问题。

  • 抽象思维:编程需要将现实问题抽象成数据结构和算法,适合做程序员的人通常能在高度抽象的层次上思考问题。

  • 沟通与协作能力:尽管编程看似是独立工作,但在实际项目中,程序员需要与产品经理、设计师、其他开发者等团队成员密切合作,清晰的沟通能力是关键。

  • 抗压能力:程序开发过程中难免会遇到紧急需求、临时修改或技术难题,良好的抗压能力能够帮助程序员在高压环境下保持冷静。

  • 结果导向:编程工作最终是为了实现功能或解决业务需求,适合做程序员的人能够以目标为导向,高效达成任务。


同时我觉得有以下特点的人是不适合干程序员:



  • 不喜欢学习新技术:程序员需要不断学习和适应新技术、编程语言、框架等。如果对新知识没有兴趣,或排斥学习,那么很难跟上技术发展的步伐。

  • 缺乏耐心和细心:编程需要大量的调试和修复错误,这些过程往往繁琐且时间长。如果缺乏耐心或不注重细节,容易导致代码质量低或频繁出错。

  • 逻辑思维较弱:编程本质上是逻辑的工作,如果无法清晰地理解和推理复杂的逻辑问题,或者在面对问题时感觉思路混乱,可能会在编程中遇到较大困难。

  • 不喜欢长时间独立工作:程序员的工作常常需要长时间独立思考和编码。如果不喜欢独处或静心工作,可能难以适应编程的工作节奏。

  • 抗压能力差:程序开发中常会遇到紧急需求、项目延期、复杂的技术难题等高压情况。如果在压力下容易崩溃或无法保持稳定的心态,可能会影响工作效率和结果。

  • 缺乏责任感和自律性:编程工作需要高度的自律和责任感,特别是在解决bug、优化性能时。如果没有足够的责任心,容易出现偷工减料或敷衍了事的情况,进而影响项目的整体质量。

  • 不擅长沟通和团队协作:尽管编程常被认为是独立工作,但在实际项目中,需要与团队中的其他角色(如产品经理、测试人员等)频繁沟通。如果不善于沟通或抗拒与他人合作,可能难以融入团队工作环境。

  • 厌恶重复性工作:编程虽然涉及创新和解决问题,但也有大量重复性的编码、调试、测试等工作。如果对这些重复性任务感到极度厌烦,可能会降低工作热情和效率。

  • 缺乏结果导向:编程最终是为了实现功能和解决问题。如果过于追求完美或沉迷于技术细节,无法在规定时间内交付有用的结果,可能会影响项目进度和团队合作。

  • 对电脑和技术无兴趣:程序员需要长时间与电脑打交道,对技术本身的兴趣是持续发展的动力。如果对电脑操作、技术细节等完全无感或厌烦,难以在这个行业长期坚持。


4.2、职业规划


如果你已经选择进入了这个行业,那我觉得必须要有一个清晰的职业规划,毕竟程序员的职业生涯是比较短暂的,黄金时期可能也就十年左右,我觉得程序员的职业规划主要分为三部分,前期工作三年内、中期工作四到七年,后期工作八年以上,这三个阶段的工作重点或者说目标是不一样的。



  • 前期(三年内)


这个阶段我觉得是:面向技术开发。因为是刚参加工作,技术能力比较差的,工资也是非常低的,所以我觉得这个时期的工作重点是快速提升自己的技术能力,你所做的一切必须要快速的提升你的技术能力,要快速的多做项目、多学习,如果公司升职加薪通道比较窄,我觉得要果断跳槽, 因为你的技术在快速的提升,如果你的工资和职位增速与你的技术增速不匹配,唯有跳槽。



  • 中期(四到七年)


这个阶段我觉得是:面向履历开发。拥有一个好的履历将会受益终生,比如你在BAT待过,比如你参加了某某公司的双十一架构设计,这些写在简历上都是浓墨重彩的一笔。这一时期你的技术基本稳定成长,如果有去大厂的机会,建议还是去大厂。如果没有,我觉得需要在当前公司找到属于自己的位置,要有自己的核心负责产品,能帮助自己稳步晋升,如果没有而且技术也增长不上去、自己学习也没什么动力,我觉得这个时期转型是比较明智的。因为在这时期你大概三十不到,可能没有结婚,没有房贷,不需要养娃,也不用养老人,自己应该也有点积蓄,是工作后最没有压力的时期,所以有大把的时间和精力来寻找机会。如果到了后期,可能有房贷、车贷、养娃、养老人,压力是非常大的,没有太多的空窗期让你转型,你很可能就只能向生活低头,在公司一直苟着,担心自己终于有一天被裁,也会越来越焦虑,这就是所谓的中年危机,这一时期也是避免后期中年危机的最好时间段。



  • 后期 (八年以上)


这个阶段如果还能做技术,我觉得是:面向管理开发。因为这个阶段你在技术方面,如果不是那种不断学习进步,你的技术可能慢慢不如新人,你的技术慢慢变的没有优势,你对公司的价值也在慢慢变小,但是能在公司项目组留下来的都是元老级人物,对公司的环境、 业务、领导、同事、甚至行业都非常的了解,公司是需要一个有经验和公司一起成长的人来管理、带团队的,所以这时期我觉得工作重点是要了解公司的整体业务、行业的发展,要提升自己管理能力,向管理岗位不断靠近。但是管理岗位毕竟是少数,如果自己没有管理能力该怎么办?破解之法 就是:副业。我有很多同事都是副业转正,现在赚的可不是打工人每月这点工资了。作为程序员具体的副业就太多了,常见的如:做外包、做开源、做自建站、写工具、做自媒体,还有一点要重点说的是:程序员一定要关注海外市场,还记录承德的程序员Github接单被没收百万收入并处以罚款的新闻吗?国内是个小市场,只有打开国际视野你才能看到一个更大的市场,会有更多的机会。


最后


以上仅仅是我个人的一些浅薄观点,不一定适合你,需要根据自己的实际情况多加思考,今年的整体行情非常的差,在可预见的几年内可能都不会有太大的转变,未来程序员这个行业可能会越来越卷。我也在一直思考自己在这个行业的定位,和未来的发展方向, 其实也没能找到自己的答案,也是走一步,看一步,再想一步。未来行情是卷的,但是中国在未来十年内随着人口的减少,必须要加大数字化和智能化建设,所以只要你不断的学习,哪怕学习一项新技术生命周期只有五年,未来十年内这个行业找口饭吃还是不难的, 如何互联网赛道太卷,也可以换个赛道,比如去传统行业做数字化,目前传统行业对程序员需求量非常大,互联网过去的都算是高级人才,而且很有可能比在互联拿的更多!!



作者:赵侠客
来源:juejin.cn/post/7403576996394385444
收起阅读 »

社会现实告诉我,00后整顿职场就是个笑话

00后整顿职场,也算是我之前的关键词吧。 我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。 甚至还能在即将被开除的时候,反将一军把老板开除。 而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。 也...
继续阅读 »

00后整顿职场,也算是我之前的关键词吧。


我硬怼老板要加班费和提成,和他们辩论什么是我的财产,什么是公司的财产。


甚至还能在即将被开除的时候,反将一军把老板开除。


而正是因为这一次把老板开除,让我得到了机会。可以站在了相对于之前更高的位置,来俯瞰整个职场。


也真正意义上让我感受到了,00后整顿职场,就是一个互联网笑话罢了。


1、职场宫斗,成功上位


我之前在苏州工作,堪称工作中的宫斗,并且在这场宫斗大戏中胜出,将原有的项目负责人开除,成功上位。


而这个项目存在的问题非常多,我就在六月被派遣去项目的总部合肥进行学习,等到打通项目的全部链路后,再回到苏州。


届时我将以这个项目的负责人,重新搭建团队,开展这个项目。所以我在合肥那边,以员工的身份深入各个工作组进行学习。


在市场部,运营部的办公大厅工作过,也在各部门的独立办公室工作过。


我感觉自己像个间谍,一边在以平级的打工人身份和我的同事们相处,一边又以苏州负责人的身份,参与那些领导才能参与的内部会议。


2、内心变化的开端


我在合肥总部工作中,接触了很多躺平摆烂的同事,但这个“躺平摆烂“要加上双引号。


他们是00后,90后,甚至有85后。如果放在三个月前,我可以不假思索地说,他们全都是我最讨厌的人。他们如同牛羊一般任人宰割,上级让加班,他们就加班,有时候加班甚至超过四五个小时也没有怨言。


我甚至从来没听他们感慨过为什么没有加班费。亲眼看着他们被自己的上级用一些与工作无关的鸡毛蒜皮之事骂得狗血淋头,但他们也只会在被骂完之后,背地里吐槽那个领导估计是在家被老婆骂了,才来拿他们泄愤。


我打听了他们的工资,只能说中规中矩,起码不是能让人当牛做马的数字。偶尔我见到一两个有骨气的人,觉得拿这么点钱就应该干这么点事。干不爽就马上离职,但马上就会有下一个人替补他的位置,形成闭环。


我惊讶于怎么有人能惹到这个地步,但后来和他们日渐熟落,我们一起吃饭,一起打游戏,一起下班顺路回家,还参加了他们的生日聚会。我发现他们活得其实真的很洒脱。一切都是随遇而安,下班时间一到,他们就真的可以无忧无虑。


因为他们有一份工资还行的工作,养活自己。他们没有啃老,也没有用卑鄙的手段,去抢想要努力的人应该分到的蛋糕,也压根不去想要赚很多钱,因为没有什么需要太高消费的需求。


加上现在的环境,找到一份可观收入的工作确实很难。所以公司偶尔的加班,领导偶尔的泄愤,这些毕竟还是少数时候的偶尔,也都没有超过他们的心理承受阈值,那也就得过且过了。


所以我们其实都一样,只是个普通人罢了。而像我们这样的普通人,取之不尽,用之不竭。这到底是好事还是坏事呢?


3、复杂的职场生态环境


建立在这个基础上,视觉转换到高层领导们这里。他们当着我的面说,这样的人就是个底层打工仔,缺人就招,加班照旧,心情不好还要扣他们的全勤绩效。


压根就不怕这些底层打工仔闹事,纵使有一两个所谓的决心者辞职,也能在很快时间找到下一位。


两者形成互补,共同铸就了这样恶劣的职场生态环境。但我说职场无法改变,远不止这么一点原因。


在这个项目中,我说好听一些只能算是项目负责人,在此之上还有着项目股东,这还要细分成大股东和小股东。而我所在的项目属于互联网赛道,也就是说需要一些新鲜事物的眼光和思维来对待。


但这些股东们经常提出一些奇怪的意见,就如同用微商时代的卖货思维,来指点直播带货,并且他们是出钱的股东,他们提出的战略方针不容我驳回,因为在他们的光辉历史中,有大量的成功案例,来佐证他们的思路是对的。


我刚开始觉得也有道理。他们能有钱投资,肯定是有什么过人的本领能让他们赚到钱,但是随着相处下来,我发现不过是他们本身家里条件就优越,在九几年就能拿出一百万给他们创业。


他们把这一百万分散到二十个领域,每个投资五万总能撞上那么一两个风口,让他们实现钱生钱。


九几年的五万也算是一笔不少的投资。他们这样的发财经历,让我很难不产生质疑,这不是给我我也行吗?


毕竟他们如果真的有什么过人的本领和远见,也不至于在每次内部开会之前,都要组织喊这样的口号:“好,很好,非常好,越来越好“


甚至试图把这样的口号,带到每一次迎接客户的项目介绍会上。我以自曝式要挟制止他们这个行为,我说如果你们这么干,那我当天就辞职,内部都是自己人,我可以陪你们这样弄,但如果对外这么搞,被录下来说我们是传销,我都不知道怎么辩解。


4、职场中的背锅人


他们就是这样坚信着自己能成功,是因为自己有过人的才华。所以自我洗脑着自己提出的方向没有错。如果出错了,亏损了,那一定是负责人的问题。


但好巧不巧,我就是那个负责人。我已经无数次告诉他们,我们这个项目压根就不需要穿黑丝短裙跳舞的小姐姐。


我甚至写了一篇报告给他们,分析我们的项目为什么不能用擦边这种手段引流。但他们执意要,说这样来流量快,我都有点分不清到底是他们自己想看,还是深信这样做确实是可行。


但如果最后这样还是没成功,导致项目亏损,大概率还是在我身上找原因吧。


面对他们这样的大佬,我心里很清楚,这已经远远不是宫斗了,这也绝对不是靠几个心计,或者有实力撑腰就能取胜上位了。这场权力的游戏,不是我等草民玩得起的。


5、换个思路,创造属于自己的职场


一边是被提供资金,但是瞎指挥的股东们摧残,一边是在有限的预算下,我作为负责人,确实很难做到尊重打工人的内心挣扎,回到苏州我虽然能身居高位,但我终将成为我曾经最鄙视的人。


我不要当这个背锅侠,我也不想在这个环境中,去逐渐接受这样的价值观。


这样看来确实如此,00后整顿职场不过是一场互联网的狂欢罢了。


这个题材的故事,也永远只能发生在职场的最底层。由一群家境优越,体验生活的公子哥和我这种不知好歹的普通人共同出演。


大部分人只是在手机屏幕前把我们当个乐子,成了扣个666,然后一起吃胜利的果实。没成,那就确实是看了个乐子。


或许是因为他们心里也清楚,凭我们压根就做不到。


00后现在确实整顿不了职场,因为社会的资源和命脉还不掌握在00后手上。


但就止步于此了吗?我曾说过我想有一个自己的小工作室,遵守劳动法,双休,按时发工资,交纳五险一金。


是的,换个思路,也许00后不需要整顿职场,而是直接创造属于自己的职场,那么接下来我就要向着这个目标去努力了,毕竟二十年后我也还是00后,不如到时候再来说00后整顿职场吧。


作者:程序员Winn
来源:juejin.cn/post/7311603432929984552
收起阅读 »

Android ConstraintLayout使用进阶

前言 曾经Android有五大布局,LinearLayout、FrameLayout、TableLayout、RelativeLayout和AbsoluteLayout,那会我们比较常用的布局就两三个,写xml的时候根据界面灵活选择布局,但是往往会面临布局嵌套...
继续阅读 »

前言


曾经Android有五大布局,LinearLayout、FrameLayout、TableLayout、RelativeLayout和AbsoluteLayout,那会我们比较常用的布局就两三个,写xml的时候根据界面灵活选择布局,但是往往会面临布局嵌套过深的问题,阅读也不方便。随着Android生态的发展,Google后来推出了新的布局——ConstraintLayout(约束布局)。


我很快去学习并将其用在项目中,刚开始的时候觉得比较抽象难懂,各种不适应;一段时间过后,这玩意儿真香!


本文不讲ConstraintLayout基本使用(网上资料很多),而是关于使用ConstraintLayout的进阶。


导入依赖:(2.x版本)


implementation 'androidx.constraintlayout:constraintlayout:2.0.2'

进阶1


在开发中可能需要实现如下效果:
在这里插入图片描述
长文本
文本外层有背景,短文本的时候宽度自适应,长文本超过屏幕的时候,背景贴右边,文字显示...,这样的UI需求很常见,我们来一步步拆解。


1、文本背景需要占满屏幕,并且文本显示...


<TextView
android:layout_width="0dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:background="@drawable/xxx"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>


2、这时候TextView会水平居中,我们需要添加


app:layout_constraintHorizontal_bias="0"

layout_constraintHorizontal_bias表示水平偏移,即“当组件左侧和右侧 ( 或者 开始 和 结束 ) 两边被约束后, 两个联系之间的比例”,取值为0-1,具体看ConstraintLayout 偏移 ( Bias ) 计算方式详解,我们只需要将水平偏移量设置为0,控件就会被约束在左侧了。


3、最后一步,短文本的时候宽度自适应,长文本的时候占满屏幕,需要添加


app:layout_constraintWidth_max="wrap"

layout_constraintWidth_max表示指定视图的最大宽度,取值为“wrap”,它和“wrap_content”不同,虽然都是适应内容,但仍然允许视图比约束要求的视图更小。
最终代码:


<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/xxx"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_max="wrap"
tools:text="这是一个测试文案"
/>


进阶2


再来看个效果图:
在这里插入图片描述
在这里插入图片描述
还是文本适配的问题,短昵称的时候自适应,长昵称的时候,性别图标跟随文本长度移动,但是图标必须在“聊天”按钮左侧,文本显示...


我们再来一步步拆解(仅针对昵称Textview):


一、重复上面的步骤1和步骤2,代码如下(注意layout_width="wrap_content",上面的是0dp)


<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是昵称"
android:singleLine="true"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_head"
app:layout_constraintEnd_toStartOf="@id/iv_gender"
/>


二、这时候我们会发现布局是居中的,而且昵称TextView都需要收尾元素相连,我们可以使用layout_constraintHorizontal_chainStyle改变整条链的约束状态,它有三个值,分别是spread、spread_inside和packed,其中packed表示将所有 Views 打包到一起不分配多余的间隙(当然不包括通过 margin 设置多个 Views 之间的间隙),然后将整个组件组在可用的剩余位置居中(可以查看Chains链布局),同时由于layout_constraintHorizontal_bias="0"的作用,布局将会向左侧偏移。


app:layout_constraintHorizontal_chainStyle="packed"

三、最后,当我们输入文本时,发现文本并没有约束到“聊天”按钮左侧,因为layout_width="wrap_content",添加的约束是不起作用的,所以需要强制约束


 app:layout_constrainedWidth="true"

代码动态改变约束


初始约束:
在这里插入图片描述
修改后的约束:
在这里插入图片描述
如上图,初始状态,中间按钮约束在按钮1右侧,某个条件下需要将中间按钮约束在按钮2左侧,这种时候,我们就需要在代码动态设置约束了。
具体代码:


constraintLayout?.let {
//初始化一个ConstraintSet
val set = ConstraintSet()
//将原布局复制一份
set.clone(it)
//分别将“中间按钮”START方向和BOTTOM方向的约束清除
set.clear(“中间按钮”, ConstraintSet.START)
set.clear(“中间按钮”, ConstraintSet.BOTTOM)
//重新建立新的约束
//“中间按钮”的END约束“按钮2”控件的START
//相当于 app:layout_constraintEnd_toStartOf="@id/按钮2"
set.connect(
“中间按钮”,
ConstraintSet.END,
“按钮2”,
ConstraintSet.START,
resources.getDimensionPixelSize(R.dimen.dp_9)
)
//以及底部方向的约束
...
//最后将更新的约束应用到布局
set.applyTo(it)
}

MotionLayout


接下来是今天重头戏——MotionLayout。


MotionLayout继承自ConstraintLayout,能够通过约束关系构建丰富的view动画,动画状态分为start与end两个状态,它还能作为支持库,兼容到api 14。


来看下效果图,这是我司App某个页面的动画效果,就是用MotionLayout实现。


在这里插入图片描述


我们可以写个简单的demo实现上面一部分动画效果,如下图


在这里插入图片描述


首先我们需要在资源文件夹 res 下新建一个名为 xml 的资源文件夹,然后在 文件夹内新建一个根节点是 MotionScene 的 xml 文件,文件名为 test_motion_scene.xml,如下:


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">


</MotionScene>

activity的xml根布局改为MotionLayout,使用app:layoutDescription与之关联


在这里插入图片描述
再编写视图,定义视图具体的view和对应id


<androidx.constraintlayout.motion.widget.MotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:layoutDescription="@xml/test_motion_scene"
...
>


<ImageView
android:id="@+id/iv_head"
...
/>

<TextView
android:id="@+id/tv1"
...
/>


然后切换到test_motion_scene.xml,我们需要明确动画布局的两个状态,start和end。
在MotionScene标签下定义Transition标签,指定动画的start和end状态


<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="500">

</Transition>

之后,在Transition同级下再定义ConstrainSet标签,它表示用于指定所有视图在动画序列中某一点上的位置和属性,你可以把它理解成一个集合,集合了所有参与动画的view相关位置和属性,如下:


<?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition>
...
</Transition>

<ConstraintSet android:id="@+id/start">
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>

大体的框架搭建好了,最后就是填充约束view状态的代码了。这时候我们需要明确动画的start状态和end状态,即


(start状态)↓
在这里插入图片描述


(end状态)↓
在这里插入图片描述


前面提到,ConstraintSet是存放一些view 约束和属性的的集合,而具体描述View约束和属性是通过Constraint 标签。我们声明Constraint标签,它支持一组标准 ConstraintLayout 属性,用于添加每个view start状态的约束。


<ConstraintSet android:id="@+id/start">
<Constraint
<!-- "android:id"表示activity的xml对应的view id
android:id="@id/iv_head"
android:layout_width="90dp"
android:layout_height="90dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintEnd_toEndOf="parent"/>

<Constraint
android:id="@id/iv1"
.../>

<Constraint
android:id="@id/iv2"
.../>

...
</ConstraintSet>

接下来以同样的方式添加end状态的view约束


<ConstraintSet android:id="@+id/end">
...
</ConstraintSet>

最后,我们需要让它动起来,在Transition标签写添加一个OnClick标签,run,就能让动画动起来


<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">

<!-- 点击-->
<OnClick
motion:clickAction="toggle"
motion:targetId="@id/search_go_btn"/>

</Transition>

OnClick:表示由用户点击触发


属性:


motion:targetId="@id/target_view" (目标View的id)
如果不指定次属性,就是点击整个屏幕触发如果写了这个属性,就是点击对应id的View 触发转场动画


motion:clickAction=“action” 点击后要进行的行为 ,此属性可以设置以下几个值:


transitionToStart
过渡到 元素 motion::constraintSetStart 属性指定的状态,有过度动画效果。


transitionToEnd
过渡到 元素motion:constraintSetEnd 属性指定的状态,有过度动画效果。


jumpToStart
直接跳转到 元素 motion::constraintSetStart 属性指定的状态,没有动画效果。


jumpToEnd
直接跳转到 元素 motion:constraintSetEnd 属性指定的状态。


toggle
默认值就是这个,在 元素motion:constraintSetStart和 motion:constraintSetEnd 指定的布局之间切换,如果处于start状态就过度到end状态,如果处于end状态就过度到start状态,有过度动画。


除了OnClick之外,还有OnSwipe,它是根据用户滑动行为调整动画的进度,具体可查看文末资料。


改变动画运动过程(关键帧KeyFrameSet)


上面讲解了动画的start与end状态,但是如果我们想在动画运动过程去改变一些属性,比如设置view的透明度、旋转,又或者是改变动画运动过程的轨迹等,这时候可以用到关键帧。


KeyFrameSet是Transition的子元素,与OnClick、OnSwipe同级。KeyFrameSet中可以包含KeyPositionKeyAttributeKeyCycleKeyTimeCycleKeyTrigger,它们都可以用来改变动画过程。


此外还有与KeyFrameSet同级的KeyPositionKeyAttribute,具体大家根据需要自行了解即可。


最后再提一下MotionLayout一些常用的java api:


loadLayoutDescription() ——对应xml"app:layoutDescription",通过代码加载MotionScene;


transitionToStart() ——表示切换到动画start状态;


transitionToEnd() ——表示切换到动画end状态;


它们都默认有过渡效果,如果不需要过渡效果,可以通过**setProgress(float pos)**处理过渡进度,取值0-1;


transitionToState(int id) ——表示切换到动画某个状态,可以是start也可以是end,参数id指的是ConstraintSet标签定义的id;


setTransitionListener(MotionLayout.TransitionListener listener) ——监听MotionLayout动画执行过程,接口有四个方法,onTransitionStartedonTransitionChangeonTransitionCompletedonTransitionTrigger


OK,最最后,ConstraintLayout能有效提升日常的开发效率,通过这篇文章的介绍,此刻你学废了嘛~


参考


MotionLayout官网文档


ConstraintLayout / MotionLayout GitHub示例


MotionLayout 使用说明书(入门级详解)


ConstraintLayout使用小技巧


作者:哆Laker梦
来源:juejin.cn/post/6886337167279259661
收起阅读 »

开发小同学的骚操作,还好被我发现了

大家好,我是程序员鱼皮。今天给朋友们还原一个我们团队真实的开发场景。 开发现场 最近我们编程导航网站要开发 用户私信 功能,第一期要做的需求很简单: 能让两个用户之间 1 对 1 单独发送消息 用户能够查看到消息记录 用户能够实时收到消息通知 这其实是一...
继续阅读 »

大家好,我是程序员鱼皮。今天给朋友们还原一个我们团队真实的开发场景。


开发现场


最近我们编程导航网站要开发 用户私信 功能,第一期要做的需求很简单:



  1. 能让两个用户之间 1 对 1 单独发送消息

  2. 用户能够查看到消息记录

  3. 用户能够实时收到消息通知



这其实是一个双向实时通讯的场景,显然可以使用 WebSocket 技术来实现。


团队的后端开发小 c 拿到需求后就去调研了,最后打算采用 Spring Boot Starter 快速整合 Websocket 来实现,接受前端某个用户传来的消息后,转发到接受消息的用户的会话,并在数据库中记录,便于用户查看历史。


小 c 的代码写得还是不错的,用了一些设计模式(像策略模式、工厂模式)对代码进行了一些抽象封装。虽然在我看来对目前的需求来说稍微有点过度设计,但开发同学有自己的理由和想法,表示尊重~



前端同学小 L 也很快完成了开发,并且通过了产品的验收。


看似这个需求就圆满地完成了,但直到我阅读前端同学的代码时,才发现了一个 “坑”。



这是前端同学小 L 提交的私信功能代码,看到这里我就已经发现问题了,朋友们能注意到么?



解释一下,小 L 引入了一个 nanoid 库,这个库的作用是生成唯一 id。看到这里,我本能地感到疑惑:为什么要引入这个库?为什么前端要生成唯一 id?


难道。。。是作为私信消息的 id?


果不其然,通过这个库在前端给每个消息生成了一个唯一 id,然后发送给后端。



后端开发的同学可能会想:一般情况下不都是后端利用数据库的自增来生成唯一 id 并返回给前端嘛,怎么需要让前端来生成呢?


这里小 L 的解释是,在本地创建消息的时候,需要有一个 id 来追踪状态,不会出现消息没有 id 的情况。


首先,这么做的确 能够满足需求 ,所以我还是通过了代码审查;但严格意义上来说,让前端来生成唯一 id 其实不够优雅,可能会有一些问题。


前端生成 id 的问题


1)ID 冲突:同时使用系统的前端用户可能是非常多的,每个用户都是一个客户端,多个前端实例可能会生成相同的 ID,导致数据覆盖或混乱。


2)不够安全:切记,前端是没有办法保证安全性的!因为攻击者可以篡改或伪造请求中的数据,比如构造一个已存在的 id,导致原本的数据被覆盖掉,从而破坏数据的一致性。


要做这件事成本非常低,甚至不需要网络攻击方面的知识,打开 F12 浏览器控制台,重放个请求就行实现:



3)时间戳问题:某些生成 id 的算法是依赖时间戳的,比如当前时间不同,生成的 id 就不同。但是如果前端不同用户的电脑时间不一致,就可能会生成重复 id 或无效 id。比如用户 A 电脑是 9 点时生成了 id = 06030901,另一个用户 B 电脑时间比 A 慢了一个小时,现在是 8 点,等用户 B 电脑时间为 9 点的时候,可能又生成了重复 id = 06030901,导致数据冲突。这也被称为 “分布式系统中的全局时钟问题”。


明确前后端职责


虽然 Nanoid 这个库不依赖时间戳来生成 id,不会受到设备时钟不同步的影响,也不会因为时间戳重复而导致 ID 冲突。根据我查阅的资料,生成大约 10 ^ 9 个 ID 后,重复的可能性大约是 10 ^ -17,几乎可以忽略不计。但一般情况下,我个人会更建议将业务逻辑统一放到后端实现,这么做的好处有很多:



  1. 后端更容易保证数据的安全性,可以对数据先进行校验再生成 id

  2. 前端尽量避免进行复杂的计算,而是交给后端,可以提升整体的性能

  3. 职责分离,前端专注于页面展示,后端专注于业务,而不是双方都要维护一套业务逻辑


我举个典型的例子,比如前端下拉框内要展示一些可选项。由于选项的数量并不多,前端当然可以自己维护这些数据(一般叫做枚举值),但后端也会用到这些枚举值,双方都写一套枚举值,就很容易出现不一致的情况。推荐的做法是,让后端返回枚举值给前端,前端不用重复编写。



所以一般情况下,对于 id 的生成,建议统一交给后端实现,可以用雪花算法根据时间戳生成,也可以利用数据库主键生成自增 id 或 UUID,具体需求具体分析吧~


作者:程序员鱼皮
来源:juejin.cn/post/7376148503087169562
收起阅读 »

HR的骚操作,真的是卧龙凤雏!

现在基本已经对通过面试找工作不抱啥希望了。 有时候面试官和我聊的,还没有前面hr小姐姐和我聊的多,我一听开场白就基本知道就是拿我走个过场,没戏! 现在的面试流程都是人事先和你聊半天,没什么硬伤大坑才会放你去见面试官。 二零一几年那会可不是这样,第一次的详聊都是...
继续阅读 »

现在基本已经对通过面试找工作不抱啥希望了。


有时候面试官和我聊的,还没有前面hr小姐姐和我聊的多,我一听开场白就基本知道就是拿我走个过场,没戏!


现在的面试流程都是人事先和你聊半天,没什么硬伤大坑才会放你去见面试官。


二零一几年那会可不是这样,第一次的详聊都是直接业务层,业务的人觉得你ok,你再和人事沟通,定个薪资就完了。


13年的时候我在一家外企,三千的月薪。当时我一个小目标就是月薪过五千。


可别笑,13年的月薪五千,那还是能勉强算上一个小白领的。


我就老琢磨着升职加薪。但眼下的公司规模小,人员基本不扩增,不流通,我就想跳槽了。


当时我同时面了AB两家外资游戏公司。都过了业务层的面试,只剩和人事定薪资。


我给A公司报价5500,给B公司报价6000,因为我知道B公司刚来国内开拓业务,属于扩张期。


这时候,A公司HR的骚操作就来了,她说:“嗯,5500嘛,有难度,但不是不可能,我可以帮你跟老板争取。”


然后又问我:“你已经从现在的公司里面离职了吗?”


我说:“还没呢,我想先把下家定了。”


她就说:“哎呀,那有点难办,你得先从现在这家公司离职,我得确保我帮你争取下来后,你不会鸽我,不然我没法和老板交代,要不你先把那边离职办了吧。”


我说:“那我再考虑考虑吧。”


然后没过两天,我收到了B公司的offer。人家都没还价,直接定了6000,我就开始走离职手续。


这时候A公司的HR又出来问我,你从现在的公司离职了吗?


我说离了,她说你给我看看离职证明,我就拍照给她看离职证明。


然后她连假装让我等一会儿,说自己去问一下老板的戏都不演了,直接秒回说:“我帮你问了老板了,老板说不行,5500给不了,最多给到4500,要不你先入职呢,后面有机会提加薪的。”


瞬间给我恶心的,怎么会有这么恶心的人事!先把你忽悠离职,然后翻脸不认人,可劲往下砍价,为了公司的KPI,自己做人的脸都不要了。


我当时就觉得这样的人真傻,就算我认了4500的杀价入了职,我把和她的对话记录公司群里一发,老板会怎么看她,同事会怎么看她。


咱做人得有底线呀,你用这种脏办法帮公司省那几百块钱,还把自己的名声信誉搭进去了,真的值得吗?


后来我在入职B公司差不多半年后,传来了A公司解散倒闭的消息,我心里还暗爽了一把,幸亏当年没进那个火坑。


但半年后,我所在的B公司也解散了。


2013年那是一个手游刚兴起的疯狂年代,数不清的大小公司起家,创业,失败,解散,换批核心班子,再起家,再失败,浮浮沉沉,我也成了疯狂年代下的沧海一粟。


作者:程序员Winn
来源:juejin.cn/post/7426685644230213643
收起阅读 »

初中都没念完的我,是怎么从IT这行坚持下去的...

大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。 现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。 在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,...
继续阅读 »

大家好,我是一名二线(伪三线,毕竟连续两年二线城市了)的程序员。


现阶段状态在职,28岁,工作了10年左右,码农从事了5年左右,现薪资9k左右。如文章标题所说,初二辍学,第一学历中专,自己报的成人大专。


在掘金也看了不少经历性质的文章,大多都是很多大牛的文章,在大城市的焦虑,在大厂的烦恼,所以今天换换口味,看一看我这个没有学历的二线的程序员的经历。


1.jpg


1.辍学


我是在初二的时候辍学不上的,原因很简单,太二笔了。


现在想来当时的我非常的der,刚从村里的小学出来上中学之后(我还是年级第7名进中学,殊不知这就是我这辈子最好的成绩了),认为别人欺负我我就一定要还回来,完全不知道那是别人的地盘,嚣张的一批,不出意外就被锤了,但是当时个人武力还是很充沛的,按着一个往地上锤,1V7的战绩也算可以了。自此之后,我就开始走上了不良的道路,抽烟喝酒打架,直到中专毕业那天。



我清楚的记得我推着电车望着天,心里只想着一个问题,我毕业了,要工作了,我除了打游戏还会什么呢,我要拿什么生存呢...



这是当时我心里真实的想法,我好像就在这一刻、这一瞬间长大了。


2.jpg


2.深圳之旅


因为我特别喜欢玩游戏,而且家里电脑总是出问题,所以我就来到了我们这当地的一个电脑城打工,打了半年工左右想学习一下真正的维修技术,也就是芯片级维修,毅然决然踏上了深圳的路。


在深圳有一家机构叫做迅维的机构,还算是在业内比较出名的这么一个机构,学习主板显卡的维修,学习电路知识,学习手机维修的技术。现在的我想想当时也不太明白我怎么敢自己一个人就往深圳冲,家里人怎么拦着我都没用,当时我就好像着了魔一样必须要去...


不过在深圳的生活真的很不错,那一年的时光仍旧是我现在非常怀念的,早晨有便宜好吃的肠粉、米粉、甜包,中午有猪脚饭、汤饭、叉烧饭,晚上偶尔还会吃一顿火锅,来自五湖四海的朋友也是非常的友好,教会了我很多东西,生活非常的不错。


3.jpg


3.回家开店


为什么说我工作了10年左右呢,因为我清楚记得我18岁那年在本地开了一个小店,一个电脑手机维修的小店。现在想想我当时也是非常的二笔,以下列举几个事件:



  1. 修了一个显示器因为没接地线烧了,还跟人家顾客吵了一架。

  2. 修苹果手机翘芯片主板线都翘出来了,赔了一块。

  3. 自己说过要给人家上门保修,也忘了,人家一打电话还怼了一顿。

  4. 因为打游戏不接活儿。


以上这几种情况比比皆是,哪怕我当时这么二笔也是赚了一些钱,还是可以维持的,唯一让我毅然决然转行的就是店被偷了,大概损失了顾客机器、我的机器、图纸、二手电脑等一系列的商品,共计7万元左右,至今仍没找回!


4.jpg


4.迷茫


接下来这三年就是迷茫的几年了,第一件事就是报成人大专,主要从事的行业就杂乱无章了,跟我爸跑过车,当过网吧网管,超市里的理货员,但是这些都不是很满意,也是从这时候开始接触了C和C++开始正式踏入自学编程的路,直到有一次在招聘信息里看到java,于是在b站开始自学java,当时学的时候jdk还是1.6,学习资料也比较古老,但是好歹是入了门了。


5.jpg


5.入职


在入门以后自我感觉非常良好,去应聘了一个外包公司,当时那个经理就问了我一句话,会SSM吗,我说会,于是我就这么入职了,现在想想还是非常幸运的。


当时的我连SSM都用不明白,就懂一些java基础,会一些线程知识,前端更是一窍不通,在外包公司这两年也是感谢前辈带我做一些项目,当时自己也是非常争气,不懂就学,回去百度、b站、csdn各种网站开始学习,前端学习了H5、JS、CSS还有一个经典前端框架,贤心的Layui。


干的这两年我除了学习态度非常认真,工作还是非常不在意,工作两年从来没有任何一个月满勤过,拖延症严重,出现问题从来就是逃避问题,职场的知识是一点也不懂,当时的领导也很包容我,老板都主持了我的婚礼哈哈哈。但是后来我也为我的嚣张买了单,怀着侥幸心理喝了酒开车,这一次事情真真正正的打醒了我,我以后不能这样了...


6.jpg


6.第二家公司


在第二家公司我的态度就变了很多很多 当时已经25岁了,开始真真正正是一个大人了,遵纪守法,为了父母和家人考虑,生活方面也慢慢的好了起来(在刚结婚两年和老婆经常吵架,从这时候开始到现在没有吵过任何架了就),生活非常和睦。工作方面也是从来不迟到早退,听领导的安排,认真工作,认真学习,认识了很多同行,也得到了一些人的认可,从那开始才开始学习springboot、mq、redis、ES一些中间件,学习了很多知识,线程知识、堆栈、微服务等一系列的知识,也算是能独当一面了。但好景不长,当时我的薪资已经到13K左右了,也是因为我们部门的薪资成本、服务器成本太大,入不敷出,公司决定代理大厂的产品而不是自研了,所以当时一个部门就这么毕业了...


7.png


7.现阶段公司


再一次找工作就希望去一些自研的大公司去做事情了,但是也是碍于学历,一直没有合适的,可以说是人厌狗嫌,好的公司看不上我,小公司我又不想去,直到在面试现在公司的时候聊得非常的好,也是给我个机会,说走个特批,让我降薪入职,算上年终奖每个月到手大概10k(构成:9k月薪,扣除五险一金到手7.5k,年终奖27k,仨月全薪,所以每个月到手10k),我也是本着这个公司非常的大、非常的稳定、制度非常健全、工作也不是很忙也就来了,工作至今。


8.jpg


总结



  1. 任何时候想改变都不晚,改变不了别人改变自己。

  2. 面对问题绝对不能逃避,逃避没有任何用,只有面对才能更好的继续下去。

  3. 不要忘了自己为什么踏入这行,因为我想做游戏。

  4. 解决问题不要为了解决而解决,一定要从头学到尾,要不然以后出现并发问题无从下手。

  5. 任何事情都要合规合法。

  6. 工作了不要脱产做任何事情,我是因为家里非常支持,我妈至今都难以相信我能走到今天(我认为我大部分是运气好,加上赶上互联网浪潮的尾巴)。

  7. 最重要的,任何事情都没有家人重要,想回家就回家吧,挣钱多少放一边,IT行业找个副业还是非常简单的,多陪陪他们!


作者:奇怪的程序员
来源:juejin.cn/post/7309645869644480522
收起阅读 »

请不要自己写,Spring Boot非常实用的内置功能

在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。 松哥来和大家列举几个。 一 请求数据记录 Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFi...
继续阅读 »

在 Spring Boot 框架中,内置了许多实用的功能,这些功能可以帮助开发者高效地开发和维护应用程序。


松哥来和大家列举几个。


一 请求数据记录


Spring Boot提供了一个内置的日志记录解决方案,通过 AbstractRequestLoggingFilter 可以记录请求的详细信息。


AbstractRequestLoggingFilter 有两个不同的实现类,我们常用的是 CommonsRequestLoggingFilter



通过 CommonsRequestLoggingFilter 开发者可以自定义记录请求的参数、请求体、请求头和客户端信息。


启用方式很简单,加个配置就行了:


@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter logFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
filter.setIncludeHeaders(true);
filter.setIncludeClientInfo(true);
filter.setAfterMessagePrefix("REQUEST ");
return filter;
}
}

接下来需要配置日志级别为 DEBUG,就可以详细记录请求信息:


logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG


二 请求/响应包装器


2.1 什么是请求和响应包装器


在 Spring Boot 中,请求和响应包装器是用于增强原生 HttpServletRequestHttpServletResponse 对象的功能。这些包装器允许开发者在请求处理过程中拦截和修改请求和响应数据,从而实现一些特定的功能,如请求内容的缓存、修改、日志记录,以及响应内容的修改和增强。


请求包装器



  • ContentCachingRequestWrapper:这是 Spring 提供的一个请求包装器,用于缓存请求的输入流。它允许多次读取请求体,这在需要多次处理请求数据(如日志记录和业务处理)时非常有用。


响应包装器



  • ContentCachingResponseWrapper:这是 Spring 提供的一个响应包装器,用于缓存响应的输出流。它允许开发者在响应提交给客户端之前修改响应体,这在需要对响应内容进行后处理(如添加额外的头部信息、修改响应体)时非常有用。


2.2 使用场景



  1. 请求日志记录:在处理请求之前和之后记录请求的详细信息,包括请求头、请求参数和请求体。

  2. 修改请求数据:在请求到达控制器之前修改请求数据,例如添加或修改请求头。

  3. 响应内容修改:在响应发送给客户端之前修改响应内容,例如添加或修改响应头,或者对响应体进行签名。

  4. 性能测试:通过缓存请求和响应数据,可以进行性能测试,而不影响实际的网络 I/O 操作。


2.3 具体用法


请求包装器的使用

import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class RequestWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 可以在这里处理请求数据
byte[] body = requestWrapper.getContentAsByteArray();
// 处理body,例如记录日志
//。。。
filterChain.doFilter(requestWrapper, response);
}
}

响应包装器的使用

import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class ResponseWrapperFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(request, responseWrapper);

// 可以在这里处理响应数据
byte[] body = responseWrapper.getContentAsByteArray();
// 处理body,例如添加签名
responseWrapper.setHeader("X-Signature", "some-signature");

// 必须调用此方法以将响应数据发送到客户端
responseWrapper.copyBodyToResponse();
}
}

在上面的案例中,OncePerRequestFilter 确保过滤器在一次请求的生命周期中只被调用一次,这对于处理请求和响应数据尤为重要,因为它避免了在请求转发或包含时重复处理数据。


通过使用请求和响应包装器,开发者可以在不改变原有业务逻辑的情况下,灵活地添加或修改请求和响应的处理逻辑。


三 单次过滤器


3.1 OncePerRequestFilter


OncePerRequestFilter 是 Spring 框架提供的一个过滤器基类,它继承自 Filter 接口。这个过滤器具有以下特点:



  1. 单次执行OncePerRequestFilter 确保在一次请求的生命周期内,无论请求如何转发(forwarding)或包含(including),过滤器逻辑只执行一次。这对于避免重复处理请求或响应非常有用。

  2. 内置支持:它内置了对请求和响应包装器的支持,使得开发者可以方便地对请求和响应进行包装和处理。

  3. 简化代码:通过继承 OncePerRequestFilter,开发者可以减少重复代码,因为过滤器的执行逻辑已经由基类管理。

  4. 易于扩展:开发者可以通过重写 doFilterInternal 方法来实现自己的过滤逻辑,而不需要关心过滤器的注册和执行次数。


3.2 OncePerRequestFilter 使用场景



  1. 请求日志记录:在请求处理之前和之后记录请求的详细信息,如请求头、请求参数和请求体,而不希望在请求转发时重复记录。

  2. 请求数据修改:在请求到达控制器之前,对请求数据进行预处理或修改,例如添加或修改请求头,而不希望这些修改在请求转发时被重复应用。

  3. 响应数据修改:在响应发送给客户端之前,对响应数据进行后处理或修改,例如添加或修改响应头,而不希望这些修改在请求包含时被重复应用。

  4. 安全控制:实现安全控制逻辑,如身份验证、授权检查等,确保这些逻辑在一次请求的生命周期内只执行一次。

  5. 请求和响应的包装:使用 ContentCachingRequestWrapperContentCachingResponseWrapper 等包装器来缓存请求和响应数据,以便在请求处理过程中多次读取或修改数据。

  6. 性能监控:在请求处理前后进行性能监控,如记录处理时间,而不希望这些监控逻辑在请求转发时被重复执行。

  7. 异常处理:在请求处理过程中捕获和处理异常,确保异常处理逻辑只执行一次,即使请求被转发到其他处理器。


通过使用 OncePerRequestFilter,开发者可以确保过滤器逻辑在一次请求的生命周期内只执行一次,从而避免重复处理和潜在的性能问题。这使得 OncePerRequestFilter 成为处理复杂请求和响应逻辑时的一个非常有用的工具。


OncePerRequestFilter 的具体用法松哥就不举例了,第二小节已经介绍过了。


四 AOP 三件套


在 Spring 框架中,AOP(面向切面编程)是一个强大的功能,它允许开发者在不修改源代码的情况下,对程序的特定部分进行横向切入。AopContextAopUtilsReflectionUtils 是 Spring AOP 中提供的几个实用类。


我们一起来看下。


4.1 AopContext


AopContext 是 Spring 框架中的一个类,它提供了对当前 AOP 代理对象的访问,以及对目标对象的引用。


AopContext 主要用于获取当前代理对象的相关信息,以及在 AOP 代理中进行一些特定的操作。


常见方法有两个:



  • getTargetObject(): 获取当前代理的目标对象。

  • currentProxy(): 获取当前的代理对象。


其中第二个方法,在防止同一个类中注解失效的时候,可以通过该方法获取当前类的代理对象。


举个栗子:


public void noTransactionTask(String keyword){    // 注意这里 调用了代理类的方法
((YourClass) AopContext.currentProxy()).transactionTask(keyword);
}

@Transactional
void transactionTask(String keyword) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) { //logger
//error tracking
}
System.out.println(keyword);
}

同一个类中两个方法,noTransactionTask 方法调用 transactionTask 方法,为了使事务注解不失效,就可以使用 AopContext.currentProxy() 去获取当前代理对象。


4.2 AopUtils


AopUtils 提供了一些静态方法来处理与 AOP 相关的操作,如获取代理对象、获取目标对象、判断代理类型等。


常见方法有三个:



  • getTargetObject(): 从代理对象中获取目标对象。

  • isJdkDynamicProxy(Object obj): 判断是否是 JDK 动态代理。

  • isCglibProxy(Object obj): 判断是否是 CGLIB 代理。


举个栗子:


import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;

public class AopUtilsExample {
public static void main(String[] args) {
MyService myService = ...
// 假设 myService 已经被代理
if (AopUtils.isCglibProxy(myService)) {
System.out.println("这是一个 CGLIB 代理对象");
}
}
}

4.3 ReflectionUtils


ReflectionUtils 提供了一系列反射操作的便捷方法,如设置字段值、获取字段值、调用方法等。这些方法封装了 Java 反射 API 的复杂性,使得反射操作更加简单和安全。


常见方法:



  • makeAccessible(Field field): 使私有字段可访问。

  • getField(Field field, Object target): 获取对象的字段值。

  • invokeMethod(Method method, Object target, Object... args): 调用对象的方法。


举个栗子:


import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.Map;

public class ReflectionUtilsExample {
public static void main(String[] args) throws Exception {
ExampleBean bean = new ExampleBean();
bean.setMapAttribute(new HashMap<>());

Field field = ReflectionUtils.findField(ExampleBean.class, "mapAttribute");
ReflectionUtils.makeAccessible(field);

Object value = ReflectionUtils.getField(field, bean);
System.out.println(value);
}

static class ExampleBean {
private Map<String, String> mapAttribute;

public void setMapAttribute(Map<String, String> mapAttribute) {
this.mapAttribute = mapAttribute;
}
}
}

还有哪些实用内置类呢?欢迎小伙伴们留言~


作者:江南一点雨
来源:juejin.cn/post/7417630844100231206
收起阅读 »

你小子,一个bug排查一整天,你在🐟吧!

web
楔子   在每日的例行会议上,空气中弥漫着紧张的气息。一位实习组员语速略急地说道:“昨天我主要的工作是排查一个线上bug,目前还没有得到解决,今天我得继续排查。”。   组长眉头微皱,冷冷地盯了他一眼:“你小子,一个bug排查一整天,怕是在摸鱼吧!到底是什么问...
继续阅读 »

楔子


  在每日的例行会议上,空气中弥漫着紧张的气息。一位实习组员语速略急地说道:“昨天我主要的工作是排查一个线上bug,目前还没有得到解决,今天我得继续排查。”。


  组长眉头微皱,冷冷地盯了他一眼:“你小子,一个bug排查一整天,怕是在摸鱼吧!到底是什么问题?说来听听,我稍后看看。”。


  组员无奈地摊了摊手,耸了耸肩,长叹一口气:“前两天,订单表格新增定制信息匹配失败情况的展示。自己没有经过仔细的测试,就直接发布上线了,导致现在整个订单列表渲染缓慢。这个bug超出了我的能力范围,我排查了一天也排查不出来,摸鱼是404的。”。


  组长深吸一口气,眼神中露出几分聪慧:“那不就是你编写的组件有问题吗?你最好没有摸鱼!不然你就等着吃鱼吧!”。


  组员按捺不住心中的窃喜:“我如果不说一天,又怎么能请动你这尊大神呢?”。


你小子.jpg


排查


  果不其然,控制台果真报错了。组长看了眼报错信息,摇了摇头,面色凝重:“你小子,居然都不看控制台的报错信息?这bug怎么排查的?”。组员下意识地捏紧了拳头,声音也不自觉地低了几分,结结巴巴道:“我、我真的不知道控制台还有这操作!学废了。”。


image.png

  组长怀着忐忑不安的心情打开vsCode, 只见一大串代码赫然映入眼帘:


<template>
<div class="design-wrapper">
<designProducts v-if="showBtn" btnText="设计" class="mr10" :data="data" @success="success" />
<el-tooltip v-if="showStatus" trigger="click" placement="right" :disabled="disabled">
<baseTable1 class="hide-tabs" :data="tableData" :option="option">
<template #content="{ row }">
<defaultImg v-if="imageType(row)" :src="image(row)" :size="100" />
<span v-else>{{ text(row) }}</span>
</template>
<template #mapping="{ row }">
<i :class="icon(row)"></i>
</template>
<template #importLabelCode="{ row }">
<span v-if="!row.hide">{{ row.importLabelCode }}</span>
</template>
<template #matchStatus="{ row }">
{{ matchSuccess(row) ? '已匹配' : '未匹配' }}
</template>
<template #design="{ row }">
<defaultImg
v-if="!row.hide && imageType(row)"
:disabled="row.disabled"
:src="row.importContent"
:size="100"
@error="error(row)"
>

<template #defaultImg>
<div class="flex-middle">{{ row.importContent || '无' }}</div>
</template>
</defaultImg>
<div v-else-if="!row.hide && !imageType(row)">{{ row.importContent || '无' }}</div>
</template>
</baseTable1>
<color-text-btn slot="reference" @click="toDesign">{{ status }}</color-text-btn>
</el-tooltip>
<span v-else></span>
</div>
</template>

  当扫到el-tooltip (文字提示), 组长拍案而起,额头上暴起的青筋在不断颤抖。急切的声音,仿佛要撕裂虚空:“你小子,短短几十行代码,至少犯了2个致命错误!”。


问题分析


1. 从代码源头分析el-tooltip(控制台报错的原因)



  • el-tooltip组件主要是针对文字提示,而el-popover才是针对组件进行展示。这两者是截然不同的,不然element也不会分出两套组件,去分别处理这两种情况。

  • 我们的项目之所以能正常使用vueXrouter,是因为我们在main.js中引入并挂载了


    QQ截图20241009142821.png

    同理,分析el-tooltip组件的代码实现,它只挂载了data属性。因此,当强行在el-tooltip组件中使用自定义组件:如果组件内部使用的是非国际语言(i18n)的纯文本,控制台不会报错;如果在该组件中使用了诸如vueX、路由跳转等在内的变量或者方法时,控制台就会疯狂报错,因为这些并没有在初始化时注入到el-tooltip组件中。


    image.png


2. 如何在el-tooltip中使用i18n?


  假定有一个非常执拗的人,他看到el-tooltip组件描述的功能是文字提示。他就不想在el-popover中使用$t, 而想在el-tooltip组件中使用i18n。那么可以做到吗?答案是肯定的。


  我们可以直接通过继承法则:封装一个base-tooltip组件,继承el-tooltip组件。并根据继承规则:先执行el-tooltip组件的生命周期钩子方法,再执行base-tooltip组件里面的生命周期钩子方法。通过这种方式,我们成功挂载了i18n。此时,在base-tooltip组件中使用$t,就不会报错了。


<script>
import { Tooltip } from 'element-ui'
import i18n from '@/i18n'
import Vue from 'vue'

export default {
extends: Tooltip,

beforeCreate() {
this.popperVM = new Vue({
data: { node: '' },
i18n,
render(h) {
return this.node;
}
}).$mount()
}
}

</script>

3. el-tooltip的局限性(订单列表渲染缓慢的原因)


  前文提及,我们可以继承el-tooltip组件。那么,我们如果通过按需引入的方式,将所需要的资源全部挂载到vue中。这样,就算在base-tooltip组件中使用vueX$route变量,也不会在控制台上报错。的确如此,但是我们需要注意到el-tooltipel-popover的局限性: 悬浮框内容是直接渲染的,不是等你打开悬浮框才渲染。


  这也就意味着,如果我们在表格的每一行都应用了el-tooltipel-popover组件,而且在el-tooltipel-popover的生命周期钩子函数中请求了异步数据。就会导致页面初始化渲染数据的同时,会请求N个接口(其中,N为当前表格的数据条数)。一次性请求大于N + 1个接口,你就说页面会不会卡顿就完事了!


  但是,el-popover这个组件不一样。在它的组件内部,提供了一个show方法,这个方法在trigger触发后才执行。于是,我们可以在show方法中,去请求我们需要的异步数据。 同时注意一个优化点:在悬浮框打开之后,才渲染Popover内嵌的html文本,避免页面加载时就渲染数据。


  由于el-popover的内容是在弹窗打开后才异步加载的,弹窗可能会在内容完全加载之前就开始计算和渲染位置,导致弹出的位置不对。但是我们遇到事情不要慌,el-popover组件的混入中提供了一个方法updatePopper,用于矫正popover的偏移量,以期获取正确的popover布局。


image.png

解决方法


  将上述所有思路结合在一起,我们就能够封装一个公共组件,兼容工作中的大多数场景。


<template>
<el-popover
ref="popover"
@show="onShow"
v-bind="$attrs"
v-on="$listeners"
>

<template v-if="isOpened">
<slot></slot>
</template>
<template slot="reference">
<slot name="reference"></slot>
</template>
</el-popover>

</template>

<script>
import agentMixin from '@/mixins/component/agentMixin'

export default {
// 方便我们直接调用popover组件中的方法
mixins: [agentMixin({ ref: 'popover', methods: ['updatePopper', 'doClose'] })],

props: {
// 方便在打开悬浮框之前,做一些前置操作,比如数据请求等
beforeOpen: Function
},

data() {
return {
isOpened: false
}
},

methods: {
async onShow() {
if(!this.beforeOpen) {
return this.isOpened = true
}
const res = await this.beforeOpen()
if(!res) return this.isOpened = false
this.isOpened = true
await this.$nextTick()
this.updatePopper()
}
}
}
</script>


/* eslint-disable */

import { isArray, isPlainObject } from 'lodash'

export default function ({ ref, methods } = {}) {
if (isArray(methods)) {
methods = methods.map(name => [name, name])
// 如果传入是对象,可以设置别名,防止方法名重复
} else if (isPlainObject(methods)) {
methods = Object.entries(methods)
}

return {
methods: {
...methods.reduce((prev, [name, alias]) => {
prev[alias] = function (...args) {
return this.$refs[ref][name](...args)
}
return prev
}, {})
}
}
}

<template>
<div class="design-wrapper">
<designProducts v-if="showBtn" btnText="设计" class="mr10" :data="data" @success="success" />
<basePopover v-if="showStatus" trigger="click" placement="right" :beforeOpen="beforeOpen" :disabled="disabled">
<baseTable1 class="hide-tabs" :data="tableData" :option="option">
<template #content="{ row }">
<defaultImg v-if="imageType(row)" :src="image(row)" :size="100" />
<span v-else>{{ text(row) }}</span>
</template>
<template #mapping="{ row }">
<i :class="icon(row)"></i>
</template>
<template #importLabelCode="{ row }">
<span v-if="!row.hide">{{ row.importLabelCode }}</span>
</template>
<template #matchStatus="{ row }">
{{ matchSuccess(row) ? '已匹配' : '未匹配' }}
</template>
<template #design="{ row }">
<defaultImg
v-if="!row.hide && imageType(row)"
:disabled="row.disabled"
:src="row.importContent"
:size="100"
@error="error(row)"
>

<template #defaultImg>
<div class="flex-middle">{{ row.importContent || '无' }}</div>
</template>
</defaultImg>
<div v-else-if="!row.hide && !imageType(row)">{{ row.importContent || '无' }}</div>
</template>
</baseTable1>
<color-text-btn slot="reference" @click="toDesign">{{ status }}</color-text-btn>
</basePopover>
<span v-else></span>
</div>

</template>

<script>
methods: {
async beforeOpen() {
const res = await awaitResolveDetailLoading(
microApi.getMatchInfo({
id: this.data.id
})
)
if (!res) return false
this.tableData = res
return true
}
}
</script>


反思


  在组长的悉心指导下,组员逐渐揭开了问题的真相。回想起自己在面对bug时的轻率和慌乱,他不禁感到一阵羞愧。组长平静而富有耐心的声音再次在耳边响起:“排查问题并非一朝一夕之功。急于上线而忽视测试,只会让问题愈加复杂。”这一番话如同醍醐灌顶,瞬间点醒了他,意识到自己的错误不仅在于代码的疏漏,更在于对整个工作流程的轻视。


  “编写代码不是一场竞赛,速度永远无法替代质量。”组长边调试代码,边语重心长地说道。组长的语气虽然平淡,却蕴含着深邃的力量。组员心中的敬佩之意油然而生,细细回味着这番话,顿时明白了面对复杂bug时,耐心与细致才是解决问题的最强利器。组长的话语简洁而富有哲理,令他意识到,曾经追求的“快速上线”与开发中的严谨要求完全背道而驰。


  不久之后,组员陷入了沉思,轻声开口:“起初,我还真觉得自己运气不好,偏偏遇上如此棘手的bug。但现在看来,这更像是一场深刻的教训。若能在上线前认真测试,这个问题本是可以避免的。”他的声音中透出几分懊悔,眼中闪烁着反思的光芒。


  组长微微一笑,点头示意:“每一个bug都是一次学习的契机,能意识到问题的根源,已是进步。”他稍作停顿,眼神愈加坚定:“编程的速度固然重要,但若未经过深思熟虑的测试与分析,那无疑只是纸上谈兵。写代码不仅需要实现功能,更需要经得起时间的考验。”这番话语透着无可辩驳的真理,给予了组员莫大的启迪。


  组员感慨道:“今天的排查让我真正领悟到耐心与细致的重要性。排查bug就像走出迷宫,急躁只会迷失方向,而冷静思考才能找到出路。”他不禁回忆起自己曾经的粗心大意,心中暗自发誓,今后在每一次提交前都要更加谨慎,绝不再犯同样的错误。


  “你小子,倒也不算愚钝。”组长调侃道,嘴角勾起一丝轻松的笑意,“但记住,遇到问题时要先冷静分析错误信息,找出原因再行动。不要盲目修改,开发不仅仅是写代码,更需要学会深思熟虑。”他轻轻拍了拍组员的肩膀,那一拍似乎传达着无限的关心与期望。


  这一拍虽轻,却如雷霆般震动着组员的心灵。他明白,这不仅是组长对他的鼓励,更是一份期待与责任的传递。心中顿时涌起一股暖流,他暗自立誓:今后的每一次开发,必将怀揣严谨的态度,赋予每一行代码以深刻的责任感,而不再仅仅是为了完成任务。


  在回家的路上,组员默默在心中念道:“这次bug排查,不仅修复了代码,更矫正了我对待开发工作的态度。感谢组长,给予我如此宝贵的经验和鼓励。”他深知,从这次经历中所学到的,绝不仅是技术层面的知识,更需要以一种成熟与稳重的心态,来面对未来的每一个挑战。


  怀着这样的领悟,组员的内心充满了期待。他坚信,这必将成为他在开发道路上迈向更高境界的起点。


作者:沐浴在曙光下的贰货道士
来源:juejin.cn/post/7423378897381130277
收起阅读 »

花了一天时间帮财务朋友开发了一个实用小工具

大家好,我是晓凡。 写在前面 不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。 一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平...
继续阅读 »

大家好,我是晓凡。


写在前面


不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。


来自朋友的抱怨


一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平衡了很多。



身为牛马,大家都不容易啊。我不羡慕你数钱数到手抽筋,你也别羡慕我整天写CRUD 写到手起老茧🤣


吐槽归吐槽,饭还得吃,工作还得继续干。于是乎,真好赶上周末,花了一天的时间,帮朋友写了个小工具


一、功能需求


跟朋友吹了半天牛,终于把需求确定下来了。就一个很简单的功能,通过名字,将表一和表二中相同名字的金额合计。


具体数据整合如下图所示


数据整合


虽然一个非常简单的功能,但如果不借助工具,数据太多,人工来核对,整合数据,还是需要非常消耗时间和体力的。


怪不得,这朋友到月底就消失了,原来时间都耗在这上面了。


二、技术选型


由于需求比较简单,只有excel导入导出,数据整合功能。不涉及数据库相关操作。


综合考虑之后选择了



  • PowerBuilder

  • Pbidea.dll


使用PowerBuilder开发桌面应用,虽然界面丑一点,但是开发效率挺高,简单拖拖拽拽就能完成界面(对于前端技术不熟的小伙伴很友好)


其次,由于不需要数据库,放弃web开发应用,又省去了云服务器费用。最终只需要打包成exe文件即可跑起来


Pbidea.dll 算是Powerbuilder最强辅助开发,没有之一。算是PBer们的福音吧


三、简单界面布局


界面布局1


界面布局2


界面布局3


四、核心代码


① 导入excel



string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net
long rows
dw_1.reset()
uo_datawindowex dw
dw = create uo_datawindowex
dw_1.setredraw(false)
ll_Net = GetFileSaveName("请选择文件",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")

rows = dw.ImportExcelSheet(dw_1,ls_pathName,1,0,0)
destroy dw
dw_1.setredraw(true)
MessageBox("提示信息","导入成功 " + string(rows) + "行数据")


② 数据整合


long ll_row,ll_sum1,ll_sum2
long ll_i,ll_j
long ll_yes

string ls_err

//重置表三数据

dw_3.reset()

//处理表一数据
ll_sum1 = dw_1.rowcount( )

if ll_sum1<=0 then
ls_err = "表1 未导入数据,请先导入数据"
goto err
end if

for ll_i=1 to ll_sum1
ll_row = dw_3.insertrow(0)
dw_3.object.num[ll_row] =ll_row                                                          //序号
dw_3.object.name[ll_row]=dw_1.object.name[ll_i]                                 //姓名
dw_3.object.salary[ll_row]=dw_1.object.salary[ll_i]                                //工资
dw_3.object.endowment[ll_row]=dw_1.object.endowment[ll_i]               //养老
dw_3.object.medical[ll_row]=dw_1.object.medical[ll_i]                          //医疗
dw_3.object.injury[ll_row]=dw_1.object.injury[ll_i]                                        //工伤
dw_3.object.unemployment[ll_row]=dw_1.object.unemployment[ll_i]      //失业
dw_3.object.publicacc[ll_row]=dw_1.object.publicacc[ll_i]                      //公积金
dw_3.object.annuity[ll_row]=dw_1.object.annuity[ll_i]                           //年金

next

//处理表二数据

ll_sum2 = dw_2.rowcount( )

if ll_sum2<=0 then
ls_err = "表2未导入数据,请先导入数据"
goto err
end if

for ll_j =1 to ll_sum2
string ls_name
ls_name = dw_2.object.name[ll_j]

ll_yes = dw_3.Find("name = '"+ ls_name +"' ",1,dw_3.rowcount())

if ll_yes<0 then
ls_err = "查找失败!"+SQLCA.SQLErrText
goto err
end if

if ll_yes = 0 then  //没有找到
ll_row = dw_3.InsertRow (0)
dw_3.ScrollToRow(ll_row)
dw_3.object.num[ll_row]                   = ll_row                                                          //序号
dw_3.object.name[ll_row]                 = dw_1.object.name[ll_j]                                 //姓名
dw_3.object.salary[ll_row]                 = dw_1.object.salary[ll_j]                                //工资
dw_3.object.endowment[ll_row]         = dw_1.object.endowment[ll_j]               //养老
dw_3.object.medical[ll_row]              = dw_1.object.medical[ll_j]                          //医疗
dw_3.object.injury[ll_row]                 = dw_1.object.injury[ll_j]                                        //工伤
dw_3.object.unemployment[ll_row]    = dw_1.object.unemployment[ll_j]      //失业
dw_3.object.publicacc[ll_row]            = dw_1.object.publicacc[ll_j]                      //公积金
dw_3.object.annuity[ll_row]               = dw_1.object.annuity[ll_j]                           //年金
end if

if ll_yes >0 then  //找到        
dec{2} ld_salary,ld_endowment,ld_medical,ld_injury,ld_unemployment,ld_publicacc,ld_annuity
ld_salary = dw_3.object.salary[ll_yes] + dw_2.object.salary[ll_j]
ld_endowment =  dw_3.object.endowment[ll_yes] + dw_2.object.endowment[ll_j]
ld_medical = dw_3.object.medical[ll_yes] + dw_2.object.medical[ll_j]
ld_injury = dw_3.object.injury[ll_yes] + dw_2.object.injury[ll_j]
ld_unemployment = dw_3.object.unemployment[ll_yes] + dw_2.object.unemployment[ll_j]
ld_publicacc = dw_3.object.publicacc[ll_yes] + dw_2.object.publicacc[ll_j]
ld_annuity = dw_3.object.annuity[ll_yes] + dw_2.object.annuity[ll_j]

dw_3.object.salary[ll_yes]=  ld_salary                             //工资
dw_3.object.endowment[ll_yes]=ld_endowment               //养老
dw_3.object.medical[ll_yes]=ld_medical                          //医疗
dw_3.object.injury[ll_yes]=ld_injury                                     //工伤
dw_3.object.unemployment[ll_yes]=ld_unemployment   //失业
dw_3.object.publicacc[ll_yes]=ld_publicacc                    //公积金
dw_3.object.annuity[ll_yes]=ld_publicacc                      //年金

end if

next

return 0

err:
messagebox('错误信息',ls_err)

③ excel导出


string ls_err
string ls_pathName,ls_FileName //路径+文件名,文件名
long ll_Net

if dw_3.rowcount() = 0 then
ls_err = "整合数据为空,不能导出"
goto err
end if

uo_wait_box luo_waitbox
luo_waitbox = create uo_wait_box
luo_waitBox.OpenWait(64,RGB(220,220,220),RGB(20,20,20),TRUE,"正在导出 ", 8,rand(6) - 1)

long rows
CreateDirectory("tmp")
uo_datawindowex dw
dw = create uo_datawindowex

ll_Net = GetFileSaveName("选择路径",ls_pathName,ls_FileName,"xlsx","Excel文(*.xlsx),*.xlsx")

rows = dw.ExportExcelSheet(dw_3,ls_pathName,true,true)
destroy dw
destroy luo_waitbox
MessageBox("提示信息","成功导出 " + string(rows) + " 行数据")

return 0

err:
messagebox('错误信息',ls_err)

五、最终效果


财务辅助系统


这次分享就到这吧,★,°:.☆( ̄▽ ̄)/$: .°★ 。希望对您有所帮助,也希望多来几个这样的朋友,不多说了, 蹭饭去了


我们下期再见ヾ(•ω•`)o (●'◡'●)


作者:程序员晓凡
来源:juejin.cn/post/7404036818973245478
收起阅读 »

浅谈“过度封装”

web
干了很多很多所谓的“敏捷”开发的项目之后,对于封装组件有了新的看法,在这里和大家分享一下 为什么要封装组件 封装组件可以复用共通的代码,增加可读性,可以统一UI样式,可以十分方便的管理代码结构。这是所有同学都知道的封装代码的好处,特别是当公司遇到需要“敏捷”开...
继续阅读 »

干了很多很多所谓的“敏捷”开发的项目之后,对于封装组件有了新的看法,在这里和大家分享一下


为什么要封装组件


封装组件可以复用共通的代码,增加可读性,可以统一UI样式,可以十分方便的管理代码结构。这是所有同学都知道的封装代码的好处,特别是当公司遇到需要“敏捷”开发一个项目,封装组件可以帮助我们提高效率(为了绩效)


往往我们就会选择开源的成熟的好用的组件库(element-ui、ant design等)这些组件库帮助我们开发的同时,也帮助我们更加高效的完成任务。


但是每个人对使用组件库的理解都不一样,很多可以使用组件库中的组件的地方自己反而会手动实现,虽然看上去像是实现了效果,但是严重的破坏了代码结构,极大的增加了后续的维护工作量,对于这些封装往往都是“过度封装”


浅谈“过度封装”


“过度封装”在不同的项目组同学中都有不一样的理解,但是很难有一个标准,我封装的这个组件到底算不算“过度封装”呢?



  1. 对与项目中已有的组件做二次封装的封装可以算是“过度封装”

  2. 手动实现一个组件库中存在的类似的组件在项目中使用可以算是“过度封装”


以上是我对一个组件是否是“过度封装”的理解,也可以判断一个方法是不是“过度封装”


对与项目中已有的组件做二次封装的封装可以算是“过度封装”


当我作为后续开发接手一个快要离职的同事的代码时,往往会在components文件夹里看到很多针对element-ui(antd、等其他的组件库)的table组件做的二次封装


image.png
这类的封装往往伴随着一些不够灵活的问题。当一些特殊的页面需要不一样的table设置时,往往需要修改组件的带啊才能支持使用,当这个table支持了很多不同页面的个性化需求之后,大量的props没有一个文档说明后续开发人员要阅读这个封装的组件的源码并熟悉之后快速使用。后续维护产生了大量的工作量,十分不友好。


手动实现一个组件库中存在的类似的组件在项目中使用可以算是“过度封装”


有时候设计稿中出现一个组件和组件库中的很像,但是存在差别的时候,我们需要思考一下,组件库中的组件是否完全支持我们的功能(可以多看看文档上的props,或者打开在线编辑器,调试一下看看),而不是看着有一点点的差异就手动实现一个,比如:tag标签组件、image图像组件等等在项目中基本不去使用,往往直接使用原生标签就手动开发了。


不仅仅是组件当中存在这类问题,封装方法的时候也存在这里问题,明明项目导入了lodash、momentjs、dayjs等库,反而在utils中手动实现formatDate、formatTime、节流防抖、深拷贝等方法,实在令人费解。


关于样式封装


关于组件的样式是最难封装的地方,往往开发到最后每一个文件里面就会出现一大堆的修改样式的代码


image.png


就算是在统一的样式文件中同意修改还是不免出现超长的修改颜色的各种代码


image.png


对于element-ui实在硬伤,摊牌了、我不会了🤷🏻‍♀️


所以我推荐使用naiveUI开发,对于样式的处理绝对的一流,加之vue3使用hooks配合组件,开发体验也很不错😎,(arco Design、antd 现在处理统一的样式风格也是很棒了)


总结


简单聊了一下“过度封装”,希望这种代码不会出现在大家的代码当中,不要去封装 my-button、my-table 这种组件,世界会更加美好。(^▽^)


作者:我的username
来源:juejin.cn/post/7426643406305296419
收起阅读 »

架构师蔡超亲身经历的十年架构感悟分享

一、介绍 最近在学习的时候,看到了蔡超老师自己的十年架构感悟的分享,非常棒,感觉很多内容都是目前实际工作的问题,很不错,今天分享给大家。 蔡超老师已经工作 17 年了, 担任架构师的职位也超过了 10 年,担任过像 HP、Amazon 这样的世界级团队的架构师...
继续阅读 »

一、介绍


最近在学习的时候,看到了蔡超老师自己的十年架构感悟的分享,非常棒,感觉很多内容都是目前实际工作的问题,很不错,今天分享给大家。


蔡超老师已经工作 17 年了, 担任架构师的职位也超过了 10 年,担任过像 HP、Amazon 这样的世界级团队的架构师,也担任过像汇量科技这样快速成长的中小企业的技术领导。Mobvista 技术 VP 兼首席架构师。SpotMax 云服务创始人。
原文视频链接如下:
time.geekbang.org/opencourse/…


二、正文


以下是老师分享的内容:


“提出问题”难于“解决问题”


跟大家分享的第一个感悟是:“提出问题”难于“解决问题”。包括我在内,工程师们最大的一个特点就是善于解决问题,因为我们通常都是从问题解决者的角度来进行工作的。但是,我们很少会主动提出一些问题,主动从用户的场景出发去提出问题、提出需求。


很多时候,公司里的一些矛盾就来自于工程师和产品经理之间,比如我们常常会说产品经理不懂技术,需求提得不够专业。但我们作为工程师也可以想一下,我们是不是应该把自己的位置再往前挪一点,去看看用户到底有哪些困惑,然后提出一个合理的需求去解决它;或者我们自己去体验一下用户的场景,然后提出一个全新的问题并解决它。


简而言之,我们不要仅仅去做一个解决问题的人,也要做一个提出问题的人,主动去思考什么样的问题、需求,能让我们的业务更加先进。


很多时候,我们会觉得设计一个架构、写一个程序去解决问题是一件很难的事情,当然这也是一个很棒的工作。但如果你静下心来去尝试提出一些问题,改进一些用户的需求,你会发现,这是一件更难的事情,至少对我来讲是如此。


当然,这样的感觉不仅仅是我有,很多伟大的科学家也会有这样的困惑和感悟。



“The mere formulation of a problem is far more essential than its solution, which may be merely a matter of mathematical or experimental skills. To raise new questions, new possibilities, to regard old problems from a new angle requires creative imagination and marks real advances in science.”



这是爱因斯坦说过的一段话,大致意思是:我们解决一个问题的时候,常常只需要用到一些数学以及实验的能力就可以了,但提出一个新的问题,以一种新的角度去看待旧的问题,是需要用到我们的创造力才能够做到的,而这恰恰是真正推动科学进步的一部分。


不仅仅是爱因斯坦,软件大师 Frederick P. Brooks Jr.,《人月神话》的作者,在他最新一本讲设计原理的书《The Design of Design(设计原本)》里也谈到,“The hardest part of design is deciding what to design”,大概意思就是,设计最难的部分就是去决定我们要设计什么。


决定“不要什么”比“要做什么”更难


跟大家分享的第二个感悟是:决定“不要什么”比“要做什么”更难。也许因为人性的本质是贪婪的,所以我们在做项目或架构的时候,常常是什么都想要,什么需求都往里放,对于非功能性需求就更是这样了。


我们去看一个架构设计说明书,往往会看到它在开头提到高的可用性、高的性能、高的扩展性、高的可维护性……几乎所有的架构设计书都是这样,这些非功能性需求仿佛成了一个公共的列表,所有的架构都要满足这些需求。当然,功能性需求更是不用说,产品经理会一个不拉地往里面塞。


我们回过头来仔细想想什么是架构。其实在很多层面上,架构是一种 tradeoff,一种权衡和平衡。作为一个架构师,你才最应该是那个说不的人。


在现实中,有很多东西是不可兼得的。比如产品是尽早发布,还是把所有功能都加上,发布一个完美的产品;再比如一致性和性能之间的 balance,我们是选择强一致性,还是选择性能等等。所有架构师都非常熟悉的 CAP 原则,其实本质上就是一个关于 balance 的准则。


因此,作为架构师,我非常推荐大家在做架构设计的一开始,就去确立一些做事的原则。比如数据一致性优先级最高,再比如尽早发布基础功能版本的优先级大于延迟发布完善功能产品等。当出现矛盾的时候,我们就可以利用这些原则来进行取舍。


这些原则是非常重要的,它们能够指导我们在做架构的时候做出正确的取舍,而不会随着工作的推进而迷失。



“Deciding what not to do is as important as deciding what to do.”



这是引自乔布斯的一句话,意思是决定不做什么和决定做什么同样重要。他在取舍方面一直做得非常棒。


可能大家都知道,由于和董事会的关系,乔布斯在 Apple 2 发布一段时间后,就被逐出了苹果。之后他去创办了自己的事业,NeXT,也非常成功。1997 年的时候,苹果收购 NeXT,乔布斯以顾问的形式回到苹果,却发现苹果有很多问题。


当然,那个时候,苹果之所以请他回来做临时 CEO,也是因为意识到自身出现了很多问题。乔布斯意识到苹果最大的问题在于它的产品线非常多,非常的繁杂,他就在白板上画了一个象限图,四个维度分别是 Desktop、Portable、Pro 和 Consumer,并要求苹果在每个象限里面只做一个产品,然后把这个产品做到极致。



“People think focus means saying yes to the thing you’ve got to focus on. But that’s not what it means at all. It means saying no to the hundred other good ideas that there are. You have to pick carefully.I’m actually as proud of the things we haven’t done as the things I have done. Innovation is saying no to 1,000 things.



”这就是乔布斯非常著名的谈专注的那段话,大概意思是,人们认为专注意味着对你需要专注的事情说 Yes。但并非如此,专注意味着你要对其他 100 个好主意说 No,你必须谨慎选择。相比已经完成的工作,他对那些没有完成的工作一样感到自豪。创新就是对 1000 件事情说 No。


非功能性需求决定架构


跟大家分享的第三个感悟是:非功能性需求决定架构。在很多人心目中,做架构的第一步是收集需求,把各种需求都收集上,这个架构的目的就是要满足这些功能性需求的,毕竟最终产品是要为用户服务的。


事实并非如此,一个好的架构,其实是由非功能性需求决定的,而不是由功能性需求决定的。你会发现,一个功能可以有无数的架构方案来实现,但你为什么最终选择了某个方案,其实是由非功能性需求来进行筛选的。


大家非常清楚什么是非功能性需求,包括性能、伸缩性、可扩展性、可维护性等,甚至还包括了你的团队结构,你团队的技术水平,你对发布周期的要求等等,通过所有这些需求来筛选可使用的方案,最终找到一个合适的架构。


所以,非功能性需求是非常重要的,甚至可以说是在你的架构设计中起到决定性因素的。架构设计完之后,少一个功能性需求,我们很容易就能看出来,未来也可以加上去,它对你的架构不会有本质上的影响。但如果我们忽略的是某一种非功能性需求,那在未来这可以说是一种灾难性的麻烦,很有可能你就需要重写了。比如你架构中的数据一致性问题无法解决,或者在设计的时候没有充分考虑性能问题,这样,所有的功能性的实现其实都没有意义。基本就是 Refactor 了,甚至不应该叫 Refactor,要叫 Recreate 或者 Rewrite,等于你要完全重写整个架构。


实际上在架构领域,大家对这点也是有共识的。比如下图中这个 Micro-Kernel 的架构模式来自《面向模式的软件架构》的第一卷,它一大特点就是有比较好的可扩展性,同时通过 Plugin 之间的隔离,能够提高系统的可用性。


s1.png


《面向模式的软件架构》这套书多年来一直是架构师的必读经典,书中很多架构都是从非功能性需求的角度展开去讲的,如果你想成为架构师,那就非常推荐给你去看。


“简单”并不容易


跟大家分享的第四个感悟是:“简单”并不容易。很多架构师都会提到保持简单,keep the simple,但很多时候我们会混淆简单和容易,简单是 simple,容易是 easy,我们是 keep it easy,而不是 keep it simple。


正如乔布斯所说,简单有时候要比复杂更难,需要你对问题、事物的研究非常地深入,你才能找到真正简单的方法。简单其实是蕴含着一种巧妙在其中的。例如我们熟知的布隆过滤器,是一个十分简单的高效重复数据过滤算法,它就非常巧妙地解决了一个问题。如果你想把一个事情做简单,你需要做很多深入的工作,比如对于架构的简化,很大程度上来自于我们对于技术、开发过程,以及不同业务场景的深入理解,而不仅仅是这个架构写起来好不好写。


举个例子,我们来回顾一下软件生命周期中各个阶段的成本消耗占比。


s2.png


可以看到,在整个软件生命周期中,成本消耗最高的并不是设计、编码这些阶段,而是维护阶段。也就是说,如果你让维护变得简单,这会是最有性价比的。


我之前在一家国际公司工作过,主要是为移动运营商设计一个移动设备管理系统,运营商可以通过这个系统实现移动设备的自动注册,固件和软件的同步更新等。当时的移动设备还是摩托罗拉、爱立信之类早期智能手机的时代,打开手机会看到移动菜单或联通菜单,移动运营商就通过这些菜单跟你同步更新,也会对你的系统固件进行升级。这些工作是根据一些管理系统与移动设备之间预定义的协议来完成的,比如 SyncML。而电信专家们会根据业务场景及需求不断调整和新增这些交互协议。


刚开始设计系统的时候,我们也想着 keep it simple,就采用了一种看似简单的实现方式,团队里的软件工程师拿到电信专家设计好的协议后,把协议翻译成对应的程序语言,每一种协议对应一个程序语言。这时候每个程序语言都是一个插件,扩展也很容易,把这个语言实现的 Plugin 插到系统中,或者 Update 一个 Plugin,就可以支持一种新功能了。


这么看你可能觉得还行,反正也是插件结构,看起来也相当简单、直接,于是照着这个设计我们实现了一个系统:任何一个新业务过来,先由电信专家设计协议,再由工程师把协议转换为代码,然后将这个代码写成一个 Server 插件部署到 Server 端,这个协议就被支持了。


但很快,我们就发现事情没那么简单,这套系统的维护成本高到令人发指。为什么?原因其实可以用 Martin Fowler 的一句话来解释:



“I believe that the hardest part of software projects, the most common source of project failure, is communication with the customers and users of that software.



沟通往往是导致软件项目失败的主要问题,的确是这样。


这个系统最大的问题是在上线后的运行维护阶段,电信专家和工程师之间会不断地就新的协议修改和增加进行持续的沟通,但是他们之间的领域知识和词汇都有很大的差别,对彼此专业领域的理解有限,结果就大大影响沟通的效率。这期间系统修改每次都十分艰难,不仅协议更新上线时间慢,而且很多问题由于工程师对于电信协议理解程度有限,都要在开发完成,实际使用后才能被电信专家发现。导致了很多的交换和反复,也造成了很多客户的抱怨。


所以,这个系统只是表面上看起来简单,最终整个过程演变得没那么简单。那什么才是真正的简单?发现上面提到的这些问题,以及背后的原因是沟通后,我们开始重新思考解决的方法。后来我们和电信专家一起设计了一种协议设计语言 DSL,Domain Specific Language。DSL 是用电信专家熟悉的词汇来进行描述的,我们还提供了可视化工具,让电信专家能非常轻松容易地使用。然后这个协议会通过一个类似于编辑器的工具,将电信专家定义好的协议模型转换为内存中的 Java 结构,在线上进行运行。这样整个项目的运行和维护就变得更加简单高效了,省去了低效的交流和不准确人工转换。


这其实并不是一件简单的事情,不论是设计 DSL 语言、做类似编译器的工具,还是构建内存模型,都不简单,但一旦上线,一切就变得简单了。而一开始按电信专家的需求直接实现协议的方法是更为容易的,但是就整个软件生命周期来看,它却不是一个简单高效的方法。


通过这个例子,你应该能体会到什么是简单、什么是容易,两者之间的差别。真正的简单是来自于不容易的,就像那句话说的,It's hard to simple,It's easy to complex,简单是很难的,复杂反而是很容易的


永远不要停止编码


跟大家分享的第五个感悟是:永远不要停止编码。
这一点非常重要,对一个架构师来说,要永远记住自己是一个程序员。作为架构师,我们可能设计了一个非常 high-level 的架构,但代码是软件的最终实现形态,每一个程序员在架构落地过程中的实现,都可能会影响架构的最终呈现。


另外,如果你放弃编码,最大的影响不是说你代码的技术落后了,或者是敲代码变慢了,最大的影响是你会逐渐丧失对编程的敬畏,忘记作为程序员的感受,特别是编码过程中的那些痛苦所在。你会有一些不切实际的幻想,做出一些不切实际的设计,这才是最大的问题。


大家都知道的 Java 之父 James Gosling,他在 Amazon 的职位是 Distinguished Engineer,level 相当于 SVP,而他依旧在坚持编码,每年的代码量是非常惊人的,常常会超过 10 万行。总而言之,作为一个架构师,一旦你开始放弃编码,那你一定要非常小心,因为你可能正在走向一条不归路,一条为大家设计一些充满幻想但又较为虚无的设计的不归路。


风险优先


跟大家分享的第六个感悟是:风险优先。


可以先思考一个问题,我们为什么要做架构设计?


在我看来,架构设计最主要的功能就是转化、降低、避免整个开发过程中的风险。而架构师很大的一个职责就是在早期识别出系统可能存在的风险,并通过你的设计来转换它、去除它。


我们常说的原型方式,或者架构切片的快速迭代方式,其实也是从另一个角度在早期尽量去测试风险,去测试我们的架构能不能解决相关问题,尤其是那些非功能性需求实现的风险,这些风险往往没有功能性需求这么容易在初期被发现,但修正的代价通常要比修正功能性需求大非常多,甚至可能导致项目的失败。


比如敏捷开发,很多人认为敏捷开发就是更快地开发出一个产品,然后快速地 deliver 到市场上,其实这只是敏捷的一部分。另一部分很重要的是,如果一个项目要失败,也要快速地失败,绝对不要把风险放到最后,这也是一种敏捷。这里再给大家推荐一本书《Just Enough Software Architecture(恰如其分的软件架构)》,这是最近非常流行的一本架构书籍,书中强调,架构设计的目的就是为了化解软件实现中的风险。如果你项目中所有的风险都可以通过未来重构来解决的话,那你根本就不需要进行架构设计,直接等着重构就可以了。这也是我非常赞同的观点,风险优先。


从“问题”开始,而不是“技术”


跟大家分享的第七个感悟是:从“问题”开始,而不是“技术”。


作为技术人员,我们非常乐意学习一些新技术,并且学了之后,我们还会非常有热情去应用这个技术。我经常会有这样的感觉,感觉在某一时刻被某个技术上身,特别想去实践它,以至于忽略当前手上的问题用这个技术来解决是不是最合适的,不知道你有没有相同的感觉。


冷静的时候,其实我们每个人都知道,要从实际出发,从需求出发,从用户的问题出发,而不要从技术出发,但在实际工作中,我们却常常不自觉地忽略这一点。就像手里有了一把锤子,看到什么都是钉子。


但其实这样做有很大的害处,这里想给你分享一个故事,来自我之前工作过的一个团队。当时团队里有一个工程师,他维护的是一个非常简单的服务,就是一个利用 MySQL 作为数据存储的简单服务。后来一个他对当时新出的 DynamoDB 产生了兴趣,并学习了相关知识。然后就发生下面的事:使用 DynamoDB 替换了 MySQL,这是一个噩梦的开始。很快发现 DynamoDB 并不能很好的支持事务特性,在当时只有一个性能极差的客户端类库来支持事务,而由于采用客户端方式,引入了大量的额外交互,导致性能差别达 7 倍之多,效率非常之低。


这时候,这个同学就改用了当时在 NoSQL 领域广泛流行的最终一致技术,采用了一个消息队列来实现,这样每一个数据存储对象的改变都会发布一个消息,如果关心这个改变的业务逻辑,就会订阅这个消息,然后改变其他相关的对象,从而实现最终一致。如果过程中出现错误,就会利用消息队列的重试机制。接着发现 DynamoDB 无法提供 SQL 那样方便的查询机制,为了进行数据分析就采用 EMR/MapReduce Job 来完成。大家可以看到实现一样的功能,但是复杂性大大增加,维护工作也由一个人变成了一个团队。


如果让我总结一下这个故事的话,可以说是我们对技术的热情让事情变得复杂,是我们对技术的热情把生活搞得没有那么美好,也让自己的工作更加烦恼。


过度繁忙使你落后


跟大家分享的第八个感悟是:过度繁忙使你落后。


对于 IT 人而言,忙碌成了习惯,加班常挂在嘴边,“996”似乎也变成了公司高效的标志。但有时候我们需要反思一下,有多久没有在业余时间看和技术相关的书了。我之前在公司也问过这个问题,百分之百的人回答我,下班后已经很晚了,回到家基本上没有时间再看书,刷一下手机,就可以直接睡觉了。这是一个非常值得我们去思考的问题。


作为一个技术人,如果你不更新你的知识,或者繁忙让你没有时间更新知识,那会有什么样的结果呢?


给大家分享一个有意思的现象,我遇到过不少程序员,有之前的同事,也有自己的朋友,他们换了一份工作,一开始进入那家公司的时候跟我说,“这个公司我不是特别看好它,我了解一下它的技术,就准备换家公司。”过了两三年我再问他,“你怎么还在这,还没跳走。”结果他回答我,“我看现在的招聘形势不大好,不太好动。”干了几年倒对公司越来越“忠诚”了。


实际情况是,在一个公司没日没夜地干了几年,没有留一点学习时间给自己,忙碌的工作导致他没有时间更新知识,再想回到市场上的时候,却发现自己已经落伍了,连跳槽的能力和勇气都失去了。在这个高速发展的时代,如果因为过度忙碌,导致你没有时间学习和更新自己的知识,那必然会让你落后。即使你不跳槽,呆在同一家公司里,公司的业务不断发展,数据量会越来越大,用户需求会越来越刁钻,你要面对的问题和场景也会越来越复杂,如果你长期不更新知识,掌握的技能没有发生变化,你会觉得越来越难以应付,最终只能通过不断地加班来应对。


另外还有一种可能是,你不更新知识,不深入思考,那么很大概率,你所创造的技术和业务丧失了领先性,没有领先优势,只能被动紧紧跟随竞争对手,而紧紧跟随就意味着你只能加班。试想一下,你要是都领先同行业五年了,还会在乎通过加班来早一个月发布吗?


这其实是一个恶性循环,你花越多的时间去忙碌,就越没有时间去学习,去提高自己的工作技能,就只能靠加班来追赶,结果就更忙碌,更没有时间学习,最终成为一个井底之蛙,陷在恶性循环里无法挣脱。我是一个健身爱好者,练过健身的朋友都知道,光靠锻炼是不行的,营养的补充和锻炼同样重要,你得专门吃一些蛋白粉、补剂之类的。而且越到后面,营养的重要性就越高,至少能跟锻炼达到 50:50 的比重。个人技术成长其实也是一样的,锻炼就好像实践,营养就好像学习。


人们常说 practice makes you perfect,但光 practice 是不行的,还需要坚持学习。


我们在一个领域工作了一段时间,比方说三五年之后,会对这个领域的业务越来越熟悉,解决问题越来越顺手,但相应的,能学到的知识和技能也就会越来越少。有些人会说这是进入了舒适区,要逃离舒适区,换一个领域,我倒觉得不必如此。


本质上来说,换一个领域其实是促进你进一步学习一些新的知识,你在原来的领域也可以这么做。你可以有意识地摆脱那种麻木,挤出时间来重新学习,然后即使做的是相同的事情,也可以用不同的方式更好更高效地完成它。你会发现,即使在同一个领域,你也完全可以做和别人不一样的事情。


所以,每个技术人员都要保证充足的学习时间,否则很容易成为井底之蛙,从而陷入前面提到的低效循环。最后用一句话来跟大家共勉,不忘初心,坚持匠心,谢谢大家。


三、总结



  1. “提出问题”难于“解决问题”。程序员要学会如何从用户的角度发现困难,提出需求问题,适配用户场景。不仅仅是一个解决问题的人,而是提出问题的人,不断地思考什么样的需求问题能让我们的产品更先进。

  2. “设计最难的部分就是去设计我们要设计什么样的问题” - 《设计原本》Brooks

  3. 什么是架构? 从很多层面上说,架构是一种tradeoff,是一种权衡、平衡。作为一个架构师,你应该是那个说“不”的人。决定不要什么比要什么更难。先确定一个大原则,之后在做选择的时候根据这个原则来取舍,这样就不会随着工作的推进而迷失了。

  4. “决定不做什么和决定做什么是一样重要的” - 史蒂夫·乔布斯

  5. “人们认为专注是对你要关注的事情说yes。但是这完全不是专注的本意。专注是对其他一千个好的想法说no。你必须非常谨慎地挑选。实际上我对我拒绝做的事和我做过的事一样感到骄傲。创造力就是对一千件事说NO的能力。” - 史蒂夫·乔布斯

  6. 非功能需求决定架构。所谓的非功能需求,包括性能,伸缩性,可扩展性,可维护性,甚至还包括了你的团队结构、团队技术水平和发布周期的要求。这些因素来筛选可以使用的方案,最终找到一个合适的架构。

  7. 非功能性需求在架构中起决定性作用,因为功能性需求在设计完后,即便未来需要添加新功能,对架构的本质影响不大。但忽略非功能性需求可能会导致灾难性后果,可能需要重写整个系统,比如由于架构问题导致的数据一致性问题或性能问题。建议了解一下Micro-Kernel模式架构。

  8. “简单可以比复杂更难。你必须非常努力地把你的想法想清楚之后,才有可能把事情做得很简单。但这个努力是值得的,因为一旦你达到了这个目的,它会给你带来排山倒海的能量。为了达到真正的简单,你必须思考得足够深入。” -- 史蒂夫·乔布斯

  9. 简单不同于容易。真正的简单恰恰是来自于不容易,复杂才是容易的,简单蕴含了巧妙。

  10. 在软件开发生命周期中,软件发布后的维护占据整个成本的一半以上。如果让一件事情变得简单,后期维护也会变得简单,这是性价比最高的选择。

  11. “我相信软件开发中最难的部分,也是最经常导致失败的部分,是与软件用户的沟通交流。” -- Martin Fowler

  12. 作为一个架构师,你永远都不要停止编码。如果你停止编码,你就会丧失对编写代码的痛苦感知,容易产生不切实际的幻想,进而做出不切实际的设计。(罗胖也坚持自己做启发俱乐部磨练自己的手艺)

  13. 风险优先。架构设计中,你要在早期识别系统可能的风险,通过设计消除或转换这些风险,比如通过原型或架构切片的早期迭代,测试架构是否还存在风险。

  14. 敏捷开发的精髓是,如果项目会失败,那就让它快速失败。

  15. “如果项目中能预见的风险都可以通过重构解决,那么就没必要设计软件架构,你重写就好了。” -- 《恰如其分的软件架构》

  16. 从“问题”开始,而不是“技术”

  17. 过度繁忙使你落后。如果你没有时间更新知识,几年后你容易“被忠诚”,因为失去了跳槽的能力和勇气。随着工作中的问题变复杂,你会发现越来越难应对,只能通过不断加班解决。如果不能紧跟行业发展,就会陷入恶性循环。试想一下,如果你已经领先业界五年了,还在乎休息一个礼拜吗?

  18. 做更好的自己。锻炼配合营养,实践结合学习,才能变得更好。

  19. 要不断学习。你不一定要换领域,但可以用不同的方式做同样的事,并做得更好。

  20. 不忘初心,坚持匠心


作者:爱海贼的无处不在
来源:juejin.cn/post/7426916970662215690
收起阅读 »

虽然炒股赚了十多万,但差点倾家荡产!劝你别入坑

今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。 2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然...
继续阅读 »

今天,五阳哥不打算聊技术,而是聊一下炒股的话题。我自认为在这方面有发言权,自述一个程序员的炒股经历。


2019年,我开始涉足股市,在2021年中旬为了购房,将持有的股票全部卖出,赚了十多万元。在最高峰时期,我获利超过了二十多万元,但后来又回吐了一部分利润。虽然我的炒股成绩不是最出色的,但也超过了很多人。因为大多数股民都是亏损的,能够在股市长期盈利的人真的是凤毛麟角。


股市中普遍流传的七亏二平一赚的说法并不只是传闻,事实上,现实中的比例更加残酷,能够长期赚钱的人可能连10%都达不到。


接下来,我想谈谈我的炒股经历和心路历程,与大家分享一下我的内心体验,为那些有意向或正在炒股的朋友提供一些参考。希望劝退大家,能救一个是一个!


本文倒叙描述,先聊聊最后的疯狂和偏执!


不甘失败,疯狂上杠杆


股市有上涨就有下跌,在我卖出以后,股市继续疯涨了很多。当时长春高新,我是四百一股买入,六百一股就卖出了,只赚了2万。可是在我卖出去的两个月以后,它最高涨到了一千。相当于我本可以赚六万,结果赚了两万就跑了。


我简直想把大腿拍烂了,这严重的影响了我的认知。我开始坚信,这只股票和公司就是好的,非常牛,是我始乱终弃,我不应该早早抛弃人家。 除了悔恨,我还在期盼它下跌,好让我再次抄底,重新买入,让我有重新上车的机会!


终于这只股票后来跌了10%,我觉得跌的差不多了,于是我开始抄底买入!抄底买入的价格在900一股(复权前)。


没想到,这次抄底是我噩梦的开始。我想抄他的底,他想抄我的家!


image.png


这张图,完美的诠释了我的抄底过程。地板底下还有底,深不见底,一直到我不再敢抄底为止。一直抄到,我天天睡不着觉!


当时我九百多一股开始抄底买入,在此之前我都是100股,后来我开始投入更多的资金在这只股票上。当时的我 定下了规矩,鸡蛋不能放在一个篮子里;不能重仓一只股票,要分散投资;这些道理我都明白,但是真到了节骨眼上,我不想输,我想一把赢回来,我要抄底,摊平我的成本。


正所谓:高位加仓,一把亏光。之前我赚的两万块钱,早就因为高位加仓,亏回去了。可是我不甘心输,我想赢回来。当时意识不到也不愿意承认:这就是赌徒心理。


后来这只股票,从1000,跌倒了600,回调了40%。而我已经被深深的套牢。当时我盈利时,只买了1股。等我被套牢时,持有了9股。 按照1000一股,就是九十万。按照600一股,就是54万。


我刚毕业,哪来的那么多钱!


我的钱,早就在800一股的时候,我就全投进去了,我认为800已经算是底了吧,没想到股价很快就击穿了800。


于是我开始跟好朋友借钱。一共借了10万,商量好借一年,还他利息。后来这10万块钱,也禁不住抄底,很快手里没钱了,股价还在暴跌。我已经忘记当时亏多少钱了,我当时已经不敢看账户了,也不敢细算亏了多少钱!


于是,我又开始从支付宝和招商银行借贷,借钱的利率是相当高的,年利息在6%以上。当时一共借了30万。但是股价还不见底,我开始焦虑的睡不着觉。


不光不见底,还在一直跌,我记得当时有一天,在跌了很多以后,股价跌停 -10%。当时的我已经全部资金都投进去了,一天亏了5万,我的小心脏真的要受不了了。跌的我要吐血! 同事说,那天看见我的脸色很差,握着鼠标手还在发抖!


跌成这样,我没有勇气打开账户…… 我不知道什么时候是个头,除了恐惧只有恐惧,每天活在恐惧之中。


我盘算了一下,当时最低点的我,亏了得有二十多万。从盈利六万,一下子到亏二十多万。只需要一个多月的时间。


我哪里经历过这些,投资以来,我都是顺风顺水的,基本没有亏过钱,从来都是挣钱,怎么会成这个样子。


当时的我,没空反思,我只希望,我要赚回来!我一定会赚回来,当时能借的支付宝和招行都已经借到最大额度了…… 我也没有什么办法了,只能躺平。


所以股价最低点的时候,基本都没有钱加仓。


侥幸反弹,但不忍心止盈


股价跌了四个月,这是我人生极其灰暗的四个月。后来因为种种原因,股价涨回来了,当时被传闻的事情不攻自破,公司用实际的业绩证明了自己。


股价开始慢慢回暖,后来开始凶猛的反弹,当时的我一直认为:股价暴跌时我吃的所有苦,所有委屈,我都要股市给我补回来!


后来这段时间,股价最高又回到了1000元一股(复权前)。最高点,我赚了二十多万,但是我不忍心止盈卖出。


我觉得还会继续涨,我还在畅想:公司达到,万亿市值。


我觉得自己当时真的 失了智了。


结婚买房,卖在最高点


这段时间,不光股市顺丰顺水,感情上也比较顺利,有了女朋友,现在是老婆了。从那时起,我开始反思自己的行为,我开始意识到,自己彻彻底底是一个赌徒。


因为已经回本了,也赚了一点钱,我开始不断的纠结要不要卖出,不再炒股了。


后来因为两件事,第一件是我姐姐因为家里要做小买卖,向我借钱。 当时的我,很纠结,我的钱都在股市里啊,借她钱就得卖股票啊,我有点心疼。奈何是亲姐,就借了。


后来我盘算着,不对劲。我还有带款没还呢,一共三十万。我寻思,我从银行借钱收6%的利息,我借给别人钱,我一分利息收不到。 我TM 妥妥的冤大头啊。


不行,我要把带款全部还上,我Tm亏大了,于是我逐渐卖股票。一卖出便不可收拾。


我开始担心,万一股价再跌回去,怎么办啊。我和女朋友结婚时,还要买房,到时候需要一大笔钱,万一要是被套住了,可怎么办啊!


在这这样的焦虑之下,我把股票全部都卖光了!


冥冥之中,自有天意。等我卖出之后的第二周,长春高新开启了下一轮暴跌,而这一轮暴跌之后,直至今日,再也没有翻身的机会。从股价1000元一股,直至今天 300元一股(复权前是300,当前是150元)。暴跌程度大达 75%以上!


image.png


全是侥幸


我觉得我是幸运的,如果我迟了那么一步!假如反应迟一周,我觉得就万劫不复。因为再次开启暴跌后,我又会开始赌徒心理。


我会想,我要把失去的,重新赢回来!我不能现在卖,我要赢回来。再加上之前抄底成功一次,我更加深信不疑!


于是我可能会从1000元,一路抄底到300元。如果真会如此,我只能倾家荡产!


不是每个人都有我这么幸运,在最高点,跑了出去。 雪球上之前有一个非常活泼的用户, 寒月霖枫,就是因为投资长春高新,从盈利150万,到亏光100万本金,还倒欠银行!


然而这一切,他的家人完全不知道,他又该如何面对家人,如何面对未来的人生。他想自杀,想过很多方式了结。感兴趣的朋友可以去 雪球搜搜这个 用户,寒月霖枫。


我觉得 他就是世界上 另一个自己。我和他完全类似的经历,除了我比他幸运一点。我因为结婚买房和被借钱,及时逃顶成功,否则我和他一样,一定会输得倾家荡产!


我觉得,自己就是一个赌狗!


image.png


image.png


然而,在成为赌狗之前,我是非常认真谨慎对待投资理财的!


极其谨慎的理财开局


一开始,我从微信理财通了解到基金,当时2019年,我刚毕业两年,手里有几万块钱,一直存在活期账户里。其中一个周末,我花时间研究了一下理财通,发现有一些债券基金非常不错。于是分几批买了几个债券基金,当时的我对于理财既谨慎又盲目。


谨慎的一面是:我只敢买债券基金,就是年利息在 5%上下的。像股票基金这种我是不敢买的。


盲目的一面是:我不知道债券基金也是风险很大的,一味的找利息最多的债券基金。


后来的我好像魔怔了,知道了理财这件事,隔三差五就看看收益,找找有没有利息更高的债券基金。直到有一天,我发现了一个指数基金,收益非常稳定。


是美股的指数基金,于是我买了1万块钱,庆幸的是,这只指数基金,三个月就赚了八百多,当时的我很高兴。那一刻,我第一次体会到:不劳而获真的让人非常快乐!


如饥似渴的学习投资技巧


经过一段时间的理财,我对于理财越来越熟悉。


胆子也越来越大,美股的指数基金赚了一点钱,我害怕亏回去,就立即卖了。卖了以后就一直在找其他指数基金,这时候我也在看国内 A股的指数基金,甚至行业主题的基金。


尝到了投资的甜头以后,我开始花更多的时间用来 找基。我开始从方方面面评估一只基金。


有一段时间,我特别自豪,我在一个周末,通过 天天基金网,找到了一个基金,这只基金和社保投资基金的持仓 吻合度非常高。当时的我思想非常朴素, 社保基金可是国家队,国家管理的基金一定非常强,非常专业,眼光自然差不了。这只基金和国家队吻合度如此高,自然也差不了。


于是和朋友们,推荐了这只基金。我们都买了这只基金,而后的一个月,这只基金涨势非常喜人,赚了很多钱,朋友们在群里也都感谢我,说我很厉害,投资眼光真高!


那一刻,我飘飘然……


我开始投入更多的时间用来理财。下班后,用来学习的时间也不学习了,开始慢慢的过度到学习投资理财。我开始不停地 找基。当时研究非常深入,我会把这只基金过往的持仓记录,包括公司都研究到。花费的时间也很多。


我也开始看各种财经分析师对于股市的分析,他们会分析大盘何时突破三千点,什么时候股市情绪会高昂起来,什么行业主题会热门,什么时候该卖出跑路了。


总之,投资理财,可以学习的东西多种多样!似乎比编程有趣多了。


换句话说:我上头了


非常荒谬的炒股开局


当时我还是非常谨慎地,一直在投资基金,包括 比较火爆的 中欧医疗创新C 基金,我当时也买了。当时葛兰的名气还很响亮呢。后来股市下行,医疗股票都在暴跌,葛兰的基金 就不行了,有句话调侃:家里有钱用不完,中欧医疗找葛兰。腰缠万贯没人分,易方达那有张坤。


由此可见,股市里难有常胜将军!


当时的我,进入股市,非常荒谬。有一天,前同事偷偷告诉我,他知道用友的内幕,让我下午开盘赶紧买,我忙追问,什么内幕,他说利润得翻五倍。 我寻思一下,看了一眼用友股票还在低位趴着,心动了。于是我中午就忙不迭的线上开户,然后下午急匆匆的买了 用友。 事后证明,利润不光没有翻五倍,还下降了。当然在这之前,我早就跑了,没赚着钱,也没咋亏钱。


当时的我,深信不疑这个假的小道消息,恨不得立即买上很多股票。害怕来不及上车……


自从开了户,便一发不可收拾,此时差2个月,快到2019年底!席卷全世界的病毒即将来袭


这段时间,股市涨势非常好,半导体基金涨得非常凶猛! 我因为初次进入股市,没有历史包袱,哪个股票是热点,我追哪个,胆子非常大。而且股市行情非常好,我更加相信,自己的炒股实力不凡!


换句话说:越来越上头,胆子越来越大。 学习编程,学个屁啊,炒股能赚钱,还编个屁程序。


image.png


刚入股市,就赶上牛市,顺风顺水


2019年底到2020年上半年,A股有几年不遇的大牛市,尤其是半导体、白酒、医疗行业行情非常火爆。我因为初入股市,没有历史包袱,没有锚点。当前哪个行业火爆,我就买那个,没事就跑 雪球 刷股票论坛的时间,比上班的时间还要长。


上班摸鱼和炒股 是家常便饭。工作上虽然不算心不在焉,但是漫不经心!


image.png


在这之前,我投入的金额不多。最多时候,也就投入了10万块钱。当时基金收益达到了三万块。我开始飘飘然。


开始炒股,也尝到了甜头,一开始,我把基金里的钱,逐渐的转移到股市里。当时的我给自己定纪律。七成资金投在基金里,三成资金投在股市里。做风险平衡,不能完全投入到风险高的股市里。


我自认为,我能禁得住 炒股这个毒品。


但是逐渐的,股票的收益越来越高,这个比例很快就倒转过来,我开始把更多资金投在股市中,其中有一只股票,我非常喜欢。这只股票后来成为了很多人的噩梦,成为很多股民 人生毁灭的导火索!


长春高新 股票代码:000661。我在这只股票上赚的很多,后来我觉得股市涨了那么多,该跌了吧,于是我就全部卖出,清仓止盈。 当时的我利润有六万,我觉得非常多了,我非常高兴。


其中 长春高新 一只股票的利润在 两万多元。当时这是我最喜欢的一只股票。我做梦也想不到,后来这只股票差点让我倾家荡产……


当时每天最开心的事情就是,打开基金和证券App,查看每天的收益。有的时候一天能赚 两千多,比工资还要高。群里也非常热闹,每个人都非常兴奋,热烈的讨论哪个股票涨得好。商业互吹成风……


换句话说:岂止是炒股上头,我已经中毒了!


image.png


之后就发生了,上文说的一切,我在抄底的过程中,越套越牢……


总结


以上都是我的个人真实经历。 我没有谈 A 股是否值得投资,也不评论当前的股市行情。我只是想分享自己的个人炒股经历。


炒股就是赌博


我想告诉大家,无论你在股市赚了多少钱,迟早都会还回去,越炒股越上头,赚的越多越上头。


赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。即使你没有遇到长春高新,也会有其他暴跌的股票等着你!


什么🐶皮的价值投资! 谈价值投资,撒泡尿照照自己,你一个散户,你配吗?


漫漫人生路,总会错几步。股市里错几步,就会让你万劫不复!



”把钱还我,我不玩了“




”我只要把钱赢回来,我就不玩了“



这都是常见的赌徒心理,奉劝看到此文的 程序员朋友,千万不要炒股和买基金。


尤其是喜欢打牌、打德州扑克,喜欢买彩-票的 赌性很强的朋友,一定要远离炒股,远离投资!


能救一个是一个!


作者:五阳
来源:juejin.cn/post/7303348013934034983
收起阅读 »

Nginx UI:全新的 Nginx 在线管理平台

前言 Nginx在程序部署中扮演着至关重要的角色,其高性能、高安全性、易于配置和管理的特点,使得它成为现代Web应用部署中不可或缺的一部分。今天大姚给大家分享一款实用的 Nginx Web UI 工具,希望能够帮助到有需要的同学。 工具介绍 Nginx UI一...
继续阅读 »

前言


Nginx在程序部署中扮演着至关重要的角色,其高性能、高安全性、易于配置和管理的特点,使得它成为现代Web应用部署中不可或缺的一部分。今天大姚给大家分享一款实用的 Nginx Web UI 工具,希望能够帮助到有需要的同学。


工具介绍


Nginx UI一个功能丰富、易于使用的 Nginx Web UI 工具,它极大地简化了 Nginx 服务器的管理和配置过程。



主要功能



  • 在线统计:提供服务器指标如 CPU 使用率、内存使用率、负载平均值和磁盘使用率的在线统计。

  • ChatGPT 助手:内置 ChatGPT 助手,提供智能辅助功能。

  • 一键部署和自动续期:支持一键部署 Let's Encrypt 证书,并自动续期。

  • 在线编辑配置:在线编辑 Nginx 配置文件,编辑器支持 Nginx 配置语法高亮。

  • 查看 Nginx 日志:提供在线查看 Nginx 日志的功能。

  • 自动测试和重载:自动测试配置文件并在保存后重载 Nginx。

  • Web 终端:提供 Web 终端访问功能。

  • 暗色模式:支持暗色模式,保护用户视力。

  • 响应式网页设计:确保在不同设备上都能良好显示。



支持语言


支持多语言,包括英语、简体中文、繁体中文等。


在线演示










开源地址



程序员常用的工具软件


该工具已收录到程序员常用的工具软件栏目中,欢迎关注该栏目发现更多优秀实用的开发工具!



github.com/YSGStudyHar…





作者:追逐时光者
来源:juejin.cn/post/7425885062922174498
收起阅读 »

还在用轮询、websocket查询大屏数据?sse用起来

web
常见的大屏数据请求方式 1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新) 2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大...
继续阅读 »

常见的大屏数据请求方式


1、http请求轮询:使用定时器每隔多少时间去请求一次数据。优点:简单,传参方便。缺点:数据更新不实时,浪费服务器资源(一直请求,但是数据并不更新)

2、websocket:使用websocket实现和服务器长连接,服务器向客户端推送大屏数据。优点:长连接,客户端不用主动去请求数据,节约服务器资源(不会一直去请求数据,也不会一直去查数据库),数据更新及时,浏览器兼容较好(web、h5、小程序一般都支持)。缺点:有点大材小用,一般大屏数据只需要查询数据不需要向服务端发送消息,还要处理心跳、重连等问题。

image.png


3、sse:基于http协议,将一次性返回数据包改为流式返回数据。优点:sse使用http协议,兼容较好、sse轻量,使用简单、sse默认支持断线重连、支持自定义响应事件。缺点:浏览器原生的EventSource不支持设置请求头,需要使用第三方包去实现(event-source-polyfill)、需要后端设置接口的响应头Content-Type: text/event-stream

image.png


sse和websocket的区别



  1. websocket支持双向通信,服务端和客户端可以相互通信。sse只支持服务端向客户端发送数据。

  2. websocket是一种新的协议。sse则是基于http协议的。

  3. sse默认支持断线重连机制。websocket需要自己实现断线重连。

  4. websocket整体较重,较为复杂。sse较轻,简单易用。


Websocket和SSE分别适用于什么业务场景?


根据sse的特点(轻量、简单、单向通信)更适用于大屏的数据查询,业务应用上查询全局的一些数据,比如消息通知未读消息等。


根据websocket的特点(双向通信)更适用于聊天功能的开发


前端代码实现


sse的前端的代码非常简单


 const initSse = () => {
const source = new EventSource(`/api/wisdom/terminal/stats/change/notify/test`);

// 这里的stats_change要和后端返回的数据结构里的event要一致
source.addEventListener('stats_change', function (event: any) {
const types = JSON.parse(event.data).types;
});
// 如果event返回的是message 数据监听也可以这样监听
// source.onmessage =function (event) {
// var data = event.data;
// };

// 下面这两个监听也可以写成addEventListener的形式
source.onopen = function () {
console.log('SSE 连接已打开');
};

// 处理连接错误
source.onerror = function (error: any) {
console.error('SSE 连接错误:', error);
};
setSseSource(source);
};

// 关闭连接
sseSource.close();

这种原生的sse连接是不能设置请求头的,但是在业务上接口肯定是要鉴权需要传递token的,那么怎么办呢? 我们可以使用event-source-polyfill这个库


 const source = new EventSourcePolyfill(`/api/wisdom/terminal/stats/change/notify/${companyId}`, {
headers: {
Authorization: sessionStorage.get(StorageKey.TOKEN) || storage.get(StorageKey.TOKEN),
COMPANYID: storage.get(StorageKey.COMPANYID),
COMPANYTYPE: 1,
CT: 13
}
});

//其它的事件监听和原生的是一样

后端代码实现


后端最关键的是设置将响应头的Content-Type设置为text/event-streamCache-Control设置为no-cacheConnection设置为keep-alive。每次发消息需要在消息体结尾用"/n/n"进行分割,一个消息体有多个字段每个字段的结尾用"/n"分割。


var http = require("http");

http.createServer(function (req, res) {
var fileName = "." + req.url;

if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");

interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);

req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");

其它开发中遇到的问题


我在开发调试中用的是umi,期间遇到个问题就是sse连接上了但是在控制台一直没有返回消息,后端那边又是正常发出了的,灵异的是在后端把服务干掉的一瞬间可以看到控制台一下接到好多消息。我便怀疑是umi的代理有问题,然后我就去翻umi的文档,看到了下面的东西:


image.png


一顿操作之后正常


image.png


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

离职后的这半年,我前所未有的觉得这世界是值得的

大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界。 为什么要辞职,一是因为各种社会、家庭层面...
继续阅读 »

大家好,我是一名前端开发工程师,属于是没有赶上互联网红利,但赶上了房价飞涨时代的 95 后社畜。2024 年 3 月份我做了个决定,即使已经失业半年、负收入 10w+ 的如今的我,也毫不后悔的决定:辞职感受下这个世界


为什么要辞职,一是因为各种社会、家庭层面的处境对个人身心的伤害已经达到了不可逆转的程度,传播互联网负面情绪的话我也不想多说了,经历过的朋友懂得都懂,总结来说就是,在当前处境和环境下,已经没有办法感受到任何的快乐了,只剩焦虑、压抑,只能自救;二是我觉得人这一辈子,怎么也得来一次难以忘怀、回忆起来能回甘的经历吧!然而在我的计划中,不辞职的话,做不到。


3 月


在 3 月份,我去考了个摩托车驾-照,考完后购买了一辆摩托车 DL250,便宜质量也好,开始着手准备摩旅。


webwxgetmsgimg.jpg


4 月份正式离职后,我的初步计划是先在杭州的周边上路骑骑练下车技,直接跑长途还是很危险的,这在我后面真的去摩旅时候感受颇深,差点交代了。


4 月


4.19 号我正式离职,在杭州的出租屋里狠狠地休息了一个星期,每天睡到自然醒,无聊了就打打游戏,或者骑着摩托车去周边玩,真的非常非常舒服。


不过在五一之前,我家里人打电话跟我说我母亲生病了,糖尿病引发的炎症,比较严重,花了 2w+ 住院费,也是从这个时候才知道我父母都没有交医保(更别说社保),他们也没有正式、稳定的工作,也没有一分钱存款,于是我立马打电话给老家的亲戚让一个表姐帮忙去交了农村医保。所有这些都是我一个人扛,还有个亲哥时不时问我借钱。


381728547058_ 拷贝.jpg


说实话,我不是很理解我的父母为什么在外打工那么多年,一分钱都存不下来的,因为我从小比较懂事,没让他们操过什么心,也没花过什么大钱。虽然从农村出来不是很容易,但和周围的相同条件的亲戚对比,我只能理解为我父母真的爱玩,没有存钱的概念。


我可能也继承了他们的基因吧?才敢这样任性的离职。过去几年努力地想去改变这个处境,发现根本没用,还把自己搞得心力交瘁,现在想想不如让自己活开心些吧。


5 月


母亲出院后,我回到杭州和摩友去骑了千岛湖,还有周边的一些山啊路啊,累计差不多跑了 2000 多公里,于是我开始确立我的摩旅计划,路线是杭州-海南岛-云南-成都-拉萨,后面实际跑的时候,因为云南之前去过,时间又太赶,就没去云南了。


2024-10-11 103931.jpg


6 月


在摩友的帮助下,给摩托车简单进行了一些改装,主要加了大容量的三箱和防雨的驮包,也配备了一些路上需要的药品、装备,就一个人出发了。


2024-10-11 103949.jpg


从杭州到海南这部分旅行,我也是简单记录了一下,视频我上传了 B 站,有兴趣的朋友可以看看:


拯救焦虑的29岁,考摩托车驾-照,裸辞,买车,向着自由,出发。


摩托车确实是危险的,毕竟肉包铁,即使大部分情况我已经开的很慢,但是仍然会遇到下大雨路滑、小汽车别我、大货车擦肩而过这种危险情况,有一次在过福建的某个隧道时,那时候下着大雨,刚进隧道口就轮胎打滑,对向来车是连续的大货车,打滑之后摩托车不受控制,径直朝向对向车道冲过去,那两秒钟其实我觉得已经完蛋了,倒是没有影视剧中的人生画面闪回,但是真的会在那个瞬间非常绝望,还好我的手还是强行在对龙头进行扳正,奇迹般地扳回来且稳定住了。


过了隧道惊魂未定,找了个路边小店蹲在地上大口喘气,雨水打湿了全身加上心情无法平复,我全身都是抖的,眼泪也止不住流,不是害怕,是那种久违地从人类身体发出的求生本能让我控制不住情绪的肆意发泄。


在国道开久了人也会变得很麻木,因为没什么风景,路况也是好的坏的各式各样,我现在回看自己的记录视频,有的雨天我既然能在窄路开到 100+ 码,真的很吓人,一旦摔车就是与世长辞了。


不过路上的一切不好的遭遇,在克服之后,都会被给予惊喜,到达海南岛之后,我第一次感觉到什么叫精神自由,沿着海边骑行吹着自由的风,到达一个好看的地方就停车喝水观景,玩沙子,没有工作的烦扰,没有任何让自己感受到压力的事情,就像回到了小时候无忧无虑玩泥巴的日子,非常惬意。


稿定设计导出-20241011-112615.jpg


在完成海南环岛之后,我随即就赶往成都,与前公司被裁的前同事碰面了。我们在成都玩了三天左右,主要去看了一直想看的大熊猫🐼!


2024-10-11 174426.jpg


之后我们在 6.15 号开始从成都的 318 起始点出发,那一天的心情很激动,感觉自己终于要做一件不太一样的事,见不一样的风景了。


401728642422_.pic.jpg


小时候在农村,读书后在小镇,大学又没什么经济能力去旅行,见识到的事物都非常有限,但是这一切遗憾在川藏线上彻底被弥补了。从开始进入高原地貌,一路上的风景真的美到我哭!很多时候我头盔下面都是情不自禁地笑着的,发自内心的那种笑,那种快乐的感觉,我已经很久很久很久没有了。


稿定设计导出-20241011-184041.jpg


同样地,这段经历我也以视频的方式记录了下来,有兴趣的朋友可以观看:


以前只敢想想,现在勇敢向前踏出了一步,暂时放下了工作,用摩托跑完了318


到拉萨了!


411728642433_.pic.jpg


花了 150 大洋买的奖牌,当做证明也顺便做慈善了:)


421728642441_.pic_h111d.jpg


后面到拉萨之后我和朋友分开了,他去自驾新疆,我转头走 109 国道,也就是青藏线,这条线真的巨壮美,独自一人行驶在这条路,会感觉和自然融合在了一起,一切都很飘渺,感觉自己特别渺小。不过这条线路因为冻土层和大货车非常非常多的原因,路已经凹凸不平了,许多炮弹坑,稍微骑快点就会飞起来。


这条线还会经过青海湖,我发誓青海湖真的是我看到过最震撼的景色了,绿色和蓝色的完美融合,真的非常非常美,以后还要再去!


2024-10-11 185558.jpg


拍到了自己的人生照片:


2024-10-11 185623.jpg


经历了接近一个半月的在外漂泊,我到了西宁,感觉有点累了,我就找了个顺丰把摩托车拖运了,我自己就坐飞机回家了。


这一段经历对我来说非常宝贵,遇到的有趣的人和事,遭遇的磨难,见到的美景我无法大篇幅细说,但是每次回想起这段记忆我都会由衷地感觉到快乐,感觉自己真的像个人一样活着。


这次旅行还给了我感知快乐和美的能力,回到家后,我看那些原来觉得并不怎么样的风景,现在觉得都很美,而且我很容易因为生活中的小确幸感到快乐,这种能力很重要。


7 月


回到家大概 7 月中旬。


这两个多月的经历,我的身体和心态都调整的不错了,但还不是很想找工作,感觉放下内心的很多执念后,生活还是很轻松的,就想着在家里好好陪陪母亲吧,上班那几年除了过年都没怎么回家。


在家里没什么事,但是后面工作的技能还是要继续学习的,之前工作经历是第一家公司用的 React 16,后面公司用的是 Vue3,对 React 有些生疏,我就完整地看了下 React 18 的文档,感觉变化也不是很大。


8、9 月


虽然放下了许多执念,对于社会评价(房子、结婚、孩子)也没有像之前一样过于在乎了,但还是要生活的,也要有一定积蓄应对未来风险,所以这段时间在准备面试,写简历、整理项目、看看技术知识点、刷刷 leetcode。


也上线了一个比较有意义的网站,写了一个让前端开发者更方便进行 TypeScript 类型体操的网站,名字是 TypeRoom 类型小屋,题源是基于 antfu 大佬的 type-challenges


目前 Type Challenges 官方提供了三种刷题方式



这几种方式其实都很方便,不过都在题目的可读性上有一定的不足,还对开发者有一定的工具负担、IDE 负担。


针对这个问题,也是建立 TypeRoom 的第一个主要原因之一,就是提供直接在浏览器端就能刷题的在线环境,并且从技术和布局设计上让题目描述和答题区域区分开来,更为直观和清晰。不需要额外再做任何事,打开一个网址即可直接开始刷题,并且你的答题记录会存储到云端。


欢迎大家来刷题,网址:typeroom.cn


截屏2024-10-12 21.53.26.png


因为个人维护,还有很多题目没翻译,很多题解没写,也还有很多功能没做,有兴趣一起参与的朋友可以联系我哦,让我一起造福社区!


同时也介绍下技术栈吧:


前端主要使用 Vue3 + Pinia + TypeScript,服务端一开始是 Koa2 的,后面用 Nest 重写了,所以现在服务端为 Nest + Mysql + TypeORM。


另外,作为期待了四年,每一个预告片都看好多遍的《黑神话·悟空》的铁粉,玩了四周目,白金了。


WechatIMG43.jpg


现在


现在是 10 月份了,准备开始投简历找工作了,目前元气满满,不急不躁,对工作没有排斥感了,甚至想想工作还蛮好的,可能是闲久了吧,哈哈哈,人就是贱~


最后


其实大多数我们活得很累,都是背负的东西太多了,而这些大多数其实并不一定要接受的,发挥主观能动性,让自己活得开心些最重要,加油啊,各位,感谢你看到这里,祝你快乐!


这是我的 github profile,上面有我的各种联系方式,想交个朋友的可以加我~❤️


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

为什么JQuery会被淘汰?Vue框架就一定会比JQuery好吗?

web
前言 曾经面试时碰到过一个问题:为什么现有的Vue框架开发可以淘汰之前的JQuery? 我回答:Vue框架无需自己操作DOM,可以避免自己频繁的操作DOM 面试官接着反问我:Vue框架无需自己操作DOM,有什么优势吗,不用操作DOM就一定是好的吗? 我懵了,在...
继续阅读 »

前言


曾经面试时碰到过一个问题:为什么现有的Vue框架开发可以淘汰之前的JQuery?


我回答:Vue框架无需自己操作DOM,可以避免自己频繁的操作DOM


面试官接着反问我:Vue框架无需自己操作DOM,有什么优势吗,不用操作DOM就一定是好的吗?


我懵了,在我的认知里Vue框架无需自己操作DOM性能是一定优于自己来操作DOM元素的,其实并不是的.....


声明式框架与命令式框架


首先我们得了解声明式框架和命令式框架的区别


命令式框架关注过程


JQuery就是典型的命令式框架


例如我们来看如下一段代码


$( "button.continue" ).html( "Next Step..." ).on('click', () => { alert('next') })

这段代码的含义就是先获取一个类名为continue的button元素,它的内容为 Next Step...,并为它绑定一个点击事件。可以看到自然语言描述与代码是一一对应的,这更符合我们做事的逻辑


声明式框架更关注结果


现有的Vue,React都是典型的声明式框架


接着来看一段Vue的代码


<button class="continue" @click="() => alert('next')">Next Step...</button>

这是一段类HTML模板,它更像是直接提供一个结果。至于怎么实现这个结果,就交给Vue内部来实现,开发者不用关心


性能比较


首先告诉大家结论:声明式代码性能不优于命令式代码性能


即:声明式代码性能 <= 命令式代码性能


为什么会这样呢?


还是拿上面的代码举例


假设我们要将button的内容改为 pre Step,那么命令式的实现就是:


button.textContent = "pre Step"

很简单,就是直接修改


声明式的实现就是:


<!--之前 -->
<button class="continue" @click="() => alert('next')">Next Step...</button>
<!--现在 -->
<button class="continue" @click="() => alert('next')">pre Step</button>

对于声明式框架来说,它需要找到更改前后的差异并只更新变化的地方。但是最终更新的代码仍然是


button.textContent = "pre Step"

假设直接修改的性能消耗为 A, 找出差异的性能消耗为 B,
那么就有:



  • 命令式代码的更新性能消耗 = A

  • 声明式代码的更新性能消耗 = A + B


可以看到声明式代码永远要比命令式代码要多出找差异的性能消耗


那既然声明式代码的性能无法超越命令式代码的性能,为什么我们还要选择声明式代码呢?这就要考虑到代码可维护性的问题了。当项目庞大之后,手动完成dom的创建,更新与删除明显需要更多的时间和精力。而声明式代码框架虽然牺牲了一点性能,但是大大提高了项目的可维护性降低了开发人员的心智负担


那么,有没有办法能同时兼顾性能和可维护性呢?
有!那就是使用虚拟dom


虚拟Dom


首先声明一个点,命令式代码只是理论上会比声明式代码性能高。因为在实际开发过程中,尤其是项目庞大之后,开发人员很难写出绝对优化的命令式代码。
而Vue框架内部使用虚拟Dom + 内部封装Dom元素操作的方式,能让我们不用付出太多精力的同时,还能保证程序的性能下限,甚至逼近命令式代码的性能


在讨论虚拟Dom的性能之前,我们首先要说明一个点:JavaScript层面的计算所需时间要远低于Dom层面的计算所需时间 看过浏览器渲染与解析机制的同学应该很明白为什么会这样。


我们在使用原生JavaScript编写页面时,很喜欢使用innerHTML,这个方法非常特殊,下面我们来比较一下使用虚拟Dom和使用innerHTML的性能差异


创建页面时


我们在使用innerHTML创建页面时,通常是这样的:


const data = "hello"
const htmlString = `<div>${data}</div>`
domcument.querySelect('.target').innerHTML = htmlString

这个过程需要先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算 (将字符串赋值给Dom元素的innerHTML属性时会将字符串解析为Dom树)


而使用虚拟Dom的方式通常是编译用户编写的类html模板得到虚拟Dom(JavaScript对象),然后遍历虚拟Dom树创建真实Dom对象


两者比较:


innerHTML虚拟Dom
JavaScript层面运算计算拼接HTML字符串创建JavaScript对象(虚拟Dom)
Dom层面运算新建所有Dom元素新建所有Dom元素

可以看到两者在创建页面阶段的性能差异不大。尽管在JavaScript层面,创建虚拟Dom对象貌似更耗时间,但是总体来说,Dom层面的运算是一致的,两者属于同一数量级,宏观来看可认为没有差异


更新页面时


使用innerHTML更新页面,通常是这样:


//更新
const newData = "hello world"
const newHtmlString = `<div>${newData}</div>`
domcument.querySelect('.target').innerHTML = newHtmlString

这个过程同样是先通过JavaScript层的字符串运算,然后是Dom层的innerHTML的Dom运算。但是它在Dom层的运算是销毁所有旧的DOM元素,再全量创建新的DOM元素


而使用虚拟Dom的方式通常是重新创建新的虚拟Dom(JavaScript对象),然后比较新旧虚拟Dom,找到需要更改的地方并更新Dom元素


两者比较:


innerHTML虚拟Dom
JavaScript层面运算计算拼接HTML字符串创建JavaScript对象(虚拟Dom)+ Diff算法
Dom层面运算销毁所有旧的Dom元素,新建所有新的DOM元素必要的DOM更新

可以看到虚拟DOM在JavaScript层面虽然多出一个Diff算法的性能消耗,但这毕竟是JavaScript层面的运算,不会产生数量级的差异。而在DOM层,虚拟DOM可以只更新差异部分,对比innerHTML的全量卸载与全量更新性能消耗要小得多。所以模板越大,元素越多,虚拟DOM在更新页面的性能上就越有优势


总结


现在我们可以回答这位面试官的问题了:JQuery属于命令式框架,Vue属于声明式框架。在理论上,声明式代码性能是不优于命令式代码性能的,甚至差于命令式代码的性能。但是声明式框架无需用户手动操作DOM,用户只需关注数据的变化。声明式框架在牺牲了一点性能的情况下,大大降低了开发难度,提高了项目的可维护性,且声明式框架通常使用虚拟DOM的方式,使其在更新页面时的性能大大提升。综合来说,声明式框架仍旧是更好的选择


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

每一个失业的前端er都必须有一个稳定盈利的独立开发项目

如题,最近非常焦虑,因为考试临近了,所以只好来祸害一下网友了 俺从2023年离职,经历了考研,独立开发,remote,好几段经历 首先是考研,去年考的其实还行,但还是复试被刷,至今被刷原因未知,盲猜是因为本科是民办三本吧 然后remote就是找了个美国的区块链...
继续阅读 »

如题,最近非常焦虑,因为考试临近了,所以只好来祸害一下网友了


俺从2023年离职,经历了考研,独立开发,remote,好几段经历


首先是考研,去年考的其实还行,但还是复试被刷,至今被刷原因未知,盲猜是因为本科是民办三本吧


然后remote就是找了个美国的区块链公司,但是因为四月份我忙着搞调剂,过程十分煎熬,根本无心顾暇remote那边天天开会的节奏,所以只能离职,当然啦,最终也没调剂上


这都不是重点,重点是独立开发


从我离职到现在,也快两年了,聪明的人已经发现了,整个互联网技术栈这两年可以说毫无变化,新的端没有,新的框架没有,新的红利也没有,新的独角兽公司也没有


道理很简单,因为现在是僧多粥少的时代,每个人手机上就固定几个app,而且都是存量状态(不需要推翻重来,只需要shi山跳舞)


与此同时,还有若干小公司不断倒闭


懂了吧,现在是需求没了,业务没了,招聘的公司没了


独立开发就只不过是,没有业务,我们自己发现制造业务罢了


但是呢,会更难,因为,资本虽然是傻逼,但它们也不是完全没脑子,如果轻易能成功,他们就不需要跑路了


现实就是,我朋友圈有很多独立开发的,推特上也有很多,但能做到稳定盈利的人,几乎为0


有的是卖小册,有的是搞博客,还有开公司做面试辅导的,也有外包接活的,也有收费技术咨询的


这些统统都是噶韭菜——因为我说的很清楚了,现在是业务没了,是需求没了,但凡不制造需求的,都是瞎扯


——所以我把c站卖了,c站转让前日活5w,但是动漫行业实在太卷了,各种各样的竞品,让我自己都不想看番,更别提服务给他人看了


之前在携程,我的老板和我说,你就当独立创业,携程三万人就是你的第一批客户,我觉得老板说的没错,就是比起b端,我更喜欢c端的用户


所以毫无疑问,我不可能再回去写前端框架了,纯粹浪费时间,浪费我的❤


唉,说了这么多,总而言之,言而总之


回到题目,那就是,每个人失业的前端er都必须有一个稳定盈利的独立开发项目


我也在开新坑了,敬请期待~


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

老板想集成地图又不想花钱,于是让我...

web
前言 在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案...
继续阅读 »

前言


在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。


天地图简介


天地图(lbs.tianditu.gov.cn/server/guid…
是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。


具体实现代码


为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。


1. 逆地理编码


逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:


public static MapLocation reverseGeocode(String longitude, String latitude) {
Request request = new Request();
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
String status = jsonObject.getString("status");
if (!"0".equals(status)) {
return null;
}
JSONObject resultObject = jsonObject.getJSONObject("result");
MapLocation mapLocation = new MapLocation();
String formattedAddress = resultObject.getString("formatted_address");
mapLocation.setAddress(formattedAddress);
String locationStr = resultObject.getString("location");
JSONObject location = JSON.parseObject(locationStr);
String lon = location.getString("lon");
String lat = location.getString("lat");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));
lon = String.valueOf(locateInfo.getLongitude());
lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
JSONObject addressComponent = resultObject.getJSONObject("addressComponent");
String address = addressComponent.getString("address");
mapLocation.setName(address);
mapLocation.setCity(addressComponent.getString("city"));
return mapLocation;
}
return null;
}

2. 周边搜索


周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:


public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
Request request = new Request();
String longLat = longitude + "," + latitude;
String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

3. 文本搜索


文本搜索功能允许用户根据关键词搜索地点。实现代码如下:


public static List<MapLocation> searchByText(String query, String mapBound) {
Request request = new Request();
String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");

if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}

4. 坐标系转换


由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:



/**
* WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)
* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用
* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)
* <p>
* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,
* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,
* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。
*/

public class GCJ02_WGS84Utils {

public static double pi = 3.1415926535897932384626;//圆周率
public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴a
public static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方

/**
* 从GPS转高德
* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。
* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。
* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。
*/

public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
} else {
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
}
return info;
}

//从高德转到GPS
public static LocateInfo gcj02_To_Wgs84(double lat, double lon) {
LocateInfo info = new LocateInfo();
LocateInfo gps = transform(lat, lon);
double lontitude = lon * 2 - gps.getLongitude();
double latitude = lat * 2 - gps.getLatitude();
info.setChina(gps.isChina());
info.setLatitude(latitude);
info.setLongitude(lontitude);
return info;
}

// 判断坐标是否在国外
private static boolean isOutOfChina(double lat, double lon) {
if (lon < 72.004 || lon > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
}

//转换
private static LocateInfo transform(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
return info;
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);

return info;
}

//转换纬度所需
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}

//转换经度所需
private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
}

结论


通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。


注意事项



  • 确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。

  • 由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。

  • 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。


通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。


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

凌晨四点,线上CPU告警,绩效没了

前言 凌晨4点,我被一阵刺耳的手机铃声惊醒。迷迷糊糊地摸索着手机,屏幕上赫然显示着"线上CPU告警"的字样。瞬间,我的困意全无,取而代之的是一阵冷汗和心跳加速。作为公司核心系统的负责人,我深知这意味着什么——用户体验受损、可能的数据丢失,更糟糕的是,我的年终绩...
继续阅读 »

前言


凌晨4点,我被一阵刺耳的手机铃声惊醒。迷迷糊糊地摸索着手机,屏幕上赫然显示着"线上CPU告警"的字样。瞬间,我的困意全无,取而代之的是一阵冷汗和心跳加速。作为公司核心系统的负责人,我深知这意味着什么——用户体验受损、可能的数据丢失,更糟糕的是,我的年终绩效可能就此化为泡影。


我迅速起身,开始了一场与时间赛跑的故障排查之旅。


1. 初步诊断:快速定位问题


首先,我登录了服务器,使用top命令查看系统资源使用情况:


$ top

输出显示CPU使用率接近100%,load average远超服务器核心数。这确实是一个严重的问题。


接下来,我使用htop命令获取更详细的进程信息:


$ htop

我发现有几个Java进程占用了大量CPU资源。这些进程正是我们的核心服务。


2. JVM层面分析:寻找热点方法


确定了问题出在Java应用上,我开始进行JVM层面的分析。首先使用jstat命令查看GC情况:


$ jstat -gcutil [PID] 1000 10

输出显示Full GC频繁发生,这可能是导致CPU使用率高的原因之一。


接着,我使用jstack命令生成线程转储,查看线程状态:


$ jstack [PID] > thread_dump.txt

分析thread dump文件,我发现大量线程处于RUNNABLE状态,执行着相似的方法调用。


为了进一步定位热点方法,我使用了async-profiler工具:


$ ./profiler.sh -d 30 -f cpu_profile.svg [PID]

生成的火焰图清晰地显示了一个自定义的排序算法占用了大量CPU时间。


3. 应用层面优化:重构算法


找到了罪魁祸首,我立即查看了相关代码。这是一个用于大量数据的自定义排序算法,原本设计用于小规模数据,但随着业务增长,它的性能问题暴露无遗。


我迅速重构了算法,使用Java 8的并行流进行优化:


List<Data> sortedData = data.parallelStream()
.sorted(Comparator.comparing(Data::getKey))
.collect(Collectors.toList());

同时,我添加了缓存机制,避免重复计算:


@Cacheable("sortedData")
public List<Data> getSortedData() {
// 优化后的排序逻辑
}

4. 数据库优化:索引与查询改进


在排查过程中,我还发现了一些低效的数据库查询。使用explain命令分析SQL语句:


EXPLAIN SELECT * FROM large_table WHERE status = 'ACTIVE';

结果显示这个查询导致了全表扫描。我立即添加了合适的索引:


CREATE INDEX idx_status ON large_table(status);

并重写了部分ORM查询,使用更高效的原生SQL:


@Query(value = "SELECT * FROM large_table WHERE status = :status", nativeQuery = true)
List<LargeTable> findByStatus(@Param("status") String status);

5. 部署优化:资源隔离


为了防止单个服务影响整个系统,我决定使用Docker进行资源隔离。创建了如下的Dockerfile:


FROM openjdk:11-jre-slim
COPY target/myapp.jar app.jar
ENTRYPOINT ["java", "-Xmx2g", "-jar", "/app.jar"]

并使用Docker Compose进行服务编排,限制了CPU和内存使用:


version: '3'
services:
myapp:
build: .
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M

6. 监控告警:防患未然


最后,为了避免类似问题再次发生,我升级了监控系统。使用Prometheus和Grafana搭建了全面的监控平台,并设置了更加智能的告警规则:


- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is above 80% for more than 5 minutes"

结语:危机与成长


经过近4小时的奋战,系统终于恢复了正常。CPU使用率降到了30%以下,服务响应时间也恢复到了毫秒级。


这次经历让我深刻意识到,在追求业务快速发展的同时,我们不能忽视技术债务的累积。定期的代码审查、性能测试和压力测试是必不可少的。同时,建立完善的监控和告警机制,能够帮助我们更快地发现和解决问题。


虽然这次事件可能会影响我的年终绩效,但它带给我的经验和教训是无价的。持续学习和改进永远是我们的必修课。


凌晨的阳台上,我望着渐亮的天空,心中暗自庆幸:又一次化险为夷。但我知道,明天将是新的挑战,我们还有很长的路要走。


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

我终于从不想上班又不能裸辞的矛盾中挣扎出来了

最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。 我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。 相比上面三点,我更希望的是我可以早上起来不用上班,但...
继续阅读 »

最近的状态有一种好像一个泄了气的皮球的感觉一样,就是对生活中很多事情都提不起来兴趣。


我希望自己可以多看一点书,但是我不想动;我希望自己可以练习书法,但是我不想动;我希望自己可以学会一门乐器,但是我不想动。


相比上面三点,我更希望的是我可以早上起来不用上班,但是这只是我的希望而已。


这就是我最近的生活状态。


我有一种我的生活仿佛失去了控制的感觉,每一天我的内心好像都有一个小人在不断呐喊,说我不想上班。因为这个声音,我一度非常非常想要裸辞,但是我为什么没有裸辞呢?


还不是因为我买房买车欠了十几万,我到现在才还了两万而已,再加上我每个月还有房贷要还。


然而,当我经常不情愿地做着跟我心里想法相悖的行为的时候,我发现自己常常会做一些小动作来向自己表达抗议和不满。


比如说,我的工作会变得越来越低效,上班的时候会偷偷地摸鱼,还有就是变得越来越容易拖延。


就好像这样的我,可以让那个不想上班的我,取得了一丢丢的小胜利一样。


一旦开始接受自己没有办法辞职,并且还要上个几十年班这样的结果时,就会让人有一种破罐子破摔的想法。


而且随之而来的是一种对未来,对生活的无力感。


这种无力感渐渐地渗透在我生活的方方面面,以至于让我慢慢地对很多东西都提不起兴趣,我生活中的常态就变成了不想动。


但是有趣的事情发生了,有一天我在和我朋友聊天的时候,我的脑子里面突然出现了一个想法,就是我决定两年之后我要实现我不上班的这个目标。


当有了这个想法之后,我就开始认真思考这件事情的可行度。


通过分析我现在收支情况,我把两年之内改成了2026年之前,因为我觉得这样会让我更加信服这个目标的可行性。


同时我把这个想法也拆分成了两个更为具体的目标,其中一个就是我要在2026年之前还完欠的所有钱。


第二个就是我需要给自己存够20万,这20万是不包括投资理财或者基金股票里面的钱,而是我完全可以自由支配的。


毕竟没有人可以在没有工作的情况下,没有收入的情况下。没有存款的情况下,还能保持一个不焦虑的状态。


当我得出了这两个具体的目标之后,我整个人瞬间被一种兴奋的状态填满,我瞬间找到了工作的意义和动力。


也许你会说,我的这个想法对我现在生活根本起不到任何的改变作用。


我依旧还需要每天七点起床,还是要每天重复地去过我两点一线的生活。


但是于我自己而言,当我给我上班的这件事情加了一个两年的期限之后,我突然觉得我的未来,我的生活都充满了希望。


我整个人从不想动的状态,变成了一种被兴奋的状态填满的感觉。


所以,如果你和我一样有一些类似的困扰,有一些你不想做而又不得不做的事情,让你有一种深陷泥潭,无法前进的感觉,那你不妨试一下这个方法。


结合你自己的实际情况,为你不想做这件事情,设计一个期限,这个期限必须要是你认可,你接受,并且你认为你可以在这个截止时间之前完成的。


我想这个决定应该会对你的生活带来一些改变。


作者:程序员Winn
来源:juejin.cn/post/7428154034480906278
收起阅读 »

35 岁时我改掉的三个习惯

大家好,我是双越老师,wangEditor 作者。 我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。 开始 虽然标题是 35 岁,但其实本文 202...
继续阅读 »

大家好,我是双越老师,wangEditor 作者。



我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。



开始


虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。


35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?


本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。


生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。


1. 戒烟


我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。


我为什么要戒烟呢?


是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。


我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。


还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。


最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。


烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。


关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。


所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。


2. 戒酒


之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。


有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。


我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。


白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。


现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。


啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!


那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。


我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。


3. 不看和自己无关的事情


我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。


但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。


其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?


这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。


更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。


所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。


另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。


总结


35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~


作者:前端双越老师
来源:juejin.cn/post/7417630844100247590
收起阅读 »

处理异常的13条军规

前言 在我们日常工作中,经常会遇到一些异常,比如:NullPointerException、NumberFormatException、ClassCastException等等。 那么问题来了,我们该如何处理异常,让代码变得更优雅呢? 苏三的免费刷题网站:...
继续阅读 »

前言


在我们日常工作中,经常会遇到一些异常,比如:NullPointerException、NumberFormatException、ClassCastException等等。


那么问题来了,我们该如何处理异常,让代码变得更优雅呢?


图片



苏三的免费刷题网站:http://www.susan.net.cn 里面:面试八股文、BAT面试真题、工作内推、工作经验分享、技术专栏等等什么都有,欢迎收藏和转发。



1 不要忽略异常


不知道你有没有遇到过下面这段代码:


反例:


Long id = null;
try {
   id = Long.parseLong(keyword);
catch(NumberFormatException e) {
  //忽略异常
}

用户输入的参数,使用Long.parseLong方法转换成Long类型的过程中,如果出现了异常,则使用try/catch直接忽略了异常。


并且也没有打印任何日志。


如果后面线上代码出现了问题,有点不太好排查问题。


建议大家不要忽略异常,在后续的工作中,可能会带来很多麻烦。


正例:


Long id = null;
try {
   id = Long.parseLong(keyword);
catch(NumberFormatException e) {
  log.info(String.format("keyword:{} 转换成Long类型失败,原因:{}",keyword , e))
}

后面如果数据转换出现问题,从日志中我们一眼就可以查到具体原因了。


2 使用全局异常处理器


有些小伙伴,经常喜欢在Service代码中捕获异常。


不管是普通异常Exception,还是运行时异常RuntimeException,都使用try/catch把它们捕获。


反例:


try {
  checkParam(param);
catch (BusinessException e) {
  return ApiResultUtil.error(1,"参数错误");
}

在每个Controller类中都捕获异常。


在UserController、MenuController、RoleController、JobController等等,都有上面的这段代码。


显然这种做法会造成大量重复的代码。


我们在Controller、Service等业务代码中,尽可能少捕获异常。


这种业务异常处理,应该交给拦截器统一处理。


在SpringBoot中可以使用@RestControllerAdvice注解,定义一个全局的异常处理handler,然后使用@ExceptionHandler注解在方法上处理异常。


例如:


@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 统一处理异常
     *
     * @param e 异常
     * @return API请求响应实体
     */

    @ExceptionHandler(Exception.class)
    public ApiResult handleException(Exception e) {
        if (e instanceof BusinessException) {
            BusinessException businessException = (BusinessException) e;
            log.info("请求出现业务异常:", e);
            return ApiResultUtil.error(businessException.getCode(), businessException.getMessage());
        } 
        log.error("请求出现系统异常:", e);
        return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误,请联系系统管理员!");
    }

}

有了这个全局的异常处理器,之前我们在Controller或者Service中的try/catch代码可以去掉。


如果在接口中出现异常,全局的异常处理器会帮我们封装结果,返回给用户。


3 尽可能捕获具体异常


在你的业务逻辑方法中,有可能需要去处理多种不同的异常。


你可能你会觉得比较麻烦,而直接捕获Exception。


反例:


try {
   doSomething();
catch(Exception e) {
  log.error("doSomething处理失败,原因:",e);
}

这样捕获异常太笼统了。


其实doSomething方法中,会抛出FileNotFoundException和IOException。


这种情况我们最好捕获具体的异常,然后分别做处理。


正例:


try {
   doSomething();
catch(FileNotFoundException e) {
  log.error("doSomething处理失败,文件找不到,原因:",e);
catch(IOException e) {
  log.error("doSomething处理失败,IO出现了异常,原因:",e);
}

这样如果后面出现了上面的异常,我们就非常方便知道是什么原因了。


4 在finally中关闭IO流


我们在使用IO流的时候,用完了之后,一般需要及时关闭,否则会浪费系统资源。


我们需要在try/catch中处理IO流,因为可能会出现IO异常。


反例:


try {
    File file = new File("/tmp/1.txt");
    FileInputStream fis = new FileInputStream(file);
    byte[] data = new byte[(int) file.length()];
    fis.read(data);
    for (byte b : data) {
        System.out.println(b);
    }
    fis.close();
catch (IOException e) {
    log.error("读取文件失败,原因:",e)
}

上面的代码直接在try的代码块中关闭fis。


假如在调用fis.read方法时,出现了IO异常,则可能会直接抛异常,进入catch代码块中,而此时fis.close方法没办法执行,也就是说这种情况下,无法正确关闭IO流。


正例:


FileInputStream fis = null;
try {
    File file = new File("/tmp/1.txt");
    fis = new FileInputStream(file);
    byte[] data = new byte[(int) file.length()];
    fis.read(data);
    for (byte b : data) {
        System.out.println(b);
    } 
catch (IOException e) {
    log.error("读取文件失败,原因:",e)
finally {
   if(fis != null) {
      try {
          fis.close();
          fis = null;
      } catch (IOException e) {
          log.error("读取文件后关闭IO流失败,原因:",e)
      }
   }
}

在finally代码块中关闭IO流。


但要先判断fis不为空,否则在执行fis.close()方法时,可能会出现NullPointerException异常。


需要注意的地方时,在调用fis.close()方法时,也可能会抛异常,我们还需要进行try/catch处理。


最近就业形势比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。


你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。


添加苏三的私人微信:su_san_java,备注:掘金+所在城市,即可加入。


5 多用try-catch-resource


前面在finally代码块中关闭IO流,还是觉得有点麻烦。


因此在JDK7之后,出现了一种新的语法糖try-with-resource。


上面的代码可以改造成这样的:


File file = new File("/tmp/1.txt");
try (FileInputStream fis = new FileInputStream(file)) {
    byte[] data = new byte[(int) file.length()];
    fis.read(data);
    for (byte b : data) {
        System.out.println(b);
    }
catch (IOException e) {
    e.printStackTrace();
    log.error("读取文件失败,原因:",e)
}

try括号里头的FileInputStream实现了一个AutoCloseable接口,所以无论这段代码是正常执行完,还是有异常往外抛,还是内部代码块发生异常被截获,最终都会自动关闭IO流。


我们尽量多用try-catch-resource的语法关闭IO流,可以少写一些finally中的代码。


而且在finally代码块中关闭IO流,有顺序的问题,如果有多种IO,关闭的顺序不对,可能会导致部分IO关闭失败。


而try-catch-resource就没有这个问题。


6 不在finally中return


我们在某个方法中,可能会有返回数据。


反例:


public int divide(int dividend, int divisor) {
    try {
        return dividend / divisor;
    } catch (ArithmeticException e) {
        // 异常处理
    } finally {
        return -1;
    }
}

上面的这个例子中,我们在finally代码块中返回了数据-1。


这样最后在divide方法返回时,会将dividend / divisor的值覆盖成-1,导致正常的结果也不对。


我们尽量不要在finally代码块中返回数据。


正解:


public int divide(int dividend, int divisor) {
    try {
        return dividend / divisor;
    } catch (ArithmeticException e) {
        // 异常处理
        return -1;
    }
}

如果dividend / divisor出现了异常,则在catch代码块中返回-1。


7 少用e.printStackTrace()


我们在本地开发中,喜欢使用e.printStackTrace()方法,将异常的堆栈跟踪信息输出到标准错误流中。


反例:


try {
   doSomething();
catch(IOException e) {
  e.printStackTrace();
}

这种方式在本地确实容易定位问题。


但如果代码部署到了生产环境,可能会带来下面的问题:



  1. 可能会暴露敏感信息,如文件路径、用户名、密码等。

  2. 可能会影响程序的性能和稳定性。


正解:


try {
   doSomething();
catch(IOException e) {
  log.error("doSomething处理失败,原因:",e);
}

我们要将异常信息记录到日志中,而不是保留给用户。


8 异常打印详细一点


我们在捕获了异常之后,需要把异常的相关信息记录到日志当中。


反例:


try {
   double b = 1/0;
catch(ArithmeticException e) {
    log.error("处理失败,原因:",e.getMessage());
}

这个例子中使用e.getMessage()方法返回异常信息。


但执行结果为:


doSomething处理失败,原因:

这种情况异常信息根本没有打印出来。


我们应该把异常信息和堆栈都打印出来。


正例:


try {
   double b = 1/0;
catch(ArithmeticException e) {
    log.error("处理失败,原因:",e);
}

执行结果:


doSomething处理失败,原因:
java.lang.ArithmeticException: / by zero
 at cn.net.susan.service.Test.main(Test.java:16)

将具体的异常,出现问题的代码和具体行数都打印出来。


9 别捕获了异常又马上抛出


有时候,我们为了记录日志,可能会对异常进行捕获,然后又抛出。


反例:


try {
  doSomething();
catch(ArithmeticException e) {
  log.error("doSomething处理失败,原因:",e)
  throw e;
}

在调用doSomething方法时,如果出现了ArithmeticException异常,则先使用catch捕获,记录到日志中,然后使用throw关键抛出这个异常。


这个骚操作纯属是为了记录日志。


但最后发现日志记录两次。


因为在后续的处理中,可能会将这个ArithmeticException异常又记录一次。


这样就会导致日志重复记录了。


10 优先使用标准异常


在Java中已经定义了许多比较常用的标准异常,比如下面这张图中列出的这些异常:图片


反例:


public void checkValue(int value) {
    if (value < 0) {
        throw new MyIllegalArgumentException("值不能为负");
    }
}

自定义了一个异常表示参数错误。


其实,我们可以直接复用已有的标准异常。


正例:


public void checkValue(int value) {
    if (value < 0) {
        throw new IllegalArgumentException("值不能为负");
    }
}

11 对异常进行文档说明


我们在写代码的过程中,有一个好习惯是给方法、参数和返回值,增加文档说明。


反例:


/*  
 *  处理用户数据
 *  @param value 用户输入参数
 *  @return 值 
 */

public int doSomething(String value) 
     throws BusinessException 
{
     //业务逻辑
     return 1;
}

这个doSomething方法,把方法、参数、返回值都加了文档说明,但异常没有加。


正解:


/*  
 *  处理用户数据
 *  @param value 用户输入参数
 *  @return 值
 *  @throws BusinessException 业务异常
 */

public int doSomething(String value) 
     throws BusinessException 
{
     //业务逻辑
     return 1;
}

抛出的异常,也需要增加文档说明。


12 别用异常控制程序的流程


我们有时候,在程序中使用异常来控制了程序的流程,这种做法其实是不对的。


反例:


Long id = null;
try {
   id = Long.parseLong(idStr);
catch(NumberFormatException e) {
   id = 1001;
}

如果用户输入的idStr是Long类型,则将它转换成Long,然后赋值给id,否则id给默认值1001。


每次都需要try/catch还是比较影响系统性能的。


正例:


Long id = checkValueType(idStr) ? Long.parseLong(idStr) : 1001;

我们增加了一个checkValueType方法,判断idStr的值,如果是Long类型,则直接转换成Long,否则给默认值1001。


13 自定义异常


如果标准异常无法满足我们的业务需求,我们可以自定义异常。


例如:


/**
 * 业务异常
 *
 * 
@author 苏三
 * 
@date 2024/1/9 下午1:12
 */

@AllArgsConstructor
@Data
public class BusinessException extends RuntimeException {

    public static final long serialVersionUID = -6735897190745766939L;

    /**
     * 异常码
     */

    private int code;

    /**
     * 具体异常信息
     */

    private String message;

    public BusinessException() {
        super();
    }

    public BusinessException(String message) {
        this.code = HttpStatus.INTERNAL_SERVER_ERROR.value();
        this.message = message;
    }
}

对于这种自定义的业务异常,我们可以增加code和message这两个字段,code表示异常码,而message表示具体的异常信息。


BusinessException继承了RuntimeException运行时异常,后面处理起来更加灵活。


提供了多种构造方法。


定义了一个序列化ID(serialVersionUID)。


作者:苏三说技术
来源:juejin.cn/post/7429267019445387276
收起阅读 »

我开发的一些开发者小工具

web
在 2020 年,我辞职在家,每天都有大把时间。于是,我开始开发一些与开发相关的小工具,目的是解决开发中遇到的问题,或者帮助更深入地理解某些技术概念。 每天写写小工具,时间就这样一天天过去,回想起来,这段经历其实挺有意思的。 刚开始时,这些工具的 UI 确实比...
继续阅读 »

在 2020 年,我辞职在家,每天都有大把时间。于是,我开始开发一些与开发相关的小工具,目的是解决开发中遇到的问题,或者帮助更深入地理解某些技术概念。


每天写写小工具,时间就这样一天天过去,回想起来,这段经历其实挺有意思的。


刚开始时,这些工具的 UI 确实比较简陋。不过随着时间推移,我也在不断改进它们的外观。虽然现在看来可能还是不够精美,但已经有了很大进步。


说实话,这些工具的用户引导和文档都很少,更像是我自己的一个小天地。通过 Google Analytics 的数据,我发现有些工具的使用者可能只有我自己,比如微图床。但正因为我自己在用,即使最近添加新工具的频率减少了,我也一直在维护它们。


令我感到欣慰的是,我把其中一些工具提交到了阮一峰老师的博客,很多小工具都得到了他的推荐。这对我来说是一种莫大的鼓励。


一些与深入原理相关的工具


这些工具旨在帮助开发者更深入地理解一些基础概念和底层原理。


IEEE754 浮点数转换


这个工具可以帮助你理解 IEEE 754 标准中双精度浮点数的内部表示。它能将十进制数转换为对应的二进制表示,并清晰地展示符号位、指数位和尾数位。这对于理解计算机如何处理浮点数非常有帮助。


根据 IEEE754 标准,Infinity 的浮点数转换为:指数位全为 1,尾数位全为 0。


以下是 Infinity 的浮点数转换:


Infinity 的浮点数转换


根据 IEEE754 标准,0 的浮点数转换为:符号位为 0,指数位全为 0,尾数位全为 0。


以下是 0 的浮点数转换:


0 的浮点数转换


UTF-8 编码转换


UTF-8 是一种可变长度的字符编码,这个工具可以帮助你理解 Unicode 字符是如何被编码成 UTF-8 的。你可以输入任何 Unicode 字符,工具会显示其 UTF-8 编码的二进制表示,让你直观地看到编码过程。


UTF-8 编码转换示例


base64 编码转换


Base64 是一种常用的编码方式,特别是在处理二进制数据时。这个工具不仅可以帮助你理解 Base64 编码的原理,还提供了便捷的编码和解码功能。它对于处理需要在文本环境中传输二进制数据的场景特别有用。


base64 编码转换示例


文件类型检测


这个工具可以帮助你理解如何通过文件的魔数(magic number)来判断文件类型。你可以上传一个文件,工具会读取文件的二进制数据,并根据魔数判断文件类型。这在处理未知文件或验证文件类型时非常有用。


比如,JPEG 是因为它的 Magic Number 为 FF D8 FF DB


文件类型检测示例


图片相关


图片处理是 Web 开发中的一个重要方面,以下是一些与图片处理相关的工具。


微图


这是一个快速的图片压缩工具,可以帮助你减小图片文件的大小,而不会显著降低图片质量。


它支持多种图片格式,并且没有文件大小或数量的限制。这个工具对于优化网站加载速度特别有帮助。


最主要的是它借助于前端实现,无需服务器成本,所以你不需要担心隐私问题。它的实现方式与 squoosh 类似,都是借助于 WebAssembly 实现。


微图示例


微图床


这是一个个人图床工具,允许你将 GitHub 仓库用作个人图床。它提供了简单的上传和管理功能,让你可以方便地在文章或网页中引用图片。对于经常需要在线分享图片的开发者来说,这是一个非常实用的工具。


微图床示例


图片分享


这个工具可以帮助你快速生成带有文字的图片,适合用于社交媒体分享或创建简单的海报。它简化了图文组合的过程,让你无需使用复杂的图像编辑软件就能创建吸引人的图片。


图片分享示例


图片占位符


这是一个图片占位符生成工具,可以快速创建自定义尺寸和颜色的占位图片,非常适合在开发过程中使用。它可以帮助你在实际图片还未准备好时,保持页面布局的完整性。


图片占位符示例


编码与加密


在 Web 开发中,我们经常需要处理各种编码和加密。以下是一些相关的工具:


URL 编码


这个工具可以帮助你进行 URL 编码和解码,对于处理包含特殊字符的 URL 非常有用。它可以确保你的 URL 在各种环境中都能正确传输和解析。


HTML 实体编码


HTML 实体编码工具可以帮助你将特殊字符转换为 HTML 实体,确保它们在 HTML 中正确显示。这对于防止 XSS 攻击和确保 HTML 文档的正确渲染都很重要。


哈希生成器


这个工具可以生成多种常用的哈希值,包括 MD5、SHA1、SHA256 等。它在数据完整性验证、密码存储等场景中非常有用。


颜色工具


颜色是 Web 设计中的重要元素,以下是一些与颜色相关的工具:


颜色转换


这个工具可以在 RGB、HSL、CMYK 等不同颜色模型之间进行转换。它可以帮助设计师和开发者在不同的颜色表示方法之间自如切换。


颜色转换示例


调色板生成器


这个工具可以帮助你生成颜色的色调和阴影,非常适合创建一致的颜色主题。它可以让你快速构建和谐的配色方案,提高设计效率。


调色板生成器示例


对比度计算器


这个工具可以计算两种颜色之间的对比度,帮助你确保文本在背景上的可读性。它对于创建符合可访问性标准的设计非常重要。


对比度计算器示例


结语


虽然有些工具可能只有我自己在用,但正是这种持续的学习和创造过程让我感到充实和快乐。


我会继续维护和改进这些工具,也欢迎大家使用并提供反馈。


作者:程序员山月
来源:juejin.cn/post/7426151241470476298
收起阅读 »

35 岁时我改掉的三个习惯

大家好,我是双越老师,wangEditor 作者。 我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。 开始 虽然标题是 35 岁,但其实本文 202...
继续阅读 »

大家好,我是双越老师,wangEditor 作者。



我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。



开始


虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。


35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?


本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。


生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。


1. 戒烟


我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。


我为什么要戒烟呢?


是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。


我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。


还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。


最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。


烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。


关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。


所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。


2. 戒酒


之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。


有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。


我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。


白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。


现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。


啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!


那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。


我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。


3. 不看和自己无关的事情


我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。


但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。


其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?


这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。


更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。


所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。


另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。


总结


35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~


作者:前端双越老师
来源:juejin.cn/post/7417630844100247590
收起阅读 »

尤雨溪成立VoidZero,Rust要一统JavaScript工具链?

web
尤雨溪在Vite Conf 2024上宣布成立公司Void Zero,目前已经完成$460万种子轮融资,由Accel领头,并且有Amplify以及在dev tools领域有丰富经验的创始人参与。 主要目标是搭建下一代JavaScript工具链,实现一套工具覆盖...
继续阅读 »

尤雨溪在Vite Conf 2024上宣布成立公司Void Zero,目前已经完成$460万种子轮融资,由Accel领头,并且有Amplify以及在dev tools领域有丰富经验的创始人参与。 主要目标是搭建下一代JavaScript工具链,实现一套工具覆盖从源码到最终产物的中间过程,例如semantic analysis、transformer、linter、formatter、minifier、boundler等。


image.png


好的工具链不外乎好用, 本文将结合尤雨溪在Vite Conf 2024上分享的内容来介绍什么是下一代JavaScript工具链, 以及好用体现在哪些方面, 最后再上手试验下相关工具看是否真的


Vite工具现状


相信现在大多数前端开发人员的构建工具首选一定是Vite,Vite确实易上手并且快,涵盖了Vue、React、Preact等主流前端框架,也支持TypeScript、Nuxt等。Vite仅需简单的几个指令即可运行起项目:


// 运行create指令,选择前端框架和语言,例如Vue、TypeScript
npm create vite@latest

// Done. Now run:

cd vite-project
npm install
npm run dev

Vite为什么快?


有对比才能体现快,几年前构建前端项目还是使用webpack、Rollup、Parcel等工具,当项目代码指数级增长,这些工具的性能瓶颈愈发明显,动不动需要几分钟才能启动dev server,即使是模块热更新(HMR),文件修改后也需要几秒钟才能反馈到浏览器,这严重影响开发者工作幸福指数


浏览器的快速发展造就了Vite,Vite的优势在两个方面:首先是支持了Native ES Modules,其次是build过程接入了编译型语言(如go、rust)开发的工具。这些优势体现在服务器启动和热更新两个阶段:



  • 服务器启动: Vite将应用中的模块区分为依赖、源码两种,改进了开发服务器启动时间。



    • 依赖:开发时不会变动的纯JavaScript,或者是较大的依赖(上百个模块的组件库),这些代码的处理代价比较高,Vite会使用esbuild预构建这些依赖,由于esbuild使用Go编写,因此比以JavaScript编写的打包器预构建快10-100倍。

    • 源码:对于Vue、JSX等频繁变动的代码文件,Vite以原生ESM方式提供源码,让浏览器接管了打包程序的部分工作,Vite只需要在浏览器请求源码时进行转换并安需提供,也就是需安需导入、安需加载。


    image.png


  • 热更新(HMR)


    在Vite中,HMR是在原生ESM上执行的。当编辑一个文件时,Vite只需要精确地使已编辑Module与其最近的HMR边界之间的链失活,使得无论应用大小如何,HMR能保持快速更新。


    Vite同时利用HTTP头来加速整个页面的重新加载:源码模块请求根据304 Not Modified协商缓存,而预构建的依赖模块请求则通过Cache-Control:max-age=31536000,immutable进行强缓存,因此一旦被缓存将不需要再次请求。



Vite也有缺陷


image.png


Vite当前架构底层依赖于esbuildRollupSWC,三者的作用如下:



  • esbuild: Vite使用esbuild执行依赖项预构建,转化TypeScript、JSX,并且作为生成环境构建的默认minifier。

  • Rollup: Rollup直接基于ES6模块格式,因此能够实现除屑优化(Tree Shaking),然后基于插件生态来支持打包过程的扩展。Vite基于Rollup的插件模板实现插件生态,构建生产环境的bundling chunk和静态资源。

  • SWC: SWC使用Rust语言实现,号称super-fastJavaScript编译器,能够将TypeScript、JSX编译为浏览器支持的JavaScript,编译速度比babel快20倍。Vite主要使用SWC来打包React代码以及实现React代码的HMR。


Vite充分利用esbuild、Rollup、SWC各自的优势来组合成一套打包工具链,虽然对使用者来说是无感的,但对于Vite内部,三套打包框架组合在一起本身就显得比较臃肿。


接下来我们就分析下这一套组合拳会有哪些缺陷:



  • 两套bundling



    虽然esbuild构建非常快,但它的tree shaking以及代码切分不像rollup的配置化那样灵活,插件系统设计的也不尽如人意,因此Vite仅在DEV环境使用esbuild预构建依赖项。rollup正好拟补了ebuild的缺点,比较好的chunck control,以及易配置的tree shaking,因此适合在生成环境打包代码。




  • 生产环境构建速度慢



    由于Rollup基于JavaScript实现,虽然比Webpack快很多,但相比于native工具链,速度就相形见绌了。




  • 比较大的二进制包



    SWC的二进制包有多大?在Mac系统下,达到37MB,比Vite和其依赖项文件总和大了2倍。




  • SWC虽然快,但缺少bundler能力



    SWC有比较完善的transform和minifier,但没有提供可用的bundler,这也就说明了SWC不能直接作为打包工具。




  • 不一致的bundler行为



    DEV环境使用esbuild预构建依赖项,而PROD环境使用rollup构建包,在包含ESM、CJS多模块形式场景下,esbuild和rullup的构建行为存在差异,导致一些仅在线上出现的问题。




  • 低效率的构建管道



    由于混合了JavaScript、Go、Rust三种程序,同一代码片段可能会在不同进程重复执行AST、transform、serialize,并将结果在不同进程间传递。另一方面,在进程间传递大量的代码块本身也会有比较大的开销,特别是传递source map此类文件时开销更大。





总结这些问题,就三点:碎片化、不兼容、低效率。 为了解决这些种种问题,统一的打包工具迫在眉睫,这也是尤雨溪提出Rolldown的主要原因。


image.png


基于Rust的下一代工具链


image.png


VoidZero提出的下一代工具链是什么?下图为 VoieZero规划蓝图,不管是Vue、React、Nuxt还是其他前端框架,一个Vite统统给你搞定,测试框架仅需Vitest即可。Vite底层依赖Rolldown打包器,而打包过程完全交由工具链Oxc负责。实际干活的RolldownOxc都基于Rust实现,因此够快。


image.png


OxcRolldown离正式使用还有一段距离,预计是2025年初投入使用,但这也不妨碍我们先了解下这两个工具让人惊掉下巴的牛,毕竟值460万美金。


Oxc


Oxc作为统一的语言工具链,将提供包含代码检查Linter、代码格式化Formatter、代码打包的组合式NPM包或者Rust包。代码打包过程分为TransformerMinifierResolverParserSemantic Analysis


Oxc官网地址: oxc.rs/ , 目前已经发布了oxlint v0.9.9oxc-transform alpha 版本



  • oxlint v0.9.9检查速度比eslint快50-100倍。oxlint已经在Shopify投入使用,之前使用eslint检查代码需要75分钟/CI,而使用oxlint仅需要10秒钟,你就说快吧!

  • oxc-transform alpha转换速度是SWC的3到5倍, 并且内存占用减少20%, 更小的包体积(2MB vs SWC的37MB). 已实现的主要3个功能:

    • 转换TypeScript至ESNext;

    • 转换React JSX至ESNext,并支持React Refresh;

    • TypeScript DTS 声明;




Oxc目前已完成Parser Linter Resolver,正在快马加鞭地完善Transformer


image.png


Rolldown


Rolldown是基于Rust实现的JavaScript快速打包器,与Rollup API兼容。作为打包器应包含的功能有:



  • Bundling:



    • 阶段1:使用Rolldown替换esbuild,执行依赖项预生成,主要包括多entry的代码切分、cjs/esm混合打包、基础插件支持;

    • 阶段2:Rolldown能够支持生成环境构建,包括命令行和配置文件支持、Treeshaking、Source map、Rollup插件兼容等;



  • Transform: 使用oxc代替esbuild的Transform,尽可能使用同一套AST。核心功能模块Typescript/JSX转换、代码缩减(minification)以及语法降级;

  • 与Vite深度集成: 替换esbuild和rollup,Vite内部核心插件使用Rust实现,提升构建效率。


image.png


Rolldown在性能方面表现如何?官方给出了两个测试。


测试一:打包数量为19k的模块文件,其中10k为React JSX组件,9k为图标js文件。不同打包框架的耗时如下:



  • rolldown: 0.63s

  • rolldown-vite: 1.49s

  • esbuild: 1.23s

  • fram: 2.08s

  • rsbuild: 3.14s



rolldown打包速度比esbuild快了近2倍。



测试二:打包Vue Core代码,基于TypeScript的多包仓库,包含11个包/62个dist bundles,耗时如下:


Vue版本构建框架构建耗时
Vue3.2Rollup + rullup-plugin-typescript2+ terser tsx114s
Vue3.5(main branch)Rollup + rollup-plugin-esbuild + swc minify tsc8.5s
Vue3.5(rolldown branch)Rolldown(tranform+minify) + oxc-transform1.11s


基于rolldown的Vue3.5源代码比Rollup构建快了近8倍。



单从测试数据来看,基于Rust开发的rolldown,打包速度确实带来惊人的提升。以下为下一代Vite的架构概览,预计2025年初发布。


image.png


总结


VoidZero宣称的下一代JavaScript工具链,价值460万美金,其商业价值可见一斑,对于研发个体来说没有明显的感受,但对于大型企业来说,VoidZero能实打实的为企业节省每年几百万的CI构建成本。


VoidZero将清一色的使用Rust来搭建底层构建逻辑,如果能够成型,也证明了Rust在前端构建领域的地位。这也让我们反思,借助于Rust独特的性能和安全性优势,它还能够为前端带来哪些价值?例如WASM支持,基于Tauri、Electon.rs框架的桌面应用,支持Flutter和Dart语言的移动端应用。


究竟VoidZero会为前端领域带来怎样的变革,Vite能不能一统JavaScript工具链,让我们拭目以待吧。



我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!



作者:前端下饭菜
来源:juejin.cn/post/7422404598360948748
收起阅读 »

谈谈在大环境低迷下,找工作和入职三个月后的感受

前言 今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,...
继续阅读 »

前言


今天是新公司入职后的三个多月了个人也是如愿的转正了,对于我个人而言此时的心情好像没有三个月前刚拿到offer那样的喜悦和兴奋了,更像一件很普通的事情在你身边发生了吧。从2023年底离职在到2024年初开始找工作中间休息了三个月,找工作到入职花了一个多月,在这个过程中也是第一次真真切切感受到了所谓大环境低迷下的“前端已死的言论”,也给大家分享一下自己入职三个月的个人感受吧。


从上一家公司离职时的个人感受


因为上一家公司的工作性质是人力外包驻场开发,年底客户公司(中国移动成都产业研究院)我所在的项目组不需要外包人员了,个人也是被迫拿了赔偿灰溜溜的走人了。


工作感受:对于这段工作经历我个人还是比较认可的,毕竟这里没有任何工作压力,也不加班,工作量少,有很多时间去学习新东西,做做自己的开源,认识了新的朋友等等。


学历的重要性:在这里面随便拎一个人出来可能就是研究生学历的国企单位,自己真实的意识到了学历的重要性(第一学历小专科的我瑟瑟发抖)。


和优秀的人共事:如果在一个长期压抑低沉消极的环境下工作无论你的性格在怎么积极乐观开朗,可能也很容易被影响到。相反如果是和在一群积极乐观开朗充满自信的环境和人一起工作,相信你也会变得积极,乐观,自信这或许也是我这一段工作经历最大的收获吧。


2023年底找工作的市场就业环境


抱着试一试的心态在boss上更新了自己的简历状态,不出所料软件上面安静的奇怪ps:49入国军的感觉已读未回可能是很失望的感觉吧,但年底找工作令人绝望的是大多数公司都是未读未回,这也就意味着年底基本上是没有正常公司招聘的了。


大概投了两周简历后终于在智联招聘上约到了一个短期三个月岗位的面试,现场两轮面试通过了,不过最终还是没有选择去。


原因有很多:



  1. 现场的工作环境个人感觉很压抑,从接待我前台和面试官都能感觉满脸写着疲惫

  2. 说公司最近在996,你也需要和我们一起

  3. 招聘岗位和工作内容是threejs开发,薪资却说只能给到普通前端开发的水平

  4. 人力外包公司hr的反复无常令我恶心,二面通过后hr给我打电话最主要就是聊薪资吧,电话内容也很简单hr:成都大部分前端的薪资是在XX-XX,可能给不到你想要的薪资,可能要往下压个1-2K。:我提的薪资完全是在你们发布招聘岗位薪资的区间,既然你们给不到为什么要这样写了(有感到被侮辱了)。过了几天之后人力外包的hr又给我电话,说可以在原来提的薪资基础上加1.4K,希望能早点去客户公司入职。


总结:年底招聘的公司基本上没啥好鸟,如果你的经济能力还行的话让自己放松休息一段时间也是不错的选择


2024年初找工作:真实的感受到了大环境的低迷下的市场行情


印象最深刻的是在疫情时期的2021年,那会儿出来找工作boos上会有很多HR主动给你打招呼,一周大概能五六个面试,大专学历也有机会去自研公司


解封之后本以为市场行情会变得回缓,结果大概就是今年可是未来十年行情最好的一年


简单总结一下2024年的成都就业环境大概这样的:



  1. 只有外包公司会招专科学历

  2. boss上只给hr发一句打招呼的快捷语,99% 都是已读不回

  3. 大多数要完简历之后就没有后续了

  4. 待遇好的公司对于学历的要求更严格了(211,985)

  5. 给你主动打招呼的基本上都是人力外包公司


截至入职新公司前boss上面的投递状况:沟通了1384个岗位,投递了99份简历,一共约到了 8 家公司的面试


image.png


今年找工作的个人感受:不怕面试,就怕没有面试机会


首先说一下个人的一些情况吧,因为在创业小公司待过在技术栈方面个人认为算是比较全面的了


项目经验:做过管理系统(CRM,B2C,ERP,saas等管理系统)、商城和门户网站(响应式,自适应)、移动端(H5,小程序,app)、低代码和可视化(工作流,可视化大屏,3d编辑器)、第三方开发(腾讯IM,企业微信侧边栏)、微前端


项目经历:从0-1搭建过整个大项目的基建工作,封装过项目中很多功能性组件和UI组件二次封装(提高开发效率),接手过屎山代码并重构优化,约定项目的开发规范,处理很多比较棘手的疑难bug和提供相关的技术方案,没有需求概念下的敏捷开发,从0-1的技术调研等


代码方面:写过几个开源项目虽然star数量不多(目前最多一个项目是600+),但在代码规范和可读性方面个人认为还是比较OK的(至少不会写出屎山代码吧)


工作经验(4年):2020毕业至今一直从事前端开发工作


学历:自考本科学历(貌似没啥卵用)


学历确实是我很硬伤的一个点但是没办法,人嘛总归要为年轻时的无知买单吧


在这样的背景下开启了24年的找工作,从2月26号开始投递简历到4月1号拿到offer差不多一个多月左右时间,一共约到了8加公司的面试,平均一周两家公司


大概统计了一下这些公司的面试情况:


公司A:



  1. 数组哪些方法会触发Vue监听,哪些不会触发监听

  2. position 有哪些属性

  3. vue watch和computed的区别,computed和method的区别

  4. vue的watch是否可以取消? 怎么取消?

  5. position:absolute, position:fixed那些会脱离文档流

  6. 如何获取到 pomise 多个then 之后的值

  7. 常见的http状态码

  8. 谈谈你对display:flex 弹性盒子属性的了解

  9. 如何判断一个值是否是数组

  10. typeof 和instanceof的区别

  11. es6-es10新增了那些东西

  12. 离职原因,期望薪资,职业规划


公司B


到现场写了一套笔试题,内容记不清楚了


公司C



  1. vue router 和route 区别

  2. 说说重绘和重排

  3. css 权重

  4. 项目第一次加载太慢优化

  5. 谈谈你对vue这种框架理解

  6. sessionstorage cookie localstorage 区别

  7. 了解过.css 的优化吗?

  8. 闭包

  9. 内存泄漏的产生

  10. 做一个防重复点击你有哪些方案

  11. 解释一些防抖和节流以及如何实现

  12. 说一下你对 webScoket的了解,以及有哪些API

  13. 说一下你对pomise的理解

  14. vue2,vue3 中 v-for 和v-if的优先级

  15. 说说你对canvas的理解


公司D


笔试+面试



  1. vue 首屏加载过慢如何优化

  2. 说说你在项目中封装的组件,以及如何封装的

  3. 后台管理系统权限功能菜单和按钮权限如何实现的

  4. vue 中的一些项目优化

  5. 期望薪资,离职原因,

  6. 其他的记不清楚了


公司E


笔试+面试+和老板谈薪资


1.笔试:八股文


2.面试:主要聊的是项目内容比如项目的一些功能点的实现,和项目的技术点


3.老板谈薪资:首先就是非技术面的常规三件套(离职原因,期望薪资,职业规划),然后就是谈薪资(最终因为薪资给的太低了没有选择考虑这家)


公司F


也是最想去的一家公司,一个偏管理的前端岗位(和面试官聊的非常投缘,而且整个一面过程也非常愉快感受到了十分被尊重)


可惜的是复试的时候因为学历原因,以及一些职业规划和加班出差等方面上没有达到公司的预期也是很遗憾的错过了


一面:



  1. vue 响应式数据原理

  2. 说说es6 promise async await 以及 promise A+规范的了解

  3. 谈谈es6 Map 函数

  4. 如何实现 list 数据结构转 tree结构

  5. webScoke api 介绍

  6. webScoke 在vue项目中如何全局挂载

  7. vuex 和 pinia 区别

  8. 谈谈你对微任务和宏任务的了解

  9. call apply bind 区别

  10. 前端本地数据存储方式有哪些

  11. 数组方法 reduce 的使用场景

  12. 说说你对 css3 display:flex 弹性盒模型 的理解

  13. vue template 中 {{}} 为什么能够被执行

  14. threejs 加载大模型有没有什么优化方案

  15. 离职原因,住的地方离公司有多远,期望薪资

  16. 你有什么想需要了解的,这个岗位平时的工作内容


二面:


1.我看写过一个Express+Mongoose服务端接口的开源项目,说说你在写后端项目时遇到过的难点


2.介绍一下你写的threejs 3d模型可视化编辑器 这个项目


3.以你的观点说一下你对three.js的了解,以及three.js在前端开发中发挥的作用


4.现在的AI工具都很流行,你有没有使用过AI工具来提高你对开发效率


5.说说你认为AI工具对你工作最有帮助的地方是哪些


6.以你的观点谈谈你对AI的看法,以及AI未来发展的趋势


7.你能接受出差时间是多久


8.你是从去年离职的到今天这这几个月时间,你是去了其他公司只是没有写在简历上吗?


9.说说你的职业规划,离职原因,你的优点和缺点,平时的学习方式


公司G


一共两轮面试,也是最终拿到正式offer入职的公司


一面:



  1. 主要就是聊了一下简历上写的项目

  2. 项目的技术难点

  3. 项目从0-1搭建的过程

  4. 项目组件封装的过程

  5. vue2 和 vue3 区别

  6. vue响应式数据原理

  7. 对于typescript的熟练程度

  8. 会react吗? 有考虑学习react吗?

  9. 说一下你这个three.js3d模型可视化编辑器项目的一个实现思路,为什么会想写这样一个项目


二面:



  1. 说说了解的es6-es10的东西有哪些

  2. 说说你对微任务和宏任务的了解

  3. 什么是原型链

  4. 什么是闭包,闭包产生的方式有哪些

  5. vue3 生命周期变化

  6. vue3 响应式数据原理

  7. ref 和 reactive 你觉得在项目中使用那个更合适

  8. 前端跨越方式有哪些

  9. 经常用的搜索工具有哪些?

  10. 谷歌搜索在国内能使用吗?你一般用的翻墙工具是哪种?

  11. 用过ChatGPT工具吗? 有付费使用过吗?

  12. 你是如何看待面试造航母工作拧螺丝螺丝的?

  13. 谈谈你对加班的看法?

  14. 你不能接受的加班方式是什么?

  15. 为什么会选择自考本科?

  16. 你平时的学习方式是什么?

  17. 一般翻墙去外网都会干什么?,外网学习对你的帮助大吗?

  18. 上一家公司的离职原因是什么,期望薪资是多少, 说说你的职业规划

  19. 手里有几个offer?


hr电话:



  1. 大概说了一下面试结果通过了

  2. 然后就是介绍了一下公司的待遇和薪资情况?

  3. 问了一下上一家公司的离职原因以及上一家公司的规模情况?

  4. 手里有几个offer?

  5. 多久能入职?


因为后面没有别的面试了,再加上离职到在到找工作拿到offer已经有四个月时间没有上班了,最终选择了入职这家公司


image.png


入职第三天:我想跑路了!


入职后的第一天,先是装了一下本地电脑环境然后拉了一下项目代码熟悉一下,vue3,react,uniapp 项目都有


崩溃的开始:PC端是一个saas 系统由五个前端项目构成,用的是react umi 的微前端项目来搭建的,也是第一次去接触微前端这种技术栈,要命的是这些项目没有一个是写了readme文档的,项目如何启动以及node.js版本这些只能自己去package.json 文件去查看,在经过一番折腾后终于是把这几个项目给成功跑起来了,当天晚上回家也是专门了解了一下微前端


开始上强度: 入职的第二天被安排做了一个小需求,功能很简单就是改个小功能加一下字段,但是涉及的项目很多,pc端两个项目,小程序两个项目。在改完PC端之后,开始启动小程序项目不出所料又是一堆报错,最终在别的前端同事帮助下终于把小程序项目给启动成功了。


人和代码有一个能跑就行:入职的第三天也从别的同事那里了解到了,之前sass项目组被前端大规模裁员过,原因嘛懂得都懂? 能写出这样一堆屎山代码的人,能不被裁吗?


第一次知道 vue 还可以这样写


image.png


对于一个有代码强迫症的人来说,在以后的很长一段时间里要求优化和接触完全是一堆屎山一样代码,真的是很难接受的


入职一个月:赚钱嘛不寒掺


在有了想跑路的想法过后,也开始利用上班的空余时间又去投递简历了,不过现实就是在金三银四的招聘季,boss上面依旧是安静的可怕,在退一步想可能其他公司的情况也和这边差不多,于是最终还是选择接受了现实,毕竟赚钱嘛不寒掺


入职两个月:做完一个项目迭代过后,感觉好多了


在入职的前一个月里,基本上每天都要加班,原因也很简单:


1.全是屎山的项目想要做扩展新功能是非常困难的


2.整个项目的逻辑还是很多很复杂的只能边写项目边熟悉


3.因为裁了很多前端,新人还没招到,但是业务量没有减少只能加班消化


功能上线的晚上,加班到凌晨3点


image.png


在开发完一个项目迭代过后也对项目有了一些大概的了解,之后的一些开发工作也变得简单了许多


入职三个月:工作氛围还是很重要滴


在入职三个月后,前端组团队的成员也基本上是组建完成了,一共14人,saas项目组有四个前端,虽然业务量依然很多但是好在有更多的人一起分担了,每周的加班时间也渐渐变少了


在一次偶然间了解到我平时也喜欢打篮球后,我和公司后端组,产品组的同事之间也开始变得有话题了,因为大家也喜欢打球,后来还拉了一个篮球群周末有时间大家也会约出来一起打打球


image.png


image.png


当你有存在价值后一切的人情世故和人际关系都会变得简单起来


在这个世界上大多数时候除了你的父母等直系亲属和另一半,可能会对你无条件的付出


其余任何人对你尊重和示好,可能都会存在等价的利益交换吧


尤其是在技术研发的岗位,只有当你能够完全胜任这份工作时,并且能够体现出足够的价值时才能够有足够的话语权


入职三个月后的感受



  1. 公司待遇:虽然是一个集团下面的子公司 (200+人)但待遇只能说一般吧,除了工资是我期望的薪资范围,其他的福利待遇都只能是很一般(私企嘛,懂得都懂)

  2. 工作强度: 听到过很多从大厂来的新同事抱怨说这边的工作量会很大,对我来说其实都还ok,毕竟之前在极端的高压环境下工作过

  3. 工作氛围:从我的角度来讲的话,还是很不错的,相处起来也很轻松简单,大家也有很多共同话题,没有之前在小公司上班那么累


大环境低迷下,随时做好被裁掉的准备


从2020年毕业工作以来,最长的一段工作经历是1年4个月,有过三家公司的经历


裁员原因也很简单:创业小公司和人力外包,要么就是小公司经营问题公司直接垮掉,或者就是人力外包公司卸磨杀驴


除非你是在国企单位上班,否则需要随时做好被裁掉的准备


什么都不怕,就怕太安逸了


这句话出自《我的团长我的团》电视剧里面龙文章故意对几十个过江的日本人围而不歼时和虞啸卿的对话,龙文章想通过这几十个日本人将禅达搅得鸡犬不宁,来唤醒还在沉睡在自己温柔乡的我们,因为就在我们放松警惕时日本人就已经将枪口和大炮对准了我们。


或许大家会很不认同这句话吧,如果你的父母给你攒下了足够的资本完全可以把我刚才的话当做放屁,毕竟没有哪一个男生毕业之前的梦想是车子和房子,从事自己喜欢的工作不好吗? 但现实却是你喜欢工作的收入很难让你在这座城市里体面的生活


于我而言前端行业的热爱更多是因为能够给我带来不错的收入所以我选择了热爱它吧,所以保持终身学习的状态也是我需要去做的吧


前端已死?


前端彻底死掉肯定是不会的,在前后端分离模式下的软件开发前端肯定是必不可少的一个岗位,只不过就业环境恶劣下的情况里肯定会淘汰掉很多人,不过35岁之后我还是否能够从事前端行业真的是一个未知数


结语


选择或者躺平,只是两种不同的生活态度没有对与错,偶尔躺累了起来卷一下也是可以的,偶尔卷累了躺起了休息一下也是不错的。


在这个网络上到处是人均年收入百万以及各种高质量生活的时代,保持独立思考,如何让自己不被负面情绪所影响才是最重要的吧


作者:答案answer
来源:juejin.cn/post/7391065678546157577
收起阅读 »

SQLite这么小众的数据库,到底是什么人在用

前几天在一个群里看到一位同学说:“SQLite这么小众的数据库,到底是什么人在用啊?”首先要说的是 SQLite 可不是小众的数据库,相反,SQLite 是世界上装机量最多的数据库,远超 MySQL,只不过比较低调而已。低调到我想在官网上找一个好看的用来当插图...
继续阅读 »

前几天在一个群里看到一位同学说:“SQLite这么小众的数据库,到底是什么人在用啊?”

首先要说的是 SQLite 可不是小众的数据库,相反,SQLite 是世界上装机量最多的数据库,远超 MySQL,只不过比较低调而已。低调到我想在官网上找一个好看的用来当插图的图片都找不到,只能截一张官网首页来撑一撑,看起来十分朴素。

 我最早听说 SQLite 是刚毕业工作的时候,我们部门做微软内容管理产品的二次开发,其中有一个客户端即时沟通工具叫做 Lync,搭配上 LDAP 的组织架构,其功能就和现在的企业微信差不多。

Lync 支持二次扩展,结合我们的产品需要在其中做一些功能拓展,负责这项工作的是一位厉害的 C++ 大佬。有一次我和他聊起来,我说客户端要记住用户自己的配置和数据,是不是要在目录下放一个配置文件啊,那数据量大了会不会很慢。他说,用配置文件也行,但是咱这个不用配置文件,用 SQLite。

也是孤陋寡闻,那是我第一次听说 SQLite,才知道这也是个数据库,只不过多用在客户端而不是服务器上。

SQLite

SQLite是一个轻量级的嵌入式关系型数据库管理系统。它由D. Richard Hipp在2000年开发,它实现了一个小型、快速、独立、高可靠性、功能齐全的SQL数据库引擎。

SQLite 用C语言开发,最开始的设计目标是嵌入式系统,它可以在不需要单独的服务器进程的情况下,直接嵌入到应用程序中。后来正好赶上智能手机等智能设备普及,正好契合 SQLite 的使用场景,于是大量的智能设备都在使用 SQLite 。这么说吧,你用的手机上,一定有 SQLite 存在。

像 MySQL 一样,SQLite 也是开源且免费的,据官方统计,目前正在使用的 SQLite 数据库超过 1 万亿个。

SQLite 也可以通过配置像MySQL 那样装在服务器上,通过网络连接访问,但是,完全没有必要。

SQLite 支持C、C++、Java、Python、Swift等大多数语言直接使用。

为什么说你的手机上肯定有 SQLite 呢?因为 SQLite 会随着应用程序代码一起打包,所以这样说来,你的手机上还不止一个 SQLite ,可能有很多,例如微信有一个、美团有一个、网易云音乐等等 APP ,都可能包含自己的 SQLite。

使用场景有哪些

移动应用

前面也一直在说手机上的SQLite。Android就默认集成了SQLite作为应用数据存储的标准解决方案。

Apple 的 IOS 其实提供了自己的数据存储方案,比如 CoreData,但是很多开发者都觉得官方提供的方案实在太难用,所以,有很多应用开发者还是选择 SQLite 作为本地存储方案使用。

嵌入式系统

SQLite 本来就是为了嵌入式系统设计的,所以它的特点就是轻量和高性能吗,这也使得他在嵌入式系统中被广泛使用。包括嵌入式Linux设备、物联网(IoT)设备、路由器,以及汽车电子系统等等。 

桌面应用

许多桌面应用程序使用SQLite作为其内部数据库,我第一次听说 SQLite 就是那位同事大佬为了拓展桌面客户端。

尤其是一些纯的本地应用,不需要联网的,所有的配置和数据都会存在本地,这种场景正好适合SQLite 这种轻量级数据库。

数据分析和处理

SQLite还可以用于处理和分析小规模的数据集。例如,数据科学家可以使用SQLite来存储和操作中小型数据集,以进行数据清理、转换和分析。

网站加速

最近看了一篇文章,介绍 Notion 技术团队如何使用WASM SQLite在浏览器中加速Notion 的性能。

WebAssembly (WASM) 是一种低级字节码格式,能够在现代浏览器中高效运行。它被设计为一个可移植的目标,可以被多种编程语言编译成它。 它有接近原生的性能,同时可以安全地运行在浏览器的沙箱环境中。

所以为了追求更好的性能,有些像 Notion 这样的网站直接将 SQLite 编译到 WebAssembly,相当于在网站中加入了 SQLite。

这样一来,更多的数据存到本地 SQLite ,减少不必要的网络交互,对于网站的速度和性能会有很大提升。


作者:古时的风筝
来源:juejin.cn/post/7396563478012051495
收起阅读 »

2024年全面的多端统一开发解决方案推荐!

web
前言 最近在DotNetGuide技术社区交流群看到有不少小伙伴问:有没有一套代码能够同时在多个平台运行的框架推荐?今天大姚给大家分享8个多端统一开发框架其中语言包括C#、C++、Vue、React、Dart、Kotlin等等(一套代码,可以运行到多个平台从而...
继续阅读 »


前言


最近在DotNetGuide技术社区交流群看到有不少小伙伴问:有没有一套代码能够同时在多个平台运行的框架推荐?今天大姚给大家分享8个多端统一开发框架其中语言包括C#、C++、Vue、React、Dart、Kotlin等等(一套代码,可以运行到多个平台从而大幅减轻开发者的开发与维护负担),同学们可以按需选择对应框架(排名不分先后,适合自己的才是最好的)。


使用情况投票统计


微信使用情况投票统计: mp.weixin.qq.com/s/9DNgjTIUX…


image.png


uni-app


uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)、快应用等多个平台。




功能框架图


从下面uni-app功能框架图可看出,uni-app在跨平台的过程中,不牺牲平台特色,可优雅的调用平台专有能力,真正做到海纳百川、各取所长。



为什么要选择uni-app?


uni-app在开发者数量、案例、跨端抹平度、扩展灵活性、性能体验、周边生态、学习成本、开发成本等8大关键指标上拥有更强的优势。



Taro


Taro是一个开放式跨端跨框架解决方案,支持使用 React/Vue/Nerv 等框架来开发微信/京东/百度/支付宝/字节跳动/ QQ 小程序/H5/React Native 等应用。




多端转换支持



Flutter


Flutter是由Google开发的一款开源、跨平台的UI(用户界面)框架,一份代码兼顾 Android、iOS、Web、Windows、macOS 和 Linux 六个平台,编译为原生机器代码,助力提升应用的流畅度并实现优美的动画效果。




主要特性



React Native


React Native由Facebook开发,允许开发者使用JavaScript和React来构建原生体验的移动应用,支持iOS和Android平台。


React Native不仅适用于 Android 和 iOS - 还有社区支持的项目将其应用于其他平台,例如:





Avalonia


Avalonia是一个强大的框架,使开发人员能够使用.NET创建跨平台应用程序。它使用自己的渲染引擎绘制UI控件,确保在Windows、macOS、Linux、Android、iOS和WebAssembly等不同平台上具有一致的外观和行为。这意味着开发人员可以共享他们的UI代码,并在不同的目标平台上保持统一的外观和感觉。



Avalonia 已经成熟且可用于生产,并被 Schneider Electric、Unity、JetBrains 和 GitHub 等公司使用。





.NET MAUI


.NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架,用于使用 C# 和 XAML 创建本机移动和桌面应用。使用 .NET MAUI,您可以从单个共享代码库开发可在 Android、iOS、iPadOS、macOS 和 Windows 上运行的应用程序。





Uno


Uno平台是一个开源平台,用于快速构建单一代码库原生移动、Web、桌面和嵌入式应用程序。它允许 C# 和 WinUI XAML 和/或 C# 代码在所有目标平台上运行,同时允许您控制每个像素。它支持开箱即用的 Fluent、Material 和 Cupertino 设计系统。Uno 平台实现了越来越多的 WinRT 和 WinUI API,例如 Microsoft.UI.Xaml,使 WinUI 应用程序能够以本机性能在所有平台上运行。






Eto.Forms


Eto.Forms是一个.NET开源、跨平台的桌面和移动应用的统一框架,该框架允许开发者使用单一的UI代码库构建在多个平台上运行的应用程序,并利用各自平台的原生工具包,从而使应用程序在所有平台上看起来和工作都像原生应用一样。



支持的平台:支持Windows Forms、WPF、MonoMac和GTK#等桌面平台,以及正在开发中的iOS(使用Xamarin.iOS)和Android(使用Xamarin.Android)移动平台支持(尽管目前尚不完整)。





作者:追逐时光者
来源:juejin.cn/post/7426554951349747762
收起阅读 »

入职第一天,看了公司代码,牛马沉默了

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
继续阅读 »

入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

打开代码发现问题不断

  1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

image.png

image.png

image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

prop_c.setProperty(key, value);

value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
  1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
  1. 日志打印居然sout和log混合双打

image.png

image.png

先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

5.随意更改生产数据库,出不出问题全靠开发的职业素养;

6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

<type>pom

来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

那有什么优点呢:

  1. 不用太怎么写文档
  2. 束缚很小
  3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

解决之道

怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


作者:小红帽的大灰狼
来源:juejin.cn/post/7371986999164928010
收起阅读 »

BOE(京东方)携手雷神联合发布全球首款仿生蜂鸟屏 以全新升级ACR技术引领显示产业高端化的升维发展

2024年10月22日,BOE(京东方)携手雷神重磅发布双方联合研发的全球首款仿生科技蜂鸟屏,该屏幕应用了BOE(京东方)全新升级的ACR(Ambient Contrast Ratio)环境光对比度技术,具有高环境光对比度、低反射率、广色域及防眩光等显著优势,...
继续阅读 »

2024年10月22日,BOE(京东方)携手雷神重磅发布双方联合研发的全球首款仿生科技蜂鸟屏,该屏幕应用了BOE(京东方)全新升级的ACR(Ambient Contrast Ratio)环境光对比度技术,具有高环境光对比度、低反射率、广色域及防眩光等显著优势,这是双方共建京雷显示创新联合实验室后推出的首款标杆性新品,也是双方合作的一个新的里程碑。该产品的成功发布,不仅为广大用户带来前所未有的视觉体验,也充分彰显了BOE(京东方)以创新技术赋能合作伙伴,引领显示产业高端化升维发展的实力。

BOE(京东方)高级副总裁、首席技术官刘志强表示,“长久以来,BOE(京东方)与雷神科技建立了稳固而紧密的合作伙伴关系,双方聚焦于市场洞察及消费者的实际需求,联合打造出一系列实现双方优势互补、引领行业的产品标杆案例。我们联合发布的雷神蜂鸟屏搭载了全球首发的ACR技术及360Hz超高刷新率,可以给用户带来更加流畅、细腻的画面效果,推动整个IT行业向更高端的显示技术标准迈进。未来,我们将继续携手共进,依托京雷显示创新联合实验室这一坚实后盾,共同探索更多创新技术和产品,为用户创造更大的价值。”

相比实验室中的暗光对比度数值,在真实应用场景中,室内常规照明情况下的环境光对比度对用户才更有意义,也是决定用户真实体验的关键指标。BOE(京东方)此次全新升级的ACR技术能够实现200:1的超高环境光对比度,并配合1800:1的高对比度,600nit高亮度,DCI-P3 100%高色域,使屏幕在不同光线条件下均能够呈现出更深邃的黑色及更明亮的白色,为用户带来更加清晰、逼真的显示画面。同时,全新的ACR技术在低反射方面的表现也可圈可点,通过应用定制化低反偏光片材料及工艺技术的提升,实现显示模组反射率及出射光散射率大幅度降低,有效解决了显示产品在不同环境光条件下强反射、刺眼问题,保证用户在不同使用场景下都能够获得出色的观看体验。实测数据显示,搭载BOE(京东方)全新ACR技术的屏幕反射率平均值降低至2.7%,特别是在电竞游戏场景下,显著改善室内顶灯对屏幕带来的反射投影问题,降低外界环境光对屏幕的干扰,提升屏幕信息的可读性,为用户带来更具沉浸式的互动体验。值得关注的是,京雷显示创新联合实验室联合推出的全球首款搭载ACR防眩光 2.5K 360Hz仿生科技雷神蜂鸟屏的电竞本将于10月31日新品开售,让用户可畅享超高画质、超高对比度、超高刷新率的的极致视觉体验。

雷神科技创始人、董事长路凯林表示,“多年来我们与BOE(京东方)紧密合作,共同攻克技术难关,不断探索电竞显示的新境界。雷神蜂鸟屏不仅是京雷显示创新联合实验室的骄傲,也是雷神科技的骄傲。我们相信,这款产品将重新定义电竞显示的标准,也是我们对未来电竞产业发展的一次有力推动。我们期待未来与BOE(京东方)继续携手,深入合作,为用户带来更多令人激动的创新产品。”

作为电竞领域的重要合作伙伴,BOE(京东方)携手雷神科技在十余年间推出了多款全球首发的创新技术和产品,在笔记本、显示器等品类终端领域打造多款行业标杆案例,引领电竞显示发展的风向标。2018年,双方联合研发全球首款16.6英寸笔记本,以用户需求为导向在屏显尺寸优化方面取得全新突破;2023年,双方合作推出全球首发搭载4K HSR双模电竞屏的雷神ZERO旗舰电竞本,再次刷新电竞显示产品新高度。同年,BOE(京东方)还携手雷神共建京雷显示创新联合实验室,在电竞显示领域的技术研发、产品设计等多维度展开全面合作,助力显示技术持续升维。作为全球显示产业的龙头企业,BOE(京东方)始终坚持以“技术+品牌”双价值驱动,从技术、产品、生态等多个方面助力显示产业发展,持续以创新显示技术赋能全球众多一线品牌的首发新品发布,获得了国内外客户的一致支持和好评,充分彰显BOE(京东方)强大的技术实力与行业领导力。

面向未来,BOE(京东方)始终秉持对技术的尊重和对创新的坚持,聚焦显示技术的革新及创新应用,将推出更多具有技术创新性及市场竞争力的产品,满足用户对高端技术与极致体验的美好追求。同时,BOE(京东方)也将秉承“屏之物联”发展战略,携手合作伙伴共创显示产业新高地,积极构建“Powered by BOE”产业价值创新生态,持续推动全产业链的高质量可持续发展。


收起阅读 »

在我硬盘安监控了?纯 JS 监听本地文件的一举一动

web
💰 点进来就是赚到知识点!本文带你用 JS 代码监控本地文件,点赞、收藏、评论更能促进消化吸收! 🚀 想解锁更多 Web 文件系统的技能吗?快来订阅专栏「Web 玩转文件操作」! 📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 Java...
继续阅读 »

💰 点进来就是赚到知识点!本文带你用 JS 代码监控本地文件点赞收藏评论更能促进消化吸收!


🚀 想解锁更多 Web 文件系统的技能吗?快来订阅专栏「Web 玩转文件操作」!


📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 JavaScript 迷弟,Web 技术带给我太多乐趣。如果你也和我一样,欢迎关注私聊



开门见 demo


先来玩玩这个 demo —— 在 Chrome 中监控本地文件夹


20241005-220737.gif


在上面的 demo 中,点击按钮选择一个本地文件夹后,无论是在该文件夹中新增、修改还是删除内容,网页端都能够感知到每一步操作的细节,包括操作时间、操作对象是文件还是文件夹、具体执行了什么操作等等。



如果你的感觉是:”哟?有点儿意思!“ 那么这篇文章就是专门为你而写的,读下去吧。



本专栏的前几篇文章中,我们已经知道,Web 应用能对本地文件进行各种花式操作,例如选择文件/文件夹、增/删/改/查文件等等。网页好像能伸出长长的手臂,穿过浏览器触摸到了用户的本地文件。但你可能还不知道,网页也能长出千里眼、顺风耳,本地文件有什么风吹草动,都能被网页端监控到。如此灵通的耳目,它的名字就是 File System Observer API(文件系统观察者)。


API 简介


现在想象我们要开发一个 Web 端相册应用,展示用户本地文件夹中的图片。我们希望这个相册能实时响应用户的操作,例如增加/删掉几张图片后,无需用户手动在 Web 端刷新,就能自动更新到最新状态。


如果请你来实现自动刷新,阁下又该如何应对?


经典思路可能会是以短时间间隔轮询文件夹状态,读取并缓存每个文件的 lastModified 时间戳,如果前后两次轮询的时间戳发生了变化,再把前后差异更新到 Web 视图中。这种实现方式能达到效果,但还是有一些缺点,比如不能真正做到即时响应,且会有很大的性能问题等。


其实咱们都知道,最优雅高效的做法是仅在文件被操作时触发更新。原生操作系统如 WIndows 和 MacOS 都有这样的文件监听机制,但显然目前 Web 端还无法享受其便利性。除了在用户端,Node.js 应用也面临这样的问题。开发者苦此久矣。


直到 2023 年 6 月,来自谷歌的贡献者们开始推进一项 W3C 提案 —— File System Observer(为方便叙述,下文将简称其为 FSO),旨在从浏览器层面向 Web 应用提供跨平台的文件监听支持。如果这项提案能够顺利进入 ECMAScript 标准,那么 Web 文件系统的又一块重要功能版图将得以补全,Web 生态将会变得更友好、更强大。


解锁尝鲜:加入 Origin Trial


FSO 还是一套崭新的 API,有多新呢?MDN 和 CanIUse 中还没有建立关于它的词条。但这并不意味着我们完全无法用于生产环境 —— 正如你在本文开头的 demo 中体验到的,我已经用到线上功能中了。只要做一点配置工作,你和你的用户就能成为全球第一批享受到 FSO 的人 😎。


Chrome 已经对 FSO 开启了试用,版本范围是 129 到 134,你可以为你的 Web App 域名注册一个试用 token,你可以跟着我一步一步操作:


首先我们访问 developer.chrome.com/origintrial… 并登录账号。


20241006-163407.jpeg


点击界面下方的「REGISTER」按钮,进入表单页:


20241006-163919.jpeg


按照上图的标注填写信息。每一个域名都需要单独注册一次。例如我本地开发调试时用的是localhost:3000,而线上域名是 rejax.fun,那么就需要给这两个域名分别走一遍 REGISTER 流程。


填写信息后提交表单,你会得到一串字符串 token:


20241006-164840.jpeg


将 token 复制出来,写到 Web App 的 html 文件中,像这样:


<meta http-equiv="origin-trial" content="把 token 粘贴到这里" />

或者用 JavaScript 动态插入:


const meta = document.createElement('meta')
meta.httpEquiv = 'origin-trial'
meta.content = token
document.head.appendChild(meta)

最后,在 Chrome 中打开你注册的域名所在的页面,在 Console 中输入 FileSystemObserver 并回车:


20241006-165520.jpeg


如果打印出了「native code」而不是「undefined」,那么恭喜,你已经成功解锁了 FSO 试用!


监听一个文件


有了试用资格,我们来监听一个文件,边调试代码边研究 FSO 的设计和实现。


实例化


上一小节的最后,我们用来测试是否解锁成功的 FileSystemObserver 就是 FSO 的构造函数,它接收一个回调函数作为参数。我们可以像这样实例化一个观察者:


function callback (params) {
console.log(params)
}
const observer = new FileSystemObserver(callback)

callback 函数会在被监听的文件发生变动时被执行,所以我们可以把响应变动的业务处理逻辑放在其中。


绑定目标文件


实例 observer 有一个 observe 方法,它接收两个参数。第二个参数暂且按下不表,我们先专心看第一个参数。


这个参数是一个 FileSystemHandle 格式的对象,代表着本地文件在 JavaScript 运行时中的入口。我们可以通过 showOpenFilePicker 来选择一个文件(假如我们选择了文件 a.js),并获取到对应的 FileSystemHandle


const [fileHandle] = await window.showOpenFilePicker()
observer.observe(fileHandle)


如果你想看 FileSystemHandleshowOpenFilePicker 的详解,可以移步至本专栏的上一篇文章谁也别拦我们,网页里直接增删改查本地文件!



调用 observe 方法后,这个文件就算是进入了我们的监控区域 📸 了,直到我们主动解除监听或者网页被关闭/刷新。


监听文件操作


当我们编辑文件 a.js 的内容时,给 observe() 传入的回调函数被调用,并且会接收到两个参数,第一个是本次的变动记录 records,第二个是实例 observer 本身。我们打印 records 可以看到如下结构:


20241006-201248.jpeg


records 是一个数组,其元素是 FileSystemChangeRecord 类型的对象,我们重点关注以下几个属性:



  • changedHandle:可以理解为这就是我们绑定的文件。

  • type:变动类型,可取值及对应含义如下:


    type 值含义
    appeared新建文件,或者移入被监听的根目录
    disappeared文件被删除,或者移出被监听的根目录
    modified文件内容被编辑
    moved文件被移动
    unknown未知类型
    errored出现报错



一般情况下,如果我们监听的是单个文件而不是一个目录,那么无论是把文件移走、重命名、删除, record 中的 type 值都会是 disappeared。


监听一个文件夹


监听文件夹的方式和监听文件类似,我们先用 showDirectoryPicker 选择一个文件夹(以文件夹 foo 为例),再把 DirectoryHandle 传入 observe 方法。



为方便描述,我们假设文件夹 foo 的结构如下:


/foo


├── 文件夹 dir1


├── 文件夹 **dir2**


└── 文件 a.js



const dirHandle = await window.showDirectoryPicker()
observer.observe(dirHandle)

与文件有所不同的是,文件夹会有子文件夹和子文件,这是一个树形结构。如果我们只想监听 foo 下面的一级子内容,那么使用像上方代码块那样的调用方式就可以了。但如果我们想密切掌控每一子级的变动,就需要额外的配置参数,也就是前文提到的第二个参数:


observer.observe(dirHandle, {
recursive: true
})

此时你可以在 foo 文件夹里面任意增、删、改子文件或文件夹,一切操作都能在回调函数里以 record 的形式被捕获到。子文件和子文件夹所支持的操作类型,record 值也具有相同结构,因此接下来我们从监听子文件的视角来观察 FSO。


监听子文件


创建和移入、删除和移出 a.js 的情况,record.type 的值分布如下:


文件移入 foo在 foo 中创建文件文件从 foo 中移出删除文件
appearedappeareddisappeareddisappeared

其中移出和删除的表现,与监听单文件的情况是相同的。


我们来试试把 a.js 移到与它同级的文件夹 dir1 中,看看会得到怎样的 record


20241006-211746.jpeg


有几个点值得我们注意:



  • type 的值是 moved,说明只要 a.js 还在 foo 内,不管处于第几层,都不会触发 type: appeared/disappeared

  • relativePathMovedFrom 是一个单元素数组,它代表移动前 a.js 的文件路径

  • relativePathComponents 有两个数组元素,代表被移动文件的新路径是 dir1/a.js


但重命名子文件和监听单文件时不同。例如我们将 a.js 更名为 b.js,会监听到如下 record


20241006-210550.jpeg


我们本以为 type 的值是 renamed,但其实是 moved,确实有点反直觉。从 record 上来看,与真正的移动操作相比,重命名的不同之处在于:



  • changedHandle 指向了重命名后的新文件 b.js

  • relativePathMovedFromrelativePathComponents 分别包含的是旧名和新名


FSO 在状态设计上并没有直接定义一个重命名状态,但我们可以自己来区分。重命名的响应数据有这样的特征:



  • relativePathMovedFromrelativePathComponents 这两个数组的 length 一定相等

  • 除了最后一个元素,两个数组的其他元素一定是一一对应相等的


因此我们可以这样判断重命名操作:


const { oldList: relativePathMovedFrom, newList: relativePathComponents } = recors[0]
let operation = '是常规的移动操作'
// 重命名前后,文件的目录路径没变,只是文件名变了
if (oldList.length === newList.length) {
const len = newList.length
for (let i = 0; i < len; i++) {
// 相同序号的新旧路径是否一样
const isEqual = newList[i] === oldList[i]
if (i < len - 1) {
if (!isEqual) break
} else if (!isEqual) {
operation = '是重命名操作,不是移动操作'
}
}
}

至此,我们已经摸清了如何监听子文件上的不同操作,除了监听单文件部分已经覆盖到的内容,增量知识点仅有移动和重命名这两块。


监听子文件夹


对子文件夹的操作,也不外乎新建、删除、移动、重命名,和子文件在逻辑上基本一致,我们可以直接复用子文件的监听逻辑,再加上用 record.changedHandle.kind === ‘directory’ 来判断是否是文件夹即可。


解除监听


当我们想主动解除对文件或文件夹的监听时,只需要调用对应 observerdisconnect 即可:


observer.disconnect()

结语


恭喜你读完了本文,你真棒!


这一次,我们勇敢地品尝了一只新鲜生猛的螃蟹,对 File System Observer API 进行了较为深入的理解和实践。如果你之前一直苦于 JS 无法监听文件,无法带给用户完备的功能和极致的体验,那么从现在开始,你可以开始着手准备升级你的 Web App 了!


这套船新版本的 API 有力地补齐了 Web 文件系统 API 的短板,增强了 Web App 的实现能力,提升了开发者和用户的体验。它还在不断修改完善中,非常需要我们开发者积极参与到标准的制定中来,让 Web 技术栈变得更高效、更易用!


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

如何实现一个稳如老狗的 websocket?

web
前言 彦祖们,前端开发中对于 setTimeout setInterval 一定用得烂熟于心了吧? 但你知道你的定时器并没那么靠谱吗? 本文涉及技术栈(非必要) vue2 场景复现 今天笔者在开发业务的时候就遇到了这样一个场景 前后端有一个 ws 通道,我...
继续阅读 »

前言


彦祖们,前端开发中对于 setTimeout setInterval 一定用得烂熟于心了吧?


但你知道你的定时器并没那么靠谱吗?


本文涉及技术栈(非必要)



  • vue2


场景复现


今天笔者在开发业务的时候就遇到了这样一个场景


前后端有一个 ws 通道,我们暂且命名为 channel


前后端约定如下:



  1. 前端每隔 5000ms 发送一个 ping 消息

  2. 后端收到 ping 后回复一个 pong 消息

  3. 后端如果 15000ms 未收到 ping,则视为 channel 失活,直接 kill

  4. kill 后前端会主动发起重连


文章还没写两分钟,一只暴躁的测试老哥说道:"你们的 ws 也太不稳定了,几十秒就断开一次?废物?"


骂骂咧咧的甩过来一张截图


image.png


笔者心想:"为什么我的界面稳如老狗?浏览器问题,绝对是浏览器问题..."


起身查看,遂发现毫无问题,和笔者一模一样的 chrome 版本...


静心而坐,对着浏览器屏幕茶颜悦色(哦,察言观色)...


10 分钟过去了,半小时过去了...还是稳如老狗,根本不断


image.png


问题分析


那么问题到底出在哪里呢?


笔者坐在测试妹纸身边仔细观察了她的操作后!


发现她不断得切屏,此时已初步心虚,不禁问道 GPT



当浏览器标签页变为非活动状态时,setIntervalsetTimeout 的执行频率通常会被降级。大多数现代浏览器将其执行频率限制在 1 秒(1000 毫秒)或更高,以减少 CPU 和电池的消耗。



问题原因大致是这样了


问题复现


此时笔者在本地写了个 demo


let prev = performance.now()

setInterval(()=>{
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)

},1000)

理想的情况,我们这个 offset 应该是一直维持在 1000ms 左右


那么后续我们就要看页面激活 | 失活时候的情况了


页面激活时


我们先看下页面激活时的打印数据


image.png


没什么问题,符合我们的期望值


页面失活时


接下来我们,切换到其他浏览器标签,保持几分钟,几分钟后我们看下打印数据


image.png


明显发现有些数据不符合我们的期望值


甚至有些夸张到长达 41003ms,将近 40 倍,不靠谱!


寻找方案


用 setTimeout 模拟 setInterval


其实网上最多的方案就是说用 setTimeout 模拟 setInterval


但是很可惜,笔者亲自模拟下来,也是同样的结果,我们看截图


image.png


而且发现更加不靠谱了...错误的概率明显更高了...


其实可想而知,setIntervalsetTimeout 在事件循环中都属于 Task


事件循环的优先级是一样的,同样都属于主线程任务(标记起来,后面考重点)


Web Worker


其实网上还有类似于 requestAnimationFrame 的方案


但是测试下来更离谱,就不浪费彦祖们的时间了


进入正题吧


其实上文说了,主线程任务的优先级会被降低,那么我们思考一下子线程任务呢?


子线程任务在前端领域,我们不就能想到 Web Worker 吗?


当然除了 Web Worker,还有 SharedWorker Service Worker


非本文重点,不做赘述


什么是 Web Worker


首先我们来认识下什么是 Web Worker



Web Worker 是一种运行在浏览器后台的独立 JavaScript 线程,允许我们在不阻塞主线程(即不影响页面 UI 和用户交互的情况下)执行一些耗时的任务,比如数据处理、文件操作、复杂计算等。



不阻塞主线程这恰恰是我们的所需要的!


使用 Web Worker


其实 Web Worker常规使用vue 中还是有一定的区别的


常规使用


常规使用其实非常简单,我们还是以上文中的 demo 为例


改造一下



  • index.html


const worker = new Worker('./worker.js');

我们还需要一个 worker.js文件



  • worker.js


let prev = performance.now()

setInterval(() => {
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)
}, 1000)

切换 tab 几分钟后让我们来看看打印结果


image.png


非常完美,几乎都保持在 1000ms 左右


在 vue 中使用 Web Worker


在 vue 中使用就和常规使用有所不同了


这也是笔者今天踩坑比较多的地方


网上很多文中配置了 webpackworker-loader,然后改造 vue.config.js


但是笔者多次尝试,还是各种报错(如果有大佬踩过坑,请在评论区留言)


最后笔者翻到了之前的笔记,其实早在多年之前就记录了在 vue 中使用 Web Worker 的文章


使用方式非常简单


我们只需要把 worker.js 放置于 public 目录即可!


看下我们此时的代码



  • App.vue


// 此处注意要访问 根路径 /
const myWorker = new Worker('/worker.js')


  • public

    • worker.js




let prev = performance.now()

setInterval(() => {
const offset = performance.now() - prev
prev = performance.now()
console.log('__SY__🎄 ~ setInterval ~ offset:', offset)
}, 1000)

测试一下


image.png


非常完美!


解决业务问题


彦祖们此时可能要问道,你只是证明了 Web Worker 不会阻塞主进程


和你的业务有什么关系吗?


其实这还得依赖于Web Worker的通信机制


我们继续改造



  • App.vue


const myWorker = new Worker('/worker.js')

myWorker.postMessage('createPingInterval') //向 worker 发送开启定时器的指令
// 接收 Web Worker 的执行指令,执行对应业务
myWorker.onmessage = function (event) {
console.log('__SY__🎄 ~ event:', event)
}


  • worker.js


// 接收到主进程 `开启定时器的指令` 处理定时器逻辑
self.onmessage = function(event) {
const interval = setInterval(() => {
self.postMessage('executor') // 定时向主进程发送定时器执行指令
}, 1000)
}

封装一个 setWorkerInterval


其实有了以上的代码模型,我们就能封装一个不受主进程阻塞的定时器了


我们暂且命名它为 setWorkerInterval


函数设计


首先设计一下我们的函数


为了减少开发者心智负担


我们需要把函数设计成和 setInterval 一样的用法


我们在使用 setInterval 的时候,日常最常用的参数就是 callbackdelay


它的返回值是一个 intervalID


由此可见我们的函数签名如下


function setWorkerInterval(callback,delay){
const intervalID = xxx(callback,delay) // 定时执行
return intervalID
}

动手实现


有了上面的函数设计,我们就开始来实现


目前我们遇到一个问题,那就是上文中的 xxx 具体是个啥?


这其实就是 Web Worker 中的 setInterval


我们只需要把 Web Worker 中的 setInterval的功能暴露给主线程不就完事了吗?


来看 代码



  • setWorkerInterval.js


export default function(callback, delay) {
//创建一个 worker
const worker = new Worker('/worker.js')
worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
// 收到 worker 的 setInterval 触发,触发对应业务逻辑
}
}


  • worker.js



// 处理定时器逻辑
self.onmessage = function(event) {
const interval = setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)
}

这样我们就初步完成了以上 xxx 的逻辑


但随之而来又有两个问题


1.如何触发对应业务逻辑?


2.如何清除定时器?


触发对应业务逻辑

其实第一个问题非常容易解决,我们不是传递了一个 callback 吗?


这不就是我们的业务逻辑吗


改造一下



  • setWorkerInteraval.js


export default function(callback, delay) {
const worker = new Worker('/worker.js')

worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 定时执行业务 callback
}
}

清除定时器

这个问题还是踩了坑的,刚开始以为 intervalID 的来源不就在 worker.js吗?


那我们只需要把它通知给主线程即可,后来发现不可行,主线程的 clearInteravl 对于 workerintervalID 并不生效...


那我们换个思路,在主线程发送一个 clear 指令不就行了吗? 说干就干,思路有了,直接看代码



  • worker.js


// 处理定时器逻辑
self.onmessage = function(event) {

const intervalID = setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)

/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(intervalID)
}

}


  • setWorkerInteraval


export default function(callback, delay) {
const worker = new Worker('/worker.js');
// 因为 onmessage 是异步的, 所以我们要抛出一个 promise
return new Promise((resolve) => {
worker.postMessage('') // 开启定时器
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 执行业务逻辑
}
})
const clear = () => {worker.postMessage('clear')}
return clear // 返回一个函数, 用于关闭定时器
}

让我们看下使用方式


let prev = performance.now()
const clear = setWorkerInteraval(function(){
const offset = performance.now() - prev
console.log('__SY__🎄 ~ setWorkerInteraval ~ offset:', offset)
prev = performance.now()
},1000)

setTimeout(clear,5000) // 5000ms 后清除

以上代码看似没问题,但是使用下来并不生效,也就是定时器并未被清除


问题出在哪里呢?


其实我们在发送 clear 指令的时候,也会进入 self.onmessage 函数


那么此时又会新建一个 interval,而我们清空的只是当前 interval 而已


那么我们必须想个方法,使得 interval 在当前实例是唯一的


其实非常简单,借助于 JS 万物皆对象 的思想,我们的 self 不也是一个对象吗?


那我们在它上面挂载一个 interval 有何不可呢?说干就干



  • worker.js


// 处理定时器逻辑
self.onmessage = function (event) {
// 返回一个非零值 所以我们可以大胆使用 ||=
self.intervalID ||= setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, 1000)

/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(self.intervalID)
}
}

测试后,非常完美,至此,一个靠谱的定时器我们就完成了!


当然我们还可以把上文中的 1000ms 改成 delay 传参,直接看完成代码吧


完整代码



  • worker.js (vue项目 需要放在 public 中)


// 处理定时器逻辑
self.onmessage = function (event) {
/// 收到clear消息后, 清除定时器
if (event.data === 'clear') {
clearInterval(self.intervalID)
} else {
const delay = event.data
self.intervalID ||= setInterval(() => {
self.postMessage({}) // 定时通知主线程,即上文中的 xxx
}, delay)
}
}


  • setWorkerInteraval


export default function(callback, delay) {
const worker = new Worker('/worker.js');

worker.postMessage(delay) // 传递 delay 延时参数
// 接收 Web Worker 的消息
worker.onmessage = function(event) {
callback() // 执行业务逻辑
}
const clear = () => {worker.postMessage('clear')}
return clear
}


写在最后


技术服务于业务,但最怕局限于业务


希望彦祖们在开发业务中,能获取更多更深层次的思考和能力!共勉✨


感谢彦祖们的阅读


个人能力有限


如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟


彩蛋


宁波团队还有一个资深前端hc, 带你实现海鲜自由。 欢迎彦祖们私信😚


作者:前端手术刀
来源:juejin.cn/post/7418391732163182607
收起阅读 »

css3+vue做一个带流光交互效果的功能入口卡片布局

web
前言 该案例主要用到了css的新特性 @property来实现交互流光效果 @property是CSS Houdini API的一部分,通过它,开发者可以在样式表中直接注册自定义属性,而无需运行任何JavaScript代码。 Demo在线预览 @prope...
继续阅读 »

前言


该案例主要用到了css的新特性 @property来实现交互流光效果

@property是CSS Houdini API的一部分,通过它,开发者可以在样式表中直接注册自定义属性,而无需运行任何JavaScript代码。


Demo在线预览



@propert语法说明


@property --自定义属性名 {  
syntax: '语法结构';
initial-value: '初始值';
inherits: '是否允许该属性被继承';
}

自定义属性名:需要以--开头,这是CSS自定义属性的标准命名方式。

syntax:描述该属性所允许的语法结构,是必需的。它定义了自定义属性可以接受的值的类型,如颜色、长度、百分比等。

initial-value:用于指定自定义属性的默认值。它必须能够按照syntax描述符的定义正确解析。在syntax为通用语法定义时,initial-value是可选的,否则它是必需的。

inherits:用于指定该自定义属性是否可以被其他元素所继承,通过布尔值truefalse赋值。


Html代码部分


  <div class="flex-x-box">
<!-- 内容 start -->
<div class="card" v-for="(i,index) in list" :key="index">
<img :src="i.url"/>
<div class="info-box">
<h1>{{ i.label +'管理' }}</h1>
<p>{{ i.desc }}</p>
</div>
<div class="details-box">
<span>
<h2>{{ i.total }}</h2>
<p>{{i.label +'总数'}}</p>
</span>
<span>
<h2>{{ i.add }}</h2>
<p>今日新增</p>
</span>
</div>
</div>
<!-- 内容 end -->
</div>

数据结构部分


list:[
{
label:'报表',
desc:'深度数据挖掘,多维度报表生成,实时监控业务动态,为企业决策提供有力数据支撑',
url:'图标地址',
total:'108',
add:'12'
},{
label:'产品',
desc:'全生命周期管理,从研发到销售无缝衔接,优化产品组合,提升市场竞争力',
url:'图标地址',
total:'267',
add:'25'
},{
label:'文档',
desc:'高效知识存储,智能检索体系,确保信息准确传递,助力团队协作与知识共享',
url:'图标地址',
total:'37',
add:'2'
}
]

css样式控制部分


.flex-x-box{
display:flex;
}

/* 卡片宽度 */
$card-height:280px;
$card-width:220px;


div,span,p{
box-size:border-box;
}
.card {
position: relative;
width: $card-width;
height: $card-height;
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
color: rgba(88,199,250,0%);
cursor: pointer;
box-size:border-box;
background: linear-gradient(210deg,#203656,#426889);
margin:0px 15px;

img{
width:90%;
margin-top:-80px;
transition:200ms linear;
}
.info-box{
width:100%;
}
h1{
font-size:14px;
color:#f3fbff;
}
p{
font-size:12px;
color:#a1c4de;
padding: 0px 28px;
line-height: 1.6;
}
.details-box{
position: absolute;
display:flex;
align-items:center;
width: 100%;
height:120px;
box-sizing: border-box;
bottom: 0px;
padding-top: 45px;
z-index: 10;
background: linear-gradient(0deg, #152d4a 70%, transparent);
border-radius: 0px 0px 12px 12px;
transition:200ms linear;
opacity:0;
span{
width:0px;
flex-grow:1;
}
h2{
margin:0px;
font-size:14px;
line-height:1.8;
color:#fff;
}
p{
margin:0px;
}
}
}

.card:hover{
&::before{
background-image: linear-gradient(var(--rotate) , #5ddcff, #3c67e3 43%, #ffddc5);
animation: spin 3s linear infinite;
}
&::after{
background-image: linear-gradient(var(--rotate), #5ddcff, #3c67e3 43%, #ff8055);
animation: spin 3s linear infinite;
}
.details-box{
opacity:1;
}
img{
width:96%;
margin-top:-60px;
}
}

.card::before {
content: "";
width:$card-width +2px;
height:$card-height +2px;
border-radius: 13px;
background-image: linear-gradient(var(--rotate) , #435b7c, #5f8cad 50%, #729bba);
position: absolute;
z-index: -1;
top: -1px;
left: -1px;
}
.card::after {
position: absolute;
content: "";
top: calc($card-height / 6);
left: 0;
right: 0;
z-index: -1;
height: 100%;
width: 100%;
margin: 0 auto;
transform: scale(0.9);
filter: blur(20px);
opacity: 1;
transition: opacity .5s;
}

/* CSS 自定义属性 */
@property --rotate {
syntax: "<angle>";
initial-value: 90deg;
inherits: false;
}

@keyframes spin {
0% {
--rotate: 0deg;
}
100% {
--rotate: 360deg;
}
}

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

入职N天的我,终于接到了我的第一个需求

web
我是9月份中下旬入职的,入职了之后,每天的工作就是熟悉代码和框架,就这样过了几天之后,迎来了我在新公司的第一个节日:国庆节。每年国庆节都是农民最开心的日子,因为要秋收了,秋收就意味着赚钱了,但是今年农作物的行情就跟程序员的行情一样,超级不好,各种粮食的价格都很...
继续阅读 »

我是9月份中下旬入职的,入职了之后,每天的工作就是熟悉代码和框架,就这样过了几天之后,迎来了我在新公司的第一个节日:国庆节。

每年国庆节都是农民最开心的日子,因为要秋收了,秋收就意味着赚钱了,但是今年农作物的行情就跟程序员的行情一样,超级不好,各种粮食的价格都很便宜,辛辛苦苦一年只能算个收支平衡(来自一个农二代的吐槽)。

节后第一天,由于领导请假了,所以我还是在自己看代码(是的,还在看)。等到了第二天,领导来上班了,我迫不及待的问领导有没有什么需求给我,领导说快有了,然后我就只能默默地等待,好在中午快吃饭的时候,领导跟我说有个功能需要修改一下,让我来做(激动地心,颤抖的手,终于有需求可做了)

项目排序功能

需求:根据汉字拼音进行排序。

有以下几种实现方案

方式一:通过String.prototype.localCompare()

const arr = ['我十分', '喜欢', '写', '代码'];

function sortArr(data) {
    return data.sort((a, b) => a.localeCompare(b, 'zh', { sensitivity: 'base' }))
}
sortArr(arr)

如果大家有对localeCompare不熟悉的可以继续往下看,如果有比较熟悉的,那么可以直接跳过。

localeCompare

返回一个数字,用于表示引用字符串(调用者referenceString)在比较字符串(第一个参数compareString)的前面、后面、或者相等

参数
  • compareString: 要与referenceString进行比较的字符串,所有值都会被强制转换为字符串。如果不传或者传入undefined会出现你不想要的情况
  • locals:表示语言类型,比如zh就是中文
  • options: 一堆属性
  • localeMather:区域匹配算法
  • usage:比较是用于排序还是用于搜索匹配的字符串,支持sort和search,默认sort
  • sensitivity:字符串哪些差异导致非0
    • base:字母不同的字符串比较不相等,a≠b,a=A
    • accent:字母不同 || 重音符号和其他变音符号的不同字符串比较不相等, a ≠ b、a ≠ á、a = A。
    • case:字母不同 或者 大小写的字母表示不同 a ≠ b,a ≠ A
    • variant:字符串的字母、重音和其他变音符号,或者不同大小写比较不相等,a ≠ b、a ≠ á、a ≠ A

    注意:当useage的字为sort时,sensitivity的字默认为variantignore

  • Punctuation:是否忽略标点符号,默认为falsen
  • umeric:是否使用数字对照,使得“1”<“2”<“10”,默认是false
  • caseFirst:是否个根据大小写排序,可能的值是upper、lower、false(默认设置)
  • collation:区域的变体
    • pinyin:汉语
    • compat:阿拉伯语

locals中的配置会和options内的配置会发生重叠,如果有重叠,options的优先级更高

注意:某些浏览器中可能不支持locals或者options,那么此时会忽略这些属性

返回值

返回一个数字

  • 如果引用字符串在比较字符串前面,返回负数
  • 如果引用字符串在比较字符串后面,返回正数
  • 如果两者相同,那么返回0

warning:不要依赖具体的返回值去做一些事情,因为在不同的浏览器、或者相同浏览器的不同版本中,返回的具体数值可能是不一样的,W3C只要求返回值是正数或者负数,而不规定具体的值。

方式2:通过pinyin库

  1. 需要安装pinyin库,在命令行中执行
npm install pinyin
  1. 实现排序逻辑

const { pinyin } = require('pinyin')

const arr = [ '我十分', '喜欢', '写', '代码' ]

const sortedWords = arr.sort((a, b) => {
const pinyinA = pinyin(a, { style: pinyin.STYLE_NORMAL }).flat().join('')
const pinyinB = pinyin(b, { style: pinyin.STYLE_NORMAL }).flat().join('')
return pinyinA.localeCompare(pinyinB)
})

console.log(sortedWords)

方式3:自己实现一套映射

因为我们的文案不是确定的,且可以随意修改,所以这种方案不提倡,但是如果只有几个固定的文案,这样可以自己实现一套映射

const pinyinMap = {
我十分: 'woshifen',
喜欢: 'xihuan',
写: 'xie',
代码: 'daima',
}
const arr = [ '我十分', '喜欢', '写', '代码' ]

const sortedWords = arr.sort((a, b) => pinyinMap[a].localeCompare(pinyinMap[b]))
console.log(sortedWords)

上面三种方式,可以看的出来,第一种还是存在一定的误差,但是我还是选择了第一种实现方式,有以下几个原因

  1. 不需要额外的引入库
  2. 我们的文案是随时可以修改的
  3. 我们对于排序的要求没有那么强烈,只要排一个大致的顺序即可

以上就是我对根据汉字拼音排序实现方案的理解,欢迎大家补充,希望大家一起进步!


作者:落魄的开发
来源:juejin.cn/post/7423573726400299027
收起阅读 »

在线人数统计功能怎么实现?

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方...
继续阅读 »

一、前言


大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。


在线人数统计这个功能相信大家一眼就明白是啥,这个功能不难做,实现的方式也很多,这里说一下我常使用的方式:使用Redis的有序集合(zset)实现。
核心方法是这四个:zaddzrangeByScorezremrangeByScorezrem


二、实现步骤


1. 如何认定用户是否在线?


认定用户在线的条件一般跟网站有关,如果网站需要登录才能进入,那么这种网站就是根据用户的token令牌有效性判断是否在线;
如果网站是公开的,是那种不需要登录就可以浏览的,那么这种网站一般就需要自定一个规则来识别用户,也有很多方式实现如IPdeviceId浏览器指纹,推荐使用浏览器指纹的方式实现。


浏览器指纹可能包括以下信息的组合:用户代理字符串 (User-Agent string)、HTTP请求头信息、屏幕分辨率和颜色深度、时区和语言设置、浏览器插件详情等。现成的JavaScript库,像 FingerprintJSClientJS,可以帮助简化这个过程,因为它们已经实现了收集上述信息并生成唯一标识的算法。


使用起来也很简单,如下:


// 安装:npm install @fingerprintjs/fingerprintjs

// 使用示例:
import FingerprintJS from '@fingerprintjs/fingerprintjs';

// 初始化指纹JS Library
FingerprintJS.load().then(fp => {
// 获取访客ID
fp.get().then(result => {
const visitorId = result.visitorId;
console.log(visitorId);
});
});


这样就可以获取一个访问公开网站的用户的唯一ID了,当用户访问网站的时候,将这个ID放到访问链接的Cookie或者header中传到后台,后端服务根据这个ID标示用户。


2. zadd命令添加在线用户


(1)zadd命令介绍
zadd命令有三个参数



key:有序集合的名称。
score1、score2 等:分数值,可以是整数值或双精度浮点数。
member1、member2 等:要添加到有序集合的成员。
例子:向名为 myzset 的有序集合中添加一个成员:ZADD myzset 1 "one"



(2)添加在线用户标识到有序集合中


// expireTime给用户令牌设置了一个过期时间
LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireTimeout);
String expireTimeStr = DateUtil.formatFullTime(expireTime);
// 添加用户token到有序集合中
redisService.zadd("user.active", Double.parseDouble(expireTimeStr), userToken);


由于一个用户可能户会重复登录,这就导致userToken也会重复,但为了不重复计算这个用户的访问次数,zadd命令的第二个参数很好的解决了这个问题。
我这里的逻辑是:每次添加一个在线用户时,利用当前时间加上过期时间计算出一个分数,可以有效保证当前用户只会存在一个最新的登录态。



3. zrangeByScore命令查询在线人数


(1)zrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:查询分数在 1 到 3之间的所有成员:ZRANGEBYSCORE myzset 1 3



(2)查询当前所有的在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 查询当前日期到"+inf"之间所有的用户
Set userOnlineStringSet = redisService.zrangeByScore("user.active", now, "+inf");


利用zrangeByScore方法可以查询这个有序集合指定范围内的用户,这个userOnlineStringSet也就是在线用户集,它的size就是在线人数了。



4. zremrangeByScore命令定时清除在线用户


(1)zremrangeByScore命令介绍



key:指定的有序集合的名字。
min 和 max:定义了查询的分数范围,也可以是 -inf 和 +inf(分别表示“负无穷大”和“正无穷大”)。
例子:删除分数在 1 到 3之间的所有成员:ZREMRANGEBYSCORE myzset 1 3



(2)定时清除在线用户


// 获取当前的日期
String now = DateUtil.formatFullTime(LocalDateTime.now());
// 清除当前日期到"-inf"之间所有的用户
redisService.zremrangeByScore(""user.active"","-inf", now);


由于有序集合不会自动清理下线的用户,所以这里我们需要写一个定时任务去定时删除下线的用户。



5. zrem命令用户退出登录时删除成员


(1)zrem命令介绍



key:指定的有序集合的名字。
members:需要删除的成员
例子:删除名为xxx的成员:ZREM myzset "xxx"



(2)定时清除在线用户


// 删除名为xxx的成员
redisService.zrem("user.active", "xxx");


删除 zset中的记录,确保主动退出的用户下线。



三、小结一下


这种方案的核心逻辑就是,创建一个在线用户身份集合为key,利用用户身份为member,利用过期时间为score,然后对这个集合进行增删改查,实现起来还是比较巧妙和简单的,大家有兴趣可以试试看。


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

nginx(前端必会-项目部署-精简通用篇)

前言 最近在公司部署项目时遇上了一点关于nginx的问题,于是就想着写一篇关于nginx的文章... 主要给小白朋友分享,nginx是什么,nginx有什么用,最后到nginx的实际应用,项目部署。 nginx 公司项目刚刚上线,用户量少访问量不大,并发量低,...
继续阅读 »

前言


最近在公司部署项目时遇上了一点关于nginx的问题,于是就想着写一篇关于nginx的文章...


主要给小白朋友分享,nginx是什么,nginx有什么用,最后到nginx的实际应用,项目部署。


nginx


公司项目刚刚上线,用户量少访问量不大,并发量低,一个jar包启动应用就能够解决问题了。但是随着项目的不断扩大,用户体量大增加,一台服务器可能就无法满足我们的需求了(当初是一个人用一个服务器,现在是多人用一个服务器,时间长了服务器都要红温)。


于是乎,我们就会想着增加服务器,那么我们多个项目就启动在多个服务器上面,用户需要访问,就需要做一个代理服务器,通过代理请求服务器来做前后端之间的转发和请求包括跨域等等问题。


那么到这就不得不说一下nginx的反向代理了,正向代理指的其实就是比如我们通过VPN去请求xxx,这里就是因为用到了其他地方的代理服务器,这是一个从客户端到服务端的过程,然而反向代理其实就是,因为我们有多个服务器,最后都映射到了代理服务器身上,客户端最终访问的都是例如:baidu.com,但是事实上他底下是有多台服务器的。


既然他有多台服务器,每台服务器的性能,各种条件都是不同的,这里就要说到nginx的另一个能力---负载均衡,可以给不同的服务器增加不同的权重,能力更强的服务器可以增大他的负荷,减轻其他服务器的负荷


这就是大家常说的nginx:Nginx 是一个高性能的 HTTP反向代理服务器,它还支持 IMAP/POP3/SMTP 代理服务器。


nginx的特点:



  1. 高性能



    • 高并发连接处理能力:Nginx 使用异步事件驱动模型(如 epoll, kqueue 等),能够高效地处理大量并发连接。

    • 低资源消耗:与 Apache 相比,Nginx 在相同硬件环境下通常消耗更少的内存和其他系统资源。



  2. 稳定性



    • 运行稳定:在高负载情况下依然保持稳定运行,崩溃或错误的发生率较低。

    • 平滑升级:可以在不停止服务的情况下进行升级或配置更改。



  3. 丰富的功能集



    • 反向代理:可以作为反向代理服务器,将请求转发到后端服务器。

    • URL 重写:通过简单的配置即可实现复杂的 URL 重写规则。

    • 动态内容与静态内容分离:可以配置为只处理静态文件请求,而动态请求则交给后端应用服务器处理。



  4. 高度可配置性



    • 灵活的配置选项:可以根据需要定制各种配置选项,以适应不同的应用场景。

    • 容易管理:配置文件结构清晰,易于理解和修改。



  5. 负载均衡



    • 支持多种负载均衡算法,例如轮询、加权轮询、最少连接数等,可以帮助分散到多个后端服务器的流量。



  6. 缓存功能



    • 可用作HTTP缓存服务器,减少对后端数据库或应用服务器的压力。



  7. 安全性



    • 提供 SSL/TLS 加密支持,保障数据传输安全。

    • 可以设置访问控制、防火墙规则等来增强安全性。



  8. 模块化架构



    • 支持第三方模块扩展功能,比如 Nginx+Lua 使得开发者可以在 Nginx 中直接使用 Lua 脚本语言编写插件或处理逻辑。



  9. 日志与监控



    • 详细的访问和错误日志记录,便于故障排查和性能分析。

    • 支持实时监控和统计,方便管理员了解当前系统的状态。




nginx下载


nginx.org/ 大家自行下载,我下载的是一个稳定版本,以防万一。下载完毕之后大家自行解压即可(默认大家是windows系统),解压完毕之后,可以看到nginx.exe就是我们的启动文件,conf就是配置文件,nginx.config中可以看到server的listen监听端口为80,这意味着当我们访问了80端口就会被nginx拦截,首先启动nginx,可以直接双击nginx.exe也可以通过cmd命令行直接输入nginx.exe运行(推荐,因为这样不会关闭窗口,双击的话就是一闪而过了)


image.png


接下来我们浏览器访问localhost:80


image.png


启动成功。


nginx常用命令


停止:nginx.exe -s stop
安全退出:nginx.exe -s quit
重新加载配置文件:nginx.exe -s reload(常用)例如我们更改了端口
查看nginx进程:ps aux|grep nginx


实际应用


下载完毕后打开可以看到:


image.png


image.png


于是我们建立aa、bb两个文件夹,我们将index.html 分别放入aa和bb,这两个页面都打上自己的标记aa/bb


image.png


然后我们对nginx进行配置 nginx.conf


server {
listen 8001;
server_name localhost;

location / {
root html/aa;
index index.html index.htm;
}
}

server {
listen 8002;
server_name localhost;

location / {
root html/bb;
index index.html index.htm;
}
}

如果没结束,记得reload


nginx.exe -s reload

image.png


image.png


接下来我们放一个项目进去,打包后放入html中


image.png


修改配置文件,然后reload


 server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html/dist;
index index.html index.htm;
}


然后我们访问localhost,端口默认是80所以不用写,如果失败,可能是reload失败,再次reload就可


image.png


其他配置问题


Nginx的主配置文件(conf/nginx.conf)按以下结构组织:



  • 全局块 与Nginx运行相关的全局设置

  • events 与网络连接有关的设置

  • http 代理、缓存、日志、虚拟主机等的配置

  • server 虚拟主机的参数设置(一个http块可包含多个server块)

  • location 定义请求路由及页面处理方式


前端开发中经常会遇到跨域问题,nginx可以做代理轻松解决,事实上原理和cors一样,设置请求头


server {
   location /api {
       proxy_pass http://backend_server;
       add_header Access-Control-Allow-Origin *;
       add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
       add_header Access-Control-Allow-Headers 'Origin, Content-Type, Accept';
  }
}


缓存问题:


proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;

server {
   location / {
       proxy_cache my_cache;
       proxy_pass http://backend;
       add_header X-Cache-Status $upstream_cache_status;
  }
}


https提升安全性


server {
   listen 443 ssl;
   server_name example.com;

   ssl_certificate /etc/nginx/ssl/example.com.crt;
   ssl_certificate_key /etc/nginx/ssl/example.com.key;

   location / {
       proxy_pass http://backend_server;
  }
}


一个比较全面的配置


# 全局段配置
# ------------------------------

# 指定运行nginx的用户或用户组,默认为nobody。
#user administrator administrators;

# 设置工作进程数,通常设置为等于CPU核心数。
#worker_processes 2;

# 指定nginx进程的PID文件存放位置。
#pid /nginx/pid/nginx.pid;

# 指定错误日志的存放路径和日志级别。
error_log log/error.log debug;

# events段配置信息
# ------------------------------
events {
# 设置网络连接序列化,用于防止多个进程同时接受到新连接的情况,这种情况称为"惊群"
accept_mutex on;

# 设置一个进程是否可以同时接受多个新连接。
multi_accept on;

# 设置工作进程的最大连接数。
worker_connections 1024;
}

# http配置段,用于配置HTTP服务器的参数。
# ------------------------------
http {
# 包含文件扩展名与MIME类型的映射。
include mime.types;

# 设置默认的MIME类型。
default_type application/octet-stream;

# 定义日志格式。
log_format myFormat '$remote_addr–$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for';

# 指定访问日志的存放路径和使用的格式。
access_log log/access.log myFormat;

# 允许使用sendfile方式传输文件。
sendfile on;

# 限制每次调用sendfile传输的数据量。
sendfile_max_chunk 100k;

# 设置连接的保持时间。
keepalive_timeout 65;

# 定义一个上游服务器组。
upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #此服务器为备份服务器。
}

# 定义错误页面的重定向地址。
error_page 404 https://www.baidu.com;

# 定义一个虚拟主机。
server {
# 设置单个连接上的最大请求次数。
keepalive_requests 120;

# 设置监听的端口和地址。
listen 4545;
server_name 127.0.0.1;

# 定义location块,用于匹配特定的请求URI
location ~*^.+$ {
# 设置请求的根目录。
#root path;

# 设置默认页面。
#index vv.txt;

# 将请求转发到上游服务器组。
proxy_pass http://mysvr;

# 定义访问控制规则。
deny 127.0.0.1;
allow 172.18.5.54;
}
}
}


如果有不明白的地方,遇到问题可以通过ai去迅速了解,在ai时代,我们的学习成本也大大下降了。


小结


本次主要带小白朋友学习nginx是什么、用于做什么,nginx的常用命令,nginx如何进行配置,最后实际操作一次简单的nginx。


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

几种神秘鲜为人知但却有趣的前端技术

web
测定网速 navigator.connection.downlink 是一个用于表示当前连接下行速度的属性,它是 JavaScript 的 Network Information API 的一部分。此属性返回一个数值,单位为 Mbps(Megabits per...
继续阅读 »

测定网速


navigator.connection.downlink 是一个用于表示当前连接下行速度的属性,它是 JavaScript 的 Network Information API 的一部分。此属性返回一个数值,单位为 Mbps(Megabits per second),表示网络的下行带宽。


例如,你可以通过以下方式使用它:


if (navigator.connection) {
const downlink = navigator.connection.downlink;
console.log(`当前下行速度为: ${downlink} Mbps`);
} else {
console.log("当前浏览器不支持Network Information API");
}

需要注意的是,Network Information API 并不是所有浏览器都支持,因此在使用时最好进行兼容性检查。


在智能手机上启用振动


window.navigator.vibrate 是一个用于触发设备震动的 Web API,通常用于移动设备上。这个方法允许开发者控制设备的震动模式,它接受一个数字或一个数组作为参数。



  • 如果传入一个数字,这个数字表示震动的时长(以毫秒为单位)。

  • 如果传入一个数组,可以定义震动和静止的模式,例如 [200, 100, 200] 表示震动200毫秒,静止100毫秒,再震动200毫秒。


以下是一个简单的示例:


// 使设备震动 500 毫秒
if (navigator.vibrate) {
navigator.vibrate(500);
} else {
console.log("当前浏览器不支持震动 API");
}

// 使用数组来创建震动模式
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200, 100, 200]);
}

请注意,并不是所有的设备和浏览器都支持震动 API,因此在使用时最好确认设备的兼容性。


禁止插入文字


你可能不希望用户在输入字段中粘贴从其他地方复制的文本(请仔细考虑清楚是否真的要这样做)。通过跟踪事件paste并调用其方法preventDefault()就很容易完成了。


<input type="text"></input>
<script>
  const input = document.querySelector('input');

  input.addEventListener("paste"function(e){
    e.preventDefault()
  })

</script>

好了,现在你无法复制粘贴,必须得手动编写和输入所有的内容了


快速隐藏dom


要隐藏DOM元素,不是非得用到JavaScript。原生的HTML属性完全可以实现hidden。效果类似于添加样式display: none;。元素就从页面上消失了。


<p hidden>我在页面看不到了</p>

注意,这个技巧不适用于伪元素


快速使用定位


你知道CSS的inset属性吗?这是我们所熟悉的topleftrightbottom的缩写版本。通过类比短语法margin或属性padding,只要一行代码就可以设置元素的所有偏移量。


/* 普通写法 */ 
div {
  position: absolute;
  top0;
  left0;
  bottom0;
  right0;
}

/* inset写法 */ 
div {
  position: absolute;
  inset: 0;
}

使用简短的语法可以大大减小CSS文件的体积,这样代码看起来更干净。但是,可别忘了inset是一个布尔属性,它考虑了内容排版方向。换句话说,如果站点使用的是具有rtl方向(从右到左)的语言,那么left要变成right,反之亦然。


你不知道的Console的用法


通常我们用的最多的console.log(xxx),其实在 JavaScript 中,console 对象提供了一些很有用的方法用于调试和查看信息。以下是一些可能不太常见的 console 用法:



  1. console.table() : 可以用来以表格的格式输出数组或对象,非常适合查看数据结构。


    const data = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 }
    ];
    console.table(data);


  2. console.group() 和 console.groupEnd() : 可以将相关日志信息分组,方便查看和组织输出。


    console.group('Gr0up Label');
    console.log('这是一条 log');
    console.log('这是一条 log 2');
    console.groupEnd();


  3. console.time() 和 console.timeEnd() : 用于测量代码块的执行时间。


    console.time('myTimer');
    // 执行一些操作
    console.timeEnd('myTimer'); // 输出所用的时间


  4. console.error() 和 console.warn() : 用于输出错误和警告信息,通常会以不同的颜色高亮显示。


    console.error('这是一个错误信息');
    console.warn('这是一个警告信息');


  5. console.assert() : 用于在条件为 false 时输出错误信息。


    const condition = false;
    console.assert(condition, '条件为 false,输出这条信息');


  6. console.clear() : 清空控制台的输出。


    console.clear();


  7. console.dir() : 用于打印对象的可枚举属性,方便查看对象的详细结构。


    const obj = { name: 'Alice', age: 25 };
    console.dir(obj);



禁止下拉刷新


下拉刷新是当前流行的移动开发模式。如果你不喜欢这样做,只需将overscroll-behavior-y属性的值设为contains即可达到此效果。


body {
 overscroll-behavior-y: contain;
}

这个属性对于组织模态窗口内的滚动也非常有用——它可以防止主页在到达边框时拦截滚动


使整个网页的 <body> 内容可编辑


document.body.contentEditable='true'; 是一段 JavaScript 代码,用于使整个网页的 <body> 内容可编辑。这意味着用户可以直接在浏览器中点击并编辑文本,就像在文本编辑器中一样。


以下是一些相关的要点:



  1. 启用编辑模式:将 contentEditable 属性设置为 'true',浏览器会允许用户更改页面的内容。


    document.body.contentEditable = 'true';


  2. 禁用编辑模式:如果希望用户无法编辑页面,您可以将该属性设置为 'false'


    document.body.contentEditable = 'false';


  3. 注意事项



    • 这种做法在很多场景中很方便,比如在展示一些信息并希望用户能快速修改的时候。例如,创建自定义的富文本编辑器。

    • 但是,使用 contentEditable 也可能会带来一些不便,比如用户修改了页面的结构,甚至可能影响脚本的运行。因此在使用时要谨慎,并确保有合适的方法来处理用户的输入。

    • 启用 contentEditable 后,如果网页中有表单元素,用户的输入可能与表单的默认行为产生冲突。



  4. 样式和功能:在启用编辑模式后,你可能还想添加一些 CSS 来改变光标样式,或者结合 JavaScript 进一步增强编辑体验,比如自动保存用户的修改。


示例代码:


// 启用编辑功能
document.body.contentEditable = 'true';

// 禁用编辑功能
// document.body.contentEditable = 'false';

这种功能可以非常方便地用于快速原型设计或需要快速内容编辑的应用,但在生产环境中要慎重使用。


带有Id属性的元素,会创建全局变量


在一张HTML页面中,所有设置了ID属性的元素会在JavaScript的执行环境中创建对应的全局变量,这意味着document.getElementById像人的智齿一样显得多余了。但实际项目中最好还是老老实实该怎么写就怎么写,毕竟常规代码出乱子的机会要小得多。


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

网站平滑滚动


<html> 元素中添加 scroll-behavior: smooth,以实现整个页面的平滑滚动。


html{    
scroll-behavior: smooth;
}

:empty 表示空元素


此选择器定位空的 <p> 元素并隐藏它们


p:empty{   
display: none;
}

作者:鱼樱前端
来源:juejin.cn/post/7423314983884292134
收起阅读 »

Kafka 为什么要抛弃 Zookeeper?

嗨,你好,我是猿java 在很长一段时间里,ZooKeeper都是 Kafka的标配,现如今,Kafka官方已经在慢慢去除ZooKeeper,Kafka 为什么要抛弃 Zookeeper?这篇文章我们来聊聊其中的缘由。 Kafka 和 ZooKeeper 的关...
继续阅读 »

嗨,你好,我是猿java


在很长一段时间里,ZooKeeper都是 Kafka的标配,现如今,Kafka官方已经在慢慢去除ZooKeeper,Kafka 为什么要抛弃 Zookeeper?这篇文章我们来聊聊其中的缘由。


Kafka 和 ZooKeeper 的关系


ZooKeeper 是一个分布式协调服务,常用于管理配置、命名和同步服务。长期以来,Kafka 使用 ZooKeeper 负责管理集群元数据、控制器选举和消费者组协调等任务理,包括主题、分区信息、ACL(访问控制列表)等。


ZooKeeper 为 Kafka 提供了选主(leader election)、集群成员管理等核心功能,为 Kafka提供了一个可靠的分布式协调服务,使得 Kafka能够在多个节点之间进行有效的通信和管理。然而,随着 Kafka的发展,其对 ZooKeeper的依赖逐渐显露出一些问题,这些问题也是下面 Kafka去除 Zookeeper的原因。


抛弃ZooKeeper的原因


1. 复杂性增加


ZooKeeper 是独立于 Kafka 的外部组件,需要单独部署和维护,因此,使用 ZooKeeper 使得 Kafka的运维复杂度大幅提升。运维团队必须同时管理两个分布式系统(Kafka和 ZooKeeper),这不仅增加了管理成本,也要求运维人员具备更高的技术能力。


2. 性能瓶颈


作为一个协调服务,ZooKeeper 并非专门为高负载场景设计, 因此,随着集群规模扩大,ZooKeeper在处理元数据时的性能问题日益突出。例如,当分区数量增加时,ZooKeeper需要存储更多的信息,这导致了监听延迟增加,从而影响Kafka的整体性能34。在高负载情况下,ZooKeeper可能成为系统的瓶颈,限制了Kafka的扩展能力。


3. 一致性问题


Kafka 内部的分布式一致性模型与 ZooKeeper 的一致性模型有所不同。由于 ZooKeeper和 Kafka控制器之间的数据同步机制不够高效,可能导致状态不一致,特别是在处理集群扩展或不可用情景时,这种不一致性会影响消息传递的可靠性和系统稳定性。


4. 发展自己的生态


Kafka 抛弃 ZooKeeper,我个人觉得最核心的原因是:Kafka生态强大了,需要自立门户,这样就不会被别人卡脖子。纵观国内外,有很多这样鲜活的例子,当自己弱小时,会先选择使用别家的产品,当自己羽翼丰满时,再选择自建完善自己的生态圈。


引入KRaft


为了剥离和去除 ZooKeeper,Kafka 引入了自己的亲儿子 KRaft(Kafka Raft Metadata Mode)。KRaft 是一个新的元数据管理架构,基于 Raft 一致性算法实现的一种内置元数据管理方式,旨在替代 ZooKeeper 的元数据管理功能。其优势在于:



  1. 完全内置,自包含:KRaft 将所有协调服务嵌入 Kafka 自身,不再依赖外部系统,这样大大简化了部署和管理,因为管理员只需关注 Kafka 集群。

  2. 高效的一致性协议:Raft 是一种简洁且易于理解的一致性算法,易于调试和实现。KRaft 利用 Raft 协议实现了强一致性的元数据管理,优化了复制机制。

  3. 提高元数据操作的扩展性:新的架构允许更多的并发操作,并减少了因为扩展性问题导致的瓶颈,特别是在高负载场景中。

  4. 降低延迟:在消除 ZooKeeper 作为中间层之后,Kafka 的延迟性能有望得到改善,特别是在涉及选主和元数据更新的场景中。

  5. 完全自主:因为是自家产品,所以产品的架构设计,代码开发都可以自己说了算,未来架构走向完全控制在自己手上。


KRaft的设计细节



  1. 控制器(Controller)节点的去中心化:KRaft 模式中,控制器节点由一组 Kafka 服务进程代替,而不是一个独立的 ZooKeeper 集群。这些节点共同负责管理集群的元数据,通过 Raft 实现数据的一致性。

  2. 日志复制和恢复机制:利用 Raft 的日志复制和状态机应用机制,KRaft 实现了对元数据变更的强一致性支持,这意味着所有控制器节点都能够就集群状态达成共识。

  3. 动态集群管理:KRaft 允许动态地向集群中添加或移除节点,而无需手动去 ZooKeeper 中更新配置,这使得集群管理更为便捷。


下面给出一张 Zookeeper 和 KRaft的对比图:


why-kafka-deprecates-zookeeper-1.jpg


总结


本文,我们分析了为什么 Kafka 要移除 ZooKeeper,主要原因有两个:ZooKeeper不能满足 Kafka的发展以及 Kafka想创建自己的生态。在面临越来越复杂的数据流处理需求时,KRaft 模式为 Kafka 提供了一种更高效、简洁的架构方案。不论结局如何,Kafka 和 ZooKeeper曾经也度过了一段美好的蜜月期,祝福 Kafka 在 KRaft模式越来越强大,为使用者带来更好的体验。


学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7425812523129634857
收起阅读 »

面试官:为什么你们项目中还在用多表关联!

我们来看这样一个面试场景。面试官:“在你们的项目中,用到多表关联查询了吗?”候选人:“嗯,每个项目都用到了。”面试官听了似乎有些愤怒,说:“多表关联查询这么慢,为什么你们还要用它,那你们项目的性能如何保障呢?”面对这突如其来地质问,候选人明显有些慌了,解释道:...
继续阅读 »

我们来看这样一个面试场景。

面试官:“在你们的项目中,用到多表关联查询了吗?”

候选人:“嗯,每个项目都用到了。”

面试官听了似乎有些愤怒,说:“多表关联查询这么慢,为什么你们还要用它,那你们项目的性能如何保障呢?”

面对这突如其来地质问,候选人明显有些慌了,解释道:“主要是项目周期太紧张了,这样写在开发效率上能高一些,后期我们会慢慢进行优化的。”

面试官听了,带着三分理解、三分无奈、四分恨铁不成钢地摇了摇头。

面试之后,这个同学问我说:“学长,是不是在项目中使用多表关联太low了,可是从业这五年多,我换过三家公司,哪个公司的项目都是这么用的。”

下面我来从实现原理的角度,回答一下这个问题。

“是否应该使用多表关联”,这绝对是个王炸级别的话题了,其争议程度,甚至堪比“中医废存之争”了。

一部分人坚定地认为,应该禁止使用SQL语句进行多表关联,正确的方式是把各表的数据读到应用程序中来,再由程序进行数据merge操作。

而另一部分人则认为,在数据库中进行多表关联是没有问题的,但需要多关注SQL语句的执行计划,只要别产生过大的资源消耗和过长的执行时间即可。

嗯,我完全支持后者的观点,况且MySQL在其底层算法的优化上,一直在努力完善。

MySQL表连接算法

我们以最常用的MySQL数据库为例,其8.0版本支持的表连接算法为Nested-Loops Join(嵌套循环连接)和Hash Join(哈希连接)。

如下图所示:

嵌套循环连接算法,顾名思义,其实现方式是通过内外层循环的方式进行表连接,SQL语句中有几个表进行连接就实现为几层循环。

接下来,我们看看嵌套循环连接算法的四种细分实现方式。

1、简单嵌套循环连接(Simple Nested-Loops Join)

这是嵌套循环连接中最简单粗暴的一种实现方式,直接用驱动表A中符合条件的数据,一条一条地带入到被驱动表B中,进行全表扫描匹配。

其伪代码实现为:

for each row in table A matching range {
for each row in table B matching join column{
if row satisfies join conditions,send to client
}
}

我们以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一条记录。

(2)以内循环的方式,将该条记录与order表中的记录进行关联键的逐一比较,并找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

也就是说,如果驱动表A中符合条件的数据有一万条,那么就需要带入到被驱动表B中进行一万次全表扫描,这种查询效率会非常慢。因此,除非特殊场景,否则查询优化器不太会选择这种连接算法。

2、索引嵌套循环连接(Index Nested-Loops Join)

接上文,如果在被驱动表B的关联列上创建了索引,那MySQL查询优化器极大概率会选择这种这种实现方式,因为其非常高效。

我们依然以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一条记录。

(2)以内循环的方式,将该条记录与order表中关联键的索引进行匹配,直接找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

这里需要说明一下,若order表的关联列是主键索引,则可以直接在表中查到记录;若order表的关联列是二级索引,则通过索引扫描的方式查到记录的主键,然后再回表查到具体记录。

3、缓存块嵌套循环连接(Block Nested-Loops Join)

我们在上文中说过,在简单嵌套循环连接中,如果驱动表A中符合条件的数据有一万条,那么就需要带入到被驱动表B中进行一万次全表扫描,这种查询效率会非常慢。

而缓存块嵌套循环连接,则正是针对于该场景进行的优化。

当驱动表A进行循环匹配的时候,数据并不会直接带入到被驱动表B,而是使用Join Buffer(连接缓存)先缓存起来,等到Join Buffer满了再去一次性关联被驱动表B,这样可以减少被驱动表B被全表扫描的次数,提升查询性能。

其伪代码实现为:

for each row in table A matching range {
store join column in join buffer
ifjoin buffer is full){
for each row in table B matching join column{
if row satisfies join conditions,send to client
}
}
}

我们依然以下面的SQL语句举例:

SELECT * FROM product p INNER JOIN order o ON p.id = o.product_id;

其对应的详细执行步骤为:

(1)外循环从product表中每次读取一定数量的记录,将Join Buffer装满。

(2)以内循环的方式,将Join Buffer中记录与order表中的记录进行关联键的逐一比较,并找到匹配记录。

(3)将product表中的记录和order表中的匹配记录进行关联,放到结果集中。

(4)重复执行步骤1、2、3,直到内外两层循环结束。

需要注意的是,从MySQL 8.0.20开始,MySQL就不再使用缓存块嵌套循环连接了,将以前使用缓存块嵌套循环连接的场景全部改为哈希连接。

所以,MySQL的研发者一直在努力优化这款产品,其产品本身也没有大家所想的那么弱鸡。

4、批量键访问连接(Batched Key Access Joins)

说到这里,不得不先提一下MySQL5.6版本的新特性,多范围读(Multi-Range Read)。

我们继续拿product表的场景进行举例。

SELECT * FROM product WHERE price > 5 and price < 20;

没有使用MRR的情况下:

由上图可见,在MySQL InnoDB中使用二级索引的范围扫描时,所获取到的主键ID是无序的,因此在数据量很大的情况下进行回表操作时,会产生大量的磁盘随机IO,从而影响数据库性能。

使用MRR的情况下:

显而易见的是,在二级索引和数据表之间增加了一层buffer,在buffer中进行主键ID的排序操作,这样回表操作就从磁盘随机I/O转化为顺序I/O,并可减少磁盘的I/O次数(1个page里面可能包含多个命中的record),提高查询效率。

而从MySql 5.6开始支持Batched Key Access Joins,则正是结合了MRR和Join Buffer,以此来提高多表关联的执行效率,其发生的条件为被驱动表B有索引,并且该索引为非主键。

其伪代码实现和详细步骤为:

for each row in table A matching range {
store join column in join buffer
ifjoin buffer is full){
for each row in table B matching join column{
send to MRR interface,and order by its primary key
if row satisfies join conditions,send to client
}
}
}

另外,如果查询优化器选择了批量键访问连接的实现方式,我们可以在执行计划中的Extra列中看到如下信息:

Using where; Using join buffer (Batched Key Access)

结语

在本文中,我们介绍了嵌套循环连接的四种实现方式,下文会继续讲解哈希连接算法,以及跟在程序中进行多表数据merge的实现方式进行对比。


作者:托尼学长
来源:juejin.cn/post/7387626171158675466
收起阅读 »

大公司如何做 APP:背后的开发流程和技术

我记得五六年前,当我在 Android 开发领域尚处初出茅庐阶段之时,我曾有一个执念——想看下大公司在研发一款产品的流程和技术上跟小公司有什么区别。公司之大,对开发来说,不在于员工规模,而在于产品的用户量级。只有用户量级够大,研发过程中的小问题才会被放大。当用...
继续阅读 »

我记得五六年前,当我在 Android 开发领域尚处初出茅庐阶段之时,我曾有一个执念——想看下大公司在研发一款产品的流程和技术上跟小公司有什么区别。公司之大,对开发来说,不在于员工规模,而在于产品的用户量级。只有用户量级够大,研发过程中的小问题才会被放大。当用户量级够大,公司才愿意在技术上投入更多的人力资源。因此,在大公司里做技术,对个人的眼界、技术细节和深度的提升都有帮助。


我记得之前我曾跟同事调侃说,有一天我离职了,我可以说我毕业了,因为我这几年学到了很多。现在我想借这个机会总结下这些年在公司里经历的让我印象深刻的技术。


1、研发流程


首先在产品的研发流程上,我把过去公司的研发模式分成两种。


第一种是按需求排期的。在评审阶段一次性评审很多需求,和开发沟通后可能删掉优先级较低的需求,剩下的需求先开发,再测试,最后上线。上线的时间根据开发和测试最终完成的时间确定。


第二种是双周迭代模式,属于敏捷开发的一种。这种开发机制里,两周一个版本,时间是固定的。开发、测试和产品不断往时间周期里插入需求。如下图,第一周和第三周的时间是存在重叠的。具体每个阶段留多少时间,可以根据自身的情况决定。如果需求比较大,则可以跨迭代,但发布的时间窗口基本是固定的。


截屏2023-12-30 13.00.33.png


有意思的是,第二种开发机制一直是我之前的一家公司里负责人羡慕的“跑火车”模式。深度参与过两种开发模式之后,我说下我的看法。


首先,第一种开发模式适合排期时间比较长的需求。但是这种方式时间利用率相对较低。比如,在测试阶段,开发一般是没什么事情做的(有的会在这个时间阶段布置支线需求)。这种开发流程也有其好处,即沟通和协调成本相对较低。


注意!在这里,我们比较时间利用率的时候是默认两种模式的每日工作时间是相等的且在法律允许范围内。毕竟,不论哪一种研发流程,强制加班之后,时间利用率都“高”(至少老板这么觉得)。


第二种开发方式的好处:



  1. 响应速度快。可以快速发现问题并修复,适合快速试错。

  2. 时间利用率高。相比于按需求排期的方式,不存在开发和测试的间隙期。


但这种开发方式也有缺点:



  1. 员工压力大,容易造成人员流失。开发和测试时间穿插,开发需要保证开发的质量,否则容易影响整个迭代内开发的进度。

  2. 沟通成本高。排期阶段出现人力冲突需要协调。开发过程中出现问题也需要及时、有效的沟通。因此,在这种开发模式里还有一个角色叫项目经理,负责在中间协调,而第一种开发模式里项目经理的存在感很低。

  3. 这种开发模式中,产品要不断想需求,很容易导致开发的需求本身价值并不大。


做了这么多年开发,让人很难拒绝一个事实是,绝大多数互联网公司的壁垒既不是技术,也不是产品,而是“快速迭代,快速试错”。从这个角度讲,双周迭代开发机制更适应互联网公司的要求。就像我们调侃公司是给电脑配个人,这种开发模式里就是给“研发流水线”配个人,从产品、到开发、到测试,所有人都像是流水线上的一员。


2、一个需求的闭环


以上是需求的研发流程。如果把一个需求从产品提出、到上线、到线上数据回收……整个生命周期列出来,将如下图所示,


需求闭环.drawio.png


这里我整合了几个公司的研发过程。我用颜色分成了几个大的流程。相信每个公司的研发流程里或多或少都会包含其中的几个。在这个闭环里,我说一下我印象比较深刻的几个。


2.1 产品流程


大公司做产品一个显著的特点是数据驱动,一切都拿数据说话。一个需求的提出只是一个假设,开发上线之后效果评估依赖于数据。数据来源主要有埋点上报和舆情监控。


1. 数据埋点


埋点数据不仅用于产品需求的验证,也用于推荐算法的训练。因此,大公司对数据埋点的重视可以说是深入骨髓的。埋点数据也经常被纳入到绩效考核里。


开发埋点大致要经过如下流程,



  • 1). 产品提出需要埋的点。埋点的类型主要包括曝光和点击等,此外还附带一些上报的参数,统计的维度包括用户 uv 和次数 pv.

  • 2). 数据设计埋点。数据拿到产品要埋的点之后,设计埋点,并在埋点平台录入。

  • 3). 端上开发埋点。端上包括移动客户端和 Web,当然埋点框架也要支持 RN 和 H5.

  • 4). 端上验证埋点。端上埋点完成之后需要测试,上报埋点,然后再在平台做埋点校验。

  • 5). 产品提取埋点数据。

  • 6). 异常埋点数据修复。


由此可见,埋点及其校验对开发来说也是需要花费精力的一环。它不仅需要多个角色参与,还需要一个大数据平台,一个录入、校验和数据提取平台,以及端上的上报框架,可以说成本并不低。


2. 舆情监控


老实说,初次接触舆情监控的时候,它还是给了我一点小震撼的。没想到大公司已经把舆情监控做到了软件身上。


舆情监控就是对网络上关于该 APP 的舆情的监控,数据来源不仅包括应用内、外用户提交的反馈,还包括主流社交平台上关于该软件的消息。所有数据在整合到舆情平台之后会经过大数据分析和分类,然后进行监控。舆情监控工具可以做到对产品的负面信息预警,帮助产品经理优化产品,是产品研发流程中重要的一环。


3. AB 实验


很多同学可能对 AB 实验都不陌生。AB 实验就相当于同时提出多套方案,然后左右手博弈,从中择优录用。AB 实验的一个槽点是,它使得你代码中同时存在多份作用相同的代码,像狗皮膏药一样,也不能删除,非常别扭,最后导致的结果是代码堆积如山。


4. 路由体系建设


路由即组件化开发中的页面路由。但是在有些应用里,会通过动态下发路由协议支持运营场景。这在偏运营的应用里比较常见,比如页面的推荐流。一个推荐流里下发的模块可能打开不同的页面,此时,只需要为每个页面配置一个路由路径,然后推荐流里根据需要下发即可。所以,路由体系也需要 Android 和 iOS 双端统一,同时还要兼容 H5 和 RN.


mdn-url-all.png


在路由协议的定义上,我们可以参考 URL 的格式,定义自己的协议、域名、路径以及参数。以 Android 端为例,可以在一个方法里根据路由的协议、域名对原生、RN 和 H5 等进行统一分发。


2.2 开发流程


在开发侧的流程里,我印象深的有以下几个。


1. 重视技术方案和文档


我记得之前在一家公司里只文档平台就换了几个,足见对文档的重视。产品侧当然更重文档,而对研发侧,文档主要有如下几类:1). 周会文档;2).流程和规范;3).技术方案;4).复盘资料等。


对技术方案,现在即便我自己做技术也保留了写大需求技术方案先行的习惯。提前写技术方案有几个好处:



  • 1). 便于事后回忆:当我们对代码模糊的时候,可以通过技术方案快速回忆。

  • 2). 便于风险预知:技术方案也有助于提前预知开发过程中的风险点。前面我们说敏捷开发提前发现风险很重要,而做技术方案就可以做到这点。

  • 3). 便于全面思考:技术方案能帮助我们更全面地思考技术问题。一上来就写代码很容易陷入“只见树木,不见森林”的困境。


2. Mock 开发


Mock 开发也就是基于 Mock 的数据进行开发和测试。在这里它不局限于个人层面(很多人可能有自己 Mock 数据开发的习惯),而是在公司层面将其作为一种开发模式,以实现前后端分离。典型的场景是客户端先上线预埋,而后端开发可能滞后一段时间。为了支持 Mock 开发模式,公司需要专门的平台,提供以接口为维度的 Mock 工具。当客户端切换到 Mock 模式之后,上传到网络请求在后端的网关直接走 Mock 服务器,拉取 Mock 数据而不是真实数据。


这种开发模式显然也是为了适应敏捷开发模式而提出的。它可以避免前后端依赖,减轻人力资源协调的压力。这种开发方式也有其缺点:



  • 1). 数据结构定义之后无法修改。客户端上线之后后端就无法再修改数据结构。因此,即便后端不开发,也需要先投入人力进行方案设计,定义数据结构,并拉客户端进行评审。

  • 2). 缺少真实数据的验证。在传统的开发模式中,测试要经过测试和 UAT 两个环境,而 UAT 本身已经比较接近线上环境,而使用 Mock 开发就完全做不到这么严谨。当我们使用 Mock 数据测试时,如果我们自己的 Mock 的数据本身失真比较严重,那么在意识上你也不会在意数据的合理性,因此容易忽视一些潜在的问题。


3. 灰度和热修复


灰度的机制是,在用户群体中选择部分用户进行应用更新提示的推送。这要求应用本身支持自动更新,同时需要对推送的达到率、用户的更新率进行统计。需要前后端一套机制配合。灰度有助于提前发现应用中存在的问题,这对超大型应用非常有帮助,毕竟,现在上架之后发现问题再修复的成本非常高。


但如果上架之后确实出现了问题就需要走热修复流程。热修复的难点在于热修复包的下发,同时还需要审核流程,因此需要搭建一个平台。这里涉及的细节比较多,后面有时间再梳理吧。


4. 配置下发


配置下发就是通过平台录入配置,推送,然后在客户端读取配置信息。这也是应用非常灵活的一个功能,可以用来下发比如固定的图片、文案等。我之前做个人开发的时候也在服务器上做了配置下发的功能,主要用来绕过某些应用商店的审核,但是在数据结构的抽象上做得比较随意。这里梳理下配置下发的细节。



  • 首先,下发的配置是区分平台特征的。这包括,应用的目标版本(一个范围)、目标平台(Android、iOS、Web、H5 或者 RN)。

  • 其次,为了适应组件化开发,也为了更好地分组管理,下发的配置命名时采用 模块#配置名称 的形式。

  • 最后,下发的数据结构支持,整型、布尔类型、浮点数、字符串和 Json.


我自己在做配置下发的时候还遇到一个比较棘手的问题——多语言适配。国内公司的产品一般只支持中文,这方面就省事得多。


5. 复盘文化


对于敏捷开发,复盘是不可或缺的一环。有助于及时发现问题,纠正和解决问题。复盘的时间可以是定期的,在一个大需求上线之后,或者出现线上问题之后。


3、技术特点


3.1 组件化开发的痛点


在大型应用开发过程中,组件化开发的意义不仅局限于代码结构层面。组件化的作用体现在以下几个层面:



  • 1). 团队配合的利器。想想几十个人往同一份代码仓库里提交代码的场景。组件化可以避免无意义的代码冲突。

  • 2). 提高编译效率。对于大型应用,全源码编译一次的时间可能要几十分钟。将组件打包成 aar 之后可以减少需要编译的代码的数量,提升编译效率。

  • 3). 适应组织架构。将代码细分为各个组件,每个小团队只维护自己的组件,更方便代码权限划分。


那么,在实际开发过程中组件化开发会存在哪些问题呢?


1. 组件拆分不合理


这在从单体开发过渡到组件化开发的应用比较常见,即组件化拆分之后仍然存在某些模块彼此共用,导致提交代码的时候仍然会出现冲突问题。冲突包含两个层面的含义,一是代码文件的 Git 冲突,二是在打包合入过程中发布的 aar 版本冲突。比较常见的是,a 同学合入了代码到主干之后,b 同学没有合并主干到自己的分支就打包,导致发布的 aar 没有包含最新的代码。这涉及打包的问题,是另一个痛点问题,后面再总结。


单就拆分问题来看,避免上述冲突的一个解决办法是在拆分组件过程中尽可能解耦。根据我之前的观察,存在冲突的组件主要是数据结构和 SPI 接口。这是我之前公司没做好的地方——数据结构仓库和 SPI 接口是共用的。对于它们的组件化拆分,我待过的另一家公司做得更好。他们是如下拆分的,这里以 A 和 B 来命名两个业务模块。那么,在拆分的时候做如下处理,


模块:A-api
模块:A
模块:B-api
模块:B

即每个业务模块拆分成 api 和实现两部分。api 模块里包含需要共享的数据结构和 SPI 接口,实现模块里是接口的具体实现。当模块 A 需要和模块 B 进行交互的时候,只需要依赖 B 的 api 模块。可以参考开源项目:arch-android.


2. 打包合入的痛点


上面我们提到了一种冲突的情况。在我之前的公司里,每个组件有明确的负责人,在每个迭代开发的时候,组件负责人负责拉最新 release 分支。其他同学在该分支的开发需要经过负责人同意再合入到该分支。那么在最终打包的过程中,只需要保证这个分支的 aar 包含了全部最新的代码即可。也就是说,这种打包方式只关心每个 aar 的版本,而不关心实际的代码。因为它最终打包是基于 aar 而不是全源码编译。


这种打包方式存在最新的分支代码没有被打包的风险。一种可行的规避方法是,在平台通过 Git tag 和 commit 判断该分支是否已经包含最新代码。此外,还可能存在某个模块修改了 SPI 接口,而另一个模块没有更新,导致运行时异常的风险。


另一个公司是基于全源码编译的。不过,全源码编译只在最终打包阶段或者某个固定的时间点进行,而不是每次合入都全源码编译(一次耗时太久)。同时,虽然每个模块有明确的负责人,但是打包的 aar 不是基于当前 release 分支,而是自己的开发分支。这是为了保障当前 release 分支始终是可用的。合并代码到 release 分支的同时需要更新 aar 的版本。但它也存在问题,如果合并到 release 而没有打包 aar,那么可能导致 release 分支无法使用。如果打包了 aar 但是此时其他同学也打包了 aar,则可能导致本次打包的 aar 落后,需要重新打包。因此,这种合入方式也是苦不堪言。


有一种方法可以避免上述问题,即将打包和合入事件设计成一个消息队列。每次合入之前自动化执行上述操作,那么自然就可以保证每次操作的原子性(因为本身就是单线程的)。


对比两种打包和合入流程,显然第二种方式更靠谱。不过,它需要设计一个流程。这需要花费一点功夫。


3. 自动化切源码


我在之前的一家公司开发时,在开发过程中需要引用另一个模块的修改时,需要对另一个模块打 SNAPSHOT 包。这可行,但有些麻烦。之前我也尝试过手动修改 settings.gradle 文件进行源码依赖开发。不过,太麻烦了。


后来在另一个公司里看到一个方案,即动态切换到源码开发。可以将某个依赖替换为源码而只需要修改脚本即可。这个实践很棒,我已经把它应用到独立开发中。之前已经梳理过《组件化开发必备:Gradle 依赖切换源码的实践》.


3.2 大前端化开发


1. React Native


如今的就业环境,哪个 Android 开发不是同时会五六门手艺。跨平台开发几乎是不可避免的。


之前的公司为什么选择 React Native 而不是 Flutter 等新锐跨平台技术呢?我当时还刻意问了这个问题。主要原因:



  • 1). 首先是 React Native 相对更加成熟,毕竟我看了下 Github 第一个版本发布已经是 9 年前的事情了,并且至今依旧非常活跃。

  • 2). React Native 最近更新了 JavaScript 引擎,页面启动时间、包大小和内存占用性能都有显著提升。参考这篇文章《干货 | 加载速度提升15%,携程对RN新一代JS引擎Hermes的调研》.

  • 3). 从团队人才配置上,对 React Native 熟悉的更多。


React Native 开发是另一个领域的东西,不在本文讨论范围内。每个公司选择 React Native 可能有它的目的。比如,我之前的一家公司存粹是为了提效,即一次开发双端运行。而另一家公司,则是为了兼顾提效和动态化。如果只为提效,那么本地编译和打包 js bundle 就可以满足需求。若要追求动态化,就需要搭建一个 RN 包下发平台。实际上,在这个公司开发 RN 的整个流程,除了编码环节,从代码 clone 到最终发布都是在平台上执行的。平台搭建涉及的细节比较多,以后用到再总结。对于端侧,RN 的动态化依赖本地路由以及 RN 容器。


2. BFF + DSL


DSL 是一种 UI 动态下发的方案。相比于 React Native,DSL 下发的维度更细,是控件级别的(而 RN 是页面级别的)。简单的理解是,客户端和后端约定 UI 格式,然后按照预定的格式下发的数据。客户端获取到数据之后渲染。DSL 不适合需要复杂动画的场景。若确实要复杂动画,则需要自定义控件。


工作流程如下图中左侧部分所示,右侧部分是每个角色的责任。


DSL workflow.drawio.png


客户端将当前页面和位置信息传给 DSL 服务器。服务器根据上传的信息和位置信息找到业务接口,调用业务接口拉取数据。获取到数据后根据开发过程中配置的脚本对数据进行处理。数据处理完成之后再交给 DSL 服务器渲染。渲染完成之后将数据下发给客户端。客户端再根据下发的 UI 信息进行渲染。其中接口数据的处理是通过 BFF 实现的,由客户端通过编写 Groovy 脚本实现数据结构的转换。


这种工作流程中,大部分逻辑在客户端这边,需要预埋点位信息。预埋之后可以根据需求进行下发。这种开发的一个痛点在于调试成本高。因为 DSL 服务器是一个黑盒调用。中间需要配置的信息过多,搭建 UI 和编写脚本的平台分散,出现问题不易排查。


总结


所谓他山之石,可以攻玉。在这篇文章中,我只是选取了几个自己印象深刻的技术点,零零碎碎地写了很多,比较散。对于有这方面需求的人,会有借鉴意义。


作者:开发者如是说
来源:juejin.cn/post/7326268908984434697
收起阅读 »

threejs渲染高级感可视化风力发电车模型

web
本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图, 视频讲解及源码见文末 技术栈 three.js 0.165.0 vite 4.3.2 nodej...
继续阅读 »

本文使用threejs开发一款风力发电机物联可视化系统,包含着色器效果、动画、补间动画和开发过程中使用模型材质遇到的问题,内含大量gif效果图,



视频讲解及源码见文末



技术栈



  • three.js 0.165.0

  • vite 4.3.2

  • nodejs v18.19.0


效果图


一镜到底动画


一镜到底 (1).gif


切割动画


切割动画.gif


线稿动画


线稿动画.gif


外壳透明度动画


外壳透明度动画.gif


展开齿轮动画


展开齿轮动画.gif


发光线条动画


发光线条.gif


代码及功能介绍


着色器


文中用到一个着色器,就是给模型增加光感的动态光影


创建顶点着色器 vertexShader:


varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

创建片元着色器 vertexShader:


varying vec2 vUv;
uniform vec2 u_center; // 添加这一行

void main() {
// 泡泡颜色
vec3 bubbleColor = vec3(0.9, 0.9, 0.9); // 乳白色
// 泡泡中心位置
vec2 center = u_center;
// 计算当前像素到泡泡中心的距离
float distanceToCenter = distance(vUv, center);
// 计算透明度,可以根据实际需要调整
float alpha = smoothstep(0.1, 0.0, distanceToCenter);

gl_FragColor = vec4(bubbleColor, alpha);

创建着色器材质 bubbleMaterial


export const bubbleMaterial = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true, // 开启透明
depthTest: true, // 开启深度测试
depthWrite: false, // 不写入深度缓冲
uniforms: {
u_center: { value: new THREE.Vector2(0.3, 0.3) } // 添加这一行
},
});


从代码中可以看到 uniform声明了一个变量u_center,目的是为了在render方法中动态修改中心位置,从而实现动态光效的效果,


具体引用 render 方法中


 // 更新中心位置(例如,每一帧都改变)  
let t = performance.now() * 0.001;
bubbleMaterial.uniforms.u_center.value.x = Math.sin(t) * 0.5 + 0.5; // x 位置基于时间变化
bubbleMaterial.uniforms.u_center.value.y = Math.cos(t) * 0.5 + 0.5; // y 位置基于时间变化

官网案例 # Uniform,详细介绍了uniform的使用方法,支持通过变量对着色器材质中的属性进行改变


光影着色器.gif


从模型上可能看不出什么,下面的图是在一个圆球上加的这个效果


光影着色器-球体.gif


着色器中有几个参数可以自定义也可以自己修改, float alpha = smoothstep(0.6, 0.0, distanceToCenter);中的smoothstep 是一个常用的函数,用于在两个值之间进行平滑插值。具体来说,smoothstep(edge0, edge1, x) 函数会计算 x 在 edge0 和 edge1 之间的平滑过渡值。当 x 小于 edge0 时,返回值为 0;当 x 大于 edge1 时,返回值为 1;而当 x 在 edge0 和 edge1 之间时,它返回一个在 0 和 1 之间的平滑过渡值。


切割动画


切割动画使用的是数学库平面THREE.Plane和属性 constant,通过修改constant值即可实现动画,从normal法向量起至constant的距离为可展示内容。



从原点到平面的有符号距离。 默认值为 0.



constant取模型的box3包围盒的min值,至max值做补间动画,以下是代码示意


const wind = windGltf.scene
const boxInfo = wind.userData.box3Info;

const max = boxInfo.worldPosition.z + boxInfo.max.z
const min = boxInfo.worldPosition.z + boxInfo.min.z

let tween = new TWEEN.Tween({ d: min - 0.2 })
.to({ d: max + 0.1 }, 1000 * 2)
.start()
.onUpdate(({ d }) => {
clippingPlane.constant = d
})

详看切割效果图


切割动画.gif


图中添加了切割线的辅助线,可以通过右侧的操作面板显示或隐藏。


模型材质需要注意的问题


由于齿轮在风车的内容部,并且风车模型开启了transparent=true,那么计算透明度深度就会出现问题,首先要设置 depthWrite = true,开启深度缓存区,renderOrder = -1



这个值将使得scene graph(场景图)中默认的的渲染顺序被覆盖, 即使不透明对象和透明对象保持独立顺序。 渲染顺序是由低到高来排序的,默认值为0



threejs的透明材质渲染和不透明材质渲染的时候,会互相影响,而调整renderOrder顺序则可以让透明对象和不透明对象相对独立的渲染。


depthWrite对比


depthwrite对比.jpeg


renderOrder 对比


renderOrder 对比.jpeg


自定义动画贝塞尔曲线


众所周知,贝塞尔曲线通常用于调整关键帧动画,创建平滑的、曲线的运动路径。本文中使用的tweenjs就内置了众多的运动曲线easing(easingFunction?: EasingFunction): this;类型,虽然有很多内置,但是毕竟需求是无限的,接下来介绍的方法就是可以自己设置动画的贝塞尔曲线,来控制动画的执行曲线。


具体使用


// 使用示例
const controlPoints = [ { x: 0 }, { x: 0.5 }, { x: 2 }, { x: 1 }];
const cubicBezier = new CubicBezier(controlPoints[0], controlPoints[1], controlPoints[2], controlPoints[3]);

let tween = new TWEEN.Tween(edgeLineGr0up.scale)
.to(windGltf.scene.scale.clone().set(1, 1, 1), 1000 * 2)
.easing((t) => {
return cubicBezier.get(t).x
})
.start()
.onComplete(() => {
lineOpacityAction(0.3)
res({ tween })
})

在tween的easing的回调中添加一个方法,方法中调用了cubicBezier,下面就介绍一下这个方法


源码


[p0] – 起点  
[p1] – 第一个控制点
[p2] – 第二个控制点
[p3] – 终点

export class CubicBezier {
private p0: { x: number; };
private p1: { x: number; };
private p2: { x: number; };
private p3: { x: number; };

constructor(p0: { x: number; }, p1: { x: number; }, p2: { x: number; }, p3: { x: number; }) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}

get(t: number): { x: number; } {
const p0 = this.p0;
const p1 = this.p1;
const p2 = this.p2;
const p3 = this.p3;

const mt = 1 - t;
const mt2 = mt * mt;
const mt3 = mt2 * mt;
const t2 = t * t;
const t3 = t2 * t;

const x = mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x;

return { x };
}
}


CubicBezier支持get方法,通过四个关键点位信息,绘制三次贝塞尔曲线,参数t在0到1之间变化,当t从0变化到1时,曲线上的点从p0平滑地过渡到p3


mt = 1 - t;:这是t的补数(1减去t)。
mt2 = mt * mt; 和 mt3 = mt2 * mt;:计算mt的平方和立方。
t2 = t * t; 和 t3 = t2 * t;:计算t的平方和立方。


这是通过取四个点的x坐标的加权和来完成的,其中权重是基于t的幂的。具体来说,p0的权重是(1-t)^3p1的权重是3 * (1-t)^2 * tp2的权重是3 * (1-t) * t^2,而p3的权重是t^3


{ x: 0 },{ x: 0.5 },{ x: 2 },{ x: 1 } 这组数据形成的曲线效果是由start参数到end的两倍参数再到end参数


具体效果如下


贝塞尔曲线.gif


齿轮


齿轮动画


模型中自带动画


齿轮动画数据.jpeg


源码中有一整套的动画播放类方法,HandleAnimation,其中功能包含播放训话动画,切换动画,播放一次动画,绘制骨骼,镜头跟随等功能。


具体使用方法:


   // 齿轮动画
/**
*
* @param model 动画模型
* @param animations 动画合集
*/

motorAnimation = new HandleAnimation(motorGltf.scene, motorGltf.animations)
// 播放动画 take 001 是默认动画名称
motorAnimation.play('Take 001')

在render中调用


motorAnimation && motorAnimation.upDate()

齿轮展开(补间动画)


补间动画在齿轮展开时调用,使用的tweenjs,这里讲一下定位运动后的模型位置,使用# 变换控制器(TransformControls),代码中有封装好的完整的使用方法,在TransformControls.ts中,包含同时存在轨道控制器时与变换控制器对场景操作冲突时的处理。


使用方法:


/**
* @param mesh 受控模型
* @param draggingChangedCallback 操控回调
*/

TransformControls(mesh, ()=>{
console.log(mesh.position)
})

齿轮展开定位.jpeg


齿轮发光


发光效果方法封装在utls/index.ts中的unreal方法,使用的是threejs提供的虚幻发光通道RenderPass,UnrealBloomPass,以及合成器EffectComposer,方法接受参数如下



// params 默认参数
const createParams = {
threshold: 0,
strength: 0.972, // 强度
radius: 0.21,// 半径
exposure: 1.55 // 扩散
};

/**
*
* @param scene 渲染场景
* @param camera 镜头
* @param renderer 渲染器
* @param width 需要发光位置的宽度
* @param height 发光位置的高度
* @param params 发光参数
* @returns
*/


调用方法如下:



const { finalComposer: F,
bloomComposer: B,
renderScene: R, bloomPass: BP } = unreal(scene, camera, renderer, width, height, params)
finalComposer = F
bloomComposer = B
renderScene = R
bloomPass = BP
bloomPass.threshold = 0


除了调用方法还有一些需要调整的地方,比如发光时模型什么材质,又或者不发光时又是什么材质,这里需要单独定义,并在render渲染函数中调用


 if (guiParams.isLight) {
if (bloomComposer) {
scene.traverse(darkenNonBloomed.bind(this));
bloomComposer.render();
}
if (finalComposer) {
scene.traverse(restoreMaterial.bind(this));
finalComposer.render();
}
}

scene.traverse的回调中,检验模型是否为发光体,再进行材质的更换,这里用的标识是 object.userData.isLighttrue时,判定该物体为发光物体。其他物体则不发光


回调方法


function darkenNonBloomed(obj: THREE.Mesh) {
if (bloomLayer) {
if (!obj.userData.isLight && bloomLayer.test(obj.layers) === false) {
materials[obj.uuid] = obj.material;
obj.material = darkMaterial;
}
}

}

function restoreMaterial(obj: THREE.Mesh) {
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid];
// 用于删除没必要的渲染
delete materials[obj.uuid];
}
}


再场景的右上角我们新增了几个参数,用来调整线条的发光效果,下面通过动图看一下,图片有点大,请耐心等待加载


调试发光效果.gif


好啦,本篇文章到此,如看源码有不明白的地方,可私信~


最近正在筹备工具库,以上可视化常用的方法都将涵盖在里面


历史文章


three.js——商场楼宇室内导航系统 内附源码


three.js——可视化高级涡轮效果+警报效果 内附源码


高德地图巡航功能 内附源码


three.js——3d塔防游戏 内附源码


three.js+物理引擎——跨越障碍的汽车 可操作 可演示


百度地图——如何计算地球任意两点之间距离 内附源码


threejs——可视化地球可操作可定位


three.js 专栏


源码及讲解



源码 http://www.aspiringcode.com/content?id=…


体验地址:display.aspiringcode.com:8888/html/171422…


作者:孙_华鹏
来源:juejin.cn/post/7379906492038889512
收起阅读 »

都说PHP性能差,但PHP性能真的差吗?

今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题 先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。 <?php $dsn = 'mysql:host=localhost;dbname=test;charset=utf8...
继续阅读 »

今天本能是想测试一个PDO持久化,会不会带来会话混乱的问题
先贴一下PHP代码, 代码丑了点,但是坚持能run就行,反正就是做个测试。


<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$user = 'root';
$password = 'root';

// 设置 PDO 选项,启用持久化连接
$options = [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
];

try {
// 创建持久化连接
$pdo = new PDO($dsn, $user, $password, $options);

$stmt = $pdo->prepare("INSERT INTO test_last_insert_id (uni) VALUES (:uni);");
$uni = uniqid('', true);
$stmt->bindValue(':uni', $uni);
$aff = $stmt->execute(); //
if ($aff === false) {
throw new Exception("insert fail:");
}
$id = $pdo->lastInsertId();


function getExecutedSql($stmt, $params)
{
$sql = $stmt->queryString;
$keys = array();
$values = array();

// 替换命名占位符 :key with ?
$sql = preg_replace('/\:(\w+)/', '?', $sql);

// 绑定的参数可能包括命名占位符,我们需要将它们转换为匿名占位符
foreach ($params as $key => $value) {
$keys[] = '/\?/';
$values[] = is_string($value) ? "'$value'" : $value;
}

// 替换占位符为实际参数
$sql = preg_replace($keys, $values, $sql, 1, $count);

return $sql;
}


$stmt = $pdo->query("SELECT id FROM test_last_insert_id WHERE uni = '{$uni}'", PDO::FETCH_NUM);
$row = $stmt->fetch();
$value = $row[0];
if ($value != $id) {
throw new Exception("id is diff");
}

echo "success" . PHP_EOL;

} catch (PDOException $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Database connection failed: ' . $e->getMessage());
} catch (Exception $e) {
header('HTTP/1.1 500 Internal Server Error');
file_put_contents('pdo_perisistent.log', $e->getMessage() . PHP_EOL);
die('Exception: ' . $e->getMessage());
}

用wrk压测,一开始uniqid因为少了混淆参数还报了500,加了一下参数,用来保证uni值


% ./wrk -c100 -t2 -d3s --latency  "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.17ms 7.48ms 103.38ms 80.57%
Req/Sec 0.96k 133.22 1.25k 75.81%
Latency Distribution
50% 51.06ms
75% 54.17ms
90% 59.45ms
99% 80.54ms
5904 requests in 3.10s, 1.20MB read
Requests/sec: 1901.92
Transfer/sec: 397.47KB

1900 ~ 2600 之间的QPS,其实这个数值还是相当满意的,测试会话会不会混乱的问题也算完结了。
但是好奇心突起,之前一直没做过go和php执行sql下的对比,正好做一次对比压测


package main

import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"

_ "github.com/go-sql-driver/mysql"
"log"
)

var id int64 = time.Now().Unix() * 1000000

func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}

func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()

//// 设置连接池参数
//db.SetMaxOpenConns(100) // 最大打开连接数
//db.SetMaxIdleConns(10) // 最大空闲连接数
//db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var err error
uni := generateUniqueID()

// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}

lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}

// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}

fmt.Println("success")
})

_ = http.ListenAndServe(":8080", nil)

}

truncate表压测结果,这低于预期了吧


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.05ms 36.86ms 308.57ms 80.77%
Req/Sec 0.98k 243.01 1.38k 63.33%
Latency Distribution
50% 43.70ms
75% 65.42ms
90% 99.63ms
99% 190.18ms
5873 requests in 3.01s, 430.15KB read
Requests/sec: 1954.08
Transfer/sec: 143.12KB

开个连接池,清表再测,结果半斤八两


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 54.07ms 35.87ms 281.38ms 79.84%
Req/Sec 0.97k 223.41 1.40k 60.00%
Latency Distribution
50% 44.91ms
75% 66.19ms
90% 99.65ms
99% 184.51ms
5818 requests in 3.01s, 426.12KB read
Requests/sec: 1934.39
Transfer/sec: 141.68KB

然后开启不清表的情况下,php和go的交叉压测


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 52.51ms 43.28ms 436.00ms 86.91%
Req/Sec 1.08k 284.67 1.65k 65.00%
Latency Distribution
50% 40.22ms
75% 62.10ms
90% 102.52ms
99% 233.98ms
6439 requests in 3.01s, 471.61KB read
Requests/sec: 2141.12
Transfer/sec: 156.82KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.41ms 10.44ms 77.04ms 78.07%
Req/Sec 1.21k 300.99 2.41k 73.77%
Latency Distribution
50% 38.91ms
75% 47.62ms
90% 57.38ms
99% 69.84ms
7332 requests in 3.10s, 1.50MB read
Requests/sec: 2363.74
Transfer/sec: 493.98KB

// 这里骤降是我很不理解的不明白是因为什么
% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 156.72ms 75.48ms 443.98ms 66.10%
Req/Sec 317.93 84.45 480.00 71.67%
Latency Distribution
50% 155.21ms
75% 206.36ms
90% 254.32ms
99% 336.07ms
1902 requests in 3.01s, 139.31KB read
Requests/sec: 631.86
Transfer/sec: 46.28KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.47ms 10.04ms 111.41ms 90.21%
Req/Sec 1.15k 210.61 1.47k 72.58%
Latency Distribution
50% 41.17ms
75% 46.89ms
90% 51.27ms
99% 95.07ms
7122 requests in 3.10s, 1.45MB read
Requests/sec: 2296.19
Transfer/sec: 479.87KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 269.08ms 112.17ms 685.29ms 73.69%
Req/Sec 168.22 125.46 520.00 79.59%
Latency Distribution
50% 286.58ms
75% 335.40ms
90% 372.61ms
99% 555.80ms
1099 requests in 3.02s, 80.49KB read
Requests/sec: 363.74
Transfer/sec: 26.64KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 41.74ms 9.67ms 105.86ms 91.72%
Req/Sec 1.20k 260.04 2.24k 80.33%
Latency Distribution
50% 38.86ms
75% 46.77ms
90% 49.02ms
99% 83.01ms
7283 requests in 3.10s, 1.49MB read
Requests/sec: 2348.07
Transfer/sec: 490.71KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 464.85ms 164.66ms 1.06s 71.97%
Req/Sec 104.18 60.01 237.00 63.16%
Latency Distribution
50% 467.00ms
75% 560.54ms
90% 660.70ms
99% 889.86ms
605 requests in 3.01s, 44.31KB read
Requests/sec: 200.73
Transfer/sec: 14.70KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost/pdo_perisistent.php"
Running 3s test @ http://localhost/pdo_perisistent.php
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 50.62ms 9.16ms 85.08ms 75.74%
Req/Sec 0.98k 170.66 1.30k 69.35%
Latency Distribution
50% 47.93ms
75% 57.20ms
90% 61.76ms
99% 79.90ms
6075 requests in 3.10s, 1.24MB read
Requests/sec: 1957.70
Transfer/sec: 409.13KB

% ./wrk -c100 -t2 -d3s --latency "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 568.84ms 160.91ms 1.04s 66.38%
Req/Sec 81.89 57.59 262.00 67.27%
Latency Distribution
50% 578.70ms
75% 685.85ms
90% 766.72ms
99% 889.39ms
458 requests in 3.01s, 33.54KB read
Requests/sec: 151.91
Transfer/sec: 11.13KB

go 的代码随着不断的测试,很明显处理速度在不断的下降,这说实话有点超出我的认知了。
PHP那边却是基本稳定的,go其实一开始我还用gin测试过,发现测试结果有点超出预料,还改了用http库来测试,这结果属实差强人意了。


突然明白之前经常看到别人在争论性能问题的时候,为什么总有人强调PHP性能并不差。
或许PHP因为fpm的关系导致每次加载大量文件导致的响应相对较慢,比如框架laravel 那个QPS只有一两百的家伙,但其实这个问题要解决也是可以解决的,也用常驻内存的方式就好了。再不行还有phalcon


我一直很好奇一直说PHP性能问题的到底是哪些人, 不会是从PHP转到其他语言的吧。


% php -v
PHP 8.3.12 (cli) (built: Sep 24 2024 18:08:04) (NTS)
Copyright (c) The PHP Gr0up
Zend Engine v4.3.12, Copyright (c) Zend Technologies
with Xdebug v3.3.2, Copyright (c) 2002-2024, by Derick Rethans
with Zend OPcache v8.3.12, Copyright (c), by Zend Technologies

% go version
go version go1.23.1 darwin/amd64

image.png


这结果,其实不太能接受,甚至都不知道原因出在哪了,有大佬可以指出问题一下吗


加一下时间打印再看看哪里的问题


package main

import (
"database/sql"
"fmt"
"net/http"
"sync/atomic"
"time"

_ "github.com/go-sql-driver/mysql"
"log"
)

var id int64 = time.Now().Unix() * 1000000

func generateUniqueID() int64 {
return atomic.AddInt64(&id, 1)
}

func main() {
dsn := "root:root@tcp(localhost:3306)/test?charset=utf8"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("Error opening database: %v", err)
}
defer func() { _ = db.Close() }()

// 设置连接池参数
db.SetMaxOpenConns(100) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
reqStart := time.Now()
var err error
uni := generateUniqueID()

start := time.Now()
// Insert unique ID int0 the database
insertQuery := `INSERT INTO test_last_insert_id (uni) VALUES (?)`
result, err := db.Exec(insertQuery, uni)
fmt.Printf("insert since: %v uni:%d \n", time.Since(start), uni)
if err != nil {
log.Fatalf("Error inserting data: %v", err)
}

lastInsertID, err := result.LastInsertId()
if err != nil {
log.Fatalf("Error getting last insert ID: %v", err)
}

selectStart := time.Now()
// Verify the last insert ID
selectQuery := `SELECT id FROM test_last_insert_id WHERE uni = ?`
var id int64
err = db.QueryRow(selectQuery, uni).Scan(&id)
fmt.Printf("select since:%v uni:%d \n", time.Since(selectStart), uni)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

if id != lastInsertID {
log.Fatalf("ID mismatch: %d != %d", id, lastInsertID)
}

fmt.Printf("success req since:%v uni:%d \n", time.Since(reqStart), uni)
})

_ = http.ListenAndServe(":8080", nil)

}

截取了后面的一部分输出,这不会是SQL库的问题吧,


success req since:352.310146ms uni:1729393975000652 
insert since: 163.316785ms uni:1729393975000688
insert since: 154.983173ms uni:1729393975000691
insert since: 158.094503ms uni:1729393975000689
insert since: 136.831695ms uni:1729393975000697
insert since: 141.857079ms uni:1729393975000696
insert since: 128.115216ms uni:1729393975000702
select since:412.94524ms uni:1729393975000634
success req since:431.383768ms uni:1729393975000634
select since:459.596445ms uni:1729393975000601
success req since:568.576336ms uni:1729393975000601
insert since: 134.39147ms uni:1729393975000700
select since:390.926517ms uni:1729393975000643
success req since:391.622183ms uni:1729393975000643
select since:366.098937ms uni:1729393975000648
success req since:373.490764ms uni:1729393975000648
insert since: 136.318919ms uni:1729393975000699
select since:420.626209ms uni:1729393975000640
success req since:425.243441ms uni:1729393975000640
insert since: 167.181068ms uni:1729393975000690
select since:272.22808ms uni:1729393975000671

单次请求的时候输出结果是符合预期的, 但是并发SQL时会出现执行慢的问题,这就很奇怪了


% curl localhost:8080
insert since: 1.559709ms uni:1729393975000703
select since:21.031284ms uni:1729393975000703
success req since:22.62274ms uni:1729393975000703

经群友提示还和唯一键的区分度有关,两边算法一致有点太难了,Go换了雪法ID之后就正常了。
因为之前 Go这边生成的uni值是递增的导致区分度很低,最终导致并发写入查询效率变低。


% ./wrk -c100 -t2 -d3s --latency  "http://localhost:8080"
Running 3s test @ http://localhost:8080
2 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 44.51ms 24.87ms 187.91ms 77.98%
Req/Sec 1.17k 416.31 1.99k 66.67%
Latency Distribution
50% 37.46ms
75% 54.55ms
90% 80.44ms
99% 125.72ms
6960 requests in 3.01s, 509.77KB read
Requests/sec: 2316.02
Transfer/sec: 169.63KB

2024-10-23 更新


今天本来是想验证一下有关,并发插入自增有序的唯一键高延迟的问题,发现整个有问题的只有一行代码。
就是在查询时,类型转换的问题,插入和查询都转换之后,空表的情况下QPS 可以到4000多。即使在已有大数据量(几十万)的情况也有两千多的QPS。
现在又多了一个问题,为什么用雪花ID时不会有这样的问题。雪花ID也是int64类型的,这是为什么呢。


// 旧代码
err = db.QueryRow(selectQuery, uni).Scan(&id)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}


// 新代码 变化只有一个就是把uni 转成字符串之后就没有问题了
var realId int64
err = db.QueryRow(selectQuery, fmt.Sprintf("%d", uni)).Scan(&realId)
if err != nil {
log.Fatalf("Error selecting data: %v", err)
}

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

AI 治好了我的 CSS 框架恐惧症

web
00. 写在前面 大家好,我是大家的林语冰。 前端工程中,苦于“前 CSS3 时代”相对落后的原生语法,CSS 架构一直是前端架构师的痛点之一。 因此,我们一般会在项目里引入更先进的 CSS 框架或预处理器,比如国内比较火的 SCSS/LESS,或者海外人气爆...
继续阅读 »

00. 写在前面


大家好,我是大家的林语冰。


前端工程中,苦于“前 CSS3 时代”相对落后的原生语法,CSS 架构一直是前端架构师的痛点之一。


因此,我们一般会在项目里引入更先进的 CSS 框架或预处理器,比如国内比较火的 SCSS/LESS,或者海外人气爆棚的 Tailwind CSS/UnoCSS 等。


00-css.png


问题在于,当我们学习从原生 CSS 升级到 SCSS,或者老板要求从 SCSS 迁移到人气更高的 Tailwind 框架时,不同 CSS 框架的学习成本也不容小觑。


本质上而言,这些 CSS 框架提供的高级语法最终都会被转译为原生 CSS,而这种语法转换工作恰恰是 AI 编程助手的拿手好戏。


所以,本期我想分享如何利用 VSCode 和 MarsCode AI 插件,在原生 CSS 和不同 CSS 框架中无缝衔接,直接让 AI 解放我们的双手,不必再因为不同的 CSS 框架而头大。


01. 前期准备


本文的示例代码是用原生 CSS 实现一个仿真的 iPhone 手机,类似的产品模型网页预览效果在很多电商网站都比较常见,最终实现效果如下所示:


01-iphone.png


上述手机模型对应的原生的 HTML 结构和 CSS 代码如下:


(PS:此处代码仅供参考,大家可以用自己的样式代码进行后续测试,不需要关注这里的代码细节)


02-html.png


03-css.png


02. VSCode AI 插件


假设上述示例是项目遗留的旧代码,我们想要使用其他 CSS 框架重构为可维护的高级样式代码,就需要和 AI 助手联动,让 AI 帮我们写代码。


首先,我们需要可以使用手机号或邮箱注册一个账号,然后在 VSCode 里搜索和安装 MarsCode 扩展插件,登录后就可以在 VSCode 里直接使用 AI 编程助手。


另外,豆包 MarsCode 使用的是字节跳动的国产大模型,所以我们不需要考虑科学上网等复杂问题。


接着就可以让 AI 干活了,我们可以把原生 CSS 抽离到单独的样式文件中,然后让 AI 把它转译为 SCSS 版本,只需要通过聊天的方式命令 AI 执行任务即可,不需要我们手动敲一行代码。


04-ai.png


MarsCode 比较人性化的一点是,生成的代码可以直接一键保存到新文件中,然后我们可以测试生成的 SCSS 代码是否和原生版本等效,如果效果有偏差,可以尝试多生成几次。


我这里生成的 SCSS 代码也可以正常工作,因为样式逻辑并不复杂,但所有原生 CSS 都被重构为 SCSS 的嵌套语法。


毋庸置疑,在代码编译或重构方面,AI 可以明显提高我们的生产力,哪怕是复杂的样式代码也不例外。


03. 样式构建


目前前端工程中,大部分项目可能会依赖 Vite 工具链构建,因此我们也可以引入 Vite,再集成需要的 CSS 框架。


Vite 配置在官方文档有具体介绍,以 SCSS 为例,我们需要安装模块,然后更改配置文档。


05-vite.png


实际测试中,我偷懒不看文档,而是直接询问 AI 助手如何配置,MarsCode 虽然给出了答案,但是答案未必有效,可能出现配置失败,或者配置生效,但不是最佳配置的情况,我猜可能跟目前 MarsCode 的预训练模型的局限性有关。


这也说明和 AI 编程助手一起使用时,我们最好还是有对应 CSS 框架的知识储备,才能放心地偷懒,遇到 bug 也能了然于胸。


另外,在 CSS 框架选型方面,目前我更推荐 UnoCSS,因为它是一个同构引擎,这意味着,UnoCSS 默认兼容 Tailwind 同款语法,也能够支持类似 SCSS 的功能,更加通用。


在 AI 生成代码过程中,不同 CSS 框架语法本身不会给 AI 带来太大负担,我们同样只需要通过对话,就能生成对应框架的代码。


比如我让 MarsCode 生成的 UnoCSS/Tailwind 代码,也能一键实现相同的样式效果。


06-uno.png


高潮总结


CSS 框架或预处理器的本质是提供了某些比原生 CSS 高级的语法,方便我们在前端工程中实现可维护的样式架构,但它们最终还是要编译为原生 CSS。


一般而言,在不同的 CSS 框架中迁移,我们需要重新学习和手动重构,AI 编程助手可以辅助我们一键迁移。


在 VSCode 中,我们可以借助 MarsCode 插件,轻松地将原生 CSS 代码重构为不同 CSS 框架的代码,无需手动敲一行代码,这提高了我们的开发效率,但同时也要注意 AI 工具的局限性。


目前 AI 无法淘汰程序员,但 AI 会淘汰不懂 AI 的程序员。你可以注册和安装 VSCode 插件,在 VSCode 中提前尝试 AIGC 时代的低代码编程方式。


官方链接和二维码在这里分享给大家:http://www.marscode.cn/events/s/ik…


#豆包MarsCode 双节创意征文话题 #豆包 MarsCode 放码过来


poster.png


作者:前端俱乐部
来源:juejin.cn/post/7424016262094012443
收起阅读 »

纠结多年终于决定彻底放弃Tailwindcss

web
上图来源Fireship 团队代价=深入成本 Tailwindcss很容易火,因为从看到它的的第一眼就会觉得超级直观、超级简单、超级方便,所以无论是写文章 出视频 都很容易吸引一波流量。 但是真要用在企业级应用我们需要考虑全部的复杂性,你至少要吃透官方文档的...
继续阅读 »

image.png



上图来源Fireship



团队代价=深入成本


Tailwindcss很容易火,因为从看到它的的第一眼就会觉得超级直观、超级简单、超级方便,所以无论是写文章 出视频 都很容易吸引一波流量。


但是真要用在企业级应用我们需要考虑全部的复杂性,你至少要吃透官方文档的大部分内容,最好是写几个稍微复杂点的Demo,光是吃透文档就需要至少10小时以上的成本你才能彻底在企业级应用all in tailwind,如果你的团队有10名前端同学,你们将会付出100个小时的代价,这些代价不光是个人的,更是企业的损失,而花了这100小时掌握之后能够靠那一点点便捷提速弥补损失吗?不能。或许100小时早就用以前的方式写完了全部样式。团队还会扩大,新招进来的同学还得培训一下。


范式强偏好


Tailwindcss是非常opinionated强偏好的,他会鼓励一种他们特定的范式和规则,这种规则不是通用的,是tw创新的。那scss less是不是强偏好呢?不是,因为你还是以标准的css范式书写,只是scss less给你提供了额外的语法和工具方法 你的范式没有改变。


tw强偏好的范式包括不限于:



  • tailwindconfig 配置文件

  • 默认主题、工具类

  • 行内class书写规则

  • IDE插件


强偏好本身没有对错之分,通常我们使用UI组件库就是强偏好的,但是对于样式的书写,这种强偏好会缺少一定的规范一致性,说白了就是潜规则太多了。


强IDE插件依赖


没有IDE的插件提示,tw基本不可用,因为你可用的类名强依赖与上面说的范式中的tailwindconfig配置文件。


但是这好像也没什么问题,装个插件也很轻松,事情没这么简单,你失去了静态类型检查 和可测试性。假设你写了个错误的类名shadows,tailwind插件可不会给你报红,而传统的css样式文件是会报红的。


image.png


image.png


既然静态阶段无法发现这个错误,那编译时能不能发现呢?也不能,tw会将主题中未定义的类名 当成你自己的类名,所以对tw来说不是错误。


单元测试?很遗憾,也不行,这个范式最大的好处也是最大的缺点,样式全部在类目中,你不可能去equal所有的类名 这样就没有用tw的意义了。


所以tw最方便的地方,也是最容易出错且难以被发现的地方。


完全错误的主题范式


官方文档提供了Dark Mode暗色主题切换的方式,但是如果现在客户提个需求,需要增加4套颜色主题 和亮暗色无关 就是额外的主题,你会发现tw根本没有考虑到这点(或者说很难实现,网上几乎没有解决方案,我有但我不说😝(下面补充解释了


tw是通过类名中以dark:前缀开头来表示暗色下的样式,默认不加就是亮色, 所以你根本无法增加这两种主题以外的更多主题,你只能在亮色暗色这两之间切换,这就是tw官方强偏好导致的弊端。


我们假设,即使tw实现了可以增加主题前缀比如 onedark: monokai: ...,那么你需要在每一个元素类名上 书写所有这些前缀的样式


<div
className="
bg-blue-500 //亮色
dark:bg-blue-700 //暗色
onedark:bg-black-900 //onedark主题
monokai:bg-gold-600 //monokai主题
kanagawa:bg-green-200 //kanagawa主题
"

></div>

真的你会写疯掉,因为每增加一个主题,意味着你要在源码中所有元素身上加上新的主题和样式类名,想象一下如果有20个主题,你一个标签的类名可能就占了100行。


并且你无法动态增加主题,因为tw是编译时的,生产环境下,你无法实现让用户自己任意配置主题、持久、载入这样的功能。


总结


文章还会更新,想当什么补充什么。以上几个最大的痛点是导致我对这个库关注多年,尝试多次,却迟迟没有投入使用,最终决定放弃的原因。我相信肯定很多同学会有同感,也会有很多持反对意见,非常欢迎评论区讨论,如果真能解决这几个大痛点,我会毅然决然All in tw。




↓↓↓↓🆕 以下为更新内容 时间升序↓↓↓↓


2024-08-22 17:45:21


难以调试


image.png



实现复杂UI会让类名又臭又长,无法根据类名理解样式,影响对html结构的浏览


image.png


来对比看一下传统类名的可读性,这是MDN网站的类名,干净整洁,一眼就知道每一个标签块代表什么内容



类名即样式导致dev tool中无法通过style面板根据特定一类元素修改样式,因为你改的是工具类名 而不是一类元素的类名,例如修改.text-left {text-align: right} 会将所有元素的样式修改完全不符合预期


菜就多练?


好吧我猜到评论区会有此类不和谐的声音,怪我没有事先叠甲,但是文章的开始其实已经说的很清楚了,个人能力再强是没用的,开发从来不是一个人的事,在公司需要跟同事配合,在开源社区需要和世界各地的开源爱好者协作。


如果你是组长、技术经理、CTO甚至老板,你一定要站在团队的角度对新兴技术栈评估收益比,因为对于企业来说商业价值永远是第一位的,所以你不能只考虑自己的效率,还要考虑团队整体的效率和质量。


如果你是开源作者,你也要为贡献者的参与门槛考虑,如果你的技术栈不是主流 只是一小挫人会用 甚至难度极高,那么你很难收获世界各地的爱心,只能自己一个人默默发电。你甚至要考虑技术栈的可替换性,因为我们大部分的依赖库都是开源的,人家也是为爱发电,意味着人家很有可能哪天累了不再维护了,你要留足能够用其他库或框架平滑替换的可能,否则为了某个库的废弃你可能需要做大量的重构工作甚至Breaking Change破坏性升级,再甚至你也没办法坚持下去了,因为你花了大量的时间在填坑而不是专注于自己项目的开发。


复杂性守恒原理


泰斯勒定律 复杂性守恒,临界复杂性不能凭空消失 只能被转移。我经常用这个原理审视各种新兴技术栈的两面性,因为你们懂得-前端娱乐圈,经常会出现很多让你快乐的新东西,而往往容易忽视背后的代价。当我们收获巨大的简化后,一定要思考 曾经的复杂性被转移到哪里去了呢?如果你能搞清楚两面性,仔细评估后再做决定,会走的更顺。


就如上面所述,tw在简化的背后,牺牲了静态类型检查、单元测试、调试、运行时动态主题载入、文档强依赖、IDE插件强依赖、构建工具强依赖等等诸多缺点。


2024-08-23 17:53:51


关于tw错误主题范式的补充



掘友还是很多大佬的,评论区发表了很多关于解决主题受限于默认和dark这两种的局限,这里补充一下我自己的方式

tw配置文件 theme中不配置darkMode,将所有主题值绑定css变量如 colors: {primary: 'var(--primary)'},然后依然是动态控制css变量来切换主题。


坏处很多: classname不再允许使用dark变量;tw配置麻烦,变量套变量需要两边维护,css变量文件 和tw配置要保持同步;


我为什么前面不把解决办法说出来?因为不重要。


tw是范式强偏好的,首先理解什么是范式?我的理解是很有fan的方式就是范式,有fan的前提是流行、统一。


所以我们在使用强偏好的库时,一定要在范式之内,不要自己创新方式,否则你会脱离流行、统一、一致性,并且会因为库的迭代升级导致适配问题,并且会与其他人无法达成共识 你们之间的技术经验产生越来越大的偏差 为合作带来困扰。



如果我遵循tw范式不魔改,与别人协作只需要告诉别人“看tw文档就行了”;但如果我不遵循,魔改了主题范式,我需要格外提醒别人“注意! 我们的主题不是按tw用的 请不要写dark:xxx”


这还只是一例,如果项目中有10例你自己创新的范式别人就很难快速上手了



2024-08-24 00:36:52


与tw同样方便的CSS in JS用法


image.png


上图是react中使用styled-component(后面简称sc)结合style工具写的一段样式,它既能拥有sc组件即样式的好处 又能拥有类似vue中样式分离且scoped的方便。


还能倒过来,jsx在上面,style在下面


image.png


通用组件通常用sc组件即样式来定义


const Button = styled.button`...`

view组件(业务域)通常结合sc组件和style来灵活使用


...
return style`font-size: 12px;`(
<div>...</div>
)

我们还能将常用样式抽离出来,达到如同tw的方便程度


return style`
${[button.md, bg.200, text.color.primary]}
`
(<Button>...</Button>)

如果样式很长,你可以抽离,也可以直接折叠,完全不需要像tw那样还需要vscode插件


image.png


篇幅关系,只是简单介绍,后续可能单独出个文章细讲css in js


2024-08-27 12:52:07


@apply解决类名过多的问题?


评论区出现多个建议用@apply在css复用类名的样式来减少class中书写类名,因此觉得有必要单独拿出来讲一下给大家避坑。


结论是千万万万不要这么用!这就是文档没看完,上手就用犯的错误,10小时的学习成本你是逃不掉的,不然以后麻烦就会找上你。


在tw官方文档中明确强调不要以减少类名为目的使用@apply,鼓励你就把所有类名写在class中。


image.png



上图来源 tailwindcss.com/docs/reusin…



除此之外,@apply语法其实是一个被废弃的css标准语法,曾经在chromium内核中被短暂实现过,后来废弃掉了,废弃的原因也是因为会破坏常规书写类名+样式的范式,会导致用户无节制的重用类名样式,最终无法溯源,修改困难。


image.png


image.png


作者:于谦
来源:juejin.cn/post/7405449753741328393
收起阅读 »

第三届OpenHarmony技术大会|Watch生态分论坛成功举行

2024年10月12日第三届OpenHarmony技术大会在上海成功举办。开源四年以来,OpenAtom OpenHarmony(以下简称“OpenHarmony”)不仅在覆金融,电力,交通,教育,医疗,航天等众多国计民生行业,也在消费电子领域逐步确定影响力,...
继续阅读 »

2024年10月12日第三届OpenHarmony技术大会在上海成功举办。开源四年以来,OpenAtom OpenHarmony(以下简称“OpenHarmony”)不仅在覆金融,电力,交通,教育,医疗,航天等众多国计民生行业,也在消费电子领域逐步确定影响力,OpenHarmony项目群工作委员会主席龚体在大会上给大家分享了OpenHarmony在穿戴领域的突破和发展。

本次大会专门为穿戴产业安排了生态分论坛,论坛聚集了穿戴产业上下游伙伴,有芯片商,解决方案商,品牌商,应用厂商,设计厂家,Top独立开发者以及应用分发商,与会产业大佬们分享了基于OpenHarmony做穿戴产品的经验,并对当前穿戴行业遇到的问题展开了讨论,对穿戴产业未来的发展做了展望。 这不仅为OpenHarmony在穿戴领域的开源生态拓展提供了重要参考,也为行业合作与创新开辟了新的思路,必将推动智能穿戴设备的跨越式发展。

华为OpenHarmony使能部副部长李彦举主持了本次论坛。OpenHarmony社区执行总监陶铭;华为OpenHarmony使能专家黎亮齐;华为技术专家,OpenHarmony兼容性工作组成员纪永;华为穿戴软件架构师段谦;上海海思穿戴芯片产品薛总监;恒玄科技商务拓展副总裁高亢;深圳市领为创新科技有限公司CEO林义巡;深圳市魔样科技股份有限公司执行总裁黄立阳;深圳市岍丞技术有限公司总经理张昊;Gomore inc.执行长郭信甫;OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生;上海喜马拉雅科技有限公司IOT事业部副总经理娄建林;TOP独立开发者李尚儒;深圳仪品信息技术有限公司设计总监董奎出席本场论坛并发表主题演讲。

(华为OpenHarmony使能部副部长李彦举发言)

OpenHarmony社区执行总监陶铭即以《OpenHarmony赋能穿戴行业,开启智能新篇章》为主题发表报告。演讲中陶铭对当前OpenHarmony助力穿戴生态伙伴实现技术领先,商业成功做出了肯定,对OpenHarmony在穿戴行业做出更大的成绩很期待,陶总监表示OpenHarmony社区将持续支持穿戴行业更大发展。

(OpenHarmony社区执行总监陶铭发言)

华为OpenHarmony的使能专家黎亮齐强调了智能穿戴设备统一架构的重要性,他指出统一架构对屏蔽不同平台和设备之间的内部差异,确保各类应用和服务的兼容具有非常重要意义。通过基于统一的架构方案,API标准化,互联标准化,统一工具,兼容性评测,以及统一的应用市场,可以有效解决当前穿戴北向生态的问题,从而提升行业效能,降低生态伙伴成本。 这一生态战略不仅能促进行业伙伴合作,而且能推动智能穿戴设备行业的良性发展。他表示:独行快,众行远,期待更多的生态伙伴一起把穿戴行业做的更大,事业做的更长久。

(华为OpenHarmony使能专家黎亮齐发言)

华为技术专家、OpenHarmony兼容性工作组成员纪永介绍了当前兼容性工作组的职责,对OpenHarmony兼容性评测进行了解读,他强调:兼容性测评服务是看护OpenHarmony生态的重要手段,针对穿戴设备品类,兼容性工作组专门制定一套行之有效的测评标准,并与OpenHarmony 5.0一起发布,发布后可以有效防止生态的分裂,并将助力穿戴行业更健康发展。

(华为技术专家、OpenHarmony兼容性工作组成员纪永发言)

华为穿戴软件架构师段谦围绕OpenHarmony助力华为穿戴产品软件平台构建,分享了华为基于OpenHarmony构建的支持穿戴多系列产品的软件平台和华为穿戴今年秋季发布的玄玑感知系统,段谦的介绍让与会者了解到OpenHarmony底座能力、生态和产品三者的相辅相成,在相互协同前进的过程中,可以不断给消费者提供满意的产品。 段谦表示华为穿戴将会和业界一起持续的支持和贡献OpenHarmony社区,支持穿戴产业的发展。

(华为穿戴软件架构师段谦)

面向穿戴迈入OpenHarmony时代,上海海思分享了基于OpenHarmony在智能穿戴产业的探索与创新,构建底层芯片底座。上海海思W610 是首家支持OpenHarmony智能穿戴解决方案,具有卓越处理能力、领先连接和定位能力、智能化语音图片交互等特性,助力产业上下游做出更有竞争力的产品,大会现场上海海思展台展示了伙伴(魔样,领为,北斗,宜准,酷泰丰,库觅等)的10+款商用产品,此外百度、喜马拉雅、腕上阅读等多家应用厂家也展示了他们在穿戴上的适配效果。未来上海海思也将全力的支持OpenHarmony 穿戴产业的发展,引领智能穿戴将会朝着更智能、更强连接、更大生态方向演进。

(上海海思穿戴芯片产品薛总监发言)

恒玄科技商务拓展副总裁高亢强调了发展穿戴生态的重要意义,并在本次论坛中分享了恒玄科技基于OpenHarmony在可穿戴方案应用探索。恒玄科技基于OpenHarmony的技术底座,进行了多方面的探索和推进工作,致力于提升产品的功能和用户体验。这些努力不仅推动了企业自身的创新,也为整个穿戴设备的生态发展注入了新活力,他说:芯片 + OpenHarmony + 行业伙伴一定会实现1+1+1>3的效果。

(恒玄科技商务拓展副总裁高亢发言)

深圳市领为创新科技有限公司CEO林义巡以《依托OpenHarmony生态打造智能穿戴科技第三极》为主题发表演讲。他分享了领为创新科技在智能手表上的多年耕耘历程,也对比剖析了海思芯片和OpenHarmony的优势:开放、包容、互通互联、潜力无限;领为科技为更是为大会带来了多款基于海思W610+OpenHarmony的商用穿戴展品,他对OpenHarmony助力穿戴第三级品牌崛起有很强的信心。

(深圳市领为创新科技有限公司CEO林义巡发言)

深圳市魔样科技股份有限公司执行总裁黄立阳介绍了其基于海思W610+OpenHarmony打造的穿戴解决方案,智能穿戴设备与OpenHarmony融合创新与开发心得展开分享。黄立阳表示有了OpenHarmony的加持,将有力的推动穿戴设备向更加智能化、专业化及多样化方向发展,帮助中小厂家拉平与大厂的技术代差。 “依托OpenHarmony平台优势,将推出更多定制化,专业化的解决方案引领行业的创新。”黄立阳说。

(深圳市魔样科技股份有限公司执行总裁黄立阳发言)

深圳市岍丞技术有限公司总经理张昊分享了岍丞技术这几年基于OpenHarmony为底座打造的AOS系统,这一系统实现了针对多种设备,包括耳机,手表,AR眼镜等众多带屏的显示方案。张昊表示岍丞将专注于以技术作为核心驱动力,做好A-IOT应用和服务。

(深圳市岍丞技术有限公司总经理张昊发言)

Gomore Inc. 执行长郭信甫从AI算法与IC搭配角度切入,分享了他对于人工智能算法与晶片发展如何赋能穿戴装置健康应用的看法。他强调,AI算法的演进离不开集成电路(IC)的发展,两者的紧密结合对推动穿戴设备健康应用发展至关重要。郭信甫呼吁行业关注这一重要结合,携手利用人工智能提升全球人口的健康水平。

(Gomore inc 执行长郭信甫发言)

OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生以《轻应用市场助力OpenHarmony Watch生态》为主题发表演讲。他观察到穿戴设备的智能化发展,对应用的诉求越来越强烈。面对七国八制的现状,如何给穿戴设备提供统一的应用商店,实现行业效率更高,是当前面临的关键的问题。他表示鸿信智联将帮助穿戴行业做好应用分发商店,并呼吁行业伙伴们加入到穿戴应用生态,助力穿戴生态发展,实现穿戴行业的互利共赢。

(OpenHarmony项目群技术指导委员会委员、深圳鸿信智联数字科技有限公司CEO张兆生发言)

上海喜马拉雅科技有限公司IOT事业部副总经理娄建林分享了喜马拉雅在音频领域的多年耕耘成果,以及携手海思+OpenHarmony打造腕间声音标杆体验的实践和探索经历。 娄建林指出,与OpenHarmony的合作,不仅丰富了穿戴产品的功能,也推动了音频内容消费方式变革,同时也对未来一起合作做出“更有温度”的用户体验提出了展望。

(上海喜马拉雅科技有限公司IOT事业部副总经理娄建林发言)

吉林市独立开发者李尚儒的演讲围绕基于OpenHarmony穿戴打造腕上阅读器以实现极致的长文本阅读体验展开。李尚儒以独立开发者的角度分享了基于海思 + OpenHarmony穿戴设备上的应用开发体验,完善的开发工具,容易上手的开发语言,丰富的API文档都成为开发者的福音。他希望在手腕阅读器应用上用更细节、更精致、更人性化的交互设计,打破穿戴设备的屏幕局限,未来通过小小的表盘即可阅读长篇巨著,真正实现文字不限载体的灵活管理。

(吉林市九七软件部CTO李尚儒发言)

表盘作为智能手表生态中的重要组成部分,它不仅能够提升用户体验,丰富生态内容,还具有潜在的商业价值。深圳仪品信息技术有限公司设计总监董奎从设计角度出发,对表盘创意助力OpenHarmony穿戴生态进行了展望。董奎指出,在OpenHarmony Watch 生态系统的加持下,表盘不仅仅是美学的体现,更是功能与用户体验的完美融合,能为用户带来全新的智能、便捷、个性化体验,也为 OpenHarmony Watch 生态系统注入新的活力。

(深圳仪品信息技术有限公司设计总监董奎发言)

现场与会的行业大咖KUMI总裁郭锦炜表示:穿戴生态依托OpenHarmony强大的技术底座支撑、繁荣的生态发展以及不断扩展的行业应用,必将全面开花,穿戴领域必将迎来蓬勃繁荣。

经过一下午的热烈讨论,与会专家一致认为,当前,OpenHarmony是支撑手表产业标准化,规模化,智能化发展的更优选择,并同意加入OpenHarmony Watch SIG,实现手表生态的共建,共享,共赢。也非常期待明年的大会,为产业带来更新的技术,更多的产品,更具创新的体验。

收起阅读 »

前端大佬都在用的useFetcher究竟有多强?

web
useFetcher:让数据管理变得如此简单 大家好,今天我要和你们分享一个让我惊喜万分的小工具——useFetcher。说实话,第一次用它的时候,我感觉自己像是发现了新大陆!它彻底改变了我处理数据预加载和跨组件更新的方式。 alovajs简介 在介绍useF...
继续阅读 »

useFetcher:让数据管理变得如此简单


大家好,今天我要和你们分享一个让我惊喜万分的小工具——useFetcher。说实话,第一次用它的时候,我感觉自己像是发现了新大陆!它彻底改变了我处理数据预加载和跨组件更新的方式。


alovajs简介


在介绍useFetcher之前,我们先来聊聊alovajs。它是一个革命性的新一代请求工具,可以大大简化我们的API集成流程。 与react-query和swrjs等hooks库不同,alovajs提供了针对各种请求场景的完整解决方案。


alovajs的强大之处在于:



  • 它将API的集成从7个步骤降低为只需要1个步骤

  • 提供了15+个针对特定场景的"请求策略"

  • 不仅能在客户端使用,还提供了服务端的请求策略


如果你想深入了解alovajs,强烈推荐去官网 alova.js.org 看看。相信你会像我一样,被它的强大功能所吸引。


useFetcher的妙用


现在,让我们聚焦到今天的主角——useFetcher。这个小工具真的太棒了,它让我轻松实现了一些以前觉得很复杂的功能。


数据预加载


想象一下,你正在开发一个分页列表,希望在用户浏览当前页面时就预加载下一页的数据。useFetcher可以轻松实现这一点:


const { fetch } = useFetcher({ updateState: false });

const currentPage = ref(1);
const { data } = useWatcher(() => getTodoList(currentPage.value), [currentPage], {
immediate: true
}).onSuccess(() => {
fetch(getTodoList(currentPage.value + 1));
});

这段代码会在当前页加载成功后,自动预加载下一页的数据。是不是感觉很简单?我第一次实现这个功能时,都被自己的效率惊到了!


跨组件更新


另一个让我惊喜的功能是跨组件更新。假设你在一个组件中修改了todo数据,想要在另一个组件中更新列表。useFetcher配合method快照匹配器可以轻松实现:


const { fetch } = useFetcher();

const handleSubmit = () => {
// 提交数据...
const lastMethod = alovaInstance.snapshots.match({
name: 'todoList',
filter: (method, index, ary) => index === ary.length - 1
}, true);
if (lastMethod) {
await fetch(lastMethod);
}
};

这段代码会在提交数据后,自动找到最后一个名为'todoList'的method实例并重新获取数据,从而更新列表。这种优雅的数据管理方式,让我的代码结构变得更加清晰了。


总结


useFetcher真的改变了我对数据管理的看法。它不仅可以帮我们实现数据预加载,还能轻松处理跨组件更新的问题。使用它,我们可以写出更加高效、更加优雅的代码。


你们平时是怎么处理这些数据管理的问题的呢?有没有遇到过什么困难?我很好奇大家的经验和想法,欢迎在评论区分享。如果这篇文章对你有帮助,别忘了点个赞哦!让我们一起探讨,一起进步!


作者:坡道口
来源:juejin.cn/post/7425225508613816320
收起阅读 »

解析vue中nextTick

web
在 Vue.js 中,nextTick 是一个非常重要的函数,它用于延迟回调函数的执行,直到下次 DOM 更新循环之后执行。这是因为 Vue 的数据绑定系统需要确保在数据变化后 DOM 被更新之前不执行某些依赖于 DOM 状态的操作。 实现原理 Vue 的 n...
继续阅读 »

在 Vue.js 中,nextTick 是一个非常重要的函数,它用于延迟回调函数的执行,直到下次 DOM 更新循环之后执行。这是因为 Vue 的数据绑定系统需要确保在数据变化后 DOM 被更新之前不执行某些依赖于 DOM 状态的操作。


实现原理


Vue 的 nextTick 主要是为了处理异步更新队列的问题。当数据发生变化时,并不是立刻更新 DOM,而是将这个更新放入一个队列中,等到当前执行栈清空时(也就是当前同步任务执行完毕),Vue 会进行一次更新,将所有需要更新的内容一次性渲染到 DOM 中,这样做是为了减少 DOM 操作次数,提高性能。


在 Vue 2 中的实现


在 Vue 2 中,nextTick 的实现在 src/core/util/next-tick.js 文件里。它利用了 setTimeout, setImmediate(如果支持的话),以及 MessageChannel 这几种不同的异步方法来实现。如果浏览器环境支持 Promise,那么它也会使用 Promise.then 来实现异步操作。


Javascript
深色版本
function nextTick (cb, ctx) {
var _resolve
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})

if (!pending) {
pending = true
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 使用 Promise 的话,创建一个 resolve 函数,并立即调用它。
var resolve = Promise.resolve
// 使用 then 回调来执行
resolve.call(Promise, function () { _resolve = $internal; })
} else {
// 如果没有 Promise 支持,就退而求其次使用 setImmediate 或 setTimeout
setTimeout(_flushCallbacks, 0)
}
}
}

这里的关键在于 Promise.resolve() 后面跟着的 .then() 会放到 JavaScript 引擎的微任务队列中,等待当前执行栈清空后执行。这样可以确保 nextTick 的回调是在所有同步任务完成后被执行。


在 Vue 3 中的实现


Vue 3 中使用了更现代的方法来实现 nextTick,主要是基于 queueMicrotask API。这个 API 可以让回调函数在当前任务完成后的下一个微任务时机执行。如果没有 queueMicrotask 支持,则退回到 Promise.then() 方法。


Javascript
深色版本
export function nextTick(cb?: Function, delay = 0): PromiseLike<any> | void {
if (cb) {
// 如果提供了回调函数,则注册回调
registerFlushSyncCallback(() => {
queueMicrotask(() => {
// 使用 queueMicrotask 来确保回调是在微任务时机执行
// 这样可以保证在任何其他异步任务之前执行
callWithAsyncErrorHandling(cb, activeInstance, SchedulerGuards.NEXT_TICK_HANDLER);
});
});
} else {
// 如果没有提供回调,则返回一个 promise
return new Promise((res) => {
queueMicrotask(res);
});
}
}

总结来说,nextTick 的主要作用是确保某些操作在 DOM 更新之后进行,从而避免了由于数据变化但 DOM 尚未更新导致的问题。通过使用异步机制(如微任务或宏任务),nextTick 能够确保在适当的时机执行回调。


作者:侯亮平
来源:juejin.cn/post/7426206782022074431
收起阅读 »

关于WebView的秒开实战

web
通过这篇文章你可以了解到: 性能优化的思维方式 WebView进阶知识 写在前面 最近组里做新的Web容器的,一次承载多个H5页面,以实现左右切换,默认展示主会场页,并要达到提升打开率的目标。要达到这个目标,那势必要从加载优化入手,缩短页面的打开时间。优化...
继续阅读 »

通过这篇文章你可以了解到



  • 性能优化的思维方式

  • WebView进阶知识


写在前面


最近组里做新的Web容器的,一次承载多个H5页面,以实现左右切换,默认展示主会场页,并要达到提升打开率的目标。要达到这个目标,那势必要从加载优化入手,缩短页面的打开时间。优化的点包括但不限于,Activity初始化、ViewPager和Fragment的初始化、WebView的初始化等等。


上一片文章给大家分享了我在ViewPager上面做的优化,本篇文章再接着给大家分享下WebView秒开的尝试。


优化效果



我们以提升打开率为目标,口径是资源位点击WebView的onPageFinished



使用新的容器之后,打开率提升了大约10%-20%65%—>85%),在低端机上的提升较为明显。为了让各位同学更加直观的感受到优化后的效果,这里用两张图简化的流程图来表示:


ago.png


以上是我们的容器简略的加载过程需经过6个步骤,加载时长从Activity的onCreate开始计算到WebView的onPageFinished大约需要3000ms(低端机)。很显然,在如今这个快节奏的社会,用户是不会等待这么长时间的。为此我们对它进行了一场手术,把它整成了下边的样子:


after.png


???what's the ****?玩俄罗斯方块吗?


同学憋急,你现在只需要关注的是:它由6个冗长的步骤,变成了两个步骤(Na组件放在了WebView初始化完成后加载),大大缩减了我们首页的加载时间。关于你的疑问,我会在下边的章节解释。


过程分析



上一节我们讲到,一次完整的打开过程需要经过6个步骤,经过了我们大刀阔斧的改造后,只需要两个步骤。这节接着给大家剖析我们这么做的底层逻辑。



Native优化


资源预加载


WebView组件加载过程有三处网络耗时分别是主文档HTML的加载、JS/CSS的加载和内容数据的加载,串行的流程是效率及其低下的。那么我们是不是改成并行的?当然不能!



  • 主文档HTML其实就是一个H5的框架,一个页面内所有的资源都是先通过主文档来触发加载,在主文档被加载之前我们是不能知道有哪些JS和CSS文件的。

  • 内容数据(包括图片)是由我们的业务方决定的,涵盖了各个营销场景,不像新闻浏览类的页面有固定的排版样式。由于页面不统一,单独对它进行下载再注入的改造成本有点大。(后续的离线化方案可实现


基于上述两点,可取的做法是:先把主文档数据预取后缓存,待WebView loadUrl之后,通过WebViewClient的监听去拦截主文档的请求


@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
for (RequestInterceptor interceptor : Interceptors) {
//拦截请求,去缓存中取出主文档数据
WebResourceResponse response = interceptor.intercept(view, request);
if (response != null) {
return response;
}
}
return super.shouldInterceptRequest(view, request);
}

预加载时机


预加载放在点击资源位(资源位在首页区域)之前是最理想的,这也是我们的初步设想。但是涉及到首页模块的改造,需要对应组件方的配合支持,会导致开发周期的延长。所以我们决定在第一版以HOOK Instumentation的方式,在点击资源位之后,Activity的onCreate之前去开启子线程对主文档进行预加载


wait4.png


同时跟首页组件方协调方案:在首页的T2阶段(不影响其它优先级更高的任务)对资源位进行预加载,点击后如果首页预加载成功则直接打开Activity,否则继续Instrumentation加载逻辑。


JS/CSS预加载


主文档加载完成了之后,可以对缓存的数据进行识别查找到需要加载的JS/CSS文件,紧接着开始进行JS/CSS的预加载。


下面时查找JS文件的伪代码:


private static final String JS_PATTERN = "<script\\s+[^>]*?src=[\"']([^\"']+)[\"'][^>]*>(?:<\\/script>)?";

/**
*@param htmlData 将主文档的二进制文件转换成的String类型
*@param JSPattern 用于从主文档内匹配JS文件的正则表达式
*/

private void recognitionJS(String htmlData, String JSPattern) {
try {
Pattern scriptPattern = Pattern.compile(JSPattern, Pattern.CASE_INSENSITIVE);
Matcher scriptMatcher = scriptPattern.matcher(htmlData);
while (scriptMatcher.find()) {
String link = scriptMatcher.group(1) + "";
if (TextUtils.isEmpty(link)) {
continue;
}

mResSet.add(link);
}
} catch (Exception e) {
}
}

这样一来我们的流程在第二版就变成了:


wait6.png


到这里我们在数据请求这一块所做的优化就结束了,那么我们的矛头接下来该指向哪里?


WebView预热


首次创建耗时较长


我从埋点的数据中发现,容器冷启打开的时间比热启要长的多,从Activity onCreate到WebView loadUrl之前的耗时比起热启大约慢了200多ms。这个过程中初始化的组件除了WebView还有有ViewPager和Fragment,通过再次细分阶段的埋点统计耗时发现,启动方式对这两者的初始化时间影响不大,WebView初始化时间自然就成了我们攻克的对象


我们找来了其它几个机型重复上述的步骤,高端机上表现并不明显,但也存在差异(大约80ms)。进一步确定了是WebView自身的原因,可以得出结论:WebView第一次初始化的时间会比后续创建的时间长,具体差异取决于机型性能


WebView Pool


利用前面得出的结论,可以在App启动时开始WebView的第一次初始化再销毁,以减少后续使用过程的创建时间。但还是避免不了往后创建带来的时间开销,这个时候池化技术就呼之欲出了。


我们可以将创建好的WebView放入容器中,可以一个也可以多个,取决于业务。由于创建WebView需要和Context绑定,而预创建WebView是无法提前获知所需要挂载的Activity的,为此我们找到了MutableContextWrappe。引用官方对它的介绍:



Special version of ContextWrapper that allows the base context to be modified after it is initially set. Change the base context for this ContextWrapper. All calls will then be delegated to the base context. Unlike ContextWrapper, the base context can be changed even after one is already set.



翻译成人话:它允许在运行时动态地更改 Context。这在某些特定场景下非常有用,例如,当您需要在不同的 Context 之间灵活切换或修改 Context 时。真是完美的解决了我们预创建绑定Context的问题!


//预创建WebView,存入缓存池
MutableContextWrapper contextWrapper = new MutableContextWrapper(getAppContext());
mWebViewPool.push(new WebView(contextWrapper));

//取出WebView,替换我们所需要的Context
WebView webView = mWebViewPool.pop();
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(activityContext);

看到这里,如果你是不是以为WebView的池化就这样结束了?


那是不可能滴


那是不可能滴


那是不可能滴


子进程承接


众所周知,一个亿级DAU的商业化App是非常庞杂的。在App启动时,有许多的任务需要初始化,势必会带来很大的性能开销。如果在这个阶段进行WebView的创建和池化的操作。前者可能会引出ANR,后者则是会面临内存溢出的风险。一波刚平,一波又起!


怎么办?再开个线程?WebView不能在子线程初始化,即使可以也解决不了内存开销的问题。PASS!


线程不行,进程呢?Bingo!


我们可以在App启动时开启一个子进程,在子进程进行WebView的初始化和池化的任务。系统会为子进程重新开辟内存空间,同时在子进程创建WebView也不会阻塞主进程的主线程,顺带也可以提高我们主进程的稳定性,可谓是一举多得。整个加载流程也就变成了三个大步骤。


wait67.png


组件懒加载


上一篇文章里有讲到我们容器的页面结构,没看过的请点击这里。在开始WebView加载之前会经过ViewPager和Fragment的初始化,经过线下实验统计,省去这两玩意儿大约可以提升67%(口径:Activity onCreate到WebView loadUrl,也就是说ViewPager和Fragment占这个过程的67%)。这不,优化点又来了。


打开容器的第一阶段,只需加载一个页面。因此我们可以将WebView直接放在Activity上显示,无需ViewPager和Fragment的介入,等到首页加载完成后再初始化这两组件,并开始缓存其它页面。


到这里我们的加载流程就变成了开头的样子了:


wait68.png


不要抬杠:你开头画的也不是这个样子的啊?


咱这不是为了更方便的理解,所以在开头小小的抽象了一下吗。手动狗头


其它优化


剩下还有一些前端的通用优化方式、网络通用优化方式在网上有同学总结的很清楚,在这里我就不一一列举。感兴趣的可以跳转对应文章进行查阅


今日头条品质优化 - 图文详情页秒开实践


作者:图灵1024
来源:juejin.cn/post/7364283070869028899
收起阅读 »