Vue代码风格

良好的代码风格更容易维护代码,虽然没有绝对的标准,但保持自己一贯的代码风格可以提高代码质量。接下来就来介绍我开发 Vue 代码的大致风格吧。

# DatePicker.vue

<template>
  <!-- 组件名字应该大写字母开头,组件最外层节点类名和组件文件名保持一致,PascalCase改成下划线 -->
  <div class="date-picker" v-click-outside="clickClose">
    <!-- 属性定义应该先定义原生属性,再定义Vue属性 -->
    <div
      class="date-picker-rel"
      ref="reference"
      @click="handleClick"
      :class="{ 'o-disabled': disabled }"
    >
      <slot name="picker">
        <VueInput
          v-model="currentDate"
          class="date-picker-input"
          :disabled="disabled"
          :placeholder="placeholder"
          :clearable="clearable"
          @on-clear="clearDate"
          @on-input="input"
          @on-blur="updateDate"
        >
          <template v-slot:suffix>
            <svg-icon
              name="date"
              class="date-picker-icon"
              :class="{ 'o-disabled': disabled, 'o-show': visible }"
            />
          </template>
        </VueInput>
      </slot>
    </div>
    <transition name="fade">
      <div
        class="date-picker-popper"
        :style="styles"
        ref="popper"
        v-show="visible && !disabled"
      >
        <DateTable
          :selectedDate="formatedDate"
          :isRange="isRange"
          :visible="visible"
          @select="selectDate"
        />
      </div>
    </transition>
  </div>
</template>

<script lang="ts">
/**
 * 模块定义:
 * 1. 先第三方后自定义
 * 2. 除非是组件内部私有模块,否则全部使用 @ 绝对路径引用
 * 3. 按需引用
 */
import { Component, Mixins, Prop, Watch } from 'vue-property-decorator';
// @ts-ignore
import { directive as clickOutside } from 'v-click-outside-x';
import Input from '@/components/input';
import PopperMixin from '@/mixins/popper';
import dateUtils from '@/utils/date';
import DateTable from './DateTable.vue';
import { namespace } from 'vuex-class';

const Info = namespace('information');

@Component({
  directives: {
    clickOutside
  },
  components: {
    DateTable,
    VueInput: Input
  },
  provide() {
    return {
      picker: this
    };
  }
})
// 类名和组件名保持一致,方便调试
export default class DatePicker extends Mixins(PopperMixin) {
  /**
   * 属性定义区域
   */
  @Prop() readonly value!: Date | null | Array<Date | null>;
  @Prop({ default: false }) readonly disabled!: boolean;
  @Prop({ default: false, type: Boolean }) readonly clearable!: boolean;
  @Prop({ default: '' }) readonly placeholder!: string;
  /**
   * State映射区域,定义顺序是: State Getter Action Mutation
   */
  @Info.State notifications!: Array<IAnnouncement>;

  /**
   * data定义区域
   */
  currentDate = '';
  updateByInput = false;

  /**
   * computed区域, 如果同时有 get 和 set 记得配对写一起
   */
  get formatedDate(): Array<Date | null> {
    if (Array.isArray(this.value)) {
      return this.value;
    } else {
      return [this.value];
    }
  }

  get isRange() {
    return Array.isArray(this.value);
  }

  /**
   * watch区域, 函数名 on + 属性名 + Changed
   */
  @Watch('value', { immediate: true })
  onValueChanged() {
    this.currentDate = dateUtils.formatDate(this.value);
  }

  /**
   * 生命周期函数
   * beforeCreate -> created -> beforeMount -> mounted -> beforeUpdate ->
   * updated -> activated -> deactivated -> beforeDestroy -> destroyed -> errorCaptured
   */
  created() {
    // noop
  }

  /**
   * methods 区域
   */
  clickClose() {
    if (this.visible && !this.disabled) {
      this.closePopper();
    }
  }

  handleClick() {
    if (!this.disabled) {
      if (this.visible) {
        this.closePopper();
      } else {
        this.showPopper();
      }
    }
  }

  clearDate() {
    if (this.isRange) {
      this.$emit('input', [null, null]);
    } else {
      this.$emit('input', null);
    }
  }

  input() {
    this.updateByInput = true;
  }

  updateDate() {
    if (this.updateByInput) {
      if (this.isRange) {
        const dates = this.currentDate.split(' - ');
        if (dates.length > 1) {
          const [start, end] = dates;
          const startDate = dateUtils.parseDate(start);
          const endDate = dateUtils.parseDate(end);
          if (
            !Number.isNaN(startDate.getTime()) &&
            !Number.isNaN(endDate.getTime())
          ) {
            this.$emit(
              'input',
              [startDate, endDate].sort((s, e) => {
                return s.getTime() - e.getTime();
              })
            );
          } else {
            this.$emit('input', this.value);
            this.currentDate = dateUtils.formatDate(this.value);
          }
        }
      } else {
        const startDate = new Date(this.currentDate);
        if (!Number.isNaN(startDate.getTime())) {
          this.$emit('input', startDate);
        } else {
          this.$emit('input', this.value);
          this.currentDate = dateUtils.formatDate(this.value);
        }
      }
      this.updateByInput = false;
    }
  }

  selectDate(date: Date | Array<Date>) {
    this.currentDate = dateUtils.formatDate(date);
    this.$emit('input', date);
    this.clickClose();
  }
}
</script>
<style lang="scss">
.date-picker {
  display: block;
  /**
   * CSS书写顺序
   * 以 Formatting Model (布局方式,位置) -> Box Model (尺寸) -> Typographic (文本相关) -> Visual (视觉效果)
   * 的顺序书写,可以提高代码的可读性。
   * Formatting Model 相关属性包括: position, top, right, bottom, left, float, display, overflow 等
   * Box Model 相关属性包括: border, margin, padding, width, height 等
   * Typographic 相关属性包括: font, line-height, text-align, word-wrap 等
   * Visual 相关属性包括: background, color, transition, list-style 等
   * 如果有 content 属性, 应该放在最前面。
   */

  &-popper {
    width: 272px;
  }
}
</style>
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226

# 总结

良好的代码风格可以提高代码的可读性,建议大家都保持一致的风格,方便团队合作。

# 参考

Organizing your CSS (opens new window)

数据库查询的N+1问题

在工作中有时也会涉及到后端开发,后端开发的性能优化多是查询优化。类似前端 DOM 操作很消耗性能,后端开发数据库查询次数也会相当消耗性能。我们在开发中应该尽量减少 SQL 查询次数,避免 N+1 问题来提升性能。接下来就来介绍数据库查询的 N+1 问题。

# SQL N+1

举一个简单的例子,数据库中有一张 cars 表存储汽车的相关信息,还有一张 wheels 表存储汽车的轮胎信息。

查询汽车的语句是:

SELECT * FROM cars;
1

查询每个汽车的轮子的语句是:

SELECT * FROM wheels WHERE car_id = ${car_id};
1

这里的 N 就是汽车的数量,如果需要知道每个汽车的轮胎信息那就正好需要查询 N+1 次。

# 解决 N+1

如果汽车和轮胎是一对一的关系可以通过使用 join 语句来减少数据库查询次数。把查询降为 1 次。修正之后的查询语句是:

SELECT cars.*, wheels.* FROM cars INNER JOIN wheels ON wheels.car_id = cars.id
1

如果汽车和轮胎是一对多的关系可以通过使用:

SELECT * FROM cars;
SELECT * FROM wheels WHERE wheels.car_id IN (1,2,3)
1
2

一次性查询所有需要的轮胎把查询降为两次。

# 现有框架解决方案

# Django

Django 可以使用select-related (opens new window)prefetch-related (opens new window)来解决这个问题。

select-related 主要用于一对一的关系,而 prefetch-related 主要用于多对多或者多对一的关系。

# Ruby on Rails

使用 includes (opens new window) 来解决 N+1 问题。

# 总结

后端开发需要时刻注意 N+1 问题, 在开发模式下,日志会输出 SQL 查询语句,需要多加注意。也可以使用一些工具来检测这个问题。

# 参考

What is the “N+1 selects problem” in ORM (opens new window)

Active Record Query Interface (opens new window)

提高网页的易读性

正常人可以通过鼠标和键盘来操作浏览器,使用滚轮来滚动网页,使用鼠标左键和右键来执行操作,使用键盘输入内容。但是对于残疾人来说可能就不是这样的了,如果一个人身体行动不便只能通过键盘敲击来操作电脑,无法使用鼠标的情况下,如果网页不支持键盘 Tab 键来切换各种点击按钮,那就会给残疾人的使用带来困难。如果视力不好的人只能借助屏幕阅读器来查看网页,如果按钮或者图片没有合适的标签信息,那就无法通过屏幕阅读器来听到这个按钮或者图片的含义。我们不仅要考虑正常人如何查看网页,也需要顾及到特殊人群的需求,这样才能让每个人都享受到互联网带来的便利。

# focus

focus 状态对于网页易读性非常重要,foucs 决定了键盘事件的去向。但是我们在项目开发的时候经常会给 button 添加鼠标的 hover 状态,却忘记了添加 focus 状态。添加 hover 状态的 CSS 时,记得同时添加 focus 状态的 CSS。同时,浏览器会有默认的 focus 样式, Chrome 下是使用 outline 属性添加高亮的蓝色边框,记得不要轻易覆盖这个属性为 none。 但是并不是所有的 HTML 元素都是可以 focus 的,下面是查找可以 focus 的元素的方法。

const candidateSelectors = [
  'input:not([disabled])',
  'select:not([disabled])',
  'textarea:not([disabled])',
  'a[href]',
  'area[href]',
  'button:not([disabled])',
  '[tabindex="0"]',
  'audio[controls]',
  'video[controls]',
  '[contenteditable]:not([contenteditable="false"])',
  'details>summary'
];
const candidateSelector = candidateSelectors.join(',');

type Option = {
  shouldIgnoreVisibility: boolean;
};
const defaultOption: Option = {
  shouldIgnoreVisibility: false
};

function isDisplayNone(node: Element | null): boolean {
  if (!node) {
    return false;
  }
  if (getComputedStyle(node).display === 'none') return true;
  return isDisplayNone(node.parentElement);
}

function isHidden(node: Element) {
  if (getComputedStyle(node).visibility === 'hidden') return true;
  if (isDisplayNone(node)) return true;

  return false;
}

export function tabbable(el: HTMLElement, option?: Partial<Option>) {
  const mergedOption = {
    ...defaultOption,
    ...option
  };
  const candidates = Array.from(
    el.querySelectorAll<HTMLElement>(candidateSelector)
  );
  if (mergedOption.shouldIgnoreVisibility) {
    return candidates;
  }
  return candidates.filter(candidate => !isHidden(candidate));
}
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

# DOM Order

当你使用键盘的 Tab 来选择下一个按钮的时候,是如何决定顺序的呢? 答案是根据 DOM 的顺序而不是界面上 UI 的顺序,你可以使用 CSS 来调整 UI 顺序,但是 Tab 顺序不会改变。 针对隐藏的元素, Tab 键也不会触发这个元素的 focus,隐藏的元素 CSS 属性可能含有 display: none 或者 visibility: hidden

# tabindex

对于 buttona 元素,当然是可以 focus 的,但是针对类似 div 这样的元素,如何让它可以 focus 呢? 答案是添加 tabindex 属性。

<custom-button tabindex="0">Press Tab to Focus Me!</custom-button>
1

通过添加 tabindex="0" 可以使这个元素被 focus。那么如何使一个元素不被 focus呢?

<button id="foo" tabindex="-1">I'm not keyboard focusable</button>
<button onclick="foo.focus();">Focus my sibling</button>
1
2

使用 tabindex="-1" 可以确保这个元素不会使用键盘 focus,但是鼠标没有影响。 tabindex 当然可以设置大于 0 的值,但我们不推荐这么做,因为默认元素的 tabindex 都是 0,所以大于 0 的元素最后才会触发 focus。 现在的网页开发,我们都会自己手动实现 select 或者 dropdown 元素,但由于这些不是默认的 HTML 元素,所以需要自己实现键盘的 Tab 键或者方向键的触发。

# Modals and keyboard traps

网页开发中经常会出现弹窗,在有弹窗的情况下我们是不能选中背景中的元素的,所以我们需要手动处理 Tab 键保证只有 Modal 里的元素可以被 focus。并且在关闭 Modal 的时候 focus 回原来的元素。

# Semantics and ARIA

我们经常使用 HTML 元素来模拟一些功能,例如使用 button 按钮来模拟菜单功能。这时候我们需要为 button 添加 aria-label 属性方便屏幕阅读器理解这个按钮代表的含义。

<button aria-label="menu"></button>
1

当然啦,这种类似的属性还有很多,例如 role 属性来描述这个元素的定位。

# Accessibility Review

如何检查自己网页的易读性呢,现在也有现成的网页工具和可以集成的 CI 工具。我们可以使用 Chrome 浏览器自带的 Lighthouse 来检查自己网页的易读性。如果想集成 CI 的话,可以试试axe-core (opens new window)

# 总结

提高网页的易读性,不仅可以使用键盘提高操作网页的便利性,也可以照顾特殊人群的使用,何乐而不为呢。

# 参考

Modals and keyboard traps sample code (opens new window)

Accessibility (opens new window)

WAI-ARIA Authoring Practices (opens new window)