IO operation of client JavaScript

1. Local disk IO - "click" to read

1.1 create FileReader object

The FileReader object is used for read operations in IO operations

let fd = new FileReader()

1.2 using FileReader object to read files

  1. fd.readAsArrayBuffer() reads a file asynchronously and returns ArrayBuffer

  2. fd.readAsBinaryString() reads a file asynchronously and returns binary form

  3. fd.readAsDataURL() reads a file asynchronously and returns Base64 encoding

  4. fd.readAsText() reads a file asynchronously and returns the text form

1.3 setting the attribute of input tag

Click the input tab to open the file manager

  1. The type attribute is set to file, indicating that the input tag is used to read the file

  2. The multiple attribute can be optionally set to multiple, indicating whether multiple files are allowed to be read

  3. Listen for change events

<input type="file" multiple="multiple" onchange="handleChanged()">
<script>
    function handleChanged() {
        // Traverse all open files
        Array.prototype.forEach.call(event.target.files, (file => {
            let fd = new FileReader()
            // Using ArrayBuffer storage
            fd.readAsArrayBuffer(file)
            fd.onload = function(event) {
                console.log(event.target.result)
            }
        }))
    }
</script>

1.4 click read of special style

In actual use, we usually do not directly use the input tag, because the style often does not meet the requirements, which can be solved in the following ways.

  1. Set the display property to hide the input tag

  2. Create a click upload button and set the desired style on the button

  3. Set that clicking this button will trigger the click event of the input tag

<div style="width: 100px; height: 100px; background: blue;" onclick="handleClick()"></div>
<input type="file" style="display: none;" onchange="handleChanged()" id="input">
<script>
    let input = document.querySelector("#input");
    
    function handleClick() {
        input.click()
    }
    
    function handleChanged() {
        // Suppose only one file is allowed to be read
        let fd = new FileReader(), file = event.target.files[0]
        fd.readAsDataURL(file)
        fd.onload = function(event) {
            console.log(event.target.result)
        }
    }
</script>

2. Local disk IO - "drag" read

2.1 basic drag and drop event learning

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-oqxqwtud-1642742310) (. / assets / dragevent. PNG)]

draggable = true indicates that the element is allowed to be dragged

  1. The ondragstart drag start event is registered on the container for status judgment and drag information carrying
  2. The ondragener drag into the container event is registered on the container for status judgment
  3. The ondragover event is dragged into the container and registered on the container. The default event needs to be disabled. The default event is prohibited from being dragged into the container
  4. The ondragleave drag and drop event is registered on the container for status judgment
  5. The ondrop drag mouse release event is registered on the container. The default event needs to be disabled to read drag information and place drag elements
  6. Ondragenddrag end event, used to judge the status

2.2 basic drag event demonstration

<div style="border: 1px solid black; width: 200px; height: 100px;"
     ondragstart="handleDragStart()"
     ondrop="handleDrop()"
     ondragover="handleDragOver()"
     ondragenter="handleDragEnter()"
     ondragleave="handleDragLeave()"
     ondragend="handleDragEnd()">
    <p draggable="true" id="p">Danny</p>
</div>

<div style="border: 1px solid black; width: 200px; height: 100px; margin-top: 44px;"
     ondragstart="handleDragStart()"
     ondrop="handleDrop()"
     ondragover="handleDragOver()"
     ondragenter="handleDragEnter()"
     ondragleave="handleDragLeave()"
     ondragend="handleDragEnd()"></div>
<script>
    
    // ondragstart
    // Triggered when drag starts
    function handleDragStart() {
        // Add the id of the drag element to the attribute of the drag event. When the drag is over, the drag element can be found
        event.dataTransfer.setData("Danny", event.target.id)
    }
    
    // ondragenter
    // Triggered when an element is dragged into the container
    function handleDragEnter() {
        console.log("Enter container", event.target)
    }
    
    // ondragleave
    // Triggered when an element is dragged out of the container
    function handleDragLeave() {
        console.log("Leave container", event.target)
    }
    
    // ondragend
    // Triggered when the drag is complete
    function handleDragEnd() {
        console.log("Drag and drop complete")
    }
    
    // ondrop
    // Triggered when the mouse is released while dragging
    function handleDrop() {
        // Native events cannot be dragged into other places
        event.preventDefault()
        // Place the dragged element into the current element
        event.target.append(document.querySelector(`#${event.dataTransfer.getData("Danny")}`))
    }
    
    // ondragover
    // Triggered when an element is dragged into a container
    function handleDragOver() {
        // Native events cannot be dragged into other places
        event.preventDefault()
    }
</script>

2.3 drag read

  1. Register the ondrop event for the container, prohibit native events, and use FileReader to read

  2. Register the ondragover event for the container and prohibit native events

<div style="width: 200px; height: 100px; border: 1px solid black;" ondrop="handleDrop()" ondragover="handleDragOver()"></div>
<script>
    function handleDragOver() {
        event.preventDefault()
    }

    function handleDrop() {
        event.preventDefault()
        // By default, only one file is dragged and uploaded here
        let file = event.dataTransfer.files[0]
        let fd = new FileReader()
        fd.readAsArrayBuffer(file)
        fd.onload = function(event) {
            console.log(event.target.result)
        }
    }
</script>

2.4 read progress issues

The instantiated object of FileReader, which can monitor the reading progress

fd.onprogress = function(event) {
    console.log(event.loaded)
}

3. Local disk IO - write

3.1 object URL and Blob

  1. Blob represents a binary large object. In the IO of client JavaScript, blob object and File object are in the same position (but actually File is a subclass of BLOB), both of which represent a large binary File.

  2. Object URL is different from blob and File. Blob and File are understood as the whole binary large File, and the object URL is understood as the URL pointing to the File, excluding the File itself. URL. Can be used to generate object URL Createobjecturl (), and the parameter can be File or blob.

3.2 a label writing

  1. Set the href attribute of the a tag to the URL of the corresponding resource. If there is no URL, you can obtain the file and generate the URL through the object URL

  2. Set the download attribute of the a tag to the name of the file you want to download

3.3 special style a label writing

It is also similar to the problem of using input flexibly mentioned above

  1. Set the display attribute of a tag to none to hide it

  2. Set a new Click to download element, and set its click event as the click event that triggers the a tag

4. Network IO ---- Download

4.1 readable stream learning of client JavaScript

  1. Readable stream object: ReadableStream

  2. Controllable readable flow: pass in the parameter {start(control) {}} in the constructor to represent the readable flow control parameters

  3. Encapsulation of controllable readable stream: in the start function, through control Enqueue writes data to the readable stream. When the data is written, use control Close closes the readable stream

  4. Read of controllable readable stream: through readablestream GetReader () obtains the read lock s, which can be read safely. After that, we call the read lock S.read() to read the data. read() is actually the next() method that calls the asynchronous iterator. For details, see "JavaScript asynchronous programming" blog.

<script>
    // Create an asynchronous iterator, which is implemented by the generator
    let asyncIter = (function* asyncMaker() {
        for (let i = 0; i < 5; i++)
            yield new Promise(res => setTimeout(res.bind(globalThis, i), 1000))
    })();

    // Create a readable stream to encapsulate the underlying data
    let stream = new ReadableStream({
        async start(control) {
            // Traverse the asynchronous iterator and load the data into a readable stream
            for await(let value of asyncIter)
                control.enqueue(value)
            // After writing data, the readable stream is closed
            control.close()
        }
    });

    // Get read lock of readable stream
    let readLock = stream.getReader();

    // Read the readable stream automatically, here in recursive form
    (function () {
        let that = arguments.callee
        readLock.read().then(res => {
            if (!res.done) {
                console.log(res.value)
                that()
            }
        })
    })();
</script>

4.2 fetchAPI to realize network IO download progress bar

By getting the readable stream response getReader(). Read() to get the current download progress

Web code

<p>Downloading</p>
<progress id="progress" value="0"></progress>
<script>
    // The actual time is: data flow transmission time in the network + data flow into binary objects + local disk IO time (only three time-consuming delays are listed)
    // The time displayed in the progress bar is the data flow transmission time in the network
    // This problem is also true in XHR. Large files can be downloaded directly with the a tag, but you can't customize the style to display the progress

    let progress = document.querySelector("#progress")

    fetch("test.mp4")
        .then(res => {
            let size = res.headers.get("Content-Length")
            let readLock = res.body.getReader()
            let sum = 0
            // Set progress bar capacity
            progress.max = size

            let stream = new ReadableStream({
                async start(control) {
                    while(true) {
                        let { done, value } = await readLock.read()
                        if(done) {
                            control.close()
                            break
                        }
                        sum += value.length
                        progress.value = sum

                        // Put the data block into a new stream. After reading, the readLock stream has ended. If you want to give it to a tag for download, you need a new stream from scratch
                        control.enqueue(value)
                    }
                }
            })
            return new Response(stream)
        })
        .then(res => res.blob())
        .then(blob => {
            let a = document.createElement("a")
            a.style.display = "none"
            a.href = URL.createObjectURL(blob)
            a.download = "video.mp4"
            document.body.prepend(a)
            a.click()
            // Reclaiming URL memory and removing the a tag is equivalent to reducing a reference to the file
            URL.revokeObjectURL(a.href)
            document.body.firstElementChild.remove()
        })
</script>

Proxy server code

Video test mp4 (the following code is in mp4 format) is placed in the current directory. Although it is placed locally, in fact, during the local loop test, the data will pass the loop test network card, and there will be network delay.

let fs = require("fs").promises
require("http").createServer((req, res) => {
    if(req.url.includes("html")) {
        fs.readFile("." + req.url).then(data => {
            res.writeHead(200, {
                "Content-Type": "text/html"
            })
            res.write(data)
            res.end()
        })
    } else if(req.url.includes("mp4")) {
        fs.readFile("." + req.url).then(data => {
            res.writeHead(200, {
                "Content-Type": "video/mp4",
                "Content-Length": data.length
            })
            res.write(data)
            res.end()
        })
    }
}).listen(3000)

If the download speed is too fast, you can set the download speed limit in the debugging platform to more clearly display the progress of the progress bar. The following is the settings in Google browser. The download speed is set to 2MB per second. The unit in the figure is bit/s.

4.3 XHR realizes network IO download progress bar

By listening to XHR Use the onprogress event to get the download progress
Note: pay attention to setting responseType, otherwise garbled code will be received

Web code

<p>Downloading</p>
<progress id="progress" value="0"></progress>
<script>
    let progress = document.querySelector("#progress")

    let xhr = new XMLHttpRequest()
    xhr.open("get", "test.mp4")
    xhr.responseType = "blob"
    xhr.send(null)

    xhr.onload = function() {
        if(xhr.readyState === 4 && xhr.status === 200)
            xhrCallback(xhr.response)
    }

    xhr.onprogress = function(event) {
        progress.max = event.total
        progress.value = event.loaded
    }

    function xhrCallback(res) {
        console.log(res)
        let a = document.createElement("a")
        a.href = URL.createObjectURL(res)
        a.download = "video.mp4"
        a.style.display = "none"
        document.body.append(a)
        a.click()
        URL.revokeObjectURL(a.href)
        document.body.lastElementChild.remove()
    }
</script>

The proxy server code is the same as above

5. Network IO - Upload

5.1 FormData serialization and JSON serialization

The upload operation uses the post method to carry the payload by default, which is the body part of the HTTP request. When the client sends a request, the body needs to be serialized before it can be correctly received by the server.

  1. JSON serialization: JSON serialization is structured cloning, but it has defects. Some special reference types cannot be structured cloning (see another blog "deep copy and shallow copy of JavaScript data" for details)

  2. FormData serialization: it is a little more troublesome than JSON, but it can solve binary file transmission. The transmission can be of string or Blob type. If you want to transfer Blob objects, JSON serialization cannot structurally clone them, but FormData can be transferred normally.

5.1.1 presentation of formdata

Web code

<script>
    let fd = new FormData()
    fd.append("name", "Danny")
    fd.append("age", "20")
    fd.append("gender", "man")

    fetch("/Vanghua", {
        method: "post",
        body: fd
    }).then(res => res.text()).then(console.log)
</script>

Server code

let fs = require("fs").promises
require("http").createServer((req, res) => {
    if(req.url.includes("html")) {
        fs.readFile("." + req.url).then(data => {
            res.writeHead(200, {
                "Content-Type": "text/html"
            })
            res.write(data)
            res.end()
        })
    } else if(req.url.includes("Vanghua")) {
        req.on("data", function(chunk) {
            console.log(chunk.toString())
        })
        res.writeHead(200)
        res.write("success")
        res.end()
    }
}).listen(3000)

Server output

"------- WebKitFormBoundaryqd9O7Vy6pSDqNKIq" is the field separator. The boundary field is automatically added in the content type, and the field value is this separator.

------WebKitFormBoundaryqd9O7Vy6pSDqNKIq
Content-Disposition: form-data; name="name"

Danny
------WebKitFormBoundaryqd9O7Vy6pSDqNKIq
Content-Disposition: form-data; name="age"

20
------WebKitFormBoundaryqd9O7Vy6pSDqNKIq
Content-Disposition: form-data; name="gender"

man
------WebKitFormBoundaryqd9O7Vy6pSDqNKIq–

5.1.2 division of formdata field

Web code

<script>
    let fd = new FormData()
    fd.append("name", "Danny")
    fd.append("age", "20")
    fd.append("gender", "man")
    fd.append("school", JSON.stringify({
        name: "SDU",
        age: 120
    }))

    fetch("/Vanghua", {
        method: "post",
        body: fd
    }).then(res => res.text()).then(console.log)
</script>

Server code

Note: the following cutting code is only applicable to cutting ordinary transmission strings, not media transmission

let fs = require("fs").promises
require("http").createServer((req, res) => {
    if(req.url.includes("html")) {
        fs.readFile("." + req.url).then(data => {
            res.writeHead(200, {
                "Content-Type": "text/html"
            })
            res.write(data)
            res.end()
        })
    } else if(req.url.includes("Vanghua")) {
        let ct = req.headers["content-type"]
        let boundary = ct.slice(ct.indexOf("boundary") + 9)
        let data = ""

        req.on("data", function(chunk) {
            // The received chunk is Buffer type data and stream data. It is spliced with data and converted into string form
            data += chunk
        })

        req.on("end", function() {
            // Give the spliced data blocks to the segmentation function to process the separator to obtain the final result object
            let res = getData(data, boundary)
            console.log(res)
        })

        res.writeHead(200)
        res.write("success")
        res.end()
    }
}).listen(3000)

// Split data for FormData
function getData(str, boundary) {
    let res = {}
    // When the separator is actually applied, there are two "--" in front of it
    boundary = "--" + boundary
    while(str.length) {
        // Each separator is followed by a carriage return + line feed, so 2 additional separators are cut off
        str = str.slice(str.indexOf(boundary) + boundary.length + 2)
        // There will also be a separator at the end of the last field. So far, after cutting, there will be a carriage return + line feed left. Avoid assigning an empty key and value to res and exit directly
        if(str.length === 2)
            break

        let keyStart = str.indexOf("name") + 6
        // End position of key: after the key is enter + line feed. Find the carriage return position, and then - 1 arrive at the "position. Since the slice is closed on the left and opened on the right, it can arrive"
        let keyEnd = str.indexOf("\r") - 1

        // Start position of value: the first line feed is at the end of the key, and the end of the key is enter + line feed. There is a blank line under the key, which is enter + line feed. Below the blank line is the value, so add 3
        let valueStart = str.indexOf("\n") + 3
        // Minus 2 is minus carriage return + line feed
        let valueEnd = str.indexOf(boundary) - 2

        let key = str.slice(keyStart, keyEnd)
        let value = str.slice(valueStart, valueEnd)

        res[key] = value
    }
    return res
}

Server output

{
name: 'Danny',
age: '20',
gender: 'man',
school: '{"name":"SDU","age":120}'
}

5.1.3 use Multiparty package to solve the cutting of FormData

npm install Multiparty to install and transfer the package processing FormData
The following demonstrates the basic usage:

  1. For ordinary fields, the function of getData function implemented in 5.1.2 can be realized, and ordinary fields can be accessed through fields. (the field form is key: [value])

  2. For Blob type, you can access it through files. The following code demonstrates the code that the server receives the uploaded video from the client and saves it.

let fs = require("fs").promises
let fss = require("fs")
let multiparty = require("multiparty")
require("http").createServer((req, res) => {
    if(req.url.includes("html")) {
        fs.readFile("." + req.url).then(data => {
            res.writeHead(200, {
                "Content-Type": "text/html"
            })
            res.write(data)
            res.end()
        })
    } else if(req.url.includes("Vanghua")) {
        // Use multiparty to parse the passed FormData
        let form = new multiparty.Form()
        form.parse(req, function(err, fields, files) {
            // Save common data in fields and file data in files (Blob type passed)
            if(files.video[0]) {
                // file.path is the current video cache address. multiparty parses the file in FormData and saves it to the cache
                let file = files.video[0]
                // Read and write the video in the cache to the specified location
                let readStream = fss.createReadStream(file.path)
                let writeStream = fss.createWriteStream("./Danny.mp4")
                readStream.pipe(writeStream)
                // Listen for the status of the stream
                writeStream.on("finish", function() {
                    console.log("Write complete")
                })
                // Clear the video in the C disk cache
                fss.unlinkSync(file.path)
            }
            res.writeHead(200)
            res.write("success")
            res.end()
        })
    }
}).listen(3000)

5.1.4 precautions for using formdata

When the client uses FormData, pay attention to the content type in the Headers of the HTTP request. FormData will automatically add it to the boundary, and will automatically generate the content type according to the content you transmit. It's easy to make mistakes in writing content type by yourself. Many blog posts on online blogs report that the content type they think is correct is wrong. Therefore, the content type setting in the request can be ignored.

5.2 upload and display progress bar

The upload progress bar can only be completed by XHR by listening to the onprogress event of XHR's upload. The fetch can receive the data stream during downloading, but cannot obtain the data stream of the fetch during uploading, so there is no way to start monitoring the progress.

Web code

The following code demonstration still takes the video as an example

<input style="display: none;" type="file" accept="video/*" id="input" onchange="handleChanged()"/>
<button onclick="handleClick()">Select upload file</button>
<p>Uploading</p>
<progress id="progress" value="0"></progress>
<video id="video" style="display: block;" controls></video>
<script>
    let progress = document.querySelector("#progress")
    let input = document.querySelector("#input")

    function handleClick() {
        input.click()
    }

    function handleChanged() {
        // Realize video preview
        video.src = URL.createObjectURL(event.target.files[0])

        let xhr = new XMLHttpRequest()
        xhr.open("post", "/Vanghua")
        
        // Monitor progress
        xhr.upload.onprogress = function(event) {
            progress.max = event.total
            progress.value = event.loaded
        }
        
        // Receive server reply
        xhr.onload = function() {
            console.log(xhr.responseText)
        }
        
        // Mount through FormData introduced in 5.1
        let fd = new FormData()
        fd.append("video", event.target.files[0])
        xhr.send(fd)
    }
</script>

The server code is the same as that in 5.1.3

5.3 upload problem -- breakpoint retransmission

Keywords: Javascript Front-end html5

Added by chrispos on Sat, 22 Jan 2022 02:23:42 +0200