最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

JavaScript/TypeScript 前端實現文件上傳到 MinIO 完整指南

 更新時間:2025年12月15日 10:49:45   作者:喵了幾個咪  
本文介紹了前端使用JavaScript/TypeScript實現文件上傳到MinIO對象存儲的完整方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

以往前端實現文件上傳到服務端,常用方案為 HTTP 上傳或 FTP 上傳,但這兩種方式均存在明顯短板:HTTP 上傳易受網絡波動影響,可靠性較差;FTP 配置復雜且安全性不足。隨著對象存儲服務(Object Storage Service, OSS)的普及,這一問題得到了有效解決。
對象存儲(基于對象的存儲)是一種專為海量非結構化數據設計的存儲架構。與傳統(tǒng)存儲不同,它將數據封裝為獨立對象,捆綁元數據和唯一標識符,便于快速查找與訪問。OSS 提供與平臺無關的 RESTful API 接口,支持在任意應用、任意時間、任意地點存儲和訪問各類數據。

目前主流的開源 OSS 方案包括 MinIO和Ceph。其中 MinIO 憑借輕量、易用、兼容 S3 接口等優(yōu)勢,使用率持續(xù)攀升,成為開源對象存儲的首選方案之一。本文將詳細介紹如何基于 JavaScript/TypeScript 前端實現文件上傳到 MinIO。

一、什么是 MinIO?

官方定義:MinIO 是基于 Apache License v2.0 開源協(xié)議,采用 Golang 開發(fā)的對象存儲服務。

它完全兼容亞馬遜 S3 云存儲服務接口,特別適合存儲圖片、視頻、日志文件、備份數據、容器/虛擬機鏡像等大容量非結構化數據,支持單個對象文件從幾 KB 到 5T 的大小范圍。

MinIO 具備輕量特性,可輕松與 NodeJS、Redis、MySQL 等應用集成。同時,它通過糾刪碼(erasure code)和校驗和(checksum)保障數據安全——即使丟失一半數量(N/2)的硬盤,仍可完整恢復數據。

二、本地 Docker 部署 MinIO 測試服務

通過 Docker 可快速部署 MinIO 測試環(huán)境,步驟如下:

# 拉取最新版 MinIO 鏡像
docker pull bitnami/minio:latest

# 啟動 MinIO 容器
# 注意:MINIO_ROOT_USER 至少 3 個字符,MINIO_ROOT_PASSWORD 至少 8 個字符
# 首次運行后服務可能自動關閉,手動重啟容器即可正常使用
docker run -itd \
    --name minio-server \
    -p 9000:9000 \  # API 端口
    -p 9001:9001 \  # 控制臺端口
    --env MINIO_SERVER_URL="http://127.0.0.1:9000" \
    --env MINIO_BROWSER_REDIRECT_URL="http://127.0.0.1:9001" \    
    --env MINIO_ROOT_USER="root" \
    --env MINIO_ROOT_PASSWORD="123456789" \
    --env MINIO_DEFAULT_BUCKETS='images' \  # 自動創(chuàng)建名為 images 的存儲桶
    --env MINIO_FORCE_NEW_KEYS="yes" \
    --env BITNAMI_DEBUG=true \
    bitnami/minio:latest

三、TypeScript 實現文件上傳的核心方案

前端基于 TypeScript 上傳文件到 MinIO,核心有三種 HTTP 請求方案可選,分別適配不同開發(fā)場景:

  1. XMLHttpRequest:傳統(tǒng)方案,兼容性好,支持進度監(jiān)聽等細粒度控制
  2. Fetch API:現代瀏覽器原生支持,基于 Promise,語法更簡潔
  3. Axios:第三方 HTTP 庫,支持攔截器、取消請求等增強功能,生態(tài)完善

3.1 XMLHttpRequest 實現

function xhrUploadFile(file: File, url: string) {
  const xhr = new XMLHttpRequest();
  xhr.open('PUT', url, true);
  xhr.send(file);

  xhr.onload = () => {
    if (xhr.status === 200) {
      console.log(`${file.name} 上傳成功`);
    } else {
      console.error(`${file.name} 上傳失敗`);
    }
  };
}

3.2 Fetch API 實現

function fetchUploadFile(file: File, url: string) {
  fetch(url, {
    method: 'PUT',
    body: file,
  })
    .then((response) => {
      console.log(`${file.name} 上傳成功`, response);
    })
    .catch((error) => {
      console.error(`${file.name} 上傳失敗`, error);
    });
}

3.3 Axios 實現

function axiosUploadFile(file: File, url: string) {
  const instance = axios.create();
  instance
    .put(url, file, {
      headers: {
        'Content-Type': file.type, // 需指定文件真實 Content-Type
      },
    })
    .then(function (response) {
      console.log(`${file.name} 上傳成功`, response);
    })
    .catch(function (error) {
      console.error(`${file.name} 上傳失敗`, error);
    });
}

四、MinIO 上傳 API 選型:安全優(yōu)先

MinIO 提供 4 種核心上傳 API,需根據安全性和使用場景選擇:

  1. putObject:從流上傳
  2. fPutObject:從文件上傳
  3. PresignedPutObject:生成臨時 PUT 預簽名 URL,用于前端上傳
  4. PresignedPostPolicy:生成臨時 POST 預簽名 URL,用于前端上傳

關鍵選型說明:

使用 putObject 和 fPutObject 時,需在前端暴露 MinIO 的訪問密鑰(Access Key/Secret Key),存在嚴重安全隱患;且 MinIO 官方 JavaScript 客戶端未針對瀏覽器環(huán)境適配,因此不推薦前端直接使用這兩種方案。

PresignedPutObject 和 PresignedPostPolicy 方案通過服務端生成臨時預簽名 URL,前端僅需使用該臨時 URL 上傳文件,無需暴露核心密鑰,安全性極高,因此本文重點講解這兩種方案。

MinIO 官方關于預簽名 URL 上傳的詳細說明:Upload Files Using Pre-signed URLs

五、前后端完整實現

整體架構:前端通過調用后端接口獲取 MinIO 預簽名 URL,再通過該 URL 直接將文件上傳到 MinIO。其中后端采用 Go + Gin 框架實現,負責 MinIO 客戶端封裝和預簽名 URL 生成。

5.1 Go 后端實現

首先封裝 MinIO 客戶端,統(tǒng)一管理連接和預簽名 URL 生成邏輯:

package minio

import (
	"context"
	"log"
	"net/url"
	"time"

	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

const (
	defaultExpiryTime = time.Second * 24 * 60 * 60 // 1 day

	endpoint        string = "localhost:9000"
	accessKeyID     string = "root"
	secretAccessKey string = "123456789"
	useSSL          bool   = false
)

type Client struct {
	cli *minio.Client
}

func NewMinioClient() *Client {
	cli, err := minio.New(endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
		Secure: useSSL,
	})
	if err != nil {
		log.Fatalln(err)
	}

	return &Client{
		cli: cli,
	}
}

func (c *Client) PostPresignedUrl(ctx context.Context, bucketName, objectName string) (string, map[string]string, error) {
	expiry := defaultExpiryTime

	policy := minio.NewPostPolicy()
	_ = policy.SetBucket(bucketName)
	_ = policy.SetKey(objectName)
	_ = policy.SetExpires(time.Now().UTC().Add(expiry))

	presignedURL, formData, err := c.cli.PresignedPostPolicy(ctx, policy)
	if err != nil {
		log.Fatalln(err)
		return "", map[string]string{}, err
	}

	return presignedURL.String(), formData, nil
}

func (c *Client) PutPresignedUrl(ctx context.Context, bucketName, objectName string) (string, error) {
	expiry := defaultExpiryTime

	presignedURL, err := c.cli.PresignedPutObject(ctx, bucketName, objectName, expiry)
	if err != nil {
		log.Fatalln(err)
		return "", err
	}

	return presignedURL.String(), nil
}

然后實現 HTTP 接口,對外提供預簽名 URL 獲取服務:

package http

import (
	"context"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"main/minio"
	"net/http"
)

type Response struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

func ResponseJSON(c *gin.Context, httpCode, errCode int, msg string, data interface{}) {
	c.JSON(httpCode, Response{
		Code: errCode,
		Msg:  msg,
		Data: data,
	})
	return
}

type Server struct {
	srv         *gin.Engine
	minioClient *minio.Client
}

func NewHttpServer() *Server {
	srv := &Server{
		srv:         gin.New(),
		minioClient: minio.NewMinioClient(),
	}

	srv.init()

	return srv
}

func (s *Server) init() {
	s.srv.Use(
		gin.Logger(),
		gin.Recovery(),
		cors.Default(),
	)
	s.registerRouter()
}

func (s *Server) registerRouter() {
	s.srv.GET("/presignedPutUrl/:filename", s.handlePutPresignedUrl)
	s.srv.GET("/presignedPostUrl/:filename", s.handlePostPresignedUrl)
}

func (s *Server) handlePutPresignedUrl(c *gin.Context) {
	fileName := c.Param("filename")

	presignedURL, err := s.minioClient.PutPresignedUrl(context.Background(), "images", fileName)
	if err != nil {
		c.String(500, "get presigned url failed")
		return
	}

	type ResponseData struct {
		Url string `json:"url"`
	}
	var resp ResponseData
	resp.Url = presignedURL
	ResponseJSON(c, http.StatusOK, 200, "", resp)
}

func (s *Server) handlePostPresignedUrl(c *gin.Context) {
	fileName := c.Param("filename")

	presignedURL, formData, err := s.minioClient.PostPresignedUrl(context.Background(), "images", fileName)
	if err != nil {
		c.String(500, "get presigned url failed")
		return
	}

	type ResponseData struct {
		Url      string            `json:"url"`
		FormData map[string]string `json:"formData"`
	}
	var resp ResponseData
	resp.Url = presignedURL
	resp.FormData = formData
	ResponseJSON(c, http.StatusOK, 200, "", resp)
}

func (s *Server) Run() {
	// Listen and serve on 0.0.0.0:8080
	_ = s.srv.Run(":8080")
}

5.2 前端 PUT 方式上傳實現

封裝三種 PUT 上傳方案(XMLHttpRequest/Fetch/Axios),并通過后端接口獲取預簽名 URL:

import axios from 'axios';

export class PutFile {
  static xhr(file: File, url: string) {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', url, true);
    xhr.send(file);

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 204) {
        console.log(`[${xhr.status}] ${file.name} 上傳成功`);
      } else {
        console.error(`[${xhr.status}] ${file.name} 上傳失敗`);
      }
    };
  }

  static fetch(file: File, url: string) {
    fetch(url, {
      method: 'PUT',
      body: file,
    })
      .then((response) => {
        console.log(`${file.name} 上傳成功`, response);
      })
      .catch((error) => {
        console.error(`${file.name} 上傳失敗`, error);
      });
  }

  static axios(file: File, url: string) {
    axios
      .put(url, file, {
        headers: {
          'Content-Type': file.type,
        },
      })
      .then(function (response) {
        console.log(`${file.name} 上傳成功`, response);
      })
      .catch(function (error) {
        console.error(`${file.name} 上傳失敗`, error);
      });
  }
}

export function retrievePutUrl(file: File, cb: (file: File, url: string) => void) {
  const url = `http://localhost:8080/presignedPutUrl/${file.name}`;
  axios.get(url)
    .then(function (response) {
      cb(file, response.data.data.url);
    })
    .catch(function (error) {
      console.error(error);
    });
}

export function xhrPutFile(file?: File) {
  console.log('XhrPutFile', file);
  if (file) {
    retrievePutUrl(file, (file, url) => {
      PutFile.xhr(file, url);
    });
  }
}

export function fetchPutFile(file?: File) {
  console.log('FetchPutFile', file);
  if (file) {
    retrievePutUrl(file, (file, url) => {
      PutFile.fetch(file, url);
    });
  }
}

export function axiosPutFile(file?: File) {
  console.log('AxiosPutFile', file);
  if (file) {
    retrievePutUrl(file, (file, url) => {
      PutFile.axios(file, url);
    });
  }
}

5.3 前端 POST 方式上傳實現

POST 方式需攜帶后端返回的表單數據,封裝三種上傳方案:

import axios from 'axios';

export class PostFile {
  static xhr(file: File, url: string, data: object) {
    const formData = new FormData();
    Object.entries(data).forEach(([k, v]) => {
      formData.append(k, v);
    });
    formData.append('file', file);

    const xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.send(formData);

    xhr.onload = () => {
      if (xhr.status === 200 || xhr.status === 204) {
        console.log(`[${xhr.status}] ${file.name} 上傳成功`);
      } else {
        console.error(`[${xhr.status}] ${file.name} 上傳失敗`);
      }
    };
  }

  static fetch(file: File, url: string, data: object) {
    const formData = new FormData();
    Object.entries(data).forEach(([k, v]) => {
      formData.append(k, v);
    });
    formData.append('file', file);

    fetch(url, {
      method: 'POST',
      body: formData,
    })
      .then((response) => {
        console.log(`${file.name} 上傳成功`, response);
      })
      .catch((error) => {
        console.error(`${file.name} 上傳失敗`, error);
      });
  }

  static axios(file: File, url: string, data: object) {
    const formData = new FormData();
    Object.entries(data).forEach(([k, v]) => {
      formData.append(k, v);
    });
    formData.append('file', file);

    axios.post(
      url,
      formData,
      {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      })
      .then(function (response) {
        console.log(`${file.name} 上傳成功`, response);
      })
      .catch(function (error) {
        console.error(`${file.name} 上傳失敗`, error);
      });
  }
}

export function retrievePostUrl(file: File, cb: (file: File, url: string, data: object) => void) {
  const url = `http://localhost:8080/presignedPostUrl/${file.name}`;
  axios.get(url)
    .then(function (response) {
      cb(file, response.data.data.url, response.data.data.formData);
    })
    .catch(function (error) {
      console.error(error);
    });
}

export function xhrPostFile(file?: File) {
  console.log('xhrPostFile', file);
  if (file) {
    retrievePostUrl(file, (file: File, url: string, data: object) => {
      PostFile.xhr(file, url, data);
    });
  }
}

export function fetchPostFile(file?: File) {
  console.log('fetchPostFile', file);
  if (file) {
    retrievePostUrl(file, (file: File, url: string, data: object) => {
      PostFile.fetch(file, url, data);
    });
  }
}

export function axiosPostFile(file?: File) {
  console.log('axiosPostFile', file);
  if (file) {
    retrievePostUrl(file, (file: File, url: string, data: object) => {
      PostFile.axios(file, url, data);
    });
  }
}

六、實戰(zhàn)踩坑指南

在實現過程中,容易遇到以下問題,整理解決方案如下:

6.1 PresignedPutObject 必須用 PUT 方法

PresignedPutObject 生成的預簽名 URL 僅支持 PUT 方法,若使用 POST 方法上傳會直接失敗。需嚴格匹配 API 定義的請求方法。

6.2 PUT 上傳無需構造 FormData

部分開發(fā)者會習慣性構造 FormData 上傳文件,但 PresignedPutObject 方案不支持這種方式——FormData 會導致請求體包含額外的協(xié)議數據(如 ------WebKitFormBoundary 分隔符),MinIO 無法正確解析文件內容。

正確做法:直接將 File 對象作為請求體發(fā)送,無需封裝 FormData。

6.3 Axios 上傳需手動指定 Content-Type

XMLHttpRequest 和 Fetch API 會自動根據文件類型設置正確的 Content-Type,但 Axios 不會。若未手動指定 Content-Type: file.type,MinIO 會將文件 Content-Type 設為 Axios 默認的 application/x-www-form-urlencoded,導致文件無法正常預覽。

6.4 POST 上傳時 file 表單域必須在最后

使用 PresignedPostPolicy 方案時,FormData 中的 file 字段必須放在所有表單數據的最后一位。否則會報以下錯誤:

The body of your POST request is not well-formed multipart/form-data
# 或
The name of the uploaded key is missing

原因:MinIO 對 POST 表單數據的解析順序有嚴格要求,file 字段需作為最后一個參數提交。

6.5 403 錯誤:主機名不匹配

PUT 上傳時出現 403 錯誤,大概率是預簽名 URL 中的主機名與 MinIO 服務的主機名不匹配。核心原因:

MinIO 預簽名 URL 會將主機名(host)納入簽名驗證范圍(對應 X-Amz-SignedHeaders: host)。若后端連接 MinIO 使用的 endpoint(如 localhost:9000)與前端實際訪問的 MinIO 地址(如 192.168.1.100:9000)不一致,會導致簽名驗證失敗。

解決方案:

  1. 后端連接 MinIO 時,使用前端可訪問的地址(如外網 IP 或域名)作為 endpoint;
  2. 通過環(huán)境變量 MINIO_SERVER_URLMINIO_BROWSER_REDIRECT_URL 綁定 MinIO 服務的域名/IP:MINIO_SERVER_URL:指定 API 服務地址(默認 9000 端口);
  3. MINIO_BROWSER_REDIRECT_URL:指定控制臺地址(默認 9001 端口);
  4. 注意:必須添加 http://https:// 前綴,例如 http://minio.example.com:9000

Docker 部署時,可通過 --env 參數注入這兩個環(huán)境變量(參考本文第二部分的 Docker 啟動命令)。

七、示例代碼倉庫

本文完整示例代碼已上傳至 Github 和 Gitee,包含后端 Go + Gin 實現,以及前端 React、Vue 兩種框架的上傳示例(支持進度條、多文件上傳等擴展功能):

到此這篇關于JavaScript/TypeScript 前端實現文件上傳到 MinIO 完整指南的文章就介紹到這了,更多相關JavaScript/TypeScript 文件上傳到MinIO內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 基于JS正則表達式實現模板數據動態(tài)渲染(實現思路詳解)

    基于JS正則表達式實現模板數據動態(tài)渲染(實現思路詳解)

    這篇文章主要介紹了基于JS正則表達式實現模板數據動態(tài)渲染 ,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03
  • 基于JavaScript實現大文件上傳后端代碼實例

    基于JavaScript實現大文件上傳后端代碼實例

    這篇文章主要介紹了基于JavaScript實現大文件上傳后端代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-08-08
  • 純JS實現簡單的日歷

    純JS實現簡單的日歷

    這篇文章主要為大家詳細介紹了純JS實現簡單日歷的的相關代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • es6數據變更同步到視圖層的方法

    es6數據變更同步到視圖層的方法

    這篇文章主要介紹了es6數據變更同步到視圖層的方法,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • JavaScript使用Promise實現分批處理接口請求

    JavaScript使用Promise實現分批處理接口請求

    當我們在實際項目中遇到需要批量發(fā)起上百條接口請求怎么辦呢,本文就來為大家介紹一下JavaScript如何使用Promise實現分批處理接口請求,需要的小伙伴可以參考一下
    2023-11-11
  • JavaScript獲取select中text值的方法

    JavaScript獲取select中text值的方法

    這篇文章主要介紹了JavaScript獲取select中text值的方法,涉及javascript針對HTML頁面元素相關判斷與獲取操作技巧,需要的朋友可以參考下
    2017-02-02
  • javascript中對對層的控制

    javascript中對對層的控制

    javascript中對對層的控制...
    2006-12-12
  • javascript模擬php函數in_array

    javascript模擬php函數in_array

    就是判斷一個元素是否存在于數組中的函數,既然js里string都有indexOf函數,為什么不在Array對象里設置一個這樣的函數呢,其實就用indexOf這個思想挺好的,不知道制定JS標準的人是基于什么考慮,把這樣一個如此常用的功能沒考慮在內的。
    2015-04-04
  • JavaScript異步編程的干貨知識點分享

    JavaScript異步編程的干貨知識點分享

    異步是什么意思?如何實現異步編程?不同的異步模式有哪些?本文將圍繞這些問題和大家分享JavaScript異步編程中的重要知識點,需要的可以學習一下
    2023-06-06

最新評論

游戏| 榕江县| 湖北省| 辽中县| 察雅县| 阿拉善右旗| 兴城市| 长汀县| 潮安县| 石河子市| 玛曲县| 聂荣县| 宜章县| 百色市| 都匀市| 嵊泗县| 布尔津县| 西乡县| 临湘市| 望都县| 河源市| 祁阳县| 和平区| 罗山县| 济阳县| 阜城县| 江陵县| 清河县| 安远县| 疏附县| 嫩江县| 大厂| 庐江县| 潼南县| 上虞市| 信宜市| 连山| 军事| 桐庐县| 长沙市| 延庆县|