webpack构建流程
从启动webpack构建到输出结果经历了一系列过程:
- 解析webpack.config.js配置参数,调用shell并追加命令行参数,通过 optimist将前两者参数整合成 options 对象传到了下一个流程的控制对象中
- 注册所有配置的插件,让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
- 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
- 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
- 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk。
- 输出所有chunk到文件系统。
入口和上下文(entry and context)
entry
指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
- 单入口
entry: './src/app.js'
//等同于上面写法
entry: {
main: './src/app.js'
}
- 多入口
entry: {
app: './src/pages/app/index.js',
list: './src/pages/list/index.js'
}
这告诉我们 webpack 从 app.js 和 .js 开始创建依赖图(dependency graph)。这些依赖图是彼此完全分离、互相独立的。
多页面应用,入口文件动态获取,下面方法用于,获取动态键值对入口文件
const glob = require('glob')
const path = require('path')
const GLOB_FILE_PATH = './src/pages/**/index.js'
const CUT_PATH = './src/pages/'
exports.getEntries = function(argv){
let paths = glob.sync(GLOB_FILE_PATH)
let entries = {}
for (let i = 0; i < paths.length; i++){
let pathName = path.dirname(paths[i]).replace(new RegExp('^' + CUT_PATH), '')
entries[pathName] = paths[i]
}
return entries
}
context
webpack 编译时的基础目录,
entry
和loader
会相对于此目录查找文件
默认值为项目根目录,不建议修改
出口(output)
告诉 webpack 在哪里输出它所创建的 bundles
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: 'js/[name].js',
clean: true, // 在生成文件之前清空 output 目录
}
publicPath
用于指定打包后的文件需要加载的外部资源(如图片、js、css等)的根路径
默认值是一个空字符串 "",通常设置成"/"
静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径
- loader 输出图片文件配置
{ name: 'imgs/[name].[ext]' }
// 那么图片最终的访问路径为
output.publicPath('/') + 'imgs/[name].[ext]' = '/imgs/[name].[ext]'
- plugin 提取css文件配置:
new ExtractTextPlugin('css/[name].[contenthash:10].css')
// html中加载css打包后代码
<link href="/css/app.9502b0c565.css" rel="stylesheet">
- html中加载js打包后代码
<script type="text/javascript" src="/js/runtime.4ece365fd5.js"></script>
path
打包文件输出的目录
建议绝对路径;默认值为当前路径。
path 中用使用 [hash] 模板可用于版本回归
output: {
path: path.resolve('./dist/[hash:8]/')
}
loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)
注意:module.loaders 改为 module.rules;链式loader
- webpack1旧语法
module: {
loaders: [{
test: /\.less$/,
loader: "style!css!less"
})
}
- webpack新语法语法
module: {
rules: [{
test: /\.less$/,
use: [
"style-loader",
"css-loader",
"less-loader"
]
}]
}
样式类
less-loader(依赖less)
npm i less less-loader --save-dev
less-loader加载less文件,less转移成
输出→CSS
sass-loader(依赖node-sass)
npm i node-sass sass-loader --save-dev
sass-loader加载sass/scss文件,node-sass转移成css
输出→CSS
postcss-loader 加载和转译 CSS 文件
npm i postcss-loader autoprefixer --save-dev
postcss
是一个通过JS插件转换css的工具,提供JS API,开发者可以根据API接口,定制开发postcss插件 ,比较流行的工具有:
autoprefixer
补全浏览器厂商前缀stylelint
代码检查工具postcss-pxtorem
将px转换成rem单位
工作流:
- 把 CSS 解析成 JavaScript 可以操作的 抽象语法树(AST)结构
- 调用插件来处理 AST 并得到结果 输出→CSS
postcss.config
配置如下:
module.exports = {
plugins: {
autoprefixer: {},
"postcss-pxtorem": {
rootValue: 16,
propList: ['*'],
minPixelValue: 1,
exclude: (e) => {
if (/src(\\|\/)excludeDirName(\\|\/)/.test(e)) {
return false
}
return true
},
}
}
}
}
webpack4 请使用 postcss-loader v4;webpack5请使用最新版,并安装依赖
postcss
css-loader
npm i css-loader --save-dev
作用:解析css后,将css代码转换成js模块,然后对css中 @import
和 url()
这样的外部资源进行处理。(就像 js 解析 import/require()
一样)
@import './style.css' => require('./style.css')
url(./image.png) => require('./image.png')
输出可以被执行的js代码字符串数组
启用 CSS 模块
module: {
rules: [
{
test: /.css$/i,
loader: "css-loader",
options: {
modules: {
localIdentName:'[name]-[local]-[hash:base64:5]'
}
},
},
],
},
};
启用css模块的作用,就是在编译后产生唯一的class,避免和其他文件的class冲突。
下面是一个React组件
import style from './style.less'
export default ()=>{
return( <div className={style.container}>主体内容</div> )
}
.container{
font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
css-loader编译后的结果
.style-container-32JMV{
font-family: "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "Microsoft YaHei", "WenQuanYi Micro Hei", sans-serif;
}
但在项目中也会遇到比如antd、swiper等第三方组件库,需要修改原样式的场景,CSS Module也提供了全局规则模式
:global(.container) {
width: 100%;
}
配合less可以同时定义多个全局class
:global {
.layout{
margin: 0;
padding: 0;
}
.container{
width: 100%;
}
}
style-loader 将模块的导出作为样式添加到 DOM 中
npm i style-loader --save-dev
把css通过style标签形式插入到DOM中,一般用在开发环境,生产环境会用
mini-css-extract-plugin
插件将css提取到独立文件中。
最终style loader配置
const getStyleLoaders = () =>
[
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]-[local]-[hash:base64:5]',
},
},
},
'postcss-loader',
'sass-loader',
].filter(Boolean);
module: {
rules: [
{
test: /\.(sc|c)ss$/,
use: getStyleLoaders(),
}
]
}
转换编译类
babel-loader
由于浏览器只能读懂ES5语法,需要babel将ES2015+语法编译为ES5语法
-
安装
npm i @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev
-
用法
{
test: /\.js$/,
use: {
loader: 'babel-loader',
exclude: /node_modules/,
options: {
cacheDirectory: true,
}
}
}
options
- cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果
- babelrc 默认true,设置false,.babelrc将不会启用 .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
}
],
"@babel/typescript",
"@babel/preset-react"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": true
}
]
]
}
备注:@babel/preset-env
已加入@babel/plugin-proposal-class-properties
,不需要在plugins中额外引入
script-loader
在全局上下文(global context)执行一次 JS脚本,就像你在网页上通过<script>把它们引进来一样。
文件类
raw-loader 加载文件原始内容,比如.txt文件
file-loader 将文件打包导到输出目录
{
test: /\.(gif|png|jpe?g|svg)$/,
use: [{
loader: 'file-loader',
options:{
name: 'static/img/[name].[ext]?[hash]',
}
}]
}
默认输出到output的根目录下,name为32为hash值
url-loader 功能类似file-loader
对file-loader的扩展,可以设置小图片转换base64图片
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit:10240,
name: 'static/img/[name].[ext]?[hash]'
}
}
]
}
webpack5内置静态资源构建能力
{
test: /\.(gif|png|jpe?g|svg)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'static/img/[name].[ext]?[hash]',
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
},
},
}
type取值说明:
- asset/source — 功能相当于 raw-loader
- asset/inline — 功能相当于 url-loader,若想要设置编码规则,可以在 generator 中设置 dataUrlCondition
- asset/resource —功能相当于 file-loader
- asset — 默认会根据文件大小来选择使用哪种类型,当文件小于 8 KB 的时候会使用 asset/inline,否则会使用 asset/resource,也可手动进行阈值的设定
清理和测试类
mocha-loader 使用 mocha 测试(浏览器/NodeJS)
eslint-loader 使用 ESLint 清理代码
jshint-loader 使用 JSHint 清理代码
模版类
html-loader 解决html里加载图片问题
minimize:true 压缩html文件
handlebars-loader 加载handlebars文件并编译为html文件
handlebars-template-loader 解决handlebars图片路径问题
markup-inline-loader
将内联的 SVG/MathML 文件转换为 HTML。在应用于图标字体,或将 CSS 动画应用于 SVG 时非常有用
{
test: /\.html$/,
use: [
'html-loader',
'markup-inline-loader'
]
}
html中使用:
<img markup-inline src="./_images/camera.svg" />
<img data-markup-inline src="./_images/camera.svg" />
框架类
vue-loader 加载和转译 Vue 组件
angular2-template-loader 加载和转译 Angular 组件
插件
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量
html-webpack-plugin(生成html文件)
- 为html文件中引入的外部资源如script、link,动态添加每次compile后的hash,防止引用缓存的外部文件问题
- 可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new htmlWebpackPlugin(options),
new htmlWebpackPlugin(options)
]
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
title | {String} | `` | 用于生成的HTML文档的标题 |
filename | {String} | 'index.html' | 生成html的文件名 |
template | {String} | `` | 模版路径及文件名(路径相对与output.context) |
inject | {Boolean|String} | true | true或'body':所有JavaScript资源将被放置在正文元素的底部。 'head':将脚本放置在head元素中 false:不会将脚本放进html中 |
favicon | {String} | `` | 将给定的图标路径添加到输出HTML |
minify | {Boolean|Object} | true | 缩小输出html html-minifier |
hash | {Boolean} | false | true:将webpack所有包含的脚本和CSS文件附加一个独特的编译哈希。这对缓存清除非常有用 |
cache | {Boolean} | true | 仅在文件被更改时才发出文件 |
mini-css-extract-plugin(提取css文件)
webpack 把所有的资源都当成了一个模块, CSS,Image, JS 字体文件资源, 都打包到一个 bundle.js 文件中,它将css模块代码提单独取到css文件中
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
plugins: [new MiniCssExtractPlugin()],
};
webpack4使用extract-text-webpack-plugin
terser-webpack-plugin
(压缩js文件)
webpack5内置了
terser-webpack-plugin
,如果需要配置或webpack4,需要安装
const TerserPlugin = require('terser-webpack-plugin');
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
compress: {
comparisons: false,
drop_console: true, //丢掉console
inline: 2,
},
output: {
comments: false,
}
}
})]
}
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
test | {RegExp|Array<RegExp>} | /.m?js(?.*)?$/i | 匹配文件 |
parallel | {Boolean|Number} | true | 使用多进程并行运行来提高构建速度 |
terserOptions | {Object} | default | minify options |
webpack4项目可能用的uglifyjs-webpack-plugin
插件压缩JS
copy-webpack-plugin(拷贝静态资源)
const CopyPlugin = require("copy-webpack-plugin")
CopyWebpackPlugin(
patterns: [
{
from: 'src/static',
to: 'static',
},
])
from 定义要拷贝的源目录.
to 定义要拷贝的目标目录,默认output.path
filter 忽略拷贝指定的文件 可以用模糊匹配.
clean-webpack-plugin 打包前清理构建目录
默认会删除output目录下的文件,但目录本身不会被删除;此功能已经被webpack 5.20.0+内置了,详见上面的output设置
使用方法和常用的配置项
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [new CleanWebpackPlugin({
verbose:true, //打开日志,默认关闭
cleanStaleWebpackAssets: true //默认删除未使用的资源
})]
compression-webpack-plugin gzip压缩
http gzip解压过程
- 浏览器请求资源时会自带
accept-encoding
请求头,告诉服务器支持的压缩编码类型 - 服务器开启gzip配置=>接收客户端资源文件请求,服务器通过gzip,来对response进行编码(如果有gzip文件,则直接使用gzip文件,减少服务器压力),编码后header中有content-encoding:gzip,发送给浏览器
- 浏览器接到response后,根据
content-encoding:gzip
来对response进行解码,之后进行页面渲染。
模块热替换方案-HotModuleReplacementPlugin(HMR)
- 讲模块热替换前,先说下热更新。热更新就是当代码变更后页面自动刷新,代码如下:
devServer: {
hot: true // 激活服务器的HMR
}
配置很简单,就是有个缺点,页面会刷新,所有资源都需要重新加载,所以就有了热替换。
- 热替换是在不刷新页面的前提下,替换模块内容;基本原理就是
webpack-dev-server
通过websocket
把更新后的代码推送到浏览器,通过module.hot.accept
注册的回调被执行
新增如下配置
// webpack.config.js
new webpack.HotModuleReplacementPlugin()
项目入口文件
import App from './App';
if (module.hot) {
module.hot.accept('./App', () => {
ReactDOM.render( <App />, document.getElementById('root') );
});
}
webpack内置的HMR方案解决了页面刷新的问题,但仍然存在内部状态问题,不利于开发环境调试。
缺点:如果用react或者vue,刷新后组件内的状态会变成初始值,对React来说,在HMR做的是重新引入root 组件,然后重新渲染;因为HMR是对根组件的热替换,所以对于根组件和它的子组件的state都会被丢失,但存储于redux store中的状态仍会可以保持
- 方案1:还以React来说,引入
react-hot-loader
来解决上面遗留的问题。
使用react-hot-loader
后,可以删除webpack.HotModuleReplacementPlugin
插件
// App.jsx
import { hot } from 'react-hot-loader/root'
export default hot(App)
// .babelrc
{
"plugins": ["react-hot-loader/babel"]
}
// package.json
{
"dependencies": {
"react-hot-loader": "^4.13.0",
"@hot-loader/react-dom": "^17.0.1"
}
}
// webpack.config.js
{
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom' //解决React 16.6+ 新特性的热替换问题
}
}
}
- 方案2:
react-refresh
和@pmmmwh/react-refresh-webpack-plugin
react官方模块热替换(HMR)方案
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
{
module: {
{
test: /\.tsx?$/,
loader: 'babel-loader',
options: {
plugins: [isEnvDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
}
}
},
plugins:[isEnvDevelopment && new ReactRefreshPlugin(),]
}
要求:react-dom@16.9+
webpack自带插件
webpack.DefinePlugin 配置全局常量
项目中定义常量以区分环境(是常量,在项目中不可以重新赋值),并非挂在在window
上,但在所有模块中都可以被访问。一般会配合cross-env
一起使用
cross-env(跨平台设置node环境变量)
什么是node的环境变量?process是存在于nodejs中的一个全局变量,process.env就是项目所在运行环境的一些信息。
环境变量又有什么用呢?最常见的一个场景就是,webpack打包时,会创建一个node服务环境,通过设置环境变量,给服务打上一个标签,用来标注当前的运行环境
// package.json
"scripts": {
"dev": "cross-env NODE_ENV_MARK=dev webpack-dev-server --config config/start.js",
}
//开发环境 dev.env.js
module.exports = {
NODE_ENV: '"development"',
prefix: '"//devapi.abc.com"',
};
const env = require(`../env/${process.env.NODE_ENV_MARK}.env`)
new webpack.DefinePlugin({
'process.env': env
})
webpack.ProgressPlugin 进度插件
webpack.optimize.CommonsChunkPlugin 提取chunks
之间共享的通用模块
webpack将多个模块打包之后的代码集合称为chunk。
此插件已在webpack4及更高版本中移除,如需拆包,请参考文章中优化模块的SplitChunksPlugin
通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,利用缓存、按需加载提高页面的加载速度
module.exports = {
entry: {
main1: '/src/main1.js',
main2: '/src/main2.js',
jquery:["jquery"],
vue:["vue"]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ["common",'jquery','vue'],//对应于上面的entry的key
minChunks:2
})
]
};
打包后jquery和vue会生成独立chunk,main1和main2中的公共业务模块会打包到common.js中;
minChunks为infinity时,公共业务模块会分别打包到main1.js和main2.js中
其他
webpack-dev-server
- 开发环境用于调试报错信息等,生成一个开发用的服务器,在文件有变化的时候自动给我们打包,然后刷新页面
- 它还有个模块热替换的功能 .. 就是它可以只替换有变化的地方 .. 不需要刷新整个页面 ...
speed-measure-webpack-plugin 构建速度分析
webpack-bundle-analyzer 构建体积分析
优化
优化分两大类:构建速度和构建输出大小
下面是构建速度类:
1.更新webpack版本(构建时间)
webpack5 较于 webpack4,新增了持久化硬盘缓存、改进缓存算法提升构建性能,更好的tree shaking和代码生成逻辑优化产物大小。
2.缓存(构建时间)
- 持久化硬盘缓存
cache: {
type: 'filesystem', // 使用文件缓存
},
dll❎:首次将公共库打包成单独的文件,此后只需打包业务代码,减少打包时间,webpack4+已经官方内置了相关功能
- 开启 babel-loader 缓存 babel编译过程很耗时,好在babel-loader提供缓存编译结果选项,在重启webpack时不需要创新编译而是复用缓存结果减少编译流程。babel-loader缓存机制默认是关闭的,打开的配置如下:
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}
3.缩小loader搜索范围(构建时间)
{
test: /\.js$/,
loader: 'babel-loader',
include: path.resolve(__dirname, 'src'),
}
4.优化resolve 更快的解析(构建时间)
- 配置解析模块时搜索的目录
类似
import redux from 'redux'
这样既非相对又非绝对路径的写法时,会查找当前目录下的node_modules目录,默认的配置会采用向上递归搜索的方式去寻找node_modules,但通常项目里只有一个node_modules在根目录
module.exports = {
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, 'src'),
]
}
};
下面是优化构建输出大小
5.tree-shaking(打包体积)
借助es6模块的静态性的特点(在编译时就确定模块间的依赖关系),来删掉export
但是没有import
过的东西
@babel/presets-env
默认会将ES Module转换化为CommonJS形式,从而导致Webpack的tree-shaking特性失效,设置false后会禁用模块化语句的转化,将Module的语法交给Webpack处理
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
- purgecss-webpack-plugin 对css tree shaking
6.文件压缩(打包体积)
- 压缩css
css-minimizer-webpack-plugin
使用cssnano优化和压缩css,支持缓存、并发模式下运行,css的热替换也很好的支持。webpack5不推荐使用optimize-css-assets-webpack-plugin
optimization: {
minimizer: [
new CssMinimizerPlugin({
parallel: true, //默认使用多线程运行,os.cpus().length - 1
}),
],
},
- 压缩JS
new TerserPlugin({
terserOptions: {
compress: {
comparisons: false,
drop_console: true, //丢掉console
inline: 2,
},
output: {
comments: false,
}
}
})
- 用
imagemin-webpack-plugin
压缩图片
7.chunk拆分(打包体积)
- 对三方库和公共代码拆分 splitChunks
- 运行时文件 runtimeChunk
runtime:模块代码的链接,打包完后,浏览器加载入口文件后,webpack会有一个启动程序,控制模块的创建和执行
异步模块的创建步骤:
- 创建一个Promise对象,使用installedChunks记录下其resolve和reject,便于后面获取资源后切换上下文,控制.then()的执行实际
- installedChunks记录一家在和加载中的chunk
- 创建script标签,发起异步请求
- webpack特性import() 按需加载
optimization: {
splitChunks: {
chunks: 'all',
minChunks: 2,
cacheGroups: {
dll: {
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'dll',
priority: 100,
enforce: true,/* 为此缓存组创建块时,告诉webpack忽略minSize,minChunks,maxAsyncRequests,maxInitialRequests选项。*/
reuseExistingChunk: true,
},
commons: {
name: 'commons',
minChunks: 2,
chunks: 'all',
reuseExistingChunk: true,
},
},
},
runtimeChunk: true,
}
另外还有文件名生成hash值,利用浏览器缓存减少文件加载
output: {
filename: `static/js/[name]${isEnvProduction ? '.[contenthash:8]' : ''}.js`,
},
8.其他
- 使用 alias
resolve.alias 配置路径映射。
发布到npm的库大多数都包含两个目录,一个是放着cmd模块化的lib目录,一个是把所有文件合成一个文件的dist目录,多数的入口文件是指向lib里面下的。
默认情况下webpack会去读lib目录下的入口文件再去递归加载其它依赖的文件这个过程很耗时,alias配置可以让webpack直接使用dist目录的整体文件减少文件递归解析。配置如下:
module.exports = {
resolve: {
alias: {
'moment': 'moment/min/moment.min.js',
'react': 'react/dist/react.js',
'react-dom': 'react-dom/dist/react-dom.js'
}
}
};
- 使用 noParse
module.noParse
配置哪些文件可以脱离webpack的解析。 有些库是自成一体不依赖其他库的没有使用模块化的,比如jquey、momentjs、chart.js,要使用它们必须整体全部引入。
webpack是模块化打包工具完全没有必要去解析这些文件的依赖,因为它们都不依赖其它文件体积也很庞大,要忽略它们配置如下:
module.exports = {
module: {
noParse: /node_modules\/(jquey|moment|chart\.js)/
}
};
- webpack4的一些优化
由于webpack4没有启用多线程构建,可以借助
happypack
这个启用多线程
优化总结:
- 在加快构建时间方面,包括升级webpack、配置cache,可大大加快二次构建速度。
- 在减小打包体积方面,包括压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。
- 在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。