【前端】踩坑日记:作为一个合格的前端开发人员,这些坑你肯定踩过

374 阅读2分钟

历经多年,亦或参与过多个多个项目,亦踩坑之数,不可细数。今日在这里记下一笔,当作日后回看之用,如果有什么不对的,请各位看官帮忙指出,以便日后修正。多谢!多谢!

声明:下文中种种案例,收集的均为真实案例(代码均做过脱敏处理,如有雷同,纯属巧合!)

作为程序员,代码规范,是一个必谈的话题,本文是对Code Review主要Review什么?的延续,欢迎各位同学点评讨论!

1. 异常报错

Cannot read properties of undefined

不知此画面是否熟悉,明明上周代码运行还好好的,怎么这周就出问题了呢? 难道是后端同学的数据返回有问题?或者是哪位前端同学调用函数时传递过来的参数不对。

image.png

function getUserIconUrl(userInfo){
    const icon = userInfo.icon
    if(icon.startsWith('https://'){
        return icon
    }
    return `https://example.icon.com/images/${icon}`
}

代码分析

简单分析下此函数,可能存在以下问题:

  • 入参合法性未校验,入参需是一个Object对象,但实际只用到icon属性的值。在入参传入正确时,没有什么问题,但若非Object对象,如:Array, undefined, null, false, string等类型的异常值时,则均可能会出现问题
    • 建议: 入参,即外部输入的数据,均不可信,在编码时,我们需要对其进行核验,如若出现异常时,做出相应的处理,避免程序执行出错。

代码整改

在不修改入参的情况下,我们修改函数如下:

function getUserIconUrl(userInfo, defaultIcon = 'https://example.icon.com/images/user/default.png'))
    const icon = _.get(userInfo, 'icon', '')
    if(typeof icon === 'string'){
        if(icon.startsWith('https://')) return icon
        else return `https://example.icon.com/images/${icon}`
    }
    return defaultIcon
}

如果可以修改入参,建议修改为:

function getUserIconUrl(iconUrl, defaultIcon = 'https://example.icon.com/images/user/default.png'))
    if (typeof iconUrl === 'string'){
        if (iconUrl.startsWith('https://')) return iconUrl
        else return `https://example.icon.com/images/${iconUrl}`
    }
    return defaultIcon
}

小结

  • 函数入参尽量选择使用基本数据类型, 如:string, number, boolean等,这样会使函数更易受控。
  • 若使用复杂的数据类型,如:Object, Array,或都有甚者使用 二维数组等更复杂的数据作为函数的入参(若因历史原因等特殊情况,必须要传递这样的入参,建议将函数对入参的数据进行分层处理)

Uncaught TypeError

不知这样的报错,是否熟识? 和大多数“祖传”代码类似,维护或新增代码时,需格外小心谨慎,但还是防不住。 image.png

报错的原因,很快可定位出来:

const entities = _.get(db, 'source.entities')

取得的变量entities值为undefined。因此,后面执行forEach报错,就好解释了。

function getEntities(srcDBs, type) {
    const entity = _.get(getCurrentDB(), 'source.entity')
    const arr = []
    const result = []
    Object.values(srcDBs).forEach((db) => {
        const entities = _.get(db, 'source.entities')
        entities.forEach((e) => {
            arr.push(e.name)
            const entNum = _.filter(arr, (v) => v === e.name).length
            if (entNum === viewLen) {
                result.push(type === 'options' ? {text: `${e.name}-${e.label}`, value: e.name, default: e.name === entity} : e.name )
            }
        })
    })
    return result
}

代码分析

初步分析,代码存在以下问题:

  • 建议以不可信任的态度处理外部数据,以确保程序能够正常执行。
  • 三目运算符,建议在一些简单场景使用,如: a ? true : false, 复杂的场景,建议使用if/else,例如下面这段,个人建议从可读性考虑,使用if/else代替。
result.push(type === 'options' ? {text: `${e.name}-${e.label}`, value: e.name, default: e.name === entity} : e.name )

代码整改

function getEntities(srcDBs, type) {
    if (!_.isObject(srcDBs)) return []
    const entity = _.get(getCurrentDB(), 'source.entity', '')
    const arr = []
    const result = []
    Object.values(srcDBs).forEach((db) => {
        const entities = _.get(db, 'source.entities', [])
        if(_.isArray(entities)) {
            entities.forEach((e) => {
                arr.push(e.name)
                const entNum = _.filter(arr, (v) => v === e.name).length
                if (entNum === viewLen) {
                    if (type === 'options') {
                        result.push({
                            text: `${e.name}-${e.label}`,
                            value: e.name,
                            default: e.name === entity
                        })
                    } else {
                        result.push(e.name)
                    }
                }
            })
        }        
    })
    return result
}

小结

对于接收到的外部输入的数据,在实现函数时,建议采用不可信任的态度,对其进行严格校验或判断,避免异常(极端)场景未处理到,导致执行时报错。

2. 代码可读性

“魔鬼变量”

即不知道从哪冒出来的,无任何意义的变量或值,例如:

function isShowBtnGroups(){
    return getCurrentMode() === 0 || getCurrentMode === 1
}

函数isShowBtnGroups在执行一些业务判断,以决定BtnGroup是否显示。咋一看,函数的实现好像没有什么问题,但仔细读起来会发现 getCurrentMode() === 0getCurrentMode() === 1 这里的 01代码什么意思呢?

代码分析

可以看出,函数体内部实现比较简单,但突然冒出的变量,着实让代码的可读性差不少,建议整改!

代码整改

function isShowBtnGroups(){
    const MODE = {
        read: 0,
        edit: 1
    }
    return getCurrentMode() === MODE.read || getCurrentMode === MODE.edit
}

小结

整改后代码,虽然新增了一个看起来“多余”的变量,但可读性增强不少,不用写注释。 同时,可维护性也增强不少,例于后续因业务变化而进行代码重构。

简单问题复杂化

项目中,有使用到一些成熟稳定的公共工具函数或库(如:lodash),在实际开发中,却编码随意,例如:

function initToolBarItems(toolBars){
    console.log('toolBars', toolBars)
}
function getToolsInfo(config) {
 let toolBarItems = []
 if (config && config.toolbar) {
   let toolbar = config.toolbar || {}
   if (toolbar && toolbar.groups && toolbar.groups.length) {
     toolBarItems = toolbar.groups[0].items || []
   }
 }
 return initToolBarItems(toolBarItems)
}

代码分析

可以看出,函数在做各种判断,以便对入参进行校验。但仍存在以下问题:

  • 可读性差
    • 建议:可以考虑使用lodash来简化处理
  • 函数复杂度高
    • 建议:可以考虑使用lodash来简化处理

代码整改

function initToolBarItems(toolBars){
    console.log('toolBars', toolBars)
}
function getToolsInfo(config) {
 const toolBarItems = _.get(config, 'toolbar.groups[0].items', [])
 return initToolBarItems(toolBarItems)
}

小结

合理的使用工具库,能够帮助我们提高工作效率,增强代码可读性。

问题综合体

下文所示的示例代码,为实际项目中所发现的,作者是一位有近2年前端开发经验的同学,项目的基本功能都能较好的完成,但每次在阅读他的代码时,难以言表,比如:

export function setDefaultImg(res) {
    let imgUrl = getImagePrefixUrl()
    let previewImgSrc = require('@/asserts/images/icon.png')
    if(res.imgId) {
        previewImgSrc = `${imgUrl}${res.imgId}.${res.type}?t=${+new Date()}`
    }
    return previewImgSrc
}

代码分析

上例函数, 其实是想对传入的参数进行判断, 如果值无效, 取默认图片, 否则返回拼接地址好的img src相对地址. 从函数的实现来看, 有如下几个问题:

  • 函数名setDefaultImg并不合适, 无任何对数据的更新/修改, 最终目的是想拿到img的拼接地址(或者默认图片)
  • 形参res是一个对象, 实际使用到的属性只有两个基本类型, 遵循最少知识原则, 应修改形参.
  • let imgUrl, 其实是拼接url的前缀, 取名如 imgPrefixUrl会比较合适.
  • let 使用不当, 如 let imgUrl, 因涉及不到变量再赋值, 使用const是比较合适的
  • res.imgId, 即异常情况的处理, 缺少了对res.type的处理
  • 变量命名, 如previewImgSrc, 取此名也不太合适, 该src使用的目的并不确定, 建议使用有比较通用涵义的名字

代码整改

export function getImgSrc(imgId, imgType) {
    if(typeof imgId !== 'string' || typeof imgType !== 'string') {
        return require('@/asserts/images/icon.png')
    }
    return `${getImagePrefixUrl()}${imgId}.${imgType}?t=${+new Date()}`
}

小结

函数功能尽量单一,入参尽量简单化,函数更易控制。

项目开发中,程序都是对数据进行读取或写入,因此,最常使用的是 getter / setter, 关于getter / setter,有兴趣的同学,可以阅读下这篇文章JavaScript getter-setter金字塔

3. 大复杂度函数

import _ from 'lodash'
function codeScript(data, codes){
    if (data.item.type === 'customType') {
      cells = data.item.layout.cells
      cells && cells.forEach((cell) => {
        if (cell.cont && cell.cont.type === 'demoType') {
          if (cell.cont.toolbar && cell.cont.toolbar.groups) {
            (cell.control.toolbar.groups || []).forEach((group) => {
              (group.items || []).forEach((item) => {
                (item.events || []).forEach((event) => {
                  let code = _.find(codes, (v) => {
                    return v.contId === item.itemId
                  }) || {}
                  event.script = code.script || ''
                })
              })
            })
          }
        }
      })
    }
    return data
}

代码分析

初步来看,代码可能存在以下问题:

  • 可读性差, 相信看到这样的函数,真是头大!
    • 建议:复杂的业务函数,建议使用分层函数进行处理
  • 入参合法性未检验,程序执行时可能会出错
    • 建议:对入参进行合法性校验
  • 函数复杂度高
    • 建议函数复杂度不要超过5-10,同时建议开启eslint函数复杂度强制检查 eslint-complexity

代码整改

import _ from 'lodash'
function codeScript(data, codes){
  const type = _.get(data, 'item.type', '')
  if(type === 'customType'){
    forEachFn(data, 'item.layout.cells', [], cell => {
      const contType = _.get(cell, 'cont.type', '')
      if(contType === 'demoType'){
        forEachFn(cell, 'cont.toolbar.groups', [], group => {
          forEachFn(group, 'items', [], (item) => {
            forEachFn(item, 'events', [], event => {
              const itemId = _.get(item, 'itemId', '')
              const code = _.find(codes, v => v && v.contId === itemId) || {}
              event.script = code.script
            })
          })
        })
      }
    })
  }
  return data
}

function forEachFn(obj, path, callback){
  const value = _.get(obj, path, defaultValue)
  _.isArray(value) && value.forEach(item => callback(item))
}

小结

整改后,看起来会稍微好些,但奈何数据结构和业务如此,还未想到更好的代码表达方式。 不知道各位看官,有没有更好的整改办法。

相关链接

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿