注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Systeminformation.js: 为什么不试试最强的系统信息获取工具?

web
大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。前言在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款获取系统信息的工具库:systeminformation。

前言

在现代开发环境中,跨平台获取系统信息已经成为许多应用程序的重要需求。无论你是在开发需要详细系统信息的应用,还是需要获取硬件和软件的状态信息,一个强大且灵活的工具库可以显著提升你的开发效率。今天,我们要分享的是systeminformation这个 Node.js 库,可以帮你轻松获取到你想要的各种系统信息。

基本信息

什么是 systeminformation?

systeminformation 是一个轻量级的 Node.js 库,旨在提供跨平台的系统信息获取功能。无论是在 Windows、macOS 还是 Linux 上,它都能为你提供一致的接口,获取系统的硬件和软件信息。自2015年发布以来,systeminformation 已经成为开发者们获取系统信息的首选工具之一。

它提供了超过 50 个函数,用于检索详细的硬件、系统和操作系统信息。该库支持 Linux、macOS、部分 Windows、FreeBSD、OpenBSD、NetBSD、SunOS 以及 Android 系统,并且完全无依赖。无论你需要全面了解系统状况,还是仅仅想获取特定的数据,systeminformation 都能满足你的需求,帮助你在各个平台上轻松获取系统信息。

主要特点

  • 跨平台支持:支持 Windows、macOS 和 Linux 系统,提供一致的接口。
  • 全面的信息获取:能够获取 CPU、内存、磁盘、网络、操作系统等详细信息。
  • 实时监控:支持获取实时的系统性能数据,如 CPU 使用率、内存使用率、网络速度等。
  • 易于集成:通过简单的 API 调用即可获取所需信息,便于集成到各种应用程序中。

使用场景

  • 服务器监控:实时监控服务器性能,获取 CPU、内存、磁盘等硬件信息。
  • 桌面应用:获取本地系统信息,展示系统状态和性能数据。
  • IoT 设备:在物联网设备上获取系统信息,进行设备管理和监控。

快速上手

要在你的 Node.js 项目中使用 systeminformation,只需以下简单步骤:

    1. 安装 systeminformation
npm install systeminformation
    1. 获取系统信息示例
const si = require('systeminformation');

// 获取 CPU 信息
si.cpu()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取内存信息
si.mem()
.then(data => console.log(data))
.catch(error => console.error(error));

// 获取操作系统信息
si.osInfo()
.then(data => console.log(data))
.catch(error => console.error(error));
    1. 实时监控示例
const si = require('systeminformation');

// 实时监控 CPU 使用率
setInterval(() => {
si.currentLoad()
.then(data => console.log(`CPU Load: ${data.currentload}%`))
.catch(error => console.error(error));
}, 1000);

// 实时监控内存使用情况
setInterval(() => {
si.mem()
.then(data => console.log(`Memory Usage: ${data.used / data.total * 100}%`))
.catch(error => console.error(error));
}, 1000);

结语

systeminformation 是一个功能强大且灵活的 Node.js 库,能够帮助你轻松获取系统的各种信息。无论你是需要实时监控服务器性能,还是需要获取本地系统的详细信息,systeminformation 都能为你提供稳定且易用的解决方案。

希望这篇文章能帮助你了解 systeminformation 的强大功能,并激发你在项目中使用它的灵感。赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7413643760771072015
收起阅读 »

axios VS alova.js,谁是真正的通信王者?

web
新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。 想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;...
继续阅读 »

新年快乐!在这个快速发展的前端世界里,咱们工程师面临的挑战也是一个接一个。今天,咱们就来聊聊前端实时通信这个话题。



想想看,你在使用那些传统的HTTP客户端时,比如axios,是否遇到过这样的问题:与React、Vue等框架的结合不够紧密,导致开发效率低下;在性能方面表现不佳,尤其是在处理频繁或重复的请求时;还有那略显臃肿的体积,以及混乱的响应数据类型定义?


哎呀妈呀,这些问题听着就让人头大。但别急,有个叫做alovajs的工具,可能会让你眼前一亮。


alovajs是一个轻量级的请求策略库,它不仅提供了与axios相似的API设计,让你能更快上手,还解决了上述的那些问题。它如何解决?咱们来一探究竟。


首先,alovajs能够与UI框架深度融合,自动管理请求相关的数据。这意味着你在Vue或React等框架中使用alovajs时,不再需要手动创建和维护请求状态,大大提高了开发效率。


其次,alovajs默认开启了内存缓存和请求共享,这些功能可以在提高请求性能的同时,提升用户体验并降低服务端的压力。比如,当你实现一个列表页,用户点击列表项进入详情页时,alovajs可以智能地使用缓存数据,避免不必要的重复请求。


最后,alovajs的体积只有4kb+,仅是axios的30%左右,而且它提供了更加直观的响应数据TS类型定义,对于重度使用Typescript的同学来说,这绝对是个福音。


说了这么多,是不是有点心动了?如果你对alovajs感兴趣,可以访问它的官网查看更多详细信息:alovajs官网。也欢迎你在评论区分享你对alovajs的看法和使用经验,让我们一起交流学习吧!
有任何问题,你可以加入以下群聊咨询,也可以在github 仓库中发布 Discussions,如果遇到问题,也请在github 的 issues中提交,我们会在最快的时间解决。


作者:胡镇alovajs
来源:juejin.cn/post/7334503381200437299
收起阅读 »

一文搞懂JS类型判断的四种方法

web
前言 在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeof、instanceof、Object.prototype.t...
继续阅读 »

前言


在JavaScript中,类型判断是一个非常基础但也十分重要的知识点。不同的类型判断方法适用于不同的场景,掌握这些方法可以帮助我们更好地理解和使用JavaScript。本文将详细介绍typeofinstanceofObject.prototype.toString以及Array.isArray这四种常用的类型判断方法,并通过实例代码帮助大家加深理解。


正文


typeof


typeof操作符可以用来判断基本数据类型,如stringnumberbooleanundefinedsymbolbigint等。它对于null和所有引用类型的判断会返回"object",而对于函数则会返回"function"


特点:



  1. 可以判断除null之外的所有原始类型。

  2. 除了function,其他所有的引用类型都会被判断成object

  3. typeof是通过将值转换为二进制后判断其二进制前三位是否为0,是则为object


示例代码:


let s = '123'; // string
let n = 123; // number
let f = true; // boolean
let u = undefined; // undefined
let nu = null; // null
let sy = Symbol(123); // Symbol
let big = 1234n; // BigInt

console.log(typeof s); // "string"
console.log(typeof n); // "number"
console.log(typeof f); // "boolean"
console.log(typeof u); // "undefined"
console.log(typeof sy); // "symbol"
console.log(typeof big); // "bigint"
console.log(typeof nu); // "object" - 特殊情况

let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof date); // "object"
console.log(typeof fn); // "function"

function isObject(o) {
if (typeof o === 'object' && o !== null) {
return true;
}
return false;
}

let res = isObject({a: 1});
console.log(res); // true

instanceof


instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它主要用于判断引用类型


特点:



  1. 只能判断引用类型。

  2. 通过原型链查找来判断类型。


示例代码:


let obj = {};
let arr = [];
let fn = function() {};
let date = new Date();

console.log(obj instanceof Object); // true
console.log(arr instanceof Array); // true
console.log(fn instanceof Function); // true
console.log(date instanceof Date); // true
console.log(arr instanceof Object); // true

console.log(arr instanceof String); // false
console.log(n instanceof Number); // false

因为原始类型没有原型而引用类型有原型,所有instanceof主要用于判断引用类型,那么根据这个我们是不是可以手写一个instanceof


手写·instanceof实现:


首先我们要知道v8创建对象自变量是这样的,拿let arr = []举例子:


function createArray() {
// 创建一个新的对象
let arr = new Array();
// 设置原型
arr.__proto__ = Array.prototype;
// 返回创建的数组对象
return arr;
}

V8 引擎会调用 Array 构造函数来创建一个新的数组对象,Array 构造函数的内部实现会创建一个新的空数组对象,并初始化其内部属性并且将新创建的数组对象的 __proto__ 属性设置为 Array.prototype,这意味着数组对象会继承 Array.prototype 上的所有方法和属性,最后,新创建的数组对象会被赋值给变量 arr


那么我们是不是可以通过实例对象的隐式原型等于其构造函数的显式原型来判断类型,代码如下:


function myInstanceOf(L,R){
if(L.__proto__ === R.prototype){
return true;
}
return false;
}

但是我们看到console.log([] instanceof Object); // true,所有还要改进一下:


我们要知道这么一件事情:



  1. 内置构造函数的原型链



    • 大多数内置构造函数(如 ArrayFunctionDateRegExpErrorNumberStringBooleanMapSetWeakMapWeakSet 等)的原型(Constructor.prototype)都会直接或间接地继承自 Object.prototype

    • 这意味着这些构造函数创建的对象的原型链最终会指向 Object.prototype



  2. Object.prototype 的原型



    • Object.prototype 的隐式原型(即 __proto__)为 null。这是原型链的终点,表示没有更多的原型可以继承。




所以我们是不是可以这样:


function myinstanceof(L, R) {
while (L !== null) {
if (L.__proto__ === R.prototype) {
return true;
}
L = L.__proto__;
}
return false;
}

console.log(myinstanceof([], Array)); // true
console.log(myinstanceof([], Object)); // true
console.log(myinstanceof({}, Array)); // false

所以就完美实现了。


Object.prototype.toString.call


Object.prototype.toString.call 是一个非常有用的工具,可以用来获取任何 JavaScript 值的类型信息。它结合了 Object.prototype.toStringFunction.prototype.call 两个方法的功能。


特点:



  1. 可以判断任何类型


代码示例


console.log(Object.prototype.toString.call(null));       // [object Null]
console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call('hello')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(123n)); // [object BigInt]

Object.prototype.toString


底层逻辑


根据官方文档,Object.prototype.toString 方法的执行步骤如下:



  1. 如果此值未定义,则返回 "[object undefined]"

  2. 如果此值为 null,则返回 "[object Null]"

  3. 定义 O 是调用 ToObject (该方法作用是把 O 转换为对象) 的结果,将 this 值作为参数传递

  4. 定义 class 是 O 的 [[Class]] 内部属性的值

  5. 返回 "[object" 和 class 和 "]" 组成的字符串的结果


关键点解释



  • ToObject 方法:将传入的值转换为对象。对于原始类型(如 stringnumberboolean),会创建对应的包装对象(如 StringNumberBoolean)。对于 null 和 undefined,会有特殊处理。

  • [[Class]] 内部属性:每个对象都有一个 [[Class]] 内部属性,表示对象的类型。例如,数组的 [[Class]] 值为 "Array",对象的 [[Class]] 值为 "Object"


console.log(Object.prototype.toString(123));//[object Object]
console.log(Object.prototype.toString('123'));//[object Object]
console.log(Object.prototype.toString({}));//[object Object]
console.log(Object.prototype.toString([]));//[object Object]

为什么需要 call


Object.prototype.toString 方法默认的 this 值是 Object.prototype 本身。如果我们直接调用 Object.prototype.toString(123)this 值仍然是 Object.prototype,而不是我们传入的值。因此,我们需要使用 call 方法来改变 this 值,使其指向我们传入的值。


手写call


obj = {
a:1,
}

function foo(){
console.log(this.a);
}

//我们需要将foo中的this指向obj里面
Function.prototype.myCall = function(context){
if(!(this instanceof Function)){ 在构造函数原型上,this指向的是实例对象,这里即foo
return new TypeError(this+'is not function')
}

const fn = Symbol('key'); //使用symbol作为key是因为可能会同名
context[fn] = this;//添加变量名为fn,值为上面的,context={Symbol('key'): foo}
context[fn](); // 触发了隐式绑定
delete context[fn]; //删除这个方法
}

foo.myCall(obj) // 1
console.log(obj); // {a:1}

我们知道call方法是将函数里面的this强行掰弯到我们传入的对象里面去,它的原理是这样的,首先判断你传入的参数是不是一个函数,因为只有函数身上才有call方法,函数调用然后通过隐式绑定规则,将this指向这个对象,那么不就强行更改了this的指向,[不知道this的可以看这篇文章](你不知道的JavaScript(核心知识点概念详细整理-掘金 (juejin.cn))


Array.isArray


Array.isArray是一个静态方法,用于检测给定的值是否为数组。


示例代码:


let arr = [];
let obj = {};

console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false

手写Array.isArray实现:


function myIsArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
}

console.log(myIsArray(arr)); // true
console.log(myIsArray(obj)); // false

总结



  • typeof适合用于检查基本数据类型,但对于null和对象类型的判断不够准确。

  • instanceof用于检查对象的构造函数,适用于引用类型的判断。

  • Object.prototype.toString提供了一种更通用的方法来判断所有类型的值。

  • Array.isArray专门用于判断一个值是否为数组。


希望这篇文章能够帮助你更好地理解和使用JavaScript中的类型判断方法,感谢你的阅读!


image.png


作者:反应热
来源:juejin.cn/post/7416657615369388084
收起阅读 »

三方接口不动声色将http改为了https,于是开启了我痛苦的一天

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。 原来是调的一个三方接口报错了: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorExc...
继续阅读 »

早上刚来,就看到仓库那边不停发消息说,我们的某个功能用不了了。赶紧放下早餐加紧看。


原来是调的一个三方接口报错了:


javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.ssl.Alert.createSSLException(Alert.java:131)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:353)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:296)
at sun.security.ssl.TransportContext.fatal(TransportContext.java:291)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.checkServerCerts(CertificateMessage.java:652)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.onCertificate(CertificateMessage.java:471)
at sun.security.ssl.CertificateMessage$T12CertificateConsumer.consume(CertificateMessage.java:367)
at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:376)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
at sun.security.ssl.TransportContext.dispatch(TransportContext.java:183)
at sun.security.ssl.SSLTransport.decode(SSLTransport.java:154)
at sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1279)
at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1188)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:401)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:373)

查看原因:



由于JVM默认信任证书不包含该目标网站的SSL证书,导致无法建立有效的信任链接。



奥...原来是他们把接口从http改为了https,导致我们获取数据报错了。再看看他们的证书,奥...新的。


image.png


好了,看看我们的逻辑,这其实是一个获取对方生成的PDF文件的接口


PdfReader pdfReader = new PdfReader(url);

url就是他们给的链接,是这行代码报的错。这时候,开始研究,在网上扒拉,找到了初版方案


尝试1


写一个程序专门获取安全证书,这代码有点长,全贴出来影响阅读。我给扔我hithub上了github.com/lukezhao6/I… 将这个文件贴到本地,执行javac InstallCert.java将其进行编译


image.png


编译完长这样:


image.png
然后再执行java InstallCert http://www.baidu.com (这里我们用百度举例子,实际填写的就是你想要获取证书的目标网站)


image.png
报错不用怕,因为它会去检查目标服务器的证书,如果出现了SSLException,表示证书可能存在问题,这时候会把异常信息打印出来。


在生成的时候需要输入一个1


image.png
这样,我们需要的证书文件就生成好了


image.png


这时候,将它放入我们本地的 jdk的lib\security文件夹内就行了


image.png


重启,这时候访问是没有问题了。阶段性胜利。


但是,但是。一顿操作下来,对于测试环境的docker,还有生产环境貌似不能这么操作。 放这个证书文件比较费事。


那就只能另辟蹊径了。


尝试2


搜到了,还有两种方案。



1.通过System.setProperty("javax.net.ssl.trustStore", "你的jssecacerts证书路径");


2.程序启动命令-Djavax.net.ssl.trustStore=你的jssecacerts证书路径 -Djavax.net.ssl.trustStorePassword=changeit



我尝试了第一种,System.setProperty可以成功,但是读不到文件,权限什么的都是ok的。
检查了蛮多地方



  • 路径格式问题

  • 文件是否存在

  • 文件权限

  • 信任库密码

  • 系统属性优先级


貌似都是没问题的,但肯定又是有问题的,因为没起作用。但是想着这样的接口有4个,万一哪天其他三个也改了,我又得来一遍。所以就算研究出来了,还是不能稳坐钓鱼台。有没有一了百了的方法嘞。


尝试3


还真找到了:这个错是因为对方网站的证书不被java信任么,那咱不校验了,直接全部信任。这样就算其他接口改了,咱也不愁。而且这个就是获取pdf,貌似安全性没那么重。那就开搞。


代码贴在了下方,上边的大概都能看懂吧,下方的我加了注释。


URL console = new URL(url);
HttpURLConnection conn = (HttpURLConnection) console.openConnection();
if (conn instanceof HttpsURLConnection) {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom());
((HttpsURLConnection) conn).setSSLSocketFactory(sc.getSocketFactory());
((HttpsURLConnection) conn).setHostnameVerifier(new TrustAnyHostnameVerifier());
}
conn.connect();
InputStream inputStream = conn.getInputStream();
PdfReader pdfReader = new PdfReader(inputStream);
inputStream.close();
conn.disconnect();

private static class TrustAnyTrustManager implements X509TrustManager {
//这个方法用于验证客户端的证书。在这里,方法体为空,表示不对客户端提供的证书进行任何验证。
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法用于验证服务器的证书。同样,方法体为空,表示不对服务器提供的证书进行任何验证。
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
//这个方法返回一个信任的证书数组。在这里,返回空数组,表示不信任任何证书,也就是对所有证书都不做任何信任验证。
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
//这个方法用于验证主机名是否可信。在这里,无论传入的主机名是什么,方法始终返回 true,表示信任任何主机名。这就意味着对于 SSL 连接,不会对主机名进行真实的验证,而是始终接受所有主机名。
private static class TrustAnyHostnameVerifier implements HostnameVerifier {
public boolean verify(String hostname, SSLSession session) {
return true;
}
}

解决了解决了,这样改算是个比较不错的方案了吧。


作者:奔跑的毛球
来源:juejin.cn/post/7362587412066893834
收起阅读 »

大屏页面崩溃排查(Too many active WebGL contexts. Oldest context will be lost)

web
1 问题背景 顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。 我们的页面类似于这样的布局(下方的是直接从网络上找的截图) 点击下方红线框住的区域,可以展示不同的图表(echarts图表) 区别在于我们的...
继续阅读 »

1 问题背景


顾问反馈大屏在反复切换的过程中,页面会出现白屏,导致后续的操作无法进行,只能通过刷新页面才能恢复正常。



  1. 我们的页面类似于这样的布局(下方的是直接从网络上找的截图

  2. 点击下方红线框住的区域,可以展示不同的图表(echarts图表)

  3. 区别在于我们的主区域不是图片,用的是基于cesium封装的地图(webgl


image.png


2 问题复现


测试同事经过几分钟的快速切换导航后,复现了,报错了如下内容
image.png


问题如果复现了,其实就解决了一半了


3 查找问题


经过复现后,发现除了上面的报错,每当页面崩溃前,chrome总会有下方的warning。然后基于cesium封装的地图就会崩溃。


image.png


翻译成中文:警告:目前有了太多激活的webgl上下文,最早的上下文将会丢失


4 排查问题


经过和地图组的人沟通,得到一个结论WebGL一个页面上最多有16个实例



  • 怀疑echarts在下方菜单切换过程中,没有进行销毁


检查了代码中的echats的页面在销毁的时候,发现都进行了dispose,排除了这个的原因



  • 怀疑起echarts的3d的饼状图
    之前设计师设计了一个3d的饼状图,参考了 3d柄图环图,一个页面上有多这个组件。
    效果如下:


f8a015b3-0b94-4fd2-a3b3-07a745fa401a.gif


5 锁定组件进行验证



  1. 先把一个页面上的所有组件改为上方的饼状图,然后点击导航栏,频繁进行切换,

  2. 页面很快就崩溃了,然后检查这个组件在页面销毁的时候,是否进行dispose
    检查后,发现没有,添加后进行测试,问题依旧

  3. 继续检查发现这个组件导入了echarts-gl,就去ecahrts的github的issues进行搜索,终于找到了一个类似的问题
    github.com/ecomfe/echa…


image.png


加入了类似的代码,进行验证后解决了此问题


6 总结



  1. chrome浏览器中最多有16个webgl的实例。当过多的时候,会把最早创建的实例销毁

  2. 当使用echarts在页面销毁的时候及时进行dispose,释放上下文

  3. 当使用echarts-gl的时候,调用dispose的时候是不生效的,需要找到页面上的canvas,然后手动将上下文释放,类似下方的代码


const canvasArr = myChart.getDom().getElementsByTagName('canvas');
for(let i=0; i<canvasArr.length; i++){
canvasArr[i].getContext('webgl').getExtension('WEBGL_lose_context').loseContext()
}


7 参考文档



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

不是,哥们,谁教你这样处理生产问题的?

你好呀,我是歪歪。 最近遇到一个生产问题,我负责的一个服务触发了内存使用率预警,收到预警的时候我去看了内存使用率已经到了 80%,看了一眼 GC 又发现还没有触发 FullGC,一次都没有。 基于这个现象,当时推测有两种可能,一种是内存溢出,一种是内存泄漏。 ...
继续阅读 »

你好呀,我是歪歪。


最近遇到一个生产问题,我负责的一个服务触发了内存使用率预警,收到预警的时候我去看了内存使用率已经到了 80%,看了一眼 GC 又发现还没有触发 FullGC,一次都没有。


基于这个现象,当时推测有两种可能,一种是内存溢出,一种是内存泄漏。


好,假设现在是面试,面试官目前就给了这点信息,他问你到底是溢出还是泄漏,你怎么回答?


在回答之前,我们得现明确啥是溢出,啥情况又是泄漏。



  • 内存溢出(OutOfMemoryError):内存溢出指的是程序请求的内存超出了 JVM 当前允许的最大内存容量。当 JVM 试图为一个对象分配内存时,如果当前可用的堆内存不足以满足需求,就会抛出 java.lang.OutOfMemoryError 异常。这通常是因为堆空间太小或者由于某些原因导致堆空间被占满。

  • 内存泄漏 (Memory Leak):内存泄漏是指不再使用的内存空间没有被释放,导致这部分内存无法再次被使用。虽然内存泄漏不会立即导致程序崩溃,但它会逐渐消耗可用内存,最终可能导致内存溢出。


虽然都与内存相关,但它们发生的时机和影响有所不同。内存溢出通常发生在程序运行时,当数据结构的大小超过预设限制时,常见的情况是你要分配一个大对象,比如一次从数据中查到了过多的数据。


而内存泄漏和“过多”关系不大,是一个细水长流的过程,一次内存泄漏的影响可能微乎其微,但随着时间推移,多次内存泄漏累积起来,最终可能导致内存溢出。


概念就是这个概念,这两个玩意经常被大家搞混,所以多嘴提一下。


概念明确了,回到最开始这个问题,你怎么回答?


你回答不了。


因为这些信息太不完整了,所以你回答不了。


面试的时候面试官就喜欢出这种全是错误选项的题目来迷惑你,摸摸你的底子到底怎么样。


首先,为什么不能判断,是因为前面说了:一次 FullGC 都没有。


虽然现在内存使用率已经到 80% 了,万一一次 FullGC 之后,内存使用率又下去了呢,说明程序没有任何问题。


如果没有下去,说明大概率是内存溢出了,需要去代码里面找哪里分配了大对象了。


那如果下去了,能说明一定没有内存泄漏吗?


也不能,因为前面又说了:内存泄漏是一个细水长流的过程。


关于内存溢出,如果监控手段齐全到位的话,你就记住左边这个走势图:



一个缓慢的持续上升的内存趋势图, 最后疯狂触发 GC,但是并没有内存被回收,最后程序直接崩掉。


内存泄漏,一眼定真假。


这个图来自我去年写的这篇文章:《虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。》


里面就是描述了一个内存泄漏的问题,通过分析 Dump 文件的方式,最终成功定位到泄漏点,修复代码。


一个不论多么复杂的内存泄漏问题,处理起来都是有方法论的。


不过就是 Dump 文件分析、工具的使用以及足够的耐心和些许的运气罢了。


所以我不打算赘述这些东西了,我想要分享的是我这次是怎么对应文章开始说的内存预警的。


我的处理方式就是:重启服务。


是的,常规来说都是会保留现场,然后重启服务。但是我的处理方式是:直接执行重启服务的预案。没有后续动作了。


我当时脑子里面的考虑大概是这样的。


首先,这个服务是一个边缘服务,它所承载的数据量不多,其业务已经超过一年多没有新增,存量数据正在慢慢的消亡。代码近一两年没啥改动,只有一些升级 jar 包,日志埋点这类的横向改造。


其次,我看了一下这个服务已经有超过四个月没有重启过了,这期间没有任何突发流量,每天处理的数据呈递减趋势,内存走势确实是一个缓慢上升的过程,我初步怀疑是有内存泄漏。


然后,这个服务是我从别的团队那边接手的一个服务,基于前一点,业务正在消亡这个因素,我也只是知道大概的功能,并不知道内部的细节,所以由于对系统的熟悉度不够,如果要定位问题,会较为困难。


最后,基于公司制度,虽然我知道应该怎么去排查问题,命令和工具我都会使用,但是我作为开发人员是没有权限使用运维人员的各类排查工具和排查命令的,所以如果要定位问题,我必须请求协调一个运维同事帮忙。


于是,在心里默默的盘算了一下投入产出比,我决定直接重启服务,不去定位问题。


按照目前的频率,程序正常运行四五个月后可能会触发内存预警,那么大不了就每隔三个月重启一次服务嘛,重启一次只需要 30s。一年按照重启 4 次算,也就是才 2 分钟。


这个业务我们就算它要五年后才彻底消亡,那么也就才 10 分钟而已。


如果我要去定位到底是不是内存泄露,到底在哪儿泄露的,结合我对于系统的熟悉程度和公司必须有的流程,这一波时间消耗,少说点,加起来得三五个工作日吧。


10 分钟和三五个工作日,这投入产出比,该选哪个,一目了然了吧?


我分享这个事情的目的,其实就是想说明我在这个事情上领悟到的一个点:在工作中,你遇到的问题,不是每一个都必须被解决的,也可以选择绕过问题,只要最终结果是好的就行。


如果我们抛开其他因素,只是从程序员的本职工作来看,那么遇到诸如内存泄漏的问题的时候,就是应该去定位问题、解决问题。


但是在职场中,其实还需要结合实际情况,进行分析。


什么是实际情况呢?


我前面列出来的那个“首先,其次,然后,最后”,就是我这个问题在技术之外的实际情况。


这些实际情况,让我决定不用去定位这个问题。


这也不是逃避问题,这是权衡利弊之后的最佳选择。


同样是一天的时间,我可以去定位这个“重启就能解决”的问题,也可以去做其他的更有价值事情,敲一些业务价值更大的代码。


这个是需要去权衡的,一个重要的衡量标准就是前面说的:投入产出比。


关于“不是所有的问题都必须被解决的,也可以选择绕过问题”这个事情,我再给你举一个我遇到的真实的例子。


几年前,我们团队遇到一个问题,我们使用的 RPC 框架是 Dubbo,有几个核心服务在投产期间滚动发布的时候,流量老是弄不干净,导致服务已经下线了,上游系统还在调用。


当时安排我去调研一下解决方案。


其实这就是一个优雅下线的问题,但是当时资历尚浅,我认真研究了一段时间,确实没研究出问题的根本解决方案。


后来我们给出的解决方案就是做一个容错机制,如果投产期间有因为流量不干净的问题导致请求处理失败的,我们把这些数据记录下来,然后等到投产完成后再进行重发。


没有解决根本问题,选择绕过了问题,但是从最终结果上看,问题是被解决了。


再后来,我们搭建了双中心。投产之前,A,B 中心都有流量,每次投产的时候,先把所有流量从 A 中心切到 B 中心去,在 A 中心没有任何流量的情况下,进行服务投产。B 中心反之。


这样,从投产流程上就规避了“流量老是弄不干净”的问题,因为投产的时候对应的服务已经没有在途流量了,不需要考虑优雅的问题了,从而规避了优雅下线的问题。


问题还是没有被解决,但是问题被彻底绕过。


最后,再举一个我在知乎上看到的一个回答,和我想要表达的观点,有异曲同工之妙:



http://www.zhihu.com/question/63…




这个回答下面的评论也很有意思,有兴趣的可以去翻一下,我截取两个我觉得有意思的:





在职场上,甚至在生活中,一个虽然没有解决方案但是可以被绕过的问题,我认为不是问题。


但是这个也得分情况,不是所有问题都能绕开的,假如是一个关键服务,那肯定不能置之不理,硬着头皮也得上。


关键是,我在职场上和生活中遇到过好多人,遇到问题的时候,似乎只会硬着头皮往上冲。


只会硬着头皮往上冲和知道什么时候应该硬着头皮往上冲,是两种截然不同的职场阶段。


所以有时候,遇到问题的时候,不要硬上,也让头皮休息一下,看看能不能绕过去。


作者:why技术
来源:juejin.cn/post/7417842116506058771
收起阅读 »

谁也别拦我们,网页里直接增删改查本地文件!

web
欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面! 转载请联系作者 Jax。 先来玩玩这个 Demo —— 一个网页端的本地文件管理器。 在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个...
继续阅读 »

欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面!


转载请联系作者 Jax。



先来玩玩这个 Demo —— 一个网页端的本地文件管理器


demo.gif


在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个文件夹来打开。然后就能对其中的文件做各种操作了,包括新建、右键打开、编辑、删除等等。你会发现,在网页上的操作会立刻在本地文件上实际生效。


如果你感觉是:”哟?有点儿意思!“,那么这篇文章就是专门为你而写的,读下去吧。


正所谓「边牧是边牧、狗是狗」,Web 应用早已不是当初的简陋网页了,它进化出了令人眼花缭乱的新能力,同时也在迅速追齐系统原生能力。如 Demo 所示,我们甚至可以把文件管理系统搬进网页,并且简单的 JavaScript 代码就能实现。Web 应用能有这样的威力,得益于浏览器对文件系统 API 的实现。


文件系统(File System Access)API 目前还处于社区草稿状态,但早在 2016 年,WHATWG 社区就已经开始着手设计这一标准,并且八年来一直在持续不断地打磨,迄今已经被各大主流浏览器所实现。


venders.jpeg


这套 API 足够简单易用,你很快就能掌握它,并且让它成为你的得力助手。但由于涉及到文件管理这个课题,它有很多方面都可以挖掘得很深很深,比如读写流、OPFS、安全策略等等。咱们这个专栏本着少吃多餐的原则,不会一次性囊括那么多内容。咱们的这第一铲,就先浅浅地挖一个「憨豆」 —— FileSystemHandle


FileSystemHandle


在文件系统中,文件和文件夹是一个个实体,是所有状态和交互的核心。相对应地,在文件系统 API 中,我们用「FileSystemHandle」这个对象来对实体进行抽象。为了能够简洁地描述,我们在下文中将称之为「憨豆」。我们用对象属性来保存实体的名称、类型,通过执行对象方法来操作实体。


那么 FileSystemHandle 从何而来呢?一般是从用户操作而来。以文件为例,当用户通过文件窗口选择了某个本地文件之后,我们就能从代码层面获取到通向这个文件的入口,从而可以对文件进行管理操作。除此之外,拖拽文件进入也能得到憨豆


属性:name 和 kind


name:无论是文件还是文件夹,必然都有一个名字。


kind:实体的类型,值为 ‘file’ 代表文件;值为 ‘directory’ 代表文件夹。


校验方法 isSameEntry()


用于判断两个憨豆是否代表相同的实体。例如用户上传图片时,先后两次选择的是同一张图片,那么两个憨豆指向的其实是同一个文件。


const [handle1] = await showOpenFilePicker() // 第一次上传
const [handle2] = await showOpenFilePicker() // 第二次选择了同一个文件

const isSame = await handle1.isSameEntry(handle2)
console.log(isSame) // true

该方法也同样适用于文件夹校验。


我们可以借此来检测重复性。


删除方法 remove()


用于删除实体。比如,执行下列代码会现场抽取一个幸运文件来删除:


const [handle] = await showOpenFilePicker()
handle.remove()

但如果我们想要选中文件夹来删除,像上面那样直接调用是会报错并删除失败的。我们需要额外传入配置参数:


handle.remove({ recursive: true })

传参后的删除,是对文件夹里面的嵌套结构进行递归删除,这个设计理念旨在让开发者和用户在删除前知悉操作后果。


权限方法 queryPermission() 和 requestPermission()


用于向用户查询和获取读写权限,可以传入一个参数对象来细分权限类型。


const permission = await handle.queryPermission({ mode: 'read' }) // mode 的值还可以是 readwrite
console.log(permission)// 若值为 'granted',则代表有足够权限

我们在对实体进行操作前,最好总是先查询权限。因此一个最佳实践是把这两个方法封装进通用操作逻辑中。


其他特性


除此之外,FileSystemHandle 还具有一些其他特性,比如可以转化为 indexDB 实例、可以通过 postMessage 传输到 Web Workers 里进行操作等等。我们会在后续的专栏文章中继续顺藤摸瓜地了解它们。


两个子类


到目前为止,FileSystemHandle 所具有的属性和方法都在上面了。你可能也意识到了,单靠这三招两式是不可能实现像 Demo 里那么丰富完备的文件操作的。


没错,这个憨豆只是一个抽象父类,它还有两个子类 FileSystemFileHandleFileSystemDirectoryHandle,这两位才是文件和文件夹实体的真正代言人。对实体的常规操作,几乎都是通过这两个紫憨豆来执行的。


除了从父类继承而来的基因,两个紫憨豆都各自具有独特的属性和方法,满足了开发者对文件和文件夹两种不同实体的不同操作需求。


FileSystemFileHandle


在本专栏的上一篇文章《绕过中间商,不用 input 标签也能搞定文件选择》中,我们曾与文件憨豆有过一面之缘。那时候,我们通过 showOpenFilePicker 获取了文件憨豆,并调用它的 getFile 方法拿到了 文件 Blob


此外,文件憨豆还具有的方法如下:



  • createSyncAccessHandle():用于同步读写文件,但是仅限于在 Web Workers 中。

  • createWritable:创建一个写入流对象,用于向文件写入数据。


FileSystemDirectoryHandle


文件夹憨豆的特有方法如下:



  • getDirectoryHandle():按名称查找子文件夹。

  • getFileHandle():按名称查找子文件。

  • removeEntry():按名称移除子实体。

  • resovle():返回指向子实体的路径。


经过上述罗列,我们对两种憨豆的能力有了一个大概的印象。下面,我们将回到 Demo 的交互场景,从具体操作上手,看看文件系统 API 是如何在 JavaScript 中落地的。


操作 & 用法


载入文件夹


我们首先需要载入一个文件夹,然后才能对其中的子实体进行操作。


如果你碰巧读过上一篇文章,知道如何用 showOpenFilePicker() 选择文件,那你一定能举一反三推断出,选择文件夹的方法是 showDirectoryPicker()


const dirHandle = await showDirectoryPicker()

showDirectoryPicker 方法也接收一些参数,其中 idstartIn 这两个参数与 showOpenFilePicker 方法 的同名参数完全对应。另外还支持一个参数 mode ,其值可以是 readreadwrite,用于指定所需的权限。


用户选择文件夹后得到的 dirHandle,就是一个 FileSystemDirectoryHandle 格式的对象。我们可以遍历出它的子实体:


for await (const sub of dirHandle.values()) {
const { name, kind } = sub
console.log(name, kind)
}

从子实体中取出名称和类别属性值,就可以对文件夹内容一目了然了。


读取文件内容


在上一步中,我们已经读取到了子实体的名字和类型,那么对于文件类型,我们可以先按名称检索到对应的憨豆:


// if sub.kind === 'file'
const fileHandle = await dirHandle.getFileHandle(sub.name)

再从文件憨豆中掏出文件 Blob,进一步读取到文件的内容:


const file = await fileHandle.getFile()
const content = file.text()

如果你用来调试的文件是文本内容的文件,那么打印 content 的值,你就可以看到内容文本了。


同理,获取子文件夹的方式是 dirHandle.getDirectoryHandle(sub.name)


新建文件、文件夹


除了指定名称参数,getFileHandlegetDirectoryHandle 这两个方法还支持第二个参数,是一个一个配置对象 { create: true/false },用于应对指定名称的实体不存在的情况。


例如,我们对一个文件夹实体执行 dirHandle.getFileHandle('fileA'),但该文件夹中并没有名为 fileA 的文件。此时第二个参数为空,等同于 create 的默认值为 false,那么此时会抛出一个 NotFoundError 错误,提示我们文件不存在。


而如果我们这样执行:dirHandle.getFileHandle('fileA', { create: true }),那么就会在当前文件夹中新建一个名为 fileA 的空文件。


同理,我们也可以用 dirHandle.getDirectoryHandle('dirA', { create: true }) 新建一个名为 dirA 的空文件夹。


在 Demo 中,我们实现了让用户在新建文件时自定义文件名,这其实是使用了 prompt 方法:


const fileName = prompt('请输入文件名')
await dirHandle.getFileHandle(fileName, { create: true })

在当下这个 AI 时代,Prompt 更多是以「LLM 提示词」被大家初识。在更早时候,它是浏览器实现的一个组件。其实这两种形态的含义是一致的,都是在人机交互中,从一端到另一端的输入输出流程。


编辑文件内容


刚刚我们读取了文件的内容,现在我们来对文件内容进行修改,然后再存回去。


我们已经能够通过 getFile() 方法拿到文本内容,那应该把内容放到哪里进行编辑呢?你有很多种选择:富文本编辑器、给 div 设置 contenteditable、唤起 VS Code…… 但本着最(能)小(用)原(就)则(行),我们还有更便捷的选项 —— prompt!


prompt() 方法也支持第二个参数,我们把文本内容传入,弹出弹窗后,你就会看到内容已经填在了输入框中,现在就可以随意编辑里面的字符了。


const file = await fileHandle.getFile()
const fileContent = await file.text()
const newContent = prompt('', fileContent) // newContent 即为修改后的文件内容

但是点击 Prompt 的确认按钮并不会让新内容自动写入文件,这里就需要用到上面提到的 createWritable 了。下面是一个完整的写入流流程:


const writable = await fileHandle.createWritable() // 对待修改文件开启写入流
await writable.write(newContent) // 把新内容写入到文件
await writable.close() // 关闭写入流

至此,新的内容就已经被保存到文件中了。你可以在 Demo 中再次右键、打开文件查看内容,你会发现确实读取到了修改后的内容。


文件重命名


修改文件名也是文件管理中的常规操作,文件系统 API 也提供了对应的方法来实现。可能手快的同学已经在尝试 fileHandle.rename() 方法了。但 API 中还真没有这个方法,我们其实是要用一个 move() 方法。惊不惊喜意不意外?


因为从底层视角看,重命名文件和移动文件的处理过程类型,都是需要先新建一个文件(使用新的命名或者放到新的位置),再把源文件的数据复制过去,最后把源文件删除。目前在 Web 端还没有更高效的处理方式。


我们只需从 Prompt 获取新名称,再传给 move() 方法即可:


const newName = prompt('请输入新的文件名')
await fileHandle.move(newName)

这样,文件重命名就搞定了。


删除文件、文件夹


删除实体就没有那么多幺蛾子了,我们甚至不用对文件和文件夹做逻辑上的区分,简单直接地调用 currentHandle.remove({ recursive: true }) 就行了。


但越是方便,就越要谨慎,因为涉及到删除用户的文件,所以如果要用于生产环境,最好给用户提供二次确认的机会。


写在结尾


恭喜你读完了本文,你真棒!


这次我们通过实现一个 Web 版文件管理器 demo,对文件管理 API 进行了较为深入的理解和实践。我再嘱咐两句:



  1. 涉及到操作用户文件,请务必谨慎。

  2. 为了保障安全性,文件系统 API 仅支持 https。



我是 Jax,在畅游 Web 技术领域的第 7 年,我仍然是坚定不移的 JavaScript 迷弟,Web 开发带给我太多乐趣。如果你也喜欢 Web 技术,或者想讨论本文内容,欢迎来聊!你可以通过下列方式找到我:


掘金:juejin.cn/user/113435…


GitHub:github.com/JaxNext


微信:JaxNext



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

35 岁时我改掉的三个习惯

大家好,我是双越老师,wangEditor 作者。 我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。 开始 虽然标题是 35 岁,但其实本文 202...
继续阅读 »

大家好,我是双越老师,wangEditor 作者。



我正在开发一个 Nodejs 全栈 AIGC 知识库项目 划水AI,包括富文本编辑器,多人协同编辑,AI 写作,AI 处理文本等。有兴趣的同学可以围观项目。



开始


虽然标题是 35 岁,但其实本文 2024 年我 36 岁时写的。36 岁总结 35 岁,也没毛病。


35 岁对于我们程序员来说是一个非常敏感的年龄,但我已过 35 ,看看往后会越来越敏感,还是越来越麻木?


本文和大家分享一下我个人在业余生活中的,我觉得很有意义的事情。甚至我觉得这些是事情的意义,都大于工作的成绩。


生活是一个整体,工作只是其中一部分,只是我们养家糊口的一个手段。工作以外的其他部分应该更加重要,例如业余爱好、饮食、休息、娱乐、家庭等。


1. 戒烟


我从大学毕业开始学着吸烟,到 35 岁已经有十几年的烟龄了。虽然每天吸烟量不大,但也断不了,有瘾。


我为什么要戒烟呢?


是觉得吸烟有害健康?—— 对,吸烟肯定有害健康,但人类天生是一种及时行乐的动物,谁会在乎 20 年 30 年以后的健康呢?人类天生是一个心存侥幸的动物(赌徒性质),也不是每个吸烟者都会有 xxx 病,对吧?之前看过一个段子,说某呼吸外科医生做完,累几个小时做完一台手术,先出去吸烟休息一下。


我戒烟,单纯就是想戒。我想摆脱烟草和尼古丁的控制,而且是想了很久了,不是一时突发奇想,之前就有充分的心理准备。


还有,我想在 35+ 的年纪做一点叛逆的事情,叛逆使人年轻,叛逆使人保持活力。年轻时吸烟是叛逆,年龄大了戒烟就是叛逆。年轻时叛逆是狂,年龄大了叛逆是帅。所以,各位大龄程序员,有机会要让自己叛逆起来,做一点帅的事情。


最后,当时 2023 年夏天,我正好不忙,天天闲着,总得找点事儿干。既然工作上不忙,那就在自己身上找点有意义事情做吧 —— 外求不得而向内求,如果没法从外面挣取,那就去提高自身。


烟瘾是什么?就是尼古丁的戒断反应,没有其他理由,没人其他任何事情会让你 1-2 小时就想一次,而且持续想。


关于烟草的本质,烟草的历史,烟草的商业化推广过程,烟草的洗脑广告…… 还有很多内容可以讲,本文就不展开了,有兴趣的可以留言咨询。我很早之前就看过,去学习这些,并且接受这些,才能更好的帮助戒烟。


所以,就这么戒了,到写作本文的时候,正好是戒了一年。我觉得这是我去年做过的最有价值的事情,比我工作挣的钱都有价值。


2. 戒酒


之前我是比较喜欢喝酒的,喜欢一帮人聚餐那种兴奋的状态。但后来喝多了就肚子难受,一躺下就想吐,于是决定不喝了。


有些人说:你可以少喝点。但在中国北方的酒桌上,只有 0 和 1 ,没有中间态。只要你喝了,一开始朋友劝你多喝点,再喝多一点就不用别人劝了,自己就开始主动找酒瓶子了。


我不懂酒,没喝过啥好酒,很少喝红酒。就日常喝的白酒和啤酒而言,我觉得都不好喝。


白酒,度数高,辣(尤其酱味的),容易醉。全世界就中国及其周边人喝白酒,国内几千的白酒没有国际市场。而且单就中国而言,白酒蒸馏技术几百年了,也只有最近这不到 100 年喝白酒。《红楼梦》上层人不喝白酒,《孔乙己》下层人也不喝白酒。


现在喝白酒的人,有两类:1. 被酒桌文化感染而顺从的人; 2. 有酒瘾想快速体验酒精的人。


啤酒,要分好多种,咱日常喝的瓶装的、桶装的,都可以统称为工业啤酒,像水一样,轻薄寡淡 —— 但它有酒精啊!


那种全麦啤酒(忘记名字了,不常喝)还是很好喝的,但价格较高,自己买点喝还行,聚餐喝那个太贵了(普通饭店也没有卖的),很少喝。


我身边也有一些朋友,每周都喝好几次,大部分是为了工作,拉拢客户关系。我觉得我还是比较幸运的,写写代码,改改 bug ,也不用太考虑人际关系。程序员的为数不多的好处。


3. 不看和自己无关的事情


我从不刷抖音(虽然我会发布一些抖音视频),但我之前很喜欢看今日头条 app ,每天闲了就刷一下,吃饭也看,睡前也看。


但是我都看了些啥呢?有一些是娱乐性的小视频,搞笑的,猎奇的,做饭吃饭的,我觉得这些很好,提供情绪价值。


其他的,什么俄 x 战争,什么国外 xxx 冲突,什么体育明星谁比谁厉害,什么传统武术,什么中医 …… 还有这些的评论,各路网友互怼。有时候看的都让人很带情绪,感觉有些人是不是傻,这么简单的道理看不明白?有些人是不是坏,不看事实,只看立场?


这些不仅不能提供情绪价值,反而会增加你的负面情绪。而且,根据《乌合之众》大众心理学研究,你只要参与了其中,你参与了互怼,你就会成为他们其中的一员,也变成傻子或坏人。围观,也是一种参与,你的心里会支持某一方。


更关键的是,这些事情我自己有关系吗?或者我的表态能决定这件事儿的走向吗?哪怕投一票决定一点点呢 —— 答案是显然的,要么没有任何关系,要么自己瞎操心。


所以,我卸载了今日头条 app ,不看了,眼不见心不烦,这些事情我不知道,也不影响我个人工作生活。从此以后,我觉得我的世界瞬间清净了,至少不会被那些负面情绪所打扰。


另外,我睡前也不看手机了,把手机扔在书房充电,直接睡觉。如果偶尔失眠或想事情,那就想,也不用非得拿着手机想,对吧。


总结


35 岁是一个里程碑和转折点,工作上如此,生活中也是如此。程序员是一个相对来说比较“单纯”的群体,我觉得更应该关注个人生活中的成长,共勉,加油~


作者:前端双越老师
来源:juejin.cn/post/7417630844100247590
收起阅读 »

BOE(京东方)携故宫博物院举办2024“照亮成长路”公益项目落地仪式以创新科技赋能教育可持续发展

2024年9月20日,BOE(京东方)“照亮成长路”智慧教室落成暨百堂故宫传统文化公益课山西活动落地仪式在山西省太原市娄烦县实验小学隆重举行。自“照亮成长路”教育公益项目正式设立以来,BOE(京东方)持续以创新科技赋能偏远地区的教育升级,随着2024年捐赠的2...
继续阅读 »

2024年9月20日,BOE(京东方)“照亮成长路”智慧教室落成暨百堂故宫传统文化公益课山西活动落地仪式在山西省太原市娄烦县实验小学隆重举行。自“照亮成长路”教育公益项目正式设立以来,BOE(京东方)持续以创新科技赋能偏远地区的教育升级,随着2024年捐赠的23间智慧教室全面圆满竣工并正式投入使用,BOE(京东方)捐建的智慧教室总数已达126间,它们不仅代表了教育创新、文化传承与先进技术的融合,也开启了BOE(京东方)面向新三十年发展征程、积极践行企业社会责任的新起点。

故宫博物院作为“照亮成长路”公益项目的重要合作伙伴,一直致力于促进中华优秀传统文化在青少年中的普及与传播。尤其是与京东方科技集团共同发起的“百堂故宫传统文化公益课”项目是故宫博物院教育推广的又一次重要实践。项目启动后近一年间已为26所学校,2万余名学生送去了400余场线上公益课程。此次落地的山西娄烦实验小学和静乐君宇中学也成为该计划的线下落地试点学校。活动现场,娄烦县委副书记、县长景博,娄烦县委常委、常务副县长任瑛,娄烦县委常委、副县长李学斌,静乐县副县长许龙平,中国乡村发展基金会副秘书长丁亚冬,故宫博物院副院长朱鸿文,京东方科技集团执行副总裁、艺云科技董事长姚项军,京东方科技集团副总裁、首席品牌官司达等出席了本次仪式,共同见证这一重要时刻。

在活动现场,京东方科技集团执行副总裁姚项军表示:“教育数字化是推进教育现代化的关键力量。BOE(京东方)充分发挥自身在物联网创新领域的专长,通过首创的多项类纸护眼显示技术,制定的低蓝光健康显示技术国际标准,推出了一系列智慧校园产品与服务;同时还充分发挥企业产业优势,开发科学与工程教育产品,用科学创新实践支持做公益。BOE(京东方)将携手各界同仁开启‘照亮成长路’教育公益项目的下一个十年篇章,继续推动教育与科技的深度融合,迈向一个更加智慧、更加光明、更加美好的未来!”

中国乡村发展基金会副秘书长丁亚冬在致辞中表示:“BOE(京东方)是我们多年的合作伙伴,持续关注乡村数字化教育的发展,携手实施的‘照亮成长路’教育公益项目已改造完成126间智慧教室,用科技力量助力消弭教育鸿沟,照亮乡村学生的成长之路。未来,我们将继续与BOE(京东方)、故宫博物院及社会各界通力合作,充分发挥各自优势,着力推进教育公平,促进教育均衡发展,为助力全面乡村振兴做出不懈努力。”

故宫博物院副院长朱鸿文在致辞中表示:“故宫博物院作为中华民族五千多年文明的重要承载者、中华优秀传统文化的汇聚地,始终将传承弘扬中华优秀传统文化作为己任,不断探索创新,希望通过丰富多彩的博物馆教育项目,将中华优秀传统文化传递给广大观众。很高兴能够携手京东方这样的科技企业通过传统文化振兴乡村发展,在以科技赋能偏远地区提升数字化水平的基础上,融入传统文化教育,增强师生文化自信,建设文化强国,助力中华民族伟大复兴。”故宫博物院也在活动中向学校的全体师生赠送了《我要去故宫》系列图书。

本次活动上,中国乡村发展基金会副秘书长丁亚冬,京东方科技集团副总裁、首席品牌官司达,娄烦县委副书记、县长景博,静乐县副县长许龙平,故宫博物院社会教育部主任吕晓刚,艺云科技智慧校园事业部总经理李慧军,共同为2024年“照亮成长路”项目新建的23间智慧教室举行了揭牌仪式。仪式结束后,故宫博物院社教人员还在新落成的智慧教室中,通过生动有趣的互动式教学,将故宫蕴含的中华优秀传统文化展现给孩子们,课堂上孩子们积极与老师交流,并动手制作多种手工材料包,获得了一份来自故宫的珍贵文化礼物。同时,BOE(京东方)志愿者也为孩子们带来了生动有趣的科学实践课,通过讲解屏幕显示的原理,让孩子们充分了解屏幕背后的技术知识,感受显示科技的精妙;此外,还设置了小组实践环节,模拟工厂流水线,让孩子们合作组装屏幕像素模拟装置,在动手中加深对知识的理解,在体验中收获知识,在实践中收获成长。

2024 BOE(京东方)“照亮成长路”教育公益项目的成功落地与本次活动的顺利举办,得益于山西省娄烦县政府和故宫博物院的大力支持,也离不开中国乡村发展基金会在项目推进过程中的通力合作。此次活动过程中,各方领导嘉宾围绕科技文化在教育领域的融合应用、智慧教育的未来趋势以及公益事业的长足发展进行了深入探讨。娄烦县政府相关领导作为代表对BOE(京东方)“百所校园”的公益新里程表示肯定与祝贺,并祝愿“照亮成长路”及“百堂故宫传统文化公益课”在未来能够惠及更多校园,助力更多偏远地区师生了解优秀传统文化、体验智慧教育。

作为一家全球化的科技公司,BOE(京东方)坚持Green+、Innovation+、Community+可持续发展理念,在教育、文化、健康等领域积极开展公益活动,通过引领绿色永续发展、持续驱动科技创新、赋能整个产业和社会。其中,“照亮成长路”是BOE(京东方)2014年启动的教育公益项目,通过智慧教室建设、教育资源融合、教师赋能培训计划等,携手社会各界力量,将技术融入社区公益发展与乡村振兴事业。目前,BOE(京东方)已在全国8大省市地区建成126间智慧教室,为63500余名师生提供软硬融合的智慧教育解决方案和教师赋能计划,切实帮助偏远地区学生群体获得更优质的教育和成长机会,在缩小城乡间数字差距、推动区域教育现代化、促进社会全面进步方面彰显了重要价值。

作为“照亮成长路”的特色项目,“百堂故宫传统文化公益课”让偏远地区的孩子能够通过BOE(京东方)的智慧教育创新技术跨越时间和空间的限制,近距离感受故宫的魅力,了解中国传统文化的精髓。接下来,更多课程将陆续在更为广泛的偏远地区展开,到2025年故宫博物院建院百年之际,双方将联手在北京故宫博物院为孩子们带来第100堂特别课程。

未来,BOE(京东方)与故宫博物院也将继续携手,以科技和文化双重赋能教育,让知识的光芒照亮每一个孩子的未来。

收起阅读 »

独家授权!广东盈世获网易邮箱反垃圾服务的独家授权,邮件反垃圾更全面

近日,广东盈世计算机科技有限公司(以下简称“Coremail”)成功获得了网易(杭州)网络有限公司(以下简称“网易”)授予的网易邮箱反垃圾服务独家使用权。这一授权使得Coremail能够在邮件安全产品上运用网易邮箱反垃圾服务,进一步强化其反垃圾能力,邮件反垃圾...
继续阅读 »

近日,广东盈世计算机科技有限公司(以下简称“Coremail”)成功获得了网易(杭州)网络有限公司(以下简称“网易”)授予的网易邮箱反垃圾服务独家使用权。这一授权使得Coremail能够在邮件安全产品上运用网易邮箱反垃圾服务,进一步强化其反垃圾能力,邮件反垃圾更全面。

凭借24年的反垃圾反钓鱼技术沉淀,Coremail邮件安全致力于提供一站式邮件安全解决方案,为用户提供安全、可靠的安全解决方案。而网易作为国内邮箱行业的佼佼者,拥有强大的技术实力和丰富的经验,其网易邮箱反垃圾服务更是享有盛誉。

通过合作,网易为Coremail提供Saas在线网关服务,进行进信和外发的在线反垃圾检测。Coremail邮件安全反垃圾服务将以自研反垃圾引擎为主,网易反垃圾服务为辅,以“双引擎”机制保障用户享有最高等级的邮件反垃圾服务。

此外,除网易自身、广州网易计算机系统有限公司及其关联公司外,Coremail是唯一被授权在服务期内独家使用网易邮箱反垃圾服务的公司。这一独家授权充分体现了网易对Coremail的高度认可和信任,同时也彰显了Coremail在邮件安全领域的卓越实力。

438d0df1a5824e3eb3cec6ffdfd1e1e9.jpg

此次独家授权,为Coremail带来更多的技术优势和市场竞争优势,进一步巩固其在邮件安全领域的领先地位。同时,对于广大用户来说,这也意味着用户将能够享受到更加安全、高效的邮件安全服务。

未来,Coremail将继续秉持技术创新的精神,致力于为用户提供更优质、安全、智能的邮件安全服务。与此同时,Coremail也将与网易保持紧密的合作关系,为企业的邮件安全保驾护航。

收起阅读 »

微信小程序避坑scroll-view,用tween.js实现吸附动画

web
背景 在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果): 很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-...
继续阅读 »

背景


在开发一个小程序项目时,遇到一个需要列表滚动,松手时自动吸附的动画需求,如下(最终效果):


吸附动画.gif


很自然用了scroll-view组件,但使用过程中发现scroll-view 里包含'position: fixed;top:0;'的元素时,应用scroll-with-animation=true,搭配更改scroll-top时,松手后fixed的元素会抖一下......


问题.gif


于是决定不用组件内置的scroll-with-animation,改用手动控制scroll-top实现吸附的效果。


思路


通常,要做动画,我们就得确定以下信息,然后用代码实现:



  • 初始状态

  • 结束状态

  • 动画时长

  • 动画过程状态如何变化(匀速/先加速后减速/...)


这四个信息一般从UI/交互那里确认,前三个代码实现很简单,第四个在css动画里(如transition/animation)用 timing-function 指定:


image.png


在js动画里,改变css的属性可通过 Web Animations API 里的 easing 属性指定:


image.png


而如果需要动画的状态不是css的属性呢(例如上面的scrollTop)?这就要用到补间/插值工具了,tween.js登场!


关于 tween.js


tween翻译有‘补间‘的意思



补间(动画)(来自 in-between)是一个概念,允许你以平滑的方式更改对象的属性。你只需告诉它哪些属性要更改,当补间结束运行时它们应该具有哪些最终值,以及这需要多长时间,补间引擎将负责计算从起始点到结束点的值。



简单点就是tweenjs可以指定状态从初始值到结束值该怎么变化,下面是简单的使用例子:


const box = document.getElementById('box') // 获取我们想要设置动画的元素。
const coords = {x: 0, y: 0} // 从 (0, 0) 开始

const tween = new TWEEN.Tween(coords, false) // 创建一个修改“坐标”的新 tween。
.to({x: 300, y: 200}, 1000) // 在 1 秒内移动到 (300, 200)。
.easing(TWEEN.Easing.Quadratic.InOut) // 使用缓动函数使动画流畅。
.onUpdate(() => {
// 在 tween.js 更新“坐标”后调用。
// 使用 CSS transform 将 'box' 移动到 'coords' 描述的位置。
box.style.setProperty('transform', 'translate(' + coords.x + 'px, ' + coords.y + 'px)')
})
.start() // 立即开始 tween。

// 设置动画循环。
function animate(time) {
tween.update(time)
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)


在微信小程序里使用tween.js


导入适配


下载 github.com/tweenjs/twe… 文件,修改下‘now’的实现,把performance.now()改成Date.now() 即可在小程序里使用:


image.png


动画循环


小程序里没有直接支持requestAnimationFrame,这个可以用canvas组件的requestAnimationFrame方法代替:


    // wxml
// ...
<canvas type="2d" id="canvas" style="width: 0; height: 0; pointer-events: none; position: fixed"></canvas>
// ...


// js
wx.createSelectorQuery()
.select("#canvas")
.fields({
node: true,
})
.exec((res) => {
this.canvas = res[0].node;
});
// ...

// ...
const renderLoop = () => {
TWEEN.update();
this.canvas.requestAnimationFrame(renderLoop);
};
renderLoop();

其他


锁帧


手动改scrolltop还是得通过setData方法,频繁调用可能会导致动画卡顿,requestAnimationFrame一般1s跑60次,也就是60fps,根据需要可以增加锁帧逻辑:


const fps = 30; // 锁30fps
const interval = 1000 / fps;
let lastTime = Date.now();
const renderLoop = () => {
this.canvas.requestAnimationFrame(renderLoop);

const now = Date.now();
if(now - lastTime > interval){
// 真正的动作在这里运行
TWEEN.update();
lastTime = now;
}
};
renderLoop();

官方支持?


要是 scrollView 组件支持 wxs 更改scrollTop就好了
developers.weixin.qq.com/community/d…


作者:思路为王
来源:juejin.cn/post/7300771357523820594
收起阅读 »

前端滑块旋转验证登录

web
效果图如下 实现: 封装VerifyImg组件 <template> <el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog"> ...
继续阅读 »
效果图如下

效果.gif


实现: 封装VerifyImg组件

<template>
<el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog">
<div class="verify-v">
<div
class="check"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
@mousemove="onMouseMove"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
>

<p>拖动滑块使图片角度为正</p>
<div class="img-con">
<img :src="imgUrl" :style="{ transform: imgAngle }" />
<div v-if="showError" class="check-state">验证失败</div>
<div v-else-if="showSuccess" class="check-state">验证成功</div>
<div v-else-if="checking" class="check-state">验证中</div>
</div>
<div
ref="sliderCon"
class="slider-con"
:class="{ 'err-anim': showError }"
:style="{ '--percent': percent, '--bgColor': showError ? bgError : bgColor }"
>

<div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
<el-icon size="22"><Right id="slider" /></el-icon>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
export default {
data() {
return {
imgUrl: '',
dialogShow: false,
showError: false,
showSuccess: false,
checking: false,
sliding: false,
slidMove: 0,
percent: 0,
sliderConWidth: 0,
bgColor: 'rgba(25, 145, 250, 0.2)',
bgError: 'rgba(255,78,78,0.2)',
imgList: [
new URL(`../../assets/images/verify/fn1.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn2.png`, import.meta.url).href,
new URL(`../../assets/images/verify/fn3.png`, import.meta.url).href
]
}
},

computed: {
angle() {
let sliderConWidth = this.sliderConWidth ?? 0
let sliderWidth = this.sliderWidth ?? 0
let ratio = this.slidMove / (sliderConWidth - sliderWidth)
return 360 * ratio
},
imgAngle() {
return `rotate(${this.angle}deg)`
}
},
mounted() {
this.imgUrl = this.imgList[this.rand(0, 2)]
},

methods: {
onTouchMove(event) {
console.log('onTouchMove')
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.touches[0].clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
console.log(this.slidMove)

this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
onTouchEnd() {
console.log('onTouchEnd')
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
onTouchStart(event) {
console.log('onTouchStart', event)
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.touches[0].clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
console.log(this.sliderLeft, this.sliderConWidth, this.sliderWidth)
},
rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1) + m - 1)
},

showVerify() {
this.imgUrl = this.imgList[this.rand(0, 2)]
this.dialogShow = true
},
closeVerify() {
//1.5s后关闭弹框
setTimeout(() => {
this.dialogShow = false
}, 1500)
},
// 重置滑块
resetSlider() {
this.sliding = false
this.slidMove = 0
this.checking = false
this.showSuccess = false
this.showError = false
this.percent = 0
},
//拖拽开始
onMouseDown(event) {
console.log(event.target.id, this.checking)
if (event.target.id !== 'slider') {
return
}

if (this.checking) return
// 设置状态为滑动中
this.sliding = true
// 下面三个变量不需要监听变化,因此不放到 data 中
this.sliderLeft = event.clientX // 记录鼠标按下时的x位置
this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
},
//拖拽停止
onMouseUp(event) {
if (this.sliding && this.checking === false) {
this.checking = true
this.validApi(this.angle)
.then(isok => {
if (isok) {
this.showSuccess = true
} else {
this.showError = true
}
return new Promise((resolve, reject) => {
// setTimeout(() => {
if (isok) {
resolve(Math.round(this.angle))
} else {
reject()
}
setTimeout(() => {
this.resetSlider()
}, 1000)
// }, 1500)
})
})
.then(angle => {
// 处理业务,或者通知调用者验证成功
console.log(angle)
this.$emit('toLogin')
})
.catch(e => {
console.log(e)

// alert('旋转错误')
})
}
},
//拖拽进行中
onMouseMove(event) {
if (this.sliding && this.checking === false) {
// 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
let m = event.clientX - this.sliderLeft
if (m < 0) {
// 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
// 所以直接等于 0,以防止越界
m = 0
} else if (m > this.sliderConWidth - this.sliderWidth) {
// 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
// 因为css的 translateX 函数是以元素的左上角坐标计算的
// 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
m = this.sliderConWidth - this.sliderWidth
}
this.slidMove = m
this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
}
},
// 验证角度是否正确
validApi(angle) {
return new Promise((resolve, reject) => {
// 模拟网络请求
setTimeout(() => {
// 图片已旋转的角度
const imgAngle = 90
// 图片已旋转角度和用户旋转角度之和
let sum = imgAngle + angle
// 误差范围
const errorRang = 20
// 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
// 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
let isOk = Math.abs(360 - sum) <= errorRang

resolve(isOk)
}, 1000)
})
}
}
}
</script>

<style lang="scss">
.verifyDialog {
.el-dialog__body {
padding: 15px !important;
}
}
</style>
<style lang="scss" scoped>
.verify-v {
display: flex;
justify-content: center;
align-items: center;
}
.check {
--slider-size: 40px;
width: 300px;
background: white;
box-shadow: 0px 0px 12px rgb(0 0 0 / 8%);
border-radius: 5px;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
.img-con {
position: relative;
overflow: hidden;
width: 120px;
height: 120px;
border-radius: 50%;
margin-top: 20px;
img {
width: 100%;
height: 100%;
user-select: none;
}
.check-state {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
color: white;
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.check .slider-con {
width: 80%;
height: var(--slider-size);
border-radius: 3px;
margin-top: 1rem;
position: relative;
background: #f5f5f5;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;
background: linear-gradient(to right, var(--bgColor) 0%, var(--bgColor) var(--percent), #fff var(--percent), #fff 100%);
.slider {
&:hover {
background: #1991fa;
color: #fff;
}
background: #fff;
width: var(--slider-size);
height: var(--slider-size);
border-radius: 3px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
cursor: move;
display: flex;
justify-content: center;
align-items: center;
--move: 0px;
transform: translateX(var(--move));
.sliding {
background: #4ed3ff;
}
}
}
.slider-con.err-anim {
animation: jitter 0.5s;
.slider {
background: #ff4e4e;
}
}
body {
padding: 0;
margin: 0;
background: #fef5e0;
}

@keyframes jitter {
20% {
transform: translateX(-5px);
}
40% {
transform: translateX(10px);
}
60% {
transform: translateX(-5px);
}
80% {
transform: translateX(10px);
}
100% {
transform: translateX(0);
}
}
</style>


使用

<VerifyImg ref="verifyRef" @to-login="handleLogin"></VerifyImg>


handleLogin(){
...
}

作者:点赞侠01
来源:juejin.cn/post/7358004857889275958
收起阅读 »

API接口超时,网络波动,不要一直弹Alert了!

web
前言前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时,服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误。由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请...
继续阅读 »

前言

前段时间,我司的IoT平台的客户大量上新了一堆设备后,在使用过程中,出现了网络连接超时服务器错误等问题,客户小题大做,抓着这个尾巴不放手,喷我司系统不健壮,要求我们杜绝API错误

由于IoT多是日志、设备、测试、Action、图表、通信类的功能,API请求&返回数据量大、时间长。在后端添加索引的情况下,接口仍然达不到秒级,接口普遍都是1~3s,部分接口甚至达到小10s。我司把服务器的硬盘和CPU都进行了一波升级,效果都不是很理想。

这个烫手的山芋,落在了前端的头上。我们被迫进行了一波系统升级优化

解决方案

我们结合这个需求,制定了以下几条标准:

  1. 不能入侵其他的功能
  2. 对系统的破坏尽可能的小
  3. 杜绝或者尽可能的减少弹框问题
  4. 保证数据的正确展示,对于错误要正确的暴露出来

根据以上几条标准,于是方案就自然的确定了:

API请求时间

拉长API的请求时间,将超时时间由30s,更新为60s

const service = axios.create({
baseURL: '/xxx',
timeout: 60 * 1000 // 请求超时时间
})

重发机制

  1. API请求超时: 当新页面大量接口请求,某个接口请求时间过长,总时间>60s时,我们会对这个接口进行至多重发3次,用180s的时间去处理这个接口,当请求成功后,关闭请求

重发的接口不能超过timeout时长,否则一直会重发失败。也可以自定义重发的时间

  1. 偶发的服务器异常: 当接口出现50X时,重发一次

可以使用axois自带的方法,也可以使用axios-retry插件,axios-retry插件更简单,底层也是axois方法实现的。相比较而言,axios-retry更简单,但不知为什么,我没有实现

// request.js
// 默认重发3次,每次的间隔时间为3s
service.defaults.retry = 3;
service.defaults.retryDelay = 3000;

export function againRequest(
error,
axios,
time = error.config.retry
) {
const config = error.config;

if (!config || !config.retry) return Promise.reject(error);

// 设置用于记录重试计数的变量 默认为0
config.__retryCount = config.__retryCount || 0;

// 判断是否超过了重试次数
if (config.__retryCount >= time) {
// alert

return Promise.reject(error);
}

config.__retryCount += 1;

const backoff = new Promise(resolve => {
setTimeout(() => {
resolve();
}, config.retryDelay || 1);
});

return backoff.then(() => {
/*
以下三行,根据项目实际调整
这三行是处理重发请求接口变成string,并且重定向的问题
*/

if (config.data && isJsonStr(config.data)) {
config.data = JSON.parse(config.data);
}
config.baseURL = "/";

return axios(config);
});
}

export let isLoop = config => {
if (
config.isLoop &&
config.isLoop.count >= 0 &&
config.url == config.isLoop.url
) {
return true;
}
return false;
};

export let isJsonStr = str => {
if (typeof str == "string") {
try {
var obj = JSON.parse(str);
if (typeof obj == "object" && obj) {
return true;
} else {
return false;
}
} catch (e) {
console.log("error: " + str + "!!!" + e);
return false;
}
}
};

注意到是: axois不能是0.19.x

issue中提到使用0.18.0。经过实践,0.20.x也是可以的,但需要进行不同的配置,具体请看issue。参考资料中有提到 axios issues github

也可以使用axios-retry

axios-retry

npm install axios-retry

// ES6
import axiosRetry from 'axios-retry';

axiosRetry(axios, { retries: 3 });

取消机制

当路由发生变化时,取消上一个路由正在请求的API接口

监控路由页面: 调用cancelAllRequest方法

// request.js
const pendingRequests = new Set();

service.cancelAllRequest = () => {
pendingRequests.forEach(cancel => cancel());
pendingRequests.clear();
};

轮询

轮询有2种情况,一种是定时器不停的请求,一种是监听请求N次后停止。

比如: 监听高低电平的变化 - 如快递柜的打开&关闭。

  1. 一直轮询的请求:

    • 使用WebSocket
    • 连续失败N次后,谈框。
  2. 轮询N次的请求:

    • 连续失败N次后,谈框。
export function api(data, retryCount) {
return request({
url: `/xxx`,
method: "post",
isLoop: {
url: "/xxx",
count: retryCount
},
data: { body: { ...data } }
});
}

自定义api url的原因是:

同一个页面中,有正常的接口和轮询的接口,url是区分是否当前的接口是否是轮询的接口

监听滚动

对于图表类的功能,监听滚动事件,根据不同的高度请求对应的API

节流机制

  1. 用户连续多次请求同一个API
    • 按钮loading。最简单有效
    • 保留最新的API请求,取消相同的请求

错误码解析

网络错误 & 断网

if (error.toString().indexOf("Network Error") !== -1) {
if (configData.isLoop && configData.isLoop.count >= 0) {
networkTimeout();
// 关闭所有的定时器
let t = setInterval(function() {}, 100);
for (let i = 1; i <= t; i++) {
clearInterval(i);
}
return;
}
networkTimeout();
}

404

else if (error.toString().indexOf("404") !== -1) {
// 404
}

401

else if (error.toString().indexOf("401") !== -1) {
// 清除token,及相应的数据,返回到登录页面
}

超时

else if (error.toString().indexOf("Error: timeout") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service);
}
if (configData.isLoop && configData.isLoop.count === 3) {
requestTimeout();
}
}

50X

else if (error.toString().indexOf("50") !== -1) {
if (!axios.isCancel(error) && !isLoop(configData)) {
return againRequest(error, service, 1, 2);
}
if (configData.isLoop && configData.isLoop.count === 1) {
requestTimeout();
}
}

未知错误

else {
// 未知错误,等待以后解析
}

总结

结果将状态码梳理后,客户基本看不到API错误了,对服务的稳定性和可靠性非常满意,给我们提出了表扬和感谢。我们也期待老板升职加薪!

参考资料


作者:高志小鹏鹏
来源:juejin.cn/post/7413187186131533861

收起阅读 »

为什么2.01 变成了 2.00 ,1分钱的教训不可谓不深刻

前些日子,测试提过来一个bug,说下单价格应该是 2.01,但是在订单详情中展示了2.00元。我头嗡的一下子,艹,不会是因为double 的精度问题吧~果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用double 进行了金额转换,导致金额不准。我马...
继续阅读 »

前些日子,测试提过来一个bug,说下单价格应该是 2.01,但是在订单详情中展示了2.00元。我头嗡的一下子,艹,不会是因为double 的精度问题吧~

果不其然,经过排查代码,最终定位原因订单详情展示金额时,使用double 进行了金额转换,导致金额不准。

我马上排查核心购买和售后链路,发现涉及资金交易的地方没有问题,只有这一处问题,要不然这一口大锅非得扣我身上。

为什么 2.01 变成了 2.0

2.01等小数 在计算机中按照2进制补码存储时,存在除不尽,精度丢失的问题。 例如 2.01的补码为 000000010.009999999999999787 。正如十进制场景存在 1/3等无限小数问题,二进制场景也存在无限小数,所以一定会存在精度问题。

什么场景小数转换存在问题

for (int money = 0; money < 10000; money++) {
String valueYuan = String.format("%.2f", money * 1.0 / 100);

int value = (int) (Double.valueOf(valueYuan) * 100);
if (value != money) {
System.out.println(String.format("原值: %s, 现值:%s", money, value));
}
}

如上代码中,先将数字 除以 100,转为元, 精度为2位,然后将double 乘以100,转为int。 在除以、乘以两个操作后,精度出现丢失。

我把1-10000 的范围测试一遍,共有573个数字出现精度转换错误。 这个概率已经相当大了。

如何转换金额更安全?

Java 提供了BigDecimcal 专门处理精度更高的浮点数。简单封装一下代码,元使用String表示,分使用int表示。提供两个方法实现 元和分的 互转。

public static String change2Yuan(int money) {
BigDecimal base = BigDecimal.valueOf(money);
BigDecimal yuanBase = base.divide(new BigDecimal(100));
return yuanBase.setScale(2, BigDecimal.ROUND_HALF_UP).toString();
}

public static int change2Fen(String money) {
BigDecimal base = new BigDecimal(money);

BigDecimal fenBase = base.multiply(new BigDecimal(100));
return fenBase.setScale(0, BigDecimal.ROUND_HALF_UP).intValue();
}

测试

测试0-1 亿 的金额转换逻辑,均成功转换,不存在精度丢失。

int error = 0;
long time = System.currentTimeMillis();
for (int money = 0; money < 100000000; money++) {
String valueYuan = change2Yuan(money);

int value = change2Fen(valueYuan);
if (value != money) {
error++;
}
}
System.out.println(String.format("时间:%s", (System.currentTimeMillis() - time)));
System.out.println(error);

性能测试

网上很多人说使用 BigDecimcal 存在性能影响,但是我测试性能还是不错的。可能首次耗时略高,大约2ms

标题耗时
0-1亿14.9 秒
0-100万0.199秒
0-1万0.59秒
0-1000.004秒
0-10.002秒

总结

涉及金额转换的 地方,一定要小心处理,防止出现精度丢失问题。可以使用代码审查工具,查看代码中是否存在使用double 进行金额转换的代码, 同时提供 金额转换工具类。


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

Video.js:视频播放的全能解决方案

web
大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。前言在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Vid...
继续阅读 »

大家好,我是徐徐。今天跟大家分享一款多媒体类前端工具库:Video.js。

前言

在现代网页开发中,视频播放功能已经成为用户体验的一个重要组成部分。无论你是开发一个视频分享平台还是一个简单的博客,选择合适的视频播放器都至关重要。今天,我们要介绍的 Video.js 是一个强大且灵活的 HTML5 视频播放器,它能够满足你对视频播放的所有需求。

基本信息

什么是 Video.js?

Video.js 是一个从零开始为 HTML5 世界打造的网页视频播放器。它不仅支持 HTML5 视频和现代流媒体格式,还支持 YouTube 和 Vimeo。自2010年中期项目启动以来,Video.js 已经发展成为一个拥有数百名贡献者并广泛应用于超过** 80 **万个网站的播放器。

主要特点

  • 全能播放Video.js 支持传统的视频格式,如 MP4 和 WebM,同时也支持自适应流媒体格式,如 HLS 和 DASH。对于直播流,Video.js 还提供了专门的用户界面,使直播体验更加流畅。
  • 易于定制:虽然 Video.js 自带的播放器界面已经非常美观,但它的设计也考虑到了可定制性。通过简单的 CSS 你可以轻松地为播放器增添个人风格,使其更符合你的网页设计需求。
  • 丰富的插件生态:当你需要额外功能时,Video.js 的插件架构能够满足你的需求。社区已经开发了大量的插件和皮肤,包括 Chromecast、IMA 以及 VR 插件,帮助你快速扩展播放器的功能。

使用场景

Video.js 适用于各种视频播放场景:

  • 视频分享平台:无论是播放本地视频还是流媒体内容,Video.js 都能提供稳定的播放体验。
  • 直播应用:通过专用的直播流 UI,Video.js 能够实现高质量的实时视频播放。
  • 教育和培训平台:支持多种格式和流媒体,确保你的教学视频能够在不同设备上顺畅播放。

快速上手

要在你的网页中使用 Video.js,只需以下简单步骤:

  1. 引入 Video.js 的库

<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.min.js">script>


<link href="https://unpkg.com/video.js@8.17.3/dist/video-js.min.css" rel="stylesheet">
<script src="https://unpkg.com/video.js@8.17.3/dist/video.min.js">script>


<link href="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video-js.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.17.3/video.min.js">script>

  1. 添加视频播放器元素
<video
id="my-player"
class="video-js"
controls
preload="auto"
poster="//vjs.zencdn.net/v/oceans.png"
data-setup='{}'>

<source src="//vjs.zencdn.net/v/oceans.mp4" type="video/mp4">source>
<source src="//vjs.zencdn.net/v/oceans.webm" type="video/webm">source>
<source src="//vjs.zencdn.net/v/oceans.ogv" type="video/ogg">source>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a
web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">
supports HTML5 video
a>
p>
video>
  1. 初始化播放器
var player = videojs('my-video');

就这样,你就可以在网页上嵌入一个功能丰富的视频播放器了。

videojs函数还接受一个options对象和一个回调:

var options = {};

var player = videojs('my-player', options, function onPlayerReady() {
videojs.log('Your player is ready!');

// In this context, `this` is the player that was created by Video.js.
this.play();

// How about an event listener?
this.on('ended', function() {
videojs.log('Awww...over so soon?!');
});
});

结语

Video.js 是一个功能强大且灵活的视频播放器,它支持多种视频格式和流媒体协议,并且具有丰富的插件生态和良好的定制性。无论你是构建一个视频分享平台还是需要实现高质量的直播播放,Video.js 都能为你提供稳定且可扩展的解决方案。

希望这篇文章能帮助你了解 Video.js 的强大功能,并激发你在项目中使用它的灵感,这么好的东西,赶快分享给你的朋友们吧!


作者:前端徐徐
来源:juejin.cn/post/7411046020840964131
收起阅读 »

文档协同软件是如何解决编辑冲突的?

web
前言 本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。 解决冲突的方案 在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决...
继续阅读 »

前言


本文将介绍在线协同文档编辑器是如何解决冲突的,大部分公司在解决冲突上目前用的都是 OT 算法,与之对应,也有一个 CRDT 算法实现。接下来,我们将深入这两种算法的实现及原理。


解决冲突的方案


在线协同文档编辑器通常使用不同的算法来解决冲突,具体算法取决于编辑器的实现和设计。以下是一些常见的解决冲突的算法:



  1. OT(Operational Transformation,操作转换):这是一种常见的解决冲突的算法,用于实现实时协同编辑。OT算法通过将用户的编辑操作转换为操作序列,并在多个用户同时编辑时进行操作转换,以确保最终的文档状态一致。

  2. CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型):这是一种基于数据结构的解决冲突的算法,它允许多个用户在不同的副本上进行并发编辑,并最终将编辑结果合并为一致的文档状态。CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。


这些算法都有各自的优缺点,并且在不同的场景和需求下可能更适合不同的编辑器实现。


接下来,我们先聊聊 OT 算法。


OT 算法


image.png


当多个用户同时编辑同一文档时,OT算法通过操作转换来保持文档状态的一致性。下面是一个简单的示例,帮助你理解OT算法的工作原理。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为 "Hello, world!"。


用户 A 在文本末尾添加了字符 " How are you?"。


用户 B 在文本末尾添加了字符 " I'm fine."。


在 OT 算法中,每个操作都由一个操作类型和操作内容组成。在这个例子中,添加字符操作的操作类型为 "insert",操作内容为被插入的字符。


用户 A 的操作序列为:[insert(" How are you?")]
用户 B 的操作序列为:[insert(" I'm fine.")]


首先,服务器收到用户 A 的操作序列,将其应用到初始文本上,得到 "Hello, world! How are you?"。然后,服务器收到用户 B 的操作序列,将其应用到初始文本上,得到 "Hello, world! I'm fine."。


接下来,服务器需要进行操作转换,将用户 B 的操作序列转换为适应用户 A 的文本状态。在这个例子中,用户 B 的操作 "insert(" I'm fine.")" 需要转换为适应 "Hello, world! How are you?" 的操作。


操作转换的过程如下:



  1. 用户 A 的操作 "insert(" How are you?")" 在用户 B 的操作 "insert(" I'm fine.")" 之前发生,因此用户 B 的操作不会受到影响。

  2. 用户 B 的操作 "insert(" I'm fine.")" 在用户 A 的操作 "insert(" How are you?")" 之后发生,因此用户 B 的操作需要向后移动。

  3. 用户 B 的操作 "insert(" I'm fine.")" 向后移动到 "Hello, world! How are you? I'm fine."。


最终,服务器将转换后的操作序列发送给用户 A 和用户 B,他们将其应用到本地文本上,最终得到相同的文本状态 "Hello, world! How are you? I'm fine."。


这个例子展示了 OT 算法如何通过操作转换来保持多个用户同时编辑时文档状态的一致性。在实际应用中,OT 算法需要处理更复杂的操作类型和情况,并进行更详细的操作转换。


接下来,我们聊聊 CRDT 算法:


CRDT 算法


image.png


当多个用户同时编辑同一文档时,CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。下面是一个简单的示例,帮助你理解CRDT算法的工作原理。


在CRDT算法中,文档被表示为一个数据结构,常见的数据结构之一是有序列表(Ordered List)。每个用户的编辑操作会被转换为操作序列,并应用到本地的有序列表上。


假设有两个用户 A 和 B 同时编辑一段文本,初始文本为空。用户 A 在文本末尾添加了字符 "Hello",用户 B 在文本末尾添加了字符 "World"。


在CRDT算法中,每个字符都被赋予一个唯一的标识符,称为标记(Marker)。在这个例子中,我们使用递增的整数作为标记。


用户 A 的操作序列为:[insert("H", 1), insert("e", 2), insert("l", 3), insert("l", 4), insert("o", 5)]
用户 B 的操作序列为:[insert("W", 6), insert("o", 7), insert("r", 8), insert("l", 9), insert("d", 10)]


每个操作都包含要插入的字符以及对应的标记。


当用户 A 应用自己的操作序列时,有序列表变为 "Hello"。同样地,当用户 B 应用自己的操作序列时,有序列表变为 "World"。


接下来,服务器需要将两个用户的操作序列合并为一致的文本状态。在CRDT算法中,合并的过程是通过比较标记的大小来确定字符的顺序,并确保最终的文本状态一致。


合并的过程如下:



  1. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "H" 在 "W" 之前。

  2. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "e" 在 "o" 之前。

  3. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "r" 之前。

  4. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "l" 在 "l" 之前。

  5. 用户 A 的操作序列中的标记小于用户 B 的操作序列中的标记,因此 "o" 在 "d" 之前。


最终,合并后的有序列表为 "HelloWorld"。


这个例子展示了CRDT算法如何通过设计特定的数据结构和操作规则,以及比较标记的大小来实现冲突自由的复制。在实际应用中,CRDT算法可以应用于各种数据结构和操作类型,并进行更复杂的合并过程。


CRDT 的标记实现方案



  1. 递增整数标记:最简单的标记实现方式是使用递增的整数作为标记。每个操作都会分配一个唯一的整数标记,标记的大小可以用来比较操作的发生顺序。

  2. 时间戳标记:另一种常见的标记实现方式是使用时间戳作为标记。每个操作都会被分配一个时间戳,可以使用系统时间或逻辑时钟来生成时间戳。时间戳可以用来比较操作的发生顺序。

  3. 向量时钟标记:向量时钟是一种用于标记并发事件的数据结构。每个操作都会被分配一个向量时钟标记,向量时钟由多个节点的时钟组成,每个节点维护自己的逻辑时钟。向量时钟可以用来比较操作的发生顺序,并检测并发操作之间的关系。

  4. 哈希值标记:有些情况下,可以使用操作内容的哈希值作为标记。每个操作的内容都会被哈希为一个唯一的标记,这样可以通过比较哈希值来比较操作的发生顺序。


方案选型


OT算法和CRDT算法都是用于实现实时协同编辑的算法,但它们有不同的优点、缺点和工作原理。


OT算法的优点:



  1. 简单性:OT算法相对较简单,易于理解和实现。

  2. 实时协同编辑:OT算法可以实现实时协同编辑,多个用户可以同时编辑文档,并通过操作转换保持文档状态的一致性。


OT算法的缺点:



  1. 操作转换复杂性:OT算法的操作转换过程相对复杂,需要考虑多种情况和操作类型,实现和测试上有一定的挑战。

  2. 需要服务器参与:OT算法需要中央服务器来处理操作转换,这可能会导致一定的延迟和依赖性。


CRDT算法的优点:



  1. 冲突自由:CRDT算法设计了特定的数据结构和操作规则,可以实现冲突自由的复制,多个用户同时编辑时不会发生冲突。

  2. 去中心化:CRDT算法可以在去中心化的环境中工作,不依赖于中央服务器进行操作转换,每个用户可以独立地进行编辑和合并。


CRDT算法的缺点:



  1. 数据结构复杂性:CRDT算法的数据结构和操作规则相对复杂,需要更多的设计和实现工作。

  2. 数据膨胀:CRDT算法可能会导致数据膨胀,特别是在复杂的编辑操作和大规模的协同编辑场景下。


OT算法和CRDT算法的区别:



  1. 算法原理:OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则来实现冲突自由的复制。

  2. 中心化 vs. 去中心化:OT算法需要中央服务器进行操作转换,而CRDT算法可以在去中心化的环境中工作,每个用户都有完整的本地数据,可以将操作发送给服务器,然后网络分发到其他客户端。

  3. 复杂性:OT算法相对较简单,但在处理复杂操作和转换时可能会变得复杂;CRDT算法相对复杂,需要更多的设计和实现工作。


选择使用哪种算法取决于具体的应用场景和需求。OT算法适用于需要实时协同编辑的场景,而CRDT算法适用于去中心化和冲突自由的场景。


总结


本文介绍了在线协同文档编辑器中解决冲突的算法,主要包括OT算法和CRDT算法。OT算法通过操作转换来保持文档状态的一致性,而CRDT算法通过设计特定的数据结构和操作规则实现冲突自由的复制。OT算法需要中央服务器进行操作转换,适用于需要实时协同编辑的场景。CRDT算法可以在去中心化环境中工作,每个用户都有完整的本地数据,适用于去中心化和冲突自由的场景。选择使用哪种算法取决于具体的应用场景和需求。


作者:谦宇
来源:juejin.cn/post/7283018190593785896
收起阅读 »

audio自动播放为什么会失败

web
背景 某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音 复线步骤 测试后发现如下结论 当刷新页面后,audio不会自动播放 当从另外的一个页面进入到当前页面,可以直接播放声音 如果你想测试...
继续阅读 »

背景


某天客户报了一个问题,说是大屏的声音不能自动播放了,我们的大屏应用是有个报警的,当有报警的时候,自动会播放警报的声音


复线步骤


测试后发现如下结论



  1. 当刷新页面后,audio不会自动播放

  2. 当从另外的一个页面进入到当前页面,可以直接播放声音


如果你想测试,可以点我进行测试


你可以先点击上方链接的 尝试一下 ,下方为截图


image.png


这个时候你会听到一声马叫声


然后,你刷新下马叫声的页面,这个时候声音的自动播放将不会生效


image.png


报错问题排查


打开控制台,不出意外看到了一个报错信息。


image.png


翻译为中文的意思为
不允许的错误。播放失败,因为用户没有先与文档交互。goo.gl/xX8pDD


尝试解决


那我给通过给body添加点击事件,自动触发点击事件,在点击的事件后自动播放声音。
(当是我的想法是,这个大概率是不行的,chrome应该不会忽略这一点,不然这个功能就相当于不存在)


经过测试后,发现确实还不行,在意料中。


参考别人的网站,用抖音测试


点击我跳转抖音


想到了我们可以参考抖音,我用抖音的进行测试,当你不做任何的操作,页面应该如下
image.png


我们从这里得出结论,这个应该是浏览器的限制,需要查看官方文档,看看原因


查阅官方文档


点我查看chrome的官方文档


我截取了一些关键的信息


image.png


注意浏览器会有一个媒体互动指数,这是浏览器自动计算的,该分越高,才会触发自动播放


查看电脑的媒体互动指数


在url上输入 about://media-engagement,你会看到如下的截图,


image.png


经过测试后 当网站变成了is High,音频会自动播放,不会播放失败。


这就解释了为什么有的网站可以自动播放声音,有的网站不可以自动播放声音


ok,我们继续往下看,这个时候看到了一些关键的信息。


作为开发者,我们不应该相信音频/视频会播放成功,要始终在播放的回掉中来进行判断


image.png


看到这些,我们来模仿抖音的实现.在播放声音的catch的时候,显示一个错误的弹窗,提示用户,当用户点击的时候,自动播放声音


   this.alarmAudio = new Audio(require("@/assets/sound/alarm.mp3"));
this.alarmAudio
.play()
.then(() => {
this.notifyId && this.notifyId.close();
})
.catch((error) => {
if (error instanceof DOMException) {
// 这里可以根据异常类型进行相应的错误处理
if (error.name === "NotAllowedError") {
if (this.notifyId) return;
this.notifyId = Notification({
title: "",
duration: 0,
position: "bottom-left",
dangerouslyUseHTMLString: true,
onClick: this.onAudioNotifyConfirm,
showClose: false,
customClass: "audio-notify-confirm",
message:
"<div style='color:#fff;font-size:18px;cursor:pointer'>因浏览器限制,需<span style='color:#ff2c55'>点击打开声音</span></div>",
});
}
}
});

实现效果如下


image.png


总结



  1. 在用到video或者audio的时候,要始终不相信他会播放声音成功,并且添加catch处理异常场景,给用户友好的提示

  2. video或者audio的自动播放跟媒体互动指数有关(MEI),当媒体指数高,会自动播放,否则需要用户先交互后,audio才可以自动播放。

  3. 从一个页面window.open另外一个页面可以自动播放声音,当刷新页面后,需要有高的MEI,audio才会自动播放,如果你需要在后台打开一个大屏的页面,刚好可以这样设计,不要用页面跳转


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

我的 Electron 客户端也可以全量/增量更新了

前言 本文主要介绍 Electron 客户端应用的自动更新,包括全量和增量这两种方式。 全量更新: 运行新的安装包,安装目录所有资源覆盖式更新。 增量更新: 只更新修改的部分,通常是渲染进程和主进程文件。 本文并没有拿真实项目来举例子,而是起一个 新的项目 从...
继续阅读 »

前言


本文主要介绍 Electron 客户端应用的自动更新,包括全量和增量这两种方式。


全量更新: 运行新的安装包,安装目录所有资源覆盖式更新。


增量更新: 只更新修改的部分,通常是渲染进程和主进程文件。


本文并没有拿真实项目来举例子,而是起一个 新的项目 从 0 到 1 来实现自动更新。


如果已有的项目需要支持该功能,借鉴本文的主要步骤即可。


前置说明:



  1. 由于业务场景的限制,本文介绍的更新仅支持 Windows 操作系统,其余操作系统未作兼容处理。

  2. 更新流程全部由前端完成,不涉及到后端,没有轮询 更新的机制。

  3. 发布方式限制为 generic,线上服务需要配置 nginx 确保访问到资源文件。


准备工作


脚手架搭建项目


我们通过 electron-vite 快速搭建一个基于 Vite + React + TSElectron 项目。


1.jpg


该模板已经包括了我们需要的核心第三方库:electron-builderelectron-updater


前者是用来打包客户端程序的,后者是用来实现自动更新的。


在项目根目录下,已经自动生成了两份配置文件:electron-builder.ymldev-app-update.yml


electron-builder.yml


该文件描述了一些 打包配置,更多信息可参考 官网


在这些配置项中,publish 字段比较重要,因为它关系到更新源。


publish:
provider: generic // 使用一个通用的 HTTP 服务器作为更新源。
url: https://example.com/auto-updates // 存放安装包和描述文件的地址

provider 字段还有其他可选项,但是本文只介绍 generic 这种方式,即把安装包放在 HTTP 服务器里。


dev-app-update.yml


provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: electron-update-demo-updater

其中,updaterCacheDirName 定义下载目录,也就是安装包存放的位置。全路径是C:\Users\用户名\AppData\Local\electron-update-demo-updater,不配置则在C:\Users\用户名\AppData\Local下自动创建文件夹,开发环境下为项目名,生产环境下为项目名-updater


image.png


模拟服务器


我们直接运行 npm run build:win,在默认 dist 文件夹下就出现了打包后的一些资源。


image.png


其实更新的基本思路就是对比版本号,即对比本地版本和 线上服务器 版本是否一致,若不一致则需要更新。


image.png


因此我们在开发调试的时候,需要起一个本地服务器,来模拟真实的情况。


新建一个文件夹 mockServer,把打包后的 setup.exe 安装包和 latest.yml 文件粘贴进去,然后通过 serve 命令默认起了一个 http://localhose:3000 的本地服务器。


既然有了存放资源的本地地址,在开发调试的时候,我们更新下 dev-app-update.yml 文件的 url 字段,也就是修改为 http://localhose:3000


注:如果需要测试打包后的更新功能,需要同步修改打包配置文件。


全量更新


与主进程文件同级,创建 update.ts 文件,之后我们的更新逻辑将在这里展开。


import { autoUpdater } from 'electron-updater' //核心库

需要注意的是,在我们开发过程中,通过 npm run dev 起来的 Electron 程序其实不能算是打包后的状态。


你会发现在调用 autoUpdater 的一些方法会提示下面的错误:


Skip checkForUpdates because application is not packed and dev update config is not forced


因此我们需要在开发环境去做一些动作,并且手动指定 updateConfigPath


// update.ts
if (isDev) {
Object.defineProperty(app, 'isPackaged', {
get: () => true
})
autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml')
}

核心对象 autoUpdater 有很多可以主动调用的方法,也有一些监听事件,同时还有一些属性可以设置。


这里只展示了本人项目场景所需的一些配置。


autoUpdater.autoDownload = false // 不允许自动下载更新
autoUpdater.allowDowngrade = true // 允许降级更新(应付回滚的情况)

autoUpdater.checkForUpdates()
autoUpdater.downloadUpdate()
autoUpdater.quitAndInstall()

autoUpdater.on('checking-for-update', () => {
console.log('开始检查更新')
})
autoUpdater.on('update-available', info => {
console.log('发现更新版本')
})
autoUpdater.on('update-not-available', info => {
console.log('不需要全量更新', info.version)
})
autoUpdater.on('download-progress', progressInfo => {
console.log('更新进度信息', progressInfo)
})
autoUpdater.on('update-downloaded', () => {
console.log('更新下载完成')
})
autoUpdater.on('error', errorMessage => {
console.log('更新时出错了', errorMessage)
})

在监听事件里,我们可以拿到下载新版本 整个生命周期 需要的一些信息。


// 新版本的版本信息(已筛选)
interface UpdateInfo {
readonly version: string;
releaseName?: string | null;
releaseNotes?: string | Array<ReleaseNoteInfo> | null;
releaseDate: string;
}

// 下载安装包的进度信息
interface ProgressInfo {
total: number;
delta: number;
transferred: number;
percent: number;
bytesPerSecond: number;
}

在写完这些基本的方法之后,我们需要决定 检验更新 的时机,一般是在应用程序真正启动之后,即 mainWindow 创建之后。


运行项目,预期会提示 不需要全量更新,因为刚才复制到本地服务器的 latest.yml 文件里的版本信息与本地相同。修改 version 字段,重启项目,主进程就会提示有新版本需要更新了。


频繁启动应用太麻烦,除了应用初次启动时主进程主动帮我们检验更新外,还需要用户 手动触发 版本更新检测,此外,由于产品场景需要,当发现有新版本的时候,需要在 渲染进程 通知用户,包括版本更新时间和更新内容。因此我们需要加上主进程与渲染进程的 IPC通信 来实现这个功能。


其实需要的数据主进程都能够提供了,渲染进程具体的展示及交互方式就自由发挥了,这里简单展示下我做的效果。


1. 发现新版本
GIF 2024-9-19 16-48-44.gif


2. 无需更新


image.png


增量更新


为什么要这么做


其实全量更新是一种比较简单粗暴的更新方式,我没有花费太大的篇幅去介绍,基本上就是使用 autoUpdater 封装的一些方法,开发过程中更多的注意力则是放在了渲染进程的交互上。


此外,我们的更新交互用的是一种 用户感知 的方式,即更不更新是由用户自己决定的。而 静默更新 则是用户无感知,在监测到更新后不用去通知用户,而是自行在后台下载,在某个时刻执行安装的逻辑,这种方式往往是一键安装,不会弹出让用户去选择安装路径的窗口。接下去要介绍的增量更新其实是这种 用户无感知 的形式,只不过我们更新的不是整个应用程序。


由于产品迭代比较频繁,我们的业务方经常收到更新提示,意味着他们要经常手动更新应用,以便使用到最新的功能特性。众所周知,Electron 给前端开发提供了一个比较容易上手的客户端开发解决方案,但在享受 跨平台 等特性的同时,还得忍受 臃肿的安装包


带宽、流量在当前不是什么大问题,下载和安装的速度也挺快,但这频繁的下载覆盖一模一样的资源文件还是挺糟心的,代码改动量小时,全量更新完全没有必要。我们希望的是,开发更新了什么代码,应用程序就替换掉这部分代码就好了,这很优雅。在 我们的场景 中,这部分代码指的是 —— main、preload、renderer,并不包括 dll第三方SDK等资源。


网上有挺多种增量更新的 解决方案,例如:



  1. 通过 win.loadURL(一个线上地址) 实现,相当于就套了一层客户端的壳子,与加载的Web 端同步更新。这种方法对于简单应用来说是最省心的,但是也有一定的局限性,例如不能使用 node 去操作一些底层的东西。

  2. 设置 asar 的归档方式,替换app.asarapp.asar.unpack来实现。但后者在我实践过程中存在文件路径不存在的问题。

  3. 禁用 asar 归档,解压渲染进程压缩包后实现替换。简单尝试过,首次执行安装包的时候写入文件很慢,不知道是不是这个方式引起的。

  4. 欢迎补充。


本文我们采用较普遍的 替换asar 来实现。


优化 app.asar 体积



asar是 Electron提供的一种将多个文件合并成一个文件的类 tar 风格的归档格式,不仅可以缓解Windows下路径名过长的问题, 还能够略微加快一下 require的速度, 并且可以隐藏你的源代码。(并非完全隐藏,有方法可以找出来)



Electron 应用程序启动的时候,会读取 app.asar.unpacked 目录中的内容合并到 app.asar 的目录中进行加载。在此之前我们已经打包了一份 Demo 项目,安装到本地后,我们可以根据安装路径找到 app.asar 这个文件。


例如:D:\你的安装路径\electron-update-demo\resources


image.png


在这个 Demo 项目中,脚手架生成的模板将图标剥离出来,因此出现了一个 app.asar.unpacked 文件夹。我们不难发现,app.asar 这个文件竟然有 66 MB,再回过头看我们真正的代码文件,甚至才 1 MB !那么到底是什么原因导致的这个文件这么大呢,刚才提到了,asar 其实是一种压缩格式,因此我们只要解压看看就知道了。


npm i -g asar // 全局安装

asar e app.asar folder // 解压到folder文件夹

image.png


解压后我们不难发现,out 文件夹里的才是我们打包出来的资源,罪魁祸首竟然是 node_modules,足足有 62.3 MB


查阅资料得知,Electron 在打包的时候,会把 dependencies 字段里的依赖也一起打包进去。而我们的渲染进程是用 React开发的,这些第三方依赖早就通过 Vite 等打包工具打到资源文件里了,根本不需要再打包一次,不然我们重新下载的 app.asar 文件还是很大的,因此需要尽可能减少体积。


优化应用程序体积 == 减少 node_modules 文件夹的大小 == 减少需要打包的依赖数量 == 减少 dependencies 中的依赖。


1. 移除 dependencies


最开始我想的是把 package.json 中的 dependencies 全都移到 devDependencies,打包后的体积果然减小了,但是在测试过程中会发现某些功能依赖包会缺失,比如 @electron/remote。因此在修改这部分代码的时候,需要特别留意,哪些依赖是需要保留下来的。


由于不想影响 package.json 的版本结构,我只是在写了一个脚本,在 npm i 之后,执行打包命令前修改 devDependencies 就好了。


2. 双 package.json 结构


这是 electron-builder 官网上看到的一种技巧,传送门, 创建 app 文件夹,再创建第二个 package.json,在该文件中配置我们应用的名称、版本、主进程入口文件等信息。这样一来,electron-builder 在打包时会以该文件夹作为打包的根文件夹,即只会打包这个文件夹下的文件。


但是我原项目的主进程和预加载脚本也放在这,尝试修改后出现打包错误的情况,感兴趣的可以试试


image.png


这是我们优化之后的结果,1.32 MB,后面只需要替换这个文件就实现了版本的更新。


校验增量更新


全量更新的校验逻辑是第三方库已经封装好了的,那么对于增量更新,就得由我们自己来实现了。


首先明确一下 校验的时机package.jsonversion 字段表示应用程序的版本,用于全量更新的判断,那也就意味着,当这个字段保持不变的时候,会触发上文提到的 update-not-available 事件。所以我们可以在这个事件的回调函数里来进行校验。


autoUpdater.on('update-not-available', info => { console.log('不需要全量更新', info.version) })

然后就是 如何校验,我们回过头来看 electron-builder 的打包配置,在 releaseInfo 字段里描述了发行版本的一些信息,目前我们用到了 releaseNotes 来存储更新日志,查阅官网得知还有个 releaseName 好像对于我们而言是没什么用的,那么我们就可以把它作为增量更新的 热版本号。(但其实 官网 配置项还有个 vendor 字段,可是我在用的时候打包会报错,可能是版本的问题,感兴趣的可以试试。)


每次发布新版本的时候,只要不是 Electron自身版本变化 等重大更新,我们都可以通过修改 releaseInforeleaseName 来发布我们的热版本号。现在我们已经有了线上的版本,那 本地热版本号 该如何获取呢。这里我们在每次热更新完成之后,就会把最新的版本号存储在本地的一个配置文件中,通常是 userData 文件夹,用于存储我们应用程序的用户信息,即使程序卸载 了也不会影响里面的资源。在 Windows 下,路径如下:
C:\Users\用户名\AppData\Roaming\electron-update-demo


因此,整个 校验流程 就是,在打开程序的时候,autoUpdater 触发 update-not-available 事件,拿到线上 latest.yml 描述的 releaseName 作为热版本号,与本地配置文件(我们命名为 config.json)里存储的热版本号(我们命名为 hotVersion)进行对比,若不同就去下载最新的 app.asar 文件。


我们使用 electron-log 来记录日志,代码如下所示。


// 本地配置文件
const localConfigFile = path.join(app.getPath('userData'), 'config.json')

const checkHotVersion = (latestHotVersion: string): boolean => {
let needDownload = false
if (!fs.existsSync(localConfigFile)) {
fs.writeFileSync(localConfigFile, '{}', 'utf8')
log.info('配置文件不存在,已自动创建')
} else {
log.info('监测到配置文件')
}

try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
// 对比版本号
if (latestHotVersion !== configData.hotVersion) {
log.info('当前版本不是最新热更版本')
needDownload = true
} else {
log.info('当前为最新版本')
}
} catch (error) {
log.error('读取配置文件时出错', error)
}

return needDownload
}

下载增量更新包


通过刚才的校验,我们已经知道了什么时候该去下载增量更新包。


在开发调试的时候,我们可以把新版本的 app.asar 也放到起了本地服务器的 mockServer 文件夹里来模拟真实的情况。有很多方式可以去下载文件,这里我用了 nodejshttp 模块去实现,如果是 https 的需要引用 https 模块。


下载到本地的时候,我是放在了与 app.asar 同级目录的 resources 文件夹,我们不能直接覆盖原文件。一是因为进程权限占用的问题,二是为了容错,所以我们需要以另一个名字来命名下载后的文件(这里我们用的是 app.asar-temp),也就不需要去备份原文件了,代码如下。


const resourcePath = isDev
? path.join('D:\\测试\\electron-update-demo\\resources')
: process.resourcesPath

const localAsarTemp = path.join(resourcePath, 'app.asar-temp')

const asarUrl = 'http://localhost:3000/app.asar'

downloadAsar() {
const fileStream = fs.createWriteStream(localAsarTemp)
http.get(asarUrl, res => {
res.pipe(fileStream)
fileStream
.on('finish', () => {
log.info('asar下载完成')
})
.on('error', error => {
// 删除文件
fs.unlink(localAsarTemp, () => {
log.error('下载出现异常,已中断', error)
})
})
})
}

因此,我们的流程更新为:发现新版本后,下载最新的 app.asarresources 目录,并重命名为 app.asar-temp。这个时候我们启动项目进行调试,找到记录在本地的日志—— C:\Users\用户名\AppData\Roaming\electron-update-demo\logs,会有以下的记录:


[2024-09-20 13:49:22.456] [info]  监测到配置文件
[2024-09-20 13:49:22.462] [info] 当前版本不是最新热更版本
[2024-09-20 13:49:23.206] [info] asar下载完成

在看看项目 resources 文件夹,多了一个 app.asar-temp文件。


image.png


至此,下载这一步已经完成了,但这样还会有一个问题,因为文件名没有加上版本号,也没有重复性校验,倘若一直没有更新,那么每次启动应用需要增量更新的时候都需要再次下载。


替换 app.asar 文件


好了,新文件也有了,接下来就是直接替换掉老文件就可以了,但是 替换的时机 很重要。


Windows 操作系统下,直接替换 app.asar 会提示程序被占用而导致失败,所以我们应该在程序关闭的时候实现替换。



  1. 进入应用,下载新版本,自动关闭应用,替换资源,重启应用。

  2. 进入应用,下载新版本,不影响使用,用户 手动关闭 后替换,下次启动 就是新版本了。


我们有上面两种方案,最终采用了 方案2


在主进程监听 app.on('quit') 事件,在应用退出的时候,判断 app.asarapp.asar-temp 是否同时存在,若同时存在,就去替换。这里替换不能直接用 nodejs 在主进程里去写,因为主进程执行的时候,资源还是被占用的。因此我们需要额外起一个 子进程 去做。


nodejs 可以通过 spawnexec 等方法创建一个子进程。子进程可以执行一些自定义的命令,我们并没有使用子进程去执行 nodejs,因为业务方的机器上不一定有这个环境,而是采用了启动 exe 可执行文件的方式。可能有人问为什么不直接运行 .bat 批处理文件,因为我调试关闭应用的时候,会有一个 命令框闪烁 的现象,这不太优雅,尽管已经设置 spawnwindowsHide: true


那么如何获得这个 exe 可执行文件呢,其实是通过 bat 文件去编译的,命令如下:


@echo off
timeout /T 1 /NOBREAK
del /f /q /a %1\app.asar
ren %1\app.asar-temp app.asar

我们加了延时,等待父进程完全关闭后,再去执行后续命令。其中 %1 为运行脚本传入的参数,在我们的场景里就是 resources 文件夹的地址,因为每个人的安装地址应该是不一样的,所以需要作为参数传进去。


转换文件的工具一开始用的是 Bat To Exe Converter 下载地址,但是在实际使用的过程中,我的电脑竟然把转换后的 exe 文件 识别为病毒,但在其他同事电脑上没有这种表现,这肯定就不行了,最后还是同事使用 python 帮我转换生成了一份可用的文件(replace.exe)。


这里我们可以选择不同的方式把 replace.exe 存放到 用户目录 上,要么是从线上服务器下载,要么是安装的时候就已经存放在本地了。我们选择了后者,这需要修改 electron-builder 打包配置,指定 asarUnpack, 这样就会存放在 app.asar.unpacked 文件夹中,不经常修改的文件都可以放在这里,不会随着增量更新而替换掉。


有了这个替换脚本之后,开始编写子进程相关的代码。


import { spawn } from 'child_process'

cosnt localExePath = path.join(resourcePath, 'app.asar.unpacked/resources/replace.exe')

replaceAsar() {
if (fs.existsSync(localAsar) && fs.existsSync(localAsarTemp)) {
const command = `${localExePath} ${resourcePath}`
log.info(command)
const logPath = app.getPath('logs')
const childOut = fs.openSync(path.join(logPath, './out.log'), 'a')
const childErr = fs.openSync(path.join(logPath, './err.log'), 'a')
const child = spawn(`"${localExePath}"`, [`"${resourcePath}"`], {
detached: true, // 允许子进程独立
shell: true,
stdio: ['ignore', childOut, childErr]
})
child.on('spawn', () => log.info('子进程触发'))
child.on('error', err => log.error('child error', err))
child.on('close', code => log.error('child close', code))
child.on('exit', code => log.error('child exit', code))
child.stdout?.on('data', data => log.info('stdout', data))
child.stderr?.on('data', data => log.info('stderr', data))
child.unref()
}
}

app.on('quit', () => {
replaceAsar()
})

在这块代码中,创建子进程的配置项比较重要,尤其是路径相关的。因为用户安装路径是不同的,可能会存在 文件夹名含有空格 的情况,比如 Program Files,这会导致在执行的时候将空格识别为分隔符,导致命令执行失败,因此需要加上 双引号。此外,detached: true 可以让子进程独立出来,也就是父进程退出后可以执行,而shell: true 可以将路径名作为参数传过去。


const child = spawn(`"${localExePath}"`, [`"${resourcePath}"`], {
detached: true,
shell: true,
stdio: ['ignore', childOut, childErr]
})

但这块有个 疑惑,为什么我的 closeexit 以及 stdout 都没有触发,以至于不能拿到命令是否执行成功的最终结果,了解的同学可以评论区交流一下。


至此,在关闭应用之后,app.asar 就已经被替换为最新版本了,还差最后一步,更新本地配置文件里的 hotVersion,防止下次又去下载更新包了。


child.on('spawn', () => {
log.info('子进程触发')
updateHotVersion()
})

updateHotVersion() {
fs.writeFileSync(localConfigFile, JSON.stringify({ hotVersion }, null, 2))
}

增量更新日志提示


既然之前提到的全量更新有日志说明,那增量更新可以不用,也应该需要具备这个能力,不然我们神不知鬼不觉帮用户更新了,用户都不知道 新在哪里


至于更新内容,我们可以复用 releaseInforeleaseNotes 字段,把更新日志写在这里,增量更新完成后展现给用户就好了。


但是总不能每次打开都展示,这里需要用户有个交互,比如点击 知道了 按钮,或者关闭 Modal 后,把当前的热更新版本号保存在本地配置文件,记录为 logVersion。在下次打开程序或者校验更新的时候,我们判断一下 logVersion === hotVersion若不同,再去提示更新日志。


日志版本 校验和修改的代码如下所示:


checkLogVersion() {
let needShowLog = false
try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
const { hotVersion, logVersion } = configData
if (hotVersion !== logVersion) {
log.info('日志版本与当前热更新版本不同,需要提示更新日志')
needShowLog = true
} else {
log.info('日志已是最新版本,无需提示更新日志')
}
} catch (error) {
log.error('读取配置文件失败', error)
}
return needShowLog
}

updateLogVersion() {
try {
const configStr = fs.readFileSync(localConfigFile, 'utf8')
const configData = JSON.parse(configStr || '{}')
const { hotVersion } = configData
fs.writeFileSync(localConfigFile, JSON.stringify({ hotVersion, logVersion: hotVersion }, null, 2))
log.info('日志版本已更新')
} catch (error) {
log.error('读取配置文件失败', error)
}
}

读取 config.json 文件的方法自行封装一下,我这里就重复使用了。主进程 ipcMain.on 监听一下用户传递过来的事件,再去调用 updateLogVersion 即可,渲染进程效果如下:


提示增量更新日志


image.png


点击 知道了 后,再次打开应用后就不会提示日志了,因为本地配置文件已经被修改了。


image.png


当然可能也有这么个场景,开发就改了一点点无关功能的代码,无需向用户展示日志,我们只需要加一个判断 releaseNotes 是否为空的逻辑就好了,也做到了 静默更新


小结


不足之处


本文提出的增量更新方案应该算是比较简单的,可能并不适用于所有的场景,考虑也不够全面,例如:



  1. dll第三方SDK 等资源的更新。

  2. 增量更新失败后应该通过全量更新 兜底

  3. 用户在使用过程中发布新版本,得等到 第二次打开 才能用到新版本。


流程图


针对本文的解决方案,我简单画了一个 流程图


未命名绘图.png


参考文章


网上其实有不少关于 Electron 自动更新的文章,在做这个需求之前也是浏览了好多,但也有些没仔细看完,所以就躺在我的标签页没有关闭,在这罗列出来,也当作收藏了。


写这篇文章的目的也是为了记录和复盘,如果能为你提供一些思路那就再好不过了。


鸣谢:



作者:小陈同学吗
来源:juejin.cn/post/7416311252580352034
收起阅读 »

国产语言MoonBit崛起,比Rust快9倍,比GO快35倍

在这个技术日新月异的时代,编程语言的更新换代似乎已经成为了家常便饭。但国产语言却一直不温不火,对于程序员来说,拥有一款属于我们自己的语言,一直是大家梦寐以求的事情。 如果我说有一种国产编程语言,它的运行速度比Rust快9倍,比GO快35倍,你会相信吗? 这不是...
继续阅读 »

在这个技术日新月异的时代,编程语言的更新换代似乎已经成为了家常便饭。但国产语言却一直不温不火,对于程序员来说,拥有一款属于我们自己的语言,一直是大家梦寐以求的事情。


如果我说有一种国产编程语言,它的运行速度比Rust快9倍,比GO快35倍,你会相信吗?


这不是天方夜谭,最近,被称为“国产编程语引领者”的MoonBit(月兔),宣布正式进入Beta预览版本阶段啦!


一听月兔这名字起得挺中式的。


一、初识MoonBit



MoonBit是由粤港澳大湾区数字经济研究院(福田)研发的全新编程语言。



① 官网


http://www.moonbitlang.cn/


官网


② 目前状态


MoonBit是2022年推出的国产编程语言,并在2023年8月18日海外发布后,立即获得国际技术社区的广泛关注。


经过一年多的高速迭代,MoonBit推出了beta预览版。


MoonBit 目前处于 Beta-Preview 阶段。官方希望能在 2024/11/22 达到 Beta 阶段,2025年内达到 1.0 阶段。


③ 由来


诞生于AI浪潮,没有历史包袱:MoonBit 诞生于 ChatGPT 出世之后,使得 MoonBit 团队有更好的机会去重新构想整个程序语言工具链该如何与 AI 友好的协作,不用承担太多的历史包袱


二、MoonBit 语言优势


编译与运行速度快


MoonBit在编译速度和运行时性能上表现出色,其编译626个包仅需1.06秒,比Rust快了近9倍;运行速度比GO快35倍!


编译速度比较


代码体积小


MoonBit 在输出 Wasm 代码体积上相较于传统语言有显著优势。


一个简单的HTTP 服务器时,MoonBit 的输出文件大小仅为 27KB,而 WasmCloud提供的http-hello-world 模板中 Rust 的输出为 100KBTypeScript8.7MBPython 更是高达 17MB


代码体积比较


多重安全保障


MoonBit 采用了强大的类型系统,并内置静态检测工具,在编译期检查类型错误,


MoonBit自身的静态控制流分析能在编译器捕获异常的类型,从而提高代码的正确性和可靠性。


高效迭代器


MoonBit创新地使用了零开销的迭代器设计,使得用户能够写出既优雅又高效的代码。


创新的泛型系统设计


MoonBit语言在它的测试版里就已经搞定了泛型和特殊的多态性,而且在编译速度特别快的同时,还能做到用泛型时不增加额外负担。


你要知道,这种本事在很多流行的编程语言里,都是正式发布很久之后才慢慢有的,但MoonBit一开始就做到了。这种设计在现在编程语言越来越复杂的大背景下特别关键,因为一个好的类型系统对于整个编程语言生态的健康成长是特别重要的。


三、应用场景


① 云计算


② 边缘计算


③ AI 以及教学领域的发展


四、开发样例


我们在官网 http://www.moonbitlang.cn/gallery/ 可以看到用使用MoonBit 开发的游戏样例



  • 罗斯方块游戏

  • 马里奥游戏

  • 数独求解器

  • 贪吃蛇游戏


游戏开发样例


五、语言学习


5.1 语法文档



如果你也对MoonBit感兴趣,想学习它,访问官方文档docs.moonbitlang.cn/。文档算是比较详细的了



image-20240921212615386


5.2 在线编译器



无需本地安装编译器即可使用,官方提供了在线编译器



① 在线编辑器地址


try.moonbitlang.cn/


在线编辑器


② 点击这儿运行代码


运行代码


5.3 VS Code 中安装插件编写代码、


① 安装插件


安装插件


搜索插件


② 下载程序


按下shift+cmd+p快捷键(mac快捷键,windows和linux快捷键是ctrl+shift+p),输入 MoonBit:install latest moonbit toolchain,随后会出现提示框,点击“yes”,等待程序下载完成。


下载程序


③ 创建并打开新项目


下载完成后,点击terminal,输入moon new hello && code hello以创建并打开新项目。


④ 始执行代码


项目启动后,再次打开terminal,输入moon run main命令,即可开始执行代码。


六、小结


下面是晓凡的一些个人看法


MoonBit 作为一款新兴的国产编程语言,其在性能和安全性方面的表现令人印象深刻。


特别是它在编译速度和运行效率上的优化,对于需要处理大量数据和高并发请求的现代应用来说,是一个很大的优势。


同时,它的设计理念符合当前软件开发的趋势,比如对云计算和边缘计算的支持,以及对 AI 应用的适配。


此外,MoonBit 团队在语言设计上的前瞻性思考,比如泛型系统的实现,显示出了其对未来编程语言发展趋势的深刻理解。


而且,提供的游戏开发样例不仅展示了 MoonBit 的实用性,也降低了初学者的学习门槛。


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

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

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

大家好,我是晓凡。


写在前面


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


来自朋友的抱怨


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



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


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


一、功能需求


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


具体数据整合如下图所示


数据整合


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


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


二、技术选型


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


综合考虑之后选择了



  • PowerBuilder

  • Pbidea.dll


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


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


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


三、简单界面布局


界面布局1


界面布局2


界面布局3


四、核心代码


① 导入excel



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

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


② 数据整合


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

string ls_err

//重置表三数据

dw_3.reset()

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

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

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

next

//处理表二数据

ll_sum2 = dw_2.rowcount( )

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

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

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

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

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

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

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

end if

next

return 0

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

③ excel导出


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

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

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

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

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

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

return 0

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

五、最终效果


财务辅助系统


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


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


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

37K star!实时后端服务,一个文件实现

如果你经常开发web类的的项目,那你一定经常会接触到后端服务,给项目找一个简单、好用的后端服务可以大大加速开发。 今天我们分享的开源项目,它可以作为SaaS或者是Mobile的后端服务,最简单情况只要一个文件,它就是:PocketBase PocketBas...
继续阅读 »


如果你经常开发web类的的项目,那你一定经常会接触到后端服务,给项目找一个简单、好用的后端服务可以大大加速开发。


今天我们分享的开源项目,它可以作为SaaS或者是Mobile的后端服务,最简单情况只要一个文件,它就是:PocketBase



PocketBase 是什么


PocketBase是一个开源的Go后端框架,它以单个文件的形式提供了一个实时的后端服务。这个框架特别适合于快速开发小型到中型的Web应用和移动应用。它的设计哲学是简单性和易用性,使得开发者能够专注于他们的产品而不是后端的复杂性。



PocketBase包含以下功能:



  • 内置数据库(SQLite)支持实时订阅

  • 内置文件和用户管理

  • 方便的管理面板 UI

  • 简洁的 REST 风格 API


安装使用PocketBase


首先你可以下载PocketBase的预构建版本,你可以在github的release页面下载到对应平台的包。



下载后,解压存档并./pocketbase serve在解压的目录中运行。


启动完成后会3个web服务的路由:



默认情况下,PocketBase 在端口上运行8090。但您可以通过在 serve 命令后附加--http和--https参数将其绑定到任何端口。


Admin panel


第一次访问管理仪表板 UI 时,它会提示您创建第一个管理员帐户。在管理页面里您可以完全使用 GUI 构建数据架构、添加记录并管理数据库。



API


它带有一个开箱即用的 API,可让您操作任何集合,还具有一个优雅的查询系统,可让您分页搜索记录。这将使您不必自己编写和维护同样无聊的 CRUD 操作,而可以更专注于产品特定的功能。


内置访问规则


PocketBase 可让您通过简单的语法直接从 GUI 定义对资源的访问规则。例如,这有助于定义访问范围和控制对用户特定数据的访问。同样,这将使您无需担心编写身份验证和授权代码。



SDK


使用PocketBase的API可以通过官方SDK,目前官方提供了JS SDK和Dart SDK。



  • JavaScript - pocketbase/js-sdk (浏览器和nodejs)

  • Dart - pocketbase/dart-sdk(网页、移动、桌面)


它们提供了用于连接数据库、处理身份验证、查询、实时订阅等的库,使开发变得简单。


开发定制应用


PocketBase 作为常规 Go 库包分发,允许您构建自己的自定义应用程序特定的业务逻辑,并且最后仍具有单个可移植的可执行文件。


这是一个简单的例子:



  1. 首先如果你没有Go的环境,那么需要安装 Go1.21以上版本

  2. 创建一个新的项目目录,并创建一个main.go文件,文件包含以下内容:


package main

import (
"log"
"net/http"

"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
)

func main() {
app := pocketbase.New()

app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
// add new "GET /hello" route to the app router (echo)
e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/hello",
Handler: func(c echo.Context) error {
return c.String(200, "Hello world!")
},
Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(app),
},
})

return nil
})

if err := app.Start(); err != nil {
log.Fatal(err)
}
}


  1. 初始化依赖项,请运行go mod init myapp && go mod tidy。

  2. 要启动应用程序,请运行go run main.go serve。

  3. 要构建静态链接的可执行文件,您可以运行CGO_ENABLED=0 go build,然后使用 启动创建的可执行文件./myapp serve。


总结


整体来说PocketBase是一个非常不错的后端服务,它兼顾了易用性和定制的灵活性,如果你有项目的需要或是想要自己开发一个SAAS的服务,都可以选择它来试试。



项目信息



作者:IT咖啡馆
来源:juejin.cn/post/7415672130190704640
收起阅读 »

Vue3真的不需要用pinia!!!

web
前言 之前使用vue3都是在公司的基建项目中,为了快速达到目的,把以前vue2的模板拿来简单改改就直接用了,所以项目中用法特别乱,比如:状态管理依旧用的vuex,各种类型定义全是any,有些代码是选项式API,有些代码是组合式API... 最近终于有时间推动一...
继续阅读 »

前言


之前使用vue3都是在公司的基建项目中,为了快速达到目的,把以前vue2的模板拿来简单改改就直接用了,所以项目中用法特别乱,比如:状态管理依旧用的vuex,各种类型定义全是any,有些代码是选项式API,有些代码是组合式API...


最近终于有时间推动一下业务项目使用vue3了。作为极简主义的我,始终奉行少即是多,既然是新场景,一切从新,从头开始写模版:



  • 使用最新的vue3版本v3.5.x

  • 所有使用的内部库全部生成ts类型并引入到环境中。

  • 将所有的mixins重写,包装成组合式函数。

  • 将以前的vue上的全局变量挂载到app.config.globalProperties

  • 全局变量申明类型到vue-runtime-core.d.ts中,方便使用。

  • 全部使用setup语法,使用标签<script setup lang="ts">

  • 使用pinia作为状态管理。


pinia使用


等等,pinia?好用吗?打开官方文档研究了下,官方优先推荐的是选项式API的写法。


调用defineStore方法,添加属性state, getters, actions等。


export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment () {
this.count++
},
},
})

使用的时候,调用useCounterStore即可。


import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'

const store = useCounterStore()
setTimeout(() => {
store.increment()
}, 1000)
const doubleValue = computed(() => store.doubleCount)


看上去还不错,但是我模版中全部用的是组合式写法,肯定要用组合式API,试着写了个demoref就是选项式写法中的statecomputed就是选项式中的gettersfunction就是actions


// useTime.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import vueConfig from '../../../common/config/vueConfig'
import * as dayjs from 'dayjs'

export default defineStore('time', () => {
const $this = vueConfig()
const time = ref<number>()
const timeFormat = computed(() => dayjs(time.value).format('YYYY-MM-DD HH:mm:ss'))
const getSystemTime = async () => {
const res = await $this?.$request.post('/system/time')
time.value = Number(res.timestamp)
}
return { timeFormat, getSystemTime }
})

调用时解构赋值,就可以直接用了。


// index.vue
<script setup lang="ts">
import { onMounted } from 'vue'
import useTime from './use/useTime'

const { timeFormat, getSystemTime } = useTime()
onMounted(async () => {
// 请求
await getSystemTime()
console.log('当前时间:', timeFormat)
})
</script>

优雅了很多,之前用vuex时还有个问题,storeA中的state、actions等,会在storeB中使用,这一点pinia文档也有说明,直接在storeB调用就好了,比如我想在另一个组件中调用上文中提到的timeFormat


defineStore('count', () => {
const count = ref<number>(0)
const { timeFormat } = useTime()
return {
count,
timeFormat,
}
})

怎么看着这么眼熟呢,这不就是组合式函数吗?为什么我要用defineStore再包一层呢?试一试不用pinia,看能不能完成状态管理。


组合式函数


直接添加一个useCount.ts文件,申明一个组合式函数。


// useCount.ts
import { computed, ref } from 'vue'

const useCount = () => {
const count = ref<number>(0)
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
count,
doubleCount,
setCount,
}
}
export default useCount

使用时直接解构申明,并使用。


import useCount from './use/useCount'

const { count, setCount } = useCount()
onMounted(async () => {
console.log('count', count.value) // 0
setCount(10)
console.log('count', count.value) // 10

})

最大的问题来了,如何在多个地方共用count的值呢,这也是store最大的好处,了解javascript函数机制的我们知道useCount本身是一个闭包,每次调用,里面的ref就会重新生成。count就会重置。


import useCount from './use/useCount'

const { count, setCount } = useCount()
const { doubleCount } = useCount()

onMounted(async () => {
console.log('count', count.value, doubleCount.value) // 0 0
setCount(10)
console.log('count', count.value, doubleCount.value) // 10 0

})

这个时候doubleCount用的并不是第一个useCount中的count,而是第二个重新生成的,所以setCount并不会引起doubleCount的变化。


怎么办呢?简单,我们只需要把count的声明暴露在全局环境中,这样在import时就会申明了,调用函数时不会被重置。


import { computed, ref } from 'vue'

const count = ref<number>(0)
const useCount = () => {
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
count,
doubleCount,
setCount,
}
}
export default useCount

当我们多次调用时,发现可以共享了。


import useCount from './use/useCount'

const { count, setCount } = useCount()
const { doubleCount } = useCount()

onMounted(async () => {
console.log('count', count.value, doubleCount.value) // 0 0
setCount(10)
console.log('count', count.value, doubleCount.value) // 10 20

})

但是这个时候count是比较危险的,store应该可以保护state不被外部所修改,很简单,我们只需要用readonly包裹一下返回的值即可。


import { computed, readonly, ref } from 'vue'

const count = ref<number>(0)
const useCount = () => {
const doubleCount = computed(() => {
return count.value * 2
})
const setCount = (v: number) => {
count.value = v
}
return {
// readonly可以确保引用对象不会被修改
count: readonly(count),
doubleCount,
setCount,
}
}
export default useCount


总结


经过我的努力,vue3又减少了一个库的使用,我就说不需要用pinia,不过放弃pinia也就意味着放弃了它自带的一些方法store.$statestore.$patch等等,这些方法实现很简单,很轻松就可以手写出来,如果你是这些方法的重度用户,保留pinia也没问题,如果你也想代码更加精简,赶紧尝试下组合式函数吧。


作者:自在的小李子
来源:juejin.cn/post/7411328136740847654
收起阅读 »

上6休3上3休2……,为了理清这烧脑的调休安排我制作一个调休日历!

调休日历 前些天,使用python做了一个日历,可以打印出调休的节假日。效果还可以,但是遇到了一些小问题。 有一个朋友提出了这样一个问题:“日历确实是很好,但是,使用控制台print不怎么样,别人的日历都是很好看的,而我们的日历还只能用print。这太丢人了,...
继续阅读 »

调休日历


前些天,使用python做了一个日历,可以打印出调休的节假日。效果还可以,但是遇到了一些小问题。


有一个朋友提出了这样一个问题:“日历确实是很好,但是,使用控制台print不怎么样,别人的日历都是很好看的,而我们的日历还只能用print。这太丢人了,我出门说自己是你的粉丝,都没什么面子了啊!”


还有一个朋友提出了另外一个问题:“在我们国家,‘农历’也是很重要的,比如说‘农历新年’是一年中最重要的日子。所以,我们的日历也应该能看到农历才比较好。”


好吧,这次就来解决一下这些问题!


农历


农历介绍


农历,也称为“阴历”,“月亮历”,“阴阳历”,是一种重要的历法,起源于中国的古代,这种历法同时结合了太阳运动,和月亮周期。其中月球围绕地球旋转的周期,大约为29.5天,这构成了农历的一个月,而一年通常是12个月。然而,一个太阳年(太阳公转一周)大概是365.24天,显然月亮的12个月不够这个数字,因此,为了填补到太阳年的时长,就会加入“闰月“,大概每3年就会出现一个”闰月“,”闰月“和公历中的“闰年”有异曲同工之妙。


那么农历有什么用呢?农历有很大的作用!节日安排,就是农历决定的(新年,中秋,端午等)。农业活动也和农历有很大的关系,比如说,仍然有很多农民,根据农历的指导,进行农业活动,什么时候播种,什么时候收割,都是农历决定的。还有很多人过生日,都会选择过农历生日。除此之外,像“季节变化”也和农历有微妙的关系,我们经常可以听到,“现在已经立秋了,已经是秋天了,马上就要凉快了。”,诸如此类的话,可见,农历在日常生活中发挥了重要作用。是我们祖先的伟大发明。


农历实现


那么,农历到底是怎么确定日子的呢?这和天文观测有很大的关系。我们要通过观察天象,来确定农历的准确性。比如说,在中国古代,就有专门的机构,如皇家天文台负责观测天文,然后调整历法,这是一个重要的活动。在历史上,历法也经过了多次修订。


到了现代,对于天文的观测,不再是必须的了。因为现代的科技较为发达,已经能够通过数学计算,历史天文数据推导等,精确的计算出农历。因此,即使我们没有“夜观星象”,也可以知道未来上百年的农历运作。


但是,我们既不会观察天象,也不会科学计算月亮运动,怎么办呢?当然没关系啦,因为,别人已经算好了,我们直接引用就可以了!


安装:pip install lunarcalendar


from lunarcalendar import Converter, Solar, Lunar


# 将公历日期转换为农历
solar = Solar(2024, 9, 17)
lunar = Converter.Solar2Lunar(solar)


# 输出结果为农历日期
print(lunar)


# 将农历日期转换为公历
lunar = Lunar(2024, 8, 15)
solar = Converter.Lunar2Solar(lunar)


# 输出结果为公历日期
print(solar)

转换为汉字日期


一般在农历中,我们并不使用阿拉伯数字的记录,而是常说,“正月初三”,“八月廿二”,这样的表达方式。因此,我们还需要将数字的农历,转为常见的汉字日期:


from lunarcalendar import Converter, Solar




def lunar_date_str(year, month, day):
lunar = Converter.Solar2Lunar(Solar(year, month, day))
leap_month = lunar.isleap
months = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"]
days = ["初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"]
month_str = months[lunar.month - 1]
if leap_month:
month_str = "闰" + month_str
day_str = days[lunar.day - 1]

# 该实现是特别为了符合日历的实现,仅在每个月的第一天返回月份,例如正月初一,返回“正月”
# 而其他日子,不返回月份,仅返回日期,例如正月初三,返回“初三”
if lunar.day == 1:
return month_str
else:
return day_str

如果要更广泛意义的月份加日期,只需要简单的修改返回值即可:f"{month_str}{day_str}"


日历实现


假期判断


因为“调休”的存在,所以我们的放假日期不总是固定的,每年都会有很大的变化,那么如何判断某个日子是不是节假日呢?是不是需要调休呢?


实际上,这是一件困难的事情,没有办法提前知道,只能等到每一年,国家公布了放假安排以后,我们才能够知道准确的放假调休日期。比如说,一些日历等不及,还没等公布放假安排,就已经开始提前印刷了,那么这样的日历,其上包含的信息,就是不完整的,他只能告诉你常规的节日和星期,没办法告诉你调休。


看过我上一期关于日历文章的,应该知道在当时,我是使用了“标记调休”的方式,实现这一点的,大概像这样:


rili2.png


这当然是简单有效,且可行的,只不过一次标记只能管一年,到了明年就不能用了,还得自己重新标记,况且,标记也是一件麻烦的事情,有没有什么更好的办法呢?


当然是有的,我们让别人给我们标记好了,自己等着用现成的,不就好了吗?那么哪里能找到这样的好心人呢?当然是有的,python有一个库叫做chinese-calendar,其中维护了每年的中国节假日,我们只需要使用这个库,让他告诉我们,今天休不休息,就好了。


安装:pip install chinese_calendar


import chinese_calendar as cc
from datetime import date


# 检查某一天是否是工作日或节假日
on_holiday, holiday_name = cc.get_holiday_detail(date(2024, 10, 1))


# 输出是否为假日,假日名称
print(on_holiday, holiday_name)

唉,世界上还是好人多啊!用上了这个,我们可以省很多事,真是好东西啊。


matplotlib绘制日历


matplotlib非常常用,今天就不主要介绍了,虽然它不常用于绘制日历,但是,它的功能其实是很广泛的,包括我们今天的绘制日历。


在下面的实现中,允许提供一个额外信息表,来覆盖默认的农历。也就是你可以通过extra_info_days来增加节日,纪念日的提示。


import datetime
import chinese_calendar as cc
import matplotlib.pyplot as plt


from calendar import monthrange
from lunarcalendar import Converter, Solar




class CalendarDrawer:
plt.rcParams['font.family'] = 'SimHei' # 设置显示字体为黑体
months = ["正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"]
days = ["初一", "初二", "初三", "初四", "初五", "初六", "初七", "初八", "初九", "初十",
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
"廿一", "廿二", "廿三", "廿四", "廿五", "廿六", "廿七", "廿八", "廿九", "三十"]


def __init__(self, year, month, extra_info_days=):
self.year = year
self.month = month
self.extra_info_days = extra_info_days or {}
self.fig, self.ax = plt.subplots()


def ax_init(self):
self.ax.axis([0, 7, 0, 7])
self.ax.axis("on")
self.ax.grid(True)
self.ax.set_xticks([])
self.ax.set_yticks([])


@staticmethod
def lunar_date_str(year, month, day):
lunar = Converter.Solar2Lunar(Solar(year, month, day))
month_str = "闰" + CalendarDrawer.months[lunar.month - 1] if lunar.isleap else CalendarDrawer.months[lunar.month - 1]
return month_str if lunar.day == 1 else CalendarDrawer.days[lunar.day - 1]


def plot_month(self):
self.ax.text(3.5, 7.5, f"{self.year}{self.month}月", color="black", ha="center", va="center")


def plot_weekday_headers(self):
for i, weekday in enumerate(["周一", "周二", "周三", "周四", "周五", "周六", "周日"]):
x = i + 0.5
self.ax.text(x, 6.5, weekday, ha="center", va="center", color="black")


def plot_day(self, day, x, y, color):
ex_day = datetime.date(self.year, self.month, day)
day_info = f"{day}\n{self.extra_info_days.get(ex_day, self.lunar_date_str(self.year, self.month, day))}"
self.ax.text(x, y, day_info, ha="center", va="center", color=color)


def check_color_day(self, day):
date = datetime.date(self.year, self.month, day)
return "red" if cc.get_holiday_detail(date)[0] else "black"


def save(self):
self.ax_init()
self.plot_month()
self.plot_weekday_headers()


weekday, num_days = monthrange(self.year, self.month)
y = 5.5
x = weekday + 0.5


for day in range(1, num_days + 1):
color = self.check_color_day(day)
self.plot_day(day, x, y, color)
weekday = (weekday + 1) % 7
if weekday == 0:
y -= 1
x = weekday + 0.5


plt.savefig(f"日历{self.year}-{self.month}.png")




if __name__ == "__main__":
extra_info_days = {
datetime.date(2024, 1, 1): "元旦",
datetime.date(2024, 2, 10): "春节",
datetime.date(2024, 2, 14): "情人节",
datetime.date(2024, 2, 24): "元宵节",
datetime.date(2024, 3, 8): "妇女节",
datetime.date(2024, 4, 4): "清明节",
datetime.date(2024, 5, 1): "劳动节",
datetime.date(2024, 5, 12): "母亲节",
datetime.date(2024, 5, 20): "520",
datetime.date(2024, 6, 1): "儿童节",
datetime.date(2024, 6, 10): "端午节",
datetime.date(2024, 6, 16): "父亲节",
datetime.date(2024, 8, 10): "七夕节",
datetime.date(2024, 9, 17): "中秋节",
datetime.date(2024, 10, 1): "国庆节",
datetime.date(2024, 11, 1): "万圣节",
datetime.date(2024, 11, 11): "双十一",
datetime.date(2024, 12, 12): "双十二",
datetime.date(2024, 12, 24): "平安夜",
datetime.date(2024, 12, 25): "圣诞节"
}


calendar_drawer = CalendarDrawer(2024, 12, extra_info_days) # 第一个参数为年份,第二个参数为月份,第三个参数为额外信息字典
calendar_drawer.save()

绘制结果,2024-09:
日历2024-9.png


2024-10:
日历2024-10.png


2024-11:
日历2024-11.png


2024-12:
日历2024-12.png


引用与致谢


日历样式,部分参考了便民查询:wannianrili.bmcx.com/


matplotlib绘制,部分参考了shimo164:medium.com/@shimo164/


详细的中国节假日,不再需要个人手动标记了,chinese-calendar:github.com/LKI/chinese…


快速将公历转为农历,支持转换到2100年,LunarCalendar:github.com/wolfhong/Lu…


总结


从我们的日历中,可以清晰的看出,中秋到国庆,我们经历了:



  1. 上6休3

  2. 上3休2

  3. 上5休1

  4. 上2休7

  5. 上5休1


嗯,确实是太烧脑了,没有日历,很难算的清楚啊,最后,那么问题来了,3+7到底等于几呢?


作者:瞎老弟
来源:juejin.cn/post/7414013230954774579
收起阅读 »

还在用 top htop? 赶紧换 btop 吧,真香!

top 在 Linux 服务器上,或类 Unix 的机器上,一般我们想查看每个进程的 CPU 使用率、内存使用情况以及其他相关信息时会使用 top 命令。 top 是一个标准的 Linux/Unix 工具,实际上我从一开始接触 Linux 就一直使用 top...
继续阅读 »

top


在 Linux 服务器上,或类 Unix 的机器上,一般我们想查看每个进程的 CPU 使用率、内存使用情况以及其他相关信息时会使用 top 命令。



top 是一个标准的 Linux/Unix 工具,实际上我从一开始接触 Linux 就一直使用 top , 一般是两种场景:



  • Linux 服务器上用

  • 自己的 Mac 电脑上用


top 有一些常用的功能,比如可以动态的显示进程的情况,按照 CPU 、内存使用率排序等。说实话,这么多年了,使用最多的还就是 top ,一来是因为习惯了,工具用惯了很多操作都是肌肉记忆。二来是 top 一般系统自带不用安装,省事儿。


htop


top 挺好的,但 top 对于初学者和小白用户不太友好,尤其是它的用户界面和操作。于是后来有了 htop



htop 是 top 的一个增强替代品,提供了更加友好的用户界面和更多的功能。与 top 相比,htop 默认以颜色区分不同的信息,并且支持水平滚动查看更多的进程信息。htop 还允许用户使用方向键来选择进程,并可以直接发送信号给进程(如 SIGKILL)。htop 支持多种视图和配置选项,使得用户可以根据自己的喜好定制显示的内容。


htop 我也用了几年,确实舒服一些,但由于需要安装和我对 top 的肌肉记忆 ,htop 在我的使用中并未完全替代 top。 直到 btop 的出现


btop


现在,我本机使用的是 btop,有了 btop,top 和 htop 一点儿都不想用了,哈哈。


在服务器上有时候因为懒不想安装,一部分时间还是 top,一部分用 btop。



第一印象是真漂亮啊,然而它不止好看,功能也是很实用,操作还很简单,你说能不喜欢它吗?


说是 btop ,实际上人家真正的名字是 btop++ , 用 C++ 开发的



安装


btop 支持各种类 Unix 系统,你可以在它的文档中找到对应系统的安装方法 github.com/aristocrato…



本文演示,我是用我自己的 Mac 笔记本电脑,用 Mac 安装很简单,用 brew 一行搞定


brew install btop

我的系统情况是这样的:



安装完成后,直接运行 btop 就可以看到如上图的界面了。


功能界面


打开 btop 后不要被它的界面唬住了,其实非常的简单,我们来介绍一下。


打开 btop 后,其实显示的是它给你的 “预置” 界面。 默认有 4 个预置界面,你可以按 p 键进行切换。命令行界面上会分别显示:



  • preset 0

  • preset 1

  • preset 2

  • preset 3



你可能注意到了,这 4 个预置界面中有很多内容是重复的,没错,其实 btop 一共就 4 个模块,预置界面只是把不同的模块拼在一起显示罢了。这 4 个模块分别是:



  • CPU 模块

  • 存储 模块

  • 网络 模块

  • 进程 模块


这 4 个模块对应的快捷键分别就是 1234 你按一下模块显示,再按一下模块隐藏。



所以如果你对预置界面的内容想立刻调整,就可以按快捷键来显示/隐藏 你想要的模块,当然预置界面也是可以通过配置文件调整的,这个我们后面说。


CPU 模块


CPU 模块可以显示 CPU 型号、各内核的使用率、温度,CPU 整体的负载,以及一个直观的图象,所有数据都是实时显示的。



存储 模块


存储模块包括两部分,一个是内存使用情况,一个是磁盘使用情况:



因为比较直观,具体内容我就不解释了。


网络模块


网络模块可以看下网络的整体负载和吞吐情况,主要包括上行和下行数据汇总,你可以通过按快捷键 bn 来切换看不同的网卡。



进程模块


初始的进程模块可以看到:



  • pid

  • Program: 进程名称

  • Command: 执行命令的路径

  • Threads: 进程包含的线程数

  • User: 启动进程的用户

  • MemB: 进程所占用内存

  • Cpu%: 进程所占用 CPU 百分比



你可以按快捷键 e 显示树状视图:



可以按快捷键 r 对进行排序,按一下是倒序,再按一下是正序。具体排序列可以按左右箭头,根据界面显示进行选择,比如我要按照内存使用排序,那么右上角就是这样的:



f 键输入你想过滤的内容然后回车,可以过滤一下界面显示的内容,比如我只想看 chrome 的进程情况:


还可以通过 上下箭头选中某一个进程按回车查看进程详情,再次按回车可以隐藏详情:



显示进程详情后可以对进程进行操作,比如 Kill 只需要按快捷键 k 就可以了,然后会弹出提示:


主题


怎么样,是不是很方便,操作简单,上手容易,还好看。关于 btop 的主要操作就这些了,剩下的可以参考 helpmenu 中显示的内容自行操作和设置都很简单。


btop 的配置文件默认在这里:$HOME/.config/btop ,你可以直接修改配置文件中的详细参数,如我们前文提到的 “预置” 界面以及预置界面内容都可以在配置文件中设置 :



此外 btop 还有很多好看的主题配色,但默认安装的情况下只带了一个 Default 的,如果你想切换用其他的主题,需要先下载这些主题,主题文件在这里:github.com/aristocrato…


下载好以后放到本地对应的文件夹中 ~/.config/btop/themes


然后你就可以要界面上进行主题的切换了,具体流程是先按快捷键 m ,然后选 OPTIONS



接着在 Color theme 中就能看到你当前拥有的 theme 数据,按方向键就可以切换主题配色了:



主题有很多,我这里给大家一个完整的预览:



我目前使用的就是 Default 我觉得最符合我的审美。


最后


用了 btop 后你就再也回不去了,一般情况下再也不会想用 htop 和 top 了,大家没有换的可以直接换了


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

简单实现一个插件系统(不引入任何库),学会插件化思维

插件系统被广泛应用在各个系统中,比如浏览器、各个前端库如vue、webpack、babel,它们都可以让用户自行添加插件。插件系统的好处是允许开发人员在保证内部核心逻辑不变的情况下,以安全、可扩展的方式添加功能。 本文参考了webpack的插件,不引入任何库,...
继续阅读 »

插件系统被广泛应用在各个系统中,比如浏览器、各个前端库如vue、webpack、babel,它们都可以让用户自行添加插件。插件系统的好处是允许开发人员在保证内部核心逻辑不变的情况下,以安全、可扩展的方式添加功能。


本文参考了webpack的插件,不引入任何库,写一个简单的插件系统,帮助大家理解插件化思维。


下面我们先看看插件有哪些概念和设计插件的流程。


准备


三个概念



  • 核心系统(Core):有着系统的基本功能,这些功能不依赖于任何插件。

  • 核心和插件之间的联系(Core <--> plugin):即插件和核心系统之间的交互协议,比如插件注册方式、插件对核心系统提供的api的使用方式。

  • 插件(plugin):相互独立的模块,提供了单一的功能。



插件系统的设计和执行流程


那么对着上面三个概念,设计插件的流程:



  • 首先要有一个核心系统。

  • 然后确定核心系统的生命周期和暴露的 API。

  • 最后设计插件的结构。

    • 插件的注册 -- 安装加载插件到核心系统中。

    • 插件的实现 -- 利用核心系统的生命周期钩子和暴露的 API。




最后代码执行的流程是:



  • 注册插件 -- 绑定插件内的处理函数到生命周期

  • 调用插件 -- 触发钩子,执行对应的处理函数


直接看代码或许更容易理解⬇️


代码实现


准备一个核心系统


一个简单的 JavaScript 计算器,可以做加、减操作。


class Calculator {
constructor(options = {}) {
const { initialValue = 0 } = options
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
this.currentValue = value;
}
plus(addend) {
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.setValue(this.currentValue - subtrahend);
}
}

// test
const calculator = new Calculator()
calculator.plus(10);
calculator.getCurrentValue() // 10
calculator.minus(5);
calculator.getCurrentValue() // 5

确定核心系统的生命周期


实现Hooks


核心系统想要对外提供生命周期钩子,就需要一个事件机制。不妨叫Hooks。(日常开发可以考虑使用webpack的核心库 Tapable


class Hooks {
constructor() {
this.listeners = {};
}
on(eventName, handler) {
let listeners = this.listeners[eventName];
if (!listeners) {
this.listeners[eventName] = listeners = [];
}
listeners.push(handler);
}
off(eventName, handler) {
const listeners = this.listeners[eventName];
if (listeners) {
this.listeners[eventName] = listeners.filter((l) => l !== handler);
}
}
trigger(eventName, ...args) {
const listeners = this.listeners[eventName];
const results = [];
if (listeners) {
for (const listener of listeners) {
const result = listener.call(null, ...args);
results.push(result);
}
}
return results;
}
destroy() {
this.listeners = {};
}
}

暴露生命周期(通过Hooks)


然后将hooks运用在核心系统中 -- JavaScript 计算器


每个钩子对应的事件:



  • pressedPlus 做加法操作

  • pressedMinus 做减法操作

  • valueWillChanged 即将赋值currentValue,如果执行此钩子后返回值为false,则中断赋值。

  • valueChanged 已经赋值currentValue


class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0 } = options
this.currentValue = initialValue;
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
const result = this.hooks.trigger('valueWillChanged', value);
if (result.length !== 0 && result.some( _ => ! _ )) {
} else {
this.currentValue = value;
}
this.hooks.trigger('valueChanged', this.currentValue);
}
plus(addend) {
this.hooks.trigger('pressedPlus', this.currentValue, addend);
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.hooks.trigger('pressedMinus', this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}
}

设计插件的结构


插件注册


class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options
this.currentValue = initialValue;
// 在options中取出plugins
// 通过plugin执行apply来注册插件 -- apply执行后会绑定(插件内的)处理函数到生命周期
plugins.forEach(plugin => plugin.apply(this.hooks));
}
...
}

插件实现


插件一定要实现apply方法。在Calculator的constructor调用时,才能确保插件“apply执行后会绑定(插件内的)处理函数到生命周期”。


apply的入参是this.hooks,通过this.hooks来监听生命周期并添加处理器。


下面实现一个日志插件和限制最大值插件:


// 日志插件:用console.log模拟下日志
class LogPlugins {
apply(hooks) {
hooks.on('pressedPlus',
(currentVal, addend) => console.log(`${currentVal} + ${addend}`));
hooks.on('pressedMinus',
(currentVal, subtrahend) => console.log(`${currentVal} - ${subtrahend}`));
hooks.on('valueChanged',
(currentVal) => console.log(`结果: ${currentVal}`));
}
}

// 限制最大值的插件:当计算结果大于100时,禁止赋值
class LimitPlugins {
apply(hooks) {
hooks.on('valueWillChanged', (newVal) => {
if (100 < newVal) {
console.log('result is too large')
return false;
}
return true
});
}
}

全部代码


class Hooks {
constructor() {
this.listener = {};
}

on(eventName, handler) {
if (!this.listener[eventName]) {
this.listener[eventName] = [];
}
this.listener[eventName].push(handler);
}

trigger(eventName, ...args) {
const handlers = this.listener[eventName];
const results = [];
if (handlers) {
for (const handler of handlers) {
const result = handler(...args);
results.push(result);
}
}
return results;
}

off(eventName, handler) {
const handlers = this.listener[eventName];
if (handlers) {
this.listener[eventName] = handlers.filter((cb) => cb !== handler);
}
}

destroy() {
this.listener = {};
}
}

class Calculator {
constructor(options = {}) {
this.hooks = new Hooks();
const { initialValue = 0, plugins = [] } = options;
this.currentValue = initialValue;
plugins.forEach((plugin) => plugin.apply(this.hooks));
}
getCurrentValue() {
return this.currentValue;
}
setValue(value) {
const result = this.hooks.trigger("valueWillChanged", value);
if (result.length !== 0 && result.some((_) => !_)) {
} else {
this.currentValue = value;
}
this.hooks.trigger("valueChanged", this.currentValue);
}
plus(addend) {
this.hooks.trigger("pressedPlus", this.currentValue, addend);
this.setValue(this.currentValue + addend);
}
minus(subtrahend) {
this.hooks.trigger("pressedMinus", this.currentValue, subtrahend);
this.setValue(this.currentValue - subtrahend);
}
}

class LogPlugins {
apply(hooks) {
hooks.on("pressedPlus", (currentVal, addend) =>
console.log(`${currentVal} + ${addend}`)
);
hooks.on("pressedMinus", (currentVal, subtrahend) =>
console.log(`${currentVal} - ${subtrahend}`)
);
hooks.on("valueChanged", (currentVal) =>
console.log(`结果: ${currentVal}`)
);
}
}

class LimitPlugins {
apply(hooks) {
hooks.on("valueWillChanged", (newVal) => {
if (100 < newVal) {
console.log("result is too large");
return false;
}
return true;
});
}
}

// run test
const calculator = new Calculator({
plugins: [new LogPlugins(), new LimitPlugins()],
});
calculator.plus(10);
calculator.minus(5);
calculator.plus(1000);

脚本的执行结果如下,大家也可以自行验证一下



看完代码可以回顾一下“插件系统的设计和执行流程”哈。


更多实现


假如要给Calculator设计一个扩展运算方式的插件,支持求平方、乘法、除法等操作,这时候怎么写?


实际上目前核心系统Calculator是不支持的,因为它并没有支持的钩子。那这下只能改造Calculator。


可以自行尝试一下怎么改造。也可以直接看答案:github.com/coder-xuwen…


最后


插件化的好处


在上文代码实现的过程中,可以感受到插件让Calculator变得更具扩展性。



  • 核心系统(Core)只包含系统运行的最小功能,大大降低了核心代码的包体积。

  • 插件(plugin)则是互相独立的模块,提供单一的功能,提高了内聚性,降低了系统内部耦合度。

  • 每个插件可以单独开发,也支持了团队的并行开发。

  • 另外,每个插件的功能不一样,也给用户提供了选择功能的能力。


本文的局限性


另外,本文的代码实现很简单,仅供大家理解,大家还可以继续完善:



  • 增加ts类型,比如给把所有钩子的类型用emun记录起来

  • 支持动态加载插件

  • 提供异常拦截机制 -- 处理注册插件插件的情况

  • 暴露接口、处理钩子返回的结构时要注意代码安全


参考


Designing a JavaScript Plugin System | CSS-Tricks


当我们说插件系统的时候,我们在说什么 - 掘金


干货!撸一个webpack插件(内含tapable详解+webpack流程) - 掘金


精读《插件化思维》


【干货】React 组件插件化的简洁实现


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

拖拽神器:Pragmatic-drag-and-drop!

web
哈喽,大家好 我是 xy👨🏻‍💻。今天给大家分享一个开源的前端最强拖拽组件 — pragmatic-drag-and-drop! 前言 在前端开发中,拖拽功能是一种常见的交互方式,它能够极大提升用户体验。 今天,我们要介绍的是一个开源的前端拖拽组件 — p...
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。今天给大家分享一个开源的前端最强拖拽组件 — pragmatic-drag-and-drop



前言


在前端开发中,拖拽功能是一种常见的交互方式,它能够极大提升用户体验。



今天,我们要介绍的是一个开源的前端拖拽组件 — pragmatic-drag-and-drop,它以其轻量级高性能强大的兼容性,成为了前端开发者的新宠。


什么是 pragmatic-drag-and-drop?


pragmatic-drag-and-drop 是由 Atlassian 开源的一款前端拖拽组件。



Atlassian,作为全球知名的软件开发公司,其核心产品 TrelloJiraConfluence 都采用了 pragmatic-drag-and-drop 组件。


这不仅证明了该组件的实用性和可靠性,也反映了 Atlassian 对前端交互体验的极致追求。


组件的作者:Alex Reardon,也是流行 React 开源拖拽组件 react-beautiful-dnd 的开发者。


pragmatic-drag-and-drop 继承了作者对拖拽交互的深刻理解,支持多种拖拽场景,包括列表面板表格网格绘图调整大小等。


为什么选择 pragmatic-drag-and-drop?



  • 轻量化:核心包大小仅为 4.7KB,轻量级的体积使得它在加载速度上具有优势。

  • 灵活性:提供无头(headless)解决方案,开发者可以完全自定义视觉效果和辅助技术控制。

  • 框架无关性:适用于所有主流前端框架,如 React、Svelte、Vue 和 Angular。

  • 高性能:支持虚拟化,适应各种复杂的用户体验,确保拖拽操作流畅。

  • 全平台覆盖:在所有主流浏览器移动设备上运行良好,包括 Firefox、Safari、Chrome 以及 iOS 和 Android 设备。

  • 无障碍支持:为非鼠标操作用户提供友好体验,确保所有用户都能享受拖拽体验。


应用场景


pragmatic-drag-and-drop 功能适用于多种场景,包括但不限于:



  • 任务管理应用:通过拖放操作,轻松实现卡片式任务列表的排序与整理。

  • 文档管理系统:简化文件夹和文件的移动与组织过程,提高工作效率。

  • 在线编辑器:提供直观的内容布局调整体验,增强用户自定义能力。

  • 数据可视化工具:允许用户动态调整图表元素位置,实现更丰富的信息展示。

  • 设计工具:在组件库中轻松排列组合元素,激发创意无限可能。


案例演示


列表拖拽排序:



面板拖拽:



表格拖拽排序:



树形节点拖拽:



绘图功能鼠标拖动:



可拖动棋子的棋盘:



在线演示地址:https://atlassian.design/components/pragmatic-drag-and-drop/examples


最后



如果觉得本文对你有帮助,希望能够给我点赞支持一下哦 💪 也可以关注wx公众号:前端开发爱好者 回复加群,一起学习前端技能 公众号内包含很多实战精选资源教程,欢迎关注



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

为什么不写注释?写“为什么不”注释?

原文:Hillel - 2024.09.10 代码是用结构化的机器语言编写的,而注释是用富有表现力的人类语言编写的。人类语言让注释比代码更具表达性和沟通能力。代码中也包含少量类似于人类语言的内容,例如标识符。所谓“注释要写为什么,而不是写做了什么”,意思是尽可...
继续阅读 »

why-not-comments.0.png


原文Hillel - 2024.09.10


代码是用结构化的机器语言编写的,而注释是用富有表现力的人类语言编写的。人类语言让注释比代码更具表达性和沟通能力。代码中也包含少量类似于人类语言的内容,例如标识符。所谓“注释要写为什么,而不是写做了什么”,意思是尽可能将信息嵌入到标识符中。并非所有“做了什么”都能这样嵌入,但很多都可以。


近年来,我看到越来越多的人主张,连“为什么”也不应该出现在注释中,它们可以通过LongFunctionNames(长函数名)或测试用例的名称体现出来。几乎所有“自解释”代码库都通过增加标识符来进行文档化。


那么,有哪些人类表达的内容是无法通过更多代码来呈现的呢?


反面信息,也就是引起注意系统中“没有的”东西。“为什么不”的问题。


一个近期的例子


这是一个来自《Logic for Programmers》的例子。由于技术上的复杂原因,epub 电子书构建过程中未能将数学符号(\forall)正确转换为符号()。我写了一个脚本,手动遍历并将数学字符串中的标记替换为对应的 Unicode 等价符号。最简单的方法是对每个需要替换的 16 个数学符号依次调用string = string.replace(old, new)(一些数学字符串包含多个符号)。


这种方法效率非常低,我可以将所有 16 个替换在一次遍历中完成。但那将是一个更复杂的解决方案。因此,我选择了简单的方法,并加了一条注释:


对每个字符串进行了 16 次遍历。
整本书中只有 25 个数学字符串,大多数字符少于 5 个。
因此,速度仍然足够快。

你可以把这看作是“为什么我用了慢的代码”的解释,但也可以理解为“为什么不用快的代码”。它引起了对“没有的东西”的关注。


为什么要有注释


如果慢速代码没有造成任何问题,为什么还要写注释呢?


首先,这段代码可能以后会成为问题。如果将来的《Logic for Programmers》版本中有上百个数学字符串,而不是几十个,这个构建步骤将成为整个构建过程的瓶颈。现在留下标注,方便将来知道该修复什么。


即使这段代码永远不会有问题,注释仍然很重要:它表明我意识到了权衡。假设两年后我回到这个项目,打开epub_math_fixer.py,看到我这段糟糕的慢代码。我会问自己:“当时为什么写了这么糟糕的代码?” 是因为缺乏经验,时间紧迫,还是纯粹的随机失误?


这条反面注释告诉我,我知道这段代码很慢,考虑过替代方案,并决定不做优化。这样,我不必花大量时间重新调查,却得出同样的结论。


为什么这不能通过代码“自解释”(self-documented)


当我第一次尝试这个想法时,有人告诉我,我的反面注释没有必要,只需将函数命名为RunFewerTimesSlowerAndSimplerAlgorithmAfterConsideringTradeOffs。除了名字过长、未解释权衡点,并且如果我优化了代码,还得在所有地方修改函数名外……这实际上使代码更不能自解释。因为它没有告诉你函数实际做了什么


核心问题在于,函数和变量的标识符只能包含一条信息。我无法在一个标识符中同时存储“函数做了什么”和“它作出了什么权衡”。


那么用测试代替注释呢?我猜你可以写一个测试,用grep查找书中的数学块,并在超过 80 个时失败?但这并没有直接测试EpubMathFixer。函数本身没有任何内容可以让你直接关联上。


这是“自解释”反面信息的根本问题。“自解释”是伴随代码书写的,它描述了代码在做什么。而反面信息是关于代码没有做什么的。


最后的思考


我在想,是否可以将“为什么不”注释视为反事实的一个例子。如果是这样,那么“人类沟通的抽象”是否一般都无法“自解释”?你能“自解释”一个比喻吗?不确定性呢?伦理主张呢?


作者:阿然a
来源:juejin.cn/post/7413311432970993704
收起阅读 »

用了Go的匿名结构体,搬砖效率更高,产量更足了

今天给大家分享一个使用匿名结构体,提升Go编程效率的小技巧,属于在日常写代码过程中积累下来的一个提升自己效率的小经验。 这个技巧之所以提效率主要体现在两方面: 减少一些不会复用的类型定义 节省纠结该给类型起什么名字的时间 尤其第二项,通过匿名结构体这个名字...
继续阅读 »

今天给大家分享一个使用匿名结构体,提升Go编程效率的小技巧,属于在日常写代码过程中积累下来的一个提升自己效率的小经验。


这个技巧之所以提效率主要体现在两方面:



  • 减少一些不会复用的类型定义

  • 节省纠结该给类型起什么名字的时间


尤其第二项,通过匿名结构体这个名字就能体现出来,它本身没有类型名,这能节省不少想名字的时间。再一个也能减少起错名字给其他人带来的误解,毕竟并不是所有人编程时都会按照英文的词法做命名的。


下面我先从普通结构体说起,带大家看看什么情形下用匿名结构体会带来编码效率的提升。


具名结构体


具名结构体就是平时用的普通结构体。


结构体大家都知道,用于把一组字段组织在一起,来在Go语言里抽象表达现实世界的事物,类似“蓝图”一样。


比如说定义一个名字为Car的结构体在程序里表示“小汽车”


// 定义结构体类型'car'
type car struct {
    make    string
    model   string
    mileage int
}

用到这个结构体的地方通过其名字引用其即可,比如创建上面定义的结构体的实例


// 创建car 的实例
newCar := car{
    make:    "Ford",
    model:   "taurus",
    mileage: 200000,
}

匿名结构体


匿名结构体顾名思义就是没有名字的结构体,通常只用于在代码中仅使用一次的结构类型,比如


func showMyCar() {
    newCar := struct {
        make    string
        model   string
        mileage int
    }{
        make:    "Ford",
        model:   "Taurus",
        mileage: 200000,
    }
    fmt.Printlb(newCar.mode)
}

上面这个函数中声明的匿名结构体赋值给了函数中的变量,所以只能在函数中使用。


如果一个结构体初始化后只被使用一次,那么使用匿名结构体就会很方便,不用在程序的package中定义太多的结构体类型,比如在解析接口的响应到结构体后,就可以使用匿名结构体


用于解析接口响应


func createCarHandler(w http.ResponseWriter, req *http.Request) {
    defer req.Body.Close()
    decoder := json.NewDecoder(req.Body)
    newCar := struct {
        Make    string `json:"make"`
        Model   string `json:"model"`
        Mileage int    `json:"mileage"`
    }{}
    err := decoder.Decode(&newCar)
    if err != nil {
        log.Println(err)
        return
    }
    ......
    return
}

类似上面这种代码一般在控制层写,可以通过匿名结构体实例解析到请求后再去创建对应的DTO或者领域对象供服务层或者领域层使用。


有人会问为什么不直接把API的响应解析到DTO对象里,这里说一下,匿名结构体的使用场景是在觉得定一个Struct 不值得、不方便的情况下才用的。 比如程序拿到接口响应后需要按业务规则加工下才能创建DTO实例这种情况,就很适合用匿名结构体先解析响应。


比用map更健壮


这里再说一点使用匿名结构体的好处。


使用匿名解析接口响应要比把响应解析到map[string]interface{}类型的变量里要好很多,json数据解析到匿名结构体的时候在解析的过程中会进行类型检查,会更安全。使用的时候直接通过s.FieldName访问字段也比map访问起来更方便和直观。


用于定义项目约定的公共字段


除了上面这种结构体初始化后只使用一次的情况,在项目中定义各个接口的返回或者是DTO时,有的公共字段使用匿名结构体声明类型也很方便。


一般在启动项目的时候我们都会约定项目提供的接口的响应值结构,比如响应里必须包含CodeMsgData三个字段,每个接口会再细分定义返回的Data的结构,这个时候用匿名结构题能节省一部分编码效率。


比如下面这个Reponse的结构体类型的定义


type UserCouponResponse struct {
 Code int64  `json:"code"`
 Msg  string `json:"message"`
 Data []*struct {
  CouponId           int    `json:"couponId"`
  ProdCode           string `json:"prodCode"`
  UserId             int64  `json:"userId"`
  CouponStatus       int    `json:"couponStatus"`
  DiscountPercentage int    `json:"discount"`
 } `json:"data"`
}

就省的先去定义一个UserCoupon类型


type UserCoupon struct {
    CouponId           int    `json:"couponId"`
    ProdCode           string `json:"prodCode"`
    UserId             int64  `json:"userId"`
    CouponStatus       int    `json:"couponStatus"`
    DiscountPercentage int    `json:"discount"`


再在Response声明里使用定义的UserCoupon了


type UserCouponResponse struct {
    Code int64  `json:"code"`
    Msg  string `json:"message"`
    Data []*UserCoupon `json:"data"`
}

当然如果UserCoupon是你的项目其他地方也会用到的类型,那么先声明,顺带在Response结构体里也使用是没问题的,只要会多次用到的类型都建议声明成正常的结构体类型。


还是那句话匿名结构体只在你觉得"这还要定义个类型?”时候使用,用好的确实能提高点代码生产效率。


总结


本次的分享就到这里了,内容比较简单,记住这个口诀:匿名结构体只在你写代码时觉得这还要定义个类型,感觉没必要的时候使用,采纳这个技巧,时间长了还是能看到一些自己效率的提高的。


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

不到50元如何自制智能开关?

前言家里床头离开关太远了,每次躺在床上玩手机准备睡觉时候还的下去关灯,着实麻烦,所以用目前仅有的一点单片机知识,做了一个小小小的智能开关,他有三个模块构成,如下:主模块是ESP32(20元)他是一个低成本、低功耗的微控制器,集成了 Wi-Fi 和蓝牙功能,因为...
继续阅读 »

前言

家里床头离开关太远了,每次躺在床上玩手机准备睡觉时候还的下去关灯,着实麻烦,所以用目前仅有的一点单片机知识,做了一个小小小的智能开关,他有三个模块构成,如下:

  1. 主模块是ESP32(20元)

    他是一个低成本、低功耗的微控制器,集成了 Wi-Fi 和蓝牙功能,因为我们需要通过网络去开/关灯,还有一个ESP8266,比这个便宜,大概6 块钱,但是他烧录程序的时候比较慢。

image.png

  1. 光电开关(10元)

    这个可有可无,这个是用来当晚上下班回家后,开门自动开灯使用的,如果在他前面有遮挡,他的信号线会输出高/低电压,这个取决于买的是常开还是常闭,我买的是常开,当有物体遮挡时,会输出低电压,所以当开门时,门挡住了它,它输出低电压给ESP32,ESP32读取到电压状态后触发开灯动作。

image.png

  1. 舵机 SG90(5元)

    这是用来触发开/关灯动作的设备,需要把它用胶粘在开关上,他可以旋转0-180度,力度也还行,对于开关足够了。还有一个MG90舵机,力度特别大,但是一定要买180度的,360度的舵机只能正转和反转,不能控制角度。

image.png

eb46b573f6d1479e9a904699d652893.jpg

  1. 杜邦线(3元)

image.png

Arduino Ide

Arduino是什么就不说了,要烧录代码到ESP32,需要使用官方乐鑫科技提供的ESP-IDF工具,它是用来开发面向ESP32和ESP32-S系列芯片的开发框架,但是,Arduino Ide提供了一个核心,封装了ESP-IDF一些功能,便于我们更方便的开发,当然Arduino还有适用于其他开发板的库。

Arduino配置ESP32的开发环境比较简单,就是点点点、选选选即可。

接线

下面就是接线环节,先看下ESP32的引脚,他共有30个引脚,有25个GPIO(通用输入输出)引脚,如下图中紫色的引脚,在我们的这个设备里,舵机和光电开关都需要接入正负级到下图中的红色(VCC)和黑色(GND)引脚上,而他们都需要在接入一个信号作为输出/输入点,可以在着25个中选择一个,但还是有几个不能使用的,比如有一些引脚无法配置为输出,只用于作输入,还有RX和TX,我们这里使用26(光电开关)和27(舵机)引脚就可以了。

image.png

esp32代码

下面写一点点代码,主要逻辑很简单,创建一个http服务器,用于通过外部去控制舵机的转向,外部通过http请求并附带一个角度参数,在通过ESP32Servo这个库去使舵机角度发生改变。

esp32的wifi有以下几种模式。

  1. Station Mode(STA模式): 在STA模式下,esp32可以连接到一个wifi,获取一个ip地址,并且可以与网络中的其他设备进行通信。
  2. Access Point Mode(AP模式): 在AP模式下,它充当wifi热点,其他设备可以连接到esp32,就像连接到普通路由器一样,一般用作配置模式使用,经常买到的智能设备,进入配置模式和后,他会开一个热点,你的手机连接到这个热点后,在通过他们提供的app去配置,就是用这种模式。
  3. Soft Access Point Mode(SoftAP模式): 同时工作在STA模式和AP模式下。

下一步根据自己的逻辑,比如当光电开关被遮挡时,并且又是xxxx时,就开灯,或者当xxx点后就关灯。

#include 
#include
#include
#include
#include
#define SERVO_PIN_NUMBER 27
#define STATE_PIN_NUMBER 26
#define CLOSE_VALUE 40
#define OPEN_VALUE 150
const char* ssid = "wifi名称";
const char* password = "wifi密码";

AsyncWebServer server(80);
Servo systemServo;

bool openState = false;
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_AP_STA);
WiFi.begin(ssid, password);
Serial.println("\nConnecting");

while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(100);

}
systemServo.attach(SERVO_PIN_NUMBER);
systemServo.write(90);
openState = false;
write_state(CLOSE_VALUE);//启动时候将灯关闭

Serial.print("Local ESP32 IP: ");
Serial.println(WiFi.localIP());
pinMode(STATE_PIN_NUMBER, INPUT);
int timezone = 8 * 3600;
configTime(timezone, 0, "pool.ntp.org");

server.on("/set_value", HTTP_GET, [](AsyncWebServerRequest * request) {
if (request->hasParam("value")) {
String value = request->getParam("value")->value();
int intValue = value.toInt();
write_state(intValue);
request->send(200, "text/plain", "value: " + String(intValue));
} else {
request->send(400, "text/plain", "error");
}
});
server.begin();
}

void write_state(int value) {
openState = value < 90 ? false : true;

systemServo.write(value);
delay(100);
systemServo.write(90);
}
void loop() {
time_t now = time(nullptr);
struct tm *timeinfo;
timeinfo = localtime(&now);

//指定时间关灯
int currentMin = timeinfo->tm_min;
int currentHour = timeinfo->tm_hour;
if (currentHour == 23 && currentMin == 0 && openState ) {
write_state(CLOSE_VALUE);
openState = false;
}
//下班开灯
if (digitalRead(STATE_PIN_NUMBER) == 0 && currentHour > 18 && !openState) {
write_state(OPEN_VALUE);
openState = true;
}
}

Android下控制

当然,还得需要通过外部设备进行手动开关,这里就简单写一个Android程序,上面写了一个http服务,访问esp32的ip地址,发起一个http请求就可以了,所以浏览器也可以,但更方便的是app,效果如下。

6a6462a333e0124fc1ad0d5c2a4e5cf.jpg


package com.example.composedemo

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.example.composedemo.ui.theme.ComposeDemoTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL

class MainActivity : ComponentActivity() {
private val state = State()
private lateinit var sharedPreferences: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
state.ipAddressChange = {
with(sharedPreferences.edit()) {
putString("ipAddress", it)
apply()
}
}
state.slideChange = {setValue(it) }
state.lightChange = {
Log.i(TAG, "onCreate: $it")
if (it) openLight()
if (!it) closeLight()
}
state.esp32IpAddress.value = sharedPreferences.getString("ipAddress", "")!!

setContent {
ComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
SlidingOvalLayout(state)
}
}
}
}

private fun closeLight() =setValue(40)

private fun openLight() = setValue(150)

private fun setValue(value: Int) {
sendHttpRequest("http://${state.esp32IpAddress.value}/set_value/?value=$value:")
}

private fun sendHttpRequest(url: String) {
GlobalScope.launch(Dispatchers.IO) {
try {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connect()
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val response = connection.inputStream.bufferedReader().readText()
withContext(Dispatchers.Main) {
}
} else {
withContext(Dispatchers.Main) {
}
}
connection.disconnect()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
}
}
}
}
}


@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeDemoTheme {
}
}

ui组件

package com.example.composedemo

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.example.composedemo.ui.theme.ComposeDemoTheme

const val TAG = "TAG"

@Composable
fun SlidingOvalLayout(state: State) {
var offset by remember { mutableStateOf(Offset(0f, 0f)) }
var parentWidth by remember { mutableStateOf(0) }
var sliderValue by remember { mutableStateOf(0) }
var closeStateColor by remember { mutableStateOf(Color(0xFFDF2261)) }
var openStateColor by remember { mutableStateOf(Color(0xFF32A34B)) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(40.dp)
.width(100.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
Column {
Box() {
TextField(
value = state.esp32IpAddress.value,
onValueChange = {
state.esp32IpAddress.value = it
state.ipAddressChange(it)
},
colors = TextFieldDefaults.textFieldColors(
disabledTextColor = Color.Transparent,
backgroundColor = Color(0xFFF1EEF1),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
),
modifier = Modifier
.fillMaxWidth()
.background(color = Color(0xFFF1EEF1))
)

}
Box() {
Column() {
Text(text = sliderValue.toString())
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Slider(
value = sliderValue.toFloat(),
onValueChange = {
sliderValue = it.toInt()
state.slideChange(sliderValue)},
valueRange = 0f..180f,
onValueChangeFinished = {

},
colors = SliderDefaults.colors(
thumbColor = Color.Blue,
activeTrackColor = Color.Blue
)
)
}
}

}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(70.dp)
.shadow(10.dp, shape = RoundedCornerShape(100.dp))
.background(color = Color(0xFFF1EEF1))
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
parentWidth = placeable.width
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
) {
Box(
modifier = Modifier
.offset {
if (state.lightValue.value) {
IntOffset((parentWidth - 100.dp.toPx()).toInt(), 0)
} else {
IntOffset(0, 0)
}
}
.graphicsLayer {
translationX = offset.x
}
.clickable() {
state.lightValue.value = !state.lightValue.value
state.lightChange(state.lightValue.value )
}
.pointerInput(Unit) {
}
.background(
color = if(state.lightValue.value) openStateColor else closeStateColor,
shape = RoundedCornerShape(100.dp)
)
.size(Dp(100f), Dp(80f))
)
}
}
}
}

@Preview
@Composable
fun PreviewSlidingOvalLayout() {
ComposeDemoTheme {
}
}
class State {
var esp32IpAddress: MutableState = mutableStateOf("")
var lightValue :MutableState<Boolean> = mutableStateOf(false)

var ipAddressChange :(String)->Unit={}

var slideChange:(Int)->Unit={}

var lightChange:(Boolean)->Unit={}

}

作者:i听风逝夜
来源:juejin.cn/post/7292245569482407988

收起阅读 »

从《逆行人生》聊聊中年程序员的出路

赶在下架前去看了《逆行人生》。 这部电影讲述了高志垒——一个架构师,被裁员后,在找工作两个月颗粒无收的情况下,被逼无奈去跑外卖的故事。 个人感觉还是很值得一看的,是一部很好的现实题材电影,并没有网上说的资本迫害打工人还要打工人努力奋斗的感觉。 有年轻人说,难以...
继续阅读 »

a84ac789e4ab76f547708661ed1630f2367c47ce.jpg


赶在下架前去看了《逆行人生》。


这部电影讲述了高志垒——一个架构师,被裁员后,在找工作两个月颗粒无收的情况下,被逼无奈去跑外卖的故事。


个人感觉还是很值得一看的,是一部很好的现实题材电影,并没有网上说的资本迫害打工人还要打工人努力奋斗的感觉。


有年轻人说,难以共情。70万年薪的人最后要落到为了 15k 的月薪而奔波,他不理解为什么。然而就我亲身经历而言,无路可走的时候,我们的确会做这样的选择。


我们先来看看中年程序员有哪些选择。


中年程序员有哪些出路?


中年三宝插画-2.jpeg


继续打工


打工,无疑是多数人的选择。毕竟上一天班赚一天的钱,这种稳稳的幸福还是大部分人的追求。但打工也不能停滞不前,还是要不断学习、拓展自己的能力,尤其是我们IT行业,技术更新迭代快。如果不学习,很可能 3 到 5 年就被淘汰了。


程序员要晋升、跳槽,主要学习方向以下两种:



  • 拓展技术的深度与广度:高级开发、架构师、热门行业的开发如AI等;

  • 向外拓展自己的能力:培训机构老师、高校老师;技术管理;


自己单干


继续打工,无疑都会碰到被裁员的风险,你个人的命运始终掌握在老板甚至顶头上司的手里。如果你不甘于此,就要开创性地走其他路了。这对个人的挑战都是极大的。


程序员可以凭借技术能力逐渐向外扩展:



  • 独立开发:承接项目或者自研产品

  • 创业:成立公司、团队,完成开发工作


彻底转行


也有部分人被彻底伤透了心,完全不再进入这个行业了,转向其他行业了。



  • 网络兼职:写手、影视剪辑等;

  • 中年三保、铁人三项:保安、保姆、保洁、快递、司机、外卖。这个是被大家调侃最多的;

  • 其他行业的打工者:如制造业、外贸等行业;

  • 开店或者创业:存上一笔钱开店或者做一间自己喜欢的公司,也是一些人的选择。


我们应该如何选择?


如上所见,程序员能做的选择还是比较多的。我们将这些工作列个表,列一下所需要的能力与所承担的责任,以及最后的风险,以便做选择:


截屏2024-09-16 14.25.39.png


可以看到,每个方向其实都是有风险的,并没有不存在无风险的职业与方向。那是不是我们就可以完全凭借个人喜好来决定呢?并非如此,这些选择对大部分人而言,还是有优劣之分的。


不推荐铁人三项、中年三宝


中年三宝插画-7.jpeg


首先,我个人其实非常不建议程序员转行去做起他行业的,除非迫不得已,尤其是从事体力劳动。


因为这需要消耗大量的体力与时间。中年人无法靠比拼体力取胜,工作时间长,也无法取得工作生活平衡。在电影《逆行人生》中,高志垒虽然赢了第一个单王,但可以看出其靠的更多是运气,行业老大哥或退出竞赛、或家里有事提早离开。


另外就是,AI 技术发展和市场供需变化。不久前武汉的萝卜快跑落地,相信大部分滴滴司机都感受到了被淘汰的可能。而且这类工作市场基本上已经饱和,所以薪酬只会越来越低。


其他的网络兼职、去制造业服务业打工,这些都是门槛低,程序员即使有技术与能力,也不见得有任何优势的,所以也是不推荐的。


而开店或按自己的兴趣来创业,则非常看你个人能力了,同样需要更谨慎的考虑,当然你如果家财万贯,倒是可以任性一把。


更推荐提早规划、提早行动


剩下的职业方向其实都是推荐的,因为多多少少跟我们自身学习的技术是相关的。将我们的能力逐步往外扩,逐渐走出舒适圈,是更合适的一个发展路径。但是需要注意的是,建议尽早立下目标,提前规划,尽快行动的。


如,希望做老师,可以提早在企业内部做讲师、技术讲师,给新人讲解。锻炼好自己的沟通表达能力,多想想如何让新人更好地融入企业、进入工作状态。


又如,你想自己创业,那可以开始就留意你手头上做的产品是如何开发、运营的。公司如何分配人力物力去做的,如何做商业变现的,如何寻找客户的等等这些问题。不仅要站在技术角度、也要站在公司的角度多思考、多学习、多实践。甚至在时机成熟的时候,提出转岗去做产品、技术管理,更早地锻炼自己所需的创业的能力,能让自己日后的路走的更顺。


高志垒为何还是选择送外卖?


中年三宝插画-5.jpeg


回到电影,既然都不建议程序员从事体力劳动,高志垒好好的一个架构师,也是有脑子的,为啥最后还是选择了外卖员呢?


首先,从影片一开始可以看出,高志垒选择了架构师或者技术管理偏技术方向,因其手头上还有一线开发的任务。显然对于 45 岁的他,在打工这条路上几乎已经到顶了。


然而,他并没有做好职业规划,甚至从未考虑过失业的风险。在突然失业时,才发现市场上几乎找不到自己的职位、薪酬,最后简历也是乱投一气了;而中产返贫三件套:高额房贷、全职太太、国际学校,他几乎全都拥有;并且还大笔地投资了 P2P ,因其爆雷导致家庭财产大量损失;再加上其父亲突发重病,住院急需要钱。


所有的状况同时出现,所有的压力压在身上,在两个月投递简历无果时,他听说送外卖能补上房贷月供差额的数目,宛如找到救命稻草一般,毅然加入了外卖行业。


如何避免陷入被动状况?


如何避免我们也陷入高志垒的状况?


除了像上面说的提早积攒自己的能力,提早做规划、更早地行动外,程序员也应提升技能多样性,特别是专业外的技能;同时在职业中后期应寻找到更利于个人发展的公司或项目;还需要拓展人脉,保持与行业内的沟通交流;在最后,保持健康的生活习惯和平衡好工作,让自己的职业寿命尽可能地延长。


中年三宝插画-9.jpeg


而在财务上,做好失业准备、甚至为后续独立开发、创业等积攒资金都是必要的,所以需要采取一些措施,做好家庭财务的规划,如:



  1. 留出紧急备用金:为应对突发事件,如失业或疾病,应建立足够的紧急基金,一般建议为家庭日常开支的3-6个月。

  2. 谨慎投资:只投资自己熟悉的产品;了解自身的风险承受能力再投资;同时避免将所有资金投入到单一的高风险产品中,如P2P,应进行资产配置,分散风险。

  3. 购买保险:为家庭成员购买适当的健康保险,以减轻因病致贫的风险。

  4. 做好财务预算、规划:每年、每月做好财务预算;同时对于房贷和教育投资等大额支出,应进行详细的财务规划,确保在收入中断时也能应对。

  5. 增加收入来源:尽可能地增加家庭收入来源,比如配偶就业或开展副业,减少对单一收入的依赖。


总结与思考


66bf3e22-63b4-443c-9411-038325654067.jpg


在戏里的高志垒无疑是幸运的,家庭和睦,家人都给予最大的支持,愿意一起度过难关。再加上自己开发的小程序“路路通”,同事间互助互利,最后,成功拿到了单王,并帮家里度过经济危机。


然而最后的结局,高志垒并没有“逆袭”人生,而是在“逆行”人生中,调整了自己。最后他卖掉了大房子,搬到了小房子住,老婆依然在工作,孩子也放弃了就读国际学校、老人靠自身意志力完成了康复。


这也是我觉得这部电影还算现实主义之处。并没有理想中的事情发生,就像现实生活中那些受挫的人们一样,最后选择降低生活标准,继续前行。


最后的最后,问一下大家,如果你面临电影结尾彩蛋中的情景,有一个外卖公司的高层老板对你开发的“路路通”小程序感兴趣,你会如何选择?



  • 卖掉小程序,拿钱走人

  • 加入外卖公司,继续开发

  • 不卖,开源


欢迎留下你的答案与思考,一起讨论。


作者:陈佬昔的编程人生
来源:juejin.cn/post/7414732910240972835
收起阅读 »

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

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
收起阅读 »