Logo

site iconDnevend |小学后生

全栈工程师 | 技术 | 人文 | 价值分享
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Dnevend |小学后生 RSS 预览

为革命保护“视”力

2024-10-28 08:00:00

  “观察自己发现,其实不是注意力不集中,而是注意力非常容易集中,比如一个小红点,一条信息,都会让自己从放空的状态中跳出来,进入到具体的事物中去。 或许问题不该是如何集中注意力,而是想办法把环境中容易吸引注意力的事物挪掉。 更深处的原因,则是这些自己对于「存在」的焦虑,所以任何社交媒体/工具的反馈,总是能第一时间得到自己关注及行动,以便再次确认自己「存在」且「有价值」。” —— 产品沉思录 Vol.029

前言

  信息成瘾和注意力的夺取,在互联网平台和用户两者之间形成了相互同构的力量: 从平台角度,通过对产品细节的设计,给用户以信息反馈(如小红点,精准的内容推荐)机制,使用户养成对这种反馈的习惯,来保持用户的活跃度; 从用户角度,为了更多的流量的媒体和帐号,在发布的内容中精心的挑选和设计或穿插一些可能无关联甚至吸引性的图片。 大量无关联的配图造成了注意力的分散使得信息获取的过程受到干扰,无法专注。 图片和媒体资源的滥用,形成了一种噪音,使得纯文字的阅读能力和信息的表达深度在这视觉的转移运动中逐渐削弱。

  如何获取更多的注意力主动权,保护自己的时间和行为价值,就像做眼保健操一样,是在“课前”需要逐渐的养成良好习惯。

  另外推特这样的内容开放的平台,在公共场合浏览时,有时跳出一两条不合时宜的图片,让自己老脸一红或措手不及。

  因为这样的出发点,顺手也想尝试下浏览器插件的开发,便产生了 X-Comfort-Browser 这个小工具,用来解决上述场景的问题。

开发

框架选择

  在插件阵营,Webkit(Chrome、Edge、Safari、360...)和 Gecko(Firefox)两种不同内核的浏览器分占两壁江山。开发模式也存在差异,好在有成熟的框架可以对两者兼容,能省去不少开发的琐事。

目前主流的插件开发框架 Plasmo (10.5k)WXT (4.4k) 对 Typescript 和多种前端框架都做了支持,开发时可以根据自己的技术偏好来进行选择。

因为是先接触到了 WXT,所以该文章和插件就基于 WXT 来展开进行。

插件构成

  • Manifest.json 配置文件:定义插件的基本信息和权限。

  • Popup 弹窗页面:用户界面,展示信息和接收用户输入。

  • Content Script:内容脚本,用于与网页内容交互。

  • Background Script:后台脚本,处理长期任务和事件。

  • 其他组件:如 Options 页面、Browser Action、Page Action、通信机制等。

核心代码

content.ts
import { defineContentScript } from "wxt/sandbox";
import { defaultBlur, storageKeys } from "@/const";
import { createButton } from "@/utils";

const BLUR_EMOJI = "👀";
const UN_BLUR_EMOJI = "🙈";

// 元素状态
const statusMap = new Map<string, boolean>();

// 资源选择器(需要被模糊屏蔽的媒体资源)
const selectors = [
'[data-testid="tweetPhoto"]',
'[data-testid="videoComponent"]',
'[data-testid="videoPlayer"]',
'[data-testid="card.layoutLarge.media"]',
'[data-testid="collection-hero-image"]',
'[data-testid="article-cover-image"]',
];

async function handleElements() {
const enable = (await storage.getItem<boolean>(storageKeys.enable)) ?? true;
const blur = (await storage.getItem<number>(storageKeys.blur)) ?? defaultBlur;

selectors.forEach((selector) => {
let elements: HTMLElement[] = Array.from(
document.querySelectorAll(selector)
);

elements.forEach((element) => {
let current = element;
let hasBlur = false;

while (current.parentElement !== null) {
current = current.parentElement;
if (current.matches(selectors.join(","))) {
hasBlur = true;
break;
}
}

// 检测是否已存在被处理的父级
if (hasBlur) return;

let comfortId = element.getAttribute("data-comfort-id");

if (!comfortId) {
// 标记待处理元素,生成唯一ID
comfortId = crypto.randomUUID();
element.setAttribute("data-comfort-id", comfortId);

// 添加按钮手动切换元素状态
const button = createButton(comfortId, handleElements);
button.onclick = (e) => {
e.preventDefault();
e.stopPropagation();

const newStatus = !statusMap.get(comfortId!);

statusMap.set(comfortId!, newStatus);

if (newStatus) {
element.style.filter = `blur(${blur}px)`;
button.innerText = BLUR_EMOJI;
} else {
element.style.filter = "none";
button.innerText = UN_BLUR_EMOJI;
}
};

element.parentElement?.insertBefore(button, element);
}

// 保存元素的模糊状态
if (!statusMap.has(comfortId)) {
statusMap.set(comfortId, enable);
}

const targetElement = element as HTMLElement;
const toggleButton = document.getElementById(comfortId) as HTMLElement;

if (!enable) {
targetElement.style.filter = "none";
toggleButton.style.display = "none";
statusMap.clear();
return;
} else {
targetElement.style.transition = ".3s";
toggleButton.style.display = "block";
}

const blurStatus = statusMap.get(comfortId);
if (blurStatus && targetElement.style.filter !== `blur(${blur}px)`) {
targetElement.style.filter = `blur(${blur}px)`;
toggleButton.innerText = BLUR_EMOJI;
}

if (!blurStatus && targetElement.style.filter !== "none") {
targetElement.style.filter = "none";
toggleButton.innerText = UN_BLUR_EMOJI;
}
});
});
}

export default defineContentScript({
// 匹配脚本生效的域名
matches: ["*://x.com/*"],
// 脚本运行时机
runAt: "document_idle",
main(ctx) {
console.log("Hello from X-Comfort-Browser.");

// 监听 自定义参数值 变化
[storageKeys.blur, storageKeys.enable].forEach((key) => {
storage.watch<number | boolean>(key, (v) => {
handleElements();
});
});

// 监听 页面元素 变化
const observer = new MutationObserver(() => handleElements());
observer.observe(document.body, { childList: true, subtree: true });
},
});

体验

参考

Telegram 支付机器人开发小记

2024-08-01 08:00:00

随着 Telegram 迈向区块链&小程序时代,Telegram 内部已经与 TON 钱包做了集成,并为了应对 Apple 和 Google 关于数字产品销售的政策监管需要上线了 Telegram Stars 作为支付方式。依托 Telegram 生态的数亿用户,存在着大量机遇,并为区块链走向 Mass Adoption 铺设了一条新的高速公路。本文基于 grammY 框架,分享支付机器人开发过程中的心得,助你成功。

支付机器人

点击此处访问完整 Demo 地址

初始化

在使用测试环境进行机器人开发时,创建 Bot 实例,需要将environment指定为test,否则将会产生401 Unauthorized错误。

另外如果当前的网络环境需要使用科学上网才能访问 Telegram,还需要配置baseFetchConfig.agent为你的代理地址。

Bot Init
new Bot(process.env.BOT_TOKEN!, {
client: {
baseFetchConfig: {
agent: isDevEnv ? new HttpsProxyAgent('http://127.0.0.1:7890') : null
},
environment: isDevEnv ? 'test' : 'prod'
}
})

Stars 支付流程

Pay With Stars
// 1. 调用 `sendInvoice` 发送发票,currency 参数指定为`XTR`
ctx.api.sendInvoice(ctx.chat!.id, 'Title', 'Description', `payload`, 'XTR', [{ label: 'Label', amount: 1 }])

// 2. 检查发票,等待字段 `pre_checkout_query` 的更新
bot.on('pre_checkout_query', (ctx) => {

// 3. 通过 `answerPreCheckoutQuery` 批准或取消订单
ctx.answerPreCheckoutQuery(true)
// ctx.answerPreCheckoutQuery(false, {
// error_message: 'An unexpected error occurred. Please try again later.'
// })
})

// 4. 等待字段 `successful_payment` 的更新
bot.on(':successful_payment', ctx => {

// 5. 支付成功回调,存储成功支付的 `telegram_payment_charge_id`(未来可能需要用它来发起退款)
console.log(ctx.message?.successful_payment.telegram_payment_charge_id)

// 6. 向用户交付其所购买的商品和服务,业务逻辑...
ctx.reply('payment-success').catch(console.error)
})

TON 支付流程

  1. 生成指定钱包的支付链接
function generatePaymentLink(
toWallet: string,
amount: number | string | bigint,
comment: string,
app: "tonhub" | "tonkeeper"
) {
if (app === "tonhub") {
return `https://tonhub.com/transfer/${toWallet}?amount=${toNano(
amount
)}&text=${comment}`;
}

return `https://app.tonkeeper.com/transfer/${toWallet}?amount=${toNano(
amount
)}&text=${comment}`;
}
  1. 将生成的链接以菜单形式返回给用户,并提供check_transaction事件用于检查交易
const tonhubPaymentLink = generatePaymentLink(process.env.OWNER_WALLET!, amount, comment, 'tonhub')
const tonkeeperPaymentLink = generatePaymentLink(process.env.OWNER_WALLET!, amount, comment, 'tonkeeper')

const menu = new InlineKeyboard()
.url("Click to pay in TonHub", tonhubPaymentLink)
.row()
.url("Click to pay in TonKeeper", tonkeeperPaymentLink)
.row()
.text(`I sent ${amount} TON`, "check_transaction");

await ctx.reply(
`Tips`,
{ reply_markup: menu, parse_mode: "HTML" }
);
  1. 监听check_transaction事件,校验支付状态,处理支付成功的逻辑
bot.callbackQuery("check_transaction", checkTransaction);

async function checkTransaction(ctx) {
await verifyTransactionExistance(
process.env.OWNER_WALLET,
ctx.session.amount,
ctx.session.comment
);
}

async function verifyTransactionExistance(
toWallet: Address,
value: number,
comment: string
) {
const endpoint =
process.env.NETWORK === "mainnet"
? "https://toncenter.com/api/v2/jsonRPC"
: "https://testnet.toncenter.com/api/v2/jsonRPC";

const httpClient = new HttpApi(endpoint, {
apiKey: process.env.TONCENTER_TOKEN,
});

const transactions = await httpClient.getTransactions(toWallet, {
limit: 100,
});

let incomingTransactions = transactions.filter(
(tx) => Object.keys(tx.out_msgs).length === 0
);

for (let i = 0; i < incomingTransactions.length; i++) {
let tx = incomingTransactions[i];
// Skip the transaction if there is no comment in it
if (!tx.in_msg?.msg_data) {
continue;
}

// Convert transaction value from nano
let txValue = fromNano(tx.in_msg.value);
// Get transaction comment
let txComment = tx.in_msg.message;
if (txComment === comment && txValue === value.toString()) {
return true;
}
}

return false;
}

注意事项

  • 测试环境账号注册

    在 Telegram 的账号体系中,测试环境与主环境完全隔离,因此在进行测试环境登录时,无法直接使用现有账号进行登录,在扫码时会提示AUTH_TOKEN_INVALID2错误,以及无法收到验证码的情况。 所以你需要先注册一个测试账号,截止 2024 年 8 月,测试账号只能通过 iPhone 端 Telegram 进行。具体操作流程如下:

    1、登录 Telegram iPhone 2、多次点击右下角SettingTab 进入 Debug 页面 3、点击操作列表中的Accounts项 4、点击Login to another account选择Test环境,完成账号注册

    账号注册完成后,就可以按官方流程进入测试环境。在使用测试环境时,您可以采用未加密的 HTTP 链接来测试您的 Web 应用或 Web 登录功能。

    另外测试环境的 Telegram Star 也需要进行购买,不过可以参考下文使用 stripe 提供的测试信用卡无限制进行购买。

  • 信用卡测试支付

    在您的机器人支付功能仍在开发和测试阶段时,请使用 “Stripe 测试模式” 提供商。在此模式下,您可以进行支付操作而不会实际计费任何账户。测试模式中无法使用真实信用卡,但您可以使用测试卡,如 4242 4242 4242 4242 (完整测试卡列表)。您可以随意在测试模式与实时模式间切换,但在正式上线前,请务必查阅上线检查清单

引用参考

视差滚动实践

2024-07-01 08:00:00

视差滚动是一种在网页设计和视频游戏中常见的视觉效果技术,它通过在不同速度上移动页面或屏幕上的多层图像,创造出深度感和动感。 这种效果通过前景、中景和背景以不同的速度移动来实现,使得近处的对象看起来移动得更快,而远处的对象移动得较慢。

parallax-scroll

实现方式

1、background-attachment

通过配置该 CSS 属性值为fixed可以达到背景图像的位置相对于视口固定,其他元素正常滚动的效果。但该方法的视觉表现单一,没有纵深,缺少动感。

.parallax-box {
width: 100%;
height: 100vh;
background-image: url("https://picsum.photos/800");
background-size: cover;
background-attachment: fixed;

display: flex;
justify-content: center;
align-items: center;
}

2、Transform 3D

在 CSS 中使用 3D 变换效果,通过将元素划分至不同的纵深层级,在滚动时相对视口不同距离的元素,滚动所产生的位移在视觉上就会呈现越近的元素滚动速度越快,相反越远的元素滚动速度就越慢。

为方便理解,你可以想象正开车行驶在公路上,汽车向前移动,你转头看向窗外,近处的树木一闪而过,远方的群山和风景慢慢的渐行渐远,逐渐的在视野中消失,而天边的太阳却只会在很长的一段距离细微的移动。

.parallax {
perspective: 1px; /* 设置透视效果,为3D变换创造深度感 */
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
}

.parallax__group {
transform-style: preserve-3d; /* 保留子元素3D变换效果 */
position: relative;
height: 100vh;
}

.parallax__layer {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
}

/* 背景层样式,设置为最远的层 */
.parallax__layer--back {
transform: translateZ(-2px) scale(3);
z-index: 1;
}

/* 中间层样式,设置为中等距离的层 */
.parallax__layer--base {
transform: translateZ(-1px) scale(2);
z-index: 2;
}

/* 前景层样式,设置为最近的层 */
.parallax__layer--front {
transform: translateZ(0px);
z-index: 3;
}

principle

通过设置 perspective 属性,为整个容器创建一个 3D 空间。

使用 transform-style: preserve-3d 保持子元素的 3D 变换效果。

将内容分为多个层(背景、中间、前景),使用 translateZ() 将它们放置在 3D 空间的不同深度。

对于较远的层(如背景层),使用 scale() 进行放大,以补偿由于距离产生的视觉缩小效果。

当用户滚动页面时,由于各层位于不同的 Z 轴位置,它们会以不同的速度移动,从而产生视差效果。

3、ReactScrollParallax

想得到更炫酷的滚动视差效果,纯 CSS 的实现方式就会有些吃力。

如下是在 React 中实现示例,通过监听滚动事件,封装统一的视差组件,来达到多样的动画效果。

const Parallax = ({ children, effects = [], speed = 1, style = {} }) => {
// 状态hooks:用于存储动画效果的当前值
const [transform, setTransform] = useState("");

useEffect(() => {
if (!Array.isArray(effects) || effects.length === 0) {
console.warn("ParallaxElement: effects should be a non-empty array");
return;
}

const handleScroll = () => {
// 计算滚动进度
const scrollProgress =
(window.scrollY /
(document.documentElement.scrollHeight - window.innerHeight)) *
speed;

let transformString = "";

// 处理每个效果
effects.forEach((effect) => {
const { property, startValue, endValue, unit = "" } = effect;
const value =
startValue +
(endValue - startValue) * Math.min(Math.max(scrollProgress, 0), 1);

switch (property) {
case "translateX":
case "translateY":
transformString += `${property}(${value}${unit}) `;
break;
case "scale":
transformString += `scale(${value}) `;
break;
case "rotate":
transformString += `rotate(${value}${unit}) `;
break;
// 更多的动画效果...
default:
console.warn(`Unsupported effect property: ${property}`);
}
});

// 更新状态
setTransform(transformString);
};

window.addEventListener("scroll", handleScroll);
// 初始化位置
handleScroll();

return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [effects, speed]);

// 渲染带有计算样式的子元素
return <div style={{ ...style, transform }}>{children}</div>;
};

在此基础上你可以添加缓动函数使动画效果更加平滑;以及使用requestAnimationFrame获得更高的动画性能。

requestAnimationFrame 带来的性能提升

同步浏览器渲染周期:requestAnimationFrame 会在浏览器下一次重绘之前调用指定的回调函数。这确保了动画更新与浏览器的渲染周期同步,从而产生更流畑的动画效果。

提高性能:与使用 setInterval 或 setTimeout 相比,requestAnimationFrame 可以更高效地管理动画。它只在浏览器准备好进行下一次重绘时才会执行,避免了不必要的计算和重绘。

优化电池使用:在不可见的标签页或最小化的窗口中,requestAnimationFrame 会自动暂停,这可以节省 CPU 周期和电池寿命。

适应显示器刷新率:requestAnimationFrame 会自动适应显示器的刷新率。这意味着在 60Hz、120Hz 或其他刷新率的显示器上,动画都能保持流畑。

避免丢帧:由于与浏览器的渲染周期同步,使用 requestAnimationFrame 可以减少丢帧现象,特别是在高负荷情况下。

更精确的时间控制:requestAnimationFrame 提供了一个时间戳参数,允许更精确地控制动画的时间。

4、组件库方案

在当前成熟的前端生态中,想要获得精彩的视差动画效果,你可以通过现有的开源组件库来高效的完成开发。

以下是一些你可以尝试的主流组件库:

引用参考

MDN - background-attachment

MDN - transform-style

Pure CSS Parallax Websites

How to create parallax scrolling with CSS

视差滚动实践

移动端调试指南

2024-06-01 08:00:00

当移动端 web 项目部署在生产环境,项目部署在 Android/iOS 或其他移动设备下发生问题, 或想要模拟请求与拦截响应数据时,你会通过什么方式来进行处理与调试?

当项目是移动端的普通网页时,你可以通过 Chrome 浏览器自带的 chrome://inspect 功能,通过数据线连接真机设备以实现开发调试。

但当遇到需要模拟特定请求或响应内容、注入脚本、修改请求响应头的场景时,inspect 就难以应对了。又或者,你的项目作为 WebView 或 JsBridge 页面嵌入在其他的第三方应用内,在这几种场景下,你就可以使用代理抓包工具来完成更高级的调试操作。

常见的主流代理工具有:Fiddler、Charles,本文主要讲述利用Whistle进行调试操作,Whistle 基于 Node 实现跨平台,无需第三方安装包,更符合前端的操作习惯。

使用 chrome://inspect

设备开启 USB 调试

在访问调试工具之前,你需要对设备开放基础的调试配置,在 Android 和 iOS 下有不同的开启方式,你可以访问下面链接了解更多:

访问调试工具

在完成设备的配置后,请在浏览器地址栏中输入 chrome://inspect 访问开发者工具。

inspect page

dev-tools

使用 Whistle

官方介绍

whistle(读音[ˈwɪsəl],拼音[wēisǒu]) 基于 Node 实现的跨平台 web 调试代理工具,类似的工具有 Windows 平台上的 Fiddler,主要用于查看、修改 HTTP、HTTPS、Websocket 的请求、响应,也可以作为 HTTP 代理服务器使用,不同于 Fiddler 通过断点修改请求响应的方式,whistle 采用的是类似配置系统 hosts 的方式,一切操作都可以通过配置实现,支持域名、路径、正则表达式、通配符、通配路径等多种匹配方式,且可以通过 Node 模块扩展功能

安装&启动

步骤: 安装 Node > 安装 whistle > 启动 whistle > 配置代理 > 安装根证书

# 安装
npm install -g whistle

# 检查: 执行下方命令后如果正常输出whistle帮助信息, 代表安装成功
w2 help

# 启动: 运行后默认访问地址为 http://127.0.0.1:8899/
w2 start

更多命令见 官方文档

代理&证书配置

在使用工具前,请在移动设备上安装工具的 HTTPS 证书,以获取完整的调试能力。

当代理工具拦截 HTTPS 请求时,它会充当客户端和服务器之间的中间人(MITM),并生成一个伪造的证书来替换原有的服务器证书。如果客户端没有信任这个伪造的证书,就会出现证书错误。

qrcode

network

原理&流程

principle

常用规则

# 修改请求列表显示Style
style://color=@fff&fontStyle=italic&bgColor=red
# 修改UserAgent
ua://{ua}
# 修改状态码
statusCode://[statusCode]
# 修改请求头/响应头
reqHeaders://{value} resHeaders://{value}
# 修改请求内容
reqBody://{request.json}
# 修改响应内容
resBody://{response.json}
# 模拟延时
reqDelay://[delayTime]
# 处理跨域
resCors://*
# 修改Host配置
[originHost] [targetHost]

引用参考

Whistle