1.3 优化组件建构

[toc]


为了方便展示,使用 tab 切换不同组件组合,同时也为了之后添加新组建

  • VueButton 重构组件 Buttons Inputs 优化组件建构
  • 使用 tab 展示不同组件组合
  • 使用动态缓存 + 动态组件标签
    • <component></component>
    • <keep-alive></keep-alive>

重构

在线将文件生成树形结构纯文本工具 Dir Tree Noter

 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
src
├─ App.vue
├─ assets
│    ├─ icons
│    │    └─ svg.js
│    └─ logo.png
├─ components
│    ├─ Buttons.vue
│    ├─ HelloWorld.vue
│    ├─ Inputs.vue
│    ├─ Nav.vue
│    ├─ button
│    │    └─ VueButton.vue
│    ├─ button-group
│    │    └─ ButtonGroup.vue
│    ├─ icon
│    │    └─ VueIcon.vue
│    └─ input
│           └─ VueInput.vue
├─ main.ts
├─ router
│    └─ index.ts
├─ shims-tsx.d.ts
├─ shims-vue.d.ts
├─ store
│    └─ index.ts
├─ style
│    └─ normalize.scss
├─ views
│       └─ Layout.vue
├─ .eslintrc.js
├─ .github
│    └─ workflows
│           └─ unit-test-actions.yml
├─ .gitignore
├─ LICENSE
├─ README.md
├─ index.ts
├─ karma.conf.js
├─ package-lock.json
├─ package.json
├─ public
│    ├─ favicon.ico
│    └─ index.html
├─ test
│    └─ button.test.ts
├─ tsconfig.json
├─ vue.config.js
└─ yarn.lock


入口文件

index.ts

1
2
3
4
5
6
import VueButton from './src/components/button/VueButton.vue'
import VueButtonGroup from './src/components/button-group/ButtonGroup.vue'
import VueIcon from './src/components/icon/VueIcon.vue'

export {VueButton, VueButtonGroup, VueIcon}

main.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

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
<template>
  <main id="app">
    <router-view></router-view>
  </main>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import VueButton from './components/button/VueButton.vue';
import VueButtonGroup from './components/button-group/ButtonGroup.vue';
import VueIcon from './components/icon/VueIcon.vue';
import VueInput from './components/input/VueInput.vue';

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

@Component
export default class App extends Vue {
  name = 'App';
}

</script>

<style lang="scss" scoped>
::v-deep {
  --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;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /*text-align: center;*/
  color: #2c3e50;
  margin-top: 60px;
}

</style>


结构组件

Layout.vue 动态组件<component :is="xxx"></component>

 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
<template>
  <section class="layout-wrapper"
           :class="classPrefix && `${classPrefix}-wrapper`">
    <Nav @update:tabName="changeTabComponent"/>
    <section class="content"
             :class="classPrefix && `${classPrefix}-content`">
      <keep-alive>
        <component :is="currentTabComponent"
                   class="tab">
        </component>
      </keep-alive>
    </section>
  </section>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import Buttons from '../components/Buttons.vue';
import Inputs from '../components/Inputs.vue';
import Nav from '../components/Nav.vue';

@Component({
  components: {
    Buttons,
    Inputs,
    Nav
  }
})
export default class Layout extends Vue {
  name = 'Layout';
  currentTabText = 'buttons';

  // 由动态外部参数 获取 类样式
  @Prop(String) ['classPrefix']: string;

  changeTabComponent(tabName: string) {
    this.currentTabText = tabName;
  }

  get currentTabComponent() {
    return this.currentTabText;
  }

}
</script>

<style lang="scss" scoped>
.layout-wrapper {
  display: flex;
  overflow-scrolling: touch;
  flex-direction: column;
  height: 100vh;

  .tab {
    border: 1px solid #ccc;
    padding: 20px;
  }

  .content {
    flex-grow: 1;
    overflow-y: auto;
    overflow-x: hidden;
  }
}
</style>

  • 使用动态缓存 + 动态组件标签
    • <component></component>
    • <keep-alive></keep-alive>

Nav.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
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component
export default class Nav extends Vue {
  name = 'Nav';
  currentTabText = 'JsonP';
  tabs = [
    'Buttons',
    'Inputs',
    'GridSystems',
    'LayoutGroups',
    'ToastsTips',
    'Tabs',
    'Popovers',
    'Collapses',
    'Switches',
    'BooksInfo',
    'GetGithubAvatar',
    'JsonP'
  ];

  showTab(tab: string) {
    // 切换对应样式
    this.currentTabText = tab;
    // 发布 切换对应组件 的自定义事件
    this.$emit('update:tabName', tab);
  }

}
</script>

<template>
  <nav>
    <button v-for="tab in tabs"
            :key="tab"
            :class="['tab-button', { active: currentTabText === tab }]"
            @click="showTab(tab)">
      <span>
        {{ tab }}
      </span>
    </button>
  </nav>
</template>

<style lang="scss" scoped>
nav {
  display: flex;
  flex-direction: row;
  align-items: center;
  font-size: 12px;

  .tab-button {
    padding: 6px 10px;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    border: 1px solid #ccc;
    cursor: pointer;
    background: #f0f0f0;
    margin-bottom: -1px;
    margin-right: -1px;

    &:hover {
      background: #e0e0e0;
    }

    &.active {
      background: #e0e0e0;
    }
  }
}

</style>


UI组件

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<script lang="ts">
import VueIcon from '@/components/icon/VueIcon.vue';
import {Component, Emit, Prop, Vue, Watch} from 'vue-property-decorator';

@Component({
  components: {
    VueIcon,
  }
})
export default class VueButton extends Vue {
  name = 'VueButton';
  isDisabledFake = false;

  @Prop({type: Boolean, default: false}) isDisabled!: boolean;
  @Prop({type: Boolean, default: false}) isLoading!: boolean;
  @Prop({
    type: String,
    default: 'normal',
    validator(colorType) {
      return ['normal', 'primary', 'warning', 'danger', 'info', 'success', 'attention']
        .indexOf(colorType) > -1;
    }
  }) colorType!: string;
  @Prop({type: String, default: 'button'}) theme!: 'button' | 'link' | 'text';
  @Prop({type: String, default: 'normal'}) size!: 'small' | 'normal' | 'big';
  @Prop({type: String, default: ''}) icon!: 'settings' | 'loading' | 'right' |
    'left' | 'download' | 'arrow-down' | 'thumbs-up' | '';
  @Prop({
    type: String,
    default: 'left',
    validator(userValue) {
      return userValue === 'left' || userValue === 'right';
    }
  }) iconPosition!: string;

  get loadingStatus() {
    // return this.isLoading ? this.isLoading : (!!this.icon && !this.isLoading);
    return this.isLoading || (!!this.icon && !this.isLoading);
  }

  get loadingName() {
    return this.isLoading ? 'loading' : this.icon;
  }

  get classes() {
    return {
      'vue-button': 'vue-button',
      'is-disabled': this.isDisabled,
      'is-disabled-fake': this.isDisabledFake,
      'activeHover': !this.isDisabled,
      [`icon-${this.iconPosition}`]: true,
      [`vue-button-${this.colorType}`]: true,
      [`vue-button-theme-${this.theme}`]: true,
      [`vue-button-size-${this.size}`]: true,
    };
  }

  @Watch('isLoading')
  onIsLoadingChange(val: boolean) {
    this.isDisabledFake = val;
  }

  @Emit('click')
  clickLoading(/*e: MouseEvent*/) {
    return /*e.target.value*/;
  }
}
</script>

<template>
  <div class="vue-button-wrapper">
    <button :class="classes"
            :colorType="colorType"
            :disabled="isDisabled"
            :theme="theme"
            @click="clickLoading"
            type="button">
      <VueIcon v-if="loadingStatus"
               class="vue-svg"
               :icon-name="loadingName"
               :scale="1"
               :class="{loading: isLoading && !isDisabled,
                       [`vue-button-size-${this.size}`]: true}"/>
      <div class="content">
        <slot></slot>
      </div>
    </button>
  </div>
</template>

VueButtonGroup.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
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component
export default class VueIcon extends Vue {
  name = 'VueButtonGroup';

  checkSon() {
    for (const node of Array.from(this.$el.children)) {
      const name = node.children[0].nodeName.toLowerCase();
      if (name !== 'button') {
        throw new Error(`vue-button-group 的子元素应该全是 VueButton, 但你写的是${name}`);
      }
    }

  }

  mounted() {
    this.checkSon();
  }
}
</script>

<template>
  <div class="vue-button-group">
    <slot></slot>
  </div>
</template>

VueIcon.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
<script lang="ts">
import '@/assets/icons/svg.js';
import {Component, Prop, Vue} from 'vue-property-decorator';

@Component
export default class VueIcon extends Vue {
  name = 'VueIcon';
  // 动态引入 svg name
  @Prop({
    default: '',
    type: String
  }) iconName!: string;
  @Prop({
    type: Number,
    default: 1,
    validator(value: number): boolean {
      return Number.isInteger(value) || value >= 1 && value <= 10;
    }
  }) scale!: number;

  get iconScale() {
    return {
      [`vue-icon-${this.scale}x`]: true
    };
  }

}
</script>

<template>
  <span class="vue-icon-wrapper" :class="iconScale">
    <svg class="vue-icon-svg">
      <use :xlink:href="`#i-${iconName}`"></use>
    </svg>
  </span>
</template>


VueDetail.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
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

@Component
export default class VueDetail extends Vue {
  name = 'VueDetail';
  @Prop({type: String}) summaryString!: string;
  @Prop({
    type: Boolean,
    default: true
  }) isOpen!: boolean;

}
</script>

<template>
  <details class="vue-details"
           :open="isOpen">
    <summary class="vue-summary">
      <span class="vue-summary-string">
        {{ summaryString }}
      </span>
    </summary>
    <div class="vue-details-content">
      <slot></slot>
    </div>
  </details>
</template>


VueDetail.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
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import VueDetail from './VueDetail.vue';

@Component({
  components: {
    VueDetail
  }
})

export default class VueCodePresentation extends Vue {
  name = 'VueCodePresentation';
  @Prop() legendName!: string;
  @Prop({
    type: Array,
    default: () => ([])
  }) detailsDataList!: detailsDataList;

}
</script>

<template>
  <div>
    <form>
      <fieldset class="vue-fieldset">
        <legend>{{ legendName }}</legend>
        <slot>
          <VueDetail v-for="item in detailsDataList"
                     :summaryString="item.summaryString"
                     :open="item.isOpen"
                     :key="item.id">
          </VueDetail>
        </slot>
      </fieldset>
    </form>
  </div>
</template>

Buttons.vue 展示 Demo

  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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import VueButton from './button/VueButton.vue';
import VueButtonGroup from './button/VueButtonGroup.vue';
import VueCodePresentation from './code-presentation/VueCodePresentation.vue';
import VueDetail from './code-presentation/VueDetail.vue';
import VueIcon from './icon/VueIcon.vue';

@Component({
  components: {
    VueButton,
    VueButtonGroup,
    VueIcon,
    VueDetail,
    VueCodePresentation
  }
})
export default class Buttons extends Vue {
  name = 'Buttons';
  isLoading1 = false;
  isLoading2 = true;
  isLoading3 = false;
  colorTypeStringList = [
    'primary',
    'danger',
    'info',
    'success',
    'warning',
    'attention'
  ];
}
</script>

<template>
  <div>
    <VueCodePresentation legendName="Button Types">
      <VueDetail summaryString="Normal Button 基本样式">
        <VueButton>按钮</VueButton>
        <VueButton theme="link">链接按钮</VueButton>
        <VueButton theme="text">文字按钮</VueButton>
        <VueButton :isDisabled="true">禁用按钮</VueButton>
      </VueDetail>

      <VueDetail summaryString="Buttons with icons">
        <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>
      </VueDetail>
      <VueDetail summaryString="Buttons with Color"
                 class="rainbow">
        <VueButton v-for="type of colorTypeStringList"
                   :colorType="type"
                   :key="type">
          {{ type }}
        </VueButton>
      </VueDetail>

      <VueDetail summaryString="Size">
        <VueButton size="small" icon="download">
          小尺寸按钮
        </VueButton>
        <VueButton icon="settings">
          正常尺寸按钮
        </VueButton>
        <VueButton size="big" icon="thumbs-up">
          大尺寸按钮
        </VueButton>
      </VueDetail>

      <VueDetail summaryString="Button Group">
        <vue-button-group>
          <VueButton icon="left">上一页</VueButton>
          <VueButton icon="settings">更多</VueButton>
          <VueButton icon="right"
                     icon-position="right">下一页
          </VueButton>
        </vue-button-group>
      </VueDetail>

    </VueCodePresentation>
  </div>
</template>

<style lang="scss" scoped>
::v-deep:not(.vue-button-group) > button {
  margin: 5px 5px;
}

.rainbow::v-deep {
  > .vue-summary {
    > .vue-summary-string {
      background-image: linear-gradient(to left, violet, indigo, blue, green, yellow, orange, red);
      -webkit-background-clip: text;
      color: transparent;

      &::selection {
        color: #000;
        background-color: #fff;
      }
    }

  }

}

</style>