使用 github 快速搭建屬于自己的圖床
起因
說(shuō)起來(lái),圖床應(yīng)用這東西,在github上有很多,但是大多都是基于一些云廠商免費(fèi)的靜態(tài)存儲(chǔ)服務(wù)來(lái)實(shí)現(xiàn)的,比如七牛云的靜態(tài)存儲(chǔ),考慮到這些云廠商的賺錢欲望,所以我并不放心將他們作為圖床的服務(wù)提供商。
也有支持github的,比如picgo,不過(guò)涉及到personal token,我也不是很放心將自己的token寫(xiě)入到一個(gè)開(kāi)源項(xiàng)目的桌面應(yīng)用里。而且picgo導(dǎo)出的github圖片鏈接是以 githubusercontent.com 為host的鏈接,眾所周知,該域名在中國(guó)很多地區(qū)都被DNS污染了,只有通過(guò)改host或是科學(xué)上網(wǎng)進(jìn)行訪問(wèn),所以結(jié)論是,picgo基于github導(dǎo)出的圖片鏈接,在國(guó)內(nèi)等于沒(méi)用。
那有沒(méi)有一種方式,既能讓圖片鏈接不被DNS污染或是被墻掉,又不會(huì)涉及到開(kāi)發(fā)者personal token,影響賬戶安全呢?
于是,就有了picpic。picpic是我在做一個(gè)另一個(gè)大型的開(kāi)源項(xiàng)目的過(guò)程中抽空實(shí)現(xiàn)的,初始版本只用了兩天就寫(xiě)出來(lái)了,但是我本人自認(rèn)為是一個(gè)合格和還不錯(cuò)的product maker,并不愿意產(chǎn)出一個(gè)使用繁瑣,功能殘缺的半成品給別人使用——關(guān)鍵是自己用的也不爽。
我做產(chǎn)品,核心觀點(diǎn)就是,做出來(lái)的東西自己愿不愿意用,用起來(lái)有沒(méi)有感受到“美”,是不是能夠沉靜在產(chǎn)品中去感受它,這很重要,正是因?yàn)槲覐臎](méi)將自己定位成一個(gè)前端,或是node開(kāi)發(fā),而是product maker,終極理想就是artist,就是做藝術(shù),內(nèi)心始終有一個(gè)想法:你不是在寫(xiě)代碼,你是在畫(huà)一幅畫(huà),你享受這個(gè)過(guò)程,如果能夠讓別人享受到“結(jié)果”,那是再好不過(guò)了。
所以就有了它:
DEMO地址:https://matrixage.github.io/picpic_example/
項(xiàng)目地址:https://github.com/MatrixAges/picpic

基于離線版本,脫離了webpack的vue.js構(gòu)建的單頁(yè)面應(yīng)用,原理就是通過(guò)node把圖片數(shù)據(jù)預(yù)編譯并寫(xiě)入到window對(duì)象中,然后通過(guò)chunk進(jìn)行分片,提供翻頁(yè)功能,至于文件夾模式,則是通過(guò)node把a(bǔ)ssets文件夾下的文件結(jié)構(gòu)預(yù)編譯成樹(shù)形數(shù)據(jù),寫(xiě)入到window對(duì)象,然后給頁(yè)面中的js進(jìn)行調(diào)用。
服務(wù)基于github pages,自動(dòng)化構(gòu)建使用的是github actions,通過(guò)自動(dòng)化構(gòu)建部署,部署靜態(tài)文件到gh-pages分支,然后訪問(wèn) username.github.io/${repo}/${img_path} 即可訪問(wèn)不被槍的靜態(tài)圖片。
幾經(jīng)打磨,最后我把它做成了cli,你只需要npm i @matrixage/picpic,即可使用。
下面講講,我是如何通過(guò)node和vue構(gòu)建這樣一個(gè)單頁(yè)面應(yīng)用的。
沒(méi)有webpack的web應(yīng)用
使用github actions也有一段時(shí)間了,在經(jīng)歷過(guò)很多次構(gòu)建之后,我觀察到了一個(gè)現(xiàn)象:那就是80%的時(shí)間都是webpack花掉的,關(guān)鍵是一些很簡(jiǎn)單的項(xiàng)目,因?yàn)閣ebpack,還是會(huì)有一個(gè)比較長(zhǎng)的安裝npm包的時(shí)間,那這對(duì)于一個(gè)圖床應(yīng)用來(lái)說(shuō),是致命的。
所以我決定擺脫webpack,使用離線版本的vue.min.js來(lái)構(gòu)建應(yīng)用,將部署時(shí)間控制在30s以內(nèi),做到提交圖片,即刻可用。
<script?src='./libs/js/vue.min.js'>script>
<script?src='./libs/js/lodash.chunk.js'>script>
<script?src='./libs/js/lodash.throttle.js'>script>
<script?src='./libs/js/clipboard.js'>script>
<script?src='./index.js'>script>
使用XHR和CustomEvent進(jìn)行組件化開(kāi)發(fā)
在html頂部引入include.js,改文件的作用是在文檔加載完成之后將include標(biāo)簽中的地址通過(guò)同步的XHR,請(qǐng)求到組件的html內(nèi)容,然后寫(xiě)入到頁(yè)面中。
//?include.js
getFileContent:?function?(url){
????var?o?=?new?XMLHttpRequest()
?
?o.open('get',?url,?false)
?o.send(null)
?
?return?o.responseText
}
接著通過(guò)自定義事件發(fā)出通知:
//?include.js
var?evt?=?new?CustomEvent('included',?{
?bubbles:?true,
?cancelable:?false
})
window.onload?=?function?(){
????new?Include().replaceIncludeElements()
????
????document.dispatchEvent(evt);
}
在其他腳本中接收通知:
//?index.js
document.addEventListener('included',?function?(){...})
通過(guò)node預(yù)編譯組件
僅僅是使用include是不夠的,組件的js和css代碼同樣要分離出來(lái),這樣才有意義,于是node出場(chǎng),其實(shí)你理解的webpack,不過(guò)時(shí)穿上紳士馬甲的node編譯腳本,本質(zhì)上還是預(yù)編譯。
所以不用webpack,我們直溯本源,手寫(xiě)預(yù)編譯代碼。在picpic項(xiàng)目根目錄新建一個(gè)build文件夾,其中的文件就是預(yù)編譯要用的代碼。
//?build/index.js
const?fs?=?require('fs-extra')
const?globby?=?require('globby')
const?inject?=?require('./inject')
const?paths?=?require('./utils/paths')
const?main?=?async?()?=>?{
?if?(!fs.existsSync(paths.dist))?{
??fs.mkdirSync(paths.dist)
?}?else?{
??fs.removeSync(paths.dist)
??fs.mkdirSync(paths.dist)
??????}
??????
?fs.writeFileSync(`${paths.dist}/index.html`,?await?inject())
?fs.copySync(paths.assets,?paths.dist)
?fs.copySync(paths.getPath('../../src'),?paths.dist)
?fs.removeSync(`${paths.dist}/source.html`)
?const?less?=?await?globby(`${paths.dist}/**/*.less`)
??????less.map(item?=>?fs.removeSync(item))
??????
?console.log('----------?picpic?build?success!?----------?\n')
}
try?{
?main()
}?catch?(error)?{
?console.log('----------?picpic?build?error!?----------?\n')
?console.error(error)
}
這里的inject就是注入組件和數(shù)據(jù)之后的html,接下來(lái)展示一下如何進(jìn)行組件注入。
//?build/inject/index.js
const?fs?=?require('fs-extra')
const?injectData?=?require('./injectData')
const?injectStyles?=?require('./injectStyles')
const?injectTemplates?=?require('./injectTemplates')
const?injectJs?=?require('./injectJs')
const?paths?=?require('../utils/paths')
function?Inject?(){
?this.html?=?''
?this.getSource?=?()?=>?{
??this.html?=?fs.readFileSync(paths.getPath('../../src/source.html')).toString()
??return?new?Promise(resolve?=>?resolve(this.html))
?}
?this.injectData?=?async?()?=>?{
??this.html?=?await?injectData(this.html)
??return?new?Promise(resolve?=>?resolve(this.html))
?}
?this.injectStyles?=?async?()?=>?{
??this.html?=?await?injectStyles(this.html)
??return?new?Promise(resolve?=>?resolve(this.html))
?}
?this.injectTemplates?=?async?()?=>?{
??this.html?=?await?injectTemplates(this.html)
??return?new?Promise(resolve?=>?resolve(this.html))
?}
}
const?inject?=?async?()?=>?{
?return?await?new?Inject()
??.getSource()
??.then(res?=>?injectData(res))
??.then(res?=>?injectStyles(res))
??.then(res?=>?injectTemplates(res))
??.then(res?=>?injectJs(res))
}
module.exports?=?inject
通過(guò)返回this的方法進(jìn)行鏈?zhǔn)秸{(diào)用,比一層一層用方法包裹優(yōu)雅很多,有沒(méi)有感受到代碼之美,嘻嘻。
injectStyles injectTemplates injectJs這三種方法異曲同工,原理特簡(jiǎn)單,就是字符串替換,不過(guò)這里要注意空格,少一個(gè)都匹配不到。
//?build/inject/injectStyles.js
const?globby?=?require('globby')
const?paths?=?require('../utils/paths')
module.exports?=?async?str?=>?{
?const?paths_source?=?await?globby([?`${paths.getPath('../../src/components/**/*.css')}`?])
?const?paths_target?=?[]
?paths_source.map(item?=>
??paths_target.push(item.replace('src',?'.').split('/').slice(-4).join('/'))
??????)
?const?items?=?paths_target.map(item?=>?'@import?'?+?"'"?+?item?+?"'"?+?';'?+?'\n')
?return?str.replace(
??`
??????
`,
??`
??????
`
?)
}
在頁(yè)面中,三種占位符分別用于注入組件相關(guān)的文件:
<style>style>
<template-slot>template-slot>
<script?id="component_scripts">script>
注入之后的結(jié)果為:
<style>
@import?'./components/Detail/index.css';
@import?'./components/Empty/index.css';
@import?'./components/FolderSelect/index.css';
@import?'./components/Header/index.css';
@import?'./components/ImgItems/index.css';
@import?'./components/Msg/index.css';
@import?'./components/Pagination/index.css';
style>
<include?src="./components/Detail/index.html">include>
<include?src="./components/Empty/index.html">include>
<include?src="./components/FolderSelect/index.html">include>
<include?src="./components/Header/index.html">include>
<include?src="./components/ImgItems/index.html">include>
<include?src="./components/Msg/index.html">include>
<include?src="./components/Pagination/index.html">include>
<script?src="./components/Detail/index.js">script>
<script?src="./components/Empty/index.js">script>
<script?src="./components/FolderSelect/index.js">script>
<script?src="./components/Header/index.js">script>
<script?src="./components/ImgItems/index.js">script>
<script?src="./components/Msg/index.js">script>
<script?src="./components/Pagination/index.js">script>
不要詬病組件文件夾大寫(xiě),我是react的擁躉,如果不是因?yàn)閣eb-component強(qiáng)制使用-分割符小寫(xiě),所有的組件我都希望大寫(xiě),因?yàn)楸孀R(shí)度比前者高很多。
通過(guò)node預(yù)編譯目錄數(shù)據(jù)
主要是通過(guò)dree到處樹(shù)形數(shù)據(jù),通過(guò)imageinfo獲取圖片長(zhǎng)寬,然后再進(jìn)行數(shù)據(jù)裁剪,把需要的數(shù)據(jù)進(jìn)行組裝后導(dǎo)出。代碼多且雜,這里僅結(jié)果,有興趣的可以去github看代碼。
{
????"name":"assets",
????"type":"directory",
????"size":"1.14MB",
????"children":[
????????{
????????????"name":"projects",
????????????"type":"directory",
????????????"size":"1.14MB",
????????????"children":[
????????????????{
????????????????????"name":"picpic",
????????????????????"type":"directory",
????????????????????"size":"1.14MB",
????????????????????"children":[
????????????????????????{
????????????????????????????"name":"choose_gh_pages.jpg",
????????????????????????????"type":"file",
????????????????????????????"extension":"jpg",
????????????????????????????"size":"61.1KB",
????????????????????????????"dimension":"2020x940",
????????????????????????????"path":"projects/picpic/choose_gh_pages.jpg"
????????????????????????},
????????????????????????{
????????????????????????????"name":"folder_hover_status.jpg",
????????????????????????????"type":"file",
????????????????????????????"extension":"jpg",
????????????????????????????"size":"116.74KB",
????????????????????????????"dimension":"956x1896",
????????????????????????????"path":"projects/picpic/folder_hover_status.jpg"
????????????????????????}
????????????????????]
????????????????}
????????????]
????????}
????]
}
然后寫(xiě)入到html中:
//?build/inject/injectData.js
const?{?getFileTree?}?=?require('../utils')
module.exports?=?async?str?=>?{
?const?tree?=?await?getFileTree()
?return?str.replace(
??`
??????
????????????PicPic
??????
`,
??`
??????
????????????PicPic
????????????
??????
`
?)
}
做成命令行工具
僅僅做成上面那樣使用起來(lái),還需要?jiǎng)e人clone你的倉(cāng)庫(kù),后續(xù)升級(jí)麻煩,而且編譯源文件什么的都暴露出來(lái)了,看起來(lái)臟的不行,所以不僅要產(chǎn)品本身美,使用方式也需要簡(jiǎn)單優(yōu)雅。
在 package.json 中添加如下字段,發(fā)布包之后,當(dāng)別人在 npm i @matrixage/picpic 時(shí)會(huì)生成命令行工具文件:
"bin":?{
????"picpic":?"./bin/index.js"
}
編寫(xiě)命令行工具代碼:
//?bin/index.js
#!/usr/bin/env?node
const?fs?=?require('fs-extra')
const?path?=?require('path')
const?child_process?=?require('child_process')
const?pkg?=?require(`${process.cwd()}/package.json`)
const?main?=?()?=>?{
?const?args?=?process.argv[2]
?const?root?=?process.cwd()
?const?getPath?=?p?=>?path.join(__dirname,?p)
?switch?(args)?{
??case?'init':
???pkg['scripts']['build']?=?'picpic?build'
???fs.writeFileSync('./package.json',?JSON.stringify(pkg,?null,?2).concat('\n'))
???if?(!fs.existsSync(`${root}/assets`))?fs.mkdirSync(`${root}/assets`)
???if?(!fs.existsSync(`${root}/.github`))?fs.mkdirSync(`${root}/.github`)
???if?(!fs.existsSync(`${root}/.gitignore`))?fs.writeFileSync(`${root}/.gitignore`,`/dist?\n/node_modules?\n.DS_Store`)
???fs.copySync(getPath('../.github'),?`${root}/.github`)
???console.log('----------?picpic?init?success!?----------?\n')
???break
??case?'build':
???child_process.execSync(`node?${getPath('../build/index.js')}`)
???break
??default:
???break
?}
}
try?{
?main()
?process.exit(0)
}?catch?(e)?{
?console.error(e)
?process.exit(1)
}
當(dāng)用戶 npm i @matrixage/picpic 之后,在 package.json 的 scripts 字段中加入 "init": "picpic init" ,然后執(zhí)行npm run init,項(xiàng)目根目錄會(huì)生成 .github assets 文件夾以及 .gitignore 文件。
這個(gè)時(shí)候用戶只需要把圖片移動(dòng)到assets文件夾中,支持在assets中新建任意不超過(guò)12層的文件夾。然后提交到github,github action將自動(dòng)進(jìn)行構(gòu)建,然后把構(gòu)建出的dist文件夾推送到倉(cāng)庫(kù)的gh-pages上,如果沒(méi)有開(kāi)啟gh-pages請(qǐng)自行開(kāi)啟。
至此,全部構(gòu)建流程講解完畢。這個(gè)過(guò)程,寫(xiě)預(yù)編譯代碼其實(shí)是最簡(jiǎn)單,麻煩的是:
如何構(gòu)建美的應(yīng)用? 如何讓用戶簡(jiǎn)單且優(yōu)雅地使用?
回首我做過(guò)的所有項(xiàng)目,花在邏輯上的時(shí)間其實(shí)是最少的,寫(xiě)邏輯是跟機(jī)器對(duì)話,機(jī)器嘛,就那幾句話,記住就行了。而畫(huà)界面,做交互,是在跟人,首先就是跟自己進(jìn)行對(duì)話,了解自己內(nèi)心深處的想法,然后就是跟用戶進(jìn)行對(duì)話,其實(shí)你把用戶當(dāng)成千千萬(wàn)萬(wàn)個(gè)我,那你就能感受到,你的idea,該如何生長(zhǎng),你的畫(huà),該是何模樣。
總之,以人為本。
DEMO地址:https://matrixage.github.io/picpic_example/
項(xiàng)目地址:https://github.com/MatrixAges/picpic
注意,在github的readme文件中使用username.github.io/repo/~這樣的鏈接,github會(huì)將之自動(dòng)轉(zhuǎn)化為camo.githubusercontent.com該host下的圖片鏈接,該鏈接被DNS污染了,如要預(yù)覽,請(qǐng)?jiān)趆ost中加入如下DNS解析:
199.232.96.133?raw.githubusercontent.com
199.232.96.133?camo.githubusercontent.com
如果你發(fā)現(xiàn)訪問(wèn)github很慢,那是因?yàn)楸镜胤?wù)商在進(jìn)行DNS網(wǎng)絡(luò)過(guò)濾,加入如下host跳過(guò)服務(wù)商網(wǎng)絡(luò)過(guò)濾:
140.82.112.3?github.com
如果你的倉(cāng)庫(kù)的主分支是master而不是main,請(qǐng)自行修改構(gòu)建腳本依賴分支為master,在.github/workflows/ci.yml中。
