[One step at a time] front pull up load, pull down refresh
[One step at a time] front-end file upload a
【 One step at a time 】 Front-end file upload ii: fast upload of large files
File upload
File upload is essentially sending a request to the background in the form of a binary file
How do we pass it around for binary files
Before we talk more about this, we need to understand the concept of content-Type
HTTP request header Content-type
Content-type The entity header is used to indicate the MEDIA Type of the MIME Type of the resource
In a request (such as POST or PUT), the client tells the server what type of data is actually being sent
The commonly used media types include data interface Application/JSON, text/ CSS, image/ JPEG
application/json |
text/css |
image/jpeg |
---|---|---|
If you want to get a better understanding of media Type look at this, portal
For file uploads, the content-type should be set to multipart/form-data
Multipart /form-data can be used for HTML forms to send information from the browser to the server. As a multi-part document format, it consists of different parts separated by a boundary (a string beginning with a ‘–‘). Each part has its own entity, as well as its own HTTP request header, content-Disposition and Content-Type are used in the file upload world, the most common (Content-Length is ignored because the border is used as a separator)
Let’s look at the difference between a form request with multipart/form-data set and one without
/ / the front<form action="http://localhost:8080/upload" method="post">
<input type="file" name="file" id="mfile2">
<button type="submit">upload</button>
</form>
<form action="http://localhost:8080/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="mfile3">
<button type="submit">upload</button>
</form>
Copy the code
/ / the back end
router.post('/upload'.ctx= > {
console.log(ctx.request.files)
ctx.set("Access-Control-Allow-Origin"."*");
ctx.body = "111"
})
Copy the code
Don’t addmultipart/form-data |
addedmultipart/form-data |
|
---|---|---|
Request header | ||
After the end result |
Obviously for form forms, content-type defaults to Application/X-www-form-urlencoded, which you’ll have to specify if you’re going to upload files
Easiest file upload
In the previous demo, file uploads used the form form, specifying its encType for file uploads. However, there are too many disadvantages to using the form form, and FormData is now recommended for file uploads
The FormData interface provides a way to construct key/value pairs that represent FormData and can easily send the data through the xmlhttprequest.send () method, both of which are fairly straightforward
Examples are as follows:
To use FormData, do the following:
- Create a new form instance:
new FormData();
- Add the file you want to pass to the form:
form.append('file', file);
- Sending Ajax Requests
<input type="file" name="" id="mfile">
<button id='btn'>upload</button>
Copy the code
const mfile = document.querySelector('#mfile');
const btn = document.querySelector('#btn')
const form = new FormData();
const url = 'http://localhost:8080/upload';
btn.addEventListener('click', upload)
async function upload() {
const file = mfile.files[0];
form.append('file', file);
uploadAjaxApi()
// const result = await uploadAxiosApi(url, form)
}
function uploadAjaxApi() {
/ / underlying Ajax
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.onload = function () {
console.log(xhr.responseText)
}
xhr.send(form)
}
async function uploadAxiosApi(url, prams) {
/ / based on axios
return await axios.post(url, prams)
}
Copy the code
Request header | The result from the back end |
---|---|
Make an upload component
According to the upload component written by myself, except for the core problem file upload (explained above), other problems and solutions of components are summarized as follows
-
Question Point 1:
- Note: The native Input :file form style is ugly and varies from browser to browser. The upload function must borrow the input form
- Redefine the form style, or implement input:file indirectly based on custom/user-defined button implementations
-
Question Point 2:
- Problem Description: Progress bar display, how to achieve multi-file progress bar, upload files one to one correspondence
- Each file sends a request. The progress bar monitors the upload progress through the onProgress method under xmlHttprequest. upload. E. loaded and E. total represent the uploaded file size and total file size respectively
-
【 extension 】
- How to write the back-end code
- Nodejs is used to create a server environment using KOA. The received files are saved locally (on other servers), and the file addresses and information are saved in the database
After understanding the above three problems, the next step is to solve the file directory
Ps: normal logic uploaded files are in the folder where the back-end code (i.e., upload a file location), but I want to save directly on the front end (that is, the upload file location), through the vue – service can access to the cli, the ideal is plump reality very bone feeling, vue – cli up service image will bring a hash value, the method doesn’t work
1. Input: file form
My method is to hide the input:file form, listen for the user-set button click event through the slot, and use the hidden input element, the official document portal, through the input.click() method
Use the hidden File Input element with the click() method
<input type="file" id="file" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)">
<button id="btn">Select some files</button>
Copy the code
const btn = document.getElementById("btn"),
file = document.getElementById("file");
btn.addEventListener("click".function (e) {
if(file) { file.click(); }},false);
Copy the code
Now that we know how to do this, let’s start making the components
<! -- app.vue -->
<upload :multiple="true" action="/api/upload">
<button class="btn">upload</button>
</upload>
<! -- upload.vue -->
<template>
<div>
<input
type="file"
name="file"
id=""
ref="input"
:multiple="multiple"
@change="handleChange"
/>
<div class="upload" @click="onClickTrigger">
<slot></slot>
</div>
</div>
</template>
<style>
input {
display: none;
}
</style>
Copy the code
export default {
name: "Upload".props: {
multiple: { // Whether multiple files can be uploaded
type: Boolean.default: false,},action: { // Back-end interface
type: String.default: "".required: true,}},data() {
return {
files: null.// List of files to be uploaded
uploadFinishList: [].// After the upload is complete, the data returned by the back end is in the following format
/ / [{
FilesData :[], // Store the data returned by the backend
// processBar:{} // Stores progress bar information
/ /}]
};
},
methods: {
onClickTrigger() {
this.$refs.input.click();
},
async handleChange(e) {
const files = e.target.files;
this.files = files;
this.uploadFinishList = [];
files.forEach((file, index) = > { // Place the files that need to be uploaded in formData
const form = new FormData();
form.append(`files`, file);
this.uploadFinishList.push({ // Initializes the progress bar
processBar: { loaded: 0.total: 0}});this.sendUpload(form, index); // Send an upload request}); ,}}};Copy the code
The click event bubbles to the parent element that wraps the button and triggers the click event. The click() method uses the hidden file Input element to trigger the change event of the Input: File form when a file is uploaded, sending the upload request
2. The progress bar
The progress bar can monitor the upload progress through the onProgress method under xmlHttprequest. upload. E. loaded and E. total represent the uploaded file size, the total file size, and the official document portal respectively
/* onprogress method under xmlhttprequest. upload to get the current uploaded file size and total file size */
// @/server/index.js
export function upload({ methods, url, form }, cb, index) {
return new Promise(resolve= > {
const xhr = new XMLHttpRequest();
xhr.open(methods, url);
xhr.onload = function () {
resolve(xhr.responseText);
}
xhr.upload.onprogress = function (e) {
// Pass the data required by the progress bar and execute the progress bar functioncb(index, e.loaded, e.total) } xhr.send(form); })}Copy the code
<! -- app.vue -->
<upload :multiple="true" action="/api/upload">
<button class="btn">upload</button>
</upload>
<! -- upload.vue -->
<template>
<div>
<input
type="file"
name="file"
id=""
ref="input"
:multiple="multiple"
@change="handleChange"
/>
<div class="upload" @click="onClickTrigger">
<slot></slot>
</div>
<ul class="upload-list" >
<li v-for="(list,index) in uploadFinishList" :key="list.id">
<span>{{ files[index].name }}</span> - <span>{{list.processbar. total}}</span> -
<span>{{list.processbar.loaded}}</span>
</li>
</ul>
</div>
</template>
Copy the code
import { upload } from "@/server/index.js";
export default {
name: "Upload".props: {... },data() {
return {
files: null.// Get the file to upload
uploadFinishList: [].// Store the data returned by the back end after the upload
processBarIsShow: false}; },methods: {
onClickTrigger() {
this.$refs.input.click();
},
async handleChange(e){...this.processBarIsShow = true;
files.forEach((file, index) = > { // Place the files that need to be uploaded in formData.this.sendUpload(form, index);
});
},
async sendUpload(form, index) {
/ / upload
const result = await upload(
{
methods: "post".url: this.action,
form,
},
this.progress,
index
);
if (this.uploadFinishList[index]) {
this.$set(
this.uploadFinishList[index],
"filesData".JSON.parse(result).files
);
} else {
this.uploadFinishList.push({
filesData: JSON.parse(result).files, }); }},progress(index, loaded, total) {
// Progress bar function, CSS does not do, with the current progress and total size to create a progress bar is very simple
this.uploadFinishList[index].processBar.loaded = loaded;
this.uploadFinishList[index].processBar.total = total; ,}}};Copy the code
Analysis: Before sending the request, upload the file, index and progress bar function, together with the API request interface, in the onProgress function to execute the progress bar function, at the same time through the file index and upload the file to form a connection
3. Back-end code
Since there is no ready-made interface to use, nodeJS is used to build a simple back-end interface
const Koa = require('koa');
const serve = require("koa-static");
const Router = require('koa-router');
const Koabody = require('koa-body');
const fs = require('fs');
const path = require('path');
const mysql = require("mysql2/promise");
const app = new Koa();
const router = new Router();
app.use(Koabody({
multipart: true,}));class DB { // With mysql database, monogo does not
constructor(options) {
this.options = Object.assign({
host: "localhost".password: "123456".user: "root".database: "hcm",
}, options);
}
async initDB() {
const connection = await mysql.createConnection(this.options);
returnconnection; }}// Initialize the database
const db = new DB();
router.post('/upload'.async ctx => {
/ * * *@description: ctx.request.files must be an object passed with an object FileList error */
ctx.set("Access-Control-Allow-Origin"."*");// Allow cross-domain
const files = ctx.request.files.files;
if(! files) { ctx.body = {'msg': "No pictures passed".'code': 401 }
return;
}
const res = await uploadControl(files); // Upload handler
ctx.body = JSON.stringify({
files: res
});
})
function uploadControl(files) {
return new Promise(resolve= > {// asynchronous save is enabled for each file, and all files are saved using promise. all
const asyncQueue = [];// Asynchronous queue
if (Array.isArray(files)) {
files.forEach(file= >{ asyncQueue.push(uploadService(file)); })}else {
asyncQueue.push(uploadService(files));
}
Promise.all(asyncQueue).then(res= >{ resolve(res); })})}function uploadService(file) {
return new Promise(async resolve => {
let temp = null;
const img = file;
if (!Object.keys(img).length) {
// ctx.body = ctx.request.files.file
ctx.body = { 'msg': "No pictures passed".'code': 401 }
return;
}
const { name = ' '.path: imgPath } = img;
const newTime = createNewTime();
const newName = createImgName(name);
// Save the image and provide the save address
const imgSavePath = saveImg(newName, imgPath);
// Save the newly obtained image address to the database
const result = await insertToDb({ // Insert the image information into the database
title: name,
from: 'local'.// Image source
imgUrl: imgSavePath,// The address where the image is saved
newTime,
});
if (result.affectedRows > 0) {
temp = {
title: name,
imgUrl: imgSavePath,
newTime,
id: Date.now() } } resolve(temp); })}function saveImg(newName, imgPath) {
const readStream = fs.createReadStream(imgPath);
const uploadPath = path.resolve(__dirname, '.. /frontEnd/app/src/img', newName);
const writeStream = fs.createWriteStream(uploadPath);
readStream.pipe(writeStream);
return './img/' + newName;
}
async function insertToDb({ title, from, newTime, imgUrl }) {
const connection = await db.initDB();
const sql =
"INSERT INTO news (id,title,imgUrl,`from`,newTime) VALUES (0,? ,? ,? ,?) ";
const [result] = await connection.execute(sql, [title, imgUrl, from, newTime]);
return result;
}
function createImgName(name) {
//vue packs the hash value of the image
const hash = '.da76812b.'
const nameArr = name.split('. ');
// 1606563510414_dog.da76812b.jpg
return Date.now() + "_" + nameArr[0] + hash + nameArr[1];
}
function createNewTime() {
const date = new Date(a);return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
app.use(serve(path.resolve(__dirname + "/img")))
app.use(router.routes())
app.listen(8080)
Copy the code
Analysis: After receiving the upload information, the backend starts asynchronous file saving, inserts the file information and save path into the database, and finally returns all saved data to the front-end
Here the most basic file upload function is done, but it does not mean that the end of the file drag upload, how to transfer large files faster, breakpoint continuation and how to do, these problems still need to be explored and improved