3.1 简单轮子 VueGrid:网格系统

大纲链接 §

[toc]


大纲

  • 什么是 Grid System 网格系统
  • 组件UI
  • 组件代码
  • 单元测试

什么是 网格系统/栅格系统 Grid System

一些名词概念

  • 栅格grid
  • 布局:layout
  • 空隙gutter
  • 偏移:offset
  • 跨度:span 分别有
    • 12 x 2
    • 8 x 3
    • 6 x 4
    • 4 x 6
    • 3 x 8
    • 2 x 12

组件UI结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<v-row gutter="10">
  <v-col span="12"></v-col>
  <v-col span="12"></v-col>
</v-row>

<v-row gutter="10">
  <v-col span="8"></v-col>
  <v-col span="8"></v-col>
  <v-col span="8"></v-col>
</v-row>

<v-row gutter="10">
  <v-col span="6"></v-col>
  <v-col span="6"></v-col>
  <v-col span="6"></v-col>
  <v-col span="6"></v-col>
</v-row>

<v-row gutter="10">
  <v-col span="2"></v-col>
  <v-col span="22"></v-col>
</v-row>
  • flex布局
    • 横向布局
    • 纵向布局
  • 有无空隙 gutter
  • 自适应 or 响应式
    • 只要是flex布局都可以是自适应的
    • 响应式专指利用媒体查询@media做各种客户端的适配

API设计

父组件 <vue-row></vue-row>

  • 参数:
    • align
    • gutter
    • colData

子组件 <vue-col></vue-col>

  • 参数:
    • span
    • offset
    • mobile
    • pad
    • laptop
    • pc
    • pcw
    • pcx

组件UI设计

设计图

GridSystems

语雀设计稿

  • 链接
  • 采用 24 栅格系统

参考 gulu UI

  • 仓库代码
  • 请注意看 row.vuecol.vue,已经对应的 *.test.js 文件
  • 测试代码
    • 异步代码中 done 的使用

组件代码

实现基本样式

列组件 row.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
<template>
  <div class="row">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component
export default class VueRow extends Vue {
  name = 'VueRow';
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  justify-content: center;

  &:not(last-child) {
    margin-bottom: 16px;
  }

}
</style>

  • 独占一整行
  • 设置插槽<slot></slot>,存放 一个或多个 行组件

行组件col.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
<template>
  <div class="col">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  line-height: 45px;
  max-width: 100%;
  display: inline-flex;
  flex-grow: 1;
  justify-content: center;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

}
</style>

  • 设置插槽<slot></slot>,存放 其他元素
  • 行内弹性盒

展示组件GridSystems.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
<template>
  <div>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>Average Span</summary>
          <VueRow>
            <VueCol>100%</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>
  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import VueCol from './grid-system/VueCol.vue';
import VueRow from './grid-system/VueRow.vue';

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>


实现等分栅栏

GridSystems.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
<template>
  <div>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>Average Span</summary>
          <VueRow>
            <VueCol>100%</VueCol>
          </VueRow>

          <VueRow>
            <VueCol>50%</VueCol>
            <VueCol>50%</VueCol>
          </VueRow>

          <VueRow>
            <VueCol>33.3%</VueCol>
            <VueCol>33.3%</VueCol>
            <VueCol>33.3%</VueCol>
          </VueRow>

          <VueRow>
            <VueCol>25%</VueCol>
            <VueCol>25%</VueCol>
            <VueCol>25%</VueCol>
            <VueCol>25%</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>

  </div>
</template>

实现不等分栅栏

GridSystems.vue

  • 一种做法是尝试使用自定义属性data-span定义跨度值,通过属性选择器定义样式
 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
<template>
    <VueRow>
      <VueCol :data-span="2">1/12</VueCol>
      <VueCol :data-span="22">11/12</VueCol>
    </VueRow>
</template>

<style lang="scss" scoped>
  & > [data-span="2"] {
    width: 8.333333%;
  }
  //... /* 需要定义 .col.col-1 ~ .col.col-24 */ 
  & > [data-span="22"] {
    width: 91.666667%;
  }
</style>

<script lang="ts">
//import ...
@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>

  • 需要定义 .col.col-1 ~ .col.col-24 每一个样式
    • 太麻烦
    • 重复

推荐使用 外部数据 + scss 循环语法 代替自定义属性 实现不等分栅栏

./src/components/GridSystems.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>
  <div>
    <VueRow>
      <VueCol span="2">1/12</VueCol>
      <VueCol span="22">11/12</VueCol>
    </VueRow>
  </div>
</template>

<script lang="ts">
//import ...

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>

  • 设置外部属性span

./src/components/grid-system/VueCol.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
<template>
  <div class="col" :class="[`col-${ span }`]">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: [String, Number],
    default: '12'
  }) span!: string;

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  max-width: 100%;
  flex-grow: 1;
  display: inline-flex;
  justify-content: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  // 循环语法 @for
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

}
</style>

  • 外部数据的类型可以给多个类型 @Prop({type: [String, Number],}) span!: string
  • 绑定样式 :class="[`col-${ span }`]"
  • scss
    • 循环语法 @for
    • 插值语法 #{xxx}
    • 定义变量 $xxx: yyy

为什么定义了width: 50%;,在父容器display: flex;中的三个元素仍可以平均分配整个宽度

  • 定义了display: flex;的元素 默认属性flex-wrap: nowrap;,会使子元素 默认不换行
  • 而且不换行的子元素的flex-grow 默认为1,自动平均撑满父元素
  • 设置flex-wrap: wrap;,或者设置flex-shrink: 0; 不收缩,超出宽度的子元素会被挤到下一行

实现栅栏偏移

精确地定义偏移量offset

  • GridSystems.vue传值offset
  • VueCol.vue设置外部数据offset

GridSystems.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>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>Offset</summary>
          <VueRow>
            <VueCol span="2">1/12</VueCol>
            <VueCol span="22" offset="2">11/12</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>
  </div>
</template>

<script lang="ts">
//import ...

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>

VueCol.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
<template>
  <div class="col"
       :class="[`col-${span}`,
       offset && `offset-${offset}`
       ]">
    <slot></slot>
  </div>
</template>

<script lang="ts">
//import ...

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

  @Prop({
    type: [String, Number],
    default: '12'
  }) span!: string;

  @Prop({
    type: [String, Number],
  }) offset!: string;

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  max-width: 100%;
  flex-grow: 1;
  display: inline-flex;
  justify-content: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

}
</style>


实现固定空隙 gutter

  • 设定空隙为10px
  • 所有元素box-sizing: border-box;
  • 子元素使用 margin: 0 10px;
  • 父元素使用 负margin: 0 -20px; 消除左右内部两边多余的margin
  • 左右和整个父元素宽度(页面宽度)对齐

GridSystems.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
<template>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>gutter</summary>
          <VueRow>
            <VueCol span="4">1/12</VueCol>
            <VueCol span="20">5/6</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>
  </div>
</template>

<script lang="ts">
//import ...

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>

<style lang="scss" scoped>
::v-deep.row {
  background-color: #ccc;
}
</style>

VueRow.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
<template>
  <div class="row">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';

@Component
export default class VueRow extends Vue {
  name = 'VueRow';
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  justify-content: center;
  align-items: center;
  
  margin: 0 -20px;
  outline: 1px solid green;

  &:not(last-child) {
    margin-bottom: 16px;
  }

}
</style>

VueCol.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
<template>
  <div class="col"
       :class="[
         `col-${span}`,
        offset && `offset-${offset}`
       ]">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: [String, Number],
    default: '12'
  }) span!: string;

  @Prop({
    type: [String, Number],
  }) offset!: string;

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  max-width: 100%;
  flex-grow: 1;
  display: inline-flex;
  justify-content: center;
  line-height: 45px;
  color: #fff;

  outline: 1px solid red;
  margin: 0 10px;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .offset-2 ~ .offset-24
  $class-prefix: offset-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      margin-left: ($n / 24) * 100%;
    }
  }
}
</style>


实现 可设置 的固定空隙 gutter

margin值改为可设置的属性,VueRow.vue传外部数据gutter

  • 绑定style属性::style="{marginLeft: -gutter + 'px', marginRight: -gutter + 'px'}"
 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
<template>
  <div class="row"
       :style="{ 
         marginLeft: -gutter + 'px',
         marginRight: -gutter + 'px',
       }">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: [String, Number],
    default: '20'
  }) gutter!: string;

}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  justify-content: center;
  align-items: center;

  &:not(last-child) {
    margin: 16px 0;
  }

}
</style>

VueCol.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
<template>
  <div class="col"
       :class="[
         span && `col-${span}`,
         offset && `offset-${offset}`,
       ]"
       :style="{
         marginLeft: gutter/2 + 'px',
         marginRight: gutter/2 + 'px',
       }">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: [String, Number],
    default: '12'
  }) span!: string;

  @Prop({
    type: [String, Number],
  }) offset!: string;

  @Prop({
    type: [String, Number],
    default: '20'
  }) gutter!: string;

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  max-width: 100%;
  flex-grow: 1;
  display: inline-flex;
  justify-content: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .offset-2 ~ .offset-24
  $class-prefix: offset-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      margin-left: ($n / 24) * 100%;
    }
  }
}
</style>

GridSystems.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<template>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>gutter</summary>
          <VueRow gutter="20">
            <VueCol span="4" gutter="20">1/12</VueCol>
            <VueCol span="20" gutter="20">5/6</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>
</template>

用Vue钩子实现 添加空隙

重复写属性gutter,能否只写一次,父组件拿到数据传给子组件

  • created时拿到子组件``
  • gutter属性传入

VueRow.vue中控制台打印出子组件

  • created() {console.log(this.$children);}
  • 控制台得到空
  • 点开数组,有属性
  • chrome bug const a = []; console.log(a); a.push(1);点开数组,有属性
  • created时,还没有子组件

createdmounted的区别

  • mounted() {console.log(this.$children);} 控制台得到包含子组件的数组
  • 类比
    • created好比const div = document.createElement('div')在内存中创建对象
    • mounted好比document.body.appendChild(div)将对象挂载到页面中去
  • Vue.js处理 父子组件挂载顺序,可在控制台打印:
    • VueRow.vue中写:
      • created() {console.log(row created);}
      • mounted() {console.log(row mounted);}
    • VueCol.vue中写:
      • created() {console.log(col created);}
      • mounted() {console.log(col mounted);}
  • 先创建父组件,再创建子组件,将子组件挂载到父组件上,将父组件挂载到根组件上
  • 当父组件已经挂载到页面上,即mounted()时,说明父组件可取到所有子组件
  • 为每个获取到的子组件添加属性
  • 必须判断子组件类型是否为VueRow

VueRow.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
<template>
  <div class="row"
       :style="{
         marginLeft: -gutter + 'px',
         marginRight: -gutter + 'px',
       }">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: [String, Number],
    default: '0'
  }) gutter!: string;

  mounted() {
    this.$children.forEach((vm) => {
      (vm as any).gutter = this.gutter;
    });
  }
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  justify-content: center;
  align-items: center;
  &:not(last-child) {
    margin: 16px 0;
  }

}
</style>

  • VueRow上接受一个外部数据gutter
  • VueRowgutter传入每一个子组件VueCol
  • 注意类型声明(vm as any).gutter = this.gutter;
  • 代替写法
    • const source = {'gutter': this.gutter};
    • Object.assign(vm, source);
  • 在子组件里需要声明data数据 gutter
  • 或者使用this.$set(vm, 'gutter', gutter); / Vue.set(vm,'gutter', gutter);
  • 或者使用Provide Inject的装饰器写法 @Provide('gutterToSon') gutterToSon = this.gutter;来传递 gutter 属性
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  // Provide 传递 gutter 属性
  // @Provide('gutterToSon') gutterToSon = this.gutter;
  // 传递 gutter 属性
  gutterToCol() {
    const {$children, gutter} = this;
    // console.log('$children in VueRow', $children);
    if ($children && gutter) {
      $children.forEach((vm: Vue) => {
        const state = Vue.observable({gutter});
        Object.assign(vm, state);

        /*
        // const source = {'gutter': gutter};
        const source = {gutter};
        Object.assign(vm, source);
        */
        // (vm as any).gutter = gutter
        // Vue.set(vm,'gutter', gutter);
        // this.$set(vm, 'gutter', gutter);
      });
    }
  }
  • 在组件挂载时,调用this.gutterToCol()向子组件传递 gutter 属性

重构VueRowVueCol组件

  • 将组件标签内的js代码:class="{...}"提取到计算属性中computed: {getClass() {...}}
  • 不提取到data中时因为data只会在一开始读取引用的数据
  • 当引用的数据变了,不会相应改变
  • 当一个属性时依赖(引用)另一个属性时,必须使用computed

VueCol.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
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
  }) offset!: number;

  get colClass() {
    const {span, offset} = this;
    return [
      span && `col-${span}`,
      offset && `offset-${offset}`
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  max-width: 100%;
  flex-grow: 1;
  display: inline-flex;
  justify-content: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: ($i / 24) * 100%;
    }
  }
}
</style>

  • const {span, offset} = this;解构赋值变量
  • style内联样式优先级高于class
  • span默认不为0@for $n from 1 through 24n1 开始
  • offset默认为0,所以 @for $i from 0 through 24i0 开始

VueRow.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
<template>
  <div class="row"
       :style="rowStyle">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: Number,
    default: 0
  }) gutter!: number;

  get rowStyle() {
    return {
      marginLeft: (-this.gutter / 2) + 'px',
      marginRight: (-this.gutter / 2) + 'px'
    };
  }

  mounted() {
    this.$children.forEach((vm) => {
      const source = {'gutter': this.gutter};
      Object.assign(vm, source);
    });
  }
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  justify-content: center;
  align-items: center;

  &:not(last-child) {
    margin: 16px 0;
  }

}
</style>

需要重构的代码

  • 重复两次及以上:重复代码就是潜在的 bug,存在遗漏更新的风险
  • 一眼看不懂

如何重构

  • 提取变量
  • 模块化

添加对齐align属性

VueRow.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
<template>
  <div class="row"
       :style="rowStyle"
       :class="rowClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: Number,
    default: 0
  }) gutter!: number;

  @Prop({
    type: String,
    default: 'center',
    validator(value: string): boolean {
      return ['', 'left', 'right', 'center'].includes(value);
    }
  }) align!: string;

  get rowStyle() {
    return {
      marginLeft: (-this.gutter / 2) + 'px',
      marginRight: (-this.gutter / 2) + 'px'
    };
  }

  get rowClass() {
    const {align} = this;
    return [
      align && `align-${align}`
    ];
  }

  mounted() {
    this.$children.forEach((vm) => {
      const source = {'gutter': this.gutter};
      Object.assign(vm, source);
    });
  }
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  flex: auto;

  // 对齐字符映射
  $align-types: (
    'left': flex-start,
    'right': flex-end,
    'center': center,
  );
  @each $name, $type in $align-types {
    &.align-#{$name} {
      justify-content: $type;
    }
  }

}
</style>

  • 无对齐'', 左对齐'left', 右对齐'right', 居中对齐'center'
  • 对齐字符映射$align-types: ('left': flex-start, ...);
  • scss循环语法@each $name, $type in $align-types {...}

GridSystems.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
<template>
  <div>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>align</summary>
          <VueRow>
            <VueCol :span="9">
              <VueRow align="left">
                <VueCol>1</VueCol>
                <VueCol>2</VueCol>
                <VueCol>3</VueCol>
                <VueCol>4</VueCol>
              </VueRow>
            </VueCol>

            <VueCol :span="15">
              <VueRow align="right">
                <VueCol>1</VueCol>
                <VueCol>2</VueCol>
                <VueCol>3</VueCol>
                <VueCol>4</VueCol>
                <VueCol>5</VueCol>
                <VueCol>6</VueCol>
                <VueCol>7</VueCol>
              </VueRow>
            </VueCol>
          </VueRow>

          <VueRow>
            <VueCol :span="4">
              <VueRow align="center">
                <VueCol>LOGO</VueCol>
              </VueRow>
            </VueCol>
            <VueCol :span="14">Main</VueCol>
            <VueCol :span="6">Aside</VueCol>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>
  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import VueCol from './grid-system/VueCol.vue';
import VueRow from './grid-system/VueRow.vue';

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>

<style lang="scss" scoped>
// BFC
details {
  overflow: hidden;

  > div:not(last-child) {
    padding: 16px 0;
  }
}
</style>


实现响应式@media

根据屏幕不同的宽度预设子项不同比列,页面变化时,比列改变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>
    <!--> bad </-->
    <g-row>
      <g-col span=4 phone-span=12 offset=2 phone-offset=...></g-col>
      <g-col span=20 phone-span=12 offset=2 phone-offset=...></g-col>
    </g-row>
    
    <!--> good </-->
    <g-row>
      <g-col span=4 :phone="{span: 12, offset: 2}"></g-col>
      <g-col span=20 :phone="{span: 12, offset: 2}"></g-col>
    </g-row>
</template>
  • 通过传的外部数据,将不同类型响应式的属性体现在html标签上
  • 外部数据类型为对象,将多个属性值写入,覆盖默认值

VueCol.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 {Component, Prop, Vue} from 'vue-property-decorator';

type media = {
  'span': number;
  'offset': number;
};

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
  }) offset!: number;

  @Prop({
    type: Object,
    validator(value: media): boolean {
      const keys = Object.keys(value);
      let valid = true;
      keys.forEach((key) => {
        if (!['span', 'offset'].includes(key)) {
          valid = false;
        }
      });
      return valid;
    }
  }) mobile!: media;

</script>
  • 验证属性'span', 'offset'是否存在于moblie
  • 改用数组实现
  • 一个数组必须包含在里一个数组里
    • [1, 2][1, 2, 3]
  • 取得对象的属性列表 keysObject.keys(xxx)
    • 判断子集['span', 'offset']
    • ['span', 'offset'].includes(key)
  • 抽出src/libs/objKeyValidator.ts函数

实现验证属性的方法@/utils/objKeyValidator.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type mediaQuery = {
  'span': number;
  'offset': number;
};

const objKeyValidator = (attributeObj: mediaQuery, attributeData: string[]): boolean => {
  let valid = true;
  const keys = Object.keys(attributeObj);
  keys.forEach((key) => {
    if (!attributeData.includes(key)) {
      valid = false;
    }
  });
  return valid;
};

export default objKeyValidator;

VueCol.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
<script lang="ts">
import objKeyValidator from '@/utils/objKeyValidator';
import _ from 'lodash';
import {Component, Emit, Prop, Vue} from 'vue-property-decorator';

// const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);
const validator = (value: mediaQuery) => { return objKeyValidator(value, ['span', 'offset']); };

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

  @Prop({
    type: Object,
    validator
  }) mobile!: mediaQuery;
  @Prop({
    type: Object,
    validator
  }) pad!: mediaQuery;
  @Prop({
    type: Object,
    validator
  }) laptop!: mediaQuery;
  @Prop({
    type: Object,
    validator
  }) pc!: mediaQuery;
  @Prop({
    type: Object,
    validator
  }) pcw!: mediaQuery;
  @Prop({
    type: Object,
    validator
  }) pcx!: mediaQuery;

  // ...
}

</script>

CSS不能读取JS变量

  • 需要将JS的变化体现在组件的:class
  • :class的变化改变样式
  • 使用不同的CSS类切换
  • @media中设置对应不同的样式
  • 在不同的客户端中,写在后面的@media起效时覆盖写在前面的样式,优先级更高
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ...
// mobile
  @media (max-width: 576px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-mobile-;
    @for $n from 1 through 24 {
      &.#{$class-prefix}#{$n} {
        width: ($n / 24) * 100%;
      }
    }

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-mobile-;
    @for $i from 0 through 24 {
      &.#{$class-prefix}#{$i} {
        margin-left: ($i / 24) * 100%;
      }
    }
  }

VueCol.vue 实现moblie适配

  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
117
118
119
120
121
122
123
124
125
126
127
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
    default: 0
  }) offset!: number;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator(value: media): boolean {
      const keys = Object.keys(value);
      let valid = true;
      keys.forEach((key) => {
        if (!['span', 'offset'].includes(key)) {
          valid = false;
        }
      });
      return valid;
    }
  }) mobile!: media;

  get colClass() {
    const {span, offset, mobile} = this;
    let mobileClass: string[] = [''];
    if (mobile) {
      mobileClass = [
        `col-mobile-${mobile.span}`,
        `offset-mobile-${(mobile.offset)}`,
      ];
    }
    return [
      span && `col-${span}`,
      offset && `offset-${offset}`,
      ...(mobileClass)
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  flex: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: ($i / 24) * 100%;
    }
  }

  // mobile
  @media (max-width: 576px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-mobile-;
    @for $n from 1 through 24 {
      &.#{$class-prefix}#{$n} {
        width: ($n / 24) * 100%;
      }
    }

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-mobile-;
    @for $i from 0 through 24 {
      &.#{$class-prefix}#{$i} {
        margin-left: ($i / 24) * 100%;
      }
    }
  }
  
}

</style>

VueRow.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
<template>
  <div class="row"
       :style="rowStyle"
       :class="rowClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';

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

  @Prop({
    type: Number,
    default: 0
  }) gutter!: number;

  @Prop({
    type: String,
    default: 'center',
    validator(value: string): boolean {
      return ['', 'left', 'right', 'center'].includes(value);
    }
  }) align!: string;

  get rowStyle() {
    return {
      marginLeft: (-this.gutter / 2) + 'px',
      marginRight: (-this.gutter / 2) + 'px'
    };
  }

  get rowClass() {
    const {align} = this;
    return [
      align && `align-${align}`
    ];
  }

  mounted() {
    this.$children.forEach((vm) => {
      const source = {'gutter': this.gutter};
      Object.assign(vm, source);
    });
  }
}
</script>

<style lang="scss" scoped>
.row {
  display: flex;
  flex: auto;

  // Align
  $align-types: (
    'left': flex-start,
    'right': flex-end,
    'center': center,
  );
  @each $name, $type in $align-types {
    &.align-#{$name} {
      justify-content: $type;
    }
  }

  // mobile
  @media (max-width: 576px) {
    .row {
      flex-wrap: wrap;
    }
  }

}
</style>

  • 当处于mobile屏幕尺寸时,设置flex-wrap: wrap;可以换行

各屏幕尺寸参考

ant.design Col API

  • xs < 576px
  • 这样的命名不够直接表示屏幕的种类,所以改为
    • mobile - < 576px
    • pad - 577 ~ 768px
    • laptop - 769 ~ 992px
    • pc - 993 ~ 1200px
    • pcw PC wide - 1201 ~ 1600px
    • pcx PC extreamly wide - > 1601px

VueCol.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
 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<template>...</template>
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import objKeyValidator from '@/utils/objKeyValidator';

// const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);
const validator = (value: mediaQuery) => { return objKeyValidator(value, ['span', 'offset']); };

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
    default: 0
  }) offset!: number;

  @Prop({
    type: Object,
    validator
  }) mobile!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pad!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) laptop!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pc!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcw!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcx!: mediaQuery;

  get colClass() {
    const {span, offset, mobile, pad, laptop, pc, pcw, pcx} = this;
    return [
      span && `col-${span}`,
      offset && `offset-${offset}`,
      ...(mobile && [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`]),
      ...(pad && [`col-pad-${pad.span}`, `offset-pad-${(pad.offset)}`]),
      ...(laptop && [`col-laptop-${laptop.span}`, `offset-laptop-${(laptop.offset)}`]),
      ...(pc && [`col-pc-${pc.span}`, `offset-pc-${(pc.offset)}`]),
      ...(pcw && [`col-pcw-${pcw.span}`, `offset-pcw-${(pcw.offset)}`]),
      ...(pcx && [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`]),
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  flex: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: ($i / 24) * 100%;
    }
  }

  // mobile
  @media (max-width: 576px) {/*...*/}

  // pad
  @media (max-width: 577px) and (max-width: 768px) {/*...*/}

  // laptop
  @media (max-width: 769px) and (max-width: 992px) {/*...*/}

  // pc
  @media (max-width: 993px) and (max-width: 1200px) {/*...*/}

  // pcw
  @media (max-width: 1201px) and (max-width: 1600px) {/*...*/}

  // pcx
  @media (min-width: 1601px) {/*...*/}

}
</style>


重构 VueCol.vue

VueCol.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
 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import objKeyValidator from '../../libs/objKeyValidator';

const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
    default: 0
  }) offset!: number;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) mobile!: mediaQuery;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) pad!: mediaQuery;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) laptop!: mediaQuery;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) pc!: mediaQuery;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) pcw!: mediaQuery;

  @Prop({
    type: Object,
    default: () => ({span: 12, offset: 0}),
    validator
  }) pcx!: mediaQuery;

  get colClass() {
    const {span, offset, mobile, pad, laptop, pc, pcw, pcx} = this;
    return [
      span && `col-${span}`,
      offset && `offset-${offset}`,
      ...(mobile && [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`]),
      ...(pad && [`col-pad-${pad.span}`, `offset-pad-${(pad.offset)}`]),
      ...(laptop && [`col-laptop-${laptop.span}`, `offset-laptop-${(laptop.offset)}`]),
      ...(pc && [`col-pc-${pc.span}`, `offset-pc-${(pc.offset)}`]),
      ...(pcw && [`col-pcw-${pcw.span}`, `offset-pcw-${(pcw.offset)}`]),
      ...(pcx && [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`]),
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

}
</script>

<style lang="scss" scoped>
.col {
  min-height: 45px;
  flex: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: ($n / 24) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: ($i / 24) * 100%;
    }
  }

  // mobile
  @media (max-width: 576px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-mobile-;
    @for $n from 1 through 24 {
      &.#{$class-prefix}#{$n} {
        width: ($n / 24) * 100%;
      }
    }

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-mobile-;
    @for $i from 0 through 24 {/*...*/}
  }

  // pad
  @media (max-width: 577px) and (max-width: 768px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-pad-;
    @for $n from 1 through 24 {/*...*/}

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-pad-;
    @for $i from 0 through 24 {/*...*/}
  }

  // laptop
  @media (max-width: 769px) and (max-width: 992px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-laptop-;
    @for $n from 1 through 24 {/*...*/}

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-laptop-;
    @for $i from 0 through 24 {/*...*/}
  }

  // pc
  @media (max-width: 993px) and (max-width: 1200px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-pc-;
    @for $n from 1 through 24 {/*...*/}

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-pc-;
    @for $i from 0 through 24 {/*...*/}
  }

  // pcw
  @media (max-width: 1201px) and (max-width: 1600px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-pcw-;
    @for $n from 1 through 24 {/*...*/}

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-pcw-;
    @for $i from 0 through 24 {/*...*/}
  }

  // pcx
  @media (min-width: 1601px) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-pcx-;
    @for $n from 1 through 24 {/*...*/}

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-pcx-;
    @for $i from 0 through 24 {/*...*/}
  }

}
</style>

  • 需要判断 默认样式
  • 检查没有传外部数据的情况...[],不能为undefined
    • ...(mobile && [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] )
    • 改为...(mobile ? [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] : [] )

GridSystems.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
<template>
  <div>
    <form>
      <fieldset>
        <legend>Grid</legend>
        <details open>
          <summary>media query</summary>
          <VueRow>
            <VueCol :span="2"
                    :mobile="{span: 12}"
                    :pad="{span: 8}"
                    :laptop="{span: 6}"
                    :pc="{span: 4}"
                    :pcw="{span: 2}"
                    :pcx="{span: 1}">
              Aside
            </VueCol>

            <VueCol :span="18" :offset="2"
                    :mobile="{span: 12, offset: 0}"
                    :pad="{span: 16}"
                    :laptop="{span: 18}"
                    :pc="{span: 20}"
                    :pcw="{span: 22}"
                    :pcx="{span: 23}">
              Main
            </VueCol>
          </VueRow>

          <VueRow>
            <VueRow>
              <VueCol :span="6"
                      :mobile="{span: 24}">
                Aside
              </VueCol>

              <VueCol :span="18"
                      :mobile="{span: 24}">
                Main
              </VueCol>
            </VueRow>
          </VueRow>
        </details>
      </fieldset>
    </form>
    <br>

  </div>
</template>

<script lang="ts">
import {Component, Vue} from 'vue-property-decorator';
import VueCol from './grid-system/VueCol.vue';
import VueRow from './grid-system/VueRow.vue';

@Component({
  components: {
    VueCol,
    VueRow
  }
})
export default class GridSystems extends Vue {
  name = 'GridSystems';
}
</script>


重构重复样式

VueCol.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
 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import objKeyValidator from '../../libs/objKeyValidator';

const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
    default: 0
  }) offset!: number;

  @Prop({
    type: Object,
    validator
  }) mobile!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pad!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) laptop!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pc!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcw!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcx!: mediaQuery;

  get colClass() {
    const {span, offset, mobile, pad, laptop, pc, pcw, pcx} = this;
    return [
      ...(pcx ? [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`] : []),
      ...(pcw ? [`col-pcw-${pcw.span}`, `offset-pcw-${(pcw.offset)}`] : []),
      ...(pc ? [`col-pc-${pc.span}`, `offset-pc-${(pc.offset)}`] : []),
      ...(laptop ? [`col-laptop-${laptop.span}`, `offset-laptop-${(laptop.offset)}`] : []),
      ...(pad ? [`col-pad-${pad.span}`, `offset-pad-${(pad.offset)}`] : []),
      ...(mobile ? [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] : []),
      span && `col-${span}`,
      offset && `offset-${offset}`,
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

}
</script>

<style lang="scss" scoped>
@use "sass:list";
@use "sass:math";

.col {
  min-height: 45px;
  flex: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: (math.div($n, 24)) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: (math.div($i, 24)) * 100%;
    }
  }

  // media loops
  // $media-types: ($type, $sizeList)
  $media-types: (
    'mobile': (0 576px),
    'pad': (577px 768px),
    'laptop': (769px 992px),
    'pc': (993px 1200px),
    'pcw': (1201px 1600px),
    'pcx': (1601px 10000px),
  );
  @each $type, $sizeList in $media-types {
    @media (min-width: (list.nth($sizeList, 1))) and (max-width: (list.nth($sizeList, 2))) {
      // .col.col-1 ~ .col.col-24
      $class-prefix: col-#{$type}-;
      @for $n from 1 through 24 {
        &.#{$class-prefix}#{$n} {
          width: (math.div($n, 24)) * 100%;
        }
      }

      // .col.offset-2 ~ .col.offset-24
      $class-prefix: offset-#{$type}-;
      @for $i from 0 through 24 {
        &.#{$class-prefix}#{$i} {
          margin-left: (math.div($i, 24)) * 100%;
        }
      }
    }
  }

}
</style>

  • 双重循环media loops,外层@each $type, $size in $media-types {...},内层@for $n from 1 through 24 {...}
  • 数据结构$media-types: ($type, $size)
 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
@use "sass:list";

// $media-types: ($type, $size)
$media-types: (
  'mobile': 372px,
  'pad': 577px,
  'laptop': 769px,
  'pc': 993px,
  'pcw': 1201px,
  'pcx': 1601px,
);
@each $type, $size in $media-types {
  @media (min-width: $size) {
    // .col.col-1 ~ .col.col-24
    $class-prefix: col-#{$type}-;
    @for $n from 1 through 24 {
      &.#{$class-prefix}#{$n} {
        width: (math.div($n, 24)) * 100%;
      }
    }

    // .col.offset-2 ~ .col.offset-24
    $class-prefix: offset-#{$type}-;
    @for $i from 0 through 24 {
      &.#{$class-prefix}#{$i} {
        margin-left: (math.div($i, 24)) * 100%;
      }
    }
  }
}

选择一种作为默认屏幕

由需求决定默认的屏幕尺寸

  • mobile 作为默认样式,移除所有 mobile 相关代码
  • 设置各类屏幕匹配参数{span, offset, mobile, pad, laptop, pc, pcw, pcx}
    • 去除外部数据默认值default: () => ({span: 12, offset: 0}),
    • 匹配屏幕时启用对应的样式 ...(pcx ? [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`] : []) ,否则为空

更智能的响应式

如果使用组件库的开发者未在VueCol上添加媒体查询的属性如:pad="{xxx}",如何兼容样式

  • 原先的媒体查询是既有min-width又有max-width,限定死了范围
  • 只有特定档位的宽度才能匹配对应样式
  • 当不写对应的属性时,就没有样式

尺寸 向小兼容 依次增大宽度

  • 只写最小宽度@media (min-width: ***px) {xxx} 注意代码顺序与样式覆盖
    • @media (min-width: 370px) {...} 对应mobile样式
    • @media (min-width: 577px) {...} 对应pad样式
    • @media (min-width: 769px) {...} 对应laptop样式
    • @media (min-width: 993px) {...} 对应pc样式
    • @media (min-width: 1201px) {...} 对应pcw样式
    • @media (min-width: 1601px) {...} 对应pcx样式
  • 例如
    • width: 666px; 最先匹配mobile,然后向下匹配pad,直到没有再能匹配的宽度最终应用到pad样式
    • 假设没传:pad="{xxx}"属性,width: 666px; 找到@media (min-width: 370px) {...},会自动匹配mobile的样式

VueCol.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
 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Emit, Prop, Vue} from 'vue-property-decorator';
import objKeyValidator from '../../utils/objKeyValidator';
import _ from 'lodash';

const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;
  screenWidth = document.body.clientWidth;

  @Prop({
    type: Number,
    default: 12
  }) span!: number;

  @Prop({
    type: Number,
    default: 0
  }) offset!: number;

  @Prop({
    type: Object,
    validator
  }) mobile!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pad!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) laptop!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pc!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcw!: mediaQuery;

  @Prop({
    type: Object,
    validator
  }) pcx!: mediaQuery;

  get colClass() {
    const {span, offset, mobile, pad, laptop, pc, pcw, pcx} = this;
    return [
      ...(pcx ? [`col-pcx-${pcx.span}`, `offset-pcx-${(pcx.offset)}`] : []),
      ...(pcw ? [`col-pcw-${pcw.span}`, `offset-pcw-${(pcw.offset)}`] : []),
      ...(pc ? [`col-pc-${pc.span}`, `offset-pc-${(pc.offset)}`] : []),
      ...(laptop ? [`col-laptop-${laptop.span}`, `offset-laptop-${(laptop.offset)}`] : []),
      ...(pad ? [`col-pad-${pad.span}`, `offset-pad-${(pad.offset)}`] : []),
      ...(mobile ? [`col-mobile-${mobile.span}`, `offset-mobile-${(mobile.offset)}`] : []),
      span && `col-${span}`,
      offset && `offset-${offset}`,
    ];
  }

  get colStyle() {
    if (!this.gutter) {
      return {};
    }
    return {
      marginLeft: this.gutter / 2 + 'px',
      marginRight: this.gutter / 2 + 'px'
    };
  }

  @Emit('update:ClientWidth')
  listenResize() {
    _.debounce(() => {
      this.screenWidth = document.body.clientWidth;
    }, 500)();
    return this.screenWidth;
  }

  mounted() {
    window.addEventListener('resize', this.listenResize, true);
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.listenResize, true);
    });
  }

}
</script>

<style lang="scss" scoped>
@use "sass:math";
@use "sass:list";

.col {
  min-height: 45px;
  flex: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  line-height: 45px;
  color: #fff;

  &:nth-child(odd) {
    background-color: #3D8FEE;
  }

  &:nth-child(even) {
    background-color: #7CB8FF;
  }

  // .col.col-1 ~ .col.col-24
  $class-prefix: col-;
  @for $n from 1 through 24 {
    &.#{$class-prefix}#{$n} {
      width: (math.div($n, 24)) * 100%;
    }
  }

  // .col.offset-2 ~ .col.offset-24
  $class-prefix: offset-;
  @for $i from 0 through 24 {
    &.#{$class-prefix}#{$i} {
      margin-left: (math.div($i, 24)) * 100%;
    }
  }

  // media loops
  // $media-types: ($type, $size)
  $media-types: (
    'mobile': 372px,
    'pad': 577px,
    'laptop': 769px,
    'pc': 993px,
    'pcw': 1201px,
    'pcx': 1601px,
  );
  @each $type, $size in $media-types {
    @media (min-width: $size) {
      // .col.col-1 ~ .col.col-24
      $class-prefix: col-#{$type}-;
      @for $n from 1 through 24 {
        &.#{$class-prefix}#{$n} {
          width: (math.div($n, 24)) * 100%;
        }
      }

      // .col.offset-2 ~ .col.offset-24
      $class-prefix: offset-#{$type}-;
      @for $i from 0 through 24 {
        &.#{$class-prefix}#{$i} {
          margin-left: (math.div($i, 24)) * 100%;
        }
      }
    }
  }

}
</style>

  • 实现Mobile First 响应式 移动端优先

根据内部VueRow的子组件数量 即兄弟元素的数量来设置样式

  • gutterToCol(){}gutter属性值传递给子组件
  • const {$children, gutter} = this;
  • $children.forEach((vm: Vue) => {}

VueRow.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
91
92
93
94
95
96
97
<script lang="ts">
import {Component, Prop, Vue} from 'vue-property-decorator';
import VueCol from './VueCol.vue';

@Component({
  components: {VueCol}
})
export default class VueRow extends Vue {
  name = 'VueRow';

  @Prop({
    type: Array, default() {
      return [];
    }
  }) colData!: Record<string, unknown>;

  @Prop({
    type: [Number, String],
  }) gutter!: number | string;

  @Prop({
    type: String,
    default: 'center',
    validator(value: string): boolean {
      return (['', 'left', 'right', 'center', 'space-between', 'space-around']
        .includes(value));
    }
  }) align!: string;

  get rowStyle() {
    const {gutter} = this;
    // console.log('gutter in VueRow', gutter);
    if (this.gutter) {
      return {
        marginLeft: `${-gutter / 2}px`,
        marginRight: `${-gutter / 2}px`
      };
    }
    return {};
  }

  get rowClass() {
    const {align} = this;
    return [
      align && `align-${align}`
    ];
  }

  // Provide 传递 gutter 属性
  // @Provide('gutterToSon') gutterToSon = this.gutter;
  // 传递 gutter 属性
  gutterToCol() {
    const {$children, gutter} = this;
    // console.log('$children in VueRow', $children);
    if ($children && gutter) {
      $children.forEach((vm: Vue) => {
        const state = Vue.observable({gutter});
        Object.assign(vm, state);
        /*
        // const source = {'gutter': gutter};
        const source = {gutter};
        Object.assign(vm, source);
        */
        // (vm as any).gutter = gutter
        // Vue.set(vm,'gutter', gutter);
        // this.$set(vm, 'gutter', gutter);
      });
    }
  }

  mounted() {
    this.gutterToCol();
  }

}
</script>

<template>
  <div class="vue-row"
       :style="rowStyle"
       :class="rowClass">
    <slot>
      <VueCol v-for="(item, index) in colData"
              :span="item.span"
              :offset="item.offset"
              :mobile="item.mobile"
              :pad="item.pad"
              :laptop="item.laptop"
              :pc="item.pc"
              :pcw="item.pcw"
              :pcx="item.pcx"
              :key="index">
      </VueCol>
    </slot>
  </div>
</template>


VueGrid 组件实时监听窗口尺寸变化

Vue.js 在监听 window 上的 事件 时,往往会显得 力不从心

window.resize 比如 canvas 自适应。 根据窗口的变化去变化 canvas 的宽度

1.定义 一个记录宽度属性 并赋默认值

  • screenWidth: document.body.clientWidth 不包括滚动条

2.方法 更新(重新赋值)this.screenWidth

1
2
3
4
5
6
//...
  listenResize() {
    console.log('窗口大小改变时的操作');
    this.screenWidth = document.body.clientWidth;
  }
//...

3.挂载并在销毁前移除方法

reisze 事件在 created或者mounted 的时候 去监听事件,并设置监听回调

beforeDestroy 的时候,移除监听回调

1
2
3
4
5
6
7
8
9
//...
  created() {
    window.addEventListener('resize', this.listenResize);
  }

  beforeDestroy() {
    window.removeEventListener('resize', this.listenResize);
  }
//...
  • 注意注册监听不可用箭头函数,否则移除回调时无法引用
  • 添加事件监听、移除事件监听的格式必须一致,否则会移除失效

This will:

  • register your Vue method on component creation
  • trigger myEventHandler when the browser window is resized
  • free up memory once your component is destroyed.

参考

高内聚化

  • 通过hook监听组件销毁钩子函数,并取消监听事件,代替 写beforeDestroy 钩子
1
2
3
4
5
6
7
8
9
//...
  mounted() {
    window.addEventListener('resize', this.listenResize);
    // 通过hook监听组件销毁钩子函数,并取消监听事件
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.listenResize);
    });
  }
//...
  • 在Vue组件中,可以用过$on$once去监听所有的生命周期钩子函数
  • 如监听组件的updated钩子函数可以写成 this.$on('hook:updated', () => {})

参考


4.方法改为 发布自定义事件 传递参数至父组件

1
2
3
4
5
6
7
//...
  @Emit('update:ClientWidth')
  listenResize() {
    console.log('窗口大小改变时的操作');
    return this.screenWidth = document.body.clientWidth;
  }
//...

5.做一下防抖处理

自己写的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ...
  resizeTimer: number | null = null;

  @Emit('update:ClientWidth')
  listenResize() {
    if (this.resizeTimer) {
      clearTimeout(this.resizeTimer);
    }
    this.resizeTimer = setTimeout(() => {
      console.log('resize');
      this.screenWidth = document.body.clientWidth;
    }, 1000);
    return this.screenWidth;
  }
//...

提取为debounce函数

  • 如果使用了debounce 防抖
  • 不要将 debounce 放到 addEventListener 的方法里,直接放在处理函数里

例如:

  • window.addEventListener('resize', debounce(this.pageResize,200)) 移除失效
  • 需要将debounce()放在this.pageResize方法里面

./src/utils/debounce.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export const debounce = <T extends (...args: any[]) => any>(
  callback: T,
  waitFor = 200
) => {
  let timeout: ReturnType<typeof setTimeout>;
  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any;
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      result = callback(...args);
    }, waitFor);
    return result;
  };
};

VueCol.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
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Emit, Prop, Vue} from 'vue-property-decorator';
import objKeyValidator from '../../utils/objKeyValidator';
import debouce from '../../utils/debounce';

const validator = (value: mediaQuery) => objKeyValidator(value, ['span', 'offset']);

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;
  screenWidth = document.body.clientWidth;

  //...

  get colClass() {...}

  get colStyle() {...}

  @Emit('update:ClientWidth')
  listenResize() {
    debounce(() => {
      this.screenWidth = document.body.clientWidth;
    }, 500)();
    return this.screenWidth;
  }

  mounted() {
    window.addEventListener('resize', this.listenResize, true);
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.listenResize, true);
    });
  }

}
</script>

使用lodash

  • 安装yarn add lodash
  • 安装类型yarn add -D @types/lodash
  • 引用import _ from 'lodash';
 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
<template>
  <div class="col"
       :style="colStyle"
       :class="colClass">
    <slot></slot>
  </div>
</template>

<script lang="ts">
import {Component, Emit, Prop, Vue} from 'vue-property-decorator';
import _ from 'lodash';
// ...

@Component
export default class VueCol extends Vue {
  name = 'VueCol';
  gutter = 0;
  screenWidth = document.body.clientWidth;

  @Prop...

  get colClass() {/*...*/}

  get colStyle() {/*...*/}

  @Emit('update:ClientWidth')
  listenResize() {
    _.debounce(() => {
      this.screenWidth = document.body.clientWidth;
    }, 500)();
    return this.screenWidth;
  }

  mounted() {
    window.addEventListener('resize', this.listenResize, true);
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.listenResize, true);
    });
  }

}
</script>

6.其他

  • 不可直接在组件上监听事件@resize,内部无法处理windowdocument 上的事件

7.使用 Vue.js 的第三方库监听resize事件

1.使用库vue-resize 添加监听组件<resize-observer @notify="handleResize" />

1
2
3
4
5
6
<template>
  <div class="demo">
    <h1>Hello world!</h1>
    <resize-observer @update:notify="handleResize" />
  </div>
</template>

2.或者使用第三方封装的指令David-Desmaisons/Vue.resize

1
2
3
4
5
6
7
<template>
<div v-resize="onResize">
<div v-resize:throttle="onResize">
<div v-resize:throttle.100="onResize">
<div v-resize:debounce="onResize">
<div v-resize:debounce.50="onResize">
</template>

Vue directive to detect HTML resize events based on CSS Element Queries with debouncing and throttling capacity.

3.或者使用vue-window-size取得 width, height属性


参考


单元测试

异步测试

时机

1
2
3
4
5
6
7
let a = [];
console.log(a);
// []
a.push(1);
// 1
// 再次点开 Chrome 控制台 返回的 [] 发现有 1
// 变更时异步的过程

VueRow中的 gutter

  • 使用了钩子传递参数时,必须使用异步测试

业界知名UI



参考

相关文章


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