
注意,本文使用 webpack5.68.0,不同版本可能会有细微区别。
webpack是一个模块化打包 JavaScript 的工具,在 Wepack 里一切文件皆模块,通过Loader翻译转化文件,通过Plugin注入钩子,最后输出多个模块组合的文件,而Webpack专注于构建模块化项目。官网的首页图说明了Webpack是什么:

对于webpack的基础配置及概念可以从官网文档中学习,本文列举webpack的在生产环境和开发环境中通用的Plugin和Loader及配置。
本文后续会出现经常提及chunk和bundle在这里我们先解析一下这个概念
chunk从字面意思是代码块,在Webpack中chunk代表许多关联module的集合。比如在webpack.config.js声明了入口模块entry,入口模块又关联了其他模块,这一系列关联的模块就是一个chunk。
在webpack中产生chunk有三种途径。
在webpack.config.js中可以声明entry来指代入口模块,entry合法类型有string | Array | object
entry字段值如果是string | Array会产生一个名为main的chunk;如果是Array则将数组里的源代码都打包到一个chunk中。如下:
module.exports = {
entry: "./src/main.js",
entry: ["./src/main.js", "./src/other.js"],
// ...
};
如果是一个对象,则会声明与key数量相同chunk,并且会使用key来作为chunk的名称。这就是为什么entry是对象,output.filename如果写死名称会直接报错的原因,因为一个名称不够让多个chunk输出。
module.export = {
entry: {
main: "./src/main.js",
other: "./src/other.js",
},
output: {
path: path.join(__dirname, "./dist"),
// 会产生 main.js other.js两个文件
filename: "[name].js",
},
};
按需加载(异步)加载的模块也会产生chunk,这个chunk名称可以在代码中使用webpackChunkName自行定义。如果需要打包的时候使用chunk名称,则需要在output.chunkFilename中引用。
// webpack.config.js
module.export = {
output: { chunkFilename: "[name].js" },
};
// module
import(/* webpackChunkName: "async-model" */ "./async-model");
在webpack5中代码分割使用SplitChunksPlugin插件实现,这个插件内置在webpack中,在使用时直接用配置的方式即可。代码分隔时也会产生chunk,我们用代码来说明一下:
// webpack.config.js
module.export = {
entry: {
a: "./src/a.js",
b: "./src/b.js",
},
optimization: {
// 分隔webpack运行时代码
runtimeChunk: "single",
splitChunks: {
chunks: "all",
// 分隔组
cacheGroups: {
// 抽取第三方模块
vendors: {
test: /node_modules/,
prioity: -10,
reuseExistingChunk: true,
},
// 抽取
commons: {
minSize: 0, // 抽取的chunk最小大小
minChunks: 2, // 最小引用数
prioity: -20,
reuseExistingChunk: true,
},
},
},
},
};
// a.js
import "c.js";
import $ from "jquery";
console.log($);
// b.js
import "c.js";
// commons.js
console.log("hello");
上方代码使用了代码分隔,总共会产生 6 个chunk
a和b的chunkruntimeChunk声明抽离webpack运行时代码抽离到一个唯一的名称的chunk,我们有两个入口,有两份运行时chunkjquery符合cacheGroups.vendors规则,抽离到名为vendors的chunkcommons.js符合cacheGroups.commons规则,抽离到名为commons的chunk如下图所示

bundle就是我们最终输出的一个或多个文件,大多数情况下一个chunk至少会产生一个bundle,但是不完全是一对一的关系。比如我们在模块中引用图片又经过url-loader打包到外部;或者是引用了样式,通过extract-text-webpack-plugin抽离出来,这样一个chunk就会出现产生多个bundle的情况。
简单来说bundle就是chunk在构建完成的呈现。
entry是webpack的启动模块入口,webpack将根据指定的这个起点来查找模块,生成chunk,entry的合法值有:
string,入口起点的模块路径,webapck将生成名为main的chunkArray,入口起点的一组模块的路径,webpack将组的中每个模块拼合在一起,并生成名为main的chunk{ [key in string]: string | Array },多个入口起点,webpack根据value (跟 1,2 合法值一致) 为入口起点,key为名称的chunk。function,获取入口起点的方法,返回值为 1,2,3 或Promise<1,2,3>,我们可以指定动态入口。module.exports = {
entry: ["./app/entry1", "./app/entry2"],
entry: {
a: "./app/entry-a",
b: ["./app/entry-b1", "./app/entry-b2"],
},
entry: () => "./app/entry-a",
entry: () => new Promise((resolve) => resolve("./app/entry-a")),
// ...
};
我们看到上方代码的入口模块值都是相对路径,默认情况下webpack想以当前目录为基础路径来查找。我们可以通过context来更改基础路径。
webapck查找loader和启动入口模块时,会以配置中的context为基础目录。下面我们来改上方代码
module.exports = {
context: path.join(__dirname, "./app"),
entry: ["./entry1", "./entry2"],
entry: {
a: "./entry-a",
b: ["./entry-b1", "./entry-b2"],
},
entry: () => "./entry-a",
entry: () => new Promise((resolve) => resolve("./entry-a")),
// ...
};
默认情况下webpack会在dist输出chunk生成的bundle,文件名就是chunk名称。
我们可以通过output.filename来修改非按需加载chunk生产bundle的名称。output.filename的合法值有string | (pathData: PathData) => string,默认值为[name].js。
如果我们的项目中只有一个非按需加载的chunk(几乎不存在),可以使用静态名称来定义生产的bundle名称,如果有多个chunk就不行了,因为多个chunk无法放到一个bundle中
module.exports = {
output: {
filename: "bundle.js",
},
};
我们可以使用webpack内提供的模板字符串来定义bundle文件名,下表列举了常用的模板字符串:
| 模板 | 描述 | 稳定性 |
|---|---|---|
| [name] | chunk的名称 |
只要chunk名称不修改就不会变化 |
| [hash] | 根据所有chunk生成的hash |
工程中某个chunk被修改就会引起变化 |
| [chunkhash] | 根据chunk生成的hash值 |
某个chunk被修改,只会引起被修改chunk的hash |
| [contenthash] | 根据bundle内容内容产生的hash |
chunk中某个bundle被修改,只会引起被修改bundle的hash |
下面代码例举了如何使用模板。
module.exports = {
outpit: {
// 直接使用
filename: "[name].js",
filename: "[hash].js",
filename: "[chunkhash].js",
filename: "[contenthash].js",
// 组合使用
filename: "[name]-[contenthash].js",
// 限定hash位数
filename: "[hash:5].js",
filename: "[contenthash:5].bundle.js",
},
};
在使用[hash]时需要注意,因为它是所有chunk都共享的,所以直接用[hash]可能会引起错误,跟上方使用静态变量一样,多个chunk无法放置到一个bundle。
前面我们讲解了filename的配置方式,它们对非按需加载(异步)的chunk生效,如果需要对按需加载的chunk配置,那就需要用到outpit.chunkFilename了,它和filename的使用方式是一样的,这里不在赘述。
module.exports = {
outpit: {
chunkFilename: "[name].js",
chunkFilename: "[hash].js",
chunkFilename: "[chunkhash].js",
chunkFilename: "[hash:5].js",
chunkFilename: "[contenthash:5].bundle.js",
},
};
默认情况下webpack会将打包的文件输出到dist文件夹中,我们可以通过配置output.path来更改输出目录。
在path可以使用[hash]的模板字符串,因为每次文件修改后hash都不一样,很容易就能生成所有版本构建后代码文件。
const path = require("path");
module.exports = {
output: {
path: path.join(__dirname, "./dist-[hash]"),
},
};
如果要实现缓存目的,可以使hash来输出bundle。由于每次打包后的hash都可能会更改,在html手动引入bundle很麻烦,我们可以借助html-webpack-plugin插件来实现自动引入bundle。
html-webpack-plugin的使用方式非常简单,只需创建一个实例,放置到plugins中就会自动生成在output.path中生成一个默认的html,这个html将会自动引入所有chunk生成的bundle。接下来我们创建最一个简单的使用。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// ...
plugins: [new HtmlWebpackPlugin()],
};
在创建html-webpack-plugin实例时可以传入许多参数,可以通过官方文档了解,接下来我们看看最常用的几种定制。
template参数可以指定生成html的模板,我们可以在这个html中编写自己需要的东西。filename参数可以指定html-webpack-plugin将html输出到哪里,这个参数的配置方式跟outpit.filename一样。minify参数可以决定是否压缩html,在production模式下会自动为true。
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "assets/template.html"),
filename: "index.html",
minify: true,
}),
],
};
上面我们说的html-webpack-plugin会自动产生一个html并引入所有chunk,这很适合SPA (单页面) 开发。而MPA需要多个html,而且每个html需要的chunk也不一样,接下来我们看看这个插件是如何适配MPA开发的。
一个html-webpack-plugin实例会生产输出一个html,多个实例就会生产输出多个html,只需要在plugins中添加多个实例即可。html-webpack-plugin中有个chunks参数来指定html要关联的所有chunk。
module.exports = {
content: path.join(__dirname, './src'),
entry: {
login: './login.js',
home: './home.js',
base: './base.js'
}
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'assets/template.html'),
filename: 'login.html',
chunks: ['login', 'base'] //需要关联的所有chunk
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'assets/template.html'),
filename: 'home.html',
chunks: ['home', 'base'] //需要关联的所有chunk
})
]
}
如果我们把bundle都上传到了cdn上,就需要对打包后的资源引入路径进行修改,我们可以通过publicPath参数给我们指定资源的前缀路径。
使用cdn一般会产生缓存问题,如果你的output.filename没有使用hash那更新后的文件不会立即生效。我们可以使用hash参数给引入路径加上此次构建缓存。 (更好的方式是使用 output.filename),因为如果使用构建缓存意味着每次更新所有缓存都会被刷新。
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "assets/template.html"),
publicPath: "//www.cdn.com",
hash: true,
filename: "login.html",
chunks: ["login", "base"], //需要关联的所有chunk
}),
],
};
html-webpack-plugin的template是使用ejs 模板语言。html-webpack-plugin注入了许多变量给模板使用,我们可以直接在模板内使用这些变量来自定义扩展我们的html。
| 变量 | 描述 |
|---|---|
htmlWebpackPlugin.options |
创建HtmlWebpackPlugin实例的参数 |
htmlWebpackPlugin.files |
htmlWebpackPlugin准备注入的bundle,如果inject为true则自动注入 |
webpackConfig |
webpack的配置 |
compilation |
webpack的编译对象 |
htmlWebpackPlugin.files的类型为
type File {
publicPath: string;
js: string[];
css: string[];
manifest?: string;
favicon?: string;
}
接下来我们做个 demo,关闭inject,手动将webpack打包后的css和js都内联到html中:
// webpack.config.js
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "assets/template.ejs"),
inject: false,
title: 'login',
filename: "login.html",
chunks: ["login", "base"], //需要关联的所有chunk
}),
],
};
<!-- template.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> <%= htmlWebpackPlugin.options.titlea %> </title>
<% for (cssFile of htmlWebpackPlugin.files.css ) { %>
<style>
<%= compilation.assets[cssFile.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</style>
<% } %>
</head>
<body>
<% for (jsFile of htmlWebpackPlugin.files.js ) { %>
<script>
<%= compilation.assets[jsFile.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
<% } %>
</body>
</html>
SplitChunksPlugin是改进CommonsChunkPlugin而重新设计和实现的代码分片插件。SplitChunksPlugin可以配置式的定义如何抽取chunk,这是个十分有用的功能。
chunk都会有,默认情况下生成的两个bundle都会包含这个公用模块的内容,我们可以通过SplitChunksPlugin将公用模块单独抽成一个chunk,生成三个bundle,两个入口bundle引用这个公用的bundle。node_modules) 通常情况下是不怎么会有大的变动的,为了充分利用缓存,我们可以把项目业务的代码和第三方库代码分成两个chunk,生成两份bundle,这样我们修改业务代码,第三方库的代码并不会变动,也就能利用上缓存了。 (需要将output.filename修改成[chunkhash|contenthash].js)在默认情况下,webpack只会分片按需加载 (只针对import(...)异步加载) 的chunks。影响范围是在optimization.splitChunks.chunks中声明的,他们分别表示:
initial只影响入口chunk关联async (默认) 只影响按需加载的chunkall 入口和按需加载都影响默认情况下的提取规则及主要配置代码:
chunk被多次引用或是来自node_modulesjs chunk体积大于30Kcss chunk体积大于50K// splitChunks 默认配置
const options = {
// 工作于哪里 async initial(只对入口的chunk生效) all: async + all
chunks: 'async',
// 抽取时chunk最小大小
minSize: 20000,
// 另一种方式,指定模块类型
// minSize: {
// javascript: 300000,
// style: 500000
// },
// 最少被多少个chunk使用
minChunks: 1,
// 按需加载并行数最小值
maxAsyncRequests: 30,
// 首次加载最大并行数
maxInitialRequests: 30
// 拆分后体积最小多少
minRemainingSize: 0,
// 体积大于多少强制拆分
enforceSizeThreshold: 50000,
// 名称分隔符如模块A被chunk B和chunk C同时引用名称可能是default~B~C
automaticNameDelimiter: '~',
// 自定义抽取的chunk的名称,切忌不要设置固定值,因为chunk名称相同所有分离代码将会合并
// 设置为false 将使用合并chunk的名称并用automaticNameDelimiter作为分隔生成为chunk名称
name: false,
// 上方是公用配置,可以自定义组配置,如果没有覆盖则会集成上方配置
// key 为抽取后的chunk name
cacheGroups: {
// 没覆盖的会继承上方配置,比如minChunks: 1,
vendors: {
// group 特有属性 提取第三方模块
test: /[\\/]node_modules[\\/]/,
// 优先级,优先级会影响webpack选中哪个group
// 比如一个chunk即符合default规则也符合vendors规则,则查看优先级,哪个高用哪个
priority: -10,
// 告诉webpack强制拆分,忽略除test外条件
enforce: false
},
// 多次引用提取
default: {
// 最小被两个chunk引用
minChunks: 2,
// 优先级
prority: -20,
}
}
}
更多相关配置查看官网
一些历史项目上有些第三方库不支持模块化,或者因为其他局限性不能模块化,只能全局导入。比如jquery如果使用模块化方式导入,有第三方的插件将找不到它,因为它们是直接用全局版本的。如果直接在全局上引用jquery是可以解决问题,但我们项目上良好的ts提示也没有了。我们可以使用webpack的externals属性来解决这类问题。
externals能放置将import的包打包到bundle中,而是在运行时根据配置找到我们声明的扩展依赖并使用它。
module.exports = {
// ...
externals: {
// key 为我们import的包名, value 为全局中变量名
jquery: 'jQuery',
}
}
// 使用
import { ajax } from 'jquery'
import $ from 'jquery'
// 将会转化为 jQuery.ajax({ ... })
ajax({ ... })
// 将会转化为 jQuery('#app')
$('#app')
更多相关内容参考官网
如果我们的项目非常复杂而且项目路径比较深就可以考虑建立访问快捷路径,比如我们项目结构如下
project
|---packages
|-----components
|-------assets
| 1.png
|-----sdk
|-------config
| default.json
|-----pro1
| main.js
|-----pro2
| scripts
| webpack.config.js
在main.js中要访问components中的1.png就需要import img from '/packages/components/assets/1.png',路径非常的长。为了解决这个问题我们可以使用webpack的resplve.alias。
module.exports = {
resolve: {
alias: {
'@ui': path.resolve(__dirname, './packages/components'),
'@sdk': path.resolve(__dirname, './packages/sdk'),
// 末尾用$标识精准匹配 只匹配 import '@sdkConfig'
// import '@sdkConfig/xxx'不会流向这里
'@sdkConfig$': path.resolve(__dirname, './packages/sdk/config/default.json')
}
}
}
然后我们就可以直接使用import img from '@ui/assets/1.png'了。
在ES6发布以后,TC39规定每年都会发布新的版本。通常把ES5及其之前的版本统称做ES5。为了明确区分各个版本的内容,可以按照版本发布的时间进行描述,比如ES2015,ES2016。虽然目前部分浏览器都开始支持了,但由于各个浏览器标准支持不全,以及新特性支持没那么快,这导致在开发中不敢全面地使用新特性。
通常我们需要给新的API注入polyfill或者把新的ES6语法用ES5来使用新特性,babel就可以方便的完成。
Babel是一个JavaScript编译器,能将新特性语法代码转为ES5代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。在于webpack结合时需要使用babel-loader作为桥梁。
在编写配置之前我们需要安装babel核心库@babel/core,babel各个新特性转换的库@babel/preset-env以及babel-loader
npm install --save-dev @babel/core @babel/preset-env babel-loader
module.exports = {
// ...
model: {
rules: [
{
test: /.js/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
]
}
}
preset-env会根据你编写代码按需加载支持代码,在没有配置的情况下会转换ES2015及以上的所有特性。我们可以通过targets参数来传递需要打包的目标环境,babel会根据caniuse的数据来决定是否需要转换。
module.exports = {
// ...
model: {
rules: [
{
test: /.js/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
// 指定目标环境
targets: {
{
// chrome: '90',
// ie: 11
// node: 'current',
browsers: ['last 2 versions', 'ie > 10']
}
}
]
}
}
]
}
}
默认情况下babel的所有转换代码都会直接内联到js bundle中,假如我们有多个chunk那每个chunk生成的js bundle都有转换代码,我们需要将他们统一抽离,减少js bundle的大小。我们可以使用babel提供的@babel/plugin-transform-runtime插件解决这一需求
npm install --save-dev @babel/plugin-transform-runtime
module.exports = {
// ...
model: {
rules: [
{
test: /.js/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
targets: {
{ browsers: ['last 2 versions', 'ie > 10'] }
}
],
plugins: ['@babel/plugin-transform-runtime']
}
}
]
}
}
babel转换是件耗时的事,我们可以通过babel-loader的cacheDirectory来开启缓存,不变动的文件没必要再次编译,加快编译。默认情况下缓存文件会存放在node_modules/.cache中。
module.exports = {
// ...
model: {
rules: [
{
test: /.js/,
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
targets: {
{ browsers: ['last 2 versions', 'ie > 10'] }
}
],
plugins: ['@babel/plugin-transform-runtime'],
cacheDirectory: true
}
}
]
}
}
资源模块是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外。在webpack5中使用资源模块来代替raw-loader、url-loader、file-loader管理资源模块。
资源模块类型总共有4中类型来替换这些loader
asset/resource 发送一个单独的文件并导出URL。之前通过使用file-loader实现。asset/inline 导出一个资源的data URI。之前通过使用url-loader实现。asset/source 导出资源的源代码。之前通过使用raw-loader实现。asset 在导出一个data URI和发送一个单独的文件之间自动选择,默认下小于8k的将视为inline。之前通过使用url-loader,并且配置资源体积限制实现。module.exports = {
// ...
module: {
rules: [
{
// 模型文件,统一导出为 URL
test: /.(obj|mtl)/i,
type: 'asset/resource',
// 覆盖outpit.assetModuleFilename 自定义导出bundle路径
generator: {
filename: 'static/model/[hash][ext]'
}
},
{
// 图片文件,如果大于 10KB将导出为URL否则内联源代码
test: /\.(png|jpg|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024
}
}
},
{
// svg比较特殊,没有指定要raw时用URL,指定时用源码,所以需要二选一rules
oneOf: [
{
test: /.svg$/i,
// 匹配模块引用路径query是否有raw,如果有则返回源码
// 如 import svg from 'a.svg?raw'
resourceQuery: /raw/,
type: 'asset/source'
},
{
test: /.svg$/i,
type: 'asset/resource'
}
]
}
],
output: {
// 输出asset资源
assetModuleFilename: 'images/[hash][ext]'
}
}
}
outpit.assetModuleFilename和generator.filename与output.filename相同不过适用于Asset Modules,用法参照上方output.filename。
如果不是写lib库,那css对于前端来说是必不可少的,对于webpack来说一切皆模块,我们只需要在loader中定义好css支持,即可引入css文件。
module.exports = {
// ...
module: {
rules: [
{
test: /.css/,
use: ['style-loader', 'css-loader']
}
]
}
}
上面使用了css-loader来解析css模块生成代码文件,style-loader将生成的代码自动嵌入到html。但是这样有个问题,css文件无法使用缓存,我们需要将生成的css代码抽出到一个bundle中。mini-css-extract-plugin插件就能帮助我们完成这个任务。
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
// ...
module: {
rules: [
{
test: /.css/,
use: [
{ loader: MiniCssExtractPlugin.loader },
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'style/[name]-[contenthash:5].css',
chunkFilename: 'style/[name]-[chunkhash:5].css'
})
]
}
其中创建mini-css-extract-plugin实例的参数的filename和chunkFilename可配置的值与output.filename是一样的。filename表示输出非按需加载的chunk中css的bundle,chunkFilename表示输出按需加载的chunk中css的bundle。
我们知道开发大部分时候css选择器是根据类名去匹配元素的,如果工程比较大多人开发,或者有两个组件使用了一个相同的类名,后者就会把前者的样式给覆盖掉,为了解决这个问题产生出了CSS模块化概念。
其实css-loader已经内置了模块化功能,在默认情况下,css-loader会用/\.module\.\w+$/i.test(filename)匹配文件名,如果匹配上了就开启模块化。也就是说我们的css文件名以module.css结尾即可开启模块化。开启模块化的css文件,模块中 (css文件) 每个类选择器和id选择器都会替换成hash名称,如果我们不需要替换可以使用:global()来包裹住不需要替换的选择器。
/* login.module.css 打包前 */
.name {
width: 1px;
}
.user-age {
width: 1
}
#pwd {
width: 1px;
}
.user .age div {
width: 1px;
}
.user :global(.age) div {
width: 2px;
}
/* 打包后 */
.ah4VvUCzdSHinav53q40 {
width: 1px;
}
.M7m9Yez7JZkfnGJZgkWc {
width: 1
}
#UB1EFfX0F7izHsHajHV8 {
width: 1px;
}
.Oos422SahRxya3J3gTZ7 .cs0aFUaAnLU8hThBYcPk div {
width: 1px;
}
.Oos422SahRxya3J3gTZ7 .age div {
width: 2px;
}
由于打包后的名称会更改,无法引用,所以我们使用时需要换一种使用方式。使用css-loader为我们生成的对象来引用名称。
import style from '../style/login.module.css'
const $app = documnet.querySelect('#app')
$app.innerHTML = `
<div>名称:<input class="${style.name}"></div>
<div>年龄:<select class="${style['user-age']}"></select>
`
虽然上面开启了模块化,但是我们需要对其进行优化,给css-loader传入参数
css只会产生一个默认对象,来引用所有生成名称,如果我们想用es5 module需要将namedExport属性改为true。localIdentName属性,localIdentName也是字符串模板,下面列出可支持使用模板:compiler.context或者modules.localIdentContext配置项的相对路径。compiler.context或者modules.localIdentContext配置项的相对路径。localIdentHashSalt、localIdentHashFunction、localIdentHashDigest、localIdentHashDigestLength、localIdentContext、resourcePath和exportName生成。css文件时一般采用-来代替驼峰命名,我们需要将-转化为驼峰,需要将exportLocalsConvention属性改为camelCaseOnlymodule.exports = {
// ...
module: {
rules: [
{
test: /.css/,
use: [
{
loader: 'css-loader',
options: {
// 模块化定制
modules: {
// 更改模块化导出的样式名称 使用文件名称加原始类名加hash
localIdentName: '[name]__[local]--[hash:base64:5]',
// 驼峰化对象,并保留原始key 比如 userAge user-age都可用
exportLocalsConvention: 'camelCaseOnly',
// 启用es5 模块
namedExport: true
}
}
}
]
}
]
}
}
接下来我们看看更换后打包结果,
.login-module__name--ah4Vv {
width: 1px;
}
.login-module__user-age--M7m9Y {
width: 1
}
#login-module__pwd--UB1EF {
width: 1px;
}
.login-module__user--Oos42 .login-module__age--cs0aF div {
width: 1px;
}
.login-module__user--Oos42 .age div {
width: 2px;
}
使用方式
import { name, userAge } from '../style/login.module.css'
const $app = documnet.querySelect('#app')
$app.innerHTML = `
<div>名称:<input class="${name}"></div>
<div>年龄:<select class="${userAge}"></select>
`
要使用scss或less就首先要把这两种预处理语言转化成css语言,scss使用node-sass库来转化,而less则是使用less库。转化成功后要将结果给到webpack使用,我们可以使用sass-loader和less-loader来完成这个工作。我们看看怎么集成
npm install --save-dev less-loader less
npm install --save-dev sass-loader node-sass
const cssRuleLoader = [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
namedExport: true
}
}
}
]
module.exports = {
// ...
module: {
rules: [
{
test: /.less/,
use: [
...cssRuleLoader,
'less-loader'
]
},
{
test: /.scss/,
use: [
...cssRuleLoader,
'sass-loader'
]
}
]
}
}
scss和less同样也可以开启模块化,我们前面说了只要符合/\.module\.\w+$/i.test(filename)即可,所以我们只需将文件名改为login.module.scss或login.module.less即可开启模块化。
PostCSS是一个CSS处理工具,它通过插件机制可以灵活的扩展其支持的特性。目前用法最广泛的就是为CSS自动添加厂商前缀、使用下一代CSS语法,然后转化为现代浏览器能识别的语法,这个工作可以借助postcss的postcss-preset-env插件完成。使用时需要将样式内容传入postcss库,然后将生成的内容提供给webpack,需要使用postcss-loader完成这一工作。
npm install --save-dev postcss-loader postcss postcss-preset-env
const cssRuleLoader = [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
namedExport: true
}
}
},
// 在传入webpack前需要先使用postcss转化
{
loader: 'post-loader',
options: {
// postss需要使用的插件
plugins: [
// 数组,第一个使用插件,第二个参数
[
'postcss-preset-env',
{
stage: 2,
browsers: {
// 正式环境
production: [ '> 0.2%', 'ie > 10' ],
// 开发环境
development: [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
]
]
}
}
]
stage决定哪些css特性需要被polyfill,他们的值分别代表
browsers来配置需要支持的浏览器环境,其中数据和是否需要加前缀是根据caniuse决定的。查看的更多browsers定义规则
如果我们使用上方的配置在样式文件中使用@import导入其他样式文件时会有出乎预料情况发生。我们看看打包情况
/* ------------打包前------------- */
/* base.scss */
* {
margin: 0;
padding: 0;
border: 0;
}
.user {
.name {
color: #12312312;
}
display: flex;
}
:fullscreen {
height: auto;
}
/* login.scss */
@import url('./base.scss');
.name {
.age {
background: #12312312;
height: 100px;
}
}
:global(.age) {
display: flex;
}
:fullscreen {
height: 100px;
}
/* ---------------------------- */
/* ------------打包后------------- */
* {
margin: 0;
padding: 0;
border: 0;
}
.base__user--VIoKM {
.base__name--BOmiJ {
color: #12312312;
}
display: flex;
}
:fullscreen {
height: auto;
}
.home-module__name--x_p3i .home-module__age--jCPqC {
background: rgba(18,49,35,0.07059);
height: 100px; }
.age {
display: -ms-flexbox;
display: flex; }
:-webkit-full-screen {
height: 100px; }
:-ms-fullscreen {
height: 100px; }
:fullscreen {
height: 100px; }
/* ---------------------------- */
我们看到被@import进来的样式文件样式开启了模块化,证明它经过了css-loader处理。但是并有将scss转化为css,该添加的前缀也被加,并没有被postcss-loader和scss-loader处理。这是因为@import是在css-loader中处理的,默认导入的css只从当前loader开始处理,然后流向下一个loader。
css-loader有一个参数可以定义一个参数来配置由@import进来的文件如何处理,importLoaders设置在css-loader之前应用的loader的数量。上方的情况在css-loader之前还需要应用到postcss-loader和scss-loader所以应该设置为2。
const cssRuleLoader = [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
namedExport: true
}
}
},
// 在传入webpack前需要先使用postcss转化
{
loader: 'post-loader',
options: {
// postss需要使用的插件
plugins: [
// 数组,第一个使用插件,第二个参数
[
'postcss-preset-env',
{
stage: 2,
browsers: {
// 正式环境
production: [ '> 0.2%', 'ie > 10' ],
// 开发环境
development: [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
]
]
}
}
]
更改配置后的打包结果就正常了:
* {
margin: 0;
padding: 0;
border: 0; }
.base__user--VIoKM {
display: -ms-flexbox;
display: flex; }
.base__user--VIoKM .base__name--BOmiJ {
color: rgba(18,49,35,0.07059); }
:-webkit-full-screen {
height: auto; }
:-ms-fullscreen {
height: auto; }
:fullscreen {
height: auto; }
.home-module__name--x_p3i .home-module__age--jCPqC {
background: rgba(18,49,35,0.07059);
height: 100px; }
.age {
display: -ms-flexbox;
display: flex; }
:-webkit-full-screen {
height: 100px; }
:-ms-fullscreen {
height: 100px; }
:fullscreen {
height: 100px; }
TypeScript是JavaScript的一个超集,主要提供了类型检查系统和对最新特性语法的支持。首先需要一个将ts编译成js的编译器typescript,然后将它集成到webpack中,使用ts-loader。
npm install --save-dev typescript ts-loader
我们还需要一个配置编译选项文件告诉typescript如何编译它,编译器默认会读取和使用在当前项目根目录下的tsconfig.json文件。
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2016",
"incremental": true,
"tsBuildInfoFile": "./node_modules/ts-cache",
"sourceMap": true,
"rootDir": "./",
"baseUrl": "./",
},
"include": ["src/**/*.ts"],
"exclude": []
}
然后我们需要在webpack中声明ts文件的处理
const jsUseLoader = () => ({
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
})
module.exports = {
// ...
module: {
rules: [
{
test: /.js/,
use: jsUseLoader()
},
{
test: /.tsx?$/,
use: [jsUseLoader(), 'ts-loader']
}
]
},
resolve: {
// 没有填写后缀时使用下列顺讯寻找
extensions: ['.ts', '.tsx', '.js']
}
}
下面是这个tsconfig.json文件的常用配置及说明:
{
"compilerOptions": {
/* -----------------------基本选项----------------------- */
// 开启增量编译:TS 编译器在第一次编译的时候,会生成一个存储编译信息的文件,下一次编译的时候,会根据这个文件进行增量的编译,以此提高 TS 的编译速度
"incremental": true,
// 指定存储增量编译信息的文件位置
"tsBuildInfoFile": "./node_modules/ts-cache",
// 控制编译后输出的是什么js版本,默认值为es3。
"target": "es2016",
// 指定要引入的库文件,默认值为 target === 'ES6' ? [DOM,ES6,DOM.Iterable,ScriptHost] : [DOM,ES5,ScriptHost]
// 可选值有
// JavaScript 功能: es5 es6 es2015 es7 es2016 es2017 esnext
// 运行环境: dom dom.iterable webworker scripthost
// ESNext功能选项: es2015.core es2015.collection es2015.generator es2015.iterable es2015.promise
// es2015.proxy es2015.reflect es2015.symbol es2015.symbol.wellknown es2016.array.include
// es2017.object es2017.sharedmemory esnext.asynciterable
"lib": ["esnext", "dom"],
// 是否对js文件进行编译,默认false
"allowJs": false,
// 报告 javascript 文件中的错误
"checkJs": false,
// 指定jsx代码用于的开发环境:'preserve','react-native',or 'react
// 'react' 模式下:TS 会直接把 jsx 编译成 js
// 'preserve' 模式下:TS 不会把 jsx 编译成 js,会保留 jsx
"jsx": "preserve",
// 生成相应的 '.d.ts' 文件
"declaration": false,
// 生成相应的 '.map' 文件
"sourceMap": true,
// 是否生成sourceMap,默认false
"sourceMap": false,
// 将输出文件合并为一个文件
"outFile": "./"
// 输入文件的根目录
"rootDir": ".",
// 指定编译结果的输出目录的,默认是将编译结果输出文件输出到源文件目录下
"outDir": "dist",
// 删除注释
"removeComments": false,
/* -----------------------模块解析处理----------------------- */
// 支持使用es模块引入commonjs包,并让ts默认处理
"esModuleInterop": true,
// 指定类型声明文件的查找路径。默认值为node_modules/@types
"typeRoots": ['node_modules/@types', './typings'],
// 配合typeRoots使用,指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载进来
"types": ["jest", "node"],
// 拓宽引入非相对模块时的查找路径的。其默认值就是"./",
// 如果找不到模块还会到baseUrl中指定的目录下查找
"baseUrl": ".",
// 配合baseUrl一起使用的,用于到baseUrl所在目录下指定的路径映射。
"paths": {
"@": ["src/"],
}
// 指定要使用的模块标准,如果不显式配置module,那么其值与target的配置有关,其默认值为target === "es3" or "es5" ?"commonjs" : "es6"
// 'None', 'CommonJS', 'AMD', 'System', 'UMD', 'ES6'/'ES2015', 'ES2020' or 'ESNext'
"module": "esnext",
// 模块的解析规则,默认值为 module ==="amd" or "system" or "es6" or "es2015"? "classic" : "node"
// classic
// 对于相对路径模块: 只会在当前相对路径下查找是否存在该文件(.ts文件)
// 非相对路径模块: 编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的ts文件或者d.ts类型声明文件
// node
// 对于相对路径模块:除了会在当前相对路径下查找是否存在该文件(.ts文件)外,还会作进一步的解析,如果在相对目录下没有找到对应的.ts文件,
// 那么就会看一下是否存在同名的目录,如果有,那么再看一下里面是否有package.json文件,然后看里面有没有配置,main属性,如果配置了,
// 则加载main所指向的文件(.ts或者.d.ts),如果没有配置main属性,那么就会看一下目录里有没有index.ts或者index.d.ts,有则加载。
// 对于非相对路径模块: 对于非相对路径模块,那么会直接到a.ts所在目录下的node_modules目录下去查找,也是遵循逐层遍历的规则,查找规则同上
"moduleResolution": "node",
// 允许导入.json文件
"resolveJsonModule": true,
/* -----------------------模块解析处理----------------------- */
// 开启全局严格检查,默认false
"strict": true,
// 是否检查检查未使用的局部变量,默认false
"noUnusedLocals": true,
// 不允许使用隐式的 any 类型
"noImplicitAny": false,
// 不允许把 null、undefined 赋值给其他类型变量
"strictNullChecks": false,
// 不允许函数参数双向协变 如下方将不被允许
// type fn = (a: number) => void
// let a: number | string
// fn(a)
"strictFunctionTypes": true,
// 使用 bind/call/apply 时,严格检查函数参数类型
"strictBindCallApply": true,
// 有未使用到的函数参数时报错
"noUnusedParameters": true,
// 不允许 switch 的 case 语句贯穿
"noFallthroughCasesInSwitch": true,
/* -----------------------其他选项----------------------- */
// 开启装饰器特性
"experimentalDecorators": true,
// 给源码里的装饰器声明加上设计类型元数据。
"emitDecoratorMetadata": false,
// 开启使用Object.defineProperty替换class声明中的字段
"useDefineForClassFields": false,
},
// 编译包含的源文件
"include": ["src/"]
// 需要排除的源文件
"exclude": ['src/test']
}
更全配置参考官网,接下来就要接入webpack
webpack一切皆模块可以在js中导入如png、css等文件在loader中解析,但是ts并不认识这些文件,如果不做处理直接引入会导致ts编译失败。我们需要对这些模块类型声明,ts并不处理这些模块,在转译成js后在webpack处理即可。
接下来我们声明一份global.d.ts然后将它放到源码根目录,ts就可以识别到他们了。
// global.d.ts
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
declare module '*.css'
declare module '*.sass'
declare module '*.scss'
declare module '*.less'
上方的类型声明只是让ts支持这些模块的导入,如果我们使用模块化css无法检测导入变量是否正确,也无法享受ts的提示,为了解决这个我们可以使用typescript的typescript-plugin-css-modules插件。首先我们需要安装它
npm install --save-dev typescript-plugin-css-modules
我们需要修改tsconfig.json来引用这个插件
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2016",
"incremental": true,
"tsBuildInfoFile": "./node_modules/ts-cache",
"sourceMap": true,
"rootDir": "./",
"baseUrl": "./",
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": {
// classname转化 与css-loader的exportLocalsConvention保持一致
"classnameTransform": "camelCaseOnly",
// 启用es5模块 与 css-loader的namedExport保持一致
"namedExports": true
}
}
]
},
"include": ["src/**/*.ts"],
"exclude": []
}
因为这个插件并不是针对真正ts编译的,而是针对vscode编辑器的,所以我们还需要将编辑器的当前使用的TypeScript的版本改成当前工作区版本。
ts文件{} Typescript选择版本
选择后如果没生效重启vscode因为有可能有缓存。然后就可以看到效果了

我们在webpack建立的快捷路径访问模块,typescript并不认识它,除了在webpack.config.js建立外,我们还需要在tsconfig.json中配置快捷路径访问模块,而且两者配置规则不太一样
"compilerOptions": {
"paths": {
"@ui/*": ["packages/components/*"],
"@sdk/*": ["packages/sdk/*"],
"@sdkConfig": ["packages/sdk/config/default.json"]
},
}
如果可能我们应该尽可能只保留一份配置文件,这样方便管理,我们保留tsconfig.json版本,然后写一个函数来生成webpack的resolve.alias。我们可以用convert-tsconfig-paths-to-webpack-aliases包来做这件事。
npm install --save-dev convert-tsconfig-paths-to-webpack-aliases
const tsconfigPathToAlias = require('convert-tsconfig-paths-to-webpack-aliases').default
const tsconfig = require('./tsconfig.json')
const aliass = tsconfigPathToAlias(tsconfig)
module.exports = {
// ...
resolve: {
alias: aliass
}
}
在没有使用vue或react等框架提供方便模板和jsx环境下编写项目下,编写html可能会显得比较吃力,我们可以使用一些模板来替代它们的工作,这里介绍一下ejs如何集成到webpack。
要将ejs如何集成到webpack首先要使用ejs-loader,然后再loader预设。
npm install --save-dev ejs-loader
module.exports = {
module: {
rules: [
{
test: /.ejs$/
exclude: /node_modules|bower_compunents/,
use: {
loader: 'ejs-loader',
options: {
// 因为ejs使用了with语法,在esModule模式下会禁止使用,直接报错
esModule: false
}
}
}
]
}
}
然后我们在js的模块中就可以使用它们了,如果是ts则还需要为这类模块进行声明。
declare module '*.ejs' {
const EjsTemplate: (args: { [key in string]: any }) => string
export default EjsTemplate
}
// webpack.config.js
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const coverPathsToAliases = require('convert-tsconfig-paths-to-webpack-aliases').default
const aliass = coverPathsToAliases(require('./tsconfig.json'))
const getPath = (p) => {
const ps = Array.isArray(p) ? p : [p]
return path.join(__dirname, ...ps)
}
const cssLoaders = [
{
loader: MiniCssExtractPlugin.loader
},
{
loader: 'css-loader',
options: {
url: true,
importLoaders: 8,
modules: {
localIdentName: "[name]__[local]--[hash:base64:5]",
exportLocalsConvention: 'camelCaseOnly',
namedExport: true,
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
stage: 2,
browsers: {
// 正式环境
production: [ '> 0.2%', 'ie > 10' ],
// 开发环境
development: [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
]
]
}
}
}
]
const scriptLoaders = [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: {
chrome: '90',
ie: 11
}
}
],
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-syntax-top-level-await'
],
cacheDirectory: true
}
}
]
module.exports = {
context: getPath('src'),
entry: {
login: './app/login.js',
home: './app/home.js',
},
experiments: {
topLevelAwait: true
},
output: {
clean: true,
path: getPath('./dist'),
chunkFilename: '[name]-[contenthash:5].js',
assetModuleFilename: 'images/[hash][ext]',
filename: '[name]-[contenthash:5].js'
},
module: {
rules: [
{
test: /.(obj|mtl)/i,
type: 'asset/resource',
generator: { filename: 'static/model/[hash][ext]' }
},
{
test: /\.(png|jpg|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 10 * 1024 }
}
},
{
oneOf: [
{
test: /.svg$/i,
resourceQuery: /raw/,
type: 'asset/source'
},
{
test: /.svg$/i,
type: 'asset/resource'
}
]
},
{
test: /\.scss/,
use: [ ...cssLoaders, 'sass-loader' ]
},
{
test: /\.less/,
use: [ ...cssLoaders, 'less-loader' ]
},
{
test: /\.css/,
use: cssLoaders
},
{
test: /\.js$/,
exclude: /node_modules|bower_compunents/,
use: scriptLoaders,
},
{
test: /.ts$/,
exclude: /node_modules|bower_compunents/,
use: [ ...scriptLoaders, { loader: 'ts-loader' } ]
},
{
test: /.ejs$/,
exclude: /node_modules|bower_compunents/,
use: {
loader: 'ejs-loader',
options: { esModule: false }
}
}
],
},
devtool: 'source-map',
optimization: {
runtimeChunk: true,
splitChunks: {
chunks: 'all',
minChunks: 1,
automaticNameDelimiter: '~',
cacheGroups: {
vendors: {
name: false,
test: /node_modules/,
priority: 10,
automaticNameDelimiter: '~',
reuseExistingChunk: true
},
commons: {
minSize: 0,
minChunks: 2,
priority: 20,
reuseExistingChunk: true
}
}
}
},
plugins: [
new MiniCssExtractPlugin({
filename: 'style/[name]-[contenthash:5].css',
chunkFilename: 'style/[name]-[chunkhash:5].css'
}),
new HtmlWebpackPlugin({
template: getPath('./src/pages/login.ejs'),
filename: 'login.html',
titlea: 'login',
chunks: ['login'],
})
],
resolve: {
extensions: ['.js', '.ts'],
alias: aliass
},
externals: {
jquery: 'jQuery',
}
}
// tsconfig.json
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2016",
"incremental": true,
"tsBuildInfoFile": "./node_modules/ts-cache",
"sourceMap": true,
"typeRoots": ["node_modules/@types"],
"rootDir": "./",
"types": ["node"],
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": {
"classnameTransform": "camelCaseOnly",
"namedExports": true
}
}
]
},
"include": ["src/**/*.ts"],
"exclude": []
}
到这里webpack5的基础知识我们就讲完了,下一章我们说说如何优化开发环境,使webpack能更快的构建本地应用,并充分利用编译缓存。
©2021 - bill-lai 的小站 -站点源码