注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

大屏适配方案--scale

web
CSS3的scale等比例缩放 宽度比率 = 当前网页宽度 / 设计稿宽度 高度比率 = 当前网页高度 / 设计稿高度 设计稿: 1920 * 1080 适配屏幕:1920 * 1080 3840 * 2160(2 * 2) 7680 * 2160(4 * ...
继续阅读 »

CSS3的scale等比例缩放


宽度比率 = 当前网页宽度 / 设计稿宽度


高度比率 = 当前网页高度 / 设计稿高度


设计稿: 1920 * 1080


适配屏幕:1920 * 1080 3840 * 2160(2 * 2) 7680 * 2160(4 * 2)


方案一:根据宽度比率进行缩放(超宽屏比如9/16的屏幕会出现滚动条)


方案二:动态计算网页的宽高比,决定根据宽度比率还是高度比率进行缩放


首先基于1920 * 1080进行基础的布局,下面针对两种方案进行实现


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body,
ul {
margin: 0;
padding: 0;
}
body {
width: 1920px;
height: 1080px;
box-sizing: border-box;

/* 在js中添加translate居中 */
position: relative;
left: 50%;

/* 指定缩放的原点在左上角 */
transform-origin: left top;
}

ul {
width: 100%;
height: 100%;
list-style: none;

display: flex;
flex-direction: row;
flex-wrap: wrap;
}

li {
width: 33.333%;
height: 50%;
box-sizing: border-box;
border: 2px solid rgb(198, 9, 135);
font-size: 30px;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>

<script>
// ...实现适配方案
</script>
</body>
</html>

方案一:根据宽度比率进行缩放


// 设计稿尺寸以及宽高比
let targetWidth = 1920;

// html的宽 || body的宽
let currentWidth =
document.documentElement.clientWidth || document.body.clientWidth;

console.log(currentWidth);
// 按宽度计算缩放比率
let scaleRatio = currentWidth / targetWidth;

// 进行缩放
document.body.style = `transform: scale(${scaleRatio})`;

实现效果如下:


b.gif
这时我们发现在7680 * 2160尺寸下,屏幕根据宽度缩放会出现滚动条,为了解决这个问题,我们就要动态的选择根据宽度缩放还是根据高度缩放。


方案二:动态计算网页的宽高比,决定根据宽度比率还是高度比率进行缩放


// 设计稿尺寸以及宽高比
let targetWidth = 1920;
let targetHeight = 1080;
let targetRatio = 16 / 9; // targetWidth /targetHeight

// 当前屏幕html的宽 || body的宽
let currentWidth =
document.documentElement.clientWidth || document.body.clientWidth;
// 当前屏幕html的高 || body的高
let currentHeight =
document.documentElement.clientHeight || document.body.clientHeight;

// 当前屏幕宽高比
let currentRatio = currentWidth / currentHeight;

// 默认 按宽度计算缩放比率
let scaleRatio = currentWidth / targetWidth;
if (currentRatio > targetRatio) {
scaleRatio = currentHeight / targetHeight;
}

// 进行缩放
document.body.style = `transform: scale(${scaleRatio}) translateX(-50%);`;

效果如下:


b.gif
这样就可以解决在超宽屏幕下出现滚动条的问题,另外我们做了居中的样式处理,这样在超宽屏幕时,两边留白,内容居中展示显得更加合理些。


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

Java循环操作哪个快?

开发的时候我发现个问题,就是在学习玩streamAPI和lambda表达式后,我就变得越来越喜欢直接使用streamAPI,而不是使用for循环这种方式了,但是这种方式也有一定的缺点,但是直到某一次代码review,我的同事点醒了我,“小火汁,你的stream...
继续阅读 »

开发的时候我发现个问题,就是在学习玩streamAPI和lambda表达式后,我就变得越来越喜欢直接使用streamAPI,而不是使用for循环这种方式了,但是这种方式也有一定的缺点,但是直到某一次代码review,我的同事点醒了我,“小火汁,你的stream流写的是挺好,但是问题是为什么从同一个源取相似的对象,要分别写两次stream,你不觉得有点多余了吗?程序员不只是写代码,反而是最初的设计阶段就要把全局流程想好,要避免再犯这种错误哦~”,这句话点醒了我,所以我打算先看一下stream遍历、for循环、增强for循环、迭代器遍历、并行流parallel stream遍历的时间消耗,查看一下这几种方式的异同。

使用stream主要是在做什么?

此时我们先准备一个类

@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
class Item {
   private Integer name;

   private Integer value;
}
  1. list转成map
list.stream().collect(Collectors.toMap(Item::getName, Item::getValue, (newValue, oldValue) -> newValue))
  1. List过滤,返回新List
List collect = list.stream().filter(x -> x.getValue() > 50).collect(Collectors.toList());
  1. 模拟多次stream,因为我在开发中经常出现这种问题
Map collect = list.stream().collect(Collectors.toMap(Item::getName, Item::getValue, (newValue, oldValue) -> newValue));
Map collect3 = list.stream().collect(Collectors.toMap(Item::getName, Item::getValue, (newValue, oldValue) -> newValue));
  1. 取出list<类>中某一个属性的值,转成新的list
List collect = list.stream().map(Item::getValue).collect(Collectors.toList());
  1. list<类>中进行一组操作,并且转成新的list
        List collect1 = list.stream().parallel().map(x -> {
   Integer temp = x.getName();
   x.setName(x.getValue());
   x.setValue(temp);
   return x;
}).collect(Collectors.toList());

实际消耗

49c98bb4e7df8b966d8feb3818701e1b.png

选择1、10、100、100_00、100_000的原因

1、10、100主要是业务决定的,实际代码编写中这块的数据量是占大头的,10_000,100_000是因为为了查看实际的大数据量情况下的效果。

结果结论如下:

  1. 如果只是用filter的API,则建议只使用普通for循环,其他情况下数据量较少时,虽然stream和for循环都是10ms以内,但是性能上会差着3-4倍
  2. 普通for循环可以使用for (Item item : list),因为这个是for (int i = 0; i < ; i++)的语法糖
  3. 增强for循环底层是Iterator接口,但是实际的验证时发现特别慢,暂时没发现原因,但是不推荐使用
  4. stream串行流转成并行流操作后普遍还是不如串行流快,速度如下:执行时间:串行流转并行流>串行流>并行流,所以串行流转并行流不推荐使用
  5. 串行流转并行流和并行流都会使用ForkJoinsPool.commonPool(),这是个进程共用的CPU型线程池,且数据不方便修改,我记得是需要在启动的时候进行修改
  6. 串行流转并行流和并行流均会产生线程争抢资源与线程安全问题
  7. 在单次stream多次中继操作的情况下,执行速度和单次中继操作差不多

总结

  1. 写一次stream操作耗时较少,但是会导致开发人员无意之间多次使用stream流做类似操作(如从订单类中多次取不一致但是相似的一组对象),从而导致可读性变差,不利于后续拓展
  2. 尽量使用普通for循环做遍历,迭代器循环做删除或者使用collection的remove、removeIf等API实现(如果只需要删除的话)
  3. 使用普通for循环比stream流节省时间,因此在提高性能的角度看开发中尽量使用普通for循环。


    作者:艾迪的技术之路
    来源:juejin.cn/post/7427173759713951753
    收起阅读 »

    WebSocket太笨重?试试SSE的轻量级魅力!

    web
    一、 前言 Hello~ 大家好。我是秋天的一阵风~ 关注我时间长一点的同学们应该会了解,我最近算是跟旧项目 “较上劲” 了哈哈哈。 刚发布了一篇清除项目里的“僵尸”文件文章,这不,我又发现了旧项目上的一个问题。请听我慢慢说来~ 在2024年12月18日的午后...
    继续阅读 »

    一、 前言


    Hello~ 大家好。我是秋天的一阵风~


    关注我时间长一点的同学们应该会了解,我最近算是跟旧项目 “较上劲” 了哈哈哈。


    刚发布了一篇清除项目里的“僵尸”文件文章,这不,我又发现了旧项目上的一个问题。请听我慢慢说来~


    在2024年12月18日的午后,两点十八分,阳光透过窗帘的缝隙,洒在键盘上。我像往常一样,启动了那个熟悉的本地项目。浏览器的network面板静静地打开,准备迎接那个等待修复的bug。就在这时,一股尿意突然袭来,我起身,走向了厕所。


    当我回来,坐回那把椅子,眼前的一幕让我愣住了。network面板上,不知何时,跳出了一堆http请求,它们像是一场突如其来的雨,让人措手不及。我的头皮开始发麻,那种麻,是那种从心底里涌上来的,让人无法忽视的麻。这堆请求,它们似乎在诉说着什么,又或许,它们只是在提醒我,这个世界,有时候,比我们想象的要复杂得多。


    好了,矫情的话咱不说了,直接步入正题。😄😄😄


    image.png

    在查看代码以后发现这些频繁的请求是因为我们的项目首页有一个待办任务数量和消息提醒数量的展示,所以之前的同事使用了定时器,每隔十秒钟发送一次请求到后端接口拿数据,这也就是我们常说的轮询做法


    1. 轮询的缺点


    我们都知道轮询的缺点有几种:


    资源浪费



    • 网络带宽:频繁的请求可能导致不必要的网络流量,增加带宽消耗。

    • 服务器负载:每次请求都需要服务器处理,即使是空返回,也会增加服务器的CPU和内存负载。


    用户体验



    • 界面卡顿:频繁的请求和更新可能会造成用户界面的卡顿,影响用户体验。


    2. websocket的缺点


    那么有没有替代轮询的做法呢? 聪明的同学肯定会第一时间想到用websocket,但是在目前这个场景下我觉得使用websocket是显得有些笨重。我从以下这几方面对比:



    1. 客户端实现



      • WebSocket 客户端实现需要处理连接的建立、维护和关闭,以及可能的重连逻辑。

      • SSE 客户端实现相对简单,只需要处理接收数据和连接关闭。



    2. 适用场景



      • WebSocket 适用于需要双向通信的场景,如聊天应用、在线游戏等。

      • SSE 更适合单向数据推送的场景,如股票价格更新、新闻订阅等。



    3. 实现复杂性



      • WebSocket 是一种全双工通信协议,需要在客户端和服务器之间建立一个持久的连接,这涉及到更多的编程复杂性。

      • SSE 是单向通信协议,实现起来相对简单,只需要服务器向客户端推送数据。



    4. 浏览器支持



      • 尽管现代浏览器普遍支持 WebSocket,但 SSE 的支持更为广泛,包括一些较旧的浏览器版本。



    5. 服务器资源消耗



      • WebSocket 连接需要更多的服务器资源来维护,因为它们是全双工的,服务器需要监听来自客户端的消息。

      • SSE 连接通常是单向的,服务器只需要推送数据,减少了资源消耗。




    二、 详细对比


    对于这三者的详细区别,你可以参考下面我总结的表格:


    以下是 WebSocket、轮询和 SSE 的对比表格:


    特性WebSocket轮询PollingServer-Sent Events (SSE)
    定义全双工通信协议,支持服务器和客户端之间的双向通信。客户端定期向服务器发送请求以检查更新。服务器向客户端推送数据的单向通信协议。
    实时性高,服务器可以主动推送数据。低,依赖客户端定时请求。高,服务器可以主动推送数据。
    开销相对较高,需要建立和维护持久连接。较低,但频繁请求可能导致高网络和服务器开销。相对较低,只需要一个HTTP连接,服务器推送数据。
    浏览器支持现代浏览器支持,需要额外的库来支持旧浏览器。所有浏览器支持。现代浏览器支持良好,旧浏览器可能需要polyfill。
    实现复杂性高,需要处理连接的建立、维护和关闭。低,只需定期发送请求。中等,只需要处理服务器推送的数据。
    数据格式支持二进制和文本数据。通常为JSON或XML。仅支持文本数据,通常为JSON。
    控制流客户端和服务器都可以控制消息发送。客户端控制请求发送频率。服务器完全控制数据推送。
    安全性需要wss://(WebSocket Secure)来保证安全。需要https://来保证请求的安全。需要SSE通过HTTPS提供,以保证数据传输的安全。
    适用场景需要双向交互的应用,如聊天室、实时游戏。适用于更新频率不高的场景,如轮询邮箱。适用于服务器到客户端的单向数据流,如股票价格更新。
    跨域限制默认不支持跨域,需要服务器配置CORS。默认不支持跨域,需要服务器配置CORS。默认不支持跨域,需要服务器配置CORS。
    重连机制客户端可以实现自动重连逻辑。需要客户端实现重连逻辑。客户端可以监听连接关闭并尝试重连。
    服务器资源较高,因为需要维护持久连接。较低,但频繁的请求可能增加服务器负担。较低,只需要维护一个HTTP连接。

    这个表格概括了 WebSocket、轮询和 SSE 在不同特性上的主要对比点。每种技术都有其适用的场景和限制,选择合适的技术需要根据具体的应用需求来决定。


    三、 SSE(Server-Sent Events)介绍


    我们先来简单了解一下什么是Server-Sent Events


    Server-Sent Events (SSE) 是一种允许服务器主动向客户端浏览器推送数据的技术。它基于 HTTP 协议,但与传统的 HTTP 请求-响应模式不同,SSE 允许服务器在建立连接后,通过一个持久的连接不断地向客户端发送消息。


    工作原理



    1. 建立连接



      • 客户端通过一个普通的 HTTP 请求订阅一个 SSE 端点。

      • 服务器响应这个请求,并保持连接打开,而不是像传统的 HTTP 响应那样关闭连接。



    2. 服务器推送消息



      • 一旦服务器端有新数据可以发送,它就会通过这个持久的连接向客户端发送一个事件。

      • 每个事件通常包含一个简单的文本数据流,遵循特定的格式。



    3. 客户端接收消息



      • 客户端监听服务器发送的事件,并在收到新数据时触发相应的处理程序。



    4. 连接管理



      • 如果连接由于任何原因中断,客户端可以自动尝试重新连接。




    著名的计算机科学家林纳斯·托瓦兹(Linus Torvalds) 曾经说过:talk is cheap ,show me your code


    我们直接上代码看看效果:


    java代码



    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;


    @RestController
    @RequestMapping("platform/todo")
    public class TodoSseController {

    private final ExecutorService executor = Executors.newCachedThreadPool();

    @GetMapping("/endpoint")
    public SseEmitter refresh(HttpServletRequest request) {
    final SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
    executor.execute(() -> {
    try {
    while (true) { // 无限循环发送事件,直到连接关闭
    // 发送待办数量更新
    emitter.send(SseEmitter.event().data(5));
    // 等待5秒
    TimeUnit.SECONDS.sleep(5);
    }
    } catch (IOException e) {
    emitter.completeWithError(e);
    } catch (InterruptedException e) {
    // 当前线程被中断,结束连接
    Thread.currentThread().interrupt();
    emitter.complete();
    }
    });
    return emitter;
    }
    }

    前端代码


     beforeCreate() {
    const eventSource = new EventSource('/platform/todo/endpoint');
    eventSource.onmessage = (event) => {
    console.log("evebt:",event)
    };
    eventSource.onerror = (error) => {
    console.error('SSE error:', error);
    eventSource.close();
    };

    this.$once('hook:beforeDestroy', () => {
    if (eventSource) {
    eventSource.close();
    }
    });

    },

    改造后的效果


    image.png
    image.png

    可以看到,客户端只发送了一次http请求,后续所有的返回结果都可以在event.data里面获取,先不谈性能,对于有强迫症的同学是不是一个很大改善呢?


    总结


    虽然 SSE(Server-Sent Events)因其简单性和实时性在某些场景下提供了显著的优势,比如在需要服务器向客户端单向推送数据时,它能够以较低的开销维持一个轻量级的连接,但 SSE 也存在一些局限性。例如,它不支持二进制数据传输,这对于需要传输图像、视频或复杂数据结构的应用来说可能是一个限制。此外,SSE 只支持文本格式的数据流,这可能限制了其在某些数据传输场景下的应用。还有,SSE 的兼容性虽然在现代浏览器中较好,但在一些旧版浏览器中可能需要额外的 polyfill 或者降级方案。


    考虑到这些优缺点,我们在选择数据通信策略时,应该基于项目的具体需求和上下文来做出决策。如果项目需要双向通信或者传输二进制数据,WebSocket 可能是更合适的选择。


    如果项目的数据更新频率不高,或者只需要客户端偶尔查询服务器状态,传统的轮询可能就足够了。


    而对于需要服务器频繁更新客户端数据的场景,SSE 提供了一种高效的解决方案。


    总之,选择最合适的技术堆栈需要综合考虑项目的需求、资源限制、用户体验和未来的可维护性。


    作者:秋天的一阵风
    来源:juejin.cn/post/7451991754561880115
    收起阅读 »

    为什么现在连Date类都不建议使用了?

    本文已经授权【稀土掘金技术社区】官方公众号独家原创发布。 一、有什么问题吗java.util.Date? java.util.Date(Date从现在开始)是一个糟糕的类型,这解释了为什么它的大部分内容在 Java 1.1 中被弃用(但不幸的是仍在使用)。 ...
    继续阅读 »

    本文已经授权【稀土掘金技术社区】官方公众号独家原创发布。


    一、有什么问题吗java.util.Date


    image.png


    java.util.DateDate从现在开始)是一个糟糕的类型,这解释了为什么它的大部分内容在 Java 1.1 中被弃用(但不幸的是仍在使用)。


    设计缺陷包括:



    • 它的名称具有误导性:它并不代表 a Date,而是代表时间的一个瞬间。所以它应该被称为Instant——正如它的java.time等价物一样。

    • 它是非最终的:这鼓励了对继承的不良使用,例如java.sql.Date(这意味着代表一个日期,并且由于具有相同的短名称而也令人困惑)

    • 它是可变的:日期/时间类型是自然值,可以通过不可变类型有效地建模。可变的事实Date(例如通过setTime方法)意味着勤奋的开发人员最终会在各处创建防御性副本。

    • 它在许多地方(包括)隐式使用系统本地时区,toString()这让许多开发人员感到困惑。有关此内容的更多信息,请参阅“什么是即时”部分

    • 它的月份编号是从 0 开始的,是从 C 语言复制的。这导致了很多很多相差一的错误。

    • 它的年份编号是基于 1900 年的,也是从 C 语言复制的。当然,当 Java 出现时,我们已经意识到这不利于可读性?

    • 它的方法命名不明确:getDate()返回月份中的某一天,并getDay()返回星期几。给这些更具描述性的名字有多难?

    • 对于是否支持闰秒含糊其辞:“秒由 0 到 61 之间的整数表示;值 60 和 61 仅在闰秒时出现,即使如此,也仅在实际正确跟踪闰秒的 Java 实现中出现。” 我强烈怀疑大多数开发人员(包括我自己)都做了很多假设,认为 for 的范围getSeconds()实际上在 0-59 范围内(含)。

    • 它的宽容没有明显的理由:“在所有情况下,为这些目的而对方法给出的论据不必落在指定的范围内; 例如,日期可以指定为 1 月 32 日,并被解释为 2 月 1 日。” 多久有用一次?


    原文如下:为什么要避免使用Date类?


    二、为啥要改?


    我们要改的原因很简单,我们的代码缺陷扫描规则认为这是一个必须修改的缺陷,否则不给发布,不改不行,服了。


    image.png



    解决思路:避免使用java.util.Datejava.sql.Date类和其提供的API,考虑使用java.time.Instant类或java.time.LocalDateTime类及其提供的API替代。



    三、怎么改?


    只能说这种基础的类改起来牵一发动全身,需要从DO实体类看起,然后就是各种Converter,最后是DTO。由于我们还是微服务架构,业务服务依赖于基础服务的API,所以必须要一起改否则就会报错。这里就不细说修改流程了,主要说一下我们在改造的时候遇到的一些问题。


    1. 耐心比对数据库日期字段和DO的映射


    (1)确定字段类型


    首先你需要确定数据对象中的 Date 字段代表的是日期、时间还是时间戳。



    • 如果字段代表日期和时间,则可能需要使用 LocalDateTime

    • 如果字段仅代表日期,则可能需要使用 LocalDate

    • 如果字段仅代表时间,则可能需要使用 LocalTime

    • 如果字段需要保存时间戳(带时区的),则可能需要使用 Instant 或 ZonedDateTime


    (2)更新数据对象类


    更新数据对象类中的字段,把 Date 类型改为适当的 java.time 类型。


    2. 将DateUtil中的方法改造


    (1)替换原来的new Date()和Calendar.getInstance().getTime()


    原来的方式:


    Date nowDate = new Date();
    Date nowCalendarDate = Calendar.getInstance().getTime();

    使用 java.time 改造后:


    // 使用Instant代表一个时间点,这与Date类似
    Instant nowInstant = Instant.now();

    // 如果需要用到具体的日期和时间(例如年、月、日、时、分、秒)
    LocalDateTime nowLocalDateTime = LocalDateTime.now();

    // 如果你需要和特定的时区交互,可以使用ZonedDateTime
    ZonedDateTime nowZonedDateTime = ZonedDateTime.now();

    // 如果你需要转换回java.util.Date,你可以这样做(假设你的代码其他部分还需要使用Date)
    Date nowFromDateInstant = Date.from(nowInstant);

    // 如果需要与java.sql.Timestamp交互
    java.sql.Timestamp nowFromInstant = java.sql.Timestamp.from(nowInstant);

    一些注意点:



    1. Instant 表示的是一个时间点,它是时区无关的,相当于旧的 Date 类。它通常用于表示时间戳。

    2. LocalDateTime 表示没有时区信息的日期和时间,它不能直接转换为时间戳,除非你将其与时区结合使用(例如通过 ZonedDateTime)。

    3. ZonedDateTime 包含时区信息的日期和时间,它更类似于 Calendar,因为 Calendar 也包含时区信息。

    4. 当你需要将 java.time 对象转换回 java.util.Date 对象时,可以使用 Date.from(Instant) 方法。这在你的代码需要与旧的API或库交互时非常有用。


    (2)一些基础的方法改造


    a. dateFormat


    原来的方式


    public static String dateFormat(Date date, String dateFormat) {
    SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
    return formatter.format(date);
    }

    使用java.time改造后


    public static String dateFormat(LocalDateTime date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    return date.format(formatter);
    }

    b. addSecond、addMinute、addHour、addDay、addMonth、addYear


    原来的方式


    public static Date addSecond(Date date, int second) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(13, second);
    return calendar.getTime();
    }

    public static Date addMinute(Date date, int minute) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(12, minute);
    return calendar.getTime();
    }

    public static Date addHour(Date date, int hour) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(10, hour);
    return calendar.getTime();
    }

    public static Date addDay(Date date, int day) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(5, day);
    return calendar.getTime();
    }

    public static Date addMonth(Date date, int month) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(2, month);
    return calendar.getTime();
    }

    public static Date addYear(Date date, int year) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.add(1, year);
    return calendar.getTime();
    }

    使用java.time改造后


    public static LocalDateTime addSecond(LocalDateTime date, int second) {
    return date.plusSeconds(second);
    }

    public static LocalDateTime addMinute(LocalDateTime date, int minute) {
    return date.plusMinutes(minute);
    }

    public static LocalDateTime addHour(LocalDateTime date, int hour) {
    return date.plusHours(hour);
    }

    public static LocalDateTime addDay(LocalDateTime date, int day) {
    return date.plusDays(day);
    }

    public static LocalDateTime addMonth(LocalDateTime date, int month) {
    return date.plusMonths(month);
    }

    public static LocalDateTime addYear(LocalDateTime date, int year) {
    return date.plusYears(year);
    }

    c. dateToWeek


    原来的方式


    public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
    public static String dateToWeek(Date date) {
    Calendar cal = Calendar.getInstance();
    cal.setTime(date);
    return WEEK_DAY_OF_CHINESE[cal.get(7) - 1];
    }

    使用java.time改造后


    public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};

    public static String dateToWeek(LocalDate date) {
    DayOfWeek dayOfWeek = date.getDayOfWeek();
    return WEEK_DAY_OF_CHINESE[dayOfWeek.getValue() % 7];
    }

    d. getStartOfDay和getEndOfDay


    原来的方式


    public static Date getStartTimeOfDay(Date date) {
    if (date == null) {
    return null;
    } else {
    LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
    LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
    return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }
    }

    public static Date getEndTimeOfDay(Date date) {
    if (date == null) {
    return null;
    } else {
    LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
    LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
    return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
    }
    }

    使用java.time改造后


    public static LocalDateTime getStartTimeOfDay(LocalDateTime date) {
    if (date == null) {
    return null;
    } else {
    // 获取一天的开始时间,即00:00
    return date.toLocalDate().atStartOfDay();
    }
    }

    public static LocalDateTime getEndTimeOfDay(LocalDateTime date) {
    if (date == null) {
    return null;
    } else {
    // 获取一天的结束时间,即23:59:59.999999999
    return date.toLocalDate().atTime(LocalTime.MAX);
    }
    }

    e. betweenStartAndEnd


    原来的方式


    public static Boolean betweenStartAndEnd(Date nowTime, Date beginTime, Date endTime) {
    Calendar date = Calendar.getInstance();
    date.setTime(nowTime);
    Calendar begin = Calendar.getInstance();
    begin.setTime(beginTime);
    Calendar end = Calendar.getInstance();
    end.setTime(endTime);
    return date.after(begin) && date.before(end);
    }

    使用java.time改造后


    public static Boolean betweenStartAndEnd(Instant nowTime, Instant beginTime, Instant endTime) {
    return nowTime.isAfter(beginTime) && nowTime.isBefore(endTime);
    }


    我这里就只列了一些,如果有缺失的可以自己补充,不会写的话直接问问ChatGPT,它最会干这事了。最后把这些修改后的方法替换一下就行了。



    四、小结一下


    这个改造难度不高,但是复杂度非常高,一个地方没改好,轻则接口报错,重则启动失败,非常耗费精力,真不想改。



    文末小彩蛋,自建摸鱼网站,各大网站热搜一览,上班和摸鱼很配哦!



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

    Android 新一代图片加载库 - Coil

    Coil 是 Android 的新一代图片加载库,它的全名叫做 Coroutine Image Loader,即协程图片加载器,用于显示网络或本地图像资源。 特点 快速:执行了多项优化,包括内存和磁盘缓存,图像降采样,自动暂停/取消请求等。 轻量级:依赖于 ...
    继续阅读 »

    Coil 是 Android 的新一代图片加载库,它的全名叫做 Coroutine Image Loader,即协程图片加载器,用于显示网络或本地图像资源。


    特点



    • 快速:执行了多项优化,包括内存和磁盘缓存,图像降采样,自动暂停/取消请求等。

    • 轻量级:依赖于 Kotlin,协程和 Okio,并与谷歌的 R8 等代码缩减器无缝协作。

    • 易于使用:API 利用 Kotlin 的语言特性来实现简洁性和最小化的样板代码。

    • 现代化:以 Kotlin 为首要语言,并与协程,Okio,Ktor 和 OkHttp 等现代库实现互操作。


    加载图片


    先引入依赖


    implementation(libs.coil)

    最简单的加载方法就是使用这个扩展函数了


    inline fun ImageView.load(
    data: Any?,
    imageLoader: ImageLoader = context.imageLoader,
    builder: ImageRequest.Builder.() -> Unit = {}
    )
    : Disposable {
    val request = ImageRequest.Builder(context)
    .data(data)
    .target(this)
    .apply(builder)
    .build()
    return imageLoader.enqueue(request)
    }

    使用扩展函数来加载本地或网络中的图片


    // 加载网络图片
    binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")
    // 加载资源图片
    binding.imageView.load(R.drawable.girl)
    // 加载文件中的图片
    val file = File(requireContext().getExternalFilesDir(null), "saved_image.jpg")
    binding.imageView.load(file.absolutePath)

    支持设置占位图,裁剪变换,生命周期关联等


    binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg") {
    crossfade(true) //渐进渐出
    crossfade(1000) //渐进渐出时间
    placeholder(R.mipmap.sym_def_app_icon) //加载占位图
    error(R.mipmap.sym_def_app_icon) //加载失败占位图
    allowHardware(true) //硬件加速
    allowRgb565(true) //支持565格式
    lifecycle(lifecycle) //生命周期关联
    transformations(CircleCropTransformation()) //圆形裁剪变换
    }

    变为圆角矩形


    binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg") {
    lifecycle(lifecycle)
    transformations(RoundedCornersTransformation(20f))
    }

    可以创建自定义的图片加载器,为其添加一些日志拦截器等。


    class LoggingInterceptor : Interceptor {

    companion object {
    private const val TAG = "LoggingInterceptor"
    }

    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
    val url = chain.request.data.toString()
    val width = chain.size.width.toString()
    val height = chain.size.height.toString()
    Log.i(TAG, "url: $url, width: $width, height: $height")
    return chain.proceed(chain.request)
    }
    }

    class MyApplication : Application(), ImageLoaderFactory {

    override fun newImageLoader() =
    ImageLoader.Builder(this.applicationContext).components { add(LoggingInterceptor()) }
    .build()
    }

    替换 Okhttp 实例


    val okHttpClient = OkHttpClient.Builder()
    .retryOnConnectionFailure(true)
    .connectTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .build()
    val imageLoader = ImageLoader.Builder(requireContext()).okHttpClient {
    okHttpClient
    }.build()
    Coil.setImageLoader(imageLoader)
    binding.imageView.load("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")

    加载 gif


    添加依赖


    implementation(libs.coil.gif)

    按照官方的做法,设置 ImageLoader。


    val imageLoader = ImageLoader.Builder(requireContext())
    .components {
    if (SDK_INT >= 28) {
    add(ImageDecoderDecoder.Factory())
    } else {
    add(GifDecoder.Factory())
    }
    }.build()
    Coil.setImageLoader(imageLoader)
    binding.imageView.load(GIF_URL)

    下载监听


    可以监听下载过程


    binding.imageView.load(IMAGE_URL) {
    listener(
    onStart = {
    Log.i(TAG, "onStart")
    },
    onError = { request, throwable ->
    Log.i(TAG, "onError")
    },
    onSuccess = { request, result ->
    Log.i(TAG, "onSuccess")
    },
    onCancel = { request ->
    Log.i(TAG, "onCancel")
    }
    )
    }

    取消下载


    val disposable = binding.imageView.load(IMAGE_URL)
    disposable.dispose()

    对 Jetpack Compose 的支持


    引入依赖:


    implementation(libs.coil.compose)

    使用 AsyncImage


    @Composable
    @NonRestartableComposable
    fun AsyncImage(
    model: Any?,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    transform: (State) -> State = DefaultTransform,
    onState: ((State) -> Unit)? = null,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    filterQuality: FilterQuality = DefaultFilterQuality,
    clipToBounds: Boolean = true,
    modelEqualityDelegate: EqualityDelegate = DefaultModelEqualityDelegate,
    )


    比如显示一张网络图片,就可以这样干。


    @Composable
    fun DisplayPicture() {
    AsyncImage(
    model = "https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg",
    contentDescription = null
    )
    }

    支持设置占位图,过程监听,裁剪等


    @Composable
    fun DisplayPicture() {
    AsyncImage(
    modifier = Modifier
    .clip(CircleShape)
    .size(200.dp),
    onSuccess = {
    Log.i(TAG, "onSuccess")
    },
    onError = {
    Log.i(TAG, "onError")
    },
    onLoading = {
    Log.i(TAG, "onLoading")
    },
    model = ImageRequest.Builder(LocalContext.current)
    .data("https://img2.huashi6.com/images/resource/2020/07/12/h82924904p0.jpg")
    .crossfade(true)
    .placeholder(R.drawable.default_image)
    .error(R.drawable.default_image)
    .build(),
    contentScale = ContentScale.Crop,
    contentDescription = null
    )
    }

    这里介绍一下这个 ContentScale,它是用来指定图片如何适应其容器大小的,有以下几个值:



    • ContentScale.FillBounds:图片会被拉伸或压缩以完全填充其容器的宽度和高度,这可能会导致图片的宽高比失真。

    • ContentScale.Fit:图片会保持其原始宽高比,并尽可能大地缩放以适应容器,同时确保图片的任一边都不会超出容器的边界,这可能会导致容器的某些部分未被图片覆盖。

    • ContentScale.Crop:图片会被裁剪以完全覆盖其容器的宽度和高度,同时保持图片的宽高比,这通常用于需要确保整个容器都被图片覆盖的场景,但可能会丢失图片的一部分内容。

    • ContentScale.FillWidth:图片会保持其原始宽高比,并调整其高度以完全填充容器的宽度,这可能会导致图片的高度超出容器的高度,从而被裁剪或需要额外的布局处理。

    • ContentScale.FillHeight:图片会保持其原始宽高比,并调整其宽度以完全填充容器的高度,这可能会导致图片的宽度超出容器的宽度,从而需要相应的处理。

    • ContentScale.Inside:图片会保持其原始宽高比,并缩放以确保完全位于容器内部,同时其任一边都不会超出容器的边界。

    • ContentScale.:图片将以其原始尺寸显示,不会进行任何缩放或裁剪。


    作者:阿健君
    来源:juejin.cn/post/7403546034763235378
    收起阅读 »

    延迟双删如此好用,为何大厂从来不用

    摘要: 在绝大多数介绍缓存与数据库一致性方案的文章中,随着 Cache-aside 模式的数据变更几乎无例外的推荐使用删除缓存的策略,为进一步降低数据不一致的风险通常会配合延迟双删的策略。但是令人意外的是,在一些互联网大厂中的核心业务却很少使用这种方式。这背后...
    继续阅读 »

    摘要: 在绝大多数介绍缓存与数据库一致性方案的文章中,随着 Cache-aside 模式的数据变更几乎无例外的推荐使用删除缓存的策略,为进一步降低数据不一致的风险通常会配合延迟双删的策略。但是令人意外的是,在一些互联网大厂中的核心业务却很少使用这种方式。这背后的原因是什么呢?延迟双删策略有什么致命缺陷么?以及这些大厂如何选择缓存与数据库一致性保障的策略呢?如果你对此同样抱有有疑问的话,希望本文能为你答疑解惑。




    当数据库(主副本)数据记录变更时,为了降低缓存数据不一致状态的持续时间,通常会选择主动 失效 / 更新 缓存数据的方式。绝大多数应用系统的设计方案中会选择通过删除缓存数据的方式使其失效。但同样会出现数据不一致的情况,具体情况参见下图:
    Cache-Aside Pattern - remove data approach - no consistency.gif
    所以延迟双删又成为了组合出现的常见模式。延迟双删最复杂的技术实现在于对延迟时间的确定上,间隔时间久的话数据不一致的状态持续时间会变长,如果间隔时间过短可能无法起到一致性保障的作用。所以基于经验会将这个时间设定在秒级,如 1-2 秒后执行第二次删除操作。


    延迟双删的致命缺陷


    但是延迟时间最大的问题不在于此,而是两次删除缓存数据引起的缓存穿透,短时间对数据库(主副本)造成的流量与负载压力。绝大多数应用系统本身流量与负载并不高,使用缓存通常是为了提升系统性能表现,数据库(主副本)完全可以承载一段时间内的负载压力。对于此类系统延迟双删是一个完全可以接受的高性价比策略。


    现实世界中的系统响应慢所带来的却是流量的加倍上涨。回想一下当你面对 App 响应慢的情况,是如何反应与对待便能明白,几乎所有用户的下意识行为都是如出一辙。


    所以对于那些流量巨大的应用系统而言,短时的访问流量穿透缓存访问数据库(主副本),恐怕很难接受。为了应对这种流量穿透的情况,通常需要增加数据库(主副本)的部署规格或节点。而且这类应用系统的响应变慢的时候,会对其支持系统产生影响,如果其支持系统较多的情况下,会存在影响的增溢。相比延迟双删在技术实现上带来高效便捷而言,其对系统的影响与副作用则变得不可忽视。


    Facebook(今 Meta)解决方案


    早在 2013 年由 Facebook(今 Meta)发表的论文 “Scaling Memcache at Facebook” 中便提供了其内部的解决方案,通过提供一种类似 “锁” 的 “leases”(本文译为“租约”)机制防止并发带来的数据不一致现象。


    租约机制实现方法大致如下:



    当有多个请求抵达缓存时,缓存中并不存在该值时会返回给客户端一个 64 位的 token ,这个 token 会记录该请求,同时该 token 会和缓存键作为绑定,该 token 即为上文中租约的值,客户端在更新时需要传递这个 token ,缓存验证通过后会进行数据的存储。其他请求需要等待这个租约过期后才可申请新的租约。



    可结合下图辅助理解其作用机制。也可阅读缓存与主副本数据一致性系统设计方案(下篇)一文中的如何解决并发数据不一致,又能避免延迟双删带来的惊群问题章节进一步了解。
    Cache-Aside Pattern - leases.gif


    简易参考实现


    接下来我们以 Redis 为例,提供一个 Java 版本的简易参考实现。本文中会给出实现所涉及的关键要素与核心代码,你可以访问 Github 项目 来了解整个样例工程,并通过查阅 Issue 与 commits 来了解整个样例工程的演化进程。


    要想实现上述租约机制,需要关注的核心要素有三个:



    1. 需要复写 Redis 数据获取操作,当 Redis 中数据不存在时增加对租约的设置;

    2. 需要复写 Redis 数据设置操作,当设置 Redis 中数据时校验租约的有效性;

    3. 最后是当数据库(主副本)数据变更时,删除 Redis 数据同时要连带删除租约信息。


    同时为了保障 Redis 操作的原子性,我们需要借助 Lua 脚本来实现上述三点。这里以字符串类型为例,对应脚本分别如下:


    Redis 数据获取操作


    返回值的第二个属性作为判断是否需要执行数据获取的判断依据。当为 false 时表示 Redis 中无对应数据,需要从数据库中加载,同时保存了当前请求与 key 对应的租约信息。


    local key = KEYS[1]
    local token = ARGV[1]
    local value = redis.call('get', key)
    if not value then
    redis.replicate_commands()
    local lease_key = 'lease:'..key
    redis.call('set', lease_key, token)
    return {false, false}
    else
    return {value, true}
    end

    Redis 数据设置操作


    返回值的第二个属性作为判断是否成功执行数据设置操作的依据。该属性为 false 表示租约校验失败,未成功执行数据设置操作。同时意味着有其他进程/线程 执行数据查询操作并对该 key 设置了新的租约。


    local key = KEYS[1]
    local token = ARGV[1]
    local value = ARGV[2]
    local lease_key = 'lease:'..key
    local lease_value = redis.call('get', lease_key)
    if lease_value == token then
    redis.replicate_commands()
    redis.call('set', key, value)
    return {value, true}
    else
    return {false, false}
    end

    Redis 数据删除操作


    当数据库变更进程/线程 完成数据变更操作后,尝试删除缓存需要同时清理对应数据记录的 key 以及其关联租约 key。防止数据变更前的查询操作通过租约校验,将旧数据写入 Redis 。


    local key = KEYS[1]
    local token = ARGV[1]
    local lease_key = 'lease:'..key
    redis.call('del', key, leask_key)

    该方案主要的影响在应用层实现,主要在集中在三个方面:



    1. 应用层不能调用 Redis 数据类型的原始操作命令,而是改为调用 EVAL 命令;

    2. 调用 Redis 返回结果数据结构的变更为数组,需要解析数组;

    3. 应用层对于 Redis 的操作变复杂,需要生成租约用的 token,并根据每个阶段返回结果进行后续处理;


    为应对上述三点变化,对应操作 Redis 的 Java 实现如下:


    封装返回结果


    为便于后续操作,首先是对脚本返回结果的封装。


    public class EvalResult {  

    String value;
    boolean effect;

    public EvalResult(List<?> args) {
    value = (String) args.get(0);
    if (args.get(1) == null) {
    effect = false;
    } else {
    effect = 1 == (long) args.get(1);
    }
    }
    }

    组件设计


    Components UML Design.png


    封装 Redis 操作


    因为在样例工程中独立出了一个 Query Engine 组件,所以需要跨组件传递 token,这里为了实现简单采用了 ThreadLocal 进行 token 的传递,具体系统可查阅样例工程中的用例


    public class LeaseWrapper extends Jedis implements CacheCommands {  

    private final Jedis jedis;
    private final TokenGenerator tokenGenerator;
    private final ThreadLocal<String> tokenHolder;

    public LeaseWrapper(Jedis jedis) {
    this.jedis = jedis;
    this.tokenHolder = new ThreadLocal<>();
    this.tokenGenerator = () -> UUID.randomUUID().toString();
    }

    @Override
    public String get(String key) {
    String token = this.tokenGenerator.get();
    tokenHolder.set(token);
    Object result = this.jedis.eval(LuaScripts.leaseGet(), List.of(key), List.of(token));
    EvalResult er = new EvalResult((List<?>) result);
    if (er.effect()) {
    return er.value();
    }
    return null;
    }

    @Override
    public String set(String key, String value) {
    String token = tokenHolder.get();
    tokenHolder.remove();
    Object result = this.jedis.eval(LuaScripts.leaseSet(), List.of(key), List.of(token, value));
    EvalResult er = new EvalResult((List<?>) result);
    if (er.effect()) {
    return er.value();
    }
    return null;
    }

    }

    补充


    在上面的简易参考实现中,我们并没有实现其他请求需要等待这个租约过期后才可申请新的租约。该功能主要是防止惊群问题,进一步降低可能对数据库造成的访问压力。要实现该功能需要在 Redis 数据获取操作中改进脚本:


    local key = KEYS[1]
    local token = ARGV[1]
    local value = redis.call('get', key)
    if not value then
    redis.replicate_commands()
    local lease_key = 'lease:'..key
    local current_token = redis.call('get', lease_key)
    if not current_token or token == current_token then
    redis.call('set', lease_key, token)
    return {token, false}
    else
    return {current_token, false}
    end
    else
    return {value, true}
    end

    同时也可以为租约数据设定一个短时 TTL,并在应用层通过对 EvalResult 的 effect 判断为 false 的情况下等待一段时间后再次执行。


    上述实现的复杂点在于租约过期的时间的选取,以及超过设定时间的逻辑处理。我们可以实现类似自旋锁的机制,在最大等待时间内随时等待一个间隙向 Redis 发起查询请求,超过最大等待时间后直接查询数据库(主副本)获取数据。


    Uber 解决方案


    在 Uber 今年 2 月份发表的一篇技术博客 “How Uber Serves Over 40 Million Reads Per Second from Online Storage Using an Integrated Cache” 中透露了其内部的解决方案,通过比对版本号的方式避免将旧数据写入缓存。


    版本号比对机制实现方法大致如下:



    将数据库中行记录的时间戳作为版本号,通过 Lua 脚本通过 Redis EVAL 命令提供类似 MSET 的更新操作,基于自定义编解码器提取 Redis 记录中的版本号,在执行数据设置操作时进行比对,只写入较新的数据。



    其中 Redis 的数据记录对应的 Key-Value 编码格式如所示:
    Uber Redis CacheFront Codec.jpeg


    简易参考实现


    接下来我们以 Redis 为例,提供一个 Java 版本的简易参考实现。本文中会给出实现所涉及的关键要素与核心代码,你可以访问 Github 项目 来了解整个样例工程,并通过查阅 Issue 与 commits 来了解整个样例工程的演化进程。


    我们这里不采取定制数据格式,而是通过额外的缓存 Key 存储数据版本,要想实现类似版本号比对机制,需要关注的核心要素有两个:



    1. 需要复写 Redis 数据设置操作,当设置 Redis 中数据时校验版本号;

    2. 在版本号比对通过后需要绑定版本号数据,与主数据同步写入 Redis 中。


    同时为了保障 Redis 操作的原子性,我们需要借助 Lua 脚本来实现上述两点。这里以字符串类型为例,对应脚本分别如下:


    Redis 数据设置操作


    返回值的第二个属性作为判断是否成功执行数据设置操作的依据。该属性为 false 表示数据未成功写入 Redis。同时意味当前 进程/线程 执行写入的数据为历史数据,在次过程中数据已经发生变更并又其他数据写入。


    local key = KEYS[1]  
    local value = ARGV[1]
    local current_version = ARGV[2]
    local version_key = 'version:'..key
    local version_value = redis.call('get', version_key)
    if version_value == false or version_value < current_version then
    redis.call('mset', version_key, current_version, key, value)
    return {value, true}
    else
    return {false, false}
    end

    该方案主要的影响在应用层实现,需要在调用 Redis 的 EVAL 命令前从数据实体中提取时间戳作为版本号,同时需要保障数据实体中包含时间戳相关属性。


    封装 Redis 操作


    结合我们的样例工程代码,我们通过实现 VersionWrapper 对 Redis 的操作进行如下封装。


    public class VersionWrapper extends Jedis implements CacheCommands {  

    private final Jedis jedis;

    public VersionWrapper(Jedis jedis) {
    this.jedis = jedis;
    }

    @Override
    public String set(String key, String value, String version) {
    Object result = this.jedis.eval(LuaScripts.versionSet(), List.of(key), List.of(value, version));
    EvalResult er = new EvalResult((List<?>) result);
    if (er.effect()) {
    return er.value();
    }
    return null;
    }
    }

    补充


    透过该方案我们推测 Uber 采取的并非数据变更后删除缓存的策略,很可能是更新缓存的策略(在 Uber 的技术博客中也间接的提到了更新缓存的策略)。


    因为整个版本号比对的方式与删除缓存的逻辑相悖。我们抛开 Uber CacheFront 的整体架构,仅仅将该方案应用在简单架构模型中。采取删除缓存的策略,可能会产生如下图所示的结果,此时应用服务 Server - 2 因为查询缓存未获取到值,而从数据库加载并写入缓存,但是此时缓存中写入的为历史旧值,而在该数据过期前或者下次数据变更前,都不会再触发更新了。
    Cache-Aside Pattern - remove data approach - always wrong.gif
    当然对于更新缓存的策略同样面临这个问题,因为当数据变更发生期间,缓存中并没有该数据记录时,通常我们不会采取主动刷新缓存的策略,那么则依然会面对上面的问题。


    而 Uber 的 CacheFront 基于企业内部的 Flux 技术组件实现对缓存的异步处理,通过阅读文章我们也可以发现这个异步延迟在秒级,那么在如此长的时间间隙后,无论采用删除还是更新策略想要产生上图中的不一致现象都比较难,因为对应用系统来说,进程/线程阻塞 2-3 秒是很难以忍受的现象,所以通常不会出现如此漫长的阻塞与卡顿。


    如果你想进一步了解如何实现与 Uber 利用 Flux 实现缓存异步处理的内容,也可阅读我们此前缓存与主副本数据一致性系统设计方案(下篇)文章中更新主副本数据后更新缓存并发问题解决方案章节。


    总结


    本文并非对延迟双删的全盘否定,而是强调在特殊场景下,延迟双删策略的弊端会被放大,进而完全盖过其优势。对于那些业务体量大伴随着流量大的应用系统,必应要从中权衡取舍。


    每一种策略都仅适配应用系统生命周期的一段。只不过部分企业随着业务发展逐步壮大,其研发基础设施的能力也更完善。从而为系统设计带来诸多便捷,从而使得技术决策变得与中小研发团队截然不同。


    所以当我们在学习他人经验的过程中,到了落地执行环节一定要结合实际团队背景、业务需求、开发周期与资金预算进行灵活适配。如果你希望了解更多技术中立(排除特定基础设施)的系统设计方案,欢迎你关注我的账号或订阅我的系统设计实战:常用架构与模式详解专栏,我将在其中持续更新技术中立的系统设计系列文章。如果您发现文章内容中任何不准确或遗漏的部分。非常希望您能评论指正,我将尽快修正疏漏,为大家提供优质技术内容。



    相关阅读





    你好,我是 HAibiiin,一名探索技术之外更多可能性的 Product Engineer。如果本篇文章对你有所启发或提供了一定价值,还请不要吝啬点赞、收藏和关注。
    haibiiin.github.io.png



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

    我这🤡般的7年开发生涯

    前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。 我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单...
    继续阅读 »

    前两天线上出了个漏洞,导致线上业务被薅了 2w 多块钱。几天晚上没咋睡,问 ChatGPT,查了几晚资料,复盘工作这么久来犯下的错误。



    我在公司做的大部分是探索性、创新性的需求,行内人都知道这些活都是那种脏活累活,需求变化大,经常一句话;需求功能多,看着简单一细想全是漏洞;需求又紧急,今天不上线业务就要没。


    所以第一个建议就是大家远离这些需求,否则你会和我一样变得不幸。


    但是👴🐂🍺啊,接下来也就算了,还全干完了。正常评估一个月的需求,我 tm 半个月干完上线;你给我一句话,我干完一整条链路上的事;你说必须今天上线,那就加班加点干上线。


    就这样干了几年,黄了很多,也有做起来的。但是不管业务怎么发展,这样做时间长了会出现很多致命问题。


    开发忙成狗


    一句话需求太多,到最后只有开发最了解业务,所有人所有事都来找开发,开发也是人,开发还要写代码呢。最先遇到的问题就是时间严重不够,产品跟个摆设一样,什么忙都帮不上,我成了产品开发结合体。


    bug 来了


    开发一忙,节奏就乱了,乱则生 bug,再加上原本需求上逻辑不完整的深坑,坑上叠坑,出 bug 是迟早的事。


    形象崩塌


    一旦出现 bug,人设就毁了。记住一句话,没人会感谢你把原本一个月的需求只用半个月上线,大家都觉得这玩意本来就半个月工时。慢慢的开始以半个月的工时要求你。


    那些 bug 自己回头,慢慢做都是可以避免的,就像考试的时候做完了卷子复查一遍,很多问题回头看一下都能发现,结果因为前期赶工,没时间回看,而且有很多图快的写法,后期都是容易出问题的。


    形象崩塌在职场中是最恐怖的,正所谓好事不出门,坏事传千里。


    一旦出了问题,团队、领导、所有人对你的体感,那都是直线下降,你之前做的所有好事,就跟消失了一样,别人对你的印象,一提起来说的都是,这不是当时写出 xxx bug 的人吗?这还怎么在职场生存?脸都没了,项目好处也跟自己没关系了。


    我 tm 真是愣头青啊蠢的💊💩,从入职开始都想的是多学点多干点,结果干的越多错的越多,现在心态干崩了,身体干垮了,钱还没混子多,还背了一身骂名和黑锅。


    之前我看同事写代码贼慢,鼠标点来点去,打字也慢一拍,我忍不住说他你这写代码速度太慢了,可以用 xxx 快捷键等等,现在回想起来,我说他不懂代码,其实是我不懂职场。


    我真是个纯纯的可悲🤡。


    提桶跑路


    bug 积累到一定程度,尤其是像我这样出现点资金的问题,那也差不多离走人不远了,我感觉我快到这个阶段了,即使不走,扣钱扣绩效也是在所难免的,综合算下来,还没那些混子赚的多。


    我亲自接触的联调一哥们儿,一杯茶,一包烟,一个 bug 修一天。是真真正正的修了一天,从早到晚。那天我要上线那个需求,我不停的催他,后来指着代码说着逻辑让他写,最终半夜转点上线。我累的半死不活,我工资和他差不多,出了问题我还要背锅。


    我现在听到 bug 都 PTSD 了,尤其是资金相关的,整个人就那种呆住,大脑空白,心脏像被揪住,我怀疑我有点心理问题了都。


    为什么别人可以那么安心的摸鱼?为什么我要如此累死累活还不讨好?我分析出几点我的性格问题。


    责任心过强


    什么事都觉得跟自己有关系,看着别人做的不好,我就自己上手。


    到后期产品真 tm 一句话啊,逻辑也不想,全等着我出开发方案,产品流程图,我再告诉她哪里要改动。不是哥们?合着我自己给出需求文档再自己写代码?


    为人老实


    不懂拒绝,不懂叫板。


    运营的需求,来什么做什么,说什么时候上线就什么时候上线。不是哥们?我都还不知道要做什么,你们把上线时间都定了?就 tm 两字,卑微。


    用力过猛


    十分力恨不得使出十一分,再加一分吃奶的劲儿。一开始就领导很高的期望,后面活越来越多,而且也没什么晋升机会了,一来的门槛就太高了知道吧,再想提升就很难了。


    先总结这么多吧,我现在心情激荡的很,希望给各位和我性格差不多一点提醒,别像我这样愣头青,吃力不讨好,还要遭人骂。后面再写写改进办法。


    作者:小兵张健
    来源:juejin.cn/post/7450047052804161576
    收起阅读 »

    被阿里抛弃的那个项目,救活了!

    众所周知,上个月的时候,GitHub 知名开源项目 EasyExcel 曾对外发布公告将停止主动更新,当时这个消息在网上还引发了不少讨论。 至此,这个运营了 6 年,在 GitHub 上累计收获 32k+ star 的知名项目基本就宣告停更了,大家都觉得挺可...
    继续阅读 »

    众所周知,上个月的时候,GitHub 知名开源项目 EasyExcel 曾对外发布公告将停止主动更新,当时这个消息在网上还引发了不少讨论。



    至此,这个运营了 6 年,在 GitHub 上累计收获 32k+ star 的知名项目基本就宣告停更了,大家都觉得挺可惜的。


    然而,就在阿里官宣停更的同时,EasyExcel 的原作者个人当即也站出来向大家透露了一个新项目计划,表示自己将会继续接手,并重启一个新项目开源出来。


    那现在,这个承诺已经如期兑现了


    就在上周,EasyExcel 作者自己也正式发文表示,EasyExcel 的替代方案正式来了,相信不少同学也看到了这个消息。


    作者把新项目定名为:FastExcelps:作者一开始初定叫 EasyExcel-Plus,后来改名为了 FastExcel),用于取代已经被阿里官方停更的 EasyExcel 项目。



    新项目 FastExcel 同样定位在一个基于 Java 的、快速、简洁、解决大文件内存溢出的 Excel 处理工具。



    并且新项目将兼容老的已停更的 EasyExcel 项目,并提供项目维护、性能优化、以及bugFix。


    同时作者还表示,新项目 FastExcel 将始终坚持免费开源,并采用最开放的 MIT 协议,使其适用于任何商业化场景,以便为广大开发者和企业继续提供极大的自由度和灵活性。


    不得不说,作者的格局还是相当打开的。



    大家都知道,其实 EasyExcel 项目的作者原本就是工作在阿里,负责主导并维护着这个项目。


    然而就在去年,Easyexcel 作者就从阿里离职出来创业了。


    所以在上个月 EasyExcel 被宣布停更的时候,当时就有不少网友猜测各种原因。当然背后的真相我们不得而知,不过作为用户的角度来看,Easyexcel 以另外一种形式被继续开源和更新也何尝不是一件利好用户的好消息。



    之前的 EasyExcel 之所以受到开发者们的广泛关注,主要是因为它具备了许多显著的特点和优势,而这次的新项目 FastExcel 更可谓是有过之而无不及。



    • 首先,FastExcel 同样拥有卓越的读写性能,能够高效地处理大规模的Excel数据,这对于需要处理大量数据的开发者来说依然是一大福音。

    • 其次,FastExcel 的 API设计简洁直观,开发者可以轻松上手,无需花费大量时间学习和熟悉。

    • 再者,FastExcel 同样支持流式读取,可以显著降低内存占用,避免在处理大规模数据时可能出现的内存溢出问题。

    • 此外,新项目 FastExcel 完全兼容原来 EasyExcel 的功能和特性,用户可以在项目中无缝过渡,从 EasyExcel 迁移到 FastExcel 只需更换包名和依赖即可完成升级。


    FastExcel 的安装配置也非常简单。


    对于使用 Maven 或 Gradle 进行构建的项目来说,只需在相应的配置文件中添加如下所示的 FastExcel 的依赖即可。



    • Maven项目


    <dependency>
    <groupId>cn.idev.excel</groupId>
    <artifactId>fastexcel</artifactId>
    <version>1.0.0</version>
    </dependency>


    • Gradle项目


    dependencies {
    implementation 'cn.idev.excel:fastexcel:1.0.0'
    }

    在实际使用中,以读取Excel文件为例,开发者只需定义一个数据类和一个监听器类,然后在主函数中调用 FastExcel 的读取方法,并传入数据类和监听器类即可。


    FastExcel 会自动解析 Excel 文件中的数据,并将其存储到数据类的实例中,同时触发监听器类中的方法,让开发者可以对解析到的数据进行处理。


    // 实现 ReadListener 接口,设置读取数据的操作
    public class DemoDataListener implements ReadListener<DemoData> {
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
    System.out.println("解析到一条数据" + JSON.toJSONString(data));
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
    System.out.println("所有数据解析完成!");
    }
    }

    public static void main(String[] args) {
    String fileName = "demo.xlsx";
    // 读取 Excel 文件
    FastExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
    }

    同样地,写 Excel 文件也非常简单,开发者只需定义一个数据类,并填充要写入的数据,然后调用 FastExcel 的写入方法即可。


    // 示例数据类
    public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
    @ExcelIgnore
    private String ignore;
    }

    // 填充要写入的数据
    private static List<DemoData> data() {
    List<DemoData> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
    DemoData data = new DemoData();
    data.setString("字符串" + i);
    data.setDate(new Date());
    data.setDoubleData(0.56);
    list.add(data);
    }
    return list;
    }

    public static void main(String[] args) {
    String fileName = "demo.xlsx";
    // 创建一个名为“模板”的 sheet 页,并写入数据
    FastExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
    }

    过程可谓是清晰易懂、直观明了,所以这对于开发者来说,在使用 FastExcel 时可以轻松上手。


    新项目 FastExcel 刚开源不久,目前在 GitHub 上的 star 标星就已经突破了 2000!这也可见其受欢迎程度。



    而且伴随着新项目的开源上线,开发者们的参与热情也是十分高涨的。


    这才多少天,项目就已经收到上百条issue了。


    仔细看了一下会发现,其中一大部分是开发者们对于新项目所提的需求或反馈。



    而还有另外一部分则是对于新项目 FastExcel 以及作者的肯定与鼓励。



    文章的最后也感谢项目作者的辛勤维护,大家有需要的话也可以上去提需求或者反馈一些意见,另外感兴趣的同学也可以上去研究研究相关的代码或者参与项目,尤其是数据处理这一块,应该会挺有收获的。



    注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。



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

    面试官:GROUP BY和DISTINCT有什么区别?

    在 MySQL 中,GR0UP BY 和 DISTINCT 都是用来处理查询结果中的重复数据,并且在官方的描述文档中也可以看出:在大多数情况下 DISTINCT 是特殊的 GR0UP BY,如下图所示:官方文档地址:dev.mysql.com/doc/refm...
    继续阅读 »

    在 MySQL 中,GR0UP BY 和 DISTINCT 都是用来处理查询结果中的重复数据,并且在官方的描述文档中也可以看出:在大多数情况下 DISTINCT 是特殊的 GR0UP BY,如下图所示:

    官方文档地址:dev.mysql.com/doc/refman/…

    但二者还是有一些细微的不同,接下来一起来看。

    1.DISTINCT 介绍

    1. 用途:DISTINCT 用于从查询结果中去除重复的行,确保返回的结果集中每一行都是唯一的。
    2. 语法:通常用于 SELECT 语句中,紧跟在 SELECT 关键字之后。例如以下 SQL:
    SELECT DISTINCT column1, column2 FROM table_name;
    1. 工作机制:DISTINCT 会对整个结果集进行去重,即只要结果集中的某一行与另一行完全相同,就会被去除。

    2.GR0UP BY 介绍

    1. 用途:GR0UP BY 主要用于对结果集按照一个或多个列进行分组,通常与聚合函数(如 COUNT, SUM, AVG, MAX, MIN 等)一起使用,以便对每个组进行统计。
    2. 语法:GR0UP BY 通常跟在 FROM 或 WHERE 子句之后,在 SELECT 语句的末尾部分。例如以下 SQL:
    SELECT column1, COUNT(*) FROM table_name GR0UP BY column1;
    1. 工作机制:GR0UP BY 将数据按指定的列进行分组,每个组返回一行数据。

    3.举例说明

    3.1 使用 DISTINCT

    假设有一个表 students,包含以下数据:

    idnameage
    1Alice20
    2Bob22
    3Alice20

    使用 DISTINCT 去除重复行:

    SELECT DISTINCT name, age FROM students;

    结果:

    nameage
    Alice20
    Bob22

    3.2 使用 GR0UP BY

    假设还是上面的表 students,我们想要统计每个学生的数量:

    SELECT name, COUNT(*) AS count FROM students GR0UP BY name;

    结果:

    namecount
    Alice2
    Bob1

    4.主要区别

    1. 功能不同:DISTINCT 用于去除重复行,而 GR0UP BY 用于对结果集进行分组,通常与聚合函数一起使用。
    2. 返回结果不同:DISTINCT 返回去重后的结果集,查询结果集中只能包含去重的列信息,有其他列信息会报错;GR0UP BY 返回按指定列分组后的结果集,可以展示多列信息,并可以包含聚合函数的计算结果。
    3. 应用场景不同:DISTINCT 更适合单纯的去重需求,GR0UP BY 更适合分组统计需求。
    4. 性能略有不同:如果去重的字段有索引,那么 GR0UP BY 和 DISTINCT 都可以使用索引,此情况它们的性能是相同的;而当去重的字段没有索引时,DISTINCT 的性能就会高于 GR0UP BY,因为在 MySQL 8.0 之前,GR0UP BY 有一个隐藏的功能会进行默认的排序,这样就会触发 filesort 从而导致查询性能降低。

    课后思考

    count(*)、count(1) 和 count(字段) 有什么区别?

    本文已收录到我的面试小站 http://www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。


    作者:Java中文社群
    来源:juejin.cn/post/7415914114650685481
    收起阅读 »

    2024年的安卓现代开发

    大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀 如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化. 免责声明 📝 本文反映了我的个人观点和专业见解, 并参考...
    继续阅读 »


    大家好 👋🏻, 新的一年即将开始, 我不想错过这个机会, 与大家分享一篇题为《2024年的安卓现代开发》的文章. 🚀


    如果你错过了我的上一篇文章, 可以看看2023年的安卓现代开发并回顾一下今年的变化.


    免责声明


    📝 本文反映了我的个人观点和专业见解, 并参考了 Android 开发者社区中的不同观点. 此外, 我还定期查看 Google 为 Android 提供的指南.


    🚨 需要强调的是, 虽然我可能没有明确提及某些引人注目的工具, 模式和体系结构, 但这并不影响它们作为开发 Android 应用的宝贵替代方案的潜力.


    Kotlin 无处不在 ❤️



    Kotlin是由JetBrains开发的一种编程语言. 由谷歌推荐, 谷歌于 2017 年 5 月正式发布(查看这里). 它是一种与 Java 兼容的现代编程语言, 可以在 JVM 上运行, 这使得它在 Android 应用开发中的采用非常迅速.


    无论你是不是安卓新手, 都应该把 Kotlin 作为首选, 不要逆流而上 🏊🏻 😎, 谷歌在 2019 年谷歌 I/O 大会上宣布了这一做法. 有了 Kotlin, 你就能使用现代语言的所有功能, 包括协程的强大功能和使用为 Android 生态系统开发的现代库.


    请查看Kotlin 官方文档


    Kotlin 是一门多用途语言, 我们不仅可以用它来开发 Android 应用, 尽管它的流行很大程度上是由于 Android 应用, 我们可以从下图中看到这一点.




    KotlinConf ‘23


    Kotlin 2.0 要来了


    另一个需要强调的重要事件是Kotlin 2.0的发布, 它近在眼前. 截至本文发稿之日, 其版本为 2.0.0-beta4



    新的 K2 编译器 也是 Kotlin 2.0 的另一个新增功能, 它将带来显著的性能提升, 加速新语言特性的开发, 统一 Kotlin 支持的所有平台, 并为多平台项目提供更好的架构.


    请查看 KotlinConf '23 的回顾, 你可以找到更多信息.


    Compose 🚀




    Jetpack Compose 是 Android 推荐的用于构建本地 UI 的现代工具包. 它简化并加速了 Android 上的 UI 开发. 通过更少的代码, 强大的工具和直观的 Kotlin API, 快速实现你的应用.




    Jetpack Compose 是 Android Jetpack 库的一部分, 使用 Kotlin 编程语言轻松创建本地UI. 此外, 它还与 LiveData 和 ViewModel 等其他 Android Jetpack 库集成, 使得构建具有强反应性和可维护性的 Android 应用变得更加容易.


    Jetpack Compose 的一些主要功能包括



    1. 声明式UI

    2. 可定制的小部件

    3. 与现有代码(旧视图系统)轻松集成

    4. 实时预览

    5. 改进的性能.


    资源:



    Android Jetpack ⚙️




    Jetpack是一套帮助开发者遵循最佳实践, 减少模板代码, 编写在不同Android版本和设备上一致运行的代码的库, 这样开发者就可以专注于他们的业务代码.


    _ Android Jetpack 文档



    其中最常用的工具有:



    Material You / Material Design 🥰



    Material You 是在 Android 12 中引入并在 Material Design 3 中实现的一项新的定制功能, 它使用户能够根据个人喜好定制操作系统的视觉外观. Material Design 是一个由指南, 组件和工具组成的可调整系统, 旨在坚持UI设计的最高标准. 在开源代码的支持下, Material Design 促进了设计师和开发人员之间的无缝协作, 使团队能够高效地创造出令人惊叹的产品.



    目前, Material Design 的最新版本是 3, 你可以在这里查看更多信息. 此外, 你还可以利用Material Theme Builder来帮助你定义应用的主题.


    代码仓库


    使用 Material 3 创建主题


    SplashScreen API



    Android 中的SplashScreen API对于确保应用在 Android 12 及更高版本上正确显示至关重要. 不更新会影响应用的启动体验. 为了在最新版本的操作系统上获得一致的用户体验, 快速采用该 API 至关重要.


    Clean架构



    Clean架构的概念是由Robert C. Martin提出的. 它的基础是通过将软件划分为不同层次来实现责任分离.


    特点



    1. 独立于框架.

    2. 可测试.

    3. 独立于UI

    4. 独立于数据库

    5. 独立于任何外部机构.


    依赖规则


    作者在他的博文Clean代码中很好地描述了依赖规则.



    依赖规则是使这一架构得以运行的首要规则. 这条规则规定, 源代码的依赖关系只能指向内部. 内圈中的任何东西都不能知道外圈中的任何东西. 特别是, 外圈中声明的东西的名称不能被内圈中的代码提及. 这包括函数, 类, 变量或任何其他命名的软件实体.




    安卓中的Clean架构:


    Presentation 层: Activitiy, Composable, Fragment, ViewModel和其他视图组件.
    Domain层: 用例, 实体, 仓库接口和其他Domain组件.
    Data层: 仓库的实现类, Mapper, DTO 等.


    Presentation层的架构模式


    架构模式是一种更高层次的策略, 旨在帮助设计软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案. 架构模式与设计模式类似, 但它们的规模更大, 解决的问题也更全面, 如系统的整体结构, 组件之间的关系以及数据管理的方式等.


    在Presentation层中, 我们有一些架构模式, 我想重点介绍以下几种:



    • MVVM

    • MVI


    我不想逐一解释, 因为在互联网上你可以找到太多相关信息. 😅


    此外, 你还可以查看应用架构指南.



    依赖注入


    依赖注入是一种软件设计模式, 它允许客户端从外部获取依赖关系, 而不是自己创建依赖关系. 它是一种在对象及其依赖关系之间实现控制反转(IoC)的技术.



    模块化


    模块化是一种软件设计技术, 可将应用划分为独立的模块, 每个模块都有自己的功能和职责.



    模块化的优势


    可重用性: 拥有独立的模块, 就可以在应用的不同部分甚至其他应用中重复使用.


    严格的可见性控制: 模块可以让你轻松控制向代码库其他部分公开的内容.


    自定义交付: Play特性交付 使用应用Bundle的高级功能, 允许你有条件或按需交付应用的某些功能.


    可扩展性: 通过独立模块, 可以添加或删除功能, 而不会影响应用的其他部分.


    易于维护: 将应用划分为独立的模块, 每个模块都有自己的功能和责任, 这样就更容易理解和维护代码.


    易于测试: 有了独立的模块, 就可以对它们进行隔离测试, 从而便于发现和修复错误.


    改进架构: 模块化有助于改进应用的架构, 从而更好地组织和结构代码.


    改善协作: 通过独立的模块, 开发人员可以不受干扰地同时开发应用的不同部分.


    构建时间: 一些 Gradle 功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化提高构建性能.


    更多信息请参阅官方文档.


    网络



    序列化


    在本节中, 我想提及两个我认为非常重要的工具: 与 Retrofit 广泛结合使用的Moshi, 以及 JetBrains 的 Kotlin 团队押宝的Kotlin Serialization.



    MoshiKotlin Serialization 是用于 Kotlin/Java 的两个序列化/反序列化库, 可将对象转换为 JSON 或其他序列化格式, 反之亦然. 这两个库都提供了友好的接口, 并针对移动和桌面应用进行了优化. Moshi 主要侧重于 JSON 序列化, 而 Kotlin Serialization 则支持包括 JSON 在内的多种序列化格式.


    图像加载



    要从互联网上加载图片, 有几个第三方库可以帮助你处理这一过程. 图片加载库为你做了很多繁重的工作; 它们既处理缓存(这样你就不用多次下载图片), 也处理下载图片并将其显示在屏幕上的网络逻辑.


    _ 官方安卓文档




    响应/线程管理




    说到响应式编程和异步进程, Kotlin协程凭借其suspend函数和Flow脱颖而出. 然而, 在 Android 应用开发中, 承认 RxJava 的价值也是至关重要的. 尽管协程和 Flow 的采用率越来越高, 但 RxJava 仍然是多个项目中稳健而受欢迎的选择.


    对于新项目, 请始终选择Kotlin协程❤️. 可以在这里探索一些Kotlin协程相关的概念.


    本地存储


    在构建移动应用时, 重要的一点是能够在本地持久化数据, 例如一些会话数据或缓存数据等. 根据应用的需求选择合适的存储选项非常重要. 我们可以存储键值等非结构化数据, 也可以存储数据库等结构化数据. 请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.



    建议:



    测试 🕵🏼



    软件开发中的测试对于确保产品质量至关重要. 它能检测错误, 验证需求并确保客户满意. 下面是一些最常用的测试工具::



    工具文档的测试部分


    截屏测试 📸



    Android 中的截屏测试涉及自动捕获应用中各种 UI 元素的屏幕截图, 并将其与基线图像进行比较, 以检测任何意外的视觉变化. 它有助于确保不同版本和配置的应用具有一致的UI外观, 并在开发过程的早期捕捉视觉回归.



    R8 优化


    R8 是默认的编译器, 可将项目的 Java 字节码转换为可在 Android 平台上运行的 DEX 格式. 通过缩短类名及其属性, 它可以帮助我们混淆和减少应用的代码, 从而消除项目中未使用的代码和资源. 要了解更多信息, 请查看有关 缩减, 混淆和优化应用 的 Android 文档. 此外, 你还可以通过ProGuard规则文件禁用某些任务或自定义 R8 的行为.




    • 代码缩减

    • 缩减资源

    • 混淆

    • 优化


    第三方工具



    • DexGuard


    Play 特性交付





    Google Play 的应用服务模式称为动态交付, 它使用 Android 应用Bundle为每个用户的设备配置生成并提供优化的 APK, 因此用户只需下载运行应用所需的代码和资源.




    自适应布局



    随着具有不同外形尺寸的移动设备使用量的增长, 我们需要一些工具, 使我们的 Android 应用能够适应不同类型的屏幕. 因此, Android 为我们提供了Window Size Class, 简单地说, 就是三大类屏幕格式, 它们是我们开发设计的关键点. 这样, 我们就可以避免考虑许多屏幕设计的复杂性, 从而将可能性减少到三组, 它们是 Compat, MediumExpanded.


    Window Size Class




    支持不同的屏幕尺寸


    我们拥有的另一个重要资源是Canonical Layout, 它是预定义的屏幕设计, 可用于 Android 应用中的大多数场景, 还为我们提供了如何将其适用于大屏幕的指南.



    其他相关资源



    Form-Factor培训


    本地化 🌎



    本地化包括调整产品以满足不同地区不同受众的需求. 这包括翻译文本, 调整格式和考虑文化因素. 其优势包括进入全球市场, 增强用户体验, 提高客户满意度, 增强在全球市场的竞争力以及遵守当地法规.


    注: BCP 47 是安卓系统使用的国际化标准.


    参考资料



    性能 🔋⚙️



    在开发 Android 应用时, 我们必须确保用户体验更好, 这不仅体现在应用的开始阶段, 还体现在整个执行过程中. 因此, 我们必须使用一些工具, 对可能影响应用性能的情况进行预防性分析和持续监控:



    应用内更新



    当你的用户在他们的设备上不断更新你的应用时, 他们可以尝试新功能, 并从性能改进和错误修复中获益. 虽然有些用户会在设备连接到未计量的连接时启用后台更新, 但其他用户可能需要提醒才能安装更新. 应用内更新是 Google Play 核心库的一项功能, 可提示活跃用户更新你的应用*.


    运行 Android 5.0(API 等级 21)或更高版本的设备支持应用内更新功能. 此外, 应用内更新仅支持 Android 移动设备, Android 平板电脑和 Chrome OS 设备.


    - 应用内更新文档




    应用内评论



    Google Play 应用内评论 API 可让你提示用户提交 Play Store 评级和评论, 而无需离开你的应用或游戏.


    一般来说, 应用内评论流程可在用户使用应用的整个过程中随时触发. 在流程中, 用户可以使用 1-5 星系统对你的应用进行评分, 并添加可选评论. 一旦提交, 评论将被发送到 Play Store 并最终显示出来.


    *为保护用户隐私并避免 API 被滥用, 你的应用应严格遵守有关何时请求应用内评论评论提示的设计的规定.


    - 应用内评论文档




    可观察性 👀



    在竞争日益激烈的应用生态系统中, 要获得良好的用户体验, 首先要确保应用没有错误. 确保应用无错误的最佳方法之一是在问题出现时立即发现并知道如何着手解决. 使用 Android Vitals 来确定应用中崩溃和响应速度问题最多的区域. 然后, 利用 Firebase Crashlytics 中的自定义崩溃报告获取更多有关根本原因的详细信息, 以便有效地排除故障.


    工具



    辅助功能



    辅助功能是软件设计和构建中的一项重要功能, 除了改善用户体验外, 还能让有辅助功能需求的人使用应用. 这一概念旨在改善的不能包括: 有视力问题, 色盲, 听力问题, 灵敏性问题和认知障碍等.


    考虑因素:



    • 增加文字的可视性(颜色对比度, 可调整文字大小)

    • 使用大而简单的控件

    • 描述每个UI元素


    更多详情请查看辅助功能 - Android 文档


    安全性 🔐



    在开发保护设备完整性, 数据安全性和用户信任的应用时, 安全性是我们必须考虑的一个方面, 甚至是最重要的方面.



    • 使用凭证管理器登录用户: 凭据管理器 是一个 Jetpack API, 在一个单一的 API 中支持多种登录方法, 如用户名和密码, 密码匙和联合登录解决方案(如谷歌登录), 从而简化了开发人员的集成.

    • 加密敏感数据和文件: 使用EncryptedSharedPreferencesEncryptedFile.

    • 应用基于签名的权限: 在你可控制的应用之间共享数据时, 使用基于签名的权限.


    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <permission android:name="my_custom_permission_name"
    android:protectionLevel="signature" />


    • 不要将密钥, 令牌或应用配置所需的敏感数据直接放在项目库中的文件或类中. 请使用local.properties.

    • 实施 SSL Pinning: 使用 SSL Pinning 进一步确保应用与远程服务器之间的通信安全. 这有助于防止中间人攻击, 并确保只与拥有特定 SSL 证书的受信任服务器进行通信.


    res/xml/network_security_config.xml


    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
    <domain-config>
    <domain includeSubdomains="true">example.com</domain>
    <pin-set expiration="2018-01-01">
    <pin digest="SHA-256">ReplaceWithYourPin</pin>
    <!-- backup pin -->
    <pin digest="SHA-256">ReplaceWithYourPin</pin>
    </pin-set>
    </domain-config>
    </network-security-config>


    • 实施运行时应用自我保护(RASP): 这是一种在运行时保护应用免受攻击和漏洞的安全技术. RASP 的工作原理是监控应用的行为, 并检测可能预示攻击的可疑活动:



      • 代码混淆.

      • 根检测.

      • 篡改/应用钩子检测.

      • 防止逆向工程攻击.

      • 反调试技术.

      • 虚拟环境检测

      • 应用行为的运行时分析.




    想了解更多信息, 请查看安卓应用中的运行时应用自我保护技术(RASP). 还有一些 Android 安全指南.


    版本目录


    Gradle 提供了一种集中管理项目依赖关系的标准方式, 叫做版本目录; 它在 7.0 版中被试验性地引入, 并在 7.4 版中正式发布.


    优点:



    • 对于每个目录, Gradle 都会生成类型安全的访问器, 这样你就可以在 IDE 中通过自动补全轻松添加依赖关系.

    • 每个目录对构建过程中的所有项目都是可见的. 它是声明依赖项版本的中心位置, 并确保该版本的变更适用于每个子项目.

    • 目录可以声明 dependency bundles, 即通常一起使用的 "依赖关系组".

    • 目录可以将依赖项的组和名称与其实际版本分开, 而使用版本引用, 这样就可以在多个依赖项之间共享一个版本声明.


    请查看更多信息


    Secret Gradle 插件


    Google 强烈建议不要将 API key 输入版本控制系统. 相反, 你应该将其存储在本地的 secrets.properties 文件中, 该文件位于项目的根目录下, 但不在版本控制范围内, 然后使用 Secrets Gradle Plugin for Android 来读取 API 密钥.


    日志


    日志是一种软件工具, 用于记录程序的执行信息, 重要事件, 错误调试信息以及其他有助于诊断问题或了解程序运行情况的信息. 日志可配置为将信息写入不同位置, 如日志文件, 控制台, 数据库, 或将信息发送到日志记录服务器.



    Linter / 静态代码分析器



    Linter 是一种编程工具, 用于分析程序源代码, 查找代码中的潜在问题或错误. 这些问题可能是语法问题, 代码风格不当, 缺乏文档, 安全问题等, 它们会对代码的质量和可维护性产生影响.



    Google Play Instant



    Google Play Instant 使本地应用和游戏无需安装即可在运行 Android 5.0(API 等级 21)或更高版本的设备上启动. 你可以使用 Android Studio 构建这类体验, 称为即时应用即时游戏. 通过允许用户运行即时应用或即时游戏(即提供即时体验), 你可以提高应用或游戏的发现率, 从而有助于吸引更多活跃用户或安装.



    新设计中心



    安卓团队提供了一个新的设计中心, 帮助创建美观, 现代的安卓应用, 这是一个集中的地方, 可以了解安卓在多种形态因素方面的设计.


    点击查看新的设计中心


    人工智能



    GeminiPalM 2是谷歌开发的两个最先进的人工智能(AI)模型, 它们将改变安卓应用开发的格局. 这些模型具有一系列优势, 将推动应用的效率, 用户体验和创新.



    人工智能编码助手工具


    Studio Bot



    Studio Bot 是你的 Android 开发编码助手. 它是 Android Studio 中的一种对话体验, 通过回答 Android 开发问题帮助你提高工作效率. 它采用人工智能技术, 能够理解自然语言, 因此你可以用简单的英语提出开发问题. Studio Bot 可以帮助 Android 开发人员生成代码, 查找相关资源, 学习最佳实践并节省时间.


    Studio Bot



    Github Copilot


    GitHub Copilot 是一个人工智能配对程序员. 你可以使用 GitHub Copilot 直接在编辑器中获取整行或整个函数的建议.


    Amazon CodeWhisperer


    这是亚马逊的一项服务, 可根据你当前代码的上下文生成代码建议. 它能帮助你编写更高效, 更安全的代码, 并发现新的 API 和工具.


    Kotlin Multiplatform 🖥 📱⌚️ 🥰 🚀



    最后, 同样重要的是, Kotlin Multiplatform 🎉 是本年度最引人注目的新产品. 它是跨平台应用开发领域的有力竞争者. 虽然我们的主要重点可能是 Android 应用开发, 但 Kotlin Multiplatform 为我们提供了利用 KMP 框架制作完全本地 Android 应用的灵活性. 这一战略举措不仅为我们的项目提供了未来保障, 还为我们提供了必要的基础架构, 以便在我们选择多平台环境时实现无缝过渡. 🚀


    如果你想深入了解 Kotlin Multiplatform, 我想与你分享这几篇文章. 在这些文章中, 探讨了这项技术的现状及其对现代软件开发的影响:



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

    Java 8 魔法:利用 Function 接口告别冗余代码,打造高效断言神器

    前言 在 Java 开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,Java 8 带来了函数式编程的春风,以 Function 接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。本文将以一个实际应用...
    继续阅读 »

    前言


    Java 开发的征途中,我们时常与重复代码不期而遇。这些重复代码不仅让项目显得笨重,更增加了维护成本。幸运的是,Java 8 带来了函数式编程的春风,以 Function 接口为代表的一系列新特性,为我们提供了破除这一难题的利剑。本文将以一个实际应用场景为例,即使用 Java 8 的函数式编程特性来重构数据有效性断言逻辑,展示如何通过 SFunction(基于 Java 8Lambda 表达式封装)减少代码重复,从而提升代码的优雅性和可维护性。


    背景故事:数据校验的烦恼


    想象一下,在一个复杂的业务系统中,我们可能需要频繁地验证数据库中某个字段值是否有效,是否符合预期值。传统的做法可能充斥着大量相似的查询逻辑,每次都需要手动构建查询条件、执行查询并处理结果,这样的代码既冗长又难以维护。


    例如以下两个验证用户 ID 和部门 ID 是否有效的方法,虽然简单,但每次需要校验不同实体或不同条件时,就需要复制粘贴并做相应修改,导致代码库中充满了大量雷同的校验逻辑,给维护带来了困扰。


    // 判断用户 ID 是否有效
    public void checkUserExistence(String userId) {
    User user = userDao.findById(userId);
    if (user == null) {
    throw new RuntimeException("用户ID无效");
    }
    }

    // 判断部门 ID 是否有效
    public void checkDeptExistence(String deptId) {
    Dept dept = deptDao.findById(deptId);
    if (dept == null) {
    throw new RuntimeException("部门ID无效");
    }
    }

    Java 8 的魔法棒:函数式接口


    Java 8 引入了函数式接口的概念,其中 Function<T, R> 是最基础的代表,它接受一个类型 T 的输入,返回类型 R 的结果。而在 MyBatis Plus 等框架中常用的 SFunction 是对 Lambda 表达式的进一步封装,使得我们可以更加灵活地操作实体类的属性。


    实战演练:重构断言方法


    下面的 ensureColumnValueValid 方法正是利用了函数式接口的魅力,实现了对任意实体类指定列值的有效性断言:


    /**
    * 确认数据库字段值有效(通用)
    *
    * @param <V> 待验证值的类型
    * @param valueToCheck 待验证的值
    * @param columnExtractor 实体类属性提取函数
    * @param queryExecutor 单条数据查询执行器
    * @param errorMessage 异常提示信息模板
    */

    public static <T, R, V> void ensureColumnValueValid(V valueToCheck, SFunction<T, R> columnExtractor, SFunction<LambdaQueryWrapper<T>, T> queryExecutor, String errorMessage) {
    if (valueToCheck == null) return;

    LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(columnExtractor);
    wrapper.eq(columnExtractor, valueToCheck);
    wrapper.last("LIMIT 1");

    T entity = queryExecutor.apply(wrapper);
    R columnValue = columnExtractor.apply(entity);
    if (entity == null || columnValue == null)
    throw new DataValidationException(String.format(errorMessage, valueToCheck));
    }

    这个方法接受一个待验证的值、一个实体类属性提取函数、一个单行数据查询执行器和一个异常信息模板作为参数。通过这四个参数,不仅能够进行针对特定属性的有效性检查,而且还能生成具有一致性的异常信息。


    对比分析


    使用 Function 改造前


    // 判断用户 ID 是否有效
    public void checkUserExistence(String userId) {
    User user = userDao.findById(userId);
    if (user == null) {
    throw new RuntimeException("用户ID无效");
    }
    }

    // 判断部门 ID 是否有效
    public void checkDeptExistence(String deptId) {
    Dept dept = deptDao.findById(deptId);
    if (dept == null) {
    throw new RuntimeException("部门ID无效");
    }
    }

    使用 Function 改造后


    public void assignTaskToUser(AddOrderDTO dto) {
    ensureColumnValueValid(dto.getUserId(), User::getId, userDao::getOne, "用户ID无效");
    ensureColumnValueValid(dto.getDeptId(), Dept::getId, deptDao::getOne, "部门ID无效");
    ensureColumnValueValid(dto.getCustomerId(), Customer::getId, customerDao::getOne, "客户ID无效");
    ensureColumnValueValid(dto.getDeptId(), Dept::getId, deptDao::getOne, "部门ID无效");
    ensureColumnValueValid(dto.getSupplieId(), Supplie::getId, supplierDao::getOne, "供应商ID无效");

    // 现在可以确信客户存在
    Customer cus = customerDao.findById(dto.getCustomerId());

    // 创建订单的逻辑...
    }

    对比上述两段代码,我们发现后者不仅大幅减少了代码量,而且通过函数式编程,表达出更为清晰的逻辑意图,可读性和可维护性都有所提高。


    优点



    1. 减少重复代码: 通过 ensureColumnValueValid 方法,所有涉及数据库字段值有效性检查的地方都可以复用相同的逻辑,将变化的部分作为参数传递,大大减少了因特定校验逻辑而产生的代码量。

    2. 增强代码复用: 抽象化的校验方法适用于多种场景,无论是用户ID、订单号还是其他任何实体属性的校验,一套逻辑即可应对。

    3. 提升可读性和维护性: 通过清晰的函数签名和 Lambda 表达式,代码意图一目了然,降低了后续维护的成本。

    4. 灵活性和扩展性: 当校验规则发生变化时,只需要调整 ensureColumnValueValid 方法或其内部实现,所有调用该方法的地方都会自动受益,提高了系统的灵活性和扩展性。


    举一反三:拓展校验逻辑的边界


    通过上述的实践,我们见识到了函数式编程在简化数据校验逻辑方面的威力。但这只是冰山一角,我们可以根据不同的业务场景,继续扩展和完善校验逻辑,实现更多样化的校验需求。以下两个示例展示了如何在原有基础上进一步深化,实现更复杂的数据比较和验证功能。


    断言指定列值等于预期值


    首先,考虑一个场景:除了验证数据的存在性,我们还需确认查询到的某列值是否与预期值相符。这在验证用户角色、状态变更等场景中尤为常见。为此,我们设计了 validateColumnValueMatchesExpected 方法:


    /**
    * 验证查询结果中指定列的值是否与预期值匹配
    *
    * @param <T> 实体类型
    * @param <R> 目标列值的类型
    * @param <C> 查询条件列值的类型
    * @param targetColumn 目标列的提取函数,用于获取想要验证的列值
    * @param expectedValue 期望的列值
    * @param conditionColumn 条件列的提取函数,用于设置查询条件
    * @param conditionValue 条件列对应的值
    * @param queryMethod 执行查询的方法引用,返回单个实体对象
    * @param errorMessage 验证失败时抛出异常的错误信息模板
    * @throws RuntimeException 当查询结果中目标列的值与预期值不匹配时抛出异常
    */

    public static <T, R, C> void validateColumnValueMatchesExpected(
    SFunction<T, R> targetColumn, R expectedValue,
    SFunction<T, C> conditionColumn, C conditionValue,
    SFunction<LambdaQueryWrapper<T>, T> queryMethod,
    String errorMessage)
    {

    // 创建查询包装器,选择目标列并设置查询条件
    LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(targetColumn);
    wrapper.eq(conditionColumn, conditionValue);

    // 执行查询方法
    T one = queryMethod.apply(wrapper);
    // 如果查询结果为空,则直接返回,视为验证通过(或忽略)
    if (one == null) return;

    // 获取查询结果中目标列的实际值
    R actualValue = targetColumn.apply(one);

    // 比较实际值与预期值是否匹配,这里假设notMatch是一个自定义方法用于比较不匹配情况
    boolean doesNotMatch = notMatch(actualValue, expectedValue);
    if (doesNotMatch) {
    // 若不匹配,则根据错误信息模板抛出异常
    throw new RuntimeException(String.format(errorMessage, expectedValue, actualValue));
    }
    }

    // 假设的辅助方法,用于比较值是否不匹配,根据实际需要实现
    private static <R> boolean notMatch(R actual, R expected) {
    // 示例简单实现为不相等判断,实际情况可能更复杂
    return !Objects.equals(actual, expected);
    }


    这个方法允许我们指定一个查询目标列(targetColumn)、预期值(expectedValue)、查询条件列(conditionColumn)及其对应的条件值(conditionValue),并提供一个查询方法(queryMethod)来执行查询。如果查询到的列值与预期不符,则抛出异常,错误信息通过 errorMessage 参数定制。


    应用场景:
    例如在一个权限管理系统中,当需要更新用户角色时,系统需要确保当前用户的角色在更新前是 “普通用户”,才能将其升级为 “管理员”。此场景下,可以使用 validateColumnValueMatchesExpected 方法来验证用户当前的角色是否确实为“普通用户”。


    // 当用户角色不是 “普通用户” 时抛异常
    validateColumnValueMatchesExpected(User::getRoleType, "普通用户", User::getId, userId, userMapper::getOne, "用户角色不是普通用户,无法升级为管理员!");

    断言指定值位于期望值列表内


    进一步,某些情况下我们需要验证查询结果中的某一列值是否属于一个预设的值集合。例如,验证用户角色是否合法。为此,我们创建了 validateColumnValueMatchesExpectedList 方法:


    /**
    * 验证查询结果中指定列的值是否位于预期值列表内
    *
    * @param <T> 实体类型
    * @param <R> 目标列值的类型
    * @param <C> 查询条件列值的类型
    * @param targetColumn 目标列的提取函数,用于获取想要验证的列值
    * @param expectedValueList 期望值的列表
    * @param conditionColumn 条件列的提取函数,用于设置查询条件
    * @param conditionValue 条件列对应的值
    * @param queryMethod 执行查询的方法引用,返回单个实体对象
    * @param errorMessage 验证失败时抛出异常的错误信息模板
    * @throws RuntimeException 当查询结果中目标列的值不在预期值列表内时抛出异常
    */

    public static <T, R, C> void validateColumnValueInExpectedList(
    SFunction<T, R> targetColumn, List<R> expectedValueList,
    SFunction<T, C> conditionColumn, C conditionValue,
    SFunction<LambdaQueryWrapper<T>, T> queryMethod,
    String errorMessage)
    {

    LambdaQueryWrapper<T> wrapper = new LambdaQueryWrapper<>();
    wrapper.select(targetColumn);
    wrapper.eq(conditionColumn, conditionValue);

    T one = queryMethod.apply(wrapper);
    if (one == null) return;

    R actualValue = targetColumn.apply(one);
    if (actualValue == null) throw new RuntimeException("列查询结果为空");

    if (!expectedValueList.contains(actualValue)) {
    throw new RuntimeException(errorMessage);
    }
    }

    这个方法接受一个目标列(targetColumn)、一个预期值列表(expectedValueList)、查询条件列(conditionColumn)及其条件值(conditionValue),同样需要一个查询方法(queryMethod)。如果查询到的列值不在预期值列表中,则触发异常。


    应用场景: 在一个电商平台的订单处理流程中,系统需要验证订单状态是否处于可取消的状态列表里(如 “待支付”、“待发货”)才允许用户取消订单。此时,validateColumnValueInExpectedList 方法能有效确保操作的合法性。


    // 假设 OrderStatusEnum 枚举了所有可能的订单状态,cancelableStatuses 包含可取消的状态
    List<String> cancelableStatuses = Arrays.asList(OrderStatusEnum.WAITING_PAYMENT.getValue(), OrderStatusEnum.WAITING_DELIVERY.getValue());

    // 验证订单状态是否在可取消状态列表内
    validateColumnValueInExpectedList(Order::getStatus, cancelableStatuses, Order::getOrderId, orderId, orderMapper::selectOne, "订单当前状态不允许取消!");

    通过这两个扩展方法,我们不仅巩固了函数式编程在减少代码重复、提升代码灵活性方面的优势,还进一步证明了通过抽象和泛型设计,可以轻松应对各种复杂的业务校验需求,使代码更加贴近业务逻辑,易于理解和维护。


    核心优势



    1. 代码复用:通过泛型和函数式接口,该方法能够适应任何实体类和属性的校验需求,大大减少了重复的查询逻辑代码。

    2. 清晰表达意图:方法签名直观表达了校验逻辑的目的,提高了代码的可读性和可维护性。

    3. 灵活性:使用者只需提供几个简单的 Lambda 表达式,即可完成复杂的查询逻辑配置,无需关心底层实现细节。

    4. 易于维护与扩展:



      1. 当需要增加新的实体验证时,仅需调用 ensureColumnValueValid 并传入相应的参数,无需编写新的验证逻辑,降低了维护成本。

      2. 修改验证规则时,只需调整 ensureColumnValueValid 内部实现,所有调用处自动遵循新规则,便于统一管理。

      3. 异常处理集中于 ensureColumnValueValid 方法内部,统一了异常抛出行为,避免了在多个地方处理相同的逻辑错误,减少了潜在的错误源。




    函数式编程的力量


    通过这个实例,我们见证了函数式编程在简化代码、提高抽象层次上的强大能力。在 Java 8 及之后的版本中,拥抱函数式编程思想,不仅能够使我们的代码更加简洁、灵活,还能在一定程度上促进代码的正确性和可测试性。因此,无论是日常开发还是系统设计,都值得我们深入探索和应用这一现代编程范式,让代码如魔法般优雅而高效。


    作者:最光阴2023
    来源:juejin.cn/post/7384256110280572980
    收起阅读 »

    让同事用Cesium写一个测量工具并支持三角测量,他说有点难。。

    web
    大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合。这是2024年输出的第39/100篇文章。 可视化&Webgis交流群+V:brown_7778(备注来意) 前言 最近在开发智慧城市的项目,产品想让同事基于Cesium开发一个测量工具,需要...
    继续阅读 »

    大家好,我是日拱一卒的攻城师不浪,致力于技术与艺术的融合。这是2024年输出的第39/100篇文章。

    可视化&Webgis交流群+V:brown_7778(备注来意)



    前言


    最近在开发智慧城市的项目,产品想让同事基于Cesium开发一个测量工具,需要支持长度测量面积测量以及三角测量,但同事挠了挠头,说做这个有点费劲,还反问了产品:做这功能有啥意义?


    产品经理:测量工具在智慧城市中发挥了重要的作用,通过对城市道路,地形,建筑物,场地等的精确测量,确保施工规划能够与现实场景精准吻合,节省人力以及施工成本。


    对桥梁、隧道、地铁、管网等城市基础设施进行结构健康监测,安装传感器,实时监测结构体震动以及结构体偏移量等数据,确保设施安全运行并能够提前发现问题,防患于未然。


    开发同事听完,觉得还蛮有道理,看向我:浪浪,如何应对?


    我:呐,拿走直接抄!下班请吃铜锅涮肉!



    三角测量


    先来了解下三角测量:是一种基于三角形几何原理的测量方法,用于确定未知点的位置。它通过已知基线(即两个已知点之间的距离)和从这两个已知点测量的度,计算出目标点的精确位置。


    例如在建筑施工中,工程师使用三角测量法来测量楼体高度、桥梁等结构的位置角度,确保建筑的精准施工。



    代码解析


    接下来看下这个MeasureTool类,主要包含以下功能:



    1. 坐标转换:整理了地理坐标(WGS84)与笛卡尔坐标(Cartesian)之间的转换功能。

    2. 拾取功能:通过屏幕坐标拾取场景中的三维位置,并判断该位置是位于模型上、地形上还是椭球体表面。

    3. 距离测量:绘制线段,并在场景中显示起点和终点之间的距离。

    4. 面积测量:通过给定的一组坐标,计算它们组成的多边形面积。

    5. 三角测量:绘制一个三角形来测量水平距离、直线距离和高度差。


    坐标转换功能



    • transformWGS84ToCartesian: 将WGS84坐标(经度、纬度、高度)转换为Cesium中的三维笛卡尔坐标。

    • transformCartesianToWGS84: 将Cesium的三维笛卡尔坐标转换为WGS84坐标。


    核心代码:


    transformWGS84ToCartesian(position, alt) {
    return position
    ? Cesium.Cartesian3.fromDegrees(
    position.lng || position.lon,
    position.lat,
    (position.alt = alt || position.alt),
    Cesium.Ellipsoid.WGS84
    )
    : Cesium.Cartesian3.ZERO;
    }

    transformCartesianToWGS84(cartesian) {
    var ellipsoid = Cesium.Ellipsoid.WGS84;
    var cartographic = ellipsoid.cartesianToCartographic(cartesian);
    return {
    lng: Cesium.Math.toDegrees(cartographic.longitude),
    lat: Cesium.Math.toDegrees(cartographic.latitude),
    alt: cartographic.height,
    };
    }

    Cesium的Cartesian3.fromDegreesEllipsoid.WGS84.cartesianToCartographic方法分别用于实现经纬度与笛卡尔坐标系的相互转换。


    拾取功能


    拾取功能允许通过屏幕像素坐标来获取3D场景中的位置。主要依赖scene.pickPositionscene.globe.pick来实现拾取。


    核心代码:


    getCatesian3FromPX(px) {
    var picks = this._viewer.scene.drillPick(px);
    var cartesian = this._viewer.scene.pickPosition(px);
    if (!cartesian) {
    var ray = this._viewer.scene.camera.getPickRay(px);
    cartesian = this._viewer.scene.globe.pick(ray, this._viewer.scene);
    }
    return cartesian;
    }

    这里首先尝试从3D模型或地形上拾取位置,如果未能拾取到模型或地形上的点,则尝试通过射线投射到椭球体表面。


    距离测量



    通过拾取点并记录每个点的坐标,计算相邻两个点的距离,并显示在Cesium场景中。通过ScreenSpaceEventHandler来捕获鼠标点击和移动事件。


    核心代码:


    drawLineMeasureGraphics(options = {}) {
    var positions = [];
    var _handlers = new Cesium.ScreenSpaceEventHandler(this._viewer.scene.canvas);
    _handlers.setInputAction(function (movement) {
    var cartesian = this.getCatesian3FromPX(movement.position);
    positions.push(cartesian);
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    _handlers.setInputAction(function (movement) {
    var cartesian = this.getCatesian3FromPX(movement.endPosition);
    positions.pop();
    positions.push(cartesian);
    }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

    _handlers.setInputAction(function () {
    _handlers.destroy();
    }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);
    }

    测距的基本思想是通过鼠标点击获取多个点的坐标,然后计算每两个相邻点的距离。


    面积测量



    面积测量通过计算多个点围成的多边形的面积,基于Cesium的PolygonHierarchy实现多边形绘制。


    核心代码:


    getPositionsArea(positions) {
    let ellipsoid = Cesium.Ellipsoid.WGS84;
    let area = 0;
    positions.push(positions[0]); // 闭合多边形
    for (let i = 1; i < positions.length; i++) {
    let p1 = ellipsoid.cartographicToCartesian(this.transformWGS84ToCartographic(positions[i - 1]));
    let p2 = ellipsoid.cartographicToCartesian(this.transformWGS84ToCartographic(positions[i]));
    area += p1.x * p2.y - p2.x * p1.y;
    }
    return Math.abs(area) / 2.0;
    }

    这里通过一个简单的多边形面积公式(叉乘)来计算笛卡尔坐标下的面积。


    三角测量


    三角测量通过拾取三个点,计算它们之间的直线距离水平距离以及高度差,构建一个三角形并在场景中显示这些信息。


    核心代码:


    drawTrianglesMeasureGraphics(options = {}) {
    var _positions = [];
    var _handler = new Cesium.ScreenSpaceEventHandler(this._viewer.scene.canvas);
    _handler.setInputAction(function (movement) {
    var position = this.getCatesian3FromPX(movement.position);
    _positions.push(position);
    if (_positions.length === 3) _handler.destroy();
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
    }

    该方法核心思想是获取三个点的坐标,通过高度差来构建水平线和垂线,然后显示相应的距离和高度差信息。


    使用


    封装好,之后,使用起来就非常简单了。


    import MeasureTool from "@/utils/cesiumCtrl/measure.js";
    const measure = new MeasureTool(viewer);
    **
    * 测距
    */
    const onLineMeasure = () => {
    measure.drawLineMeasureGraphics({
    clampToGround: true,
    callback: (e) => {
    console.log("----", e);
    },
    });
    };
    /**
    * 测面积
    */

    const onAreaMeasure = () => {
    measure.drawAreaMeasureGraphics({
    clampToGround: true,
    callback: () => {},
    });
    };
    /**
    * 三角量测
    */

    const onTrianglesMeasure = () => {
    measure.drawTrianglesMeasureGraphics({
    callback: () => {},
    });
    };

    最后


    这些测量工具都是依赖于Cesium提供的坐标转换、拾取以及事件处理机制,核心思路是通过ScreenSpaceEventHandler捕捉鼠标事件,获取坐标点,并通过几何算法计算距离、面积和高度。


    【完整源码地址】:github.com/tingyuxuan2…


    如果认为有帮助,希望可以给我们一个免费的star,激励我们持续开源更多代码。



    如果想系统学习Cesium,可以看下作者的Cesium系列教程《Cesium从入门到实战》,将Cesium的知识点进行串联,让不了解Cesium的小伙伴拥有一个完整的学习路线,学完后直接上手做项目,+作者:brown_7778(备注来意)了解课程细节。




    另外有需要进可视化&Webgis交流群可以加我:brown_7778(备注来意),也欢迎数字孪生可视化领域的交流合作。



    作者:攻城师不浪
    来源:juejin.cn/post/7424902468243669029
    收起阅读 »

    因离线地图引发的惨案

    web
    小王最近接到了一个重要的需求,要求在现有系统上实现离线地图功能,并根据不同的经纬度在地图上显示相应的标记(marks)。老板承诺,如果小王能够顺利完成这个任务,他的年终奖就有着落了。 为了不辜负老板的期望并确保自己能够拿到年终奖,小王开始了马不停蹄的开发工作。...
    继续阅读 »

    小王最近接到了一个重要的需求,要求在现有系统上实现离线地图功能,并根据不同的经纬度在地图上显示相应的标记(marks)。老板承诺,如果小王能够顺利完成这个任务,他的年终奖就有着落了。


    为了不辜负老板的期望并确保自己能够拿到年终奖,小王开始了马不停蹄的开发工作。他查阅了大量文档,研究了各种离线地图解决方案,并一一尝试。经过48小时的连续奋战,凭借着顽强的毅力和专业的技术能力,小王终于成功完成了需求。


    他在系统中集成了离线地图,并实现了根据经纬度显示不同区域标记的功能。每个标记都能准确地反映地理位置的信息,系统的用户体验得到了极大的提升。小王的心中充满了成就感和对未来奖励的期待。


    然而,天有不测风云。当小王准备向老板汇报工作成果时,却得知一个令人震惊的消息:老板因涉嫌某些违法行为(爬取不当得利)被逮捕了,公司也陷入了一片混乱。年终奖的承诺随之泡汤,甚至连公司未来的发展都蒙上了一层阴影。


    尽管如此,小王并没有因此而气馁。这次通过技术让老板成功的获得了编制,他深知只有不断技术的积累和经验的增长才能更好的保护老板。


    1.离线地图


    首先需要怎么做呢,你需要一个地图瓦片生成器(爬取谷歌、高德、百度等各个平台的地图瓦片,其实就是一张张缩小的图片,这里爬取可以用各种技术手段,但是违法偶,老板就是这么进去的),有个工具推荐:


    81F0D197D9F28F04820B441F560501D6.png


    链接:pan.baidu.com/s/1nflY8-KL…
    提取码:yqey
    下载解压打开下面的文件


    image.png


    打开了界面就长这样


    image.png
    可以调整瓦片样式


    image.png
    下载速度龟慢,建议开启代理,因为瓦片等级越高数量越多,需要下载的包越大,这里建议下载到11-16级别,根据自己需求
    下载完瓦片会保存在自己定义的文件夹,这里不建议放在c盘,会生成以下文件
    image.png
    使用一个文件服务去启动瓦片额静态服务,可以使用http-server
    安装http-server



    yarn add http-server -g



    cd到下载的mapabc目录下



    http-server roadmap



    image.png
    本地可以这么做上线后需要使用nginx代理这个静态服务


    server {
    listen 80;
    server_name yourdomain.com; # 替换为你的域名或服务器 IP

    root /var/www/myapp/public; # 设置根目录
    index index.html; # 设置默认文件

    location / {
    try_files $uri $uri/ =404;
    }

    # 配置访问 roadmap 目录下的地图瓦片
    location ^~/roloadmap/{
    alias /home/d5000/iot/web/roloadmap/;
    autoindex on; # 如果你想列出目录内容,可以开启这个选项
    }

    # 配置其他静态文件的访问(可选)
    location /static/ {
    alias /var/www/myapp/public/static/;
    }

    # 其他配置,例如反向代理到应用服务器等
    # location /api/ {
    # proxy_pass http://localhost:3000;
    # proxy_set_header Host $host;
    # proxy_set_header X-Real-IP $remote_addr;
    # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # proxy_set_header X-Forwarded-Proto $scheme;
    # }
    }

    配置完重启一下ngix即可
    对于如何将瓦片结合成一张地图并在vue2中使用,这里采用vueLeaflet,它是在leaflet基础上进行封装的


    这个插件需要安装一系列包



    yarn add leaflet vue2-leaflet leaflet.markercluster



    <l-tile-layer url="http://192.168.88.211:8080/{z}/{x}/{y}.png" ></l-tile-layer>
    这里的url就是上面启动的服务,包括端口和ip,要能访问到瓦片

    编写代码很简单


    <template>
    <div class="map">
    <div class="search">
    <map-search @input_val="inputVal" @select_val="selectVal" />
    </div>
    <div class="map_container">
    <l-map
    :zoom="zoom"
    :center="center"
    :max-bounds="bounds"
    :min-zoom="9"
    :max-zoom="15"
    :key="`${center[0]}-${center[1]}-${zoom}`"
    style="height: 100vh; width: 100%"
    >

    <l-tile-layer
    url="http://192.168.88.211:8080/{z}/{x}/{y}.png"
    >
    </l-tile-layer>
    <l-marker-cluster>
    <l-marker
    v-for="(marker, index) in markers"
    :key="index"
    :lat-lng="marker.latlng"
    :icon="customIcon"
    @click="handleMarkerClick(marker)"
    >

    <l-tooltip :offset="tooltipOffset">
    <div class="popup-content">
    <p>设备名称: {{ marker.regionName }}</p>
    <p>主线设备数量: {{ marker.endNum }}</p>
    <p>边缘设备数量: {{ marker.edgNum }}</p>
    </div>
    </l-tooltip>
    </l-marker>
    </l-marker-cluster>

    </l-map>
    </div>
    </div>

    </template>

    <script>
    import { LMap, LTileLayer, LMarker, LPopup, LTooltip, LMarkerCluster } from "vue2-leaflet";
    import mapSearch from "./search.vue";
    import "leaflet/dist/leaflet.css";
    import L from "leaflet";
    // import geojsonData from "./city.json"; // 确保这个路径是正确的
    import geoRegionData from "./equip.json"; // 确保这个路径是正确的

    // 移除默认的图标路径
    delete L.Icon.Default.prototype._getIconUrl;
    L.Icon.Default.mergeOptions({
    iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
    iconUrl: require("leaflet/dist/images/marker-icon.png"),
    shadowUrl: require("leaflet/dist/images/marker-shadow.png"),
    });

    export default {
    name: "Map",
    components: {
    LMap,
    LTileLayer,
    LMarker,
    LPopup,
    LTooltip,
    mapSearch,
    LMarkerCluster
    },
    data() {
    return {
    zoom: 9,
    center: [32.0617, 118.7636], // 江苏省的中心坐标
    bounds: [
    [30.7, 116.3],
    [35.1, 122.3],
    ], // 江苏省的地理边界
    markers: geoRegionData,
    customIcon: L.icon({
    iconUrl: require("./equip.png"), // 自定义图标的路径
    iconSize: [21, 27], // 图标大小
    iconAnchor: [12, 41], // 图标锚点
    popupAnchor: [1, -34], // 弹出框相对于图标的锚点
    shadowSize: [41, 41], // 阴影大小(如果有)
    shadowAnchor: [12, 41], // 阴影锚点(如果有)
    }),
    tooltipOffset: L.point(10, 10), // 调整偏移值
    };
    },
    methods: {
    inputVal(val) {
    // 处理输入值变化
    this.center = val;
    this.zoom = 15;
    },
    selectVal(val) {
    // 处理选择值变化
    this.center = val;
    this.zoom = 15;
    },
    handleMarkerClick(marker) {
    this.center = marker.latlng;
    this.zoom = 15;
    },
    },
    };
    </script>


    <style scoped lang="less">
    @import "~leaflet/dist/leaflet.css";
    @import "~leaflet.markercluster/dist/MarkerCluster.css";
    @import "~leaflet.markercluster/dist/MarkerCluster.Default.css";
    .map {
    width: 100%;
    height: 100%;
    position: relative;

    .search {
    position: absolute;
    z-index: 1000;
    left: 20px;
    top: 10px;
    padding: 10px; /* 设置内边距 */
    }
    }

    .popup-content {
    font-family: Arial, sans-serif;
    text-align: left;
    }

    .popup-content h3 {
    margin: 0;
    font-size: 16px;
    font-weight: bold;
    }

    .popup-content p {
    margin: 4px 0;
    font-size: 14px;
    }

    /deep/.leaflet-control {
    display: none !important; /* 隐藏默认控件 */
    }
    /deep/.leaflet-control-zoom {
    display: none !important; /* 隐藏默认控件 */
    }
    </style>



    这里使用遇到一个坑,需要切换地图中心center,需要给l-map绑定一个key="${center[0]}-${center[1]}-${zoom}",不然每次切换第一次会失败,第二次才能成功


    可以给行政区添加范围,这里需要geojson数据,可以在阿里云数据平台上获取
    通过组件加载即可


    <l-geo-json :geojson="geojson"></l-geo-json>

    效果如下


    image.png
    以上方法,不建议使用,如果是商业使用,不建议使用,不然容易被告侵权,最好能是使用官方合法的地图api,例如谷歌、百度、腾讯、高德,这里我使用高德api给兄弟们们展示一下


    2.高德在线地图


    2.1首先需要在高德的开放平台申请一个账号


    创建一个项目,如下,我们需要使用到这个key和密钥,这里如果是公司使用可以使用公司的信息注册一个账号,公司的账号权限高于个人,具体区别如下参看官网
    developer.amap.com/api/faq/acc…


    image.png


    2.2如何在框架中使用


    image.png


    image.png
    因为不想在创建一个react应用了,这里还是用vue2演示,vue2需要下载一个高德提供的npm包



    yarn add @amap/amap-jsapi-loader



    编写代码



    <template>
    <div class="map">
    <div class="serach">
    <map-search @share_id="shareId" @input_val="inputVal" @select_val="selectVal" @change_theme="changeTheme" />
    </div>
    <div class="map_container" id="container"></div>
    </div>

    </template>
    <script>
    import AMapLoader from "@amap/amap-jsapi-loader";
    import mapSearch from "./search.vue";
    import cityJson from "../../assets/area.json";
    window._AMapSecurityConfig = {
    //这里是高德开放平台创建项目时生成的密钥
    securityJsCode: "xxxx",
    };
    export default {
    name: "mapContainer",
    components: { mapSearch },
    mixins: [],
    props: {},
    data() {
    return {
    map: null,
    autoOptions: {
    input: "",
    },
    auto: null,
    AMap: null,
    placeSearch: null,
    searchPlaceInput: "",
    polygons: [],
    positions: [],
    //地图样式配置
    inintMapStyleConfig: {
    //设置地图容器id
    viewMode: "3D", //是否为3D地图模式
    zoom: 15, //初始化地图级别
    rotateEnable: true, //是否开启地图旋转交互 鼠标右键 + 鼠标画圈移动 或 键盘Ctrl + 鼠标左键画圈移动
    pitchEnable: true, //是否开启地图倾斜交互 鼠标右键 + 鼠标上下移动或键盘Ctrl + 鼠标左键上下移动
    mapStyle: "amap://styles/whitesmoke", //设置地图的显示样式
    center: [118.796877, 32.060255], //初始化地图中心点位置
    },
    //地图配置
    mapConfig: {
    key: "xxxxx", // 申请好的Web端开发者Key,首次调用 load 时必填
    version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
    plugins: [
    "AMap.AutoComplete",
    "AMap.PlaceSearch",
    "AMap.Geocoder",
    "AMap.DistrictSearch",
    ], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
    },
    // 实例化DistrictSearch配置
    districtSearchOpt: {
    subdistrict: 1, //获取边界不需要返回下级行政区
    extensions: "all", //返回行政区边界坐标组等具体信息
    },
    //这里是mark中的设置
    icon: {
    type: "image",
    image: require("../../assets/equip.png"),
    size: [15, 21],
    anchor: "bottom-center",
    fitZoom: [14, 20], // Adjust the fitZoom range for scaling
    scaleFactor: 2, // Zoom scale factor
    maxScale: 2, // Maximum scale
    minScale: 1 // Minimum scale
    }
    };
    },


    created() {
    this.initMap();
    },

    methods: {
    //初始化地图
    async initMap() {
    this.AMap = await AMapLoader.load(this.mapConfig);
    this.map = new AMap.Map("container", this.inintMapStyleConfig);
    //根据地理位置查询经纬度
    this.positions = await Promise.all(cityJson.map(async item => {
    try {
    const dot = await this.queryGeocodes(item.cityName, this.AMap);
    return {
    ...item,
    dot: dot
    };
    } catch (error) {

    }
    }));

    //poi查询
    this.addMarker();
    //显示安徽省的区域
    this.drawBounds("安徽省");

    },

    //查询地理位置
    async queryGeocodes(newValue, AMap) {
    return new Promise((resolve, reject) => {
    //加载行政区划插件
    const geocoder = new AMap.Geocoder({
    // 指定返回详细地址信息,默认值为true
    extensions: 'all'
    });
    // 使用地址进行地理编码
    geocoder.getLocation(newValue, (status, result) => {
    if (status === 'complete' && result.geocodes.length) {
    const geocode = result.geocodes[0];
    const latitude = geocode.location.lat;
    const longitude = geocode.location.lng;
    resolve([longitude, latitude]);
    } else {
    reject('无法获取该地址的经纬度');
    }
    });
    });
    },
    //结合输入提示进行POI搜索
    shareId(val) {
    this.autoOptions.input = val;
    },
    //根据设备搜索
    inputVal(val) {
    if (val?.length === 0) {
    //poi查询
    this.addMarker();
    //显示安徽省
    this.drawBounds("安徽省");
    return;
    }
    var position = val
    this.icon.size = [12, 18]
    this.map.setCenter(position)
    this.queryPoI()
    this.map.setZoom(12, true, 1);
    },
    //修改主题
    changeTheme(val) {
    const styleName = "amap://styles/" + val;
    this.map.setMapStyle(styleName);
    },
    //区域搜索
    selectVal(val) {
    if (val && val.length > 0) {
    let vals = val[val?.length - 1];
    vals = vals.replace(/\s+/g, '');
    this.queryPoI()
    this.placeSearch.search(vals);
    this.drawBounds(vals);
    this.map.setZoom(15, true, 1);
    }
    },

    //添加marker
    addMarker() {
    const icon = this.icon
    let layer = new this.AMap.LabelsLayer({
    zooms: [3, 20],
    zIndex: 1000,
    collision: false,
    });
    // 将图层添加到地图
    this.map.add(layer);
    // 普通点
    let markers = [];
    this.positions.forEach((item) => {
    const content = `
    <div class="custom-info-window">
    <div class="info-window-header"><b>${item.cityName}</b></div>
    <div class="info-window-body">
    <div>边设备数 : ${item.edgNum} 台</div>
    <div>端设备数 : ${item.endNum} 台</div>
    </div>
    </div>
    `
    ;
    let labelMarker = new AMap.LabelMarker({
    position: item.dot,
    icon: icon,
    rank: 1, //避让优先级
    });
    const infoWindow = new AMap.InfoWindow({
    content: content, //传入字符串拼接的 DOM 元素
    anchor: "top-left",
    });
    labelMarker.on('mouseover', () => {
    infoWindow.open(this.map, item.dot);
    });

    labelMarker.on('mouseout', () => {
    infoWindow.close();
    });
    labelMarker.on('click', () => {
    this.map.setCenter(item.dot)
    this.queryPoI()
    this.map.setZoom(15, true, 1);
    })
    markers.push(labelMarker);
    });
    // 一次性将海量点添加到图层
    layer.add(markers);
    },

    //POI查询
    queryPoI() {
    this.auto = new this.AMap.AutoComplete(this.autoOptions);
    this.placeSearch = new this.AMap.PlaceSearch({
    map: this.map,
    }); //构造地点查询类
    this.auto.on("select", this.select);

    this.addMarker();
    },
    //选择数据
    select(e) {
    this.placeSearch.setCity(e.poi.adcode);
    this.placeSearch.search(e.poi.name); //关键字查询查询
    this.map.setZoom(15, true, 1);
    },

    // 行政区边界绘制
    drawBounds(newValue) {
    //加载行政区划插件
    if (!this.district) {
    this.map.plugin(["AMap.DistrictSearch"], () => {
    this.district = new AMap.DistrictSearch(this.districtSearchOpt);
    });
    }
    //行政区查询
    this.district.search(newValue, (_status, result) => {
    if (Object.keys(result).length === 0) {
    this.$message.warning("未查询到该地区数据");
    return
    }
    if (this.polygons != null) {
    this.map.remove(this.polygons); //清除上次结果
    this.polygons = [];
    }
    //绘制行政区划
    result?.districtList[0]?.boundaries?.length > 0 &&
    result.districtList[0].boundaries.forEach((item) => {
    let polygon = new AMap.Polygon({
    strokeWeight: 1,
    path: item,
    fillOpacity: 0.1,
    fillColor: "#22886f",
    strokeColor: "#22886f",
    });
    this.polygons.push(polygon);

    });
    this.map.add(this.polygons);
    this.map.setFitView(this.polygons); //视口自适应

    });
    },
    },
    };
    </script>

    <style lang="less" scoped>
    .map {
    width: 100%;
    height: 100%;

    position: relative;

    .map_container {
    width: 100%;
    height: 100%;
    }

    .serach {
    position: absolute;
    z-index: 33;
    left: 20px;
    top: 10px;
    }
    }
    </style>

    <style>
    //去除高德的logo
    .amap-logo {
    right: 0 !important;
    left: auto !important;
    display: none !important;
    }

    .amap-copyright {
    right: 70px !important;
    left: auto !important;
    opacity: 0 !important;
    }

    /* 自定义 infoWindow 样式 */
    .custom-info-window {
    font-family: Arial, sans-serif;
    padding: 10px;
    border-radius: 8px;
    background-color: #ffffff;
    max-width: 250px;
    }
    </style>


    在子组件中构建查询


    <template>
    <div class="box">
    <div class="input_area">
    <el-input placeholder="请输入设备名称" :id="search_id" v-model="input" size="mini" class="input_item" />
    <img src="../../assets/input.png" alt="" class="img_logo" />
    <span class="el-icon-search search" @click="searchMap"></span>
    </div>
    <div class="select_area">
    <el-cascader :options="options" size="mini" placeholder="选择地域查询" :show-all-levels="false" :props="cityProps"
    clearable v-model="cityVal" @change="selectCity">
    </el-cascader>
    </div>
    <div class="date_area">
    <el-select v-model="themeValue" placeholder="请选择地图主题" size="mini" @change="changeTheme">
    <el-option v-for="item in themeOptions" :key="item.value" :label="item.label" :value="item.value">
    </el-option>
    </el-select>
    </div>
    </div>

    </template>
    <script>
    import cityRegionData from "../../assets/area"
    import cityJson from "../../assets/city.json";
    export default {
    name: "search",
    components: {},
    mixins: [],
    props: {},
    data() {
    return {
    search_id: "searchId",
    input: "",
    options: cityRegionData,
    cityProps: {
    children: "children",
    label: "business_name",
    value: "business_name",
    checkStrictly: true
    },
    cityVal: "",
    themeOptions: [
    { label: "标准", value: "normal" },
    { label: "幻影黑", value: "dark" },
    { label: "月光银", value: "light" },
    { label: "远山黛", value: "whitesmoke" },
    { label: "草色青", value: "fresh" },
    { label: "雅士灰", value: "grey" },
    { label: "涂鸦", value: "graffiti" },
    { label: "马卡龙", value: "macaron" },
    { label: "靛青蓝", value: "blue" },
    { label: "极夜蓝", value: "darkblue" },
    { label: "酱籽", value: "wine" },
    ],
    themeValue: ""
    };
    },
    computed: {},
    watch: {},
    mounted() {
    this.sendId();
    },
    methods: {
    sendId() {
    this.$emit("share_id", this.search_id);
    },
    searchMap() {
    console.log(this.input,'ssss');
    if (!this.input) {
    this.$emit("input_val", []);
    return
    }
    let val = cityJson.find(item => item.equipName === this.input)
    if (val) {
    this.$emit("input_val", val.dot);
    return
    }

    this.$message.warning("未查询到该设备,请输入正确的设备名称");
    },
    selectCity() {
    this.$emit("select_val", this.cityVal);
    },
    changeTheme(val) {
    this.$emit("change_theme", val);
    }

    },
    };
    </script>

    <style lang="less" scoped>
    .box {
    display: flex;

    .input_area {
    position: relative;
    width: 170px;
    height: 50px;
    display: flex;
    align-items: center;

    .input_item {
    width: 100%;

    /deep/ .el-input__inner {
    padding-left: 30px !important;
    }
    }

    .img_logo {
    position: absolute;
    left: 5px;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    height: 20px;
    margin-right: 10px;
    }

    span {
    position: absolute;
    right: 10px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 16px;
    color: #ccc;
    cursor: pointer;
    }
    }

    .select_area {
    width: 150px;
    display: flex;
    align-items: center;
    height: 50px;
    margin-left: 10px;
    }

    .date_area {
    width: 150px;
    display: flex;
    align-items: center;
    height: 50px;
    margin-left: 10px;
    }
    }
    </style>


    效果如下


    image.png


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

    用Three.js搞个炫酷雷达扩散和扫描特效

    web
    1.画点建筑模型 添加光照,开启阴影 //开启renderer阴影 this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;...
    继续阅读 »

    1.画点建筑模型


    添加光照,开启阴影


    //开启renderer阴影
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;

    //设置环境光
    const light = new THREE.AmbientLight(0xffffff, 0.6); // soft white light
    this.scene.add(light);

    //夜晚天空蓝色,假设成蓝色的平行光
    const dirLight = new THREE.DirectionalLight(0x0000ff, 3);
    dirLight.position.set(50, 50, 50);
    this.scene.add(dirLight);

    平行光设置阴影


    //开启阴影
    dirLight.castShadow = true;
    //阴影相机范围
    dirLight.shadow.camera.top = 100;
    dirLight.shadow.camera.bottom = -100;
    dirLight.shadow.camera.left = -100;
    dirLight.shadow.camera.right = 100;
    //阴影影相机远近
    dirLight.shadow.camera.near = 1;
    dirLight.shadow.camera.far = 200;
    //阴影贴图大小
    dirLight.shadow.mapSize.set(1024, 1024);


    • 平行光的阴影相机跟正交相机一样,因为平行光的光线是平行的,就跟视线是平行一样,切割出合适的阴影视角范围,用于计算阴影。

    • shadow.mapSize设置阴影贴图的宽度和高度,值越高,阴影的质量越好,但要花费计算时间更多。


    增加建筑


    //添加一个平面
    const pg = new THREE.PlaneGeometry(100, 100);
    //一定要用受光材质才有阴影效果
    const pm = new THREE.MeshStandardMaterial({
    color: new THREE.Color('gray'),
    transparent: true,//开启透明
    side: THREE.FrontSide//只有渲染前面
    });
    const plane = new THREE.Mesh(pg, pm);
    plane.rotateX(-Math.PI * 0.5);
    plane.receiveShadow = true;//平面接收阴影
    this.scene.add(plane);

    //随机生成建筑
    this.geometries = [];
    const helper = new THREE.Object3D();
    for (let i = 0; i < 100; i++) {
    const h = Math.round(Math.random() * 15) + 5;
    const x = Math.round(Math.random() * 50);
    const y = Math.round(Math.random() * 50);
    helper.position.set((x % 2 ? -1 : 1) * x, h * 0.5, (y % 2 ? -1 : 1) * y);
    const geometry = new THREE.BoxGeometry(5, h, 5);
    helper.updateWorldMatrix(true, false);
    geometry.applyMatrix4(helper.matrixWorld);
    this.geometries.push(geometry);
    }
    //长方体合成一个形状
    const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
    //建筑贴图
    const texture = new THREE.TextureLoader().load('assets/image.jpg');
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    const material = new THREE.MeshStandardMaterial({ map: texture,transparent: true });
    const cube = new THREE.Mesh(mergedGeometry, material);
    //形状产生阴影
    cube.castShadow = true;
    //形状接收阴影
    cube.receiveShadow = true;
    this.scene.add(cube);

    image.png


    效果就是很多高楼大厦的样子,为什么楼顶有窗?别在意这些细节,有的人就喜欢开天窗呢~


    2.搞个雷达扩散和扫描特效


    改变建筑材质shader,计算建筑的俯视uv


    material.onBeforeCompile = (shader, render) => {
    this.shaders.push(shader);
    //范围大小
    shader.uniforms.uSize = { value: 50 };
    shader.uniforms.uTime = { value: 0 };
    //修改顶点着色器
    shader.vertexShader = shader.vertexShader.replace(
    'void main() {',
    ` uniform float uSize;
    varying vec2 vUv;
    void main() {`

    );
    shader.vertexShader = shader.vertexShader.replace(
    '#include <fog_vertex>',
    `#include <fog_vertex>
    //计算相对于原点的俯视uv
    vUv=position.xz/uSize;`

    );
    //修改片元着色器
    shader.fragmentShader = shader.fragmentShader.replace(
    'void main() {',
    `varying vec2 vUv;
    uniform float uTime;
    void main() {`

    );
    shader.fragmentShader = shader.fragmentShader.replace(
    '#include <dithering_fragment>',
    `#include <dithering_fragment>
    //渐变颜色叠加
    gl_FragColor.rgb=gl_FragColor.rgb+mix(vec3(0,0.5,0.5),vec3(1,1,0),vUv.y);`

    );
    };

    image.png


    然后你将同样的onBeforeCompile函数赋值给平面的时候,没有对应的效果。


    因为平面没有z,只有xy,而且经过了-90度旋转后,坐标位置也要对应反转,由此可以得出平面的uv计算公式


    vUv=vec2(position.x,-position.y)/uSize;

    image.png


    至此,建筑和平面的俯视uv一致了。


    雷达扩散特效



    • 雷达扩散就是一段渐变的环,随着时间扩大。

    • 顶点着色器不变,改一下片元着色器,增加扩散环颜色uColor,对应shader.uniforms也要添加


    shader.uniforms.uColor = { value: new THREE.Color('#00FFFF') };

    const fragmentShader1 = `varying vec2 vUv;
    uniform float uTime;
    uniform vec3 uColor;
    uniform float uSize;
    void main() {`
    ;
    const fragmentShader2 = `#include <dithering_fragment>
    //计算与中心的距离
    float d=length(vUv);
    if(d >= uTime&&d<=uTime+ 0.1) {
    //扩散圈
    gl_FragColor.rgb = gl_FragColor.rgb+mix(uColor,gl_FragColor.rgb,1.0-(d-uTime)*10.0 )*0.5 ;
    }`
    ;

    shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragmentShader1);
    shader.fragmentShader = shader.fragmentShader.replace(
    '#include <dithering_fragment>',
    fragmentShader2);

    //改变shader的时间变量,动起来
    animateAction() {
    if (this.shaders?.length) {
    this.shaders.forEach((shader) => {
    shader.uniforms.uTime.value += 0.005;
    if (shader.uniforms.uTime.value >= 1) {
    shader.uniforms.uTime.value = 0;
    }
    });
    }
    }

    20240322_224153.gif


    噔噔噔噔,完成啦!是立体化的雷达扩散,看起来很酷的样子。


    雷达扫描特效


    跟上面雷达扩散差不多,只要修改一下片元着色器



      const fragmentShader1 = `varying vec2 vUv;
    uniform float uTime;
    uniform vec3 uColor;
    uniform float uSize;
    //旋转角度矩阵
    mat2 rotate2d(float angle)
    {
    return mat2(cos(angle), - sin(angle),
    sin(angle), cos(angle));
    }
    //雷达扫描渐变扇形
    float vertical_line(in vec2 uv)
    {
    if (uv.y > 0.0 && length(uv) < 1.2)
    {
    float theta = mod(180.0 * atan(uv.y, uv.x)/3.14, 360.0);
    float gradient = clamp(1.0-theta/90.0,0.0,1.0);
    return 0.5 * gradient;
    }
    return 0.0;
    }
    void main() {`
    ;


    const fragmentShader2 = `#include <dithering_fragment>
    mat2 rotation_matrix = rotate2d(- uTime*PI*2.0);
    //将雷达扫描扇形渐变混合到颜色中
    gl_FragColor.rgb= mix( gl_FragColor.rgb, uColor, vertical_line(rotation_matrix * vUv)); `
    ;

    20240322_232006.gif


    GitHub地址


    https://github.com/xiaolidan00/my-earth


    作者:敲敲敲敲暴你脑袋
    来源:juejin.cn/post/7349837128508964873
    收起阅读 »

    粒子特效particles.js

    web
    效果图 版本:"particles.js": "^2.0.0" npm i particles.js Vue2版本 组件代码:src/App.vue <template> <div class="particles-js-box"&g...
    继续阅读 »

    效果图


    效果图


    版本:"particles.js": "^2.0.0"


    npm i particles.js

    Vue2版本


    组件代码:src/App.vue


    <template>
    <div class="particles-js-box">
    <div id="particles-js"></div>
    </div>
    </template>

    <script>
    import particlesJs from "particles.js";
    import particlesConfig from "./assets/particles.json";
    export default {
    data() {
    return {};
    },
    mounted() {
    this.init();
    },
    methods: {
    init() {
    particlesJS("particles-js", particlesConfig);
    document.body.style.overflow = "hidden";
    },
    },
    };
    </script>

    <style scoped>
    .particles-js-box {
    position: fixed;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    z-index: -1;
    }
    #particles-js {
    background-color: #18688d;
    width: 100%;
    height: 100%;
    }
    </style>

    代码里的json数据:


    目录:src/assets/particles.json


    {
    "particles": {
    "number": {
    "value": 60,
    "density": {
    "enable": true,
    "value_area": 800
    }
    },
    "color": {
    "value": "#ddd"
    },
    "shape": {
    "type": "circle",
    "stroke": {
    "width": 0,
    "color": "#000000"
    },
    "polygon": {
    "nb_sides": 5
    },
    "image": {
    "src": "img/github.svg",
    "width": 100,
    "height": 100
    }
    },
    "opacity": {
    "value": 0.5,
    "random": false,
    "anim": {
    "enable": false,
    "speed": 1,
    "opacity_min": 0.1,
    "sync": false
    }
    },
    "size": {
    "value": 3,
    "random": true,
    "anim": {
    "enable": false,
    "speed": 40,
    "size_min": 0.1,
    "sync": false
    }
    },
    "line_linked": {
    "enable": true,
    "distance": 150,
    "color": "#ffffff",
    "opacity": 0.4,
    "width": 1
    },
    "move": {
    "enable": true,
    "speed": 4,
    "direction": "none",
    "random": false,
    "straight": false,
    "out_mode": "out",
    "bounce": false,
    "attract": {
    "enable": false,
    "rotateX": 100,
    "rotateY": 1200
    }
    }
    },
    "interactivity": {
    "detect_on": "Window",
    "events": {
    "onhover": {
    "enable": true,
    "mode": "grab"
    },
    "onclick": {
    "enable": true,
    "mode": "push"
    },
    "resize": true
    },
    "modes": {
    "grab": {
    "distance": 140,
    "line_linked": {
    "opacity": 1
    }
    },
    "bubble": {
    "distance": 400,
    "size": 40,
    "duration": 2,
    "opacity": 8,
    "speed": 3
    },
    "repulse": {
    "distance": 200,
    "duration": 0.4
    },
    "push": {
    "particles_nb": 4
    },
    "remove": {
    "particles_nb": 2
    }
    }
    },
    "retina_detect": true
    }

    Vue3版本


    {
    "name": "vue3-test",
    "version": "0.0.0",
    "private": true,
    "type": "module",
    "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint . --fix",
    "format": "prettier --write src/"
    },
    "dependencies": {
    "particles.js": "^2.0.0",
    "vue": "^3.5.13"
    },
    "devDependencies": {
    "@eslint/js": "^9.14.0",
    "@vitejs/plugin-vue": "^5.2.1",
    "@vue/eslint-config-prettier": "^10.1.0",
    "eslint": "^9.14.0",
    "eslint-plugin-vue": "^9.30.0",
    "prettier": "^3.3.3",
    "sass-embedded": "^1.83.0",
    "vite": "^6.0.1",
    "vite-plugin-vue-devtools": "^7.6.5"
    }
    }

    需要修改 /node_modules/particles.js/particles.js 的代码





    修改此 2 处,


    我是把它拷贝到 src 目录下


    组件使用:跟 vue2 一样,就是上面第8行引入不一样


    import particlesJs from "@/particles.js";

    作者:辉1106
    来源:juejin.cn/post/7452931747785883684
    收起阅读 »

    为什么Rust 是 web 开发的理想选择

    web
    为什么Rust 是 web 开发的理想选择 Rust 经常被视为仅仅是一种系统编程语言,但实际上它是一种多用途的通用语言。像 Tauri(用于桌面应用)、Leptos(用于前端开发)和 Axum(用于后端开发)这样的项目表明 Rust 的用途远不止于系统编程。...
    继续阅读 »

    为什么Rust 是 web 开发的理想选择


    Rust 经常被视为仅仅是一种系统编程语言,但实际上它是一种多用途的通用语言。像 Tauri(用于桌面应用)、Leptos(用于前端开发)和 Axum(用于后端开发)这样的项目表明 Rust 的用途远不止于系统编程。


    当我开始学习 Rust 时,我构建了一个网页应用来练习。因为我主要是一名后端工程师,这是我最为熟悉的领域。很快我就意识到 Rust 非常适合做网页开发。它的特性让我有信心构建出可靠的应用程序。让我来解释一下为什么 Rust 是网页编程的理想选择。


    错误处理


    一段时间以前,我开始学习 Python,那时我被机器学习的热潮所吸引,甚至是在大型语言模型(LLM)热潮之前。我需要让机器学习模型可以被使用,因此我选择了编写一个 Django REST API。在 Django 中获取请求体,你可能会这样写代码:


    class User(APIView):
    def post(self, request):
    body = request.data

    这段代码大多数时候都能正常工作。然而,当我意外地发送了一个格式不正确的请求体时,它就不再起作用了。访问数据时抛出了异常,导致返回了 500 状态码的响应。我没有意识到这种访问可能会抛出异常,并且也没有明确的提示。


    Rust 通过不抛出异常而是以 Result 形式返回错误作为值来处理这种情况。Result 同时包含值和错误,你必须处理错误才能访问该值。


    let body: RequestBody = serde_json::from_slice(&requestData)?;

    问号 (?) 表示你想在调用函数中处理错误,将错误向上一级传播。


    我认为任何将错误作为值来处理的语言都是正确处理错误的方式。这种方法允许你编写代码时避免出现意外的情况,就像 Python 示例中的那样。


    默认不可变性


    最近,我的一位同事在我们的一个开源项目上工作,他需要替换一个客户端库为另一个。这是他使用的代码:


    newClient(
    WithHTTPClient(httpClient), // &http.Client{}
    WithEndpoint(config.ApiBasePath),
    )

    突然间,集成测试开始抛出竞态条件错误,他搞不清楚为什么会这样。他向我求助,我们一起追踪问题回到了这行代码。我们在其他客户端之间共享了这个HTTP客户端,这导致了错误的发生。多个协程在读取客户端,而 WithHttpClient 函数修改了客户端的状态。在同一资源上同时有读线程和写线程会导致未定义的行为或在 Go 语言中引发恐慌。


    这又是一个令人不悦的意外。而在 Rust 中,所有变量默认是不可变的。如果你想修改一个变量,你需要显式地声明它,使用 mut 关键字。这有助于 API 客户端理解发生了什么,并避免了意外的修改。


    fn with_httpclient(client: &mut reqwest::Client) {}


    在像 Java 和 Python 这样的语言中,你们有注解;而在 Rust 中,我们使用宏。注解可以在某些环境下如 Spring 中带来优雅的代码,其中大量的幕后工作是通过反射完成的。虽然 Rust 的宏提供的“魔法”较少,但也同样能产生更清晰的代码。这里有一个 Rust 宏的例子:


    sqlx::query_as!(Student, "DELETE FROM student WHERE id = ? RETURNING *", id)

    Rust 中的宏会在后台生成代码,编译器在构建过程中会检查这些代码的正确性。通过宏,你甚至可以在编译时扩展编译器检查并验证 SQL 查询,方法是在编译期间生成运行查询的真实数据库上的代码。


    这种能够在编译时检查代码正确性的能力开辟了新的可能性,特别是在 web 开发中,我们经常编写原始的数据库语句或 HTML 和 CSS 代码。它帮助我们写出更少 bug 的代码。


    这里提到的宏被称为声明式宏。Rust 还有过程式宏,它们更类似于其他语言中的注解。


    #[instrument(name = "UserRepository::begin")]
    pub async fn begin(&self) {}

    核心思想保持不变:在后台生成代码,并在方法调用前后执行一些逻辑,从而确保代码更加健壮和易于维护。


    Chaining


    来看看这段在 Rust 中优雅的代码:


    let key_value = request.int0_inner()
    .key_value
    .ok_or_else(|| ServerError::InvalidArgument("key_value must be set".to_string()))?;

    与这种更为冗长的方法相比:


    Optional<KeyValue> keyValueOpt = request.getInner().getKeyValue();
    if (!keyValueOpt.isPresent()) {
    throw new IllegalArgumentException("key_value must be set");
    }
    KeyValue keyValue = keyValueOpt.get();

    在 Rust 中,我们可以将操作链接在一起,从而得到简洁且易读的代码。但是,为了实现这种流畅的语法,我们通常需要实现诸如 From 这样的特质。


    功能性技术大佬们可能会认识并欣赏这种方法,他们有这样的见解是有道理的。我认为任何允许混合函数式和过程式编程的语言都是走在正确的道路上。它为开发者提供了灵活性,让他们可以选择最适合其特定应用场景的方式。


    线程安全


    这里有没有人曾经因为竞态条件而在生产环境中触发过程序崩溃?我羞愧地承认,我有过这样的经历。是的,这是一个技能问题。当你在启动多个线程的同时对同一内存地址进行修改和读取时,很难不去注意到这个问题。但让我们考虑这样一个例子:


    type repo struct {
    m map[int]int
    }

    func (r *repo) Create(i int) {
    r.m[i] = i
    }

    type Server struct {
    repo *repo
    }

    func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
    s.repo.Create(1)
    }

    没有显式启动任何线程,乍一看,一切似乎都很好。然而实际上,HTTP 服务器是在多个线程上运行的,这些线程被抽象隐藏了起来。在 web 开发中,这种抽象可能会掩盖与多线程相关的潜在问题。现在,让我们用 Rust 实现相同的功能:


    struct repo {
    m: std::collections::HashMap<i8, i8>
    }

    #[post("/maps")]
    async fn crate_entry(r: web::Data<repo>) -> HttpResponse {
    r.m.insert(1, 2);
    HttpResponse::Ok().json(MessageResponse {
    message: "good".to_string(),
    })
    }

    当我们尝试编译这个程序时,Rust 编译器将会抛出一个错误:


    https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*mSeO7GROEzcXsergQWhP0w.png


    error[E0596]: cannot 
    borrow data in an
    `Arc` as mutable
    --> src\main.rs:117:5
    |
    117 | r.m.insert(1, 2);
    | ^^^ cannot borrow as mutable
    |
    = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Arc<repo>`

    很多人说 Rust 的错误信息通常是很有帮助的,这通常是正确的。然而,在这种情况下,错误信息可能会让人感到困惑并且不是立即就能明白。幸运的是,如果知道如何解决,修复方法很简单:只需要添加一个小的互斥锁:


    struct repo {
    m: HashMap<i8, i8>
    }

    #[post("/maps")]
    async fn create_entry(r: web::Data<Mutex<repo>>) -> HttpResponse {
    let mut map = r.lock().await();
    map.m.insert(1, 2);
    HttpResponse::Ok().json(MessageResponse {
    message: "good".to_string(),
    })
    }

    确实非常美妙,编译器能够帮助我们避免这些问题,让我们的代码保持安全和可靠。


    空指针解引用


    大多数人认为这个问题只存在于 C 语言中,但你也会在像 Java 或 Go 这样的语言中遇到它。这里是一个典型问题的例子:


    type valueObject struct {
    value *int
    }

    func getValue(vo *valueObject) int {
    return *vo.value
    }

    你可能会说,“在使用值之前检查它是否为 nil 就好了。”这是 Go 语言中最大的陷阱之一 —— 它的指针机制。有时候我们会优化内存分配,有时候我们使用指针来表示可选值。


    空指针解引用的风险在处理接口时尤其明显。


    type Repository interface {
    Get() int
    }

    func getValue(r Repository) int {
    return r.Get()
    }

    func main() {
    getValue(nil)
    }

    在许多语言中,将空值作为接口的有效选项传递是可以的。虽然代码审查通常会发现这类问题,但我还是见过一些空接口进入开发阶段的情况。在 Rust 中,这类问题是不可能发生的,这是对我们错误的另一层保护:


    trait Repository {
    fn get(&self) -> i32;
    }

    fn get_value(r: impl Repository) -> i32 {
    r.get()
    }

    fn main() {
    get_value(std::ptr::null());
    }

    Not to mention that it does not compile.


    更不用说这段代码根本无法编译。


    我承认,我是端口和适配器模式的大粉丝,这些模式包括了一些抽象概念。根据复杂度的不同,这些抽象可能是必要的,以便在你的应用程序中创建清晰的边界,从长远来看提高单元测试性和可维护性。批评者的一个论点是性能会下降,因为通常需要动态调度,因为在编译时无法确定具体的接口实现。让我们来看一个 Java 的例子:


    @Service
    public class StudentServiceImpl implements StudentService {

    private final StudentRepository studentRepository;

    @Autowired
    public StudentServiceImpl(StudentRepository studentRepository) {
    this.studentRepository = studentRepository;
    }
    }

    Spring 为我们处理了很多幕后的事务。其中一个特性就是使用 @Autowired 注解来进行依赖注入。当应用程序启动时,Spring 会进行类路径扫描和反射。然而,这种便利性却伴随着性能成本。


    在 Rust 中,我们可以创建这些清晰的抽象而不付出性能代价,这得益于所谓的零成本抽象:


    struct ServiceImpl<T: Repository> {
    repo: T,
    }

    trait Service{}

    fn new_service<T: Repository>(repo: T) -> impl Service {
    ServiceImpl { repo: repo }
    }

    这些抽象在编译时就被处理好,确保在运行时不会有任何性能开销。这使我们能够在不牺牲性能的情况下保持代码的整洁和高效。


    数据转换


    在企业级应用中,我们经常使用端口和适配器模式来处理复杂的业务需求。这种模式涉及将数据转换成不同层次所需的表示形式。我们可能通过异步通信接收到用户数据作为事件,或者通过同步通信接收到用户数据作为请求。然后,这些数据被转换成领域模型并通过服务和适配器层进行传递。


    这就提出了一个问题:数据转换的逻辑应该放在哪里?应该放在领域包中吗?还是放在数据映射所在的包中?我们应该如何调用方法来转换数据?这些问题常常导致整个代码库中出现不一致性。


    Rust 在提供一种清晰的方式来处理数据转换方面表现出色,使用 From 特质。如果我们需要将数据从适配器转换到领域,我们只需在适配器中实现 From 特质:


    impl From<UserRequest> for domain::DomainUser {
    fn from(user: UserRequest) -> Self {
    domain::DomainUser {}
    }
    }

    impl From<domain::DomainUser> for UserResponse {
    fn from(user: domain::DomainUser) -> Self {
    UserResponse {}
    }
    }

    fn create_user(user: UserRequest) -> Result<()> {
    let domain_user = domain::upsert_user(user.int0());
    send_response(domain_user.int0())?;
    Ok(())
    }

    通过在需要的地方实现 From 特质,Rust 提供了一种一致且直接的方式来处理数据转换,减少了不一致性,并使代码库更加易于维护。


    性能


    当然,Rust 很快这一点毋庸置疑,但它实际上给我们带来了哪些好处呢?我记得第一次将我的 Django 应用部署到 Kubernetes 上,并使用 kubectl top pods 命令来检查 CPU 和内存使用情况的时候。我很震惊地发现,即使没有任何负载,这个应用也几乎占用了 1GB 的 RAM。Java 也没好到哪里去。后来我发现了像 Rust 和 Go 这样的新语言,意识到事情可以做得更高效。


    我查找了一些性能和资源使用方面的基准测试,并发现使用能够高效利用资源的语言可以节省很多成本。这里有一个例子:


    https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*tY9ZYw1_JcHcFh4WcbgMOQ.png


    Link to the original article


    https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*CDPVSFk_wEqDSWCGknk90g.png


    Link to the original article


    想象一下,有一个 Lambda 函数被创建用来列出 AWS 账户中的所有存储桶,并确定每个存储桶所在的区域。你可能会认为,进行一些 REST API 调用并使用 for 循环在性能上不会有太大的区别。任何语言都应该能够合理地处理这个任务,对吧?


    然而,测试显示 Rust 在执行这项任务时比 Python 快得多,并且使用更少的内存来达到这些执行时间。事实上,他们每百万次调用节省了 6 美元。


    来自 web 和 Kubernetes 的背景,在那里我们根据用户负载进行扩缩容,我可以确认高效的资源使用能够节省成本并提高系统的可靠性。每个副本使用较少的资源意味着更多的容器可以装入一个节点。如果每个副本能够处理更多的请求,则总体上需要的副本数量就会减少。高性能和高效的资源利用对于构建成本效益高且可靠的系统至关重要。


    我已经在 web 开发领域使用 Rust 三年了,对此我非常满意。那些具有挑战性的方面,比如编写异步代码或宏,都被我们使用的库很好地处理了。例如,如果你研究过 Tokio 库,你会知道它可能相当复杂。但在 web 开发中,我们专注于业务逻辑并与外部系统(如数据库或消息队列)交互,我们得以享受更简单的一面,同时受益于 Rust 出色的安全特性。


    试试 Rust 吧;你可能会喜欢它,甚至成为一名更好的程序员。


    作者:傻梦兽
    来源:juejin.cn/post/7399288740908531712
    收起阅读 »

    我:CSS,你怎么变成紫色了?CSS:别管这些,都快2025了,这些新特性你还不知道吗?🤡

    web
    事情起因是这样的,大二的苦逼学生在给老外做页面的时候,做着做着无意间瞥见了css的图标。 wait!你不是我认识的css!你是谁? 我的天呐,你怎么成了这种紫色方块?(如果只关心为什么图标换了,可以直接跳到文章末尾) 这提起了我的兴趣,立马放下手中的工作...
    继续阅读 »

    事情起因是这样的,大二的苦逼学生在给老外做页面的时候,做着做着无意间瞥见了css的图标。
    image.png


    wait!你不是我认识的css!你是谁?
    image.png


    我的天呐,你怎么成了这种紫色方块?(如果只关心为什么图标换了,可以直接跳到文章末尾)


    image.png


    这提起了我的兴趣,立马放下手中的工作去了解。查才知道,这是有原因的,而且在这之间CSS也更新了很多新特性。


    不过看了许多博客,发现没啥人说这件事(也可能是我没找到),所以到我来更新了!😄


    这里主要谈谈我认为还算有点用的新特性,全文不长,如果对您有用的话,麻烦给个赞和收藏加关注呗🙏!如果可以的话,掘金人气作者评选活动给孩子投一票吧😭



    先叠个甲:所有观点纯本菜🐔意淫,各位大佬地方看个乐呵就行。



    参考文献:张鑫旭的个人主页 » 张鑫旭-鑫空间-鑫生活
    MDN Web Docs


    块级元素居中新方式:Align Content for Block Elements


    元素垂直居中对齐终于有了专门的CSS属性,之前Flex布局和Grid布局中使用的align-content属性,现在已经可以在普通的block块级元素中使用。



    垂直居中的快级元素不再需要 flex 或 grid,注意是在垂直居中!!!



          display: block; <-非块级元素请加上这个代码
    align-content: center;


    不过我好像以前用过,不过用的很少,不知道是不是发生了改变造成了这种效果🤡



    请看如下代码


      <style>
    .father {
    display: block;
    align-content: center;
    background-color: aqua;
    width: 300px;
    height: 300px
    }

    .son {
    width: 100px;
    height: 100px;
    background-color: red;
    }
    </style>

    可以发现是div是垂直居中显示的


    image.png


    实现效果和用flex是一样的


      <style>
    .father {
    display: flex;
    align-item:center
    background-color: aqua;
    width: 300px;
    height: 300px
    }

    .son {
    width: 100px;
    height: 100px;
    background-color: red;
    }
    </style>

    image.png



    提醒一下,目前普通元素并不支持justify-content属性,必须Flex布局或者Grid布局。



    subgrid


    额,这个特性似乎国内没有很多文章讲解,但是我记得之前看过一个统计这个特性在老外那里很受欢迎,所以我还是讲解一下。


    subgrid并不是一个CSS属性,而是 grid-template-columnsgrid-template-rows属性支持的关键字,其使用的场景需要外面已经有个Grid布局,否则……嗯,虽然语法上不会识别为异常,但是渲染结果上却是没有区别的。


    例如


    .container {
    display: grid;
    }
    .item {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: subgrid;
    }

    那我们什么时候使用它呢?🤔


    当我们想实现这种效果


    image.png
    Grid布局负责大的组织结构,而里面更细致的排版对齐效果,则可以使用subgrid布局。,这对于复杂的嵌套布局特别有用,在 subgrid 出现之前,嵌套网格往往会导致 CSS 变得复杂冗长。(其实你用flex也可以)
    子网格允许子元素与父网格无缝对齐,从而简化了这一过程。


        .container {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    }
    .item {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 4;
    gap: .5rem;
    }
    /* 以下CSS与布局无关 */
    .item {
    padding: .75rem;
    background: #f0f3f9;
    }
    .item blockquote {
    background-color: skyblue;
    }
    .item h4 {
    background-color: #333;
    color: #fff;
    }

      <div class="container">
    <section class="item">
    <h4>1</h4>
    <p>负责人:张三</p>
    <blockquote>脑子和我只能活一个</blockquote>
    <footer>3人参与 12月21日</footer>
    </section>
    <section class="item">
    <h4>1</h4>
    <p>负责人:张三</p>
    <blockquote>脑子和我只能活一个</blockquote>
    <footer>3人参与 12月21日</footer>
    </section>
    </div>

    效果
    image.png


    @property


    @property规则属于CSS Houdini中的一个特性,可以自定义CSS属性的类型,这个特性在现代CSS开发中还是很有用的,最具代表性的例子就是可以让CSS变量支持动画或过渡效果。


    我个人认为这个东西最大的作用就是在我们写颜色渐变的时候很好避免使用var()不小心造成颜色继承的,而导致效果不理想。


    用法


    @property --rotation {
    syntax: "<angle>";
    inherits: false;
    initial-value: 45deg;
    }

    描述符



    • syntax 描述已注册自定义属性允许的值类型的字符串。可以是数据类型名称(例如<color><length><number>等),带有乘数(+#)和组合符(|),或自定义标识。

    • inherits 一个布尔值,控制指定的自定义属性注册是否默认@property继承。

    • initial-value设置属性的起始值的值。


    描述



    注意




    • 规则@property必须同时包含syntaxinherits描述符。如果缺少其中之一,则整个@property规则无效并被忽略。

    • 未知描述符无效并会被忽略,但不会使@property规则无效。


    简单演示


        @property --box-pink {
    syntax: "<color>";
    inherits: false;
    initial-value: pink;
    }
    .box {
    width: 100px;
    height: 100px;
    background-color: var(--box-pink);
    }

    image.png


    使用它进行颜色渐变


        @property --colorA {
    syntax: "<color>";
    inherits: false;
    initial-value: red;
    }

    @property --colorB {
    syntax: "<color>";
    inherits: false;
    initial-value: yellow;
    }

    @property --colorC {
    syntax: "<color>";
    inherits: false;
    initial-value: blue;
    }



    .box {
    width: 300px;
    height: 300px;
    background: linear-gradient(45deg,
    var(--colorA),
    var(--colorB),
    var(--colorC));
    animation: animate 3s linear infinite alternate;
    }

    @keyframes animate {
    20% {
    --colorA: blue;
    --colorB: #F57F17;
    --colorC: red;
    }

    40% {
    --colorA: #FF1744;
    --colorB: #5E35B1;
    --colorC: yellow;
    }

    60% {
    --colorA: #E53935;
    --colorB: #1E88E5;
    --colorC: #4CAF50;
    }

    80% {
    --colorA: #76FF03;
    --colorB: teal;
    --colorC: indigo;
    }
    }

    </style>

    Recording%202024-12-20%20230242_converted.gif


    transition-behavior让display none也有动画效果


    大家都知道我们在设置一个元素隐藏和出现是一瞬间的,那有没有办法让他能出现类似于淡入淡出的动画效果呢?


    这里我们就要介绍transition-behavior了,但是也有其他方法,这里就只介绍它。
    语法如下:


    transition-behavior: allow-discrete;
    transition-behavior: normal;


    • allow-discrete表示允许离散的CSS属性也支持transition过渡效果,其中,最具代表性的离散CSS属性莫过于display属性了。


    使用案例


    仅使用transition属性,实现元素从 display:inline ↔ none 的过渡效果。


        img {
    transition: .25s allow-discrete;
    opacity: 1;
    height: 200px;
    }
    img[hidden] {
    opacity: 0;
    }

      <button id="trigger">图片显示与隐藏</button>
    <img id="target" src="./1.jpg" />

      trigger.onclick = function () {
    target.toggleAttribute('hidden');
    };

    Recording%202024-12-21%20013159_converted.gif
    这里我们可以发现消失的时候是有淡出效果的,但是出现却是一瞬间的,这是为什么?


    原因是:
    display:nonedisplay:block的显示是突然的,在浏览器的渲染绘制层面,元素display计算值变成block和opacity设为1是在同一个渲染帧完成的,由于没有起始opacity,所以看不到动画效果。


    那有没有什么办法能解决呢?🤔


    使用@starting-style规则声明过渡起点


    @starting-style顾名思义就是声明起始样式,专门用在transition过渡效果中。


    例如上面的例子,要想让元素display显示的时候有淡出效果,很简单,再加三行代码就可以了:


    img {
    transition: .25s allow-discrete;
    opacity: 1;
    @starting-style {
    opacity: 0;
    }
    }

    或者不使用CSS嵌套语法,这样写也是可以的:


    img {
    transition: .25s allow-discrete;
    opacity: 1;
    }
    @starting-style {
    img {
    opacity: 0;
    }
    }

    此时,我们再点击按钮让图片显示,淡入淡出效果就都有了。


    Recording%202024-12-21%20013930_converted.gif



    注意:@starting-style仅与 CSS 过渡相关。使用CSS 动画实现此类效果时,@starting-style就不需要。



    light-dark


    先说明一下,我认为 CSS 的新 light-dark() 函数是 2024 年实现暗模式的最佳方式!


    你自 2019 年以来,开发人员只需一行 CSS 就可以为整个站点添加暗模式?只需在 :root 中添加 color-scheme: light dark;,就可以获得全站暗模式支持——但它只适用于未指定颜色的元素,因此使用默认的浏览器颜色。


    如果你想让自定义颜色的暗模式生效(大多数网站都需要),你需要将每个颜色声明包装在笨拙的 @media (prefers-color-scheme: ...) 块中:


    @media (prefers-color-scheme: dark) {
    body {
    color: #fff;
    background-color: #222;
    }
    }

    @media (prefers-color-scheme: light) {
    body {
    color: #333;
    background-color: #fff;
    }
    }

    基本上,你需要把每个颜色声明写两遍。糟糕!这种冗长的语法使得编写和维护都很麻烦。因此,尽管 color-scheme 已发布五年,但从未真正流行起来。


    light-dark很好解决了这个问题


    基本语法


    /* Named color values */
    color: light-dark(black, white);

    /* RGB color values */
    color: light-dark(rgb(0 0 0), rgb(255 255 255));

    /* Custom properties */
    color: light-dark(var(--light), var(--dark));

    body {
    color-scheme: light dark; /* 启用浅色模式和深色模式 */
    color: light-dark(#333, #fff); /* 文本浅色和深色颜色 */
    background-color: light-dark(#fff, #222); /* 背景浅色和深色颜色 */
    }

    在这个示例代码中,正文文本在浅色模式下定义为 #333,在深色模式下定义为 #fff,而背景色则分别定义为 #fff 和 #222。就这样!浏览器会根据用户的系统设置自动选择使用哪种颜色。


    无需 JavaScript 逻辑、自定义类或媒体查询。一切都能正常工作!


    :user-vaild pseudo class


    :user-validCSS伪类表示任何经过验证的表单元素,其值根据其验证约束正确验证。然而,与:valid此不同的是,它仅在用户与其交互后才匹配。


    有什么用呢?🤔



    这就很好避免了我们在进行表单验证的时候,信息提示在你交互之前出现的尴尬。



      <form>
    <label for="email">Email *: </label>
    <input
    id="email"
    name="email"
    type="email"
    value="test@example.com"
    required />

    <span></span>
    </form>

    input:user-valid {
    border: 2px solid green;
    }

    input:user-valid + span::before {
    content: "😄";
    }

    在以下示例中,绿色边框和😄仅在用户与字段交互后显示。我们将电子邮件地址更改为另一个有效的电子邮件地址就可以看到了


    image.png


    interpolate-size


    interpolate-sizecalc-size()函数属性的设计初衷是一致的,就是可以让width、height等尺寸相关的属性即使值是auto,也能有transition过渡动画效果。


    最具代表性的就是height:auto的过渡动画实现。


        p {
    height: 0;
    transition: height .25s;
    overflow: hidden;
    }

    .active+p {
    height: auto;
    height: calc-size(auto, size);
    }

      <button onClick="this.classList.toggle('active');">点击我</button>
    <p>
    <img src="./1.jpg" width="256" />
    </p>

    其实,要让height:auto支持transition过渡动画,还有个更简单的方法,就是在祖先容器上设置:


    interpolate-size: allow-keywords;

    换句话说,calc-size()函数是专门单独设置,而interpolate-size是全局批量设置。


    interpolate-size: allow-keywords;
    interpolate-size: numeric-only;

    /* 全局设置 */
    /* :root {
    interpolate-size: allow-keywords;

    } */

    div {
    width: 320px;
    padding: 1em;
    transition: width .25s;
    /* 父级设置 */
    interpolate-size: allow-keywords;
    background: deepskyblue;
    }

    .active+div {
    width: 500px;
    }
    </style>

      <button onClick="this.classList.toggle('active');">点击我</button>
    <div>
    <img src="./1.jpg" width="256" />
    </div>

    Recording%202024-12-21%20002244_converted.gif


    全新的CSS相对颜色语法-使用from


    from的作用我认为是简化了我们让文字自动适配背景色的步骤


    我们先来看看用法


    p {
    color: rgb(from red r g b / alpha);
    }

    image.png


    原因:r g b 以及 alpha 实际上是对red的解构,其计算值分别是255 0 0 / 1(或者100%)。



    注意:实际开发,我们不会使用上面这种用法,这里只是为了展示语法作用。



    使用from让文字适配背景色


    <button id="btn" class="btn">我是按钮</button>

    <p>请选择颜色:<input
    type="color"
    value="#2c87ff"
    onInput="btn.style.setProperty('--color', this.value);"
    >
    </p>

    <p>请选择颜色:<input 
    type="color"
    value="#2c87ff"
    onInput="btn.style.setProperty('--color', this.value);"
    ></p>

    Recording%202024-12-21%20004850_converted.gif


    rebecca purple(#663399)


    好了,最重要的东西来了,关于为什么变成了紫色,其实他们把它叫做rebecca紫,为什么叫这个名字呢?这其实是一个令人悲伤的故事😭。


    在关于css这个新颜色以及logo的时候,内部发生了许多争议。


    但是相信大部分人都读过CSS The Definitive guide,他的作者Eric A.Myer的女儿在这期间因为癌症去世了
    在她去世的前几周,Rebecca说她即将成为一个六岁大的女孩,而becca是一个婴儿的名字。六岁后,他希望每个人都叫他Rebecca,而不是becca。


    而那个女孩和病魔抗争,一直坚持到她到六岁,
    我无法想象假如我是父亲,失去一个那么可爱的一个六岁的孩子,那个心情有多么痛苦。


    最终社区被他的故事感动了,css的logo也就变成了这样。


    总结


    新特性多到让人麻木,真的非常非常多!!!!😵‍💫
    这些新特性出现的速度比某四字游戏出皮肤的速度还快🚀,关键这些特性浏览器支持情况参差不齐,短时间无法在生产环境应用。


    我真的看了非常都非常久,从早上五点起来开始看文档,除去吃饭上课,加上去写文章一直弄到凌晨三点,才选出这么几个我认为还算有点作用的新特性。


    而且现有的JavaScript能力已经足够应付目前所有的交互场景,很多新特性没有带来颠覆性的改变,缺少迫切性和必要性,很难被重视。



    最后的最后,希望大家的身边的亲人身体都健健康康的,也希望饱受癌症折磨的人们能够早日康复🙏



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

    Nuxt 3手写一个搜索页面

    web
    Nuxt 3手写一个搜索页面 前言 前面做了一个小型搜索引擎,虽然上线了,但总体来说还略显粗糙。所以最近花了点时间打磨了一下这个搜索引擎的前端部分,使用的技术是Nuxt,UI组件库使用的是Vuetify,关于UI组件库的选择,你也可以查看我之前写的这篇对比。 ...
    继续阅读 »

    Nuxt 3手写一个搜索页面


    前言


    前面做了一个小型搜索引擎,虽然上线了,但总体来说还略显粗糙。所以最近花了点时间打磨了一下这个搜索引擎的前端部分,使用的技术是Nuxt,UI组件库使用的是Vuetify,关于UI组件库的选择,你也可以查看我之前写的这篇对比


    本文不会介绍搜索引擎的其余部分,算是一篇前端技术文...


    重要的开源地址,应用部分的代码我也稍微整理了一下开源了,整体来说偏简单,毕竟只有一个页面,算是真正的“单页面应用”了🤣🤣🤣


    演示



    为什么要重写


    这次重写的目的如下:



    1. 之前写的代码太乱了,基本一个页面就只写了一个文件,维护起来有点困难;

    2. 之前的后端使用nest单独写的,其实就调调API,单独起一个后端服务感觉有点重;

    3. 最后一点也是最重要的:使用SSR来优化一下SEO


    具体如下:



    1. 比如当用户输入搜索之后,对应的url路径也会发生变化,比如ssgo.app/?page=1&que…

    2. 如果用户将该url分享到其他平台被搜索引擎抓取之后,搜索引擎得到的数据将不再是空白的搜索框,而是包含相关资源的结果页,

    3. 这样有可能再下一次用户在其他搜索引擎搜索对应资源的时候,有可能会直接跳转到该应用的搜索结果页,这样就可以大大提高该应用的曝光率。


    这样,用户之后不仅可以通过搜索“阿里云盘搜索引擎”能搜到这个网站,还有可能通过其他资源的关键词搜索到该网站


    页面布局


    首先必须支持移动端,因为从后台的访问数据看,移动端的用户更多,所以整体布局以竖屏为主,至于宽屏PC,则增加一个类似于max-width的效果。


    其次为了整体实现简单,采取的还是搜索框与搜索结果处在一个页面,而非google\baidu之类的搜索框主页与结果页分别是两个页面,笔者感觉主页也没啥存在的必要(单纯对于搜索功能而言)


    页面除了搜索框、列表项,还应该有logo,菜单,最终经过排版如下图所示:



    左右两边为移动端的效果演示图,中间为PC端的效果演示。


    nitro服务端部分


    这里只需要实现两个API:



    1. 搜索接口,如/api/search

    2. 搜索建议的接口,如/api/search/suggest


    说到这里就不得不夸一下nuxt的开发者体验,新建一个API是如此的方便:



    对比nest-cli中新建一个service/controller要好用不少,毕竟我在nest-cli中基本要help一下。


    回到这里,我的server目录结构如下:


    ├─api
    │ └─search # 搜索接口相关
    │ index.ts # 搜索
    │ suggest.ts # 搜索建议

    └─elasticsearch
    index.ts # es客户端

    elasticsearch目录中,我创建了一个ES的客户端,并在search中使用:


    // elasticsearch/index.ts

    import { Client } from '@elastic/elasticsearch';

    export const client = new Client({
    node: process.env.ES_URL,
    auth: {
    username: process.env.ES_AUTH_USERNAME || '',
    password: process.env.ES_AUTH_PASSWORD || ''
    }
    });

    然后使用,使用部分基本上没有做任何的特殊逻辑,就是调用ES client提供的api,然后组装了一下参数就OK了:


    // api/search/index
    import { client } from "~/server/elasticsearch";

    interface ISearchQuery {
    pageNo: number;
    pageSize: number;
    query: string;
    }

    export default defineEventHandler(async (event) => {
    const { pageNo = 1, pageSize = 10, query }: ISearchQuery = getQuery(event);

    const esRes = await client.search({
    index: process.env.ES_INDEX,
    body: {
    from: (pageNo - 1) * pageSize, // 从哪里开始
    size: pageSize, // 查询条数
    query: {
    match: {
    title: query, // 搜索查询到的内容
    },
    },
    highlight: {
    pre_tags: ["<span class='highlight'>"],
    post_tags: ['</span>'],
    fields: {
    title: {},
    },
    fragment_size: 40,
    },
    },
    });

    const finalRes = {
    took: esRes.body.took,
    total: esRes.body.hits.total.value,
    data: esRes.body.hits?.hits.map((item: any) => ({
    title: item._source.title,
    pan_url: item._source.pan_url,
    extract_code: item._source.extract_code,
    highlight: item.highlight?.title?.[0] || '',
    })),
    };

    return finalRes;
    });

    // api/search/suggest
    import { client } from "~/server/elasticsearch";

    interface ISuggestQuery {
    input: string;
    }

    export default defineEventHandler(async (event) => {
    const { input }: ISuggestQuery = getQuery(event);

    const esRes = await client.search({
    index: process.env.ES_INDEX,
    body: {
    suggest: {
    suggest: {
    prefix: input,
    completion: {
    field: "suggest"
    }
    }
    }
    },
    });

    const finalRes = esRes.body.suggest.suggest[0]?.options.map((item: any) => item._source.suggest)

    return finalRes;
    });


    值得注意的是,客户端的ES版本需要与服务端的ES版本相互对应,比如我服务端使用的是ES7,这路也当然得使用ES7,如果你是ES8,这里需要安装对应版本得ES8,并且返回参数有些变化,ES8中上述esRes就没有body属性,而是直接使用后面的属性


    page界面部分


    首先为了避免出现之前所有代码均写在一个文件中,这里稍微封装了几个组件以使得page/index这个组件看起来相对简单:


    /components
    BaseEmpty.vue
    DataList.vue
    LoadingIndicator.vue
    MainMenu.vue
    PleaseInput.vue
    RunSvg.vue
    SearchBar.vue

    具体啥意思就不赘述了,基本根据文件名就能猜得大差不差了...


    然后下面就是我的主页面部分:


    <template>
    <div
    class="d-flex justify-center bg-grey-lighten-5 overflow-hidden overflow-y-hidden"
    >

    <v-sheet
    class="px-md-16 px-2 pt-4"
    :elevation="2"
    height="100vh"
    :width="1024"
    border
    rounded
    >

    <v-data-iterator :items="curItems" :page="curPage" :items-per-page="10">
    <template #header>
    <div class="pb-4 d-flex justify-space-between">
    <span
    class="text-h4 font-italic font-weight-thin d-flex align-center"
    >

    <RunSvg style="height: 40px; width: 40px"></RunSvg>
    <span>Search Search Go...</span>
    </span>
    <MainMenu></MainMenu>
    </div>
    <SearchBar
    :input="curInput"
    @search="search"
    @clear="clear"
    >
    </SearchBar>
    </template>
    <template #default="{ items }">
    <v-fade-transition>
    <DataList
    v-if="!pending"
    :items="items"
    :total="curTotal"
    :page="curPage"
    @page-change="pageChange"
    >
    </DataList>
    <LoadingIndicator v-else></LoadingIndicator>
    </v-fade-transition>
    </template>
    <template #no-data>
    <template v-if="!curInput || !pending">
    <v-slide-x-reverse-transition>
    <BaseEmpty v-if="isInput"></BaseEmpty>
    </v-slide-x-reverse-transition>
    <v-slide-x-transition>
    <PleaseInput v-if="!isInput"></PleaseInput>
    </v-slide-x-transition>
    </template>
    </template>
    </v-data-iterator>
    </v-sheet>
    </div>
    </template>

    <script lang="ts" setup>
    const route = useRoute();
    const { query = "", page = 1 } = route.query;
    const router = useRouter();
    const defaultData = { data: [], total: 0 };

    const descriptionPrefix = query ? `正在搜索“ ${query} ”... ,这是` : "";
    useSeoMeta({
    ogTitle: "SearchSearchGo--新一代阿里云盘搜索引擎",
    ogDescription: `${descriptionPrefix}一款极简体验、优雅、现代化、资源丰富、免费、无需登录的新一代阿里云盘搜索引擎,来体验找寻资源的快乐吧~`,
    ogImage: "https://ssgo.app/logobg.png",
    twitterCard: "summary",
    });

    interface IResultItem {
    title: string;
    pan_url: string;
    extract_code: string;
    highlight: string;
    }

    interface IResult {
    data: IResultItem[];
    total: number;
    }

    const curPage = ref(+(page || 1));

    const curInput = ref((query || "") as string);
    const isInput = computed(() => !!curInput.value);

    let { data, pending }: { data: Ref<IResult>; pending: Ref<boolean> } =
    await useFetch("/api/search", {
    query: { query: curInput, pageNo: curPage, pageSize: 10 },
    immediate: !!query,
    });
    data.value = data.value || defaultData;

    const curItems = computed(() => data.value.data);
    const curTotal = computed(() => data.value.total);

    function search(input: string) {
    curPage.value = 1;
    curInput.value = input;
    router.replace({ query: { ...route.query, query: input, page: 1 } });
    }

    function pageChange(page: number) {
    curPage.value = page;
    router.replace({ query: { ...route.query, page: page } });
    }

    function clear() {
    curInput.value = "";
    data.value = defaultData;
    // 这里就不替换参数了,保留上一次的感觉好一些
    }
    </script>

    大部分代码都是调用相关的子组件,传递参数,监听事件之类的,这里也不多说了。比较关键的在于这两部分代码:


    useSeoMeta({
    ogTitle: "SearchSearchGo--新一代阿里云盘搜索引擎",
    ogDescription: `${descriptionPrefix}一款极简体验、优雅、现代化、资源丰富、免费、无需登录的新一代阿里云盘搜索引擎,来体验找寻资源的快乐吧~`,
    ogImage: "https://ssgo.app/logobg.png",
    twitterCard: "summary",
    });

    这里的SEO显示的文字是动态的,比如当前用户正在搜索AI,那么url路径参数也会增加AI,分享出去的页面描述就会包含AI,在twitter中的显示效果如下:



    还有部分代码是这一部分:


    let { data, pending }: { data: Ref<IResult>; pending: Ref<boolean> } =
    await useFetch("/api/search", {
    query: { query: curInput, pageNo: curPage, pageSize: 10 },
    immediate: !!query,
    });

    其中immediate: !!query表示如果当前路径包含搜索词,则会请求数据,渲染结果页,否则不立即执行该请求,而是等一些响应式变量如curInputcurPage发生变化后执行请求。


    子组件部分这里就不详细解释了,具体可以查看源码,整体来说并不是很复杂。


    其他


    除此之外,我还增加了google analytics和百度 analytics,代码都非常简单,在plugins/目录下,如果你需要使用该项目,记得将对应的id改为你自己的id。


    最后


    这次也算是第一次使用nuxt来开发一个应用,总体来说安装了nuxt插件之后的开发体验非常不错,按照目录规范写代码也可以少掉很多导入导出的一串串丑代码。



    关于笔者--justin3go.com



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

    又报gyp ERR!为什么有那么多人被node-sass 坑过?

    web
    前言 node-sass: Command failed., gyp ERR! build error 这几个词相信很多小伙伴一定看着眼熟,当你的终端出现这些词时那么毫无疑问,你的项目跑不起来了。。。。。。 你可能通过各种方式去解决了这个报错,但是应该没有人去...
    继续阅读 »

    前言


    node-sass: Command failed.gyp ERR! build error 这几个词相信很多小伙伴一定看着眼熟,当你的终端出现这些词时那么毫无疑问,你的项目跑不起来了。。。。。。


    你可能通过各种方式去解决了这个报错,但是应该没有人去深究到底是咋回事,接下来让我们彻底弄懂,再遇到类似问题时能够举一反三,顺利解决。


    关键词:node-sass libsass node-gyp


    先来看个截图感受一下


    image.png


    image-1.png


    熟悉吧?截图里我们看到了几个关键词 node-sass libsass node-gyp .cpp,我们一一来解释一下


    node-sass


    node-sass 是一个用于在 Node.js 中编译 Sass 文件的库,node-sass 可以将 .scss 或 .sass 文件编译为标准的 .css 文件,供浏览器或其他工具使用。



    • node-sass 可以看作是 libsass 的一个包装器(wrapper)或者说是 nodejs 和 libsass 之间的桥梁。

    • 它提供了一个 Node.js 友好的接口来使用 libsass 的功能。

    • 当你使用 node-sass 时,实际的 Sass 编译工作是由底层的 libsass 完成的。


    当你的项目中使用 sass 来写样式时,会直接或间接地引入这个这个库。


    libsass


    一个用 C++ 编写的高性能 Sass 编译器,这就是为什么你能在终端日志中看到 .cpp 的文件。


    注意,搞这么麻烦就是为了高性能编译sass。


    node-gyp


    node-sass引入了 c++ 编写的库,那么直接在 node 中肯定是使用不了的,毕竟是两种不同的语言嘛,那么就需要 node-gyp 登场了。


    node-gyp 是一个用于编译和构建原生 Node.js 模块的工具,这些原生模块通常是用 C++ 编写的。


    node-sass 需要在安装时编译 libsass 的 C++ 代码,以生成可以在本地机器上运行的二进制文件。在这个编译过程中,node-gyp 就被用作构建工具,它负责调用 C++ 编译器(如 g++ 或 clang++),并将 libsass 的源代码编译为与当前系统兼容的二进制文件。


    node-gyp 本身是一个用 JavaScript 编写的工具。它使用 Node.js 的标准模块(如 fs 和 path)来处理构建和配置任务,但是需要一些外部工具来实际进行编译和构建,例如 make、gcc等,这些工具必须安装在系统中,node-gyp 只是协调和使用这些工具。


    普通模块(JavaScript/TypeScript)


    普通模块是用 JavaScript 或 TypeScript 编写的,Node.js 本身可以直接执行或通过编译(如 TypeScript 编译器)转换为可执行代码,Node.js 使用 V8 引擎执行 JavaScript 代码。


    原生模块(C/C++ 编写)


    原生模块是用 C/C++ 编写的代码,这些模块通常用于高性能需求或需要直接与底层系统 API 交互的场景。它们通过 node-gyp 进行编译,并在 Node.js 中以二进制文件的形式加载。


    例如以下模块:



    • node-sass:编译 Sass 文件,依赖 libsass 进行高性能的样式编译。

    • sharp:处理图像文件,使用 libvips(c++库) 提供高效的图像操作。

    • bcrypt:进行加密处理,提供安全的密码哈希功能。

    • fsevents:用于 macOS 系统的文件系统事件监听。


    许多现有的高性能库和工具是用 C++ 编写的。为了利用这些库的功能,Node.js 可以通过模块接口调用这些 C++ 库,尤其某些高级功能,如加密算法、图像处理等,已经在 C++ 中得到了成熟的实现,Node.js 可以直接集成这些功能,而不必再重新造轮子而且性能还肯定不如用 c++ 写的库。



    所谓的原生模块接口就是 node-gyp(还有Node-API (N-API)等),感兴趣的可以看看 node-gyp 的实现,或者了解下包信息 npm info node-gyp



    node-gyp 基于 gyp,并在其之上添加了 Node.js 特定的功能,以支持 Node.js 模块的编译。


    gyp


    gyp(Generate Your Projects)是一个用于生成构建配置文件的工具,它负责将源代码(C++ 代码)转换为特定平台的构建配置文件(如 Makefile、Visual Studio 项目文件等),生成构建文件后,使用生成的构建文件来编译项目。例如,在 Unix 系统上,运行 make;在 Windows 上,使用 Visual Studio 编译项目。



    从前端的角度来看,你可以把gyp理解成 webpack,而 make 命令则是 npm run build



    为什么报错?


    现在,我们已经了解了一些基本概念,并且知道了c++原生模块是需要在安装时编译才能使用的,那么很明显,上面的错误就是在编译时出错了,一般出现这个错误都是因为我们有一些老的项目的 node 版本发生了变化,例如上面的报错就是,在 node14 还运行的好好的,到了 node16 就报错了,我们再来看一下报错细节


    image-2.png


    这个错误发生在编译 node-sass 的过程中,具体是在编译 binding.cpp 文件时。
    binding.cpp 是 node-sass 的一部分,用于将 libsass 与 Node.js 接口连接起来。


    报错发生在 v8-internal.h 文件里,大概意思是这个c++头文件里使用了比较高级的语法,node-sass中的c++标准不支持这个语法,导致报错,就相当于你在 js 项目里引入一个三方库,这个三方库使用了 es13 的新特性(未编译为es5),但是你的项目只支持 es6,从而导致你的项目报错。


    所以解决思路就很清晰了,要么降级 node 版本,要么升级 node-sass,要么干脆就不用node-sass了


    解决方案


    rebuild 大法


    npm rebuild node-sass 重新编译模块,对于其他编译错误该方法可能会有效果,但是如果 node-sass 和 node 版本不匹配那就没得办法了,适合临时使用,不推荐。


    升级node-sass


    以下是 node-sass 支持的最低和最高版本的指南,找对应的版本升级就好了


    image-3.png


    但是!node-sass 与node版本强绑定,还得去想着保持版本匹配,心智负担较重,所以使用 sass 更好


    更换为 sass


    官方也说了,不再释放新版本,推荐使用 Dart Sass
    image-4.png


    Sass 和 node-sass 的区别


    实现语言:



    • Sass:Sass 是一个用 Ruby 编写的样式表语言,最初是基于 Ruby 的实现(ruby-sass)。在 Ruby 版本被弃用后,Sass 社区推出了 Dart 语言实现的版本,称为 dart-sass。这个版本是当前推荐的实现,提供了最新的功能和支持。

    • node-sass:node-sass 封装了 libsass,一个用 C++ 编写的 Sass 编译器。node-sass 使用 node-gyp 编译 C++ 代码和链接 libsass 库。


    构建和编译:



    • Sass:dart-sass 是用 Dart 语言编写的,直接提供了一个用 JavaScript 实现的编译器,因此不需要 C++ 编译过程。它在 Node.js 环境中作为纯 JavaScript 模块运行,避免了编译问题。

    • node-sass:node-sass 依赖于 libsass(C++ 编写),需要使用 node-gyp 进行编译。很容易导致各种兼容性问题。


    功能和维护:



    • Sass:dart-sass 是 Sass 的官方推荐实现,拥有最新的功能和最佳的支持。它的更新频繁,提供了对最新 Sass 语言特性的支持。

    • node-sass:由于 node-sass 依赖于 libsass,而 libsass 的维护在 2020 年已停止,因此 node-sass 逐渐不再接收新的功能更新。它的功能和特性可能滞后于 dart-sass。


    性能:



    • Sass:dart-sass 的性能通常比 node-sass 更佳,因为 Dart 编译器的性能优化更加现代。

    • node-sass:虽然 libsass 在性能上曾经表现良好,但由于它不再更新,可能不如现代的 Dart 实现高效。


    总结


    gyp ERR!node-sass 问题是由 node-sass 与 node 版本兼容引起的编译问题,推荐使用 sass 替代,如果老项目不方便改的话就采用降级 node 版本或升级 node-sass 的办法。


    其实这种问题没太大必要刨根究底的,但是如果这次简单解决了下次遇到类似的问题还是懵逼,主要还是想培养自己解决问题的思路,再遇到类似天书一样的坑时不至于毫无头绪,浪费大量时间。


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

    记一次让我压力大到失控的 bug

    事情经过 前两天线上发生了结算的漏洞,这里的代码是我写的,出问题的时候是周日晚上,那天大领导打电话过来问我具体的损失情况。 最后查出来是有两个人逮到了系统漏洞,一共 87 笔订单出现了多结算的问题,薅了大概 2.6 w,有个人当时已经跑了,还有个账户里面只有几...
    继续阅读 »

    事情经过


    前两天线上发生了结算的漏洞,这里的代码是我写的,出问题的时候是周日晚上,那天大领导打电话过来问我具体的损失情况。


    最后查出来是有两个人逮到了系统漏洞,一共 87 笔订单出现了多结算的问题,薅了大概 2.6 w,有个人当时已经跑了,还有个账户里面只有几百块钱。


    发现问题后紧急停止提现,其他的明天上班再处理。


    但我当晚已经无法入睡了,压力非常非常大。


    普通开发和项目负责人最大的区别,可能是后者要承担风险和责任,但这个项目就我一个开发,我要同时兼顾两种角色。


    周一早上去上班,前一天晚上也没睡,精神状态可想而知,浑浑噩噩的,先把问题和场景分析清楚,最终发现是用户在加价的时候回写了订单状态,导致重复结算。紧急加上各种校验,然后把所有相关的接口全部加强校验。


    代码还没写完,就被拉去会议室开批斗会了,直属领导和大领导怼着我一顿骂,诸如什么高级开发、别人犯这种错误早滚蛋了,我还不识趣的解释了下具体原因,结果被骂的更凶了。


    所以记住了,以后犯错了被骂,不要说为什么出错,只要认错,再加上解决时间。


    心态更差了,压力可想而知,花了一天干到凌晨,修复上线。


    结果过了几天,又出现重复问题了。


    仔细一看真是倒了血霉,底层架构分布式锁写的有问题,没锁住请求,在 finally 用了 redission 的 forceunlock,导致并发场景下当前线程的锁被别的线程释放了。


    然后就有了第二次批斗会,这次简短扼要得多,大致就说了两句,今天必须修复完成,再出问题自己提离职申请。


    回到工位先修好了分布式锁,又在资金流水加了道数据库锁。又是干到凌晨,加上中间还有个比较紧急的需求,有几次干到了凌晨一两天。


    到今天事情过去了快一周。


    回想那几天的经历,晚上基本上没睡觉,压力非常大,到凌晨三四点都是常态,对身体和精神都是非常强烈的折磨,辞职的念头此起彼伏。对 bug 也有了 PTSD。


    昨天晚上商务同事告诉我已经追回了 1.5w,预计可以追回 2.4w,后面的分批追回。


    真心感谢商务同事,也给别人造成了不少麻烦。


    心理感悟


    不知道你如果遇到我经历的情况有什么感受。希望你不要有我这么大的压力。不过这次压力也让我更深刻的体会到了一些事。


    就一份工作而已


    就一份工作而已,大不了辞职不干了嘛,不要给自己那么大的压力,压垮了身体,公司也不会负责,受伤的还是自己。


    人得自私一点,多为自己想想,少点集体荣誉感责任心什么的,任何一份工作都是自己成长的垫脚石。


    要成为工作的主人,不要反被工作拿捏了。


    我之前觉得公司给我工资了,我得好好为它卖命。


    现在看看自己这些年写的系统,今年已经给公司创造了 600w+ 的GMV,尽管还有其他商务、运营同事共同点功劳,但我的工资是远远远远不及这个的,从这个角度看,公司应该感谢我这么多年辛苦工作。


    最重要永远是身体、亲人和朋友


    人生说长不长,说短不短。大家都是几十年时间,身外之物,生不带来,死不带走。


    自己的身体健康才是最重要的,不要等身体垮了再追悔莫及,很多健康问题是无法恢复只能缓解的,医学没我们想的那么发达。


    其次是家人、伴侣、朋友。想想自己出生和去世的时候,身边会有哪些人?这时候工作在哪里呢?


    我以前对家人很不耐烦,现在我对他们有耐心了很多,连老婆都觉得我 “好欺负” 了哈哈哈,或许说再多都不会有用,直到自己刻骨铭心的经历一波。


    业务、公司、地球没了你照样转,但是亲人、爱人、朋友没了你,他们会很伤心。


    心态非常重要


    出天大的 bug,业务也不会凉,公司也不会倒闭,天也不会塌,地球不会停止自转,太阳不会熄灭,更重要的是,写 bug 的人不会死。


    极大的压力、焦虑并不会让我把事情做的更好,像我这种搞的几晚上睡不了的人,再去修 bug,效率真不如美美睡一觉再去。


    现在回过头看,自己那么大压力完全是自己给的,像是自残。人得对自己好一点,不要给自己那么大的压力。


    还有领导说的什么自己提离职、这么低级的错误等等骂人的话,别太往心里去,他也是气头上找个东西宣泄下,或者是想告诉你这很重要必须搞好,这是一种情绪的表达。


    你别管他说什么,你现在就是他的出气筒,等他骂完你只要表达你必须解决点问题的态度和解决的时间点就行了。


    给他点情绪价值。领导也有更大的领导要汇报内容,到时候指不定他被骂的更凶呢哈哈。


    付出行动


    纠正完认知和心态,还是要回归到行动上,如何避免类似的情况再发生,从失败中学习。


    像这次结算出问题后,我真正理解了结算所涉及的各种细节和要点,以后再写的任何项目,结算都不可能再出问题了。


    感兴趣的同学可以翻翻我前两篇动态,有关于结算系统设计的文档,后面我还会模拟订单场景,结合建表语句做个详细的结算架构设计。


    对我来说,事情解决才会让我安心。


    就像我必须把系统里涉及结算的场景、代码重新梳理清楚,搞明白了,保证下次不可能再出问题了,我才会真正的没有压力。


    我希望你知道这很重要,只是把心态调整好而不去真正的解决问题,下次还是会出问题,甚至会有不知道什么时候会出问题的恐惧。


    解决问题需要付出精力和行动,这可能更难,但这才是人成长和进步的原因。


    加油,共勉。


    作者:小兵张健
    来源:juejin.cn/post/7450700990389305396
    收起阅读 »

    Tauri+MuPDF 实现 pdf 文件裁剪,侄子再也不用等打印试卷了🤓

    web
    基于MuPDF.js实现的 PDF 文件 A3 转 A4 小工具。(其实就是纵切分成2份🤓) 开发背景 表哥最近经常找我给我侄子的试卷 pdf 文件 A3 转 A4(因为他家只有 A4 纸,直接打印字太小了)。 WPS提供了pdf的分割工具,不过这是会...
    继续阅读 »

    基于MuPDF.js实现的 PDF 文件 A3 转 A4 小工具。(其实就是纵切分成2份🤓)



    开发背景


    表哥最近经常找我给我侄子的试卷 pdf 文件 A3 转 A4(因为他家只有 A4 纸,直接打印字太小了)。



    018963927d97b70ab63347bab8790d3.jpg



    WPS提供了pdf的分割工具,不过这是会员功能,我也不是总能在电脑前操作。于是我想着直接写一个小工具,拿Tauri打包成桌面应用就好了。



    image.png



    在掘金里刷到了柒八九大佬的文章:Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转... 。发现MuPDF.js这个包有截取pdf文件的API,并且提供了编译好的wasm文件,这意味着可以在浏览器中直接体验到更高的裁切性能,于是我果断选择了基于MuPDF开发我的小工具。


    项目简介


    MuPDF-Crop-Kit是一个基于MuPDF.jsReactViteTauri开发的小工具,用于将 PDF 文件从 A3 纸张大小裁切为 A4 纸张大小。它具有以下特点:



    • 免费使用:无需任何费用;

    • 无需后台服务:可以直接在浏览器中运行,无需依赖服务器;

    • 高性能:利用 WebAssembly (WASM) 技术,提供高效的文件裁切性能;

    • 轻量级桌面应用:通过 Tauri 打包成桌面软件,安装包体积小,方便部署;

    • 开源项目:欢迎社区贡献代码和建议,共同改进工具。


    项目代码地址



    开发过程与踩坑



    • MuPDF.js只支持ESM,官网中给出的要么使用.mjs文件,要么需要项目的type改成module


      npm pkg set type=module

      我在我的Rsbuild搭建的项目中都没有配置成功🤷‍♂️,最后发现用Vite搭建的项目直接就可以用...


    • 因为没有直接提供我想要的功能,肯定是要基于现有的API手搓了。但是截取页面的API会修改原页面,那么自然想到是要复制一份出来,一个截左边一个截右边了。但是MuPDF.jscopyPage复制出来的pdf页修改之后,原pdf页居然也会被修改。
      于是我想到了,一开始就new两个PDFDocument对象,一个截左边一个截右边,最后再合并到一起,我很快实现了两个文档的分别截取,并通过转png图片之后再合并,完成了裁切后的文档的浏览器预览。
      然后我考虑直接使用jspdfpng图片转pdf文件,结果2MB的原文件转换后变成了12MB,并且如果原文件是使用扫描全能王扫描出来的,生成的pdf文件会很糊。
      最后,终于让我在文档中发现merge方法:


      image.png


      不过依赖包提供的方法很复杂:


      merge(sourcePDF, fromPage = 0, toPage = -1, startAt = -1, rotate = 0, copyLinks = true, copyAnnotations = true) {
       if (this.pointer === 0) {
           throw new Error("document closed");
       }
       if (sourcePDF.pointer === 0) {
           throw new Error("source document closed");
       }
       if (this === sourcePDF) {
           throw new Error("Cannot merge a document with itself");
       }
       const sourcePageCount = sourcePDF.countPages();
       const targetPageCount = this.countPages();
       // Normalize page numbers
       fromPage = Math.max(0, Math.min(fromPage, sourcePageCount - 1));
       toPage = toPage < 0 ? sourcePageCount - 1 : Math.min(toPage, sourcePageCount - 1);
       startAt = startAt < 0 ? targetPageCount : Math.min(startAt, targetPageCount);
       // Ensure fromPage <= toPage
       if (fromPage > toPage) {
           [fromPage, toPage] = [toPage, fromPage];
       }
       for (let i = fromPage; i <= toPage; i++) {
           const sourcePage = sourcePDF.loadPage(i);
           const pageObj = sourcePage.getObject();
           // Create a new page in the target document
           const newPageObj = this.addPage(sourcePage.getBounds(), rotate, this.newDictionary(), "");
           // Copy page contents
           const contents = pageObj.get("Contents");
           if (contents) {
               newPageObj.put("Contents", this.graftObject(contents));
           }
           // Copy page resources
           const resources = pageObj.get("Resources");
           if (resources) {
               newPageObj.put("Resources", this.graftObject(resources));
           }
           // Insert the new page at the specified position
           this.insertPage(startAt + (i - fromPage), newPageObj);
           if (copyLinks || copyAnnotations) {
               const targetPage = this.loadPage(startAt + (i - fromPage));
               if (copyLinks) {
                   this.copyPageLinks(sourcePage, targetPage);
               }
               if (copyAnnotations) {
                   this.copyPageAnnotations(sourcePage, targetPage);
               }
           }
       }
      }

      而且在循环调用这个MuPDF.js提供的merge方法时,wasm运行的内存被爆了🤣。
      仔细阅读代码发现其核心实现就是:



      • addPage新增页面;

      • put("Resources")复制原文档页面中的内容到新页面;

      • insertPage将新增的页面插入到指定文档中。


      因为我并没有后续添加的linkannotation,所以经过设计后,决定使用一个空的pdf文档,逐页复制原文档两次到空白文档中。主要逻辑如下:



      • 加载 PDF 文件:读取并解析原始 A3 PDF 文件。

      • 复制页面:创建两个新的 PDF 文档,分别截取每页的左半部分和右半部分。

      • 合并页面:将两个新文档中的页面合并到一个新的 PDF 文档中。

      • 设置裁剪框:根据 A4 纸张尺寸设置裁剪框(CropBox)和修整框(TrimBox)。


      export function merge(
       targetPDF: mupdfjs.PDFDocument,
       sourcePage: mupdfjs.PDFPage
      ) {
       const pageObj = sourcePage.getObject();
       const [x, y, width, height] = sourcePage.getBounds();
       // Create a new page in the target document
       const newPageObj = targetPDF.addPage(
        [x, y, width, height],
         0,
         targetPDF.newDictionary(),
         ""
      );
       // Copy page contents
       const contents = pageObj.get("Contents");
       if (contents) newPageObj.put("Contents", targetPDF.graftObject(contents));
       // Copy page resources
       const resources = pageObj.get("Resources");
       if (resources) newPageObj.put("Resources", targetPDF.graftObject(resources));
       // Insert the new page at the specified position
       targetPDF.insertPage(-1, newPageObj);
      }

      export function generateNewDoc(PDF: mupdfjs.PDFDocument) {
       const count = PDF.countPages();
       const mergedPDF = new mupdfjs.PDFDocument();
       for (let i = 0; i < count; i++) {
         const page = PDF.loadPage(i);
         merge(mergedPDF, page);
         merge(mergedPDF, page);
      }

       for (let i = 0; i < count * 2; i++) {
         const page = mergedPDF.loadPage(i); // 使用 mergedPDF 的页码
         const [x, y, width, height] = page.getBounds();
         if (i % 2 === 0)
           page.setPageBox("CropBox", [x, y, x + width / 2, y + height]);
         else page.setPageBox("CropBox", [x + width / 2, y, x + width, y + height]);

         page.setPageBox("TrimBox", [0, 0, 595.28, 841.89]);
      }
       return mergedPDF;
      }

      完成以上核心方法后,便可以成功将我侄子的试卷裁切为A4大小进行打印了✅。



    体验与安装使用


    浏览器版



    桌面版



    使用教程


    使用本工具非常简单,只需几个步骤即可完成 PDF 文件的裁切:



    1. 选择需要裁切的 A3 PDF 文件;


      image.png


    2. 点击裁切按钮;


      image.png


    3. 下载裁切后的 A4 PDF 文件。


      image.png



    不足



    • 项目所使用的wasm文件大小有10MB,本工具真正用到的并没有那么多,但是优化需要修改原始文件并重新编译;

    • 浏览器端的性能受限,并且wasm运行可以使用的内存也是有限的;

    • 没有使用Web Worker,理论上转换这种高延迟的任务应当放在Woker线程中进行来防止堵塞主线程。


    替代方案


    如果在使用过程中遇到问题或需要更多功能,可以尝试以下在线工具:



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

    9个要改掉的TypeScript坏习惯

    web
    为了提升TypeScript技能并避免常见的坏习惯,以下是九个需要改掉的坏习惯,帮助你编写更高效和规范的代码。 1. 不使用严格模式 错误做法: 不启用tsconfig.json中的严格模式。 正确做法: 启用严格模式。 原因: 更严格的规则有助于未来代码的维...
    继续阅读 »

    为了提升TypeScript技能并避免常见的坏习惯,以下是九个需要改掉的坏习惯,帮助你编写更高效和规范的代码。


    1. 不使用严格模式


    错误做法: 不启用tsconfig.json中的严格模式。

    正确做法: 启用严格模式。

    原因: 更严格的规则有助于未来代码的维护,修复代码的时间会得到回报。



    2. 使用 || 确定默认值


    错误做法: 使用 || 处理可选值。



    正确做法: 使用 ?? 运算符或在参数级别定义默认值。

    原因: ?? 运算符只对 null 或 undefined 进行回退,更加精确。



    3. 使用 any 作为类型


    错误做法: 使用 any 类型处理不确定的数据结构。



    正确做法: 使用 unknown 类型。

    原因: any 禁用类型检查,可能导致错误难以捕获。



    4. 使用 val as SomeType


    错误做法: 强制编译器推断类型。


    正确做法: 使用类型守卫。

    原因: 类型守卫确保所有检查都是明确的,减少潜在错误。



    5. 在测试中使用 as any


    错误做法: 在测试中创建不完整的替代品。


    正确做法: 将模拟逻辑移到可重用的位置。

    原因: 避免在多个测试中重复更改属性,保持代码整洁。



    6. 可选属性


    错误做法: 将属性定义为可选。


    正确做法: 明确表达属性的组合。

    原因: 更明确的类型可以在编译时捕获错误。



    7. 单字母泛型


    错误做法: 使用单字母命名泛型。


    正确做法: 使用描述性的类型名称。

    原因: 描述性名称提高可读性,便于理解。



    8. 非布尔判断


    错误做法: 直接将值传递给 if 语句。
    转存失败,建议直接上传图片文件


    正确做法: 明确检查条件。

    原因: 使代码逻辑更清晰,避免误解。



    9. 感叹号操作符


    错误做法: 使用 !! 将非布尔值转换为布尔值。



    正确做法: 明确检查条件。

    原因: 提高代码可读性,避免混淆。



    作者:叶知秋水
    来源:juejin.cn/post/7451586771781861426
    收起阅读 »

    低成本创建数字孪生场景-数据篇

    web
    众所周知在做数字孪生相关项目的时候,会划出相当一部分费用会用于做建模工作,然而有一天老板跑过来告诉我,由于客户地处于偏远经济欠发达地区,没有多少项目经费,因此不会花很大的价钱做建模,换言之如果这个项目连建模师都请不起了,阁下该如何应对。 常规的场景建模方式无非...
    继续阅读 »

    众所周知在做数字孪生相关项目的时候,会划出相当一部分费用会用于做建模工作,然而有一天老板跑过来告诉我,由于客户地处于偏远经济欠发达地区,没有多少项目经费,因此不会花很大的价钱做建模,换言之如果这个项目连建模师都请不起了,阁下该如何应对。


    常规的场景建模方式无非就是CAD、激光点云辅助、倾斜摄影建模,人工建模的成本自不必说,几百平方公理的场景如果用无人机倾斜等等建模也是一笔不小的开销,在客户对场景还原度和模型精细度不高的前提下,最省成本的办法还是尽肯能让程序自动建模。经过几天摸索,我终于找到一些稍微靠谱的应对方法,本篇着重讲述如何获取场景的数据,以及处理为展示所需的数据格式。


    Guanlian-cesium3.gif


    准备工作


    使用的工具


    工具用途成本
    水经注获取全国GIS数据部分免费
    地理空间数据云获取地形高程图免费
    QGIS空间数据编辑开源
    cityEingine自动生成建筑和道路模型部分免费
    CesiumLab3转换地理模型部分免费
    CesiumJS3D地图引擎,用于做最终场景展示开源

    QGIS(全称Quantum GIS)是一个开源的地理信息系统, 提供了一种可视化、编辑和分析地理数据的途径。它支持各种矢量、栅格和数据库格式,并且支持丰富的GIS功能,包括地图制作、空间数据编辑、地图浏览等。


    CesiumLab3是一款强大的3D地理信息数据处理软件,可以将各种类型的数据,如点云、模型、影像、地形等,转换为3D Tiles格式,以便在CesiumJS中可视化。此外,CesiumLab3还提供了一套完整的数据管理和分发服务,使得大规模3D地理信息数据的存储、管理和服务变得更加便捷。


    CesiumJS是一款开源的JavaScript库,它用于创建世界级的3D地球和地图的Web应用程序。无论是高精度的地形、影像,还是高度详细的3D模型,CesiumJS都可以轻松地将它们集成到一个统一的地理空间上下文中。该库提供了丰富的接口和功能,让开发者可以轻松实现地理信息可视化、虚拟地球等应用。


    基础数据


    图层数据形态数据格式文件格式
    卫星影像图片栅格TIF
    水域分布多边形矢量SHP
    建筑面轮廓多边形矢量SHP
    植被分布散点矢量SHP
    地形高程图图片栅格TIF

    数据处理


    1. 影像和地形


    带有地形信息的卫星影像作为底图可以说是非常重要,所有其他图层都是它的基础上进行搭建,因此我们必须尽可保证在处理数据时的底图和最终展示时使用的底图是一致的,至少空间参考坐标系必须是一致的。


    由于在后面步骤用到的cityEgine工具可选坐标系里没有ESPG43226,这里使用EPSG32650坐标系。


    在编辑过程中需要用到TIF格式的卫星底图,可以使用QGIS对栅格图层,图层的数据在平面模式下进行编辑,主要工作是对齐数据、查漏补缺。


    地形处理步驟如下:



    1. 打开地理空间数据云,登录并进入“高级检索”

    2. 设置过数据集和过滤条件后会自动返回筛选结果,点击结果项右侧的下载图标即可下载。
      Untitled.png

    3. 在 QGIS 中打开菜单栏 Raster > Miscellaneous > Merge,并将下载的高程文件添加到 Input layers 中。
      Untitled 1.png

    4. 可以使用 Processing Toolbox 中的 Clip raster by mask layer 工具来裁剪高程图层,处理好之后导出TIF格式备用


      Untitled 2.png


    5. 使用esiumlab3做地形转换,在这里会涉及到转换算法的选择,VCG算法适合用于小范围,对精度要求高的地形;CTD算法则适合用于大范围,对精度要求低的地形。根据具体情况选择即可。地形具体步骤看 高程地形切片和制作水面效果


      Untitled 3.png


      Untitled 4.png



    2. 建筑轮廓


    建筑轮廓通常为矢量数据,在2D模式下它就是由多个内封闭多边形组成的图层,而在3D模式下可以对这些多边形进行挤压形成体力几何体,并在此基础上做变形和贴图,形成大量城市建筑的视觉效果。


    这部分数据很难在互联网上拿到,即使有免费的渠道(比如OMS下载),数据也是较为老旧的或者残缺的,有条件的话建议让业主提供或者购买水经注VIP会员下载。即使这样,我们还需要使用QGIS等工具进行二次编辑。


    操作步骤如下:



    1. 在QGIS处理数据,对数据进行筛选、补充、填充基本信息等处理,导出SHP格式数据,建议将卫星影像和地形底图一并导出,用于做模型位置对齐。为方便后面的自动化建模,需要将每个建筑面的基本信息(比如名称和建筑高度)录入到每个多边形中,如果对建筑高度要求没有那么精细也可以用QGIS自带的方法随机生成高度数值。
      Untitled 5.png

    2. 在cityEngine中新建工程(存放各种数据、图层、规则文件)和场景(从工程中抽取素材搭建场景)

    3. 使用cityEngine进行自动程序化建模,有了建筑面轮廓和高度,就可以直接使用规则生成建筑了,建筑的风格也是可以灵活配置的,比如商业区住宅区,或者CBD风格城乡结合部风格等等。 具体操作看 CityEngine-建筑自动化程序建模

      Guanlian-cityEngine1.gif


      Untitled 6.png


    4. 将模型导出为FBX,并使用cesiumlab3转换为3Dtiles ,具体操作见 常见3D模型转3Dtiles文件。这里要注意空间参考必须设置为与在QGIS处理数据时的坐标系一致,这里有个坑的地方就是非授权(收费)用户是无法使用ENU之外的其他坐标系将FBX或OBJ文件转为3DTiles的,只能用SHP转出3DTiles白模。这就是为什么很多人反映导出3DTiles后放到地图里又对不上的原因。
      Untitled 7.png


    3. 绿地和植被图层


    绿地和植被图层为我们展示了特定区域内的自然或人工绿地及植被分布情况,包括公园、森林、草地、田地等各类植被区域。获取数据通常可以通过遥感卫星图像,或者通过地面调查和采集。这些数据经过处理后,可以以矢量或栅格的形式在GIS中进行展示和分析。


    在本文案例中为了制作立体茂密树林的视觉效果,我们将植被图层数据转换为LOD图层,即在不同的地图缩放尺度下有不同的细节呈现。基本原理就是在指定的范围内随机生成一个树模型的大量实例操作步骤如下:



    1. 获取植被区域多边形,使用QGIS通过数据导入或手绘的方式得到植被的覆盖区域,可以給区域增加一些快速计算的属性,比如area
      Untitled 8.png

    2. 在覆盖区域内生成随机分布点,调整点的数量和密度达到满意状态即可

      Untitled 9.png

    3. 如有需要,可以手动调整随机生成的点数据,确认无误后导出文件shp
      Untitled 10.png

    4. 准备好带LOD的树模型(cesiumlab3\tools\lodmodel\yangshuchun有自带一个示例模型可用)和地形高程信息文件(.pak格式)
      Untitled 11.png

    5. 使用cesiumlab3创建实例模型切片,具体的流程可以看这里 CesiumLab3实例模型切片

      Untitled 12.png


    以上方法适合创建模型单一、更新频度低、且数据量巨大的模型图层,比如树木、城市设备如路灯、垃圾桶、井盖等等。


    4. 水域分布


    地理上水域包括湖泊、河流、海洋和各种人工水体,在专业领域项目水域的分布对研究环境生态有重大意义,而比较通用的场景就是跟卫星影像、地形底图结合展示,我们同样需要矢量数据绘制多边形,并加上动态材质出效果。


    由于最终展示的地图引擎cesium自带水域材质效果,这里的操作也变得简单,只要把水域多边形获取到手就行:



    1. 打开QGIS,导入从水经注下载的水域数据或者对着卫星影像地图手动绘制水域数据,导出为shp文件格式

    2. 在cesiumlab3 生成地形切片,在cesium里,水域是作为地形的一部分进行处理的,所以将地形高程图tif文件和水域shp文件一起上传处理即可。具体步骤看 高程地形切片和制作水面效果

      Untitled 13.png


    组合数据


    至此数据篇就介绍完了,由于cesiumlab3自带分发服务,我们可以直接在上面新建一个场景,将上文生成的数据图层组合到一个场景里作展示。另外还可以测试一些场景效果比如天气、轮廓、泛光等等,还挺有意思的。后续的单模型加载、可视化图层加载、鼠标事件交互等等就留在开发篇吧,今天就先这样。



    1. 叠加地形、建筑白模、植被实例切片图层


      Guanlian-cesium1.gif


    2. 测试建筑模型积雪效果


      Guanlian-cesium2.gif



    相关链接


    cesiumlab软件设置3dtiles模型位置


    在Cesium中种植大量树木


    地形和水面处理


    植被3DTiles处理


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

    wasm真的比js快吗?

    web
    一. 前言 首先提一句话,本人是Rust新手!如果有什么不足的地方麻烦指出哈! 最近一直在玩Rust(摸鱼),本来是想着,多学一点,多练一练,之后把我们这边的一些可视化项目里面核心的代码用Rust重构一下。但是我最近在练习一个demo的时候,发现了跟我预期不一...
    继续阅读 »

    一. 前言


    首先提一句话,本人是Rust新手!如果有什么不足的地方麻烦指出哈!


    最近一直在玩Rust(摸鱼),本来是想着,多学一点,多练一练,之后把我们这边的一些可视化项目里面核心的代码用Rust重构一下。但是我最近在练习一个demo的时候,发现了跟我预期不一样的地方。


    WechatIMG68186.png


    具体如何,我用下面几个案例展开细说。


    二. 案例1: 使用canvas绘制十万个不同颜色的圆


    首先我想到的是,把canvas的复杂图像绘制功能用Rust重写一下。这里我用canvas绘制大量的圆形为例子。


    2.1 Rust绘制canvas


    跟上一篇文章的流程差不多,我们需要先新建一个Rust项目:


       cargo new canvas_circel_random --lib

    然后更新一下Cargo.toml文件里面的依赖内容:


    [package]
    name = "canvas_circle_random"
    version = "0.1.0"
    edition = "2021"

    [lib]
    crate-type = ["cdylib"]

    [dependencies]
    wasm-bindgen = "0.2"
    js-sys = "0.3"
    web-sys = { version = "0.3", features = ["HtmlCanvasElement", "CanvasRenderingContext2d"] }

    完成之后,我们简单在src/lib.rs写一点代码:


    // 引入相关的依赖
    use wasm_bindgen::prelude::*;
    use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
    use js_sys::Math;

    // 给js调用的方法
    #[wasm_bindgen]
    pub fn draw_circles(canvas: HtmlCanvasElement) {
    // 获取ctx绘画上下文
    let context = canvas.get_context("2d").unwrap().unwrap().dyn_int0::<CanvasRenderingContext2d>().unwrap();
    let width = canvas.client_width() as f64;
    let height = canvas.client_height() as f64;

    // 循环绘制
    for _ in 0..100_0000 {
    // 设置一下写x,y的位置
    let x = Math::random() * width;
    let y = Math::random() * height;
    let radius = 10.0;
    let color = format!(
    "rgba({}, {}, {}, {})",
    (Math::random() * 255.0) as u8,
    (Math::random() * 255.0) as u8,
    (Math::random() * 255.0) as u8,
    Math::random()
    );
    draw_circle(&context, x, y, radius, &color);
    }
    }

    fn draw_circle(context: &CanvasRenderingContext2d, x: f64, y: f64, radius: f64, color: &str) {
    // 调用canvas的API绘制
    context.begin_path();
    context.arc(x, y, radius, 0.0, 2.0 * std::f64::consts::PI).unwrap();
    context.set_fill_style(&JsValue::from_str(color));
    context.fill();
    context.stroke();```

    }


    简单解释一下代码:



    • 0..100_0000 创建了一个从 0 开始到 999,999 结束的范围注意,Rust 的范围是左闭右开的,这意味着它包含起始值但不包含结束值。

    • &JsValue::from_str(color)从变量中取值。


    完成之后,我们去打包一下。


    wasm-pack build --target web     

    然后我们得到一个pkg包,如下图:


    image.png


    然后我们在项目中引入一下,具体流程可以看我上一篇文章。
    回到我们的Vue项目中,我们引入一下:


    import init, { draw_circles } from 'canvas_circle_random/canvas_circle_random'
    onMounted(async () => {
    await init();
    const begin = new Date().getTime();
    drawWasmCircle();
    const end = new Date().getTime();
    console.log('wasm cost time: ' + (end - begin) + 'ms');
    })

    之后我们打开一下页面:


    image.png


    多次加载了几次,加载范围大概在2750ms~2900ms之间。


    2.2 使用js绘制canvas


    const drawJsCircle = () => {
    const canvas = document.getElementById('my-canvas') as HTMLCanvasElement;
    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
    for (let i = 0; i < 1000000; i++) {
    drawRandomCircle(ctx, 800, 600);
    }
    }

    const drawRandomCircle = (ctx: CanvasRenderingContext2D, width: number, height: number) => {
    const radius = 10;
    const x = Math.random() * (width - 2 * radius) + radius;
    const y = Math.random() * (height - 2 * radius) + radius;
    const color = `rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.random().toFixed(2)})`;
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
    ctx.fillStyle = color;
    ctx.fill();
    ctx.stroke();
    }


    没什么好说的,有手就会。


    然后我们在页面上试一下:


    image.png
    加载范围大概在1950ms~2200ms之间。


    卧槽,难道说js的性能比wasm快???


    然后我又对绘制的数量和绘制的形状做了多次实验:



    • 绘制10000个圆, wasm用时大概在1000ms,js用时大概在700ms。

    • 绘制100000个长方形wasm用时大概在1700ms, js用时在1100ms。


    无一例外,在绘制canvas上面,js的性能确实优于wasm


    三. 案例2:算法性能


    考虑到通过canvas绘制图形来判断性能,有点太前端化了,我想可不可以通过写一些算法来做一下性能的比较。
    试了很多算法,这里我用一下斐波那契算法,比较简单也比较有代表性。


    在同级目录下新建一个Rust项目:


        cargo new fb-lib --lib

    然后在fb-lib中修改一下Cargo.toml


    [package]
    name = "fb-lib"
    version = "0.1.0"
    edition = "2021"

    [lib]
    crate-type = ["cdylib", "rlib"]

    [dependencies]
    wasm-bindgen = "0.2"

    把斐波那契数列代码写到src/lib.rs文件中:


    use wasm_bindgen::prelude::*;

    #[wasm_bindgen]
    pub fn fb_wasm(n: i32) -> i32 {
    if n <= 1 {
    1
    }
    else {
    fb_wasm(n - 1) + fb_wasm(n - 2)
    }
    }

    很简单,没什么好说的。完成之后,我们在项目中使用一下。


    <script setup lang="ts">
    import init, { fb_wasm } from 'fb-lib/fb_lib'
    import { onMounted } from 'vue';

    onMounted(async () => {
    await init();
    const begin = new Date().getTime();
    fb_wasm(42);
    const end = new Date().getTime();
    console.log('wasm cost time: ' + (end - begin) + 'ms');
    })

    </script>

    image.png


    大概试了一下时间在1700ms~1850ms左右。


    然后我们用js实现一下:代码如下:


    import init, { fb_wasm } from 'fb-lib/fb_lib'
    import { onMounted } from 'vue';

    onMounted(async () => {
    await init();
    const begin = new Date().getTime();
    fn_js(42);
    const end = new Date().getTime();
    console.log('js cost time: ' + (end - begin) + 'ms');
    })

    const fn_js = (n: number): number => {
    if (n <= 1) {
    return 1;
    } else {
    return fn_js(n - 1) + fn_js(n - 2);
    }
    }

    然后我们在页面上看一下:


    image.png


    大概试了一下时间在2550ms~2700ms左右。


    很明显,这时的wasm的性能是要优秀于js


    四. 总结


    大概试了一下canvas,dom操作,高性能算法(排序、递归)等。我大概得出了一个这样的结论:



    • Wasm代码比JavaScript代码更加精简,因此从网络上获取Wasm代码的速度更快。

    • 对于一些高性能的算法,在基数足够大的情况下,wasm的性能确实高于js,但是当基数比较小的时候,两者其实差不多。

    • 由于Wasm是一种二进制格式,需要将DOM操作的数据进行转换,才能在Wasmjs之间进行传递。这个数据转换过程可能导致额外的开销。以及 Wasm操作DOM时,需要通过js提供的API进行通信。每次调用js的API都会引入一定的开销,从而影响性能。所以在一些页面交互操作上,wasm的性能并不会优于js


    综上,个人觉得wasmjs之间是一种互相选择互相依靠的关系,并不是取代的关系。日常开发中,也要结合实际情况选择不同的方式进行开发。


    往期文章:前端仔,用rust来写canvas


    作者:尝尝你的优乐美
    来源:juejin.cn/post/7444450769488674825
    收起阅读 »

    8年前端,那就聊聊被裁的感悟吧!!

    前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。 另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的...
    继续阅读 »

    前端开发,8年工作经验,一共呆了2家公司,一个是做积分兑换的广告公司。这是一个让我成长并快乐的公司,并不是因为公司多好,而是遇到了一群快乐的朋友,直到现在还依旧联系着。
    另一家是做电子签名的独角兽,我人生的至暗时刻就是在这里这里并不是说公司特别不好,而是自己的际遇


    我的经历


    第一家公司


    第一家公司说来也巧,本来是准备入职一家外包的,在杭州和同学吃个饭,接到了面试通知,一看地址就在楼上,上去一共就3轮面试,不到2个小时直接给了offer。有些东西真的就是命中注定


    第二家公司


    第二家公司我入职以后挖了上家公司诸多墙角,我一共挖了6个前端,2个后端。拯救朋友们于水深火热之中。





    我本以为我能开启美好的新生活,结果第二年就传来我父亲重病的噩耗 肺癌晚期,我学习了大量的肺癌知识什么小细胞,非小细胞,基因检测呀等等。。。





    可是最后还是没有挽留住他的生命,我记得我俩在最后一次去武汉的时候,睡在一起,他给我说了很多。


    他说:治不好就算了,只是没能看到自己的孙子有些可惜罢了。

    他说:我这一辈碌碌无为,没给你带来多么优越的条件,结婚、买房、工作都没给到任何帮助,唯一让我感到欣慰的是你那么努力,比我强多了,家里邻居很多都眼馋你呢。
    他说:你小孩的名字想好了吗?你媳妇真是个孝顺的孩子,性格也好,心地善良,你要好好对待她。

    他说了很多。。。我都快忘了他说了啥了,我不想忘来着,可是可是,想起来就又好难过。


    这只是我人生历程的一部分,我把这些讲出来,是为了让大家明白,你现在所经历的困苦其实没有那么严重,人在逆境之中会放大自己的困难,以博得同情。所以现在很多人给我倒苦水的时候,我总有点不屑一顾的感觉,并不是我有多强,我只是觉得都能过去。


    在灰暗的时候,工作总是心不在焉,情绪莫名冲动,我和领导吵过架,和ui妹妹撕破脸,导致人家天天投诉我。我leader说我态度极其嚣张,我说你再多说一句,我干死你所以不裁我裁谁


    我的人生感悟


    我时常以我爸的角度换位思考,我在得知这个消息后我该咋办?是积极面对,还是放弃治疗?可是所有的都是在假设的前提之下,一切不可为真。只有在其中的才最能明白其中的感受。
    那一年我看着他积极想活着的毅力,也看到了他身体日渐消瘦的无奈,无奈之余还要应付各种亲戚的嘘寒问暖


    我现在很能明白《天道》中那段,丁元英说的如果为了孝顺的名声,让父亲痛苦没有尊严地活着,还不如让父亲走了。 的意思了。在他昏迷不醒的时候,大小便失禁的时候,真不如有尊严的走了。


    我其实已经预感到自己要被裁,我原本是挺担心的,可是后来想想父亲的话,我总结成一句话圆滑对事,诚以待人。 这句话看上去前后矛盾,无外乎俩个观点。


    圆滑对事的意思是:就是要学会嘴甜,事嘛能少干就少干,能干几分是几分,累的是别人,爽的是自己,在规则中寻求最大的自我利益化。


    诚以待人的意思是:圆滑归圆滑,不能对谁都圆滑,你得有把事情办的很好的能力,你需要给真正需要的人创造价值,而不是为了给压榨者提供以自我健康为代价的价值。



    用现在最流行的词来说就是「佛系」。


    什么叫活明白了,通常被理解为不争不抢,得之淡然、失之泰然、顺其自然的一种心理状态。


    活明白的人一般知道自己要什么样的生活,他们不世故、不圆滑,坦荡的、磊落的做自己应该做的事儿。他们与社会上潜规则里的不良之风格格不入,却不相互抵触,甚至受到局中人的青睐与欣赏。


    活明白的人看着更为洒脱,得不张扬,失不气馁,心态随和、随遇而安。


    不过,还有一种活明白的人,不被多数人所接受。他们玩世不恭、好吃懒做,把所有一切交给命运去背锅。这种人极度自我,没有什么可以超越他自己的利益,无法想象这种活法,简直就是在浪费六道轮回的名额。


    总之,有的人活明白了,是调整自己的心态,维护社会的稳定和安宁。有的人活明白了,是以自我为中心,一边依赖着社会救济,一边责备社会龌蹉。


    所以,活明白的人也分善与恶,同样是一种积极向善,另一种是消极向恶,二者同出而异名。



    我对生活的态度


    离职的第一个月,便独自一人去了南京,杭州,长沙,武汉,孝感。我见了很多老朋友,听听他们发发牢骚,然后找一些小众的景点完成探险。


    在南京看了看中医,在杭州露营看了看日落,在长沙夜爬了岳麓山,在武汉坐了超级大摆锤,在孝感去了无名矿坑并在一个奶奶家蹭了中午饭。


    我的感受极其良好,我体验了前所未有生活态度,我热情待人,嘻嘻笑笑,我站在山顶敞怀吹风,在无尽的树林中悠然自得,治愈我不少的失落情绪。我将继续为生活的不易奔波,也将继续热爱生活,还会心怀感恩对待他人,也会圆滑处事 事事佛系。


    背景1.png


    图层 1.png


    IMG_6214.JPG


    IMG_6198.JPG


    IMG_6279.JPG


    可能能解决你的问题


    要不要和家里人说


    我屏蔽了家里人,把负面情绪隐藏,避免波及母亲本就脆弱的内心世界,我还骗她说公司今年不挣钱,提前让我们放假,只给基础工资。如果你家境殷实,家庭和睦,我建议大方的说,这样你和父母又多了一个可以聊的话题,不妨和他们多多交流,耐心一些。


    裁员,真不是你的问题


    请记住,你没有任何问题,你被裁员是公司的损失,你不需要为此担责,你需要做的是让自己更强,不管是心理、身体还是技术,你得让自己变得精彩,别虚度了这如花般的时光。可能你懒,可能也没什么规划,那就想到啥就做啥好了,可能前几次需要鼓足干劲,后面就会发现轻而易举。


    如何度过很丧的阶段


    沮丧需要一个发泄的出口,可以保持运动习惯,比如日常爬楼梯、跑步等,一场大汗淋漓后,又是一个打满鸡血积极向上的你。

    不要总在家待着,要想办法出门,多建立与社会的联系,多和朋友吹吹牛逼,别把脸面看的那么重要,死皮赖脸反而是一种讨人喜欢的性格。



    不管环境怎样,希望你始终向前,披荆斩棘

    如果你也正在经历这个阶段,希望你放平心态,积极应对

    如果你也在人生的至暗时刻,也请不要彷徨,时间总会治愈一切

    不妨试试大胆一点,生活给的惊喜也同样不少

    我在一个冬天的夜晚写着文字,希望能对你有些帮助



    进群聊:技术、段子我都有





    群已满,请加15757172732入群


    作者:顾昂_
    来源:juejin.cn/post/7331657679012380722
    收起阅读 »

    原来科技感的三维地图可以这么简单实现

    web
    前言 2024.02.20 下午摸鱼时接到一个客户的数字孪生项目的需求,客户什么也没说,就要求“炫酷”和“科技感”地图,还要把他们的模型都放上去,起初我以为又是一个可视化大屏的项目,准备用高德地图应付过去,然后他们又在群里发什么要求高之类的,我们的数据种...
    继续阅读 »

    前言


    Screen-2023-06-15-135413_0001.gif



    2024.02.20



    下午摸鱼时接到一个客户的数字孪生项目的需求,客户什么也没说,就要求“炫酷”和“科技感”地图,还要把他们的模型都放上去,起初我以为又是一个可视化大屏的项目,准备用高德地图应付过去,然后他们又在群里发什么要求高之类的,我们的数据种类多,说什么高德、百度、Mapbox、Cesium之类框架都试过了,满足不了需求,好嘛,这下给我犯了难了,会的技术栈全都给我排除了, 手撸Three.js源码我可不干,于是就在网上晃悠,尝试找一些灵感



    2024.02.24



    又试了几个地图框架,还是不行,被客户和经理催了一顿,再不行他们要换人了



    2024.02.28



    在QQ群里闲逛,发现了群友发的一个叫 Mapmost SDK for WebGL 的地图框架,于是抱着试一试的态度做了一下,好家伙,一下就对客户味了


    image.png



    2024.03.04



    后面我发现这个SDK蛮有意思,于是把我实现客户需求的过程记录下来,分享给大家


    初始化


    这个SDK看似是个商用软件,不过目前是免费试用,官网上申请一下就行了,然后按照他们的文档,填一下参数,就能初始化一个地图,和一般的地图SDK用法差不多


    image.png


    image.png


        <script src ='https://delivery.mapmost.com/cdn/sdk/webgl/v3.5.0/mapmost-webgl-min.js'></script>
    <script>
    let mapmost = window.mapmost
    /*
    * 初始化地图
    */

    let map = new mapmost.Map({
    container: 'map', //地图容器
    style:'http://192.168.126.44/mms-style/darkMap.json', //矢量底图
    center: [120.74014004382997, 31.32975410974069], //地图中心点
    bearing: 50.399999999999636, //方位
    pitch: 78.99999999999993, //倾斜角
    zoom: 19.964625761228117, //缩放
    userId: '***',

    })
    </script>



    image.png


    不过,在此之前,要把底图中的矢量建筑图层隐藏掉,客户要加载真正的建筑三维模型。


    代码和效果图如下:


    const buildingLayers = [
    'buildings-di',
    'buildings-faguang-1',
    'buildings-faguang-2',
    'buildings-faguang-3',
    'buildings-high',
    'buildings-high-top',
    'buildings-low',
    'buildings-low-top',
    'buildings-lowmid',
    'buildings-lowmid-top',
    'buildings-mid',
    'buildings-mid-top',
    'buildings-midhigh',
    'buildings-midhigh-copy',
    ]

    map.on('load', (e) => {
    buildingLayers.forEach((layer, index) => {
    let layerObj = map.getLayer(layer)
    map.setLayoutProperty(layerObj.id, 'visibility', 'none');
    })
    })


    image.png


    加载建筑三维模型


    这里我们准备了城市建筑的模型,格式为glb,按照文档描述,我们添加一个idmodelLayer的三维图层,代码和效果图如下


    //...

    /*
    * 加城市f载建筑模型
    */


    let Gr0up = null
    let Layer = null

    let models = ["./model/YQ.glb"].map(item => ({
    type: 'glb',
    url: item
    }));

    map.on('load',(e) => {
    let modelOptions = {
    id: 'modelLayer',
    type: 'model',
    models: models,
    sky: "./sky/satara_night_no_lamps_2k.hdr",
    exposure: 2.4,
    center: [120.74155610348487, 31.328251735532746, 0],
    callback: (group, layer) => {
    Gr0up = group
    Layer = layer
    }
    };

    map.addLayer(modelOptions);
    })


    image.png


    image.png


    添加三维效果


    接下来就是客户的G点了,为三维场景添加特效:


    添加建筑流光渐变效果


    参考SDK的接口文档,给建筑加上流光渐变的效果


    image.png


    image.png


    定义一个添加特效的函数addModelEffect,然后按照文档上的参数说明来配置相关属性


     const  addModelEffect = () =>{
    Layer.addModelEffect(Gr0up, [{
    type: "gradient",
    startColor: "rgba(63, 177, 245,.5)",
    endColor: "rgba(58, 142, 255,.8)",
    opacity: 0.8,
    percent: 0.5
    }, {
    type: "flow",
    speed: 1,
    color: "rgba(241, 99, 114, .4)",
    opacity: 0.8,
    percent: 0.05
    }])
    Gr0up.addFrame(0x3FB1F5);

    }


    然后我们在模型加载完成后调用这个函数:



    //...

    map.on('load',(e) => {
    let modelOptions = {
    id: 'modelLayer',
    type: 'model',
    models: models,
    sky: "./sky/satara_night_no_lamps_2k.hdr",
    exposure: 2.4,
    center: [120.74155610348487, 31.328251735532746, 0],
    callback: (group, layer) => {
    Gr0up = group
    Layer = layer
    addModelEffect()
    }
    };

    map.addLayer(modelOptions);
    })

    效果如下:


    image.png


    image.png


    添加粒子飞线


    同样,在SDK的文档上找到添加流动线的接口,定义一个addFlowLine的函数,然后按照要求配置参数:



    这里我们借助了一个生成贝塞尔曲线的函数,以及一些随机的坐标点位数据。
    他们的作用是为我们提供必要的模拟数据



    import { getBSRPoints } from './bezierFunction.js'
    import { flowLineData } from './flowLineData.js'

    //...

    const addFlowLine = () => {
    //生成贝塞尔曲线测试数据
    let data_trail1 = getBSRPoints(120.71541557869517, 31.316503949907542,
    120.73787560336916, 31.321925190347713, 800);
    let data_trail2 = getBSRPoints(120.71541557869517, 31.316503949907542,
    120.72619950480242, 31.33360076088249, 1500);
    let data_trail3 = getBSRPoints(120.71541557869517, 31.316503949907542,
    120.69933418653403, 31.332725809914024, 900);

    [data_trail1, data_trail2, data_trail3].map(data => {
    Layer.addFlowLine({
    type: "trail",
    color: '#1ffff8',
    speed: 4,
    opacity: 0.9,
    width: 8,
    data: {
    coordinate: data
    }
    });
    })

    flowLineData.map(data => {
    Layer.addFlowLine({
    type: "flow",
    color: '#ff680d',
    speed: 4,
    opacity: 1,
    percent: 0.08,
    gradient: 0.02,
    width: 5,
    data: {
    coordinate: data
    }
    });
    })
    }


    同样,我们在模型加载完成后调用这个函数:



    //...

    map.on('load',(e) => {
    let modelOptions = {
    id: 'modelLayer',
    type: 'model',
    models: models,
    sky: "./sky/satara_night_no_lamps_2k.hdr",
    exposure: 2.4,
    center: [120.74155610348487, 31.328251735532746, 0],
    callback: (group, layer) => {
    Gr0up = group
    Layer = layer
    addModelEffect()
    addFlowLine()
    }
    };

    map.addLayer(modelOptions);
    })

    效果图如下:


    image.png


    image.png


    添加特效球


    类似的,文档上的添加特效球的接口,给场景里添加两个”能量半球“


    image.png


    代码和效果图如下:



    const addSphere = () => {
    let sphere = Layer.addSphere({
    color: "rgb(53, 108, 222)",
    radius: 3300, //半径,单位米,默认为10米
    segment: 256, //构成圆的面片数
    phiLength: Math.PI,
    speed: 3,
    opacity: 1,
    center: [120.67727020663829, 31.31997024841401, 0.0]
    });

    let sphere2 = Layer.addSphere({
    color: "rgb(219, 74, 51)",
    radius: 2300, //半径,单位米,默认为10米
    segment: 256, //构成圆的面片数
    phiLength: Math.PI,
    speed: 6,
    opacity: 1,
    center: [120.67727020663829, 31.31997024841401, 0.0]
    });
    }


    //...

    map.on('load',(e) => {
    let modelOptions = {
    id: 'modelLayer',
    type: 'model',
    models: models,
    sky: "./sky/satara_night_no_lamps_2k.hdr",
    exposure: 2.4,
    center: [120.74155610348487, 31.328251735532746, 0],
    callback: (group, layer) => {
    Gr0up = group
    Layer = layer
    addModelEffect()
    addFlowLine()
    addSphere()
    }
    };
    map.addLayer(modelOptions);
    })


    image.png


    环境效果调优


    仔细看整个环境,发现天是白的,和整体环境不搭配


    image.png


    更改一下地图初始化时的参数,将天空设置为暗色:


    let map = new mapmost.Map({
    container: 'map', //地图容器
    style: 'http://192.168.126.44/mms-style/darkMap.json', //矢量底图
    center: [120.74014004382997, 31.32975410974069], //地图中心点
    bearing: 60.399999999999636, //方位
    pitch: 78.99999999999993, //倾斜角
    zoom: 14.964625761228117, //缩放
    sky: 'dark' //天空颜色
    })


    然后整体效果如下:


    image.png


    如果觉得场景本身太亮,可以降低添加模型时的曝光度:



    let modelOptions = {
    exposure: .4,
    callback: (group, layer) => {
    //...
    }
    };


    这样整体环境就会偏暗一点,更有黑夜下的赛博朋克城市的味道


    image.png


    当然,在这我又换了一张更暗的底图:


    image.png


    最后,再调整一下特效球的半径和位置,行了,这就是客户喜欢的样子,哈哈哈,2小时搞定,而且不用手撸Three.js代码:


      const addSphere = () => {
    let sphere = Layer.addSphere({
    color: "rgb(53, 108, 222)",
    radius: 3300, //半径,单位米,默认为10米
    segment: 256, //构成圆的面片数
    phiLength: 180,
    speed: 3,
    opacity: 1,
    center: [120.67943361712065, 31.306450929918768]
    });

    let sphere2 = Layer.addSphere({
    color: "rgb(219, 74, 51)",
    radius: 2300, //半径,单位米,默认为10米
    segment: 256, //构成圆的面片数
    phiLength: 180,
    speed: 6,
    opacity: 1,
    center: [120.67727020663829, 31.31997024841401, 0.0]
    });
    }


    image.png


    总结


    仔细看,不难发现,这个SDK集成了 Mapbox 和 Three.js 的核心功能,主打的是一个高颜值的三维地图引擎,当然除了好看之外,其他地图框架该有的功能它也具备,只是官网给人的感觉过于粗糙,不够吸引人;另外产品试用的门槛有些高,希望后面能优化吧


    image.png


    image.png


    作者:清风夜半
    来源:juejin.cn/post/7342279484488138802
    收起阅读 »

    盘点一下用了这么长时间遇到的Wujie 无界微前端的坑

    web
    目前也用无界微前端很长时间了,当时选择无界没有选择乾坤的原因就是无界的保活模式更加丝滑,客户就想要这种方式,但是在这段过程也遇到了很多问题,甚至有些就是无界目前解决不了的问题,所以希望大家今后遇到了也能提前避免 已经解决的问题 1、子应用使用wangEdit...
    继续阅读 »

    目前也用无界微前端很长时间了,当时选择无界没有选择乾坤的原因就是无界的保活模式更加丝滑,客户就想要这种方式,但是在这段过程也遇到了很多问题,甚至有些就是无界目前解决不了的问题,所以希望大家今后遇到了也能提前避免



    已经解决的问题


    1、子应用使用wangEditor 在主应用中无法编辑 ,无法粘贴,工具栏无法正常使用


    ✨问题复现



    • 子项目中正常


    子项目的wangEditor.gif



    • 主项目无法选中,无法粘贴


    主项目的wangEditor.gif


    ✨出现这种问题的原因:



    1. 子应用运行在 iframe内,dom在主应用的shadowdom中,当选中文字时,在主应用监听selectionchange,并且通过 document.getSelection()获取选中的selection,在wangEditor中 会判断这个 selection instanceof window.Selection,很明显主应用的selection 不可能是 iframe 里面window Selection的实例,所以出现了问题

    2. shadowDom 大坑,在shadowDom中 Selection.isCollapsed永远为true,相当于永远没有选中,所以只能修改 wangEditor 的代码,让读取 isCollapsed 修改成 baseOffset 和 focusOffset的对比,就知道是否选中了文字了


    ✨解决方案


    1、将 wangeditor 替换成 wangEditor-next,因为wangeditor 作者大大已经说因为种种原因后面不会再继续更新了,所以担心更新的同学可以使用 wangEditor-next 这个还有人在继续更新


    2、直接替换一个富文本组件 vue-quill,我使用的就是这个,因为我们项目对富文本的需求没有那么重,只要能用就行,所以你也可以替换一个在无界中没有问题的富文本组件


    3、由此我们知道了在无界中getSelection是有问题的 如果遇到了可以用这个插件尝试解决 wujie-polyfill.github.io/doc/plugins…


    2、子应用使用vue-office 的 pdf 预览,在主应用中白屏


    ✨问题复现



    • 子应用 正常显示


    pdf-1.jpg



    • 主应用直接加载不出来


    pdf-2.jpg


    ✨解决方案:


    直接换个轮子,因为vue-office 源码不对外开放,你根本不知道是它内部做了何种处理,所以最好的办法就是直接换个其他的能够预览pdf 的方式,我这边选择的是 kkfile 的方式,kkfile 不仅可以预览pdf 还可以预览很多其他的格式的文件,让后端去生成预览地址 然后前端直接用 iframe 去打开即可


    3、开发环境下 vite4 或者 vite5 子应用的 element-plus的样式丢失或者自己写的:root 不生效


    ✨问题复现



    • 子应用正常


    css.jpg



    • 主应用样式丢失


    css2.jpg


    ✨出现这种问题的原因:


    主要的原因是因为子应用的 :root 失效了,因为无界中是将:root 转成了:host ,但是如果你是在main.js 中外链的样式


    import 'element-plus/dist/index.css'

    这样的话无界将无法劫持到将 :root 转成:host
    css3.jpg


    ✨解决办法:


    增加插件


     <WujieVue
    width="100%"
    height="100%"
    name="pro1"
    :url
    :sync="true"
    :exec="true"
    :alive="true"
    :props="{ jump }"
    :plugins="[{
    patchElementHook(element, iframeWindow) {
    if (element.nodeName === "
    STYLE") {
    element.insertAdjacentElement = function (_position, ele) {
    iframeWindow.document.head.appendChild(ele);
    };
    }
    },
    }]"
    >
    </WujieVue>

    如果不生效请清除缓存重新启动,多试几次就行


    4、el-select 位置偏移以及 el-table tootip 位置偏移的问题


    ✨问题复现:


    select.jpg


    ✨出现这种问题的原因:


    el中依赖了poper.js 的fixed定位偏移,子应用的dom挂载在shadowRoot上,导致计算错误


    官网解决办法


    select-2.jpg


    试了,发现没啥用,所以维护官网的大大请更新快点,要不然哪个还想用无界啊!!


    ✨最后解决办法:


    使用插件 wujie-polyfill.github.io/doc/plugins…


    // import { DocElementRectPlugin } from "wujie-polyfill";
    <WujieVue
    width="100%"
    height="100%"
    name="xxx"
    :url="xxx"
    :plugins="[DocElementRectPlugin()]"
    >
    </WujieVue>

    我的element-plus 的版本:"element-plus": "^2.9.0",


    这个版本比较新,如果你们使用的是比较老的版本或者使用的是element-ui的话,上面的插件可能不生效,可以看下面的解决方案


    github.com/Tencent/wuj…


    总结下来无非是几个办法,要么是改element-ui 中的源码,然后在项目中打补丁


    要么直接在子应用加代码


    body{position: relative !important} 
    .el-popper {position: absolute !important}

    大家都可以都试一下,说不准哪个生效了


    5、异步获取e.target 的 e.target 变成了 wujie-app


    ✨问题复现:


    eTarget.jpg


    eTarget2.jpg


    官网文档方法
    eTarget3.jpg


    上面尝试了不行


    ✨最后解决办法:


    使用 插件 wujie-polyfill.github.io/doc/plugins…


    import { EventTargetPlugin } from "wujie-polyfill";
    // vue
    <WujieVue
    width="100%"
    height="100%"
    name="xxx"
    :url="xxx"
    :plugins=“[EventTargetPlugin()]”
    ></WujieVue>

    完美解决


    6、全局样式污染了子应用元素的样式


    ✨问题复现:


    scope.jpg


    scope2.jpg


    ✨最后解决办法:


    比如主项目中你给 html 或者 body 文字居中,在子项目中也会受影响,其实这个不算是框架的问题,因为你写了这个是全局样式,那就说明了这个会影响所有的,所以建议大家样式尽量写scope 并且对全局的样式尽量主项目和子项目同步,不要出现不一样的情况,要不然很难去排查这种问题


    目前还没找到解决办法的问题:


    1、自定义事件


    很多项目集成了第三方sdk 比如埋点、错误监控、数据通信,其中sdk 可能会使用了js 的自定义事件,这个时候在子组件中会失效


    ✨问题复现:


    const customEvent = new CustomEvent('update', {
    bubbles: true,
    composed: true,
    detail: {
    msg:'我的数据更新喽'
    }
    })
    setTimeout(() => {
    console.log(window.__WUJIE_RAW_WINDOW__,'window.__WUJIE_RAW_WINDOW__');
    window.dispatchEvent(customEvent)
    window.__WUJIE_RAW_WINDOW__ && window.__WUJIE_RAW_WINDOW__.dispatchEvent(customEvent)
    }, 2000)


    window.addEventListener('update', function(event) {
    // 主应用没有反应,子组件中正常
    console.log(event)
    })
    window.__WUJIE_RAW_WINDOW__ && window.__WUJIE_RAW_WINDOW__ .addEventListener('testEvent', function(event) {
    // 主应用没有反应,子组件中正常
    console.log(event)
    })

    会发现使用 window.addEventListener 或者 window.WUJIE_RAW_WINDOW .addEventListener 都没有用


    ✨出现这种问题的原因:


    看了issue中作者说这个是无界中的bug ,所以如果有子组件用到这个自定义事件的,只能先将子组件用iframe 嵌入进去,等作者更新了再改回来


    2、主应用路由切换和子应用路由切换点击浏览器退回没反应


    ✨问题复现:


    官方示例:wujie-micro.github.io/demo-main-v…


    大家也可以试一下,先点击主应用中的左侧vite 下的页面切换,然后再点击子应用中中间的页面切换,会发现需要点击两次浏览器的返回才能正常返回,可以看到我录屏下的点击返回前进和退回都没反应,只有多点一次才可以,目前还没找到好的解决办法,如果大家有办法解决可以告诉我。


    两次点击.gif


    结语


    用了无界这么长时间给我的感觉还是比较好的,子应用保活非常丝滑,开箱即用,子应用基本上不需要更改任何代码直接可以继承到无界之中,这个优点真的是非常棒!不像qiankun 还得写很多适配的代码,当然qiankun 的社区真的比无界强大很多,很多问题你都能找到解决方案,只能说各有优缺点吧,主要看你自己怎么选择了


    作者:前端摸鱼杭小哥
    来源:juejin.cn/post/7444134659719610380
    收起阅读 »

    把鸽鸽放到地图上唱跳rap篮球需要几步?

    web
    事情的起因 最近在做地图类的创意应用没什么灵感,有一天晚上看到我弟弟玩游戏,发现机箱里有个ikun手办,这创作灵感就来了,鸽鸽+唱歌跳舞有没有搞头? 说做就做,WebStorm启动 1.初始化地图 这里的地图框架我用的是Mapmost SDK for We...
    继续阅读 »

    事情的起因


    最近在做地图类的创意应用没什么灵感,有一天晚上看到我弟弟玩游戏,发现机箱里有个ikun手办,这创作灵感就来了,鸽鸽+唱歌跳舞有没有搞头?


    image.png


    说做就做,WebStorm启动


    image.png


    1.初始化地图


    这里的地图框架我用的是Mapmost SDK for WebGL,代码和效果如下



    官网地址:http://www.mapmost.com




    <script src="https://delivery.mapmost.com/cdn/sdk/webgl/v9.3.0/mapmost-webgl-min.js"></script>
    <script>
    let map = new mapmost.Map({
    container: 'map',
    style: 'https://www.mapmost.com/cdn/styles/sample_data.json',
    center: [120.71330725753552, 31.29683781822105],
    zoom: 16,
    userId: '*******************', //填入你自的授权码
    pitch: 60,
    bearing: 75,
    sky:"light",
    env3D:{
    exposure:0.3,
    defaultLights: true,
    envMap: "./yun.hdr",
    }
    });
    </script>


    image.png


    2.设置地图样式


    这里为了和模型本身的颜色契合,我隐藏了一些图层,然后把水系、道路、和陆地的颜色改了,代码和效果如下


    image.png


     //更改背景、水系、道路配色
    map.setPaintProperty('bacground', 'background-color', 'rgb(159, 208, 137)')
    map.setPaintProperty('ground_grass2', 'fill-color', 'rgb(103, 173, 144)')
    map.setPaintProperty('water_big', 'fill-color', 'rgb(106, 190, 190)')
    map.setPaintProperty('water_samll', 'fill-color', '#ADDCDF')
    map.setPaintProperty('road_city_polygon-tex', 'fill-color', '#F1ECCC')
    map.setPaintProperty('ground_playground', 'fill-color', '#FBD9E1')

    //隐藏道路名
    map.setLayoutProperty('road_metroline_line','visibility','none')
    map.setLayoutProperty('road_metro_line', 'visibility', 'none')
    map.setLayoutProperty('road_metroline_name', 'visibility', 'none')
    map.setLayoutProperty('road_metro_name', 'visibility', 'none')
    map.setLayoutProperty('road_city_name', 'visibility', 'none')
    map.setLayoutProperty('road_country_name', 'visibility', 'none')
    map.setLayoutProperty('road_others_name', 'visibility', 'none')

    image.png


    3.加载模型和图标


    然后从网上下载了鸽鸽的obj模型


    image.png


    直接加载上去,这里作为模型图层添加,用法参考下面的文档:



    http://www.mapmost.com/mapmost_doc…




    //定义模型对象
    let models_obj = [{
    type: 'obj',
    url: "./XHJ.obj",
    mtl: "./XHJ.mtl",
    }]

    //配置模型图层参数
    let options = {
    id: 'model_id',
    type: 'model',
    models: models_obj,
    center: [120.71482081366986, 31.299511106127838, 145],
    callback: function (group, layer) {
    }
    };

    //添加图层
    map.addLayer(options);

    鸽鸽就这样水灵灵的出现了


    image.png


    然后我们加几个图标上去,这里利用geojson数据,添加的自定义图层


    image.png


    //增加ikun图标
    map.addSource("ikun", {
    "type": "geojson",
    "data": "./ikun.geojson"
    })
    map.loadImage('./111.png', (error, image) => {
    if (error) {
    throw error;
    }
    map.addImage('icon', image)
    map.addLayer({
    "id": "icon_ikun",
    "type": "symbol",
    "source": "ikun",
    "layout": {
    "icon-image": "icon",
    "icon-size": 0.15,
    "visibility": "visible"
    }
    })
    })

    好了,大功告成了
    image.png


    后续:如何实现唱跳rap篮球?


    当然只看模型肯定不行,主要得让鸽鸽动起来,唱、跳、rap、篮球一样不能少
    怎么办,MasterGO启动, 急的我UI和交互都给一起做了,不会UI设计的开发不是好前端。


    image.png


    image.png


    后面越想功能越多,干脆搞个小游戏算了,游戏名字我都想好了,叫:唤醒鸽鸽,输入不同的口令,激活鸽鸽不一样的动作,伴随着地图一起舞动。


    image.png


    但是开发遇到了点困难,手机和模型材质的适配还在解决中....,另外模型骨骼动画有点僵硬,这个等我解决了再给大家分享,目前的效果还比较粗糙:


    Screen-2024-12-18-152936_0001.gif


    Screen-2024-12-18-152936_0002.gif


    如果大家想到什么好玩的功能,也可以评论区讨论一下,不过一定要与地图结合才好玩。


    关于代码:源码地址在这


    基础场景的代码我先发给大家:
    链接: pan.baidu.com/s/1G-r5qIXN… 提取码: y1p5




    完整的代码等我解决掉bug了再分享


    作者:清风夜半
    来源:juejin.cn/post/7449599345371283482
    收起阅读 »

    来自全韩国最好的程序员的 Nest.js 中 TypeSafe 方案

    web
    Nest.js 中的 TypeSafe 方案 在现代 Web 开发中,类型安全(TypeSafe)是提升代码质量和减少运行时错误的关键因素。 Nest.js 作为一个渐进式的 Node.js 框架,结合了 TypeScript 的强大功能,提供了构建高效、可扩...
    继续阅读 »

    Nest.js 中的 TypeSafe 方案


    在现代 Web 开发中,类型安全(TypeSafe)是提升代码质量和减少运行时错误的关键因素。


    Nest.js 作为一个渐进式的 Node.js 框架,结合了 TypeScript 的强大功能,提供了构建高效、可扩展服务器端应用的理想平台。


    笔者在使用 Nest.js 构建全栈应用时最大的痛点是写了这么多类型检查,好像没有办法和前端通用啊~。相信许多人也有这个问题,所以也冒出了在 Nest.js 中集成 tRPC 的教程。


    而本文介绍的 Nestia,一个专为 Nest.js 设计的类型安全解决方案,帮助开发者在构建应用时实现更高的类型安全性和开发效率。


    韩国最好的程序员


    Jeongho Nam,GitHub用户名为 samchon他在 README 中自称为韩国最好的程序员。他自1998年开始编程,拥有25年的丰富经验。在这段时间里,他开发了许多程序,并不断提升自己的技能。他不仅在工作中开发程序,还在业余时间开发开源项目,以满足自身需求或改进现有功能。这些开源项目逐渐形成了新的开源库,其中最著名的就是 typianestia


    什么是Nestia?


    Nestia 是一个专为 Nest.js 开发的库,旨在通过利用 TypeScript 的类型系统,提供更高效和类型安全的开发体验。Nestia 的核心目标是简化数据传输对象(DTOs)的定义和验证,减少类型错误,并提升代码的可维护性。


    Nestia的主要功能



    1. 类型安全的 DTO 定义和验证


      Nestia 利用 TypeScript 的类型系统,允许开发者定义类型安全的 DTOs。通过自动生成的类型定义,Nestia 确保了数据在传输和处理过程中的一致性,避免了常见的类型错误。


      NestJS需要三个重复的 DTO 模式定义。第一个是定义 TypeScript 类型,第二个和第三个是调用 class-validator@nestjs/swagger 的装饰器函数。这不仅繁琐,而且容易出错。如果你在第 2 或第 3 处写错了的话,TypeScript 编译器是无法检测到的。只有在运行时才能检测到。换句话说,它并不是类型安全的。


    2. 自动生成API客户端


      Nestia 可以根据服务器端的 API 定义,自动生成类型安全的 API 客户端。这种方式不仅减少了手动编写客户端代码的工作量,还确保了前后端的一致性


      这一功能与著名的 tRPC 库有相似之处。tRPC 是一个端到端类型安全的 RPC 框架,它允许你轻松构建和使用完全类型安全的 API,无需模式或代码生成。tRPC 的主要作用是在全栈 TypeScript 项目中提供类型安全的 API 调用,大大提高了开发效率和代码质量。但 tRPC 的问题是通常要求前后端代码位于同一个 monorepo 中,以便共享类型定义。这种紧耦合的架构可能不适合所有项目,特别是那些前后端分离开发或需要为第三方提供 API 的场景。相比之下,Nestia 通过自动生成独立的 API 客户端解决了这个问题。它允许你在保持类型安全的同时,将生成的 SDK 作为独立的包分发给客户端开发者。这种方式既保留了类型安全的优势,又提供了更大的灵活性,使得 Nestia 在更广泛的项目结构和开发场景中都能发挥作用。


    3. 高效的JSON序列化和反序列化


      Nestia 提供了高效的 JSON 序列化和反序列化功能,利用 TypeScript 的类型信息,显著提升了性能和类型安全性。



    如何使用Nestia


    安装Nestia


    你可以运行以下命令通过模版代码来快速上手 Nestia。
    模板将自动构建在<directory>中。作为参考,这是一个最小的模板项目,只集中于从 NestJS 服务器生成 SDK。它不包含数据库连接。


    npx nestia start <directory>

    你也可以运行下面的命令来将 Nestia 集成至现有的项目中。设置向导将自动安装并配置所有内容。


    npx nestia setup

    定义类型安全的DTO


    你不需要掌握特殊的语法只使用 TypeScript 就可以编写一个带有类型检查的 DTO。当然,Nestia 也通过 Typia 提供了编写复杂类型检查的可能,例如,我们可以定义一个论坛完整的 DTO:


    import { tags } from "typia";

    export interface IBbsArticle {
    /* Primary Key. */
    id: string & tags.Format<"uuid">;

    /* Title of the article. */
    title: null | (string & tags.MinLength<5> & tags.MaxLength<100>);

    /* Main content body of the article. */
    body: string;

    /* Creation time of article. */
    created_at: string & tags.Format<"date-time">;
    }


    Controller 中调用 Nestia 的装饰器


    NestJS 原生的装饰器(如 @Get(), @Post() 等)虽然使用方便,但在性能和类型安全方面存在一些局限:



    • 使用 class-validator 和 class-transformer 进行验证和转换,性能相对较低

    • 需要定义额外的 DTO 类和装饰器,增加了代码量

    • 类型安全性不够强,运行时可能出现类型错误


    Nestia 的装饰器(如 @TypedRoute.Get(), @TypedBody() 等)则解决了这些问题:



    • 利用 typia 库进行高性能的运行时类型验证,比 class-validator 快 20,000 倍

    • 支持使用纯 TypeScript 接口定义 DTO,无需额外的类定义

    • 在编译时进行类型检查,提供更强的类型安全性

    • JSON 序列化速度比 class-transformer 快 200 倍


    import { TypedRoute } from "@nestia/core";
    import { Controller } from "@nestjs/common";

    import { IBbsArticle } from "./IBbsArticle";

    @Controller("bbs/articles")
    export class BbsArticlesController {

    @TypedRoute.Get("random")
    public async random(): Promise<IBbsArticle> {
    return {
    id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6",
    title: "Hello nestia users",
    body: "Just use `TypedRoute.Get()` function like this",
    created_at: "2023-04-23T12:04:54.168Z",
    files: [],
    };
    }

    @TypedRoute.Post()
    public async store(
    @TypedBody() input: IBbsArticle.IStore,
    ): Promise<IBbsArticle> {
    return {
    ...input,
    id: "2b5e21d8-0e44-4482-bd3e-4540dee7f3d6",
    created_at: "2023-04-23T12:04:54.168Z",
    };
    }
    }

    自动生成API客户端


    Nestia可以根据服务器端的API定义,自动生成类型安全的API客户端。在根目录配置 nestia.config.ts 文件


    import { INestiaConfig } from '@nestia/sdk';
    import { NestFactory } from '@nestjs/core';
    import { FastifyAdapter } from '@nestjs/platform-fastify';

    import { AppModule } from './src/app.module';

    const NESTIA_CONFIG: INestiaConfig = {
    input: async () => {
    const app = await NestFactory.create(AppModule, new FastifyAdapter());
    app.setGlobalPrefix('api');
    return app;
    },
    output: 'src/api',
    clone: true,
    distribute: 'sdk',
    };
    export default NESTIA_CONFIG;

    运行命令 npx nestia sdk 即可生成 SDK package,你可以直接在 Monorepo 中使用它,也可以将其分发到 npm.


    尝试使用生成的 SDK 看到类型提示的那一刻,整个人都通畅了~


    703_1x_shots_so.png


    结论


    Nestia 作为一个专为 Nest.js 设计的类型安全解决方案,通过简化 DTO 定义和验证、自动生成 API 客户端以及高效的 JSON 序列化和反序列化功能,帮助开发者在构建应用时实现更高的类型安全性和开发效率。无论是大型企业级应用还是个人项目,Nestia 都是提升代码质量和开发效率的理想选择。


    通过本文的介绍,希望您对 Nestia 有了更深入的了解,并能在您的 Nest.js 项目中尝试使用这一强大的工具,享受类型安全带来的诸多好处。


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

    准备离开杭州

    上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要...
    继续阅读 »

    上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要多久。


    本人是前端,工作 6 年,期间经历过 4 家公司,前两份是外包,后面两份都是领大礼包走的,回想起来,职业生涯也是够惨的。虽然说惨,但是最近领的这一份大礼包个人认为还是值得,工作很难待下去,也没有任何成长,继续待着也是慢性死亡。


    这几天我每天都会在 BOSS 上面投十几家公司,能回复非常少,邀请面试的就更少了。外包公司倒是挺多的,而我是从那个火坑里出来的,是不会选择再进去的。于是,我需要做好打持久战的准备,说不定不做程序员了。


    我的房子 7 月底就要到期了,我必须要马上做决定,杭州的行情对我来说很不友好,短期内我大概率找不到工作。基于对未来的悲观考虑,我不想把过多的钱花费在房租上面,所以希望就近找一个三线城市,我搜了一下嘉兴,整租 95 平左右的房子只需要 1200 块钱,还是民用水电,思前想后,打算移居到那里嘉兴去。


    一方面,我想尝试一下在三线城市生活是一种什么感觉。另一方面,这可以省钱,如果一个月的房租是 1000,民用水电,一个月的开销只要 2500 块。我搜索了一下货拉拉,从我的位置运到嘉兴,需要花费 600 块钱,这个价格也是可以接受的。思考了这些,我觉得是时候离开待了 5 年的杭州。


    未来要到哪里去呢,目前可能的选择是上海。我还得想想未来能做什么,我想学一门手艺傍身,比如修理电器、炒菜。毕竟骑手行业太拥挤了,估计也不是长久之计。


    房租降下来了,等我把行李都安置妥当,我打算回老家待一段时间。自从上大学以来,很少有长时间待在家里的时候,眼看父母年纪也越来越大了,很想多陪陪他们。如果进入正常的工作节奏,想做到这样还是会受到局限,这次也算是一个弥补的机会。


    被裁也是一件好事,可以让我提前考虑一下未来的出路。


    这段时间我想把时间用来专门学英语,再自己做几个项目,学英语的目的是为了 35 岁之后做打算,做项目是为了写到简历上面,并且个人觉得自己需要多做一个项目,这样自己才能成长到下一个级别。虽然不知道收益怎么样,但是我想尝试一下。人还活着,有精力,就还是瞎折腾一下。


    离职没敢和家里说,说了估计要担心死了,反正是年轻人,有事就先自己扛一扛,我前几天把我的行李寄回去了一批,我妈问我,怎么,寄东西回来了?我回答说要搬家了。本来也想找机会开口说自己离职了,她说,这次搬家也别离公司远了,我也把话憋了进去,只好说“没事的,放心就行”。我自己没觉得离职有什么,正常的起起落落,只是觉得父母可能会过度的担心。


    如果做最坏的打算,那就是回去种地,应该这几年还饿不死。有还没离职的同学,建议还是继续苟着。希望社会的低谷期早点过去,希望我们都能有美好的未来。




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

    Taro v4框架开发微信小程序(配置)

    web
    环境变量文件 将 .env.dev 文件重命名为 .env.development,以及将 .env.prod 文件重命名为 .env.production,以适配环境配置。 为了演示如何使用环境变量,我们在 .env.development 文件中添加两...
    继续阅读 »

    环境变量文件


    image.png


    .env.dev 文件重命名为 .env.development,以及将 .env.prod 文件重命名为 .env.production,以适配环境配置。


    image.png


    为了演示如何使用环境变量,我们在 .env.development 文件中添加两个变量 TARO_APP_IDTARO_APP_API,然后在源代码中读取这些变量的值。


    TARO_APP_ID="xxxxxxxxxxxxxxxxxx"

    TARO_APP_API="https://api.tarojs.com"

    image.png


    接下来需要在 project.config.json 文件中更新 appid 的值。因为上一章节中为了测试修改了这个值,现在我们需要把它改回原来的 appid


    "appid": "touristappid",

    在完成以上操作后,重新启动项目(使用命令 pnpm dev:weapp),控制台会显示相关的提示信息,并且可以看到 dist/project.config.json 文件中的 appid 已经变成了我们在 .env.development 文件中指定的 TARO_APP_ID 值。


    image.png


    image.png


    为了在代码中使用环境变量,可以在 src/pages/index/index.tsx 文件的 useLoad 钩子中添加 console.log 语句来打印 TARO_APP_API 的值:


    console.log(process.env.TARO_APP_API)

    这样做的结果是,当程序运行时,可以在微信开发者工具的控制台中看到 TARO_APP_API 环境变量的值被成功打印出来。


    image.png


    这里需要记得将环境变量的appid改为你正常使用的appid,否则小程序会报错。


    之后运行程序,并在微信开发者工具中浏览:


    image.png


    需要注意的是,只有以 TARO_APP_ 开头的环境变量才会被 webpack 的 DefinePlugin 插件静态嵌入到客户端代码中。这是为了避免环境变量与系统内置变量冲突。在构建过程中,代码中的 process.env.TARO_APP_API 会被替换为实际的环境变量值。例如,我们在小程序开发者工具中查看编译后的代码,会看到 console.log(process.env.TARO_APP_API) 被替换成了 console.log("https://api.tarojs.com");


    image.png


    编译配置


    编译配置是 Taro 项目开发过程中重要的一部分,它决定了项目的编译行为。Taro 的编译配置主要存放在项目根目录下的 config 文件夹内,由 index.ts 文件统一导出。其中,index.ts 通过合并 dev.tsprod.ts 来分别处理开发时的配置和构建时的生产配置。dev.js 适用于项目预览时的设置,而 prod.js 则适用于项目打包时的设置。


    在 Taro 的编译配置中,可以设置项目名称、创建日期、设计稿尺寸、源码目录等基本配置信息。下面的代码片段展示了一部分编译配置的内容:


    const config = {
    // 项目名称
    projectName: 'learn-taro-wxapp',
    // 项目创建日期
    date: '2024-3-11',
    // 设计稿尺寸
    designWidth: 750,
    // 设计稿尺寸换算规则
    deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    375: 2,
    828: 1.81 / 2
    },
    // 项目源码目录
    sourceRoot: 'src',
    // 项目产出目录
    outputRoot: 'dist',
    // Taro 插件配置
    plugins: [],
    // 全局变量设置
    defineConstants: {},
    // 文件 copy 配置
    copy: {
    patterns: [],
    options: {},
    },
    // 框架,react,nerv,vue, vue3 等
    framework: 'react',
    // 使用 webpack5 编译
    compiler: 'webpack5',
    cache: {
    enable: false // Webpack 持久化缓存配置,建议开启
    },
    // 小程序端专用配置
    mini: {
    postcss: {
    pxtransform: {
    enable: true,
    config: {

    }
    },
    autoprefixer: {
    enable: true,
    },
    cssModules: {
    enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
    config: {
    namingPattern: 'module', // 转换模式,取值为 global/module
    generateScopedName: '[name]__[local]___[hash:base64:5]',
    },
    },
    },
    // 自定义 Webpack 配置
    webpackChain(chain) {
    chain.resolve.plugin('tsconfig-paths').use(TsconfigPathsPlugin)
    }
    },
    // H5 端专用配置
    h5: {
    publicPath: '/',
    staticDirectory: 'static',
    postcss: {
    autoprefixer: {
    enable: true,
    },
    cssModules: {
    enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
    config: {
    namingPattern: 'module', // 转换模式,取值为 global/module
    generateScopedName: '[name]__[local]___[hash:base64:5]',
    },
    },
    },
    // 自定义 Webpack 配置
    webpackChain(chain, webpack) {},
    devServer: {},
    },
    rn: {
    appName: 'taroDemo',
    postcss: {
    cssModules: {
    enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
    }
    }
    }
    }

    module.exports = function (merge) {
    if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'))
    }
    return merge({}, config, require('./prod'))
    }

    在编译配置文件中,alias 被用来设置路径别名,避免在代码中书写过多的相对路径。在配置文件中,默认已经将 @ 设置为指向 src 目录,这样,在代码中就可以使用 @ 快捷引用 src 下的文件了。


    我们还可以增加额外的配置,例如:


    alias: {
    '@/components': path.resolve(__dirname, '..', 'src/components'),
    }

    使用 defineConstants 可以定义全局常量,例如,可以基于不同的环境设置不同的全局变量。


    defineConstants: {
    __DEV__: JSON.stringify(process.env.NODE_ENV === 'development'),
    __PROD__: JSON.stringify(process.env.NODE_ENV === 'production')
    }

    等等...
    如果想要查阅每个配置项的具体意义和用法,可以按住 Ctrl + 鼠标左键 点击属性名,跳转到 project.d.ts 类型声明文件中查看对应注释和示例代码。


    动画.gif


    designWidth 用于指定设计稿的宽度,这里设置的是 750px,这意味着使用的 UI 设计稿的宽度标准是 750px。Taro 提供了多个设计稿尺寸的换算规则,当前项目中已经设置了几种不同尺寸对应的换算比例。如下所示:


      // 设计稿尺寸换算规则
    deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    375: 2,
    828: 1.81 / 2
    },

    对于 UI 设计师而言,推荐使用 750px 作为设计尺寸标准,它便于开发者使用 Taro 进行开发时,进行适配和转换。


    对于更详细的编译配置,可以查询官方文档中的编译配置详情


    app.config.ts 通用配置文件


    在 Taro 框架中,app.config.ts 是小程序的通用配置文件,其主要职责是定义小程序的页面及其全局属性。以下是针对 app.config.ts 文件中一些关键配置项的说明:


    pages


    pages 数组用于指明组成小程序的页面列表,每个元素都指向一个页面的路径加文件名,无需包含文件扩展名。由于 Taro 会自动处理寻找文件,这会带来方便。改变小程序页面结构时,如新增或删除页面,都应相应地更新 pages 配置。


    pages: [
    'pages/index/index',
    'pages/other/other',
    // ... 其他页面路径
    ]

    其中,数组中的第一个条目表示小程序的入口页面(首页)。


    window


    window 对象用于设置小程序的状态栏、导航条、标题和窗口背景色等界面表现。


    window: {
    navigationBarBackgroundColor: '#ffffff',
    navigationBarTextStyle: 'black',
    backgroundColor: '#eeeeee',
    // ... 其他窗口属性
    }

    动画.gif


    查看属性详细信息和支持程度,你可以通过按住 Ctrl + 鼠标左键 点击任意属性,跳转至 taro.config.d.ts 类型声明文件。支持程度不同的平台详细请查阅官方文档


    tabBar


    对于包含多个 tab(在客户端窗口底部或顶部有 tab 栏切换页面的)的小程序,tabBar 配置用于定义 tab 栏的外观以及每个 tab 对应的页面。


    tabBar: {
    color: "#434343",
    selectedColor: "#333333",
    // ... tabBar 属性和 tab 列表
    }

    tabBar 中的 list 属性是一个包含若干对象的数组,每个对象定义了一个 tab 项的页面路径、图标和文字等。点击 tab 时,应用会切换到对应的页面。


    关于 tabBar 的更多详细配置项,也可以通过点击属性,跳转至 TypeScript 的类型声明文件中查看功能描述。


    动画.gif


    支持程度不同的平台详细请查阅官方文档


    页面的config.ts配置文件


    单个页面也可以有自己的配置文件,通常是 config.ts。页面配置会被 definePageConfig 函数包裹,并作为参数传入,其参数类型是 Taro.PageConfigPageConfig 继承自 CommonConfig 类型。


    export default definePageConfig({
    navigationBarTitleText: '页面标题'
    })

    动画.gif


    project.config.json 微信小程序配置文件


    除了 Taro 的配置外,微信小程序也需一个 project.config.json,这个配置文件包含了微信小程序的专有配置。关于此配置你可以参考微信小程序官方文档


    作者:辰火流光
    来源:juejin.cn/post/7345063548705718312
    收起阅读 »

    8年老前端开发,成仙路已断

    前言 到今年,我已经写了8年代码。在这段时间里,我的职业生涯一直处于不上不下的状态。尽管在二线城市的薪资看起来还不错,但我早就明白,这行不过是用青春换取血汗钱。从疫情开始,我内心变得愈发焦躁不安,尤其是今年,心态更加低迷。无论是职场上的无尽业务,还是长期的工作...
    继续阅读 »

    前言


    到今年,我已经写了8年代码。在这段时间里,我的职业生涯一直处于不上不下的状态。尽管在二线城市的薪资看起来还不错,但我早就明白,这行不过是用青春换取血汗钱。从疫情开始,我内心变得愈发焦躁不安,尤其是今年,心态更加低迷。无论是职场上的无尽业务,还是长期的工作倦怠,都在促使我做出改变的决定。


    最终,在三月底,我主动离职了(没错,在最艰难的时刻做出了这个决定)。从那时起,这三个多月来,我一直在思考和寻求变化。转眼间,已经到了七月底。虽然这段时间收入不如上班时稳定,但我的状态却越来越好。


    简单回顾下个人开发经历


    本人是双非辣鸡学校毕业。


    2016年 初开始入行前端开发,那时候还处在前后端不分离的时代,开始主要是写 JSP,主要以原生JS和JQ为主,那时候前端主要是司职辅助功能,事情也比较少,相对比较轻松。


    2017年 开始前后端分离大行其道,也开始用“所谓”的现代化前端框架,那时候主要是用react全家桶,当时觉得,卧槽,这TM比一个个操作dom节点确实方便不少,最关键的是webpack之流的工具让前端开发更加成体系,不像以前是寄生在jsp里面了,工程化更明显了,当时,前端真正独立成了一个岗位,不再是纯辅助,而是研发流程的重要一环。


    2018年 一整年都在用RN开发APP,第一次接触移动端开发,RN是我一生的噩梦,以至于后面我看到跨平台的东西做复杂应用就谨慎的很,当时还是0.5x的时代,各种安卓、ios端的BUG。我记得RN官网写的slogan一次编写,多端运行,后面可能是投诉太多了,官网后面好像改成还是需要原生安卓/ios知识了。我记得那时候做一个IM应用,长列表白屏,realm数据库大量消息写入卡顿,各种原生不兼容,我都不记得晚上花了多少时间在github的issue上找兼容方案和各种hack了。再加上当时五大厂商各种魔改安卓,华为刚刚切换鸿蒙系统(就这么巧),无尽的debuff(现在想想当初直接学原生梭哈更好,泪水不争气的落下)


    2019年 开始用vue开发web,比起react,更多的语法糖,当时觉得slot插槽这种封组件的方法确实要更方便。后面接私单果断vue梭哈,做公司产品还是react,毕竟没有slot给队友乱用


    2020年-至今 从疫情开始整个职业规划紊乱。一开始为了更具竞争力,开始往全栈方向发展,努力自学node.js/koa ,开始用公司的几个小型产品实践,当时一度以为自己全栈,后面才知道我这叫全不占工程师。后面又因为公司业务需求,又开始用electron开发桌面应用,用uniapp开发多端小程序,用python套壳gpt的api(有了后面的经历,我才知道不是巨复杂的应用跨平台的坑也还行),并且机缘巧合之下,还有了机会用laya引擎开发微信小游戏。直至最后,彻底成为全不栈工程师,纯纯牛马。


    总结职业生涯 没什么规划,跟着公司的业务随波逐流,属于先知后觉,觉醒了又不够决心,总是想着现在行情不好,实在不行再攒一点钱,然后就一直蹉跎到了2024年,一事无成。


    我的反思



    既然已经这样了,只能自救了,不然咋办呢哈哈哈哈



    1、定位问题



    1. 离开了公司无法生存,没法形成独立生产单元

    2. 学历、年纪、技术都不占优势

    3. 行业环境急剧恶化,现在长沙公司基本都是大小周或者单休,属于买方市场,并且加班严重


    2、分析原因


    说白了,核心原因就几个:



    1. 程序员不能形成独立生产单元,寄生于公司,也没有机会了解整个商品从规划到收款的全生命周期

    2. 可替代性高,开源生态太强,且总有更厉害的人,总有学历更好的人,怎么卷最终都会失败

    3. 自身可出售的商品只有时间一项,缺少其他收入


    image.png


    3、解决方案


    3.1、最优解



    1. 开发独立产品,这是每一个程序员的梦想,却也是最难得路,属于是下限低,上限高。

    2. 卖课,也就是所谓的知识付费,现在卖课的平台很多,但是需要人设、背景和标签,还需要运营推广,属于极稳的做法。

    3. 找个一辈子的养老公司,不是没有,只是不好进,我上次还看见图书馆和纪念馆招人,只是有编制很难。


    3.2、临时解



    1. 接外包,我现在也在接,个中滋味,只能说懂的都懂。

    2. 找个班上,如果实在缺钱或者不知道做什么,也可以先干着。

    3. 技术顾问,不同于外包,就是靠着一点经验解决点冷门问题。


    3.3、风险解



    1. 开店,重投入,不成功便成仁,需要梭哈。

    2. 出海,去日本、东南亚、甚至是更远的地方,写代码也好,干其他的也好,需要勇气。

    3. 开公司,只要有业务,就能苟活,问题是怎么有业务,需要资源。


    4、我的解题思路



    1. 做自己的独立产品,以尝试的心态,,将自己内心的idea实现,切勿抱着什么多少爆款的思路,多学习别的独立开发者好的思路和见解。做了就上线,反复试错。

    2. 关于上班,现在很少有什么新公司,新产品出现,都是旧项目在维护,成仙路已断,注定走不了了。现在基本只接外包单子,上班继续做web是不可能了。

    3. 关于小游戏开发,参照发达国家的经验,可以一直做下去,创意类很难被替代。并且不像web,这行业的经验不怎么共享,开源代码也少,这是良性生态,由于岗位少,对于职场不友好,但是对于个人开发很不错。做了就上线,反复试错。

    4. 关于转行,盲目转行不可取,大环境下都是大哥笑二哥。

    5. 关于技术自媒体,不给自己设限,也可以去做做。


    我这3个月做了什么


    今年4月迷茫了半个月,然后躺平了半个月,真的好开心。


    今年5月上半个月学习开传奇私服,然后去自驾去青海湖玩了半个月,真的好开心。


    今年6月



    • 做了一个AI做菜小程序,套壳子那种,微信不给上架,说个人不能做信息咨询,放弃。

    • 写了一个模拟用户操作爬取数据的桌面工具,后面可能有用。

    • 不信邪,做了一个躺平记账的小程序,刚刚上架,是人都要做一次三件套。

    • 用laya和cocos做了几个入门小游戏,并开源,给想web转游戏的小伙伴们降低一点曲线,因为游戏引擎的文档不是一般的烂(我原以为小程序的文档很差,直到我看到了几个游戏引擎的文档),大部分时间一边看源码一边开发


    今年7月



    • 做了10+传奇私服,并且赚了几百元(聊胜于无),不敢做大,不敢宣传,容易被封印。

    • 做了一个创意肉鸽小游戏(有没有美术搭子,全靠AI生图走的很艰难),在到处寻找美术资源,代码已写完(等替换完资源就上)。


    image.png


    我的规划


    2024年度规划



    • 继续做肉鸽、塔防类游戏

    • 继续开私服

    • 学习新的私服技术(暂定DNF)

    • 继续做自己的独立产品


    远景规划



    • 追随自己的内心,不给自己设限

    • 动态寻找可长期耕耘的赛道


    写在最后


    多年以后,我总是想起2016年刚入行时,只用写写js就能拿薪水的那个下午


    作者:飞翔的羊驼
    来源:juejin.cn/post/7393312386348138530
    收起阅读 »

    站住!送你一个漂亮的毛玻璃渐变背景

    web
    大家好呀,我是 HOHO。 不知道你之前有没有接触过这种需求,实现一个下面这样的背景: 一层毛玻璃,然后后面有一些渐变的色块动来动去。这种效果大家应该都比较熟悉,之前有段时间 Apple 很热衷于使用类似的特效,说实话确实挺好看。我们一般管这种效果叫做 bl...
    继续阅读 »

    大家好呀,我是 HOHO。


    不知道你之前有没有接触过这种需求,实现一个下面这样的背景:


    image.png


    一层毛玻璃,然后后面有一些渐变的色块动来动去。这种效果大家应该都比较熟悉,之前有段时间 Apple 很热衷于使用类似的特效,说实话确实挺好看。我们一般管这种效果叫做 blurry gradient、或者模糊渐变、磨砂渐变、毛玻璃渐变。


    本来以为一个背景而已,有什么难度,没成想一路走来还真踩到了不少的坑。本着我摔了一身泥不能让大家再摔一次的原则。我把这种效果封装成了一个 React 组件 react-blurry-gradient,大家可以直接拿来用,省的再抄代码浪费脑细胞。


    在讲组件之前,我先来介绍一下都踩了哪些坑,如果你急用的话,前面直接无视就行。OK 话不多说,我们现在开始。


    心路历程


    1、shader gradient


    其实一开始和 UI 沟通的时候,这个效果并不是典型的模糊渐变。而是一个 Shader,你可以在 这个网站 看到类似的效果,这个 Shader Gradient 还有对应的 Figma 插件


    这个效果其实实现起来不难,因为它提供了对应的 react 插件 ruucm/shadergradient: Create beautiful moving gradients on Framer, Figma and React。只需要把 UI 给的参数填进去就可以了。


    但是事情并没有想象的那么简单,单纯的复刻效果倒没什么问题,问题出在这玩意居然自带一个十分离谱的入场特效:


    bug1.gif


    可以看到,这个效果初始化的时候会有个旋转缩放的“入场动画”,让人忍俊不禁。不仅如此,这个背景还非常离谱的是可以拖动和缩放的:


    bug2.gif


    这两个问题在组件的文档里并没有任何说明,我猜测这个效果组件是基于 threejs 实现的,出现这两个问题应该是 threejs 的一些默认设置没有关闭导致的。


    不过这些也不是什么大问题,我们可以通过控制入场透明度和添加蒙层来解决。真正阻止我继续使用的是性能问题。因为这个项目要支持 H5 端,而老板的破水果手机打开这个效果都要燃起来了。没办法只能作罢。


    2、css-doodle


    听说这个效果差点让老板换手机之后,UI 挠了挠头,说要不我给你点颜色,你干脆写个毛玻璃渐变得了。我觉得他说的有道理,随手一搜,这不就来了:妙用滤镜构建高级感拉满的磨砂玻璃渐变背景


    css-doodle 我之前研究过,虽然不太喜欢这种写法风格,但是谁能拒绝直接抄的代码呢?三下五除二就搞成了 React 版本:


    import 'css-doodle';
    import styles from './index.module.less';

    const DOODLE_RULES = `
    :doodle {
    @grid: 1x8 / 100vmin;
    width: 100vw;
    height: 100vh;
    }
    @place-cell: center;
    width: @rand(40vw, 80vw);
    height: @rand(40vh, 80vh);
    transform: translate(@rand(-50%, 50%), @rand(-60%, 60%)) scale(@rand(.8, 1.8)) skew(@rand(45deg));
    clip-path: polygon(
    @r(0, 30%) @r(0, 50%),
    @r(30%, 60%) @r(0%, 30%),
    @r(60%, 100%) @r(0%, 50%),
    @r(60%, 100%) @r(50%, 100%),
    @r(30%, 60%) @r(60%, 100%),
    @r(0, 30%) @r(60%, 100%)
    );
    background: @pick(#FBF1F7, #B27CEE, #E280AE, #c469ee, #a443ee, #e261bb, #e488ee);
    opacity: @rand(.3, .8);
    position: relative;
    top: @rand(-80%, 80%);
    left: @rand(-80%, 80%);

    animation: pos-change @rand(4.1s, 10.1s) infinite 0s linear alternate;
    @keyframes pos-change {
    100% {
    left: 0;
    top: 0;
    transform: translate(@rand(-50%, 50%), @rand(-60%, 60%)) scale(@rand(.8, 1.8)) skew(@rand(45deg))
    }
    }`
    ;

    export const Bg = () => {
    return (
    <div className={styles.loginBg}>
    {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
    {/* @ts-ignore */}
    <css-doodle>{DOODLE_RULES}</css-doodle>
    </div>

    );
    };

    index.module.less


    .loginBg {
    position: absolute;
    margin: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background-color: #fff;
    z-index: -1;

    &::after {
    content: '';
    position: absolute;
    top: -200%;
    left: -200%;
    right: -200%;
    bottom: -200%;
    backdrop-filter: blur(200px);
    z-index: 1;
    }
    }

    windows 上打开正常,但是 Safari 打开之后发现毛玻璃效果直接消失了,就像下面这样:


    image.png


    这给我整不会了,按理说 Safari 是支持 backdrop-filter 的。是 css-doodle 在 safari 上有什么兼容性问题?还是 react 和 css-doodle 的集成上出了什么毛病?我没深入了解,再加上本来不小的包体积,于是 css-doodle 方案也被我放弃了。


    image.png


    看看新组件 react-blurry-gradient


    OK,踩了一圈子坑,下面该请出本文的主角 react-blurry-gradient 了,我们直接看效果:


    banner.gif


    如果 GIF 比较模糊的话可以试一下这个 codesandbox 在线 demo


    用法也很简单,安装,然后引入组件和对应的 css 文件即可:


    npm install react-blurry-gradient

    import { BlurryGradient } from 'react-blurry-gradient';
    import 'react-blurry-gradient/style.css';

    const colors = ['#bfdbfe', '#60a5fa', '#2563eb', '#c7d2fe', '#818cf8', '#4f46e5'];

    export default function App() {
    return (
    <div style={{ width: '100vw', height: '100vh' }}>
    <BlurryGradient colors={colors} />
    </div>

    );
    }

    组件会自动的从你指定的颜色列表中随机挑选颜色来生成渐变和动效。


    如果你颜色也不想找,没问题,组件还内置了一套渐变颜色组,直接用就行了(目前包含红黄蓝绿紫五套):


    import { BlurryGradient, COLORS } from 'react-blurry-gradient';
    import 'react-blurry-gradient/style.css';

    export default function App() {
    return (
    <div style={{ width: '100vw', height: '100vh' }}>
    <BlurryGradient colors={COLORS.BLUE} />
    </div>

    );
    }

    这些颜色就是单纯的字符串数组,所以你可以把这些颜色兑在一起来实现撞色的效果:


    <BlurryGradient colors={[...COLORS.BLUE, ...COLORS.RED]} />

    image.png


    image.png


    如果你想设置背景色,没问题,react-blurry-gradient 本身就是透明的,也就是说你背景用什么都可以,直接设置给父元素就行了:


    export default function App() {
    return (
    <div style={{ backgroundColor: COLORS.BLUE[0], width: '100vw', height: '100vh' }}>
    <BlurryGradient colors={COLORS.BLUE} />
    </div>

    );
    }

    预设的 COLORS 第一个元素都是颜色最淡的,你可以直接拿来当作背景


    把这个背景色设置给 BlurryGradient 组件的 style 也可以,不过这样会在边缘形成一圈扩散的效果,而设置给父元素的话背景色就会更均一。


    另外毛玻璃的模糊效果也是可以调整的,只需要设置 blur 参数即可,比如你可以调低一点,我觉得也挺好看的:


    <BlurryGradient colors={[...COLORS.BLUE, ...COLORS.RED]} blur='20px' />

    image.png


    image.png


    除此之外还有很多额外参数,例如可以通过 itemNumber 来控制生成色块的数量。可以通过 itemTopitemLeft 来控制色块随机生成的位置范围。更多的属性可以在 react-blurry-gradient 配置参数 找到。


    如果你不希望随机生成,想精确控制每个色块的颜色、尺寸、位置和运动轨迹,没问题,BlurryGradient 组件的 items 参数允许你指定每个色块的详细配置。参数说明可以看 这里。但是要注意,启用了 items 之后,colors 的配置就会被忽视了。


    零依赖 & 极小体积


    这个组件从一开始就以够轻量为宗旨,所以你可以看到它没有依赖任何第三方包,只要你项目里有 React,那它就能用:


    image.png


    而包本身也足够的清爽,不会往你电脑里拉屎:


    image.png


    组件所有的代码加起来只有 4Kb 不到。


    image.png


    当然,如果你真的不想因为这种小事再引一个包,没问题,都帮你收拾好了,把 这个文件夹 直接复制到你的项目里就能用,给个 star 意思一下就行~


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

    都快2025年了,你们的前端代码都上装饰器了没?

    web
    可能很多人都听说过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器,那么你真的知道装饰器在前端还能玩出哪些花吗? 我们今天不讲基础概念,也不写一些你可能在很...
    继续阅读 »

    可能很多人都听说过 TypeScript 的装饰器,也可能很多人已经在很多 NestJS 项目中使用装饰器了,也有一些前端开发者可能在某些前端框架中使用过一些简单的装饰器,那么你真的知道装饰器在前端还能玩出哪些花吗?


    我们今天不讲基础概念,也不写一些你可能在很多文章里都看到过的没有意义的示例代码,我们直接拿装饰器来实战实现一些需求:


    一、类装饰器


    虽然很多前端对于类和面向对象是排斥的、抵触的,但不影响我们这篇文章继续来基于面向对象通过装饰器玩一些事情。



    我已经写了很多关于面向对象在前端的使用了,实在是不想在这个问题上继续扯了,可以参考本专栏内的其他文章。


    虽然但是,不论如何,你可以不用,但你不能不会,更不能不学。



    不管在前端还是后端,我们可能都会用到类的实例来做一些事情,比如声明一个用户的类,让用户的类来完成一些事情。


    我们可能会为类配置名称,比如给 User 类定义为 用户:


    // 声明一个装饰器,用来保存类的文案
    function Label(label: string) {
    return (target: any) => {
    Reflect.set(target, "label", label)
    }
    }

    @Label("用户")
    class User {

    }


    我们不限制被标记的类,你可以把any 用泛型约束一下,限制这个装饰器可以标记到哪些类的子类上。



    我们可以通过 Reflect 来获取到类上的元数据,比如 Label 这个类上的 name 属性,通过 Reflect.getMetadata('name', User) 来获取到:


    // 将打印 "用户"
    console.log(Reflect.get(User, "label"))

    通过这种方式,我们可以为类标记很多配置,然后在使用的时候就不会在代码里再出现很多类似 “用户” 的魔法值了。如果有改动的话,也只需要将 @Label("用户") 改成 @Label("XXX") 就好了。


    当然,事实上我们不会单独为了一个小功能去声明一个装饰器,那样到时候会给类上标记很多的 @ 看着难受,于是我们可以直接声明一个 ClassConfig 的装饰器,用来保存类的各种配置:


    interface IClassConfig {
    // 刚才的 label
    label?: string

    // 添加一些其他的配置

    // 表格的空数据文案
    tableEmptyText?: string

    // 表格删除提醒文案
    tableDeleteTips?: string
    }
    function ClassConfig(config: IClassConfig){
    return (target: any) => {
    Reflect.set(target, "config", config)
    }
    }
    @ClassConfig({
    label: "用户",
    tableEmptyText: "用户们都跑光啦",
    tableDeleteTips: "你确定要删除这个牛逼的人物吗?"
    })

    当然,我们可以通过 Reflect.getMetadata('config', User) 来获取到 ClassConfig 这个类上的配置,然后就可以在代码里使用这些配置了.



    比如,我们还封装了一个 Table 组件,我们就只需要将 User 这个类传过去,表格就自动知道没数据的时候应该显示什么文案了:



    <Table :model="User" :list="list" />

    上面的表格内部,可以获取 model 传入的类,再通过 Reflect 来获取到这些配置进行使用,如果没有配置装饰器或者装饰器没有传入这个参数,那么就使用默认值。


    二、属性装饰器


    很多人都知道,装饰器不仅仅可以配置到类上,属性上的装饰器用处更多。


    这个和上面第一点中的一样,也可以为属性做一些配置,比如给用户的账号属性做配置,而且我们还可以根据主要功能来声明不同的装饰器,比如表单的 @Form,表格的 @Table 等等。


    class User {
    @Field({
    label: "账号",
    // 如果下面的没有配置,那我来兜底。
    isEmail: true,
    })
    @Form({
    // 表单验证的时候必须是邮箱
    isEmail: true,
    // 表单验证的时候不能为空
    isRequired: true,
    placeholder: "请输入你牛逼的邮箱账号"
    })
    @Table({
    // 表示表格列的邮箱点击后会打开邮件 App
    isEmail: true,

    // 表格列的宽度
    width: 200,

    // 需要脱敏显示
    isMask: true
    })
    account!: string
    }

    当然,属性的装饰器声明和类的声明方式不太一致:


    interface IFieldConfig {
    label?: string
    isEmail?: boolean
    }
    function Field(config: any) {
    return (target: any, propertyKey: string) => {
    Reflect.set(target, propertyKey, config)
    }
    }

    使用 Reflect 获取的时候也不太一致:


    const fieldConfig = Reflect.get(User.prototype, "account")
    // 将打印出 `@Field` 配置的属性对象
    console.log(fieldConfig)

    想象一下,你封装的表格我也这么使用,我虽然没有传入有哪些表格列,但你是不是能够通过属性是否标记了 @Table 装饰器来判断是否需要显示这个邮箱列呢?


    <Table :model="User" :list="list" />

    你也可以再封装一些其他的组件,比如表单,比如搜索等等等等,像这样:


    <Input :model="User" :field="account" />

    上面的 Input 组件就会自动读取 User 这个类上的 account 属性的配置,然后根据配置来渲染表单和验证表单,是不是美滋滋?


    三、方法装饰器和参数装饰器


    这两个方式的装饰器我们在这篇文章不细讲,等装饰器这玩意在前端能被大家接受,或者前端娱乐圈骂面向对象不再那么狠的时候再细化一下吧,今天我们只讲讲简单使用:


    3.1 方法装饰器


    说到方法装饰器,我想先提一嘴 AOP 编程范式。



    AOP(Aspect Oriented Programming) 是一种编程范式,它把应用程序切面化,即把应用程序的各个部分封装成可重用的模块,然后通过组合这些模块来构建应用程序。



    举个简单的例子,我们最开始写好了很多代码和方法:


    class User {
    add(name: string) {
    console.log("user " + name + " added!")
    }
    delete(name: string) {
    console.log("user " + id + " deleted!")
    }
    }

    const user = new User();
    user.add("Hamm")
    user.delete("Hamm")

    以前调用这些方法都是正常的,突然有一天需求变了,只允许超级管理员才能调用这两个方法,你可能会这么写:


    class User {
    add(name: string) {
    checkAdminPermission()
    console.log("user " + name + " added!")
    }
    // 其他方法
    }

    function checkAdminPermission() {
    if(!你的条件){
    throw new Error("没有权限")
    }
    }
    const user = new User();
    user.add("Hamm")

    虽然也没毛病,但需要去方法内部添加代码,这属于改动了已有的逻辑。



    而 AOP 存在的意义,就是通过切面来修改已有的代码,比如在方法执行前,执行一段代码,在方法执行后,执行一段代码,在方法执行出错时,执行一段代码,等等。用更小的粒度来减少对已有代码的入侵。像这样:



    class User {
    @AdminRequired
    add(name: string) {
    console.log("user " + name + " added!")
    }
    }

    function AdminRequired(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value
    descriptor.value = function (...args: any[]) {
    if (你的条件) {
    return originalMethod.apply(this, args)
    }
    throw new Error("没有权限")
    }
    }

    const user = new User()
    console.log(user.add("Hamm"))

    乍一看,我就知道又会有人说:“你这代码不是更多了么?” 看起来好像是。


    但事实上,从代码架构上来说,这没有对原有的代码做任何改动,只是通过 AOP 的方式,在原有代码的基础上,添加了一些前置方法处理,所以看起来好像多了。但当我再加上一些后置的方法处理的话,代码量并没有添加多少,但结构会更清晰,代码入侵也没有。


    传统写法(入侵)


    class Test{
    张三的方法(){
    // 李四的前置代码

    // 张三巴拉巴拉写好的代码

    // 李四的后置代码
    }
    }


    张三:“李四,你为什么用你的代码包围了我的代码!”



    装饰器写法(不入侵)


    class Test {
    @LiSiWantToDoSomething
    张三的方法() {
    // 张三巴拉巴拉写好的代码
    }
    }
    function LiSiWantToDoSomething(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value
    descriptor.value = function (...args: any[]) {
    console.log("李四的前置代码")
    const result = originalMethod.apply(this, args)
    console.log("张三干完了,结果是" + result)
    return "我是李四,张三的结果被我偷走了"
    }
    }

    这时,张三的代码完全在不改动的情况下添加了前置和后置代码。


    3.2 参数装饰器


    参数装饰器的使用场景在前端比较少,在 Nest 中比较多,这篇文章就不过多介绍了,如果后续大伙有兴趣我们再聊。


    四、总结


    装饰器是一种新的语法,可以让你的前端代码更加的架构化,增加代码的可维护性。


    如果你有兴趣,还可以阅读本专栏内的这些文章:


    用TypeScript和装饰器优雅的为前端数据脱敏


    TypeScript使用枚举封装和装饰器优雅的定义字典]


    TypeScript中如何用装饰器替代JSON配置项封装表单


    TypeScript装饰器之我们是这么做表单和校验的


    当然,其他文章也很有意思哟~


    今天就这样,欢迎继续关注我们的专栏 《用TypeScript写前端》


    也欢迎关注我们的开源项目: AirPower4T,里面有很多装饰器在前端的应用场景,也许可以让你耳目一新。


    Bye.


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

    不要再二次封装 axios 了,它真的是“灵丹妙药”吗?

    web
    引言 最近,看到不少开发者在讨论如何“优雅地”封装 axios 时,我的内心不禁发出一声叹息——“收手吧阿祖,别再封装了!”我们都知道,axios 是一个轻量级的 http 客户端库,广泛用于前端和 node.js 环境中,因其简洁易用、功能丰富而备受喜爱。但...
    继续阅读 »

    引言


    最近,看到不少开发者在讨论如何“优雅地”封装 axios 时,我的内心不禁发出一声叹息——“收手吧阿祖,别再封装了!”我们都知道,axios 是一个轻量级的 http 客户端库,广泛用于前端和 node.js 环境中,因其简洁易用、功能丰富而备受喜爱。但问题来了:为什么那么多人非要二次封装它?是想追求什么“优雅”代码,还是只是满足某种程序员的“封装癖”?在我看来,二次封装 axios 的行为,其实更多的是“低效”和“麻烦”!


    在这篇文章中,我将分析二次封装 axios 的现象,揭示它的弊端,并提出更合理的解决方案。


    二次封装 axios 背后的动机


    首先,得承认,在许多开发者的心目中,二次封装 axios 是“提升代码复用性”、“提升可维护性”的一种手段。究竟是什么驱动他们这么做呢?让我们来看看,通常的封装动机有哪些。



    1. 全局配置管理

      很多人为了避免在每个请求中都写重复的配置(如 baseURL、timeout、headers 等),于是将 axios 封装成一个单独的模块,统一管理。这样一来,代码看似简洁了。

    2. 请求/响应拦截器

      除了常见的全局配置外,二次封装通常还会加入请求和响应拦截器,用于做统一的错误处理、日志记录、token 刷新等。这听起来很有吸引力,似乎让项目更加“健壮”了。

    3. 封装错误处理

      统一处理 HTTP 错误,诸如 400、500 错误等,这让开发者避免了在每个请求中重复编写错误处理逻辑。

    4. 功能扩展

      比如:增加一些额外的功能,比如请求重试、自动刷新 token、请求取消等。


    这些动机听起来有理有据,似乎是为了减少重复代码、提高效率。但,二次封装真的是解决问题的最佳方法吗?


    二次封装 axios 的弊端:看似优雅,实际繁琐


    虽然二次封装看起来很“高级”,但它带来的问题也是显而易见的。


    1. 失去灵活性,降低可维护性


    当我们通过二次封装 axios 将所有请求逻辑集中到一个地方时,代码复用的确得到了提高,但灵活性却大大下降。每当我们需要调整请求方式、处理特殊错误、或者添加新的请求功能时,必须在封装层修改代码,这意味着对每一个新请求的修改都变得更加复杂,导致代码膨胀,维护成本上升。


    举个例子,假设你有一个简单的请求需要添加一个额外的请求头或参数,但你的封装类已经把一切都“包裹”得很严实,你不得不进入封装类的内部进行修改。这种情况下,封装的意义反而变得虚假。


    2. 过度封装,增加不必要的复杂性


    封装本应是为了简化代码,但过度封装反而让事情变得更加复杂。例如,很多二次封装的 axios 都包含了一堆的“自定义配置”,导致请求时你不得不先了解封装类的具体实现,甚至可能在不同项目之间迁移时也要重新学习一套封装规范。每次需要调用 api 的时候,都要与一个封装层打交道,这显然不是开发者想要的高效体验。


    3. 性能问题:拦截器是双刃剑


    请求和响应拦截器的设计初衷无疑是为了统一处理请求逻辑,但过多的拦截器往往会导致性能瓶颈。特别是在大型项目中,拦截器的链式执行可能带来额外的延迟。此外,拦截器中常常会加入错误处理、token 刷新等额外逻辑,这会影响整个请求链的执行效率。


    4. 可能引发版本兼容性问题


    随着项目的不断迭代,封装的代码与 axios 原生的更新频繁不一致,导致二次封装的代码容易发生维护上的“断层”或兼容性问题。每当 axios 更新时,可能需要你手动修复封装类中的依赖,甚至重构整个封装层,造成额外的开发工作量。


    为什么我们不需要二次封装 axios?


    那么,既然二次封装带来了这么多麻烦,我们应该如何解决 axios 使用中的痛点呢?


    1. 使用 axios 的内置功能


    axios 本身就有非常强大的功能,很多二次封装中提到的配置(比如 baseURL、headers 等)都可以直接通过 axios 的实例化来轻松解决。例如:


    const axiosInstance = axios.create({
    baseURL: 'https://api.example.com',
    timeout: 1000,
    headers: {'X-Custom-Header': 'foobar'}
    });

    这样,所有请求都可以通过统一的实例管理,无需复杂的封装,且灵活性保持不变。


    2. 合理使用拦截器


    axios 的请求和响应拦截器非常强大,用得好可以让你的代码更简洁。错误处理、token 刷新、请求取消等功能,都可以直接在 axios 拦截器中完成,而不需要一个额外的封装类。


    axios.interceptors.response.use(
    response => response,
    error => {
    if (error.response.status === 401) {
    // Token 刷新逻辑
    }
    return Promise.reject(error);
    }
    );

    通过这种方式,我们在全局进行处理,而不需要一层一层的封装,让代码保持简洁并且具有良好的扩展性。


    3. 利用第三方库增强功能


    如果你确实需要一些特殊的功能(比如请求重试、缓存、自动重定向等),可以使用现成的第三方库,而不是自己重复造轮子。比如:



    • axios-retry:轻松实现请求重试

    • axios-cache-adapter:请求缓存

    • axios-auth-refresh:自动刷新 token


    这些库都能与 axios 配合得很好,帮助你解决二次封装中出现的某些功能问题,避免在项目中增加冗余的封装层。


    4. 模块化的请求管理


    而对于需要统一管理的 api 请求,推荐将每个请求模块化,分层管理,而不是在一个封装类中把所有请求都硬编码。你可以根据需求将每个 api 的请求抽象成一个独立的函数或模块,保持高内聚低耦合的设计。


    // api/user.js
    export function getUserInfo(userId) {
    return axios.get(`/users/${userId}`);
    }

    这样做的好处是,当某个接口发生变化时,只需要修改相应的模块,而不需要担心影响到其他的请求。


    总结


    二次封装 axios 是一种源自“代码复用”的良好初衷,但它往往带来了灵活性不足、复杂度增加、性能损失等一系列问题。在面对实际开发中的 http请求时,我们可以通过直接使用 axios 的内置功能、合理利用拦截器、借助现成的第三方库以及模块化管理等方式来更高效、更优雅地解决问题。


    所以,不要再二次封装 axios 了,它并不是“灵丹妙药”。让我们回归简单,享受 axios 原生的优雅与高效吧!


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

    不懂这些GIS基础,开发Cesium寸步难行!

    web
    大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端提效、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第30/100篇文章。 前言 想学Cesium开发,你如果对一些GIS基础,特别是坐标系概念不了解的话,会让你非常难受,今天我们就...
    继续阅读 »

    大家好,我是日拱一卒的攻城师不浪,专注可视化、数字孪生、前端提效、nodejs、AI学习、GIS等学习沉淀,这是2024年输出的第30/100篇文章。



    前言


    想学Cesium开发,你如果对一些GIS基础,特别是坐标系概念不了解的话,会让你非常难受,今天我们就来聊聊WebGiser开发过程中常用到的一些坐标系概念。


    GIS坐标系


    要熟悉Cesium中常用到的一些坐标类型以及它们之间是如何进行转换的,到了真正用到的时候可以再返回来细看,加深理解。


    经纬度坐标(球面坐标)


    经纬度坐标通常被称为地理坐标地球坐标,它是一种基于地球表面的坐标系统,用于确定地球上任何点的位置。这种坐标系统使用两个主要的数值来表示位置:经度和纬度。




    1. 经度(Longitude):表示从本初子午线(通常通过英国伦敦的格林尼治天文台)向东或向西的角度距离。经度的范围是从 -180° 到 +180°,其中 0° 表示本初子午线。

    2. 纬度(Latitude):表示从赤道向北或向南的角度距离。纬度的范围是从 -90°(南极点)到 +90°(北极点),其中 0° 表示赤道。


    经纬度坐标也常常被称为:



    • 球面坐标(Spherical Coordinates):因为地球近似为一个球体,经纬度坐标可以看作是在球面上确定点的位置。

    • 大地坐标(Geodetic Coordinates):在大地测量学中,这种坐标系统用于描述地球表面的点。

    • WGS84坐标:WGS84(World Geodetic System 1984)是一种广泛使用的全球地理坐标系统,它提供了一个标准化的参考框架,用于地理定位。


    经纬度坐标广泛应用于地图制作、导航、地理信息系统(GIS)、航空和海洋导航等领域。在数字地图服务和应用程序中,经纬度坐标是最常见的位置表示方式之一。


    地理坐标(弧度)


    在地理信息系统(GIS)中,地理坐标通常指的是地球上某个点的位置,使用经纬度来表示。然而,由于地球是一个近似的椭球体,使用弧度而非角度来表示经纬度坐标可以避免在计算中引入的某些复杂性,尤其是在进行距离和面积的测量时。



    弧度是一种角度的度量单位,它基于圆的周长和半径之间的关系。一个完整的圆周被定义为 2π弧度。弧度与角度的转换关系如下:



    在GIS中,使用弧度的地理坐标可以简化一些数学运算,尤其是涉及到三角函数和地球曲率的计算。例如,计算两点之间的大圆距离(即地球表面的最短路径)时,使用弧度可以更直接地应用球面三角学公式。


    地理坐标(弧度)的应用



    1. 距离计算:使用球面三角学公式,可以更准确地计算出两点之间的距离。

    2. 方向计算:确定从一个点到另一个点的方向,使用弧度可以简化计算过程。

    3. 地图投影:在某些地图投影中,使用弧度可以更自然地处理地球表面的曲率。


    屏幕坐标系


    屏幕坐标系(Screen Coordinate System)是一种二维坐标系统,它用于描述屏幕上的点或区域的位置。屏幕坐标系通常以屏幕的左上角为原点,水平向右为 x 轴正方向,垂直向下为 y 轴正方向。



    屏幕坐标系在Cesium中叫做二维笛卡尔平面坐标。


    new Cesium.Cartesian2(x, y)

    屏幕坐标系的特点:



    1. 原点位置:屏幕坐标系的原点(0,0)位于屏幕的左上角

    2. 正方向:x 轴正方向向右,y 轴正方向向下。

    3. 单位:通常使用像素(px)作为单位。

    4. 范围:坐标值的范围取决于屏幕或窗口的大小。


    空间直角坐标系


    在地理信息系统(GIS)中,空间直角坐标系(Spatial Cartesian Coordinate System)是一种三维坐标系统,用于在三维空间中精确地表示点、线、面的位置。这种坐标系通常由三个正交的坐标轴组成:X、Y 和 Z 轴。



    空间直角坐标系的特点:



    1. 正交性:X、Y 和 Z 轴相互垂直,形成一个直角坐标系。

    2. 三维性:可以表示三维空间中的任何位置,包括高度或深度信息。

    3. 标准化:通常以地球的质心或某个参考点为原点,建立一个标准化的坐标系统。

    4. 应用广泛:广泛应用于地理测量、城市规划、建筑设计、3D 建模等领域。


    Cesium中的坐标系


    Cesium中支持两种坐标系:3D笛卡尔坐标系经纬度坐标系


    3D笛卡尔坐标系


    先来了解下笛卡尔空间直角坐标系,它的X、Y、Z三个轴的正方向如下图所示:



    坐标系的原点位于地球的中心。因此,这些坐标通常是负的。单位通常是


    Cesium.Cartesian3(x, y, z)

    地理坐标系


    是一种基于经度和纬度的坐标系,它使用度数来表示位置。


    在Cesium中,地理坐标可以通过将经度、纬度和高度值传递给Cartographic对象来表示。


    其中经度和纬度是以度数表示的,高度值可以是以米或其他单位表示的。


    Cesium将地理坐标转换为笛卡尔坐标以在地球表面上进行可视化。


    坐标系转换


    Cesium提供了很多坐标系互相转换的大类。


    经纬度转空间直角


    const cartesian3 = Cesium.Cartesian3.fromDegrees(lng, lat, height);

    经纬度转地理坐标(弧度)


    const radians = Cesium.Math.toRadians(degrees) 

    地理坐标(弧度)转经纬度


    const degrees = Cesium.Math.toDegrees(radians) 

    空间直角转经纬度


    // 先将3D笛卡尔坐标转为地理坐标(弧度) 
    const cartographic = Cesium.Cartographic.fromCartesian(cartesian3);
    // 再将地理坐标(弧度)转为经纬度
    const lat = Cesium.Math.toDegrees(cartographic.latitude);
    const lng = Cesium.Math.toDegrees(cartographic.longitude);
    const height = cartographic.height;

    屏幕坐标转经纬度


    // 监听点击事件,拾取坐标
    const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
    handler.setInputAction((e) => {
    const clickPosition = viewer.scene.camera.pickEllipsoid(e.position);
    const randiansPos = Cesium.Cartographic.fromCartesian(clickPosition);
    console.log(
    "经度:" +
    Cesium.Math.toDegrees(randiansPos.longitude) +
    ", 纬度:" +
    Cesium.Math.toDegrees(randiansPos.latitude)
    );
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    屏幕坐标转空间直角坐标


    var cartesian3 = viewer.scene.globe.pick(viewer.camera.getPickRay(windowPostion),    viewer.scene); 

    世界坐标转屏幕坐标


    windowPostion = Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian3); 

    结语



    作者的Cesium系列课程**《Cesium从入门到实战》**即将完结,课程介绍:ww7rybwvygd.feishu.cn/docx/PG1TdA…



    如果想自学Cesium的也可以参考我的【开源项目】:github.com/tingyuxuan2…



    有需要进技术产品开发交流群(可视化&GIS)可以加我:brown_7778(备注来意),也欢迎数字孪生可视化领域的交流合作。



    最后,如果觉得文章对你有帮助,也希望可以一键三连👏👏👏,支持我持续开源和分享~


    作者:攻城师不浪
    来源:juejin.cn/post/7404091675666055209
    收起阅读 »

    苹果 visionOS for web

    web
    苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。 我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。 一开始,我以为这不会太难...
    继续阅读 »

    苹果的 Vision Pro 已经发布了,虽然没有拿到手,但我还是对它的操作界面充满了好奇。


    我看到很多小伙伴写了 Windows for Web,Mac OS for Web,所以我也想来实现一下 Vision Pro 的系统主页。


    一开始,我以为这不会太难,当头一棒的就是苹果祖传优势: 动画。


    CPT2401291503-845x461.gif


    这动画,这模糊,还是从中心点开始逐渐向外层扩散,应该根据人眼的视觉特征进行设计的。


    问题是,该如何实现呢?


    模糊我知道怎么实现,


    filter: blur(15px);

    从中心点开始逐渐向外层扩散的效果,我直接来个
    transition-delay: 0.1s;


    一通操作之下,也实现就似是而非的效果。而且边缘处app图标的缓缓落下的效果也不好。


    CPT2401291508-1281x733.gif


    然后就是光影效果的实现,因为它的很美,让人很难忽略。


    在 Vision Pro 系统演示中可以看出,为了模拟菜单栏使用了磨砂玻璃材质,而为了营造真实感,会模拟光照射到玻璃上而形成的光线边框。


    我不知道这是不是菲涅尔效应,但问题是,这又该如何在前端实现呢?


    我想到了 CSS Houdini,可以利用 Houdini 开放的底层能力 paint 函数来实现一个菜单栏效果。


    if ('paintWorklet' in CSS) {
    CSS.paintWorklet.addModule('data:text/javascript,' + encodeURIComponent(`

    class FresnelAppRectPainter {
    static get inputProperties() { return ['--light-angle']; }

    paint(ctx, size, properties) {
    const borderRadius = 30;
    const fresnelColor = 'rgba(255, 255, 255, .9)';
    const lightAngle = parseFloat(properties.get('--light-angle')[0]) || 0;

    // 绘制圆角矩形
    ctx.beginPath();
    ctx.moveTo(borderRadius, 0);
    ctx.lineTo(size.width - borderRadius, 0);
    ctx.arcTo(size.width, 0, size.width, borderRadius, borderRadius);
    ctx.lineTo(size.width, size.height - borderRadius);
    ctx.arcTo(size.width, size.height, size.width - borderRadius, size.height, borderRadius);
    ctx.lineTo(borderRadius, size.height);
    ctx.arcTo(0, size.height, 0, size.height - borderRadius, borderRadius);
    ctx.lineTo(0, borderRadius);
    ctx.arcTo(0, 0, borderRadius, 0, borderRadius);
    ctx.closePath();
    ctx.fillStyle = 'rgba(163, 163, 163)';
    ctx.fill();

    // 模拟光照效果
    const gradient = create360Gradient(ctx, size, lightAngle)
    ctx.fillStyle = gradient;
    ctx.fill();

    // 添加菲涅尔效果
    const borderGradient = ctx.createLinearGradient(0, 0, size.width, size.height);
    borderGradient.addColorStop(0, fresnelColor);
    borderGradient.addColorStop(0.2, 'rgba(255,255,255, 0.7)');
    borderGradient.addColorStop(1, fresnelColor);

    ctx.strokeStyle = borderGradient;
    ctx.lineWidth = 1.5;
    ctx.stroke();
    }
    }

    registerPaint('fresnelAppRect', FresnelAppRectPainter);
    `));
    }

    结果效果还可以,我甚至可以接收一个光的入射角度,来实时绘制光影效果。


     function create360Gradient(ctx, size, angle) {
    // 将角度转换为弧度
    const radians = angle * Math.PI / 180;

    // 计算渐变的起点和终点
    const x1 = size.width / 2 + size.width / 2 * Math.cos(radians);
    const y1 = size.height / 2 + size.height / 2 * Math.sin(radians);
    const x2 = size.width / 2 - size.width / 2 * Math.cos(radians);
    const y2 = size.height / 2 - size.height / 2 * Math.sin(radians);

    // 创建线性渐变
    const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
    gradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)');
    gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');

    return gradient;
    }

    CPT2401291454-249x209.gif


    演示效果图


    哦对了,还有一个弹层底边角的缩放效果,我目前还没想到什么好办法来实现,年底还得抓紧搬砖,只能先搁置了,如果小伙伴们有好办法,欢迎告知或者讨论。


    1706511484530.png


    最终效果图


    这里是 Demo 地址


    本来是冲着纯粹娱乐(蹭流量)来写的,但写着写着就发现好像没那么简单,三个晚上过去,也只写了个首页,不得不感慨苹果真的太细了呀。


    以上。


    作者:于益
    来源:juejin.cn/post/7329280514627600425
    收起阅读 »

    美女运营老师,天天找我改配置,我用node给她写了个脚本,终于安静了

    web
    美女运营老师,天天找我改配置,给她写了个脚本,终于安静了 事情的起因是,加入到新的小组中,在开发低代码后台管理页面的需求,需要配置一些下拉选项,后端因为一些特殊的原因,没法提供api接口,所以需要前端写成配置选项。这样问题就来了,新增了选项,但是没有给前端配置...
    继续阅读 »

    美女运营老师,天天找我改配置,给她写了个脚本,终于安静了


    事情的起因是,加入到新的小组中,在开发低代码后台管理页面的需求,需要配置一些下拉选项,后端因为一些特殊的原因,没法提供api接口,所以需要前端写成配置选项。这样问题就来了,新增了选项,但是没有给前端配置。美女运营老师都会来找开发,说:为什么新导入的数据没有显示啊,是不是有bug。。找了一圈发现是配置没加


    请在此添加图片描述


    我让运营老师,把新增数据表格给我配置下,丢过来新增数据上来就是1000+,手动加要哭死。于是我就想能否用脚本生成一个。


    刚开始借用在线CSV转换JSON


    在线CSV转换JSON


    把csv下载到本地,转换成json,返回数据倒是能返回,但是不符合运营老师的要求,key值需要是 key: ${data.value}-${data.key}


    请在此添加图片描述


    请在此添加图片描述


    于是我就写了下面第一个简单版的node脚本


    const fs = require('fs')
    const csv = require('csv-parser');

    const uidsfilePath = './uids.json';

    const datas = [`复制生成的json数据`];
    let newarr = [];
    format(datas);

    fs.writeFile(uidsfilePath, JSON.stringify(newarr), () => {
    console.log('done')
    })

    const format = (results) => {
    newarr = results.map(item => {
    return {
    label: `${item.value}-${item.key}`,
    value: item.value
    }
    })
    }

    okok 到这里可以生成了。但是吧,想把这个事情还给运营老师,嘿


    于是我又在这个基础上加上了读取CSV文件,格式化数据,输出JSON文件


    使用 csv-parser读取 csv文件


    csv-parser 是一个为Node.js设计的高效且流线型库,专注于快速解析CSV数据。它旨在提供最小的开销,保持简单轻量,特别适配于Node.js的流处理。此库遵循RFC 4180标准,并通过csv-spectrum的酸性测试套件,确保了对各种CSV变体的广泛兼容性和正确性。性能方面,csv-parser在处理大文件时表现出色,无论是带引号还是不带引号的CSV数据。


    快速使用csv-parser


    开始使用csv-parser,首先确保你的环境中已安装Node.js。接着,通过npm安装


    csv-parser:


    npm install csv-parser

    示例代码

    const fs = require('fs');
    const parse = require('csv-parser');
    fs.createReadStream('yourfile.csv')
    .pipe(parse({ headers: true }))
    .on('data', (row) => {
    console.log(row);
    })
    .on('end', () => {
    console.log('CSV file successfully processed');
    });

    第二版脚本


    直接获取csv文件,生成输出JSON


    const fs = require('fs')
    const csv = require('csv-parser');

    const csvfilePath = './新增UID.csv';
    const uidsfilePath = './uids.json';

    const results = [];
    let newarr = [];

    fs.createReadStream(csvfilePath)
    .pipe(csv({ headers: true }))
    .on('data', (data) => {
    results.push(data);
    })
    .on('end',async () => {
    await format(results);
    fs.writeFile(uidsfilePath, JSON.stringify(newarr), () => {
    console.log('done')
    })
    });
    const format = (results) => {
    newarr = results.map(item => {
    if(item._0 === 'key' || item._1 === 'value') {
    return {}
    }
    return {
    label: `${item._1}-${item._0}`,
    value: item._1
    }
    })
    }

    部分生成的数据


    请在此添加图片描述


    到这里又节省了一些时间,但是运营老师既不会安装node,也不会使用命令执行node CSVtoJSON.js,太复杂了不会弄。。。我说你提个需求吧,后面给您做成页面上传csv文件,返回JSON数据,点击一键复制好不好。


    仅供娱乐,欢迎吐槽


    未完待续,持续更新中...


    感谢关注点赞评论~


    作者:一起重学前端
    来源:juejin.cn/post/7442489501590044672
    收起阅读 »

    【环信uniapp uikit】手把手教你uniapp uikit运行到鸿蒙

    写在前面:好消息好消息,环信uniapp出uikit啦~更好的消息,环信uniapp sdk也支持鸿蒙系统啦!!!!那么我们一起来看看uniapp uikit如何运行到鸿蒙系统~~let's gouniapp uikit以及支持鸿蒙系统的uniapp sdk版...
    继续阅读 »

    写在前面:
    好消息好消息,环信uniapp出uikit啦~
    更好的消息,环信uniapp sdk也支持鸿蒙系统啦!!!!那么我们一起来看看uniapp uikit如何运行到鸿蒙系统~~let's go
    uniapp uikit以及支持鸿蒙系统的uniapp sdk版本都是`4.11.0`



    准备工作
    1. HBuilderX 4.36
    2. DevEco-Studio 5.0.5.310
    3. sass:sass-loader 10.1.1 及之前版本
    4. node:12.13.0 - 17.0.0,推荐 LTS 版本 16.17.0
    5. npm:版本需与 Node.js 版本匹配
    6. 已经在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了App Key
    7. 了解uniapp创建运行鸿蒙系统
    8. 了解uniapp UIkit各功能以及api调用



    开始集成:
    第一步:创建一个uniapp+vue3项目进度10%

    第二步:安装依赖  进度15%
    npm init -y

    npm i easemob-websdk@4.11.0 pinyin-pro@3.26.0 mobx@6.13.4 --save
    第三步:下载uniapp uikit源码 进度20%

    git clone https://github.com/easemob/easemob-uikit-uniapp.git
    第四步:拷贝uikit组件 进度50%

    mkdir -p ./ChatUIKit 

     # macOS 

     mv ${组件项目路径}/ChatUIKit/* ./ChatUIKit 

     # windows 

     move ${组件项目路径}/ChatUIKit/* .\ChatUIKit



    第五步:替换pages/index/index.vue文件 进度70%






    第六步:替换app.vue文件 进度80%




    第七步:在pages.json配置路由 进度90%

    {
    "pages": [
    {
    "path": "pages/index/index",
    "style": {
    "navigationBarTitleText": "uni-app"
    }
    },
    {
    "path": "ChatUIKit/modules/Chat/index",
    "style": {
    "navigationStyle": "custom",
    // #ifdef MP-WEIXIN
    "disableScroll": true,
    // #endif
    "app-plus": {
    "bounce": "none",
    "softinputNavBar": "none"
    }
    }
    },
    {
    "path": "ChatUIKit/modules/VideoPreview/index",
    "style": {
    "navigationBarTitleText": "Video Preview",
    "app-plus": {
    "bounce": "none"
    }
    }
    }
    ]
    }





    第八步:运行到chrome浏览器看下效果 进度90% 



     第九步:运行到鸿蒙并发送一条消息 进度100%





    遇到的问题:
    问题1:
    详细报错信息如下
    hvigor ERROR: Invalid product for target 'default'.
    Detail: Check the target applyToProducts field for 'default': [ 'default', 'release' ].
    at /Users/admin/Desktop/ouyeel_worksheet/unpackage/debug/app-harmony-2f573459/build-profile.json5
    解决方案: 
    在harmony-configs/build-profile.json5文件,复制default配置,将default改为relese,参考


    问题2:
    登录报错

    解决方案:
    在/harmony-configs/entry/src/main/module.json5文件添加以下代码

    "requestPermissions": [ {"name": "ohos.permission.GET_NETWORK_INFO"}, { "name": "ohos.permission.INTERNET"}, ],


    问题3:
    HBuilderX无限重连

    解决方案:

    看看sdk 是不是最新版,无限重连的问题已经在 4.12.0版本的sdk修复~

    总结:
    初步运行到鸿蒙的话问题会比较多,大家可以善用百度大法解决掉它!!!


    收起阅读 »

    老板不让用ECharts,还想让我画很多圆环!

    web
    需求简介 这几天下来个新需求:要在页面上动态渲染多个表格,每个表格内还要实现若干圆环! 刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷 然而,老板无情的拒绝了我!他说: 咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而...
    继续阅读 »

    需求简介


    这几天下来个新需求:要在页面上动态渲染多个表格,每个表格内还要实现若干圆环!



    刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷



    然而,老板无情的拒绝了我!他说:



    咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而且一个页面这么多ECharts实例,性能怎么保障?不准用ECharts,用CSS实现!



    没办法,我只好百度如何用CSS画圆环。幸运的是,我确实找到了类似的文章:



    不幸的事,效果和我的差异很大,代码根本 无法复用!没办法,只能用别的办法实现了。经过一番研究,最终借助Canvas搞出来了,简单的分享一下我的实现思路吧。


    圆环组件简介


    为了方便复用,我把这个功能封装成了项目可直接复用的组件。并支持自定义圆环大小、圆环宽度和圆环颜色比例配置属性。


    <Ring
    :storkWidth="5"
    :size="60"
    :ratioList="[
    { ratio: 0.3, color: '#FF5733' },
    { ratio: 0.6, color: '#33FF57' },
    { ratio: 0.1, color: '#3357FF' }
    ]"

    ></Ring>


    技术方案


    实现目标


    根据我们的UX需求,我们需要实现一个简单的组件,该组件可以展示一个圆环图表,并根据外部传入的比例数据(如 ratioList)绘制不同颜色的环形区域。



    • 使用 Vue 3 和 TypeScript。

    • 动态绘制环形图,根据传入的数据绘制多个环。

    • 支持自定义环形图的大小和环宽。


    创建 Vue 组件框架


    首先,我们创建一个名为 RingChart.vue的组件。组件的初始结构非常简单,只包含一个 canvas 元素。


    <template>
    <!-- 创建一个 canvas 元素,用于绘制图表 -->
    <canvas ref="canvasDom"></canvas>
    </template>

    <script lang="ts" setup>
    import { ref, onMounted } from 'vue';

    // 获取 canvas DOM 元素的引用
    const canvasDom = ref<HTMLCanvasElement | null>(null);

    // 初始化 canvas 和上下文变量
    let ctx: CanvasRenderingContext2D | null = null;
    let width: number, height: number;

    // 初始化 canvas 尺寸和绘图环境
    const initCanvas = () => {
    const dom = canvasDom.value;
    if (!dom) return;
    ctx = dom.getContext('2d');
    if (!ctx) return;

    // 设置 canvas 的宽高
    dom.width = dom.offsetWidth;
    dom.height = dom.offsetHeight;
    width = dom.offsetWidth;
    height = dom.offsetHeight;
    };

    // 在组件挂载后执行初始化
    onMounted(() => {
    initCanvas();
    });
    </script>

    <style scoped>
    canvas {
    width: 100%;
    height: 100%;
    }
    </style>

    上述代码中,我们初始化了 canvas 元素,并且设定了 widthheight 属性。


    绘制基本的圆环


    接下来,我们添加绘制圆环的功能:通过 arc 方法来绘制圆环,设置 lineWidth 来调整环的宽度。


    <script lang="ts" setup>
    import { ref, onMounted } from 'vue';

    // 获取 canvas DOM 元素的引用
    const canvasDom = ref<HTMLCanvasElement | null>(null);

    // 初始化 canvas 和上下文变量
    let ctx: CanvasRenderingContext2D | null = null;
    let width: number, height: number;

    // 初始化 canvas 尺寸和绘图环境
    const initCanvas = () => {
    const dom = canvasDom.value;
    if (!dom) return;
    ctx = dom.getContext('2d');
    if (!ctx) return;

    // 设置 canvas 的宽高
    dom.width = dom.offsetWidth;
    dom.height = dom.offsetHeight;
    width = dom.offsetWidth;
    height = dom.offsetHeight;

    // 调用绘制圆环的方法
    drawCircle({
    ctx,
    x: width / 2,
    y: height / 2,
    radius: 8,
    lineWidth: 4,
    color: '#C4C9CF4D',
    startAngle: -Math.PI / 2,
    endAngle: Math.PI * 1.5,
    });
    };

    // 绘制一个圆环的方法
    const drawCircle = ({
    ctx,
    x,
    y,
    radius,
    lineWidth,
    color,
    startAngle,
    endAngle,
    }: {
    ctx: CanvasRenderingContext2D;
    x: number;
    y: number;
    radius: number;
    lineWidth: number;
    color: string;
    startAngle: number;
    endAngle: number;
    }
    ) => {
    ctx.beginPath();
    ctx.arc(x, y, radius, startAngle, endAngle);
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = color;
    ctx.stroke();
    ctx.closePath();
    };

    onMounted(() => {
    initCanvas();
    });
    </script>


    • drawCircle 函数是绘制圆环的核心。我们通过 arc 方法绘制圆形路径,使用 lineWidth 来调整环的宽度,并用 strokeStyle 给圆环上色。

    • startAngleendAngle 参数决定了圆环的起始和结束角度,通过改变它们可以控制环的覆盖区域。


    绘制多个环形区域


    现在,我们来实现绘制多个环形区域的功能。我们将通过传入一个 ratioList 数组来动态生成多个环,每个环代表不同的比例区域。


    <script lang="ts" setup>
    import { ref, computed, onMounted } from 'vue';

    // 定义 props 的类型
    interface RatioItem {
    ratio: number;
    color: string;
    }

    const props = defineProps<{
    size?: number; // 画布大小
    storkWidth?: number; // 环的宽度
    ratioList?: RatioItem[]; // 比例列表
    }>();

    // 默认值
    const defaultSize = 200;
    const defaultStorkWidth = 4;
    const defaultRatioList: RatioItem[] = [
    { ratio: 1, color: '#C4C9CF4D' },
    ];

    // canvas DOM 和上下文
    const canvasDom = ref<HTMLCanvasElement | null>(null);
    let ctx: CanvasRenderingContext2D | null = null;

    // 动态计算 canvas 的中心点和半径
    const size = computed(() => props.size || defaultSize);
    const center = computed(() => ({
    x: size.value / 2,
    y: size.value / 2,
    }));
    const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));

    // 初始化 canvas
    const initCanvas = () => {
    const dom = canvasDom.value;
    if (!dom) return;

    ctx = dom.getContext('2d');
    if (!ctx) return;

    dom.width = size.value;
    dom.height = size.value;

    drawBackgroundCircle();
    drawDataRings();
    };

    // 绘制背景圆环
    const drawBackgroundCircle = () => {
    if (!ctx) return;

    drawCircle({
    ctx,
    x: center.value.x,
    y: center.value.y,
    radius: radius.value,
    lineWidth: props.storkWidth || defaultStorkWidth,
    color: '#C4C9CF4D',
    startAngle: -Math.PI / 2,
    endAngle: Math.PI * 1.5,
    });
    };

    // 绘制数据圆环
    const drawDataRings = () => {
    const { ratioList = defaultRatioList } = props;
    if (!ctx) return;

    let startAngle = -Math.PI / 2;
    ratioList.forEach(({ ratio, color }) => {
    const endAngle = startAngle + ratio * Math.PI * 2;

    drawCircle({
    ctx,
    x: center.value.x,
    y: center.value.y,
    radius: radius.value,
    lineWidth: props.storkWidth || defaultStorkWidth,
    color,
    startAngle,
    endAngle,
    });

    startAngle = endAngle;
    });
    };

    // 通用绘制函数
    const drawCircle = ({
    ctx,
    x,
    y,
    radius,
    lineWidth,
    color,
    startAngle,
    endAngle,
    }: {
    ctx: CanvasRenderingContext2D;
    x: number;
    y: number;
    radius: number;
    lineWidth: number;
    color: string;
    startAngle: number;
    endAngle: number;
    }
    ) => {
    ctx.beginPath();
    ctx.arc(x, y, radius, startAngle, endAngle);
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = color;
    ctx.stroke();
    ctx.closePath();
    };

    // 监听画布大小变化
    onMounted(() => {
    initCanvas();
    });
    </script>

    上述代码中,我们通过 ratioList 数组传递每个环的比例和颜色,使用 startAngleendAngle 来控制每个环的绘制区域。其中,drawDataRings 函数遍历 ratioList,根据每个数据项的比例绘制环形区域。


    现在,我们的组件就实现完毕了,可以在其他地方引入使用了


    <RingChart
    :storkWidth="8"
    :size="60"
    :ratioList="[
    { ratio: 0.3, color: '#F8766F' },
    { ratio: 0.6, color: '#69CD90' },
    { ratio: 0.1, color: '#FFB800' }
    ]"

    ></RRingChart>


    组件代码


    <template>
    <canvas ref="canvasDom"></canvas>
    </template>

    <script lang="ts" setup>
    import { ref, computed, onMounted, watchEffect } from 'vue';

    // 定义 props 的类型
    interface RatioItem {
    ratio: number;
    color: string;
    }

    const props = defineProps<{
    size?: number; // 画布大小
    storkWidth?: number; // 环的宽度
    ratioList?: RatioItem[]; // 比例列表
    }>();

    // 默认值
    const defaultSize = 200; // 默认画布宽高
    const defaultStorkWidth = 4;
    const defaultRatioList: RatioItem[] = [{ ratio: 1, color: '#C4C9CF4D' }];

    // canvas DOM 和上下文
    const canvasDom = ref<HTMLCanvasElement | null>(null);
    let ctx: CanvasRenderingContext2D | null = null;

    // 动态计算 canvas 的中心点和半径
    const size = computed(() => props.size || defaultSize);
    const center = computed(() => ({
    x: size.value / 2,
    y: size.value / 2
    }));
    const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));

    // 初始化 canvas
    const initCanvas = () => {
    const dom = canvasDom.value;
    if (!dom) return;

    ctx = dom.getContext('2d');
    if (!ctx) return;

    dom.width = size.value;
    dom.height = size.value;

    drawBackgroundCircle();
    drawDataRings();
    };

    // 绘制背景圆环
    const drawBackgroundCircle = () => {
    if (!ctx) return;

    drawCircle({
    ctx,
    x: center.value.x,
    y: center.value.y,
    radius: radius.value,
    lineWidth: props.storkWidth || defaultStorkWidth,
    color: '#C4C9CF4D',
    startAngle: -Math.PI / 2,
    endAngle: Math.PI * 1.5
    });
    };

    // 绘制数据圆环
    const drawDataRings = () => {
    const { ratioList = defaultRatioList } = props;
    if (!ctx) return;

    let startAngle = -Math.PI / 2;
    ratioList.forEach(({ ratio, color }) => {
    const endAngle = startAngle + ratio * Math.PI * 2;

    drawCircle({
    ctx,
    x: center.value.x,
    y: center.value.y,
    radius: radius.value,
    lineWidth: props.storkWidth || defaultStorkWidth,
    color,
    startAngle,
    endAngle
    });

    startAngle = endAngle;
    });
    };

    // 通用绘制函数
    const drawCircle = ({
    ctx,
    x,
    y,
    radius,
    lineWidth,
    color,
    startAngle,
    endAngle
    }: {
    ctx: CanvasRenderingContext2D;
    x: number;
    y: number;
    radius: number;
    lineWidth: number;
    color: string;
    startAngle: number;
    endAngle: number;
    }
    ) => {
    ctx.beginPath();
    ctx.arc(x, y, radius, startAngle, endAngle);
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = color;
    ctx.stroke();
    ctx.closePath();
    };

    // 监听画布大小变化
    watchEffect(() => {
    initCanvas();
    });

    onMounted(() => {
    initCanvas();
    });
    </script>

    <style scoped>
    canvas {
    display: block;
    margin: auto;
    border-radius: 50%;
    }
    </style>

    使用


    <Ring
    :storkWidth="5"
    :size="60"
    :ratioList="[
    { ratio: 0.3, color: '#FF5733' },
    { ratio: 0.6, color: '#33FF57' },
    { ratio: 0.1, color: '#3357FF' }
    ]"

    ></Ring>

    总结


    累了,今天不想写总结!


    作者:快乐就是哈哈哈
    来源:juejin.cn/post/7444014749321510963
    收起阅读 »

    三维gis的就业前景如何

    一、前言 三维GIS是一个伪概念,GIS是地理信息系统,三维GIS就是三维地理信息系统,在课本上,专业概念上,也没有这一说法吧,所以三维GIS,就是技术人员造概念拼凑造出来的,本质上就是GIS三维可视化。 二、详细介绍 GIS从广义层面分为两大方面,第一是数据...
    继续阅读 »

    一、前言


    三维GIS是一个伪概念,GIS是地理信息系统,三维GIS就是三维地理信息系统,在课本上,专业概念上,也没有这一说法吧,所以三维GIS,就是技术人员造概念拼凑造出来的,本质上就是GIS三维可视化。


    二、详细介绍


    GIS从广义层面分为两大方面,第一是数据及数据管理,第二是业务功能,其中业务,就是分析、显示、交互。按照云计算服务划分,分别对应着DAAS层和SAAS层。按照所谓三维GIS的说法,那就是三维数据管理、三维分析、三维显示、三维交互。目前三维GIS基本上就是围绕着三维数据、三维分析、三维数据显示以及三维交互这四方面展开,其中三维数据是主要表现力。


    1.三维数据


    三维GIS场景与游戏场景的最主要也是最本质区别在于,三维GIS的数据都是真实的,来源于真实世界映射数据,包括卫星影像、三维地形DEM/DSM/DTM、倾斜摄影、点云等各式各样数据。游戏场景绝大部分人工建模出来的理想中的场景。一个是面向现实,一个面向未来,如果把两者结合,是不是有“元宇宙”的味道了呢?
    在这里插入图片描述


    2.三维分析


    三维分析,即基于三维数据进行的分析计算,三维即三维空间,三维空间的GIS分析,三维空间距离量算、三维体积量算、三维高度量算……本质上应该比二维分析更丰富才对。可惜的是,三维空间分析,绝大部分在课本上找不到,更别说概念化描述。
    在这里插入图片描述


    3.三维可视化


    三维可视化绝大部分是三维数据的可视化,我们常见的有地形可视化、倾斜摄影可视化
    在这里插入图片描述


    4.三维交互


    除了普通的漫游、缩放、旋转、更有俯仰、翻转等操作。从上面可以看出三维GIS重点还是在于可视化,做三维GIS可视化,两大核心是GIS+图形学。想要在三维GIS深耕,就需要在计算机图形学有很好的基础,较强的GIS开发概念,以及自己的想象力。而图形学是公认的成长缓慢的大后期,比C++成长更慢,更别说与Java、JavaScript等相提并论。因此以后把三维GIS作为自己的就业方向,前景很光明,即使以后不在GIS圈,去互联网游戏行业都能胜任,而且比纯游戏开发更有优势。


    image.png


    三、结语


    目前GIS公司对三维GIS的开发需求很强烈,但是受限于大学所学,能在大学既能懂GIS又懂图形学的,凤毛麟角,所以三维GIS开发很抢手,但是学校都找不到人,绝大部分都是招来基础好的学生,从零开始培养。


    学好三维GIS,不仅仅会一个Cesium、MapBox就完了,这最初级阶段,只是二次开发,熟悉了接口,你就需要从原理角度去熟悉底层原理,渲染管线、地形瓦片及3dtiles调度与渲染等等,这些都需要你去认真深入学习的,还有一点,也是很三维GIS的最重要的一件武器:shader。这是必须熟练掌握的,包括glsl,hlsl等。


    最后,就是要学游戏引擎,目前,做三维GIS与游戏引擎结合的越来越紧密,随着信创产业的快速发展,UE以后会越来越得到应用,做三维GIS,离不开熟练使用UE或者Unity。以上是我个人的一些看法,希望看到有更多的人投入到三维GIS中去,创造越来越多很酷的三维GIS产品,服务社会,造福人类!


    作者:按图索迹
    来源:juejin.cn/post/7337188759060267023
    收起阅读 »

    23年计算机科学与技术毕业生的2024 年终总结

    ### 2024年年终工作总结     引言随着2024年的落幕,作为一名计算机科学与技术专业的毕业生,我有幸参与到一款聊天应用的开发工作中。这一年,我不仅在技术层面获得了长足的进步,也在团队协作和项目管理方面...
    继续阅读 »

    ### 2024年年终工作总结

         引言

    随着2024年的落幕,作为一名计算机科学与技术专业的毕业生,我有幸参与到一款聊天应用的开发工作中。这一年,我不仅在技术层面获得了长足的进步,也在团队协作和项目管理方面积累了宝贵的经验。以下是我这一年工作的详细总结。

         技术成长

    1.    安卓开发技能提升    
       在这一年中,我深入学习了Android开发框架,掌握了Kotlin和Java两种编程语言,熟悉了Android Studio开发环境。
       学习并实践了Material Design设计原则,优化了用户界面和用户体验。
       掌握了网络编程,包括HTTP协议、RESTful API以及使用Retrofit和Volley等网络库进行数据通信。

    2.    项目管理与协作    
       参与了敏捷开发流程,学习了Scrum框架,每周参与Scrum会议,及时同步项目进度。
       学会了使用Git进行版本控制,以及在GitHub上进行代码管理和团队协作。

    3.    性能优化与问题解决    
       学习了Android性能优化的技巧,包括内存管理、布局优化和多线程编程。
       通过日志分析和性能监控工具,定位并解决了多个应用崩溃和性能瓶颈问题。

         项目成果

    1.    聊天应用核心功能开发    
       负责聊天应用的核心聊天功能模块开发,包括消息发送、接收和展示。
       实现了消息加密传输,保障用户通信安全。

    2.    用户界面与交互设计    
       参与设计并实现了应用的用户界面,使其更加直观和易用。
       根据用户反馈,迭代优化了多个界面元素,提升了用户满意度。

    3.    跨平台兼容性测试    
       参与了应用的跨平台测试工作,确保应用在不同设备和操作系统版本上的兼容性和稳定性。

        团队协作与领导力

    1.    团队沟通与协作    
       作为团队的一员,我积极参与团队讨论,提出建设性意见,并协助解决同事遇到的问题。
       学会了如何更有效地与团队成员沟通,提高了团队的整体效率。

    2.    领导力的培养    
       在项目中担任小组长,负责协调小组内的工作分配和进度跟踪,锻炼了我的领导能力。

        个人成长与反思

    1.    自我学习与提升    
       通过在线课程和专业书籍,不断学习新的技术和行业动态,保持自己的竞争力。
       反思过去一年的工作,认识到自己在时间管理和优先级排序上的不足,并制定了改进计划。

    2.    职业规划    
       明确了自己的职业发展方向,计划在未来几年内深入学习人工智能和机器学习,为公司的技术创新贡献力量。



    2024年对我来说是充满挑战和成长的一年。我不仅在技术上有所提升,也在团队协作和项目管理方面获得了宝贵的经验。展望未来,我将继续努力,为公司的发展贡献自己的力量,并实现个人职业生涯的进一步发展。 环信的easyimkit很好用。


    收起阅读 »

    半死不活的2024

    生活篇:1、今年是否完成了脱单/结婚:又是等花花的一年!2、描述今年的身体状况:马马虎虎,还活着3、描述今年的精神状态:癫疯4、今年最令你高兴/开心的那个瞬间:面的学校都收到offer了5、 今年最大的收获是什么:心态更平稳6、 实现了年初的什么目标:好好学习...
    继续阅读 »

    生活篇:
    1、今年是否完成了脱单/结婚:又是等花花的一年!

    2、描述今年的身体状况:马马虎虎,还活着

    3、描述今年的精神状态:癫疯

    4、今年最令你高兴/开心的那个瞬间:面的学校都收到offer了

    5、 今年最大的收获是什么:心态更平稳

    6、 实现了年初的什么目标:好好学习(?)

    7、新增了哪一项热爱的东西:捡垃圾(包括但不限于手柄、充电宝、路由器)

    8、失去了哪一项曾经热衷的东西:心气

    9、今年你一定增长了很多见识,请问最让你印象深刻的是什么?为啥?:遇上了很多人,有好有坏,人教人学不会,事儿教人一次就会

    10、一句话描述现在的自己:

    正在向着理想中的地方的学校半死不活的间歇性挣扎着(冲冲冲)

    工作篇:
    11、今年跳槽了吗?冇

    12、今年失业了吗?冇

    13、今年涨薪了吗?冇

    14、加班占工作时间的百分比:(或具体描述加班情况)看ddl什么时候通知

    15、发量占头的百分比:怒发冲冠

    收起阅读 »

    2024年年终总结 — 移动端开发者的路该怎么走

    2024年即将结束,作为一名移动端开发者,回顾过去的一年,心中充满了感慨与收获。这一年,移动端开发领域依旧充满挑战与机遇,新技术的涌现、行业需求的变化以及团队协作的深化,都促使我们不断调整自己的发展路径和技术积累。2024年,移动端开发领域发生了诸多变化,其中...
    继续阅读 »

    2024年即将结束,作为一名移动端开发者,回顾过去的一年,心中充满了感慨与收获。这一年,移动端开发领域依旧充满挑战与机遇,新技术的涌现、行业需求的变化以及团队协作的深化,都促使我们不断调整自己的发展路径和技术积累。

    2024年,移动端开发领域发生了诸多变化,其中最显著的是跨平台开发的兴起。Flutter 和 UniApp 等等作为最流行的跨平台框架,逐渐在大型项目中得到应用。然而原生开发者们的路越走越窄。

    2024年,随着AI技术的发展,越来越多的智能化应用涌现。作为移动端开发者,学习如何将AI技术应用于移动端开发中,未来移动端开发将不再仅仅局限于传统的UI和功能实现,AI、AR/VR以及5G等技术将逐步融入到移动应用中,带来更加丰富和个性化的用户体验。

    2025年,是机遇还是更加刺骨的寒冬,拭目以待。

    收起阅读 »

    我的2024

    作为一名前端开发者,2024年的年终总结应该既反映技术层面的成长,又体现工作中的挑战与收获。以下是一个适合前端开发者的年终总结框架,涵盖技术发展、项目经验、软技能提升等多个维度:2024年是我技术进步的重要一年,掌握了很多新的前端技术和工具,深入了解了前端生态...
    继续阅读 »

    作为一名前端开发者,2024年的年终总结应该既反映技术层面的成长,又体现工作中的挑战与收获。以下是一个适合前端开发者的年终总结框架,涵盖技术发展、项目经验、软技能提升等多个维度:

    2024年是我技术进步的重要一年,掌握了很多新的前端技术和工具,深入了解了前端生态的最新趋势。具体而言,我在以下几个方面取得了显著进步:

    JavaScript:加强了对ES6+新特性(如解构赋值、箭头函数、Promise、async/await、模块化等)的理解,提升了编写现代化代码的能力。尤其是在异步编程和状态管理方面,有了更深入的掌握。

    框架与库:深入学习并应用了ReactVue,尤其是在React中,使用了React Hooks和Context进行组件管理,提升了代码的可维护性与复用性。Vue 3的Composition API也让我在处理复杂状态时更加高效。

    前端工具链:深入掌握了WebpackVite等构建工具,优化了构建流程和性能。

                    熟悉了TypeScript,不仅能更好地进行类型检查,还提高了开发效率。

                   学习了ESLintPrettier等代码规范工具,规范了团队的代码质量。

    CSS与设计系统

                 在CSS方面,掌握了FlexboxGrid布局,更加灵活地解决了复杂的布局问题。

                 参与了公司设计系统的建设,理解并实现了组件化开发,提高了UI组件的复用率与一致性。

                学习了CSS-in-JS的方案(如styled-components),使得在React项目中可以更好地管理样式。

    性能优化

                深入理解了前端性能优化的关键技术,如代码分割、懒加载、图片优化、减少重排重绘等,提高了应用的加载速度与响应性能。

    前端工程化:引入了CI/CD流程,搭建了自动化构建与部署的流程,减少了人工干预,提升了开发效率。在项目中积极推动模块化和组件化的开发,推动了代码重构,使得代码更加易于维护和扩展。
























    回顾2024年,作为一名前端开发者,能运用vue、react等语言进行前端开发。我在技术和工作能力上都有了很大的进步。在未来,我会继续保持学习的热情,不断提升自己的技术水平和软技能,为团队和公司创造更多的价值。希望在接下来的工作中能够实现更多的突破,迎接新的挑战,取得更大的成就!

    对于学习了react并且用react开发工程,是我最大的收获

    收起阅读 »

    我的2024年终终结

            回望过去的这一年,我在技术领域经历了显著的成长与转型。作为一名拥有多年安卓开发经验的工程师,我见证了安卓平台从蓬勃兴起到逐步融入多元化技术生态的历程。尽管安卓原生开...
    继续阅读 »

            回望过去的这一年,我在技术领域经历了显著的成长与转型。作为一名拥有多年安卓开发经验的工程师,我见证了安卓平台从蓬勃兴起到逐步融入多元化技术生态的历程。尽管安卓原生开发依然占据重要地位,但不可否认的是,技术的浪潮已经带来了新的变革。

            随着小程序、UniApp等新兴技术的崛起,我深刻感受到了市场对于快速迭代、跨平台兼容性的强烈需求。这些新技术不仅极大地提升了开发效率,还为用户带来了更加流畅、一致的使用体验。因此,我积极拥抱变化,将自己的技能树向这些新领域拓展。

            在过去的一年里,我不仅继续深化了安卓原生开发的能力,还深入学习了小程序和UniApp的开发技术。通过实际项目的历练,我掌握了如何快速构建小程序应用,以及如何利用UniApp实现一次开发、多端运行的高效开发模式。这些经历不仅丰富了我的技术栈,也让我更加自信地面对多变的市场需求。

            同时,我也积极参与了后台开发的工作。通过深入了解后端架构和数据处理流程,我对于整个技术链条有了更加全面的认识。这种跨领域的实践不仅提升了我的综合技术能力,也让我在团队协作中发挥了更大的价值。

            展望未来,我将继续紧跟技术发展的步伐,不断学习和探索新的技术领域。我相信,只有不断适应变化、勇于创新,才能在激烈的市场竞争中立于不败之地。期待在新的一年里,我能够为公司贡献更多的价值,同时也实现自我价值的不断提升。

    收起阅读 »

    我乱七八糟的2024

    **App开发团队2024年年终总结报告****一、引言**随着2024年的圆满结束,我们的App开发团队在这一年中取得了显著的成就。本报告旨在详细回顾我们的工作成果、总结经验教训,并为2025年的发展规划提供指导。**二、项目回顾**1. **项目概览** ...
    继续阅读 »


    **App开发团队2024年年终总结报告**

    **一、引言**

    随着2024年的圆满结束,我们的App开发团队在这一年中取得了显著的成就。本报告旨在详细回顾我们的工作成果、总结经验教训,并为2025年的发展规划提供指导。

    **二、项目回顾**

    1. **项目概览**
    - **项目A:健康追踪App**
    - 目标:为用户提供一个全面的健康追踪和管理平台。
    - 成果:成功上线,用户反馈积极,下载量突破100万。

    - **项目B:在线教育平台**
    - 目标:打造一个互动性强、资源丰富的在线学习环境。
    - 成果:完成Beta测试,用户增长率达到20%。

    2. **关键里程碑**
    - **项目A:**
    - 1月:项目启动。
    - 6月:完成初步开发,开始内部测试。
    - 9月:正式上线。
    - **项目B:**
    - 4月:项目启动。
    - 8月:完成初步开发,开始Beta测试。
    - 12月:收集反馈,准备全面上线。

    3. **技术亮点**
    - **项目A:**
    - 引入了最新的机器学习算法,为用户提供个性化健康建议。
    - **项目B:**
    - 采用了最新的实时通讯技术,增强了师生间的互动体验。

    **三、成果与数据分析**

    1. **用户增长**
    - **项目A:**
    - 新增用户:150万
    - 活跃用户:60万
    - **项目B:**
    - 新增用户:50万
    - 活跃用户:10万

    2. **用户反馈与市场表现**
    - **项目A:**
    - 用户满意度:90%
    - 应用商店排名:健康类App第3位
    - **项目B:**
    - 用户满意度:85%
    - 应用商店排名:教育类App第5位

    3. **财务概况**
    - **项目A:**
    - 收入:500万美元
    - 成本:300万美元
    - 利润:200万美元
    - **项目B:**
    - 收入:100万美元
    - 成本:80万美元
    - 利润:20万美元

    **四、团队建设与协作**

    1. **团队规模与构成**
    - 团队规模从20人增长至30人,包括5名新加入的高级工程师和2名产品经理。

    2. **团队文化与氛围**
    - 举办了多次团队建设活动,增强了团队凝聚力和创新能力。

    3. **跨部门协作**
    - 与市场部门合作,成功推广了项目A,与客服部门合作,提高了用户满意度。

    **五、挑战与应对**

    1. **遇到的主要挑战**
    - **项目A:**
    - 用户隐私保护问题。
    - **项目B:**
    - 内容更新速度跟不上用户需求。

    2. **解决方案与调整**
    - **项目A:**
    - 加强数据加密和隐私政策的透明度。
    - **项目B:**
    - 增加内容团队,提高内容更新频率。

    **六、经验总结**

    1. **成功经验**
    - **项目A:**
    - 个性化推荐系统大幅提升了用户粘性。
    - **项目B:**
    - 实时反馈机制有效提高了学习效率。

    2. **教训与反思**
    - **项目A:**
    - 需要更早地关注用户隐私问题。
    - **项目B:**
    - 内容质量比数量更重要,需要更注重内容的深度和质量。

    **七、未来规划**

    1. **技术趋势与创新**
    - 计划引入更多的AI技术,提升用户体验。

    2. **项目规划**
    - **项目C:**
    - 计划开发一款面向儿童的教育游戏App。

    3. **团队发展**
    - 计划增加10名开发人员,以支持新项目的启动。

    **八、结语**

    感谢团队成员一年来的辛勤工作和贡献。我们期待在新的一年里,继续携手前进,创造更多的价值和成就。

    收起阅读 »

    智源最新评测:豆包视觉理解模型排名全球第二

    12月19日,智源研究院发布最新一期大模型综合及专项评测结果。在覆盖国内外100余个开源和商业闭源大模型的评测中,豆包通用模型pro获得大语言模型主观评测最高分;在多模态模型评测中,豆包视觉理解模型排名视觉语言模型第二,成绩仅次于GPT-4o;豆包文生图模型、...
    继续阅读 »

    12月19日,智源研究院发布最新一期大模型综合及专项评测结果。在覆盖国内外100余个开源和商业闭源大模型的评测中,豆包通用模型pro获得大语言模型主观评测最高分;在多模态模型评测中,豆包视觉理解模型排名视觉语言模型第二,成绩仅次于GPT-4o;豆包文生图模型、豆包视频生成模型(即梦P2.0 pro)也分别在相应测试中获得全球第二。

    图片1.png

    据智源研究院介绍,大模型评测平台FlagEval联合了全国10余家高校和机构合作共建。此次公布的榜单中,大语言模型主观评测重点考察的是模型的中文能力,多模态模型评测榜单中,视觉语言模型主要考察的是模型在图文理解、长尾视觉知识、文字识别以及复杂图文数据分析能力。FlagEval大模型角斗场则是向用户开放的模型对战评测服务,反映了用户对模型的偏好。

    在大语言模型主观评测中,豆包通用模型pro的知识运用和推理能力均获得最高分,简单理解、数学能力、安全等项目也排名前三,最终综合成绩排名第一。在FlagEval大模型角斗场榜单中,基于模型对战的用户投票结果,豆包通用模型pro得分排名第二,仅次于OpenAI的o1-mini。

    在多模态模型评测榜单中,GPT-4o在视觉语言模型中排名第一,豆包视觉理解模型获第二。在中文的通用知识、文字识别中,豆包表现突出,相比国外模型有较大优势。在文生图测试中,混元和豆包排名前两位;在文生视频测试中,国产模型更是优势显著,可灵1.5高品质版、即梦P2.0 pro、爱诗科技PixVerse v3和海螺AI排名前列。

    据悉,豆包视觉理解模型在不久前的火山引擎Force大会上首次发布,现已对企业客户开放使用。火山引擎方面表示,豆包大模型通过算法、工程、软硬件结合的技术创新,大幅优化使用成本,让每一家企业都能用得起,推动AI技术普惠和应用发展。(作者:李双)

    收起阅读 »

    我发现很多程序员都不会打日志。。

    大家好,我是程序员鱼皮。我发现很多程序员都不打日志,有的是 不想 打、有的是 意识不到 要打、还有的是 真不会 打日志啊! 前段时间的模拟面试中,我问了几位应届的 Java 开发同学 “你在项目中是怎么打日志的”,得到的答案竟然是 “支支吾吾”、“阿巴阿巴”,...
    继续阅读 »

    大家好,我是程序员鱼皮。我发现很多程序员都不打日志,有的是 不想 打、有的是 意识不到 要打、还有的是 真不会 打日志啊!


    前段时间的模拟面试中,我问了几位应届的 Java 开发同学 “你在项目中是怎么打日志的”,得到的答案竟然是 “支支吾吾”、“阿巴阿巴”,更有甚者,竟然表示:直接用 System.out.println() 打印一下吧。。。



    要知道,日志是我们系统出现错误时,最快速有效的定位工具,没有日志给出的错误信息,遇到报错你就会一脸懵逼;而且日志还可以用来记录业务信息,比如记录用户执行的每个操作,不仅可以用于分析改进系统,同时在遇到非法操作时,也能很快找到凶手。


    因此,对于程序员来说,日志记录是重要的基本功。但很多同学并没有系统学习过日志操作、缺乏经验,所以我写下这篇文章,分享自己在开发项目中记录日志的方法和最佳实践,希望对大家有帮助~


    一、日志记录的方法


    日志框架选型


    有很多 Java 的日志框架和工具库,可以帮我们用一行代码快速完成日志记录。


    在学习日志记录之前,很多同学应该是通过 System.out.println 输出信息来调试程序的,简单方便。


    但是,System.out.println 存在很严重的问题!



    首先,System.out.println 是一个同步方法,每次调用都会导致 I/O 操作,比较耗时,频繁使用甚至会严重影响应用程序的性能,所以不建议在生产环境使用。此外,它只能输出简单的信息到标准控制台,无法灵活设置日志级别、格式、输出位置等。


    所以我们一般会选择专业的 Java 日志框架或工具库,比如经典的 Apache Log4j 和它的升级版 Log4j 2,还有 Spring Boot 默认集成的 Logback 库。不仅可以帮我们用一行代码更快地完成日志记录,还能灵活调整格式、设置日志级别、将日志写入到文件中、压缩日志等。


    可能还有同学听说过 SLF4J(Simple Logging Facade for Java),看英文名就知道了,这玩意并不是一个具体的日志实现,而是为各种日志框架提供简单统一接口的日志门面(抽象层)。


    啥是门面?


    举个例子,现在我们要记录日志了,先联系到前台接待人员 SLF4J,它说必须要让我们选择日志的级别(debug / info / warn / error),然后要提供日志的内容。确认之后,SLF4J 自己不干活,屁颠屁颠儿地去找具体的日志实现框架,比如 Logback,然后由 Logback 进行日志写入。



    这样做有什么好处呢?无论我们选择哪套日志框架、或者后期要切换日志框架,调用的方法始终是相同的,不用再去更改日志调用代码,比如将 log.info 改为 log.printInfo。


    既然 SLF4J 只是玩抽象,那么 Log4j、Log4j 2 和 Logback 应该选择哪一个呢?



    值得一提的是,SLF4J、Log4j 和 Logback 竟然都是同一个作者(俄罗斯程序员 Ceki Gülcü)。



    首先,Log4j 已经停止维护,直接排除。Log4j 2 和 Logback 基本都能满足功能需求,那么就看性能、稳定性和易用性。



    • 从性能来说,Log4j 2 和 Logback 虽然都支持异步日志,但是 Log4j 基于 LMAX Disruptor 高性能异步处理库实现,性能更高。

    • 从稳定性来说,虽然这些日志库都被曝出过漏洞,但 Log4j 2 的漏洞更为致命,姑且算是 Logback 得一分。

    • 从易用性来说,二者差不多,但 Logback 是 SLF4J 的原生实现、Log4j2 需要额外使用 SLF4J 绑定器实现。


    再加上 Spring Boot 默认集成了 Logback,如果没有特殊的性能需求,我会更推荐初学者选择 Logback,都不用引入额外的库了~


    使用日志框架


    日志框架的使用非常简单,一般需要先获取到 Logger 日志对象,然后调用 logger.xxx(比如 logger.info)就能输出日志了。


    最传统的方法就是通过 LoggerFactory 手动获取 Logger,示例代码如下:


    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    public class MyService {
       private static final Logger logger = LoggerFactory.getLogger(MyService.class);

       public void doSomething() {
           logger.info("执行了一些操作");
      }
    }

    上述代码中,我们通过调用日志工厂并传入当前类,创建了一个 logger。但由于每个类的类名都不同,我们又经常复制这行代码到不同的类中,就很容易忘记修改类名。


    所以我们可以使用 this.getClass 动态获取当前类的实例,来创建 Logger 对象:


    public class MyService {
       private final Logger logger = LoggerFactory.getLogger(this.getClass());

       public void doSomething() {
           logger.info("执行了一些操作");
      }
    }

    给每个类都复制一遍这行代码,就能愉快地打日志了。


    但我觉得这样做还是有点麻烦,我连复制粘贴都懒得做,怎么办?


    还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成一个名为 log 的 SLF4J Logger 对象,简化了 Logger 的定义过程。示例代码如下:


    import lombok.extern.slf4j.Slf4j;

    @Slf4j
    public class MyService {
       public void doSomething() {
           log.info("执行了一些操作");
      }
    }

    这也是我比较推荐的方式,效率杠杠的。



    此外,你可以通过修改日志配置文件(比如 logback.xmllogback-spring.xml)来设置日志输出的格式、级别、输出路径等。日志配置文件比较复杂,不建议大家去记忆语法,随用随查即可。



    二、日志记录的最佳实践


    学习完日志记录的方法后,再分享一些我个人记录日志的经验。内容较多,大家可以先了解一下,实际开发中按需运用。


    1、合理选择日志级别


    日志级别的作用是标识日志的重要程度,常见的级别有:



    • TRACE:最细粒度的信息,通常只在开发过程中使用,用于跟踪程序的执行路径。

    • DEBUG:调试信息,记录程序运行时的内部状态和变量值。

    • INFO:一般信息,记录系统的关键运行状态和业务流程。

    • WARN:警告信息,表示可能存在潜在问题,但系统仍可继续运行。

    • ERROR:错误信息,表示出现了影响系统功能的问题,需要及时处理。

    • FATAL:致命错误,表示系统可能无法继续运行,需要立即关注。


    其中,用的最多的当属 DEBUG、INFO、WARN 和 ERROR 了。


    建议在开发环境使用低级别日志(比如 DEBUG),以获取详细的信息;生产环境使用高级别日志(比如 INFO 或 WARN),减少日志量,降低性能开销的同时,防止重要信息被无用日志淹没。


    注意一点,日志级别未必是一成不变的,假如有一天你的程序出错了,但是看日志找不到任何有效信息,可能就需要降低下日志输出级别了。


    2、正确记录日志信息


    当要输出的日志内容中存在变量时,建议使用参数化日志,也就是在日志信息中使用占位符(比如 {}),由日志框架在运行时替换为实际参数值。


    比如输出一行用户登录日志:


    // 不推荐
    logger.debug("用户ID:" + userId + " 登录成功。");

    // 推荐
    logger.debug("用户ID:{} 登录成功。", userId);

    这样做不仅让日志清晰易读;而且在日志级别低于当前记录级别时,不会执行字符串拼接,从而避免了字符串拼接带来的性能开销、以及潜在的 NullPointerException 问题。所以建议在所有日志记录中,使用参数化的方式替代字符串拼接。


    此外,在输出异常信息时,建议同时记录上下文信息、以及完整的异常堆栈信息,便于排查问题:


    try {
       // 业务逻辑
    catch (Exception e) {
    logger.error("处理用户ID:{} 时发生异常:", userId, e);
    }

    3、控制日志输出量


    过多的日志不仅会占用更多的磁盘空间,还会增加系统的 I/O 负担,影响系统性能。


    因此,除了根据环境设置合适的日志级别外,还要尽量避免在循环中输出日志。


    可以添加条件来控制,比如在批量处理时,每处理 1000 条数据时才记录一次:


    if (index % 1000 == 0) {
       logger.info("已处理 {} 条记录", index);
    }

    或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:


    StringBuilder logBuilder = new StringBuilder("处理结果:");
    for (Item item : items) {
       try {
           processItem(item);
           logBuilder.append(String.format("成功[ID=%s], ", item.getId()));
      } catch (Exception e) {
           logBuilder.append(String.format("失败[ID=%s, 原因=%s], ", item.getId(), e.getMessage()));
      }
    }
    logger.info(logBuilder.toString());

    如果参数的计算开销较大,且当前日志级别不需要输出,应该在记录前进行级别检查,从而避免多余的参数计算:


    if (logger.isDebugEnabled()) {
       logger.debug("复杂对象信息:{}"expensiveToComputeObject());
    }

    此外,还可以通过更改日志配置文件整体过滤掉特定级别的日志,来防止日志刷屏:


    <!-- Logback 示例 -->
    <appender name="LIMITED" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 只允许 INFO 级别及以上的日志通过 -->
       <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
           <level>INFO</level>
       </filter>
       <!-- 配置其他属性 -->
    </appender>

    4、把控时机和内容


    很多开发者(尤其是线上经验不丰富的开发者)并没有养成记录日志的习惯,觉得记录日志不重要,等到出了问题无法排查的时候才追悔莫及。


    一般情况下,需要在系统的关键流程和重要业务节点记录日志,比如用户登录、订单处理、支付等都是关键业务,建议多记录日志。


    对于重要的方法,建议在入口和出口记录重要的参数和返回值,便于快速还原现场、复现问题。


    对于调用链较长的操作,确保在每个环节都有日志,以便追踪到问题所在的环节。


    如果你不想区分上面这些情况,我的建议是尽量在前期多记录一些日志,后面再慢慢移除掉不需要的日志。比如可以利用 AOP 切面编程在每个业务方法执行前输出执行信息:


    @Aspect
    @Component
    public class LoggingAspect {

       @Before("execution(* com.example.service..*(..))")
       public void logBeforeMethod(JoinPoint joinPoint) {
           Logger logger = LoggerFactory.getLogger(joinPoint.getTarget().getClass());
           logger.info("方法 {} 开始执行", joinPoint.getSignature().getName());
      }
    }

    利用 AOP,还可以自动打印每个 Controller 接口的请求参数和返回值,这样就不会错过任何一次调用信息了。


    不过这样做也有一个很重要的点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。



    5、日志管理


    随着日志文件的持续增长,会导致磁盘空间耗尽,影响系统正常运行,所以我们需要一些策略来对日志进行管理。


    首先是设置日志的滚动策略,可以根据文件大小或日期,自动对日志文件进行切分。比如按文件大小滚动:


    <!-- 按大小滚动 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedRollingPolicy">
       <maxFileSize>10MB</maxFileSize>
    </rollingPolicy>

    如果日志文件大小达到 10MB,Logback 会将当前日志文件重命名为 app.log.1 或其他命名模式(具体由文件名模式决定),然后创建新的 app.log 文件继续写入日志。


    还有按照时间日期滚动:


    <!-- 按时间滚动 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
       <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>
    </rollingPolicy>

    上述配置表示每天创建一个新的日志文件,%d{yyyy-MM-dd} 表示按照日期命名日志文件,例如 app-2024-11-21.log


    还可以通过 maxHistory 属性,限制保留的历史日志文件数量或天数:


    <maxHistory>30</maxHistory>

    这样一来,我们就可以按照天数查看指定的日志,单个日志文件也不会很大,提高了日志检索效率。


    对于用户较多的企业级项目,日志的增长是飞快的,因此建议开启日志压缩功能,节省磁盘空间。


    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
       <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
    </rollingPolicy>

    上述配置表示:每天生成一个新的日志文件,旧的日志文件会被压缩存储。


    除了配置日志切分和压缩外,我们还需要定期审查日志,查看日志的有效性和空间占用情况,从日志中发现系统的问题、清理无用的日志信息等。


    如果你想偷懒,也可以写个自动化清理脚本,定期清理过期的日志文件,释放磁盘空间。比如:


    # 每月清理一次超过 90 天的日志文件
    find /var/log/myapp/ -type f -mtime +90 -exec rm {} ;

    6、统一日志格式


    统一的日志格式有助于日志的解析、搜索和分析,特别是在分布式系统中。


    我举个例子大家就能感受到这么做的重要性了。


    统一的日志格式:


    2024-11-21 14:30:15.123 [main] INFO com.example.service.UserService - 用户ID:12345 登录成功
    2024-11-21 14:30:16.789 [main] ERROR com.example.service.UserService - 用户ID:12345 登录失败,原因:密码错误
    2024-11-21 14:30:17.456 [main] DEBUG com.example.dao.UserDao - 执行SQL:[SELECT * FROM users WHERE id=12345]
    2024-11-21 14:30:18.654 [main] WARN com.example.config.AppConfig - 配置项 `timeout` 使用默认值:3000ms
    2024-11-21 14:30:19.001 [main] INFO com.example.Main - 应用启动成功,耗时:2.34秒

    这段日志整齐清晰,支持按照时间、线程、级别、类名和内容搜索。


    不统一的日志格式:


    2024/11/21 14:30 登录成功 用户ID: 12345
    2024-11-21 14:30:16 错误 用户12345登录失败!密码不对
    DEBUG 执行SQL SELECT * FROM users WHERE id=12345
    Timeout = default
    应用启动成功

    emm,看到这种日志我直接原地爆炸!



    建议每个项目都要明确约定和配置一套日志输出规范,确保日志中包含时间戳、日志级别、线程、类名、方法名、消息等关键信息。


    <!-- 控制台日志输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
       <encoder>
           <!-- 日志格式 -->
           <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
       </encoder>
    </appender>

    也可以直接使用标准化格式,比如 JSON,确保所有日志遵循相同的结构,便于后续对日志进行分析处理:


    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
       <!-- 配置 JSON 编码器 -->
    </encoder>

    此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如用户 ID、请求 ID 等,方便追踪。在 Java 代码中,可以为 MDC 变量设置值:


    MDC.put("requestId""666");
    MDC.put("userId""yupi");
    logger.info("用户请求处理完成");
    MDC.clear();

    对应的日志配置如下:


    <!-- 文件日志配置 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <encoder>
           <!-- 包含 MDC 信息 -->
           <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
       </encoder>
    </appender>

    这样,每个请求、每个用户的操作一目了然。


    7、使用异步日志


    对于追求性能的操作,可以使用异步日志,将日志的写入操作放在单独的线程中,减少对主线程的阻塞,从而提升系统性能。


    除了自己开线程去执行 log 操作之外,还可以直接修改配置来开启 Logback 的异步日志功能:


    <!-- 异步 Appender -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
       <queueSize>500</queueSize> <!-- 队列大小 -->
       <discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
       <neverBlock>true</neverBlock> <!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
       <appender-ref ref="CONSOLE" /> <!-- 生效的日志目标 -->
       <appender-ref ref="FILE" />
    </appender>

    上述配置的关键是配置缓冲队列,要设置合适的队列大小和丢弃策略,防止日志积压或丢失。


    8、集成日志收集系统


    在比较成熟的公司中,我们可能会使用更专业的日志管理和分析系统,比如 ELK(Elasticsearch、Logstash、Kibana)。不仅不用每次都登录到服务器上查看日志文件,还可以更灵活地搜索日志。


    但是搭建和运维 ELK 的成本还是比较大的,对于小团队,我的建议是不要急着搞这一套。




    OK,就分享到这里,洋洋洒洒 4000 多字,希望这篇文章能帮助大家意识到日志记录的重要性,并养成良好的日志记录习惯。学会的话给鱼皮点个赞吧~


    日志不是写给机器看的,是写给未来的你和你的队友看的!


    更多


    💻 编程学习交流:编程导航

    📃 简历快速制作:老鱼简历

    ✏️ 面试刷题神器:面试鸭


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

    一种简单粗暴的大屏自适应方案,原理及案例

    web
    现状 现在最流行的大屏自适应手法: scale缩放 为了解决2d/3d的点击交互问题,通常设计成了2个层容器。图表层和2d/3d层。图表层负责缩放,2d/3d层保持100%显示,避免缩放引起的交互事件event问题。 下图是一个1920*1080的大屏示意...
    继续阅读 »

    现状



    现在最流行的大屏自适应手法: scale缩放

    为了解决2d/3d的点击交互问题,通常设计成了2个层容器。图表层和2d/3d层。图表层负责缩放,2d/3d层保持100%显示,避免缩放引起的交互事件event问题。



    下图是一个1920*1080的大屏示意图


    image.png


    使用常规的缩放方法,让大屏在窗口内最大化显示。大屏在不同的窗口中出现了空白区域,并没有充满整个屏幕。


    image.png
    image.png


    新的方法 


    在缩放的基础上,对指定的要素进行贴边处理。我们希望上下吸附到窗口最上面和最下面。左右图表吸附到窗口的最左边和最右边。


    这里面需要简单的计算,其中a是图表层 scale属性


    var halftop = (window.innerHeight- (1080*a.scaleY))/2/a.scaleY;
    var halfleft = (window.innerWidth- (1920*a.scaleX))/2/a.scaleX;

    对指定id的容器,在resize事件中设置上下左右浮动。如下图


    image.png


    image.png


    image.png


    实战项目效果



    注,下面图片中的数据指标、城市名、姓名、图像均为虚拟数据。



    在实际应用中,一般1920*1080设计稿已宽屏为主,如果是竖屏大屏(下图6),需要设计竖屏UI。


    211.png


    2024-12-04_134349.jpg


    2024-12-04_123059.jpg


    2024-12-04_120938.jpg


    2024-12-04_120604.jpg


    2024-12-04_123119.jpg


    你也可以下载该项目demo, 对窗口进行缩放查看效果 pan.baidu.com/s/1hE_C9x9i…


    作者:波泼
    来源:juejin.cn/post/7444378390843768843
    收起阅读 »