带你一步步深入webpack!

1,389 阅读13分钟

准备工作

首先必须保证本地安装了nodejs,因为webpack是构建在其之上,请参考官网。下载最新的LTS,也就是长期维护版,像安装其他软件一样,狂按下一步即可,完成之后打开命令行工具,也就是cmd

node -v
npm -v

通过上面两条命令即可得到node以及npm的版本信息,注意:npmnode的包管理工具。

58FDFC0F-6D11-465B-95FF-2B8031104EC9.png

安装

在安装webpack的时候一般有两种情况:全局本地工作目录。二者的区别便是,通过全局安装的webpack可以在计算机任何目录执行webpack命令,而后者只能在当前工作目录执行。

在安装的时候我们通常会安装2个包:webpackwebpack-cli,前者是wepack主包,后者可以在命令行里面执行webpack命令。

全局安装

下面就开始全局安装webpack

npm install webpack webpack-cli --global

上面是完整的命令行代码,也可通过下面的简写模式。

npm i -g webpack webpack-cli

这里需要注意的是,在使用mac的小伙伴,一定要记得在最前面加上sudo

sudo npm i -g webpack webpack-cli

按照提示输入电脑的登陆密码即可。

安装完成后,通过webpack -v来查看下相关信息。

AD70D233-F002-4522-A2DD-D427BC602968.png

到这里,webpack全局安装就完成了,但是在实际工作中还是不大推荐这种形式,这样会让你所有项目锁定一个webpack版本,而且在运行不同webpack版本项目会导致构建失败。

为了更加灵活且避免一些不必要的错误,在实际工作中还是推荐在当前工作目录下安装,这样就算换了一台电脑也不会出现问题。

本地工作目录安装

在开始之前,需要先创建一个工作目录,并且初始化一个node的配置文件,也就是我们常见的package.json

npm init -y

这样就会在项目根目录自动创建好package.json文件。

接下来,就开始在当前目录执行如下命令:

npm install webpack webpack-cli --save-dev
//简写
npm i -D webpack webpack-cli

到此,本地安装就完成了,我们就可以在当前目录下执行webpack命令。

运行

为了方便我们测试简单的webpack打包,我们先创建2个js文件。

src/c1.js

c1.js中,我们通过export默认导出一个add函数。

export default function add (a, b) {
  console.log(`a+b=${a + b}`);
}

src/index.js

index.js中,我们通过import引入c1.js导出的add函数,并进行一次调用。

import add from './c1.js'
add(3, 4)

通过在当前目录下执行webpack命令,这时候会自动创建一个dist目录,里面包含一个main.js,这就是webpack为我们打包出来的js代码。

仔细思考一下,我们貌似并没有配置什么,也就是单纯的安装了webpack必须的两个依赖包,那到底webpack是依据什么来进行打包的?

webpack --stats detailed

执行上述命令,我们就可以看到详细的webpack打包信息了。

思考:这时候在命令行执行的webpack命令是全局的还是当前目录下的呢?

为了测试这一结果,我们卸载掉全局的webpack

npm uninstall --global webpack webpack-cli

然后,我们再次在工作目录下执行webpack指令,我们会发现,webpack找不到了,这就说明,我们刚才执行的指令是全局指令。

2F25E651-AFC4-4947-B29F-BDAF89F7422F.png

如果我们没有全局的webpack,是不是这个项目就没法构建了?不是的,这时候我们可以使用npx来执行webpack命令。

npx webpack

392D2498-3D72-499C-9A9D-6542C77E5D9D.png

这时候,我们又能正常进行打包了。

有小伙伴这时候可能就有疑问了,npx到底是什么?居然有如此魔法,那他到底使用的是哪里的webpack

npxnpm5.2之后发布的一个命令,简单理解就是我们可以用npx来执行各种命令,npx会默认从当前目录去查找有没有webpack,如果没有就会依次往上一层去寻找,而这时候我们当前目录正好有webpack,所以,npx就会执行当前目录下的webpack

到这里,webpack的准备工作以及如何运行就介绍完了,但是我们还并不会去控制webpack按照我们的需求来进行构建,例如,我要以哪个文件为入口,我要将打包文件放在哪里等等。接下来我们就逐步介绍webpack的配置文件。

配置

webpack默认提供了两种配置形式:终端命令配置文件

终端命令

webpack给我们提供了丰富的命令行指令,通过webpack --help来查看。

接下来,我们小试牛刀,来看看常用的命令行指令。

例如:我们要指定入口为:src/index.js,并且指定modeproduction

npx webpack --entry ./src/index.js --mode production

这样,我们会看到再次生成dist/main.js,如果我们想自定义目录以及生成的文件名呢,这时候我们需要配置其他参数,这样一来,我们的命令会变得非常长,这样肯定不是最好的方案,webpack也给我们提供了另外一种配置方案,通过webpack.config.js来配置这些打包信息。

配置文件

首先,在项目根目录创建webpack.config.js文件(不可更改文件名),配置文件默认采用的是commonjs语法,通过module.exports导出一个配置对象,webpack会自动去读取该配置文件。

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist') //这里必须用path处理路径,否则报错。
  },
  mode: 'none' //指定开发模式,避免控制台报错。
}

上面就是一个最基本的webpack的配置文件,指定了入口,出口以及开发环境,当我们再次执行:npx webpack,也会生成dist/bundle.js

难道webpack就这点能耐?接下来我们开始了解webpack的插件,这样可以简化我们的开发流程。

插件

我们可以把webpack想象成一条生产线,入口文件我们就想象成原材料,中间会经过各种工序才能生产出我们需要的成品,而这中间有可能会用到各种工具来辅助,我们就可以把webpack的插件想象成这些工具。

下面我们来介绍几种工作中常用的插件。

HtmlWebpackPlugin

在实际工作中,我们肯定不仅仅是为了生成一个js文件这么简单,我们还需要将生成的js文件自动引入html中才算完美,不然每次还要手动去引入,稍不注意还会出错,这个插件就是为了解决我们这个问题。

首先,当然是安装插件:

npm i -D html-webpack-plugin

安装完之后,我们当然要在webpack.config.js中配置该插件。

...
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  ...
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

再次执行:npx webpack,这时候在dist目录下不仅仅只有一个bundle.js了,还有一个index.html,并且已经自动引入我们生成的js文件,在浏览器中打开html文件,js也是可以正常运行的。

我们打开生成的index.html发现里面的代码都是插件给我们自动生成的,可恶的控制欲不能忍受不可控的代码,我们能让他按照我们提供的html格式生成文件吗?当然是可以的!

new HtmlWebpackPlugin({
  template: 'index.html', //模板文件
  filename: 'app.html', //生成的html文件名称
  inject: 'body' //js插入的位置,默认在head。
})

这样,我们就可以为所欲为了。

再次打开熟悉的dist目录,除了之前的html文件,又给我们生成了新的app.html,之前的html文件自然就没用了,总不能每次要我们手动去删除吧,当然不行,文件少还行,万一来个几十上百个无用文件,不说了,手抖。

清理dist

要在每次打包之前都清理掉之前的旧文件,只需要在output中配置clean属性即可。

output: {
  ...,
  clean: true
}
使用 source map

这时候,我们故意写个错误的语法,

修改src/index.js

...
cosole.log('hhh');

webpack.config.js配置文件中的mode改成development,再次执行:npx webpack,并且在浏览器打开dist/app.html

54078A9C-CB3A-41D5-9018-734BF1935DAB.png

8ADC79B8-4573-4733-9363-C0EA40A5864D.png

我们可以发现他并不能指向我们源码中的错误位置,这对我们开发调试来说简直太难了。

接着,我们修改webpack.config.js,配置devtool为:inline-source-map,并再次执行打包,并刷新浏览器。

module.exports = {
  ...,
  devtool: 'inline-source-map'
}

1370904F-6759-402D-9C74-EBF009F1375D.png

CDA6644D-B842-4CFA-80B7-FA1168CF5806.png

这次就正确的指出了错误语法的位置了。

思考:不知小伙伴注意到没有,我们每需改一次都要手动执行npx webpack,这哪里是前端自动化,这跟工地搬砖有啥区别,那怎样可以一劳永逸,很简单,执行:npx webpack --watch,给webpack加个监工不就是了,这样只要我们的项目有文件改变了,就会自动执行打包。

自动打包解决了,可每次我们还是要手动刷新一下浏览器才能看到最新的结果,这显然不是我们所期待的。

搭建开发环境

webpack-dev-server就是用来解决我们上一个痛点,通过它可以在我们本地启动一个服务器,并且可以实现实时刷新。

安装 webpack-dev-server

npm i -D webpack-dev-server

修改webpack.config.js

module.exports = {
  ...,
  devServer: {
    static: './dist'
  }
}

执行npx webpack-dev-server,这时候就会自动启动一个server

B59EDC7F-43F8-41E9-96E9-FAD19B228AC5.png

在浏览器打开:http://localhost:8080

真相:webpack-dev-server并不需要dist目录的文件,而是直接读取的内存,所以就算我们删掉dist目录也依然能正常访问。

资源模块

webpack除了可以打包我们的js文件,还可以打包其他文件吗?例如:字体图片等等,在这之前,我们通常用gulp或者grunt来处理我们的资源文件,将资源移动到dist或者build,其实webpack最为出色的除了引入js还可以使用内置的资源模块(asset modules)来引入任何其他类型的资源。

资源模块类型分为如下4种:

asset/resource: 发送一个单独的文件并导出URI

asset/inline:导出资源的Data URI

asset/source:导出资源的源代码

asset:会在导出一个URI和发送一个单独的文件之间自动选择

Resource资源

接下来,就引入一张图片来了解下Resource的用法,首先当然是要添加一张图片了,地址:src/assets/1.jpg

修改webpack.config.js文件。

module.exports = {
  ...,
  module: {
    rules: [
      {
        test: /\.jpg$/,
        type: 'asset/resource'
      }
    ]
  }
}

修改src/index.js

...
import imgurl from './assets/1.jpg';
...
const imgdom = document.createElement('img')
imgdom.src = imgurl
document.body.appendChild(imgdom)

执行:npx webpack,我们可以看到在dist目录会自动生成一张图片,文件名称是webpack自动生成的,打开dist目录的app.html便能看到我们添加的图片正常的显示在页面中了。

现在有一个问题,所有的资源文件都是直接打包到dist根目录,这样文件一多就会很乱,如何把图片文件打包到dist/images目录?有如下两种方式:

1、配置output,添加assetModuleFilename属性。

提示:contenthash表示根据内容自动生成hash值

module.exports = {
  ...,
  output: {
    ...,
    assetModuleFilename: 'images/[contenthash][ext]'
  }
}

2、配置rules的时候,添加generator属性。

module.exports = {
  ...,
  rules: [
    {
      ...,
      generator: {
        filename: 'images/[contenthash][ext]'
      }
    }
  ]
}

上面两种配置都可以自定义资源生成的目录及文件名,generator优先级高于assetModuleFilename

Inline资源

导出的是文件的URI,为了测试效果,这次我们引入一张svg图片。

编辑webpack.config.js,添加一个新的rule规则。

module.exports = {
  module: {
    rules: [
      ...,
      {
        test: /\.svg$/,
        type: 'asset/inline'
      }
    ]
  }
}

再次在src/index.js中导入svg图片。

...
import svgurl from './assets/2.svg'
const svgdom = document.createElement('img')
svgdom.src = svgurl
document.body.appendChild(svgdom)

执行npx webpack,这次可以看到并没有在dist/images里面生成一个svg文件,打开控制台,可以看到svg文件是以base64形式引入的。

499B600F-D772-4618-968D-977E276B6F10.png

Source

用于导出资源源代码。

编辑webpack.config.js

module.exports = {
  module: {
   rules: [
     ...,
     {
       test: /\.txt$/,
       type: 'asset/source'
     }
   ]
  }
}

创建src/assets/3.txt

Hello Webpack!

修改src/index.js

...
import txt from './assets/3.txt';
...
const txtdom = document.createElement('h2')
txtdom.innerText = txt
document.body.appendChild(txtdom)

执行npx webpack之后我们就能看到Hello Webpack!就能正常在页面中显示了。

asset

通用资源类型,在导出一个 data URI和发送一个单独的文件之间自动选择,默认情况,小于8kb的文件将被视为inline资源类型,否则被视为resource资源模型。

修改webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.jpg$/,
        type: 'asset'
      }
    ]
  }
}

思考:如果想修改这个临界值该怎么办?

其实很简单,只需要在上面的rule里面再加上parser配置即可。

parser: {
  dataUrlCondition: {
    maxSize: 4* 1024 * 1024 //4M
  }
}

上述的配置表明当png文件大于4M才会生成URI,否则便会生成base64

Loader

webpack默认支持jsjson这种文件,比如我们常用的css文件等,webpack默认是不能正确解析的,这时候我们就需要对应的loader来解析这些文件,并且将这些文件转换成对应的模块,供我们使用。

定义

loader的定义是在rule的基础上,上面我们了解了testtypeparser,loader是通过use来指定当前资源类型使用了什么loader来进行转换。

加载css文件

css是我们web开发中不可缺少的资源类型,下面就开始使用loader来加载并对css文件进行转换。

创建src/assets/main.css

body {
  background: #333;
  color: #fff;
}

修改src/index.js

...
import './assets/main.css';
...

安装loader

npm i -D css-loader style-loader

修改webpack.config.js,添加如下rule

{
  test: /\.css$/,
  use: ['style-loader', 'css-loader']
}

执行npx webpack这样我们的css就成功引入到页面中了。

注意:use后面的数组是从后往前执行的,上面的意思也就是先调用css-loader,然后才调用style-loader

现在css我们能正常引入了,那我们能用sass这种吗?

sass预处理

npm i -D sass-loader sass

我们需要再安装sass-loader,并且在use数组最后面加上sass-loader,同理,我们可以用less-loaderless处理less,用postcss-loaderpostcss处理postcss,这些大家自行使用就行了。

{
  test: /\.css$/,
  use: ['style-loader', 'css-loader', 'sass-loader']
}

配置好之后,我们创建src/assets/main.scss

$a: #f00;
h2 {
  color: $a;
}

src/index.js中引入scss文件。

import './assets/main.scss';

执行npx webpack,我们再次打开app.html,就能看到我们刚写的scss生效了。

思考:上面我们通过style-loadercss-loadersass-loader让我们的项目支持了css引入,可引入的css都是直接在html中插入,我们怎样把css分离出来呢?

Css抽离压缩

我们需要借助mini-css-extract-plugin来实现。

注意:必须是webpack5以上版本。

修改webpack.config.js

在配置文件里面引入插件,在plugins进行实例化,替换之前的style-loaderMiniCssExtractPlugin.loader

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
  ...,
  plugins: [
    ...,
    new MiniCssExtractPlugin()
  ],
  module: {
    rules: [
      ...,
      {
        test: /\.(css|scss)\$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  }
}

执行npx webpack,可以看到在dist目录下多了个main.css,我们上面引入的cssscss都打包在这里,并且app.html也会自动引入打包的这个css文件,(html-webpack-plugin)。

同样的问题,我们能否自己指定生成css文件的路径以及文件名呢?

修改webpack.config.js,只需要在实例化MiniCssExtractPlugin的时候传入对应的参数即可。

module.exports = {
  ...,
  plugins: [
    ...,
    new MiniCssExtractPlugin({
      filename: 'css/[contenthash].css'
    })
  ]
}

执行npx webpack,这样我们就能在dist目录的对应位置生成一个随机文件名的css文件,打开css文件我们会发现里面有很多注释,并且代码并没有压缩,要怎么把css代码压缩打包呢?

安装css-minimizer-webpack-plugin

npm i -D css-minimizer-webpack-plugin

修改配置文件

const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
  ...,
  mode: 'production',
  optimization: {
    minimizer: [
      new CssMinimizerPlugin()
    ]
  }
}

再次执行打包,我们的css文件就被压缩了。

加载Font字体

src/assets准备好一个iconfont.ttf文件,并且在src/assets/main.css里面使用。

@font-face {
  font-family: 'iconfont';
  src: url('./iconfont.ttf') format('truetype');
}
.icon{
  font-family: 'iconfont';
  font-size: 30px;
}

修改src/index.js

const span = document.createElement('span')
span.className = 'icon'
span.innerHTML = '&#xe01d'
document.body.appendChild(span)

执行编译,我们就能看到我们添加的iconfont图标了。

加载数据

这里做简单介绍,了解即可。

安装依赖

npm i -D csv-loader xml-loader

修改配置文件

module.exports = {
  module: {
    rules: [
      {
        test: /\.(csv|tsv)$/,
        use: 'csv-loader'
      }, {
        test: /\.xml$/,
        use: 'xml-loader'
      }
    ]
  }
}

接下来我们就能直接在业务代码中直接引用对应的文件格式。

提示:xml-loader会转换成数组,csv-loader会转换成对象。

自定义Json模块Parser

安装3个包:tomlyamljson5

cnpm i -D toml yaml json5

修改配置文件:

const toml = require('toml')
const yaml = require('yaml')
const json5 = require('json5')
module.exports = {
  module: {
    rules: [
      ...,
      {
        test: /\.taml$/,
        type: 'json',
        parser: {
          parse: toml.parse
        }
      }, {
        test: /\.yaml$/,
        type: 'json',
        parser: {
          parse: yaml.parse
        }
      }, {
        test: /\.json5$/,
        type: 'json',
        parser: {
          parse: json5.parse
        }
      }
    ]
  }
}

到这里,我们就可以在项目中引入对应的文件类型了。

Babel

babel是为了解决不同浏览器js的兼容性转换,下面我们稍微修改一下cs.js,编写一些es6的语法。

function nadd (a, b) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(a + b);
    }, 1000)
  })
}

async function add () {
  let string = await nadd(2, 4)
  console.log(string)
}

export default add

里面我们用到了Promise,我们再执行打包看看效果。

chrome浏览器上能够正常执行,可在ie上就没那么幸运了,这时候我们就需要用到babel来对我们的js代码进行转换。

babel-loader

安装相关依赖

npm i -D babel-loader @babel/core @babel/preset-env

修改 webpack.config.js,我们继续添加一个rule

{
  test: /\.js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env']
    }
  }
}

再次执行打包,这次是不是就可以了呢?打开浏览器控制台,引入眼帘的却是一个报错。

DE011D1E-6274-4EAB-8477-218F43991362.png

regeneratorRuntime是webpack打包生成的全局辅助函数,由babel生成,用于兼容async/await的语法。

下面开始配置babel的运行时,也就是@babel/runtime@babel/plugin-transform-runtime会在需要regeneratorRuntime的时候自动打包。

npm i -D @babel/runtime @babel/plugin-transform-runtime

修改webpack.config.js配置文件,给上面的usepresets后面添加plugins

plugins: [
  [
    '@babel/plugin-transform-runtime'
  ]
]

代码分离

代码分离可以把代码分离到不同的bundle中,可以获取更小的bundle以及控制资源加载的优先级。

常见的分离方法有如下三种:

为了测试分离,安装lodash

npm i -S lodash

配置入口起点

使用entry配置手动分离代码。

缺点:如果是多个入口,那么这些入口共享的代码文件会被重复打包。

创建src/module1.js

import _ from 'lodash'
console.log(_.join(['a', 'module', 'loaded']))

接下来,在webpack.config.js中配置多入口

entry: {
  index: './src/index.js',
  module1: './src/module1.js'
}

再次执行打包

895B8A4B-B57B-4F4F-9BE6-39FDE59FA700.png

很明显,还要修改一下output中的filename

filename: '[name].bundle.js'

再次执行打包就可以了。

修改src/index.js,在这里我们也引用一下lodash的方法。

...
import _ from 'lodash'
...
console.log(_.join(['index', 'module', 'loaded']))

执行打包,我们可以看到lodash同时被打进index.bundle.jsmodule1.bundle.js,这样肯定是不行的。

防止重复

用来解决第一种方案的代码重复,使用Entry dependencies或者SplitChunksPlugin去重和分离代码。

Entry dependencies

修改webpack.config.js中的entry,给每个入口添加一个dependOn属性

entry: {
  index: {
    import: './src/index.js',
    dependOn: 'shared'
  },
  module1: {
    import: './src/module1.js',
    dependOn: 'shared'
  },
  shared: 'lodash'
}

再次打包,可以看到多出了一个shared-bundle.js,这就是共享的lodash代码。

SplitChunksPlugin

还有另外一种方式来达到代码分离,我们再次修改webpack.config.js中的entryoptimization

entry: {
  input: './src/index.js',
  module1: './src/module1.js'
},
...,
optimization: {
  ...,
  splitChunks: {
    chunks: 'all'
  }
}

动态导入

通过模块的内联函数调用来分离代码。

修改webpack.config.js入口,只保留src/index.js,将上面的代码分离配置注释掉,也就是splitChunks配置。

entry: {
  input: 'src/index.js'
}

创建src/module2.js

function getComponent () {
  return import('lodash').then(({default: _}) => {
    const element = document.createElement('div')
    element.innerHTML = _.join(['hello', 'lodash'], ' ')
    return element
  })
}

getComponent().then((element) => {
  document.body.appendChild(element)
})

src/index.js中的lodash引入和使用都注释掉,并引入module2.js

import './module2.js';

懒加载

懒加载或者按需加载,是一种很好的优化网页或应用的形式,这种方式实际上是先把代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或者即将引用另外一些新的代码块。这样加快了引用的初始加载速度,减轻总体体积。

创建module3.js

export const add = (a, b) => {
  return a + b
}
export const minus = (a, b) => {
  return a - b
}

修改src/index.js,在页面中创建一个按钮,点击执行add方法。

const button = document.createElement('button')
button.textContent = '点击执行'
document.body.appendChild(button)

button.addEventListener('click', () => {
  import(/*webpackChunkName: 'math'*/'./module3.js').then(({add}) => {
    console.log(add(4, 5))
  })
})

页面初始化的时候并没有引用math.bundle.js,而是当点击按钮的时候才加载,这就是懒加载。

预获取/预加载

webpack 4.6.0+增加了对预获取和预加载的支持。

在声明import时,使用下面这些内置指令,可以让webpack输出resource hint(资源提示),来告知浏览器。

prefetch:预获取,将来某些导航下可能需要的资源。

页面加载的时候会加载一次,点击按钮的时候会再次加载。

preload:预加载,当前导航下可能需要的资源。

页面加载的时候不会加载,只有当按钮点击的时候加载,这个类似上面我们说到的懒加载。

修改src/index.js中点击按钮的代码如下,添加:webpackPrefetch: true

button.addEventListener('click', () => {
  import(/*webpackChunkName: 'math', webpackPrefetch: true*/'./module3.js').then(({add}) => {
    console.log(add(4, 5))
  })
})

缓存

默认情况下浏览器都会有缓存,如果每次打包的时候我们的文件名没有改变浏览器会默认我们文件没做修改,这时候我们就需要处理一下每次打包的文件名来解决浏览器缓存问题。

修改webpack.config.jsoutput

filename: '[name].[contenthash].js'

再次打包都会根据内容生成hash值。

思考:业务代码实时更新要解决缓存问题,而实际项目中我们会引用一些外部框架,而这些框架是不需要做频繁修改的,如何减少浏览器的请求。

修改webpack.config.jsoptimization添加splitChunks属性。

splitChunks: {
  cacheGroups: {
    vendor: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendors',
      chunks: 'all'
    }
  }
}

到这里为止就处理好所有的js打包分离功能了,现在所有的js都是打包在项目根目录,到底怎么把js文件单独放在一个目录呢?

修改配置文件,修改outputfilename

filename: 'js/[name].[contenthash].js'

这样,我们所有的js代码就都打包到js目录了。

环境变量

首先我们了解下什么是公共路径(publicPath)?

publicPath配置选项在各种场景中都非常有用,可以通过它来指定应用程序中所有资源的基础路径。

修改配置文件,给output添加publicPath配置项。

publicPath: 'http://localhost:5500/'

获取环境变量

通常情况下,为了区分productiondevelopment,我们都会在命令行输入有辨识度的命令,例如:

npx webpack --env production

这时候我们需要修改webpack.config.js,现在是直接导出一个object,我们需要导出一个function,然后在函数里面导出object,这时候我们就能在函数体内获取用户传过来的变量信息。

...
module.exports = (env) => {
  console.log(env)
  return {
    ...
    mode: env.production ? 'production' : 'development'
  }
}

这样我们就可以动态的设置配置文件里面的mode

通过production模式下,会对代码进行压缩,接下来,我们通过terser插件来实现。

npm i -D terser-webpack-plugin

修改配置文件:

...
const TerserWebpackPlugin = require('terser-webpack-plugin')
...
module.exports = {
  ...,
  optimization: {
    minimizer: {
      ...,
      new TerserWebpackPlugin()
    }
  }
}

我们再次执行npx webpack --env production的时候,代码就会被自动压缩了。

思考:现在项目还比较简单,我们可以通过判断的方式来修改配置项,如果项目一复杂,到时候配置文件估计没几个人能看得懂了,可以把不同环境的配置文件拆分开来嘛?

拆分配置文件

下面,我们开始对项目配置文件进行优化。

新建开发环境配置文件config/webpack.config.dev.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const toml = require('toml')
const yaml = require('yaml')
const json5 = require('json5')
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    filename: 'js/[name].js',
    path: path.resolve(__dirname, '../dist'),
    clean: true,
    assetModuleFilename: 'images/[contenthash][ext]'
  },
  mode: 'development',
  devtool: 'inline-source-map',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html', //模板文件
      filename: 'app.html', //生成的html文件名称
      inject: 'body' //js插入的位置,默认在head。
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[contenthash].css'
    })
  ],
  devServer: {
    static: './dist'
  },
  module: {
    rules: [
      ...
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

执行npx webpack -c ./config/webpack.config.dev.js,这时候也能正常打包项目。

新建config/webpack.config.prod.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const toml = require('toml')
const yaml = require('yaml')
const json5 = require('json5')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    filename: 'js/[name].[contenthash].js',
    path: path.resolve(__dirname, '../dist'),
    clean: true,
    assetModuleFilename: 'images/[contenthash][ext]',
    publicPath: 'http://localhost:5500/'
  },
  mode: 'production',
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html', //模板文件
      filename: 'app.html', //生成的html文件名称
      inject: 'body' //js插入的位置,默认在head。
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[contenthash].css'
    })
  ],
  module: {
    rules: [
      ...
    ]
  },
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin()
    ],
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

执行npx webpack -c ./config/webpack.config.prod.js,这时候也能正常打包项目。

思考:每次打包都要手动敲这么长的命令,有办法简化嘛?

修改package.json,添加scripts

"scripts": {
    "start": "webpack serve -c ./config/webpack.config.dev.js",
    "build": "webpack -c ./config/webpack.config.prod.js"
  }

接下来,我们只需要执行yarn start或者yarn build即可。

思考:生产环境下,每次打包都会给我们一长串提示信息,如果我们不想看到这些信息,该怎么办呢?

修改config/webpack.config.prod.js,添加performance

performance: {
  hints: false
}

通过上面的配置文件分离,我们已经成功拆分了两个环境下的配置文件,细心的小伙伴会发现上面的配置中我用了省略号代替了很长一串相似的配置,既然都是相同的,是不是可以抽离出来?

配置合并

新建config/webpack.config.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const toml = require('toml')
const yaml = require('yaml')
const json5 = require('json5')


module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    clean: true,
    assetModuleFilename: 'images/[contenthash][ext]'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html', //模板文件
      filename: 'app.html', //生成的html文件名称
      inject: 'body' //js插入的位置,默认在head。
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[contenthash].css'
    })
  ],
  module: {
    rules: [
      {
        test: /\.jpg$/,
        type: 'asset/resource',
        generator: {
          filename: 'images/[contenthash][ext]'
        }
      }, {
        test: /\.svg$/,
        type: 'asset/inline'
      }, {
        test: /\.txt$/,
        type: 'asset/source'
      }, {
        test: /\.png$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4* 1024 * 1024
          }
        }
      }, {
        test: /\.(css|scss)$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }, {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        type: 'asset/resource'
      }, {
        test: /\.taml$/,
        type: 'json',
        parser: {
          parse: toml.parse
        }
      }, {
        test: /\.yaml$/,
        type: 'json',
        parser: {
          parse: yaml.parse
        }
      }, {
        test: /\.json5$/,
        type: 'json',
        parser: {
          parse: json5.parse
        }
      }, {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              [
                '@babel/plugin-transform-runtime'
              ]
            ]
          }
        }
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

修改config/webpack.config.dev.js

module.exports = {
  output: {
    filename: 'js/[name].js'
  },
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    static: './dist'
  }
}

修改config/webpack.config.prod.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    filename: 'js/[name].[contenthash].js',
    publicPath: 'http://localhost:5500/'
  },
  mode: 'production',
  optimization: {
    minimizer: [
      new CssMinimizerPlugin(),
      new TerserWebpackPlugin()
    ]
  },
  performance: {
    hints: false
  }
}

新建config/webpack.config.js

首先,我们需要安装webpack-merge

npm i -D webpack-merge
const {merge} = require('webpack-merge')

const commonConfig = require('./webpack.common')
const productionConfig = require('./webpack.config.prod')
const developmentConfig = require('./webpack.config.dev')

module.exports = (env) => {
  switch (true) {
    case env.development:
      return merge(commonConfig, developmentConfig)
    case env.production:
      return merge(commonConfig, productionConfig)
    default:
      return new Error('No matching configuration was found')
  }
}

最后,我们还需要修改package.json

"scripts": {
    "start": "webpack serve -c ./config/webpack.config.js --env development",
    "build": "webpack -c ./config/webpack.config.js --env production"
  }

这样,我们整个webpack的配置文件就简单多了,不同环境的配置项单独出来,更利于我们维护。