不管是Vue.js源码,还是UI组件库Element-plus,只要有多人协同开发,代码规范上多多少少都会有一些"百花齐放"。即使像Vue.js源码,不同开发者在function、enum、variable命名上也会附带个人风格。
Vue.js部分枚举定义, 三个大牛,三种不同的风格。
// Type结尾
enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2,
}
// Types结尾
enum OptionTypes {
PROPS = 'Props',
DATA = 'Data',
COMPUTED = 'Computed',
METHODS = 'Methods',
INJECT = 'Inject',
}
// 枚举项使用pascalCase
enum BooleanFlags {
shouldCast,
shouldCastTrue,
}
通过对Vue、Element-plus源码浅读分析,罗列出30个代码规范建议,前半部分和Vue相关,后半部分和Javascript相关。如果表述有误,欢迎指正。需要说明的是:
代码规范没有标准答案,只有适合、不适合。其目的是让项目代码保持风格统一, 提升易读性, 降低上手成本。
代码规范往期介绍:
-
-
200+收藏的Vue3规范,如何配置eslint、prettier、editorconfig
-
Vue3黑神话:悟空版 eslint: eslint-plugin-wukong
-
目录规范
模块命名
采用kebab-case命名方式,多个noun单词用短横线"-"分割,一般不超过2个单词,命名采用
模块-子模块
方式,这样module会按模块级别归类排序。
如vue的编译器compiler分core、dom、ssr、sfc模块,因此命名为
compiler-core
,将根模块放在第一个单词。
//BAD
compilerCore
compilerDom
compilerSSR
// GOOD
vue
vue-compat
compiler-core
compiler-dom
compiler-ssr
compiler-sfc
runtime-core
runtime-dom
runtime-test
文件夹命名
文件夹命名需要特别注意,windows系统对大小写不敏感,如components和Components在windows系统下会认为是同一个,因此把名字由Components改为components,git是识别不到差异。由于文件夹大小写重命名导致在linux环境编译报错的问题屡见不鲜。
和module类似,采用kebab-case规则,以noun单词命名。如果为同类功能的文件夹,则一般以复数形式结尾,常用的有
components
、
utils
等等。
layouts
components
directives
hooks
utils
patches
scripts
transforms
services
组件文件夹命名
一个复杂组件通常会拆分为多个文件夹,每个文件夹以kebab-case方式命名。
dropdown
dropdown-item
dropdown-menu
checkbox
checkbox-button
checkbox-group
assets目录
assets存放静态资源,images, styles, icons、svgs等静态目录以复数形式结尾,静态资源文件以kebab-case形式命名。
- assets
- images
- icons
- styles
- svgs
- ant-design-vue.svg
组件规范
组件结构化
组件编写大部分事件聚焦逻辑,因此将script放在顶层,文件结构可按sript、template、style顺序布局。
script可以按props、emits、ref、 computed、watch、methods、events顺序排列代码。为你而是每个文件顺序统一,可以使用vscode定义代码片段,按以上顺序添加注释,开发组件直接在对应注释下添加对应代码。
/** imports */
import { computed, ref, useSlots } from 'vue'
import { ElIcon } from '@element-plus/components/icon'
import { TypeComponents, TypeComponentsMap } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { alertEmits, alertProps } from './alert'
/** props */
const props = defineProps(alertProps)
/** emits */
const emit = defineEmits(alertEmits)
/** refs */
const visible = ref(true)
/** computed */
const iconComponent = computed(() => TypeComponentsMap[props.type])
const iconClass = computed(() => [
ns.e('icon'),
{ [ns.is('big')]: !!props.description || !!slots.default },
])
const withDescription = computed(() => {
return { 'with-description': props.description || slots.default }
})
/** methods */
const close = (evt: MouseEvent) => {
visible.value = false
emit('close', evt)
}
组件中单引号、双引号
html中、vue的template中标签属性使用
双引号
:is="tag"
ref="_ref"
v-bind="_props"
:class="buttonKls"
:style="buttonStyle"
@click="handleClick"
>
所有js中的字符串使用
单引号
ns.is('disabled', _disabled.value),
ns.is('loading', props.loading),
ns.is('plain', props.plain),
ns.is('round', props.round),
所有js中的代码行换行,要么统一使用分号";",要么统一不使用分号,不能混着用。
// BAD
import { buttonProps } from './button';
import type { ExtractPropTypes } from 'vue'
// GOOD
import { buttonProps } from './button'
import type { ExtractPropTypes } from 'vue'
组件名命名规范
采用kebab-case,一个项目必须保持统一,组件名一般不超过2 到 3 个单词。
checkbox-button.vue
checkbox-group.vue
组件以高优单词开头
组件命名以高优先单词开头,以描述性单词结尾,重要单词放前面可实现有序排列。例如查询组件,使用Search前缀,输入组件命名为SearchInputXXX,而按钮组件使用SearchButtonXXX。
components/
|- search-button.vue
|- search-button-clear.vue
|- search-input.vue
|- search-input-query.vue
|- settings-checkbox.vue
|- settings-checkbox-terms.vue
父、子组件命名
和父组件紧密相关的子组件应该以父组件名作为前缀命名,例如:
components
|- todo-list.vue
|- todo-list-item.vue
|- todo-list-item-button.vue
组件名应使用完整单词
组件命名不能使用缩写,而应该使用完整单词组成的名称。因为时间一长,或者换个人,可能完全看不出SdSettings组件为何意。由于文件引入一般都有智能提示,因此也不容易出现拼写类错误。
BAD
components/
|- SdSettings.vue
|- UProfOpts.vue
GOOD
components/
|- student-dashboard-settings.vue
|- user-profile-options.vue
组件Props命名
组件props定义使用lowerCamelCase命名,在template中使用组件时,使用kebab-case规则。例如定义greetingText属性,模板使用方式为
greeting-text=""
。
// BAD
// component
const props = defineProps({
'greeting-text': String
})
// for in-DOM templates
// GOOD
component
const props = defineProps({
greetingText: String
})
组件事件命名规则
事件命名需要注意定义和template使用。
事件定义:通常使用一个verb定义的事件名居多,例如open、close、click、change、focus、blur、select。对于状态类事件定义一般采用noun + "-" + verb形式,例如state-change、active-change。生命周期类通常采用prep + verb形式表示事件,例如before-enter、after-enter。或者是标识动作的事件,采用verb + "-" + noun。
// verb
defineEmits(['open'])
defineEmits(['close'])
defineEmits(['focus'])
// noun + "-" + verb
defineEmits(['state-change'])
// verb + "-" + noun
defineEmits(['open-menu'])
// 生命周期
'before-enter'
'before-leave'
'after-enter'
'after-leave'
事件定义推荐使用对象的形式,而不是简写。对象形式能够直观看到每一个事件入参和返回值类型。
export const cascaderEmits = {
focus: (evt: FocusEvent) => evt instanceof FocusEvent,
blur: (evt: FocusEvent) => evt instanceof FocusEvent,
clear: () => true,
visibleChange: (val: boolean) => isBoolean(val),
expandChange: (val: CascaderValue) => !!val,
removeTag: (val: CascaderNode['valueByOption']) => !!val,
}
const emit = defineEmits(cascaderEmits)
// template中使用组件时,注册事件使用kebab-case形式。
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
template中组件属性设置单独占用一行
在使用属性时,每个属性独占一行可提升代码的可读性。
// BAD
// GOOD
foo="a"
bar="b"
baz="c"
/>
组件模板应仅包含简单表达式,复杂的应提取到computed中
在template中避免使用复杂的表达式,包含计算逻辑的值应提取到computed。
// BAD
height: '30rem', width: '100%',
transition: '.3s ease-out all',
transform: `rotateX(${parallax.roll}deg) rotateY(${parallax.tilt}deg)`,
}">
// GOOD
const cardStyle = computed(() => ({
height: '30rem',
width: '100%',
transition: '.3s ease-out all',
transform: `rotateX(${parallax.roll}deg) rotateY(${parallax.tilt}deg)`,
}))
复杂的computed应提取为多个简单计算
包含多个值计算的computed应当通过拆分,简化逻辑。
// BAD
const price = computed(() => {
const basePrice = manufactureCost.value / (1 - profitMargin.value)
return basePrice - basePrice * (discountPercent.value || 0)
})
// GOOD
const basePrice = computed(
() => manufactureCost.value / (1 - profitMargin.value)
)
const discount = computed(
() => basePrice.value * (discountPercent.value || 0)
)
const finalPrice = computed(() => basePrice.value - discount.value)
属性赋值规则
template中属性值使用引号(equoted)包裹。如果值为匿名对象则需要增加空格,保证值可读性。
directive指令统一使用简写,不能简写、全称混用。
directive统一使用简写::
for v-bind:
, @
for v-on:
and #
for v-slot
.
组件属性定义添加校验
在定义组件属性时,如果属性值是必填、类型明确,则可添加type、required、validator限定值的有效性。
// BAD
const props = defineProps({ status: String })
// GOOD
const props = defineProps({
status: {
type: String,
required: true,
validator: (value) => {
return ['created','loading', 'loaded'].includes(
value
)
}
}
})
Javascript 规范
boolean类型变量命名
boolean变量、属性命名可添加is、has前缀,可以使用
按统一前缀规范,代码中只要看到is、has前缀的,则可初步判断为boolean类型。
isString // is + Noun
isStatic // is + Noun
isSlot // is + Noun
isMemberExpressionBrowser // is + Noun, 为表达清楚变量意义,可多个名词链接
isSimpleIdentifier // is + Adjective + Noun
isCompatEnabled // is + Adjective
isVBind
isVOn
isFromSetup
isUsedInTemplate
hasFallback // has + Noun
hasVnodeHook // has + Noun
hasStyleBinding // has + Noun
hasDynamicKeys // has + Noun
hasText // has + Noun
hashPrefix // has + Noun
hasCommas // has + Noun
hasName // has + Noun
hasAttrsChanged // has + Noun + Adjective
hasCloned // has + Adjective
boolean类型参数命名
函数、方法、构造函数的参数,如果为boolean类型,命名:
不管使用那种方式,前提是直观上能推断是boolean类型。
isLocal // is + Noun
isReference is + Noun
inline // adv
inheritAttrs // verb + Noun.
optimized // adjective
checked // adjective
allowRecurse // allow + Verb
force?: boolean // verb
sync?: boolean // noun
function genModulePreamble(
genScopeId: boolean,
inline?: boolean,
) {}
export function findProp(
dynamicOnly: boolean = false,
allowEmpty: boolean = false,
) {}
export interface WatchOptions extends WatchOptionsBase {
immediate?: Immediate // adjective
deep?: boolean // adjective
once?: boolean // adv
}
boolean类型方法命名
boolean类型方法命名,一般以is、has、should、include、exclude、check等为前缀, 以Exists、Enabled、Equal等为后缀。
// is前缀
isInDestructureAssignment(...)
isInNewExpression(...)
isFragmentTemplate(...)
isTagStartChar(...)
isSameKey(...)
// has前缀
hasScopeRef(...)
hasForwardedSlots(...)
hasPropsChanged(...) // 表示状态变化的
hasMultipleChildren(...)
hasExplicitCallback(...)
hasCSSTransform(...)
shouldSkipAttr(...)
shouldReloadHmr(...)
includeBooleanAttr(...)
checkCompatEnabled(...)
// Exists、Equal后缀
fileExists(...)
looseEqual(...)
Function:业务方法命名
使用lowerCamelCase小驼峰,以verb作为前缀,少数情况以描述性noun作为前缀。常用动词前缀:
- create、init、gen、walk、to、process、extract、unwrap、patch。
createObjectMatcher(obj: Record) {}
createElementWithCodegen(...)
genFlagText(...)
parseWithForTransform(...)
createConditionalExpression(...)
convertToBlock(...)
walkFunctionParams(...) // work + Noun
walkBlockDeclarations(...) // 遍历Block定义
extractIdentifiers(...)
unwrapTSNode(...)
patchEvent(...)
patchDOMProp(...)
patchStyle(...)
// 描述性noun作为前缀
ssrRender(_ctx, _push, _parent, _attrs)
Function:事件方法命名
注册事件的回调方法,经常纠结前缀要不要加"on",后缀要不要加"ed",错误示范:onChanged。
如果是交互类事件的函数定义,通常采用on + Verb + Noun?形式,如onClick、onConfirm、onClose、onChange等等。
Delete Item
// on + Verb + Noun
如果是逻辑处理类事件,通常采用handle+Verb,handle + Noun + Verb
,例如:
v-model="value"
:options="options"
:props="props"
@change="handleChange"
/>
如果是处理单一业务逻辑,则可直接调用function,不需要专门定义事件方法
,例如:
如果是更改单一状态,则可直接在template赋值
,例如: