我的项目背景
前端开发过程中不可避免会用到图片、视频等多媒体物料,常见的解决计划通常会进行动静拆散,将图片等资源搁置在图床上,除了应用业界罕用的图床资源,比方:七牛云、微博图床等,除了借助第三方图床外,咱们也能够本人搭建一个图床,为团队业务开发提供更好的根底服务,晋升开发体验及效率。本文旨在回顾总结下自建图床的前端局部实现计划,心愿可能给有相似需要的同学一些借鉴和计划。
计划
前端局部架构选型,思考到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>
总结
前端图床作为前端基建侧的一项重要的开发工具,不仅可能为业务开发人员提供更好的开发体验,也能节俭业务开发过程中造成的效率升高,从而晋升开发效率,降低成本损耗。前端展现的实现有多种不同的计划,对于有着更高要求的前端图床实现也能够基于需要进行更高层次的展现与晋升。