虽然HTTP/2目前已经逐渐的在各大网站上开始了使用,但是在目前最新的Node.js上仍然处于实验性API,还没有能有效解决生产环境各种问题的应用示例。因此在应用HTTP/2的道路上我自己也遇到了许多坑,下面介绍了项目的主要架构与开发中遇到的问题及解决方式,也许会对你有一点点启示。
配置
虽然W3C的规范中没有规定HTTP/2协议一定要使用ssl加密,但是支持非加密的HTTP/2协议的浏览器实在少的可怜,因此我们有必要申请一个自己的域名和一个ssl证书。
本项目的测试域名是 you.keyin.me ,首先我们去域名提供商那把测试服务器的地址绑定到这个域名上。然后使用Let's Encrypt生成一个免费的SSL证书:
sudo certbot certonly --standalone -d you.keyin.me
输入必要信息并通过验证之后就可以在 /etc/letsencrypt/live/you.keyin.me/ 下面找到生成的证书了。
改造Koa
Koa是一个非常简洁高效的Node.js服务器框架,我们可以简单改造一下来让它支持HTTP/2协议:
class KoaOnHttps extends Koa { constructor() { super(); } get options() { return { key: fs.readFileSync(require.resolve('/etc/letsencrypt/live/you.keyin.me/privkey.pem')), cert: fs.readFileSync(require.resolve('/etc/letsencrypt/live/you.keyin.me/fullchain.pem')) }; } listen(...args) { const server = http2.createSecureServer(this.options, this.callback()); return server.listen(...args); } redirect(...args) { const server = http.createServer(this.callback()); return server.listen(...args); } } const app = new KoaOnHttps(); app.use(sslify()); //... app.listen(443, () => { logger.ok('app start at:', `https://you.keyin.cn`); }); // receive all the http request, redirect them to https app.redirect(80, () => { logger.ok('http redirect server start at', ``); });
上述代码简单基于Koa生成了一个HTTP/2服务器,并同时监听80端口,通过sslify中间件的帮助自动将http协议的连接重定向到https协议。
静态文件中间件
静态文件中间件主要用来返回url所指向的本地静态资源。在http/2服务器中我们可以在访问html资源的时候通过服务器推送(Server push)将该页面所依赖的js\css\font等资源一起推送回去。具体代码如下:
const send = require('koa-send'); const logger = require('../util/logger'); const { push, acceptsHtml } = require('../util/helper'); const depTree = require('../util/depTree'); module.exports = (root = '') => { return async function serve(ctx, next) { let done = false; if (ctx.method === 'HEAD' || ctx.method === 'GET') { try { // 当希望收到html时,推送额外资源。 if (/(\.html|\/[\w-]*)$/.test(ctx.path)) { depTree.currentKey = ctx.path; const encoding = ctx.acceptsEncodings('gzip', 'deflate', 'identity'); // server push for (const file of depTree.getDep()) { // server push must before response! // https://huangxuan.me/2017/07/12/upgrading-eleme-to-pwa/#fast-skeleton-painting-with-settimeout-hack push(ctx.res.stream, file, encoding); } } done = await send(ctx, ctx.path, { root }); } catch (err) { if (err.status !== 404) { logger.error(err); throw err; } } } if (!done) { await next(); } }; };
需要注意的是,推送的发生永远要先于当前页面的返回。否则服务器推送与客户端请求可能就会出现竞争的情况,降低传输效率。
依赖记录
从静态文件中间件代码中我们可以看到,服务器推送资源取自depTree这个对象,它是一个依赖记录工具,记录当前页面 depTree.currentKey 所有依赖的静态资源(js,css,img...)路径。具体的实现是:
const logger = require('./logger'); const db = new Map(); let currentKey = 'https://www.jb51.net/'; module.exports = { get currentKey() { return currentKey; }, set currentKey(key = '') { currentKey = this.stripDot(key); }, stripDot(str) { if (!str) return ''; return str.replace(/index\.html$/, '').replace(/\./g, '-'); }, addDep(filePath, url, key = this.currentKey) { if (!key) return; key = this.stripDot(key); if(!db.has(key)){ db.set(key,new Map()); } const keyDb = db.get(key); if (keyDb.size >= 10) { logger.warning('Push resource limit exceeded'); return; } keyDb.set(filePath, url); }, getDep(key = this.currentKey) { key = this.stripDot(key); const keyDb = db.get(key); if(keyDb == undefined) return []; const ret = []; for(const [filePath,url] of keyDb.entries()){ ret.push({filePath,url}); } return ret; } };
当设置好特定的当前页 currentKey 后,调用 addDep 将方法能够为当前页面添加依赖,调用 getDep 方法能够取出当前页面的所有依赖。 addDep 方法需要写在路由中间件中,监控所有需要推送的静态文件请求得出依赖路径并记录下来: