基本配置
webpack的配置是在项目根路径下的webpack.config.js这个文件中进行配置的。由于这个文件将会运行在nodejs环境中,所以需要使用CommonJS规范。这个文件需要导出一个对象,通过对该对象属性的配置来完成对webpack的配置。
module.exports = {
enrty: './src/main.js', // 将webpack的打包入口指定为src下面的main.js
}
entry这个属性中的路径如果是相对路径的话,./是不能被省略的
还可以通过output这个属性去设置打包后的文件的输出路径:
const path = require('path')
module.exports = {
enrty: './src/main.js', // 将webpack的打包入口指定为src下面的main.js
output: {
filename: 'bundle.js', // 打包结果的文件名
path: path.join(__dirname, 'dist'), // 打包结果的路径
publicPath: 'dist/' // 配置静态资源路径
}
}
path属性必须是绝对路径
webpack工作模式配置
在使用webpack进行打包的时候,可以通过mode参数来指定工作模式:
yarn webpack --mode development #开发环境
#或
yarn webpack --mode production #生产环境(默认)
#或
yarn webpack --mode none #仅打包,不做任何处理
也可以在配置文件中进行配置:
module.exports = {
mode: 'development', // 'production' or 'none'
}
下面是官方对于这三个模式的描述:
| 选项 | 描述 |
|---|---|
development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。 |
production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。 |
none | 不使用任何默认优化选项 |
webpack加载资源的方式
在webpack打包的过程中,以下几种方式会触发模块的加载:
-
遵循
ES Modules标准的import声明 -
遵循
CommonJS的require函数通过
require去载入ESM的默认导出结果的话需要的是require结果的default属性,如const Header = require('Header').default -
遵循
AMD标准的define函数和require函数 -
一些
loader中也会触发模块加载,如:css-loader中的@import指令和url函数html-loader中如img标签的src属性
webpack导入资源模块
webpack打包非js文件的时候需要安装对应的loader,例如css文件就需要css-loader以及style-loader,同时需要在配置文件中加入以下配置:
module.exports = {
// ...other config
module: {
// rules是一个数组,它是对各种文件的加载规则的配置
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
rules的每一配置对象都需要test和use属性。test属性的值是一个正则表达式,它用来匹配对应文件的路径;use属性是用来规定加载当前test所对应的文件的loader。use属性可以是字符串,也可以是数组,当use属性为数组时,对应的loader的执行顺序是从后往前执行的。
webpack引入文件资源
对于一些例如图片等的文件资源,需要配合文件资源加载器来对其进行打包操作:
可以使用file-loader来将文件拷贝至dist文件夹下,以便打包后的结果能使用文件:
module.exports = {
// ...other config
module: {
rules: [
{
// 引入png文件
test: /.png$/,
use: 'file-loader'
}
]
}
}
也可以使用url加载器:
module.exports = {
// ...other config
module: {
rules: [
{
// 引入png文件
test: /.png$/,
use: 'url-loader'
}
]
}
}
url-loader是将文件转换成base64字符串的形式打包文件的,生成的字符串一般都会比较长。
综上:
- 对于小文件,例如
icon等可以使用url-loader来将其转成base64字符串以减少HTTP请求次数; - 对于大文件,还是使用
file-loader来进行打包以提高加载速度;
这样的话配置文件可以如下配置:
module.exports = {
// ...other config
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10 * 1024 // 在文件大小不超过10k时使用url-loader
}
}
}
]
}
}
使用
url-loader的时候一定要同时安装file-loader
webpack常用加载器分类
webpack的loader大致可分为三类:
-
编译转换类
如
css-loader,它是将css文件转换为打包后js文件中的一个模块,通过js来运行css -
文件操作类
将文件拷贝至输出目录,并将访问路径导出,如
file-loader -
代码检查类
检查代码格式、语法等问题,提高代码质量
webpack与ES6
webpack自己并不能处理js中的新语法,因此需要babel-loader来将其进行转译。
使用前需要安装以下模块:babel-loader、@babel/core以及@babel/preset-env
使用时需要在module.rules中配置:
module.exports = {
// ...other config
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
preset: ['@babel/preset-env']
}
}
}
]
}
}
webpack核心工作原理
一般项目中会按照文件夹的划分有各种的文件,webpack会根据配置找到对应的入口文件为打包入口(一般情况下都会是js文件),然后根据入口文件中的import、require等导入语句来找到其所依赖的资源模块,再分别去解析每个资源模块所对应的依赖,并生成整个项目各文件的“依赖关系树”:
webpack会递归这个依赖树找到每个节点所对应的资源文件,然后通过webpack.config.js文件的rules配置的属性去找到对应的加载器(loader),再用该加载器去加载当前资源模块。
最后会将加载到的结果加载到bundle.js——即打包结果中,从而去实现整个项目的打包。
整个过程中loader机制是webpack的核心,
开发一个简易的markdown-loader
**需求:**将md文件中的内容转换成html内容
loader运行机制:loader运行机制类似node中的管道pipe,运行时中会有一个或多个loader按顺序处理输入并将结果转交给下一个,最后输出一段js代码。
实现:
-
先创建个文件,取名
markdown-loader.js,loader中需要先导出一个函数,这个函数里面就是要对该种文件处理的过程。函数接收(输入)的是加载到资源文件的内容,输出是此次加工后的结果:module.exports = source => { console.log(source) return 'console.log('hello')' }同时在配置文件中配置一下:
module.exports = { // ...other config module: { rules: [ { test: /.md$/, use: './markdown-loader' // use属性也可以设置为路径 } ] } }执行打包时会打印出
md文档中的内容。 -
安装并引入
marked模块:npm i marked --devconst marked = required('marked') module.exports = source => { const html = marked(source) return `module.exports = ${JSON.stringify(html)}` // 或使用ESM的导出 // return `export default ${JSON.stringify(html)}` }同时还可以使用
html-loader来处理这个html字符串:module.exports = { // ...other config module: { rules: [ { test: /.md$/, use: [ 'html-loader', './markdown-loader' ] } ] } }
webpack插件机制
除了loader,插件(plugin)机制是webpack另一个核心特性。插件是为了增强webpack的自动化能力。
webpack常见插件
-
自动清除上次打包结果:
clean-webpack-plugin安装:
npm i clean-webpack-plugin --dev使用:
// webpack.config.js // 需要引入插件导出的CleanWebpackPlugin构造函数 const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { // ...other config plugin: [ // plugin属性的值是一个数组 new CleanWebpackPlugin() ] } -
自动生成使用
bundle.js的HTML:html-webpack-plugin安装:
npm i html-webpack-plugin --dev使用:
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { // ...other config plugin: [ new HtmlWebpackPlugin({ // 可以传入一个对象对其进行配置 title: 'Webpack', // 设置title标签内容 meta: { // 设置meta标签属性 viewport: 'width=device-width' } }) ] }如果需要大量的设置
dom元素等,可以使用模板。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack</title> </head> <body> <div class="container"> <!-- 可以通过模板字符串的形式给对应的位置设上变量 --> <h1><%= htmlWebpackPlugin.options.title %></h1> </div> </body> </html>同时在配置中加入
template属性:new HtmlWebpackPlugin({ template: './src/index.html' })如果需要输出多个页面文件,可以再添加一个插件实例:
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { // ...other config plugin: [ new HtmlWebpackPlugin({ // 可以传入一个对象对其进行配置 title: 'Webpack', // 设置title标签内容 meta: { // 设置meta标签属性 viewport: 'width=device-width' }, template: './src/index.html' }), new HtmlWebpackPlugin({ filename: 'about.html' // 指定输出文件的名称 }) ] } -
处理不需要处理的静态文件:
copy-webpack-plugin安装:
npm i copy-webpack-plugin --dev使用:
const CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { plugin: [ new CopyWebpackPlugin([ // 传入一个数组,来指定需要拷贝的文件的路径,例如网站的ico './public/favicon.ico', // 文件的相对路径 './public', // 或者整个文件夹 ]) ] }
webpack插件机制原理
plugin是由钩子机制(hooks)实现的。钩子机制类似“事件”,在webpack工作过程中会有很多的环节,为了webpack的扩展,每个环节都被埋下了钩子,然后在这些钩子下添加不同的任务就可以扩展webpack的能力。
webpack要求插件必须是一个函数或者是一个包含apply方法的对象。一般都会把它定义成一个类型,然后在类型里面添加一个apply方法,使用时通过这个类型去构建一个实例去使用。
class MyPlugin {
// apply方法接收一个compiler对象
// compiler对象是webpack工作时最核心的一个对象,它包含了此次构建的所有信息
// 可以通过这个对象去注册钩子函数
apply(compiler) {
console.log('plugin working')
// tap方法第一个参数是插件的名称
// 第二个参数是个回调函数,里面传入的compilation可以理解成此次打包的上下文
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation下的assets属性包含了此次打包过程中的各个文件
for(const name in compilation.assets){
if(name.endWith('.js')) {
// compilation.assets下每一个键的值都有source方法,该方法会返回当前文件的内容
const contents = compilation.assets[name].source()
// 替换js文件中的注释
const withoutComments = contents.replace(/\/\*\*+\//g, '')
// 然后再将当前name下的source方法和size给重写一遍
compilation.assets[name] = {
source: () => withoutComments,
// size方法会返回当前文件的大小,这个是必需的
size: () => withoutComments.length
}
}
}
})
}
}
这是以上示例中
emit钩子的官方解释:
emitAsyncSeriesHook生成资源到 output 目录之前。
参数:
compilation
相关钩子的官方文档: compiler
更深层的webpack的插件机制需要靠阅读源码来进行了解,后面有机会再写一篇关于webpack源码的笔记。
webpack自动编译
使用webpack-cli中的watch工作模式可以实现自动编译的功能。在watch模式下,会监视文件变化,然后再自动重新打包。
npx webpack --watch
#or
yarn webpack --watch
webpack dev server
使用webpack-dev-server可以实现“修改源代码,页面就能同步”的效果。而且通过传入open参数可以实现自动打开浏览器。
yarn webpack-dev-server --open
对于一些静态文件,可以通过配置devServer属性来让其在打包过程中不参与打包,这样就能提高源代码修改后重新打包的效率。
module.exports = {
// ...other config
devServer: {
contentBase: './public' // contentBase可以是一个路径的字符串也可以是一个包含多个路径字符串的数组
}
}
当使用webpack-dev-server时,项目自然就是在本地localhost服务上启动的,此时发起网络请求就不可避免的产生跨域问题,此时可以通过配置devServer下的proxy属性来让带有指定请求路径前缀的请求被代理以此解决跨域问题。
module.exports = {
// ...other config
devServer: {
contentBase: './public',
proxy: {
'/api': { // 这个是请求路径前缀,所有以/api开头的请求都会被代理到接口中
target: 'https://www.exanple.com', // 代理目标,原本要访问的地址
// 当请求https://localhost:8080/api/xxx时,会被代理到https://www.example.com/api/xxx
// 若代理目标不需要这个请求前缀,可以通过pathRewrite属性实现路径重写
pathRewrite: {
'^/api': ''
},
changeOrigin: true, // 改变发送请求的host,
}
}
}
}
Source Map
在开发过程中,打包后的结果报错需要通过source map来定位该错误在源代码中的位置,从而提高开发体验。
module.exports = {
// ... otehr config
devtool: 'source-map', // 开启source map
}
webpack支持大概12种不同的source map方式,每种方式的效率和效果都是不一样的,效果好的生成效率就会降低。以下是官方给出的不同模式下的效果和效率的对比:
| devtool | performance | production | quality | comment |
|---|---|---|---|---|
| (none) | build: fastest rebuild: fastest | yes | bundle | Recommended choice for production builds with maximum performance. |
eval | build: fast rebuild: fastest | no | generated | Recommended choice for development builds with maximum performance. |
eval-cheap-source-map | build: ok rebuild: fast | no | transformed | Tradeoff choice for development builds. |
eval-cheap-module-source-map | build: slow rebuild: fast | no | original lines | Tradeoff choice for development builds. |
eval-source-map | build: slowest rebuild: ok | no | original | Recommended choice for development builds with high quality SourceMaps. |
cheap-source-map | build: ok rebuild: slow | no | transformed | |
cheap-module-source-map | build: slow rebuild: slow | no | original lines | |
source-map | build: slowest rebuild: slowest | yes | original | Recommended choice for production builds with high quality SourceMaps. |
inline-cheap-source-map | build: ok rebuild: slow | no | transformed | |
inline-cheap-module-source-map | build: slow rebuild: slow | no | original lines | |
inline-source-map | build: slowest rebuild: slowest | no | original | Possible choice when publishing a single file |
eval-nosources-cheap-source-map | build: ok rebuild: fast | no | transformed | source code not included |
eval-nosources-cheap-module-source-map | build: slow rebuild: fast | no | original lines | source code not included |
eval-nosources-source-map | build: slowest rebuild: ok | no | original | source code not included |
inline-nosources-cheap-source-map | build: ok rebuild: slow | no | transformed | source code not included |
inline-nosources-cheap-module-source-map | build: slow rebuild: slow | no | original lines | source code not included |
inline-nosources-source-map | build: slowest rebuild: slowest | no | original | source code not included |
nosources-cheap-source-map | build: ok rebuild: slow | no | transformed | source code not included |
nosources-cheap-module-source-map | build: slow rebuild: slow | no | original lines | source code not included |
nosources-source-map | build: slowest rebuild: slowest | yes | original | source code not included |
hidden-nosources-cheap-source-map | build: ok rebuild: slow | no | transformed | no reference, source code not included |
hidden-nosources-cheap-module-source-map | build: slow rebuild: slow | no | original lines | no reference, source code not included |
hidden-nosources-source-map | build: slowest rebuild: slowest | yes | original | no reference, source code not included |
hidden-cheap-source-map | build: ok rebuild: slow | no | transformed | no reference |
hidden-cheap-module-source-map | build: slow rebuild: slow | no | original lines | no reference |
hidden-source-map | build: slowest rebuild: slowest | yes | original | no reference. Possible choice when using SourceMap only for error reporting purposes. |
webpack HMR(模块热替换)
当开启webpack dev server后,会在开发过程中提供很多便利。但是还有些问题,比如当输入框内输入文本后再去修改源代码,此时dev server会重新渲染界面,之前输入框内的文本就会消失了。若想解决这样的问题需要使用HMR这个功能。HMR可以只将修改的模块实时替换到应用中而不是重新渲染整个页面。
HMR集成在webpack dev server之中,使用时需要在命令中加入hot参数来开启HMR:
yarn webpack-dev-serve --hot
也可以在配置文件中开启:
// 由于需要在插件中使用HMR,所以先导入webpack
const webpack = require('webpack')
module.exports = {
// ...other config
devServer: {
hot: true
},
plugins: [
// 使用这个插件
new webpack.HotModuleReplacementPlugin()
]
}
这样会在样式文件修改时自动替换样式,对于js文件还需要额外的配置。
在打包的入口文件可以使用HMR的API来实现js文件的热替换:
import header from './header.js' // 引入的文件
// 引入的这个js文件是一个函数,其调用结果是在body上创建一个元素
// 通过module.hot.accept方法来处理需要热替换的文件
// 第一个参数是要处理的文件的路径,
// 第二个参数是文件发生改变后的回调函数
let head = header() // 将结果保存下,便于后面更新时替换
module.hot.accept('./header.js', () => {
// 在文件发生变化时将页面上的元素移除
document.body.removeChild(head)
const newHead = header() // 这是新的
document.body.appenChild(newHead)
head = newHead // 将这次的结果保存,便于后面更新时替换
})
如果想要保存里面可输入这样的内容不变,也可以先将这样的内容保存下来,然后在更新后再给它重新赋值上去,这样就能保持内容不变。
对于每个不同的js文件都需要不同的处理函数来应对热替换。
DefinePlugin
在使用vue开发过程中,经常会看到process.NODE.ENV这样的全局属性。如果想要自定义这些属性就可以通过DefinePlugin来做到。
DefinePlugin是webpack内置的一个插件,所以使用的时候直接引用就可以了:
const webpack = require('webpack')
module.exports = {
// ...other config
plugins: [
new webpack.DefinePlugin({
// 属性值的引号里面是需要一个js片段,这里我们需要的是字符串,所以需要在引号里面再加个引号
API_BASE_URL: '"www.example.com"', // 为全局注入API_BASE_URL这个属性
})
]
}
这样在项目中使用这个地址的话就可以直接使用API_BASE_URL这个地址,当需要修改这个地址时,只要在配置文件中修改一次即可,不需要全局去找地址然后修改。
Tree-Shaking(摇树)
在开发过程中,有时会有一些导出但未被引用的代码(Dead-code),这些代码对于整个项目来说是无用,所以需要不需要将其打包到生产环境中。将这些未引用代码给剔除的过程就叫摇树tree-shaking。
在webpack打包的过程中会有自动的tree-shaking。如果想要手动配置,也可以通过optimization这个属性来配置:
module.exports = {
// ...other config
optimization: {
usedExports: true, // 只导出外部使用了的成员
minimize: true, // 清除未被引用的成员
}
}
usedExports相当于负责标记dead-code,minimize相当于负责清除dead-code
还可以通过concatenateModules这个属性来将所有的模块合并到同一个函数中以此减少体积。
注意:
Tree-Shaking的前提是使用ES Modules,也就是说交给webpack去打包的代码需要使用ES Modules去实现了模块化。
当使用了babel去转译代码时,它会先将ES Modules转换成CommonJS,导致tree-shaking失效。
但是结果却是tree-shaking并不会失效,因为在最新版本的babel-loader中已经关闭了ES Modules转换的插件。
SideEffects
通过配置的方式去标识代码是否有副作用,从而为tree-shaking提供更大的压缩空间。
sideEffects一般用于npm包标记是否有副作用。
module.exports = {
// ...other config
optimization: {
sideEffects: true, // 开启sideEffects
}
}
开启后打包文件webpack会先检查package.json中是否有sideEffects标识,以此来判断这个模块是否有副作用。如果没有副作用,那么这些没有用到的模块就不会去打包。
{
"sideEffects": false
}
设为
false是表示整个项目没有副作用。
注意:
使用sideEffects时要确定整个代码没有副作用,否则打包时会删除有副作用的代码。
比如当有的js文件只进行一些操作而未导出成员时,这类代码就有副作用:
import Vue from 'vue'
Vue.prototype.$http = xxx
还有打包入口导入的样式文件,也会有副作用。
解决办法就是关闭sideEffects或者在package.json中标识这些文件:
{
"sideEffects": [
"./utils/http.js",
"./assets/styles/main.less"
]
}
也可以使用通配符的方式来配置:
*.less。
Code Splitting
在打包过程中,所有模块都会被打包进入同一个模块,这就导致生产环境下在项目启动时会一次性加载所有模块,从而产生性能问题。
这时候就需要根据不同的模块需要来分包按需加载。
webpack实现分包的方式主要有两种:
- 多入口打包
- 动态导入
Multi Entry(多入口打包)
一般适用于多页面应用程序,常见的规则就是一个页面对应一个打包入口,公共代码可以单独提取。
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// ...other config
// entry属性的值改成一个对象,一个路径对应一个页面
entry: {
index: './src/index.js',
detail: './src/detail.js'
},
output: {
// 输出的文件名也需要修改
filename: '[name].bundle.js', // 这样是动态输出文件名,name对应entry里面的属性名
},
plugins: [
// 设置多个页面
new HtmlWebpackPlugin({
title: 'index',
template: './src/index.html',
filename: 'index.html',
// 设置chunks来让每个页面只引入自己的chunk,而不是一下引入所有的chunk
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'detail',
template: './src/detail.html',
filename: 'detail.html',
chunks: ['detail']
})
]
}
当不同的页面有相同的模块时,还需要额外的配置:
module.exports = {
// ...other config
optimization: {
splitChunks: {
chunks: 'all' // 设置所有的公共模块都提取到单独的bundle中
}
}
}
动态导入
需要某个模块时才加载这些模块。所有动态导入的模块都会被自动分包。
在项目导入时,可以这样:
const hash = window.location.hash || '#index'
if(hash === '#index') {
// 使用import函数导入
import('./utils/greeting').then(({ default }) => {
default()
})
}
Magic Comments(魔法注释)
当需要给分包后的文件取名时,可以使用魔法注释来实现bundle的自定义名称:
// 在import函数的路径前面加上注释
import(/* webpackChunkName: 'greeting' */ './utils/greeting')
相同的
chunk name会被打包到一起
MiniCssExtractPlugin
MiniCssExtractPlugin可以将css模块提取出来打包到单独的文件中实现css文件的懒加载。
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// style-loader改成MiniCssExtractPlugin下的loader
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
// ...other config
plugins: [
new MiniCssExtractPlugin()
]
}
不使用这个插件时,
css模块将通过css-loader、style-loader的加工后在页面上生成一个style标签,而使用MiniCssExtractPlugin后,样式将会被存放在单独的文件中,然后通过link的方式去引入。当样式文件不大时(size <= 150kb),建议使用
style-loader
optimize-css-assets-webpack-plugin
上面生成的样式文件并不会被压缩,所以需要这个插件来将其进行压缩一下:
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
// ...other config
optimization: {
minimizer: [
// 再次配置js压缩插件
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
}
OptimizeCssAssetsWebpackPlugin放在optimization下的minimizer属性中而不是在plugins中的原因是便于统一通过minimize属性来控制打包结果是否压缩。但是这样会有一个问题,就是
js文件不会自动压缩了。这时就需要手动配置js压缩插件。
substitutions(输出文件名hash)
在部署静态资源文件时,都会启用静态文件缓存。当静态资源缓存时间过短可能会导致没什么效果,过长则在更新后没有办法更新到客户端。
所以在生成模式下,可以给文件名使用hash,这样当文件发生改变时,文件名也可以随之改变。当文件名改变时,浏览器就会重新发起请求。这样就可以在缓存策略中把缓存时间设置的很长。
module.export = {
// ...other config
output: {
// 这样是整个项目级别的hash
filename: '[name]-[hash].bundle.js'
// 这样是chunk级别的hash
// filename: '[name]-[chunkhash].bundle.js'
// 这样是文件级别的hash
// filename: '[name]-[contenthash].bundle.js'
// 还可以指定hash的长度
// filename: '[name]-[contenthash:8].bundle.js'
}
}