继后台接口后完成后进行页面编写
1-开始前准备
- 采用 element-plus 为主要的ui库
- 采用 iconfont 为主要图标库
- 采用 node 搭建图片服务器
2-总结
2-1 node搭建图片服务器
这里采用的是数据流的形式进行读取文件和写入文件的
const fs = require('mz/fs');
const path = require('path');
const crypto = require('crypto');
import { Inject, Controller, Post, Get, Provide } from '@midwayjs/decorator';
import { Context } from 'egg';
import { Response } from '../../interface';
@Provide()
@Controller('/zf')
export class ApiController {
@Inject()
ctx: Context;
// 上传
@Post('/upload')
async uplaodFile() : Promise<Response> {
try {
// *** 文件形式 ***
const hash = crypto.createHash('md5');
const time = new Date().getTime();
const fileBase = file.filename.split('.')
const key = this.ctx.ip + fileBase[0] + String(time);
hash.update(key);
const fileName = hash.digest('hex') + `.${fileBase[fileBase.length - 1]}`;
// 处理文件,比如上传到云端
const reader = fs.createReadStream(file.filepath);
let filePath = path.join(__dirname, 'files/') + fileName;
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream);
return { success: true, message: '上传成功', data: { url:`/blingzf/zf/files/${fileName}` } }
} catch (e) {
return { success: false, message: '上传失败', data: {} }
}
}
// 查看图片-权限
@Get('/files/*')
async show() {
try {
const fileName = this.ctx.url.substring(10)
let filePath = path.join(__dirname, 'files/') + fileName;
const arr = filePath.split('.');
const type = arr[arr.length - 1];
const ctx = this.ctx;
let data = fs.readFileSync(filePath,'binary');
ctx.res.writeHead(200,{'Content-Type':`image/${type}`});
ctx.res.write(data, 'binary');
ctx.res.end();
return false;
}catch(e) {
let filePath = path.join(__dirname, 'files/null.png');
let data = fs.readFileSync(filePath,'binary');
this.ctx.res.write(data, 'binary');
this.ctx.res.end();
}
}
// 查看图片-普通
@Get('/img/*')
async img() {
const fileName = this.ctx.url.substring(8)
let filePath = path.join(__dirname, 'files/') + fileName;
const arr = filePath.split('.');
const type = arr[arr.length - 1];
const ctx = this.ctx;
try {
let data = fs.readFileSync(filePath,'binary');
ctx.res.writeHead(200,{'Content-Type':`image/${type}`});
ctx.res.write(data, 'binary');
ctx.res.end();
return false;
}catch(e) {
let filePath = path.join(__dirname, 'files/null.png');
let data = fs.readFileSync(filePath,'binary');
ctx.res.writeHead(200,{'Content-Type':`image/${type}`});
ctx.res.write(data, 'binary');
ctx.res.end();
}
}
}
- fs是文件系统模块,负责读写文件
- crypto这个模块的主要功能是加密解密
- path这个模块的主要功能是获取当前文件路径
2-2 携带请求头的HTML标签
2-2-1 href和src分别是什么。
href (Hypertext Reference)指定网络资源的位置,从而在当前元素或者当前文档和由当前属性定义的需要的锚点或资源之间定义一个链接或者关系。
src(source),指向外部资源的位置,指向的内容将会应用到文档中当前标签所在位置。
区别:
请求资源目的不同
- href 是为了建立联系,让当前标签能够链接到目标地址。(类似于请求重定向)
- src 是为了在请求资源时会将其指向的资源下载并应用到文档中。
请求结果不同
- href 是在当前文档和引用资源之间建立联系。
- src 是替换当前内容。
补充:
CSRF(Cross Site Request Forgery)跨站点请求伪造。这个是利用src可绕过同源限制的特性产生的一个漏洞。
假如一家银行用以运行转账操作的URL地址如下:
`https://bank.example.com/withdraw?account=AccoutName&amount=1000&for=PayeeName`
那么,一个恶意攻击者可以在另一个网站上放置如下代码,再引诱别人点开:
`<img src="https://bank.example.com/withdraw?account=Alice&amount=1000&for=Badman" />`
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
2-2-2 目的
由于src是img标签的一个属性,作为管理后台,某些图片需要满足权限才可以查看,这里就需要做一个验证,而这个验证就是在src请求时添加一个请求头。所以就需要用到Web Components
2-2-3 构建自定义标签
<template>
<auth-img :src="src" :style="style" :class="class" />
</template>
<script>
import { onBeforeMount, toRefs, reactive, watch } from 'vue';
let requestImage = function (url, element) {
let request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', url, true);
const arr = document.cookie.split(';');
let token = '';
arr.forEach((item) => {
if(item.split('=')[0].trim() === 'token'){
token = arr[i].split('=')[1].trim()
}
})
request.setRequestHeader('token', token);
request.onreadystatechange = e => {
if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
if (request.response.type == 'image/jpeg' || request.response.type == 'image/png' || request.response.type == 'image/jpg') {
element.src = URL.createObjectURL(request.response);
element.style.display = "block";
element.onload = () => {
URL.revokeObjectURL(element.src);
}
} else {
element.src = "https://z3.ax1x.com/2021/09/08/h7LuxP.png";
}
} else {
element.src = "https://z3.ax1x.com/2021/09/08/h7LuxP.png";
}
};
request.send(null);
};
class AuthImg extends HTMLElement {
constructor() {
super();
this.img = new Image();
const shadow = this.attachShadow({ mode: 'open' });
this.img.setAttribute("style", 'width: 100%;height: 100%;')
shadow.appendChild(this.img);
}
// 监听属性修改
static get observedAttributes() { return ['src', 'style', 'class']; }
// 生命周期钩子函数
connectedCallback() {
requestImage(this.getAttribute('src'), this.img);
if (this.getAttribute("style")) {
this.img.setAttribute("style", this.getAttribute("style"));
}
if (this.getAttribute("class")) {
this.img.className = this.getAttribute("class");
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'src' && (oldValue !== null && newValue !== oldValue)) {
requestImage(this.getAttribute('src'), this.img);
} else if (name == 'style') {
this.img.setAttribute("style", this.getAttribute("style"));
} else if (name == 'class') {
this.img.setAttribute("class",this.getAttribute("class"));
}
}
disconnectedCallback() {
this.img.src = '';
}
}
window.customElements.define('auth-img', AuthImg); // 定义自定义标签名称
export default {
props:{
src: {
type: String,
default: 'http://127.0.0.1:8080/blingzf/zf/files/eedf6bf3f2e6e36416d00dec7177b732.png'
},
style: {
type: Object,
default: {}
},
class: {
type: String,
default: ''
}
},
setup(props) {
}
}
</script>
2-2-3 vue中使用的问题
在vue中可以自定义组件与自定义标签类似都是根据个人需求对dom进行一层封装。 但是在vue默认情况下,会优先尝试将一个非原生的 HTML 标签解析为一个注册的 Vue 组件,如果失败则会将其渲染为自定义元素。这种行为会导致在开发模式下的 Vue 发出“failed to resolve component”的警告。 所以将以上的标签带入到页面使用是不会出现问题的,但是会有警告。
2-2-4 解决使用Web Components在vue的警告
这里使用的是vuecli
// vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => ({
...options,
compilerOptions: {
// 将所有以 auth- 开头的标签作为自定义元素处理
isCustomElement: tag => tag.startsWith('auth-')
}
}))
}
}
这么做的话就是绕过了vue的组件解析,警告就消失了。
这里vue的官网也是有介绍的
2-3 可以拖动组件搭建
在日常工作中,项目都是支持点开弹窗这个弹窗可以移动,我觉得挺好,既然觉得好就得拿来自己用,所以就写了一个自定义大小和位置的可移动弹窗具体长这样:
代码如下:
<template>
<div class="move-box"
:style="`left:${left}px;
top:${top}px;
width:${width}px;
height:${height}px;
background-color:${TitleBackgroundColor}`"
@mousedown="down"
@mouseup="up"
v-if="moveShow">
<div class="move-top" :style="`background-color:${TitleBackgroundColor}`">
<div class="move-title">{{title}}</div>
<el-tooltip
class="item"
effect="dark"
content="关闭"
placement="top"
>
<i class="iconfont close" @click="close"></i>
</el-tooltip>
</div>
<div class="move-view" @mousedown.stop="">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
export default defineComponent ({
props:{
width: {
type: Number,
default: 0
},
height: {
type: Number,
default: 0
},
TitleBackgroundColor: {
type: String,
default: "#3cc2d8"
},
moveShow: {
type: Boolean,
default: false
},
title: {
type: String,
default: "提示"
}
},
setup(props,context) {
const state = reactive({
left: (window.innerWidth-props.width)/2,
top: (window.innerHeight-props.height)/2,
layerX: 0,
layerY: 0
});
// 移动窗口事件
function move() {
const e:any = window.event;
state.left = e.clientX-state.layerX;
state.top = e.clientY-state.layerY;
};
const down = (e:any) => {
state.layerX = e.layerX;
state.layerY = e.layerY;
window.addEventListener('mousemove',move)
};
const up = () => {
window.removeEventListener('mousemove',move)
};
const close = () => {
context.emit('close')
};
return {
...toRefs(state),
down,
up,
close
}
}
})
</script>
2-4 vue3.0的this
2-4-1 getCurrentInstance
按照文档中的说明
getCurrentInstance被作为代替setup中可以使用的this
<el-form ref="ruleForm">
************************
// this的代替品,不推荐使用
const internalInstance:any = getCurrentInstance();
const submit = async(formName:string) => {
internalInstance.refs[formName].validate(async(valid:boolean) => {
if (valid) {
state.fullscreenLoading = true;
await store.dispatch('user/setUser',state.formData);
if(!store.getters.username){
let timer = setTimeout(()=>{
state.fullscreenLoading = false;
clearTimeout(timer);
},4000);
}else{
router.push('/')
}
} else {
return false;
}
})
};
2-4-2 ref
使用ref绑定响应性,在vue中使用比较多this的地方是结合了element表单做验证或者其他需要使用element原素对象方法的地方,这里可以利用ref建立响应性。
<el-form ref="ruleForm">
************************
setup(){
const form = ref();
const submit = async(formName:string) => {
form.value.validate(async(valid:boolean) => {
if (valid) {
state.fullscreenLoading = true;
await store.dispatch('user/setUser',state.formData);
if(!store.getters.username){
let timer = setTimeout(()=>{
state.fullscreenLoading = false;
clearTimeout(timer);
},4000);
}else{
router.push('/')
}
} else {
return false;
}
})
};
return {
form
}
}
2-5 路由跳转动画以及路由缓存
在使用github观摩一些大佬的作品时发现他们的路由切换特别柔顺,且切换路由不会重新发起数据请求。然后结合官网发现了
transition和keep-alive这两个一结合就达到了那样的效果,动画可以自定义成自己想要的样子。
在v3.0+ts中的使用如下
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<keep-alive>
<component :is="Component"/>
</keep-alive>
</transition>
</router-view>
- mode 是定义触发动画的时机
规划完成度
在构建过程中出现不少问题边改边写,目前完成了人员和账号的增删改查,整个权限模块的最终成型依然未实现(还得继续抽时间写代码)