• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

zzaz

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

拆解一款在线实用工具箱:前端实现与工程化思路

作为常年折腾在线工具的开发者,我最近研究了bbab.net/tools这款主打「本地处理、零数据上传」的在线工具箱,是工作效率有利的提升助手。翻了它的打包产物和网络请求后,挖到不少能直接复用的工程化细节,今天就分享给技术社区的朋友们。

一、整体架构:为什么选「纯前端核心+轻量后端」?

  • 从网络请求和产物分析,这个站点的核心工具逻辑全在前端本地运行,后端只负责静态资源托管和极简的访问统计,这也是它能做到「零数据上传」的核心原因
  • 技术栈选型:原生JS + Tailwind CSS + Vite构建,没有用React/Vue这类重型框架,这是它首屏加载速度能做到300ms内的关键
  • 架构设计的巧思:工具模块采用插件化拆分,每个工具是独立的代码块,实现了真正的按需加载,不会一次性加载所有工具的代码

二、前端工程化:小项目也能做的模块化设计

从打包后的chunk文件推测,项目目录按「工具类型+公共模块」拆分,同时在Vite里做了精准的代码分割配置:

// vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from 'tailwindcss'

export default defineConfig({
  plugins: [tailwindcss()],
  build: {
    rollupOptions: {
      output: {
        // 按工具模块拆分chunk,实现按需加载
        manualChunks: (id) => {
          if (id.includes('src/tools/image')) return 'image-tools'
          if (id.includes('src/tools/text')) return 'text-tools'
          if (id.includes('src/tools/office')) return 'office-tools'
          if (id.includes('src/utils')) return 'utils'
        }
      }
    }
  }
})
  • 项目目录结构清晰,每个工具模块独立成文件,便于维护:
    src/
    ├── utils/          # 公共工具函数(防抖、格式化、存储)
    ├── components/     # 通用UI组件(导航、工具卡片、模态框)
    ├── tools/          # 各工具模块(image、text、office、network)
    ├── worker/         # Web Worker代码(大文件处理)
    └── main.js         # 入口文件
    
  • 代码规范:用ESLint + Prettier做校验,从打包后的变量命名和注释来看,遵循了前端工程化的基本规范,没有冗余代码

三、核心功能实现:本地处理的代码要点

1. 图片工具:Canvas API的实际应用

图片压缩功能的核心代码,通过Canvas实现等比缩放和质量压缩,同时兼容不同浏览器:

// utils/image-compress.js
export function compressImage(file, quality = 0.8) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      // 解决跨域问题
      img.crossOrigin = 'anonymous';
      img.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        // 等比缩放,限制最大尺寸为1920*1080
        const scale = Math.min(1920 / img.width, 1080 / img.height, 1);
        canvas.width = img.width * scale;
        canvas.height = img.height * scale;
        // 绘制图片,添加轻微模糊优化压缩效果
        ctx.filter = 'blur(0.5px)';
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        // 兼容旧浏览器的toBlob API
        if (canvas.toBlob) {
          canvas.toBlob(resolve, 'image/jpeg', quality);
        } else {
          const dataURL = canvas.toDataURL('image/jpeg', quality);
          const blob = dataURLToBlob(dataURL);
          resolve(blob);
        }
      };
      img.onerror = reject;
      img.src = e.target.result;
    };
    reader.readAsDataURL(file);
  });
}

// 把base64转成blob
function dataURLToBlob(dataURL) {
  const arr = dataURL.split(',');
  const mime = arr[0].match(/:(.*?);/)[1];
  const bstr = atob(arr[1]);
  let n = bstr.length;
  const u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new Blob([u8arr], { type: mime });
}

2. 文本工具:高效的字符串处理

JSON格式化功能的核心代码,用原生API配合正则实现,没有依赖第三方库:

// utils/json-format.js
export function formatJSON(jsonStr) {
  try {
    const obj = JSON.parse(jsonStr);
    // 用原生JSON.stringify实现格式化,同时添加缩进
    return JSON.stringify(obj, null, 2);
  } catch (err) {
    // 格式化失败时返回原始字符串并标记错误
    return `// 格式错误: ${err.message}\n${jsonStr}`;
  }
}

// 代码高亮的极简实现
export function highlightCode(code, lang = 'javascript') {
  // 匹配关键词、字符串、注释
  const keywordRegex = /(function|const|let|var|if|else|for|while|return|class)/g;
  const stringRegex = /(".*?"|'.*?')/g;
  const commentRegex = /(\/\/.*?$|\/\*[\s\S]*?\*\/)/gm;
  
  let highlighted = code
    .replace(commentRegex, '<span class="text-gray-500">$1</span>')
    .replace(stringRegex, '<span class="text-green-600">$1</span>')
    .replace(keywordRegex, '<span class="text-blue-600">$1</span>');
  
  return `<pre class="bg-gray-100 p-4 rounded"><code>${highlighted}</code></pre>`;
}

3. 性能优化:本地缓存与懒加载

用localStorage缓存用户的工具使用记录和搜索结果,减少重复请求:

// utils/storage.js
export const Storage = {
  set(key, value, expire = 7 * 24 * 60 * 60 * 1000) {
    const data = {
      value,
      expire: Date.now() + expire
    };
    localStorage.setItem(key, JSON.stringify(data));
  },
  get(key) {
    const data = localStorage.getItem(key);
    if (!data) return null;
    const { value, expire } = JSON.parse(data);
    if (Date.now() > expire) {
      localStorage.removeItem(key);
      return null;
    }
    return value;
  }
};

// 工具卡片懒加载实现
export function lazyLoadImages() {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.classList.remove('opacity-0');
        observer.unobserve(img);
      }
    });
  });
  
  document.querySelectorAll('[data-src]').forEach(img => {
    observer.observe(img);
  });
}

四、核心工具模块:几个值得复用的实现逻辑

网络Ping工具:前端无后端实现

通过请求静态资源计算Ping值,无需后端接口:

// tools/network-ping.js
export function getPing() {
  return new Promise((resolve) => {
    const startTime = Date.now();
    // 请求一个极小的静态资源
    fetch('/favicon.ico', { cache: 'no-cache' })
      .then(() => {
        const ping = Date.now() - startTime;
        resolve(ping);
      })
      .catch(() => {
        resolve('检测失败');
      });
  });
}

五、技术难点与解决方案:小项目也会遇到的坑

1. 大文件处理:Web Worker避免主线程阻塞

处理10MB以上的文件时,用Web Worker离线处理,避免页面卡顿:

// worker/image-worker.js
self.onmessage = (e) => {
  const { file, quality } = e.data;
  const reader = new FileReader();
  
  reader.onload = (event) => {
    const img = new Image();
    img.onload = () => {
      // 使用OffscreenCanvas,性能更好
      const canvas = new OffscreenCanvas(img.width * 0.5, img.height * 0.5);
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      
      canvas.convertToBlob({ type: 'image/jpeg', quality })
        .then(blob => {
          self.postMessage(blob);
        });
    };
    img.src = event.target.result;
  };
  
  reader.readAsDataURL(file);
};

// 主线程调用Worker
// utils/worker-helper.js
export function useWorker(workerPath, data) {
  return new Promise((resolve) => {
    const worker = new Worker(workerPath);
    worker.postMessage(data);
    worker.onmessage = (e) => {
      resolve(e.data);
      worker.terminate();
    };
  });
}

六、后端与部署:低成本的运维方案

后端只用了一个极简的Go服务做统计,代码不到100行:

// main.go
package main

import (
	"encoding/json"
	"net/http"
	"os"
	"sync"
)

type Stats struct {
	ToolHits map[string]int `json:"tool_hits"`
	mu       sync.Mutex
}

func main() {
	stats := &Stats{ToolHits: make(map[string]int)}
	stats.loadStats()

	// 统计工具访问次数
	http.HandleFunc("/api/stats", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPost {
			var req struct {
				Tool string `json:"tool"`
			}
			json.NewDecoder(r.Body).Decode(&req)
			
			stats.mu.Lock()
			stats.ToolHits[req.Tool]++
			stats.mu.Unlock()
			stats.saveStats()
			
			w.WriteHeader(http.StatusOK)
			json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
		}
	})

	http.Handle("/", http.FileServer(http.Dir("dist")))
	http.ListenAndServe(":8080", nil)
}

// 加载已有统计数据
func (s *Stats) loadStats() {
	data, err := os.ReadFile("stats.json")
	if err != nil {
		return
	}
	json.Unmarshal(data, s)
}

// 保存统计数据到文件
func (s *Stats) saveStats() {
	data, _ := json.Marshal(s)
	os.WriteFile("stats.json", data, 0644)
}
  • 部署方案:静态资源放到CDN,Go服务部署在轻量云服务器上,每月成本不到50元,也可以用Cloudflare Pages托管静态资源,进一步降低成本

七、总结

这个站点的技术实现没有什么黑科技,但胜在把基础技术用到了极致,用极简的技术栈实现了高性能、高隐私的在线工具站。如果你也想做一款轻量的在线工具站,不妨参考bbab.net/tools的实现思路,用基础技术堆出靠谱的产品。

posted on 2026-03-05 20:52  独立开发者+  阅读(3)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3