1.1 简单轮子 VueButton 和 VueIcon 组件:按钮组件

大纲链接 §

[toc]


需求分析

用例图 user case 参考 ant-design

  • 点击按钮
    • 按钮 loading 状态 加载完毕 自动回复初始状态
    • 按钮 不可点击 状态 disable 提示不可点击
    • 按钮 hover 状态(手机没有hover
    • 按钮 按下状态
  • 其他
    • 圆形按钮
    • 腰圆形按钮
    • 按钮组
    • 弹出初级菜单按钮

按钮状态 11 种

  • enable默认状态
  • hover状态 (移动端无)
  • focus状态
  • error状态 (危险色提示文字)
    • error hover状态 (移动端无)
    • error focus状态
  • success状态 (原谅色提示文字)
    • success hover状态 (移动端无)
    • success focus状态
  • disable状态
  • readonly状态

注意点

  • 按钮高度 和 input 输入框 一致
  • 不写死按钮的宽度,而是设置padding: 0 1em,左右的内边距空出各一个字

API设计

  • 设置颜色样式
  • 设置图标与图标位置
  • 设置尺寸
  • 设置禁用状态
  • 设置加载中状态
  • 按钮组

项目初始化

新建目录,自定义目录名

1
2
cd ~/desktop
mkdir lunzi-demo

创建 github 仓库

  • 略略略

声明软件许可

  • github中新建文件LICENSE
  • choose a license template
    • 选择 MIT License

初始化仓库

1
2
3
4
yarn init
# 全部使用默认选项 # yarn init -y
# 或者用
# npm init
  • 按提示输入

安装 vue@2.6.11

1
2
yarn add vue@2.6.11
yarn add --dev @types/vue
  • 开发者使用的包,加--dev或者-D

添加 .gitignore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.DS_Store/
node_modules/
dist/
.cache/
.yarn/

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode

使用@vue/cli 搭建前端项目目录架子

  • 过程略略略
  • 页面 ./public/index.html
  • 入口文件
    • ./src/main.ts
    • ./src/App.vue
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 ┣ src
 ┃ ┣ assets
 ┃ ┃ ┣ icons
 ┃ ┃ ┃ ┣ account.svg
 ┃ ┃ ┃ ┣ bills.svg
 ┃ ┃ ┃ ┣ ...
 ┃ ┃ ┃ ┗ svg.js
 ┃ ┃ ┗ logo.png
 ┃ ┣ components
 ┃ ┃ ┣ button
 ┃ ┃ ┃ ┣ VueButton.vue
 ┃ ┃ ┃ ┗ VueButtonGroup.vue
 ┃ ┃ ┣ button-group
 ┃ ┃ ┣ icon
 ┃ ┃ ┃ ┗ VueIcon.vue
 ┃ ┃ ┣ Buttons.vue
 ┃ ┃ ┗ Nav.vue
 ┃ ┣ router
 ┃ ┃ ┗ index.ts
 ┃ ┣ store
 ┃ ┃ ┗ index.ts
 ┃ ┣ styles
 ┃ ┃ ┣ components
 ┃ ┃ ┃ ┣ index.scss
 ┃ ┃ ┃ ┣ vue-button-group.scss
 ┃ ┃ ┃ ┣ vue-button.scss
 ┃ ┃ ┃ ┗ vue-icon.scss
 ┃ ┃ ┣ global.scss
 ┃ ┃ ┣ index.scss
 ┃ ┃ ┣ normalize.scss
 ┃ ┃ ┗ reset.scss
 ┃ ┣ types
 ┃ ┃ ┗ custom.d.ts
 ┃ ┣ views
 ┃ ┃ ┣ About.vue
 ┃ ┗ ┗ Layout.vue
 ┣ App.tsx
 ┣ main.ts
 ┣ shims-tsx.d.ts
 ┣ shims-vue.d.ts
 ┣ vue.config.js
 ┣ .eslintrc.js
 ┣ .gitignore.js
 ┣ babel.config.js
 ┣ index.textscr
 ┣ package.json
 ┣ README.md
 ┗ tsconfig.json

VueButton结构

VueButton.vue使用<slot></slot> 在自定义组件的双标签(可嵌套)中传入内容

1
2
3
4
5
6
7
<template>
  <div>
    <button class="v-button">
      <slot></slot>
    </button>
  </div>
</template>

在父组件中使用

1
2
3
4
5
6
7
<template>
  <div id="app">
    <VueButton>
      按钮
    </VueButton>
  </div>
</template>
  • 效果是显示<button class="v-button">按钮</button>

添加icon

批量添加icon

  • 使用 iconfont.cn 提供的SVG图标,来代替过时的CSS雪碧图
  • 搜索「设置」、「点赞」、「下载」、「左」,选取按钮样式的SVG图标,添加至项目文件夹
  • Unicode/Font class/Symbol点选Symbol
  • 在更多操作中,更改前缀为i,命名都用英文
    • i-arrow-down向下箭头
    • i-download下载
    • i-settings设置
    • i-thumbs-up点赞
    • i-left
  • iconfont 查看大图,是否居中对齐,调整大小,基本一致
  • 点击SVG下载,调整SVG,打开SKETCH,或者 Photoshop 制作 i-right右,翻转180
  • 上传,去色,放至项目文件夹中
  • 批量去色
  • 查看在线链接,生成JS代码 //at.alicdn.com/t/font_2138557_5w8054iu1s4.js
  • 可在帮助文档 功能介绍中查看 代码应用
  • svg 图标的颜色 可在标签上添加fill属性 fill: red;

index.html中引入JS <script src="https://at.alicdn.com/t/font_2138557_5w8054iu1s4.js"></script>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <link rel="icon" href="<%= BASE_URL %>favicon.ico">
  <title><%= htmlWebpackPlugin.options.title %></title>
  <link rel="stylesheet" href="../src/style/normalize.scss">
</head>
<body>
<noscript>
  <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
    Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="test"></div>
<!-- built files will be auto injected -->
</body>
<script src="////at.alicdn.com/t/font_2138557_fmq5zqg2y0j.js"></script>
</html>

VueButton.vue组件中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
    <div class="vue-demo-button">
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-arrow-down`"></use>
        </svg>
        <slot></slot>
      </button>
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-download`"></use>
        </svg>
        <slot></slot>
      </button>
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-settings`"></use>
        </svg>
        <slot></slot>
      </button>
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-thumbs-up`"></use>
        </svg>
        <slot></slot>
      </button>
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-left`"></use>
        </svg>
        <slot></slot>
      </button>
      <button class="vue-button">
        <svg v-if="icon" class="icon" aria-hidden="true">
          <use :xlink:href="`#i-right`"></use>
        </svg>
        <slot></slot>
      </button>
    </div>
</template>
  • <svg>...</svg> 移至父组件定义在<slot></slot>
  • 写样式.icon,使之适合按钮尺寸,一般为一个字的大小,即width: 1em; height: 1em;

App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<template>
  <div id="app">
    <VueButton>
      <svg class="icon" aria-hidden="true"><use xlink:href="#i-settings"></use></svg>按钮
    </VueButton>
  </div>
</template>

<style scoped>
/* ali iconfont common css */
.icon {
  width: 1em; height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
  }
</style>
  • 1em和字体一样高
  • 文字对齐的样式写在VueButton.vue组件内部样式中,而在外部App.vue直接使用不考虑具体样式
  • VueButton.vue接受外部数据props,将icon名称属性在<VueButton></VueButton>中传入

App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<template>
  <div id="app">
    <VueButton icon="settings">
      按钮
    </VueButton>
  </div>
</template>

<script>
import VueButton from './components/vuebutton/VueButton.vue'

export default {
  name: 'App',
  components: {
    VueButton,
  }
}
</script>

VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
  <div class="VueButton">
    <button class="vue-button">
      <svg class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
      <slot></slot>
    </button>
  </div>
</template>

<script>
export default {
  props: ['icon'],
</script>

<style lang="scss">
:root {
  --button-height: 32px;
  --font-size: 14px;
  --button-bg: white;
  --button-active-bg: #eee;
  --border-radius: 4px;
  --color: #333;
  --border-color: #999;
  --border-color-hover: #666;
  }

.vue-button {
  font-size: var(--font-size);
  height: var(--button-height);
  padding: 0 1em;
  font: inherit;
  border-radius: var(--border-radius);
  border: 1px solid var(--border-color);
  background: var(--button-bg);
  &:hover {
    border-color: var(--border-color-hover);
    }
  &:active {
    background-color: var(--button-active-bg);
    }
  &:focus {
    outline: none;
    }
  }
/* ali iconfont common css */
.icon {
  width: 1em; height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
  }
</style>
  • 注意在VueButton.vue中的反引号表示JS字符串,使用插值${icon}定义字符串变量
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
    <use :xlink:href="`#i-${icon}`"></use>
</template>

<script>
//...
export default {
  props: ['icon'],
  //...
  }
</script>
  • 外部数据props导入['icon']
  • 绑定属性:xlink:href=""
  • JS字符串#i-${icon},模板字符串的插值${icon}
  • ${icon}的值是从props中取得
  • props的值是从外部的<VueButton icon="settings">按钮</VueButton>自定义属性取得settings,拼接成#i-settings
  • 之前引入的iconfont JS使之生效
  • 动态绑定属性 :xlink:href="`#i-${icon}`"

解决在App.vue未设置icon属性时的空字符占用的bug

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- App.vue -->
<template>
  <div id="app">
    <VueButton>
      按钮
    </VueButton>
    <VueButton icon="settings">
      按钮
    </VueButton>
  </div>
</template>

<!-- 显示在页面中的 -->
<template>
  <svg class="icon" aria-hidden="true">
    <use :xlink:href="`#i-undefined`"></use>
  </svg>
</template>
  • 使用v-if判断是否存在icon属性,为空,即undefined,则不显示<svg></svg>
  • 隐藏默认不传 SVG

VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
  <div>
    <button class="vue-button">
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
      <slot></slot>
    </button>
  </div>
</template>
  • 注意:v-if加在<svg>上,表示icon变量存在时,才出现<svg></svg>
  • SVG 添加类.icon {fill: red}添加颜色

控制图标icon位置

  • 首先排除两边都加icon的需求
  • 绑定属性:class,动态添加类icon-${iconPosition}
  • 控制图标的位置( CSS flex布局 order属性 间接控制图标与文字的顺序),注意设置初始值 'left'
  • 根据外部数据的不同名称,添加相应的类,切换名称即切换类
  • 使用<slot></slot>控制组件嵌中套的文本
  • 注意:在外部数据props中添加驼峰式的变量iconPosition
    • vue会自动转成XML可识别的属性icon-position
    • 在外部组件中可识别<VueButton icon="settings" icon-position="right">按钮</VueButton>
    • 可取值"left" || "right"

App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
  <div id="app">
    <VueButton>
      按钮
    </VueButton>
    <VueButton icon="settings">
      按钮
    </VueButton>
    <VueButton icon="settings" icon-position="right">
      按钮
    </VueButton>
  </div>
</template>

<script>
import VueButton from './components/vuebutton/VueButton.vue'

export default {
  name: 'App',
  components: {
    VueButton,
  }
}
</script>

VueButton.vue

1
2
3
export default {
  props: ['icon', 'iconPosition'], // 'iconPosition': 'left' || 'right'
}

v-if控制位置 VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div class="vue-demo-button">
    <button class="vue-button" v-if="!iconPosition || iconPosition === 'left'">
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
      <slot></slot>
    </button>
    <button class="vue-button" v-else>
      <slot></slot>
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
    </button>
  </div>
</template>
  • 使用<button v-if='...'> <button v-else> 判断语句来改变不同位置,重复代码太多
  • 默认v-if="icon-position === right"

VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<template>
  <div class="vue-demo-button">
    <button class="vue-button" v-if="iconPosition === 'right'">
      <slot></slot>
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
    </button>
    <button class="vue-button" v-else>
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
      <slot></slot>
    </button>
  </div>
</template>
  • 有重复就可能出现 bug,修改时可能遗漏
  • 有重复就可能出现 bug,修改时可能遗漏
  • 有重复就可能出现 bug,修改时可能遗漏

用CSS控制位置,做样式相关

  • 动态绑定一个类的属性:class="{ [iconPosition]: true }"
    • <button :class="{ 'icon-undefined': true}">
    • <button :class="{ 'icon-left': true}">
    • <button :class="{ 'icon-right': true}">
  • 类名变量iconPosition作为key,绑定:class
  • 使用插值字符串
1
<button :class="{ [`icon-${iconPosition}`]: true }">
  • <slot></slot>上加class属性无效
    • 需要在外层包一层<div class="content"><slot></slot></div>
  • 父子元素在一行中,父元素display: index-flex,子元素加order属性
  • 使用 CSS 间接控制子元素顺序
    • 默认> .icon { order: 1; } > .content { order: 2; }
    • 属性iconPositionright,则样式为&.icon-right { > .icon { order: 2; } > .content { order: 1; } }
  • 不建议使用CSS direction,不符合文字阅读习惯

VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
  <div class="vue-demo-button">
    <button class="vue-button" :class="{[`icon-${iconPosition}`]: true}">
      <svg v-if="icon" class="icon" aria-hidden="true">
        <use :xlink:href="`#i-${icon}`"></use>
      </svg>
      <div class="content">
        <slot/>
      </div>
    </button>
  </div>
</template>

<style lang="scss">
.vue-demo-button {
    display: inline-block;
    margin-right: 10px;
  }
.vue-button {
    &.icon-right {
        > .icon {
          order: 2;
          }
        > .content {
          order: 1;
          }
        }
      /* ali iconfont common css */
      > .icon {
        width: 1em; height: 1em;
        vertical-align: -0.15em;
        fill: currentColor;
        overflow: hidden;
        order: 1;
        }
      > .content {
        order: 2;
        }
      }
</style>
  • inline-* 元素导致 行内元素不对齐,需要加样式 vertical-align: middle;
  • 使icon和文字间有空隙,逼近法设置近似的值,使整体宽度近似设计稿
    • 注意margin两边都要设置:
      • &.icon-right{ > .icon {margin-left: .3em; margin-right: 0;}}
    • 都要清除(覆盖之前样式)另一边的空隙:
      • > .icon {margin-right: .3em; margin-left: 0;}
  • 调整padding.3em,符合设设计稿宽度

VueButton.vue 设置props的默认值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<script>
export default {
  props: {
    /*
    'icon': 'settings' || 'loading' || 'right' ||
      'left' || 'download' || 'arrow-down' || 'thumbs-up'
    */
    icon: {
      type: String,
    },
    // 'iconPosition': 'left' || 'right'
    iconPosition: {
      type: String,
      default: 'left'
    }
  },
}
</script>
  • props使用对象形式{}
    • key作为每个外部数据值的名字
    • 后面加值的配置,typedefault属性
  • 防呆,用外部数据的属性检查器validator,排除错误的(不合法的)属性值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script>
export default {
  props: {
    /*
    'icon': 'settings' || 'loading' || 'right' ||
      'left' || 'download' || 'arrow-down' || 'thumbs-up'
    */
    icon: {
      type: String,
    },
    // 'iconPosition': 'left' || 'right'
    iconPosition: {
      type: String,
      default: 'left',
      validator(userValue) {
        console.log(userValue)
        /*
        if (userValue !== 'left' && userValue !== 'right') {
          return false
        }else {
          return true
        }
        */
        // simplify if-else
        // return value !== 'left' && value !== 'right' ? false : true;
        return !(userValue !== 'left' && userValue !== 'right');
      },
    }
  },
</script>
  • 使用webStorm的智能简化if...else...代码
    • if (userValue !== 'left' && userValue !== 'right') {...} else {...}
    • return value !== 'left' && value !== 'right' ? false : true;
    • return !(userValue !== 'left' && userValue !== 'right');

每次使用图标时,重复定义了 <svg></svg> 及其样式,考虑代码复用

抽出组件Icon.vue

App.vue中全局注册组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script>
import Vue from 'vue'
import VueButtonGroup from './components/ButtonGroup/ButtonGroup.vue'
import VueButton from './components/vuebutton/VueButton.vue'
import VueIcon from './components/icon/Icon.vue'

// 全局注册组件
Vue.component('v-button', VueButton)
Vue.component('v-icon', VueIcon)
// ...
</script>

Icon.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<template>
    <svg class="v-icon">
      <use :xlink:href="`#i-${name}`"></use>
    </svg>
</template>

<script>
    export default {
      props: {
        name: {type: String},
      },
    };
</script>

<style lang="scss">
 .v-icon {
   width:  1em;
   height: 1em;
   }
</style>

VueButton.vue 使用子组件 Icon.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div class="vue-demo-button">
  <button class="vue-button" :class="{[`icon-${iconPosition}`]: true}">
    <!-- 组件化 <Icon> -->
    <VueIcon v-if="!!icon" :name="icon" class="icon" />
    <div class="content">
      <slot />
    </div>
  </button>
</div>
</template>

<script>
import VueIcon from '../icon/Icon.vue'
export default {
  props: {
    icon: {
      type: String,
    },
    iconPosition: {
      type: String,
      default: 'left',
      validator(userValue) {
        console.log(userValue)
        return (userValue === 'left' || userValue === 'right')
      },
    },
  },
  components: {
    VueIcon
  },
}
  • 注意传变量:name="icon"
  • 根据是否存在外部数据icon,来渲染<VueIcon v-if="!!icon" class="icon" :name="icon"></VueIcon>
  • 三层数据传递:
    1. App.vue中的VueButton icon="settings" icon-position="right">,传递icon="settings"icon-position="right"VueButton.vueprops
    2. VueButton.vue中的<VueIcon v-if="!!icon" :name="icon" class="icon" /><button class="vue-button" :class="{[icon-${iconPosition}]: true}">,接受props中的iconiconPosition,传递:name="icon"VueIcon.vueprops
    3. VueIcon.vue中的<svg class="v-icon"><use :xlink:href="#i-${name}"></use></svg>,接受props: {name, },
  • 经过字符模板拼接,来使用引入的字体图标<script src="//at.alicdn.com/t/font_2138557_rt8obmx2qyd.js"></script>的SVG symbol
  • App.vue中直接写<VueButton icon="download" icon-position="right">下载</VueButton>就可显示带对应图标和文字的按钮

添加loading状态

  • 更新图标库
  • 要求:
    • 点击按钮,显示loading图标,并隐藏原来的图标
    • 再点击,恢复原样

添加菊花动画VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
   <button class="vue-button" :class="{[`icon-${iconPosition}`]: true}">
      <VueIcon v-if="!!icon" :name="icon" class="icon"/>
      <div class="content">
        <slot/>
      </div>
    </button>
</template>

<style lang="scss">
@keyframes spin {
  0% {
    transform: rotate(0deg);
    }
  100% {
    transform: rotate(360deg);
    }
  }
 ...
.loading {
  animation: spin 1s infinite linear;
}
</style>

控制loading出现的逻辑

  • VueButton.vue添加外部数据isLoading,来判断是否显示loading图标
    • isLoading: { type: Boolean, default: false, }
  • VueButton.vue添加
    • <VueIcon v-if="!!icon && !isLoading" :name="icon" class="icon" />
    • <VueIcon v-if="isLoading" name="loading" class="loading icon"/>
    • 显示的逻辑,注意菊花图标同样有.icon的样式class="loading icon"

VueButton.vue添加条件渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<template>
  <button class="vue-button"
          :class="{ [`icon-${iconPosition}`]: true }"
          @click="$emit('click')">
    <VueIcon v-if="!!icon && !isLoading"
             :name="icon"
             class="icon"
             @click="clickLoading"/>
    <VueIcon v-if="isLoading"
             name="loading"
             class="icon loading"/>
    <div class="content">
      <slot/>
    </div>
  </button>
</template>

<style lang="scss">
@keyframes spin {
  0% {
    transform: rotate(0deg);
    }
  100% {
    transform: rotate(360deg);
    }
  }
.loading {
  animation: spin 1s infinite linear;
}

.vue-button {
  //...

  &:hover {
    //...
  }

  &:active {
    //...
  }

  &:focus {
    //...
  }

  &.icon-right {
    > .icon {
      order: 2;
      margin-left: 0.3em;
      margin-right: 0;
      margin-top: 0.1em;
    }

    > .content {
      order: 1;
    }
  }

  /* ali iconfont common css */
  > .icon {
    width: 1em;
    height: 1em;
    margin-right: 0.3em;
    margin-left: 0;
    margin-top: 0.1em;
    fill: currentColor;
    overflow: hidden;
    order: 1;
  }

  > .content {
    order: 2;
  }

</style>

由外部传入是否loading的条件

  • App.vue绑定属性:isLoading="true"
  • App.vue添加数据
    • data() { return { isLoading: false }
    • 更改绑定属性为变量:isLoading="isLoading"
  • 绑定点击事件
    • <VueButton :isLoading="isLoading" @click="isLoading = !isLoading" icon="settings">按钮</VueButton>
    • 点击取反 @click="isLoading = !isLoading"
  • 自定义组件中触发的自定义点击事件
    • 自定义组件中,Vue并不识别点击哪一个部分算是点击(在原生的标签中,Vue可以识别默认的点击事件)
    • 需要在当前对象(自定义组件)主动触发click事件:<button class="vue-button" :class="{[icon-${iconPosition}]: true}" @click="$emit('click')" />
    • 在模板中写$emit,不用加this.,直接写@click="$emit('click')"
    • 或者用.native修饰符
  • 代码如下

App.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
  <div id="app">
    <VueButton>
      按钮
    </VueButton>
    <VueButton :isLoading="isLoading1" @click="isLoading1 = !isLoading1" icon="settings">
      按钮
    </VueButton>
    <VueButton :is-loading="isLoading2" @click="isLoading2 = !isLoading2" icon="settings" icon-position="right">
      按钮
    </VueButton>
    <VueButton :is-loading="isLoading3" @click="isLoading3 = !isLoading3" icon="download" icon-position="right">
      下载
    </VueButton>
  </div>
</template>

<script>
import VueButton from './components/vuebutton/VueButton.vue'

export default {
  name: 'App',
  data() {
    return {
      isLoading1: false,
      isLoading2: false,
      isLoading3: false,
    }
  },
  components: {
    VueButton,
  }
}
</script>

VueButton.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<template>
  <div class="vue-demo-button">
    <button class="vue-button" :class="{[`icon-${iconPosition}`]: true}" @click="$emit('click')">
      <VueIcon v-if="!!icon && !isLoading" :name="icon" class="icon" @click="kClick"/>
      <VueIcon v-if="isLoading" name="loading" class="loading icon"/>
      <div class="content">
        <slot/>
      </div>
    </button>
  </div>
</template>

<script>
import VueIcon from '../icon/Icon.vue'
export default {
  props: {
    icon: {
      type: String, // ['settings'. 'loading'. 'right'. 'left'. 'download'. 'arrow-down'. 'thumbs-up']
    },
    isLoading: {
      type: Boolean,
      default: false,
    },
    iconPosition: {
      type: String,
      default: 'left',
      validator(userValue) {
        return (userValue === 'left' || userValue === 'right')
      },
    },
  },
  methods: {
    kClick() {
      this.$emit('click')
    },
  },
  components: {
    VueIcon
  },
}
</script>

合并按钮组ButtonGroup.vue

ButtonGroup.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
  <div class="vue-button-group">
    <slot></slot>
  </div>
</template>

<style lang="scss" scoped>
.vue-button-group {
  display: inline-flex;
  vertical-align: middle;
  //...
}
</style>

  • 不能直接使用<slot></slot>作为组件的根节点,需要包裹一层div
  • <slot></slot>渲染时会消失,可能含有多个节点

App.vue中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
  <div id="app">
    <VueButton>
      按钮
    </VueButton>
    <VueButton :isLoading="isLoading1"
               @click="isLoading1 = !isLoading1"
               icon="settings">设置
    </VueButton>
    <VueButton :is-loading="isLoading2"
               @click="isLoading2 = !isLoading2"
               icon="settings" icon-position="right">设置
    </VueButton>
    <VueButton :is-loading="isLoading3"
               @click="isLoading3 = !isLoading3"
               icon="download" icon-position="right">下载
    </VueButton>
    <vue-button-group>
      <VueButton icon="left">上一页</VueButton>
      <VueButton icon="">更多</VueButton>
      <VueButton icon="right"
                 icon-position="right">下一页
      </VueButton>
    </vue-button-group>
  </div>
</template>

<script>
import Vue from 'vue'
import VueButton from './components/vuebutton/VueButton.vue'
import VueButtonGroup from './components/ButtonGroup/ButtonGroup.vue'

// 全局注册组件
Vue.component('v-button', VueButton)
Vue.component('v-button-group', VueButtonGroup)

export default {
  name: 'App',
  data() {
    return {
      isLoading1: false,
      isLoading2: true,
      isLoading3: false,
    }
  },
  components: {
    VueButton,
    VueButtonGroup
  }
}

</script>


border的一个 bug,所有子元素左移了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.vue-button-group {
  display: inline-flex;
  vertical-align: middle;

  > .vue-button {
    margin-right: 0;
    border-radius: 0;

    &:first-child {
      border-top-left-radius: var(--border-radius);
      border-bottom-left-radius: var(--border-radius);
    }

    &:last-child {
      border-top-right-radius: var(--border-radius);
      border-bottom-right-radius: var(--border-radius);
    }

    /*
    * 错误地显示 border 三边显示 一边被遮盖
    &:not(:first-child) {
      border-left: none;
      }
    */
    &:not(:first-child) {
      margin-left: -2px;
    }

    // 解决border被后面的遮挡掉一边
    &:hover {
      position: relative;
      z-index: 1;
    }
  }
}
  • 非第一个元素使用 负margin 合并一边的 border:margin-left: -2px;
  • 解决 border 被后面的遮挡掉一边:hover时,提升z-index

防止开发者错误使用UI,挂载时处理检查逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<script>
export default {
  mounted() {
    // console.log(this.$children)
    // console.log(this.$el)
    // console.log(this.$el.children)
    for (const node of this.$el.children) {
      // console.log(node)
      const name = node.nodeName.toLowerCase()
      if (name !== 'button') {
        console.warn(`vue-button-group 的子元素应该全是 VueButton,但你写的是${name}`)
      }
    }
  },
}
</script>
  • mounted时,检查一下子元素是否为button元素
  • console.log(this.$children)中能识别Vue对象
  • 识别非button标签,并在后台打印警告

其他

增加 hover 样式,模拟outline radius(-moz-outline-radius)

1
2
3
4
5
input[type=text]:focus {
    box-shadow: 0 0 0 1pt red;
    outline-width: 1px;
    outline-color: red;
}

CSS变量和SCSS变量互不兼容

  • darken(var(--button-hover), 5%);

webStorm右键查看本地历史local history功能

  • 无法替代的智能IDE


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名