前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue编码技巧与规范 九条能落到代码里的实践

首页2019-06-02 00:25:32Front-End
Vue编码规范前端优化

review 别人的 Vue 代码,最常挑出来的不是什么高深问题,翻来覆去就那么几类:一串写了七八个分支的 if else、v-for 上挂着 :key="index"、一个 watch 追着另一个 watch 改数据、路由跳转里散落着写死的 path。这些写法都能跑,也都不会报错,但它们会在项目做大之后一点点变成维护负担。这篇整理九条我自己在项目里反复用到的编码技巧,每一条都讲清它解决什么问题、什么时候适用、什么时候反而不该用。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 用对象映射代替一长串 if 和 switch,以及它的适用边界
  • Array.from 快速生成规律数组
  • router.beforeEach 里做跳转拦截的正确姿势,以及一个很容易写出的死循环
  • 用 v-if 做按需渲染,它和 v-show 到底该怎么选
  • 路由跳转为什么优先用 name 而不是 path
  • v-for 的 key 拿什么当值,用 index 会出什么问题
  • computed 和 watch 的分工
  • 缓存变量统一管理,以及别用 for in 遍历数组

# 一、用对象映射代替 if 和 switch

循环判断然后赋值,是业务代码里出现频率极高的一类场景。一般的写法是这样:

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
}
@前端进阶之旅: 代码已经复制到剪贴板

分支一多,这段代码就开始难看了。改成对象映射之后是这样:

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

let age = obj[name] || 18;
@前端进阶之旅: 代码已经复制到剪贴板

原文这里说 if 和 switch 的写法「代码执行效率不高」,这条我得改一下。对象查找是哈希查找,分支特别多的时候确实比线性比较快,但在三五个分支的规模上,这点差异现代 JS 引擎里几乎测不出来。这个技巧真正的价值不在性能,在于把「判断逻辑」变成了「数据结构」。数据结构是可以从接口拿、可以配置化、可以单独抽成文件维护的,if else 不行。

有个坑要注意,obj[name] || 18 这个兜底写法在值可能是 0 或空字符串时会失效,会被当成 falsy 然后走默认值。这种情况用 ?? 空值合并操作符更安全,只有 null 和 undefined 才走默认。

还有一点,这个技巧只适用于「判断一次然后赋一个值」的场景。如果每个分支后面还跟着一大段处理逻辑,硬塞进对象里会变成一堆函数字面量,可读性反而不如老老实实写 if。对象里存函数也不是不行,但那时候你要考虑的就是策略模式了,得配上统一的入参出参约定。

# 二、用 Array.from 快速生成数组

生成有规律的数组,比如时间选择器要用的 24 小时列表,最直觉的写法是循环 push:

let hours = [];

for (let i = 0; i < 24; i++) {
    hours.push(i + '时');
}
@前端进阶之旅: 代码已经复制到剪贴板

Array.from 的第二个参数是一个映射函数,配合一个只有 length 属性的类数组对象,一行就能搞定:

let hours = Array.from({ length: 24 }, (value, index) => index + '时');
@前端进阶之旅: 代码已经复制到剪贴板

这里 { length: 24 } 是关键。Array.from 只认 length,其余索引都是 undefined,所以第一个参数 value 永远是 undefined,真正有用的是 index。写的时候一般直接用下划线占位。

这个技巧在写 mock 数据的时候特别顺手,生成 20 条假列表数据配合模板字符串,比循环 push 干净得多。

# 三、用 router.beforeEach 处理跳转前逻辑

路由跳转之前经常要做点事:设置页面标题、校验登录态、埋点、权限拦截。这些逻辑放在每个页面的 created 里会重复无数遍,正确的位置是全局前置守卫:

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

Vue.use(Router)

// 首页
const Home = (resolve => {
    require.ensure(['../views/home.vue'], () => {
        resolve(require('../views/home.vue'))
    })
})

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
    }
    
    next();
})

export default router
@前端进阶之旅: 代码已经复制到剪贴板

meta 字段是个好东西,页面标题、是否需要登录、需要什么权限,全塞在这里,守卫里统一读,路由表就成了一份自带元信息的配置。

原文这段守卫里还有一段拦截跳转的逻辑,写法是先调一个封装好的 $openRouter 跳到 page2,然后再调 next()。这个写法有问题,我上面把它从主流程里拿掉了,单独说。

先说注释里那个「$openRouter 方法在第 5 节中封装」,本文第五节讲的是路由用 name 不用 path,并没有这个封装,这句应该是原稿从更长的文章里摘出来时留下的残句,实际它指的是项目里自己包了一层的全局跳转方法。

更要紧的是逻辑本身。在守卫里想改变跳转目标,正确做法是把新目标交给 next,而不是自己发起一次跳转再放行原来的:

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

    // 想改跳转目标,把新目标交给 next,不要另起一次 push
    if (to.name === 'home') {
        next({ name: 'page2' });
        return;
    }

    next();
})
@前端进阶之旅: 代码已经复制到剪贴板

按原文那种写法,push 会触发一次新的导航,同时 next() 又放行了当前这次导航,两次导航互相打架,运气不好就是控制台一堆 NavigationDuplicated 警告,或者直接进死循环。这个我踩过,页面卡死浏览器标签页直接没响应,排查了半天才想明白是守卫在自己递归自己。

另外 next() 必须调,而且每个分支上有且只能调一次。忘了调,页面就永远停在原地,白屏还不报错,是新手最容易懵的一类问题。

顺带更新两处写法。require.ensure 是 webpack 特有的老式异步加载 API,现在统一用标准的动态 import(),也就是 const Home = () => import('../views/home.vue'),写法短还能被 Rollup、Vite 一起理解。另外 Vue Router 4(配 Vue 3)里守卫已经不强制调 next 了,可以直接 return 一个路由地址表示重定向、return false 表示取消导航,next 仍然兼容但官方推荐用返回值的写法。

# 四、用 v-if 优化页面加载

页面上有些模块要用户主动触发才显示,弹框、抽屉、折叠面板里的复杂表单都属于这类。这种没必要一进页面就渲染,用 v-if 按需渲染就行:

<template>
    <div>
        <div @click="isShowModuleB = true">打开 B 模块</div>
        <module-b v-if="isShowModuleB"></module-b>
    </div>
</template>

<script>
import moduleB from 'components/moduleB'
export default {
    data() {
        return {
            isShowModuleB: false
        }  
    },
    components: {
        moduleB
    }
}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

原文这段代码有两处得改。一是模板里 div 和 module-b 是并列的两个根节点,Vue 2 的单文件组件模板必须单根,这样写编译不过,上面套了一层 div。二是点击事件里改的是 showModuleB,v-if 判断的却是 isShowModuleB,两个名字对不上,点了也不会显示,上面统一成了 isShowModuleB。

isShowModuleB 为 false 时,这个组件的实例根本不会创建,它内部的 created、mounted 都不会跑,钩子里那些耗时的接口调用自然也就省掉了。这才是 v-if 相比 v-show 真正省下来的东西,不是少了几个 DOM 节点,是少了一整套组件初始化和副作用。

那什么时候用 v-show?切换频繁的时候。v-if 每次切换都要销毁重建,v-show 只是改 display。一个 tab 用户会来回点几十次,用 v-if 就是几十次组件重建,这时候 v-show 明显更划算。

但有一条是死规矩:涉及权限的内容一律用 v-if。v-show 只是把元素藏起来,DOM 还在页面里,打开控制台改一行样式就看见了。这个跟性能无关,是安全问题。

再往前一步,如果这个模块本身体积很大,光 v-if 还不够,因为组件代码已经被打进主包了。这时候要配合异步组件,把 import moduleB from '...' 改成 components: { moduleB: () => import('components/moduleB') },让它单独拆出一个 chunk,用户不点就不下载。组件怎么拆、拆完之后彼此怎么通信,我在 Vue组件化实践详解 里系统写过。

# 五、路由跳转尽量用 name 而不是 path

前期定的路由路径,后期改动是常态。产品说 /user-center 要改成 /account,如果项目里跳转全写的 path,你得全局搜一遍挨个改,还怕漏。

name 就没这个问题。它是路由的唯一标识,跟 path 解耦,path 怎么改都不影响跳转代码:

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

// 而不是
this.$router.push({ 
    path: 'page1'
});
@前端进阶之旅: 代码已经复制到剪贴板

顺便说一个很多人没注意的细节:用 name 跳转时传参用 params,用 path 跳转时 params 会被直接忽略,只有 query 有效。这个规则不知道的话,调试起来会很莫名其妙,参数明明传了对面就是收不到。

fe
  • 一、用对象映射代替 if 和 switch
  • 二、用 Array.from 快速生成数组
  • 三、用 router.beforeEach 处理跳转前逻辑
  • 四、用 v-if 优化页面加载
  • 五、路由跳转尽量用 name 而不是 path
  • 六、用 key 来优化 v-for 循环
  • 七、什么时候用 computed 代替 watch
  • 八、统一管理缓存变量
  • 九、不要用 for in 遍历数组
  • 总结
  • 参考

← Vue之学会编写可复用性模块 函数组件与插件三种粒度Vue之项目整合与优化 alias 入口收敛与 Gzip 压缩 →