基於vue-simplemde實現圖片拖拽、貼上功能的一些思考

weixin_34249678發表於2018-04-11

前言

專案使用的是vue框架,需要一個markdown的編輯框,就在npm上找了一下,發現simplemde挺不錯的,由於我比較懶,就順便在npm又搜了一下,找到了vue-simplemde這個package,那就開始使用它吧。

但是這個vue-simplemde不支援圖片拖拽上傳、貼上上傳,也不能說是因為這個vue-simplemde,因為vue-simplemde只是對simplemde的基礎上封裝成一個Vue外掛。所以最後還是由於simplemde沒有提供相關的功能,但是為了使用者體驗考慮,這個功能時必要的,除非不使用markdown編輯器。而去使用富文字編輯器,那樣的話,專案很多的程式碼都要進行更改。所以就在網上查了文章,及在github上查了一些程式碼。下面將進行分析

拖拽

拖拽的API核心是drop這個事件,就是當我們從桌面拖動一個檔案到瀏覽器裡時,鬆開的時候,而觸發的事件名。

我們都知道,你隨便拖動一個圖片到瀏覽器裡,會直接開啟這個圖片,這是因為瀏覽器預設你拖動檔案到瀏覽器裡時,將開啟這個檔案,所以,我們需要阻止原生的操作。

我們現在先寫一段程式碼,讓其遮蔽掉預設事件

window.addEventListener("drop", e => {
  e = e || event
  if (e.target.className === 'CodeMirror-scroll') { // 如果進入到編輯器的話,將阻止預設事件
    e.preventDefault()
  }
}, false)
複製程式碼

CodeMirror-scroll這個Class就是simplemde編輯框的Class名稱。

現在我們拖拽檔案到這個編輯框,然後鬆掉,不會出現任何反應。如果在編輯框之外的地方,還是會繼續觸發預設事件。

下面就是獲取simplemde方法,給他drop事件處理方法。

// 假設頁面一共有三個編輯視窗,所以需要迴圈監聽事件
[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) => {
  simplemde.codemirror.on('drop', (editor, e) => {
    if (!(e.dataTransfer && e.dataTransfer.files)) {
      // 彈窗說明,此瀏覽器不支援此操作
      return
    }

    let dataList = e.dataTransfer.files
    let imageFiles = [] // 要上傳的檔案例項陣列

    // 迴圈,是因為可能會同時拖動幾個圖片檔案
    for (let i = 0; i < dataList.length; i++) {
    // 如果不是圖片,則彈窗警告 僅支援拖拽圖片檔案
      if (dataList[i].type.indexOf('image') === -1) {
        // 下面的continue,作用是,如果使用者同時拖動2個圖片和一個文件,那麼文件不給於上傳,圖片照常上傳。
        continue
      }
      imageFiles.push(dataList[i])  // 先把當前的檔案push進陣列裡,等for迴圈結束之後,統一上傳。
    }
    // uploadImagesFile方法是上傳圖片的方法
    // simplemde.codemirror的作用是用於區分當前的圖片上傳是處於哪個編輯框
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    // 因為已經有了下面這段程式碼,所以上面的遮蔽預設事件程式碼就不用寫了
    e.preventDefault()
  })
})
複製程式碼

詐一看,程式碼好像有點多,那是因為註釋的原因,下面是沒有註釋的程式碼。你可以根據下面的程式碼,有自己的見解和理解:

[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) => {
  simplemde.codemirror.on('drop', (editor, e) => {
    if (!(e.dataTransfer && e.dataTransfer.files)) {
      return
    }
    let dataList = e.dataTransfer.files
    let imageFiles = []
    for (let i = 0; i < dataList.length; i++) {
      if (dataList[i].type.indexOf('image') === -1) {
        continue
      }
      imageFiles.push(dataList[i])
    }
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    e.preventDefault()
  })
})
複製程式碼

貼上

貼上的API是paste方法,這個不像上面一樣,貼上不需要禁止預設事件,因為我們可以看到,你複製一個圖片,到瀏覽器裡按下ctrl+v的時候,是不會發生任何變化的,所以沒用必要禁止預設事件。

下面是程式碼:

simplemde.codemirror.on('paste', (editor, e) => { // 貼上圖片的觸發函式
  if (!(e.clipboardData && e.clipboardData.items)) {
    // 彈窗說明,此瀏覽器不支援此操作
    return
  }
  try {
    let dataList = e.clipboardData.items
    if (dataList[0].kind === 'file' && dataList[0].getAsFile().type.indexOf('image') !== -1) {
      this.uploadImagesFile(simplemde.codemirror, [dataList[0].getAsFile()])
    }
  } catch (e) {
    // 彈窗說明,只能貼上圖片
  }
})
複製程式碼

之所以這裡寫上try...catch方法,是因為如果你貼上的時候,如果是一個檔案,items將是空的,而在下面的if迴圈裡,使用dataList[0].kind。也就是e.clipboardData.items[0].kind。當item為空時,還去訪問一個不存的kind屬性時,就會報錯了。所以這裡需要使用try...catch方法進行判斷。

dataList[0].getAsFile().type.indexOf('image') !== -1這個句話是判斷,貼上的東西確認是圖片,而不是其他東西。

if裡的上傳圖片,不一樣的地方是[dataList[0].getAsFile()],因為為了統一格式,方便uploadImagesFile函式進行處理,我加上了[],使之成為陣列。dataList[0].getAsFile()就是獲取檔案例項了。

上傳

上傳就有一點麻煩了:

uploadImagesFile (simplemde, files) {
  // 把每個檔案例項使用FormData進行包裝一下,然後返回一個陣列
  let params = files.map(file => {
    let param = new FormData()
    param.append('file', file, file.name)
    return param
  })

  let makeRequest = params => {
    return this.$http.post('/Api/upload', params)
  }
  let requests = params.map(makeRequest)

  this.$http.spread = callback => {
    return arr => {
      return callback.apply(null, arr)
    }
  }

  // 服務端返回的格式是{state: Boolean, data: String}
  // state為false時,data就是返回的錯誤資訊
  // state為true時,data是圖片上傳後url地址,這個地址是針對網站的絕對路徑。如下:
  // /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png
  Promise.all(requests)
    .then(this.$http.spread((...resps) => {
      for (let i = 0; i < resps.length; i++) {
        let {state, data} = resps[i].data
        if (!state) {
          // 彈窗顯示data的錯誤資訊
          continue
        }
        let url = `![](${location.origin + data})`  // 拼接成markdown語法
        let content = simplemde.getValue()
        simplemde.setValue(content + url + '\n')  // 和編輯框之前的內容進行拼接
      }
    }))
}
複製程式碼

因為我是把axiox封裝成vue外掛來使用,這樣會導致,this.$http是例項化後的,而不是他本身。 axios維護者說的解決方案是,重新引入axios包,來使用。但是我覺得沒有必要。axios.all內部是Promise.allaxios.spread實現程式碼比較少,就直接拿過來,重新賦值給axios就好了

所以上面有段程式碼是

Promise.all(requests)
  .then(this.$http.spread((...resps) => {
    // code
  })
複製程式碼

把這段程式碼翻譯一下就是

axios.all(requests)
  .then(axios.spread((...resps) => {
    // code
  })
複製程式碼

關於這個問題,請看下官方的解釋:axios-all-is-not-a-function-inside-vue-component。也可以看下axios的程式碼:axios.js#L45-L48

這個問題,暫時就不深究了,我們回到剛剛的話題上。

上面我說到當state為true時,data是檔案相對於網站的絕對路徑,如: /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png

如果我們需要進行拼接一下,所以就有了![](${location.origin + data})這段程式碼進行拼接。最後的兩行是獲取指的獲取之前的內容,然後在追加url地址。

結尾

下面是最終的效果圖,因為掘金無法上傳gif圖片。所以就直接附上動態圖連線: 7xppwd.com1.z0.glb.clouddn.com/2b971f90-3d…

完整程式碼:Subject.vue#L378-L465

參考 && 感謝

skecozo作者的laravel-demo專案裡的部分程式碼

Lemon作者的《simplemde 實現拖拽、貼上圖片上傳》文章

f-loat作者的vue-simplemde專案

wescossick作者的simplemde專案

相關文章