浅读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',
}

// 枚举项使用pascalCase
enum BooleanFlags {
  shouldCast,
  shouldCastTrue,
}

通过对Vue、Element-plus源码浅读分析,罗列出30个代码规范建议,前半部分和Vue相关,后半部分和Javascript相关。如果表述有误,欢迎指正。 需要说明的是:代码规范没有标准答案,只有适合、不适合。其目的是让项目代码保持风格统一, 提升易读性, 降低上手成本。

目录规范

模块命名

采用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单词命名。如果为同类功能的文件夹,则一般以复数形式结尾,常用的有componentsutils等等。

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定义使用pascalCase命名,在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:业务方法命名

使用pascalCase小驼峰,以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标识私有,所以私有成员使用pascalCase命名即可。

Vue.js源码在早年前的Class偏向于使用_pascalCase命名,现在也逐渐使用pascalCase。

// 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等工具辅助检查。
  • 主观上:要有工匠精神,个人认为正确的代码规范就得持续保持,"自成一派",不能潜移默化地受其他风格影响。

希望这篇文章对你有所帮助!接下来给大家推荐一篇我针对前端面试的多年经验文章,希望大家看完以后都可以领取到心仪的offer哦!

文章:《聊聊前端面试那些事儿》

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值