代理Sentry的请求

Sentry (opens new window)是一款非常不错的网页监控工具,可以帮忙收集网页端的报错方便分析用户使用过程中遇到的问题。但是因为有收集用户隐私的风险,有些广告屏蔽软件会屏蔽 Sentry 的请求,所以我们需要使用反向代理的方式,让 Sentry 把请求发到我们自己的后端,我们再转发到 Sentry 服务器上。

# Sentry 端配置

添加 tunnel 选项,Sentry 就会把请求发到这个路径下,然后我们后台再处理转发。

Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  tunnel: '/sentry'
});
1
2
3
4

# 后台转发请求

app.set('trust proxy', true);

app.use(compression());
app.use(express.text());
app.use(express.json());
// 注意这三行代码一定要放在前面才能正常处理请求

app.post(/^\/sentry/, (req, res) => {
  const envelope = req.body;
  const piece = envelope.split('\n')[0];
  const header = JSON.parse(piece);
  if (header.dsn) {
    const dsn = new URL(header.dsn);
    const projectId = dsn.pathname.substr(1);
    const options = {
      hostname: dsn.hostname,
      port: 443,
      path: `/api/${projectId}/envelope/`,
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-sentry-envelope',
        'X-Forwarded-For': req.ip
      }
    };
    const request = https.request(options);
    request.write(envelope);
    request.end();
  }
  res.json({});
});
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

后台 Web 服务器是使用经典的 express,转发的时候记得开启trust proxy,因为我们的服务器基本会放在 Load balancing 后面,为了获取到真实的客户 IP 地址,我们需要层层转发保证收集到的信息是真实 IP 地址,而不是我们后端 IP 地址。

# 总结

Sentry 的反向代理本身并不是很复杂,但是网络上并没有相关的 JS 代码实现和处理 IP 转发问题,所以希望我的代码例子能帮助大家快速的解决这个问题。

# 参考

Dealing with Ad-Blockers (opens new window)

CSS如何处理固定Header哈希标记跳转的问题

有些前端网站一直都有固定的 Header 显示导航,如果我们文档内部有使用 ID 和#符号来帮助跳转到文档某个位置的话,就会被 Header 遮挡住,那我们怎么处理才能让跳转内容正好显示在 Header 下方呢?答案很简单就是让带有 ID 的这个元素占据的空间包含 Header 的高度。

# padding 和 marging 搭配使用

为了保证多占据的空间和 Header 的高度保持一致,我们可以使用 CSS 变量来存储 Header 的高度,这样就能保证一致性。

h1[id] {
  padding-top: var(--header-height);
  margin-top: calc(var(--header-height) * -1);
}
1
2
3
4

margin 可以设定成负数来吃掉撑起来的高度,所以会使元素占据的高度等于显示的高度加上 Header 的高度,这样跳转的时候就能保证内容显示到 Header 下方了。

# 伪元素来撑起空间

h1[id]:before {
  content: '';
  display: block;
  width: 0;
  height: var(--header-height);
  margin-top: calc(var(--header-height) * -1);
}
1
2
3
4
5
6
7

有些时候我们不方便编辑 id 元素本身的 CSS 属性,所以我们可以借助伪元素来实现这个效果。

# 总结

在 CSS 的世界中,margin 的负值有很多妙用,例如显示列表中如果卡片之间有左右间距,但是最外层却需要和容器保持左右一直,我们也可以使用 margin 的负值来解决这个问题。

# 参考

offsetting an html anchor to adjust for fixed header (opens new window)

JS 关于颜色的相关处理

项目开发中,需要根据网站设定的主题色来动态更改网站的所有配色,例如文字颜色,鼠标 hover 状态的按钮颜色,这就涉及到对颜色的相关处理,接下来我们就讲解下如何处理。

# 颜色的转换

颜色的表示方式目前主要有三种,rgba, hex 和 hls。这三种表示方式可以互相转换。主要是 hex 和 hls 进行转换的公式比较复杂,网上有现成的示例代码可以参考。

# 判断颜色是深色还是浅色

W3C 提供了官方的说明,把 RGB 格式的颜色转换成 YIQ 格式,然后通过 brightness 数值来进行判断。

  isDark() {
    const { R, G, B } = this.rgbColor;
    // ref https://www.w3.org/TR/AERT/#color-contrast
    const brightness = (R * 299 + G * 587 + B * 114) / 1000;
    return brightness < 128;
  }
1
2
3
4
5
6

# 计算对比度

W3C 提供了官方的对比度计算公式,对比度大于等于 4.5 属于 AA,对比度大于等于 7.1 属于 AAA。至少要达到 AA 才能有足够的对比度。

# 调节亮度

把颜色格式转换成 HLS, L 就代表亮度,调节这个数值就可以了。

# 完整代码

export interface RGBColor {
  R: number;
  G: number;
  B: number;
}

export interface HSLColor {
  H: number;
  S: number;
  L: number;
}

export type ColorType = string | RGBColor | HSLColor;

export class Color {
  rgbColor: RGBColor;

  constructor(color: ColorType) {
    if (this.isString(color)) {
      this.rgbColor = this.stringToRGB(color);
    } else if (this.isHSLColor(color)) {
      this.rgbColor = this.HSLToRGB(color);
    } else {
      this.rgbColor = color;
    }
  }

  get hexColor() {
    return this.RGBToHex(this.rgbColor);
  }
  get hslColor() {
    return this.RGBToHSL(this.rgbColor);
  }

  isString(color: string | RGBColor | HSLColor): color is string {
    return typeof color === 'string';
  }

  isRGBColor(color: string | RGBColor | HSLColor): color is RGBColor {
    return (color as RGBColor).R !== undefined;
  }

  isHSLColor(color: string | RGBColor | HSLColor): color is HSLColor {
    return (color as HSLColor).H !== undefined;
  }

  RGBToHex(rgbColor: RGBColor) {
    const { R, G, B } = rgbColor;
    const hexR = R.toString(16).padStart(2, '0');
    const hexG = G.toString(16).padStart(2, '0');
    const hexB = B.toString(16).padStart(2, '0');

    return '#' + hexR + hexG + hexB;
  }

  stringToRGB(color: string) {
    if (color.length === 4) {
      color =
        color[0] +
        color[1] +
        color[1] +
        color[2] +
        color[2] +
        color[3] +
        color[3];
    }

    const R = Number.parseInt(color.substring(1, 3), 16);
    const G = Number.parseInt(color.substring(3, 5), 16);
    const B = Number.parseInt(color.substring(5, 7), 16);
    return { R, G, B };
  }

  HSLToRGB(hslColor: HSLColor) {
    let { S, L } = hslColor;
    const { H } = hslColor;
    S /= 100;
    L /= 100;

    const C = (1 - Math.abs(2 * L - 1)) * S;
    const X = C * (1 - Math.abs(((H / 60) % 2) - 1));
    const M = L - C / 2;
    let R = 0;
    let G = 0;
    let B = 0;
    if (0 <= H && H < 60) {
      R = C;
      G = X;
      B = 0;
    } else if (60 <= H && H < 120) {
      R = X;
      G = C;
      B = 0;
    } else if (120 <= H && H < 180) {
      R = 0;
      G = C;
      B = X;
    } else if (180 <= H && H < 240) {
      R = 0;
      G = X;
      B = C;
    } else if (240 <= H && H < 300) {
      R = X;
      G = 0;
      B = C;
    } else if (300 <= H && H < 360) {
      R = C;
      G = 0;
      B = X;
    }
    R = Math.round((R + M) * 255);
    G = Math.round((G + M) * 255);
    B = Math.round((B + M) * 255);

    return { R, G, B };
  }

  RGBToHSL(rgbColor: RGBColor) {
    let { R, G, B } = rgbColor;
    R /= 255;
    G /= 255;
    B /= 255;

    const cmin = Math.min(R, G, B);
    const cmax = Math.max(R, G, B);
    const delta = cmax - cmin;
    let H, S, L;

    if (delta === 0) {
      H = 0;
    } else if (cmax === R) {
      H = ((G - B) / delta) % 6;
    } else if (cmax == G) {
      H = (B - R) / delta + 2;
    } else {
      H = (R - G) / delta + 4;
    }
    H = Math.round(H * 60);
    if (H < 0) {
      H += 360;
    }
    L = (cmax + cmin) / 2;
    S = delta == 0 ? 0 : delta / (1 - Math.abs(2 * L - 1));
    S = +(S * 100).toFixed(1);
    L = +(L * 100).toFixed(1);

    return { H, S, L };
  }

  isDark() {
    const { R, G, B } = this.rgbColor;
    // ref https://www.w3.org/TR/AERT/#color-contrast
    const brightness = (R * 299 + G * 587 + B * 114) / 1000;
    return brightness < 128;
  }

  isLight() {
    return !this.isDark();
  }

  darken(percent: number, color = this.hslColor) {
    let { L } = color;
    const { H, S } = color;
    L -= L * percent;
    return new Color({ H, S, L });
  }

  lighten(percent: number, color = this.hslColor) {
    let { L } = color;
    const { H, S } = color;
    L += L * percent;
    return new Color({ H, S, L });
  }

  luminance(rgbColor = this.rgbColor) {
    // ref https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
    let { R, G, B } = rgbColor;
    [R, G, B] = [R, G, B].map(color => {
      color = color / 255;
      return color <= 0.03928
        ? color / 12.92
        : Math.pow((color + 0.055) / 1.055, 2.4);
    });
    return 0.2126 * R + 0.7152 * G + 0.0722 * B;
  }

  contrast(bgColor: ColorType = '#fff', contrastColor = this.rgbColor) {
    const color = new Color(bgColor);
    const bgLuminance = this.luminance(color.rgbColor);
    const textLuminance = this.luminance(contrastColor);
    return bgLuminance > textLuminance
      ? (bgLuminance + 0.05) / (textLuminance + 0.05)
      : (textLuminance + 0.05) / (bgLuminance + 0.05);
  }

  level(bgColor: ColorType = '#fff') {
    const color = new Color(bgColor);
    const contrastRatio = this.contrast(color.rgbColor);
    return contrastRatio >= 7.1 ? 'AAA' : contrastRatio >= 4.5 ? 'AA' : '';
  }

  highContrastColor(bgColor: ColorType = '#fff') {
    let contrastColor = new Color(this.rgbColor);
    if (this.isLight()) {
      const color = new Color(bgColor);
      let contrastRatio = this.contrast(color.rgbColor, contrastColor.rgbColor);
      while (contrastRatio < 4.5) {
        contrastColor = contrastColor.darken(0.1);
        contrastRatio = this.contrast(color.rgbColor, contrastColor.rgbColor);
      }
    }
    return contrastColor;
  }

  textColor() {
    if (this.isLight()) {
      return new Color('#000');
    } else {
      return new Color('#fff');
    }
  }
}
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

本代码实现了颜色格式的转换,计算颜色属于深色还是浅色,计算两个颜色之间的对比度。给定一个颜色,计算出相应的高对比度颜色,可以用于背景和文本的展示。

# 总结

颜色相关计算有 W3C 提供了标准的计算公式,但是还没有完整的代码库可以参考。希望我总结的代码可以给大家提供参考。

# 参考

Converting Color Spaces in JavaScript (opens new window)

Calculating Color Contrast (opens new window)

Color and contrast accessibility (opens new window)

WebAIM: Contrast Checker (opens new window)

ColorShark – WCAG 2.1 AA and AAA Color Contrast Tool (opens new window)