在Webpack构建流程中接入http2服务器推送,核心并不是改打包逻辑,而是让构建产物能告诉服务端“当用户访问某个路由时,请顺手把这几个文件推过去”。这套机制依赖一份被称为Push Manifest的映射文件,它记录了路由路径与待推送资源之间的对应关系。很多项目之所以推送不生效,是因为只开了http2却没有生成或读取这份清单。

Push Manifest 是什么以及为什么需要它
http2的服务器推送允许服务端在响应主文档时,主动将客户端可能需要的资源(如CSS、JS、字体)通过同一个连接推送给浏览器,减少往返延迟。但服务端本身并不知道Webpack打包后哪些chunk属于哪个页面,这就需要一种声明式配置文件来描述这种归属关系,它就是Push Manifest。
从原理上看,Push Manifest通常是一个JSON结构,键是请求路径或路由标识,值是该路径下应被推送的资源数组。例如访问首页时推送首页的运行时、vendor和样式文件。没有它,服务端只能盲推或者完全不推;有了它,推送变得精准且可维护。在Webpack生态中,这类清单一般由特定插件在emit阶段扫描资源依赖后写入磁盘。
需要注意的是,Push Manifest只负责“描述”,真正执行推送的是http2服务端(如Node的http2模块、Nginx等)。因此生成清单只是第一步,还必须配合服务端逻辑读取并调用stream.pushStream才能完成推送。如果两端格式约定不一致,就会出现推送失效或推送了错误文件的情况。
使用 webpack 插件生成 Push Manifest
社区中可用http2-push-manifest这类Webpack插件来自动生成清单。它在编译完成、资源即将写入时,根据入口和子资源引用关系输出一个push-manifest.json。下面展示一个基础的Webpack配置示例,演示如何引入并配置该插件。
const Http2PushManifest = require('http2-push-manifest');
module.exports = {
entry: {
home: './src/home.js',
about: './src/about.js'
},
output: {
filename: '[name].[contenthash].js',
path: __dirname + '/dist'
},
plugins: [
new Http2PushManifest({
// 指定输出的清单文件名
fileName: 'push-manifest.json',
// 只推送特定后缀资源,避免推送sourcemap
include: ['**.js', '**.css'],
exclude: ['**.map']
})
]
};
上述配置在构建后会在dist目录生成清单,内容大致形如{"/home":["/home.abc123.js","/home.def456.css"]}。插件通过解析资源图谱,把每个入口对应的同步依赖收集进来。如果你用了代码分割,动态import的chunk默认不会进入推送列表,因为它们不是首屏必需,盲目推送反而浪费带宽。
在多页面项目中,建议按入口名自然映射路由。比如home入口对应/home路径。若你的路由是带参数的(如/user/:id),则需要在服务端做前缀匹配,而不是完全相等匹配。生成阶段也可以通过插件的transform钩子自定义键名,使其贴合实际路由表。
在 Node http2 服务端读取并配置推送
生成清单后,需要在http2服务中加载它,并在收到请求时查找对应资源进行推送。下面给出一个简化的Node服务代码,展示如何读取JSON并调用推送接口。这里使用原生http2模块,避免引入过重框架。
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const manifest = JSON.parse(fs.readFileSync('./dist/push-manifest.json', 'utf8'));
const server = http2.createSecureServer({
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
});
server.on('stream', (stream, headers) => {
const pathname = headers[':path'];
// 查找当前路径对应的推送资源
const pushes = manifest[pathname] || [];
pushes.forEach(function (asset) {
const filePath = path.join(__dirname, 'dist', asset);
const content = fs.readFileSync(filePath);
stream.pushStream({ ':path': asset }, function (err, pushStream) {
if (err) return;
pushStream.respond({ 'content-type': 'application/javascript' });
pushStream.end(content);
});
});
stream.respond({ 'content-type': 'text/html' });
stream.end('<html><body>home page</body></html>');
});
server.listen(8443);
这段代码在收到流时先查清单,再为每个资源开一个pushStream。需要特别留意内容类型的设置:JS应标application/javascript,CSS为text/css,否则浏览器可能拒绝执行。推送的文件内容必须真实存在且未被 gzip 二次封装,因为http2层有自身压缩,服务端推送原始字节即可。
另一个常见坑是缓存对齐。如果浏览器已经缓存了某资源,服务端仍推送就会造成浪费,甚至触发浏览器取消推送流。高级做法是在请求头中检查if-none-match或利用RFC中定义的缓存摘要帧,但多数中小型项目可简单通过只推送首屏关键小体积文件来规避。同时,Nginx等反向代理若开启http2,也需要配置http2_push_preload并配合Link头,此时Webpack清单可转为Link头输出,而非上面Node直推方式。
多页面与路由拆分的进阶处理
当应用包含数十个页面时,单一清单文件会变得庞大,且服务端每次请求都要遍历匹配。更好的方案是按路由拆分清单,或在清单中使用通配前缀。比如将/admin/*统一指向后台公共chunk,避免每个子页都写一遍。
{
"/home": ["/home.a1.js", "/base.css"],
"/admin/*": ["/admin-common.b2.js", "/admin.css"],
"/about": ["/about.c3.js"]
}
服务端匹配时可用Object.keys(manifest).find(k => k.endsWith('*') && pathname.startsWith(k.slice(0,-1)))来捕获通配项。这样新增后台页面无需改清单,只要静态资源命名规律一致即可。对于使用HTML模板插件(如html-webpack-plugin)的项目,还可让插件把清单内联进HTML的Link rel=preload头,作为推送的降级方案,保证不支持推送的环境也能预加载。
最后要强调,推送不是越多越好。Webpack生成的清单若包含了过大图片或字体,推送会阻塞关键渲染。应通过插件include严格限制类型,并结合真实网络环境用Chrome的http2水族馆面板观察推送流是否被立即使用。只有被立即使用的推送才真正起到优化作用,其余都应从清单移除。
webpackhttp2_server_pushpush_manifest修改时间:2026-08-14 21:12:35