Skip to content
On this page

服务器推送功能解决了什么痛点?

客户端发送请求给服务器,服务器返回请求的资源,通常是 HTML 文件,HTML 文件包含一些资源链接(比如.js, .css 等)。浏览器解析 HTML 文件,获取资源链接,然后分别请求这些资源。

这一机制的问题在于,它迫使用户等待这样一个过程:直到一个 HTML 文档下载完毕后,浏览器才能发现和获取页面的关键资源。从而延缓了页面渲染,拉长了页面加载时间

有了 Server Push,就有了解决上述问题的方案。Server Push 能让服务器在用户没有明确询问下,抢先地“推送”一些网站资源给客户端。只要正确地使用,我们可以根据用户正在访问的页面,给用户发送一些即将被使用的资源。

代码实操

使用内置的 http2 模块,我们可以创建一个 http2 服务器。有趣的一点在于,当 index.html 被请求时,我们会主动推送其他资源:bundle1.jsbundle2.js。这样的话,bundle1.jsbundle2.js 可以在浏览器请求它们之前就推送过去了。

目录

bash
├── package.json
├── public
│   ├── bundle1.js
│   ├── bundle2.js
│   └── index.html
├── src
│   ├── helper.js
│   └── server.js
└── ssl
    ├── cert.pem
    └── key.pem

index.html

html
<html>
  <body>
    <h1>HTTP2 Push!</h1>
  </body>
  <script src="bundle1.js"></script>
  <script src="bundle2.js"></script>
</html>

server.js 主要是 pushStream

js
const http2 = require('http2');
const server = http2.createSecureServer({ cert, key }, requestHandler);

function push(stream, filePath) {
  const { file, headers } = getFile(filePath);
  const pushHeaders = { [HTTP2_HEADER_PATH]: filePath };

  stream.pushStream(pushHeaders, (pushStream) => {
    pushStream.respondWithFD(file, headers);
  });
}

function requestHandler(req, res) {
  // Push files with index.html
  if (reqPath === '/index.html') {
    push(res.stream, 'bundle1.js');
    push(res.stream, 'bundle2.js');
  }

  // Serve file
  res.stream.respondWithFD(file.fileDescriptor, file.headers);
}
src

server.js

js
const fs = require('fs');
const path = require('path');
const http2 = require('http2');
const helper = require('./helper');

const { HTTP2_HEADER_PATH } = http2.constants;
const PORT = process.env.PORT || 3000;
const PUBLIC_PATH = path.join(__dirname, '../public');

const publicFiles = helper.getFiles(PUBLIC_PATH);
const server = http2.createSecureServer(
  {
    cert: fs.readFileSync(path.join(__dirname, '../ssl/cert.pem')),
    key: fs.readFileSync(path.join(__dirname, '../ssl/key.pem')),
  },
  requestHandler
);

// Push file
function push(stream, path) {
  const file = publicFiles.get(path);

  if (!file) {
    return;
  }

  stream.pushStream({ [HTTP2_HEADER_PATH]: path }, (pushStream) => {
    pushStream.respondWithFD(file.fileDescriptor, file.headers);
  });
}

// Request handler
function requestHandler(req, res) {
  const reqPath = req.url === '/' ? '/index.html' : req.url;
  const file = publicFiles.get(reqPath);

  // File not found
  if (!file) {
    res.statusCode = 404;
    res.end();
    return;
  }

  // Push with index.html
  if (reqPath === '/index.html') {
    push(res.stream, '/bundle1.js');
    push(res.stream, '/bundle2.js');
  }

  // Serve file
  res.stream.respondWithFD(file.fileDescriptor, file.headers);
}

server.listen(PORT, (err) => {
  console.log(`Server listening on ${PORT}`);
});

helper

js
const fs = require('fs');
const path = require('path');
const mime = require('mime');

function getFiles(baseDir) {
  const files = new Map();

  fs.readdirSync(baseDir).forEach((fileName) => {
    const filePath = path.join(baseDir, fileName);
    const fileDescriptor = fs.openSync(filePath, 'r');
    const stat = fs.fstatSync(fileDescriptor);
    const contentType = mime.lookup(filePath);

    files.set(`/${fileName}`, {
      fileDescriptor,
      headers: {
        'content-length': stat.size,
        'last-modified': stat.mtime.toUTCString(),
        'content-type': contentType,
      },
    });
  });

  return files;
}

module.exports = { getFiles };
public

bandle1

js
console.log('Bundle 1');
document.body.innerHTML += '<p>Bundle 1 loaded</p>';

bandle2

js
console.log('Bundle 2');
document.body.innerHTML += '<p>Bundle 2 loaded</p>';
ssl

cert.pem

js
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIJAN9jdYICeY33MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTcwODE2MTMyNDA2WhcNMjcwODE0MTMyNDA2WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA4NGiIEgDf3NdOlpGAROY8ZNPJrhR6gNPTAIZKz314wDUJTF54a8ioeUn
rsKL0Z5H67MmZ5LcsZO9jprrBaSFAIyFdiqQKOSWkXA+LZCEhqaCR0cOIOy0TEKb
MxsABW+6hSoBXk1tK3IQntEcs22qQZll+Y++jUBck+XaCwAmpsk4ofqfLPHcvidj
gHOYkKQg/fFIm81M1NKgdbQpW2/akob56kdh4scZrpwa6MxEylKppTjZ0jfF0cnv
rrs4QTWvoIowfAMrrd/TJV5P8Ei5KcckJe8shlgoEOxtRrAjhIEUmZOT0yho7BcA
C+Zudul0XJhcAPjbHirQ0+G4Agy3FwIDAQABo4GnMIGkMB0GA1UdDgQWBBQqMeql
vTY7xAXbu5YXhnD4lBK1xjB1BgNVHSMEbjBsgBQqMeqlvTY7xAXbu5YXhnD4lBK1
xqFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAN9jdYICeY33MAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEm90w4C0rFqxVVVojAYXrcvLD0E5iwA
3eEn+uD1dMy56he0LZNXlf3s3mZlRYE8+oGvydcMuXrmisVcvIuFQlES2M6y8S2b
CW6xeir4+VKWR97c3p+M48rUfV8YGb2d6YBLNlekAT/S55Bhfy15sYWt+9LIi4i+
ToqywlIfiMcwpBZLD1UTPs7b66Rx7LHmMFXpNfDugp8JHphLJRmXFNgZNNmi0Re0
vDMgUWlgVzV9oMTjp0Ioafbtqcykg0JS+3KsXHWVQPWrw1wsBjje4XJLIIY1+W75
tHBk3PL42xP5vPgRJ/8X22pFXnC/0MK6fKg5VdvwJQDBlYxbpcM3W+g=
-----END CERTIFICATE-----

key.pem

js
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4NGiIEgDf3NdOlpGAROY8ZNPJrhR6gNPTAIZKz314wDUJTF5
4a8ioeUnrsKL0Z5H67MmZ5LcsZO9jprrBaSFAIyFdiqQKOSWkXA+LZCEhqaCR0cO
IOy0TEKbMxsABW+6hSoBXk1tK3IQntEcs22qQZll+Y++jUBck+XaCwAmpsk4ofqf
LPHcvidjgHOYkKQg/fFIm81M1NKgdbQpW2/akob56kdh4scZrpwa6MxEylKppTjZ
0jfF0cnvrrs4QTWvoIowfAMrrd/TJV5P8Ei5KcckJe8shlgoEOxtRrAjhIEUmZOT
0yho7BcAC+Zudul0XJhcAPjbHirQ0+G4Agy3FwIDAQABAoIBAHHi4B04PcVffHel
6VZ8RfsCY5M6xgwklxPq8DMOlTPkZJNex95CqOmYOwz1cnzCkK5et3K6W9/89oZ6
Bdp66AFKLgWZNCPzAC82y9irH+dSDCbtYMPfBMqo5xPxdoZKfhMdH0pVMJtUkgTR
65cdU6UdfyH35lCJrRwi0NzHu8y6qMyH5tOcNOIXwn/LfZ6ZwVx2lKAjthmac2z0
35qknvCNfgadrsl5PLWZBYwy3qx208pUUojMciL9RtfO17F6ofMhueOoChnaHv6h
Lra3NpcuJlYdTTt3tgkTvfMXiZCMsWoV9q1+y9AcSojDJk4V6qiy0Nu8uQmSCRX8
g/2ilqECgYEA/9yejCxaUfgHlA6LkFFMNNkgh3AwHxfIiEBzCDxkUPlu0J1z+65S
UJLGLIgQoZN0UmN6a0YHHDxh0oxIvxoq+wc4PPIr9mmSsEt6YTUjYbYNIJ/5978b
CH+41VGCszqzd0NSJTW7aKL3XFE34kDascgTiDd82KmqaC5chShCoZECgYEA4PC4
qoNnAzILI9yM9hJcolrW9hf4pUXqG9orO7pr5/ETDVi74pNdKkb7MKE1snjTQBdu
rV5q09wu9+Owa5AoHLkvTify+EXlUrc4UeVxEdlOfoc/laONZkGaENxn4DTH4rMj
6nlzMNPcR0Hxud/UqLpWiI20I+dJwVzBA3z3eicCgYEAqNb+LQPLqlGhNpuOj3KG
dk1dwOJQbwQzyW22OxYXILQo4zMz6T50hUUFzzcOuoDifse0bfutD33tE5KNIsZy
3Go8O0OXrSinqvxzypfVPFJ1QTUwL8OFZEtcPjBmrj0rVqUvHOzjOb5ouxvBY+Vm
K3EbKoVrNlJn6A3H8frKVXECgYEAsnQnfRdcbTuRfPTnW/07Qo6gxYJFABGUZl5S
OENwggVOoSMJg/p3Sigf9fefWyTiK5Gre51RURz4oi8f8mXefNMpxW6KIw+InHPB
Ga/WYVuuG1F/T17+ueZHrSK+wi/9eEu4rbeGfHFH67xUYqtB0k5qglExXd6LM/07
H2JQD7cCgYAeU4FtW0VZdEtUkBLJhHHZ0l/UBtN/XhkpFjdrG9uJryX/2rnA8Zoh
vKhSG5iWdTDak4R2dt9yJrTr1sriI5P176t76eAR4jg0F3xeRpiaDDk7wdznwh9F
OKOJlLEHd9G2gryXAtUU4KPii3/Awgj/Kd37j7fssuMTQLB9bWIygQ==
-----END RSA PRIVATE KEY-----

Node.js 8.4.0 已经开始支持 HTTP/2,执行 node 命令时,加上 --expose-http2 选项就可以使用了。

Released under the MIT License.