问题列表
- webpack是用来解决什么问题的 ?
- 它有什么亮点 ?
- loader 和 plugin 有什么区别 ?
- 运行原理 ?
- 如何编写 Loader?
- Webpack 和 Rollup 有什么相同点与不同点?
webpack 作用
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
根据官网的高度概述,我们可以简单理解webpack
是一个针对JavaScript
项目的打包工具。
简单浏览一遍官方文档,再看看上面这张图,就会发现webpack
的关键点在于模块处理
和打包
,在webpack构建的流程中,通过loader
处理非 JS 文件,将其转换为 webpack 能够处理的有效模块,使用各式各样的plugin
在构建时的关键步骤进行一些操作(比如按需加载,代码压缩等),最终输出浏览器能使用的静态资源。
这里思考一下为什么需要模块化呢?
那怎么理解模块化呢?
在webpack
中,核心思想就是一切皆模块,比如说一张图片,一个SCSS文件。
这样做的好处就是能够清晰地描述出各个模块之间依赖关系,便于webpack
对模块进行组合、打包。
为什么是 webpack
这些年前框框架、工具非常多,那为什么webpack
更受青睐呢?
这里来分析一下其它的工具
基于任务执行的工具
这些工具能够自动执行指定的任务。它们简单高效,社区活跃,有着丰富的插件,可以方便打造各种工作流。
基于模块化打包的工具
比如说:browserify、webpack、rollup
在 Node 环境下,如果需要引用组件,使用
require
关键字即可,这类工具就是这个模式,还可以实现按需加载、异步加载模块整合型工具
这类工具使用了多种技术栈实现的脚手架工具,好处是即开即用,缺点就是它们约束了技术选型,并且学习成本相对较高。
为什么选用 webpack
- 大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,
webpack
可以为这些新项目提供一站式的解决方案 webpack
有良好的生态链和维护团队,能提供良好的开发体验和保证质量webpack
被全世界的大量 Web 开发者使用和验证,能找到各个层面所需的教程和经验分享
核心概念
- entry: 入口
webpack
执行构建的第一步就从入口开始,可以抽象理解为输入 - module: 模块
在webpack
中一切皆模块,一个文件就是一个模块,webpack
会从入口开始递归遍历找出所有依赖的模块 - chunk: 代码块
一个Chunk
由多个模块组合而成,它是代码合并、分割的产物 - loader: 模块转换器
webpack
使用它把原内容转换成浏览器能够使用的文件 - plugin: 插件拓展
webpack
在构建流程中的特定时机注入扩展逻辑来改变构建结果 - output: 输出结果
webpack
经过一系列处理后,输出的最终结果
loader
webpack
只能理解JavaScript
和JSON
文件,这是webpack
开箱可用的自带能力。
loader
让webpack
能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。
用法
处理文件时可以使用多个loader
,loader
的执行顺序和配置中的顺序是相反的。也就是说最后一个loader
先执行,它接收源文件作为参数,处理之后把处理结果传给下一个loader
常用的 loader
- css-loader
加载CSS
、支持模块化、压缩、文件导入等特性 - style-loader
把CSS
代码注入到JavaScript
中,通过DOM
操作去加载CSS
- file-loader
把文件输出到一个文件夹中,在代码中通过相对URL去引用输出的文件 - url-loader
它和 filr-loader 类似,但是能在文件很小的情况下以base64
的方式把文件内容注入到代码中 - babel-loader
让开发者能够在项目中使用新的JavaScript
特性
plugin
插件是webpack
的支柱功能。
插件目的在于解决loader
无法实现的其他事。
webpack
自身也是构建于插件系统之上。
原理
在webpack
运行的生命周期中会广播出许多事件,插件可以监听这些事件,在合适的时机通过webpack
提供的API改变输出结果,在这个过程中插件可以访问到compile
和compilation
,通过钩子拦截webpack
的执行。
常用的 plugin
- clean-webpack-plugin
每次打包后自动清理output.path
内的文件,只保留本次构建的结果 - html-webpack-plugin
创建一个html
文件,并把webpack
打包后的静态文件自动插入到这个html
文件当中 - DefinePlugin
这是一个定义全局变量的插件,定义的变量可以在webpack
打包范围内任意JavaScript
环境内访问 - MiniCssExtractPlugin
将CSS
单独打包成一个文件,它为每个包含CSS
和JavaScript
文件都创建一个CSS
文件,结合html-webpack-plugin
插件,以link
的形式插入到html
文档中 - webpack-merge
该插件用于合并配置 - webpack-bundle-analyzer
项目打包后进行性能分析
构建流程和原理
构建之前先了解几个名词:
- Compiler:编译管理器,
webpack
启动后会创建compiler
对象,该对象一直存活直到结束退出 - Complilation:单次编译过程的管理器,比如
watch=true
时,运行过程中只有一个compiler
,但每次文件变更触发重新编译时,都会创建一个新的compilation
对象 - Dependence:依赖对象,
webpack
基于该类型记录模块间依赖关系 - Module:
webpack
内部所有资源都会以module
对象形式存在,所有关于资源的操作、转译、合并都是以module
为基本单位进行的 - Chunk:编译完成准备输出时,
webpack
会将module
按特定的规则组织成一个一个的chunk
,这些chunk
某种程度上跟最终输出一一对应
构建流程:
初始化阶段
- 初始化参数:从配置文件、配置对象、Shell参数中读取,进行错误检查,与默认配置结合得出最终的参数
- 创建编译器对象:用上一步得到的参数创建
Compiler
对象 - 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化RuleSet集合、加载配置的插件等
- 开始编译:执行
compiler
对象的run
方法 - 确定入口:根据配置中的
entry
找出所有的入口文件,调用compilition.addEntry
将入口文件转换为dependence
对象
构建阶段
- 编译模块:根据
entry
对应的dependence
创建module
对象,调用loader
将模块转移为标准JS内容,调用JS解释器将内容转换为AST对象,从中找出该模块依赖的模块,再递归这个步骤直到所有入口依赖的文件都经过了本步骤的处理 - 完成模块编译:上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的
依赖关系图
- 编译模块:根据
生成阶段
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表。这个步骤是可以修改输出内容的最后机会 - 写入文件系统:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
源码中的一些重要方法:
- webpack_require: 我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导入导出以来的模块,webpack在打包的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性
- webpack_modules:存放了编译后的各个文件模块的JS内容
- webpack_module_cache:用来做模块缓存
配置优化
webpack
配置虽然简单,但是配置项非常多,每个配置项的取值可能也有很多的选择,故而优化的空间很大。
讲一些平常用的多的配置优化,主要分为以下三种:
优化构建速度
使用多进程多实例解析资源
- thread-loader
- happypack
使用
DLLPlugin
对基础依赖库进行分包,减少编译次数DllPlugin动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取。dll中大多包含的是常用的第三方模块,如react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。
externals,从输出的bundle中排除依赖
从 CDN 引入 jQuery,而不是把它打包
缩小范围
配置loader时,指定loader的作用目录或者需要排除的目录,通过使用include和exclude两个配置项
noParse,使用noParse进行忽略的模块文件中不会解析import、require等语法
使用缓存
- babel-loader
- cache-loader
优化构建结果
构建结果分析,使用
webpack-bundle-analyzer
插件可以看到项目各模块大小,进行按需优化
stat表示文件的输入大小,parsed表示文件的输出大小,gzip表示通过gzip压缩运行解析的包/模块大小
webpack默认支持,使用
tree shaking
擦除无用代码移除
JavaScript
上下文中的未引用代码开启
scope hosting
减少函数申明和内存开销这个名词直译过来就是作用域提升,它把所有的代码按照引用顺序放在一个函数作用域里面,适当的重命名变量防止冲突
优化运行时体验
入口分割点分割,多页打包
使用
splitChunksPlugin
分离页面公共文件,抽取公共代码代码懒加载
prefetch,在浏览器空闲的时候进行资源的拉取
preload,提前加载后面会用到的关键资源
总结来看,可以使用大量插件对编译过程进行优化。
参考
书目:
- 深入浅出 Webpack