每個人都需要github,每個人都需要圖床,so,github = 圖床

語言: CN / TW / HK

起因

說起來,圖床應用這東西,在github上有很多,但是大多都是基於一些雲廠商免費的靜態儲存服務來實現的,比如七牛雲的靜態儲存,考慮到這些雲廠商的賺錢慾望,所以我並不放心將他們作為圖床的服務提供商。

也有支援github的,比如picgo,不過涉及到personal token,我也不是很放心將自己的token寫入到一個開源專案的桌面應用裡。而且picgo匯出的github圖片連結是以 githubusercontent.com 為host的連結,眾所周知,該域名在中國很多地區都被DNS汙染了,只有通過改host或是科學上網進行訪問,所以結論是,picgo基於github匯出的圖片連結,在國內等於沒用。

那有沒有一種方式,既能讓圖片連結不被DNS汙染或是被牆掉,又不會涉及到開發者personal token,影響賬戶安全呢?

於是,就有了picpic。picpic是我在做一個另一個大型的開源專案的過程中抽空實現的,初始版本只用了兩天就寫出來了,但是我本人自認為是一個合格和還不錯的product maker,並不願意產出一個使用繁瑣,功能殘缺的半成品給別人使用——關鍵是自己用的也不爽。

我做產品,核心觀點就是,做出來的東西自己願不願意用,用起來有沒有感受到“美”,是不是能夠沉靜在產品中去感受它,這很重要,正是因為我從沒將自己定位成一個前端,或是node開發,而是product maker,終極理想就是artist,就是做藝術,內心始終有一個想法:你不是在寫程式碼,你是在畫一幅畫,你享受這個過程,如果能夠讓別人享受到“結果”,那是再好不過了。

所以就有了它:

DEMO地址:https://matrixage.github.io/picpic_example/

專案地址:https://github.com/MatrixAges/picpic

picpic

基於離線版本,脫離了webpack的vue.js構建的單頁面應用,原理就是通過node把圖片資料預編譯並寫入到window物件中,然後通過chunk進行分片,提供翻頁功能,至於資料夾模式,則是通過node把assets資料夾下的檔案結構預編譯成樹形資料,寫入到window物件,然後給頁面中的js進行呼叫。

幾經打磨,最後我把它做成了cli,你只需要npm i @matrixage/picpic,即可使用。

下面講講,我是如何通過node和vue構建這樣一個單頁面應用的。

沒有webpack的web應用

使用github actions也有一段時間了,在經歷過很多次構建之後,我觀察到了一個現象:那就是80%的時間都是webpack花掉的,關鍵是一些很簡單的專案,因為webpack,還是會有一個比較長的安裝npm包的時間,那這對於一個圖床應用來說,是致命的。

所以我決定擺脫webpack,使用離線版本的vue.min.js來構建應用,將部署時間控制在30s以內,做到提交圖片,即刻可用。

<!-- source.html -->

<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進行元件化開發

在html頂部引入include.js,改檔案的作用是在文件載入完成之後將include標籤中的地址通過同步的XHR,請求到元件的html內容,然後寫入到頁面中。

// include.js

getFileContent: function (url){
    var o = new XMLHttpRequest()
	
	o.open('get', url, false)
	o.send(null)
	
	return o.responseText
}
複製程式碼

接著通過自定義事件發出通知:

// 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 (){...})
複製程式碼

通過node預編譯元件

僅僅是使用include是不夠的,元件的js和css程式碼同樣要分離出來,這樣才有意義,於是node出場,其實你理解的webpack,不過時穿上紳士馬甲的node編譯指令碼,本質上還是預編譯。

所以不用webpack,我們直溯本源,手寫預編譯程式碼。在picpic專案根目錄新建一個build資料夾,其中的檔案就是預編譯要用的程式碼。

// 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就是注入元件和資料之後的html,接下來展示一下如何進行元件注入。

// 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
複製程式碼

通過返回this的方法進行鏈式呼叫,比一層一層用方法包裹優雅很多,有沒有感受到程式碼之美,嘻嘻。

injectStyles injectTemplates injectJs這三種方法異曲同工,原理特簡單,就是字串替換,不過這裡要注意空格,少一個都匹配不到。

// 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(
		`
      <style></style>
`,
		`
      <style>
            ${items.reduce((total, item) => (total += item), '')}
      </style>
`
	)
}

複製程式碼

在頁面中,三種佔位符分別用於注入元件相關的檔案:

<!-- source.html -->

<!-- 注入樣式匯入程式碼 -->
<style></style>

<!-- 注入模版匯入程式碼 -->
<template-slot></template-slot>

<!-- 注入指令碼匯入程式碼 -->
<script id="component_scripts"></script>
複製程式碼

注入之後的結果為:

<!-- dist/index.html -->

<!-- 注入樣式匯入程式碼 -->
<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>
複製程式碼

不要詬病元件資料夾大寫,我是react的擁躉,如果不是因為web-component強制使用-分割符小寫,所有的元件我都希望大寫,因為辨識度比前者高很多。

通過node預編譯目錄資料

主要是通過dree到處樹形資料,通過imageinfo獲取圖片長寬,然後再進行資料裁剪,把需要的資料進行組裝後匯出。程式碼多且雜,這裡僅結果,有興趣的可以去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"
                        }
                    ]
                }
            ]
        }
    ]
}
複製程式碼

然後寫入到html中:

// build/inject/injectData.js

const { getFileTree } = require('../utils')

module.exports = async str => {
	const tree = await getFileTree()

	return str.replace(
		`
      <head>
            <title>PicPic</title>
      </head>
`,
		`
      <head>
            <title>PicPic</title>
            <script>
                  window.img_paths=${JSON.stringify(tree)}
            </script>
      </head>
`
	)
}
複製程式碼

做成命令列工具

僅僅做成上面那樣使用起來,還需要別人clone你的倉庫,後續升級麻煩,而且編譯原始檔什麼的都暴露出來了,看起來髒的不行,所以不僅要產品本身美,使用方式也需要簡單優雅。

package.json 中新增如下欄位,釋出包之後,當別人在 npm i @matrixage/picpic 時會生成命令列工具檔案:

"bin": {
    "picpic": "./bin/index.js"
}
複製程式碼

編寫命令列工具程式碼:

// 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)
}
複製程式碼

當用戶 npm i @matrixage/picpic 之後,在 package.jsonscripts 欄位中加入 "init": "picpic init" ,然後執行npm run init,專案根目錄會生成 .github assets 資料夾以及 .gitignore 檔案。

這個時候使用者只需要把圖片移動到assets資料夾中,支援在assets中新建任意不超過12層的資料夾。然後提交到github,github action將自動進行構建,然後把構建出的dist資料夾推送到倉庫的gh-pages上,如果沒有開啟gh-pages請自行開啟。

至此,全部構建流程講解完畢。這個過程,寫預編譯程式碼其實是最簡單,麻煩的是:

  • 如何構建美的應用?
  • 如何讓使用者簡單且優雅地使用?

回首我做過的所有專案,花在邏輯上的時間其實是最少的,寫邏輯是跟機器對話,機器嘛,就那幾句話,記住就行了。而畫介面,做互動,是在跟人,首先就是跟自己進行對話,瞭解自己內心深處的想法,然後就是跟使用者進行對話,其實你把使用者當成千千萬萬個我,那你就能感受到,你的idea,該如何生長,你的畫,該是何模樣。

總之,以人為本。

DEMO地址:https://matrixage.github.io/picpic_example/

專案地址:https://github.com/MatrixAges/picpic

注意,在github的readme檔案中使用username.github.io/repo/~這樣的連結,github會將之自動轉化為camo.githubusercontent.com該host下的圖片連結,該連結被DNS汙染了,如要預覽,請在host中加入如下DNS解析:

199.232.96.133 raw.githubusercontent.com
199.232.96.133 camo.githubusercontent.com
複製程式碼

如果你發現訪問github很慢,那是因為本地服務商在進行DNS網路過濾,加入如下host跳過服務商網路過濾:

140.82.112.3 github.com
複製程式碼

如果你的倉庫的主分支是master而不是main,請自行修改構建指令碼依賴分支為master,在.github/workflows/ci.yml中。