前言
專案使用的是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.all
。axios.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專案裡的部分程式碼