普通视图

发现新文章,点击刷新页面。
昨天以前星辰日记

临时邮箱:开发者的隐私工具实践笔记

作者 xcsoft
2025年6月21日 10:00

上周,我正在为一个新的开源项目收集各类API文档和开发工具时,遇到了一个令人烦恼的问题:几乎每个开发资源网站都要求我注册并验证邮箱。在此之后,我的邮箱已经开始收到营销邮件以及各种营销通知。

那一刻,我意识到自己需要一个临时邮箱。

困境与解决方案

作为一名开发者,我经常需要注册各种服务:测试新的API、下载SDK文档、注册测试账号验证自己应用的注册流程…每次都使用真实邮箱,意味着你的收件箱将永远被各种营销邮件和通知填满。

朋友曾开玩笑说:”你的主邮箱已经不是工作工具,而是各大平台的营销战场了。” 这句话虽然夸张,但却戳中了痛点。

就在我为了测试一个新开发的注册流程需要创建20个测试账号时,临时邮箱成了我的救星。

临时邮箱

临时邮箱(也称一次性邮箱或匿名邮箱)是我现在开发工作中不可或缺的工具。它无需注册,打开网站就能即时获得一个可用邮箱地址,用完即丢,简单高效。

我第一次使用临时邮箱时惊讶于它的简洁性 —— 没有复杂的界面,没有强制注册,只有一个随机生成的邮箱地址和一个接收邮件的界面。作为崇尚简洁设计的开发者,这正合我意。

我的实际应用场景

在日常开发工作中,我发现临时邮箱在以下场景特别有用:

  1. 批量测试注册流程:当我需要验证应用的注册逻辑时,创建10个不同测试账号变得异常轻松。再也不用担心”邮箱已被注册”的提示了!

  2. API服务对比测试:上个月,我需要对比三家不同的支付API服务。每家都需要注册开发者账号,使用临时邮箱让我能快速注册并获取测试凭证,而不必担心后续的营销邮件轰炸。

  3. 下载开发资源:很多SDK、教程和开发工具都藏在”免费下载”的邮件验证后面。使用临时邮箱,我能快速获取资源而不必担心隐私泄露。

  4. 临时团队协作:有次与外部开发者短期合作,我们使用临时邮箱注册了一个共享的项目管理平台,项目结束后自然”过期”,不留痕迹。

使用心得与建议

经过几个月的实践,我总结了一些使用临时邮箱的经验:

优点体验

  • 开发效率提升:测试各种服务时再也不用担心邮箱问题,专注于代码和功能。
  • 隐私得到保护:我的主邮箱垃圾邮件明显减少,重要邮件不再被淹没。
  • 使用无负担:不需要记住额外的账号密码,打开网站即用。

需要注意的限制

  • GitHub、AWS等重要开发平台会识别并拒绝临时邮箱域名,这很合理,毕竟这些平台需要可靠的身份验证。
  • 有些临时邮箱服务稳定性不佳,我曾在等待一个重要的API密钥邮件时遇到服务中断。

一些推荐

目前我在使用这些临时邮箱服务,是我认为比较稳定和易用的:

写在最后

作为注重效率和隐私的开发者,临时邮箱已经成为我工具箱中的常用工具。它不仅保护了我的主邮箱和个人信息,也简化了测试流程,让我能更专注于代码本身。

当然,它不适合注册重要的开发账号或长期项目协作平台。对于GitHub、AWS、Azure等核心开发平台,我依然使用专业的开发者邮箱。临时邮箱是辅助工具,而非替代品。

如果你也经常需要测试注册流程或频繁注册各类开发资源网站,不妨试试临时邮箱,它可能会为你节省大量时间,并让你的主邮箱恢复清爽。

为 QuMagie 备份的照片添加 Exif 信息

作者 xcsoft
2024年11月5日 10:11

前言

一直想入手一台成品的 NAS。在此之前, 我一直用的是一台蜗牛星际的黑群晖 配了 4块各种型号的 500G 二手硬盘, 两两组 RAID1, 连续运行快4年了, 也遇到了停电几次,别说, 矿机就是耐造, 依旧稳得一批。

正好遇到双十一, 看到威联通的 TS-564 这款机型, 配上 4T * 3 只要 3.7k, 还是希捷酷狼。于是便入手了, 还顺带整了台 APC 的 UPS。对了, 顺带还入了快 1T 的希捷酷鱼 SSD 作为系统盘, 来避免 HDD 的频繁读写。 总计 4.5k 吧

别问我为什么不买 464C, 因为这个四盘位的外观已经烂大街了, 个人感觉有亿点丑了。

u1s1, 威联通的系统是真的比不上 群晖的 DSM (众所周知 群晖是买系统送 NAS), 动不动就 加载中… 和老年机一样。

后期再使用一段时间,应该还会再出一个具体的评测。

QuMagie

QuMagie 是 QNAP 出品的一个类似AI相册的平台, 和群晖的 photos 差不多, 也支持自动分类, 人脸识别等功能。

但是这个 QuMagie 的时间线是真的不大行, 在手机客户端备份照片时, 部分截图等因为没有 exif 信息, 会全部使用备份时间作为拍摄时间, 导致时间线混乱。

添加 Exif 信息

在网上找了一圈, 也没有找到一个较为完美的解决方案, 于是花了点时间, 写了一个脚本, 用于为 QuMagie 备份的照片添加 Exif 信息。

可以在 Github 中找到, 原理就是读取文件名中的时间信息, 然后写入到 exif 的 DateTime 和 DateTimeOriginal 中。

使用方法

  1. 在控制台中启用 SSH
    启用ssh

  2. 使用 SSH 连接到 NAS, 用户名和密码同你的管理账号密码

  3. Release 下载最新的 quexif, 对于 qnap 的 intel 机型, 通常为 quexif-linux-amd64
    下载

  4. 通过 File Station 或 SFTP / SMB 等方式将 quexif 上传到 NAS
    上传至NAS

  5. 在 ssh 中, 运行 chmod +x quexif 使其可执行

  6. 运行 ./quexif -p /path/to/your/photo/folder 即可
    运行

提示

目前只支持 jpg (jpeg) / png 格式的照片, 其他格式的照片会被忽略。

Riverpod - flutter 状态管理的应用

作者 xcsoft
2024年7月19日 05:24

前言

Riverpod 是 Flutter 下知名度较高的状态管理依赖,同样出自 Provider 的开发者 rrousselGit 之手。

其实仔细去看 Riverpod 似乎只是 Provider 的拼写打乱了顺序,其提供了更简洁的API 设计,实现了依赖注入。

如果去看过 rrousselGit 的主页,你可以发现,他也是著名的 Flutter_hooks 的作者,RiverPod 也理所当然的拥有 hook 相关的血统 > HookConsumerWidget, 我们可以在享受 hooks 的同时,直接使用Widget.ref(provider).watch 来监听变更并自动刷新页面。

为什么 Flutter 需要状态管理

Flutter 作为优秀的跨端框架,其使用的声明式UI有诸多优势,但嵌套的组件给数据传递带来了极大的挑战。

如果将数据在 组件类的构造函数中携带,并在数层中进行传递,随着代码量的提升,将会极大的增加代码的复杂和易理解程度。

因此状态管理组件出现了,其提供了一个清晰的模型来管理数据流,确保数据在正确的时机以正确的方式流动。这有助于避免数据不一致和难以追踪的 bug。通过集中的状态管理,我们可以更加容易的理解和增删需要传递的数据。

举个例子

我们可以使用最常见的 Flutter demo 来看, 在初始化完成项目之后,我们便可以看到这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

因为 需要渲染的页面和按钮在 同一个组件 MyHomePage 下 因此我们可以很简单的在按钮点击的同时 setState 来使Flutter 感知数据的变化 并重新渲染页面。

组件分离

但是 多数情况下, 我们需要渲染的页面,和改变数据的按钮 并不在一个组件中,例如,如果我们将这个按钮单独封装在一个类中。这种情况下,我们应该如何在点击按钮的时候增加数据呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${_counter._count}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: const Button(),
);
}
}

class Button extends StatelessWidget {
const Button({super.key});

@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: _counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
}

使用 ChangeNotify

ChangeNotify 是 Flutter 下用于监听数据变更的组件,在这个场景下,我们可以使用其监听 counter 的变化, 并重新渲染页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}

Counter _counter = Counter();
......

class _MyHomePageState extends State<MyHomePage> {

@override
void dispose() {
_counter.dispose();
super.dispose();
}

@override
void initState() {
super.initState();
_counter.addListener(() {
setState(() {});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${_counter._count}',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: const Button(),
);
}
}
......

使用 RiverPod

再简单场景下,ChangeNotifyValueNotify 已经能够胜任, 但对于复杂场景,还是有一定的局限性。

先添加如下依赖 (这里使用 Flutter_hooks 举例)

1
2
3
4
5
6
7
8
dependencies:
flutter_hooks: ^0.20.5
hooks_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5

dev_dependencies:
build_runner: ^2.4.11
riverpod_generator: ^2.4.0

别忘记在最外围增加一个 ProvideScope

1
2
3
void main() {
runApp(const ProviderScope(child: MyApp()));
}

接着新增一个 counter_provider.dart 文件

1
2
3
4
5
6
7
8
9
10
11
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_provider.g.dart';

@Riverpod(keepAlive: true)
class Counter extends _$Counter {
@override
int build() => 0;
void increment() {
state++;
}
}

运行 代码生成

1
$ dart run build_runner build

他将会生成一个 counter_provider.g.dart 文件

1
2
3
4
// 接着我们可以使用 `ref.watch` 来监听数据的变化
ref.watch(counterProvider);
// 使用 ref.read().increment() 增加1
ref.read(counterProvider.notifier).increment;

完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import 'package:daka/counter_provider.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

void main() {
runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends HookConsumerWidget {
const MyHomePage({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
ref.watch(counterProvider).toString(),
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: const Button(),
);
}
}

class Button extends HookConsumerWidget {
const Button({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {

final counter = ref.read(counterProvider.notifier);

return FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
}

大疆 Mini 4K 初体验

作者 xcsoft
2024年6月7日 13:35

大疆 最近发布了一个 特别便宜的 mini 系列无人机, 新品+618 直接给单电的价格干到了 1499, 于是 果断下单, 在附近尝试了一下。

可惜旁边是个机场,只能限高 120, 不过从第三视角看还是很不错的,虽然宣称是 4k, 但放在电脑上有点勉勉强强,不过对于这个价格来说,已经很不错了。

先简单的看一下拍摄效果吧 (图片非原图)

历经59天, 终于拿到软著

作者 xcsoft
2023年2月25日 17:02

目前的话, 大部分的国内安卓应用商店如果需要上架软件,都需要提供软著。App Store 的话是不需要的,但是需要每年99美元的 “苹果税”。

所以需要上架自己开发的软件就需要去申请软著,这个价格的话是免费的,大概需要3个月左右。如果着急的话也可以去找一些代理,不过需要一定的费用。还是比较建议大家去申请下软著的,如果遇到别人恶意使用或模仿软件的时候,软著是一个很好的保护手段。

我是直接自己去 中国版权保护中心 自行申请的,第一次由于未成年,给驳回了,给出的理由是要求成年人代理申请,后来也就没有去管了。

然后在2022年12月7日,完善了下使用文档再次进行了申请. 申请时需要提供 申请表(官网可下载),身份证复印件,程序源代码(60页)以及软件详细的使用说明。 这里的程序源代码时需要打印下来,( ps: 建议自己打印,外面打印不仅贵,而且还有可能泄漏源代码) 接着需要将这些资料寄送至北京的版权保护中心。

从邮寄到审查大概经过了 20天,也就是 12月27日。最终在 2023年2月2日 审核通过了,状态变为待发放,此时软著证书已经可以下载电子稿了。

申请下来比想象中的要容易些,就是比较浪费时间,不过好在准备的比较充分,没有遇到像别人需要补正的情况。

目前(2月28日)状态依旧处于待发放状态,不知道什么时候才能拿到纸质的软著证书呢。(等拿到了再更新下)

最后配上一张软著证书图片:

拾光
拾光

你好 2024

作者 xcsoft
2024年1月27日 22:10

前言

和, 往年一样,又到了年度总结的时候了。似乎好像已经很久没有更新过博客了。

今年陆陆续续 写过一些东西,也放弃过很多东西。

其实我还是很喜欢大一上学期的住宿生活,六个人的寝室,住着五个人。随说因为疫情,我们总是过着两点一线的生活,穿梭在教学楼与宿舍之间。

还记得那时候,接触到了森林这款游戏,还想念着五个人追着野人跑的日子。也许我是个特别讨厌 名利的人,我不喜欢得到太多的关注,大学是一个小社会,其中也充斥着太多的利益关系。

其实很多人就像是那森林中的野人,喜欢着对自己有利的东西,不断的追逐着,却忘记了自己的初心。通俗的讲,谁对他好,他就把他捧上天。在我遇到的人中,总感觉充斥着太多的虚伪,等级的划分。

人心是最难以琢磨的,请记住,没有永远的朋友,只有永远的利益。

有些东西,我不想讲的太细,我不太喜欢这样的环境,也许我喜欢简单些的生活,不用去想太多,不用去计较太多。

一些奖项

字节跳动 2023 青训营 优秀营员

年初,我参加了字节跳动的 青训营,有幸学习了字节关于 Go 语言相关的知识,也和队友一起完成了一个简易的抖音。第一次接触 微服务,感受到了微服务的魅力,也学习到了很多东西。

我使用了 Go-Zero 这款框架,但是对于日志收集,链路追踪,监控等等,我还没有详细的使用,对于数据库分库分表只有一些简单的了解,还没有实际的使用过。

优秀寝室

其实能获得这个奖项,还挺意外的 :>

看看我们的寝室:

去看了 Quinn 的演唱会!

一直很喜欢 Quinn 的歌曲,他的歌曲真的很有感染力。

节选自 Quinn 的专辑《理想》- 《樂色》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
爱你的在自我感动幻想中遨游

恨你的想诋毁不管理由多荒谬

你望着苍穹,心不自觉惶恐

你的生命也许从来不在你掌控

而那匿名的恐吓,你提防的红色

把你击落下马之后居然哭泣得痛彻

然后安慰你,却也没忘邀功颂德

先把一切重设,还想和你同乐

第一份兼职

我的第一份兼职 是 <瑞幸咖啡>

一开始是 16/h,在考取咖啡师资质后,涨到了 21/h

第一天去 就是贴贴杯子,按按糖浆什么的,但是站着那么久,真的会脚疼。 要背很多很多配方,别说了,头会爆炸 …

瑞幸真的很严格,每小时都需要洗手,碰到鼻子,脸,还得去洗。真的,很讨厌瑞纳冰!每次做完要洗好久!(少点少点这个)

终于拿到了 拾光的软著

ps: 审核真的很慢, 等了我块 6个月

准备参加 集成电路应用与设计大赛

这是我第一次参加这样的比赛,也是第一次接触到了 FPGA, Verilog, 华大九天等等。

现在我的脑子里都是 LM324, CD4511, 运放, ADC .. 爆炸

我主要负责的是 使用 lk8810s 测试机 测试芯片的 IOH,VOH, 开短路 等等数据。 还有华大九天的 模拟电路设计。

今年(2024) 3月份的比赛,其实还是挺紧张的。

2024

2024年,希望自己能有所改变,能有所进步。不在局限于过去的那个小空间,不在局限于过去的那个自己。

HHKB 使用体验

作者 xcsoft
2023年3月11日 11:30

应该是去年双11吧,入手了第一款属于自己的 HHKB, 我选择的是 HHKB Professional HYBRID Type-S (无刻)

fall

很早就听说 HHKB 了,不过因为价格等因素,一直都没有入手,双11应该是便宜了一点吧,还是狠狠心入手了。经过几个月的使用,也来谈谈我的使用体验吧。

作为一款 Happy Hacking Kyeborad, 我个人感觉吧,手感还挺好,噪音大小也在可接受范围内。在此之前对于长轴键盘也只使用过 Keychron 的 K3 (矮茶轴) 对比起来,HHKB 键程稍微长一点,同时的话键盘的高度也要高许多,还是建议搭配掌托使用的。

keyboard

噪音方面,由于怕被舍友撵出去,还是选择了 Type-S (静音版),综合下来,还可以接受吧。比我原来的矮茶轴可小多了。

连接方面,HYBRID 版本支持双模,即 蓝牙和有线。蓝牙支持三设备切换,可以通过 Fn + Control + 1/2/3 快捷切换连接设备,实际体验下来,设备切换在 1s 内,这点做的还是比其他厂商要好很多。虽然其还使用 蓝牙4.2,但是并没有出现过断连(原来的 Keychron 日常断连)

HHKB 的按键与普通键盘有着较大的区别,它将 Control 移动到了 Shift 上方,将 TabCapsLock 合并,使用 Fn + Tab 触发大写锁定, 它取消了普通的 方向键,取而代之的是通过 Fn组合来进行操作。对于新入手HHKB的用户还是建议先去购入 有刻的版本,不然真的会需要很长时间来适应。

缺点方面,贵,然后就是对于无刻的版本,数字总是按错。电池仓的设计,也总感觉不太协调。

渺软公益CDN - 又一个静态资源加速站

作者 xcsoft
2023年1月11日 18:17

渺软公益 CDN - 又一个前端静态资源加速站。
目前支持 jsdeivr, unpkg, cdnjs 的加速

项目官网

项目介绍

记得以前有篇文章介绍了 Jsdelivr 加速 Github Repo, 来实现静态资源加速,不过目前,因为一些原因,使用体验也下降了很多。

在前端开发者日常开发中,总会去使用很多现成的框架或前端UI,如 我用的比较多的: React.JsAntDesign 等。

如果将这些静态资源都存放在自己服务器上,不论对用户还是开发者来说,都会带来较差的体验. 但目前来说,国内的 Jsdelivr 和 Unpkg 的体验并不好。

在这些情况下 渺软公益CDN 便诞生了. 它使用 DogeCloud 对这些站点进行反向代理,同时也拥有完善的 Abuse 措施. 用户可在官网对违规资源进行举报,审核通过后将会对其封禁。

限制

本项目的开发初衷是为了方便开发者使用公共静态资源如js,css等文件,不支持托管私人静态图片, 私人字体文件等大流量文件(公共字体,知名前端UI库除外)。

目前限制的话是 24G / Day,还是比较充足的. 我们也在官网为其开发了一个简易图表来展示实时的使用情况,具体可在官网查看。

你好 2023

作者 xcsoft
2023年1月3日 23:40

前言

时间过得很快,转眼 2022 年就离我们远去了。

2022 年,发生了挺多事。很多细节也就不一一叙述了。

  • 这一年,搬家了,新的家小了些,但比原来“精致“了些。
  • 这一年,步入大学了,低于预期,但也不错。
  • 这一年 …

日常

今年的生活其实挺没意思的。

就很像网上说的那样,今年似乎就是 1~11月常态化的核酸与时不时的隔离 以及 12月的阳了

上半年在家,几乎接近中午才起床,吃点东西,下午呆在家中或是到门口的星巴克一坐一下午。

年初了解到了 光·遇,“光是遇到,就很美好”,画风很唯美的一款游戏。从中也结识到一些朋友,似乎玩这个游戏的有很多年纪很小的朋友。也有一个人给我留下了很深的印象。但是他也很久没上线了,不知道还能不能遇到他。

我们每天都可能遇到不同的人,其中很多都是过客,很少有人会与你的人生产生交集。我算是个比较念旧的人,经常回忆过去。似乎过去,还有着太多的遗憾。

每天都在重复,重复,重复 … 直到六月份,高考结束,当然,这与我没有太大关系。原先的一些朋友放假了,他们大多考的不错。也去参加了他们的升学宴。

我们学校开学很晚,别的学校似乎9月初就开学了。唯独我们一直延迟到10月9号。为了早点体验大学生活,我参加了学校的志愿者,提前一天到校。等到第二天跑到当地的车站去迎接新生。可能是疫情原因吧,我们这届连开学典礼都没有举办。

草草上了近一个月的课,才开始军训,这期间还因为疫情原因在宿舍上了一段时间的网课。军训不算太累,但也有些许遗憾。我是在数字方队,原先排练几天的汇演,却在汇报当天因下雨而取消了。

12月初,也去自愿参加了无偿献血,算是挺有意义的一件事吧。

风景

平常也挺喜欢摄影,周末有空也会经常去海边,公园转转。照片似乎是留下记忆的最好的方式。每当没事做时,都会翻一翻过去的照片,想一想曾经发生的事。

7月份去了趟安吉,不同于我们这儿,安吉似乎是山中的城市。感觉住在这儿,生活节奏也慢下许多。去了那儿的浙北大峡谷,也体验了将军关漂流。

反思

时间过的很快,2022年也是我成年的一年。小时候总感觉时间很慢总幻想着长大后的日子,长大了却总想回到过去,小时候是多么的无忧无虑。

今年空闲时间很多,却没有多读上几本书,多做些有意义的事情。天天呆在家/宿舍,也没多出去运动运动,也是该多锻炼锻炼了。

希望2023年的自己,能多些进步,多些成长。

记一次 RAID 硬盘离线

作者 xcsoft
2022年12月26日 18:19

事故

使用 Dedipath 的服务器应该已经超过三年了吧。价格便宜,客服服务也挺不错的。但是,这次的事情,让我对其服务的可靠性及服务态度产生了怀疑。

事情应该是发生在今年的12月1日,收到Uptime-Kuma发来的邮件, 说我的 OpenID 的后端似乎离线了。

检查了一下,发现其所在的服务器 文件系统变成了 Readonly Filesystem . 无奈,只能发工单联系其客服,客服给出的回复是:

We are currently trying to run maintenance on the host server for this node. We will contact you once we have an update. Sorry for the delay. and thank you for your patience.

简单来说,就是我们正在对宿主机所在的节点进行回复。但其并没有任何的预先通知,再次询问过后,客服给出了如下的回复:

VPS’ on Node Los Angeles OpenVZ Node DC04R11SRV36
We discovered a disk in this node which fell out of the RAID-10 array, but was marked as in good health. As such, we re-added the disk to the array, but for unknown reasons, we immediately begun to see file system errors.
As such, we are requesting that all customers on this node login to SSH (we have tested SSH on various VMs and it does respond) and create a backup of any important data. Once complete, please open a technical support ticket and request that we re-create your VPS on a new node.
Our sincere apologies for the troubles caused. We are not sure why the behaviour that occured has occured, but an examination into our processes will be undertaken to try and prevent such issues from re-occuring.

简单来说,就是因为 RAID-10 阵列的一块硬盘离线, 但其 SMART 数据显示状态正常。将其重新加入到阵列中,但是出现了文件系统错误。

什么是 RAID

可能有的人不懂什么是 RAID, 这里简单的介绍一下.

RAID, 即磁盘阵列。通常由多块硬盘组成,通过硬件或者软件的方式,将多块硬盘组合成一个逻辑的磁盘,以提高数据的可靠性和可用性。

此处使用的 RAID-10 至少需要4快硬盘。可以理解为 RAID10 是: 2快硬盘组成 RAID1,再由2组 RAID-1 组成 RAID-0。

通常来说, 这似乎能够允许最少两块硬盘的损坏,不会影响数据的可靠性和可用性。

给出的解决方案

Dedipath 给出的解决方案是: 给我在一个新的 node 上重新开一台 VPS。

在我询问对于我原先的数据如何迁移,以及这期间造成的损失时,客服表示,他们没有 SLA 赔偿协议。

It is your responsibility to maintain frequent backups of your data. We are not liable for any data loss.

挺离谱的,但还好对于数据库有定期备份,所以并没有造成太大的损失。

反思

不论使用的是一些不知名的服务商,还是一些大厂如腾讯云,阿里云。对于服务器,还是建议对其进行定期备份。

DB-Backuper

对于数据库的备份,花了点时间,通过 Golang 实现了一个简单的数据库定时备份工具: DB-Backuper

目前仅支持 Mysql, 以及自动备份至腾讯云的对象存储。

大一 碎语

作者 xcsoft
2022年12月13日 15:14

好像已经挺久没有写文章了,上次似乎还在8月份。

大一上半学期很短。10月8日开学去车站做志愿者,隔离三天。10月12日到自己宿舍,上床下桌,独立卫浴,还算不错。虽说不算什么好学校,但命运已定,也没有什么其他办法,唯有接受。

我选择的是现代移动通信技术(通信工程),主要是移动方面的学习。大一上半学期,主要专业课也就是,移动通信概览,讲的也挺模糊,然后就是电路基础,挺有趣也挺复杂的。

作为一个从来没有住宿经历的人来说,大学住宿生活其实还算不错。晚上和舍友玩到12点多,早上7:50的课,7:30起来匆匆忙忙赶过去。星期一星期三晚上上到8:30的高数,嗯… 还算不错。比走读生要多些乐趣,多些时间。

周六周日也会去去图书馆,补补上课摸鱼不太理解的内容。图书馆人人挺多,用书占位置的也挺多。三层的图书馆,很多书籍仍然是10年代甚至90年代的,可能文学方面的书籍仍然存在价值,但一些技术方面,天天都在迭代,大多数书籍已没有意义。

在这之间,也有过部分项目,仍然是对不蒜子的略微修改,以及新写了某学校的自动打卡。主要技术栈仍然是 Golang,php 到挺久没写了。Golang 算是一个不错的语言,写起来很优雅,很方便,跨平台,支持交叉编译,平常在Macbook上写的软件,编译后可以直接在 Linux 上运行。作为一个编译性语言,源码安全性方面也有一定保障。前端的话,依旧是 NextJs + Typescript, Typescript 作为 Javascript的超集,很不错。React 的渐进式开发和 Flutter 有些类似,我个人认为吧,React 是优于 Vue 的。不太喜欢 Vue.js.

”放假“很匆忙,因为疫情原因,本来计划是月底放假,突然通知能离校的尽快离校。于是用了半天的时间稍做收拾,第二天便匆匆离校了。两个月的时间,体验了下大学生活,嗯… 很匆忙,和想象中的大学生活有些出入,但还算不错。

疫情相关,也似乎有了些改变,不做太多评价。青春几年,疫情占了三年。很烦,但没太多办法。希望明年会有些好转吧。希望明年的自己,能有个新的开始。未来是不可预测的,少些期待,多些脚踏实地,订些可行的近期目标,以及一些可行的长期目标。一步一步走下去。

未来可期,前程似锦。

Waline 邮件异步推送

作者 xcsoft
2022年7月11日 17:34

前言

目前我使用的评论是 Waline,总体来说 体验还算可以。但是在使用过程中也遇到过一些问题,比如加上邮件推送后, 评论的速度会变的很慢。

研究过后发现 Waline 貌似是在评论时 直接进行发送邮件的,同步进行发信便会导致评论耗时较长,很影响用户体验,有时评论需要耗时几秒甚至超时。

Waline 应该是使用的 ThinkJs,理论上可以通过异步函数来进行异步发信,不清楚为什么要同步发信。由于我对于 ThinkJs 并没有太深入了解,直接修改源码也可能造成部署麻烦,于是我尝试寻找另外的方式来实现异步发信,在研究过后, 发现 Waline 可以设置一个 Webhook 地址,在评论时会自动向该地址发送一个 POST 请求。

其 POST 内容大致为:

评论文章时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"type": "new_comment",
"data": {
"comment": {
"link": "https://blog.example.com",
"mail": "email@example.com",
"nick": "xcsoft",
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.115 Safari/537.36",
"url": "/",
"comment": "test",
"ip": "127.0.0.1",
"insertedAt": "2022-07-09T13:46:52.085Z",
"status": "approved",
"objectId": 1,
"rawComment": "test"
}
}
}

回复别人评论时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"type": "new_comment",
"data": {
"comment": {
"link": "https://blog.example.com",
"mail": "email@example.com",
"nick": "xcsoft",
"pid": 1,
"rid": 1,
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.115 Safari/537.36",
"url": "/",
"comment": "[@xcsoft](#1): reply",
"ip": "127.0.0.1",
"insertedAt": "2022-07-09T13:47:53.526Z",
"user_id": 1,
"status": "approved",
"objectId": 2,
"rawComment": "reply"
},
"reply": {
"user_id": null,
"comment": "test",
"insertedAt": "2022-07-09 21:46:52",
"ip": "127.0.0.1",
"link": "https://blog.example.com",
"mail": "email@example.com",
"nick": "xcsoft",
"rid": null,
"pid": null,
"sticky": null,
"status": "approved",
"like": null,
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.115 Safari/537.36",
"url": "/",
"createdAt": "datetime('now', 'localtime')",
"updatedAt": "datetime('now', 'localtime')",
"objectId": 1
}
}
}

实现

了解了 Webhook 内容后, 便打算使用 golang-gin 开发服务端, 得益于 Golang 的协程 异步发信还是很方便的。

我这里直接通过 gin 提供的 ShouldBindJSON来解析 Waline 发送的 json, 其结构体类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type CommentStruct struct {
Type string `json:"type"`
Data struct {
Comment struct {
Nick string `json:"nick"`
Mail string `json:"mail"`
Url string `json:"url"`
Comment string `json:"comment"`
RawComment string `json:"rawComment"`
Ip string `json:"ip"`
InsertedAt string `json:"insertedAt"`
Status string `json:"status"`
} `json:"comment"`
Reply struct {
Nick string `json:"nick"`
Mail string `json:"mail"`
Comment string `json:"comment"`
InsertedAt string `json:"insertedAt"`
Status string `json:"status"`
} `json:"reply"`
} `json:"data"`
}

其中 Type 始终为 new_comment, Data.Comment 为评论信息, Data.Reply 为被回复信息(可能为空)

邮件采用的是 gomail 库, 由于大多数服务商默认是有发送频率限制的, 于是我通过 Redis 实现了一个消息队列, 用来控制发信频率。

发信效果

reply

模版及图片 来源于网络

开源

源码以 Apache 2.0 协议开源于 soxft/waline-async-mail

基本部署方式可以在 wiki 找到

Centos 编译安装 php81

作者 xcsoft
2022年6月16日 14:45

出于 开发环境配置及学习的需求, 需要直接在 Centos 7 上编译安装 php-81.

最近在使用 webman 重构 拾光 的后端, 虽然 这次迭代可能存在一定的破坏性, 但得益于 Webman 的现有生态 以及 复用了多数的 Composer 库. 将会为 以后的版本迭代及功能扩展 带来一定的便利.

我们可以直接在 官网 找到源码, 这里选择 php-8.1.7.tar.gz, 通过 wget 等工具 下载至服务器.

相关依赖

编译之前, 需要安装一些依赖.

1
$ yum install libxml2-devel openssl-devel sqlite-devel libcurl-devel libicu-devel gcc-c++ oniguruma oniguruma-devel libxslt-devel libpng-devel libjpeg-devel freetype-devel libsodium libsodium-devel epel-release -y

接着通过tar -xzvf php-8.1.7.tar.gz 解压下载后的文件. 进入文件夹

开始编译

我们可以使用 cconfigure 来检验当前的系统环境, 看是否满足安装软件所必需的条件:

1
$ ./configure --prefix=/root/php/81 --with-config-file-path=/root/php/81/etc --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-iconv-dir --with-freetype --with-jpeg --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-intl --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --enable-soap --with-gettext --disable-fileinfo --enable-opcache --with-sodium=/usr/local/libsodium

如果此处 出现错误, 可以尝试根据提示 安装相关依赖

这里 我们已经选择安装了一些 常用的php扩展, 您也可以在 ext 目录下 找到更多信息.

在校验完成后 我们会看到 类似 Thank you for using PHP. 的相关信息

接着 使用 make && make install 将其 编译安装至 /root/php/81 目录下, 安装完成后, 我们需要将 当前目录下的 php.ini-development 或 php.ini-production 文件 复制到 /root/php/81/etc/ 目录下, 并重命名为 php.ini

接着添加 PATH="/root/php/81/bin:$PATH"~/.bashrc 中 来将 php 添加至环境变量中, 重新打开终端 或使用 source ~/.bashrc 使环境变量生效

验证安装

1
2
3
4
5
6
$ php -v

PHP 8.1.7 (cli) (built: Jun 16 2022 14:52:05) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.7, Copyright (c) Zend Technologies

其他

对于 Centos 8 后, 使用 dnf 替代了 yum, 需要启用 PowerTool 来安装部分 开发依赖

1
2
$ sudo yum install dnf-plugins-core
$ sudo dnf config-manager --set-enabled powertools

Redis 统计实时在线人数

作者 xcsoft
2022年6月6日 14:14

zset

Redis 中的 sorted set (有序集合) 也称为 zset. 它提供了两个参数, 一个为 score, 一个为 member.

其中, score 为排序的分数, 它可以是双精度或者整数. 其结构类似

1
2
3
        | (score) member1
key => | (score) member2
| (score) member3

如何实现

我们可以简单的通过 zadd 方法向一个有序集合内添加成员.

为了方便演示, 在这里使用 Interval 的方式, 定时请求指定接口. 将 score 设置为请求时的 unix时间戳

zset 提供了一个 zcount 方法, 可以让我们快速获取指定 socre 范围内的成员数量.

我们假设用户5秒钟内无操作, 即掉线. 这样便可以通过 zcount 获取实时在线人数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
// 使用 gin 提供 web 服务
r := gin.Default()

r.GET("/", func(c *gin.Context) {
_redis := redisutil.R.Get() // redis pool
defer redis.Close()

// 用户标识
userId := c.ClientIp()
_, _ = _redis.Do("ZADD", "online_user", time.Now().Unix(), userId)

// 获取在线人数
count, _ := redis.Int(_redis.Do("ZCOUNT", "online_user", time.Now().Unix()-5, "+inf"))

c.JSON(200, gin.H{
"count": count,
})
})

r.Run()
}

当然, 为了防止 数据一直增加, 对于 5秒前的数据, 应该定时将其移出.

我们可以通过 zremrangebyscore 方法, 来移除 指定范围内的数据.

1
2
3
4
5
6
7
8
9
10
11
func AutoRemover() {
_redis := redisutil.R.Get() // redis pool
defer redis.Close()

c := Cron.New()

c.AddFunc("@every 5s", func() {
_, _ = _redis.Do("ZREMRANGEBYSCORE", "online_user", "-inf", time.Now().Unix()-5)
})
c.Start()
}

zset 的其他方法

除了上述提到的方法, zset 还提供了很多其他的方法, 如:

  • zcard key 直接获取 指定 key 内的成员数量
  • zscore key member 获取指定成员的分数
  • zincrby key incr member 增加指定成员的分数
  • zrange key start end [WITHSCORES] 获取指定范围内的成员 并按照分数从小到大排序
  • zrevrange key start end [WITHSCORES] 与上一个相反
  • etc.

其他

可以在 Github 找到 示例.

这里同时实现了, 所有用户及自己的 在线时间显示.

当然, 也可以通过 WebSocket 来实现类似的功能, 这样会更加准确, 但是可能会带来一定的性能损失.

参考

Nginx 重写 Query 参数

作者 xcsoft
2022年5月25日 15:13

由于旧版项目, 依旧使用php-fpm模式提供api服务, 采用 类似 /api.php?m=account&act=login类似这种方式进行控制器分类.

目前的话 打算更换为其他框架, 使用路由来分配控制器.

由于前端及软件迁移成本较高, 就想着是否可以通过Nginx的伪静态来重写.

于是一开始尝试使用

1
2
3
4
location / {
proxy_pass https://127.0.0.1:8787;
rewrite ^api.php\?m=(.*)&act=(.*) /$1/$2 break;
}

直接对其进行重写, 但测试后发现并为达到预期的效果, 服务端接收到的 依旧只有/api.php

貌似并未被nginx匹配到, 查询过后发现 这样貌似是匹配不到路径的?

最终在查询过后, 发现可以通过 query_string 来对其进行匹配

1
2
3
4
5
6
7
8
9
10
11
12
location /api.php {
proxy_pass http://127.0.0.1:8787;
if ( $query_string ~ ^m=(.*)&act=(.*)) {
set $a $1;
set $b $2;
rewrite ^/(.*)$ /$a/$b break;
}
}

location / {
proxy_pass http://127.0.0.1:8787;
}

修改配置文件后, 再次对其进行测试, 成功将/api.php?m=account&act=login重写为/account/login

Typecho 迁移至 Hexo

作者 xcsoft
2022年5月24日 13:19

原来我的博客使用的是Typecho, 主题为Handsome, 整体感觉还是挺不错的, 但是typecho在 大概80并发下, cpu占用就达到了90%以上. 还是比较离谱的, 也可能是我服务器不大行.

我一直使用的是 Dedipath的 2c2g的服务器, 整体感觉还不错. 所有静态资源都存放在腾讯云的COS里, 选择的是私有读写搭配腾讯云的CDN使用. 博客CDN使用的是Cloudfalre, 自己访问速度其实还可以, 但是在某些网站上测了下, 貌似还是比较慢的.

其实很久之前就了解过Hexo这类静态博客, 但一直没去尝试它们. 毕竟迁移还是较为麻烦的.

迁移

文章

对于文章的话,我使用的是 Typecho2Hexo这款工具, 它已经挺久没有更新了, 使用的torndb貌似还是基于Python2的, 但是依旧不影响使用. 简单填写数据库信息, 安装一下所需的依赖就可以了. 他会帮助你将typecho中的所有文章转换成Hexo的格式, 还是比较方便的.

评论

对于评论, 我使用的是Waline, 服务端使用的依旧是Vercel, 本来也想全部放在自己服务器的, 但是nodejs环境貌似存在一些问题, 最终还是放在了Vercel. 不过Vercel还是挺不错的, 访问速度也很快. 数据存储采用的是leancloud, 免费额度理论上是够用了. 评论的迁移我是用的是Waline官方提供的typecho插件. 但他貌似并不支持php8+, 空安全处理还是存在一定问题, 自己稍微改了改, 勉强能用.

Waline的评论貌似是使用网址和路径进行定位的, 我在Typecho中使用的路径风格应该是/archives/:title.html这种, 在插件中也需要去手动修改下, 否则给出的链接是/:title.html这种格式.

主题

博客主题, 一开始我看中的是Maupassant这款, 他宣传的就是 大道至简, 真的是很简洁, 没有过多无用的装饰, 专注于阅读体验. 不过其依赖的插件貌似并不支持m1的arm64, 最终还是选择了比较有名的butterfly.

插件

Hexo的插件还是比较丰富的, 我装了以下几款

  • hexo-generator-feed: 用于生成Feed
  • hexo-generator-sitemap: 用于生成sitemap
  • hexo-hide-posts: 用于隐藏文章
  • hexo-filter-nofollow: 给外链添加nofollow属性

部署

我依旧使用的是Vercel进行部署, 仓库使用的是Bitbucket.

邮件退订的设计与实现

作者 xcsoft
2022年5月16日 15:54

何为邮件退订

在平常的验证码, 推广邮件中, 我们通常会在最下角找到 退订链接。通常访问它, 我们就不会再收到他们发送的邮件。

但是, 如何以最简单, 最节省性能的方式去实现这一功能呢?

一开始

其实最简单的方式, 貌似就是在 发送邮件时, 生成一段随机字符, 存储在数据库或缓存中(已经缓存, 可直接读取), 然后拼接成一个网址, 附在邮件底部. 用户打开后 将字符串传递给后端, 从数据库或缓存中找到这段字符串所对应的邮箱.

获取到请求对定的邮箱后, 将其存储至数据库, 后续发信时, 只需查找一次便可.

这个过程其实很简单, 有没有办法让其更加简单, 发信时不去依赖服务端的持久化存储呢?

了解jwt

其实我们可以借鉴一下 jwt (json web token) 的验证思路. jwt 是由 header, payload, signature 通过小数点间隔 组成的一段字符串.

header

其中 header是由 typ和alg 组成的json 经过base64得出

1
2
3
4
{
"typ": "JWT",
"alg": "HS256"
}

typ是固定的, 指出这段字符为 jwt. 而 alg 则指出了签名的生成方式. 这里使用的是 sha256

payload

payload是有效负荷, 其中存储了jwt的签发单位, 签发时间, 有效时间, 公开信息 等, 它同样也是由这些信息的json 再经过base64得出.

通常情况下, payload建议包含以下字符, 但也不是必须的:

  • iss: jwt签发者

  • sub: jwt所面向的用户

  • aud: 接收jwt的一方

  • exp: jwt的过期时间,这个过期时间必须要大于签发时间

  • nbf: 定义在什么时间之前,该jwt都是不可用的.

  • iat: jwt的签发时间

  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

如, 在此处场景下, 我们可以这样

1
2
3
4
5
{
"iss": "website",
"aud": "email@example.com",
"iat": "timestamp"
}

signature

签名用于对这串 jwt 进行合法性校验. 来判断它是否经过伪造. 进行签名我们需要一个密钥, 这个密钥只能存储在服务端, 并严格保密. 这串密钥也是通常所称的 salt.

签名方式便是header中给出的alg, 如此处 我们通过 SHA256(header + payload + salt) 生成签名.

最终jwt的格式应为 header.payload.sha256(header+payload+salt)

使用

我们只需在发送邮件时, 生成这段jwt. 发送给用户, 所有的信息仅存储在用户的邮件中, 也不用担心伪造. 我们需要做的只是保证密钥的安全.

此时当用户需要退订时, 服务器接收到这段jwt后, 需先对其进行验签, 判断其是否经过伪造. 如果通过, 接着从payload中取出用户邮箱. 将其加入不发送的名单内即可.

参考

自建不蒜子 访问统计

作者 xcsoft
2022年4月25日 13:36

不蒜子 应该算是一款比较好用的前端访问统计工具

一段Js就可以实现统计 站点总 uv pv 及文章的 pv

自建 不蒜子 API

因为其官网可能访问量太大, 不时出现502

于是我花了点时间, 用 Golang 实现了其功能, 数据存储采用 Redis.

uv 通过用户 ip + UserAgent 判断, pv 通过 referer 判断. 所有信息仅存储 md5

使用

1
2
3
4
5
6
7
<!-- 引入js -->
<script async src="https://busuanzi.9420.ltd/js"></script>

本文总阅读量 <span id="busuanzi_page_pv"></span>
本文总访客量 <span id="busuanzi_page_uv"></span>
本站总访问量 <span id="busuanzi_site_pv"></span>
本站总访客数 <span id="busuanzi_site_uv"></span>

开源

源码采用 Apache 2.0 协议开源

仓库: soxft/busuanziGitee

支持 二进制运行 或 Docker 容器运行, 详细安装可查看 安装指引

其他

不同于原版的 不蒜子, 我没有选择使用JSONP这种形式. 因为貌似有部分浏览器已经使用了更为严格的同源策略, 所以部分浏览器可能会出现问题.
我选用的是 通过POST请求后端, 同时携带一个x-bsz-referer请求头 来判断当前网址.对于使用Pjax技术的网站, 可以在引入JS的标签中 添加pjax属性, 来使脚本自动监听pjax页面切换, 详见:帮助文档

Demo

此站点无法保证 SLA

https://busuanzi.9420.ltd

Flutter event_bus 发布/订阅 总线

作者 xcsoft
2022年4月4日 15:10

最近在捣鼓Flutter的时候, 一直在解决有关fileProvider的问题. 当需要打开安卓设备自带的文件管理器时, 一直报错
exposed beyond app through Intent.getData() 在配置 FileProvider相关权限后, 依旧不行, 貌似需要原生调用.这估计就需要flutter与原生相互调用了, 奈何我暂时没有了解过有关原生开发的信息. 就只能先去解决一些页面交互的问题.

event_bus

Flutter 的event_bus扩展, 可以简单的实现一个 发布<->订阅 模型. 如果需要其他的跨组建数据共享, 则可以选择 Provider 扩展.

举个例子, 当我们在 login.dart 中登录后, 需要在 user.dart 中更新信息时. 我们可以直接在 user.dart 中订阅一个事件, 当用户在 login.dart 中登录后发布一个信息, user.dart中监听到事件则重新获取用户信息, 重新渲染页面.

实现

目前 我需要的场景是, 当用户登录后, 直接返回首页, 刷新信息.

大致的文件结构为:

1
2
3
4
5
6
7
8
9
|_ main.dart
|_ account |_ login.dart
|
|_ home |_ index.dart
| |_ home.dart
| |_ user.dart
|
|_ event |_ bus.dart
|_ account_event.dart

其中 main.dart 是我们的启动文件, account/login 为登录页面, home/index 为首页的 入口文件, 有 user 和 home 两个tab.

首先 我们可以新建一个 ~/event/bus.dart 文件, 来全局共享 EventBus 类

1
2
3
4
5
6
7
8
9
10
import 'package:event_bus/event_bus.dart';

// event/bus.dart
class Bus {
static late EventBus eventBus;

static init() {
Bus.eventBus = EventBus();
}
}

接着在 main.dart 中初始化 event_bus

1
2
3
4
5
6
7
/* code */
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Bus.init();

runApp(const App());
}

在发布/订阅消息时, 我们需要一个模型来指定参数, 比如这里 我们只需要一个简单的登录通知 >

1
2
3
4
5
class AccountEvent {
final String type;

const AccountEvent(this.type);
}

接着 我们可以在需要的页面(user.dart)订阅通知 >

1
2
3
4
5
6
7
8
9
10
11
import 'package:example/event/account_event.dart';
import 'package:example/event/bus.dart';

/* code */

Future<void> _listen() async {
Bus.eventBus.on<AccountEvent>().listen((event) {
LogUtil.v(event.type)
/* code */
});
}

此时 当用户登录后, 我们可以直接 通过 EventBus.fire 发布信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'package:example/event/account_event.dart';
import 'package:example/event/bus.dart';

Future<void> _login() async {
httpR.post('/login',{
'user': xx,
'pass': xx,
}).then((res) async {
if (res['code'] == 0){
prefs.setString('token', res['token']); // 存储凭证
Bus.eventBus.fire(const AccountEvent('login'));
Navigator.of(context).pop();
}
})
}

此时, 当用户登录后, 在user.dart中 我们期待会返回一个 内容为 ‘login’ 字符串

❌
❌