开源网盘工具对比:AList / Cloudreve / go-drive
想把 Google Drive 变成自己的网盘前端,有几款成熟的开源工具可以选。 AList(最推荐,个人使用) 定位:单用户多存储聚合,支持在线播放和分享 支持的存储后端:Google Drive、OneDrive、S3、WebDAV、阿里云盘、百度网盘等,几乎覆盖所有主流云存储。 Docker 一键部署: docker run -d \ --name alist \ -p 5244:5244 \ -v /opt/alist:/opt/alist/data \ xhofe/alist:latest初始密码: docker exec -it alist ./alist admin random特点:部署极简,资源占用极低 支持视频在线播放、图片预览 支持目录密码、分享链接 WebDAV 挂载(可接 rclone、Infuse)适合:个人云盘聚合展示,或用 WebDAV 将 Google Drive 挂载到本地工具。 Cloudreve(多用户场景) 定位:完整的网盘系统,支持用户注册和管理 比 AList 功能更重,适合做小型共享网盘站:多用户注册/管理 文件分享链接(有效期、访问次数) WebDAV 支持 离线下载(Aria2 集成) 后台管理面板 存储策略(本地/OneDrive/S3/七牛等)docker run -d \ --name cloudreve \ -p 5212:5212 \ -v /opt/cloudreve:/cloudreve/uploads \ cloudreve/cloudreve:latest适合:需要用户系统、搭建私有云盘分享站的场景。 go-drive(多云聚合) 定位:专注将多个云盘聚合成统一视图 支持:Google Drive、OneDrive、Dropbox、S3、WebDAV、本地存储。 特点:前后端分离,Docker 部署 支持直接上传到各云盘(不经过中转服务器) 断点续传适合:纯聚合需求,不需要用户系统。 功能对比功能 AList Cloudreve go-driveGoogle Drive 支持 ✅ ✅ ✅多用户 ❌ ✅ ❌离线下载 ❌ ✅ ❌视频在线播放 ✅ ✅ ✅部署复杂度 低 中 低资源占用 极低 中 低选择建议个人用:AList,Docker 部署 5 分钟搞定 小团队共享:Cloudreve,有用户管理 多云盘统一入口:AList 或 go-drive 需要离线下载:Cloudreve + Aria2Google Drive 授权配置 以 AList 为例,管理后台 → 存储 → 添加 → Google Drive:在 Google Cloud Console 创建 OAuth 2.0 凭据 填入 Client ID 和 Client Secret 点击授权,完成 OAuth 流程 授权后即可访问 Google Drive 的全部内容注意:Google Drive API 有每日配额限制(默认 100 次/秒、每日 10 亿次查询),个人使用基本不会触及。
Vue 3 前端导出 Excel:xlsx + file-saver 封装 useExportExcel
安装 npm install xlsx file-saver基础用法 import * as XLSX from 'xlsx' import { saveAs } from 'file-saver'const data = [ { name: '张三', age: 18, city: '北京' }, { name: '李四', age: 20, city: '上海' } ]const ws = XLSX.utils.json_to_sheet(data) const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })saveAs(blob, '数据.xlsx')封装 useExportExcel // useExportExcel.js import * as XLSX from 'xlsx' import { saveAs } from 'file-saver' import { ref } from 'vue'export function useExportExcel() { const exporting = ref(false) const exportExcel = (data = [], fileName = '数据表', headerMap = {}) => { if (!data.length) return exporting.value = true try { // 按 headerMap 重命名列 const exportData = data.map(row => { const newRow = {} Object.keys(row).forEach(key => { newRow[headerMap[key] || key] = row[key] }) return newRow }) const ws = XLSX.utils.json_to_sheet(exportData) const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, 'Sheet1') const buf = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }) const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) saveAs(blob, `${fileName}.xlsx`) } finally { exporting.value = false } } const exportSelected = (allData, selectedRows, fileName, headerMap = {}) => { exportExcel(selectedRows?.length ? selectedRows : allData, fileName, headerMap) } return { exporting, exportExcel, exportSelected } }配合 Element Plus el-table 使用 <template> <el-button :loading="exporting" @click="onExportAll">导出全部</el-button> <el-button :loading="exporting" @click="onExportSelected">导出选中</el-button> <el-table :data="tableData" @selection-change="selectedRows = $event"> <el-table-column type="selection" /> <el-table-column prop="name" label="姓名" /> <el-table-column prop="age" label="年龄" /> <el-table-column prop="city" label="城市" /> </el-table> </template><script setup> import { ref } from 'vue' import { useExportExcel } from './useExportExcel'const tableData = ref([ { name: '张三', age: 18, city: '北京' }, { name: '李四', age: 20, city: '上海' } ])const selectedRows = ref([])const { exporting, exportExcel, exportSelected } = useExportExcel()const headerMap = { name: '姓名', age: '年龄', city: '城市' }const onExportAll = () => exportExcel(tableData.value, '用户列表', headerMap) const onExportSelected = () => exportSelected(tableData.value, selectedRows.value, '选中用户', headerMap) </script>导出 DOM table 如果页面已有 <table> 元素,不需要操作数据,直接从 DOM 生成: const table = document.getElementById('myTable') const wb = XLSX.utils.table_to_book(table) XLSX.writeFile(wb, 'table.xlsx')大数据后端流式下载 超过 5 万行时前端导出会明显卡顿(浏览器内存限制),推荐后端生成流式 Excel: const exportExcelFromServer = async (params) => { const res = await axios({ url: '/api/export', method: 'get', params, responseType: 'blob' }) const blob = new Blob([res.data]) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = '数据.xlsx' link.click() URL.revokeObjectURL(link.href) }后端(Laravel + Maatwebsite\Excel): return Excel::download(new UserExport(), 'users.xlsx');
油猴脚本获取 __webpack_require__:webpackChunk.push 注入比 Hook call 更稳
为什么 Hook Function.prototype.call 不好用 一种常见思路是拦截 webpack 模块执行时的 call: const oldCall = Function.prototype.call;Function.prototype.call = function (...args) { if (this.name == '84686') { webpackRequire = args[3]; } return oldCall.apply(this, args); };这个方法有两个核心问题:webpack 模块函数通常没有名字,this.name 是 '' 或 anonymous,而不是模块 ID,所以 this.name == '84686' 几乎永远不成立。性能灾难:现代前端框架(React、Vue、Webpack Runtime)每秒调用 call 数以万次,全部经过你的 hook,页面直接卡死。另外,如果 webpack 已经执行完才开始 hook,Function.prototype.call 根本拿不到任何东西——模块早就加载完了。 正确方案:向 webpackChunk 注入入口模块 webpack 5 会在 window 上挂一个全局数组,名字通常是 webpackChunk 加项目名前缀。向这个数组 push 一个特殊的 chunk,可以在 runtime 函数里拿到 __webpack_require__: // 找到 webpackChunk 数组 const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));if (chunkKey) { let webpackRequire; window[chunkKey].push([ [Symbol()], // chunk ID(用 Symbol 避免冲突) {}, // 模块定义(空) function (require) { webpackRequire = require; // 在这里拿到 __webpack_require__ } ]); console.log(webpackRequire); // 现在可以用 webpackRequire(模块ID) 访问任意模块 }这个方法有效的原因:即使 webpack 已经执行完,也可以通过这种方式从现有的 chunk 数组里拿到 __webpack_require__,因为 webpack 会立即处理新 push 进来的 chunk。 在 webpack 执行前 Hook(如果需要监听所有模块) 如果你的目标是在每个模块加载时做点什么(比如修改特定模块的导出),需要在 webpack 执行之前 hook push 方法: const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));const oldPush = window[chunkKey].push;window[chunkKey].push = function (...args) { const runtime = args[0][2]; // 拦截 runtime 函数 if (typeof runtime === 'function') { args[0][2] = function (__webpack_require__) { // 保存引用 window._webpackRequire = __webpack_require__; return runtime.apply(this, arguments); }; } return oldPush.apply(this, args); };用 webpack_require 访问模块 拿到 __webpack_require__ 后,可以直接访问任意 webpack 模块: // 按模块 ID 获取模块导出 const lodash = webpackRequire(96486); const mtopModule = webpackRequire(82122);console.log(lodash); console.log(mtopModule.default);模块 ID 可以在浏览器 Sources 面板里搜索特征字符串定位,也可以遍历 webpackRequire.m(模块注册表)查找: Object.keys(webpackRequire.m).forEach(id => { const src = webpackRequire.m[id].toString(); if (src.includes('targetFunction')) { console.log('found at module id:', id); } });油猴脚本注意:@run-at 时机 在 Tampermonkey 里使用时,需要根据目的选择合适的 @run-at:时机 说明document-start 最早,适合在 webpack 执行前 hook pushdocument-end 适合页面加载完后注入 chunk 拿引用如果 webpack 在 DOMContentLoaded 之前执行完,用 document-start + push hook;如果只是要访问模块,用 document-end + push 注入即可。
分页 API 全量遍历:Python 与 JavaScript 实现模式
分页接口通常返回 totalCount、pageSize、当页数据。需要全量数据时,用 while 循环按页请求即可。 Python 实现 按 totalCount 推进(稳定写法) import mathpage_size = 20 current = 1 all_items = []while True: params = { "status": "ON_SALE", "current": current, "pageSize": page_size } response = fetchProductList(params) data = response.get("model", []) all_items.extend(data) total_count = response.get("totalCount", 0) total_pages = math.ceil(total_count / page_size) print(f"已获取第 {current}/{total_pages} 页") if current >= total_pages: break current += 1print(f"总共获取 {len(all_items)} 条")按当页数量判断末页(防 totalCount 不准) 如果接口的 totalCount 有时不准确,用当页返回数量是否小于 pageSize 来判断末页更稳: page_size = 20 current = 1 all_items = []while True: params = {"status": "ON_SALE", "current": current, "pageSize": page_size} response = fetchProductList(params) items = response.get("model", []) if not items: break all_items.extend(items) print(f"获取第 {current} 页,{len(items)} 条") if len(items) < page_size: break current += 1生成器模式(内存友好) 适合商品数量很大、需要边翻页边处理的场景: def iter_products(): current = 1 page_size = 20 while True: resp = fetchProductList({ "status": "ON_SALE", "current": current, "pageSize": page_size }) items = resp.get("model", []) if not items: break yield from items if len(items) < page_size: break current += 1for item in iter_products(): print(item["title"])JavaScript 实现 async/await 版本 async function getAllProducts() { const pageSize = 20; let current = 1; const allItems = []; while (true) { const response = await fetchProductList({ status: "ON_SALE", current, pageSize }); const items = response?.model || []; if (items.length === 0) { break; } allItems.push(...items); console.log(`获取第 ${current} 页,${items.length} 条`); if (items.length < pageSize) { break; } current++; } console.log(`总共获取 ${allItems.length} 条`); return allItems; }const products = await getAllProducts();按 totalCount 计算总页数 async function getAllProducts() { const pageSize = 20; let current = 1; let totalPages = 1; const allItems = []; while (current <= totalPages) { const response = await fetchProductList({ status: "ON_SALE", current, pageSize }); const items = response?.model || []; allItems.push(...items); totalPages = Math.ceil((response.totalCount || 0) / pageSize); console.log(`获取第 ${current}/${totalPages} 页`); current++; } return allItems; }流式处理(不存全量数据) 适合直接对每条数据执行操作(改价格、同步库存等),避免一次性把几千条数据加载到内存: let current = 1; const pageSize = 20;while (true) { const response = await fetchProductList({ status: "ON_SALE", current, pageSize }); const items = response?.model || []; if (!items.length) { break; } for (const item of items) { // 在这里处理每条商品 await updateStock(item.id, item.quantity); } if (items.length < pageSize) { break; } current++; }两种终止条件对比策略 适用场景 风险current >= totalPages totalCount 准确 totalCount 不准时多/少请求items.length < pageSize 更通用 最后一页恰好整除时多一次空请求两种可以结合使用:先按 totalPages 推进,同时检查当页数量作为保险。
API 分页设计:Offset vs Cursor(Keyset)怎么选
后端接口拉列表几乎都要分页。最常见的是 current + pageSize: { "current": 1, "pageSize": 20 }简单直观,但数据量大 + 频繁翻页 + 数据在变化的场景下有致命问题。生产上大厂 API(GitHub、Slack、Stripe)都在推 Cursor 分页。 Offset 分页的两个坑 1. 深度分页越来越慢 SQL 长这样: SELECT * FROM products ORDER BY created_at DESC LIMIT 20 OFFSET 1000000;数据库要扫过前 100 万行才能扔掉、再取 20 行。1000 万条数据翻到第 500 页,实测能到秒级。 原因:MySQL 不知道"跳过 100 万行"能不能走索引——即使能,也要真的扫过去。 2. 数据漂移 分页过程中数据在增删——用户看到重复或者漏掉。 例子:翻第 1 页看到 20 条 → 有人删了第 5 条 → 翻第 2 页时,原本第 21 条变成了第 20 条,被跳过。 对于列表实时更新的场景(订单、动态、评论流),Offset 分页几乎必然会有这种漏项。 Cursor 分页(Keyset) 思路:用"上一页最后一条的位置"作为下一页起点,不用 offset。 -- 第一页 SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT 20;-- 第二页(用上一页最后一条的 created_at + id 作为起点) SELECT * FROM products WHERE (created_at, id) < ('2026-06-04 13:00:00', 998765) ORDER BY created_at DESC, id DESC LIMIT 20;关键点:有索引的排序字段(created_at、id) 元组比较处理并列时间 走索引 range scan,速度不随深度衰减接口设计 Offset 版本: // 请求 { "current": 3, "pageSize": 20 }// 响应 { "list": [...], "total": 12345, "current": 3, "pageSize": 20 }Cursor 版本: // 请求 { "cursor": "eyJ0IjoxNzE3NDg...", "pageSize": 20 }// 响应 { "list": [...], "nextCursor": "eyJ0IjoxNzE3NDg2...", "hasMore": true }cursor 通常是 Base64 编码的 {time, id} 之类的组合,客户端不需要理解内容,直接透传。 Cursor 具体怎么生成 import base64, jsondef encode_cursor(last_row): payload = {"t": last_row.created_at.isoformat(), "id": last_row.id} return base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()def decode_cursor(cursor): return json.loads(base64.urlsafe_b64decode(cursor))# API 侧 if cursor: c = decode_cursor(cursor) rows = db.execute(""" SELECT * FROM products WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT ? """, [c["t"], c["id"], page_size]) else: rows = db.execute(""" SELECT * FROM products ORDER BY created_at DESC, id DESC LIMIT ? """, [page_size])next_cursor = encode_cursor(rows[-1]) if len(rows) == page_size else None两者对比维度 Offset Cursor简单度 ✅ 直观 需要理解元组比较深度分页性能 ❌ 越来越慢 ✅ 常数时间支持随机跳页 ✅ 可以直接第 N 页 ❌ 只能顺序翻数据变化时稳定 ❌ 漏项 / 重复 ✅ 稳定需要 total 计数 ✅ 好算 ❌ 通常不返回支持排序 任意 排序字段必须唯一 + 有索引什么时候用哪个 用 Offset:后台管理系统的表格(数据不常变、用户会跳页) 数据总量不大(几千到几万条) 客户端要显示"第 5 / 100 页"这种明确导航用 Cursor:时间流列表(订单、聊天、动态、日志) 数据量大(十万+) 客户端做"上拉加载更多"(不需要跳页) 数据实时增删两个都做:GitHub API 大多支持 ?page=N(Offset)和 ?before=cursor(Cursor),场景多的时候都提供。 顺带:total 慢查询 Offset 分页返回 total 时,SELECT COUNT(*) 在千万级表上也会慢。优化:缓存 total(Redis,几分钟一次异步刷新) 估算 total(PostgreSQL pg_class.reltuples、MySQL INFORMATION_SCHEMA.TABLES) 只算前几页的精确 total,之后返回 total: null 或 hasMore: true 就够一句话总结 大数据 + 时间流用 Cursor(速度不衰减、结果稳定)、后台管理小表格用 Offset(简单、支持跳页)。别把 Offset 用在千万级实时数据的 API 上。
