浏览器API完全指南:FileReader、Canvas、Web Workers深度解析
说个让我至今记忆犹新的事。前年做一个内部工具,有个"一键复制配置"的按钮,本地测得好好的,上线第二天就被同事追着问"为什么点了没反应"。我盯着代码看了半天没看出问题——后来才反应过来,测试环境我开的是 localhost,浏览器把它当安全上下文,navigator.clipboard 正常工作;可生产环境用的是公司内网的 HTTP 域名,navigator.clipboard 直接是 undefined。那天我才真正记住一件事:很多浏览器 API 不是"能不能用"的问题,而是"在什么环境下才让你用"。
后来踩的坑只多不少。localStorage 存个 base64 缩略图缓存,存到第三十几张直接 QuotaExceededError,整个保存逻辑崩了还没 try-catch;geolocation 想做个"附近门店",结果上线一周看数据,九成用户直接点了"拒绝",那个功能基本等于没做;还有一次滚动监听写得太天真,列表一长滚动就卡成 PPT,最后换成 IntersectionObserver 才算救回来。
这篇就把我这些年在浏览器 API 上真正用熟、也真正栽过跟头的东西攒一块写下来——FileReader、Canvas、Web Workers、IndexedDB、Clipboard 这些。能用的写法都给你,坑也都标出来,你别再像我当年那样上线了才发现。
目录
先把存储方案捋清楚
讲具体 API 之前,我想先把"本地存哪儿"这个问题摊开说。这几个东西名字都带"存储",但用错地方真的会很难受,下面这张表是我自己踩过坑之后才总结明白的:
| 方案 | 容量 | 同步/异步 | 关掉标签页还在吗 | 我一般拿来干嘛 | 踩坑点 |
|---|---|---|---|---|---|
| localStorage | 约 5MB(各家不一) | 同步 | 在 | 主题、语言、小配置 | 同步阻塞主线程;超额直接抛 QuotaExceededError,别忘了 try-catch |
| sessionStorage | 约 5MB | 同步 | 不在(关页就没) | 一次性的表单草稿、单次流程状态 | 同一个标签页刷新还在,但用户开新标签页就是空的,别指望它跨页共享 |
| IndexedDB | 几百 MB 到上 GB | 异步 | 在 | 大量结构化数据、离线缓存、图片 Blob | API 啰嗦得让人想哭,事务一不小心就 auto-commit 了,建议套一层 Promise 封装 |
| Cookie | 约 4KB | 同步 | 看 expires | 需要随请求带给服务端的东西 | 每次 HTTP 请求都带上,存多了纯属给带宽找罪受,别拿它当存储用 |
一句话经验:要随请求发给后端才用 Cookie,存一点点就 localStorage,量大或者要离线就上 IndexedDB。我见过有人拿 localStorage 缓存几 MB 的 JSON,结果首屏 JSON.parse 卡半秒,那场面真不忍直视。
File API文件处理
FileReader基础
// FileReader用于读取文件内容
class FileProcessor {
// 读取为文本
static readAsText(file, encoding = 'UTF-8') {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsText(file, encoding);
});
}
// 读取为DataURL(Base64)
static readAsDataURL(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// 读取为ArrayBuffer(二进制)
static readAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
}
// 使用
const file = document.querySelector('#fileInput').files[0];
// 读取文本文件
const text = await FileProcessor.readAsText(file);
console.log(text);
// 读取图片为Base64
const dataUrl = await FileProcessor.readAsDataURL(file);
document.querySelector('img').src = dataUrl;
文件上传进度监控
function uploadWithProgress(file, url, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// 监听上传进度
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`上传失败: ${xhr.status}`));
}
});
xhr.addEventListener('error', reject);
xhr.open('POST', url);
xhr.send(formData);
});
}
// 使用
uploadWithProgress(file, '/api/upload', (progress) => {
console.log(`上传进度: ${progress.toFixed(2)}%`);
progressBar.style.width = `${progress}%`;
});
大文件分片上传
class ChunkedUploader {
constructor(file, chunkSize = 1024 * 1024) {
this.file = file;
this.chunkSize = chunkSize;
this.chunks = Math.ceil(file.size / chunkSize);
}
async upload(url) {
for (let i = 0; i < this.chunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.file.size);
const chunk = this.file.slice(start, end);
await this.uploadChunk(chunk, i, url);
console.log(`上传进度: ${(((i + 1) / this.chunks) * 100).toFixed(2)}%`);
}
// 通知服务器合并文件
await this.mergeChunks(url);
}
async uploadChunk(chunk, index, url) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', index);
formData.append('filename', this.file.name);
formData.append('totalChunks', this.chunks);
const response = await fetch(url, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`分片${index}上传失败`);
}
}
async mergeChunks(url) {
const response = await fetch(`${url}/merge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: this.file.name,
totalChunks: this.chunks,
}),
});
return response.json();
}
}
// 使用
const uploader = new ChunkedUploader(largeFile, 1024 * 1024); // 1MB分片
await uploader.upload('/api/upload');
Blob对象操作
// 创建Blob
const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
// Blob转文本
async function blobToText(blob) {
return await blob.text();
}
// Blob转ArrayBuffer
async function blobToArrayBuffer(blob) {
return await blob.arrayBuffer();
}
// 下载Blob
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// ArrayBuffer转Blob
function arrayBufferToBlob(buffer, mimeType) {
return new Blob([buffer], { type: mimeType });
}
// 合并多个Blob
function mergeBlobs(blobs, mimeType) {
return new Blob(blobs, { type: mimeType });
}
Canvas绘图API
Canvas基础
class CanvasDrawing {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
// 绘制矩形
drawRect(x, y, width, height, color) {
this.ctx.fillStyle = color;
this.ctx.fillRect(x, y, width, height);
}
// 绘制圆形
drawCircle(x, y, radius, color) {
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2);
this.ctx.fill();
}
// 绘制文字
drawText(text, x, y, options = {}) {
const { fontSize = 16, fontFamily = 'Arial', color = 'black', align = 'left' } = options;
this.ctx.font = `${fontSize}px ${fontFamily}`;
this.ctx.fillStyle = color;
this.ctx.textAlign = align;
this.ctx.fillText(text, x, y);
}
// 绘制线条
drawLine(x1, y1, x2, y2, color = 'black', width = 1) {
this.ctx.strokeStyle = color;
this.ctx.lineWidth = width;
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
// 清空画布
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
// 使用
const canvas = document.querySelector('#myCanvas');
const drawing = new CanvasDrawing(canvas);
drawing.drawRect(50, 50, 100, 100, 'blue');
drawing.drawCircle(200, 100, 50, 'red');
drawing.drawText('Hello Canvas!', 100, 200, { fontSize: 24 });
图片处理
class ImageProcessor {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
}
// 加载图片
async loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
// 绘制图片
drawImage(img, x = 0, y = 0, width, height) {
if (width && height) {
this.ctx.drawImage(img, x, y, width, height);
} else {
this.ctx.drawImage(img, x, y);
}
}
// 获取像素数据
getImageData(x = 0, y = 0, width, height) {
width = width || this.canvas.width;
height = height || this.canvas.height;
return this.ctx.getImageData(x, y, width, height);
}
// 修改像素数据
putImageData(imageData, x = 0, y = 0) {
this.ctx.putImageData(imageData, x, y);
}
// 灰度滤镜
grayscale() {
const imageData = this.getImageData();
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
}
this.putImageData(imageData);
}
// 反色滤镜
invert() {
const imageData = this.getImageData();
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Red
data[i + 1] = 255 - data[i + 1]; // Green
data[i + 2] = 255 - data[i + 2]; // Blue
}
this.putImageData(imageData);
}
// 亮度调整
brightness(value) {
const imageData = this.getImageData();
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] += value; // Red
data[i + 1] += value; // Green
data[i + 2] += value; // Blue
}
this.putImageData(imageData);
}
// 导出为Blob
async toBlob(type = 'image/png', quality = 0.92) {
return new Promise((resolve) => {
this.canvas.toBlob(resolve, type, quality);
});
}
}
// 使用
const processor = new ImageProcessor(canvas);
const img = await processor.loadImage('photo.jpg');
processor.drawImage(img, 0, 0, canvas.width, canvas.height);
processor.grayscale(); // 应用灰度滤镜
const blob = await processor.toBlob('image/jpeg', 0.8);
Canvas动画
class CanvasAnimation {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.animationId = null;
this.isRunning = false;
}
start(callback) {
this.isRunning = true;
const animate = (timestamp) => {
if (!this.isRunning) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
callback(timestamp);
this.animationId = requestAnimationFrame(animate);
};
this.animationId = requestAnimationFrame(animate);
}
stop() {
this.isRunning = false;
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
}
}
// 使用:弹跳球动画
const animation = new CanvasAnimation(canvas);
let x = 50;
let y = 50;
let vx = 2;
let vy = 3;
const radius = 20;
animation.start(() => {
// 绘制球
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
// 更新位置
x += vx;
y += vy;
// 碰撞检测
if (x + radius > canvas.width || x - radius < 0) {
vx = -vx;
}
if (y + radius > canvas.height || y - radius < 0) {
vy = -vy;
}
});
Web Workers多线程
基础使用
// main.js
const worker = new Worker('worker.js');
// 发送消息给Worker
worker.postMessage({ type: 'start', data: [1, 2, 3, 4, 5] });
// 接收Worker的消息
worker.onmessage = (e) => {
console.log('Worker返回:', e.data);
};
worker.onerror = (error) => {
console.error('Worker错误:', error);
};
// 终止Worker
// worker.terminate();
// worker.js
self.onmessage = (e) => {
const { type, data } = e.data;
if (type === 'start') {
// 执行耗时计算
const result = heavyCalculation(data);
// 返回结果
self.postMessage({ type: 'result', data: result });
}
};
function heavyCalculation(data) {
// 模拟耗时计算
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
实用案例:大数据排序
// main.js
class WorkerPool {
constructor(workerScript, size = 4) {
this.workers = Array(size)
.fill(null)
.map(() => new Worker(workerScript));
this.queue = [];
this.activeWorkers = new Set();
}
async execute(data) {
return new Promise((resolve, reject) => {
const task = { data, resolve, reject };
const worker = this.getAvailableWorker();
if (worker) {
this.runTask(worker, task);
} else {
this.queue.push(task);
}
});
}
getAvailableWorker() {
return this.workers.find((w) => !this.activeWorkers.has(w));
}
runTask(worker, task) {
this.activeWorkers.add(worker);
worker.onmessage = (e) => {
task.resolve(e.data);
this.activeWorkers.delete(worker);
// 处理队列中的下一个任务
if (this.queue.length > 0) {
const nextTask = this.queue.shift();
this.runTask(worker, nextTask);
}
};
worker.onerror = (error) => {
task.reject(error);
this.activeWorkers.delete(worker);
};
worker.postMessage(task.data);
}
terminate() {
this.workers.forEach((w) => w.terminate());
}
}
// 使用
const pool = new WorkerPool('sort-worker.js', 4);
const arrays = [
[5, 2, 8, 1, 9],
[3, 7, 4, 6, 0],
// ... 更多数组
];
const results = await Promise.all(arrays.map((arr) => pool.execute(arr)));
console.log('所有排序完成:', results);
// sort-worker.js
self.onmessage = (e) => {
const data = e.data;
const sorted = data.sort((a, b) => a - b);
self.postMessage(sorted);
};
Shared Worker(共享Worker)
// shared-worker.js
const connections = [];
self.onconnect = (e) => {
const port = e.ports[0];
connections.push(port);
port.onmessage = (event) => {
// 广播消息给所有连接
connections.forEach((p) => {
p.postMessage(event.data);
});
};
port.start();
};
// page1.js
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = (e) => {
console.log('收到消息:', e.data);
};
worker.port.postMessage('来自页面1的消息');
IndexedDB本地存储
基础操作
class IndexedDBManager {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open(stores) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (e) => {
const db = e.target.result;
stores.forEach(({ name, options }) => {
if (!db.objectStoreNames.contains(name)) {
db.createObjectStore(name, options);
}
});
};
});
}
async add(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async update(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// 使用
const db = new IndexedDBManager('MyDatabase');
await db.open([
{
name: 'users',
options: { keyPath: 'id', autoIncrement: true },
},
{
name: 'posts',
options: { keyPath: 'id', autoIncrement: true },
},
]);
// 添加数据
await db.add('users', { name: 'Alice', email: 'alice@example.com' });
// 获取数据
const user = await db.get('users', 1);
// 获取所有数据
const allUsers = await db.getAll('users');
索引查询
class IndexedDBWithIndex extends IndexedDBManager {
async createIndex(storeName, indexName, keyPath, options = {}) {
// 需要在onupgradeneeded中创建索引
// 这里展示如何使用已存在的索引查询
}
async getByIndex(storeName, indexName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAllByIndex(storeName, indexName, query) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(query);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
// 创建数据库时添加索引
const request = indexedDB.open('MyDatabase', 2);
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
// 创建索引
store.createIndex('email', 'email', { unique: true });
store.createIndex('name', 'name', { unique: false });
}
};
// 通过email索引查询
const user = await db.getByIndex('users', 'email', 'alice@example.com');
Clipboard API剪贴板
读写剪贴板
class ClipboardManager {
// 复制文本到剪贴板
static async copyText(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.error('复制失败:', error);
return false;
}
}
// 读取剪贴板文本
static async pasteText() {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (error) {
console.error('粘贴失败:', error);
return null;
}
}
// 复制图片到剪贴板
static async copyImage(blob) {
try {
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
return true;
} catch (error) {
console.error('复制图片失败:', error);
return false;
}
}
// 读取剪贴板图片
static async pasteImage() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
if (type.startsWith('image/')) {
const blob = await item.getType(type);
return blob;
}
}
}
return null;
} catch (error) {
console.error('粘贴图片失败:', error);
return null;
}
}
}
// 使用
// 复制文本
await ClipboardManager.copyText('Hello, Clipboard!');
// 粘贴文本
const text = await ClipboardManager.pasteText();
// 复制图片
const imageBlob = await fetch('image.jpg').then((r) => r.blob());
await ClipboardManager.copyImage(imageBlob);
// 粘贴图片
const pastedImage = await ClipboardManager.pasteImage();
if (pastedImage) {
const url = URL.createObjectURL(pastedImage);
document.querySelector('img').src = url;
}
其他实用API
Geolocation API(地理位置)
class GeolocationHelper {
static async getCurrentPosition() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
});
},
reject,
{ enableHighAccuracy: true, timeout: 5000 }
);
});
}
static watchPosition(callback) {
return navigator.geolocation.watchPosition(
(position) => {
callback({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
console.error,
{ enableHighAccuracy: true }
);
}
}
// 使用
const position = await GeolocationHelper.getCurrentPosition();
console.log(`位置: ${position.latitude}, ${position.longitude}`);
Notification API(通知)
class NotificationHelper {
static async requestPermission() {
if (!('Notification' in window)) {
throw new Error('浏览器不支持通知');
}
const permission = await Notification.requestPermission();
return permission === 'granted';
}
static show(title, options = {}) {
if (Notification.permission === 'granted') {
return new Notification(title, options);
}
}
}
// 使用
const granted = await NotificationHelper.requestPermission();
if (granted) {
NotificationHelper.show('新消息', {
body: '您有一条新消息',
icon: '/icon.png',
tag: 'message-1',
});
}
Battery API(电池状态)
async function getBatteryInfo() {
if (!('getBattery' in navigator)) {
console.log('不支持Battery API');
return;
}
const battery = await navigator.getBattery();
console.log(`电量: ${battery.level * 100}%`);
console.log(`充电中: ${battery.charging}`);
console.log(`充电时间: ${battery.chargingTime}秒`);
console.log(`放电时间: ${battery.dischargingTime}秒`);
// 监听电量变化
battery.addEventListener('levelchange', () => {
console.log(`电量变化: ${battery.level * 100}%`);
});
}
写在最后
写到这儿,我最想说的其实不是哪个 API 多强,而是:浏览器现在能干的事,比大多数人想象的多得多。图片要灰度、要压缩,没必要传后端转一圈,Canvas 自己就办了;几万条数据要在前端筛选排序,丢给 Web Worker,主线程一点都不卡;要离线、要存大文件,IndexedDB 顶得住。这几年我做工具越来越倾向"能在浏览器里干完就别麻烦服务器",开发省事,用户也快。
要说我个人最看重的好处,是隐私。说实话,文件转换、图片处理这类东西,用户最担心的就是"我的文件是不是被你上传走了"。我们 ToolsForge 上的工具基本都是纯浏览器跑的——文件读进 FileReader、在 Canvas 里处理完、用 IndexedDB 缓存,全程数据不出你这台机器,根本没有上传这一步。这事儿不是营销话术,就是这些 API 天然带来的:本地能算明白的,干嘛要传出去。
还有几条踩过坑才记牢的提醒,顺手留给你:
- 凡是
navigator.clipboard、geolocation这种,先想清楚安全上下文(HTTPS / localhost),不然生产环境一上就翻车; localStorage写入永远当它会失败,QuotaExceededError比你想的来得早;geolocation、Notification这类要权限的,做好"用户就是不给"的降级,别让整个功能卡死;- 长列表别再手写 scroll 监听了,
IntersectionObserver真香。
好了,就唠到这。想直接上手感受一下"全程不上传"的,去站里随便点个工具试试,比看十篇文章实在。
相关工具
- 📁 文件处理工具 - 多种文件格式转换
- 🎨 Canvas图片编辑器 - 在线图片编辑
- 📋 剪贴板工具 - 剪贴板增强
关键词: 浏览器API, File API, Canvas, Web Workers, IndexedDB, Clipboard API
更新时间: 2026-01-05
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们