在 Vue 3 项目里接入 pCloud,通常不是为了单纯做一个网盘界面,而是要把云存储能力嵌入到具体业务中,比如合同附件归档、审计日志备份或用户上传的敏感资料管理。官方对浏览器端直接暴露 client_secret 并不友好,更稳妥的做法是让后端完成 OAuth 2.0 授权,前端只接收短期访问令牌。本文假设你已经通过自建服务拿到了 pCloud 的 access_token,接下来重点解决三个问题:请求层如何统一维护令牌、文件如何在离开浏览器前完成加密、上传下载过程如何给用户明确的进度反馈。

下面从工程结构开始,逐步拆解一个可以在生产环境使用的 pCloud 接入方案。
一、项目初始化与依赖取舍
创建 Vite 的 Vue 3 TypeScript 模板是常见起点,执行 npm create vite@latest vue3-pcloud -- --template vue-ts 后,再安装 pinia 和 axios。这里没有引入 pCloud 官方 JavaScript SDK,原因是官方 SDK 对树摇和类型支持并不理想,而且很多能力仍然要回到 REST API 处理。直接基于 REST 接口封装一层轻量客户端,能更清楚地控制 token 注入、超时和错误码。
目录可以按 src/services/pcloud.ts 管理 API 请求,src/utils/crypto.ts 管理加密,src/composables/usePCloudUpload.ts 管理上传状态。这个分层的好处是后续如果 pCloud 换到自有后端做代理,只需要改 service 层的 baseURL 和请求头,组件层几乎不用动。
pCloud 的 API 返回结构相对统一,一般包含 result 字段,0 表示成功,非 0 对应具体错误码。把这一层类型定义清楚,能避免每个接口都重复写判断逻辑。
npm create vite@latest vue3-pcloud -- --template vue-ts cd vue3-pcloud npm install pinia axios
安装完成后,建议在 src/services/pcloud.ts 中先定义统一的响应类型和 axios 实例。下面的代码展示了如何创建请求客户端并自动附加访问令牌。
import axios, { AxiosInstance } from 'axios';
interface PCloudResponse<T> {
result: number;
metadata?: T;
error?: string;
}
const client: AxiosInstance = axios.create({
baseURL: 'https://api.pcloud.com/',
timeout: 30000
});
client.interceptors.request.use((config) => {
const token = localStorage.getItem('pcloud_access_token');
if (token) {
config.params = {
...config.params,
access_token: token
};
}
return config;
});
二、请求层与令牌管理的落地细节
访问令牌的存储位置直接关系到安全边界。短期 access_token 放在 localStorage 里虽然方便,但如果页面存在 XSS 风险,令牌就可能被脚本读取。对安全等级更高的项目,建议只用内存变量保存,刷新页面后重新走授权;对一般后台系统,短期令牌配合刷新接口已经够用。本文采用 Pinia 管理令牌状态,并在应用初始化时从本地恢复。
封装 API 方法时,不要把所有请求都写在组件里。像 listfolder、createfolder、uploadfile、getfilelink 这些接口,应当作为纯函数导出。下面以列出目录和获取下载链接为例:
export interface FolderMetadata {
folderid: number;
name: string;
contents?: Array<{ name: string; isfolder: boolean; fileid?: number }>;
}
export async function listFolder(path: string): Promise<FolderMetadata> {
const response = await client.get('listfolder', {
params: { path }
});
const data = response.data as PCloudResponse<FolderMetadata>;
if (data.result !== 0) {
throw new Error(data.error || 'listfolder failed');
}
return data.metadata as FolderMetadata;
}
export async function getFileLink(fileId: number): Promise<string> {
const response = await client.get('getfilelink', {
params: { fileid: fileId }
});
const data = response.data as PCloudResponse<{ hosts: string[]; path: string }>;
if (data.result !== 0) {
throw new Error(data.error || 'getfilelink failed');
}
const linkData = data.metadata as { hosts: string[]; path: string };
return 'https://' + linkData.hosts[0] + linkData.path;
}
这段代码把错误处理和类型收敛在 service 层,组件拿到的是已经可用的数据。返回的下载链接通常以 https:// 开头,可以直接用于预览或下载。不过要留意,pCloud 生成的直链默认带有效期,短期有效,不适合持久化存储。
401 处理是请求层不能绕开的一环。可以在 axios 响应拦截器里统一判断,如果令牌失效,就触发后端刷新令牌,并挂起期间的其他请求。这里不展开刷新队列的完整实现,核心思路是让 token 更新逻辑只发生一次,避免并发请求同时刷新。
三、用 Web Crypto API 做前端加密
pCloud 本身有客户端加密订阅,但 API 层并不会替前端完成文件加密。如果业务要求即使 pCloud 服务器被拖库,文件内容也必须是密文,那么就可以用浏览器自带的 Web Crypto API 在文件上传前进行对称加密。相比引入 crypto-js 等第三方库,原生 API 的密钥不容易被无意间暴露到全局对象,也能减少供应链风险。
加密方案采用 AES-GCM,它在保证机密性的同时提供完整性校验。密钥不直接使用用户密码,而是通过 PBKDF2 从密码加随机盐派生。这样即使同一个密码加密多个文件,每个文件的密文头部盐值不同,密钥也不同。下面是一个加密文件的实现:
async function deriveKey(password: string, salt: Uint8Array): Promise<CryptoKey> {
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 310000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
export async function encryptFile(file: File, password: string): Promise<Blob> {
const plainBuffer = await file.arrayBuffer();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const cipherBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
plainBuffer
);
const header = new Uint8Array([...salt, ...iv]);
return new Blob([header, cipherBuffer], { type: 'application/octet-stream' });
}
这里将 16 字节盐和 12 字节 IV 直接拼接在密文前面,解密时先切出前 28 字节再还原。这个方式适合中小文件,优点是只需一个 Blob 就能完成上传,不用额外维护元数据。缺点是整块读取会占用内存,如果文件超过 200MB 甚至 1GB,浏览器标签页可能崩溃。对于大文件,要改成按分块读取,并对每个分块单独加密,再把分块顺序写入新 Blob。
解密流程相反,下载到 Blob 后读取 ArrayBuffer,取出盐和 IV,再调用 crypto.subtle.decrypt。解密完的明文应当只在内存中生成 ObjectURL 供预览,不要落到 localStorage 或 IndexedDB 明文存储。
四、上传下载进度与组合式函数设计
用户上传一个大文件时,如果没有进度反馈,很容易误以为页面卡死。axios 的 onUploadProgress 和 onDownloadProgress 能提供 XMLHttpRequest 级别的进度事件。把这两个回调封装到一个组合式函数里,组件只需要读取响应式对象。
import { ref } from 'vue';
export function usePCloudUpload() {
const progress = ref(0);
const uploading = ref(false);
const error = ref<string | null>(null);
async function uploadEncryptedFile(file: File, password: string, folderId: number) {
uploading.value = true;
error.value = null;
progress.value = 0;
try {
const encryptedBlob = await encryptFile(file, password);
const form = new FormData();
form.append('folderid', String(folderId));
form.append('filename', file.name + '.enc');
form.append('file', encryptedBlob);
await client.post('uploadfile', form, {
onUploadProgress: (event) => {
if (event.total) {
progress.value = Math.round((event.loaded / event.total) * 100);
}
}
});
} catch (err) {
error.value = err instanceof Error ? err.message : 'upload failed';
} finally {
uploading.value = false;
}
}
return { progress, uploading, error, uploadEncryptedFile };
}
注意 progress.value 在计算时每次都会触发组件更新,频繁触发可能带来性能压力。对中小文件影响不大,如果是大文件,可以考虑用 requestAnimationFrame 或节流函数控制更新频率,避免拖慢主线程。
工程化还要考虑取消上传。axios 的 AbortController 可以中止请求,配合组合式函数返回 abort 方法,让用户能主动中断。同时,pCloud 对并发请求数有一定限制,如果同一页面有多个上传任务,建议维护一个简单队列,避免短时间创建过多连接。
五、安全边界与生产环境建议
把加密放在浏览器端不是万能方案。它解决的是文件离开浏览器后保持机密性的问题,但无法防止恶意客户端脚本在加密前读取明文。因此,XSS 防御、CSP 策略和依赖审计必须同步做。密码策略也要明确:PBKDF2 的迭代次数在桌面浏览器上可以承受 31 万次,但在低端移动设备上会明显变慢,做用户体验时可以在注册阶段测试设备耗时,给出合理预期。
另一个容易忽略的问题是密钥找回。如果用户忘记解密密码,前端无法恢复文件,这是端到端加密的代价。业务上需要提前告知用户,并且不要把解密密码写入服务器日志或数据库。企业场景可以选择由管理员托管恢复密钥,但这会削弱端到端加密的绝对性,需要结合合规要求评估。
最后,pCloud 的 API 直连适合内部工具或小规模应用。若产品用户量较大,建议在自有后端增加一层代理,统一处理令牌刷新、审计日志、限流和文件去重,不要把 pCloud 的访问令牌直接暴露给所有终端。这样工程化程度更高,也为后续切换云存储供应商留出空间。