在本文中,我們將探討如何利用 Vue 3 和 MinIO 實現一個簡單的文件上傳功能。MinIO 是一個高性能的 object storage server,它兼容 Amazon S3 API,可以作爲靜態網站託管的後端存儲解決方案。而 Vue.js 是目前最流行的 JavaScript 框架之一,它的版本 3(Vue 3)提供了更快的渲染性能和更好的開發者體驗。我們將會創建一個基於 Vue CLI 的項目,然後逐步添加組件和邏輯來實現我們的目標。
首先,我們需要確保你已經安裝了 Node.js 和 npm。如果沒有,你可以從官方網站下載並安裝它們。接着,我們可以通過以下命令來初始化一個新的 Vue 項目:
npx vue create my-vue-app
cd my-vue-app
npm install --save axios @types/node @types/form-data @types/axios vue-router@4 pinia @vue/compiler-sfc core-js typescript @vitejs/plugin-vue @vitejs/plugin-legacy @vitejs/plugin-react @rollup/plugin-alias @rollup/plugin-typescript minio jszip form-data filepond filepond-plugin-file-validate-size filepond-plugin-image-preview filepond-plugin-file-poster-generate filepond-plugin-image-exif-orientation filepond-plugin-image-transform filepond-plugin-file-rename filepond-plugin-file-upload-retry filepond-plugin-hashfile @types/filepond @types/filepond-plugin-file-validate-size @types/filepond-plugin-image-preview @types/filepond-plugin-image-exif-orientation @types/filepond-plugin-image-transform @types/filepond-plugin-file-rename @types/filepond-plugin-file-upload-retry
npm i -D jest ts-jest @types/jest @testing-library/vue @testing-library/dom @testing-library/jest-dom
npm i -D @types/chai chai sinon mocha sinon-chai cross-env
這裏使用了大量的第三方庫,這些庫可以幫助我們更好地處理文件的上傳過程,比如 `FilePond` 插件及其相關的插件。安裝完畢後,我們可以開始編寫代碼。
Step 1: 配置環境
首先,我們需要修改 `tsconfig.json` 以支持一些額外的特性:
{
"compilerOptions": {
// ...其他選項
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"target": "ES5",
"typeRoots": ["src/shims- typings/", "node_modules/@types/"],
"allowSyntheticDefaultImports": true
}
}
接下來,我們在根目錄下新建一個 `.env` 文件,用於存放環境變量,例如:
VUE_APP_MINIO_ENDPOINT=http://localhost:9000
VUE_APP_MINIO_ACCESSKEY=<YourAccessKeyHere>
VUE_APP_MINIO_SECRETKEY=<YourSecretKeyHere>
Step 2: 設置路由
由於我們是純前端應用,因此需要考慮瀏覽器的前向代理問題。爲了解決這個問題,我們可以使用 `vue-router`。在 `main.ts` 中引入路由模塊:
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
const routes = [
{ path: '/', component: Home },
];
export const router = createRouter({
history: createWebHistory(),
routes, // short for `routes: routes`
});
Step 3: 創建組件
現在我們來創建一個名爲 `UploadComponent.vue` 的組件,這個組件將負責整個文件的上傳流程。以下是該組件的基本結構:
<template>
<!-- Your HTML here -->
</template>
<script lang="ts" setup>
import FilePond from 'filepond';
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
import FilePondPluginFileRename from 'filepond-plugin-file-rename';
import FilePondPluginFileRetry from 'filepond-plugin-file-upload-retry';
import FilePondPluginHashFile from '@szwacz/filepond-plugin-hash-file';
import FilePondPluginFilePosters from 'filepond-plugin-file-poster-generate';
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation';
import FilePondPluginImageTransform from 'filepond-plugin-image-transform';
import FilePondPluginFileRenameGenerate from 'filepond-plugin-file-rename-generate';
import type { Ref } from 'vue';
import { ref } from 'vue';
interface FileItem extends File {
name: string;
serverId?: string;
status?: any;
}
interface State {
files: Ref<Array<FileItem>>;
processingFiles: Ref<number[] | null>;
hasErrored: Ref<boolean>;
}
const state: State = reactive({
files: ref([]),
processingFiles: ref(null),
hasErrored: ref(false),
});
const options = {
maxFileSize: '5MB',
};
function handleError(errorMessage: string): void {
console.error('An error occurred:', errorMessage);
}
function getFileNameWithoutExtension(filename: string): string {
return filename.replace(/(\.[^.]*$)/, '');
}
function generateNewFilename(originalName: string): string {
let newName = originalName;
for (let i = 1; ; ++i) {
newName += `-${i}`;
if (!state.files.value.some((item) => item.name === newName)) break;
}
return newName;
}
async function uploadToMinio(file: FileItem): Promise<void> {
try {
const data = await fetch(`${process.env.VUE_APP_MINIO_ENDPOINT}/uploads/${file.name}`, {
method: 'PUT',
headers: {
'Content-Type': file.type || '',
'X-Amz-Meta-Source': `${window.location.origin}${window.location.pathname}`,
'Authorization': `Basic ${btoa(`${process.env.VUE_APP_MINIO_ACCESSKEY}:${process.env.VUE_APP_MINIO_SECRETKEY}`)}`,
},
body: file,
}).then((response) => response.blob());
file.serverId = window.URL.createObjectURL(data);
file.status = 'success';
} catch (e) {
handleError(e.message);
file.status = 'error';
state.hasErrored.value = true;
} finally {
state.processingFiles.value = null;
}
}
function addFileToState(file: FileItem): void {
state.files.value.push(file);
}
function removeFileFromState(index: number): void {
state.files.value.splice(index, 1);
}
function renameFile(file: FileItem, name: string): void {
file.name = name;
}
function processQueue(): void {
if (state.processingFiles.value == null) {
state.processingFiles.value = [];
} else if (state.processingFiles.value.length < state.files.value.length) {
state.processingFiles.value.push(state.files.value.length - 1);
}
}
function clearProcessingFiles(): void {
state.processingFiles.value = null;
}
function resetForm(): void {
state.files.value = [];
state.processingFiles.value = null;
state.hasErrored.value = false;
}
const pond = FilePond.create({
allowMultiple: true,
allowReorder: true,
instantUpload: false,
labelIdle: 'Drop files or click to browse your computer…',
acceptedFileTypes: ['audio/*', 'video/*', 'image/*'],
maxFileSize: '5mb',
server: {
url: `${process.env.VUE_APP_MINIO_ENDPOINT}/uploads`,
withCredentials: false,
},
plugins: [
FilePondPluginFileValidateSize,
FilePondPluginImagePreview,
FilePondPluginFileRename,
FilePondPluginFileRetry,
FilePondPluginHashFile,
FilePondPluginFilePosters,
FilePondPluginImageExifOrientation,
FilePondPluginImageTransform,
FilePondPluginFileRenameGenerate,
],
style: {
width: '100%',
height: 'auto',
},
});
pond.addFile = async (file: Blob) => {
const addedFile = await pond.getFileById(await file.id);
addedFile && addFileToState(addedFile as unknown as FileItem);
};
pond.processFile = async ({ status }) => {
switch (status) {
case 'processstart':
processQueue();
break;
case 'progress':
clearProcessingFiles();
break;
case 'processend':
clearProcessingFiles();
removeFileFromState(state.processingFiles.value!);
break;
default:
break;
}
};
pond.setOption('onupdatefiles', () => {
state.files.value = Array.from(pond.getFiles()).map((file) => ({
...file,
name: getFileNameWithoutExtension(file.name) + '-' + generateNewFilename(file.name),
serverId: undefined,
status: 'waiting',
})) as unknown as Array<FileItem>;
});
pond.setOption('onbeforefiledeleted', ({ id }) => {
state.files.value = state.files.value.filter((f) => f.id !== id);
});
pond.setOption('onrestorefile', ({ file }) => {
state.files.value.unshift(file);
});
pond.setOption('onprocessallcomplete', () => {
resetForm();
});
pond.setOption('onerror', (reason) => {
handleError(reason);
state.hasErrored.value = true;
});
pond.setOption('onprocessfiles', (files) => {
state.files.value = files.map((file) => ({
...file,
name: getFileNameWithoutExtension(file.name) + '-' + generateNewFilename(file.name),
serverId: undefined,
status: 'waiting',
})) as unknown as Array<FileItem>;
});
pond.setOption('onprocessfile', (file) => {
if (file.status === 'queued') {
uploadToMinio(file);
}
});
pond.setOption('onempty', () => {
state.files.value = [];
state.hasErrored.value = false;
});
pond.setOption('oninit', () => {
if (pond.isServerSide()) return;
pond.deselectAll();
});
document.addEventListener('DOMContentLoaded', () => {
pond.mount(document.querySelector('#file-drop'));
});
function reset(): void {
pond.clear();
}
function startUploads(): void {
while (state.processingFiles.value !== null && state.processingFiles.value.length > 0) {
uploadToMinio(state.files.value[state.processingFiles.value[0]]);
state.processingFiles.value.shift();
}
}
function retryFailedUploads(): void {
state.files.value = state.files.value.filter((file) => file.status === 'error');
startUploads();
}
function deleteSelected(): void {
const selectedIds = pond.getSelectedFileIds();
selectedIds.forEach((id) => pond.deleteFile(id));
}
function selectAll(): void {
pond.selectAll();
}
function deselectAll(): void {
pond.deselectAll();
}
function toggleSelectionForCurrentlySelected(): void {
pond.toggleSelectionForCurrentlySelected();
}
function setName(event: Event, input: string): void {
const target = event.currentTarget as HTMLInputElement;
renameFile(state.files.value[target.dataset['index']], input);
}
function setDescription(event: Event, input: string): void {
const target = event.currentTarget as HTMLInputElement;
state.files.value[target.dataset['index']]['description'] = input;
}
function setTags(event: Event, input: string): void {
const target = event.currentTarget as HTMLInputElement;
state.files.value[target.dataset['index']]['tags'] = input.split(',');
}
function setThumbnail(event: Event, input: string): void {
const target = event.currentTarget as HTMLInputElement;
state.files.value[target.dataset['index']]['thumbnail'] = input;
}
export default {
methods: {
reset,
startUploads,
retryFailedUploads,
deleteSelected,
selectAll,
deselectAll,
toggleSelectionForCurrentlySelected,
setName,
setDescription,
setTags,
setThumbnail,
},
setup() {},
};
</script>
<style scoped>
#file-drop {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem;
}
input[type='text'] {
margin: 10px auto;
width: 80%;
border: none;
outline: none;
border-bottom: solid thin #ccc;
}
button {
background: white;
color: black;
font-weight: bold;
border: solid thin grey;
cursor: pointer;
padding: 0.7em 1.2em;
}
button:hover {
background: lightgrey;
}
</style>
這段代碼可能包含了一些你可能不熟悉的函數和方法,但大部分都是標準的 JavaScript 和 TypeScript。如果你對其中某個部分有疑問,可以在網上搜索相關文檔或教程進行學習。
Step 4: 測試與部署
完成上述步驟後,你應該已經有了一個基本的文件上傳系統。然而,請記住這只是一個基礎指南,實際的開發過程中可能會遇到更多複雜的情況。建議你在本地充分測試你的應用程序,確保它在不同的設備和環境中都能正常工作。一旦你覺得一切就緒,你可以選擇將其部署到雲服務或其他平臺上。
以上就是如何在純前端項目中使用 Vue 3 和 MinIO 來實現文件上傳功能的指導。希望這篇文章對你有所幫助!