使用 github 快速搭建属于自己的图床

共 8923字,需浏览 18分钟

 ·

2021-02-10 03:05

作者:_Wendao 
https://juejin.cn/post/6906791889777721352

起因

说起来,图床应用这东西,在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进行调用。

服务基于github pages,自动化构建使用的是github actions,通过自动化构建部署,部署静态文件到gh-pages分支,然后访问 username.github.io/${repo}/${img_path} 即可访问不被枪的静态图片。

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

下面讲讲,我是如何通过node和vue构建这样一个单页面应用的。

没有webpack的web应用

使用github actions也有一段时间了,在经历过很多次构建之后,我观察到了一个现象:那就是80%的时间都是webpack花掉的,关键是一些很简单的项目,因为webpack,还是会有一个比较长的安装npm包的时间,那这对于一个图床应用来说,是致命的。

所以我决定摆脱webpack,使用离线版本的vue.min.js来构建应用,将部署时间控制在30s以内,做到提交图片,即刻可用。



<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

getFileContentfunction (url){
    var o = new XMLHttpRequest()
 
 o.open('get', url, false)
 o.send(null)
 
 return o.responseText
}

接着通过自定义事件发出通知:

// include.js

var evt = new CustomEvent('included', {
 bubblestrue,
 cancelablefalse
})

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>


<template-slot>template-slot>


<script id="component_scripts">script>

注入之后的结果为:




<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(
  `
      
            PicPic
      
`
,
  `
      
            PicPic
            
      
`

 )
}

做成命令行工具

仅仅做成上面那样使用起来,还需要别人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, null2).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中。

浏览 34
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报