首先,来个官方文档对webpack的定义:
webpack 是代码编译工具,有入口、出口、loader 和插件。webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。
webpack 的核心价值就是前端源码的打包,即将前端源码中每一个文件(无论任何类型)都当做一个 pack ,然后分析依赖,将其最终打包出线上运行的代码。
webpack 的五个核心部分:
- entry:规定入口文件,一个或者多个。指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
- output:规定输出文件的位置。告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为
./dist
- loader:让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块
- plugin:被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
- Mode:指示Webpack使用相应模式的配置
Mode 有三种固定的写法(名称固定,不能改):
development(开发环境:自动优化打包速度,添加一些调试过程中的辅助)
production(生产环境:自动优化打包结果)
none(运行最原始的打包,不做任何额外处理)
通过 process.env.NODE_ENV 可以获得当前的 Mode。
那么,webpack在日常工作中,是如何配置的呢?
通常情况下,webpack的配置文件在项目内会分为:
- webpack.common.js
- webpack.dev.js
- webpack.prod.js
分别代表公共配置,开发环境配置,线上环境配置。
接下来,将按这三个文件来讲解如何配置,部分基础知识和要点会写在注释中,要知道全部配置内容,建议查看官方文档:https://www.webpackjs.com/concepts/
公共配置webpack.common.js:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const srcPath = path.join(__dirname, '..', 'src')
module.exports = {
entry: path.join(srcPath, 'index.js'),
//以该路径下的index.js作为构建的开始进入入口起点后,
//webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的
module: {
rules: [
{
test: /\.js$/,
loader: ['babel-loader'], //使用哪个loader来处理文件
include: srcPath,
exclude: /node_modules/
//test,include,exclude都是拿来匹配哪些文件需要被loader处理的,
//优先级是exclude > include > test
},
{
test: /\.css$/,
// loader 的执行顺序是:从后往前
loader: ['style-loader', 'css-loader', 'postcss-loader']
// 加了 postcss
},
{
test: /\.less$/,
// 增加 'less-loader' ,注意顺序
loader: ['style-loader', 'css-loader', 'less-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
}
开发环境配置webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')//该模块可以将公用配置合并在开发环境配置
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'development',
module: {
rules: [
// 直接引入图片 url
{
test: /\.(png|jpg|jpeg|gif)$/,
use: 'file-loader'
}
]
},
plugins: [
new webpack.DefinePlugin({
// window.ENV = 'development'
ENV: JSON.stringify('development')
})
],
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
// 设置代理,进行跨域
proxy: {
// 将本地 /api/xxx 代理到 localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到 localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
pathRewrite: {
'/api2': ''
}
}
}
}
})
线上环境webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如
//cdn 域名),这里暂时用不到
},
//以该路径下的index.js作为构建的结束,即出口,
//webpack 会将模块和库(直接和间接)依赖输出为对应的打包文件
module: {
rules: [
// 图片 - 考虑 base64 编码的情况
{
test: /\.(png|jpg|jpeg|gif)$/,
use: {
loader: 'url-loader',
options: {
// 小于 5kb 的图片用 base64 格式产出
// 否则,依然延用 file-loader 的形式,产出 url 格式
limit: 5 * 1024,
// 打包到 img 目录下
outputPath: '/img1/',
// 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那
//将作用于所有静态资源)
// publicPath: 'http://cdn.abc.com'
}
}
},
]
},
plugins: [
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
})
]
})
以上为webpack基础配置,以下将介绍其高级配置,如多入口文件,如何压缩抽离css文件,如何抽离公共代码和第三方代码以及如何异步加载js文件
如何配置多入口文件:
之前基础配置的公共配置入口entry只有一个文件,这是项目为单页面的情况。如果项目为多页面或者有需求需要打包出多个页面,则需要配多入口文件。
在公共配置webpack.common.js中需要这么修改(仅显示需修改的位置):
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const srcPath = path.join(__dirname, '..', 'src')
module.exports = {
entry: {
index: path.join(srcPath, 'index.js'),
other: path.join(srcPath, 'other.js')
},
//多文件时入口entry为对象形式,key对应的是引入的模块的重新命名,value为引入的模块
//的文件位置(在webpack中任何文件都是模块)
plugins: [
// new HtmlWebpackPlugin({
// template: path.join(srcPath, 'index.html'),
// filename: 'index.html'
// })
// 多入口 - 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全
//部引用
chunks: ['index'] // 只引用 index.js
}),
// 多入口 - 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other'] // 只引用 other.js
})
]
//针对每一个入口文件都要建一个HtmlWebpackPlugin的插件,即new一个
//HtmlWebpackPlugin实例。针对chunks,如果不写上的话,每一个html文件都会index.js
//和other.js
}
由于修改了多入口,所以webpack.prod.js也需要修改其出口:
output: {
// filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如
//cdn 域名),这里暂时用不到
},
如何压缩抽离css文件,抽离公共代码和第三方代码
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')
module.exports = smart(webpackCommonConf, {
mode: 'production',
module: {
rules: [
// 抽离 css
{
test: /\.css$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-
//loader
'css-loader',
'postcss-loader'
]
},
// 抽离 less
{
test: /\.less$/,
loader: [
MiniCssExtractPlugin.loader, // 注意,这里不再用 style-
//loader
'css-loader',
'less-loader',
'postcss-loader'
]
}
]
},
plugins: [
//每抽离一个新的文件都需要在plugins里new一个新的实例来导出对应的模块(文件)
new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
new webpack.DefinePlugin({
// window.ENV = 'production'
ENV: JSON.stringify('production')
}),
// 抽离 css 文件
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
}),
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory']
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
],
optimization: {
// 压缩 css
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
// 分割代码块
splitChunks: {
chunks: 'all',
/**
* initial 入口chunk,对于异步导入的文件不处理
async 异步chunk,只对异步导入的文件处理
all 全部chunk
*/
// 缓存分组
cacheGroups: {
// 第三方模块
vendor: {
name: 'vendor', // chunk 名称
priority: 1, // 权限更高,优先抽离,重要!!!
test: /node_modules/,
minSize: 0, // 大小限制,大于等于该限制的才会被抽离成新的模块
minChunks: 1 // 最少复用过几次,即被引用的次数大于等于该次数
//才会被抽离成新的模块
},
// 公共的模块
common: {
name: 'common', // chunk 名称
priority: 0, // 优先级
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
}
})
因为多抽离出了文件,为了保证一一对应且不引入多余的模块,需要对webpack.common.js的plugins的chunks重新赋值,即:
plugins: [
// new HtmlWebpackPlugin({
// template: path.join(srcPath, 'index.html'),
// filename: 'index.html'
// })
// 多入口 - 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),
//默认全部引用
chunks: ['index', 'vendor', 'common'] // 增加vendor.js,common.js
}),
// 多入口 - 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other', 'common'] // 增加common.js
})
]
如何异步加载js文件
在需要异步加载JS文件的位置写上setTimeout等异步任务即可。
那么,以上内容出现的module,chunk和bundle有什么区别呢?
- module-各个源码文件,webpack中一切皆为模块。
- chunk-多模块合并成的,如entry,import()和splitChunk
- bundle-最终输出的文件
以上概念的包含关系为: module < chunk < bundle
webpack如何优化构建速度(性能优化)?
- 优化babel-loader:即开启babel-loader的缓存,并且使用include或者exclude明确打包范围。
- 使用ignorePlugin,即不引入某些不需要的模块,缩小产出文件的体积;使用noParse引入模块,但是不打包。
- 使用happyPack开启多进程打包。
- 使用ParalleUglifyPlugin开启多进程进行代码压缩。
- 使用自动刷新或者热更新。
- 使用DllPlugin,将一些大的库事先打包成dll文件来引用,不用每次都去打包。
module: {
noParse: [react\.min\.js$/],
rules: [
{
test: /\.js$/,
loader: ['babel-loader?cacheDirectory'],
//cacheDirectory用于开启缓存
include: srcPath,
// exclude: /node_modules/
}
],
plugins: [
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/\.\/locale/, /moment/),
// happyPack 开启多进程打包
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory']
}),
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
]
]
什么是babel?
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。简而言之就对超过ES5的js代码(如ES6),编译为ES5代码,承担的是编译转换的工作。
babel-polyfill与babel-runtime
由于babel只是简单地将符合JS语法的代码转换为ES5代码,对于如ES6无能为力,如转换abc.includes()时,判断出该代码符合JS语法便进行转换,并不考虑.includes()在ES5下是否可运行。
所以需要使用babel-polyfill对.includes()进行扩展,简单来说就是babel-polyfill相当于打补丁,通过引入第三方 polyfill 模块,例如 core-js,会包含诸如.includes()的扩展,使得编译转换后的代码能正常运行。
另外,babel-polyfill支持按需引入以减小打包后的代码大小。
babel-polyfill的缺点就是这些扩展是在全局定义的,会污染全局变量(全局环境),因此在这种情况下,babel-runtime孕育而生,babel-runtime是通过对babel-polyfill提供的扩展方法进行重命名,如includes变成_includes来杜绝对原本includes方法的污染。
这里只是简单讲解其中由来,具体配置细节可以参考以下文章:
关于Babel你只需要知道三个插件
基础知识
1.占位符
目前webpack支持的占位符:
占位符 | 含义 |
---|---|
[hash] | 模块标识符的 hash |
[chunkhash] | chunk 内容的 hash |
[name] | 模块名称 |
[id] | 模块标识符 |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
[function] | 一个 return 出一个 string 作为 filename 的函数 |
- [hash]、[chunkhash]和[contenthash]都支持[xxx:length]的语法。
- Tips: 占位符是可以组合使用的,例如[name]-[hash:8]。
[hash]
和 [chunkhash]
的长度可以使用 [hash:16]
(默认为 20)来指定。或者,通过指定output.hashDigestLength
在全局配置长度,那么他们之间有什么区别吗?
[hash]
:是整个项目的 hash
值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash
,即修改任何文件都会导致所有文件的 hash
发生改变;在一个项目中虽然入口不同,但是 hash
是相同的;hash
无法实现前端静态资源在浏览器上长缓存,这时候应该使用 chunkhash
;
[chunkhash]
:根据不同的入口文件(entry
)进行依赖文件解析,构建对应的 chunk
,生成相应的 hash
;只要组成 entry
的模块文件没有变化,则对应的 hash
也是不变的,所以一般项目优化时,会将公共库代码拆分到一起,因为公共库代码变动较少的,使用 chunkhash
可以发挥最长缓存的作用;
面试题
1.webpack的构建流程是怎么样的?
答:分为初始化流程,编译构建流程和输出流程。
1.初始化参数:解析webpack配置参数,合并shell语句和webpack.config.js文件配置的参数,形成最后的配置结果。
2.开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。
3.确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。
4.编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
5.完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6.输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会。
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。
初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:
- compile 开始编译
- make 从入口点分析模块及其依赖的模块,创建这些模块对象
- build-module 构建模块
- seal 封装构建结果
- emit 把各个chunk输出到结果文件
2.webpack的tree-shaking原理?
答:Webpack 中,Tree-shaking 的实现一是先「标记」出模块导出值中哪些没有被用过,二是使用 Terser 删掉这些没被用到的导出语句。标记过程大致可划分为三个步骤:
- Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
- Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
- 生成产物时,若变量没有被其它模块使用则删除对应的导出语句
注:标记功能需要配置 optimization.usedExports = true 开启。也就是说,标记的效果就是删除没有被其它模块使用的导出语句。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。