[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:

  1. Create a new form instance:new FormData();
  2. Add the file you want to pass to the form:form.append('file', file);
  3. 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

  1. Question Point 1:

    1. Note: The native Input :file form style is ugly and varies from browser to browser. The upload function must borrow the input form
    2. Redefine the form style, or implement input:file indirectly based on custom/user-defined button implementations
  2. Question Point 2:

    1. Problem Description: Progress bar display, how to achieve multi-file progress bar, upload files one to one correspondence
    2. 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
  3. 【 extension 】

    1. How to write the back-end code
    2. 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