仿 ElementPlus 组件库(一)—— 初始化项目与 Button 组件实现

在前端开发领域,组件库极大地提升了开发效率和项目的一致性。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-devnpm install postcss-color-mix --save-devnpm 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;
}
相关推荐
2301_764441332 分钟前
小说文本分析工具:基于streamlit实现的文本分析
前端·python·信息可视化·数据分析·nlp
jackl的科研日常16 分钟前
“个人陈述“的“十要“和“十不要“
前端
一个处女座的程序猿O(∩_∩)O20 分钟前
Vue 中 this 使用指南与注意事项
前端·javascript·vue.js
程序员大澈37 分钟前
7个 Vue 路由守卫的执行顺序
javascript·vue.js
程序员大澈1 小时前
4个 Vue mixin 的原理拆解
javascript·vue.js
程序员大澈1 小时前
3个 Vue $set 的应用场景
javascript·vue.js
大有数据可视化1 小时前
数字孪生像魔镜,映照出无限可能的未来
前端·html·webgl
程序员大澈1 小时前
3个 Vue nextTick 原理的关键点
javascript·vue.js
一个处女座的程序猿O(∩_∩)O1 小时前
使用 Docker 部署前端项目全攻略
前端·docker·容器
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_10空状态的固定表头表格
前端·javascript·vue.js·ecmascript·deepseek