基于webpack来配置html的preload和prefetch

起因

最近在做一个手机站点,服务器用的Google Cloud,但是用户多是国内的。所以在提升页面加载和响应速度这块,是很重视的。

一开始是想使用manifest.json来做Application Cache, 不过后面发现Google和FireFox都有提到打算废弃这个标准并建议大家改用Service Worker。

可是我要做到站点是文章居多的偏新闻性质的,而且交互也不多,用Service Worker来做PWA,感觉有点大材小用。所以想找一些更好实践,而且对于css和js资源的缓存和快速加载有意义的方案。

prefetch和preload就出现在眼前了。

关于prefetch和preload

具体的技术介绍,这里就不详述,大家可以看看MDN的这篇文章

这里说一下preload和prefetch的区别:

preload是通知浏览器尽快的去加载当前页面现在或者将来需要的一些资源文件(css,js,image,viedo,audio,fonts等等),这个加载跟页面解析是并行的,不会阻塞页面本身的加载。

prefetch是一种预测,预测访问当前页面A的用户很可能会访问页面B,所以在页面A提前告知浏览器去加载页面B的资源。这个提前加载的优先级最低,不会干扰页面的正常资源的加载解析,也不会干扰preload。

兼容性

preload在pc上,chrome和firefox支持比较好, eage是还在开发状态,safari的最新版本也已经支持了。在手机端的支持就不是很好,Andriod 需要5之后的webview才支持,国内的UC,QQ和Baidu都不支持;iOS是最新的11.3开始支持

具体参看:caniuse preload

prefetch的兼容性,相对好点,除了MacOS和iOS上的safari不支持之外,其他浏览器都支持了。

具体参看:caniuse prefetch

另外,preload和prefetch都是申明性质的,所以就算不支持,也不会影响现有页面的任何功能

关于站点的webpack配置

为了方便js和css资源的缓存,在nginx上是配置了很长的过期时间,所以对应的,css和js的编译后的文件名都做了hash后缀。这样,线上的更新,都是文件的替换而不是刷新缓存。

为了方便的实现html里面css和js资源文件能在编译后自动更新路径,使用了HtmlWebpackPlugin来实现html的编译和输出

而在HtmlWebpackPlugin本身也有插件机制,其中一个由GoogleChromeLabs开发的插件PreloadWebpackPlugin,就能方便的给编译的html文件添加相关资源的preload和pretch

具体实现(针对多页面多Entry情况)

如果你是一个单页面应用,那么一切都很简单,就不多说了。这里主要说一下在处理多页面的输出时的注意点

首先,说一下我的几个module的版本:

"webpack": "^4.10.2",
"html-webpack-plugin": "^3.2.0",
"preload-webpack-plugin": "^3.0.0-alpha.3",

PreloadWebpackPlugin,我一开始用的是正式版的v2.3.0,可是我的webpack是4.x版本,有点不兼容。按照官方issues上的解决方案,改为安装了preload-webpack-plugin@latest,然后才可以的。

HtmlWebpackPlugin多页面配置

首先,你需要配置好HtmlWebpackPlugin,不同的页面使用不同的template,编译到不同的filename,同时还要指定哪些chunks需要加载到页面里面。

我的站点是目前包括四个页面index.html, book.html, content.html和search.html,对应配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),

完成这一步之后,编译,dist目录应该有对应的四个文件,里面的资源也能正确加载。接下来就是配置preload了

preload配置

PreloadWebpackPlugin这个插件更适用于单页面应用,对于多页面应用,在配置上比较繁琐一点。

首先,需要使用excludeHtmlNames来反向排除所有页面,只关联你要配置的那个页面,

再然后,使用include选项来明确指定哪些资源要被preload

最后,每一个页面对应添加一个PreloadWebpackPlugin的实例

还有一点需要补充说明一下,在webpack配置的plugins里面,PreloadWebpackPlugin的配置需要在HtmlWebpackPlugin之后,不然PreloadWebpackPlugin找不到HtmlWebpackPlugin提供的hooks,会报错。

完成上面这一步之后的配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['index']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'content.html'],
      include: ['search']
    }),

可以看到,在excludeHtmlNames的配置上比较繁琐,如果页面多的话,最好写个函数来实现过滤。

prefetch的配置

一个PreloadWebpackPlugin实例同时只能干一件事情,根据rel字段区分,要么是preload,要么是prefetch。所以,还得新增PreloadWebpackPlugin的实例。

不过不着急,先想好如何配置prefetch。在我的这个站点里面,在index页面,大部分的链接都是指向的book页面,所以可以在index页面prefetch book页面的资源。同样的还有,在book页面prefetch content页面的资源。

定好之后,就是配置了,跟preload的配置比较类似,需要区分的就是rel和include这两个选项

rel需要改成prefetch,这不用多说。include,这个时候需要改成你打算prefetch的页面的资源,而不是当前页面的资源。

最终的配置如下:

    new HtmlWebpackPlugin({
      title: 'index',
      template: './src/pages/index/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'book',
      template: './src/pages/book/book.html',
      filename: 'book.html',
      chunks: ['book']
    }),
    new HtmlWebpackPlugin({
      title: 'content',
      template: './src/pages/content/content.html',
      filename: 'content.html',
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      title: 'search',
      template: './src/pages/search/search.html',
      filename: 'search.html',
      chunks: ['search']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['index']
    }),
    new PreloadWebpackPlugin({
      rel: 'prefetch',
      excludeHtmlNames: ['book.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['book']
    }),
    new PreloadWebpackPlugin({
      rel: 'prefetch',
      excludeHtmlNames: ['index.html', 'content.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'search.html'],
      include: ['content']
    }),
    new PreloadWebpackPlugin({
      rel: 'preload',
      excludeHtmlNames: ['index.html', 'book.html', 'content.html'],
      include: ['search']
    }),

以上,就能顺利的实现,在webpack编译过程中,在html页面里面自动配置上preload和prefetch。希望能帮到大家

发表评论

This site uses Akismet to reduce spam. Learn how your comment data is processed.