VUE- 代码规范-前端

305 阅读11分钟

2 代码规范

2.1 命名规范

2.1.1 SFC组件文件/目录命名

官方文档对组件、基础组件、单例组件和紧密耦合的组件都有优先级为强烈推荐的风格规范。
针对其命名规则结合我们实际项目的经验,在项目规模较大、组件较多的情况下,推荐结合文件夹划分进行组件的分类和管理。
对于组件入口文件,统一命名为index,例如index.js

// 官方针对基础组件名的规范
components/
├ BaseButton.vue
├ BaseTable.vue
├ BaseIcon.vue

// 官方针对单例组件名的规范
components/
├ TheHeading.vue
├ TheSidebar.vue

// 官方针对紧密耦合组件名的规范
components/
├ TodoList.vue
├ TodoListItem.vue
├ TodoListItemButton.vue

// 我们建议的组件命名规范
components/
├ base(或者shared)/   (基础组件使用文件夹管理)
├─ Button.vue
├─ Table.vue
├─ Icon.vue
├ layout/           (单例组件或功能性交互组件,文件夹名称需表意,不再使用「The」作为前缀)
├─ AppHeading.vue
├─ AppSider.vue
├ todo-list/        (遵循目录命名规范,使用kebab-case进行文件夹命名)
├─ index.js         (有必要时提供入口文件统一引入,若无必要则省略)
├─ TodoList.vue     (组件命名保留父组件名字)
├─ TodoListItem.vue
├─ TodoListItemButtom.vue

2.1.2 组件命名

除了App.vue这种根节点组件之外,其它自定义组件命名(不是组件的文件或目录命名)必须使用多个单词的PascalCase形式,以避免与HTML原生的元素出现冲突。
此外,对组件进行命名能够有效的提高调试效率,因为你的命名会出现在Vue Devtools的组件面板中(无论你在引用它的时候是否重命名)。

ESLint规则:

虽然可以通过配置vue/match-component-file-name强制让组件的name属性与文件名称保持一致。
但这样做可能会导致在使用Vue Devtools时难以分辨同名组件。所以并不推荐这么做,而是通过人为控制进行约束。

// bad 不推荐
export default {
  name: 'Button'
}

// bad 虽然不会有元素命名冲突,但也不推荐
export default {
  name: '404'
}

// bad 虽然是多个单词,但不建议使用kebab-case进行组件命名
export default {
  name: 'product-detail'
}

// bad 不推荐,不对自定义组件进行命名可能会导致调试困难
export default {
  // 不定义name的值
}

// good 推荐
export default {
  name: 'MyButton'  // 只是示例
}

export default {
  name: 'NotFound'  // 只是示例
}

export default {
  name: 'ProductDetail'
}

2.1.3 组件引用命名

组件引用的命名包含以下两个部分:

  • import语句引入组件时变量的名称
  • components配置项中注册组件的名称

在引入和注册组件时,请尽可能保持与组件自身的name属性一致,或与组件文件的名称一致。

ESLint规则:

// 文件层级
components/
|- BasicInfoPanel.vue
views/
|- portfolio/
|- - Detail.vue

// bad 不推荐

// 在BasicInfoPanel.vue文件中
<script>
export default {
  // 不定义name属性
  ...
}
</script>

// 在Detail.vue文件中
<script>
import AFancyComp from '../../components/BasicInfoPanel'

export default {
  components: {
    MyFancyInfoPanel: AFancyComp // 此时Vue Devtools中只能用MyFancyInfoPanel搜索该组件实例
  }
}
</script>

// good 推荐

// 在BasicInfoPanel.vue中,定义组件名称
<script>
export default {
  name: 'PortfolioBasicInfoPanel'
}
</script>

// 在Detail.vue文件中
<script>
import PortfolioBasicInfoPanel from '../../components/BasicInfoPanel' // 与组件名称保持一致
import BasicInfoPanel from '../../components/BasicInfoPanel' // 或者与组件文件名称保持一致

export default {
  components: {
    // 无论哪种做法,在Vue Devtools中,组件节点的名称都会是PortfolioBasicInfoPanel
    PortfolioBasicInfoPanel // 与组件名称保持一致
    BasicInfoPanel // 或者与组件文件名称一致
  }
}
</script>

2.1.4 组件属性命名

在自定义组件中,props中的自定义属性名称必须使用camelCase形式的字符串。

ESLint规则:vue/prop-name-casing

// bad 不推荐
export default {
  props: {
    'first-name': String,
    LastName: String
  }
}

// good 推荐
export default {
  props: {
    firstName: String,
    lastName: String
  }
}

2.1.5 组件事件命名

在组件中触发自定义事件时:

  • 对于Vue 2项目,使用kebab-case形式进行事件命名。
  • 对于Vue 3项目,建议使用camelCase形式命名,当然也可以延续Vue 2的习惯。

此外,事件的命名要清晰表述事件含义,而不是随意的进行命名。

ESLint规则:# vue/custom-event-name-casing

// bad 不推荐
<script>
export default {
  methods: {
    onClick () {
      this.$emit('takeThis')
      this.$emit('updateSomeThing', this.productInfo)
    }
  }
}
</script>

// good 推荐
<script>
export default {
  methods: {
    onClick () {
      this.$emit('submit-product-info')
      this.$emit('update-product-info', this.productInfo)
    }
  }
}
</script>

2.1.6 事件响应命名

在响应组件事件时,响应方法(函数)的命名需要:

  • 遵循camelCase形式。
  • onhandle开头,团队内部可任选其一并保持一致。
  • 与组件事件名称有关联性,且不该与业务逻辑类方法直接绑定。

这样做一是可以增加代码可读性,二是能做到交互响应与逻辑代码的分离,在需要调整业务逻辑时更加简单。

// bad 不推荐
<template>
  <div>
    <basic-info-panel @ready="stopLoadingAnimation" />
    <upload-modal @submit="saveDocuments">
  </div>
</template>

<script>
export default {
  ...
  methods: {
    stopLoadingAnimation () { ... },
    saveDocuments (docs) { ... }
  }
}
</script>

// good 推荐
<template>
  <div>
    <basic-info-panel @ready="onBasicInfoPanelReady" />
    <upload-modal @submit="onUploadModalSubmit">
  </div>
</template>

<script>
export default {
  ...
  methods: {
    onBasicInfoPanelReady () {
      // 一开始可能只需要做这件事
      this.stopLoadingAnimation()
      // 也许第二天老板加了个新需求
      this.showUploadModal()
    },
    // 也可以是onUploadModalSubmitted
    onUploadModalSubmit (docs) {
      this.saveDocuments(docs)
    },
    stopLoadingAnimation () { ... },
    saveDocuments (docs) { ... }
  }
}
</script>

2.2 模板代码规范

2.2.1 对齐方式

在模板代码中,主要的对齐方式规范有以下几个方面:

  • 缩进:使用两个空格作为每行代码的缩进对齐符;当出现属性换行时,曾对其元素位置再进行一次缩进。
  • 换行:减少不必要的元素属性换行,虽然官方建议每个属性都换一行,但这样可能导致 html 片段的层级不清晰,对于较多属性的情况,在小屏幕中难以阅读,故建议减少不必要的换行,同时,对于元素的闭合符>/>,都不要进行换行。
  • 引号:属性的值推荐使用双引号",这也符合HTML代码书写的习惯。
  • 空格:多个属性间使用一个空格进行分隔;使用双大括号进行数据绑定时,需要在{{后和}}前各空一格;对于自闭合的元素,在元素结尾的/>前空一格;其它模板代码中不应该包含空格

ESLint规则:

// good 好例子
<template>
<one-component
    ref="oneComponent"
    foo="a"
    bar="b"
    baz="c"
    @click="onComponentClicked"
    @mouse-enter="onComponentMouseEntered"
  >
  <two-component     foo="a"
    bar="b"
  baz="c"
 />
</one-component>
</template>

// bettre 推荐,但自动格式化工具目前支持得不好
<template>
  <one-component ref="oneComponent" foo="a" bar="b" baz="c"
    @click="onComponentClicked" @mouse-enter="onComponentMouseEntered">
    <two-component  foo="a" bar="b" baz="c" />
  </one-component>
</template>

2.2.2 内置指令

对于内置指令v-bind v-on 和v-slot,在模板代码(template)中尽量使用他们的缩写(shorthand)形式。

ESLint规则:

// bad 不推荐,因为这样太繁琐了
<template>
  <MyTable v-bind:data-source="products" v-on:ready="handleTableReady">
    <template v-slot:operation="text, record">
      ...
    </template>
  </MyTable>
</template>

// good 推荐
<template>
  <MyTable :data-source="products" @ready="handleTableReady">
    <template #operation="text, record">
      ...
    </template>
  </MyTable>
</template>

2.2.3 自定义组件

在模板代码中调用自定义组件时,请使用PascalCase,这条规则有两个考量:

  1. 结合组件文件命名的规则、引用命名的规则以及组件命名规则,使用PascalCase会更便于使用IDE的全局搜索功能,也更易于使用Vue Devtools快速定位使用自定义组件。
  2. 考虑JSX兼容问题、以及与React项目规范的对齐问题,所以推荐使用PascalCase

ESLint规则:vue/component-name-in-template-casing

// bad 不推荐
<template>
  <my-table v-bind:data-source="products" v-on:ready="handleTableReady" />
</template>

// good 推荐
<template>
  <MyTable :data-source="products" @ready="handleTableReady" />
</template>
2.2.4 自定义属性

在模板代码中绑定自定义属性时,使用kebab-case

ESLint规则:vue/attribute-hyphenation

// bad 不推荐
<template>
  <MyTable :dataSource="products" />
</template>

// good 推荐
<template>
  <MyTable :data-source="products" />
</template>

2.2.5 自定义事件

在模板代码中响应自定义事件时,使用kebab-case

ESLint规则:vue/v-on-event-hyphenation

// bad 不推荐
<template>
  <MyTable @myEvent="handleMyEvent" />
</template>

// good 推荐
<template>
  <MyTable @my-event="handleMyEvent" />
</template>

2.2.6 属性顺序

在模板代码中,针对属性attributes,包括指令、HTML属性、自定义属性和事件等,在书写时需要按照字母表顺序排序,便于查找和阅读。
注:这项规则听起来很是在自讨苦吃,但其实只需要配置好插件即可实现自动排序

ESLint规则:vue/attributes-order

// bad 不推荐
<template>
  <one-component foo="a" ref="oneComponent" bar="b" baz="c"
    @mouse-enter="onComponentMouseEntered" @click="onComponentClicked">
    ...
  </one-component>
</template>

// good 推荐
<template>
  <one-component ref="oneComponent" bar="c" baz="a" foo="b"
    @click="onComponentClicked" @mouse-enter="onComponentMouseEntered">
    ...
  </one-component>
</template>

2.3 脚本代码规范

2.3.1 配置项顺序

在使用Options API时,除了要遵循基本的JavaScript/TypeScript规范之外,还需要遵守严格的配置项顺序。
统一的配置项顺序能够让开发者轻松的阅读和定位同事书写的代码,避免因为顺序问题导致的错误理解和判断。
注意:我们不推荐使用vue-component-class,因为它会带来额外的学习成本和维护成本

ESLint规则:vue/order-in-components
我们建议使用官方提供的默认顺序,具体顺序可在此处查阅

// bad 不推荐
<script>
export default {
  data () { ... },

  name: 'MyComponent',

  methods: { ... },

  beforeRouteEnter () { ... }
}
</script>

// good 推荐
<script>
export default {
  name: 'MyComponent',

  beforeRouteEnter () { ... },

  data () { ... },

  methods: { ... }
}
</script>

2.3.2 配置项间空行

在配置项之间空行可以便于定位代码片段,虽然ESLint并不能检查和自动进行代码调整,但手动遵循此规则能够以极小的精力消耗带来优异的阅读和维护体验。

// bad 不推荐,但也不会有ESLint的报错提示
<script>
export default {
  name: 'MyComponent',
  beforeRouteEnter () { ... },
  data () { ... },
  methods: { ... }
}
</script>

// good 推荐
<script>
export default {
  name: 'MyComponent',

  beforeRouteEnter () { ... },

  data () { ... },

  methods: { ... }
}
</script>

2.3.3 使用路径别名

在引入项目中其它文件的时候,如果出现跨越目录层级较多的情况,建议使用别名定义来替代长串的引用路径字符串。

// bad 不推荐
import MyComponent from '../../../../../../../../components/MyComponent'
import { myTypeEnums } from '../../../../../../../../enums/myTypes'

// good 推荐
import MyComponent from '@/components/MyComponent'
import MyOtherComponent from '@components/MyOtherComponent'
import { myTypeEnums } from '../../../../../../../../enums/myTypes'

// webpack配置示例:
resolve: {
  alias: {
    '@': path.resolve(root, 'src'),
    '@components': path.resolve(root, 'src/components'),
    '@enums': path.resolve(root, 'src/enums')
  }
}

2.4 样式代码规范

样式代码规范首先需要遵循《CSS代码规范》

2.4.1 使用作用域

为组件样式设置作用域是非常有必要的,但同时也要注意:

  • 避免在scoped样式中出现元素选择器,因为元素选择器与特性选择器(scoped样式最终会被处理为特性选择器)时,效率会大幅度下降,而如果使用classid则不会有太大性能影响。
  • 需要仅在某个组件内覆盖第三方UI组件库样式时,要使用深度作用选择器>>>
  • 如果你正在使用dart-sass这类Sass预处理器,则深度选择器需要使用::v-deep/deep/,否则预处理器将会无法正确工作。

2.5 其它

2.5.1 注意引用字符串的大小写

请务必注意,在项目中引用组件或模块时,字符串的大小写必须与文件名称一致,否则会出现资源寻址错误的问题!
这是在真实场景中发现的问题:在 Windows 和 MacOs 中,引用模块名称的字符串大小写并不敏感;而在流水线的 Linux/Unix 服务器中进行打包时,则会因为大小敏感问题,导致令人迷惑的报错。

// 目录和文件示例
components/
├ MyComponent.vue
views/
├ MyPage.vue

// bad 
  ...
<script>
// 此处的组件引用在流水线编译时,会提示无法找到mycomponent(.js/.vue/...)文件,阻碍编译
import MyComponent from '../components/mycomponent'
...
</script>

// good
  ...
<script>
// 注意引入组件时的字符串大小写
import MyComponent from '../components/MyComponent'
...
</script>

2.5.2 代码片段顺序

自定义组件如果是*.vue文件,代码片段需要遵循固定的顺序,在一个团队中可以任选其一:

  • template > script > style
  • script > template > style

ESLint规则:vue/component-tags-order

<!-- bad 不推荐 -->
<style>/* ... */</style>
<script>/* ... */</script>
<template>...</template>

<!-- good 推荐,但注意要团队内保持该顺序 -->
<template>...</template>
<script>/* ... */</script>
<style>/* ... */</style>

<!-- good 推荐,但注意要团队内保持该顺序 -->
<script>/* ... */</script>
<template>...</template>
<style>/* ... */</style>

2.5.3 停止使用keyCode(@v2.x)

按键修饰符在 Vue 3 中已弃用,包括:

  • 不再支持使用数字 (即键码) 作为v-on修饰符
  • 不再支持config.keyCodes

其根本原因是KeyboardEvent.keyCode已被废弃。

/** 以下代码在Vue 3中不兼容 */
<!-- 全局配置 -->
Vue.config.keyCodes = {
  f1: 112
}

<!-- 键码版本 -->
<input v-on:keyup.13="submit" />

<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />

<!-- 键码版本 -->
<input v-on:keyup.112="showHelpText" />

<!-- 自定义别名版本 -->
<input v-on:keyup.f1="showHelpText" />
/** Vue 3中的代码实现,对任何要用作修饰符的键使用kebab-cased名称 */
<!-- Vue 3 在 v-on 上使用按键修饰符 -->
<input v-on:keyup.page-down="nextPage">

<!-- 同时匹配 q 和 Q -->
<input v-on:keypress.q="quit">

2.5.4 停止使用实例的$on $once $off方法(@v2.x)

$on $off$once实例方法在Vue 3中已被移除,组件实例不再实现事件触发接口。
与事件相关的实例 API 仅保留了 $emit 这一项。

/** 以下代码在Vue 3中不兼容 */
export default {
  mounted () {
    // $once的情况与之相同
    this.$on('custom-event', () => {
      console.log('Custom event triggered!')
    })
  },
  beforeDestroy () {
    this.$off('custom-event')
  }
}
/** 在Vue 3中的代码实现 */
// 模板语法与之前一致,once使用修饰符
<my-component @custom-event="onCustomEvent" />
<button @click.once="onButtonClicked">Click Me</button>

// 在渲染函数中的使用,事件修饰符使用驼峰写法将他们拼接在事件名后面
render() {
  return h('input', {
    onClickCapture: this.doThisInCapturingMode,
    onKeyupOnce: this.doThisOnce,
    onMouseoverOnceCapture: this.doThisOnceInCapturingMode
  })
}

// 如果你仍然需要eventBus,可以借助mitt或者tiny-emitter
import emitter from 'tiny-emitter/instance'

export default {
  $on: (...args) => emitter.on(...args),
  $once: (...args) => emitter.once(...args),
  $off: (...args) => emitter.off(...args),
  $emit: (...args) => emitter.emit(...args),
}

2.5.5 停止使用过滤器filter(@v2.x)

过滤器filterVue 3中已被移除,且官方认为该功能会增加学习和理解成本。
Vue 3中,建议使用计算属性替换组件内的过滤器,针对全局过滤器,则使用全局属性来替代。

/** 以下代码在Vue 3中不兼容 */
<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountBalance | currencyCNY }}</p>
</template>

<script>
  export default {
    ...
    filters: {
      currencyCNY (value) {
        return ${value}`
      }
    }
  }
</script>

/** 使用计算属性替换组件内的过滤器,针对全局过滤器,则使用全局属性来替代 */

// 定义全局属性
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyCNY (value) {
    return `¥${value}'
  }
}

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountInCNY }}</p>
  <!-- 或者 -->
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>

<script>
  export default {
   ...
    computed: {
      accountInCNY () {
        return ${this.accountBalance}`
      }
    }
  }
</script>

2.5.6 停止使用内联模板inline-template(@v2.x)

Vue 3中不再支持内联模板这一特性。

/** 以下代码在Vue 3中不兼容 */
<my-comp inline-template :msg="parentMsg">
  {{ msg }} {{ childState }}
</my-comp>
/** 在Vue 3中,你可以通过使用默认插槽作为替代方案 */
<!-- 默认插槽版本 -->
<my-comp v-slot="{ childState }">
  {{ parentMsg }} {{ childState }}
</my-comp>

<!-- 在子组件模板中,渲染默认插槽 -->
<template>
  <slot :childState="childState" />
</template>

一般来说,简单的宣传页面可能会出现不使用任何构建工具,直接将代码写在 HTML 里,同时大量使用内联模板特性。
针对这种情况,可以直接用 script 标签替换内联模板相关的代码。

// 在HTML代码中
<script type="text/html" id="my-comp-template">
  <div>{{ hello }}</div>
</script>

// 在JavaScript代码中
const MyComp = {
  template: '#my-comp-template'
  // ...
}

2.5.7 停止使用实例属性$children访问子组件实例(@v2.x)

Vue 3中不再支持$children这一实例属性,请使用 $refs 作为替代方案。

/** 以下代码在Vue 3中不兼容 */
export default {
  ...
  mounted() {
    console.log(this.$children) // [VueComponent]
  }
}
</script>
/** 在Vue 3中使用$refs */
export default {
  ...
  mounted() {
    console.log(this.$refs)
  }
}
</script>

2.5.8 停止使用$destory方法(@v2.x)

Vue 3中,开发人员不应再手动管理单个Vue组件的生命周期。

/** 以下代码在Vue 3中不兼容 */
export default {
  ...
  methods: {
    onSomeThingHappends () {
      this.$refs[foo].$destory()
    }
  }
}
</script>

2.5.9 停止使用全局set delete方法和实例的$set $delete方法(@v2.x)

由于Vue 3使用Proxy对象替代了Vue 2中的Object.defineProperty,所以没有必要再使用这些方法去设置和移除响应式数据了。

/** 以下代码在Vue 3中不兼容,且并不需要这么做了 */
export default {
  ...
  created () {
    this.$set(this, 'foo', { bar: 1 })
  }
}
</script>

**