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