前端规范 ⭐️⭐️
更新: 2/10/2025 字数: 0 字 时长: 0 分钟
1、项目规范
assets
: 为静态资源,里面存放images
,styles
,icons
等静态资源。静态资源命名格式为 kebab-casecomponents
: 一个组件一个目录是为了将来组件的扩展性,方便后期维护。目录命名为 kebab-case
src 源码目录
|-- api 所有api接口
|-- assets 静态资源,images, icons, styles等
| |-- icons
| |-- images
| | |-- background-color.png
| | |-- upload-header.png
|-- styles
|-- components 公用组件
| |-- markdown-editor
| | |-- index.vue
| | |-- index.js
|-- config 配置信息
|-- constants 常量信息,项目所有Enum, 全局常量等
|-- directives 自定义指令
|-- i18n 国际化
|-- lib 外部引用的插件存放及修改文件
|-- mock 模拟接口,临时存放
|-- plugins 插件,全局使用
|-- router 路由,统一管理
|-- store vuex, 统一管理
|-- theme 自定义样式主题
|-- utils 工具类
|-- views 视图目录
| |-- role role模块名
| | |-- role-list.vue role列表页面
| | |-- role-add-form.vue role新建页面
| | |-- role-update-modal.vue role更新页面
| | |-- index.less role模块样式
| | |-- components role模块通用组件文件夹
| | | |-- role-title-modal.vue role弹出框组件
| |-- employee employee模块
| |-- behavior-log 行为日志log模块
| |-- code-generator 代码生成器模块
2、Vue 组件规范
组件文件名应该为 pascal-case 格式,例如
my-component.vue
。
components
|- my-component.vue
components
|- MyComponent.vue
2.1、父子组件文件名
和父组件紧密耦合的子组件应该以父组件名作为前缀命名。
components
|- todo-list.vue
|- todo-list-item.vue
|- todo-list-item-button.vue
|- user-profile-options.vue (完整单词)
components
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
|- UProfOpts.vue (使用了缩写)
2.2、模板中表达式
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。复杂表达式会让你的模板变得不那么声明式。应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。
<template>
<p>{{ normalizedFullName }}</p>
</template>
<script>
// 复杂表达式已经移入一个计算属性
const normalizedFullName = () => {
return this.fullName
.split(' ')
.map(function (word) {
return word[0].toUpperCase() + word.slice(1)
})
.join(' ')
}
</script>
<template>
<p>
{{
fullName
.split(' ')
.map(function (word) {
return word[0].toUpperCase() + word.slice(1)
})
.join(' ')
}}
</p>
</template>
2.3、组件类型命名规范
按照设计师和前端工程师的共同认知进行命名和归类
基础(Basic) | 布局(Layout) | 导航(Navigations) | 数据类(Data) | 沟通类(Notice) | 输入类(Input) | 其他 Others |
---|---|---|---|---|---|---|
色彩 Color | 布局 Layout | 选项卡 Tabs | 图表 Diagram | 全局提醒 Message | 输入框 Input | 模态抽屉 Drawer |
字体 Fonts | 栅格 Grid | 步骤条 Steps | 列表 List | 文字提示 Tootip | 日期选择器 DatePicker | 锚点 Anchor |
图标 Icon | 卡片 Card | 面包屑 Breadcrumb | 表格 Table | 加载中 Loading | 时间选择器 TimePicker | 标签 Tag |
动效 Motion | 走马灯 Carousel | 分页 Pagination | 表单 Form | 警告提示 Alert | 单选框 Radio | |
按钮 Button | 分割线 Divider | 导航菜单 Menu | 树 Tree | 徽标数 Badge | 多选框 Checkbox | |
用户引导 Onboarding | 搜索框 Search | |||||
气泡确认 Popconfirm | 穿梭框 Transfer | |||||
对话框 Dialog | 下拉菜单 Dropdown | |||||
消息通知 Notification | 树选择 TreeSelect | |||||
上传 Upload | ||||||
开关 Switch | ||||||
选择器 Select |
3、接口设计
好的 Interface 是开发者最快能搞清楚组件入参的途径,也是让你后续开发能够有更好代码提示的前提。
type Size = any // 😖 ❌
type Size = string // 🤷🏻♀️
type Size = 'small' | 'medium' | 'large' // ✅
3.1、DOM属性
组件最终需要变成页面DOM,所以如果你的组件不是那种一次性的,请默认顺手定义基础的DOM属性类型。
3.2、型注释
export
组件props
类型定义为组件暴露的类型添加 规范的注释
export type IListProps = {
/**
* Custom suffix element.
* Used to append element after list
*/
suffix?: React.ReactNode
/**
* List column definition.
* This makes List acts like a Table, header depends on this property
* @default []
*/
columns?: IColumn[]
/**
* List dataSource.
* Used with renderRow
* @default []
*/
dataSource?: Array<Record<string, any>>
}
上面的类型注释就是一个规范的类型注释,清晰的类型注释可以让消费者,直接点击进入你的类型定义中查看到关于这个参数的清晰解释。
小技巧:如果你非常厌倦写这些注释,不如试试著名的AI代码插件:Copilot
,它可以帮你快速生成你想要表达的文字。
3.3、组件插槽
对于一个组件开发新手来说,往往会犯 string
类型替代 ReactNode
的错误。
比如要对一个 Input 组件定义一个 label 的 props ,许多新手开发者会使用 string 作为 label 类型,但这是错误的。
export type IInputProps = {
label?: string // ❌
}
export type IInputProps = {
label?: React.ReactNode // ✅
}
遇到这种类型时,需要意识到我们其实是在提供一个 React 插槽类型,如果在组件消费中仅仅是让他展示出来,而不做其他处理的话,就应当使用 ReactNode 类型作为类型定义。
3.4、受控 与 非受控
如果要封装的组件类型是 数据输入
的用途,也就是存在双向绑定的组件。请务必提供以下类型定义:
export type IFormProps<T = string> = {
value?: T
defaultValue?: T
onChange?: (value: T, ...args) => void
}
并且,这类接口定义不一定是针对 value
, 其实对于所有有 受控需求 的组件都需要,比如:
export type IVisibleProps = {
/**
* The visible state of the component.
* If you want to control the visible state of the component, you can use this property.
* @default false
*/
visible?: boolean
/**
* The default visible state of the component.
* If you want to set the default visible state of the component, you can use this property.
* The component will be controlled by the visible property if it is set.
* @default false
*/
defaultVisible?: boolean
/**
* Callback when the visible state of the component changes.
*/
onVisibleChange?: (visible: boolean, ...args) => void
}
具体原因请查看:《受控组件和非受控组件》
3.5、表单类常用属性
如果你正在封装一个表单类型的组件,未来可能会配合 antd
/ fusion
等 Form 组件来消费,以下这些类型定义你可能会需要到:
export type IFormProps = {
/**
* Field name
*/
name?: string
/**
* Field label
*/
label?: ReactNode
/**
* The status of the field
*/
state?: 'loading' | 'success' | 'error' | 'warning'
/**
* Whether the field is disabled
* @default false
*/
disabled?: boolean
/**
* Size of the field
*/
size?: 'small' | 'medium' | 'large'
/**
* The min value of the field
*/
min?: number
/**
* The max value of the field
*/
max?: number
}
3.6、选择类型
如果你正在开发一个需要选择的组件,可能以下类型你会用到:
export interface ISelection<T extends object = Record<string, any>> {
/**
* The mode of selection
* @default 'multiple'
*/
mode?: 'single' | 'multiple'
/**
* The selected keys
*/
selectedRowKeys?: string[]
/**
* The default selected keys
*/
defaultSelectedRowKeys?: string[]
/**
* Max count of selected keys
*/
maxSelection?: number
/**
* Whether take a snapshot of the selected records
* If true, the selected records will be stored in the state
*/
keepSelected?: boolean
/**
* You can get the selected records by this function
*/
getProps?: (record: T, index: number) => { disabled?: boolean; [key: string]: any }
/**
* The callback when the selected keys changed
*/
onChange?: (selectedRowKeys: string[], records?: Array<T>, ...args: any[]) => void
/**
* The callback when the selected records changed
* The difference between `onChange` is that this function will return the single record
*/
onSelect?: (selected: boolean, record: T, records: Array<T>, ...args: any[]) => void
/**
* The callback when the selected all records
*/
onSelectAll?: (selected: boolean, keys: string[], records: Array<T>, ...args: any[]) => void
}
上述参数定义,你可以参照 Merlion UI - useSelection[10]查看并消费。
另外,单选与多选存在时,组件的 value 可能会需要根据下传的 mode 自动变化数据类型。
比如,在 Select 组件中就会有以下区别:
mode="single" -> value: string | number
mode="multiple" -> value: string[] | number[]
所以对于需要 多选、单选 的组件来说,value 的类型定义会有更多区别。
对于这类场景可以使用 Merlion UI - useCheckControllableValue[11]进行抹平。
4、组件设计
4.1、服务请求
这是一个在业务组件设计中经常会遇到的组件设计,对于很多场景来说,或许我们只是需要替换一下请求的 url ,于是便有了类似下面这样的API设计:
export type IAsyncProps = {
requestUrl?: string
extParams?: any
}
后面接入方增多后,出现了后端的 API 结果不符合组件解析逻辑,或者出现了需要请求多个API组合后才能得到组件所需的数据,于是一个简单的请求就出现了以下这些参数:
export type IAsyncProps = {
requestUrl?: string
extParams?: any
beforeUpload?: (res: any) => any
format?: (res: any) => any
}
这还只是其中一个请求,如果你的业务组件需要 2个、3个呢?组件的API就会变得越来越多,越来越复杂,这个组件慢慢的也就变得没有易用性 ,也慢慢没有了生气。
对于异步接口的API设计最佳实践应该是:提供一个 Promise 方法,并且详细定义其入参、出参类型。
export type ProductList = {
total: number
list: Array<{ id: string; name: string; image: string }>
}
export type AsyncGetProductList = (pageInfo: { current: number; pageSize: number }, searchParams: { name: string; id: string }) => Promise<ProductList>
export type IComponentProps = {
/**
* The service to get product list
*/
loadProduct?: AsyncGetProductList
}
通过这样的参数定义后,对外只暴露了 1 个参数,该参数类型为一个 async 的方法。开发者需要下传一个符合上述入参和出参类型定义的函数。
在使用时组件内部并不关心请求是如何发生的,使用什么方式在请求,组件只关系返回的结果是符合类型定义的即可。
这对于使用组件的开发者来说是完全白盒的,可以清晰的看到需要下传什么,以及友好的错误提示等等。
4.2、Hooks
很多时候,或许你不需要组件!
对于很多业务组件来说,很多情况我们只是在原有的组件基础上封装一层浅浅的业务服务特性,比如:
- Lazada Uploader:Upload + Lazada Upload Service
- Address Selector: Select + Address Service
- Brand Selector: Select + Brand Service
- ...
而对于这种浅浅的胶水组件,实际上组件封装是十分脆弱的。因为业务会对UI有各种调整,对于这种重写成本极低的组件,很容易导致组件的垃圾参数激增。
实际上,对于这类对服务逻辑的状态封装,更好的办法是将其封装为 React Hooks ,比如上传:
export function Page() {
const lzdUploadProps = useLzdUpload({ bu: 'seller' })
return <Upload {...lzdUploadProps} />
}
这样的封装既能保证逻辑的高度可复用性,又能保证 UI 的灵活性。
4.3、Consumer
对于插槽中需要使用到组件上下文的情况,我们可以考虑使用 Consumer 的设计进行组件入参设计。
对于这种类型的组件,明显容器内的内容需要拿到 isExpand 这个关键属性,从而决定索要渲染的内容,所以我们在组件设计时,可以考虑将其设计成可接受一个回调函数的插槽:
export type IExpandProps = {
children?: (ctx: { isExpand: boolean }) => React.ReactNode
}
而在消费侧,则可以通过以下方式轻松消费:
export function Page() {
return (
<Expand>
{({ isExpand }) => {
return isExpand ? <Table /> : <AnotherTable />
}}
</Expand>
)
}
5、文档设计
5.1、package.json
请确保你的 repository 是正确的仓库地址,因为这里的配置是很多平台溯源的唯一途径,比如: npmjs.com\npm.alibaba-inc.com\mc.lazada.com
请确保 package.json 中存在常见的入口定义,比如 main\module\types\exports,以下是一个 package.json 的示范:
{
"name": "xxx-ui",
"version": "1.0.0",
"description": "Out-of-box UI solution for enterprise applications from B-side.",
"author": "yee.wang@xxx.com",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"repository": {
"type": "git",
"url": "git@github.com:yee94/xxx.git"
}
}
5.2、README.md
如果你在做一个库,并希望有人来使用它,请至少为你的库提供一段描述,在我们的脚手架模板中已经为你生成了一份模板,并且会在编译过程中自动加入在线 DEMO 地址,但如果可以请至少为它添加一段描述。
这里的办法有很多,如果你实在不知道该如何写,可以找一些知名的开源库来参考,比如 antd
\ react
\ vue
等。
还有一个办法,或许你可以寻求 ChatGPT
的帮助,屡试不爽😄。