个人总结记录-vue使用

401 阅读13分钟

Vue2

1. 生命周期流程

  1. 通过new Vue()创建Vue实例
  2. beforeCreate,到这一步已初始化了事件和生命周期
  3. created,到这一步已初始化了依赖注入和响应式数据
  4. beforeMount,到这一步已把模板渲染成了虚拟dom
  5. mounted,到这一步已根据虚拟dom生成了实际dom元素
  6. beforeUpdate,响应式数据改变时,触发这一步
  7. updated,到这一步,虚拟dom已重新渲染和装载
  8. beforeDestroy,使用vm.$destroy()触发
  9. destroyed,到这一步,组件、监察者、事件均已卸载
  10. activated,keep-alive的生命周期,当被其包裹的组件激活时触发
  11. deactivated,keep-alive的生命周期,当被其包裹的组件关闭激活时触发

2. v-if 和 v-show

  • v-if如果为false,则不会渲染节点,反之会重新渲染节点
  • v-show如果为false,已渲染节点依然存在,但会通过display: none;隐藏显示

如果是需要频繁操作显示隐藏的节点,建议使用v-show,这样不会频繁渲染导致过大的性能损耗

3. 内置指令

  • v-once,使用它的元素或组件只渲染一次,其本身和子级都不会随着数据改变而重新渲染,是非响应式的静态内容
  • v-bind,用于绑定元素上的属性,让属性可以随响应式改变,比如v-bind:class,一般都简写为:class
  • v-on,用于监听dom事件,比如v-on:click,一般简写为@click
  • v-html,相当于innerHTML,需要谨慎使用,有遭受XSS攻击的风险
  • v-text,相当于innerText
  • v-model,是元素或组件value属性的语法糖
  • v-for,用于循环dom结构,优先级比v-if高,尽量不与v-if一起使用,不然v-for中的每个循环都会执行一次v-if,增大了浏览器性能开销;需要加上:key的绑定,vnode缺少比对的唯一标识,也会增大比对开销
  • v-if/v-else/v-else-if,配合template使用,相当于render()中的三元表达式
  • v-show,上面已有说明
  • v-pre,跳过这个元素及其子元素的编译过程
  • v-cloak,解决vue加载慢导致屏幕闪动的问题,也就是避免先出现vue源代码,然后再出现渲染内容的情况

4. computed计算属性

computed可以设置get和set,一般用来定义根据其他属性计算来的值,computed的值有缓存,只有依赖的属性值改变时,才会重新计算并返回结果

<template>
  <div>
    <input type="text" v-model="msg" />
    <p>{{text}}</p>
  </div>
</tempalte>

export default {
  name: 'test',
  data() {
    return {
      msg: '掘金'
    }
  },
  computed: {
    text: {
      get() {
        return '欢迎来到' + this.msg;
      }
    }
  }
}

5. watch监听属性

watch可以监听属性的变化,并通过回调函数执行一些逻辑处理

<template>
  <div>
    <input type="text" v-model="msg" />
    <input type="text" v-model="form.name" />
  </div>
</template>

export default {
  name: 'test',
  data() {
    return {
      msg: ''
    },
    form: {
      name: ''
    }
  },
  watch: {
    msg(newVal, oldVal) {
      console.log('old value is ' + oldVal);
    },
    form: {
      //监听引用类型数据的内部数据变化,默认为false
      deep: true,
      //刚刚初始化组件时,就执行属性监听回调,默认为false
      immediate: true,
      handler(newVal, oldVal) {
        console.log('name is changed');
      }
    },
    //监听路由变化
    '$route'(to, from) {}
  }
}

如果是使用vm.$watch绑定的监听,那么注销组件时还需要注销watch绑定

const unWatch = vm.$watch('msg', (newVal, oldVal) => {
  console.log('old value is ' + oldVal);
});

unWatch(); //手动注销

6. mixin混入

mixin一般用来定义公用方法或逻辑代码,方便其他组件统一调用。

如果mixin中定义的属性或函数,与当前组件重名,则会以当前组件的属性或函数为准。

//mixins.js
export default {
  data() {
    return {
      msg: '掘金'
    }
  },
  methods: {
    getMsg() {
      return this.msg;
    }
  }
}

//组件
import globalMixins from 'mixins.js';

export default {
  name: 'test',
  /*
   * 使用了mixins后,globalMixins中的data/computed/methods都会合并到当前组件实例下
   * mixins是一个数组,也就是说,可以用来混入多个vue实例
   */
  mixins: [globalMixins],
  data() {
    return {
      txt: 'hello world'
    }
  },
  created() {
    this.printMsg();
  },
  methods: {
    printMsg() {
      //可以使用由globalMixins混入进来的getMsg()
      const msg = this.getMsg();
      console.log(msg);
    }
  }
}

还有一个全局方法Vue.mixin,因为vue是一个基于Vue构造函数的单实例框架,内部所有的组件都是Vue实例的子实例,因此通过全局的Vue.mixin方法,可以直接影响Vue实例,从而影响其内部关联的所有组件实例,所以需要谨慎使用,可以在创建中间件时用到。

/*
 * 通过全局的Vue.mixin,可以影响所有组件的生命周期
 * 下面这个例子会导致所有组件的created生命周期,都会打印出'全局混入的created'
 */
Vue.mixin({
  created() {
    console.log('全局混入的created');
  }
});

7. nextTick

vm.$nextTick的回调是在下一次dom更新渲染之后执行的,因为vue的dom更新是异步的,因此需要借助这个回调方法获取重新渲染后的dom结构。

nextTick函数在源码中,会通过判断是否支持下列原生方法函数来依次进行实现:

先判断是否支持setImmediate(),支持就使用setImmediate()来执行 ->

否则判断是否支持MessageChannel(),支持就使用new MessageChannel()来执行 ->

否则使用setTimeout()来执行

上面是宏任务(macro task)的判断执行流程,如果是微任务(micro task),就会先判断是否支持原生Promise,

支持就用new Promise()来执行,否则就把宏任务的判断流程走一遍。

<template>
  <div>
    <p ref="domMsg">{{msg}}</p>
    <button @click="changeMsg()">change msg</button>
  </div>
</template>

export default {
  name: 'test',
  data() {
    return {
      msg: 'hello world'
    }
  },
  methods: {
    changeMsg() {
      this.msg = '掘金';
      //此时打印的还是hello world,因为dom更新是异步的
      console.log(this.$refs.domMsg.innerHTML);
      //nextTick中打印的才是新渲染的dom内容
      this.$nextTick(() => {
        let domMsg = this.$refs.domMsg.innerHTML;
        console.log(domMsg);
      });
    }
  }
}

8. keep-alive

keep-alive是vue内置的组件,可以缓存被其包裹住的组件,组件切换时不会对当前组件进行卸载,因此无需变动内容的组件可以通过它降低性能损耗。

可用参数有三个:

  • include,表示需要缓存的组件,以逗号分隔的名称列表对应的是组件的name
  • exclude,表示不需要被缓存的组件,也就是列表以外的组件都会被缓存
  • max,表示最大缓存数,不常用

keep-alive运用LRU算法,会将最近访问的组件插入到访问列表尾部,越在列表前面的组件,表示越久没有被访问过,当列表数量超过max属性值时,会自动销毁列表最前面的组件。

keep-alive是缓存当前页面虚拟dom中的节点,所以对iframe中的内容不起作用,因为iframe中的内容不在当前页面的虚拟dom中。

//app.js
<template>
  <div>
    <keep-alive include="globalMenu,globalHeader">
      <router-view></router-view>
    </keep-alive>
    <keep-alive exclude="test">
      <router-view></router-view>
    </keep-alive>
  </div>
</template>

//globalMenu.js
export default {
  name: 'globalMenu',
  data() {
    return {}
  },
  mounted() {
    console.log('组件已装载');
  },
  /*
   * 组件第一次生成时,activated是发生在mounted之后的;
   * 以后再切换回该组件,会直接触发activated,不会再触发之前的生命周期
   */
  activated() {
    console.log('组件被激活了');
  },
  deactivated() {
    console.log('组件关闭了激活');
  }
}

9. Vue.set

使用方式:Vue.set(Array或Object, 数组下标或对象key, 新的值);

因为vue是在初始化的时候,利用Object.defineProperty的访问器属性一次性在data数据上绑定的响应式,因此引用类型的数据,如数组和对象,在下面两种情况下修改数据,会导致数据不是响应式的:

  • 在实例创建之后,在响应式对象上添加新的属性
  • 直接通过数组下标来修改数组的值

这两种情况下,就需要用到Vue.set去修改数组或对象,保证修改后的值是响应式的:

<template>
  <div>
    <div v-for="(item,idx) in arr" :key="idx">{{item}}</div>
    <div>{{obj.text}}</div>
    <button @click="addArrVal()">修改数组</button>
    <button @click="addObjVal()">修改对象</button>
  </div>
</template>

export default {
  name: 'test',
  data() {
    return {
      arr: [11,22,33],
      obj: {
        name: '对象'
      }
    }
  },
  methods: {
    addArrVal() {
      //让数组变成[66,22,33]
      //this.arr[0] = 66;这样赋值的66,是非响应式的,无法触发页面渲染
      Vue.set(this.arr, 0, 66);
    },
    addObjVal() {
      //this.obj.text = '掘金';这样赋的值,是非响应式的,页面上不会渲染出来
      Vue.set(this.obj, 'text', '掘金');
    }
  }
}

Vue.set的执行原理:初始化实例时,vue给数组和对象数据都增加了__ob__属性,代表Observer实例。当给对象新增不存在的属性时,首先会把新的属性进行响应式跟踪,然后触发ob.dep.notify(),通知dep把收集的watcher进行更新。当改变数组值时,因为vue重写了数组的push/splice/unshift,增加了响应式转换逻辑,因此会调用数组的splice进行数值替换或添加,将修改的数组值转换成响应式的。

10. Vue.extend

Vue.extend使用基础Vue构造器,创建一个子类,参数是包含组件options的对象。

一般的开发场景其实是用不到Vue.extend的,但是如下使用场景可能会需要使用:

  • 想从接口动态渲染组件时
  • 想要实现一个类似于window.alert()的组件,要求像js一样可全局调用它
//test.js
import Vue from 'vue';

const testComponent = Vue.extend({
  template: '<div>{{msg}}</div>',
  props: {
    msg: String
  }
});

//局部注册,这里给组件传参,要使用porpsData
new testComponent({
  porpsData: {
    msg: '掘金'
  }
}).$mount('#app');

//全局注册
Vue.component('testComponent', testComponent);



//在其他组件中调用
export default {
  components: {
    testComponent
  }
}

11. Vue.directive自定义指令

Vue.directive可以用来自定义类似于v-bind的标签指令

自定义指令有bind/inserted/update/componentUpdate/unbind五个钩子函数

每个钩子函数有el/binding/vnode/oldVnode四个参数。el表示所绑定的元素,可直接用来操作dom。binding表示指令所对应的对象。vnode表示Vue生成的虚拟节点。oldVnode表示上一个虚拟节点,只在update和componentUpdate可用。

<template>
  <div>
    <input type="text" v-model="msg" v-focus />
  </div>
</template>

import Vue from 'vue';

Vue.directive('focus', {
  //只调用一次,指令第一次绑定到元素时调用
  bind(el, binding, vnode) {
    console.log('first bind');
  },
  //被绑定元素插入父节点时调用
  inserted(el, binding, vnode) {
    el.focus();
  },
  //所在组件的vnode更新时调用
  update(el, binding, vnode, oldVnode) {
    console.log('component is update');
  },
  //指令所在组件的vnode及其子级vnode全部更新后调用
  componentUpdate(el, binding, vnode, oldVnode) {
    console.log('component all vnode is updated');
  },
  //只调用一次,指令与元素解绑时调用
  unbind(el, binding, vnode) {
    console.log('directive is unbind');
  }
});

12. Vue修饰符

事件修饰符

比如@click.stop=""

  • .stop,阻止事件冒泡
  • .prevent,阻止标签默认行为
  • .capture,使用事件捕获模式,也就是先在触发元素自身上处理事件,然后再向内部元素逐级触发
  • .self,只有当event.target是当前元素自身时,触发处理函数
  • .once,事件只触发一次
  • .passive,告诉浏览器不阻止事件的默认行为
  • .native,使用原生方式给当前元素绑定事件,而不是vue内部定义的$on
v-model修饰符

比如v-model.lazy=""

  • .lazy,使v-model的值在触发change后才执行数据响应
  • .number,自动将输入的值转化为数值类型
  • .trim,自动过滤用户输入的首位空格
键盘事件修饰符

使动作事件可以捕获按键类型,比如@keyup.enter=""

  • .enter
  • .tab
  • .delete,捕获删除和退格键
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
系统修饰键

比如@click.ctrl="",相当于鼠标点击 + Ctrl

比如@keyup.alt.67="",相当于Alt + C

  • .ctrl
  • .alt
  • .shift
  • .meta,对应windows键盘上的视窗键
鼠标按钮修饰符

比如@click.left="",@click.middle=""

  • .left
  • .right
  • .middle

13. slot插槽

插槽分为下面几种:

  • 匿名插槽或者叫默认插槽,一个组件中只能有一个匿名插槽
//父组件
<template>
  <div class="father>
    <h2>这是父组件</h2>
    <child>
      <div class="child-info">
        <p>子组件的插槽内容</p>
      </div>
    </child>
  </div>
</template>

//子组件
<template>
  <div class="child">
    <h3>这是子组件</h3>
    <slot></slot>
  </div>
</template>

vue会把上面例子中子组件的slot标签替换成父组件中被child标签包裹的内容,也就是说,子组件内容会变成下面的样子:

<template>
  <div class="child">
    <h3>这是子组件</h3>
    <div class="child-info">
      <p>子组件的插槽内容</p>
    </div>
  </div>
</template>
  • 具名插槽,一个组件中可以有多个具名插槽
//父组件
<template>
  <div class="father">
    <h2>这是父组件</h2>
    <child>
      <div class="name1" slot="name1">
        <p>这是name1的内容</p>
      </div>
      <div class="name2" slot="name2">
        <p>这是name2的内容</p>
        <p>hello world</p>
      </div>
      <div class="default">
        <p>这是匿名插槽的内容</p>
      </div>
    </child>
  </div>
</template>

//子组件
<template>
  <div class="child">
    <h3>这是子组件</h3>
    <slot name="name1"></slot>
    <slot name="name2"></slot>
    <slot></slot>
  </div>
</template>

vue会把上面例子中的子组件内容通过slot标签替换成下面的样子:

<template>
  <div class="child">
    <h3>这是子组件</h3>
    <div class="name1">
      <p>这是name1的内容</p>
    </div>
    <div class="name2">
      <p>这是name2的内容</p>
      <p>hello world</p>
    </div>
    <div class="default">
      <p>这是匿名插槽的内容</p>
    </div>
  </div>
</template>
  • 作用域插槽,也就是绑定了数据的插槽

在下面例子中,slot-scope对应的名称取名叫slotProps,这是一个包含插槽所有prop的对象集合,这个名字是可以随便起的,只是一个对象的代称,类似于形参的概念

//父组件
<template>
  <div class="father">
    <h2>这是父组件</h2>
    <child>
      <template slot-scope="slotProps">
        <ul>
          <li v-for="(item,idx) in slotProps.list" :key="idx">{{item.name}}</li>
        </ul>
      </template>
    </child>
    
    <child>
      <template slot-scope="slotProps">
        <p>{{slotProps.txt}}</p>
      </template>
    </child>
  </div>
</template>
//子组件
<template>
  <div class="child">
    <h3>这是子组件</h3>
    <slot :list="list" :txt="txt"></slot>
  </div>
</template>

export default {
  name: 'childComponent',
  data() {
    return {
      list: [
        { name: '列表1' },
        { name: '列表2' }
      ],
      txt: '这是子组件文字'
    }
  }
}

通过slot标签替换后,最终父组件的dom结构会变成下面的样子:

<template>
  <div class="father">
    <h2>这是父组件</h2>
    <div class="child">
      <h3>这是子组件</h3>
      <ul>
        <li>列表1</li>
        <li>列表2</li>
      </ul>
    </div>
    <div class="child">
      <h3>这是子组件</h3>
      <p>这是子组件文字</p>
    </div>
  </div>
</template>

上面说的关于slot的用法是vue2.6.0之前的,从2.6.0开始,具名插槽和作用域插槽的写法有了一点儿变化

具名插槽新写法:

//父组件
<template>
  <div class="father">
    <h2>这是父组件</h2>
    <child>
      <!--具名插槽可以缩写成<template #name1>-->
      <template v-slot:name1>
        <div class="name1">
          <p>这是name1的内容</p>
        </div>
      </template>
      
      <template v-slot:name2>
        <div class="name2">
          <p>这是name2的内容</p>
          <p>hello world</p>
        </div>
      </template>
      
      <div class="default">
        <p>这是匿名插槽的内容</p>
      </div>
    </child>
  </div>
</template>

//子组件
<template>
  <div class="child">
    <h3>这是子组件</h3>
    <slot name="name1"></slot>
    <slot name="name2"></slot>
    <slot></slot>
  </div>
</template>

作用域插槽新写法:

//父组件
<template>
  <div class="father">
    <h2>这是父组件</h2>
    <child>
      <!--具名作用域插槽可以缩写成<template #name1="{slotProps}">-->
      <template v-solt:name1="slotProps">
        <ul>
          <li v-for="(item,idx) in slotProps.list" :key="idx">{{item.name}}</li>
        </ul>
      </template>
    </child>
    
    <child>
      <!--匿名作用域插槽可以缩写成<template #default="{slotProps}">-->
      <template v-solt:default="slotProps">
        <p>{{slotProps.txt}}</p>
      </template>
    </child>
  </div>
</template>
//子组件
<template>
  <div class="child">
    <h3>这是子组件</h3>
    <slot name="name1" :list="list"></slot>
    <slot :txt="txt"></slot>
  </div>
</template>

export default {
  name: 'childComponent',
  data() {
    return {
      list: [
        { name: '具名插槽列表1' },
        { name: '具名插槽列表2' }
      ],
      txt: '这是匿名作用域插槽文字'
    }
  }
}

14. vue组件通信方式

父组件向子组件通信
//父组件
<template>
  <div>
    <child :msg=""></child>
  </div>
</template>

export default {
  name: 'ComponentFather',
  data() {
    return {
      msg: '掘金'
    }
  }
}

//子组件
<template>
  <div>
    <p>{{msg}}</p>
  </div>
</template>

export default {
  name: 'ComponentChild',
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
子组件向父组件通信
//父组件
<template>
  <div>
    <child @msgChange="msgUpdate"></child>
    <p>{{msg}}</p>
  </div>
</template>

export default {
  name: 'ComponentFather',
  data() {
    return {
      msg: '掘金'
    }
  },
  methods: {
    msgUpdate(msg) {
      this.msg = msg;
    }
  }
}

//子组件
<template>
  <div>
    <button @click="changeMsg"></button>
  </div>
</template>

export default {
  name: 'ComponentChild',
  methods: {
    changeMsg() {
      this.$emit('msgChange', 'hello world');
    }
  }
}
使用EventBus方式实现父子、兄弟、跨级通信
//main.js
//在Vue原型上挂一个Vue实例,使得全局可调用
Vue.prototype.bus = new Vue();

//组件A
<template>
  <div>
    <button @click="sendMsg('掘金')"></button>
  </div>
</template>

export default {
  name: 'ComponentA',
  methods: {
    sendMsg(msg) {
      this.bus.$emit('eventMsg', msg);
    }
  }
}

//组件B
<template>
  <div>
    <p>{{msg}}</p>
  </div>
</template>

export default {
  name: 'ComponentB',
  data() {
    return {
      msg: ''
    }
  },
  mounted() {
    this.bus.$on('eventMsg', (msg) => {
      this.msg = msg;
    });
  }
}
使用attrs/attrs/listeners
//组件A
<template>
  <div>
    <p>{{msg}}</p>
    <component-b :txt="txt" :msg="msg" @emitMsg="changeMsg"></component-b>
  </div>
</template>

import ComponentB from './ComponentB';
export default {
  name: 'ComponentA',
  components: {
    ComponentB
  },
  data() {
    return {
      msg: '哈哈哈',
      txt: '啦啦啦'
    }
  },
  methods: {
    changeMsg(msg) {
      this.msg = msg;
    }
  }
}

//组件B
<template>
  <div>
    <p>{{txt}}</p>
    <component-c v-bind="$attrs" v-on="$listeners"></component-c>
  </div>
</template>

import ComponentC from './ComponentC';
export default {
  name: 'ComponentB',
  components: {
    ComponentC
  },
  //可以关闭自动挂载到组件根元素上的没有在props声明的属性
  inheritAttrs: false,
  props: {
    txt: {
      type: String
    }
  }
}

//组件C
<template>
  <div>
    <p>{{msg}}</p>
    <button @click="sendMsg"></button>
  </div>
</template>

export default {
  name: 'ComponentC',
  props: {
    msg: {
      type: String
    }
  },
  methods: {
    sendMsg() {
      this.$emit('emitMsg', '掘金');
    }
  }
}
使用provide/inject实现祖先元素与子孙元素的通信
//祖先组件
export default {
  provide: {
    msg: '掘金'
  }
}

//子孙组件
export default {
  inject: ['msg'],
  mounted() {
    console.log(this.msg);
  }
}
父组件使用v-on:hook监听子组件的生命周期
//父组件
<template>
  <div>
    <child @hook:mounted="listenerChild"></child>
  </div>
</template>

export default {
  name: 'Father',
  methods: {
    listenerChild() {
      console.log('父组件监听子组件');
    }
  }
}

//子组件
export default {
  name: 'Child',
  mounted() {
    console.log('触发子组件mounted');
  }
}
使用Vue.observable(),模拟一个简易的状态管理
//store.js
import Vue from 'vue';

export const state = Vue.observable({
  count: 0
});

export const mutations = {
  SET_COUNT(payload) {
    if(payload > 0) state.count = payload; 
  }
}

//组件
import { state, mutations } from '../store.js';

<template>
  <div>
    <div @click="setCount">{{count}}</div>
  </div>
</template>

export default {
  name: 'test',
  computed() {
    count() {
      return state.count;
    }
  },
  methods: {
    setCount() {
      mutations.SET_COUNT(state.count++);
    }
  }
}
使用vuex通信

15. 自定义v-model

Vue.component('custom-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input 
      type="checkbox"
      :checked="checked"
      @change="$emit('change', $event.target.checked)"
    />
  `
});

16. vue style scoped

在组件中的style标签上加scoped属性,会让style标签中的样式只对当前组件元素生效,因为会在每一层dom节点上生成一个data-v-hash属性,作为当前组件的style的唯一标识

<template>
  <div>
    <p>test</p>
  </div>
</template>

//这个加了scoped属性的style,只会对当前组件元素有效
<style scoped>
.p{
  color: red;
}
</style>

17. 自定义组件

建议阅读这篇:手把手教你封装 Vue 组件,并使用 npm 发布

常规的vue自定义组件方式:

// customComponent.vue
<template>
  <div class="custom-component">{{ text }}</div>
</template>

<script>
export default {
  name: 'custom-component',

  props: {
    text: {
      type: String,
      default: '自定义组件内容'
    }
  },

  computed: {},
  
  methods: {}
};
</script>

// customComponent.js(导出自定义组件的js)
import CustomComponent from './customComponent.vue';

export default {
  install(Vue) {
    Vue.component(CustomComponent.name, CustomComponent);
  }
};

// 引用自定义组件的代码,一般是在入口文件中
import Vue from 'vue';
import CustomComponent from '@/packages/customComponent.js';

Vue.use(CustomComponent);

一个调用其他组件的自定义组件:

import { MessageBox } from 'elementui';

export const Confirm = function (content, options = {}) {
  return MessageBox({
    title: options.title || '提示',
    message: content,
    confirmButtonText: options.okText,
    cancelButtonText: options.cancelText,
    showCancelButton: true,
    showClose: true,
    stopPropagation: true
  });
};

// 通过将组件挂载在Vue的原型上,方便全局所有组件通过this.$confirm(content, options)来调用
export default {
  install(Vue) {
    Vue.prototype.$confirm = Confirm;
  }
};

18. vue-router

这是vue专用的路由插件,通过hash、history、abstract三种模式,方便用户管理页面路由跳转

插件hash模式主要依赖于原生的hashchange监听事件来实现

插件history模式主要依赖于原生的pushState/replaceState来实现的,popstate监听事件在history执行前进、后退或跳转时会触发,可以在时间的event对象上找到event.state,这是pushState/replaceState改变url时通过第一个参数传的一个对象。

插件abstract模式不依赖bom和dom事件,因此可以脱离浏览器环境执行,比如在node中执行

<template>
  <div>
    <router-view />
  </div>
</template>

import Vue from 'vue';
import VueRouter, { RouteConfig } from 'vue-router';

Vue.use(VueRouter);

const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'Layout',
      component: () => import('页面地址'),
      meta: {
        //此属性配合vue内置的keep-alive组件使用,为true表示需要被缓存,false表示不需要缓存
        keepAlive: true
      }
    }
  ]
});

export default router;

特别说明一下,abstract模式,可以实现不改变浏览器路由地址的页面内嵌,示例如下:

//某个组件内
<template>
  <div>
    <el-drawer>
      <router-view />
    </el-drawer>
  </div>
</template>

import { routes } from '../router/index';
import VueRouter from 'vue-router';
export default {
  name: 'ComponentText',
  //在组件中创建一个独立的vue-router实例,不影响总路由的配置,只控制当前组件包裹的<router-view>的路由
  router: new VueRouter({
    mode: 'abstract',
    base: '/',
    routes
  })
}

全局守卫:

import VueRouter from 'vue-router';

const router = new VueRouter();

//从beforeRouteLeave离开后,会调用到这个钩子函数
router.beforeEach((to, form, next) => {})

router.beforeResolve((to, from, next) => {});

//导航跳转完了会走到这个钩子函数
router.afterEach((to, from) => {});

路由守卫:

import VueRouter from 'vue-router';
const router = VueRouter({
  mode: 'hash',
  routes: [
    {
      name: '',
      path: '',
      component: () => import('页面地址')
      beforeEnter: (to, from, next) => {
        
      }
    }
  ]
});

组件守卫:

export default {
  name: 'ComponentText',
  data() {
    return {
      msg: '掘金'
    }
  },
  beforeRouteEnter(to, from, next) {
    //beforeRouteEnter这一步,组件还没创建好实例,因此访问不到this,需要在next的回调中调取组件实例
    next(vm => {
      console.log(vm.msg);
    })
  },
  //访问过的组件被再次激活时,调用到此钩子函数
  beforeRouteUpdate(to, from, next) {
    console.log(this.msg);
  },
  //离开组件时,会先调用到这个钩子函数
  beforeRouteLeave(to, from, next) {
    console.log(this.msg);
  }
}

因为vue-router会对组件缓存用于激活复用,因此下面的情况,id可能会失效:

const router = new VueRouter({
  routes: [
    {
      name: 'detail',
      path: '/detail/:id',
      component: Detail
    }
  ]
})

可以使用下面两种方式解决:

//通过watch监听路由,获取参数进行请求
export default {
  watch: {
    '$route'() {
      this.getData(this.$route.params.id);
    }
  }
}

//或者通过设置key,防止vue-router进行组件复用
//不推荐这一种,相当于把vue-router复用功能废掉了
<template>
  <div>
    <router-view :key="$route.fullPath" />
  </div>
</template>

19. vuex

vuex是一个专为vue提供的状态管理工具,在全局用一个state存放数据,利用其封装的mutation方法来修改state,以达到更新数据状态的目的。虽然可以直接通过mutation修改state,但vuex不建议直接操作mutation,因为mutation必须是同步的,而action可以异步操作,所以推荐使用action来操作mutation,然后再由mutation修改state。

vuex本身没做数据持久化,所以刷新页面,state中的数据状态就会消失,因此可以使用插件vuex-persist,将state数据存储到cookie或localStorage中来完成数据持久化。

具体使用可查看官网例子,官网写的很简洁明白:vuex官网

20. 注意事项

  • 某些情况下,通过v-if删除了一个dom节点,但是dom节点包裹的子节点代码片段并没有被删除,仍然在浏览器中占用着内存,最终导致内存溢出问题。需要通过在destoryed生命周期中销毁不再需要的dom片段、事件、监察者等。或者通过keep-alive的deactivated生命周期,销毁不再需要的dom片段、事件、监察者等。

  • vue秉承单向数据流原则,父级组件传给子级组件的props,不应该在子组件直接修改,假如有修改需求,应该通过computed计算属性,做一个根据props改变的计算数据。如果在子组件直接修改了props的数据,那么基本类型数据会报错,引用类型数据不会报错,但不会对父组件产生响应式影响,毕竟子组件会根据父组件改变,如果子组件可以直接影响父组件的响应式数据,那么响应式结构就会发生混乱,因此子组件不能修改父组件的props。

21. vue2开发优化

  • 路由懒加载,在vue-router中使用import方式引入组件,然后在webpack中配置chunkFilename进行文件打包切割
  • 注意v-if和v-show的使用场景
  • v-for不要跟v-if一起使用,且需要加有唯一性的key
  • 合理的使用computed和watch
  • 图片懒加载,可以使用插件vue-lazyload实现,这个插件有个使用的坑,需要注意:
<template>
  <div>
    <ul>
      <!--这里的循环中的img必须要加key,不然分页显示时图片还是显示第一页的图片-->
      <li v-for="(item,idx) in imgs" :key="item.id">
        <img v-lazy="item.src" :key="'img'-item.id" />
      </li>
    </ul>
  </div>
</template>
  • 适当采用keep-alive
  • 响应式的数据对象,层级尽量不要太深,因为vue绑定对象响应式,是递归绑定的,对性能损耗较大
  • 运用SSR或预渲染,预渲染可以使用插件prerender-spa-plugin,对指定路由页面生成静态的html,防止首屏加载过慢的问题
  • 尽量利用tree shaking技术
  • 如果不得不使用v-html属性,可以使用sanitize-html插件,对html进行消毒,尽量保证前端html片段的安全性
  • webpack配置中,使用terser-webpack-plugin做多线程压缩,使用cache-loader做编译文件的缓存,使用thread-loader做loader多线程执行
npm install sanitize-html

import sanitizeHTML from 'sanitize-html';
Vue.prototype.$sanitize = sanitizeHTML;

v-html="$sanitize(html)"

说到这里,顺便提一下,webpack5新出的模块联邦功能,很神奇,可以做到让一个项目调用另一个项目封装的组件,但这有一个问题,就是需要不同的项目使用相同的技术栈,不然react项目的组件拿到vue项目中也用不了。

Vue3

1. 生命周期

vue3的生命周期钩子是通过在setUp()中调用的,setUp()是围绕beforeCreate和created执行的,因此不需要给这两个生命周期专门提供钩子函数

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
  • onErrorCapture
  • onRenderTracked
  • onRenderTriggered
  • onActivated
  • onDeactivated

2. 用法

vue3语法有三种用法:

  • 一种是与vue2基本一致的写法,应该是为了方便vue2的项目迁移而做的。
export default {
  name: 'TestOne',
  props: {
    msg: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      testList: [
        {name: '列表一'},
        {name: '列表二'},
        {name: '列表三'},
      ]
    }
  },
  computed: {
  
  },
  created() {
    
  },
  mounted() {
    
  },
  mothods: {

  }
};
  • 一种写法是js代码都写在setup函数中,然后通过vue3提供的各种钩子处理数据和逻辑。
//vue3的template中包裹的第一层节点,就不需要非得是个div了
<template>
  <ul>
    <li v-for="(item, idx) in list" :key="item.id"></li>
  </ul>
  <p>{{msg}}</p>
</template>

import { 
  defineComponent,
  onBeforeMount,
  onMounted,
  reactive,
  toRefs
} from 'vue';

type PageState = {
  list: any
}

export default defineComponent({
  name: 'TestTwo',
  setup(props) {
    const { msg } = toRefs(props);
  
    let state = reactive<PageState>({
      list: []
    });

    const getList = () => {
      state.list = [
        {id: '1',name: '列表一'}, 
        {id: '2',name: '列表二'}, 
        {id: '3',name: '列表三'},
      ];
    };

    onBeforeMount(() => {
      getList();
    });

    onMounted(() => {
      
    });

    return {
      msg,
      ...toRefs(state)
    }
  }
});
  • 一种是直接在script标签带上属性setup,表示内部执行的都是setup运行的代码。
<script lang="ts" setup>
import { defineProps, computed } from 'vue';
import { useStore } from 'vuex';
import { key } from '@src/store';

type Props = {
  msg: string
}
defineProps<Props>();
const store = useStore(key);
const count = computed(() => {
  return store.state.count;
});
const inCrement = () => {
  store.commit('increment');
};
</script>

axios笔记

1. 常规options设置

//会返回一个promise
let request = axios({
  method: 'GET',
  url: '',
  params: {},
  body: {},
  responseType: 'json',
  baseURL: '/api',
  timeout: 30 * 1000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
});

更多配置项需要参考官方文档:axios中文文档

2. 拦截器

  • 拦截器使用
/*
 * 请求拦截器和响应拦截器都可以定义多个,它们会按照顺序执行
 * request是先定义的后执行,response是先定义先执行,这跟它们往任务队列中的插入方式有关
 */

//请求拦截器
axios.intercepters.request.use(
  config => {
  },
  error => {
  }
);
axios.intercepters.request.use(
  config => {
  },
  error => {
  }
);

//响应拦截器
axios.intercepters.response.use(
  res => {
  },
  error => {
  }
);
  • 拦截器实现原理
//此代码只是简单示意,并不是axios真实的内部代码实现

//定义一个拦截器任务队列
let queue = [];

//每个拦截器任务都是一个数组键值对
let task = [fulfilled, rejected];

//request拦截器,通过unshift插入到队列前面,因此越晚插入的越先执行
this.interceptors.request.forEach(interceptor => {
  let task = [interceptor.fulfilled, interceptor.rejected];
  queue.unshift(task);
});

//response拦截器,通过push插入到队列后面,因此越先插入的越先执行
this.interceptors.response.forEach(interceptor => {
  let task = [interceptor.fulfilled, interceptor.rejected];
  queue.push(task);
});

//最终形成的队列类似于下面的样子
[requestInterceptor, requestInterceptor, promise, responseInterceptor, responseInterceptor]