一文搞定webpack

1,043 阅读20分钟

Webpack概述

什么是Webpack

官方给出的定义:Webpack是现代JS应用程序的静态打包器(moudule bundler)。
Webpack是现在最流行的模块打包工具之一。
中文文档:webpack.docschina.org/

ScreenGif.gif

为什么要使用Webpack

  • ES Modules 等新特性存在环境兼容问题
  • 模块文件过多,网络请求频繁,影响效率
  • 所有的前端资源都需要模块化 前面两个问题我们可以使用一系列模块化工具可以实现,而所有前端资源的模块化无法用一般的模块化工具实现。我们需要工具来统一解决这些问题,而它就是webpack打包工具。
    总之,我们使用webpack打包工具是为了解决前端整体资源的模块化,并不单指JavaScript模块化。 image.png

Webpack核心概念

  • 入口(Entry)
  • 出口(Output)
  • 加载器(Loader)
  • 插件(Plugins)
  • 模式(Mode)
  • 模块(Module)
  • 依赖图(Dependency Graph) 入口(Entry):打包时第一个访问的源码文件。默认是src/index.js(可以通过配置文件指定),Webpack通过入口,加载整个项目的依赖。

出口(Output):打包后输出的文件名称,默认是dist/main.js(可以通过配置文件指定)

加载器(Loader):专门用来处理一类文件(非JS)的工具。webpack默认只能识别JS文件,想要处理其他类型的文件,需要对应的loader,如(css-loader | html-loader | file-loader)。以-loader为后缀。
常用加载器:www.webpackjs.com/loaders/

image.png

插件(Plugins):实现loader之外的其他功能。

  • Plugin是webpack的支柱,用来实现丰富的功能
  • 命名方式xxx-webpack-plugin(如html-webpack-plugin),以-webpack-plugin为后缀
  • 常用插件:www.webpackjs.com/plugins/

Loader和Plugins本质上都是npm包。

模式(Mode):用来区分环境的关键字。不同环境的打包逻辑不同,因此,需要区分。 三种模式:

  • development(自动优化打包速度,添加一些调试过程中的辅助)
  • production(自动优化打包结果)
  • none(运行最原始的打包,不做任何额外处理)

模块(Module):Webpack中,模块的概念比较宽泛,一切皆为模块。

依赖图(Dependency Graph):Webpack从入口文件开始加载整个项目的文件依赖,形成的依赖关系图就是依赖图。

Webpack最佳实践

初始化项目

mkdir myproject

cd myproject

npm init -y

安装Webpack

# 局部安装,推荐
npm install -D webpack webpack-cli
# 全局安装,不推荐,这会将你项目中的 webpack 锁定到指定版本,
# 并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败
npm install -G webpack webpack-cli

创建入口文件,myproject/src/index.js

执行打包(必须指定mode,npm版本需在5以上)

npx webpack ./src/index.js --output-path ./dist --mode=development

Webpack版本

Webpack4于2018年2月发布

Webpack5于2020年10月发布

安装命令需要注意(默认安装5)

npm install webpack -D # webpack5

npm install webpack@4 -D #webpack4

添加配置文件

假如所有参数都用命令行来设置,会导致命令行特别长,难敲,也容易出错。因此,我们使用默认的设置文件要方便些。以后我们也都是在配置文件上进行配置,来实现我们想要的效果。

  • 配置文件是用来简化命令行选项的

    配置前:

    npx webpack ./src/index.js --output-path ./dist --mode=development
    

    配置后:

    npx webpack
    
  • 默认的配置文件名称是webpack.config.js

    • webpack.config.js是以CommonJS规范进行组织的
    • 使用Webpack的过程,大部分就是跟配置文件打交道的过程

配置详情:www.webpackjs.com/configurati…

常见配置项:

  • mode(模式)
  • entry(入口)
  • output(出口)
  • module(模块配置一不同类型文件的配置一loader配置)
  • plugins(插件)
  • devserver(开发服务器的配置) 最佳实践项目中,创建文件,myproject/webpack.config.js

内容为:

const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  module: {
    rules: [//这里添加loader的处理规则
      //{
      //  test: /\.css$/i,
      //  use: ['style-loader', 'css-loader']
      //}
    ]
  },
  
  // 开发服务器
  devServer: {
  
  },
  
  // 插件配置
  plugins: [
      
  ]
}

重新运行打包命令,即可实现打包:

npx webpack

Webpack基础

打包css

打包逻辑

  • 非JS文件打包,需要对应的loader
    • css-loader将css转化为JS(将css输出到打包后的JS文件中)
    • 把包含css内容的JS代码,挂载到页面的<style>标签当中
  • 引入css
    // 在js文件中调用css模块
    import "./css/main.css"
    
  • 安装loader
    npm i css-loader style-loader -D
    
  • 配置webpack.config.js
    • 匹配后缀名:
    // 表示正则匹配以.css结尾不区分大小写的文件
    test: /\.css$/i,
    
    • 指定加载器:
    // 先调用的loader要放在后面
    use: ['style-loader', 'css-loader']
    

打包less

打包逻辑和上面类似:

  • 引入less
    // 在js文件中调用less模块
    import "./css/main.less"
    
  • 安装loader
    npm i less less-loader -D
    
  • 配置
    • 匹配后缀名:
    // 表示正则匹配以.less结尾不区分大小写的文件
    test: /\.less$/i,
    
    • 指定加载器:
    // 先调用的loader要放在后面
    use: ['style-loader', 'css-loader', less-loader]
    

打包成独立的CSS文件

有的时候我们不想把css样式挂载到html文件的<style>标签中,而是单独打包成一个css文件。

  • 安装插件
    npm i mini-css-extract-plugin -D
    
  • 引入插件(在 webpack.config.js 中)
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    
  • 替换style-loader(在 webpack.config.js 中)
    use: [MiniCssExtractPlugin.loader, 'css-loader']
    
    • style-loader:将css打包到<style>标签中
    • MiniCssExtractPlugin.loader:将CSS打包到独立文件中
  • 配置插件(在 webpack.config.js 中)
    // 插件配置
    plugins: [
      new MiniCssExtractPlugin({
        filename: 'css/[name].css' // 表示将文件打包到css文件夹中,文件名不变
      })
    ]
    
    有关mini-css-extract-plugin的详细配置信息,可以在npmjs官网上找到:www.npmjs.com/package/min…

添加样式前缀

  • 安装
    npm install postcss-loader autoprefixer -D
    
  • 配置 webpack.config.js
    use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader']
    
  • 配置 postcss.config.js
    plugins: [require('autoprefixer')]
    
  • 配置需要兼容的浏览器
    • 方法一,package.json中指定 browserslist (推荐)
      "browserslist": [
        "last 1 version", // 最新版本
        "> 1%"            // 代表全球使用率超过1%的浏览器
      ]
      
      详情参考: www.npmjs.com/packaee/bro…
    • 方法二,在项目根目录创建 .browserslistrc 文件
      last 1 version
      > 1%
      

格式校验

  • 安装

    npm i stylelint stylelint-config-standard stylelint-webpack-plugin -D
    
    • stylelint

      其中有很多校验规则,比如 number-teading-zero:
      line-height: .5;// 错误
      line-height: 0.5;// 正确
      官网:stvlelint.io/

    • stylelint-config-standard

      规则集,包含很多大公司所使用的校验规则,具体可以在github上看到: github.com/stvlelint/s…

    • stylelint-webpack-plugin

      stylelint的webpack插件 webpack.docschina.org/plugins/stv…

  • 引入 webpack.config.js

    const StylelintPlugin = require('stylelint-webpack-plugin');
    
  • 配置插件 webpack.config.js

    // 插件配置
    plugins: [
      new StylelintPlugin({
        // 指定需要格式校验的文件
        files: ['src/css/*.{css,less,sass,scss}']
      })
    ]
    
  • 指定校验规则(在package.json中指定stylelint)

    "stylelint": {
      "extends""stylelint-config-standard",
      "rules": {//自定义规则
        //"number-teading-zero": "never"
      }
    }
    

    指定现则配有三种方式。按加载的先后顺字依次是:

    • 在 package.json 中的stylelint属性指定规则
    • 在 .stylelintrc中指定规则
    • 在 stylelint.config.js中指定规则 这里我们推荐第一种

压缩CSS

  • 安装
    npm install optimize-css-assets-webpack-plugin -D
    
  • 引入 webpack.config.js
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    
  • 配置插件 webpack.config.js
    plugins: [
      new OptimizeCssAssetsPlugin()
    ]
    

打包HTML

打包HTML文件需要使用插件html-webpack-plugin,它的主要作用是:

  • 生成HTML文件(用于服务器访问),并在HTML中加载所有的打包资源
  • 指定HTML模板、设置HTML变量、压缩HTML

使用流程:

  • 安装
    npm i html-webpack-plugin -D
    
  • 引入 webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
  • 配置插件 webpack.config.js
    plugins: [
      new HtmlWebpackPlugin({
      // 指定打包后的html名称
      filename: 'index.html',
      // 使用html模板创建
      // template: './src/index.html',
      // 指定html模板中使用的变量, 可使用EJS模板引擎的语法调用
      // title: 'webpack Demo',
      // 设置html压缩, 一般只有在mode: 'production'时才压缩
      minify: {
          collapseWhitespace: true,
          keepClosingSlash: true,
          removeComments: true,
          removeRedundantAttributes: true,
          removeScriptTypeAttributes: true,
          removeStyleLinkTypeAttributes: true,
          useShortDoctype: true
      }
      }),
      
      //打包其他的html文件
      new HtmlWebpackPlugin({
      // 指定打包后的html名称
      filename: 'about.html',
      // 使用html模板创建
      // template: './src/about.html',
      // 指定html模板中使用的变量, 可使用EJS模板引擎的语法调用
      // title: 'about Demo'
      })
    ]
    

详细使用方法:www.npmjs.com/package/htm…

EJS 官网:ejs.bootcss.com

打包JS

转译

转译目的:

将ES6+转成ES5,从而保证,巧在低版本浏览器的兼容性。

  • 安装
    npm install babel-loader @babel/core @babel/preset-env -D
    # 假如报错'源文本中存在无法识别的标记',就使用如下命令
    # npm install babel-loader '@babel/core' '@babel/preset-env' -D
    
    @babel/core包含一系列转换插件的插件集,'@babel/preset-env'是转换规则集,包含各个版本最新的转换规则。
  • 配置 webpack.config.js
    module: {
      rules: [
        {
          test: /\.m?js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', { targets: "defaults" }]
              ],
              plugins: ['@babel/plugin-proposal-class-properties']
            }
          }
        }
      ]
    }
    
    配置详见:www.npmjs.com/package/bab…

但是,@babel/preset-env只能转译基本语法(promise不能转换),我们可以使用@babel/polyfill转译所有JS新语法。然而这种方法也有弊端,那就是它会将所有新语法转译,导致转译后的文件太大。于是,我们需要使用core-js

  • @babel/polyfill 安装

    npm i @babel/polyfill -D
    # 假如报错'源文本中存在无法识别的标记',就使用如下命令
    # npm i '@babel/polyfill' -D
    
  • @babel/polyfill 引用(在入口文件中引用)

    import '@babel/polyfill'
    
  • core-js安装

    npm i core-js -D
    
  • core-js配置 (在webpack.config.js中)

    • 注释@babel/polyfill 引用
    // 不注释掉,@babel/polyfill还是会将所有新语法转译
    // import '@babel/polyfill'
    
    • 按需加载 useBuiltIns: 'usage'
    • 指定版本 corejs: 3 // 在 package,json 中可以看到版本号
    module: {
      rules: [
        {
          test: /\.m?js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: {
              presets: [
                ['@babel/preset-env', 
                  {
                    useBuiltIns: 'usage',
                    corejs: 3,
                    //targets: "defaults" 
                    // 手动指定兼容浏览器版本
                    targets: {
                      Chrome: '58',
                      ie: '9',
                      firefox: '60',
                      safari: '10',
                      edge: '17'
                    }
                  }
                ]
              ],
              plugins: ['@babel/plugin-proposal-class-properties']
            }
          }
        }
      ]
    }
    

JS代码格式校验

  • 安装

    npm i eslint eslint-config-airbnb-base eslint-webpack-plugin eslint-plugin-import -D
    
  • 配置

    • eslint-webpack-plugin

      在入口文件中引入:

      const ESLintPlugin = require('eslint-webpack-plugin");
      

      在 webpack.config.js 中配置:

      plugins: [
        new ESLintPlugin({
          // 自动解决常规的代码格式报错
          fix: true
        })
      ]
      

      忽略下一行代码的格式校验,只需要在相应报错代码前一行加入以下注释
      //eslint-disable-next-line

    • eslintConfig (在package.json中配置)

      "eslintConfig": {"extends": "airbnb-base"}

打包图片

  • file-loader

    将用到的图片复制到输出目录,用不到的图片不复制。

    • 图片文件引入
    // 在相关JS文件中进行图片文件引入
    import img from './file.png'
    
    • 安装
    npm i file-loader -D
    
    • 配置(在webpack.config.js中)
    module: {
        rules: [
          {
            test: /\.(png|jpe?g|gif)$/i,
            loader: 'file-loader',
            options: {
              name: '[name].[ext]', // [name]表示保留原文件名,.[ext]表示保留原后缀名
              outputPath: 'images'// 输出到单独目录
            }
          }
        ]
    }
    

    PS:按如上设置,将图片输出到单独目录时,css文件调用 url() 函数,可能会出现图片无法加载的情况,这是因为路径被改变导致的,我们只需要将设置中关于less和css的处理部分的

    // less
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
    // css
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
    

    改成

    // less
    use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../'
            }
          },
          'css-loader', 'postcss-loader', 'less-loader'
         ]
    // css
    use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../'
            }
          },
          'css-loader', 'postcss-loader'
         ] 
    

    配置详情: www.npmjs.com/package/fil…

  • url-loader

    一个网站包含各种大大小小的文件,假如每个文件都去请求获取就会造成效率低下,网页加载慢。这时候我们可以使用url-loader对文件资源进行处理。

    url-loader可以设置处理原则:

    • 小于一定大小的文件,会被打包成Data URLs格式的URL
    • 大于一定大小的文件,则调用文件加载器 file-loader 进行处理

    Data URLs是一种特殊的URL协议,用来表示文件。文件的内容就在URL中。通过这种方式,可以减少浏览器的请求。Data URLs的格式如下:

    image.png 简单举两个例子。

    html文件内容,可表示为:

    data:text/html;charset=UTF-8,<h1>html content<h1>
    

    图片文件内容,可表示为:

    data:image/png;base64,iVBORwKGgoAAANSUhE...SuQmCC
    

    url-loader 如何安装配置呢?答案如下:

    • 安装
    npm i url-loader -D
    
    • 配置(在webpack.config.js中) 将 file-loader 配置部分替换为如下配置:
    module: {
      rules: [
        {
          test: /\.(png|jpe?g|gif)$/i,
          loader: 'url-loader',
          options: {
            limit: 10 * 1024, // 小于10KB的文件转换成Data URLs字符串
            // 大于 10KB 文件调用 file-loader 进行处理
            name: '[name].[ext]',// [name]表示保留原文件名,.[ext]表示保留原后缀名
            outputPath: 'images'// 输出到单独目录
          }
        }
      ]
    }
    

    配置详情: www.npmjs.com/package/url…

  • html-loader

    当HTML代码中图片标签存在src属性时,我们发现打包后的HTML文件中该src内容并没有被处理,这样会导致图片无法被加载。因此,我们需要html-loader来处理。
    html-loader的作用就是将HTML内容导出为字符串,负责引入img,从而能被url-loader处理。

    • 安装
    npm i html-loader -D
    
    • 配置(在webpack.config.js中)
    module: {
      rules: [
        {
          test: /\.(png|jpe?g|gif)$/i,
          loader: 'url-loader',
          options: {
            limit: 10 * 1024, // 小于10KB的文件转换成Data URLs字符串
            // 大于 10KB 文件调用 file-loader 进行处理
            name: '[name].[ext]', // [name]表示保留原文件名,.[ext]表示保留原后缀名
            outputPath: 'images', // 输出到单独目录
            // url-loader 默认采用ES Modules规范进行解析,但是html-loader引入图片采用的是 CommonJS 规范,这会导致解析错误
            // 解决: 关闭url-loader的ES Modules规范,强制采用CommonJS 规范
            esModule: false
          }
        },
        {
          test: /\.(htm|html)$/i,
          loader: 'html-loader',
          options: {
            // Webpack4 只需要在url-loader中设置esModule: false
            // Webpack5在html-loader中也需要设置
            esModule: false
          }
        }
      ]
    }
    

    配置详情:www.npmjs.com/package/htm…

    html-loader适用于静态html,对于需要存在ejs语法的html,使用html-loader会造成与html-webpack-plugin冲突,无法实现ejs语法效果。

  • html-loader与html-webpack-plugin的冲突

    原因:html-webpack-plugin会检测是否有其他loader处理了html文件,html-loader处理过后,html-webpack-plugin就不再处理,造成ejs语法效果失效。

    解决:不使用html-loader,在html中图片的引用改为采用ejs语法表示。

    <img src="<%= requires('./images/telephone.png') %>" alt="telephone.png">
    

总结:静态html可采用html-loader来处理,如果需要使用ejs语法,则需要禁用html-loader,修改html文件中图片引用路径为ejs语法表示,采用html-webpack-plugin来处理

打包字体

  • 字体文件

    可在阿里巴巴矢量库下载:www.iconfont.cn/

  • file-loader

    打包字体文件还是用到file-loader。

    • 安装
    npm i file-loader -D
    
    • 配置(在webpack.config.js中)
    module: {
        rules: [
          {
            test:/\.(eot|svg|ttf|woff|woff2)$/i,
            loader: 'file-loader',
            options: {
              name: 'fonts/[name].[ext]', // 输出到单独目录fonts,[name]表示保留原文件名,.[ext]表示保留原后缀名
            }
          }
        ]
    }
    

    PS:按如上设置,将字体文件输出到单独目录时,css文件调用 url() 函数,可能会出现字体文件无法加载的情况,这是因为路径被改变导致的,我们只需要将设置中关于less和css的处理部分的

    // less
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
    // css
    use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
    

    改成

    // less
    use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../'
            }
          },
          'css-loader', 'postcss-loader', 'less-loader'
         ]
    // css
    use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '../'
            }
          },
          'css-loader', 'postcss-loader'
         ] 
    

复制不需要处理的文件

copy-webpack-plugin

不需要处理的其他文件,可以直接使用copy-webpack-plugin复制到输出目录。

详情:www.npmjs.com/package/cop…

  • 安装
    npm install copy-webpack-plugin -D
    
  • 配置(在webpack.config.js中)
    const CopyPlugin = require("copy-webpack-plugin");
    
    module.exports = {
      plugins: [
        new CopyPlugin({
          patterns: [
            { from: "source", to: "dest" },// from 源文件/文件夹路径, to 目标文件/文件夹路径
            { from: "other", to: "public" },
          ],
        }),
      ],
    };
    

打包之前删除历史文件

clean-webpack-plugin

使用详情:www.npmjs.com/package/cle…

  • 安装
    npm install clean-webpack-plugin -D
    
  • 配置(在webpack.config.js中)
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');// 必须利用解构引用
    
    module.exports = {
      plugins: [
          new CleanWebpackPlugin(),
      ],
    };
    

资源模块(Asset modules)

资源模块是webpack5的一个新特性,是一种模块类型,它允许使用资源文件,而无需配置额外loader。

资源文件包含:字体、图片、图标、HTML等等。

特点:不使用file-loader、url-loader也能加载图片和字体

官方文档:webpack.docschina.org/guides/asse…

  • asset/resource 发送一个单独的文件并导出URL(之前通过用file-loader实现)

  • asset/inline 导出一个资源的dataURL(之前通过使用url-loader实现)

  • asset/source 导出资源的源代码(之前通过使用raw-loader实现)

  • asset 在导出一个dataURL和发送一个单独的文件之间自动选择(之前通过使用url-loader实现)

    • 配置(在webpack.config.js中)

      将使用file-loader和url-loader的部分配置进行修改,test文件匹配不需要更改。

      // 打包字体配置
      {
        test:/\.(eot|svg|ttf|woff|woff2)$/i,
        type: asset,
        parser: {
          dataUrlCondition: {
            // 假如不设置,maxSize默认就是8KB
            // 超过8KB调用asset/resource
            // 小于8KB调用asset/inline
            maxSize: 8 * 1024 
          }
        },
        generator: {
          filename: 'fonts/[name][ext]'// 输出到单独目录font,和file-loader不一样,保留原后缀名是[ext]而不是.[ext]
        }
      },
      // 打包图片配置
      {
        test: /\.(png|jpe?g|gif)$/i,
        type: asset,
        // 这里省略maxsize的设置,采用默认的8KB,如需修改参考上面字体相关部分设置
        generator: {
          filename: 'images/[name][ext]'// 输出到单独目录images,和file-loader不一样,保留原后缀名是[ext]而不是.[ext]
        }
      },
      

开发服务器(Dev Server)

webpack dev server

  • 作用:发布web服务,提高开发效率

  • 详情:

    www.npmjs.com/package/web…

    webpack.docschina.org/configurati…

  • 安装

    npm i webpack-dev-server -D
    
  • 配置(在webpack.config.js中)

    // 开发服务器
    devServer: {
      // 指定加载内容的路径,一般是打包输出路径
      contentBase: resolve(__dirname, 'output'),
      // 启用gzip压缩
      compress: true,
      // 热更新,webpack4
      // hot: true,
      // 自动更新,webpack5,禁用hot
      liveReload: true,
      // 服务器端口号
      port: 9200
      // 配置代理,解决跨域问题
      proxy: {
        'api': {// 表示对 http://localhost:9200/api 进行处理
          // 替换,例如 http://localhost:9200/api/users 替换为 https://api.github.com/api/users
          target:'https://api.github.com',
          // 域名路径替换,http://localhost:9200/api/users 替换为 https://api.github.com/users
          pathRewrite: {
            '^/api': '',// 表示匹配路径开始处的'/api'
          },
          // 不能使用 localhost:9200 作为github的主机名
          changeOrigin: true
        }
      }
    },
    // 配置目标
    target: "web",
    
  • 服务器启动命令

    Webpack4:

    npx webpack-dev-server
    

    Webpack5:

    npx webpack serve
    

    只要修改保存了代码内容,在浏览器网址 http://localhost:9200/ 中就可以即时自动更新网页效果,提升了开发效率。

webpack进阶

区分打包环境

在实际工作过程中,我们需要根据不同的环境设置不同的打包配置,比如生产环境需要设置代码压缩,开发环境就不需要,因为压缩过后不好阅读代码。

有两种方式进行环境的区分:通过环境变量区分 和 通过配置文件区分。

  • 通过环境变量区分

    • 在命令行中设置环境变量:

      # 设置env.production, 不赋值默认设置为true
      # webpack 4
      npx webpack --env.production
      # webpack 5
      npx webpack --env production
      
    • 在 webpack.config.js 中判断env

      读取环境变量env.production,再根据环境变量指定不同的配置

      // 修改为箭头函数传参
      module.exports = (env, argv) => {
        // 开发环境配置
        const config = {
          mode: 'development',
          // 更多配置...
        }
        if (env.production) {
          // 生产环境配置
          config.mode = 'production',
          // 更多配置...
        }
        
        return config
      }
      
    • 切换回开发环境配置打包

      # 设置env.production, 赋值为空字符串
      # webpack 4
      npx webpack --env.production=""
      # webpack 5
      npx webpack --env production=""
      
      

    详情:www.webpackjs.com/guides/envi…

  • 通过配置文件区分

    1. 安装 webpack-merge (用于将多个配置合并到一起)。

      npm i webpack-merge -D
      

      使用详情:www.npmjs.com/package/web…

    2. 新建两个配置文件:webpack.dev.conf.js 和 webpack.prod.conf.js,分别写入开发环境和生产环境下所需配置,提取它们公共配置部分,新建 webpack.base.conf.js 并写入。
      webpack.base.conf.js

      // 公共配置部分
      

      webpack.dev.conf.js

      // 开发环境配置部分
      const { merge } = require('webpack-merge')
      const baseWebpackConfig = require('./webpack.base.conf')
      
      const devWebpackConfig = merge(baseWebpackConfig, {
        //  开发模式对应的配置...
      })
      module.exports = devWebpackConfig
      

      webpack.prod.conf.js

      // 生产环境配置部分
      const { merge } = require('webpack-merge')
      const baseWebpackConfig = require('./webpack.base.conf')
      
      const prodWebpackConfig = merge(baseWebpackConfig, {
        //  生产模式对应的配置...
      })
      module.exports = prodWebpackConfig
      
    3. 执行打包时根据不同环境指定配置文件。

      # 开发环境
      npx webpack --config webpack.dev.conf.js
      # 生产环境
      npx webpack --config webpack.prod.conf.js
      

      也可以在package.json中进行配置,来简化命令:

      "scripts": {
          "dev": "npx webpack --config webpack.dev.conf.js",
          "prod": "npx webpack --config webpack.prod.conf.js"
      },
      

      打包

      # 开发环境打包
      npm run dev
      # 生产环境打包
      npm run prod
      

自定义plugin

webpack插件是一个具有apply方法的Javascript对象。apply方法会被webpackcompiler调用,并目在整个编译生命周期都可以访问compiler对象。

通过在生命周期的钩子中挂载函数,来实现功能扩展。

  • 生命周期

    生命周期就是整个生命过程中的关键节点。
    程序的生命周期:初始化->挂载->渲染->展示->销毁

  • 钩子

    钩子是提前在可能增加功能的地方,埋好(顺设)一个函数。生命周期中的函数就是钩子。
    webpack常用钩子:www.webpackjs.com/api/compile…

    钩子描述类型
    environment环境准备好SyncHook
    compile编译开始SyncHook
    compilation编译结束SyncHook
    emit打包资源到output之前AsyncSeriesHook
    afterEmit打包资源到output之后AsyncSeriesHook
    done打包完成SyncHook

    需要操作的代码必须加在emit或者之前的钩子上。 image.png

自定义plugin的基本流程:

  1. 项目根目录新建plugin文件夹,新建my-plugin.js文件,写入代码
    class MyPlugin {
      constructor(options) {
        console.log('插件选项', options)// 如果有配置选项,可以在这里做处理
      }
    
      // 必须带有 apply 方法
      apply(compiler) {
        compiler.hooks.emit.tap('MyPlugin',  (compilation) => {
          // compilation是此次打包的上下文
          // 这里是你需要进行的操作
          console.log('webpack 构建过程开始', compilation)
        })
      }
    }
    
    module.exports = MyPlugin
    
  2. 在webpack.config.js中进行引用、配置
    const MyPlugin = require('./plugin/my-plugin')
    
    module.exports = {
      plugins: [
        new MyPlugin({
          // 传入配置项
        }),
      ]
    }
    

详情:webpack.docschina.org/concepts/pl…

自定义loader

loader本质上就是一个ES Module模块,它导出一个函数,在函数中对打包资源进行转换。

现在我们制作一个读取markdown(.md)文件内容的loader

  1. 安装相关包

    npm i marked loader-utils -D
    

    marked(将markdown语法转成html),loader-utils(接受loader的配置项)

  2. 项目根目录新建loader文件夹,在其中新建markdown-loader.js,写入相关代码

    const { getOptions } = require('loader-utils');
    const marked = require('marked')
    
    // 自定义loader,建议使用普通函数
    module.exports = function(source) {
      // 获取loader配置选项,方便进行处理
      const options = getOptions(this)
      // 对输入的内容进行处理
      const html = marked(source)
      // 一般loader需要返回JS代码,除非返回给下一个loader处理
      // return 'module.exports = ${(JSON.stringify(html))}'
      // 返还给下一个loader处理
      return html
    }
    
  3. 在src目录下新建一个test.md文件,写入内容

    # 主题
    爱我中华
    
  4. 在index.js中进行md文件引用

    import MDtest from './test.md'
    
    console.log(MDtest)
    
  5. 在webpack.config.js中进行引用、配置

    {
          test:/\.md$/i,
          use: ['html-loader', {
            loader: './loader/markdown-loader.js',
            options: {}
          }]
    }
    

自定义loader单独使用需保证最后输出为JS代码,假如有多个自定义loader被连续使用,则需保证最后一个loader输出JS代码 image.png

代码分离(Code Spilitting)

如果把所有代码都打包到一起,可能导致最终的代码文件非常大,从而影响加载时间;而且,很多代码是初始加载时不需要的:因此,我们可以根据代码使用的紧急程度,将代码分割打包后,按需加载。

代码分离有三种方式:

  • 多入口打包
  • 提取公共模块
  • 动态导入

多入口打包方式

在 webpack.config.js :

  • 配置entry(后面写成对象)
    entry: {index:'./src/index.js', about:'./src/about.js'},
    
  • 配置output.filename(不能写成固定名称,否则报错)
    output: {
      // filename: 'bundle.js',
      filename: '[name].bundle.js',
      path: path.join(__dirname, 'output')
    },
    
  • 配置HtmlWebpackPlugin(不同页面加载各自的bundle)
    // HtmlWebpackPlugin关于index.html部分的配置
    // 表示index.html加载index.bundle.js
    chunks:['index']
    // HtmlWebpackPlugin关于about.html部分的配置
    // 表示about.html加载about.bundle.js
    chunks:['about']
    

提取公共模块方式

如果多个页面都用到了一个公共文件(例如:jQuery),每个页 面都将公共文件打包一次是不合理的。更好的办法是将公共文件 提取出来。

例如:京东的商品页超过1000000个,如果打包的1000000个文件都包含jQuery,打包文件会超过80G(88KB*1000000)

我们只需要在 webpack.config.js 中 进行如下设置:

// 将公共文件提取出来,单独打包 
optimization: {
  splitChunks: {
    chunks:'all'
  }
},

动态导入方式

动态导入又分为两种方式:

  • 懒加载

    默认不加载,事件触发后才加载。我们可以通过类似以下代码实现懒加载

    document.getElementById('btn').onclick = () => {
      // 只有在点击事件触发之后才会导入该wp.js文件
      import('./wp').then(
        alert('这是动态导入')
      )
    }
    

    上面的写法固然可以实现懒加载,但其中的wp.js经过打包后的文件名不受控制,所以我们需要增加一条命令来设置加载名称。

    document.getElementById('btn').onclick = () => {
      // 只有在点击事件触发之后才会导入该wp.js文件
      // 通过/* webpackChunkName: 'desc' */注释来指定wp.js打包后的名称为desc.bundle.js
      import(/* webpackChunkName: 'desc' */'./wp').then(
        alert('这是懒加载方式')
      )
    }
    
  • 预加载

    只需要在懒加载的基础上增加一条注释命令实现

    document.getElementById('btn').onclick = () => {
      // 只有在点击事件触发之后才会导入该wp.js文件
      // 通过/* webpackChunkName: 'desc' */注释来指定wp.js打包后的名称为desc.bundle.js
      // 通过/* webpackPrefetch: true */注释来指定加载模式为预加载
      import(/* webpackChunkName: 'desc', webpackPrefetch: true */'./wp').then(
        alert('这是预加载方式')
      )
    }
    

    预加载的效果是先等待其他资源加载,在浏览器空闲时,再加载。这是一个很好的效果,相比懒加载,减少了事件触发时所需要的加载时间。

    缺点:在移动端有兼容性问题

源码映射(Source Map)

  • 什么是SourceMap

    SourceMap是一种源代码和构建后代码之间的映射技术。 通过.map文件,将构建后的代码与源代码之间建立映射关系。

  • 为什么要用sourceMap

    构建后的代码,出了问题之后不好定位。有了sourceMap后,可以快涑定位问题代码

  • 如何生成sourceMap

    在webpack.config.js中增加配置

    // 映射模式,webpack4一共13种,webpack5一共26种
    // 不同模式对应不同的效果,我们这里选择的是'source-map',实际生产不会采用该模式
    devtool: 'source-map'
    

    image.png

  • 如何选取合适的映射模式(个人建议-不绝对)

    • 开发环境(eval-cheap-module-source-map)
    • 生产环境(none | nosources-source-map)

使用SourceMap不生效,需要确认两个条件:一、是否使用谷歌浏览器;二、是否在浏览器中开启相关source-map设置(默认是开启的)image.png

删除冗余代码(Tree Shaking)

Tree Shaking 的作用是删除未引用代码(dead code), 返回剩下的代码。 未引用代码包含:

  • 只声明,未使用的函数
  • 只引入,未使用的代码

ScreenGif.gif

如上图,依赖关系就是树,未使用的代码就像多余的树叶,使用Tree Shaking(摇树)技术“将多余的树叶摇下来”,就能使代码更精简,体积更小,从而提高加载速度。

使用前提:

  • 使用ES Modules规范的模块,才能执行Tree Shaking
  • Tree Shaking依赖于ES Modules的静态语法分析 如何使用:
  • 生产模式:Tree Shaking会自动开启
  • 开发模式:有 usedExports 和 sideEffects 两种方法

usedExports

image.png

  1. 配置usedExports(标记没用的代码)

    在 webpack.config.js 中

    optimization: {
      // 标记没用的代码
      usedExports: true,
    },
    
  2. 使用terser-webpack-plugin(删除没用的代码)

    • 安装

      webpack 5无需安装,webpack 4需要单独安装

      npm i terser-webpack-plugin -D
      
    • 配置(在 webpack.config.js 中)

      const TerserPlugin = require('terser-webpack-plugin')
      
      optimization: {
        // 删除 unused harmony export XXXXX 标记的代码
        minimize: true,
        minimizer: [new TerserPlugin()]
      },
      

    详情:www.npmjs.com/package/ter…

Tree Shaking与 Source Map存在兼容性问题

Source Map 的 eval模式会将JS输出为字符串(不是ES Modules规范),导致Tree Shaking失效。
因此,同时使用Source Map时,其模式只能指定为source-map | inline-source-map | hidden-source-map | nosources-source-map中的一种

sideEffects

无副作用:如果一个模块单纯地导入导出变量,那它就无副作用

有副作用:如果一个模块还修改其他模块或者全局的一些东西,就有副作用。比如:修改全局变量,在原型上扩展方法,css的引入。

sideEffects的作用:删除未使用且无副作用的模块。

ScreenGif.gif

sideEffects的使用:

  • 开启副作用(在 webpack.config.js 中配置)
    optimization: {
      // 开启副作用
      sideEffects: true,
    },
    
  • 标识代码是否有副作用(在 package.json 中配置)
    // false,表示所有代码都没有副作用(告诉webpack可以安全地删除未用的exports)
    // true,表示所有代码都有副作用
    // 数组,告诉webpack哪些模块有副作用,不删除,比如`['./src/wp.js','*.css']`
    "sideEffects": false,
    

缓存

有些时候,我们只修改了小部分内容然后又重新打包,如果所有内容都重新打包,那么效率就会很低,但利用缓存技术,未更改的内容直接从缓存读取,只打包修改的部分,这样效率会高很多。

webpack关于缓存的设置页比较简单:

  • Babel缓存

    在webpack.config.js中,关于babel-loader的设置,options对象增加一条属性cacheDirectory: true即可

    {
      test: /\.m?js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          // 第二次及之后构建时,会读取之前的缓存
          cacheDirectory: true,
          presets: [
            ['@babel/preset-env', 
              { 
                useBuiltIns: 'usage',
                corejs: 3,
                targets: "defaults" 
              }
            ]
          ],
          plugins: ['@babel/plugin-proposal-class-properties']
        }
      }
    },
    
  • 文件资源缓存

    利用文件资源缓存技术在实际项目中可大大减少文件的重复请求,减轻服务器负担,提升效率。对于同一个文件资源我们只需要请求一次文件内容,之后从缓存中读取文件资源。但这也会存在一个问题:如果代码在缓存期内,代码更新后会看不到实时效果。

    解决方案:将代码文件名称设置为哈希名称,当名称发生变化,就加载最新的内容。

    Webpack中哈希名称设置:在webpack.config.js中,

    output: {
      //filename: 'bundle.js',
      //filename: '[name].bundle.js',
      // [hash],整个项目打包生成的hash值
      // [chunkhash],不同chunk打包时生成的hash值
      // [contenthash],不同内容打包时生成的hash值
      filename: '[name].[hash].js',
      path: path.join(__dirname, 'output')
    },
    

模块解析(resolve)

通过resolve配置项配置模块解析的规则,我们可以简化模块导入的代码。比如引入时省略css文件后缀名(import './css/style'),再简化表示文件目录(import '@/style')等等。

在webpack.config.js中进行配置:

// 模块的解析规则
resolve: {
  alias: {// 配置模块加载的路径别名
    // 使用 @ 代替 src 目录
    '@': resolve('src')
  },
  // 引入模块时可以省略.js和.json后缀
  extensions: ['.js', '.json'],
  // 指定模块默认加载的路径
  modules: [resolve(__dirname, './node_modules'), 'node_modules']
},

详情:webpackjs.com/configurati…

排除依赖(externals)

有些时候,我们需要排除打包依赖项,防止对某个依赖项进行打包。 一般来说,一些成熟的第三方库,是不需要打包的。 例如:jQuery,然后我们可以在模板文件中直接引入CDN中的压缩,这样就不用对其打包。

在webpack.config.js中进行配置:

// 排除打包依赖项
externals: {
  'jquery': 'jQuery'
},

在模板文件中直接引入CDN中的压缩:

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

详情:www.webpackjs.com/configurati…

模块联邦

模块联邦是Webpack 5新增的特性。它使得多个应用,可以共享一个模块,即本地可以调远程的模块。这相当于是微前端的一个解决方案。

模块提供方配置(webpack.config.js),来暴露模块给调用方使用:

// 引入模块联邦插件
const Mfp = require('webpack').container.ModuleFederationPlugin

module.exports = (env, argv) => {
  // 插件配置
  plugins: [
    // 模块提供方
    new Mfp({
      // 应用名称,给调用方使用
      name: 'app1',
      // 调用方引入的文件名称
      filename: 'app1.js',
      // 暴露模块
      exposes: {
        // 模块名称: 模块对应的代码路径
        './Sitename': './src/Sitename.js'
      }
    }),
  ]
}

模块使用方配置(webpack.config.js):

// 引入模块联邦插件
const Mfp = require('webpack').container.ModuleFederationPlugin

module.exports = (env, argv) => {
  // 插件配置
  plugins: [
    // 导入模块
    remotes: {
      // 应用别名(自己设置): "远程应用名称@远程应用地址/远程导出的文件名称"
      // 假如远程应用地址未启用则模块无法调用
      appone: 'aap1@localhost:3001/app1.js'
    }
  ]
}

模块使用方在项目js文件中调用:

// import(应用别名/模块名称).then(模块的使用代码)
import('appone/Sitename').then(res => {
  res.default()
  //...
})

详情:webpack.js.org/concepts/mo…