圖床就是公網(wǎng)上存放圖片的地方,常用的有七牛,又拍云,新浪微博等,前面兩個(gè)圖床是免費(fèi)的,但是需要綁定到自己已備案的域名,自己沒有域名,就算了,新浪微博直接用就行了,但是據(jù)說有可能會(huì)被刪除。用github的圖床就沒有上面這兩個(gè)問題,而且查看和管理起來也很方便,所以就在想怎么實(shí)現(xiàn)。一頓搜索之后在網(wǎng)上找到了一個(gè)APP,PicGo,它里面有一個(gè)上傳圖片到GitHub圖床的功能。

PicGo在使用GitHub圖床之前要先配置,倉庫名一定要加上你的username,我剛開始填的是repository的名字,就一直上傳不成功,分支名一般就用master,Token需要到設(shè)置中添加一個(gè),路徑是這個(gè),點(diǎn)擊Generate a new token,填寫一個(gè)Note,把第一組Scope勾上,生成就可以了,注意這個(gè)token只能在生成的時(shí)候可以查看,關(guān)掉頁面就看不到了,需要重新生成了。


把token填進(jìn)去,點(diǎn)擊確定,去測(cè)試一下,沒有問題。
這是一個(gè)開源的APP,可以去項(xiàng)目中找找GitHub圖床上傳的代碼,于是直接把代碼clone下來。項(xiàng)目是用electron-vue寫的,之前雖然沒寫過,但是HTML/JS還是看得懂一點(diǎn)的。就一點(diǎn)一點(diǎn)地扒代碼。
首先全文搜索了一下github,一通找了之后發(fā)現(xiàn)GitHub.vue這個(gè)頁面的布局跟上面的設(shè)置頁面很像。找到了一個(gè)確定和設(shè)為默認(rèn)圖床兩個(gè)按鈕,確定按鈕調(diào)用了confirm方法,首先是form校驗(yàn),通過后把form數(shù)據(jù)保存起來。
confirm () {
this.$refs.github.validate((valid) => {
if (valid) {
this.$db.set('picBed.github', this.form).write()
const successNotification = new window.Notification('設(shè)置結(jié)果', {
body: '設(shè)置成功'
})
successNotification.onclick = () => {
return true
}
} else {
return false
}
})
}
設(shè)為默認(rèn)圖床,這個(gè)按鈕點(diǎn)擊后調(diào)用setDefaultPicBed這個(gè)方法
// ConfirmButtonMixin.js
setDefaultPicBed (type) {
this.$db.read().set('picBed.current', type).write()
this.defaultPicBed = type
const successNotification = new window.Notification('設(shè)置默認(rèn)圖床', {
body: '設(shè)置成功'
})
successNotification.onclick = () => {
return true
}
}
到這里GitHub設(shè)置相關(guān)的已經(jīng)完成了,那我要找一下上面的設(shè)置在哪里有用到。我就又全文搜索了一下上面的keypicBed.current,在一個(gè)Upload.vue文件中找到了獲取了這個(gè)key對(duì)應(yīng)的值,經(jīng)過比對(duì),發(fā)現(xiàn)這就是上面第一個(gè)節(jié)目對(duì)接的文件。
然后分析里面的布局,有一個(gè)引起了我的注意:
<div id="upload-dragger" @click="openUplodWindow">
<i class="el-icon-upload"></i>
<div class="upload-dragger__text">
將文件拖到此處,或 <span>點(diǎn)擊上傳</span>
</div>
<input type="file" id="file-uploader" @change="onChange" multiple>
</div>
這是中心區(qū)域的布局,可以看到有一個(gè)file-uploader,觸發(fā)的事件是onChange方法:
onChange (e) {
this.ipcSendFiles(e.target.files)
document.getElementById('file-uploader').value = ''
},
ipcSendFiles (files) {
let sendFiles = []
Array.from(files).forEach((item, index) => {
let obj = {
name: item.name,
path: item.path
}
sendFiles.push(obj)
})
this.$electron.ipcRenderer.send('uploadChoosedFiles', sendFiles)
},
說實(shí)話到這里就開始悶逼了,ipcSendFiles方法里最后一句是什么意思,完全不明白。然后就開始猜測(cè),uploadChoosedFiles是不是一個(gè)方法,sendFiles是參數(shù),這個(gè)就有點(diǎn)像OC語法中的objc_msgSend,全文搜索uploadChoosedFiles,果然在index.js中找到了這個(gè)方法的定義:
const uploadChoosedFiles = async (webContents, files) => {
const input = files.map(item => item.path)
const imgs = await new Uploader(input, webContents).upload()
// 省略后面代碼...
}
這里最關(guān)鍵的就是構(gòu)建了一個(gè)Uploader對(duì)象,并調(diào)用upload方法,里面創(chuàng)建了一個(gè)PicGo對(duì)象,調(diào)用upload方法,再追蹤這個(gè)PicGo類就找不到了,只找到下面這個(gè)import:
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
const PicGo = requireFunc('picgo')
剛開始真的不知道該怎么繼續(xù)了,因?yàn)閷?duì)JS語法的理解也就只是在寫寫業(yè)務(wù)的層面,基本沒有接觸過webpack,去接了杯水,回來后想這是不是就是一個(gè)普通的依賴呢,代碼里沒有一個(gè)picgo.js的文件,就到package.json中找(這是一個(gè)前端項(xiàng)目管理依賴的文件,類似已iOS中的Podfile),果然在dependencies中有一個(gè)"picgo": "^1.3.7"。帶NPM官網(wǎng)搜索picgo這個(gè)包,找到了下面這個(gè)

右邊有這個(gè)包的官網(wǎng)和github代碼倉庫,于是跳轉(zhuǎn)過去,又把代碼clone下來。拿到代碼后在src目錄下面翻找,看到有一個(gè)PicGo.ts的文件(TypeScript),應(yīng)該就是這個(gè)了。
里面有一個(gè)upload方法
async upload (input?: any[]): Promise<void | string | Error> {
if (this.configPath === '') return this.log.error('The configuration file only supports JSON format.')
// upload from clipboard
if (input === undefined || input.length === 0) {
try {
const { imgPath, isExistFile } = await getClipboardImage(this)
if (imgPath === 'no image') {
throw new Error('image not found in clipboard')
} else {
this.once('failed', async () => {
if (!isExistFile) {
await fs.remove(imgPath)
}
})
this.once('finished', async () => {
if (!isExistFile) {
await fs.remove(imgPath)
}
})
await this.lifecycle.start([imgPath])
}
} catch (e) {
this.log.error(e)
this.emit('failed', e)
throw e
}
} else {
// upload from path
await this.lifecycle.start(input)
}
}
看到最終調(diào)用的是lifecycle.start()方法,繼續(xù)追進(jìn)去,lifecycle.start() -> lifecycle.doUpload()
private async doUpload (ctx: PicGo): Promise<PicGo> {
this.ctx.log.info('Uploading...')
let type = ctx.config.picBed.uploader || ctx.config.picBed.current || 'smms'
let uploader = this.ctx.helper.uploader.get(type)
if (!uploader) {
type = 'smms'
uploader = this.ctx.helper.uploader.get('smms')
ctx.log.warn(`Can't find uploader - ${type}, swtich to default uploader - smms`)
}
await uploader.handle(ctx)
for (let i in ctx.output) {
ctx.output[i].type = type
}
return ctx
}
里面有個(gè)根據(jù)type獲取uploader的方法,這個(gè)type使用的是picBed.current,這個(gè)是不是很熟悉,就是上面設(shè)置默認(rèn)圖床設(shè)置的key。分析獲取uploader對(duì)象的方法,ctx是PicGo對(duì)象,helper以及uploader是在PicGo對(duì)象的構(gòu)造方法中初始化的,構(gòu)造函數(shù)中uploader還是一個(gè)空的plugin,在init方法中調(diào)用了uploaders方法,這個(gè)方法正真給uploader對(duì)象添加了多個(gè)不同類型的Uploader具體實(shí)現(xiàn)。這也是策略模式的一種實(shí)踐。
export default (ctx: PicGo): void => {
ctx.helper.uploader.register('smms', SMMSUploader)
ctx.helper.uploader.register('tcyun', tcYunUploader)
ctx.helper.uploader.register('weibo', weiboUploader)
ctx.helper.uploader.register('github', githubUploader)
ctx.helper.uploader.register('qiniu', qiniuUploader)
ctx.helper.uploader.register('imgur', imgurUploader)
ctx.helper.uploader.register('aliyun', aliYunUploader)
ctx.helper.uploader.register('upyun', upYunUploader)
}
到這里,我們已經(jīng)找到了github圖床上傳的具體實(shí)現(xiàn)類,除此之外,還有很多其他類型的,比例微博,七牛,又拍云等,這次我們只關(guān)注github的實(shí)現(xiàn)。進(jìn)入github.ts這個(gè)文件,查看handle方法。
const postOptions = (fileName: string, options: any, data: any): any => {
const path = options.path || ''
const { token, repo } = options
return {
method: 'PUT',
url: `https://api.github.com/repos/${repo}/contents/${encodeURI(path)}${encodeURI(fileName)}`,
headers: {
Authorization: `token ${token}`,
'User-Agent': 'PicGo'
},
body: data,
json: true
}
}
// handle方法片段
let base64Image = imgList[i].base64Image || Buffer.from(imgList[i].buffer).toString('base64')
const data = {
message: 'Upload by PicGo',
branch: githubOptions.branch,
content: base64Image,
path: githubOptions.path + encodeURI(imgList[i].fileName)
}
const postConfig = postOptions(imgList[i].fileName, githubOptions, data)
const body = await ctx.Request.request(postConfig)
對(duì)圖片進(jìn)行base64,轉(zhuǎn)成字符串,構(gòu)建請(qǐng)求頭和請(qǐng)求參數(shù),PUT方法發(fā)送請(qǐng)求。
到這里已經(jīng)很興奮了,這其實(shí)就是一個(gè)Http請(qǐng)求就可以把圖片放到github上去了,趕緊用第三方的HTTP客戶端試一下自己拼一個(gè)請(qǐng)求。我這里使用Paw這個(gè)軟件。


這里User-Agent使用了默認(rèn)的,文件名使用了Paw中的Timestamp方法,content使用文件的base64,成功了,到github倉庫中也看到了上傳的這張圖片。好厲害。
開發(fā)者網(wǎng)站
后來去github的developer頁面,才發(fā)現(xiàn)自己走了一大段彎路,在Github開發(fā)者網(wǎng)站有專門一個(gè)章節(jié)介紹怎么組裝一個(gè)創(chuàng)建文件的請(qǐng)求,以后想要實(shí)現(xiàn)一個(gè)功能,要先找一下官方有沒有相關(guān)的文檔。

其實(shí),不光是圖片,任何的文件都可以通過這種方式上傳到github上。
這篇文章也是記錄一下自己通過這個(gè)開源項(xiàng)目尋找關(guān)鍵代碼的過程,可能會(huì)有點(diǎn)啰嗦,但是這個(gè)過程對(duì)我來說很奇妙。通過這個(gè)工程也可以看其他幾個(gè)圖床的上傳方式。