点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
不管是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相关。如果表述有误,欢迎指正。需要说明的是:代码规范没有标准答案,只有适合、不适合。其目的是让项目代码保持风格统一, 提升易读性, 降低上手成本。
代码规范往期介绍:
浅读Vue3代码10万行,总结出30个代码规范 200+收藏的Vue3规范,如何配置eslint、prettier、editorconfig Vue3黑神话:悟空版 eslint: eslint-plugin-wukong 这样的Git规范,Leader看了都说好!
目录规范
模块命名
采用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 setup lang="ts">
...
</script>
<template>
...
</template>
<style lang="less" scoped>
...
</style>
script可以按props、emits、ref、 computed、watch、methods、events顺序排列代码。为你而是每个文件顺序统一,可以使用vscode定义代码片段,按以上顺序添加注释,开发组件直接在对应注释下添加对应代码。
<script lang="ts" setup>
/** 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)
}
</script>
组件中单引号、双引号
html中、vue的template中标签属性使用双引号
<component
: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
<welcome-message greetingText="hi"></welcome-message>
// GOOD
component
const props = defineProps({
greetingText: String
})
<WelcomeMessage greeting-text="hi"/>
组件事件命名规则
事件命名需要注意定义和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形式。
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
template中组件属性设置单独占用一行
在使用属性时,每个属性独占一行可提升代码的可读性。
// BAD
<my-component foo="a" bar="b" baz="c"/>
// GOOD
<my-component
foo="a"
bar="b"
baz="c"
/>
组件模板应仅包含简单表达式,复杂的应提取到computed中
在template中避免使用复杂的表达式,包含计算逻辑的值应提取到computed。
// BAD
<div :style="{
height: '30rem',
width: '100%',
transition: '.3s ease-out all',
transform: `rotateX(${parallax.roll}deg) rotateY(${parallax.tilt}deg)`,
}">
</div>
// GOOD
<div :style="cardStyle">
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)包裹。如果值为匿名对象则需要增加空格,保证值可读性。
// BAD
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
// GOOD
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
directive指令统一使用简写,不能简写、全称混用。
directive统一使用简写::
for v-bind:
, @
for v-on:
and #
for v-slot
.
// BAD
// v-bind全称和":"混用
<input
v-bind:value="newTodoText"
:placeholder="newTodoInstructions"
>
// v-on和@混用
<input v-on:input="onInput" @focus="onFocus" >
// v-slot和#混用
<template v-slot:header> <h1>Here might be a page title</h1> </template> <template #footer> <p>Here's some contact info</p> </template>
// GOOD
<input :value="newTodoText" :placeholder="newTodoInstructions" >
<input @input="onInput" @focus="onFocus" >
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
组件属性定义添加校验
在定义组件属性时,如果属性值是必填、类型明确,则可添加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 + Noun、 is + Adjective is + Adjective + Noun is + Noun + Adjective has + X
按统一前缀规范,代码中只要看到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类型,命名:
is + Noun is + Adjective adjective adv verb + Noun allow + Verb
不管使用那种方式,前提是直观上能推断是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<Immediate = boolean> 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<string, any>) {}
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等等。
<el-button type="primary" size="small" @click="onConfirm">
<el-button @click="onDelete">Delete Item</el-button>
// on + Verb + Noun
<el-button v-if="!isAdding" @click="onAddOption">
如果是逻辑处理类事件,通常采用handle+Verb,handle + Noun + Verb
,例如:
<el-icon class="el-input__icon" @click="handleIconClick">
<edit />
</el-icon>
<el-cascader
v-model="value"
:options="options"
:props="props"
@change="handleChange"
/>
<el-autocomplete @select="handleSelect" >
如果是处理单一业务逻辑,则可直接调用function,不需要专门定义事件方法
,例如:
<Transition name="el-fade-in" @enter="lock" @after-leave="cleanup">
<el-button type="primary" plain @click="updateServiceWorker()">
如果是更改单一状态,则可直接在template赋值
,例如:
<el-button plain @click="alwaysRefresh = true">
<el-button plain @click="needRefresh = false">
el-button @click="centerDialogVisible = false">Cancel</el-button>
<el-button type="primary" @click="centerDialogVisible = false">
Funtion: 数据请求、处理类方法
数据查询通常采用get或fetch作为前缀,数据提交通常采用post、send、upload前缀。命名规则采用前缀 + Noun + Noun?.例如:
getBookData(id)
fetchBookData(id)
postBookData(data)
uploadBookFile(file)
Class命名
Class采用PascalCase方式命名,命名可使用
多个Noun Adjective + Noun Adjective + Adjective + Noun Noun + Verb + Noun。
// Noun + Noun
export class TypeScope { }
// Adjective + Adjective + Noun
export class BaseReactiveHandler {}
// Adjective + Adjective + Noun
export class ReadonlyReactiveHandler {}
// Adjective + Noun + Noun
export class ComputedRefImpl {}
// Noun + Verb + Noun
export class ScriptCompileContext {}
Class私有方法、私有属性命名
TS官方是极力反对Class私有方法或属性使用下划线"_"前缀,由于有private标识私有,所以私有成员使用lowerCamelCase命名即可。
Vue.js源码在早年前的Class偏向于使用_lowerCamleCase命名,现在也逐渐使用loweCamelCase。
// BAD
export class VueElement extends BaseClass {
_instance: ComponentInternalInstance | null = null
private _connected = false
private _resolved = false
private _numberProps: Record<string, true> | null = null
private _styles?: HTMLStyleElement[]
private _ob?: MutationObserver | null = null
}
// GOOD
export default class Tokenizer {
/** The current state the tokenizer is in. */
private state = State.Text
/** The read buffer. */
private buffer = ''
private stateInterpolation(c: number): void {
...
}
private stateInterpolationClose(c: number) {
...
}
}
常量命名
常量统一放到一个文件定义,例如定义constants.ts文件,每个常量命名使用大写字母加下划线。组成可使用:
多个NOUN ADJECTIVE_NOUN VERB_NOUN
const PURE_ANNOTATION = `/*#__PURE__*/`
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
const DAYS_IN_WEEK = 7;
const MONTHS_IN_YEAR = 12;
const MAX_DOG_WEIGHT = 150;
枚举命名
枚举名称使用PascalCase命名规则,如果是集合类的枚举,通常以复数s形式结尾,表示复数类的后缀常有Types、Levels、Tags、Codes、Hooks等。如果是表示动作类状态枚举,则使用Verb + Noun。
枚举值和常量定义规则一致,采用全大写模式, 多个单词用下划线"_"分割。
// Types
export enum Namespaces {
HTML,
SVG,
MATH_ML,
}
// Codes
export enum ErrorCodes {
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
CDATA_IN_HTML_CONTENT,
DUPLICATE_ATTRIBUTE,
END_TAG_WITH_ATTRIBUTES,
END_TAG_WITH_TRAILING_SOLIDUS,
}
// Flags
export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw',
}
// Hooks
export enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
}
// Verb + Noun
export enum MoveType {
ENTER,
LEAVE,
REORDER,
}
总结
代码规范重要吗?
重要!优秀的代码规范,能够让别人在短时间能对你有正向的评价。特别在笔试时,优雅的代码也能让面试官眼前一亮,为后续流程降低门槛。
如何保持代码规范?
客观上:可以借助代码检查eslint、prettier、StyleLint、HTMLLint、SonarQube等工具辅助检查。 主观上:要有工匠精神,个人认为正确的代码规范就得持续保持,"自成一派",不能潜移默化地受其他风格影响。
我是
前端下饭菜
,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!
我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。
“分享、点赞、在看” 支持一波👍