Add infinite-scroll pagination to home page and publish new blog posts

This commit is contained in:
2026-05-14 01:16:15 +08:00
parent a68f14259a
commit 520206f391
13 changed files with 667 additions and 23 deletions
-2
View File
@@ -2,8 +2,6 @@
title: FuKun
layout: hextra-home
---
{{< hextra/hero-subtitle >}}
Fukun.Net
{{< /hextra/hero-subtitle >}}
-1
View File
@@ -1,6 +1,5 @@
---
---
{{< hextra/hero-subtitle >}}
Fukun.Net/blog
{{< /hextra/hero-subtitle >}}
+98
View File
@@ -0,0 +1,98 @@
---
title: "CI/CD Patterns with GitHub Actions"
date: 2026-05-15T09:00:00+08:00
draft: false
description: "Build matrices, caching strategies, environment-specific deployments, and reusable workflows. Patterns that scale from solo projects to team repos."
tags:
- CI/CD
- GitHub
- DevOps
---
GitHub Actions is now the default CI for most open-source projects. Build matrices save config lines, caching `node_modules` and Go modules saves minutes per run. Reusable workflows keep things DRY across repos.
## Build Matrices
Don't copy-paste job definitions for different Node versions or OS targets. Use a matrix:
```yaml
jobs:
test:
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
```
This single definition expands to 6 parallel jobs. Add `fail-fast: false` if you want all runs to complete even when one fails — helpful for catching platform-specific bugs in one CI run.
## Smart Caching
Caching is the easiest way to cut CI minutes:
```yaml
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: npm-${{ runner.os }}-
```
The `restore-keys` fallback is important: even a partial cache hit saves time. For Go modules, cache `~/go/pkg/mod`. For Docker builds, use BuildKit's registry cache (`--cache-from type=registry`) for layer caching across CI runs.
## Environment-Specific Deployments
Use environments to gate production deploys:
```yaml
deploy-prod:
needs: [test, build]
environment: production
steps:
- run: ./deploy.sh production
```
The `environment: production` lets you set required reviewers, wait timers, and environment-specific secrets in the GitHub UI. No more accidentally deploying to prod from a feature branch.
## Reusable Workflows
When you maintain multiple repos, reusable workflows prevent drift:
```yaml
# .github/workflows/deploy.yml in a shared repo
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
AWS_ROLE:
required: true
```
Call it from any repo:
```yaml
jobs:
deploy:
uses: org/shared-workflows/.github/workflows/deploy.yml@main
with:
environment: staging
secrets:
AWS_ROLE: ${{ secrets.AWS_ROLE }}
```
Update the shared workflow once, and every repo benefits. This pattern scales well for orgs with 10+ services.
## Practical Tips
- Set `timeout-minutes` on every job — the default is 360 minutes, and you don't want a hung test burning your quota.
- Use `concurrency` to cancel redundant runs when pushing to the same PR multiple times.
- Pin action versions to SHA hashes for supply-chain security, not just tags.
+48
View File
@@ -0,0 +1,48 @@
---
title: "数据库索引优化实战"
date: 2026-05-10T16:00:00+08:00
draft: false
description: "慢查询分析、联合索引的最左前缀原则、覆盖索引与回表的性能差异。几个真实 SQL 优化案例,从几秒降到毫秒级。"
tags:
- 数据库
- SQL
- 性能
---
`EXPLAIN` 是 DBA 的眼睛。看 `type` 字段从 `ALL` 变成 `ref` 是最有成就感的事。
## 从慢查询开始
一切优化从定位问题开始。打开 MySQL 的慢查询日志(`slow_query_log`),设置 `long_query_time = 0.1`,跑一段时间后分析。PostgreSQL 用 `pg_stat_statements` 扩展,同样有效。
拿到慢查询,先 `EXPLAIN` 看执行计划。关注几个关键字段:
| 字段 | 含义 | 目标 |
|------|------|------|
| type | 访问类型 | 至少到 range,最好到 ref/const |
| key | 使用的索引 | 不为 NULL |
| rows | 扫描行数 | 越小越好 |
| Extra | 额外信息 | 避免 Using filesort / Using temporary |
## 最左前缀原则
联合索引是最容易出错的地方。假设索引是 `(a, b, c)`:
- `WHERE a = 1` ✅ 用到索引
- `WHERE a = 1 AND b = 2` ✅ 用到索引
- `WHERE b = 2` ❌ 用不到
- `WHERE a = 1 AND c = 3` ⚠️ 只用到了 a 列
排序也受这个规则约束。`ORDER BY a, b` 没问题,`ORDER BY b, c` 就需要额外排序。
## 覆盖索引
如果查询需要的所有列都在索引里,就不需要回表取数据。`Extra` 列显示 `Using index` 就是覆盖索引。对一个高频查询,覆盖索引能减少大量随机 I/O。
代价是索引变大了。不要为了覆盖索引把所有列都塞进索引——算一笔写入性能和存储空间的账。
## 真实案例
某个订单列表查询,原始 SQL 跑了 8 秒。`EXPLAIN` 一看,`type: ALL`,全表扫描 200 万行。加了 `(user_id, created_at)` 联合索引后,`type: ref`,扫描 20 行,耗时 12ms。
**索引不是越多越好**——每个索引都会拖慢写入速度并占用磁盘空间。建索引前问自己:这个查询的执行频率值得一个索引吗?
+49
View File
@@ -0,0 +1,49 @@
---
title: "家庭网络与 NAS 搭建方案"
date: 2026-05-17T15:00:00+08:00
draft: false
description: "软路由、Wi-Fi 6 AP、自建 NAS 的硬件选型和软件方案。Plex 媒体服务器 + Syncthing 文件同步 + Tailscale 远程访问,一套低成本高品质的家庭网络实践。"
tags:
- NAS
- 网络
- 硬件
---
折腾家庭网络的终点是克制。和 Linux 桌面美化一样,容易用力过猛。本文记录一套低成本但够用的家庭网络方案。
## 网络拓扑
```
光猫 → 软路由(OpenWrt)→ 交换机
├── Wi-Fi 6 AP ×2
├── NAS(TrueNAS)
└── 有线设备
```
软路由选 J4125 或 N100 这类低功耗 x86,刷 OpenWrt,负责拨号、DHCP、科学上网。AP 选 Wi-Fi 6 的,TP-Link XDR 系列性价比不错,刷 OpenWrt 或者用原厂固件都可以。
两个 AP 组 Mesh,不需要折腾 AC 控制器。家里 120 平米基本无死角覆盖。
## NAS 硬件与系统
旧电脑加几块硬盘就是一台 NAS。电费要算:不要拿老 i7 加独显当 NAS,待机功耗四五十瓦,一个月电费够买半块硬盘。
推荐方案:J4125 或 N100 小板 + 8GB 内存 + 2~4 块 4TB 硬盘组 RAID-Z1。系统选 TrueNAS Scale,基于 Linux,容器和虚拟机支持比 Core 版更好。待机功耗 15W 左右,一年电费不到一百块。
## 软件方案
三个核心应用:
| 软件 | 用途 | 替代方案 |
|------|------|----------|
| **Plex** | 媒体服务器,自动刮削海报和元数据 | Jellyfin(开源免费) |
| **Syncthing** | 文件同步,多设备自动备份照片和文档 | Resilio Sync |
| **Tailscale** | 虚拟局域网,外面也能访问家里的服务 | ZeroTier、WireGuard 裸配 |
Plex 装在 TrueNAS 的 App 里,显卡直通给 Plex 做硬件转码——手机上用较低码率看片时自动转码,画质损失不大。Syncthing 每台设备装一个,照片目录设为"仅发送",电脑上的文档文件夹设为"双向同步"。
Tailscale 是真正的体验质变。不需要公网 IP,不需要端口转发,不需要记 IP 地址。手机、笔记本、NAS 在同一个虚拟局域网里,出门在外访问 Plex 和 NAS 文件就像在家一样。免费版支持最多 100 台设备,个人用完全够。
## 总结
方案的核心思路:**够用即可**。不要想着一步到位,先从旧硬件开始跑起来,哪里不够补哪里。家庭网络的价值在于稳定无声地跑着,不是折腾本身。
+32
View File
@@ -0,0 +1,32 @@
---
title: "Linux 桌面美化指北"
date: 2026-05-11T10:00:00+08:00
draft: false
description: "从默认 Gnome 到一套顺手的桌面环境。GTK 主题、图标包、终端配色、窗口管理器的选择与配置思路。不追求花哨,追求每天盯着看不累。"
tags:
- Linux
- 桌面
- 效率
---
Linux 桌面环境的选择很多,但真正好用的配置往往很简单。本文记录从 Ubuntu 默认桌面开始,逐步调整到一套耐看、高效的配置。
## 主题选择
白天用亮色主题降低反光,晚上切暗色保护眼睛。推荐 **Nord** 和 **Catppuccin** 两套配色——前者冷峻克制,后者柔和温暖,每天盯着看八小时不累。
GTK 主题建议选择维护活跃的项目,避免 Gnome 大版本升级后样式错乱。Qt 应用在 Gnome 下可以用 `qt5ct` 统一风格。
## 字体配置
终端字体一定要等宽且清晰。推荐 **Fira Code** 做编程字体,连字(ligatures)让 `=>`、`!=`、`>=` 这类符号组合更易读。中文字体用 **Noto Sans CJK**,和西文搭配自然。
## 终端与 Shell
终端模拟器推荐 **Alacritty**——GPU 加速,配置文件清晰。配合 zsh + **starship** 提示符,日常使用效率提升不少。Starship 的优势在于跨 shell 兼容,换到 fish 或 nu 不用重新配置。
## 窗口管理
Gnome 默认的三指滑动切换工作区已经很好用。如果追求更极致的键盘操作,可以试试 **Pop Shell** 平铺扩展,或者直接上 **i3/sway**。但对大多数人来说,Gnome 的默认交互已经够用。
关键原则:**不追求花哨,追求每天盯着看不累。**
+67
View File
@@ -0,0 +1,67 @@
---
title: "Python Async: Beyond the Basics"
date: 2026-05-12T11:00:00+08:00
draft: false
description: "Understanding the event loop, writing proper async context managers, and avoiding the most common pitfalls with asyncio.gather and cancel scopes."
tags:
- Python
- Async
- Programming
---
Async Python is no longer optional if you're doing any kind of I/O-bound work. Beyond the basics of `async`/`await`, understanding the event loop helps debug elusive issues.
## The Event Loop in Practice
The event loop runs one coroutine at a time, switching at `await` points. This is cooperative multitasking — a coroutine that never awaits blocks everything else. If you have CPU-bound work inside an async function, offload it to a thread pool with `asyncio.to_thread()`.
```python
import asyncio
# Bad: blocks the event loop
async def bad():
result = heavy_computation() # CPU-bound, no await
return result
# Good: offloads to a thread
async def good():
result = await asyncio.to_thread(heavy_computation)
return result
```
## Async Context Managers
Writing proper async context managers is essential for managing connections and locks:
```python
class AsyncConnection:
async def __aenter__(self):
self.conn = await create_connection()
return self.conn
async def __aexit__(self, *args):
await self.conn.close()
```
The `contextlib.asynccontextmanager` decorator makes this even cleaner for simple cases.
## Common Pitfalls with `asyncio.gather`
`gather` runs tasks concurrently, but one failing task doesn't cancel the others by default. If you need all-or-nothing semantics, use `asyncio.TaskGroup` (Python 3.11+):
```python
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch(url1))
t2 = tg.create_task(fetch(url2))
# Both succeeded, or both were cancelled
```
## Cancel Scopes
Cancellation in asyncio is cooperative: `asyncio.CancelledError` is raised at the next `await`. If your coroutine catches and suppresses it, the task becomes uncancellable — usually a bug. Use `asyncio.shield()` sparingly to protect critical cleanup code.
## Structured Concurrency
The trend across languages (Trio in Python, Kotlin coroutines, Swift concurrency) is toward structured concurrency: every task has a clear parent scope that determines its lifetime. `TaskGroup` is Python's step in this direction. Embrace it over bare `create_task()` calls.
Async Python has matured significantly. Most of the old footguns have been addressed, but the fundamentals — understand the event loop, respect cancellation, and structure your concurrency — remain essential.
+95
View File
@@ -0,0 +1,95 @@
---
title: "Rust Error Handling: From unwrap to anyhow"
date: 2026-05-16T13:00:00+08:00
draft: false
description: "The journey from panic-driven development to proper error types. When to use thiserror for library crates and anyhow for applications."
tags:
- Rust
- Error Handling
- Programming
---
Rust forces you to think about errors. Starting with `unwrap()` everywhere is fine for prototypes, but production code needs proper error types.
## Stage 1: The Unwrap Phase
```rust
let config = read_config().unwrap();
let db = connect(&config).unwrap();
let result = query(&db).unwrap();
```
Every Rust beginner goes through this. It's fine for scripts and prototypes — if something fails, the panic message with a line number is good enough. The problem starts when you need graceful error handling or meaningful error messages for users.
## Stage 2: Box<dyn Error>
The quick upgrade path:
```rust
fn do_thing() -> Result<(), Box<dyn std::error::Error>> {
let config = read_config()?;
let db = connect(&config)?;
query(&db)?;
Ok(())
}
```
The `?` operator works with any error type that implements `From<T>`. `Box<dyn Error>` catches everything. But you lose type information — the caller can't match on specific error variants.
## Stage 3: Proper Error Types with thiserror
For libraries, define explicit error types:
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("configuration error: {0}")]
Config(String),
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {0}")]
NotFound(String),
}
```
The `#[from]` attribute auto-implements `From<T>`, so `?` works seamlessly. Callers can match on variants:
```rust
match do_thing().await {
Err(AppError::NotFound(id)) => { /* handle 404 */ }
Err(e) => { /* log and return 500 */ }
Ok(data) => { /* success */ }
}
```
## Stage 4: Application Errors with anyhow
For binaries, `anyhow` provides flexible error handling without the ceremony:
```rust
use anyhow::{Context, Result};
fn main() -> Result<()> {
let config = read_config()
.with_context(|| "failed to read config file")?;
let db = connect(&config)
.context("database connection failed")?;
Ok(())
}
```
`context()` and `with_context()` attach human-readable messages at each fallible step. The error output is a chain of contexts, making debugging straightforward.
## The Ecosystem Split
| Crate | Use Case | Key Feature |
|-------|----------|-------------|
| `thiserror` | Library crates | Derive macro, typed errors |
| `anyhow` | Application binaries | Context chains, easy `?` |
| `eyre` | Applications (alternative) | Colorful error reports, custom hooks |
The ecosystem has settled on this pragmatic split: **thiserror for libraries, anyhow for binaries**. Libraries expose typed errors so callers can react programmatically; applications benefit from rich, human-readable error chains.
Remember: `unwrap()` and `expect()` still have their place — in tests, in examples, and when an invariant violation truly merits a panic. But for production code paths, make errors part of your type signature.
+53
View File
@@ -0,0 +1,53 @@
---
title: "SSH 隧道实用指南"
date: 2026-05-09T14:00:00+08:00
draft: false
description: "本地端口转发、远程端口转发、动态端口转发三种模式的实际应用场景。从绕过防火墙访问内网服务,到把本地开发环境暴露给公网调试。"
tags:
- SSH
- 网络
- 教程
---
SSH 隧道是开发者的必备技能,但很多人只用过最基本的 `ssh user@host`。三种端口转发模式记清楚了,日常开发基本够用。
## 本地端口转发(-L)
把远程内网的服务映射到本机。
```bash
ssh -L 3306:db.internal:3306 jump-host
```
这条命令让本地的 `localhost:3306` 流量通过跳板机转发到内网的数据库服务器。典型场景:你在家开发,公司的数据库只在内网可访问。连上跳板机后,`mysql -h 127.0.0.1 -P 3306` 就能直接操作数据库。
## 远程端口转发(-R)
反过来,把本地端口暴露给远程服务器。
```bash
ssh -R 8080:localhost:3000 public-server
```
这个场景适合调试 webhook:你在本地起了服务,第三方服务需要回调你的端点。把本地的 3000 端口映射到公网服务器的 8080,webhook URL 填 `http://public-server:8080` 就能收到回调。
记得在 remote 服务器上设置 `GatewayPorts yes` 才能让外部访问。
## 动态端口转发(-D)
临时搭建 SOCKS 代理。
```bash
ssh -D 1080 jump-host
```
然后浏览器或系统代理设置为 `socks5://localhost:1080`,流量就全走跳板机出去了。配合 SwitchyOmega 这类浏览器扩展,按域名自动切换代理规则,体验更好。
## 实用技巧
- 加 `-N` 不执行远程命令,纯隧道。
- 加 `-f` 后台运行。
- 加 `-C` 压缩传输,慢网络下有用。
- 用 `~/.ssh/config` 给常用隧道起别名,避免每次敲长命令。
三种模式记清楚,SSH 隧道基本够用。
+45
View File
@@ -0,0 +1,45 @@
---
title: "Svelte 初体验:不一样的思路"
date: 2026-05-10T09:30:00+08:00
draft: false
description: "试用了 Svelte 5 的 runes 语法和 SvelteKit 框架。没有虚拟 DOM,编译时直接把组件转成 DOM 操作代码,打包体积小得惊人。"
tags:
- Svelte
- 前端
- JavaScript
---
用惯了 React 和 Vue,第一次接触 Svelte 的时候确实有种新鲜感。最直观的感受:**代码量少了很多**。
## Runes 语法
Svelte 5 引入了 runes 语法,让响应式声明更显式:
- **`$state`** — 声明响应式变量,替代了之前靠赋值触发响应式的隐式规则。
- **`$derived`** — 衍生值,类似 Vue 的 computed。
- **`$effect`** — 副作用,自动追踪依赖并在变化时执行。
```svelte
<script>
let count = $state(0);
let doubled = $derived(count * 2);
$effect(() => {
console.log(`count is now ${count}`);
});
</script>
```
逻辑清晰,没有 `useEffect` 的依赖数组心智负担。
## 编译时魔法
Svelte 的核心理念:没有虚拟 DOM,编译器在构建时直接把组件转成高效的 DOM 操作代码。打包体积小得惊人,一个简单的组件可能只有几百字节。对移动端场景尤其友好。
## SvelteKit
SvelteKit 是 Svelte 的全栈框架,文件路由设计简洁。约定优于配置——新建一个 `src/routes/about/+page.svelte`,就自动有了 `/about` 路由。内置 SSR、SSG、CSR 多种渲染模式,服务器端数据加载用 `load` 函数,类型安全做得不错。
## 总结
Svelte 适合想要减少样板代码、追求小打包体积的项目。React 和 Vue 的生态更成熟,但 Svelte 的思路值得关注。下一步打算用 SvelteKit 重写个人博客试试看。
+64
View File
@@ -0,0 +1,64 @@
---
title: "Migrating to Tailwind CSS v4"
date: 2026-05-13T08:00:00+08:00
draft: false
description: "CSS-first configuration, the new @theme directive, container queries, and what actually changed from v3. A practical migration log."
tags:
- CSS
- Tailwind
- Frontend
---
Tailwind v4 drops the JavaScript config file in favor of CSS-based configuration. The migration is mostly mechanical but there are gotchas with the `@apply` directive and custom variants.
## CSS-First Configuration
The biggest change: `tailwind.config.js` is replaced by CSS. Your design tokens live in the `@theme` directive:
```css
@import "tailwindcss";
@theme {
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--font-sans: "Inter", sans-serif;
--spacing-container: 1280px;
}
```
VSCode IntelliSense still works — the Tailwind CSS extension reads theme values from your stylesheet.
## The Migration Path
1. Install v4: `npm install tailwindcss@next`
2. Run the automatic migration tool: `npx @tailwindcss/upgrade`
3. Manually check `@apply` usages — the tool handles most cases, but deeply nested `@apply` with custom utilities sometimes breaks.
4. Replace `theme.extend` in your old config with `@theme` blocks in CSS.
## New Features Worth Using
### Container Queries
No more media queries for component-level responsiveness. Tailwind v4 ships container query utilities:
```html
<div class="@container">
<div class="grid @lg:grid-cols-2">...</div>
</div>
```
### Dynamic Utility Values
Arbitrary values are simpler: `w-(--sidebar-width)` references a CSS custom property. No more bracket syntax for common cases.
### Native Cascade Layers
Tailwind v4 uses `@layer` to control specificity. Base, components, and utilities are layered in the correct order — `@apply` in component layer no longer risks specificity wars.
## Gotchas
- Custom variants defined in old JS config need to be re-registered via `@variant` in CSS.
- The `important` config option now needs an explicit CSS strategy.
- Some plugins haven't updated yet — check compatibility before migrating production projects.
Overall, the migration took about two hours for a mid-sized project. The CSS-first approach feels more natural, and the build is noticeably faster thanks to the new Oxide engine.
+70
View File
@@ -0,0 +1,70 @@
---
title: "Deploying to AWS ECS with Terraform"
date: 2026-05-14T10:00:00+08:00
draft: false
description: "Infrastructure as code from scratch: VPC, subnets, security groups, ECR repository, ECS service with Fargate, and an ALB in front. Every resource explained."
tags:
- AWS
- Terraform
- DevOps
---
ECS with Fargate is the sweet spot between managing servers and going full serverless. Terraform makes it reproducible.
## The Stack
Here's what we need to provision:
| Resource | Purpose |
|----------|---------|
| VPC | Isolated network |
| Public & private subnets | Multi-AZ placement |
| Security groups | Network access control |
| ECR repository | Container image storage |
| ECS cluster + service | Run containers |
| ALB + target group | Traffic distribution |
## Security Groups: Getting Them Right
The key is getting the security groups right — one misconfigured rule and your containers can't talk to each other, or worse, your database is exposed to the internet.
```hcl
resource "aws_security_group" "ecs_service" {
name = "ecs-service-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
The principle: **allow the minimum**. The ALB security group allows inbound from the internet on 80/443. The ECS security group only allows inbound from the ALB. The database security group only allows inbound from ECS.
## Fargate Configuration
Fargate abstracts the EC2 instances. You specify CPU and memory, and AWS handles the rest. For most web services, 0.25 vCPU / 512 MB is a good starting point — scale up based on load testing, not guessing.
## CI/CD Integration
After Terraform creates the infrastructure, your CI pipeline needs to:
1. Build and tag the Docker image
2. Push to ECR
3. Update the ECS service with the new task definition
Use a Terraform backend with state locking — S3 + DynamoDB is the standard approach. Without state locking, two people running `terraform apply` at the same time can corrupt your infrastructure state.
## Cost Notes
Fargate is more expensive per vCPU-hour than raw EC2, but you save the operational cost of patching instances and managing capacity. For small-to-medium workloads, the difference is negligible compared to the engineering time saved.