{T}

Vue.js 开发技巧

编码技巧与规范开始

1-使用对象代替 if 及 switch

在很多情况下,经常会遇到循环判断执行赋值操作的场景,一般都会使用 if 及 switch 的条件判断,如果符合则执行赋值,不符合则进入下个判断,比如:

javascript
let name = 'lisi';
let age = 18;

if (name === 'zhangsan') {
  age = 21;
} else if (name === 'lisi') {
  age = 18;
} else if (name === 'wangwu') {
  age = 12;
}

// 或者
switch(name) {
  case 'zhangsan':
    age = 21;
    break
  case 'lisi':
    age = 18;
    break
  case 'wangwu':
    age = 12;
    break
}

这样的写法不仅冗余,而且代码执行效率不高,可以使用对象的形式简写:

javascript
let name = 'lisi';
let obj = {
    zhangsan: 21,
    lisi: 18,
    wangwu: 12
};

let age = obj[name] || 18;

这种技巧适用于循环判断一次赋值的情况,如果判断过后有较多处理逻辑的还需要使用 if 或 switch 等方法。

2-Array.from 快速生成数组

生成一个有规律的数组会使用循环插入的方法,比如使用时间选择插件时,可能需要将小时数存放在数组中:

javascript
let hours = [];

for (let i = 0; i < 24; i++) {
  hours.push(i + '时');
}

如果使用 Array.from 可以简写为:

javascript
let hours = Array.from({ length: 24 }, (value, index) => index + '时');

3-router.beforeEach 来处理跳转前逻辑

在某些情况下,需要在路由跳转前处理一些特定的业务逻辑,比如修改路由跳转、设置 title 等,代码如下:

javascript
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

let base = `${process.env.BASE_URL}`;

let router =  new Router({
  mode: 'history',
  base: base,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: { title: '首页' }
    },
  ]
})

router.beforeEach((to, from, next) => {
  let title = to.meta && to.meta.title;

  if (title) {
    document.title = title; // 设置页面 title
  }

  if (to.name === 'home') {

    // 拦截并跳转至 page2 单页,$openRouter 方法在第 5 节中封装
    Vue.$openRouter({
      name: 'page2'
    });
  }

  next();
})

export default router

注意最后需要调用 next() 方法执行路由跳转

4-路由跳转尽量使用 name 而不是 path

前期配置的路由路径后期难免会进行修改,如果页面跳转的地方全是使用的 path,那么需要修改所有涉及该 path 的页面,这样不利于项目的维护。

而相对于 path,name 具有唯一性,即使修改了 path,还可以使用原来的 name 值进行跳转

javascript
this.$router.push({ 
  name: 'page1'
});

// 而不是
this.$router.push({ 
  path: 'page1'
});

5-使用 computed 代替 watch

很多时候页面会出现 watch 的滥用而导致一系列问题的产生,而通常更好的办法是使用 computed 属性,首先需要区别它们有什么区别:

  • watch:当监测的属性变化时会自动执行对应的回调函数
  • computed:计算的属性只有在它的相关依赖发生改变时才会重新求值
html
<template>
  <div>
    <input type="text" v-model="firstName">
    <input type="text" v-model="lastName">
    <span>{{ fullName }}</span>
    <span>{{ fullName2 }}</span>
  </div>
</template>

<script>
  export default {
    data() {
      reurn {
        firstName: '',
          lastName: '',
            fullName2: ''
      }
    },

    // 使用 computed
    computed: {
      fullName() {
        return this.firstName + ' ' + this.lastName
      }
    },

    // 使用 watch
    watch: {
      firstName: function(newVal, oldVal) {
        this.fullName2 = newVal + ' ' + this.lastName;
      },
      lastName: function(newVal, oldVal) {
        this.fullName2 = this.firstName + ' ' + newVal;
      },
    }
  }
</script>

通过对比可以看到,在处理多数据联动的情况下,使用 computed 会更加合理一点。

computed 监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算;而 watch 监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作

6-统一管理缓存变量

在项目中或多或少会使用浏览器缓存,比如 sessionStorage 和 localStorage,当一个项目中存在很多这样的缓存存取情况的时候就会变得难以维护和管理,因为其就像全局变量一样散落在项目的各个地方,这时候应该将这些变量统一管理起来,放到一个或多个文件中去,比如:

javascript
/* types.js */

export const USER_NAME = 'userName';
export const TOKEN = 'token';

在需要存取的时候,直接引用:

javascript
import { USER_NAME, TOKEN } from '../types.js'

sessionStorage[USER_NAME] = '张三';
localStorage[TOKEN] = 'xxx';

使用这种方法的好处在于一旦需要修改变量名,直接修改管理文件中的值即可,无需修改使用它的页面,同时这也可以避免命名冲突等问题的出现,这类似于 vuex 中 mutations 变量的管理

7-使用 setTimeout 代替 setInterval

在项目里不建议使用 setInterval,因为其会存在代码的执行间隔比预期小以及 “丢帧” 的现象,原因在于其本身的实现逻辑。很多人会认为 setInterval 中第二个时间参数的作用是经过该毫秒数执行回调方法,其实不然,其真正的作用是经过该毫秒数将回调方法放置到队列中去,但是如果队列中存在正在执行的方法,其会等待之前的方法完毕再执行,如果存在还未执行的代码实例,其不会插入到队列中去,也就产生了 “丢帧”

而 setTimeout 并不会出现这样的现象,因为每一次调用都会产生了一个新定时器,同时在前一个定时器代码执行完之前,不会向队列插入新的定时器代码。

javascript
// 该定时器实际会在 3s 后立即触发下一次回调
setInterval(() => {
  // 执行完这里的代码需要 2s
}, 1000);

// 使用 setTimeout 改写,4秒后触发下一次回调
let doSometing = () => {
  // 执行完这里的代码需要 2s

  setTimeout(doSometing, 1000);
}

doSometing();

延伸阅读:对于“不用setInterval,用setTimeout”的理解

8-不要使用 for in 循环来遍历数组

都知道 for in 循环是用于遍历对象的,但它可以用来遍历数组吗?答案是可以的,因为数组在某种意义上也是对象,但是如果用其遍历数组会存在一些隐患:其会遍历数组原型链上的属性。

javascript
let arr = [1, 2];

for (let key in arr) {
    console.log(arr[key]); // 会正常打印 1, 2
}

// 但是如果在 Array 原型链上添加一个方法
Array.prototype.test = function() {};

for (let key in arr) {
    console.log(arr[key]); // 此时会打印 1, 2, ƒ () {}
}

因为不能保证项目代码中不会对数组原型链进行操作,也不能保证引入的第三方库不对其进行操作,所以不要使用 for in 循环来遍历数组

编写可复用性模块

在 Vue 项目中,每一个页面都可以看作是由大大小小的模块构成的,即便是一行代码、一个函数、一个组件都可以看作是一个个自由的模块。

下面代码中出现重复性的代码片段 data.obj.items

javascript
let person = [];

for (let i = 0; i < data.obj.items.length; i++) {
  person.push({
    name: data.obj.items[i].name,
    age: data.obj.items[i].age
  });
}

发现重复的地方就相当于找到可以复用的模块。在不复用的情况下,上述代码一旦需要修改变量 itemslists,那么就得修改 3 处地方,不知不觉就增加了维护成本。而到时候往往修改你代码的人并不是你自己,所以对自己好点,对他人也会好点。复用后的代码如下:

javascript
let person = [];
let values = data.obj.items;

for (let i = 0; i < values.length; i++) {
  person.push({
    name: values[i].name,
    age: values[i].age
  });
}

封装成函数

除了使用变量的赋值缓存使用来解决数据的重复读取外,在开发过程中重复性更多的也许是功能点的重复,比如:

html
<tempalte>
  <div>
    <input type="text" v-model="str1">
    <input type="text" v-model="str2">
    <div>{{ str1.slice(1).toUpperCase() }}</div>
    <div>{{ str2.slice(1).toUpperCase() }}</div>
  </div>
 </template>

需要对其进行优化,这里可以把功能点封装成一个函数:

javascript
export default {
  methods: {
    sliceUpperCase(val) {
      return val.slice(1).toUpperCase()
    }
  }
}

只要在用到该方法的地方调用即可,将值传入其中并返回新值。当然像在双花括号插值和 v-bind 表达式中重复的功能点我们可以封装成过滤器比较合适:

javascript
// 单文件组件注册过滤器
filters: {
    sliceUpperCase(val) {
        return val.slice(1).toUpperCase()
    }
}

// 全局注册过滤器
Vue.filter('sliceUpperCase', function (val) {
    return val.slice(1).toUpperCase()
})

然后在 html 中使用“管道”符进行过滤,过滤器本质都是函数的封装

html
<div>{{ str1 | toUpperCase }}</div>
<div>{{ str2 | toUpperCase }}</div>

封装成组件

封装组件主要包含两种方式,一种是最常见的整体封装,用户通过改变数据源来呈现不同的页面状态,代码结构不可定制化。例如:

html
<div>
  <my-component data="我是父组件传入子组件的数据"></my-component>
</div>

另一种便是自定义封装,也就是插槽(slot),可以开放一部分槽位给父组件,使其能够进行一定程度的定制化,例如:

html
<div>
  <my-component data="我是父组件传入子组件的数据">
    <template slot="customize">
      <span>这是定制化的数据</span>
    </template>
  </my-component>
</div>

在 myComponent 组件中我们便可以接收对应的 slot:

html
<div class="container">
  <span>{{ data }}</span>
  <slot name="customize"></slot>
  <div>

通过定义 slot 标签的 name 值为 customize 来接收父组件在使用该组件时在 template 标签上定义的 slot="customize" 中的代码,不同父组件可以定制不同的 slot 代码来实现差异化的插槽。最终渲染出来的代码如下:

html
<div>
    <div class="container">
        <span>我是父组件传入子组件的数据</span>
        <span>这是定制化的数据</span>
    </div>
</div>

这样我们就完成了一个小型组件的封装,将共用代码封装到组件中去,页面需要引入的时候直接使用 import 并进行相应注册即可,当然你也可以进行全局的引入:

javascript
import myComponent from '../myComponent.vue'

// 全局
Vue.component('my-component', myComponent)

封装成插件

Vue 提供 install 方法来编写插件,使用该方法中的第一个 Vue 构造器参数可以为项目添加全局方法、资源、选项等。比如可以给组件添加一个简单的全局调用方法来实现插件的编写:

javascript
/* toast.js */
import ToastComponent from './toast.vue' // 引入组件

let $vm

export default {    
  install(Vue, options) {

    // 判断实例是否存在
    if (!$vm) {            
      const ToastPlugin = Vue.extend(ToastComponent); // 创建一个“扩展实例构造器”

      // 创建 $vm 实例
      $vm = new ToastPlugin({                
        el: document.createElement('div')  // 声明挂载元素          
      });            

      document.body.appendChild($vm.$el); // 把 toast 组件的 DOM 添加到 body 里
    } 

    // 给 toast 设置自定义文案和时间
    let toast = (text, duration) => {
      $vm.text = text;
      $vm.duration = duration;

      // 在指定 duration 之后让 toast 消失
      setTimeout(() => {
        $vm.isShow = false;  
      }, $vm.duration);
    }

    // 判断 Vue.$toast 是否存在
    if (!Vue.$toast) {            
      Vue.$toast = toast;        
    }        

    Vue.prototype.$toast = Vue.$toast; // 全局添加 $toast 事件
  }
}

成功编写完插件的 JS 脚本后,在入口文件中需要通过 Vue.use() 来注册一下该插件:

javascript
import Toast from '@/widgets/toast/toast.js'

Vue.use(Toast); // 注册 Toast

最后在需要调用它的地方直接传入配置项使用即可,比如:

javascript
this.$toast('Hello World', 2000);

当然也可以不使用 install 方法来编写插件,直接采用导出一个封装好的实例方法并将其挂载到 Vue 的原型链上来实现相同的功能