webpack多页运用架构系列(十六):善用浏览器缓存,该去则去,该留则留

本文首发于
Array_Huang的手艺博客——
有用至上,非经作者赞同,请勿转载。

原文地点:
https://segmentfault.com/a/1190000010317802

假如您对本系列文章感兴趣,迎接关注定阅这里:
https://segmentfault.com/blog/array_huang

媒介

一个成熟的项目,天然离不开迭代更新;那末在布置前端这一块,我们免不了老是要顾及到浏览器缓存的,本文将引见如安在 webpack (架构)的协助下,妥善处置惩罚好浏览器缓存。

现实上,我很早以前就想写这一part了,只是苦于当时我所掌握的计划不如人意,便不敢献丑了;而自从
webpack 晋级到 v2 版本后,以及第三方plugin的日趋雄厚,我们也有了更多的手腕来处置惩罚cache。

浏览器缓存简朴引见

下面来简朴引见一下浏览器缓存,以及为什么我要在标题中强调“该去则去,该留则留”。

浏览器缓存是啥?

浏览器缓存(Browser Cache),是浏览器为了节约网络带宽、加速网站接见速率而推出的一项功用。浏览器缓存的运转机制是如许的:

  1. 用户运用浏览器第一次接见某网站页面,该页面上引入了林林总总的静态资本(js/css/图片/字体……),浏览器会把这些静态资本,以至是页面自身(html文件),都逐一贮存到当地。
  2. 用户在后续的接见中,假如须要再次要求一样的静态资本(依据 url 举行婚配),且静态资本没有逾期(服务器端有一系列鉴别资本是不是逾期的战略,比方Cache-ControlPragmaETagExpiresLast-Modified),则直接运用前面当地贮存的资本,而不须要反复要求。

由于webpack只担任构建天生网站前端的静态资本,不触及服务器,因而本文不议论以HTTP Header为基本的缓存掌握战略;那我们议论什么呢?

很简朴,由于浏览器是依据静态资本的url来推断该静态资本是不是已有缓存,而静态资本的文件目次又是相对牢固的,那末重点显著就在于静态资本的文件名了;我们就经由过程操控静态资本的文件名,来决议静态资本的“去留”。

浏览器缓存,该留不留会怎样?

每次布置上线新版本,静态资本的文件名如有变化,则浏览器推断是第一次读取这个静态资本;那末,即使这个静态资本的内容跟上一版的完整一致,浏览器也要从新下载这个静态资本,糟蹋网络带宽、拖慢页面加载速率。

浏览器缓存,该去不去会怎样?

每次布置上线新版本,静态资本的文件名若没有变化,则浏览器推断可加载之前缓存下来的静态资本;那末,即使这个静态资本的内容跟上一版的有所变化,浏览器也发觉不到,运用了老版本的静态资本。那这会构成什么样的影响呢?小大由之,小至用户看到的依旧是老版的资本,达不到上线更新版本的目标;大至构成网站运转报错、规划错位等题目。

怎样经由过程操控静态资本的文件名到达掌握浏览器缓存的目标呢?

在webpack关于文件名定名的设置中,存在一系列的变量(也许明白成定名划定规矩也可),经由过程这些变量,我们能够依据所要天生的文件的详细情况来举行定名,而没必要预设好一个牢固的称号。在缓存处置惩罚这一块,我们主要用到[hash][chunkhash]这两个变量。关于这两个变量的引见,我在之前的文章 —— 《webpack设置经常使用部份有哪些?》就已诠释过是什么意义了,这里就不再累述。

这里总结下[hash][chunkhash]这两个变量的用法:

  • [hash]的话,由于每次运用 webpack 构建代码的时刻,此 hash 字符串都邑更新,因而相当于强迫革新浏览器缓存
  • [chunkhash]的话,则会依据详细 chunk 的内容来构成一个 hash 字符串来插进去到文件名上;换句说, chunk 的内容稳定,该 chunk 所对应天生出来的文件的文件名也不会变,由此,浏览器缓存便能得以继承运用

有哪些资本是须要统筹浏览器缓存的?

理论上来讲,除了HTML文件外(HTML文件的途径须要坚持相对牢固,只能从服务器端入手),webpack天生的一切文件都须要处置惩罚好浏览器缓存的题目。

js

在 webpack 架构下,js文件也有差别范例,因而也须要差别的设置:

  1. 进口文件(Entry):在webpack设置中的output.filename参数中,让天生的文件名中带上[chunkhash]即可。
  2. 异步加载的chunk:output.chunkFilename参数,操纵同上。
  3. 经由过程CommonsChunkPlugin天生的文件:在CommonsChunkPlugin的设置参数中有filename这一项,操纵同上。但须要注重的是,假如你运用[chunkhash]的话,webpack 构建的时刻但是会报错的哦;那可咋办呢,用[hash]的话,这common chunk不就每次上线新版本都强迫革新了吗?这实在是由于,webpack 的 runtime && manifest 会一致保存在你的common chunk里,处理的要领,就请看下面关于“webpack 的 runtime && manifest”的部份了。

css

关于css来讲,假如你是用style-loader直接把css内联到<head>里的,那末,你管好引入该css的js文件的浏览器缓存就好了。

而假如你是运用extract-text-webpack-plugin把css自力打包成css文件的,那末在文件名的设置上,一样加上[chunkhash]即可加上[contenthash]即可(谢谢@FLYiNg_hbt 提示)。这个[contenthash]是什么东西呢?实在就是extract-text-webpack-plugin为了与[chunkhash]区离开,而自定义的一个定名划定规矩,实在际寄义跟[chunkhash]能够说是一致的,只是[chunkhash]已被占用作为 chunk 的内容 hash 字符串了,继承用[chunkhash]会构成下述题目

图片、字体文件等静态资本

《据说webpack连图片和字体也能打包?》里引见的,处置惩罚这类静态资本平常运用url-loaderfile-loader

关于url-loader来讲,就不须要体贴浏览器缓存了,由于它是把静态资本转化成 dataurl 了,而并不是自力的文件。

而关于file-loader来讲,一样是在文件名的设置上加上[chunkhash]即可。别的须要注重的是,url-loader平常搭配有降级到file-loader的设置(运用loader加载的文件大于一个你设定的值就降级到运用file-loader来加载),一样须要在文件名的设置上加上[chunkhash]

webpack 的runtime && manifest

所谓的runtime,就是协助 webpack 编译构建后的打包文件在浏览器运转的一些辅佐代码段,换句话说,打包后的文件,除了你本身的源码和npm库外,另有 webpack 供应的一点辅佐代码段。

而 manifest,则是 webpack 用以查找 chunk 实在途径所运用的一份关联表,简朴来讲,就是 chunk 名对应 chunk 途径的关联表。manifest 平常来讲会被藏到 runtime 里,因而我们检察 runtime 的时刻,虽然能找获得 manifest,但平常都不那末直观,形如下面这一段(仅common chunk部份):

u.type = "text/javascript", u.charset = "utf-8", u.async = !0, u.timeout = 12e4, n.nc && u.setAttribute("nonce", n.nc), u.src = n.p + "" + e + "." + {
    0: "e6d1dff43f64d01297d3",
    1: "7ad996b8cbd7556a3e56",
    2: "c55991cf244b3d833c32",
    3: "ecbcdaa771c68c97ac38",
    4: "6565e12e7bad74df24c3",
    5: "9f2774b4601839780fc6"
}[e] + ".bundle.js";

runtime && manifest被打包到那里去了?

那末,这runtime && manifest的代码段,会被放到那里呢?平常来讲,假如没有运用CommonsChunkPlugin天生common chunkruntime && manifest会被放在以进口文件为首的chunk(俗称“大包”)里,假如是我们这类多页(又称多进口)运用,则会每一个大包一份runtime && manifest;这夸大的冗余我们天然是不能忍的,那末
用上CommonsChunkPlugin后,runtime && manifest就会一致迁到common chunk了。

runtime && manifestcommon chunk带来的缓存危急

虽然说把runtime && manifest迁到common chunk后,代码冗余的题目算是处理了,但却构成另一题目:由于我们在上述的静态资本的文件名定名上都采用了[chunkhash]的计划,因而也使得只需我们稍一修正源代码,就会有最少一个 chunk 的定名会发生变化,这就会致使我们的runtime && manifest也发生变化,从而致使我们的common chunk也发生变化,这也许就是 webpack 划定含有runtime && manifestcommon chunk不能运用[chunkhash]的缘由吧(横竖chunkhash肯定会变的,还不如不必呢是不是是)。

要处理上述题目(这题目很严峻啊我摔,common chunk怎样能用不上缓存啊,这但是最大的chunk啊),我们就须要把runtime && manifest给自力出去。要领也很简朴,在用来打包common chunkCommonsChunkPlugin后,再加一CommonsChunkPlugin

  /* 抽掏出一切通用的部份 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'commons/commons',      // 须要注重的是,chunk的name不能雷同!!!
    filename: '[name]/bundle.[chunkhash].js', // 由于runtime自力出去了,这里便能够运用[chunkhash]了
    minChunks: 4,
  }),
  /* 抽掏出webpack的runtime代码,防止轻微修正一下进口文件就会修正commonChunk,致使底本有用的浏览器缓存失效 */
  new webpack.optimize.CommonsChunkPlugin({
    name: 'webpack-runtime',
    filename: 'commons/commons/webpack-runtime.[hash].js', // 注重runtime只能用[hash]
  }),

如许一来,runtime && manifest代码段就会被打包到这个名为webpack-runtime的 chunk 里了。这是什么道理呢?据说是在运用CommonsChunkPlugin的情况下, webpack 会把runtime && manifest打包到末了面的一个CommonsChunkPlugin天生的 chunk 里,而假如这个chunk没有别的代码,那末天然就到达了把runtime && manifest自力出去的目标了。

须要注重的是,假如你用了html-webpack-plugin来天生html页面,记得要把这runtime && manifest的 chunk 插进去到html页面上,不然页面报错了可不怪我哦。

至此,由于runtime && manifest自力出去成一个chunk了,因而common chunk的定名便能够运用[chunkhash]了,也就是说,common chunk如今也能做到大众模块内容有更新了,才更新文件名;另一方面,这个自力出去的 runtime && manifest chunk,是每次 webpack 打包构建的时刻都邑更新了。

有必要把 manifest 从 runtime && manifest chunk 中自力出去吗?

是的,不必惊奇,确实是有这么一个骚操纵。

把 manifest 自力出去的理由是如许的:manifest 自力出去后,runtime 的部份基本上就不会有更改了;到这里,我们就晓得,runtime && manifest里现实上就是 manifest 在变;因而把 manifest 自力出去,也是进一步地运用浏览器缓存(能够把 runtime 的缓存保存下来)。

详细是怎样做的呢?主流有俩计划:

我试用过第二种计划,好使,但终究照样摒弃了,为什么呢?

把 manifest 自力出去后,只剩下 runtime 的 chunk 的定名照样只能用[hash],而不能运用[chunkhash],这就致使我们基础没法运用浏览器缓存。厥后,我又想出一个折中的方法,连[hash]也不要了,直接写死一个文件名;如许的话,确实浏览器缓存就可以保存下来了。但厥后我照样反转了本身,这类要领虽然能留下浏览器缓存,却做不到“该去则去”。也许人人会有疑问,你不是说 runtime 不会变的吗,那留下缓存有什么关联呀?是的,在统一 webpack 环境下 runtime 确实不会变,但难保 webpack 环境转变后,这runtime会怎样呀。比方说 webpack 的版本晋级了、 webpack 的设置改了、loader & plugin 的版本晋级了,在这些情况下,谁敢保证 runtime 永久不会变啊?这 runtime 一用错了逾期的缓存,那极能够全部体系都邑崩溃的啊,这个险我实在是冒不起,所以只能作罢。

不过我看了下Array-Huang/webpack-seedruntime && manifest chunk,也才 2kb 罢了嘛,你们管好本身的强迫症和代码洁癖好吗?!

缓存题目杂项

模块id带来的缓存题目

webpack 处置惩罚模块(module)间依靠关联时,须要给各个模块定一个 id 以作标识。webpack 默许的 id 定名划定规矩是依据模块引入的递次,给予一个整数(1、2、3……)。当你在源码中恣意增加或删减一个模块的依靠,都邑对全部
id 序列构成极大的影响,可谓是“牵一发而动全身”了。那末这对我们的浏览器缓存会有什么样直接的影响呢?影响就是会构成,各个chunk中都不一定有本质的变化,但援用的依靠模块id却都变了,这显著就会构成 chunk 的文件名的更改,从而影响浏览器缓存。

webpack 官方文档里引荐我们运用一个已内置进 webpack2 里的 plugin:HashedModuleIdsPlugin,这个 plugin 的官方文档在这里

webpack1 时期便有一个NamedModulesPlugin,它的道理是直接运用模块的相对途径作为模块的 id,如许只需模块的相对途径,模块 id 也就不会变了。那末这个HashedModuleIdsPlugin对比起NamedModulesPlugin来讲又有什么提高呢?

是如许的,由于模块的相对途径有能够会很长,那末就会占用大批的空间,这一点是一向为社区所诟病的;但这个HashedModuleIdsPlugin是依据模块的相对途径天生(默许运用md5算法)一个长度可设置(默许截取4位)的字符串作为模块的 id,那末它占用的空间就很小了,人人也就可以够放心服用了。

To generate identifiers that are preserved over builds, webpack supplies the NamedModulesPlugin (recommended for development) and HashedModuleIdsPlugin (recommended for production).

从上可知,官方是引荐开辟环境用NamedModulesPlugin,而临盆环境用HashedModuleIdsPlugin的,缘由似乎是与热更新(hmr)有关;不过就我看来,仅在临盆环境用HashedModuleIdsPlugin就好了,开辟环境还管啥浏览器缓存啊,俺开 chrome dev-tool 设置了不必任何浏览器缓存的。

用法也挺简朴的,直接加到plugin参数就成了:

plugins: {
  // 别的plugin
  new webpack.HashedModuleIdsPlugin(),  
}

由某些 plugin 构成的文件修正监测失利

有些 plugin 会天生自力的 chunk 文件,比方CommonsChunkPluginExtractTextPlugin(从js中提掏出css代码段并天生自力的css文件) 。

这些 plugin 在天生 chunk 的文件名时,能够没料想到后续还会有别的 plugin (比方用来殽杂代码的UglifyJsPlugin)会对代码举行修正,因而,由此天生的 chunk 文件名,并不能完整反应文件内容的变化。

别的,ExtractTextPlugin有个比较严峻的题目,那就是它天生文件名所用的[chunkhash]是直接取自于援用该css代码段的 js chunk ;换句话说,假如我只是修正 css 代码段,而不动 js 代码,那末末了天生出来的css文件名依旧没有变化,这可算是异常严峻的浏览器缓存“该去不去”题目了。
2017-07-26 修正:改用[contenthash]便不会涌现此题目,上见css部份

有一款 plugin 能处理以上题目:webpack-plugin-hash-output

There are other webpack plugins for hashing out there. But when they run, they don’t “see” the final form of the code, because they run before plugins like webpack.optimize.UglifyJsPlugin. In other words, if you change webpack.optimize.UglifyJsPlugin config, your hashes won’t change, creating potential conflicts with cached resources.

The main difference is that webpack-plugin-hash-output runs in the last compilation step. So any change in webpack or any other plugin that actually changes the output, will be “seen” by this plugin, and therefore that change will be reflected in the hash.

简朴来讲,就是这个webpack-plugin-hash-output会在 webpack 编译的末了阶段,从新对一切的文件取文件内容的 md5 值,这就保证了文件内容的变化一定会反应在文件名上了。

用法也比较简朴:

plugins: {
  // 别的plugin
  new HashOutput({
    manifestFiles: 'webpack-runtime', // 指定包括 manifest 在内的 chunk
  }),
}

总结

浏览器缓存很主要,很主要,很主要,出题目了怕不是要给指导追着打。别的,这一块的细节迥殊多,必需各个方面都顾到,不然哪一方面出了马虎就全局泡汤。

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seedhttps://github.com/Array-Huang/webpack-seed)。

附系列文章目次(同步更新)

本文首发于
Array_Huang的手艺博客——
有用至上,非经作者赞同,请勿转载。

原文地点:
https://segmentfault.com/a/1190000010317802

假如您对本系列文章感兴趣,迎接关注定阅这里:
https://segmentfault.com/blog/array_huang

    原文作者:array_huang
    原文地址: https://segmentfault.com/a/1190000010317802
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞