拆解一款在线实用工具箱:前端实现与工程化思路
作为常年折腾在线工具的开发者,我最近研究了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的实现思路,用基础技术堆出靠谱的产品。
浙公网安备 33010602011771号