Vue特训

Vue高级学习

快速原型开发

npm i -g @vue/cli 
npm i -g @vue/cli-service-global

您可以*.vue使用vue serve和vue build命令只使用一个文件快速进行原型设计,但是它们需要先安装一个额外的全局插件:
npm install -g @vue/cli-service-global

组件

组件基础知识

组件分类
  • vue-router 产生的每个页面,它本质上也是一个组件(.vue)
  • 不包含业务,独立、具体功能的基础组件,比如日期选择器模态框等。这类组件作为项目的基础控件
  • 业务组件。它不像第二类独立组件只包含某个功能,而是在业务中被多个页面复用的,它与独立组件的区别是,业务组件只在当前项目中会用到,不具有通用性,而且会包含一些业务,比如数据请求;

业务组件更像是介于第一类和第二类之间,在开发上也与独立组件类似,但寄托于项目,你可以使用项目中的技术栈,比如 Vuex、axios、echarts 等,所以它的开发难度相对独立组件要容易点,但也有必要考虑组件的可维护性和复用性。

组件构成

一个再复杂的组件,都是由三部分组成的:prop、event、slot,它们构成了 Vue.js 组件的 API

属性prop

prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。

<template>
  <button class="x-button" :class="'x-button-size-' + size" :disabled="disabled">
    <span><slot></slot></span>
  </button>
</template>

<script>
  // 判断参数是否是其中之一
  function oneOf(value, validList){
    /*for(let i = 0 ; i < validList.length ; i++){
      if(value === validList[ i ]){
        return true;
      }
    }*/
    return validList.some(item => item === value.toString());
    // return false;
  }

  export default {
    name: 'myButton',
    props: {
      size: {
        validator(value){
          console.log('---', value);
          return oneOf(value, [ 'small', 'large', 'default' ]);
        },
        type: String,
        default: 'default'
      },
      disabled: {
        type: Boolean,
        default: false
      }
    },
    data(){return {};},
    mounted(){},
    methods: {},
    components: {},
    watch: {},
    beforeDestroy(){}
  };
</script>

<style scoped lang="scss">
  .x-button {
    display: inline-block;
    border: none;
    outline: none;
    /*基础公共的样式*/
    user-select: none;
    padding: 5px 15px 6px;
    font-size: 12px;
    border-radius: 4px;
    cursor: pointer;
  }

  .x-button-size-large {
    padding: 6px 15px;
    font-size: 14px;
    border-radius: 4px;
  }

  .x-button-size-default {

  }

  .x-button-size-small {
    padding: 1px 7px 2px;
    font-size: 12px;
    border-radius: 3px;
  }
</style>

组件中定义了两个属性:尺寸 size 和 是否禁用 disabled。其中 size 使用 validator 进行了值的自定义验证,也就是说,从父级传入的 size,它的值必须是指定的 small、large、default 中的一个,默认值是 default,如果传入这三个以外的值,都会抛出一条警告。

使用

<myButton size="small" :disabled="false">测试</myButton>
<myButton size="default" :disabled="false">default测试</myButton>
<myButton size="large" :disabled="false">测试</myButton>

属性验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtcTR4wW-1581341665189)(Vue特训.assets/1566653497295.png)]

插槽slot

如果要给上面的按钮组件 <mYbutton> 添加一些文字内容,就要用到组件的第二个 API:插槽 slot,它可以分发组件的内容,比如在上面的按钮组件中定义一个插槽:

<button class="x-button" :class="'x-button-size-' + size" :disabled="disabled">
  <span><slot></slot></span>
</button>

当需要多个插槽时,会用到具名 slot,比如上面的组件我们再增加一个 slot,用于设置另一个图标组件:

<template>
  <button :class="'i-button-size' + size" :disabled="disabled">
    <slot name="icon"></slot>
    <slot></slot>
  </button>
</template>

自定义事件

组件通信

ref
p a r e n t / parent/ parent/children
  • ref:给元素或组件注册引用信息;
  • $parent / $children:访问父 / 子实例。

`$parent` 和 `$children` 类似,也是基于当前上下文访问父组件或全部子组件的。
这两种方法的弊端是,无法在**跨级**或**兄弟**间通信


provide / inject

https://cn.vuejs.org/v2/api/#provide-inject](https://cn.vuejs.org/v2/api/#provide-inject)

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

看不懂上面的介绍没有关系,不过上面的这句提示应该明白,就是说 Vue.js 不建议在业务中使用这对 API,而是在插件 / 组件库(比如 iView,事实上 iView 的很多组件都在用)。不过建议归建议,如果你用好了,这个 API 会非常有用。

//父组件
provide(){
      return {
        parent : this,
      };
},
//子组件
inject : [ 'parent' ],//注入,this.parent

注意点

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

$on 与 $emit

$emit会在**当前组件**实例上触发自定义事件,并传递一些参数给监听器的回调,一般来说,都是在父级调用这个组件时,使用@on` 的方式来监听自定义事件的,比如在子组件中触发事件:

// child.vue,部分代码省略
export default {
  methods: {
    handleEmitEvent () {
      this.$emit('test', 'Hello Vue.js');
    }
  }
}

在父组件中监听由 child.vue 触发的自定义事件 test

<!-- parent.vue,部分代码省略-->
<template>
  <child-component @test="handleEvent">
</template>
<script>
  export default {
    methods: {
      handleEvent (text) {
      	console.log(text);  // Hello Vue.js
      }
    }
  }
</script>

这里看似是在父组件 parent.vue 中绑定的自定义事件 test 的处理句柄,然而事件 test 并不是在父组件上触发的,而是在子组件 child.vue 里触发的,只是通过 v-on 在父组件中监听。既然是子组件自己触发的,那它自己也可以监听到,这就要使用 $on 来监听实例上的事件,换言之,组件使用 $emit 在自己实例上触发事件,并用 $on 监听它。

$on 监听了自己触发的自定义事件 test,因为有时不确定何时会触发事件,一般会在 mountedcreated 钩子中来监听。

仅上面的示例,的确是多此一举的,因为大可在 handleEmitEvent 里直接写 window.alert(text),没必要绕一圈。

之所以多此一举,是因为 handleEmitEvent 是当前组件内的 <button> 调用的,如果这个方法不是它自己调用,而是其它组件调用的,那这个用法就大有可为了。

自行实现 dispatch 和 broadcast 方法

虽然 Vue.js 1.x 已经成为过去时,但为了充分理解本节通信方法的使用场景,还是有必要来了解一点它的历史。

在 Vue.js 1.x 中,提供了两个方法:$dispatch$broadcast ,前者用于向上级派发事件,只要是它的父级(一级或多级以上),都可以在组件内通过 $on (或 events,2.x 已废弃)监听到,后者相反,是由上级向下级广播事件的。

先来看下 emitter.js 的代码:

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    const name = child.$options.name;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.name;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

因为是用作 mixins 导入,所以在 methods 里定义的 dispatch 和 broadcast 方法会被混合到组件里,自然就可以用 this.dispatchthis.broadcast 来使用。

这两个方法都接收了三个参数,第一个是组件的 name 值,用于向上或向下递归遍历来寻找对应的组件,第二个和第三个就是上文分析的自定义事件名称和要传递的数据。

可以看到,在 dispatch 里,通过 while 语句,不断向上遍历更新当前组件(即上下文为当前调用该方法的组件)的父组件实例(变量 parent 即为父组件实例),直到匹配到定义的 componentName 与某个上级组件的 name 选项一致时,结束循环,并在找到的组件实例上,调用 $emit 方法来触发自定义事件 eventName。broadcast 方法与之类似,只不过是向下遍历寻找。

使用

// 部分代码省略
import Emitter from '../mixins/emitter.js'

export default {
  mixins: [ Emitter ],
  methods: {
    handleDispatch () {
      this.dispatch();  // ①
    },
    handleBroadcast () {
      this.broadcast();  // ②
    }
  }
}

上例中行 ① 和行 ② 的两个方法就是在导入的混合 emitter.js 中定义的,这个稍后我们再讲,先来分析这两个方法应该传入什么参数。一般来说,为了跟 Vue.js 1.x 的方法一致,第一个参数应当是自定义事件名,比如 “test”,第二个参数是传递的数据,比如 “Hello, Vue.js”,但在这里,有什么问题呢?只通过这两个参数,我们没办法知道要在哪个组件上触发事件,因为自行实现的这对方法,与 Vue.js 1.x 的原生方法机理上是有区别的。上文说到,实现这对方法的关键点在于准确地找到组件实例。那在寻找组件实例上,我们的“惯用伎俩”就是通过遍历来匹配组件的 name 选项,在独立组件(库)里,每个组件的 name 值应当是唯一的,name 主要用于递归组件,在后面小节会单独介绍。

来看一下具体的使用方法。有 A.vueB.vue 两个组件,其中 B 是 A 的子组件,中间可能跨多级,在 A 中向 B 通信:

<!-- A.vue -->
<template>
	<button @click="handleClick">触发事件</button>
</template>
<script>
  import Emitter from '../mixins/emitter.js';
  
  export default {
    name: 'componentA',
    mixins: [ Emitter ],
    methods: {
      handleClick () {
        this.broadcast('componentB', 'on-message', 'Hello Vue.js');
      }
    }
  }
</script>
// B.vue
export default {
  name: 'componentB',
  created () {
    this.$on('on-message', this.showMessage);
  },
  methods: {
    showMessage (text) {
      window.alert(text);
    }
  }
}

同理,如果是 B 向 A 通信,在 B 中调用 dispatch 方法,在 A 中使用 $on 监听事件即可。

以上就是自行实现的 dispatch 和 broadcast 方法,相比 Vue.js 1.x,有以下不同:

  • 需要额外传入组件的 name 作为第一个参数;
  • 无冒泡机制;
  • 第三个参数传递的数据,只能是一个(较多时可以传入一个对象),而 Vue.js 1.x 可以传入多个参数,当然,你对 emitter.js 稍作修改,也能支持传入多个参数,只是一般场景传入一个对象足以。

找到任意组件实例——findComponents 系列方法

findComponents 系列方法,它并非 Vue.js 内置,而是需要自行实现,以工具函数的形式来使用,它是一系列的函数,可以说是组件通信的终极方案。findComponents 系列方法最终都是返回组件的实例,进而可以读取或调用该组件的数据和方法。

它适用于以下场景:

  • 由一个组件,向上找到最近的指定组件;
  • 由一个组件,向上找到所有的指定组件;
  • 由一个组件,向下找到最近的指定组件;
  • 由一个组件,向下找到所有指定的组件;
  • 由一个组件,找到指定组件的兄弟组件。

5 个不同的场景,对应 5 个不同的函数,实现原理也大同小异。

向上找到最近的指定组件——findComponentUpward
// assist.js
// 由一个组件,向上找到最近的指定组件
function findComponentUpward (context, componentName) {
  let parent = context.$parent;
  let name = parent.$options.name;

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}
export { findComponentUpward };

findComponentUpward 接收两个参数,第一个是当前上下文,比如你要基于哪个组件来向上寻找,一般都是基于当前的组件,也就是传入 this;第二个参数是要找的组件的 name

findComponentUpward 方法会在 while 语句里不断向上覆盖当前的 parent 对象,通过判断组件(即 parent)的 name 与传入的 componentName 是否一致,直到直到最近的一个组件为止。

使用

<script>
  import { findComponentUpward } from '../utils/assist.js';

  export default {
    name: 'componentB',
    mounted () {
      const comA = findComponentUpward(this, 'componentA');
      
      if (comA) {
        console.log(comA.name);  // Aresn
        comA.sayHello();  // Hello, Vue.js
      }
    }
  }
</script>

使用起来很简单,只要在需要的地方调用 findComponentUpward 方法就行,第一个参数一般都是传入 this,即当前组件的上下文(实例)。

上例的 comA,保险起见,加了一层 if (comA) 来判断是否找到了组件 A,如果没有指定的组件而调用的话,是会报错的。

findComponentUpward 只会找到最近的一个组件实例,如果要找到全部符合要求的组件,就需要用到下面的这个方法。

向上找到所有的指定组件——findComponentsUpward
// assist.js
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward (context, componentName) {
  let parents = [];
  const parent = context.$parent;

  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent);
    return parents.concat(findComponentsUpward(parent, componentName));
  } else {
    return [];
  }
}
export { findComponentsUpward };

与 findComponentUpward 不同的是,findComponentsUpward 返回的是一个数组,包含了所有找到的组件实例(注意函数名称中多了一个“s”)。

findComponentsUpward 的使用场景较少,一般只用在递归组件里面(后面小节会介绍),因为这个函数是一直向上寻找父级(parent)的,只有递归组件的父级才是自身。事实上,iView 在使用这个方法也都是用在递归组件的场景,比如菜单组件 Menu。由于递归组件在 Vue.js 组件里面并不常用,那自然 findComponentsUpward 也不常用了。

向下找到最近的指定组件——findComponentDownward

代码如下:

// assist.js
// 由一个组件,向下找到最近的指定组件
function findComponentDownward (context, componentName) {
  const childrens = context.$children;
  let children = null;

  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name;

      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}
export { findComponentDownward };

context.$children 得到的是当前组件的全部子组件,所以需要遍历一遍,找到有没有匹配到的组件 name,如果没找到,继续递归找每个 $children 的 $children,直到找到最近的一个为止。

向下找到所有指定的组件——findComponentsDownward
// assist.js
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward (context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) components.push(child);
    const foundChilds = findComponentsDownward(child, componentName);
    return components.concat(foundChilds);
  }, []);
}
export { findComponentsDownward };

这个函数实现的方式有很多,这里巧妙使用 reduce 做累加器,并用递归将找到的组件合并为一个数组并返回,代码量较少,但理解起来稍困难。

用法与 findComponentDownward 大同小异

找到指定组件的兄弟组件——findBrothersComponents
// assist.js
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents (context, componentName, exceptMe = true) {
  let res = context.$parent.$children.filter(item => {
    return item.$options.name === componentName;
  });
  let index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}
export { findBrothersComponents };

相比其它 4 个函数,findBrothersComponents 多了一个参数 exceptMe,是否把本身除外,默认是 true。寻找兄弟组件的方法,是先获取 context.$parent.$children,也就是父组件的全部子组件,这里面当前包含了本身,所有也会有第三个参数 exceptMe。Vue.js 在渲染组件时,都会给每个组件加一个内置的属性 _uid,这个 _uid 是不会重复的,借此我们可以从一系列兄弟组件中把自己排除掉。

举个例子,组件 A 是组件 B 的父级,在 B 中找到所有在 A 中的兄弟组件(也就是所有在 A 中的 B 组件):

<!-- component-a.vue -->
<template>
  <div>
    组件 A
    <component-b></component-b>
  </div>
</template>
<script>
  import componentB from './component-b.vue';
  
  export default {
    name: 'componentA',
    components: { componentB }
  }
</script>
<!-- component-b.vue -->
<template>
  <div>
    组件 B
  </div>
</template>
<script>
  import { findBrothersComponents } from '../utils/assist.js';
  
  export default {
    name: 'componentB',
    mounted () {
      const comsB = findBrothersComponents(this, 'componentB');
      console.log(comsB);  // ① [],空数组
    }
  }
</script>

在 ① 的位置,打印出的内容为空数组,原因是当前 A 中只有一个 B,而 findBrothersComponents 的第三个参数默认是 true,也就是将自己除外。如果在 A 中再写一个 B:

<!-- component-a.vue -->
<template>
  <div>
    组件 A
    <component-b></component-b>
    <component-b></component-b>
  </div>
</template>

这时就会打印出 [VueComponent],有一个组件了,但要注意在控制台会打印两遍,因为在 A 中写了两个 B,而 console.log 是在 B 中定义的,所以两个都会执行到。如果你看懂了这里,那应该明白打印的两遍 [VueComponent],分别是另一个 <component-b>(如果没有搞懂,要仔细琢磨琢磨哦)。

如果将 B 中 findBrothersComponents 的第三个参数设置为 false:

// component-b.vue
export default {
  name: 'componentB',
  mounted () {
    const comsB = findBrothersComponents(this, 'componentB', false);
    console.log(comsB);
  }
}

此时就会打印出 [VueComponent, VueComponent],也就是包含自身了。

以上就是 5 个函数的详细介绍,get 到这 5 个,以后就再也不用担心组件通信了。

Vue组件间的多种通信

props数据传递

父向子传递数据通过props

1-父组件直接传递数据

<template>
  <div class="parent">
    <!--父组件向子组件传递a = 100-->
    <son1 :a="100"></son1>
  </div>
</template>
========================================================
<template>
  <div class="son1">
    <p>父组件传递 a = {{ a }}</p>
  </div>
</template>

<script>
  export default {
    name : 'son1',
    props : [ 'a' ],//父组件传递
    data(){return {};},
    mounted(){},
    methods : {},
    components : {},
    watch : {},
    beforeDestroy(){},
  };
</script>

$emit使用

1 - props数据实现双向绑定-结合$emit
=====父组件
<div class="parent">
    <Divider>parent父组件</Divider>
    <!--传递动态数据-->
    <son1 :a.sync="a" @changeA="changeA"></son1>
    <p>父组件: <Input v-model="a" number/></p>
  </div>
  
methods : {
      changeA(value){
        this.a = value;
      },
},
    
=====子组件
  <div class="son1">
    <Divider>son1子组件</Divider>
    <p>父组件传递过来数据 a = {{ a }}</p>
    <p>子组件son1 <Input v-model="b" number @input="handleChangeB"/></p>
  </div>

<script>
  export default {
    name : 'son1',
    props : {
      a : {
        type : Number,
        default : 0,
        required : true,
      },
      changeA : {
        type : Function,
      },
    },//父组件传递
    data(){
      return {
        b : this.a,
      };
    },
    methods : {
      handleChangeB(){
        //实现父子组件的数据双向绑定
        this.$emit('changeA', this.b);
      },
    },
  };
</script>

2 - .sync修饰符
<son1 :a.sync="a"></son1>


子组件触发
this.$emit('update:a', this.b);//.sync父子组件双向数据通信
3 - v-model
<!--v-model只可以传递value值,适用性没有.sync广泛-->
<son1 v-model="value"></son1>


//子组件触发
this.$emit('input', this.b);

v-model 就是value和input的语法糖

p a r e n t / parent/ parent/children/ d i s p a t c h / dispatch/ dispatch/broadcast

methods:{
        changeParent(){ 
            // eventBus
            // $dispatch 只会通知自己的父亲 
            this.$dispatch('input',200)
        }
    },
// 向上通知
Vue.prototype.$dispatch = function(eventName,value){
    let parent = this.$parent;
    while(parent){
        parent.$emit(eventName,value);
        parent = parent.$parent
    }
}
// 向下传递
Vue.prototype.$broadcast = function(eventName,value){
     // 获取当前组件下的所有的孩子
     const broadcast = (children) =>{
        children.forEach(child => {
            child.$emit(eventName,value);
            if(child.$children){
                broadcast(child.$children);
            }
         });
     }
     broadcast(this.$children);
   
}

a t t r s / attrs/ attrs/listeners

$attrs:属性的集合

$listeners方法的集合

inheritAttrs : false,//属性不挂载在DOM结构上

//parent
<son2 name="李四" age="26岁" gender="meal" @son="testSon"></son2>


//son2
<template>
  <section class="son2">
    <p>son2 </p>
    <grandSon2 xxx='xxx' v-bind="$attrs" v-on="$listeners"></grandSon2>
  </section>
</template>

//孙子grandSon2
<template>
  <div class="grandSon2">
    <h1>grandSon2</h1>
    <p>{{ $attrs }}</p>
  </div>
</template>

Provide / Inject

所有的子组件都可以使用

数据的注入

父组件

provide(){
  return {
    parent : this,
  };
},

子组件使用父组件数据

inject : [ 'parent' ],//注入,this.parent

调用父组件的数据
<p>{{ this.parent.pA }}</p>

数据会变为全局的数据 , 一般不是很推荐使用

Ref使用

父组件

<son2 name="李四" age="26岁" gender="meal" @son="testSon" ref="son2"></son2>

this.$nextTick(() => {
        console.log(this.$refs.son2.son2);
        this.$refs.son2.sonTest();
});

EventBus使用

//EventBus的使用,直接挂载在Vue的原型上
Vue.prototype.$bus = new Vue(); //$on $emit
mounted(){
  this.$bus.$on('son1', (params) => {
    console.log('son1的$bus挂载的方法', params);
  });
},
this.$nextTick(() => {
  console.log('======= granSon2');
  this.$bus.$emit('son1', 'granSon2');
});

插槽

函数式模板

Vuex通信

数据渲染

render函数值JSX应用

函数式组件 , 没有模板必须有一个render函数

///函数式组件
export default {
  props : {
    t : {},
  },
  render(h){//createElement
    let tag = 'h' + this.t;
    return <tag>{this.$slots.default}</tag>
  },
};


//使用
<div class="jsx">
    <Divider>jsx</Divider>
    <Leave :t="1">标题1</Leave>
    <Leave :t="2">标题2</Leave>
    <Leave :t="3">标题3</Leave>
    <Leave :t="4">标题4</Leave>
    <Leave :t="5">标题5</Leave>
    <Leave :t="6">标题6</Leave>
  </div>

列表循环的原始写法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fiye38Ie-1581341665193)(assets/1566059177219.png)]

//父组件
<list :data="['香蕉','鸭梨','西瓜']"></list>

<section>
    <template v-for="(item,index) in data">
      <!--缺点: li标签的可定制性不强-->
      <li :key="index">{{ item }}</li>
    </template>
  </section>

JSX写法

//父组件
<list :data="['香蕉','鸭梨','西瓜']" :render="renderAction"></list>
methods : {
      renderAction(h, data){
        console.log(data);//data为每一个参数数据
        return <span>{ data }</span>;
      },
 },


//子组件
<section>
    <template v-for="(item,index) in data">
      <listItem :key="'a'+index" v-if="render" :render="render" :item="item"></listItem>
      <!--缺点: li标签的可定制性不强-->
      <li :key="index" v-else>{{ item }}</li>
    </template>
  </section>
  
//函数式组件
export default {
  props : {
    item : {//数据
      type : String,
    },
    render : {//用户自定义的处理函数
      type : Function,
    },
  },
  render(h){
    //return this.render(h, this.item);
    
    //也可以直接自定义render
    return [ h('div', {
      style : {
        background : 'rgba(153,153,153,0.42)',
      },
    }, [
      h('p', {
        style : {
          color : 'red',
        },
      }, this.item),
      h('Tag', {
        props : {
          color : 'primary',
        },
      }, this.item),
    ]),
    ];
  },
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4kCASiz8-1581341665194)(assets/1566060673778.png)]

插槽slot

JWT认证

什么是jwt

服务端返回token

路由配置

axios封装

接口测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mw9QQPJW-1581341665195)(assets/1566038161017.png)]

级联组件的编写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-COaLEYt4-1581341665197)(assets/1566038171520.png)]

Vue单元测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aMbcy4Pr-1581341665198)(assets/1566038192007.png)]

Vue权限菜单及按钮权限设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FM9n71iA-1581341665200)(assets/1566038236099.png)]

Vue原理剖析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmDvffUg-1581341665200)(assets/1566038339711.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Reiq1LJv-1581341665201)(assets/1566038351159.png)]

Vue优化

编码优化

data属性
  1. 不要将和视图无关的数据存放在data ,
  2. data中的数据都会增加get特人和setter , 会手机watcher
SPA页面采用keep-alive缓存组件

keep-alive可以实现组件的缓存功能 , 缓存当前组件的实例

1、keep-alive 用法(官方):

Props:

include - 字符串或正则表达式。只有匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何匹配的组件都不会被缓存。

用法:

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

2、当一个组件a.vue被缓存的时候,那么第一次加载这个组件的时候,会执行组件的所有周期函数created()、mounted()等等,但是第二次打开a组件时,就不会触发这些生命周期钩子函数,但是会触发activated 和 deactivated这两个钩子函数(因为被缓存了);

keep-alive源码

/* @flow */

import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

拆分组件

提高复用性 / 增加在吗的可维护性

减少不必要的渲染 (尽可能细化拆分组件)

v-if

当值为false的时候内部指令不执行 , 具有阻断功能 , 很多情况下使用v-if代替v-show

异步组件: https://github.com/Coffcer/Blog/issues/3

这是我推荐的一种做法,简单有效。还是那个结构,我们给要延迟渲染的组件加上v-if:

<app>
    <A></A>
    <B v-if="showB"></B>
    <C v-if="showC"></C>
</app>
new Vue({
    data: {
        showB: false,
        showC: false
    },
    created () {
        // 显示B
        setTimeout(() => {
            this.showB = true;
        }, 0);
        // 显示C
        setTimeout(() => {
            this.showC = true;
        }, 0);
    }
});

这个示例写起来略显啰嗦,但它已经实现了我们想要的顺序渲染的效果。页面会在A组件初始化完后显示,然后再按顺序渲染其余的组件,整个页面渲染方式看起来是流式的。

有些人可能会担心v-if存在一个编译/卸载过程,会有性能影响。但这里并不需要担心,因为v-if是惰性的,只有当第一次值为true时才会开始初始化。

这种写法看起来很麻烦,如果我们能实现一个类似v-if的组件,然后直接指定多少秒后渲染,那就更好了,例如:

<app>
    <A></A>
    <B v-lazy="0"></B>
    <C v-lazy="100"></C>
</app>

一个简单的指令即可,不需要js端任何配合,并且可以用在普通dom上面,Nice!

在vue里,类似v-ifv-for这种是terminal指令,会在指令内部编译组件。如果你想要自己实现一个terminal指令,需要加上terminal: true,例如:

Vue.directive('lazy', {
    terminal: true,
    bind () {},
    update () {},
    unbind () {}
});

这是vue在1.0.19+新增的功能,由于比较冷门,文档也没有特别详细的叙述,最好的方式是参照着v-ifv-for的源码来写。

我已经为此封装了一个terminal指令,你可以直接使用:
https://github.com/Coffcer/vue-lazy-component

key保证唯一性
  • 默认vue会采用就地复用策略
  • 如果数据项顺序呗改变 , Vue不会移动DOM元素用来匹配数据项顺序
  • 应该使用数据ID作为key属性

如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

不要使用对象或数组之类的非基本类型值作为 v-forkey。请用字符串或数值类型的值。

Object.freeze

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略enumerablefalse的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

如果真的想将对象冻结,应该使用Object.freeze方法。

路由懒加载 , 异步组件

动态加载组件 ,依赖 webpack-codespliting 功能

export default new Router({
  routes : [
    ...testRouter,
    {
      path : '/',
      component : () => import('../views/main'),
    }, {
      path : '/sw',
      name : 'sw',
      component : () => import('../views/sw'),
    }
});

https://zhuanlan.zhihu.com/p/26710831

动态导入组件

imprt Dialog from "./Dialog"

export default{
	components:{
		Dialog:()=>import("./Dialog")
	}


}
runtime运行时

开发时尽量使用单文件的方式 , vue在webpack打包时会执行模板的转化

数据持久化的问题

vuex-persist 合理使用 (防抖/节流)

vue加载性能优化

第三方模块按需导入 (babel-plugin-component)

图片懒加载 滚动到可视区域动态加载

滚动渲染可视化区域 数据较大时只渲染可视区域(计算scroolTop)

用户体验

app-skeleton

配置webpack插件 vue-skeleton-webpack-plugin

单页骨架屏

app-shell

pwa manifest serviceWorker

百度课堂

http://bit.baidu.com/course/datalist/column/124.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IjYwCGzp-1581341665203)(Vue特训.assets/1566225340517.png)]

SEO优化方案

vue的预渲染插件

什么是服务端渲染

webpack打包优化

解决方法很简单,打包 vender 时不打包 vue、vuex、vue-router、axios 等,换用国内的 bootcdn 直接引入到根目录的 index.html 中。

<script src="//cdn.bootcss.com/vue/2.2.5/vue.min.js"></script>
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>
<script src="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
<script src="//cdn.bootcss.com/axios/0.15.3/axios.min.js"></script>

在 webpack 里有个 externals,可以忽略不需要打包的库

externals: {
  'vue': 'Vue',
  'vue-router': 'VueRouter',
  'vuex': 'Vuex',
  'axios': 'axios'
}

多线程打包 happypack

splitChunks抽离公共文件

sourceMap的配置

缓存/压缩

服务端缓存/ 客户端缓存

服务端gzip压缩

参考

https://juejin.im/post/5c179dcc51882521eb44a3b4

https://juejin.im/post/5cc81076e51d456e361ed97e

Vue+TS开发应用

服务端部署

持续集成和部署

技术栈:前台Vue 后台Nodejs

服务器 前台nginx 后台Nodejs

jenkins持续交付流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8XuO80ld-1581341665204)(Vue特训.assets/16519d61b8d7710e-1566228124825)]

编写后端服务

前端项目

CICD服务器

配置服务器

后台部署

前台部署

Vue组件化开发

Vue源码实现

Render

什么是虚拟DOM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8MU8HEj3-1581341665205)(assets/1566151013750.png)]

vNode和普通DOM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WHP6s4ls-1581341665206)(assets/1566151032038.png)]

Vue2的虚拟DOM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tUiII9du-1581341665207)(assets/1566151091744.png)]

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}

其几个核心的关键属性就差不多了,例如:

  • tag 属性即这个vnode的标签属性
  • data 属性包含了最后渲染成真实dom节点后,节点上的classattributestyle以及绑定的事件
  • children 属性是vnode的子节点
  • text 属性是文本属性
  • elm 属性为这个vnode对应的真实dom节点
  • key 属性是vnode的标记,在diff过程中可以提高diff的效率
vNode类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EfbsoxfH-1581341665207)(assets/1566151124688.png)]

环境构建

Eslint代码检查

// http://eslint.org/docs/user-guide/configuring

module.exports = {
  // 将 ESLint 限制到一个特定的项目,在配置文件里设置 "root": true。ESLint 一旦发现配置文件中有 "root": true,它就会停止在父级目录中寻找。
  root: true,
  // 检测ES6代码
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  // 
  env: {
    browser: true,
  },
  // 消除no-undef影响
  globals: {
    _: true
  },
  // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
  extends: 'standard',
  // required to lint *.vue files
  plugins: [
    'vue',
    'html'
  ],
  // add your custom rules here
//   0或’off’:关闭规则。 
// 1或’warn’:打开规则,并且作为一个警告(并不会导致检查不通过)。 
// 2或’error’:打开规则,并且作为一个错误 (退出码为1,检查不通过)。

// 参数说明: 
// 参数1 : 错误等级 
// 参数2 : 处理方式
  'rules': {
    'prefer-promise-reject-errors': 0,
    'space-unary-ops': 0,
    'no-unused-expressions': 0,
    'no-useless-return': 0,
    'standard/no-callback-literal': 0,
    'import/first': 0,
    'import/export': 0,
    'no-mixed-operators': 0,
    'no-use-before-define': 0,
    // 允许使用分号
    'semi': [0, 'never'],
    // 允许使用==
    'eqeqeq': 0,
    // 缩进使用不做限制
    'indent': 0,
    // 允许使用tab
    'no-tabs': 0,
    // 函数圆括号之前没有空格
    'space-before-function-paren': [2, "never"],
    // 不要求块内空格填充格式
    'padded-blocks': 0,
    // 不限制变量一起声明
    'one-var': 0,
    // debugger使用
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
    // 开发模式允许使用console
    'no-console': 0,
    // 条件语句中复制操作符需要用圆括号括起来
    'no-cond-assign': [2, 'except-parens'],
    // 允许使用条件表达式使用常量
    'no-constant-condition': 0,
    // 单行可忽略大括号,多行不可忽略
    'curly': [2, 'multi-line'],
    // 不允许使用var变量
    'no-var': 2,
    // 不允许出现多个空格
    'no-multi-spaces': ["error", { ignoreEOLComments: true }],
    'camelcase': 0,
    // 对象字面量的键值空格风格
    'key-spacing': 2,
    // if语句包含一个return语句, else就多余
    'no-else-return': 2,
    // 建议将经常出现的数字提取为变量
    'no-magic-numbers': [0, {ignoreArrayIndexes: true}],
    // 不允许重复声明变量
    'no-redeclare': [2, {builtinGlobals: true}],
    // 立即执行函数风格
    'wrap-iife': [2, 'inside'],
    // 不允许圆括号中出现空格
    'space-in-parens': [2, 'never'],
    // 确保运算符周围有空格
    'space-infix-ops': 2,
    // 强制点号与属性同一行
    'dot-location': [2, 'property'],
    // 强制单行代码使用空格
    'block-spacing': [2, 'always'],
    // 约束for-in使用hasOwnProperty判断
    'guard-for-in': 0,
    // 采用one true brace style大括号风格
    'brace-style': [2, '1tbs', {'allowSingleLine': true}],
    // 统一逗号周围空格风格
    'comma-spacing': [2, {'before': false, 'after': true}],
    // 禁止出现多个空行
    'no-multiple-empty-lines': [2, {'max': 1, 'maxEOF': 2}],
    // 允许箭头函数不使用圆括号
    'arrow-parens': 0,
    // 规范generator函数的使用
    'generator-star-spacing': [2, {'before': false, 'after': true}],
    // 要求在块级
    'lines-around-comment': [2, {'beforeBlockComment': true, 'afterBlockComment': false, 'beforeLineComment': true, 'afterLineComment': false}]
  }
}

webpack多线程打包

parallel: require('os').cpus().length > 1, 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nElWcBTE-1581341665209)(Vue特训.assets/1566659087789.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值