笨办法学前端之图片上传

6,666 阅读2分钟

笨办法学前端」新增轮子一枚:原生 JS 实现的图片上传。

预览:Image Picker

源码:

{
  let FormData = window.FormData
  class ImagePicker {
    constructor(options) {
      let defaultOptions = {
        element: null,
        upload: {
          url: '',
          method: '',
          inputName: '',
        },
        parseResponse: null,
        fallbackImage: '',
      }
      this.options = Object.assign({}, defaultOptions, options)
      this.checkOptions()
      this.domRefs = {
        img: this.options.element.querySelector('img'),
      }
      this.initHtml()
      this.bindEvents()
    }
    checkOptions() {
      let { element, upload: { url, method, inputName } } = this.options
      if (!element || !url || !method || !inputName) {
        throw new Error('Some option is required')
      }
      return this
    }
    initHtml() {
      let { element } = this.options
      let fileInput = (this.domRefs.fileInput = dom.create('<input type="file"/>'))
      dom.append(element, fileInput)
    }
    willUpload(formData) {
      this.options.element.classList.add('uploading')
      this.domRefs.fileInput.disabled = true
    }
    didUpload(formData) {
      let { element } = this.options
      element.classList.remove('uploading')
      this.domRefs.fileInput.disabled = false
      this.domRefs.fileInput.value = ''
      dom.dispatchEvent(element, 'uploaded')
    }
    failedUpload(formData) {
      this.domRefs.fileInput.disabled = false
      this.domRefs.fileInput.value = ''
      dom.dispatchEvent(element, 'uploadFailed')
    }
    willDownload(path) {
      this.options.element.classList.add('downloading')
    }
    didDownload(path) {
      this.domRefs.img.src = path
      let { element } = this.options
      element.classList.remove('downloading')
      dom.dispatchEvent(element, 'uploadedImageLoaded')
    }
    failedDownload(path) {
      let { element, fallbackImage } = this.options
      element.classList.remove('downloading')
      if (fallbackImage) {
        this.domRefs.img.src = fallbackImage
      }
      dom.dispatchEvent(element, 'uploadedImageFailed')
    }
    upload(formData) {
      let { element, upload, parseResponse } = this.options
      http(upload.method, upload.url, formData).then(
        responseBody => {
          let path = parseResponse(responseBody)
          this.didUpload(formData)
          this.willDownload(path)
          prefetch(path).then(
            () => {
              this.didDownload(path)
            },
            () => {
              this.failedDownload()
            }
          )
        },
        () => this.failedUpload(formData)
      )
    }
    bindEvents() {
      this.domRefs.fileInput.addEventListener('change', e => {
        let { upload } = this.options
        let formData = new FormData()
        formData.append(upload.inputName, e.target.files[0])
        this.willUpload(formData)
        this.upload(formData)
      })
    }
  }

  window.ImagePicker = ImagePicker

  function prefetch(url) {
    return new Promise((resolve, reject) => {
      let img = new Image()
      img.onload = resolve
      img.onerror = reject
      img.src = url
    })
  }

  function http(method, url, data) {
    return new Promise((resolve, reject) => {
      let xhr = new XMLHttpRequest()
      xhr.open(method, url)
      xhr.onload = () => resolve(xhr.responseText, xhr)
      xhr.onerror = () => reject(xhr)
      xhr.send(data)
    })
  }
}
<!DOCTYPE html>
<html lang="zh-Hans">
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <title>Image Picker</title>
  <style>
    *{box-sizing: border-box;}
    body{ display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #362057; }
    .card { background: white; width: 20em; height: 80vh; box-shadow: 0 0 5px hsla(0,0%,0%,0.95); border-radius: 2px;
      display: flex; justify-content: flex-start; align-items: center; padding-top: 3em; flex-direction: column;}
  </style>
  <style>
    .image-picker{ width: 100px; height: 100px; border-radius: 50%; overflow: hidden; position: relative; }
    .image-picker::after{
      content:''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 50%;
      box-shadow: inset 0 0 5px hsla(264, 46%, 23%, 0.5); color: white;
      display: flex; justify-content: center; align-items: center; cursor: pointer;
    }
    .image-picker:hover::after{
      content:'编辑'; background: hsla(0,0%,0%,0.2);
    }
    .image-picker>img{ max-width: 100%; max-height: 100%; }
    .image-picker>input[type=file]{ position: absolute; right:0; top: 0; width: 300%;
      height: 100%; z-index: 1; cursor: pointer; opacity: 0;
    }
    .image-picker.uploading::after{
      content:'上传中'; background: hsla(0,0%,0%,0.2);
    }
    .image-picker.downloading::after{
      content:'处理中'; background: hsla(0,0%,0%,0.2);
    }
  </style>


  <div class="card">
    <div class="image-picker">
      <img src="https://avatars0.githubusercontent.com/u/839559" width=100 height=100>
    </div>
    <p>点击图片编辑</p>
  </div>
  <script src="../lib/dom/index.js"></script>
  <script src="view-source.js"></script>
  <script src="../lib/image-picker/index.js"></script>
  <script>
    new ImagePicker({
      element: document.querySelector('.image-picker'),
      upload: {
        url: 'https://frankfang.com/image-server/upload',
        method: 'PUT',
        inputName: 'file'
      },
      parseResponse: (response) => {
        response = JSON.parse(response)
        return `https://frankfang.com/image-server/upload/${response.key}`
      },
      fallbackImage: 'https://avatars0.githubusercontent.com/u/839559'
    })
  </script>

  <!--百度统计-->
  <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?950926001a84a4f88cd3e1c7c0bfac08"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script>
</html>
const express = require('express')
const multer  = require('multer')
const cors = require('cors')
const upload = multer({ dest: 'uploads/' })
const p = require('path')

const app = express()

app.options('/upload', cors())
app.put('/upload', cors(), upload.single('file'), function (req, res, next) {
  res.json({key: req.file.filename})
})
app.get('/upload/:key', cors(), function(req, res, next){
  res.sendFile(`uploads/${req.params.key}`, {
    root: __dirname,
    headers:{
      'Content-Type': 'image/jpeg',
    },
  }, (error)=>{
    if(error){
      res.status(404).send('Not found')
    }
  })
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})

前端没有任何依赖,没有 webpack、npm,全手工打造,代码工整,值得阅读。

如果你对 ES6 不熟,那么赶紧补课!

ES 6 新特性汇总(一图全览)