禁用页面滚动

在设计组件的时候,例如 Modal 弹窗组件,在显示组件的时候我们不希望页面可以滚动。这时候就需要临时禁用页面的滚动功能。接下来就讲解下禁用页面滚动的方法。

# 禁用滚动

document.body.style.setProperty('overflow', 'hidden');
1

禁用滚动的方法很简单,只需要在页面的 body 元素上设置 overflow 属性为 hidden,就可以禁用页面的滚动了。但是我们需要考虑到,如果我们的页面有滚动条,滚动条的消失会影响页面的显示效果。 所以完美的禁用滚动方式需要考虑到当前页面滚动条的宽度。

function getScrollbarWidth() {
  const scrollbarWidth =
    window.innerWidth - document.documentElement.clientWidth;
  return scrollbarWidth;
}

function lockBody() {
  const scrollbarWidth = getScrollbarWidth();
  if (scrollbarWidth) {
    document.body.style.setProperty('padding-right', `${scrollbarWidth}px`);
  }
  document.body.style.setProperty('overflow', 'hidden');
}
1
2
3
4
5
6
7
8
9
10
11
12
13

为了保证不影响页面的显示效果,我们需要计算出当前页面滚动条的宽度,然后设置 body 的 padding-right 属性,这样就可以保证页面的显示效果。

# 恢复滚动

function unlockBody() {
  document.body.style.removeProperty('overflow');
  document.body.style.removeProperty('padding-right');
}
1
2
3
4

恢复滚动只需要把添加到 CSS 属性删除即可。

# 总结

如果一直使用 Mac 的触摸板来编程的话,有时候很难意识到滚动条的存在,但是对于大多数的 Windows 用户来说,滚动条会一直存在的。

# 参考

use-scroll-lock.ts (opens new window)

代理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)

使用Service Worker的 Web Push 功能

现代的 Android 应用和 iOS 应用都有相应的推送系统,可以给用户推送消息。即使用户没有打开这个应用,也同样可以收到消息。当然,网页既然要向手机应用靠拢的话,那推送功能也是必不可少。 今天我们就来介绍一下,网页如何实现类似的推送功能。

# 基于 Service Worker 的推送功能

Service Worker 提供了两种推送服务,一种是基于 Firebase Cloud Messaging 的推送功能,一种是基于 VAPID 的推送功能。第一种已经过时了,所以本文只介绍第二种。

# Create a public/private key pair

首先,我们需要创建公钥和私钥,类似与 HTTPS 的 RSA 加密。客户端使用公钥订阅推送,服务端使用私钥来发送通知。创建既可以在浏览器进行,也可以使用 Node 创建。当然,推荐在后端生成更安全。

前端生成代码参考链接:util.ts (opens new window)

后端使用web-push (opens new window)可以更方便的生成公钥和私钥。代码如下:

function generateVAPIDKeys() {
  const vapidKeys = webpush.generateVAPIDKeys();

  return {
    publicKey: vapidKeys.publicKey,
    privateKey: vapidKeys.privateKey
  };
}
1
2
3
4
5
6
7
8

# Subscribing with the public key

接下来我们需要订阅推送:

navigator.serviceWorker.ready
  .then(registration => {
    return registration.pushManager.getSubscription().then(subscription => {
      if (subscription === null) {
        return registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: this.vapidPublicKey
        });
      } else {
        return subscription;
      }
    });
  })
  .then(subscription => {
    this.subJSON = subscription.toJSON();
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们使用公钥进行订阅,并且把订阅之后的内容打印出来,方便服务器使用。实际工程中需要发送给服务器进行保存。使用 Chrome 浏览器订阅的信息是这样的:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/d3cMmjx0Eg8:APA91bF1i7wgLJRw-VgOh3Evn6RG1xqdOR6Y0CeTUm1xiD36BCHXaDoceVfilDYiifWdI_rWdU8IdJjqSxaCVscRp5zl9lon8u4mf9mha0fmSVKJzUOx5r5Jba2yiNmCFRxxKcTJm51S",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDtGmJB0Bkyum0WJw8NiiCn4U9ckX8UjhzXPUad2HM0yID0ced8zUHKr-Yhf6p2Z7IS0G07dGG7Tnl5jlwQVog4",
    "auth": "25JGSYqTKvj_nAeodHBHSQ"
  }
}
1
2
3
4
5
6
7
8

我们可以看到推送的网址是 Chrome 提供的服务,Firefox 浏览器下订阅的信息是这样的:

{
  "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABghCrItYXGJw0eCyp0Ae1lTxMXkb6Fxhg8tRck2cMoY4bZjvkV2j5t95FfrPftdieUgeaNthjmb0_XyoIVqWIy7cpy9lMjczHb5TYpC7sKnOw4IekwrtQbmBo6Vn54TZaUSrBIb40PEy2KXF5QlyOj2QxlTz6d6NPB6mMvJxuYNSg-5xs",
  "keys": {
    "auth": "MCeQnXlaz4A-CBuALNPbcQ",
    "p256dh": "BL4am0lzx005spT_UBbMagfWb93Cfgh8XtkCtP7y697dODFnO0wCVVI783BsiHePRTl-mrpoHolJ0gKTYR1T4SQ"
  }
}
1
2
3
4
5
6
7

endpoint 我们可以看出推送服务本身是浏览器提供的,如果无法访问这个网址,自然就无法使用推送服务。

# Server push

接下来,我们让服务端发送一个消息试试:

const webpush = require('web-push');

const vapidPublicKey = 'public key';

const vapidPrivateKey = 'private key';

// PushSubscriptionJSON Info
const pushSubscription = {
  endpoint: '',
  expirationTime: null,
  keys: {
    p256dh: '',
    auth: ''
  }
};

const payload = 'hello world!';

const options = {
  vapidDetails: {
    subject: 'mailto:example@web-push-node.org',
    publicKey: vapidPublicKey,
    privateKey: vapidPrivateKey
  },
  TTL: 60
};
webpush
  .sendNotification(pushSubscription, payload, options)
  .then(function(result) {
    console.log('success!');
    console.log(result);
  })
  .catch(function(err) {
    console.log('fail!');
    console.error(err);
  });
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

在上述代码中填入相应的认证信息,就可以使用了。浏览器的推送功能并不需要把相应的网站打开,只要浏览器是打开的就可以收到推送。

# 总结

浏览器的推送功能非常方便,但是首先需要用户授权通知权限才能使用,其次是浏览器提供的推送服务,可能在中国无法正常使用。所以现在这个功能还没有大规模的应用。

# 参考

Introduction to Push Notifications (opens new window)

Adding Push Notifications to a Web App (opens new window)

Service Worker Push Notifications Demo (opens new window)