Vue3 代码规范
目录规范
模块命名
采用 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"
></component>所有 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 + Adjectiveboolean 类型参数命名
函数、方法、构造函数的参数,如果为 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。
reateObjectMatcher(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 等工具辅助检查。
- 主观上:要有工匠精神,个人认为正确的代码规范就得持续保持,"自成一派",不能潜移默化地受其他风格影响。
