构建属于你的七牛云文件上传工具:Qiniu Uploader 详解(从 0 到 1 实现)

news2025/1/8 22:05:23

GitHub 仓库地址:https://github.com/hahala2333/qiniu-upload

📚 简介

在现代 Web 开发中,静态资源的上传和管理是不可避免的需求。为了简化将本地资源上传到七牛云存储的过程,我们构建了 Qiniu Uploader 工具。它具备灵活的配置方式、现代化的构建工具支持(Vite)、兼容多种模块格式以及使用 TypeScript 提供强大的类型支持。本文将手把手带你从 0 开始搭建一个完整的七牛云上传工具,涵盖项目初始化、核心功能实现、打包以及发布到私有仓库的完整流程。


🎯 设计思路与项目规划

在开始编写代码之前,我们需要明确工具的功能和设计思路:

  1. 支持通过配置文件或直接传入参数配置七牛云账号信息。
  2. 使用 Vite 构建项目,支持输出 ESM 和 CommonJS 两种模块格式。
  3. 使用 TypeScript 提供类型支持,提高代码的可读性和可维护性。

🛠 第一步:初始化项目

1️⃣ 创建项目目录

首先,在本地创建一个新的项目目录,并进入该目录:

mkdir qiniu-uploader
cd qiniu-uploader

2️⃣ 初始化 package.json

在项目根目录下运行以下命令,初始化 package.json 文件:

npm init -y

此时,项目目录下会生成一个 package.json 文件。

3️⃣ 安装必要的依赖

接下来,我们需要安装项目所需的依赖:

npm install qiniu vite
npm install -D typescript @types/node tsx
  • qiniu:七牛云官方 SDK。
  • vite:现代化的构建工具。
  • typescript:TypeScript 编译器。
  • @types/node:Node.js 的类型定义。
  • tsx:用于运行 TypeScript 文件的工具。

✏️ 第二步:编写配置文件生成脚本

为了简化用户的配置操作,我们提供一个 CLI 工具来生成默认的配置文件。

1️⃣ 创建 cli 目录和脚本文件

在项目根目录下创建 cli 目录,并在其中创建 init-config.js 文件:

mkdir cli
cd cli
nano init-config.js

2️⃣ 编写配置文件生成脚本

cli/init-config.js 中,编写以下代码:

#!/usr/bin/env node

const fs = require("fs");

const configContent = {
  accessKey: "your-access-key",
  secretKey: "your-secret-key",
  bucket: "your-bucket",
  zone: "Zone_z1",
  basePath: "static/assets",
  distPath: "./dist",
};

fs.writeFileSync(
  "qiniu-config.json",
  JSON.stringify(configContent, null, 2),
  "utf8"
);
console.log("qiniu-config.json 文件已生成!");

3️⃣ 修改 package.json

将该脚本添加到 package.jsonbin 字段中,使其可以通过命令行直接运行:

"bin": {
  "qiniu-config": "cli/init-config.js"
}

4️⃣ 生成配置文件

运行以下命令,生成默认的配置文件:

npx qiniu-config

此时,项目根目录下会生成一个 qiniu-config.json 文件。


💻 第三步:编写核心上传逻辑

接下来,我们开始编写核心的上传逻辑,这部分将是整个工具包的核心功能。

1️⃣ 创建 src/index.ts

src 目录下创建 index.ts 文件,用于编写核心的上传逻辑。

mkdir src
cd src
nano index.ts

2️⃣ 编写核心逻辑

src/index.ts 中编写核心逻辑的主要功能模块:

✅ 加载配置文件并检查是否缺失的必填字段
  1. 在构造函数中,首先尝试加载 qiniu-config.json 文件,如果文件存在,则读取配置。
  2. 如果都存在,则合并配置传入参数会覆盖配置文件中的同名参数
  3. 检查是否缺失的必填字段
✅ 验证存储空间是否存在

通过七牛云的 SDK 验证存储空间(Bucket)是否存在。

✅ 获取上传 Token

生成上传所需的 Token。

✅ 递归获取本地文件路径

递归获取本地文件夹中所有的文件路径,准备上传。

✅ 上传文件到七牛云

使用七牛云的 SDK 将文件上传至指定的存储空间。

详细代码
import qiniu from "qiniu";
import fs from "fs";
import path from "path";

const ZoneMap = {
  Zone_z0: qiniu.zone.Zone_z0, // 华东
  Zone_z1: qiniu.zone.Zone_z1, // 华北
  Zone_z2: qiniu.zone.Zone_z2, // 华南
  Zone_na0: qiniu.zone.Zone_na0, // 北美
  Zone_as0: qiniu.zone.Zone_as0, // 东南亚
};

export interface UploadOptions {
  accessKey?: string; // 七牛云 Access Key
  secretKey?: string; // 七牛云 Secret Key
  bucket?: string; // 存储空间名称
  zone?: keyof typeof ZoneMap; // 存储区域
  basePath?: string; // 在七牛云存储中添加的路径前缀
  distPath?: string; // 本地静态资源路径
}

export class QiniuUploader {
  private mac: qiniu.auth.digest.Mac;
  private config: qiniu.conf.Config;
  private options: UploadOptions;
  // 初始化上传类的配置
  constructor(options?: UploadOptions) {
    const configFilePath = path.join(process.cwd(), "qiniu-config.json");
    const fileOptions = QiniuUploader.loadConfigFile(configFilePath);
    // 合并配置文件和传入参数,优先使用传入参数
    this.options = { ...fileOptions, ...options };

    // 3. 验证必填字段
    const missingFields = this.getMissingRequiredFields();
    if (missingFields.length > 0) {
      throw new Error(`Missing required fields: ${missingFields.join(", ")}`);
    }

    this.mac = new qiniu.auth.digest.Mac(
      this.options.accessKey,
      this.options.secretKey
    );
    this.config = new qiniu.conf.Config();
    this.config.zone = this.options.zone
      ? ZoneMap[this.options.zone]
      : qiniu.zone.Zone_z1; // 默认华东
  }
  // 检查缺失的必填字段
  private getMissingRequiredFields(): string[] {
    const requiredFields = ["accessKey", "secretKey", "bucket", "distPath"];
    const missingFields: string[] = [];

    for (const field of requiredFields) {
      if (!this.options[field as keyof UploadOptions]) {
        missingFields.push(field);
      }
    }

    return missingFields;
  }

  // 静态方法:加载配置文件
  private static loadConfigFile(
    configFilePath: string
  ): Partial<UploadOptions> {
    if (!fs.existsSync(configFilePath)) {
      return {};
    }
    try {
      const fileContent = fs.readFileSync(configFilePath, "utf-8").trim();

      // 如果文件内容为空,则返回一个空对象
      if (!fileContent) {
        return {};
      }

      const config = JSON.parse(fileContent);
      return config;
    } catch (error: any) {
      throw new Error(`Failed to load configuration file: ${error.message}`);
    }
  }

  // 验证存储空间是否存在
  private async verifyBucket(): Promise<boolean> {
    const bucketManager = new qiniu.rs.BucketManager(this.mac, this.config);
    return new Promise((resolve, reject) => {
      // 调用 getBucketInfo 方法
      bucketManager.getBucketInfo(
        this.options.bucket,
        (err, respBody, respInfo) => {
          if (err) {
            return reject(err); // 网络或 SDK 错误
          }
          if (respInfo.statusCode === 200) {
            resolve(true); // Bucket 存在
          } else if (respInfo.statusCode === 631) {
            resolve(false); // Bucket 不存在
          } else {
            reject(new Error(`Unexpected status code: ${respInfo.statusCode}`));
          }
        }
      );
    });
  }

  // 获取上传 Token
  private getUploadToken(): string {
    const options = {
      scope: this.options.bucket,
      expires: 3600,
    };
    const putPolicy = new qiniu.rs.PutPolicy(options);
    return putPolicy.uploadToken(this.mac);
  }

  // 上传方法
  public async upload(): Promise<void> {
    // 验证存储空间是否存在
    const bucketExists = await this.verifyBucket();
    if (!bucketExists) {
      throw new Error(`Bucket "${this.options.bucket}" does not exist.`);
    }

    // 获取上传 Token
    const uploadToken = this.getUploadToken();
    // 创建上传器
    const formUploader = new qiniu.form_up.FormUploader(this.config);
    // 设置上传时的额外参数 默认不设置任何参数
    const putExtra = new qiniu.form_up.PutExtra();
    // 获取所有待上传的文件路径
    const files = this.getFiles(this.options.distPath);

    // 逐个上传文件
    for (const file of files) {
      // 计算目标路径(key)
      const key = this.options.basePath
        ? path.join(
            this.options.basePath,
            path.relative(this.options.distPath, file)
          )
        : path.relative(this.options.distPath, file);

      await new Promise((resolve, reject) => {
        formUploader.putFile(
          uploadToken,
          key,
          file,
          putExtra,
          (err, body, info) => {
            if (err) return reject(err);
            if (info.statusCode === 200) {
              console.log(`Uploaded: ${key}`);
              resolve(body);
            } else {
              reject(body);
            }
          }
        );
      });
    }
  }

  // 通过递归方法获取所有需要上传的文件路径。
  private getFiles(dir: string): string[] {
    const files: string[] = [];
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        files.push(...this.getFiles(fullPath));
      } else {
        files.push(fullPath);
      }
    }
    return files;
  }
}


✅ 验证核心功能

编写一个测试文件来验证上传功能是否正常工作:

1️⃣ 创建 test.ts

在项目根目录下创建 test.ts 文件:

import { QiniuUploader } from "./src/index";

(async () => {
  try {
    const uploader = new QiniuUploader();
    await uploader.upload();
    console.log("Upload completed successfully!");
  } catch (error) {
    console.error("Upload failed:", error);
  }
})();
2️⃣ 配置 package.json 的导出字段
 "scripts": {
    "dev": "tsx test.ts"
  },

运行以下命令进行测试:

npm run dev

🛠 第四步:如何配置 Vite 打包工具

1️⃣ 配置 vite.config.ts 文件

为了支持 ESM 和 CommonJS 两种格式,我们可以通过 Vitebuild.lib 配置来实现。具体配置如下:

vite.config.ts
import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  build: {
    lib: {
      entry: path.resolve(__dirname, "src/index.ts"), // 工具库的入口文件
      name: "QiniuUploader", // 打包后的全局变量名称
      fileName: (format) => `index.${format === "es" ? "mjs" : "cjs.js"}`, // 输出的文件名
      formats: ["es", "cjs"], // 同时输出 ESM 和 CommonJS 两种格式
    },
    rollupOptions: {
      external: ["qiniu", "fs", "path"], // 指定外部依赖,不打包进最终文件
      output: {
        globals: {
          qiniu: "qiniu", // 配置全局变量
        },
      },
    },
  },
});
解释:
  • entry:工具库的入口文件,通常是 src/index.ts
  • name:为全局变量命名,适用于在浏览器中直接引入。
  • fileName:根据模块格式不同,分别输出 index.mjs(ESM 格式)和 index.cjs.js(CommonJS 格式)。
  • formats:指定打包的模块格式,可以是 ["es", "cjs"]

2️⃣ 配置 TypeScript 类型文件的输出

为了让用户在使用工具库时获得 TypeScript 类型提示,我们需要配置 TypeScript 编译器 输出类型声明文件。

配置 tsconfig.json 文件
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "declaration": true,             // 启用类型声明文件的生成
    "emitDeclarationOnly": true,     // 只生成类型声明文件
    "outDir": "dist",                // 指定输出目录
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"]         // 指定要编译的文件
}
解释:
  • declaration:启用类型声明文件的生成(.d.ts)。
  • emitDeclarationOnly:仅输出类型声明文件,不生成 JavaScript 文件。
  • outDir:指定输出目录为 dist,将类型声明文件输出到打包目录中。
  • include:指定编译的文件路径。

3️⃣ 添加打包脚本

package.json 文件中,添加以下脚本用于执行打包命令:

修改 package.json
"scripts": {
  "build": "vite build && tsc"
}
解释:
  • vite build:使用 Vite 构建工具库。
  • tsc:运行 TypeScript 编译器,仅输出类型声明文件。

4️⃣ 配置 package.json 的导出字段

为了让用户能够根据项目的模块类型自动选择 ESM 或 CommonJS 格式,我们需要在 package.json 文件中配置 exports 字段:

修改 package.json
{
  "main": "dist/index.cjs.js",       // CommonJS 入口文件
  "module": "dist/index.mjs",        // ESM 入口文件
  "types": "dist/index.d.ts",        // 类型声明文件入口
  "exports": {
    ".": {
      "import": "./dist/index.mjs",  // ESM 格式的导出路径
      "require": "./dist/index.cjs.js" // CommonJS 格式的导出路径
    }
  }
}
解释:
  • main:指定 CommonJS 格式的入口文件。
  • module:指定 ESM 格式的入口文件。
  • types:指定类型声明文件的入口文件。
  • exports:根据模块类型动态导出对应的文件路径。

5️⃣ 执行打包命令

配置完成后,运行以下命令生成打包文件和类型声明文件:

npm run build

打包完成后,dist 目录下将生成以下文件:

dist/
├── index.cjs.js    # CommonJS 格式的文件
├── index.mjs       # ESM 格式的文件
└── index.d.ts      # 类型声明文件
完整的 package.json
{
  "name": "qiniu-upload",
  "version": "1.0.0",
  "description": "A library for uploading files to Qiniu Cloud",
  "main": "dist/index.cjs.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "license": "MIT",
  "author": "haha",
  "files": [
    "dist",
    "README.md",
    "cli/init-config.js"
  ],
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs.js"
    }
  },
  "scripts": {
    "build": "vite build && tsc",
    "dev": "tsx test.ts"
  },
  "bin": {
    "qiniu-config": "cli/init-config.js"
  },
  "dependencies": {
    "qiniu": "^7.14.0"
  },
  "devDependencies": {
    "@types/node": "^22.10.5",
    "tsx": "^4.19.2",
    "vite": "^6.0.7"
  }
}


🚀 第五步:发布到私有仓库

1️⃣ 登录私有仓库

运行以下命令,登录你的私有仓库:

npm login --registry=https://your-private-registry.com/

2️⃣ 发布工具库

运行以下命令,将工具库发布到私有仓库:

npm publish

至此,你的七牛云文件上传工具库已经成功发布到私有仓库,可以在其他项目中通过 NPM 进行安装和使用。

✅ 如何使用

1️⃣ 在 ESM 项目中验证

创建一个新的 ESM 项目,并通过 import 语法引入工具库:

import { QiniuUploader } from 'qiniu-uploader';

const uploader = new QiniuUploader();
await uploader.upload();

2️⃣ 在 CommonJS 项目中验证

创建一个新的 CommonJS 项目,并通过 require 语法引入工具库:

const { QiniuUploader } = require('qiniu-uploader');

const uploader = new QiniuUploader();
uploader.upload().then(() => {
  console.log('Upload complete!');
}).catch(console.error);

3️⃣ 在 TypeScript 项目中使用

import { QiniuUploader, UploadOptions } from "qiniu-upload";

const options: UploadOptions = {
  accessKey: "your-access-key",
  secretKey: "your-secret-key",
  bucket: "your-bucket",
  distPath: "./dist",
};

const uploader = new QiniuUploader(options);

await uploader.upload();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2273441.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Sam Altman发布博客,回顾OpenAI九年历程,直言目标已瞄准ASI超级人工智能

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Python爬虫与1688图片搜索API接口:深度解析与显著收益

在电子商务的浩瀚海洋中&#xff0c;数据是驱动业务决策的核心引擎。阿里巴巴旗下的1688平台&#xff0c;作为全球领先的B2B在线市场&#xff0c;不仅汇聚了海量的商品信息&#xff0c;还提供了丰富的API接口&#xff0c;为开发者提供了强大的数据获取工具。本文将深入探讨1688…

回归预测 | MATLAB实LSTM多输入单输出回归预测

回归预测 | MATLAB实LSTM多输入单输出回归预测 目录 回归预测 | MATLAB实LSTM多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 LSTM多输入单输出回归预测 程序设计 完整代码&#xff1a;MATLAB实LSTM多输入单输出回归预测 %% 清空环境变量 warni…

从 0 到 1,用 FastGPT 搭建专属私有化知识库与超智能 AI 助理

田园课堂私有化知识库搭建流程与总结 引言 在当今数字化时代&#xff0c;知识管理与智能交互对于教育领域的创新发展至关重要。FastGPT作为一款高效的AI流程构建可视化开源工具&#xff0c;为田园课堂实现私有化知识库的快速搭建提供了有力支持。本文将详细阐述使用FastGPT搭…

centos服务器 /1ib64/libm.so.6: version “GLIBc 2.27’ not found 异常

centos服务器 /1ib64/libm.so.6: version “GLIBc 2.27’ not found 异常 问题 在服务器使用open3d时&#xff0c;报错缺失GLIBC_2.27&#xff0c;因为后续操作出问题会导致服务器挂&#xff0c;所以最好先备份一下。 解决 查询glibc版本 输入指令查询系统glibc版本&#x…

UE播放声音

蓝图中有两个播放声音的函数 Play Sound 2D 和 Play Sound at Location Play Sound 2D没有声音距离衰减&#xff0c;一般用于界面ui Play Sound at Location 有声音距离衰减&#xff0c;一般用于枪声&#xff0c;场景声等&#xff0c;比较常用

【情感】程序人生之情感关系中的平等意识(如何经营一段长期稳定的关系 沸羊羊舔狗自查表)

【情感】程序人生之情感关系中的平等意识&#xff08;如何经营一段长期稳定的关系 & 沸羊羊舔狗自查表&#xff09; 文章目录 1、情感关系中的平等意识2、如何经营一段长期稳定的关系&#xff08;避免左倾 | 敬畏与担当&#xff09;3、沸羊羊/舔狗自查表&#xff08;避免右…

借助免费GIS工具箱轻松实现las点云格式到3dtiles格式的转换

在当今数字化浪潮下&#xff0c;地理信息系统&#xff08;GIS&#xff09;技术日新月异&#xff0c;广泛渗透到城市规划、地质勘探、文化遗产保护等诸多领域。而 GISBox 作为一款功能强大且易用的 GIS 工具箱&#xff0c;以轻量级、免费使用、操作便捷等诸多优势&#xff0c;为…

Chapter4.1 Coding an LLM architecture

文章目录 4 Implementing a GPT model from Scratch To Generate Text4.1 Coding an LLM architecture 4 Implementing a GPT model from Scratch To Generate Text 本章节包含 编写一个类似于GPT的大型语言模型&#xff08;LLM&#xff09;&#xff0c;这个模型可以被训练来生…

Git revert回滚

回退中间的某次提交&#xff08;此操作在预生产分支上比较常见&#xff09;&#xff0c;建议此方式使用命令进行操作&#xff08;做好注释&#xff0c;方便后续上线可以找到这个操作&#xff09; Git操作&#xff1a; 命令&#xff1a;revert -n 版本号 1&#xff1a;git re…

1. 使用springboot做一个音乐播放器软件项目【前期规划】

背景&#xff1a; 现在大部分音乐软件都是要冲会员才可以无限常听的。对于喜欢听音乐的小伙伴&#xff0c;资金又比较紧张&#xff0c;是那么的不友好。作为程序员的我&#xff0c;也是喜欢听着歌&#xff0c;敲着代码。 最近就想做一个音乐播放器的软件&#xff0c;在内网中使…

Flutter项目开发模版,开箱即用(Plus版本)

前言 当前案例 Flutter SDK版本&#xff1a;3.22.2 本文&#xff0c;是由这两篇文章 结合产出&#xff0c;所以非常建议大家&#xff0c;先看完这两篇&#xff1a; Flutter项目开发模版&#xff1a; 主要内容&#xff1a;MVVM设计模式及内存泄漏处理&#xff0c;涉及Model、…

配置数据的抗辐照加固方法

SRAM 型FPGA 的配置存储器可以看成是由0 和1 组成的二维阵列&#xff0c;帧的高度为矩阵阵列的高度&#xff0c;相同结构的配置帧组成配置列&#xff0c;如CLB 列、IOB 列、输入输出互联(Input Output Interconnect,IOI)列、全局时钟(Global Clock, GCLK)列、BRAM 列和BRAM 互联…

【学习路线】Python 算法(人工智能)详细知识点学习路径(附学习资源)

学习本路线内容之前&#xff0c;请先学习Python的基础知识 其他路线&#xff1a; Python基础 >> Python进阶 >> Python爬虫 >> Python数据分析&#xff08;数据科学&#xff09; >> Python 算法&#xff08;人工智能&#xff09; >> Pyth…

【Vue3项目实战系列一】—— 全局样式处理,导入view-ui-plus组件库,定制个性主题

&#x1f609; 你好呀&#xff0c;我是爱编程的Sherry&#xff0c;很高兴在这里遇见你&#xff01;我是一名拥有十多年开发经验的前端工程师。这一路走来&#xff0c;面对困难时也曾感到迷茫&#xff0c;凭借不懈的努力和坚持&#xff0c;重新找到了前进的方向。我的人生格言是…

SCAU期末笔记 - 数据库系统概念往年试卷解析

数据库搞得人一头雾水&#xff0c;题型太多太杂&#xff0c;已经准备摆烂了。就刷刷往年试卷&#xff0c;挂不挂听天由命。 2019年 Question 1 选择题 1. R ∩ S R∩S R∩S等于一下哪个选项&#xff1f; 画个文氏图秒了 所以选A. R ∩ S R − ( R − S ) R∩SR-(R-S) R∩…

oxml中创建CT_Document类

概述 本文基于python-docx源码&#xff0c;详细记录CT_Document类创建的过程&#xff0c;以此来加深对Python中元类、以及CT_Document元素类的认识。 元类简介 元类&#xff08;MetaClass&#xff09;是Python中的高级特性。元类是什么呢&#xff1f;Python是面向对象编程…

Fabric环境部署

官方下载文档&#xff1a;A Blockchain Platform for the Enterprise — Hyperledger Fabric Docs main documentation 1.1 创建工作目录 将Fabric代码按照GO语言的推荐方式进行存放&#xff0c;创建目录结构并切换到该目录下。具体命令如下&#xff1a; mkdir -p ~/go/src/g…

TCP与DNS的报文分析

场景拓扑&#xff1a; 核心路由配置&#xff1a; 上&#xff08;DNS&#xff09;&#xff1a;10.1.1.1/24 下(WEB)&#xff1a;20.1.1.1/24 左&#xff08;client&#xff09;&#xff1a;192.168.0.1/24 右(PC3)&#xff1a;192.168.1.1/24Clint2配置&a…

怎么管理电脑usb接口,分享四种USB端口管理方法

怎么管理电脑usb接口&#xff0c;分享四种USB端口管理方法 USB接口作为电脑重要的外部接口&#xff0c;方便了数据传输和设备连接。 然而&#xff0c;不加管理的USB接口也可能带来安全隐患&#xff0c;例如数据泄露、病毒传播等。 因此&#xff0c;有效管理电脑USB接口至关重…