注意,本文使用 webpack5.68.0,不同版本可能会有细微区别。
webpack是一个模块化打包 JavaScript 的工具,在 Wepack 里一切文件皆模块,通过Loader
翻译转化文件,通过Plugin
注入钩子,最后输出多个模块组合的文件,而Webpack
专注于构建模块化项目。官网的首页图说明了Webpack
是什么:
对于webpack
的基础配置及概念可以从官网文档中学习,本文列举webpack
的在生产环境和开发环境中通用的Plugin
和Loader
及配置。
本文后续会出现经常提及chunk
和bundle
在这里我们先解析一下这个概念
chunk
chunk
从字面意思是代码块,在Webpack
中chunk
代表许多关联module
的集合。比如在webpack.config.js
声明了入口模块entry
,入口模块又关联了其他模块,这一系列关联的模块就是一个chunk
。
在webpack
中产生chunk
有三种途径。
entry 产生的 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
,这个chunk
名称可以在代码中使用webpackChunkName
自行定义。如果需要打包的时候使用chunk
名称,则需要在output.chunkFilename
中引用。
// webpack.config.js
module.export = {
output: { chunkFilename: "[name].js" },
};
// module
import(/* webpackChunkName: "async-model" */ "./async-model");
代码分隔产生的 chunk
在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
的chunk
runtimeChunk
声明抽离webpack
运行时代码抽离到一个唯一的名称的chunk
,我们有两个入口,有两份运行时chunk
jquery
符合cacheGroups.vendors
规则,抽离到名为vendors
的chunk
commons.js
符合cacheGroups.commons
规则,抽离到名为commons
的chunk
如下图所示
bundle
bundle
就是我们最终输出的一个或多个文件,大多数情况下一个chunk
至少会产生一个bundle
,但是不完全是一对一的关系。比如我们在模块中引用图片又经过url-loader
打包到外部;或者是引用了样式,通过extract-text-webpack-plugin
抽离出来,这样一个chunk
就会出现产生多个bundle
的情况。
简单来说bundle
就是chunk
在构建完成的呈现。
entry 和 context
entry
是webpack
的启动模块入口,webpack
将根据指定的这个起点来查找模块,生成chunk
,entry
的合法值有:
string
,入口起点的模块路径,webapck
将生成名为main
的chunk
Array
,入口起点的一组模块的路径,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")),
// ...
};
output
默认情况下webpack
会在dist
输出chunk
生成的bundle
,文件名就是chunk
名称。
filename 和 chunkFilename
我们可以通过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",
},
};
path
默认情况下webpack
会将打包的文件输出到dist
文件夹中,我们可以通过配置output.path
来更改输出目录。
在path
可以使用[hash]
的模板字符串,因为每次文件修改后hash
都不一样,很容易就能生成所有版本构建后代码文件。
const path = require("path");
module.exports = {
output: {
path: path.join(__dirname, "./dist-[hash]"),
},
};
html 自动引入 bundle 文件
如果要实现缓存目的,可以使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
实例时可以传入许多参数,可以通过官方文档了解,接下来我们看看最常用的几种定制。
定制 html
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
})
]
}
cdn 加速和缓存处理
如果我们把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
(默认) 只影响按需加载的chunk
all
入口和按需加载都影响
默认情况下的提取规则及主要配置代码:
chunk
被多次引用或是来自node_modules
js chunk
体积大于30Kcss chunk
体积大于50K- 按需加载并行数小于等于30 (同时间import(...)的数量)
- 首次加载并行数小于等于30(在html初次加载的 script引用数量)
// 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'
了。
使用ES语法新特性
在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
模块化概念。
其实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
。 - 使用默认的name生成方式非常难调试,因为生成名称基本上没有阅读性。我们需要自定义生成名称,需要自定义
localIdentName
属性,localIdentName
也是字符串模板,下面列出可支持使用模板:- [name] 源文件名称
- [folder] 文件夹相对于
compiler.context
或者modules.localIdentContext
配置项的相对路径。 - [path] 源文件相对于
compiler.context
或者modules.localIdentContext
配置项的相对路径。 - [file] - 文件名和路径。
- [ext] - 文件拓展名。
- [hash] - 字符串的哈希值。基于
localIdentHashSalt
、localIdentHashFunction
、localIdentHashDigest
、localIdentHashDigestLength
、localIdentContext
、resourcePath
和exportName
生成。 - [local] - 原始类名。
- 编写
css
文件时一般采用-
来代替驼峰命名,我们需要将-
转化为驼峰,需要将exportLocalsConvention
属性改为camelCaseOnly
module.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
要使用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
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
,他们的值分别代表
- 0 非官方草案
- 1 编辑草案或早期工作草案
- 2 工作草案
- 3 候选版本
- 4 推荐标准
browsers
来配置需要支持的浏览器环境,其中数据和是否需要加前缀是根据caniuse决定的。查看的更多browsers定义规则
@import处理
如果我们使用上方的配置在样式文件中使用@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; }
使用ts
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
下面是这个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'
vscode ts支持模块化css
上方的类型声明只是让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
因为有可能有缓存。然后就可以看到效果了
ts 快捷路径访问模块
我们在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
}
}
使用ejs
在没有使用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
能更快的构建本地应用,并充分利用编译缓存。