注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

数字签名 Signature

这一章,我们将简单的介绍以太坊中的数字签名ECDSA,以及如何利用它发放NFT白名单。代码中的ECDSA库由OpenZeppelin的同名库简化而成。 数字签名 如果你用过opensea交易NFT,对签名就不会陌生。下图是小狐狸(metamask)钱包进行签名...
继续阅读 »

这一章,我们将简单的介绍以太坊中的数字签名ECDSA,以及如何利用它发放NFT白名单。代码中的ECDSA库由OpenZeppelin的同名库简化而成。


数字签名


如果你用过opensea交易NFT,对签名就不会陌生。下图是小狐狸(metamask)钱包进行签名时弹出的窗口,它可以证明你拥有私钥的同时不需要对外公布私钥。


截屏2024-06-04 14.35.26.png


以太坊使用的数字签名算法叫双椭圆曲线数字签名算法(ECDSA),基于双椭圆曲线“私钥-公钥”对的数字签名算法。它主要起到了三个作用



  1. 身份认证:证明签名方是私钥的持有人。

  2. 不可否认:发送方不能否认发送过这个消息。

  3. 完整性:消息在传输过程中无法被修改。


ECDSA合约


ECDSA标准中包含两个部分:



  1. 签名者利用私钥(隐私的)对消息(公开的)创建签名(公开的)。

  2. 其他人使用消息(公开的)和签名(公开的)恢复签名者的公钥(公开的)并验证签名。 我们将配合ECDSA库讲解这两个部分。本教程所用的私钥公钥消息以太坊签名消息签名如下所示:


私钥: 0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b
公钥: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2
消息: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
以太坊签名消息: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
签名: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

创建签名


1. 打包消息:  在以太坊的ECDSA标准中,被签名的消息是一组数据的keccak256哈希,为bytes32类型。我们可以把任何想要签名的内容利用abi.encodePacked()函数打包,然后用keccak256()计算哈希,作为消息。我们例子中的消息是由一个address类型变量和一个uint256类型变量得到的:


    /*
* 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
* _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
* _tokenId: 0
* 对应的消息msgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
*/

function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
return keccak256(abi.encodePacked(_account, _tokenId));
}

截屏2024-06-04 14.42.33.png


2. 计算以太坊签名消息:  消息可以是能被执行的交易,也可以是其他任何形式。为了避免用户误签了恶意交易,EIP191提倡在消息前加上"\x19Ethereum Signed Message:\n32"字符,并再做一次keccak256哈希,作为以太坊签名消息。经过toEthSignedMessageHash()函数处理后的消息,不能被用于执行交易:


    /**
* @dev 返回 以太坊签名消息
* `hash`:消息
* 遵从以太坊签名标准:https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191`
* 添加"\x19Ethereum Signed Message:\n32"字段,防止签名的是可执行交易。
*/
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
// 哈希的长度为32
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}

处理后的消息为:


以太坊签名消息: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b

截屏2024-06-04 14.44.07.png


3-1. 利用钱包签名:  日常操作中,大部分用户都是通过这种方式进行签名。在获取到需要签名的消息之后,我们需要使用metamask钱包进行签名。metamaskpersonal_sign方法会自动把消息转换为以太坊签名消息,然后发起签名。所以我们只需要输入消息签名者钱包account即可。需要注意的是输入的签名者钱包account需要和metamask当前连接的account一致才能使用。


因此首先把例子中的私钥导入到小狐狸钱包,然后打开浏览器的console页面:Chrome菜单-更多工具-开发者工具-Console。在连接钱包的状态下(如连接opensea,否则会出现错误),依次输入以下指令进行签名:


ethereum.enable()
account = "0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2"
hash = "0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c"
ethereum.request({method: "personal_sign", params: [account, hash]})

在返回的结果中(PromisePromiseResult)可以看到创建好的签名。不同账户有不同的私钥,创建的签名值也不同。利用教程的私钥创建的签名如下所示:


0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

截屏2024-06-04 14.57.06.png


3-2. 利用web3.py签名:  批量调用中更倾向于使用代码进行签名,以下是基于web3.py的实现。


from web3 import Web3, HTTPProvider
from eth_account.messages import encode_defunct

private_key = "0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b"
address = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
rpc = 'https://rpc.ankr.com/eth'
w3 = Web3(HTTPProvider(rpc))

#打包信息
msg = Web3.solidityKeccak(['address','uint256'], [address,0])
print(f"消息:{msg.hex()}")
#构造可签名信息
message = encode_defunct(hexstr=msg.hex())
#签名
signed_message = w3.eth.account.sign_message(message, private_key=private_key)
print(f"签名:{signed_message['signature'].hex()}")

运行的结果如下所示。计算得到的消息,签名和前面的案例一致。


消息:0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
签名:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

验证签名


为了验证签名,验证者需要拥有消息签名,和签名使用的公钥。我们能验证签名的原因是只有私钥的持有者才能够针对交易生成这样的签名,而别人不能。


4. 通过签名和消息恢复公钥: 签名是由数学算法生成的。这里我们使用的是rsv签名签名中包含r, s, v三个值的信息。而后,我们可以通过r, s, v以太坊签名消息来求得公钥。下面的recoverSigner()函数实现了上述步骤,它利用以太坊签名消息 _msgHash签名 _signature恢复公钥(使用了简单的内联汇编):


    // @dev 从_msgHash和签名_signature中恢复signer地址
function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address){
// 检查签名长度,65是标准r,s,v签名的长度
require(_signature.length == 65, "invalid signature length");
bytes32 r;
bytes32 s;
uint8 v;
// 目前只能用assembly (内联汇编)来从签名中获得r,s,v的值
assembly {
/*
前32 bytes存储签名的长度 (动态数组存储规则)
add(sig, 32) = sig的指针 + 32
等效为略过signature的前32 bytes
mload(p) 载入从内存地址p起始的接下来32 bytes数据
*/

// 读取长度数据后的32 bytes
r := mload(add(_signature, 0x20))
// 读取之后的32 bytes
s := mload(add(_signature, 0x40))
// 读取最后一个byte
v := byte(0, mload(add(_signature, 0x60)))
}
// 使用ecrecover(全局函数):利用 msgHash 和 r,s,v 恢复 signer 地址
return ecrecover(_msgHash, v, r, s);
}

参数分别为:


// 以太坊签名消息
_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
// 签名
_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c

截屏2024-06-04 15.09.23.png


5. 对比公钥并验证签名:  接下来,我们只需要比对恢复的公钥与签名者公钥_signer是否相等:若相等,则签名有效;否则,签名无效:


    /**
* @dev 通过ECDSA,验证签名地址是否正确,如果正确则返回true
* _msgHash为消息的hash
* _signature为签名
* _signer为签名地址
*/

function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) {
return recoverSigner(_msgHash, _signature) == _signer;
}

参数分别为:


_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b
_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c
_signer:0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2

截屏2024-06-04 15.10.34.png


利用签名发放白名单


NFT项目方可以利用ECDSA的这个特性发放白名单。由于签名是链下的,不需要gas。方法非常简单,项目方利用项目方账户把白名单发放地址签名(可以加上地址可以铸造的tokenId)。然后mint的时候利用ECDSA检验签名是否有效,如果有效,则给他mint


SignatureNFT合约实现了利用签名发放NFT白名单。


状态变量


合约中共有两个状态变量:



  • signer公钥,项目方签名地址。

  • mintedAddress是一个mapping,记录了已经mint过的地址。


函数


合约中共有4个函数:



  • 构造函数初始化NFT的名称和代号,还有ECDSA的签名地址signer

  • mint()函数接受地址addresstokenId_signature三个参数,验证签名是否有效:如果有效,则把tokenIdNFT铸造给address地址,并将它记录到mintedAddress。它调用了getMessageHash()ECDSA.toEthSignedMessageHash()verify()函数。

  • getMessageHash()函数将mint地址(address类型)和tokenIduint256类型)拼成消息

  • verify()函数调用了ECDSA库的verify()函数,来进行ECDSA签名验证。


contract SignatureNFT is ERC721 {
address immutable public signer; // 签名地址
mapping(address => bool) public mintedAddress; // 记录已经mint的地址

// 构造函数,初始化NFT合集的名称、代号、签名地址
constructor(string memory _name, string memory _symbol, address _signer)
ERC721(_name, _symbol)
{
signer = _signer;
}

// 利用ECDSA验证签名并mint
function mint(address _account, uint256 _tokenId, bytes memory _signature)
external
{
bytes32 _msgHash = getMessageHash(_account, _tokenId); // 将_account和_tokenId打包消息
bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // 计算以太坊签名消息
require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // ECDSA检验通过
require(!mintedAddress[_account], "Already minted!"); // 地址没有mint过
_mint(_account, _tokenId); // mint
mintedAddress[_account] = true; // 记录mint过的地址
}

/*
* 将mint地址(address类型)和tokenId(uint256类型)拼成消息msgHash
* _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
* _tokenId: 0
* 对应的消息: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c
*/

function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){
return keccak256(abi.encodePacked(_account, _tokenId));
}

// ECDSA验证,调用ECDSA库的verify()函数
function verify(bytes32 _msgHash, bytes memory _signature)
public view returns (bool)
{
return ECDSA.verify(_msgHash, _signature, signer);
}
}

总结


这一讲,我们介绍了以太坊中的数字签名ECDSA,如何利用ECDSA创建和验证签名,还有ECDSA合约,以及如何利用它发放NFT白名单。代码中的ECDSA库由OpenZeppelin同名库简化而成。



  • 由于签名是链下的,不需要gas,因此这种白名单发放模式比Merkle Tree模式还要经济;

  • 但由于用户要请求中心化接口去获取签名,不可避免的牺牲了一部分去中心化;

  • 额外还有一个好处是白名单可以动态变化,而不是提前写死在合约里面了,因为项目方的中心化后端接口可以接受任何新地址的请求并给予白名单签名。


作者:Subs
来源:juejin.cn/post/7376324160484327424
收起阅读 »

我写了个ffmpeg-spring-boot-starter 使得Java能剪辑视频!!

最近工作中在使用FFmpeg,加上之前写过较多的SpringBoot的Starter,所以干脆再写一个FFmpeg的Starter出来给大家使用。 首先我们来了解一下FFmpeg能干什么,FFmpeg 是一个强大的命令行工具和库集合,用于处理多媒体数据。它可以...
继续阅读 »

最近工作中在使用FFmpeg,加上之前写过较多的SpringBoot的Starter,所以干脆再写一个FFmpeg的Starter出来给大家使用。


首先我们来了解一下FFmpeg能干什么,FFmpeg 是一个强大的命令行工具和库集合,用于处理多媒体数据。它可以用来做以下事情:



  • 解码:将音频和视频从压缩格式转换成原始数据。

  • 编码:将音频和视频从原始数据压缩成各种格式。

  • 转码:将一种格式的音频或视频转换为另一种格式。

  • 复用:将音频、视频和其他流合并到一个容器中。

  • 解复用:从一个容器中分离出音频、视频和其他流。

  • 流媒体:在网络上传输音频和视频流。

  • 过滤:对音频和视频应用各种效果和调整。

  • 播放:直接播放媒体文件。


FFmpeg支持广泛的编解码器和容器格式,并且由于其开源性质,被广泛应用于各种多媒体应用程序中,包括视频会议软件、在线视频平台、编辑软件等。
例如


在这里插入图片描述
作者很喜欢的一款截图软件ShareX就使用到了FFmpeg的功能。


现在ffmpeg-spring-boot-starter已发布,maven地址为
ffmpeg-spring-boot-starter


在这里插入图片描述


那么如何使用ffmpeg-spring-boot-starter 呢?


第一步,新建一个SpringBoot项目


SpringBoot入门:如何新建SpringBoot项目(保姆级教程)


第二步,在pom文件里面引入jar包


<dependency>
<groupId>io.gitee.wangfugui-ma</groupId>
<artifactId>ffmpeg-spring-boot-starter</artifactId>
<version>${最新版}</version>
</dependency>

第三步,配置你的ffmpeg信息


在yml或者properties文件中配置如下信息


ffmpeg.ffmpegPath=D:\\ffmpeg-7.0.1-full_build\\bin\\

注意这里要配置为你所安装ffmpeg的bin路径,也就是脚本(ffmpeg.exe)所在的目录,之所以这样设计的原因就是可以不用在系统中配置环境变量,直接跳过了这一个环节(一切为了Starter)


第四步,引入FFmpegTemplate


    @Autowired
private FFmpegTemplate ffmpegTemplate;

在你的项目中直接使用Autowired注解注入FFmpegTemplate即可使用


第五步,使用FFmpegTemplate


execute(String command)



  • 功能:执行任意FFmpeg命令,捕获并返回命令执行的输出结果。

  • 参数command - 需要执行的FFmpeg命令字符串。

  • 返回:命令执行的输出结果字符串。

  • 实现:使用Runtime.getRuntime().exec()启动外部进程,通过线程分别读取标准输出流和错误输出流,确保命令执行过程中的所有输出都被记录并可被进一步分析。

  • 异常:抛出IOExceptionInterruptedException,需在调用处妥善处理。


FFmpeg执行器,这是这里面最核心的方法,之所以提供这个方法,是来保证大家的自定义的需求,例如FFmpegTemplate中没有封装的方法,可以灵活自定义ffmpeg的执行参数。


convert(String inputFile, String outputFile)



  • 功能:实现媒体文件格式转换。

  • 参数inputFile - 待转换的源文件路径;outputFile - 转换后的目标文件路径。

  • 实现:构建FFmpeg命令,调用FFmpeg执行器完成媒体文件格式的转换。


就像这样:


    @Test
void convert() {
ffmpegTemplate.convert("D:\\video.mp4","D:\\video.avi");
}

extractAudio(String inputFile)



  • 功能:精确提取媒体文件的时长信息。

  • 参数inputFile - 需要提取时长信息的媒体文件路径。

  • 实现:构造特定的FFmpeg命令,仅请求媒体时长数据,直接调用FFmpeg执行器并解析返回的时长值。


就像这样:


    @Test
void extractAudio() { System.out.println(ffmpegTemplate.extractAudio("D:\\video.mp4"));
}


copy(String inputFile, String outputFile)



  • 功能:执行流复制,即在不重新编码的情况下快速复制媒体文件。

  • 参数inputFile - 源媒体文件路径;outputFile - 目标媒体文件路径。

  • 实现:创建包含流复制指令的FFmpeg命令,直接调用FFmpeg执行器,以达到高效复制的目的。


    就像这样:



    @Test
void copy() {
ffmpegTemplate.copy("D:\\video.mp4","D:\\video.avi");
}

captureVideoFootage(String inputFile, String outputFile, String startTime, String endTime)



  • 功能:精准截取视频片段。

  • 参数inputFile - 源视频文件路径;outputFile - 截取片段的目标文件路径;startTime - 开始时间;endTime - 结束时间。

  • 实现:构造FFmpeg命令,指定视频片段的开始与结束时间,直接调用FFmpeg执行器,实现视频片段的精确截取。


@Test
void captureVideoFootage() {
ffmpegTemplate.captureVideoFootage("D:\\video.mp4","D:\\cut.mp4","00:01:01","00:01:12");
}

scale(String inputFile, String outputFile, Integer width, Integer height)



  • 功能:调整媒体文件的分辨率。

  • 参数inputFile - 源媒体文件路径;outputFile - 输出媒体文件路径;width - 目标宽度;height - 目标高度。

  • 实现:创建包含分辨率调整指令的FFmpeg命令,直接调用FFmpeg执行器,完成媒体文件分辨率的调整。


    @Test
void scale() {
ffmpegTemplate.scale("D:\\video.mp4","D:\\video11.mp4",640,480);
}

cut(String inputFile, String outputFile, Integer x, Integer y, Integer width, Integer height)



  • 功能:实现媒体文件的精确裁剪。

  • 参数inputFile - 源媒体文件路径;outputFile - 裁剪后媒体文件路径;x - 裁剪框左上角X坐标;y - 裁剪框左上角Y坐标;width - 裁剪框宽度;height - 裁剪框高度。

  • 实现:构造FFmpeg命令,指定裁剪框的坐标与尺寸,直接调用FFmpeg执行器,完成媒体文件的精确裁剪。


    @Test
void cut() {
ffmpegTemplate.cut("D:\\video.mp4","D:\\video111.mp4",100,100,640,480);
}

embedSubtitle(String inputFile, String outputFile, String subtitleFile)



  • 功能:将字幕文件内嵌至视频中。

  • 参数inputFile - 视频文件路径;outputFile - 输出视频文件路径;subtitleFile - 字幕文件路径。

  • 实现:构造FFmpeg命令,将字幕文件内嵌至视频中,直接调用FFmpeg执行器,完成字幕的内嵌操作。


    @Test
void embedSubtitle() {
ffmpegTemplate.embedSubtitle("D:\\video.mp4","D:\\video1211.mp4","D:\\srt.srt");
}

merge(String inputFile, String outputFile)



  • 功能: 通过外部ffmpeg工具将多个视频文件合并成一个。

  • 参数:

    • inputFile: 包含待合并视频列表的文本文件路径。

    • outputFile: 合并后视频的输出路径。




是这样用的:


   @Test
void merge() {
ffmpegTemplate.merge("D:\\mylist.txt","D:\\videoBig.mp4");
}

注意,这个mylist.txt文件长这样:
在这里插入图片描述


后续版本考虑支持



  1. 添加更多丰富的api

  2. 区分win和Linux环境(脚本执行条件不同)

  3. 支持在系统配置环境变量(用户如果没有配置配置文件的ffmpegPath信息可以自动使用环境变量)



在这里插入图片描述



作者:掉头发的王富贵
来源:juejin.cn/post/7391326728461647872
收起阅读 »

Swoole v6 能否让 PHP 再次伟大?

大家好,我是码农先森。 现状 传统的 PHP-FPM 也是多进程模型的的运行方式,但每个进程只能处理完当前请求,才能接收下一个请求。而且对于 PHP 脚本来说,只是接收请求和响应请求,并不参与网络通信。对数据库资源的操作,也是一次请求一次有效,用完即销毁不能复...
继续阅读 »

大家好,我是码农先森。


现状


传统的 PHP-FPM 也是多进程模型的的运行方式,但每个进程只能处理完当前请求,才能接收下一个请求。而且对于 PHP 脚本来说,只是接收请求和响应请求,并不参与网络通信。对数据库资源的操作,也是一次请求一次有效,用完即销毁不能复用,在系统高负载的情况下对数据库等资源的消耗会很大,能承受的并发量有限。



Swoole 的出现给 PHP 带来了一种新的运行方式,完全接管了 PHP-FPM 的功能,并且弥补了 PHP 在异步网络通信领域的空白。Swoole 提供了 PHP 的全生命周期管理,此外 Swoole 的常驻进程模式,也能够高效的利用资源,比如可以建立数据库连接池、共享内存变量等。还有 Swoole 中能够支撑高并发的利器「协程」,更加使 PHP 的性能上了一个新的台阶,甚至在某些特定场景下都可以与 Go 语言的性能相媲美。


虽说 Swoole 给 PHP 带来了很大的性能提升,但也还是一个基于多进程模型的异步通信扩展,多进程的模式也存在着许多的问题,比如跨进程间的通信、进程间的资源共享等问题。简而言之,多进程会带来一定的系统资源消耗及产生新的问题。


因此 Swoole 官方为了解决多进程的问题,引进了多线程的支持,这意味着 v6 版本之后,Swoole 将会变成单进程多线程的运行模式。


v6 新特性


根据 Swoole 作者韩天峰发布的预告,在 v6 版本中增加多线程的支持。其中多线程的实现是基于 PHP 的 ZTS 机制和 TSRM API,在 PHP 层面隔离所有全局变量,实现线程安全。Swoole v6 的多线程将是真正的多线程实现,在单进程的模式下所有的 PHP 程序代码均是在多核并行执行,能够高效的利用好 CPU 资源。



v6 版本还提供了线程安全的 Map 和 ArrayList 数据结构,可以实现跨线程的数据共享读写。在 Server 端的 Event Worker、Task Worker、User Process 等将全部替换为 线程的运行方式,在同一个进程空间内执行,彻底摒弃了多进程的模式。


当然新的特性势必会带来新的开销,对于 Map 等共享的数据结构在多线程的模式下需要加锁,来避免数据竞争,可能会损耗一些性能。


以下是列举的一些线程相关的 API 方法:



  • use Swoole\Thread 线程对象。

  • use Swoole\Thread\Map 线程安全下的 Map 数据结构。

  • use Swoole\Thread\ArrayList 线程安全下的 ArrayList 数据结构。

  • Swoole\Thread::getId() 获取当前线程的 ID。

  • Swoole\Thread::getArguments() 获取父线程传递给子线程的参数列表。

  • Swoole\Thread::join() 等待子线程退出,请注意 $thread 对象销毁时会自动执行 join() ,这可能会导致进程阻塞。

  • Swoole\Thread::joinable() 检测子线程是否已退出。

  • Swoole\Thread::detach() 使子线程独立运行,不再需要 Thread::join()。

  • Swoole\Thread::HARDWARE_CONCURRENCY 硬件层支持的并行线程数量。

  • Swoole\Thread::$id 获取子线程的 ID。

  • Swoole\Thread::exec() 开启一个新的线程。


最后


自 Swoole 从 2012 年发布第一个版本开始,就扛起了 PHP 领域异步通信的大旗,但这多年以来 Swoole 的发展也是实属不易。还记得刚开始时的异步回调模式的套娃式编程方式,开发起来异常艰难,到后来的同步式编程,直接降低了PHP程序员的学习门槛,让 PHP 在实时通信、物联网通信、游戏开发等领域也能大展拳脚,同时在 PHP 的发展史上也产生了重大的影响。


随着 Go 语言在编程界的持续火热,Swoole 常常被 PHP 程序员拿来和 Go 语言一决高下,总是被诟病 Swoole 无法有效利用多核 CPU、进程间的通信困难等问题。话又说回来,Swoole 作为一个 PHP 的扩展程序和天生具有高性能的 Go 语言自然是不可比拟的,但 Swoole 也是在逐渐的向 Go 语言靠近,比如 Swoole 中也使用了「go、channel」关键词来实现协程及通信通道,虽说底层的实现机制还是大不相同的。


当然 Swoole 也在不断地努力持续优化,就像将要推出的 v6 版本增加多线程的支持,来改变目前多进程的局面。至于这个版本对 PHP 发展来说有没有很大的影响,我认为影响有限。但对 Swoole 的发展还是有很大的影响,毕竟以后再也不用受多进程的困扰了,这也是一大进步。


在 Web 领域作为世界上最好的语言,尽管 PHP 近年来的发展不尽如人意,但作为一名 PHPer 也有必要和有义务一起来维护和推动 PHP 生态的发展。





欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。



作者:A码农先森
来源:juejin.cn/post/7384696986845085731
收起阅读 »

微信公众号推送消息笔记

根据业务需要,开发一个微信公众号的相关开发,根据相关开发和整理总结了一下相关的流程和需要,进行一些整理和总结分享给大家,最近都在加班和忙碌,博客已经很久未更新了,打气精神,再接再厉,申请、认证公众号的一系列流程就不在这里赘述了,主要进行的是技术的分享,要达到的...
继续阅读 »

根据业务需要,开发一个微信公众号的相关开发,根据相关开发和整理总结了一下相关的流程和需要,进行一些整理和总结分享给大家,最近都在加班和忙碌,博客已经很久未更新了,打气精神,再接再厉,申请、认证公众号的一系列流程就不在这里赘述了,主要进行的是技术的分享,要达到的效果如下图:


999999.png


开发接入


首先说明我这里用的是PHP开发语言来进行的接入,设置一个url让微信公众号的服务回调这个url,在绑定之前需要一个token的验证,设置不对会提示token不正确的提示


官方提供的测试Url工具:developers.weixin.qq.com/apiExplorer…


private function checkSignature()
{
$signature = isset($_GET["signature"]) ? $_GET["signature"] : '';
$timestamp = isset($_GET["timestamp"]) ? $_GET["timestamp"] : '';
$nonce = isset($_GET["nonce"]) ? $_GET["nonce"] : '';
$echostr = isset($_GET["echostr"]) ? $_GET["echostr"] : '';
$token = 'klsg2024';
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr );
if( $tmpStr == $signature ){
return $echostr;
}else{
return false;
}
}

在设置的地方调用: 微信公众号的 $echostr 和 自定义的匹配上说明调用成功了


public function console(){
//关注公众号推送
$posts = $this->posts;
if(!isset($_GET['openid'])){
$res = $this->checkSignature();
if($res){
echo $res;
return true;
}else{
return false;
}
}
}

设置access_token


公众号的开发的所有操作的前提都是先设置access_token,在于验证操作的合法性,所需要的token在公众号后台的目录中获取:公众号-设置与开发-基本设置 设置和查看:


#POST https://api.weixin.qq.com/cgi-bin/token
{
"grant_type": "client_credential",
"appid": "开发者ID(AppID)",
"secret": "开发者密码(AppSecret)"
}

返回的access_token,过期时间2个小时,Http url 返回结构如下:


{
"access_token": "82_W8kdIcY2TDBJk6b1VAGEmA_X_DLQnCIi5oSZBxVQrn27VWL7kmUCJFVr8tjO0S6TKuHlqM6z23nzwf18W1gix3RHCw6uXKAXlD-pZEO7JcAV6Xgk3orZW0i2MFMNGQbAEARKU",
"expires_in": 7200
}

为了方便起见,公众号平台还开放了一个稳定版的access_token,参数略微有不同。


POST https://api.weixin.qq.com/cgi-bin/stable_token
{
"grant_type": "client_credential",
"appid": "开发者ID(AppID)",
"secret": "开发者密码(AppSecret)",
"force_refresh":true
}

自定义菜单


第一个疑惑是公众号里的底部菜单 是怎么搞出来的,在官方文档中获取到的,如果公众号后台没有设置可以根据自定义菜单来进行设置。


1、创建菜单,参数自己去官方文档上查阅


POST https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN

2、查询菜单接口,文档和调试工具给的有点不一样,我使用的是调试工具给出的url


GET https://api.weixin.qq.com/cgi-bin/menu/get?access_token=ACCESS_TOKEN

3、删除菜单


GET https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=ACCESS_TOKEN

事件拦截


在公众号的开发后台里会设置一个Url,每次在操作公众号时都会回调接口,用事件去调用和处理,操作公众号后,微信公众平台会请求到设置的接口上,公众号的openid 比较重要,是用来识别用户身份的唯一标识,openid即当前用户。


{
"signature": "d43a23e838e2b580ca41babc78d5fe78b2993dea",
"timestamp": "1721273358",
"nonce": "1149757628",
"openid": "odhkK64I1uXqoUQjt7QYx4O0yUvs"
}

用户进行相关操作时,回调接口会收到这样一份请求,都是用MsgType和Event去区分,下面是关注的回调:


{
"ToUserName": "gh_d98fc9c8e089",
"FromUserName": "用户openID",
"CreateTime": "1721357413",
"MsgType": "event",
"Event": "subscribe",
"EventKey": []
}

下面是点击菜单跳转的回调:


{
"ToUserName": "gh_d98fc9c8e089",
"FromUserName": "用户openID",
"CreateTime": "1721381657",
"MsgType": "event",
"Event": "VIEW",
"EventKey": "https:\/\/zhjy.rchang.cn\/api\/project_audit\/getOpenid?type=1",
"MenuId": "421351906"
}

消息推送


消息能力是公众号中最核心的能力,我们这次主要分享2个,被动回复用户消息和模板推送能力。


被动回复用户消息


被动回复用户消息,把需要的参数拼接成xml格式的,我觉得主要是出于安全上的考虑作为出发点。


<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>

在php代码里的实现即为:


protected function subscribe($params)
{
$time = time();
$content = "欢迎的文字";
$send_msg = '<xml>
<ToUserName><![CDATA['
.$params['FromUserName'].']]></ToUserName>
<FromUserName><![CDATA['
.$params['ToUserName'].']]></FromUserName>
<CreateTime>'
.time().'</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA['
.$content.']]></Content>
</xml>'
;

echo $send_msg;
return false;
}

模板推送能力


模版推送的两个关键是申请了模版,还有就是模版的data需要和模版中的一致,才能成功发送,模版设置和申请的后台位置在 广告与服务-模版消息


public function project_message()
{
$touser = '发送人公众号openid';
$template_id = '模版ID';
$url = 'https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=' . $this->access_token;
$details_url = '点开链接,需要跳转的详情url';
$thing1 = '模版里定义的参数';
$time2 = '模版里定义的参数';
$const3 = '模版里定义的参数';
$send_data = [
'touser' => $touser,
'template_id' => $template_id,
'url' => $details_url,
'data' => [
'thing1' => ['value' => $thing1],
'time2' => ['value' => $time2],
'const3' => ['value' => $const3],
]
];

$result = curl_json($url, $send_data);
}

错误及解决方式


1、公众号后台: 设置与开发-安全中心-IP白名单 把IP地址加入白名单即可。


{
"errcode": 40164,
"errmsg": "invalid ip 47.63.30.93 ipv6 ::ffff:47.63.30.93, not in whitelist rid: 6698ef60-27d10c40-100819f9"
}

2、模版参数不正确时,接口返回


{
"errcode": 47003,
"errmsg": "argument invalid! data.time5.value invalid rid: 669df26e-538a8a1a-15ab8ba4"
}

3、access_token不正确


{
"errcode": 40001,
"errmsg": "invalid credential, access_token is invalid or not latest, could get access_token by getStableAccessToken, more details at https://mmbizurl.cn/s/JtxxFh33r rid: 669df2f1-74be87a6-05e77d20"
}

4、access_token超过调用次数


{
"errcode": 45009,
"errmsg": "reach max api daily quota limit, could get access_token by getStableAccessToken, more details at https:\/\/mmbizurl.cn\/s\/JtxxFh33r rid: 669e5c4c-2bb4e05f-61d6917c"
}

文档参考


公众号开发文档首页: developers.weixin.qq.com/doc/offiacc…


分享一个微信公众号的调试工具地址,特别好用 : mp.weixin.qq.com/debug/


作者:stark张宇
来源:juejin.cn/post/7394392321988575247
收起阅读 »

首屏优化之:import 动态导入

web
 前言 前面我们聊过可以通过不同的 script 属性比如 defer,async 来实现不同的加载效果,从而实现首屏优化。 今天我们来聊一下动态导入之 import,当然 import 动态导入也不是一把梭的,也是需要根据项目情况进行使用,在面试以...
继续阅读 »

 前言


前面我们聊过可以通过不同的 script 属性比如 defer,async 来实现不同的加载效果,从而实现首屏优化。


今天我们来聊一下动态导入之 import,当然 import 动态导入也不是一把梭的,也是需要根据项目情况进行使用,在面试以及实际工作当中都可以用到,一起来看看吧!


在了解动态导入之前,我们先来看一下什么是静态导入。 


静态导入


静态导入会在编译时解析所有导入的模块,并在程序开始执行之前加载它们。这意味着所有被导入的模块在应用启动时就已经加载完毕


什么意思,我们先来看一下下面这段代码:


这段代码很简单,我在页面导入了 import.js,当点击按钮时打印输出语句。



我们来看一下浏览器初始化加载情况:



很明显,程序开始执行之前,import.js 就被加载了。


但是在某些时刻,我们不希望文件在没有被使用时就被加载,只希望在使用时加载,这样可以优化首屏的加载速度,这些时刻我们就可以使用动态导入。


动态导入


动态导入是一种在代码执行时按需加载模块的技术,而不是在应用程序初始化时加载所有模块。


默认不会一上来加载所有文件,只会在用到时加载,这样可以优化初始加载时间,提升页面响应速度。


动态导入与静态导入不同,动态导入使用 ES6 中的 import() 语法,可以在函数或代码块中调用,从而实现条件加载、按需加载或延迟加载。例如:


import('./import.js')

 还是上面的代码,我们使用动态导入来进行实现一下:



我们再来看一下浏览器的加载情况:


可以看到一上来并没有加载 import.js



当点击按钮时,才加载了 import.js 文件,这就说明import导入的文件不会一上来就直接被加载,而是在相关代码被执行时才进行加载的。



 一些应用


路由懒加载


在 react 中我们常常使用 lazy 和 Suspense 来实现路由的懒加载,这样做的好处就是初始化时不会一下加载所有的页面,而是当切换到相应页面时才会加载相应的页面,例如:



组件动态导入


 对于一些不常用或者不需要直接加载的组件我们也可以采用动态导入,比如弹出框。


我们只需要在点击时进行加载显示即可。



 分包优化


这里就简单说一下分包的优化,webpack 默认的分包规则有以下三点:



  1. 通过配置多个入口 entry,可以将不同的文件分开打包。

  2. 使用 import() 语法,Webpack 会自动将动态导入的模块放到单独的包中。‘

  3. entry.runtime 单独组织成一个 chunk。


根据第二点,被动态导入的文件会被单独进行打包,不会被分包进同一个文件,也就不会在初始加载 bundle.js 时被一起进行加载。


通过将代码分割成多个小包,可以在用户需要时才加载特定的模块,从而显著减少初始加载的时间。


总结


在进行首屏优化时,可以采取动态导入的方式实现,使用 import('./文件路径')实现,虽然动态导入有一些优化首屏渲染的优势,但是也有一些缺点,比如首次加载延迟,不利于 SEO 优化等,所以在使用动态导入时应该好好进行规划,比如一些不常用的模块或者内容不太复杂,对加载速度无要求的文件可以进行动态导入,这个还是要根据项目的需求来进行使用的。


作者:JacksonChen
来源:juejin.cn/post/7400332893158391819
收起阅读 »

优雅的处理async/await错误

web
async/await使用 async/await 解决了Promise的链式调用(.then)造成的回调地狱,使异步回调变得像同步一样美观! 使用的方式如下: // 异步函数1 let postFun1 = function () { retur...
继续阅读 »

async/await使用



async/await 解决了Promise的链式调用(.then)造成的回调地狱,使异步回调变得像同步一样美观!



使用的方式如下:


// 异步函数1
let postFun1 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve('postFun1') }, 2000)
})
}
// 异步函数2
let postFun2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve('postFun2') }, 1000)
})
}

// async/await
async function syncFun() {
let s1 = await postFun1()
console.log(s1)
let s2 = await postFun2()
console.log(s2)
console.log('s1、s2都获取到了,我才会执行')
}

syncFun()

可以看出,在syncFun函数中,我们获取异步信息,书写方式就跟同步一样,不用.then套.then,很美观!


不捕获错误会怎样


// 异步函数1
let postFun1 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { resolve('postFun1') }, 2000)
})
}
// 异步函数2
let postFun2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('err') }, 1000)
})
}

async function asyncFun() {
let s1 = await postFun1();
let s2 = await postFun2();
console.log('s1、s2都获取到了,我才会执行')
}
asyncFun();

控制台:


async1.jpg


可以看出,控制台没有我们想要打印的信息console.log('s1、s2都获取到了,我才会执行')


try/catch捕获错误


我们日常开发中,都是使用try/catch捕获错误,方式如下:


let postFun1 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('err1') }, 2000)
})
}
let postFun2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('err2') }, 1000)
})
}

async function asyncFun() {
try{
let s1 = await postFun1();
let s2 = await postFun2();
}catch(e){
console.log(e)
}
console.log('s1、s2都获取到了,我才会执行')
}
asyncFun();

控制台:


async2.png


可以看出,我们抛出两个reject,但是只捕获到了一个错误!


那么捕获多个错误,我们就需要多个try/catch如此,代码便像现在这样:


let postFun1 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('err1') }, 2000)
})
}
let postFun2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject('err2') }, 1000)
})
}

async function asyncFun() {
try{
let s1 = await postFun1();
}catch(e){
console.log(e)
}
try{
let s2 = await postFun2();
}catch(e){
console.log(e)
}
console.log('s1、s2都获取到了,我才会执行')
}
asyncFun();

控制台:


async3.png



仅仅是两个try/catch已经看起来很难受了,那么10个呢?



await-to-js


/**
* @param promise 传进去的请求函数
* @param errorExt 拓展错误信息
* @return 返回一个Promise
*/

function to(promise, errorExt) {
return promise
.then(res => [null, res])
.catch(err => {
if (errorExt) {
const parsedError = Object.assign({}, err, errorExt)
return [parsedError, undefined]
}
return [err, undefined]
})
}


await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值




这里封装了一个to函数,接收promise和扩展的错误信息为参数,返回promise[err,res]分别代表错误信息和成功结果,.then()成功时,[null,res]代表错误信息为null;.catch()失败时,[err,undefined]代表,成功结果为undefined。我们获取捕获的结果直接从返回的数组中取就行,第一个是失败信息,第二个是成功结果!



完整代码加使用


function to(promise, errorExt) {
return promise
.then(res => [null, res])
.catch(err => {
if (errorExt) {
const parsedError = Object.assign({}, err, errorExt)
return [parsedError, undefined]
}
return [err, undefined]
})
}

let postFun1 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject({err:'err1'}) }, 2000)
})
}
let postFun2 = function () {
return new Promise((resolve, reject) => {
setTimeout(() => { reject({err:'err2'}) }, 1000)
})
}

async function asyncFun() {
let [err1,res1] = await to(postFun1(), {msg:'抱歉1'});
let [err2,res2] = await to(postFun2(), {msg:'抱歉2'});
console.log(err1,err2)
console.log('s1、s2都获取到了,我才会执行')
}

asyncFun()

把这个学会,在面试官面前装一波,面试官定会直呼优雅!!!


作者:爱得莱姆
来源:juejin.cn/post/7278280824846925861
收起阅读 »

threejs 搭建智驾自车场景

web
智能驾驶业务里常用web 3d来可视化地图和传感器数据、显示路径规划结果等方法协助算法调试和仿真,可以用threejs来做,毕竟在国内社区相对活跃,也比较容易上手,效果类似下图: 当然以上图片都是客户端的版本,web3d版本的ui其实并不会这么精致,毕竟只...
继续阅读 »

智能驾驶业务里常用web 3d来可视化地图和传感器数据、显示路径规划结果等方法协助算法调试和仿真,可以用threejs来做,毕竟在国内社区相对活跃,也比较容易上手,效果类似下图:




当然以上图片都是客户端的版本,web3d版本的ui其实并不会这么精致,毕竟只是服务于内部算法和研发。这个专栏纯属作者一时兴起并希望能产出一个麻雀虽小五脏俱全的行泊场景(简称人太闲),本文就先把自车的基础场景搭建起来



本文基于 three^0.167.1 版本



初始化项目


用 Vite 脚手架快速搭一个 react 项目用来调试


pnpm create vite autopilot --template react-ts

把 threejs 官网的例子稍微改下,加到项目里看看。新建一个 renderer 对象如下:


// src/renderer/index.ts
import * as THREE from "three";

class Renderer {
constructor() {
//
}

initialize() {
const container = document.getElementById("my-canvas")!;
const width = container.offsetWidth,
height = container.offsetHeight;
const camera = new THREE.PerspectiveCamera(70, width / height, 0.01, 10);
camera.position.z = 1;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.setAnimationLoop(animate);
container.appendChild(renderer.domElement);
function animate(time: number) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render(scene, camera);
}
}
}

export const myRenderer = new Renderer();

// App.tsx
import { useEffect } from "react";
import { myRenderer } from "./renderer";
import "./App.css";

function App() {
useEffect(() => {
myRenderer.initialize();
}, []);

return (
<>
<div id="my-canvas"></div>
</>

);
}

export default App;


加载自车


ok,跨出第一步了,接下来整辆自车(egoCar)



“自车”指的是自动驾驶汽车本身,它能够通过搭载的传感器、计算平台和软件系统实现自主导航和行驶



可以上 free3d 下载个免费的车辆模型,里面有很多种格式的,尽量找 gltf/glb 格式的(文件体积小,加载比较快)。


这里以加载 glb 格式的模型为例,可以先把模型文件放到 public 目录下,因为加载器相对网页的根路径(index.html)解析,而 public 目录在打包后会原封不动保存到根目录里


import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
const gltfLoader = new GLTFLoader();

class Renderer {
scene = new THREE.Scene();
// ...
loadEgoCar() {
gltfLoader.load("./su7.glb", (gltf) => {
const car = gltf.scene;
car.scale.set(0.1, 0.1, 0.1);
this.scene.add(car);
});
}
// ...
initialize() {
// ...
this.loadEgoCar();
}
}

但如果一定要放到 src/assets/models 目录里呢?然后通过import方式引入文件来用,那这么操作下来就会遇到这个报错(You may need to install appropriate plugins to handle the .glb file format, or if it's an asset, add "**/*.glb" to assetsInclude in your configuration):



怎么解?在 vite.config.ts 文件加入 assetsInclude。顺带把 vite 指定路径别名 alias 也支持一下


// vite .config.ts 
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { fileURLToPath, URL } from "node:url";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
// 指定路径别名
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
assetsInclude: ["**/*.glb"],
});


node:url 如果提示没有该模块,先安装下@types/node,可能要重启下vscode才能生效


pnpm i @types/node -D



接下来就可以直接用 import 导入 glb 文件来用了


import carModel from "@/assets/models/su7.glb";

class Renderer {
// ...
loadEgoCar() {
gltfLoader.load(carModel, (gltf) => {
const car = gltf.scene;
car.scale.set(0.1, 0.1, 0.1);
this.scene.add(car);
});
}
}

OrbitControls


增加 OrbitControls 插件,便于调节自车视角,这个插件除了围绕目标点(默认是原点[0,0,0])旋转视角,还支持缩放(滚轮)和平移(鼠标右键,触摸板的话是双指长按)


import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
class Renderer {
initialize() {
// ...
const controls = new OrbitControls(camera, renderer.domElement);
function animate() {
// ...
controls.update();
renderer.render(scene, camera);
}
}
}


光源设置


看起来场景和自车都比较暗,咱们调下光源,加一个环境光 AmbientLight 和平行光 DirectionalLight,平行光位置放自车后上方,沿着自车方向(也就是原点方向)发射光源


// ...
// 没有特定方向,影响整个场景的明暗
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
// 平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(60, 80, 40);
scene.add(directionalLight);


地面网格


增加坐标网格,新建一个 Grid 对象,提供一个水平的基准面,便于观察


// ...
// 50表示网格模型的尺寸大小,20表示纵横细分线条数量
const gridHelper = new THREE.GridHelper(50, 20);
scene.add(gridHelper);
// 顺带调高下相机位置
camera.position.set(0, 1, 1.8);
// 设置场景背景色(颜色值,透明度)
renderer.setClearColor(0x000000, 0.85);


道路实现


这里先简单实现一段不规则道路,封装一个 freespace 对象,还要考虑它的不规则和带洞的可能,所以需要做好接口定义,其实数据主要是点集,一般这些点集都是地图上游发下来的,可能是 protobuf 或者 json 的格式


export interface IFreespace {
// 一般可以用于判断元素是否可复用
id: string;
position: IPos;
contour: IPos[];
// 洞可能有多个,所以这里应该设置成二维数组
holes?: IPos[][];
color?: IColor;
}
export interface IPos {
x: number;
y: number;
z?: number;
}
export interface IColor {
r: number;
g: number;
b: number;
a?: number;
}

因为只是一个平面形状,所以可以用 THREE.Shape 来实现,它可以和 ExtrudeGeometryShapeGeometry 一起使用来创建二维形状


// src/renderers/freespace.ts
class Freespace {
scene = new THREE.Scene();

constructor(scene: THREE.Scene) {
this.scene = scene;
}

draw(data: IFreespace) {
const {
contour,
holes = [],
color = { r: 0, g: 0, b: 0 },
position,
} = data;
if (contour.length < 3) {
return;
}
const shape = new THREE.Shape();
// 先绘制轮廓
// 设置起点
shape.moveTo(contour[0].x, contour[0].y);
contour.forEach((item) => shape.lineTo(item.x, item.y));
// 绘制洞
holes.forEach((item) => {
if (item.length < 3) {
return;
}
const path = new THREE.Path();
path.moveTo(item[0].x, item[0].y);
item.forEach((subItem) => {
path.lineTo(subItem.x, subItem.y);
});
// 注意这一步
shape.holes.push(path);
});
const shapeGeometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshPhongMaterial();
// 注意:setRGB传参颜色值需要介于0-1之间
material.color.setRGB(color.r / 255, color.g / 255, color.b / 255);
material.opacity = color.a || 1;
const mesh = new THREE.Mesh(shapeGeometry, material);
mesh.position.set(position.x, position.y, position.z || 0);
mesh.rotateX(-Math.PI / 2);
this.scene.add(mesh);
}
}

export default Freespace;

ok先用mock的数据画一段带洞的十字路口,加在 initialize 代码后就行,其实道路上还应该有一些交通标线,后面再加上吧



最后再监听下界面的 resize 事件,使其能根据容器实际大小变化动态调整场景


  // ...
constructor() {
// 初始化渲染对象
this.renderers = {
freespace: new Freespace(this.scene),
};
}
initialize() {
// ...
this.loadEgoCar();
this.registerDefaultEvents();
// mock
this.mockData();
}
mockData() {
this.renderers.freespace.draw(freespaceData1);
}
// 监听resize事件
registerDefaultEvents() {
window.addEventListener("resize", this.onResize.bind(this), false);
}
unmountDefaultEvents() {
window.removeEventListener("resize", this.onResize.bind(this), false);
}
onResize() {
const container = document.getElementById("my-canvas")!;
const width = container.offsetWidth,
height = container.offsetHeight;
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
}

最后


ok先到这了,主要是先把项目搭起来,后面会继续分享下更多地图和感知元素以及他车、行人、障碍物等效果的实现



作者:_lucas
来源:juejin.cn/post/7406643531697913867
收起阅读 »

想学 pinia ?一文就够了

web
有时候不得不承认,官方的总结有时就是最精简的: Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 虽然作为Vuex的升级版,但为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3的Vuex,同时...
继续阅读 »

有时候不得不承认,官方的总结有时就是最精简的:



Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。



虽然作为Vuex的升级版,但为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3的Vuex,同时,pinia提供了一种更简洁、更直观的方式来处理应用程序的状态,更为重要的是,pinia的学习成本更低,低到一篇文章就能涵盖pinia的全部。


Pinia的安装与配置:


首先自然是安装pinia,在基于Vue3的项目环境中,提供了npmyarn两种安装方式:



npm install pinia




yarn add pinia



随后,通常就是在src目录下新建一个专属的store文件夹,在其中的js文件中创建并抛出这个仓库。


import { createPinia } from 'pinia'  // 引入pinia模块

const store = createPinia() // 创建一个仓库

export default store // 抛出这个仓库

既然把这个仓库抛出了,那么现在便是让它能在全局起作用,于是在Vue的主要应用文件中(通常为main.js),引入使用pinia


import { createApp } from 'vue'
import App from './App3.vue'

import store from './store' //引入这个仓库

createApp(App).use(store).mount('#app') // 再use一下

这样一来pinia仓库就能全局生效了!


Pinia的主要功能:


在官方文档中,Pinia提供了四种功能,分别是:



  1. Store:在Pinia中,每个状态管理模块都被称为一个Store。开发者需要创建一个Store实例来定义和管理状态。

  2. State:在Store中定义状态。可以使用defineState函数来定义一个状态,并通过state属性来访问它。

  3. Getters:类似于Vuex中的getters,用于从State中派生出一些状态。可以使用defineGetters函数来定义getters。

  4. Actions:在Pinia中,Actions用于处理异步操作或执行一些副作用。可以使用defineActions函数来定义Actions。


那么接下来我会通过一个具体的实例来表现出这四个功能,如下图:


未命名.jpg


分别是充当仓库的Store功能。存储子组件User.vue中数据的State功能。另一个子组件Update-user.vue中,点击按钮后数据会实现更新,也就是修改State中数据的Actions功能。与无论点击多少次” 经过一年后按钮 ”,页面都会实现同步更新的Getters功能。


State:


简单来说,State的作用就是作为仓库的数据源。


就比如说,我想在仓库的数据源里面放上一个对象来进行使用,那我们只需在先前创建的store文件夹中再创建一个js文件,这里我给它起名为user,然后再其中这样添加对象。


(第一行引入的defineStore代表defineStorestore的一部分。)


import { defineStore } from 'pinia'   // defineStore 是 store 的一部分

export const useUserStore = defineStore({
id: 'user',
state: () => ({ // 仓库数据源
userInfo: {
name: '小明',
age: 18,
sex:'boy'
}
})
})

那么现在,我们想使用仓库中的数据就成为了一件非常容易的事。


正如上图,这里有一个父组件App.vue,两个子组件User.vueUpdate-user.vue


父组件不做任何动作,只包含对两个子组件的引用:


<template>
<User/>
<Updateuser/>
</template>

<script setup>
import User from './components/User.vue'
import Updateuser from './components/Update-user.vue'


</script>


<style lang="css" scoped>

</style>


子组件User.vue:


可以看到在这个子组件中,我们通过import { useUserStore } from '@/store/user'引用仓库,从而获得了仓库中小明姓名年龄性别的数据。


由于接下来的Update-user.vue组件中会添加几个按钮对这些数据进行修改,那么我们就要把这些数据设置成响应式。


正常情况下,store自带响应性,但如果我们不想每次都写userStore.userInfo.name这么长一大串,就可以尝试将这些值取出来赋给其他变量:


这里有两种方法,第一种是引入computed模块,如第14行年龄的修改。另一种是引入storeToRefs模块,这是一种属于Pinia仓库的模块,将整个userInfo变成响应式。


于是接下来,就轮到我们的Actions登场了


<template>
<ul>
<li>姓名:{{ userStore.userInfo.name }}</li>
<li>年龄:{{ age }}</li>
<li>性别;{{ userInfo.sex }}</li>
</ul>

</template>

<script setup>
import { useUserStore } from '@/store/user'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const age = computed(() => userStore.userInfo.age) // 1. 计算属性使响应式能生效
const { userInfo } = storeToRefs(userStore) // 2. 专门包裹仓库中函数用来返回对象

</script>


<style lang="scss" scoped>

</style>


Actions:


简单来说,Actions的作用就是专门用来修改State,如果你想要修改仓库中的响应式元素,只需要进行两步操作:


第一步:在user.js也就是我们的仓库中添加actions,专门设置函数用来修改state对象中的值。例如changeUserName作用是修改姓名, changeUserSex作用是修改性别。


import { defineStore } from 'pinia'   // defineStore  是 store的一部分

export const useUserStore = defineStore({
id: 'user',
state: () => ({ // 仓库数据源
userInfo: {
name: '小明',
age: 18,
sex:'boy'
}
}),
actions: { // 专门用来修改state
changeUserName(name) {
this.userInfo.name = name
},
changeUserSex(sex){
this.userInfo.sex = sex
}
}
})

子组件Update-user.vue:


第二步,在控制按钮的组件Update-user.vue中触发这两个函数,就如第10与14行的两个箭头函数。


<template>
<button @click="changeName">修改仓库中用户姓名</button>
<button @click="changeSex">修改仓库中用户性别</button>
</template>

<script setup>
import { useUserStore } from '@/store/user' // 引入Pinia仓库
const userStore = useUserStore() // 声明仓库

const changeName = () => { // 触发提供的函数
userStore.changeUserName('小红')

}
const changeSex = () => {
userStore.changeUserSex('gril')
}

</script>


<style lang="css" scoped>

</style>


1724744897493.png


这样一来,依赖于Actions,我们就成功完成了响应式修改仓库中数据的功能,也就是前两个按钮的功能!


Getters:


简单来说Getters就是仓库中的计算属性。


现在我们来实现第三个按钮功能,首先就是在User.vue组件中第5行,添加 “ 十年之后年龄 ” 一栏:


<template>
<ul>
<li>姓名:{{userStore.userInfo.name}}</li>
<li>年龄:{{ age }}</li>
<li>十年后年龄:{{ userStore.afterAge }}</li> // 添加的栏
<li>性别:{{ userInfo.sex }}</li>
</ul>

</template>

<script setup>
import { useUserStore } from '@/store/user'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()
const age = computed(() => userStore.userInfo.age)
const { userInfo } = storeToRefs(userStore)

</script>


<style lang="scss" scoped>

</style>


那么现在你一定能注意到这一栏其中的userStore.afterAge,这正是我们将在getters中返回的值。


那么关于getters,具体的使用方法就是继续在user.js中添加进getters,我们在其中打造了一个afterAge函数来返回userStore.afterAge,正如第25行。


import { defineStore } from 'pinia'   // defineStore  是 store的一部分

export const useUserStore = defineStore({
id: 'user',
state: () => ({ // 仓库数据源
userInfo: {
name: '小明',
age: 18,
sex:'boy'
}
}),
actions: { // 专门用来修改state
changeUserName(name) {
this.userInfo.name = name
},
changeUserSex(sex){
this.userInfo.sex = sex
},
changeUserAge(age){ // 新添加的一年后年龄计算方法
this.userInfo.age += age
}
},
getters: { // 仓库中的计算属性,所依赖的值改变会重新执行
afterAge(state) {
return state.userInfo.age + 10
}
}
})

准备工作完毕,现在就该在页面上添加这个按钮,于是在组件Update-user.vue添加上按钮与执行函数。


 <button @click="changeAge">经过一年后</button>

const changeAge = () => {
userStore.changeUserAge(1)
}

有了这些之后,这个项目的功能便彻底完善,无论点击多少次“ 经过一年后 ”按钮,在页面上显示的值都是正确且实时更新的,这就是Getters的功劳!


补充:数据持久化


关于整个项目的功能实现确实已经结束,但人的贪心却是不得满足的,如果我们想要在原有的基础上实现网页刷新数据却不刷新,也就是说数据的持久化,那又该怎么办呢?


未命名3.jpg


很简单,也就是堪堪三步,便能实现。


第一步:安装persist插件。



npm i pinia-plugin-persist



第二步:在storejs文件中引入这个插件。


import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist' //引入插件

const store = createPinia()
store.use(piniaPluginPersist) // 使用插件

export default store

第三步:在我们前文user.jsdefineStore库内继续添加上persist功能。


persist: { // 持久化
enabled: true,
strategies: [ // 里面填想要持久化的数据
{
paths: ['userInfo'], // 指明持久化的数据
storage: localStorage // 指明存储
}
]
}

现在可以看到点击按钮后的数据都被存储到浏览器的存储空间中,无论多少次刷新都不会被重置!


1724747584370.png


最后:


至此,这样一个简简单单的项目,却解释清楚了Pinia功能的核心,读完这篇文章,相信每一个学习Pinia的人都能有所收获。


作者:木笙
来源:juejin.cn/post/7407407711879807026
收起阅读 »

你知道为什么template中不用加.value吗?

web
Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。 询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢? proxyRefs Vue3 中有有个方法prox...
继续阅读 »

Vue3 中定义的ref类型的变量,在setup中使用这些变量是需要带上.value才可以访问,但是在template中却可以直接使用。


询其原因,可能会说 Vue 自动进行ref解包了,那具体如何实现的呢?


proxyRefs


Vue3 中有有个方法proxyRefs,这属于底层 API 方法,在官方文档中并没有阐述,但是 Vue 里是可以导出这个方法。


例如:


<script setup>
import { onMounted, proxyRefs, ref } from "vue";

const user = {
name: "wendZzoo",
age: ref(18),
};
const _user = proxyRefs(user);

onMounted(() => {
console.log(_user.name);
console.log(_user.age);
console.log(user.age);
});
</script>

上面代码定义了一个普通对象user,其中age属性的值是ref类型。当访问age值的时候,需要通过user.age.value,而使用了proxyRefs,可以直接通过user.age来访问。



这也就是为何template中不用加.value的原因,Vue3 源码中使用proxyRefs方法将setup返回的对象进行处理。


实现proxyRefs


单测


it("proxyRefs", () => {
const user = {
name: "jack",
age: ref(10),
};
const proxyUser = proxyRefs(user);

expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);

proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);

proxyUser.age = ref(30);
expect(proxyUser.age).toBe(30);
expect(user.age.value).toBe(30);
});

定义一个age属性值为ref类型的普通对象userproxyRefs方法需要满足:



  1. proxyUser直接访问age是可以直接获取到 10 。

  2. 当修改proxyUserage值切这个值不是ref类型时,proxyUser和原数据user都会被修改。

  3. age值被修改为ref类型时,proxyUseruser也会都更新。


实现


既然是访问和修改对象内部的属性值,就可以使用Proxy来处理getset。先来实现get


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {}
});
}

需要实现的是proxyUser.age能直接获取到数据,那原数据target[key]ref类型,只需要将ref.value转成value


使用unref即可实现,unref的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


get(target, key) {
return unref(Reflect.get(target, key));
}

实现set


export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unref(Reflect.get(target, key));
},
set(target, key, value) {},
});
}

从单侧中可以看出,我们是测试了两种情况,一种是修改proxyUserageref类型, 一种是修改成不是ref类型的,但是结果都是同步更新proxyUseruser。那实现上也需要考虑这两种情况,需要判断原数据值是不是ref类型,新赋的值是不是ref类型。


使用isRef可以判断是否为ref类型,isRef的实现参见本专栏上篇文章,文章地址:mp.weixin.qq.com/s/lLkjpK9TG…


set(target, key, value) {
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
}

当原数据值是ref类型且新赋的值不是ref类型,也就是单测中第 1 个情况赋值为 10,将ref类型的原值赋值为valueref类型值需要.value访问;否则,也就是单测中第 2 个情况,赋值为ref(30),就不需要额外处理,直接赋值即可。


验证


执行单测yarn test ref



作者:wendZzoo
来源:juejin.cn/post/7303435124527333416
收起阅读 »

将html转化成图片

web
如何将指定html内容转化成图片保存?这个问题很值得深思,实际应用中也很有价值。最直接的想法就是使用canvas,熟悉canvas的同学可以尝试一下。这里不做太多的说明,本文采用html2canvas库来实现。 html2canvas库的使用非常简单,只需要...
继续阅读 »

如何将指定html内容转化成图片保存?这个问题很值得深思,实际应用中也很有价值。最直接的想法就是使用canvas,熟悉canvas的同学可以尝试一下。这里不做太多的说明,本文采用html2canvas库来实现。



html2canvas库的使用非常简单,只需要引入html2canvas库,然后调用html2canvas方法即可,官方地址


接下来说一下简单的使用,以react项目为例。


获取整个页面截图,可以使用底层IDroot,这样下载的就是root下的所有元素。



import html2canvas from "html2canvas";

const saveCanvas = () => {
// 画布基础元素,要绘制的元素
const canvas: any = document.getElementById("root");
const options: any = { scale: 1, useCORS: true };
html2canvas(canvas, options).then((canvas) => {
const type = "png";
// 返回值是一个数据url,是base64组成的图片的源数据
let imgDt = canvas.toDataURL(type);
let fileName = "img" + "." + type;
// 保存为文件
let a = document.createElement("a");
document.body.appendChild(a);
a.href = imgDt;
a.download = fileName;
a.click();
});
};

图片的默认背景色是#ffffff,如果想要透明色可设置为null,比如设置为红色。



const options: any = { scale: 1, useCORS: true, backgroundColor: "red" };

正常情况下网络图片是无法渲染的,可以使用useCORS属性,设置为true即可。



const options: any = { scale: 1, useCORS: true };

保存某块元素的截图



const canvas: any = document.getElementById("swiper");

如果希望将某些元素排除,可以将data-html2canvas-ignore属性添加到这些元素中,html2canvas将从渲染中排除这些元素。



<Button
data-html2canvas-ignore
color="primary"
fill="solid"
onClick={saveCanvas}
>

download
</Button>

完整代码


npm install html2canvas

// demo.less
.contentSwiper {
width: 710px;
height: 375px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
user-select: none;
}
.swiper {
padding: 0 20px;
}

import React from "react";
import { Button, Space, Swiper } from "antd-mobile";
import html2canvas from "html2canvas";
import styles from "./demo.less";

export default () => {
const saveCanvas = () => {
// 画布基础元素,要绘制的元素
const canvas: any = document.getElementById("root");
const options: any = { scale: 1, useCORS: true, backgroundColor: "red"
};
html2canvas(canvas, options).then((canvas) => {
const type = "png";
// 返回值是一个数据url,是base64组成的图片的源数据
let imgDt = canvas.toDataURL(type);
let fileName = "img" + "." + type;
// 保存为文件
let a = document.createElement("a");
document.body.appendChild(a);
a.href = imgDt;
a.download = fileName;
a.click();
});
};
const colors: string[] = ["#ace0ff", "#bcffbd", "#e4fabd", "#ffcfac"];
const items = colors.map((color, index) => (
<Swiper.Item key={index}>
<div className={styles.contentSwiper} style={{ background: color }}>
{index + 1}
</div>
</Swiper.Item>

));
return (
<div className="content">
<div id="swiper" className={styles.swiper}>
<Swiper
style={{
"--track-padding": " 0 0 16px",
}}
defaultIndex={1}
>

{items}
</Swiper>
</div>
<div>
<img
width={200}
src="https://t7.baidu.com/it/u=2621658848,3952322712&fm=193&f=GIF"
/>

</div>
<Space>
<Button
data-html2canvas-ignore
color="primary"
fill="solid"
onClick={saveCanvas}
>

download
</Button>
<Button color="primary" fill="solid">
Solid
</Button>
<Button color="primary" fill="outline">
Outline
</Button>
<Button color="primary" fill="none">

</Button>
</Space>
</div>

);
};


作者:小小愿望
来源:juejin.cn/post/7407457177483608118
收起阅读 »

Vue3中watch好用,但watchEffect、watchSyncEffect、watchPostEffect简洁

web
比较好奇vue项目中使用watch还是watchEffect居多,查看了element-plus、ant-design-vue两个UI库, 整体上看,watch使用居多,而watchEffect不怎么受待见,那这两者之间有什么关系? APIwatchwatch...
继续阅读 »

比较好奇vue项目中使用watch还是watchEffect居多,查看了element-plus、ant-design-vue两个UI库, 整体上看,watch使用居多,而watchEffect不怎么受待见,那这两者之间有什么关系?


APIwatchwatchEffectwatchSyncEffectwatchPostEffect
element-plus1982800
ant-design-vue26316800

watchEffect是watch的衍生


为什么说watchEffect是watch的衍生?



  • 首先,两者提供功能是有重叠。大部分监听场景,两者都能满足。


const list = ref([]);
const count = ref(0);

watch(
list,
(newValue) => {
count.value = newValue.length;
}
)

watchEffect(() => {
count.value = list.value.length;
})


  • 其次,源码上两者也都是同一出处。以下是两者的函数定义:


export function watch(
source: T | WatchSource
,
cb: any,
options?: WatchOptions
,
): WatchStopHandle {
return doWatch(source as any, cb, options)
}

export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase,
): WatchStopHandle {
return doWatch(effect, null, options)
}

两者内部都调用doWatch函数,并且返回都是WatchStopHandle类型。唯独入参上有比较大的区别,watch的source参数就像大杂烩,支持PlainObject、Ref、ComputedRef以及函数类型;而watchEffect的effect参数仅仅是一个函数类型。


watch早于watchEffect诞生,watch源代码有这样一句提示:


if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`,
)
}

也就是说历史的某一个版本,watch也是支持watch(fn, options?)用法,但为了降低API复杂度,将这部分功能迁移至watchEffect函数。一个优秀框架的发展历程也不过如此,都是在不断的重构升级。


话又说回来,到目前,为什么大部分Vue开发者更偏向于使用watch,而不是watchEffect?,带着这个问题,庖丁解牛式层层分析。


watch、watchEffect底层逻辑


当我们把watch、watchEffect底层逻辑看透,剩下的watchSyncEffect、watchPostEffect也就自然了解。


先回顾下watch、watchEffect内部调用doWatch的参数:


// watch
doWatch(source as any, cb, options)
// demo
watch(
list,
(newValue) => {
count.value = newValue.length;
}
)

// watchEffect
doWatch(effect, null, options)
// demo
watchEffect(() => {
count.value = list.value.length;
})

入参的区别,如下表所示:


APIarg1arg2arg3
watchT | WatchSourcecbWatchOptions
watchEffectWatchEffectnullWatchOptionsBase

根据参数对比,先抛出两个问题:


1. doWatch为什么能自动监听WatchEffect函数内的数据变更,并且能重新执行?


2. 第三个参数WatchOptions、WatchOptionBase有什么区别?


watchOptions、WatchOptionBase的定义如下:


export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
once?: boolean
}

export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}


WatchOptionsBase仅提供了flush,因此watchEffect函数的第三个参数也只有flush一个选项。
flush包含prepostsync三个值,缺省为pre。它明确了监听器的触发时机,pre和post比较明确,对应渲染前、渲染后。


sync官方定义为:在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。简而言之,依赖的多个变量,只要其中一个有更新,监听器就会触发一次。


const list = ref([]);
const page = ref(1);
const message = ref('');

watchEffect(() => {
message.value = `总量${list.value.length}, 当前页:${page.value}`
console.log(message.value);
}, { flush: 'sync' })

例如上述的list、page任意一个有更新,则会输出一次console。sync模式得慎重使用,例如监听的是数组,其中一项有更新都会触发监听器,可能带来不可预知的性能问题。


post也有明确的应用场景,例如:当页面侧边栏显示或隐藏后,需要容器渲染完成后再更新内部的图表等元素。不使用flush选项的解法,一般是监听visible变化并使用setTimeout延迟更新。有了post,一个属性即可搞定。


watch(visible, (value) => {
setTimeout(() => {
// 更新容器内图表
}, 1000);
})

watch(visible, (value) => {
// 更新容器内图表
}, { flush: 'post' })

完成了第二个问题的解答, 要回答第一个问题,需要深入doWatch函数, 在上一篇《写Vue大篇幅的ref、computed,而reactive为何少见?》也有对doWatch做局部介绍,可以作为辅助参考。


doWatch源码


先从doWatch函数签名上,对其有概括性的认识:


function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{
immediate,
deep,
flush,
once,
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ,
): WatchStopHandle

由于我们主要目的是回答问题:doWatch为什么能自动监听WatchEffect函数内的数据变更,并且能重新执行?


因此仅分析source为WatchEffect的情况,此时,cb为null, 第三个参数仅有flush选项。


WatchEffect类型定义如下:


export type WatchEffect = (onCleanup: OnCleanup) => void

onCleanup参数的作用是,在下一次监听器执行前被触发,通常用于状态清理。


doWatch函数实现,最核心的片段是ReactiveEffect的生成:


const effect = new ReactiveEffect(getter, NOOP, scheduler)

为什么ReactiveEffect是其核心?因为它起到了"中介"的作用,在监听器函数内,每一个可监听的变量都对应有依赖项集合deps,当调用这些变量的getter时,ReactiveEffect会把自身注入到依赖集合deps中,这样每当执行变量的setter时,deps集合中的副作用都会触发,而每个副作用effect内部会调用scheduler, scheduler可理解为调度器,负责处理视图更新时机,scheduler内部选择合适的时机触发监听器。


image.png


接下来着重看getter、scheduler定义,当source为WatchEffect类型时,getter定义片段如下:


 // no cb -> simple effect
getter = () => {
if (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}

首先执行cleanup,也就是说如果参数传入有onCleanup回调,那么每次在获取新值前都会触发onCleanup。其次是return语句,调用callWithAsyncErrorHandling函数,从函数可探察之,一方面支持异步,另一方面处理异常错误。


支持异步:也就是我们传入的监听器可以是一个异步函数,那么我们可以在其中执行远程请求的调用,例如官方给的示例, 当id.value值变化,从远端请求数据await response,并赋值给data.value。


watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前未完成的请求
onCleanup(cancel)
data.value = await response
})

上述示例中,如果id.value频繁更新,则会导致触发多次远端请求,要解决该问题,可调用onCleanup(cancel),将cancel传入到doWatch内部,并且每次执行cleanup时被调用。onCleanup定义如下:


let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}

其中,fn即为上述示例中的cancel,这样就建立了cancel和cleanup的关联,因此每次更新前,先调用cancel中断上一次请求。


callWithAsyncErrorHandling函数定义如下:


export function callWithAsyncErrorHandling(fn,instance,type,args?): any {
...
const res = callWithErrorHandling(fn, instance, type, args)
if (res && isPromise(res)) {
res.catch(err => {
handleError(err, instance, type)
})
}
return res
...
}

res为fn函数执行结果,由于支持同步、异步。如果fn为异步函数,那么res为Promise类型,并且对异常做了兜底处理。


当fn函数执行后,内部所有可监听变量的deps都会添加上当前effect,所以只要变量有更新,effect的scheduler就被触发。


watchEffect官方定义有:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。“立即运行一个函数”如何体现?


doWatch函数的最后几行代码如下:


if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
effect.run()
}

如果flush不为post,那么立即执行effect.run(), 而run函数会调用getter,因此会立即运行监听器函数一次;如果flush为post,那么effect将会在vue下一次渲染前第一次执行effect.run()


至此,我们就分析完watchEffect的底层逻辑,总结其特点:立即执行,支持异步,并且会自动监听变量更新。


为什么不能两者取一,而必须共存


再次回顾watch的定义:


export function watch(
source: T | WatchSource
,
cb: any,
options?: WatchOptions
,
)
: WatchStopHandle {

return doWatch(source as any, cb, options)
}

其中WatchOptions包含的选项有:immediate、deep、once、flush。如果是watchEffect,选项仅有flush,并且immediate相当于true,剩下的deep、once不支持配置。


先说watchEffect的缺点



  • 不支持immediate为false,必须是立即执行。例如下面的代码,由于autoplay默认false,初始化时不需要立即执行。如果是watchEffect,则pauseTimer初始化会执行一次,完全没必要。


watch(
() => props.autoplay,
(autoplay) => {
autoplay ? startTimer() : pauseTimer()
}
)


  • 不支持deep为true的场景,只能见监听当前使用的属性。但如果是调用watch(source, cb, { deep: true }), 则会通过traverse(source)将source所有深度属性读取一次,和effect建立关联,达到自动监听所有属性的目的。

  • 异步使用有坑,watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。


再说watchEffect优点


优点也是非常明显,写法非常简洁,无需显式声明监听哪些变量,一个回调函数搞定,并且默认为立即执行,我认为能满足开发中80%的应用场景。另一方面,由于只监听回调中使用的属性,相比于deep为true的一锅端方式,watchEffect则更加直观明了。


总结


watchSyncEffect、watchPostEffect和watchEffect唯一的区别是:flush分别固定为syncpost。所以,watchEffect为watch的衍生,而watchSyncEffect、watchPostEffect为watchEffect的衍生。


对于开发使用上:



  • watchPostEffect、watchSyncEffect仅在极少数的特殊场景下才使用,完全可以用watchEffect(fn, { flush: 'sync' | 'post' })代替,多了反而对入门开发者来说是徒增干扰。

  • 个人认为应优先使用watchEffect函数,毕竟代码写法上更加简洁,属性依赖上也更加明确。满足不了的场景,再考虑使用watch。

作者:前端下饭菜
来源:juejin.cn/post/7401415643981185078
收起阅读 »

vue3为啥推荐使用ref而不是reactive

web
在 Vue 3 中,ref 和 reactive 都是用于声明响应式状态的工具,但它们的使用场景和内部工作机制有所不同。Vue 3 推荐使用 ref 而不是 reactive 的原因主要涉及到以下几个方面: 简单的原始值响应式处理: ref 更适合处理简单...
继续阅读 »

在 Vue 3 中,refreactive 都是用于声明响应式状态的工具,但它们的使用场景和内部工作机制有所不同。Vue 3 推荐使用 ref 而不是 reactive 的原因主要涉及到以下几个方面:



  1. 简单的原始值响应式处理



    • ref 更适合处理简单的原始值(如字符串、数字、布尔值等),而 reactive 更适合处理复杂的对象或数组。



  2. 一致性和解构



    • 使用 ref 时,解构不会丢失响应性,因为 ref 会返回一个包含 .value 属性的对象。而 reactive 对象在解构时会丢失响应性。



  3. 类型推导和代码提示



    • ref 更容易与 TypeScript 配合使用,提供更好的类型推导和代码提示。




示例代码


以下是一个详细的代码示例,演示为什么在某些情况下推荐使用 ref 而不是 reactive


使用 ref 的示例


import { ref } from 'vue';

export default {
setup() {
// 使用 ref 声明响应式状态
const count = ref(0);

function increment() {
count.value++;
}

return {
count,
increment
};
}
};

使用 reactive 的示例


import { reactive } from 'vue';

export default {
setup() {
// 使用 reactive 声明响应式状态
const state = reactive({
count: 0
});

function increment() {
state.count++;
}

return {
state,
increment
};
}
};

解构问题


使用 ref 解构


import { ref } from 'vue';

export default {
setup() {
const count = ref(0);

function increment() {
count.value++;
}

// 解构时不会丢失响应性
const { value: countValue } = count;

return {
countValue,
increment
};
}
};

使用 reactive 解构


import { reactive } from 'vue';

export default {
setup() {
const state = reactive({
count: 0
});

function increment() {
state.count++;
}

// 解构时会丢失响应性
const { count } = state;

return {
count,
increment
};
}
};

代码解释



  1. 使用 ref



    • ref 返回一个包含 .value 属性的对象,因此在模板中使用时需要通过 .value 访问实际值。

    • 解构时,可以直接解构 .value 属性,不会丢失响应性。



  2. 使用 reactive



    • reactive 适用于复杂的对象或数组,返回一个代理对象。

    • 直接解构 reactive 对象的属性会丢失响应性,因为解构后得到的属性是原始值,不再是响应式的。




总结



  • 简单值:对于简单的原始值(如字符串、数字、布尔值等),推荐使用 ref,因为它更简洁,并且在解构时不会丢失响应性。

  • 复杂对象:对于复杂的对象或数组,推荐使用 reactive,因为它可以更方便地处理嵌套属性的响应性。

  • 一致性ref 在解构时不会丢失响应性,而 reactive 在解构时会丢失响应性,这使得 ref 在某些情况下更为可靠。


通过理解 refreactive 的不同使用场景和内部工作机制,可以更好地选择适合的工具来管理 Vue 3 应用中的响应式状态。


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

Node拒绝当咸鱼,Node 22大进步

web
这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。 这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币...
继续阅读 »

这几年,deno和bun风头正盛,大有你方唱罢我登场的态势,deno和bun的每一次更新版本,Node都会被拿来比较,比较结果总是Node落后了。


这种比较是不是非常熟悉,就像卖手机的跟iPhone比,卖汽车的跟特斯拉比,比较的时候有时候还得来个「比一分钱硬币还薄」的套路。


1.png


Node虽然没有落后了,但是确实有点压力了,所以20和22版本都大跨步前进,拒绝当咸鱼了。


因为Node官网对22版本特性的介绍太过简单,所以我决定来一篇详细介绍新特性的文章,让学习Node的朋友们知道,Node现在在第几层。


首先我把新特性分为两类,分别是:开发者可能直接用到的特性、开发者相对无感知的底层更新。本文重点介绍前者,简单介绍后者。先来一个概览:


开发者可能直接用到的特性:



  1. 支持通过 require() 引入ESM

  2. 运行 package.json 中的脚本

  3. 监视模式(--watch)稳定化

  4. 内置 WebSocket 客户端

  5. 增加流的默认高水位线

  6. 文件模式匹配功能


开发者相对无感知的底层更新:



  1. V8 引擎升级至 12.4 版本

  2. Maglev 编译器默认启用

  3. 改进 AbortSignal 的创建性能


接下来开始介绍。


支持通过 require() 导入 ESM


以前,我们认为 CommonJS 与 ESM 是分离的。


例如,在 CommonJS里,我们用并使用 module.exports 导出模块,用 require() 导入模块:


// CommonJS

// math.js
function add(a, b) {
return a + b;
}
module.exports.add = add;

// useMath.js
const math = require('./math');
console.log(math.add(2, 3));

在 ECMAScript Modules (ESM) **** 里,我们使用 export 导出模块,用 import 导入模块:


// ESM

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
import { add } from './math.mjs';
console.log(add(2, 3));

Node 22 支持新的方式——用 require() 导入 ESM:


// Node 22

// math.mjs
export function add(a, b) {
return a + b;
}

// useMath.js
const { add } = require('./mathModule.mjs');
console.log(add(2, 3));

这么设计的原因是为了给大型项目和遗留系统提供一个平滑过渡的方案,因为这类项目难以快速全部迁移到 ESM,通过允许 require() 导入 ESM,开发者就可以逐个模块迁移,而不是一次性对整个项目进行修改。


目前这种写法还是实验性功能,所以使用是有“门槛”的:



  • 启动命令需要添加 -experimental-require-module 参数,如:node --experimental-require-module app.js

  • 模块标记:确保 ESM 模块通过 package.json 中的 "type": "module" 或文件扩展名是 .mjs

  • 完全同步:只有完全同步的ESM才能被 require() 导入,任何含有顶级 await 的ESM都不能使用这种方式加载。


运行package.json中的脚本


假设我们的 package.json 里有一个脚本:


"scripts": {
"test": "jest"
}

在此之前,我们必须依赖 npm 或者 yanr 这样的包管理器来执行命令,比如:npm run test


Node 22 添加了一个新命令行标志 --run,允许直接从命令行执行 package.json 中定义的脚本,可以直接使用 node --run test 这样的命令来运行脚本。


刚开始我还疑惑这是不是脱裤子放屁的行为,因为有 node 的地方一般都有 npm,我要这 node —run 有何用?


后来思考了一下,主要原因应该还是统一运行环境和提升性能。不同的包管理器在处理脚本时可能会有微小的差异,Node 提供一个标准化的方式执行脚本,有助于统一这些行为;而且直接使用 node 执行脚本要比通过 npm 执行脚本更快,因为绕过了 npm 这个中间层。


监视模式(--watch)稳定化


在 19 版本里,Node 引入了 —watch 指令,用于监视文件系统的变动,并自动重启。22 版本开始,这个指令成为稳定功能了。


要启用监视模式,只需要在启动 Node 应用时加上 --watch ****参数。例如:


node --watch app.js

正在用 nodemon 做自动重启的朋友们可以正式转战 --watch 了~


内置 WebSocket 客户端


以前,要用 Node 开发一个 socket 服务,必须使用 ws、socket.io 这样的第三方库来实现。第三方库虽然稳如老狗帮助开发者许多年,但是终究是有点不方便。


Node 22 正式内置了 WebSocket,并且属于稳定功能,不再需要 -experimental-websocket 来启用了。


除此之外,WebScoket 的实现还遵循了浏览器中 WebSocket API 的标准,这意味着在 Node 中使用 WebSocket 的方式将与在 JavaScript 中使用 WebSocket 的方式非常相似,有助于减少学习成本并提高代码的一致性。


用法示例:


const socket = new WebSocket("ws://localhost:8080");

socket.addEventListener("open", (event) => {
socket.send("Hello Server!");
});

增加流(streams)的默认高水位线(High Water Mark)


streams 在 Node 中有举足轻重的作用,读写数据都得要 streams 来完成。而 streams 可以设置 highWaterMark 参数,用于表示缓冲区的大小。highWaterMark 越大,缓冲区越大,占用内存越多,I/O 操作就减少,highWaterMark 越小,其他信息也对应相反。


用法如下:


const fs = require('fs');

const readStream = fs.createReadStream('example-large-file.txt', {
highWaterMark: 1024 * 1024 // 设置高水位线为1MB
});

readStream.on('data', (chunk) => {
console.log(`Received chunk of size: ${chunk.length}`);
});

readStream.on('end', () => {
console.log('End of file has been reached.');
});

虽然 highWaterMark 是可配置的,但通常情况下,我们是使用默认值。在以前的版本里,highWaterMark 的默认值是 16k,Node 22 版本开始,默认值被提升到 64k 了。


文件模式匹配——glob 和 globSync


Node 22 版本在 fs 模块中新增了 globglobSync 函数,它们用于根据指定模式匹配文件路径。


文件模式匹配允许开发者定义一个匹配模式,以找出符合特定规则的文件路径集合。模式定义通常包括通配符,如 *(匹配任何字符)和 ?(匹配单个字符),以及其他特定的模式字符。


glob 函数(异步)


glob 函数是一个异步的函数,它不会阻塞 Node.js 的事件循环。这意味着它在搜索文件时不会停止其他代码的执行。glob 函数的基本用法如下:


const { glob } = require('fs');

glob('**/*.js', (err, files) => {
if (err) {
throw err;
}
console.log(files); // 输出所有匹配的.js文件路径
});

在这个示例中,glob 函数用来查找所有子目录中以 .js 结尾的文件。它接受两个参数:



  • 第一个参数是一个字符串,表示文件匹配模式。

  • 第二个参数是一个回调函数,当文件搜索完成后,这个函数会被调用。如果搜索成功,err 将为 null,而 files 将包含一个包含所有匹配文件路径的数组。


globSync 函数(同步)


globSyncglob 的同步版本,它会阻塞事件循环,直到所有匹配的文件都被找到。这使得代码更简单,但在处理大量文件或在需要高响应性的应用中可能会导致性能问题。其基本用法如下:


const { globSync } = require('fs');

const files = globSync('**/*.js');
console.log(files); // 同样输出所有匹配的.js文件路径

这个函数直接返回匹配的文件数组,适用于脚本和简单的应用,其中执行速度不是主要关注点。


使用场景


这两个函数适用于:



  • 自动化构建过程,如自动寻找和处理项目中的 JavaScript 文件。

  • 开发工具和脚本,需要对项目目录中的文件进行批量操作。

  • 任何需要从大量文件中快速筛选出符合特定模式的文件集的应用。


V8 引擎升级至 12.4 版本


从这一节开始,我们了解一下开发者相对无感知的底层更新,第一个就是 V8 引擎升级到 12.4 版本了,有了以下特性升级:



  • WebAssembly 垃圾回收:这一特性将改善 WebAssembly 在内存管理方面的能力。

  • Array.fromAsync:这个新方法允许从异步迭代器创建数组。

  • Set 方法和迭代器帮助程序:提供了更多内建的Set操作和迭代器操作的方法,增强了数据结构的操作性和灵活性。


Maglev 编译器默认启用


Maglev 是 V8 的新编译器,现在在支持的架构上默认启用。它主要针对短生命周期的命令行程序(CLI程序)性能进行优化,通过改进JIT(即时编译)的效率来提升性能。这对开发者编写的工具和脚本将带来明显的速度提升。


改进AbortSignal的创建性能


在这次更新中,Node 提高了 AbortSignal 实例的创建效率。AbortSignal 是用于中断正在进行的操作(如网络请求或任何长时间运行的异步任务)的一种机制。通过提升这一过程的效率,可以加快任何依赖这一功能的应用,如使用 fetch 进行HTTP请求或在测试运行器中处理中断的场景。


AbortSignal 的工作方式是通过 AbortController 实例来管理。AbortController 提供一个 signal 属性和一个 abort() 方法。signal 属性返回一个 AbortSignal 对象,可以传递给任何接受 AbortSignal 的API(如fetch)来监听取消事件。当调用abort()方法时,与该控制器关联的所有操作将被取消。


const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', err);
}
});

// 取消请求
controller.abort();

总结


最后,我只替 Node 说一句:Node 没有这么容易被 deno 和 bun 打败~


3.jpeg


作者:程普
来源:juejin.cn/post/7366185272768036883
收起阅读 »

用了这么久Vue,你用过这几个内置指令提升性能吗?

web
前言 Vue的内置指令估计大家都用过不少,例如v-for、v-if之类的就是最常用的内置指令,但今天给大家介绍几个平时用的比较少的内置指令。毕竟这几个Vue内置指令可用可不用,不用的时候系统正常跑,但在对的地方用了却能提升系统性能,下面将结合示例进行详细说明。...
继续阅读 »

前言


Vue的内置指令估计大家都用过不少,例如v-forv-if之类的就是最常用的内置指令,但今天给大家介绍几个平时用的比较少的内置指令。毕竟这几个Vue内置指令可用可不用,不用的时候系统正常跑,但在对的地方用了却能提升系统性能,下面将结合示例进行详细说明。


一、v-once


作用:在标签上使用v-once能使元素或者表达式只渲染一次。首次渲染之后,后面数据再发生变化时使用了v-once的地方都不会更新,因此用在数据不需要变化的地方就能进行性能优化。


v-once指令实现原理: Vue组件初始化时会标记上v-once,首次渲染会正常执行,后续再次渲染时如果看到有v-once标记则跳过二次渲染。


示例代码: 直接作用在标签上,可以是普通标签也可以是图片标签,当2S后数据变化时标签上的值不会重新渲染更新。


<template>
<div>
<span v-once>{{ message }}</span>
<img v-once :src="imageUrl"></img>
</div>
</template>

<script setup>
import { ref } from 'vue';

let message = ref('Vue指令!');
let imageSrc = ref('/path/my/image.jpg');

setTimeout(() => {
message.value = '修改内容!';
imageUrl.value = '/new/path/my/images.jpg';
}, 2000);

</script>

注意: 作用v-once会使属性失去响应式,要确保这个地方不需要响应式更新才能使用,否则会导致数据和页面视图对不上。


二、v-pre


作用: 在标签上使用v-pre后,Vue编译器会自动跳过这个元素的编译。使用此内置指令后会被视为静态内容。


v-pre指令实现原理: Vue初次编译时如果看到有v-pre标记,那么跳过这部分的编译,直接当成原始的HTML插入到DOM中。


示例代码: 常规文本会正常编译成您好!,但使用了v-pre后会跳过编译原样输出{{ message }}


<template>
<div>
<h2>常规: {{ message }}</h2>
<h2 v-pre>使用v-pre后: {{ message }}</h2>
</div>
</template>

<script setup>
import { ref } from 'vue';

let message = ref('您好!');
</script>

image.png


注意: 要区分v-prev-once的区别,v-once用于只渲染一次,而v-pre是直接跳过编译。



这个指令可能很多人没想到应用场景有那些,其实最常见的用途就是要在页面上显示Vue代码,如果不用v-pre就会被编译。如下所示使用v-pre场景效果。



<template>
<div>
<pre v-pre>
&lt;template&gt;
&lt;p&gt;{{ message }}&lt;/p&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { ref } from 'vue';

const message = ref('Hello Vue!');
&lt;/script&gt;
</pre>
</div>
</template>

<script setup>
import { ref } from 'vue';

let message = ref('您好!');
</script>

页面上展示: 代码原始显示不会被编译。


image.png


三、v-memo(支持3.2+版本)


作用: 主要用于优化组件的渲染方面性能,能控制达到某个条件才重新当堂组件,否则不重新渲染。v-memo 会缓存 DOM,只有当指定的数据发生变化时才会重新渲染,从而减少渲染次数提升性能。


v-memo 指令实现原理: Vue初始化组件时会识别是否有v-memo标记,如果有就把这部分vnode缓存起来,当数据变化时会对比依赖是否变化,变化再重新渲染。


示例代码:v-memo 绑定了arr,那么当arr的值变化才会重新渲染,否则不会重新渲染。


<template>
<div>
<ul v-memo="arr">
<li v-for="(item, index) in arr" :key="index">
{{ item.text }}
</li>
</ul>
</div>
</template>

<script setup>
import { ref } from 'vue';

let arr = ref([
{ text: '内容1' },
{ text: '内容2' },
{ text: '内容3' }
]);

setInterval(() => {
arr.value[1].text = '修改2';
}, 2000);
</script>

注意:v-memo来指定触发渲染的条件,但只建议在长列表或者说复杂的渲染结构才使用。


小结


总结了几个比较冷门的Vue内置指令,平时用的不多,但用对了地方却能明显提升性能。如果那里写的不对或者有好建议欢迎大佬指出啊。


作者:天天鸭
来源:juejin.cn/post/7407340295115767808
收起阅读 »

火山引擎携零售巨头成立大模型联盟,抖音电商及生活服务加盟助阵

2024年虽被外界普遍认为是“大模型应用落地元年”,但至今仍有很多声音,质疑大模型在具体行业的应用落地效果。大模型究竟如何更好发挥自身作用,助力企业实现AI转型,促进创新增长,也一直是媒体和行业的热议话题。8月21日,在2024火山引擎 AI 创新巡展(上海站...
继续阅读 »

2024年虽被外界普遍认为是“大模型应用落地元年”,但至今仍有很多声音,质疑大模型在具体行业的应用落地效果。大模型究竟如何更好发挥自身作用,助力企业实现AI转型,促进创新增长,也一直是媒体和行业的热议话题。

8月21日,在2024火山引擎 AI 创新巡展(上海站)期间,火山引擎发布了豆包大模型的一系列产品升级,并携手多点DMALL等零售巨头,成立了零售大模型生态联盟。联盟首批成员包括物美集团、抖音电商、抖音生活服务、百胜、麦当劳等。

火山引擎总裁谭待表示,企业要真正做好AI转型,是一件非常有挑战的事。希望通过大模型生态联盟,与更多企业伙伴一起探索,共同促进零售企业的AI转型,让大模型更好地为企业发展服务。

火山引擎智能算法负责人、火山方舟负责人吴迪则以《豆包大模型,助力企业AI转型》为题,现场分享了大模型如何在具体行业应用落地的细节。

吴迪表示,自今年5月15日豆包大模型发布以来,60天时间里云计算客户总调用量增长了三倍左右。随着处理的问题越来越多,火山引擎对市场挑战的理解也越来越深刻,并将AI大模型落地具体行业所面临的问题总结为“三大挑战”:

一是基础模型是否足够“聪明”;二是价格和成本;三是落地过程中所面临新工作范式和企业原有IT系统之间的改造,以及兼容成本等具体问题。

而对于这些问题,豆包大模型则以更强模型、更低价格、更易落地的解决方案加以应对。

吴迪表示,目前在字节跳动企业内部,包括抖音、剪映、头条、豆包APP、飞书、懂车帝、猫箱、河马、番茄等约50余个业务线在使用豆包大模型,在外部每天则有30余个行业客户在使用。

而在价格方面,豆包通用模型pro的推理输入为0.8厘/千tokens,输出为2厘/千tokens。之所以能够把价格做到这个水平,背靠的则是强劲的系统承载力、充沛算力,以及积累多年的推理算法、系统优化及系统调度能力。

首先,火山引擎拥有海量GPU资源,目前在豆包大模型和火山方舟平台,已投入多达数万张不同型号GPU算力。

同时,造成算力枯竭的一个重要原因,是很多企业做不到灵活调配GPU算力,从而造成2/3甚至更多时间里,算力出现闲置或低效率表现。而火山引擎通过极致调度,避免浪费,则可以进一步将成本优势控制到同行的1/3甚至1/10。

第三则是极致弹性。火山引擎可以做到分钟级完成数千卡伸缩,有效支持突发流量和业务高峰。而火山引擎推出的多种批量推理模式,则提供了业界领先的TPU初始额度。

除此之外,火山引擎还配备了优秀精干的算法工程师团队,支撑企业客户需求以及疑难问题的解决,用抖音内容、抖音搜索、知识库等插件,配合Coze扣子平台,打造更易使用的开发者环境,并利用安全沙箱,使客户可以更加放心地使用大模型。

在安全方面,首先通过TLS和安全沙箱实现双向身份认证和加密,建立互信连接,保证用户访问的安全。

其次则通过全链路数据加密,确保用户的使用安全。

第三则是通过安全沙箱技术,杜绝内外风险入侵和数据泄露的风险。

第四是“信息无痕”,做到“全链路”、“全内存”、“零日志”,在任务结束时安全沙箱自动销毁,用户画像全程无痕。

第五是操作可审计,对沙箱系统及用户流量的访问均有日志记录,客户也可以自行通过token API的方式对日志进行审计。

目前,火山引擎新升级的内容和联网插件提供包括金融、旅游、影视、生活服务等27个行业垂直内容的数据源,并新增抖音百科类型数据。

吴迪表示,升级后的知识库,在文档解析和检索能力方面都有了大幅提高,可以应对包括图片、多列表格、PPT、Markdown等更丰富的文档类型,并更具性价比,支持向量库的语义检索以及类似传统搜索引擎的准确检索等。

在活动现场,火山引擎还公开发布了全新的Coze扣子专业版,用于企业开发智能体。吴迪表示,火山引擎将在Coze扣子专业版上提供企业级稳定性保障,以及一键式接入火山方舟模型的能力、更高的tokens配额。

作为零售行业大模型生态联盟的发起者之一,多点DMALL创始人、物美集团创始人张文中博士也来到现场,并从具体的操作层面,与在场与会者分享了大模型如何在零售行业中具体落地。

张文中提出,目前AI大模型已经可以广泛应用于包括超市智能防损、智能补货、智能客服、以及折扣出清等多个方面。由于豆包大模型tokens定价极低,很多以往很难解决的难题,现在都有了很高性价比的解决方案。

张文中最后表示,AI时代,零售企业再也不能“单打独斗”。大模型时代,行业更需要携手共进,希望与火山引擎一起,向零售界发出呼吁,通过全面拥抱AI,一起努力共创智慧零售的新未来。(作者:李双)

收起阅读 »

一文揭秘:火山引擎云基础设施如何支撑大模型应用落地

2024年被普遍认为是“大模型落地应用元年”,而要让大模型真正落地应用到企业的生产环节中,推理能力至关重要。所谓“推理能力”,即大模型利用输入的新数据,一次性获得正确结论的过程。除模型本身的设计外,还需要强大的硬件作为基础。在8月21日举办的2024火山引擎A...
继续阅读 »

2024年被普遍认为是“大模型落地应用元年”,而要让大模型真正落地应用到企业的生产环节中,推理能力至关重要。所谓“推理能力”,即大模型利用输入的新数据,一次性获得正确结论的过程。除模型本身的设计外,还需要强大的硬件作为基础。

在8月21日举办的2024火山引擎AI创新巡展上海站活动上,火山引擎云基础产品负责人罗浩发表演讲,介绍了火山引擎AI全栈云在算力升级、资源管理、性能和稳定性等方面做出的努力,尤其是分享了针对大模型推理问题的解决方案。

罗浩表示,在弹性方面,与传统的云原生任务相比,推理任务,以及面向AI native应用,由于其所对应的底层资源池更加复杂,因此面临的弹性问题也更加复杂。传统的在线任务弹性,主要存在于CPU、内存、存储等方面,而AI native应用的弹性问题,则涉及模型弹性、GPU弹性、缓存弹性,以及RAG、KV Cache等机制的弹性。

同时,由于底层支撑算力和包括数据库系统在内的存储都发生了相应的变化,也导致对应的观测体系和监控体系出现不同的变化,带来新的挑战。

在具体应对上,火山引擎首先在资源方面,面向不同的需求,提供了更多类型的多达几百种计算实例,包括推理、训练以及不同规格推理和训练的实例类型,同时涵盖CPU和GPU。

在选择实例时,火山引擎应用了自研的智能选型产品,当面训练场景或推理场景时,在给定推理引擎,以及该推理引擎所对应的模型时,都会给出更加适配的GPU或CPU实例。该工具也会自动探索模型参数,包括推理引擎性能等,从而找到最佳匹配实例。

最后,结合整体资源调度体系,可以通过容器、虚拟机、Service等方式,满足对资源的需求。

而在数据领域,目前在训练场景,最主要会通过TOS、CFS、VPFS支持大模型的训练和分发,可以看到所有的存储、数据库等都在逐渐转向高维化,提供了对应的存储和检索能力。

在数据安全方向,当前的存储数据,已经有了更多内容属性,企业和用户对于数据存储的安全性也更加在意。对此,火山引擎在基础架构层面提供全面的路审计能力,可通过专区形式,支持从物理机到交换机,再到专属云以及所有组件的对应审计能力。

对此,罗浩以火山引擎与游戏公司沐瞳的具体合作为例给予了解释。在对移动端游戏里出现的语言、行为进行审计和审核时,大量用到各种各样的云基础,以及包括大模型在内的多种AI产品,而火山引擎做到了让所有的产品使用都在同一朵云上,使其在整体调用过程当中,不出现额外的流量成本,也使整体调用延时达到最优化。

另外,在火山引擎与客户“美图”合作的案例中,在面对新年、元旦、情人节等流量高峰时,美图通过火山引擎弹性的资源池,同时利用火山潮汐的算力,使得应用整体使用GPU和CPU等云资源时,成本达到最优化。

罗浩最后表示,未来火山引擎AI全栈云在算力、资源管理、性能及稳定性等方面还将继续探索,为AI应用在各行业的落地,奠定更加坚实的基础,为推动各行业智能化和数字化转型的全新助力。(作者:李双)

收起阅读 »

逻辑删除用户账号合规吗?

事情的起因是这样: 有一个小伙伴说自己用某电动车 App,由于种种原因后来注销了账号,注销完成之后,该 App 提示 “您的账户已删除。与您的账户关联的所有个人数据也已永久删除”。当时当他重新打开 App 之后,发现账户名变为了 unknown,邮箱和电话变...
继续阅读 »

事情的起因是这样:



有一个小伙伴说自己用某电动车 App,由于种种原因后来注销了账号,注销完成之后,该 App 提示 “您的账户已删除。与您的账户关联的所有个人数据也已永久删除”。当时当他重新打开 App 之后,发现账户名变为了 unknown,邮箱和电话变成了账号的 uid@delete.account.品牌.com。更炸裂的是,这个 App 此时还是可以正常控制电动车,可以查看定位、电量、客服记录、维修记录等等信息。



小伙伴觉得心塞,感觉被这个 App 耍了,明明就没有删除个人信息,却信誓旦旦的说数据已经永久删除了。


其实咱们做后端服务的小伙伴都知道,基本上都是逻辑删除,很少很少有物理删除。


大部分公司可能都是把账号状态标记为删除,然后踢用户下线;有点良心的公司除了将账号状态标记为删除,还会将用户信息脱敏;神操作公司则把账号状态标记为删除,但是忘记踢用户下线。


于是就出现了咱们小伙伴遇到的场景了。


逻辑删除这事,其实不用看代码,就从商业角度稍微分析就知道不可能是物理删除。比如国内很多 App 对新用户都会送各种优惠券、代金券等等,如果物理删除岂不是意味着可以反复薅平台羊毛。


当然这个是各个厂的实际做法,那么这块有没有相关规定呢?松哥专门去查看了一下相关资料。



根据 GB/T 35273 中的解释,我挑两段给大家看下。


首先文档中解释了什么是删除:



去除用户个人信息的行为,使其保持不可被检索、访问的状态。


理论上来说,逻辑删除也能够实现用户信息不可被检索和访问。


再来看关于用户注销账户的规范:



删除个人信息或者匿名化处理


从这两处解释大家可以看到,平台逻辑删除用户信息从合规上来说没有问题。


甚至可能物理删除了反而有问题。


比如张三注册了一个聊天软件实施诈骗行为,骗到钱了光速注销账号,平台也把张三的信息删除了,最后取证找不到人,在目前这种情况下,平台要不要背锅?如果平台要背锅,那你说平台会不会就真把张三信息给清空了?


对于这个小伙伴的遭遇,其实算是一个系统 BUG,账户注销,应该强制退出登录,退出之后,再想登录肯定就登录不上去了,所以也看不到自己之前的用户信息了。


小伙伴们说说,你们的系统是怎么处理这种场景的呢?


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

【在线聊天室😻】前端进阶全栈开发🔥

web
项目效果登录注册身份认证、私聊、聊天室 项目前端React18仓库:github.com/mcmcCat/mmc…项目后端Nestjs仓库:github.com/mcmcCat/mmc…语雀上的笔记:http://www.yuque.com/maim...
继续阅读 »


项目效果

登录注册身份认证、私聊、聊天室

即时聊天演示.gif 项目前端React18仓库:github.com/mcmcCat/mmc…
项目后端Nestjs仓库:github.com/mcmcCat/mmc…
语雀上的笔记:http://www.yuque.com/maimaicat/t…

技术栈:
Nestjs企业级Node服务端框架+TypeOrm(Mysql)+JWT+Socket.IO🎉
React18/服务端渲染Nextjs+Redux-toolkit+styled-components🎉

前言

Nestjs 是一个用于构建高效可扩展的一个基于Node js 服务端 应用程序开发框架。本文不过多赘述,网上的教程有很多。
(注意:对于聊天中user模块和message模块的接口可参考仓库代码,在这里只分析登录注册的身份认证)

下面可以放张图稍微感受一下,用nest写接口很方便。 @Post('auth/register')使用装饰器的方式,当你请求这个接口时,会自动调用下方函数AuthRegister。另外还可以加一大堆装饰器,用于生成swagger接口文档,做本地验证、jwt验证等等。

import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
Post,
Req,
Res,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AppService } from './app.service';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth/auth.service';
import { CreateUserDto } from './user/dto/create-user.dto';
import { LoginDTO } from './auth/dto/login.dto';
import { AuthGuard } from '@nestjs/passport';
// @Controller装饰器来定义控制器,如每一个要成为控制器的类,都需要借助@Controller装饰器的装饰
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@ApiTags('JWT注册')
@Post('auth/register')
async AuthRegister(@Body() body: CreateUserDto) {
return await this.appService.authRegister(body);
}

@UseInterceptors(ClassSerializerInterceptor) //返回的数据中去除实体中被@Exclude()的字段
@UseGuards(AuthGuard('local')) //使用本地策略验证用户名和密码的正确性
@ApiTags('JWT登录')
@Post('auth/login')
async AuthLogin(@Body() body: LoginDTO, @Req() req) {
// 通过了本地策略证明身份验证通过
return await this.appService.authLogin(req.user);
}
}

Nestjs中如何进行身份认证?

密码加密 和 生成token

我们可以跟着代码仓库,带有详细的注释,一步步地走
app.service.ts 负责定义注册authRegister和登录authLogin

  • 在注册时,拿到用户输入的密码,使用**bcryptjs.hash()**将其转换为 hash加密字符串,并存入数据库
    image.png
  • 在(身份认证的)登录时,先进行校验用户登录信息是否正确在这里我们使用的是[passport-local](http://nestjs.inode.club/recipes/passport#%E5%AE%9E%E7%8E%B0-passport-%E6%9C%AC%E5%9C%B0%E7%AD%96%E7%95%A5)本地策略来验证,@UseGuards(AuthGuard('local'))这个装饰器会在此处的post请求@Post('auth/login')后进行拦截,去local.strategy.ts中进行validate检索出该用户的信息,然后我们使用**bcryptjs.compareSync()**将 **用户输入的密码 **与数据库中用 **hash加密过的密码 **进行解析对比,若登录信息正确则接着调用AuthLogin,进而调用(认证成功的)登录接口authService.login(),即向客户端发送登录成功信息并且是携带有**token**的,
async login(user: any) {
// 准备jwt需要的负载
const payload = { username: user.username, sub: user.id };
return {
code: '200',
// 配合存储着用户信息的负载 payload 来生成一个包含签名的JWT令牌(access_token)。。
access_token: this.jwtService.sign(payload),
msg: '登录成功',
};
}

校验token合法性

那么这个token我们在哪里去拦截它进行校验呢?

那就要提到我们 nest 的guard(守卫)这个概念。其实就好比我们在vue项目中,封装路由前置守卫拦截路由跳转,去获取存储在localStoragetoken一样。

在 nest 守卫中我们可以去获取到请求体req,从而获取到请求头中的Authorization字段,查看是否携带token,然后去校验token合法性,authService.verifyToken()中调用jwtService.verify()进行token的令牌格式校验、签名验证、过期时间校验,确保令牌的完整性、真实性和有效性

import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from 'src/app.module';
import { AuthService } from './auth.service';
import { UserService } from 'src/user/user.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor() {
super();

}
async canActivate(context: ExecutionContext): Promise<any> {
const req = context.switchToHttp().getRequest();

// 如果是请求路由是白名单中的,则直接放行
if (this.hasUrl(this.whiteList, req.url)) return true;
try {
const accessToken = req.get('Authorization');

if (!accessToken) throw new UnauthorizedException('请先登录');

const app = await NestFactory.create<NestExpressApplication>(AppModule);
const authService = app.get(AuthService);
const userService = app.get(UserService);

const tokenUserInfo = await authService.verifyToken(accessToken);
const resData = await userService.findOne(tokenUserInfo.username);

if (resData[0].id) return true;
} catch (e) {
console.log('1h 的 token 过期啦!请重新登录');
return false;
}
}
// 白名单数组
private whiteList: string[] = ['/auth/register','/auth/login'];

// 验证该次请求是否为白名单内的路由
private hasUrl(whiteList: string[], url: string): boolean {
let flag = false;
if (whiteList.indexOf(url) !== -1) {
flag = true;
}
return flag;
}
}


guard中,当我们return true时,好比路由前置守卫的next(),就是认证通过了放行的意思

当然,别忘了注册守卫,我们这里可以采用全局守卫的形式注册,在main.tsapp.useGlobalGuards(new JwtAuthGuard());

Socket.IO如何实现即时聊天?

Nest中WebSocket网关的作用

使用 @WebSocketGateway 装饰器配置 WebSocket 网关在 Nest.js 应用中具有以下作用

  1. 提供 WebSocket 的入口点:WebSocket 网关允许客户端通过 WebSocket 协议与后端建立实时的双向通信。通过配置网关,你可以定义用于处理 WebSocket 连接、消息传递和事件的逻辑。
  2. 处理跨域请求:在 WebSocket 中,默认存在跨域限制,即只能与同源的 WebSocket 服务进行通信。通过设置 origin 选项,WebSocket 网关可以解决跨域请求问题,允许来自指定源的请求进行跨域访问。

关于Socket.IO是怎么通讯的可以看看官网给出的图
image.png
socketIO是通过事件监听的形式,我们可以很清晰的区分出消息的类型,方便对不同类型的消息进行处理,客户端和服务端双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以把各种消息进行有序管理了
image.png
下面是一个简单的通讯事件示例:

import {
WebSocketGateway,
SubscribeMessage,
WebSocketServer,
MessageBody,
ConnectedSocket,
} from '@nestjs/websockets';
import { SocketService } from './socket.service';
import { CreateSocketDto } from './dto/create-socket.dto';
import { UpdateSocketDto } from './dto/update-socket.dto';
import { Socket } from 'socket.io';

const roomList = {};
let roomId = null;
let user = '';
@WebSocketGateway(3001, {
allowEIO3: true, // 开启后要求前后端使用的 Socket.io 版本要保持一致
//后端解决跨域
cors: {
// 允许具体源的请求进行跨域访问
origin: 'http://localhost:8080', //这里不要写*,要写 true 或者具体的前端请求时所在的域,否则会出现跨域问题
// 允许在跨域请求中发送凭据
credentials: true,
},
})
export class SocketGateway {
constructor(private readonly socketService: SocketService) {}

@SubscribeMessage('ToClient')
ToClient(@MessageBody() data: any) {
// 转发信息
const forwardMsg: string = '服务端=>客户端';
return {
//通过return返回客户端转发事件
event: 'forward',
data: forwardMsg, //data后面跟携带数据
};
}

//接收并处理来自客户端的消息
@SubscribeMessage('toServer')
handleServerMessage(client: Socket, data: string) {
console.log(data + ' (让我服务端来进行一下处理)');
client.emit('ToClient', data + '(处理完成给客户端)');
}
}

私聊模块中的 socket 事件

通过使用client.broadcast.emit('showMessage')  client.emit('showMessage'),你可以实现多人实时聊天的功能。
当一个客户端发送一条消息时,通过 client.broadcast.emit('showMessage') 将该消息广播给其他客户端,让其他客户端可以接收到这条消息并进行相应的处理,从而实现多人实时聊天的效果。
同时,使用 client.emit('showMessage') 可以将消息发送给当前连接的客户端,这样当前客户端也会收到自己发送的消息,以便在界面上显示自己发送的内容。

@SubscribeMessage('sendMessage')
sendMessage(client: Socket) {
// 将该消息广播给其他客户端
client.broadcast.emit('showMessage');
// 将消息发送给当前连接的客户端
client.emit('showMessage');
return;
}

前端中会在UserList.tsx监听该事件showMessage,并触发更新信息逻辑

useEffect(() => {
socket.on('showMessage', getCurentMessages)
return () => {
socket.off('showMessage')
}
})

房间模块中的 socket 事件

@SubscribeMessage('sendRoomMessage')
sendRoomMessage(client: Socket, data) {
console.log('服务端接收到了');

// // 将消息发送给指定房间内的所有客户端
this.socketIO.to(roomId).emit('sendRoomMessage', data);
return;
}

在需要发送消息给指定房间时,即我们需要在全局中找到指定房间,所以我们需要整个 WebSocket 服务器的实例

@WebSocketServer()
socketIO: Socket; //它表示整个 WebSocket 服务器的实例。它可以用于执行全局操作,如向所有连接的客户端广播消息或将客户端连接到特定的房间。

加入和退出房间的 socket API

// 加入房间
client.join(roomId);
// 退出房间
client.leave(roomId);

注意这个socket API的作用只是会被用于this.socketIO.to(roomId).emit('sendRoomMessage', data)时的指定to房间去发送信息,而对于房间人员的变动情况得自己准备一个对象来记录,如roomList

踩坑

  1. socket实例的创建写在了函数组件内,useState中的变量的频繁改变,导致的组件不断重新渲染,socket实例也被不断创建,形成过多的连接,让websocket服务崩溃!!!

解决:
把socket实例的创建拿出来放在单独的文件中,这样在各个函数组件中若使用的话只用引用这一共同的socket实例,仅与websocket服务器形成一个连接

  1. socket事件的监听没有及时的停止,导致对同一事件的监听不断叠加(如sys事件),当触发一次这一事件时,会同时触发到之前叠加的所有监听函数!!!
    项目中的效果就是不断重新进入房间时,提示信息的渲染次数会递增的增加,而不是只提示一次

解决:
在离开房间后要socket.off('sys');要停止事件监听,另外最好是在组件销毁时停止所有事件的监听(此处为Next/React18,即项目前端的代码

/* client */
useEffect(() => {
console.log('chat组件挂载');
// 连接自动触发
socket.on('connect', () => {
socket.emit('connection');
// 其他客户端事件和逻辑
});
return () => {
console.log('chat组件卸载');
socket.off();// 停止所有事件的监听 !!!
};
}, []);

/* server */
@SubscribeMessage('connection')
connection(client: Socket, data) {
console.log('有一个客户端连接成功', client.id);
// 断连自动触发
client.on('disconnect', () => {
console.log('有一个客户端断开连接', client.id);
// 处理断开连接的额外逻辑
});
return;
}

作者:麦麦猫
来源:juejin.cn/post/7295681529606832138

收起阅读 »

颠覆霍金猜想!数学家证明极端黑洞可能存在

明敏 发自 凹非寺 量子位 | 公众号 QbitAI 霍金50年前提出的猜想被颠覆了! 数学家们最新证明,极端黑洞可能存在。 这与霍金等人在1973年提出的黑洞热力学第三定律**相悖。 极端黑洞是一种非常特殊的情况,指黑洞表面或事件视界的引力为零,它的表面...
继续阅读 »

明敏 发自 凹非寺


量子位 | 公众号 QbitAI



霍金50年前提出的猜想被颠覆了!


数学家们最新证明,极端黑洞可能存在


图片


这与霍金等人在1973年提出的黑洞热力学第三定律**相悖。


极端黑洞是一种非常特殊的情况,指黑洞表面或事件视界的引力为零,它的表面不吸引任何东西,但是如果把粒子推出到黑洞中心,还是无法逃逸。


而且由于黑洞的温度与表面重力成正比,表面重力不存在即意味着黑洞没有温度,无法发射热辐射。


这又与霍金辐射理论相违背,该理论提出黑洞不是完全“黑暗”的,而是能以特定方式缓慢向外辐射能量,从而逐渐失去质量并最终可能消失。


但是来自MIT的克里斯托夫·凯勒(Christoph Kehle)和斯坦福大学的瑞安·昂格尔(Ryan Unger)用数学方法证明,这种情况可能存在。


图片


而且它们还证明,极端黑洞存在并不会导致裸奇点存在


诺奖得主彭罗斯**之前提出,自然界不允许裸奇点存在,如果它存在将破坏宇宙因果性,奇点附近的空间区域可能会允许违反因果关系的行为,导致时间和空间在局部变得不再有序。


哥伦比亚大学数学家艾琳娜·乔治(Elena Giorgi)评价:



这是数学回馈物理学一个很棒的例子。



极端黑洞是什么?


自然界中绝大多数黑洞都是旋转的。


当带电荷的物质掉入黑洞后,因为角动量守恒,黑洞自旋转速度会增加,同时黑洞本身也会带上电荷。


理论上,随着黑洞吸入越来越多物质,它的电荷量和转速将会无限大,这样就会出现极端黑洞。


对于极端黑洞,只要再加上任何一点电荷,它的视界就会消失,并留下一个裸奇点。


而且它的表面不再吸引任何东西。


图片


1973年,霍金、约翰·巴丁、布兰登·卡特提出,极端黑洞不可能形成。


这条定律指出黑洞的表面引力不可能在有限时间内降至0,三位科学家认为任何允许黑洞的电荷或自旋达到极限的过程都有可能导致黑洞视界完全消失。


学界普遍认为没有视界的黑洞(即裸奇点)是不可能存在的。


此外,由于黑洞的温度和表面重力呈正比,如果没有表面重力黑洞也不会有温度,这样黑洞就无法发射热辐射。但是霍金提出,向外发出辐射是黑洞的必备属性。


1986年,物理学家沃纳·伊斯雷尔(Werner Israel)曾试图模拟用一个普通黑洞构建极端黑洞,并试着让它自旋更快、带上更多电荷,但最终结论表明,这样做并不能让黑洞的表面重力在有限时间内降低到0。


无心插柳找到证明方法


凯勒和昂格尔本身并没有在研究极端黑洞。


他们是在琢磨带电黑洞如何形成时,意外发现可以构建一个具有极高电荷量的黑洞,这是极端黑洞的一个重要标志。


他们从一个不旋转、没有电荷的黑洞开始,模拟它被放置到标量场中可能发生的情况。


图片


他们利用磁场脉冲冲击黑洞,给它增加电荷。这些脉冲为黑洞提供了电磁能量,也增加了黑洞的质量。


通过发射漫射的低频脉冲,就能让黑洞质量(M)的增速大于电荷(q)的增速。


按照分类,当|q|=M时,代表极端黑洞形成;|q|M时为非极端黑洞。


如果质量增速超过电荷增速,意味着黑洞能从亚极端状态向极端状态转变。


论文不仅提出了一种新的特征粘连方法,而且展示了如何构造黑洞内部的结构、分析了黑洞形成和演化的过程,包括从规则初始数据出发的引力坍缩以及黑洞外部的几何结构等。


不过需要注意的是,尽管利用数学方法证明了极端黑洞理论存在,但是也不能说明极端黑洞一定存在。


理论中的例子具有最大电荷量,但是目前人类还没有观测到明显带有电荷的黑洞。找到一个快速自旋的黑洞更有可能,所以凯勒和昂格尔还想构建一个模型,让黑洞能够在自旋速度上达到极限。


但是构建这样一个模型在数学上的挑战更大。目前他们才刚刚开始着手研究。


一直以来,凯勒和昂格尔都在尝试利用数学方法探索黑洞的秘密。


2023年,凯勒和老师艾琳娜等还通过一项1000页的研究证明,数学意义上,缓慢自旋的黑洞是稳定的。这对于验证广义相对论很重要,因为如果在数学意义上不稳定,那么可能意味着基础理论存在问题。


** **

图片

左为凯勒,右为昂格尔

而今年最新发表的研究,不仅颠覆了霍金提出的猜想,也为广义相对论、量子力学、弦理论等前沿领域研究提供新见解。


参考链接:

http://www.quantamagazine.org/mathematici…


作者:量子位
来源:juejin.cn/post/7407259722430119947
收起阅读 »

前端时间分片渲染

web
在经典的面试题中:”如果后端返回了十万条数据要你插入到页面中,你会怎么处理?” 除了像 useVirtualList 这样的虚拟列表来处理外,我们还可以通过 时间分片 来处理 通过 setTimeout 直接上一个例子: <!-- * @Author:...
继续阅读 »

在经典的面试题中:”如果后端返回了十万条数据要你插入到页面中,你会怎么处理?


除了像 useVirtualList 这样的虚拟列表来处理外,我们还可以通过 时间分片 来处理


通过 setTimeout


直接上一个例子:


<!--
* @Author: Jolyne
* @Date: 2023-09-22 15:45:45
* @LastEditTime: 2023-09-22 15:47:24
* @LastEditors: Jolyne
* @Description:
-->

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>十万数据渲染</title>
</head>

<body>
<ul id="list-container"></ul>

<script>
const oListContainer = document.getElementById('list-container')

const fetchData = () => {
return new Promise(resolve => {
const response = {
code: 0,
msg: 'success',
data: [],
}

for (let i = 0; i < 100000; i++) {
response.data.push(`content-${i + 1}`)
}

setTimeout(() => {
resolve(response)
}, 100)
})
}

// 模拟请求后端接口返回十万条数据
// 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
const renderData = (data, total, page, pageCount) => {
// base case -- total 为 0 时没有数据要渲染 不再递归调用
if (total <= 0) return

// total 比 pageCount 少时只渲染 total 条数据
pageCount = Math.min(pageCount, total)

setTimeout(() => {
const startIdx = page * pageCount
const endIdx = startIdx + pageCount
const dataList = data.slice(startIdx, endIdx)

// 将 pageCount 条数据插入到容器中
for (let i = 0; i < pageCount; i++) {
const oItem = document.createElement('li')
oItem.innerText = dataList[i]
oListContainer.appendChild(oItem)
}

renderData(data, total - pageCount, page + 1, pageCount)
}, 0)
}

fetchData().then(res => {
renderData(res.data, res.data.length, 0, 200)
})

</script>
</body>

</html>

上面的例子中,我们使用了 setTimeout,在每一次宏任务中插入一页数据,然后设置多个这样地宏任务,直到把所有数据都插入为止。


1111111.webp


但是很明显能看到的问题是,快速拖动滚动条时,数据列表中会有闪烁的情况


这是因为:



当使用 setTimeout 来拆分大量的 DOM 插入操作时,虽然我们将延迟时间设置为 0ms,但实际上由于 JavaScript 是单线程的,任务执行时会被放入到事件队列中,而事件队列中的任务需要等待当前任务执行完成后才能执行。所以即使设置了 0ms 延迟,setTimeout 的回调函数也不一定会立即执行,可能会受到其他任务的阻塞。




setTimeout 的回调函数执行的间隔超过了浏览器每帧更新的时间间隔(一般是 16.7ms),就会出现丢帧现象。丢帧指的是浏览器在更新页面时,没有足够的时间执行全部的任务,导致部分任务被跳过,从而导致页面渲染不连续,出现闪烁的情况



所以,我们改善一下,通过 requestAnimationFrame 来处理


通过 requestAnimationFrame


<!--
* @Author: Jolyne
* @Date: 2023-09-22 15:45:45
* @LastEditTime: 2023-09-22 15:47:24
* @LastEditors: Jolyne
* @Description:
-->

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>直接插入十万条数据</title>
</head>

<body>
<ul id="list-container"></ul>

<script>
const oListContainer = document.getElementById('list-container')

const fetchData = () => {
return new Promise(resolve => {
const response = {
code: 0,
msg: 'success',
data: [],
}

for (let i = 0; i < 100000; i++) {
response.data.push(`content-${i + 1}`)
}

setTimeout(() => {
resolve(response)
}, 100)
})
}

// 模拟请求后端接口返回十万条数据
// 渲染 total 条数据中的第 page 页,每页 pageCount 条数据
const renderData = (data, total, page, pageCount) => {
// base case -- total 为 0 时没有数据要渲染 不再递归调用
if (total <= 0) return

// total 比 pageCount 少时只渲染 total 条数据
pageCount = Math.min(pageCount, total)

requestAnimationFrame(() => {
const startIdx = page * pageCount
const endIdx = startIdx + pageCount
const dataList = data.slice(startIdx, endIdx)

// 将 pageCount 条数据插入到容器中
for (let i = 0; i < pageCount; i++) {
const oItem = document.createElement('li')
oItem.innerText = dataList[i]
oListContainer.appendChild(oItem)
}

renderData(data, total - pageCount, page + 1, pageCount)
})
}

fetchData().then(res => {
renderData(res.data, res.data.length, 0, 200)
})

</script>
</body>

</html>

222222.webp


很明显,闪烁的问题被解决了


这是因为:



requestAnimationFrame 会在浏览器每次进行页面渲染时执行回调函数,保证了每次任务的执行间隔是稳定的,避免了丢帧现象。所以在处理大量 DOM 插入操作时,推荐使用 requestAnimationFrame 来拆分任务,以获得更流畅的渲染效果



作者:Jolyne_
来源:juejin.cn/post/7282756858174980132
收起阅读 »

前端:“这需求是认真的吗?” —— el-select 的动态宽度解决方案

web
Hello~大家好。我是秋天的一阵风 ~ 前言 最近我遇到了一个神奇的需求,客户要求对 el-select 的 宽度 进行动态设置。 简单来说,就是我们公司有一些选择框,展示的内容像“中华人民共和国/广西壮族自治区/南宁市/西乡塘区”这么长,一不小心就会内容超...
继续阅读 »

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


前言


最近我遇到了一个神奇的需求,客户要求对 el-select宽度 进行动态设置。


简单来说,就是我们公司有一些选择框,展示的内容像“中华人民共和国/广西壮族自治区/南宁市/西乡塘区”这么长,一不小心就会内容超长,显示不全。详情请看下面动图:


1.gif


一般来说,想解决内容展示不全的问题,有几种方法。


第一种:给选择框加个tooltip效果,在鼠标悬浮时展示完整内容。


第二种:对用户选择label值进行切割,只展示最后一层内容。


但是我们的客户对这两种方案都不接受,要求选择的时候让select选择框的宽度动态增加。


有什么办法呢?客户就是上帝,必须满足,他们说什么就是什么,所以我们只能开动脑筋,动手解决。


思路


我们打开控制台,来侦察一下el-select的结构,发现它是一个el-input--suffixdiv包裹着一个input,如下图所示。


image.png

内层input的宽度是100%,外层div的宽度是由这个内层input决定的。也就是说,内层input的宽度如果动态增加,外层div的宽度也会随之增加。那么问题来了,如何将内层input的宽度动态增加呢?



tips:


如果你对width的100%和auto有什么区别感兴趣,可以点击查看我之前的文章


探究 width:100%与width:auto区别



解决方案


为了让我们的el-select宽度能够跟着内容走,我们可以在内层input同级别增加一个元素,内容就是用户选中的内容。内容越多,它就像一个胃口很大的小朋友,把外层div的宽度撑开。下面来看图示例 :


image.png

借助prefix


幸运的是,el-select本身有一个prefix的插槽选项,我们可以借助这个选项实现:


image.png

我们添加一个prefix的插槽,再把prefix的定位改成relative,并且把input的定位改成绝对定位absolute。最后将prefix的内容改成我们的选项内容。看看现在的效果:



<template>
<div>
<el-select class="autoWidth" v-model="value" placeholder="请选择">
<template slot="prefix">
{{optionLabel}}
</template>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>

</el-option>
</el-select>
</div>

</template>

<script>
export default {
data() {
return {
options: [
{
value: "选项1",
label: "中华人民共和国/广东省/深圳市/福田区",
},
{
value: "选项2",
label: "中华人民共和国/广西壮族自治区/南宁市/西乡塘区",
},
{
value: "选项3",
label: "中华人民共和国/北京市",
},
{
value: "选项4",
label: "中华人民共和国/台湾省",
},
{
value: "选项5",
label: "中华人民共和国/香港特别行政区",
},
],
value: "",
};
},
computed: {
optionLabel() {
return (this.options.find((item) => item.value === this.value) || {})
.label;
},
},
};
</script>

<style lang="scss" scoped>

::v-deep .autoWidth .el-input__prefix {
position: relative;
}

::v-deep .autoWidth input {
position: absolute;
}
</style>



2.gif

细节调整


现在el-select已经可以根据选项label的内容长短动态增加宽度了,但是我们还需要继续处理一下细节部分,将prefix的内容调整到和select框中的内容位置重叠,并且将它隐藏。看看现在的效果


::v-deep .autoWidth .el-input__prefix {
position: relative;
box-sizing: border-box;
border: 1px solid #fff;
padding: 0 30px;
height: 40px;
line-height: 40px;
left: 0px;
visibility: hidden;
}

3.gif

调整初始化效果(用户未选择内容)


目前已经基本实现了效果了,还有最后一个问题,当用户没有选择内容的时候,select的宽度是“没有”的,如下图所示。


image.png

所以我们还得给他加上一个最小宽度


image.png

我们加上最小宽度以后,发现这个select的图标又没对齐,这是因为我们在重写.el-input__prefix样式的时候设置了padding: 0 30px,当用户没有选择内容的时候,select的图标应该是默认位置,我们需要继续调整代码,最后效果如下图所示:


4.gif

完整代码


最后附上完整代码:



<template>
<div>
<el-select
class="autoWidth"
:class="{ 'has-content': optionLabel }"
v-model="value"
placeholder="请选择"
clearable
>

<template slot="prefix">
{{ optionLabel }}
</template>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
>

</el-option>
</el-select>
</div>

</template>

<script>
export default {
data() {
return {
options: [
{
value: "选项1",
label: "中华人民共和国/广东省/深圳市/福田区",
},
{
value: "选项2",
label: "中华人民共和国/广西壮族自治区/南宁市/西乡塘区",
},
{
value: "选项3",
label: "中华人民共和国/北京市",
},
{
value: "选项4",
label: "中华人民共和国/台湾省",
},
{
value: "选项5",
label: "中华人民共和国/香港特别行政区",
},
],
value: "",
};
},
computed: {
optionLabel() {
return (this.options.find((item) => item.value === this.value) || {})
.label;
},
},
};
</script>

<style lang="scss" scoped>
.autoWidth {
min-width: 180px;
}
::v-deep .autoWidth .el-input__prefix {
position: relative;
box-sizing: border-box;
border: 1px solid #fff;
padding: 0 30px;
height: 40px;
line-height: 40px;
left: 0px;
visibility: hidden;
}

::v-deep .autoWidth input {
position: absolute;
}
.autoWidth {
// 当.has-content存在时设置样式
&.has-content {
::v-deep .el-input__suffix {
right: 5px;
}
}
// 当.has-content不存在时的默认或备选样式
&:not(.has-content) {
::v-deep .el-input__suffix {
right: -55px;
}
}
}
</style>



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

程序媛28岁前畅游中国是什么体验?

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗。 前几年互联网飞速发展高薪招人时,大家...
继续阅读 »

本人计算机硕士毕业,先后在三家厂工作,工作节奏虽说不是 007 吧,但偶尔 996 是有的,勤勤恳恳搬砖是常态,也偶尔累了就划划水摸鱼。在这行业不焦虑是假的,35 岁危机时刻提醒着每一位年轻的程序员,这行主打一个精神内耗


前几年互联网飞速发展高薪招人时,大家都有肉吃,现在遇到互联网寒冬了,有汤喝就不错了,尤其对晚入行的95 后社畜,现在回过头看,已经是互联网红利退潮的末期了。对于 80 后早一批入行的程序员, 肯定钱也挣够了,房子也早就翻几倍了,早就有抗御风险的能力了,即使裁员了也能拿着分手费找个差不多的厂子继续苟着。但是对于 95 后来说,惨不忍睹,行业内卷及其严重,刚有点工作经验就遭遇大规模裁员,重点买房都是踩在最高点接盘,现在房价跌了,车子打价格战,直接把前几年辛辛苦苦挣的首付跌没了,这几年白干了,说起来,心就抽搐的疼。不像人家00 后,直接看开了,不破三个 dai,房贷,车贷和传宗接代,直接卷老家公务员躺平,享受人生,逃离大城市的拥挤,拒绝被房子的套牢。


金融危机,经济下行,行业越来越卷,精神内耗极其严重,身体健康堪忧。我突然顿悟了,我决定,为自己而活。想看世界的心也越来越强烈,最后我坐不住了,做了个大胆的决定,畅游中国。刚好疫情快结束时,航空公司推出了自己的产品,畅游中国随心飞,我立刻入手了,入手价是三千多点,全国飞不限次数。我一边安排好自己的时间订机票,一边计划旅行路线,一个女生独自环游中国之旅开始了。没有队友,不给生活中任何糟心事打断我的计划,一人吃饱,全家不饿,当时就已经下定决心了,哪怕一天就只吃个树上的野果子就好,我也要去看世界,可能喜欢宅的人不太理解,但我明白自己想要什么,我理解自己就好,我并不是给别人活的。


下面给大家说说我去了哪些地方。


贵州-贵阳


我看好时间后立刻定机票,从上海飞到了贵阳,准备打卡黄果树瀑布。我定的酒店就在黄果树景点附近不远,一大清早7点我就起床了,呼吸着让人神清气爽的空气,吃了一些自己带的进口苹果作为早餐,特别甘甜,饱腹感足足的。8点进山了,那一刻,我别提多开心了。


回想起当社畜时,每次都是8.30起床,9.30左右到公司,每天上班心情比上坟都沉重,永远干不完的KPI,OCR,不是被PUA就是吃老板画的大饼,再丰盛的早餐一想到一堆任务要做,吃着也如同嚼蜡,更别提神清气爽,心境开拓了。


在进入黄果树后,我欢快的脚步往前走,因为我是一个女孩子独行,所以不太愿意跟陌生人说话,一路上虽然很沉默,但看到这些壮观的自然景色,闻着草木花果香,内心激动不已。爬了一个钟左右的山,终于看到了大瀑布。


下面是我实拍的景点图:


WechatIMG110.jpg
WechatIMG114.jpg
WechatIMG111.jpg
WechatIMG115.jpg

有句古诗,疑似银河落九天,一路好山好水,逛完黄果树后我出来就去吃了贵州的特色菜,价格美丽,味道很不错,超级喜欢,
当时就在感慨,上海要是能吃到这么好吃又鲜美的酸汤鱼就好了。


WechatIMG109.jpg

重庆


本来下一步去梵净山再顺路去成都自驾318路线的,但时间紧迫,我弟弟在重庆读书,说要跟我一起去自驾318,我就先去重庆跟他汇合了。


1191711609983_.pic.jpg
1211711610320_.pic.jpg
1241711610640_.pic.jpg

最后那个火锅要适度吃啊,吃两顿辣的我的陈年胃病都犯了,好几天没缓过来,哭晕在厕所,我弟跟个没事人一样,这是我深刻认识到当了多年的社畜的后果就是,经常熬夜加班点外卖,把好好的身体给造坏了。重庆的洪崖洞,解放碑也去了很多次了,这里给个图


1201711609995_.pic.jpg

成都


抵达成都,在春熙路逛了逛,宽窄巷子之前逛过就没去了,


1221711610329_.pic.jpg
1461711611961_.pic.jpg

本来想租自驾神车-坦克300的,价格是普通suv的2倍,结果路上纠结一会的功夫就被抢先租走了(自我反思:以后看准就下手吧,人生有几次这种机会,有啥好犹豫的),租了一辆1.5T的大众SUV,跟我弟一起直奔车行,然后去超市采购路上的食物,大包小包买了一堆,放车后备箱,深夜就起航了


都江堰


教科书上的都江堰,真正去看了,才深深佩服古人治水的智慧,我不是文盲,所以不用一句:卧槽,发表感叹。之前也去过洛阳的黄河小浪底水库,武汉的长江大桥,这些水利工程的智慧。


1261711610658_.pic.jpg
1251711610651_.pic.jpg

青城山


1451711611634_.pic.jpg

这里是青城山下白素贞的故事发源地。爬山是个体力活,当时穿着拖鞋就上山了,下山就傻眼了,不好意思,这里我偷懒了,坐缆车下车,嘿嘿。


泸定桥


1281711611190_.pic.jpg
1271711610783_.pic.jpg

打卡泸定桥,走上面摇摇晃晃确实需要一些勇气,特别怕手机掉下去。


海螺沟


一鼓作气,一路直行,抵达海螺沟。来之前,我觉得新能源车咋自驾318,路上看到同样是特斯拉车主,我感觉自己有点狭隘。人啊,果然要多出去看看,不能活在自己的局限认知中。


1301711611238_.pic.jpg
1481711613272_.pic.jpg
1311711611256_.pic.jpg

不过开车还是要小心,路上遇到有车盘山时发生侧翻的。还有山上偶尔会有落石下来,要当心了。


木格措


一路景色壮观,蓝天白云,川西一定要必去。到了康定情歌的原地。打个卡。


1331711611313_.pic.jpg
1471711612939_.pic.jpg

不过我路上听的歌一直都是朴树的《平凡之路》,一路循环:


我曾经跨过山和大海 也穿过人山人海

我曾经拥有着的一切 转眼都飘散如烟

我曾经失落失望 失掉所有方向

直到看见平凡 才是唯一的答案
....


不正是正值青春的我受伤了,但又奋力前行寻找答案吗。


四姑娘山


一路直行。。。抵达四姑娘山,四姑娘山有四座雪山组成,远看景色很壮观,雪已经化了很多。


1291711611214_.pic.jpg
1441711611506_.pic.jpg
1421711611464_.pic.jpg

当地信仰


遇到了一群一动不动的牦牛,还有一匹热情好客的长脸马。拿出来一个饼给它,它吃的还很香。本来开心的事现在记录起来突然感觉在暗示自己在公司当牛做马,不说了,emo了。据说那白色塔这是当地的信仰,表示尊重。


1361711611362_.pic.jpg
1351711611349_.pic.jpg
1391711611410_.pic.jpg

雅拉山口


盘山路,1.5T的车开着有点吃力,油门上不去。终于爬上山了,下车拍照时,激动过头了,开始缺氧,头疼,吸氧。。。。。。。。


1371711611374_.pic.jpg
1381711611398_.pic.jpg
1561711614555_.pic.jpg

后面走着走着身体扛不住了,我去当地买了高反的药,吃了没啥用,氧气越吸头越疼,我弟要回去上课,我身体不抗造,遗憾的半途而归了。再次强调一下,好风景要趁年轻,体力好,等老了走不动了,确实再好的风景,都没那心情和体力去欣赏了。


1551711614521_.pic.jpg
1571711614582_.pic.jpg

乐山


跟我弟散伙后,我自己开车去了乐山大佛,保佑我顺风顺水吧。还去看了东方佛群,卧佛,药师佛,看了各种佛,记不清楚了。。。


1531711614452_.pic.jpg
1601711617453_.pic.jpg
1611711617471_.pic.jpg

峨眉山


接着我自己又自驾去了峨眉山,两个地方相差不是很远,看到了峨眉山的云海,云雾缭绕,超级刺眼!


1521711614435_.pic.jpg
1511711614416_.pic.jpg

下山后当晚接着又踩着点返回成都还车。休息一晚后,又顺路打卡了锦里。感受人世间的烟火和繁华


1591711614637_.pic.jpg
1581711614623_.pic.jpg

又吃了一顿火锅后,回上海。这时,胃没有不舒服,看来,这一圈下来,肠胃好很多了。


1621711617506_.pic.jpg

又回到了我熟悉的大上海。


安徽


经过一段时间的调养后,我觉得的身体状态老好了,爬山那不是小意思,走,爬山去,什么黄山,三清山,庐山,武功山,离沪这么近,爬起来不费劲!我到了安徽省,黄山市,休息一晚准备去爬山。当晚被出租车司机拉到了老街逛逛。


1631711617723_.pic.jpg

就一个小型的徽派建筑青砖白瓦的特色,跟顾村差不多。逛完后突然下起了大雨,我猝不及防没带伞,
就记得那晚的雨,比情深深雨蒙蒙中依萍找她爸要钱被鞭子抽回去时遇到的那场大雨还大。。。。。。


黄山


不凑巧,上山时遇到了大雾,但来都来了,那就爬山下去吧。到了光明顶也啥都看不见,但幸运的时,下山时,守得云开见月明,气喘吁吁的开心拍照。


1641711617970_.pic.jpg
1651711617990_.pic.jpg
1661711618010_.pic.jpg

江西


黄山结束后,顺路就来到了江西,江西景色比较集中,一定要去上饶啊,那就先去望仙谷看看吧。


上饶-望仙谷


人工打造的经典,现实版的仙侠世界。小雨朦胧,青山傍水,景色秀丽。


1681711618049_.pic.jpg
1671711618030_.pic.jpg
1691711618070_.pic.jpg

上饶-三清山


谁说黄山归来不看山,我觉得三清山值得一去,至少我是不后悔的。每座山都有每座山的特色,爬到这时,腿开始抖了,但我可不是那么轻易就能认输的人啊,继续爬,专挑难爬的道:一线天!!!!!


1721711618536_.pic.jpg
1701711618509_.pic.jpg
1711711618524_.pic.jpg

哈哈,说这个像蟒蛇,像吗?


1731711618548_.pic.jpg

下山时腿疼的不行,扛不住了,嘴不硬了,不去庐山了,武功山了。。。。


南昌


对了,不明白为啥江西彩礼那么高?


1741711619525_.pic.jpg

广东


广州


从南昌飞到广州了,看了小蛮腰,在附近喝喝茶,遛遛弯,吃点茶点


1751711619548_.pic.jpg
1761711619562_.pic.jpg

深圳


到深圳后租了个车溜达到海边吃海鲜,还去华强北也溜一溜,吃了很多粤菜


2121711623487_.pic.jpg
2131711623507_.pic.jpg
2141711623518_.pic.jpg

香港


从深圳坐高铁到香港也就十几分钟,跟快的。香港巴士,香港茶餐厅,路过金店,想买项链的,但又怕弄丢了就没买,现在金价那么高,有点损失。


2171711623568_.pic.jpg
2181711623590_.pic.jpg
2161711623554_.pic.jpg
2151711623538_.pic.jpg

新疆


从上海飞新疆要4个多小时,一路太无聊了,下飞机后,心情就好很多


1781711619752_.pic.jpg

乌鲁木齐


去了大巴扎,吃了羊肉串和切糕,还有新疆大盘鸡


1911711619947_.pic.jpg
1921711619959_.pic.jpg
1931711619971_.pic.jpg
1951711620020_.pic.jpg

无人区


没信号,没水,荒漠一片。。。


1901711619929_.pic.jpg
1891711619916_.pic.jpg

伊犁


到了伊犁市区后,去了小吃街,吃了羊肉


1961711620038_.pic.jpg

赛里木湖


高原湖泊,非常适合自驾游玩,我这里是跟人拼车去的。看着真舒服,可惜我把单反带来,也背不动,这是人家的


1851711619856_.pic.jpg
1841711619835_.pic.jpg
1801711619776_.pic.jpg
1831711619820_.pic.jpg
1811711619793_.pic.jpg
1821711619807_.pic.jpg

边境-国门,果子沟大桥, 薰衣草


1881711619895_.pic.jpg
1871711619882_.pic.jpg
1861711619867_.pic.jpg

新疆白天长,夜里段,到了晚上9点多,天才慢慢开始变黑。


北京


这次我飞到了老北京,看了天安门,看了老城墙


1971711620074_.pic.jpg
1981711620099_.pic.jpg

内蒙古


从北京顺路来了内蒙古呼和浩特,先填饱肚了,去那个什么街买了一堆牛肉干


呼和浩特


1991711623013_.pic.jpg
2011711623052_.pic.jpg
2021711623101_.pic.jpg

青甘环线


说到去青甘,想起有个在学生时期就在玩的狐朋狗友,听说我打算去自驾就想跟我一起去。因为我的车是新能源,自驾充电比较麻烦,他打算提混动车方便些,他说让我等他提车带他一起去自驾,本来约定好了时间,到快出发时,一会又说不打算提车了,又说等他面试换好工作后,最后他自己又各种理由怂了,这种又想出去玩,又想挣钱,又不舍得花钱,这种拧巴的状态,我很无语,当然,这也是现实中大部分人的写实吧,这里我想说,做好权衡利弊和取舍就好,既然决定去追求诗和远方,就不要再去跟钱分文必争了,不可否认,旅行确实需要花钱,我们能做的就是按照自己能承担的最低的成本去看世界。人家说勇敢的人先享受世界,让他纠结犹豫去吧,我就先溜了,毕竟老祖宗给的经验是:欲买桂花同载酒,终不似,少年游。再后来,他说他提车了,问我还去不去,我说我早就已经打卡过了。我问他新工作找好了?他说还没有。。。所以他白拧巴了,车还是要提,想去的地方最终还是要去,挣不了的钱最终还是没到口袋里去。毕竟能随时说走就走的同行者只有自己。


我是从内蒙飞到了青海的西宁。


西宁市


填饱肚子先,然后出发去青海湖,远看蓝色,近看青色,全靠天气


2031711623308_.pic.jpg
2351711690717_.pic.jpg

青海湖


2091711623426_.pic.jpg
2051711623359_.pic.jpg

茶卡盐湖


天空之境,名不虚传。


2061711623379_.pic.jpg
2081711623412_.pic.jpg
2361711690772_.pic.jpg

丹霞地貌,策马奔腾


2111711623453_.pic.jpg
2101711623438_.pic.jpg

策马奔腾很潇洒,归来草原上都是马粪,有点臭。。。
仙气飘飘的牦牛,跟川西的大黑牛不一样
2231711624048_.pic.jpg
2041711623341_.pic.jpg


后面的敦煌,莫高窟去不了了,青海也是有3000多海拔的,玩嗨了,又又又高反了,不得已要回去了,哎,当了这么多年生产驴,身体熬废了。回去后多锻炼身体吧。毕竟身体是革命的成本。


武汉


于是,先飞回了武汉玩几天。回家转转,熟悉的感觉。喜欢武汉的大江大湖和历史文化。黄鹤一去不复返,白云千载空悠悠。
然后又从武汉飞到上海狗着。


2241711624590_.pic.jpg
2261711624823_.pic.jpg
2251711624807_.pic.jpg

上海市


这个城市充满了魅力。只要你有钱,就可以纸醉金迷,去和平饭店享受,去挥霍。没钱,只能继续搬砖。


2291711625200_.pic.jpg
2301711625213_.pic.jpg
2271711624846_.pic.jpg

回去后改善饮食,一边努力干活学习,一边下定决心锻炼,都有马甲线了,五公里so easy ,哈哈哈哈。每次回到上海这个繁华的国际大都市,我都深深感受到,这座城市虽然压力大,但终究是自由的,没人关心和打扰你的私人生活,你可以为自己而活,安排自己的一生,不必循规蹈矩,不必顾及世俗的眼光,这个城市包容能力很强,不妨大胆一些,追求自己的人生。去不同的城市体验不一样的生活和文化。




总结


在买随心飞之前我也去过很多城市,比如:湖北的荆州,湖南的岳阳,张家界,广东的东莞,广西的桂林和北海,海南的三亚,云南的昆明大理丽江,江浙沪包邮一带的杭州,南京,无锡,湖州,台州,宁波,福建的厦门,河南的洛阳,开封,郑州,信阳,山东青岛,陕西西安,安徽合肥等城市。时间有限,码字不易,很抱歉这里我就不全部列出了。尤其在学生时代,那是真的快乐,没有一丝丝杂念,单纯的快乐。后面打算环游世界了,已经去了东南亚的一些国家,这里我想说我本来就是为了WLB努力的,工作生活两不误,我的旅途未完待续~


回顾这么多年,走过的国内大大小小的城市,也没具体统计过,开始逐渐让自己的眼界开阔起来,不让自己的眼光那么狭隘了,看待任何事物更具包容性吧,以前不理解的东西,现在慢慢理解了。也许人生就是这样,思想和观念一直变化。还是那句话,勇敢的人先享受人生吧,不要辜负努力写代码的自己。




后续


关于有人问我旅游的钱哪来的?我说我钱抢银行来的你信吗?开头已经提到自己已经牛马些年了,不然之前身上也不至于带这么大的班味,而且平时也不是月光族,手里有点小存款算是可以抵御日常的一些风险吧。


旅行中机票费用占大头,不过都在随心飞里头了,真是省了一笔巨款吧!我每次定机票只需要付100元建机燃油费就行(约等于一周的奶茶或者咖啡费,那会的机票费用价格还没有现在高的这么离谱)。酒店也不是住啥五星级酒店,基本上都是找的干净评分比较高的。吃的也不是啥高档餐厅,都是网红性价比高的饭店,全程主打一个性价比,一人吃饱全家不饿。总费用加起来差不多消耗了三个月工资吧,在自己的消费能力范围之内。因为每个人的消费标准和收入不一样,这里就没必要去扣一个死数字了,当然这个消费标准肯定要根据收入水平来的,不建议超额负担消费,我路上碰到过有人住青年旅舍吃泡面都能一路玩的特别开心,也见过有的人开豪车,晚上吃烤全羊喝茅台,一路有专人专车服务着,这一路的所见真的不是在家里坐着就能接触到的。所以,我个人觉得,穷游有穷的开心,富人有富玩的旅途,所以,大家不该纠结比别人多花多少碎银,而是应该多些出发的勇气和和收获快乐。本身我在上学时期就喜欢跟家人一起自驾游,后备箱塞满了干粮,哈哈,就差煤气开火了,那会真的很快乐,有时出去玩坐绿皮车吃泡面都能激动一路,不过工作后,时间不自由了。收获多少快乐跟赚多少钱并不能成正比!


关于时间问题:每年有二十多天假(不包括调休假,另算累加),同时加上换工作GAP,基本上约等于一个缓冲期了,不再像刚毕业一样把自己当牛马使了,可能打工人血脉开始觉醒了,相对自己好点,有时真的,自己想明白,比一直低头苦干重要多了,极端的逼自己很有可能是压死骆驼的最后一根稻草,适当的给自己放个假,反而更容易想明白很多事情,别太喜欢跟自己较真,放过自己,面对生活更从容一点不好吗?


不理解的掘友请绕过,你继续熬夜加你的班走好你的奈何桥,我看我的风景过好我的阳关道,我不需要用别人的执念去过我短暂的一生,我知道自己的人生该是什么样,我为自己而活。


最后,勇敢的人先享受世界!做好取舍就行,至少我已经完成了自己人生的一段旅途!在此做个记录,顺便鼓励迷茫中的“同道中人”!


作者:为了WLB努力
来源:juejin.cn/post/7351301965034586152
收起阅读 »

告别频繁登录:教你用Axios实现无感知双Token刷新

web
一、引言在现代系统中,Token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳。随着Token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的Tok...
继续阅读 »

一、引言

在现代系统中,Token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳。随着Token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的Token刷新机制。通过结合Vue3和Axios这两大前端技术栈,我们可以借助Promise机制,开发出一种更加完善的自动化Token刷新方案,显著提升系统的稳定性和用户体验。本文将深入探讨这一实现过程,帮助你解决Token刷新难题。

二、示意图

image.png

三、具体实现

了解了基本步骤后,实际的实现过程其实相当简洁。然而,在具体操作中,仍有许多关键细节需要我们仔细考量,以确保Token刷新机制的稳定性和可靠性。

  1. Token 存储与管理:首先,明确如何安全地存储和管理Access Token与Refresh Token。这涉及到浏览器的存储策略,比如使用localStoragesessionStorage,存储策略不在本文中提及,本文采用localStorage 进行存储。
  2. 请求拦截器的设置:在Axios中设置请求拦截器,用于在每次发送请求前检查Token的有效性。如果发现Token过期,则触发刷新流程。这一步骤需注意避免并发请求引发的重复刷新。
  3. 处理Token刷新的响应逻辑:当Token过期时,通过发送Refresh Token请求获取新的Access Token。在这里,需要处理刷新失败的情况,如Refresh Token也失效时,如何引导用户重新登录。
  4. 队列机制的引入:在Token刷新过程中,可能会有多个请求被同时发出。为了避免重复刷新Token,可以引入队列机制,确保在刷新Token期间,其他请求被挂起,直到新的Token可用。
  5. 错误处理与用户体验:最后,要对整个流程中的错误进行处理,比如刷新失败后的重试逻辑、错误提示信息等,确保用户体验不受影响。

通过以上步骤的实现,你可以构建一个用户无感知、稳定可靠的双Token刷新机制,提升应用的安全性与用户体验。接下来,我们将逐一解析这些关键步骤的具体实现。

1. 编写请求拦截器

实现请求拦截器的基本逻辑比较简单,即在每次请求时自动附带上Token以进行认证。

service.interceptors.request.use((config: InternalAxiosRequestConfig) => {  
const userStore = useUserStore()
if (userStore.authInfo.accessToken && userStore.authInfo.accessToken !== "") {
// 设置头部 token
config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
}
return config;
}, (error: any) => {
return Promise.reject(error);
}
);

目前的实现方案是,在请求存在有效Token时,将其附带到请求头中发送给服务器。但在一些特殊情况下,某些请求可能不需要携带Token。为此,我们可以在请求配置中通过config对象来判断是否需要携带Token。例如:

request: (deptId: number, deptForm: DeptForm): AxiosPromise<void> => {  
return request<void>({
url: DeptAPI.UPDATE.endpoint(deptId),
method: "put",
data: deptForm,
headers: {
// 根据需要添加Token,或者通过自定义逻辑决定是否包含Authorization字段
token: false
}
});
}

那么在请求拦截器中,您需要多加一个判断,就是判断请求头中token是否需要

// 代码省略

2. 深究响应拦截器

对于双token刷新的难点就在于响应拦截器中,因为在这里后端会返回token过期的信息。我们需要先清楚后端接口响应内容

2.1 接口介绍

  • 正常接口响应内容
// Status Code: 200 OK
{
"code":"0000",
"msg":"操作成功",
"data":{}
}
  • accessToken 过期响应内容
// Status Code: 401 Unauthorized
{
"code":"I009",
"msg":"登录令牌过期"
}
  • accessToken 刷新响应内容
// Status Code: 200 OK
{
"code": "0000",
"msg": "操作成功",
"data": {
"accessToken": "",
"refreshToken": "",
"expires": ""
}
}
  • refreshToken 过期响应内容
// Status Code: 200 OK
{
"code": "I009",
"msg": "登录令牌过期"
}

注意 : Status Code不是200时,Axios的响应拦截器会自动进入error方法。在这里,我们可以捕捉到HTTP状态码为401的请求,从而初步判断请求是由于Unauthorized(未授权)引发的。然而,触发401状态码的原因有很多,不一定都代表Token过期。因此,为了准确判断Token是否真的过期,我们需要进一步检查响应体中的code字段。

2.2 响应拦截器编写

有上面的接口介绍,我们编写的就简单,判断error.response?.status === 401、code === I009 即可,如果出现这种情况就直接刷新token。

service.interceptors.response.use(async (response: AxiosResponse) => {
// 正常请求代码忽略
return Promise.reject(new Error(msg || "Error"));
},
async (error: any) => {
const userStore = useUserStore()
if (error.response?.status === 401) {
if (error.response?.data?.code === RequestConstant.Code.AUTH_TOKEN_EXPIRED) {
// token 过期处理
// 1. 刷新 token
const loginResult: LoginResult = await userStore.refreshToken()
if (loginResult) {
// refreshToken 未过期
// 2.1 重构请求头
error.config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
// 2.2 请求
return await service.request(error.config);
} else {
// refreshToken 过期
// 1. 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else {
// 如果是系统发出的401 , 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else if (error.response?.status === 403) {
// 403 结果处理 , 代码省略
} else {
// 其他错误结果处理 , 代码省略
}
return Promise.reject(error.message);
}
);

2.3 解决重复刷新问题

编写完成上面的内容,考虑一下多个请求可能同时遇到 Token 过期,如果没有适当的机制控制,这些请求可能会同时发起刷新 Token 的操作,导致重复请求,甚至可能触发后端的安全机制将这些请求标记为危险操作。

为了解决这个问题,我们实现了一个单例 Promise 的刷新逻辑,通过 singletonRefreshToken 确保在同一时间只有一个请求会发起 Token 刷新操作。其核心思想是让所有需要刷新的请求共享同一个 Promise,这样即使有多个请求同时遇到 Token 过期,它们也只会等待同一个刷新操作的结果,而不会导致多次刷新。

/**
* 刷新 token
*/

refreshToken(): Promise<LoginResult> {
// 如果 singletonRefreshToken 不为 null 说明已经在刷新中,直接返回
if (singletonRefreshToken !== null) {
return singletonRefreshToken
}
// 设置 singletonRefreshToken 为一个 Promise 对象 , 处理刷新 token 请求
singletonRefreshToken = new Promise<LoginResult>(async (resolve) => {
await AuthAPI.REFRESH.request({
accessToken: this.authInfo.accessToken as string,
refreshToken: this.authInfo.refreshToken as string
}
).
then(({data}) => {
// 设置刷新后的Token
this.authInfo = data
// 刷新路由
resolve(data)
}
).
catch(() => {
this.resetToken()
}
)
}
)
// 最终将 singletonRefreshToken 设置为 null, 防止 singletonRefreshToken 一直占用
singletonRefreshToken.
finally(() => {
singletonRefreshToken =
null;
}
)
return singletonRefreshToken
}

重要点解析:

  1. singletonRefreshToken 的使用

    • singletonRefreshToken 是一个全局变量,用于保存当前正在进行的刷新操作。如果某个请求发现 singletonRefreshToken 不为 null,就说明另一个请求已经发起了刷新操作,它只需等待这个操作完成,而不需要自己再发起新的刷新请求。
  2. 共享同一个 Promise

    • 当 singletonRefreshToken 被赋值为一个新的 Promise 时,所有遇到 Token 过期的请求都会返回这个 Promise,并等待它的结果。这样就避免了同时发起多个刷新请求。
  3. 刷新完成后的处理

    • 刷新操作完成后(无论成功与否),都会通过 finally 将 singletonRefreshToken 置为 null,从而确保下一次 Token 过期时能够重新发起刷新请求。

通过这种机制,我们可以有效地避免重复刷新 Token 的问题,同时也防止了由于过多重复请求而引发的后端安全性问题。这种方法不仅提高了系统的稳定性,还优化了资源使用,确保了用户的请求能够正确地处理。

四、测试

  1. 当我们携带过期token访问接口,后端就会返回401状态和I009。

image.png

这时候进入

const loginResult: LoginResult = await userStore.refreshToken()
  1. 携带之前过期的accessToken和未过期的refreshToken进行刷新
  • 携带过期的accessToken的原因 :
    • 防止未过期的 accessToken 进行刷新
    • 防止 accessToken 和 refreshToken 不是同一用户发出的
    • 其他安全性考虑

image.png

  1. 获取到正常结果

image.png


作者:翼飞
来源:juejin.cn/post/7406992576513589286

收起阅读 »

前端到底该如何安全的实现“记住密码”?

web
在 web 应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且...
继续阅读 »

web 应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且安全性也是杠杠的!


1. 使用 localStorage


localStorage 是一种持久化存储方式,数据在浏览器关闭后仍然存在。适用于需要长期保存的数据。


示例代码


// 生成对称密钥
async function generateSymmetricKey() {
const key = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"]
);
return key;
}

// 加密数据
async function encryptData(data, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
new TextEncoder().encode(data)
);
return { iv, encryptedData };
}

// 解密数据
async function decryptData(encryptedData, key, iv) {
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
encryptedData
);
return new TextDecoder().decode(decryptedData);
}

// 保存用户信息
async function saveUserInfo(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
localStorage.setItem('username', username);
localStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfo() {
const username = localStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(localStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function login(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfo(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfo();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

2. 使用 sessionStorage


sessionStorage 是一种会话级别的存储方式,数据在浏览器关闭后会被清除。适用于需要临时保存的数据。


示例代码


// 保存用户信息
async function saveUserInfoSession(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
sessionStorage.setItem('username', username);
sessionStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoSession() {
const username = sessionStorage.getItem('username');
const { iv, encryptedData } = JSON.parse(sessionStorage.getItem('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function loginSession(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoSession(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoSession();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

3. 使用 IndexedDB


IndexedDB 是一种更为复杂和强大的存储方式,适用于需要存储大量数据的场景。


示例代码


// 打开数据库
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('UserDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'username' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

// 保存用户信息
async function saveUserInfoIndexedDB(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
const db = await openDatabase();
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.put({ username, iv, encryptedData });
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoIndexedDB(username) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction('users', 'readonly');
const store = transaction.objectStore('users');
const request = store.get(username);
request.onsuccess = async (event) => {
const result = event.target.result;
if (result) {
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(result.encryptedData, key, result.iv);
resolve({ username: result.username, password });
} else {
resolve(null);
}
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}

// 示例:用户登录时调用
async function loginIndexedDB(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoIndexedDB(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const username = 'exampleUsername'; // 从某处获取用户名
const userInfo = await getUserInfoIndexedDB(username);
if (userInfo && userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

4. 使用 Cookie


Cookie 是一种简单的存储方式,适用于需要在客户端和服务器之间传递少量数据的场景。需要注意的是,Cookie 的安全性较低,建议结合 HTTPS 和 HttpOnly 属性使用。


示例代码


// 设置 Cookie
function setCookie(name, value, days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}

// 获取 Cookie
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}

// 保存用户信息
async function saveUserInfoCookie(username, password) {
const key = await generateSymmetricKey();
const { iv, encryptedData } = await encryptData(password, key);
setCookie('username', username, 7);
setCookie('password', JSON.stringify({ iv, encryptedData }), 7);
// 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoCookie() {
const username = getCookie('username');
const { iv, encryptedData } = JSON.parse(getCookie('password'));
const key = await generateSymmetricKey(); // 这里应使用同一个密钥
const password = await decryptData(encryptedData, key, iv);
return { username, password };
}

// 示例:用户登录时调用
async function loginCookie(username, password, rememberMe) {
if (rememberMe) {
await saveUserInfoCookie(username, password);
}
// 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
const userInfo = await getUserInfoCookie();
if (userInfo.username && userInfo.password) {
document.getElementById('username').value = userInfo.username;
document.getElementById('password').value = userInfo.password;
}
};

5. 使用 JWT(JSON Web Token)


JWT 是一种常用的身份验证机制,特别适合在前后端分离的应用中使用。JWT 可以安全地传递用户身份信息,并且可以在客户端存储以实现“记住密码”功能。


示例代码


服务器端生成 JWT


假设你使用 Node.js 和 Express 作为服务器端框架,并使用 jsonwebtoken 库来生成和验证 JWT。


const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const SECRET_KEY = 'your_secret_key';

// 用户登录接口
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 验证用户名和密码
if (username === 'user' && password === 'password') {
// 生成 JWT
const token = jwt.sign({ username }, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ message: 'Invalid credentials' });
}
});

// 受保护的资源
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Failed to authenticate token' });
}
res.json({ message: 'Protected resource', user: decoded.username });
});
});

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

客户端存储和使用 JWT


在客户端,可以使用 localStoragesessionStorage 来存储 JWT,并在后续请求中使用。


// 用户登录
async function login(username, password, rememberMe) {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
const token = data.token;
if (rememberMe) {
localStorage.setItem('token', token);
} else {
sessionStorage.setItem('token', token);
}
} else {
console.error(data.message);
}
}

// 获取受保护的资源
async function getProtectedResource() {
const token = localStorage.getItem('token') || sessionStorage.getItem('token');
if (!token) {
console.error('No token found');
return;
}
const response = await fetch('/protected', {
method: 'GET',
headers: {
'Authorization': token
}
});
const data = await response.json();
if (response.ok) {
console.log(data);
} else {
console.error(data.message);
}
}

// 示例:用户登录时调用
document.getElementById('loginButton').addEventListener('click', async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rememberMe = document.getElementById('rememberMe').checked;
await login(username, password, rememberMe);
});

// 示例:页面加载时自动填充
window.onload = async function() {
await getProtectedResource();
};

总结


如上示例,展示了如何使用 localStoragesessionStorage、IndexedDB、Cookie 和 JWT 来实现“记住密码”功能。每种方式都有其适用场景和安全考虑,大家可以根据具体需求选择合适的实现方式。


欢迎在评论区留言讨论~
Happy coding! 🚀


作者:我是若尘
来源:juejin.cn/post/7397284874652942363
收起阅读 »

uniapp 地图如何添加?你要的教程来喽!

web
地图在 app 中使用还是很广泛的,常见的应用常见有: 1、获取自己的位置,规划路线。 2、使用标记点进行标记多个位置。 3、绘制多边形,使用围墙标记位置等等。 此篇文章就以高德地图为例,以上述三个常见需求为例,教大家如何在 uniapp 中添加地图。 作为一...
继续阅读 »

地图在 app 中使用还是很广泛的,常见的应用常见有:


1、获取自己的位置,规划路线。


2、使用标记点进行标记多个位置。


3、绘制多边形,使用围墙标记位置等等。


此篇文章就以高德地图为例,以上述三个常见需求为例,教大家如何在 uniapp 中添加地图。


作为一个不管闲事的前端姑娘,我就忽略掉那些繁琐的账号申请,假设需要的信息问项目经理都要来了,如果你没有现成的信息,还需要申请,请查看:


lbs.amap.com/api/javascr…


去高德地图注册账号,根据官网指示获取 key。然后就正式开始前端 uniapp + 高德地图之旅啦!


一、地图配置


在使用地图之前需要配置一下你的地图账号信息,找到项目中的 manifest.json 文件,打开 web 配置,如图:


图片


此处是针对 h5 端,如果我们要打包 安卓和 IOS app 需要配置对应的key信息,如图:


图片


如果这些信息没有人给你提供,就需要自己去官网注册账号实名认证获取。


二、地图使用


2.1、使用标记点进行标记多个位置,具体效果图如下:


图片


<template>
<view class="map-con">
<map style="width: 100%; height: 300px;"
:latitude="latitude"
:longitude="longitude"
:markers="covers"
:scale="12">

</map>
</view>
</template>

<script>
export default {
data() {
return {
longitude: '116.473115',
latitude: '39.993207',
covers: [{
id: 1,
longitude: "116.474595",
latitude: "40.001321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.274595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.101321",
iconPath: '/static/images/point.png',
},
{
id: 1,
longitude: "116.374595",
latitude: "40.011321",
width: 44,
height: 50,
iconPath:'/static/images/point.png',
}
]
}
}
}
</script>

注意:


看着代码很简单,运行在 h5 之后一切正常,但是运行在安卓模拟器的时候,发现自定义图标没有起作用,显示的是默认标记点。


图片


iconpath 的路径不是相对路径,没有 ../../ 这些,直接根据官网提示写图片路径,虽然模拟器不显示但是真机是正常的。


2.2、绘制多边形,使用围墙标记位置等等。


图片


<template>
<view class="map-con">
<map style="width: 100%; height: 400px;" :latitude="latitude" :longitude="longitude" :scale="11"
:polygons="polygon" :markers="covers">

</map>
</view>
</template>

<script>
export default {
data() {
return {
longitude: '116.304595',
latitude: '40.053207',
polygon: [{
fillColor: '#f00',
strokeColor: '#0f0',
strokeWidth: 3,
points: [{
latitude: '40.001321',
longitude: '116.304595'
},
{
latitude: '40.101321',
longitude: '116.274595'
},
{
latitude: '40.011321',
longitude: '116.374595'
}
]
}],
covers: [{
id: 1,
width: 30,
height: 33,
longitude: "116.314595",
latitude: "40.021321",
iconPath: '/static/images/point.png',
}, ]
}
}
}
</script>

更多样式配置我们去参考官网,官网使用文档写的很细致,地址为:


uniapp 官网:uniapp.dcloud.net.cn/component/m…


三、易错点


1、地图已经显示了,误以为地图未展示


图片


左下角有高德地图标识,就说明地图已经正常显示了,此时可以使用鼠标进行缩放,或设置地图的缩放比例或者修改下地图中心点的经纬度。


2、标记点自定义图标不显示


marker 中的 iconPath 设置标记点的图标路径,可以使用相对路径、base64 等,但是在 h5 查看正常,app 打包之后就不能正常显示了,务必参考官网。


3、uni.getLocation 无法触发


在调试模式中,调用 uni.getLocation 无法触发,其中的 success fail complete 都无法执行,不调用的原因是必须在 https 环境下,所以先保证是在 https 环境下。****


四、有可用插件吗?


uniapp 插件:ext.dcloud.net.cn/search?q=ma…


搜索地图插件的时候,插件挺多的,有免费的也有付费的,即使使用插件也是需要需要注册第三方地图账号的。


我个人认为 uniapp 已经将第三方地图封装过了,使用挺便捷的,具体是否使用插件就根据项目实际情况定。


作者:前端人_倩倩
来源:juejin.cn/post/7271942371637559348
收起阅读 »

告别轮询,SSE 流式传输可太香了!

web
今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。 对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。 接下来,我将从 SSE ...
继续阅读 »

今天想和大家分享的一个技术是 SSE 流式传输 。如标题所言,通过 SSE 流式传输的方式可以让我们不再通过轮询的方式获取服务端返回的结果,进而提升前端页面的性能。


对于需要轮询的业务场景来说,采用 SSE 确实是一个更好的技术方案。


接下来,我将从 SSE 的概念、与 Websocket 对比、SSE 应用场景多个方面介绍 SSE 流式传输,感兴趣的同学一起来了解下吧!


什么是 SSE 流式传输


SSE 全称为 Server-sent events , 是一种基于 HTTP 协议的通信技术,允许服务器主动向客户端(通常是Web浏览器)发送更新。


它是 HTML5 标准的一部分,设计初衷是用来建立一个单向的服务器到客户端连接,使得服务器可以实时地向客户端发送数据。


这种服务端实时向客户端发送数据的传输方式,其实就是流式传输。


我们在与 ChatGPT 交互时,可以发现 ChatGPT 的响应总是间断完成。细扒 ChatGPT 的网络传输模式,可以发现,用的也是流式传输。


图片


SSE 流式传输的好处


在 SSE 技术出现之前,我们习惯把需要等待服务端返回的过程称为长轮询。


长轮询的实现其实也是借助 http 请求来完成,一个完整的长轮询过程如下图所示:


图片


从图中可以发现,长轮询最大的弊端是当服务端响应请求之前,客户端发送的所有请求都不会被受理。并且服务端发送响应的前提是客户端发起请求。


前后端通信过程中,我们常采用 ajax 、axios 来异步获取结果,这个过程,其实也是长轮询的过程。


而同为采用 http 协议通信方式的 SSE 流式传输,相比于长轮询模式来说,优势在于可以在不需要客户端介入的情况下,多次向客户端发送响应,直至客户端关闭连接。


这对于需要服务端实时推送内容至客户端的场景可方便太多了!


SSE 技术原理


1. 参数设置

前文说到,SSE 本质是一个基于 http 协议的通信技术。


因此想要使用 SSE 技术构建需要服务器实时推送信息到客户端的连接,只需要将传统的 http 响应头的 contentType 设置为 text/event-stream 。


并且为了保证客户端展示的是最新数据,需要将 Cache-Control 设置为 no-cache 。


在此基础上,SSE 本质是一个 TCP 连接,因此为了保证 SSE 的持续开启,需要将 Connection 设置为 keep-alive 。


Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

完成了上述响应头的设置后,我们可以编写一个基于 SSE 流式传输的简单 Demo 。


2. SSE Demo

服务端代码:


const express = require('express');
const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('/events'function(req, res) {
    res.setHeader('Content-Type''text/event-stream');
    res.setHeader('Cache-Control''no-cache');
    res.setHeader('Connection''keep-alive');

    let startTime = Date.now();

    const sendEvent = () => {
        // 检查是否已经发送了10秒
        if (Date.now() - startTime >= 10000) {
            res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
            res.end(); // 关闭连接
            return;
        }

        const data = { message'Hello World'timestampnew Date() };
        res.write(`data: ${JSON.stringify(data)}\n\n`);

        // 每隔2秒发送一次消息
        setTimeout(sendEvent, 2000);
    };

    sendEvent();
});

app.listen(PORT() => {
    console.log(`Server running on http://localhost:${PORT}`);
});

客户端代码:


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>SSE Example</title>
</head>

<body>
    <h1>Server-Sent Events Example</h1>
    <div id="messages"></div>

    <script>
        const evtSource = new EventSource('/events');
        const messages = document.getElementById('messages');

        evtSource.onmessage = function(event) {
            const newElement = document.createElement("p");
            const eventObject = JSON.parse(event.data);
            newElement.textContent = "Message: " + eventObject.message + " at " + eventObject.timestamp;
            messages.appendChild(newElement);
        };
    
</script>
</body>
</html>

当我们在浏览器中访问运行在 localhost: 3000 端口的客户端页面时,页面将会以 流式模式 逐步渲染服务端返回的结果:


图片


需要注意的是,为了保证使用 SSE 通信协议传输的数据能被客户端正确的接收,服务端和客户端在发送数据和接收数据应该遵循以下规范:


服务端基本响应格式

SSE 响应主要由一系列以两个换行符分隔的事件组成。每个事件可以包含以下字段:


data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。

字段之间用单个换行符分隔,而事件之间用两个换行符分隔。


客户端处理格式

客户端使用 EventSource 接口监听 SSE 消息:


const evtSource = new EventSource('path/to/sse');
evtSource.onmessage = function(event) {
    console.log(event.data); // 处理收到的数据
};

SSE 应用场景


SSE 作为基于 http 协议由服务端向客户端单向推送消息的通信技术,对于需要服务端主动推送消息的场景来说,是非常适合的:


图片


SSE 兼容性


图片


可以发现,除了 IE 和低版本的主流浏览器,目前市面上绝大多数浏览器都支持 SSE 通信。


SSE 与 WebSocket 对比


看完 SSE 的使用方式后,细心的同学应该发现了:


SSE 的通信方式和 WebSocket 很像啊,而且 WebSocket 还支持双向通信,为什么不直接使用 WebSocket ?


下表展示了两者之间的对比:


特性/因素SSEWebSockets
协议基于HTTP,使用标准HTTP连接单独的协议(ws:// 或 wss://),需要握手升级
通信方式单向通信(服务器到客户端)全双工通信
数据格式文本(UTF-8编码)文本或二进制
重连机制浏览器自动重连需要手动实现重连机制
实时性高(适合频繁更新的场景)非常高(适合高度交互的实时应用)
浏览器支持良好(大多数现代浏览器支持)非常好(几乎所有现代浏览器支持)
适用场景实时通知、新闻feed、股票价格等需要从服务器推送到客户端的场景在线游戏、聊天应用、实时交互应用
复杂性较低,易于实现和维护较高,需要处理连接的建立、维护和断开
兼容性和可用性基于HTTP,更容易通过各种中间件和防火墙可能需要配置服务器和网络设备以支持WebSocket
服务器负载适合较低频率的数据更新适合高频率消息和高度交互的场景

可以发现,SSE 与 WebSocket 各有优缺点,对于需要客户端与服务端高频交互的场景,WebSocket 确实更适合;但对于只需要服务端单向数据传输的场景,SSE 确实能耗更低,且不需要客户端感知


参考文档


developer.mozilla.org/zh-CN/docs/…


作者:veneno
来源:juejin.cn/post/7355666189475954725
收起阅读 »

高德地图 JS API key 的保护,安全密钥的使用方案

背景 因为高德地图的 key 被盗用,导致额度不耗尽。增加了不必要的成本,所以对 key 的保护尤为重要。 目前情况 现在项目中使用高德地图是直接将 key 写在代码中。 在调用高德 api 的时候,key 会明文拼接在请求地址上,因此会被别有用心的人利用。...
继续阅读 »

背景


因为高德地图的 key 被盗用,导致额度不耗尽。增加了不必要的成本,所以对 key 的保护尤为重要。


目前情况


现在项目中使用高德地图是直接将 key 写在代码中。
carbon.png


在调用高德 api 的时候,key 会明文拼接在请求地址上,因此会被别有用心的人利用。


解决方案


业务运营多年,高德地图的 key 已是多年前创建的,所以第一步就是创建一个新的 key。
Snipaste_2024-08-20_15-50-12.png


明文密钥配合域名白名单


2021年12月02日以后创建的 key 需要配合安全密钥一起使用,而且添加了域名白名单配置。
Snipaste_2024-08-20_15-50-48.png
项目代码做个简单的修改即可:
carbon.png
如果在域名白名单中的调用接口能正常使用,如域名不在白名单中,则提示没有权限。
Snipaste_2024-08-20_11-55-32.png
从此看已经起到了限制作用,但实际是防君子不防小人的方案。不建议在生产环境使用,至于原因,你琢磨琢磨。


代理转发请求


因为需要 key 需要配合安全密钥一起使用,不然就会提示没有权限,所以只需要将安全密钥“隐藏”起来就可以了。
Snipaste_2024-08-20_16-03-27.png
请求会将 key 和安全密钥明文拼接在一起,为了将安全密钥“隐藏”起来,只需要将请求代理到自己的服务器上,然后在服务器上将安全密钥拼接上。


以 Nginx 为例:
carbon.png


项目代码配置代理地址即可:
carbon.png


到处,完美收官。


后记


个人项目,可以随意玩耍。公司项目凡是涉及到钱财的东西都要谨慎一些,不要低估灰产的能力。


作者:前端提桶人
来源:juejin.cn/post/7405777954516025370
收起阅读 »

整理最近的生活

web
Hi,见到你真好 :)写在开始自从去年 8月 搬到 上海 之后,就很少再写文章,很多的思路都是断断续续,导致也不知道该写点什么,所以最后是是草稿攒了一堆,大纲整整齐齐,内容空空如也,甚是尴尬。时间长了后,逐渐就有点开摆的心态。也有点理解为什么很多技术同学突然就...
继续阅读 »

Hi,见到你真好 :)

写在开始

自从去年 8月 搬到 上海 之后,就很少再写文章,很多的思路都是断断续续,导致也不知道该写点什么,所以最后是是草稿攒了一堆,大纲整整齐齐,内容空空如也,甚是尴尬。

时间长了后,逐渐就有点开摆的心态。也有点理解为什么很多技术同学突然就不更新了。🫨

不过最近好在有些念头又开始跃跃欲试,故想着写一些东西,活跃活跃生锈的脑子🫡。

故本篇,其实算是一个随记,想到哪里就写到哪里,不包含任何技术指南。

搬家三两事

背景

因为当时是需要从北京搬到上海,家里还有两只猫以及超级多的行李,故需要考虑的事情有下面几点

  1. 琐碎的行李怎么处理?
  2. 如何将 两只猫 安全的送到上海?
  3. 升降桌、工学椅、冰柜、猫厕所等大件怎么处理?

头脑风暴

  1. 两只猫托运走

    成本过高,一只平均需要 1000+ ,以及需要疫苗齐全,以及需要提前7天以上准备。

  2. 升降桌、工学椅、冰柜出二手

    出二手 = 5折以下,最主要是刚买没半年。😑

  3. 行李快递走?

    货拉拉跨城搬运+快递一部分。

上述总费用: 托运🐱2000 + 二手折损(3000) + 货拉拉(4000+) + 动态费用1000 = 9500 左右。

备注点:

  1. 托运猫的安全性;
  2. 如果喊货拉拉,那就不需要二手回收;

算完上述费用之后,我忍不住拍了一下家里两只,嘴里嚷嚷着:要不是你两,我何至于如此!!!

最终解决

小红书约了一个跨城搬家师傅,车型依维柯(长4.9,宽1.9,高2.2),最后只花了 3500 解决了。

关于爱好的倒腾

世界奇奇怪怪,生活慢慢悠悠。

有时候会想,人的一生难得有几个爱好,那可能就会包含折腾 电子小垃圾:)

下面列一下今年折腾过的一些小垃圾:

  • Pixel 7 绿色
  • 戴尔 U2723QX
  • ikbc 高达97键盘
  • ipadAir M1丐版
  • PS5 国行双手柄Slim
  • Studio Display 带升降
  • 富士 XT-3、35-1.4镜头
  • MacMini M1 16+256 丐版

关于相机

一直以来,其实都比较喜欢富士相机的直出,主要因为自己较懒。所以对于学习修图,实在是提不起感觉,而对于摄像的技巧,也只是草草了解几个构图方式,也谈不上研究,故富士就比较适合我这种[懒人]。

但其实这个事情的背景是,在最开始接触相机时,大概是21年,那时想买富士 xs-10,结果因为疫情缺货,国内溢价到了1w,属实是离谱。所以当时就买了佳能 M6 Mark2

等过了半年左右,觉得佳能差点意思,就又换了索尼的 A7C,对焦嘎嘎猛,主打的就是一个激情拍摄,后期就是套个滤镜就行🫡。

等新手福利期一过,时间线再往后推半年,相机开始吃土,遂出了二手👀。

来上海后,又想起了相机,故在闲鱼上又收了一个富士XT3,属于和xs20同配置(少几个滤镜),主打的就是一个拍到就是赚到(当然现在也是吃土🤡)。

关于PS5

因为21年淘过一个 PS4 Pro ,平时也是处于吃灰,玩的最多的反而是 双人成行(现在也没和老婆打完,80%)😬。

但抵不住冲动的心,没事就会看几眼 PS5 ,为此,老婆专门买了一个,以解我没事的念想。

不过真得知快递信息后,还是心里有点不舍。就以家里还有一个在吃土,买这个没啥用为由退掉🫨。

抵得过暂时,抵不过长久,过了一段时间,念头又上来了,没事又翻起了pdd和二手鱼。

于是,在一个风和日丽的中午,激情下单。

结果卖家的名字居然和我只有一字之差,真的是造化弄人。🤡

到手之后,每周的愿望就是能在周末打一会游戏,结果很难有实现过,唯一一次畅玩 [潜水员戴夫🐟],结果导致身心疲惫。

ps: 截止写完这篇时候,最近在爽玩黑神话悟空,故真正实现了使用。

最近抽空在打大镖客2和黑悟空,遂带上几张图。

IMG_8222

image-20240825173156146

关于小主机

之前因为只有一个笔记本,上下班都需要背笔记本,遇到冬天还好,到了夏天,就非常反感,故最近诞生出了买一个 小主机 的想法。

因为不玩pc游戏,故 winodws 系天然不用选择,当然也就不用考虑同事推荐的 零刻 这种小主机,直接去搞一个 mac mini 即可。

最后综合对比了一下,觉得 Mac Mini(M1-16/256) 即可,闲鱼只需要3000左右即可。

对 M1 及以后的Mac设备而言,RAM>CPU>固态 ,故 cpu 的性能对我而言足矣。

故而以 16g 作为标准,而存储方面,可以考虑外接固态做扩展盘解决,这套方案是性价比最高的。🫡

有了上面的结论之后,就直接去闲鱼收,最后 3100 收了一个 MacMini M1/16g

然后又在京东买了一个 硬盘盒子(海康威视20Gbps)+雷神固态(PR5000/读写4.5k)。

本来想去买一个 40Gbps 的硬盘盒,结果一看价,399起步,有点夸张(我盘才4xx),遂放弃。

Tips:

  1. 不要轻易把系统装到扩展固态(特别是硬盘盒速度不够40Gbps时)上。
  • 开机引导会变慢(如果一直不关机当我没说);
  • 如果硬盘散热跟不上,系统会卡顿,如同ANR了一样;
  1. 因为 MacMini 雷电口 不支持 usb3.2 Gen2x2,在不满足雷电口的情况下,最快读写 只有1k。故导致硬盘速度只能到达 1k 读写,也就是硬盘盒的一半速度

最后全家福如下:

image.png

关于显示器

最近一年连着用过了 3 个显示器,价位一路从 1K -> 1.2W,也算又多了一点点经验分享。

先说背景:因为没有游戏与高刷需求,更多的是追求接近Mac体验(用同事话说就是,被果子惯得),故下面的评测仅限于特定范围。

本次的参照物:MacBook Pro14寸自带的 XDR显示器、峰值亮度 1600nt

Redmi 27/4k (1.5k)

HDR400、65W Typc、95%P3、E<2

全功能typc、支持kvm,色彩模式有forMac。

塑料机身,但设计挺好看的,观感不错,for Mac模式 能接近 70% 体验;

仔细对比,与mac屏幕差距最大的是通透度与色准问题,如果说MacBook是 100% 通透,红米只有 60-70% 之间;

后期偶尔会出现连接不稳定,屏幕闪烁问题,时好时不好,猜测可能与 typc65w 电压不稳有关。

综合来说,这个价位,性价比非常之高

戴尔U2723QX (3k)

HDR400、90W Typc、98%P3

全功能typc、支持kvm、接口大满贯,多到发指。

全金属机身,屏幕是 IPS Black ,也就是 LG 的 NanoIPS 面板,不过也算是一块老屏幕了。

整体体验下来不错,有接近MacBook 80% 的体验,色准什么的都很ok,在使用过程中,也没遇见过任何问题。

综合来说,算是 普通消费级代码显示器的王者 ,不过如果要提性价比,可能不如同款屏幕的 LG。

至于戴尔的售后,因为没体验过,所以难评。

Studio Display (1.2w)

5k分辨率、600nits、A13、六扬声器、4麦克风、自带摄像头

接口方面:雷电3(40G) + 3 x typc(10G)

Mac系列的最佳搭配,最接近 MacBook 的色准,非常通透,95%+ 的接近水平,亮度差点,不过已经够了;

整体来说,如果对屏幕要求,或者眼睛比较敏感,那么这个显示器是比较不错的选择(别忘了开原彩+自动亮度,看习惯了后,还是很舒服)。

至于不足之处,可能就只有价格这一个因素。

家用设备的倒腾

来上海之后,家庭设备倒腾的比较少,少有的几个物件是:

  • 小米除湿机 22L;
  • 追觅洗地机 H30Mix;
  • 小米55寸 MiniLed 电视;

关于除湿机

当时刚来上海,作为一个土生土长的北方人,遇到黄梅天,那感觉,简直浑身难受,一个字 黏,两个字 闷热

故当时紧急下单了一款除湿机,一晚上可以除满满一桶,实测大概4-5L,最高档开1小时左右,家里基本就可以感受到干爽,比较适合40m的小家使用。再说说缺点:

  • 吵!
  • 如果没有接下水管,可能需要隔两天换一次水;

再说说现状,成功实现 100% 吃土状态,几乎毫无悬念,因为空调更省事。。。

最后给一些北方人首次去南方居住的建议:

  • 不要选3层以下,太潮;
  • 注意白天看房,看看光线如何;
  • 注意附近切记不是建筑工地等等;
  • 上海合租比较便宜,自如挺合适;

关于洗地机

刚到上海时,之前的 米家扫拖机器人 也一起带过来了,但因为实在 版本太旧,逐渐不堪大用 ,只能用勉强可用这个词来形容,而且特别容易 积攒头发加手动加水清洁 ,时间久了,就比较烦。

故按照我的性格,没事就在看新的替代物件,最开始锁定的是追觅扫拖机器人,但最后经过深思熟虑,觉得家里太小(60m) ,故扫地机器人根本转不开腿,可能咣咣撞椅子了,故退而求其次,去看洗地机。入手了追觅的 H30mix洗地吸尘都能干

最后经过实际证明:洗地机就老老实实洗地,吸尘还是交给专门的吸尘器,主要是拆卸太麻烦🤡。故家里本来已经半个身子准备退休的德尔玛又被强行续命了一波。

再说优点,真的很好用,拖完地放架子上自动洗烘一体,非常方便。实测拖的比我干净,唯一缺点就是,每天洗完需要手动倒一下脏水(不倒可能会反味)。

关于电视

来上海后,一直想换个电视打游戏用,就没事看了看电视。因为之前的 Tcl 邮给了岳父老房子里,于是按耐不住的心又开始躁动了,故某个夜晚就看了下电视,遂对比后下单了小米的55寸 miniLed。考虑到家里地方不是很大,故也顺带卖了一个可移动的支架。

现在电视的价格是真的便宜,Miniled 都被小米干到了 2k 附近,但一分钱一分货,纸面参数终究是纸面参数,最后看看实际观感,也就那样。

image.png

关于一些想法

人生不过几十载,如果工作要占用掉最宝贵的20年华,那未免太过于糟糕。

不知为何,最近总感觉上班的时间过得尤为快,每周过了周二后,周五的下一步就又要到了,下个月初也越来越近了。

来上海后,几乎每天都会和老婆晚上下楼走走,近的时候绕着小区,远的时候绕着小区外面的路。起初刚来上海时,脑子里依然会有过几年会北京的想法,但在上海有段时间后,这个想法就变得没那么重了,直到现在,我两都变成了留在上海也许更好(仔细算了算)😐。

写在最后

兜兜转转,这篇也是写了近一个月,属于是想起来写一点,接下来会更新的频繁一点。

下次再见,朋友们 👋

关于我

我是 Petterp ,一个 Android 工程师。如果本文,你觉得写的还不错,不妨点个赞或者收藏,你的支持,是我持续创作的最大鼓励!


作者:Petterp
来源:juejin.cn/post/7406258856953790515
收起阅读 »

if-else嵌套太深怎么办?

web
在前端开发中,if-else 嵌套过深往往会导致代码可读性下降、维护难度增加,甚至引发潜在的逻辑错误。本文将从一个典型的深层 if-else 嵌套案例出发,逐步分析并探讨多种优化策略,帮助开发者解决这一问题。 一、深层 if-else 嵌套的案例 假设我们正在...
继续阅读 »

在前端开发中,if-else 嵌套过深往往会导致代码可读性下降、维护难度增加,甚至引发潜在的逻辑错误。本文将从一个典型的深层 if-else 嵌套案例出发,逐步分析并探讨多种优化策略,帮助开发者解决这一问题。


一、深层 if-else 嵌套的案例


假设我们正在开发一个处理订单状态的功能,根据订单的不同状态执行相应的操作。下面是一个典型的 if-else 嵌套过深的代码示例:


function processOrder(order) {
if (order) {
if (order.isPaid) {
if (order.hasStock) {
if (!order.isCanceled) {
// 处理已付款且有库存的订单
return 'Processing paid order with stock';
} else {
// 处理已取消的订单
return 'Order has been canceled';
}
} else {
// 处理库存不足的订单
return 'Out of stock';
}
} else {
// 处理未付款的订单
return 'Order not paid';
}
} else {
// 处理无效订单
return 'Invalid order';
}
}
****

这段代码展示了多个条件的嵌套判断,随着条件的增多,代码的层级不断加深,使得可读性和可维护性大幅降低。


二、解决方案


1. 使用早返回


早返回是一种有效的方式,可以通过尽早退出函数来避免不必要的嵌套。


function processOrder(order) {
if (!order) {
return 'Invalid order';
}
if (!order.isPaid) {
return 'Order not paid';
}
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}


通过早返回,条件判断被简化为一系列独立的判断,减少了嵌套层级,代码更直观。


2. 使用对象字面量或映射表


当条件判断基于某个特定的值时,可以利用对象字面量替代 if-else


const orderStatusActions = {
'INVALID': () => 'Invalid order',
'NOT_PAID': () => 'Order not paid',
'OUT_OF_STOCK': () => 'Out of stock',
'CANCELED': () => 'Order has been canceled',
'DEFAULT': () => 'Processing paid order with stock',
};

function processOrder(order) {
if (!order) {
return orderStatusActions['INVALID']();
}
if (!order.isPaid) {
return orderStatusActions['NOT_PAID']();
}
if (!order.hasStock) {
return orderStatusActions['OUT_OF_STOCK']();
}
if (order.isCanceled) {
return orderStatusActions['CANCELED']();
}
return orderStatusActions['DEFAULT']();
}


使用对象字面量将条件与行为进行映射,使代码更加模块化且易于扩展。


3. 使用策略模式


策略模式可以有效应对复杂的多分支条件,通过定义一系列策略类,将不同的逻辑封装到独立的类中。


class OrderProcessor {
constructor(strategy) {
this.strategy = strategy;
}

process(order) {
return this.strategy.execute(order);
}
}

class PaidOrderStrategy {
execute(order) {
if (!order.hasStock) {
return 'Out of stock';
}
if (order.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}

class InvalidOrderStrategy {
execute(order) {
return 'Invalid order';
}
}

class NotPaidOrderStrategy {
execute(order) {
return 'Order not paid';
}
}

// 使用策略模式
const strategy = order ? (order.isPaid ? new PaidOrderStrategy() : new NotPaidOrderStrategy()) : new InvalidOrderStrategy();
const processor = new OrderProcessor(strategy);
processor.process(order);


策略模式将不同逻辑分散到独立的类中,避免了大量的 if-else 嵌套,增强了代码的可维护性。


4. 使用多态


通过多态性,可以通过继承和方法重写替代 if-else 条件分支。


优化后的代码:


class Order {
process() {
throw new Error('This method should be overridden');
}
}

class PaidOrder extends Order {
process() {
if (!this.hasStock) {
return 'Out of stock';
}
if (this.isCanceled) {
return 'Order has been canceled';
}
return 'Processing paid order with stock';
}
}

class InvalidOrder extends Order {
process() {
return 'Invalid order';
}
}

class NotPaidOrder extends Order {
process() {
return 'Order not paid';
}
}

// 通过多态处理订单
const orderInstance = new PaidOrder(); // 根据order实例化相应的类
orderInstance.process();


多态性允许我们通过不同的子类实现不同的逻辑,从而避免在同一个函数中使用大量的 if-else


5. 使用函数式编程技巧


函数式编程中的 map, filter, 和 reduce 可以帮助我们避免复杂的条件判断。


优化后的代码:


const orderProcessors = [
{condition: (order) => !order, process: () => 'Invalid order'},
{condition: (order) => !order.isPaid, process: () => 'Order not paid'},
{condition: (order) => !order.hasStock, process: () => 'Out of stock'},
{condition: (order) => order.isCanceled, process: () => 'Order has been canceled'},
{condition: () => true, process: () => 'Processing paid order with stock'},
];

const processOrder = (order) => orderProcessors.find(processor => processor.condition(order)).process();


通过 findfilter 等函数式编程方法,我们可以避免嵌套的 if-else 语句,使代码更加简洁和易于维护。


三、总结


if-else 嵌套过深的问题是前端开发中常见的挑战。通过本文提供的多种解决方案,如早返回、对象字面量、策略模式、多态和函数式编程技巧,开发者可以根据实际需求选择合适的优化方案,从而提高代码的可读性、可维护性和性能。


希望这些方法能对你的开发工作有所帮助,欢迎在评论区分享你的经验与想法!


作者:争取不脱发的程序猿
来源:juejin.cn/post/7406538050228633641
收起阅读 »

Oracle开始严查Java许可!

web
0x01、 前段时间在论坛里就看到一个新闻,说“Oracle又再次对Java下手,开始严查Java许可,有企业连夜删除JDK”,当时就曾在网上引起了一阵关注和讨论。 这不最近在科技圈又看到有媒体报道,Oracle再次严查,对于Java许可和版权的审查越来越严格...
继续阅读 »

0x01、


前段时间在论坛里就看到一个新闻,说“Oracle又再次对Java下手,开始严查Java许可,有企业连夜删除JDK”,当时就曾在网上引起了一阵关注和讨论。


这不最近在科技圈又看到有媒体报道,Oracle再次严查,对于Java许可和版权的审查越来越严格了。


其实很早之前就有看到新闻报道说,甲骨文公司Oracle已经开始将Java纳入其软件许可审查中,并且对一些公司的Java采用情况开启审计,目的是找出那些处于不合规边缘或已经违规的客户。


之前主要还是针对一些小公司发出过审查函件,而现在,甚至包括财富200强在内的一些组织或公司都收到了来自Oracle有关审查方面的信件。



0x02、


还记得去年上半年的时候,Oracle就曾发布过一个PDF格式的新版Java SE收费政策《Oracle Java SE Universal Subscription Global Price List (PDF)》。



打开那个PDF,在里面可以看到Oracle新的Java SE通用订阅全球价目表:



表格底部还举了一个具体计费的例子。


比方说一个公司有28000名总雇员,里面可能包含有23000名全职、兼职、临时雇员,以及5000其他类型员工(比如说代理商、合约商、咨询顾问),那这个总价格是按如下方式进行计算:


28000 * 6.75/12个月=2268000/月 * 12个月 = 2268000/年


合着这个新的收费标准是直接基于公司里总的员工数来进行计算的,而不仅仅是使用Java SE的员工数。


这样一来,可能就会使企业在相同软件的的使用情况下会多出不少费用,从而增加软件成本。


看到这里不得不说,Oracle接手之后把Java的商业化运作这块整得是明明白白的。


0x03、


众所周知,其实Java最初是由Sun公司的詹姆斯·高斯林(James Gosling,后来也被称为Java之父)及其团队所研发的。



并且最开始名字并不叫Java,而是被命名为:Oak,这个名字得自于 Gosling 想名字时看到了窗外的一棵橡树。



就在 Gosling 的团队即将发布成果之前,又出了个小插曲——Oak 竟然是一个注册商标。Oak Technology(OAKT)是一家美国半导体芯片制造商,Oak 是其注册商标。


既然不能叫Oak,那应该怎么命名好呢?


后来 Gosling 看见了同事桌上有一瓶咖啡,包装上写着 Java,于是灵感一现。至此,Java语言正式得名,并使用至今。


1995年5月,Oak语言才更名为Java(印度尼西亚爪哇岛的英文名称,因盛产咖啡而闻名),并于当时的SunWorld大会上发布了JAVA 1.0,而且那句“Write Once,Run Anywhere”的slogan也是那时候推出的。



此后,Java语言一直由Sun公司来进行维护开发,一直到早期的JDK 7。


2009年4月,Oracle以74亿美元现金收购了Sun公司,至此一代巨头基本没落。


与此同时,Java商标也被列入Oracle麾下,成为了Oracle的重要资源。



众所周知,Oracle接手Java之后,就迅速开始了商业化之路的实践,也于后续推出了一系列调整和改革的操作。


其实Oracle早在2017年9月就宣布将改变JDK版本发布周期。新版本发布周期中,一改原先以特性驱动的发布方式,而变成了以时间为驱动的版本迭代。


也即:每6个月会发布一个新的Java版本,而每3年则会推出一个LTS版本。



而直到前段时间,Java 22都已经正式发布了。



0x04、


那针对Oracle这一系列动作,以及新的定价策略和订阅问题,有不少网友讨论道,那就不使用Oralce JDK,切换到OpenJDK,或者使用某些公司开源的第三方JDK。


众所周知,OpenJDK是一个基于GPL v2 许可的开源项目,自Java 7开始就是Java SE的官方参考实现。


既然如此,也有不少企业或者组织基于OpenJDK从而构建了自己的JDK版本,这些往往都是基于OpenJDK源码,然后增加或者说定制一些自己的专属内容。


比如像阿里的Dragonwell,腾讯的Kona,AWS的Amazon Corretto,以及Azul提供的Zulu JDK等等,都是这类典型的代表。




它们都是各自根据自身的业务场景和业务需求并基于OpenJDK来打造推出的开源JDK发行版本,像这些也都是可以按需去选用的。


文章的最后,也做个小调查:


大家目前在用哪款JDK和版本来用于开发环境或生产环境的呢?



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



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

网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!

web
网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知! 大家好,我是蒜鸭。今天我们来聊聊如何让网页像 QQ 那样在右下角弹出消息通知。这个功能不仅能提升用户体验,还能增加网站的互动性。让我们一起探索如何在网页中实现这个酷炫的功能吧! 1. 为什么需要网页通知...
继续阅读 »

网页也能像 QQ 一样发出右下角消息?轻松实现桌面通知!


大家好,我是蒜鸭。今天我们来聊聊如何让网页像 QQ 那样在右下角弹出消息通知。这个功能不仅能提升用户体验,还能增加网站的互动性。让我们一起探索如何在网页中实现这个酷炫的功能吧!


1. 为什么需要网页通知?


在当今信息爆炸的时代,获取用户注意力变得越来越困难。传统的网页通知方式,如弹窗或页面内提示,往往会打断用户的浏览体验。而类似 QQ 那样的右下角消息通知,既能及时传递信息,又不会过分干扰用户,可以说是一种相当优雅的解决方案。


实现这种通知功能,我们有两种主要方式:使用 Web Notifications API 或自定义 CSS+JavaScript 实现。接下来,我们将详细探讨这两种方法的实现过程、优缺点以及适用场景。


2. 使用 Web Notifications API


2.1 Web Notifications API 简介


Web Notifications API 是现代浏览器提供的一个强大功能,它允许网页向用户发送通知,即使在用户没有打开网页的情况下也能工作。这个 API 的使用非常简单,但功能却十分强大。


2.2 基本实现步骤



  1. 检查浏览器支持

  2. 请求用户授权

  3. 创建并显示通知


让我们来看看具体的代码实现:


// 检查浏览器是否支持通知
if ("Notification" in window) {
// 请求用户授权
Notification.requestPermission().then(function (permission) {
if (permission === "granted") {
// 创建并显示通知
var notification = new Notification("Hello from Web!", {
body: "这是一条来自网页的通知消息",
icon: "path/to/icon.png"
});

// 点击通知时的行为
notification.onclick = function() {
window.open("https://example.com");
};
}
});
}

2.3 优点和注意事项


优点:

– 原生支持,无需额外库

– 可以在用户未浏览网页时发送通知

– 支持富文本和图标


注意事项:

– 需要用户授权,一些用户可能会拒绝

– 不同浏览器的显示样式可能略有不同

– 过度使用可能会引起用户反感


3. 自定义 CSS+JavaScript 实现


如果你想要更多的样式控制,或者希望通知始终显示在网页内,那么使用自定义的 CSS+JavaScript 方案可能更适合你。


3.1 基本思路



  1. 创建一个固定位置的 div 元素作为通知容器

  2. 使用 JavaScript 动态创建通知内容

  3. 添加动画效果使通知平滑显示和消失


3.2 HTML 结构


<div id="notification-container"></div>

3.3 CSS 样式


#notification-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}

.notification {
background-color: #f8f8f8;
border-left: 4px solid #4CAF50;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
padding: 16px;
margin-bottom: 10px;
width: 300px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease-in-out;
}

.notification.show {
opacity: 1;
transform: translateX(0);
}

.notification-title {
font-weight: bold;
margin-bottom: 5px;
}

.notification-body {
font-size: 14px;
}

3.4 JavaScript 实现


function showNotification(title, message, duration = 5000) {
const container = document.getElementById('notification-container');

const notification = document.createElement('div');
notification.className = 'notification';

const titleElement = document.createElement('div');
titleElement.className = 'notification-title';
titleElement.textContent = title;

const bodyElement = document.createElement('div');
bodyElement.className = 'notification-body';
bodyElement.textContent = message;

notification.appendChild(titleElement);
notification.appendChild(bodyElement);

container.appendChild(notification);

// 触发重绘以应用初始样式
notification.offsetHeight;

// 显示通知
notification.classList.add('show');

// 设置定时器移除通知
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
}, duration);
}

// 使用示例
showNotification('Hello', '这是一条自定义通知消息');

3.5 优点和注意事项


优点:

– 完全可定制的外观和行为

– 不需要用户授权

– 可以轻松集成到现有的网页设计中


注意事项:

– 仅在用户浏览网页时有效

– 需要考虑移动设备的适配

– 过多的通知可能会影响页面性能


4. 高级技巧和最佳实践


4.1 通知分级


根据通知的重要性进行分级,可以使用不同的颜色或图标来区分:


function showNotification(title, message, level = 'info') {
// ... 前面的代码相同

let borderColor;
switch(level) {
case 'success':
borderColor = '#4CAF50';
break;
case 'warning':
borderColor = '#FFC107';
break;
case 'error':
borderColor = '#F44336';
break;
default:
borderColor = '#2196F3';
}

notification.style.borderLeftColor = borderColor;

// ... 后面的代码相同
}

// 使用示例
showNotification('成功', '操作已完成', 'success');
showNotification('警告', '请注意...', 'warning');
showNotification('错误', '出现问题', 'error');

4.2 通知队列


为了避免同时显示过多通知,我们可以实现一个简单的通知队列:


const notificationQueue = [];
let isShowingNotification = false;

function queueNotification(title, message, duration = 5000) {
notificationQueue.push({ title, message, duration });
if (!isShowingNotification) {
showNextNotification();
}
}

function showNextNotification() {
if (notificationQueue.length === 0) {
isShowingNotification = false;
return;
}

isShowingNotification = true;
const { title, message, duration } = notificationQueue.shift();
showNotification(title, message, duration);

setTimeout(showNextNotification, duration + 300);
}

// 使用示例
queueNotification('通知1', '这是第一条通知');
queueNotification('通知2', '这是第二条通知');
queueNotification('通知3', '这是第三条通知');

4.3 响应式设计


为了确保通知在各种设备上都能正常显示,我们需要考虑响应式设计:


@media (max-width: 768px) {
#notification-container {
left: 20px;
right: 20px;
bottom: 20px;
}

.notification {
width: auto;
}
}

4.4 无障碍性考虑


为了提高通知的可访问性,我们可以添加 ARIA 属性和键盘操作支持:


function showNotification(title, message, duration = 5000) {
// ... 前面的代码相同

notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');

const closeButton = document.createElement('button');
closeButton.textContent = '×';
closeButton.className = 'notification-close';
closeButton.setAttribute('aria-label', '关闭通知');

closeButton.addEventListener('click', () => {
notification.classList.remove('show');
setTimeout(() => {
container.removeChild(notification);
}, 300);
});

notification.appendChild(closeButton);

// ... 后面的代码相同
}

5. 性能优化与注意事项


在实现网页通知功能时,我们还需要注意以下几点:



  1. 防抖和节流:对于频繁触发的事件(如实时通知),使用防抖或节流技术可以有效减少不必要的通知显示。

  2. 内存管理:确保在移除通知时,同时清理相关的事件监听器和 DOM 元素,避免内存泄漏。

  3. 优雅降级:对于不支持 Web Notifications API 的浏览器,可以降级使用自定义的 CSS+JavaScript 方案。

  4. 用户体验:给用户提供控制通知显示的选项,如允许用户设置通知的类型、频率等。

  5. 安全考虑:在使用 Web Notifications API 时,确保只在 HTTPS 环境下请求权限,并尊重用户的权限设置。


网页通知是一个强大的功能,能够显著提升用户体验和网站的互动性。无论是使用 Web Notifications API 还是自定义的 CSS+JavaScript 方案,都能实现类似 QQ 那样的右下角消息通知。选择哪种方式取决于你的具体需求和目标用户群。通过合理使用通知功能,你可以让你的网站变得更加生动和用户友好。



作者:Syferie
来源:juejin.cn/post/7403283321793314850
收起阅读 »

localhost和127.0.0.1的区别是什么?

今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个: localhost和127.0.0.1的区别是什么? 前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显...
继续阅读 »

今天在网上逛的时候看到一个问题,没想到大家讨论的很热烈,就是标题中这个:


localhost和127.0.0.1的区别是什么?



前端同学本地调试的时候,应该没少和localhost打交道吧,只需要执行 npm run 就能在浏览器中打开你的页面窗口,地址栏显示的就是这个 http://localhost:xxx/index.html


可能大家只是用,也没有去想过这个问题。


联想到我之前合作过的一些开发同学对它们俩的区别也没什么概念,所以我觉得有必要普及下。


localhost是什么呢?


localhost是一个域名,和大家上网使用的域名没有什么本质区别,就是方便记忆。


只是这个localhost的有效范围只有本机,看名字也能知道:local就是本地的意思。


张三和李四都可以在各自的机器上使用localhost,但获取到的也是各自的页面内容,不会相互打架。


从域名到程序


要想真正的认清楚localhost,我们还得从用户是如何通过域名访问到程序说起。


以访问百度为例。


1、当我们在浏览器输入 baidu.com 之后,浏览器首先去DNS中查询 baidu.com 的IP地址。


为什么需要IP地址呢?打个比方,有个人要寄快递到你的公司,快递单上会填写:公司的通讯地址、公司名称、收件人等信息,实际运输时快递会根据通信地址进行层层转发,最终送到收件人的手中。网络通讯也是类似的,其中域名就像公司名称,IP地址就像通信地址,在网络的世界中只有通过IP地址才能找到对应的程序。


DNS就像一个公司黄页,其中记录着每个域名对应的IP地址,当然也有一些域名可能没做登记,就找不到对应的IP地址,还有一些域名可能会对应多个IP地址,DNS会按照规则自动返回一个。我们购买了域名之后,一般域名服务商会提供一个域名解析的功能,就是把域名和对应的IP地址登记到DNS中。


这里的IP地址从哪里获取呢?每台上网的电脑都会有1个IP地址,但是个人电脑的IP地址一般是不行的,个人电脑的IP地址只适合内网定位,就像你公司内部的第几栋第几层,公司内部人明白,但是直接发给别人,别人是找不到你的。如果你要对外部提供服务,比如百度这种,你就得有公网的IP地址,这个IP地址一般由网络服务运营商提供,比如你们公司使用联通上网,那就可以让联通给你分配一个公网IP地址,绑定到你们公司的网关服务器上,网关服务器就像电话总机,公司内部的所有网络通信都要通过它,然后再在网关上设置转发规则,将网络请求转发到提供网络服务的机器上。


2、有了IP地址之后,浏览器就会向这个IP地址发起请求,通过操作系统打包成IP请求包,然后发送到网络上。网络传输有一套完整的路由协议,它会根据你提供的IP地址,经过路由器的层层转发,最终抵达绑定该IP的计算机。


3、计算机上可能部署了多个网络应用程序,这个请求应该发给哪个程序呢?这里有一个端口的概念,每个网络应用程序启动的时候可以绑定一个或多个端口,不同的网络应用程序绑定的端口不能重复,再次绑定时会提示端口被占用。通过在请求中指定端口,就可以将消息发送到正确的网络处理程序。


但是我们访问百度的时候没有输入端口啊?这是因为默认不输入就使用80和443端口,http使用80,https使用443。我们在启动网络程序的时候一定要绑定一个端口的,当然有些框架会自动选择一个计算机上未使用的端口。



localhost和127.0.0.1的区别是什么?


有了上边的知识储备,我们就可以很轻松的搞懂这个问题了。


localhost是域名,上文已经说过了。


127.0.0.1 呢?是IP地址,当前机器的本地IP地址,且只能在本机使用,你的计算机不联网也可以用这个IP地址,就是为了方便开发测试网络程序的。我们调试时启动的程序就是绑定到这个IP地址的。


这里简单说下,我们经常看到的IP地址一般都是类似 X.X.X.X 的格式,用"."分成四段。其实它是一个32位的二进制数,分成四段后,每一段是8位,然后每一段再转换为10进制的数进行显示。


那localhost是怎么解析到127.0.0.1的呢?经过DNS了吗?没有。每台计算机都可以使用localhost和127.0.0.1,这没办法让DNS来做解析。


那就让每台计算机自己解决了。每台计算机上都有一个host文件,其中写死了一些DNS解析规则,就包括 localhost 到 127.0.0.1 的解析规则,这是一个约定俗成的规则。


如果你不想用localhost,那也可以,随便起个名字,比如 wodehost,也解析到 127.0.0.1 就行了。


甚至你想使用 baidu.com 也完全可以,只是只能自己自嗨,对别人完全没有影响。


域名的等级划分


localhost不太像我们平常使用的域名,比如 http://www.juejin.cn 、baidu.com、csdn.net, 这里边的 www、cn、com、net都是什么意思?localhost为什么不需要?


域名其实是分等级的,按照等级可以划分为顶级域名、二级域名和三级域名...


顶级域名(TLD):顶级域名是域名系统中最高级别的域名。它位于域名的最右边,通常由几个字母组成。顶级域名分为两种类型:通用顶级域名和国家顶级域名。常见的通用顶级域名包括表示工商企业的.com、表示网络提供商的.net、表示非盈利组织的.org等,而国家顶级域名则代表特定的国家或地区,如.cn代表中国、.uk代表英国等。


二级域名(SLD):二级域名是在顶级域名之下的一级域名。它是由注册人自行选择和注册的,可以是个性化的、易于记忆的名称。例如,juejin.cn 就是二级域名。我们平常能够申请到的也是这种。目前来说申请 xxx.com、xxx.net、xxx.cn等等域名,其实大家不太关心其顶级域名com\net\cn代表的含义,看着简短好记是主要诉求。


三级域名(3LD):三级域名是在二级域名之下的一级域名。它通常用于指向特定的服务器或子网。例如,在blog.example.com中,blog就是三级域名。www是最常见的三级域名,用于代表网站的主页或主站点,不过这只是某种流行习惯,目前很多网站都推荐直接使用二级域名访问了。


域名级别还可以进一步细分,大家可以看看企业微信开放平台这个域名:developer.work.weixin.qq.com,com代表商业,qq代表腾讯,weixin代表微信,work代表企业微信,developer代表开发者。这种逐层递进的方式有利于域名的分配管理。


按照上边的等级定义,我们可以说localhost是一个顶级域名,只不过它是保留的顶级域,其唯一目的是用于访问当前计算机。


多网站共用一个IP和端口


上边我们说不同的网络程序不能使用相同的端口,其实是有办法突破的。


以前个人博客比较火的时候,大家都喜欢买个虚拟主机,然后部署个开源的博客程序,抒发一下自己的感情。为了挣钱,虚拟主机的服务商会在一台计算机上分配N多个虚拟主机,大家使用各自的域名和默认的80端口进行访问,也都相安无事。这是怎么做到的呢?


如果你有使用Nginx、Apache或者IIS等Web服务器的相关经验,你可能会接触到主机头这个概念。主机头其实就是一个域名,通过设置主机头,我们的程序就可以共用1个网络端口。


首先在Nginx等Web程序中部署网站时,我们会进行一些配置,此时在主机头中写入网站要使用的域名。


然后Nginx等Web服务器启动的时候,会把80端口占为己有。


然后当某个网站的请求到达Nginx的80端口时,它会根据请求中携带的域名找到配置了对应主机头的网络程序。


然后再转发到这个网络程序,如果网络程序还没有启动,Nginx会把它拉起来。


私有IP地址


除了127.0.0.1,其实还有很多私有IP地址,比如常见的 192.168.x.x。这些私有IP地址大部分都是为了在局域网内使用而预留的,因为给每台计算机都分配一个独立的IP不太够用,所以只要局域网内不冲突,大家就可劲的用吧。你公司可以用 192.168.1.1,我公司也可以用192.168.1.1,但是如果你要访问我,就得通过公网IP进行转发。


大家常用的IPv4私有IP地址段分为三类:


A类:从10.0.0.0至10.255.255.255


B类:从172.16.0.0至172.31.255.255


C类:从192.168.0.0至192.168.255.255。


这些私有IP地址仅供局域网内部使用,不能在公网上使用。


--


除了上述三个私有的IPv4地址段外,还有一些保留的IPv4地址段:


用于本地回环测试的127.0.0.0至127.255.255.255地址段,其中就包括题目中的127.0.0.1,如果你喜欢也可以给自己分配一个127.0.0.2的IP地址,效果和127.0.0.1一样。


用于局域网内部的169.254.0.0至169.254.255.255地址段,这个很少接触到,如果你的电脑连局域网都上不去,可能会看到这个IP地址,它是临时分配的一个局域网地址。


这些地址段也都不能在公网上使用。


--


近年来,还有一个现象,就是你家里或者公司里上网时,光猫或者路由器对外的IPv4地址也不是公网IP了,这时候获得的可能是一个类似 100.64.x.x 的地址,这是因为随着宽带的普及,运营商手里的公网IP也不够了,所以运营商又加了一层局域网,而100.64.0.0 这个网段是专门分给运营商做局域网用的。如果你使用阿里云等公有云,一些云产品的IP地址也可能是这个,这是为了将客户的私有网段和公有云厂商的私有网段进行有效的区分。


--


其实还有一些不常见的专用IPv4地址段,完整的IP地址段定义可以看这里:http://www.iana.org/assignments…



IPv6


你可能也听说过IPv6,因为IPv4可分配的地址太少了,不够用,使用IPv6甚至可以为地球上的每一粒沙子分配一个IP。只是喊了很多年,大家还是喜欢用IPv4,这里边原因很多,这里就不多谈了。


IPv6地址类似:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX

它是128位的,用":"分成8段,每个X是一个16进制数(取值范围:0-F),IPv6地址空间相对于IPv4地址有了极大的扩充。比如:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b 就是一个有效的IPv6地址。


关于IPv6这里就不多说了,有兴趣的可以再去研究下。


关注萤火架构,加速技术提升!


作者:萤火架构
来源:juejin.cn/post/7321049446443417638
收起阅读 »

url请求参数带有特殊字符“%、#、&”时,参数被截断怎么办?

web
是的,最近又踩坑了! 事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。 一排查,发现特殊字符“%%%”并未成功传给后端。 我们的这个全局搜索功能是需要跳转页面才能查看到搜索结...
继续阅读 »

是的,最近又踩坑了!


事情是这样的,我们测试小姐姐在一个全局搜索框里输入了一串特殊字符“%%%”,然后进行搜索,结果报错了。而输入常规字符,均可以正常搜索。


一排查,发现特殊字符“%%%”并未成功传给后端。


我们的这个全局搜索功能是需要跳转页面才能查看到搜索结果的。所以,搜索条件是作为参数拼接在页面url上的。


正常的传参:


image.png


当输入的是特殊字符“%、#、&”时,参数丢失


image.png


也就是说,当路由请求参数带有浏览器url中的特殊含义字符时,参数会被截断,无法正常获取参数。


那么怎么解决这个问题呢?


方案一:encodeURIComponent/decodeURIComponent


拼接参数时,利用encodeURIComponent()进行编码,接收参数时,利用decodeURIComponent()进行解码。


// 编码
this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(searchValue)}`});

// 解码
const text = decodeURIComponent(this.$route.query.text)

此方法对绝大多数特殊字符都适用,但是唯独输入“%”进行搜索时不行,报错如下。


image.png


所以在编码之前,还需进行一下如下转换:



this.$router.push({path: `/crm/global-search/search-result?type=${selectValue}&text=${encodeURIComponent(encodeSpecialChar(searchValue))}`});


/**
* @param {*} char 字符串
* @returns
*/

export const encodeSpecialChar = (char) => {
// #、&可以不用参与处理
const encodeArr = [{
code: '%',
encode: '%25'
},{
code: '#',
encode: '%23'
}, {
code: '&',
encode: '%26'
},]
return char.replace(/[%?#&=]/g, ($) => {
for (const k of encodeArr) {
if (k.code === $) {
return k.encode
}
}
})
}


方案二: qs.stringify()


默认情况下,qs.stringify()方法会使用encodeURIComponent方法对特殊字符进行编码,以保证URL的合法性。


const qs = require('qs');

const searchObj = {
type: selectValue,
text: searchValue
};
this.$router.push({path: `/crm/global-search/search-result?${qs.stringify(searchObj)}`});


使用了qs.stringify()方法,就无需使用encodeSpecialChar方法进行转换了。


作者:HED
来源:juejin.cn/post/7332048519156776979
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:



  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。

  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。

  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。

  • Total lines:所有文件的总行数。

  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。

  • Clones found:找到的重复块数量。

  • Duplicated lines:重复的代码行数和占比。

  • Duplicated tokens:重复的token数量和占比。

  • Detection time:检测耗时。


工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:


<!--
// jscpd:ignore-start
-->

<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>
<!--
// jscpd:ignore-end
-->


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

2种纯前端换肤方案

web
前言 换肤功能是一项普遍的需求,尤其是在夜晚,用户更倾向于使用暗黑模式。在我负责的公司项目中,每个项目都有换肤功能的需求。 过去,我主要使用 SCSS 变量,并利用其提供的函数,如 @each、map-get来实现换肤功能。但因其使用成本高,只能适用于SCSS...
继续阅读 »

前言


换肤功能是一项普遍的需求,尤其是在夜晚,用户更倾向于使用暗黑模式。在我负责的公司项目中,每个项目都有换肤功能的需求。


过去,我主要使用 SCSS 变量,并利用其提供的函数,如 @eachmap-get来实现换肤功能。但因其使用成本高,只能适用于SCSS项目,于是后来我改用 CSS 变量来实现换肤。这样无论是基于 LESS 的 React 项目,还是基于 SCSS 的 Vue 项目,都能应用换肤功能。并且使用时只需调用var函数,降低了使用成本。


Demo地址:github.com/cwjbjy/vite…


1. 一键换肤


1. 前置知识


CSS变量:声明自定义CSS属性,它包含的值可以在整个文档中重复使用。属性名需要以两个减号(--)开始,属性值则可以是任何有效的 CSS 值


--fontColor:'#fff'

Var函数:用于使用CSS变量。第一个参数为CSS变量名称,第二个可选参数作为默认值


color: var(--fontColor);

CSS属性选择器:匹配具有特定属性或属性值的元素。例如[data-theme='black'],将选择所有 data-theme 属性值为 'black' 的元素


2. 定义主题色


1. 新建src/assets/theme/theme-default.css


这里定义字体颜色与布局的背景色,更多CSS变量可根据项目的需求来定义


[data-theme='default'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #2f3542;
--background-aside: #545c64;
--background-main: #0678be;
}

2. 新建src/assets/theme/theme-black.css


再定义一套暗黑主题色


[data-theme='black'] {
/* 字体 */
--font-primary: #fff;
--font-highlight: #434a50;
/* 布局 */
--background-header: #303030;
--background-aside: #303030;
--background-main: #393939;
}

3. 新建src/assets/theme/index.css


在index.css文件中导出全部主题色


@import './theme-default.css'; 
@import './theme-black.css';

4. 引入全局样式


在入口文件引入样式,比如我这里是main.tsx


import '@/assets/styles/theme/index.css';

3. 在html标签上增加自定义属性


修改index.html,在html标签上增加自定义属性data-theme


<html lang="en" data-theme="default"></html>

这里使用data-theme是为了被CSS属性选择器[data-theme='default']选中,也可更换为其他自定义属性,只需与CSS属性选择器对应上即可。


4. 修改CSS主题色


关键点:监听change事件,使用document.documentElement.setAttribute动态修改data-theme属性,然后CSS属性选择器将自动选择对应的css变量


<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
</div>

</template>

<script setup lang="ts">
const handleChange = (e: Event) => {
window.document.documentElement.setAttribute('data-theme', (e.target as HTMLSelectElement).value);
};
</script>


<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
</style>


效果图,默认色:


1708935487468.png


效果图,暗黑色:


1708935536950.png


5. 修改JS主题色


切换主题色,除了需要修改css样式,有时也需在js文件中修改样式,例如修改echarts的配置文件,来改变柱状图、饼图等的颜色。


1. 新建src/config/theme.js


定义图像的颜色,这里定义字体的颜色,默认情况下字体为黑色,暗黑模式下,字体为白色


const themeColor = {
default: {
font: '#333',
},
black: {
font: '#fff',
},
};

export default themeColor;

2. 修改vue文件


关键点:



  1. 定义主题色TS类型,规定默认和暗黑两种:type ThemeTypes = 'default' | 'black';

  2. 定义theme响应式变量,用来记录当前主题色:const theme = ref<ThemeTypes>('default');

  3. 监听change事件,将选中的值赋给theme:theme.value = selectTheme;

  4. 使用watch进行监听,如果theme改变,则重新绘制echarts图形


完整的vue文件:


<template>
<div>
<select name="pets" @change="handleChange">
<option value="default">默认色</option>
<option value="black">黑色</option>
</select>
<div>登录页面</div>
<div ref="echartRef" class="myChart"></div>
</div>

</template>

<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import themeColor from '@/config/theme';
import * as echarts from 'echarts';

type ThemeTypes = 'default' | 'black';

const echartRef = ref<HTMLDivElement | null>(null);
const theme = ref<ThemeTypes>('default');
const handleChange = (e: Event) => {
const selectTheme = (e.target as HTMLSelectElement).value as ThemeTypes;
theme.value = selectTheme;
window.document.documentElement.setAttribute('data-theme', selectTheme);
};

const drawGraph = () => {
let echartsInstance = echarts.getInstanceByDom(echartRef.value!);
if (!echartsInstance) {
echartsInstance = echarts.init(echartRef.value);
}
echartsInstance.clear();
var option = {
color: ['#3398DB'],
title: {
text: '柱状图',
left: 'center',
textStyle: {
color: themeColor[theme.value].font,
},
},
xAxis: [
{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
yAxis: [
{
type: 'value',
axisLabel: {
show: true,
color: themeColor[theme.value].font,
},
nameTextStyle: {
color: themeColor[theme.value].font,
},
},
],
series: [
{
name: '直接访问',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334, 390, 330, 220],
},
],
};

echartsInstance.setOption(option);
};
onMounted(() => {
drawGraph();
});
watch(theme, () => {
drawGraph();
});
</script>

<style lang="scss">
body {
color: var(--font-primary);
background-color: var(--background-main);
}
.myChart {
width: 300px;
height: 300px;
}
</style>

2. 一键变灰


在特殊的日子里,网页有整体变灰色的需求。可以使用filter 的 grayscale() 改变图像灰度,值在 0% 到 100% 之间,值为0%展示原图,值为100% 则完全转为灰度图像


body {
filter: grayscale(1); //1相当于100%
}

结尾


本文只是介绍大概的思路,更多的功能可根据业务增加。例如将主题色theme存储到pinia上,应用到全局上;将主题色存储到localStorage上,在页面刷新时,防止主题色恢复默认。


本文可结合以下文章阅读:



如果有更多的换肤方案,欢迎在留言区留言讨论。我会根据留言区内容实时更新。


作者:敲代码的彭于晏
来源:juejin.cn/post/7342527074526019620
收起阅读 »

实现一个支持@的输入框

web
近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件: 但是不难发现跟微信飞书对比下,有两个细节没有处...
继续阅读 »

近期产品期望在后台发布帖子或视频时,需要添加 @用户 的功能,以便用户收到通知,例如“xxx在xxx提及了您!”。然而,现有的开源库未能满足我们的需求,例如 ant-design 的 Mentions 组件:



20240415161851.gif


但是不难发现跟微信飞书对比下,有两个细节没有处理。



  1. @用户没有高亮

  2. 在删除时没有当做一个整体去删除,而是单个字母删除,首先不谈用户是否想要整体删除,在这块有个模糊查询的功能,如果每删一个字母之后去调接口查询数据库造成一些不必要的性能开销,哪怕加上防抖。


然后也是找了其他的库都没达到产品的期望效果,那么好,自己实现一个,先看看最终实现的效果


6ND88RssMr.gif


封装之后使用:


<AtInput
height={150}
onRequest={async (searchStr) => {
const { data } = await UserFindAll({ nickname: searchStr });
return data?.list?.map((v) => ({
id: v.uid,
name: v.nickname,
wechatAvatarUrl: v.wechatAvatarUrl,
}));
}}
onChange={(content, selected) => {
setAtUsers(selected);
}}
/>

那么实现这么一个输入框大概有以下几个点:



  1. 高亮效果

  2. 删除/选中用户时需要整体删除

  3. 监听@的位置,复制给弹框的坐标,联动效果

  4. 最后我需要拿到文本内容,并且需要拿到@那些用户,去做表单提交


大多数文本输入框我们会使用input,或者textarea,很明显以上1,2两点实现不了,antd也是使用的textarea,所以也是没有实现这两个效果。所以这块使用富文本编辑,设置contentEditable,将其变为可编辑去做。输入框以及选择器的dom就如下:


 <div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div
id="atInput"
ref={atRef}
className={'editorDiv'}
contentEditable
onInput={editorChange}
onClick={editorClick}
/>

{/* 选择用户框 */}
<SelectUser
options={options}
visible={visible}
cursorPosition={cursorPosition}
onSelect={onSelect}
/>

</div>

实现思路:



  1. 监听输入@,唤起选择框。

  2. 截取@xxx的xxx作为搜素的关键字去查询接口

  3. 选择用户后需要将原先输入的 @xxx 替换成 @姓名,并且将@的用户缓存起来

  4. 选择文本框中的姓名时需要变为整体选中状态,这块依然可以给标签设置为不可编辑状态就可实现,contentEditable=false,即可实现整体删除,在删除的同时需要将当前用户从之前缓存的@过的用户数组删除

  5. 那么可以拿到输入框的文本,@的用户, 最后将数据抛给父组件就完成了


以上提到了监听@文本变化,通常绑定onChange事件就行,但是还有一种用户通过点击移动光标,这块需要绑定change,click两个时间,他们里边的逻辑基本一样,只需要额外处理点击选中输入框中用户时,整体选中g功能,那么代码如下:


    const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (event) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

每次点击或者文本改变时都会去调用onObserveInput,以上onObserveInput该方法中主要做了以下逻辑:




  1. 通过getSelection方法可以获取光标的偏移位置,那么可以截取光标之前的字符串,并且使用lastIndexOf从后向前查找最后一个“@”符号,并记录他的下标,那么有了【光标之前的字符串】,【@的下标】就可以拿到到@之后用于过滤用户的关键字,并将其缓存起来。

  2. 唤起选择器,并通过关键字去过滤用户。这块涉及到一个选择器的位置,直接使用window.getSelection()?.getRangeAt(0).getBoundingClientRect()去获取光标的位置拿到的是光标相对于窗口的坐标,直接用这个坐标会有问题,比如滚动条滚动时,这个选择器发生位置错乱,所以这块同时去拿输入框的坐标,去做一个相减,这样就可以实现选择器跟着@符号联动的效果。


 const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

选择器弹出后,那么下面就到了选择用户之后的流程了,


 /**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

选择用户的时候需要做的以下以下几点:



  1. 删除之前的@xxx字符

  2. 插入不可编辑的span标签

  3. 将当前选择的用户缓存起来

  4. 重新获取输入框的内容

  5. 关闭选择器

  6. 将输入框重新聚焦


最后


在选择的用户或者内容发生改变时将数据抛给父组件


 const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

完整组件代码


输入框主要逻辑代码:


let timer: NodeJS.Timeout | null = null;

const AtInput = (props: AtInputProps) => {
const { height = 300, onRequest, onChange, value, onBlur } = props;
// 输入框的内容=innerText
const [content, setContent] = useState<string>('');
// 选择用户弹框
const [visible, setVisible] = useState<boolean>(false);
// 用户数据
const [options, setOptions] = useState<Options[]>([]);
// @的索引
const [currentAtIdx, setCurrentAtIdx] = useState<number>();
// 输入@之前的字符串
const [focusNode, setFocusNode] = useState<Node | string>();
// @后关键字 @郑 = 郑
const [searchStr, setSearchStr] = useState<string>('');
// 弹框的x,y轴的坐标
const [cursorPosition, setCursorPosition] = useState<Position>({
x: 0,
y: 0,
});
// 选择的用户
const [selected, setSelected] = useState<Options[]>([]);
const atRef = useRef<any>();

/** 获取选择器弹框坐标 */
const getCursorPosition = () => {
// 坐标相对浏览器的坐标
const { x, y } = window.getSelection()?.getRangeAt(0).getBoundingClientRect() as any;
// 获取编辑器的坐标
const editorDom = window.document.querySelector('#atInput');
const { x: eX, y: eY } = editorDom?.getBoundingClientRect() as any;
// 光标所在位置
setCursorPosition({ x: x - eX, y: y - eY });
};

/**获取用户下拉列表 */
const fetchOptions = (key?: string) => {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(async () => {
const _options = await onRequest(key);
setOptions(_options);
}, 500);
};

useEffect(() => {
fetchOptions();
// if (value) {
// /** 判断value中是否有at用户 */
// const atUsers: any = StringTools.filterUsers(value);
// setSelected(atUsers);
// atRef.current.innerHTML = value;
// setContent(value.replace(/<\/?.+?\/?>/g, '')); //全局匹配内html标签)
// }
}, []);

const onObserveInput = () => {
let cursorBeforeStr = '';
const selection: any = window.getSelection();
if (selection?.focusNode?.data) {
cursorBeforeStr = selection.focusNode?.data.slice(0, selection.focusOffset);
}
setFocusNode(selection.focusNode);
const lastAtIndex = cursorBeforeStr?.lastIndexOf('@');
setCurrentAtIdx(lastAtIndex);
if (lastAtIndex !== -1) {
getCursorPosition();
const searchStr = cursorBeforeStr.slice(lastAtIndex + 1);
if (!StringTools.isIncludeSpacesOrLineBreak(searchStr)) {
setSearchStr(searchStr);
fetchOptions(searchStr);
setVisible(true);
} else {
setVisible(false);
setSearchStr('');
}
} else {
setVisible(false);
}
};

const selectAtSpanTag = (target: Node) => {
window.getSelection()?.getRangeAt(0).selectNode(target);
};

const editorClick = async (e?: any) => {
onObserveInput();
// 判断当前标签名是否为span 是的话选中当做一个整体
if (e.target.localName === 'span') {
selectAtSpanTag(e.target);
}
};

const editorChange = (event: any) => {
const { innerText } = event.target;
setContent(innerText);
onObserveInput();
};

/**
* @param id 唯一的id 可以uid
* @param name 用户姓名
* @param color 回显颜色
* @returns
*/

const createAtSpanTag = (id: number | string, name: string, color = 'blue') => {
const ele = document.createElement('span');
ele.className = 'at-span';
ele.style.color = color;
ele.id = id.toString();
ele.contentEditable = 'false';
ele.innerText = `@${name}`;
return ele;
};

/**
* 选择用户时回调
*/

const onSelect = (item: Options) => {
const selection = window.getSelection();
const range = selection?.getRangeAt(0) as Range;
// 选中输入的 @关键字 -> @郑
range.setStart(focusNode as Node, currentAtIdx!);
range.setEnd(focusNode as Node, currentAtIdx! + 1 + searchStr.length);
// 删除输入的 @关键字
range.deleteContents();
// 创建元素节点
const atEle = createAtSpanTag(item.id, item.name);
// 插入元素节点
range.insertNode(atEle);
// 光标移动到末尾
range.collapse();
// 缓存已选中的用户
setSelected([...selected, item]);
// 选择用户后重新计算content
setContent(document.getElementById('atInput')?.innerText as string);
// 关闭弹框
setVisible(false);
// 输入框聚焦
atRef.current.focus();
};

const getAttrIds = () => {
const spans = document.querySelectorAll('.at-span');
let ids = new Set();
spans.forEach((span) => ids.add(span.id));
return selected.filter((s) => ids.has(s.id));
};

/** @的用户列表发生改变时,将最新值暴露给父组件 */
useEffect(() => {
const selectUsers = getAttrIds();
onChange(content, selectUsers);
}, [selected, content]);

return (
<div style={{ height, position: 'relative' }}>
{/* 编辑器 */}
<div id="atInput" ref={atRef} className={'editorDiv'} contentEditable onInput={editorChange} onClick={editorClick} />
{/* 选择用户框 */}
<SelectUser options={options} visible={visible} cursorPosition={cursorPosition} onSelect={onSelect} />
</div>

);
};

选择器代码


const SelectUser = React.memo((props: SelectComProps) => {
const { options, visible, cursorPosition, onSelect } = props;

const { x, y } = cursorPosition;

return (
<div
className={'selectWrap'}
style={{
display: `${visible ? 'block' : 'none'}`,
position: 'absolute',
left: x,
top: y + 20,
}}
>

<ul>
{options.map((item) => {
return (
<li
key={item.id}
onClick={() =>
{
onSelect(item);
}}
>
<img src={item.wechatAvatarUrl} alt="" />
<span>{item.name}</span>
</li>
);
})}
</ul>
</div>

);
});
export default SelectUser;

以上就是实现一个支持@用户的输入框功能,就目前而言,比较死板,不支持自定义颜色,自定义选择器等等,未来,可以进一步扩展功能,例如添加@用户的高亮样式定制、支持键盘快捷键操作等,从而提升用户体验和功能性。


作者:tech_zjf
来源:juejin.cn/post/7357917741909819407
收起阅读 »

Vue.js 自动路由:告别手动配置,让开发更轻松!

web
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts 或 route.js 文件简直是一场噩梦! 我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是...
继续阅读 »

在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.tsroute.js 文件简直是一场噩梦!


我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!


所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!


那就是 Unplugin Vue Router! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。


创建项目,安装插件


首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。


pnpm create vue@latest 

我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。


然后,进入项目目录,安装依赖。我最近开始用 pnpm 来管理依赖,感觉还不错。


pnpm add -D unplugin-vue-router 

接下来,更新 vite.config.ts 文件, 注意要把插件放在第 0 个位置


import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},

});

然后,更新 env.d.ts 文件,让编辑器能够识别插件的类型。


/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />

最后,更新路由文件 src/router/index.ts


import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

if (import.meta.hot) {
handleHotUpdate(router);
}

export default router;

创建页面,自动生成路由


现在,我们可以创建 src/pages 目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!


我们先在 src\pages\about.vue 创建一个关于页面:


<template>
    <div>This is the about page</div>
</template>

然后在 src\pages\index.vue 创建首页:


<template>
    <div>This is Home Page</div>
</template>

运行 pnpm dev 启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。


怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…


动态路由


我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue 创建一个组件,内容如下:


<script setup>
const { id } = useRoute().params;
</script>
<template>
    <div>This is the blog post with id: {{ id }}</div>
</template>

再次运行 pnpm dev ,然后访问 http://localhost:5173/blog/6 ,你就会看到以下内容:


vuejs 自动路由


是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!


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

JS类型判断的四种方法,你掌握了吗?

web
引言 JavaScript中有七种原始数据类型和几种引用数据类型,本文将清楚地介绍四种用于类型判断的方法,分别是typeOf、instanceOf、Object.prototype.toString.call()、Array.isArray(),并介绍其使用方...
继续阅读 »

引言


JavaScript中有七种原始数据类型和几种引用数据类型,本文将清楚地介绍四种用于类型判断的方法,分别是typeOfinstanceOfObject.prototype.toString.call()Array.isArray(),并介绍其使用方法和判定原理。


typeof



  1. 可以准确判断除null之外的所有原始类型,null会被判定成object

  2. function类型可以被准确判断为function,而其他所有引用类型都会被判定为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

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

console.log(typeof s); // string typeof后面有无括号都行
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

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

console.log(typeof(fn)); // function

判定原理


typeof是通过将值转换为二进制之后,判断其前三位是否为0:都是0则为object,反之则为原始类型。因为原始类型转二进制,前三位一定不都是0;反之引用类型被转换成二进制前三位一定都是0。


null是原始类型却被判定为object就是因为它在机器中是用一长串0来表示的,可以把这看作是一个史诗级的bug。


所以用typeof判断接收到的值是否为一个对象时,还要注意排除null的情况:


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

你丢一个值给typeof,它会告诉你这个字值是什么类型,但是它无法准确告诉你这是一个Array或是Date,若想要如此精确地知道一个对象类型,可以用instanceof告诉你是否为某种特定的类型


instanceof


只能精确地判断引用类型,不能判断原始类型


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(s instanceof String);// false
console.log(n instanceof Number);// false
console.log(arr instanceof Object);// true

判定原理


instanceof既能把数组判定成Array,又能把数组判定成Object,究其原因是原型链的作用————顺着数组实例 arr 的隐式原型一直找到了 Object 的构造函数,看下面的代码:


arr.__proto__ = Array.prototype
Array.prototype.__proto__ = Object.prototype

所以我们就知道了,instanceof能准确判断出一个对象是否为某种类型,就是依靠对象的原型链来查找的,一层又一层地判断直到找到null为止。


手写instanceOf


根据这个原理,我们可以手写出一个instanceof:


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


对象的隐式原型 等于 构造函数的显式原型!可看文章 给我三分钟,带你完全理解JS原型和原型链前言



Object.prototype.toString.call()


可以判断任何数据类型


在浏览器上执行这三段代码,会得到'[object Object]''[object Array]''[object Number]'


var a = {}
Object.prototype.toString.call(a)

var a = {}
Object.prototype.toString.call(a)

var a = 123
Object.prototype.toString.call(a)

原型上的toString的内部逻辑


调用Object.prototype.toString的时候执行会以下步骤: 参考官方文档:带注释的 ES5



  1. 如果此值是undefined类型,则返回 ‘[object Undefined]’

  2. 如果此值是null类型,则返回 ‘[object Null]’

  3. 将 O 作为 ToObject(this) 的执行结果。toString执行过程中会调用一个ToObject方法,执行一个类似包装类的过程,我们访问不了这个方法,是JS自己用的

  4. 定义一个class作为内部属性[[class]]的值。toString可以读取到这个值并把这个值暴露出来让我们看得见

  5. 返回由 "[object"class"]" 组成的字符串


为什么结合call就能准确判断值类型了呢?


① 首先我们要知道Object.prototype.toString(xxx)往括号中不管传递什么返回结果都是'[object Object]',因为根据上面五个步骤来看,它内部会自动执行ToObject()方法,xxx会被执行一个类似包装类的过程然后转变成一个对象。所以单独一个Object.prototype.toString(xxx)不能用来判定值的类型


② 其次了解call方法的核心原理就是:比如foo.call(obj),利用隐式绑定的规则,让obj对象拥有foo这个函数的引用,从而让foo函数的this指向obj,执行完foo函数内部逻辑后,再将foo函数的引用从obj上删除掉。手搓一个call的源码就是这样的:


// call方法只允许被函数调用,所以它应该是放在Function构造函数的显式原型上的
Function.prototype.mycall = function(context) {
// 判断调用我的那个哥们是不是函数体
if (typeof this !== 'function') {
return new TypeError(this+ 'is not a function')
}

// this(函数)里面的this => context对象
const fn = Symbol('key') // 定义一个独一无二的fn,防止使用该源码时与其他fn产生冲突
context[fn] = this // 让对象拥有该函数 context={Symbol('key'): foo}
context[fn]() // 触发隐式绑定
delete context[fn]
}

③ 所以Object.prototype.toString.call(xxx)就相当于 xxx.toString(),把toString()方法放在了xxx对象上调用,这样就能精准给出xxx的对象类型



toString方法有几个版本:



  1. {}.toString() 得到由"[object" 和 class 和 "]" 组成的字符串

  2. [].toString() 数组的toString方法重写了对象上的toString方法,返回由数组内部元素以逗号拼接的字符串

  3. xx.toString() 返回字符串字面量,比如


let fn = function(){}; 
console.log( fn.toString() ) // "function () {}"


Array.isArray(x)


只能判断是否是数组,若传进去的x是数组,返回true,否则返回false


总结


typeOf:原始类型除了null都能准确判断,引用类型除了function能准确判断其他都不能。依靠值转为二进制后前三位是否为0来判断


instanceOf:只能把引用类型丢给它准确判断。顺着对象的隐式原型链向上比对,与构造函数的显式原型相等返回true,否则false


Object.prototype.toString.call():可以准确判断任何类型。要了解对象原型的toString()内部逻辑和call()的核心原理,二者结合才有精准判定的效果


Array.isArray():是数组则返回true,不是则返回false。判定范围最狭窄


作者:今天一定晴q
来源:juejin.cn/post/7403288145196580904
收起阅读 »

学TypeScript必然要了解declare

web
背景 declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可...
继续阅读 »

背景


declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。


$('#foo');
// or
jQuery('#foo');

然而在ts文件中,使用语法,语法,底下就会爆出一条红线提示到:Cannot find name '$'



因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。


定义


在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。

注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:


// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

使用



  • declare var 声明全局变量

  • declare function 声明全局方法

  • declare class 声明全局类

  • declare enum 声明全局枚举类型

  • declare namespace 声明(含有子属性的)全局对象

  • declare global 扩展全局变量

  • declare module 扩展模块


声明文件


通常,在使用第三方库或模块时,有两种方式引入声明文件:



  • 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。

  • 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。


有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。


image.png


可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:


npm install @types/jquery --save-dev

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

厉害了,不用js就能实现文字中间省略号

web
今天发现一个特别有意思的效果,并进行解析,就是标题的效果 参考链接 实现地址 CodePen 如果要实现这个功能,我想很多人第一时间想到的都是用js去计算dom容器和文字之间是否溢出吧?但今天带来一个用css实现的效果,不用自己计算,只需要寥寥几行(bu...
继续阅读 »

今天发现一个特别有意思的效果,并进行解析,就是标题的效果



参考链接



如果要实现这个功能,我想很多人第一时间想到的都是用js去计算dom容器和文字之间是否溢出吧?但今天带来一个用css实现的效果,不用自己计算,只需要寥寥几行(bushi)就可以实现让人头疼的文字中间省略号功能。




实现思路


1. 简单实现


在用css实现的时候我们不妨用这个思路想想,设置一个当前显示文字span伪元素的width为50%,浮动到当前span上面,并且设置direction: rtl; 显示右边文字,不就可以很简单的实现这个功能了?让我们试试:


<style>
.wrap {
width: 200px;
border: 1px solid white;
}
.test-title {
display: block;
color: white;
overflow: hidden;
height: 20px;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
<body>
<div class="wrap">
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>

Untitled


💡 此处应有图

2. 优化效果


在上面我们已经看到,其实效果我们已经实现了,现在文字中间已经有了省略号了!但是这其中其实有一个弊端不知道大家有没有发现,那就是文本不溢出的情况呢?伪元素是不是会一直显示在上面?这该怎么办?难道我们需要用js监听文本不溢出的情况然后手动隐藏吗?


Untitled


既然是用css来进行实现,那么我们当然不能用这种方式了。这里原作者用了一种很取巧,但也很好玩的一种方法,让我们来看看吧!


既然我们上面实现的是文本溢出的情况,那么当文本不溢出的时候我们直接显示文字不就行了?你可能想说:“这不是废话吗?但我现在不就是不知道怎么判断吗? ”。hhhhh对,那我们就要用css来想想,css该怎么判断呢?我就不卖关子了,让我们想想,我们给文本的容器添加一个固定宽度,那么当文本溢出的时候会发生什么呢?是不是会换行,高度变大呢,那么当我们设置两个文本元素,一个是正常样式,一个是我们上方的溢出样式。等文本不溢出没换行的时候,显示正常样式,当文本溢出高度变大的时候显示溢出样式可以吗?让我们试试吧


<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
background: #333;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
font-size: 14px;
}
.wrap {
width: 300px;
background: #333;

/* 设置正常行高,并隐藏溢出画面 */
height: 2em;
overflow: hidden;
line-height: 2;
position: relative;
text-align: -webkit-match-parent;
resize: horizontal;
}

.normal-title {
/* 设置最大高度为双倍行高使其可以换行 */
display: block;
max-height: 4em;
}
.test-title {
position: relative;
top: -4em;
display: block;
color: white;
overflow: hidden;
height: 2em;
text-align: justify;
background: inherit;
overflow: hidden;
}
.test-title::before {
content: attr(title);
width: 50%;
float: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
}
</style>
</head>

<body>
<div class="wrap">
<span class="normal-title">这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字</span>
<span class="test-title" title="这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字">
这是一行文字文字文字文字文字文字文字文字文字文字文字文字文字
</span>
</div>
</body>

</html>

大家都试过了吧?那让我们来讲一下这段代码的实现:


实现方式:简单来说这段代码实现的就是一个覆盖的效果,normal-title元素平常是普通高度(1em),等到换行之后就会变成2em,那么我们的溢出样式test-title怎么实现的覆盖呢?这主要依赖于test-title的top属性,让我们这样子想,当normal-title高度为1em的时候,test-title的top为-2em,那么这时候因为wrap的hidden效果,所以test-title是看不到的。那么当normal-title的高度为2em的时候呢?test-title刚好就会覆盖到normal-title上面,所以我们刚好可以看到test-title的省略号效果。


这就是完整的实现过程和方式,css一些取巧的判断方式总会让我们大开眼界,不断学习,方得始终。


作者:一_个前端
来源:juejin.cn/post/7401812292211081226
收起阅读 »

1. 使用openai api实现一个智能前端组件

0. 注意 本文只是提供一个思路,由于现在大模型正在飞速发展,整个生态在不久的将来或许会发生巨大的变化,文章中的代码仅供参考。 1. 一个简单的示例 假设当前时间是2023年12月28日,时间段选择器通过理解用户输入表述,自动设置值。 可以看到组件正确理解了...
继续阅读 »

0. 注意


本文只是提供一个思路,由于现在大模型正在飞速发展,整个生态在不久的将来或许会发生巨大的变化,文章中的代码仅供参考。


1. 一个简单的示例


msedge_2ReNz11Waq.gif


假设当前时间是2023年12月28日,时间段选择器通过理解用户输入表述,自动设置值。


可以看到组件正确理解了用户想要设置的时间。


2.原理简介


graph TD
输入文字描述 --> 请求语言模型接口 --> 处理语言模型响应 --> 功能操作

其实原理很简单,就是通过代码的方式问模型问题,然后让他回答。这和我们使用chatgpt一样的。


3. 实现


输入描述就不说了,就是输入框。关键在于请求和处理语言模型的接口。


最简单的就是直接使用api请求这些大模型的官方接口,但是我们需要处理各种平台之间的接口差异和一些特殊问题。这里我使用了一个开发语言模型应用的框架LangChain


3.1. LangChain


简单的说,这是一个面向语言处理模型的编程框架,从如何输入你的问题,到如何处理回答都有规范的工具来实现。


LangChain官网


// 这是一个最简单的例子
import { OpenAI } from "langchain/llms/openai";
import { ChatOpenAI } from "langchain/chat_models/openai";
// 初始化openai模型
const llm = new OpenAI({
temperature: 0.9,
});
// 准备一个输入文本
const text =
"What would be a good company name for a company that makes colorful socks?";
// 输入文本,获取响应
const llmResult = await llm.predict(text);
//=> 响应一段文本:"Feetful of Fun"

整个框架主要就是下面三个部分组成:


graph LR
A["输入模板(Prompt templates)"] --- B["语言模型(Language models)"] --- C["输出解释器(Output parsers)"]


  • Prompt templates:输入模板分一句话(not chat)对话(chat)模式,区别就是输入一句话和多句话,而且对话模式中每句话有角色区分是谁说的,比如人类AI系统。这里简单介绍一下非对话模式下怎么创建输入模板。


import { PromptTemplate } from "langchain/prompts";  

// 最简单的模板生成,使用fromTemplate传入一句话
// 可以在句子中加入{}占位符表示变量
const oneInputPrompt = PromptTemplate.fromTemplate(
`You are a naming consultant for new companies.
What is a good name for a company that makes {product}?`

);
// 也可以直接实例化设置
const twoInputPrompt = new PromptTemplate({
inputVariables: ["adjective"],
template: "Tell me a {adjective} joke.",
});

// 如果你想要这样和模型对话
// 先给出几个例子,然后在问问题
Respond to the users question in the with the following format:

Question: What is your name?
Answer: My name is John.

Question: What is your age?
Answer: I am 25 years old.

Question: What is your favorite color?
Answer:
// 可以使用FewShotPromptTemplate
// 创建一些模板,字段名随便你定
const examples = [
{
input:
"Could the members of The Police perform lawful arrests?",
output: "what can the members of The Police do?",
},
{
input: "Jan Sindel's was born in what country?",
output: "what is Jan Sindel's personal history?",
},
];
// 输入模板,包含变量就是模板要填充的
const prompt = `Human: {input}\nAI: {output}`;
const examplePromptTemplate = PromptTemplate.fromTemplate(prompt);
// 创建example输入模板
const fewShotPrompt = new FewShotPromptTemplate({
examplePrompt: examplePromptTemplate,
examples,
inputVariables: [], // no input variables
});
console.log(
(await fewShotPrompt.formatPromptValue({})).toString()
);
// 输出
Human: Could the members of The Police perform lawful arrests?
AI: what can the members of The Police do?

Human: Jan Sindel's was born in what country?
AI: what is Jan Sindel'
s personal history?
// 还有很多可以查询官网


  • Language models: 语言模型同样分为LLM(大语言模型)chat模型,其实两个差不多,就是输入多少和是否可以连续对话的区别。


import { OpenAI } from "langchain/llms/openai";  

const model = new OpenAI({ temperature: 1 });
// 可以添加超时
const resA = await model.call(
"What would be a good company name a company that makes colorful socks?",
{ timeout: 1000 } // 1s timeout
);
// 注册一些事件回调
const model = new OpenAI({
callbacks: [
{
handleLLMStart: async (llm: Serialized, prompts: string[]) => {
console.log(JSON.stringify(llm, null, 2));
console.log(JSON.stringify(prompts, null, 2));
},
handleLLMEnd: async (output: LLMResult) => {
console.log(JSON.stringify(output, null, 2));
},
handleLLMError: async (err: Error) => {
console.error(err);
},
},
],
});
// 还有一些配置可以参考文档


  • Output parsers: 顾名思义就是处理输出的模块,当语言模型回答了一段文字程序是很难提取出有用信息的, 我们通常需要模型返回一个程序可以处理的答案,比如JSON。虽然叫输出解释器,实际上是在输入信息中加入一些额外的提示,让模型能够按照需求格式输出。


// 这里用StructuredOutputParser,结构化输出解释器为例
// 使用StructuredOutputParser创建一个解释器
// 定义了输出有两个字段answer、source
// 字段的值是对这个字段的描述在
const parser = StructuredOutputParser.fromNamesAndDescriptions({
answer: "answer to the user's question",
source: "source used to answer the user's question, should be a website.",
});
// 使用RunnableSequence,批量执行任务
const chain = RunnableSequence.from([
// 输入包含了两个变量,一个是结构化解释器的“格式说明”,一个是用户的问题
PromptTemplate.fromTemplate(
"Answer the users question as best as possible.\n{format_instructions}\n{question}"
),
new OpenAI({ temperature: 0 }),
parser,
]);
// 与模型交互
const response = await chain.invoke({
question: "What is the capital of France?",
format_instructions: parser.getFormatInstructions(),
});
// 响应 { answer: 'Paris', source: 'https://en.wikipedia.org/wiki/Paris' }
// 输入的模板是这样
Answer the users question as best as possible. // 这句话就是prompt的第一句
// 下面一大段是StructuredOutputParser自动加上的,大概就是告诉模型json的标准格式应该是什么
The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {{"properties": {{"foo": {{"title": "Foo", "description": "a list of strings", "type": "array", "items": {{"type": "string"}}}}}}, "required": ["foo"]}}}}
the object {{"foo": ["bar", "baz"]}} is a well-formatted instance of the schema. The object {{"properties": {{"foo": ["bar", "baz"]}}}} is not well-formatted.

Here is the output schema:
```
{"type":"object","properties":{"answer":{"type":"string","description":"answer to the user's question"},"sources":{"type":"array","items":{"type":"string"},"description":"sources used to answer the question, should be websites."}},"required":["answer","sources"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}
`
``
// 这段就是调用的时候传入的问题
What is the capital of France?


// 还有很多不同的解释器
// 如StringOutputParser字符串输出解释器
// JsonOutputFunctionsParser json函数输出解释器等等

除了这三部分,还有一些方便程序操作的一些功能模块,比如记录聊天状态的Memory模块,知识库模块Retrieval等等,这些官网有比较完整的文档,深度的使用后面再来探索。


3.2. 简单版本


// 初始化语言模型
// 这里使用的openai
const llm = new OpenAI({
openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
temperature: 0,
});

function App() {
const [res, setRes] = useState<string>();
const [from] = Form.useForm();
return (
<>
<div>结果:{res}</div>
<Form wrapperCol={{ span: 6 }} form={from}>
<Form.Item label="输入描述">
<Input.Search
onSearch={async (value) =>
{
setRes("正在请求");
// 直接对话模型
const text =
`现在是${dayjs().format("YYYY-MM-DD")},${value},开始结束时间是什么。请用这个格式回答{startTime: '开始时间', endTime: '结束时间'}`;
// 简单预测文本
const llmResult = await llm.predict(text);
const response = JSON.parse(llmResult)
// 解析
const { startTime, endTime } = response;
// 设置
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});
setRes(llmResult)
}}
enterButton={<Button type="primary">确定</Button>}
/>
</Form.Item>
<Form.Item label="时间段" name="times">
<DatePicker.RangePicker />
</Form.Item>
</Form>
</>

);
}

export default App;


前面虽然能实现功能,但是有很多边界条件无法考虑到,比如有的模型无法理解你这个返回格式是什么意思,或者你有很多个字段那你就要写一大串输入模板。


3.3. 使用结构化输出解释器


// 修改一下onSearch
setRes("正在请求");
// 定义输出有两个字段startTime、endTime
const parser = StructuredOutputParser.fromNamesAndDescriptions({
startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
});
const chain = RunnableSequence.from([
// 输入模板
PromptTemplate.fromTemplate(
`{format_instructions}\n现在是${dayjs().format(
"YYYY-MM-DD"
)}
,{question},开始结束时间是什么`

),
llm,
parser,
]);
const response = await chain.invoke({
question: value,
// 把输出解释器的提示放入输入模板中
format_instructions: parser.getFormatInstructions(),
});
// 这个时候经过结构化解释器处理,返回的就是json
setRes(JSON.stringify(response));
const { startTime, endTime } = response;
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});

对于大型一点的项目,使用langChainapi可以更规范的组织我们的代码。


// 完整代码
import { OpenAI } from "langchain/llms/openai";
import { useState } from "react";
import {
PromptTemplate,
} from "langchain/prompts";
import { StructuredOutputParser } from "langchain/output_parsers";
import { RunnableSequence } from "langchain/runnables";
import { Button, DatePicker, Form, Input } from "antd";
import "dayjs/locale/zh-cn";
import dayjs from "dayjs";

const llm = new OpenAI({
openAIApiKey: import.meta.env.VITE_OPENAI_KEY,
temperature: 0,
});

function App() {
const [res, setRes] = useState<string>();
const [from] = Form.useForm();
return (
<>
<div>结果:{res}</div>
<Form wrapperCol={{ span: 6 }} form={from}>
<Form.Item label="输入描述">
<Input.Search
onSearch={async (value) =>
{
setRes("正在请求");
const parser = StructuredOutputParser.fromNamesAndDescriptions({
startTime: "开始时间,格式是YYYY-MM-DD HH:mm:ss",
endTime: "结束时间,格式是YYYY-MM-DD HH:mm:ss",
});
const chain = RunnableSequence.from([
PromptTemplate.fromTemplate(
`{format_instructions}\n现在是${dayjs().format(
"YYYY-MM-DD"
)},{question},开始结束时间是什么`
),
llm,
parser,
]);
const response = await chain.invoke({
question: value,
format_instructions: parser.getFormatInstructions(),
});
setRes(JSON.stringify(response));
const { startTime, endTime } = response;
from.setFieldsValue({
times: [dayjs(startTime), dayjs(endTime)],
});

}}
enterButton={<Button type="primary">确定</Button>}
/>
</Form.Item>
<Form.Item label="时间段" name="times">
<DatePicker.RangePicker />
</Form.Item>
</Form>
</>

);
}

export default App;

4.总结


这篇文章只是我初步使用LangChain的一个小demo,在智能组件上面,大家其实可以发挥更大的想象去发挥。还有很多组件可以变成自然语言驱动的。


随着以后大模型的小型化,专门化,我相信肯定会涌现更多的智能组件。


作者:头上有煎饺
来源:juejin.cn/post/7317440781588840486
收起阅读 »

Vue.js 自动路由:告别手动配置,让开发更轻松!

web
在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.ts 或 route.js 文件简直是一场噩梦! 我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是...
继续阅读 »

在使用 Vue.js 开发项目的时候,我最头疼的就是创建路由,尤其是项目越来越大的时候,管理 route.tsroute.js 文件简直是一场噩梦!


我曾经做过一个页面超级多的项目,每新增一个页面就要更新路由,删除页面也要更新路由文件,不然就会报错,真是烦死人了!


所以,我开始寻找自动生成路由的方法。我在网上搜了很久,但大部分结果都是针对 Webpack 和 Vue 2 的,很难找到适合我的方案。最后,我在 Vue 的 GitHub 仓库的讨论区里提问,终于找到了答案!


那就是 Unplugin Vue Router! 它可以为 Vue 3 实现基于文件的自动路由,而且支持 TypeScript,设置起来也超级简单! 虽然官方说它还在实验阶段,但用起来已经很方便了。


创建项目,安装插件


首先,我们创建一个新的 Vue 项目。 相信大家都很熟悉用 Vue CLI 创建项目了,这里就不赘述了,不熟悉的小伙伴可以去看看 Vue.js 官网的快速入门指南。


pnpm create vue@latest 

我创建项目的时候选择了 TypeScript 和 Vue Router,这样它就会自动生成一些页面和路由。


然后,进入项目目录,安装依赖。我最近开始用 pnpm 来管理依赖,感觉还不错。


pnpm add -D unplugin-vue-router 

接下来,更新 vite.config.ts 文件, 注意要把插件放在第 0 个位置


import { fileURLToPath, URL } from "node:url";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter({
/* options */
}),
// ⚠️ Vue must be placed after VueRouter()
vue(),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},

});

然后,更新 env.d.ts 文件,让编辑器能够识别插件的类型。


/// <reference types="vite/client" />
/// <reference types="unplugin-vue-router/client" />

最后,更新路由文件 src/router/index.ts


import { createRouter, createWebHistory } from "vue-router";
import { routes, handleHotUpdate } from "vue-router/auto-routes";

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

if (import.meta.hot) {
handleHotUpdate(router);
}

export default router;

创建页面,自动生成路由


现在,我们可以创建 src/pages 目录了,在这个目录下创建的 Vue 组件会自动变成路由和页面,就像 Nuxt 一样方便!


我们先在 src\pages\about.vue 创建一个关于页面:


<template>
    <div>This is the about page</div>
</template>

然后在 src\pages\index.vue 创建首页:


<template>
    <div>This is Home Page</div>
</template>

运行 pnpm dev 启动开发服务器,点击 “Home” 链接就会跳转到首页,点击 “About” 链接就会跳转到关于页面。


怎么样,是不是很方便? 如果你不熟悉路由文件夹结构,可以看看这个文档: uvr.esm.is/guide/file-…


动态路由


我们再来试试创建带参数的动态路由。在 src/pages/blog/[id].vue 创建一个组件,内容如下:


<script setup>
const { id } = useRoute().params;
</script>
<template>
    <div>This is the blog post with id: {{ id }}</div>
</template>

再次运行 pnpm dev ,然后访问 http://localhost:5173/blog/6 ,你就会看到以下内容:


vuejs 自动路由


是不是很神奇? 希望这篇简短的博客能帮助你在 Vue.js 的旅程中更轻松地创建路由!


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

学TypeScript必然要了解declare

web
背景 declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可...
继续阅读 »

背景


declare关键字是为了服务TypeScript的。TypeScript是什么在这里就不多介绍了,但是我们要知道ts文件是需要TypeScript编译器转换为js文件才可以执行,并且在编译阶段就会进行类型检查。但是在TypeScript中并不支持js可识别的所有类型,例如我们使用第三方库JQuery,我们通过一下方法获取一个id为‘foo’的标签元素。


$('#foo');
// or
jQuery('#foo');

然而在ts文件中,使用语法,语法,底下就会爆出一条红线提示到:Cannot find name '$'



因此,需要declare来声明,告诉TypeScript编译器该标识符已存在,通过编译时的检查并在开发时提供类型提示。


定义


在 TypeScript 中,declare关键字告诉编译器存在一个对象(并且可以在代码中引用)。它向 TypeScript 编译器声明该对象。简而言之,它允许开发人员使用在其他地方声明的对象。

注:编译器不会将declare语句编译为 JavaScript。对比下面两段代码:


// declare声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。
// 该声明并不会生成真正的 JavaScript 代码,而只是告诉 TypeScript 编译器该变量存在。
declare var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

// 直接声明了一个名为 myGlobal 的全局变量,并指定其类型为 any。这会生成真正的 JavaScript 代码。
var myGlobal: any;

// 给 myGlobal 赋值为 42。
myGlobal = 42;
console.log(myGlobal); // 42

使用



  • declare var 声明全局变量

  • declare function 声明全局方法

  • declare class 声明全局类

  • declare enum 声明全局枚举类型

  • declare namespace 声明(含有子属性的)全局对象

  • declare global 扩展全局变量

  • declare module 扩展模块


声明文件


通常,在使用第三方库或模块时,有两种方式引入声明文件:



  • 全局声明:如果第三方库或模块是全局可访问的,你可以在整个项目的任何地方直接使用它们,而无需显式导入。此时,你只需要确保在 TypeScript 项目中正确引入了相应的声明文件。一般情况下,TypeScript 会自动查找并加载全局声明文件。如果没有自动加载,你可以使用 /// 的方式在具体的源文件中将声明文件引入。

  • 模块导入:如果第三方库或模块是通过模块化方式提供的,你需要使用 import 语句将其导入到你的代码中,同时也需要确保相应的声明文件被正确引入。在这种情况下,你可以使用 import 或 require 来引入库,并且不需要显式地引入声明文件,因为 TypeScript 编译器会根据模块的导入语句自动查找和加载相应的声明文件。


有很多第三方库提供了声明文件,可以在packages.json文件中查看。types表示类型声明文件是哪一个。


image.png


可以使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例:


npm install @types/jquery --save-dev

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

前端如何将git的信息打包进html

为什么要做这件事 定制化项目我们没有参与或者要临时更新客户的包的时候,需要查文档才知道哪个是最新分支 当测试环境打包的时候,不确定是否是最新的包,需要点下功能看是否是最新的代码,不能直观的看到当前打的是哪个分支 多人开发的时候,某些场景下可能被别人覆盖了,需...
继续阅读 »

为什么要做这件事



  1. 定制化项目我们没有参与或者要临时更新客户的包的时候,需要查文档才知道哪个是最新分支

  2. 当测试环境打包的时候,不确定是否是最新的包,需要点下功能看是否是最新的代码,不能直观的看到当前打的是哪个分支

  3. 多人开发的时候,某些场景下可能被别人覆盖了,需要查下jenkins或者登录服务器看下


实现效果


如下,当打开F12,可以直观的看到打包日期、分支、提交hash、提交时间
image.png


如何做


主要是借助 git-revision-webpack-plugin的能力。获取到git到一些后,将这些信息注入到变量中,html读取这个变量即可


1. 安装dev的依赖


    npm install --save-dev git-revision-webpack-plugin

2. 引入依赖并且初始化


   const { GitRevisionPlugin } = require('git-revision-webpack-plugin')
const gitRevisionPlugin = new GitRevisionPlugin()

3. 注入变量信息


我这里用的是vuecli,可直接在chainWebpack中注入,当然你可以使用DefinePlugin进行声明


     config.plugin('html').tap((args) => {
args[0].banner = {
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
branch: gitRevisionPlugin.branch(),
commitHash: gitRevisionPlugin.commithash(),
lastCommitDateTime: dayjs(gitRevisionPlugin.lastcommitdatetime()).format('YYYY-MM-DD HH:mm:ss'),
}
return args
});

4. 使用变量


在index.html的头部插入注释


  <!-- date  <%= htmlWebpackPlugin.options.banner.date %> -->
<!-- branch <%= htmlWebpackPlugin.options.banner.branch %> -->
<!-- commitHash <%= htmlWebpackPlugin.options.banner.commitHash %> -->
<!-- lastCommitDateTime <%= htmlWebpackPlugin.options.banner.lastCommitDateTime %> -->

5. 查看页面


image.png


6. 假如你用的是vuecli


当你使用的是vuecli,构建完后会发现index.html上这个注释丢失了


原因如下


vueCLi 对html的打包用的html-webpack-plugin,默认在打包的时候会把注释删除掉


image.png


修改vuecli的配置如下可以解决,将removeComments设置为false即可


module.exports = {
// 其他配置项...
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].minify = {
removeComments: false,
// 其他需要设置的参数
};
return args;
});
},
};

7. 假如你想给qiankun的子应用添加一些git的注释信息,可以在meta中添加


 <meta name="description"
content="Date:<%= htmlWebpackPlugin.options.banner.date %>,Branch:<%= htmlWebpackPlugin.options.banner.branch %>,commitHash: <%= htmlWebpackPlugin.options.banner.commitHash %>,lastCommitDateTime:<%= htmlWebpackPlugin.options.banner.lastCommitDateTime %>">


渲染在html上如下,也可以快速的看到子应用的构建时间和具体的分支
image.png


总结



  1. 借助git-revision-webpack-plugin的能力读取到git的一些信息

  2. 将变量注入在html中或者使用DefinePlugin进行声明变量

  3. 读取变量后显示在html上或者打印在控制台上可以把关键的信息保留,方便我们排查问题


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

码农的畅想:年入10个小目标

本文将以自己真实的创业项目为例,给大家分享如何写一个用于融资的BP(商业计划书)。 使命愿景 我们为小型艺培机构或个体老师提供好用的招生引流工具和教学课件,让老师能够更加专注教学和提升服务体验。 使命:让天下没有难做的艺术培训,通过艺术培训提升人类的幸福感。...
继续阅读 »

本文将以自己真实的创业项目为例,给大家分享如何写一个用于融资的BP(商业计划书)。


使命愿景


我们为小型艺培机构或个体老师提供好用的招生引流工具和教学课件,让老师能够更加专注教学和提升服务体验。



  • 使命:让天下没有难做的艺术培训,通过艺术培训提升人类的幸福感。

  • 愿景:成为艺培行业的贝壳(线下培训蜗牛艺术中心类似链家,线上平台艺培助理类似贝壳),实现年营收一百亿。


产品及服务


线下业务为蜗牛艺术中心,以提供融合了绘本阅读、艺术创作和图形编程的跨学科美育培训为主,同时也开设书法、舞蹈和音乐等品类的培训。


线上业务为艺培助理,是美术、音乐和舞蹈等培训机构教研、招生及运营的好帮手,引流产品为3D画展,现金产品为海报设计,利润产品为课程加盟。



  • 载体:小程序、网页应用和APP。

  • 服务:3D展厅、海报设计、拼团招生和课程加盟。

  • 策略:移动端优先,通过海报设计和课程研发大赛获取目标用户,借助AIGC技术提升生产力,用产品力说话,靠口碑裂变。


团队



  • 陈XX:创始人 CEO 产研负责人 美团技术专家 毕业于交大和某军校 曾在798当过美术馆长 有5年以上线下艺培经验。

  • 杨XX:运营合伙人 毕业于交大和暨南大学,和创始人认识了16年 并一起经营了一家跨境电商服务公司,实现年营收500多万。

  • 熊XX:教研合伙人 清华美院硕士和美育研究所委员 曾任探月学院美育教研负责人 和创始人认识了10年,三年前就一起尝试过创业,拥有15年艺培经验。


财务顾问:xxx,创始人的亲戚,曾任上市公司董事长助理,北大光华管理学院硕士。


行业背景


从21年底开始,国家大力限制学科培训,并于23年底教育部下发通知要大力推进跨学科美育,艺术教育将迎来大爆发,2025年的市场规模将由之前预期的2000亿增长为3000亿。


虽然新生儿的人口相比高峰下降了近一半,但艺术教育中偏兴趣的低龄目标学生(2到12岁),五年内也能维持在一个亿左右,而偏应试的大龄目标学生(12到19岁),也有一个亿左右,还有正在高速发展的成人艺培培训。


所以,整个艺培市场的规模,还能保持年复20%左右的增长,未来五年至少能达到5000亿(仅考虑低龄艺培市场,若人均5000元,渗透率50%即可达到)。


存在痛点


但由于经济下行,家长对课程和服务体验的要求越来越高,艺培机构普遍招生很难,急需技术赋能传统教育机构,提升产品服务标准程度、提高管理效率和坪效、缩短回本周期,技术的完善应用,将给予连锁教育机构实现标准化、规模化的机会。


相比学科培训,艺培的标准化要难很多,而且做服务的天花板很低,最多只能达到10亿的级别,巨头看不上,小团队又搞不定,目前还没有平台能够给艺培机构,提供系统化的通用解决方案,简化机构的日常工作,让机构能够更多的关注学生及家长,做好最核心的教学服务。


近几年,移动互联网发展已经非常成熟,服务艺培机构招生运营某个环节的软件,在市面上已经有了很多,不论是拼团招生的工具,还是海报设计的平台,或是校区管理的saas软件,都只能部分解决艺培机构的需求。


竞品及我们的优势


从海报设计看,有稿定设计、美图设计室、创客贴、爱设计、图怪兽等知名的设计平台,只有稿定设计的艺培模板素材相对丰富,但都不是专门面向艺培机构的,模板和素材不够丰富,且机构使用海报模板后,还需结合自己的招生运营方案做较大调整。我们提供的海报都是我们线下培训门店真实用于招生的海报,并且通过设计大赛,让用户也能为其他用户提供海报及素材,所以使用海报模板后,只需简单改一改品牌、logo和图片即可投入使用。


从3D展厅看,目前很多公司提供的展厅以面向政府企业的宣传为主,比如党建展览、历史回顾等,多数不支持实时在3D模型中去动态加载图片,使用成本比较高。我们提供3D展厅,是专门面向艺培机构的,可以实时动态修改画展中的作品,只要在后台替换了图片,系统无需上线,用户再次打开,展现的就是最新作品,我们可以做到一个展厅最低只需10元,甚至可以作为免费引流的工具。


从拼团招生看,市面上有很多第三方的招生公司,他们一般提供的方案是198元6次课,学生购买拼团课的钱,机构一分钱也拿不到,并且课次太多,机构的转化率也不高。我通过师训教会机构自己按照流程去组织拼团活动即可,我们的收费不到竞品的十分之一,甚至为了引流,可以完全免费。


从课程加盟看,目前市面美术做的比较好的有本来计画和小央美,但他们提供的都是传统的美术课程,并且软件使用的是第三方的课件系统,用户体验比较差;我们提供的是教育部23年底倡导的跨学科美育课程,不仅课程辨识度高,而且我们将艺术创作与绘本阅读及图形编程结合,其他机构很难模仿。


项目现状及展望


当前,线下培训方向:我们已经在深圳开了一家门店并实现盈利,近期正在郑州和北京各开一家分店(都有一定学生基础,基于已有的店进行跨学科升级),招聘了5个全职的美术老师和3个实习生;线上培训方向:艺培助理的小程序和网站均已上线,海报设计、拼团招生和3D展厅均已正式投入使用,近期正在接入一个合作的课程加盟老师,将带来一千个种子用户,日活突破一千(每天都需使用课件进行备课和上课)。


接下来的计划是,先融资500到1000万,组建10人的产研团队,根据种子用户的反馈,优化系统和收费方案,组织海报设计和课程研发大赛,实现线上业务月营收突破一百万;同时,在多个大城市打造10家线下培训旗舰店,实现月营收突破200万,为开展师训和课程加盟做好准备。


后续将会根据发展进行多轮融资,不断完善艺培助理的功能和服务,比如打造社区、增加招聘板块、提供短视频、支持直播等,为1000万个学生提供个性化的3D展厅(每个每年20元),发展100万个艺培助理会员(年费300元),同时在大城市开设100家直营门店,并为5000家艺培机构提供课程加盟,年营收达到:100020万+ 100300万 + 300100万 + 50004万 = 10亿。


资金及项目规划



  • 股权架构:创始人 45% 联合创始人 25% 运营合伙人+技术合伙人20% 其他员工股权池 10%。

  • 融资需求:500~1000万,出让20%的股权。

  • 资金使用:50%用于搭建产研团队,30%用于为海报设计和课程制作大赛提供奖金,20%用于投放广告。

  • 未来融资:一年后实现月营收100万,启动A轮融资2000~5000万,扩大规模,实现月营收一千万,三年后启动B轮融资一到三亿元,实现月营收五千万,五年后启动C轮融资,业务多元化,实现月营收一亿以上。


总结


小富靠勤,大富靠命。要坚信,命运永远掌握在自己手中。


随着这两年大模型技术的突飞猛进,很多简单重复的智力工作将被AI替代,大家将有更多的时间去丰富自己的精神需要,艺培行业的市场规模一定可以超过万亿。


如今,美术在线培训的美术宝和钢琴在线培训的vip陪练,都已经实现了年营收超过20亿,我们作为oMo模式的先行者,未来五年实现年营收10个亿只是个小目标。


我相信,这个商业计划能够实现的可能性很大的,在此分享给大家,即使我没有实现,肯定也会有其他人可以实现。


作者:文艺码农天文
来源:juejin.cn/post/7376925694613274674
收起阅读 »

还在用&nbsp;来当作空格?别忽视他对样式的影响!

web
许久没有更新博客了,今天就抽空来分享下之前遇到个有意思的现象~ 奇怪的现象,被换行的单词 在一次新需求完工之后,进行国际化样式优化时,我发现了一个奇怪的现象:即使页面元素有word-wrap:break-word; 样式属性,单词也照样会被直接裁断换行。 这...
继续阅读 »

许久没有更新博客了,今天就抽空来分享下之前遇到个有意思的现象~


奇怪的现象,被换行的单词


在一次新需求完工之后,进行国际化样式优化时,我发现了一个奇怪的现象:即使页面元素有word-wrap:break-word; 样式属性,单词也照样会被直接裁断换行。


image.png


这又是为什么嘞?细细分析页面元素,突然发现或许这与之前的踩过的坑:特殊的不换行空格有关?!


来复现吧!


那我们马上就来试一试!


  <style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>

<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This&nbsp;is&nbsp;a&nbsp;long&nbsp;a&nbsp;long&nbsp;sentence</div>
</div>

image.png


很明显,单词直接被强行换行拆分了!


那会不会是页面解析的时候,把 &nbsp; 连同其他单词一起,当作一长串单词来处理了,所以才不换行的嘞?


你知道空格转义符有几种写法吗?


那我们就再来试试!不使用 &nbsp; 转而使用其他空格转义符呢?


其实除了 &nbsp; ,还有其他很多种空格转义符。


1. 半角空格


&ensp;

它才是典型的“半角空格”,全称是En Space,en是字体排印学的计量单位,为em宽度的一半。根据定义,它等同于字体度的一半(如16px字体中就是8px)。名义上是小写字母n的宽度。此空格传承空格家族一贯的特性:透明的,此空格有个相当稳健的特性,就是其占据的宽度正好是1/2个中文宽度,而且基本上不受字体影响。


2. 全角空格


&emsp;

从这个符号到下面, 我们就很少见到了, 它叫“全角空格”,全称是Em Space,em是字体排印学的计量单位,相当于当前指定的点数。例如,1 em在16px的字体中就是16px。此空格也传承空格家族一贯的特性:透明的,此空格也有个相当稳健的特性,就是其占据的宽度正好是1个中文宽度,而且基本上不受字体影响。


3. 窄空格


&thinsp;

窄空格,全称是Thin Space。我们不妨称之为“瘦弱空格”,就是该空格长得比较瘦弱,身体单薄,占据的宽度比较小。它是em之六分之一宽。


4. 零宽不连字


&zwnj;

它叫零宽不连字,全称是Zero Width Non Joiner,简称“ZWNJ”,是一个不打印字符,放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。Unicode中的零宽不连字字符映射为“”(zero width non-joiner,U+200C),HTML字符值引用为: ‌


5. 零宽连字


&zwj;

它叫零宽连字,全称是Zero Width Joiner,简称“ZWJ”,是一个不打印字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。零宽连字符的Unicode码位是U+200D (HTML: ‍ ‍)。


再次尝试复现-&thinsp;


  <style>
.normal_style{
width:70px;
height:200px;
margin-right:150px;
border:1px solid red;
/* 👇表示 如果一个单词超出行长度,要截取换行,其他默认;👇 */
word-wrap:break-word;
}
</style>

<div style="display:flex;">
<div class='normal_style'>This is a long a long sentence</div>
<div class='normal_style'>This&nbsp;is&nbsp;a&nbsp;long&nbsp;a&nbsp;long&nbsp;sentence</div>
<div class='normal_style'>This&thinsp;is&thinsp;a&thinsp;long&thinsp;a&thinsp;long&thinsp;sentence</div>
</div>

image.png


我们可以看到 &thinsp; 进行转义的话,单词截取换行是正常的!所以,真凶就是 &nbsp; 特殊的不换行空格!


如何修订?


因为这个提示框是使用公司自制的 UI 组件实现的,而之所以使用 &nbsp; 进行转义是为了修订XSS注入。(对,这个老东西现在没人维护,还是我去啃源码加上的,使用了公共的转义方法)。最后就简单去修改这个公共方法吧!使用了最贴近 &nbsp; 宽度的空格转义符:&thinsp;


作者:bachelor98
来源:juejin.cn/post/7403953367766859828
收起阅读 »

数据大屏的解决方案

web
1. 使用缩放比例适配各种设备(适用16*9比例的屏幕分辨率) 封装一个获取缩放比例的工具函数 /** * 大屏效果需要满足16:9的屏幕比例,才能达到完美的大屏适配效果 * 其他比例的大屏效果,不能铺满整个屏幕 * @param {*} w 设备宽度...
继续阅读 »

1. 使用缩放比例适配各种设备(适用16*9比例的屏幕分辨率)



  1. 封装一个获取缩放比例的工具函数


    /**
    * 大屏效果需要满足16:9的屏幕比例,才能达到完美的大屏适配效果
    * 其他比例的大屏效果,不能铺满整个屏幕
    * @param {*} w 设备宽度 默认 1920
    * @param {*} h 设备高度 默认 1080
    * @returns 返回值是缩放比例
    */

    export function getScale(w = 1920, h = 1080) {
    const ww = window.innerWidth / w
    const wh = window.innerHeight / h
    return ww < wh ? ww : wh
    }


  2. vue中使用方案如下


    <template>
    <div class="full-screen-container">
    <div id="screen">
    大屏展示的内容
    </div>
    </div>
    </template>
    <script>
    import { getScale } from "@/utils/tool";
    import screenfull from "screenfull";
    export default {
    name: "cockpit",
    mounted() {
    if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
    screenfull.request();
    }
    this.setFullScreen();
    },
    methods: {
    setFullScreen() {
    const screenNode = document.getElementById("screen");
    // 非标准设备(笔记本小于1920,如:1366*768、mac 1432*896)
    if (window.innerWidth < 1920) {
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    } else if (window.innerWidth === 1920) {
    // 标准设备 1920 * 1080
    screenNode.style.left = 0;
    screenNode.style.transform = `scale(1) translate(0, 0)`;
    } else {
    // 大屏设备(4K 2560*1600)
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    }
    // 监听视口变化
    window.addEventListener("resize", () => {
    if (window.innerWidth === 1920) {
    screenNode.style.left = 0;
    screenNode.style.transform = `scale(1) translate(0, 0)`;
    } else {
    screenNode.style.left = "50%";
    screenNode.style.transform = `scale(${getScale()}) translate(-50%, 0)`;
    }
    });
    },
    },
    };
    </script>
    <style lang="scss">
    .full-screen-container {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background-color: #131a2b;
    #screen {
    position: fixed;
    width: 1920px;
    height: 1080px;
    top: 0;
    transform-origin: left top;
    color: #fff;
    }
    }
    </style>


  3. mac设备上的屏幕分辨率,在适配的时候,可能不是那么完美,以短边缩放为准,所以宽度到达百分之百后,高度不会铺满



    1. 1432*896 13寸mac本

    2. 2560*1600 4k屏幕




2. 使用第三方插件来实现数据大屏(mac设备会产生布局错落)



  1. 建议在全屏容器内使用百分比搭配flex进行布局,以便于在不同的分辨率下得到较为一致的展示效果。

  2. 使用前请注意将bodymargin设为0,否则会引起计算误差,全屏后不能完全充满屏幕。

  3. 使用方式


    1. npm install @jiaminghi/data-view
    2. yarn add @jiaminghi/data-view

    // 在vue项目中的main.js入口文件,将自动注册所有组件为全局组件
    import {fullScreenContainer} from '@jiaminghi/data-view'
    Vue.use(fullScreenContainer)

    <template>
    <dv-full-screen-container>
    要展示的数据大屏内容
    这里建议高度使用百分比来布局,而且要考虑mac设备适配问题,防止百分比发生布局错乱
    需要注意的点是,一个是宽度,一个是字体大小,不产生换行
    </dv-full-screen-container>
    </template>
    <script>
    import screenfull from "screenfull";
    export default {
    name: "cockpit",
    mounted() {
    if (screenfull && screenfull.enabled && !screenfull.isFullscreen) {
    screenfull.request();
    }
    }
    };
    </script>
    <style lang="scss">
    #dv-full-screen-container {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    background-color: #131a2b;
    }
    </style>


  4. 插件地址


3. 效果图


image.png


作者:狗尾巴花的尖
来源:juejin.cn/post/7372105071573663763
收起阅读 »

从编程语言的角度,JS 是不是一坨翔?一个来自社区的暴力观点

web
给前端以福利,给编程以复利。大家好,我是大家的林语冰。 00. 写在前面 毋庸置疑,JS 的历史包袱确实罄竹难书。用朱熹的话说,天不生 ES6,JS 万古如长夜。 就技术细节而言,ES6 之前,JS 保留了若干臭名昭著的反人类设计,包括但不限于: typeo...
继续阅读 »

给前端以福利,给编程以复利。大家好,我是大家的林语冰。


00. 写在前面


毋庸置疑,JS 的历史包袱确实罄竹难书。用朱熹的话说,天不生 ES6,JS 万古如长夜。


就技术细节而言,ES6 之前,JS 保留了若干臭名昭著的反人类设计,包括但不限于:



  • typeof null 的抽象泄露

  • == 的无理要求

  • undefined 不是关键词

  • 其他雷区......


幸运的是,ES5 的“阉割模式”(strict mode)把一大坨 JS 的反人类设计都屏蔽了,后 ES6 时代的 JS 焕然一新。


所以,本期我们就从编程语言的宏观设计来思考,先不纠结 JS 极端情况的技术瑕疵,探讨一下 JS 作为地球上人气最高的编程语言,设计哲学上到底有何魅力?


00-js.png



免责声明:上述统计数据来源于 GitHub 社区,可能存在统计学偏差,仅供粉丝参考。



01. 标准化


编程语言人气排行榜屈居亚军的是 Python,那我们就用 JS 来打败 Python。


根据 MDN 电子书,JS 的核心语言是 ECMAScript,是一门由 ECMA TC39 委员会标准化的编程语言。并不是所有的编程语言都有标准化流程,JS 恰好是“天选之子”,后 ES6 的提案流程也相对稳定,虽然最近突然增加了 stage 2.7,但整体标准化无伤大雅。


值得一提的是,JS 的标准是向下兼容的,用户友好。JS 的大多数技术债务已经诉诸“阉割模式”禁用,而向下兼容则避免了近未来的 ESNext 出现主版本级别的破坏性更新。


举个栗子,ES2024 最新支持的数组分组提案,出于兼容性考虑,提案一度从 Array.prototype.group() 修改为 Object.groupBy()


02-group.png


可以看到,JS 的向下兼容设计正是为了防止和某些遗留代码库产生命名冲突,降低迁移成本。


换而言之,JS 不会像 Python 2 升级 Python 3 那样,让用户承担语言迭代伴生的兼容性税,学习成本和心智负担相对较小。


02. 动态类型


编程语言人气排行榜屈居季军的是 TS,那我们就用 JS 来打败 TS。


JS 和 TS 都是 ECMAScript 的超集,TS 则是 JS 的超集。简而言之,TS ≈ JS + 静态类型系统。


JS 和 TS 区别在于动态类型 vs 静态类型,不能说孰优孰劣,只能说各有千秋。我的个人心证是,静态类型对于工程化生态而言不可或缺,比如 IDE 或 Linting,但对于编程应用则不一定,因为没有静态类型,JS 也能开发 Web App。


03-ts.png


可以看到,因为 TS 是 JS 的超集,所以虽然没有显式的类型注解,上述代码也可作为 TS 代码,只不过 TS 和 JS 类型检查的粒度和时机并不一致。


一个争论点在于,静态类型的编译时检查有利于服务大型项目,但其实在大型项目中,一般会诉诸单元测试保障代码质量和回归测试。严格而言,静态类型之于大型项目的充要条件并不成立。


事实上,剥离静态类型注解的源码,恰恰体现了编程的本质和逻辑,这也是后来的静态类型语言偏爱半自动化的智能类型系统,因为有的类型注解可能是画蛇添足,而诉诸智能的类型推论可以解放程序猿的生产力。


03. 面向对象


编程语言人气排行榜第四名是 Java,那我们就用 JS 来打败 Java。


作为被误解最深的语言,前 ES6 的 JS 有一个普遍的误区:JS 不是面向对象语言,原因在于 ES5 没有 class。


这种“思想钢印”哪里不科学呢?不科学的地方在于,面向对象编程是面向对象,而不是 面向类。换而言之,类不是面向对象编程的充要条件


作为经典的面向对象语言,Java 不同于 C艹,不支持多继承。如果说多继承的 C艹 是“面向对象完备”的,那么能且仅能支持单继承的 Java,其面向对象也一定有不完备的地方,需要诉诸其他机制来弥补。


JS 的面向对象是不同于经典类式继承的原型机制,为什么无类、原型筑基的 JS 也能实现多继承的 C艹 的“面向对象完备”呢?搞懂这个问题,才能深入理解“对象”的本质。


04-oop.png


可以看到,JS 虽然没有类,但也通过神秘机制具备面向对象的三大特征。如果说 JS 不懂类,那么经典面向对象语言可能不懂面向对象编程,只懂“面向类编程”。


JS 虽然全称 JavaScript,但其实和 Java 一龙一猪,原型筑基的 JS 没有类也问题不大。某种意义上,类可以视为 JS 实现面向对象的语法糖。很多人知道封装、继承和多态的面向对象特性,但不知道面向对象的定义和思想,所以才会把类和“面向对象完备”等同起来,认为 JS 不是面向对象的语言。


04. 异步编程


编程语言人气排行榜第五名是 C#,那我们就用 JS 来打败 C#。


一般认为,JS 是一门单线程语言,有且仅有一个主线程。JS 有一个基于事件循环的并发模型,这个模型与 C# 语言等模型一龙一猪。


在 JS 中,当一个函数执行时,只有在它执行完毕后,JS 才会去执行任何其他的代码。换而言之,函数执行不会被抢占,而是“运行至完成”(除了 ES6 的生成器函数)。这与 C 语言不同,在 C 语言中,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。


单线程的优势在于,大部分情况下,JS 不需要考虑多线程某些让人头大的复杂处理。因为我只会 JS,所以无法详细说明多线程的痛点,比如竞态、死锁、线程间通信(消息、信道、队列)、Actor 模型等。


但 JS 的单线程确实启发了其他支持多线程的语言,比如阮一峰大大的博客提到,Python 的 asyncio 模块只存在一个线程,跟 JS 一样。


05-async.jpg


在异步编程方面,JS 的并发模型其实算是奇葩,因为主流的编程语言都支持多线程模型,所以学习资料可以跨语言互相借鉴,C# 关于多线程的文档就有 10 页,而单线程的 JS 就像非主流的孤勇者,很多异步的理论都用不上,所以使用起来较为简单。


05. 高潮总结


JS 自诞生以来就是一种混合范式的“多面手语言”,这是 JS 二十年来依然元气满满的根本原因。


举个栗子,你可能已经见识过“三位一体”的神奇函数了:


06-fn.png


可以看到,在 ES6 之前,JS 中的函数其实身兼数职,正式由于 JS 是天生支持混合范式导致的。


前 JS 时代的先驱语言部分是单范式语言,比如纯粹的命令式过程语言 C 语言无法直接支持面向对象编程,而 C艹 11、Java 8 等经典面向对象语言则渐进支持函数式编程等。


后 JS 时代的现代编程语言大都直接拥抱混合范式设计,比如 TypeScript 和 Rust 等。作为混合范式语言,JS 是一种原型筑基、动态弱类型的脚本语言,同时支持面向对象编程、函数式编程、异步编程等编程范式或编程风格。


粉丝请注意,你可能偶尔会看到“JS 是一门解释型语言”的说法,其实后 ES6 时代的 JS 已经不再是纯粹的解释型语言了,也可能是 JIT(即时编译)语言,这取决于宿主环境中 JS 引擎或运行时的具体实现。


值得一提的是,混合范式语言的优势在于博采众家之长,有助于塑造攻城狮的开放式编程思维和心智模型;缺陷在于不同于单范式语言,混合范式语言可能支持了某种编程范式,但没有完全支持。(PS:编程范式完备性的判定边界是模糊的,所以偶尔也解读为风格或思维。)


因此,虽然 JS 不像 PHP 一样是地球上最好的语言,但 JS 作为地表人气最高的语言,背后是有深层原因的。


参考文献



作者:前端俱乐部
来源:juejin.cn/post/7392787221097545780
收起阅读 »

此生最佩服数学家

大家好啊,我是董董灿。 之前在和不少小伙伴聊天时,都时不时的提到,搞人工智能尤其是搞算法,数学是一座很难跨过去的砍,数学太难了。 我本身也不是数学专业的,在搞AI算法的过程中,也确实遇到了很多数学问题。 用大学学的那点线性代数、概率论和微积分的知识,来推一些...
继续阅读 »

大家好啊,我是董董灿。


之前在和不少小伙伴聊天时,都时不时的提到,搞人工智能尤其是搞算法,数学是一座很难跨过去的砍,数学太难了。



我本身也不是数学专业的,在搞AI算法的过程中,也确实遇到了很多数学问题。


用大学学的那点线性代数、概率论和微积分的知识,来推一些枯燥的数学公式,就好像是拄着拐杖去跑马拉松,虽然查查资料磨蹭磨蹭也能弄出来,但是感觉很费劲。


数学真的就是算法的基石,数学能力强、抽象能力强的人,有时候在学算法时,就像降维打击,他们会从很不可思议的角度来论证,某某算法确实是好的。


我之前见过一个同事,中科大少年班毕业,数学专业的(数学水平很高,至少比我高),有一次在和他讨论某个算法的实现时,他全程用一种我听不懂的话在说,我当时是记了一些关键字,回到工位查了很久很久。


被打击了。


数学公式和理论对我而言是枯燥的,但是数学故事是有趣的。今天,就说一个与数学相关的故事,号称“数学史上的三次危机”。


很早就听说过这个说法了,前几天在查概率论相关的资料时,突然想起来,分享一下。


在很多数学专业的学生来看,这种说法并不严谨。在中外文献中,并不存在所谓的“数学史上的三次危机”,这种说法更多的出现在科普文章以及流行度较高的民间科学杂志上。


1、第一次数学危机:长方形的对角线是什么?


在古希腊时期,毕达哥拉斯学派统治的年代,人们对于数学的认识就是:一切都是数字,这里说的数字,是我们现在理解的有理数,也就是1、2、3这种。


那时的人们认为,万事万物都可以用有理数来衡量,在一个数轴上,任何一个点都可以用一个确定的有理数字来表示。


可突然有一天,一个人站出来说,边长为 1 的正方形的对角线,在数轴上就表示不出来。



人们慌了,毕达哥拉斯学派更慌了。


对这个问题他们百思不得其解,随着问题传播的越来越广,人们开始担心,引以为傲的“一切都是数字”的数学理论,是不是有可能是错误的。


这引起了第一次数学理论基础的危机。


毕达哥拉斯学派,不允许这种不和谐的声音出现,来诋毁自己的地位,但是,当时的他们又解决不了这个问题。


于是,他们秉着解决不了这个人提出的问题,就解决了这个人的原则,把提出这个问题的人解决了。


这次危机持续了很久,直到人们提出了无理数,并且接受了无理数的存在,第一次数学危机才得以解决。


2、第二次数学危机:兔子到底能不能追上乌龟?


这是关于龟兔赛跑的故事。


有人说,如果乌龟先跑,兔子后跑。


当兔子跑到乌龟已跑出距离的一半时,乌龟又前进了一段距离,而当兔子又跑到这一段距离的时候,乌龟此时又前进了一段距离,就这样无穷无尽的跑下去,兔子永远也追不上乌龟。


这就是“龟兔赛跑”悖论,这个悖论直接导致了当时数学界的恐慌。


悖论很反直觉,但是好像又无懈可击。



人们钻研了很久,却始终找不出问题出在什么地方。


当时的人们认为:数学完了,这么简单的问题都解决不了,数学根本不靠谱。


这就好像现在有人告诉你,高铁永远追不上骑自行车的人,但是你又没办法反驳一样。


明知是错的,我却无能为力。


这便是人们津津乐道的第二次数学危机,并且直接导致了无穷与极限的发展,以及后来微积分思想的发展。


到现在,无穷级数和微积分的数学根基已经很牢固了,但是如果回过头来,如果你想反驳一下这个悖论,你应该怎么说呢?


或许你可以这么说:


世界上没有这样的兔子和乌龟,可以活无穷长的时间,这是因为时间是不可能无穷拆分的。


那如果有人继续反驳问你,你怎么证明时间是不可以无穷拆分的呢?


你就说,那是物理学家的事,不是数学家的事,让物理学家思考去吧。


反正,在无穷与极限的概念的发展中,这次危机也算是渡过了。


3、第三次数学危机:理发师该不该给自己理发


一个村子里有个理发师,突然有一天这个理发师贴了一个公告说:我只给这个村子里不给自己理发的人理发。


然后有个人跑上门问他:那你自己的头发应该谁来理呢?


理发师懵了。


如果他给自己理发,那他就不是不给自己理发的人,他就不应该给自己理发。


如果他不给自己理发,那么他就是不给自己理发的人,他就应该给自己理发。



也就说,如果存在两个互相独立的集合,一个是给自己理发的人,一个是不给自己理发的人,那么理发师属于哪个集合呢?


这就是著名的罗素悖论。


这个悖论的威力在于,当时一个著名的数学家要发表一本数学著作,在收到罗素关于这个悖论的描述后尴尬地说:我以为数学的大厦已经盖好了,没想到地基还这么不牢固。


这个问题通俗点讲就是,你可以说出一件事,如果这件事是真的,那么它就是假的,如果他是假的,那么他就是真的。


数学里的自相矛盾,然而它却符合康托尔关于集合的定义。


这个问题的解决好像是一位大佬级别的数学家,在研究了一段时间后说:不存在这样理发师,他说的话不能当做数学公理,从源头上解决了这个问题。


但是这个悖论促进促进了集合论的进一步发展。


三次数学危机,每一次都让人惶恐不安,但事实却是,每一次都极大的促进了当时数学理论的发展。


好啦,故事就分享到这。


说回AI,AI的发展绝对离不开数学,这也是为什么华为愿意花大价钱雇佣很多数学家、物理学家搞基础研究,阿里每年搞全球数学竞赛,吸纳全球数学精英。


三体里有句话,如果一旦外星文明打来,我们能与之拼一拼的绝对不是火箭大炮,而是基础物理学理论,核弹都得益于数学物理,更何况其他呢。


如果此时你正在高数课堂上,请你打起精神好好听课,没准未来拯救世界的重任就落到了你的肩上。🙃


一直很膜拜数学、物理大佬,如果有数学物理专业的大佬,可在下面留言,让小弟膜拜下~


作者:董董灿是个攻城狮
来源:juejin.cn/post/7294619778987622411
收起阅读 »

你真的了解圣杯和双飞翼布局吗?

web
前言 圣杯和双飞翼布局作为面试常考的题目之一,相信大家肯定都会有着自己的一套知识储备,但是,你真的了解这两种经典布局吗?本文将介绍这两种经典布局产生的来龙去脉,以及实现这两种布局的一些方式,希望大家能够喜欢。 为什么需要圣杯和双飞翼布局 大家思考一个问题,这样...
继续阅读 »

前言


圣杯和双飞翼布局作为面试常考的题目之一,相信大家肯定都会有着自己的一套知识储备,但是,你真的了解这两种经典布局吗?本文将介绍这两种经典布局产生的来龙去脉,以及实现这两种布局的一些方式,希望大家能够喜欢。


为什么需要圣杯和双飞翼布局


大家思考一个问题,这样一种布局,你该怎么处理呢?


image.png

常规情况下,我们的布局思路应该这样写,从上到下,从左到右


<div>header</div>
<div>
<div>left</div>
<div>main</div>
<div>right</div>
</div>
<div>footer</div>

这样的三栏布局也没有什么问题,但是我们要知道一个网站的主要内容就是中间的部分,比如像掘金:


image.png

那么对于用户来说,他们当然是希望最中间的部分首先加载出来的,能看到最重要的内容,但是因为浏览器加载dom的机制是按顺序加载的,浏览器从HTML文档的开头开始,逐步解析并构建文档对象模型,所以,我们想让main首先加载出来的话,那就将它前置,然后通过一些CSS的样式,将其继续展示出上面的三栏布局的样式:


<div>header</div>
<div>
<div>main</div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>

这就是所谓的圣杯布局,最早是Matthew Levine 在2006年1月30日在In Search of the Holy Grail 这篇文章中提出来的;


那么完整效果、实现代码如下:


image.png


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 100px 0 100px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 100px;
margin-left: -100%;
left: -100px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
margin-left: -100px;
right: -100px;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

如上述代码所示,,使用了相对定位浮动负值margin,将left和right装到main的两侧,所以顾名:圣杯


但是呢,圣杯是有问题的,在某些特殊的场景下,比如说,left和right盒子的宽度过宽的情况下,圣杯就碎掉了,比如将上述代码的left和right盒子的宽度改为以500px为基准:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0 500px 0 500px;
overflow: hidden;
}
.col {
position: relative;
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
left: -500px;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
right: -500px;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

正常情况下,布局还是依然正常,只是两侧宽了而已:


image.png


但是我们将整个窗口缩小,圣杯就碎掉了:


image.png


原因是因为 padding: 0 500px 0 500px;,当整个窗口的最大宽度已经小于左右两边的padding共1000px,left和right就被挤下去了;


于是针对这种情况,淘宝UED的玉伯大大提出来了双飞翼布局,效果和圣杯布局一样,只是他将其比作一只鸟,左翅膀、中间、右翅膀;


相比于圣杯布局,双飞翼布局在原有的main盒子再加了一层div:


<div>header</div>
<div>
<div><div>main</div></div>
<div>left</div>
<div>right</div>
</div>
<div>footer</div>

实际的效果代码如下,哪怕再怎么缩,都不会被挤下去:


image.png


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
padding: 0;
overflow: hidden;
}
.col {
float: left;
height: 400px;
}
.left {
background-color: green;
width: 500px;
margin-left: -100%;
}
.main {
width: 100%;
background-color: blue;
}
.main-in {
margin: 0 500px 0 500px;
}
.right {
width: 500px;
background-color: green;
margin-left: -500px;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

圣杯布局实现方式补充


上面介绍了一种圣杯布局的实现方式,这里再介绍一种用绝对定位的,这种方法其实也能避免上述说的当左右两侧的盒子过于宽时,圣杯被挤破的情况:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
padding: 0 100px;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.main {
width: 100%;
background-color: blue;
}
.right {
width: 100px;
background-color: green;
position: absolute;
right: 0;
top: 0;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="main col">main</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

双飞翼布局实现方式补充


也是使用绝对定位的:


<style>
.header,
.footer {
width: 100%;
height: 200px;
background-color: pink;
}
.container {
position: relative;
}
.col {
height: 400px;
}
.left {
background-color: green;
width: 500px;
position: absolute;
top: 0;
left: 0;
}
.main {
width: calc(100% - 1000px);
background-color: blue;
margin-left: 500px;
}
.main-in {
/* margin: 0 500px 0 500px; */
}
.right {
width: 500px;
background-color: green;
position: absolute;
top: 0;
right: 0;
}
</style>
<div class="header">header</div>
<div class="container">
<div class="main col">
<div class="main-in">main</div>
</div>
<div class="left col">left</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>


其它普通的三列布局的实现


flex布局实现


<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
display: flex;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
}
.main {
flex: 1;
width: 100%;
height: 300px;
background-color: green;
}

.right {
width: 100px;
height: 300px;
background-color: pink;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>

绝对定位实现


<style>
* {
padding: 0;
margin: 0;
}
.header,
.footer {
width: 100%;
height: 200px;
background-color: blue;
}
.container {
position: relative;
padding: 0 100px;
}
.left {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
left: 0;
}
.main {
width: 100%;
height: 300px;
background-color: green;
}

.right {
width: 100px;
height: 300px;
background-color: pink;
position: absolute;
top: 0;
right: 0;
}
</style>

<div class="header">header</div>
<div class="container">
<div class="left col">left</div>
<div class="main col">main</div>
<div class="right col">right</div>
</div>
<div class="footer">footer</div>


总结


现在真正了解到圣杯布局、双飞翼布局和普通三列布局的思想了吗?虽然它们三者最终的效果可能一样,但是实际的思路,优化也都不一样,希望能对你有所帮助!!!!感谢支持


作者:进阶的鱼
来源:juejin.cn/post/7405467437564428299
收起阅读 »