Webpack基础
Webpack从一个或多个入口(Entry)开始寻找相关的依赖关系来建立依赖图,然后识别他们的内容,webpack通过加载器(Loader)来识别模块的内容,再将内容整理分类打包成不同的块,最后放到输出(Output)目录下。在这个过程中,可以使用一些有用的插件(Plugin)来辅助完成各种任务,例如注入环境变量、资产管理或打包优化等。
1.安装与运行
首先计算机中必须预先安装好node.js运行环境,再安装webpack依赖。
npm install --save-dev webpack webpack-cli
最后通过npx webpack
即可执行打包任务。
2.核心配置
从4.0.0版本开始,Webpack不必用一个配置文件去捆绑你的项目,默认情况下,webpack使用src/index.js
作为入口,输出的包是dist/main.js
文件,然而也可以详细地配置各项参数来适配各自的需求。
2.1创建配置文件
这时候就需要在项目里面新增一个webpack.config.js
文件,这里有个工具可以在线创建。
2.2多配置源
可以根据不同的情况选择不同的配置文件:
"scripts": {
"build": "webpack --config prod.config.js"
}
2.3编译目标
通过设置target
属性,指定webpack编译输出的文件在什么样的环境中运行。例如服务器、浏览器分别可设置成"target": "node"
和"target": "web"
。
2.4入口配置
默认情况下使用./src/index.js
作为默认入口。若要配置其他入口,可通过设置配置文件的 entry
属性来实现:
module.exports = {
entry: './path/to/my/entry/file.js',
};
除了配置一个入口,还可以配置多个,多个入口就会对应多个输出,请确保每个输出的文件的名字要唯一,可以使用替换模版来确保名称的冲突。
{
entry: {
index: './index/file/path',
print: './print/file/path'
}
}
以上配置完以后,就会生成两个入口块index和print。默认情况的名字是main。
entry属性能接受的数据结构有:路径字符串、路径字符串的数组、对象结构。
关于entry对象的详细配置如下:
dependOn
: The entry points that the current entry point depends on. They must be loaded before this entry point is loaded.filename
: Specifies the name of each output file on disk.import
: Module(s) that are loaded upon startup.library
: Specify library options to bundle a library from current entry.runtime
: The name of the runtime chunk. When set, a new runtime chunk will be created. It can be set tofalse
to avoid a new runtime chunk since webpack 5.43.0.publicPath
: Specify a public URL address for the output files of this entry when they are referenced in a browser. Also, see output.publicPath.
更多设置请查阅这里。
2.5输出配置
默认情况下使用./dist/main.js
,若要改用其他设定,可设置配置文件的output
属性来实现:
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',
},
};
此例子中,使用path
来指定输出路径,__dirname
指定的事当前文件所在的根目录。filename
指定文件名称。
当需要动态的生成打包名称的时候,可以这样配置:
{
output: {
filename: '[name].bundle.js',
}
}
在这个例子中,[name]
将被文件的原名替换。
更多配置请查阅这里。
2.6加载器
通常情况下Webpack只能识别JavaScript和JSON两种类型的文件,其他类型的模块要使用不同的载入器。总体上看,载入器的配置有两个:
test
确定哪些文件可以被转换;use
指定哪个载入器来执行这些转换。
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
注意:test
属性后面的正则表达式没有引号,表示包含.txt
结尾的文件,若包含引号则表示的是绝对路径,即以.txt
结尾的文件。
2.7插件
插件是用来执行一系列任务来辅助完成整个打包过程,例如打包优化、资产管理、注入环境变量等。如果要使用一个插件,首先要用require()
将它导入,然后再添加到配置文件的plugins
属性数组里面。
插件本质上是一个拥有apply方法的javascript对象,apply方法能够被webpack编译器调用,且能访问整个汇编周期。
有很多插件可以在这里找到,还有很多用例值得探索,相关信息在这里找到。
插件可以传入参数,所以在使用中,可以通过传入参数来新建一个实例,在把这个实例传入webpack.config.js
的plugins
属性里。
2.8模式
预设的有三种模式:development
, production
, none
,每种模式都有不同的优化细度。默认情况是production
。
module.exports = {
mode: 'production',
};
更多配置请查看这里。
2.9NPM脚本
在package.json文件里面设置scripts脚本,可以更方便为以后提供cli命令。例如:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "webpack"
},
当配置好build
以后,可以通过命令行直接输入npm run build
即可执行对应的命令:npx webpack
。
任何参数(包括webpack.json里面的参数)都可以加在脚本的后面,前面带上--
,例如"build": "webpack --mode=production"
。
3.资产管理
webpack会将项目所有相关的依赖文件都打包到一起(没有用到的除外),默认情况下,webpack只能识别javascript和json两种文件(内部已经有了这两种文件的载入器或者资产模块支持),其他类型的文件还需要配置特定的加载器。
多个加载器,可以被前后链接,前面处理的结果可以被后面的加载器使用,直到最后一个加载器返回结果。
webpack内部的资产模块(asset modules),是一种不需要配置额外加载器的模块,可以用来处理字体、图标等文件的模块。主要有以下:
asset/resource
, 发出一个单独的文件并导出URLasset/inline
,导出资产的数据URIasset/source
, 导出资产的源代码asset
, 自动选择导出数据URI和发出一个单独的文件。
通常,资产文件都是全局的放在一个asset
目录之下,但是也可以把一些高度耦合的文件放在与它们关联的模块放在一起,这样打包出来的“私有”资产文件就会更加紧密的与关联的模块放在一起。
3.1载入CSS
webpack默认情况无法识别css文件,因此需要安装一个加载器:
npm install --save-dev style-loader css-loader
然后在webpack的模块部分进行相关配置:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};
在以上例子中,css文件使用了两个加载器来加载,分别是styled-loader
和css-loader
,前一个处理的结果作为后一个的输入,最后由css-loader
返回的结果作为css文件加载的最终结果。
当配置完毕以后直接运行npx webpack
就可完成打包。
3.2载入图片
可以直接使用资产模块来加载图片:
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource'
}
所有配置类型的图片都会被加载器处理,然后放到输出目录下。任何引用图片的变量都会包含得有被处理的这些图片的url地址。
3.3载入字体
同样的,字体也可以直接使用资产模块来加载:
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
}
通过在项目中引入字体文件(woff和woff2),再在css文件中引入,例如:
@font-face {
font-family: 'MyFont';
src: url('./my-font.woff2') format('woff2'),
url('./my-font.woff') format('woff');
font-weight: 600;
font-style: normal;
}
.hello {
color: red;
font-family: 'MyFont';
background: url('./icon.png');
}
最后直接打包项目即可。
3.4载入数据
webpack默认是支持加载json文件的,但是其他的数据文件,例如CSVs
, TSVs
和XML
这些文件,就需要使用其他加载器。
npm install --save-dev csv-loader xml-loader
在webpack中配置:
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
},
{
test: /\.xml$/i,
use: ['xml-loader'],
},
在项目里导入这类数据以后,运行打包命令即可。
其他类型的数据,例如toml
, yaml
, json5
等,可以加载后作为JSON模块来使用,但是要使用到自定义的解析器。
npm install toml yamljs json5 --save-dev
然后在webpack中添加配置:
{
test: /\.toml$/i,
type: 'json',
parser: {
parse: toml.parse,
},
},
{
test: /\.yaml$/i,
type: 'json',
parser: {
parse: yaml.parse,
},
},
{
test: /\.json5$/i,
type: 'json',
parser: {
parse: json5.parse,
},
},
最终运行打包命令即可。
4.输出管理
4.1HtmlWebpackPlugin
如果要更改webpack的入口,而不想动输出目录下index文件里面的旧引用。则需要配置使用HtmlWebpackPlugin
插件。
npm install --save-dev html-webpack-plugin
然后在web pack.config.js文件里面配置:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
title: 'Output Management',
}),
]
};
通过这样的配置以后,运行打包,此插件将在输出目录下生成一个index.html
文件(若文件已存在,将替代原文件)。
更多有关配置,请查看这里。
4.2清理/dist
目录
默认情况家,/dist
目录里面的文件不会被清理,每次运行打包,生成的新文件和旧文件会混合在一起。为了让每次打包都自动的清理旧的文件,可以通过配置来实现。
{
output: {
...
clean: true
}
}
4.3生成清单(Manifest)
如果要理清开发的模块和生成的目录下的文件的对应关系,可以使用WebpackManifestPlugin
插件。
5.开发工具
5.1源代码映射
当项目被打包到一个文件(bundle.js)里面以后,源文件里面包含有错误,那么在bundle.js文件里的栈追踪就会很困难。为了更容易地找到问题和警告,javascript提供了源映射(source map),它能映射编译后的代码和源代码。
简单的配置如下:
module.exports = {
...
devtool: "inline-source-map",
}
编译以后,在浏览器中打开,任何问题都能映射到源代码中。上面例子中的inline-source-map
是远吗映射中的一个工具,还有很多,可以参阅这里。
5.2开发选项
如果在开发过程中每次添加代码或者修改代码都要去运行npm run build
, 那么开发效率将会很低下。webpack提供了几个配置选项供开发者使用:
5.3监视模式
有多种方式启动这个模式,可以直接在webpack.config.js文件里使能watch:true
,当项目在第一次编译以后,项目就会持续去监视变化过程。另外一只是直接在命令行启动过程中添加参数npx webpack --watch
。
另外,在webpack-dev-server和web pack-dev-middleware两种开发工具激活的情况下,监视模式是自动打开的。
5.4webpack-dev-server
这个工具为开发者提供一个基本的web服务器,能够实时的重加载。
npm install --save-dev webpack-dev-server
修改配置文件web pack.config.js
:
module.exports={
devServer: {
static: './dist',
},
optimization: {
runtimeChunk: 'single'
}
}
如果项目中有多个入口则需要配置Optimization.runtimeChunk: 'single'
以避免运行时错误。
最后在package.json文件的script项添加运行命令:
"scripts": {
"start": "webpack serve --open"
}
更多关于dev-server的配置,请参阅这里。
5.5webpack-dev-middleware
这个包装器会把处理的文件发射到服务器。它是在webpack-dev-server
里面使用的,然而它可以作为独立的包来支持自定义的设置。以下例子演示结合一个express服务器的用例(把文件发射到express服务器里):
npm install --save-dev express webpack-dev-middleware
设置webpack.config.js
配置:
module.exports = {
...
output: {
...
publicPath: '/'
}
}
在脚本里使用的publicPath
确保了文件在http://localhost:3000
上运行正确,下一步配置express服务器:
在根目录下创建server.js
文件:
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// Serve the files on port 3000.
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
最后在package.json
文件里添加配置,并运行npm run server
就可以了。
{
"scripts": {
"server": "node server.js"
}
}
有关热模块替换的使用,请参阅这里。
6.代码分割
当打包的代码过大,影响性能和效率的时候,就可以把打包的程序分割成多个小包,分情况加载。常用的代码分割方式有:
- 多入口(entry point)
- 防止重复(prevent duplication)
- 动态导入(dynamic imports)
6.1多入口
直接在webpack里面配置多个入口即可。但是这种方式有两个缺点:
- 如果多个入口块(entry chunks)之间重复地用了多个模块,则这些模块都将被包含在这些块里;
- 不够灵活,且不能用核心应用逻辑来动态的分割代码
6.2防止重复
通过配置dependOn
选项来共享多个包之间的模块,当有多个入口块时,还需要配置optimization.runtimeChunk:'single'
。
const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
runtimeChunk: 'single'
}
};
通过以上的配置,shared会是一个单独的块,其他依赖它的块都会从它这里获取引用。
6.3提取公共依赖
使用它可以把公共的依赖提取出来放到一个现有的入口块或者新的入口块里面,通过SplitChunksPlugin实现。
const path = require('path');
module.exports = {
...
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
6.4动态导入
动态导入主要有两种方法,第一种,也是推荐的方法,使用import()
语法;第二种,采用require.ensure
。
6.5包分析
这里有一些官方的分析工具。
7.ESM模块支持
默认情况下,webpack会把ESM模块导出成其它类型的模块,也会把其他模块导入成ESM类型。webpack也能自动检测文件是ESM类型还是其他类型。
NodeJs在package.json文件里面预留了"type":"mode"
来强制将所有当前目录之下的文件转换成ESM模块,果"type":"commonjs"
,则将所有转化成commonjs类型。
7.1支持TypeScript
首先安装相关依赖:
npm install --save-dev typescript ts-loader
项目中添加tsconfig.json
文件,然后配置以下必备内容:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
最后在webpack.config.json
文件中添加配置:
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
};
8.缓存
为了加快服务器读取项目的时间,减少网络流量,webpack提供缓存机制。
8.1文件名字
可以使用output.filename
来定义输出的文件的名字。webpack提供了一种使用括号括起来的名字作为模版文件名的方法,叫做替换法(substitutions)。[contenthash]
将会被基于文件内容的哈希名替换。文件中的任何内容改变之后,哈希名都会改变。同样的,[name]
将会被原文件的名字替换掉。
8.2提取样板
利用splitChunksPlugin
插件将运行时代码分离出来放到一个隔离的块里面。只需要配置optimization.runtimeChunk
选项single
即可。
module.exports = {
"optimization": {
"runtimeChunk": "single"
}
}
如果要把第三方的库剥离出来放到一个单独的包里(vendor),就能提高开发效率,用户就可以一直缓存着第三方的库,更新那些经常改变的包。要达到这样的效果,只需要通过以下配置即可:
module.exports = {
optimization: {
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
}
}
8.3模块标识符
如果项目中任何位置的代码发生了变化,将引起整个项目生成的文件都变化,因为每个模块的标识符默认基于解析顺序都会自增,即每次解析顺序改变ID都会改变。为了防止这样的情况,webpack提供以下配置:
module.expects = {
//...
optimization: {
moduleIds: 'deterministic'
}
}
9.环境变量
通过CLI设定的环境变量可以被webpack.config.js配置文件读取到,从而有针对性的运行项目。
环境变量在CLI中的设定是通过在--env
后面跟上变量设定来实现的。
webpack.config.js
const path = require('path');
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log('Goal: ', env.goal); // 'local'
console.log('Production: ', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
10.解析
这个选项用来配置模块解析的方式。通常的形式如下:
module.exports = {
//...
resolve: {
// configuration options
},
};
10.1别名
创建别名,以更容易地导入模块。例如创建以下路径的别名:
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/')
},
},
};
如上使用Utilities
代表src/utilities/
路径,在需要导入的地方就可以直接使用别名:
import Utility from 'Utilities/utility';
11.模块
在模块化编程中,开发人员将程序分解成离散的功能块,叫做模块。编写良好的模块提供了坚实的抽象和封装边界,每个模块在整个应用中具有连贯的设计和明确的目标。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。