Python之 sorted() 函数的基本语法
sorted() 函数的基本语法
sorted(iterable, key=, reverse=False)
- iterable: 要排序的可迭代对象(列表、元组、字符串等)
- key: 可选,指定排序的依据函数
- reverse: 可选,True 为降序,False 为升序(默认)
基本用法
简单排序
# 数字排序
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(numbers))
# 输出: [1, 1, 2, 3, 4, 5, 6, 9]
# 字符串排序
names = ['Alice', 'Bob', 'Charlie', 'David']
print(sorted(names))
# 输出: ['Alice', 'Bob', 'Charlie', 'David']
# 字符排序
chars = ['z', 'a', 'x', 'b']
print(sorted(chars))
# 输出: ['a', 'b', 'x', 'z']
降序排序
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(numbers, reverse=True)) # 输出: [9, 6, 5, 4, 3, 2, 1, 1]
示例解析
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)
# 输出:
# BRA/CE342567
# ESP/XDA205856
# USA/31195855
解析: 对于元组列表,sorted() 默认按第一个元素排序,如果第一个元素相同,则按第二个元素排序,以此类推。
使用 key 参数进行自定义排序
按字符串长度排序
words = ['python', 'java', 'javascript', 'go', 'rust']
print(sorted(words, key=len))
# 输出: ['go', 'java', 'rust', 'python', 'javascript']
按照绝对值排序
numbers = [-5, -1, 0, 1, 3, -2]
print(sorted(numbers, key=abs))
# 输出: [0, -1, 1, -2, 3, -5]
按照元祖的特定循序排序
# 按第二个元素排序
students = [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
print(sorted(students, key=lambda x: x[1])) # 输出: [('Charlie', 78), ('Alice', 85), ('Bob', 92)]
# 按第一个元素的长度排序
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
print(sorted(traveler_ids, key=lambda x: len(x[0]))) # 输出: [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
复杂排序示例
多级排序
# 先按年龄排序,年龄相同时按姓名排序
people = [
('Alice', 25, 'Engineer'),
('Bob', 30, 'Doctor'),
('Charlie', 25, 'Teacher'),
('David', 30, 'Engineer')
]
# 按年龄升序,然后按姓名升序
sorted_people = sorted(people, key=lambda x: (x[1], x[0]))
print(sorted_people)
# 输出: [('Alice', 25, 'Engineer'), ('Charlie', 25, 'Teacher'), ('Bob', 30, 'Doctor'), ('David', 30, 'Engineer')]
字符串忽略大小写排序
names = ['alice', 'Bob', 'CHARLIE', 'david']
print(sorted(names))
# 输出: ['CHARLIE', 'Bob', 'alice', 'david'] (按ASCII码)
print(sorted(names, key=str.lower))
# 输出: ['alice', 'Bob', 'CHARLIE', 'david'] (忽略大小写)
自定义排序规则
def custom_sort_key(item):
"""自定义排序:数字优先,然后按字母顺序"""
if item.isdigit():
return (0, int(item)) # 数字类型,按数值排序
else:
return (1, item.lower()) # 字母类型,按字母顺序
data = ['z', '10', 'a', '2', 'B', '1']
print(sorted(data, key=custom_sort_key)) # 输出: ['1', '2', '10', 'a', 'B', 'z']
排序复杂数据结构
字典列表排序
students = [
{'name': 'Alice', 'grade': 85, 'age': 20},
{'name': 'Bob', 'grade': 92, 'age': 19},
{'name': 'Charlie', 'grade': 78, 'age': 21}
]
# 按成绩排序
print(sorted(students, key=lambda x: x['grade']))
# 按年龄排序
print(sorted(students, key=lambda x: x['age']))
# 按姓名排序
print(sorted(students, key=lambda x: x['name']))
使用 operator 模块
from operator import itemgetter, attrgetter
# 对字典列表排序
students = [
{'name': 'Alice', 'grade': 85},
{'name': 'Bob', 'grade': 92},
{'name': 'Charlie', 'grade': 78}
]
# 使用 itemgetter 替代 lambda
print(sorted(students, key=itemgetter('grade')))
# 等同于: sorted(students, key=lambda x: x['grade'])
# 多字段排序
print(sorted(students, key=itemgetter('grade', 'name')))
来源:juejin.cn/post/7522863398243549234
解决 Python requests.post() 返回 406 错误
解决 Python requests.post() 返回 406 错误
HTTP 406 "Not Acceptable" 错误表示服务器无法生成符合客户端请求中 Accept 头部指定的响应内容。
问题分析,最好的方法还是一段代码一段代码的调试
今天是模型一个登录的脚本,以前都用得好好的,现在突然不行了,出现了406的错误,觉得有点奇怪,代码如下:
header = {'Content-Type':'application/x-www-form-urlencoded',
'User-Agent':'Mozilla/5.0 (Macint0sh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
form_data = {
"actionFlag":"login",
"uid":"xxxxx",
"password":"xxxxx"
}
data = parse.urlencode(from_data)
response = request.post(url=login_url,header=header, data = data, verify=False)
以前上面这段代码是没有什么问题的,但最近才发现一直是406,跟踪到代码中的
sessions.py中adapter的值是对的,上面是显示访问成功而且返回值是200,这说明访问代码是没有问题的,但什么时候出的问题呢?继续往下

发现了这一段代码,其中allow_redirects的值就有点意思了,这个值的默认值是Ture,但我们登录后,后面肯定有一个redirect的,也就是说后面那个跳转的地址有问题了,所以才会导致出问题

所以只需要将这个参数设计为false就正常了
allow_redirects=False
下面是我解决问题时用deepseek查问题时,出现的一些总结,也不能说它全部不对,但也是由于这个让我有了一些思路,所以放上来,大家参考一下也是好了。
常见原因及解决方案
1. 请求头 (headers) 问题
确保你的请求头中包含正确的 Accept 和 Content-Type:
python
headers = {
'Accept': 'application/json', # 或其他服务器期望的类型
'Content-Type': 'application/json', # 或其他适当的内容类型
# 其他必要的头部,如授权信息
}
2. 数据格式问题
确保发送的数据格式与 Content-Type 头部匹配:
python
import json
data = {'key': 'value'}
response = requests.post(url, data=json.dumps(data), headers=headers)
或者使用 json 参数自动处理:
python
response = requests.post(url, json=data, headers=headers)
3. 用户代理问题
有些服务器要求特定的 User-Agent:
python
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...',
# 其他头部
}
4. 认证问题
如果 API 需要认证,确保提供了正确的凭证:
python
headers = {
'Authorization': 'Bearer your_token_here',
# 其他头部
}
5. 服务器端限制
检查 API 文档,确认:
- 是否支持 POST 方法
- 是否接受你发送的内容类型
- 是否有其他特殊要求
完整示例
python
import requests
import json
url = 'https://api.example.com/endpoint'
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'MyApp/1.0',
'Authorization': 'Bearer abc123...'
}
data = {'key1': 'value1', 'key2': 'value2'}
try:
response = requests.post(url, json=data, headers=headers)
response.raise_for_status() # 如果响应状态码不是200,抛出异常
print(response.json())
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
如果问题仍然存在,建议:
- 检查 API 文档的具体要求
- 使用开发者工具查看浏览器发送的成功请求的格式
- 联系 API 提供商获取支持
来源:juejin.cn/post/7522002830929379368
Java String.replace()原理,你真的了解吗?
大家好呀,我是猿java。
String.replace()是我们日常开发中经常用到的一个方法,那么,你有看过其底层的源码实现吗?你知道String.replace()是如何工作的吗?String.replace()的性能到底怎么样?这篇文章我们来深入地分析。
在开始今天的问题之前,让我们先来看一个问题:
String original = "Hello, World!";
// 替换字符
String result = original.replace('World', 'Java');
original.replace('World', 'Java'),是把 original的内容直接修改成Hello, Java了,还是重新生成了一个 Hello, Java的 String并返回?
1. String.replace()是什么?
String.replace()位于java.lang包中,它是 Java中的一个重要方法,用于替换字符串中的某些字符或子字符串。以下String.replace()的源码截图。

String.replace()方法用于替换字符串中的某些字符或子字符串。它有多个重载版本,常见的有:
// 用于替换单个字符
public String replace(char oldChar, char newChar);
// 用于替换子字符串
public String replace(CharSequence target, CharSequence replacement);
下面是一个简单的示例,演示了replace方法的用法:
public class ReplaceExample {
public static void main(String[] args) {
String original = "Hello, World!";
// 替换字符
String replacedChar = original.replace('o', 'a');
System.out.println(replacedChar); // 输出: "Hella, Warld!"
// 替换子字符串
String replacedString = original.replace("World", "Java");
System.out.println(replacedString); // 输出: "Hello, Java!"
}
}
在上面的例子中,我们演示了如何使用replace方法替换字符和子字符串。需要注意的是,String对象在Java中是不可变的(immutable),因此replace方法会返回一个新的字符串,而不会修改原有字符串。
2. 源码分析
上述示例,我们演示了replace方法的用法,接下来,我们来分析下replace方法的实现原理。
2.1 String的不可变性
Java中的String类是不可变的,这意味着一旦创建了一个String对象,其内容不能被改变。这样的设计有助于提高性能和安全性,尤其在多线程环境下。String源码说明如下:

2.2 replace()工作原理
让我们深入了解replace方法的内部实现。以replace(CharSequence target, CharSequence replacement)为例,以下是其基本流程:
- 检查目标和替换内容:方法首先检查传入的
target和replacement是否为null,如果是,则抛出NullPointerException。 - 搜索目标子字符串:在原始字符串中查找所有符合目标子字符串的地方。
- 构建新的字符串:基于找到的位置,将原始字符串分割,并用替换字符串进行拼接,生成一个新的字符串。
2.3 源码解析
让我们看一下String类中replace方法的源码(简化版):
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
: StringUTF16.replace(value, oldChar, newChar);
if (ret != null) {
return ret;
}
}
return this;
}
public String replace(CharSequence target, CharSequence replacement) {
String tgtStr = target.toString();
String replStr = replacement.toString();
int j = indexOf(tgtStr);
if (j < 0) {
return this;
}
int tgtLen = tgtStr.length();
int tgtLen1 = Math.max(tgtLen, 1);
int thisLen = length();
int newLenHint = thisLen - tgtLen + replStr.length();
if (newLenHint < 0) {
throw new OutOfMemoryError();
}
StringBuilder sb = new StringBuilder(newLenHint);
int i = 0;
do {
sb.append(this, i, j).append(replStr);
i = j + tgtLen;
} while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
return sb.append(this, i, thisLen).toString();
}
解析步骤
- 参数校验:首先检查
target和replacement是否为null,避免后续操作出现NullPointerException。 - 查找目标字符串:使用
indexOf方法查找目标子字符串首次出现的位置。如果未找到,直接返回原字符串。 - 替换逻辑:
- 使用
StringBuilder来构建新的字符串,这是因为StringBuilder在拼接字符串时效率更高。 - 通过循环查找所有目标子字符串的位置,并将其替换为替换字符串。
- 最后,拼接剩余的字符串部分,返回最终结果。
- 使用
性能考虑
由于String的不可变性,每次修改都会创建新的String对象。如果需要进行大量的字符串替换操作,推荐使用StringBuilder或StringBuffer来提高性能。
三、实际示例演示
接下来,我们将通过几个实际的例子,来更好地理解String.replace()的使用场景和效果。
示例1:替换字符
public class ReplaceCharDemo {
public static void main(String[] args) {
String text = "banana";
String result = text.replace('a', 'o');
System.out.println(result); // 输出: "bonono"
}
}
解释:将所有的'a'替换为'o',得到"bonono"。
示例2:替换子字符串
public class ReplaceStringDemo {
public static void main(String[] args) {
String text = "I love Java. Java is versatile.";
String result = text.replace("Java", "Python");
System.out.println(result); // 输出: "I love Python. Python is versatile."
}
}
解释:将所有的"Java"替换为"Python",结果如上所示。
示例3:替换多个不同的子字符串
有时,我们可能需要在一个字符串中替换多个不同的子字符串。例如,将文中的标点符号替换为空格:
public class ReplaceMultipleDemo {
public static void main(String[] args) {
String text = "Hello, World! Welcome to Java.";
String result = text.replace(",", " ")
.replace("!", " ")
.replace(".", " ");
System.out.println(result); // 输出: "Hello World Welcome to Java "
}
}
解释:通过链式调用replace方法,依次将,、!和.替换为空格。
示例4:替换不匹配的情况
public class ReplaceNoMatchDemo {
public static void main(String[] args) {
String text = "Hello, World!";
String result = text.replace("Python", "Java");
System.out.println(result); // 输出: "Hello, World!"
}
}
解释:由于"Python"在原字符串中不存在,replace方法不会做任何替换,直接返回原字符串。
四、String.replace()的技术架构图
虽然文字描述已能帮助我们理解replace方法的工作原理,但通过一个简化的技术架构图,可以更直观地抓住其核心流程。
+---------------------------+
| String对象 |
| "Hello, World!" |
+------------+--------------+
|
| 调用replace("World", "Java")
v
+---------------------------+
| 搜索目标子字符串 "World" |
+------------+--------------+
|
| 找到位置 7
v
+---------------------------+
| 构建新的字符串 "Hello, Java!" |
+---------------------------+
|
| 返回新字符串
v
+---------------------------+
| 新的 String对象 |
| "Hello, Java!" |
+---------------------------+
图解说明
- 调用
replace方法:在原始String对象上调用replace("World", "Java")。 - 搜索目标:方法内部使用
indexOf找到"World"的位置。 - 构建新字符串:使用
StringBuilder将"Hello, "与"Java"拼接,形成新的字符串"Hello, Java!"。 - 返回新字符串:最终返回一个新的
String对象,原始字符串保持不变。
五、总结
通过本文的介绍,相信你对Java中String.replace()方法有了更深入的理解。从基本用法到内部原理,再到实际应用示例,每一步都帮助你全面掌握这个重要的方法。
记住,String的不可变性设计虽然带来了安全性和线程安全性,但在频繁修改字符串时,可能影响性能。因此,合理选择使用String还是StringBuilder,根据具体场景优化代码,是每个Java开发者需要掌握的技能。
希望这篇文章能对你在Java编程的道路上提供帮助。如果有任何疑问或更多的讨论,欢迎在评论区留言!
8. 学习交流
如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。
来源:juejin.cn/post/7543147533368229903
一万行代码实现的多维分析表格,让数据处理效率提升 300%
上个月在 趣谈AI 发布了我实现的多维表格1.0版本,没有用到任何第三方组件,完全组件化设计。最近对多维表格进行了进一步的升级优化,满打满算花了接近3个月时间,累计代码接近1w行。

接下来就和大家聊聊我做的 flowmix/mute多维表格 的核心功能和技术实现。
核心功能介绍
1. 多视图模式

目前多维表格支持多种视图模式:表格视图,看板视图,人员分配视图。用户可以轻松在不同视图下切换并进行可视化操作数据。
2. 多条件筛选功能

我们可以基于不同维度进行筛选和排序,并支持组合筛选。
3. 多维度分组功能

表格视图中,我们可以基于用户,优先级,状态,对数据进行分组管理,提高表格数据的查看效率。
4. 表格字段管理功能

多维表格中不仅支持字段的管理控制,同时还支持添加自定义字段:

5. 表格行列支持自定义拖拽排序功能

表格我们不仅仅支持列的宽度拖拽,还支持拖拽调整列的排序,同时表格的行也支持拖拽,可以跨分组进行拖拽,也支持在组内进行拖拽排序,极大的提高了数据管理的效率。
6. 表格支持一键编辑

我们可以在菜单按钮中开启编辑模式,也可以双击编辑单元格一键编辑表格内容,同时大家还可以进行扩展。
7. 表格支持一键转换为可视化分析视图表

我们可以将表格数据转换为可视化分析图表,帮助管理者更好地掌握数据动向。
8. 表格支持一键导入任务数据

目前多维表格支持导出和导入json数据,并一键渲染为多维表格。技术实现多维表格的设计我采用了组件化的实现的方式, 并支持数据持久化,具体使用如下:
<div className="flex-1 bg-gray-50">
{currentView === "tasks" && <TaskManagementTable sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />}
{currentView === "statistics" && <StatisticsView />}
{currentView === "documentation" && <DocumentationView />}
{currentView === "assignment" && <AssignmentView />}
{currentView === "deployment" && <DeploymentView />}
</div>
在开发多维表格的过程中其实需要考虑很多复杂逻辑,比如表格用什么方式渲染,如何优化表格性能,如何实现表格的列排序,行排序,表格编辑等。传统表格组件大多基于div模拟行列,虽然灵活但渲染性能差。所以可以做如下优化:
- 虚拟滚动当数据量超过 500 行时,启用虚拟滚动机制,仅渲染可见区域的 DOM 节点,内存占用降低 70%;
- 行列冻结通过固定定位
position: sticky实现表头和固定列冻结,解决大数据表格的滚动迷失问题; - 异步加载采用
Intersection Observer监听表格滚动事件,动态加载可视区域外的数据,避免一次性请求全量数据。
接下来分享一下简版的虚拟滚动的实现方案:
// 虚拟滚动核心代码(简化版)
function renderVirtualTable(data, visibleHeight) {
const totalRows = data.length;
const rowHeight = 40; // 行高固定
const visibleRows = Math.ceil(visibleHeight / rowHeight);
const startIndex = scrollTop / rowHeight | 0;
const endIndex = startIndex + visibleRows;
// 渲染可见区域数据
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const row = document.createElement('tr');
row.innerHTML = data[i].cells.map(cell => `<td>${cell.value}</td>`).join('');
fragment.appendChild(row);
}
// 更新滚动条高度和偏移量
table.scrollHeight = totalRows * rowHeight;
table.innerHTML = `<thead>${header}</thead><tbody>${fragment}</tbody>`;
}
对于大表格数据量需要在本地缓存,所以需要设计表格数据的缓存处理逻辑,目前我采用的是hooks的实现方案,具体实现如下:
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
其实在实现多维表格的过程中,我也调研了很多开源的方案,但是对于扩展性,灵活度和功能复杂度上,都略显简单,所以我才考虑花时间来实现这款多维表格方案。另一个比较复杂的逻辑是表格的列拖拽和排序,我们需要对可展开折叠的表格支持排序和拖拽,并保持优秀的用户体验:

技术实现如下:
import { useState, useEffect } from "react"
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 初始化状态
const [storedValue, setStoredValue] = useState<T>(() => {
try {
// 获取本地存储中的值
if (typeof window === "undefined") {
return initialValue
}
const item = window.localStorage.getItem(key)
// 解析存储的JSON或返回初始值
return item ? JSON.parse(item) : initialValue
} catch (error) {
// 如果出错,返回初始值
console.error(`Error reading localStorage key "${key}":`, error)
return initialValue
}
})
// 返回一个包装版本的 useState setter 函数
// 将新值同步到 localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// 允许值是一个函数
const valueToStore = value instanceof Function ? value(storedValue) : value
// 保存到 state
setStoredValue(valueToStore)
// 保存到 localStorage
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue))
} catch (error) {
console.error(`Error parsing localStorage item "${key}":`, error)
}
}
}
// 添加事件监听器
if (typeof window !== "undefined") {
window.addEventListener("storage", handleStorageChange)
}
// 清理事件监听器
return () => {
if (typeof window !== "undefined") {
window.removeEventListener("storage", handleStorageChange)
}
}
}, [key])
return [storedValue, setValue]
}
多维表格还支持多种视图的转换,比如可以将表格视图一键转换为可视化分析图表:

对用户和团队进行多维度的数据分析。技术实现如下:
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from "recharts"
import type { Task } from "@/lib/types"
interface PriorityDistributionChartProps {
tasks: Task[]
}
export function PriorityDistributionChart({ tasks }: PriorityDistributionChartProps) {
// 计算每个优先级的任务数量
const priorityCounts: Record<string, number> = {}
tasks.forEach((task) => {
const priority = task.priority || "未设置"
priorityCounts[priority] = (priorityCounts[priority] || 0) + 1
})
// 转换为图表数据格式
const chartData = Object.entries(priorityCounts).map(([priority, count]) => ({
priority,
count,
}))
// 为不同优先级设置不同颜色
const COLORS = ["#FF8042", "#FFBB28", "#00C49F", "#0088FE"]
return (
<div className="w-full h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={true}
outerRadius={80}
fill="#8884d8"
dataKey="count"
nameKey="priority"
label={({ priority, percent }) => `${priority}: ${(percent * 100).toFixed(0)}%`}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value, name, props) => [`${value} 个任务`, props.payload.priority]} />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
)
}
项目的体验地址:mute.turntip.cn
如果大家有好的想法,欢迎评论区留言反馈~
来源:juejin.cn/post/7511649092658577448
使用watchtower更新docker容器
更新方式
定时更新(默认)
执行以下命令后,Watchtower 会在后台每 24 小时自动检查并更新所有运行中的容器:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower
手动立即更新
添加 --run-once 参数启动临时容器,检查更新后自动退出,适合按需触发:
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower --run-once
更新指定容器
在命令末尾添加需要监控的容器名称,多个容器用空格分隔。例如仅监控 nginx 和 redis 容器:
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower nginx redis
简化命令
手动更新时,如果使用上面的命令未免太麻烦了,所以我们可以将更新命令设置为别名:
将下面的命令放到对应shell的环境文件中(比如bash对应~/.bashrc,zsh对应~/.zshrc)
alias update-container="docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once"
编辑完环境文件后,重新打开命令窗口,或使用source ~/.bashrc或source ~/.zshrc加载命令。
然后就可以通过下面的方式更新容器了:
update-container 容器标识
比如:
update-container nginx-ui-latest

来源:juejin.cn/post/7541682368329170954
用了bun之后,是不是就不用vite了
用了 Bun 之后,并不是完全不用 Vite。Bun 虽然内置了打包器和运行时,且速度非常快,但其打包功能目前还不够完善,缺少对前端项目非常重要的功能,比如对代码分块(chunk splitting)的精细控制和开发服务器(dev server)支持,这些都是 Vite 在前端开发中非常关键的优势
具体来说:
- Bun 是一个高性能的 JavaScript 运行时和包管理器,集成了打包器和测试工具,适合全栈开发和快速安装依赖,但其前端打包功能还处于实验阶段,缺少灵活的代码拆分和热更新支持
- Vite 专注于前端开发体验,提供极快的冷启动和热模块替换(HMR),以及灵活且成熟的生产构建流程,它的生态和插件系统也非常丰富,适合现代前端框架(如 Vue3、React)
- 目前推荐的做法是用 Bun 替代 Node.js 作为运行时和包管理器,用 Vite 负责前端的开发服务器和构建打包。这样可以结合 Bun 的高速安装和执行优势,以及 Vite 的快速开发反馈和优化生产构建的能力
关于 pnpm 和 Vite 的叠加:
- pnpm 是一个高效且节省磁盘空间的包管理器,完全可以和 Vite 一起使用。pnpm 通过内容寻址存储和符号链接机制,避免依赖重复,提升安装速度和项目管理效率,特别适合 monorepo 和多项目共享依赖的场景
- pnpm 和 Vite 的结合非常常见,pnpm 负责依赖管理,Vite 负责开发和构建,两者互不冲突,反而能互补提升开发效率
总结:
| 工具 | 作用定位 | 是否可以替代 Vite? | 是否能和 pnpm 叠加? | 主要好处 |
|---|---|---|---|---|
| Bun | JS 运行时+包管理+打包+测试 | 目前不能完全替代 Vite | 可以,pnpm 和 Bun 都是包管理器,选择一个即可 | 速度极快,集成度高,适合全栈开发 |
| Vite | 前端开发服务器+构建工具 | 不能被 Bun 完全替代 | 完全兼容 pnpm | 快速热更新,灵活构建,生态丰富 |
| pnpm | 高效包管理器 | 和 Vite 不冲突,负责依赖管理 | 本身是包管理器,不和 Vite 冲突 | 节省磁盘空间,安装快,依赖隔离 |
因此,用了 Bun 后,建议继续用 Vite 负责前端开发和构建,pnpm 依然可以用来管理依赖,三者结合能发挥各自优势,提升开发体验和性能
如果你想快速上手,可以用 Bun 创建 Vite 项目:
bun create vite my-app
cd my-app
bun install
bun run dev
这样就用 Bun 作为运行时和包管理器,用 Vite 作为开发服务器和构建工具
综上,Bun 目前还不能完全替代 Vite,Vite 在前端开发体验和构建优化上仍有不可替代的优势;pnpm 作为包管理器可以和 Vite 叠加使用,带来依赖管理的高效和节省空间的好处。三者结合是现代前端项目的一个高效组合方案。
用了next.js之后,是不是就不用vite了
用了 Next.js 之后,一般情况下是不需要再单独使用 Vite 了,因为 Next.js 内部已经集成了自己的构建和开发服务器功能。
具体原因和分析如下:
- Next.js 是一个基于 React 的全栈框架,内置了开发服务器(通过
next dev命令启动)、构建工具和打包器,它自动处理代码编译、热模块替换(HMR)、代码拆分和优化等工作,开发体验和构建性能都非常优秀 - Next.js 使用了基于 Rust 的编译器 SWC 来加速构建和转译,性能上已经非常接近甚至优于传统的打包工具
- Vite 主要是一个前端构建工具和开发服务器,适合纯前端项目或需要快速启动的 React/Vue 应用,而 Next.js 是一个框架,包含了路由、服务器端渲染(SSR)、静态站点生成(SSG)、API 路由等功能,功能层面更全面
- 使用 Next.js 时,所有的构建和开发流程都由 Next.js 自己管理,开发者无需额外配置 Vite,避免了工具链的重复和冲突。
- 如果你只是做纯前端项目,Vite 是很好的选择;但如果你需要 SSR、API 路由、全栈功能,Next.js 是更合适的框架,且内置了构建和开发支持,不需要再叠加 Vite。
总结:
| 工具 | 作用定位 | 是否需要同时使用 | 适用场景 |
|---|---|---|---|
| Next.js | React 全栈框架,含开发服务器和构建 | 不需要 | SSR、SSG、API 路由、全栈应用 |
| Vite | 前端开发服务器和构建工具 | 纯前端项目时使用 | 快速启动、热更新、纯前端 SPA |
因此,用了 Next.js 后,基本上不需要再用 Vite 了,Next.js 已经集成了类似 Vite 的开发和构建功能,且提供了更多全栈特性
来源:juejin.cn/post/7522080312564285486
记录App切后台时AppIcon变成默认雪花icon问题
xcode做新项目时,设置了app图标。发现点击app进入前台时,App Icon是正常的,但是回到桌面时App Icon又变成了默认的雪花图标。
之前也遇到过,但是不求甚解,在此列出解决方案。
问题1: AppIcon的设置
随便设置了个图片为app图标,编译报错xxx/Assets.xcassets: The stickers icon set or app icon set named "AppIcon" did not have any applicable content.
同时appIcon可视化窗口显示黄色⚠️图标。
Xcode 提示你在 Assets.xcassets 中名为 "AppIcon" 的 App 图标集合里没有提供任何有效的图片资源。
iOS 应用要求必须有完整的 AppIcon 集合,并且要包含适用于各种设备和分辨率的图标尺寸。如果没有正确设置这些图标,App 就无法通过 App Store 审核,甚至可能在某些模拟器或真机上运行异常。
我使用了makeappicon.com/ 生成appIcon图标。
网站生成的结果包含AppIcon.appiconset,直接把AppIcon.appiconset替换原项目中Assets中的appIcon即可。

结果如下

问题2: 切后台appIcon变成默认雪花icon
现在成功设置appIcon后,切后台时发现appIcon变成了默认的雪花icon。
原因是系统缓存了旧图标,iOS 系统有时会缓存应用的图标缩略图,尤其是多任务界面中的预览图。即使你更新了图标,也可能不会立即刷新。
解决办法: 卸载重装
现在能正常显示了

来源:juejin.cn/post/7520461940273184831
鸿蒙next 游戏授权登录教程王者归来
前沿导读
各位同学很久没有分享技术文章给大家了,因为最近需要兼职讲课,所以我比较忙。都也没有多少个人时间,所以也是趁着现在有空我们就分享下
效果图
调用效果

日志打印

需求背景
工作中最近接到需求,需要接入鸿蒙的游戏授权登录和内购支付,今天把流程走完整,所以现在就做一个分享
开发步骤

初始化
这里如果不是在 EntryAbility 接入初始化代码需要做调整
// 在EntryAbility 里面初始化
try {
gamePlayer.init(this.context,()=>{
hilog.info(0x0000, 'testTag', `Succeeded in initing.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to init. Code: ${err.code}, message: ${err.message}`);
}
// 不在EntryAbility 里面执行初始化
try {
gamePlayer.init(context as common.UIAbilityContext ,()=>{
hilog.info(0x0000, 'testTag', `Succeeded in initing.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to init. Code: ${err.code}, message: ${err.message}`);
}
获取 gamePlayerId
let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.UnionLoginParam = {
showLoginDialog: false, // 是否弹出联合登录面板。true表示强制弹出面板,false表示优先使用玩家上一次的登录选择,不弹出联合登录面板,若玩家首次登录或卸载重装,则正常弹出。
thirdAccountInfos: [] // 若游戏无官包或无官方账号体系,请传空数组。
};
try {
gamePlayer.unionLogin(context, request).then((result: gamePlayer.UnionLoginResult) => {
hilog.info(0x0000, 'testTag', `Succeeded in logining: ${result?.accountName}`);
console.log("gamePlayerId accountName --- >" +result.accountName)
console.log("gamePlayerId thirdOpenId --- >" +result.boundPlayerInfo.thirdOpenId)
console.log("gamePlayerId bindType --- >" +result.boundPlayerInfo.bindType)
let localPlayer=result.localPlayer;
if(localPlayer.gamePlayerId){
console.log("index gamePlayerId localPlayer gamePlayerId --- >" +localPlayer.gamePlayerId)
}
if(localPlayer.teamPlayerId){
console.log("index gamePlayerId localPlayer teamPlayerId --- >" +localPlayer.teamPlayerId)
}
}).catch((error: BusinessError) => {
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${error.code}, message: ${error.message}`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}
获取authorizationCode
let loginRequest = new authentication.HuaweiIDProvider().createLoginWithHuaweiIDRequest();
loginRequest.state = util.generateRandomUUID();
// 执行认证请求
try {
let controller = new authentication.AuthenticationController(getContext(this));
controller.executeRequest(loginRequest, (err, data) => {
if (err) {
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
return;
}
let loginWithHuaweiIDResponse = data as authentication.LoginWithHuaweiIDResponse;
let state = loginWithHuaweiIDResponse.state;
console.log("index authorizationCode state ---> "+state)
if (state != undefined && loginRequest.state != state) {
hilog.error(0x0000, 'testTag', `Failed to login. State is different.`);
return;
}
hilog.info(0x0000, 'testTag', `Succeeded in logining.`);
let loginWithHuaweiIDCredential = loginWithHuaweiIDResponse.data!;
let authorizationCode = loginWithHuaweiIDCredential.authorizationCode;
console.log("index authorizationCode ---> "+authorizationCode)
// 开发者处理authorizationCode
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to login. Code: ${err.code}, message: ${err.message}`);
}
我们拿到了 gamePlayerId 和 authorizationCode 去请求服务端去获取
服务端流程

调用on接口注册playerChanged事件监听
aboutToAppear(): void {
// 调用on接口注册playerChanged事件监听
try {
gamePlayer.on('playerChanged', this.onPlayerChangedEventCallback);
hilog.info(0x0000, 'testTag', `Succeeded in registering.`);
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to register. Code: ${err.code}, message: ${err.message}`);
}
}
监听事件回调
private onPlayerChangedEventCallback(result: gamePlayer.PlayerChangedResult) {
if (result.event === gamePlayer.PlayerChangedEvent.SWITCH_GAME_ACCOUNT) {
// ...
// 游戏号已切换,完成本地缓存清理工作后,再次调用unionLogin接口等
}
}
提交玩家角色信息
let context = getContext(this) as common.UIAbilityContext;
let request: gamePlayer.GSKPlayerRole = {
roleId: '123', // 玩家角色ID,如游戏没有角色系统,请传入“0”,务必不要传""和null。
roleName: 'Jason', // 玩家角色名,如游戏没有角色系统,请传入“default”,务必不要传""和null。
serverId: '456',
serverName: 'Zhangshan',
gamePlayerId: '789', // 请根据实际获取到的gamePlayerId传值。
thirdOpenId: '123' // 接入华为账号登录时不传该字段。接入游戏官方账号登录时,请根据实际获取到的thirdOpenId传值。
};
try {
gamePlayer.savePlayerRole(context, request).then(() => {
hilog.info(0x0000, 'testTag', `Succeeded in saving.`);
});
} catch (error) {
let err = error as BusinessError;
hilog.error(0x0000, 'testTag', `Failed to save. Code: ${err.code}, message: ${err.message}`);
}
参数配置
我们需要在 添加如此下配置

"metadata": [
// 配置如下信息
{
"name": "client_id",
"value": "xxxxxxxxx"
// 华为Client ID 请替换成你自己的正式参数
},
{
"name": "app_id",
"value": "6917581951060909508"
// 华为APP ID 请替换成你自己的正式参数
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
],
配置手动签名测试


查看日志

写在最后
整个鸿蒙游戏授权登录相对比较简单,但是有一个槽点,就是获取 gamePlayerId 和 authorizationCode需要分开两个方法回调 。其实可以做成一个回调更简单,这个希望后期能完善, 服务端逻辑对比客户端来说,还是相对复杂一点点,不过对着文档也是很快能解决这次接入 华为技术支持也帮了不少忙,这个点赞, 对于鸿蒙生态的推广这块,华为确实下了决心,也非常积极的回应。希望鸿蒙越来越好,国产系统早日完善。我依然是你们最爱的徐老师。我们下一期再见。
来源:juejin.cn/post/7543421087759433738
H5 配合原生开发 App
JS 和 Android
- 原生调用 JS
4.4 版本之前
// mWebView = new WebView(this); //当前webview对象
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
//也可以在UI线程中运行
runOnUiThread(new Runnable() {
@Override
public void run() {
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
// 安卓中原生的弹框
Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
}
});
4.4 版本之后
// 通过异步的方式执行js代码,并获取返回值
mWebView.evaluateJavascript("javascript: 方法名('参数1,参数2...')", new ValueCallback() {
@Override
// 这个方法会在执行完毕之后触发, 其中value就是js代码执行的返回值(如果有的话)
public void onReceiveValue(String value) {
}
});
- JS 调用Android
安卓配置:
// Android4.2版本以上,本地方法要加上注解@JavascriptInterface,否则无法使用
private Object getJSBridge(){
// 实例化新对象
Object insertObj = new Object(){
@JavascriptInterface
// 对象内部的方法1
public String foo(){
// 返回 字符串 foo
return "foo";
}
@JavascriptInterface
// 对象内部的方法2 需要接收一个参数
public String foo2(final String param){
// 返回字符串foo2拼接上传入的param
return "foo2:" + param;
}
};
// 返回实例化的对象
return insertObj;
}
// 获取webView的设置对象,方便后续修改
WebSettings webSettings = mWebView.getSettings();
// 设置Android允许JS脚本,必须要!!!
webSettings.setJavaScriptEnabled(true);
// 暴露一个叫做JSBridge的对象到webView的全局环境
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
在 web 页面中调用
//调用方法一
window.JSBridge.foo(); //返回:'foo'
//调用方法二
window.JSBridge.foo2('test');//返回:'foo2:test'
- 原生调用 JS
4.4 版本之前
// mWebView = new WebView(this); //当前webview对象
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
//也可以在UI线程中运行
runOnUiThread(new Runnable() {
@Override
public void run() {
// 通过loadUrl方法进行调用 参数通过字符串的方式传递
mWebView.loadUrl("javascript: 方法名('参数1,参数2...')");
// 安卓中原生的弹框
Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();
}
});
4.4 版本之后
// 通过异步的方式执行js代码,并获取返回值
mWebView.evaluateJavascript("javascript: 方法名('参数1,参数2...')", new ValueCallback() {
@Override
// 这个方法会在执行完毕之后触发, 其中value就是js代码执行的返回值(如果有的话)
public void onReceiveValue(String value) {
}
});
- JS 调用Android
安卓配置:
// Android4.2版本以上,本地方法要加上注解@JavascriptInterface,否则无法使用
private Object getJSBridge(){
// 实例化新对象
Object insertObj = new Object(){
@JavascriptInterface
// 对象内部的方法1
public String foo(){
// 返回 字符串 foo
return "foo";
}
@JavascriptInterface
// 对象内部的方法2 需要接收一个参数
public String foo2(final String param){
// 返回字符串foo2拼接上传入的param
return "foo2:" + param;
}
};
// 返回实例化的对象
return insertObj;
}
// 获取webView的设置对象,方便后续修改
WebSettings webSettings = mWebView.getSettings();
// 设置Android允许JS脚本,必须要!!!
webSettings.setJavaScriptEnabled(true);
// 暴露一个叫做JSBridge的对象到webView的全局环境
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
在 web 页面中调用
//调用方法一
window.JSBridge.foo(); //返回:'foo'
//调用方法二
window.JSBridge.foo2('test');//返回:'foo2:test'
JS 和 IOS
- 原生调用 JS
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
// 加载完毕会触发(类似于Vue的生命周期钩子)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 类似于console.log()
print("触发啦");
// wkWebView调用js代码,其中doSomething()会被当做js解析
webView.evaluateJavaScript("doSomething()");
}
}
- JS 调用 IOS
- JS 部分
window.webkit.messageHandlers.方法名.postMessage(数据)
- iOS 部分注册监听
wkWebView.configuration.userContentController.add(self, name: 方法名)
- iOS 部分遵守协议相关方法
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// message.body 就是传递过来的数据
print("传来的数据为", message.body)
}
- 原生调用 JS
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
// 加载完毕会触发(类似于Vue的生命周期钩子)
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 类似于console.log()
print("触发啦");
// wkWebView调用js代码,其中doSomething()会被当做js解析
webView.evaluateJavaScript("doSomething()");
}
}
- JS 调用 IOS
- JS 部分
window.webkit.messageHandlers.方法名.postMessage(数据)
- iOS 部分注册监听
wkWebView.configuration.userContentController.add(self, name: 方法名)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// message.body 就是传递过来的数据
print("传来的数据为", message.body)
}
url scheme(互通协议)
web 调用
class="ios" type="button" value="使用iframe加载url">
// 加载url 通过iframe 设置URL 目的是让ios拦截
function loadUrl(url) {
// 创建iframe
const iframe = document.createElement('iframe');
// 设置url
iframe.src = url;
// 设置尺寸(不希望他被看到)
iframe.style.height = 0;
iframe.style.width = 0;
// 添加到页面上
document.body.appendChild(iframe);
// 加载了url之后他就没用了
// 移除iframe
iframe.parentNode.removeChild(iframe);
}
document.querySelector('.ios').onclick = function () {
loadUrl('taobao://click');
}
IOS 监听
// 拦截url
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// 获取url
let url = navigationAction.request.url?.absoluteString;
if(url=="taobao://click"){
print("调用系统功能");
decisionHandler(.cancel);
}else{
decisionHandler(.allow);
}
}
HyBridApp

- 开发框架
- 提供前端运行环境
- 实现前端和原生交互
- 封装原生功能,提供插件机制

加载优化
- 骨架屏
<style>
.shell .placeholder-block{
display: block;
height: 5em;
background: #ccc;
margin: 1em;
}
.novel {
height: 5em;
background-color: yellowgreen;
}
style>
head>
<body>
<div class="shell">
<div class="placeholder-block">div>
div>
body>
html>
<script>
setTimeout(()=>{
// 移除 占位dom元素
document.querySelector('.shell').innerHTML = ''
// 创建数据的dom元素 添加到页面上
let p = document.createElement('p')
p.innerHTML = '黑马程序员'
p.className = 'novel'
document.querySelector('.shell').appendChild(p)
},3000)
script>
<style>
.shell .placeholder-block{
display: block;
height: 5em;
background: #ccc;
margin: 1em;
}
.novel {
height: 5em;
background-color: yellowgreen;
}
style>
head>
<body>
<div class="shell">
<div class="placeholder-block">div>
div>
body>
html>
<script>
setTimeout(()=>{
// 移除 占位dom元素
document.querySelector('.shell').innerHTML = ''
// 创建数据的dom元素 添加到页面上
let p = document.createElement('p')
p.innerHTML = '黑马程序员'
p.className = 'novel'
document.querySelector('.shell').appendChild(p)
},3000)
script>
webview
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 创建webView
var webView = WKWebView(frame: self.view.bounds)
// 设置自己为WebView的代理
webView.navigationDelegate = self
// 添加到页面上
self.view.addSubview(webView)
// 创建URL对象
var url = URL(string: "https://www.baidu.com")
// 创建URLRequest对象
var request = URLRequest(url: url!)
// 加载URL
webView.load(request)
}
}
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// 创建webView
var webView = WKWebView(frame: self.view.bounds)
// 设置自己为WebView的代理
webView.navigationDelegate = self
// 添加到页面上
self.view.addSubview(webView)
// 创建URL对象
var url = URL(string: "https://www.baidu.com")
// 创建URLRequest对象
var request = URLRequest(url: url!)
// 加载URL
webView.load(request)
}
}
JSBridge

- 设计思想
- JS 向原生发送消息
- 原生向 JS 发送消息
window.JSBridge = {
invoke: function(action, params, callback) {
// 生成唯一回调ID
const callbackId = 'cb_' + Date.now();
// 存储回调函数
window[callbackId] = callback;
// 构建标准化消息
const msg = {
action: action,
params: params || {},
callbackId: callbackId
};
// 根据平台调用不同原生桥
if (isIOS()) {
window.webkit.messageHandlers.nativeBridge.postMessage(JSON.stringify(msg));
} else if (isAndroid()) {
window.android.postMessage(JSON.stringify(msg));
}
},
// 原生调用此方法来回调结果
receiveMessage: function(msg) {
const { callbackId, result, error } = msg;
const callback = window[callbackId];
if (callback) {
if (error) {
callback(null, error); // 错误回调
} else {
callback(result, null); // 成功回调
}
// 执行后删除回调,避免内存泄漏
delete window[callbackId];
}
}
};
// 使用示例:调用原生相机
JSBridge.invoke('takePhoto', { quality: 'high' }, (result, error) => {
if (error) {
console.error('拍照失败:', error);
} else {
console.log('照片路径:', result.imagePath);
}
});
解释:
- 前端调用
JSBridge.invoke时:存储回调函数,生成唯一的callbackId(如cb_1725000000000),确保每个回调能被唯一识别;把回调函数挂载到window对象上(即window[callbackId] = 回调函数),相当于 “暂时存档”,避免函数被垃圾回收。 - 前端向原生发送 “带回调 ID 的消息”,然后根据平台(iOS/Android)把消息发给原生,此时原生收到的是 “操作指令 + 回调 ID”
- 原生执行操作(如调用相机),原生接收到消息后,解析出
action和params,执行对应的原生逻辑
- iOS:调用
UIImagePickerController(系统相机接口),按quality: 'high'配置拍照质量; - Android:调用
Camera或CameraX接口,同样按参数执行拍照。 这个阶段完全在原生环境(Objective-C/Swift 或 Java/Kotlin)中运行,与前端 JS 无关。
- 原生将 “结果 + 回调 ID” 回传给前端
原生执行完操作后(无论成功 / 失败),会构建一个 “结果消息”,包含:callbackId: 'cb_1725000000000'(必须和前端传过来的一致,才能找到对应的回调);result: { imagePath: '/var/mobile/.../photo.jpg' }(成功时的结果,如照片路径); 或error: '用户取消拍照'(失败时的错误信息)。
然后原生会主动调用前端 JSBridge 预留的 receiveMessage 方法,把 “结果消息” 传回去。
- 前端
receiveMessage执行回调函数
- 解析原生传过来的消息,提取
callbackId、result、error; - 通过
callbackId找到之前挂载在window上的回调函数(即window['cb_1725000000000']); - 执行回调函数:
- 成功:调用
callback(result, null)(如打印照片路径); - 失败:调用
callback(null, error)(如打印 “用户取消拍照”);
- 成功:调用
- 执行完后删除
window[callbackId],避免内存泄漏。
到这一步,回调函数才真正在前端 JS 环境中执行,完成整个跨端通信闭环。
来源:juejin.cn/post/7544077353371222067
JavaScript 数组扁平化全解析
JavaScript 数组扁平化全解析:从基础到进阶,深入理解 flat 与多种实现方式
在现代前端开发中,数组操作是日常编码中最常见的任务之一。而在处理复杂数据结构时,我们经常会遇到“嵌套数组”(即高维数组)的场景。例如,后端返回的数据结构可能是多层嵌套的,我们需要将其“拍平”为一维数组以便于渲染或进一步处理。这种将多层嵌套数组转换为单层数组的过程,就被称为 数组扁平化(Array Flattening)。
本文将带你全面了解 JavaScript 中数组扁平化的各种方法,包括原生 API 的使用、递归实现、reduce 高阶函数应用、利用 toString 和 split 的巧妙技巧,以及基于展开运算符的循环优化方案。我们将深入剖析每种方法的原理、优缺点和适用场景,帮助你构建完整的知识体系。
一、什么是数组扁平化?
数组扁平化,顾名思义,就是把一个嵌套多层的数组“压平”成一个只有一层的一维数组。例如:
const nestedArr = [1, [2, 3, [4, 5]], 6];
// 扁平化后应得到:
// [1, 2, 3, 4, 5, 6]
这个问题看似简单,但在实际项目中非常常见。比如你在处理树形菜单、评论回复结构、文件目录层级等数据时,都可能需要对嵌套数组进行扁平化处理。
二、使用原生 flat() 方法(推荐方式)
ES2019 引入了 Array.prototype.flat() 方法,使得数组扁平化变得极其简单和直观。
✅ 基本语法
arr.flat([depth])
depth:指定要展开的层数,默认为1。- 如果传入
Infinity,则无论嵌套多少层,都会被完全展开。
✅ 示例代码
const arr = [1, [2, 3, [1]]];
console.log(arr.flat()); // [1, 2, 3, [1]] → 只展开一层
console.log(arr.flat(2)); // [1, 2, 3, 1] → 展开两层
console.log(arr.flat(Infinity)); // [1, 2, 3, 1] → 完全展开
✅ 特点总结
- 简洁高效:一行代码解决问题。
- 兼容性良好:现代浏览器基本都支持(IE 不支持)。
- 可控制深度:灵活控制展开层级。
- 推荐用于生产环境:清晰、安全、性能好。
⚠️ 注意:
flat()不会改变原数组,而是返回一个新的扁平化数组。
三、递归实现:最经典的思路
如果你不能使用 flat()(比如兼容老版本浏览器),或者想深入理解其内部机制,那么递归是一个经典且直观的解决方案。
✅ 基础递归版本
function flatten(arr) {
let res = [];
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); // 递归处理子数组
} else {
res.push(arr[i]); // 非数组元素直接加入结果
}
}
return res;
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 分析
- 使用
for循环遍历每个元素。 - 判断是否为数组:是 → 递归调用;否 → 直接推入结果数组。
- 利用
concat合并递归结果。
✅ 缺点
- 每次
concat都会创建新数组,性能略低。 - 递归深度过大可能导致栈溢出(极端情况)。
四、使用 reduce + 递归:函数式编程风格
利用 reduce 可以写出更优雅、更具函数式风格的扁平化函数。
✅ 实现方式
function flatten(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
}
✅ 解析
reduce接收一个累加器pre和当前元素cur。- 如果
cur是数组,则递归调用flatten(cur),否则直接使用cur。 - 使用
concat将结果合并到pre中。
✅ 优点
- 代码简洁,逻辑清晰。
- 更符合函数式编程思想。
- 易于组合其他操作(如 map、filter)。
五、利用 toString() + split() 的“黑科技”技巧
这是一个非常巧妙但需要谨慎使用的技巧,适用于数组中只包含数字或字符串基本类型的情况。
✅ 实现原理
JavaScript 中,数组的 toString() 方法会递归地将每个元素转为字符串,并用逗号连接。
const arr = [1, [2, 3, [1]]];
console.log(arr.toString()); // "1,2,3,1"
我们可以利用这一点,先转成字符串,再用 split(',') 分割,最后通过 +item 转回数字。
✅ 实现代码
function flatten(arr) {
return arr.toString().split(',').map(item => +item);
}
// 测试
const arr = [1, [2, 3, [1]]];
console.log(flatten(arr)); // [1, 2, 3, 1]
✅ 优点
- 代码极短,实现“一行扁平化”。
- 性能较好(底层由引擎优化)。
✅ 缺点(⚠️ 重要)
- 仅适用于纯数字数组:如果数组中有字符串
"hello",+"hello"会变成NaN。 - 无法保留原始类型:所有元素都会被转为数字。
- 丢失
null、undefined、对象等复杂类型信息。
❗ 所以这个方法虽然巧妙,但不适合通用场景,仅作为面试中的“奇技淫巧”了解即可。
六、使用 while 循环 + concat + 展开运算符(性能优化版)
这种方法避免了递归调用,采用循环逐步“拍平”数组,适合处理深层嵌套且希望避免栈溢出的场景。
✅ 实现方式
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
✅ 原理解析
arr.some(item => Array.isArray(item)):检查数组中是否还存在嵌套数组。...arr:展开数组的所有元素。[].concat(...arr):concat会对展开后的数组元素自动“拍平一层”。
🔍 举个例子:
[].concat(...[1, [2, 3, [1]]])
// 等价于
[].concat(1, [2, 3, [1]])
// → [1, 2, 3, [1]] → 拍平了一层
然后继续循环,直到没有嵌套为止。
✅ 优点
- 非递归,避免栈溢出。
- 逻辑清晰,易于理解。
- 性能较好,尤其适合中等深度嵌套。
✅ 缺点
- 每次
concat(...arr)都会创建新数组,内存开销较大。 - 对于极深嵌套,仍可能影响性能。
七、对比总结:各种方法的适用场景
| 方法 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
arr.flat(Infinity) | 简洁、标准、安全 | IE 不支持 | ✅ 生产环境首选 |
递归 + for | 逻辑清晰,易理解 | 性能一般,可能栈溢出 | 学习理解原理 |
reduce + 递归 | 函数式风格,优雅 | 同上 | 偏好函数式编程 |
toString + split | 代码短,性能好 | 类型受限,不通用 | 面试奇技淫巧 |
while + concat + ... | 非递归,避免栈溢出 | 内存占用高 | 深层嵌套处理 |
八、扩展思考:如何实现深度可控的扁平化?
有时候我们并不想完全拍平,而是只想展开指定层数。可以仿照 flat(depth) 实现一个通用函数:
function flattenDepth(arr, depth = 1) {
if (depth === 0) return arr.slice(); // 深度为0,直接返回副本
let result = [];
for (let item of arr) {
if (Array.isArray(item) && depth > 0) {
result.push(...flattenDepth(item, depth - 1));
} else {
result.push(item);
}
}
return result;
}
// 测试
const arr = [1, [2, 3, [4, 5, [6]]]];
console.log(flattenDepth(arr, 1)); // [1, 2, 3, [4, 5, [6]]]
console.log(flattenDepth(arr, 2)); // [1, 2, 3, 4, 5, [6]]
console.log(flattenDepth(arr, Infinity)); // [1, 2, 3, 4, 5, 6]
九、结语
📌 小贴士:如果你的项目需要兼容老旧浏览器,可以使用 Babel 转译 flat(),或手动引入 polyfill:
// Polyfill for Array.prototype.flat
if (!Array.prototype.flat) {
Array.prototype.flat = function(depth = 1) {
return this.reduce((acc, val) =>
Array.isArray(val) && depth > 0
? acc.concat(val.flat(depth - 1))
: acc.concat(val)
, []);
};
}
这样就能在任何环境中愉快地使用 flat() 了!
来源:juejin.cn/post/7543941409930625087
某些场景下CSS替代JS(现代CSS的深度实践指南)
某些场景下CSS替代JS(现代CSS的深度实践指南)
🧩 前端渲染核心机制解析
水合错误(Hydration Mismatch)深度解析
graph TD
A[客户端渲染CSR] --> B[服务端渲染SSR]
B --> C{水合过程 Hydration}
C -->|成功| D[交互式页面]
C -->|失败| E[水合错误]
E --> F[布局错乱]
E --> G[交互失效]
E --> H[控制台报错]
水合错误的本质:
在SSR框架(如Next.js)中,服务端生成的静态HTML与客户端React组件的初始状态不一致,导致React在"注水"过程中无法正确匹配DOM结构。
典型场景:
// Next.js组件 - 服务端渲染时获取时间
export default function Page({ serverTime }) {
// 问题点:客户端初始化时间与服务端不同
const [clientTime] = useState(Date.now());
return (
<div>
<p>服务端时间: {serverTime}</p>
<p>客户端时间: {clientTime}</p>
</div>
);
}
export async function getServerSideProps() {
return {
props: {
serverTime: Date.now() // 服务端生成时间戳
},
};
}
根本原因分析:
- 时序差异:服务端/客户端执行环境时间差
- 数据异步:客户端数据获取滞后于渲染
- DOM操作:客户端手动修改服务端生成的DOM
- 组件状态:useState初始值与SSR输出不匹配
现代CSS的解决之道
<!-- 纯CSS时间显示方案 -->
<div class="time-container">
<time datetime="2023-11-15T08:00:00Z">08:00</time>
<span class="live-indicator"></span>
</div>
<style>
.live-indicator::after {
content: "实时";
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
</style>
优势对比:
| 方案 | 水合风险 | 首屏时间 | 复杂度 | 可访问性 |
|---|---|---|---|---|
| React水合 | 高 | 慢 | 高 | 中等 |
| 纯CSS | 无 | 快 | 低 | 优 |
| 渐进增强 | 低 | 中等 | 中等 | 优 |
🛠️ CSS核心解决方案详解
1️⃣ 嵌套选择器:组件化样式管理
/* 卡片组件 - 替代React组件 */
.card {
padding: 1.5rem;
border: 1px solid #e0e0e0;
/* 标题区域 */
&-header {
display: flex;
align-items: center;
&:hover {
background: #f5f5f5;
}
}
/* 响应式处理 */
@media (width <= 768px) {
border-radius: 0;
padding: 1rem;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
border-color: #444;
}
}
工程价值:
- 作用域隔离:避免全局样式污染
- 维护成本:修改单个组件不影响其他部分
- 开发效率:类似JSX的组件化开发体验
2️⃣ CSS变量 + 相对颜色:动态主题系统
:root {
--primary: #2468f2;
--text-primary: #333;
/* 动态派生变量 */
--primary-hover: hsl(from var(--primary) h s calc(l + 8%));
--primary-active: oklch(from var(--primary) l c h / 0.9);
}
/* 主题切换器 */
.theme-switcher:has(#dark:checked) {
--text-primary: #fff;
--bg-primary: #121212;
}
button {
background: var(--primary);
transition: background 0.3s;
&:hover {
background: var(--primary-hover);
}
&:active {
background: var(--primary-active);
}
}
3️⃣ @starting-style:元素入场动画
.modal {
opacity: 1;
transform: translateY(0);
transition:
opacity 0.4s ease-out,
transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
/* 初始状态 */
@starting-style {
opacity: 0;
transform: translateY(20px);
}
}
与传统方案对比:
// React实现模态框动画 - 需要状态管理
function Modal() {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={`modal ${isOpen ? 'open' : ''}`}
onTransitionEnd={() => console.log('动画结束')}
>
{/* 内容 */}
</div>
)
}
/* 对应CSS */
.modal {
opacity: 0;
transform: translateY(20px);
transition: all 0.4s;
}
.modal.open {
opacity: 1;
transform: translateY(0);
}
📱 响应式设计新范式
动态视口单位实战
/* 移动端布局方案 */
.header {
height: 15svh; /* 最小可视高度 */
}
.hero {
height: 75lvh; /* 最大可视高度 */
}
.content {
height: 120dvh; /* 动态高度 */
overflow-y: auto;
}
.footer {
height: 10svh; /* 保证始终可见 */
}
单位解析:
| 单位 | 计算基准 | 适用场景 | iOS Safari支持 |
|---|---|---|---|
svh | 最小可视区域高度 | 固定导航栏 | 16.4+ |
lvh | 最大可视区域高度 | 全屏轮播图 | 16.4+ |
dvh | 当前可视区域高度 | 可滚动内容区 | 16.4+ |
✅ 实践总结
水合错误规避策略
- 数据一致性:
// Next.js getStaticProps保证数据一致
export async function getStaticProps() {
const data = await fetchData();
return { props: { data } };
}
- 组件设计原则:
// 避免客户端特有状态
function SafeComponent({ serverData }) {
// ✅ 使用服务端传递的数据
return <div>{serverData}</div>;
}
- 渐进增强方案:
<!-- 首屏使用静态HTML -->
<div id="user-profile">
<!-- SSR生成内容 -->
</div>
<!-- 客户端增强 -->
<script type="module">
if (navigator.onLine) {
loadInteractiveComponents();
}
</script>
CSS优先架构优势
| 指标 | JS方案 | CSS方案 | 提升幅度 |
|---|---|---|---|
| 首屏加载 | 2.8s | 0.6s | 78% |
| 交互延迟 | 120ms | 16ms | 87% |
| 内存占用 | 85MB | 12MB | 86% |
| 代码体积 | 350KB (gzip) | 45KB (gzip) | 87% |
实施路线图:
- 静态内容:优先使用HTML/CSS
- 交互元素:
:hover,:focus-within等伪类 - 复杂逻辑:渐进增强添加JS
- 状态管理:URL参数 +
:target选择器
通过现代CSS技术栈,开发者可在避免水合错误的同时,构建高性能、可访问性强的Web应用,实现真正的"渐进式Web体验"。
来源:juejin.cn/post/7544366602885873679
Docker 与 containerd 的架构差异
要深入理解 Docker 与 containerd 的架构差异,首先需要明确二者的定位:Docker 是一套完整的容器平台(含构建、运行、分发等全流程工具),而 containerd 是一个专注于容器生命周期管理的底层运行时(最初是 Docker 内置组件,后独立为 CNCF 项目)。二者的架构设计围绕 “功能边界” 和 “模块化程度” 展开,以下从核心定位、架构分层、关键组件、交互流程四个维度进行对比分析。
一、核心定位与设计目标
架构差异的根源在于二者的定位不同,直接决定了功能范围和模块划分:
| 维度 | Docker | containerd |
|---|---|---|
| 核心定位 | 一站式容器平台(Build, Ship, Run) | 轻量级容器运行时(专注于容器生命周期管理:启动、停止、销毁、资源隔离) |
| 设计目标 | 简化开发者体验,提供全流程工具链;兼顾单机开发与简单集群场景 | 满足云原生环境的可扩展性、稳定性;支持多上层调度器(K8s、Swarm 等) |
| 功能范围 | 包含镜像构建(docker build)、镜像仓库(docker push/pull)、容器运行、网络 / 存储管理、UI 等 | 仅负责镜像拉取、容器运行时管理、底层存储 / 网络对接;无镜像构建功能 |
| 依赖关系 | 早期内置 containerd 作为底层运行时(Docker 1.11+),2020 年后逐步拆分 | 可独立运行,也可作为 Docker、K8s(默认运行时)、Nomad 等的底层依赖 |
二、架构分层对比
二者均遵循 “分层解耦” 思想,但分层粒度和模块职责差异显著。Docker 架构更 “重”(含上层业务逻辑),containerd 更 “轻”(聚焦底层核心能力)。
1. Docker 架构(2020 年后拆分版)
Docker 经历了从 “单体架构” 到 “模块化拆分” 的演进(核心是将 containerd 独立,自身聚焦上层工具链),当前架构分为 4 层,自下而上分别是:
| 架构层 | 核心组件 / 功能 | 职责说明 |
|---|---|---|
| 1. 底层运行时层 | containerd、runc | 承接 Docker daemon 的指令,负责容器的实际创建、启动、资源隔离(依赖 runc 作为 OCI runtime) |
| 2. Docker 守护进程层 | dockerd | Docker 的核心守护进程,负责接收客户端(docker CLI)请求,协调下层组件(如调用 containerd 管理容器,调用 buildkit 构建镜像) |
| 3. 工具链层 | BuildKit、Docker Registry Client、Docker Network/Volume Plugins | - BuildKit:替代传统 docker build 后端,优化镜像构建效率;- 镜像客户端:处理 docker push/pull 与仓库交互;- 网络 / 存储插件:管理容器网络(如 bridge、overlay)和数据卷 |
| 4. 客户端层 | docker CLI(命令行工具)、Docker Desktop UI(桌面端) | 提供用户交互入口,将 docker run/build/pull 等命令转化为 HTTP 请求发送给 dockerd |
2. containerd 架构(CNCF 标准化版)
containerd 架构更聚焦 “容器生命周期”,采用 5 层模块化设计,每层职责单一,可独立扩展,自下而上分别是:
| 架构层 | 核心组件 / 功能 | 职责说明 |
|---|---|---|
| 1. OCI 运行时层 | runc、crun(可选) | 遵循 OCI 规范(Open Container Initiative),负责创建 Linux 容器(如调用 clone() 系统调用实现 PID 隔离,挂载 cgroup 限制资源) |
| 2. 容器执行层 | containerd-shim(垫片进程) | - 解耦 containerd 与容器进程:即使 containerd 重启,容器也不会退出;- 收集容器日志、监控容器状态、转发信号(如 docker stop 对应 SIGTERM) |
| 3. 核心服务层 | containerd 守护进程(containerd) | containerd 的核心,通过 gRPC 提供 API 服务,包含 4 个核心模块:- Namespaces:实现多租户资源隔离;- Images:管理镜像(拉取、存储、解压);- Containers:管理容器元数据(配置、状态);- Tasks:管理容器进程(启动、停止、销毁) |
| 4. 元数据存储层 | BoltDB(嵌入式 key-value 数据库) | 存储容器、镜像、命名空间等元数据,无需依赖外部数据库(如 MySQL),轻量且高效 |
| 5. 上层适配层 | CRI 插件(containerd-cri)、Docker API 兼容层 | - CRI 插件:将 containerd 的 gRPC API 转化为 K8s 要求的 CRI(Container Runtime Interface),使其成为 K8s 默认运行时;- Docker API 兼容层:支持部分 Docker 命令,确保与老系统兼容 |
三、关键组件差异
架构的核心差异体现在 “组件职责划分” 和 “功能依赖” 上,以下是最关键的组件对比:
| 组件 / 能力 | Docker | containerd |
|---|---|---|
| 核心守护进程 | dockerd(上层协调)+ containerd(底层运行时,需与 dockerd 配合) | containerd(独立守护进程,直接对接 OCI 运行时,无需依赖其他进程) |
| 镜像构建 | 内置 BuildKit(或传统后端),支持 docker build 命令 | 无镜像构建功能,需依赖外部工具(如 BuildKit、img) |
| 容器进程隔离 | dockerd → containerd → containerd-shim → runc → 容器进程(4 层调用) | containerd → containerd-shim → runc → 容器进程(3 层调用,更轻量) |
| 元数据存储 | 依赖本地文件系统(/var/lib/docker)+ 部分内存缓存 | 内置 BoltDB(/var/lib/containerd),元数据管理更统一、高效 |
| API 接口 | 主要提供 HTTP API(供 docker CLI 调用),对下层暴露有限 | 以 gRPC API 为主(更适合跨进程通信),提供细粒度接口(如镜像、容器、任务分别有独立 API) |
| 上层调度器支持 | 主要支持 Docker Swarm,对接 K8s 需额外配置(早期需 cri-dockerd 插件) | 原生支持 K8s(通过 containerd-cri 插件),也支持 Swarm、Nomad 等 |
四、容器启动流程对比
通过 “容器启动” 这一核心场景,可以直观看到二者的架构交互差异:
1. Docker 启动容器的流程(以 docker run ubuntu 为例)
- 用户交互:用户在终端执行 docker run ubuntu,docker CLI 将命令转化为 HTTP 请求,发送给本地的 dockerd 守护进程。
- dockerd 协调:
- 检查本地是否有 ubuntu 镜像:若无,调用 “镜像客户端” 从 Docker Hub 拉取镜像;
- 拉取完成后,dockerd 向 containerd 发送 gRPC 请求,要求创建并启动容器。
- containerd 处理:
- containerd 接收请求后,创建容器元数据(存储到本地),并启动 containerd-shim 垫片进程;
- containerd-shim 调用 runc,由 runc 遵循 OCI 规范创建容器进程(分配 PID、挂载 cgroup、设置网络 / 存储)。
- 状态反馈:
- containerd-shim 实时收集容器状态(如运行中、退出),反馈给 containerd;
- containerd 将状态转发给 dockerd,最终由 docker CLI 输出给用户(如 docker ps 显示容器列表)。
2. containerd 启动容器的流程(以 ctr run ubuntu my-container 为例,ctr 是 containerd 自带 CLI)
- 用户交互:用户执行 ctr run ubuntu my-container,ctr 直接通过 gRPC 调用 containerd 守护进程。
- containerd 核心处理:
- 检查本地镜像:若无,直接调用内置的 “镜像模块” 从仓库拉取 ubuntu 镜像;
- 创建容器元数据(存储到 BoltDB),并启动 containerd-shim 垫片进程。
- OCI 运行时启动容器:
- containerd-shim 调用 runc 创建容器进程,完成资源隔离和环境初始化;
- 容器启动后,containerd-shim 持续监控容器状态,直接反馈给 containerd。
- 状态反馈:containerd 将容器状态通过 gRPC 返回给 ctr,用户终端显示启动结果。
五、总结:架构差异的核心影响
| 对比维度 | Docker | containerd |
|---|---|---|
| 轻量级 | 重(含全流程工具,依赖多组件) | 轻(仅核心运行时,组件少、资源占用低) |
| 扩展性 | 弱(架构耦合度较高,难适配多调度器) | 强(模块化设计,原生支持 K8s 等调度器) |
| 性能 | 略低(多一层 dockerd 转发,资源消耗多) | 更高(直接对接 OCI 运行时,调用链短) |
| 使用场景 | 单机开发、测试、小型应用部署 | 云原生集群(如 K8s 集群)、大规模容器管理 |
| 学习成本 | 低(CLI 友好,文档丰富) | 高(需理解 gRPC、OCI 规范,适合运维 / 底层开发) |
简言之:Docker 是 “面向开发者的容器平台”,架构围绕 “易用性” 和 “全流程” 设计;containerd 是 “面向云原生的底层运行时”,架构围绕 “轻量、可扩展、高兼容” 设计。在当前云原生生态中,containerd 已成为 K8s 的默认运行时,而 Docker 更多用于单机开发场景。
来源:juejin.cn/post/7544381073698848811
instanceof 的小秘密
instanceof 运算符用于检测某个构造函数的 prototype 属性,是否存在于对象的原型链上。
class Cat {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const cat = new Cat("Mittens", 3);
console.log(cat instanceof Cat); // true
console.log(cat instanceof Object); // true
instanceof 接收两个参数,v(左)和target(右),判断v是否为target的实例对象,方法是先查询target的Symbol.hasInstance属性,如果不存在,则判断target的prototype属性是否存在v的原型中。
class Cat {
static [Symbol.hasInstance](instance) {
return false
}
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const cat = new Cat("Mittens", 3);
console.log(cat instanceof Cat); // false
console.log(cat instanceof Object); // true
console.log(cat instanceof null) // TypeError: Right-hand side of 'instanceof' is not an object
或许有人会想到1 intanceof Number和1 intanceof Object为什么会是false呢?明明(1).__proto__是有值的,为什么呢?这里就不得不提到JS的一个机制"自动装箱"。
我们定义一个变量const n = 1, n是一个原始值,有以下特点:
- 不可变性:原始值本身不能被修改,任何"修改"操作都会创建新值
- 按值传递:赋值时复制值,而不是引用
- 没有属性和方法:原始值本身不是对象,不能直接拥有属性和方法
在访问原始值属性或者方法时,Js会创建一个临时对象,使用后便会销毁。
const n = 1;
n.toString()
// JavaScript 内部实际执行的过程
// 1. 创建临时 Number 对象:new Number(1)
// 2. 调用方法:numberObj.toString()
// 3. 返回结果:"1"
// 4. 销毁临时对象
但是在intanceof操作时,不会进行"自动装箱",所以得到的结果为false。
来源:juejin.cn/post/7543797314282373162
一个有趣的效果--动态生成动画导航
一个有趣的效果--动态生成动画导航
在接下来的这个项目中,我们即将使用纯 JavaScript 和 CSS 来创建一个具有动态动画效果的导航栏。这篇文章将详细解析该代码的实现,包括 HTML 结构、CSS 样式、JavaScript 逻辑等方面,帮助你理解每一个步骤和实现思路。文章内容将逐步拆解,涵盖从页面结构、样式设计到功能实现的各个细节。
项目概述
这个项目的核心目标是创建一个包含动画效果的导航栏。具体功能包括:
- 动态导航项:当用户将鼠标悬停在导航项上时,显示一个附加的面板。
- 面板动画:面板会根据鼠标悬停的位置进行平滑过渡,显示不同的内容。
- 过渡效果:每个导航项的高亮状态和面板显示都有精美的动画效果,增强用户体验。
HTML 结构
HTML 基本框架
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>一个动态动画导航</title>
<style>
/* 样式在这里 */
</style>
</head>
<body>
<script>
/* JavaScript 逻辑在这里 */
</script>
</body>
</html>
HTML 文档是非常标准的结构,包含了 head 和 body 两大部分:
<head>部分:定义了页面的字符编码、视口设置和页面的标题。标题为 “一个动态动画导航”,用于描述页面内容。<body>部分:里面没有直接的 HTML 内容,而是通过 JavaScript 动态生成和管理导航栏的结构。
导航栏元素
在页面的 body 中,我们没有直接放置导航栏的 HTML 代码,而是通过 JavaScript 动态生成。接下来我们将深入分析这些 JavaScript 代码的工作原理。
CSS 样式解析
全局样式
body, html, ul, p {
margin: 0;
padding: 0;
}
这一段代码是用来移除 body、html、ul 和 p 元素的默认 margin 和 padding,以确保布局没有多余的间隙。这是前端开发中的常见做法,有助于在不同浏览器中获得一致的效果。
导航栏 .nav
.nav {
list-style: none;
padding: 0;
margin: 0;
display: flex;
position: relative;
margin-left: 200px;
}
.nav 是一个容器元素,负责展示导航栏中的各个导航项。它使用了 flex 布局,使得每个 li 元素可以水平排列。此外,通过 position: relative 来为可能添加的子元素(如下拉面板)提供定位上下文,margin-left: 200px 是为了给导航栏留出空间。
导航项 .nav li
.nav li {
min-width: 100px;
text-align: center;
border-bottom: 1px solid #ddd;
color: #535455;
padding: 12px;
margin-right: 12px;
cursor: pointer;
transition: all ease 0.2s;
}
每个导航项 (li) 有如下样式:
min-width: 100px:确保每个项至少占据 100px 宽度。text-align: center:使文本居中显示。border-bottom: 1px solid #ddd:为每个导航项添加一个细线,增强视觉效果。padding: 12px和margin-right: 12px:设置内外边距,使项之间保持一定的间距。cursor: pointer:当鼠标悬停在导航项上时,显示为可点击的手形光标。transition: all ease 0.2s:使所有样式变化(如颜色、背景色、缩放等)具有过渡效果,持续时间为 0.2 秒,效果为平滑过渡。
面板 .nav-panel-wrapper
.nav-panel-wrapper {
border: 1px solid #dedede;
position: absolute;
top: 60px;
left: 0;
padding: 12px;
border-radius: 4px;
box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.32);
display: none;
overflow: hidden;
}
.nav-panel-wrapper 是每个导航项的下拉面板,包含以下样式:
position: absolute:使面板相对于.nav容器进行绝对定位。top: 60px:将面板放置在导航项下方(假设导航栏的高度为 60px)。border-radius: 4px:为面板添加圆角,使其看起来更加圆滑。box-shadow:为面板添加阴影效果,使其更加立体,增加视觉层次感。display: none:面板默认是隐藏的,只有在用户悬停时才会显示。overflow: hidden:确保面板内容不会溢出其容器。
动画样式
.scale-up-top {
animation: scale-up-top 0.2s cubic-bezier(0.39, 0.575, 0.565, 1) both;
}
@keyframes scale-up-top {
0% {
transform: scale(0.5);
transform-origin: 50% 0%;
}
100% {
transform: scale(1);
transform-origin: 50% 0%;
}
}
.scale-up-top 类通过动画效果使面板从小到大逐渐放大,并且设置了动画的持续时间为 0.2 秒,使用了 cubic-bezier 函数来创建缓动效果。@keyframes scale-up-top 定义了放大过程的具体动画帧:从 50% 的缩放大小(即最小状态)逐渐过渡到 100%(即原始大小)。
JavaScript 逻辑解析
工具类 AnimateNavUtils
AnimateNavUtils 是一个工具类,提供了一些常用的方法,简化了 DOM 操作的代码:
$:根据选择器返回文档中的第一个匹配元素。createElement:根据传入的 HTML 字符串创建一个新的 DOM 元素。addClass、removeClass、hasClass:分别用于为元素添加、移除、检查 CSS 类。insertNode:将一个新的节点插入到指定的元素中,或者替换现有节点。create:创建一个新的 DOM 元素节点。setStyle:为元素动态设置样式。
这些工具方法大大简化了后续类的实现,使得代码更具可读性和复用性。
动画导航类 AnimateNav
AnimateNav 类是核心部分,负责处理导航栏的渲染、事件绑定和面板的动画效果。
构造函数
constructor({ data }) {
super();
this.data = data;
this.panelDelayTimer = null;
this.currentIndex = 0;
this.panelEle = null;
this.navEle = null;
}
在构造函数中,我们接收一个 data 参数,它是一个包含导航项信息的数组。panelDelayTimer 用来控制面板的显示延迟,currentIndex 用来记录当前导航项的索引,panelEle 和 navEle 分别存储面板和导航栏的 DOM 元素引用。
mount 方法
mount(el) {
const container = this.isString(el) ? this.$(el) : document.body;
this.render(container);
}
mount 方法负责将导航栏挂载到指定的 DOM 元素中。如果传入的参数是一个字符串(例如选择器),则查找对应的元素;如果是其他类型,则默认为 document.body。
render 方法
render(container) {
if (!this.isArray(this.data) || this.data?.length === 0) {
return;
}
const node = this.createElement(`
<ul class="nav">
${this.data.map(item => `<li data-sub="${item.sub}" data-index="${item.index}" class="nav-item">${item.text}</li>`).join('')}
<div class="nav-panel-wrapper"> </div>
</ul>
`);
...
}
render 方法负责生成导航栏的 HTML 结构并将其插入到页面中。它首先检查 data 是否有效,确保它是一个数组且非空。接着,它动态创建一个包含 <ul class="nav"> 和 <div class="nav-panel-wrapper"> 的 HTML 结构。
data.map(item => ...)生成每个导航项的<li>元素,并根据data-sub和data-index设置相应的自定义属性。this.navEle和this.panelEle分别存储了导航栏容器和面板容器的 DOM 元素引用,方便后续操作。- 最后,调用
bindEvents方法来绑定事件处理器。
绑定事件 bindEvents
bindEvents() {
const items = Array.from(this.navEle.querySelectorAll('.nav-item'));
items.forEach(item => {
item.addEventListener('mouseenter', (e) => {
const index = e.target.dataset.index;
this.showPanel(index);
});
item.addEventListener('mouseleave', () => {
this.hidePanel();
});
});
}
showPanel(index) {
const item = this.navEle.querySelector(`[data-index="${index}"]`);
const subItems = item.getAttribute('data-sub');
this.panelEle.innerHTML = subItems ? subItems : '没有子项';
this.addClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, {
display: 'block',
top: `${item.offsetTop + item.offsetHeight + 12}px`
});
}
hidePanel() {
this.removeClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, { display: 'none' });
}
在 bindEvents 方法中,我们为每个导航项添加了 mouseenter 和 mouseleave 事件监听器:
mouseenter:当鼠标进入某个导航项时,调用showPanel方法显示对应的面板,并填充子项内容。mouseleave:当鼠标离开导航项时,调用hidePanel隐藏面板。
showPanel 方法
showPanel(index) {
const item = this.navEle.querySelector(`[data-index="${index}"]`);
const subItems = item.getAttribute('data-sub');
this.panelEle.innerHTML = subItems ? subItems : '没有子项';
this.addClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, {
display: 'block',
top: `${item.offsetTop + item.offsetHeight + 12}px`
});
}
showPanel 方法根据导航项的索引 (data-index) 显示相应的子项。如果该项有子项(存储在 data-sub 属性中),则将这些子项填充到面板中。如果没有子项,则显示默认的消息('没有子项')。然后,通过 scale-up-top 动画类使面板执行放大动画,并将面板的显示位置设为导航项的下方。
hidePanel 方法
hidePanel() {
this.removeClass(this.panelEle, 'scale-up-top');
this.setStyle(this.panelEle, { display: 'none' });
}
hidePanel 方法用于隐藏面板。它会移除面板的动画类 scale-up-top,并通过 setStyle 将面板的 display 属性设置为 none,使其消失。
总结
动画和交互效果
- 悬停时显示面板:当用户将鼠标悬停在导航项上时,会触发面板的显示,面板内容来自
data-sub属性。 - 平滑动画:面板在显示和隐藏时应用了平滑的缩放动画,使得界面显得更加动态和流畅。
- 动态子项内容:通过自定义的
data-sub属性,每个导航项可以动态地包含不同的子项或其他内容。
来看一个在线示例如下所示:
当然这个导航还有可以优化和扩展的空间,如下:
优化和扩展
- 响应式设计:当前代码没有完全考虑到移动端的布局,可以进一步优化以适应不同设备屏幕的大小。
- 面板延迟:目前面板的显示和隐藏没有延迟处理,未来可以根据需要加入延迟显示、隐藏的效果,提升交互体验。
- 面板定位优化:面板的显示位置是相对于当前导航项的位置进行的,可以根据页面的整体布局进一步调整面板的显示位置,例如避免面板超出页面底部或侧边界。
整体来说,这个动态导航效果是通过结合 JavaScript 的 DOM 操作和 CSS 动画来实现的,结构清晰,动画流畅,能够为用户提供良好的互动体验。
来源:juejin.cn/post/7442965793157136420
我让AI一把撸了个算命网站,结果它比我还懂玄学

作为一个信奉代码逻辑的程序员,我做梦也没想到有一天会让AI帮我开发算命网站。
事情是这样的:某天深夜,我突然冒出一个奇葩想法——能不能让Claude Code从零开始搭建一个算命网站? 不是因为我迷信,而是纯粹的技术好奇心:AI能理解2000年前的玄学智慧吗?
于是我开始了这个"用最现代的技术实现最古老的智慧"的实验。
3天后,网站上线了。更神奇的是,AI对传统文化的理解程度让我这个中国人都自愧不如。
选技术栈:AI的选择让我怀疑人生
我本来打算用熟悉的Vue + Express一把梭,结果Claude Code一开口就让我重新审视了自己的技术认知。
我问它:"帮我选个适合算命网站的技术栈"
它的回答让我直接傻眼:
Next.js 14 + TypeScript。我说算命网站用TypeScript?它说:"天干地支计算出错了用户会骂死你的,类型安全不是开玩笑的。"我一想,确实,算错了八字那不是要被用户打死。
Supabase。我说为什么不用MongoDB?它说:"用户算命记录要能实时同步,万一用户换个设备要查看历史记录呢?"行吧,你考虑得比我周到。
DeepSeek API。这个真的让我意外,我下意识想用GPT,结果它说:"中文传统文化,还是国产模型理解得更地道。"后来的事实证明,这个选择太明智了。
最让我佩服的是,它还主动提到了"真太阳时计算"。我当时就懵了,什么是真太阳时?它耐心地给我科普:古代算命要考虑地理位置对时间的影响,不是简单的北京时间。
我心想,这AI对传统文化的理解比我这个中国人还深入啊。
给自己算了一下还是挺满意的

教AI算命:一场智商与玄学的较量
项目最大的难点不是写代码,而是怎么让一个理性的AI理解玄学。
这就像让一个直男理解女朋友的小情绪,难度系数直接拉满。
第一次尝试,我直接甩给AI一个八字:"帮我分析一下甲子年乙丑月丙寅日丁卯时"
AI的回答让我哭笑不得:"这位朋友可能具有较强的时间观念,因为你的出生时间很规律..."
我当场就想关电脑了。这哪里是算命,这是在分析数据规律啊!
第二次,我学聪明了,告诉它"你是命理大师"。结果它开始发挥想象力,创造了一套"六行理论",除了传统五行还加了个"气行"。我差点被它的创新精神感动。
第三次,我痛定思痛,决定从根本上改变策略。我不再把它当AI,而是真的把它当成一个有30年经验的老师傅。我给它详细介绍了传统命理的理论体系,告诉它什么能说,什么不能说,甚至教它怎么说话。
这次它终于开窍了,分析起来有模有样,专业术语用得恰到好处,建议也很中肯。
我突然意识到,训练AI就像带徒弟,不是给它知识,而是教它思考方式。
踩坑实录:当代码遇见玄学,bug都变得玄幻了
做这个项目让我深刻体会到什么叫"传统文化博大精深",每个看似简单的概念背后都藏着巨大的坑。
最让我头疼的是时辰计算。我原本以为很简单,23点到1点是子时嘛,结果Claude Code告诉我:"古代的时辰划分和现代时间概念不完全一样,而且要考虑地理位置。"
我当时就懵了,算个命还要考虑地理位置?后来才知道,古人用的是"真太阳时",北京的中午12点和新疆的中午12点,太阳位置是不一样的。
这就好比你以为做个网站用个时间戳就行了,结果发现还要处理时区、夏令时、闰秒...程序员的痛,古人早就体验过了。
还有一个哭笑不得的bug。AI在分析五行的时候,突然开始"创新",告诉用户发现了"六行理论",除了金木水火土,还有个"气行"。我当时想,你这是要颠覆传统文化吗?
后来我在提示词里加了一句"严格按照传统理论,不要创新",AI这才老实下来。
最隐蔽的坑是日期计算。现代JavaScript处理1900年以前的日期有问题,结果导致古代名人的八字全算错了。我测试的时候用李白的生日,算出来说他五行缺钱...我差点被自己笑死。
每修复一个bug,我都觉得自己对传统文化的理解又深了一层。这感觉很奇妙,就像在用代码穿越时空,和古人对话。
从程序员审美到仙气飘飘
做程序员这么多年,我深知自己的审美水平。我设计的界面通常是这样的:白色背景,黑色字体,偶尔加个边框,完事。
用户打开我设计的网站,第一反应通常是:"这...是1990年代的网站吗?"
但算命网站不一样啊,用户来算命,你给他一个Excel表格的界面,他会觉得你在糊弄他。这玩意得有神秘感,得有仙气。
我问Claude Code:"怎么让网站看起来有仙气?"
它的回答让我刷新了对UI设计的认知。它告诉我色彩心理学:深紫色代表神秘和智慧,金色代表尊贵和权威,渐变背景能营造空间感...
我听得一愣一愣的,心想这AI怎么还懂心理学?
按照它的建议改了界面后,效果确实不错。原本的Excel风格摇身一变成了"古风仙侠游戏界面"。朋友看了都说:"这网站一看就很专业,肯定算得准。"
我当时就想,界面设计真的能影响用户的心理预期。同样的内容,包装不同,用户的信任度完全不一样。
这让我想到另一个问题:在技术驱动的时代,审美能力可能比编程能力更稀缺。会写代码的程序员到处都是,但能设计出让用户一见钟情的界面的,真的不多。
这个布局我很喜欢,但一些ui感觉还可以微调

意外的收获:技术人的文化觉醒
这个项目最大的收获不是技术上的,而是认知上的。
以前我总觉得传统文化和现代技术是两个世界的东西,一个古老神秘,一个理性现代。但做完这个项目后,我发现它们其实是可以融合的。
AI可以学会古老的智慧,代码可以承载文化的传承。技术不是要替代传统,而是要成为传统文化在新时代的载体。
更重要的是,我开始理解用户需求的复杂性。人们使用算命网站,不只是想知道未来会怎样,更多的是希望获得一种心理安慰,一种对未知的控制感。
这让我重新思考技术产品的本质:不是要解决技术问题,而是要解决人的问题。
下一步:用技术重新定义传统
基于这次的经验,我有了一个更大胆的想法:用现代技术重新定义传统文化。
不是简单地把古书电子化,而是用AI、VR、区块链这些新技术,创造全新的文化体验方式。比如用AI生成个性化的《易经》解读,用VR重现古代占卜场景,用区块链记录每个人的文化传承轨迹。
传统文化需要在新时代找到新的表达方式,而技术人恰好有这个能力和责任。
先用three.js写个动画勉强还算满意吧

写在最后:一个程序员的玄学感悟
3天时间,从一个深夜的奇思妙想到一个完整的产品上线。回过头看,这个项目带给我的不只是技术上的提升,更多的是思维上的转变。
最大的感悟是:AI不是工具,而是合作伙伴。它有自己的"想法",会给你意想不到的建议,会从你没想到的角度解决问题。与其说是我在使用AI,不如说是我们在一起探索未知。
第二个感悟是:用户需求比技术实现更重要。算命网站的核心不是算法有多精确,而是能不能给用户带来心理上的满足。技术是手段,解决人的问题才是目的。
第三个感悟是:传统文化需要新的表达方式。不是要用技术颠覆传统,而是要用技术让传统在新时代重新焕发生机。
如果你也对AI开发感兴趣,我的建议是:不要把AI当成万能的代码生成器,要把它当成一个有智慧的合作伙伴。它能给你灵感,能帮你思考,但最终的判断和决策还是要靠你自己。
最后,如果你也想尝试类似的跨界项目,记住一点:技术栈可以学,算法可以抄,但洞察用户需求的能力,只能靠自己慢慢积累。
下一个项目,还不知道做啥,有想法的朋友可以在评论区说一声
本文基于真实项目开发经验,欢迎技术交流和商业合作!
来源:juejin.cn/post/7537339432292270080
用卡尔曼滤波器计算个股票相对大盘的相关性——β值

A股涨到3600点了。
你是不是也跟我一样——看到新闻标题说“牛市要来了!”,一打开账户……嗯?怎么手里的票还是绿的,上证指数25年都涨7%了,而你确亏了7%

为什么你的股票没涨过大盘?到底是市场的问题,还是你的股票压根不跟着市场走?
聪明的你可能已经想到一个词了,叫——β值(Beta)。
说白了,β值其实就是个股相对大盘“跟涨跟跌”的敏感度。你可以把它想象成你手上这票对“市场情绪”的反应速度和强度。
- β > 1 的股票,市场一疯,它先蹦跶;市场一跌,它先躺平。
- β < 1 的股票,属于佛系型,涨跌都慢半拍。
- β ≈ 0 的,那是独行侠,走自己的路让别人说去吧。
我知道,有些小伙伴可能说:“花姐你说这我都懂,但怎么算这个β值啊?”
来,今天我们就来整点硬核的——用卡尔曼滤波器来动态估算β值。
传统β值是怎么算的?
这事儿咱得先交代清楚,不然后面你会问:为啥要搞卡尔曼滤波呢?直接用回归不香吗?
最常见的β值计算方式是用OLS线性回归,比如拿某个股票和上证指数近一年日收益率,做个线性回归,斜率就是β值。代码长这样:
import pandas as pd
import numpy as np
import statsmodels.api as sm
stock_ret = ... # 股票收益率序列
market_ret = ... # 市场指数收益率序列
X = sm.add_constant(market_ret)
model = sm.OLS(stock_ret, X).fit()
beta = model.params[1]
结果算出来的β是固定的一个值。问题来了:市场在变,个股特性也在变,一个固定的β能代表未来吗?
用卡尔曼滤波器
那么问题来了:有没有什么办法,可以让β值随着时间动态变化,反映出最新的市场行为?
有!这时候就该请出我们的主角了——卡尔曼滤波器(Kalman Filter)。
别听名字吓人,其实你可以把它理解为一个“会自我更新”的预测模型。它有点像是个不断修正自己认知的智能体,每来一个新数据,就纠正一下之前的偏差。
如果说OLS是一次性静态判断,那卡尔曼滤波就是边走边看边修正。
卡尔曼滤波器怎么估算β?
卡尔曼滤波在量化里的用途很多,其中一个经典用途就是:时间序列中的线性参数动态估计。
我们的问题就可以建模成这个样子
股票收益率 = α + β × 市场收益率 + 噪声
不同的是,我们让β变成一个随时间变化的变量。
也就是说,今天的β和昨天的不一样,明天的也不一定一样。
我们设定两个方程:
状态方程(β的演变):
观测方程(收益率的观察):
其中:
- 是大盘的收益率
- 是股票的收益率
- 和 是噪声,分别表示系统噪声和观测噪声
这个模型的核心点是:我们认为β本身在缓慢变化,而每个观测数据都能对β的估计做一次修正。
不扯了,直接上代码!
咱们用 Python 写个计算个股β值的方法,当然你也可以把上证指数换成股票对应的行业指数,这样就可以得到个股相对行业的β值了。
这里计算卡尔曼的时候用了第三方库通过以下代码安装
pip install pykalman
from xtquant import xtdata
import pandas as pd
from pykalman import KalmanFilter
import numpy as np
import matplotlib.pyplot as plt
def get_hq(code,start_date='19900101',period='1d',dividend_type='front',count=-1):
'''
这里替换成你获取行情的方法
'''
df = pd.read_csv(f'{code}.csv')
return df
def calu_kalman_beta(df_stock,df_index):
'''
计算某个股票相对某个指数的β值
'''
# 对齐日期,按日期升序
df_stock = df_stock.sort_values('date')
df_index = df_index.sort_values('date')
df_stock = df_stock[df_stock['suspendFlag'] == 0] # 剔除停牌数据
# 合并,方便对齐(外层用 inner,保证两个都有数据)
df = pd.merge(df_stock[['date', 'close']],
df_index[['date', 'close']],
on='date',
suffixes=('_stock', '_index'))
# 计算对数收益率(更平滑、更合理)
df['ret_stock'] = np.log(df['close_stock'] / df['close_stock'].shift(1))
df['ret_index'] = np.log(df['close_index'] / df['close_index'].shift(1))
# 去除缺失
df = df.dropna().reset_index(drop=True)
# 提取序列
stock_ret = df['ret_stock'].values
index_ret = df['ret_index'].values
# 初始化卡尔曼滤波器
kf = KalmanFilter(
transition_matrices=1.0,
observation_matrices=1.0 ,
initial_state_mean=0.0,
initial_state_covariance=1.0,
observation_covariance=0.01, # 控制对观测数据的信任度 可微调
transition_covariance=0.00001 # 控制 β 的平滑程度 越小越平滑
)
# 加入极端值裁剪(防止除以接近0)
index_ret_safe = np.where(np.abs(index_ret) < 1e-4, np.sign(index_ret) * 1e-4, index_ret)
# 我们把 market_ret 作为“输入变量”,用于动态预测观测值
observations = stock_ret / index_ret_safe # y_t / x_t
observations = np.clip(observations, -10, 10) # 避免除数太小导致爆炸(你也可以换个方式)
state_means, _ = kf.filter(observations)
df['beta_kalman'] = state_means.flatten()
return df[10:]
if __name__=="__main__":
start_date='20240101'
code = '这里替换成股票代码'
index = '这里是指数代码'
df_stock = get_hq(code=code,start_date=start_date,period='1d')
df_index = get_hq(code=index,start_date=start_date,period='1d')
df = calu_kalman_beta(df_stock,df_index)
# 画图
plt.rcParams['font.sans-serif'] = ['SimHei'] # 使用黑体
plt.rcParams['axes.unicode_minus'] = False # 正负号也正常显示
plt.figure(figsize=(12, 6))
plt.plot(df['date'], df['beta_kalman'], label='动态β(Kalman估计)', color='orange')
plt.axhline(1, linestyle='--', color='gray', alpha=0.5)
plt.title(f'{code} vs {index} 的动态β值')
plt.xlabel('date')
plt.ylabel('β值')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
给大家简单展示几个绘制的图



那我怎么用这个β?
有意思的来了。
用法1:选股
找出动态β长期 < 1 的股票,说明它抗跌能力强——在市场波动大的时候更稳。这类票适合熊市配置。
反过来,找动态β > 1.2 并上升中的,说明它在牛市中可能跑得飞快。
你甚至可以把这个β当作因子,搞个多空策略:多β上升的,空β下降的,构建一个方向中性的策略组合。这个思路其实在某些CTA策略里也有影子。
用法2:择时
你还可以跟踪你组合的整体β,动态调整仓位。
举个栗子:你组合的β从0.9升到1.3,说明风险在上升——这时候该减仓。反之也是。
今天的文章就到这里了,希望大家喜欢。
来源:juejin.cn/post/7533510113068630026
VitePress 彩虹动画
在查阅 VitePress 具体实践时,我被 UnoCSS 文档中的彩虹动画效果深深吸引。在查看其实现原理之后,本文也将探索如何通过自定义组件和样式增强 VitePress 站点,并实现一个炫酷的彩虹动画效果。
自定义主题
VitePress 允许你通过自定义 Layout 来改变页面的结构和样式。自定义 Layout 可以帮助你更好地控制页面的外观和行为,尤其是在复杂的站点中。
项目初始化
在终端中运行以下命令,初始化一个新的 VitePress 项目:
npx vitepress init
然后根据提示,这次选择自定义主题(Default Theme + Customization):
┌ Welcome to VitePress!
│
◇ Where should VitePress initialize the config?
│ ./docs
│
◇ Site title:
│ My Awesome Project
│
◇ Site description:
│ A VitePress Site
│
◇ Theme:
│ Default Theme + Customization
│
◇ Use TypeScript for config and theme files?
│ Yes
│
◇ Add VitePress npm scripts to package.json?
│ Yes
│
└ Done! Now run npm run docs:dev and start writing.
Tips:
- Make sure to add docs/.vitepress/dist and docs/.vitepress/cache to your .gitignore file.
- Since you've chosen to customize the theme, you should also explicitly install vue as a dev dependency.
注意提示,这里需要额外手动安装 vue 库:
pnpm add vue
自定义入口文件
找到 .vitepress/theme/index.ts 入口文件:
// <https://vitepress.dev/guide/custom-theme>
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './style.css'
export default {
extends: DefaultTheme,
Layout: () => {
return h(DefaultTheme.Layout, null, {
// <https://vitepress.dev/guide/extending-default-theme#layout-slots>
})
},
enhanceApp({ app, router, siteData }) {
// ...
}
} satisfies Theme
里面暴露了一个 Layout 组件,这里是通过 h 函数实现的,我们将其抽离成 Layout.vue 组件。
创建自定义 Layout
VitePress 的 Layout 组件是整个网站的骨架,控制了页面的基本结构和布局。通过自定义 Layout,我们可以完全掌控网站的外观和行为。
为什么要自定义 Layout?
- 增加特定的布局元素
- 修改默认主题的行为
- 添加全局组件或功能
- 实现特殊的视觉效果(如我们的彩虹动画)
我们在 .vitepress/theme 文件夹中创建 Layout.vue 组件,并将之前的内容转换成 vue 代码:
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
</script>
<template>
<DefaultTheme.Layout />
</template>
接下来,在 .vitepress/theme/index.ts 中注册自定义 Layout:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
}
这将会覆盖默认的 Layout,应用你自定义的布局结构。
覆盖原本样式
VitePress 提供了 css 变量来动态修改自带的样式,可以看到项目初始化后在 .vitepress/theme 中有一个 style.css。里面提供了案例,告诉如何去修改这些变量。
同时可以通过该链接查看全部的 VitePress 变量:VitePress 默认主题变量。
VitePress 允许我们通过多种方式覆盖默认样式。最常用的方法是创建一个 CSS 文件,并在主题配置中导入。
比如想设置 name 的颜色,就可以通过:
:root {
--vp-home-hero-name-color: blue;
}
引入 UnoCSS
UnoCSS 是一个按需生成 CSS 的工具,可以极大简化 CSS 管理,帮助快速生成高效样式。
在项目中安装 UnoCSS 插件:
pnpm add -D unocss
然后,在 vite.config.ts 中配置 UnoCSS 插件:
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UnoCSS()],
}
通过 UnoCSS,可以轻松应用样式而无需写冗余 CSS。例如,使用以下类名快速创建按钮样式:
<button class="bg-blue-500 text-white p-4 rounded-lg hover:bg-blue-600">
按钮
</button>
实现彩虹动画
彩虹动画是本文的主角,主要通过动态改变 CSS 变量值来实现色彩的平滑过渡。
定义彩虹动画关键帧
通过 @keyframes,在不同颜色之间平滑过渡,形成彩虹动画效果。创建 rainbow.css 文件:
@keyframes rainbow {
0% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
25% {
--vp-c-brand-1: #00a6e2;
--vp-c-brand-light: #56cdff;
--vp-c-brand-lighter: #87f6ff;
--vp-c-brand-dark: #0080b9;
--vp-c-brand-darker: #005c93;
--vp-c-brand-next: #9280ed;
}
50% {
--vp-c-brand-1: #c76dd1;
--vp-c-brand-light: #f194fa;
--vp-c-brand-lighter: #ffbcff;
--vp-c-brand-dark: #9e47a9;
--vp-c-brand-darker: #772082;
--vp-c-brand-next: #eb6552;
}
75% {
--vp-c-brand-1: #e95ca2;
--vp-c-brand-light: #ff84ca;
--vp-c-brand-lighter: #ffadf2;
--vp-c-brand-dark: #be317d;
--vp-c-brand-darker: #940059;
--vp-c-brand-next: #d17a2a;
}
100% {
--vp-c-brand-1: #00a98e;
--vp-c-brand-light: #4ad1b4;
--vp-c-brand-lighter: #78fadc;
--vp-c-brand-dark: #008269;
--vp-c-brand-darker: #005d47;
--vp-c-brand-next: #009ff7;
}
}
:root {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: rainbow 40s linear infinite;
}
html:not(.rainbow) {
--vp-c-brand-1: #00a8cf;
--vp-c-brand-light: #52cff7;
--vp-c-brand-lighter: #82f8ff;
--vp-c-brand-dark: #0082a7;
--vp-c-brand-darker: #005e81;
--vp-c-brand-next: #638af8;
animation: none !important;
}
这段代码定义了彩虹动画的五个关键帧,并将动画应用到根元素上。注意,我们还定义了不带动画的默认状态,这样就可以通过 CSS 类切换动画的启用/禁用。
实现彩虹动画控制组件
接下来,实现名为 RainbowAnimationSwitcher 的组件,其主要逻辑是通过添加或移除 HTML 根元素上的 rainbow 类来控制动画的启用状态,从而实现页面的彩虹渐变效果。
这个组件使用了 @vueuse/core 的两个工具函数:
useLocalStorage用于在浏览器本地存储用户的偏好设置useMediaQuery用于检测用户系统是否设置了减少动画
<script lang="ts" setup>
import { useLocalStorage, useMediaQuery } from '@vueuse/core'
import { inBrowser } from 'vitepress'
import { computed, watch } from 'vue'
import RainbowSwitcher from './RainbowSwitcher.vue'
defineProps<{ text?: string; screenMenu?: boolean }>()
const reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)').value
const animated = useLocalStorage('animate-rainbow', inBrowser ? !reduceMotion : true)
function toggleRainbow() {
animated.value = !animated.value
}
// 在这里对动画做处理
watch(
animated,
anim => {
document.documentElement.classList.remove('rainbow')
if (anim) {
document.documentElement.classList.add('rainbow')
}
},
{ immediate: inBrowser, flush: 'post' },
)
const switchTitle = computed(() => {
return animated.value ? 'Disable rainbow animation' : 'Enable rainbow animation'
})
</script>
<template>
<ClientOnly>
<div class="group" :class="{ mobile: screenMenu }">
<div class="NavScreenRainbowAnimation">
<p class="text">
{{ text ?? 'Rainbow Animation' }}
</p>
<RainbowSwitcher
:title="switchTitle"
class="RainbowAnimationSwitcher"
:aria-checked="animated ? 'true' : 'false'"
@click="toggleRainbow"
>
<span class="i-tabler:rainbow animated" />
<span class="i-tabler:rainbow-off non-animated" />
</RainbowSwitcher>
</div>
</div>
</ClientOnly>
</template>
<style scoped>
.group {
border-top: 1px solid var(--vp-c-divider);
padding-top: 10px;
margin-top: 1rem !important;
}
.NavScreenRainbowAnimation {
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 8px;
padding: 12px;
background-color: var(--vp-c-bg-elv);
max-width: 220px;
}
.text {
line-height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.animated {
opacity: 1;
}
.non-animated {
opacity: 0;
}
.RainbowAnimationSwitcher[aria-checked='false'] .non-animated {
opacity: 1;
}
.RainbowAnimationSwitcher[aria-checked='true'] .animated {
opacity: 1;
}
</style>
其中 RainbowSwitcher 组件是一个简单的开关按钮。以下是其实现:
<template>
<button class="VPSwitch" type="button" role="switch">
<span class="check">
<span v-if="$slots.default" class="icon">
<slot />
</span>
</span>
</button>
</template>
<style scoped>
.VPSwitch {
position: relative;
border-radius: 11px;
display: block;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition: border-color 0.25s !important;
}
.check {
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: var(--vp-c-neutral-inverse);
box-shadow: var(--vp-shadow-1);
transition: transform 0.25s !important;
}
.icon {
position: relative;
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
overflow: hidden;
}
</style>
挂载组件
在 .vitepress/theme/index.ts 中,在 enhanceApp 中挂载组件:
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './Layout.vue'
export default {
extends: DefaultTheme,
Layout: CustomLayout,
enhanceApp({ app, router }) {
app.component('RainbowAnimationSwitcher', RainbowAnimationSwitcher)
if (typeof window === 'undefined') return
watch(
() => router.route.data.relativePath,
() => updateHomePageStyle(location.pathname === '/'),
{ immediate: true },
)
},
}
// Speed up the rainbow animation on home page
function updateHomePageStyle(value: boolean) {
if (value) {
if (homePageStyle) return
homePageStyle = document.createElement('style')
homePageStyle.innerHTML = `
:root {
animation: rainbow 12s linear infinite;
}`
document.body.appendChild(homePageStyle)
} else {
if (!homePageStyle) return
homePageStyle.remove()
homePageStyle = undefined
}
}
在导航栏中使用彩虹动画开关
在 .vitepress/config/index.ts 的配置文件中添加彩虹动画开关按钮:
export default defineConfig({
themeConfig: {
nav: [
// 其他导航项...
{
text: `v${version}`,
items: [
{
text: '发布日志',
link: '<https://github.com/yourusername/repo/releases>',
},
{
text: '提交 Issue',
link: '<https://github.com/yourusername/repo/issues>',
},
{
component: 'RainbowAnimationSwitcher',
props: {
text: '彩虹动画',
},
},
],
},
],
// 其他配置...
},
})
这样,彩虹动画开关就成功加载到导航栏的下拉菜单中。

彩虹动画效果

如果想查看具体效果,可查看 EasyEditor 的文档。其中关于彩虹动画效果的详细实现看,可以查看内部对应的代码:EasyEditor/docs/.vitepress/theme at main · Easy-Editor/EasyEditor。
来源:juejin.cn/post/7508591120407576586
python实现的websocket日志类
背景
功能需求需要实现一个“实时日志”功能,即在一个可以在web端触发任务的系统中,可以即时显示触发的任务的日志(此后台任务在后台或其他worker主机上执行)。最后采用了websocket方法来实现该功能,即在任务执行端实现一个logger类通过websocket上传实时日志给web后端,再由web后端通过websocket连接传给web前端,实现实时日志的功能。
websocket logHandler类
协程版本
使用websockets库
import logging
import json
import time
import asyncio
import websockets
from queue import Queue
from threading import Thread
import traceback
from _queue import Empty
class WebSocketHandler(logging.Handler):
"""
自定义日志处理器,将日志通过WebSocket发送到后端服务
"""
def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
"""
初始化WebSocket处理器
Args:
ws_url: WebSocket服务器URL
reconnect_interval: 断线重连间隔(秒)
max_queue_size: 日志队列最大长度,超出时丢弃旧日志
"""
super().__init__()
self.ws_url = ws_url
self.reconnect_interval = reconnect_interval
self.max_queue_size = max_queue_size
self.is_running = False
self.thread =
self.queue = # 异步队列,在start中初始化
self.loop = # 保存事件循环引用
def emit(self, record):
"""
重写emit方法,将日志记录发送到WebSocket
Args:
record: 日志记录对象
"""
try:
# 格式化日志
msg = self.format(record)
if not msg.endswith("\n"):
msg += "\n"
# 跨线程安全添加日志到队列(关键修复)
if self.loop and self.queue:
# 使用事件循环的线程安全方法添加元素
self.loop.call_soon_threadsafe(
self._safe_put_queue, msg
)
else:
print("队列未初始化,日志发送失败")
except Exception as e:
# 处理发送失败的情况
self.handleError(record)
def _safe_put_queue(self, msg):
"""线程安全的队列添加方法(在事件循环线程执行)"""
try:
if not self.queue.full():
self.queue.put_nowait(msg)
else:
# 队列满时丢弃最旧日志
self.queue.get_nowait()
self.queue.put_nowait(msg)
except Exception as e:
print(f"队列添加失败: {e}")
def start(self):
"""启动WebSocket发送线程"""
if not self.is_running:
self.is_running = True
self.thread = Thread(target=self._ws_sender_thread)
self.thread.daemon = True
self.thread.start()
print("WebSocket发送线程启动")
def stop(self):
"""停止WebSocket发送线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2.0)
def _ws_sender_thread(self):
"""WebSocket发送线程主函数"""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
self.queue = asyncio.Queue(maxsize=self.max_queue_size)
async def custom_heartbeat(websocket):
while True:
try:
await websocket.send(json.dumps({"type":"ping"})) # 自定义心跳消息
await asyncio.sleep(30) # 30秒间隔
except Exception as e:
print(f"心跳发送失败: {e}")
break
async def _process_logs(websocket):
# 连接成功后,发送队列中积压的所有日志
while not self.queue.empty() and self.is_running:
# log_data = self.queue.get_nowait()
# await websocket.send(json.dumps(log_data, ensure_ascii=False))
msg = await self.queue.get()
await websocket.send(msg)
# 持续发送新日志
while self.is_running:
try:
# 阻塞等待新日志,带超时以检查线程是否需要停止
# log_data = self.queue.get(timeout=1)
# await websocket.send(json.dumps(log_data))
msg = await self.queue.get()
await websocket.send(msg)
self.queue.task_done()
except asyncio.TimeoutError:
continue
except Empty:
continue
async def send_logs():
while self.is_running:
try:
# 连接WebSocket服务器
async with websockets.connect(self.ws_url) as websocket:
# 并行运行日志发送和心跳任务
await asyncio.gather(
_process_logs(websocket),
custom_heartbeat(websocket)
)
except Exception as e:
traceback.print_exc()
# 连接失败或断开,等待后重试
# 等待重连间隔(使用异步sleep)
await asyncio.sleep(self.reconnect_interval)
try:
self.loop.run_until_complete(send_logs())
except Exception as e:
pass
finally:
self.loop.close()
def close(self):
"""关闭处理器"""
self.stop()
super().close()
# 配置示例
def setup_logger():
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
# 创建WebSocket处理器
ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
ws_handler.setLevel(logging.DEBUG)
json_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') # 我们在handler中自己处理JSON格式化
ws_handler.setFormatter(json_formatter)
# 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(ws_handler)
# 启动WebSocket处理器
ws_handler.start()
return logger
# 使用示例
if __name__ == "__main__":
logger = setup_logger()
try:
# 正常记录日志,会同时输出到控制台和WebSocket
logger.debug('这是一条调试日志')
logger.info('这是一条信息日志')
logger.warning('这是一条警告日志')
logger.error('这是一条错误日志')
# 模拟长时间运行的程序
while True:
time.sleep(1)
except KeyboardInterrupt:
# 程序退出时,确保WebSocket处理器正确关闭
for handler in logger.handlers:
if isinstance(handler, WebSocketHandler):
handler.stop()
多线程版本
使用websocket-client库
import logging
import json
import time
from queue import Queue, Empty
from threading import Thread
import traceback
import websocket # 需安装:pip install websocket-client
class WebSocketHandler(logging.Handler):
"""自定义日志处理器(同步版本),通过WebSocket发送日志"""
def __init__(self, ws_url, reconnect_interval=5, max_queue_size=10000):
super().__init__()
self.ws_url = ws_url
self.reconnect_interval = reconnect_interval
self.max_queue_size = max_queue_size
self.is_running = False # 控制整体运行状态
self.heartbeat_running = False # 控制心跳线程
self.thread = # 日志发送主线程
self.heartbeat_thread = # 心跳线程
self.queue = Queue(maxsize=max_queue_size) # 同步队列(线程安全)
self.ws = # WebSocket连接实例
def emit(self, record):
"""日志记录触发时调用,将日志放入队列"""
try:
# 格式化日志
msg = self.format(record)
if not msg.endswith("\n"):
msg += "\n"
# 队列满时丢弃最旧日志
if self.queue.full():
try:
self.queue.get_nowait() # 移除最旧日志
except Empty:
pass # 队列已空,无需处理
self.queue.put_nowait(msg) # 放入新日志(同步队列线程安全)
except Exception as e:
self.handleError(record)
def close(self):
"""关闭处理器"""
self.stop()
super().close()
def start(self):
"""启动日志发送线程和心跳线程"""
if not self.is_running:
self.is_running = True
# 启动日志发送主线程
self.thread = Thread(target=self._ws_sender_thread)
self.thread.daemon = True
self.thread.start()
print("WebSocket发送线程启动")
def stop(self):
"""停止所有线程和连接"""
self.is_running = False
self.heartbeat_running = False # 停止心跳线程
# 关闭WebSocket连接
if self.ws:
try:
self.ws.close()
except Exception as e:
print(f"关闭WebSocket失败: {e}")
# 等待线程结束
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2.0)
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
self.heartbeat_thread.join(timeout=1.0)
print("WebSocket发送线程已停止")
def _start_heartbeat(self):
"""启动心跳线程"""
self.heartbeat_running = True
self.heartbeat_thread = Thread(target=self._heartbeat_loop)
self.heartbeat_thread.daemon = True
self.heartbeat_thread.start()
print("心跳线程启动")
def _heartbeat_loop(self):
"""心跳发送循环(独立线程)"""
while self.heartbeat_running and self.is_running:
try:
if self.ws and self.ws.connected: # 检查连接是否有效
self.ws.send(json.dumps({"type": "ping"})) # 发送心跳
time.sleep(30) # 30秒间隔
except Exception as e:
print(f"心跳发送失败: {e}")
break # 心跳失败,退出循环(由主线程重连)
def _process_logs(self):
"""处理队列中的日志并发送(同步阻塞)"""
while self.is_running:
try:
# 阻塞等待日志(超时1秒,避免永久阻塞)
msg = self.queue.get(timeout=1)
if self.ws and self.ws.connected:
self.ws.send(msg) # 发送日志
self.queue.task_done()
else:
# 连接已断开,将日志放回队列
self.queue.put(msg)
time.sleep(0.1) # 短暂等待后重试
except Empty:
continue # 队列空,继续循环
except Exception as e:
print(f"日志发送失败: {e}")
# 发送失败,将日志放回队列重试
try:
self.queue.put(msg)
except Exception as put_err:
print(f"日志放回队列失败: {put_err}")
time.sleep(1) # 等待后重试
def _ws_sender_thread(self):
"""WebSocket发送主线程:负责连接和日志发送协调"""
while self.is_running:
try:
# 建立WebSocket连接
print(f"连接WebSocket服务器: {self.ws_url}")
self.ws = websocket.create_connection(self.ws_url)
print("WebSocket连接成功")
# 启动心跳线程(每次重连后重启心跳)
self._start_heartbeat()
# 处理日志发送
self._process_logs()
except Exception as e:
print(f"WebSocket连接/发送异常: {e}")
traceback.print_exc()
finally:
# 连接断开时清理
self.heartbeat_running = False # 停止当前心跳线程
if self.heartbeat_thread:
self.heartbeat_thread.join(timeout=1.0)
if self.ws:
try:
self.ws.close()
except Exception as e:
print(f"关闭WebSocket连接失败: {e}")
self.ws = # 重置连接实例
# 断线重连等待
if self.is_running:
print(f"等待{self.reconnect_interval}秒后重试...")
time.sleep(self.reconnect_interval)
# 配置示例
def setup_logger():
# 创建logger
logger = logging.getLogger('my_app')
logger.setLevel(logging.DEBUG)
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)
# 创建WebSocket处理器(使用同步版本)
ws_handler = WebSocketHandler(ws_url='ws://localhost:8999/logs/websocket/client-2546')
ws_handler.setLevel(logging.DEBUG)
ws_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ws_handler.setFormatter(ws_formatter)
# 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(ws_handler)
# 启动WebSocket处理器
ws_handler.start()
return logger
# 使用示例
if __name__ == "__main__":
logger = setup_logger()
try:
# 测试日志发送
logger.debug('这是一条调试日志')
logger.info('这是一条信息日志')
logger.warning('这是一条警告日志')
logger.error('这是一条错误日志')
# 模拟长时间运行的程序
while True:
logger.info('持续发送的日志...')
time.sleep(5) # 每5秒发送一条测试日志
except KeyboardInterrupt:
print("程序退出中...")
# 停止WebSocket处理器
for handler in logger.handlers:
if isinstance(handler, WebSocketHandler):
handler.stop()
print("程序已退出")
集成
集成时只需将handler的示例加到全局logger中即可,就像main函数中setup_logger()的使用那样,但需注意正式使用时最好将handler.stop()函数放在finally块中,确保正确退出。
来源:juejin.cn/post/7534661634238185481
代码界的 “建筑师”:建造者模式,让复杂对象构建井然有序
深入理解建造者模式:复杂对象的定制化构建之道
在软件开发中,我们常会遇到需要创建 “复杂对象” 的场景 —— 这类对象由多个部件组成,且部件的组合顺序、配置细节可能存在多种变化。例如,定制一台电脑需要选择 CPU、内存、硬盘等部件;生成一份报告需要包含标题、正文、图表、落款等模块。若直接在客户端代码中编写对象的构建逻辑,不仅会导致代码臃肿、耦合度高,还难以灵活应对不同的定制需求。此时,建造者模式(Builder Pattern) 便能发挥关键作用,它将复杂对象的构建过程与表示分离,让同一构建过程可生成不同的表示。
在软件开发中,我们常会遇到需要创建 “复杂对象” 的场景 —— 这类对象由多个部件组成,且部件的组合顺序、配置细节可能存在多种变化。例如,定制一台电脑需要选择 CPU、内存、硬盘等部件;生成一份报告需要包含标题、正文、图表、落款等模块。若直接在客户端代码中编写对象的构建逻辑,不仅会导致代码臃肿、耦合度高,还难以灵活应对不同的定制需求。此时,建造者模式(Builder Pattern) 便能发挥关键作用,它将复杂对象的构建过程与表示分离,让同一构建过程可生成不同的表示。
一、建造者模式的核心定义与价值
1. 官方定义
建造者模式是 “创建型设计模式” 的重要成员,其核心思想是:将一个复杂对象的构建过程抽象出来,拆分为多个独立的构建步骤,通过不同的 “建造者” 实现这些步骤,再由 “指挥者” 按指定顺序调用步骤,最终组装出完整对象。
简单来说,它就像 “组装家具” 的流程:家具说明书(指挥者)规定了先装框架、再装抽屉、最后装柜门的步骤;而不同品牌的组装师傅(具体建造者),会用不同材质的零件(部件)完成每一步;最终用户(客户端)只需告诉商家 “想要哪种风格的家具”,无需关心具体组装细节。
建造者模式是 “创建型设计模式” 的重要成员,其核心思想是:将一个复杂对象的构建过程抽象出来,拆分为多个独立的构建步骤,通过不同的 “建造者” 实现这些步骤,再由 “指挥者” 按指定顺序调用步骤,最终组装出完整对象。
简单来说,它就像 “组装家具” 的流程:家具说明书(指挥者)规定了先装框架、再装抽屉、最后装柜门的步骤;而不同品牌的组装师傅(具体建造者),会用不同材质的零件(部件)完成每一步;最终用户(客户端)只需告诉商家 “想要哪种风格的家具”,无需关心具体组装细节。
2. 核心价值
- 解耦构建与表示:构建过程(步骤顺序)和对象表示(部件配置)分离,同一过程可生成不同配置的对象(如用相同步骤组装 “游戏本” 和 “轻薄本”)。
- 灵活定制细节:支持对对象部件的精细化控制,客户端可通过选择不同建造者,定制符合需求的对象(如电脑可选择 “i7 CPU+32G 内存” 或 “i5 CPU+16G 内存”)。
- 简化客户端代码:客户端无需关注复杂的构建逻辑,只需与指挥者或建造者简单交互,即可获取完整对象。
- 解耦构建与表示:构建过程(步骤顺序)和对象表示(部件配置)分离,同一过程可生成不同配置的对象(如用相同步骤组装 “游戏本” 和 “轻薄本”)。
- 灵活定制细节:支持对对象部件的精细化控制,客户端可通过选择不同建造者,定制符合需求的对象(如电脑可选择 “i7 CPU+32G 内存” 或 “i5 CPU+16G 内存”)。
- 简化客户端代码:客户端无需关注复杂的构建逻辑,只需与指挥者或建造者简单交互,即可获取完整对象。
二、建造者模式的核心结构
建造者模式通常包含 4 个核心角色,它们分工明确、协作完成对象构建:
角色名称 核心职责 产品(Product) 需要构建的复杂对象,由多个部件组成(如 “电脑”“报告”)。 抽象建造者(Builder) 定义构建产品所需的所有步骤(如 “设置 CPU”“设置内存”),通常包含获取产品的方法。 具体建造者(Concrete Builder) 实现抽象建造者的步骤,定义具体部件的配置(如 “游戏本建造者”“轻薄本建造者”)。 指挥者(Director) 负责调用建造者的步骤,规定构建的顺序(如 “先装 CPU→再装内存→最后装硬盘”)。
建造者模式通常包含 4 个核心角色,它们分工明确、协作完成对象构建:
| 角色名称 | 核心职责 |
|---|---|
| 产品(Product) | 需要构建的复杂对象,由多个部件组成(如 “电脑”“报告”)。 |
| 抽象建造者(Builder) | 定义构建产品所需的所有步骤(如 “设置 CPU”“设置内存”),通常包含获取产品的方法。 |
| 具体建造者(Concrete Builder) | 实现抽象建造者的步骤,定义具体部件的配置(如 “游戏本建造者”“轻薄本建造者”)。 |
| 指挥者(Director) | 负责调用建造者的步骤,规定构建的顺序(如 “先装 CPU→再装内存→最后装硬盘”)。 |
三、建造者模式的实战案例:定制电脑的构建
为了更直观理解,我们以 “定制电脑” 为例,用 Java 代码实现建造者模式:
为了更直观理解,我们以 “定制电脑” 为例,用 Java 代码实现建造者模式:
1. 第一步:定义 “产品”(电脑)
首先明确需要构建的复杂对象 —— 电脑,它包含 CPU、内存、硬盘、显卡等部件:
// 产品:电脑
public class Computer {
// 电脑的部件
private String cpu;
private String memory;
private String hardDisk;
private String graphicsCard;
// Setter方法(用于建造者设置部件)
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setMemory(String memory) {
this.memory = memory;
}
public void setHardDisk(String hardDisk) {
this.hardDisk = hardDisk;
}
public void setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
}
// 展示电脑配置(对象的“表示”)
public void showConfig() {
System.out.println("电脑配置:CPU=" + cpu + ",内存=" + memory + ",硬盘=" + hardDisk + ",显卡=" + graphicsCard);
}
}
首先明确需要构建的复杂对象 —— 电脑,它包含 CPU、内存、硬盘、显卡等部件:
// 产品:电脑
public class Computer {
// 电脑的部件
private String cpu;
private String memory;
private String hardDisk;
private String graphicsCard;
// Setter方法(用于建造者设置部件)
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setMemory(String memory) {
this.memory = memory;
}
public void setHardDisk(String hardDisk) {
this.hardDisk = hardDisk;
}
public void setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
}
// 展示电脑配置(对象的“表示”)
public void showConfig() {
System.out.println("电脑配置:CPU=" + cpu + ",内存=" + memory + ",硬盘=" + hardDisk + ",显卡=" + graphicsCard);
}
}
2. 第二步:定义 “抽象建造者”(电脑建造者接口)
抽象出构建电脑的所有步骤,确保所有具体建造者都遵循统一规范:
// 抽象建造者:电脑建造者接口
public interface ComputerBuilder {
// 构建步骤1:设置CPU
void buildCpu();
// 构建步骤2:设置内存
void buildMemory();
// 构建步骤3:设置硬盘
void buildHardDisk();
// 构建步骤4:设置显卡
void buildGraphicsCard();
// 获取最终构建的电脑
Computer getComputer();
}
抽象出构建电脑的所有步骤,确保所有具体建造者都遵循统一规范:
// 抽象建造者:电脑建造者接口
public interface ComputerBuilder {
// 构建步骤1:设置CPU
void buildCpu();
// 构建步骤2:设置内存
void buildMemory();
// 构建步骤3:设置硬盘
void buildHardDisk();
// 构建步骤4:设置显卡
void buildGraphicsCard();
// 获取最终构建的电脑
Computer getComputer();
}
3. 第三步:实现 “具体建造者”(游戏本 / 轻薄本建造者)
针对不同需求,实现具体的部件配置。例如,“游戏本” 需要高性能 CPU 和显卡,“轻薄本” 更注重便携性(低功耗部件):
// 具体建造者1:游戏本建造者
public class GamingLaptopBuilder implements ComputerBuilder {
private Computer computer = new Computer(); // 持有产品实例
@Override
public void buildCpu() {
computer.setCpu("Intel i9-13900HX(高性能CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("32GB DDR5(高带宽内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("2TB SSD(高速硬盘)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("NVIDIA RTX 4080(高性能显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
// 具体建造者2:轻薄本建造者
public class UltrabookBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i5-1335U(低功耗CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("16GB LPDDR5(低功耗内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("1TB SSD(便携性优先)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("Intel Iris Xe(集成显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
针对不同需求,实现具体的部件配置。例如,“游戏本” 需要高性能 CPU 和显卡,“轻薄本” 更注重便携性(低功耗部件):
// 具体建造者1:游戏本建造者
public class GamingLaptopBuilder implements ComputerBuilder {
private Computer computer = new Computer(); // 持有产品实例
@Override
public void buildCpu() {
computer.setCpu("Intel i9-13900HX(高性能CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("32GB DDR5(高带宽内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("2TB SSD(高速硬盘)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("NVIDIA RTX 4080(高性能显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
// 具体建造者2:轻薄本建造者
public class UltrabookBuilder implements ComputerBuilder {
private Computer computer = new Computer();
@Override
public void buildCpu() {
computer.setCpu("Intel i5-1335U(低功耗CPU)");
}
@Override
public void buildMemory() {
computer.setMemory("16GB LPDDR5(低功耗内存)");
}
@Override
public void buildHardDisk() {
computer.setHardDisk("1TB SSD(便携性优先)");
}
@Override
public void buildGraphicsCard() {
computer.setGraphicsCard("Intel Iris Xe(集成显卡)");
}
@Override
public Computer getComputer() {
return computer;
}
}
4. 第四步:定义 “指挥者”(电脑组装指导者)
指挥者负责规定构建顺序,避免具体建造者与步骤顺序耦合。例如,统一按 “CPU→内存→硬盘→显卡” 的顺序组装:
// 指挥者:电脑组装指导者
public class ComputerDirector {
// 接收具体建造者,按顺序调用构建步骤
public Computer construct(ComputerBuilder builder) {
builder.buildCpu(); // 步骤1:装CPU
builder.buildMemory(); // 步骤2:装内存
builder.buildHardDisk();// 步骤3:装硬盘
builder.buildGraphicsCard();// 步骤4:装显卡
return builder.getComputer(); // 返回组装好的电脑
}
}
指挥者负责规定构建顺序,避免具体建造者与步骤顺序耦合。例如,统一按 “CPU→内存→硬盘→显卡” 的顺序组装:
// 指挥者:电脑组装指导者
public class ComputerDirector {
// 接收具体建造者,按顺序调用构建步骤
public Computer construct(ComputerBuilder builder) {
builder.buildCpu(); // 步骤1:装CPU
builder.buildMemory(); // 步骤2:装内存
builder.buildHardDisk();// 步骤3:装硬盘
builder.buildGraphicsCard();// 步骤4:装显卡
return builder.getComputer(); // 返回组装好的电脑
}
}
5. 第五步:客户端调用(定制电脑)
客户端只需选择 “具体建造者”,无需关心构建步骤,即可获取定制化电脑:
public class Client {
public static void main(String[] args) {
// 1. 创建指挥者
ComputerDirector director = new ComputerDirector();
// 2. 定制游戏本(选择游戏本建造者)
ComputerBuilder gamingBuilder = new GamingLaptopBuilder();
Computer gamingLaptop = director.construct(gamingBuilder);
gamingLaptop.showConfig(); // 输出:游戏本配置
// 3. 定制轻薄本(选择轻薄本建造者)
ComputerBuilder ultrabookBuilder = new UltrabookBuilder();
Computer ultrabook = director.construct(ultrabookBuilder);
ultrabook.showConfig(); // 输出:轻薄本配置
}
}
运行结果:
电脑配置:CPU=Intel i9-13900HX(高性能CPU),内存=32GB DDR5(高带宽内存),硬盘=2TB SSD(高速硬盘),显卡=NVIDIA RTX 4080(高性能显卡)
电脑配置:CPU=Intel i5-1335U(低功耗CPU),内存=16GB LPDDR5(低功耗内存),硬盘=1TB SSD(便携性优先),显卡=Intel Iris Xe(集成显卡)
客户端只需选择 “具体建造者”,无需关心构建步骤,即可获取定制化电脑:
public class Client {
public static void main(String[] args) {
// 1. 创建指挥者
ComputerDirector director = new ComputerDirector();
// 2. 定制游戏本(选择游戏本建造者)
ComputerBuilder gamingBuilder = new GamingLaptopBuilder();
Computer gamingLaptop = director.construct(gamingBuilder);
gamingLaptop.showConfig(); // 输出:游戏本配置
// 3. 定制轻薄本(选择轻薄本建造者)
ComputerBuilder ultrabookBuilder = new UltrabookBuilder();
Computer ultrabook = director.construct(ultrabookBuilder);
ultrabook.showConfig(); // 输出:轻薄本配置
}
}
运行结果:
电脑配置:CPU=Intel i9-13900HX(高性能CPU),内存=32GB DDR5(高带宽内存),硬盘=2TB SSD(高速硬盘),显卡=NVIDIA RTX 4080(高性能显卡)
电脑配置:CPU=Intel i5-1335U(低功耗CPU),内存=16GB LPDDR5(低功耗内存),硬盘=1TB SSD(便携性优先),显卡=Intel Iris Xe(集成显卡)
四、建造者模式的适用场景
并非所有对象创建都需要建造者模式,以下场景最适合使用:
- 复杂对象的定制化构建:对象由多个部件组成,且部件配置、组合顺序存在多种变化(如定制电脑、生成个性化报告、构建汽车)。
- 需要隐藏构建细节:客户端无需知道对象的具体构建步骤,只需获取最终结果(如用户无需知道电脑 “先装 CPU 还是先装内存”)。
- 同一构建过程生成不同表示:通过更换具体建造者,可让同一指挥者(步骤顺序)生成不同配置的对象(如同一组装流程,既做游戏本也做轻薄本)。
并非所有对象创建都需要建造者模式,以下场景最适合使用:
- 复杂对象的定制化构建:对象由多个部件组成,且部件配置、组合顺序存在多种变化(如定制电脑、生成个性化报告、构建汽车)。
- 需要隐藏构建细节:客户端无需知道对象的具体构建步骤,只需获取最终结果(如用户无需知道电脑 “先装 CPU 还是先装内存”)。
五、建造者模式的优缺点
优点
- 灵活性高:支持对对象部件的精细化定制,轻松扩展新的具体建造者(如新增 “工作站电脑建造者”,无需修改原有代码)。
- 代码清晰:将复杂构建逻辑拆分为独立步骤,职责单一,便于维护(构建步骤由指挥者管理,部件配置由建造者管理)。
- 解耦性强:客户端与具体构建步骤、部件配置分离,降低代码耦合度。
- 灵活性高:支持对对象部件的精细化定制,轻松扩展新的具体建造者(如新增 “工作站电脑建造者”,无需修改原有代码)。
缺点
- 增加类数量:每个具体产品需对应一个具体建造者,若产品类型过多,会导致类数量激增(如电脑有 10 种型号,需 10 个具体建造者)。
- 不适用于简单对象:若对象仅由少数部件组成(如 “用户” 对象仅含姓名、年龄),使用建造者模式会显得冗余,不如直接 new 对象高效。
- 增加类数量:每个具体产品需对应一个具体建造者,若产品类型过多,会导致类数量激增(如电脑有 10 种型号,需 10 个具体建造者)。
六、建造者模式与工厂模式对比表
建造者模式与工厂模式虽同属 “创建型模式”,但核心意图和适用场景差异显著,以下是两者的关键对比:
| 对比维度 | 建造者模式(Builder Pattern) | 工厂模式(Factory Pattern) |
|---|---|---|
| 核心意图 | 关注 “如何构建”:拆分复杂对象的构建步骤,定制部件细节 | 关注 “创建什么”:统一创建对象,隐藏实例化逻辑 |
| 产品复杂度 | 适用于复杂对象(由多个部件组成,需分步构建) | 适用于简单 / 标准化对象(单一完整对象,无需分步) |
| 客户端控制度 | 客户端可控制部件配置(选择不同建造者) | 客户端仅控制产品类型(告诉工厂 “要什么”,不关心细节) |
| 角色构成 | 产品、抽象建造者、具体建造者、指挥者(4 个角色) | 产品、抽象工厂、具体工厂(3 个角色,无指挥者) |
| 典型场景 | 定制电脑、组装汽车、生成个性化报告 | 生产标准化产品(如不同品牌的手机、不同类型的日志器) |
| 类比生活场景 | 按需求定制家具(选材质、定尺寸,分步组装) | 从工厂批量购买标准化家电(直接拿成品,不关心生产) |
来源:juejin.cn/post/7543448572341157927
用户请求满天飞,如何精准『导航』?聊聊流量路由那些事儿
嘿,各位未来的技术大佬们,我是老码小张。
不知道大家有没有遇到过这样的场景:你美滋滋地打开刚部署上线的应用 cool-app.com,在国内访问速度飞快。结果第二天,海外的朋友跟你吐槽,说访问你的应用慢得像蜗牛。或者更糟,某个区域的用户突然反馈说服务完全访问不了了!这时候你可能会挠头:用户来自天南海北,服务器也可能部署在不同地方,我怎么才能让每个用户都能又快又稳地访问到我的服务呢?

别慌!这其实就是咱们今天要聊的互联网流量路由策略要解决的问题。搞懂了它,你就掌握了给网络请求“精准导航”的秘诀,让你的应用在全球范围内都能提供更好的用户体验。
流量路由:不止是 DNS 解析那么简单
很多初级小伙伴可能觉得,用户访问网站不就是 浏览器 -> DNS 查询 IP -> 连接服务器 嘛?没错,DNS 是第一步,但现代互联网应用远不止这么简单。特别是当你的服务需要部署在多个数据中心、覆盖不同地理区域的用户时,仅仅返回一个固定的 IP 地址是远远不够的。
我们需要更智能的策略,来决定当用户请求 cool-app.com 时,DNS 服务器应该返回哪个(或哪些)IP 地址。这就引出了各种路由策略(Routing Policies)。你可以把它们想象成 DNS 服务器里的“智能导航系统”,根据不同的规则把用户导向最合适的目的地。
下面,咱们就来盘点几种最常见也最实用的路由策略。
策略一:按地理位置『就近安排』 (Geolocation Routing)
这是最直观的一种策略。顾名思义,它根据用户请求来源的 IP 地址,判断用户的地理位置(比如国家、省份甚至城市),然后将用户导向物理位置上距离最近或者预设好的对应区域的服务器。
工作原理示意:
sequenceDiagram
participant User as 用户 (来自北京)
participant DNS as 智能 DNS 服务器
participant ServerCN as 北京服务器 (1.1.1.1)
participant ServerUS as 美国服务器 (2.2.2.2)
User->>DNS: 查询 cool-app.com 的 IP 地址
activate DNS
DNS-->>DNS: 分析来源 IP,判断用户在北京
DNS-->>User: 返回北京服务器 IP (1.1.1.1)
deactivate DNS
User->>ServerCN: 连接 1.1.1.1
啥时候用?
- 需要为特定地区用户提供本地化内容或服务。
- 有合规性要求,比如某些数据必须存储在用户所在国家境内(像 GDPR)。
- 希望降低跨区域访问带来的延迟。
简单配置示例(伪代码):
// 类似 AWS Route 53 或其他云 DNS 的配置逻辑
RoutingPolicy {
Type: Geolocation,
Rules: [
{ Location: '中国', TargetIP: '1.1.1.1' },
{ Location: '美国', TargetIP: '2.2.2.2' },
{ Location: '*', TargetIP: '3.3.3.3' } // * 代表默认,匹配不到具体位置时使用
]
}
策略二:追求极致速度的『延迟优先』 (Latency-Based Routing)
这个策略的目标是:快! 它不关心用户在哪儿,只关心用户访问哪个服务器的网络延迟最低(也就是 RTT,Round-Trip Time 最短)。DNS 服务商会持续监测从全球不同网络到你各个服务器节点的网络延迟,然后把用户导向响应最快的那个节点。
工作原理示意:
sequenceDiagram
participant User as 用户
participant DNS as 智能 DNS 服务器
participant ServerA as 服务器 A (东京)
participant ServerB as 服务器 B (新加坡)
User->>DNS: 查询 cool-app.com 的 IP 地址
activate DNS
DNS-->>DNS: 检测用户到各服务器的延迟 (到 A: 50ms, 到 B: 30ms)
DNS-->>User: 返回延迟最低的服务器 B 的 IP
deactivate DNS
User->>ServerB: 连接服务器 B
啥时候用?
- 对响应速度要求极高的应用,比如在线游戏、实时通讯。
- 全球用户分布广泛,希望动态地为每个用户找到最快接入点。
注意点: 延迟是动态变化的,所以这种策略依赖于 DNS 服务商持续、准确的延迟探测。
策略三:灵活调度的『按权重分配』 (Weighted Routing)
这种策略允许你给不同的服务节点分配不同的权重(百分比),DNS 服务器会按照你设定的比例,把用户的请求随机分配到这些节点上。
工作原理示意:
假设你有两个版本的服务 V1 和 V2,部署在不同的服务器组上。
// 类似云 DNS 配置
RoutingPolicy {
Type: Weighted,
Targets: [
{ TargetIPGr0up: 'V1_Servers', Weight: 90 }, // 90% 流量到 V1
{ TargetIPGr0up: 'V2_Servers', Weight: 10 } // 10% 流量到 V2
]
}
DNS 会根据这个权重,概率性地返回 V1 或 V2 服务器组的 IP。
啥时候用?
- A/B 测试:想测试新功能?分一小部分流量(比如 5%)到新版本,看看效果。
- 灰度发布/金丝雀发布:新版本上线,先给 1% 的用户试试水,没问题再逐步增加权重到 10%、50%、100%。稳!
- 负载均衡:如果你的服务器配置不同(比如有几台是高性能的,几台是普通配置的),可以按性能分配不同权重,让高性能机器承担更多流量。
策略四:保障高可用的『故障转移』 (Failover Routing)
这个策略是为了高可用性。你需要设置一个主服务节点和一个或多个备用节点。DNS 服务器会持续对主节点进行健康检查(比如探测端口是否存活、HTTP 接口是否返回 200 OK)。
- 正常情况:所有流量都导向主节点。
- 主节点挂了:DNS 检测到主节点 N 次健康检查失败后,会自动把流量切换到备用节点。
- 主节点恢复:一旦主节点恢复健康,流量可以自动切回来(取决于你的配置)。
工作原理示意:
graph LR
A[用户请求] --> B{DNS 健康检查};
B -- 主节点健康 --> C[主服务器];
B -- 主节点故障 --> D[备用服务器];
C --> E[提供服务];
D --> E;
啥时候用?
- 任何对可用性要求高的关键服务。谁也不想服务宕机了用户还一直往坏掉的服务器上撞吧?
- 实现基本的灾备能力。
关键点: 健康检查的配置(频率、失败阈值)和 DNS 记录的 TTL(Time-To-Live,缓存时间)设置很关键。TTL 太长,故障切换就不够及时;TTL 太短,会增加 DNS 查询压力和成本。需要权衡。
策略五:CDN 和大厂最爱『任播』 (Anycast)
Anycast 稍微特殊一点,它通常是在更底层的网络层面(BGP 路由协议)实现的,但 DNS 经常与之配合。简单来说,就是你用同一个 IP 地址在全球多个地点宣告你的服务。用户的请求会被沿途的网络路由器自动导向“网络距离”上最近的那个宣告了该 IP 的节点。
效果: 用户感觉就像是连接到了离他最近的“入口”。
啥时候用?
- CDN 服务:为什么你访问各大 CDN 厂商(如 Cloudflare, Akamai)的资源总是很快?Anycast 是核心技术之一,让用户从最近的边缘节点获取内容。
- 公共 DNS 服务:像 Google 的
8.8.8.8和 Cloudflare 的1.1.1.1都使用了 Anycast,你在全球任何地方 ping 这个 IP,响应的都是离你最近的数据中心。
对于应用开发者来说,你可能不会直接配置 BGP,但你会选择使用提供了 Anycast 网络的服务商(比如某些云厂商的负载均衡器或 CDN 服务)。
选哪个?一张表帮你捋清楚
这么多策略,到底该怎么选呢?别急,我给你整理了个表格,对比一下:
| 策略名称 | 核心原理 | 主要应用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 地理位置路由 | 基于用户 IP 判断地理位置 | 本地化内容、合规性、区域优化 | 实现区域隔离、满足合规 | IP 库可能不准、无法反映真实网络状况 |
| 延迟路由 | 基于网络延迟 (RTT) | 追求最低访问延迟、全球性能优化 | 用户体验好、动态适应网络变化 | 依赖准确探测、成本可能较高 |
| 权重路由 | 按预设比例分配流量 | A/B 测试、灰度发布、按能力负载均衡 | 灵活控制流量分配、上线平稳 | 无法基于用户体验动态调整(除非结合其他策略) |
| 故障转移路由 | 健康检查 + 主备切换 | 高可用、灾备 | 提升服务可靠性、自动化故障处理 | 切换有延迟(受 TTL 和检查频率影响) |
| 任播 (Anycast) | 同一 IP 多点宣告,网络路由就近转发 | CDN、公共 DNS、全球入口优化 | 显著降低延迟、抵抗 DDoS 攻击(分散) | 配置复杂(通常由服务商提供)、成本高 |
实战经验分享:组合拳出奇效!
在实际项目中,我们很少只用单一策略。更常见的是打组合拳:
- 地理位置 + 故障转移:先按区域分配流量(比如中国用户到上海,美国用户到硅谷),然后在每个区域内部署主备服务器,使用故障转移策略保障区域内的高可用。这是很多应用的标配。
- 地理位置 + 权重路由:在一个特定的地理区域内(比如只在中国区),对新上线的服务 V2 使用权重路由进行灰度发布。
- Anycast + 后端智能路由:使用 Anycast IP 作为全球统一入口,流量到达最近的接入点后,再根据后端服务的实际负载、延迟等情况,通过内部的负载均衡器或服务网格(Service Mesh)进行更精细的二次路由。
别忘了监控! 无论你用哪种策略,监控都至关重要。你需要关注:
- 各节点的健康状况。
- 用户的实际访问延迟(可以用 RUM - Real User Monitoring)。
- DNS 解析成功率和解析耗时。
- 流量分布是否符合预期。
有了监控数据,你才能知道你的路由策略是否有效,是否需要调整。
好了,今天关于互联网流量路由策略就先和大家聊这么多。希望这些内容能帮助你理解,当用户的请求“满天飞”时,我们是如何通过这些“智能导航”技术,确保他们能又快又稳地到达目的地的。这不仅仅是运维同学的事,作为开发者,理解这些原理,能让你在设计和部署应用时考虑得更周全。
我是老码小张,一个喜欢研究技术原理,并且在实践中不断成长的技术人。如果你觉得这篇文章对你有帮助,或者有什么想法想交流,欢迎在评论区留言!咱们下次再见!
来源:juejin.cn/post/7498292516493656098
提升React移动端开发效率:Vant组件库
在React中使用Vant组件库的指南
Vant是一套轻量、可靠的移动端组件库,特别适合在React项目中使用。本文将详细介绍如何在React项目中集成和使用Vant组件库,并通过Button按钮和NavBar导航栏等常用组件作为示例,展示其基本用法和高级特性。
一、Vant简介与安装
1.1 Vant是什么
Vant是由有赞前端团队开发的一套基于Vue的移动端组件库,后来也推出了React版本(Vant React)。它提供了60+高质量组件,覆盖了移动端开发的大部分场景,具有以下特点:
- 性能极佳:组件经过精心优化,运行流畅
- 样式美观:遵循统一的设计语言,视觉效果出色
- 功能丰富:提供大量实用组件和灵活配置
- 文档完善:中文文档详细,示例丰富
- 社区活跃:GitHub上star数高,问题响应快
1.2 安装Vant
在React项目中使用Vant前,需要先安装它。确保你已经创建了一个React项目(可以使用create-react-app或其它脚手架工具),然后在项目目录下执行:
bash
npm install vant --save
# 或者使用yarn
yarn add vant
1.3 引入组件样式
Vant的样式文件需要单独引入。推荐在项目的入口文件(通常是src/index.js或src/App.js)中添加以下代码:
jsx
import 'vant/lib/index.css';
这一步非常重要,否则组件将没有样式效果。
二、Button按钮组件使用详解
Button是Vant中最基础也是最常用的组件之一,下面详细介绍它的使用方法。
2.1 基本用法
首先引入Button组件:
jsx
import { Button } from 'vant';
然后在你的组件中使用:
jsx
function MyComponent() {
return (
<div>
<Button type="primary">主要按钮</Button>
<Button type="info">信息按钮</Button>
<Button type="default">默认按钮</Button>
</div>
);
}
2.2 按钮类型
Vant提供了多种按钮类型,通过type属性来设置:
primary: 主要按钮,蓝色背景success: 成功按钮,绿色背景danger: 危险按钮,红色背景warning: 警告按钮,橙色背景default: 默认按钮,灰色背景info: 信息按钮,浅蓝色背景
jsx
<Button type="success">成功按钮</Button>
<Button type="danger">危险按钮</Button>
<Button type="warning">警告按钮</Button>
2.3 按钮形状
除了类型,还可以设置按钮的形状:
- 方形按钮(默认)
- 圆形按钮:添加
round属性 - 圆角按钮:添加
square属性
jsx
<Button round>圆形按钮</Button>
<Button square>圆角按钮</Button>
2.4 按钮尺寸
Vant提供了三种尺寸的按钮:
- 大号按钮:
size="large" - 普通按钮(默认)
- 小号按钮:
size="small" - 迷你按钮:
size="mini"
jsx
<Button size="large">大号按钮</Button>
<Button size="small">小号按钮</Button>
<Button size="mini">迷你按钮</Button>
2.5 按钮状态
按钮有不同的状态,可以通过以下属性控制:
- 禁用状态:
disabled - 加载状态:
loading - 朴素按钮:
plain(边框样式)
jsx
<Button disabled>禁用按钮</Button>
<Button loading>加载中...</Button>
<Button plain>朴素按钮</Button>
2.6 按钮图标
可以在按钮中添加图标,使用icon属性:
jsx
import { Icon } from 'vant';
<Button icon="plus">添加</Button>
<Button icon="search">搜索</Button>
<Button icon={<Icon name="like" />}>点赞</Button>
Vant内置了大量图标,可以在官方文档中查看所有可用图标。
2.7 按钮事件
按钮最常用的就是点击事件:
jsx
function handleClick() {
console.log('按钮被点击了');
}
<Button onClick={handleClick}>点击我</Button>
三、NavBar导航栏组件使用详解
NavBar是移动端常用的顶部导航栏组件,下面详细介绍它的使用方法。
3.1 基本用法
首先引入NavBar组件:
jsx
import { NavBar } from 'vant';
然后在你的组件中使用:
jsx
function MyComponent() {
return (
<NavBar
title="标题"
leftText="返回"
rightText="按钮"
leftArrow
/>
);
}
3.2 主要属性
NavBar组件的主要属性包括:
title: 导航栏标题leftText: 左侧文字rightText: 右侧文字leftArrow: 是否显示左侧箭头fixed: 是否固定在顶部placeholder: 是否生成一个等高的占位元素(配合fixed使用)border: 是否显示下边框zIndex: 设置z-index
jsx
<NavBar
title="个人中心"
leftText="返回"
rightText="设置"
leftArrow
fixed
placeholder
border
zIndex={100}
/>
3.3 自定义内容
除了使用属性,还可以通过插槽自定义导航栏内容:
jsx
<NavBar>
<template #left>
<Icon name="arrow-left" /> 返回
</template>
<template #title>
<span style={{ color: 'red' }}>自定义标题</span>
</template>
<template #right>
<Icon name="search" />
<Icon name="more-o" style={{ marginLeft: '10px' }} />
</template>
</NavBar>
3.4 事件处理
NavBar组件提供了以下事件:
click-left: 点击左侧区域时触发click-right: 点击右侧区域时触发
jsx
function handleClickLeft() {
console.log('点击了左侧');
// 通常用于返回上一页
// history.goBack();
}
function handleClickRight() {
console.log('点击了右侧');
// 可以打开设置页面等
}
<NavBar
title="事件示例"
leftText="返回"
rightText="设置"
leftArrow
onClickLeft={handleClickLeft}
onClickRight={handleClickRight}
/>
3.5 配合路由使用
在实际项目中,NavBar通常需要配合路由使用:
jsx
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleBack = () => {
navigate(-1); // 返回上一页
};
const handleToSettings = () => {
navigate('/settings'); // 跳转到设置页
};
return (
<NavBar
title="路由示例"
leftText="返回"
rightText="设置"
leftArrow
onClickLeft={handleBack}
onClickRight={handleToSettings}
/>
);
}
四、高级用法与注意事项
4.1 主题定制
Vant支持主题定制,可以通过CSS变量来修改主题样式。在项目的全局CSS文件中添加:
css
:root {
--van-primary-color: #ff6a00; /* 修改主题色为橙色 */
--van-border-radius: 8px; /* 修改圆角大小 */
--van-nav-bar-height: 60px; /* 修改导航栏高度 */
}
更多可定制的CSS变量可以参考官方文档。
4.2 按需引入
如果担心引入全部组件会增加包体积,可以使用按需引入的方式。首先安装babel插件:
bash
npm install babel-plugin-import --save-dev
然后在babel配置中添加:
json
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}, "vant"]
]
}
之后就可以按需引入组件了:
jsx
import { Button, NavBar } from 'vant';
这种方式只会打包你实际使用的组件,可以有效减小最终打包体积。
4.3 国际化支持
Vant支持多语言,可以通过LocaleProvider组件设置:
jsx
import { LocaleProvider, Button } from 'vant';
import enUS from 'vant/es/locale/lang/en-US';
function App() {
return (
<LocaleProvider locale={enUS}>
<Button>Submit</Button>
</LocaleProvider>
);
}
4.4 常见问题与解决方案
- 样式不生效:确保已经正确引入了Vant的样式文件
- 组件未定义:检查组件名称拼写是否正确,是否已经正确引入
- TypeScript支持:Vant提供了完整的TypeScript类型定义,可以直接在TS项目中使用
- 移动端适配:建议在项目中同时使用postcss-pxtorem或postcss-px-to-viewport等插件进行移动端适配
五、总结
通过合理使用Vant组件库,可以显著提高React移动端应用的开发效率,同时保证UI的一致性和美观性。建议读者在实际项目中多加练习,掌握更多组件的使用方法。
Vant还提供了许多其他实用组件,如Toast轻提示、Dialog弹出框、List列表等,都可以在官方文档中找到详细的使用说明。
来源:juejin.cn/post/7531667016286863394
transform、translate、transition分别是什么属性,CSS中常用的实现动画方式
transform、translate、transition分别是什么属性,CSS中常用的实现动画方式
在 CSS 中,transform、translate 和 transition 是用于实现元素变换和动画的重要属性。它们各自有不同的作用,通常结合使用可以实现丰富的动画效果。
1. 属性详解
1.1 transform
- 作用:用于对元素进行 2D 或 3D 变换,如旋转、缩放、倾斜、平移等。
- 常用函数:
translate(x, y):平移元素。rotate(angle):旋转元素。scale(x, y):缩放元素。skew(x-angle, y-angle):倾斜元素。matrix(a, b, c, d, e, f):定义 2D 变换矩阵。
- 示例:
.box {
transform: translate(50px, 100px) rotate(45deg) scale(1.5);
}
1.2 translate
- 作用:
translate是transform的一个函数,用于平移元素。 - 语法:
translate(x, y):水平方向移动x,垂直方向移动y。translateX(x):仅水平方向移动。translateY(y):仅垂直方向移动。translateZ(z):在 3D 空间中沿 Z 轴移动。
- 示例:
.box {
transform: translate(50px, 100px);
}
1.3 transition
- 作用:用于定义元素在样式变化时的过渡效果。
- 常用属性:
transition-property:指定需要过渡的属性(如all、opacity、transform等)。transition-duration:指定过渡的持续时间(如1s、500ms)。transition-timing-function:指定过渡的速度曲线(如ease、linear、ease-in-out)。transition-delay:指定过渡的延迟时间(如0.5s)。
- 简写语法:
transition: property duration timing-function delay;
- 示例:
.box {
transition: transform 0.5s ease-in-out, opacity 0.3s linear;
}
2. CSS 中常用的实现动画方式
2.1 使用 transition 实现简单动画
- 适用场景:适用于简单的状态变化动画(如 hover 效果)。
- 示例:
.box {
width: 100px;
height: 100px;
background-color: lightblue;
transition: transform 0.5s ease-in-out;
}
.box:hover {
transform: scale(1.2) rotate(45deg);
}
2.2 使用 @keyframes 和 animation 实现复杂动画
- 适用场景:适用于复杂的多帧动画。
- 步骤:
- 使用
@keyframes定义动画关键帧。 - 使用
animation属性将动画应用到元素上。
- 使用
- 示例:
@keyframes slideIn {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
.box {
width: 100px;
height: 100px;
background-color: lightblue;
animation: slideIn 1s ease-in-out;
}
2.3 使用 transform 和 transition 结合实现交互效果
- 适用场景:适用于用户交互触发的动画(如点击、悬停)。
- 示例:
.box {
width: 100px;
height: 100px;
background-color: lightblue;
transition: transform 0.3s ease-in-out;
}
.box:active {
transform: scale(0.9);
}
2.4 使用 will-change 优化动画性能
- 作用:提前告知浏览器元素将会发生的变化,以优化渲染性能。
- 示例:
.box {
will-change: transform;
}
3. 综合示例
示例 1:按钮点击效果
.button {
padding: 10px 20px;
background-color: lightblue;
border: none;
transition: transform 0.2s ease-in-out;
}
.button:active {
transform: scale(0.95);
}
示例 2:卡片翻转动画
.card {
width: 200px;
height: 200px;
position: relative;
perspective: 1000px;
}
.card-inner {
width: 100%;
height: 100%;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.card:hover .card-inner {
transform: rotateY(180deg);
}
.card-front, .card-back {
width: 100%;
height: 100%;
position: absolute;
backface-visibility: hidden;
}
.card-front {
background-color: lightblue;
}
.card-back {
background-color: lightcoral;
transform: rotateY(180deg);
}
示例 3:加载动画
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.loader {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
总结
| 属性/方法 | 作用 | 适用场景 |
|---|---|---|
transform | 对元素进行 2D/3D 变换 | 平移、旋转、缩放、倾斜等 |
translate | transform 的一个函数,用于平移元素 | 移动元素位置 |
transition | 定义元素样式变化的过渡效果 | 简单的状态变化动画 |
@keyframes | 定义动画关键帧 | 复杂的多帧动画 |
animation | 将@keyframes 定义的动画应用到元素上 | 复杂的多帧动画 |
will-change | 优化动画性能 | 性能优化 |
通过灵活运用这些属性和方法,可以实现丰富的动画效果,提升用户体验。
更多vue相关插件及后台管理模板可访问vue admin reference,代码详情请访问github
来源:juejin.cn/post/7480766452653260852
尝试解决 Android 适配最后一公里
框架介绍
- Android 碎片化至始至终是一个令人非常头疼的问题,特别为 XXPermissions 上面为不同的厂商做适配的时候就非常头疼,因为市面上能找到的开源库只能判断机型的品牌,而不能判断 Android 厂商定制的系统类型,用机型的品牌去做适配会导致出现误判的情况,例如在小米手机上面运行的厂商系统不一定是 MIUI 或者 HyperOS,也有可能是被人刷成了 Android 原生的系统或者其他,反过来也一样,我时常在想,要是有这么一个工具就好了,可以判断 Android 厂商系统的类型及获取厂商系统的版本号,这样就很方便我们做 Android 适配,于是 DeviceCompat 就诞生了,可以轻松识别各种国内外手机厂商和系统版本,帮助大家解决 Android 适配最后一公里的问题。
- 截至到目前,我是行业内第一个也是唯一一个开源这套方案的人,在这里先感谢网上的同行分享的各种方法和思路,让我在开发的过程中少走了很多弯路,另外我也很能理解为什么行业内一直没有人愿意站出来开源这种框架,因为过程非常麻烦,这不仅仅是一个技术问题,还是一个苦活,因为要针对成千上万的机型进行适配。

框架亮点
- 支持识别各种定制 Android 系统(HarmonyOS、MagicOS、MIUI、HyperOS、ColorOS、OriginOS 等)
- 支持判断多种手机厂商品牌(华为、小米、OPPO、vivo、三星等)
- 使用简单,一行代码即可判断设备品牌、厂商系统类型、厂商系统版本
- 兼容性好,支持 Android 4.0 及以上系统
- 体积小巧,仅 12 KB,不会增加应用体积负担
集成步骤
- 如果你的项目 Gradle 配置是在
7.0以下,需要在build.gradle文件中加入
allprojects {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
- 如果你的 Gradle 配置是
7.0及以上,则需要在settings.gradle文件中加入
dependencyResolutionManagement {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
- 配置完远程仓库后,在项目 app 模块下的
build.gradle文件中加入远程依赖
dependencies {
// 设备兼容框架:https://github.com/getActivity/DeviceCompat
implementation 'com.github.getActivity:DeviceCompat:1.0'
}
框架 API 介绍
- 判断系统类型
// 判断当前设备的厂商系统是否为 HyperOS(小米新系统)
DeviceOs.isHyperOs();
// 判断当前设备的厂商系统是否为国内版本的 HyperOS
DeviceOs.isHyperOsByChina();
// 判断当前设备的厂商系统是否为国际版本的 HyperOS
DeviceOs.isHyperOsByGlobal();
// 判断当前设备的厂商系统开启了 HyperOS 的系统优化选项
DeviceOs.isHyperOsOptimization();
// 判断当前设备的厂商系统是否为 MIUI(小米老系统)
DeviceOs.isMiui();
// 判断当前设备的厂商系统是否为国内版本的 MIUI
DeviceOs.isMiuiByChina();
// 判断当前设备的厂商系统是否为国际版本的 MIUI
DeviceOs.isMiuiByGlobal();
// 判断当前设备的厂商系统是否开启了 MIUI 优化选项
DeviceOs.isMiuiOptimization();
// 判断当前设备的厂商系统是否为 RealmeUI(真我系统)
DeviceOs.isRealmeUi();
// 判断当前设备的厂商系统是否为 ColorOS(OPPO 系统)
DeviceOs.isColorOs();
// 判断当前设备的厂商系统是否为 OriginOS(VIVO 系统)
DeviceOs.isOriginOs();
// 判断当前设备的厂商系统是否为 FuntouchOS(VIVO 的老系统)
DeviceOs.isFuntouchOs();
// 判断当前设备的厂商系统是否为 MagicOS(荣耀系统)
DeviceOs.isMagicOs();
// 判断当前设备的厂商系统是否为 HarmonyOS(华为鸿蒙的系统)
DeviceOs.isHarmonyOs();
// 判断当前设备的厂商系统是否为 EMUI(华为和荣耀的老系统)
DeviceOs.isEmui();
// 判断当前设备的厂商系统是否为 OneUI(三星系统)
DeviceOs.isOneUi();
// 判断当前设备的厂商系统是否为 OxygenOS(一加的老系统)
DeviceOs.isOxygenOs();
// 判断当前设备的厂商系统是否为 H2OS(一加的老系统)
DeviceOs.isH2Os();
// 判断当前设备的厂商系统是否为 Flyme(魅族系统)
DeviceOs.isFlyme();
// 判断当前设备的厂商系统是否为 MyOS(中兴或者努比亚的系统)
DeviceOs.isMyOs();
// 判断当前设备的厂商系统是否为 MifavorUI(中兴老系统)
DeviceOs.isMifavorUi();
// 判断当前设备的厂商系统是否为 SmartisanOS(锤子系统)
DeviceOs.isSmartisanOs();
// 判断当前设备的厂商系统是否为 EUI(乐视的系统)
DeviceOs.isEui();
// 判断当前设备的厂商系统是否为 ZUI(摩托罗拉的系统)
DeviceOs.isZui();
// 判断当前设备的厂商系统是否为 360UI(360 系统)
DeviceOs.is360Ui();
// 获取当前设备的厂商系统名称
DeviceOs.getOsName();
// 获取当前设备的厂商系统名称
DeviceOs.getOsName();
// 获取经过美化的厂商系统版本号
DeviceOs.getOsVersionName();
// 获取厂商系统版本的大版本号
DeviceOs.getOsBigVersionCode();
// 获取原始的厂商系统版本号
DeviceOs.getOriginalOsVersionName();
- 判断设备品牌
// 判断当前设备的品牌是否为华为
DeviceBrand.isHuaWei();
// 判断当前设备的品牌是否为荣耀
DeviceBrand.isHonor();
// 判断当前设备的品牌是否为 vivo
DeviceBrand.isVivo();
// 判断当前设备的品牌是否为小米
DeviceBrand.isXiaoMi();
// 判断当前设备的品牌是否为 OPPO
DeviceBrand.isOppo();
// 判断当前设备的品牌是否为真我
DeviceBrand.isRealMe();
// 判断当前设备的品牌是否为乐视
DeviceBrand.isLeEco();
// 判断当前设备的品牌是否为 360
DeviceBrand.is360();
// 判断当前设备的品牌是否为中兴
DeviceBrand.isZte();
// 判断当前设备的品牌是否为一加
DeviceBrand.isOnePlus();
// 判断当前设备的品牌是否为努比亚
DeviceBrand.isNubia();
// 判断当前设备的品牌是否为酷派
DeviceBrand.isCoolPad();
// 判断当前设备的品牌是否为 LG
DeviceBrand.isLg();
// 判断当前设备的品牌是否为 Google
DeviceBrand.isGoogle();
// 判断当前设备的品牌是否为三星
DeviceBrand.isSamsung();
// 判断当前设备的品牌是否为魅族
DeviceBrand.isMeiZu();
// 判断当前设备的品牌是否为联想
DeviceBrand.isLenovo();
// 判断当前设备的品牌是否为锤子
DeviceBrand.isSmartisan();
// 判断当前设备的品牌是否为 HTC
DeviceBrand.isHtc();
// 判断当前设备的品牌是否为索尼
DeviceBrand.isSony();
// 判断当前设备的品牌是否为金立
DeviceBrand.isGionee();
// 判断当前设备的品牌是否为摩托罗拉
DeviceBrand.isMotorola();
// 判断当前设备的品牌是否为传音
DeviceBrand.isTranssion();
// 获取当前设备的品牌名称
DeviceBrand.getBrandName();
- 系统属性相关的方法
// 获取单个系统属性值
SystemPropertyCompat.getSystemPropertyValue((@Nullable String key);
// 获取多个系统属性值
SystemPropertyCompat.getSystemPropertyValues(@Nullable String[] keys);
// 获取多个系统属性中的任一一个值
SystemPropertyCompat.getSystemPropertyAnyOneValue(@Nullable String[] keys);
// 判断某个系统属性是否存在
SystemPropertyCompat.isSystemPropertyExist(@Nullable String key);
// 判断多个系统属性是否有任一一个存在
SystemPropertyCompat.isSystemPropertyAnyOneExist(@Nullable String[] keys);
附上项目开源地址:DeviceCompat
来源:juejin.cn/post/7540524749425180735
Python StringIO 和 BytesIO 用法
在 Python 中,我们平时处理的输入输出(I/O)大多数是针对文件或网络数据的,但有时候,我们希望直接在内存中进行读写,而不必先把数据存到硬盘上。
这时候,StringIO 和 BytesIO 就派上用场了。
参考文章:Python StringIO 和 BytesIO | 简单一点学习 easyeasy.me
简单理解:
- StringIO → 操作内存中的 字符串(
str类型) - BytesIO → 操作内存中的 二进制数据(
bytes类型)
它们都来自 io 模块,接口和文件对象几乎一模一样,所以学会文件操作就能直接用它们。
1. 为什么要用 StringIO / BytesIO
- 无需磁盘 I/O,速度快
- 便于测试(不需要真实文件)
- 在网络编程中常用(比如 HTTP 响应先存在内存中再处理)
- 数据临时存储(比如生成报告、图片)
2. StringIO 基础用法
StringIO 适合处理 文本数据,使用方式类似普通文件:
from io import StringIO
# 创建 StringIO 对象
f = StringIO()
# 写入数据
f.write("Hello ")
f.write("Python")
# 获取当前内容
print(f.getvalue()) # Hello Python
注意:
getvalue()用来获取缓冲区的全部数据。
3. StringIO 读取数据
我们也可以直接用 StringIO 来模拟读取文件:
from io import StringIO
data = "Line1\nLine2\nLine3"
f = StringIO(data)
# 一次性读取
print(f.read())
# 重置游标
f.seek(0)
# 按行读取
for line in f:
print(line.strip())
要点:
seek(0)→ 把“文件指针”移动到开头- 读取方法(
read、readline、readlines)和文件对象一致
4. BytesIO 基础用法
BytesIO 用于处理 二进制数据,比如图片、音频、压缩包等。
from io import BytesIO
# 创建 BytesIO
f = BytesIO()
# 写入二进制数据
f.write(b"Hello World")
# 获取数据
print(f.getvalue()) # b'Hello World'
5. BytesIO 读取数据
from io import BytesIO
data = b"Binary\x00Data"
f = BytesIO(data)
# 读取前6个字节
print(f.read(6)) # b'Binary'
# 继续读剩下的
print(f.read()) # b'\x00Data'
6. StringIO vs BytesIO 区别
| 特性 | StringIO | BytesIO |
|---|---|---|
| 处理数据类型 | str(文本) | bytes(二进制) |
| 读取写入方式 | 和文本文件一致 | 和二进制文件一致 |
| 编码解码 | 不需要手动编码 | 需要自己 .encode() / .decode() |
| 常见应用 | 日志处理、临时文本 | 图片、音频、网络传输数据 |
7. 从文件到内存的转换
有时我们需要把磁盘文件内容读到内存中用 BytesIO 处理,比如处理图片:
from io import BytesIO
# 假设有个图片
with open("test.png", "rb") as f:
data = f.read()
# 存入 BytesIO
bio = BytesIO(data)
# 读取前10个字节
print(bio.read(10))
8. 从内存保存到文件
反过来,我们也可以把 BytesIO 或 StringIO 的数据写到磁盘:
from io import BytesIO
# 创建内存数据
bio = BytesIO()
bio.write(b"Save me to file")
# 写入文件
with open("output.bin", "wb") as f:
f.write(bio.getvalue())
9. 常见使用场景
- 临时构建数据,避免磁盘 I/O
- 测试代码时,模拟文件对象
- 网络数据处理(HTTP 请求、Socket 传输)
- 图片或音频处理(配合 Pillow、pydub 等库)
10. 最佳实践
- 如果处理的是文本,用
StringIO,并且直接写str类型。 - 如果处理的是二进制数据(比如图片、压缩包),用
BytesIO,并且确保写入的是bytes。 - 操作完成后不一定非要
close()(因为它们是内存对象),但为了习惯最好加上。 - 需要频繁读取时,记得用
seek(0)重置指针。
来源:juejin.cn/post/7536182225328046122
摆动序列
摆动序列
一、问题描述
LeetCode:376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如,[1, 7, 4, 9, 2, 5] 是一个摆动序列,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
给定一个整数数组 nums,返回 nums 中作为摆动序列的最长子序列的长度。
二、解题思路
摆动序列的关键在于寻找数组中的峰和谷。每当序列发生方向变化时,摆动序列的长度就会增加。因此,可以通过遍历数组,统计方向变化的次数来得到最长摆动序列的长度。
- 记录初始趋势:计算数组前两个元素的差值作为最开始的变化趋势preTrend = nums[1] - nums[0],若差值不为 0,说明前两个元素构成了摆动序列的初始趋势,此时摆动序列长度初始化为 2;若差值为 0,意味着前两个元素相等,不构成摆动趋势,摆动序列长度初始化为 1。
- 遍历数组寻找变化趋势:记录当前变化趋势curTrend = nums[i] - nums[i - 1],若当前变化趋势curTrend 与之前的变化趋势preTrend 不同,preTrend <= 0 && curTrend > 0 或者 preTrend >= 0 && curTrend < 0 时 更新变化趋势preTrend ,摆动序列加1
三、代码实现
以下是使用 JavaScript 实现的代码:
var wiggleMaxLength = function (nums) {
// 统计波峰波谷的数量
// 若长度为1 或为 0
if (nums.length < 2) return nums.length;
let preTrend = nums[1] - nums[0];
let reLen = preTrend !== 0 ? 2 : 1;
for (let i = 2; i < nums.length; i++) {
let curTrend = nums[i] - nums[i - 1];
// 当出现波谷或者波峰时,更新preTrend
if ((preTrend <= 0 && curTrend > 0) || (preTrend >= 0 && curTrend < 0)) {
preTrend = curTrend;
reLen++;
}
}
return reLen;
};
四、注意事项
- 边界条件需谨慎:在处理数组前两个元素确定初始趋势时,要特别注意数组长度为 2 的情况。若两个元素相等,初始化摆动序列长度为 1,此时不能因为后续没有更多元素判断趋势变化,就错误认为长度还能增加。在遍历过程中,若遇到数组结尾,也应保证最后一次趋势变化能正确统计,避免遗漏。
- 趋势判断避免误判:在比较差值判断趋势变化时,条件 (preTrend <= 0 && curTrend > 0) 与 (preTrend >= 0 && curTrend < 0) 中的 “小于等于” 和 “大于等于” 不能随意替换为 “小于” 和 “大于”。例如,当出现连续相等元素后趋势改变的情况,若使用严格的 “小于” 和 “大于” 判断,可能会错过第一个有效趋势变化点,导致结果错误。
五、复杂度分析
- 时间复杂度:O(n),其中 n 是数组的长度。只需要遍历一次数组。
- 空间复杂度:O(1),只需要常数级的额外空间。
来源:juejin.cn/post/7518198430662492223
绿盟科技重磅发布AI安全运营新成果,全面驱动智能攻防体系升级

8月29日,绿盟科技在北京成功举办以“智御新境·安全无界”为主题的AI赋能安全运营线上成果发布会,全面展示了公司在AI安全领域的最新技术成果与实践经验。
会议总结了“风云卫”AI安全能力平台上半年在客户侧的实际运营成效,介绍了AISOC平台的新特性与能力,进一步验证了“AI+安全运营”在降噪、研判、处置闭环以及未知威胁检测等核心场景中的规模化应用价值。
此外,还正式发布了“绿盟AI智能化渗透系统”,该系统依托AI技术全面赋能渗透测试全流程,可广泛应用于常态化扫描和日常安全运维等场景,有效帮助客户降本增效,显著降低渗透测试的专业门槛。
双底座多智能体架构,构建三位一体AI安全生态
2020年至2022年,绿盟科技连续发布三部AI安全白皮书《安全AI赋能安全运营白皮书》、《知识图谱白皮书》和《AI安全分析白皮书》,并于2023年推出“风云卫”安全大模型,深度融合AI与攻防知识。2025年,公司再度升级,构建“风云卫+DeepSeek”双底座与多智能体架构,打造AI驱动的安全生态,覆盖模型生产、场景适配与应用复制三大层面,全面提升安全检测、运营、数据安全及蓝军对抗能力,实现全域智能赋能。

安全运营实现“智防”质变,绿盟“风云卫”AI实战成效显著

绿盟科技产品BG总经理吴天昊
绿盟科技产品BG总经理吴天昊表示,安全运营人员每天面临几万到几十万不等的原始攻击告警,绿盟“风云卫”AI安全能力平台依托千亿级安全样本训练的大模型,能够自动识别系统日志中的无效告警与重复信息,达到百级左右的高价值告警的优先推荐。
针对不同攻击事件,可自动展开研判分析,精准解析攻击路径和手法,并通过可视化分析界面清晰呈现完整攻击链条。通过自主调查,智能开展横向溯源,自动关联跨端、跨网、跨身份数据,构建出完整的攻击图谱;同时进行并案分析,深度挖掘同类攻击线索,精准定位攻击组织;最后通过SOAR剧本自动生成与执行,实现分钟级事件闭环,并为未来同类事件建立自动化处置范式。
实际应用数据显示,绿盟科技的AI降噪率平均达到95%以上,AI综合辅助研判准确率超过90%。在处置响应方面,依托自主响应可实现超过40%的安全事件端到端的自动化响应处置。特别值得关注的是,经过实际观测和验证,针对13大类77小类的攻击类型,绿盟风云卫AI辅助识别准确率超过95%。
会上,绿盟科技全流程演示了AI赋能安全运营的过程,生动体现了AI技术在安全运营各环节的深度融合——从海量告警的智能降噪、攻击链路的自动重构,到复杂事件的自主研判和自动化响应,真正实现了安全运营从"人防"到"智防"的质变升级。

AI赋能安全检测:混淆检测+自主基线,让未知威胁检测变成可能
在攻防演练中,统计数据显示有76%的攻击属于“已知家族、未知变种”类型,这类攻击因具备一定家族特征又存在变异特性,给检测工作带来不小挑战。
绿盟“风云卫”AI安全能力平台在此类场景中展现出显著优势:在混淆检测方面,AI凭借强大的语义理解能力,能够深入剖析恶意程序的本质特征。即便攻击手段经过混淆处理,改变了表面形态,AI也能透过现象看本质,精准识别出其属于已知家族的核心属性,从而有效识破“未知变种”的伪装。
在自主基线构建上,AI能够自主解读并理解全量日志,从中提炼出账号、流量、进程等各类实体在正常时段的行为画像。基于这些画像,AI可以秒级输出动态阈值,形成精准的正常行为基线。当“已知家族、未知变种”的攻击出现,导致相关实体行为偏离动态阈值时,系统能快速察觉异常,为及时发现和应对威胁提供有力支撑。
智能体中心成效显著,20多个安全领域智能体协同赋能
绿盟“风云卫”AI安全能力平台汇聚绿盟安全专家经验,内置20+安全领域智能体,覆盖网络安全多个关键环节,包含钓鱼邮件检测、可疑样本分析、敏感数据识别、零配置日志解析、情报分析、报告生成等多个智能体。这些智能体既可以赋能产品,也可以独立运行。值得一提的是,智能体中心支持智能体可视化编排,这一特性为用户带来了极大便利。即便是非专业的技术人员,也能通过简单的拖拽、连线操作,如同搭建积木一般,将多个智能体按照企业自身的业务逻辑与安全需求,灵活组合成个性化的安全工作流程。
例如,用户可通过可视化方式自定义编排敏感信息检测智能体,将企业特定的敏感信息嵌入其中,从而实现更精准的自定义检测。这种低代码的编排方式不仅大幅降低了使用门槛,还能灵活应对企业不断变化的安全需求,实现安全防护的定制化与敏捷化,全面提升网络安全工作的效能。

多行业落地实践,安全运营效率大幅提升

绿盟科技鹰眼安全运营中心技术经理李昀磊
截至目前,绿盟科技已助力电信、金融、政府、高校等行业的多家客户实现安全运营智能化转型。在近期多项攻防演练中,公司依托“风云卫”AI安全能力平台,为客户提供全面支撑,多项智能体——包括未知威胁检测、行为分析、钓鱼邮件识别等——均发挥关键作用。
绿盟科技鹰眼安全运营中心技术经理李昀磊介绍,绿盟安全运营中心已持续为超2000家企业提供安全运营服务,并于2023年起全面融合“风云卫”大模型,AI赋能成效主要体现在三方面:
●高频场景AI全自动处置:对实时性要求极高的常见攻击,实现从检测、研判、通知到响应的全自动闭环,无需人工干预;
●复杂事件智能辅助调查:针对约占20%+的复杂事件,AI可自主拓展调查路径,完成情报搜集与初步总结,提升分析师决策效率;
●工具调度与客户交互自动化:AI统一调度多类分析工具,并自动完成工单发送、报告生成与客户反馈响应,显著节约人力。
截至目前,绿盟云端安全运营中心约77%的告警日志依托AI实现辅助研判与处置,在客户预授权条件下5分钟内发现确认并处置闭环事件,运营效率大幅提升。
绿盟AI智能化渗透系统发布

绿盟科技产品总监许伟强
绿盟科技产品总监许伟强表示,公司基于多年攻防实战经验与大模型技术,正式推出新一代绿盟AI智能化渗透系统。该系统全面覆盖常态化扫描与日常安全运维等多种场景,在国内首次实现AI智能体在真实网络环境中完成端到端渗透测试,显著提升渗透效率与响应能力。该系统具备四大核心能力:
●智能任务规划:通过多智能体分层协作,结合专业攻防知识库,实现对复杂渗透场景的智能化任务分解;
●智能工具调度:依托工具调度智能体,无缝调用并协同多种渗透工具,破除工具间壁垒,增强协同作战效能;
●渗透路径推荐:基于安全大模型技术,融合知识图谱与漏洞利用知识,提供渗透路径规划、过程可视化及标准化报告输出;
●AI智能对话:支持自然语言交互,可依据用户指令智能推荐并自动执行渗透工具,大幅降低操作门槛。
绿盟AI智能化渗透系统基于“风云卫”平台构建,采用“人在环路”安全机制与多智能体协同架构,具备“直接模式+深度模式”双轨机制,可快速响应常规攻击面,也可深入攻坚复杂高对抗场景,动态适应多样化的实战攻防需求。

随着国务院常务会议审议通过《关于深入实施“人工智能+”行动的意见》,“人工智能+”正成为产业升级的关键方向,各领域在快速发展的同时,安全问题将不容忽视。绿盟科技始终站在技术前沿,目前形成了以风云卫AI安全能力平台为核心,构建“模型生产、场景适配、应用赋智”的“三位一体”AI安全生态体系,可为不同用户提供全方位的智能安全保障。面向未来,绿盟科技将继续以创新为引擎,携手客户与合作伙伴,共同迎接智能安全新时代。
本地Docker部署Transmission,内网穿透无限远程访问教程
Transmission是一款开源、轻量且资源占用极低的BitTorrent客户端。借助Docker,可以在几分钟内把它跑起来;再借助贝锐花生壳的内网穿透功能,又能随时随地从外网安全访问。下面给出一条龙的部署与远程访问流程,全部命令可直接复制粘贴。

一、准备Docker环境
1. 一键安装最新版Docker(已包含阿里云镜像加速):


2. 启动并设为开机自启:


二、拉取Transmission镜像


如果拉取超时,可在 `/etc/docker/daemon.json` 中追加国内镜像源,例如:

三、运行Transmission容器
下面命令把Web端口9091、BT 监听端口41234(TCP/UDP)都映射出来,并把配置、下载目录、监控目录挂到宿主机持久化。按需替换UID/GID、时区、用户名密码以及宿主机路径。


启动后,浏览器访问 http://局域网IP:9091即可看到Transmission Web UI。

点击左上角图标即可上传种子或粘贴磁力链接开始下载。


四、安装并配置贝锐花生壳
1. 下载客户端
在同一内网任意设备上,从花生壳官网下载最新Linux版客户端,可根据实际情况,选择docker安装花生壳。(`phddns_5.x.x_amd64.deb`)。


2. 安装
根据不同位数的系统输入下面的命令进行安装,安装完成会自动生成SN码与登录密码。


3. 激活与登录
浏览器打开 [花生壳管理平台](http://b.oray.com),用SN和默认密码登录。

首次登录需激活设备,支持微信扫码或绑定贝锐账号。

4. 添加映射
激活成功后,进入「内网穿透」→「添加映射」,填写新增的映射信息。

保存后,贝锐花生壳会生成一个 `http://xxxx.hsk.oray.com:端口` 的外网地址。可访问外网地址访问transmission。

五、外网访问与日常使用
任何地点打开浏览器,输入花生壳提供的外网地址,即可远程管理Transmission:添加种子、查看进度、做种、限速等操作与局域网完全一致。

至此,借助贝锐花生壳内网穿透就可以使本地Docker版Transmission已可安全、便捷地实现远程访问。
收起阅读 »当上组长一年里,我保住了俩下属
前言
人类的悲喜并不相通,有人欢喜有人愁,更多的是看热闹。
就在上周,"苟住"群里的一个小伙伴也苟不住了。

在苟友们的"墙裂"要求下,他分享了他的经验,以他的视角看看他是怎么操作的。
1. 组织变动,意外晋升
两年前加入公司,依然是一线搬砖的码农。
干到一年的时候公司空降了一位号称有诸多大厂履历的大佬来带领研发,说是要给公司带来全新的变化,用技术创造价值。
大领导第一件事:抓人事,提效率。
在此背景下,公司不少有能力的研发另谋出处,也许我看起来人畜无害,居然被提拔当了小组长。
2. 领取任务,开启副本
当了半年的小组长,我的领导就叫他小领导吧,给我传达了大领导最新规划:团队需要保持冲劲,而实现的手段就是汰换。
用人话来说就是:
当季度KPI得E的人,让其填写绩效改进目标,若下一个季度再得到E,那么就得走人
我们绩效等级是ABCDE,A是传说中的等级,B是几个人有机会,大部分人是C和D,E是垫底。
而我们组就有两位小伙伴得到了E,分别是小A和小B。
小领导意思是让他们直接走得了,大不了再招人顶上,而我想着毕竟大家共事一场,现在大环境寒气满满,我也是过来人,还想再争取争取。
于是分析了他们的基本资料,他俩特点还比较鲜明。
小A资料:
- 96年,单身无房贷
- 技术栈较广,技术深度一般,比较粗心
- 坚持己见,沟通少,有些时候会按照自己的想法来实现功能
小B资料:
- 98年,热恋有房贷
- 技术基础较薄弱,但胜在比较认真
- 容易犯一些技术理解上的问题
了解了小A和小B的历史与现状后,我分别找他们沟通,主要是统一共识:
- 你是否认可本次绩效评估结果?
- 你是否认可绩效改进的点与风险点(未达成被裁)?
- 你是否还愿意在这家公司苟?
最重要是第三点,开诚布公,若是都不想苟了,那就保持现状,不要浪费大家时间,我也不想做无用功。
对于他们,分别做了提升策略:
对于小A:
- 每次开启需求前都要求其认真阅读文档,不清楚的地方一定要做记录并向相关人确认
- 遇到比较复杂的需求,我也会一起参与其中梳理技术方案
- 需求开发完成后,CR代码看是否与技术方案设计一致,若有出入需要记录下来,后续复盘为什么
- 给足时间,保证充分自测
对于小B:
- 每次需求多给点时间,多出的时间用来学习技术、熟悉技术
- 要求其将每个需求拆分为尽可能小的点,涉及到哪些技术要想清楚、弄明白
- 鼓励他不懂就要问,我也随时给他解答疑难问题,并说出一些原理让他感兴趣的话可以继续深究
- 分配给他一些技术调研类的任务,提升技术兴趣点与成就感
3. 结束?还是是另一个开始?
半年后...
好消息是:小A、小B的考核结果是D,达成了绩效改进的目标。
坏消息是:据说新的一轮考核算法会变化,宗旨是确保团队血液新鲜(每年至少得置换10%的人)。
随缘吧,我尽力了,也许下一个是我呢?

来源:juejin.cn/post/7532334931021824034
一文说透WebSocket协议(秒懂)
本文首发于公众号:托尼学长,立个写 1024 篇原创技术面试文章的flag,欢迎过来视察监督~
为避免同学们概念混淆,先声明一下,其实WebSocket和Socket之间是毫无关系的,就像北大青鸟和北大一样,大家不要被名字给带偏了。
WebSocket是一种建立在TCP底层连接上,使web客户端和服务器端可进行持续全双工通信的协议。
用大白话来说,WebSocket协议最大的特点是支持服务器端给客户端发送消息。
只需先通过HTTP协议进行握手并进行协议升级,即可让服务器端和客户端一直保持连接并实现通信,直到连接关闭。
如下图所示:

一定会有同学存在疑问,WebSocket协议所具备的“支持服务器端给客户端发送消息”的特点,具体适用场景是什么呢?
下面我们就来详细地讲解一下。
适用场景
对于这个问题,我们有必须逆向思考一下,WebSocket协议所适用的场景,必然是其他协议不适用的场景,这个协议就是HTTP。
由于HTTP协议是半双工模式,只能由客户端发起请求并由服务器端进行响应。
所以在线聊天、实时互动游戏、股票行情、物联网设备监控等业务场景下,只能通过客户端以轮询、长轮询的方式去服务器端获取最新数据。
股票行情场景,如下图所示:


这种方式所带来的问题有两点:
1、客户端频繁发送HTTP请求会带来网络开销,也会给服务器端带来负载压力;2、轮询间隔难以把控,间隔过短同样会带来问题(1)中提到的点,间隔过长会导致数据延迟。
而WebSocket协议只有在服务器端有事件发生的时候,才会第一时间给客户端发送消息,彻底杜绝了HTTP轮询所带来的网络开销、服务器负载和数据时延问题。

实现步骤
阶段一、客户端通过 HTTP 协议发送包含特殊头部的请求,触发协议升级:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
- Upgrade: websocket明确请求升级协议。
- Sec-WebSocket-Key:客户端生成的随机字符串,用于安全验证。
- Sec-WebSocket-Version:指定协议版本(RFC 6455 规定为 13)。
阶段二、服务器端进行响应确认,返回 101 Switching Protocols 响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- Sec-WebSocket-Accept:服务器将客户端的 Sec-WebSocket-Key 与固定字符串拼接后,计算 SHA-1 哈希并进行 Base64 编码,生成验证令牌。
阶段三、此时 TCP 连接从 HTTP 升级为 WebSocket 协议,后续数据可通过二进制帧进行传输。
阶段四、数据传输,WebSocket是一种全双工通信协议,客户端与服务端可同时发送/接收数据,无需等待对方请求,数据帧是以二进制格式进行传输的。
如下图所示:

- FIN (1 bit):标记是否为消息的最后一个分片。
- Opcode (4 bits):定义数据类型(如文本 0x1、二进制 0x2、关闭连接 0x8、Ping 0x9、Pong 0xA)。
- Mask (1 bit):客户端发送的数据需掩码处理(防止缓存污染攻击),服务端发送的数据无需掩码。
- Payload Length (7 or 7+16 or 7+64 bits):帧内容的长度,支持最大 2^64-1 字节。
- Masking-key(32 bits),掩码密钥,由上面的标志位 MASK 决定的,如果使用掩码就是 4 个字节的随机数,否则就不存在。
- payload data 字段:这里存放的就是真正要传输的数据
阶段五、连接关闭,客户端或服务器端都可以发起关闭。
示例代码
前端代码:
<!DOCTYPE html>
<html>
<body>
<input type="text" id="messageInput" placeholder="输入消息">
<button onclick="sendMessage()">发送</button>
<div id="messages"></div>
<script>
// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080/ws');
// 连接打开时触发
socket.addEventListener('open', () => {
logMessage('连接已建立');
});
// 接收消息时触发
socket.addEventListener('message', (event) => {
logMessage('收到消息: ' + event.data);
});
// 连接关闭时触发
socket.addEventListener('close', () => {
logMessage('连接已关闭');
});
// 错误处理
socket.addEventListener('error', (error) => {
logMessage('连接错误: ' + error.message);
});
// 发送消息
function sendMessage() {
const message = document.getElementById('messageInput').value;
socket.send(message);
logMessage('发送消息: ' + message);
}
// 日志输出
function logMessage(message) {
const messagesDiv = document.getElementById('messages');
const p = document.createElement('p');
p.textContent = message;
messagesDiv.appendChild(p);
}
</script>
</body>
</html>
我们通过 Spring WebSocket 来实现服务器端代码。
1、添加 Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.14</version>
</dependency>
2、配置类启用 WebSocket:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws")
.setAllowedOrigins("*");
}
}
3、消息处理器实现:
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class MyWebSocketHandler extends TextWebSocketHandler {
private static final Set<WebSocketSession> sessions =
Collections.synchronizedSet(new HashSet<>());
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
log("新连接: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
log("收到消息: " + payload);
// 广播消息
sessions.forEach(s -> {
if (s.isOpen() && !s.equals(session)) {
try {
s.sendMessage(new TextMessage("广播: " + payload));
} catch (Exception e) {
log("发送消息失败: " + e.getMessage());
}
}
});
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
log("连接关闭: " + session.getId());
}
private void log(String message) {
System.out.println("[MyWebSocketHandler] " + message);
}
}
结语
在本文中,我们先是对WebSocket协议的概念进行了讲解,也对其适用场景、实现步骤进行描述,最后给出了实例代码,旨在帮助大家一站式熟悉WebSocket协议。
来源:juejin.cn/post/7503811248288661558
优化Mini React:避免状态未变更时的重复渲染
优化Mini React:避免状态未变更时的重复渲染
在构建Mini React时,我们发现一个常见的性能问题:即使状态值未发生改变,组件也会进行不必要的重复渲染。本文将深入分析问题原因并实现优化方案。
问题现象分析
以下面代码为例:
function Foo() {
console.log('fooo') // 每次点击都会打印
const [bar, setBar] = React.useState('bar')
function handleClick() {
setBar('bar') // 设置相同的值
}
return (
<div>
{bar}
<button onClick={handleClick}>clickbutton>
div>
);
}
当点击按钮时,虽然状态值bar没有实际变化,但每次点击都会触发组件重新渲染(控制台持续输出"fooo")。这在性能敏感场景下会造成资源浪费。
优化原理与实现
React的核心优化策略之一是:当状态值未改变时,跳过渲染流程。我们在useState的setState函数中加入值比较逻辑:
function useState(initial) {
// ... 状态初始化逻辑
const setState = (action) => {
// 计算期望的新状态
const eagerState = typeof action === 'function'
? action(stateHook.state)
: action;
// 关键优化:状态值未改变时提前返回
if (Object.is(eagerState, stateHook.state)) {
return;
}
// 状态更新及重新渲染逻辑
stateHook.state = eagerState;
scheduleUpdate();
};
return [stateHook.state, setState];
}
优化关键点解析
- 提前计算状态值:
- 处理函数式更新:
action(currentState) - 处理直接赋值:
action
- 处理函数式更新:
- 精准状态比较:
- 使用
Object.is()代替===运算符 - 正确处理特殊值:
NaN、+0/-0等边界情况 - 性能考虑:先比较再更新,避免不必要的渲染流程
- 使用
- 渲染流程优化:
- 状态未变更时直接return,阻断后续更新
- 状态变更时才触发重新渲染调度
优化效果验证
优化后,当点击按钮设置相同状态值时:
setBar('bar') // 与当前状态相同
- 控制台不再输出"fooo"
- 组件不会触发重新渲染
- 虚拟DOM不会进行diff比较
- 真实DOM不会更新
实际应用场景
- 表单控件:输入框失去焦点时重置状态
- 多次相同操作:重复点击相同选项
- 防抖/节流:快速触发时的状态保护
- 数据同步:避免接口返回相同数据时的渲染
扩展思考
- 引用类型优化:
setObj({...obj}) // 内容相同但引用不同
需配合immutable.js或immer等库实现深度比较
- 类组件优化: 在setState方法中实现相同的值比较逻辑
- 性能权衡: 简单值比较成本低,复杂对象比较需评估成本
总结
通过实现状态变更的精准判断,我们:
- 减少不必要的渲染流程
- 降低虚拟DOM diff成本
- 避免真实DOM的无效更新
- 提升组件整体性能
在Mini React中实现的这一优化,体现了React框架设计中的核心性能优化思想。理解这一机制有助于我们编写更高效的React应用代码。
优化本质:计算成本 < 渲染成本时,用计算换渲染
来源:juejin.cn/post/7524992966084083766
前端使用CountUp.js制作数字动画效果的教程
在现代网页设计中,动态数字展示能够显著提升用户体验,吸引访客注意力。无论是数据统计、销售数字还是还是评分展示,平滑的数字增长动画都能让信息传递更加生动。CountUp.js 正是一款专门用于创建这种数字动画效果的轻量级 JavaScript 库,本文将详细介绍其使用方法与技巧。
1. 前言
CountUp.js 是一个零依赖的 JavaScript 库,用于创建从一个数字平滑过渡到另一个数字的动画效果。它体积小巧(压缩后仅约 3KB),使用简单,且高度可定制,能够满足各种数字动画需求。
CountUp.js 的特点
- 零依赖,无需引入其他库
- 轻量级,加载迅速
- 高度可配置(动画时长、延迟、小数位数等)
- 支持多种 easing 动画效果
- 支持暂停、恢复、重置等控制
- 兼容所有现代浏览器
2. 快速开始
CountUp.js 有多种引入方式,可根据项目需求选择:
1. 通过 npm 安装
npm install countup.js
然后在项目中导入:
import CountUp from 'countup.js';
2. 直接引入 CDN
<script src="https://cdn.jsdelivr.net/npm/countup.js@2.0.8/dist/countUp.umd.min.js">script>
3. 下载源码
从 GitHub 仓库 下载源码,直接引入本地文件。
2.1. 基本用法
使用 CountUp.js 只需三步:
- 在 HTML 中准备一个用于显示数字的元素
<div id="counter">div>
- 初始化 CountUp 实例
// 获取 DOM 元素
const element = document.getElementById('counter');
// 目标数值
const target = 1000;
// 创建 CountUp 实例
const countUp = new CountUp(element, target);
- 启动动画
// 检查是否初始化成功,然后启动动画
if (!countUp.error) {
countUp.start();
} else {
console.error(countUp.error);
}
3. 配置选项
CountUp.js 提供了丰富的配置选项,让你可以精确控制动画效果:
const options = {
startVal: 0, // 起始值,默认为 0
duration: 2, // 动画时长(秒),默认为 2
decimalPlaces: 0, // 小数位数,默认为 0
useGr0uping: true, // 是否使用千位分隔符,默认为 true
useEasing: true, // 是否使用缓动效果,默认为 true
smartEasingThreshold: 999, // 智能缓动阈值
smartEasingAmount: 300, // 智能缓动数量
separator: ',', // 千位分隔符,默认为 ','
decimal: '.', // 小数点符号,默认为 '.'
prefix: '', // 数字前缀
suffix: '', // 数字后缀
numerals: [] // 数字替换数组,用于本地化
};
// 使用配置创建实例
const countUp = new CountUp(element, target, options);
3.1. 示例:带前缀和后缀的动画
// 显示"$1,234.56"的动画
const options = {
startVal: 0,
duration: 3,
decimalPlaces: 2,
prefix: '$',
suffix: ''
};
const countUp = new CountUp(document.getElementById('price'), 1234.56, options);
countUp.start();
4. 高级控制方法
CountUp.js 提供了多种方法来控制动画过程:
// 开始动画
countUp.start();
// 暂停动画
countUp.pauseResume();
// 重置动画
countUp.reset();
// 更新目标值并重新开始动画
countUp.update(2000);
// 立即完成动画
countUp.finish();
4.1. 示例:带回调函数的动画
// 动画完成后执行回调函数
countUp.start(() => {
console.log('动画完成!');
// 可以在这里执行后续操作
});
5. 实际应用场景
下面是实际应用场景的模拟:
5.1. 数据统计展示
<div class="stats">
<div class="stat-item">
<h3>用户总数h3>
<div class="stat-value" id="users">div>
div>
<div class="stat-item">
<h3>总销售额h3>
<div class="stat-value" id="sales">div>
div>
<div class="stat-item">
<h3>转化率h3>
<div class="stat-value" id="conversion">div>
div>
div>
<script>
// 初始化多个计数器
const usersCounter = new CountUp('users', 12500, { suffix: '+' });
const salesCounter = new CountUp('sales', 458920, { prefix: '$', decimalPlaces: 0 });
const conversionCounter = new CountUp('conversion', 24.5, { suffix: '%', decimalPlaces: 1 });
// 同时启动所有动画
document.addEventListener('DOMContentLoaded', () => {
usersCounter.start();
salesCounter.start();
conversionCounter.start();
});
script>
5.2. 滚动触发动画
结合 Intersection Observer API,实现元素进入视口时触发动画:
<div id="scrollCounter" class="counter">div>
<script>
// 创建计数器实例但不立即启动
const scrollCounter = new CountUp('scrollCounter', 5000);
// 配置交叉观察器
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入视口,启动动画
scrollCounter.start();
// 只观察一次
observer.unobserve(entry.target);
}
});
});
// 观察目标元素
observer.observe(document.getElementById('scrollCounter'));
script>
5.3. 结合按钮控制
<div id="controlledCounter">div>
<button id="startBtn">开始button>
<button id="pauseBtn">暂停button>
<button id="resetBtn">重置button>
<button id="updateBtn">更新到 2000button>
<script>
const counter = new CountUp('controlledCounter', 1000);
// 按钮事件监听
document.getElementById('startBtn').addEventListener('click', () => {
counter.start();
});
document.getElementById('pauseBtn').addEventListener('click', () => {
counter.pauseResume();
});
document.getElementById('resetBtn').addEventListener('click', () => {
counter.reset();
});
document.getElementById('updateBtn').addEventListener('click', () => {
counter.update(2000);
});
script>
6.自定义缓动函数
CountUp.js 允许你自定义缓动函数,创建独特的动画效果:
// 自定义缓动函数
function myEasing(t, b, c, d) {
// t: 当前时间
// b: 起始值
// c: 变化量 (目标值 - 起始值)
// d: 总时长
t /= d;
return c * t * t * t + b;
}
// 使用自定义缓动函数
const options = {
duration: 2,
easingFn: myEasing
};
const countUp = new CountUp(element, target, options);
countUp.start();
7. 常见问题与解决方案
下面是一些常见问题与解决方案:
7.1. 动画不生效
- 检查元素是否正确获取
- 确保目标值大于起始值(如需从大到小动画,可设置 startVal 大于 target)
- 检查控制台是否有错误信息
7.2. 数字格式问题
- 使用 separator 和 decimal 选项配置数字格式
- 对于特殊数字系统,使用 numerals 选项进行替换
7.3. 性能问题
- 避免在同一页面创建过多计数器实例
- 对于非常大的数字,适当增加动画时长
- 考虑使用滚动触发,而非页面加载时同时启动所有动画
8. 总结
CountUp.js 是一个简单而强大的数字动画库,能够为你的网站增添专业感和活力。它的轻量级特性和丰富的配置选项使其适用于各种场景,从简单的数字展示到复杂的数据可视化。
通过本文介绍的基础用法和高级技巧,你可以轻松实现各种数字动画效果,提升用户体验。无论是个人博客、企业官网还是电商平台,CountUp.js 都能成为你前端工具箱中的得力助手。
参考资源
来源:juejin.cn/post/7542403996917989422
交替打印最容易理解的实现——同步队列
前言
原创不易,禁止转载!
本文旨在实现最简形式的交替打印。理解了同步队列,你可以轻松解决60%以上的多线程面试题。同步队列作为JUC提供的并发原语之一,使用了无锁算法,性能更好,但是却常常被忽略。
交替打印是一类常见的面试题,也是很多人第一次学习并发编程面对的问题,如:
- 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC.......
- 两个线程交替打印1-100的奇偶数
- N个线程循环打印1-100
很多文章(如: zhuanlan.zhihu.com/p/370130458 )总结了实现交替打印的多种做法:
- synchronized + wait/notify: 使用synchronized关键字和wait/notify方法来实现线程间的通信和同步。
- join() : 利用线程的join()方法来确保线程按顺序执行。
- Lock: 使用ReentrantLock来实现线程同步,通过锁的机制来控制线程的执行顺序。
- Lock + Condition: 在Lock的基础上,使用Condition对象来实现更精确的线程唤醒,避免不必要的线程竞争。
- Semaphore: 使用信号量来控制线程的执行顺序,通过acquire()和release()方法来管理线程的访问。
- 此外还有LockSupport、CountDownLatch、AtomicInteger 等实现方式。
笔者认为,在面试时能够选择一种无bug实现即可。
缺点
这些实现使用的都是原语,也就是并发编程中的基本组件,偏向于底层,同时要求开发者深入理解这些原语的工作原理,掌握很多技巧。
问题在于:如果真正的实践中实现,容易出现 bug,一般也不推荐在生产中使用;
这也是八股文的弊端之一:过于关注所谓的底层实现,忽略了真正的实践。
我们分析这些组件的特点,不外乎临界区锁定、线程同步、共享状态等。以下分析一个实现,看看使用到了哪些技巧:
class Wait_Notify_ACB {
private int num;
private static final Object LOCK = new Object();
private void printABC(int targetNum) {
for (int i = 0; i < 10; i++) {
synchronized (LOCK) {
while (num % 3 != targetNum) {
try {
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.print(Thread.currentThread().getName());
LOCK.notifyAll();
}
}
}
public static void main(String[] args) {
Wait_Notify_ACB wait_notify_acb = new Wait_Notify_ACB ();
new Thread(() -> {
wait_notify_acb.printABC(0);
}, "A").start();
new Thread(() -> {
wait_notify_acb.printABC(1);
}, "B").start();
new Thread(() -> {
wait_notify_acb.printABC(2);
}, "C").start();
}
}
整体观之,使用的是 synchronized 隐式锁。使用等待队列实现线程同步,while 循环避免虚假唤醒,维护了多线程共享的 num 状态,此外需要注意多个任务的启动和正确终止。
InterruptedException 的处理是错误的,由于我们没有使用到中断机制,可以包装后抛出 IllegalStateException 表示未预料的异常。实践中,也可以设置当前线程为中断状态,待其他代码进行处理。
Lock不应该是静态的,可以改成非静态或者方法改成静态也行。
总之,经过分析可以看出并发原语的复杂性,那么有没有更高一层的抽象来简化问题呢?
更好的实现
笔者在项目的生产环境中遇到过类似的问题,多个线程需要协作,某些线程需要其他线程的结果,这种结果的交接是即时的,也就是说,A线程的结果直接交给B线程进行处理。
更好的实现要求我们实现线程之间的同步,同时应该避免并发修改。我们很自然地想到 SynchronousQueue,使用 CSP 实现 + CompletableFuture,可以减少我们考虑底层的心智负担,方便写出正确的代码。SynchronousQueue 适用于需要在生产者和消费者之间进行直接移交的场景,通常用于线程之间的切换或传递任务。
看一个具体例子:
以下是两个线程交替打印 1 - 100 的实现,由于没有在代码中使用锁,也没有状态维护的烦恼,这也是函数式的思想(减少状态)。
实现思路为:任务1从队列1中取结果,计算,提交给队列2。任务2同理。使用SynchronousQueue 实现直接交接。
private static Stopwatch betterImpl() {
Stopwatch sw = Stopwatch.createStarted();
BlockingQueue<Integer> q1 = new SynchronousQueue<>();
BlockingQueue<Integer> q2 = new SynchronousQueue<>();
int limit = 100;
CompletableFuture<Void> cf1 = CompletableFuture.runAsync(() -> {
while (true) {
Integer i = Uninterruptibles.takeUninterruptibly(q1);
if (i <= limit) {
System.out.println("thread1: i = " + i);
}
Uninterruptibles.putUninterruptibly(q2, i + 1);
if (i == limit - 1) {
break;
}
}
});
CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> {
while (true) {
Integer i = Uninterruptibles.takeUninterruptibly(q2);
if (i <= limit) {
System.out.println("thread2: i = " + i);
}
if (i == limit) {
break;
}
Uninterruptibles.putUninterruptibly(q1, i + 1);
}
});
Uninterruptibles.putUninterruptibly(q1, 1);
CompletableFuture.allOf(cf1, cf2).join();
return sw.stop();
}
Uninterruptibles 是 Guava 中的并发工具,很实用,可以避免 try-catch 中断异常这样的样板代码。
线程池配置与本文讨论内容关系不大,故忽略。
一般实践中,阻塞方法都要设置超时时间,这里也忽略了。
这个实现简单明了,性能也不错。如果不需要即时交接,可以替换成缓冲队列(如 ArrayBlockingQueue)。
笔者简单比较了两种实现,结果如下:
private static Stopwatch bufferImpl() {
Stopwatch sw = Stopwatch.createStarted();
BlockingQueue<Integer> q1 = new ArrayBlockingQueue<>(2);
BlockingQueue<Integer> q2 = new ArrayBlockingQueue<>(2);
// ...
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
betterImpl();
bufferImpl();
// 预热
}
Stopwatch result1 = bufferImpl();
Stopwatch result2 = betterImpl();
System.out.println("result1 = " + result1);
System.out.println("result2 = " + result2);
}
// ...
thread2: i = 92
thread1: i = 93
thread2: i = 94
thread1: i = 95
thread2: i = 96
thread1: i = 97
thread2: i = 98
thread1: i = 99
thread2: i = 100
result1 = 490.3 μs
result2 = 469.1 μs
结论:使用 SynchronousQueue 性能更好,感兴趣的读者可以自己写 JMH 比对。
如果你觉得本文对你有帮助的话,欢迎给个点赞加收藏,也欢迎进一步的讨论。
后续我将继续分享并发编程、性能优化等有趣内容,力求做到全网独一份、深入浅出,一周两更,欢迎关注支持。
来源:juejin.cn/post/7532925096828026899
uniapp图片上传添加水印/压缩/剪裁
一、前言
最近遇到一个需求,微信小程序上传图片添加水印的需求,故此有该文章做总结, 功能涵盖定理地位,百度地址解析,图片四角水印,图片压缩,图片压缩并添加水印,图片剪裁,定位授权,保存图片到相册等
二、效果

三、代码实现核心
3.1)添加水印并压缩
核心实现
// 添加水印并压缩
export function addWatermarkAndCompress(options, that, isCompress = false) {
return new Promise((resolve, reject) => {
const {
errLog,
config
} = dealWatermarkConfig(options)
that.watermarkCanvasOption.width = 0
that.watermarkCanvasOption.height = 0
if (!errLog.length) {
const {
canvasId,
imagePath,
watermarkList,
quality = 0.6
} = config
uni.getImageInfo({ // 获取图片信息,以便获取图片的真实宽高信息
src: imagePath,
success: (info) => {
const {
width: oWidth,
height: oHeight,
type,
orientation
} = info; // 获取图片的原始宽高
const fileTypeObj = {
'jpeg': 'jpg',
'jpg': 'jpg',
'png': 'png',
}
const fileType = fileTypeObj[type] || 'png'
let width = oWidth
let height = oHeight
if (isCompress) {
const {
cWidth,
cHeight
} = calcRatioHeightAndWight({
oWidth,
oHeight,
quality,
orientation
})
// 按对折比例缩小
width = cWidth
height = cHeight
}
that.watermarkCanvasOption.width = width
that.watermarkCanvasOption.height = height
that.$nextTick(() => {
// 获取canvas绘图上下文
const ctx = uni.createCanvasContext(canvasId, that);
// 绘制原始图片到canvas上
ctx.drawImage(imagePath, 0, 0, width, height);
// 绘制水印项
const drawWMItem = (ctx, options) => {
const {
fontSize,
color,
text: cText,
position,
margin
} = options
// 添加水印
ctx.setFontSize(fontSize); // 设置字体大小
ctx.setFillStyle(color); // 设置字体颜色为红色
if (isNotEmptyArr(cText)) {
const text = cText.filter(Boolean)
if (position.startsWith('bottom')) {
text.reverse()
}
text.forEach((str, ind) => {
const textMetrics = ctx.measureText(str);
const {
calcX,
calcY
} = calcPosition({
height,
width,
position,
margin,
ind,
fontSize,
textMetrics
})
ctx.fillText(str, calcX, calcY, width);
})
} else {
const textMetrics = ctx.measureText(cText);
const {
calcX,
calcY
} = calcPosition({
height,
width,
position,
margin,
ind: 0,
fontSize,
textMetrics
})
// 在图片底部添加水印文字
ctx.fillText(text, calcX, calcY, width);
}
}
watermarkList.forEach(ele => {
drawWMItem(ctx, ele)
})
// 绘制完成后执行的操作,这里不等待绘制完成就继续执行后续操作,因为我们要导出为图片
ctx.draw(false, () => {
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width,
height,
fileType,
quality, // 图片的质量,目前仅对 jpg 有效。取值范围为 (0, 1],不在范围内时当作 1.0 处理。
destWidth: width,
destHeight: height,
success: (res) => {
console.log('res.tempFilePath', res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif
// #ifdef MP-ALIPAY
ctx.toTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width: width,
height: height,
destWidth: width,
destHeight: height,
quality,
fileType,
success: (res) => {
console.log('res.tempFilePath', res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif
});
})
}
});
} else {
const errStr = errLog.join(';')
showMsg(errStr)
reject(errStr)
}
})
}
3.2)剪切图片
// 剪切图片
export function clipImg(options, that) {
return new Promise((resolve, reject) => {
const {
errLog,
config
} = dealClipImgConfig(options)
that.watermarkCanvasOption.width = 0
that.watermarkCanvasOption.height = 0
if (!errLog.length) {
const {
canvasId,
imagePath,
cWidth,
cHeight,
position
} = config
// 获取图片信息,以便获取图片的真实宽高信息
uni.getImageInfo({
src: imagePath,
success: (info) => {
const {
width,
height
} = info; // 获取图片的原始宽高
// 自定义剪裁范围要在图片内
if (width >= cWidth && height >= cHeight) {
that.watermarkCanvasOption.width = width
that.watermarkCanvasOption.height = height
that.$nextTick(() => {
// 获取canvas绘图上下文
const ctx = uni.createCanvasContext(canvasId, that);
const {
calcSX,
calcSY,
calcEX,
calcEY
} = calcClipPosition({
cWidth,
cHeight,
position,
width,
height
})
// 绘制原始图片到canvas上
ctx.drawImage(imagePath, 0, 0, width, height);
// 绘制完成后执行的操作,这里不等待绘制完成就继续执行后续操作,因为我们要导出为图片
ctx.draw(false, () => {
// #ifndef MP-ALIPAY
uni.canvasToTempFilePath({ // 将画布内容导出为图片
canvasId,
x: calcSX,
y: calcSY,
width: cWidth,
height: cHeight,
destWidth: cWidth,
destHeight: cHeight,
success: (res) => {
console.log('res.tempFilePath',
res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif
// #ifdef MP-ALIPAY
ctx.toTempFilePath({ // 将画布内容导出为图片
canvasId,
x: 0,
y: 0,
width: width,
height: height,
destWidth: width,
destHeight: height,
// fileType: 'png',
success: (res) => {
console.log('res.tempFilePath',
res)
resolve(res.tempFilePath)
},
fail() {
reject(false)
}
}, that);
// #endif
});
})
} else {
return imagePath
}
}
})
} else {
const errStr = errLog.join(';')
showMsg(errStr)
reject(errStr)
}
})
}
3.3)canvas画布标签
<!-- 给图片添加的标签 -->
<canvas v-if="watermarkCanvasOption.width > 0 && watermarkCanvasOption.height > 0"
:style="{ width: watermarkCanvasOption.width + 'px', height: watermarkCanvasOption.height + 'px' }"
canvas-id="watermarkCanvas" id="watermarkCanvas" style="position: absolute; top: -10000000rpx;" />
以上代码具体的实现功能不做一一讲解,详细请看下方源码地址
四、源码地址
github: github.com/ArcherNull/…
五、总结
- 图片的操作,例如压缩/剪裁/加水印都是需要借助canvas标签,也就是说需要有canvas实例通过该api实现这些操作
- 当执行 ctx.drawImage(imagePath, 0, 0, width, height) 后,后续的操作的是对内存中的数据,而不是源文件
完结撒花,如果对您有帮助,请一键三连
来源:juejin.cn/post/7513183180092031011
订单表超10亿数据,如何设计Sharding策略?解决跨分片查询和分布式事务?
订单表超10亿数据,如何设计Sharding策略?解决跨分片查询和分布式事务?
引言:
在电商平台高速发展的今天,海量订单处理已成为技术团队必须面对的挑战。当订单数据突破10亿大关,传统单库架构在查询性能、存储容量和运维复杂度上都会遇到瓶颈。
作为有8年经验的Java工程师,我曾主导多个日订单量百万级系统的分库分表改造。今天我将分享从Sharding策略设计到分布式事务落地的完整解决方案,其中包含核心代码实现和实战避坑指南。
一、业务场景分析
1.1 订单数据特点
- 数据量大:日增订单50万+,年增1.8亿
- 访问模式:
- 写操作:高频下单(峰值5000 TPS)
- 读操作:订单查询(用户端+运营端)
- 数据生命周期:热数据(3个月)占80%访问量
1.2 核心挑战
graph LR
A[10亿级订单] --> B[查询性能]
A --> C[存储瓶颈]
A --> D[跨分片聚合]
A --> E[分布式事务]
二、Sharding策略设计
2.1 分片键选择
| 候选方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 用户ID | 用户维度查询快 | 可能导致数据倾斜 | C端主导业务 |
| 订单ID | 数据分布均匀 | 用户订单需跨分片查询 | 均匀分布场景 |
| 商户ID | 商户维度查询快 | C端查询效率低 | B2B平台 |
| 创建时间 | 冷热数据分离 | 范围查询可能跨分片 | 推荐方案 |
最终方案:复合分片键(用户ID+创建时间)
2.2 分片算法设计
/**
* 自定义复合分片算法
* 分片键:user_id + create_time
*/
public class OrderShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
private static final String USER_KEY = "user_id";
private static final String TIME_KEY = "create_time";
@Override
public Collection<String> doSharding(
Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> shardingValue) {
Map<String, Collection<Long>> columnMap = shardingValue.getColumnNameAndShardingValuesMap();
List<String> shardingResults = new ArrayList<>();
// 获取用户ID分片值
Collection<Long> userIds = columnMap.get(USER_KEY);
Long userId = userIds.stream().findFirst().orElseThrow();
// 获取时间分片值
Collection<Long> timestamps = columnMap.get(TIME_KEY);
Long createTime = timestamps.stream().findFirst().orElse(System.currentTimeMillis());
// 计算用户分片(16个分库)
int dbShard = Math.abs(userId.hashCode()) % 16;
// 计算时间分片(按月分表)
LocalDateTime dateTime = Instant.ofEpochMilli(createTime)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
String tableSuffix = dateTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
// 构建目标分片
String targetDB = "order_db_" + dbShard;
String targetTable = "t_order_" + tableSuffix;
shardingResults.add(targetDB + "." + targetTable);
return shardingResults;
}
}
2.3 分片策略配置(ShardingSphere)
# application-sharding.yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,...,ds15
# 配置16个数据源...
sharding:
tables:
t_order:
actualDataNodes: ds${0..15}.t_order_${202301..202412}
tableStrategy:
complex:
shardingColumns: user_id,create_time
algorithmClassName: com.xxx.OrderShardingAlgorithm
keyGenerator:
column: order_id
type: SNOWFLAKE
三、跨分片查询解决方案
3.1 常见问题及对策
| 问题类型 | 传统方案痛点 | 优化方案 |
|---|---|---|
| 分页查询 | LIMIT 0,10 扫描全表 | 二次查询法 |
| 排序聚合 | 内存合并性能差 | 并行查询+流式处理 |
| 全局索引 | 无法直接建立 | 异步构建ES索引 |
3.2 分页查询优化实现
/**
* 跨分片分页查询优化(二次查询法)
* 原SQL:SELECT * FROM t_order WHERE user_id=1001 ORDER BY create_time DESC LIMIT 10000,10
*/
public Page<Order> shardingPageQuery(Long userId, int pageNo, int pageSize) {
// 第一步:全分片并行查询
List<Order> allShardResults = shardingExecute(
shard -> "SELECT order_id, create_time FROM t_order "
+ "WHERE user_id = " + userId
+ " ORDER BY create_time DESC"
);
// 第二步:内存排序取TopN
List<Long> targetIds = allShardResults.stream()
.sorted(Comparator.comparing(Order::getCreateTime).reversed())
.skip(pageNo * pageSize)
.limit(pageSize)
.map(Order::getOrderId)
.collect(Collectors.toList());
// 第三步:精准查询目标数据
return orderRepository.findByIdIn(targetIds);
}
/**
* 并行执行查询(使用CompletableFuture)
*/
private List<Order> shardingExecute(Function<Integer, String> sqlBuilder) {
List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
for (int i = 0; i < 16; i++) {
final int shardId = i;
futures.add(CompletableFuture.supplyAsync(() -> {
String sql = sqlBuilder.apply(shardId);
return jdbcTemplate.query(sql, new OrderRowMapper());
}, shardingThreadPool));
}
return futures.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.collect(Collectors.toList());
}
3.3 聚合查询优化
/**
* 分布式聚合计算(如:用户总订单金额)
* 方案:并行查询分片结果 + 内存汇总
*/
public BigDecimal calculateUserTotalAmount(Long userId) {
List<CompletableFuture<BigDecimal>> futures = new ArrayList<>();
for (int i = 0; i < 16; i++) {
futures.add(CompletableFuture.supplyAsync(() -> {
String sql = "SELECT SUM(amount) FROM t_order WHERE user_id = ?";
return jdbcTemplate.queryForObject(
sql, BigDecimal.class, userId);
}, shardingThreadPool));
}
return futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
四、分布式事务解决方案
4.1 方案对比
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC | 强一致 | 差 | 高 | 银行核心系统 |
| TCC | 强一致 | 中 | 高 | 资金交易 |
| Saga | 最终一致 | 优 | 中 | 订单系统(推荐) |
| 本地消息表 | 最终一致 | 良 | 低 | 低要求场景 |
4.2 Saga事务实现(订单创建场景)
sequenceDiagram
participant C as 应用
participant O as 订单服务
participant I as 库存服务
participant P as 支付服务
C->>O: 创建订单
O->>I: 预扣库存
I-->>O: 扣减成功
O->>P: 发起支付
P-->>O: 支付成功
O->>C: 返回结果
alt 支付失败
O->>I: 释放库存(补偿)
end
4.3 核心代码实现
/**
* Saga事务管理器(使用Seata框架)
*/
@Service
@Slf4j
public class OrderSagaService {
@Autowired
private InventoryFeignClient inventoryClient;
@Autowired
private PaymentFeignClient paymentClient;
@Transactional
public void createOrder(OrderCreateDTO dto) {
// 1. 创建本地订单(状态:待支付)
Order order = createPendingOrder(dto);
try {
// 2. 调用库存服务(Saga参与者)
inventoryClient.deductStock(
new DeductRequest(order.getOrderId(), dto.getSkuItems()));
// 3. 调用支付服务(Saga参与者)
paymentClient.createPayment(
new PaymentRequest(order.getOrderId(), order.getTotalAmount()));
// 4. 更新订单状态为已支付
order.paySuccess();
orderRepository.update(order);
} catch (Exception ex) {
// 触发Saga补偿流程
log.error("订单创建失败,触发补偿", ex);
handleCreateOrderFailure(order, ex);
throw ex;
}
}
/**
* 补偿操作(需要幂等)
*/
@Compensable(compensationMethod = "compensateOrder")
private void handleCreateOrderFailure(Order order, Exception ex) {
// 1. 释放库存
inventoryClient.restoreStock(order.getOrderId());
// 2. 取消支付(如果已发起)
paymentClient.cancelPayment(order.getOrderId());
// 3. 标记订单失败
order.cancel("系统异常: " + ex.getMessage());
orderRepository.update(order);
}
/**
* 补偿方法(幂等设计)
*/
public void compensateOrder(Order order, Exception ex) {
// 通过状态判断避免重复补偿
if (order.getStatus() != OrderStatus.CANCELLED) {
handleCreateOrderFailure(order, ex);
}
}
}
五、性能优化实践
5.1 分片路由优化
/**
* 热点用户订单查询优化
* 方案:用户分片路由缓存
*/
@Aspect
@Component
public class ShardingRouteCacheAspect {
@Autowired
private RedisTemplate<String, Integer> redisTemplate;
private static final String ROUTE_KEY = "user_route:%d";
@Around("@annotation(org.apache.shardingsphere.api.hint.Hint)")
public Object cacheRoute(ProceedingJoinPoint joinPoint) throws Throwable {
Long userId = getUserIdFromArgs(joinPoint.getArgs());
if (userId == null) {
return joinPoint.proceed();
}
// 1. 查询缓存
String cacheKey = String.format(ROUTE_KEY, userId);
Integer shardId = redisTemplate.opsForValue().get(cacheKey);
if (shardId == null) {
// 2. 计算分片ID(避免全表扫描)
shardId = calculateUserShard(userId);
redisTemplate.opsForValue().set(cacheKey, shardId, 1, TimeUnit.HOURS);
}
// 3. 设置分片Hint强制路由
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setDatabaseShardingValue(shardId);
return joinPoint.proceed();
}
}
private int calculateUserShard(Long userId) {
// 分片计算逻辑(与分片算法保持一致)
return Math.abs(userId.hashCode()) % 16;
}
}
5.2 冷热数据分离
-- 归档策略示例(每月执行)
CREATE EVENT archive_orders
ON SCHEDULE EVERY 1 MONTH
DO
BEGIN
-- 1. 创建归档表(按年月)
SET @archive_table = CONCAT('t_order_archive_', DATE_FORMAT(NOW(), '%Y%m'));
SET @create_sql = CONCAT('CREATE TABLE IF NOT EXISTS ', @archive_table, ' LIKE t_order');
PREPARE stmt FROM @create_sql; EXECUTE stmt;
-- 2. 迁移数据(6个月前)
SET @move_sql = CONCAT(
'INSERT INTO ', @archive_table,
' SELECT * FROM t_order WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH)'
);
PREPARE stmt FROM @move_sql; EXECUTE stmt;
-- 3. 删除原表数据
DELETE FROM t_order WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH);
END
六、避坑指南
6.1 常见问题及解决方案
| 问题类型 | 现象 | 解决方案 |
|---|---|---|
| 分片键选择不当 | 数据倾斜(70%数据在1个分片) | 增加分片基数(复合分片键) |
| 分布式事务超时 | 库存释放失败 | 增加重试机制+人工补偿台 |
| 跨分片查询性能差 | 分页查询超时 | 改用ES做全局搜索 |
| 扩容困难 | 增加分片需迁移数据 | 初始设计预留分片(32库) |
6.2 必须实现的监控项
graph TD
A[监控大盘] --> B[分片负载]
A --> C[慢查询TOP10]
A --> D[分布式事务成功率]
A --> E[热点用户检测]
A --> F[归档任务状态]
七、总结与展望
分库分表本质是业务与技术的平衡艺术,经过多个项目的实践验证,我总结了以下核心经验:
- 分片设计三原则:
- 数据分布均匀性 > 查询便捷性
- 业务可扩展性 > 短期性能
- 简单可运维 > 技术先进性
- 演进路线建议:
graph LR
A[单库] --> B[读写分离]
B --> C[垂直分库]
C --> D[水平分表]
D --> E[单元化部署]
- 未来优化方向:
- 基于TiDB的HTAP架构
- 使用Apache ShardingSphere-Proxy
- 智能分片路由(AI预测热点)
最后的话:
处理10亿级订单如同指挥一场交响乐——每个分片都是独立乐器,既要保证局部精准,又要实现全局和谐。
好的分库分表方案不是技术参数的堆砌,而是对业务深刻理解后的架构表达。
来源:juejin.cn/post/7519688814395719714
如何将canvas动画导成一个视频?
引言
某一天我突然有个想法,我想用canvas做一个音频可视化的音谱,然后将这个音频导出成视频。
使用canvas实现音频可视化,使用ffmpeg导出视频与音频,看起来方案是可行的,技术也是可行的,说干就干,先写一个demo。
这里我使用vue来搭建项目
- 创建项目
vue create demo
- 安装ffmpeg插件
npm @ffmpeg/ffmpeg @ffmpeg/core
- 组件videoPlayer.vue
这里有个点需要注意:引用@ffmpeg/ffmpeg可能会报错
需要将node_modules中@ffmpeg文件下面的 - ffmpeg-core.js
- ffmpeg-core.wasm
- ffmpeg-core.worker.js
这三个文件复制到public文件下面 - 并且需要在vue。config.js中进行如下配置
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer:{
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
}
}
})
准备好这些后,下面是实现代码
<template>
<div class="wrap" v-loading="loading" element-loading-text="正在下载视频。。。">
<div>
<input type="file" @change="handleFileUpload" accept="audio/*" />
<button @click="playAudio">播放</button>
<button @click="pauseAudio">暂停</button>
</div>
<div class="canvas-wrap">
<canvas ref="canvas" id="canvas"></canvas>
</div>
</div>
</template>
<script>
import RainDrop from './rain'
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
export default {
name: 'canvasVideo',
data() {
return {
frames: [],
recording: false,
ffmpeg: null,
x: 0,
loading: false,
canvasCtx: null,
audioContext: null,
analyser: null,
bufferLength: null,
dataArray: null,
audioFile: null,
audioElement: null,
audioSource: null,
// 谱频个数
barCount: 64,
// 宽度
barWidth: 10,
marginLeft: 10,
player: false,
rainCount: 200,
rainDrops: [],
pausePng: null,
offscreenCanvas: null
};
},
mounted() {
this.ffmpeg = createFFmpeg({ log: true });
this.initFFmpeg();
},
methods: {
async initFFmpeg() {
await this.ffmpeg.load();
this.initCanvas()
},
startRecording() {
this.recording = true;
this.captureFrames();
},
stopRecording() {
this.recording = false;
this.exportVideo();
},
async captureFrames() {
const canvas = this.canvasCtx.canvas;
const imageData = canvas.toDataURL('image/png');
this.frames.push(imageData);
},
async exportVideo() {
this.loading = true
this.recording = false
const { ffmpeg } = this;
console.log('frames', this.frames)
try {
for (let i = 0; i < this.frames.length; i++) {
const frame = this.frames[i];
const frameData = await fetchFile(frame);
ffmpeg.FS('writeFile', `frame${i}.png`, frameData);
}
// 将音频文件写入 FFmpeg 文件系统
ffmpeg.FS('writeFile', 'audio.mp3', await fetchFile(this.audioFile));
// 使用 FFmpeg 将帧编码为视频
await ffmpeg.run(
'-framerate', '30', // 帧率 可以收费
'-i', 'frame%d.png', // 输入文件名格式
'-i', 'audio.mp3', // 输入音频
'-c:v', 'libx264', // 视频编码器
'-c:a', 'aac', // 音频编码器
'-pix_fmt', 'yuv420p', // 像素格式
'-vsync', 'vfr', // 同步视频和音频
'-shortest', // 使视频长度与音频一致
'output.mp4' // 输出文件名
);
const files = ffmpeg.FS('readdir', '/');
console.log('文件系统中的文件:', files);
const data = ffmpeg.FS('readFile', 'output.mp4');
const url = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
const a = document.createElement('a');
a.href = url;
a.download = 'output.mp4';
a.click();
} catch (e) {
console.log('eeee', e)
}
this.loading = false
},
initCanvas() {
const dom = document.getElementById('canvas');
this.canvasCtx = dom.getContext('2d');
const p = document.querySelector('.canvas-wrap')
console.log('p', p.offsetWidth)
this.canvasCtx.canvas.width = p.offsetWidth;
this.canvasCtx.canvas.height = p.offsetHeight;
console.log('canvasCtx', this.canvasCtx)
this.initAudioContext()
this.createRainDrops()
},
handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
this.audioFile = file
const fileURL = URL.createObjectURL(file);
this.loadAudio(fileURL);
}
},
loadAudio(url) {
this.audioElement = new Audio(url);
this.audioElement.addEventListener('error', (e) => {
console.error('音频加载失败:', e);
});
this.audioSource = this.audioContext.createMediaElementSource(this.audioElement);
this.audioSource.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
},
playAudio() {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume().then(() => {
console.log('AudioContext 已恢复');
this.audioElement.play();
this.player = true
this.draw();
});
} else {
this.audioElement.play().then(() => {
this.player = true
this.draw();
}).catch((error) => {
console.error('播放失败:', error);
});
}
},
pauseAudio() {
if (this.audioElement) {
this.audioElement.pause();
this.player = false
this.stopRecording()
}
},
initAudioContext() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
this.dataArray = new Uint8Array(this.barCount);
},
bar() {
let barHeight = 20;
const allBarWidth = this.barCount * this.barWidth + this.marginLeft * (this.barCount - 1)
const left = (this.canvasCtx.canvas.width - allBarWidth) / 2
let x = left
for (let i = 0; i < this.barCount; i++) {
barHeight = this.player ? this.dataArray[i] : 0
// console.log('barHeight', barHeight)
// 创建线性渐变
const gradient = this.canvasCtx.createLinearGradient(0, 0, this.canvasCtx.canvas.width, 0); // 从左到右渐变
gradient.addColorStop(0.2, '#fff'); // 起始颜色
gradient.addColorStop(0.5, '#ff5555');
gradient.addColorStop(0.8, '#fff'); // 结束颜色
// 设置阴影属性
this.canvasCtx.shadowColor = i <= 10 ? '#fff' : i > 54 ? '#fff' : '#ff5555';
this.canvasCtx.shadowBlur = 5;
this.canvasCtx.fillStyle = gradient;
this.canvasCtx.fillRect(x, this.canvasCtx.canvas.height - barHeight / 2 - 100, this.barWidth, barHeight / 2);
this.canvasCtx.shadowColor = i <= 10 ? '#fff' : i > 54 ? '#fff' : '#ff5555';
this.canvasCtx.shadowBlur = 5;
this.canvasCtx.beginPath();
this.canvasCtx.arc(x + 5, this.canvasCtx.canvas.height - barHeight / 2 - 99, 5, 0, Math.PI, true)
// this.canvasCtx.arc(x + 5, this.canvasCtx.canvas.height - barHeight / 2, 5, 0, Math.PI, false)
this.canvasCtx.closePath();
this.canvasCtx.fill()
this.canvasCtx.shadowColor = i <= 10 ? '#fff' : i > 54 ? '#fff' : '#ff5555';
this.canvasCtx.shadowBlur = 5;
this.canvasCtx.beginPath();
// this.canvasCtx.arc(x + 5, this.canvasCtx.canvas.height - barHeight / 2 - 100, 5, 0, Math.PI, true)
this.canvasCtx.arc(x + 5, this.canvasCtx.canvas.height - 100, 5, 0, Math.PI, false)
this.canvasCtx.closePath();
this.canvasCtx.fill()
x += this.barWidth + this.marginLeft;
}
},
draw() {
if (this.player) requestAnimationFrame(this.draw);
this.startRecording()
// 获取频谱数据
this.analyser.getByteFrequencyData(this.dataArray);
this.canvasCtx.fillStyle = 'rgb(0, 0, 0)';
this.canvasCtx.fillRect(0, 0, this.canvasCtx.canvas.width, this.canvasCtx.canvas.height); // 清除画布
this.bar()
this.rainDrops.forEach((drop) => {
drop.update();
drop.draw(this.canvasCtx);
});
},
// 创建雨滴对象
createRainDrops() {
for (let i = 0; i < this.rainCount; i++) {
this.rainDrops.push(new RainDrop(this.canvasCtx.canvas.width, this.canvasCtx.canvas.height, this.canvasCtx));
}
},
}
};
</script>
当选择好音频文件点击播放时如下图

点击暂停则可对已经播放过的音频时长进行视频录制下载

如果有什么其他问题欢迎在评论区交流
来源:juejin.cn/post/7521685642431053863
MCP简介:从浏览器截图的自动化说起
在当今 AI 飞速发展的时代,大型语言模型 (LLM) 如 Claude、ChatGPT 等已经在代码生成、内容创作等方面展现出惊人的能力。然而,这些强大的模型存在一个明显的局限性——它们通常与外部系统和工具隔离,无法直接访问或操作用户环境中的资源和工具。
而 Model Context Protocol (MCP) 的出现,正是为了解决这一问题。
什么是MCP?
Model Context Protocol (MCP) 是由 Anthropic 公司推出的一个开放协议,它标准化了应用程序如何向大型语言模型 (LLM) 提供上下文和工具的方式。我们可以将 MCP 理解为 AI 应用的"USB-C 接口"——就像 USB-C 为各种设备提供了标准化的连接方式,MCP 为 AI 模型提供了与不同数据源和工具连接的标准化方式。
简单来说,MCP可以做到以下事情:
- 读取和写入本地文件
- 查询数据库
- 执行命令行操作
- 控制浏览器
- 与第三方 API 交互
这极大地扩展了 AI 助手的能力边界,使其不再仅限于对话框内的文本交互。
MCP的架构

以上图片来源于 MCP 官方文档
MCP 的架构相对简单,主要包含两个核心组件:
- MCP 服务器 (Server):提供工具和资源的服务端,可以使用任何编程语言实现,只要能够通过
stdout/stdin或 HTTP 通信。 - MCP 客户端 (Client):使用 MCP 服务器提供的工具和资源的 AI 应用,如
Claude Desktop、Cursor编辑器等。
MCP 服务器向客户端提供两种主要能力:
- 工具 (Tools):可执行的函数,允许 AI 执行特定操作
- 资源 (Resources):提供给 AI 的上下文信息,如文件内容、数据库结构等
浏览器自动化:MCP的实际应用
为了更直观地理解 MCP 的强大之处,让我们看一个案例:使用 Playwright MCP 服务器进行浏览器自动化。
Playwright 是一个由 Microsoft 开发的浏览器自动化工具,可以控制 Chrome、Firefox、Safari 等主流浏览器。通过 Playwright MCP 服务器,我们可以让 AI 助手直接操作浏览器,执行各种任务。
先讲讲使用场景
- 博客写作。当我写博客时,我需要打开浏览器,打开目标网站,然后截图,并保存到本地特定的目录中,并在
markdown中引用图片地址。 - 端到端测试。当我需要测试网站时,我需要打开浏览器,打开目标网站,然后进行一些操作,比如填写表单、点击按钮等。就算有
Playwright的测试框架,但仍需要人工介入,比如自定义data-cy,浏览器操作一遍保存 playwright 的测试代码并扔给 cursor 生成测试。
场景一:博客写作的图片自动化
作为技术博主,我经常需要在文章中引用网站截图来说明问题或展示效果。在传统流程中,这个过程相当繁琐:
- 打开浏览器访问目标网站
- 使用截图工具截取所需区域
- 保存截图到特定目录
- 处理图片(可能需要裁剪、压缩等)
- 在
Markdown文件中手动添加图片链接 - 确认图片正确显示
这个过程不仅耗时,而且容易出错。使用 Playwright MCP,整个流程可以简化为:
请访问 https://tailwindcss.com,截取首页顶部导航栏区域,保存到 @public/images/ 下,并生成 markdown 图片引用代码
Cursor 通过 MCP 协议会:
- 自动打开网站
- 精确定位并截取导航栏元素
- 保存到指定目录
- 自动生成符合博客格式的图片引用代码
这不仅节省了时间,还保证了图片引用的一致性和准确性。对于需要多张截图的长篇技术文章,效率提升尤为显著。
更进阶的应用还包括:
- 自动为截图添加高亮或注释
- 对比同一网站在不同设备上的显示效果
- 跟踪网站的 UI 变化并自动更新文章中的截图
场景二:端到端测试的自动化
端到端测试是前端开发中的重要环节,但传统方式存在诸多痛点:
- 繁琐的测试编写:即使使用
Cypress等工具,编写测试脚本仍需要手动规划测试路径、定位元素、设计断言等 - 元素选择器维护:需要在代码中添加特定属性(如
data-cy)用于测试,且这些选择器需要随着 UI 变化而维护 - 测试代码与产品代码分离:测试逻辑往往与开发逻辑分离,导致测试更新滞后于功能更新
- 复杂交互流程难以模拟:多步骤的用户操作(如表单填写、多页面导航)需要精确编排
即便使用 Chrome 的 DevTools 的 Recorder 功能,也只能生成 Playwright 的测试代码,并且需要人工介入,比如自定义 data-cy,浏览器操作一遍保存 playwright 的测试代码并扔给 cursor 生成测试。
或者通过 cursor 与 recorder 提效后的环节:
- 让 cursor 在关键位置插入
data-cy属性 - 使用
Chrome DevTools的Recorder功能生成测试代码 - 将测试代码扔给 cursor 生成测试
而通过 Playwright MCP,开发者可以自然语言描述测试场景,让 Cursor 直接生成并执行测试:
用户:测试我的登录流程:访问 http://localhost:3000/login,使用测试账号 test@example.com 和密码 Test123!,验证登录成功后页面应跳转到仪表盘并显示欢迎信息
Cursor 会:
- 在必要位置插入
data-cy属性 - 自动访问登录页面
- 填写表单并提交
- 验证跳转和欢迎信息
- 报告测试结果
- 生成可复用的
Playwright测试代码
这种方式不仅降低了编写测试的门槛,还能根据测试结果智能调整测试策略。例如,如果登录按钮位置变化,Cursor 可以通过视觉识别重新定位元素,而不是简单地报告选择器失效。
对于快速迭代的项目尤其有价值:
- 在代码修改后立即验证功能完整性
- 快速生成回归测试套件
- 模拟复杂的用户行为路径
- 根据用户反馈自动创建针对性测试
这两个场景说明,MCP 不仅仅是连接 AI 与工具的技术桥梁,更是能够实质性改变开发者工作流程的革新力量。通过消除重复性工作,开发者可以将更多精力集中在创意和解决问题上。
示例:使用executeautomation/mcp-playwright
executeautomation/mcp-playwright 是一个基于 Playwright 的 MCP 服务器实现,它提供了一系列工具,使得 AI 助手能够:
- 打开网页
- 截取网页或元素截图
- 填写表单
- 点击按钮
- 提取网页内容
- 执行
JavaScript代码 - 等待网页加载或元素出现
下面以一个简单的场景为例:让 AI 助手打开一个网站并截图。
传统方式下,这个任务可能需要你:
- 安装
Playwright - 编写自动化脚本
- 配置环境
- 运行脚本
- 处理截图结果
而使用 MCP,整个过程可以简化为与 AI 助手的对话:
用户:请打开 Google 首页并截图
AI 助手:好的,我将为您打开 Google 首页并截图。
[AI 助手通过 MCP 控制浏览器,打开 google.com 并截图]
AI 助手:已成功截图,这是 Google 首页的截图。[显示截图]
整个过程中,用户不需要编写任何代码,AI 助手通过 MCP 服务器直接控制浏览器完成任务。
Playwright MCP 服务器的安装与配置
如果你想尝试使用 Playwright MCP 服务器,可以按照以下步骤进行设置:
- 使用
npm安装Playwright MCP服务器:
npm install -g @executeautomation/playwright-mcp-server
- 配置
Claude Desktop客户端(以 MacOS 为例):
编辑配置文件~/Library/Application\ Support/Claude/claude_desktop_config.json,添加以下内容:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}
- 重启
Claude客户端,你会看到一个新的 "Attach MCP" 按钮。 - 点击该按钮,选择
Playwright MCP服务器,现在你的 AI 助手就可以控制浏览器了!
在 Cursor 中使用 Playwright MCP
Cursor 是一款集成了 AI 能力的代码编辑器,它也支持 MCP 协议。我们可以在 Cursor 中配置 Playwright MCP 服务器,使 AI 助手能够在开发过程中直接操作浏览器。
配置步骤
- 首先确保已安装
Playwright MCP服务器:
npm install -g @executeautomation/playwright-mcp-server
- 在
Cursor中配置 MCP 服务器,有两种方式:
方式一:通过配置文件(推荐)
编辑
~/.cursor/mcp.json文件(如果不存在则创建),添加以下内容:
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["-y", "@executeautomation/playwright-mcp-server"]
}
}
}
方式二:通过项目配置
在项目根目录下创建
.cursor/mcp.json文件,内容同上。这样配置的 MCP 服务器只在当前项目中可用。 - 重启
Cursor编辑器,使配置生效。
使用场景示例
在 Cursor 中使用 Playwright MCP 可以大大提升前端开发和测试效率。以下是一些常见的使用场景:
- 快速页面测试:
在开发 Web 页面时,可以让 AI 助手直接打开页面,检查渲染效果,无需手动切换到浏览器。
用户:请打开我当前开发的页面 http://localhost:3000,检查响应式布局在移动设备上的显示效果
Cursor:[通过 Playwright MCP 打开页面并进行移动设备模拟,然后截图展示结果]
- 自动化截图对比:
在进行 UI 改动时,可以让 AI 助手截取改动前后的页面对比图。
用户:我刚修改了导航栏的样式,请打开 http://localhost:3000,截图并与 production 环境 myapp.com 的页面进行对比
Cursor:[使用 Playwright MCP 分别截取两个环境的页面,并进行对比分析]
- 交互测试:
让 AI 助手模拟用户交互,验证功能是否正常工作。
用户:请测试我的登录表单,打开 http://localhost:3000/login,使用测试账号填写表单并提交,检查是否成功登录
Cursor:[使用 Playwright MCP 打开页面,填写表单并提交,验证登录流程]
- 开发过程中的实时调试:
在编码过程中,可以让 AI 助手实时检查页面变化。
用户:我刚刚修改了 Button 组件的样式,请打开组件预览页面检查不同状态下的按钮外观
Cursor:[打开页面,截取不同状态的按钮截图,并分析样式是否符合预期]
通过这些场景,我们可以看到,Playwright MCP 在 Cursor 中的应用不仅简化了前端开发工作流,还提供了更直观的开发体验,让 AI 助手成为开发过程中的得力助手。
MCP 的优势与局限性
优势
- 扩展 AI 能力:让 AI 助手能够与外部系统交互,大大扩展其应用场景
- 标准化接口:提供统一的协议,降低 AI 工具集成的复杂度
- 安全可控:用户可以审核 AI 助手的操作请求,确保安全
- 灵活扩展:可以根据需要开发自定义 MCP 服务器
局限性
- 新兴技术:MCP 仍处于发展早期,协议可能会变化
- 远程开发限制:MCP 服务器需要在本地机器上运行,远程开发环境可能存在问题
- 资源支持:部分 MCP 客户端如
Cursor尚未支持resources/prompts功能
Cursor 的 MCP 支持限制:

未来展望
MCP 作为一种连接 AI 与外部系统的标准化协议,有着广阔的应用前景:
- 智能化开发工作流:AI 助手可以更深入地参与到开发流程中,自动化执行测试、部署等任务
- 数据分析与可视化:AI 助手可以直接访问数据库,生成分析报告和可视化结果
- 跨平台自动化:统一的协议使 AI 助手能够操作不同平台和工具
- 个性化智能助手:用户可以配置自己的 MCP 服务器,创建专属于自己工作流的 AI 助手
结语
Model Context Protocol (MCP) 正在打破 AI 助手与外部世界之间的壁垒,使 AI 能够更加深入地融入我们的工作流程。从浏览器自动化到代码编辑器集成,MCP 展示了 AI 与传统工具结合的强大潜力。
以前可以说,Cursor 虽然代码敲的好,但它不能直接操作浏览器,不能直接操作数据库,不能直接操作文件系统,开发这个流程还是需要我频繁接手的。
现在来说,需要我们接手的次数会越来越少。
最后再推荐两个 MCP 相关的资源:
参考资料
来源:juejin.cn/post/7481861001189621800
理解 devDependencies:它们真的不会被打包进生产代码吗?
在前端开发中,很多开发者都有一个常见误解:package.json 中的 devDependencies 是开发时依赖,因此不会被打包到最终的生产环境代码中。这个理解在一定条件下成立,但在真实项目中,打包工具(如 Vite、Webpack 等)并不会根据 devDependencies 或 dependencies 的位置来决定是否将依赖打包到最终的 bundle 中,而是完全俗义于代码中是否引用了这些模块。
本文将通过一个实际例子来说明这个问题,并提出一些实践建议来避免误用。
一、dependencies vs devDependencies 回顾
在 package.json 中,我们通常会看到两个依赖字段:
{
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"vite": "^5.0.0"
}
}
dependencies:运行时依赖,通常用于项目在生产环境中运行所需的库。devDependencies:开发时依赖,通常用于构建、测试、打包等过程,比如 Babel、ESLint、Vite 等。
很多人认为把某个库放到 devDependencies 中就意味着它不会被打包进最终代码,但这只是约定俗成,并非构建工具的实际行为。
二、一个实际例子:lodash 被错误地放入 devDependencies
我们以一个使用 Vite 构建的库包为例:
目录结构:
my-lib/
├── src/
│ └── index.ts
├── package.json
├── vite.config.ts
└── tsconfig.json
src/index.ts
import _ from 'lodash';
export function capitalizeName(name: string) {
return _.capitalize(name);
}
错误的 package.json
{
"name": "my-lib",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"scripts": {
"build": "vite build"
},
"devDependencies": {
"vite": "^5.0.0",
"lodash": "^4.17.21",
"typescript": "^5.4.0"
}
}
注意:lodash 被放到了 devDependencies 中,而不是 dependencies中。
构建后结果:
执行 npm run build 后,你会发现 lodash 的代码被打包进了最终输出的 bundle 中,尽管它被标记为 devDependencies。
dist/
├── index.js ← 包含 lodash 的代码
├── index.mjs
└── index.d.ts
三、为什么会发生这种情况?
构建工具(如 Vite、Webpack)在处理打包时,并不会关心某个依赖是 dependencies 还是 devDependencies。
它只会扫描你的代码:
- 如果你
import了某个模块(如lodash),构建工具会把它包含进 bundle 中,除非你通过external配置显式告诉它不要打包进来。 - 你放在
devDependencies中只是告诉 npm install:这个依赖只在开发阶段需要,npm install --production时不会安装它。
换句话说,打包行为取决于代码,而不是依赖声明。
四、修复方式:将运行时依赖移到 dependencies
为了正确构建一个可以发布的库包,应该:
{
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"vite": "^5.0.0",
"typescript": "^5.4.0"
}
}
这样使用你库的开发者才能在安装你的包时自动获取 lodash。
五、如何防止此类问题?
1. 使用 peerDependencies(推荐给库开发者)
如果你希望使用者自带 lodash,而不是你来打包它,可以这样配置:
{
"peerDependencies": {
"lodash": "^4.17.21"
}
}
同时在 Vite 配置中加上:
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
name: 'MyLib'
},
rollupOptions: {
external: ['lodash'], // 不打包 lodash
}
}
})
这样打包出来的 bundle 中就不会再包含 lodash 的代码。
2. 使用构建工具的 external 配置
像上面这样将 lodash 标为 external 可以避免误打包。
3. 静态分析工具检测
使用像 depcheck 或 eslint-plugin-import 等工具,可以帮你发现未声明或声明错误的依赖。
六、总结
| 依赖位置 | 作用说明 |
|---|---|
dependencies | 生产环境运行时必须使用的库 |
devDependencies | 开发、构建过程所需的工具库 |
peerDependencies | 你的库需要,但由使用者提供的依赖(库开发推荐) |
构建工具不会参考 package.json 中依赖的位置来决定是否打包,而是基于代码的实际引用。作为库作者,你应该确保:
- 所有运行时依赖都放在
dependencies或peerDependencies; - 构建工具正确配置 external,避免不必要地打包外部依赖;
- 使用工具检查依赖定义的一致性。
来源:juejin.cn/post/7530180739729555491
使用three.js搭建3d隧道监测-2



加载基础线条与地面效果



在我们的隧道监控系统中,地面网格和方向指示器是重要的视觉元素,它们帮助用户理解空间关系和导航方向。
1. 网格地面的创建与优化
javascript
// 初始化场景中的地面
const addGround = () => {
const size = 40000; // 网格大小
const divisions = 100; // 分割数(越高越密集)
// 主网格线颜色(亮蓝色)
const color1 = 0x6E7DB9; // 蓝色
// 次网格线颜色(深蓝色)
const color2 = 0x282C3C; // 深蓝色
const gridHelper = new THREE.GridHelper(size, divisions, color1, color2);
// 调整网格线的透明度和材质
gridHelper.material.opacity = 1;
gridHelper.material.transparent = true;
gridHelper.material.depthWrite = false; // 防止网格阻挡其他物体的渲染
// 设置材质的混合模式以实现发光效果
gridHelper.material.blending = THREE.AdditiveBlending;
gridHelper.material.vertexColors = false;
// 增强线条对比度
gridHelper.material.color.setHex(color1);
gridHelper.material.linewidth = 100;
// 旋转网格,使其位于水平面
gridHelper.rotation.x = Math.PI;
sceneRef.current.add(gridHelper);
};
知识点: Three.js 中的网格地面实现技术
- GridHelper:Three.js 提供的辅助对象,用于创建二维网格,常用于表示地面或参考平面
- 材质优化:通过设置
depthWrite = false避免渲染排序问题,防止网格阻挡其他物体
- 混合模式:
AdditiveBlending混合模式使重叠线条颜色叠加,产生发光效果
- 性能考量:网格分割数(divisions)会影响性能,需要在视觉效果和性能间平衡
- 旋转技巧:通过
rotation.x = Math.PI将默认垂直的网格旋转到水平面
这种科幻风格的网格地面在虚拟现实、数据可视化和游戏中非常常见,能够提供空间参考而不显得过于突兀。
2. 动态方向指示器的实现
javascript
const createPolygonRoadIndicators = (dis) => {
const routeIndicationGeometry = new THREE.PlaneGeometry(3024, 4000); // 创建平面几何体
// 创建文本纹理的辅助函数
const getTextCanvas = (text) => {
const width = 200;
const height = 300;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.font = "bold 40px Arial"; // 设置字体大小和样式
ctx.fillStyle = '#949292'; // 设置字体颜色
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, width / 2, height / 2);
return canvas;
};
// 创建方向1文本平面
const textMap = new THREE.CanvasTexture(getTextCanvas('方向1'));
const textMaterial = new THREE.MeshBasicMaterial({
map: textMap,
transparent: true,
depthTest: false
});
const plane = new THREE.Mesh(routeIndicationGeometry, textMaterial);
plane.castShadow = false;
plane.position.set(1024, 0, 1400);
plane.rotateX(-Math.PI / 2);
// 创建方向2文本平面
const textMap1 = new THREE.CanvasTexture(getTextCanvas('方向2'));
const textMaterial1 = new THREE.MeshBasicMaterial({
map: textMap1,
transparent: true,
depthTest: false
});
const plane1 = new THREE.Mesh(routeIndicationGeometry, textMaterial1);
plane1.castShadow = false;
plane1.position.set(1024, 0, -1400);
plane1.rotateX(-Math.PI / 2);
// 创建箭头指示器
const loader = new THREE.TextureLoader();
const texture = loader.load('/image/arrow1.png', (t) => {
t.wrapS = t.wrapT = THREE.RepeatWrapping;
t.repeat.set(1, 1);
});
const geometryRoute = new THREE.PlaneGeometry(1024, 1200);
const materialRoute = new THREE.MeshStandardMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide, // 确保可以从两个面看见
});
const plane2 = new THREE.Mesh(geometryRoute, materialRoute);
plane2.receiveShadow = false;
plane2.position.set(1000, 0, 0);
plane2.rotateX(dis==="left"?-Math.PI / 2:Math.PI / 2);
// 将所有元素组合成一个组
const group = new THREE.Gr0up();
group.add(plane2, plane, plane1);
group.scale.set(0.4, 0.4, 0.4);
group.position.set(dis==="left"?500:500-4000, 0, 0);
return group;
};
知识点: Three.js 中的动态文本与指示器实现技术
- Canvas 纹理:使用 HTML Canvas 动态生成文本,然后转换为 Three.js 纹理,这是在 3D 场景中显示文本的高效方法
- CanvasTexture:Three.js 提供的特殊纹理类型,可以直接从 Canvas 元素创建纹理,支持动态更新
- 透明度处理:通过设置
transparent: true和适当的depthTest设置解决透明纹理的渲染问题
- 几何体组织:使用
THREE.Gr0up将多个相关的 3D 对象组织在一起,便于统一变换和管理
- 条件旋转:根据参数
dis动态决定箭头的朝向,实现可配置的方向指示
- 纹理重复:通过
RepeatWrapping和repeat设置可以控制纹理的重复方式,适用于创建连续的纹理效果
这种动态方向指示器在导航系统、虚拟导览和交互式地图中非常有用,可以为用户提供直观的方向引导。
3.地面方向指示器实现
在隧道监控系统中,方向指示是帮助用户理解空间方向和导航的关键元素。我们实现了一套包含文本标签和箭头的地面方向指示系统。
javascript
import * as THREE from "three";
const createPolygonRoadIndicators = (dis) => {
const routeIndicationGeometry = new THREE.PlaneGeometry(3024, 4000); // 创建平面几何体
const getTextCanvas = (text) => {
const width = 200;
const height = 300;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.font = "bold 40px Arial"; // 设置字体大小和样式
ctx.fillStyle = '#949292'; // 设置字体颜色
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, width / 2, height / 2);
return canvas;
};
const textMap = new THREE.CanvasTexture(getTextCanvas('方向1'));
const textMaterial = new THREE.MeshBasicMaterial({ map: textMap, transparent: true, depthTest: false }); // 创建材质,depthTest解决黑色块问题
const plane = new THREE.Mesh(routeIndicationGeometry, textMaterial);
plane.castShadow = false; // 不投影阴影"
plane.position.set(1024, 0, 1400);
plane.rotateX(-Math.PI / 2);
const textMap1 = new THREE.CanvasTexture(getTextCanvas('方向2'));
const textMaterial1 = new THREE.MeshBasicMaterial({ map: textMap1, transparent: true, depthTest: false }); // 创建材质,depthTest解决黑色块问题
const plane1 = new THREE.Mesh(routeIndicationGeometry, textMaterial1);
plane1.castShadow = false; // 不投影阴影
plane1.position.set(1024, 0, -1400);
plane1.rotateX(-Math.PI / 2);
const loader = new THREE.TextureLoader();
const texture = loader.load('/image/arrow1.png', (t) => {
t.wrapS = t.wrapT = THREE.RepeatWrapping;
t.repeat.set(1, 1);
});
const geometryRoute = new THREE.PlaneGeometry(1024, 1200);
const materialRoute = new THREE.MeshStandardMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide, // 确保可以从两个面看见
});
const plane2 = new THREE.Mesh(geometryRoute, materialRoute);
plane2.receiveShadow = false; // 不接收阴影
plane2.position.set(1000, 0, 0);
plane2.rotateX(dis==="left"?-Math.PI / 2:Math.PI / 2);
const group = new THREE.Gr0up();
group.add(plane2, plane, plane1);
group.scale.set(0.4, 0.4, 0.4);
group.position.set(dis==="left"?500:500-4000, 0, 0);
return group;
};
export default createPolygonRoadIndicators;
知识点: Three.js 中的地面方向指示器实现技术
- 平面投影标记:使用
PlaneGeometry创建平面,通过旋转使其平行于地面,形成"地面投影"效果
- 使用
rotateX(-Math.PI / 2)将平面从垂直旋转到水平位置
- 动态文本生成:使用 Canvas API 动态生成文本纹理
getTextCanvas函数创建一个临时 Canvas 并在其上绘制文本
- 使用
CanvasTexture将 Canvas 转换为 Three.js 可用的纹理
- 这种方法比使用 3D 文本几何体更高效,特别是对于频繁变化的文本
- 纹理渲染优化:
transparent: true启用透明度处理,使背景透明
depthTest: false禁用深度测试,解决半透明纹理的渲染问题,防止出现"黑色块"
castShadow: false和receiveShadow: false避免不必要的阴影计算
- 方向性指示:使用箭头纹理创建明确的方向指示
- 通过
TextureLoader加载外部箭头图像
- 根据
dis参数动态调整箭头方向(rotateX(dis==="left"?-Math.PI / 2:Math.PI / 2))
side: THREE.DoubleSide确保从任何角度都能看到箭头
- 组织与缩放:
- 使用
THREE.Gr0up将相关元素(文本标签和箭头)组织在一起
- 通过
group.scale.set(0.4, 0.4, 0.4)统一调整组内所有元素的大小
- 根据方向参数设置整个组的位置,实现左右两侧不同的指示效果
- 纹理重复设置:
RepeatWrapping和repeat.set(1, 1)控制纹理的重复方式
- 这为创建连续的纹理效果提供了基础,虽然本例中设为1(不重复)
这种地面方向指示系统在大型空间(如隧道、机场、展馆)的导航中特别有用,为用户提供直观的方向感,不会干扰主要视觉元素。
隧道指示牌制作

在隧道监控系统中,指示牌是引导用户和提供空间信息的重要元素。我们实现了一种复合结构的隧道指示牌,包含支柱、横梁和信息板。
javascript
import * as THREE from 'three';
import {TextGeometry} from "three/examples/jsm/geometries/TextGeometry";
/**
* 创建石头柱子(竖直 + 横向)
* @returns {THREE.Gr0up} - 返回包含柱子和横梁的组
*/
const createStonePillar = () => {
const pillarGr0up = new THREE.Gr0up();
// 创建六边形的竖直柱子
const pillarGeometry = new THREE.CylinderGeometry(6, 6, 340, 6); // 直径12, 高度340, 六边形柱体
const pillarMaterial = new THREE.MeshStandardMaterial({color: 0x808080}); // 石头颜色
const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial);
pillar.position.set(0, 0, 0);
// 创建第一根横向长方体
const beam1Geometry = new THREE.BoxGeometry(100, 10, 0.1);
const beam1Material = new THREE.MeshStandardMaterial({color: 0x808080});
const beam1 = new THREE.Mesh(beam1Geometry, beam1Material);
beam1.position.set(-50, 150, 0);
// 创建第二根横向长方体
const beam2Geometry = new THREE.BoxGeometry(100, 10, 0.1);
const beam2Material = new THREE.MeshStandardMaterial({color: 0x808080});
const beam2 = new THREE.Mesh(beam2Geometry, beam2Material);
beam2.position.set(-50, 130, 0);
// 将柱子和横梁添加到组
pillarGr0up.add(pillar);
pillarGr0up.add(beam1);
pillarGr0up.add(beam2);
return pillarGr0up;
};
/**
* 创建一个用于绘制文本的 Canvas
* @param {string} text - 要绘制的文本
* @returns {HTMLCanvasElement} - 返回 Canvas 元素
*/
const getTextCanvas = (text) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 设置 Canvas 尺寸
const fontSize = 32;
canvas.width = 512;
canvas.height = 128;
// 设置背景色
context.fillStyle = '#1E3E9A'; // 蓝底
context.fillRect(0, 0, canvas.width, canvas.height);
// 设置文本样式
context.font = `${fontSize}px Arial`;
context.fillStyle = '#ffffff'; // 白色文本
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText(text, canvas.width / 2, canvas.height / 2);
return canvas;
};
/**
* 创建交通指示牌并添加到场景中
* @param {Object} sceneRef - React ref 对象,指向 Three.js 的场景
* @returns {Promise<THREE.Gr0up>} - 返回创建的指示牌组
*/
export default (sceneRef, png, dis) => {
const createSignBoard = async () => {
const signGr0up = new THREE.Gr0up();
const loader = new THREE.TextureLoader();
loader.load(png, texture => {
// 创建一个平面作为标志背景
const signGeometry = new THREE.PlaneGeometry(100, 50); // 宽100,高50
texture.encoding = THREE.sRGBEncoding // 设置纹理的颜色空间
texture.colorSpace = THREE.SRGBColorSpace;
const signMaterial = new THREE.MeshStandardMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide,
})
const sign = new THREE.Mesh(signGeometry, signMaterial);
sign.position.set(-60, 140, 0.3)
signGr0up.add(sign);
})
// 创建并添加石头柱子
const pillar = createStonePillar();
signGr0up.add(pillar);
if (dis == "left") {
signGr0up.position.set(370, 180, 3750); // 左侧位置
} else {
signGr0up.rotateY(Math.PI); // 旋转180度
signGr0up.position.set(-370 - 2000, 180, 3450 - 7200); // 右侧位置
}
signGr0up.add(pillar);
sceneRef.current.add(signGr0up);
return signGr0up; // 返回整个组
};
// 调用创建指示牌函数
return createSignBoard().then((signGr0up) => {
console.log('交通指示牌创建完成:', signGr0up);
return signGr0up;
});
};
知识点: Three.js 中的复合结构与指示牌实现技术
- 模块化设计:将指示牌分解为柱子、横梁和信息板三个主要组件,便于维护和复用
- 几何体组合:使用简单几何体(圆柱体、长方体、平面)组合构建复杂结构
CylinderGeometry创建六边形柱体作为支撑
BoxGeometry创建横向支撑梁
PlaneGeometry创建平面显示信息
- 空间层次:使用
THREE.Gr0up将相关元素组织在一起,便于整体变换和管理
- 纹理映射:使用
TextureLoader加载外部图像作为指示牌内容
- 设置
colorSpace = THREE.SRGBColorSpace确保颜色正确显示
- 使用
side: THREE.DoubleSide使平面从两面都可见
- 条件定位:根据
dis参数动态决定指示牌的位置和朝向
- 使用
rotateY(Math.PI)旋转180度实现方向反转
- Canvas 动态文本:使用
getTextCanvas函数创建动态文本纹理
- 可以方便地生成不同内容和样式的文本标识
- 异步处理:使用 Promise 处理纹理加载的异步过程,确保资源正确加载
- 返回 Promise 使调用者可以在指示牌创建完成后执行后续操作
这种组合式设计方法允许我们创建高度可定制的指示牌,适用于隧道、道路、建筑内部等多种场景,同时保持代码的可维护性和可扩展性。
多渲染器协同工作机制
在我们的项目中,实现了 WebGL 渲染器、CSS2D 渲染器和 CSS3D 渲染器的协同工作:
const initRenderer = () => {
// WebGL 渲染器
rendererRef.current = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
logarithmicDepthBuffer: true
});
rendererRef.current.setSize(window.innerWidth, window.innerHeight);
rendererRef.current.setPixelRatio(Math.min(window.devicePixelRatio, 2));
rendererRef.current.shadowMap.enabled = true;
rendererRef.current.shadowMap.type = THREE.PCFSoftShadowMap;
rendererRef.current.outputEncoding = THREE.sRGBEncoding;
rendererRef.current.toneMapping = THREE.ACESFilmicToneMapping;
containerRef.current.appendChild(rendererRef.current.domElement);
};
const initCSS2DScene = () => {
// CSS2D 渲染器
css2DRendererRef.current = new CSS2DRenderer();
css2DRendererRef.current.setSize(window.innerWidth, window.innerHeight);
css2DRendererRef.current.domElement.style.position = 'absolute';
css2DRendererRef.current.domElement.style.top = '0';
css2DRendererRef.current.domElement.style.pointerEvents = 'none';
containerRef.current.appendChild(css2DRendererRef.current.domElement);
};
const initCSS3DScene = () => {
// 初始化 CSS3DRenderer
css3DRendererRef.current = new CSS3DRenderer();
css3DRendererRef.current.setSize(sizes.width, sizes.height);
css3DRendererRef.current.domElement.style.position = 'absolute';
css3DRendererRef.current.domElement.style.top = '0px';
css3DRendererRef.current.domElement.style.pointerEvents = 'none'; // 确保CSS3D元素不阻碍鼠标事件
containerRef.current.appendChild(css3DRendererRef.current.domElement);
};
知识点: Three.js 支持多种渲染器同时工作,每种渲染器有不同的优势:
- WebGLRenderer:利用 GPU 加速渲染 3D 内容,性能最佳
- CSS2DRenderer:将 HTML 元素作为 2D 标签渲染在 3D 空间中,适合信息标签
- CSS3DRenderer:将 HTML 元素转换为 3D 对象,支持 3D 变换,适合复杂 UI
多渲染器协同可以充分发挥各自优势,实现复杂的混合现实效果。
后期处理管线设计
项目中实现了基于 EffectComposer 的后期处理管线:
const initPostProcessing = () => {
composerRef.current = new EffectComposer(rendererRef.current);
// 基础渲染通道
const renderPass = new RenderPass(sceneRef.current, cameraRef.current);
composerRef.current.addPass(renderPass);
// 环境光遮蔽通道
const ssaoPass = new SSAOPass(
sceneRef.current,
cameraRef.current,
window.innerWidth,
window.innerHeight
);
ssaoPass.kernelRadius = 16;
ssaoPass.minDistance = 0.005;
ssaoPass.maxDistance = 0.1;
composerRef.current.addPass(ssaoPass);
// 抗锯齿通道
const fxaaPass = new ShaderPass(FXAAShader);
const pixelRatio = rendererRef.current.getPixelRatio();
fxaaPass.material.uniforms['resolution'].value.x = 1 / (window.innerWidth * pixelRatio);
fxaaPass.material.uniforms['resolution'].value.y = 1 / (window.innerHeight * pixelRatio);
composerRef.current.addPass(fxaaPass);
};
知识点: 后期处理(Post-processing)是一种在 3D 场景渲染完成后对图像进行额外处理的技术:
- EffectComposer:Three.js 中的后期处理管理器,可以将多个处理效果组合在一起
- RenderPass:基础渲染通道,将场景渲染到目标缓冲区
- SSAOPass:屏幕空间环境光遮蔽,增强场景深度感和真实感
- FXAAShader:快速近似抗锯齿,提高图像质量
后期处理可以大幅提升画面质量,添加如景深、发光、色彩校正等专业效果。
多层次动画系统
项目实现了一个多层次的动画系统:
// 骨骼动画控制
const getActions = (animations, model) => {
const mixer = new THREE.AnimationMixer(model);
const mixerArray = [];
mixerArray.push(mixer);
const actions = {};
animations.forEach((clip) => {
const action = mixer.clipAction(clip);
actions[clip.name] = action;
});
return {actions, mixerArray};
};
// 动画播放控制
const playActiveAction = (actions, name, startTime = true, loopType = THREE.LoopOnce, clampWhenFinished = true) => {
const action = actions[name];
if (!action) return;
action.reset();
action.clampWhenFinished = clampWhenFinished;
action.setLoop(loopType);
if (startTime) {
action.play();
}
};
知识点: Three.js 提供了多种动画技术:
- AnimationMixer:用于播放和控制模型骨骼动画的核心类,相当于动画播放器
- AnimationClip:包含一组关键帧轨道的动画数据,如"走路"、"跑步"等动作
- AnimationAction:控制单个动画的播放状态,包括播放、暂停、循环设置等
- 动画混合:可以实现多个动画之间的平滑过渡,如从走路切换到跑步
合理使用这些技术可以创建流畅、自然的角色动画和场景变换。
第一人称视角控制算法
项目实现了一种先进的第一人称视角控制算法:
const animate1 = () => {
requestRef1.current = requestAnimationFrame(animate1);
if (isFirstPerson && robotRef.current) {
// 获取机器人的世界坐标
const robotWorldPosition = new THREE.Vector3();
robotRef.current.getWorldPosition(robotWorldPosition);
// 计算摄像机位置偏移
const offset = new THREE.Vector3(0, 140, 20);
// 获取机器人的前方方向向量
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(robotRef.current.quaternion);
const lookAheadDistance = 150;
// 计算摄像头位置和视线目标
const targetCameraPosition = robotWorldPosition.clone().add(offset);
const lookAtPosition = robotWorldPosition.clone().add(forward.multiplyScalar(lookAheadDistance));
// 使用 TWEEN 实现平滑过渡
cameraTweenRef.current = new TWEEN.Tween(cameraRef.current.position)
.to({
x: targetCameraPosition.x,
y: targetCameraPosition.y,
z: targetCameraPosition.z,
}, 1000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate(() => {
cameraRef.current.lookAt(lookAtPosition);
controlsRef.current.target.set(lookAtPosition.x, lookAtPosition.y, lookAtPosition.z);
})
.start();
}
};
知识点: 第一人称相机控制涉及多个关键技术:
- 世界坐标计算:通过
getWorldPosition()获取对象在世界坐标系中的位置
- 四元数旋转:使用
applyQuaternion()将向量按对象的旋转方向进行变换
- 向量运算:通过向量加法和标量乘法计算相机位置和视线方向
- 平滑过渡:使用 TWEEN.js 实现相机位置的平滑变化,避免生硬的跳变
- lookAt:让相机始终"看着"目标点,实现跟随效果
这种技术常用于第一人称游戏、虚拟导览等应用。
递归资源释放算法
项目实现了一种递归资源释放算法,用于彻底清理 Three.js 资源:
const disposeSceneObjects = (object) => {
if (!object) return;
// 递归清理子对象
while (object.children.length > 0) {
const child = object.children[0];
disposeSceneObjects(child);
object.remove(child);
}
// 清理几何体
if (object.geometry) {
object.geometry.dispose();
}
// 清理材质
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(material => disposeMaterial(material));
} else {
disposeMaterial(object.material);
}
}
// 清理纹理
if (object.texture) {
object.texture.dispose();
}
};
// 清理材质的辅助函数
const disposeMaterial = (material) => {
if (!material) return;
// 清理所有纹理属性
const textureProperties = [
'map', 'normalMap', 'roughnessMap', 'metalnessMap',
'emissiveMap', 'bumpMap', 'displacementMap',
'alphaMap', 'lightMap', 'aoMap', 'envMap'
];
textureProperties.forEach(prop => {
if (material[prop] && material[prop].dispose) {
material[prop].dispose();
}
});
material.dispose();
};
知识点: WebGL 资源管理是 3D 应用开发中的关键挑战:
- JavaScript 垃圾回收的局限性:虽然 JS 有自动垃圾回收,但 WebGL 资源(如纹理、缓冲区)需要手动释放
- 深度优先遍历:通过递归算法遍历整个场景图,确保所有对象都被正确处理
- 资源类型处理:不同类型的资源(几何体、材质、纹理)需要不同的释放方法
- 内存泄漏防护:不正确的资源管理是 WebGL 应用中最常见的内存泄漏原因
合理的资源释放策略对长时间运行的 3D 应用至关重要,可以避免性能下降和浏览器崩溃。
资源预加载与缓存策略
项目实现了资源预加载与缓存策略:
// 资源管理器
const ResourceManager = {
// 资源缓存
cache: new Map(),
// 预加载资源
preload: async (resources) => {
const loader = new GLTFLoader();
// 并行加载所有资源
const loadPromises = resources.map(resource => {
return new Promise((resolve, reject) => {
loader.load(
resource.url,
(gltf) => {
ResourceManager.cache.set(resource.id, {
data: gltf,
lastUsed: Date.now(),
refCount: 0
});
resolve(gltf);
},
undefined,
reject
);
});
});
return Promise.all(loadPromises);
},
// 获取资源
get: (id) => {
const resource = ResourceManager.cache.get(id);
if (resource) {
resource.lastUsed = Date.now();
resource.refCount++;
return resource.data;
}
return null;
},
// 释放资源
release: (id) => {
const resource = ResourceManager.cache.get(id);
if (resource) {
resource.refCount--;
if (resource.refCount <= 0) {
// 可以选择立即释放或稍后由缓存清理机制释放
}
}
}
};
知识点: 3D 应用中的资源管理策略:
- 预加载:提前加载关键资源,减少用户等待时间
- 并行加载:使用 Promise.all 并行加载多个资源,提高加载效率
- 资源缓存:使用 Map 数据结构存储已加载资源,避免重复加载
- 引用计数:跟踪资源的使用情况,只有当引用计数为零时才考虑释放
- 最近使用时间:记录资源最后使用时间,可用于实现 LRU (最近最少使用) 缓存策略
这种资源管理策略可以平衡内存使用和加载性能,适用于资源密集型的 3D 应用。
总结
通过这个隧道监控可视化系统的开发,我们深入实践了 Three.js 的多项高级技术,包括多渲染器协同、后期处理、动画系统、相机控制和资源管理等。这些技术不仅适用于隧道监控,还可以应用于数字孪生、产品可视化、教育培训等多个领域。
希望这次分享对大家了解 Web 3D 开发有所帮助!如有任何问题或改进建议,非常欢迎与我交流讨论。我将在后续分享中带来更多 Three.js 开发的实用技巧和最佳实践。
来源:juejin.cn/post/7540129382540247103
前端如何判断用户设备
在前端开发中,判断用户设备类型是常见需求,可通过浏览器环境检测、设备能力特征分析等方式实现。以下是具体实现思路及代码示例:
一、通过User-Agent检测设备类型
原理:User-Agent是浏览器发送给服务器的标识字符串,包含设备、系统、浏览器等信息。
实现步骤:
- 提取
navigator.userAgent字符串 - 通过正则表达式匹配特征关键词
// 设备检测工具函数
function detectDevice() {
const userAgent = navigator.userAgent.toLowerCase();
const device = {};
// 判断是否为移动设备
const isMobile = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
device.isMobile = isMobile;
// 具体设备类型
if (/(iphone|ipad|ipod)/i.test(userAgent)) {
device.type = 'ios';
device.model = /iphone/i.test(userAgent) ? 'iPhone' : 'iPad';
} else if (/android/i.test(userAgent)) {
device.type = 'android';
// 提取Android版本
const androidVersion = userAgent.match(/android (\d+\.\d+)/);
device.version = androidVersion ? androidVersion[1] : '未知';
} else if (/windows phone/i.test(userAgent)) {
device.type = 'windows phone';
} else if (/macint0sh/i.test(userAgent)) {
device.type = 'mac';
} else if (/windows/i.test(userAgent)) {
device.type = 'windows';
} else {
device.type = '其他';
}
// 判断是否为平板(需结合屏幕尺寸进一步确认)
device.isTablet = (/(ipad|android tablet|windows phone 8.1|kindle|nexus 7)/i.test(userAgent)) && !device.isMobile;
// 浏览器类型
if (/chrome/i.test(userAgent)) {
device.browser = 'Chrome';
} else if (/firefox/i.test(userAgent)) {
device.browser = 'Firefox';
} else if (/safari/i.test(userAgent) && !/chrome/i.test(userAgent)) {
device.browser = 'Safari';
} else if (/msie|trident/i.test(userAgent)) {
device.browser = 'IE/Edge';
} else {
device.browser = '未知';
}
return device;
}
// 使用示例
const deviceInfo = detectDevice();
console.log('设备类型:', deviceInfo.type);
console.log('是否为移动设备:', deviceInfo.isMobile);
console.log('浏览器:', deviceInfo.browser);
二、结合屏幕尺寸与触摸事件检测
原理:移动设备通常屏幕较小,且支持触摸操作,而PC设备以鼠标操作为主。
function enhanceDeviceDetection() {
const device = detectDevice(); // 基于User-Agent的检测
// 1. 屏幕尺寸检测(响应式设备类型)
if (window.innerWidth <= 768) {
device.layout = 'mobile'; // 移动端布局
} else if (window.innerWidth <= 1024) {
device.layout = 'tablet'; // 平板布局
} else {
device.layout = 'desktop'; // 桌面端布局
}
// 2. 触摸事件支持检测
device.hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// 3. 指针类型检测(WebKit特有属性,判断鼠标/触摸/笔)
if (navigator.maxTouchPoints === 0) {
device.pointerType = 'mouse';
} else if (navigator.maxTouchPoints > 2) {
device.pointerType = 'pen';
} else {
device.pointerType = 'touch';
}
return device;
}
三、设备能力API检测(更准确的现代方案)
原理:通过浏览器原生API获取设备硬件特性,避免User-Agent被伪造的问题。
async function detectDeviceByAPI() {
const device = {};
// 1. NavigatorDevice API(需HTTPS环境)
if (navigator.device) {
try {
const deviceInfo = await navigator.device.getCapabilities();
device.brand = deviceInfo.brand; // 设备品牌
device.model = deviceInfo.model; // 设备型号
device.vendor = deviceInfo.vendor; // 厂商
} catch (error) {
console.log('NavigatorDevice API获取失败:', error);
}
}
// 2. 屏幕像素密度(区分高清屏)
device.retina = window.devicePixelRatio >= 2;
// 3. 电池状态(移动端常用)
if (navigator.getBattery) {
navigator.getBattery().then(battery => {
device.batteryLevel = battery.level;
device.batteryCharging = battery.charging;
});
}
return device;
}
四、框架/库方案(简化实现)
如果项目中使用框架,可直接使用成熟库:
- react-device-detect(React专用)
- mobile-detect.js(轻量级通用库)
- ua-parser-js(专业User-Agent解析库)
五、注意事项
- User-Agent不可靠:用户可手动修改UA,或某些浏览器(如微信内置浏览器)会伪装UA。
- 结合多种检测方式:建议同时使用User-Agent、屏幕尺寸、触摸事件等多重检测,提高准确性。
- 响应式设计优先:现代开发中更推荐通过CSS媒体查询(
@media)实现响应式布局,而非完全依赖设备检测。 - 性能优化:避免频繁检测设备,可在页面加载时缓存检测结果。
六、面试延伸问题
- 为什么User-Agent检测不可靠?请举例说明。
- 在iOS和Android上,如何区分手机和平板?
- 如果用户强制旋转屏幕(如手机横屏),设备检测结果需要更新吗?如何处理?
通过以上方案,可全面检测用户设备类型、系统、浏览器及硬件特性,为前端适配提供依据。
来源:juejin.cn/post/7515378780371501082
我用Python写了个实时板块资金热力图 🎨💰
我用Python写了个实时板块资金热力图 🎨💰

大家好,这里是花姐,今天带来一个有点“热辣滚烫”的Python实战项目——实时板块资金热力图!🔥
这两年,股市的热度时高时低,但大家对资金流向的关注度始终不减。有没有办法直观地看到哪些板块在吸金,哪些板块在被资金抛弃呢?答案是:当然有!
于是,我撸起袖子,用 Python + Streamlit + AkShare + Plotly 搞了一款实时的资金流向可视化工具,颜值爆表,还能自动刷新,堪称炒股助手!🎯

📌 需求分析
在金融市场里,资金流向是一个很重要的指标,主力资金的流入流出往往决定了一个板块的短期走势。
我们希望做到:
✅ 实时获取资金流向数据,并展示行业板块的资金进出情况。
✅ 可视化呈现数据,用颜色区分资金净流入和净流出,一眼就能看出哪些板块是**“香饽饽”,哪些是“弃儿”**。
✅ 自动刷新,让用户无需手动点刷新,信息一直是最新的。
有了目标,就开始撸代码吧!🚀
🛠 技术栈
- Streamlit:Python神器,一键搞定Web应用。
- AkShare:国内行情数据神器,能直接获取资金流向数据。
- Plotly:强大的可视化库,这次用它做树状热力图。
- Pandas:数据处理少不了它。
📊 代码实现
1. 获取资金流向数据
股市数据当然得从靠谱的地方获取,我们用 AkShare 的 stock_sector_fund_flow_rank 来搞定资金流数据:
import akshare as ak
import pandas as pd
def process_data(indicator):
"""获取并处理资金流数据"""
try:
raw = ak.stock_sector_fund_flow_rank(
indicator=indicator,
sector_type="行业资金流"
)
df = raw.rename(columns={'名称': '板块名称'})
df['资金净流入(亿)'] = df['主力净流入-净额'] / 100000000 # 转换为“亿”
df['资金净流入(亿)'] = df['资金净流入(亿)'].round(2) # 保留两位小数
df['涨跌幅'] = pd.to_numeric(df['涨跌幅'], errors='coerce')
df['流向强度'] = abs(df['资金净流入(亿)'])
return df.dropna(subset=['资金净流入(亿)'])
except Exception as e:
print(f"数据获取失败: {e}")
return pd.DataFrame()
这样,我们可以用 process_data("今日") 来获取今日的行业板块资金流数据。
2. 生成热力图 🎨
有了数据,接下来就是可视化部分了,我们用 Plotly 画一个树状热力图:
import plotly.express as px
COLOR_SCALE = [
[0.0, "#00ff00"], # 绿色(流出最大)
[0.45, "#dfffdf"], # 浅绿色(小幅流出)
[0.5, "#ffffff"], # 白色(平衡点)
[0.55, "#ffe5e5"], # 浅红色(小幅流入)
[1.0, "#ff0000"] # 红色(流入最大)
]
def generate_heatmap(df):
"""生成树状热力图"""
fig = px.treemap(
df,
path=['板块名称'],
values='流向强度',
color='资金净流入(亿)',
color_continuous_scale=COLOR_SCALE,
hover_data={
'涨跌幅': ':%',
'资金净流入(亿)': ':'
},
height=800
)
return fig
这张图的颜色代表资金的流向,红色表示资金流入,绿色表示资金流出,一眼就能看出主力资金的动向!
3. 使用 Streamlit 构建交互界面
有了数据和图表,我们用 Streamlit 搭建一个 Web 界面:
import streamlit as st
from datetime import datetime
def sidebar_controls():
with st.sidebar:
st.header("控制面板")
indicator = st.radio("分析周期", ["今日", "5日", "10日"], index=0, horizontal=True)
refresh_interval = st.slider("自动刷新间隔 (秒)", 60, 3600, 60, 60)
return indicator, refresh_interval
def main_display(df):
st.title("📊 资金流向热力图")
st.caption(f"数据更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if not df.empty:
st.plotly_chart(generate_heatmap(df), use_container_width=True)
else:
st.warning("⚠️ 数据获取失败,请检查网络连接")
4. 自动刷新系统 ⏳
资金流是动态的,当然不能只显示静态数据,我们加个 自动刷新功能:
import time
def auto_refresh_system(refresh_interval):
time.sleep(refresh_interval)
st.rerun()
5. 整合一键运行 🚀
if __name__ == "__main__":
st.set_page_config(layout="wide")
indicator, refresh_interval = sidebar_controls()
df = process_data(indicator)
main_display(df)
auto_refresh_system(refresh_interval)
只要运行 streamlit run app.py,就能看到实时的资金流向热力图了!🎉
🎯 成果展示
运行之后,你会看到一个 大屏可视化的热力图,资金流向一目了然:
- 颜色:红色代表资金流入,绿色代表流出,越深代表金额越大。
- 自动刷新,完全不用手动点击更新!
- 交互性:鼠标悬停可以查看详细数据。
这比在 Excel 里手动分析好用多了吧?😆
最终源码(粘贴过来就能用)
import streamlit as st
import akshare as ak
import plotly.express as px
import pandas as pd
import time
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
# 自定义颜色映射
COLOR_SCALE = [
[0.0, "#00ff00"], # 绿色(资金流出最大)
[0.45, "#dfffdf"], # 浅绿色(小幅流出)
[0.5, "#ffffff"], # 白色(平衡点)
[0.55, "#ffe5e5"], # 浅红色(小幅流入)
[1.0, "#ff0000"] # 红色(资金流入最大)
]
# 数据预处理增强版
@st.cache_data(ttl=300)
def process_data(indicator):
"""强化数据预处理逻辑"""
try:
raw = ak.stock_sector_fund_flow_rank(
indicator=indicator,
sector_type="行业资金流"
)
raw.columns = raw.columns.str.replace(indicator, '', regex=False)
# 数值转换
df = raw.rename(columns={'名称': '板块名称'})
df['资金净流入(亿)'] = df['主力净流入-净额'] / 100000000
df['资金净流入(亿)'] = df['资金净流入(亿)'].round(2)
df['涨跌幅'] = pd.to_numeric(df['涨跌幅'], errors='coerce')
# 流向强度计算
df['流向强度'] = abs(df['资金净流入(亿)'])
return df.dropna(subset=['资金净流入(亿)'])
except Exception as e:
st.error(f"数据错误: {str(e)}")
return pd.DataFrame()
# 热力图生成引擎
def generate_heatmap(df):
"""生成符合金融行业标准的树状热力图"""
fig = px.treemap(
df,
path=['板块名称'],
values='流向强度',
color='资金净流入(亿)',
color_continuous_scale=COLOR_SCALE,
range_color=[-max(abs(df['资金净流入(亿)'].min()), abs(df['资金净流入(亿)'].max())),
max(abs(df['资金净流入(亿)'].min()), abs(df['资金净流入(亿)'].max()))],
color_continuous_midpoint=0,
branchvalues='total',
hover_data={
'涨跌幅': ':%',
'资金净流入(亿)': ':',
'主力净流入-净占比': ':%'
},
height=1000
)
# 高级样式配置
fig.update_traces(
texttemplate=(
"<b>%{label}</b><br>"
"📈%{customdata[0]}%<br>"
"💰%{customdata[1]}亿"
),
hovertemplate=(
"<b>%{label}</b><br>"
"涨跌幅: %{customdata[0]}%<br>"
"资金净流入: <b>%{customdata[1]}</b>亿<br>"
"主力占比: %{customdata[2]}%"
),
textfont=dict(size=18, color='black')
)
fig.update_layout(
margin=dict(t=0, l=0, r=0, b=0),
coloraxis_colorbar=dict(
title="资金流向(亿)",
ticks="inside",
thickness=20,
len=0.6,
y=0.7
)
)
return fig
# 侧边栏控件组
def sidebar_controls():
with st.sidebar:
st.header("控制面板")
indicator = st.radio(
"分析周期",
["今日", "5日", "10日"],
index=0,
horizontal=True
)
refresh_interval = st.slider(
"自动刷新间隔 (秒)",
min_value=60, max_value=3600,
value=60, step=60,
help="设置自动刷新间隔,默认1分钟"
)
return indicator, refresh_interval
# 主界面
def main_display(df):
st.title("📊 资金流向热力图")
st.caption(f"数据更新于: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if not df.empty:
with st.spinner("生成可视化..."):
st.plotly_chart(generate_heatmap(df), use_container_width=True)
# 动态摘要面板
with st.expander("📌 实时快报", expanded=True):
col1, col2, col3 = st.columns(3)
col1.metric("🔥 最强流入",
f"{df['资金净流入(亿)'].max():.2f}亿",
df.loc[df['资金净流入(亿)'].idxmax(), '板块名称'])
col2.metric("💧 最大流出",
f"{df['资金净流入(亿)'].min():.2f}亿",
df.loc[df['资金净流入(亿)'].idxmin(), '板块名称'])
col3.metric("⚖️ 多空比",
f"{len(df[df['资金净流入(亿)']>0])}:{len(df[df['资金净流入(亿)']<0])}",
f"总净额 {df['资金净流入(亿)'].sum():.2f}亿")
else:
st.warning("⚠️ 数据获取失败,请检查网络连接")
# 自动刷新系统
def auto_refresh_system(refresh_interval):
time.sleep(refresh_interval)
st.rerun()
print("数据刷新了")
# 主程序
if __name__ == "__main__":
st.set_page_config(layout="wide")
indicator, refresh_interval = sidebar_controls()
df = process_data(indicator)
main_display(df)
auto_refresh_system(refresh_interval)
🏆 总结
这个项目用 Python 搞定了:
✅ 数据抓取(AkShare)
✅ 数据处理(Pandas)
✅ 交互式可视化(Plotly)
✅ Web 界面(Streamlit)
✅ 自动刷新机制
一句话总结:用最少的代码,做出了最直观的金融数据可视化!💡
对于炒股的朋友,这个工具可以帮助你快速了解市场资金流向,不用再看密密麻麻的表格了!📈
喜欢的话,记得点个赞❤️,咱们下次见!🎉
来源:juejin.cn/post/7492990918702137380
前端获取本地文件目录内容
前端获取本地文件目录内容
一、核心原理说明
由于浏览器的 “沙箱安全机制”,前端 JavaScript 无法直接访问本地文件系统,必须通过用户主动授权(如选择目录操作)才能获取文件目录内容。目前主流实现方案基于两种 API:传统 File API(兼容性优先)和现代 FileSystem Access API(功能优先),以下将详细介绍两种方案的实现流程、代码示例及适用场景。
二、方案一:基于 File API 实现(兼容性首选)
1. 方案概述
通过隐藏的 <input type="file"> 标签(配置 webkitdirectory 和 directory 属性)触发用户选择目录操作,用户选择后通过 files 属性获取目录下所有文件的元数据(如文件名、大小、相对路径等)。该方案兼容几乎所有现代浏览器(包括 Chrome、Firefox、Safari 等),但仅支持 “一次性获取选中目录内容”,无法递归遍历子目录或修改文件。
2. 完整使用示例
2.1 HTML 结构(含 UI 交互区)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>File API 目录访问示例</title>
<!-- 引入 Tailwind 简化样式(也可自定义 CSS) -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
.file-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
.file-icon { margin-right: 8px; font-size: 18px; }
.file-info { flex: 1; }
.file-size { color: #666; font-size: 14px; }
</style>
</head>
<body class="p-8 bg-gray-50">
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow">
<h2 class="text-2xl font-bold mb-4">File API 目录内容获取</h2>
<!-- 触发按钮(隐藏原生 input) -->
<button id="selectDirBtn" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
选择本地目录
</button>
<input type="file" id="dirInput" webkitdirectory directory style="display: none;">
<!-- 文件列表展示区 -->
<div class="mt-4 border rounded-lg max-h-80 overflow-y-auto">
<div id="fileList" class="p-4 text-center text-gray-500">
请选择目录以查看文件列表
</div>
</div>
</div>
<script>
// 2.2 JavaScript 逻辑实现
const dirInput = document.getElementById('dirInput');
const selectDirBtn = document.getElementById('selectDirBtn');
const fileList = document.getElementById('fileList');
// 1. 点击按钮触发原生 input 选择目录
selectDirBtn.addEventListener('click', () => {
dirInput.click();
});
// 2. 监听目录选择变化,处理文件数据
dirInput.addEventListener('change', (e) => {
const selectedFiles = e.target.files; // 获取选中目录下的所有文件(含子目录文件)
if (selectedFiles.length === 0) {
fileList.innerHTML = '<div class="p-4 text-center text-gray-500">未选择任何文件</div>';
return;
}
// 3. 解析文件数据并渲染到页面
let fileHtml = '';
Array.from(selectedFiles).forEach(file => {
// 判断是否为目录(通过 type 为空且 size 为 0 间接判断)
const isDir = file.type === '' && file.size === 0;
// 获取文件在目录中的相对路径(webkitRelativePath 为非标准属性,但主流浏览器支持)
const relativePath = file.webkitRelativePath || file.name;
// 格式化文件大小(辅助函数)
const fileSize = isDir ? '—' : formatFileSize(file.size);
fileHtml += `
<div class="file-item">
<span class="file-icon ${isDir ? 'text-yellow-500' : 'text-gray-400'}">
${isDir ? '📁' : '📄'}
</span>
<div class="file-info">
<div class="font-medium">${file.name}</div>
<div class="text-xs text-gray-500">${relativePath}</div>
</div>
<div class="file-size text-sm">${fileSize}</div>
</div>
`;
});
fileList.innerHTML = fileHtml;
});
// 辅助函数:格式化文件大小(Bytes → KB/MB/GB)
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const units = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`;
}
</script>
</body>
</html>
3. 关键特性与限制
- 优势:兼容性强(支持 Chrome 15+、Firefox 4+、Safari 6+),无需额外依赖,实现简单。
- 限制:
- 无法直接识别 “目录” 类型,需通过
type和size间接判断; - 仅能获取选中目录下的 “扁平化文件列表”,无法递归获取子目录结构;
- 无文件读写能力,仅能获取元数据。
三、方案二:基于 FileSystem Access API 实现(功能优先)
1. 方案概述
FileSystem Access API 是 W3C 正在标准化的现代 API(目前主要支持 Chromium 内核浏览器,如 Chrome 86+、Edge 86+),提供 “目录选择、递归遍历、文件读写、持久化权限” 等更强大的能力。通过 window.showDirectoryPicker() 直接请求用户授权,授权后可主动遍历目录结构,支持复杂的文件操作。
2. 完整使用示例
2.1 HTML 结构(含子目录遍历功能)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>FileSystem Access API 目录访问示例</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.dir-tree-item { padding: 4px 0 4px 16px; border-left: 1px solid #eee; }
.dir-header { display: flex; align-items: center; cursor: pointer; padding: 4px 0; }
.dir-icon { margin-right: 8px; }
.file-meta { color: #666; font-size: 14px; margin-left: 8px; }
</style>
</head>
<body class="p-8 bg-gray-50">
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow">
<h2 class="text-2xl font-bold mb-4">FileSystem Access API 目录遍历</h2>
<!-- 触发目录选择按钮 -->
<button id="openDirBtn" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
打开并遍历目录
</button>
<!-- 目录树展示区 -->
<div class="mt-4 border rounded-lg p-4 max-h-80 overflow-y-auto">
<div id="dirTree" class="text-gray-500">
请点击按钮选择目录
</div>
</div>
</div>
<script>
// 2.2 JavaScript 逻辑实现(含递归遍历)
const openDirBtn = document.getElementById('openDirBtn');
const dirTree = document.getElementById('dirTree');
openDirBtn.addEventListener('click', async () => {
try {
// 1. 检查浏览器兼容性
if (!window.showDirectoryPicker) {
alert('您的浏览器不支持该功能,请使用 Chrome 或 Edge 浏览器');
return;
}
// 2. 请求用户选择目录(获取 DirectoryHandle 对象)
const dirHandle = await window.showDirectoryPicker({
mode: 'read', // 权限模式:read(只读)/ readwrite(读写)
startIn: 'documents' // 默认打开目录(可选:documents、downloads 等)
});
// 3. 递归遍历目录结构并渲染
dirTree.innerHTML = '<div class="text-center text-gray-500">正在读取目录...</div>';
const treeHtml = await renderDirectoryTree(dirHandle, 0);
dirTree.innerHTML = treeHtml;
} catch (err) {
// 捕获用户取消选择或权限拒绝错误
if (err.name === 'AbortError') {
dirTree.innerHTML = '<div class="text-center text-gray-500">用户取消选择</div>';
} else {
dirTree.innerHTML = `<div class="text-center text-red-500">错误:${err.message}</div>`;
console.error('目录访问失败:', err);
}
}
});
/**
* 递归渲染目录树
* @param {DirectoryHandle} handle - 目录/文件句柄
* @param {number} depth - 目录深度(用于缩进)
* @returns {string} 目录树 HTML
*/
async function renderDirectoryTree(handle, depth) {
const isDir = handle.kind === 'directory';
const indent = 'margin-left: ' + (depth * 16) + 'px;'; // 按深度缩进
let itemHtml = '';
if (isDir) {
// 处理目录:添加展开/折叠功能
itemHtml += `
<div class="dir-header" style="${indent}" onclick="toggleDir(this)">
<span class="dir-icon text-yellow-500">📁</span>
<span class="font-medium">${handle.name}</span>
<span class="file-meta">(目录)</span>
</div>
<div class="dir-children" style="display: none;">
`;
// 遍历目录下的所有子项(递归)
for await (const childHandle of handle.values()) {
itemHtml += await renderDirectoryTree(childHandle, depth + 1);
}
itemHtml += '</div>'; // 闭合 dir-children
} else {
// 处理文件:获取文件大小等元数据
const file = await handle.getFile();
const fileSize = formatFileSize(file.size);
itemHtml += `
<div style="${indent} display: flex; align-items: center; padding: 4px 0;">
<span class="dir-icon text-gray-400">📄</span>
<span>${handle.name}</span>
<span class="file-meta">${fileSize}</span>
</div>
`;
}
return itemHtml;
}
// 目录展开/折叠切换(全局函数,用于 HTML 内联调用)
function toggleDir(el) {
const children = el.nextElementSibling;
children.style.display = children.style.display === 'none' ? 'block' : 'none';
el.querySelector('.dir-icon').textContent = children.style.display === 'none' ? '📁' : '📂';
}
// 复用文件大小格式化函数(同方案一)
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const units = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`;
}
</script>
</body>
</html>
3. 关键特性与限制
- 优势:
- 直接识别 “目录 / 文件” 类型(通过
handle.kind); - 支持递归遍历目录结构,可实现 “目录树” 交互;
- 提供文件读写能力(通过
fileHandle.createWritable()); - 可请求持久化权限(
handle.requestPermission()),下次访问无需重新授权。
- 限制:兼容性差,仅支持 Chromium 内核浏览器,Firefox 和 Safari 暂不支持。
四、两种方案对比分析
| 对比维度 | 方案一(File API) | 方案二(FileSystem Access API) |
|---|---|---|
| 浏览器兼容性 | 强(支持所有现代浏览器) | 弱(仅 Chromium 内核浏览器) |
| 目录识别能力 | 间接判断(依赖 type 和 size) | 直接识别(handle.kind) |
| 目录遍历能力 | 仅扁平化列表,无递归支持 | 支持递归遍历,可构建目录树 |
| 文件操作能力 | 仅读取元数据,无读写能力 | 支持文件读写、删除等完整操作 |
| 权限持久化 | 不支持(每次刷新需重新选择) | 支持(可请求持久化权限) |
| 交互体验 | 依赖隐藏 input,体验较基础 | 原生 API 调用,体验更流畅 |
| 适用场景 | 兼容性优先的简单目录查看需求 | 现代浏览器下的复杂文件管理需求 |
五、注意事项与最佳实践
- 安全合规:无论哪种方案,都必须通过 “用户主动操作” 触发授权(如点击按钮),禁止自动触发目录选择,否则浏览器会拦截操作。
- 错误处理:需捕获 “用户取消选择”(AbortError)和 “权限拒绝”(PermissionDeniedError)等错误,避免页面展示异常。
- 兼容性适配:可通过 “特性检测” 实现方案降级,例如:
if (window.showDirectoryPicker) {
// 使用方案二(FileSystem Access API)
} else {
// 使用方案一(File API)
}
- 性能优化:遍历大量文件时(如超过 1000 个文件),建议使用 “分页加载” 或 “虚拟滚动”,避免一次性渲染导致页面卡顿。
- 隐私保护:不建议存储用户本地文件路径等敏感信息,仅在前端临时处理文件数据,避免隐私泄露风险。
来源:juejin.cn/post/7542308569641074724
用代码绘制独一无二的七夕玫瑰(Trae版)
前言
七夕,这个充满浪漫气息的传统节日,总是让人心生期待。对于程序员来说,虽然我们日常与代码为伴,但浪漫的心思也从不缺席。今年七夕,不妨用一种特别的方式表达爱意——用代码绘制一朵玫瑰花,送给那个特别的他/她。
编程与浪漫的结合
程序员的世界里,代码是我们的语言,逻辑是我们的画笔。虽然我们不常在言语上表达情感,但通过代码,我们可以创造出独一无二的浪漫。
绘制一朵玫瑰花,不仅是一次技术的挑战,更是一份心意的传递。在这个特别的日子里,用代码绘制的玫瑰花,或许能成为你表达爱意的特别方式。
依旧是让我们的ai编程大师Trae出手,看看能不能有惊艳的效果
第一次的提问,生成的效果很差

然后我就让他搜索一下目前互联网上的玫瑰花demo,模仿一下

这次看得出是一朵花,但是没有叶子,花瓣得仔细看才有,所以再次提问

一运行报错了,不要慌,我让我们得ai编程大师Trae 他自己修复一下

过了一分钟,Trae修复了之前的报错,看起来还是不错的,还可以支持旋转,很有艺术感的气氛,非常好~


Trae的实现思路
1. 结构优化
- 花瓣结构 :引入了多层花瓣概念(5层),每层花瓣具有不同的形状、大小和卷曲效果,使花朵更加立体和真实
- 花茎改进 :增加了花茎长度,调整了半径,并添加了椭圆横截面、自然弯曲和小刺,增强真实感
- 叶子优化 :增加了叶子数量,采用交错排列,并实现了更复杂的叶子形状,包括中脉、宽度变化和向下弯曲效果

创建花茎和花茎的弯曲程度

玫瑰花的叶子,设计一定的曲度,看起来像真实的叶子

粒子系统优化
- 粒子数量 :增加了粒子总数从20,000到30,000,提供更细腻的视觉效果
- 粒子分配 :优化了花朵(70%)、花茎(20%)和叶子(10%)的粒子分配比例
- 粒子大小和透明度 :根据粒子在花朵、花茎和叶子中的位置,以及与中心的距离,动态调整粒子大小和透明度

总结
在这个充满爱的节日里,程序员也可以用自己独特的方式表达浪漫。
用代码绘制一朵玫瑰花,不仅是一次有趣的编程实践,更是一份特别的礼物,希望这个小小的创意能为你的七夕增添一份特别的浪漫,如果你还有其他想法,可以把你的idea发给Trae,让他帮助你完成你的idea,对他/她进行爱意的表达,祝愿天下有情人终成眷属。
在实际开发中,你可以根据具体需求进一步优化和调整样式。希望这篇文章能对你有所帮助!
来源:juejin.cn/post/7542501413760761896
Nginx+Keepalive 实现高可用并启用健康检查模块
1. 目标效果
keepalived 负责监控 192.168.1.20 和 192.168.1.30 这两台负载均衡的服务器,并自动选择一台作为主服务器。用户访问 http://192.168.1.10 时,由主服务器接收该请求。当 keepalived 检测到主服务器不可访问时,会将备服务器升级为主服务器,从而实现高可用。
在主服务器中,通过 nginx(tengine)实现负载均衡,将访问请求分流到 192.168.1.100 和 192.168.1.200 这两台业务服务器。 nginx 中的健康检查模块会检测业务服务器状态,如果检测到 192.168.1.100 不可访问,则不再将访问请求发送给该服务器。
2. 部署 Keepalived
2.1 主机 IP
| 主机 | IP |
|---|---|
| 虚拟 IP | 192.168.1.10 |
| 主服务器 | 192.168.1.20 |
| 备服务器 | 192.168.1.30 |
2.2 主服务器设置
官方配置说明文档:Keepalived for Linux
yun install vim epel-release keepalived -y
cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak
cat > /etc/keepalived/keepalived.conf <<EOF
! Configuration File for keepalived
global_defs {
router_id nginx01
}
vrrp_script check_nginx {
script "/etc/keepalived/nginx_check.sh"
interval 2
weight 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass linalive
}
virtual_ipaddress {
192.168.1.10
}
track_script {
check_nginx
}
}
EOF
touch /etc/keepalived/nginx_check.sh && chmod +x /etc/keepalived/nginx_check.sh
cat > /etc/keepalived/nginx_check.sh <<EOF
#!/bin/bash
if ! pgrep -x "nginx" > /dev/null; then
systemctl restart nginx
sleep 2
if ! pgrep -x "nginx" > /dev/null; then
pkill keepalived
fi
fi
EOF
2.2 备服务器设置
yun install vim epel-release keepalived -y
cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf.bak
cat > /etc/keepalived/keepalived.conf <<EOF
! Configuration File for keepalived
global_defs {
router_id nginx02
}
vrrp_script check_nginx {
script "/etc/keepalived/nginx_check.sh"
interval 2
weight 2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass linalive
}
virtual_ipaddress {
192.168.1.10
}
track_script {
check_nginx
}
}
EOF
touch /etc/keepalived/nginx_check.sh && chmod +x /etc/keepalived/nginx_check.sh
cat > /etc/keepalived/nginx_check.sh <<EOF
#!/bin/bash
if ! pgrep -x "nginx" > /dev/null; then
systemctl restart nginx
sleep 2
if ! pgrep -x "nginx" > /dev/null; then
pkill keepalived
fi
fi
EOF
3. 部署 Tengine (主备服务器)
3.1 准备 Tengine 压缩文件
下载 tengine 压缩文件,将文件上传到 /opt 文件夹下。下载地址:The Tengine Web Server
本文章编写时,最新版是:tengine-3.1.0.tar.gz
3.2 解压并编译
yum install -y gcc gcc-c++ make pcre-devel zlib-devel openssl-devel
tar zxvf /opt/tengine-3.1.0.tar.gz -C /opt
cd /opt/tengine-3.1.0
# configure 有众多的参数可设置,可使用 ./configure --help 进行查看
# 按照官方说法默认应该是开启了健康检查模块,但实测需要手动添加参数
./configure --add-module=modules/ngx_http_upstream_check_module/
make && make install
3.3 添加服务项
cat > /etc/systemd/system/nginx.service <<EOF
[Unit]
Description=The Tengine HTTP and reverse proxy server
After=network.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
3.4 编辑 nginx 配置文件
此处配置的内容项可参考官方文档:ngx_http_upstream_check_module
# tengine 默认的安装路径是 /usr/local/nginx
# 配置文件路径: /usr/local/nginx/conf/nginx.conf
# /favicon.ico 是接口地址,需替换成真实的 api 接口
worker_processes auto;
events {
worker_connections 1024;
}
http {
upstream cluster1 {
server 192.168.1.100:8082;
server 192.168.1.200:8089;
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_http_send "HEAD /favicon.ico HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
server {
listen 80;
server_name localhost;
location / {
index Index.aspx;
proxy_pass http://cluster1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /status {
check_status;
access_log off;
}
}
}
3.5 启动服务并访问
使用 systemctl start nginx启动服务,并访问 localhost:80/status查看健康检查报表页
4. 写在最后
来源:juejin.cn/post/7483314478957232138
创建型模式:抽象工厂模式
什么是抽象工厂模式
抽象工厂模式是一种创建型设计模式,它提供一个接口来创建一系列相关或相互依赖的对象家族,而无需指定它们的具体类。简单来说,抽象工厂模式是工厂模式的升级版,它不再只生产一种产品,而是生产一整套产品。
抽象工厂vs工厂方法:关键区别
- 工厂方法模式:关注单个产品的创建,一个工厂创建一种产品
- 抽象工厂模式:关注产品族的创建,一个工厂创建多种相关产品
这就像一个生产手机的工厂(工厂方法)和一个生产整套电子设备(手机、平板、耳机)的工厂(抽象工厂)的区别。
抽象工厂模式的核心实现
// 产品A接口
public interface ProductA {
void operationA();
}
// 产品B接口
public interface ProductB {
void operationB();
}
// 产品A1实现
public class ConcreteProductA1 implements ProductA {
@Override
public void operationA() {
System.out.println("产品A1的操作");
}
}
// 产品A2实现
public class ConcreteProductA2 implements ProductA {
@Override
public void operationA() {
System.out.println("产品A2的操作");
}
}
// 产品B1实现
public class ConcreteProductB1 implements ProductB {
@Override
public void operationB() {
System.out.println("产品B1的操作");
}
}
// 产品B2实现
public class ConcreteProductB2 implements ProductB {
@Override
public void operationB() {
System.out.println("产品B2的操作");
}
}
// 抽象工厂接口
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
// 具体工厂1 - 创建产品族1(A1+B1)
public class ConcreteFactory1 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB1();
}
}
// 具体工厂2 - 创建产品族2(A2+B2)
public class ConcreteFactory2 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA2();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB2();
}
}
抽象工厂模式的关键点
- 产品接口:为每种产品定义接口
- 具体产品:实现产品接口的具体类
- 抽象工厂接口:声明一组创建产品的方法
- 具体工厂:实现抽象工厂接口,创建一个产品族
- 产品族:一组相关产品的集合(例如PC系列组件、移动系列组件)
实际应用示例:跨平台UI组件库
下面通过一个跨平台UI组件库的例子来展示抽象工厂模式的强大应用:
// ===== 按钮组件 =====
public interface Button {
void render();
void onClick();
}
// Windows按钮
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("渲染Windows风格的按钮");
}
@Override
public void onClick() {
System.out.println("Windows按钮点击效果");
}
}
// MacOS按钮
public class MacOSButton implements Button {
@Override
public void render() {
System.out.println("渲染MacOS风格的按钮");
}
@Override
public void onClick() {
System.out.println("MacOS按钮点击效果");
}
}
// ===== 复选框组件 =====
public interface Checkbox {
void render();
void toggle();
}
// Windows复选框
public class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("渲染Windows风格的复选框");
}
@Override
public void toggle() {
System.out.println("Windows复选框切换状态");
}
}
// MacOS复选框
public class MacOSCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("渲染MacOS风格的复选框");
}
@Override
public void toggle() {
System.out.println("MacOS复选框切换状态");
}
}
// ===== 文本框组件 =====
public interface TextField {
void render();
void getText();
}
// Windows文本框
public class WindowsTextField implements TextField {
@Override
public void render() {
System.out.println("渲染Windows风格的文本框");
}
@Override
public void getText() {
System.out.println("获取Windows文本框内容");
}
}
// MacOS文本框
public class MacOSTextField implements TextField {
@Override
public void render() {
System.out.println("渲染MacOS风格的文本框");
}
@Override
public void getText() {
System.out.println("获取MacOS文本框内容");
}
}
// ===== GUI工厂接口 =====
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
TextField createTextField();
}
// Windows GUI工厂
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
@Override
public TextField createTextField() {
return new WindowsTextField();
}
}
// MacOS GUI工厂
public class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
@Override
public TextField createTextField() {
return new MacOSTextField();
}
}
如何使用抽象工厂模式
// 应用类 - 与具体工厂解耦
public class Application {
private Button button;
private Checkbox checkbox;
private TextField textField;
// 构造函数接收一个抽象工厂
public Application(GUIFactory factory) {
button = factory.createButton();
checkbox = factory.createCheckbox();
textField = factory.createTextField();
}
// 渲染表单
public void renderForm() {
System.out.println("=== 开始渲染表单 ===");
button.render();
checkbox.render();
textField.render();
System.out.println("=== 表单渲染完成 ===");
}
// 表单操作
public void handleForm() {
System.out.println("\n=== 表单交互 ===");
button.onClick();
checkbox.toggle();
textField.getText();
}
}
// 客户端代码
public class GUIDemo {
public static void main(String[] args) {
// 检测当前操作系统
String osName = System.getProperty("os.name").toLowerCase();
GUIFactory factory;
// 根据操作系统选择合适的工厂
if (osName.contains("windows")) {
factory = new WindowsFactory();
System.out.println("检测到Windows系统,使用Windows风格UI");
} else {
factory = new MacOSFactory();
System.out.println("检测到非Windows系统,使用MacOS风格UI");
}
// 创建并使用应用 - 注意应用不依赖于具体组件类
Application app = new Application(factory);
app.renderForm();
app.handleForm();
}
}
运行结果(Windows系统上)
检测到Windows系统,使用Windows风格UI
=== 开始渲染表单 ===
渲染Windows风格的按钮
渲染Windows风格的复选框
渲染Windows风格的文本框
=== 表单渲染完成 ===
=== 表单交互 ===
Windows按钮点击效果
Windows复选框切换状态
获取Windows文本框内容
运行结果(MacOS系统上)
检测到非Windows系统,使用MacOS风格UI
=== 开始渲染表单 ===
渲染MacOS风格的按钮
渲染MacOS风格的复选框
渲染MacOS风格的文本框
=== 表单渲染完成 ===
=== 表单交互 ===
MacOS按钮点击效果
MacOS复选框切换状态
获取MacOS文本框内容
抽象工厂模式的常见应用场景
- 跨平台UI工具包:为不同操作系统提供一致的界面组件
- 数据库访问层:支持多种数据库系统(MySQL、Oracle、MongoDB等)
- 游戏开发:创建不同主题的游戏元素(中世纪、未来、童话等)
- 多环境配置系统:为开发、测试、生产环境提供不同实现
- 电子设备生态系统:创建配套的产品(手机、耳机、手表都来自同一品牌)
- 多主题应用:切换应用的视觉主题(暗色模式/亮色模式)
抽象工厂模式的实际案例
许多知名框架和库使用抽象工厂模式,如:
- Java的JDBC:
ConnectionFactory创建特定数据库的连接 - Spring Framework:通过BeanFactory创建和管理各种组件
- javax.xml.parsers.DocumentBuilderFactory:创建DOM解析器
- Hibernate:
SessionFactory为不同数据库创建会话
抽象工厂模式的优点
- 产品一致性保证:确保一个工厂创建的产品相互兼容
- 隔离具体类:客户端与具体类隔离,只与接口交互
- 开闭原则:引入新产品族不需要修改现有代码
- 替换产品族:可以整体替换产品族(如UI主题切换)
抽象工厂模式的缺点
- 扩展困难:添加新的产品类型需要修改工厂接口及所有实现
- 复杂度增加:产品较多时,类的数量会急剧增加
- 接口污染:接口中可能包含部分工厂不支持的创建方法
抽象工厂的实现变体
使用反射简化工厂实现
public class ReflectiveFactory implements GUIFactory {
private String packageName;
public ReflectiveFactory(String stylePrefix) {
packageName = "com.example.gui." + stylePrefix.toLowerCase();
}
@Override
public Button createButton() {
return (Button) createComponent("Button");
}
@Override
public Checkbox createCheckbox() {
return (Checkbox) createComponent("Checkbox");
}
@Override
public TextField createTextField() {
return (TextField) createComponent("TextField");
}
private Object createComponent(String type) {
try {
Class<?> clazz = Class.forName(packageName + "." + type);
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("无法创建组件", e);
}
}
}
带有默认实现的抽象工厂
public abstract class BaseGUIFactory implements GUIFactory {
// 提供默认实现
@Override
public TextField createTextField() {
return new DefaultTextField(); // 所有平台通用的默认实现
}
// 其他方法需要子类实现
@Override
public abstract Button createButton();
@Override
public abstract Checkbox createCheckbox();
}
实现抽象工厂的设计考虑
- 产品族边界:明确定义哪些产品属于同一族
- 接口设计:保持工厂接口精简,避免方法爆炸
- 工厂选择机制:考虑如何选择/切换具体工厂
- 扩展策略:提前考虑如何添加新产品类型
- 组合与单一职责:大型产品族可考虑拆分为多个子工厂
抽象工厂模式最佳实践
- 适度使用:当确实需要创建一系列相关对象时才使用
- 懒加载:考虑延迟创建产品,而不是一次创建所有产品
- 结合其他模式:与单例、原型、构建者等模式结合使用
- 依赖注入:通过依赖注入框架传递工厂
- 配置驱动:使用配置文件或注解选择具体工厂实现
// 使用配置驱动的工厂
public class ConfigurableGUIFactory {
public static GUIFactory getFactory() {
String factoryType = ConfigLoader.getProperty("ui.factory");
switch (factoryType) {
case "windows": return new WindowsFactory();
case "macos": return new MacOSFactory();
case "web": return new WebFactory();
default: throw new IllegalArgumentException("未知UI工厂类型");
}
}
}
抽象工厂与依赖倒置原则
抽象工厂是实现依赖倒置原则的绝佳方式:高层模块不依赖于低层模块,两者都依赖于抽象。
// 不好的设计:直接依赖具体类
public class BadForm {
private WindowsButton button; // 直接依赖具体实现
private WindowsCheckbox checkbox;
public void createUI() {
button = new WindowsButton(); // 硬编码创建具体类
checkbox = new WindowsCheckbox();
}
}
// 好的设计:依赖抽象
public class GoodForm {
private Button button; // 依赖接口
private Checkbox checkbox;
private final GUIFactory factory; // 依赖抽象工厂
public GoodForm(GUIFactory factory) {
this.factory = factory;
}
public void createUI() {
button = factory.createButton(); // 通过工厂创建
checkbox = factory.createCheckbox();
}
}
抽象工厂模式小结
抽象工厂模式是一种强大但需谨慎使用的创建型模式。它在需要一套相关产品且系统不应依赖于产品的具体类时非常有用。这种模式有助于确保产品兼容性,并为产品族提供统一的创建接口。
适当应用抽象工厂模式可以使代码更具灵活性和可维护性,但也要避免过度设计导致的复杂性。理解产品族的概念和如何设计良好的抽象工厂接口是掌握这一模式的关键。
来源:juejin.cn/post/7491963395284549669
Spring Boot Admin:一站式监控微服务,这个运维神器真香!
关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
在现代微服务架构中,应用实例的数量动辄成百上千。传统的逐个登录服务器查看日志、检查状态的方式早已变得低效且不现实。
因此,一个集中化、可视化、且能提供实时健康状态的管理平台变得至关重要。Spring Boot Admin (SBA) 正是为了满足这一需求而生的强大工具。
然而,各种厂商的云服务提供了各种监控服务解决客户的各种痛点。Spring Boot Admin这样的工具似乎关注度没有那么高。小编也是无意间发现这款产品,分享给大家。
02 简介

Spring Boot Admin 是一个用于管理和监控 Spring Boot 应用程序的开源社区项目。它并非官方 Spring 项目,但在社区中备受推崇并被广泛使用。
其核心原理是:一个作为“服务器”(Server)的中央管理端,通过收集并展示众多作为“客户端”(Client)的 Spring Boot 应用的监控信息。
Spring Boot Admin 通过集成 Spring Boot Actuator 端点来获取应用数据,并提供了一个友好的 Web UI 界面来展示这些信息。
主要分为两部分:
- 服务端:监控平台
- 客户端:业务端

SpringBoot的版本和Spring Boot Admin有一定的对应关系:

GitHub地址:github.com/codecentric…
文档地址:docs.spring-boot-admin.com/
03 Admin服务端
服务的端配置相当简单,只需要引入依赖,启动增加注解。服务端的基础配置就算完成了。
3.1 基础配置
Maven依赖
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${latest.version}</version>
</dependency>
增加注解
@EnableAdminServer

这两个配置就可访问项目的IP+端口,进入管理页面了。

3.2 增加鉴权
为了数据安全,可以增加鉴权。拥有账号和密码方可进入。
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置文件
# 设置自己的用户名和密码
spring.security.user.name=admin
spring.security.user.password=123456
输入对应的用户名和密码就可以进入了。

3.3 增加邮件推送
官方提供了各种通知,也可以自定义,如图:
我们以邮件通知为例。
Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置
# 邮箱配置
spring.mail.host=smtp.163.com
spring.mail.port=25
spring.mail.username=用户名
spring.mail.password=*****[授权码]
# 发送和接受邮箱
spring.boot.admin.notify.mail.to=wsapplyjob@163.com
spring.boot.admin.notify.mail.from=wsapplyjob@163.com
客户端下线之后会触发邮件:

04 Adamin客户端
因为服务端是依赖Spring Boot Actuator 端点来获取应用数据,所以我们需要开放期其所有的服务。
4.1 基础配置
Maven依赖
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${latest.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# 服务端地址
spring.boot.admin.client.url=http://127.0.0.1:8081
# 鉴权信息
spring.boot.admin.client.username=admin
spring.boot.admin.client.password=123456
# 开发所有的暴漏的信息
management.endpoints.web.exposure.include=*
4.2 监控界面

进入之后,我们就会发现上面的页面。点击应用墙,就会展示所有监控的实例。进入之后如图:

进入之后就可以看到五大块。其中②就是我们之前看到的日志级别的控制。还包含了缓存、计划任务、映射甚至类之间的依赖关系。
因为界面支持中文,里面具体的功能就不做描述,感兴趣的可以自己的探索。
4.3 日志配置增加日志

默认的日志进去只有日志的级别,并不会打印日志。
这是官方的描述:

我们增加配置:
logging.file.name=/var/log/boot-log.log
logging.pattern.file=%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID}){magenta} %clr(---){faint} %clr([.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx (2)
启动查看结果:

我们就可以看到信的菜单:日志文件
4.4 缓存
【缓存】是没有数据的:

缓存依赖
<!-- 监控缓存需要的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
代码

触发缓存任务之后,就会出现缓存的管理:

4.5 计划任务
【计划任务】和缓存基本一样,但是无需引入第三方依赖。使用@Scheduled即可。

监控结果:

05 小结
Spring Boot Admin 以其简洁的配置、强大的功能和友好的界面,成为了 Spring Boot 微服务监控领域的事实标准。它极大地降低了监控和运维的复杂度,让开发者能够更专注于业务逻辑开发。
对于中小型规模的微服务集群,直接使用 SBA 是一个高效且成本低廉的解决方案。
赶快去探索里面不同的功能的吧!
来源:juejin.cn/post/7542450691911155762
儿子不收拾玩具,我用AI给他量身定制开发一个APP,这下舒服了
1. 前言
比上班更可怕的是什么?是加班。
比加班更可怕的是什么?是固定加班,也就是 996,大小周。
作为一个荣获 996 福报的牛马,我认为我的际遇已经很可怕了。
没想到还有比这更可怕的,拖着被996折腾过的疲惫身体回家后。我儿子向我展示他一天的劳动成果。

这时候你肯定会说让他收起来不就行了?这时候我应该拿出标志性的礼貌三问:你有对象吗?你有儿子吗?你让你儿子收他就收吗?

不会吧,你儿子收啊。那我和你换个儿子吧。
我对我儿子威逼利诱什么招式都试过了,他每次就3招我就没辙了:
爸爸,我累了你帮我收吧。
爸爸,地上的玩具太多了你和我一起收吧,收着收着这小子就不见了。
爸爸,我要睡觉了,晚安。
每天晚上我都要花时间收拾这个烂摊子,收过十几次后我后知后觉有了个想法。
平时我工作的时候,一个5分钟就能手写完搞定的配置我都要花10分钟写个脚本自动化。
为啥不能让收玩具这件事情自动化呢?我可是个优雅的程序员啊。重复做一个动作在我这应该是严格禁止的才对。
所以我打算做一个自动收玩具的机器。

不是哥们,这我真做不了。在我这自动化是什么意思呢?
不需要自己动手干的就是自动化,把配置做到管理后台,让运营自己去配置算不算自动化?那必须是的呀。
那么,想一种办法让我儿子自己把玩具收起来是不是自动化?那也必须是的呀。
自动化的定义就是不需要自己动手干,管是机器干还是人干,反正不要我来干。

说干就干,我儿子特别喜欢数字,迷恋加法。那不就盖了帽了。给他安排一个任务APP,收完一件玩具就加分,他肯定特满足特有成就感。
我虽然是一个前端后端运维测试攻城狮,但我的的确确没有开发过APP。除了大学要交 Android 作业抱过同学大腿喵了一眼,从那之后我就下定决定干后端去了。因为艺术细菌不是说本人没有,是本人想有但它对我不感冒啊。
但是别忘了,现在是 AI 的时代。产品的活我不会,AI 来。APP 开发我不会,AI 来。貌似不需要后端,那我只能当当测试了。
2. 正片开始
我调研了一圈,目前有几种方案可以实现:
- 直接刚原生
- 退而求其次,flutter
- 一退再退,直接uniapp 网页糊上
原生做起来体验最好,但是搭个环境真是要了我的老命了,所以弃之。
flutter总体感觉不错,但是要另外学一门语言,想想我就脑壳疼,亦弃之。
uni-app 看起来不咋滴,蛮多人吐槽但也有真正的案例。但我发现它能云打包,不用我在本地配一堆乱七八糟的。下载一个HBuilder 就行了,虽然很多人吐槽这个 IDE,但关我啥事?是 AI 来写代码,又不是我写代码,尽管恶心 AI 去。选它选它
2.1 画原型图
Cursor,Gemini,claude code 我都试了,Gemini的设计感最强,豆包的体验最好。豆包的效果看起来非常的奈斯啊!

2.2 开发
有了原型那就好办了,直接贴图让cursor 或者 claude code 对着实现就行了。
这里要吐槽一下claude code,不能粘贴板直接贴图,只能把图片拖进去,差评。
现在可以粘贴图片了,Mac 可以尝试用ctrl+v(没错,不是command+v)
把所有的原型图贴进去之后,输入这句简单的Prompt,claude code 就会开始干活了。
请根据原型图,使用uniapp 开发一个app
2.3 加需求
第一版完成了他的使命,最近儿子有点腻烦了,收个玩具磨磨蹭蹭的。不行,我得想点法子,加点东西让他保持新鲜感,然后养成习惯,以后就不用我管了,想想就非常的苏胡啊。
所以为了调动他的积极性,更营造一个紧张的氛围,我加入了倒计时功能:
接下来有个新功能。我想为任务增加一个计时完成功能:
1. 完成任务时,不再是简单的点击即可;
2. 完成任务时,应该提供一个开始完成按钮,然后启动倒计时
3. 创建任务时,应该配置预计完成时间
4. 完成任务时,遵循规则:a.如果在预计时间的一半完成可以得到2倍的分数;b.如果超过一半时间完成则得到1.5倍分数;c.如果超时完成则得到1倍分数
直接把需求丢给AI实现去,自己测试测试,没问题就打包。

2.3 测试打包
先浏览器运行看看效果,可以 F12 切换成手机视图看有没有挤压之类的。

测试没问题就直接打包。因为我是尊贵的 Android 用户,所以我只跑了 Android 打包。

我坦白,uni-app部分我基本是看这个老哥的:juejin.cn/post/729631…
2.4 看看效果

来源:juejin.cn/post/7538276577605632046
别再混淆了!一文彻底搞懂System.identityHashCode与Object.hashCode的区别
在Java开发中,哈希码的使用无处不在,但许多开发者对
System.identityHashCode()和Object.hashCode()的区别仍然模糊不清。本文将深入剖析二者的核心区别,并通过实际代码演示它们在不同场景下的行为差异。
一、本质定义:两种哈希码的起源
- Object.hashCode()
- 所有Java对象的默认方法(定义在
Object类中) - 可被子类重写(通常基于对象内容计算)
// 默认实现(未重写时)
public native int hashCode();
- 所有Java对象的默认方法(定义在
- System.identityHashCode()
System类提供的静态工具方法- 无视任何重写,始终返回JVM原始哈希码
public static native int identityHashCode(Object x);
二、核心区别对比(表格速查)
| 特性 | Object.hashCode() | System.identityHashCode() |
|---|---|---|
| 是否可重写 | ✅ 子类可重写改变行为 | ❌ 行为固定不可变 |
| 对重写的敏感性 | 返回重写后的自定义值 | 永远返回JVM原始哈希码 |
null处理 | 调用抛NullPointerException | 安全返回0 |
| 返回值一致性 | 内容改变时可能变化 | 对象生命周期内永不改变 |
| 典型用途 | HashMap/HashSet等基于内容的集合 | IdentityHashMap等身份敏感操作 |
三、关键差异深度解析
1. 重写行为对比(核心区别)
class CustomObject {
private int id;
// 重写hashCode(基于内容)
@Override
public int hashCode() {
return id * 31;
}
}
public static void main(String[] args) {
CustomObject obj = new CustomObject();
obj.id = 100;
System.out.println("hashCode: " + obj.hashCode()); // 3100
System.out.println("identityHashCode: " + System.identityHashCode(obj)); // 356573597
}
输出说明:
✅ hashCode()返回重写后的计算值
✅ identityHashCode()无视重写,返回JVM原始哈希
2. null安全性对比
Object obj = null;
// 抛出NullPointerException
try {
System.out.println(obj.hashCode());
} catch (NullPointerException e) {
System.out.println("调用hashCode()抛NPE");
}
// 安全返回0
System.out.println("identityHashCode(null): "
+ System.identityHashCode(obj));
3. 哈希码不变性验证
String str = "Hello";
int initialIdentity = System.identityHashCode(str);
str = str + " World!"; // 修改对象内容
// 身份哈希码保持不变
System.out.println(initialIdentity == System.identityHashCode(str)); // true
四、经典应用场景
1. 使用Object.hashCode()的场景
// 在HashMap中作为键(依赖内容哈希)
Map<Student, Grade> gradeMap = new HashMap<>();
Student s = new Student("2023001", "张三");
gradeMap.put(s, new Grade(90));
// 重写需遵守规范:内容相同则哈希码相同
class Student {
private String id;
private String name;
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
2. 使用identityHashCode()的场景
场景1:实现身份敏感的容器
// IdentityHashMap基于身份哈希而非内容
IdentityHashMap<Object, String> identityMap = new IdentityHashMap<>();
String s1 = new String("ABC");
String s2 = new String("ABC");
identityMap.put(s1, "第一对象");
identityMap.put(s2, "第二对象"); // 不同对象,均可插入
System.out.println(identityMap.size()); // 2
场景2:检测hashCode是否被重写
boolean isHashCodeOverridden(Object obj) {
return obj.hashCode() != System.identityHashCode(obj);
}
// 测试
System.out.println(isHashCodeOverridden(new Object())); // false
System.out.println(isHashCodeOverridden(new String("Test"))); // true
场景3:调试对象内存关系
Object objA = new Object();
Object objB = objA; // 指向同一对象
// 身份哈希相同证明是同一对象
System.out.println(System.identityHashCode(objA)
== System.identityHashCode(objB)); // true
五、底层机制揭秘
- 存储位置:身份哈希码存储在对象头中
- 生成时机:首次调用
hashCode()或identityHashCode()时生成 - 计算规则:通常基于内存地址,但JVM会优化(非直接地址)
- 不变性:一旦生成,在对象生命周期内永不改变
六、总结与最佳实践
| 方法 | 选用原则 |
|---|---|
Object.hashCode() | 需要基于对象内容的哈希逻辑时使用 |
System.identityHashCode() | 需要对象身份标识时使用 |
黄金准则:
- 当对象作为
HashMap等内容敏感容器的键时 → 重写hashCode()+equals() - 当需要对象身份标识(如调试、
IdentityHashMap)时 → 使用identityHashCode() - 永远不要在重写的
hashCode()中调用identityHashCode(),这违反哈希契约!
通过合理选择这两种哈希码,可以避免常见的
HashMap逻辑错误和身份混淆问题。理解它们的差异,将使你的Java代码更加健壮高效!
来源:juejin.cn/post/7519797197925367818
DeepSeek回答过于笼统,提示词如何优化
针对DeepSeek回答过于笼统的问题,可通过以下方法优化,使输出更具体、详细:
一、优化提示词设计
- 明确具体要求
在提问中嵌入「背景+限制+示例」,例如:
"作为跨境电商运营新手,请详细说明如何优化亚马逊产品标题(要求包含SEO关键词布局、字符数限制、禁用词清单,并给出3个具体案例)"。
- 强制结构化输出
使用模板化指令:
"请按以下框架回答:问题背景→核心原理→实施步骤→注意事项→参考案例"
或要求特定格式:
`"用带注释的Python代码演示数据清洗流程,每个步骤添加中文注释说明"。 - 动态调整抽象层级
通过关键词控制详细程度:
- 追加指令:
"请展开说明第三点中的用户画像构建方法" - 降低理解门槛:`"我是中学生,请用生活案例解释区块链技术"。
- 追加指令:
二、参数调整与功能设置
- 关键参数配置
- 提高
max_tokens至300-500(延长输出长度) - 设置
temperature=0.7(增强创造性,避免模板化)。
- 提高
- 启用深度思考模式
勾选界面左下角的「深度思考」选项,激活R1模型的专业分析能力,适合需要逻辑推导的复杂问题。 - 文件辅助增强
上传相关文档作为背景资料:
`"基于附件中的销售数据表,请逐月分析用户复购率变化趋势,并输出可视化图表建议"。
三、迭代优化技巧
- 追问细化
对笼统回答二次提问:
"请补充第一步'市场调研'中竞品分析的具体方法论""能否用表格对比方案A和方案B的优缺点?"。
- 对抗模糊话术
添加反制指令:
"避免概括性描述,需包含可量化的执行标准"
`"拒绝理论阐述,直接给出操作手册式指引"。 - 多模态输出引导
要求混合内容形式:
`"请结合流程图+代码片段+ bullet points 解释API对接流程"。
四、进阶解决方案
若常规方法仍不理想,可尝试:
- 本地部署R1模型
通过Ollama平台运行本地模型,配合Cherry Studio客户端的知识库功能,训练专属应答模板。 - API定制开发
在火山引擎API中设置system_prompt参数,预定义回答风格:
{"role":"system", "content":"你是一位擅长拆解复杂问题的经济学教授,回答需包含数学模型、现实案例和批判性思考"}
效果对比示例
| 原始提问 | 优化后提问 |
|---|---|
| "如何提升转化率?" | "作为护肤品电商运营,请制定小红书618促销转化率提升方案,要求:①分预热期/爆发期设计3种UGC玩法 ②ROI预估表格 ③规避平台限流的5个实操技巧" |
通过以上方法,可系统化解决回答笼统的问题。
来源:juejin.cn/post/7497075881467428873
Vue-Command-Component:让弹窗开发不再繁琐
前言
在Vue项目开发中,弹窗组件的管理一直是一个令人头疼的问题。传统的声明式弹窗开发方式需要管理大量的状态变量、处理复杂的props传递,甚至可能面临多个弹窗嵌套时的状态管理困境。今天给大家介绍一个能够彻底改变这种开发体验的库:Vue-Command-Component。
为什么需要命令式组件?
在传统的Vue开发中,弹窗的使用通常是这样的:
<template>
<el-dialog v-model="visible" title="提示">
<span>这是一段信息</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleConfirm">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
const visible = ref(false)
const handleConfirm = () => {
// 处理确认逻辑
visible.value = false
}
</script>
这种方式存在几个明显的问题:
- 需要手动管理弹窗的显示状态
- 组件代码和业务逻辑混杂在一起
- 多个弹窗时代码会变得非常臃肿
- 弹窗之间的嵌套关系处理复杂
Vue-Command-Component 解决方案
使用Vue-Command-Component,上述问题都可以得到优雅的解决。来看看它是如何使用的:
import { useDialog } from "@vue-cmd/element-plus";
const CommandDialog = useDialog()
// 直接调用函数显示弹窗
CommandDialog(<div>这是一段信息</div>)
是的,就是这么简单!一行代码就能唤起一个弹窗,不需要管理状态,不需要写模板,一切都变得如此流畅。
核心特性
1. 极简API设计
无需管理状态,一个函数调用搞定所有事情,符合直觉的开发体验。
2. 完整的类型支持
提供完整的TypeScript类型支持,开发体验一流。
3. 灵活的控制能力
提供了多种控制方式:
destroy:销毁弹窗hide/show:控制弹窗显示/隐藏destroyWithResolve/destroyWithReject:支持Promise风格的控制
4. 强大的扩展性
支持多种UI框架:
- Element Plus
- Naive UI
- Vant
- ...更多框架支持中
5. 原生特性支持
完整支持原生组件的所有特性:
- 属性传递
- 事件处理
- 插槽支持
- Provide/Inject
安装
# 使用 npm
npm install @vue-cmd/core @vue-cmd/element-plus
# 使用 yarn
yarn add @vue-cmd/core @vue-cmd/element-plus
# 使用 pnpm
pnpm add @vue-cmd/core @vue-cmd/element-plus
# 使用 bun
bun add @vue-cmd/core @vue-cmd/element-plus
实战示例
基础用法
import { useDialog } from "@vue-cmd/element-plus";
const CommandDialog = useDialog()
// 基础弹窗
CommandDialog(<Content />)
// 带配置的弹窗
CommandDialog(<Content />, {
attrs: {
title: '标题',
width: '500px'
}
})
嵌套弹窗
import { useDialog } from "@vue-cmd/element-plus";
const CommandDialog = useDialog()
CommandDialog(
<div onClick={() => {
// 在弹窗中打开新的弹窗
CommandDialog(<div>内层弹窗</div>)
}}>
外层弹窗
</div>
)
Promise风格控制
import { useDialog } from "@vue-cmd/element-plus";
import { useConsumer } from "@vue-cmd/core";
const CommandDialog = useDialog()
// 在弹窗组件内部
const FormComponent = defineComponent({
setup() {
const consumer = useConsumer()
const handleSubmit = (data) => {
// 提交数据后关闭弹窗
consumer.destroyWithResolve(data)
}
return () => <Form onSubmit={handleSubmit} />
}
})
// Promise风格的控制
try {
const result = await CommandDialog(<FormComponent />).promise
console.log('表单提交结果:', result)
} catch (error) {
console.log('用户取消或出错:', error)
}
多UI框架支持
// Element Plus
import { useDialog as useElementDialog } from "@vue-cmd/element-plus";
// Naive UI
import { useModal, useDrawer } from "@vue-cmd/naive";
// Vant
import { usePopup } from "@vue-cmd/vant";
const ElementDialog = useElementDialog()
const NaiveModal = useModal()
const VantPopup = usePopup()
// 使用不同的UI框架
ElementDialog(<Content />)
NaiveModal(<Content />)
VantPopup(<Content />)
写在最后
Vue-Command-Component 为Vue开发者带来了一种全新的弹窗开发方式。它不仅简化了开发流程,还提供了更强大的控制能力。如果你的项目中有大量弹窗交互,不妨尝试一下这个库,相信它会为你带来更好的开发体验。
相关链接
来源:juejin.cn/post/7501963430640615436
以赛育才,绽放创新之花:鲲鹏全面助力PAC2025巅峰对决
近日,第12届并行应用挑战赛(Parallel Application Challenge,简称“PAC2025”)在内蒙古鄂尔多斯圆满落幕。本届竞赛由全国信标委算力标准工作组指导,ACM中国高性能计算专家委员会与中国智能计算产业联盟联合主办,全球计算联盟高性能计算产发委协办,北京并行科技股份有限公司承办,吸引了来自全国各地高校、科研机构和产业界的200余支精英战队参与角逐。鲲鹏作为大赛的独家算力基座和核心技术支持平台,全程赋能赛事,助力打造了一场汇聚智慧与创新的并行计算盛会。

自2024年起,PAC大赛全面基于鲲鹏硬件平台,开创了“以赛促研、以赛促用、以赛育才”的自主算力生态新阶段。参赛作品基于鲲鹏处理器及KUPL并行编程框架进行代码优化与应用研发,切实推动了鲲鹏平台生态的融合与高性能计算算法的创新突破。
本届大赛最终有27支顶级战队挺进决赛,在应用赛道和优化赛道上涌现出兼具技术深度与产业价值的优秀作品。这些作品广泛应用于气象模拟、生物医药、大模型推理等关键领域,实现了显著的性能提升。通过实战检验,鲲鹏凭借其多核异构设计、高并发低功耗优势及完善的开发工具链,为参赛选手提供了稳定、高效的技术支持。选手们深度挖掘鲲鹏片上内存资源,结合向量化计算、矩阵计算、内存绑定、任务调度优化等先进技术,实现了性能的突破,充分体现了鲲鹏平台“从可用、好用到广泛用”的技术成熟度。
PAC2025的成功举办不仅展示了鲲鹏作为自主创新算力基座的强大实力,也标志着国内算力生态正从“追赶”迈向“引领”。这不仅是一场高水平的技术竞赛,更是一次凝聚行业共识、培育创新人才、推动产业升级的重要里程碑。
展望未来,鲲鹏将继续携手高校、科研机构及产业伙伴,深化“产学研用”深度融合,持续加大人才培养和生态建设投入,为我国高性能计算事业注入源源不断的强劲动力,共同迎接智能时代的到来。
以下为本届竞赛获奖名单:
附:












