我的项目背景

前端开发过程中不可避免会用到图片、视频等多媒体物料,常见的解决计划通常会进行动静拆散,将图片等资源搁置在图床上,除了应用业界罕用的图床资源,比方:七牛云、微博图床等,除了借助第三方图床外,咱们也能够本人搭建一个图床,为团队业务开发提供更好的根底服务,晋升开发体验及效率。本文旨在回顾总结下自建图床的前端局部实现计划,心愿可能给有相似需要的同学一些借鉴和计划。

计划

前端局部架构选型,思考到Vue3行将成为主版本,作为前端基建侧的利用,思考想要应用Vue3全家桶来进行前端侧的相干实现,这里应用了vite(vue-template-ts)+vue3+vuex@next+vue-router@next的应用计划,也为vite的打包构建进行一步的技术预(cai)研(keng)。(ps:vite的确快,然而目前间接上工业环境还须要考量,还有不少坑,集体认为跨语言的前端工程化可能会是后续前端工程化的倒退方向)

目录

  • src

    • assets
    • components

      • index.ts
      • Card.vue
      • Login.vue
      • Upload.vue
      • WrapperLayouts.vue
      • WrapperLogin.vue
      • WrapperUpload.vue
    • config

      • index.ts
      • menuMap.ts
      • routes.ts
    • layouts

      • index.ts
      • Aside.vue
      • Layouts.vue
      • Main.vue
      • Nav.vue
    • route

      • index.ts
    • store

      • index.ts
    • utils

      • index.ts
      • reg.ts
      • validate.ts
    • views

      • Page.vue
    • App.vue
    • index.scss
    • main.ts
    • vue-app-env.d.ts
  • index.html
  • tsconfig.json
  • vite.config.ts

实际

前端图床波及到权限验证,对于获取图片不进行认证确认,而对于须要进行上传及删除图片操作会须要进行登录鉴权

源码

vue3中能够通过class以及template两种计划来书写,应用composition-api的计划,集体倡议还是应用class-component更加难受,也更像react的写法,这里夹杂应用了composition-api和options-api的应用,目前vue是兼容的,对于从vue2中过去的同学,能够逐渐去适应composition-api的写法,而后逐渐依照hooks的函数式的思路去进行前端的业务实现

vite.config.ts

vite构建相干的一些配置,能够依据我的项目需要进行环境配置

const path = require('path')// vite.config.js # or vite.config.tsconsole.log(path.resolve(__dirname, './src'))module.exports = {  alias: {    // 键必须以斜线开始和完结    '/@/': path.resolve(__dirname, './src'),  },  /**   * 在生产中服务时的根本公共门路。   * @default '/'   */  base: './',  /**   * 与“根”相干的目录,构建输入将放在其中。如果目录存在,它将在构建之前被删除。   * @default 'dist'   */  outDir: 'dist',  port: 3000,  // 是否主动在浏览器关上  open: false,  // 是否开启 https  https: false,  // 服务端渲染  ssr: false,  // 引入第三方的配置  //   optimizeDeps: {  //     include: ["moment", "echarts", "axios", "mockjs"],  //   },  proxy: {    // 如果是 /bff 打头,则拜访地址如下    '/bff/': {      target: 'http://localhost:30096/',// 'http://10.186.2.55:8170/',        changeOrigin: true,      rewrite: (path) => path.replace(/^\/bff/, ''),    }  },  optimizeDeps: {    include: ['element-plus/lib/locale/lang/zh-cn', 'axios'],  },}

Page.vue

每个子项目页面的展现,只须要一个组件,进行不同的数据渲染即可

<template>  <div class="page-header">    <el-row>      <el-col :span="12">        <el-page-header          :content="$route.fullPath.split('/').slice(2).join(' > ')"          @back="handleBack"        />      </el-col>      <el-col :span="12">        <section class="header-button">          <!-- <el-button class="folder-add" :icon="FolderAdd" @click="handleFolder" >新建文件夹</el-button> -->          <el-button class="upload" :icon="Upload" type="success" @click="handleImage">上传图片</el-button>        </section>      </el-col>    </el-row>  </div>  <div class="page">    <el-row :gutter="10">      <el-col v-for="(item, index) in cards" :xs="12" :sm="8" :md="6" :lg="4" :xl="4">        <Card          @next="handleRouteView(item.ext, item.name)"          @delete="handleDelete"          :name="item.name"          :src="item.src"          :ext="item.ext"          :key="index"        />      </el-col>    </el-row>    <el-pagination      layout="sizes, prev, pager, next, total"      @size-change="handleSizeChange"      @current-change="handlePageChange"      :current-page.sync="pageNum"      :page-size="pageSize"      :total="total"    ></el-pagination>    <router-view />  </div>  <WrapperUpload ref="wrapper-upload" :headers="computedHeaders" />  <WrapperLogin ref="wrapper-login" /></template><script lang="ts">import {  defineComponent,} from 'vue';import { useRoute } from 'vue-router'import {  FolderAdd,  Upload} from '@element-plus/icons-vue'import { Card, WrapperUpload, WrapperLogin } from '../components'export default defineComponent({  name: 'Page',  components: {    Card,    WrapperUpload,    WrapperLogin  },  props: {  },  setup() {    return {      FolderAdd,      Upload    }  },  data() {    return {      cards: [],      total: 30,      pageSize: 30,      pageNum: 1,      bucketName: '',      prefix: '',    }  },  watch: {    $route: {      immediate: true,      handler(val) {        console.log('val', val)        if (val) {          this.handleCards()        }      }    }  },  methods: {    handleBack() {      this.$router.go(-1)    },    handleFolder() {    },    handleDelete(useName) {      console.log('useName', useName)      const [bucketName, ...objectName] = useName.split('/');      console.log('bukcetName', bucketName);      console.log('objectName', objectName.join('/'));      if (sessionStorage.getItem('token')) {        this.$http.post("/bff/imagepic/object/removeObject", {          bucketName: bucketName,          objectName: objectName.join('/')        }, {          headers: {            'Authorization': sessionStorage.getItem('token'),          }        }).then(res => {          console.log('removeObject', res)          if (res.data.success) {            this.$message.success(`${objectName.pop()}图片删除胜利`);            setTimeout(() => {              this.$router.go(0)            }, 100)          } else {            this.$message.error(`${objectName.pop()}图片删除失败,失败起因:${res.data.data}`)          }        })      } else {        this.$refs[`wrapper-login`].handleOpen()      }    },    handleImage() {      sessionStorage.getItem('token')        ? this.$refs[`wrapper-upload`].handleOpen()        : this.$refs[`wrapper-login`].handleOpen()    },    handleRouteView(ext, name) {      // console.log('extsss', ext)      if (ext == 'file') {        console.log('$router', this.$router)        console.log('$route.name', this.$route.name, this.$route.path)        this.$router.addRoute(this.$route.name,          {            path: `:${name}`,            name: name,            component: () => import('./Page.vue')          }        )        console.log('$router.options.routes', this.$router.options.routes)        this.$router.push({          path: `/page/${this.$route.params.id}/${name}`        })      } else {      }    },    handlePageChange(val) {      this.pageNum = val;      this.handleCards();    },    handleSizeChange(val) {      this.pageSize = val;      this.handleCards();    },    handleCards() {      this.cards = [];      let [bucketName, prefix] = this.$route.path.split('/').splice(2);      this.bucketName = bucketName;      this.prefix = prefix;      console.log('bucketName', bucketName, prefix)      this.$http.post("/bff/imagepic/object/listObjects", {        bucketName: bucketName,        prefix: prefix ? prefix + '/' : '',        pageSize: this.pageSize,        pageNum: this.pageNum      }).then(res => {        console.log('listObjects', res.data)        if (res.data.success) {          this.total = res.data.data.total;          if (prefix) {            this.total -= 1;            return res.data.data.lists.filter(f => f.name != prefix + '/')          }          return res.data.data.lists        }      }).then(data => {        console.log('data', data)        data.forEach(d => {          // 当前目录下          if (d.name) {            this.$http.post('/bff/imagepic/object/presignedGetObject', {              bucketName: bucketName,              objectName: d.name            }).then(url => {              // console.log('url', url)              if (url.data.success) {                const ext = url.data.data.split('?')[0];                // console.log('ext', ext)                let src = '', ext_type = '';                switch (true) {                  case /\.(png|jpg|jpeg|gif|svg|webp)$/.test(ext):                    src = url.data.data;                    ext_type = 'image';                    break;                  case /\.(mp4)$/.test(ext):                    src = 'icon_mp4';                    ext_type = 'mp4';                    break;                  case /\.(xls)$/.test(ext):                    src = 'icon_xls';                    ext_type = 'xls';                    break;                  case /\.(xlsx)$/.test(ext):                    src = 'icon_xlsx';                    ext_type = 'xlsx';                    break;                  case /\.(pdf)$/.test(ext):                    src = 'icon_pdf';                    ext_type = 'pdf';                    break;                  default:                    src = 'icon_unknow';                    ext_type = 'unknown';                    break;                }                this.cards.push({                  name: d.name,                  src: src,                  ext: ext_type                })              }            })          } else {            if (d.prefix) {              const src = 'icon_file', ext_type = 'file';              this.cards.push({                name: d.prefix.slice(0, -1),                src: src,                ext: ext_type              })            }          }        })      })    }  },  computed: {    computedHeaders: function () {      console.log('this.$route.fullPath', this.$route.fullPath)      return {        'Authorization': sessionStorage.getItem('token'),        'bucket': this.bucketName,        'folder': this.$route.fullPath.split('/').slice(3).join('/')      }    }  }})</script><style lang="scss">@import "../index.scss";.page-header {  margin: 1rem;  .header-info {    display: flex;    align-items: center;    justify-content: space-between;  }  .header-button {    display: flex;    align-items: center;    justify-content: right;    .el-button.upload {      background-color: $color-primary;    }    .el-button.upload:hover {      background-color: lighten($color: $color-primary, $amount: 10%);    }  }}.page {  margin: 1rem;  height: 90vh;  .el-row {    height: calc(100% - 6rem);    overflow-y: scroll;  }  .el-pagination {    margin: 1rem 0;  }}</style>

Login.vue

进行根底的登录/注册实现,可在外侧进行弹窗及嵌入的包裹,将业务逻辑与展示模式拆散

<template>  <div :class="loginClass">    <section class="login-header">      <span class="title">{{ title }}</span>    </section>    <section class="login-form">      <template v-if="form == 'login'">        <el-form          ref="login-form"          label-width="70px"          label-position="left"          :model="loginForm"          :rules="loginRules"        >          <el-form-item            :key="item.prop"            v-for="item in loginFormItems"            :label="item.label"            :prop="item.prop"          >            <el-input              v-model="loginForm[`${item.prop}`]"              :placeholder="item.placeholder"              :type="item.type"            ></el-input>          </el-form-item>        </el-form>      </template>      <template v-else-if="form == 'register'">        <el-form          ref="register-form"          label-width="100px"          label-position="left"          :model="registerForm"          :rules="registerRules"        >          <el-form-item            :key="item.prop"            v-for="item in registerFormItems"            :label="item.label"            :prop="item.prop"          >            <el-input              v-model="registerForm[`${item.prop}`]"              :placeholder="item.placeholder"              :type="item.type"            ></el-input>          </el-form-item>        </el-form>      </template>    </section>    <section class="login-select">      <span class="change" v-if="form == 'login'" @click="isShow = true">批改明码</span>      <span class="go" @click="handleGo(form)">{{ form == 'login' ? ' 去注册 >>' : ' 去登录 >>' }}</span>    </section>    <section class="login-button">      <template v-if="form == 'login'">        <el-button @click="handleLogin">登录</el-button>      </template>      <template v-else-if="form == 'register'">        <el-button @click="handleRegister">注册</el-button>      </template>    </section>  </div>  <el-dialog v-model="isShow">    <el-form      ref="change-form"      label-width="130px"      label-position="left"      :model="changeForm"      :rules="changeRules"    >      <el-form-item        :key="item.prop"        v-for="item in changeFormItems"        :label="item.label"        :prop="item.prop"      >        <el-input          v-model="changeForm[`${item.prop}`]"          :placeholder="item.placeholder"          :type="item.type"        ></el-input>      </el-form-item>    </el-form>    <div class="change-button">      <el-button class="cancel" @click="isShow = false">勾销</el-button>      <el-button class="confirm" @click="handleConfirm" type="primary">确认</el-button>    </div>  </el-dialog></template><script lang="ts">import {  defineComponent} from 'vue';import { validatePwd, validateEmail, validateName, validatePhone } from '../utils/index';export default defineComponent({  name: 'Login',  props: {    title: {      type: String,      default: ''    },    border: {      type: Boolean,      default: false    }  },  data() {    return {      form: 'login',      isShow: false,      loginForm: {        phone: '',        upwd: ''      },      loginRules: {        phone: [          {            required: true,            validator: validatePhone,            trigger: 'blur',          }        ],        upwd: [          {            validator: validatePwd,            required: true,            trigger: 'blur',          }        ]      },      loginFormItems: [        {          label: "手机号",          prop: "phone",          placeholder: '请输出手机号'        },        {          label: "明码",          prop: "upwd",          placeholder: '',          type: 'password'        }      ],      registerForm: {        name: '',        tfs: '',        email: '',        phone: '',        upwd: '',        rpwd: ''      },      registerFormItems: [        {          label: "姓名",          prop: "name",          placeholder: ''        },        {          label: "TFS账号",          prop: "tfs",          placeholder: ''        },        {          label: "邮箱",          prop: "email",          placeholder: ''        },        {          label: "手机号",          prop: "phone",          placeholder: ''        },        {          label: "请输出明码",          prop: "upwd",          placeholder: '',          type: 'password'        },        {          label: "请确认明码",          prop: "rpwd",          placeholder: '',          type: 'password'        }      ],      registerRules: {        name: [          {            validator: validateName,            trigger: 'blur',          }        ],        tfs: [          {            required: true,            message: '请按要求输出tfs账号',            trigger: 'blur',          }        ],        email: [          {            required: true,            validator: validateEmail,            trigger: 'blur',          }        ],        phone: [          {            required: true,            validator: validatePhone,            trigger: 'blur',          }        ],        upwd: [          {            required: true,            validator: validatePwd,            trigger: 'blur',          }        ],        rpwd: [          {            required: true,            validator: validatePwd,            trigger: 'blur',          },          {            validator(rule: any, value: any, callback: any) {              if (value != this.registerForm.upwd) {                callback(new Error('输出的明码不同'))              }            },            trigger: 'blur',          }        ],      },      changeForm: {        phone: '',        opwd: '',        npwd: '',        rpwd: ''      },      changeFormItems: [        {          label: "手机号",          prop: "phone",          placeholder: '请输出手机号'        },        {          label: "请输出原始明码",          prop: "opwd",          placeholder: '',          type: 'password'        },        {          label: "请输出新密码",          prop: "npwd",          placeholder: '',          type: 'password'        },        {          label: "请反复新密码",          prop: "rpwd",          placeholder: '',          type: 'password'        }      ],      changeRules: {        phone: [          {            required: true,            validator: validatePhone,            trigger: 'blur',          }        ],        opwd: [          {            required: true,            validator: validatePwd,            trigger: 'blur',          }        ],        npwd: [          {            required: true,            validator: validatePwd,            trigger: 'blur',          }        ],        rpwd: [          {            required: true,            validator: validatePwd,            trigger: 'blur',          },          {            validator(rule: any, value: any, callback: any) {              if (value != this.changeForm.npwd) {                callback(new Error('输出的明码不同'))              }            },            trigger: 'blur',          }        ],      }    }  },  computed: {    loginClass() {      return this.border ? 'login login-unwrapper' : 'login login-wrapper'    }  },  methods: {    handleGo(form) {      if (form == 'login') {        this.form = 'register'      } else if (form == 'register') {        this.form = 'login'      }    },    handleLogin() {      this.$http.post("/bff/imagepic/auth/login", {        phone: this.loginForm.phone,        upwd: this.loginForm.upwd      }).then(res => {        if (res.data.success) {          this.$message.success('登录胜利');          sessionStorage.setItem('token', res.data.data.token);          this.$router.go(0);        } else {          this.$message.error(res.data.data.err);        }      })    },    handleRegister() {      this.$http.post("/bff/imagepic/auth/register", {        name: this.registerForm.name,        tfs: this.registerForm.tfs,        email: this.registerForm.email,        phone: this.registerForm.phone,        upwd: this.registerForm.upwd      }).then(res => {        if (res.data.success) {          this.$message.success('注册胜利');        } else {          this.$message.error(res.data.data.err);        }      })    },    handleConfirm() {      this.$http.post("/bff/imagepic/auth/change", {        phone: this.changeForm.phone,        opwd: this.changeForm.opwd,        npwd: this.changeForm.npwd      }).then(res => {        if (res.data.success) {          this.$message.success('批改明码胜利');        } else {          this.$message.error(res.data.data.err);        }      })    }  }})</script><style lang="scss">@import "../index.scss";.login-wrapper {}.login-unwrapper {  border: 1px solid #ececec;  border-radius: 4px;}.login {  &-header {    text-align: center;    .title {      font-size: 1.875rem;      font-size: bold;      color: #333;    }  }  &-form {    margin-top: 2rem;  }  &-select {    display: flex;    justify-content: right;    align-items: center;    cursor: pointer;    .go {      color: orange;      text-decoration: underline;      margin-left: 0.5rem;    }    .go:hover {      color: orangered;    }    .change {      color: skyblue;    }    .change:hover {      color: rgb(135, 178, 235);    }  }  &-button {    margin-top: 2rem;    .el-button {      width: 100%;      background-color: $color-primary;      color: white;    }  }}.change-button {  display: flex;  justify-content: space-around;  align-items: center;  .confirm {    background-color: $color-primary;  }}</style>

routes.ts

vue-router@next中的动静路由计划略有不同,有相似rank的排名机制,具体能够参考vue-router@next的官网文档

import { WrapperLayouts } from '../components';import menuMap from './menuMap'// 1. 定义路由组件, 留神,这里肯定要应用 文件的全名(蕴含文件后缀名)const routes = [    {         path: "/",        component: WrapperLayouts,        redirect: `/page/${Object.keys(menuMap)[0]}`,        children: [            {                path: '/page/:id',                name: 'page',                component: () => import('../views/Page.vue'),                children: [                {                    path: '/page/:id(.*)*',                    // redirect: `/page/${Object.keys(menuMap)[0]}`,                    name: 'pageno',                    component: () => import('../views/Page.vue')                }                ]            }        ]    },];export default routes;
import {createRouter, createWebHashHistory} from 'vue-router';import { routes } from '../config';// Vue-router新版本中,须要应用createRouter来创立路由export default  createRouter({  // 指定路由的模式,此处应用的是hash模式  history: createWebHashHistory(),  routes // short for `routes: routes`})

Aside.vue

联合路由进行右边侧边栏的路由跳转及显示

<template>  <div class="aside">    <el-menu @select="handleSelect" :default-active="Array.isArray($route.params.id) ? $route.params.id[0] : $route.params.id">      <el-menu-item v-for="(menu, index) in menuLists" :index="menu.id" >        <span>{{menu.label}}</span>      </el-menu-item>    </el-menu>  </div></template><script lang="ts">import {  computed,  defineComponent,  getCurrentInstance,  onMounted,  reactive,  ref,  toRefs,} from 'vue';export default defineComponent({  name: 'Aside',  props: {    menuMap: {      type: Object,      default: () => {}    }  },  components: {  },  methods: {    handleSelect(e) {      console.log('$route', this.$route.params.id)      console.log('select', e)      this.$router.push(`/page/${e}`)    }  },  setup(props, context) {    console.log('props', props.menuMap)    //援用全局变量    const { proxy } = getCurrentInstance();    const menuMap = props.menuMap;    let menuLists = reactive([]);    //dom挂载后    onMounted(() => {      handleMenuLists();    });    function handleMenuLists() {      (proxy as any).$http.get('/bff/imagepic/bucket/listBuckets').then(res => {        console.log('listBuckets', res);        if(res.data.success) {          res.data.data.forEach(element => {            menuMap[`${element.name}`] && menuLists.push({              id: element.name,              label: menuMap[`${element.name}`]            })           })        }      })    }    return {      ...toRefs(menuLists),      handleMenuLists,      menuLists    };  }})</script><style lang="scss">.aside {  height: 100%;  background-color: #fff;  width: 100%;  border-right: 1px solid #d7d7d7;}</style>

总结

前端图床作为前端基建侧的一项重要的开发工具,不仅可能为业务开发人员提供更好的开发体验,也能节俭业务开发过程中造成的效率升高,从而晋升开发效率,降低成本损耗。前端展现的实现有多种不同的计划,对于有着更高要求的前端图床实现也能够基于需要进行更高层次的展现与晋升。