最近读了一本书《webpack实战:入门、进阶与调优》一作者居玉皓。发现以往很多七七八八的概念明白的更加通透,又是开心的一天,开始结合以前的总结重新来认识下webpack。
Part1: 简单认识下Ta
什么是webpack?
webpack是一个开源的JavaScript模块打包工具。(我们可以把webpack理解为一个模块处理工厂,我们把源代码交给webpack,由它去进行加工,拼装处理,最后产出最终的资源文件)
那么是谁创造它的?
作者是德国纽伦堡Tobias Koppers,一位自由软件开发者
创造它是想解决什么问题?
谷歌曾经推出过一个工具,叫GWT(Google Web Toolkit),让Java程序员能用Java编写客户端应用。GWT其实是一个Java应用到JavaScript SPA的编译器,也使用了谷歌的一些应用。GWT有一个功能作者研究了很长时间,就是代码拆分(code splitting)。这个功能可以延迟加载不常用的代码。对于要保持初始加载速度的大型应用,这个功能非常重要。但作者没发现JavaScript的开源工具(2012年)中哪个具备这个功能,于是就想写一个这样的工具,也就是webpack。
总结就说,webpack诞生之初主要想解决代码拆分的问题
代码拆分又是干啥用的?
从字面意思理解,就是它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态加载。这对于资源体积较大的应用来说尤为重要,可以有效的减小资源体积,提升首页渲染速度。
Part2: 简单了解它后,再来复杂认识下
webpack核心概念走起
- 入口(entry): 入口模块位置,告诉webpack从哪里开始进行打包
- 输出(output):资源的输出位置
- loader:预处理器,它赋予webpack处理不同资源类型的能力
- 插件(plugin): 插件,用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
基本配置一个个走起
配置资源入口:
webpack通过context和entry这两个配置项来共同决定入口文件的路径
module.exports = { context: path.resolve(__dirname, '../'), entry: { app: './src/main.js' } }
context可以理解为资源入口的路径前缀, 只能为字符串, 配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。 当然context也可省略, 只需配置entry即可。
与context只能为字符串不同, entry可以是对象,可以是字符串,也可以是对象,函数。
module.exports = { entry: {} / '' / [] / function, // entry可以是对象,可以是字符串,可以是对象,函数 };
1) 字符串类型入口
直接写入文件路径
2) 数组类型入口
传入一个数组的作用是将多个资源预先合并,在打包时webpack会将数组中的最后一个元素作为实际的入口路径,如:
module.exports = { entry: [ 'react-hot-loader/patch', `webpack-hot-middleware/client?path=http://${config.host}:${config.port}/__webpack_hmr`, 'babel-polyfill', "./app/index.js" ] }
3) 对象类型入口
如果想定义多入口,则必须使用对象的形式。对象的属性名是chunk name, 属性值是入口路径,如:
module.exports = { entry: { index: './src/index.js', lib: './src/lib.js' } }
4) 函数类型入口
用函数定义入口时,返回值只要为上面说的三种配置形式即可,如:
module.exports = { entry: () =>({ index: './src/index.js', lib: './src/lib.js' }) }
传入一个函数的优点在于我们可以在函数体内添加一些动态的逻辑来处理项目的入口。
入口文件的进一步优化
对于单页应用来说,一般定义单一入口即可, 这样做的好处是只会产生一个JS文件,依赖关系清晰,而这种做法的弊端是当应用的规模上升到一定程度之后会导致产品的资源体积过大,降低用户的页面渲染速度。同时试想一旦产生代码更新,即便只有一点点改动,用户都要重新下载整个资源文件,这对于页面的性能也是非常不友好的。
为了解决这个问题,我们可以提取vendor, 在webpack中,vendor一般指的是工程所使用的库,框架等第三方模块集中打包而产生的bundle,如下:
module.exports = { entry: { app: './src/main.js', vendor: ['react', 'react-dom', 'react-router'] } }
通过这样的配置(加上optimization.splitChunks,文末针对此处补充), main.js产生的bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle。vendor包含的第三方模块代码不会经常变动,因此可以有效的利用客户端缓存,在用户后续请求页面时会加快整体的渲染速度。
配置资源出口:
module.exports = { output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].js',
publicPath: ‘/’
}
}
path用来指定资源的输出位置,而publicPath则用来指定资源的请求位置。
输出位置: 打包完成后资源产生的目录,一般将其指定为工程中的dist目录
请求位置: 由JS或CSS所请求的间接资源路径。页面中的资源分为两种,一种是由HTML页面直接请求的,比如通过script标签加载的JS;另一种是由JS或CSS请求的,如异步加载的JS,从CSS请求的图片字体等。publicPath的作用就是指定这部分间接资源的请求位置。
比如当前项目地址为: http://localhost:8000
加载的资源名假如为main.js
当设置publicPath: '' 时, 资源访问的实际路径为http://localhost:8000/main.js
当设置publicPaht: './test'时,资源访问的实际路径为http://localhost:8000/test/main.js
预处理器Loader
来看看我们常用的loader有哪些
1) css-loader: 项目中引入css文件, wepback无法处理css语法,抛出一个错误提示引入合适的loader处理这种文件,我们引入css-loader后,报错消失。
{ test: /.css$/, use: 'css-loader', }
但是css样式没有生效,这是因为css-loader的作用仅仅是处理css的各种语法,如果要使得样式生效还需要style-loader来把样式插入页面
2) style-loader: 将样式字符串包装成style标签插入页面
{ test: /.css$/, use: ['style-loader', 'css-loader'] }
处理某一类资源时需要使用多个loader,如上,我们把style-loader放到css-loader前面,这样因为webpack打包时是按照数组从后往前的顺序将资源交给loader处理的,因此要把最后生效的放在前面
3) babel-loader: 处理es6+语言特性
{ test: /.js$/, use: ['babel-loader'],
exclude: /node_modules/,
include: /src/ },
顺便介绍下关于exclude与include
exclude: 所有被正则匹配到的模块都排除在该规则之外,也就是说node_modules中的模块不会执行这条规则,以此加快整体的打包速度。(像我们用babel-loader来处理ES6+语言特性,但是对于node_modules中的JS文件来说,很多都是已经编译为ES5的,因此没有必要再使用babel-loader来进行额外处理)
include: 代表该规则只对正则匹配到的模块生效。如上设置为项目的源码目录,因此node_modules等目录就被排除掉了。
exclude和include同时存在时,exclude的优先级更高
4) eslint-loader: 对源码进行质量检测
{ test: /.js$/, enforce: 'pre', use: ['eslint-loader'] },
顺便介绍下enforce: 用来指定一个loader的种类,只接受'pre'或‘post’两z种字符串类型的值
当设置enforce值为'pre'时,代表它将在所有正常loader之前执行,这样可以保证其检测的代码不是被其他loader更改过。
当设置enforce值为'post'时,代表它将在所有正常loader之后执行
5) file-loader: 用于打包文件类型的资源,并返回其publicPath
{ test: /.(png|jpg|gif)$/, use: 'file-loader' },
对这类图片资源使用file-loader后,就可以在JS中加载图片了
配置资源出口章节讲过publicPath则用来指定资源的请求位置的,当配置中没有配置指定的output.publicPath时,图片的路径即文件名,当有了如下配置后(file-loader本身也支持publicPath的配置):
module.exports = { output: { path: path.resolve(__dirname, '../dist'), filename: '[name].js', publicPath: ‘./assets’ }, module: { rules: [ {
test: /.(png|jpg|gif)$/,
use: 'file-loader'
} ] } }
此时图片的路径地址为: ./assets/文件名.文件后缀
6) url-loader: 作用与file-loader类似,唯一不同的是可以设置一个文件大小的阈值,当大于该阈值时与file-loader一样返回,而小于该阈值时则返回文件base64形式编码
rules: [ { test: /.(png|jpg|gif)$/, use: { loader: 'url-loader', options: { limit: 1024', name: '[name].[ext]', publicPath: './assets' } } } ]
loader本质上是一个函数。第一个loader的输入是源文件,之后所有的loader的输入是上一个loader的输出,最后一个loader则直接输出给webpack.
script
标签的 body 中的所有 webpack 包CommonsChunkPlugin
已经被移除了,现在是使用optimization.splitChunks
代替相关配置项:
module.exports = { //... optimization: { splitChunks: { chunks: 'async', minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~', name: true, cacheGroups: {
vendors: {},
default: {}
} } } }
- chunks: 表示哪些代码需要优化,有三个可选值:initial(初始块)、async(按需加载块)、all(全部块),默认为async
- minSize: 表示在压缩前的最小模块大小,默认为30000
- minChunks: 表示被引用次数,默认为1
- maxAsyncRequests: 按需加载时候最大的并行请求数,默认为5
- maxInitialRequests: 一个入口最大的并行请求数,默认为3
- automaticNameDelimiter: 命名连接符
- name: 拆分出来块的名字,默认由块名和hash值自动生成
- cacheGroups: 缓存组