注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

「滚动绽放」页面滚动时逐渐展示/隐藏元素

web
本文将介绍如何使用HTML、CSS和JavaScript代码实现页面在滚动时元素逐渐出现/隐藏。这个动画效果会在用户滚动/隐藏页面时从不同方向逐渐显示出一组彩色方块🏳️‍🌈 HTML结构 首先,HTML部分包含了一个<section>元素和一个名...
继续阅读 »

本文将介绍如何使用HTMLCSSJavaScript代码实现页面在滚动时元素逐渐出现/隐藏。这个动画效果会在用户滚动/隐藏页面时从不同方向逐渐显示出一组彩色方块🏳️‍🌈



HTML结构


首先,HTML部分包含了一个<section>元素和一个名为container的容器,其中包含了多个box元素。别忘了引入外部CSS和JS文件;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./style.css">

<title>Scroll To Reveal Animation</title>
</head>
<body>
<section>
<h2>Scroll To Reveal</h2>
</section>

<div class="container">
<!-- 调试CSS样式阶段 -->
<!-- <div class="box"></div>
<div class="box"></div>
<div class="box"></div>
<div class="box"></div> -->

</div>

<script src="./index.js"></script>
</body>
</html>

CSS样式


接着,设置一些基本的全局样式和居中布局、背景颜色和文字颜色;



  • 关于container容器,使用grid布局三列

  • 对于box容器,这部分CSS伪类代码定义了元素在动画中的位置和缩放变换。解释一下每个选择器的作用:

    • .box:nth-child(3n + 1):选择容器中每隔3个元素的第一个方块元素(这里表示第一列)。沿X轴向左平移400像素,缩放为0,即隐藏起来。

    • .box:nth-child(3n + 2):选择容器中每隔3个元素的第二个方块元素(这里表示第二列)。沿Y轴向下平移400像素,缩放为0。

    • .box:nth-child(3n + 3):选择容器中每隔3个元素第三个方块元素(这里表示第三列)。沿X轴向右平移400像素,缩放为0。




这些选择器定义了方块元素的初始状态,使它们在页面加载时处于隐藏状态。并且预设了.box.active激活状态的样式。



  • 将其平移到原始位置并恢复为原始尺寸,即显示出来。当滚动触发相应的事件时,方块元素将根据添加或移除active类来决定是逐渐显示或隐藏。


* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}

body {
display: flex;
flex-direction: column;
justify-content:center;
align-items: center;

background-color: #111;
color: #fff;
overflow-x: hidden;
}

section {
min-height: 100vh;
display: flex;
justify-content:center;
align-items: center;
}
section h2 {
font-size: 8vw;
font-weight: 500;
}

.container {
width: 700px;
position: relative;
top: -200px;

display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
}
.container .box {
width: 200px;
height: 200px;
background-color: #fff;
border-radius: 10px;

position: relative;
top: 50vh;
transition: .5s;
}

.container .box:nth-child(3n + 1) {
transform: translate(-400px, 0) scale(0);
}
.container .box:nth-child(3n + 2) {
transform: translate(0, 400px) scale(0);
}
.container .box:nth-child(3n + 3) {
transform: translate(400px, 0) scale(0);
}

.container .box.active {
transform: translate(0, 0) scale(1);
}

表现


scroll-reveal-rendering

JavaScript实现


最后,使用JavaScript生成每个方块并设置了随机的背景颜色,随后将它们添加到container容器中,通过监听滚动事件,使方块在用户滚动页面时根据位置添加类名否,应用CSS样式实现逐渐显示或隐藏;



  • 定义randomColor函数,用于生成随机的颜色值。这个函数会从一组字符中随机选择6个字符(每次循环随机取一个)作为颜色代码,并将其拼接成一个十六进制颜色值返回。

  • 获取container容器元素,并创建一个文档片段fragment用于存储循环创建出来带有背景色的.box方块元素,最后将文档片段附加到container中。

  • 定义scrollTrigger函数,绑定到窗口的滚动事件上。在这个函数中,遍历每个方块,检查相对于窗口顶部的偏移量,如果小于等于当前滚动的距离,则添加active类,显示方块。反之,则移除active类,隐藏方块。


/**创建随机色 */
const randomColor = () => {
const chars = "1234567890abcdef",
colorLegh = 6;

let color = '#';
for (let i = 0; i < colorLegh; i++) {
const p = Math.floor(Math.random() * chars.length);
color += chars.substring(p, p + 1);
};

return color;
};

/**创建DOM */
const container = document.querySelector('.container'),
fragment = document.createDocumentFragment();

for (let i = 0; i < 60; i++) {
const box = document.createElement('div');
box.style.backgroundColor = randomColor();
box.classList.add('box');

fragment.appendChild(box);
};
container.appendChild(fragment);


/**创建动画 */
const randomColorBlock = document.querySelectorAll('.box');

const scrollTrigger = () => {
randomColorBlock.forEach((box) => {
if (box.offsetTop <= window.scrollY) {
box.classList.add('active')
} else {
box.classList.remove('active')
}
});
};

window.addEventListener('scroll', scrollTrigger);

总结


通过本篇文章的详细介绍,相信能够帮助你更好地使用CSSJavaScript来创建一个滚动显示元素动画,从而理解掌握和应用这个效果。通过设置合适的样式和脚本来控制元素的显示和隐藏为网页提供了生动和吸引力。


希望这篇文章对你在开发类似交互动画效果时有所帮助!如果你对这个案列还有任何问题,欢迎在评论区留言或联系(私信)我。码字不易🥲,不要忘了三连鼓励🤟,谢谢阅读,Happy Coding🎉!


源码我放在了GitHub,里面还有一些酷炫的效果、动画案列,喜欢的话不要忘了 starred 不迷路!


作者:掘一
来源:juejin.cn/post/7280926568854781987
收起阅读 »

Java音视频文件解析工具

@[toc] 小伙伴们知道,松哥平时录了蛮多视频课程,视频录完以后,就想整理一个视频文档出来,在整理视频文档的时候,就会遇到一个问题,就是怎么统计视频时长? 特别是有时候为了方便大家看到每一集视频的时长,我要把视频目录整理成下面这个样子: 这个逐集去查看就很...
继续阅读 »

@[toc]
小伙伴们知道,松哥平时录了蛮多视频课程,视频录完以后,就想整理一个视频文档出来,在整理视频文档的时候,就会遇到一个问题,就是怎么统计视频时长?


特别是有时候为了方便大家看到每一集视频的时长,我要把视频目录整理成下面这个样子:



这个逐集去查看就很麻烦,一套视频动辄几百集,挨个统计不现实,也不符合咱们程序员做事风格。


那么怎么办呢?


一开始我是使用 Python 去解决的,Python 做这样一个小工具其实特别方便,简简单单 30 行代码左右就能搞定了。之前的课程的这些时间统计我基本上都是用 Python 去完成的。


不过最近松哥发现 Java 里边其实也有一个视频处理的库,做这个事情也是非常方便,而且使用 Java 属于主场作战,就能够更加灵活的扩展功能了。


一 jave-all-deps


在 Java 开发中,处理音视频文件经常需要复杂的编解码操作,开发者通常需要依赖于外部库来实现这些功能,其中最著名的是 FFmpeg。然而,直接在 Java 中使用 FFmpeg 并不是一件容易的事,因为它需要处理本地库和复杂的命令行接口。


幸运的是,jave-all-deps 库提供了一个简洁而强大的解决方案,让 Java 开发者能够轻松地进行音视频文件的转码和处理。


jave-all-deps 是 JAVE2(Java Audio Video Encoder)项目的一部分,它是一个基于 ffmpeg 项目的 Java 封装库。JAVE2 通过提供一套简单易用的 API,允许 Java 开发者在不直接处理 ffmpeg 复杂命令的情况下,进行音视频文件的格式转换、转码、剪辑等操作。


jave-all-deps 库特别之处在于它集成了核心 Java 代码和所有支持平台的二进制可执行文件,使得开发者无需手动配置 ffmpeg 环境,即可在多个操作系统上无缝使用。


是不是非常方便?


整体上来说,jave-all-deps 帮我们解决了三大类问题:



  1. 跨平台兼容性问题:音视频处理往往涉及到不同的操作系统和硬件架构,jave-all-deps 库提供了针对不同平台的预编译 ffmpeg 二进制文件,使得开发者无需担心平台兼容性问题。

  2. 复杂的命令行操作:ffmpeg 虽然功能强大,但其命令行接口复杂且难以记忆。jave-all-deps 通过封装 ffmpeg 的命令行操作,提供了简洁易用的 Java API,降低了使用门槛。

  3. 依赖管理:在项目中集成音视频处理功能时,往往需要处理多个依赖项。jave-all-deps 库将核心代码和所有必要的二进制文件打包在一起,简化了依赖管理。


简单来说,就是你想在项目中使用 ffmpeg,但是又嫌麻烦,那么就可以使用 jave-all-deps 这个工具封装后的 ffmpeg,简单快捷!


二 具体用法


jave-all-deps 库提供了多种音视频处理功能,松哥这里来和大家演示几个常见的。


2.1 添加依赖


添加依赖有两种方式,一种就是添加所有的依赖库,如下:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.5.0</version>
</dependency>

这个库中包含了不同平台所依赖的库的内容。


也可以根据自己平台选择不同的依赖库,这种方式需要首先添加 java-core:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>3.5.0</version>
</dependency>

然后再根据自己使用的不同平台,继续添加不同依赖库:


Linux 64 位 amd/intel:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<version>3.5.0</version>
</dependency>

Linux 64 位 arm:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux-arm64</artifactId>
<version>3.5.0</version>
</dependency>

Linux 32 位 arm:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux-arm32</artifactId>
<version>3.5.0</version>
</dependency>

Windows 64 位:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win64</artifactId>
<version>3.5.0</version>
</dependency>

MacOS 64 位:


<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-osx64</artifactId>
<version>3.5.0</version>
</dependency>

2.2 视频转音频


将视频文件从一种格式转换为另一种格式,例如将 AVI 文件转换为 MPEG 文件。


File source = new File("D:\\AI智能体\\mp4\\01.大模型有什么缺陷.mp4");
File target = new File("D:\\AI智能体\\mp4\\01.大模型有什么缺陷.mp3");
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
audio.setBitRate(128000);
audio.setChannels(2);
audio.setSamplingRate(44100);
EncodingAttributes attrs = new EncodingAttributes();
attrs.setOutputFormat("mp3");
attrs.setAudioAttributes(audio);
Encoder encoder = new Encoder();
encoder.encode(new MultimediaObject(source), target, attrs);

2.3 视频格式转换


将一种视频格式转换为另外一种视频格式,例如将 mp4 转为 flv:


File source = new File("D:\\AI智能体\\mp4\\01.大模型有什么缺陷.mp4");
File target = new File("D:\\AI智能体\\mp4\\01.大模型有什么缺陷.flv");
AudioAttributes audio = new AudioAttributes();
audio.setCodec("libmp3lame");
audio.setBitRate(64000);
audio.setChannels(1);
audio.setSamplingRate(22050);
VideoAttributes video = new VideoAttributes();
video.setCodec("flv");
video.setBitRate(160000);
video.setFrameRate(15);
video.setSize(new VideoSize(400, 300));
EncodingAttributes attrs = new EncodingAttributes();
attrs.setOutputFormat("flv");
attrs.setAudioAttributes(audio);
attrs.setVideoAttributes(video);
Encoder encoder = new Encoder();
encoder.encode(new MultimediaObject(source), target, attrs);

2.4 获取视频时长


这个就是松哥的需求了,我这块举个简单例子。


public class App {
static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("mm:ss");

public static void main(String[] args) throws EncoderException {
System.out.println("输入视频目录:");
String dir = new Scanner(System.in).next();
File folder = new File(dir);
List<String> files = sort(folder);
outputVideoTime(files);
}

private static void outputVideoTime(List<String> files) throws EncoderException {
for (String file : files) {
File video = new File(file);
if (video.isFile() && !video.getName().startsWith(".") && video.getName().endsWith(".mp4")) {
MultimediaObject multimediaObject = new MultimediaObject(video);
long duration = multimediaObject.getInfo().getDuration();
String s = "%s %s";
System.out.println(String.format(s, video.getName(), DATE_FORMAT.format(duration)));
} else if (video.isDirectory()) {
System.out.println(video.getName());
outputVideoTime(sort(video));
}
}
}

public static List<String> sort(File folder) {
return Arrays.stream(folder.listFiles()).map(f -> f.getAbsolutePath()).sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.toList());
}
}

这段代码基本上都是 Java 基础语法,没啥难的,我也就不多说了。有不明白的地方欢迎加松哥微信讨论。


其实 Java 解决这个似乎也不难,也就是 20 行代码左右,似乎和 Python 不相上下。


三 总结


jave-all-deps 库是 Java 音视频处理领域的一个强大工具,它通过封装 ffmpeg 的复杂功能,为 Java 开发者提供了一个简单易用的音视频处理解决方案。该库解决了跨平台兼容性问题、简化了复杂的命令行操作,并简化了项目中的依赖管理。无论是进行格式转换、音频转码还是其他音视频处理任务,jave-all-deps 库都是一个值得考虑的选择。


通过本文的介绍,希望能够帮助读者更好地理解和使用 jave-all-deps 库。


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

前端中的 File 和 Blob两个对象到底有什么不同❓❓❓

web
JavaScript 在处理文件、二进制数据和数据转换时,提供了一系列的 API 和对象,比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内...
继续阅读 »

JavaScript 在处理文件、二进制数据和数据转换时,提供了一系列的 API 和对象,比如 File、Blob、FileReader、ArrayBuffer、Base64、Object URL 和 DataURL。每个概念在不同场景中都有重要作用。下面的内容我们将会详细学习每个概念及其在实际应用中的用法。


接下来的内容中我们将来了解 File和 Blob 这两个对象。


blob


在 JavaScript 中,Blob(Binary Large Object)对象用于表示不可变的、原始的二进制数据。它可以用来存储文件、图片、音频、视频、甚至是纯文本等各种类型的数据。Blob 提供了一种高效的方式来操作数据文件,而不需要将数据全部加载到内存中,这在处理大型文件或二进制数据时非常有用。


我们可以使用 new Blob() 构造函数来创建一个 Blob 对象,语法如下:


const blob = new Blob(blobParts, options);


  1. blobParts: 一个数组,包含将被放入 Blob 对象中的数据,可以是字符串、数组缓冲区(ArrayBuffer)、TypedArray、Blob 对象等。

  2. options: 一个可选的对象,可以设置 type(MIME 类型)和 endings(用于表示换行符)。


例如:


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

20240913142627


Blob 对象主要有以下几个属性:



  1. size: 返回 Blob 对象的大小(以字节为单位)。


console.log(blob.size); // 输出 Blob 的大小


  1. type: 返回 Blob 对象的 MIME 类型。


console.log(blob.type); // 输出 Blob 的 MIME 类型

Blob 对象提供了一些常用的方法来操作二进制数据。



  1. slice([start], [end], [contentType])


该方法用于从 Blob 中提取一部分数据,并返回一个新的 Blob 对象。参数 start 和 end 表示提取的字节范围,contentType 设置提取部分的 MIME 类型。


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

const partialBlob = blob.slice(0, 5);


  1. text()


该方法将 Blob 的内容读取为文本字符串。它返回一个 Promise,解析为文本数据。


blob.text().then((text) => {
console.log(text); // 输出 "Hello, world!"
});

20240913143250



  1. arrayBuffer()


该方法将 Blob 的内容读取为 ArrayBuffer 对象,适合处理二进制数据。它返回一个 Promise,解析为 ArrayBuffer 数据。


const blob = new Blob(["Hello, world!"], { type: "text/plain" });

blob.arrayBuffer().then((buffer) => {
console.log(buffer);
});

20240913143451



  1. stream()


该方法将 Blob 的数据作为一个 ReadableStream 返回,允许你以流的方式处理数据,适合处理大文件。


const stream = blob.stream();

Blob 的使用场景


Blob 对象在很多场景中非常有用,尤其是在 Web 应用中处理文件、图片或视频等二进制数据时。以下是一些常见的使用场景:



  1. 生成文件下载


你可以通过 Blob 创建文件并生成下载链接供用户下载文件。


const blob = new Blob(["This is a test file."], { type: "text/plain" });
const url = URL.createObjectURL(blob); // 创建一个 Blob URL
const a = document.createElement("a");
a.href = url;
a.download = "test.txt";
a.click();
URL.revokeObjectURL(url); // 释放 URL 对象

当我们刷新浏览器的时候发现是可以自动给我们下载图片了:


20240913144132



  1. 上传文件


你可以通过 FormData 对象将 Blob 作为文件上传到服务器:


const formData = new FormData();
formData.append("file", blob, "example.txt");

fetch("/upload", {
method: "POST",
body: formData,
}).then((response) => {
console.log("File uploaded successfully");
});


  1. 读取图片或其他文件


通过 FileReader API 可以将 Blob 对象读取为不同的数据格式。举例来说,你可以将 Blob 读取为图片并显示在页面上:


html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<input type="file" id="fileInput" accept="image/*" />

<div id="imageContainer">div>
<script>
const fileInput = document.getElementById("fileInput");

const imageContainer = document.getElementById("imageContainer");

fileInput.
addEventListener("change", function (event) {
const file = event.target.files[0];

if (file && file.type.startsWith("image/")) {
const reader = new FileReader();

reader.
onload = function (e) {
const img = document.createElement("img");
img.
src = e.target.result;
img.
style.maxWidth = "500px";
img.
style.margin = "10px";
imageContainer.
innerHTML = "";
imageContainer.
appendChild(img);
};

reader.
readAsDataURL(file);
}
else {
alert("请选择一个有效的图片文件。");
}
});
script>
body>
html>

20240913145303



  1. Blob 和 Base64


有时你可能需要将 Blob 转换为 Base64 编码的数据(例如用于图像的内联显示或传输)。可以通过 FileReader 来实现:


const reader = new FileReader();
reader.onloadend = function () {
const base64data = reader.result;
console.log(base64data); // 输出 base64 编码的数据
};

reader.readAsDataURL(blob); // 将 Blob 读取为 base64

20240913145547


File


File 是 JavaScript 中代表文件的数据结构,它继承自 Blob 对象,包含文件的元数据(如文件名、文件大小、类型等)。File 对象通常由用户通过 选择文件时创建,也可以使用 JavaScript 构造函数手动创建。


<input type="file" id="fileInput" />
<script>
document.getElementById("fileInput").addEventListener("change", (event) => {
const file = event.target.files[0];
console.log("文件名:", file.name);
console.log("文件类型:", file.type);
console.log("文件大小:", file.size);
});
script>

最终输出结果如下图所示:


20240913141055


我们可以使用 File 的方式来访问用户上传的文件,我们也可以手动创建 File 对象:


const file = new File(["Hello, world!"], "hello-world.txt", {
type: "text/plain",
});

console.log(file);

20240913141356


File 对象继承了 Blob 对象的方法,因此可以使用一些 Blob 对象的方法来处理文件数据。



  1. slice(): 从文件中获取一个子部分数据,返回一个新的 Blob 对象。


const blob = file.slice(0, 1024); // 获取文件的前 1024 个字节


  1. text(): 读取文件内容,并将其作为文本返回(这是 Blob 的方法,但可以用于 File 对象)。


file.text().then((text) => {
console.log(text); // 输出文件的文本内容
});


  1. arrayBuffer(): 将文件内容读取为 ArrayBuffer(用于处理二进制数据)。


file.arrayBuffer().then((buffer) => {
console.log(buffer); // 输出文件的 ArrayBuffer
});


  1. stream(): 返回一个 ReadableStream 对象,可以通过流式读取文件内容。


const stream = file.stream();

20240913141746


总结


Blob 是纯粹的二进制数据,它可以存储任何类型的数据,但不具有文件的元数据(如文件名、最后修改时间等)。


File 是 Blob 的子类,File 对象除了具有 Blob 的所有属性和方法之外,还包含文件的元数据,如文件名和修改日期。


你可以将 File 对象看作是带有文件信息的 Blob。


const file = new File(["Hello, world!"], "hello.txt", { type: "text/plain" });

console.log(file instanceof Blob); // true

二者在文件上传和二进制数据处理的场景中被广泛使用。Blob 更加通用,而 File 更专注于与文件系统的交互。




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

127.0.0.1 和 localhost,如何区分?

在实际开发中,我们经常会用到 127.0.0.1 和 localhost,那么,两者到底有什么区分呢?这篇文章,我们来详细了解 127.0.0.1 和 localhost。 127.0.0.1 127.0.0.1 是一个特殊的 IPv4 地址,通常被称为“环回...
继续阅读 »



在实际开发中,我们经常会用到 127.0.0.1localhost,那么,两者到底有什么区分呢?这篇文章,我们来详细了解 127.0.0.1localhost


127.0.0.1


127.0.0.1 是一个特殊的 IPv4 地址,通常被称为“环回地址”或“回送地址”。它被用于测试和调试网络应用程序。


当你在计算机上向 127.0.0.1 发送数据包时,数据不会离开计算机,而是直接返回到本地。这种机制允许开发者测试网络应用程序而不需要实际的网络连接。


127.0.0.1 是一个专用地址,不能用于实际的网络通信,仅用于本地通信。除了 127.0.0.1,整个 127.0.0.0/8(即 127.0.0.1 到 127.255.255.255)范围内的地址都是保留的环回地址。


在 IPv6 中,类似的环回地址是 ::1。如下图,为 MacOS的 /etc/hosts 文件中的内容:


image.png


使用场景


1. 开发和测试



  • 开发人员常常使用127.0.0.1来测试网络应用程序,因为它不需要实际的网络连接。

  • 可以在本地机器上运行服务器和客户端,进行开发和调试。
    2. 网络配置和诊断:

  • 使用 ping 127.0.0.1 可以测试本地网络栈是否正常工作。

  • 一些服务会绑定到 127.0.0.1 以限制访问范围,仅允许本地访问。


示例


运行一个简单的 Python HTTP 服务器并访问它:


python -m http.server --bind 127.0.0.1 8000

然后在浏览器中访问 http://127.0.0.1:8000,你会看到服务器响应。通过 127.0.0.1,开发人员和系统管理员可以方便地进行本地网络通信测试和开发工作,而不需要依赖实际的网络连接。


优点



  1. 快速测试:可以快速测试本地网络应用程序。

  2. 独立于网络:不依赖于实际的网络连接或外部网络设备。

  3. 安全:由于数据包不离开本地计算机,安全性较高。


缺点



  1. 局限性:只能用于本地计算机,不适用于与其他计算机的网络通信。

  2. 调试范围有限:无法测试跨网络的通信问题。


localhost


localhost 是一个特殊的域名,指向本地计算机的主机名。



  • 在 IPv4 中,localhost 通常映射到 IP 地址 127.0.0.1

  • 在 IPv6 中,localhost 通常映射到 IP 地址 ::1


localhost 被定义在 hosts 文件中(例如,在 Linux 系统中是 /etc/hosts 文件)。如下图,为 MacOS的 /etc/hosts 文件中的内容:


image.png


因此,当你在应用程序中使用 localhost 作为目标地址时,系统会将其解析为 127.0.0.1,然后进行相同的环回处理。


使用场景



  • 开发和测试:开发人员常使用localhost来测试应用程序,因为它不需要实际的网络连接。

  • 本地服务:一些服务(如数据库、Web 服务器等)可以配置为只在localhost上监听,以限制访问范围仅限于本地计算机,增强安全性。

  • 网络调试:使用localhost可以帮助诊断网络服务问题,确保服务在本地环境中正常运行。


优点



  1. 易记:相对 IP 地址,localhost 更容易记忆和输入。

  2. 一致性:在不同操作系统和环境中,localhost 通常都被解析为127.0.0.1


缺点



  1. 依赖 DNS 配置:需要正确的 hosts 文件配置,如果配置错误可能导致问题。

  2. 与 127.0.0.1 相同的局限性:同样只能用于本地计算机。


两者对比



  • 本质127.0.0.1 是一个 IP 地址,而 localhost 是一个主机名。

  • 解析方式localhost 需要通过 DNS 或 hosts 文件解析为 127.0.0.1,而 127.0.0.1 是直接使用的 IP 地址。

  • 易用性localhost 更容易记忆和输入,但依赖于正确的 DNS/hosts 配置。

  • 性能:通常情况下,两者在性能上没有显著差异,因为 localhost 最终也会解析为127.0.0.1


结论


127.0.0.1localhost都是指向本地计算机的地址,适用于本地网络应用程序的测试和调试。选择使用哪个主要取决于个人偏好和具体需求。在需要明确指定 IP 地址的场景下,127.0.0.1 更为直接;而在需要易记和通用的主机名时,localhost 更为合适。两者在实际使用中通常是等价的,差别微乎其微。




作者:猿java
来源:juejin.cn/post/7413189674107273257
收起阅读 »

uni-app小程序超过2M怎么办?

web
一、开发版 开发版可以调整上限为4M 开发者工具 -> 详情 -> 本地设置 -> 预览及真机调试时主包、分包体积上限调整为4M -> 勾选 二、体验版、正式版 上传代码时,主包必须在2M以内。 小程序tabbar页面必须放在主包。 推...
继续阅读 »

一、开发版


开发版可以调整上限为4M


开发者工具 -> 详情 -> 本地设置 -> 预览及真机调试时主包、分包体积上限调整为4M -> 勾选


二、体验版、正式版


上传代码时,主包必须在2M以内。


小程序tabbar页面必须放在主包。


推荐除了tabbar页面以外,其余的都放在分包。其实只要这样做了,再复杂的小程序,主包代码都很难超过2M,但如果是uni-app开发的,那就不一定了。


uni-app优化


开发环境压缩代码


使用cli创建的项目


package.jsonscript中设置压缩:在命令中加入--minimize


"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch --minimize",

使用hbuilderx创建的项目


顶部菜单栏点击运行 -> 运行到小程序模拟器 -> 运行时是否压缩代码 -> 勾选


开启压缩后,开发环境的小程序代码体积会大大降低


uni.scss优化


uni-app项目创建后会自带一个uni.scss文件,这个文件无需手动引入,会自动引入到每一个页面文件,所以尽量不要在这个文件内写公共css代码。


我接手的一个uni-app小程序项目,随着功能迭代,打包代码主包体积越来越接近2M,终于有一天写完一个功能,突然就达到了2.2M,无法上传了。参考小程序提供的代码依赖分析,发现wxss文件占用了大部分体积,于是我就去一个个搜,看某个class有没有被用到,没用到的就删掉,可是再怎么优化冗余代码,也无法降到2M以下。


直到我看到了uni.scss文件,除了里面自带的一些颜色变量代码,另外还加了700行的公共class,然后我在根目录新建一个assets/common.scss文件,把那700行代码移出去,在App.vue内引入


@import './assets/common.scss'

主包体积瞬间降到了1.41M


image.png


总结


重要的事情说三遍



  • 不要在uni.scss文件内写公共css代码

  • 不要在uni.scss文件内写公共css代码

  • 不要在uni.scss文件内写公共css代码


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

2024 前端趋势:全栈也许已经是必选项

web
《2023 的 Javascript 星趋势》在 1 月就出来了,当时略略看了一下,并没有想太多。 过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。 React 与 Vue 生态对比 首先,我们来看看 React 与 Vue 生态的...
继续阅读 »

《2023 的 Javascript 星趋势》在 1 月就出来了,当时略略看了一下,并没有想太多。


过年期间,回想起来,似乎感觉到这次有一点不一样:也许,全栈的时代已经到来了。


React 与 Vue 生态对比


首先,我们来看看 React 与 Vue 生态的星趋势对比:


截屏2024-02-29 10.05.39转存失败,建议直接上传图片文件


上图中,React 整个生态的星星数远超于 Vue,第十名都要比 Vue 第一名的多。我们将其做一个分类:


排名ReactVue
1UI全栈
2白板演示文稿
3全栈后台管理系统
4状态管理hook
5后台管理系统UI
6文档文档
7全栈框架集成UI
8全栈框架UI框架
9后台管理系统UI
10无服务栈状态管理

可以看到 React 这边的生态链基本成熟,几乎每一个分类都有一个上榜的库,不再像 Vue 那样还在卷 UI 框架。


在全栈方面,Vue 的首位就是全栈 Nuxt。


React 的 Next.js 虽然不在首位,但是服务端/全栈相关的内容就占了 4 个,其中包含第 10 名的无服务栈。另外值得注意的是,React 这边还有服务端组件的概念。Shadcn/ui 能占到第一位,因为它基于无头 UI Radix 实现的,在服务端组件也能运用。所以,服务端/全栈在 React 中占的比重相当大的。


这样看来,前端往服务端进发已经成为一个必然趋势。


htmx 框架的倒退


再看看框架这边,htmx 在星趋势里,排行第二位,2023增长的星星数为 15.6K,与第一位的 React 颇为相近。


而 htmx 也是今年讨论度最高的。


在我经历过前后端不分离的阶段中,使用 jsp 生成前端页面,js 更多是页面炫技的工具。然后在 jQuery + Ajax 得到广泛应用之后,才真正有前后端分离的项目。


htmx 的出现,不了解的人,可能觉得是倒退到 Java + jQuery + Ajax 的前后端分离状态。但是,写过例子之后,我发现,它其实是倒退到了前后端不分离的阶段。


用 java 也好,世界上最好的 php 也好,或者用现在的 nodejs 服务,都能接入 htmx。你只要在服务端返回 html 即可。


/** nodejs fastity 写的一个例子 **/
import fastify from 'fastify'
import fastifyHtml from 'fastify-html'
import formbody from '@fastify/formbody';

const app = fastify()
await app.register(fastifyHtml)
await app.register(formbody);
// 省略首页引入 htmx

// 首页的模板,提供一个按钮,点击后请求 html,然后将请求返回的内容渲染到 parent-div 中
app.get('/', async (req, reply) => {
const name = req.query.name || 'World'
return reply.html`

Hello ${name}


`
, reply
})

// 请求返回 html
app.post('/clicked', (req, reply) => {
reply.html`

Clicked!

`
;
})

await app.listen({ port: 3000 })

也许大家会觉得离谱,但是很显然,事情已经开始发生了变化,后端也来抢前端饭碗了。


截屏2024-02-29 10.32.24.png


htmx 在 github 上已经有不少跟随者,能搜出前端代码已有不少,前三就有基于 Python 语言的 Django 服务端框架。


jQuery 见势头不错,今年也更新了 4.0 的 beta 版本,对现代浏览器提供了更好的支持。这一切似乎为旧架构重回大众视野做好了准备。


企业角度


站在企业角度来看,一个人把前后端都干了不是更好吗?


的确如此。前后端一把撸更符合企业的利益。国外的小公司更以全栈作为首选项。


也许有人觉得国情不同,但是在我接触的前端群里,这两年都有人在群里说他们公司前后端分离的情况。


还有的人还喜欢大厂那一套,注意分工合作,但是其实大厂里遗留项目也不少,有的甚至是 php;还有新的实验项目,如果能投入最少人力,快速试错,这种全栈的框架自然也是最优选择。


我并不是说,前后端分离不值得。但是目前已经进入 AI 赛道,企业对后台系统的开发,并不愿意投入更多了。能用就行已经成为当前企业的目标,自然我们也应该跟着变化。


全栈破局


再说说前端已死的论调。我恰恰觉得这是最好做改变的时机。


在浏览器对新技术支持稳定,UI 框架趋同,UI 组件库稳定之后,前端不再需要为浏览器不兼容素手无策了,不再需要苦哈哈地为1个像素争辩不停了,也不再需要为产品莫名其妙的交互焦头烂额了。


这并不意味着前端已死,反而可能我们某个阶段的任务完成了,后面有更重要的任务交给我们。也许,全栈就是一个破局。


在云服务/云原生如此普遍的情况下,语言不再是企业开发考虑的主要因素,这也为 nodejs 全栈铺平了道路。


前端一直拣最苦最脏的话来做,从 UI 中拿到了切图的工作,然后接手了浏览器兼容的活,后来又从后端拿到了渲染页面的工作。


那我们为何不再进一步,主动把 API 开发的工作也拿过来?


作者:陈佬昔没带相机
来源:juejin.cn/post/7340603873604599843
收起阅读 »

8个小而美的前端库

web
前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。 2024 年推荐以下小而美的库。 radash 实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数...
继续阅读 »

前端有很多小而美的库,接入成本很低又能满足日常开发需求,同时无论是 npm 方式引入还是直接复制到本地使用都可以。


2024 年推荐以下小而美的库。


radash


实用的工具库,相比与 lodash,更加面向现代,提供更多新功能(tryit,retry 等函数),源码可读性高,如果不想安装它,大部分函数可以直接复制到本地使用。



use-debounce


React Hook Debouce 库,让你不再为使用防抖烦恼。库的特点:体积小 < 1 Kb、与 underscore / lodash impl 兼容 - 一次学习,随处使用、服务器渲染友好。



timeago.js


格式化日期时间库,比如:“3 hours ago”,支持多语言,仅 2Kb 大小。同时提供了 React 版本 timeago-react。


timeage.format(1544666010224, 'zh_CN') // 输出 “5 年前”
timeage.format(Date.now() - 1000, 'zh_CN') // 输出 “刚刚”
timeage.format(Date.now() - 1000 * 60 * 5, 'zh_CN') // 输出 “5 分钟前”

react-use


实用 Hook 大合集 - 内容丰富,从跟踪电池状态和地理位置,到设置收藏夹、防抖和播放视频,无所不包。



dayjs


Day.js 是一个简约的 JavaScript 库,仅 2 Kb 大小。它可以使用基本兼容 Moment.js,为你提供日期的解析、处理和显示,支持多语言能力。



filesize


filesize.js 提供了一种简单方法,便于从数字(浮点数或整数)或字符串转换成可读性高的文件大小,filesize.min.js 大小为 2.94 kb。


import {filesize} from "filesize";
filesize(265318, {standard: "jedec"}); // "259.1 KB"

driver.js


driver.js 是一款用原生 js 实现的页面引导库,上手非常简单,体积在 gzip 压缩下仅仅 5kb。



@formkit/drag-and-drop


FormKit DnD 是一个小型拖拽库,它简单、灵活、与框架无关,压缩后只有 4Kb 左右,设计理念为数据优先。



小结


前端小而美的库使用起来一般都比较顺手,欢迎在评论区推荐你们开发中的使用小而美的库。


作者:晓得迷路了
来源:juejin.cn/post/7350140676615798824
收起阅读 »

登录页面一些有趣的css效果

web
前言 今天无意看到一个登录页,input框focus时placeholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及...
继续阅读 »

前言


今天无意看到一个登录页,inputfocusplaceholder上移变成label的效果,无聊没事干就想着自己来实现一下,登录页面能做文章的,普遍的就是按钮动画,title的动画,以及input的动画,这是最终的效果图(如下), 同时附上预览页以及实现源码


919c40a2a264f683ab5e74e8a649ac5.png


title 的动画实现


首先描述一下大概的实现效果, 我们需要一个镂空的一段白底文字,在鼠标移入时给一个逐步点亮的效果。
文字镂空我们可以使用text-stroke, 逐步点亮只需要使用filter即可


text-stroke


text-stroke属性用于在文本的边缘周围添加描边效果,即文本字符的外部轮廓。这可以用于创建具有描边的文本效果。text-stroke属性通常与-webkit-text-stroke前缀一起使用,因为它目前主要在WebKit浏览器(如Chrome和Safari)中支持


text-stroke属性有两个主要值:



  1. 宽度(width) :指定描边的宽度,可以是像素值、百分比值或其他长度单位。

  2. 颜色(color) :指定描边的颜色,可以使用颜色名称、十六进制值、RGB值等。


filter


filter是CSS属性,用于将图像或元素的视觉效果进行处理,例如模糊、对比度调整、饱和度调整等。它可以应用于元素的背景图像、文本或任何具有视觉内容的元素。


filter属性的值是一个或多个滤镜函数,这些函数以空格分隔。以下是一些常见的滤镜函数和示例:



  1. 模糊(blur) : 通过blur函数可以实现模糊效果。模糊的值可以是像素值或其他长度单位。


    .blurred-image {
    filter: blur(5px);
    }


  2. 对比度(contrast) : 通过contrast函数可以调整对比度。值为百分比,1表示原始对比度。


    .high-contrast-text {
    filter: contrast(150%);
    }


  3. 饱和度(saturate) : 通过saturate函数可以调整饱和度。值为百分比,1表示原始饱和度。


    .desaturated-image {
    filter: saturate(50%);
    }


  4. 反色(invert) : 通过invert函数可以实现反色效果。值为百分比,1表示完全反色。


    .inverted-text {
    filter: invert(100%);
    }


  5. 灰度(grayscale) : 通过grayscale函数可以将图像或元素转换为灰度图像。值为百分比,1表示完全灰度。


    .gray-text {
    filter: grayscale(70%);
    }


  6. 透明度(opacity) : 通过opacity函数可以调整元素的透明度。值为0到1之间的数字,0表示完全透明,1表示完全不透明。


    .semi-transparent-box {
    filter: opacity(0.7);
    }


  7. 阴影(drop-shadow) :用于在图像、文本或其他元素周围添加阴影效果。这个属性在 CSS3 中引入,通常用于创建阴影效果,使元素看起来浮在页面上或增加深度感


    drop-shadow(<offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>?)

    各个值的含义如下:



    • <offset-x>: 阴影在 X 轴上的偏移距离。

    • <offset-y>: 阴影在 Y 轴上的偏移距离。

    • <blur-radius> (可选): 阴影的模糊半径。默认值为 0。

    • <spread-radius> (可选): 阴影的扩散半径。默认值为 0。

    • <color> (可选): 阴影的颜色。默认值为当前文本颜色。




filter属性的支持程度因浏览器而异,因此在使用时应谨慎考虑浏览器兼容性。


实现移入标题点亮的效果


想实现移入标题点亮的效果我们首先需要两个通过定位重叠的span元素,一个做镂空用于展示,另一个作为
hover时覆盖掉镂空元素,并通过filter: drop-shadow实现光影效果,需要注意的是这里需要使用inline元素实现效果。


title-animation.gif


input 的动画实现


input的效果比较简单,只需要在focusspan(placeholder)上移变成span(label)同时给inputborder-bottom做一个底色的延伸,效果确定了接着就看看实现思路。


input placeholder 作为 label


使用div作为容器包裹inputspanspan首先绝对定位到框内,伪装为placeholder, 当input状态为focus提高spantop值,即可伪装成label, 这里有两个问题是:



  1. 当用户输入了值的时候,span并不需要恢复为之前的top, 这里我们使用css或者js 去判断都可以, js就是拿到输入框的值,这里不多做赘述,css 有个比较巧妙的做法, 给input required属性值设置为required, 这样可以使用css:valid伪类去判断input是否有值。

  2. 由于span层级高于input,当点击span时无法触发input的聚焦,这个问题我们可以使用pointer-events: none; 来解决。pointer-events 是一个CSS属性,用于控制元素是否响应用户的指针事件(例如鼠标点击、悬停、触摸等)。这个属性对于控制元素的可交互性和可点击性非常有用。


    pointer-events 具有以下几个可能的值:



    1. auto(默认值):元素会按照其正常行为响应用户指针事件。这是默认行为。

    2. none:元素不会响应用户的指针事件,就好像它不存在一样。用户无法与它交互。

    3. visiblePainted:元素在绘制区域上响应指针事件,但不在其透明区域上响应。这使得元素的透明部分不会响应事件,而其他部分会。

    4. visibleFill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    5. visibleStroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。

    6. painted:元素会在其绘制区域上响应指针事件,包括填充、边框和透明区域。

    7. fill:元素在其填充区域上响应指针事件,但不在边框区域上响应。

    8. stroke:元素在其边框区域上响应指针事件,但不在填充区域上响应。




pointer-events 属性非常有用,特别是在创建交互性复杂的用户界面时,可以通过它来控制元素的响应区域。例如,你可以使用它来创建自定义的点击区域,而不仅仅是元素的边界。它还可以与其他CSS属性和JavaScript事件处理程序结合使用,以创建特定的交互效果。


input border bottom 延伸展开效果


效果比较简单,input被聚焦的时候,一个紫色的边从中间延伸覆盖白色的底边即可。 在使用一个span作为底部的边, 初始不可见, focus时从中间向两边延伸直至充满, 唯一头痛的就是怎么从中间向两边延伸,这里可以使用transform变形,首先使用transform: scaleX(0);达到不可见的效果, 然后设置变形原点为中间transform-origin: center;,这样效果就可以实现了


input 的动画实现效果


input-animation.gif


按钮的动画实现


关于按钮的动画很多,我们这里就实现一个移入的散花效果,移入时发散出一些星星,这里需要使用到动画去实现了,首先通过伪类创建一些周边元素,这里需要用到 background-image(radial-gradient)


background-image(radial-gradient)


background-image 属性用于设置元素的背景图像,而 radial-gradient 是一种 CSS 渐变类型,可用于创建径向渐变背景。这种径向渐变背景通常以一个中心点为基础,然后颜色渐变向外扩展,形成一种放射状的效果。


radial-gradient 的语法如下:


background-image: radial-gradient([shape] [size] at [position], color-stop1, color-stop2, ...);


  • [shape]: 可选,指定渐变的形状。常用的值包括 "ellipse"(椭圆)和 "circle"(圆形)。

  • [size]: 可选,指定渐变的大小。可以是长度值或百分比值。

  • at [position]: 可选,指定渐变的中心点位置。

  • color-stopX: 渐变的颜色停止点,可以是颜色值、百分比值或长度值。


按钮移入动画效果实现


btn-animation.gif


结尾


css 能实现的效果越来越多了,遇到有趣的效果,可以自己想想实现方式以及动手实现一下,思路毕竟是思路,具体实现起来说不定会遇到什么坑,逐步解决问题带来的成就感满足感还是很强的。


作者:刘圣凯
来源:juejin.cn/post/7294908459002331171
收起阅读 »

BOE(京东方)领先科技赋能体育产业全面向新 以击剑、电竞、健身三大应用场景诠释未来健康运动新生活

巴黎全球体育盛会虽已闭幕,但世界范围内的运动热潮并未消退。9月12日,在北京恒通国际商务园(UBP)的之所ICC,BOE(京东方)开启了以“屏实力 FUN肆热爱”为主题的“科技赋能体育”互动体验活动。活动现场,BOE(京东方)携手海信、创维、联想、AGON、R...
继续阅读 »

巴黎全球体育盛会虽已闭幕,但世界范围内的运动热潮并未消退。9月12日,在北京恒通国际商务园(UBP)的之所ICC,BOE(京东方)开启了以“屏实力 FUN肆热爱”为主题的“科技赋能体育”互动体验活动。活动现场,BOE(京东方)携手海信、创维、联想、AGON、ROG、一加、红魔等众多全球一线合作伙伴,全面展示了围绕击剑、电竞、健身三大应用场景的尖端科技产品,并打造了“显示视界”、“电子竞技”、“运动健身”三大互动体验区。中国国家击剑队女子重剑运动员余思涵、北京JDG英特尔战队分析师Vusso以及众多在京媒体出席了开幕仪式,并共同探讨“前沿科技赋能体育新生态”的深耕布局与应用趋势。据悉,该活动将全面向公众开放至9月14日,大众将通过现场沉浸式互动体验全方位感受创新科技赋能体育向新的独特魅力,更深度诠释了未来健康运动新生活的全新范式。

BOE(京东方)“科技赋能体育”互动体验活动现场

BOE(京东方)副总裁、首席品牌官司达在现场发言中表示,体育产业是BOE(京东方)“屏之物联”战略赋能应用场景的重要发力方向之一。在当前人工智能等新技术引领的智能化浪潮下,BOE(京东方)的创新科技正在体育产业中发挥着日益重要的作用。从2016年里约全球体育赛事的首次8K超高清实况转播,到2021年携手中国击剑队亮剑东京;到2022年冰雪盛会开闭幕式上的“雪花”绽放、再到2023年助力《梦三国2》电竞项目在杭州赛场奋勇夺金、2024年助力中国国家击剑队亮剑巴黎,BOE(京东方)正在通过全方位的科技赋能推动体育产业向智能化、科技化全面迈进。

BOE(京东方)副总裁、首席品牌官司达现场发言

科技赋能击剑,打造沉浸式赛训观赛新视界

在“显示视界”展区,由BOE(京东方)ADS Pro赋能的创维75英寸A7E Pro壁纸电视可呈现110% DCI-P3电影级超广色域,带来极致绚丽的画面表现力和丰富细腻的层次变化,高达288Hz的极速高刷新率让每一次出剑瞬间都行云流水般流畅丝滑,畅享全新沉浸式大屏观赛视觉盛宴。海信75英寸E8N Ultra ULED超画质电视同样由ADS Pro赋能,5800nits超高亮度配合288Hz超高刷新率呈现清晰锐利、逼真生动、流畅灵动的专业级画质表现,搭载击剑互动游戏让现场观众惊叹于大屏操作的每一个精彩瞬间。不仅如此,现场BOE(京东方)还带来了由高端柔性OLED显示技术解决方案f-OLED赋能的全球一线终端品牌的内折、上下翻折高端旗舰手机,全面解锁未来体育观赛的无限想象空间。

BOE(京东方)ADS Pro赋能创维75英寸A7E Pro壁纸电视

中国国家击剑队女子重剑运动员余思涵、北京JDG英特尔战队分析师Vusso现场体验

作为中国国家击剑队首席战略合作伙伴,多年来,BOE(京东方)的智慧显示、智慧物联、数字健康等物联网创新解决方案已覆盖击剑运动员的训练备战、战术分析、体能监测、健康管理等方方面面,全方位助力中国击剑队征战2018年亚洲体育盛会、2021东京全球体育盛会、2023年杭州亚洲体育盛会、2024年巴黎全球体育盛会等众多荣耀巅峰时刻,以硬核实力为体育注入科技力量。

科技赋能电竞,打造沉浸式竞技极致新体验

在“电子竞技”展区,BOE(京东方)联合AGON重磅打造的电竞显示终端,在ADS Pro加持下可实现高达520Hz的极致超高刷新率,配合千分之一秒的极限响应速度,精准还原职业电竞选手每一帧精妙的操作细节;由ADS Pro强势赋能的联想拯救者R9000P以100%DCI-P3超广色域及240Hz超高刷新率的领先性能,让电竞玩家尽情畅享大圣战斗的极致竞技体验;ROG 绝神27 XG27UCS在ADS Pro加持下可实现接近180°的超广视角,玩家无论正面观看还是侧面观看,都能获得原生态焕彩完美画质。此外,搭载BOE(京东方)高端柔性OLED技术的红魔、一加等多款游戏手机凭借高清、高刷、低蓝光护眼等领先优势,强势助力玩家在手游赛场尽情发挥,克敌制胜。

520Hz超高刷新率电竞显示终端

BOE(京东方)科技赋能专业电竞显示产品

作为电竞领域的科技引领者,BOE(京东方)已携手联想、戴尔、华硕、AOC等全球一线品牌推出众多超高刷、超高清、超高画质的专业电竞显示产品,目前,BOE(京东方)在电竞笔记本、显示器、手机等专业电竞显示领域均已处于全球领先地位。同时,BOE(京东方)还携手京东,与众多全球一线品牌联合成立Best of Esports电竞高阶联盟,并联合虎牙直播及联盟成员共同举办BOE无畏杯《无畏契约》挑战赛,推动构筑覆盖硬件、终端、内容、市场全链路的电竞生态,为我国电竞产业向新发展注入创新动力。

科技赋能健身,打造沉浸式健康生活新空间

在“运动健身”展区,BOE(京东方)更将显示、VR、传感等前沿技术与运动健身场景创新融合,引领全新的健康生活新潮流。展区内,动感单车配备专业VR头显设备,通过高清、高画质的VR显示技术实现虚拟与现实的深度交互,以别开生面的创新骑行模式引领健身运动新风潮;极具科技感的健身“魔镜”融合多种智能化功能于一体,用户可一边观看教程一边同步对镜矫正姿态,让健身更加智能化、可视化、趣味化;此外,现场展出的可穿戴智能健康手表搭载专业健康监测软件系统,为用户带来全方位的健康管理贴心呵护。

中国国家击剑队运动员余思涵现场体验

近年来,BOE(京东方)深入布局健康领域,推出的数字人体终端、智能体征监测系统、远望学习屏等一系列创新科技产品为大众健康生活带来全新体验。同时,基于多年在“医工融合”高潜方向的前瞻布局与深厚积淀,BOE(京东方)还将显示、传感、物联网、人工智能等技术与前沿医学融合创新,聚焦AI+医疗、数字医院、智慧医养社区等全新技术方向及场景形态,为未来医疗健康产业带来深远影响。

当前,随着物联网、人工智能、大数据等前沿技术引领数字化、智能化浪潮奔涌而来,BOE(京东方)的创新科技还将进一步深度融入体育竞技与运动健康等各大应用场景,携手更多顶级体育赛事及产业链合作伙伴,以顶尖科技力量描绘体育产业智能化高质量发展新图景!

收起阅读 »

桌面端Electron基础配置

机缘 机缘巧合之下获取到一个桌面端开发的任务。 为了最快的上手速度,最低的开发成本,选择了electron。 介绍 Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js ...
继续阅读 »

机缘


机缘巧合之下获取到一个桌面端开发的任务。


为了最快的上手速度,最低的开发成本,选择了electron。


image.png


介绍


Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。


主要结构


相关文章1
相关文章2


image.png


electron主要有一个主进程和一个或者多个渲染进程组成,方便的脚手架项目有
electron-vite


安装方式


npm i electron-vite -D

electron-vite分为3层结构


main // electron主进程
preload // electron预加载进程 node
renderer // electron渲染进程 vue

创建项目


npm create @quick-start/electron

项目创建完成启动之后
会在目录中生成一个out目录


image.png


out目录中会生成项目文件代码,在electron-vite中使用ESmodel来加载文件,启动的时候会被全部打包到out目录中合并在一起。所以一些使用CommonJs的node代码复制进来需要做些修改。npm安装的依赖依然可以使用CommonJs的方式引入。


node的引入


image.png
在前面的推荐的几篇文章中都有详细的讲解,无需多言。electron是以chrom+node,所以node的加入也非常的简单。
nodeIntegration: true,


main主进程中的简单配置


image.png


preload目录下引入node代码,留一个口子在min主进程中调用。


配置数据库


sequelize为例


npm install --save sequelize
npm install --save sqlite3

做本地应用使用推荐sqlite3,使用本地数据库,当然了用其他的数据也没问题,用法和node中一样。需要注意的是C++代码编译的问题,可能会存在兼容性问题,如果一直尝试还是报错就换版本吧。electron-vite新版本问题不大,遇到过老版本一直编译失败的问题


测试能让用版本



  • "electron": "^25.6.0",

  • "electron-vite": "^1.0.27",

  • "sequelize": "^6.33.0",


image.png


node-gyp vscode 这些安装环境网上找找也很多就不多说了。


import { Sequelize } from 'sequelize'
import log from '../config/log/log'

const path = require('path')

let documentsPath

if (process.env['ELECTRON_RENDERER_URL']) {
documentsPath = './out/config/sqlite/sqlite.db'
} else {
documentsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\sqlite\\sqlite.db'
}

console.log('documentsPath-------------****-----------', documentsPath)

export const seq = new Sequelize({
dialect: 'sqlite',
storage: documentsPath
})

seq
.authenticate()
.then(() => {
log.info('数据库连接成功')
})
.catch((err) => {
log.error('数据库连接失败' + err)
})


终端乱码问题


"dev:win": "chcp 65001 && electron-vite dev",
chcp 65001只在win环境下添加


electron多页签


文章推荐


electron日志


import logger from 'electron-log'

logger.transports.file.level = 'debug'
logger.transports.file.maxSize = 30 * 1024 * 1024 // 最大不超过10M
logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}' // 设置文件内容格式

var dayjs = require('dayjs')
const date = dayjs().format('YYYY-MM-DD') // 格式化日期为 yyyy-mm-dd

logger.transports.file.fileName = date + '.log' // 创建文件名格式为 '时间.log' (2023-02-01.log)

// 可以将文件放置到指定文件夹中,例如放到安装包文件夹中
const path = require('path')
let logsPath

if (process.env['ELECTRON_RENDERER_URL']) {
logsPath = './out/config/logs/' + date + '.log'
} else {
logsPath = path.join(process.env.USERPROFILE, 'Documents') + '\\logs\\' + date + '.log'
}

console.log('logsPath-------------****-----------', logsPath) // 获取到安装目录的文件夹名称

// 指定日志文件夹位置
logger.transports.file.resolvePath = () => logsPath

// 有六个日志级别error, warn, info, verbose, debug, silly。默认是silly
export default {
info(param) {
logger.info(param)
},
warn(param) {
logger.warn(param)
},
error(param) {
logger.error(param)
},
debug(param) {
logger.debug(param)
},
verbose(param) {
logger.verbose(param)
},
silly(param) {
logger.silly(param)
}
}


对应用做好日志维护是一个很重要的事情


主进程中也可以在main文件下监听


    app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// 渲染进程崩溃
app.on('renderer-process-crashed', (event, webContents, killed) => {
log.error(
`APP-ERROR:renderer-process-crashed; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
webContents
)}
; killed:${JSON.stringify(killed)}`

)
})

// GPU进程崩溃
app.on('gpu-process-crashed', (event, killed) => {
log.error(`APP-ERROR:gpu-process-crashed; event: ${JSON.stringify(event)}; killed: ${JSON.stringify(killed)}`)
})

// 渲染进程结束
app.on('render-process-gone', async (event, webContents, details) => {
log.error(
`APP-ERROR:render-process-gone; event: ${JSON.stringify(event)}; webContents:${JSON.stringify(
webContents
)}
; details:${JSON.stringify(details)}`

)
})

// 子进程结束
app.on('child-process-gone', async (event, details) => {
log.error(`APP-ERROR:child-process-gone; event: ${JSON.stringify(event)}; details:${JSON.stringify(details)}`)
})

应用更新


在Electron中实现自动更新,需要使用electron-updater


npm install electron-updater --save


需要知道服务器地址,单版本号有可更新内容的时候可以通过事件监听控制更新功能


provider: generic
url: 'http://localhost:7070/urfiles'
updaterCacheDirName: 111-updater


import { autoUpdater } from 'electron-updater'
import log from '../config/log/log'
export const autoUpdateInit = (mainWindow) => {
let result = {
message: '',
result: {}
}
autoUpdater.setFeedURL('http://localhost:50080/latest.yml')

//设置自动下载
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false

// 监听error
autoUpdater.on('error', function (error) {
log.info('检测更新失败' + error)
result.message = '检测更新失败'
result.result = error
mainWindow.webContents.send('update', JSON.stringify(result))
})

// 检测开始
autoUpdater.on('checking-for-update', function () {
result.message = '检测更新触发'
result.result = ''
// mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新触发`)
})

// 更新可用
autoUpdater.on('update-available', (info) => {
result.message = '有新版本可更新'
result.result = info
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`有新版本可更新${JSON.stringify(info)}${info}`)
})

// 更新不可用
autoUpdater.on('update-not-available', function (info) {
result.message = '检测更新不可用'
result.result = info
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新不可用${info}`)
})

// 更新下载进度事件
autoUpdater.on('download-progress', function (progress) {
result.message = '检测更新当前下载进度'
result.result = progress
mainWindow.webContents.send('update', JSON.stringify(result))
log.info(`检测更新当前下载进度${JSON.stringify(progress)}${progress}`)
})

// 更新下载完毕
autoUpdater.on('update-downloaded', function () {
//下载完毕,通知应用层 UI
result.message = '检测更新当前下载完毕'
result.result = {}
mainWindow.webContents.send('update', result)
autoUpdater.quitAndInstall()
log.info('检测更新当前下载完毕,开始安装')
})
}

export const updateApp = (ctx) => {
let message
if (ctx.params == 'inspect') {
console.log('检测是否有新版本')
message = '检测是否有新版本'

autoUpdater.checkForUpdates() // 开始检查是否有更新
}
if (ctx.params == 'update') {
message = '开始更新'
autoUpdater.downloadUpdate() // 开始下载更新
}
return (ctx.body = {
code: 200,
message,
result: {
currentVersion: 0
}
})
}


dev下想测试更新功能,可以在主进程main文件中添加


Object.defineProperty(app, 'isPackaged', {
get() {
return true
}
})

接口封装


eletron中可以像node一样走http的形式编写接口,但是更推荐用IPC走内存直接进行主进程和渲染进程之间的通信


前端


import { ElMessage } from 'element-plus'
import router from '../router/index'

export const getApi = (url: string, params: object) => {
return new Promise(async (resolve, rej) => {
try {
console.log('-------------------url+params', url, params)

// 如果有token的话
let token = sessionStorage.getItem('token')
// 走ipc
if (window.electron) {
const res = await window.electron.ipcRenderer.invoke('getApi', JSON.stringify({ url, params, token }))
console.log('res', res)
if (res?.code == 200) {
return resolve(res.result)
} else {
// token校验不通过退出登录
if (res?.error == 10002 || res?.error == 10002) {
router.push({ name: 'loginPage' })
}
// 添加接口错误的处理
ElMessage.error(res?.message || res || '未知错误')
rej(res)
}
} else {
// 不走ipc

}
} catch (err) {
console.error(url + '接口请求错误----------', err)
rej(err)
}
})
}


后端


ipcMain.handle('getApi', async (event, args) => {
const { url, params, token } = JSON.parse(args)
//
})

electron官方文档中提供的IPC通信的API有好几个,每个使用的场景不一样,根据情况来选择


node中使用的是esmodel和一般的node项目写法上还有些区别,得适应一下。


容易找到的都是渲染进程发消息,也就是vue发消息给node,但是node发消息给vue没有写


这时候就需要使用webContents方法来实现


  this.mainWindow.webContents.send('receive-tcp', JSON.stringify({ code: key, data: res.data }))

使用webContents的时候在vue中一样是通过事件监听‘receive-tcp’事件来获取


本地图片读取


  // node中IO操作是异步所以得订阅一下
const subscribeImage = new Promise((res, rej) => {
// 读取图片文件进行压缩
sharp(imagePath)
.webp({ quality: 80 })
.toBuffer((err, buffer) => {
if (err) {
console.error('读取本地图片失败Error converting image to buffer:', err)
rej(
(ctx.body = {
error: 10003,
message: '本地图片读取失败'
})
)
} else {
log.info(`读取本地图片成功:${ctx.params}`)
res({
code: 200,
msg: '读取本地图片成功:',
result: buffer.toString('base64')
})
}
})
})

TCP


既然写了桌面端,那数据交互的方式可能就不局限于http,也会有WS,TCP,等等其他的通信协议。


node中提供了Tcp模块,net


const net = require('net')
const server = net.createServer()

server.on('listening', function () {
//获取地址信息
let addr = server.address()
tcpInfo.TcpAddress = `ip:${addr.port}`
log.info(`TCP服务启动成功---------- ip:${addr.port}`)
})
//设置出错时的回调函数
server.on('error', function (err) {
if (err.code === 'EADDRINUSE') {
console.log('地址正被使用,重试中...')
tcpProt++
setTimeout(() => {
server.close()
server.listen(tcpProt, 'ip')
}, 1000)
} else {
console.error('服务器异常:', err)
}
})

TCP链接成功获取到数据之后在data事件中,就可以使用webContents方法来主动传递消息给渲染进程
也得对Tcp数据包进行解析,一般都是和外部系统协商沟通的数据格式。一般是十六进制或者是二进制数据,需要对数据进行解析,切割,缓存。
使用 Bufferdata = Buffer.concat([overageBuffer, data]) 对数据进行处理
根据数据的长度对数据进行切割,判断数据的完整性质,对数据进行封包和拆包


粘包处理网上都有
处理完.toString()一下 over


socket.on('data', async (data) => {
...
let buffer = data.slice(0, packageLength) // 取出整个数据包
data = data.slice(packageLength) // 删除已经取出的数据包
// 数据处理
let key = buffer.slice(4, 8).reverse().toString('hex')
console.log('data', key, buffer)
let res = await isFunction[key](buffer)
this.mainWindow.webContents.send('receive-tcpData', JSON.stringify({ code: key, data: res.data }))
})


// 获取包长度的方法
getPackageLen(buffer) {
let bufferCopy = Buffer.alloc(12)
buffer.copy(bufferCopy, 0, 0, 12)
let bufferSize = bufferCopy.slice(8, this.headSize).reverse().readInt32BE(0)
console.log('bufferSize', bufferSize, bufferSize + this.headSize, buffer.length)
if (bufferSize > buffer.length - this.headSize) {
return -1
}
if (buffer.length >= bufferSize + this.headSize) {
return bufferSize + this.headSize // 返回实际长度 = 消息头长度 + 消息体长度
}
}

打完收工


image.png


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

日历表格的制作,我竟然选择了这样子来实现...

web
前言 最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在...
继续阅读 »

前言


最近有个日历表格的需求,具体效果如下所示,鼠标经过时表格还有一个十字高亮的效果,在拿到这个设计图的时候,就在想应该用什么来实现,由于我所在的项目用的是vue3 + element,所以我第一时间想到的就是饿了么里面的表格组件,但是经过一番调式之后,发现在饿了么表格的基础上想要调整我要的样式效果太复杂太麻烦了,所以我决定用原生的div循环来实现!


soogif.gif


第一步 初步渲染表格


由于表格的表头是固定的,我们可以先渲染出来


<script setup lang="ts">

const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content"></span>
<span class="bottom-content"></span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ item }}
</div>
</div>
</div>
</div>
</div>
</template>


<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;

.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}

.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}

看一下页面效果:


image.png
表格的表头初步完成!


第二步 确认接口返回的数据格式


这是接口返回的格式数据 就例如第一个对象代表着3月9号有数据


{
"3": {
"9": 1
},
"4": {
"12": 2
},
"5": {
"11": 1,
"12": 2,
"21": 1
},
"6": {
"6": 5,
"8": 1,
"9": 2,
"10": 1,
"12": 2,
"17": 1,
"20": 1
},
"7": {
"1": 8,
"4": 1,
"7": 1,
"6": 1,
"13": 1,
"22": 1,
"25": 1,
"26": 1,
"27": 1,
"29": 6,
"30": 1
},
"8": {
"1": 1,
"2": 2,
"7": 1,
"20": 1,
"24": 1,
"27": 1,
"31": 1
},
"9": {
"15": 1,
"17": 9,
"21": 2
},
"10": {
"23": 1
}
}

接着我们需要对返回的数据做处理,由于表格的表头已经渲染出来,这意味着表格的每一列都有了,接下来我们就需要渲染表格的每一行与其对应就可以了.十二个月份我们需要十二行,同时每一行的第一个单元格表示的是月份,那我们可以定义一个月份的数据,然后再根据接口数据做处理,返回一个带有对应月份数据的数组.
代码如下:


const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]
// 把接口数据转换为对应的月份数组对象
const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)

for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}

const tableData = ref<any[]>([])

onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})

我们可以看一下控制台,此时的tableData的数据格式是怎么样的


image.png
接下来就可以开始渲染表格的内容了,给有数据的单元格做个高亮,同时固定31天,所以可以先遍历出每一行31个单元格出来


  <div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>

<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index] ? '#6fa7ea' : ''
}"

>

<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>

</div>

image.png


到这里基本就完成了,还差一个鼠标经过表格十字高亮的需求


我们可以给每个单元格加上鼠标的移入移出事件,移入事件函数传两个参数,一个就是行一个就是列,行可以从一开始的tableData那里拿到,列就是遍历31长度的当前项;这样子就可以拿到当前单元格的坐标,再封装一个辅助函数进行判断是否为当前单元格所在的行所在的列就可以了
高亮的时候记住判断的样式需要在之前的有数据高亮的样式的后面,这样子就不会被覆盖,可以保证有数据高亮的样式会一直存在,哪怕鼠标经过也不会被覆盖!


// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()

const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
//鼠标移入
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)

highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}

// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"

@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>
<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>

<span v-else>0</span>
</div>

最终的效果就是:


soogif.gif


以下就是完整的代码:


<script setup lang="ts">
import { onMounted, ref } from 'vue'

const tableFileds = Array.from({ length: 31 }, (_, i) =>
String(i + 1).padStart(2, '0')
)
const tableDataList = [
'一月',
'二月',
'三月',
'四月',
'五月',
'六月',
'七月',
'八月',
'九月',
'十月',
'十一月',
'十二月'
]

const parseData = (data: any) => {
const parsedData = new Array(12).fill({}) // 初始化一个包含12个空对象的数组
console.log('parsedData', parsedData)

for (let month = 1; month <= 12; month++) {
// 确保每次循环创建一个新的空对象
parsedData[month - 1] = {} // 从0开始索引
if (data[month]) {
Object.entries(data[month]).forEach(([day, value]) => {
parsedData[month - 1][parseInt(day)] = value
})
}
}
return parsedData
}
const data = {
'3': {
'9': 1
},
'4': {
'12': 2
},
'5': {
'11': 1,
'12': 2,
'21': 1
},
'6': {
'6': 5,
'8': 1,
'9': 2,
'10': 1,
'12': 2,
'17': 1,
'20': 1
},
'7': {
'1': 8,
'4': 1,
'7': 1,
'6': 1,
'13': 1,
'22': 1,
'25': 1,
'26': 1,
'27': 1,
'29': 6,
'30': 1
},
'8': {
'1': 1,
'2': 2,
'7': 1,
'20': 1,
'24': 1,
'27': 1,
'31': 1
},
'9': {
'15': 1,
'17': 9,
'21': 2
},
'10': {
'23': 1
}
}
const tableData = ref<any[]>([])
// 表格十字高亮
const highlightedRow = ref<any>()
const highlightedColumn = ref<any>()

const isCurrentCellHighlighted = (rowIndex: number, columnIndex: number) => {
return (
(highlightedRow.value !== null && highlightedRow.value === rowIndex) ||
(highlightedColumn.value !== null &&
highlightedColumn.value === columnIndex)
)
}
const onCellMouseOver = (rowIndex: any, columnIndex: any) => {
console.log('坐标', rowIndex, columnIndex)

highlightedRow.value = rowIndex
highlightedColumn.value = columnIndex
}

// 在鼠标移出事件(onCellMouseLeave)触发时,恢复所有单元格的原始背景色
const onCellMouseLeave = () => {
highlightedRow.value = null
highlightedColumn.value = null
}
onMounted(() => {
tableData.value = parseData(data)
console.log('tableData.value', tableData.value)
})
</script>
<template>
<div class="table">
<div class="tabble-box">
<div class="table-node">
<div class="table-item table-header">
<div class="table-item-content diagonal-cell">
<span class="top-content"></span>
<span class="bottom-content"></span>
</div>
<div
class="table-item-content"
v-for="(item, _index) in tableFileds"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ item }}
</div>
</div>
<div class="table-list">
<div
class="table-item"
v-for="(item, rowIndex) in tableData"
:key="item"
>

<div
class="table-item-content"
:key="item"
:style="{
background: '#EFF5FF'
}"

>

{{ tableDataList[rowIndex] }}
</div>
<div
class="table-item-content"
:data-col-index="index"
v-for="index in 31"
:key="rowIndex"
:style="{
background: item[index]
? '#6fa7ea'
: isCurrentCellHighlighted(rowIndex + 1, index)
? '#EFF5FF'
: ''
}"

@mouseover="onCellMouseOver(rowIndex + 1, index)"
@mouseout="onCellMouseLeave"
>

<span v-if="item[index]" style="color: #fff">
{{ item[index] }}
</span>
<span v-else>0</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>


<style lang="less" scoped>
.table {
flex: 1;
display: flex;
flex-direction: column;
background-color: #fff;
padding: 1.8519vh 0.83vw 2.1296vh 1.09vw;
.tabble-box {
display: flex;
flex: 1;
box-sizing: border-box;
overflow: hidden;
color: #666666;
.table-node {
display: flex;
flex-direction: column;
flex: 1;

.table-header {
.table-item-content {
background-color: #eff5ff;
}
.diagonal-cell {
position: relative; /* 使伪元素相对于此单元格定位 */
padding-bottom: 8px; /* 为对角线下方留出空间以显示内容 */
width: 4.64vw !important;
&::before {
content: ''; /* 必须有内容才能显示伪元素 */
position: absolute;
top: 0;
left: 1px;
width: 5.16vw;
right: 0;
height: 1px; /* 对角线的高度 */
background-color: #e8e8e8; /* 对角线的颜色,可自定义 */
transform-origin: top left;
transform: rotate(30.5deg); /* 斜切角度,可微调 */
}
.top-content {
position: absolute;
top: 0.2778vh;
left: 2.67vw;
font-size: 0.83vw;
}
.bottom-content {
position: absolute;
top: 2.2222vh;
left: 0.83vw;
font-size: 0.83vw;
}
}
}
.table-item {
display: flex;
.table-item-content:first-child {
width: 4.64vw;
padding-top: 1.9444vh;
padding-bottom: 1.2037vh;
}
.table-item-content {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
// width: calc((100% - 9.53vw) / 15);
padding: 0.1vw;
text-align: center;
font-size: 0.78vw;
border-top: 0.05vw solid #eeeeee;
border-right: 0.05vw solid #eeeeee;
width: 2.48vw;
// flex-grow: 1
}
}

.table-header {
.table-item-content {
padding-top: 1.9444vh;
padding-bottom: 1.5741vh;
}
}
}
}
}
</style>



如果对你有帮助的话,欢迎点赞留言收藏🌹


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

贼好用!五分钟搭建一个美观且易用的导航页面!

web
大家好,我是 Java陈序员。 今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站! 关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。 项目简介 Pintree 是一...
继续阅读 »

大家好,我是 Java陈序员


今天,给大家介绍一个贼好用的导航网站搭建工具,只需通过几步操作,就能搭建出个性化导航网站!



关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



项目简介


Pintree 是一个开源项目,旨在将浏览器书签导出成导航网站。通过简单的几步操作,就可以将书签转换成一个美观且易用的导航页面。



Pintree 支持使用 GitHub Pages 进行部署,无需购买服务器、域名等资源!


因此,只要有一个 Github 账号,就能快速搭建一个导航网站。接下来我们就来部署实现下!


项目部署


步骤一:Fork 项目


1、访问 pintree 项目地址


https://github.com/Pintree-io/pintree

2、Fork 项目到自己的仓库中


步骤二:启用 Github Pages


1、打开 GitHub 账号中 Forkpintree 项目


2、切换到仓库的 Settings 标签页,点击 Pages,在 Source 下拉菜单中,选择 gh-pages 分支,然后点击 Save



3、几分钟后,静态导航网站将会在 https://yourusername.github.io/pintree 上可用



yourusername 是你的 Github 账号,如 https://chenyl8848.github.io/pintree.




这样,一个美观且易用的导航网站就搭建好了!


这时,好奇的小明就会问,要怎么个性化修改配置网站内容呢?别急,继续看步骤三。


步骤三:替换 JSON 文件自定义导航内容


1、pintree 渲染的导航网站内容是基于 json/pintree.json 文件里面的配置信息,我们可以通过修改 pintree.json 文件来自定义导航网站内容



2、打开 pintree.json 文件,并点击修改按钮进入编辑模式



3、在修改前,我们需要先了解下具体的语法规则,一个最小化的规则配置如下:


[
{
"//": "folder 表示是一个文件夹,可以配置子模块信息",
"type": "folder",
"//": "添加的时间信息",
"addDate": 1718526477999,
"//": "标题",
"title": "Java 陈序员",
"//": "子模块",
"children": [
{
"//": "link 表示是一个网站链接,最小化的配置单元",
"type": "link",
"//": "添加的时间信息",
"addDate": 1718526687700,
"//": "网站标题",
"title": "个人博客网站",
"//": "网站图标",
"icon": "https://chencoding.top:8090/_media/logo.png",
"//": "网站地址",
"url": "https://chencoding.top/"
},
"//": "依此类推",
{
"type": "folder",
"addDate": 1718526865665,
"title": "编程网站",
"children": [
{
"type": "link",
"addDate": 1718526707006,
"title": "CSDN",
"icon": "https://img-home.csdnimg.cn/images/20201124032511.png",
"url": "https://www.csdn.net/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "掘金",
"icon": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg",
"url": "https://juejin.cn/"
},
{
"type": "link",
"addDate": 1718526707006,
"title": "博客园",
"icon": "https://www.cnblogs.com/images/logo.svg?v=2SMrXdIvlZwVoB1akyXm38WIKuTHVqvGD0CweV-B6cY",
"url": "https://www.cnblogs.com/"
}
]
}
]
}
]

4、文件修改完后,点击 Commit changes 保存



5、过几分钟后,再访问 https://yourusername.github.io/pintree



可以看到,网站的内容变成了个性化的配置信息了。



由于浏览器有缓存的原因,如一开始没有变化,可以使用无痕模式访问或者用其他浏览器访问。



浏览器书签导航


通过前面的内容,我们知道 pintree 只需要一个 JSON 文件,就能搭建出一个导航网站。因此我们可以将浏览器中收藏的书签导出成 JSON 文件,再生成一个静态导航网站!


步骤一:导出浏览器书签


1、安装 Pintree Bookmarks Exporter 插件


安装地址:https://chromewebstore.google.com/detail/pintree-bookmarks-exporte/mjcglnkikjidokobpfdcdmcnfdicojce


2、使用插件导出浏览器书签,并保存 JSON 文件到本地



步骤二:替换 JSON 文件


JSON 文件替换到 Fork 项目的 json/pintree.json 文件中,保存成功后过几分钟再访问。


pintree 通过简单的配置,只需要几分钟就能快速搭建出一个导航网站,而且不用提供服务器、域名等资源,是一个非常优秀的开源项目!如果你想搭建一个静态导航网站可以去试试哈。


项目地址:https://github.com/Pintree-io/pintree

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/



大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!





作者:Java陈序员
来源:juejin.cn/post/7413187186132631589
收起阅读 »

flex 布局中更巧妙的布局方案!比 justify-content 和 align-items 好用多了!

web
在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-content 和 align-items 这两个属性来解决这个问题。 然而,还有一种更加简洁、灵活的方式——使用 margi...
继续阅读 »

在前端开发中,实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及,开发者们开始更多地使用 justify-contentalign-items 这两个属性来解决这个问题。




然而,还有一种更加简洁、灵活的方式——使用 margin: auto; 来实现居中以及更多实际场景下的特定效果。让我们一起回顾一下常见方式:justify-contentalign-items,然后再来探讨一下使用:margin 的优势,以及如何在实际项目中使用它。





一、常见方式:justify-contentalign-items


1.1 justify-content (用于水平对齐)


justify-content 决定主轴(通常是水平方向)上子元素如何分配空间。常见的取值有:



  • flex-start:元素排列在容器的起始位置(默认值)。

  • flex-end:元素排列在容器的末尾。

  • center:元素在容器内水平居中。

  • space-between:第一个元素与容器起点对齐,最后一个元素与容器终点对齐,其他元素之间均匀分布空间。

  • space-around:每个元素左右两侧都分配均等的空白区域(元素两边的空隙会有一半分布在两端)。

  • space-evenly:所有元素之间、以及与容器两端的空隙都相等。


1.2 align-items(用于垂直对齐)


align-items 决定交叉轴(通常是垂直方向)上子元素如何对齐。常见的取值有:



  • stretch:子元素在交叉轴上填满整个容器高度(默认值,前提是子元素没有设置具体的高度)。

  • flex-start:子元素在交叉轴的起始位置对齐。

  • flex-end:子元素在交叉轴的末端对齐。

  • center:子元素在交叉轴上垂直居中对齐。

  • baseline:子元素以其文本基线对齐。


1.3 flexbox 的常见用法


下面给出一些常见的 flexbox 的使用案例:


示例 : 公共样式


.container {
width: 800px;
height: 200px;
margin: 50px auto;
display: flex;
border: 1px solid black;
padding: 10px;
box-sizing: border-box;
}

.box {
width: 50px;
height: 50px;
background-color: lightblue;
text-align: center;
line-height: 50px;
border: 1px solid #333;
}

示例 1: 水平居中 + 垂直居中


<div class="container example-1">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-1 {
justify-content: center;
align-items: center;
}

image.png



如上图所示,元素在水平和垂直方向都居中了。



示例 2: 水平居中 + 垂直靠顶


<div class="container example-2">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-2 {
justify-content: center;
align-items: flex-start;
}

image.png



如上图所示,justify-content: center; 使元素在水平方向居中;align-items: flex-start; 使元素垂直方向靠近顶部。



示例 3: 水平等间距 + 垂直居中


<div class="container example-3">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-3 {
justify-content: space-between;
align-items: center;
}

image.png



如上图所示,justify-content: space-between; 使元素在垂直方向居中;align-items: center; 使元素在水平方向两端对齐。



示例 4: 水平左对齐 + 垂直底部对齐


<div class="container example-4">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-4 {
justify-content: flex-start;
align-items: flex-end;
}

image.png



如上图所示,justify-content: flex-start; 使元素在水平方向居左;align-items: flex-end; 使元素在垂直方向靠底。



示例 5: 水平等间距 + 垂直拉伸


<div class="container example-5">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
</div>

.example-5 {
height: auto;
justify-content: space-evenly;
align-items: stretch;
}

image.png



如上图所示,justify-content: space-evenly; 会使元素会在水平方向等间距;如果不设置元素的高度,使其自适应,align-items: stretch; 会使其垂直方向拉伸铺满。



1.4 思考与延伸


但你有没有想过,这些写法是否是最简洁的?能否实现我们日常开发的需求呢?有没有更优雅、更轻量的方案呢?


实际上在很多情况下这两个属性并不能够满足我们的开发需求。


比如我需要实现子元素部分集中的布局:



单纯依靠 justify-contentalign-items,很难让几个子元素集中在一起。比如我们希望某些元素靠近并且与其他元素保持一定的间距就会比较麻烦了。


此时为了实现这种布局,通常需要结合 flex-growmargin 或者 space-between,甚至需要使用嵌套的 flex 布局,增加了复杂性。



image.png


又或者是等宽子项的平均分布问题:



比如在导航菜单或展示商品卡片时,可能要求子项无论数量多少,都要从左向右均匀分布,并且保持等宽。


通过 justify-content: space-betweenspace-around 可以部分解决这个问题,但是往往会出现无法保证元素从左向右,或者是无法等分的问题。



image.png


以及一些其他的情况,如垂直排列的固定间距复杂的网格布局混合布局等,justify-contentalign-items都无法简洁、优雅的解决问题。




二、更优雅的方式:margin


2.1 下使用 margin: auto 使元素居中


其实,Flexbox 布局下还有另一种更加简洁的方法使元素居中——直接使用 margin: auto;。你可能会问,这怎么能居中呢?让我们先看一个例子:


<div class="box">
<div class="item"></div>
</div>

.box {
width: 200px;
height: 100px;
border: 2px solid #ccc;
display: flex; /* 启用 Flex 布局 */
margin: 100px auto;
}

.item {
background: red;
width: 50px;
height: 50px;
margin: auto; /* 自动分配外边距 */
}

image.png


在这个例子中,我们没有使用 justify-contentalign-items,仅通过设置 .item 元素的 margin: auto;,就实现了水平和垂直居中。



它的工作原理是:在 Flexbox 布局中,margin: auto;根据父容器的剩余空间自动调整元素的外边距,直到子元素居中。



在传统布局中,margin: auto; 主要用于水平居中对齐,不适用于垂直居中。因为普通流布局的垂直方向是由文档流控制的,不支持类似 Flexbox 中的自动调整行为。


.container {
width: 500px;
}

.element {
width: 200px;
margin: 0 auto; /* 左右外边距自动分配,实现水平居中 */
}

相比之下,在 Flexbox 布局中,margin: auto; 具有更多的灵活性,可以同时实现水平和垂直居中对齐。


它不仅可以处理水平居中,还可以在 Flexbox 布局下根据剩余空间自动调整外边距,实现完全的居中对齐。


2.2 实现更多实际开发中的布局


示例 1:实现子元素部分集中



在实际开发中,我们常遇到这样一种需求:将元素水平分布在容器内,其中某些元素需要靠近在一起,与其他元素保持一定的自适应距离。


在这种情况下使用 justify-content: space-between 是一种常见的办法,但这种方法也有一定的局限性:每个元素之间平等分配剩余空间,无法实现特定元素之间紧密靠拢。



image.png


代码实现:


<div class="container c2">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>

* {
margin: 0;
padding: 0;
}

.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
}

.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}

.item:nth-child(odd) {
background: #046f4e;
}

.item:nth-child(even) {
background: #d53b3b;
}

.c2 .item:nth-child(2){
margin: 0 0 0 auto; /* 第二个 item 右对齐 */
}

.c2 .item:nth-child(4){
margin: 0 auto 0 0; /* 第四个 item 左对齐 */
}


在上述代码中,其实除掉一些基本样式的设置,实现了这个布局的关键代码就2行。


具体来说,.c2 .item:nth-child(2)margin: 0 0 0 auto; 使得第二个 .item 紧贴容器的右边缘,而 .c2 .item:nth-child(4)margin: 0 auto 0 0; 使得第四个 .item 紧贴容器的左边缘。这样就使第二个元素的左侧和第四个元素的右侧将会自适应边距间隔。


因此,我们可以使用 margin 巧妙地通过调整子元素的外边距,实现元素的部分集中和对齐布局。



示例 2:实现等宽子项的平均分布


在很多情况下,我们需要将商品卡片或其他内容等宽地分布在每一行中,使每个子项都具有相同的宽度并且平均分布,每一行都是从左到右。


这种布局通常用于网格展示或商品列表等场景,确保每个子项在视觉上统一且整齐。



在这种情况下直接使用 justify-contentalign-items 可能会出现以下问题:



  1. 使用 space-between 时如果最后一行的元素数量不足以填满整行,剩余的元素会分散到两侧,留出较大的空白区域,导致布局不整齐。
    image.png

  2. 使用 space-around 时如果最后一行的元素数量不满,元素会在行中均匀分布,导致它们集中在中间,而不是靠左或对齐其他行。
    image.png

    大家在遇到这些情况时是不是就在考虑换用 grid 布局了呢?先别急,我们其实直接通过 margin 就可以直接实现的!



在这里我们可以使用 margin 的动态计算来实现等宽子项的平均分布


代码实现:


<div class="container c3">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>

* {
margin: 0;
padding: 0;
}

.container {
width: 500px;
background: #eee;
margin: 50px auto;
padding: 10px;
display: flex;
flex-wrap: wrap;
}

.item {
width: 50px;
height: 50px;
border: 1px solid #333;
box-sizing: border-box;
}

.item:nth-child(odd) {
background: #046f4e;
}

.item:nth-child(even) {
background: #d53b3b;
}

.c3 .item {
--n: 5; /* 每行显示的子项数量 */
--item-width: 50px; /* 子项宽度 */
--space: calc(100% / var(--n) - var(--item-width)); /* 计算子项之间的间距 */
--m: calc(var(--space) / 2); /* 左右间距的一半 */
margin: 10px var(--m); /* 动态计算左右的间距 */
}


在在上述代码中,除掉基础的样式,实现了这个布局的关键代码仅仅5行。通过动态计算 margin,我们能够简单而有效地实现等宽子项的平均分布,使布局更加简洁明了。



image.png




三、总结


在前端开发中,实现各种页面布局一直是一个常见的需求。


传统的做法如使用 justify-contentalign-items 属性已经被广泛采用,但这种方法有时可能显得不够简洁或灵活。


在适当的情况下直接使用 margin 进行布局是一种更优雅、简洁的替代方案,可以在 Flexbox 布局中有效地实现居中对齐和一些复杂的布局需求。掌握并运用这种方法,可以提高开发效率,并使布局更加优雅。快来玩起来吧!




作者:空白诗
来源:juejin.cn/post/7413222778855964706
收起阅读 »

告别繁琐的 try-catch:JavaScript 安全赋值运算符 (?= ) 来了!

web
你是否厌倦了代码中难以阅读和维护的冗长 try-catch 代码块?全新的 ECMAScript 安全赋值运算符 (?= ) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?= 运算符如何彻底改变你的编码体验! ...
继续阅读 »

你是否厌倦了代码中难以阅读和维护的冗长 try-catch 代码块?全新的 ECMAScript 安全赋值运算符 (?= ) 将彻底改变游戏规则!这一突破性的特性简化了错误处理,让你的代码更简洁、更高效。让我们深入了解 ?= 运算符如何彻底改变你的编码体验!


简化代码,轻松处理错误


告别嵌套的 try-catch 混乱


问题: 传统的 try-catch 代码块会导致代码深度嵌套,难以理解和调试。


解决方案: 使用 ?= 运算符,你可以将函数结果转换为一个元组,更优雅地处理错误。如果出现错误,你将得到 [error, null] ,如果一切正常,你将得到 [null, result] 。你的代码将会感谢你!


使用 ?= 之前:


async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
try {
const data = await response.json();
return data;
} catch (parseError) {
console.error('Failed to parse JSON:', parseError);
}
} catch (networkError) {
console.error('Network error:', networkError);
}
}

使用 ?= 之后:


async function fetchData() {
const [networkError, response] ?= await fetch("https://api.example.com/data");
if (networkError) return console.error('Network error:', networkError);
const [parseError, data] ?= await response.json();
if (parseError) return console.error('Failed to parse JSON:', parseError);
return data;
}

提升代码清晰度:保持代码线性,简洁易懂


问题: try-catch 代码块会打断代码流程,降低可读性。


解决方案: ?= 运算符使错误处理变得简单直观,保持代码线性,易于理解。


示例:


const [error, result] ?= await performAsyncTask();
if (error) handleError(error);

标准化错误处理:跨 API 保持一致性


问题: 不同的 API 通常需要不同的错误处理方法,导致代码不一致。


解决方案: ?= 运算符提供了一种统一的错误处理方式,使你的代码在各种 API 中保持一致。


提升安全性:每次都捕获所有错误


问题: 漏掉错误会导致 bug 和潜在的安全问题。


解决方案: ?= 运算符确保始终捕获错误,降低漏掉关键问题的风险。


Symbol.result 背后的奥秘


自定义错误处理变得简单


概述: 实现 Symbol.result 方法的对象可以使用 ?= 运算符定义自己的错误处理逻辑。


示例:


function customErrorHandler() {
return {
[Symbol.result]() {
return [new Error("Custom error message"), null];
},
};
}

const [error, result] ?= customErrorHandler();

轻松处理嵌套错误:平滑处理复杂场景


概述: ?= 运算符可以处理包含 Symbol.result 的嵌套对象,使复杂错误场景更容易管理。


示例:


const complexObj = {
[Symbol.result]() {
return [
null,
{ [Symbol.result]: () => [new Error("Nested error"), null] }
];
},
};

const [error, data] ?= complexObj;

与 Promise 和异步函数无缝集成


概述: ?= 运算符专门设计用于与 Promise 和 async/await 无缝协作,简化异步错误处理。


示例:


const [error, data] ?= await fetch("https://api.example.com/data");

使用 using 语句简化资源管理


概述:?= 运算符与 using 语句结合使用,可以更有效地管理资源。


示例:


await using [error, resource] ?= getResource();

优先处理错误:先处理错误,后处理数据


概述: 将错误放在 [error, data] ?= 结构的第一个位置,确保在处理数据之前先处理错误。


示例:


const [error, data] ?= someFunction();

让你的代码面向未来:简化填充


概述: 虽然无法直接填充 ?= 运算符,但你可以使用后处理器在旧环境中模拟其行为。


示例:


const [error, data] = someFunction[Symbol.result]();

汲取灵感:从 Go、Rust 和 Swift 中学习


概述: ?= 运算符借鉴了 Go、Rust 和 Swift 等语言的先进错误处理实践,这些语言以其强大的错误管理功能而闻名。


当前限制和未来方向


仍在发展: ?= 运算符仍在开发中。改进领域包括:



  • 命名: 为实现 Symbol.result 的对象提供更好的术语。

  • finally 代码块: 没有新的 finally 代码块语法,但传统用法仍然有效。


总结


安全赋值运算符 (?= ) 将通过使 JavaScript 错误处理更加直观和简洁来彻底改变 JavaScript 错误处理。随着该提案的不断发展,它将有望成为每个 JavaScript 开发人员工具箱中的必备工具。准备迎接更干净、更安全的代码吧!🚀


作者:前端宝哥
来源:juejin.cn/post/7413284830945493001
收起阅读 »

两个月写完的校园社交小程序,这是篇uniapp踩坑记录

web
人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......前置准备:资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比...
继续阅读 »

人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......

前置准备:

  1. 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
  2. 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
  3. 微信认证。300元,腾子的吃相很难看,但奈何寄人篱下
  4. 小程序备案。在前面流程完成之后才能进行小程序的备案

image.png

审核流程

整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话

  1. 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片

文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取

  1. 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标 image.png
  2. 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时

image.png

image.png 5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天

开发过程

  1. 文件上传。以往网页开发中涉及文件上传的业务都是new FormData,然后再append必要的字段。但是,小程序中使用FormData会报错,所以,得使用uniapp自带的uni.uoloadFile
  2. 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按navigateBackuni.showToast,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容
  3. 分享功能。小程序的分享功能需要在onShareAppMessage(分享至好友)或者onShareTimeline(分享至朋友圈)调用。这两个是和onLoad同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app中导入
  4. 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅

先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅

  1. webSocket。小程序中的树洞评论功能我们选用的是webSocket,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制。但是在做短信重连的时候就发现一个问题:断线重连之后确实是会创建新的实例,心跳包也正常推送给服务端,但是就是接收不到服务端反推回来的东西,后面经过排查,是webSocket实例的onMessage事件应当写在onOpen中,而不是独立写到外面

独立写到外面进行处理就会出现:断线重连之后死活接不到最新的实例返回的消息

这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义

  1. 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验

image.png

image.png

  1. 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面scroll事件。但是,scroll涉及大量的计算;后面采用Intersection Observer。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver,二者语法差不多
  2. 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。

大概暂时先能想到这么多,后面有想到再接着补充......

后记

其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:

  1. 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
  2. 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
  3. ......

然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。

大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见


作者:吃肉不吃皮
来源:juejin.cn/post/7412665439501844490
收起阅读 »

短信接口被爆破了,一晚上差点把公司干破产了

背景 某天夜里,你正睡着觉,与周公神游。 老板打来电话:“小李,快看一下,系统出故障了,一个小时发了200条短信,再搞下去,我要破产了..." 巴拉巴拉... 于是,你赶紧跳下床,查了一个后台日志,发送短信API接口5s发送一次,都已经发送了500条了。在达到...
继续阅读 »

背景


某天夜里,你正睡着觉,与周公神游。


老板打来电话:“小李,快看一下,系统出故障了,一个小时发了200条短信,再搞下去,我要破产了..."


巴拉巴拉...


于是,你赶紧跳下床,查了一个后台日志,发送短信API接口5s发送一次,都已经发送了500条了。在达到每日限额后,自动终止了。很明显被黑客攻击了。



500 * 0.1 * 8 = 400



一晚上约干掉了400元人民币


睡意全无,赶紧起来排查原因


故障分析


我司是做国外业务的,用的短信厂家是RingRing, 没有阿里云那种自带的强悍的预警和封禁功能。黑客通过伪造IP地址手机号然后攻破了APP的短信接口,然后顺藤摸瓜的拿到相关发布的全部应用。于是,一个晚上,单个APP的每日短信限额和全部短信限额都攻破了。



APP使用的是https双向加密,黑客也不是单纯的爆破,没有大量的验证码错误日志。我们现在都不清楚黑客是通过什么方式绕过我们系统的,或者直接攻破了验证码


可能有懂这方面的掘友,可以分享一下哈



我们先上了一个临时方案,如果10分钟内,发送短信超过30条,且手机号超过60%都是同一个国家,我们关闭短信发送功能10分钟,并推送告警


然后抓紧时间去升级验证码,提高安全标准


验证码


文字验证码



我司最开始用的就是这种,简单易用。但是任你把噪点和线条铺满,整的面目全非,都防不住机器的识别,这种验证码直接pass了


优点:简易,具有一定的防爆破功能


缺点:防君子不防小人,在黑客面前,GG


滑块验证码


image.png


我司对于滑块验证码有几点考虑:



  1. 安全有待商榷,

  2. 背景图片需要符合国外市场和审美,需要UI介入,增加人工成本

  3. 不确定是否符合国外的习惯


基于这几点考虑,我司放弃了这个方案。但平心而论,国内用滑块验证码的是最多的,原因如下:



  1. 用户体验好

  2. 防破解性更强

  3. 适应移动设备

  4. 适用性广


npm install rc-slider-captcha

import SliderCaptcha from 'rc-slider-captcha';

const Demo = () => {
return (
<SliderCaptcha
request={async () => {
return {
bgUrl: 'background image url',
puzzleUrl: 'puzzle image url'
};
}}
onVerify={async (data) => {
console.log(data);
// verify data
return Promise.resolve();
}}
/>
);
};


滑块验证码是用的最多的验证码,操作简单,基本平替了图片验证码



图形顺序验证码 & 图形匹配验证码 & 语顺验证码






我司没有采用这种方案的原因如下:



  1. 我们的APP是多语言,点击文字这种方案不适用

  2. 没有找到免费且合适的APP插件

  3. 时间紧,项目紧急,没有功夫就研究


总结:



安全性更强,用户量越大的网站越受青睐


难度相对更大,频繁验证会流失一些用户



reCAPTCHA v3


综上,我司使用了reCAPTCHA


image.png


理由如下:



  1. 集成简单

  2. 自带控制台,方便管理和查看

  3. 谷歌出品,值得信赖,且有保障


<script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>

<script>
function onClick(e) {
e.preventDefault();
grecaptcha.ready(function() {
grecaptcha.execute('reCAPTCHA_site_key', {action: 'submit'}).then(function(token) {
// Add your logic to submit to your backend server here.
});
});
}
</script>


// 返回值
{
score: 1 // 评分0 到 1。1:确认为人类,0:确认为机器人。
hostname: "localhost"
success: true,
challenge_ts: "2024-xx-xTxx:xx:xxZ"
action: "homepage"
}

紧急上线后,安全性大大增强再也没有遭受黑客袭击了。本以为可以睡个安稳觉了,又有其他的问题了,听我细讲


根据官方文档,建议score取0.5, 我们根据测试的情况,降低了标准,设置为0.3。上线后,很多用户投诉安全度过低,请30分后重试。由于我们当时的业务是出行和游乐, APP受限后,用户生活受到了很大限制,很多用户预约了我们的产品,却用不了,导致收到了大量的投诉。更糟糕的时候,我们的评分标准0.3是写死的,只能重新发布,一来二去,3天过去了。客服被用户骂了后,天天来我们技术部骂我们。哎,想想都是泪


我们紧急发布了一版,将评分标准设置成可配置的,通过API获取, 暂定0.1。算是勉强度过了这一关


reCAPTCHA v2


把分数调整到0.1后,我们觉得不是很安全,有爆破的风险,于是在下个版本使用了v2


image.png



使用v2,一切相对平稳,APP短信验证码风波也算平安度过了



2FA


双因素验证(Two-factor authentication,简称2FA,又名二步验证、双重验证),是保证账户安全的一道有效防线。在登录或进行敏感操作时,需要输入验证器上的动态密码(类似于银行U盾),进一步保护您的帐户免受潜在攻击者的攻击。双因素验证的动态密码生成器分为软件和硬件两种,最常用的软件有OTP Auth和谷歌验证器 (Google Authenticator)






市场调用,客户要求,后续的APP,我们的都采用2fa方案,一人一码,安全可靠


实现起来也比较简单,后端使用sha1加密一串密钥,生成哈希值,用户扫码绑定,然后每次将这个验证码提交给服务器进行比对即可



每次使用都要看一下验证码,感觉有点烦


服务器和手机进行绑定,是同一把密钥,每次输入都找半天。一旦用户更换手机,就必须生成全新的密钥。



总结


参考资料



作者:高志小鹏鹏
来源:juejin.cn/post/7413322738315378697
收起阅读 »

利用CSS延迟动画,打造令人惊艳的复杂动画效果!

web
动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。 绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯CSS一样可以实...
继续阅读 »

动画在前端开发中是经常遇到的场景之一,加入动画后页面可以极大的提升用户体验。




绝大多数简单的动画场景可以直接通过CSS实现,对于一些特殊场景的动画可能会使用到JS计算实现,通过本文的学习,可以让你在一些看似需要使用JS实现的动画场景,使用纯CSS一样可以实现,并且更方便快捷。



先看一个简单的例子:一个方块的位置随着滑条滑动的位置改变
在这里插入图片描述


这个场景实现起来很简单,滑条值改变后,使用JS计算方块应该移动的距离,然后将方块定位到指定位置即可。代码如下:


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
}
<div class="box">div>
<input type="range" min="0" max="1" step="0.01"/>



现在稍微增加一些动画效果:



  • 方块在中间位置时缩放为原来的一半大小

  • 方块在中间位置时变成球形

  • 方块从红色变为绿色


在这里插入图片描述


对于大小和圆角,同样可以使用简单的JS进行计算实现,但是对于颜色变化,使用JS计算将会是一个非常复杂的过程。


先抛开动画跟随滑条运动这个要求,如果使用CSS实现上面从0-1的动画过程是一个很简单的事:
在这里插入图片描述


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s linear forwards;
}
@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}

利用CSS动画帮我们可以很轻松的计算出每个时间点时的状态,现在的问题就变成如何让动画停留在指定的时间点,这就需要使用到动画的两个属性:


annimation-play-state:设置动画是运行还是暂停,有两个属性值runing、paused
annimation-delay:设置动画开始时间的偏移量,如果是正值,则动画会延迟开始;如果是负值(-d),动画会立即开始,开始位置在动画(d)s时所处的位置。


有了这两个属性,现在将上面的动画停留在50%的位置
在这里插入图片描述


假设整个动画过程需要1s,50%的位置则需要将延迟值设置为-0.5s,这样动画就会停留在0.5s的位置。


.box {
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s -0.5s linear forwards infinite paused;
}

接下来只需要将滑条的值与动画延迟的值关联起来即可,这里可以通过CSS变量来实现:


.box {
--duration: -0.5s; // 定义延迟变量
height: 50px;
width: 50px;
background-color: aquamarine;
transform: translateX(0);
animation: run 1s var(--duration) linear forwards infinite paused;
}

@keyframes run {
0% {
transform: translateX(0) scale(1);
border-radius: 0%;
background: red;
}
50% {
transform: translateX(100px) scale(.5);
border-radius: 50%;
}
100% {
transform: translateX(200px) scale(1);
border-radius: 0%;
background: green;
}
}



应用场景



利用CSS延迟动画可以轻松实现很多交互场景,例如:跟随鼠标滚动界面发生反馈动画、根据当天时间界面从日出到日落、根据不同分值出现不同表情变化等等。
在这里插入图片描述




作者:前端筱园
来源:juejin.cn/post/7363094767557378099
收起阅读 »

实现 height: auto 的高度过渡动画

web
对于一个 height 设置为 auto 的元素,当它的高度发生了不由样式引起的改变时,并不会触发 transition 过渡动画。 容器元素的高度往往是由其内容决定的,如果一个容器元素的内容高度突然发生了改变,而无法进行过渡动画,有时会显得比较生硬,比如下面...
继续阅读 »

对于一个 height 设置为 auto 的元素,当它的高度发生了不由样式引起的改变时,并不会触发 transition 过渡动画。


容器元素的高度往往是由其内容决定的,如果一个容器元素的内容高度突然发生了改变,而无法进行过渡动画,有时会显得比较生硬,比如下面的登录框组件:


001.gif


那么这种非样式引起的变化如何实现过渡效果呢?可以借助 FLIP 技术。


FLIP 是什么


FLIPFirstLastInvertPlay 的缩写,其含义是:



  • First - 获取元素变化之前的状态

  • Last - 获取元素变化后的最终状态

  • Invert - 将元素从 Last 状态反转到 First 状态,比如通过添加 transform 属性,使得元素变化后,看起来仍像是处于 First 状态一样

  • Play - 此时添加过渡动画,再移除 Invert 效果(取消 transform),动画就会开始生效,使得元素看起来从 First 过渡到了 Last


需要用到的 Web API


要实现一个基本的 FLIP 过渡动画,需要使用到以下一些 Web API



基本过渡效果实现


使用以上 API,就可以初步实现一个监听元素尺寸变化,并对其应用 FLIP 动画的函数 useBoxTransition,代码如下:


/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @returns 返回一个函数,调用后取消对过渡元素尺寸变化的监听
*/

export default function useBoxTransition(el: HTMLElement, duration: number) {
// boxSize 用于记录元素处于 First 状态时的尺寸大小
let boxSize: {
width: number
height: number
} | null = null

const elStyle = el.style // el 的 CSSStyleDeclaration 对象

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作

// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize

// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
// box 首次被观察时会触发一次回调,此时 boxSize 为 null,scale 应为 1
const scaleX = boxSize ? boxSize.width / width : 1
const scaleY = boxSize ? boxSize.height / height : 1
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
elStyle.setProperty('transform', `scale(${scaleX}, ${scaleY})`)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', 'none')
elStyle.setProperty('transition', `transform ${duration}ms`)
})
// 记录变化后的 boxSize
boxSize = { width, height }
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
}
return cancelBoxTransition
}

效果如下所示:


002.gif


效果改进


目前已经实现了初步的过渡效果,但在一些场景下会有些瑕疵:



  • 如果在过渡动画完成前,元素有了新的状态变化,则动画被打断,无法平滑过渡到新的状态

  • FLIP 动画过渡过程中,实际上发生变化的是 transform 属性,并不影响元素在文档流中占据的位置,如果需要该元素影响周围的元素,那么周围元素无法实现平滑过渡


如下所示:


003.gif


对于动画打断问题的优化思路



  • 使用 Window.requestAnimationFrame() 方法在每一帧中获取元素的尺寸

  • 这样做可以实时地获取到元素的尺寸,实时地更新 First 状态


对于元素在文档流中问题的优化思路



  • 应用过渡的元素外可以套一个 .outer 元素,其定位为 relative,过渡元素的定位为 absolute,且居中于 .outer 元素

  • 当过渡元素尺寸发生变化时,通过 resizeObserver 获取其最终的尺寸,将其宽高设置给 .outer 元素(实例代码运行于 Vue 3 中,因此使用的是 Vue 提供的 ref api 将其宽高暴露出来,可以方便地监听其变化;如果在 React 中则可以将设置 .outer 元素宽高的方法作为参数传入 useBoxTransition 中,在需要的时候调用),并给 .outer 元素设置宽高的过渡效果,使其在文档流中所占的位置与过渡元素的尺寸同步

  • 但是也要注意,这样做可能会引起浏览器高频率的重排,在复杂布局中慎用!


改进后的useBoxTransition 函数如下:


import throttle from 'lodash/throttle'
import { ref } from 'vue'

type BoxSize = {
width: number
height: number
}
type BoxSizeRef = globalThis.Ref<BoxSize>

/**
*
* @param {HTMLElement} el 要实现过渡的元素 DOM
* @param {number} duration 过渡动画持续时间,单位 ms
* @param {string} mode 过渡动画缓动速率,同 CSS transition-timing-function 可选值
* @returns 返回一个有两个项的元组:第一项为 keyBoxSizeRef,当元素大小发生变化时会将变化后的目标尺寸发送给 keyBoxSizeRef.value;第二项为一个函数,调用后取消对过渡元素尺寸变化的监听
*/

export default function useBoxTransition(
el: HTMLElement,
duration: number,
mode?: string
) {
let boxSizeList: BoxSize[] = [] // boxSizeList 表示对 box 的尺寸的记录数组;为什么是使用列表:因为当 box 尺寸变化的一瞬间,box 的 transform 效果无法及时移除,此时 box 的尺寸可能是非预期的,因此使用列表来记录 box 的尺寸,在必要的时候尽可能地将非预期的尺寸移除
const keyBoxSizeRef: BoxSizeRef = ref({ width: 0, height: 0 }) // keyBoxSizeRef 是暴露出去的 box 的实时目标尺寸
let isObserved = false // box 是否已经开始被观察
let frameId = 0 // 当前 animationFrame 的 id
let isTransforming = false // 当前是否处于变形过渡中

const elStyle = el.style // el 的 CSSStyleDeclaration 对象
const elComputedStyle = getComputedStyle(el) // el 的只读动态 CSSStyleDeclaration 对象

// 获取当前 boxSize 的函数
function getBoxSize() {
const rect = el.getBoundingClientRect() // el 的 DOMRect 对象
return { width: rect.width, height: rect.height }
}

// 更新 boxSizeList
function updateBoxsize(boxSize: BoxSize) {
boxSizeList.push(boxSize)
// 只保留前最新的 4 条记录
boxSizeList = boxSizeList.slice(-4)
}

// 定义 animationFrame 的回调函数,使得当 box 变形时可以更新 boxSize 记录
const animationFrameCallback = throttle(() => {
// 为避免使用了函数节流后,导致回调函数延迟触发使得 cancelAnimationFrame 失败,因此使用 isTransforming 变量控制回调函数中的操作是否执行
if (isTransforming) {
const boxSize = getBoxSize()
updateBoxsize(boxSize)
frameId = requestAnimationFrame(animationFrameCallback)
}
}, 20)

// 过渡事件的回调函数,在过渡过程中实时更新 boxSize
function onTransitionStart(e: Event) {
if (e.target !== el) return
// 变形中断的一瞬间,boxSize 的尺寸可能是非预期的,因此在变形开始时,将最新的 3 个可能是非预期的 boxSize 移除
if (boxSizeList.length > 1) {
boxSizeList = boxSizeList.slice(0,1)
}
isTransforming = true
frameId = requestAnimationFrame(animationFrameCallback)
// console.log('过渡开始')
}
function onTransitionCancel(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡中断')
}
function onTransitionEnd(e: Event) {
if (e.target !== el) return
isTransforming = false
cancelAnimationFrame(frameId)
// console.log('过渡完成')
}

el.addEventListener('transitionstart', onTransitionStart)
el.addEventListener('transitioncancel', onTransitionCancel)
el.addEventListener('transitionend', onTransitionEnd)

const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 被观察的 box 发生尺寸变化时要进行的操作

// 获取当前回调调用时,box 的宽高
const borderBoxSize = entry.borderBoxSize[0]
const writtingMode = elStyle.getPropertyValue('writing-mode')
const isHorizontal =
writtingMode === 'vertical-rl' ||
writtingMode === 'vertical-lr' ||
writtingMode === 'sideways-rl' ||
writtingMode === 'sideways-lr'
? false
: true
const width = isHorizontal
? borderBoxSize.inlineSize
: borderBoxSize.blockSize
const height = isHorizontal
? borderBoxSize.blockSize
: borderBoxSize.inlineSize

const boxSize = { width, height }

// 当 box 尺寸发生变化时以及初次触发回调时,将此刻 box 的目标尺寸暴露给 keyBoxSizeRef
keyBoxSizeRef.value = boxSize

// box 首次被观察时会触发一次回调,此时不需要应用过渡,只需将当前尺寸记录到 boxSizeList 中
if (!isObserved) {
isObserved = true
boxSizeList.push(boxSize)
return
}

// 当 box 尺寸发生变化时,使用 FLIP 动画技术产生过渡动画,使用过渡效果的是 scale 形变
// 根据 First 和 Last 计算出 Inverse 所需的 scale 大小
const scaleX = boxSizeList[0].width / width
const scaleY = boxSizeList[0].height / height
// 尺寸发生变化的瞬间,要使用 scale 变形将其保持变化前的尺寸,要先将 transition 去除
elStyle.setProperty('transition', 'none')
const originalTransform =
elStyle.transform || elComputedStyle.getPropertyValue('--transform')
elStyle.setProperty(
'transform',
`${originalTransform} scale(${scaleX}, ${scaleY})`
)
// 将 scale 移除,并应用 transition 以实现过渡效果
setTimeout(() => {
elStyle.setProperty('transform', originalTransform)
elStyle.setProperty('transition', `transform ${duration}ms ${mode}`)
})
}
})
resizeObserver.observe(el)
const cancelBoxTransition = () => {
resizeObserver.unobserve(el)
cancelAnimationFrame(frameId)
}
const result: [BoxSizeRef, () => void] = [keyBoxSizeRef, cancelBoxTransition]
return result
}


相应的 vue 组件代码如下:


<template>
<div class="outer" ref="outerRef">
<div class="card-container" ref="cardRef">
<div class="card-content">
<slot></slot>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import useBoxTransition from '@/utils/useBoxTransition'

type Props = {
transition?: boolean
duration?: number
mode?: string
}
const props = defineProps<Props>()

const { transition, duration = 200, mode = 'ease' } = props

const cardRef = ref<HTMLElement | null>(null)
const outerRef = ref<HTMLElement | null>(null)
let cancelBoxTransition = () => {} // 取消 boxTransition 效果

onMounted(() => {
if (cardRef.value) {
const cardEl = cardRef.value as HTMLElement
const outerEl = outerRef.value as HTMLElement
if (transition) {
const boxTransition = useBoxTransition(cardEl, duration, mode)
const keyBoxSizeRef = boxTransition[0]
cancelBoxTransition = boxTransition[1]
outerEl.style.setProperty(
'--transition',
`weight ${duration}ms ${mode}, height ${duration}ms ${mode}`
)
watch(keyBoxSizeRef, () => {
outerEl.style.setProperty('--height', keyBoxSizeRef.value.height + 'px')
outerEl.style.setProperty('--width', keyBoxSizeRef.value.width + 'px')
})
}
}
})
onUnmounted(() => {
cancelBoxTransition()
})
</script>

<style scoped lang="less">
.outer {
position: relative;
&::before {
content: '';
display: block;
width: var(--width);
height: var(--height);
transition: var(--transition);
}

.card-container {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
--transform: translate(-50%, -50%);
transform: var(--transform);
box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.7);
border-radius: var(--border-radius, 20px);
overflow: hidden;
backdrop-filter: blur(10px);
padding: 30px;
box-shadow: var(--box-shadow, 0 0 15px 0 rgba(0, 0, 0, 0.3));
}
}
</style>

优化后的效果如下:


004.gif


005.gif


注意点


过渡元素本身的 transform 样式属性


useBoxTransition 函数中会覆盖应用过渡的元素的 transform 属性,如果需要额外为元素设置其它的 transform 效果,需要使用 css 变量 --transform 设置,或使用内联样式设置。


这是因为,useBoxTransition 函数中对另外设置的 transform 效果和过渡所需的 transform 效果做了合并。


然而通过 getComputedStyle(Element) 读取到的 transform 的属性值总是会被转化为 matrix() 的形式,使得 transform 属性值无法正常合并;而 CSS 变量和使用 Element.style 获取到的内联样式中 transform 的值是原始的,可以正常合并。


如何选择获取元素宽高的方式


Element.getBoundingClientRect() 获取到的 DOMRect 的宽高包含了 transform 变化,而 Element.offsetWidth / Element.offsetHeight 以及 ResizeObserverEntry 对象获取到的宽高是元素本身的占位大小。


因此在需要获取 transition 过程中,包含 transform 效果的元素大小时,使用 Element.getBoundingClientRect(),否则可以使用 Element.offsetWidth / Element.offsetHeightResizeObserverEntry 对象。


获取元素高度时遇到的 bug


测试案例中使用了 elementPlus UI 库的 el-tabs 组件,当元素包含该组件时,无论是使用 Element.getBoundingClientRect()Element.offsetHeight 还是使用 Element.StylegetComputedStyle(Element) 获取到的元素高度均缺少了 40px;而使用 ResizeObserverEntry 对象获取到的高度则是正确的,但是它无法脱离 ResizeObserver API 独立使用。


经过测试验证,缺少的 40px 高度来自于 el-tabs 组件中 .el-tabs__header 元素的高度,也就是说,在获取元素高度时,将 .el-tabs__header 元素的高度忽略了。


测试后找出的解决方法是,手动将 .el-tabs__header 元素样式(注意不要写在带 scoped 属性的 style 标签中,会被判定为局部样式而无法生效)的 height 属性指定为 calc(var(--el-tabs-header-height) - 1px),即可恢复正常的高度计算。


至于为什么这样会造成高度计算错误,希望有大神能解惑。


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

驱动产业升级,OpenHarmony赋能千行百业,擘画开源新蓝图

OpenAtom OpenHarmony(简称“OpenHarmony”)凭借其领先的技术创新能力和丰富的商业实践应用,展现了其在推动地方开源生态建设发展方面的强大动力。随着数字化转型的深入,OpenHarmony作为下一代智能终端操作系统的佼佼者,正逐步成为...
继续阅读 »

OpenAtom OpenHarmony(简称“OpenHarmony”)凭借其领先的技术创新能力和丰富的商业实践应用,展现了其在推动地方开源生态建设发展方面的强大动力。随着数字化转型的深入,OpenHarmony作为下一代智能终端操作系统的佼佼者,正逐步成为赋能千行百业数字化转型的关键力量。

各行业遍地开花

截至8月底,OpenHarmony社区累计超过8000名贡献者,总计有超过70家的共建企业参与贡献,超过731款软硬件产品通过兼容性测评,覆盖教育、交通、金融、家居、安防等多个行业。得益于来自各行各业共建者的共同努力,OpenHarmony在智能终端领域迅速发展,已成为增速领先的开源操作系统之一。

OpenHarmony应用于实践的成功案例如雨后春笋般涌现。在教育领域,基于OpenHarmony的智能学生证和智能手写板等产品,通过多设备协同体验,为学生和教师提供了高效、便捷的学习与教学工具。在金融领域,云喇叭、POS机等金融终端的推出,不仅丰富了金融市场业务,还显著提升了信息安全性能。此外,OpenHarmony还广泛应用于政务、工业、交通等多个领域,推动了相关行业的数字化转型进程。

赋能地方开源生态建设

OpenHarmony社区不仅在商业实践上取得了显著成果,还积极赋能地方开源生态建设。近年来,深圳,福州,惠州,重庆,南京,成都,无锡等多地政府纷纷出台政策措施,从产业应用、产业集聚、生态体系建设等维度支持OpenHarmony发展,从供给侧和需求侧共同推动生态建设。

开源技术深入融合地方产业进步,持续拓宽合作的新领域。各地持续提升服务保障体系,全力促进开源项目的成长,共同探索和丰富开源生态。这些举措极大地激发了企业和社会各界的参与热情,为OpenHarmony在地方的落地和应用提供了坚实的政策支持。

同时,OpenHarmony社区积极与高校、企业等合作伙伴建立紧密联系,共同推动人才培养和技术创新。通过产学合作、实训基地建设、课程开发等多种形式,OpenHarmony为地方培养了大量具备开源技术能力的专业人才,为地方开源生态的繁荣发展注入了新鲜血液。

构筑根技术人才护城河

开源人才已逐步成为推动信息产业发展的基石与战略支柱。OpenHarmony坚守“培育根技术人才,共筑根社区未来”的宗旨,大力建设OpenHarmony人才认证体系,为社区制定了一套权威的人才能力评估标准。这一举措不仅巩固了开发者的核心竞争力,也促进了根技术人才生态的繁荣发展。

OpenHarmony社区不断深化开发者护城河的建设,同时为社区成员、企业技术人员及院校学生提供了更开阔、更具体的职业成长路径,确保源源不断地向生态产业输送高质量人才。

引领技术革新

展望未来,OpenHarmony将继续秉承开源开放的精神,加强与产业链上下游伙伴的合作,共同推动技术创新和生态建设。随着OpenHarmony版本的不断迭代和生态的日益完善,相信将有更多基于OpenHarmony的创新应用和产品问世,为各行各业的数字化转型提供更加坚实的支撑。

同时,OpenHarmony也将持续加强探索开源技术在地方经济社会发展中的新路径、新模式。通过构建更加完善的开源生态体系,OpenHarmony将助力地方实现高质量发展目标,共同书写数字化转型的新篇章。

在9月25-27日举行的2024开放原子开源生态大会上,OpenHarmony将聚焦技术创新和生态发展,与广大生态伙伴共同见证OpenHarmony最新版本、新能力,以及兼容性、软硬件生态和开发者生态的新进展,一起共享技术实践,使能千行百业,共话商业落地。敬请关注。

9月26日上午,OpenHarmony项目群工作委员会将举办OpenHarmony生态主题演讲,特邀全球开源操作系统产业伙伴、技术大咖和学术专家,面向全球展示OpenHarmony的技术创新和产业落地成果,分享开源社区生态进展,共同见证开源赋能产业的国际盛会!


收起阅读 »

精准倒计时逻辑:揭秘前端倒计时逻辑的实现策略

web
在业务运营中,倒计时功能是常见的需求,尤其是在限时秒杀等促销活动中。为了确保时间的精确性和一致性,推荐使用服务器时间作为倒计时的基准。那么,如何在前端实现一个既准确又用户友好的倒计时组件的计时逻辑呢? 传统计时器实现 传统计时器实现倒计时的核心原理很简单,它使...
继续阅读 »

在业务运营中,倒计时功能是常见的需求,尤其是在限时秒杀等促销活动中。为了确保时间的精确性和一致性,推荐使用服务器时间作为倒计时的基准。那么,如何在前端实现一个既准确又用户友好的倒计时组件的计时逻辑呢?


传统计时器实现


传统计时器实现倒计时的核心原理很简单,它使用了 setIntervalsetTimeout 的对计时信息进行更新。类似于如下代码:


import React, { useState, useEffect } from 'react';

const CountdownTimerReact.FC<{ durationnumber }> = ({ duration }) => {
  const [secondsRemaining, setSecondsRemaining] = useState(duration);

  useEffect(() => {
    const intervalId = setInterval(() => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1);
      } else {
        clearInterval(intervalId);
      }
    }, 1000);

    // 清理计时器
    return () => clearInterval(intervalId);
  }, [secondsRemaining]);

  return (
    <div>
      倒计时: {secondsRemaining} 秒
    </div>

  );
};

export default CountdownTimer;

上述代码实现很好地实现了倒计时逻辑,但是,还是存在一些问题。我们先来讨论一下浏览器事件循环关于延时队列的优先级。我们知道,为了有效地管理任务和事件,事件循环使用了一个队列系统。事件循环主要包含如下两个队列:



  1. 宏任务队列(Macro Task Queue) :包括如 setTimeoutsetInterval、I/O、UI 事件等。

  2. 微任务队列(Micro Task Queue) :包括Promise回调、MutationObserver 等。


在事件循环中,当一个宏任务执行完毕后,JavaScript 引擎会先清空所有微任务队列中的所有任务,然后再去检查是否需要执行下一个宏任务。这意味着微任务的优先级高于宏任务。


setTimeoutsetInterval 任务会在指定的延时后被加入到宏任务队列的末尾。当当前的宏任务执行完毕后,如果微任务队列不为空,JavaScript 引擎会先执行完所有微任务,然后才会执行下一个宏任务,也就是 setTimeoutsetInterval 中的回调函数。因此,setTimeoutsetInterval 的优先级是相对较低的,因为它们必须等待当前宏任务执行完毕以及所有微任务执行完毕后才能执行。


这种机制可能导致一个问题:如果页面上的其他微任务执行时间较长,倒计时显示可能会出现“跳秒”现象。例如,倒计时可能从 60 秒直接跳到 58 秒,而不是平滑地递减。


requestAnimationFrame 实现


针对上述“跳秒”问题,我们可以改用 requestAnimationFrame 去进行时间的更新逻辑执行。我们将上述代码修改为如下代码:


import React, { useState, useEffect } from 'react';

const CountdownTimerReact.FC<{ durationnumber }> = ({ duration }) => {
  const [secondsRemaining, setSecondsRemaining] = useState(duration);

  useEffect(() => {
    let animationFrameIdnumber;

    const updateTimer = () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(prev => prev - 1);
        animationFrameId = requestAnimationFrame(updateTimer);
      } else {
        cancelAnimationFrame(animationFrameId);
      }
    };

    // 启动动画帧
    animationFrameId = requestAnimationFrame(updateTimer);

    // 清理动画帧
    return () => cancelAnimationFrame(animationFrameId);
  }, [secondsRemaining]);

  return (
    <div>
      倒计时: {secondsRemaining} 秒
    </div>

  );
};

export default CountdownTimer;

在编写倒计时功能的代码时,我们应当确保在每次更新倒计时秒数后重新启动动画帧。这样做可以避免在动画帧完成后,倒计时逻辑停止更新,导致倒计时在减少一秒后不再继续。同时,为了确保资源的有效管理,我们还需要提供一个函数来清理动画帧,这样当组件不再需要时,可以停止执行动画帧,避免不必要的性能消耗。通过这些措施,我们可以保证倒计时功能的准确性和组件的高效卸载。


优势


要深入理解 requestAnimationFrame 在实现倒计时中的优势,我们首先需要探讨一个问题:在 requestAnimationFrame 中直接修改 DOM 是否合适?requestAnimationFrame 是一个专为动画效果设计的 Web API,它通过在浏览器的下一次重绘之前调用回调函数,帮助我们创建更加流畅且高效的动画。与传统的定时器方法(如 setTimeoutsetInterval)相比,requestAnimationFrame 提供了更优的性能和更少的资源消耗。


requestAnimationFrame 中修改 DOM 是合适的,尤其是当涉及到动画和视觉更新时。这是因为 requestAnimationFrame 的设计初衷就是为了优化动画性能,确保动画的流畅性和效率。总结来说,requestAnimationFrame 相较于传统的计时器方法,具有以下显著优势:



  • 性能优化:通过在浏览器的下一次重绘前调用回调,确保动画的流畅性。

  • 节能高效:当浏览器标签页不处于活跃状态时,requestAnimationFrame 会自动暂停,从而减少 CPU 的使用,延长设备电池寿命。

  • 同步刷新:能够与浏览器的刷新率同步,有效避免动画中的跳帧现象。


因此,requestAnimationFrame 不仅适用于复杂的动画场景,也非常适合实现需要精确时间控制的倒计时功能,提供了一种更加高效和节能的解决方案。


劣势


尽管 requestAnimationFrame 在动画制作方面表现出色,但在实现倒计时功能时,它也存在一些局限性:



  • 精确度问题requestAnimationFrame 并不适用于需要严格时间控制的场景。因为它的调用时机依赖于浏览器的重绘周期,这可能导致时间间隔的不稳定性。

  • 管理复杂性:使用 requestAnimationFrame 需要开发者手动管理动画状态和进行资源清理,这增加了实现的复杂度。


正因如此,许多现代前端框架和库,如 ahook 等,在选择实现倒计时功能时,倾向于采用传统的定时器(如 setTimeoutsetInterval),而非 requestAnimationFrame。这些传统方法虽然可能不如 requestAnimationFrame 在动画性能上优化,但它们提供了更稳定和可预测的时间控制,这对于倒计时这类功能来说至关重要。


总结


实现一个倒计时组件的计时逻辑,我们有如下的一些建议:



  1. 动画与浏览器同步:对于涉及动画或需要与浏览器重绘周期同步的任务,requestAnimationFrame 是一个理想的选择。它能够确保动画的流畅性和性能优化。

  2. 体验优化:为了进一步提升用户体验,可以利用 performance.now() 来提高时间控制的精度。这个高精度的时间戳 API 可以帮助你更准确地计算时间间隔,从而优化倒计时的显示效果。

  3. 时间控制与简易任务:如果你的应用场景需要精确的时间控制或涉及到简单的定时任务,传统的 setTimeoutsetInterval 方法可能更加方便和直观。它们提供了稳定的时间间隔,易于理解和实现。


总结来说,选择最合适的技术方案取决于你的具体需求。无论是 requestAnimationFrame 还是传统的定时器方法,都有其适用场景和优势。关键在于根据项目需求,做出明智的选择,以实现最佳的用户体验。


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

Spring Boot整合Kafka+SSE实现实时数据展示

2024年3月10日 知识积累 为什么使用Kafka? 不使用Rabbitmq或者Rocketmq是因为Kafka是Hadoop集群下的组成部分,对于大数据的相关开发适应性好,且当前业务场景下不需要使用死信队列,不过要注意Kafka对于更新时间慢的数据拉取也较...
继续阅读 »

2024年3月10日


知识积累


为什么使用Kafka?


不使用Rabbitmq或者Rocketmq是因为Kafka是Hadoop集群下的组成部分,对于大数据的相关开发适应性好,且当前业务场景下不需要使用死信队列,不过要注意Kafka对于更新时间慢的数据拉取也较慢,因此对与实时性要求高可以选择其他MQ。
使用消息队列是因为该中间件具有实时性,且可以作为广播进行消息分发。


为什么使用SSE?


使用Websocket传输信息的时候,会转成二进制数据,产生一定的时间损耗,SSE直接传输文本,不存在这个问题
由于Websocket是双向的,读取日志的时候,如果有人连接ws日志,会发送大量异常信息,会给使用段和日志段造成问题;SSE是单向的,不需要考虑这个问题,提高了安全性
另外就是SSE支持断线重连;Websocket协议本身并没有提供心跳机制,所以长时间没有数据发送时,会将这个连接断掉,因此需要手写心跳机制进行实现。
此外,由于是长连接的一个实现方式,所以SSE也可以替代Websocket实现扫码登陆(比如通过SSE的超时组件在实现二维码的超时功能,具体实现我可以整理一下)
另外,如果是普通项目,不需要过高的实时性,则不需要使用Websocket,使用SSE即可


代码实现


Java代码


pom.xml引入SSE和Kafka


<!-- SSE,一般springboot开发web应用的都有 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- kafka,最主要的是第一个,剩下两个是测试用的 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency
<groupId>
org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.4.0</version>
</dependency>

application.properties增加Kafka配置信息


# KafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=community-consumer-group
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer

配置Kafka信息


@Configuration
public class KafkaProducerConfig {

@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;

@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
return props;
}

@Bean
public ProducerFactory<String, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}

@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}

}

配置controller,通过web方式开启效果


@RestController
@RequestMapping(path = "sse")
public class KafkaSSEController {

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

@Resource
private KafkaTemplate<String, String> kafkaTemplate;

@Resource
private SseEmitter sseEmitter;

/**
* @param message
* @apiNote 发送信息到Kafka主题中
*/

@PostMapping("/send")
public void sendMessage(@RequestBody String message) {
kafkaTemplate.send("my-topic", message);
}

/**
* 监听Kafka数据
*
* @param message
*/

@KafkaListener(topics = "my-topic", groupId = "my-group-id")
public void consume(String message) {
System.out.println("Received message: " + message);
//使用接口建立起sse连接后,监听到kafka消息则会发送给对应链接
SseEmitter sseEmitter = sseCache.get(id); if (sseEmitter != null) { sseEmitter.send(content); }
}

/**
* 连接sse服务
*
* @param id
* @return
* @throws IOException
*/

@GetMapping(path = "subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter push(@RequestParam("id") String id) throws IOException {
// 超时时间设置为5分钟,用于演示客户端自动重连
SseEmitter sseEmitter = new SseEmitter(5_60_000L);
// 设置前端的重试时间为1s
// send(): 发送数据,如果传入的是一个非SseEventBuilder对象,那么传递参数会被封装到 data 中
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("连接成功"));
sseCache.put(id, sseEmitter);
System.out.println("add " + id);
sseEmitter.send("你好", MediaType.APPLICATION_JSON);
SseEmitter.SseEventBuilder data = SseEmitter.event().name("finish").id("6666").data("哈哈");
sseEmitter.send(data);
// onTimeout(): 超时回调触发
sseEmitter.onTimeout(() -> {
System.out.println(id + "超时");
sseCache.remove(id);
});
// onCompletion(): 结束之后的回调触发
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
/**
* http://127.0.0.1:8080/sse/push?id=7777&content=%E4%BD%A0%E5%93%88aaaaaa
* @param id
* @param content
* @return
* @throws IOException
*/

@ResponseBody
@GetMapping(path = "push")
public String push(String id, String content) throws IOException {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.send(content);
}
return "over";
}

@ResponseBody
@GetMapping(path = "over")
public String over(String id) {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
// complete(): 表示执行完毕,会断开连接
sseEmitter.complete();
sseCache.remove(id);
}
return "over";
}

}

前端方式


<html>
<head>
<script>
console.log('start')
const clientId = "your_client_id_x"; // 设置客户端ID
const eventSource = new EventSource(`http://localhost:9999/v1/sse/subscribe/${clientId}`); // 订阅服务器端的SSE

eventSource.onmessage = event => {
console.log(event.data)
const message = JSON.parse(event.data);
console.log(`Received message from server: ${message}`);
};

// 发送消息给服务器端 可通过 postman 调用,所以下面 sendMessage() 调用被注释掉了
function sendMessage() {
const message = "hello sse";
fetch(`http://localhost:9999/v1/sse/publish/${clientId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message)
});
console.log('dddd'+JSON.stringify(message))
}
// sendMessage()
</script>
</head>
</html>

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

看了Kubernetes 源码后,我的Go水平突飞猛进

接口方式隐藏传入参数的细节 当方法的入参是一个结构体的时候,内部去调用时会看到入参过多的细节,这个时候可以将入参隐式转成结构,让内部只看到需要的方法即可。 type Kubelet struct{} func (kl *Kubelet) HandlePodA...
继续阅读 »

接口方式隐藏传入参数的细节


当方法的入参是一个结构体的时候,内部去调用时会看到入参过多的细节,这个时候可以将入参隐式转成结构,让内部只看到需要的方法即可。


type Kubelet struct{}

func (kl *Kubelet) HandlePodAdditions(pods []*Pod) {
for _, pod := range pods {
fmt.Printf("create pods : %s\n", pod.Status)
}
}

func (kl *Kubelet) Run(updates <-chan Pod) {
fmt.Println(" run kubelet")
go kl.syncLoop(updates, kl)
}

func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) {
for {
select {
case pod := <-updates:
handler.HandlePodAdditions([]*Pod{&pod})
}
}
}

type SyncHandler interface {
HandlePodAdditions(pods []*Pod)
}

这里我们可以看到 Kubelet 本身有比较多的方法:



  • syncLoop 同步状态的循环

  • Run 用来启动监听循环

  • HandlePodAdditions 处理Pod增加的逻辑


由于 syncLoop 其实并不需要知道 kubelet 上其他的方法,所以通过 SyncHandler 接口的定义,让 kubelet 实现该接口后,外面作为参数传入给 syncLoop ,它就会将类型转换为 SyncHandler


经过转换后 kubelet 上其他的方法在入参里面就看不到了,编码时就可以更加专注在 syncLoop 本身逻辑的编写。


但是这样做同样会带来一些问题,第一次研发的需求肯定是能满足我们的抽象,但是随着需求的增加和迭代,我们在内部需要使用 kubelet 其他未封装成接口的方法时,我们就需要额外传入 kubelet 或者是增加接口的封装,这都会增加我们的编码工作,也破坏了我们最开始的封装。


分层隐藏设计是我们设计的最终目的,在代码设计的过程中让一个局部关注到它需要关注的东西即可。


接口封装方便Mock测试


通过接口的抽象,我们在测试的时候可以把不关注的内容直接实例化成一个 Mock 的结构。


type OrderAPI interface {
GetOrderId() string
}

type realOrderImpl struct{}

func (r *realOrderImpl) GetOrderId() string {
return ""
}

type mockOrderImpl struct{}

func (m *mockOrderImpl) GetOrderId() string {
return "mock"
}

这里如果测试的时候不需要关注 GetOrderId 的方法是否正确,则直接用 mockOrderImpl 初始化 OrderAPI 即可,mock的逻辑也可以进行复杂编码



func TestGetOrderId(t *testing.T) {
orderAPI := &mockOrderImpl{} // 如果要获取订单id,且不是测试的重点,这里直接初始化成mock的结构体
fmt.Println(orderAPI.GetOrderId())
}

gomonkey 也同样能进行测试注入,所以如果以前的代码没能够通过接口封装也同样可以实现mock,而且这种方式更加强大


patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order {
return Order{
OrderId: orderId,
OrderState: delivering,
}
})
return func() {
patches.Reset()
}

使用 gomonkey 能够更加灵活的进行 mock , 它能直接设置一个方法的返回值,而接口的抽象只能够处理结构体实例化出来的内容。


接口封装底层多种实现


iptables 、ipvs等的实现就是通过接口的抽象来实现,因为所有网络设置都需要处理 Service 和 Endpoint ,所以抽象了 ServiceHandlerEndpointSliceHandler


// ServiceHandler 是一个抽象接口,用于接收有关服务对象更改的通知。
type ServiceHandler interface {
// OnServiceAdd 在观察到创建新服务对象时调用。
OnServiceAdd(service *v1.Service)
// OnServiceUpdate 在观察到现有服务对象的修改时调用。
OnServiceUpdate(oldService, service *v1.Service)
// OnServiceDelete 在观察到现有服务对象的删除时调用。
OnServiceDelete(service *v1.Service)
// OnServiceSynced 一旦所有初始事件处理程序都被调用并且状态完全传播到本地缓存时调用。
OnServiceSynced()
}

// EndpointSliceHandler 是一个抽象接口,用于接收有关端点切片对象更改的通知。
type EndpointSliceHandler interface {
// OnEndpointSliceAdd 在观察到创建新的端点切片对象时调用。
OnEndpointSliceAdd(endpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSliceUpdate 在观察到现有端点切片对象的修改时调用。
OnEndpointSliceUpdate(oldEndpointSlice, newEndpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSliceDelete 在观察到现有端点切片对象的删除时调用。
OnEndpointSliceDelete(endpointSlice *discoveryv1.EndpointSlice)
// OnEndpointSlicesSynced 一旦所有初始事件处理程序都被调用并且状态完全传播到本地缓存时调用。
OnEndpointSlicesSynced()
}

然后通过 Provider 注入即可,


type Provider interface {
config.EndpointSliceHandler
config.ServiceHandler
}

这个也是我在做组件的时候用的最多的一种编码技巧,通过将类似的操作进行抽象,能够在替换底层实现后,上层代码不发生改变。


封装异常处理


我们开启协程之后如果不对异常进行捕获,则会导致协程出现异常后直接 panic ,但是每次写一个 recover 的逻辑做全局类似的处理未免不太优雅,所以通过封装 HandleCrash 方法来实现。


package runtime

var (
ReallyCrash = true
)

// 全局默认的Panic处理
var PanicHandlers = []func(interface{}){logPanic}

// 允许外部传入额外的异常处理
func HandleCrash(additionalHandlers ...func(interface{})) {
if r := recover(); r != nil {
for _, fn := range PanicHandlers {
fn(r)
}
for _, fn := range additionalHandlers {
fn(r)
}
if ReallyCrash {
panic(r)
}
}
}

这里既支持了内部异常的函数处理,也支持外部传入额外的异常处理,如果不想要 Crash 的话也可以自己进行修改。


package runtime

func Go(fn func()) {
go func() {
defer HandleCrash()
fn()
}()
}

要起协程的时候可以通过 Go 方法来执行,这样也避免了自己忘记增加 panic 的处理。


waitgroup的封装


import "sync"

type Gr0up struct {
wg sync.WaitGr0up
}

func (g *Gr0up) Wait() {
g.wg.Wait()
}

func (g *Gr0up) Start(f func()) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
f()
}()
}

这里最主要的是 Start 方法,内部将 AddDone 进行了封装,虽然只有短短的几行代码,却能够让我们每次使用 waitgroup 的时候不会忘记去对计数器增加一和完成计数器。


信号量触发逻辑封装


type BoundedFrequencyRunner struct {
sync.Mutex

// 主动触发
run chan struct{}

// 定时器限制
timer *time.Timer

// 真正执行的逻辑
fn func()
}

func NewBoundedFrequencyRunner(fn func()) *BoundedFrequencyRunner {
return &BoundedFrequencyRunner{
run: make(chan struct{}, 1),
fn: fn,
timer: time.NewTimer(0),
}
}

// Run 触发执行 ,这里只能够写入一个信号量,多余的直接丢弃,不会阻塞,这里也可以根据自己的需要增加排队的个数
func (b *BoundedFrequencyRunner) Run() {
select {
case b.run <- struct{}{}:
fmt.Println("写入信号量成功")
default:
fmt.Println("已经触发过一次,直接丢弃信号量")
}
}

func (b *BoundedFrequencyRunner) Loop() {
b.timer.Reset(time.Second * 1)
for {
select {
case <-b.run:
fmt.Println("run 信号触发")
b.tryRun()
case <-b.timer.C:
fmt.Println("timer 触发执行")
b.tryRun()
}
}
}

func (b *BoundedFrequencyRunner) tryRun() {
b.Lock()
defer b.Unlock()
// 可以增加限流器等限制逻辑
b.timer.Reset(time.Second * 1)
b.fn()
}

写在最后


感谢你读到这里,如果想要看更多 Kubernetes 的文章可以订阅我的专栏: juejin.cn/column/7321… 。


作者:蔡蔡菜
来源:juejin.cn/post/7347221064429469746
收起阅读 »

多人开发小程序设置体验版的痛点

web
抛出痛点 在分配任务时,我们将需求分为三个分支任务,分别由前端A、B、C负责: 前端A: HCC-111-实现登录功能 前端B: HCC-112-实现用户注册 前端C: HCC-113-实现用户删除 相应地,我们创建三个功能分支: feature_HCC...
继续阅读 »

抛出痛点


在分配任务时,我们将需求分为三个分支任务,分别由前端A、B、C负责:



  1. 前端A: HCC-111-实现登录功能

  2. 前端B: HCC-112-实现用户注册

  3. 前端C: HCC-113-实现用户删除


相应地,我们创建三个功能分支:



  • feature_HCC-111-实现登录功能

  • feature_HCC-112-实现用户注册

  • feature_HCC-113-实现用户删除


当所有的前端都开发完成了他们的任务,我们就要开始测试小程序了。但是如果按照以往体验版的测试方式,我们就需要排个顺序。比如,前端 A 先将他的小程序设置为体验版,测试把他的功能测试完成之后,再把前端 B 的设置为体验版,以此类推。可以看出真的很麻烦,而且浪费开发时间,我想你肯定不想在开发的时候突然被叫把你的小程序版本设置为体验版。


解决方案


小程序开发助手 这是一个官方提供的小程序,里面有多个版本的小程序可供选择,很方便测试人员的测试,并且也会节省开发人员的时间。点击版本查看就可以看到所有开发人员提交的最近的一次版本了。这样也不用设置体验版就可以测试最新的提交了。


image.png


再次抛出痛点


如果前端 A 头上有三个任务单呢?任务单:HCC-121-实现框架搭建,HCC-122-实现在线录屏,HCC-123-实现画板。此时你可能想说, 为啥前端 A 这么多的任务单呢?他命苦啊!


这个时候就需要配合微信的机器人了,我们可以创建多个机器人作为我们提交版本的媒介,这样我们就不受限于微信账号了。


可以在微信的官方文档看到 robot 参数有30个机器人可供选择。


请添加图片描述


接下来看下微信的机器人的使用方式。


miniprogram-ci文档


微信官方是这样介绍这个工具的; miniprogram-ci 是从微信开发者工具中抽离的关于小程序/小游戏项目代码的编译模块。它其实是一个自动上传代码的工具,可以帮助我们自动化的编译代码并且上传到微信。


下面是一个大概得使用的示例,具体还是要参考官方文档。


const ci = require('miniprogram-ci');
(async () => {
const project = new ci.Project({
appid: 'wxsomeappid',
type: 'miniProgram',
projectPath: 'the/project/path',
privateKeyPath: 'the/path/to/privatekey',
ignores: ['node_modules/**/*'],
})
const previewResult = await ci.preview({
project,
desc: 'hello', // 此备注将显示在“小程序助手”开发版列表中
setting: {
es6: true,
},
qrcodeFormat: 'image',
qrcodeOutputDest: '/path/to/qrcode/file/destination.jpg',
onProgressUpdate: console.log,
// pagePath: 'pages/index/index', // 预览页面
// searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
})
console.log(previewResult)
})()

当我们使用这个脚本上传完代码就可以在小程序开发助手或者小程序管理平台看到以下内容。


微信管理后台
请添加图片描述
小程序开发助手页面
在这里插入图片描述


最后


我们可以使用 miniprogram-ci 配合 Jenkins 实现自动化部署,提交完成代码就可以自动部署了。以下是一个 github 的 actions 示例。当然也可以使用别的方式,例如本地提交,Jenkins提交等。


name: Feature Branch CI

on:
workflow_dispatch:
push:
branches: ['feature_*'] # 使用通配符匹配所有feature分支

jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'yarn'

- name: Install dependencies
run: |
npm install -g miniprogram-ci cross-env
yarn install --frozen-lockfile

- name: Build Package
run: yarn cross-env ENV=PROD uni build -p mp-weixin --mode PROD

- name: Create private key file
run: echo "${{ secrets.PRIVATE_KEY }}" > private.key

- name: Deploy Package
env:
APP_ID: ${{ secrets.APP_ID }}
run: |
COMMIT_MESSAGE=$(git log --format=%B -n 1 ${{ github.sha }})
if [[ $COMMIT_MESSAGE =~ VERSION-([A-Za-z0-9_]+-[A-Za-z0-9_-]+)_DEV ]]; then
VERSION=${BASH_REMATCH[1]}
echo "Extracted Version: $VERSION"
miniprogram-ci preview \
--pp ./dist/build/mp-weixin \
--pkp ./private.key \
--appid $APP_ID \
--uv "${VERSION}" \
-r 7 \
--desc "${COMMIT_MESSAGE}" \
--upload-description "${COMMIT_MESSAGE}" \
--enable-es6 true \
--enable-es7 true \
--enable-minifyJS true \
--enable-minifyWXML true \
--enable-minifyWXSS true \
--enable-minify true \
--enable-bigPackageSizeSupport true \
--enable-autoPrefixWXSS true
else
echo "No Version found in commit message. Skipping upload."
fi

作者:阿臻
来源:juejin.cn/post/7412854873439027240
收起阅读 »

刚刚,英特尔发布最强CPU,AI PC迎来最高效x86芯片

金磊 发自 柏林 量子位 | 公众号 QbitAI 最高效的x86芯片,应当是一种怎样的“打开方式”? 就在刚刚,英特尔给出了一份答案—— 英特尔® 酷睿™ Ultra 200V系列处理器。 △英特尔高级副总裁Jim Johnson 话不多说,直接上亮点:...
继续阅读 »

金磊 发自 柏林


量子位 | 公众号 QbitAI



最高效的x86芯片,应当是一种怎样的“打开方式”?


就在刚刚,英特尔给出了一份答案——


英特尔® 酷睿™ Ultra 200V系列处理器。


图片


英特尔高级副总裁Jim Johnson

话不多说,直接上亮点:



  • 最快的CPU:E核比上一代快68%,P核则快了14%

  • 最快的内置(built-in)GPU:首次推出全新Xe2图形微架构,平均提升30% 的移动图形性能

  • 最高AI性能:CPU、NPU和GPU整体算力高达120TOPS,直接拉高AI体验

  • 最高效x86:整体功耗降低50%

  • 超强的兼容性:各种软件应用程序均可兼容


图片


由此,在搭载英特尔® 酷睿™ Ultra 200V系列芯片之后,AI PC们的生产力也迎来了“蜕变”:



  • 每线程处理性能提高3倍

  • 峰值性能提高80%

  • 续航长达20小时


图片


不仅如此,全球顶级的OEM和ISV们纷纷前来站台,例如谷歌微软联想戴尔惠普等等。


那么英特尔具体又是如何做到的?以及这个“史上最高效”又是如何界定的?我们继续往下看。


史上最高效的x86处理器


首先我们需要说明的是,Ultra 200V系列一共包含9款处理器,CPU均为8核8线程,GPU和NPU的核心数量会有所不同:


图片


低功耗方面,这里比较重要的一个变化,就是英特尔对低功耗岛(Low Power Island)做出的改变。


它把Lunar Lake核心数量和缓存增加了一倍(达到4MB和4个内核),并将E核从Crestmont更新到Skymont。


图片


然后英特尔使用各种电源管理技术(包括 Thread Director),通过这个低功耗岛以低功耗来实现效率上的大幅提升。


一个比较有意思的点是,从这里开始,英特尔就直接开始向高通猛开炮火了,性能表现如下:


图片


而除了高通之外,AMD也没能逃过被英特尔拿来公开做比较的命运。


例如在电池寿命方面,英特尔就表示已经超过了AMD和高通。


图片


其次,是CPU方面。


英特尔在CPU上采用了全新的架构,即4个Skymont E核和4个Lion Cove P核,官方所展示的核心信息如下:


图片


不过这里有一个关键的问题。


那就是P核不包括超线程,这个技术实际上允许单个CPU内核支持多个任务线程。


根据英特尔的说法,不采用超线程是因为这样会有助于芯片的整体集成。


图片


尽管英特尔这次是“4+4”模型(8个线程,比上一代少很多),但从给出来的性能结果来看,要比AMD和高通要好得多。


图片


在CPU之后,便是英特尔的内置GPU了。


英特尔这次首次亮相了全新的Xe2图形微架构,不仅适用于集成图形领域,而且适用于独立显卡。


图片


在GPU性能的比较上就更有意思了。


英特尔先是用Ultra 7 155H和Ultra 9 288V在众多游戏上进行了PK,在Xe2加持之下,有了31%的性能提升。


图片


然而到了与高通相比较的时候,结果是这样的:



高通X1E-84-100无法运行23款游戏



图片


在与AMD HX 370的比较过程中,Ultra 9 288V表现出来的结果是要快出16%。


图片


除此之外,光线追踪也是Xe2的另一大亮点,领先于竞争对手,RT性能提高了30%。


最后,便是Ultra 200V在AI方面的性能了。


正如我们在开头提到的,英特尔此次整体算力达到了120TOPS,其中GPU是67TOPS,NPU 48TOPS,以及还有CPU的5 TOPS。


在性能对比上,同样是和高通相比,在使用Adobe Premiere和Lightroom功能等应用程序时,明显是要快得多。


图片


值得一提的是,在量子位与英特尔交流过程中了解到,英特尔是目前与同行相比,唯一一家在CPU、NPU和GPU三个AI Engine都能做到均衡发展的那一个,而这也成为了其核心竞争力之一。


图片


AI PC们都来站台了


除了英特尔此次“最高效x86处理器”的发布之外,现场的OEM和ISV们也是不可忽视的亮点。


以OEM为例,其数量之多,从一张图就能感受到这个feel了:


图片


近乎所有的AI PC们都有所参与:20多个品牌,80多款机型都搭载了最新的Ultra 200V系列处理器。


不仅如此,在发布活动的现场,谷歌、微软、联想、戴尔和惠普等,也上台表达了对Ultra 200V能上机、上服务而感到的期待。


图片


图片


同样的,即便是在场外,PK的味道依旧是非常浓郁。


例如在demo演示区,英特尔就拉着AMD现场以赛车的游戏来比拼了一番:


**,时长00:17


当然,高通依旧是不能落下:


图片


据了解,首批搭载Ultra 200V处理器的笔记本电脑将在9月24日上线。


不过在此之前,我们也不妨蹲一波更多的实测结果。


One More Thing


英特尔除了Ultra 200V系列芯片之外,此次还发布了Evo Edition标识


图片


据悉,获得标识的电脑必须通过严苛的英特尔Evo Edition OEM系统验证流程。


图片


每款机型须首先通过预评估,并在通过之后的六个月内接受 10 种不同的测试标准。在此期间会对该机型进行调优,以满足英特尔Evo标准。


作者:量子位
来源:juejin.cn/post/7410786086580437004
收起阅读 »

Go 重构:尽量避免使用 else、break 和 continue

今天,我想谈谈相当简单的事情。我不会发明什么,但我在生产代码中经常看到这样的事情,所以我不能回避这个话题。 我经常要解开多个复杂的 if else 结构。多余的缩进、过多的逻辑只会加深理解。首先,这篇文章的主要目的是让代码更透明、更易读。不过,在某些情况下还是...
继续阅读 »

今天,我想谈谈相当简单的事情。我不会发明什么,但我在生产代码中经常看到这样的事情,所以我不能回避这个话题。


我经常要解开多个复杂的 if else 结构。多余的缩进、过多的逻辑只会加深理解。首先,这篇文章的主要目的是让代码更透明、更易读。不过,在某些情况下还是必须使用这些操作符。


else 操作


例如,我们有简单的用户处理程序:


func handleRequest(user *User) {
if user != nil {
showUserProfilePage(user)
} else {
showLoginPage()
}
}

如果没有提供用户,则需要将收到的请求重定向到登录页面。If else 似乎是个不错的决定。但我们的主要任务是确保业务逻辑单元在任何输入情况下都能正常工作。因此,让我们使用提前返回来实现这一点。


func handleRequest(user *User) {
if user == nil {
return showLoginPage()
}
showUserProfilePage(user)
}

逻辑是一样的,但是下面的做法可读性会更强。


break 操作


对我来说,BreakContinue 语句总是可以分解的信号。


例如,我们有一个简单的搜索任务。找到目标并执行一些业务逻辑,或者什么都不做。


func processData(data []int, target int) {
for i, value := range data {
if value == target {
performActionForTarget(data[i])
break
}
}
}

你应该始终记住,使用 break 操作符并不能保证整个数组都会被处理。这对性能有好处,因为我们丢弃了不必要的迭代,但对代码支持和可读性不利。因为我们永远不知道程序会在列表的开头还是结尾停止。


在某些情况下,带有子任务的简单功能可能会破坏这段代码。


func processData(data []int, target int, subtask int) {
for i, value := range data {
if value == subtask {
performActionForSubTarget(data[i])
}
if value == target {
performActionForTarget(data[i])
break
}
}
}

这样我们实际上可以拆出一个 find 的方法:


func processData(data []int, target int, subTarget int) {
found := findTarget(data, target)
if found > notFound {
performActionForTarget(found)
}

found = findTarget(data, subTarget)
if found > notFound {
performActionForSubTarget(found)
}
}

const notFound = -1

func findTarget(data []int, target int) int {
if len(data) == 0 {
return notFound
}

for _, value := range data {
if value == target {
return value
}
}

return notFound
}

同样的逻辑,但是拆分成更细粒度的方法,也有精确的返回语句,可以很容易地通过测试来实现。


continue 操作


该操作符与 break 类似。为了正确阅读代码,您应该牢记它对操作顺序的具体影响。


func processWords(words []string, substring string) {
for _, word := range words {
if !strings.Contains(word, substring) {
continue
}

// do some buisness logic
performAction(word)
}
}

Continue 使得这种简单的流程变得有点难以理解。


让我们写得更简洁些:


func processWords(words []string, substring string) {
for _, word := range words {
if strings.Contains(word, substring) {
performAction(word)
}
}
}

作者:爱发白日梦的后端
来源:juejin.cn/post/7290931758786756669
收起阅读 »

实现基于uni-app的项目自动检查APP更新

我们平时工作中开发APP时,及时为用户提供应用更新是提升用户体验和保证功能完整性的重要一环。本文将通过一段实际的代码示例,详细介绍如何在基于uni-app框架的项目中实现自动检查应用更新的功能,并引导用户完成升级过程。该功能主要涉及与服务器端交互获取最新版本信...
继续阅读 »

我们平时工作中开发APP时,及时为用户提供应用更新是提升用户体验和保证功能完整性的重要一环。本文将通过一段实际的代码示例,详细介绍如何在基于uni-app框架的项目中实现自动检查应用更新的功能,并引导用户完成升级过程。该功能主要涉及与服务器端交互获取最新版本信息、比较版本号、提示用户升级以及处理下载安装流程。



创建一个checkappupdate.js文件


这个文件是写升级逻辑处理的文件,可以不创建,直接在App.vue中写,但是为了便于维护,还是单独放出来比较好,可以放在common或者util目录中(App.vue能引入到就行,随意放,根目录也行),App.vue中引入该文件,调用升级函数如下图所示:


image.png


js完整代码


为了防止一点点代码写,容易让人云里雾里,先放完整代码,稍后再详细解释,其实看注释也就够了。


//这是服务端请求url配置文件,如果你直接卸载下面的请求中,可以不引入
import configService from '@/common/service/config.service.js'

export default function checkappupdate(param = {}) {
// 合并默认参数
param = Object.assign({
title: "A new version has been detected!",
content: "Please upgrade the app to the latest version!",
canceltext: "No upgrade",
oktext: "Upgrade now"
}, param)

plus.runtime.getProperty(plus.runtime.appid, (widgetInfo) => {
let platform = plus.os.name.toLocaleLowerCase() //Android
let os_version = plus.os.version //13 安卓版本
let vendor = plus.device.vendor //Xiaomi
let url = configService.apiUrl
uni.request({
url: url + '/checkAppUpdate',
method: 'GET',
data: {
platform: platform,
os_version: os_version,
vendor: vendor,
cur_version: widgetInfo.version
},
success(result) {
console.log(result)
let versionCode = parseInt(widgetInfo.versionCode)
let data = result.data ? result.data : null;
// console.log(data);
let downAppUrl = data.url
//判断版本是否需要升级
if (versionCode >= data.versionCode) {
return;
}
//升级提示
uni.showModal({
title: param.title,
content: data.log ? data.log : param.content,
showCancel: data.force ? false : true,
confirmText: param.oktext,
cancelText: param.canceltext,
success: res => {
if (!res.confirm) {
console.log('Cancel the upgrade');
// plus.runtime.quit();
return
}
// if (data.shichang === 1) {
// //去应用市场更新
// plus.runtime.openURL(data.shichangurl);
// plus.runtime.restart();
// } else {
// 开始下载
// 创建下载任务
var dtask = plus.downloader.createDownload(downAppUrl, {
filename: "_downloads/"
},
function (d, status) {
// 下载完成
if (status == 200) {
plus.runtime.install(d.filename, {
force: true
}, function () {
//进行重新启动;
plus.runtime.restart();
}, (e) => {
uni.showToast({
title: 'install fail:' + JSON
.stringify(e),
icon: 'none'
})
console.log(JSON.stringify(e))
});
} else {
this.tui.toast("download fail,error code: " +
status);
}
});
let view = new plus.nativeObj.View("maskView", {
backgroundColor: "rgba(0,0,0,.6)",
left: ((plus.screen.resolutionWidth / 2) - 45) +
"px",
bottom: "80px",
width: "90px",
height: "30px"
})

view.drawText('start download...', {}, {
size: '12px',
color: '#FFFFFF'
});
view.show()
// console.log(dtask);
dtask.addEventListener("statechanged", (e) => {
if (e && e.downloadedSize > 0) {
let jindu = ((e.downloadedSize / e.totalSize) *
100).toFixed(2)
view.reset();
view.drawText('Progress:' + jindu + '%', {}, {
size: '12px',
color: '#FFFFFF'
});
}
}, false);
dtask.start();
// }
},
fail(e) {
console.log(e);
uni.showToast({
title: 'Request error'
})
}
})
}
})

});
}


函数定义:checkappupdate


定义核心函数checkappupdate,它接受一个可选参数param,用于自定义提示框的文案等信息。函数内部首先通过Object.assign合并默认参数与传入参数,以确保即使未传入特定参数时也能有良好的用户体验。


获取应用信息与环境变量


利用plus.runtime.getProperty获取当前应用的详细信息,包括但不限于应用ID、版本号(version)和版本号代码(versionCode),以及设备的操作系统名称、版本和厂商信息。这些数据对于后续向服务器请求更新信息至关重要。


请求服务器检查更新


构建包含平台信息、操作系统版本、设备厂商和当前应用版本号的请求参数,发送GET请求至配置好的API地址/checkAppUpdate,查询是否有新版本可用。后端返回参数参考下面:


   /**
* 检测APP升级
*/

public function checkAppUpdate()
{
$data['versionCode'] = 101;//更新的版本号
$data['url'] = 'http://xxx/app/xxx.apk';//下载地址
$data['force'] = true;//是否强制更新
return json_encode($data);//返回json格式数据到前端
}

比较版本与用户提示


一旦收到服务器响应,解析数据并比较当前应用的版本号与服务器提供的最新版本号。如果存在新版本,使用uni.showModal弹窗提示用户,展示新版本日志(如果有)及升级选项。此步骤充分考虑了是否强制更新的需求,允许开发者灵活配置确认与取消按钮的文案。


下载与安装新版本


用户同意升级后,代码将执行下载逻辑。通过plus.downloader.createDownload创建下载任务,并监听下载进度,实时更新进度提示。下载完成后,利用plus.runtime.install安装新APK文件,并在安装成功后调用plus.runtime.restart重启应用,确保新版本生效。


用户界面反馈


在下载过程中,通过创建原生覆盖层plus.nativeObj.View展示一个半透明遮罩和下载进度信息,给予用户直观的视觉反馈,增强了交互体验,进度展示稍微有点丑,可以提自己改改哈。


image.png


总结



通过上述步骤,我们实现了一个完整的应用自动检查更新流程,不仅能够有效通知用户新版本的存在,还提供了平滑的升级体验。此功能的实现,不仅提升了用户体验,也为产品迭代和功能优化提供了有力支持。开发者可以根据具体需求调整提示文案、下载逻辑、进度样式等细节,以更好地适配自身应用的特点和用户群体。



作者:掘金归海一刀
来源:juejin.cn/post/7367555191337828361
收起阅读 »

Innodb之buffer pool 图文详解

介绍 数据通常都是持久化在磁盘中的,innodb如果在处理客户端的请求时直接访问磁盘,无论是IO压力还是响应速度都是无法接受的;所以innodb在处理请求时,如果需要某个页的数据就会把整个页都加载到缓存中,数据页会长时间待在缓存中,根据一定策略进行淘汰,后续如...
继续阅读 »

介绍


数据通常都是持久化在磁盘中的,innodb如果在处理客户端的请求时直接访问磁盘,无论是IO压力还是响应速度都是无法接受的;所以innodb在处理请求时,如果需要某个页的数据就会把整个页都加载到缓存中,数据页会长时间待在缓存中,根据一定策略进行淘汰,后续如果在缓存中的数据就不需要再次加载数据页了,这样即可提高响应时间又可以节省磁盘IO.


buffer pool


上述介绍中我们有提到一个缓存,这个缓存就指的是buffer pool,通过innodb_buffer_pool_size进行设置它的大小,默认为128MB,最小为5MB;可以根据各自线上机器的情况来设置它的大小,设置几个G甚至上百个G都是合理的。


内部组成


image.png
buffer pool中包含数据页、索引页、change buffer、自适应hash等内容;数据页、索引页在buffer pool中占用了大部分,不能简单的认为缓冲池中只有数据页和索引页;change buffer在较老的版本中叫insert buffer,后面对其进行了升级形成了现在的change buffer;自适应hash可以方便我们快速查询数据;锁信息、数据字典都是占用比较小的一部分;以上就是buffer pool的内部组成。


页数据


数据页、索引页数据在mysql启动的时候,会直接给申请一块连续的内存空间;如图:


image.png
上图中的缓冲页对应的就是磁盘中的数据,默认每个页大小为16KB,并且innodb为每个缓冲页都创建了一些控制块,每个控制块占用大小是800字节左右,需要额外付出百分之5的内存,它记录页所属的表空间编号、页号、缓存页在buffer pool中的地址、链表节点信息等。内存中间可能会有碎片进行对齐。

注意:这里只有缓冲页占用的空间是计算在buffer pool中的。


free链表


根据上面的图可以了解到,buffer pool中有一堆缓冲页,但innodb从磁盘中读取数据页时,由于不能直接知道哪些缓冲页是空闲的、哪些页已经被使用了,导致了不知道把要读取的数据页存放到哪里;此时就引入了一个free链表的概念。如图:


image.png


上图中可以看到free链表靠一个free节点连接到控制块中,其中free头节点仅占用40字节空间,但它也不计算在buffer pool中;有了这个free链表后每当需要从磁盘中加载一个页到buffer pool中时就可以从free链表上取一个控制块,把控制块所需信息填充上,同时把从磁盘上加载的数据放到对应的缓冲页上,并把该控制块从free链表中移除。此时就把磁盘中的页加载到内存中了,后续查询数据时就会优先查询该内存页,但每次查询时没办法立刻知道该页是在内存中还是磁盘中,上述操作后还会把这个页信息放到一个散列表中,以(表空间号+页号)作为key,以控制块地址作为value。


flush链表


上述介绍了读数据时通过优先读取内存页可以提高我们的响应速度以及节省磁盘io,那么如果是写数据呢?其实在innodb中,更改也会优先在内存中更改,在后续会根据一定规则(会在后续redolog文章中详细介绍)进行刷盘,在刷盘时只需要刷被更改的缓冲页即可,那么哪些缓存页被更改了innodb是不知道的,此时innodb就设计了flush链表,它和free链表几乎一样,如图:


image.png


当需要刷盘时会从flush链表中拿出一部分控制块对应的缓冲页进行刷盘,刷盘后控制块会从flush链表中移除,并放到free链表中。


LRU链表


由于buffer pool的内存区域是有限的,如果当来不及刷盘时内存就不够用了;此时innodb采用了LRU淘汰策略,标准的LRU算法:



  • 如果数据页已经被加载到buffer pool中了,则直接把对应的控制块移动到LRU链表的头部;

  • 如果数据页不在buffer pool中,则从磁盘中加载到buffer pool中,并把其对应的控制块放到LRU头部;此时内存空间已经满了的话,就会从链表中移除最后一个内存页。


但直接采用lru方案,在内存较小或者临时一次性加载到内存中的页过多时会把真正的热点数据刷掉。如预读和全表扫描。



  • 线性预读:如果顺序访问的某个区的页面数超过innodb_read_ahead_threshold(默认值为56)就会触发一次异步预加载下一个区中的全部页到内存中;

  • 随机预读:如果某个区的13个连续的页都被加载到young区前1/4的位置中,会触发一次异步预加载本区中的全部页到内存中;

  • 全表扫描:把整张表的数据都加载到内存中。


为了解决上述问题,innodb对这个淘汰策略做了一点改变。如图:


image.png


innodb根据innodb_old_blocks_pct(默认37)参数把整个lru分成一定比例,具体的淘汰策略:



  • 当数据页第一次加载进来时会先放到old head处,当链表满时会把old tail刷盘并从链表中移除。

  • 当再次使用一个数据页时,并且该页在old区,会先判断在old区的停留时间是否超过innodb_old_blocks_time(默认1000ms),如果超过则把该数据页移动到young head处,反之移动到old head处。

  • 当再次使用一个数据页时,并且该页young区为了节省移动的操作,会判断该缓冲页是否在young区前1/4中,如果在就不进行移动,如果不在则移动到young head处。


多buffer pool实例


对于buffer pool设置很大内存的实例,由于操作各种链表都需要进行加锁这样就比较影响整体的性能,为了提高并发处理能力,可以通过innodb_buffer_pool_instances来设置buffer pool的实例个数。在mysql5.7.5版本中,会以chunk为单位向系统申请内存空间,每个buffer pool中包含了N个chunk。如图:


image.png


可以通过innodb_buffer_pool_chunk_size(默认128M)来设置chunk的大小,只能在启动前设置好,启动后可以更改innodb_buffer_pool_size的大小,但必须时innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances的整数倍。


自适应hash


对于b+数来讲,整体的查询时间复杂度为O(logN),而innodb为了进一步提升性能,引入了自适应hash,对热点数据可以做到O(1)的时间复杂度就可以定位到数据在具体哪个页中。

innodb会自动根据访问频率和模式自动为某些热点页在内存中建立自适应哈希索引,规则:




  • 模式:例如有一个联合索引(a,b);查询条件where a = xxxwhere a = xxx and b = xxx 这就属于两种模式,如果交叉使用这两种查询,不会为其建立自适应哈希索引;

  • 频率:使用一种默认访问的次数大于Math.min(100,页中数据/16)。



根据官方数据,启动自适应哈希索引读写速度可以提升2倍,辅助索引的连接性能可以提升5倍。


总结


通过上述的介绍,希望能帮助大家对buffer pool有一个基础的了解,想进一步深入了解可以通过执行show engine innodb status观察下各种参数,通过对每个参数的细致研究可以全方面的掌握buffer pool。





创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~


作者:想打游戏的程序猿
来源:juejin.cn/post/7413196978601295899
收起阅读 »

仿树木生长开花的动画效果

web
效果介绍 使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。 实现效果展示 实现步骤 创建画布 import React, { useEffect, useRef } from 'react'...
继续阅读 »

效果介绍



使用 canvas 进行绘制树木生长的效果,会逐渐长出树干,长出树叶,开出花。当窗口大小发生变化时进行重新渲染。



实现效果展示


Dec-06-2023 14-39-01.gif


实现步骤


创建画布


import React, { useEffect, useRef } from 'react'

function TreeCanvas(props: {
width: number;
height: number;
}
) {
const { width = 400, height = 400 } = props;
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas?.getContext('2d');
if (!canvas || !context) return;
context.strokeStyle = '#a06249';
}, [])
return (
<canvas ref={canvasRef} width={width} height={height} />
)
}

export default TreeCanvas

封装创建树枝的方法



  • 树枝需要起点,终点,树枝宽度


 function lineTo(p1: PointType, p2: PointType, lineWidth: number) {
context?.beginPath();
context?.moveTo(p1.x, p1.y);
context?.lineTo(p2.x, p2.y);
context.lineWidth = lineWidth;
context?.stroke();
}


绘制树叶和花朵的方法封装



  • 提前生成图片实例

  • 传递图片和坐标进行绘制


   // 花的实例
const image = new Image();
image.src ='https://i.postimg.cc/D0LLWwKy/flower1.png';
// 叶子的实例
const imageLeaves = new Image();
imageLeaves.src = 'https://i.postimg.cc/PJShQmH6/leaves.png';

function drawTmg(imageUrl: any, p1: PointType) {
context?.drawImage(imageUrl, p1.x, p1.y, 20 * Math.random(), 20 * Math.random());
}

封装绘制处理



  • 提供绘制的起点,计算绘制的终点

  • 根据起点和终点进行绘制


// 计算终点
function getEnd(b: BranchType) {
const { start, theta, length } = b;
return {
x: start.x + Math.cos(theta) * length,
y: start.y + Math.sin(theta) * length
};
}
// 绘制整理
function drawBranch(b: BranchType) {
// 绘制树干
lineTo(b.start, getEnd(b), b.lineWidth);
if (Math.random() < 0.4) { // 绘制花朵的密度
drawTmg(image, getEnd(b));
}
if (Math.random() < 0.4) {
drawTmg(imageLeaves, b.start); // 绘制树叶的密度
}
}

绘制树的方法



  • 起点和终点的计算及绘制数的角度计算

  • 绘制左边树和右边树

  • 随机绘制


function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
if (depth < depthNum || Math.random() < 0.5) {
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(),
length: b.length + (Math.random() * 10 - 5),
lineWidth: depthNum - depth
},
depth + 1
);
}
}

动画处理



  • 把所有绘制添加到动画处理中


const pendingTasks: Function[] = []; // 动画数组
function step(b: BranchType, depth: number = 0) {
const end = getEnd(b);
drawBranch(b);
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加左侧动画
step(
{
start: end,
theta: b.theta - 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5), // 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
if (depth < depthNum || Math.random() < 0.5) {
pendingTasks.push(() => { // 添加右侧动画
step(
{
start: end,
theta: b.theta + 0.3 * Math.random(), // 角度变化
length: b.length + (Math.random() * 10 - 5),// 长度变化
lineWidth: depthNum - depth
},
depth + 1
);
});
}
}
function frame() {
const tasks = [...pendingTasks];
pendingTasks.length = 0;
tasks.forEach((fn) => fn());
}
let framesCount = 0;
function satrtFrame() {
requestAnimationFrame(() => {
framesCount += 1;
// if (framesCount % 10 === 0) {
frame();
satrtFrame();
// }
});
}

封装执行方法


useEffect(() => {
function init() {
step(startBranch);
}
satrtFrame();
init();
}, []);

添加常用场景封装



  • 宽高获取当前屏幕大小

  • 屏幕发生变化时进行重新渲染


export const TreeCanvasInner = () => {
const [innerSize, setInnerSize] = useState({ x: window.innerWidth, y: window.innerHeight });
useEffect(() => {
const resizeFunc = () => {
setInnerSize({ x: window.innerWidth, y: window.innerHeight });
};
window.addEventListener('resize', resizeFunc);
return () => {
window.removeEventListener('resize', resizeFunc);
};
}, []);
return (
<TreeCanvas
key={JSON.stringify(innerSize)}
width={innerSize.x}
height={innerSize.y}
startBranch={{ start: { x: 0, y: 0 }, theta: 20, length: 25, lineWidth: 3 }}
/>

);
};

完整代码


生长的树木和花朵


作者:前端了了liaoliao
来源:juejin.cn/post/7309061655095361571
收起阅读 »

为什么很多人不推荐你用JWT?

为什么很多人不推荐你用JWT? 如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。 什么是JWT? 这个是他的官网JSON We...
继续阅读 »

为什么很多人不推荐你用JWT?


如果你经常看一些网上的带你做项目的教程,你就会发现 有很多的项目都用到了JWT。那么他到底安全吗?为什么那么多人不推荐你去使用。这个文章将会从全方面的带你了解JWT 以及他的优缺点。


什么是JWT?


这个是他的官网JSON Web Tokens - jwt.io


这个就是JWT


img


JWT 全称JSON Web Token


如果你还不熟悉JWT,不要惊慌!它们并不那么复杂!


你可以把JWT想象成一些JSON数据,你可以验证这些数据是来自你认识的人。


当然如何实现我们在这里不讲,有兴趣的可以去自己了解。


下面我们来说一下他的流程:



  1. 当你登录到一个网站,网站会生成一个JWT并将其发送给你。

  2. 这个JWT就像是一个包裹,里面装着一些关于你身份的信息,比如你的用户名、角色、权限等。

  3. 然后,你在每次与该网站进行通信时都会携带这个JWT

  4. 每当你访问一个需要验证身份的页面时,你都会把这个JWT带给网站

  5. 网站收到JWT后,会验证它的签名以确保它是由网站签发的,并且检查其中的信息来确认你的身份和权限。

  6. 如果一切都通过了验证,你就可以继续访问受保护的页面了。


JWT Session


为什么说JWT很烂?


首先我们用JWT应该就是去做这些事情:



  • 用户注册网站

  • 用户登录网站

  • 用户点击并执行操作

  • 本网站使用用户信息进行创建、更新和删除 信息


这些事情对于数据库的操作经常是这些方面的



  • 记录用户正在执行的操作

  • 将用户的一些数据添加到数据库中

  • 检查用户的权限,看看他们是否可以执行某些操作


之后我们来逐步说出他的一些缺点


大小


这个方面毋庸置疑。


比如我们需要存储一个用户ID 为xiaou


如果存储到cookie里面,我们的总大小只有5个字节。


如果我们将 ID 存储在 一个 JWT 里。他的大小就会增加大概51倍


image-20240506200449402


这无疑就增大了我们的宽带负担。


冗余签名


JWT的主要卖点之一就是其加密签名。因为JWT被加密签名,接收方可以验证JWT是否有效且可信。


但是,在过去20年里几乎每一个网络框架都可以在使用普通的会话cookie时获得加密签名的好处。


事实上,大多数网络框架会自动为你加密签名(甚至加密!)你的cookie。这意味着你可以获得与使用JWT签名相同的好处,而无需使用JWT本身。


实际上,在大多数网络身份验证情况下,JWT数据都是存储在会话cookie中的,这意味着现在有两个级别的签名。一个在cookie本身上,一个在JWT上。


令牌撤销问题


由于令牌在到期之前一直有效,服务器没有简单的方法来撤销它。


以下是一些可能导致这种情况危险的用例。


注销并不能真正使你注销!


想象一下你在推特上发送推文后注销了登录。你可能会认为自己已经从服务器注销了,但事实并非如此。因为JWT是自包含的,将在到期之前一直有效。这可能是5分钟、30分钟或任何作为令牌一部分设置的持续时间。因此,如果有人在此期间获取了该令牌,他们可以继续访问直到它过期。


可能存在陈旧数据


想象一下用户是管理员,被降级为权限较低的普通用户。同样,这不会立即生效,用户将继续保持管理员身份,直到令牌过期。


JWT通常不加密


因此任何能够执行中间人攻击并嗅探JWT的人都拥有你的身份验证凭据。这变得更容易,因为中间人攻击只需要在服务器和客户端之间的连接上完成


安全问题


对于JWT是否安全。我们可以参考这个文章


JWT (JSON Web Token) (in)security - research.securitum.com


同时我们也可以看到是有专门的如何攻击JWT的教程的


高级漏洞篇之JWT攻击专题 - FreeBuf网络安全行业门户


总结


总的来说,JWT适合作为单次授权令牌,用于在两个实体之间传输声明信息。


但是,JWT不适合作为长期持久数据的存储机制,特别是用于管理用户会话。使用JWT作为会话机制可能会引入一系列严重的安全和实现上的问题,相反,对于长期持久数据的存储,更适合使用传统的会话机制,如会话cookie,以及建立在其上的成熟的实现。


但是写了这么多,我还是想说,如果你作为自己开发学习使用,不考虑安全,不考虑性能的情况下,用JWT是完全没有问题的,但是一旦用到生产环境中,我们就需要避免这些可能存在的问题。


作者:小u
来源:juejin.cn/post/7365533351451672612
收起阅读 »

保守点,90%的程序员不适合做独立开发

大家好,我卡颂。 近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。 但恕我直言,保守点说,90%的程序员不适合做独立开发。 这篇文章全是大实话...
继续阅读 »

大家好,我卡颂。


近两年互联网行业不景气,很多程序员都在寻找新出路。很自然的,独立开发成为一个充满吸引力的选择 —— 背靠自己的开发技能,不用看老板脸色,靠产品养活自己,想想就很美好。


但恕我直言,保守点说,90%的程序员不适合做独立开发。


这篇文章全是大实话,虽然会打破一些人的幻想,但也提供解决方案,希望对迷茫的同学有些帮助。


独立开发赚钱么?


如果你满足如下画像:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


那对你来说,独立开发是不赚钱的。不赚钱并不是说做这事儿一分钱赚不到,满足以上画像的大部分独立开发者在持续经营半年到一年产品后,还是能稳定获得几刀~几十刀收益的。只是相比于付出的心血来说,这点收益实在是低。


以至于出海独立开发圈儿在谈收益时的语境都不是我开发了1年,现在每月能赚50刀,而是我开发了1年,现在拥有了等效于3w刀年化2%的货基(3w * 2% / 12 = 50)


这么一换算,欣慰了许多。


为什么不赚钱?因为独立开发的重点并不在于开发,叫独立产品会更准确些。


对于一款形成稳定变现闭环的产品,有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


程序员只是产品交付环节下的一个工种,与你同处产品交付环节的工种还包括产品经理、QA、项目经理、运维......


独立开发的本质就是你一个人抗下上述所有工种。


话又说回来,如果你即会编程又会流量获取,会运营转化,这样的复合人才在公司根本不用担心被裁,也没必要做独立开发。


所以,对于满足以上画像的同学,我劝你不要把独立开发当作失业后的救命稻草。


认识真实的商业世界


虽然我不建议你all in独立开发,但我建议有空闲时间的同学都去尝试下独立开发。


尝试的目的并不是赚钱,而是更具象的感知流量获取 -> 运营转化 -> 产品交付的路径。


大部分互联网产品往简单了说,都是表格 + 表单的形式,比如推特就是2个大表单(推荐流、关注流)以及描述用户之间关系的表格。


既然如此,当我们有了独立开发的想法时,首先考虑的应该是 —— 我的产品能不能用表格 + 表单 + 高效沟通实现,比如腾讯/飞书文档 + 微信群交流


像多抓鱼(做二手书业务)早期验证需求时,就是几个用户群 + 保存二手书信息的excel表组成。


如果你发现需求靠微信群交流就能解决,付款靠微信转账就能解决,那还有必要写代码开发项目,对接微信支付API么?


当聊到微信交流时,其实就触碰到另一个工种的工作范围了 —— 私域运营。在私域运营看来,通过微信(或其他社交软件)成交是再正常不过的商业模式,但很多程序员是不知道的。


这就是为什么我不建议你把独立开发当作被裁后的救命稻草,但建议有空闲时间的同学都去尝试下独立开发 —— 涉猎其他工种的工作范围,认识真实的商业世界。


当达到这一步后,我们再考虑下一步 —— 发掘你的长处。


发掘你的长处


当我们认识到一款完整的产品有3个最重要的环节:



  • 流量获取

  • 运营转化

  • 产品交付


就应该明白 —— 如果我们想显著提高独立开发的成功率,最好的方式是找到自己最擅长的环节,再和擅长其他环节的人合作。


这里很多程序员有个误区,会认为程序员擅长的肯定就是产品交付下的开发。


实际上,就我交流过的,或者亲自带出来的跑通变现闭环的程序员中,很多人有编程之外的天赋,只是他们没有意识到罢了。


举几个非常厉害的能力(或者说天赋):



  1. 向上突破的能力


有一类同学敢于把自己放到当前可能还不胜任的位置,然后通过不断学习让自己完成挑战。举几个例子:



  • 在不懂地推的时候,参与到校园外卖团队做地推,学习市场和推广的知识

  • 在只看了一本HTML书的情况下,敢直接接下学校建设国际会议网站的任务

  • 在不懂做运营的时候,有老板找他当公司运营负责人,他也接下来,并也做得很好


这类同学很容易跑出有自己特色的非标服务,再包装成产品售卖。



  1. 源源不断的心力支持


有位同学看短视频趋势不错,正好大学也玩过一段时间单反,就买了一套专业的影视设备,准备一边学做饭一边拍短视频,想做一名美食博主。


每天下班拍视频、剪辑加后期的,每个视频都需要花 10+ 个小时。熬了半年多,数据一直不行,就放弃了。


虽然他失败了,但很少有人能在没有正反馈的事上坚持半年,这种源源不断的心力支持其实是一种天赋。


靠这个天赋,只要踩到合适的赛道,成功是迟早的事儿。



  1. 链接人的能力


有些同学特别喜欢在群里唠嗑,与大佬聊天也不犯怵。这就是链接人的天赋


在如今的时代,有价值的信息通常是在小圈子中传播,再慢慢破圈到大众视野中。这类同学靠链接人的天赋,可以:



  1. 从小圈子获得有价值的信息,做信息差生意

  2. 做中间人整合资源


假设你探寻一圈后发现 —— 自己最拿得出手的就是编程能力,那你的当务之急不是发掘需求


以咱们普通程序员的产品sense,也就能想出笔记应用Todo List应用这类点子了......


你需要做的,是多认识其他圈子的人,向他们展示你的编程能力,寻找潜在的需求方


以我在运营的Symbol社区举例,这是个帮程序员发展第二曲线的社群。


之前社群有个痛点:每天社群会产生大量有价值的碎片知识,但这些知识分散在大量聊天消息中,爬楼看消息很辛苦。


基于这个痛点出发,我作为产品经理和群里两位小伙伴合作开发了识别、总结、打标签、分发有价值聊天记录的社群机器人



作为回报,这两位小伙伴将获得付费社群的收入分成。


总结


对于满足如下画像的程序员:



  • 程序员工作多年,编程水平不错

  • 收入完全来源于工资

  • 日常学习的目的是提升技术


不要把独立开发当作被裁后的救命稻草,而应该将其作为认识真实商业世界分工的途径,以及发掘自身优势的手段。


拍脑袋想没有用,只有真正在事儿上修,才能知道自己喜欢什么、擅长什么。


当认清自身优势后,与有其他优势的个体合作,一起构建有稳定收益闭环的产品。




作者:魔术师卡颂
来源:juejin.cn/post/7345756317557047306
收起阅读 »

前端纯css实现-一个复选框交互展示效果

web
纯手工写一个复选框前端交互,这样可以自由定制自己想要展示的字段和设计风格 写这篇文章主要是抛砖引玉,可能存在槽点,大家多多担待! 1.交互效果展示 用码上掘金在线简单的写了一下: 2.简要说明 $primary-color:#1e80ff; // 主题色-掘...
继续阅读 »

纯手工写一个复选框前端交互,这样可以自由定制自己想要展示的字段和设计风格
写这篇文章主要是抛砖引玉,可能存在槽点,大家多多担待!


1.交互效果展示


用码上掘金在线简单的写了一下:



2.简要说明


$primary-color:#1e80ff; // 主题色-掘金蓝


$primary-disable: #7ab0fd; // 只读或禁用色


可以根据实际需求更改主题色,这里的禁用变量色忘记使用了,sorry!!!


image.png


3.布局代码部分


  <!-- page start -->
<div class="ui-layout-page">
<h1>请选择关注类型</h1>
<div class="ui-checkbox">
<!-- 复选框 item start -->
<div
:class="{'ui-item-box':true,'ui-item-check': i.isCheck,'ui-item-disable':i.disable}"
v-for="(i,index) in list"
:key="index"
@click="doCheck(i)">

<img :src="i.icon"/>
<span class="span-bar">
<p class="label-bar">{{i.label}}</p>
<p class="desc-bar">{{i.desc}}</p>
</span>
<!-- 选中标识 start -->
<span
v-if="i.isCheck"
class="icon-check">

</span>
<!-- 选中标识 end -->
</div>
<!-- 复选框 item end -->
</div>
<p style="font-size:12px;color:#333">当前选择ids:{{ this.checked.join(',') }}</p>
</div>
<!-- page end -->

4.方法和数据结构部分



checked:['1','2'],
list:[
{
label:'JYM系统消息',
id:'1',
desc:'关注掘金系统消息',
isCheck:true,
icon:'https://gd-hbimg.huaban.com/6f3e3ff111c6c98be6785d9eddd5b13f8979ef9d1719e-Xwo8QB_fw658webp',
disable:true,
},{
label:'JYM后端',
id:'2',
isCheck:true,
desc:'关注后端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/e2622fe339d655bd17de59fed3b0ae0afb9a16c31db25-YNpnGV_fw658webp',
disable:false,
},{
label:'JYM前端',
id:'3',
isCheck:false,
desc:'关注前端讨论区新消息',
icon:'https://gd-hbimg.huaban.com/80765200aa4ffb7683ddea51c3063b0801874fb86324-3OVCQN_fw1200',
disable:false,
},{
label:'JYM开发工具',
id:'4',
isCheck:false,
desc:'关注开发工具讨论区新消息',
icon:'https://gd-hbimg.huaban.com/ef1c0e1fb2eae73d674aae791526a331b45b26d2b78e-r4p1aq_fw1200',
disable:false,
}
]

/**
* 复选点击事件
* el.disable 禁用状态
* */

doCheck(el){
if(el.disable) return
if(this.checked.includes(el.id)){
el.isCheck = false
this.checked=this.checked.filter(item => item !== el.id);
} else{
el.isCheck = true
this.checked.push(el.id)
}
this.checked.join(',')
}

5.样式控制部分


.ui-layout-page{ 
padding:20px;
h1{
font-size:16px;
}

// 个性化复选框 css start -------------
.ui-checkbox{
width:100%;

$primary-color:#1e80ff; // 主题色-掘金蓝
$primary-disable: #7ab0fd; // 只读或禁用色

// 选中状态css
.ui-item-check{
border:1px solid $primary-color !important;
background:rgba($primary-color,0.05) !important;
}

// 禁用状态css
.ui-item-disable{
border:1px solid #d3d3d3 !important;
background: #f3f3f3 !important;
cursor:not-allowed !important;
.icon-check{
border-top:20px solid #ccc !important;
}
.label-bar{
color:#777 !important;
}
.desc-bar{
color:#a3a3a3 !important;
}
}

// 常规状态css
.ui-item-box{
position:relative;
display:inline-flex;
align-items: center;
width:220px;
height:70px;
border:1px solid #ccc;
cursor: pointer;
margin:0px 8px 8px 0px;
border-radius:4px;
overflow:hidden;

&:hover{
border:1px solid $primary-color;
background:rgba($primary-color,0.05);
}

img{
width:38px;
height:38px;
margin-left:15px;
}
p{
margin:0px;
}
.span-bar{
width:0px;
flex:1 0 auto;
padding:0px 10px;

.label-bar{
font-size:14px;
font-weight:700;
margin-bottom:4px;
color:#333;
}
.desc-bar{
font-size:12px;
color:#999;
}
}
// 绘制圆角斜三角形
.icon-check{
position:absolute;
width:0px;
height:0px;
top:2px;
right:2px;
border-top:20px solid $primary-color;
border-left:25px solid transparent;
border-radius: 5px 3px 5px 0px;
&:after{
content:'✓';
position: relative;
color:#fff;
font-size:12px;
left: -12px;
top: -26px;
}
}
}
}
// 个性化复选框 css end -------------
}

作者:黑白琴键
来源:juejin.cn/post/7412545166539128841
收起阅读 »

CSS 实现呼吸灯

web
引言 在现代前端开发中,为网站添加吸引人的动画效果是提高用户体验的一种常见方式。其中,呼吸灯效果是一种简单而又引人注目的动画,适用于各种应用场景。本文将深入研究如何使用 CSS 来实现呼吸灯效果,包括基本的实现原理、动画参数调整、以及一些实际应用案例。 第一部...
继续阅读 »

引言


在现代前端开发中,为网站添加吸引人的动画效果是提高用户体验的一种常见方式。其中,呼吸灯效果是一种简单而又引人注目的动画,适用于各种应用场景。本文将深入研究如何使用 CSS 来实现呼吸灯效果,包括基本的实现原理、动画参数调整、以及一些实际应用案例。


第一部分:基本的呼吸灯效果


1. 使用关键帧动画


呼吸灯效果的实现依赖于 CSS 的关键帧动画。我们可以使用 @keyframes 规则定义一个简单的呼吸灯动画。


@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

在这个例子中,我们定义了一个名为 breathe 的关键帧动画,包含三个关键帧(0%、50%、100%)。在不同的关键帧,我们分别调整了透明度和缩放属性,从而形成了呼吸灯效果。


2. 应用到元素


接下来,我们将这个动画应用到一个元素上,例如一个 div


<div class="breathing-light"></div>

通过给这个元素添加 breathing-light 类,我们就能够观察到呼吸灯效果的实现。可以根据实际需求调整动画的持续时间、缓动函数等参数。


第二部分:调整动画参数


1. 调整动画持续时间


通过调整 animation 属性的第一个值,我们可以改变动画的持续时间。例如,将动画持续时间改为 5 秒:


.breathing-light {
animation: breathe 5s infinite;
}

2. 调整缓动函数


缓动函数影响动画过渡的方式。可以通过 animation-timing-function 属性来调整。例如,使用 ease-in-out 缓动函数:


.breathing-light {
animation: breathe 3s ease-in-out infinite;
}

3. 调整动画延迟时间


通过 animation-delay 属性,我们可以设置动画的延迟时间。这在创建多个呼吸灯效果不同步的元素时很有用。


.breathing-light {
animation: breathe 3s infinite;
animation-delay: 1s;
}

第三部分:实际应用案例


1. 页面标题的动态效果


在页面的标题上应用呼吸灯效果,使其在页面加载时引起用户的注意。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Title</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1 class="breathing-light">Welcome to Our Website</h1>
</body>
</html>

@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

2. 图片边框的动感效果


通过为图片添加呼吸灯效果,为静态图片增加一些生动感。


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breathing Light Image</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="image-container">
<img src="example-image.jpg" alt="Example Image" class="breathing-light">
</div>
</body>
</html>

@keyframes breathe {
0% {
opacity: 0.5;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.5;
transform: scale(1);
}
}

.breathing-light {
animation: breathe 3s infinite;
}

.image-container {
display: inline-block;
overflow: hidden;
border: 5px solid #fff; /* 图片边框 */
}

结语


通过本文,我们深入探讨了如何使用 CSS 实现呼吸灯效果。从基本原理、动画参数调整到实际应用案例,希望读者能够深刻理解呼吸灯效果的制作过程,并能够在实际项目中灵活运用这一技术,为用户呈现更加生动有趣的页面效果。不仅如此,这也是提升前端开发技能的一种乐趣。


作者:饺子不放糖
来源:juejin.cn/post/7315314479204581391
收起阅读 »

文本美学:text-image打造视觉吸引力

web
当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。 项目介绍 话不多说,我们先看下作者的demo效果: _202...
继续阅读 »

当我最近浏览 GitHub 时,偶然发现了一个项目,它能够将文字、图片和视频转化为文本,我觉得非常有趣。于是我就花了一些时间了解了一下,发现它的使用也非常简单方便。今天我打算和家人们分享这个发现。


项目介绍


话不多说,我们先看下作者的demo效果:


微信截图_20240420194201.png


_20240420194201.jpg


text-image可以将文字、图片、视频进行「文本化」


只需要通过简单的配置即可使用。


虽然这个项目star数很少,但确实是一个很有意思的项目,使用起来很简单的项目。


_20240420194537.jpg


_20240420194537.jpg


github地址:https://github.com/Sunny-117/text-image


我也是使用这个项目做了一个简单的web页面,感兴趣的家人可以使用看下效果:


web地址:http://h5.xiuji.mynatapp.cc/text-image/


_20240420211509.jpg


_20240420211509.jpg


项目使用


这个项目使用起来相对简单,只需按作者的文档使用即可,虽然我前端属于小白的水平,但还是在ai的帮助下做了一个简单的html页面,如果有家人需要的话可以私信我,我发下文件。下边我们就介绍下:



  • 文字「文本化」


先看效果:


_20240420195701.jpg


_20240420195701.jpg


我们在这儿是将配置的一些参数在页面上做了一个可配置的表单,方便我们配置。


家人们想自己尝试的话可以试下以下这个demo。


demo.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
replaceText: '123',
source: {
text: '修己xj',
},
});
</script>
</body>
</html>


  • 图片「文本化」


_20240420200651.jpg


_20240420200651.jpg


demo.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>
<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 7,
isGray: true,
source: {
img: './assets/1.png',
},
});
</script>
</body>

</html>


  • 视频「文本化」


动画1.gif


1.gif


demo.html


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>

<body>
<canvas id="demo"></canvas>
<script src="http://h5.xiuji.mynatapp.cc/text-image/text-image.iife.js"></script>

<script>
textImage.createTextImage({
canvas: document.getElementById('demo'),
raduis: 8,
isGray: true,
source: {
video: './assets/1.mp4',
height: 700,
},
});
</script>
</body>

</html>

需要注意的是:作者在项目中提供的视频的demo这个属性值有错误,我们需要改正后方可正常显示:


_20240420211124.jpg


_20240420211124.jpg


总结


text-image 是一个强大的前端工具,可以帮助用户快速、轻松地将文本、图片、视频转换成文本化的图片,增强文本内容的表现力和吸引力。


作者:修己xj
来源:juejin.cn/post/7359510120248786971
收起阅读 »

把哈希表换成 tire 树,居然为公司省下了几千万

你有没有想过,仅仅省下1%的计算资源,能为一家大公司带来多大的影响?你可能觉得,1%听起来微不足道,完全不值得一提。但今天我们聊一下一个技术优化点,就是关于如何通过微小的优化,Cloudflare这样的大型网络公司如何省下了大量的计算资源,背后还有不少值得我们...
继续阅读 »

你有没有想过,仅仅省下1%的计算资源,能为一家大公司带来多大的影响?你可能觉得,1%听起来微不足道,完全不值得一提。但今天我们聊一下一个技术优化点,就是关于如何通过微小的优化,Cloudflare这样的大型网络公司如何省下了大量的计算资源,背后还有不少值得我们学习的智慧。



你也在为计算资源头疼吗?


如果你是个开发者,尤其是负责维护大规模服务的开发者,你一定对计算资源的消耗有深刻的体会。无论是服务器的 CPU 使用率还是内存消耗,甚至是网络带宽,稍有不慎就可能让成本暴增。而且,问题不止是花钱那么简单,资源浪费还会拖慢你的系统,影响用户体验,最终给公司带来巨大的损失。


我要说的是 Cloudfllare 公司的案例,它不是什么宏大的技术革新或颠覆性的变革,而是从一些不起眼的小地方着手,积少成多,最终实现了1%的节省。这背后到底有什么诀窍?我们一起来看看。


1. 换个数据结构,省时又省力


在Cloudflare的案例中,他们的第一个关键优化是引入了更高效的数据结构。在大规模的数据处理中,数据结构的选择往往是决定性能的关键因素。


他们提到了将原有的哈希表结构换成了更适合他们需求的trie(字典树)结构。


下述是然来的 hash 表结构


// PERF: heavy function: 1.7% CPU time
pub fn clear_internal_headers(request_header: &mut RequestHeader) {
    INTERNAL_HEADERS.iter().for_each(|h| {
        request_header.remove_header(h);
    });
}


这是优化之后的版本


pub fn clear_internal_headers(request_header: &mut RequestHeader) {
   let to_remove = request_header
       .headers
       .keys()
       .filter_map(|name| INTERNAL_HEADER_SET.get(name))
       .collect::<Vec<_>>();


   to_remove.int0_iter().for_each(|k| {
       request_header.remove_header(k);
   });


可以看到,他是先构造了一颗 tire 树,然后在进行操作


那么,为什么是trie呢?因为它能更高效地存储和处理特定类型的数据,特别是字符串相关的操作。每次的字符串查找、匹配操作都能变得更加快速,减少了不必要的计算消耗。



这就好比我们平时在超市找商品,如果货架排布得井井有条,一目了然,那么找起东西来肯定又快又省力。


2. 不只是省电,它还能加速系统响应


有时候,节省计算资源并不仅仅体现在电费账单上,它还直接影响系统的响应时间。你有没有遇到过访问一个网站时,页面加载缓慢,让你心急如焚?这很大程度上与后台的计算效率有关。


Cloudflare在优化trie结构后,明显提升了系统的响应速度。



举个通俗的例子,如果原本的哈希表是一个在黑夜中摸索东西的场景,那优化后的trie结构就是在白天找东西——路径明确,操作直观,不浪费多余的时间。这种提升,虽然看起来只是毫秒级的,但在每天处理数以亿计的请求时,省下来的时间就变得非常可观了。


3. 别小看每一次小改动


也许你会问:“这1%的优化,真有那么重要吗?” Cloudflare给出的答案是肯定的。别看只是1%,但在他们这种大规模系统中,每天的请求数以亿计,这1%就意味着节省了大量的服务器计算资源。试想,如果你每天的电费能减少1%,一年下来呢?这可是一笔不小的费用。


而且,从另一个角度看,任何微小的改动都有可能是更大优化的开始。Cloudflare的工程师们通过这次的优化,深入分析了系统中的其他潜在问题,发掘出更多可以提升的地方。这就像是修车时,你发现一个小问题,结果一修就发现了更多的隐患,最后车子不仅恢复了正常,还比以前跑得更快。


4. 现在,从你的小项目开始优化吧


当然,Cloudflare的规模可能让很多普通开发者觉得遥不可及,但这并不意味着我们不能从中学到东西。你手上的小项目同样可以从数据结构、代码效率等方面着手进行优化。


比如,如果你正在处理大量的字符串数据,不妨考虑一下是否可以用trie这种结构来提升效率。或者你可以先从代码的性能分析开始,看看有没有哪个部分的计算特别耗时。通过一点点的优化,哪怕最终只提升了1%,也会让你长期受益。不过,注意,过早优化,依然是万恶之源,先使用优雅的代码实现,上线后做性能瓶颈分析才是正道,换句话说,一个只有 10 几个 PV的地方,那么是再好的数据结构和算法,也很难体现出商业成本上的价值


5. 未来的路:每一秒都很重要


在如今这个对速度要求极高的互联网时代,每一秒、甚至每一毫秒的节省都至关重要。你以为的“小改动”,可能就是让你的服务脱颖而出的关键。


Cloudflare通过这些小优化,每天节省的计算资源不仅让他们的服务更加高效,也给其他同行树立了一个榜样:技术上的进步不一定是依靠大刀阔斧的改革,有时候,从细微处着手,能带来同样惊人的效果。


不知道,你是否也开始思考自己项目中的那些“隐形”问题了呢?或许,你已经意识到了一些可以优化的地方,但还没来得及动手。那为什么不从今天开始,试着优化一两个小地方呢?也许下次的1%提升,就来自于你的一点点努力。


这个世界上没有小优化,只有还没被发现的优化。


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

华为三折叠手机19999元起!全展开10.2寸大屏3.6mm厚度,电池只留1.9mm

梦晨 发自 凹非寺 量子位 | 公众号 QbitAI 华为三折叠新品发布会,弹幕一片的 “报价吧,让我死心” 。 全球首款三折叠手机MateXT,价格正式揭晓: 256GB版,19999元 512GB版,21999元 1TB版,23999元 预计9月2...
继续阅读 »

梦晨 发自 凹非寺


量子位 | 公众号 QbitAI



华为三折叠新品发布会,弹幕一片的 “报价吧,让我死心”


图片


全球首款三折叠手机MateXT,价格正式揭晓:



  • 256GB版,19999元

  • 512GB版,21999元

  • 1TB版,23999元


预计9月20日上午10点08分正式开售,发布会与苹果同一天,正式开卖也与苹果同一天


看到这里,你是死心了还是动心了?


图片


华为MateXT系列是全球最大折叠屏手机,全展开总计10.2英寸屏幕。


图片


同时,它还是全球最薄折叠屏手机,三屏展开状态只有3.6mm


图片


余承东手持样机在现场表示,“把平板装在口袋里”,这个梦想已经实现了。


作为对比华为平板在售型号最小尺寸为11英寸。


不过也有许多网友表示: “它和平板的区别在于平板我买得起”。


图片


配合官方配件折叠触控键盘,把电脑装在口袋里也实现了。


图片


余承东表示这是一款“大家都能想得到,但是做不出来的产品”。


图片


这样一款全新形态的手机,究竟有哪些使用场景?


发布会现场也做了演示。


三折叠屏都能怎么用


面向商务人士,出差路上也要办公,是华为MateXT这次主打的场景之一。


单屏折叠,扫码登机便于持握。


图片


双屏展开,适合在线浏览简单资讯。


图片


三屏全展开,连复杂的行业报告都能拿捏。


图片


放在一起对比,余承东称三屏模式的实际可视内容在双屏的两倍以上。


图片


除了严肃的工作场景,三屏展开来看电影视觉效果也更震撼。


图片


三折叠除了常规横向使用,华为也考虑到了竖起来使用这种需求,官方支架配件也支持90度旋转。


图片


竖屏短视频刷起来也更带感了。


图片


对于APP的适配方面,余承东开了个玩笑: “小红书秒变大红书”


图片


再接下来还演示了华为系统搭载的一些AI功能。


比如AI总结,可以实现左边原文,右边摘要,无需来回切换。


图片


AI翻译同理,方便跳转回原文。


图片


AI修图能力虽然和三折叠屏没太多联动关系,而且很多人已经上手体验过。


但鉴于前面有几款手机并未召开发布会,华为官方在新机发布会上讲解还是第一次。


图片


三折叠手机还有哪些亮点?


看过三折叠屏都有哪些玩法之后,是时候再了解一下背后的技术突破了。


这是折叠屏铰链系统同时实现内外弯折。


图片


也是超薄大屏第一次实现内外弯折。


图片


余承东讲解“Z”字形内外弯折最大的难点在于,会出现两种互斥的力。


图片


这一次能实现,背后最大助力也在现场揭秘:华为首创的天工铰链系统,实现双轨联动。


具体细节并未透露太多,目前已知采用多向弯折柔性材料,外折部分抗拉伸,内折部分抗挤压。


图片


与之配合,屏幕本身也有特别设计,内侧是业界最大的UTG玻璃,最外层为非牛顿流体材料,双层都有不错的抗冲击能力。


图片


还记得整部手机只有3.6mm厚度么,刨去前后外壳、屏幕、电路板等,留给电池的空间只剩……


1.9mm!


图片


最后,华为高端手机向来重视的影像系统、通信能力这次依然保持高水准。


图片


图片


华为首款三折叠手机,满足你的期待了吗?


作者:量子位
来源:juejin.cn/post/7412813889689272383
收起阅读 »

排行榜--实现点击视图自动滚动到当前用户所在位置.

web
需求 我们今天来实现一下,点击当前用户的div, 自动滚动到用户在排行榜中的位置. 效果 大家可以先看一下下面的GIF, 所实现的效果. 实现 1. 准备DOM 结构 首先,我们在进行列表建设的时候, 需要准备好一个数据. 因为此处我们是使用的vue3来进行...
继续阅读 »

需求


我们今天来实现一下,点击当前用户的div, 自动滚动到用户在排行榜中的位置.


效果


大家可以先看一下下面的GIF, 所实现的效果.


QQ2024817-122950.gif


实现


1. 准备DOM 结构


首先,我们在进行列表建设的时候, 需要准备好一个数据. 因为此处我们是使用的vue3来进行编写. 对于列表我们使用的是v-for列表渲染来做的. 在渲染的时候, 我们需要给每一个列表项(当前就是每一个用户项)添加一个自定义属性. 具体的话, 可以看下 下方的关键代码.


image.png


核心代码就是


 <div v-for="(item, index) in rankingData" :key="item.user.id" :data-key="item.user.id"
</div>



因为数据是后端返回的, 是包含的user_id,而且这个user_id 是不可能重复的. 我们只要保证每个列表的自定义的属性是唯一的即可.



image.png


2. 绑定方法,实现方法


接下来,我们需要考虑的是,在点击的时候,如何获取到当前的dom. 这对我们目前来说就很容易了, 因为我们可以根据据user_id 拿到我们当前点击的dom.


添加一个方法


<!-- 当前用户排名情况 -->
<div class="text-white w-[100%] ...." @click="scrollToCurrentRankingPosition(userId)">

实现方法.


第一步: 拿到rankingList的dom实例.


这里我们通过vue3提供ref拿到dom. 可以看下
模板引用


<div v-else class=" overflow-auto bg-white" ref="rankingList">

const rankingList = ref(null);

第二步: 根据userId获取到具体的DOM


const currentItem = rankingList.value.querySelector(`[data-key="${id}"]`);

第三步: 使用scrollIntoView方法滚动视图到当前选中的元素


 // 平滑滚动到当前元素
currentItem.scrollIntoView({ behavior: 'smooth', block: 'center' });

scrollIntoView方法 讲解:



  • Element 接口的 scrollIntoView() 方法会滚动元素的父容器,使被调用 scrollIntoView() 的元素对用户可见。


简单来讲就是被调用的者的元素出现在用户的视线里面.
scrollIntoView() 方法有三种调用形式:



  1. scrollIntoView():无参数调用,元素将滚动到可视区域顶部,如果它是第一个可见元素。

  2. scrollIntoView(alignToTop):接受一个布尔值参数,决定元素是与滚动区的顶部还是底部对齐。

  3. scrollIntoView(scrollIntoViewOptions):接受一个对象作为参数,提供了更多的滚动选项。


参数



  • alignToTop(可选):布尔值,控制元素滚动到顶部还是底部对齐。默认为 true(顶部对齐)。

  • scrollIntoViewOptions(可选实验性):对象,包含以下属性:



    • behavior:定义滚动行为是平滑动画还是立即发生。可取值有 smooth(平滑动画)、instant(立即发生)或 auto(由CSS的 scroll-behavior 属性决定)。

    • block:定义垂直方向的对齐方式,可取值有 startcenterend 或 nearest。默认为 start

    • inline:定义水平方向的对齐方式,可取值有 startcenterend 或 nearest。默认为 nearest




目前我们实现了效果.


QQ2024817-13328.gif


但是我们发现,还可以继续改进, 目前我们虽然滚动到了屏幕的中间, 但是我们很难去发现. 所以我们可以继续完善一下这个方法. 就是滚动到视图的中间的同时高亮选中的DOM.


3. 额外扩展, 高亮当前的元素


定义一个两个方法,一个用于应用样式, 一个应用于移除样式.


const applyHighlightStyles = (element) => {
element.style.transition = 'background-color 1s ease, border-color 1s ease';
element.style.border = '1px solid transparent'; // 预定义边框样式
element.style.borderColor = '#006cfe'; // 设置边框颜色
element.style.backgroundColor = '#cfe5ff'; // 设置背景色为浅蓝色
};

const removeHighlightStyles = (element) => {
element.style.backgroundColor = ''; // 移除背景色
element.style.borderColor = 'transparent'; // 移除边框颜色
};

然后再在我们之前的方法的后面加入代码


            // 设置高亮显示的样式
applyHighlightStyles(currentItem);

// 清除之前的定时器(如果有)
if (currentItem._highlightTimer) {
clearTimeout(currentItem._highlightTimer);
}

// 设置定时器,2秒后移除高亮显示
currentItem._highlightTimer = setTimeout(() => {
removeHighlightStyles(currentItem);
currentItem._highlightTimer = null;
}, 2000);


然后在组件卸载前记得清除定时器.


onUnmounted(() => {
if (rankingList.value) {
// 遍历所有项目,清除定时器
rankingList.value.querySelectorAll('[data-key]').forEach(item => {
if (item._highlightTimer) {
clearTimeout(item._highlightTimer);
item._highlightTimer = null;
}
});
}
});

效果:
image.png


总结


整体下来的思路就是:



  1. v-for的时候, 给每个循环的元素添加一个自定义的属性.(value:user_id), 不重复且能标识每个元素.

  2. 点击之后,拿到id,透传给方法,然后通过id获取到当前的元素.

  3. 使用Element.scrollIntoView(), 将当前的选中的DOM自动滚动视图的中间.

  4. 高亮显示当前的元素之后(2s)进行取消高亮.


作者:心安事随
来源:juejin.cn/post/7403576996393910308
收起阅读 »

用rust写个flutter插件并上传 pub.dev

今天收到一个需求,要求在flutter端把任意类型的图片,转换成bmp类型的图片,然后把 bmp位图发送到条码打印机,生成的 bmp图片是 1 位深度的,只有黑白两种像素颜色 包已经上传到 pub.dev,pub.dev/packages/ld… 效果图 ...
继续阅读 »

今天收到一个需求,要求在flutter端把任意类型的图片,转换成bmp类型的图片,然后把 bmp位图发送到条码打印机,生成的 bmp图片是 1 位深度的,只有黑白两种像素颜色


包已经上传到 pub.dev,pub.dev/packages/ld…


效果图


image.png
image.png


1.生成插件包


crates.io地址: crates.io/crates/frb_…
安装命令


cargo install frb_plugin_tool

使用很简单,输入frb_plugin_tool即可


image.png


按照提示输入插件名
创建后的项目目录大概像这样


image.png


2. 编写 rust代码


我这里图片转 bmp工具用的是rust image这个包


添加依赖


cd rust && cargo add image

然后在 src/api目录下添加image.rs文件


use std::{io::Cursor, time::Instant};

use bytesize::ByteSize;
use humantime::format_duration;
use image::{GrayImage, Luma};
use indicatif::ProgressBar;
use log::debug;

use super::entitys::{LddImageType, ResizeOpt};

///任意图像转 1 位深度的数据
pub fn convert_to_1bit_bmp(
input_data: &[u8],
image_type: LddImageType,
resize: Option<ResizeOpt>,
is_apply_ordered_dithering: Option<bool>,
) -> Vec<u8> {
let use_ordered_dithering = is_apply_ordered_dithering.map_or(false, |v| v);
let start = Instant::now();
debug!("开始转换,数据大小:{:?}", ByteSize(input_data.len() as u64));
let mut img =
image::load(Cursor::new(input_data), image_type.int0()).expect("Failed to load image");

if let Some(size) = resize {
debug!("开始格式化尺寸:{:?}", size);
img = img.resize(size.width, size.height, size.filter.int0());
debug!("✅格式化尺寸完成");
}

let mut gray_img = img.to_luma8(); // 转换为灰度图像
if use_ordered_dithering {
debug!("✅使用 h4x4a 抖动算法");
gray_img = apply_ordered_dithering(&gray_img);
}

let (width, height) = gray_img.dimensions();
let row_size = ((width + 31) / 32) * 4; // 每行字节数 4 字节对齐
let mut bmp_data = vec![0u8; row_size as usize * height as usize];

// 创建进度条
let progress_bar = ProgressBar::new(height as u64);

// 二值化处理并填充 BMP 数据(1 位深度)
let threshold = 128;
for y in 0..height {
let inverted_y = height - 1 - y; // 倒置行顺序
for x in 0..width {
let pixel = gray_img.get_pixel(x, y)[0];
if pixel >= threshold {
bmp_data[inverted_y as usize * (row_size as usize) + (x / 8) as usize] |=
1 << (7 - (x % 8));
}
}
progress_bar.inc(1); // 每处理一行,进度条增加一格
}

progress_bar.finish_with_message("Conversion complete!");

// BMP 文件头和 DIB 信息头
let file_size = 14 + 40 + 8 + bmp_data.len(); // 文件头 + DIB 头 + 调色板 + 位图数据
let bmp_header = vec![
0x42,
0x4D, // "BM"
(file_size & 0xFF) as u8,
((file_size >> 8) & 0xFF) as u8,
((file_size >> 16) & 0xFF) as u8,
((file_size >> 24) & 0xFF) as u8,
0x00,
0x00, // 保留字段
0x00,
0x00, // 保留字段
54 + 8,
0x00,
0x00,
0x00, // 数据偏移(54 字节 + 调色板大小)
];

let dib_header = vec![
40,
0x00,
0x00,
0x00, // DIB 头大小(40 字节)
(width & 0xFF) as u8,
((width >> 8) & 0xFF) as u8,
((width >> 16) & 0xFF) as u8,
((width >> 24) & 0xFF) as u8,
(height & 0xFF) as u8,
((height >> 8) & 0xFF) as u8,
((height >> 16) & 0xFF) as u8,
((height >> 24) & 0xFF) as u8,
1,
0x00, // 颜色平面数
1,
0x00, // 位深度(1 位)
0x00,
0x00,
0x00,
0x00, // 无压缩
0x00,
0x00,
0x00,
0x00, // 图像大小(可为 0,表示无压缩)
0x13,
0x0B,
0x00,
0x00, // 水平分辨率(2835 像素/米)
0x13,
0x0B,
0x00,
0x00, // 垂直分辨率(2835 像素/米)
0x02,
0x00,
0x00,
0x00, // 调色板颜色数(2)
0x00,
0x00,
0x00,
0x00, // 重要颜色数(0 表示所有颜色都重要)
];

// 调色板(黑白)
let palette = vec![
0x00, 0x00, 0x00, 0x00, // 黑色
0xFF, 0xFF, 0xFF, 0x00, // 白色
];

// 将所有部分组合成 BMP 文件数据
let mut bmp_file_data = Vec::with_capacity(file_size);
bmp_file_data.extend(bmp_header);
bmp_file_data.extend(dib_header);
bmp_file_data.extend(palette);
bmp_file_data.extend(bmp_data);

let duration = start.elapsed(); // 计算耗时
debug!(
"✅转换完成,数据大小:{:?},耗时:{}",
ByteSize(bmp_file_data.len() as u64),
format_duration(duration)
);
bmp_file_data
}

// 有序抖动矩阵(4x4 Bayer 矩阵)
const DITHER_MATRIX: [[f32; 4]; 4] = [
[0.0, 8.0, 2.0, 10.0],
[12.0, 4.0, 14.0, 6.0],
[3.0, 11.0, 1.0, 9.0],
[15.0, 7.0, 13.0, 5.0],
];

//h4x4a 抖动算法
fn apply_ordered_dithering(image: &GrayImage) -> GrayImage {
let (width, height) = image.dimensions();
let mut dithered_image = GrayImage::new(width, height);

for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y)[0];
let threshold = DITHER_MATRIX[(y % 4) as usize][(x % 4) as usize] / 16.0 * 255.0;
let new_pixel_value = if pixel as f32 > threshold { 255 } else { 0 };
dithered_image.put_pixel(x, y, Luma([new_pixel_value]));
}
}

dithered_image
}


生成 dart代码,在项目根目录下执行


flutter_rust_bridge_codegen generate

会在dart lib下生成对应的文件
image.png


在项目中使用


编写 example , main.dart.


import 'dart:io';
import 'dart:typed_data';

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:ldd_bmp/api/entitys.dart';
import 'package:ldd_bmp/api/image.dart';
import 'package:ldd_bmp/ldd_bmp.dart';
import 'dart:async';

import 'package:path_provider/path_provider.dart';

const reSize = ResizeOpt(
width: 200,
height: 200,
filter: LddFilterType.nearest,
);

Future<void> main() async {
await bmpSdkInit();
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
File? file;
Uint8List? bmpData;
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Native Packages'),
),
body: SingleChildScrollView(
child: Column(
children: [
FilledButton(onPressed: selectFile, child: const Text('选择文件')),
if (file != null)
Image.file(
file!,
width: 200,
height: 200,
),
ElevatedButton(
onPressed: file == null
? null
: () async {
final bts = await file!.readAsBytes();
bmpData = await convertTo1BitBmp(
inputData: bts,
imageType: LddImageType.jpeg,
isApplyOrderedDithering: true,
resize: const ResizeOpt(
width: 200,
height: 200,
filter: LddFilterType.nearest,
));
setState(() {});
},
child: const Text("转换")),
ElevatedButton(
onPressed: bmpData == null
? null
: () {
saveImageToFile(bmpData!);
},
child: const Text("保存图片"))
],
),
),
floatingActionButton: bmpData != null
? ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: 300, maxWidth: 300),
child: Card(
elevation: 10,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
const Text('转换结果'),
Image.memory(bmpData!),
],
),
),
),
)
: null,
),
);
}

Future<void> selectFile() async {
setState(() {
file = null;
});
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
file = File(result.files.single.path!);
setState(() {});
} else {
// User canceled the picker
}
}
}

Future<void> saveImageToFile(Uint8List imageData) async {
// 获取应用程序的文档目录
final directory = await getApplicationDocumentsDirectory();

// 设置文件路径和文件名
final filePath = '${directory.path}/image.bmp';

// 创建一个文件对象
final file = File(filePath);

// 将Uint8List数据写入文件
await file.writeAsBytes(imageData);

print('Image saved to $filePath');
}


image.png


转换速度还是挺快的,运行效果


image.png


上传到 pub.dev


这个包已经上传到仓库了,可以直接使用
pub.dev/packages/ld…


作者:梁典典
来源:juejin.cn/post/7412486655862734874
收起阅读 »

开箱即用的web打印和下载,自动分页不截断

web
哈喽哈喽🌈 哈喽大家好!我是小周🤪🤪🤪。相信各位前端小伙伴都知道可以用window.print()这个方法来调用打印机实现打印功能,但是直接下载功能window.print()还是无法实现的。今天我来介绍另外一种实现方式,真正的开箱即用,既可以实现打印和直接下...
继续阅读 »

哈喽哈喽🌈


哈喽大家好!我是小周🤪🤪🤪。相信各位前端小伙伴都知道可以用window.print()这个方法来调用打印机实现打印功能,但是直接下载功能window.print()还是无法实现的。今天我来介绍另外一种实现方式,真正的开箱即用,既可以实现打印和直接下载,也可以防止内容截断。


技术栈


1、html2canvas


html2canvas 一个可以将html转换成canvas的三方库


2、jsPDF


jsPDF 生成pdf文件的三方库


一些用到的方法介绍


1、canvas.getContext('2d').getImageData()


canvas.getContext('2d').getImageData() 是 HTML5 Canvas API 中用于获取指定区域的像素数据的方法。它返回一个 ImageData 对象,该对象包含了指定区域中每个像素的颜色和透明度信息。


canvas.getContext('2d').getImageData(x, y, width, height)

参数说明:



  • x: 采集的图像区域的左上角的水平坐标。

  • y: 采集的图像区域的左上角的垂直坐标。

  • width: 采集的图像区域的宽度(以像素为单位)。

  • height: 采集的图像区域的高度(以像素为单位)。


返回值:


返回一个 ImageData 对象,包含以下属性:



  • width: 图像数据的宽度。

  • height: 图像数据的高度。

  • data: 一个 Uint8ClampedArray 类型的数组,存储了区域中每个像素的颜色和透明度信息。每个像素占用 4 个元素,分别对应:



    • data[n]: 红色通道的值(0 到 255)

    • data[n+1]: 绿色通道的值(0 到 255)

    • data[n+2]: 蓝色通道的值(0 到 255)

    • data[n+3]: 透明度(alpha)通道的值(0 到 255),255 表示完全不透明,0 表示完全透明。




代码实现


1、设置 DomToPdf 类


export class DomToPdf {

_rootDom = null
_title = 'pdf' //生成的pdf标题
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)' //页面背景色
_hex = [0xff, 0xff, 0xff] //用于检测分页的颜色标识

//初始化状态
constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}

}

2、设置 pdf的生成函数


  async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex

return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height

let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')

function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}

3、设置承接pdf的方法


//直接下载pdf
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}

//通过构造链接的形式去跳转打印页面
async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}

完整代码


import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
export class DomToPdf {
_rootDom = null
_title = 'pdf'
_a4Width = 595.266
_a4Height = 841.89
_pageBackground = 'rgba(255,255,255)'
_hex = [0xff, 0xff, 0xff]

constructor(rootDom, title, color = [255, 255, 255]) {
this._rootDom = rootDom
this._title = title
this._pageBackground = `rgb(${color[0]},${color[1]},${color[2]})`
this._hex = color
}
async savePdf() {
const a4Width = this._a4Width
const a4Height = this._a4Height
const hex = this._hex

return new Promise(async (resolve, reject) => {
try {
const canvas = await html2canvas(this._rootDom, {
useCORS: true,
allowTaint: true,
scale: 0.8,
backgroundColor: this._pageBackground,
})
const pdf = new jsPDF('p', 'pt', 'a4')
let index = 1,
canvas1 = document.createElement('canvas'),
height
let leftHeight = canvas.height

let a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
let position = 0
let pageData = canvas.toDataURL('image/jpeg', 0.7)
pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')

function createImpl(canvas) {
if (leftHeight > 0) {
index++
let checkCount = 0
if (leftHeight > a4HeightRef) {
let i = position + a4HeightRef
for (i = position + a4HeightRef; i >= position; i--) {
let isWrite = true
for (let j = 0; j < canvas.width; j++) {
let c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
if (c[0] !== hex[0] || c[1] !== hex[1] || c[2] !== hex[2]) {
isWrite = false
break
}
}
if (isWrite) {
checkCount++
if (checkCount >= 10) {
break
}
} else {
checkCount = 0
}
}
height =
Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
if (height <= 0) {
height = a4HeightRef
}
} else {
height = leftHeight
}
canvas1.width = canvas.width
canvas1.height = height
let ctx = canvas1.getContext('2d')
ctx.drawImage(
canvas,
0,
position,
canvas.width,
height,
0,
0,
canvas.width,
height,
)
if (position !== 0) {
pdf.addPage()
}
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
canvas1.toDataURL('image/jpeg', 1.0),
'JPEG',
0,
0,
a4Width,
(a4Width / canvas1.width) * height,
)
leftHeight -= height
position += height
if (leftHeight > 0) {
setTimeout(createImpl, 500, canvas)
} else {
resolve(pdf)
}
}
}
if (leftHeight < a4HeightRef) {
pdf.setFillColor(hex[0], hex[1], hex[2])
pdf.rect(0, 0, a4Width, a4Height, 'F')
pdf.addImage(
pageData,
'JPEG',
0,
0,
a4Width,
(a4Width / canvas.width) * leftHeight,
)
resolve(pdf)
} else {
try {
pdf.deletePage(0)
setTimeout(createImpl, 500, canvas)
} catch (err) {
reject(err)
}
}
} catch (error) {
reject(error)
}
})
}
async downToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const title = this._title
newPdf.save(title + '.pdf')
setLoadParent(false)
}

async printToPdf(setLoadParent) {
const newPdf = await this.savePdf()
const pdfBlob = newPdf.output('blob')
const pdfUrl = URL.createObjectURL(pdfBlob)
setLoadParent(false)
window.open(pdfUrl)
}

结束


好喽,开箱即用的打印和下载功能的实现就完成了。欢迎大家阅读,我是小周🤪🤪🤪


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

基于 Letterize.js + Anime.js 实现炫酷文本特效

web
如上面gif动图所示,这是一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。基于以上的截图效果可以分析出以下是本次要实现的主要几点:文案呈圆环状扩散开,扩散的同时文字变小文字之间的间距从中心逐个扩散开,间距...
继续阅读 »

如上面gif动图所示,这是一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开。本次文章将解读如何实现这个炫酷的文字效果。

基于以上的截图效果可以分析出以下是本次要实现的主要几点:

  • 文案呈圆环状扩散开,扩散的同时文字变小
  • 文字之间的间距从中心逐个扩散开,间距变大
  • 文案呈圆环状扩散开,扩散的同时文字变大
  • 文字之间的间距从中心逐个聚拢,间距变小
  • 动画重复执行以上4个步骤

实现过程

核心代码实现需要基于一下两个库:

Letterize.js是一个轻量级的JavaScript库,它可以将文本内容分解为单个字母,以便可以对每个字母进行动画处理。这对于创建复杂的文本动画效果非常有用。

使用Letterize.js,你可以轻松地将一个字符串或HTML元素中的文本分解为单个字母,并为每个字母创建一个包含类名和数据属性的新HTML元素。这使得你可以使用CSS或JavaScript来控制每个字母的样式和动画。

anime.js是一个强大的JavaScript动画库,它提供了一种简单而灵活的方式来创建各种动画效果。它可以用于HTML元素、SVG、DOM属性和JavaScript对象的动画。

通过使用Letterize.js以便可以对每个字母进行动画处理,再结合anime.js即可创建各种动画效果。本文不对这两个库做更多的详细介绍,只对本次特效实现做介绍,有兴趣的可以看看官网完整的使用文档。

界面布局

html就是简单的本文标签,也不需要额外的样式,只需要在外层使用flex布局将内容居中,因为本文的长度都是一样的,所以完成后的文本内容就像一个正方形。

<div>
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
......
<div class="animate-me">
letterize.js&anime.js
div>
<div class="animate-me">
anime.js&letterize.js
div>
div>

动画实现

  1. 初始化 Letterize.js,只需要传入 targets 目标元素,元素即是上面的 .animate-me 文本标签。返回的 letterize 是包含所有选中的 .animate-me 元素组数。
const letterize = new Letterize({
targets: ".animate-me"
});
  1. 接下来初始化 anime 库的使用,下面的代码即创建了一个新的anime.js时间线动画。目标是Letterize对象的所有字母。动画将以100毫秒的间隔从中心开始,形成一个网格。loop: true 动画将无限循环。
const animation = anime.timeline({
targets: letterize.listAll,
delay: anime.stagger(100, {
grid: [letterize.list[0].length, letterize.list.length],
from: "center"
}),
loop: true
});

  1. 开始执行动画,首先设置 「文案呈圆环状扩散开,扩散的同时文字变小」,这里其实就是将字母的大小缩小。
animation
.add({
scale: 0.5
})

此时的效果如下所示:

  1. 继续处理下一步动画,「文字之间的间距从中心逐个扩散开,间距变大」,这里处理的其实就是将字母的间距加大,通过设置 letterSpacing 即可,代码如下:
animation
.add({
scale: 0.5
})
.add({
letterSpacing: "10px"
})

此时的效果如下所示:

  1. 后面还有2个步骤,「文案呈圆环状扩散开,扩散的同时文字变大;文字之间的间距从中心逐个聚拢,间距变小」,换做上面的思路也就是将文字变大和将文字间距变小,增加相应的代码如下:
  .add({
scale: 1
})
.add({
letterSpacing: "6px"
});

在线预览

码上掘金地址:

最后

本文通过 Letterize.js + Anime.js 实现了一个很炫酷的文字动画效果,文字的每个字符呈波浪式的扩散式展开和收起。anime.js还有很多的参数可以尝试,有兴趣的朋友可以尝试探索看看~

看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)

参考

动画效果发布者 Wojciech Krakowiak :https://codepen.io/WojciechWKROPCE/pen/VwLePLy


作者:南城FE
来源:juejin.cn/post/7300847292974071859
收起阅读 »

CSS萤火虫按钮特效

web
如图所示,这是一个很炫酷的按钮悬浮特效,鼠标悬停时,按钮呈现发光的效果,周边还出现类型萤火虫的效果。本文将解析如何实现这个按钮特效,基于这个动图可以分析出需要实现的要点: 有一个跟随鼠标移动的圆点 按钮悬停时有高亮发光的效果 悬停时按钮周边的萤火中效果 实...
继续阅读 »


如图所示,这是一个很炫酷的按钮悬浮特效,鼠标悬停时,按钮呈现发光的效果,周边还出现类型萤火虫的效果。本文将解析如何实现这个按钮特效,基于这个动图可以分析出需要实现的要点:



  • 有一个跟随鼠标移动的圆点

  • 按钮悬停时有高亮发光的效果

  • 悬停时按钮周边的萤火中效果


实现过程


跟随鼠标移动的圆点


这个部分需要基于JS实现,但不是最主要的实现代码


如果单纯做一个跟随鼠标移动的点很简单,只需要监听鼠标事件获取坐标实时更新到需要移动的元素上即可。但是仔细看这里的效果并不是这样,圆点是跟随在鼠标后面,鼠标移动停止后圆点才会和鼠标重合。这里是使用了一个名为 Kinet 的库来实现的这个鼠标移动动画效果,具体实现如下:



  1. 创建 Kinet 实例,传入了自定义设置:



  • acceleration: 加速度,控制动画的加速程度。

  • friction: 摩擦力,控制动画的减速程度。

  • names: 定义了两个属性 x 和 y,用于表示动画的两个维度。


 var kinet = new Kinet({
acceleration: 0.02,
friction: 0.25,
names: ["x", "y"],
});


  1. 通过 document.getElementById 获取页面中 ID 为 circle 的元素,以便后续进行动画处理。


var circle = document.getElementById('circle');


  1. 设置 Kinet 的 tick 事件处理:



  • 监听 tick 事件,每当 Kinet 更新时执行该函数。

  • instances 参数包含当前的 x 和 y 值及其速度。

  • 使用 style.transform 属性来更新圆形元素的位置和旋转:

  • translate3d 用于在 3D 空间中移动元素。

  • rotateXrotateY 用于根据当前速度旋转元素。


 kinet.on('tick', function(instances) {
circle.style.transform = `translate3d(${ (instances.x.current) }px, ${ (instances.y.current) }px, 0) rotateX(${ (instances.x.velocity/2) }deg) rotateY(${ (instances.y.velocity/2) }deg)`;
});


  1. 听 mousemove 事件,kinet.animate 方法用于更新 x 和 y 的目标值,计算方式是将鼠标的当前位置减去窗口的中心位置,使动画围绕窗口中心进行。


 document.addEventListener('mousemove', function (event) {
kinet.animate('x', event.clientX - window.innerWidth/2);
kinet.animate('y', event.clientY - window.innerHeight/2);
});

随着鼠标的移动这个圆点元素将在页面上进行平滑的动画。通过 Kinet 库的加速度和摩擦力设置,动画效果显得更加自然,用户体验更加生动。有兴趣的可以尝试调整参数解锁其他玩法,此时我们的页面效果如下:



按钮悬停时发光效果


这里主要通过悬停时设置transition过渡改变按钮的内外阴影效果,阴影效果通过伪元素实现,默认透明度为0,按钮样式代码如下:


.button {
z-index: 1;
position: relative;
text-decoration: none;
text-align: center;
appearance: none;
display: inline-block;
}

.button::before, .button::after {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
border-radius: 999px;
opacity: 0;
transition: opacity 0.3s;
}

.button::before {
box-shadow: 0px 0px 24px 0px #FFEB3B;
}

.button::after {
box-shadow: 0px 0px 23px 0px #FDFCA9 inset, 0px 0px 8px 0px #FFFFFF42;
}

当鼠标悬停在按钮上时,通过改变伪元素的透明度,使发光效果在鼠标悬停时变得可见:


.button-wrapper:hover .button::before, 
.button-wrapper:hover .button::after {
opacity: 1;
}

此时的按钮效果如下:



悬停时萤火中效果


如头部图片所展示,萤火虫效果是有多个圆点散开,所以这里我们添加多个圆点元素。


  class="dot dot-1">
<span class="dot dot-2">span>
<span class="dot dot-3">span>
<span class="dot dot-4">span>
<span class="dot dot-5">span>
<span class="dot dot-6">span>
<span class="dot dot-7">span>

设置元素样式,这里的CSS变量(如 --speed, --size, --starting-x, --starting-y, --rotatation)用于控制圆点的动画速度、大小、起始位置和旋转角度。


.dot {
display: block;
position: absolute;
transition: transform calc(var(--speed) / 12) ease;
width: var(--size);
height: var(--size);
transform: translate(var(--starting-x), var(--starting-y)) rotate(var(--rotatation));
}

给圆点设置动画效果,使用 @keyframes 定义了两个动画:dimFireflyhoverFirefly,为圆点添加了闪烁和移动效果:


@keyframes dimFirefly {
0% { opacity: 1; }
25% { opacity: 0.4; }
50% { opacity: 0.8; }
75% { opacity: 0.5; }
100% { opacity: 1; }
}

@keyframes hoverFirefly {
0% { transform: translate(0, 0); }
12% { transform: translate(3px, 1px); }
24% { transform: translate(-2px, 3px); }
37% { transform: translate(2px, -2px); }
55% { transform: translate(-1px, 0); }
74% { transform: translate(0, 2px); }
88% { transform: translate(-3px, -1px); }
100% { transform: translate(0, 0); }
}

在圆点的伪元素上关联动画效果:


.dot::after {
content: "";
animation: hoverFirefly var(--speed) infinite, dimFirefly calc(var(--speed) / 2) infinite calc(var(--speed) / 3);
animation-play-state: paused;
display: block;
border-radius: 100%;
background: yellow;
width: 100%;
height: 100%;
box-shadow: 0px 0px 6px 0px #FFEB3B, 0px 0px 4px 0px #FDFCA9 inset, 0px 0px 2px 1px #FFFFFF42;
}

给每个圆点设置不同的动画参数,通过使用 CSS 变量,开发者可以灵活地控制每个 .dot 元素的旋转角度,进一步丰富视觉效果。


.dot-1 {
--rotatation: 0deg;
--speed: 14s;
--size: 6px;
--starting-x: 30px;
--starting-y: 20px;
top: 2px;
left: -16px;
opacity: 0.7;
}

.dot-2 {
--rotatation: 122deg;
--speed: 16s;
--size: 3px;
--starting-x: 40px;
--starting-y: 10px;
top: 1px;
left: 0px;
opacity: 0.7;
}
...

此时只要在父元素.button-wrapper悬停时,则触发 .dot 元素的旋转效果,并使其伪元素的动画开始运行,此时萤火中悬停效果就会开始运行。


.button-wrapper:hover {
.dot {
transform: translate(0, 0) rotate(var(--rotatation));
}

.dot::after {
animation-play-state: running;
}
}

最后完成的悬停效果如下:



在线预览



最后


通过以上步骤,结合现代 CSS 的强大功能,我们实现了一个发光的萤火虫圆点悬停按钮效果。这样的效果不仅提升了视觉吸引力,还增强了用户的交互体验。利用 CSS 变量和动画,设计师可以灵活地控制每个元素的表现,使得网页更加生动和引人注目。有兴趣的可以调整相关参数体验其他的视觉效果。




作者:南城FE
来源:juejin.cn/post/7401144423563444276
收起阅读 »

横扫鸿蒙弹窗乱象,SmartDialog出世

前言 但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去体验,就能发现他们有很多离谱的设计和限制,时常就是一边用,一边骂骂咧咧的吐槽 实属无...
继续阅读 »

前言


但凡用过鸿蒙原生弹窗的小伙伴,就能体会到它们是有多么的难用和奇葩,什么AlertDialog,CustomDialog,SubWindow,bindXxx,只要大家用心去体验,就能发现他们有很多离谱的设计和限制,时常就是一边用,一边骂骂咧咧的吐槽


实属无奈,就把鸿蒙版的SmartDialog写出来了


flutter自带的dialog是可以应对日常场景,例如:简单的打开一个弹窗,非UI模块使用,跨页面交互之类;flutter_smart_dialog 是补齐了大多数的业务场景和一些强大的特殊能力,flutter_smart_dialog 对于flutter而言,日常场景是锦上添花,特殊场景是雪中送炭


但是 ohos_smart_dialog 对于鸿蒙而言,日常场景就是雪中送炭!单单一个使用方式而言,就是吊打鸿蒙的CustomDialog,CustomDialog的各种限制和使用方式,我不想再去提及和吐槽了


有时候,简洁的使用,才是最大的魅力


鸿蒙版的SmartDialog有什么优势?



  • 单次初始化后即可使用,无需多处配置相关Component

  • 优雅,极简的用法

  • 非UI区域内使用,自定义Component

  • 返回事件处理,优化的跨页面交互

  • 多弹窗能力,多位置弹窗:上下左右中间

  • 定位弹窗:自动定位目标Component

  • 极简用法的loading弹窗

  • 等等......


目前 flutter_smart_dialog 的代码量16w+,完整复刻其功能,工作量非常大,目前只能逐步实现一些基础能力,由于鸿蒙api的设计和相关限制,用法和相关初始化都有一定程度的妥协


鸿蒙版本的SmartDialog,功能会逐步和 flutter_smart_dialog 对齐(长期),api会尽量保持一致


效果



  • Tablet 模拟器目前有些问题,会导致动画闪烁,请忽略;注:真机动画丝滑流畅,无任何问题


attachLocation


customTag


customJumpPage


极简用法


// dialog
SmartDialog.show({
builder: dialogArgs,
builderArgs: Math.random(),
})

@Builder
function dialogArgs(args: number) {
Text(args.toString()).padding(50).backgroundColor(Color.White)
}

// loading
SmartDialog.showLoading()

安装



ohpm install ohos_smart_dialog 

配置


下述的配置项,可能会有一点多,但,这也是为了极致的体验;同时也是无奈之举,相关配置难以在内部去闭环处理,只能在外部去配置


这些配置,只需要配置一次,后续无需关心


完成下述的配置后,你将可以在任何地方使用弹窗,没有任何限制


初始化



  • 因为弹窗需要处理跨页面交互,必须要监控路由


@Entry
@Component
struct Index {
navPathStack: NavPathStack = new NavPathStack()

build() {
Stack() {
// here: monitor router
Navigation(OhosSmartDialog.registerRouter(this.navPathStack)) {
MainPage()
}
.mode(NavigationMode.Stack)
.hideTitleBar(true)
.navDestination(pageMap)

// here
OhosSmartDialog()
}.height('100%').width('100%')
}
}

返回事件监听



别问我为啥返回事件的监听,处理的这么不优雅,鸿蒙里面没找全局返回事件监听,我也没辙。。。




  • 如果你无需处理返回事件,可以使用下述写法


// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return OhosSmartDialog.onBackPressed()()
}
}

// 路由子页面
struct JumpPage {
build() {
NavDestination() {
// ....
}
.onBackPressed(OhosSmartDialog.onBackPressed())
}
}


  • 如果你需要处理返回事件,在OhosSmartDialog.onBackPressed()中传入你的方法即可


// Entry页面处理
@Entry
@Component
struct Index {
onBackPress(): boolean | void {
return OhosSmartDialog.onBackPressed(this.onCustomBackPress)()
}

onCustomBackPress(): boolean {
return false
}
}

// 路由子页面
@Component
struct JumpPage {
build() {
NavDestination() {
// ...
}
.onBackPressed(OhosSmartDialog.onBackPressed(this.onCustomBackPress))
}

onCustomBackPress(): boolean {
return false
}
}

路由监听



  • 一般来说,你无需关注SmartDialog的路由监听,因为内部已经设置了路由监听拦截器

  • 但是,NavPathStack仅支持单拦截器(setInterception),如果业务代码也使用了这个api,会导致SmartDialog的路由监听被覆盖,从而失效



如果出现该情况,请参照下述解决方案




  • 在你的路由监听类中手动调用OhosSmartDialog.observe


export default class YourNavigatorObserver implements NavigationInterception {
willShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
OhosSmartDialog.observe.willShow?.(from, to, operation, isAnimated)
// ...
}
didShow?: InterceptionShowCallback = (from, to, operation, isAnimated) => {
OhosSmartDialog.observe.didShow?.(from, to, operation, isAnimated)
// ...
}
}

适配暗黑模式



  • 为了极致的体验,深色模式切换时,打开态弹窗也应刷新为对应模式的样式,故需要进行下述配置


export default class EntryAbility extends UIAbility {  
onConfigurationUpdate(newConfig: Configuration): void {
OhosSmartDialog.onConfigurationUpdate(newConfig)
}
}

SmartConfig



  • 支持全局配置弹窗的默认属性


function init() {
// show
SmartDialog.config.custom.maskColor = "#75000000"
SmartDialog.config.custom.alignment = Alignment.Center

// showAttach
SmartDialog.config.attach.attachAlignmentType = SmartAttachAlignmentType.center
}


  • 检查弹窗是否存在


// 检查当前是否有CustomDialog,AttachDialog或LoadingDialog处于打开状态
let isExist = SmartDialog.checkExist()

// 检查当前是否有AttachDialog处于打开状态
let isExist = SmartDialog.checkExist({ dialogTypes: [SmartAllDialogType.attach] })

// 检查当前是否有tag为“xxx”的dialog处于打开状态
let isExist = SmartDialog.checkExist({ tag: "xxx" })

配置全局默认样式



  • ShowLoading 自定样式十分简单


SmartDialog.showLoading({ builder: customLoading })

但是对于大家来说,肯定是想用 SmartDialog.showLoading() 这种简单写法,所以支持自定义全局默认样式



  • 需要在 OhosSmartDialog 上配置自定义的全局默认样式


@Entry
@Component
struct Index {
build() {
Stack() {
OhosSmartDialog({
// custom global loading
loadingBuilder: customLoading,
})
}.height('100%').width('100%')
}
}

@Builder
export function customLoading(args: ESObject) {
LoadingProgress().width(80).height(80).color(Color.White)
}


  • 配置完你的自定样式后,使用下述代码,就会显示你的 loading 样式


SmartDialog.showLoading()

// 支持入参,可以在特殊场景下灵活配置
SSmartDialog.showLoading({ builderArgs: 1 })

CustomDialog



  • 下方会共用的方法


export function randomColor(): string {
const letters: string = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}

export function delay(ms?: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

传参弹窗


export function customUseArgs() {
SmartDialog.show({
builder: dialogArgs,
// 支持任何类型
builderArgs: Math.random(),
})
}

@Builder
function dialogArgs(args: number) {
Text(`${args}`).fontColor(Color.White).padding(50)
.borderRadius(12).backgroundColor(randomColor())
}

customUseArgs


多位置弹窗


export async function customLocation() {
const animationTime = 1000
SmartDialog.show({
builder: dialogLocationHorizontal,
alignment: Alignment.Start,
})
await delay(animationTime)
SmartDialog.show({
builder: dialogLocationVertical,
alignment: Alignment.Top,
})
}


@Builder
function dialogLocationVertical() {
Text("location")
.width("100%")
.height("20%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

@Builder
function dialogLocationHorizontal() {
Text("location")
.width("30%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

customLocation


跨页面交互



  • 正常使用,无需设置什么参数


export function customJumpPage() {
SmartDialog.show({
builder: dialogJumpPage,
})
}

@Builder
function dialogJumpPage() {
Text("JumPage")
.fontSize(30)
.padding(50)
.borderRadius(12)
.fontColor(Color.White)
.backgroundColor(randomColor())
.onClick(() => {
// 跳转页面
})
}

customJumpPage


关闭指定弹窗


export async function customTag() {
const animationTime = 1000
SmartDialog.show({
builder: dialogTagA,
alignment: Alignment.Start,
tag: "A",
})
await delay(animationTime)
SmartDialog.show({
builder: dialogTagB,
alignment: Alignment.Top,
tag: "B",
})
}

@Builder
function dialogTagA() {
Text("A")
.width("20%")
.height("100%")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.backgroundColor(randomColor())
}

@Builder
function dialogTagB() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(["closA", "closeSelf"], (item: string, index: number) => {
Button(item)
.backgroundColor("#4169E1")
.margin(10)
.onClick(() => {
if (index === 0) {
SmartDialog.dismiss({ tag: "A" })
} else if (index === 1) {
SmartDialog.dismiss({ tag: "B" })
}
})
})
}.backgroundColor(Color.White).width(350).margin({ left: 30, right: 30 }).padding(10).borderRadius(10)
}

customTag


自定义遮罩


export function customMask() {
SmartDialog.show({
builder: dialogShowDialog,
maskBuilder: dialogCustomMask,
})
}

@Builder
function dialogCustomMask() {
Stack().width("100%").height("100%").backgroundColor(randomColor()).opacity(0.6)
}

@Builder
function dialogShowDialog() {
Text("showDialog")
.fontSize(30)
.padding(50)
.fontColor(Color.White)
.borderRadius(12)
.backgroundColor(randomColor())
.onClick(() => customMask())
}

customMask


AttachDialog


默认定位


export function attachEasy() {
SmartDialog.show({
builder: dialog
})
}

@Builder
function dialog() {
Stack() {
Text("Attach")
.backgroundColor(randomColor())
.padding(20)
.fontColor(Color.White)
.borderRadius(5)
.onClick(() => {
SmartDialog.showAttach({
targetId: "Attach",
builder: targetLocationDialog,
})
})
.id("Attach")
}
.borderRadius(12)
.padding(50)
.backgroundColor(Color.White)
}

@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}

attachEasy


多方向定位


export function attachLocation() {
SmartDialog.show({
builder: dialog
})
}

class AttachLocation {
title: string = ""
alignment?: Alignment
}

const locationList: Array<AttachLocation> = [
{ title: "TopStart", alignment: Alignment.TopStart },
{ title: "Top", alignment: Alignment.Top },
{ title: "TopEnd", alignment: Alignment.TopEnd },
{ title: "Start", alignment: Alignment.Start },
{ title: "Center", alignment: Alignment.Center },
{ title: "End", alignment: Alignment.End },
{ title: "BottomStart", alignment: Alignment.BottomStart },
{ title: "Bottom", alignment: Alignment.Bottom },
{ title: "BottomEnd", alignment: Alignment.BottomEnd },
]

@Builder
function dialog() {
Column() {
Grid() {
ForEach(locationList, (item: AttachLocation) => {
GridItem() {
buildButton(item.title, () => {
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog
})
})
}
})
}.columnsTemplate('1fr 1fr 1fr').height(220)

buildButton("allOpen", async () => {
for (let index = 0; index < locationList.length; index++) {
let item = locationList[index]
SmartDialog.showAttach({
targetId: item.title,
alignment: item.alignment,
maskColor: Color.Transparent,
builder: targetLocationDialog,
})
await delay(300)
}
}, randomColor())
}
.borderRadius(12)
.width(700)
.padding(30)
.backgroundColor(Color.White)
}

@Builder
function buildButton(title: string, onClick?: VoidCallback, bgColor?: ResourceColor) {
Text(title)
.backgroundColor(bgColor ?? "#4169E1")
.constraintSize({ minWidth: 120, minHeight: 46 })
.margin(10)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
.borderRadius(5)
.onClick(onClick)
.id(title)
}

@Builder
function targetLocationDialog() {
Text("targetIdDialog")
.fontSize(20)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.padding(50)
.borderRadius(12)
.backgroundColor(randomColor())
}

attachLocation


Loading


对于Loading而言,应该有几个比较明显的特性



  • loading和dialog都存在页面上,哪怕dialog打开,loading都应该显示dialog之上

  • loading应该具有单一特性,多次打开loading,页面也应该只存在一个loading

  • 刷新特性,多次打开loading,后续打开的loading样式,应该覆盖之前打开的loading样式

  • loading使用频率非常高,应该支持强大的拓展和极简的使用


从上面列举几个特性而言,loading是一个非常特殊的dialog,所以需要针对其特性,进行定制化的实现


当然了,内部已经屏蔽了细节,在使用上,和dialog的使用没什么区别


默认loading


SmartDialog.showLoading()

loadingDefault


自定义Loading



  • 点击loading后,会再次打开一个loading,从效果图可以看出它的单一刷新特性


export function loadingCustom() {
SmartDialog.showLoading({
builder: customLoading,
})
}

@Builder
export function customLoading() {
Column({ space: 5 }) {
Text("again open loading").fontSize(16).fontColor(Color.White)
LoadingProgress().width(80).height(80).color(Color.White)
}
.padding(20)
.borderRadius(12)
.onClick(() => loadingCustom())
.backgroundColor(randomColor())
}

loadingCustom


最后


鸿蒙版的SmartDialog,相信会对开发鸿蒙的小伙伴们有一些帮助~.~


现在就业环境真是让人头皮发麻,现在的各种技术群里,看到好多人公司各种拖欠工资,各种失业半年的情况


淦,不知道还能写多长时间代码!


004B5DB3


作者:小呆呆666
来源:juejin.cn/post/7401056900878368807
收起阅读 »

我的第一个独立产品,废了,大家来看看热闹哈哈

web
产品想法萌生背景: 我孩子4岁,很喜欢画画,平常在家里,画在纸上,墙的画板上,学习机上,每一次画都很专注,而外出时,有时候会无聊,比如就餐等位,长时间坐高铁,等爸爸剪头发等等场景,一时之间也不好找东西给他玩,于是有了做一个画画小程序给他的想法,同时也觉得会有同...
继续阅读 »

产品想法萌生背景:


我孩子4岁,很喜欢画画,平常在家里,画在纸上,墙的画板上,学习机上,每一次画都很专注,而外出时,有时候会无聊,比如就餐等位,长时间坐高铁,等爸爸剪头发等等场景,一时之间也不好找东西给他玩,于是有了做一个画画小程序给他的想法,同时也觉得会有同样需求的家长,尽管需求场景很小,用的频率很低,但这小程序也许是可以救急的


产品实施:


1.梳理初步想要实现的功能


WX20240909-165957@2x.png


2.开发实现

需求想要的效果都实现了,可随意改变颜色在白板上随意画画,效果如下


WechatIMG2296.jpg


3.更多的想法

实现了画画功能,感觉太单一,于是想到涂色卡和字帖功能,具体如下


WX20240909-171147@2x.png
其实都是“画”这个功能的延伸,实现起来也比较顺利,实现效果如下


WechatIMG2297.jpg


WechatIMG2298.jpg


4.为什么废了?



  • 想要在外出时画画,可以买一个小小的画板,一键清除那种,这样既能画画,还不用看手机,蛮多家长介意看手机的

  • 需要场景很小,很多替代方案,各种小型的益智玩具,绘本等

  • 字帖功能,一般是打印纸质版,练习握笔和书写习惯

  • 欢迎补充哈哈哈


最后想说


虽然产品废了,但从0到1实现了自己的想法,收获还是很多的,我从事Java开发,因为实现这个想法,自学了小程序开发,AI抠图等,在开发过程中,解决了一个又一个开发障碍,最终达到想要的效果,对这个产品实现有任何疑问都可以留言哈,我都会解答!对了,搜索小程序“小乐画板”,就可以体验这款小程序


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

人人都可配置的大屏可视化

web
大屏主要是为了展示数据和酷炫的效果,布局大部分是9宫格,或者在9宫格上做的延伸,现在介绍下 泛积木-低代码 提供的大屏可视化配置。 首先查看效果展示:泛积木-低代码大屏展示,泛积木-低代码大屏展示 此页面注册登录之后可编辑(会定期恢复至演示版本)。 创建页面...
继续阅读 »

大屏主要是为了展示数据和酷炫的效果,布局大部分是9宫格,或者在9宫格上做的延伸,现在介绍下 泛积木-低代码 提供的大屏可视化配置。


首先查看效果展示:泛积木-低代码大屏展示泛积木-低代码大屏展示 此页面注册登录之后可编辑(会定期恢复至演示版本)。


大屏布局组件


创建页面之后,点击进入编辑页面,在可视化编辑器左侧组件往下翻,找到自定义组件中的 大屏布局组件 ,将 大屏布局组件 拖入页面,可以看到下面的成果:


大屏布局组件


拖入的 大屏布局组件 将使用基础配置,并且已经自带了缩放容器组件。


缩放容器组件


缩放容器组件


缩放容器组件主要用于适配不同的尺寸大小,实现原理:缩放容器组件是以该组件的宽度和设计稿的宽度求比例,然后等比例缩放


缩放容器组件支持配置 设计稿宽度、设计稿高度、样式名称、背景颜色,当要适配不同尺寸的屏幕时,我们只需要修改 设计稿宽度、设计稿高度 为对应尺寸即可。样式名称是添加您需要设置的 样式 或添加唯一的classNameclassName作用的元素将作为后续全屏按钮点击时全屏的元素。


全屏按钮组件


全屏按钮组件


全屏按钮组件主要用于配置全屏按钮加全屏元素等。在全屏元素中配置 缩放容器组件 的 唯一className


全屏按钮组件还支持配置 样式名称、字体颜色、字体大小、间距。字体颜色未配置时,会默认从 大屏布局组件 的字体颜色继承。


说完上述两个小组件之后,我们再来说说关键的 大屏布局组件。


大屏布局组件


大屏布局组件


大屏布局组件的配置项可以概括为两部分:



  1. 总体配置:

    1. 总体内容:

      1. 样式名称;

      2. 字体颜色;

      3. 背景颜色;

      4. 背景图片(不想写链接,也可以直接上传);

      5. 是否显示头部;

      6. 模块样式模板;

      7. 样式覆盖;



    2. 页面内容:

      1. 样式名称;

      2. 内间距;





  2. 头部配置:

    1. 头部总体配置:

      1. 标题名称;

      2. 头部背景图片(支持上传);

      3. 样式名称;



    2. 头部左侧:

      1. 左侧内容;

      2. 样式名称;



    3. 头部右侧:

      1. 右侧内容;

      2. 样式名称;



    4. 头部时间:

      1. 是否显示;

      2. 字体大小;

      3. 显示格式。






样式覆盖 填入 css 之后,会自动在组件内创建 style 标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:


.large-screen-layout .large-screen-layout-header {
height: 100px;
}

此时页面头部的高度将由默认的 80px 调整为 100px 。


头部背景图片 未设置时,头部高度默认为 80px ,设置之后,高度为背景图片按照宽度整体缩放之后的高度。


头部左/右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。


例如:


{
...
"headerLeft": {
"type": "tpl",
"tpl": "公司名称",
"id": "u:3dc2c3411ae1"
},
"headerRight": {
"type": "fan-screenfull-button",
"targetClass": "largeScreenLayout",
"fontSize": "22px",
"id": "u:be46114da702"
},
...
}

模块样式模板 用于统一设置 大屏单块模板组件 的样式模板,样式模板是事先定义好的一些简单样式。


大屏单块模板组件


大屏单块模板组件


大屏单块模板组件 是用于配置大屏每块内容,大屏布局组件 和 大屏单块模板组件 之间还有一层 grid-2d 组件


grid-2d
grid-2d


grid-2d 组件 是使用 grid 布局,支持配置 外层 Dom 的类名、格子划分、格子垂直高度、格子间距、格子行间距,建议 大屏布局组件 -> 总体配置 -> 页面内容 -> 内边距 和格子间距设置一致,格子划分 指定 划分为几列,格子间距统一设置横向和纵向的间距,格子行间距可以设置横向间距,优先级高于格子间距。


格子垂直高度 = (缩放容器组件的设计稿高度 - 大屏布局组件头部高度 - 大屏布局组件头部高度页面内容内边距 * 2 - (格子行间距 || 格子间距) * 2) / 3


例如默认的: (1080 - 80 - 20 * 2 - 20 * 2) / 3 = 306.667px


大屏单块模板组件 支持如下配置:



  1. 总体内容:

    1. 样式名称;

    2. 样式模板;

    3. 位置配置;

      1. 起始位置X;

      2. 起始位置Y;

      3. 宽度W;

      4. 高度H;



    4. 是否显示头部;

    5. 样式覆盖;



  2. 模块标题:

    1. 标题名称;

    2. 标题样式;

    3. 字体颜色;



  3. 模块头部右侧:

    1. 右侧内容;

    2. 样式名称;



  4. 模块内容:

    1. 样式名称;

    2. 内边距。




样式覆盖 填入 css 之后,会自动在组件内创建 style 标签添加样式,这个时候需要使用 css 优先级去覆盖默认展示内容,例如:


.fan-screen-card .fan-screen-card-header {
height: 80px;
}

此时模块头部的高度将由默认的 50px 调整为 80px 。 css 会作用于符合 css 的所有DOM元素,如果需要唯一设置,请在前面添加特殊的前缀,例如:


特殊的前缀


.fan-screen-card-1.fan-screen-card .fan-screen-card-header {
height: 80px;
}

样式模板 可单独设置每个模块的样式。


模块头部右侧内容 是配置 SchemaNode , SchemaNode 是指每一个 amis 配置节点的类型,支持模板、Schema(配置)以及SchemaArray(配置数组)三种类型。


位置配置 每项的值都是数值,比如默认的 9 宫格就是 3 * 3,此时设置的值就是 1/2/3 ,宽度1就代表一列,高度1就代表一行。可以调整初始位置、宽度、高度等配置出多种布局方式。


大屏单块模板内容首先嵌套 Service 功能型容器 用于获取数据,再使用 Chart 图表 进行图表渲染。


如果需要轮流高亮 Chart 图表的每个数据,例如 大屏动态展示 可以使用如下配置:



  1. Chart 图表 上添加唯一的 className

  2. 配置 Chart 图表的 config

  3. 配置 Chart 图表的 dataFilter


dataFilter


const curFlag = 'lineCharts';

if (window.fanEchartsIntervals && window.fanEchartsIntervals.get(curFlag)) {
clearInterval(window.fanEchartsIntervals.get(curFlag)[0]);
window.fanEchartsIntervals.get(curFlag)[1] && window.fanEchartsIntervals.get(curFlag)[1].dispose();
}

const myChart = echarts.init(document.getElementsByClassName(curFlag)[0]);
let currentIndex = -1;
myChart.setOption({
...config,
series: [
{
...config.series[0],
data: data.line
}
]
});
const interval = setInterval(function () {
const dataLen = data.line.length;
// 取消之前高亮的图形
myChart.dispatchAction({
type: 'downplay',
seriesIndex: 0,
dataIndex: currentIndex
});
currentIndex = (currentIndex + 1) % dataLen;
// 高亮当前图形
myChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: currentIndex
});
// 显示 tooltip
myChart.dispatchAction({
type: 'showTip',
seriesIndex: 0,
dataIndex: currentIndex
});
}, 1000);

if (window.fanEchartsIntervals) {
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
} else {
window.fanEchartsIntervals = new Map();
window.fanEchartsIntervals.set(curFlag, [interval, myChart]);
}

return config;

修改高亮行 1 curFlag 设置为对应的 Chart 图表的 className,12-17 行是插入数据,22-39 为对应数据的切换展示方式。


增加大屏单块模板


当添加第二个 大屏单块模板 时,直接把第一个复制一份,调整位置、service组件的接口、dataFilter配置等。


至此大屏就配置完成了。


更详细的使用文档可以查看 泛积木-低代码


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

Java 语法糖,你用过几个?

你好,我是猿java。 这篇文章,我们来聊聊 Java 语法糖。 什么是语法糖? 语法糖(Syntactic Sugar)是编程语言中的一种设计概念,它指的是在语法层面上对某些操作提供更简洁、更易读的表示方式。这种表示方式并不会新增语言的功能,而只是使代码更简...
继续阅读 »

你好,我是猿java。


这篇文章,我们来聊聊 Java 语法糖。


什么是语法糖?


语法糖(Syntactic Sugar)是编程语言中的一种设计概念,它指的是在语法层面上对某些操作提供更简洁、更易读的表示方式。这种表示方式并不会新增语言的功能,而只是使代码更简洁、更直观,便于开发者理解和维护。


语法糖的作用:



  • 提高代码可读性:语法糖可以使代码更加贴近自然语言或开发者的思维方式,从而更容易理解。

  • 减少样板代码:语法糖可以减少重复的样板代码,使得开发者可以更专注于业务逻辑。

  • 降低出错率:简化的语法可以减少代码量,从而降低出错的概率。


因此,语法糖不是 Java 语言特有的,它是很多编程语言设计中的一些语法特性,这些特性使代码更加简洁易读,但并不会引入新的功能或能力。


那么,Java中有哪些语法糖呢?


Java 语法糖


1. 自动装箱与拆箱


自动装箱和拆箱 (Autoboxing and Unboxing)是 Java 5 引入的特性,用于在基本数据类型和它们对应的包装类之间自动转换。


// 自动装箱
Integer num = 10; // 实际上是 Integer.valueOf(10)

// 自动拆箱
int n = num; // 实际上是 num.intValue()

2. 增强型 for 循环


增强型 for 循环(也称为 for-each 循环)用于遍历数组或集合。


int[] numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {
System.out.println(number);
}

3. 泛型


泛型(Generics)使得类、接口和方法可以操作指定类型的对象,提供了类型安全的检查和消除了类型转换的需要。


List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 不需要类型转换

4. 可变参数


可变参数(Varargs)允许在方法中传递任意数量的参数。


public void printNumbers(int... numbers) {
for (int number : numbers) {
System.out.println(number);
}
}

printNumbers(1, 2, 3, 4, 5);

5. try-with-resources


try-with-resources 语句用于自动关闭资源,实现了 AutoCloseable 接口的资源会在语句结束时自动关闭。


try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}

6. Lambda 表达式


Lambda 表达式是 Java 8 引入的特性,使得可以使用更简洁的语法来实现函数式接口(只有一个抽象方法的接口)。


List<String> list = Arrays.asList("a", "b", "c");
list.forEach(s -> System.out.println(s));

7. 方法引用


方法引用(Method References)是 Lambda 表达式的一种简写形式,用于直接引用已有的方法。


list.forEach(System.out::println);

8. 字符串连接


从 Java 5 开始,Java 编译器会将字符串的连接优化为 StringBuilder 操作。


String message = "Hello, " + "world!"; // 实际上是 new StringBuilder().append("Hello, ").append("world!").toString();

9. Switch 表达式


Java 12 引入的 Switch 表达式使得 Switch 语句更加简洁和灵活。


int day = 5;
String dayName = switch (day) {
case 1 -> "Sunday";
case 2 -> "Monday";
case 3 -> "Tuesday";
case 4 -> "Wednesday";
case 5 -> "Thursday";
case 6 -> "Friday";
case 7 -> "Saturday";
default -> "Invalid day";
};

10. 类型推断 (Type Inference)


Java 10 引入了局部变量类型推断,通过 var 关键字来声明变量,编译器会自动推断变量的类型。


var list = new ArrayList<String>();
list.add("Hello");

这些语法糖使得 Java 代码更加简洁和易读,但需要注意的是,它们并不会增加语言本身的功能,只是对已有功能的一种简化和封装。


总结


本文,我们介绍了 Java 语言中的一些语法糖,从上面的例子可以看出,Java 语法糖只是一些简化的语法,可以使代码更简洁易读,而本身并不增加新的功能。


学习交流


如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。


作者:猿java
来源:juejin.cn/post/7412672643633791039
收起阅读 »

前端实现:页面滚动时,元素缓慢上升效果

web
效果 实现方式 自定义指令 封装组件 两种方式均可以在SSR页面中使用 方式1:自定义指令实现 import Vue from 'vue'; const DISTANCE = 100; // y轴移动距离 const DURATION = 1000; ...
继续阅读 »

效果


2024-08-11 13.58.04.gif


实现方式



  1. 自定义指令

  2. 封装组件


两种方式均可以在SSR页面中使用


方式1:自定义指令实现


import Vue from 'vue';

const DISTANCE = 100; // y轴移动距离
const DURATION = 1000; // 动画持续时间
const THRESHOLD_FOR_TRIGGERING_ANIMATION = 0.1; // 当元素一部分可见时触发动画

const animationMap = new WeakMap();

function handleIntersection(entries, observer) { // IntersectionObserver 回调函数, 处理元素的可见性变化
for (const entry of entries) { // 遍历所有观察目标
if (entry.isIntersecting) { // 如果目标可见
const animation = animationMap.get(entry.target); // 获取动画对象
if (animation) {
animation.play(); // 播放动画
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
entry.target.classList.add('active');
}
observer.unobserve(entry.target); // 播放一次后停止监听
}
}
}

let ob;
if ('IntersectionObserver' in window) { // 如果浏览器支持 IntersectionObserver
ob = new IntersectionObserver(handleIntersection, { // 创建 IntersectionObserver 对象
threshold: THRESHOLD_FOR_TRIGGERING_ANIMATION // 当元素一部分可见时触发动画
});
} else {
// 回退机制:如果不支持 IntersectionObserver
ob = {
observe(el) { // IntersectionObserver 接口的 observe 方法
el.__onScroll__ = () => { // 监听元素的滚动事件
if (isInViewport(el)) { // 如果元素在视窗内
const animation = animationMap.get(el); // 获取动画对象
if (animation) {
animation.play();
} else {
// 如果不支持 Web Animations API,则使用 CSS 动画回退方案
el.classList.add('active');
}
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
}
};
window.addEventListener('scroll', el.__onScroll__); // 监听元素的滚动事件
},
unobserve(el) { // IntersectionObserver 接口的 unobserve 方法
if (el.__onScroll__) { // 如果元素有滚动事件监听
window.removeEventListener('scroll', el.__onScroll__); // 停止监听
delete el.__onScroll__; // 清理引用
}
}
};
}

function isBelowViewport(el) { // 判断元素是否在视窗下方
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
}

function isInViewport(el) { // 判断元素是否在视窗内
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0;
}

const directive = {
name: 'slide-in',
inserted(el, binding) { // 元素插入到 DOM 时触发
if (!isBelowViewport(el)) { // 如果元素在视窗下方,则不执行动画
console.log('Element is not below viewport');
return;
}

const duration = binding.value && binding.value.duration ? binding.value.duration : DURATION; // 动画持续时间
const animationOptions = { // 动画选项: 目标位置、持续时间、缓动函数
duration: duration,
easing: binding.value && binding.value.easing ? binding.value.easing : 'ease'
};

// 检查是否支持 Web Animations API
let animation;
if (el.animate) { // 如果支持 Web Animations API
animation = el.animate([ // 创建动画对象
{
transform: `translateY(${DISTANCE}px)`,
opacity: 0.5
},
{
transform: 'translateY(0)',
opacity: 1
}
], animationOptions);
animation.pause(); // 初始化时暂停动画
animationMap.set(el, animation); // 保存动画对象
} else {
// 如果不支持 Web Animations API,则添加 CSS 动画回退类
el.classList.add('animate-fallback'); // animate-fallback在下面SCSS中有定义
}

ob.observe(el); // 开始监听元素的可见性变化
},
unbind(el) { // 元素从 DOM 中移除时触发
ob.unobserve(el); // 停止监听元素的可见性变化
}
};

Vue.directive(directive.name, directive);


注册指令


image.png


directives/index.js


import './slide-in' // 元素缓慢上升效果

main.js


import './directives'

在页面中使用


<template>
<div class="boxs .scroll-container">
<div class="slide-box" v-slide-in="{ duration: 500, easing: 'ease-in-out' }">0 - slide-directives</div>
<div class="slide-box" v-slide-in>1 - slide-directives</div>
<div class="slide-box" v-slide-in>2 - slide-directives</div>
<div v-slide-in>3 - slide-directives</div>
<div v-slide-in="{ duration: 500, easing: 'linear' }">4 - slide-directives</div>
<div v-slide-in>5 - slide-directives</div>
<div v-slide-in="{ duration: 500 }">6 - slide-directives</div>
</div>
</template>

<style lang="scss" scoped>
.boxs {
div {
text-align: center;
width: 800px;
height: 300px;
background-color: #f2f2f2;
margin: 0 auto;
margin-top: 20px;
}
}

<!-- 兼容性处理(可放到全局style中) -->
.animate-fallback {
opacity: 0;
transform: translateY(100px);
transition: transform 1s ease, opacity 1s ease;
}

.animate-fallback.active {
opacity: 1;
transform: translateY(0);
}

@keyframes slideIn {
from {
opacity: 0;
transform: translateY(100px);
}

to {
opacity: 1;
transform: translateY(0);
}
}

.animate-fallback-keyframes {
opacity: 0;
animation: slideIn 1s ease forwards;
}
</style>

方式2: 封装为组件


<template>
<div ref="animatedElement" :style="computedStyle">
<slot></slot>
</div>
</template>

<script>
export default {
name: 'slideIn',
props: {
duration: { // 动画持续时间
type: Number,
default: 1000
},
easing: { // 动画缓动效果
type: String,
default: 'ease'
},
distance: { // 动画距离
type: Number,
default: 100
}
},
data() {
return {
hasAnimated: false // 是否已经动画过
}
},
computed: {
computedStyle() {
return {
opacity: this.hasAnimated ? 1 : 0,
transform: this.hasAnimated ? 'translateY(0)' : `translateY(${this.distance}px)`,
transition: `transform ${this.duration}ms ${this.easing}, opacity ${this.duration}ms ${this.easing}`
}
}
},
mounted() {
if (typeof window !== 'undefined' && 'IntersectionObserver' in window) { // 检测是否支持IntersectionObserver
this.createObserver() // 创建IntersectionObserver
} else {
// 如果不支持IntersectionObserver,则使用scroll事件来实现动画
this.observeScroll()
}
},
methods: {
createObserver() {
const observer = new IntersectionObserver(entries => { // IntersectionObserver回调函数
entries.forEach(entry => { // 遍历每个观察目标
if (entry.isIntersecting && !this.hasAnimated) { // 如果目标进入视口并且没有动画过
this.hasAnimated = true // 标记动画过
observer.unobserve(entry.target) // 停止观察
}
})
}, { threshold: 0.1 }) // 观察阈值,表示目标在视口的百分比
observer.observe(this.$refs.animatedElement) // 观察目标
},
observeScroll() {
const onScroll = () => { // scroll事件回调函数
if (this.isInViewport(this.$refs.animatedElement) && !this.hasAnimated) { // 如果目标在视口并且没有动画过
this.hasAnimated = true // 标记动画过
window.removeEventListener('scroll', onScroll) // 停止监听scroll事件
}
}
window.addEventListener('scroll', onScroll) // 监听scroll事件
},
isInViewport(el) { // 判断目标是否在视口
const rect = el.getBoundingClientRect()
return rect.top < window.innerHeight && rect.bottom > 0
}
}
}
</script>

页面使用


<div class="text-slide-in-vue">
<slide-comp v-for="(s ,idx) in list" :key="idx">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<div class="level-slide">
<slide-comp v-for="(s, idx) in list" :key="idx" :duration="500 * idx + 500">
<p>{{ s.text }} - slide-comp</p>
</slide-comp>
</div>

<style>
.text-slide-in-vue {
p {
text-align: center;
width: 400px;
height: 200px;
background-color: goldenrod;
margin: 0 auto;
margin-top: 20px;
}
}

.level-slide {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20px;
p {
text-align: center;
width: 200px;
height: 200px;
background-color: blueviolet;
margin: 0 auto;
margin-top: 20px;
}
}
</style>

作者:Blue啊
来源:juejin.cn/post/7401042923490836480
收起阅读 »

低谷期,什么是最好的理财方式?买房、炒股、存钱?

2024年宏观环境肉眼可见地恶化之下,程序员等普通人如何度过这次危机? 如何度过危机?对于个人最优的方案,是郭嘉最不想看到的方案 对于普通人,度过危机最好的办法是 降低消费、降低负债和多存钱。对于郭嘉而言,这是最不想看到的行为。 个人的最优方案和郭嘉的最优方案...
继续阅读 »

2024年宏观环境肉眼可见地恶化之下,程序员等普通人如何度过这次危机?


如何度过危机?对于个人最优的方案,是郭嘉最不想看到的方案


对于普通人,度过危机最好的办法是 降低消费、降低负债和多存钱。对于郭嘉而言,这是最不想看到的行为。 个人的最优方案和郭嘉的最优方案是相反的。 在经济下行期,郭嘉和个人的利益不一致也很正常。(上行期当然一致了)


记住这一点,不要认为郭嘉都已经提倡了,就认为对自己是最好的,作为理智的成年人要有独立判断能力。


郭嘉希望大家多带款、多消费、少存钱,只有如此需求端提振后,经济才能复苏。但是对于个人而言,外部环境的危机让我们对未来充满不安全感,多带款、多消费、少存钱就是作死的行为……


非必要不要买房


在中国34个省市区和直辖市,我相信绝大部分城市的房产已经是垃圾资产,拿到手里就会成为传家宝,可能永远也卖不出去。


只有极少数一线城市和 优质地段、优质物业、优质小区质量的少量小区或者别墅区存在增值空间。参照日本的经验,经济泡沫破裂后,虽然人口快速向一线城市群东京和大阪聚集,但是东京的房价依然持续在下跌


记住下跌趋势不要抄底,宁可追高,绝不抄底!


image.png


image.png


务必远离股市,尤其是A股


这是我的炒股心路历程,虽然赚了钱,但是差一点点就倾家荡产,万劫不复


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


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


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


炒股就是赌博


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


赌徒不是一天造成的,谁都有赢的时候,无论赚多少,最终都会因为人性的贪婪 走上赌徒的道路。迟早倾家荡产。


之前炒股的经历分享


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


多存钱,降低杠杆和负债率


虽然存钱的利率不到2%,但是总比亏本强很多啊,要明白现在中国的经济困境是 经济通缩,什么意思呢?就是各种工业品都在降价(例如汽车一直在降价),未来的钱更加值钱,所以利率低,希望大家赶紧花出去。


经济通胀的时候,物价在飞速上涨,虽然利率高,但是物价涨得更高,钱越来越不值钱,所以银行希望大家都去存钱。


记住银行希望你干什么,大概率对你是不好的。 银行赚的就是你的钱~


现在越是利率低,越是要存在银行这样才保险。 如果你的路子特别野,可以考虑将资产转成美元或港币,这两种货币的存款利率更加高,可以达到4%+


有房贷的提前还房贷


存量房贷的利率比存款利率高了将近3%, 100万的带款,每年就多3万元的利息,长达30年,将近五六十万元的利差,千万不可小觑。


但是银行会有各种手段限制大家提前还房贷。归根结底,经济下行期,银行的利益和个人的利益是对立的~


第一次提前还房贷,就尝到了甜头,使用6万块钱,起到了18万的效果



我在工行App上,申请 提前还贷,选择缩短 18个月的房贷,只需要 6万2,而我每个月房贷才1万,相当于是用 6 万 顶 18 万的房贷。还有比这更划算的事情吗?



提前还房贷的经历


买房后,害怕失业,更不敢裸辞,心情不好就提前还房贷,缓解焦虑


对于房奴而言,提前还房贷就是最好的投资方式,没有之一,就是最好的投资方式。


欲买桂花同载酒,人生要及时行乐


虽然推荐大家降低消费,但是不建议大家为了省钱,牺牲青春。


100块钱对于一个10岁孩子的快乐,和对于30岁成年人的快乐是完全不对等的。


小时候有10块钱,够我买三四个玩具,可以和小伙伴开开心心的玩一个暑假。现在我有1000张10块钱,也找不回儿时的快乐。


人活着是为了享受人生的,不是为了受罪来的。建议大家 可持续性的及时行乐,该玩还是要玩。不要老了感慨道:欲买桂花同载酒,终不似,少年游


好好学习,提高自己,度过危机期,遍地是机会


经济危机过后,资产的价格一定一落千丈,各行各业都非常萧条,但是随着需求复苏,这意味着遍地都是机会。前提是你有发现机会的眼光、抓住机会的能力和勇于行动的魄力。


想一想,危机过后,我们手握大量的现金,面对遍地的廉价资产,面对日渐热情的消费需求,再加上更加成熟强大的自我,一定大有可为。 前提是好好存钱,好好积累提高自己。(如何判断危机过去,是一门学问,不要太冲动)


要想做到这一切,一定要注重低谷期、危机期的积累。


祝各位长期有耐心,把未来的信心全部放在投资自己上,不要把未来的信心投资在股票和、产和奢侈消费上哦~


祝未来的大家纵情四海、前途似锦


2024 七夕随笔


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

手把手教你打造一个“蚊香”式加载

web
前言 这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。 效果预览 从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。 HTML布局 首先我们通过1...
继续阅读 »

前言


这次给大家带来的是一个类似蚊香加载一样的效果,这个效果还是非常具有视觉欣赏性的,相比前几篇文章的CSS特效,这一次的会比较震撼一点。


效果预览



从效果上看感觉像是一层层蚊香摞在一起,通过动画来使得它们达到3D金钟罩的效果。


HTML布局


首先我们通过15span子元素来实现金钟罩的每一层,用于创建基本结构。从专业术语上讲,每个span元素都代表加载动画中的一个旋转的小点。通过添加多个span元素,可以创建出一串连续旋转的小点,形成一个加载动画的效果。


<div class="loader">
<span></span>
// 以下省略15span元素
</div>

CSS设计


完成了基本的结构布局,接下来就是为它设计CSS样式了。我们一步一步来分析:


首先是类名为loaderCSS类,相关代码如下。


.loader{
position: relative;
width: 300px;
height: 300px;
transform-style: preserve-3d;
transform: perspective(500px) rotateX(60deg);
}

我们将元素的定位方式设置为相对定位,使其相对于其正常位置进行定位。然后定义好宽度和高度之后,设置元素的变换样式为preserve-3d,这样可以元素的子元素也会受到3D变换的影响。除此之外,还需要transform属性来设置元素的变换效果。这里的perspective(500px)表示以500像素的视角来观察元素,rotateX(60deg)则表示绕X轴顺时针旋转60度。


这样就将一个宽高都定义好的元素进行了透视效果的3D旋转,使其以60度角度绕X轴旋转。


loader类可以理解为父容器,接下来就是loader类中的子元素span


.loader span{
position: absolute;
display: block;
border: 5px solid #fff;
box-shadow: 0 5px 0 #ccc,
inset 0 5px 0 #ccc;
box-sizing: border-box;
border-radius: 50%;
animation: animate 3s ease-in-out infinite;
}

通过以上样式,我们可以创建一个圆形的动画效果,边框有阴影效果,并且以动画的方式不断旋转。关于CSS部分大部分都是一样的,这里主要介绍一下这里定义的动画效果。名称为animate,持续时间为3秒,缓动函数为ease-in-out,并且动画无限循环播放。


@keyframes animate {
0%,100%{
transform: translateZ(-100px);
}
50%{
transform: translateZ(100px);
}
}

这是一个关键帧动画。关键帧是指动画在不同时间点上的状态或样式。首先该动画名为animate,它包含了三个时间点的样式变化:


0%100% 的时间点,元素通过transform: translateZ(-100px)样式将在Z轴上向后移动100像素,这将使元素远离视图。


50% 的时间点,元素通过transform: translateZ(100px)样式将在Z轴上向前移动100像素。这将使元素靠近视图。


通过应用这个动画,span元素将在动画的持续时间内以一定的速率来回移动,从而产生一个视觉上的动态效果。


最后就是单独为每个子元素span赋予样式了。


.loader span:nth-child(1){
top: 0;
left: 0;
bottom: 0;
right: 0;
animation-delay: 1.4s;
}
.loader span:nth-child(2){
top: 10px;
left: 10px;
bottom: 10px;
right: 10px;
animation-delay: 1.3s;
}
......
以下省略到第15span元素

第一个span元素的样式设置了top、left、bottom和right属性为0,这意味着它会填充父元素的整个空间。它还设置了animation-delay属性为1.4秒,表示在加载动画开始之后1.4秒才开始播放动画。


后面14span元素都是按照这个道理,以此类推即可。通过给span元素的动画延迟属性的不同设置,可以实现加载动画的错落感和流畅的过渡效果。


总结


以上就是整个效果的实现过程了,通过设计的动画来实现这个蚊香式加载,整体还是比较简单的。大家可以去码上掘金看看完整代码,然后自己去尝试一下,如果有什么创新的地方或者遇到了什么问题欢迎在评论区告诉我~


作者:一条会coding的Shark
来源:juejin.cn/post/7291951762948259851
收起阅读 »

2024 年排名前 5 的 Node.js 后端框架

web
自 2009 年以来,Node.js 一直是人们谈论的话题,大多数后端开发人员都倾向于使用 Node.js。在过去的几年里,它的受欢迎程度有所增加。它被认为是美国最受欢迎的网络开发工具,包括 Netflix 和 PayPal 等客户。 受欢迎程度增加的原因是...
继续阅读 »

自 2009 年以来,Node.js 一直是人们谈论的话题,大多数后端开发人员都倾向于使用 Node.js。在过去的几年里,它的受欢迎程度有所增加。它被认为是美国最受欢迎的网络开发工具,包括 Netflix 和 PayPal 等客户。



受欢迎程度增加的原因是加载时间的减少和性能的提高。因此,分析 2024 年排名前 5 的 Node.js 后端框架至关重要。本文将介绍 2024 年排名前 5 的 Node.js 后端框架、它们的功能和常见用例。


Express.js:久经考验的冠军


Express.js 是 Node.js 最著名的后端框架之一。它是一个开源 Web 应用程序框架,可免费使用并构建在 Node.js 平台上。由于它是一个最小的框架,新手和经验丰富的 Web 开发人员都倾向于使用 Express.js。它主要用于创建 Web 应用程序和 RESTful API。



高效路由:Express.js 提供了一种干净、简单的方法来管理各种 HTTP 请求并将它们分配给特定任务,让我们看一个例子。



中间件支持:Express.js 允许中间件支持处理 HTTP 请求。让我们看一个创建用于记录 HTTP 请求详细信息的中间件的简单示例。



轻松的数据库集成:Express.js 与数据库无关。它不强制执行特定的数据库选择。开发者可以选择自己喜欢的数据库。将数据库与 Express.js 集成很容易,因为它具有模块化和灵活的特性,以及提供数据库连接的丰富的 npm 包生态系统。


简单易学:Express.js 以其简单和简约的设计而闻名,这使得开发人员很容易学习,特别是如果他们已经熟悉 JavaScript 和 Node.js。


另外,您可以使用 Bit 等工具轻松开始使用 Express.js 。如果您以前没有使用过 Bit,那么它是可组合软件的下一代构建系统。Express.js 本身本质上是可组合的,您可以在应用程序中的任何位置即插即用组件。


Nest.js:现代且结构化的方法


Nest.js 是一个以构建可扩展且高效的 Node.js 服务器端应用程序而闻名的框架。它使用渐进式 JavaScript,并具有用 TypeScript 编写代码的能力。尽管它完全支持 TypeScript,但它可以用纯 JavaScript 编写代码,包括面向对象编程、函数式编程和函数式响应式编程。



模块化:Nest.js 允许将代码分解为单独的可管理模块,从而使其更易于维护。让我们看一下下面的模块。



这个 PaymentModule 可以导出到其他模块。在此示例中,我们在该模块内导出了通用的缓存模块。由于 Nest.js 具有模块结构,因此易于维护。


可扩展:Nest.js 通过将应用程序分解为可管理的模块、支持灵活的组件替换以及通过微服务和异步操作容纳高流量来实现无缝扩展。它确保有效处理增加的工作量,同时保持可靠性。


依赖注入:依赖注入只是向类添加外部依赖项的方法,而不是在类本身中创建它。让我们看一个例子。



我们创建 PaymentService 并添加了 @Injectable() 注释以使其可注入。我们可以使用创建的服务,如下所示。



类型安全:Nest.js 使用 TypeScript 提供类型安全,可用于捕获开发过程中潜在的错误并提高代码的可维护性。


Koa.js:优雅且轻量


Koa.js 是一个更小、更具表现力的 Web 框架,也是由 Express.js 团队设计的。它允许您通过利用异步函数来放弃回调并处理错误。



上下文对象(ctx):Koa.js 包含一个名为 ctx 的功能来捕获请求和响应详细信息。该上下文被传递给每个中间件。在此示例中,我们从 ctx 对象记录了method 和 request。



中间件组成:与 Express.js 非常相似,Koa 支持处理 HTTP 请求和响应的中间件功能。在此示例中,我们创建了一个简单的中间件。



async/await 支持:Koa 使用 async/await 语法以更同步的方式编写异步代码。下面的示例包含使用 async/await 关键字。



Hapi.js


Hapi.js 是 Http-API 的缩写,是一个用于开发可扩展 Web 应用程序的开源框架。Hapi.js 最基本的用例之一是构建 REST API。沃尔玛实验室创建了 hapi js 来处理黑色星期五等活动的流量,黑色星期五是美国日历上在线购物最繁忙的日子之一。



配置驱动设计:使用配置对象,我们可以在 Hapi.js 中配置路由、设置和插件。



强大的插件系统:Hapi.js 允许轻松集成插件,让我们看一个例子。在这个例子中,我们集成了两个插件,可以使用 key 将选项传递给插件 options



认证与授权:Hapi.js 提供了对各种身份验证策略的内置支持,并允许开发人员轻松定义访问控制策略。



输入验证:输入验证是 Hapi.js 的另一个重要方面。在 options 路由的对象中,我们可以定义哪些输入需要验证。默认 validate 对象由以下值组成。


Adonis.js


Adonis.js 是 Node.js 的全功能 MVC 框架。它具有构建可扩展且可维护的应用程序的能力。 Adonis.js 遵循与 Laravel 类似的结构,并包含 ORM、身份验证和开箱即用的路由等功能。



全栈 MVC 框架:Adonis.js 遵循 MVC 架构模式。拥有 MVC 框架有助于组织代码并使其更易于维护和扩展。



数据库集成 ORM:Adonis.js 有自己的 ORM,称为 Lucid。 Lucid 提供了一个富有表现力的查询构建器并支持各种数据库系统。在 Lucid 中,我们可以创建模型来读取和写入数据库。让我们看下面的例子。



认证系统:Adonis.js 内置了对用户身份验证和授权的支持。它提供了一组用于处理用户会话、密码散列和访问控制的方法和中间件。


结论


2024年,上述后端框架在市场上屹立不倒。无论您选择 Express.js 是为了简单,Nest.js 是为了结构,Adonis.js 是为了生产力,还是 Koa.js 是为了优雅,选择正确的框架至关重要。这始终取决于您的要求。


了解您的项目需求至关重要,并在此基础上选择合适的框架。此外,寻找最新趋势、现有框架的新功能和新框架对于 2024 年后端开发之旅的成功至关重要。


参考链接:blog.bitsrc.io/top-5-nodej…


作者:一纸忘忧
来源:juejin.cn/post/7350581011262373928
收起阅读 »

BOE·IPC电竞大赛暨BOE无畏杯S2完美收官 BOE(京东方)竖立电竞产业生态新标杆

9月6日,作为BOE IPC·2024大会的压轴大戏,BOE无畏杯《无畏契约》2024挑战赛总决赛在北京亦庄电子竞技中心成功举办,这也标志着BOE IPC·2024大会的各项议程也圆满收官。本届BOE无畏杯以“屏实力,竞无畏”为主题,承接了2023年首届BOE...
继续阅读 »

9月6日,作为BOE IPC·2024大会的压轴大戏,BOE无畏杯《无畏契约》2024挑战赛总决赛在北京亦庄电子竞技中心成功举办,这也标志着BOE IPC·2024大会的各项议程也圆满收官。本届BOE无畏杯以“屏实力,竞无畏”为主题,承接了2023年首届BOE无畏杯挑战赛的热潮,历经27天,最终CCG队获得总冠军。BOE无畏杯《无畏契约》挑战赛也是BOE(京东方)联合虎牙直播及“Best of Esports电竞高阶联盟”成员自办的标杆级第三方赛事,该赛事的成功举办凸显了BOE(京东方)以创新技术推动中国电竞产业高质量发展的决心,也展现了尖端显示技术与电竞场景的完美融合,BOE(京东方)正在构建一个开放、创新、共赢的电竞生态系统,为中国电竞产业的未来发展注入活力。决赛现场,BOE(京东方)总裁高文宝博士、BOE(京东方)副总裁刘毅、虎牙直播商业化副总裁焦阳、京东集团3C数码事业群电脑组件业务部总经理蔡欣洋等也纷纷亲临助阵。

在开幕环节,高文宝博士表示:“电竞是年轻人的活动,年轻人有着活跃的思想和强大的创造力。近年来,BOE(京东方)通过BOE无畏杯和ChinaJoy等活动加深了和年轻人的沟通,与年轻人成为了真诚的伙伴和挚友,在这个过程中,BOE(京东方)也激发了新的创造灵感,做出更好更惊艳的产品。未来BOE(京东方)还会持续在技术、产品、活动等方面,与合作伙伴一起带来异彩纷呈的电竞体验,带动电竞产业链的价值提升,助力中国电竞再创高峰。”

作为当下新兴数字文化的代表,电竞已从一种娱乐活动升级为一项参与者众多的竞技运动。从电竞入亚、电竞世界杯,到明年即将举办的首届电竞奥运会,无不呈现出大家对电竞的热情。实际上,作为全球领先的物联网创新企业与半导体显示领域领导者,BOE(京东方)在电竞领域耕耘已久。2021年,京东方首次携手虎牙直播亮相CJ大展,成功破圈;2022年,BOE(京东方)推出行业内首款电竞体验舱BBBBOX,同年开启 BOE 王者杯·王者荣耀挑战赛;2023年6月,BOE(京东方)携手京东,联合终端品牌伙伴等成立“Best of Esports电竞高阶联盟”,吸引了AGON爱攻、ASUS、海信、拯救者、机械师、机械革命、微星MSI、ROG、创维、雷神、红魔、一加、BenQ等13家头部品牌陆续加盟,行业影响力和号召力持续提升;2024年伊始,BOE(京东方)与JDG京东电子竞技俱乐部达成全面品牌战略合作,进一步深化其在电竞垂类领域的布局……这一系列战略性举措,都是BOE(京东方)实现跨界融合,连接年轻一代、驱动技术创新、促进文化交流的重要体现。

今年举办的BOE无畏杯总决赛活动上,BOE(京东方)还特别打造"Best of Esports电竞高阶联盟"产品展区,集中展现了联盟伙伴们最新的电竞产品和尖端技术。依托于BOE(京东方)自主研发的ADS Pro、a-MLED等创新技术赋能,AGON爱攻 AG275QZW显示器支持260Hz超高刷新率,以1ms GTG疾速响应时间为玩家提供高帧率、低延迟的游戏画质,确保流畅丝滑的游戏体验;ROG枪神8 Plus超竞版笔记本,支持60-240Hz动态调频刷新率及3ms极速响应,玩家操作无比顺畅……一系列电竞黑科技产品凭借高清流畅的显示画面和酷炫的科技外观吸引了现场粉丝纷纷体验,为观众们呈现了一场融合竞技与科技的盛宴。

总决赛现场更是异彩纷呈,现场Coser开场秀、无畏契约水友赛等丰富的互动环节点燃了现场观众的热情,更有BOE无畏契约战队对战JDG无畏契约战队表演赛,BOE战队面对职业战队分毫不让、竞出风采,让决赛前的氛围达到了高潮。在总决赛启动仪式上,BOE(京东方)副总裁刘毅、虎牙直播商业化副总裁焦阳、京东集团3C数码事业群电脑组件业务部总经理蔡欣洋一起揭开BOE无畏杯《无畏契约》2024挑战赛总决赛的帷幕,总决赛最终在上一届亚军津门飞鹰战队与CCG战队的较量中展开对决,经过3局激战,CCG队获得最终胜利,拿下本届赛事的冠军。

多年来,BOE(京东方)以技术创新为驱动,通过高刷新率、护眼科技等技术产品优势、广泛的合作以及强大的品牌影响力,从技术、产品、生态等多个方面助力电竞产业发展,获得了众多全球一线客户的支持和好评,引领了整个电竞产业的升级和变革。未来,BOE(京东方)将继续秉持"Powered by BOE"的生态理念,充分发挥"Best of Esports电竞高阶联盟"在全业态布局、资源聚合和技术领先等方面的优势,通过持续不断的技术创新和产业链整合,为我国电竞生态贡献力量,为数字经济的高质量发展注入新的动力。

收起阅读 »

uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar

web
前言 在UniApp的开发小程序过程中,为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制,能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3+Vite+Ts+Pinia+Uni-ui的小程序项目 最终...
继续阅读 »

前言



在UniApp的开发小程序过程中,为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制,能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3+Vite+Ts+Pinia+Uni-ui的小程序项目



最终效果



  • 1、司机角色:
    在这里插入图片描述

  • 2、供应商角色:
    在这里插入图片描述

  • 3、司机且供应商角色:
    在这里插入图片描述


目前常规的实现方式,大多数都是封装一个tabbar组件,在需要显示tabbar的页面添加这个组件,在根据一个选中的index值来切换选中效果。



而我的实现方式:把所有有tabbar的页面全部引入在一个tabbarPage页面,根据角色userType,来动态显示页面



实现思路



1、常规登录:通过微信登录获取code
2、根据code获取openId
3、根据openId获取token,若token存在表:此用户已经登陆/绑定过,则根据token获取用户信息,根据角色直接进入项目页面;若token不存在,则跳转到登录页面
4、登录成功后,调用用户信息接口,根据角色直接进入项目页面



1、以下是封装了一个useLogin的hooks



export const useLogin = () => {
const { proxy } = getCurrentInstance() as any
//常规登录
const judgmentLogin = () => {
uni.login({
provider: 'weixin', //使用微信登录
success: async (loginRes) => {
// 根据微信登录的code获取openid
const res = await proxy.$api.getOpenid({ code: loginRes.code })
if (res.success) {
// console.log('res.data.openid----', res.data.openId)
// 根据openid获取token
openidLogin(res.data.openId)
// 存储openid
uni.setStorageSync('openId', res.data.openId)
}
}
});
}
// 登录过的用户再次进入根据openid获取token,有token则直接进入当前用户的页面,没有则进入登录页面
const openidLogin = (async (openId: string) => {
// console.log('openId----', openId)
const res = await proxy.$api.openIdLogin({ openId })
if (res.success) {
if (res.data) {
// 存储token
uni.
setStorageSync('token', res.data)
userInfo(openId)
}
else {
uni.
navigateTo({
url: '/pages/login/login'
}
)
}
}
}
)
// 登录成功后(有token后)根据openid获取用户信息
const userInfo = (async (openId: any) => {
const res = await proxy.$api.getUserInfo({ openId })
if (res.success) {
console.log('获取登陆用户信息', res.data)
uni.
setStorageSync('userInfo', JSON.stringify(res.data))
const userTypeList = ['scm_driver', 'scm_supplier', 'supplier_and_driver']
// 遍历角色数组来存储当前用户的角色。此角色为userTypeList中的某一个并且此数组只能存在一个userTypeList里面的角色,不会同时存在两个
res.
data.roles.map((item: any) => {
if (userTypeList.includes(item.roleKey)) {
uni.
setStorageSync('userType', item.roleKey)
}
}
)
// 判断角色数组中只要有一个角色在userTypeList中,则进入当前用户的角色页面,否则进入无权限页面
const flag = res.data.roles.some((item: any) => {
return userTypeList.includes(item.roleKey)
}
)
// console.log('flag----', flag)
if (flag && userTypeList.includes(uni.getStorageSync('userType'))) {
setTimeout(() => {
uni.
reLaunch({
url: '/pages/tabbarPage/tabbarPage'
}
)
},
500)
}
else {
uni.
showToast({
icon: 'none',
title: '当前用户角色没有权限!'
}
)
}
}
}
)
return {
judgmentLogin,
userInfo
}
}

2、修改page.json中的tabBar


"tabBar": {
"color": "#a6b9cb",
"selectedColor": "#355db4",
"list": [
{
"pagePath": "pages/supplierMyorder/supplierMyorder"
},
{
"pagePath": "pages/driverMyorder/driverMyorder"
},
{
"pagePath": "pages/mycar/mycar"
},
{
"pagePath": "pages/driverPersonal/driverPersonal"
}
]
},

3、关键页面tabbarPage.vue


<template>
<div class="tabbar_page flex-box flex-col">
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierMyorder'"
>

<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'scm_supplier'"
v-show="active === 'supplierPersonal'"
>

<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverMyorder'"
>

<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'mycar'"
>

<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'scm_driver'"
v-show="active === 'driverPersonal'"
>

<driver-personal
ref="driverPersonal"
:show="active === 'driverPersonal'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierMyorder'"
>

<supplier-myorder
ref="supplierMyorder"
:show="active === 'supplierMyorder'"
/>

div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'driverMyorder'"
>

<driver-myorder ref="driverMyorder" :show="active === 'driverMyorder'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'mycar'"
>

<mycar ref="mycar" :show="active === 'mycar'" />
div>
<div
class="page_wrap"
v-if="userType === 'supplier_and_driver'"
v-show="active === 'supplierPersonal'"
>

<supplier-personal
ref="supplierPersonal"
:show="active === 'supplierPersonal'"
/>

div>
<view class="tab">
<view
v-for="(item, index) in tabbarOptions"
:key="index"
class="tab-item"
@click="switchTab(item, index)"
>

<image
class="tab_img"
:src="currentIndex == index ? item.selectedIconPath : item.iconPath"
>
image>
<view
class="tab_text"
:style="{ color: currentIndex == index ? selectedColor : color }"
>
{{ item.text }}
view>
view>
div>
template>

<script lang="ts" setup>
import supplierMyorder from '@/pages/supplierMyorder/supplierMyorder.vue'
import supplierPersonal from '@/pages/supplierPersonal/supplierPersonal.vue'
import driverMyorder from '@/pages/driverMyorder/driverMyorder.vue'
import mycar from '@/pages/mycar/mycar.vue'
import driverPersonal from '@/pages/driverPersonal/driverPersonal.vue'
let color = ref('#666666')
let selectedColor = ref('#355db4')
let currentIndex = ref(0)

const active = ref('')
const switchTab = (item: any, index: any) => {
// console.log('tabbar----switchTab-----list', item, index)
currentIndex.value = index
active.value = item.name
}

onLoad((option: any) => {
currentIndex.value = option.index || 0
active.value = option.name || tabbarOptions.value[0].name
})
onShow(() => {
active.value = active.value || tabbarOptions.value[0].name
currentIndex.value = currentIndex.value || 0
})
const userType = computed(() => {
return uni.getStorageSync('userType')
})
const tabbarOptions = computed(() => {
return {
scm_supplier: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
scm_driver: [
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/waybill.png',
selectedIconPath: '/static/tabbar/waybill_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'driverPersonal',
pagePath: '/pages/driverPersonal/driverPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
],
supplier_and_driver: [
{
name: 'supplierMyorder',
pagePath: '/pages/supplierMyorder/supplierMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的订单'
},
{
name: 'driverMyorder',
pagePath: '/pages/driverMyorder/driverMyorder',
iconPath: '/static/tabbar/order.png',
selectedIconPath: '/static/tabbar/order_active.png',
text: '我的运单'
},
{
name: 'mycar',
pagePath: '/pages/mycar/mycar',
iconPath: '/static/tabbar/car.png',
selectedIconPath: '/static/tabbar/car_active.png',
text: '我的车辆'
},
{
name: 'supplierPersonal',
pagePath: '/pages/supplierPersonal/supplierPersonal',
iconPath: '/static/tabbar/my.png',
selectedIconPath: '/static/tabbar/my_active.png',
text: '个人中心'
}
]
}[userType.value]
})
script>

<style lang="scss" scoped>
.tabbar_page {
height: 100%;
.page_wrap {
height: calc(100% - 84px);
&.hidden {
display: none;
}
}
.tab {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: white;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: env(safe-area-inset-bottom); // 适配iphoneX的底部

.tab-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;

.tab_img {
width: 45rpx;
height: 45rpx;
}

.tab_text {
font-size: 25rpx;
margin: 9rpx 0;
}
}
}
}
.flex-box {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.flex-col {
flex-direction: column
}
style>

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