純JS實作照片上傳、下載與預覽
Published: 2022-03-13 13:08:26

更多會員限定文章可以到[patreon觀看](https://www.patreon.com/wade3c)

-----

> 完整code可以到[以下gist](https://gist.github.com/johnny12150/2c1ab2efe552fd24e541cad96ad2e161)

# Client端HTML

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        img {
            width: 150px;
          }
    </style>
</head>
<body>
    <input type="file" id="file-uploader" accept="image/png" multiple="multiple"/>
    <img id='preview'> 
</body>
</html>
```

- input tag
   - type設成file，讓使用者能選擇檔案
   - accept設定使用者能選的格式，如果要所有照片格式就改成`image/*`
   - multiple讓使用者能選擇多個檔案

# Client端JS

> 我們會用JSON來傳遞資料(也可以用form-data)

### 取得使用者選擇的照片

讀進來的每個檔案會是blob（Binary Large Object）

```javascript
const fileUploader = document.querySelector('#file-uploader');
fileUploader.addEventListener('change', (e) => {
   console.log(e.target.files); // get list of file objects
});
```

`e.target.files`是fileList

### 將照片轉成base64 string

```javascript
function display_img(curFiles) {
    const curFile = curFiles[0];
    const reader = new FileReader();
    // 這會在readAS後才執行
    reader.onload = function (e) {
        // console.log('file:', e.target.result); // base64
        document.querySelector('#preview').src = e.target.result;
    };

    // to data url
    reader.readAsDataURL(curFile);
}
```

`img.src`能顯示url with base64

### 將blob轉成array buffer

相較於buffer, blob保留了照片的資訊，像是檔名、MIME type等

```javascript
function blob2buffer(blob) {
    return new Promise((resolve, reject) => {
        var arrayBuffer;
        var fileReader = new FileReader();
        fileReader.onload = function (event) {
            arrayBuffer = event.target.result;
            resolve(arrayBuffer);
        };

        fileReader.readAsArrayBuffer(blob);
        return arrayBuffer;
    });
}
```

先將blob轉成buffer，方便後續建成array

### 將array轉成字串post到server

buffer不能直接操作，要先轉成指定type的array

```javascript
arrayBuffer = await blob2buffer(pic);

fetch(url, {
    method: 'POST',
    // flask need to set header of json
    headers: {
        'content-type': 'application/json'
    },
    body: JSON.stringify({
        item_id: 1,
        format: 'png',
        img: Array.from(new Uint8Array(arrayBuffer)),
    }),
})
```

轉成array前要先將buffer轉成typed array

### 讀取server端傳回來的array

產生blob的url (不同於前面產base64的fileReader)

```javascript
function display_arr_img(arr) {
    document.querySelector('#preview').src = URL.createObjectURL(new Blob([new Uint8Array(arr)], {type: "image/png"}));
}
```

將array轉回typed array，再將其變回blob

----

# Server端API

> 這裡我們用python + flask作為例子

## DataBase

這裡我們用MySQL，然後照片的欄位type是blob，要注意blob有分可以存的size

- `TINYBLOB`: maximum length of 255 bytes
- `BLOB`: maximum length of 65,535 bytes  => 64kb
- `MEDIUMBLOB`: maximum length of 16,777,215 bytes
- `LONGBLOB`: maximum length of 4,294,967,295 bytes

### 連接到資料庫

```python
import pymysql

connection = pymysql.connect(host='localhost', user='root', password='password', db='db01', charset='', cursorclass=pymysql.cursors.DictCursor)
```

**注意**pymysql不支援connection string

## Python Code

> 所需套件在requirements.txt內

存到blob的好處是不會讓照片被額外index (會導致檔案大小增幅30%)

```python
@app.route('/upload/json/img_raw', methods=['POST'])
def raw_img():
    json_data = flask.request.json

    # save to db
    with connection.cursor() as cursor:
        sql = "UPDATE table1 SET `img_raw`=%s WHERE id=%s"
        try:
            # img現在是list, 可以用json string/ byterarray/ base64存進資料庫
            cursor.execute(sql, (json.dumps(json_data['img']), json_data['item_id']))
            connection.commit()
        except Exception as err:
            connection.rollback()
            return flask.jsonify({'err': err})

        # get a picture
        sql = "SELECT * FROM table1"
        cursor.execute(sql)
        result = cursor.fetchone()
        r = result[0]['img_raw']
        r = json.loads(r.decode())  # utf-8, 也可用ascii
        
    return flask.jsonify({'img': r})
```

存進mysql blob欄位後，會變成binary object

- `json.dumps(json_data['img']`
   - 我們要將List轉成json style的string才能存進資料庫
- `json.loads(r.decode())`
   - 存資料庫拿出來的會是binary object，我們要先將其轉回字串
   - 然後再將字串 parse回list