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)

如何瘦身docker镜像

容器化部署已经成为现在的主流,打包 docker 镜像已经是现代前端开发的必修课了。最近正好在做新项目的时候处理过相关事情,所以记录一下。

# docker 部署的需求

首先我们需要使用 docker 完成网站源代码的编译,并且还要自己使用 expressjs (opens new window) 搭建服务器。让网站可以运行,docker 跑起来之后只用负责端口转发即可。

大家都知道前端网站编译需要安装很多依赖,但这些依赖在网站运行的时候并不需要。那如何才能做到,编译之后丢弃这些不用的依赖呢? 我一开始尝试主动删除,但发现并没有减少 docker 的大小。

答案是Use multi-stage builds (opens new window)

# 编译网站

首先我们需要安装依赖并编译网站,dockerfile 内容如下:

FROM node:14.17.5-buster-slim as build
ARG GITHUB_PACKAGES_TOKEN
RUN apt-get update && apt-get install -y --no-install-recommends autoconf automake g++ libpng-dev make

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
COPY package.json yarn.lock /tmp/
RUN echo $GITHUB_PACKAGES_TOKEN
RUN cd /tmp && yarn install --forzen-lockfile --production=false
RUN mkdir -p /app && mv /tmp/node_modules /app
# From here we load our application's code in, therefore the previous docker
# "layer" thats been cached will be used if possible
WORKDIR /app
COPY . /app
ENV NODE_ENV production
RUN yarn build && yarn express:build
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面的命令编译了网站和 express 服务端代码,这里选择的 node 官方镜像并不是精简版,因为编译的时候需要很多依赖,精简版的话反而安装系统依赖更花时间。

# 安装依赖

网站运行并不需要 node_modules 但是 express 服务器是需要的,所以我们需要安装后端的依赖。

FROM node:14.17.5-alpine3.14 as deps

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
COPY server/package.json server/yarn.lock /tmp/
RUN cd /tmp && yarn install --forzen-lockfile --production=false
1
2
3
4
5
6

服务端的 package.json 文件我放在了 server 文件夹下,服务端依赖的东西很少,所以不太占用空间。

# 打包实际生成的文件

FROM node:14.17.5-alpine3.14
WORKDIR /app
COPY . /app
COPY --from=deps /tmp/node_modules ./node_modules/
COPY --from=build /app/dist ./dist/
COPY --from=build /app/server ./server/
1
2
3
4
5
6

接下来的步骤很简单了, 选择占用空间最小的 Alpine Linux 然后把编译好的文件复制进来就可以了。前两个步骤的所有东西都会被丢弃掉。所以实际生成的 docker 镜像会非常小。

# 总结

采用多阶段编译,可以有效的减少编译的依赖对空间的占用,做到最小化 docker 镜像。

# 参考

3 simple tricks for smaller Docker images (opens new window)