• 面试题总结


    文章目录

    事件传参

    • 没有传递参数,事件函数的默认第一个参数是事件对象
    • 如果传递了参数,事件函数就没有了默认参数,全部变为对应位置的实参的形参,就没有了事件对象
    • 既有自己传的的参数,也有事件对象,通过$event 传递事件对象,在事件函数内部通过对应位置的形参来接收事件对象,传递$event 没有强制性的位置,但是建议放在最后

    computed 和 watch 的区别和运用的场景?

    computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;

    watch: 更多的是「观察」的作用,可以监听的数据有(props、data、computed、$route) 类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

    运用场景:

    • 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
    • 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

    vue 组件中的 data 为什么是一个函数,返回一个对象?

    如果不是一个函数返回一个新的对象,组件如果多次使用,实际公用的是同一个数据

    但是如果是通过函数 返回一个新的对象,这样的话,每个组件的使用数据是独立的

    组件

    如何创建一个全局组件

    通过 Vue.component 来创建一个全局组件,第一个参数是组件名字,第二个参数是组件的配置对象,可以通过 template 配置组件的结构,data 定义数据等等

    如何创建一个局部组件

    在组件内部通过 components 来创建一个局部组件

    全局组件和局部组件的区别

    局部组件:只能在当前的父组件中使用

    全局组件: 在任意地方使用

    组件传值

    Props

    • 父亲怎么传:通过属性绑定形式传
    • 儿子怎么接收:通过 props 来接收

    Emit

    • 子怎么传:通过 this.$emit 触发一个自定义事件,并且发送一个值
    • 父怎么接收:通过定义自定义事件的事件函数的形参来接收

    ref

    放到dom节点上 >> 获取原生dom
    组件身上 >> 获取组件实例 >> 可以获取组件内部所有的方法和数据

    v-model

    我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

    • text 和 textarea 元素使用 value 属性和 input 事件;
    • checkbox 和 radio 使用 checked 属性和 change 事件;
    • select 字段将 value 作为 prop 并将 change 作为事件。

    以 input 表单元素为例:

    <input v-model='something'>
        
    相当于
    
    <input v-bind:value="something" v-on:input="something = $event.target.value">
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:

    父组件:
    <ModelChild v-model="message"></ModelChild>
    
    子组件:
    <div>{{value}}</div>
    
    props:{
        value: String
    },
    methods: {
      test1(){
         this.$emit('input', '小红')
      },
    },
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    sync
    
    • 1
    $parent
    
    • 1
    $children
    
    • 1

    兄弟组件传值

    eventBus
    
    • 1

    跨组件传值

    inject  provide
    
    • 1
    vuex
    
    • 1

    组件插槽

    • 默认插槽:

      • 在组件标签中间可以传递一些子节点
      • 组件内部利用 slot 标签进行接收
    • 具名插槽

      • 在组件标签中间通过定义 slot 的名字传递子节点

        <my-banner>
          <div slot="header">
            头部
          div>
          <div slot="footer">
            底部
          div>
        my-banner>   
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
      • 组件内部利用 slot 的 name 进行对应接收

        <template id="banner">
          <div>
            <slot name="header">slot>
            <slot name="footer">slot>
          div>
        template>
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
    • 作用域插槽

      • 在组件内部定义数据,将数据传递给插槽的结构

      • 通过给 slot 动态绑定属性

        <template id="my-li">
            <ul>
              <li v-for="item in arr">
                <slot :row="item"></slot>
              </li>
            </ul>
          </template>
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 插槽内部:通过 slot-scope=“scope”来接收

        <my-li>
          <template slot-scope="scope">
            <p>{{scope.row}}p>
          template>
        my-li>
        <my-li>
          <template slot-scope="scope">
            <a href="04-侦听器.html">{{scope.row}}a>
          template>
        my-li>
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10

    路由钩子

    全局钩子:都会对所有的路由进行拦截

    beforeEach:进入之前

    afterEach:已经进入了

    路由独享钩子:可以针对某一个路由进行拦截,这个需要写在路由规则里

     {
        path: '/',
        name: 'home',
        beforeEnter: (to,from,next)=>{
          console.log('即将进入home')
        },
        component: Home
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    组件内的守卫:

    针对组件进行拦截

    beforeRouteEnter (to, from, next) {
        // 在渲染该组件的对应路由被 confirm 前调用
        // 不!能!获取组件实例 `this`
        // 因为当守卫执行前,组件实例还没被创建
        next()
      },
      beforeRouteUpdate (to, from, next) {
        // 在当前路由改变,但是该组件被复用时调用
        // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
        // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        // 可以访问组件实例 `this`
      },
      beforeRouteLeave (to, from, next) {
        // 导航离开该组件的对应路由时调用
        // 可以访问组件实例 `this`
        console.log('即将离开about')
        if(confirm('当前表单没有提交?确定要离开首页?')){
          next()
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    虚拟 dom

    本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容

    作用:描述了真实dom结构,核心属性
    真实的dom节点有很多属性的,如果直接操作真实的dom,消耗性能
    vue里面template写的结构转换render函数,render函数执行会返回虚拟dom结构 >> 渲染出真实dom

    优势:

    1、数据更新的时候,虚拟dom对比(旧的虚拟dom结构和新的虚拟dom结构进行对比),对比出差异以后,更新到真实的dom里面(最小化操作dom)

    2、同级比较,如果标签名不一致或者key不一致直接替换,不会进行深层次对比

    3、次序深度优先
    跨平台

    浏览器,小程序 , 支付宝小程序,抖音小程序,打包安卓,ios应用 ,electron桌面应用

    vue/react >> 虚拟dom 渲染的时候根据不同的平台去进行渲染

    渲染浏览器环境的方法

    渲染小程序的方法

    dom diff的性能好

    跨平台

    Diff 算法

    虚拟 dom 高效更新执行过程

    • 初次渲染时,会根据 model 数据创建一个虚拟 DOM 对象(树)
    • 根据虚拟 DOM 生成真正的 DOM,渲染到页面
    • 当数据变化后,会重新根据新的数据,创建新的虚拟 DOM 对象(树)
    • 与上一次得到的虚拟 DOM 对象,使用 Diff 算法比对(找不同),得到需要更新的内容
    • 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面
    • 什么是虚拟 dom:用 js 对象来表示页面上 dom 元素的的样式体现
    • 虚拟 dom 的作用:高效更新页面,还有就是让 react 脱离了浏览器的概念
    • 怎么来高效更新页面的:就是在第一次渲染页面的时候生成一份初始的虚拟 dom 树,然后当数据改变之后,再生成一份虚拟 dom 树,然后根据新旧 dom 树进行对比,对比完成之后,把有区别的进行更新
    • diff 算法的作用就是:新旧虚拟 dom 树在对比的时候就是通过 diff 算法来对比的
    • diff 算法又是怎么去对比的:tree diff、component diff、element diff

    vue 中 route 和 router 的区别

    • route 是当前路由信息,可以获取到当前路由地址参数等等
    • router 是全局路由(VueRouter)实例对象,可以通过 router 进行路由的跳转后退等等

    Vue.use 的原理是什么?

    它是用来注册注册插件的,他只能支持对象跟函数,如果插件是一个对象的话,必须提供一个install方法,如果插件是一个函数它会被作为install方法,
    install方法调用时会将vue作为参数传入。

    this.$nextTick()的作用

    这个函数是可以等 dom 重新更新完成会调用

    数据渲染完成,页面完成更新即调用 this.$nextTik

    当修改了数据,dom 是异步的,所以,如果更改了数据,在修改数据下面重新操作 dom 会出问题,需要保证 dom 也更新完成才能操作。

    发布订阅模式

    概念

    发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

    多方订阅 》一方发布,订阅方会收到通知

    • 常见的发布订阅

      vue 中的$on - $emit

      redux 中的 subscribe

    实现发布订阅

    定义 on 和 emit

    class Events {
      // 订阅事件
      $on = function() {}
    
      // 发布
      $emit = function() {}
    }
    
    const events = new Events()
    
    events.$on('test', function() {})
    
    events.$emit('test', 'hello')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    实现 on 添加订阅

    class Events {
      clientlist = {}
      // 订阅事件
      $on = function(key, fn) {
        if (!this.clientlist[key]) {
          this.clientlist[key] = []
        }
        this.clientlist[key].push(fn)
      }
    
      // 发布
      $emit = function() {}
    }
    
    const events = new Events()
    
    events.$on('test', function(data) {
      console.log(data, '----')
    })
    events.$on('test', function(data) {
      console.log(data, '----')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    实现 emit 发布

    // 发布
    $emit = function(key, val) {
      Object.keys(this.clientlist).forEach(item => {
        if (!this.clientlist[key]) {
          return false
        }
        this.clientlist[key].forEach(fn => fn(val))
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    什么是观察者模式?

    观察者模式即一个对象被多个对象所依赖,当被依赖的对象发生更新时,会自动通知所有依赖的对象。

    vue数据响应式原理

    Object.defineProperty()方法不是实现数据响应式的,

    Object.defineProperty()作用是对一个对象进行新增,对这个属性进行配置,这才是他的作用,只不过尤雨溪很好的利用了这个特性实现了数据响应式

    Object.defineProperty()缺陷:它只能监视对象中的某一个属性也就代表不能劫持整个对象,如果想要劫持这个对象

    说一说数据响应式中哪些情况数据不响应

    有两种情况:

    一个是数组下标发生变化,一个是对象新增,首先区分一下情况也不是说不响应,也不能说响应,如果只是单纯的根据数组下标来修改数据,单纯的给对象新增数据此时是不响应的,对象不响应原因是因为他的底层Object.defineProperty()有一个已知缺陷,他只能对已有的数据劫持所以对对象新增的数据是没有劫持的,既然没有办法劫持到,它也就没有办法去更新组件,所以它不响应,但是数组不一样,数组是因为Object.defineProperty()能够劫持到这个数据但是vue中不响应是因为-vue官方团队对这个功能抛弃了,因为数组的长度对它劫持和用户体验不成正比的,我也去翻了GitHub,尤雨溪对一个用户确实是这么回答的,但是还有另一种情况就是当我们的数据发生变化是通过下标或者是通过对象新增的一些数据,我下面又操作了其他数据此时是可以达到响应式的,原因就是真正的响应式是因为底层的源码发生变化,组件就会跟着更新,所以说下面又去写了之后因为下面的数据发生变化导致组件进入更新阶段,组件进入更新阶段页面进入更新,此时页面更新它就取到了数组下标最新的值所以也就更新了,以上就是我对数据响应式不相应的两种情况

    闭包及原理

    概念:

    闭包让你可以在一个内层函数中访问到外层函数作用域

    原理:

    就是作用域链,作用域链可以一层一层的往上面去找,比方说向最里层的函数,当前访问的函数当前没有就向外找外面没有再向外找外面没有就找到全局,这个链式结构也就叫做作用域链

    computed和watch的区别是什么?

    computed

    1. 它是计算属性。主要用于值的计算并一般会返回一个值。所以它更多⽤于计算值的场景
    2. 它具有缓存性。当访问它来获取值时,它的 getter 函数所计算出来的值会进行缓存,只有当它依赖的属性值发生了改变,那下⼀次再访问时才会重新调⽤ getter 函数来计算
    3. 它适⽤于计算⽐较消耗性能的计算场景

    watch

    1. 它更多的是起到 “观察” 的作⽤,类似于对数据进行变化的监听并执行回调。主要⽤于观察 props$emit 或本组件的值,当这些值发生变化时,执⾏回调
    2. 它不具有缓存性。当⻚⾯重新渲染时,即使值没发生变化也会执⾏

    建议

    1. 当目的是进⾏数值计算,且依赖于其他数据,那么推荐使用 computed
    2. 当需要在某个数据发生变化的同时做⼀些稍复杂的逻辑操作,那么推荐使⽤ watch

    前端登录流程

    一、在登陆页点击登陆的时候会进行from表单的校验,前端校验通过后会带着用户名和密码去调用后端的登录接口。

    二、后端收到请求后会验证用户名和密码,如果验证失败会返回相关的错误信息,前端提示错误信息,如果验证成功就会给前端返回一个token值

    三、前端拿到token之后将这个token存储到vuex和LocalStorage中并跳转页面及登录成功

    四、前端每一次跳转到需要具备登录状态页面的时候都需要判断当前的token是否存在,如果不存在就跳转到登录页,存在则正常跳转,通常我们会把这个封装在路由守卫中,最后在向后端发送其他请求的时候,我们一般需要在请求头中带上这个token值,在项目中我们通常是把它封装在一个请求拦截器中,后端判断请求头中有无该token,有则验证该token,验证成功就会正常的返回数据,验证失败,比如过期就会返回相应的错误码,前端拿到相关错误信息清除token并且再回退到登录页。

    为什么token要同时存在vuex和localStorage中

    ​ vuex存储数据的特点:数据统一全局管理,一旦数据在某组件更新,其他所有组件数据都会更新,也就是说它是响应式的,但是如果数据只存在vuex中,刷新页面vuex里的数据会重新初始化,导致数据丢失,恢复到原来的状态。
    ​ localStorage(本地存储)存储数据的特点:永久性存储,但不是响应式的,当某个组件数据修改时,其他组件无法同步更新。
    ​ 另外,vuex是存储在内存中,localStorage本地存储是存储到磁盘里,从内存中读取数据,速度是远高于磁盘
    的,所以把数据存在vuex中可以提高获取token速度,提高性能。

    结论:所以我们在项目中通常是结合这两者使用,拿到token后,把token存储到localStorage和vuex中, vuex保证数据在各组件间同步更新,如果刷新页面数据丢失,我们可以从localStorage获取,通过结合vuex和localStorage本地存储,实现数据的持久化。

    虚拟列表和懒加载的区别

    ​ 虚拟列表:是任何情况下只对「可视区域」内的数据进行渲染,不会渲染多余元素,达到较高的渲染性能。
    ​ 懒加载:是当数据进入可视区域后做加载数据的操作,当数据越来越多的时候,渲染的元素也会越来越多。它在一定程度上没有解决长列表的DOM操作带来的性能问题。
    ​ 举个例子:
    ​ 比如平时我们看b站首页的时候,如果用的是虚拟列表这种方式,当我们往下滑的时候,滑出可视区域外的元素是不做渲染的,因为它只渲染可视区域内的元素。如果是懒加载这种方式,那么当我们往下滑的时候,会去加载新的数据,但是这时候滑出可视区域外的数据还是做了节点渲染。
    ​ 当一直往下滑,数据特别特别特别多的时候,就很可能会出现一定的卡顿,所以刚刚说在一定程度上懒加载并没有解决长列表的DOM操作带来的性能问题。这种情况用虚拟列表就会好很多,因为不论你划到多下面,它都只渲染可视区域内的元素。

    说说nextTick吧

    ​ nextTick:在下次 DOM 更新循环结束之后执行延迟回调。
    这句话扩展开来说,就是由于Vue中DOM更新是「异步执行」的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。经常我们会在还未更新的时候就使用了某个元素,这样是拿不到变更后的dom的,所以为了确保能够得到更新后的DOM,
    所以设置了nextTick()方法。在修改数据之后立即使用这个方法,获取更新后的DOM.
    【简单概括】
    ​ vue中的nextTick主要用于处理数据动态变化后,DOM还未及时更新的问题,用nextTick可以获取数据更新后最新dom的变化。
    【在项目中什么时候用呢?】
    ​ 比如,当我们需要在生命周期的created()函数进行一些DOM操作的时候一定要把相关代码放Vue.nextTick()
    的回调函数中。原因:在created钩子函数中, DOM 还未进行任何渲染,此时进行DOM 操作是没用的,所以如果要在这里操作dom,一定要将相关 js 代码放进
    Vue.nextTick 回调函数中。
    又或者,在数据变化后要执行某个动作,而这个动作需要使用随数据变化而改变的DOM结构的时候,也需要把相关逻辑写入Vue.nextTick ()回调函数中。

    data和props的区别

    ​ 在实际vue项目中,我们经常会在子组件里看到data和props属性,这两者里面的数据使用方式基本是一致的,但是还是有一定的区别。
    1、data不需要父组件传值,自身进行维护;而props需要父组件传值。
    2、data上的数据都是可读可写的;而props的数据都是父组件传递过来的,而且由于它是单向数据流,因此数据只可读,无法重新修改。
    这时候可能有小伙伴说了,我就改过啊,
    控制台也没飘红呀。


    ​ 控制台是否飘红也是分情况的,如果传递的props值是基本类型(像Number,Boolean, String)子组件直接修改,控制台肯定会报错的,但是如果传递的是引用类型,像Object,数组结构,子组件修改里面的属性值或者某一数组元素,控制台是不会报错的。因为对于引用类型改的只是值,而不是引用地址。
    ​ 不过,不管传递什么形式的数据,我们都是不建议在子组件中直接修改Props的值的,因为这样会破坏单一数据流,可能会导致数据的变化无法追踪。
    ​ 问:那在子组件中修改props的正确操作又是什么呢?
    ​ 答:如果子组件只是想修改后自己使用,不想影响到父组件的数据,那么我们可以在子组件中的data里定义一个变量,让这个变量的初始值等于父组件传过来的props值,相当于copy一份这个props值,后面需要修改的话就改自己data里的这个值。这样就不会影响到父组件了。如果处理后想同步修改父组件的值,那么可以通过this.$emit事件触发父组件去修改。

    v-for和v-if的优先级

    ​ 在源码里,v-for优先于v-if被解析,也就是说v-for优先级更高,所以同时使用这两者的话,每次v-for循环时,都会执行一次v-if,比如循环一个有1000个元素的数组,那v-if也会被执行1000次,这样会十分消耗性能,降低代码质量。
    在项目中应该怎么做?这里要根据判断条件是否和和循环中的内容有关系,分两种情况:
    ​ 1、在v-if不依赖v-for中某个值的前提下,什么叫不依赖,就是这个判断条件本身和循环出来的项没什么关系,可能只是用来判断这整个循环列表是否需要显示,这种情况我们可以在循环外层加一个template标签,在template标签里做v-if的判断,这样可以让v-if判断优先。
    ​ 2、当v-if的判断条件依赖于v-for的内容时,也就是说,我们需要对每个循环项做判断,符合条件才展示,针对这种情况,我们可以在computed计算属性里先做好判断,过滤出符合条件的元素重新组成新的数组,在dom里循环这个新的数组,这时就不需要加v-if的判断了,因为我们已经在计算属性里把符合条件的项都取出来了。我们要知道,在computed里过滤的成本远比用v-if的成本低得多

    模块化和组件化含义及区别

    ​ 1、模块化是从代码逻辑角度进行划分的,保证每个模块的职能单一;比如登录页的登录功能,就是一个模块,注册功能又算是一个模块。
    ​ 2、组件化,是从UI界面的角度划分的;页面上每个独立的区域,都可以视为一个组件,前端组件化开发,便于UI组件的复用,减少编码量。

    ​ 区别:划分角度不同,组件化是UI界面角度,模块化是代码逻辑角度。

    白屏时间和首屏时间区别

    ​ 1、白屏时间(First Paint):是指用户输入内容回车,到浏览器开始出现第一个字符,即开始显示内容的时间。所以,白屏时间=页面开始展示的时间点-开始请求的时间点。
    ​ 2、首屏时间(First Contentful Paint):是指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。所以,首屏时间首屏内容渲染结束时间点-开始请求的时间点。
    ​ 通过刚刚的两个概念,我们知道,首屏时间一定比白屏时间长,因为首屏时间的另一种计算是:首屏时间=白屏时间+首屏开始渲染至渲染结束的时间。

    Vue双向绑定原理?

    在 Vue 2.x 中,利⽤的是 Object.defineProperty 去劫持对象的访问器(Getter、Setter),当对象属性值发⽣变化时可获取变化,然后根据变化来作后续响应;

    在 Vue 3.0 中,则是通过 Proxy 代理对象进⾏类似的操作。

    什么情况下需要使用mixins

    当多个组件执行的方法和所需要的数据类似时,我们可以提取出公共部分写入混入对象中,哪个组件需要用直接引入即可。

    vue 中 key 的作用

    vue中我们可能在两种情况下使用key,第一种情况下就是在v-if中,第二种情况下就是在v-for中使用key。

    在 v-if 中使用 key

    在vue中如果使用v-if进行切换时,此时Vue为了更加高效的渲染,会进行前后比较,如果切换前后都存在的元素,则直接复用。如果我们在模板中切换前后都存在input框,此时我们在input框中写入一些数据,并且进行页面切换,则原有的数据就会保存。

    这时我们就可以使用key,给每一个input框,添加一个唯一的标识key,来表示元素的唯一性。

    在 v-for 中使用 key

    通过key值的作用,给每个节点做一个唯一的标识,diff算法就可以正确识别此节点,找到正确的位置插入新的节点,减少渲染节点。

    一、key是什么

    一句话来讲

    key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点

    关于项目打包你都做了什么? 你的项目优化具体做了哪些? 你的项目如何打包的?

    • 移除 console,借助 babel 插件 babel-plugin-transform-remove-console

    • 判断环境,只在发布阶段使用上面的插件, 通过 process.env.NODE_ENV ‘production’ || “development”

    • 因为开发阶段和发布阶段入口代码有区别,通过 chainWebpack 配置自定义的打包入口,

    • 发布阶段为了减小打包体积, 忽略 js 打包,配置 externals,关于 css 样式,在发布的入口文件中注释掉即可, 加载 cdn 资源文件,加载 cdn 资源文件的优势,减小自己服务器的压力,同时 cdn 资源会就近原则访问

    • 只在发布阶段使用 cdn 资源,通过给 htmlwebpackPlugin 添加标识,标识为不同的值,此时就可以在 html 中判断 htmlwebpackPlugin 标识为开发阶段还是发布阶段从而按需加载 cdn 资源

    • 路由懒加载,路由按需加载, 匹配到哪个路由规则,加载对应的资源文件

    Vue 项目进行 SEO 优化

    Vue SPA单页面应用对SEO不太友好,当然也有相应的解决方案,下面列出几种SEO方案

    1. SSR服务器渲染

      服务端渲染, 在服务端html页面节点, 已经解析创建完了, 浏览器直接拿到的是解析完成的页面解构

      关于服务器渲染:Vue官网介绍open in new window ,对Vue版本有要求,对服务器也有一定要求,需要支持nodejs环境。

      优势: 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面

      缺点: 服务器nodejs环境的要求, 且对原代码的改造成本高! nuxt.js (坑比较多, 做好踩坑的准备)

    2. 静态化 (博客, 介绍性官网)

      Nuxt.js 可以进行 generate 静态化打包, 缺点: 动态路由会被忽略。 /users/:id

      优势:

      • 编译打包时, 就会帮你处理, 纯静态文件,访问速度超快;
      • 对比SSR,不涉及到服务器负载方面问题;
      • 静态网页不宜遭到黑客攻击,安全性更高。

      不足:

      • 如果动态路由参数多的话不适用。
    3. 预渲染 prerender-spa-plugin (插件)

      如果你只是对少数页面需要做SEO处理(例如 / 首页, /about 关于等页面)

      预渲染是一个非常好的方式, 预渲染会在构建时, 简单的针对特定路由, 生成静态 HTML 文件 (打包时可以帮你解析静态化)

      优势: 设置预渲染简单, 对代码的改动小

      缺点: 只适合于做少数页面进行SEO的情况, 如果页面几百上千, 就不推荐了 (会打包很慢)

    小结:

    • 如果构建大型网站,如商城类 => SSR服务器渲染
    • 如果只是正常公司官网, 博客网站等 => 预渲染/静态化/Phantomjs 都比较方便

    Vue 项目权限处理

    现在权限相关管理系统用的框架都是element提供的vue-element-adminopen in new window模板框架比较常见。

    权限控制常见分为三大块

    • 菜单权限控制
    • 按钮权限控制
    • 请求url权限控制。

    权限管理在后端中主要体现在对接口访问权限的控制,在前端中主要体现在对菜单访问权限的控制。

    1. 按钮权限控制比较容易,主要采取的方式是从后端返回按钮的权限标识,然后在前端进行显隐操作 v-if / disabled。

    2. url权限控制,主要是后端代码来控制,前端只需要规范好格式即可。

    3. 剩下的菜单权限控制,是相对复杂一些的

      (1) 需要在路由设计时, 就拆分成静态路由和动态路由

      静态路由: 所有用户都能访问到的路由, 不会动态变化的 (登录页, 首页, 404, …)

      动态路由: 动态控制的路由, 只有用户有这个权限, 才将这个路由添加给你 (审批页, 社保页, 权限管理页…)

      (2) 用户登录进入首页时, 需要立刻发送请求, 获取个人信息 (包含权限的标识)

      在这里插入图片描述

      (3) 利用权限信息的标识, 筛选出合适的动态路由, 通过路由的 addRoutes 方法, 动态添加路由即可!

      (4) router.options.routes (拿的是默认配置的项, 拿不到动态新增的) 不是响应式的!

      为了能正确的显示菜单, 为了能够将来正确的获取到用户路由, 我们需要用vuex管理routes路由数组

      (5) 利用vuex中的 routes, 动态渲染菜单

    Vue项目中有封装过axios吗?主要是封装哪方面的?

    为什么要封装

    axios 的 API 很友好,你完全可以很轻松地在项目中直接使用。

    不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍

    这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用

    举个例子:

    axios('http://localhost:3000/data', {
      // 配置代码
      method: 'GET',
      timeout: 1000,
      withCredentials: true,
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'xxx',
      },
      transformRequest: [function (data, headers) {
        return data;
      }],
      // 其他请求配置...
    })
    .then((data) => {
      // todo: 真正业务逻辑代码
      console.log(data);
    }, (err) => {
      // 错误处理代码  
      if (err.response.status === 401) {
      // handle authorization error
      }
      if (err.response.status === 403) {
      // handle server forbidden error
      }
      // 其他错误处理.....
      console.log(err);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了

    这时候我们就需要对axios进行二次封装,让使用更为便利

    如何封装

    封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间…

    设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分

    请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务)

    状态码: 根据接口返回的不同status , 来执行不同的业务,这块需要和后端约定好

    请求方法:根据getpost等方法进行一个再次封装,使用起来更为方便

    请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问

    响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务

    小结

    • 封装是编程中很有意义的手段,简单的axios封装,就可以让我们可以领略到它的魅力
    • 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案

    如何处理 打包出来的项目(首屏)加载过慢的问题

    SPA应用: 单页应用程序, 所有的功能, 都在一个页面中, 如果第一次将所有的路由资源, 组件都加载了, 就会很慢!

    加载过慢 => 一次性加载了过多的资源, 一次性加载了过大的资源

    • 加载过多 => 路由懒加载, 访问到路由, 再加载该路由相关的组件内容
    • 加载过大 => 图片压缩, 文件压缩合并处理, 开启gzip压缩等

    比如:

    1. 配置异步组件, 路由懒加载

      const login = () => import('../pages/login.vue')
      
      • 1
    2. 图片压缩: 使用 webp 格式的图片, 提升首页加载的速度

    3. CDN加速: 配置CDN加速, 加快资源的加载效率 (花钱)

    4. 开启 gzip 压缩 (一般默认服务器开启的, 如果没开, 确实可能会很慢, 可以让后台开一下)

    在这里插入图片描述

    说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢?

    一、为什么要划分

    使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高

    在划分项目结构的时候,需要遵循一些基本的原则:

    • 文件夹和文件夹内部文件的语义一致性
    • 单一入口/出口
    • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
    • 公共的文件应该以绝对路径的方式从根目录引用
    • /src 外的文件不应该被引入

    文件夹和文件夹内部文件的语义一致性

    我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹

    这样做的好处在于一眼就从 pages文件夹看出这个项目的路由有哪些

    单一入口/出口

    举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口

    // 错误用法
    import sellerReducer from 'src/pages/seller/reducer'
    
    // 正确用法
    import { reducer as sellerReducer } from 'src/pages/seller'
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点

    就近原则,紧耦合的文件应该放到一起,且应以相对路径引用

    使用相对路径可以保证模块内部的独立性

    // 正确用法
    import styles from './index.module.scss'
    // 错误用法
    import styles from 'src/pages/seller/index.module.scss'
    
    • 1
    • 2
    • 3
    • 4

    举个例子

    假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller

    如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。

    但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改

    公共的文件应该以绝对路径的方式从根目录引用

    公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components

    在使用到的页面中,采用绝对路径的形式引用

    // 错误用法
    import Input from '../../components/input'
    // 正确用法
    import Input from 'src/components/input'
    
    • 1
    • 2
    • 3
    • 4

    同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换

    再加上绝对路径有全局的语义,相对路径有独立模块的语义

    /src 外的文件不应该被引入

    vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js,css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件

    这样的好处是方便划分项目代码文件和配置文件

    小结

    项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

    vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?

    一、是什么

    权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源

    而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发

    • 页面加载触发
    • 页面上的按钮点击触发

    总的来说,所有的请求发起都触发自前端路由或视图

    所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是:

    • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示页
    • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件
    • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截

    二、如何做

    前端权限控制可以分为四个方面:

    • 接口权限
    • 按钮权限
    • 菜单权限
    • 路由权限

    接口权限

    接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

    登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

    axios.interceptors.request.use(config => {
        config.headers['token'] = cookie.get('token')
        return config
    })
    axios.interceptors.response.use(res=>{},{response}=>{
        if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误
            router.push('/login')
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    路由权限控制

    方案一

    初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验

    const routerMap = [
      {
        path: '/permission',
        component: Layout,
        redirect: '/permission/index',
        alwaysShow: true, // will always show the root menu
        meta: {
          title: 'permission',
          icon: 'lock',
          roles: ['admin', 'editor'] // you can set roles in root nav
        },
        children: [{
          path: 'page',
          component: () => import('@/views/permission/page'),
          name: 'pagePermission',
          meta: {
            title: 'pagePermission',
            roles: ['admin'] // or you can only set roles in sub nav
          }
        }, {
          path: 'directive',
          component: () => import('@/views/permission/directive'),
          name: 'directivePermission',
          meta: {
            title: 'directivePermission'
            // if do not set roles, means: this page does not require permission
          }
        }]
      }]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这种方式存在以下四种缺点:

    • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。
    • 全局路由守卫里,每次路由跳转都要做权限判断。
    • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
    • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

    方案二

    初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制

    登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

    import router from './router'
    import store from './store'
    import { Message } from 'element-ui'
    import NProgress from 'nprogress' // progress bar
    import 'nprogress/nprogress.css'// progress bar style
    import { getToken } from '@/utils/auth' // getToken from cookie
    
    NProgress.configure({ showSpinner: false })// NProgress Configuration
    
    // permission judge function
    function hasPermission(roles, permissionRoles) {
      if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
      if (!permissionRoles) return true
      return roles.some(role => permissionRoles.indexOf(role) >= 0)
    }
    
    const whiteList = ['/login', '/authredirect']// no redirect whitelist
    
    router.beforeEach((to, from, next) => {
      NProgress.start() // start progress bar
      if (getToken()) { // determine if there has token
        /* has token*/
        if (to.path === '/login') {
          next({ path: '/' })
          NProgress.done() // if current page is dashboard will not trigger	afterEach hook, so manually handle it
        } else {
          if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
            store.dispatch('GetUserInfo').then(res => { // 拉取user_info
              const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
              store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
                router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
                next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
              })
            }).catch((err) => {
              store.dispatch('FedLogOut').then(() => {
                Message.error(err || 'Verification failed, please login again')
                next({ path: '/' })
              })
            })
          } else {
            // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
            if (hasPermission(store.getters.roles, to.meta.roles)) {
              next()//
            } else {
              next({ path: '/401', replace: true, query: { noGoBack: true }})
            }
            // 可删 ↑
          }
        }
      } else {
        /* has no token*/
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
          next()
        } else {
          next('/login') // 否则全部重定向到登录页
          NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
        }
      }
    })
    
    router.afterEach(() => {
      NProgress.done() // finish progress bar
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限

    这种方式也存在了以下的缺点:

    • 全局路由守卫里,每次路由跳转都要做判断
    • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
    • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

    菜单权限

    菜单权限可以理解成将页面与理由进行解耦

    方案一

    菜单与路由分离,菜单由后端返回

    前端定义路由信息

    {
        name: "login",
        path: "/login",
        component: () => import("@/pages/Login.vue")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

    全局路由守卫里做判断

    function hasPermission(router, accessMenu) {
      if (whiteList.indexOf(router.path) !== -1) {
        return true;
      }
      let menu = Util.getMenuByName(router.name, accessMenu);
      if (menu.name) {
        return true;
      }
      return false;
    
    }
    
    Router.beforeEach(async (to, from, next) => {
      if (getToken()) {
        let userInfo = store.state.user.userInfo;
        if (!userInfo.name) {
          try {
            await store.dispatch("GetUserInfo")
            await store.dispatch('updateAccessMenu')
            if (to.path === '/login') {
              next({ name: 'home_index' })
            } else {
              //Util.toDefaultPage([...routers], to.name, router, next);
              next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
            }
          }  
          catch (e) {
            if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
              next()
            } else {
              next('/login')
            }
          }
        } else {
          if (to.path === '/login') {
            next({ name: 'home_index' })
          } else {
            if (hasPermission(to, store.getters.accessMenu)) {
              Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
            } else {
              next({ path: '/403',replace:true })
            }
          }
        }
      } else {
        if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
          next()
        } else {
          next('/login')
        }
      }
      let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
      Util.title(menu.title);
    });
    
    Router.afterEach((to) => {
      window.scrollTo(0, 0);
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤的

    如果根据路由name找不到对应的菜单,就表示用户有没权限访问

    如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂载

    这种方式的缺点:

    • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用
    • 全局路由守卫里,每次路由跳转都要做判断

    方案二

    菜单和路由都由后端返回

    前端统一定义路由组件

    const Home = () => import("../pages/Home.vue");
    const UserInfo = () => import("../pages/UserInfo.vue");
    export default {
        home: Home,
        userInfo: UserInfo
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    后端路由组件返回以下格式

    [
        {
            name: "home",
            path: "/",
            component: "home"
        },
        {
            name: "home",
            path: "/userinfo",
            component: "userInfo"
        }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组件

    如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理

    这种方法也会存在缺点:

    • 全局路由守卫里,每次路由跳转都要做判断
    • 前后端的配合要求更高

    按钮权限

    方案一

    按钮权限也可以用v-if判断

    但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判断

    这种方式就不展开举例了

    方案二

    通过自定义指令进行按钮权限的判断

    首先配置路由

    {
        path: '/permission',
        component: Layout,
        name: '权限测试',
        meta: {
            btnPermissions: ['admin', 'supper', 'normal']
        },
        //页面需要的权限
        children: [{
            path: 'supper',
            component: _import('system/supper'),
            name: '权限测试页',
            meta: {
                btnPermissions: ['admin', 'supper']
            } //页面需要的权限
        },
        {
            path: 'normal',
            component: _import('system/normal'),
            name: '权限测试页',
            meta: {
                btnPermissions: ['admin']
            } //页面需要的权限
        }]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    自定义权限鉴定指令

    import Vue from 'vue'
    /**权限指令**/
    const has = Vue.directive('has', {
        bind: function (el, binding, vnode) {
            // 获取页面按钮权限
            let btnPermissionsArr = [];
            if(binding.value){
                // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
                btnPermissionsArr = Array.of(binding.value);
            }else{
                // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
                btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
            }
            if (!Vue.prototype.$_has(btnPermissionsArr)) {
                el.parentNode.removeChild(el);
            }
        }
    });
    // 权限检查方法
    Vue.prototype.$_has = function (value) {
        let isExist = false;
        // 获取用户按钮权限
        let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
        if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
            return false;
        }
        if (value.indexOf(btnPermissionsStr) > -1) {
            isExist = true;
        }
        return isExist;
    };
    export {has}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在使用的按钮中只需要引用v-has指令

    <el-button @click='editClick' type="primary" v-has>编辑</el-button>
    
    • 1

    小结

    关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离

    权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断

    Vue项目中你是如何解决跨域的呢?

    img

    #一、跨域是什么

    跨域本质是浏览器基于同源策略的一种安全手段

    同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

    所谓同源(即指在同一个域)具有以下三个相同点

    • 协议相同(protocol)
    • 主机相同(host)
    • 端口相同(port)

    反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

    一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

    二、如何解决

    解决跨域的方法有很多,下面列举了三种:

    • JSONP
    • CORS
    • Proxy

    而在vue项目中,我们主要针对CORSProxy这两种方案进行展开

    CORS

    CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

    CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

    只要后端实现了 CORS,就实现了跨域

    img

    koa框架举例

    添加中间件,直接设置Access-Control-Allow-Origin响应头

    app.use(async (ctx, next)=> {
      ctx.set('Access-Control-Allow-Origin', '*');
      ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
      ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
      if (ctx.method == 'OPTIONS') {
        ctx.body = 200; 
      } else {
        await next();
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host

    Proxy

    代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

    方案一

    如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

    通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

    vue.config.js文件,新增以下代码

    amodule.exports = {
        devServer: {
            host: '127.0.0.1',
            port: 8084,
            open: true,// vue项目启动时自动打开浏览器
            proxy: {
                '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                    target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                    changeOrigin: true, //是否跨域
                    pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                        '^/api': "" 
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    通过axios发送请求中,配置请求的根路径

    axios.defaults.baseURL = '/api'
    
    • 1

    方案二

    此外,还可通过服务端实现代理请求转发

    express框架为例

    var express = require('express');
    const proxy = require('http-proxy-middleware')
    const app = express()
    app.use(express.static(__dirname + '/'))
    app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
                          }));
    module.exports = app
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    方案三

    通过配置nginx实现代理

    server {
        listen    80;
        # server_name www.josephxia.com;
        location / {
            root  /var/www/html;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
        }
        location /api {
            proxy_pass  http://127.0.0.1:3000;
            proxy_redirect   off;
            proxy_set_header  Host       $host;
            proxy_set_header  X-Real-IP     $remote_addr;
            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    常见状态码

    304表示,客户端有缓存文件并向服务器发送了一个options请求,服务器返回304 Not Modified,告诉客户端,原来缓存的文件没有修改过,可以继续使用原来缓存的文件。

    403: ‘得到访问授权,但访问是被禁止’,

    404: ‘访问的是不存在的资源’,

    500: ‘服务器不可用,未返回正确的数据’,

    vue2跟vue3的区别

    一、vue2和vue3双向数据绑定原理的区别?

    vue2 的双向数据绑定是利用ES5 的一个 API Object.definePropert()对数据进行劫持 结合 发布订阅模式的方式来实现的
    vue3发生了改变,使用proxy替换Object.defineProerty,使用Proxy的优势:

    可直接监听数组类型的数据变化
    性能的提升
    监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
    可直接实现对象属性的新增/删除
    
    • 1
    • 2
    • 3
    • 4

    二、根节点的不同

    vue3在组件中支持多个根节点

    vue2

    <template>
        <div>
            <h1>h1>
        div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    vue3

    <template>
        <div>
            <h1>h1>
        div>
        <div>
            <span>span>
        div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    三、Composition API (组合api)

    vue2和vue3最大的区别就是,vue3使用了Composition API (组合api)

    在vue2中是使用的Options API,这种写法不方便我们的阅读和交流,逻辑过于分散。

    vue2

    <script>
    export default {
    	// 数据
        data() {
            return {};
        },
        mounted() {},
        // 方法
        methods: {},
        computed: {},
        components:{}
    };
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    vue3

    这样代码会更加简洁和整洁

    <script>
    export default {
        setup() {
            // 数据 和 方法都在setup里面使用
        }
    };
    </script>			
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    四、生命周期的变化

    Vue2--------------vue3

    beforeCreate  -> setup()	开始创建组件之前,创建的是data和method
    created       -> setup()
    beforeMount   -> onBeforeMount	组件挂载到节点上之前执行的函数。
    mounted       -> onMounted	组件挂载完成后执行的函数
    beforeUpdate  -> onBeforeUpdate	组件更新之前执行的函数。
    updated       -> onUpdated	组件更新完成之后执行的函数。
    beforeDestroy -> onBeforeUnmount	组件挂载到节点上之前执行的函数。
    destroyed     -> onUnmounted	组件卸载之前执行的函数。dszhuoyi
    activated     -> onActivated	组件卸载完成后执行的函数
    deactivated   -> onDeactivated
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:如果想要在vue中获取dom节点在created中用this.$nexttick

    五、 vue2和vue3的diff算法
    vue2

    vue2 diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
    vue2 diff算法会比较每一个vnode,而对于一些不参与更新的元素,进行比较是有点消耗性能的。

    vue3

    vue3 diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags就是优化的标识。
    只会比较patchFlags发生变化的vnode,进行更新视图,对于没有变化的元素做静态标记,在渲染的时候直接复用。

    vue3

    <template>
        <div>
            <h1>h1>
        div>
        <div>
            <span>span>
        div>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    三、Composition API (组合api)

    vue2和vue3最大的区别就是,vue3使用了Composition API (组合api)

    在vue2中是使用的Options API,这种写法不方便我们的阅读和交流,逻辑过于分散。

    vue2

    <script>
    export default {
    	// 数据
        data() {
            return {};
        },
        mounted() {},
        // 方法
        methods: {},
        computed: {},
        components:{}
    };
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    vue3

    这样代码会更加简洁和整洁

    <script>
    export default {
        setup() {
            // 数据 和 方法都在setup里面使用
        }
    };
    </script>			
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    四、生命周期的变化

    Vue2--------------vue3

    beforeCreate  -> setup()	开始创建组件之前,创建的是data和method
    created       -> setup()
    beforeMount   -> onBeforeMount	组件挂载到节点上之前执行的函数。
    mounted       -> onMounted	组件挂载完成后执行的函数
    beforeUpdate  -> onBeforeUpdate	组件更新之前执行的函数。
    updated       -> onUpdated	组件更新完成之后执行的函数。
    beforeDestroy -> onBeforeUnmount	组件挂载到节点上之前执行的函数。
    destroyed     -> onUnmounted	组件卸载之前执行的函数。dszhuoyi
    activated     -> onActivated	组件卸载完成后执行的函数
    deactivated   -> onDeactivated
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    注意:如果想要在vue中获取dom节点在created中用this.$nexttick

    五、 vue2和vue3的diff算法
    vue2

    vue2 diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
    vue2 diff算法会比较每一个vnode,而对于一些不参与更新的元素,进行比较是有点消耗性能的。

    vue3

    vue3 diff算法在初始化的时候会给每个虚拟节点添加一个patchFlags,patchFlags就是优化的标识。
    只会比较patchFlags发生变化的vnode,进行更新视图,对于没有变化的元素做静态标记,在渲染的时候直接复用。

  • 相关阅读:
    【Linux进阶】-- 1.python脚本实现守护进程daemon调度,启停等
    AI算法优缺点
    腾讯云2023年双十一活动时间、活动入口、活动内容详细解读
    VTK四面体文件格式
    剩余参数和展开运算符的区别
    PHP项目学习笔记-萤火商城https://www.yiovo.com/doc
    嵌入式学习:使用vscode配置esp32环境(从安装到测试)
    LeetCode--代码详解 146.LRU缓存
    JDK19新特性使用详解
    Unity反编译:IL2CPP 打包输出的cpp文件和dll(程序集)位置、Mono打包输出的dll(程序集)位置
  • 原文地址:https://blog.csdn.net/weixin_46862327/article/details/126541262