一、如何中断已经发出去的请求?
以vue项目用axios为例,由于axios提供了cancelToken的方法,只需要在发送请求的拦截器里将所有发出的接口放在vuex的全局数组cancelTokenArr里,然后在router的路由守卫里全部清理掉,相当于清理的是上个页面的所有接口。
1.请求拦截器
axios.interceptors.request.use(
config => {
config.cancelToken = new axios.CancelToken(function(cancel) {
store.commit("pushToken", { cancelToken: cancel });
});
return config;
},
error => Promise.error(error)
);
2.vuex的store里
export default {
state: {
cancelTokenArr: [],
},
mutations: {
pushToken(state,payload){
state.cancelTokenArr.push(payload.cancelToken)
},
clearToken({cancelTokenArr}){
cancelTokenArr.forEach(item => {
item('路由跳转取消请求')
});
cancelTokenArr = []
}
}
}
3.router
router.beforeEach((to, from, next) => {
store.commit("clearToken");
})
实现原理?
1.Promise
2.new XMLHttpRequest().abort()
二、vue项目如何实现权限管理?控制到按钮级别的权限怎么做?
权限管理就是对特定资源的访问许可。
在我看来,权限管理分为4个部分,由大到小:路由,视图(菜单,按钮),接口。
1.菜单权限
登录后,后端返回可访问的一级路由,就是可访问的菜单数据,并动态加入routes路由数组里,并在左侧菜单栏显示,一级路由后端接口返回,并动态加入routes路由数组里
2.路由权限
路由一般在router.beforeEach的全局守卫里进行拦截,
第一步:是否登录?有无登录返回的token
第二步:对一级路由及二级路由进行拦截,一级路由就是后端接口返回有权访问的菜单路由,二级路由就是所有有权访问的菜单路由包括的子页面
二级路由本地维护相关数据
3.按钮权限
通过自定义指令进行按钮权限的判断
由用户权限(admin,supper,normal)来控制
所有需要控制的按钮上增加等级属性permissionLevel,(admin,supper,normal),一一对应,
再const = permission = Vue.directive('permission',{
bind: function(el,binding,vnode){
//获取用户权限,与el上的属性进行比对
el.parentNode.removerChild(el)
}
})
4.请求权限
最后一道防线,发送请求前拦截
三,常见移动端兼容性问题
1.IOS移动端click事件300ms的延迟相应
移动设备上的web网页是有300ms延迟的,往往会造成按钮点击延迟甚至是点击失效。
这是由于区分单机事件和双击屏幕缩放的历史原因造成的。
解决方式:
- fastclick可以解决在手机上点击事件的300ms延迟
- zepto的touch模块,tap事件也是为了解决在click的延迟问题
- 触摸屏的相应顺序为touchstart-->touchmove-->touchend-->click,也可以通过绑定ontouchstart事件,加快事件的响应,解决300ms的延迟问题
2.一些情况下对非可点击元素(label,span)监听click事件,iso下不会触发,css增加cursor:pointer就搞定了。
3.h5底部输入框被键盘遮挡问题
h5页面有个很蛋疼的问题就是,当输入框在最底部,点击软键盘后输入框会被遮挡。可采用如下方式解决
var oHeight = $(document).height(); //浏览器当前的高度
$(window).resize(function(){
if($(document).height() < oHeight){
$("#footer").css("position","static");
}else{
$("#footer").css("position","absolute");
}
});
- ios下fixed元素容易定位出错,软键盘弹出时,影响fixed元素定位
- android下fixed表现要比iOS更好,软键盘弹出时,不会影响fixed元素定位
- ios4下不支持position:fixed
- 解决方案: 可用iScroll插件解决这个问题
4.消除 transition 闪屏
-webkit-transform-style: preserve-3d; /*设置内嵌的元素在 3D 空间如何呈现:保留 3D*/
-webkit-backface-visibility: hidden; /*(设置进行转换的元素的背面在面对用户时是否可见:隐藏
5.CSS动画页面闪白,动画卡顿
解决方法:
1.尽可能地使用合成属性transform和opacity来设计CSS3动画,不使用position的left和top来定位
2.开启硬件加速
四,读过elemntui的源码吗?
入口文件elemntui/src/index.js
import Button from '../packages/button/index.js';
const components = [
Button
]
const install = function(Vue, opts = {}) {
//全局注册组件
components.forEach(component => {
Vue.component(component.name, component);
});
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;
};
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export default {
version: '2.15.6',
install,
Button//可以通过import {Button} 按需引入
}
接下来看个例子dialog组件
首先关注下组件官方提供的参数,Slot,方法
Attributes
visible,title...
modal-append-to-body:遮罩层是否插入至 body 元素上,若为 false,则遮罩层会插入至 Dialog 的父元素上
append-to-body:Dialog 自身是否插入至 body 元素上。嵌套的 Dialog 必须指定该属性并赋值为 true
Slot
—Dialog 的内容titleDialog 标题区的内容footerDialog 按钮操作区的内容
Events
openDialog 打开的回调openedDialog 打开动画结束时的回调closeDialog 关闭的回调closedDialog 关闭动画结束时的回调
ok,然后我们看源码组件
<template>
<!-- transition 三种方法,@before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter"
控制动画结束时的回调。。。-->
<transition
name="dialog-fade"
@after-enter="afterEnter"
@after-leave="afterLeave">
<div
v-show="visible"
class="el-dialog__wrapper"
@click.self="handleWrapperClick">
<div
role="dialog"
:key="key"
aria-modal="true"
:aria-label="title || 'dialog'"
:class="['el-dialog', { 'is-fullscreen': fullscreen, 'el-dialog--center': center }, customClass]"
ref="dialog"
:style="style">
<div class="el-dialog__header">
<slot name="title">
<span class="el-dialog__title">{{ title }}</span>
</slot>
<button
type="button"
class="el-dialog__headerbtn"
aria-label="Close"
v-if="showClose"
@click="handleClose">
<i class="el-dialog__close el-icon el-icon-close"></i>
</button>
</div>
<div class="el-dialog__body" v-if="rendered"><slot></slot></div>
<div class="el-dialog__footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</transition>
</template>
<script>
import Popup from 'element-ui/src/utils/popup';
import Migrating from 'element-ui/src/mixins/migrating';
import emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElDialog',
mixins: [Popup, emitter, Migrating],//公共方法
props: {
title: {
type: String,
default: ''
},
modalAppendToBody: {
type: Boolean,
default: true
},
appendToBody: {
type: Boolean,
default: false
}
...
},
data() {
return {
closed: false,
key: 0
};
},
watch: {
visible(val) {
if (val) {
this.closed = false;
this.$emit('open');
this.$el.addEventListener('scroll', this.updatePopper);
this.$nextTick(() => {
this.$refs.dialog.scrollTop = 0;
});
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} else {
this.$el.removeEventListener('scroll', this.updatePopper);
if (!this.closed) this.$emit('close');
if (this.destroyOnClose) {
this.$nextTick(() => {
this.key++;
});
}
}
}
},
computed: {
style() {
let style = {};
if (!this.fullscreen) {
style.marginTop = this.top;
if (this.width) {
style.width = this.width;
}
}
return style;
}
},
methods: {
getMigratingConfig() {
return {
props: {
'size': 'size is removed.'
}
};
},
handleWrapperClick() {
if (!this.closeOnClickModal) return;
this.handleClose();
},
handleClose() {
if (typeof this.beforeClose === 'function') {
this.beforeClose(this.hide);
} else {
this.hide();
}
},
hide(cancel) {
if (cancel !== false) {
this.$emit('update:visible', false);
this.$emit('close');
this.closed = true;
}
},
updatePopper() {
this.broadcast('ElSelectDropdown', 'updatePopper');
this.broadcast('ElDropdownMenu', 'updatePopper');
},
afterEnter() {
this.$emit('opened');
},
afterLeave() {
this.$emit('closed');
}
},
mounted() {
if (this.visible) {
this.rendered = true;
this.open();
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
}
},
destroyed() {
// if appendToBody is true, remove DOM node after destroy
if (this.appendToBody && this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
}
};
</script>
分析源码我们可以得出,常用的一些规则
1.transition组件与,watch监听搭配,@after-enter="afterEnter"与this.$emit('open')控制动画回调;
2.外面v-model的值,里面v-show="visible"与this.$emit('update:visible', false)实现双向数据更新;
3.如果有appendToBody,在mount里和visible监听里新增document.body.appendChild(this.$el),再在destroyed移除当前组件this.$el.parentNode.removeChild(this.$el);
4.动态样式用:class=[]和computed;
5.公共方法用mixins: [Popup, emitter, Migrating]混入;也有provide和inject依赖注入;
6.插槽<slot></slot>很常见,用name区分;
五,你是做信息安全行业的,那么项目上有哪些安全相关的工作?
先来了解些安全知识:
CSRF跨站请求伪造,XSS跨站脚本攻击,点击劫持等
回答示例
1.我们零信任系统分为权限中心,审批中心等五大中心,所有中心都依赖于一个门户应用统一登录,然后在门户里通过单点登录的形式跳转各中心进行业务操作;
2.门户应用登录时采用用户名密码+图片验证码的复杂操作,并且登录后返回加密token,每次请求时header里携带,这样可以防御跨站请求伪造;
3.另外门户登录还集成了指纹,声纹,UKey等多因子认证方式,更安全;
4.还有像项目里所有输入框都要有字符长度限制,对身份证,电话等做正则校验防止恶意攻击;
5.在请求头中加入 X-FRAME-OPTIONS
属性,此属性控制页面是否可被嵌入 iframe 中,设置为DENY:不能被所有网站嵌套或加载,可以防御点击劫持;
一般到这里就可以了,如果还问?
1.使用https,数字证书加密;
2.内容安全策略(Content Security Policy)简称 CSP,CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。
通过 HTTP 头配置Content-Security-Policy
,
Content-Security-Policy: script-src 'self' https://apis.google.com
通过页面 <meta>
标签配置:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://apis.google.com">
3.禁止被使用的 iframe 对当前网站某些操作
sandbox是html5的新属性,主要是提高iframe安全系数。
我们可以设置 sandbox 属性:
- allow-same-origin:允许被视为同源,即可操作父级DOM或cookie等
- allow-top-navigation:允许当前iframe的引用网页通过url跳转链接或加载
- allow-forms:允许表单提交
- allow-scripts:允许执行脚本文件
- allow-popups:允许浏览器打开新窗口进行跳转
- '':设置为空时上面所有允许全部禁止
六,10个ajax同时发出请求 并且全部返回展示结果 并且最多允许出现三次失败 谈谈设计思路?
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
接口是否失败不是由前端能够控制的,前端能做的就是拦截发送与返回,进而检测是否需要重新发送,还可以对同一时间的请求数量进行限制
七,封装文件下载的组件?大文件上传?
前端文件下载的几种方式
1.window.open(URL)
2.使用a标签download属性
直接下载仅使用的浏览器无法识别的文件。如果是浏览器支持的文件格式(如:html、jpg、png)等。则不会触发文件下载,而是被浏览器直接触发解析展示。
针对这种情况,我们可以使用a标签的download属性,可以设置文件名。
<a href="/images/download.jpg" download="myFileName">
开发中,我们遇到的还有一部分场景是js直接触发下载,也是相同的做法,我们可以手动写一个a标签。appen到body里边,触发下载之后,将其remove
const download = (filename, link) => {
let DownloadLink = document.createElement('a');
DownloadLink.style = 'display: none'; // 创建一个隐藏的a标签
DownloadLink.download = filename;
DownloadLink.href = link;
document.body.appendChild(DownloadLink);
DownloadLink.click(); // 触发a标签的click事件
document.body.removeChild(DownloadLink);
}
3.get请求下载
axios({
method:'get',
url: '/download/file.doc'
responseType: 'blob',
headers: {
Authorization: '123456'
}
}).then(res => {
let fileUrl = window.URL.createObjectURL(res.data)
iTools.download('filename',fileUrl) // 方法二使用到的a标签download方式。
window.URL.revokeObjectURL(fileUrl)
})
大文件上传
本人没有做过大文件上传,但是我的思路是,第一,以大化小拆分上传,第二,保留上传时的状态,实现断点续传,第三,支持显示上传进度和暂停上传
具体做法:
编码方式上传中,在前端我们只要先获取文件的二进制内容,然后对其内容进行拆分,最后将每个切片上传到服务端即可。
目前社区已经存在一些成熟的大文件上传解决方案,如WebUploader,它是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件。在现代的浏览器里面能充分发挥HTML5的优势,同时又不摒弃主流IE浏览器,沿用原来的FLASH运行时,兼容IE6+,iOS 6+, android 4+。
插件的底层源码是用JQuery封装的,所以需要安装JQuery
如果是vue项目,npm安装到环境中:
npm install JQuery
npm install WebUploader然后在vue页面中引入:
import $ from Jquery
import webUploader from WebUploader