【.】浅读Vue3代码10万行,总结出30个代码规范
不管是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',}// 枚举项使用pascalCaseenum 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,将根模块放在第一个单词。
//BADcompilerCorecompilerDomcompilerSSR// GOODvuevue-compatcompiler-corecompiler-domcompiler-ssrcompiler-sfcruntime-coreruntime-domruntime-test
文件夹命名
文件夹命名需要特别注意,windows系统对大小写不敏感,如components和Components在windows系统下会认为是同一个,因此把名字由Components改为components,git是识别不到差异。由于文件夹大小写重命名导致在linux环境编译报错的问题屡见不鲜。
和module类似,采用kebab-case规则,以noun单词命名。如果为同类功能的文件夹,则一般以复数形式结尾,常用的有components、utils等等。
layoutscomponentsdirectiveshooksutilspatchesscriptstransformsservices
组件文件夹命名
一个复杂组件通常会拆分为多个文件夹,每个文件夹以kebab-case方式命名。
dropdowndropdown-itemdropdown-menucheckboxcheckbox-buttoncheckbox-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 = falseemit('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中的代码行换行,要么统一使用分号";",要么统一不使用分号,不能混着用。
// BADimport { buttonProps } from './button';import type { ExtractPropTypes } from 'vue'// GOODimport { buttonProps } from './button'import type { ExtractPropTypes } from 'vue'
组件名命名规范
采用kebab-case,一个项目必须保持统一,组件名一般不超过2 到 3 个单词。
checkbox-button.vuecheckbox-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组件为何意。由于文件引入一般都有智能提示,因此也不容易出现拼写类错误。
BADcomponents/|- SdSettings.vue|- UProfOpts.vueGOODcomponents/|- student-dashboard-settings.vue|- user-profile-options.vue
组件Props命名
组件props定义使用lowerCamelCase命名,在template中使用组件时,使用kebab-case规则。例如定义greetingText属性,模板使用方式为greeting-text=""。
// BAD// componentconst props = defineProps({'greeting-text': String})// for in-DOM templates<welcome-message greetingText="hi"></welcome-message>// GOODcomponentconst 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。
// verbdefineEmits(['open'])defineEmits(['close'])defineEmits(['focus'])// noun + "-" + verbdefineEmits(['state-change'])// verb + "-" + noundefineEmits(['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-componentfoo="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应当通过拆分,简化逻辑。
// BADconst price = computed(() => {const basePrice = manufactureCost.value / (1 - profitMargin.value)return basePrice - basePrice * (discountPercent.value || 0)})// GOODconst 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全称和":"混用<inputv-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限定值的有效性。
// BADconst props = defineProps({ status: String })// GOODconst 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 + NounisStatic // is + NounisSlot // is + NounisMemberExpressionBrowser // is + Noun, 为表达清楚变量意义,可多个名词链接isSimpleIdentifier // is + Adjective + NounisCompatEnabled // is + AdjectiveisVBindisVOnisFromSetupisUsedInTemplatehasFallback // has + NounhasVnodeHook // has + NounhasStyleBinding // has + NounhasDynamicKeys // has + NounhasText // has + NounhashPrefix // has + NounhasCommas // has + NounhasName // has + NounhasAttrsChanged // has + Noun + AdjectivehasCloned // has + Adjective
boolean类型参数命名
函数、方法、构造函数的参数,如果为boolean类型,命名:
is + Noun is + Adjective adjective adv verb + Noun allow + Verb
不管使用那种方式,前提是直观上能推断是boolean类型。
isLocal // is + NounisReference is + Nouninline // advinheritAttrs // verb + Noun.optimized // adjectivechecked // adjectiveallowRecurse // allow + Verbforce?: boolean // verbsync?: boolean // nounfunction genModulePreamble(genScopeId: boolean,inline?: boolean,) {}export function findProp(dynamicOnly: boolean = false,allowEmpty: boolean = false,) {}export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {immediate?: Immediate // adjectivedeep?: boolean // adjectiveonce?: 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 + NounwalkBlockDeclarations(...) // 遍历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-cascaderv-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 + Nounexport class TypeScope { }// Adjective + Adjective + Nounexport class BaseReactiveHandler {}// Adjective + Adjective + Nounexport class ReadonlyReactiveHandler {}// Adjective + Noun + Nounexport class ComputedRefImpl {}// Noun + Verb + Nounexport class ScriptCompileContext {}
Class私有方法、私有属性命名
TS官方是极力反对Class私有方法或属性使用下划线"_"前缀,由于有private标识私有,所以私有成员使用lowerCamelCase命名即可。
Vue.js源码在早年前的Class偏向于使用_lowerCamleCase命名,现在也逐渐使用loweCamelCase。
// BADexport class VueElement extends BaseClass {_instance: ComponentInternalInstance | null = nullprivate _connected = falseprivate _resolved = falseprivate _numberProps: Record<string, true> | null = nullprivate _styles?: HTMLStyleElement[]private _ob?: MutationObserver | null = null}// GOODexport 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。
枚举值和常量定义规则一致,采用全大写模式, 多个单词用下划线"_"分割。
// Typesexport enum Namespaces {HTML,SVG,MATH_ML,}// Codesexport enum ErrorCodes {ABRUPT_CLOSING_OF_EMPTY_COMMENT,CDATA_IN_HTML_CONTENT,DUPLICATE_ATTRIBUTE,END_TAG_WITH_ATTRIBUTES,END_TAG_WITH_TRAILING_SOLIDUS,}// Flagsexport enum ReactiveFlags {SKIP = '__v_skip',IS_REACTIVE = '__v_isReactive',IS_READONLY = '__v_isReadonly',IS_SHALLOW = '__v_isShallow',RAW = '__v_raw',}// Hooksexport enum LifecycleHooks {BEFORE_CREATE = 'bc',CREATED = 'c',BEFORE_MOUNT = 'bm',}// Verb + Nounexport enum MoveType {ENTER,LEAVE,REORDER,}
总结
代码规范重要吗?
重要!优秀的代码规范,能够让别人在短时间能对你有正向的评价。特别在笔试时,优雅的代码也能让面试官眼前一亮,为后续流程降低门槛。
如何保持代码规范?
客观上:可以借助代码检查eslint、prettier、StyleLint、HTMLLint、SonarQube等工具辅助检查。 主观上:要有工匠精神,个人认为正确的代码规范就得持续保持,"自成一派",不能潜移默化地受其他风格影响。
我是
前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!