图床就是公网上存放图片的地方,常用的有七牛,又拍云,新浪微博等,前面两个图床是免费的,但是需要绑定到自己已备案的域名,自己没有域名,就算了,新浪微博直接用就行了,但是据说有可能会被删除。用github的图床就没有上面这两个问题,而且查看和管理起来也很方便,所以就在想怎么实现。一顿搜索之后在网上找到了一个APP,PicGo,它里面有一个上传图片到GitHub图床的功能。
PicGo在使用GitHub图床之前要先配置,仓库名一定要加上你的username,我刚开始填的是repository的名字,就一直上传不成功,分支名一般就用master,Token需要到设置中添加一个,路径是这个,点击Generate a new token
,填写一个Note,把第一组Scope勾上,生成就可以了,注意这个token只能在生成的时候可以查看,关掉页面就看不到了,需要重新生成了。
把token填进去,点击确定,去测试一下,没有问题。
这是一个开源的APP,可以去项目中找找GitHub图床上传的代码,于是直接把代码clone下来。项目是用electron-vue
写的,之前虽然没写过,但是HTML/JS还是看得懂一点的。就一点一点地扒代码。
首先全文搜索了一下github
,一通找了之后发现GitHub.vue
这个页面的布局跟上面的设置页面很像。找到了一个确定和设为默认图床
两个按钮,确定按钮调用了confirm
方法,首先是form校验,通过后把form数据保存起来。
confirm () {
this.$refs.github.validate((valid) => {
if (valid) {
this.$db.set('picBed.github', this.form).write()
const successNotification = new window.Notification('设置结果', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
} else {
return false
}
})
}
设为默认图床,这个按钮点击后调用setDefaultPicBed
这个方法
// ConfirmButtonMixin.js
setDefaultPicBed (type) {
this.$db.read().set('picBed.current', type).write()
this.defaultPicBed = type
const successNotification = new window.Notification('设置默认图床', {
body: '设置成功'
})
successNotification.onclick = () => {
return true
}
}
到这里GitHub设置相关的已经完成了,那我要找一下上面的设置在哪里有用到。我就又全文搜索了一下上面的keypicBed.current
,在一个Upload.vue文件中找到了获取了这个key对应的值,经过比对,发现这就是上面第一个节目对接的文件。
然后分析里面的布局,有一个引起了我的注意:
<div id="upload-dragger" @click="openUplodWindow">
<i class="el-icon-upload"></i>
<div class="upload-dragger__text">
将文件拖到此处,或 <span>点击上传</span>
</div>
<input type="file" id="file-uploader" @change="onChange" multiple>
</div>
这是中心区域的布局,可以看到有一个file-uploader
,触发的事件是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)
},
说实话到这里就开始闷逼了,ipcSendFiles
方法里最后一句是什么意思,完全不明白。然后就开始猜测,uploadChoosedFiles
是不是一个方法,sendFiles
是参数,这个就有点像OC语法中的objc_msgSend
,全文搜索uploadChoosedFiles
,果然在index.js
中找到了这个方法的定义:
const uploadChoosedFiles = async (webContents, files) => {
const input = files.map(item => item.path)
const imgs = await new Uploader(input, webContents).upload()
// 省略后面代码...
}
这里最关键的就是构建了一个Uploader对象,并调用upload方法
,里面创建了一个PicGo对象,调用upload方法,再追踪这个PicGo类就找不到了,只找到下面这个import:
const requireFunc = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
const PicGo = requireFunc('picgo')
刚开始真的不知道该怎么继续了,因为对JS语法的理解也就只是在写写业务的层面,基本没有接触过webpack,去接了杯水,回来后想这是不是就是一个普通的依赖呢,代码里没有一个picgo.js
的文件,就到package.json
中找(这是一个前端项目管理依赖的文件,类似已iOS中的Podfile),果然在dependencies
中有一个"picgo": "^1.3.7"
。带NPM官网搜索picgo这个包,找到了下面这个
右边有这个包的官网和github代码仓库,于是跳转过去,又把代码clone下来。拿到代码后在src目录下面翻找,看到有一个PicGo.ts
的文件(TypeScript),应该就是这个了。
里面有一个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)
}
}
看到最终调用的是lifecycle.start()方法
,继续追进去,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
}
里面有个根据type获取uploader的方法,这个type使用的是picBed.current
,这个是不是很熟悉,就是上面设置默认图床设置的key。分析获取uploader对象的方法,ctx是PicGo对象,helper以及uploader是在PicGo对象的构造方法中初始化的,构造函数中uploader还是一个空的plugin,在init方法中调用了uploaders
方法,这个方法正真给uploader对象添加了多个不同类型的Uploader具体实现。这也是策略模式的一种实践。
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)
}
到这里,我们已经找到了github图床上传的具体实现类,除此之外,还有很多其他类型的,比例微博,七牛,又拍云等,这次我们只关注github的实现。进入github.ts
这个文件,查看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)
对图片进行base64,转成字符串,构建请求头和请求参数,PUT方法发送请求。
到这里已经很兴奋了,这其实就是一个Http请求就可以把图片放到github上去了,赶紧用第三方的HTTP客户端试一下自己拼一个请求。我这里使用Paw这个软件。
这里User-Agent使用了默认的,文件名使用了Paw中的Timestamp方法,content使用文件的base64,成功了,到github仓库中也看到了上传的这张图片。好厉害。
开发者网站
后来去github的developer页面,才发现自己走了一大段弯路,在Github开发者网站有专门一个章节介绍怎么组装一个创建文件的请求,以后想要实现一个功能,要先找一下官方有没有相关的文档。
其实,不光是图片,任何的文件都可以通过这种方式上传到github上。
这篇文章也是记录一下自己通过这个开源项目寻找关键代码的过程,可能会有点啰嗦,但是这个过程对我来说很奇妙。通过这个工程也可以看其他几个图床的上传方式。