my.woshicai.tech
· 前端开发

使用 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 岛屿架构构建在线工具的优势:

  1. 性能:静态 HTML + 按需水合,首屏快
  2. SEO:工具页面可被搜索引擎收录
  3. 安全:纯前端实现,无服务器成本
  4. 维护:无需担心后端漏洞、服务器宕机

适合纯前端的工具类型:

  • ✅ JSON 格式化/验证
  • ✅ Base64/URL 编解码
  • ✅ 正则表达式测试
  • ✅ 代码压缩/美化
  • ✅ 哈希计算(MD5/SHA)

需要后端的工具类型:

  • ❌ 需要调用外部 API(需处理 CORS)
  • ❌ 需要存储用户数据
  • ❌ 需要复杂计算(大文件处理)

参考链接


工具已上线,欢迎体验:my.woshicai.tech/tools