一、如何中断已经发出去的请求?

以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