MoreRSS

site iconDnevend |小学后生修改

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

Inoreader Feedly Follow Feedbin Local Reader

Dnevend |小学后生的 RSS 预览

斯多葛打印机

2026-10-20 08:00:00

自动化流程示意图:09:00 由 launchd 拉起,抓取、翻译、生词、配图、打印、朗读,最后落到热敏打印机与 Telegram

书桌一角,近来多了一台旧热敏打印机。二手市场上捡来的,原本是给小饭馆打菜单、给超市打流水的命,如今被我派了个闲差:每天早上九点,自己醒来,吐出一页斯多葛。

纸上是一篇短文,英文在前,译文在后,挑几个生词,配一幅黑白小图。印完了,电脑再把英文念一遍,送进我的 Telegram。纸在桌上,声音在耳边,一日的功课就算开了头。

这台机器从头到尾,是我指挥 AI 搭起来的。写下来,一是记它怎样转起来;二是记中途那些坑——坑是 AI 一个个排查、一个个填平的,我只在几个岔路口拍了板。这条路走下来,恰好能说明白一件事:AI 替我们干了什么,又有什么是它替不了的。

从废纸到成品:

调试时打废的一摞纸 最终效果
调试时打废的一摞纸 最终效果

为什么要一台打印机​

两个念头,撞在了一处。

其一,我想让 AI 走出那方聊天框。这几年它像个坐堂的先生,你不叩门,它不开口。我想要倒过来:它到了时辰自己起身做事,把结果递到我手上,我连一个窗口都不必打开。这是工程意义上的变化——从「人调 AI」到「AI 定时跑」。

其二,是读英文。屏幕上的字,指头一划就过去了,轻得像没来过。纸不一样:能折,能画,能压在杯底,第二天还在原处等你。热敏打印机还特别省事——不吃墨,不认驱动,开一个 raw 端口(9100)就能直接吃字节,连 SDK 都不用装。

技术选型就是这么定的:输入是 Daily Stoic 每天一篇的短文,几百个词,正好够一张 80mm 小票的肚子;输出是一台走 ESC/POS 的局域网热敏打印机,把位图字节直接推到它的 9100 端口。

每天九点,它做什么​

一条流水线,七步,全由定时任务拉起:

  1. 抓取:解析 Daily Stoic 的 sitemap,取最新一篇的正文;
  2. 清洗:剔掉正文里夹带的推销段落;
  3. 翻译:走免费翻译接口,出中文;
  4. 生词:按词频表挑出生词,配词典释义和原文例句;
  5. 配图:找一张相宜的图,转成黑白点阵;
  6. 打印:排成小票,把 ESC/POS 字节流发往打印机;
  7. 朗读:edge-tts 合成英文语音,推到 Telegram。

这里有一条设计上的分界,值得单说:生成内容的每一步,都没用大模型。 翻译走免费接口,生词靠词频表加离线词典,配图去图库搜。大模型的力气,全花在写这套程序上;程序每天跑起来的时候,不再惊动它。

好处是实打实的:跑一次几乎零成本——没有 token,只有打印机吐出的半张纸;输出稳定——不担心哪天模型换了脾气,小票的格式跟着变脸。这算是我这段时间的一条心得:能走确定接口的,就别上大模型。大模型贵在「生成」和「判断」,不在「每天按点搬砖」。

节选一段原文:

Think about how rarely we give ourselves fully
to whatever we're doing. Working, some part of
us is thinking about the errands we need to run
later or the conversation we had the other day.

机器挑出的生词是 errands、inaccessible、sphere……每个词后面都跟着它在原文里的那一句。比背单词表有意思:你知道它是从哪篇文章里长出来的,而不是标本钉在纸板上。

我是怎么用 AI 把它做出来的​

我没有一行一行地写,更像一个包工头。但分工是有讲究的:

  • OpenClaw 管调度。 我把每件活写成一页简报:要什么,边界在哪,什么不许碰。
  • OpenCode 的 worker 干活。 一件任务一个独立工作区,写完交给另一家族的模型来挑刺,挑不出毛病才合并。请外人来审,是不让它自己给自己判卷。
  • 我定方向、验货、拍板。 换朗读音色时,让 AI 生成几段试听,我一段段听过,自己挑;真机联调,也是我在人与机器之间来回传话。

代码我确实没写几行,但「要什么」这句话,始终是我说的。AI 再快,也猜不到我要的是一张清晨九点的纸,而不是一个更漂亮的网页。手艺可以外包,心意外包不了。

遇到坑,AI 排查,我拍板​

下面这四个坑,没有一个是我自己查出来的。抓日志、读字节流、翻协议文档、试方案,全是 AI 干的。我做的事情只有一件:在岔路口说往哪走。这个分工我看得很重:AI 能把每个坑查到底,可哪个坑值得填、填成什么样,得由人来定。

一个字母的大小写。 封面图印出来是一长条乱码,中间还平白被切了一刀。AI 起初怀疑旧机器的固件,想先把配图关掉绕过去。这一步我没答应:配图是这张票的门面,可以晚两天,不能砍,一定要出图。AI 只好回头,把自己发出去的字节流逐段对照 ESC/POS 手册,最后查出是位图指令写错了:GS v 写成了 GS V。小写的 v 是「打印光栅位图」,大写的 V 是「切纸」。打印机何错之有?它一丝不苟地执行了一道错误的命令。

「No route to host」。 手动跑一切正常,一挂进定时任务就找不着打印机。AI 一层层往下排,定位到 macOS 的「本地网络」权限:前台进程能连局域网,launchd 拉起的后台进程会被拦在门外。它没去跟系统授权较劲,而是给程序加了一道兜底:连不上时退回系统自带的 nc,直接往 9100 端口发字节。这一处我没插手。绕开问题而不硬碰,本身也是一种工程判断。

源站断更。 AI 先发现官方接口停在 2023 年,分类页又是 JavaScript 渲染的,于是改为解析 sitemap。后来源站几天不更新,打印机一连三天印了同一篇,它不懂重复,只懂守时。怎么处理是我定的:同一篇不重印;断了更,提醒我一声,别闷着头印。这不是技术问题,是「我想要一台什么样的机器」的问题。

难听的嗓子。 macOS 自带的 say 念起来字字铿锵,像地铁报站。AI 换成 edge-tts,一口气生成几段不同音色的试听。我一段段听过,挑了一个顺耳的女声,又让它把语速放慢一些。好不好听,是 AI 替不了的判断。

回头看,这四件事里技术含量最高的部分都是 AI 完成的,我做的只是几次选择。但正是这几次选择,决定了最后桌上这张纸的样子。工具越强,执行越不稀缺;稀缺的是审美,是判断,是站得高一点看清「要什么」的那个视角。AI 能一路钻到字节层面,人得站在整件事的上头。

几天之后​

如今每天早上九点,它准时把一张纸递到我面前,从不迟到,也从不抱怨——这两样美德,它比我有。

机翻的成色,老实说一般,偶尔还错得离谱。讲戴高乐的那篇里有一句,原意是「每个角色在它自己的位置上都是第一位的」,机器译成「每个人都是自己领域的第一人」,意思跑出去老远。可恰恰是这种错,逼我停下来对着原文,把那一句翻来覆去嚼了几遍。译文若是天衣无缝,我大概扫一眼就放过去了。

我从没指望它给我多漂亮的译文。我要的,是它每天把我按在椅子上:读一段英文,查两个词,想一分钟。聊天框做不到的事,一张纸做到了。

还有一层,是后来才想到的。热敏纸不耐久,搁上几年,字迹便一点点淡下去,最后只剩一张白纸。斯多葛派常说 memento mori——记住你终有一死。这台打印机每天印出来的,原就是一页会消失的字。正因为会消失,那天早上读它的那一分钟,才显得要紧。

项目开源,想自己攒一台的,请便:github.com/Dnevend/daily-stoic-printer。

提示

本文由Claude代为捉刀,我负责提供结构和文笔以及修订。

多么痛的领悟

2025-11-08 08:00:00

总结一下那些在二级市场不断受到的教训,现在学费交的差不多了,准备夺回属于我的一切。

先列个大纲,后续展开:

1、交易执行前不对市场环境和外部因素做判断,在乐观向上的趋势中,也存在着突然的回调,未曾考虑现在所处阶段在长(短)周期内发展到了哪一步,以及接下来可能的发展方向。

2、缺少负责的态度,有时觉得仓位不大,便任由其涨跌。这是种懒惰和逃避的行为,如果不对每一次的操作报以百分的负责,是不能带来经验和成长,如此以往便会产生麻痹的心理,缺少了对市场的敏感性,在错误的行为中反复,结果是面临被淘汰和出局。

3、凡可能发生的情况,必定会发生。

曾在一段时间内跟单交易,看到带单的收益曲线十分稳健,便投入大仓位资金。以为这样就能高枕无忧,等待着醒来时的收获。孰不知这样的行为是将自己处于被动之中,即使是经验老道的带单者也许要面对市场的雷霆变换。结果是带单者在面对剧烈的波动时也慌乱了手脚,在无情的市场博弈之中任他再成熟的操作也已无力回天。

4、未设置止盈止损,这种情况的本质是对市场缺乏判断,并且缺少每一次交易执行前的目标,任由情绪主导,不知何时收手。

5、仓位控制不合理,开单仓位的多少全凭手感,如遇兴头正旺时便无视仓位体量,随手一划拉便觉自己马上就能奔上小康。幻想的同时必须要面对野心背后隐藏的风险,这种大仓位造成的影响,会让自己懂得什么是粒粒皆辛苦。实际带来更严重的情况是对信心的打击,接着是在这被打击的心态之中,进行着叠加的谨慎的亡羊补牢式的弥补。

6、错误的交易标的,未认识到主流与山寨的区别。

延伸点​

  • 统计学引力

为革命保护“视”力

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