Update index.md
This commit is contained in:
1 parent
ac0230b396
commit
c26a5ef875
1 file changed
+103
-97
@@ -22,22 +22,22 @@ ai_comment: >-
|
|||||||
|
|
||||||
## 实现思路
|
## 实现思路
|
||||||
|
|
||||||
整体流程非常简单,分为三个环节:
|
整体流程分为三个环节:
|
||||||
|
|
||||||
1. **数据源**:读取博客现有的友链配置文件(YAML 格式)。
|
1. **数据源**:读取博客现有的友链配置文件(YAML 格式)。
|
||||||
2. **监测脚本**:编写 Node.js 脚本,批量请求友链地址,检测 HTTP 状态码。
|
2. **监测脚本**:编写 Node.js 脚本,利用并发和连接复用技术,批量检测 HTTP 状态码。
|
||||||
3. **自动化运行**:通过 GitHub Actions 定时执行脚本,生成状态报告(JSON),并在前端展示。
|
3. **自动化与通知**:通过 GitHub Actions 定时执行,生成状态报告(JSON),并在发现异常时发送邮件通知。
|
||||||
|
4. **前端展示**:在页面上以不打扰的 UI 风格(黑白极简风格)展示友链状态。
|
||||||
|
|
||||||
## 1. 编写监测脚本
|
## 1. 编写监测脚本
|
||||||
|
|
||||||
首先,在博客根目录的 `scripts` 文件夹下创建一个 `check_links.js`。我们需要用到 `axios` 来发送请求,`js-yaml` 来读取友链数据。
|
在 `scripts/check_links.js` 中,我使用了 `axios` 发送请求,并配合 `https.Agent` 实现了连接复用(Keep-Alive),同时通过并发池控制请求速率。
|
||||||
|
|
||||||
这个脚本的核心逻辑是:
|
这个脚本的核心亮点是:
|
||||||
- 读取 `themes/Ying/data/links.yaml`。
|
- **并发控制**:开启 10 个 Worker 并行检测,大幅缩短耗时。
|
||||||
- 开启 10 个并发线程池,提高检测效率。
|
- **连接复用**:开启 `keepAlive: true`,减少 TCP/TLS 握手开销。
|
||||||
- 复用 HTTP/HTTPS 连接(Keep-Alive),减少握手开销。
|
- **智能重试**:优先使用 `HEAD` 请求(省流量),失败后自动降级为 `GET` 请求。
|
||||||
- 对每个链接发起 `HEAD` 请求(如果失败则重试 `GET`)。
|
- **异常汇总**:将失效链接写入 `failed_links_summary.txt`,用于后续邮件通知。
|
||||||
- 将结果保存为 `static/link_status.json`。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -47,22 +47,16 @@ const path = require('path');
|
|||||||
const https = require('https');
|
const https = require('https');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
|
||||||
// 配置文件路径
|
// ... 路径配置 ...
|
||||||
const linksPath = path.join(__dirname, '../themes/Ying/data/links.yaml');
|
|
||||||
const outputPath = path.join(__dirname, '../static/link_status.json');
|
|
||||||
|
|
||||||
// 配置并发数和超时
|
|
||||||
const CONCURRENCY = 10;
|
|
||||||
const TIMEOUT = 10000;
|
|
||||||
|
|
||||||
// 优化 Axios 实例:开启 Keep-Alive 复用连接
|
// 优化 Axios 实例:开启 Keep-Alive 复用连接
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
timeout: TIMEOUT,
|
timeout: 10000,
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
httpAgent: new http.Agent({ keepAlive: true }),
|
httpAgent: new http.Agent({ keepAlive: true }),
|
||||||
httpsAgent: new https.Agent({ keepAlive: true }),
|
httpsAgent: new https.Agent({ keepAlive: true }),
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
'User-Agent': 'Mozilla/5.0 ...' // 模拟浏览器 UA
|
||||||
},
|
},
|
||||||
validateStatus: (status) => status >= 200 && status < 400
|
validateStatus: (status) => status >= 200 && status < 400
|
||||||
});
|
});
|
||||||
@@ -73,7 +67,7 @@ async function checkUrl(url) {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try {
|
||||||
await axiosInstance.get(url);
|
await axiosInstance.get(url); // HEAD 失败尝试 GET
|
||||||
return true;
|
return true;
|
||||||
} catch (err2) {
|
} catch (err2) {
|
||||||
return false;
|
return false;
|
||||||
@@ -81,103 +75,115 @@ async function checkUrl(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkLinks() {
|
// ... 并发 Worker 逻辑 ...
|
||||||
console.log('Starting link check...');
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileContents = fs.readFileSync(linksPath, 'utf8');
|
|
||||||
const data = yaml.load(fileContents);
|
|
||||||
const blogs = data.blogs.filter(b => b.url);
|
|
||||||
const results = {};
|
|
||||||
|
|
||||||
// 并发工作队列
|
|
||||||
async function worker() {
|
|
||||||
while (blogs.length > 0) {
|
|
||||||
const blog = blogs.shift(); // 取出一个任务
|
|
||||||
const status = await checkUrl(blog.url);
|
|
||||||
results[blog.url] = status;
|
|
||||||
console.log(`[${status ? 'OK' : 'FAIL'}] ${blog.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动 10 个 Worker 并发处理
|
|
||||||
const workers = Array(Math.min(blogs.length, CONCURRENCY))
|
|
||||||
.fill(null)
|
|
||||||
.map(() => worker());
|
|
||||||
|
|
||||||
await Promise.all(workers);
|
|
||||||
|
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
||||||
console.log(`✨ Check finished in ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkLinks();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. 配置 GitHub Actions
|
## 2. 配置 GitHub Actions
|
||||||
|
|
||||||
脚本写好了,需要让它动起来。修改 `.github/workflows/upy.yml`(或者你的部署流程文件),在 Hugo 构建之前加入监测步骤。
|
为了保证数据的实时性,我在 `.github/workflows/upy.yml` 中配置了定时任务。
|
||||||
|
|
||||||
这样每次博客更新,或者定时触发时,都会生成最新的 `link_status.json`。
|
- **定时触发**:每 12 小时自动运行一次 (`cron: '0 */12 * * *'`)。
|
||||||
|
- **邮件通知**:如果发现失效友链,Workflow 会读取脚本生成的摘要文件,并通过邮件推送到我的邮箱。
|
||||||
```yaml
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
steps:
|
|
||||||
# ... 前置环境配置 ...
|
|
||||||
|
|
||||||
- name: Check Link Status
|
|
||||||
run: node scripts/check_links.js
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: hugo --minify
|
|
||||||
```
|
|
||||||
|
|
||||||
为了保证数据的实时性,建议配置 `schedule` 定时触发:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */12 * * *' # 每12小时自动检查一次
|
- cron: '0 */12 * * *' # 每12小时自动检查一次
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
steps:
|
||||||
|
# ... 其他步骤 ...
|
||||||
|
|
||||||
|
- name: Check Link Status
|
||||||
|
run: node scripts/check_links.js
|
||||||
|
|
||||||
|
- name: Read Failed Links Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f failed_links_summary.txt ]; then
|
||||||
|
echo "LINK_SUMMARY<<EOF" >> $GITHUB_ENV
|
||||||
|
cat failed_links_summary.txt >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ... 构建步骤 ...
|
||||||
|
|
||||||
|
- name: Send deployment success notification
|
||||||
|
if: success()
|
||||||
|
uses: dawidd6/action-send-mail@v3
|
||||||
|
with:
|
||||||
|
# ... 邮件配置 ...
|
||||||
|
body: |
|
||||||
|
# ... 其他信息 ...
|
||||||
|
|
||||||
|
📊 友链监测报告
|
||||||
|
----------------------------------
|
||||||
|
${{ env.LINK_SUMMARY }}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 前端展示状态
|
## 3. 前端展示:极简设计
|
||||||
|
|
||||||
最后一步,在前端 JS 中读取这个 JSON 文件,并根据状态给友链添加样式。
|
为了不破坏博客原本的黑白极简风格,我并没有使用传统的“红/绿”大色块,而是设计了一套低调的状态指示器。
|
||||||
|
|
||||||
我在 `main.js` 中加入了如下逻辑:
|
### JS 逻辑 (`main.js`)
|
||||||
1. `fetch('/link_status.json')` 获取状态。
|
|
||||||
2. 遍历页面上的友链元素。
|
前端脚本会异步加载 `link_status.json`,并智能匹配 URL(自动处理 `https` 和尾部 `/` 的差异)。
|
||||||
3. 如果状态为 `true`,添加绿色圆点;如果为 `false`,添加红色圆点。
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 伪代码示例
|
window.initLinkStatus = function() {
|
||||||
fetch('/link_status.json')
|
fetch('/link_status.json?t=' + new Date().getTime())
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(statusMap => {
|
.then(data => {
|
||||||
document.querySelectorAll('.friend-link-item').forEach(item => {
|
// ... 遍历 DOM 元素 ...
|
||||||
const url = item.getAttribute('href');
|
// getUrlStatus 逻辑:尝试匹配 url, url/, https/http 变体
|
||||||
if (statusMap[url] === false) {
|
|
||||||
item.classList.add('dead-link'); // 样式控制显示红点
|
if (status === 'alive') {
|
||||||
} else {
|
$item.append('<span class="link-active-badge" title="可访问"></span>');
|
||||||
item.classList.add('active-link'); // 样式控制显示绿点
|
} else if (status === 'dead') {
|
||||||
}
|
$item.append('<span class="link-dead-badge" title="无法访问"></span>');
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
配合 CSS,就能实现如下效果:
|
### CSS 样式 (`main.css`)
|
||||||
- 🟢 **绿色**:网站可访问,安心。
|
|
||||||
- 🔴 **红色**:网站无法访问,需要重点关注。
|
这里花了一些心思调整配色。为了避免“红绿灯”配色的突兀感:
|
||||||
|
|
||||||
|
- **正常状态**:使用**实心灰点** (`#9ca3af`),在暗色模式下为浅灰 (`#d1d5db`)。
|
||||||
|
- **失效状态**:使用**空心灰圈**(白色/深色中心 + 灰色边框)。
|
||||||
|
|
||||||
|
这种设计既能传达状态信息,又完美融入了黑白主题。
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Card View - Active (实心灰点) */
|
||||||
|
.link-active-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; right: 8px;
|
||||||
|
width: 8px; height: 8px;
|
||||||
|
background-color: #9ca3af;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card View - Dead (空心灰圈) */
|
||||||
|
.link-dead-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px; right: 8px;
|
||||||
|
width: 8px; height: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #9ca3af; /* 灰色边框形成空心效果 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
这套方案最大的好处是**零成本、自动化**。不需要额外部署监控服务,完全利用 GitHub Actions 的计算资源。
|
这套方案最大的好处是**零成本、自动化且优雅**。
|
||||||
|
|
||||||
|
1. **零成本**:完全利用 GitHub Actions 的计算资源。
|
||||||
|
2. **自动化**:定时检查 + 邮件报警,无需人工干预。
|
||||||
|
3. **优雅**:前端 UI 克制且统一,不喧宾夺主。
|
||||||
|
|
||||||
对于注重 SEO 和网站安全的博主来说,这是一个性价比极高的“基建”工作。既对自己负责,也对访客负责。毕竟,谁也不想点击一个链接后发现是 404,或者跳转到奇怪的页面吧。
|
对于注重 SEO 和网站安全的博主来说,这是一个性价比极高的“基建”工作。既对自己负责,也对访客负责。毕竟,谁也不想点击一个链接后发现是 404,或者跳转到奇怪的页面吧。
|
||||||
Reference in new issue
Block a user