原文链接

作者:梯田

前言:

所谓【静态资源依赖分析】,指的是可以通过分析页面资源后,可以以 json 数据或者图表的方式拿到页面资源间的依赖关系。

比如 college-index(酷家乐大学首页)的入口文件 entry.js 引用了 banner.js、 同时 banner.js 又引用了 utils.js, 那么我们希望经过分析后能拿到一份这样的数据:

[{"type": "entry","path": "/xx/xx/college-index/entry.js","deps": [{"type": "module","path": "/xx/xx/college-index/banner.js","deps": [{"type": "module","path": "/xx/xx/college-index/utils.js","deps": []}]}]}
]
// type 分表表示它是一个 entry 还是一个 module
复制代码

拿到资源依赖文件之后可以做什么呢?笔者这里有几个利用场景可供参考:

  • 对一个多页面 repo 而言,每次要发布的时候,我希望通过 git diff 拿到本次改动的文件,再通过依赖分析拿到此次需要构建的资源,这样就可以做到单页面发布了。
  • 我可以拿到当前的资源依赖,为我剔除 repo 中没有用到的资源
  • 我希望在 vscode 扩展中实时预览前端 repo 中的资源依赖情况

用处还可能有很多,关键是如何快速拿到这份依赖分析数据?

一个思路

这里给出一个笔者曾经考虑过的思路:通过遍历页面入口,然后进行关键字匹配,比如对( 【import xx from xxx】、【require】) 等关键字做处理,拿到被依赖的模块路径,然后继续对模块路径做递归解析,最终汇总拿到依赖树。

这个思路乍看是可行的,而且使用一些措施会使得分析流程更加高效,比如再对关键词作匹配的时候可以借助 acorn 这类的 JavaScript 解析器,再通过对文件被解析后的 ast 作处理。

但是简单尝试后就放弃了这个思路,原因是对于现在的前端工程化项目而言,一个页面中的依赖不仅有 js 而且还会有各种各样的资源,比如 sass、less 之类的 css 预处理器、或者是别的资源等等,所以单单对 js 路径做处理是不够的。

借助 webpack 来实现

在上述思路不可行的情况下,我们将解决办法瞄向了借助 webpack 来实现,对 webpack 熟悉的开发者会知道 webpack 对于依赖对处理的过程,这里再简单的提一下:webpack 拿到入口文件 entry 后,会通过先获取资源的正确路径,再经过 loader 解析文件,最后通过遍历 ast 来拿到模块中引用的依赖 【dependences 】,再对 【dependences】 做递归处理,最终拿到依赖树。

这跟我们最初设想的思路基本一致,同时借助 loader 可以将不同的资源无法解析的问题也一并解决了。看到这里的人或许会有疑问:官方不是已经给出了 webpack-bundle-analyzer 这类的工具了吗? 而且每次构建后 stats 中都能拿到文件依赖,为啥不能直接使用呢。

不直接使用的原因很简单:首先构建一次实在太慢了,特别是有几十个页面存在的情况下,另一个原因是我只是想拿到资源依赖,我根本不想对整个前端 repo 进行一次构建,也不想生成任何 bundle。

有没有一种工具可以如同 webpack 一样,既可以使用 loader 找到文件依赖,又不需要生成和压缩 bundle 呢?

在我们改造 webpack 之前它本来是没有的,如何改造? 一个 webpack plugin 即可。

webpack 的构建流程

在介绍如何改造之前,有必要了解一下 webpack 的模块处理过程以及整体流程。

模块的处理过程:

webpack 拿到一个路径后,他会依次执行 reslove 模块路径 -> create 模块 -> build 模块 -> parse 模块等主要流程,每一步的流程的作用如下:

-【 reslove 模块 】:获取模块真实路径 -【 create 模块 】 :创建一个模块的 context -【 build 模块 】 :读取模块内容 -【 parse 模块 】 :分析模块内容(主要是找到模块中的 require 关键词,将依赖添加到该模块的依赖数组中。

最后重复上述流程

每一个流程都非常复杂,以【 reslove 模块】 这个流程为例: 处理逻辑主要在 webpack/lib/normalModuleFactory.js 这个文件中, 整个 reslove 流程会依次执行 beforeResolvefactoryresolver 以及 afterResolve 这些步骤,每个步骤对应一个 hook ,每个 hook 执行的时候会拿到关于模块的描述信息,描述信息会随着 hook 的执行愈发饱满,最终获取到了完整的模块信息,为接下来的【 create 模块】以及 【 build 模块】做好准备,在下文【具体实现】中我们会插手这部分流程。

本文不再细谈模块的处理流程,网上有许多优秀的文章可以参考,请大家自由查阅。

webpack 的整体流程:

在笔者看来,webpack 的整体流程分为 4 个步骤,图示如下:

实现依赖分析中我们只需要 webpack 的前 3 个流程就够了,下文会给出原因。

解决办法

前面我们提到 webpack 处理依赖的过程是递归分析入口的所有依赖, 由于页面中的依赖包含相对或者绝对路径引用的依赖,也包含借助 alias 引用的依赖,也包含 node_modules 中的依赖,既然我们只想拿到 repo 前的资源依赖,对于 node_modules 中的依赖可以直接屏蔽掉,这将使得模块的递归时间大大缩短,如下图所示:

由:

变成:

在上面提到的 webpack 4 个主流程中,在 【step3】 结束后,webpack 能拿到所有 modules (一次构建行为产生的所有的模块),此时已经足够我们进行依赖分析了,我们直接终止 webpack 的后续流程,不再进行生成 chunk 以及对 chunk 做的合并优化等过程。这就达到了本文题目中目的,【用十分之一的构建时间做一场页面静态资源依赖分析】。

具体实现:

写一个 webpack plugin

plugin 名字就叫 FastDependenciesAnalyzerPlugin ,plugin 写法参照官方文档。

class FastDependenciesAnalyzerPlugin {beforeResolve = (resolveData, callback) => {}afterResolve = (result, callback) => {}handleFinishModules = (modules, callback) => {}apply(compiler) {compiler.hooks.normalModuleFactory.tap("FastDependenciesAnalyzerPlugin",nmf => {nmf.hooks.beforeResolve.tapAsync("FastDependenciesAnalyzerPlugin",this.beforeResolve);nmf.hooks.afterResolve.tapAsync("FastDependenciesAnalyzerPlugin",this.afterResolve);});compiler.hooks.compilation.tap("FastDependenciesAnalyzerPlugin",compilation => {compilation.hooks.finishModules.tapAsync("FastDependenciesAnalyzerPlugin",this.handleFinishModules);});}
}
复制代码

complier.hooks.normalModuleFactory 这个 hook 的回调中继续监听 normalModuleFactorybeforeResolve hook 和 beforeResolve hook。

complier.hooks.compilation 这个 hooks 的回调中继续监听 compilationfinishModules hook。

插手 beforeResolve 流程

 beforeResolve(resolveData, callback) {const { context, contextInfo, request } = resolveData;const { issuer } = contextInfo;}
复制代码

context 表示为 解析目录的绝对路径,一个页面的 context 都是一样的, issuer 翻译为发行人,在 webpack 中表示本模块被依赖的对象路径,也就指向这个模块的来源,request 表示当前模块的的请求路径。 比如:banner.js 的资源路径为:/xx/xxx/banner.js , 文件内容是这样的:

// 1
import utils from './utils'// 2
import utils from '@utils'// 3
import utils from 'utils'
复制代码

所以对于 utils 模块来说, issuer 的值为 /xx/xxx/banner.js, request 的值分别为 "./utils.js"、"@utils"、"utils" 。

此时,我们只能知道当前模块的来源路径 issuer 以及它被请求的路径 request,拿不到当前模块的真实路径,还无法将它放入我们的依赖树中,所以我们不会在 beforeReslove 中处理我们的依赖树,我们在这一步中只是想屏蔽掉一些我们不想被处理的模块就可以,比如假设 utils 这个 npm 包里有非常多的小模块,这些模块不会被放到依赖树中,所以对于这些模块我们选择直接跳过, 事实上在 webpack 源码中有这样一句:

对于在 beforeResolve 中没有返回值的模块会直接 callback ,在 webpack 源码中 callback 里如果没有参数,往往意味着流程的提前结束,在 beforeResolvereturn callback 也就没有这个模块后续对 reslovebuild 流程了,这就实现了模块跳过。

为此,我们可以实现一个 skip 函数,这里提供一个简单版本,传入参数为 issuerrequest

// 事先获取到 package.json 里的 dependencies const ignoreDependenciesArr = Object.keys(dependencies);function skip(request, issuer) {return (ignoreDependenciesArr.some(item => request.includes(item)) ||issuer.includes("node_modules"));
}复制代码

通过比较 request 是否在 package.json 里定义的 dependencies 里,或者如果 issuer 本身包含 node_modules,就可以表示当前模块是可以跳过的。当然这个方法只是一个简单版本,实际上要考虑许多特殊情况,这里不会详细给出,感兴趣的读者可以自行实现。

借助 afterResolve

 afterResolve(result, callback) {const { resourceResolveData } = result;const { context:{issuer},path} = resourceResolveData;}// 这里添加依赖到依赖树
复制代码

webpack 使用 enhanced-resolve 对一个请求路径作解析,只需要传入模块的 contextInfo, context, request, 即可拿到当前模块的真实路径,当然前提是需要告知 enhanced-resolve ,这个模块所在 repo 的 package.json, webpack 配置中的 alias 信息等等,本文不会叙述 enhanced-resolve 的工作方法,感兴趣的读者可以自行查阅。

总之,在 afterReslove 中我们能够拿到 webpack 借用 enhanced-resolve 解析过后的模块路径了,还有依赖这个模块的父模块路径,将这两个路径添加到依赖树中,经过简单的递归操作就可以拿到完整的依赖树了,当然在依赖树中我们可以放置各种信息,比如是否是一个模块?是否是一个 js 文件、是否是一个 css 文件,这些都可以实现。

在 finishModules 里结束

webpack 官方在 4.30 提交了一次 commit,在这次提交中将 finishModules 这个 SyncHook 转化成了 AsyncSeriesHook , 同时在 finishModules hook 中加入了一行代码,如下图:

这使得我们可以监听 finishModules 这个 hook ,然后在 err 方法里传入一个值,这就直接屏蔽掉了后续的文件合并以及优化流程了,当然这个操作比较 hack,也不是官方推荐用法,希望在webpack 5.X 的更新中,官方可以提供更合适的 hook ,让我们方便跳过某些流程。

一些踩坑

  1. 对于在 js 中引用的 css 或者 scss 文件,可以通过寻常的 reslove 流程拿到依赖,但是如果在 css 中使用了 @import 语法,由于 css-loader 会自行处理这些语法,所以它不会走 webpack 本身的 reslove 流程,详见这个 issue,这里得我们自己在 beforeReslove 中对这部分做额外对处理,比如通过字符串截取的方式去掉 request 中关于 loder 描述的部分,再通过主动调用 enhance-resolve 这个方法实现对 @import 传进来对模块做处理,最终拿到正确的路径。

  2. 不同版本的 webpack 一些 hook 的用法和名称会不一样,开发者在处理内部流程的时候要注意。

总结

本文所阐述的原理并不深奥,主要是挖掘了一个 webpack 的一个用法,希望能启发想要利用 webpack 做更多工具的开发者多借助 webpack 内部原理,最后感谢大家的阅读,欢迎有兴趣的朋友在文章底下评论,给出建议和帮助。

转载于:https://juejin.im/post/5ce272c56fb9a07f0a2db1fc

巧用 webpack 做页面静态资源依赖分析相关推荐

  1. Hexo瞎折腾系列(5) - 使用hexo-neat插件压缩页面静态资源

    为什么要压缩页面静态资源 对于个人博客来说,优化页面的访问速度是很有必要的,如果打开你的个人站点,加载个首页就要十几秒,页面长时间处于空白状态,想必没什么人能够忍受得了吧.我个人觉得,如果能把页面的加 ...

  2. Hexo文件压缩:使用hexo-neat插件压缩页面静态资源

    目录 为什么要压缩页面静态资源 hexo的压缩静态资源插件 如何使用hexo-neat 在站点根目录下安装hexo-neat 为站点配置文件添加相关配置 hexo-neat插件踩坑记录 跳过压缩文件的 ...

  3. servlet,springmvc,springboot转发时页面静态资源404问题

    目录 不讨论静态资源过滤的问题... 这个问题重定向不会404,因为重定向是找到了对应的页面,是浏览器决定的. 而转发在相同目录下转发会找到资源.但是从controller(根目录)里面转发到根目录的 ...

  4. 静态资源部署分析和实验

    浏览器F5 和CRTL+F5的区别 这个两个的区别主要体现在浏览器发往服务端的http的报头的区别: 场景:在第一次成功浏览一个静态资源后,即http status 为200 F5后的报头如下: GE ...

  5. 前端静态资源缓存最优解以及max-age的陷阱

    原文地址:点这里 合理的使用缓存可以极大地提高网站资源的利用率,还可以节约带宽从而降低服务器成本.但是很多站点针对缓存的策略并不合理,甚至是完全无作为,如果是这样,就完全没有发挥出缓存的优势,而不合理 ...

  6. 项目性能优化(实现页面静态化1)

    当首页访问频繁,而且查询数据量大,其中还有大量的循环处理时,这会耗费服务器大量的资源,并且响应数据的效率,这时就需要页面静态化. 1. 页面静态化介绍 1.为什么要做页面静态化 减少数据库查询次数. ...

  7. vue 项目引用static目录资源_Vue2.0项目入门 — 静态资源目录src/assets和static/区别...

    rose.png 你应该注意到了,在项目结构上我们有静态资源两个目录:src/assets和static/.他们之间有什么区别? 通过webpack处理的资源 首先我们需要了解webpack如何处理静 ...

  8. glide默认的缓存图片路径地址_手写一个静态资源中间件,加深了解服务器对文件请求的缓存策略...

    上一篇文章<详解页面静态资源的缓存策略,搞懂强缓存和协商缓存再做性能优化>我们从理论上介绍了浏览器和服务器是如何对静态资源做缓存的,这篇文章我们把它做成一个node服务器的静态资源中间件. ...

  9. html如何获取请求头变量的值。_手写一个静态资源中间件,加深了解服务器对文件请求的缓存策略...

    上一篇文章<详解页面静态资源的缓存策略,搞懂强缓存和协商缓存再做性能优化>我们从理论上介绍了浏览器和服务器是如何对静态资源做缓存的,这篇文章我们把它做成一个node服务器的静态资源中间件. ...

最新文章

  1. 数组常见的遍历循环方法、数组的循环遍历的效率对比
  2. linux打开应用程序的命令,Windows环境下如何通过命令打开程序!
  3. 情人节——微信朋友圈浓浓爱意的9张拼图(HTML版本)
  4. 自制基于HMM的python中文分词器
  5. HitPaw Watermark Remover视频图去除水印工具V1.2.1.1
  6. “超大杯”版小米10被曝8月中下旬发布:100W快充实锤 处理器却成迷
  7. 电力行业信息安全等级保护管理办法_信息安全等级保护是什么???
  8. 调查 10,500 名 Java 开发者发现,收费的 OracleJDK 仍是主流、IntelliJ IDEA 最受欢迎...
  9. C++教程:C++工程构建常用工具有哪些?
  10. 获取synchronized锁中的阻塞队列中的线程是非公平的
  11. velocity语法小结
  12. 解决excel里面“取消隐藏”是灰色的问题
  13. 网络抖动多少ms算正常_网络延迟多少秒算正常
  14. 使用TextMeshPro实现打字机效果
  15. 第50节:初识搜索引擎_上机动手实战多搜索条件组合查询
  16. cad渐变线怎么画_花花绿绿的股票线是怎么画出来的?想怎么画就怎么画!
  17. 基于BootStrap+Django+MySQL的云笔记平台系统
  18. 我与电脑2-高中时期
  19. 【LTspice】004 Voltage Source 参数配置
  20. 用 ECharts 做出漂亮的数据统计图

热门文章

  1. 任正非:为什么华为选择与西工大合作,而没选清华北大,mysql连接查询原理
  2. vpython学习--实现滑块木板联动
  3. 高校人员信息管理系统(Python版)
  4. locaspaceviewer图新地球卫星影像地图下载
  5. tl494c封装区别_TL494集成电路引脚功能和数据
  6. 函数定义与调用,自己实现pow()函数对整数的运算
  7. Android 中获取指纹(SAH1)签名
  8. ubuntu下的截图和图像编辑软件推荐
  9. 小程序学习从入门到熟练教程
  10. 什么是掩膜(Mask)?