前端如何实现图片伪防盗链,保护页面图片
在前端开发中,实现图片防盗链通常涉及到与后端服务器的交互,因为防盗链机制主要是在服务器端实现的。然而,前端也可以采取一些措施来增强图片保护,并与服务器端的防盗链策略配合使用。以下是前端可以采用的一些方法:
一、使用 Token 保护图片资源
- 动态生成 Token
在用户请求图片时,可以在前端生成一个包含时间戳的 token,然后将其附加到图片 URL 中。这个 token 可以在服务器端验证。
前端代码示例(使用 JavaScript):
// 生成当前时间戳作为 token
function generateToken() {
return Date.now();
}
// 获取图片 URL
function getImageUrl() {
const token = generateToken();
return `https://example.com/images/photo.jpg?token=${token}`;
}
// 设置图片 src
document.getElementById('image').src = getImageUrl();
解释:
generateToken()
函数生成一个时间戳作为 token。getImageUrl()
函数将 token 附加到图片 URL 中,以便进行验证。
- 在图片请求中使用 Token
在图片加载时,确保 URL 中包含有效的 token。前端可以在页面加载时动态设置图片 URL。
前端代码示例(使用 Vue.js):
<template>
<img :src="imageUrl" alt="Protected Image" />
</template>
<script>
export default {
data() {
return {
imageUrl: ''
};
},
methods: {
generateToken() {
return Date.now(); // 或使用其他方法生成 token
}
},
created() {
const token = this.generateToken();
this.imageUrl = `https://example.com/images/photo.jpg?token=${token}`;
}
};
</script>
解释:
- 使用 Vue 的生命周期钩子
created
来生成 token 并设置图片 URL。
- 使用 Vue 的生命周期钩子
二、设置图片加载控制
- 防止右键下载
在前端,你可以通过 CSS 或 JavaScript 来禁用图片的右键菜单,从而防止用户通过右键菜单下载图片。
前端代码示例(使用 CSS):
<style>
.no-right-click {
pointer-events: none;
}
</style>
<img class="no-right-click" src="https://example.com/images/photo.jpg" alt="Protected Image" />
前端代码示例(使用 JavaScript):
document.addEventListener('contextmenu', function (e) {
if (e.target.tagName === 'IMG') {
e.preventDefault();
}
});
解释:
- 使用 CSS 属性
pointer-events: none
来禁用右键菜单。 - 使用 JavaScript 事件监听器来阻止右键菜单弹出。
- 使用 CSS 属性
- 使用水印
在图片上添加水印是另一种保护图片的方式。前端可以通过 Canvas 绘制水印,但通常这在图片生成或处理阶段进行更为合适。
前端代码示例(使用 Canvas):
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://example.com/images/photo.jpg';
img.onload = function() {
ctx.drawImage(img, 0, 0);
ctx.font = '30px Arial';
ctx.fillStyle = 'red';
ctx.fillText('Watermark', 10, 50);
};
</script>
解释:
- 使用 Canvas 绘制图片并添加水印文本。
三、与服务器端防盗链机制配合
- 验证 Referer
在前端代码中,可以通过设置
Referer
头(这通常由浏览器自动处理)来帮助服务器验证请求来源。
前端代码示例(使用 Fetch API):
fetch('https://example.com/images/photo.jpg', {
method: 'GET',
headers: {
'Referer': 'https://yourwebsite.com'
}
}).then(response => response.blob())
.then(blob => {
const url = URL.createObjectURL(blob);
document.getElementById('image').src = url;
});
解释:
- 使用
fetch
请求图片,手动设置Referer
头部(尽管大多数浏览器自动设置)。
- 使用
总结
前端在实现图片防盗链方面,主要通过动态生成 Token、设置图片加载控制(如禁用右键菜单和添加水印)以及与服务器端防盗链机制配合来保护图片资源。虽然真正的防盗链逻辑通常是在服务器端实现,但前端可以采取这些措施来增强保护效果。结合前端和后端的策略,可以有效地防止未经授权的图片访问和盗用。
来源:juejin.cn/post/7410224960298041394
【算法】最小覆盖子串
难度:困难
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.length
n == t.length
1 <= m, n <= 105
s
和t
由英文字母组成
解题思路:
- 初始化计数器:创建两个哈希表,一个用于存储字符串t中每个字符的出现次数,另一个用于存储当前窗口内每个字符的出现次数。
- 设定窗口:初始化窗口的左边界left和右边界right,以及一些辅助变量如required(表示t中不同字符的数量)、formed(表示当前窗口内满足t字符要求的数量)、windowCounts(表示当前窗口内字符的计数)。
- 扩展窗口:将right指针从0开始向右移动,直到窗口包含了t中的所有字符。在每次移动right指针时,更新窗口内的字符计数和formed变量。
- 收缩窗口:一旦窗口包含了t中的所有字符,开始移动left指针以尝试缩小窗口,同时更新窗口内的字符计数和formed变量。记录下最小覆盖子串的信息。
- 重复步骤3和4:继续移动right指针,重复上述过程,直到right指针到达s的末尾。
- 返回结果:最后返回最小覆盖子串。
JavaScript实现:
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
const need = {}, windowCounts = {};
let left = 0, right = 0;
let valid = 0;
let start = 0, length = Infinity;
// Initialize the need counter with characters from t.
for(let c of t){
need[c] ? need[c]++ : need[c] = 1;
}
// Function to check if the current window satisfies the need.
const is_valid = () => Object.keys(need).every(c => (windowCounts[c] || 0) >= need[c]);
while(right < s.length){
const c = s[right];
right++;
// Increment the count in the windowCounts if the character is needed.
if(need[c]){
windowCounts[c] ? windowCounts[c]++ : windowCounts[c] = 1;
if(windowCounts[c] === need[c])
valid++;
}
// If the current window is valid, try to shrink it from the left.
while(valid === Object.keys(need).length){
if(right - left < length){
start = left;
length = right - left;
}
const d = s[left];
left++;
// Decrement the count in the windowCounts if the character is needed.
if(need[d]){
if(windowCounts[d] === need[d])
valid--;
windowCounts[d]--;
}
}
}
return length === Infinity ? '' : s.substring(start, start + length);
};、
来源:juejin.cn/post/7410299130280722470
如何去实现浏览器多窗口互动
前段时间看到了一张神奇的 gif,如下:
感觉特别不可思议,而且是本地运行的环境,于是想自己实现一个但是碍于自己太菜了缺乏对球体、粒子和物理的3D技能,然后去了解了一下如何使一个窗口对另一个窗口的位置做出反应。
于是我做了一个极简的丑陋的版本:
首先,我们看一下在多个客户端之间共享信息的所有方法:
1. 服务器
显然,拥有服务器(使用轮询或Websockets)会简化问题。然而,我们能不能在不使用服务器的情况下去实现呢?
2. 本地存储
本地存储本质上是一个浏览器键值存储,通常用于在浏览器会话之间保持信息的持久性。虽然通常用于存储身份验证令牌或重定向URL,但它可以存储任何可序列化的内容。可以在这里了解更多信息。
最近发现了一些有趣的本地存储API,包括storage
事件,该事件在同一网站的另一个会话更改本地存储时触发。
我们可以通过将每个窗口的状态存储在本地存储中来利用这一点。每当一个窗口改变其状态时,其他窗口将通过存储事件进行更新。
这是我最初的想法,但是后来发现还有其他的方式可以实现
3. 共享 Workers
简单来说,Worker本质上是在另一个线程上运行的第二个脚本。虽然它们没有访问DOM,因为它们存在于HTML文档之外,但它们仍然可以与您的主脚本通信。 它们主要用于通过处理后台作业来卸载主脚本,比如预取信息或处理诸如流式日志和轮询之类的较不重要的任务。
我这有一篇关于web Worker 的文章 没了解过的可以先去看看。
共享的 Workers 是一种特殊类型的 WebWorkers,可以与多个相同脚本的实例通信。
4. 建立 Workers
我使用的是Vite和TypeScript,所以我需要一个worker.ts
文件,并将@types/sharedworker
作为开发依赖进行安装。我们可以使用以下语法在我的主脚本中创建连接:
new SharedWorker(new URL("worker.ts", import.meta.url));
接下来需要考虑的就是以下几方面:
- 确定每个窗口
- 跟踪所有窗口的状态
- 当一个窗口改变其状态时,通知其他窗口重新绘制
type WindowState = {
screenX: number; // window.screenX
screenY: number; // window.screenY
width: number; // window.innerWidth
height: number; // window.innerHeight
};
最关键的信息是window.screenX
和window.screenY
,因为它们可以告诉我们窗口相对于显示器左上角的位置。
将有两种类型的消息:
- 每个窗口在改变状态时,将发布一个
windowStateChanged
消息,带有其新状态。 - 工作者将向所有其他窗口发送更新,以通知它们其中一个已更改。工作者将使用sync消息发送所有窗口的状态。
// worker.ts
let windows: { windowState: WindowState; id: number; port: MessagePort }[] = [];
onconnect = ({ ports }) => {
const port = ports[0];
port.onmessage = function (event: MessageEvent ) {
console.log("We'll do something");
};
};
我们与 SharedWorker
的基本连接将如下所示。我编写了一些基本函数来生成一个ID,并计算当前窗口状态,同时我对我们可以使用的消息类型进行了一些类型定义,称为 WorkerMessage
:
// main.ts
import { WorkerMessage } from "./types";
import {
generateId,
getCurrentWindowState,
} from "./windowState";
const sharedWorker = new SharedWorker(new URL("worker.ts", import.meta.url));
let currentWindow = getCurrentWindowState();
let id = generateId();
一旦启动应用程序,应该立即通知工作者有一个新窗口,因此需要发送一条消息:
// main.ts
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow: currentWindow,
},
} satisfies WorkerMessage);
然后可以在工作者端监听此消息,并相应地更改 onmessage
。基本上,一旦接收到 windowStateChanged
消息,它要么是一个新窗口,我们将其追加到状态中,要么是一个旧窗口发生了变化。然后,我们应该通知所有窗口状态已经改变:
// worker.ts
port.onmessage = function (event: MessageEvent ) {
const msg = event.data;
switch (msg.action) {
case "windowStateChanged": {
const { id, newWindow } = msg.payload;
const oldWindowIndex = windows.findIndex((w) => w.id === id);
if (oldWindowIndex !== -1) {
// old one changed
windows[oldWindowIndex].windowState = newWindow;
} else {
// new window
windows.push({ id, windowState: newWindow, port });
}
windows.forEach((w) =>
// send sync here
);
}
break;
}
};
要发送同步消息,实际上我需要一个小技巧,因为“port
”属性无法被序列化,所以我将其转换为字符串,然后再解析回来。因为我比较懒,我不会只是将窗口映射到一个更可序列化的数组:
w.port.postMessage({
action: "sync",
payload: { allWindows: JSON.parse(JSON.stringify(windows)) },
} satisfies WorkerMessage);
接下来就是绘制内容了。
5. 使用Canvas 绘图
在每个窗口的中心画一个圆圈,并用一条线连接这些圆圈,将使用 HTML Canvas
进行绘制
const drawCenterCircle = (ctx: CanvasRenderingContext2D, center: Coordinates) => {
const { x, y } = center;
ctx.strokeStyle = "#eeeeee";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(x, y, 100, 0, Math.PI * 2, false);
ctx.stroke();
ctx.closePath();
};
要绘制线条,需要进行一些数学计算(我保证,不是很多 🤓),将另一个窗口中心的相对位置转换为当前窗口上的坐标。 基本上,正在改变基底。使用以下数学公式来实现这个功能。首先,将更改基底,使坐标位于显示器上,并通过当前窗口的 screenX/screenY
进行偏移。
const baseChange = ({
currentWindowOffset,
targetWindowOffset,
targetPosition,
}: {
currentWindowOffset: Coordinates;
targetWindowOffset: Coordinates;
targetPosition: Coordinates;
}) => {
const monitorCoordinate = {
x: targetPosition.x + targetWindowOffset.x,
y: targetPosition.y + targetWindowOffset.y,
};
const currentWindowCoordinate = {
x: monitorCoordinate.x - currentWindowOffset.x,
y: monitorCoordinate.y - currentWindowOffset.y,
};
return currentWindowCoordinate;
};
现在有了相同相对坐标系上的两个点,可以画线了!
const drawConnectingLine = ({
ctx,
hostWindow,
targetWindow,
}: {
ctx: CanvasRenderingContext2D;
hostWindow: WindowState;
targetWindow: WindowState;
}) => {
ctx.strokeStyle = "#ff0000";
ctx.lineCap = "round";
const currentWindowOffset: Coordinates = {
x: hostWindow.screenX,
y: hostWindow.screenY,
};
const targetWindowOffset: Coordinates = {
x: targetWindow.screenX,
y: targetWindow.screenY,
};
const origin = getWindowCenter(hostWindow);
const target = getWindowCenter(targetWindow);
const targetWithBaseChange = baseChange({
currentWindowOffset,
targetWindowOffset,
targetPosition: target,
});
ctx.strokeStyle = "#ff0000";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(origin.x, origin.y);
ctx.lineTo(targetWithBaseChange.x, targetWithBaseChange.y);
ctx.stroke();
ctx.closePath();
};
现在,只需要对状态变化做出反应即可。
// main.ts
sharedWorker.port.onmessage = (event: MessageEvent ) => {
const msg = event.data;
switch (msg.action) {
case "sync": {
const windows = msg.payload.allWindows;
ctx.reset();
drawMainCircle(ctx, center);
windows
.forEach(({ windowState: targetWindow }) => {
drawConnectingLine({
ctx,
hostWindow: currentWindow,
targetWindow,
});
});
}
}
};
最后一步,只需要定期检查窗口是否发生了变化,如果是,则发送一条消息。
setInterval(() => {setInterval(() => {
const newWindow = getCurrentWindowState();
if (
didWindowChange({
newWindow,
oldWindow: currentWindow,
})
) {
sharedWorker.port.postMessage({
action: "windowStateChanged",
payload: {
id,
newWindow,
},
} satisfies WorkerMessage);
currentWindow = newWindow;
}
}, 100);
来源:juejin.cn/post/7329753721018269711
领导问我:为什么一个点赞功能你做了五天?
公众号:【可乐前端】,每天3分钟学习一个优秀的开源项目,分享web面试与实战知识。
前言
可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端部分用的技术栈是 nest.js
。
某一个周一,领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系。
交代完之后,领导就去出差了。等领导回来时已是周五,他问可乐:这期的需求进展如何?
可乐回答:点赞的需求我做完了,其他的还没开始。
领导生气的说:为什么点赞这样的一个小功能你做了五天才做完???
可乐回答:领导息怒。。请听我细细道来
往期文章
- 切图仔做全栈:React&Nest.js 社区平台(一)——基础架构与邮箱注册、JWT 登录实现
- 切图仔做全栈:React&Nest.js社区平台(二)——👋手把手实现优雅的鉴权机制
- React&Nest.js全栈社区平台(三)——🐘对象存储是什么?为什么要用它?
- React&Nest.js社区平台(四)——✏️文章发布与管理实战
- React&Nest.js全栈社区平台(五)——👋封装通用分页Service实现文章流与详情
初步设计
对于上面的这个需求,我们提炼出来有三点最为重要的功能:
- 获取点赞总数
- 获取用户的点赞关系
- 点赞/取消点赞
所以这里容易想到的是在文章表中冗余一个点赞数量字段 likes
,查询文章的时候一起把点赞总数带出来。
id | content | likes |
---|---|---|
1 | 文章A | 10 |
2 | 文章B | 20 |
然后建一张 article_lile_relation
表,建立文章点赞与用户之间的关联关系。
id | article_id | user_id | value |
---|---|---|---|
1 | 1001 | 2001 | 1 |
2 | 1001 | 2002 | 0 |
上面的数据就表明了 id
为 2001
的用户点赞了 id
为 1001
的文章; id
为 2002
的用户对 id
为 1001
的文章取消了点赞。
这是对于这种关联关系需求最容易想到的、也是成本不高的解决方案,但在仔细思考了一番之后,我放弃了这种方案。原因如下:
- 由于首页文章流中也需要展示用户的点赞关系,这里获取点赞关系需要根据当前文章
id
、用户id
去联表查询,会增加数据库的查询压力。 - 有关于点赞的信息存放在两张表中,需要维护两张表的数据一致性。
- 后续可能会出现对摸鱼帖子点赞、对用户点赞、对评论点赞等需求,这样的设计方案显然拓展性不强,后续再做别的点赞需求时可能会出现大量的重复代码。
基于上面的考虑,准备设计一个通用的点赞模块,以拓展后续各种业务的点赞需求。
表设计
首先来一张通用的点赞表, DDL
语句如下:
CREATE TABLE `like_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`target_id` int(11) DEFAULT NULL,
`type` int(4) DEFAULT NULL,
`created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`value` int(4) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `like_records_target_id_IDX` (`target_id`,`user_id`,`type`) USING BTREE,
KEY `like_records_user_id_IDX` (`user_id`,`target_id`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
解释一下上面各个字段的含义:
id
:点赞记录的主键id
user_id
:点赞用户的id
target_id
:被点赞的文章id
type
:点赞类型:可能有文章、帖子、评论等value
:是否点赞,1
点赞,0
取消点赞created_time
:创建时间updated_time
:更新时间
前置知识
在设计好数据表之后,再来捋清楚这个业务的一些特定属性与具体实现方式:
- 我们可以理解这是一个相对来说读比写多的需求,比如你看了
10
篇掘金的文章,可能只会对1
篇文章点赞 - 应该设计一个通用的点赞模块,以供后续各种点赞需求的接入
- 点赞数量与点赞关系需要频繁地获取,所以需要读缓存而不是读数据库
- 写入数据库与同步缓存需考虑数据一致性
所以可乐针对这样的业务特性上网查找了一些资料,发现有一些前置知识是他所欠缺的,我们一起来看看。
mysql事务
mysql
的事务是指一系列的数据库操作,这些操作要么全部成功执行,要么全部失败回滚。事务是用来确保数据库的完整性、一致性和持久性的机制之一。
在 mysql
中,事务具有以下四个特性,通常缩写为 ACID
:
- 原子性: 事务是原子性的,这意味着事务中的所有操作要么全部成功执行,要么全部失败回滚。
- 一致性: 事务执行后,数据库从一个一致的状态转换到另一个一致的状态。这意味着事务执行后,数据库中的数据必须满足所有的约束、触发器和规则,保持数据的完整性。
- 隔离性: 隔离性指的是多个事务之间的相互独立性。即使有多个事务同时对数据库进行操作,它们之间也不会相互影响,每个事务都感觉到自己在独立地操作数据库。
mysql
通过不同的隔离级别(如读未提交、读已提交、可重复读和串行化)来控制事务之间的隔离程度。 - 持久性: 持久性指的是一旦事务被提交,对数据库的改变将永久保存,即使系统崩溃也不会丢失。
mysql
通过将事务的提交写入日志文件来保证持久性,以便在系统崩溃后能够恢复数据。
这里以商品下单创建订单并扣除库存为例,演示一下 nest+typeorm
中的事务如何使用:
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { Order } from './order.entity';
import { Product } from './product.entity';
@Injectable()
export class OrderService {
constructor(
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}
async createOrderAndDeductStock(productId: number, quantity: number): Promise<Order> {
return await this.entityManager.transaction(async transactionalEntityManager => {
// 查找产品并检查库存是否充足
const product = await transactionalEntityManager.findOne(Product, productId);
if (!product || product.stock < quantity) {
throw new Error('Product not found or insufficient stock');
}
// 创建订单
const order = new Order();
order.productId = productId;
order.quantity = quantity;
await transactionalEntityManager.save(order);
// 扣除库存
product.stock -= quantity;
await transactionalEntityManager.save(product);
return order;
});
}
}
this.entityManager.transaction
创建了一个事务,在异步函数中,如果发生错误, typeorm
会自动回滚事务;如果没有发生错误,typeorm
会自动提交事务。
在这个实例中,尝试获取库存并创建订单和减库存,如果任何一个地方出错异常抛出,则事务就会回滚,这样就保证了多表间数据的一致性。
分布式锁
分布式锁是一种用于在分布式系统中协调多个节点并保护共享资源的机制。在分布式系统中,由于涉及多个节点并发访问共享资源,因此需要一种机制来确保在任何给定时间只有一个节点能够访问或修改共享资源,以防止数据不一致或竞争条件的发生。
对于同一个用户对同一篇文章频繁的点赞/取消点赞请求,可以加分布式锁的机制,来规避一些问题:
- 防止竞态条件: 点赞/取消点赞操作涉及到查询数据库、更新数据库和更新缓存等多个步骤,如果不加锁,可能会导致竞态条件,造成数据不一致或错误的结果。
- 保证操作的原子性: 使用分布式锁可以确保点赞/取消点赞操作的原子性,即在同一时间同一用户只有一个请求能够执行操作,从而避免操作被中断或不完整的情况发生。
- 控制并发访问: 加锁可以有效地控制并发访问,限制了频繁点击发送请求的数量,从而减少系统负载和提高系统稳定性。
在 redis
中实现分布式锁通常使用的是基于 SETNX
命令和 EXPIRE
命令的方式:
- 使用
SETNX
命令尝试将lockKey
设置为lockValue
,如果lockKey
不存在,则设置成功并返回1
;如果lockKey
已经存在,则设置失败并返回0
。 - 如果
SETNX
成功,说明当前客户端获得了锁,可以执行相应的操作;如果SETNX
失败,则说明锁已经被其他客户端占用,当前客户端需要等待一段时间后重新尝试获取锁。 - 为了避免锁被永久占用,可以使用
EXPIRE
命令为锁设置一个过期时间,确保即使获取锁的客户端在执行操作时发生故障,锁也会在一定时间后自动释放。
async getLock(key: string) {
const res = await this.redis.setnx(key, 'lock');
if (res) {
// 10秒锁过期
await this.redis.expire(key, 10);
}
return res;
}
async unLock(key: string) {
return this.del(key);
}
redis中的set结构
redis
中的 set
是一种无序集合,用于存储多个不重复的字符串值,set
中的每个成员都是唯一的。
我们存储点赞关系的时候,需要用到 redis
中的 set
结构,存储的 key
与 value
如下:
article_1001:[uid1,uid2,uid3]
这就表示文章 id
为 1001
的文章,有用户 id
为 uid1
、 uid2
、 uid3
这三个用户点赞了。
常用的 set
结构操作命令包括:
SADD key member [member ...]
: 将一个或多个成员加入到集合中。SMEMBERS key
: 返回集合中的所有成员。SISMEMBER key member
: 检查成员是否是集合的成员。SCARD key
: 返回集合元素的数量。SREM key member [member ...]
: 移除集合中一个或多个成员。SPOP key [count]
: 随机移除并返回集合中的一个或多个元素。SRANDMEMBER key [count]
: 随机返回集合中的一个或多个元素,不会从集合中移除元素。SUNION key [key ...]
: 返回给定所有集合的并集。SINTER key [key ...]
: 返回给定所有集合的交集。SDIFF key [key ...]
: 返回给定所有集合的差集。
下面举几个点赞场景的例子
- 当用户
id
为uid1
给文章id
为1001
的文章点赞时:sadd 1001 uid1
- 当用户
id
为uid1
给文章id
为1001
的文章取消点赞时:srem 1001 uid1
- 当需要获取文章
id
为1001
的点赞数量时:scard 1001
redis事务
在 redis
中,事务是一组命令的有序序列,这些命令在执行时会被当做一个单独的操作来执行。即事务中的所有命令要么全部执行成功,要么全部执行失败,不存在部分执行的情况。
以下是 redis
事务的主要命令:
- MULTI: 开启事务,在执行
MULTI
命令后,后续输入多个命令来组成一个事务。 - EXEC: 执行事务,在执行
EXEC
命令时,redis
会执行客户端输入的所有事务命令,如果事务中的所有命令都执行成功,则事务执行成功,返回事务中所有命令的执行结果;如果事务中的某个命令执行失败,则事务执行失败,返回空。 - DISCARD: 取消事务,在执行
DISCARD
命令时,redis
会取消当前事务中的所有命令,事务中的命令不会被执行。 - WATCH: 监视键,在执行
WATCH
命令时,redis
会监听一个或多个键,如果在执行事务期间任何被监视的键被修改,事务将会被打断。
比如说下面的代码给集合增加元素,并更新集合的过期时间,可以如下使用 redis
的事务去执行它:
const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();
流程图设计
在了解完这些前置知识之后,可乐开始画一些实现的流程图。
首先是点赞/取消点赞接口的流程图:
简单解释下上面的流程图:
- 先尝试获取锁,获取不到的时候等待重试,保证接口与数据的时序一致。
- 判断这个点赞关系是否已存在,比如说用户对这篇文章已经点过赞,其实又来了一个对此篇文章点赞的请求,直接返回失败
- 开启
mysql
的事务,去更新点赞信息表,同时尝试去更新缓存,在缓存更新的过程中,会有3次的失败重试机会,如果缓存更新都失败,则回滚mysql事务;整体更新失败 mysql
更新成功,缓存也更新成功,则整个操作都成功
然后是获取点赞数量和点赞关系的接口
简单解释下上面的流程图:
- 首先判断当前文章
id
对应的点赞关系是否在redis
中存在,如果存在,则直接从缓存中读取并返回 - 如果不存在,此时加锁,准备读取数据库并更新
redis
,这里加锁的主要目的是防止大量的请求一下子打到数据库中。 - 由于加锁的时候,可能很多接口已经在等待,所以在锁释放的时候,再加多一次从
redis
中获取的操作,此时redis
中已经有值,可以直接从缓存中读取。
代码实现
在所有的设计完毕之后,可以做最后的代码实现了。分别来实现点赞操作与点赞数量接口。这里主要关注 service
层的实现即可。
点赞/取消点赞接口
async toggleLike(params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
}) {
const { userId, targetId, type, value } = params;
const LOCK_KEY = `${userId}::${targetId}::${type}::toggleLikeLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.toggleLike(params);
}
const record = await this.likeRepository.findOne({
where: { userId, targetId, type },
});
if (record && record.value === value) {
await this.redisService.unLock(LOCK_KEY);
throw Error('不可重复操作');
}
await this.entityManager.transaction(async (transactionalEntityManager) => {
if (!record) {
const likeEntity = new LikeEntity();
likeEntity.targetId = targetId;
likeEntity.type = type;
likeEntity.userId = userId;
likeEntity.value = value;
await transactionalEntityManager.save(likeEntity);
} else {
const id = record.id;
await transactionalEntityManager.update(LikeEntity, { id }, { value });
}
const isSuccess = await this.tryToFreshCache(params);
if (!isSuccess) {
await this.redisService.unLock(LOCK_KEY);
throw Error('操作失败');
}
});
await this.redisService.unLock(LOCK_KEY);
return true;
}
private async tryToFreshCache(
params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
},
retry = 3,
) {
if (retry === 0) {
return false;
}
const { targetId, type, value, userId } = params;
try {
const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();
return true;
} catch (error) {
console.log('tryToFreshCache error', error);
await wait();
return this.tryToFreshCache(params, retry - 1);
}
}
可以参照流程图来看这部分实现代码,基本实现就是使用 mysql
事务去更新点赞信息表,然后去更新 redis
中的点赞信息,如果更新失败则回滚事务,保证数据的一致性。
获取点赞数量、点赞关系接口
async getLikes(params: {
targetId: number;
type: ELikeType;
userId: number;
}) {
const { targetId, type, userId } = params;
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (!cacheExsit) {
await this.getLikeFromDbAndSetCache(params);
}
const count = await this.redisService.getSetLength(setKey);
const isLike = await this.redisService.isMemberOfSet(setKey, userId);
return { count, isLike };
}
private async getLikeFromDbAndSetCache(params: {
targetId: number;
type: ELikeType;
userId: number;
}) {
const { targetId, type, userId } = params;
const LOCK_KEY = `${targetId}::${type}::getLikesLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.getLikeFromDbAndSetCache(params);
}
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (cacheExsit) {
await this.redisService.unLock(LOCK_KEY);
return true;
}
const data = await this.likeRepository.find({
where: {
targetId,
userId,
type,
value: ELike.LIKE,
},
select: ['userId'],
});
if (data.length !== 0) {
await this.redisService.setAdd(
setKey,
data.map((item) => item.userId),
this.ttl,
);
}
await this.redisService.unLock(LOCK_KEY);
return true;
}
由于读操作相当频繁,所以这里应当多使用缓存,少查询数据库。读点赞信息时,先查 redis
中有没有,如果没有,则从 mysql
同步到 redis
中,同步的过程中也使用到了分布式锁,防止一开始没缓存时请求大量打到 mysql
。
同时,如果所有文章的点赞信息都同时存在 redis
中,那 redis
的存储压力会比较大,所以这里会给相关的 key
设置一个过期时间。当用户重新操作点赞时,会更新这个过期时间。保障缓存的数据都是相对热点的数据。
通过组装数据,获取点赞信息的返回数据结构如下:
返回一个 map
,其中 key
文章 id
, value
里面是该文章的点赞数量以及当前用户是否点赞了这篇文章。
前端实现
文章流列表发生变化的时候,可以监听列表的变化,然后去获取点赞的信息:
useEffect(() => {
if (!article.list) {
return;
}
const shouldGetLikeIds = article.list
.filter((item: any) => !item.likeInfo)
.map((item: any) => item.id);
if (shouldGetLikeIds.length === 0) {
return;
}
console.log("shouldGetLikeIds", shouldGetLikeIds);
getLikes({
targetIds: shouldGetLikeIds,
type: 1,
}).then((res) => {
const map = res.data;
const newList = [...article.list];
for (let i = 0; i < newList.length; i++) {
if (!newList[i].likeInfo && map[newList[i].id]) {
newList[i].likeInfo = map[newList[i].id];
}
}
const newArticle = { ...article };
newArticle.list = newList;
setArticle(newArticle);
});
}, [article]);
点赞操作的时候前端也需要加锁,接口执行完毕了再把锁释放。
<Space
onClick={(e) => {
e.stopPropagation();
if (lockMap.current?.[item.id]) {
return;
}
lockMap.current[item.id] = true;
const oldValue = item.likeInfo.isLike;
const newValue = !oldValue;
const updateValue = (value: any) => {
const newArticle = { ...article };
const newList = [...newArticle.list];
const current = newList.find(
(_) => _.id === item.id
);
current.likeInfo.isLike = value;
if (value) {
current.likeInfo.count++;
} else {
current.likeInfo.count--;
}
setArticle(newArticle);
};
updateValue(newValue);
toggleLike({
targetId: item.id,
value: Number(newValue),
type: 1,
})
.catch(() => {
updateValue(oldValue);
})
.finally(() => {
lockMap.current[item.id] = false;
});
}}
>
<LikeOutlined
style={
item.likeInfo.isLike ? { color: "#1677ff" } : {}
}
/>
{item.likeInfo.count}
</Space>
解释
可乐:从需求分析考虑、然后研究网上的方案并学习前置知识,再是一些环境的安装,最后才是前后端代码的实现,领导,我这花了五天不过份吧。
领导(十分无语):我们平台本来就没几个用户、没几篇文章,本来就是一张关联表就能解决的问题,你又搞什么分布式锁又搞什么缓存,还花了那么多天时间。我不管啊,剩下没做的需求你得正常把它正常做完上线,今天周五,周末你也别休息了,过来加班吧。
最后
以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~
来源:juejin.cn/post/7349437605858066443
2024字节跳动“安全范儿”高校挑战赛报名开启,三大赛道等你来战!
近日,第五届字节跳动“安全范儿”高校挑战赛正式开启。赛事由字节跳动安全与风控团队主办,设置三大赛道,为高校人才提供多样化的能力展示平台和竞技之路。
“安全范儿”高校挑战赛自2019年首届举办以来,共吸引了10,000+战队、近30,000名网安高手同台竞技,为高校新生力量进入职场打好前站,开启向上成长新空间。
本届“安全范儿”高校挑战赛8月29日开启报名,面向全日制高校本科生/研究生,不限专业与院校(可跨校组队),累计提供80多万元专项奖励,分ByteCTF大师赛、ByteAI安全挑战赛、ByteHACK三大赛道。
ByteCTF大师赛为定向邀请制,赛题深度结合字节跳动业务场景,覆盖AI、Web、逆向工程等7大方向,助力选手提升工程实战能力和思考深度。ByteAI赛道聚焦大模型安全,考察选手如何在大模型时代进行创新性攻击,以“攻击突破”促进大模型防御。ByteHACK赛道为选手提供丰富且真实的漏洞挖掘场景,单个漏洞最高可获得200,000元奖金。
本次大赛也得到北京理工大学、北京邮电大学、电子科技大学、福州大学、华中科技大学、南京大学、上海交通大学、西安交通大学、中国人民大学等23所国内高校的大力支持。“安全范儿”高校挑战赛相关负责人表示,希望通过搭建学术与工业的沟通桥梁,助力产学研高效合作。(作者:李双)
收起阅读 »很多人不懂什么是优势
大家好,我卡颂。
上周和10个左右同学交流了如何发展第二曲线,他们中有国企员工、宝妈、4S店管理、程序员...
在交流中,我发现个普遍现象 —— 应试教育以及职场带来的思维惯势,对人的影响非常大,最直观的影响就是 —— 很多人不懂什么是优势。
如果你一路从学校到职场,直到最近才由于各种原因(比如裁员压力、内卷、年龄焦虑...)考虑发展第二曲线,那这篇文章也许会改变你对优势的固有认知。
说真的,什么是优势?
学校与公司都是服务于具体目的的组织:
- 学校:服务于升学,表现形式是学习
- 公司:服务于盈利,表现形式是工作
所以,在学校与公司的语境下,优势通常指那些能为达成具体目的提供帮助的能力或性格,比如:
- 学校最看重应试能力,因为这跟升学息息相关
- 公司鼓励争强好胜,因为这跟盈利相关
那些与达成具体目的关联不大,甚至背道而驰的能力或性格,就会被冠以劣势的名头。
比如在公司中,不善沟通、讨好型人格就是劣势。
我们会发现,决定一个能力或性格是否是优势,取决于为了达成什么目的。
但由于长久以来的思维惯势,很多人已经慢慢忽略了为了达成什么目的这个前缀。
他们已经习惯性认为不善沟通、讨好型人格就是劣势,不管什么情况下都是劣势。
这就是思维惯势对我们带来的第一个影响 —— 不考虑目的的情况下否定自己的能力与性格。
让我们暂停思考一下 —— 当我在表达上述观点时,你有没有觉察到什么不对劲的地方?
在上面的观点中,我提到 —— 优势是那些“能为达成具体目的提供帮助的能力或性格”。
优势是能力或性格,这个表述你觉得有问题么?
在学校或公司中,这种表述是没问题的,因为在这样的组织内,个体都是螺丝钉,只要发挥你的能力或性格做好本职工作即可。
比如,一个程序员可能只看重技术能力而不看重运营能力,因为本职工作不需要后者。
这就是思维惯势对我们带来的第二个影响 —— 认为优势是单一维度的(能力或性格)。
当考虑第二曲线时
当考虑发展第二曲线时,上述两个影响会对我们的选择造成不利的结果。
带着优势是单一维度的思维惯势,我们思考第二曲线的模式会倾向于 —— 我有什么比别人厉害的能力?
再结合第一条思维惯势不考虑目的的情况下否定自己的能力与性格,最终思考的模式会倾向于 —— 我有什么拿得出手的职场能力或性格特质?
比如,当程序员考虑第二曲线时,最常见的选择是:
- 接外包
- 独立开发
- 远程工作
都是这种思维惯势下的产物。
对于更广大的普通人,正是因为职场能力不出众、性格没优势,才在职场混不下去,从而考虑第二曲线。
如果还顺着思维惯势思考,不又陷入:
- 弱所以混不下去
- 混不下去所以找出路
- 弱所以找不到出路
这样的死局了么?
第二曲线应该如何思考优势
对于发展第二曲线的同学,我们需要跳出应试教育以及职场带来的思维惯势。
首先,回归本质 —— 评判一个能力或性格是优势还是劣势,取决于为了达到什么目的。
有个找我咨询的女生,由于是讨好型人格,所以在职场混的很痛苦,处理同事关系时很内耗。
但她的第二曲线方向是线下组织人参与有趣的活动。作为组织者,她需要在活动期间照顾到所有参与者。
正是由于讨好型人格,使得她在做这件事时心情无比愉悦,也收获了满满的成就感。
再比如一个找我咨询的男程序员,小镇做题家,喜欢文史,人前内向,网上又有极强表达欲,喜欢输出知识和自嘲。
他在职场就是被领导PUA的老实人,但他非常适合做ip,因为ip最重要的就是面相。
换一个目的,之前的劣势就都是优势了。
既然目的、能力、性格互相羁绊,那显然评价优势的标准并非单一维度。
实际上,当考虑第二曲线时,我们应该考虑优势领域,而非优势本身。
所谓优势领域,是指下面三者的交集:
- 你熟悉的领域
- 你擅长的能力
- 你的性格特质
比如,一个找我咨询的4S店管理的情况如下:
- 熟悉的领域:汽车、育儿(家有一女)
- 擅长的能力:销售能力、学习能力、英语能力
- 性格特质:同理心强、敏感、有小孩缘
最后,结合他辅导女儿学英语的经历(他女儿在会说中文前就能用英文单词表达需求了,比如渴了会说water
),最终选择了一个优势领域 —— 教小朋友学英语。
注意,教小朋友学英语与学英语是两个完全不同的领域,后者竞争者众多,而前者完全是根据他的能力、性格特质出发定制的领域。
这是个集合了亲子关系、儿童心理学、英语教学、幼儿教育等学科的交叉领域。
在他的优势领域中:
- 比他英语厉害的可能没他有小孩缘
- 比他有小孩缘的可能育儿没他厉害
- 比他育儿厉害的可能英语没他厉害
总之,在自己选择的优势领域中,我们普通人也能处于绝对的优势地位。
总结
由于应试教育以及职场带来的思维惯势,大部分人对优势的理解有两个误区:
- 脱离目的谈优势能力、优势性格
- 认为优势是单一维度的
在考虑发展第二曲线时,这种思维惯势会影响我们的最终选择。
更好的方式是:考虑优势领域,而非优势本身,即找到下面三者的交集:
- 你熟悉的领域
- 你擅长的能力
- 你的性格特质
交集所在的就是你的优势领域,也就是你应该探索第二曲线的方向。
来源:juejin.cn/post/7385108486517112883
改进菜单栏动态展示样式,我被评上优秀开发!
精彩新文章:拿客户电脑,半小时完成轮播组件开发!被公司奖励500
背景
我们公司的导航菜单是动态可配置的,有的页面菜单数量比较多,有的比较少。
由于大多页面菜单都是比较少的,因此当菜单非常多时, 我们采用了朴实无华的滚动条:当横向超出的时候,滚动展示。
但很快,客户就打回来了:说我们的样式太丑,居然用滚动条!还质问我们产品这合理吗?产品斩钉截铁的告诉客户,我让开发去优化...
于是,领导让我们想解决方案。(我真谢谢产品!
)
很快,我想到一个方案(从其他地方看到的交互),我告诉领导:
我们可以做成动态菜单栏,如果展示不下了,出现一个更多按钮,多余的菜单都放到更多里面去:
领导说这个想法不错啊,那就你来实现吧!
好家伙,我只是随便说说,没想到,自己给自己挖了个大坑啊!
不过,我最后也是顺利的完成了这个效果的开发,还被评上了本季度优秀开发!分享一下自己的实现方案吧!
技术方案
基础组件样式开发
既然要开发这个效果,干脆就封装一个通用组件AdaptiveMenuBar.vue吧。我们先写一下基本样式,如图,灰色区域就是我们的组件内容,也就是我们菜单栏动态展示
的区域。
AdaptiveMenuBar.vue
<template>
<div class="adaptive-menu-bar">
</div>
</template>
<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
overflow: hidden;
}
</style>
我们写点假数据
<template>
<div class="adaptive-menu-bar">
<div class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
{{ item.name }}
</div>
</div>
<div>更多</div>
</div>
</template>
<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
</script>
<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
overflow: hidden;
.origin-menu-item-wrap{
width: 100%;
display: flex;
}
}
</style>
如图,由于菜单数量比较多,一部分已经隐藏在origin-menu-item-wrap
这个父元素里面了。
实现思路
那我们要如何才能让多余的菜单出现在【更多】按钮里呢?原理很简单,我们只要计算出哪个菜单超出展示区域即可。假设如图所示,第12个菜单被截断了,那我们前11个菜单就可以展示在显示区域,剩余的菜单就展示在【更多】按钮里。
更多按钮的展示逻辑
更多按钮只有在展示区域空间不够的时候出现,也就是origin-menu-item-wrap元素的滚动区域宽度scrollWidth 大于其宽度clientWidth的时候。
用代码展示大致如下
<template>
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>
</template>
<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
// 是否展示更多按钮
const showMoreBtn = ref(false);
onMounted(() => {
const menuWrapDom = menuBarRef.value;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
}
});
</script>
截断位置的计算
要计算截断位置,我们需要先渲染好菜单。
然后开始对menu-item元素宽度进行加和,当相加的宽度大于菜单展示区域的宽度clientWidth时,计算终止,此时的menu-item元素就是我们要截断的位置。
菜单截断的部分,我们此时放到更多里面展示就可以了。
大致代码如下:
<template>
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>
</template>
<script setup>
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
// 是否展示更多按钮
const showMoreBtn = ref(false);
onMounted(() => {
const menuWrapDom = menuBarRef.value;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
}
// 计算截断菜单的索引位置
let sliceIndex = 0
// 获取menu-item元素dom的集合
const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
// 将NodeList转换成数组
const nodeArray = Array.prototype.slice.call(menuItemNodeList);
let addWidth = 0;
for (let i = 0; i < nodeArray.length; i++) {
const node = nodeArray[i];
// clientWidth不包含菜单的margin边距,因此我们手动补上12px
addWidth += node.clientWidth + 12;
// 76是更多按钮的宽度,我们也要计算进去
if (addWidth + 76 > menuWrapDom.clientWidth) {
sliceIndex.value = i;
break;
} else {
sliceIndex.value = 0;
}
}
});
</script>
样式重整
当被截断的元素计算完毕时,我们需要重新进行样式渲染,但是注意,我们原先渲染的菜单列不能注销,因为每次浏览器尺寸变化时,我们都是基于原先渲染的菜单列进行计算的。
所以,我们实际需要渲染两个菜单列:一个原始的,一个样式重新排布后的。
如上图,黄色就是原始的菜单栏,用于计算重新排布的菜单栏,只不过,我们永远不在页面上展示给用户看!
<template>
<div class="adaptive-menu-bar">
<!-- 原始渲染的菜单栏 -->
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
</div>
<!-- 计算优化显示的菜单栏 -->
<div v-for="(item, index) in menuList" :key="index" class="menu-item">
<m-button type="default" size="small">{{ item.name }}</m-button>
</div>
<div >更多</div>
</div>
</template>
代码实现
基础功能完善
为了我们的菜单栏能动态的响应变化,我们需要再每次resize事件触发时,都重新计算样式
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
// 是否展示更多按钮
const showMoreBtn = ref(false);
const setHeaderStyle = () => {
// ....
}
window.addEventListener('resize', () => setHeaderStyle());
onMounted(() => {
setHeaderStyle();
});
</script>
完整代码
完整代码剥离了一些第三方UI组件,便于大家理解。
<template>
<div class="adaptive-menu-bar">
<!-- 原始渲染的菜单栏 -->
<div ref="menuBarRef" class="origin-menu-item-wrap">
<div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
{{ item.name }}
</div>
</div>
<!-- 计算优化显示的菜单栏 -->
<div v-for="(item, index) in menuList" :key="index" class="menu-item">
{{ item.name }}
</div>
<!-- 更多按钮 -->
<div v-if="showMoreBtn" class="dropdown-wrap">
<span>更多</span>
<!-- 更多里面的菜单 -->
<div class="menu-item-wrap">
<div v-for="(item, index) in menuOriginData.slice(menuList.length)" :key="index">{{ item.name }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { IconMeriComponentArrowDown } from 'meri-icon';
const menuBarRef = ref();
const open = ref(false);
const menuOriginData = [
{ name: '哆啦a梦', id: 1 },
{ name: '宇智波佐助', id: 1 },
{ name: '香蕉之王奥德彪', id: 1 },
{ name: '漩涡鸣人', id: 1 },
{ name: '雏田', id: 1 },
{ name: '大雄', id: 1 },
{ name: '源静香', id: 1 },
{ name: '骨川小夫', id: 1 },
{ name: '超级马里奥', id: 1 },
{ name: '自来也', id: 1 },
{ name: '孙悟空', id: 1 },
{ name: '卡卡罗特', id: 1 },
{ name: '万年老二贝吉塔', id: 1 },
{ name: '小泽玛丽', id: 1 }
];
const menuList = ref(menuOriginData);
// 是否展示更多按钮
const showMoreBtn = ref(false);
const setHeaderStyle = () => {
const menuWrapDom = menuBarRef.value;
if (!menuWrapDom) return;
if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
showMoreBtn.value = true;
} else {
showMoreBtn.value = false;
}
const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
if (menuItemNodeList) {
let addWidth = 0,
sliceIndex = 0;
// 将NodeList转换成数组
const nodeArray = Array.prototype.slice.call(menuItemNodeList);
for (let i = 0; i < nodeArray.length; i++) {
const node = nodeArray[i];
addWidth += node.clientWidth + 12;
if (addWidth + 64 + 12 > menuWrapDom.clientWidth) {
sliceIndex = i;
break;
} else {
sliceIndex = 0;
}
}
if (sliceIndex > 0) {
menuList.value = menuOriginData.slice(0, sliceIndex);
} else {
menuList.value = menuOriginData;
}
}
};
window.addEventListener('resize', () => setHeaderStyle());
onMounted(() => {
setHeaderStyle();
});
</script>
<style lang="less" scoped>
.adaptive-menu-bar {
width: 100%;
height: 48px;
background: gainsboro;
display: flex;
position: relative;
align-items: center;
overflow: hidden;
.origin-menu-item-wrap {
width: 100%;
display: flex;
position: absolute;
top: 49px;
display: flex;
align-items: center;
left: 0;
right: 0;
bottom: 0;
height: 48px;
z-index: 9;
}
.menu-item {
margin-left: 12px;
}
.dropdown-wrap {
width: 64px;
display: flex;
align-items: center;
cursor: pointer;
justify-content: center;
height: 28px;
background: #fff;
border-radius: 4px;
overflow: hidden;
border: 1px solid #c4c9cf;
background: #fff;
margin-left: 12px;
.icon {
width: 16px;
height: 16px;
margin-left: 4px;
}
}
}
</style>
代码效果
可以看到,非常丝滑!
来源:juejin.cn/post/7384256110280802356
Cesium为军工助力!动态绘制各类交互式态势图
态势图(Situation Map)是一种用于表示空间环境中动态或静态信息的地图,它能够展示事件、资源、威胁和其他关键因素的地理位置及其变化情况
前言
什么是态势图
态势图(Situation Map)
是一种用于表示空间环境
中动态或静态信息的地图,它能够展示事件
、资源
、威胁
和其他关键因素
的地理位置及其变化情况。
通过可视化的方式,态势图帮助决策者
在复杂环境中迅速获取关键信息,从而做出及时而准确的决策。
随着地理信息系统(GIS)的不断发展,态势图在军事
、应急管理
和地理规划
等领域中扮演着越来越重要的角色。
军工领域
在军工领域,态势图是军事指挥
与控制系统
中的核心组件。
它们能够实时展示战场上的动态信息
,如部队的部署位置、敌军动向、武器系统状态等。这种可视化工具对于战术指挥、作战计划制定和战场态势感知至关重要。
应急管理
在应急管理领域,态势图能够帮助管理者协调资源
和人员应对自然灾害、山林火灾、事故或突发事件。通过态势图,可以清晰地看到灾害影响范围
、救援力量分布
、资源需求
,逃生路线
等关键信息,从而实现有效的应急响应和资源调配。
地理规划
在地理规划中,态势图用于展示和分析区域开发
、土地利用
、交通网络
等方面的信息。能帮助规划者更清晰的理解地理空间关系、评估环境影响,并做出科学的规划决策。
Cesium中绘制态势图
OK,接下来我们主要介绍一下在Cesium中如何绘制态势图,主要包括各种箭头类型的绘制,如直线箭头
、攻击箭头
、钳击箭头
等。
源码地址在文末。
箭头绘制的核心算法
algorithm.js
是实现复杂箭头绘制的核心脚本。
这里定义了多种箭头类型的绘制算法,如双箭头(doubleArrow
)、三箭头(threeArrow
)以及带尾攻击箭头(tailedAttackArrow
)。
这些算法通过接收用户点击的多个点,并计算出箭头的控制点和多边形点来实现箭头形状的生成。
以下是doubleArrow
函数的部分代码与解析:
xp.algorithm.doubleArrow = function (inputPoint) {
// 初始化结果对象
var result = {
controlPoint: null,
polygonalPoint: null
};
// 根据输入点数量决定不同的箭头形状
var t = inputPoint.length;
if (!(2 > t)) {
if (2 == t) return inputPoint;
// 获取关键点
var o = this.points[0],
e = this.points[1],
r = this.points[2];
// 计算连接点和临时点位置
3 == t ? this.tempPoint4 = xp.algorithm.getTempPoint4(o, e, r) : this.tempPoint4 = this.points[3];
3 == t || 4 == t ? this.connPoint = P.PlotUtils.mid(o, e) : this.connPoint = this.points[4];
// 根据点的顺序计算箭头的左右侧点位
P.PlotUtils.isClockWise(o, e, r)
? (n = xp.algorithm.getArrowPoints(o, this.connPoint, this.tempPoint4, !1), g = xp.algorithm.getArrowPoints(this.connPoint, e, r, !0))
: (n = xp.algorithm.getArrowPoints(e, this.connPoint, r, !1), g = xp.algorithm.getArrowPoints(this.connPoint, o, this.tempPoint4, !0));
// 生成最终的箭头形状并返回
result.controlPoint = [o, e, r, this.tempPoint4, this.connPoint];
result.polygonalPoint = Cesium.Cartesian3.fromDegreesArray(xp.algorithm.array2Dto1D(f));
}
return result;
};
该函数首先根据输入点的数量确定是否继续进行箭头的绘制,接着计算关键点的位置,并通过getArrowPoints
函数计算出箭头形状的多个控制点,最终生成一个包含箭头形状顶点的数组。
基于Cesium的箭头实体管理
arrowClass.js
定义了具体的箭头类(如StraightArrow
)和其行为管理。
通过结合Cesium的ScreenSpaceEventHandler
事件处理机制,开发者可以方便地在地图上绘制、修改和删除箭头实体。
以下是StraightArrow
类的部分代码与解析:
StraightArrow.prototype.startDraw = function () {
var $this = this;
this.state = 1;
// 单击事件,获取点击位置并创建箭头起点
this.handler.setInputAction(function (evt) {
var cartesian = getCatesian3FromPX(evt.position, $this.viewer);
if (!cartesian) return;
// 处理点位并开始绘制箭头
if ($this.positions.length == 0) {
$this.firstPoint = $this.creatPoint(cartesian);
$this.floatPoint = $this.creatPoint(cartesian);
$this.positions.push(cartesian);
}
if ($this.positions.length == 3) {
$this.firstPoint.show = false;
$this.floatPoint.show = false;
$this.handler.destroy();
$this.arrowEntity.objId = $this.objId;
$this.state = -1;
}
$this.positions.push(cartesian.clone());
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
// 鼠标移动事件,实时更新箭头形状
this.handler.setInputAction(function (evt) {
if ($this.positions.length < 1) return;
var cartesian = getCatesian3FromPX(evt.endPosition, $this.viewer);
if (!cartesian) return;
$this.floatPoint.position.setValue(cartesian);
if ($this.positions.length >= 2) {
if (!Cesium.defined($this.arrowEntity)) {
$this.positions.push(cartesian);
$this.arrowEntity = $this.showArrowOnMap($this.positions);
} else {
$this.positions.pop();
$this.positions.push(cartesian);
}
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
};
在startDraw
函数中,通过设置单击和鼠标移动事件,开发者可以实时捕获用户的操作,并根据点击位置动态绘制箭头。
最终的箭头形状会随着鼠标的移动而更新,当点击完成时箭头形状被确定。
工具类与辅助函数
plotUtil.js
提供了一些用于计算几何关系的实用工具函数。
例如,distance
函数计算两个点之间的距离,而getThirdPoint
函数根据给定的两个点和角度,计算出第三个点的位置。 这些工具函数被广泛用于箭头的绘制逻辑中,以确保箭头的形状符合预期。
以下是distance
和getThirdPoint
函数的代码示例:
P.PlotUtils.distance = function (t, o) {
return Math.sqrt(Math.pow(t[0] - o[0], 2) + Math.pow(t[1] - o[1], 2));
};
P.PlotUtils.getThirdPoint = function (t, o, e, r, n) {
var g = P.PlotUtils.getAzimuth(t, o),
i = n ? g + e : g - e,
s = r * Math.cos(i),
a = r * Math.sin(i);
return [o[0] + s, o[1] + a];
};
这些函数都是为复杂箭头形状的计算提供了基础,确保在地图上绘制的箭头具有精确的几何形态。
总结
以上主要介绍了在Cesium中实现态势图的一些关键代码以及解释,更多细节请参考项目源码,如果有帮助也请给一个免费的star
。
【项目开源地址】:github.com/tingyuxuan2…
来源:juejin.cn/post/7409181068597919781
什么是混入,如何正确地使用Mixin
在 Vue.js 中,mixins
是一种代码复用机制,允许我们将多个组件共享的逻辑提取到一个独立的对象中,从而提高代码的可维护性和重用性。下面将详细介绍 mixin
的概念,并通过示例代码来说明它的用法。
什么是 Mixin?
Mixin
是一种在 Vue 组件中复用代码的方式。我们可以将一个对象(即 mixin 对象)中的数据、方法和生命周期钩子等混入到 Vue 组件中。这样,多个组件就可以共享同一份逻辑代码。
Mixin 的基本用法
1. 定义 Mixin
首先,我们定义一个 mixin 对象,其中包含数据、方法、计算属性等。比如,我们可以创建一个 commonMixin.js
文件来定义一个 mixin:
// commonMixin.js
export default {
data() {
return {
message: 'Hello from mixin!'
};
},
methods: {
greet() {
console.log(this.message);
}
},
created() {
console.log('Mixin created hook called.');
}
};
2. 使用 Mixin
在 Vue 组件中,我们可以通过 mixins
选项来引入上述定义的 mixin。例如,我们可以在 HelloWorld.vue
组件中使用这个 mixin:
<template>
<div>
<p>{{ message }}</p>
<button @click="greet">Greet</button>
</div>
</template>
<script>
// 导入 mixin
import commonMixin from './commonMixin';
export default {
name: 'HelloWorld',
mixins: [commonMixin], // 使用 mixin
mounted() {
console.log('Component mounted hook called.');
}
};
</script>
在上面的示例中,HelloWorld
组件通过 mixins
选项引入了 commonMixin
。这意味着 HelloWorld
组件将拥有 commonMixin
中定义的数据、方法和生命周期钩子。
3. Mixin 的冲突处理
如果组件和 mixin 中都定义了相同的选项,Vue 将遵循一定的优先级规则来处理这些冲突:
- 数据:如果组件和 mixin 中有相同的
data
字段,组件中的data
会覆盖 mixin 中的data
。 - 方法:如果组件和 mixin 中有同名的方法,组件中的方法会覆盖 mixin 中的方法。
- 生命周期钩子:如果组件和 mixin 中有相同的生命周期钩子(如
created
),它们都会被调用,且 mixin 中的钩子会在组件中的钩子之前调用。
// commonMixin.js
export default {
data() {
return {
message: 'Hello from mixin!'
};
},
methods: {
greet() {
console.log('Mixin greet');
}
},
created() {
console.log('Mixin created hook called.');
}
};
// HelloWorld.vue
<template>
<div>
<p>{{ message }}</p>
<button @click="greet">Greet</button>
</div>
</template>
<script>
import commonMixin from './commonMixin';
export default {
name: 'HelloWorld',
mixins: [commonMixin],
data() {
return {
message: 'Hello from component!'
};
},
methods: {
greet() {
console.log('Component greet');
}
},
created() {
console.log('Component created hook called.');
}
};
</script>
在这个例子中,HelloWorld
组件中 message
的值会覆盖 mixin 中的值,greet
方法中的实现会覆盖 mixin 中的方法,created
钩子的调用顺序是 mixin 先调用,然后组件中的 created
钩子调用。
4. 使用 Mixin 的注意事项
- 命名冲突:为了避免命名冲突,建议使用明确且独特的命名方式。
- 复杂性:过度使用 mixin 可能会导致代码难以跟踪和调试。可以考虑使用 Vue 的组合式 API 来替代 mixin,以提高代码的可读性和可维护性。
在 Vue.js 开发中,mixin
主要用于以下场景,帮助我们实现代码复用和逻辑共享:
1. 共享功能和逻辑
当多个组件需要使用相同的功能或逻辑时,mixin
是一个有效的解决方案。通过将共享的逻辑提取到一个 mixin 中,我们可以避免重复代码。例如,多个组件可能都需要处理表单验证或数据格式化,这时可以将这些功能封装到一个 mixin 中:
// validationMixin.js
export default {
methods: {
validateEmail(email) {
const re = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
return re.test(email);
}
}
};
// UserForm.vue
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" placeholder="Enter your email" />
<button type="submit">Submit</button>
</form>
</template>
<script>
import validationMixin from './validationMixin';
export default {
mixins: [validationMixin],
data() {
return {
email: ''
};
},
methods: {
handleSubmit() {
if (this.validateEmail(this.email)) {
alert('Email is valid!');
} else {
alert('Email is invalid!');
}
}
}
};
</script>
2. 封装重复的生命周期钩子
有时候,多个组件可能需要在相同的生命周期阶段执行某些操作。例如,所有组件都需要在 created
钩子中初始化数据或进行 API 请求。可以将这些操作封装到 mixin 中:
// dataFetchMixin.js
export default {
created() {
this.fetchData();
},
methods: {
async fetchData() {
// 假设有一个 API 请求
try {
const response = await fetch('https://api.example.com/data');
this.data = await response.json();
} catch (error) {
console.error('Failed to fetch data:', error);
}
}
},
data() {
return {
data: null
};
}
};
// DataComponent.vue
<template>
<div>
<pre>{{ data }}</pre>
</div>
</template>
<script>
import dataFetchMixin from './dataFetchMixin';
export default {
mixins: [dataFetchMixin]
};
</script>
3. 跨组件通信
在 Vue 2 中,mixin
可以用来管理跨组件通信。例如,多个子组件可以通过 mixin 共享父组件传递的数据或方法:
// communicationMixin.js
export default {
methods: {
emitEvent(message) {
this.$emit('custom-event', message);
}
}
};
// ParentComponent.vue
<template>
<div>
<ChildComponent @custom-event="handleEvent" />
</div>
</template>
<script>
import communicationMixin from './communicationMixin';
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
mixins: [communicationMixin],
methods: {
handleEvent(message) {
console.log('Received message:', message);
}
}
};
</script>
// ChildComponent.vue
<template>
<button @click="sendMessage">Send Message</button>
</template>
<script>
import communicationMixin from './communicationMixin';
export default {
mixins: [communicationMixin],
methods: {
sendMessage() {
this.emitEvent('Hello from ChildComponent');
}
}
};
</script>
4. 封装组件的默认行为
对于有相似默认行为的多个组件,可以将这些默认行为封装到 mixin 中。例如,处理表单提交、数据清理等:
// formMixin.js
export default {
methods: {
handleSubmit() {
console.log('Form submitted');
// 处理表单提交逻辑
},
clearForm() {
this.$data = this.$options.data();
}
}
};
// LoginForm.vue
<template>
<form @submit.prevent="handleSubmit">
<!-- 表单内容 -->
<button type="submit">Login</button>
</form>
</template>
<script>
import formMixin from './formMixin';
export default {
mixins: [formMixin]
};
</script>
结论
Vue 的 mixin
机制提供了一种简单有效的方式来复用组件逻辑,通过将逻辑封装到独立的 mixin 对象中,可以在多个组件中共享相同的代码。理解和正确使用 mixin 可以帮助我们构建更清晰、可维护的代码结构。不过,随着 Vue 3 的引入,组合式 API 提供了更强大的功能来替代 mixin,在新的开发中,可以根据需要选择合适的方案。
来源:juejin.cn/post/7409110408991768587
uniapp 授权登录、页面跳转及弹窗问题
uniapp 弹框
主要介绍了 uniapp 弹框使用的一些问题,例如 uni.showModal 中的 content 换行显示实现、uni.showToast()字数超过 7 个显示问题、以及 uni-popup 自定义弹层处理
1. uni.showModal 中的 content 换行显示实现
// 注意:\r\n在微信模拟器中无效,真机才行
const content =
"学校名:光谷一小\r\n" +
"班级名:501\r\n" +
"教师名:张哈哈\r\n"
uni.showModal({
title: "确认操作吗?",
content: content,
success: (res) => {
if (res.confirm) {
} else if (res.cancel) {
}
},
});
2. uniapp 解决 showToast 字数超过 7 个显示问题
使用 uni-app 开发小程序,不管是使用微信小程序的 wx.showToast()
API 或 uni-app 的 uni.showToast()
API 显示消息提示框,显示图标 title 文本最多显示 7 个汉字长度
,在不显示图标的情况下,大于两行不显示。
解决方案一:如果要显示超过两行的文本,使用 uview-ui
框架的 Toast 消息提示组件。
// 先在html中引入组件
<u-toast ref="uToast" />;
// 然后调用
this.$refs.uToast.show({
title: "请学生绑定图书后再布置任务!",
duration: 1500,
});
解决方案二:不显示图标,就可以显示两行了
uni.showToast({
title: "请学生绑定图书后再布置任务!",
icon: "none",
duration: 1500,
});
3. uniapp 自定义弹框 uni-popup
复杂弹框,需要自定义内容的话,只能自己写了,可以使用 uni-popup 弹出层进行实现
<template>
<uni-popup ref="popup" class="lm-popup">
<view class="lm-popup-content">
<uni-icons
class="close"
type="closeempty"
size="24"
color="#ccc"
@click="hide"
></uni-icons>
<button class="btn confirm" @click="handleAuth">微信授权登录</button>
<button class="btn cancel" @click="handleLogin">手机验证码登录</button>
</view>
</uni-popup>
</template>
<script>
export default {
name: "login-popup",
methods: {
show() {
this.$refs.popup.open("center");
},
hide() {
this.$refs.popup.close();
},
},
};
</script>
uniapp 授权登录
主要介绍了uniapp授权的几种方式,分别为临时登录授权、手机号授权、用户信息授权
1. 微信登录授权
微信小程序的登录逻辑发生了变化,要求开发者使用静默登录
,即在用户无感知的情况下进行登录操作,不需要弹出授权窗口
了。
如下所示,获取微信的临时登录凭证 code,不会弹出授权窗口了。
uni.getProvider({
// 类型为oauth,用于获取第三方登录提供商
service: "oauth",
success: (res) => {
// 输出支持的第三方登录提供商列表
if (~res.provider.indexOf("weixin")) {
// 发起登录请求,获取临时登录凭证 code
uni.login({
// 登录提供商,如微信
provider: "weixin",
success: (loginRes) => {
// 获取用户登录凭证
this.handleLogin(loginRes.code);
},
});
}
},
});
2. 微信手机号授权
对于一般的用户信息,如头像、昵称等,被视为非敏感信息,以静默登录的方式进行获取。而用户的手机号等敏感信息,是需要授权的,可以通过 open-type="getPhoneNumber"
属性来触发获取手机号码的授权弹框。
getPhoneNumber
:获取用户手机号,可以从@getphonenumber
回调中获取到用户信息,该接口一直会弹出授权弹框,具体可查看官网:uniapp.dcloud.net.cn/component/b…
<button type="default" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">获取手机号</button>
// 打开获取用户手机号的授权窗口
getPhoneNumber(e) {
console.log('getPhoneNumber', e.detail)
console.log(e.detail.encryptedData); // 获取加密数据
console.log(e.detail.iv); // 获取加密算法的初始向量
const { detail:{ code, encryptedData, iv, errMsg } } = e;
if(errMsg === 'getPhoneNumber:ok') {
// 获取成功,做对应操作
}
}
2.1 拦截默认弹框
如下图所示:一般情况下,需要先勾选隐私协议,才能弹窗。
但是@getphonenumber事件弹窗无法拦截的,就算事件中写了判断,还是会先弹窗的,没找到拦截的方法。
目前的解决方法,用了两个按钮来判断实现
<button v-if="!agree" class="login-btn" hover-class='zn-btn-green-hover' @click="handleSubmit('auth')">手机号验证登录</button>
<button v-else class="login-btn" hover-class='zn-btn-green-hover' open-type="getPhoneNumber" @getphonenumber="(e) => handleSubmit('auth', e)">手机号验证登录</button>
handleSubmit(type, e) {
if (!this.agree) {
uni.showToast({ title: '请勾选用户服务协议', icon: 'none' });
return;
}
}
3. 微信用户信息(头像、昵称)授权
可以使用uni.getUserProfile获取用户信息,如头像、昵称
该 API 对于低版本(基础库 2.10.4-2.27.0 版本
),每次触发 uni.getUserProfile
才会弹出授权窗口;
我开发时,最新的基础库为 3.3.1,不会弹出授权窗口,直接获取到值了,也是静默授权状态。
<button type="default" size="mini" @click="getUserInfo">获取用户信息</button>
getUserInfo(e) {
// 获取用户信息
uni.getUserProfile({
desc: '获取你的昵称、头像、地区及性别',
success: res => {
console.log('获取你的昵称、头像',res);
},
fail: err => {
console.log("拒绝了", err);
}
});
}
uniapp 跳转
主要介绍了 uniapp 小程序跳转的三种方式,分别为内部页面跳转、外部链接跳转、其他小程序跳转。
1. 内部页面
内部页面的跳转,可以通过如下方式:navigateTo、reLaunch、switchTab
// 保留当前页面,跳转到应用内的某个页面
uni.navigateTo({ url: "/pages/home/home" });
// 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({ url: "/pages/home/home" });
// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
uni.switchTab({ url: "/pages/home/home" });
2. 外部链接
通过 webview
打开外部网站链接,web-view
是一个 web 浏览器组件,可以用来承载网页的容器。
// 1. 新建pages/webview/webview.vue页面
<template>
<web-view :src="url"></web-view>
</template>
<script>
export default {
data() {
return {
url: ''
}
},
onLoad(e) {
// 使用web-view标签进行跳转
this.url = decodeURIComponent(e.url)
}
}
</script>
// 2. 外链跳转使用
uni.navigateTo({url: "https://www.taobao.com"})
3. 小程序 appId
通过navigateToMiniProgram
可以打开其他小程序
// 打开其他小程序
uni.navigateToMiniProgram({
appId: "AppId", // 其他小程序的AppId
path: "pages/index/index", // 其他小程序的首页路径
extraData: {}, // 传递给其他小程序的数据
envVersion: "release", // 其他小程序的版本(develop/trial/release)
success(res) {
// 打开其他小程序成功的回调函数
},
fail(err) {
// 打开其他小程序失败的回调函数
},
});
来源:juejin.cn/post/7331717626059817023
纯css实现无限循环滚动logo墙
一、需求
在许多网站的合作伙伴一栏,常常会看到一排排无限地循环滚动的logo墙。
不久前,接到一个类似的需求。需求如下:
1、无限循环滚动;
2、鼠标hover后,暂停滚动,鼠标离开后,继续滚动;
3、支持从左往右和从右往左滚动;
4、滚动速度需要可配置。
简单动画,我们先尝试只使用css实现。
二、实现
1、marquee标签
说到无限循环滚动,很久以前marquee标签可以实现类似的功能,它可以无限循环滚动,并且可以控制方向和速度。但是该标签在HTML5中已经被弃用,虽然还可以正常使用,但w3c不再推荐使用该特性。作为一个标签,只需要负责布局,而不应该有行为特性。
了解marquee标签:marquee标签
2、css3动画
说到无限循环滚动我们会想到使用css3动画。
把animation-iteration-count设置为infinite,代表无限循环播放动画。
为了使动画运动平滑,我们把animation-timing-function设置为linear,代表动画匀速运动。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
padding-top: 200px;
}
@keyframes scrolling {
to {
transform: translateX(100vw);
}
}
.wall-item {
height: 90px;
width: 160px;
background-image: linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%);
animation: scrolling 4s linear infinite;
}
</style>
</head>
<body>
<div class="wall-item"></div>
</body>
</html>
我们上面实现了一个元素的滚动,在多个元素的时候,我们只需为每个元素设置不同的动画延迟时间(animation-delay),让每一项错落开来,就可以实现我们想要的效果了。
至于鼠标hover后暂停动画,我们只需在滚动元素hover时把animation-play-state设置为暂停即可。
有了以上思路,我很快就可以写出一个纯css实现logo墙无限循环滚动的效果。
完整示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--wall-item-height: 90px;
--wall-item-width: 160px;
--wall-item-number: 9;
--duration: 16s;
}
body {
padding-top: 300px;
}
@keyframes scrolling {
to {
transform: translateX(calc(var(--wall-item-width) * -1));
}
}
.wall {
margin: 30px auto;
height: var(--wall-item-height);
width: 80vw;
position: relative;
mask-image: linear-gradient(90deg, hsl(0 0% 0% / 0),
hsl(0 0% 0% / 1) 20%,
hsl(0 0% 0% / 1) 80%,
hsl(0 0% 0% / 0));
}
.wall .wall-item {
position: absolute;
top: 0;
left: 0;
transform: translateX(calc(var(--wall-item-width) * var(--wall-item-number)));
height: var(--wall-item-height);
width: var(--wall-item-width);
background-image: linear-gradient(120deg, #d4fc79 0%, #96e6a1 100%);
animation: scrolling var(--duration) linear infinite;
cursor: pointer;
}
.wall[data-direction="reverse"] .wall-item {
animation-direction: reverse;
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
}
.wall .wall-item:nth-child(1) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 1) * -1);
}
.wall .wall-item:nth-child(2) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 2) * -1);
}
.wall .wall-item:nth-child(3) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 3) * -1);
}
.wall .wall-item:nth-child(4) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 4) * -1);
}
.wall .wall-item:nth-child(5) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 5) * -1);
}
.wall .wall-item:nth-child(6) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 6) * -1);
}
.wall .wall-item:nth-child(7) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 7) * -1);
}
.wall .wall-item:nth-child(8) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 8) * -1);
}
.wall .wall-item:nth-child(9) {
animation-delay: calc((var(--duration) / var(--wall-item-number)) * (var(--wall-item-number) - 9) * -1);
}
.wall:has(.wall-item:hover) .wall-item {
animation-play-state: paused;
}
</style>
</head>
<body>
<div class="wall" style="--duration:10s">
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
</div>
<div class="wall" data-direction="reverse" style="--wall-item-number:9">
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
<div class="wall-item"></div>
</div>
</body>
</html>
- 可配置项:
--wall-item-height: 滚动项高度
--wall-item-width: 滚动项宽度
--wall-item-number: 滚动元素个数
--duration: 动画时长
我们也可以向上面示例代码一样,在.wall标签上,通过style属性,给每个滚动墙单独配置宽高、数量、动画时间。
- 滚动方向:
动画默认从左往右运动。我们可以在.wall标签上设置data-direction="reverse",让动画从右往左运动。
三、局限性
- 滚动元素(wall-item)太少,不足以填满包装元素(wall)时,会达不到预期效果;
(解决办法:用js把所有子元素复制一份push到最后面)
- --wall-item-number默认为9,每次子元素数量变化,需手动修改--wall-item-number值。
(解决办法:使用js计算赋值。说到要用js,那么这一点局限性或许就可以忍受了。)
3. 需要手动为每一个滚动元素设置动画延迟时间(animation-delay)。
(解决办法:可用js计算赋值。)
来源:juejin.cn/post/7408441793790410804
对象有顺序吗?
前言
对象有顺序吗?换句话说,如果我们遍历一个对象,获取属性的顺序是和属性添加时的顺序一样吗?这靠谱吗?这篇文章将为你揭晓答案。
JavaScript 对象基础
在 JavaScript 中,一个对象是一个无序的键值对集合,键通常是字符串或者 Symbol,而值可以是任何类型的数据。对象的基本创建方式如下:
const obj = {
key1: 'value1',
key2: 'value2',
}
虽然我们通常认为对象的属性是无序的,但实际上,JavaScript 对对象属性的排列有其特定的规则。
属性顺序的规则
根据 ECMAScript 规范,JavaScript 对象属性的顺序受以下规则的影响:
整数属性,会按照升序排列。
何为整数属性?指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串,也就是可以被解析为整数的字符串。
const obj = {
2: 'two',
1: 'one',
'3': 'three',
'a': 'alpha',
}
console.log(Object.keys(obj)); // -> ['1', '2', '3', 'a']
所有普通的字符串属性,按照其插入的顺序排列。
const obj = {
b: 'beta',
a: 'alpha',
c: 'gamma',
}
console.log(Object.keys(obj)); // -> ['b', 'a', 'c']
Symbol 类型的属性总是放在最后,并且保留其插入顺序。
const sym1 = Symbol('key1');
const sym2 = Symbol('key2');
const obj = {
[sym1]: 'value1',
1: 'one',
[sym2]: 'value2',
'a': 'alpha',
}
console.log(Object.keys(obj)); // -> ['1', 'a']
console.log(Object.getOwnPropertySymbols(obj)); // -> [sym1, sym2]
结合上述规则,我们再看一个综合示例。
const sym1 = Symbol("key1");
const sym2 = Symbol("key2");
const obj = {
[sym2]: "value2",
z: "last",
3: "three",
2: "two",
a: "alpha",
1: "one",
b: "beta",
[sym1]: "value1",
0: "zero",
};
console.log(Reflect.ownKeys(obj)); // -> ["0", "1", "2", "3", "z", "a", "b", Symbol("key2"), Symbol("key1")];
可以看到,整数属性按升序排列,普通属性按插入顺序排列,符号则放在最后并保留插入顺序。
最后
虽然 JavaScript 对象的属性被称为“无序”,但实际上它们有“特别的顺序”。整数属性会按升序排列,普通属性按插入顺序,Symbol 类型的属性总是排在最后并保留插入时的顺序。
如果文中有错误或者不足之处,欢迎大家在评论区指正。
你的点赞是对我最大的鼓励!感谢阅读~
来源:juejin.cn/post/7409668839199883314
Flutter GPU 是什么?为什么它对 Flutter 有跨时代的意义?
Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu
,还有 flutter_scene
的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impeller 的实现。
Flutter GPU 是 Flutter 内置的底层图形 API,它可以通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器,而无需 Native 平台代码。
目前 Flutter GPU 处于早期预览阶段并只提供基本的光栅化 API,但随着 API 接近稳定,会继续添加和完善更多功能。
详细说,Flutter GPU 是 Impeller 对于 HAL 的一层很轻的包装,并搭配了关于着色器和管道编排的自动化能力,也通过 Flutter GPU 就可以使用 Dart 直接构建自定义渲染器。
Flutter GPU 和 Impeller 一样,它的着色器也是使用 impellerc 提前编译,所以 Flutter GPU 也只支持 Impeller 的平台上可用。
Impeller 的 HAL 和 Flutter GPU 都没打算成为类似 WebGPU 这样的正式标准,相反,Flutter GPU 主要是由 Flutter 社区开发和发展,专职为了 Flutter 服务,所以不需要考虑「公有化」的兼容问题。
在 Flutter GPU 上,可直接从 Dart 与 Impeller 的 HAL 对话,甚至 Impeller Scene API(3D)也将作为重写的一部分出现。
说人话就是,可以用 Dart 通过 Flutter GPU 直接构建自定义渲染效果,未来直接支持 3D
可能有的人对于 Impeller 的整体结构和 HAL 还很模式无法理解,那么这里我们简单过一下:
- 在 Framework 上层,我们知道 Widget -> Element -> RenderObject -> Layer 这样的过程,而后其实流程就来到了 Flutter 自定义抽象的 DisplayList
- DisplayList 帮助 Flutter 在 Engine 做了接耦,从而让 Flutter 可以在 skia 和 Impeller 之间进行的切换
- 之后 Impeller 架构的顶层是 Aiks,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。
- Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息(编码位置、旋转、缩放、content object),此时还不能直接作用于 GPU
- HAL(Hardware Abstraction Layer) 则为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。
所以 HAL 它包装了各种图形 API,以提供通用的设备作业调度接口、一致的资源管理语义和统一的着色器创作体验,而对于 Impeller , Entities (2D renderer) 和 Scene (3D renderer) 都是直接通过 HAL 对接,甚至可以认为,Impeller 的 HAL 抽象并统一了 Metal 和 Vulkan 的常见用法和相似结构。
Unity 现在也有在 C# 直接向用户公开其 HAL 版本,称为 "Scriptable Render Pipeline" ,并提供了两个基于该 API 构建的默认渲染器 "Universal RP" / "High Definition RP" 用于服务不同的场景,所以 Unity 开发可以从使用这些渲染器去进行修改或扩展一些特定渲染需求。
而在 Flutter 的设计上,Flutter GPU 会作为 Flutter SDK 的一部分,并以 flutter_gpu
的 Dart 包的形式提供使用。
当然,Flutter GPU 由 Impeller 支持,但重要的是要记住它不是 Impeller ,Impeller 的 HAL 是私有内部代码与 Flutter GPU 的要求非常不同, Impeller 的私有 HAL 和 Flutter GPU 的公共 API 设计之间是存在一定差异化实现,而前面的流程,如 Scene (3D renderer) ,也可以被调整为基于 Flutter GPU 的全新模式实现。
而通过 Flutter GPU,如曾经的 Scene (3D renderer) 支持,也可以被调整为基于 Flutter GPU 的全新模式实现,因为 Flutter GPU 的 API 允许完全控制渲染通道附件、顶点阶段和数据上传到 GPU。这种灵活性对于创建复杂的渲染解决方案(从 2D 角色动画到复杂的 3D 场景)至关重要。
Flutter GPU 支持的自定义 2D 渲染器的一个很好的用例:依赖于骨骼网格变形的 2D 角色动画格式。
Spine 2D 就是一个很好的例子,骨骼网格解决方案通常具有动画剪辑,可以按层次结构操纵骨骼的平移、旋转和缩放属性,并且每个顶点都有几个相关的“bone weights”,这些权重决定了哪些骨骼应该影响顶点以及影响程度如何。
使用像 drawVertices
这样的 Canvas 解决方案,需要在 CPU 上对每个顶点应用骨骼权重变换,而 使用 Flutter GPU,骨骼变换可以用统一数组或纹理采样器的形式发送到顶点着色器,从而允许根据骨架状态和每个顶点的 “bone weights” 在 GPU 上并行计算每个顶点的最终位置。
使用 Flutter GPU
首先你需要在最新的 main channel 分支,然后通过 flutter pub add flutter_gpu --sdk=flutter
将 flutter_gpu SDK 包添加到你的 pubspec。
为了使用 Flutter GPU 渲染内容,你会需要编写一些 GLSL 着色器,Flutter GPU 的着色器与 Flutter 的 fragment shader 功能所使用的着色器具有不同的语义,特别是在统一绑定方面,还需要定义一个顶点(vertex)着色器来与 fragment shader 一起使用,然后配合 gpu.ShaderLibrary
等 API 就可以直接实现 Flutter GPU 渲染。
当然,本篇不会介绍详细的 API 使用 ,这里只是单纯做一个简单的介绍,目前 Flutter GPU 进行光栅化的简单流程如下:
- 获取 GPUContext。
GpuContext.createCommandBuffer
创建一个CommandBuffer
CommandBuffer.createRenderPass
创建一个RenderPass
- 使用各种方法设置状态/管道并绑定资源
RenderPass
- 附加绘图命令
RenderPass.draw
CommandBuffer
使用CommandBuffer.submit
(异步)提交绘制,所有RenderPass
会按照其创建顺序进行编码
·····
///导入 flutter_gpu
import 'package:flutter_gpu/gpu.dart' as gpu;
ByteData float32(List<double> values) {
return Float32List.fromList(values).buffer.asByteData();
}
ByteData float32Mat(Matrix4 matrix) {
return Float32List.fromList(matrix.storage).buffer.asByteData();
}
class TrianglePainter extends CustomPainter {
TrianglePainter(this.time, this.seedX, this.seedY);
double time;
double seedX;
double seedY;
@override
void paint(Canvas canvas, Size size) {
/// Allocate a new renderable texture.
final gpu.Texture? renderTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.devicePrivate, 300, 300,
enableRenderTargetUsage: true,
enableShaderReadUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (renderTexture == null) {
return;
}
final gpu.Texture? depthTexture = gpu.gpuContext.createTexture(
gpu.StorageMode.deviceTransient, 300, 300,
format: gpu.gpuContext.defaultDepthStencilFormat,
enableRenderTargetUsage: true,
coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture);
if (depthTexture == null) {
return;
}
/// Create the command buffer. This will be used to submit all encoded
/// commands at the end.
final commandBuffer = gpu.gpuContext.createCommandBuffer();
/// Define a render target. This is just a collection of attachments that a
/// RenderPass will write to.
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: renderTexture),
depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthTexture),
);
/// Add a render pass encoder to the command buffer so that we can start
/// encoding commands.
final encoder = commandBuffer.createRenderPass(renderTarget);
/// Load a shader bundle asset.
final library = gpu.ShaderLibrary.fromAsset('assets/TestLibrary.shaderbundle')!;
/// Create a RenderPipeline using shaders from the asset.
final vertex = library['UnlitVertex']!;
final fragment = library['UnlitFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment);
encoder.bindPipeline(pipeline);
/// (Optional) Configure blending for the first color attachment.
encoder.setColorBlendEnable(true);
encoder.setColorBlendEquation(gpu.ColorBlendEquation(
colorBlendOperation: gpu.BlendOperation.add,
sourceColorBlendFactor: gpu.BlendFactor.one,
destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha,
alphaBlendOperation: gpu.BlendOperation.add,
sourceAlphaBlendFactor: gpu.BlendFactor.one,
destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha));
/// Append quick geometry and uniforms to a host buffer that will be
/// automatically uploaded to the GPU later on.
final transients = gpu.HostBuffer();
final vertices = transients.emplace(float32(<double>[
-0.5, -0.5, //
0, 0.5, //
0.5, -0.5, //
]));
final color = transients.emplace(float32(<double>[0, 1, 0, 1])); // rgba
final mvp = transients.emplace(float32Mat(Matrix4(
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
0, 0, 0.5, 1, //
) *
Matrix4.rotationX(time) *
Matrix4.rotationY(time * seedX) *
Matrix4.rotationZ(time * seedY)));
/// Bind the vertex data. In this case, we won't bother binding an index
/// buffer.
encoder.bindVertexBuffer(vertices, 3);
/// Bind the host buffer data we just created to the vertex shader's uniform
/// slots. Although the locations are specified in the shader and are
/// predictable, we can optionally fetch the uniform slots by name for
/// convenience.
final mvpSlot = pipeline.vertexShader.getUniformSlot('mvp')!;
final colorSlot = pipeline.vertexShader.getUniformSlot('color')!;
encoder.bindUniform(mvpSlot, mvp);
encoder.bindUniform(colorSlot, color);
/// And finally, we append a draw call.
encoder.draw();
/// Submit all of the previously encoded passes. Passes are encoded in the
/// same order they were created in.
commandBuffer.submit();
/// Wrap the Flutter GPU texture as a ui.Image and draw it like normal!
final image = renderTexture.asImage();
canvas.drawImage(image, Offset(-renderTexture.width / 2, 0), Paint());
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class TrianglePage extends StatefulWidget {
const TrianglePage({super.key});
@override
State<TrianglePage> createState() => _TrianglePageState();
}
class _TrianglePageState extends State<TrianglePage> {
Ticker? tick;
double time = 0;
double deltaSeconds = 0;
double seedX = -0.512511498387847167;
double seedY = 0.521295573094847167;
@override
void initState() {
tick = Ticker(
(elapsed) {
setState(() {
double previousTime = time;
time = elapsed.inMilliseconds / 1000.0;
deltaSeconds = previousTime > 0 ? time - previousTime : 0;
});
},
);
tick!.start();
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Slider(
value: seedX,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedX = value)}),
Slider(
value: seedY,
max: 1,
min: -1,
onChanged: (value) => {setState(() => seedY = value)}),
CustomPaint(
painter: TrianglePainter(time, seedX, seedY),
),
],
);
}
}
GpuContext 是分配所有 GPU 资源并调度 GPU 的存在,而 GpuContext 仅有启用 Impeller 时才能访问。
DeviceBuffer 和 Texture 就是 GPU 拥有的资源,可以通过 GPUContext 创建获取,如 createDeviceBuffer
和 createTexture
:
- DeviceBuffer 简单理解就是在 GPU 上分配的简单字节串,主要用于存储几何数据(索引和顶点属性)以及统一数据
- Texture 是一个特殊的设备缓冲区
CommandBuffer 用于对 GPU 上的异步执行进行排队和调度工作。
RenderPass 是 GPU 上渲染工作的顶层单元。
RenderPipeline 提供增量更改绘制所有状态以及附加绘制调用的方法如 RenderPass.draw()
可以想象,通过 Flutter GPU,Flutter 开发者可以更简单地对 GPU 进行更精细的控制,通过与 HAL 直接通信,创建 GPU 资源并记录 GPU 命令,从而最大限度的发挥 Flutter 的渲染能力。
另外,对于 3D 支持的 Flutter Scene , 可以通过使用 native-assets 来设置 Flutter Scene 的 3D 模型自动导入,通过导入编译模型 .model 之后,就可以通过 Dart 实现一些 3D 的渲染。
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
double elapsedSeconds = 0;
Scene scene = Scene();
@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();
Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});
super.initState();
}
@override
Widget build(BuildContext context) {
final painter = ScenePainter(
scene: scene,
camera: PerspectiveCamera(
position: Vector3(sin(elapsedSeconds) * 3, 2, cos(elapsedSeconds) * 3),
target: Vector3(0, 0, 0),
),
);
return MaterialApp(
title: 'My 3D app',
home: CustomPaint(painter: painter),
);
}
}
class ScenePainter extends CustomPainter {
ScenePainter({required this.scene, required this.camera});
Scene scene;
Camera camera;
@override
void paint(Canvas canvas, Size size) {
scene.render(camera, canvas, viewport: Offset.zero & size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
目前 Flutter GPU 和 Flutter Scene 的支持还十分有限,但是借助 Impeller ,Flutter 开启了新的可能,可以说是,Flutter 团队完全掌控了渲染堆栈,在除了自定义更丰富的 2D 场景之外,也为 Flutter 开启了 3D 游戏的可能,2023 年 Flutter Forward 大会的承诺,目前正在被落地实现。
详细 API 使用例子可以参看 :medium.com/flutter/get…
如果你对 Flutter Impeller 和其着色器感兴趣,也可以看:
来源:juejin.cn/post/7399985723673821193
常见呼吸灯闪烁动画
最近在需求里遇到了一个手指引导交互的动画需求。这篇文章就来讲讲如何CSS实现这个动画,如下图所示:
简单分析了一下效果,是一个手指移动到某处位置,然后会触发呼吸灯闪烁的效果,所有实现整个动画可以分两步:
呼吸灯闪烁动画
这里介绍下我遇到过得几种呼吸灯闪烁动画
第一种效果
@keyframes twinkling {
0% {
opacity: 0.2;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.12);
}
100% {
opacity: 0.2;
transform: scale(1);
}
}
.circle {
border-radius: 50%;
width: 12px;
height: 12px;
background: green;
position: absolute;
top: 36px;
left: 36px;
&::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
border-radius: 50%;
background: greenyellow;
animation: twinkling 1s infinite ease-in-out;
animation-fill-mode: both;
}
}
第二种效果
@keyframes scale {
0% {
transform: scale(1)
}
50%,
75% {
transform: scale(3)
}
78%,
100% {
opacity: 0
}
}
@keyframes scales {
0% {
transform: scale(1)
}
50%,
75% {
transform: scale(2)
}
78%,
100% {
opacity: 0
}
}
.circle {
position: absolute;
width: 12px;
height: 12px;
background-color: pink;
border-radius: 50%;
top: 36px;
left: 36px;
}
.circle:before {
content: '';
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .4;
background-color: pink;
animation: scale 1s infinite cubic-bezier(0, 0, .49, 1.02);
}
.bigcircle {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .4;
background-color: pink;
top: 36px;
left: 36px;
animation: scales 1s infinite cubic-bezier(0, 0, .49, 1.02);
}
第三种效果
@keyframes scaless {
0% {
transform: scale(1)
}
50%,
75% {
transform: scale(3)
}
78%,
100% {
opacity: 0
}
}
.circle {
position: absolute;
width: 12px;
height: 12px;
background-color: transparent;
border-radius: 50%;
top: 36px;
left: 36px;
}
.circle:before {
content: '';
display: block;
width: 12px;
height: 12px;
border-radius: 50%;
opacity: .7;
border: 3px solid hotpink;
background-color: transparent;
animation: scaless 1s infinite cubic-bezier(0, 0, .49, 1.02);
}
小手移动动画
@keyframes animation-hand-move {
0% {
transform: translate(0, 0);
}
20% {
transform: translate(-80px, -50px);
}
25% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}
75% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}
90% {
transform: translate(-50px, -40px) scale(1) rotate(0deg);
opacity: 0.6;
}
100% {
opacity: 0;
}
}
.hard {
position: absolute;
top: 200px;
left: 318px;
width: 61px;
height: 69px;
background-size: contain;
background-repeat: no-repeat;
background-image: url('./img//174ba1f81a4d0d91a7dc45567b59fd8b.png');
animation: animation-hand-move 4s infinite linear;
}
完整动画代码
<div class="card">
<View class="round circle"></View>
<View class="round small-circle"></View>
<View class="round less-circle"></View>
<div class="hard"></div>
</div>
.card {
margin: 100px auto;
width: 480px;
height: 300px;
background-color: #333333;
border-radius: 16px;
position: relative;
}
.hard {
position: absolute;
top: 200px;
left: 318px;
width: 61px;
height: 69px;
background-size: contain;
background-repeat: no-repeat;
background-image: url('./img/174ba1f81a4d0d91a7dc45567b59fd8b.png');
animation: animation-hand-move 4s infinite linear;
}
.round {
position: absolute;
top: 144px;
left: 227px;
width: 32px;
height: 32px;
border-radius: 50%;
background-color: transparent;
&::before {
content: '';
position: absolute;
border-radius: 50%;
background: transparent;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
border: 6px solid rgba(255, 255, 255, 0.5);
pointer-events: none;
transform: translate(-50%, -50%) scale(0);
opacity: 1;
}
}
.circle {
&::before {
animation: animation-wave 4s infinite linear;
}
}
.small-circle {
&::before {
animation: animation-small-wave 4s infinite linear;
}
}
.less-circle {
&::before {
animation: animation-less-wave 4s infinite linear;
}
}
@keyframes animation-wave {
0%,
20%,
100% {
opacity: 0;
}
25% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}
75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}
@keyframes animation-small-wave {
0%,
20%,
100% {
opacity: 0;
}
42% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}
75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}
@keyframes animation-less-wave {
0%,
20%,
100% {
opacity: 0;
}
59% {
width: 0px;
height: 0px;
transform: translate(-50%, -50%);
opacity: 1;
}
75% {
width: 64px;
height: 64px;
transform: translate(-50%, -50%);
opacity: 0;
}
}
@keyframes animation-hand-move {
0% {
transform: translate(0, 0);
}
20% {
transform: translate(-80px, -50px);
}
25% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}
75% {
transform: translate(-80px, -50px) scale(0.92) rotate(-3deg);
opacity: 1;
}
90% {
transform: translate(-50px, -40px) scale(1) rotate(0deg);
opacity: 0.6;
}
100% {
opacity: 0;
}
}
来源:juejin.cn/post/7408795408861921290
如何写出让同事崩溃的代码
废话不多说,咱直接进入主题。手把手教你如何写出让帮你维护代码的同时瞬间崩溃的代码。
一、方法或变量名字随便取
首先,让同事看不懂自己代码的第一步就是,想尽办法让他看不出来我定义的变量或者方法到底是干嘛用的。哎!对,就是让他去猜,哈哈哈。
来来来,空说没意思,举个栗子。
假设现在想要点击某个input框时,显示一个自定义的组件用于选择选择时间。
正常的写法如下:定义一个 toggleDatePicker 方法
这个一看就知道是时间选择器的显示切换方法。
但是呢,我偏不,定义成下面这样:让同事摸不着头脑,哈哈哈
当看到很多这样的方法名或变量名时,同事的表情估计时这样的。
接下来,第二招
二、方法体尽可能的长,长到不能在长
这一步至关重要,将所有逻辑全部放在一个方法中写完,坚决不分步骤,不按逻辑一步步拆分方法。让同事为我的超长方法体感到叹为观止,默默流泪。
老规矩,上栗子
假设现在有个方法需要处理比较复杂(需要递归,而且每层数据有不同的类型)的json格式的数据回显到页面上(这是用于拼一些条件)。数据格式大概是这样的
[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]
由于上面的 json 数组格式中是一个对象,对象都有 type 属性,一种是 group 类型(表示是一组条件),一种是 condition 类型(表示是真正的条件内容)。
因此,正常情况下,我们应该是遍历数组,不同的类型分别用不同的方法去处理,并且在处理条件中各个部分的时候分别处理。如下所示:
但是呢?咱主题是啥,让同时崩溃啊,怎么能把方法拆成这样清晰的逻辑呢。
来给你看看怎么让同事对你的代码叹为观止
怎么样,牛不牛,一个方法写完所以逻辑,从1825行一口气写到2103行,足足... 2103 - 1825 是多少来着,3减5 不够,向前借位 ,嗯。。。278 行。
****怎么样,有没有被哥的机智震惊到,如此代码。同事看到肯定心生敬佩,连连称绝。此时想到得到同事应该是这样的表情
同事还没进到方法体里面,就已经被我的 迷之方法名 和 超长方法体 所折服,接下来就让他在方法体里面快乐的遨游吧
接下来,继续让同时崩溃。
三、坚决不定义统一的变量
这个怎么说呢,就是因为有点懒,所有很多代码直接复制粘贴,多么的方便快捷。
正常情况下,如果某个我们需要的对象在是其他对象的属性,并且层次很深,我们先定义一个变量来接收这个对象,再对这个对象操作。
例如:
let a = {
b: {
c: {
d: {
name: "我是最里面的对象"
}
}
}
}
我们要对d对象进行很多次的操作时,一般先将d赋值给一个变量,然后对变量操作。如下:
var dOfA = a.b.c.d;
dOfA.name = "我现在被赋值给dOfA ";
dOfA.data = 1;
dOfA.other = false;
但是呢,我就不这么干,就是要写得整整齐齐
a.b.c.d.name = "就要这么干,你打我呀";
a.b.c.d.data = 1;
a.b.c.d.other = false;
老规矩,没有 实际的 栗子 怎么能说的形象呢,上图
正常写法:
我偏要这么写
多么的整齐划一,
全场动作必须跟我整齐划一
来左边儿 跟我一起画个龙
在你右边儿 画一道彩虹
来左边儿 跟我一起画彩虹...
咋突然哼起歌来了,不对,咱是要整同事的,怎么能偏题。
继续,此时同事应该是这个表情
然后,方法体里面只有这么点东西怎么够玩,继续 come on
四、代码能复制就复制,坚决不提成公用的方法
代码能 CV ,干嘛费劲封装成方法,而且这样不是显得我代码行数多吗?图片图片图片
就是玩儿,就是不封装
来,上栗子
看到没有,相同的代码。我在 1411行 - 1428行 写了一遍, 后面要用,在1459行-1476行复制一遍
这还不够?那我在1504-1521行再复制一遍
这下,爽了吧,哈哈哈
就是不提方法,就是玩儿,哎! 有意思
这个时候同事估计是这样的吧
怎么样,是不是很绝?不不不,这算啥
虽然以上这些会让看我代码的同事头疼,但是,只要我在公司,他们还会让我改啊。怎么能搞自己呢。
最后一步
五、离职
洋洋洒洒的写完代码,尽早离开。够不够绝,哈哈哈
六、申明:
以上场景纯属个人虚构的,单纯为了给文章增加点乐趣。写这个文章的目的是让各位程序员兄弟尽量避免写这种难以维护的代码。真的太痛苦了!代码质量、代码重构真的是编程过程中很重要的一个步骤。不能抱着能用就行的心态。还是要对自己有一定的要求。只有你看得起自己的代码,别人才有可能看得起你的代码。加油吧!各位
来源:juejin.cn/post/7293888785400856628
我花了一天时间,做了一个图片上传组件,看起来很酷实际上确实很酷
今天,我花了一天的时间做了一个图片上传组件。效果如下:
可能有人觉得,这个组件很简单,没什么技术含量,其实确实也啥技术含量。但是,我是想借这个组件,来表达一种封装的思想在里面,希望可以帮助到一些朋友。
简单的描述下这个组件的功能:
- 用户可以点击下面颜色比较绚丽的上传按钮,选择本地图片进行上传,也可以直接点击图片区域进行上传。
- 上传过程中,会有一个上传中的进度条,上传完成后,会有一个上传成功的提示,如果失败了,会有一个上传失败的提示,而且支持重试。
- 可以点击图片右上角的删除按钮,删除图片。
- 并发控制,最多只能同时上传 N 张图片,也就是所谓的限频,这里是 2 张。
是不是看了这么多功能之后,就开始有点头皮发麻了?哈哈,不要怕,这就带你了解下,如何拆解这种功能,而且,学会了这种拆解的办法,后面你遇到更加复杂的,也可以得心应手。
拆解功能,逐步实现
首先,我们思考,我们该使用自底向上的思路,还是自顶向下的思路来拆解这个功能呢?我的建议自顶向下的思路去思考架构,然后自底向上的去实现功能。
因为我们这个图片上传组件是支持多长图片同时上传的,而且,我们还需要支持上传失败重试的功能,所以,为了让功能更加聚焦,我们把关注点放在 PhotoItem 上,没一个 PhotoItem 就是一个图片上传的单元。他可以独立的上传,独立的删除,独立的重试。
那么,为了让 PhotoItem 这个组件更加简洁,我们把上传逻辑放在hooks useUpload
中,这样,PhotoItem 只需要关注自己的展示逻辑即可。
这样做的目的是做到关注点分离,通常来讲,也是符合单一职责原则的。写出来的组件维护性必定大大提升。
代码实现
我们先来看下 useUpload 的代码,因为PhotoItem 依赖他,我们先实现它。
"use client";
export const useUploader = (uploadAction) => {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState(null);
const upload = useCallback(async (file) => {
setIsUploading(true);
setError(null);
try {
return await uploadAction(file);
} catch (err) {
setError(err.message || 'Upload failed');
} finally {
setIsUploading(false);
}
}, [uploadAction]);
const reset = useCallback(() => {
setIsUploading(false);
setError(null);
}, []);
return { upload, isUploading, error, reset };
};
可以看到,我们的 hooks 非常之简单,就是暴露了一个实现图片上传的狗子 upload,然而,他替我们的组件管理了上传中
,上传失败
,的状态,因此,接下来看,我们的PhotoItem 组件将会有多清晰。
export const PhotoItem = ({
file,
onRemove,
onUploadComplete,
onUploadError,
}) => {
const { upload, isUploading, error, reset } = useUploader();
const startUpload = useCallback(async () => {
try {
const url = await upload(file);
onUploadComplete(url);
} catch (err) {
onUploadError();
}
}, [file, upload, onUploadComplete, onUploadError]);
useEffect(() => {
startUpload();
}, [queueUpload, startUpload]);
const handleRetry = () => {
reset();
startUpload();
};
return (
<div className="relative w-full h-20">
<img
src={URL.createObjectURL(file)}
/>
{!isUploading && !error(
Uploaded
)}
{isUploading && (
<Progress />
)}
{error && (
<span>Failed</span>
)}
</div>
);
};
OK,到目前为止,还是极其简单的,但是我们貌似忘记了一个很核心的功能,限制并发数。为什么要限制并发数,因为我们自己的服务器或者三方的服务器,可能会有并发数的限制,如果我们不限制并发数,可能会导致一次传多张图片是卡住。
思考,如何限制并发数
我们想一样,是谁触发了上传的呢?是不是 PhotoItem 组件呢?是的,我们可以在 PhotoItem 组件中,去控制并发数,但是,这样做,会导致 PhotoItem 组件的逻辑变得复杂,因为他不仅要关注自己的展示逻辑,还要关注并发数的控制逻辑。这就显的不太合适了。所以,我们应该把他丢出去对吧,截止到目前为止,我们的PhotoUploader 这个组件似乎并没有干任何事情,我们思考下,并发控制的逻辑是否应该是他来呢?
答案是显然的,我们应该把并发控制的逻辑放在 PhotoUploader 组件中,因为他是整个上传组件的入口,他应该关注并发控制,而不是 PhotoItem 组件,而且最本质的原因是,PhotoItem 也不关心是否有其他的 PhotoItem 。
那么,问题来了,并发控制怎么写呢?使用什么数据结构较为合适呢?不卖关子了,我们知道,队列是最合适的数据结构,因为他是先进先出的,我们可以把上传任务放在队列中,然后,每次上传完成,就从队列中取出一个任务,继续上传。
好,我们改造一下,我们的 PhotoItem 组件,让他不要直接执行上传逻辑,而是把他做成一个任务,然后,把任务放在队列中,然后,我们在 PhotoUploader 组件中,去控制并发数。
export const PhotoItem = ({
file,
onRemove,
...
queueUpload // 加一个队列操作器
}) => {
const { upload, isUploading, error, reset } = useUploader();
...
useEffect(() => {
queueUpload(startUpload); // 修改这里
}, [queueUpload, startUpload]);
const handleRetry = () => {
reset();
queueUpload(startUpload);//修改这里
};
// .... 其他几乎不变
在来看看我们的 PhotoUploader 组件,他是如何控制并发数的。很简单,我们只需要维护一个队列,然后,每次上传完成,就从队列中取出一个任务,继续上传。
const processQueue = useCallback(() => {
while (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS && uploadQueueRef.current.length > 0) {
const nextUpload = uploadQueueRef.current.shift();
activeUploadsRef.current++;
nextUpload();
}
}, []);
const queueUpload = useCallback((startUpload) => {
if (activeUploadsRef.current < MAX_CONCURRENT_UPLOADS) {
activeUploadsRef.current++;
startUpload();
} else {
uploadQueueRef.current.push(startUpload);
}
}, []);
这里,只给出最最核心的逻辑,实际上就是维护的了一个任务队列,然后,每次上传完成,就判断下队列中是否还有任务,并且是否超过了并发数,如果没有超过,并且队列中还有任务,就继续上传。仅此而已。
总结一下
这个图片上传组件,看似简单,但是,他涉及到了很多的知识点,比如并发控制,上传失败重试,组件拆解,自顶向下的架构设计,自底向上的功能实现。我们在实现这个组件的过程中。有过很多的思考,比如:
- 如何拆解功能,让组件更加聚焦,做到关注点分离。
- 控制并发数,使用队列是最合适的数据结构。
- 如何设计一个 hooks,让组件更加简洁。
- 以及自顶向下的架构设计,自底向上的功能实现。
只有建立起这些系统性的思维,我们才能在遇到更加复杂的问题时,得心应手。希望这篇文章对你有所帮助。
欢迎关注我老码沉思录,获取我最新的知识分享。
来源:juejin.cn/post/7394854112510951443
Flutter 七年之约:2024 年将带来哪些惊喜?
Flutter 作为目前最出色的跨平台开发框架之一,自2017年Google向公众发布它以来,已经走过了七年的历程。在这段时间里,成千上万的开发者和团队选择了使用 Flutter 来开发他们的应用程序。时至今日,Google 仍然坚定地支持着Flutter的发展。接下来就让我们通过Flutter的2024年的路线图,一起来了解 Flutter 的未来规划吧。
- 作为一个开源项目,Flutter 团队希望通过公开计划来增加透明度,并邀请社区成员共同参与项目的开发。这份路线图涵盖了核心框架和引擎的改进、对移动和桌面平台的支持,以及生态系统的扩展。
- 这份路线图充满了前瞻性,展示了 Flutter 和 Dart 社区中最活跃的贡献者今年计划进行的工作。通常情况下,很难对所有的工程工作做出明确的承诺,尤其是对于一个拥有数百名贡献者的开源项目来说。
核心框架和引擎
Flutter 将继续专注于通过 Impeller 提升质量和性能。Flutter 的目标是完成 iOS 向 Impeller 的迁移,这包括移除 iOS 上的 Skia 后端。在 Android 上,Flutter 预计 Impeller 将支持 Vulkan 和 OpenGLES。不过短期内,用户也可以选择继续使用 Skia。此外,Flutter 还计划改进 Impeller 的测试基础设施,以减少生产中的回归问题。
对于核心框架,Flutter 计划全面支持 Material 3。Flutter 也在研究如何让核心框架更通用化,以更好地支持苹果设备的设计需求,比如应用栏和选项卡栏的优化。
Flutter 还将继续推进 blankcanvas项目的工作。
移动平台(Android 和 iOS)
在 2023 年,Flutter 启动了支持多个 Flutter 视图的计划。到 2024 年,Flutter计划将这一支持扩展到 Android 和 iOS 平台。Flutter也在努力提高平台视图的性能,以及测试覆盖率和可测试性。
Flutter将继续通过支持最新的苹果标准(如 隐私清单 和 Swift 包管理器)来提升 iOS 产品的现代化水平。同时,Flutter也在评估对未来 Android 版本的支持需求。
在 Android 平台上,Flutter会研究如何在构建文件中支持 Kotlin。
互操作性对 Dart 与本地代码的交互非常重要。Flutter计划完成从 Dart 直接调用 Objective C 代码的支持工作,并探索如何支持直接调用 Swift 代码。同样,Flutter将继续改进 调用 Java 和 Android 的功能。Flutter还在研究如何更好地支持只能在主操作系统或平台线程上调用的 API。
越来越多的大型 Flutter 应用开始作为混合应用(包含 Flutter 代码和部分 Android/iOS 平台代码或 UI)开发。Flutter将研究如何在性能、资源开销和开发体验方面更好地支持这种模式。
Web 平台
Flutter 将继续专注于提升 Web 平台的性能和质量。这包括研究如何减少应用程序的整体大小、更好地利用多线程、支持平台视图、改善应用加载时间、将 CanvasKit 设为默认渲染器、改进文本输入,并探索支持 Flutter web 的 SEO 的选项。
Flutter计划完成将 Dart 编译为 WasmGC 的工作,以支持 Wasm 编译 Flutter web 应用。这还包括一个新的 Dart 的 JS 互操作 机制,支持 JS 和 Wasm 编译。
Flutter还计划恢复对 web 上的热重载 的支持。
桌面平台
虽然 Flutter 的主要精力将继续放在移动和 Web 平台上,但Flutter也计划在桌面平台上进行一些改进:
- Flutter希望在 macOS 和 Windows 上支持平台视图,以便支持例如 webview 的功能。
- 在 Linux 上,Flutter的重点是支持 GTK4 和提升无障碍性。
- 在所有平台上,Flutter将继续探索如何从一个 Dart isolate 支持多个视图,最终实现从一个小部件树渲染多个窗口。
生态系统
Flutter 计划与 AI 框架合作,推动 AI 驱动的 Flutter 应用的新时代。
Flutter不会扩大Flutter维护的 flutter.dev 插件 集合,而是专注于提升现有插件的质量,并解决核心功能差距(例如,改进 shared_preferences API,使其更好地支持隔离使用和集成到应用场景中)。Flutter还将支持社区倡议,如 Flutter Favorites。
Flutter还将继续与 Flame 社区合作,支持使用 Flutter 构建休闲游戏。
工具和 AI
Flutter 希望通过集成 AI 解决方案,为开发者提供编程任务的 AI 支持。
Flutter也将继续与谷歌的 IDX 团队 合作,探索与设计工具的集成。
编程语言
Dart 团队计划完成对在 Dart 中支持 宏 的可行性评估,并在 2024 年启动对宏的初步支持。如果发现不可克服的架构问题,Flutter将重新评估这一努力。宏的关键应用场景包括序列化/反序列化、数据类和通用扩展性。
Flutter将研究多个渐进的语言特性,例如减少冗长的语法(如 主构造函数 和 导入语法简写),以及更好地支持静态检查的类型差异。
最后,Flutter将探索如何在更多地方重用 Dart 业务逻辑,并为 Dart 提供更多的插件化和扩展性(例如在 DevTools 和分析器中)。
发布
Flutter 计划在 2024 年进行四次稳定版发布和 12 次测试版发布,这与 2023 年的节奏相似。
最后
最后,大家最关心的Flutter热更新合适支持? 在2024 Flutter团队依然没有支持热更新的计划。
来源:juejin.cn/post/7408848095615713295
改善生活的 50 个秘诀
Happiness 幸福
- You don’t have to love your job. Jobs are a way to make money. Many people live fine lives in okay jobs by using the money they make on things they care about.
1.你不必热爱你的工作。工作是赚钱的一种方式。许多人通过将赚到的钱花在自己关心的事情上,从事着不错的工作,过着美好的生活。
2. Sturgeon’s law states that 90% of everything is crap. If you dislike poetry, or fine art, or anything, it’s possible you’ve only ever seen the crap. Go looking!
2 .斯特金定律指出,90% 的事物都是垃圾。如果你不喜欢诗歌、美术或任何东西,那么你可能只见过垃圾。去寻找吧!
3. People don’t realize how much they hate commuting. A nice house farther from work is not worth the fraction of your life you are giving to boredom and fatigue.
3 .人们没有意识到他们有多么讨厌通勤。远离工作地点的好房子不值得你把生活中的一小部分时间花在无聊和疲劳上。
4. There’s some evidence that introverts and extroverts both benefit from being pushed to be more extroverted. Consider this the next time you aren’t sure if you feel like going out.
4 .有一些证据表明,内向者和外向者都可以从被迫变得更加外向中受益。下次当您不确定是否想出去时请考虑这一点。
- You don’t have to love your job. Jobs are a way to make money. Many people live fine lives in okay jobs by using the money they make on things they care about.
1.你不必热爱你的工作。工作是赚钱的一种方式。许多人通过将赚到的钱花在自己关心的事情上,从事着不错的工作,过着美好的生活。
2. Sturgeon’s law states that 90% of everything is crap. If you dislike poetry, or fine art, or anything, it’s possible you’ve only ever seen the crap. Go looking!
2 .斯特金定律指出,90% 的事物都是垃圾。如果你不喜欢诗歌、美术或任何东西,那么你可能只见过垃圾。去寻找吧!
3. People don’t realize how much they hate commuting. A nice house farther from work is not worth the fraction of your life you are giving to boredom and fatigue.
3 .人们没有意识到他们有多么讨厌通勤。远离工作地点的好房子不值得你把生活中的一小部分时间花在无聊和疲劳上。
4. There’s some evidence that introverts and extroverts both benefit from being pushed to be more extroverted. Consider this the next time you aren’t sure if you feel like going out.
4 .有一些证据表明,内向者和外向者都可以从被迫变得更加外向中受益。下次当您不确定是否想出去时请考虑这一点。
Success 成功
5. History remembers those who got to market first. Getting your creation out int0 the world is more important than getting it perfect.
5 .历史会记住那些最先进入市场的人。将您的创作推向世界比使其完美更重要。
6. Are you on the fence about breaking up or leaving your job? You should probably go ahead and do it. People, on average, end up happier when they take the plunge.
6 .您是否在分手或离职方面犹豫不决?你或许应该继续去做。平均而言,人们在冒险时最终会更快乐。
7. Done is better than perfect.
7 .完成比完美更好。
- Discipline is superior to motivation. The former can be trained, the latter is fleeting. You won’t be able to accomplish great things if you’re only relying on motivation.
8.纪律优于激励。前者是可以训练的,后者是转瞬即逝的。如果你仅仅依靠动力,你将无法完成伟大的事情。 - You can improve your communication skills with practice much more effectively than you can improve your intelligence with practice. If you’re not that smart but can communicate ideas clearly, you have a great advantage over everybody who can’t communicate clearly.
9.通过练习提高你的沟通技巧比通过练习提高你的智力更有效。如果你不那么聪明,但可以清晰地表达想法,那么你比那些不能清晰表达的人有很大的优势。 - You do not live in a video game. There are no pop-up warnings if you’re about to do something foolish, or if you’ve been going in the wrong direction for too long. You have to create your own warnings.
10.你不是生活在电子游戏中。如果您要做一些愚蠢的事情,或者您已经走错方向太久了,那么不会弹出警告。您必须创建自己的警告。
5. History remembers those who got to market first. Getting your creation out int0 the world is more important than getting it perfect.
5 .历史会记住那些最先进入市场的人。将您的创作推向世界比使其完美更重要。
6. Are you on the fence about breaking up or leaving your job? You should probably go ahead and do it. People, on average, end up happier when they take the plunge.
6 .您是否在分手或离职方面犹豫不决?你或许应该继续去做。平均而言,人们在冒险时最终会更快乐。
7. Done is better than perfect.
7 .完成比完美更好。
- Discipline is superior to motivation. The former can be trained, the latter is fleeting. You won’t be able to accomplish great things if you’re only relying on motivation.
8.纪律优于激励。前者是可以训练的,后者是转瞬即逝的。如果你仅仅依靠动力,你将无法完成伟大的事情。 - You can improve your communication skills with practice much more effectively than you can improve your intelligence with practice. If you’re not that smart but can communicate ideas clearly, you have a great advantage over everybody who can’t communicate clearly.
9.通过练习提高你的沟通技巧比通过练习提高你的智力更有效。如果你不那么聪明,但可以清晰地表达想法,那么你比那些不能清晰表达的人有很大的优势。 - You do not live in a video game. There are no pop-up warnings if you’re about to do something foolish, or if you’ve been going in the wrong direction for too long. You have to create your own warnings.
10.你不是生活在电子游戏中。如果您要做一些愚蠢的事情,或者您已经走错方向太久了,那么不会弹出警告。您必须创建自己的警告。
11. If you listen to successful people talk about their methods, remember that all the people who used the same methods and failed did not make videos about it.
11 .如果您听成功人士谈论他们的方法,请记住,所有使用相同方法但失败的人都没有制作有关该方法的视频。
- The best advice is personal and comes from somebody who knows you well. Take broad-spectrum advice like this as needed, but the best way to get help is to ask honest friends who love you.
- 最好的建议是针对个人的,来自熟悉你的人。根据需要接受此类广泛的建议,但获得帮助的最佳方法是询问爱你的诚实朋友。
- Make accomplishing things as easy as possible. Find the easiest way to start exercising. Find the easiest way to start writing. People make things harder than they have to be and get frustrated when they can’t succeed. Try not to.
- 让完成事情尽可能简单。找到开始锻炼的最简单方法。找到开始写作的最简单方法。人们让事情变得比他们必须的更困难,当他们不能成功时就会感到沮丧。尽量不要这样做。
- Cultivate a reputation for being dependable. Good reputations are valuable because they’re rare (easily destroyed and hard to rebuild). You don’t have to brew the most amazing coffee if your customers know the coffee will always be hot.
- 培养可靠的声誉。良好的声誉之所以珍贵,是因为它们很稀有(容易被摧毁,很难重建)。如果您的顾客知道咖啡永远是热的,您就不必煮出最美味的咖啡。
15. How you spend every day is how you spend your life.
15 .你怎样度过每一天,你就怎样度过一生。
Relationships 人际关系
- In relationships look for somebody you can enjoy just hanging out near. Long-term relationships are mostly spent just chilling.
- 在人际关系中寻找一个你可以享受在一起闲逛的人。长期的关系大多只是在寒冷中度过。
- Don’t complain about your partner to coworkers or online. The benefits are negligible and the cost is destroying a bit of your soul.
- 不要向同事或网上抱怨你的伴侣。好处是微不足道的,代价是摧毁你的灵魂。
- After a breakup, cease all contact as soon as practical. The potential for drama is endless, and the potential for a good friendship is negligible. Wait a year before trying to be friends again.
- 分手后,尽快停止所有联系。戏剧的潜力是无穷无尽的,而建立良好友谊的潜力则可以忽略不计。等一年再尝试再次成为朋友。
19. When dating, de-emphasizing your quirks will lead to 90% of people thinking you’re kind of alright. Emphasizing your quirks will lead to 10% of people thinking you’re fascinating and fun. Those are the people interested in dating you. Aim for them.
19 .约会时,淡化你的怪癖会让 90% 的人认为你还不错。强调你的怪癖会让 10% 的人认为你迷人且有趣。这些人有兴趣和你约会。瞄准他们。
- There are two red flags to avoid almost all dangerous people: 1. The perpetually aggrieved ; 2. The angry.
- 几乎所有危险人物都有两个危险信号需要避开: 1. 永远受委屈的人; 2. 永远受委屈的人; 2. 生气的人。
- Those who generate anxiety in you and promise that they have the solution are grifters. See: politicians, marketers, new masculinity gurus, etc. Avoid these.
21.那些让你感到焦虑并承诺他们有解决办法的人都是骗子。请参阅:政客、营销人员、新男性气概大师等。避免这些。
Body 身体
- The 20-20-20 rule: Every 20 minutes of screenwork, look at a spot 20 feet away for 20 seconds. This will reduce eye strain and is easy to remember (or program reminders for).
- 20-20-20 规则:每 20 分钟的屏幕工作,注视 20 英尺外的一个地点 20 秒。这将减轻眼睛疲劳并且易于记住(或设置提醒)。
23. Exercise is the most important lifestyle intervention you can do. Even the bare minimum (15 minutes a week) has a huge impact. Start small.
23 .锻炼是您可以采取的最重要的生活方式干预措施。即使是最低限度(每周 15 分钟)也会产生巨大的影响。从小处开始。
- Phones have gotten heavier in the last decade and they’re actually pretty hard on your wrists! Use a computer when it’s an alternative or try to at least prop up your phone.
- 在过去的十年里,手机变得越来越重,实际上它们对你的手腕来说非常困难!如果可以的话,请使用计算机,或者至少尝试支撑您的手机。
Productivity 生产率
- Learn keyboard shortcuts. They’re easy to learn and you’ll get tasks done faster and easier.
25.学习键盘快捷键。它们很容易学习,您将更快、更轻松地完成任务。 - Keep your desk and workspace bare. Treat every object as an imposition upon your attention, because it is. A workspace is not a place for storing things. It is a place for accomplishing things.
26.保持你的办公桌和工作空间空旷。将每一个物体都视为对你注意力的强加,因为它确实如此。工作空间不是存放东西的地方。这是一个完成事情的地方。 - Reward yourself after completing challenges, even badly.
- 完成挑战后奖励自己,即使是很糟糕的奖励。
25.学习键盘快捷键。它们很容易学习,您将更快、更轻松地完成任务。
26.保持你的办公桌和工作空间空旷。将每一个物体都视为对你注意力的强加,因为它确实如此。工作空间不是存放东西的地方。这是一个完成事情的地方。
Rationality 理性
28. Noticing biases in others is easy, noticing biases in yourself is hard. However, it has a much higher pay-off.
28 .注意到别人的偏见很容易,注意到自己的偏见却很困难。然而,它的回报要高得多。
29. Explaining problems is good. Often in the process of laying out a problem, a solution will present itself.
29 .解释问题很好。通常,在提出问题的过程中,解决方案就会自然出现。
30. Selfish people should listen to advice to be more selfless, selfless people should listen to advice to be more selfish. This applies to many things. Whenever you receive advice, consider its opposite as well. You might be filtering out the advice you need most.
30 .自私的人应该听建议变得更无私,无私的人应该听建议变得更自私。这适用于很多事情。每当你收到建议时,也要考虑它的反面。您可能会过滤掉您最需要的建议。
Compassion 同情
31.Call your parents when you think of them, tell your friends when you love them.
31.想念父母时给他们打电话,爱朋友时告诉他们。
- Compliment people more. Many people have trouble thinking of themselves as smart, or pretty, or kind, unless told by someone else. You can help them out.
32.多赞美别人。许多人很难认为自己聪明、漂亮或善良,除非是别人告诉的。你可以帮助他们。 - Don’t punish people for trying. You teach them to not try with you. Punishing includes whining that it took them so long, that they did it badly, or that others have done it better.
33.不要因为人们的尝试而惩罚他们。你教他们不要和你一起尝试。惩罚包括抱怨他们花了这么长时间,他们做得不好,或者其他人做得更好。
34.Don't punish people for admitting they were wrong, you make it harder for them to improve.
34.不要惩罚那些承认自己错误的人,你会让他们更难进步。
- In general, you will look for excuses to not be kind to people. Resist these.
- 一般来说,你会寻找借口不善待他人。抵制这些。
Possessions 财产
36. Things you use for a significant fraction of your life (bed: 1/3rd, office-chair: 1/4th) are worth investing in.
36 .您一生中很大一部分时间使用的物品(床:1/3,办公椅:1/4)值得投资。
- “Where is the good knife?” If you’re looking for your good X, you have bad Xs. Throw those out.
37.“好刀在哪里?”如果你正在寻找好的X,那么你就会有坏的X。把那些扔掉。 - If your work is done on a computer, get a second monitor. Less time navigating between windows means more time for thinking.
- 如果你的工作是在电脑上完成的,那就买第二台显示器。减少在窗口之间导航的时间意味着有更多的时间思考。
- Establish clear rules about when to throw out old junk. Once clear rules are established, junk will probably cease to be a problem. This is because any rule would be superior to our implicit rules (“keep this broken stereo for five years in case I learn how to fix it”).
- 制定关于何时扔掉旧垃圾的明确规则。一旦制定了明确的规则,垃圾可能将不再是一个问题。这是因为任何规则都优于我们的隐含规则(“将这个损坏的立体声音响保留五年,以防我学会如何修复它”)。
- When buying things, time and money trade-off against each other. If you’re low on money, take more time to find deals. If you’re low on time, stop looking for great deals and just buy things quickly online.
40.买东西时,时间和金钱是相互权衡的。如果您缺钱,请花更多时间寻找优惠。如果您时间不够,请停止寻找超值优惠,只需在网上快速购买即可。
Self 自己
- Deficiencies do not make you special. The older you get, the more your inability to cook will be a red flag for people.
- 缺陷并不会让你变得特别。你年纪越大,你不会做饭对人们来说就越是一个危险信号。
- If you’re under 90, try things.
- 如果你还不到90岁,就尝试一些事情。
43. Things that aren’t your fault can still be your responsibility.
43 .不是你的错的事情仍然可能是你的责任。
44. Defining yourself by your suffering is an effective way to keep suffering forever (ex. incels, trauma).
44 .通过痛苦来定义自己是永远承受痛苦的有效方法(例如非自愿者、创伤)。
- Keep your identity small. “I’m not the kind of person who does things like that” is not an explanation, it’s a trap. It prevents nerds from working out and men from dancing.
45.保持你的身份小。 “我不是那种会做那种事的人”不是解释,而是陷阱。它阻止书呆子锻炼和男人跳舞。 - Don’t confuse ‘doing a thing because I like it’ with ‘doing a thing because I want to be seen as the sort of person who does such things’.
- 不要将“因为喜欢而做一件事”与“因为我想被视为做这种事的人而做某事”混为一谈。
47. Remember that you are dying.
47 .记住你快要死了。
48. Personal epiphanies feel great, but they fade within weeks. Upon having an epiphany, make a plan and start actually changing behavior.
48 .个人的顿悟感觉很棒,但几周后就会消失。顿悟后,制定计划并开始实际改变行为。
Others 其他的
- In choosing between living with 0-1 people vs 2 or more people, remember that ascertaining responsibility will no longer be instantaneous with more than one roommate (“whose dishes are these?”).
- 在与 0-1 人同住还是与 2 人或更多人同住之间进行选择时,请记住,与超过一名室友一起居住时将不再能够立即确定责任(“这些是谁的菜?”)。
- When you ask people, “What’s your favorite book / movie / band?” and they stumble, ask them instead what book / movie / band they’re currently enjoying most. They’ll almost always have one and be able to talk about it.
50.当你问别人“你最喜欢的书/电影/乐队是什么?”他们结结巴巴地问他们目前最喜欢什么书/电影/乐队。他们几乎总是有一个并且能够谈论它。
作者:shengjk1
来源:juejin.cn/post/7403288145196236840
50.当你问别人“你最喜欢的书/电影/乐队是什么?”他们结结巴巴地问他们目前最喜欢什么书/电影/乐队。他们几乎总是有一个并且能够谈论它。
来源:juejin.cn/post/7403288145196236840
Taro搭建支付宝小程序实战
1. 引言
在当今多端应用开发的趋势下,许多开发者面临着需要在不同平台(如微信、支付宝、百度等小程序,以及H5和React Native应用)上编写和维护多套代码的挑战。为了解决这一问题,市场上涌现了多种跨端开发框架,旨在帮助开发者实现“一次编写,多端运行”的目标。Taro 是其中最为流行的框架之一,但在选择开发工具时,了解其他同类框架的优缺点非常重要。
1.1 类似框架的对比
以下是 Taro、Uniapp、WePY 和 MPX 四种多端开发框架的优缺点对比表:
框架 | 优点 | 缺点 |
---|---|---|
Taro | - 支持使用 React 语法,符合许多前端开发者的使用习惯。 - 广泛支持多端,包括微信小程序、支付宝小程序、H5、React Native 等。 - 活跃的社区和丰富的插件生态系统。 - 提供了完善的跨平台 API 兼容性,减少了平台差异的处理工作。 | - 构建时间相对较长,尤其是在多端同时输出时。 - 部分高级特性在某些平台上可能不完全兼容。 |
Uniapp | - 使用 Vue 语法,适合 Vue 开发者。 - 支持广泛的多端输出,包括小程序、H5、App(通过原生渲染)。 - 简单易上手,适合中小型项目。 | - 对复杂业务场景的支持有限,灵活性不如 Taro。 - 生态系统相对 Taro 较弱,插件丰富度不及 Taro。 |
WePY | - 针对微信小程序的开发框架,支持类 Vue 语法。 - 轻量级,专注于微信小程序的开发,简单直接。 | - 多端支持较弱,主要针对微信小程序,跨平台能力不足。 - 社区相对较小,更新频率较慢。 |
MPX | - 提供了增强的组件化编程能力,适合大型复杂小程序项目。 - 拥有更好的编译性能,构建速度较快。 | - 使用自定义语法,学习成本较高。 - 社区资源较少,生态系统不如 Taro 和 Uniapp 丰富。 |
通过这个对比表,我们可以根据项目需求和团队的技术栈选择最适合的多端开发框架。每个框架都有其独特的优势和局限性,因此选择时需要权衡各方面的因素。
1.2 为什么选择 Taro
Taro 作为一个基于 React 的多端开发框架,有以下几大优势使得它在众多选择中脱颖而出:
- 跨平台支持广泛:Taro 支持微信小程序、支付宝小程序、H5、React Native、快应用等多个平台,能够极大地提升开发效率,减少代码重复编写的成本。
- React 生态支持:Taro 使用 React 语法,这使得许多已有的 React 组件和库可以直接复用,开发者不需要学习新的开发模式,便能快速上手。
- 成熟的生态系统:Taro 拥有丰富的社区插件和第三方支持,提供了大量开箱即用的功能模块,如状态管理、路由管理等,这些都能帮助开发者更快地构建应用。
- 持续的更新与支持:Taro 由京东维护,得到了持续的更新与支持,具有较强的社区活力,能够及时响应开发者的需求和问题。
Taro 作为一个成熟且功能强大的多端开发框架,特别适合那些希望一次开发、多平台运行的项目需求。它不仅简化了跨平台开发的复杂性,还提供了丰富的功能支持,使得开发过程更加高效和愉悦。因此,我们选择 Taro 作为开发支付宝小程序的首选工具。
2. 环境搭建
2.1 安装 Taro CLI
首先,你需要安装 Taro 的 CLI 工具。确保你已经安装了 Node.js 环境,然后运行以下命令来全局安装 Taro CLI:
npm install -g @tarojs/cli
2.2 创建项目
安装完成后,可以通过以下命令来创建一个新的 Taro 项目:
taro init myApp
在创建过程中,Taro 会询问你一些选项,如选择框架(默认 React)、CSS 预处理器(如 Sass、Less)等。选择合适的选项后,Taro 会自动生成项目的目录结构和配置文件。
2.3 配置支付宝小程序
在 Taro 项目中,配置支付宝小程序的输出涉及多个方面,包括基本的项目配置、支付宝小程序特有的扩展配置、以及一些针对支付宝平台的优化设置。以下是详细的配置步骤和相关说明。
2.3.1 基本配置
首先,需要在项目的 config/index.js
文件中进行基础配置,确保 Taro 能够正确编译并输出支付宝小程序。
const config = {
projectName: 'myApp',
designWidth: 750,
deviceRatio: {
'640': 2.34 / 2,
'750': 1,
'828': 1.81 / 2
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: [],
defineConstants: {},
copy: {
patterns: [],
options: {}
},
framework: 'react',
compiler: 'webpack5',
cache: {
enable: true
},
mini: {
postcss: {
autoprefixer: {
enable: true,
config: {}
},
pxtransform: {
enable: true,
config: {}
},
url: {
enable: true,
config: {
limit: 10240 // 设置转换尺寸限制
}
}
}
},
h5: {
publicPath: '/',
staticDirectory: 'static',
esnextModules: [],
postcss: {
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: false, // 默认是 false,若需要支持 CSS Modules,设置为 true
config: {
namingPattern: 'module', // 转换模式,支持 global/module
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
}
}
}
2.3.2 支付宝小程序特有的配置
在 config/index.js
文件中,需要在 mini
配置块中进一步设置支付宝小程序的特有配置。Taro 会自动生成支付宝小程序所需的 app.json
、project.config.json
等配置文件,但你可以根据需求自定义一些额外的配置。
mini: {
compile: {
exclude: ['src/lib/alipay-lib.js'] // 示例:排除不需要编译的文件
},
postcss: {
autoprefixer: {
enable: true,
config: {
browsers: ['last 3 versions', 'Android >= 4.1', 'ios >= 8']
}
}
},
// 支付宝小程序特有的配置
alipay: {
component2: true, // 启用支付宝小程序的基础组件规范 v2
axmlStrictCheck: true, // 严格的 axml 校验,帮助发现潜在问题
renderShareComponent: true, // 启用动态组件渲染
usingComponents: { // 注册全局自定义组件
'custom-button': '/components/custom-button/index'
},
// 支付宝扩展配置
plugins: {
myPlugin: {
version: '1.0.0',
provider: 'wx1234567890' // 插件提供者的AppID
}
},
window: {
defaultTitle: '支付宝小程序', // 设置小程序默认的标题
pullRefresh: true, // 支持下拉刷新
allowsBounceVertical: 'YES' // 支持竖向弹性滚动
},
pages: [
'pages/index/index',
'pages/detail/detail'
],
tabBar: {
color: '#000000',
selectedColor: '#1c1c1c',
backgroundColor: '#ffffff',
borderStyle: 'black',
list: [{
pagePath: 'pages/index/index',
text: '首页',
iconPath: 'assets/tabbar/home.png',
selectedIconPath: 'assets/tabbar/home-active.png'
}, {
pagePath: 'pages/detail/detail',
text: '详情',
iconPath: 'assets/tabbar/detail.png',
selectedIconPath: 'assets/tabbar/detail-active.png'
}]
}
}
}
2.3.3 支付宝小程序页面配置
每个页面的配置都在 pages.json
文件中进行定义,Taro 会自动处理这些配置。但你可以根据需要进一步自定义每个页面的表现,如是否启用下拉刷新、页面背景颜色等。
{
"pages": [
"pages/index/index",
"pages/detail/detail"
],
"window": {
"defaultTitle": "我的小程序",
"titleBarColor": "#ffffff",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white",
"backgroundColor": "#ffffff",
"enablePullDownRefresh": true // 启用下拉刷新
}
}
2.3.4 引入支付宝小程序扩展组件
支付宝小程序支持扩展组件和插件,这些可以直接在 usingComponents
中进行引入。例如,如果你需要使用支付宝小程序特有的扩展组件如 RichText
或 Input
,可以在 alipay
配置中进行设置:
alipay: {
usingComponents: {
'rich-text': 'plugin://myPlugin/rich-text', // 引用插件中的组件
'custom-button': '/components/custom-button/index' // 引入自定义组件
}
}
2.3.5 设置支付宝小程序的分包加载
对于较大或复杂的支付宝小程序,你可能希望启用分包加载功能,以减少主包大小并提升加载速度。Taro 支持在配置文件中设置分包:
subPackages: [
{
root: 'packageA',
pages: [
'pages/logs/logs'
]
},
{
root: 'packageB',
pages: [
'pages/index/index'
]
}
],
2.3.6 环境变量配置
在开发和生产环境下,可能需要不同的配置。Taro 支持通过环境变量进行配置管理。你可以在项目根目录下创建 .env
文件,并定义不同环境下的变量:
// .env.development
TARO_ENV = 'alipay'
API_BASE_URL = 'https://api-dev.example.com'
// .env.production
TARO_ENV = 'alipay'
API_BASE_URL = 'https://api.example.com'
然后在代码中通过 process.env
访问这些变量:
const apiBaseUrl = process.env.API_BASE_URL
2.3.7 自定义 Webpack 配置
如果你需要更复杂的配置或优化,可以通过 config/index.js
中的 webpackChain
属性来自定义 Webpack 配置。例如,添加自定义的插件或优化构建过程:
mini: {
webpackChain (chain) {
chain.plugin('analyzer')
.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
}
}
2.3.8 部署和调试
在配置完成后,可以通过以下命令生成支付宝小程序的构建文件:
taro build --type alipay
生成的代码会存放在 dist
目录下,然后可以通过支付宝开发者工具打开并进行调试。
2.3.9 常见问题及解决方法
- 自定义组件路径错误:确保
usingComponents
中的路径正确,特别是相对于dist
目录的相对路径。 - 跨域请求问题:在支付宝小程序中进行网络请求时,确保在
config/index.js
中配置了正确的sourceRoot
和outputRoot
。 - 调试模式不一致:在开发和生产环境中使用不同的 API 端点时,确保环境变量配置正确并在代码中正确使用。
以上是使用 Taro 搭建支付宝小程序的详细配置步骤,涵盖了从基础配置到扩展功能的方方面面。通过这些配置,你可以更加灵活地控制支付宝小程序的表现和功能,满足项目的多样化需求。
3. 重要的 API 和组件
3.1 基础组件
Taro 提供了许多常用的基础组件,如 View
、Text
、Button
等,这些组件与 React 组件类似,但 Taro 的组件具有跨平台能力。
import { View, Text, Button } from '@tarojs/components'
const MyComponent = () => (
<View>
<Text>Hello, Taro!</Text>
<Button onClick={() => alert('Clicked!')}>Click me</Button>
</View>
)
3.2 事件处理
Taro 事件处理机制与 React 类似,可以直接使用 onClick
、onChange
等事件属性。此外,Taro 支持跨平台的事件兼容处理,不需要担心事件名称的差异。
3.3 路由跳转
Taro 提供了 Taro.navigateTo
、Taro.redirectTo
等 API 用于在小程序中进行页面跳转。可以根据具体需求选择合适的跳转方式。
Taro.navigateTo({
url: '/pages/detail/index?id=123'
})
3.4 使用 Hooks
在 Taro 中可以使用 React 的 Hooks,如 useState
、useEffect
等,来管理组件的状态和生命周期。
import { useState, useEffect } from 'react'
import { View } from '@tarojs/components'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
Taro.setNavigationBarTitle({ title: `Count: ${count}` })
}, [count])
return (
<View>
<Button onClick={() => setCount(count + 1)}>Increment</Button>
<Text>{count}</Text>
</View>
)
}
4. 常见问题及坑点
4.1 样式问题
支付宝小程序的 CSS 支持度与微信小程序有所不同,某些高级的 CSS 特性可能无法生效。建议使用基础的 CSS 进行布局,并在项目中引入适当的 polyfill。
4.2 API 差异
在使用 Taro 开发支付宝小程序时,虽然 Taro 提供了跨平台的 API 兼容性,但是由于各个小程序平台的底层架构和能力不同,仍然存在一些需要特别注意的 API 差异。以下将详细说明一些与原生微信小程序不同的、并且在支付宝小程序中经常会用到的特殊 API。
4.2.1 支付宝特有的 API
1. my.request 与 wx.request
尽管 Taro 封装了 Taro.request
来统一请求接口,但在某些特殊情况下,开发者可能需要直接使用原生 API。支付宝小程序的 my.request
与微信的 wx.request
基本相同,但支持一些额外的配置选项,如 timeout
等。
示例:
// 支付宝小程序
my.request({
url: 'https://api.example.com/data',
method: 'GET',
timeout: 5000, // 设置请求超时时间
success: (res) => {
console.log(res.data);
},
fail: (err) => {
console.error(err);
}
});
在微信小程序中,wx.request
不支持 timeout
配置。
2. my.alert 和 wx.showModal
my.alert
是支付宝小程序中用来展示提示框的 API,而微信小程序使用 wx.showModal
来实现类似功能。my.alert
仅支持一个按钮,而 wx.showModal
可以支持两个按钮(确定和取消)。
示例:
// 支付宝小程序
my.alert({
title: '提示',
content: '这是一个提示框',
buttonText: '我知道了',
success: () => {
console.log('用户点击了确定按钮');
}
});
// 微信小程序
wx.showModal({
title: '提示',
content: '这是一个提示框',
showCancel: false, // 不显示取消按钮
success: (res) => {
if (res.confirm) {
console.log('用户点击了确定按钮');
}
}
});
3. my.getAuthCode 与 wx.login
支付宝小程序的 my.getAuthCode
用于获取用户的授权码,通常用于登录验证或支付场景。而微信小程序使用 wx.login
获取用户的登录凭证(code
),这两者在使用上有所不同。
示例:
// 支付宝小程序
my.getAuthCode({
scopes: 'auth_user',
success: (res) => {
console.log('用户授权码:', res.authCode);
},
fail: (err) => {
console.error('获取授权码失败:', err);
}
});
// 微信小程序
wx.login({
success: (res) => {
console.log('用户登录凭证:', res.code);
},
fail: (err) => {
console.error('获取登录凭证失败:', err);
}
});
4. my.navigateToMiniProgram 与 wx.navigateToMiniProgram
这两个 API 都用于跳转到其他小程序。虽然功能类似,但在支付宝小程序中,my.navigateToMiniProgram
有一些额外的参数,比如 extraData
,用于在跳转时传递数据。
示例:
// 支付宝小程序
my.navigateToMiniProgram({
appId: '2021000000000000',
path: 'pages/index/index',
extraData: {
foo: 'bar'
},
success: () => {
console.log('跳转成功');
},
fail: (err) => {
console.error('跳转失败:', err);
}
});
// 微信小程序
wx.navigateToMiniProgram({
appId: 'wx1234567890',
path: 'pages/index/index',
extraData: {
foo: 'bar'
},
success: () => {
console.log('跳转成功');
},
fail: (err) => {
console.error('跳转失败:', err);
}
});
5. my.tradePay 与 wx.requestPayment
my.tradePay
是支付宝小程序用于发起支付的 API,而微信小程序使用 wx.requestPayment
实现同样的功能。两者的参数配置有所不同,尤其是在支付方式和返回结果处理上。
示例:
// 支付宝小程序
my.tradePay({
tradeNO: '202408280000000000001',
success: (res) => {
if (res.resultCode === '9000') {
console.log('支付成功');
} else {
console.log('支付失败', res.resultCode);
}
},
fail: (err) => {
console.error('支付失败:', err);
}
});
// 微信小程序
wx.requestPayment({
timeStamp: '1609459200',
nonceStr: '5K8264ILTKCH16CQ2502SI8ZNMTM67VS',
package: 'prepay_id=wx20170101abc1234567890',
signType: 'MD5',
paySign: 'ABCD1234',
success: () => {
console.log('支付成功');
},
fail: (err) => {
console.error('支付失败:', err);
}
});
6. my.chooseCity 与 wx.chooseLocation
支付宝小程序提供了 my.chooseCity
API 用于选择城市,而微信小程序没有直接对应的 API,但 wx.chooseLocation
可以选择位置,且在选址过程中包含了城市信息。
示例:
// 支付宝小程序
my.chooseCity({
showLocatedCity: true, // 显示当前所在城市
success: (res) => {
console.log('选择的城市:', res.city);
}
});
// 微信小程序
wx.chooseLocation({
success: (res) => {
console.log('选择的位置:', res.name);
console.log('所在城市:', res.address);
}
});
4.2.2 差异化 API 使用的注意事项
- 功能测试:由于 API 存在差异,所以我们需要在不同平台上进行充分测试,确保应用逻辑在所有平台上都能正常运行。
- 代码隔离:对于平台特有的 API,建议通过
Taro.getEnv()
判断运行环境,并使用条件判断来分别调用不同平台的 API,从而实现代码的隔离与复用。 - 兼容性处理:某些 API 在不同平台上可能有不同的参数或返回值格式,因此需要根据平台特性进行兼容性处理。
通过对以上 API 差异的详细了解,希望我们可以更好地在 Taro 项目中处理支付宝小程序与微信小程序的不同需求,提升应用的跨平台兼容性和用户体验。
4.3 路由管理
在 Taro 中,路由管理是跨平台开发中非常重要的一环。尽管 Taro 封装了微信小程序和支付宝小程序的路由操作,但在使用过程中仍然存在一些差异。以下详细说明 Taro 在支付宝和微信小程序中的路由管理方式,包括不同的跳转方式及参数的获取。
4.3.1 路由跳转方式
Taro 中提供了多种路由跳转方式,主要包括 Taro.navigateTo
、Taro.redirectTo
、Taro.switchTab
、Taro.reLaunch
和 Taro.navigateBack
。这些方法封装了微信和支付宝小程序的原生跳转方式,适用于不同的使用场景。
1. Taro.navigateTo
Taro.navigateTo
用于跳转到应用内的指定页面,新的页面会被加入到页面栈中。
示例:
Taro.navigateTo({
url: '/pages/detail/index?id=123&name=abc'
});
- 微信小程序: 页面栈最大深度为10,超过后会自动释放栈顶页面。
- 支付宝小程序: 页面栈最大深度为10,同样超过后会自动释放栈顶页面。
2. Taro.redirectTo
Taro.redirectTo
用于关闭当前页面并跳转到指定页面,跳转后无法返回到原页面。
示例:
Taro.redirectTo({
url: '/pages/home/index'
});
- 微信小程序 和 支付宝小程序 都会将当前页面从栈中移除,不允许用户回退。
3. Taro.switchTab
Taro.switchTab
用于跳转到指定的 tabBar 页面,并关闭其他所有非 tabBar 页面。
示例:
Taro.switchTab({
url: '/pages/home/index'
});
- 微信小程序:
url
必须是 tabBar 页面,否则会报错。 - 支付宝小程序: 同样必须是 tabBar 页面,但支付宝小程序支持使用
extraData
传递额外数据。
4. Taro.reLaunch
Taro.reLaunch
用于关闭所有页面并跳转到指定页面,适用于需要重置应用状态的场景。
示例:
Taro.reLaunch({
url: '/pages/home/index'
});
- 微信小程序 和 支付宝小程序 行为一致,都会关闭所有页面并创建一个新的页面栈。
5. Taro.navigateBack
Taro.navigateBack
用于关闭当前页面并返回到上一级页面或多级页面。
示例:
Taro.navigateBack({
delta: 1 // 返回上一级页面
});
- 微信小程序 和 支付宝小程序 都支持通过
delta
指定返回的页面层级。
4.3.2 获取路由参数
无论是通过哪种方式跳转,页面之间通常需要传递参数。在 Taro 中,参数的传递和获取可以通过 this.$router.params
实现。以下是如何在页面中获取路由参数的详细说明。
1. URL 参数传递
当使用 Taro.navigateTo
或其他跳转方法时,可以在 url
中通过 query
传递参数。
示例:
// 页面跳转
Taro.navigateTo({
url: '/pages/detail/index?id=123&name=abc'
});
// 在目标页面获取参数
componentDidMount() {
const { id, name } = this.$router.params;
console.log('ID:', id); // 输出:ID: 123
console.log('Name:', name); // 输出:Name: abc
}
- 微信小程序: 参数会自动编码并附加到 URL 后。
- 支付宝小程序: 行为类似微信小程序,参数通过 URL query 传递。
2. extraData
参数传递
支付宝小程序允许通过 extraData
传递复杂对象,这在某些复杂场景下非常有用。
示例:
// 页面跳转
my.navigateTo({
url: '/pages/detail/index',
extraData: {
info: {
id: 123,
name: 'abc'
}
}
});
// 在目标页面获取参数
componentDidMount() {
const { info } = this.$router.params;
console.log('Info:', info); // 输出:Info: { id: 123, name: 'abc' }
}
- 微信小程序: 目前不支持
extraData
参数传递,但可以通过globalData
或其他全局状态管理工具如 Redux 实现类似效果。
3. 场景值与 scene
参数
在小程序的入口页面,通常会涉及到场景值(scene
)的获取。Taro 提供了 this.$router.params.scene
来获取微信小程序中的 scene
值,这在处理分享或扫码进入时非常重要。
示例:
componentDidMount() {
const scene = this.$router.params.scene;
console.log('Scene:', scene); // 输出对应的场景值
}
- 微信小程序: 支持
scene
参数传递,主要用于扫码进入或分享。 - 支付宝小程序: 不直接支持
scene
,但可以通过其他方式获取进入场景(如my.getLaunchOptionsSync
)。
4.3.3 注意事项
- 页面栈限制:无论是微信还是支付宝小程序,都有页面栈深度限制(通常为10层)。在开发复杂应用时,合理控制页面跳转的深度,避免栈溢出。
- 参数编码问题:确保传递的 URL 参数已经过适当的编码,避免特殊字符引发问题。
- 页面返回的数据传递:Taro 并未封装类似
onActivityResult
的机制,但可以通过全局状态管理或eventBus
模式来实现页面返回的数据传递。
4.3.4 其他事项
通过上面的一些介绍,就可以更加灵活地使用 Taro 进行路由管理,充分利用不同平台的特性,提升应用的导航体验和用户体验。在使用 Taro 开发多端应用时,除了路由管理和 API 差异之外,还有一些关键点和常见的坑需要注意,以确保应用的稳定性、性能和可维护性。以下是一些使用 Taro 过程中需要特别注意的事项:
1. 跨平台兼容性
尽管 Taro 旨在提供跨平台的开发体验,但不同平台在渲染引擎、组件行为和 API 支持上仍有差异。开发者需要在每个目标平台上进行充分测试,确保功能和表现一致。
注意事项:
- 组件兼容性:某些 Taro 组件在不同平台上的表现可能有所不同,如
scroll-view
的行为在微信和支付宝小程序中略有差异。 - 样式兼容性:不同平台的样式支持不尽相同,如支付宝小程序对部分 CSS 属性的支持较弱,需要进行适配。
- API 兼容性:Taro 提供的统一 API 在不同平台上可能会有细微的差异,建议使用
Taro.getEnv()
进行环境判断,以便针对特定平台编写适配代码。
2. 状态管理
Taro 支持使用多种状态管理工具,如 Redux、MobX、Recoil 等。根据项目的复杂度和团队的技术栈选择合适的状态管理方案。
注意事项:
- 全局状态管理:对于跨页面的数据共享,使用全局状态管理工具能有效避免组件之间直接传递数据的问题。
- 性能优化:在使用状态管理工具时,注意避免不必要的状态更新,尤其是在大规模应用中,应当进行性能调优以减少重渲染。
3. 性能优化
Taro 封装了小程序的框架,尽管提供了便捷性,但这也带来了一些性能开销。性能优化是确保 Taro 应用顺畅运行的关键。
注意事项:
- 懒加载:对不常用的组件或页面使用懒加载技术,减少初次渲染的压力。
- 分包加载:对于较大的应用,可以使用分包加载(特别是在微信小程序中)来优化启动速度。
- 减少组件嵌套:过多的组件嵌套会增加渲染负担,尽量保持组件结构的扁平化。
- 长列表优化:对长列表(如商品列表、评论列表)使用虚拟列表技术,避免一次性加载大量数据。
4. 开发工具与调试
Taro 提供了开发者工具来简化开发和调试过程,但在实际项目中,调试复杂问题时可能会遇到一些挑战。
注意事项:
- 使用 Taro CLI:Taro CLI 提供了丰富的命令行工具,帮助你快速生成项目、构建应用和调试代码。
- 跨平台调试:确保在每个平台的开发者工具中进行调试,并使用平台特有的工具,如微信小程序开发者工具和支付宝 IDE。
- 源代码映射:使用源代码映射(Source Map)功能来调试编译后的代码,方便追踪错误。
5. 小程序的限制
各个小程序平台都有其独特的限制,如包大小限制、API 速率限制、页面栈限制等。在开发过程中,必须遵守这些限制,以免在发布或运行时遇到问题。
注意事项:
- 包大小限制:微信和支付宝小程序对主包和分包的大小都有严格限制,尽量减少不必要的资源,压缩图片和代码。
- 页面栈限制:小程序的页面栈深度通常为 10 层,超出后可能会引发崩溃,需要合理设计页面的跳转逻辑。
- 请求速率限制:各平台对网络请求的速率和并发量都有要求,应当合并请求或使用请求队列来控制频率。
6. 国际化支持
如果应用需要支持多语言,Taro 提供了基础的国际化支持,但由于不同平台的特性,可能需要额外的配置和适配。
注意事项:
- 文本管理:使用统一的国际化管理工具,如 i18next 或自定义的国际化方案。
- 格式化问题:不同平台对日期、货币等格式化方式支持不同,使用第三方库(如
moment.js
)来统一格式化操作。 - 右到左(RTL)布局:如果应用需要支持 RTL 语言,确保在每个平台上都正确实现 RTL 布局。
7. 版本管理与更新
在多端开发中,版本管理和应用更新也是需要特别注意的地方。不同平台对更新机制的支持不尽相同,需要有针对性的处理策略。
注意事项:
- 小程序版本控制:在不同平台上发布新版本时,注意同步版本号,并在应用内做好版本控制。
- 热更新:Taro 目前不直接支持热更新,但可以通过后台配置管理、版本检测等方式实现相似的效果。
- 数据迁移:在更新过程中,可能需要进行数据迁移(如数据库结构变更),确保用户数据的完整性。
8. 社区与文档支持
Taro 社区活跃,文档也在不断完善,但在实际开发中遇到问题时,了解如何有效利用社区资源和官方文档也很重要。
注意事项:
- 官方文档:Taro 官方文档非常详细,建议在遇到问题时首先查阅文档,以获取官方推荐的解决方案。
- 社区支持:遇到文档未覆盖的问题,可以到 GitHub Issues、Gitter 或其他开发者社区寻求帮助。
- 示例项目:参考官方或社区提供的示例项目,可以帮助你快速上手并解决常见问题。
通过注意以上关键点,开发者可以更好地利用 Taro 的跨平台能力,同时避免常见的坑和问题,提升开发效率和应用质量。
官方文档地址: docs.taro.zone/docs
5. 结语
结语
Taro 的出现不仅解决了多端开发的复杂性问题,还大大提升了开发效率和代码的可维护性。通过统一的 API 和组件库,Taro 让开发者无需深入了解每个小程序平台的细节,即可快速构建和部署功能丰富的应用。
然而,正如任何工具或框架一样,Taro 并非完美无缺。跨平台开发固有的挑战仍然存在,包括平台间的差异、性能优化需求、状态管理复杂性,以及不同平台特有的限制。这些挑战提醒我们,尽管 Taro 能够极大地简化开发流程,但在开发过程中依然需要细致地进行测试、调优和适配工作。
从选择 Taro 作为开发框架,到深入了解其核心功能和最佳实践,再到避开潜在的坑和问题,需要全方位地掌握 Taro 的使用技巧。通过合理使用 Taro 的能力,结合自身项目的实际需求,开发者可以实现跨平台的一致用户体验,并保持代码库的可扩展性和维护性。
展望未来,随着小程序生态的不断发展和 Taro 框架的持续更新,开发者将会有更多的机会去探索和创新。Taro 的灵活性和强大的跨平台能力为我们提供了无限的可能,无论是在支付宝小程序、微信小程序,还是在更多的平台上,都能为用户带来一致、流畅的体验。
在使用 Taro 进行多端开发的过程中,我们不仅仅是编写代码,更是在打造一款能够适应多平台需求的高质量应用。通过不断学习和实践,我们能够充分发挥 Taro 的潜力,让每一个用户无论在哪个平台上使用我们的应用,都能感受到同样的便捷与愉悦。
最终,无论是初次使用 Taro 的新手,还是已经熟练掌握的老手,持续学习和优化始终是提升开发能力的关键。Taro 为我们提供了强大的工具,剩下的就是如何用好这些工具,创造出色的产品。相信随着更多开发者的加入和贡献,Taro 生态将会更加繁荣,为跨平台开发带来更多的可能性和惊喜。
作者:洞窝-海林
来源:juejin.cn/post/7408138735798616102
优雅实现任意形状的水球图,领导看了都说好
前言
翌日
我吃着早餐,划着水。
不一会,领导走了过来。
领导:小伙子,你去XX项目实现一个设备能源图,要求能根据剩余能量显示水波高低。
我: 啊?我?这个项目我没看过代码。
领导:任务有点急,你自己安排时间吧,好好搞,给你争取机会。
我:好吧。(谁叫咱只是一个卑微的打工人,只能干咯😎👌😭。)
分析
看到图,类似要实现这样一个立方体形状的东西,然后需要根据剩余电量显示波浪高低。
我心想,这不简单吗,这不就是一个水球图,恰好之前看过echarts中有一个水球图的插件。
想到这,我立马三下五除二,从echarts官网上下载了这个插件,心想下载好了就算搞定了。
波折
哪知,这个需求没有想象中的那么简单,UI设计的图其实是一个伪3D立方体,通过俯视实现立体效果。并且A面和B面都要有波浪。
这就让我犯了难,因为官方提供的demo没有这样的形状,最相近也就是最后一个图案。
那把两个最后一个图案拼接起来,组成A、B面,不就可以达到我们的效果了吗,然后最后顶上再放一个四边形C面,不就可以完美解决了。
想法是好的,但是具体思考实践方案起来就感觉麻烦了。根据我平时的解决问题的经验:如果方案实践起来,发现很麻烦就说明方法错了,需要换个方向思考。
于是我开始翻阅官方文档,找到了关于形状的定义shape属性。
救世主shape
它支持三种方式定义形状
- 最基础的是,直接编写属性内置的值,支持一些内置的常见形状如:
'circle'
,'rect'
,'roundRect'
,'triangle'
,'diamond'
,'pin'
,'arrow'
- 其次,它还支持根据容器形状填充
container
,具体来说就是可以填充满你的渲染容器。比如一个300X300的div,设置完shape:'container
'后,他的渲染区域就会覆盖这个div大小。此时,你可以调整div的形状实现想要的图案,比如
我们用两个div演示,我们将第二个div样式设置为
border-radius: 100%;
第一个图形就为方形,第二个就成为了经典圆形水球图。我们可以根据需要自行让div变成我们想要的形状。
- 最后,也就是我们这次要说的重点,他支持SVG
path://
路径。
我们可以看到第二种方式实现复杂的图形有局促性,第三种方式告诉我们他支持svg的path路径时,这就给了我们非常多的可能性,我们可以通过路径绘制任意想要的图形。
看到这个属性后,岂不是只需要将UI切的svg文件中的path传入进去就可以实现这个效果了?随后开始了分析。
我们的图形可以由三个四边形构成,每个四边形四个顶点,合计12个顶点。
从svg文件我们可以得到如下内容
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="362.74609375"
height="513.7080078125" viewBox="0 0 362.74609375 513.7080078125" fill="none">
<path d="M128.239 177.367L128.239 512.015L361.056 397.17L361.056 76.6519L128.239 177.367Z" stroke="rgba(0, 0, 0, 1)"
stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">
</path>
<path d="M1.69043 107.409L128.231 177.364L361.048 76.6482L229.656 1.69043L1.69043 107.409Z"
stroke="rgba(0, 0, 0, 1)" stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">
</path>
<path d="M1.69043 107.413L1.69043 442.06L128.231 512.015L128.231 177.368L1.69043 107.413Z" stroke="rgba(0, 0, 0, 1)"
stroke-width="3.3802816901408472" stroke-linejoin="round" fill="#FFFFFF">
</path>
</svg>
我们可以发现,它是由三个path路径构成,而我们的水球图只支持一个path://开头的path路径字符串。解决这个问题也很简单,我们只需要将三个路径给他合并在一起就可以了,我们就可以实现这种伪3D效果了。
如此,我们便得到了路径。
path://M128.239 177.367L128.239 512.015L361.056 397.17L361.056 76.6519L128.239 177.367Z M1.69043 107.409L128.231 177.364L361.048 76.6482L229.656 1.69043L1.69043 107.409Z M1.69043 107.413L1.69043 442.06L128.231 512.015L128.231 177.368L1.69043 107.413Z
效果如图:
哇瑟,真不赖,感觉已经实现百分之七八十了,内心已经在幻想,领导看到实现后大悦,说不愧是你呀,然后给我升职加薪,推举升任CTO,赢取白富美,翘着腿坐在库里南里的场景了。
等等!我的线条呢?整个水球图(也不能叫球了,水立方?)只有外轮廓,看不到线条棱角了,其实我觉得现在这种情况还蛮好看的,但是为了忠于UI设计的还原,还是得另寻办法,可不能让人家说菜,这么简单都实现不了。
好在,解决这个问题也很简单,官方提供了边框的配置项。(真是及时雨啊)
backgroundStyle: {
borderColor: "#000",// 边框的颜色
borderWidth: 2, // 边框线条的粗细
color: "#fff", // 背景色
},
配置完边框线的粗细后,啊哈!这不就是我们想要的效果吗?
最后还差一点,再将百分比显示出来,如下图,完美!
拓展
然后我们类比别的图案也是类似,也是只需要将多个path组合在一起就可以了。
悟空
某支
钢铁侠
是不是看起来非常的炫酷,实现方式也是一样,我们只需要将这些图案的path路径传入这个shape属性就行了,然后调整适当的颜色。
注意点:
- 如果图形中包含填充的区域,可以让UI小姐姐,把填充改成线条模拟,用多个线条组成一个面模拟,类似微积分。
- 图形的样式取决于path路径,水球图只支持路径,因此路径上的颜色不能单独设置了,只能通过配置项配置一个整体的颜色。
- 关于svg矢量图标来源,可以上素材网站寻找,如我比较喜欢用的字节图标库、阿里图标库等等
思考
上面实现的水球图有一点让我十分在意,就是图案的是怎么做到根据波浪是否遮挡文字,改变文字的颜色,它做到了即使水球图的波浪漫过了文字,文字会呈现白色,不会因为水漫过后,文字颜色与水球图颜色一致,导致文字不可见。
这个特性也太酷了吧,对于用户体验来说大大增强。
由于强烈的好奇心,我开始研究源码这个是怎么实现的。
看了源码后,恍然大悟,原来是这样的
- 绘制背景
- 绘制内层的文本
- 绘制波浪
- 设置裁剪区域,将波浪覆盖的内层文本部分裁剪掉,留下没有被裁减的地方。(上半部分绿字)
- 绘制外层文本,由于设置了裁剪区域,之后的绘图被限制在裁剪区域。(裁剪区域的下半红字部分)
这样,我们就完成了这个神奇的效果。
下面我提供了一个demo,大家可以通过注释draw中的函数,就能很快明白是怎么一回事了。
值得注意的是
- 内层文本与外层文本的位置需要在同一个位置。
- 裁剪区域位置、大小和波浪的位置、大小重合。
总结
完成这个需求后,领导果然非常高兴给我升了职、加了薪,就在我得意洋洋幻想当上CTO的时候,中午闹钟响了,原来只是中午做了个梦,想到下午还有任务,就继续搬砖去了🤣。
来源:juejin.cn/post/7407995254077767707
这可能是见过的最好用的弹幕库 🔥🔥
最近把五年前写的一个弹幕库给重构了一下,本来两年前就想着做这件事,但是其中有一段工作时间压力很大,所以就搁置了,导致没有时间来做这件事情,最近周末 + 晚上花一些时间重构了下,并把文档好好写了一下。言归正传,这篇文章会介绍部分这个弹幕库有的能力,如果正好符合你的需求或者感兴趣,可以帮忙点点 star 来支持一下。
我们有哪些能力
我们提供了灵活调整轨道,自定义弹幕和容器样式,弹幕运动算法等能力,还在提供非常丰富的钩子来让用户处理自定义的行为,只要你想要的,都能做到,本文档会简单介绍一些能力和一些功能的实现。
- 在线 demo: imtaotao.github.io/danmu/
- github: github.com/imtaotao/da…
- 官方文档:imtaotao.github.io/danmu/docum…
快速开始
对于一个开箱即用的 demo,可以非常简单的接入,如下所示:
import { create } from 'danmu';
const manager = create();
manager.mount('#root');
manager.startPlaying();
// 发送弹幕
manager.push('弹幕内容')
对轨道进行调整
我们对支持类似 CSS calc 表达式的能力,一些位置/宽高等信息都可以用表达式来计算。所以对于轨道来说可以很方便的进行调整。
number
:默认单位为px
。string
:表达式计算。支持(+
,-
,*
,/
)数学计算,只支持%
和px
两种单位。
// 例如,这里的 100% 是指容器宽度(如果是高度相关的配置 100% 就是容器的高度)
manager.setGap('(100% - 10px) / 5');
限制为顶部 3 条弹幕
// 如果我们希望轨道高度为 50px
manager.setTrackHeight('100% / 3');
// 如果不设置渲染区域,轨道的高度会根据默认的 container.height / 3 得到,
// 这可能导致轨道高度不是你想要的
manager.setArea({
y: {
start: 0,
// 3 条轨道的总高度为 150px
end: 150,
},
});
限制为中间 3 条弹幕
manager.setTrackHeight('100% / 3');
manager.setArea({
y: {
start: `50%`,
end: `50% + 150`,
},
});
限制为几条不连续的轨道
限制为几条不连续的轨道,除了要做和连续轨道的操作之外,还需要借助 willRender
这个钩子来实现。
// 如果我们希望轨道高度为 50px,并渲染 0,2,4 这几条轨道
manager.setTrackHeight('100% / 6');
// 设置容器的渲染区域
manager.setArea({
y: {
start: 0,
// 6 条轨道的总高度为 300px
end: 300,
},
});
manager.use({
willRender(ref) {
// 高级弹幕和轨道不强相关,没有 trackIndex 这个属性
if (ref.trackIndex === null) return ref;
// 如果为 1,3,5 这几条轨道就阻止渲染,并重新添加等待下次渲染
if (ref.trackIndex % 2 === 1) {
ref.prevent = true;
manager.unshift(ref.danmaku);
}
return ref;
},
});
自定义渲染
弹幕和容器都允许自定义的渲染样式,你可以很方便的做到。
自定义弹幕的样式
1. 通过 manager.setStyle
来设置
import { create } from 'danmu';
// 需要添加的样式
const styles = {
color: 'red',
fontSize: '15px',
// .
};
const manager = create();
// 后续渲染的弹幕和当前已经渲染的弹幕会设置上这些样式。
for (const key in styles) {
manager.setStyle(key, styles[key]);
}
2. 通过 danamaku.setStyle
来设置
import { create } from 'danmu';
// 需要添加的样式
const styles = {
color: 'red',
fontSize: '15px',
// .
};
// 初始化的时候添加钩子处理,这样当有新的弹幕渲染时会自动添加上这些样式
const manager = create({
plugin: {
$moveStart(danmaku) {
for (const key in styles) {
danmaku.setStyle(key, styles[key]);
}
// 你也可以在这里给弹幕 DOM 添加 className
danmaku.node.classList.add('className');
},
},
});
// 对当前正在渲染的弹幕添加样式
manager.asyncEach((danmaku) => {
for (const key in styles) {
danmaku.setStyle(key, styles[key]);
}
});
自定义容器样式
import { create } from 'danmu';
// 需要添加的样式
const styles = {
background: 'red',
// .
};
const manager = create({
plugin: {
// 你可以在初始化的时候添加钩子处理
init(manager) {
for (const key in styles) {
manager.container.setStyle(key, styles[key]);
}
// 你也可以在这里给容器 DOM 添加 className
manager.container.node.classList.add('className');
},
},
});
// 或者直接调用 api
for (const key in styles) {
manager.container.setStyle(key, styles[key]);
}
高级弹幕的示例
本章节将介绍如何将弹幕固定在某一位置,以 top
和 left
这两个位置举例。由于我们需要自定义位置,所以我们需要使用高级弹幕的能力。
将弹幕固定在顶部
// 这条弹幕将会居中距离顶部 10px 的位置悬停 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
return {
x: `50% - ${danmaku.getWidth() / 2}`,
y: 10, // 具体容器顶部的距离为 10px
};
},
});
固定在顶部第 2 条轨道上
// 这条弹幕将会在第二条轨道居中的位置悬停 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
// 渲染在第 3 条轨道中
const { middle } = manager.getTrackLocation(2);
return {
x: `50% - ${danmaku.getWidth() / 2}`,
y: middle - danmaku.getHeight() / 2,
};
},
});
将弹幕固定在左边
// 这条弹幕将会在容器中间距离左边 10px 的地方停留 5s
manager.pushFlexibleDanmaku('弹幕内容', {
duration: 5000,
direction: 'none',
position(danmaku, container) {
// 渲染在第 3 条轨道中
const { middle } = manager.getTrackLocation(2);
return {
x: 10,
y: `50% - ${danmaku.getHeight() / 2}`,
};
},
});
发送带图片的弹幕
要让弹幕里面能够携带图片,要在弹幕的节点内部添加自定义的内容,实际上不止图片,你可以往弹幕的节点里面添加任何的内容。
本章节的组件以 React 来实现演示。
开发弹幕组件
export function Danmaku({ danmaku }) {
return (
<div>
<img src="https://abc.jpg" />
{danmaku.data}
</div>
);
}
渲染弹幕
import ReactDOM from 'react-dom/client';
import { create } from 'danmu';
import { Danmaku } from './Danmaku';
const manager = create<string>({
plugin: {
// 将组件渲染到弹幕的内置节点上
$createNode(danmaku) {
ReactDOM.createRoot(danmaku.node).render(<Danmaku danmaku={danmaku} />);
},
},
});
编写一个插件
编写一个插件是很简单的,但是借助内核暴露出来的钩子
和 API
,你可以很轻松的实现强大且定制化的需求。由于内核没有暴露出来根据条件来实现过滤弹幕的功能,原因在于内核不知道弹幕内容的数据结构,这和业务的诉求强相关,所以我们在此通过插件来实现精简弹幕的功能用来演示。
编写一个插件
- 你编写的插件应当取一个
name
,以便于调试定位问题(注意不要和其他插件冲突了)。 - 插件可以选择性的声明一个
version
,这在你的插件作为独立包发到npm
上时很有用。
export function filter({ userIds, keywords }) {
return (manager) => {
return {
name: 'filter-keywords-or-user',
version: '1.0.0', // version 字段不是必须的
willRender(ref) {
const { userId, content } = ref.danmaku.data.value;
console.log(ref.type); // 可以根据此字段来区分是普通弹幕还是高级弹幕
if (userIds && userIds.includes(userId)) {
ref.prevent = true;
} else if (keywords) {
for (const word of keywords) {
if (content.includes(word)) {
ref.prevent = true;
break;
}
}
}
return ref;
},
};
};
}
注册插件
你需要通过 mananger.use()
来注册插件。
import { create } from 'danmu';
const manager = create<{
userId: number;
content: string;
}>();
manager.use(
filter({
userIds: [1],
keywords: ['菜'],
}),
);
发送弹幕
- ❌ 会被插件阻止渲染
manager.push({
userId: 1,
content: '',
});
- ❌ 会被插件阻止渲染
manager.push({
userId: 2,
content: '你真菜',
});
- ✔️ 不会被插件阻止渲染
manager.push({
userId: 2,
content: '',
});
- ✔️ 不会被插件阻止渲染
manager.push({
userId: 2,
content: '你真棒',
});
总结
本文档只是简单介绍了下现在的部分能力,更详细的文档在官网可以查看,如果对你的业务或者学习有帮助的,给个 star 支持一下作者,也欢迎大家评论探讨(不止弹幕,哈哈)。
来源:juejin.cn/post/7408364808607957002
别再用模板语法和'+'来拼接url了
在前端开发中,我们经常需要处理URL,例如在发起HTTP请求时构建API端点,或在页面导航时构建动态链接、拼接动态参数。
过去,我们习惯于使用模板语法和字符串拼接来构建这些URL,现在在代码中依然可以看到新的代码还在使用这种方法。
但这种方法不仅容易出错,而且在维护和阅读代码时也不够直观。本文将介绍更现代和更安全的URL构建方法,并展示如何在实际项目中应用它们。
传统上,我们常使用字符串拼接或模板语法来构建URL。例如:
const baseUrl = "https://api.example.com";
const userId = 12345;
const endpoint = baseUrl + "/users/" + userId + "/details";
console.log(endpoint); // "https://api.example.com/users/12345/details"
import { TYPE_EDIT } from '@/constants/type.ts'
const type = TYPE_EDIT
const url = 'https://api.example.com/userInfo'
const newUrl = url + '?type=' + type + '&model=1&share=1&fromModule=wechat'
console.log(urlUrl) // https://api.example.com/userInfo?type=TYPE_EDIT&model=1&share=1&fromModule=wechat
或使用ES6模板字符串:
const baseUrl = "https://api.example.com";
const userId = 12345;
const endpoint = `${baseUrl}/users/${userId}/details`;
console.log(endpoint); // "https://api.example.com/users/12345/details"
import { TYPE_EDIT } from '@/constants/type.ts'
const type = TYPE_EDIT
const url = 'https://api.example.com/userInfo'
const newUrl = url + `?type=${type}&model=1&share=1&fromModule=wechat`
console.log(urlUrl) // https://api.example.com/userInfo?type=TYPE_EDIT&model=1&share=1&fromModule=wechat
虽然模板字符串在一定程度上提高了可读性,但这种方法仍存在几个问题:
- 易读性差:当URL变得复杂时,拼接和模板字符串会变得难以阅读和维护(现阶段已经难以阅读和维护了)。
- 错误处理麻烦:拼接过程中如果有任何错误(例如漏掉斜杠),可能会导致难以排查的BUG。
- 缺乏类型安全:拼接字符串无法提供编译时的类型检查,容易引入错误。
使用URL构造器
为了解决这些问题,现代JavaScript引入了URL构造器,可以更优雅和安全地处理URL。URL构造器提供了一种更结构化和直观的方法来构建和操作URL。
基本用法
const baseUrl = "https://api.example.com";
const userId = 12345;
const url = new URL(`/users/${userId}/details`, baseUrl);
console.log(url.href); // "https://api.example.com/users/12345/details"
添加查询参数
URL构造器还提供了一种简便的方法来添加和操作查询参数:
const baseUrl = "https://api.example.com";
const userId = 12345;
const url = new URL(`/users/${userId}/details`, baseUrl);
url.searchParams.append('type', 'EDIT');
url.searchParams.append('module', 'wechat');
console.log(url.href); // "https://api.example.com/users/12345/details?type=EDIT&module=wechat"
拼接数组参数
假设我们有一个URL,需要将一个数组作为查询参数添加到URL中。
const baseUrl = 'https://example.com';
const url = new URL(baseUrl);
const arrayParam = ['value1', 'value2', 'value3'];
// 将数组转换为逗号分隔的字符串
url.searchParams.set('array', arrayParam.join(','));
console.log(url.toString()); // https://example.com/?array=value1,value2,value3
解析数组参数
当我们获取URL并需要解析其中的数组参数时,可以使用URLSearchParams
对象进行解析。
const urlString = 'https://example.com/?array=value1,value2,value3';
const url = new URL(urlString);
const arrayParamString = url.searchParams.get('array');
// 将逗号分隔的字符串转换回数组
const arrayParam = arrayParamString ? arrayParamString.split(',') : [];
console.log(arrayParam); // ['value1', 'value2', 'value3']
以下是一个完整示例,包括拼接和解析数组参数的操作:
// 拼接数组参数到URL
const baseUrl = 'https://example.com';
const url = new URL(baseUrl);
const arrayParam = ['value1', 'value2', 'value3'];
url.searchParams.set('array', arrayParam.join(','));
console.log(url.toString()); // https://example.com/?array=value1,value2,value3
// 解析数组参数从URL
const urlString = url.toString();
const parsedUrl = new URL(urlString);
const arrayParamString = parsedUrl.searchParams.get('array');
const parsedArrayParam = arrayParamString ? arrayParamString.split(',') : [];
console.log(parsedArrayParam); // ['value1', 'value2', 'value3']
处理多个同名参数
有时我们可能会遇到需要处理多个同名参数的情况,例如?array=value1&array=value2&array=value3
。可以使用URLSearchParams
的getAll
方法:
// 拼接多个同名参数到URL
const url = new URL(baseUrl);
const arrayParam = ['value1', 'value2', 'value3'];
arrayParam.forEach(value => url.searchParams.append('array', value));
console.log(url.toString()); // https://example.com/?array=value1&array=value2&array=value3
// 解析多个同名参数从URL
const urlString = url.toString();
const parsedUrl = new URL(urlString);
const parsedArrayParam = parsedUrl.searchParams.getAll('array');
console.log(parsedArrayParam); // ['value1', 'value2', 'value3']
通过这些方法,可以更加优雅和简便地处理URL中的数组参数,提升代码的可读性和可维护性。
但实际情况往往比上面的示例更复杂,比如参数是一个对象、根据实际情况来设置参数的值、要处理undefined
、'undefined'
、0
、'0'
、Boolean
、'true'
、NaN
等不同类型和异常的值,每次使用时都去处理显然是不合理的,这时候就可以将拼接和移除参数的函数封装成方法来使用。
/**
* 获取URL查询参数并返回一个对象,支持数组
* @param {string} urlString - 需要解析的URL字符串
* @returns {Object} - 包含查询参数的对象
*/
function getURLParams(urlString) {
const url = new URL(urlString);
const params = new URLSearchParams(url.search);
const result = {};
for (const [key, value] of params.entries()) {
if (result[key]) {
if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
} else {
result[key] = value;
}
}
return result;
}
/**
* 设置URL的查询参数,支持对象和数组
* @param {string} urlString - 基础URL字符串
* @param {Object} params - 需要设置的查询参数对象
* @returns {string} - 带有查询参数的URL字符串
*/
function setURLParams(urlString, params) {
const url = new URL(urlString);
const searchParams = new URLSearchParams();
for (const key in params) {
if (params.hasOwnProperty(key)) {
const value = params[key];
if (Array.isArray(value)) {
value.forEach(val => {
if (val !== undefined && !Number.isNaN(val)) {
searchParams.append(key, val);
} else {
console.warn(`Warning: The value of "${key}" is ${val}, which is invalid and will be ignored.`);
}
});
} else if (value !== undefined && !Number.isNaN(value)) {
searchParams.append(key, value);
} else {
console.warn(`Warning: The value of "${key}" is ${value}, which is invalid and will be ignored.`);
}
}
}
url.search = searchParams.toString();
return url.toString();
}
// 测试用例
const baseUrl = 'https://example.com';
// 测试 getURLParams 方法
const testUrl = 'https://example.com/?param1=value1¶m2=value2¶m2=value3';
const parsedParams = getURLParams(testUrl);
console.log(parsedParams); // { param1: 'value1', param2: ['value2', 'value3'] }
// 测试 setURLParams 方法
const params = {
param1: 'value1',
param2: ['value2', 'value3'],
param3: undefined,
param4: NaN,
param5: 'value5',
param6: 0,
};
const newUrl = setURLParams(baseUrl, params);
console.log(newUrl); // 'https://example.com/?param1=value1¶m2=value2¶m2=value3¶m5=value5'
以上代码是根据掌握的知识编写的基本使用示例,像这种工作完全不用自己来写,现在已经有非常成熟的库可以直接使用。
qs
npmjs http://www.npmjs.com/package/qs
它是开源免费项目,每周下载量将近7千万,支持任意字符,对象进行解析和拼接,支持@types/qs
,导入后11.3k,建议打包编译时排除在打包文件外用cdn替代。
query-string
npmjs http://www.npmjs.com/package/que…
它是开源免费项目,每周下载量达千万,支持任意字符、对象进行解析和拼接,支持ts,导入后仅2.5k字节。
PC和H5如果使用了微前端,建议一开始打包时就将依赖排除在打包文件外,用cdn链接来替代,仅加载一次就可以缓存下来,可以加速页面加载、减小打包文件大小。
当然更多时候我们在编写h5、小程序项目的时候并不希望为了一个url解析参数和拼接参数的功能而引入一整个依赖。
这时候一个简单的解析和拼接的函数就可以搞定。
方法有多种实现方式,下面还有一种通过正则来实现的,但下面拼接的时候会忽略数字0,所以参数一定要用字符串。
/**
* 合并查询参数到 URL 的函数
* 将给定的查询对象 Query 合并到指定的 URL 中
*
* @param {Object} query - 要合并到 URL 中的查询对象
* @param {string} url - 作为基础的 URL,默认为当前页面的 URL
* @returns {string} 生成的合并查询参数后的新 URL
*/
export function getUrlMergeQuery(query = {}, url) {
url = url || window.location.href
const _orgQuery = getQueryObject(url)
const _query = {..._orgQuery,...query }
let _arr = []
for (let key in _query) {
const value = _query[key]
if (value) _arr.push(`${key}=${encodeURIComponent(_query[key])}`)
}
return `${url.split('?')[0]}${_arr.length > 0? `?${_arr.join('&')}` : ''}`
}
/**
* 从 URL 中提取查询参数对象
*
* @param {string} [url=window.location.href] - 要解析的 URL 字符串。如果未提供,则使用当前页面的 URL
* @returns {Object} - 包含提取的查询参数的对象
*/
export function getQueryObject(url = window.location.href) {
const search = url.substring(url.lastIndexOf('?') + 1);
const obj = {};
const reg = /([^?&=]+)=([^?&=]*)/g;
search.replace(reg, (rs, $1, $2) => {
const name = decodeURIComponent($1);
let val = decodeURIComponent($2);
val = String(val);
obj[name] = val;
return rs;
});
return obj;
}
你的项目中一定提供了合适的方法,不要在用字符串拼接的方法来拼接参数了。
来源:juejin.cn/post/7392788843097931802
职场进阶:从研发到一线主管
前言
背景是把近来看的管理相关书籍(书籍附文末)和个人思考进行了梳理。在这里顺手把个人关键角色转型的核心分享给大家,希望对大家有所帮助。
至今经历过的三次关键角色转型:
- 从新手到团队骨干
- 从团队骨干到虚线组长
- 从虚线组长到一线主管
本着大道至简的初衷,以下是角色转型的3个关键点:
- 核心:思维转变(重中之重)
- 能力:其次是技能补充(如果已知技能可以提前储备)
- 时间:基于角色的时间管理
关键角色
成为团队骨干
思维转变(重中之重):
- 主人翁意识:具备主人翁意识,不要只着眼于自己的一亩三分地。
- 结果导向:任何事情以结果为导向,适当的“吃亏”。
- 超预期交付:不仅限于完成明确的任务。
补充技能:
- 项目管理能力:协作、沟通、排期和任务管理、风险把控。
时间管理:
- 做好基于任务管理的TODO list
- 项目管理时间
其他:
- 养成思考的习惯。比如,做完一个项目后进行必要的总结。
- 提升技术广度和深度。比如,学习未来可能会用到的技术栈,逐步尝试读源码。
详细总结如下:
成为虚线组长
思维转变(重中之重):
- 具备体系化建设能力,成为某个领域的专家。
补充技能:
- 领导能力。比如能指导其他人完成工作,并且承担培养组员的责任。
时间管理:
- 体系建设时间:调研业界实现,归纳和梳理内部问题,产出基于当前的团队的最佳实践。
- 团队管理时间:组员日常任务管理,负责组员个人成长。
详细总结如下:
成为一线主管
思维转变(重中之重):
- 尽早意识到被衡量成功的方式变了:从你个人的成功转变为团队的成功
- 尽早意识你的时间必然变得零碎:招聘时间、业务和跨团队沟通时间、团队规划时间、项目管理时间、汇报文档时间、预算时间、组员1V1时间、流程和标准建立时间,这就是你的工作内容
补充技能:
- 面试技巧:基于预算范围内招聘高素质、高度自律、高绩效的员工
- 组织和梯队建设能力:基于业务发展动态调整组织,识别团队现状设计梯队成长目标
- 预算管理
时间管理:基于角色的时间管理
- 团队管理者角色:
- 招聘时间
- 流程和标准建立时间
- 业务沟通时间
- 1V1时间:提前准备问题,比如
请你想想,我常做哪些浪费你的时间又不产生效果的事情?
- 培训时间:核心
让每个人知道公司是如何运作的
- 项目管理者角色:
- 项目管理时间
- 个人角色:
- 个人事项时间
详细总结如下:
总结
- 从新手到团队骨干
- 核心思维转变:主人翁意识和超预期交付
- 从团队骨干到虚线组长
- 核心思维转变:从交付单个项目到交付好用系统
- 从虚线组长到一线主管
- 核心思维转变:团队的成功才是你的成功,接受时间碎片化的转变,做好基于角色的时间管理
接下来面临的关键角色转型:
从技术人到具备经营者意识的技术人转变。
来源:juejin.cn/post/7401812292210737162
昨天晚上,RPC 线程池被打满了,原因哭笑不得
大家好,我是五阳。
1. 故障背景
昨天晚上,我刚到家里打开公司群,就看见群里有人讨论:线上环境出现大量RPC请求报错,异常原因:被线程池拒绝。虽然异常量很大,但是异常服务非核心服务,属于系统旁路,服务于数据核对任务,即使有大量异常,也没有实际的影响。
原来有人在线上刷数据,产生了大量 binlog,数据核对任务的请求量大幅上涨,导致线程池被打满。因为并非我负责的工作内容,也不熟悉这部分业务,所以没有特别留意。
第二天我仔细思考了一下,觉得疑点很多,推导过程过于简单,证据链不足,最终结论不扎实,问题根源也许另有原因。
1.1 疑点
- 请求量大幅上涨, 上涨前后请求量是多少?
- 线程池被打满, 线程池初始值和最大值是多少,线程池队列长度是多少?
- 线程池拒绝策略是什么?
- 影响了哪些接口,这些接口的耗时波动情况?
- 服务的 CPU 负载和 GC情况如何?
- 线程池被打满的原因仅仅是请求量大幅上涨吗?
带着以上的几点疑问,第二天一到公司,我就迫不及待地打开各种监控大盘,开始排查问题,最后还真叫我揪出问题根源了。
因为公司的监控系统有水印,所以我只能陈述结论,不能截图了。
2. 排查过程
2.1 请求量的波动情况
- 单机 RPC的 QPS从 300/s 涨到了 450/s。
- Kafka 消息 QPS 50/s 无 明显波动。
- 无其他请求入口和 无定时任务。
这也能叫请求量大幅上涨,请求量增加 150/s 能打爆线程池?就这么糊弄老板…… ,由此我坚定了判断:故障另有根因
2.2 RPC 线程池配置和监控
线上的端口并没有全部被打爆,仅有 1 个 RPC 端口 8001 被打爆。所以我特地查看了8001 的线程池配置。
- 初始线程数 10
- 最大线程数 1024(数量过大,配置的有点随意了)
- 队列长度 0
- 拒绝策略是抛出异常立即拒绝。
- 在 20:11到 20:13 分,线程从初始线程数10,直线涨到了1024 。
2.3 思考
QPS 450次/秒 需要 1024 个线程处理吗?按照我的经验来看,只要接口的耗时在 100ms 以内,不可能需要如此多的线程,太蹊跷了。
2.4 接口耗时波动情况
- 接口 平均耗时从 5.7 ms,增加到 17000毫秒。
接口耗时大幅增加。后来和他们沟通,他们当时也看了接口耗时监控。他们认为之所以平均耗时这么高,是因为RPC 请求在排队,增加了处理耗时,所以监控平均耗时大幅增长。
这是他们的误区,错误的地方有两个。
- 此RPC接口线程池的队列长度为 0,拒绝策略是抛出异常。当没有可用线程,请求会即被拒绝,请求不会排队,所以无排队等待时间。
- 公司的监控系统分服务端监控和调用端监控,服务端的耗时监控不包含 处理连接的时间,不包含 RPC线程池排队的时间。仅仅是 RPC 线程池实际处理请求的耗时。 RPC 调用端的监控包含 RPC 网络耗时、连接耗时、排队耗时、处理业务逻辑耗时、服务端GC 耗时等等。
他们误认为耗时大幅增加是因为请求在排队,因此忽略了至关重要的这条线索:接口实际处理阶段的性能严重恶化,吞吐量大幅降低,所以线程池大幅增长,直至被打满。
接下来我开始分析,接口性能恶化的根本原因是什么?
- CPU 被打满?导致请求接口性能恶化?
- 频繁GC ,导致接口性能差?
- 调用下游 RPC 接口耗时大幅增加 ?
- 调用 SQL,耗时大幅增加?
- 调用 Redis,耗时大幅增加
- 其他外部调用耗时大幅增加?
2.5 其他耗时监控情况
我快速的排查了所有可能的外部调用耗时均没有明显波动。也查看了机器的负载情况,cpu和网络负载 均不高,显然故障的根源不在以上方向。
- CPU 负载极低。在故障期间,cpu.busy 负载在 15%,还不到午高峰,显然根源不是CPU 负载高。
- gc 情况良好。无 FullGC,youngGC 1 分钟 2 次(younggc 频繁,会导致 cpu 负载高,会使接口性能恶化)
- 下游 RPC 接口耗时无明显波动。我查看了服务调用 RPC 接口的耗时监控,所有的接口耗时无明显波动。
- SQL 调用耗时无明显波动。
- 调用 Redis 耗时无明显波动。
- 其他下游系统调用无明显波动。(如 Tair、ES 等)
2.6 开始研究代码
为什么我一开始不看代码,因为这块内容不是我负责的内容,我不熟悉代码。
直至打开代码看了一眼,恶心死我了。代码非常复杂,分支非常多,嵌套层次非常深,方法又臭又长,堪称代码屎山的珠穆朗玛峰,多看一眼就能吐。接口的内部分支将近 10 个,每个分支方法都是一大坨代码。
这个接口是上游 BCP 核对系统定义的 SPI接口,属于聚合接口,并非单一职责的接口。看了 10 分钟以后,还是找不到问题根源。因此我换了问题排查方向,我开始排查异常 Trace。
2.7 从异常 Trace 发现了关键线索
我所在公司的基建能力还是很强大的。系统的异常 Trace 中标注了各个阶段的处理耗时,包括所有外部接口的耗时。如SQL、 RPC、 Redis等。
我发现确实是内部代码处理的问题,因为 trace 显示,在两个 SQL 请求中间,系统停顿长达 1 秒多。不知道系统在这 1 秒执行哪些内容。我查看了这两个接口的耗时,监控显示:SQL 执行很快,应该不是SQL 的问题
机器也没有发生 FullGC,到底是什么原因呢?
前面提到,故障接口是一个聚合接口,我不清楚具体哪个分支出现了问题,但是异常 Trace 中指明了具体的分支。
我开始排查具体的分支方法……, 然而捏着鼻子扒拉了半天,也没有找到原因……
2.8 山穷水复疑无路,柳暗花明又一村
这一坨屎山代码看得我实在恶心,我静静地冥想了 1 分钟才缓过劲。
- 没有外部调用的情况下,阻塞线程的可能性有哪些?
- 有没有加锁? Synchiozed 关键字?
于是我按着关键字搜索Synchiozed
关键词,一无所获,代码中基本没有加锁的地方。
马上中午了,肚子很饿,就当我要放弃的时候。随手扒拉了一下,在类的属性声明里,看到了 Guava限流器。
激动的心,颤抖的手
private static final RateLimiter RATE_LIMITER = RateLimiter.create(10, 20, TimeUnit.SECONDS);
限流器:1 分钟 10次调用。
于是立即查看限流器的使用场景,和异常 Trace 阻塞的地方完全一致。
嘴角出现一丝很容易察觉到的微笑。
破案了,真相永远只有一个。
3. 问题结论
Guava 限流器的阈值过低,每秒最大请求量只有10次。当并发量超过这个阈值时,大量线程被阻塞,RPC线程池不断增加新线程来处理新的请求,直到达到最大线程数。线程池达到最大容量后,无法再接收新的请求,导致大量的后续请求被线程池拒绝。
于是我开始建群、摇人。把相关的同学,还有老板们,拉进了群里。把相关截图和结论发到了群里。
由于不是紧急问题,所以我开开心心的去吃午饭了。后面的事就是他们优化代码了。
4. 思考总结
4.1 八股文不是完全无用,有些八股文是有用的
有人质疑面试为什么要问八股文? 也许大部分情况下,大部分八股文是没有用的,然而在排查问题时,多知道一些八股文是有助于排查问题的。
例如要明白线程池的原理、要明白 RPC 的请求处理过程、要知道影响接口耗时的可能性有哪些,这样才能带着疑问去追踪线索。
4.2 可靠好用的监控系统,能提高排查效率
这个问题排查花了我 1.5 小时,大部分时间是在扒拉代码,实际查看监控只用了半小时。如果没有全面、好用的监控,线上出了问题真的很难快速定位根源。
此外应该熟悉公司的监控系统。他们因为不清楚公司监控系统,误认为接口监控耗时包含了线程池排队时间,忽略了 接口性能恶化 这个关键结论,所以得出错误结论。
4.2 排查问题时,要像侦探一样,不放过任何线索,确保证据链完整,逻辑通顺。
故障发生后第一时间应该是止损,其次才是排查问题。
出现故障后,故障制造者的心理往往处于慌乱状态,逻辑性较差,会倾向于为自己开脱责任。这次故障后,他们给定的结论就是:因为请求量大幅上涨,所以线程池被打满。 这个结论逻辑不通。没有推导过程,跳过了很多推导环节,所以得出错误结论,掩盖了问题的根源~
其他人作为局外人,逻辑性会更强,可以保持冷静状态,这是老板们的价值所在,他们可以不断地提出问题,在回答问题、质疑、继续排查的循环中,不断逼近事情的真相。
最终一定要重视证据链条的完整。别放过任何线索。
出现问题不要慌张,也不要吃瓜嗑瓜子。行动起来,此时是专属你的柯南时刻
我是五阳,关注我,追踪更多我在大厂的工作经历和大型翻车现场。
来源:juejin.cn/post/7409181068597313573
别再混淆了!一文带你搞懂@Valid和@Validated的区别
上篇文章我们简单介绍和使用了一下Springboot的参数校验,同时也用到了 @Valid 注解和 @Validated 注解,那它们之间有什么不同呢?
区别
先总结一下它们的区别:
- 来源
- @Validated :是Spring框架特有的注解,属于Spring的一部分,也是JSR 303的一个变种。它提供了一些 @Valid 所没有的额外功能,比如分组验证。
- @Valid:Java EE提供的标准注解,它是JSR 303规范的一部分,主要用于Hibernate Validation等场景。
- 注解位置
- @Validated : 用在类、方法和方法参数上,但不能用于成员属性。
- @Valid:可以用在方法、构造函数、方法参数和成员属性上。
- 分组
- @Validated :支持分组验证,可以更细致地控制验证过程。此外,由于它是Spring专有的,因此可以更好地与Spring的其他功能(如Spring的依赖注入)集成。
- @Valid:主要支持标准的Bean验证功能,不支持分组验证。
- 嵌套验证
- @Validated :不支持嵌套验证。
- @Valid:支持嵌套验证,可以嵌套验证对象内部的属性。
这些理论性的东西没什么好说的,记住就行。我们主要看分组和嵌套验证是什么,它们怎么用。
实操阶段
话不多说,通过代码来看一下分组和嵌套验证。
为了提示友好,修改一下全局异常处理类:
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 参数校检异常
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseResult handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
StringJoiner joiner = new StringJoiner(";");
for (ObjectError error : bindingResult.getAllErrors()) {
String code = error.getCode();
String[] codes = error.getCodes();
String property = codes[1];
property = property.replace(code ,"").replaceFirst(".","");
String defaultMessage = error.getDefaultMessage();
joiner.add(property+defaultMessage);
}
return handleException(joiner.toString());
}
private ResponseResult handleException(String msg) {
ResponseResult result = new ResponseResult<>();
result.setMessage(msg);
result.setCode(500);
return result;
}
}
分组校验
分组验证是为了在不同的验证场景下能够对对象的属性进行灵活地验证,从而提高验证的精细度和适用性。一般我们在对同一个对象进行保存或修改时,会使用同一个类作为入参。那么在创建时,就不需要校验id,更新时则需要校验用户id,这个时候就需要用到分组校验了。
对于定义分组有两点要特别注意:
- 定义分组必须使用接口。
- 要校验字段上必须加上分组,分组只对指定分组生效,不加分组不校验。
有这样一个需求,在创建用户时校验用户名,修改用户时校验用户id。下面对我们对这个需求进行一个简单的实现。
- 创建分组
CreationGr0up 用于创建时指定的分组:
public interface CreationGr0up {
}
UpdateGr0up 用于更新时指定的分组:
public interface UpdateGr0up {
}
- 创建用户类
创建一个UserBean用户类,分别校验 username
字段不能为空和id
字段必须大于0,然后加上CreationGr0up
和 UpdateGr0up
分组。
/**
* @author 公众号-索码理(suncodernote)
*/
@Data
public class UserBean {
@NotEmpty( groups = {CreationGr0up.class})
private String username;
@Min(value = 18)
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
}
- 创建接口
在ValidationController 中新建两个接口 updateUser
和 createUser
:
@RestController
@RequestMapping("validation")
public class ValidationController {
@GetMapping("updateUser")
public UserBean updateUser(@Validated({UpdateGr0up.class}) UserBean userBean){
return userBean;
}
@GetMapping("createUser")
public UserBean createUser(@Validated({CreationGr0up.class}) UserBean userBean){
return userBean;
}
}
- 测试
先对 createUser
接口进行测试,我们将id的值设置为0,也就是不满足id必须大于0的条件,同样 username 不传值,即不满足 username 不能为空的条件。 通过测试结果我们可以看到,虽然id没有满足条件,但是并没有提示,只提示了username不能为空。
再对 updateUser
接口进行测试,条件和测试 createUser
接口的条件一样,再看测试结果,和 createUser
接口测试结果完全相反,只提示了id最小不能小于1。
至此,分组功能就演示完毕了。
嵌套校验
介绍嵌套校验之前先看一下两个概念:
- 嵌套校验(Nested Validation) 指的是在验证对象时,对对象内部包含的其他对象进行递归验证的过程。当一个对象中包含另一个对象作为属性,并且需要对这个被包含的对象也进行验证时,就需要进行嵌套校验。
- 嵌套属性指的是在一个对象中包含另一个对象作为其属性的情况。换句话说,当一个对象的属性本身又是一个对象,那么这些被包含的对象就可以称为嵌套属性。
有这样一个需求,在保存用户时,用户地址必须要填写。下面来简单看下示例:
- 创建地址类 AddressBean
在AddressBean 设置 country
和city
两个属性为必填项。
@Data
public class AddressBean {
@NotBlank
private String country;
@NotBlank
private String city;
}
- 修改用户类,将AddressBean作为用户类的一个嵌套属性
特别提示:想要嵌套校验生效,必须在嵌套属性上加 @Valid
注解。
@Data
public class UserBean {
@NotEmpty(groups = {CreationGr0up.class})
private String username;
@Min(value = 18)
private Integer age;
private String email;
@Min(value = 1 ,groups = {UpdateGr0up.class})
private Long id;
//嵌套验证必须要加上@Valid
@Valid
@NotNull
private AddressBean address;
}
- 创建一个嵌套校验测试接口
@PostMapping("nestValid")
public UserBean nestValid(@Validated @RequestBody UserBean userBean){
System.out.println(userBean);
return userBean;
}
- 测试
我们在传参时,只传 country
字段,通过响应结果可以看到提示了city
字段不能为空。
可以看到使用了 @Valid
注解来对 Address 对象进行验证,这会触发对其中的 Address 对象的验证。通过这种方式,可以确保嵌套属性内部的对象也能够参与到整体对象的验证过程中,从而提高验证的完整性和准确性。
总结
本文介绍了@Valid
注解和@Validated
注解的不同,同时也进一步介绍了Springboot 参数校验的使用。不管是 JSR-303、JSR-380又或是 Hibernate Validator ,它们提供的参数校验注解都是有限的,实际工作中这些注解可能是不够用的,这个时候就需要我们自定义参数校验了。下篇文章将介绍一下如何自定义一个参数校验器。
来源:juejin.cn/post/7344958089429434406
如何访问数组最后一个元素
原文链接:blog.ignacemaes.com/the-easy-wa…
在JavaScript中,想要获取数组的最后一个元素并不是一件简单的事情,尤其是和一些其他编程语言相比。比如说,在Python里,我们可以通过负数索引轻松访问数组的最后一个元素。但是在JavaScript的世界里,负数索引这一招就不管用了,你必须使用数组长度减一的方式来定位最后一个元素。
比如说,我们有一个数组,里面装着一些流行的前端框架:
const frameworks = ['Nuxt', 'Remix', 'SvelteKit', 'Ember'];
如果我们尝试用负数索引去访问它:
frameworks[-1];// 这里是不会得到结果的
你会发现,这样做是行不通的,它不会返回任何东西。正确的做法是使用数组的长度减一来获取最后一个元素:
frameworks[frameworks.length - 1];// 这样就能拿到'Ember'了
at方法
为了让数组索引变得更加灵活,JavaScript引入了一个新方法——at
。这个方法可以让你通过索引来获取数组中的元素,并且支持负数索引。
frameworks.at(-1);// 这样就能直接拿到'Ember'了
不过,需要注意的是,at
方法只是一个访问器方法,它并不能用来改变数组的内容。如果你想要改变数组,还是得用传统的方括号方式。
// 这样是不行的
frameworks.at(-1) = 'React';
// 正确的改变数组的方法是这样的
frameworks[frameworks.length - 1] = 'React';
with方法
另外,如果你想要改变数组的元素并且得到一个新的数组,而不是改变原数组,JavaScript还提供了一个with
方法。这个方法可以帮你做到这一点,但是它会返回一个新的数组,原数组不会被改变。
// 这样会返回一个新的数组,原数组不变
frameworks.with(-1, 'React');
但是从2023年7月开始,它已经在主流浏览器中得到了支持。Node.js从20.0.0版本开始也支持了这个方法。
使用with
方法,你可以非常方便地修改数组中的元素,并且不用担心会影响到原始数组。这就好比是你在做饭的时候,想要尝尝味道,但又不想直接从锅里尝,于是你盛出一小碗来试味,锅里的菜还是原封不动的。
const updatedFrameworks = frameworks.with(-1, 'React');
// updatedFrameworks 就是 ['Nuxt', 'Remix', 'SvelteKit', 'React']
// 而 frameworks 仍然是原来的数组 ['Nuxt', 'Remix', 'SvelteKit', 'Ember']
兼容性
现在,我们来聊聊这两个方法在浏览器中的兼容性。at
方法从2022年开始已经在主流浏览器中得到了支持,Node.js的当前所有长期支持版本也都支持这个方法。
如果你需要在老旧的浏览器上使用这些方法,别担心,core-js
提供了相应的polyfill。
这样的设计思路,其实是在鼓励我们写出更加模块化和可维护的代码。你不需要担心因为修改了一个元素而影响到整个数组的状态,这对于编写清晰、可靠的代码是非常有帮助的。
如果你需要在一些比较老的浏览器上使用这些方法,你可能需要引入一个polyfill来填补浏览器的不足。core-js
这个库就提供了这样的功能,它可以让你的代码在不同的环境中都能正常运行。
总结
总结一下,at
方法和with
方法为我们在JavaScript中操作数组提供了更多的便利。它们让我们可以用一种更加直观和灵活的方式来访问和修改数组,同时也保持了代码的清晰和模块化。虽然这些方法是近几年才逐渐被引入的,但是它们已经在现代浏览器中得到了很好的支持。如果你的项目需要在老旧的浏览器上运行,记得使用polyfill来确保你的代码能够正常工作。这样,无论是新手还是经验丰富的开发者,都能够轻松地利用这些新特性来提升我们的编程体验。
来源:juejin.cn/post/7356446170477215785
用electron写个浏览器给自己玩
浏览器这种东西工程量很唬人,但是有了electron+webview我们就相当于只需要干组装的活就可以了,而且产品目标就是给自己玩,
成品的效果
😄本来想写成专业的技术博客,但是发现大家好像对那种密密麻麻,全是代码的技术博客不感兴趣,我就挑重点来写吧。
下载拦截功能
下载逻辑如果不做拦截处理的话,默认就是我们平常写web那种弹窗的方式,既然是浏览器肯定不能是那样的。
electron中可以监听BrowserWindow的页面下载事件,并把拿到的下载状态传给渲染线程,实现类似浏览器的下载器功能。
//这个global.WIN = global.WIN = new BrowserWindow({ ...})
global.WIN.webContents.session.on('will-download', (evt, item) => {
//其他逻辑
item.on('updated', (evt, state) => {
//实时的下载进度传递给渲染线程
})
})
页面搜索功能
当时做这个功能的时候我就觉得完了,这个玩意看起来太麻烦了,还要有一个的功能这不是头皮发麻啊。
查资料和文档发现这个居然是webview内置的功能,瞬间压力小了很多,我们只需要出来ctrl+f的时候把搜索框弹出来这个UI就可以了,关键字变色和下一个都是内部已经实现好了的。
function toSearch() {
let timer
return () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
if (keyword.value) {
webviewRef.value.findInPage(keyword.value, { findNext: true })
} else {
webviewRef.value.stopFindInPage('clearSelection')
}
}, 200)
}
}
function closeSearch() {
showSearch.value = false
webviewRef.value.stopFindInPage('clearSelection')
}
function installFindPage(webview) {
webviewRef.value = webview
webviewRef.value.addEventListener('found-in-page', (e) => {
current.value = e.result.activeMatchOrdinal
total.value = e.result.matches
})
}
当前标签页打开功能
就是因为chrome和edge这些浏览器每次使用的时候开非常多的标签,挤在一起,所以我想这个浏览器不能主动开标签,打开了一个标签后强制所有的标签都在当前标签覆盖。
app.on('web-contents-created', (event, contents) => {
contents.setWindowOpenHandler((info) => {
global.WIN?.webContents.send('webview-url-is-change')
if (info.disposition === 'new-window') {
return { action: 'allow' }
} else {
global.WIN?.webContents.send('webview-open-url', info.url)
return { action: 'deny' }
}
})
})
渲染线程监听到webview-open-url后也就是tart="_blank"的情况,强制覆盖当前不打开新窗口
ipcRenderer.on('webview-open-url', (event, url) => {
try {
let reg = /http|https/g
if (webviewRef.value && reg.test(url)) {
webviewRef.value.src = url
}
} catch (err) {
console.log(err)
}
})
标签页切换功能
这里的切换是css的显示隐藏,借助了vue-router
这里我们看dom就能清晰的看出来。
地址栏功能
地址栏支持输入url直接访问链接、支持关键字直接打开收藏的网站、还支持关键字搜索。优先级1打开收藏的网页 2访问网站 3关键字搜索
function toSearch(keyword) {
if (`${keyword}`.length === 0) {
return false
}
// app搜索
if (`${keyword}`.length < 20) {
let item = null
const list = [...deskList.value, ...ALL_DATA]
for (let i = 0; i < list.length; i++) {
if (
list[i].title.toUpperCase().search(keyword.toUpperCase()) !== -1 &&
list[i].type !== 'mini-component'
) {
item = list[i]
break
}
}
if (item) {
goApp(item)
return false
}
}
// 网页访问
let url
if (isUrl(keyword)) {
if (!/^https?:\/\//i.test(keyword)) {
url = 'http://' + keyword
} else {
url = keyword
}
goAppNewTab(url)
return false
} else {
// 关键字搜索
let searchEngine = localStorage.getItem('searchEngine')
searchEngine = searchEngine || CONFIG.searchEngine
url = searchEngine + keyword
if (!router.hasRoute('search')) {
router.addRoute({
name: 'search',
path: '/search',
meta: {
title: '搜索',
color: 'var(--app-icon-bg)',
icon: 'search.svg',
size: 1
},
component: WebView
})
keepAliveInclude.value.push('search')
}
router.push({
path: '/search',
query: { url }
})
setTimeout(() => {
Bus.$emit('toSearch', url)
}, 20)
}
}
桌面图标任意位置拖动
这个问题困扰了我很久,因为它不像电脑桌面大小是固定的,浏览器可以全屏也可以小窗口,如果最开始是大窗口然后拖成小窗口,那么图标就看不到了。后来想到我干脆给个中间区域固定大小,就可以解决这个问题了。因为固定大小出来起来就方便多了。这个桌面是上下两层
//背景格子
<div v-show="typeActive === 'me'" class="bg-boxs">
<div
v-for="(item, i) in 224" //这里有点不讲究了直接写死了
:key="item"
class="bg-box"
@dragenter="enter($event, { x: (i % 14) + 1, y: Math.floor(i / 14) + 1 })"
@dragover="over($event)"
@dragleave="leave($event)"
@drop="drop($event)"
></div>
</div>
// 桌面层
// ...
import { ref, computed } from 'vue'
import useDesk from '@/store/deskList'
import { storeToRefs } from 'pinia'
export default function useDrag() {
const dragging = ref(null)
const currentTarget = ref()
const desk = useDesk()
const { deskList } = storeToRefs(desk)
const { setDeskList, updateDeskData } = desk
function start(e, item) {
e.target.classList.add('dragging')
e.dataTransfer.effectAllowed = 'move'
dragging.value = item
currentTarget.value = e
console.log('开始')
}
let timer2
function end(e) {
dragging.value = null
e.target.classList.remove('dragging')
setDeskList(deskList.value)
if (timer2) {
clearTimeout(timer2)
}
timer2 = setTimeout(() => {
updateDeskData()
}, 2000)
}
function over(e) {
e.preventDefault()
}
let timer
function enter(e, item) {
e.dataTransfer.effectAllowed = 'move'
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
if (item?.x) {
dragging.value.x = item.x
dragging.value.y = item.y
}
}, 100)
}
function leave(e) {}
function drop(e) {
e.preventDefault()
}
return { start, end, over, enter, leave, drop }
}
东西太多了就先介绍这些了
安装包地址
也可以到官网后aweb123.com 如何进入微软商店下载,mac版本因为文件大于100mb没有传上去所以暂时还用不了。
来源:juejin.cn/post/7395389351641612300
准备离开杭州
上个月的时候,我被公司裁掉了,陆陆续续找了 1 个月的工作,没有拿到 1 份 Offer,从网上看着各式各样的消息和自己的亲身体会,原来对于像我这样的普通打工族,找工作是如此的难。我相信,任何时候只要实力强,都能有满意的工作,但我不知道,能达到那样的水平还需要多久。
本人是前端,工作 6 年,期间经历过 4 家公司,前两份是外包,后面两份都是领大礼包走的,回想起来,职业生涯也是够惨的。虽然说惨,但是最近领的这一份大礼包个人认为还是值得,工作很难待下去,也没有任何成长,继续待着也是慢性死亡。
这几天我每天都会在 BOSS 上面投十几家公司,能回复非常少,邀请面试的就更少了。外包公司倒是挺多的,而我是从那个火坑里出来的,是不会选择再进去的。于是,我需要做好打持久战的准备,说不定不做程序员了。
我的房子 7 月底就要到期了,我必须要马上做决定,杭州的行情对我来说很不友好,短期内我大概率找不到工作。基于对未来的悲观考虑,我不想把过多的钱花费在房租上面,所以希望就近找一个三线城市,我搜了一下嘉兴,整租 95 平左右的房子只需要 1200 块钱,还是民用水电,思前想后,打算移居到那里嘉兴去。
一方面,我想尝试一下在三线城市生活是一种什么感觉。另一方面,这可以省钱,如果一个月的房租是 1000,民用水电,一个月的开销只要 2500 块。我搜索了一下货拉拉,从我的位置运到嘉兴,需要花费 600 块钱,这个价格也是可以接受的。思考了这些,我觉得是时候离开待了 5 年的杭州。
未来要到哪里去呢,目前可能的选择是上海。我还得想想未来能做什么,我想学一门手艺傍身,比如修理电器、炒菜。毕竟骑手行业太拥挤了,估计也不是长久之计。
房租降下来了,等我把行李都安置妥当,我打算回老家待一段时间。自从上大学以来,很少有长时间待在家里的时候,眼看父母年纪也越来越大了,很想多陪陪他们。如果进入正常的工作节奏,想做到这样还是会受到局限,这次也算是一个弥补的机会。
被裁也是一件好事,可以让我提前考虑一下未来的出路。
这段时间我想把时间用来专门学英语,再自己做几个项目,学英语的目的是为了 35 岁之后做打算,做项目是为了写到简历上面,并且个人觉得自己需要多做一个项目,这样自己才能成长到下一个级别。虽然不知道收益怎么样,但是我想尝试一下。人还活着,有精力,就还是瞎折腾一下。
离职没敢和家里说,说了估计要担心死了,反正是年轻人,有事就先自己扛一扛,我前几天把我的行李寄回去了一批,我妈问我,怎么,寄东西回来了?我回答说要搬家了。本来也想找机会开口说自己离职了,她说,这次搬家也别离公司远了,我也把话憋了进去,只好说“没事的,放心就行”。我自己没觉得离职有什么,正常的起起落落,只是觉得父母可能会过度的担心。
如果做最坏的打算,那就是回去种地,应该这几年还饿不死。有还没离职的同学,建议还是继续苟着。希望社会的低谷期早点过去,希望我们都能有美好的未来。
来源:juejin.cn/post/7395523104743178279
效率跃升16倍!火山引擎ByteHouse助力销售数据平台复杂查询效率大幅提高
销售数据,是反映市场趋势、消费者行为以及产品表现的重要指标,也是企业做出精准决策的关键依据。因此,对销售数据进行全面利用、高效分析与合规管理,在企业经营中占据着重要地位。
为了更高效、安全地使用销售数据,某公司引入了开源ClickHouse作为数据分析引擎,将分散的销售数据统一到一套可视化分析平台中,并采用鉴权ACL模式来精细化管理企业内部员工的看数、用数权限。
但实际上,该公司销售数据平台在引入鉴权ACL后,出现了性能不足、用户体验受损的状况。其一,ClikHouse的性能难以满足复杂且量级巨大的查询需求,使得集群复杂恶化;其二,ClickHouse集群的CPU使用率长期处于打满状态对用户体验造成影响。
为了解决以上问题,在复杂查询领域具备显著优势且完全兼容ClickHouse的ByteHouse成为该公司迁移首选。
据了解,ByteHouse支持优化器和MPP执行模型,能够较好地支持复杂join与聚合计算的场景。其中,ByteHouse 的优化器在RBO与CBO方向上分别进行了大量的自研优化,并且实现了动态 Filter 下推、物化视图改写、计划复用以及结果复用等高阶优化能力。从而能够根据表的结构、索引等信息生成最优的查询执行计划,提高查询执行效率,减少资源消耗,整体上提升了ByteHouse在复杂场景下的查询性能。
在ByteHouse的支持下,目前该公司在销售数据的非ACL查询和ACL查询两个方向上,都实现了查询效率的显著提升。以ACL查询的60M广告客户DI场景为例,查询效率已经从优化前的16秒大幅缩短至如今的1秒,效率提升高达16倍。
图:抽取该公司销售平台某数据集测试结果
作为新一代云原生数仓产品,ByteHouse在离线、在线复杂分析性能、便捷弹性扩缩容、全场景分析引擎等核心能力上持续优化,并已在互联网、游戏、金融、气象等领域广泛应用。未来,ByteHouse持续以卓越的数据分析能力,为更多业务系统赋能,助力企业数智化转型升级。(作者:李双)
收起阅读 »豆包大模型全面升级:语言模型提升20.3%,图像、语音再进阶
在近日举办的火山引擎AI创新巡展上海站活动中,火山引擎谭待对外表示,相比于5月15日正式发布的版本,豆包语言模型在3个月内,整体综合能力提升了20.3%。
谭待表示,这意味着豆包大模型可以在越来越多的生产力环节中得到应用,在企业服务中更具竞争力。
具体来说,角色扮演能力提升了38.3%,语言理解方面提升33.3%,同时在长文任务,以及数学、专业知识、代码能力等方面也都有不同程度增强。
在图像创作方面,豆包大模型对“文生图模型”进行了升级迭代。新的模型在长文本图文匹配能力方面表现得更加精准,使用户通过文字描述,就可以对图片生成提出更精确的需求。
另外,对于多主体、多位置、人物手部结构等复杂问题,新模型均有大幅提升。新的文生图模型对于中国风格的人物、物品、艺术风格都有着更深理解,未来在设计、广告、营销、电商等多领域,都可以帮助企业解决更多实际问题。
语音模型方面,语义识别准确性进行了相关升级。
对此,谭待在现场举例加以说明。他表示,自2022年冬奥会后,越来越多的人开始喜欢滑雪运动,但在滑雪运动领域中,存在非常多专业的术语,如立刃、搓雪等等,在以往,模型对此很难识别。
但是现在,通过更加精准的上下文理解,人们在讲滑雪相关话题时,模型就可以更好地加以理解。
谭待认为,语音大模型的进一步演进,是实现AI与人之间实时流畅的对话,即在对话中,人可以像与其他人对话一样,去随机打断AI、纠正AI,甚至与AI争辩,而不是像回合制游戏一样,你说一句,我说一句。
对此,火山引擎将大模型与实时音频技术(RTC)相结合,从而能够提供端到端的大模型实时对话能力,企业可以在自身的AI应用中具体应用这一实时语音功能,让用户真正做到和模型非常直接、自由的对话。
通过视频Demo,谭待还在现场具体演示了大模型实时对话能力。他表示,通过将大模型与RTC结合,人与AI之间实现了更加自然的对话,首先是对话可以随时插话、打断,实现了如同真人之间的对话效果。
同时,在以上的前提下,AI声音仍然具备很好的表现力和情感色彩,让用户体验到与真人交流的感觉,并且AI也更加“懂”用户。
最后,通过大模型推理与RTC端到端优化的叠加,火山引擎已经可以将这种人机对话的延迟做到1秒以内,即使在网络环境很差,可能80%丢包的情况下,仍然可以保持非常清晰、流畅的通话质量。
谭待表示,相信这样的新技术,可以让AI时代的人机交互,上升到一个新的高度。(作者:李双)
收起阅读 »【前端缓存】localStorage是同步还是异步的?为什么?
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
首先明确一点,localStorage是同步的
🥝 一、首先为什么会有这样的问题
localStorage
是 Web Storage API 的一部分,它提供了一种存储键值对的机制。localStorage
的数据是持久存储在用户的硬盘上的,而不是内存。这意味着即使用户关闭浏览器或电脑,localStorage
中的数据也不会丢失,除非主动清除浏览器缓存或者使用代码删除。
当你通过 JavaScript 访问 localStorage
时,浏览器会从硬盘中读取数据或向硬盘写入数据。然而,在读写操作期间,数据可能会被暂时存放在内存中,以提高处理速度。但主要的特点是它的持久性,以及它不依赖于会话的持续性。
🍉 二、硬盘不是io设备吗?io读取不都是异步的吗?
是的,硬盘确实是一个 IO 设备,而大部分与硬盘相关的操作系统级IO操作确实是异步进行的,以避免阻塞进程。不过,在 Web 浏览器环境中,localStorage
的API是设计为同步的,即使底层的硬盘读写操作有着IO的特性。
js代码在访问 localStorage
时,浏览器提供的API接口通常会处于js执行线程上下文中直接调用。这意味着尽管硬盘是IO设备,当一个js执行流程访问 localStorage
时,它将同步地等待数据读取或写入完成,该过程中js执行线程会阻塞。
这种同步API设计意味着开发者在操作 localStorage
时不需要考虑回调函数或者Promise等异步处理模式,可以按照同步代码的方式来编写。不过,这也意味着如果涉及较多数据的读写操作时,可能对性能产生负面影响,特别是在主线程上,因为它会阻塞UI的更新和其他js的执行。
🍑 三、完整操作流程
localStorage
实现同步存储的方式就是阻塞 JavaScript 的执行,直到数据的读取或者写入操作完成。这种同步操作的实现可以简单概述如下:
- js线程调用: 当 JavaScript 代码执行一个
localStorage
的操作,比如localStorage.getItem('key')
或localStorage.setItem('key', 'value')
,这个调用发生在 js 的单个线程上。 - 浏览器引擎处理: 浏览器的 js 引擎接收到调用请求后,会向浏览器的存储子系统发出同步IO请求。此时 js 引擎等待IO操作的完成。
- 文件系统的同步IO: 浏览器存储子系统对硬盘执行实际的存储或检索操作。尽管操作系统层面可能对文件访问进行缓存或优化,但从浏览器的角度看,它会进行一个同步的文件系统操作,直到这个操作返回结果。
- 操作完成返回: 一旦IO操作完成,数据要么被写入硬盘,要么被从硬盘读取出来,浏览器存储子系统会将结果返回给 js 引擎。
- JavaScript线程继续执行: js 引擎在接收到操作完成的信号后,才会继续执行下一条 js 代码。
在同步的 localStorage
操作期间,由于 js 的单线程性质,整个线程会阻塞,即不会执行其他任何js代码,也不会进行任何渲染操作,直到 localStorage
调用返回。
🍒 四、localStorage限制容量都是因为同步会阻塞的原因吗?
- 资源公平分享:同一用户可能会访问大量不同的网站,如果没有限制,随着时间的积累,每个网站可能会消耗大量的本地存储资源。这样会导致本地存储空间被少数几个站点占用,影响到用户访问其他网页的体验。限制大小可以确保所有网站都有公平的存储机会。
- 防止滥用:如果没有存储限制,网站可能会滥用
localStorage
,存储大量数据在用户的设备上,这可能导致设备存储空间迅速耗尽,也可能侵犯用户的隐私。 - 性能限制:如之前提到的,
localStorage
的操作是阻塞的。如果网站能够存储大量数据,就会加剧读写操作对页面性能的影响。 - 存储效率:
localStorage
存储的是字符串形式的数据,不是为存储大量或结构化数据设计的。当尝试存储过多数据时,效率会降低。 - 历史和兼容性:5MB 的限制很早就已经被大多数浏览器实现,并被作为一个非正式的标准被采纳。尽管现在有些浏览器支持更大的
localStorage
,但出于跨浏览器兼容性的考虑,开发者通常会假设这个限制。 - 浏览器政策:浏览器厂商可能会依据自己的政策来设定限制,可能是出于提供用户更一致体验的角度,或者是出于管理用户数据的方便。
🍐 五、那indexDB会造成滥用吗?
虽然它们提供了更大的存储空间和更丰富的功能,但确实潜在地也可能被滥用。但是与相比 localStorage
增加了一些特性用来降低被滥用的风险:
- 异步操作:
IndexedDB
是一个异步API,即使它被用来处理更大量的数据,也不会像localStorage
那样阻塞主线程,从而避免了对页面响应性的直接影响。 - 用户提示和权限:对于某些浏览器,当网站尝试存储大量数据时,浏览器可能会弹出提示,要求用户授权。这意味着用户有机会拒绝超出合理范围的存储请求。
- 存储配额和限制:尽管
IndexedDB
提供的存储容量比localStorage
大得多,但它也不是无限的。浏览器会为IndexedDB
设定一定的存储配额,这个配额可能基于可用磁盘空间的一个百分比或者是一个事先设定的限额。配额超出时,浏览器会拒绝更多的存储请求。 - 更清晰的存储管理:
IndexedDB
的数据库形式允许有组织的存储和更容易的数据管理。用户或开发者可以更容易地查看和清理占用的数据。 - 逐渐增加的存储:某些浏览器实现
IndexedDB
存储时,可能会在数据库大小增长到一定阈值时,提示用户是否允许继续存储,而不是一开始就分配一个很大的空间。
🤖 六、一个例子简单测试一下
其实也不用测,平时写的时候你也没用异步的方式写localStorage吧,我们这里简单写个例子
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script>
const testLocalStorage = () => {
console.log("==========> 设置localStorage之前");
localStorage.setItem('testLocalStorage', '我是同步的');
console.log("==========> 获取localStorage之前");
console.log('=========获取localStorage', localStorage.getItem('testLocalStorage'))
console.log("==========> 获取localStorage之后");
}
testLocalStorage()
script>
body>
html>
来源:juejin.cn/post/7359405716090011659
高德API花式玩法:租房辅助工具
前言
做gis的同学肯定知道等时圈这个东西,即:在一定时间内通过某种出行方式能到达的范围
通过一些计算,我们可以做一些好玩的事情,比如上图通过调用mapbox等时圈接口,计算在蓝色点位附近骑车17分钟可以到达哪些公司或学校。
可能由于国内交通情况更为复杂,做实时性较差的等时圈意义不大,所以高德地图提供了比较稳当的API:公交地铁到达圈
,其概念和等时圈类似,你可以选择地铁或公交出行,或者两者兼可,高德接口将计算出可达范围。
高德公交到达圈的好玩应用
用了很久的高德,发现这个功能好像没有被很好的应用,其实高德早在1.4的API中就提供了此功能,最近正好要换个房子,突然想到这个东西正好可以拿来做一个很棒的辅助。
由于我跟我对象上班的地方离得比较远,所以找个折中的地方租房是是很重要的,我准备查询到两个到达圈后,再计算一下重合部分,在重合部分找房,就准确多了。
使用AMap.ArrivalRange画出到达圈
首先我做了一个简单的页面
左上角的面板用来设置参数,可选地铁+公交
、地铁
、公交
三种方式,出行耗时最大支持60分钟(超过了接口会报错),位置需要传入经纬度。
初始化地图的步骤就不用说了,我讲一下这个小应用的使用逻辑。
首先,点击地图时,拾取该位置的经纬度,并通过逆地理接口获取到位置文本
function handleMapClick(e: any) {
if (!e.lnglat) return
if (currPositionList.value.length >= 2) {
autolog.log("最多添加 2 个位置", 'error') // 你不会想三个人一起住吧?
return
}
var lnglat = e.lnglat;
geocoder.getAddress(lnglat, (status: string, result: {
regeocode: any; info: string;
}) => {
if (status === "complete" && result.info === "OK") {
currPositionList.value.push({ name: result.regeocode.formattedAddress, lnglat: [lnglat.lng, lnglat.lat] })
}
});
}
可以看到,我把点选的位置信息,暂时存放到了currPositionList
里面,比如你在西二旗上班,而你女朋友在国贸,则点选后效果是这样的
左侧面板新增了两个位置,点击查询时,我将依次查询这两个到达圈,并渲染到地图上
function getArriveRange() {
let loopCount = 0
for (let item of currPositionList.value) {
arrivalRange.search(item.lnglat, currTime.value, (_status: any, result: { bounds: any; }) => {
map.remove(polygons);
if (!result.bounds) return
let currPolygons = []
loopCount++
for (let item of result.bounds) {
let polygon = new AMap.Polygon(polygonStyle[`normal${loopCount}` as "normal1" | "normal2"]);
polygon.setPath(item);
currPolygons.push(polygon)
}
map.add(currPolygons);
polygons.push({
lnglat: item.lnglat,
polygon: currPolygons,
bounds: result.bounds
})
if (loopCount === currPositionList.value.length) {
map.setFitView();
}
}, { policy: currStrategy.value });
}
}
由于接口调用方式是以回调函数的形式返回的,所以我这里记录了一下回调次数,当次数满足后,再去调整视角。这段逻辑运行之后,将是如下结果:
很遗憾,你和你的女朋友,下班后的一个小时内见不成面了,这意味着,如果找一个折中的地方租房,你们上班单程通勤,无论如何都超过一个小时了,如果想尽量接近一个小时,那么看下两者的交汇处
大钟寺地铁站将会是个很好的选择。
这样仍需要我们手动去观察,那么能不能算一下两者的交集呢?
在高德地图中使用 turf.js 计算多多边形交集
在上面提到的getArriveRange
函数中,我新增了这样的逻辑
if (loopCount === currPositionList.value.length) {
let poly1 = turf.multiPolygon(toNumber(polygons[0].bounds));
let poly2 = turf.multiPolygon(toNumber(polygons[1].bounds));
var intersection = turf.intersect(turf.featureCollection([poly1, poly2]));
if (intersection) {
let geojson = new AMap.GeoJSON({
geoJSON: {
type: "FeatureCollection",
features: [intersection]
},
getPolygon: (_: any, lnglats: any) => {
return new AMap.Polygon({
path: lnglats,
...polygonStyle.overlap
});
}
});
polygons.push({
lnglat: [0, 0],
polygon: geojson,
bounds: intersection.geometry.coordinates
})
map.add(geojson);
} else {
autolog.log("暂无交集,请自行查找", 'error')
}
map.setFitView();
}
由于高德地图到达圈获取到的经纬度是字符串,放到 turf 里面会报错,所以这里写了一个简单的递归,将多维数组所有的数据都转化为数字。
// 递归的将多维数组内的字符串转为数字
function toNumber(arr: any) {
return arr.map((item: any) => {
if (Array.isArray(item)) {
return toNumber(item)
} else {
return Number(item)
}
})
}
使用turf.multiPolygon
将获取到的多维数组转化为标准的 geojson 格式,以便于 turf 处理,在turf7.x
中,turf.intersect的用法稍有改变,需要turf.featureCollection([poly1, poly2])
作为参数传入。
这一步操作 turf 将计算并返回两个多多边形的交集intersection
(geojson),但是在高德地图API2.0中,直接传入这个geojson会报错(1.4不会),看了下源码,发现高德有一个操作是直接取第 0 个features
,导致它识别不了这种格式的数据,所以我们手动处理下,即:
let geojson = new AMap.GeoJSON({
geoJSON: {
type: "FeatureCollection",
features: [intersection]
},
getPolygon: (_: any, lnglats: any) => {
return new AMap.Polygon({
path: lnglats,
...polygonStyle.overlap
});
}
});
这样,高德就可以正确渲染这个数据了,这里需要注意的是,geojson 虽然也是 AMap.Polygon 构造的,但是需要一个特殊参数:path,没有的话,也不会报错,但是渲染不出来。
渲染完成后是这样的:
使用绿色代表重合部分,说明在这之中找房都是可以的。
通过 AMap.PlaceSearch 搜索交集区域的小区
高德提供了通过多边形区域搜索POI的接口
placeSearch = new AMap.PlaceSearch({ //构造地点查询类
pageSize: 5, // 单页显示结果条数
pageIndex: 1, // 页码
map: map, // 展现结果的地图实例
autoFitView: true // 是否自动调整地图视野使绘制的 Marker点都处于视口的可见范围
});
placeSearch.searchInBounds('小区', intersection.geometry.coordinates);
效果如上图所示,这样就可以轻松租房啦!
但是由于此接口是 get 请求,如果交集区域过大,会超出 get 请求长度限制:
结语
这个就叫产品思维,一个简单的API可以延伸出很多有趣的应用。
此仓库已在 github 开源,地址:
番外
高德云镜(高德云镜三维重建平台)目前已向企业和政府开放使用(暂未对个人开发者开放)
在web端,高德开发了 Cesium 插件用作展示,但目前来看要求配置过高
- CesiumJS引擎:CesiumJS v1.117+(建议)
- 浏览器:Chrome v126+(建议)
- 显卡:16GB显存以上独立显卡,推荐 NVDIA RTX 4090
- CPU:2.5 GHz 以上(建议)
- 内存:32GB 以上(建议)
看起来类似谷歌地球的全量城市建模。
来源:juejin.cn/post/7403991780512399387
2024-08-28 跟着国家政策走
认知迭代
回顾过去改革开放以来,那些赚钱比较多的,基本都是跟对了政策的方向,吃到了由此带来的福利,比如90年代那波做外贸的,2000年后做房地产和互联网的。
在快速发展的当今时代,国家政策成为引导社会发展的重要指南针。对于个人而言,紧跟国家政策不仅是明智的选择,更是实现自身价值和梦想的关键。
国家政策代表了国家的意志和方向,它汇集了众多专家的智慧和经验,经过深思熟虑后形成。
紧跟国家政策还可以为我们提供更多的机遇和资源。国家为了推动某项政策的实施,往往会投入大量的人力、物力和财力。这意味着我们只要紧跟国家政策,就有可能获得更多的支持和帮助。
观念改变命运,思路决定出路。要想人生总富有,跟着国家政策走。要想人生总辉煌,不同时期改改行!
昨日回顾
锻炼上,昨晚先带着儿子做了蹲下起立,俯卧撑和仰卧起做,并跑了一会热身,然后把娃送回家,才开始去跑的三公里,跑起来感觉有点疲惫,确实身体素质下降得比较多。
为了解决思维导图协作的问题,昨天花了两个小时,在后端、后台系统和导图的项目,加上了增删改查的功能,因为以后还有200本的绘本需要做四维导出,存电脑本地还是不方便。
昨天下午,没想到会有一个粉丝找我付费咨询职业发展的问题,和他聊了半小时,感慨波多,即使他是985毕业,毕业后就去了腾讯工作了6年,后面一直在小公司,近期被裁员后,一直没有面试的机会,我认为除了环境的因素,更多是没有做好职业规划。
晚上和一个做校招面试辅导的朋友,交流了一下想法,制定了一些改进的方向,通过讨论,感觉这个项目还是有机会的。
今日安排
- 锻炼上:晚上跑一个三公里,俯卧撑和仰卧起坐各50个,蹲下起立100个;
- 工作上:
- 确定跨学科美育课程50个美术知识点对应的200本绘本;
- 调研一下3D模型的制作和灯光效果的技术实现;
- 讨论秋季续费和上课安排的方案;
- 3D 画展加上浏览量和支持点赞,原来的点赞改成留言。
- 参加银河的复试。
- 生活上:和美团的同事一起吃个午饭。
来源:juejin.cn/post/7408072039922581556
老板说,2 天开发一个 App,双端支持,我做到了
老板说,2 天开发一个 App,我用 Expo 做到了,当然,学习怎么使用 Expo 花了1个小时时间不算哈。Expo 是一个非常强大的工具,特别适合那些想要快速构建和发布React Native应用的开发者。你有没有遇到过这种情况?刚刚上手React Native,发现配置开发环境、调试代码这些事情耗费了太多时间,而你真正想做的是快速看到成果。那么,Expo 就是为你量身定做的解决方案。
首先,Expo 是一个开源框架,背后有一个强大的社区支持。你可以在 Expo 的 GitHub 仓库 找到它的源码、更新日志以及社区贡献的内容。这也意味着,你可以完全掌控你项目的每一个细节,而且社区成员之间的经验分享和合作让开发变得更加顺畅。
1. Expo 的核心特点
你可能会问,Expo 和普通的 React Native 开发有什么不同?Expo 的一大特点就是“省心”。它帮你封装了大量底层配置,让你不需要花时间在复杂的环境搭建上。想要启动一个新项目?只需几条命令,你的开发环境就配置好了,甚至不需要接触到原生代码。这对于不太熟悉 iOS 和 Android 原生开发的前端开发者来说,简直是福音。话又说回来,如果想看源码,人家也没拦着你,因为生态是开源的。
2. 零门槛开发
如果你还没用过 Expo CLI,那你一定要试试。通过几条简单的命令,你就可以创建并运行一个 React Native 应用。Expo Go 应用甚至允许你直接在手机上预览你的应用,而不需要复杂的配置。这就像是给你装了一双翅膀,让你可以随时随地测试你的应用。
🗄️ npx create-expo-app@latest
🌭 bunx create-expo-app
📦 pnpm create expo-app
🧶 yarn create expo-app
3. 丰富的生态系统
Expo 的生态系统也是它的一大亮点。它内置了大量的常用功能模块,比如相机、位置服务、传感器等等,你可以直接调用这些API,而不需要自己动手去编写原生代码。而且,Expo SDK 每年都会发布几次更新,哦不好意思,每个月都会更新,奶奶的,我刚用就从 50 更新到 51 了,也够速度的,但是好在,是兼容的,好处是确保你能用上最新最酷的功能,比如 react native 的恐怖性能的新架构,妥妥的给安排上。
你也不用担心这些功能的性能问题。Expo 团队非常注重性能优化,确保你的应用能在各类设备上流畅运行。
使用相机,使用数据库啥的,一个 import 搞定,兼容 API,双端几乎一致的体验简直爽大爆炸。
import { CameraView } from 'expo-camera';
import * as SQLite from 'expo-sqlite';
4. 云端构建与发布
说到发布,Expo 还提供了EAS(Expo Application Services),这个服务可以帮你处理繁琐的构建和发布流程。你只需专注于开发,剩下的事情交给EAS就好。无论是 iOS 还是 Android 平台,它都能帮你轻松搞定。更棒的是,你可以通过EAS进行云端构建,不再需要配置繁琐的构建环境。我比较好奇的是他尽然帮我托管了我的签名,所以基本上意味着交给 eas 去构建,发布到 Google play,和 App Store 就是点点鼠标的事情,但是前提是你得功能测试过,不要闪退和白屏。
5. 社区与支持
最让人欣慰的是,Expo 背后有一个活跃的社区。你可以随时在GitHub上提出问题,或者浏览别人已经解决的类似问题。除此之外,Expo 的文档非常详细,新手也能很快上手。如果你想了解某个API的用法,文档里都有详细的示例代码,这让学习曲线变得非常平滑。我遇到的一些问题就是在 docs 上找答案,比如如何本地构建,如何弹出原生模块,因为有可能需要做一些原生开发。
6. 什么时候不该用Expo?
当然,Expo 也并不是万能的。如果你需要使用某些非常特殊的原生功能,Expo 可能并不能完全满足你的需求。在这种情况下,你可能需要“弹出”Expo(也就是所谓的“eject”),从而使用纯粹的 React Native 环境。这时候,你就要自己管理所有原生模块了。
不过,对于大多数应用开发者来说,特别是那些不太熟悉原生开发的前端,Expo 已经足够强大。这里页打一只强心针,只要不是那些小众的三方库,比如腾讯云 cos,基本上问题不大。
个人感觉,Expo是简化了开发流程的,而且哦还为你提供了强大的工具和服务。你只需要专注于编写业务代码,正在做移动端,或者想做移动端开发的,快去试试吧,我相信你会爱上它的。
反问一波
那位说,你知道不是搞 Flutter 的吗,怎么突然就用 react native 了呢?我想说的是,这些都是工具而已,就好比我们夹菜用筷子,喝粥用瓢羹。关键看什么需求,如果你的 App 要求双端 UI 渲染一致性非常高,有非常多高性能动画的需求,那么 Flutter 很适合你,如果你需要快速实现需求,对双端一致性没那么强,且你对 web 开发很熟悉,ok,react native 是你比较好的选择,能说的就是这么多。
来源:juejin.cn/post/7403288145197744168
前端程序员职业发展方向和学习路线
大家好呀,我是前端创可贴。

不管是什么行业和职业,都要做好相应的了解,提前做好职业规划和学习路线,否则在现如今的大环境下,倘若摸着石头过河,很容易还没摸到石头,就被大水冲跑了。
尤其是对于现在的应届毕业生,大环境让很多人都很难找到心仪的工作,竞争的人很多,岗位却变少了。有了清晰的职业发展方向后,便可提前做好技术积累,给面试官一个 surprise。
面试官 be like:

今天咱们就来聊一聊,作为前端程序员,我们的职业发展思路和方向,可以是什么样子的呢。
Advancement Path
先来看看晋升路线。
程序员的技术职业生涯,这里咱们暂且先不提转岗为管理层,技术人的 title 可大致分为几个等级:初级
、中级
、高级
、资深
、专家
、架构师
、CTO
。

每个阶段所掌握的技术内容,和需要具备的能力,都是不尽相同的。我们需要基于当前的等级,规划未来几年需要达到什么样的层次,为了达到这个等级我需要掌握什么样的技能,而不是盲目去学习试图可以很自然的就得到了晋升。
初级前端需要掌握怎么使用框架进行基本的开发,学习产品开发思维和提升学习能力,需要掌握基本的 HTML、CSS、JS 相关知识,例如原型链、闭包、作用域、异步编程等;
中级前端需要接触一些更深刻的问题,例如如何进行一些基本的工程化配置和原理、了解浏览器的渲染原理、常用框架的一些实现原理、了解性能优化、了解网络协议、掌握 TS 等等;
高级前端需要掌握浏览器运行机制、性能优化、代码重构、不同框架的底层实现原理、前端工程化、技术选型、可视化开发、熟悉 Docker、了解后端知识、算法、设计模式、项目组织结构、指导新人等等;
资深前端需要掌握跨端、前端的高级应用、管理一个小组、掌握大型项目的架构设计、掌握前端领域的研究方法、跟进前端最新发展趋势、掌握前端运维部署、掌握计算机底层原理等等;
专家需要带领团队攻坚技术难题、管理前端团队、培养有潜力的团队成员、提高团队整体工作效率等等;
架构师一般不会仅停留在纯前端或纯后端,需要做到统筹帷幄,需要设计系统架构,保障系统的稳定性、安全性、扩展性等等;
CTO 就是一家公司技术层面的最高负责人了,需要带领公司进行技术探索,保障整个公司的系统稳定,探索公司未来的技术发展方向等等,CTO 一般需要较高的学历或极其丰富的阅历,才能作为公司的技术招牌、稳定人心、拉到投资。

不同的公司对于不同的职级会有不同的划分,以上仅是参考,以实际具体的公司要求为准,不同职级所需要掌握的技术内容也没有限制那么严格,毕竟学无止境,会的更多当然更好。当然也不是说一定非要学那么多,但是至少要保障有一些核心竞争力,才不会被淘汰。
Basic
程序员有一些永恒的话题:程序员到底需不需要高学历?程序员英文不好到底行不行?前端/Java/... 到底有没有前途?...
这些问题的答案其实每个人的回答可能都不太相同,我想以我的过往经历来认真的回答一下,给大家一个参考。
- 程序员到底需不需要高学历?
不可否认,程序员(尤其是前端)的门槛没有那么的高,不像有些职业,对于普通的岗位都设定为最低硕士,甚至是博士。程序员大部分基本上没有这个要求,在以前,有个大专、普通本科的学历就可以了,再会一点代码,应届生可以随随便便找个工作。
但是到了现在,相信很多刚毕业的同学,已经经历过社会的第一次拷打了吧,应届生越来越难找工作,因为和你竞争的人太多了,一名专科和一名 985 毕业同学,谁的入职几率更大呢?我们没有学历歧视,但是现在的情况就是僧多粥少,企业是既要还要,现在就是一个全民降本增效的年代。

所以答案是:不是一定要,但是尽量要。虽然很多人在上学期间不知道读书的重要性,在工作之后才反应过来,然后勤奋苦学,通过自己努力也可能实现不小的成就,但是企业都是现实的,不会去赌你的未来,而且有可能你在一家企业只会呆上两三年,也等不到你实现成就,为公司带来价值。
那么对于因为学历屡屡碰壁的同学该怎么办呢?只能前期猥琐发育,先找份工作安定下来,不眼高手低,先入行、后学习、再跳槽,当你有了很多的技术积累,还是能缩小很多差距的。
如果没有好项目,写在简历上不够亮眼,自己的技术积累又得不到展示,就只能另辟蹊径,让企业知道你的水平。比如去贡献开源项目、开博客写文章、自己做项目、做个人网站、包装自己简历等等,都是大家值得去试试的方法。总之,要想方设法让别人看到你的能力,给予你足够的正反馈,否则你会陷入对自己的怀疑:我学了那么多,为什么 Boss 还是已读不回,我还需要继续努力学习吗。
学习相对来说本来就很痛苦,要是得不到企业的认可,很难保持动力一直坚持下去,所以一定要表现出来,至少可以争得很多面试机会,通过面试也可以检验一下自己的水平,还可以做到查缺补漏。
- 程序员英文不好到底行不行?
我说的残酷点,不行!或者说对于想要在技术生涯里有所成就的,英文一定要好,至少可以看懂英文文档、博客等。
有人可能会嗤之以鼻,反正我都是靠百度过来的,要是实在需要看懂英文的文档我机器翻译不就好了?
踩过坑的人都知道,很多时候,技术文档和博客,英文版都比中文版要更新更全更详细,也很少像国内一些文章那样鱼龙混杂,相对来说会更靠谱。我就遇到过一次,我平时都是看英文版的 MDN,不记得为什么有一次我看了中文版的,然后我的 bug 对照文档上的说明,根本没有问题,但是就是有 bug,调试了很长时间都不知道原因是什么,最后切换回了英文版,马上就看到了不一样的说明,英文版更加详细,立马就解决了我的问题。
而且,全球🌐最大的同性交友网站 GitHub 上绝大多数都是英文,全球🌐 最大的程序员问答网站 Stack Overflow 也是如此,所以想要解决、查询或发帖询问一些前沿技术的问题,能读写英文还是非常有必要的。

而且 GitHub 上的开源项目源码,基本上全都是英文,想让你的开源项目做到世界闻名,用英文是最基本的条件啦。像 Java 这样的语言,很多类、方法等都可以直接跳转到源码,并且会有很多注释,英文好的话阅读效率杠杠的。
至于用机器翻译,个人认为还是没那么靠谱,很多专业性词汇使用机器翻译,大概率是离谱到没边儿的,终究没有自己能看得懂来的方便。
还有一点就是,大家经常会戏称,工作中大部分时间都用来取类、变量、函数名了(当然是玩笑啦),英文不太好的人需要每次都去翻译软件儿上查询,还是挺耽误时间的。更有甚者,有人变量名直接用拼音,别人看到了真的很降低专业性,就显得有点儿 low。
所以,建议大家务必学好英文!!!后续我会发表的从底层官方规范讲起的系列文章也都尽量多带上一些英文,让大家多熟悉熟悉英文。
这里也推荐一些高质量的英文网站,链接放在文末。
再提一嘴,尽量用 Google,用过 Google 再去用百度,你会发现很多搜索结果都质量挺差的,只不过得科学上网啦。

- 前端到底有没有前途?
这个问题得分两种情况:
- 满足现状、对未来发展没有方向、觉得前端工作画画页面就可以了的工程师:很难在技术上有所为,这个世界太卷了,遍地都是培训机构,大家都是卷王,当你安心于当前的工作、不去思考未来的发展方向和学习路线,残酷的现实就是没有什么前途,分分钟就被淘汰了。所以我也建议对这一行不热爱,也不能做到坚持学习的人,还是尽量别选择做程序员了,会非常痛苦的。
- 有职业发展方向和学习路线的工程师:稳步发展,目标清晰,辛苦学习得来的技术积累,未来终究有一天会得到回报。这条路会很累,需要学很多东西,并且要一直保持学习,但也是最有前途、有竞争力的方向。我们可以做 Web、移动端、桌面应用、小程序、数据可视化、架构基建、全栈工程师等等,可选择的方向非常多。
所以看到这篇文章,还在犹豫要不要入行的小伙伴,请先三思而后行~

- AI 时代来临,前端岗位会不会被取代?
不会,目前 AI 最多会取代一些重复的机械性的简单前端工作,例如画一画简单的页面、调一调页面的样式。
对于复杂的有核心竞争力的岗位,目前来说还是不会有什么影响的,最多可以借助一下 AI,帮助提高效率,所以大家也不要太焦虑~
如果还是比较担心的话,可以尝试拓展自己的知识圈,例如去接触后端、运维,技多不压身,轮到淘汰也是淘汰别人。

What to learn?
一定要注意,在我们的职业发展中,一定不能只满足于实现了普通的需求,比如说工作内容是开发某某系统中的一些模块,这些模块虽然复杂,但是工作都是一样的内容:画页面。繁重的工作会让人觉得,我已经做了这么多模块了,我已经学到了很多了,但这样其实相当于是 同一个项目 * n
,而不是 n 个不同的项目
。

在我们早期的职业生涯,很难有机会能遇到特别好的项目(我对于好的项目的定义是:有深度、有广度,能学到很多东西),经常只是写写组件、画画页面,甚至都不用太过考虑性能(我和代码,有一个能跑就行),更别提架构、工程化、前端高级应用(例如前端埋点、前端监控、白屏检测等)这些东西了。
久而久之,很多人就在一家公司里产生了舒适圈,做的都是完全一样的东西,没有任何冲出舒适圈的内容。这是国内环境催生出来的,尤其对于前端工程师,很多中小型公司在前端方面,并不需要做多少有深度的东西,他们要的是螺丝钉(说的残酷点就是纯纯工具人),能快速响应需求、迭代上线,性能方面只要不是很严重的问题,对于他们来说就够了。
我相信作为前端工程师,不管在学校还是在工作中,多多少少有时候会受到一些“歧视”,很多人对于前端的理解,仍然停留在“切图仔“的年代,这跟公司、前端工程师的职级、周边环境有很大关系,毕竟不会有人说尤雨溪、Dan Abramov 这类大佬是切图仔吧~
所以我们需要有危机意识,不能停留在画页面的舒适圈里。我见过工作两三年就对前后端各种原理、计算机相关知识、算法等等都非常熟练的人;也见过工作了十余年的人,还在跟刚毕业的同事一起做无聊重复的画页面的工作。在越来越严峻的大环境下,谁会更先被淘汰,无需多言。
所以在最初的前两三年的工作里,可以只是画画页面,学习基础和框架,顺带着了解一些底层原理。再往后,已经不是初中级工程师了,要往高级应用的方向走,比如去做前端架构基建、前端埋点监控、前端截图、探索前端发展趋势、低代码无代码平台、大型组件库、游戏、3D 等等,这些方向都是需要运用到很多底层的相关知识,并且可能有一些方向没有很多现有的例子可以参考,需要一定的创新和突破。

还可以让自己不仅仅局限在前端,可以和别的方向结合起来,跨出前端的圈子,但是服务于前端。举个例子,淘宝的代理服务器 Tengine
开发的 concat
模块,可以做到文件资源合并,比如页面有两个 css 文件,就需要发送两个 HTTP 请求,减少 HTTP 请求是很常见的性能优化的一种方式,通过 concat
模块就可以实现只发送一个 HTTP 请求,就可以把两个 css 文件都加载过来。该模块也开源给了原版 Nginx
,以下是官方介绍:
This is a module that is distributed with tengine which is a distribution of Nginx that is used by the e-commerce/auction site Taobao.com. This distribution contains some modules that are new on the Nginx scene. The
ngx_http_concat
module is one of them.
The module is inspired by Apache's modconcat. It follows the same pattern for enabling the concatenation. It uses two
?
, like this:
http://example.com/??style1.css,style2.css,foo/style3.css
举个代码例子:
在 Nginx
上下载完单独的 concat
模块后,在 location
中配置:
location /static/css {
concat on;
concat_max_files 20; # 一次最多请求多少文件数
}
并且在 HTML 文件里,原本是:
<link href="a.css" rel="stylesheet" />
<link href="b.css" rel="stylesheet" />
只需要改成:
<link href="??a.css,b.css" rel="stylesheet" />
再举个例子,大家都知道 Webpack
,它使用起来很方便,在以前是前端工程化必备的基建。但是随着时代的发展,前端代码量越来越庞大,Webpack
这种使用 JS 单线程的语言,并且在构建时需要从入口文件打包整个文件依赖树,性能上其实挺差的。所以出现了 ESBuild
、SWC
等工具来解决性能问题,ESBuild
采用 go
语言编写,SWC
采用 Rust
语言编写,通过别的语言的优势来弥补 JS 的劣势。
这就是典型的跨出前端,但服务于前端。越是到了职业生涯后期,越是可以考虑要不要通过突破前端来服务前端。
How to learn?
最好的方式,就是能在日常工作中接触到上文所说的方向,你每天 8 个小时(可能更多😭)都在接触这些东西,对自己的提升是相当大的,也可能会成为这些领域的专家。

当然肯定有人是这样的:“我工作就是这些内容,根本接触不到那些有竞争力的项目,导致换工作的时候,有广度有深度的项目,还是轮不到我”。那么该如何破局呢?
确实想把这些感兴趣的方向塞到公司的项目里,基本上是不太可能的,所以就只能靠自己业余时间自己去动手了,多查阅些资料,多看看前人已经做好的相关的项目源码,最好是能结合起来自己动手写一个项目。亲自动手写项目所带来的收获是相当丰富的。
但是我们每个人的学习时间都很有限,工作繁忙的时候在公司根本没有时间去学别的东西。所以我们要充分利用好碎片化时间,可以先把想看的网站、文档、公众号文章等先收藏起来,在工作中有碎片化时间了的时候可以迅速找到想看的东西,然后品鉴起来,就一个字儿:宣~
平时在地铁上、公交上、代码编译的时候、等同事回复消息的时候,这些碎片化的时间都可以用来看一看感兴趣的文章,工作了以后一定要尽量学会这个技能。
Where to learn?
在现在这么一个信息化爆炸的时代,不是很推荐有自制力的人去报班,学会运用 Google、AI、常用的一些网站,基本上大部分想要了解的东西都能找到相应的资源。
推荐一些学习网站:
- W3C:Web 标准全都在这里,HTML、CSS 标准和草稿都能在这里找到。
- ECMAScript Language Specification:最新最准确的 ECMAScript 规范,含有一切标准和处于 Stage 4 的提案。
- MDN:前端开发者终极利器,基本上前端的基础技术、教程全都有,可以查看浏览器兼容情况。只不过有少数文章介绍的比较简单,有时还得再结合一下别的网站。
- CodeSandbox:云编程网站,非常适合那些懒得本地启项目去学习的人,大多前端 UI、图表等框架都会有 CodeSandbox 的在线运行链接。缺点是代码调试起来稍稍有些麻烦。
- 谷歌 Web 开发指南:里面含有很多基础教程,写的非常用心,配合了很多代码。
- Chrome Devtools:谷歌官网介绍 Chrome Devtools 的使用。
- 谷歌爬虫官方文档:详细介绍了什么是
SEO
,如何优化谷歌SEO
等,写的非常详细。 - ECMAScript 6 入门:阮一峰大佬的 ECMAScript 6 教程,适合爱看中文教程的小伙伴。
- Build your own React:交互很友好的 React 教程网站,带你写一个自己的 React。
- Regulex:可视化 JS 的正则表达式执行过程,帮助理解正则表达式。
- Discover three.js:学习 Three.js 的国外网站。
- Linux Command:学习 Linux 命令的网站,就是导航做的不是很好,如果你只是想快速找一下 Linux 对应的命令,链接在这里。这里还有个中文的 Linux 命令网站。
- VisuAlgo:可视化数据结构算法过程,帮助理解算法。
- GeeksforGeeks:数据结构、算法讲解网站,写的还是很不错的。
- Medium:国外一个有很多高质量文章的网站,活跃人数还是很多的,只不过一些文章需要开通订阅计划才能看。
- Baeldung:学习 Java 和 Spring 框架及其相关内容的国外网站。
- SQLZoo:学习 SQL 的国外网站,可以直接运行 SQL 代码,省去自己动手建库建表的时间。
- ChatGPT:有条件科学上网的同学,一定要学会利用 AI 模型,大大提升学习和工作效率。
- Gemini:谷歌的 AI 模型。
...
目前想到的是这些,还有新的话以后再补充~
Conclusion
前端程序员的必备技能就是保持学习,以上这些是我的建议和心得,不一定适合所有人,如果你有更好的想法,欢迎交流~
未来我会陆续发很多系列文章,从底层的官方英文标准规范来讲解 HTML、CSS、JS 等,会附上规范里的定义,以及前端高级应用、框架、跨端、工程化、计算机网络、操作系统、算法、设计模式等,打造全能超级前端。
为啥要从底层官方英文标准规范来讲解呢,因为网上很多文章说的知识点都不够准确,让人不知道是否可信。JavaScript 不像 Java 这样的语言,很多类、方法等都可以直接跳转到源码进行分析,JavaScript 就像是个黑盒,保持着神秘。HTML、CSS 也同样如此,所以我会带着大家从标准规范讲起,有理有据,绝不再受到模糊不清的文章和知识点的影响。

来源:juejin.cn/post/7407999205493719090
大文件分片上传
前言
大文件上传是项目中的一个难点和亮点,在面试中也经常会被面试官问到,所以今天蘑菇头来聊聊这个大文件上传。
什么样的文件算的上是大文件?
对于Web前端来说,当涉及到上传或下载操作时,通常认为任何超过10MB的文件都属于较大文件,尤其是对于HTTP POST上传操作。如果文件大小达到几十MB甚至更大,那么通常就需要考虑使用分块上传、断点续传等技术来优化传输过程,减少因网络不稳定导致的失败率,并提高用户体验。
当然了,这和你的网络带宽也有关系,当你的网络带宽很小时,即时在小的文件传输速率也很慢,也可以被称之为大文件了。
接下里我们来模拟一下如何使用分块上传技术来优化传输过程。
分片上传文件
分片上传技术是解决大文件上传问题的一种有效方法。它通过将大文件分割成多个较小的部分(称为“分片”或“片段”),然后分别上传这些部分,最后再由服务器端合并这些部分来重构原始文件。这种方法的优点包括能够更好地利用网络资源、支持断点续传以及提高上传效率。
主要思想
首先,前端获取到input框里输入的文件对象,通过slice方法将大文件对象进行切割得到小的Blob对象,由于后端无法识别Blob对象,所以需要转为前后端都能识别的对象FormData,然后将这个对象通过post请求发送给后端,将切片一个一个发送给后端。
后端接收到一个一个切割好的对象进行解析,将这些切片保存到一个文件夹下。当所有的切片都发送完毕之后,后端接收到合并这个信号,将文件夹下的切片排好顺序进行合并,创建可写流,将所有的切片读成流类型并汇入到可写流中得到完整的文件资源。
详细过程
有几个点需要我们注意
文件如何切割?用什么方法?
后端什么时候知道前端已经将所有的分片都发送过来了,然后才开始合并?
合并的过程中如何保证分片的顺序?
后端怎么将前端发送过来的分片文件进行合并?
前端
监听input框的change事件,获取文件对象。
使用slice将文件对象进行切片,返回一个数组。
使用FormData构造函数,将Bolb对象包装成formdata对象,以便后端能够识别,并且给这个对象添加文件名,分片名属性,以便后来分片进行排序。
使用Promise.all方法,当所有的分片请求都成功后,在all的then方法里面发送一个分片请求已完成的信号给后端,告诉后端可以开始合并分片了。
<input type="file" name="" id="input">
<button id="btn">上传</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
let fileObj = null
input.addEventListener('change', handleFileChange);
btn.addEventListener('click', handleUpload)
function handleFileChange(e) {//监听change事件,获取文件对象
// console.log(event.target.files);
const [file] = event.target.files;
fileObj = file;
}
function handleUpload() {//点击按钮上传文件到服务器
if (!fileObj) return;
const chunkList = createChunk(fileObj);
// console.log(chunkList);
const chunks = chunkList.map(({ file }, index) => {//创建切片对象
return {
file,
size: file.size,
percent: 0,
index,
chunkName: `${fileObj.name}-${index}`,
fileName: fileObj.name,
}
});
// 发请求
uploadChunks(chunks);
}
//切片
function createChunk(file, size = 5 * 1024 * 1024) {
const chunkList = [];
let cur = 0;
while (cur < file.size) {
chunkList.push({
file: file.slice(cur, cur + size),
})
cur += size;
}
return chunkList;
}
// 发请求到后端
function uploadChunks(chunks) {
console.log(chunks); //这个数组中的元素是对象,对象中有blob类型的文件对象,后端无法识别,所以需要转换成formData对象
const formChunks = chunks.map(({ file, fileName, index, chunkName }) => {
const formData = new FormData();
formData.append('file', file);
formData.append('fileName', fileName);
formData.append('chunkName', chunkName);
return { formData, index }
})
console.log(formChunks); // 后端能识别的类型
//发请求
const requestList = formChunks.map(({ formData, index }) => {//一个一个片段发
return axios.post('http://localhost:3000/upload', formData,()=>{
console.log(index + ' 上传成功');
})
.then(res => {
})
})
Promise.all(requestList).then(() => {
console.log('全部上传成功');
mergeChunks();
})
}
// 合并请求的信号
function mergeChunks(size=5*1024*1024){
axios.post('http://localhost:3000/merge',{
fileName:fileObj.name,
size
})
.then(res=>{
console.log(fileObj.name + '合并成功');
})
}
</script>
后端
使用第三方库multiparty对传输过来的formdata进行解析。
使用fse模块对解析完成的数据进行保存。
当所有的切片都完成时,后端接收到合并切片的请求信号时,使用fse模块读取所有的切片并进行排序。
排序完成之后使用fse模块进行合并。
const http = require('http');
const path = require('path');
const multiparty = require('multiparty');
const fse = require('fs-extra');
const server = http.createServer(async (req, res) => {
res.writeHead(200, {
'access-control-allow-origin': '*',
'access-control-allow-headers': '*',
'access-control-allow-methods': '*'
})
if (req.method === 'OPTIONS') { // 请求预检
res.status = 200
res.end()
return
}
if (req.url === '/upload') {
// 接收前端传过来的 formData
const form = new multiparty.Form();
form.parse(req, (err, fields, files) => {
// console.log(fields); // 切片的描述
// console.log(files); // 切片的二进制资源被处理成对象
const [file] = files.file
const [fileName] = fields.fileName
const [chunkName] = fields.chunkName
// 保存切片
const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
if (!fse.existsSync(chunkDir)) { // 该路径是否有效
fse.mkdirSync(chunkDir)
}
// 存入
fse.moveSync(file.path, `${chunkDir}/${chunkName}`)
res.end(JSON.stringify({
code: 0,
message: '切片上传成功'
}))
})
}
if (req.url === '/merge') {
const { fileName, size } = await resolvePost(req) // 解析post参数
const filePath = path.resolve(UPLOAD_DIR, fileName) // 完整文件的路径
// 合并切片
const result = await mergeFileChunk(filePath, fileName, size)
if (result) { // 切片合并完成
res.end(JSON.stringify({
code: 0,
message: '文件合并完成'
}))
}
}
})
// 存放切片的地方
const UPLOAD_DIR = path.resolve(__dirname, '.', 'qiepian')
// 解析post参数
function resolvePost(req) {
return new Promise((resolve, reject) => {
req.on('data', (data) => {
resolve(JSON.parse(data.toString()))
})
})
}
// 合并
function pipeStream(path, writeStream) {
return new Promise((resolve, reject) => {
const readStream = fse.createReadStream(path)
readStream.on('end', () => {
fse.removeSync(path) // 被读取完的切片移除掉
resolve()
})
readStream.pipe(writeStream)
})
}
// 合并切片
async function mergeFileChunk(filePath, fileName, size) {
// 拿到所有切片所在文件夹的路径
const chunkDir = path.resolve(UPLOAD_DIR, `${fileName}-chunks`)
// 拿到所有切片
let chunksList = fse.readdirSync(chunkDir)
// console.log(chunksList);
// 万一切片是乱序的
chunksList.sort((a, b) => a.split('-')[1] - b.split('-')[1])
const result = chunksList.map((chunkFileName, index) => {
const chunkPath = path.resolve(chunkDir, chunkFileName)
// !!!!!合并
return pipeStream(chunkPath, fse.createWriteStream(filePath, {
start: index * size,
end: (index + 1) * size
}))
})
// console.log(result);
await Promise.all(result)
fse.rmdirSync(chunkDir) // 删除切片目录
return true
}
server.listen(3000, () => {
console.log('listening on port 3000');
})
来源:juejin.cn/post/7407262746700365876
uniapp开发微信小程序,我踩了大家都会踩的坑
最近使用uniapp开发了一个微信小程序(本项目技术栈是uniapp + vue3 + ts,用了最近比较火的模板unibest。),踩了一些大家普遍都会踩的坑,下面做一些总结。文章多处引用到权威官方内容和一些比较可靠的文章。如有错误,欢迎指正。
1. 使用微信昵称填写能力遇到的问题
自 2022 年 10 月 25 日 24 时后,wx.getUserProfile
和 wx.getUserInfo
的接口被收回,要想获取微信的昵称头像需要使用微信的头像昵称填写能力。
我们的设计稿中没有编辑确认按钮,所以应该失焦后就调用后端的变更昵称接口:


但是失焦之后,微信会对昵称内容做合规性校验,导致失焦后不能立马获取到输入的内容:
<uv-input v-model="form.name" type="nickname" placeholder="请输入内容" @blur="handleSubmit"></uv-input>
async function handleSubmit() {
console.log('form.value.name', form.value.name) // 测试用户001
console.log('rawName', rawName) // 测试用户001
if (form.value.name === rawName)
return
// ...
}
因此最开始的想法是等待校验结束:
async function handleSubmit() {
// 微信会对type="nickname"的输入框失焦时进行昵称违规校验,这个校验是异步的,所以需要等待一下
await new Promise((resolve) => setTimeout(resolve, 0))
console.log('form.value.name', form.value.name) // Jude
console.log('rawName', rawName) // 测试用户001
if (form.value.name === rawName) {
return
}
// ...
}
但如果真的输入了违规昵称,微信将自动清空输入框内容,而在此之前我的提交请求已经发送:
因此需要用到官方新加的一个回调事件bindnicknamereview
(文档):
<uv-input v-model="form.name" type="nickname" placeholder="请输入内容" @nicknamereview="handleSubmit"></uv-input>
function onNickNameReview(e) {
console.log('onNickNameReview', e)
if (e.detail.pass) {
// 校验通过
handleSubmit()
} else {
form.value.name = rawName
}
}
但发现 uv-ui 并没有提供这个事件,还是没有生效,只能改node_modules
的uv-input
源码,并给uv-ui
提个pr
:
2. 自定义导航栏
原生导航栏配置方面有很多限制,比如不允许修改字体大小等。所以有的时候需要自定义导航栏。
首先注意,webview的页面无法自定义导航栏!
所以:
导航栏高度 = 状态栏到胶囊的间距(胶囊上坐标位置-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
第一步:配置当前页面的json
文件
// pages.json
{ navigationStyle: "custom" }
第二步:获取状态栏和导航栏高度,只需要获取一次即可,获取到可以放到pinia
里
// 自定义导航栏
const statusBarHeight = ref(0)
const navBarHeight = ref(0)
statusBarHeight.value = uni.getSystemInfoSync().statusBarHeight
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuButtonInfo.height + (menuButtonInfo.top - statusBarHeight.value) * 2
第三步:自定义导航栏
<view class="nav-bar">
<!-- 状态栏占位 -->
<view :style="{ height: statusBarHeight + 'px' }"></view>
<!-- 真正的导航栏内容 ,请按照自己的需求自行定义-->
<view class="nav-bar-content" style="font-size: 34rpx;" :style="{ height: navBarHeight + 'px' }">导航栏标题</view>
</view>
问题:微信小程序原生导航栏会根据微信设置(字体大小,是否开启深色模式)等变化,深色模式是页面是可以获取到的,但字体大小等目前没有开放接口,所以无法根据微信设置动态变化。
3. 自定义tabbar
由于原生底部tabbar的局限性,未能满足产品需求,所以需要自定义tabbar。
首先,自定义tabbar的第一步配置pages.json
:
// pages.json
tabBar: {
custom: true,
// ...
},
然后,我们只需要在项目根目录(src)创建custom-tab-bar目录,uniapp编译器会直接它拷贝到小程序中:
<!-- src/custom-tab-bar/index.wxml -->
<view class="tab-bar">
<view class="tab-bar-border"></view>
<view wx:for="{{list}}" wx:key="index" class="tab-bar-item" data-path="{{item.pagePath}}" data-index="{{index}}" bindtap="switchTab">
<image class="tab-bar-item-img" src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></image>
<view class="tab-bar-item-text" style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</view>
</view>
</view>
// src/custom-tab-bar/index.js
Component({
data: {
selected: 0,
color: "#8d939f",
selectedColor: "#e3eaf9",
list: [{
pagePath: "/pages/index/index",
iconPath: "../static/tabbar/home01.png",
selectedIconPath: "../static/tabbar/home02.png",
text: "首页"
}, {
pagePath: "/pages/my/my",
iconPath: "../static/tabbar/user01.png",
selectedIconPath: "../static/tabbar/user02.png",
text: "我的"
}]
},
attached() {
},
methods: {
switchTab(e) {
const data = e.currentTarget.dataset
const url = data.path
wx.switchTab({url})
this.setData({
selected: data.index
})
}
}
})
// src/custom-tab-bar/index.json
{
"component": true
}
// src/custom-tab-bar/index.wxss
.tab-bar {
position: fixed;
bottom: calc(16rpx + env(safe-area-inset-bottom));
left: 0;
right: 0;
height: 100rpx;
background: linear-gradient(180deg, rgba(13, 15, 26, 0.95) 0%, rgba(42, 50, 76, 0.95) 100%);
box-shadow: 0rpx 4rpx 16rpx 0px rgba(0, 0, 0, 0.12);
display: flex;
width: calc(100% - 2 * 36rpx);
border-radius: 36rpx;
margin: 0 auto;
}
.tab-bar-item {
flex: 1;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.tab-bar-item .tab-bar-item-img {
width: 32rpx;
height: 32rpx;
}
.tab-bar-item .tab-bar-item-text {
margin-top: 10rpx;
font-size: 20rpx;
}
最后,关键坑注意:每个tab页都有自己的tabbar实例:

因此需要每个tab页渲染时设置一下自定义tabbar组件的 activeIndex
(我这里变量名是selected
):
如果是原生小程序开发像官网那样写就好,如果是uniapp
开发,需要:
onShow(() => {
const currentPage = getCurrentPages()[0]; // 获取当前页面实例
const currentTabBar = currentPage?.getTabBar?.();
// 设置当前tab页的下标index
currentTabBar?.setData({ selected: 0 });
})
效果:
4. IOS适配安全距离
当用户使用圆形设备访问页面时,就存在“安全区域”和“安全距离”的概念。安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners
)、齐刘海(sensor housing
)、小黑条(Home Indicator
)的影响。

上图来自designing-websites-for-iphone-x
uniapp适配:
uniapp适配安全距离有三个方法:
a. manifest.json配置安全距离
// manifest.json
{
"app-plus": {
"safearea": { //可选,JSON对象,安全区域配置
"background": "#RRGGBB", //可选,字符串类型,#RRGGBB格式,安全区域背景颜色
"backgroundDark": "#RRGGBB", //可选,字符串类型,#RRGGBB格式,暗黑模式安全区域背景颜色
"bottom": { //可选,JSON对象,底部安全区域配置
"offset": "auto" //可选,字符串类型,安全区域偏移值,可取值auto、none
},
"left": { //可选,JSON对象,左侧安全区域配置
"offset": "none" //可选,字符串类型,安全区域偏移值,可取值auto、none
},
"right": { //可选,JSON对象,左侧安全区域配置
"offset": "none" //可选,字符串类型,安全区域偏移值,可取值auto、none
}
},
}
}
问题: 这种方式显然不够灵活,它设置的是单独的背景色,如果需要下方一个区域是背景图,延伸到底部安全区就满足不了了。
所以,我是将以上的配置设置成none
,然后手动适配页面的安全距离:
b. js获取安全距离
let app = uni.getSystemInfoSync()
app.statusBarHeight // 手机状态栏的高度
app.bottom // 底部安全距离
c. 使用苹果官方推出的css函数env()、constant()适配
padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
注意: constant
和env
不能调换位置
可以配合calc
使用:
padding-bottom: calc(constant(safe-area-inset-bottom) + 20rpx); /*兼容 IOS<11.2*/
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); /*兼容 IOS>11.2*/
h5适配
网页适配安全距离的前提是需要将<meta name="viewport">
标签设置viewport-fit:cover;
:
<meta name='viewport' content='initial-scale=1, viewport-fit=cover'>
直观一点就是:


上图来自移动端安全区域适配方案
然后再使用env
和constant
padding-bottom: constant(safe-area-inset-bottom); /*兼容 IOS<11.2*/
padding-bottom: env(safe-area-inset-bottom); /*兼容 IOS>11.2*/
5. 列表滚动相关问题
列表滚动如果使用overflow: auto;
在首次下拉时(即使触控点在列表内)也会使整个页面下拉:
解决这个问题只需要将内容使用 scroll-view 包裹即可:
<scroll-view scroll-y class="max-h-[800rpx] overflow-auto"></scroll-view>
下拉刷新将列表滚动到顶部:
小程序默认使用webview
渲染,如果需要Skyline
渲染引擎需要配置,而srcoll-view
标签在webview
中有个独有的属性enhanced
,启用后可通过 ScrollViewContext 操作 scroll-view
:
<scroll-view id="scrollview" :enhanced="true" scroll-y class="max-h-[800rpx] overflow-auto"></scroll-view>
/** 将scrollview滚动到顶部 */
function scrollToTop(id: string) {
wx.createSelectorQuery()
.select(id)
.node()
.exec((res) => {
const scrollView = res[0].node;
scrollView.scrollTo({
top: 0,
animated: true
});
})
}
onPullDownRefresh(async () => {
console.log('下拉刷新')
try {
await fetchList()
} catch (error) {
console.log(error)
} finally {
uni.stopPullDownRefresh()
scrollToTop('#scrollview')
}
})
6. 配置小程序用户隐私保护指引
文档:小程序隐私协议开发指南
什么时候要配置:
但凡你的小程序用到上图中任何一种用户信息就得配置,否则使用wx.authorize
来获取相应授权时直接会走到fail
回调,报 { "errMsg": "authorize:fail api scope is not declared in the privacy agreement", "errno": 112 }
配置的是什么:
配置的是将来你的程序打开让用户确认授权的隐私协议内容。
如何配置:
登录微信公众平台 -> 设置 -> 服务内容声明 -> 用户隐私保护指引 -> 修改
隐私弹框触发的流程是什么:
程序调用隐私相关接口 ——> 微信判断该接口是否需要隐私授权 ——> 如果需要隐私授权且开发者没有对其响应(注册onNeedPrivacyAuthorization的监听事件)则主动弹出官方弹框(此时隐私相关接口调用处于pending状态,如果用户拒绝将会报{ "errMsg":" getLocation:fail privacy permission is not authorized", "errno":104 }
)。
代码逻辑:
配置并等待审核通过后,进行以下步骤:
1. 配置 __usePrivacyCheck__: true
尽管官方文档说明2023年10月17日之后无论是否配置改字段,隐私相关功能都会启用,但是实际尝试后发现还是得配置上才生效。
// manifest.config.ts
'mp-weixin': {
__usePrivacyCheck__: true
},
2. 自定义隐私弹框组件
尽管官方提供了官方隐私弹框组件,但是真机上没有生效,于是还是使用了自定义隐私弹框。
我是直接在插件市场找了一个下载量最多的插件,兼容vue2和vue3。
在小程序对应的页面:
<WsWxPrivacy id="privacy-popup" @agree="onAgree" @disagree="onDisAgree"></WsWxPrivacy>
function onAgree() {}
function onDisAgree() {}
tip: 这部分逻辑相对于业务是几乎没有耦合的,甚至如果没有特殊需求agree
和disagree
事件都不用写。如果将来官方主动弹框没问题了,那这个逻辑可以直接删掉。
3. 业务代码
举个例子,我这里隐私相关接口是uni.getLocation
获取用户地理位置。
function handleCheckLocation() {
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
success: async (res) => {
console.log('当前位置:', res)
try {
let r = await checkLocation({
lon: res.longitude.toString(),
lat: res.latitude.toString(),
})
// ...
resolve('success')
} catch (error) {
reject(error)
}
},
fail: (error) => {
console.log('获取位置失败:', error)
reject(error)
}
})
})
}
以上代码,在调用uni.getLocation
时,微信自动发起位置授权,发起位置授权之前又会自动发起隐私授权。到此,这一流程是ok的。但是,如果用户拒绝了隐私授权,或者拒绝了位置授权,该怎么办?
如果拒绝了隐私授权,下次调用隐私相关接口时还会再次弹出隐私授权弹框。
如果拒绝了位置授权,下次调用就不会弹出位置授权弹框,但可以通过uni.getSetting
来判断用户是否拒绝过,再通过wx.openSetting
让用户打开设置界面手动开启授权。代码如下:
function getLocationSetting() {
uni.getSetting({
success: (res) => {
console.log('获取设置:', res)
if (res.authSetting['scope.userLocation']) {
// 已经授权,可以直接调用 getLocation 获取位置
handleCheckLocation()
} else if (res.authSetting['scope.userLocation'] === false) {
// 用户已拒绝授权,引导用户到设置页面开启
wx.showModal({
title: '您未开启地理位置授权',
content: '请在设置中开启授权',
success: res => {
if (res.confirm) {
wx.openSetting({
success(settingRes) {
if (settingRes.authSetting['scope.userLocation']) {
// 用户打开了授权,再次获取地理位置
handleCheckLocation()
}
}
})
}
}
})
} else {
// 首次使用功能,请求授权
uni.authorize({
scope: 'scope.userLocation',
success() {
handleCheckLocation()
}
})
}
}
})
}
当然你也可以封装一下:
function getSetting(scopeName: string, cb: () => any) {
uni.getSetting({
success: (res) => {
console.log('获取设置:', res)
if (res.authSetting[scopeName]) {
// 已经授权,可以直接调用
cb()
} else if (res.authSetting[scopeName] === false) {
// 用户已拒绝授权,引导用户到设置页面开启
wx.showModal({
title: '您未开启相关授权',
content: '请在设置中开启授权',
success: res => {
if (res.confirm) {
wx.openSetting({
success(settingRes) {
if (settingRes.authSetting[scopeName]) {
// 用户打开了授权,再次获取地理位置
cb()
}
}
})
}
}
})
} else {
// 首次使用功能,请求授权
uni.authorize({
scope: scopeName,
success() {
cb()
}
})
}
}
})
}
这样,整个隐私协议指引流程就完整了。
来源:juejin.cn/post/7361688292351967259
Web3:未来互联网的颠覆与机遇
一、Web3 的定义与核心概念
Web3 是区块链等技术的总称,这些技术可以分散互联网上的数据所有权和控制权。大多数互联网应用程序都由集中式实体控制,这些实体决定如何保存和使用最终用户数据。Web3(也称为 Web 3.0、去中心化 Web 或语义 Web)技术允许社区驱动的项目,而不是集中式管理结构。在这些项目中,最终用户控制数据、确定定价、直接为技术开发做出贡献,并在项目的方向上拥有更重要的发言权。这些技术具有自动调节用户相互交互方式的机制。因此,不需要集中式实体来管理这些交互。
- Web3是互联网的第三代,旨在通过区块链技术实现去中心化、更加安全和隐私的数据交互。
- Web3的主要特点包括语义Web、人工智能、3D图形、无处不在的网络、开放性和互操作性。
- Web3通过去中心化的区块链技术,将权力和数据集中到用户手中,而不是某个公司。
- Web3应用程序(DApps)在去中心化网络上运行,用户可以在未经中央公司许可的情况下构建和连接不同的DApp。
Web 3.0 具备四项主要功能。
去中心化
去中心化的 Web 应用程序是 Web 3.0 的关键功能。其目的是在去中心化网络中分发和存储数据。在这些网络中,不同的实体拥有底层基础设施,用户直接向存储提供商付费以访问该空间。
去中心化的应用程序还将信息副本存储在多个位置,并确保整个过程中的数据一致性。每位用户可以控制其数据存放的位置,而不必将其移交给集中式基础设施。去中心化的互联网用户可根据需要出售自己的数据。
去信任性
在集中式 Web 应用程序和服务中,用户通常需要信任中央权威机构来管理其数据、交易和交互。这些中央权威机构可以控制用户数据,并且可以操纵系统的规则。数据可能存在安全风险或管理不善,从而导致用户信息丢失或滥用。
相比之下,Web3 引入去信任性,因此用户可以在无需信任任何特定方的情况下进行交易和交互。
语义网
借助语义网,应用程序能够通过理解 Web 数据的内容和上下文来执行复杂的任务。语义网使用元数据和人工智能为用户生成的数据提供含义(语义)。
Web 3.0 旨在更全面地转向目前存在于现有 Web 技术某些方面中的语义网技术。例如,搜索引擎可提供更准确且与上下文相关的搜索结果,而智能代理则可帮助用户更高效地执行任务。
互操作性
Web 3.0 的目标是在不同技术之间建立更多的互连,从而数据无需中介即可在不同平台之间流动。互操作性使数据具有可移植性,因此用户可以在服务之间无缝切换,同时保持自己的首选项、配置文件和设置。
与此同时,集成各种物联网(IoT)设备的协议将 Web 的覆盖范围扩展到传统边界之外。例如,支持无边界交易的加密货币技术允许跨地域和政治边界进行价值交换。
二、互联网的发展史:从Web 1.0到Web 2.0再到Web3
要充分理解Web3的含义,就必须先看互联网的发展史,以及Web3与前两个发展阶段的不同之处。
Web 1.0(1994-2004)
Web 1.0是互联网的第一个发展阶段,这个阶段从1994年一直延续到2004年,期间出现了Twitter和Facebook等社交媒体巨头。虽然大众在1994年左右才接触到Web 1.0,但实际上早在1968年,一个名为“ARPANET”(全称是Advanced Research Projects Agency Network)的美国政府项目就启动了Web 1.0。ARPANET最初是由军方承包商和大学教授组成的一个小型网络,他们在其中互相交换数据。
Web 1.0主要是静态的HTML网页,用户之间很少交互。虽然有门户网站以及私人聊天室和BBS等论坛,但总的来说当时的互联网仍没有什么交互或支付交易功能。
Web 2.0(2004年至今)
互联网在2004年左右经历了蜕变,由于当时互联网在网速、光纤基础设施和搜索引擎等方面都取得了发展,因此用户对社交、音乐、视频分享和支付交易的需求大幅上升。
这种更具互动性的全新互联网体验为用户带来了许多新的功能,并提升了用户体验。但问题也随之而来,并且直到今天也一直无法彻底解决,那就是:用户如果要使用这些新功能,就必须授权中心化的第三方平台管理大量数据。因此这些中心化的实体在数据和内容权限方面被赋予了巨大的权力和影响力。
Web3(2008年之后)
在2008年,中本聪发布了比特币白皮书,在其中指出了区块链技术的核心基础并发明了点对点的数字货币,由此掀起了Web 2.0的改革浪潮。比特币彻底颠覆了我们对数字化交易的概念,并首次提出了一种无需可信中间方的安全在线交易模式。中本聪写道:“需要基于加密证明,而非信任,来建立电子支付系统。”
直到智能合约被发明后,去中心化的互联网模式才真正进入公众视野。如果说比特币实现了点对点支付,智能合约扩展了可编程协议的概念,实现了保险、游戏、身份管理和供应链等更高级的用例,那么这一切会如何影响互联网用户体验和数字化交互呢?智能合约用户可以直接、安全地交互,因此打造了一个更加公平、透明且基于加密事实的新型互联网。
Gavin Wood将这个升级版的互联网称作“Web3”,即“一个安全的、由社会运行的系统”。
简而言之,Web3就是一个去中心化的互联网,旨在打造出一个全新的合约系统,并颠覆个人和机构达成协议的方式。Web3复刻了第一版互联网(即Web 1.0)的去中心化基础架构,Web 1.0的特色是用户自己架设博客网站以及RSS feed。在此基础上,Web3还结合了Web 2.0丰富的交互体验,比如社交媒体平台。Web 1.0和Web 2.0相结合,就形成了Web3的数字化生态,在其中用户可以真正拥有自己的数据,并且交易受到了加密技术保障。用户无需再信任品牌背书,而是可以依赖确定的软件代码逻辑来严格执行协议。
三、Web3对前端开发的影响
- 在Web3.0中,前端不再仅仅是展示层,而是成为了与智能合约、区块链网络直接交互的重要桥梁。
- 前端开发者需要掌握如何通过Web3.0技术栈,如以太坊智能合约、IPFS等,实现去中心化应用(DApp)的开发。
- Web3.0强调去中心化和用户数据主权,这要求前端开发在设计和实现应用时,更加注重用户隐私保护和数据安全。
- Web3.0时代的前端开发还面临着性能优化的挑战,由于区块链操作通常较慢,前端需要进行相应的优化。
四、Web3前端开发需要掌握的技术
- 区块链技术:了解区块链的基本原理、共识机制、加密算法等基础知识。
- 智能合约开发:掌握智能合约的编写语言(如Solidity)和开发工具,以及合约的部署和调用方法。
- 去中心化应用设计:了解去中心化应用的设计原则、用户体验和开发流程。
- 分布式存储技术:熟悉常见的分布式存储方案,如IPFS。
- 前端开发技术:具备HTML、CSS和JavaScript等基础技能,同时了解React、Vue.js、Web3.js等前端框架和库。
- 安全性和隐私保护:了解如何进行合约审计、安全防范和数据加密等方面的知识。
五、Web3前端开发的工具和库
- Web3.js:一个JavaScript API库,用于与以太坊区块链进行交互。
- Ethers.js:一个小而完整的JavaScript API库,为以太坊区块链及其生态系统提供支持。
- Truffle:一个以太坊智能合约开发框架,提供编译和测试智能合约的开发环境。
- Remix IDE:一个用于编写和使用智能合约的在线编辑器。
- MetaMask:一个Chrome扩展程序,可让用户从浏览器连接到以太坊区块链网络。
- Ganache:提供本地区块链环境,用于测试智能合约。
六、Web3前端开发挑战
Web3的去中心化特性和用户数据主权要求前端开发者在编码时采用更高的安全标准。前端开发者需要掌握如何使用端到端加密技术来保护用户数据,确保数据在传输和存储过程中的安全性。
由于区块链操作通常较慢,前端开发者需要优化性能以确保用户体验。通过使用高效的缓存策略和异步处理技术,开发者可以减少用户等待时间,提升应用的响应速度。
用户体验在Web3应用中至关重要。前端开发者需要设计直观且友好的用户界面,确保用户能够轻松地与区块链进行交互。这包括使用现代前端框架如React和Vue.js来构建动态和响应式的界面。
Web3技术栈的复杂性要求前端开发者掌握多种新技术和工具,如Web3.js、ethers.js等库。这些工具帮助开发者与区块链进行交互,实现智能合约调用和数据读取。
隐私保护是Web3前端开发的核心要求之一。开发者需要采用端到端加密技术,确保用户数据在传输和存储过程中的安全性,防止数据泄露和篡改。
七、Web3前端开发未来趋势
虚拟现实(VR)技术与Web3的融合,正在为用户提供前所未有的沉浸式交互体验。通过Web3的去中心化特性,用户可以在虚拟现实中拥有更高的自主权和数据安全性。这种技术融合不仅改变了传统互联网的边界,还为开发者提供了创造丰富多样用户体验的机会。
元宇宙是Web3技术的一个重要应用领域。通过去中心化的区块链技术,元宇宙中的虚拟世界可以实现更高的透明度和安全性。用户在元宇宙中不仅可以进行虚拟资产交易,还可以参与到去中心化的社区治理中,真正实现虚拟世界的自治。
智能合约作为Web3的重要组成部分,将继续在前端开发中发挥关键作用。前端开发者需要掌握智能合约的编写和部署,以便在去中心化应用中实现自动化和安全的交易。智能合约的应用不仅限于金融领域,还可以扩展到供应链管理、数字身份验证等多个方面。
去中心化存储技术,如IPFS,将成为Web3前端开发的重要组成部分。IPFS通过分布式存储网络,提供了更高效和安全的数据存储解决方案。前端开发者可以利用IPFS来存储和检索数据,确保数据的完整性和不可篡改性,从而提升应用的可靠性。
跨链技术的发展将促进不同区块链之间的互操作性。前端开发者需要关注跨链技术的最新进展,以便在开发去中心化应用时实现不同区块链之间的数据和资产互通。跨链技术不仅可以提升区块链网络的整体效率,还可以为用户提供更加便捷的跨链交易体验。
参考:
作者:洞窝-雪花
来源:juejin.cn/post/7407263786132308003
面试官问我为什么 [] == ![] 为 true, 我表面冷静,实则内心慌的一批
前言
面试官问我,[] == ![] 的结果是啥,我:蒙一个true; 面试官:你是对的;我:内心非常高兴;
面试官:解释一下为什么; 我:一定要冷静,要不就说不会吧;这个时候,面试官笑了,同学,感觉你很慌的一批啊!
不必慌张,我们慢慢来!
在当今的编程领域,面试不仅是技术能力的考察,更是思维灵活性与深度理解的试金石。面试中偶遇诸如 [] == ![]
表达式这类题目,虽让人初感意外,实则深藏玄机,考验着我们对于JavaScript这类动态语言特性的透彻理解。这类问题触及了类型转换、逻辑运算以及语言设计的微妙之处,促使我们跳出日常编码的舒适区,深入探索编程语言的底层机制。接下来,我们将一步步揭开这道题目的神秘面纱,不仅为解答此类问题提供思路,更旨在通过这一过程,提升我们对JavaScript核心概念的掌握与应用能力。
首先我们来聊一下基础的东西。
1.原始值转布尔
首先是原始值转布尔
console.log(Boolean(1));//true
console.log(Boolean(0));//false
console.log(Boolean(-1));//true
console.log(Boolean(NaN));//false
console.log(Boolean('abc'));//true
console.log(Boolean(''));//false
console.log(Boolean(false));//flase
console.log(Boolean(undefined));//false
console.log(Boolean(null));//false
2.原始值转数字
console.log(Number('123'));//123
console.log(Number('hello'));//NaN
console.log(Number(true));//1
console.log(Number(false));//0
console.log(Number(''));//0
console.log(Number(' '));//0
console.log(Number(undefined));//NaN
console.log(Number(null));//0
3.原始值转字符串
console.log(String(123));//'123'
console.log(String(true));//'true'
console.log(String(false));//'false'
console.log(String(undefined));//'undefined'
console.log(String(null));//'null'
然后我们来了解一下与对象有关的转换逻辑
4. 原始值转对象
let a = new Number(1)
console.log(a);//[Number: 1]
其实也没有很特殊的,就是利用构造函数去进行显式转换即可。
5.对象转原始值
5.1 对象转布尔
首先我们来到这题,最后结果会被打印,说明对象在转换为布尔值的时候,不管什么对象,都是被转换为true。
5.2 + 一元运算符
我们先来了解一下,一元运算符的作用。查阅js官方文档,我们可以知道就是调用ToNumber()得到结果。而ToNumber()就是调用Number方式所调用的内置函数,因此就是强制转换为数字。我们也可以理解为+和Number()的作用是一样的。
5.3 + 二元运算符
二元运算符调用ToPrimitive()方法(ToNumber中的,转换方式有差异)。
5.4 ToNumber()方法
那么这个方法具体执行过程是什么呢?我们可以看到,如果是基本数据类型转数字,我们之前已经聊到,因此不必多聊,而面对对象转数字的时候,我们会先调用ToPrimitive方法。
5.5 ToPrimitive()方法
关于这个方法,我们要看是被ToNumber还是Totring方法给调用了。二者在返回值的顺序上会有所差异。
我们来聊一聊里面的valueOf()和toString()方法。
5.6 toString()和valueOf()方法
1. {}.toString() 得到由"[object class ] "组成字符串
2. [].toString() 返回由数组内部元素以逗号拼接的字符串
3. xx.toString() 返回字符串字面量
- valueOf 也可以将对象转成原始类型
1. 包装类对象
5.6 == 比较
我们首先引入官方文档
首先我们看二者类型相同时的比较, 里面有一点需要注意,只要有一个NaN就返回false,其他的我们应该都清楚。
二者类型不相等时,我们需要特别注意的是,null和undefined是相等的,字符串和数字则把字符串转数字,布尔和其他把布尔转数字,出现对象先把对象转原始值。
估计上面的大量干货已经把大家快搞懵逼了,此这里我们做个简单小结,这里面的方法前面都有提到哦。
5.7 小结(重点)
对象转数字
Number(obj) => ToNumber(obj) => ToNumber(ToPrimitive(obj,Number))
对象转字符串
String(obj) => ToString(obj) => ToString(ToPrimitive(obj,String))
5.8 大量实战练习
这一题我们知道+的作用和Number的方法是一样的。因此是转换为数字123.
那么这一题,我们考虑到
Number([]) => ToNumber([]) => ToNumber(ToPrimitive([], Number))=> ToNumber('') => 0
这里只要对象转布尔均为true
这里的底层原理(5.3里说了)是,我们首先两边都调用ToPrimitive方法,看看有没有字符串,有的话就把另一方转换为字符串,没有的话就全部调用ToNumber方法相加。
这里也是一样的原理。
我们先把两边转换为原始值,左边为' ',右边为'[object object]',发现存在字符串,因此相加。
这里只要我们看5.6就可以很轻松搞懂。
同上一个。
首先有对象,我们把对象转原始值,然后为NaN,为false.
最后回到我们最开始的题目,首先碰见![],我们先把[]转为布尔,为true,!true为false,然后把左边对象转原始值,为' ' == false,出现布尔和字符串,把布尔转数字,为' ' == 0,出现字符串和数字,把字符串转数字,为0 == 0,因此最后结果为true
来源:juejin.cn/post/7371312966364332042
前端比localStorage存储还大的本地存储方案
产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。
方案选择
- 既然要存储的数量大,得排除cookie
- localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
- websql 使用简单,存储量大,兼容性差,备选
- indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选
既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。
那就是 localforage
localforage
localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage
API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。
关于兼容性
localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求
使用
解决了兼容性和存储量的点,我们就来看看localforage的基础用法
安装
# 通过 npm 安装:
npm install localforage
// 直接引用
<script src="localforage.js"></script>
<script>console.log('localforage is: ', localforage);</script>
获取存储
getItem(key, successCallback)
从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。
localforage.getItem('somekey').then(function(value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});
// 回调版本:
localforage.getItem('somekey', function(err, value) {
// 当离线仓库中的值被载入时,此处代码运行
console.log(value);
});
设置存储
setItem(key, value, successCallback)
将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:
- Array
- ArrayBuffer
- Blob
- Float32Array
- Float64Array
- Int8Array
- Int16Array
- Int32Array
- Number
- Object
- Uint8Array
- Uint8ClampedArray
- Uint16Array
- Uint32Array
- String
localforage
.setItem("somekey", "some value")
.then(function (value) {
// 当值被存储后,可执行其他操作
console.log(value);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});
// 不同于 localStorage,你可以存储非字符串类型
localforage
.setItem("my array", [1, 2, "three"])
.then(function (value) {
// 如下输出 `1`
console.log(value[0]);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});
// 你甚至可以存储 AJAX 响应返回的二进制数据
req = new XMLHttpRequest();
req.open("GET", "/photo.jpg", true);
req.responseType = "arraybuffer";
req.addEventListener("readystatechange", function () {
if (req.readyState === 4) {
// readyState 完成
localforage
.setItem("photo", req.response)
.then(function (image) {
// 如下为一个合法的 <img> 标签的 blob URI
var blob = new Blob([image]);
var imageURI = window.URL.createObjectURL(blob);
})
.catch(function (err) {
// 当出错时,此处代码运行
console.log(err);
});
}
});
删除存储
removeItem(key, successCallback)
从离线仓库中删除 key 对应的值。
localforage.removeItem('somekey').then(function() {
// 当值被移除后,此处代码运行
console.log('Key is cleared!');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});
清空存储
clear(successCallback)
从数据库中删除所有的 key,重置数据库。
localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。
localforage.clear().then(function() {
// 当数据库被全部删除后,此处代码运行
console.log('Database is now empty.');
}).catch(function(err) {
// 当出错时,此处代码运行
console.log(err);
});
localforage是否万事大吉?
用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。
内存不足的前提下,localforage继续缓存会怎么样?
在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro
解决
存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储
setItem({
value: '1',
label: 'a',
module: 'a',
timestamp: '11111111111'
})
- 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
- 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
- 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
- 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
- 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)
来源:juejin.cn/post/7273028474973012007
总要有点爱好来支撑乏味的生活
我,一名7年老前端,目前还在从事前端工作,接下来这篇文章可能有点长,主要围绕我是如何接触画画这件事。
2015年毕业后,第一份工作是浙江台州的一家药企:海正药业,在实验室从事肿瘤药的研发工作,工资3000+,包吃住。
2016-2017年,这期间算是成为前端程序员的探路阶段,在这个过程中,我接触了很多不一样的工作。第一份工作:某三方检测机构检测员,因为公司规模不大,所以和老板几乎每天待在一起,每天做的事情就是陪着老板出入各酒店,检测酒店的房间是否符合卫生标准,检测游泳池细菌是否在规定范围内,然后出具一份检测报告,用来保证酒店的正常营业,检测报告每年一检,每到一处酒店的人员都是笑脸相迎,很有面的感觉,但是这没有给我带来成就感,工资也才4000+,干了大半年后我还是决定离开;第二份工作:SGS宁波分公司检测员,每天在实验室检测各种材料的材质是否符合标准,和上一份工作有点类似,有人会好奇既然差不多,为什么还要选择尝试,因为SGS当时给人的感觉特别高大上,连邮件都是纯英文的,干了三个月,转完正我离职了,原因还是工资太低看不到希望;第三份工作:宁波荃盛粽子公司,包住,铺盖都搬进去了,突然灵光一闪,我以后就是个卖粽子的,赶紧跑。至此2016年已过半,我该怎么办,思来想去,我决定先不找工作了,专心自学安卓开发,学了小半年,感觉入门了但是心里还是空空的,出去面试的时候也不够自信,试了两家公司后我感觉还是不行,我还是没办法在这个行业扎根,于是2017年初,我毅然决然去了杭州,在那边培训了半年,回到宁波,找到了当时的第一份前端工作,老东家的同事都很好,刚去就体验了一把福利,整个部门集体三亚7日游,成年往事这里就不找照片了。也是在这一年,我在父母的帮助下,在宁波买了房,因为这时候月供才还的起,之前的工资都是养活不了自己的。
2017.08-现在,在现在的公司待了快5年了,公司敏捷开发模式,好在福利都还不错,每年有3000块的京东卡福利,逢年过节也都有很正式的礼盒,就加班稍微多一点,但早已习惯。
小插曲,2019年因为自己从事小程序开发有两年时间了,当时给自己做了两个小程序,都开源出来了,小程序云开发布道者,不知道那些粉丝还在不在,相当老的粉丝了,哈哈哈。
接下来我要讲画画这件事,这是文章的重头戏,我先附下链接从不懂油画到作品入展我用了多久。
今年,脑子里想的最多的还是,我是不是要找点事情做做,与其每天刷小视频不如利用这些时间做点有意义的事,于是有了下面这篇文章:心流C大调画展,截取里面的个人介绍方便大家查看:
在这里还是啰嗦一遍,虽然上面链接有讲到我入门油画的过程。上面说的有意义的事情其实在我脑子里已经计划了大半年,那就是画画,画什么画呢,高大上一点的,那就油画吧,油画保存的好的话可以几百年不褪色,那跟谁学 呢,不知道。一次偶然的机会,我在小红书看到了Michael老师的画画视频,发现老师真的很厉害,加上老师有留言说如果想学习就扣“666”,于是我顺利联系上了我现在的助教老师:sandy老师(mjssandy),交谈下来除了费用一下子有点难接受,其他都满意,那些天我像着了魔似的,就是想成为老师的终身会员,终身跟着老师画下去,于是经过两次发工资的倒腾,我终于报上了终身课,于是有了我现在的作品,下面我一一展示给大家看看:
田园风景
田园风景是入门油画的第一幅作品,很多人可能会觉得这不像没有基础的人画的,错,我至今不懂素描是什么,小时候喜欢一个人安静的画画,上学后就没有支撑这个爱好的资本了,一直搁置到现在。
苹果
苹果是第二幅作品,我突然发现自己对这种要求素描功底的静物反而画的有信心一些,于是第三幅作品我打算选一个有难度的。
狸花猫
这幅画用时20天,每天1-2小时,是我真正意义上的第一幅满意的作品。
清晨薄雾
这是一幅特别美的风景画,我自己还是蛮喜欢,只是前景的草画的有点凌乱。
客户定制猫咪-冥想猫
画完狸花猫,居然有人找我画订单了,这真的太不可思议了,接到这个消息后我第一时间联系我的老师,老师鼓励我勇敢去尝试,最终以1250的价格成交了这幅画。
英伦玫瑰
这幅画是继苹果之后的另一幅静物画作,画的过程中很得心应手,但是选这幅画也是有原因的,当时离520还有10天的时间,打算浪漫一下,画完送给老婆,谁知中间接到客户的猫咪定制,就把玫瑰暂时搁置了,画完冥想猫已经是520之后的事了,但是换的钱上交给了老婆,老婆很满意。
老者画像
老者画像,用时32天,每天1-2小时,这是有史以来画的最吃力的一幅作品,一个鼻子画了4天,好在最后很完美,老师都惊呆了,这是接触油画3个月的人能完成的事?!
儿子的大猩猩
儿子的大猩猩,儿子喜欢,那就安排,现在技法还太单一,以后画肯定不是这么个水平,这是继冥想猫之后的第二幅创作作品。
我是如何平衡工作,生活与画画的
一开始,我尝试晚上画,常常一画就是凌晨,导致第二天上班的精神萎靡,知道行不通我就改在周末多画画,但是有一个问题,一天画太长时间人会烦躁郁闷,周末也行不通。而且上面两个时间段我都放弃了陪家人的时间,完全不可取。后来我和老婆商量晚上早点睡,如果儿子晚上太兴奋我也不管,让老婆陪着他,我到了10点就要准时睡觉,不然无法保证我早上5点起床的目标,适应了1个月,感觉良好,一直到现在已经坚持4个月了,白天中午都会休息半小时,早上都能保证至少画1个小时,兴致高就2小时,这个时间段老婆孩子还在睡,完全属于我的个人放空时间。而且养成了早睡早起的习惯后我整个人的精神状态也好了,还有老婆孩子看到我能画出这么厉害的画,都有点崇拜主义,老婆更是每完成一幅作品必帮我发朋友圈炫耀,画画原来还有助于家庭和谐,夫妻幸福!
结语
我没敢想以后靠画画为生,但是我在坚持,坚持这个词看似不妥,但是没有坚持再好的天赋也会被埋没。最后我想对大家说的是,如果你有爱好,你不妨去尝试一下,不去尝试你永远不知道自己有多厉害!
最后的最后送给大家一句话,与君共勉:坚持和热爱是最好的天赋!
来源:juejin.cn/post/7404777095623802890
Jenkins:运维早搞定了,但我就是想偷学点前端CI/CD!
前言:运维已就绪,但好奇心作祟
- 背景故事: 虽然前端开发人员平时可能不会直接操控Jenkins,运维团队已经把这一切搞得井井有条。然而,你的好奇心驱使你想要深入了解这些自动化的流程。本文将带你一探究竟,看看Jenkins如何在前端项目中发挥作用。
- 目标介绍: 了解Jenkins如何实现从代码提交到自动部署的全过程,并学习如何配置和优化这一流程。
- 背景故事: 虽然前端开发人员平时可能不会直接操控Jenkins,运维团队已经把这一切搞得井井有条。然而,你的好奇心驱使你想要深入了解这些自动化的流程。本文将带你一探究竟,看看Jenkins如何在前端项目中发挥作用。
- 目标介绍: 了解Jenkins如何实现从代码提交到自动部署的全过程,并学习如何配置和优化这一流程。
Jenkins基础与部署流程
1. Jenkins到底是个什么东西?
- Jenkins简介: Jenkins是一个开源的自动化服务器,用于持续集成和持续交付。它能够自动化各种开发任务,提高开发效率和软件质量。
- 核心功能: Jenkins可以自动化构建、测试和部署任务,帮助开发团队实现快速、高效的开发流程。
- Jenkins简介: Jenkins是一个开源的自动化服务器,用于持续集成和持续交付。它能够自动化各种开发任务,提高开发效率和软件质量。
- 核心功能: Jenkins可以自动化构建、测试和部署任务,帮助开发团队实现快速、高效的开发流程。
2. 自动化部署流程详解
- 流程概述: 本文将重点介绍前端自动化部署的完整流程,包括代码提交、构建、打包、部署等步骤。具体流程如下:
- 代码提交:
- 开发人员通过
git push
将代码提交到远程仓库。
- 触发Jenkins自动构建:
- Jenkins配置为在代码提交时自动触发构建任务。
- 拉取代码仓库代码:
- Jenkins从仓库拉取最新代码。
- 构建打包:
- Jenkins运行构建命令(如
npm run build
),将源代码编译成可部署版本。
- 生成dist文件:
- 构建生成
dist
文件夹,包含打包后的静态资源。
- 压缩dist文件:
- 使用压缩工具(如
tar
或 zip
)将 dist
文件夹压缩成 dist.tar
或 dist.zip
。
- 迁移到指定环境目录下:
- 将压缩包迁移到目标环境目录(如
/var/www/project/
)。
- 删除旧dist文件:
- 删除目标环境目录下旧的
dist
文件,以确保保留最新版本。
- 解压迁移过来的dist.tar:
- 在目标环境目录下解压新的
dist.tar
文件。
- 删除dist.tar:
- 解压后删除压缩包,节省存储空间。
- 部署成功:
- 自动化流程完成,新的前端版本已经成功部署。
- 流程概述: 本文将重点介绍前端自动化部署的完整流程,包括代码提交、构建、打包、部署等步骤。具体流程如下:
- 代码提交:
- 开发人员通过
git push
将代码提交到远程仓库。
- 开发人员通过
- 触发Jenkins自动构建:
- Jenkins配置为在代码提交时自动触发构建任务。
- 拉取代码仓库代码:
- Jenkins从仓库拉取最新代码。
- 构建打包:
- Jenkins运行构建命令(如
npm run build
),将源代码编译成可部署版本。
- Jenkins运行构建命令(如
- 生成dist文件:
- 构建生成
dist
文件夹,包含打包后的静态资源。
- 构建生成
- 压缩dist文件:
- 使用压缩工具(如
tar
或zip
)将dist
文件夹压缩成dist.tar
或dist.zip
。
- 使用压缩工具(如
- 迁移到指定环境目录下:
- 将压缩包迁移到目标环境目录(如
/var/www/project/
)。
- 将压缩包迁移到目标环境目录(如
- 删除旧dist文件:
- 删除目标环境目录下旧的
dist
文件,以确保保留最新版本。
- 删除目标环境目录下旧的
- 解压迁移过来的dist.tar:
- 在目标环境目录下解压新的
dist.tar
文件。
- 在目标环境目录下解压新的
- 删除dist.tar:
- 解压后删除压缩包,节省存储空间。
- 部署成功:
- 自动化流程完成,新的前端版本已经成功部署。
- 代码提交:
准备工作
话不多说干就完了!!!
安装git
yum install -y git
查看是否安装成功
git --version
生成秘钥
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
查看公钥
cat ~/.ssh/id_rsa.pub
将公钥添加到GitHub或其他代码库的SSH Keys

yum install -y git
查看是否安装成功
git --version
生成秘钥
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
查看公钥
cat ~/.ssh/id_rsa.pub
将公钥添加到GitHub或其他代码库的SSH Keys
Docker安装
直接查看 菜鸟教程
安装完之后,配置docker镜像源详情参考 24年6月国内Docker镜像源失效解决办法...
编辑 /etc/resolv.conf
文件:
sudo vim /etc/resolv.conf
添加或修改以下行以使用 Cloudflare 的 DNS 服务器:
nameserver 1.1.1.1
nameserver 1.0.0.1
创建完成的docker-hub镜像输出示例:
查看docker相关的rpm源文件是否存在
rpm -qa |grep docker
作用
rpm -qa
:列出所有已安装的 RPM 包。grep docker
:筛选出包名中包含 docker
的条目。
示例输出
启动Docker服务:
- 启动Docker服务并设置为开机自启:
sudo systemctl start docker
sudo systemctl enable docker
直接查看 菜鸟教程
安装完之后,配置docker镜像源详情参考 24年6月国内Docker镜像源失效解决办法...
编辑 /etc/resolv.conf
文件:
sudo vim /etc/resolv.conf
添加或修改以下行以使用 Cloudflare 的 DNS 服务器:
nameserver 1.1.1.1
nameserver 1.0.0.1
创建完成的docker-hub镜像输出示例:
查看docker相关的rpm源文件是否存在
rpm -qa |grep docker
作用
rpm -qa
:列出所有已安装的 RPM 包。grep docker
:筛选出包名中包含docker
的条目。
示例输出 启动Docker服务:
- 启动Docker服务并设置为开机自启:
sudo systemctl start docker
sudo systemctl enable docker
Docker安装Docker Compose
Docker Compose 可以定义和运行多个 Docker 容器
应用的工具。它允许你使用一个单独的文件(通常称为 docker-compose.yml)来配置应用程序的服务,然后使用该文件快速启动整个应用的所有服务。
第一步,下载安装
curl -L https://get.daocloud.io/docker/compose/releases/download/v2.4.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
第二步,查看是否安装成功
docker-compose -v

第三步,给/docker/jenkins_home
目录设置最高权限,所有用户都具有读、写、执行这个目录的权限。(等建了/docker/jenkins_home
目录之后设置)
chmod 777 /docker/jenkins_home
Docker Compose 可以定义和运行多个 Docker 容器
应用的工具。它允许你使用一个单独的文件(通常称为 docker-compose.yml)来配置应用程序的服务,然后使用该文件快速启动整个应用的所有服务。
第一步,下载安装
curl -L https://get.daocloud.io/docker/compose/releases/download/v2.4.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
第二步,查看是否安装成功
docker-compose -v
第三步,给/docker/jenkins_home
目录设置最高权限,所有用户都具有读、写、执行这个目录的权限。(等建了/docker/jenkins_home
目录之后设置)
chmod 777 /docker/jenkins_home
创建Docker相关文件目录
可以命令创建或者相关shell可视化工具创建, 命令创建如下:
mkdir /docker
mkdir /docker/compose
mkdir /docker/jenkins_home
mkdir /docker/nginx
mkdir /docker/nginx/conf
mkdir /docker/html
mkdir /docker/html/dev
mkdir /docker/html/release
mkdir /docker/html/pro
创建docker-compose.yml
、nginx.conf
配置文件
cd /docker/compose touch docker-compose.yml
cd /docker/nginx/conf touch nginx.conf
完成后目录结构如下: 

可以命令创建或者相关shell可视化工具创建, 命令创建如下:
mkdir /docker
mkdir /docker/compose
mkdir /docker/jenkins_home
mkdir /docker/nginx
mkdir /docker/nginx/conf
mkdir /docker/html
mkdir /docker/html/dev
mkdir /docker/html/release
mkdir /docker/html/pro
创建docker-compose.yml
、nginx.conf
配置文件
cd /docker/compose touch docker-compose.yml
cd /docker/nginx/conf touch nginx.conf
完成后目录结构如下:
编写nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
#dev环境
server {
#监听的端口
listen 8001;
server_name localhost;
#设置日志
access_log logs/dev.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/dev/dist;
# root /home/html/dev/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
#release环境
server {
#监听的端口
listen 8002;
server_name localhost;
#设置日志
access_log logs/release.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/release/dist;
# root /home/html/release/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
#pro环境
server {
#监听的端口
listen 8003;
server_name localhost;
#设置日志
access_log logs/pro.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/pro/dist;
# root /home/html/pro/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
# include /etc/nginx/conf.d/*.conf;
}
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
#dev环境
server {
#监听的端口
listen 8001;
server_name localhost;
#设置日志
access_log logs/dev.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/dev/dist;
# root /home/html/dev/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
#release环境
server {
#监听的端口
listen 8002;
server_name localhost;
#设置日志
access_log logs/release.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/release/dist;
# root /home/html/release/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
#pro环境
server {
#监听的端口
listen 8003;
server_name localhost;
#设置日志
access_log logs/pro.access.log main;
#定位到index.html
location / {
#linux下HTML文件夹,就是你的前端项目文件夹
root /usr/share/nginx/html/pro/dist;
# root /home/html/pro/dist;
#输入网址(server_name:port)后,默认的访问页面
index index.html;
try_files $uri $uri/ /index.html;
}
}
# include /etc/nginx/conf.d/*.conf;
}
编写docker-compose.yml
networks:
frontend:
external: true
services:
docker_jenkins:
user: root # root权限
restart: always # 重启方式
image: jenkins/jenkins:lts # 使用的镜像
container_name: jenkins # 容器名称
ports: # 对外暴露的端口定义
- 8999:8080
- 50000:50000
environment:
- TZ=Asia/Shanghai ## 设置时区 否则默认是UTC
#- "JENKINS_OPTS=--prefix=/jenkins_home"
## 自定义 jenkins 访问前缀(设置了的话访问路径就为你的ip:端口/jenkins_home,反之则直接为ip:端口)
volumes: # 卷挂载路径
- /docker/jenkins_home/:/var/jenkins_home
# 挂载到容器内的jenkins_home目录
# docker-compose up 就会自动生成一个jenkins_home文件夹
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose
docker_nginx_dev:
restart: always
image: nginx
container_name: nginx_dev
ports:
- 8001:8001
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
docker_nginx_release:
restart: always
image: nginx
container_name: nginx_release
ports:
- 8002:8002
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
environment:
- TZ=Asia/Shanghai
docker_nginx_pro:
restart: always
image: nginx
container_name: nginx_pro
ports:
- 8003:8003
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
networks:
frontend:
external: true
services:
docker_jenkins:
user: root # root权限
restart: always # 重启方式
image: jenkins/jenkins:lts # 使用的镜像
container_name: jenkins # 容器名称
ports: # 对外暴露的端口定义
- 8999:8080
- 50000:50000
environment:
- TZ=Asia/Shanghai ## 设置时区 否则默认是UTC
#- "JENKINS_OPTS=--prefix=/jenkins_home"
## 自定义 jenkins 访问前缀(设置了的话访问路径就为你的ip:端口/jenkins_home,反之则直接为ip:端口)
volumes: # 卷挂载路径
- /docker/jenkins_home/:/var/jenkins_home
# 挂载到容器内的jenkins_home目录
# docker-compose up 就会自动生成一个jenkins_home文件夹
- /usr/local/bin/docker-compose:/usr/local/bin/docker-compose
docker_nginx_dev:
restart: always
image: nginx
container_name: nginx_dev
ports:
- 8001:8001
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
docker_nginx_release:
restart: always
image: nginx
container_name: nginx_release
ports:
- 8002:8002
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
environment:
- TZ=Asia/Shanghai
docker_nginx_pro:
restart: always
image: nginx
container_name: nginx_pro
ports:
- 8003:8003
volumes:
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
- /docker/html:/usr/share/nginx/html
- /docker/nginx/logs:/var/log/nginx
启动Docker-compose
cd /docker/compose
docker-compose up -d
此时就会自动拉取jenkins镜像与Nginx镜像
查看运行状态
docker-compose ps -a
示例输出

cd /docker/compose
docker-compose up -d
此时就会自动拉取jenkins镜像与Nginx镜像
查看运行状态
docker-compose ps -a
示例输出
验证Nginx
在/docker/html/dev/dist
目录下新建index.html
,文件内容如下:
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Nginxtitle>
head>
<body>
<h1>Hello Nginxh1>
body>
html>
浏览器打开,输入服务器地址:8001
看到下面的页面说明nginx配置没问题,同样的操作可测试下8002端口和8003端口

在/docker/html/dev/dist
目录下新建index.html
,文件内容如下:
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello Nginxtitle>
head>
<body>
<h1>Hello Nginxh1>
body>
html>
浏览器打开,输入服务器地址:8001
看到下面的页面说明nginx配置没问题,同样的操作可测试下8002端口和8003端口
验证Jenkins
浏览器输入服务器地址:8999
查看jenkins初始密码
docker logs jenkins

安装插件


浏览器输入服务器地址:8999
查看jenkins初始密码
docker logs jenkins
安装插件
安装Publish Over SSH、NodeJS
【Dashboard】——>【Manage Jenkins】——>【Plugins】——>【Available plugins】,搜索Publish Over SSH
、NodeJS
,安装后重启。 

- Publish Over SSH配置远程服务器
找到SSH Servers

点击Test Configuration
显示successs
则成功,之后再Apply
并且Save
。

- NodeJS配置
找到Nodejs

坑点!!! 使用阿里云镜像
否则会容易报错

阿里云node镜像地址
https://mirrors.aliyun.com/nodejs-release/

【Dashboard】——>【Manage Jenkins】——>【Plugins】——>【Available plugins】,搜索Publish Over SSH
、NodeJS
,安装后重启。
- Publish Over SSH配置远程服务器
找到
SSH Servers
点击
Test Configuration
显示successs
则成功,之后再Apply
并且Save
。
- NodeJS配置
找到
Nodejs
坑点!!! 使用阿里云镜像
否则会容易报错
阿里云node镜像地址
https://mirrors.aliyun.com/nodejs-release/
添加凭据
添加凭据,也就是GitHub或者其他远程仓库的凭据可以是账号密码也可以是token,方便之后使用。话不多说,Lets go!

我这里使用的是ssh
,原因用账号密码
拉不下来源码不知道为啥

添加凭据,也就是GitHub或者其他远程仓库的凭据可以是账号密码也可以是token,方便之后使用。话不多说,Lets go!
我这里使用的是
ssh
,原因用账号密码
拉不下来源码不知道为啥
创建Job

选择自由风格 
- 配置
git
仓库地址 
- 构建环境 在 Jenkins 中将 Node.js 和 npm 的 bin 文件夹添加到
PATH
中,否则可能就会报错。 
- 选择
nodejs
版本


- 创建
shell
命令


- 自动部署到对应环境项目目录:上面打包到了Jenkins中的workspace中,但是我们设置的项目环境路径跟这个不同,比如开发环境项目目录是
/docker/html/dev/dist/
,所以需要打包后,把dist文件内容推送到/docker/html/dev/dist/
目录下- 修改一下上面的
shell
脚本
#!/bin/bash
echo "Node版本:"
node -v
pnpm i
echo "依赖安装成功"
pnpm build
echo "打包成功"
rm -rf dist.tar # 每次构建删除已存在的dist压缩包
tar -zcvf dist.tar ./dist #将dist文件压缩成dist.tar
echo $PATH
- 然后
Add build step
选择Send files or execute commands over SSH
,Send files or execute

- 通过SSH连接到远程服务器执行命令和发送文件:
选择自由风格
- 配置
git
仓库地址 - 构建环境 在 Jenkins 中将 Node.js 和 npm 的 bin 文件夹添加到
PATH
中,否则可能就会报错。 - 选择
nodejs
版本
- 创建
shell
命令
- 自动部署到对应环境项目目录:上面打包到了Jenkins中的workspace中,但是我们设置的项目环境路径跟这个不同,比如开发环境项目目录是
/docker/html/dev/dist/
,所以需要打包后,把dist文件内容推送到/docker/html/dev/dist/
目录下- 修改一下上面的
shell
脚本
#!/bin/bash
echo "Node版本:"
node -v
pnpm i
echo "依赖安装成功"
pnpm build
echo "打包成功"
rm -rf dist.tar # 每次构建删除已存在的dist压缩包
tar -zcvf dist.tar ./dist #将dist文件压缩成dist.tar
echo $PATH
- 然后
Add build step
选择Send files or execute commands over SSH
,Send files or execute
- 通过SSH连接到远程服务器执行命令和发送文件:
- 修改一下上面的
ps:
脚本解释
cd /docker/html/dev
- 切换到
/docker/html/dev
目录。这是你要进行操作的工作目录。
- 切换到
rm -rf dist/
- 递归地删除
dist/
目录及其所有内容。这是为了确保旧的dist
目录被完全移除。
- 递归地删除
tar zxvf dist.tar
- 解压
dist.tar
文件,并将其中的内容解压到当前目录下。通常,dist.tar
会包含dist/
目录,解压后就会生成一个新的dist/
目录。
- 解压
rm dist.tar
- 解压完成后,删除
dist.tar
文件。这样可以节省存储空间并清理不再需要的压缩包。
- 解压完成后,删除
至此全部配置完成
- 测试ci/cd 点击
Build Now
开始构建
查看构建中任务的Console Output
,日志出现Finished: SUCCESS
即为成功
预览产物:
其他配置
GitHub webHooks配置
payload URL
为:http://ip:jenkins端口/github-webhook/
在Github webHooks
创建好后,到jenkins
中开启触发器 这样配置完后在push到相应分支就会自动构建发布
其他的个性化配置例如:钉钉通知、邮箱通知、pipeline配置等等就不做学习了,毕竟运维的事,我也学不会。😊😊
完结撒花🎉🎉🎉
来源:juejin.cn/post/7407333344889896998
前端中 JS 发起的请求可以暂停吗
在前端中,JavaScript(JS)可以使用XMLHttpRequest对象或fetch API来发起网络请求。然而,JavaScript本身并没有提供直接的方法来暂停请求的执行。一旦请求被发送,它会继续执行并等待响应。
尽管如此,你可以通过一些技巧或库来模拟请求的暂停和继续执行。下面是一种常见的方法:
1. 使用XMLHttpRequest对象
你可以在发送请求前创建一个XMLHttpRequest对象,并将其保存在变量中。然后,在需要暂停请求时,调用该对象的abort()方法来中止请求。当需要继续执行请求时,可以重新创建一个新的XMLHttpRequest对象并发起请求。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api', true);
xhr.send();
// 暂停请求
xhr.abort();
// 继续请求
xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api', true);
xhr.send();
2. 使用fetch API和AbortController
fetch API与AbortController一起使用可以更方便地控制请求的暂停和继续执行。AbortController提供了一个abort()方法,可以用于中止fetch请求。
var controller = new AbortController();
fetch('https://example.com/api', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
// 暂停请求
controller.abort();
// 继续请求
controller = new AbortController();
fetch('https://example.com/api', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
请注意,这些方法实际上是通过中止请求并重新发起新的请求来模拟暂停和继续执行的效果,并不能真正暂停正在进行的请求。
3. 曲线救国
模拟一个假暂停的功能,在前端的业务场景上,需要对这些数据进行处理之后渲染在界面上,如果我们能在请求发起之前增加一个控制器,在请求回来时,如果控制器为暂停状态则不处理数据,等待控制器恢复后再进行处理,也可以达到暂停的效果。
// 创建一个暂停控制器 Promise
function createPauseControllerPromise() {
const result = {
isPause: false, // 标记控制器是否处于暂停状态
resolveWhenResume: false, // 表示在恢复时是否解析 Promise
resolve(value) {}, // 解析 Promise 的占位函数
pause() { // 暂停控制器的函数
this.isPause = true;
},
resume() { // 恢复控制器的函数
if (!this.isPause) return;
this.isPause = false;
if (this.resolveWhenResume) {
this.resolve();
}
},
promise: Promise.resolve(), // 初始为已解决状态的 Promise
};
const promise = new Promise((res) => {
result.resolve = res; // 将解析函数与 Promise 关联
});
result.promise = promise; // 更新控制器中的 Promise 对象
return result; // 返回控制器对象
}
function requestWithPauseControl(request) {
const controller = createPauseControllerPromise(); // 创建暂停控制器对象
const controlRequest = request() // 执行请求函数
.then((data) => { // 请求成功回调
if (!controller.isPause) controller.resolve(); // 如果控制器未暂停,则解析 Promise
return data; // 返回请求结果
})
.finally(() => {
controller.resolveWhenResume = true; // 标记在恢复时解析 Promise
});
const result = Promise.all([controlRequest, controller.promise]).then(
(data) => {
controller.resolve(); // 解析控制器的 Promise
return data[0]; // 返回请求处理结果
}
);
result.pause = controller.pause.bind(controller); // 将暂停函数绑定到结果 Promise 对象
result.resume = controller.resume.bind(controller); // 将恢复函数绑定到结果 Promise 对象
return result; // 返回添加了暂停控制功能的结果 Promise 对象
}
为什么需要创建两个promise
在requestWithPauseControl函数中,需要等待两个Promise对象解析:一个是请求处理的Promise,另一个是控制器的Promise。通过使用Promise.all方法,可以将这两个Promise对象组合成一个新的Promise,该新的Promise会在两个原始Promise都解析后才会解析。这样做的目的是确保在处理请求结果之前,暂停控制器的resolve方法被调用,以便在恢复时解析Promise。
因此,将请求处理的Promise和控制器的Promise放入一个Promise数组,并使用Promise.all等待它们都解析完成,可以确保在两个Promise都解析后再进行下一步操作,以实现预期的功能。
使用
const requestFn = () => new Promise(resolve => {
setTimeout(() => resolve({ author: 'vincentzheng', msg: 'hello' }), 0)
})
const result = requestWithPauseControl(requestFn);
result.then((data) => {
console.log("返回结果", data);
});
if (Math.random() > 0.5) {
console.log('命中暂停')
result.pause();
}
setTimeout(() => {
result.resume();
}, 4000);
来源:juejin.cn/post/7310786521082560562
手撸一个精美简约loading加载功能,再也不怕复杂的网页效果了
我来看看怎么个事?
你们还记得自己为什么要做程序员吗?我先来说,就是看见别人有一个精美的网站。但是,现在很多人要么就是后端crud boy,要么就是前端vue渲染数据girl。没有现成的框架,现成的ui组件,就没法写代码了。好看的网页怎么来呢?有人会说是UI设计的,我前端只需要vue渲染数据就行了🤣(今天我们就不探讨后端技术🐶)。久而久之,自己就会慢慢变菜,最后想开发一个项目,发现无从下手,写个页面都费劲!!!所以,还是慢慢做一个全栈,这样既可以写好玩的工具,也可以提高自己的竞争力,强者恒强,没错就是我啦😅
1.loading实际效果图
字不重要,看图👉👉👉👉
pc端
移动端
2.准备css素材
这种loading的效果,网上有很多网站可以直接diy,几乎没有人手写一个。当然你也可以手写,如果觉得闲的话
推荐网站
国内也有很多,我使用的是国外网站(科学上网)
下载素材推荐svg格式,如果你的svg动图存在背景
如图,这种背景一定要去掉,给svg设置一个透明度,找到svg文件,background属性,设置rgb(255,255,255,0)就可以了,如下图:
3.loading隐藏与显示逻辑
思考🤔:
当我们点击按钮的时候,一般会触发请求,比如请求后台数据,这个时候中间就会有加载的样式。
总结就两个条件:
1.按钮要触发点击事件,开启loading效果
2.需要一个事件完成的状态(标记),关闭loading效果
4.编写looding组件,全局注册组件

<script setup>
</script>
<template>
<div class="loading">
<img src="./loading.svg" alt="loading"/>
</div>
</template>
<style scoped lang="scss">
.loading {
//通过定位,实现loading水平垂直居中
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
img {
width: 70px;
height: 70px;
}
}
</style>
全局注册loading组件
5.登录页面使用loading组件
<script setup>
import {reactive, ref, watch} from "vue";
//模拟请求数据,code===200表示事件完成
const result = reactive({data: [], code: 0})
//判断按钮是否触发点击事件,默认false,按钮没有触发点击
const clickFlag = ref(false)
//按钮提交方法
const submit = () => {
//重置code
result.code = 0
//标记请求触发
clickFlag.value = true
//模拟http请求
setTimeout(() => {
//模拟后台数据
result.data = [{'name': 'bobo', 'age': 12}]
//模拟请求完成
result.code = 200
}, 3000)
}
</script>
<template>
<div class="login-container">
<div class="login-box">
<div class="form">
<h2>用户登录</h2>
<div class="content">
<input class="input" type="text" placeholder="请输入账号">
<input class="input" type="password" placeholder="请输入密码">
<button @click="submit" class="button">登录</button>
<!--判断loading 1.有没有点击事件 2.有没有loading终止标记-->
<loading v-show="result.code!==200&&clickFlag"></loading>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-container {
.login-box {
.form {
position: relative;
padding: 24px;
text-align: center;
color: rgb(55 65 81);
line-height: 1.75rem;
}
}
}
</style>
具体页面布局的代码,请参考我之前的文章juejin.cn/post/738854…
这里不做过多叙述!能坚持✊看到这里,想必你一定很棒,给你个🍭🍭🍭
在线浏览网址http://www.codingfox.icu/#/dashboard (我不知道可以坚持到多久,靠爱发电,网站搬迁会尽量迁移)
来源:juejin.cn/post/7389178780437921803
uni-app初体验,如何实现一个外呼APP
起因
2024年3月31日,我被公司裁员了。
2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。
2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。
2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。
可行性分析
涉及到的修改:
- 系统前后端
- 拨号功能的APP
拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。
我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!
因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。
第一版
需求分析
虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。
但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。
- 拨号APP
- 权限校验
- 实现部分(拨号、录音、文件读写)
- ❌权限引导
- 查询当前手机号
- 直接使用input表单,由用户输入
- 查询当前手机号的拨号任务
- 因为后端没有socket,使用setTimeout模拟轮询实现。
- 拨号、录音、监测拨号状态
- 根据官网API和一些安卓原生实现
- 更新任务状态
- 告诉后端拨号完成
- ❌通话录音上传
- ❌通话日志上传
- ❌本地通时通次统计
- 程序运行日志
- 其他
- 增加开始工作、开启录音的状态切换
- 兼容性,只兼容安卓手机即可
- 权限校验
基础设计
一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。
开干
虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。
1、下载 HbuilderX。
2、新建项目,直接选择了默认模板。
3、清空 Hello页面,修改文件名,配置路由。
4、在vue文件里写主要的功能实现,并增加 Http.js
、Record.js
、PhoneCall.js
、Power.js
来实现对应的模块功能。
⚠️关于测试和打包
运行测试
在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:
- 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。
- 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。
- 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。
- 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。
关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。
但是不知道为什么,我这里一直显示安装自定义基座失败。。。
打包测试
除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。
点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。
我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。
另外,在打包之前我们首先要配置manifest.json
,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置、Android官方权限常量文档。以下是拨号所需的一些权限:
// 录制音频
<uses-permission android:name="android.permission.RECORD_AUDIO" />
// 修改音频设置
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
// 照相机
<uses-permission android:name="android.permission.CAMERA" />
// 写入外部存储
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
// 读取外部存储
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
// 读取电话号码
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
// 拨打电话
<uses-permission android:name="android.permission.CALL_PHONE" />
// 呼叫特权
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
// 通话状态
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
// 读取拨号日志
<uses-permission android:name="android.permission.READ_CALL_LOG" />
// 写入拨号日志
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
// 读取联系人
<uses-permission android:name="android.permission.READ_CONTACTS" />
// 写入联系人
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
// 读取SMS?
<uses-permission android:name="android.permission.READ_SMS" />
// 写入设置
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
// 唤醒锁定?
<uses-permission android:name="android.permission.WAKE_LOCK" />
// 系统告警窗口?
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
// 接受完整的引导?
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。
⚠️权限校验
1、安卓 1
好像除了这样的写法还可以写"scope.record"
或者permission.CALL_PHONE
。
permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
// 1 获得权限 2 本次拒绝 -1 永久拒绝
});
2、安卓 2
plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
// e.granted 获得权限
// e.deniedPresent 本次拒绝
// e.deniedAlways 永久拒绝
});
3、uni-app
这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。
// 检查权限
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
// 示例:请求权限
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
// 引导用户到设置中开启权限
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
// 引导用户到设置页
uni.openSetting();
}
}
});
}
});
}
});
✅拨号
三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。
另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;
,我这里只需要兼容固定机型。
1、uni-app API
uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});
2、Android
plus.device.dial(phone, false);
3、Android 原生
写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。
// Android
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
// 导入Activity、Intent类
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
// 获取主Activity对象的实例
var main = plus.android.runtimeMainActivity();
// 创建Intent
var uri = Uri.parse("tel:" + phone); // 这里可修改电话号码
var call = new Intent("android.intent.action.CALL", uri);
// 调用startActivity方法拨打电话
main.startActivity(call);
}
✅拨号状态查询
第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。
export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
//电话状态 0->空闲状态 1->振铃状态 2->通话存在
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}
⚠️录音
录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!
一坑
就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。
二坑
后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。
但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。
三坑
虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。
另辟蹊径
其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。
// 录音
var log = console.log,
recorder = null,
// innerAudioContext = null,
isRecording = false;
export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
// innerAudioContext = uni.createInnerAudioContext();
// innerAudioContext.autoplay = true;
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}
export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`); // :${res.tempFilePath}
// 处理录制的音频文件(例如,保存或上传)
// powerCheckSaveRecord(res.tempFilePath);
saveRecording(res.tempFilePath);
});
recorder.stop();
}
export function saveRecording(filePath) {
// 使用uni.saveFile API保存录音文件
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
// 保存成功后,res.savedFilePath 为保存后的文件路径
log(`录音保存成功:${res.savedFilePath}`);
// 可以将res.savedFilePath保存到你的数据中,或者执行其他保存相关的操作
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}
运行日志
为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。
联调、测试、交工
搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。
第二版
2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。
我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。
需求分析
- ✅拨号APP
- 登录
- uni-id实现
- 权限校验
- 拨号权限、文件权限、自带通话录音配置
- 权限引导
- 文件权限引导
- 通话录音配置引导
- 获取手机号权限配置引导
- 后台运行权限配置引导
- 当前兼容机型说明
- 拨号
- 获取手机号
- 是否双卡校验
- 直接读取手机卡槽中的手机号码
- 如果用户不会设置权限兼容直接input框输入
- 拨号
- 全局拨号状态监控注册、取消
- 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断
- 获取手机号
- 录音
- 读取录音文件列表
- 支持全部或按时间查询
- 播放录音
- ❌上传录音文件到云端
- 读取录音文件列表
- 通时通次统计
- 云端数据根据上面状态监控获取并上传
- 云端另写一套页面
- 本地数据读取本机的通话日志并整理统计
- 支持按时间查询
- 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等
- 云端数据根据上面状态监控获取并上传
- 其他
- 优化日志显示形式
- 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式
- 在上个组件的基础上实现权限校验和权限引导
- 在上两个组件的基础上实现主页面逻辑功能
- 增加了拨号测试、远端连接测试
- 修改了APP名称和图标
- 打包时增加了自有证书
- 优化日志显示形式
- 登录
中间遇到并解决的一些问题
关于框架模板
这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。
建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id
配置一个JSON文件来约定用户系统的一些配置。
打包的时候也要在manifest.json
将部分APP模块配置进去。
还搞了挺久的,半天才查出来。。
类聊天组件实现
- 设计
- 每个对话为一个无状态组件
- 一个图标、一个名称、一个白底的展示区域、一个白色三角
- 内容区域通过类型判断如何渲染
- 根据前后两条数据时间差判断是否显示灰色时间
- 参数
- ID、名称、图标、时间、内容、内容类型等
- 样式
- 根据左边右边区分发送接收方,给与不同的类名
- flex布局实现
样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。
关于后台运行
这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。
- 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)
- 通过不停的访问位置信息
- 通过查找相应的插件、询问GPT、百度查询
- 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)
- 通过切入后台后,发送消息实现(没测试)
测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。
关于通话状态、通话记录中的类型
这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。
通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。
通话日志:呼入、呼出、未接、语音邮件、拒接
交付
总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。
后面的计划
- 把图标改好
- 把录音文件是否已上传、录音上传功能做好
- 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等
- 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限
- 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去
- 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西
- 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的
- 增加拨号前的校验,对接平台,对于经常拉黑电销的客户号码进行过滤
大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。
最后
现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!
😂被举报标题党了,换个名字。
来源:juejin.cn/post/7368421971384860684
中国研发部门一锅端,IBM程序员的“黑色星期五”
大家好,我是晓凡。
程序员的“黑色星期五”
想象一下,你正坐在办公室,准备享受周末的轻松时刻,突然,你的工作账号被停用了,各种公司相关的权限没了,无法访问公司内网。
这不是电影情节,而是IBM中国研发部门员工的真实遭遇。一夜之间,千余名员工被一锅端。
这件事发生得太突然,几乎没有一点点征兆和信号,看得晓凡是一脸懵逼。
IBM裁员:波及千人
裁员,在互联网行业并不是新鲜事。
但IBM这次裁员的规模和速度,着实让人震惊。
据悉,IBM中国在不同区设有多个分公司,据称大约有12000名员工。
被收回权限的员工属于IBMV,下设CDL(IBM中国研发中心)和CSL(IBM中国系统中心),主要负责研发和测试。
波及到了1000+人,遍布北京、上海、大连等各地的员工。赔偿方案为N+3,但具体情况可能更为复杂。
我们来看看IBM官方给出的解释
中国的企业,尤其是民营企业,越来越重视抓住混合云和人工智能技术带来的机遇。
因此,IBM 在中国的本地战略重点将转向利用自身在技术和服务方面的丰富经验,组建一支具备相应技能的团队,以更好地与中国客户合作,共同创造符合客户需求的解决方案。
下面是网传的针对此此次裁员3分钟会议纪要
我们将内容翻译过来大概如下:
【我叫 Jack Hergenrother,是全球企业系统开发的副总裁。今天我们有一个重要的管理决策要与大家分享。
为了支持我们的全球客户和我们的业务战略,IBM 基础设施决定将开发任务从中国系统实验室转移到海外的其他 IBM基础设施基地。
我们正在退出在中国的所有开发任务。
正如你们所知道的,IBM 基础设施继续转型,以帮助释放我们组织必须提供的全部价值,并帮助我们实现具有挑战性的全球市场的可持续业务。这种转变受市场动态和激烈竞争的影响。而**中国的基建业务近年来有所下滑。
对于 IBM Z,我们做出了艰难的决定——将开发工作转移到其他国家,以便更好地抓住市场机遇,并且更加更接近客户。
在存储方面,我们正在将开发工作整合到更少的地点,以应对激烈的竞争。基础设施的协同办公战略是全球性的。协同办公也不仅限于中国。我们做出了这一艰难的商业决策,以便提高效率并简化运营。
我是 Ross Moury,IBM Z 和 Linux One 的总经理。我要感谢大家为 IBM 所做的贡献以及在这个平台成功中所扮演的重要角色。我希望获得你们的理解和今后的合作。
我是 Danny Mace,存储工程副总裁。我知道这是一个艰难的决定,但这是支持我们的全球客户和业务战略所必需的行动。在此,我也要感谢你们的贡献。】
此外有不少网友注意到,现任 IBM CEO 是一名印度人 Arvind Krishna,自从他 2020 年上任后就曾在全球范围内进行了多轮裁员。此外根据 IBM 的招聘信息显示,目前 IBM 似乎正在印度不断增设岗位,故而部分网友猜测此次 IBM 中国研发部全体被裁或许也与此有关。
多轮裁员,用AI替代近8000人
裁员,往往不是单一因素的结果。IBM的裁员,背后是市场和技术的双重压力。
随着云计算和人工智能的兴起,传统的研发模式正在发生变化。
企业为了追求发展,需要尽可能的压缩成本。说实话,这两年,大家都不好过。
IBM CEO Arvind Krishna在采访中表示,后台职能部门,如人力资源的招聘将暂停或放缓。
未来5年,我们将看到30%的人将被AI和自动化所取代。
程序员的自救
面对裁员,作为一名普通程序员,我们该怎么做呢?
① 保持良好心态,不要焦虑,不要内卷。真的不是自己不优秀,而是大环境不好。
工作没了,身体也不能跨。只要身体不垮,一切都可以重来。
② 守住自己手里的钱,不要负债,不要负债,不要负债。
正所谓:金库充盈,心绪宁静。即使不幸被裁了,也能靠积蓄养活自己
③ 虽然AI短时间不能完全替代程序员,但一些重复性的工作将被AI和自动化所取代。
保持学习,多了解一些AI,确实可以帮我们提高工作效率
④ 不要在一棵树上吊死,趁着年轻,试错成本不是那么高,多尝试尝试其他赛道,随然不一定能成。
但也有可能发现可以一直干下去的副业。
来源:juejin.cn/post/7408070878829117491
火山引擎数智平台:A/B测试个性化配置能力发布,拓展多场景策略最优解
对于这些场景,你一定不会感到陌生:打开手机时,一款购物应用推荐的正好是你心仪已久的商品;浏览网页时,新闻资讯自动排列,展示的都是你最感兴趣的话题;沉浸于在线娱乐时,所呈现的内容仿佛是为你量身定制……
这一切与“用户个性化配置发布”能力息息相关。“用户个性化配置发布”指根据行为、性别,兴趣、地理位置等数据,为不同用户提供不同的内容和体验,以提高用户满意度和转化率。
作为火山引擎数智平台旗下的核心产品,DataTester近期重磅发布了“个性化配置发布”能力,支持企业根据用户的偏好、行为和历史数据,为其精准匹配内容和服务。
据介绍,火山引擎DataTester依托于字节跳动长期技术沉淀,已支持字节内部500多个业务,并对外服务美的、华泰证券、博西家电、乐刻健身等上百家企业。此次“个性化配置发布”能力,也是DataTester长期经验的产品化输出,在易用性、灵活性等层面,对比行业其他产品更具优势。
以某购物网站举例,为了提升购物站点订单转化率,在识别到用户即将离开站点时,用户运营团队可以在“个性化配置发布”能力的支持下,及时推送优惠券或折扣信息等,进一步驱动用户留存。专属定制化服务不仅能增强用户对平台的依赖和信任,有效地留住了可能因未找到心仪商品而流失的客户,同时吸引了更多原本处于观望状态的潜在客户进行购买。这种量身定制的体验,不仅让用户在获取资讯和服务的过程中节省时间精力,还让他们感受到被“读懂”和专属对待,从而增强了用户对品牌的好感和忠诚度。
不仅仅是“用户个性化配置发布”能力,DataTester在满足业务复杂需求的过程中,更演化出一站式实验管理与场景化特型实验等全方位能力,基于其稳定可靠的分流功能、科学完善的统计引擎、智能的调优算法,为业务增长、用户转化、产品迭代、策略优化、运营提效等各个环节提供科学的决策依据,让业务真正做到数据驱动。
为了给用户提供更极致的数据服务,火山引擎数智平台旗下的更多产品也在企业数据应用场景上持续拓展,如智能数据洞察DataWind、增长分析平台DataFinder、客户数据平台VeCDP、增长营销平台GMP等工具,覆盖企业所需的全链路数智能力,助力企业实现全场景数据消费,充分释放数据价值。(作者:李双)
收起阅读 »多用户抢红包,如何保证只有一个抢到
前言
在一个百人群中,群主发了个红包,设置的3个人瓜分。如何能够保证只有3个人能抢到。100个人去抢,相当于就是100个线程去争夺这3个资源,如果处理不好,可能就会发生“超卖”,产生脏数据,威胁系统的正常运行。
当100个人同时去抢,也就是线程1,线程2,线程3...,此时线程1和线程2已经抢到了,就还剩一个红包了,而此时线程3和线程4同时发出抢红包的命令,线程3查询数据库发现还剩1个,抢下成功,而线程3还未修改库存时,线程4也来读取,发现还剩一个,也抢成功。结果这就发生“超卖”,红包被抢了4个,数据库一看红包剩余为-1。
解决思路
为了保证资源的安全,不能让多个用户同时访问到资源,也就是需要互斥的访问共有资源,同一时刻只能让一个用户访问,也就是给共享资源加上一个悲观锁,只有拿到锁的线程才能正常访问资源,拿不到锁的线程也不能让他一直等着,直接返回用户让他稍后重试。
JVM本地锁
JVM本地锁由ReentrantLock或synchronized实现
//抢红包方法加锁
public synchronized void grabRedPaper(){
...业务处理
}
不过这种同步锁粒度太大,我们需要的是针对抢同一红包的用户互斥,而这种方式是所有调用grabRedPaper方法的线程都需要等待,即限制所有人抢红包操作,效率低且不符合业务需求。每个红包应该都有一个唯一性ID,在单个红包上加锁效率就会高很多,也是单进程常用的使用方式。
private Map<String, Object> lockMap = new HashMap<>();
//抢红包方法
public void grabRedPaper(String redPaperId) {
Object lock = getLock(redPaperId);
synchronized (lock) {
// 在这里进行对业务的互斥访问操作
}
}
//获取红包ID锁对象
private Object getLock(String redPaperId) {
if (!lockMap.containsKey(redPaperId)) {
lockMap.put(redPaperId, new Object());
}
return lockMap.get(redPaperId);
}
Redis分布式锁
但当我们使用分布式系统中,一个业务功能会打包部署到多台服务器上,也就是会有多个进程来尝试获取共享资源,本地JVM锁也就无法完成需求了,所以我们需要第三方统一控制资源的分配,也就是分布式锁。
分布式锁一般一般需要满足四个基本条件:
- 互斥:同一时刻,只能有一个线程获取到资源。
- 可重入:获取到锁资源后,后续还能继续获取到锁。
- 高可用:锁服务一个宕机后还能有另一个接着服务;再者即使发生了错误,一定时间内也能自动释放锁,避免死锁发生。
- 非阻塞:如果获取不到锁,不能无限等待。
有关分布式锁的具体实现我之前的文章有讲到Java实现Redis分布式锁 - 掘金 (juejin.cn)
Mysql行锁
再者我们还可以通过Mysql的行锁实现,SELECT...FOR UPDATE,这种方式会将查询时的行锁住,不允许其他事务修改,直到读取完毕。将行锁和修改红包剩余数量放在一个事务中,也能做到互斥。不过这种做法效率较差,不推荐使用。
总结
方案 | 实现举例 | 优点 | 缺点 |
---|---|---|---|
JVM本地锁 | synchronized | 实现简单,性能较好 | 只能在单个 JVM 进程内使用,无法用于分布式环境 |
Mysql行锁 | SELECT...FOR UPDATE | 保证并发情况下的隔离性,避免出现脏数据 | 增加了数据库的开销,特别是在高并发场景下;对应用程序有一定的侵入性,需要在 SQL 语句中正确使用锁定机制。 |
分布式锁 | Redis分布式锁 | 可用于分布式,性能较高 | 实现相对复杂,需要考虑锁的续租、释放等问题。 |
来源:juejin.cn/post/7398038222985543692
二维码扫码登录业务详解
二维码扫码登录业务详解
前言
二维码登录 顾名思义 重要是在于登录这俩个字
登录简单点来说可以概括为俩点
- 告诉系统
我是谁
- 向系统证明
我是谁
下面我们就会围绕着这俩点来展开详细说明
原理解析
其实大部分的二维码 都是一个url
地址
我们以掘金扫码登录为例来进行剖析
我们进行一个解析
我们可以发现她实际就是这样的一个url
所以说 我们二维码的一个操作 做出来的就是一个url地址
那么我们知道这个后 我们就可以来进行一个流程的解析。
就是一个这样简单的流程
流程概述
简单来说氛围下面的步骤:
- PC端:进入二维码登录页面,请求服务端获取二维码的ID。
- 服务端:生成二维码ID,并将其与请求的设备绑定后,返回有效的二维码ID。
- PC端:根据二维码ID生成二维码图片,并展示出来。
- 移动端:扫描二维码,解析出二维码ID。
- 移动端:使用移动端的token和二维码ID请求服务端进行登录。
- 服务端:解析验证请求,绑定用户信息,并返回给移动端一个用于二次确认的临时token。
- PC端:展示二维码为“待确认”状态。
- 移动端:使用二维码ID、临时token和移动端的token进行确认登录。
- 服务端:验证通过后,修改二维码状态,并返回给PC端一个登录的token。
下面我们来用一个python的代码来描述一下这个过程。
首先是服务端:
from flask import Flask, request, jsonify
import uuid
import time
app = Flask(__name__)
# 存储二维码ID和对应的设备信息以及临时token
qr_code_store = {}
temporary_tokens = {}
@app.route('/generate_qr', methods=['POST'])
def generate_qr():
device_id = request.json['device_id']
qr_id = str(uuid.uuid4())
qr_code_store[qr_id] = {'device_id': device_id, 'timestamp': time.time(), 'status': 'waiting'}
return jsonify({'qr_id': qr_id})
@app.route('/scan_qr', methods=['POST'])
def scan_qr():
qr_id = request.json['qr_id']
token = request.json['token']
if qr_id in qr_code_store:
qr_code_store[qr_id]['status'] = 'scanned'
temp_token = str(uuid.uuid4())
temporary_tokens[temp_token] = {'qr_id': qr_id, 'timestamp': time.time()}
return jsonify({'temp_token': temp_token})
return jsonify({'error': 'Invalid QR code'}), 400
@app.route('/confirm_login', methods=['POST'])
def confirm_login():
qr_id = request.json['qr_id']
temp_token = request.json['temp_token']
mobile_token = request.json['mobile_token']
if temp_token in temporary_tokens and temporary_tokens[temp_token]['qr_id'] == qr_id:
login_token = str(uuid.uuid4())
qr_code_store[qr_id]['status'] = 'confirmed'
return jsonify({'login_token': login_token})
return jsonify({'error': 'Invalid confirmation'}), 400
if __name__ == '__main__':
app.run(debug=True)
之后来看PC端:
import requests
import json
# 1. 请求生成二维码ID
response = requests.post('http://localhost:5000/generate_qr', json={'device_id': 'PC_device'})
qr_id = response.json()['qr_id']
# 2. 根据二维码ID生成二维码图片 (此处省略,可以使用第三方库生成二维码图片)
print(f"QR Code ID: {qr_id}")
# 7. 显示二维码进入“待确认”状态
print("QR Code Status: Waiting for confirmation")
之后再来看移动端的代码:
import requests
# 4. 扫描二维码,解析出二维码ID
qr_id = '解析出的二维码ID'
token = '移动端token'
# 5. 请求服务端进行登录
response = requests.post('http://localhost:5000/scan_qr', json={'qr_id': qr_id, 'token': token})
temp_token = response.json()['temp_token']
# 8. 使用二维码ID、临时token和移动端的token进行确认登录
response = requests.post('http://localhost:5000/confirm_login', json={'qr_id': qr_id, 'temp_token': temp_token, 'mobile_token': token})
login_token = response.json().get('login_token')
if login_token:
print("登录成功!")
else:
print("登录失败!")
这样一个简单的二维码登录的流程就出来了
案例解析
了解了流程之后我们来看看其他大型网站是如何实施的 这里拿哔哩哔哩来举例。
我们可以看到她的那个json实例
{
"code": 0,
"message": "0",
"ttl": 1,
"data": {
"url": "",
"refresh_token": "",
"timestamp": 0,
"code": 86101,
"message": "未扫码"
}
}
我们可以发现他是不断的去发送这个请求 每过1s大概
之后当我们扫描后发现已经变成等待确认
当我们确认后 他会返回
和我们说的流程大概的相同
来源:juejin.cn/post/7389952503041884170
数据无界,存储有方:MinIO,为极致性能而生!
MinIO: 数据宇宙的超级存储引擎,解锁云原生潜能- 精选真开源,释放新价值。
概览
MinIO,这一高性能分布式对象存储系统的佼佼者,正以开源的力量重塑企业数据存储的版图。它设计轻巧且完全兼容Amazon S3接口,成为连接全球开发者与企业的桥梁,提供了一个既强大又灵活的数据管理平台。超越传统存储桶的范畴,MinIO是专为应对大规模数据挑战而生,尤其适用于AI深度学习、大数据分析等高负载场景,其单个对象高达5TB的存储容量设计,确保了数据密集应用运行无阻,流畅高效。
无论是部署于云端、边缘计算环境还是本地服务器,MinIO展现了极高的适配性和灵活性。它以超简单的安装步骤、直观的管理界面以及平滑的扩展能力,极大地降低了企业构建复杂数据架构的门槛。利用MinIO,企业能够快速部署数据湖,以集中存储海量数据,便于分析挖掘;构建高效的内容分发网络(CDN),加速内容的全球分发;或建立可靠的备份存储方案,确保数据资产的安全无虞。
MinIO的特性还包括自动数据分片、跨区域复制、以及强大的数据持久性和高可用性机制,这些都为数据的全天候安全与访问提供了坚实保障。此外,其微服务友好的架构,使得集成到现有的云原生生态系统中变得轻而易举,加速了DevOps流程,提升了整体IT基础设施的响应速度和灵活性。
主要功能
你可以进入官方网站下载体验:min.io/download
- 高性能存储
- 优化的I/O路径:MinIO通过精心设计的I/O处理逻辑,减少了数据访问的延迟,确保了数据读写操作的高速执行。
- 并发设计:支持高并发访问,能够有效利用多核处理器,即使在高负载情况下也能维持稳定的吞吐量,特别适合处理大数据量的读写请求。
- 裸机级性能:通过底层硬件的直接访问和资源高效利用,使得在普通服务器上也能达到接近硬件极限的存储性能,为PB级数据的存储与处理提供强大支撑。
- 分布式架构
- 多节点部署:允许用户根据需求部署多个节点,形成分布式存储集群,横向扩展存储容量和处理能力。
- 纠删码技术:采用先进的纠删码(Erasure Coding)代替传统的RAID,即使在部分节点故障的情况下,也能自动恢复数据,确保数据的完整性和服务的连续性,提高了系统的容错能力。
- 高可用性与持久性:通过跨节点的数据复制或纠删码,确保数据在不同地理位置的多个副本,即使面临单点故障,也能保证数据的不间断访问,满足严格的SLA要求。
- 全面的S3兼容性
- 无缝集成:MinIO完全兼容Amazon S3 API,这意味着现有的S3应用程序、工具和库可以直接与MinIO对接,无需修改代码。
- 迁移便利:企业可以从AWS S3或任何其他S3兼容服务平滑迁移至MinIO,降低迁移成本,加速云原生应用的部署进程。
- 安全与合规
- 加密传输:支持SSL/TLS协议,确保数据在传输过程中加密,防止中间人攻击,保障数据通信安全。
- 访问控制:提供细粒度的访问控制列表(ACLs)和策略管理,实现用户和群体的权限分配,确保数据访问权限的严格控制。
- 审计与日志:记录详细的系统活动日志,便于监控和审计,符合GDPR、HIPAA等国际安全标准和法规要求。
- 简易管理与监控
- 直观Web界面:用户可通过Web UI进行集群配置、监控和日常管理,界面友好,操作简便。
- Prometheus集成:集成Prometheus监控系统,实现存储集群的实时性能监控和告警通知,帮助管理员及时发现并解决问题,确保系统稳定运行。
信息
截至发稿概况如下:
- 软件地址:github.com/minio/minio
- 软件协议:AGPL 3.0
- 编程语言:
语言 | 占比 |
---|---|
Go | 99.0% |
Other | 1.0% |
- 收藏数量:44.3K
面对不断增长的数据管理挑战,MinIO不仅是一个存储解决方案,更是企业在数字化转型旅程中的核心支撑力量,助力各行各业探索数据价值的无限可能。无论是优化存储成本、提升数据处理效率,还是确保数据安全与合规,MinIO持续推动技术边界,邀请每一位技术探索者加入其活跃的开源社区,共同参与讨论,贡献智慧,共同塑造数据存储的未来。
尽管MinIO凭借其卓越的性能与易于使用的特性在存储领域独树一帜,但面对数据量的指数级增长和环境的日益复杂,一系列新挑战浮出水面。首要任务是在海量数据中实现高效的数据索引与查询机制,确保信息的快速提取与分析。其次,在混合云及多云部署的趋势下,如何平滑实现数据在不同平台间的迁移与实时同步,成为提升业务连续性和灵活性的关键。再者,数据安全虽为根基,但在成本控制上也不可忽视,优化存储策略,在强化防护的同时降低开支,实现存储经济性与安全性的完美平衡,是当前亟待探讨与解决的课题。这些问题不仅考验着技术的极限,也为MinIO及其用户社区带来了新的研究方向与实践机遇。
热烈欢迎各位在评论区分享交流心得与见解!!!
来源:juejin.cn/post/7363570869065498675