代理Sentry的请求

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

Sentry 端配置 ​

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

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

后台转发请求 ​

js
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({});
});

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

总结 ​

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

参考 ​

Dealing with Ad-Blockers

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

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

padding 和 marging 搭配使用 ​

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

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

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

伪元素来撑起空间 ​

css
h1[id]:before {
  content: '';
  display: block;
  width: 0;
  height: var(--header-height);
  margin-top: calc(var(--header-height) * -1);
}

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

总结 ​

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

参考 ​

offsetting an html anchor to adjust for fixed header

JS 关于颜色的相关处理

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

颜色的转换 ​

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

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

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

ts
  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;
  }

计算对比度 ​

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

调节亮度 ​

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

完整代码 ​

ts
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');
    }
  }
}

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

总结 ​

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

参考 ​

Converting Color Spaces in JavaScript

Calculating Color Contrast

Color and contrast accessibility

WebAIM: Contrast Checker

ColorShark – WCAG 2.1 AA and AAA Color Contrast Tool