基本概念
本质上,webpack是一个用于现代JavaScript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点(Entry)构建一个依赖图,然后将项目中所需要的每一个模块组合成一个或多个bundles,他们均为静态资源,用于展示内容。
重要概念:
- 入口(entry)
- 输出(output)
- Loader
- 插件(plugin)
- 模式(mode)
- 浏览器兼容性(browser compatibility)
- 环境(environment)
entry
入口起点(entry point)代表webpack将要使用哪个模块,来作为构建起内部依赖图的起点。在进入入口起点之后,webpack会自动找出那些直接或者间接依赖入口起点的模块。
默认的入口起点为:./src/index.js,可以在webpack configuration中自定义入口起点。
output
output属性会告诉webpack把打包后的bundle存储在哪个位置,以及重命名bundle的文件名。
主要输出文件的默认值是:./dist/main.js,其他生成文件默认放置在./dist文件夹中。
配置相关属性:
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
loader
原始的webpack只能处理JavaScript和JSON文件,而loader让webpack可以去处理其它类型的文件,并将他们转换为有效的模块,并添加到依赖图中。
允许webpack去处理非JavaScript文件:css/img,类似于“翻译官”
在webpack的配置中,在module的rules下为loader设置相关属性,有两个属性:
test属性:用来识别出哪些文件需要被转换,值通常为判断文件类型的正则表达式use属性:定义在进行文件转换时,webpack将使用哪个loader
通俗语义:webpack在对.xxx文件进行打包时,先使用use规定的loader转换一下,再去打包。
注意:在webpack配置中定义rules时,要在module.rules下进行配置,而不是在rules下进行配置
plugin
loader用于转换某些类型的模块,而plugin则可以用于执行范围更广的任务,包括:打包优化,资源管理,注入环境变量。
允许webpack执行范围更广的任务,允许webpack“开飞机开大炮”
在webpack.config.js中,想要使用一个插件,只需要require()他,然后把它添加到plugins数组中即可。
注意:如果需要在一个配置文件中多次使用同一个插件,则需要使用new操作符来创建一个新的插件实例。
例子:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
模式
通过选择development,production或none中的一个,来设置mode参数。之后,webpack会启动不同环境下的内置优化。
浏览器兼容性
webpack默认支持所有符合ES5标准的浏览器,不支持IE8及以下版本。
webpack的import()和require.ensure()需要Promise。
如果想要支持旧版本浏览器,在使用这些表达式之前,还需要提前加载polyfill。
运行环境
Webpack5运行于Node.js v10.13.0+的版本
详细配置
entry points
用法: entry: string | string[] | {}
多种写法:
- 简写:
entry:'./path/proj/file.js' - 详细写法:
entry:{
main: './path/proj/file.js'
}
- 传递文件路径数组,创建所谓的"multi-main entry",在一次注入多个依赖文件时,并且将他们的依赖关系绘制在一个"chunk"中时,可以使用这种方法:
entry:['./src/file_1.js', './src/file_2.js']
- 对象语法:
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
用于描述入口的对象,包含如下属性:
dependOn:当前入口所依赖的入口,必须在当前入口被加载前加载完毕filename:指定要输出的文件名称import:启动时需加载的模块library:指定library选项,为当前entry构建一个libraryruntime:运行时chunk的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为false可以避免创建一个新的运行时 chunkpublicPath:当该入口的输出文件在浏览器中被引用时,为他们制定一个公共URL地址
例:
entry: {
a2: 'dependingfile.js',
b2: {
dependOn: 'a2',
import: './src/app.js',
},
},
- 分离app(应用程序)和vendor(第三方库)入口:
entry: {
main: './src/app.js',
vendor: './src/vendor.js',
},
这样子可以让webpack配置两个单独的入口点,这样就可以在vendor.js中存入未做修改但是必要的library或其他文件,例如Bootstrap,jQuery,图片等,最后将他们打包在一起成为单独的chunk。这样内容哈希保持不变,可以使浏览器独立地缓存它们,减少加载时间。
- 多页面程序:
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js',
},
这样告诉webpack需要三个独立分离的依赖图。增多入口起点数量,可以使得多页应用能够复用多个入口起点之间的大量代码/模块,从而提升效率。
output
- 常见写法:
module.exports = {
output: {
filename: 'bundle.js',
},
};
- 当有多个entry文件时:需要使用占位符来确保每个文件具有唯一的名称
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js',
},
output: {
filename: '[name].js',
path: __dirname + '/dist',
},
};
// 写入到硬盘:./dist/app.js, ./dist/search.js
loader
loader用于对模块的源代码进行转换,可以在import或load(加载)模块时预处理文件。loader可以帮忙处理各种类型的文件,甚至允许项目在JavaScript模块中importCSS文件。
使用loader的两种方式:
- 在
webpack.config.js中直接配置(推荐)
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' },
],
},
};
- 在单个文件中的import语句中内联使用(不推荐)
import Styles from 'style-loader!css-loader?modules!./styles.css';
//使用 ! 来将不同的loader分开
loader常见特性:
- loader是从右到左,或从下到上执行的
- loader支持链式调用,当进行链式调用的时候,执行顺序是相反的,即:链中的第一个loader将其结果传递给下一个loader,以此类推,链中的最后一个loader来返回webpack所期望的JavaScript
- loader可以同步也可以异步
- loader运行在Node.js中
- plugin(插件)可以为loader带来更多特性
plugin
plugin是webpack的支柱功能,webpack自身也是构建在webpack配置中用到的相同的插件系统上。
plugin存在的目的在于解决loader无法实现的其他的功能。
底层剖析
plugin实际上是一个具有apply方法的JavaScript对象。apply方法会被webpack compiler调用,并且在整个生命周期都可以访问compiler对象。
ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建正在启动!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
在这其中compiler hook的tap函数中的第一个参数为将要调用的plugin的名称,名称应该遵循驼峰命名。建议为此使用一个常量,以便它可以在所有hook中重复使用。
常见用法
- 在
webpack.config.js配置: (因为可以给plugin传参,所以需要传入一个new实例)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 访问内置的插件
module.exports = {
...,
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
};
- Node API方式:
const webpack = require('webpack') // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');
let compiler = webpack(configuration);
new webpack.ProgressPlugin().apply(compiler);//为webpack中的插件调用apply方法
compiler.run(function(err, stats){
//...
})
Configuration
webpack的配置文件是JavaScript文件,文件内导出了一个webpack配置对象。
可以直接编写并导出一个配置模块:
const path = require('path');
module.exports = {
mode: 'development',
entry: './foo.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'foo.bundle.js',
},
};
模块
在模块化编程中,开发者将程序分解为功能离散的chunk,并称之为模块。
每个模块都拥有小于完整程序的体积,使得验证、调试及测试都变得轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。
webpack天生支持如下模块类型:
- ECMASript模块
- CommonJS模块
- AMD模块
- Assets
- WebAssembly模块
还为以下类型原生创建了对应的loader:
- CoffeeScript
- TypeScript
- ESNext(Babel)
- Sass
- Less
- Stylus
- Elm
runtime & manifest
webpack的runtime和manifest,共同管理所有模块的交互行为。
runtime
runtime,以及伴随的manifest数据,主要是指:在浏览器运行过程中,webpack用来连接模块化应用程序所需的所有代码。它包含:在模块交互时,连接模块所需的加载和解析逻辑,以及:已经加载到浏览器中的连接模块逻辑,和尚未加载模块的延迟加载逻辑。
manifest
一旦应用程序被打包,并发送给浏览器,然后浏览器以index.html的形式运行应用程序,项目原先的/src等目录结构就已经不复存在。而manifest就是用来去管理新目录结构下的所需模块之间的交互活动。
当compiler开始执行、解析和映射应用程序时,它会保留所有模块的详细要点,这个数据集合即为manifest。当项目被打包并发送到浏览器的时候,runtime会通过manifest来解析和加载模块。因为原先的import和require语句现在都已经被转换为__webpack_requie__方法,而此方法指向模块标识符(module identifier)。manifest中包含了各种所需模块的标识符,这样runtime就可以检索这些标识符,然后找到每个标识符背后对应的模块。
理解manifest的作用有助于正确利用浏览器缓存来提升项目性能。在通过内容散列(content hash)来作为bundle文件的名称,这样在文件内容修改时,会计算出新的hash值,浏览器会使用新的名称来加载文件,从而使得缓存无效。除此之外,即使内容没有变化,某些hash值仍然会改变,这是因为注入的runtime和manifest在每次构建之后都会发生变化。所以想要利用好浏览器缓存,需要小心利用manifest。