在前端开发领域,组件库极大地提升了开发效率和项目的一致性。Element - Plus 作为一款优秀的 Vue 组件库,深受开发者喜爱。今天,让我们踏上仿 Element - Plus 组件库的征程,通过实践深入理解组件库开发的精髓。本文将聚焦于项目初始化以及 Button 组件的实现,技术栈选用 TypeScript 和 Vue3。
一、初始化项目
(一)创建 Vue 项目
详情可参考:Vue3项目搭建全流程指南
(二)项目结构规划
项目创建完成后,进入项目目录
详情可参考:Vue3项目初始目录结构详解
(三)安装必要工具
- ESLint
ESLint 是一个用于 JavaScript 和 TypeScript 的静态代码分析工具,在 Vue 项目中使用它有诸多好处
- ESLint 通过对代码进行语法和语义分析,检查代码是否符合特定的规则和规范。它可以帮助开发者发现代码中的潜在问题,如语法错误、逻辑错误、代码风格不一致等。
- 进入项目目录,在终端输入
npm init @eslint/config
进行安装和初始化。
- Vue Macros
Vue Macros 是一个用于扩展 Vue 功能的库,它实现了一些 Vue 尚未正式支持的功能提案或想法,让开发者能够提前使用可能在未来版本中出现的特性,从而在开发过程中获得更高的灵活性与生产力。
- 在终端输入
npm i -D vue-macros
进行安装
- 添加相应配置
vite.config.js
import VueMacros from 'vue-macros/vite'
export default defineConfig({
plugins: [
VueMacros({
plugins: {
vue: vue(),
vueJsx:vueJsx(),
},
}),
tsconfig.json
"compilerOptions": {
"types": ["vue-macros/macros-global"]
},
"vueCompilerOptions": {
"plugins": ["vue-macros/volar"]
},
二、实现 Button 组件
(一)组件目录与初始代码结构
目录
components
├── Button
├── Button.vue
├── style.css
├── types.ts #辅助的typescript类型
└── Button.test.tsx #测试文件
Button.vue初始结构
Button.vue
<template>
<button>
</button>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
分别定义按钮类型、尺寸和属性接口
types.ts
export type ButtonType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
export type ButtonSize = 'large' | 'small'
export interface ButtonProps {
type?: ButtonType
size?: ButtonSize
plain?: boolean
round?: boolean
circle?: boolean
disabled?: boolean
nativeType?: NativeType
autofocus?: boolean
}
(二)编写Button组件
Button.vue
<template>
<button
class="yl-button"
:class="{
[`yl-button--${type}`]: type,
[`yl-button--${size}`]: size,
'is-plain': plain,
'is-round': round,
'is-circle': circle,
'is-disabled': disabled,
}"
:disabled="disabled"
:autofocus="autofocus"
:type="nativeType"
>
<slot>
</slot>
</button>
</template>
<script setup lang="ts">
import type { ButtonProps } from './types'
defineOptions({
name:'YlButton'
})
withDefaults(defineProps<ButtonProps>(),{
nativeType:'button'
})
</script>
- 类名采用了BEM命名规范
(三)defineExpose 暴露组件的属性
Button.vue
import {ref} from 'vue'
const _ref = ref<HTMLButtonElement>()
defineExpose({
ref: _ref,
})
此时可以在不破坏组件封装性的前提下,实现父组件与子组件之间的交互。
执行如下代码发现可以成功获取DOM节点。
App.vue
import type { ButtonInstance } from './components/Button/types'
const buttonRef = ref<ButtonInstance | null>(null)
onMounted(() => {
if (buttonRef.value) {
console.log('buttonRef', buttonRef.value.ref)
}
})
<template>
<header></header>
<main>
<Button type="primary" disabled ref="buttonRef">Test Button</Button>
</main>
</template>
(三)添加CSS样式
CSS解决方案:PostCSS
- postCSS是一个用 JavaScript 工具和插件转换 CSS 代码的工具
- 开发者可以根据需求选择不同的插件来完成特定的任务,如添加浏览器前缀、将 CSS 转换为 JavaScript 对象、压缩 CSS 代码等。
- PostCSS 可以很方便地与各种前端构建工具集成,如 Webpack、Vite、Gulp 等,成为构建流程中的一部分。
安装对应插件:在终端输入npm install --save-dev postcss postcss-nested
在根目录添加postcss.config.js
文件,并进行配置
postcss.config.js
module.exports = {
plugins: [
require('postcss-nested'),
]
}
安装相应插件
统一浏览器默认样式
在styles文件夹新建文件reset.css
。
reset.css
body {
font-family: var(--vk-font-family);
font-weight: 400;
font-size: var(--vk-font-size-base);
color: var(--vk-text-color-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: transparent;
}
a {
color: var(--vk-color-primary);
text-decoration: none;
&:hover,
&:focus {
color: var(--vk-color-primary-light-3);
}
&:active {
color: var(--vk-color-primary-dark-2);
}
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--vk-text-color-regular);
font-weight: inherit;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
h1 {
font-size: calc(var(--vk-font-size-base) + 6px);
}
h2 {
font-size: calc(var(--vk-font-size-base) + 4px);
}
h3 {
font-size: calc(var(--vk-font-size-base) + 2px);
}
h4,
h5,
h6,
p {
font-size: inherit;
}
p {
line-height: 1.8;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
sup,
sub {
font-size: calc(var(--vk-font-size-base) - 1px);
}
small {
font-size: calc(var(--vk-font-size-base) - 2px);
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid var(--vk-border-color-lighter);
}
在main.ts
,index.css
做相应引入
styles/index.css
@import './vars.css';
@import './reset.css';
main.ts
import './styles/index.css' #引入并应用外部CSS文件
为Button添加样式
在终端输入npm install postcss-each --save-dev
安装依赖,postcss-each
插件允许开发者在 CSS 中使用类似于循环的功能,对一组选择器或属性进行重复处理,
更新配置
postcss.config.js
module.exports = {
plugins: [
//...
require('postcss-each'),
]
}
Button/style.css
.yl-button {
--yl-button-font-weight: var(--yl-font-weight-primary);
--yl-button-border-color: var(--yl-border-color);
--yl-button-bg-color: var(--yl-fill-color-blank);
--yl-button-text-color: var(--yl-text-color-regular);
--yl-button-disabled-text-color: var(--yl-disabled-text-color);
--yl-button-disabled-bg-color: var(--yl-fill-color-blank);
--yl-button-disabled-border-color: var(--yl-border-color-light);
--yl-button-hover-text-color: var(--yl-color-primary);
--yl-button-hover-bg-color: var(--yl-color-primary-light-9);
--yl-button-hover-border-color: var(--yl-color-primary-light-7);
--yl-button-active-text-color: var(--yl-button-hover-text-color);
--yl-button-active-border-color: var(--yl-color-primary);
--yl-button-active-bg-color: var(--yl-button-hover-bg-color);
--yl-button-outline-color: var(--yl-color-primary-light-5);
--yl-button-active-color: var(--yl-text-color-primary);
}
.yl-button {
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
height: 32px;
white-space: nowrap;
cursor: pointer;
color: var(--yl-button-text-color);
text-align: center;
box-sizing: border-box;
outline: none;
transition: .1s;
font-weight: var(--yl-button-font-weight);
user-select: none;
vertical-align: middle;
-webkit-appearance: none;
background-color: var(--yl-button-bg-color);
border: var(--yl-border);
border-color: var(--yl-button-border-color);
padding: 8px 15px;
font-size: var(--yl-font-size-base);
border-radius: var(--yl-border-radius-base);
& + & {
margin-left: 12px;
}
&:hover,
&:focus {
color: var(--yl-button-hover-text-color);
border-color: var(--yl-button-hover-border-color);
background-color: var(--yl-button-hover-bg-color);
outline: none;
}
&:active {
color: var(--yl-button-active-text-color);
border-color: var(--yl-button-active-border-color);
background-color: var(--yl-button-active-bg-color);
outline: none;
}
&.is-plain {
--yl-button-hover-text-color: var(--yl-color-primary);
--yl-button-hover-bg-color: var(--yl-fill-color-blank);
--yl-button-hover-border-color: var(--yl-color-primary);
}
/*round*/
&.is-round {
border-radius: var(--yl-border-radius-round);
}
/*circle*/
&.is-circle {
border-radius: 50%;
padding: 8px;
}
/*disabled*/
&.is-disabled, &.is-disabled:hover, &.is-disabled:focus,
&[disabled], &[disabled]:hover, &[disabled]:focus
{
color: var(--yl-button-disabled-text-color);
cursor: not-allowed;
background-image: none;
background-color: var(--yl-button-disabled-bg-color);
border-color: var(--yl-button-disabled-border-color);
}
[class*=yl-icon] + span {
margin-left: 6px;
}
}
@each $val in primary,success,warning,info,danger {
.yl-button--$(val) {
--yl-button-text-color: var(--yl-color-white);
--yl-button-bg-color: var(--yl-color-$(val));
--yl-button-border-color: var(--yl-color-$(val));
--yl-button-outline-color: var(--yl-color-$(val)-light-5);
--yl-button-active-color: var(--yl-color-$(val)-dark-2);
--yl-button-hover-text-color: var(--yl-color-white);
--yl-button-hover-bg-color: var(--yl-color-$(val)-light-3);
--yl-button-hover-border-color: var(--yl-color-$(val)-light-3);
--yl-button-active-bg-color: var(--yl-color-$(val)-dark-2);
--yl-button-active-border-color: var(--yl-color-$(val)-dark-2);
--yl-button-disabled-text-color: var(--yl-color-white);
--yl-button-disabled-bg-color: var(--yl-color-$(val)-light-5);
--yl-button-disabled-border-color: var(--yl-color-$(val)-light-5);
}
.yl-button--$(val).is-plain {
--yl-button-text-color: var(--yl-color-$(val));
--yl-button-bg-color: var(--yl-color-$(val)-light-9);
--yl-button-border-color: var(--yl-color-$(val)-light-5);
--yl-button-hover-text-color: var(--yl-color-white);
--yl-button-hover-bg-color: var(--yl-color-$(val));
--yl-button-hover-border-color: var(--yl-color-$(val));
--yl-button-active-text-color: var(--yl-color-white);
}
}
.yl-button--large {
--yl-button-size: 40px;
height: var(--yl-button-size);
padding: 12px 19px;
font-size: var(--yl-font-size-base);
border-radius: var(--yl-border-radius-base);
}
.yl-button--small {
--yl-button-size: 24px;
height: var(--yl-button-size);
padding: 5px 11px;
font-size: 12px;
border-radius: calc(var(--yl-border-radius-base) - 1px);
}
使用PostCSS生成对应色彩变量
在终端输入npm install postcss-for --save-dev
、npm install postcss-color-mix --save-dev
和npm install --save-dev postcss-each-variables
安装依赖
postcss-for
插件允许你在 CSS 中使用类似编程语言里的for
循环结构,通过循环,你可以基于循环变量生成一系列相似的 CSS 规则。postcss-color-mix
插件让你可以在 CSS 中使用color-mix()
函数,这个函数用于混合两种颜色,生成新的颜色。这在需要动态生成颜色,或者根据不同主题调整颜色时非常有用。
更新配置
postcss.config.js
plugins: [
require('postcss-each-variables'),
//...
require('postcss-each')({
plugins: {
beforeEach: [
require('postcss-for'),
require('postcss-color-mix'),
]
}
}),
]
styles/vars.css
:root {
/* colors */
--yl-color-white: #ffffff;
--yl-color-black: #000000;
--colors: (primary: #409eff, success: #67c23a, warning: #e6a23c, danger: #f56c6c, info: #909399);
@each $val, $color in var(--colors) {
--yl-color-$(val): $(color);
@for $i from 3 to 9 by 2 {
--yl-color-$(val)-light-$(i): mix(#fff, $(color), .$(i))
}
--yl-color-$(val)-light-8: mix(#fff, $(color), .8);
--yl-color-$(val)-dark-2: mix(#000, $(color), .2);
}
/* border */
--yl-border-width: 1px;
--yl-border-style: solid;
--yl-border-color-hover: var(--yl-text-color-disabled);
--yl-border: var(--yl-border-width) var(--yl-border-style) var(--yl-border-color);
--yl-border-radius-base: 4px;
--yl-border-radius-small: 2px;
--yl-border-radius-round: 20px;
--yl-border-radius-circle: 100%;
/*font*/
--yl-font-size-extra-large: 20px;
--yl-font-size-large: 18px;
--yl-font-size-medium: 16px;
--yl-font-size-base: 14px;
--yl-font-size-small: 13px;
--yl-font-size-extra-small: 12px;
--yl-font-family:
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei',
'\5fae\8f6f\96c5\9ed1', Arial, sans-serif;
--yl-font-weight-primary: 500;
/*disabled*/
--yl-disabled-bg-color: var(--yl-fill-color-light);
--yl-disabled-text-color: var(--yl-text-color-placeholder);
--yl-disabled-border-color: var(--yl-border-color-light);
/*animation*/
--yl-transition-duration: 0.3s;
--yl-transition-duration-fast: 0.2s;
}