· 前端开发
使用 Astro 岛屿架构构建零后端在线工具:JSON 格式化与 Base64 编解码
详细介绍如何使用 Astro + React 岛屿架构构建纯前端在线工具,无需后端服务器,数据不上传,包含完整的实现代码和性能优化技巧
Astro React 岛屿架构 在线工具 前端开发
使用 Astro 岛屿架构构建零后端在线工具
最近在个人博客中添加了几个在线开发工具(JSON 格式化、Base64 编解码),完全基于前端实现,无需后端服务器。本文分享使用 Astro 岛屿架构的实践经验。
架构选择
为什么选择 Astro 岛屿架构?
传统 SPA 方案的问题:
- 首屏加载大量 JavaScript
- 工具页面通常只需要局部交互
- 不利于 SEO
Astro 岛屿架构的优势:
页面主体 = 静态 HTML(零 JS)
交互部分 = React/Vue/Svelte 组件(按需加载)
---
// 工具页面框架
import Layout from '../../layouts/Layout.astro';
---
<Layout title="JSON 格式化">
<div class="container">
<!-- 静态内容:工具说明 -->
<h1>JSON 格式化</h1>
<p>格式化、验证和美化 JSON 数据...</p>
<!-- 岛屿组件:交互区域 -->
<div id="json-tool">
<!-- React 组件将在这里水合 -->
</div>
</div>
<script>
// 纯客户端 JavaScript,无需 React
// 适合简单交互
</script>
</Layout>
实现方案
方案一:纯 JavaScript(推荐简单工具)
对于 JSON 格式化这种简单工具,不需要 React,直接用原生 JS:
---
import Layout from '../../layouts/Layout.astro';
---
<Layout title="JSON 格式化">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 输入区 -->
<div>
<label>输入</label>
<textarea id="input-json" class="w-full h-96 font-mono"></textarea>
</div>
<!-- 输出区 -->
<div>
<label>输出</label>
<textarea id="output-json" readonly class="w-full h-96 font-mono"></textarea>
</div>
</div>
<!-- 控制按钮 -->
<div class="flex gap-4 mt-6">
<button id="btn-format" class="px-6 py-2 bg-blue-500 text-white rounded">
格式化
</button>
<button id="btn-minify" class="px-6 py-2 bg-gray-700 text-white rounded">
压缩
</button>
<button id="btn-copy" class="px-6 py-2 bg-green-500 text-white rounded">
复制
</button>
</div>
<script>
// 客户端脚本
const inputEl = document.getElementById('input-json');
const outputEl = document.getElementById('output-json');
// 格式化
document.getElementById('btn-format')?.addEventListener('click', () => {
try {
const obj = JSON.parse(inputEl.value);
outputEl.value = JSON.stringify(obj, null, 2);
} catch (error) {
outputEl.value = `错误: ${error.message}`;
}
});
// 压缩
document.getElementById('btn-minify')?.addEventListener('click', () => {
try {
const obj = JSON.parse(inputEl.value);
outputEl.value = JSON.stringify(obj);
} catch (error) {
outputEl.value = `错误: ${error.message}`;
}
});
// 复制
document.getElementById('btn-copy')?.addEventListener('click', async () => {
await navigator.clipboard.writeText(outputEl.value);
alert('已复制到剪贴板');
});
// 自动格式化(防抖)
let debounceTimer;
inputEl?.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
document.getElementById('btn-format')?.click();
}, 500);
});
</script>
</Layout>
优点:
- 零 React 依赖
- 加载速度快
- 代码简单直接
方案二:React 岛屿(适合复杂工具)
对于需要复杂状态管理的工具,使用 React:
---
import Layout from '../../layouts/Layout.astro';
import FundingRateTool from '../../components/tools/FundingRateTool.tsx';
---
<Layout title="合约资金费率">
<!-- 静态说明 -->
<h1>合约资金费率查询</h1>
<p>实时获取各大交易所的永续合约资金费率...</p>
<!-- React 岛屿:客户端水合 -->
<FundingRateTool client:load />
</Layout>
// FundingRateTool.tsx
import { useState, useEffect } from 'react';
export default function FundingRateTool() {
const [rates, setRates] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetchRates();
}, []);
const fetchRates = async () => {
setLoading(true);
const data = await fetch('/api/funding-rate/binance').then(r => r.json());
setRates(data);
setLoading(false);
};
return (
<div className="space-y-4">
{loading ? (
<div>加载中...</div>
) : (
<table className="w-full">
{/* 费率表格 */}
</table>
)}
</div>
);
}
JSON 格式化工具完整实现
---
import Layout from '../../layouts/Layout.astro';
---
<Layout
title="JSON 格式化"
description="格式化、验证和美化 JSON 数据"
>
<div class="container py-8">
<!-- 面包屑 -->
<nav class="mb-6">
<ol class="flex items-center gap-2 text-sm">
<li><a href="/tools" class="text-gray-500 hover:text-blue-500">工具箱</a></li>
<li class="text-gray-400">/</li>
<li class="text-gray-900">JSON 格式化</li>
</ol>
</nav>
<!-- 标题 -->
<div class="mb-8">
<h1 class="text-3xl font-bold mb-2">JSON 格式化</h1>
<p class="text-gray-600">
格式化、验证和美化 JSON 数据。所有处理在浏览器本地完成,数据不会上传到服务器。
</p>
</div>
<!-- 工具界面 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 输入 -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<label class="text-sm font-medium">输入</label>
<div class="flex gap-2">
<button id="btn-sample" class="px-3 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">
加载示例
</button>
<button id="btn-clear" class="px-3 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">
清空
</button>
</div>
</div>
<textarea
id="input-json"
class="w-full h-96 p-4 font-mono text-sm bg-white border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500"
placeholder='{"key": "value", "array": [1, 2, 3]}'
spellcheck="false"
></textarea>
</div>
<!-- 输出 -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<label class="text-sm font-medium">输出</label>
<button id="btn-copy" class="px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600">
复制结果
</button>
</div>
<textarea
id="output-json"
readonly
class="w-full h-96 p-4 font-mono text-sm bg-gray-50 border border-gray-300 rounded-lg resize-none"
placeholder="格式化后的 JSON 将显示在这里..."
></textarea>
</div>
</div>
<!-- 控制区 -->
<div class="mt-6 flex flex-wrap items-center gap-4 p-4 bg-gray-50 rounded-lg">
<button id="btn-format" class="px-6 py-2 bg-blue-500 text-white font-medium rounded-lg hover:bg-blue-600 transition-colors">
格式化
</button>
<button id="btn-minify" class="px-6 py-2 bg-gray-700 text-white font-medium rounded-lg hover:bg-gray-800 transition-colors">
压缩
</button>
<button id="btn-escape" class="px-6 py-2 bg-gray-200 text-gray-700 font-medium rounded-lg hover:bg-gray-300 transition-colors">
转义
</button>
<button id="btn-unescape" class="px-6 py-2 bg-gray-200 text-gray-700 font-medium rounded-lg hover:bg-gray-300 transition-colors">
去转义
</button>
<!-- 缩进选择 -->
<div class="flex items-center gap-2 ml-auto">
<label class="text-sm text-gray-600">缩进:</label>
<select id="indent-select" class="px-3 py-1 bg-white border border-gray-300 rounded text-sm">
<option value="2">2 空格</option>
<option value="4" selected>4 空格</option>
<option value="\t">Tab</option>
</select>
</div>
</div>
<!-- 状态消息 -->
<div id="status-message" class="mt-4 hidden"></div>
</div>
<script>
const inputEl = document.getElementById('input-json');
const outputEl = document.getElementById('output-json');
const statusEl = document.getElementById('status-message');
const indentSelect = document.getElementById('indent-select');
// 示例数据
const sampleJSON = {
"name": "示例数据",
"version": "1.0.0",
"features": ["格式化", "验证", "压缩"],
"config": {
"indent": 2,
"strict": true
}
};
function showStatus(message, type = 'success') {
statusEl.textContent = message;
statusEl.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`;
statusEl.classList.remove('hidden');
setTimeout(() => statusEl.classList.add('hidden'), 3000);
}
function getIndent() {
const value = indentSelect.value;
return value === '\\t' ? '\\t' : ' '.repeat(parseInt(value));
}
// 格式化
document.getElementById('btn-format')?.addEventListener('click', () => {
try {
const input = inputEl.value.trim();
if (!input) return;
const obj = JSON.parse(input);
outputEl.value = JSON.stringify(obj, null, getIndent());
showStatus('格式化成功');
} catch (error) {
showStatus(`错误: ${error.message}`, 'error');
}
});
// 压缩
document.getElementById('btn-minify')?.addEventListener('click', () => {
try {
const input = inputEl.value.trim();
if (!input) return;
const obj = JSON.parse(input);
outputEl.value = JSON.stringify(obj);
showStatus('压缩成功');
} catch (error) {
showStatus(`错误: ${error.message}`, 'error');
}
});
// 转义
document.getElementById('btn-escape')?.addEventListener('click', () => {
try {
const input = inputEl.value.trim();
if (!input) return;
outputEl.value = JSON.stringify(input);
showStatus('转义成功');
} catch (error) {
showStatus(`错误: ${error.message}`, 'error');
}
});
// 去转义
document.getElementById('btn-unescape')?.addEventListener('click', () => {
try {
const input = inputEl.value.trim();
if (!input) return;
const parsed = JSON.parse(input);
outputEl.value = typeof parsed === 'string' ? parsed : JSON.stringify(parsed, null, getIndent());
showStatus('去转义成功');
} catch (error) {
showStatus(`错误: ${error.message}`, 'error');
}
});
// 复制
document.getElementById('btn-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(outputEl.value);
showStatus('已复制到剪贴板');
} catch {
showStatus('复制失败', 'error');
}
});
// 加载示例
document.getElementById('btn-sample')?.addEventListener('click', () => {
inputEl.value = JSON.stringify(sampleJSON, null, 2);
});
// 清空
document.getElementById('btn-clear')?.addEventListener('click', () => {
inputEl.value = '';
outputEl.value = '';
});
// 自动格式化(防抖)
let debounceTimer;
inputEl?.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
try {
const input = inputEl.value.trim();
if (input && input.length < 10000) {
JSON.parse(input);
document.getElementById('btn-format')?.click();
}
} catch {
// 输入中,忽略错误
}
}, 500);
});
</script>
</Layout>
Base64 工具实现
<!-- Base64 编码/解码工具 -->
<Layout title="Base64 编码/解码">
<!-- 类似结构,核心逻辑如下 -->
<script>
// 编码
function encode() {
const input = inputEl.value;
if (!input) return;
// 处理中文:先 UTF-8 编码,再 Base64
const utf8Bytes = new TextEncoder().encode(input);
const base64 = btoa(String.fromCharCode(...utf8Bytes));
// URL 安全模式
if (urlSafeCheck.checked) {
return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');
}
return base64;
}
// 解码
function decode() {
const input = inputEl.value;
if (!input) return;
// 还原 URL 安全模式
let base64 = input;
if (urlSafeCheck.checked) {
base64 = input.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) base64 += '=';
}
// Base64 解码,再 UTF-8 解码
const binary = atob(base64);
const bytes = new Uint8Array([...binary].map(c => c.charCodeAt(0)));
return new TextDecoder().decode(bytes);
}
</script>
</Layout>
性能优化
1. 延迟水合
使用 client:visible 只在组件可见时加载:
<!-- 当用户滚动到此处时才加载 React -->
<ComplexTool client:visible />
2. 代码分割
Astro 自动处理代码分割,每个工具页面独立打包。
3. 防抖处理
输入处理使用防抖,避免频繁计算:
let debounceTimer;
input?.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => process(), 300);
});
部署效果
- 工具列表页:
/tools- 展示所有工具 - JSON 格式化:
/tools/json-formatter - Base64 工具:
/tools/base64
所有工具:
- ✅ 零后端依赖
- ✅ 数据不上传服务器
- ✅ 响应式设计
- ✅ 深色模式支持
- ✅ 快捷键支持
总结
使用 Astro 岛屿架构构建在线工具的优势:
- 性能:静态 HTML + 按需水合,首屏快
- SEO:工具页面可被搜索引擎收录
- 安全:纯前端实现,无服务器成本
- 维护:无需担心后端漏洞、服务器宕机
适合纯前端的工具类型:
- ✅ JSON 格式化/验证
- ✅ Base64/URL 编解码
- ✅ 正则表达式测试
- ✅ 代码压缩/美化
- ✅ 哈希计算(MD5/SHA)
需要后端的工具类型:
- ❌ 需要调用外部 API(需处理 CORS)
- ❌ 需要存储用户数据
- ❌ 需要复杂计算(大文件处理)
参考链接
工具已上线,欢迎体验:my.woshicai.tech/tools