如何利用七牛云搭建自己的轻量级图床应用

背景

之前在16年的时候使用过七牛云的对象存储服务开发过一些小的应用,它提供 10G 免费空间、10 万次免费 PUT 以及 100 万次免费 GET 请求,对于个人开发者或者用户来说基本上足够了。我之前主要用它为一些测试应用和博客中的小图片提供存储服务,之前懒得绑定自己的域名,用的是七牛云提供的临时域名,在七牛云临时域名有效期改为一个月之后,发现博客中的图片都挂掉了,于是准备重新绑定域名然后基于七牛云搭建一个自用的图传应用,下面为开发过程的简单记录。

七牛云服务

使用七牛云对象存储服务

  • 注册一个开发者账号, 可能需要实名认证
  • 创建存储空间:在七牛云资源主页选择对象存储,然后选择新建存储空间,根据自己实际情况填写,空间名称以、区域以及访问控制属性在后面会会用到

绑定自定义域名

为了支持资源外网访问,就需要绑定一个域名(如果只是想试一试的话,可以使用七牛云提供的一个月有效期的临时域名);绑定自己的域名要求要在大陆范围内备案

  • 域名绑定:点击绑定域名
  • 填写域名:按照自己的实际情况填写一个准备解析的域名,此域名需要在域名服务提供商进行 CNAME 进行解析
  • CNAME 解析:填写上域名信息后,七牛云会生成一个 CNAME 记录(类似于your-domain.qiniudns.com),在你自己的域名服务提供商进行解析,域名解析大概需要 10 分钟,CNAME 生效大概需要 20 分钟,官方 CNAME 解析帮助
    腾讯云域名解析
  • 创建密钥: 在个人中心页面选择密钥管理,创建应用需要的密钥
    密钥创建

开发前端界面

考虑到只需要一个页面,本着前后端分离的思想,前端使用 vueelement-ui 来实现一个简单的单页面应用, 主要分为主页面、导航条组件及图片上传组件几个部分

  • 主界面:将导航和图上组件组装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    <template>
    <div id="app">
    <NavMenu />
    <el-container>
    <el-main>
    <PictureContainer />
    </el-main>
    <el-footer></el-footer>
    </el-container>
    </div>
    </template>

    <script>
    import NavMenu from '@/components/NavMenu.vue'
    import PictureContainer from '@/components/PictureContainer.vue'

    export default {
    name: 'app',
    components: {
    NavMenu,
    PictureContainer
    },
    methods: {

    }
    }

    </script>

    <style>
    #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    /* text-align: center; */
    color: #2c3e50;
    margin-top: 0px;
    margin-left: 0px;

    }
    </style>
  • 导航条:只做静态展示,没有交互

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <template>
    <el-menu :default-active="activeIndex" class="nav" mode="horizontal" background-color="#545c64"
    text-color="#fff"
    active-text-color="#ffd04b">
    <el-row>
    <el-col :offset="4" :span="16">
    <el-menu-item index="1">
    <i class="el-icon-picture"></i>
    <span slot="title">我的图床</span>
    </el-menu-item>
    </el-col>
    </el-row>

    </el-menu>
    </template>

    <script>
    export default {
    name: "NavMenu",
    data() {
    return {
    activeIndex: "1"
    }
    }
    }
    </script>

    <style>
    body {
    display: block;
    margin: 0px;
    }
    </style>
  • 图片上传组件: 使用了 element-ui 的文件上传组件,并结合 qiniu-js SDK 实现图片上传,其中上传需要的 token 通过自己开发的后端文件上传 token 接口获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    <template>
    <div>
    <el-row>
    <el-col :span="12" :offset="6">
    <el-upload class="picture" drag action="http://upload.qiniu.com" mutiple
    :file-list="myFileList"
    list-type="picture"
    :on-success="handleSuccess"
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :before-upload="beforeUpload"
    :http-request="qiniuUpload"
    :auto-upload="auto">
    <i class="el-icon-upload"></i>
    <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
    <div class="el-upload__tip" slot="tip">只能上传常见的图片、音频、视频及pdf文件,且不超过100MB!</div>
    </el-upload>

    </el-col>
    </el-row>
    </div>

    </template>

    <script>
    // import moment from 'moment'
    import * as qiniu from 'qiniu-js'

    export default {
    name: "PictureContainer",
    data() {
    return {
    form: {
    key: '',
    token: ''
    },
    myFileList: [],
    domain: 'http://picture.hudengjin.com/',
    auto: true,
    qnToken: null,
    qnConfig: {
    useCdnDomain: true,
    disableStatisticsReport: false,
    retryCount: 6,
    region: qiniu.region.z0
    },
    qnPutextra: {
    fname: '',
    params: {},
    mimeType: null
    }
    }
    },
    methods: {
    beforeUpload(file) {
    let fileTypeList = ["gif", "jpg", "jpeg", "png", "bmp", "ico", "psd", "svg",
    "mp3", "aac", "ape", "flac", "wav", "wma","amr" ,
    "mp4", "mv4", "3gp", "mvg", "flv", "f4v", "wmv", "rmvb", "mov", "mkv", "ogg", "avi", "pdf"]
    let fileTemp = file.name.split('.')
    let fileType = fileTemp[fileTemp.length - 1]
    if (!fileTypeList.includes(fileType.toLowerCase())) {
    this.$message({
    message: "文件类型不合法!",
    type: "error"
    })
    return false
    }
    const isLt100M = file.size / 1024 /1024 < 100
    if (!isLt100M) {
    this.$message({
    message: '上传文件超过100M!',
    type: 'error'
    })
    }
    return isLt100M
    },
    handleSuccess(response, file, fileList) {
    for(var i = 0; i < fileList.length; i++) {
    fileList[i].name = '七牛云地址: ' + this.domain + fileList[i].response.key
    }
    this.myFileList = fileList
    // console.log(this.myFileList)
    },
    handlePreview() {

    },
    handleRemove() {

    },
    qiniuUpload(option) {
    this.$http.get('/token')
    .then(response => {

    this.qnToken = response.token
    const fileName = this.changeFileName(option.file.name)
    const observable = qiniu.upload(
    option.file,
    fileName,
    this.qnToken,
    this.qnPutextra,
    this.qnConfig
    )
    observable.subscribe({
    next: option.onProgress,
    error: option.onError,
    complete: option.onSuccess
    })
    }).catch(error => {
    console.log(error)
    } )

    },
    changeFileName(filename) {
    return filename.replace(/.[a-zA-Z0-9]+$/, (match) => {
    return `-${Date.now()}${match}`
    })
    }
    }
    }
    </script>
    <style>
    .list {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    padding: 15px;
    width: 360px;
    height: 180px;
    }


    </style>

开发后端 token 接口

为了安全起见,一般不会选择将 ACCESSKEY 和 SECRETKEY 直接暴露在前端,一般采用通过前端调用后端接口服务的形式,获取上传文件需要的 token 的形式进行。本次后端使用 Go 语言的 gin 框架结合七牛云的 Go SDK 开发。

  • 文件上传 token 生成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    package service

    import (
    "picture-api/config"
    "github.com/qiniu/api.v7/storage"
    "github.com/qiniu/api.v7/auth/qbox"
    "fmt"
    )

    var bucket, accessKey, secretKey string

    func init() {
    bucket = config.GetEnv().QINIU_BUCKET
    accessKey = config.GetEnv().QINIU_ACCESS_KEY
    secretKey = config.GetEnv().QINIU_SECRET_KEY
    }

    func GenerateSimpleToken() string {
    fmt.Println(bucket)
    putPolicy := storage.PutPolicy{
    Scope: bucket,
    }
    mac := qbox.NewMac(accessKey, secretKey)
    uploadToken := putPolicy.UploadToken(mac)
    return uploadToken
    }

    func GenerateOverWriteToken(fileName string) string {
    putPolicy := storage.PutPolicy{
    Scope: fmt.Sprintf("%s:%s", bucket, fileName),
    }
    mac := qbox.NewMac(accessKey, secretKey)
    return putPolicy.UploadToken(mac)
    }
  • gin 跨域设置(前后端分离,不在同一个域下需要开启), 需要使用 github.com/gin-contrib/cors

    1
    2
    3
    4
    5
    import "github.com/gin-contrib/cors"
    // cors
    corsConfig := cors.DefaultConfig()
    corsConfig.AllowAllOrigins = true
    router.Use(cors.New(corsConfig))

部署

  • 前端部署,将前端源码使用 vue 打包编译后,将 dist 里的文件上传到服务器,将其作为静态站点部署,nginx 配置文件如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    server {
    server_name xxx.com;
    root "your-static-file-dir/";
    index index.html index.htm;
    charset utf-8;
    location / {
    try_files $uri $uri/ /index.html;
    }
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt { access_log off; log_not_found off; }
    access_log off;
    error_log /var/log/nginx/upload.app-error.log error;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/xxx.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/xxx.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    }
    server {
    if ($host = xxx.com) {
    return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    server_name xxx.com;
    return 404; # managed by Certbot


    }
  • 后端部署,目标部署服务器是 linux 系统,若在windows 和 MacOs 上开发的话,需要通过交叉编译,编译为对应的文件,上传到目标服务器,选择在后台运行,并在 nginx 中配置反向代理, nginx 反向代理配置如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    server {
    server_name xxx-api.com; # 服务器域名和 IP 地址
    location / {
    proxy_pass http://127.0.0.1:4000/; # 后端服务运行端口
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_redirect off;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/xxx-api.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/xxx-api.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    }
    server {
    if ($host = xxx-api.com) {
    return 301 https://$host$request_uri;
    } # managed by Certbot


    server_name xxx-api.com;
    listen 80;
    return 404; # managed by Certbot
    }

配置完重启 nginx,访问静态站点地址,应该可以看到界面,enjoy!

hudengjin wechat
huprince's 微信公众号
激情打赏,放肆挥霍