Github 仓库地址:github.com/hahala2333/…
本教程面向前端开发者,旨在帮助你从 0 到 1,快速搭建一个可重复使用的、带有 CLI 和编程化接口的 OSS 上传工具。
🎯 设计思路与项目规划
- 功能需求
-
- 通过配置文件或命令行传入阿里云 OSS 凭证及参数;
- 自动加载
oss-uploader.config.cjs - 提供命令行工具(
init、upload)和编程接口uploadStatic。
- 模块划分
-
- 配置层:生成/加载默认配置文件、合并命令行/API 参数;
- 核心逻辑层:校验参数、递归遍历本地目录、调用
ali-ossSDK 上传; - 接口层:CLI (
commander实现) 与编程接口。
- 扩展与集成
-
- 可在 Vite、Webpack 等打包后钩子(
closeBundle)中调用; - 易于集成到 CI/CD 脚本或其它自动化流程;
- 后续可添加并发控制、进度条、上传钩子等功能。
- 可在 Vite、Webpack 等打包后钩子(
📂 目录结构
oss-uploader/
├─ src/
│ ├─ index.ts # 核心逻辑
│ └─ cli.ts # CLI 实现
├─ dist/ # 编译输出目录
├─ oss-uploader.config.cjs # 用户配置文件模板
├─ package.json # 项目元信息及脚本
├─ tsconfig.json # TypeScript 配置
└─ README.md # 项目说明文档
一、前置准备
- Node.js 环境:建议使用 Node 14+,本文中以 Node 20.11 为例。
- 包管理器:npm 或 Yarn。
- 阿里云 OSS 账号,并确保拥有对应
Bucket的读写权限。
二、项目初始化
# 创建项目目录
mkdir custom-oss-uploader && cd custom-oss-uploader
# 初始化 npm
npm init -y
# 安装运行时依赖
npm install ali-oss lodash.merge fs-extra commander
# 安装开发时依赖
npm install -D typescript @types/node @types/fs-extra @types/commander
ali-oss:阿里云 OSS SDKlodash.merge:深度合并配置fs-extra:增强版文件系统操作commander:构建 CLI
三、TypeScript 配置
在项目根目录新建 tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
module: CommonJS兼容大多数项目preserveShebang保留 CLI 声明行
四、实现核心逻辑(src/index.ts)
- 核心逻辑可以拆分为以下几步:
- 加载配置
-
- 合并参数:默认值 < 配置文件 < 命令行/调用时传入
- 先从项目根目录同步读取用户写好的
oss-uploader.config.cjs(或.js)配置文件,抛出异常如果文件不存在。 - 有了文件配置后,再用一个包含默认值(空的凭证占位、默认
prefix = ""、默认sourceDir = "dist"、默认 OSS headers)的defaultConfig跟用户配置以及运行时传入的opts(CLI 参数或调用接口时传的对象)做深度合并,得到最终的config对象。
- 校验必填项
-
- 从合并后的
config里检查accessKeyId、accessKeySecret、bucket、region四个字段是否都已填写,否则直接抛错误提醒用户补全。
- 从合并后的
- 初始化 OSS 客户端
-
- 基于校验通过的
config构造new OSS(config)实例,用它来跟阿里云 OSS 交互。
- 基于校验通过的
- 递归遍历并上传
-
- 从本地要上传的根目录
baseDir = path.join(cwd, config.sourceDir)开始,调用uploadDir(client, currentPath, baseDir, prefix, headers):
- 从本地要上传的根目录
-
-
- 如果
currentPath是目录,就readdir一层层向下递归; - 如果是文件,就计算它相对于
baseDir的相对路径relativePath = path.relative(baseDir, currentPath).replace(/\/g, "/"),然后拼出 OSS 上的对象名objectName = prefix + relativePath,调用client.put(objectName, currentPath, { headers })上传,成功后打印日志。
- 如果
-
- 输出结果
-
- 每上传成功一个文件就输出一行「✔ 上传 /本地/路径 → prefix/相对/路径 成功」,方便追踪进度。
import OSS from "ali-oss";
import fs from "fs-extra";
import merge from "lodash/merge";
import path from "path";
export interface Config {
accessKeyId: string;
accessKeySecret: string;
bucket: string;
region: string;
prefix?: string;
headers?: Record<string, string>;
sourceDir?: string;
}
// 从项目根目录加载 CJS 配置文件
function loadConfigFile(cwd: string): Partial<Config> {
const cjsPath = path.join(cwd, "oss-uploader.config.cjs");
if (fs.existsSync(cjsPath)) {
return require(cjsPath);
}
throw new Error(
"找不到配置文件 oss-uploader.config.cjs,请确保在项目根目录创建该文件,并使用 .cjs 扩展名"
);
}
// 递归上传目录或文件
async function uploadDir(
client: OSS,
currentPath: string,
baseDir: string,
prefix: string,
headers: Record<string, string>
) {
const stat = await fs.stat(currentPath);
if (stat.isDirectory()) {
for (const name of await fs.readdir(currentPath)) {
await uploadDir(
client,
path.join(currentPath, name),
baseDir,
prefix,
headers
);
}
} else {
const relativePath = path
.relative(baseDir, currentPath)
.replace(/\/g, "/");
const objectName = prefix + relativePath;
const result = await client.put(objectName, currentPath, { headers });
if (result.res.status === 200) {
console.log(`✔ 上传 ${currentPath} → ${objectName} 成功`);
}
}
}
// 对外暴露的 API
export async function uploadStatic(
cwd: string,
opts: Partial<Config> = {}
): Promise<void> {
const fileConfig = loadConfigFile(cwd);
const defaultConfig: Config = {
accessKeyId: "",
accessKeySecret: "",
bucket: "",
region: "",
prefix: "",
headers: {
"x-oss-storage-class": "Standard",
"x-oss-object-acl": "public-read",
},
sourceDir: "dist",
};
const config = merge({}, defaultConfig, fileConfig, opts) as Config;
if (
!config.accessKeyId ||
!config.accessKeySecret ||
!config.bucket ||
!config.region
) {
throw new Error(
"请在 oss-uploader.config.cjs 或命令行参数中指定 accessKeyId、accessKeySecret、bucket 和 region"
);
}
const client = new OSS(config as any);
const baseDir = path.join(cwd, config.sourceDir!);
await uploadDir(client, baseDir, baseDir, config.prefix!, config.headers!);
}
五、构建 CLI(src/cli.ts)
- INIT 命令:一键在项目根目录生成配置文件模板。
- UPLOAD 命令:解析命令行参数(包括覆盖默认目录
dist、OSS 凭证、bucket、region、prefix 等),然后把这些参数“打包”给核心函数uploadStatic,由它来完成真正的文件上传。 - 整个 CLI 文件的职责就是「收集参数 → 校验/报错 → 调用核心 API」,而不涉及具体的上传、遍历、合并逻辑。
#!/usr/bin/env node
import { Command } from "commander";
import fs from "fs-extra";
import path from "path";
import { Config, uploadStatic } from "./index";
const program = new Command();
program
.name("oss-uploader")
.description("将指定目录(默认 dist)上传到阿里云 OSS")
.version("1.0.0");
program
.command("init")
.description("在项目根目录生成配置文件 oss-uploader.config.cjs 模板")
.action(() => {
const target = path.join(process.cwd(), "oss-uploader.config.cjs");
if (fs.existsSync(target)) {
console.error("⚠ oss-uploader.config.cjs 已存在");
process.exit(1);
}
fs.writeFileSync(
target,
`/**
* OSS Uploader CJS 配置文件
*/
module.exports = {
accessKeyId: "<YOUR_ACCESS_KEY_ID>",
accessKeySecret: "<YOUR_ACCESS_KEY_SECRET>",
bucket: "<YOUR_BUCKET_NAME>",
region: "<YOUR_OSS_REGION>",
prefix: "static/",
sourceDir: "dist",
headers: {
"x-oss-storage-class": "Standard",
"x-oss-object-acl": "public-read"
}
};\n`
);
console.log("✔ 已创建 oss-uploader.config.cjs,请根据实际情况填写");
});
program
.command("upload")
.description("上传指定目录到 OSS,目录默认为 dist")
.option("-a, --accessKeyId <id>")
.option("-s, --accessKeySecret <secret>")
.option("-b, --bucket <bucket>")
.option("-r, --region <region>")
.option("-p, --prefix <prefix>")
.option("-d, --dir <directory>", "本地静态资源目录,默认 dist")
.action(async (opts: Partial<Config> & { dir?: string }) => {
try {
const runOpts: Partial<Config> = { ...opts };
if (opts.dir) runOpts.sourceDir = opts.dir;
await uploadStatic(process.cwd(), runOpts);
} catch (err: any) {
console.error("✖ 上传失败:", err.message);
process.exit(1);
}
});
program.parse(process.argv);
六、完整的package.json
{
"name": "oss-uploader",
"version": "1.0.0",
"description": "OSS 上传工具。",
"main": "index.js",
"types": "dist/index.d.ts",
"bin": {
"oss-uploader": "dist/cli.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
},
"./cli": {
"import": "./dist/cli.js",
"require": "./dist/cli.js"
}
},
"scripts": {
"build": "tsc",
"prepare": "npm run build"
},
"keywords": [],
"author": "haha2333",
"license": "ISC",
"dependencies": {
"ali-oss": "^6.23.0",
"commander": "^14.0.0",
"fs-extra": "^11.3.0",
"lodash": "^4.17.21",
"lodash.merge": "^4.6.2"
},
"devDependencies": {
"@types/ali-oss": "^6.16.11",
"@types/commander": "^2.12.5",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.14",
"typescript": "^5.8.3"
},
"files": [
"dist"
]
}
七、发布到私有仓库
1️⃣ 登录私有仓库
运行以下命令,登录你的私有仓库:
npm login --registry=https://your-private-registry.com/
2️⃣ 发布工具库
运行以下命令,将工具库发布到私有仓库:
npm publish
至此,你的七牛云文件上传工具库已经成功发布到私有仓库,可以在其他项目中通过 NPM 进行安装和使用。
八、使用
一、安装
在项目根目录运行:
npm install --save-dev oss-uploader
二、初始化配置
执行初始化命令:
npx oss-uploader init
该命令会在项目根目录生成 oss-uploader.config.cjs 配置文件模板。请根据实际情况修改以下字段:
module.exports = {
accessKeyId: "<YOUR_ACCESS_KEY_ID>", // 阿里云 AccessKeyId
accessKeySecret: "<YOUR_ACCESS_KEY_SECRET>", // 阿里云 AccessKeySecret
bucket: "<YOUR_BUCKET_NAME>", // 要上传的 OSS Bucket 名称
region: "oss-cn-hangzhou", // OSS 所在地域
prefix: "static/", // 上传后在 Bucket 下的前缀目录
sourceDir: "dist", // 本地待上传目录,默认 dist
headers: {
"x-oss-storage-class": "Standard",
"x-oss-object-acl": "public-read",
},
};
三、使用
一、CLI 使用
1. 基本命令
npx oss-uploader upload
- 将读取
oss-uploader.config.cjs(.js)配置,并将sourceDir目录中的所有文件递归上传到 OSS。
2. 参数覆盖
可以通过命令行参数动态覆盖配置文件中的任意字段:
npx oss-uploader upload \
--dir=build # 覆盖 sourceDir
--prefix=assets/ # 覆盖 prefix
--accessKeyId=AKID # 覆盖 AccessKeyId
--accessKeySecret=SK # 覆盖 Secret
--bucket=my-bucket # 覆盖 Bucket
--region=oss-cn-shanghai# 覆盖 Region
| 参数 | 说明 | 默认值 |
|---|---|---|
--dir, -d | 本地静态资源目录 | dist |
--prefix, -p | OSS 上的目录前缀 | '' |
--accessKeyId, -a | 阿里云 AccessKeyId | — |
--accessKeySecret,-s | 阿里云 AccessKeySecret | — |
--bucket, -b | OSS Bucket 名称 | — |
--region, -r | OSS 地域 | — |
二、npm 脚本集成
在 package.json 中添加:
{
"scripts": {
"build": "vite build",
"deploy": "npm run build && npx oss-uploader upload"
}
}
执行:
npm run deploy
即可完成 vite build 与静态资源上传两步操作。
三、Vite 插件模式(Vite <7)
在 vite.config.js 中注册 closeBundle 钩子,自动调用 uploadStatic()。
// vite.config.js (Vite <=6)
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { uploadStatic } from "oss-uploader";
export default defineConfig({
plugins: [
vue(),
{
name: "oss-upload-plugin",
closeBundle: async () => {
console.log("📦 构建完成,开始上传 OSS...");
try {
await uploadStatic(process.cwd());
console.log("✅ 上传成功");
} catch (e) {
console.error("❌ 上传失败", e);
}
},
},
],
});