浅读Vue3代码10万行,总结出30个代码规范

科技   2024-11-15 09:53   北京  

点击上方 程序员成长指北,关注公众号

回复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',}
// 枚举项使用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-compat
compiler-corecompiler-domcompiler-ssrcompiler-sfc
runtime-coreruntime-domruntime-test

文件夹命名

文件夹命名需要特别注意,windows系统对大小写不敏感,如components和Components在windows系统下会认为是同一个,因此把名字由Components改为components,git是识别不到差异。由于文件夹大小写重命名导致在linux环境编译报错的问题屡见不鲜。

和module类似,采用kebab-case规则,以noun单词命名。如果为同类功能的文件夹,则一般以复数形式结尾,常用的有componentsutils等等。

layoutscomponentsdirectiveshooksutilspatchesscriptstransformsservices

组件文件夹命名

一个复杂组件通常会拆分为多个文件夹,每个文件夹以kebab-case方式命名。

dropdowndropdown-itemdropdown-menu
checkboxcheckbox-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 = 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中的代码行换行,要么统一使用分号";",要么统一不使用分号,不能混着用。

// 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.vue
GOODcomponents/|- 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-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应当通过拆分,简化逻辑。

// 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全称和":"混用<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限定值的有效性。

// 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 + AdjectiveisVBindisVOnisFromSetupisUsedInTemplate
hasFallback // 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 // 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 + 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-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 + Nounexport class TypeScope { }
// Adjective + Adjective + Nounexport class BaseReactiveHandler {}// Adjective + Adjective + Noun export 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 = null  private _connected = false  private _resolved = false  private _numberProps: Record<string, true> | null = null  private _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等工具辅助检查。
  • 主观上:要有工匠精神,个人认为正确的代码规范就得持续保持,"自成一派",不能潜移默化地受其他风格影响。

我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!

Node 社群


我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞在看” 支持一波👍

程序员成长指北
专注 Node.js 技术栈分享,从 前端 到 Node.js 再到 后端数据库,祝您成为优秀的高级 Node.js 全栈工程师。一个有趣的且乐于分享的人。座右铭:今天未完成的,明天更不会完成。
 最新文章