你应该了解的webpack知识

257 阅读14分钟

webpack作为目前主流的前端模块打包器,它提供了一整套的前端项目模块化方案,而不仅仅是局限于只对javascript的模块化。通过webpack提供的模块化方案,可以轻松的对前端项目开发过程中涉及到的所有资源进行模块化。

webpack配置文件

webpack会按照约定将src/index.js作为打包的入口,最终的打包结果会放到dist/index.js。我们可以在webpack.config.js中根据实际需求更改webpack的配置信息。

// webpack.config.js

const path = require('path')

module.exports = {
  // 入口文件
  entry: './src/main.js',
  // 出口
  output: {
    // 输出文件名称
    filename: 'bundle.js',
    // path是webpack所有文件的输出的路径,必须是绝对路径
    path: path.join(__dirname, 'output')
  }
}

webpack工作模式

webpack4新增了一个工作模式的用法,大大简化了webpack配置的复杂程度,可以把它理解成针对不同环境的几种预设配置。webpack默认使用production模式去工作。

const path = require('path')

module.exports = {
  // 这个属性有三种取值,分别是 production、development 和 none。
  // 1. production生产模式下,Webpack 会自动优化打包结果;
  // 2. development开发模式下,Webpack 会自动优化打包速度,添加一些调试过程中的辅助;
  // 3. None 模式下,Webpack 就是运行最原始的打包,不做任何额外处理;
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  }
}

资源模块加载

loader是整个webpack的核心特征,借助不同的loader就可以加载任何类型的资源。
loader负责资源文件从输入到输出的转换。
对于同一个资源可以依次使用多个loader。

常用加载器分类

  • 编译转换类 把加载的资源模块转化为javascript代码,例如css-loader
  • 文件操作类 将加载到的资源模块拷贝到输出目录,同时又将文件的访问路径向外导出,例如file-loader
  • 代码检查类 对加载到的资源文件(一般是代码)进行校验的加载器。这种加载器的目的是统一代码风格,提高代码质量。一般不会修改生产环境的代码。

css

module.exports = {
  module: {
    rules: [
      {
        // npm i style-loader css-loader -D
        test: /.css$/,// 正则表达式,用于匹配打包的资源路径
        use: [
          //如果配置了多个loader,执行顺序是由下往上执行,就是先执行css-loader,再执行style-loader
          'style-loader', // 把css-loader转化的结果通过style标签挂载到页面上
          'css-loader' // 将css代码转化成js模块,具体就是将css代码push到一个数组中
        ]
      }
    ]
  }
}

文件资源加载器

文件资源加载器的工作流程:webpack在打包时遇到了图片等文件,然后根据配置文件中的配置,匹配到文件资源加载器,此时文件资源加载器开始工作,先是将导入的文件拷贝到输出的目录,再将文件拷贝到输出目录过后的路径作为当前模块的返回值返回,这样对于应用来说所需要的资源被发布出来了,同时也可以通过模块的导出成员拿到这个资源的访问路径。

file-loader

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // webpack会默认将所有打包的结果放到网站的根目录下,这样会导致打包后有些文件访问不到
    // 可以通过设置publicPath,默认空字符串,表示根目录下
    // 设置为dist表示打包的结果放到dist目录下,切记最后的斜线不能省略
    publicPath: 'dist/' 
  },
  module: {
    rules: [
      {
        // npm i file-loader -D
        test: /.png$/,
        use: 'file-loader'
      }
    ]
  }
}

url-loader

除了file-loader这种通过拷贝物理文件的形式去处理文件资源以外,还可以通过Data URLs的形式去表示文件。

Data URLs是一种特殊的url协议,用来直接去表示一个文件。传统的url要求服务器上有一个对应的文件,然后通过请求这个地址得到服务器上对应的文件。而Data URLs是一种当前url就可以直接表示文件内容的方式,这种url中的文本就已经包含了文件内容,使用这种url的时候就不会发送任何http请求。例如,data:text/html;charset=UTF-8,<h1>html content</h1>,浏览器就能根据这个url解析出来这是一个html类型的文件内容,它的文件编码是UTF-8,内容是包含h1标签的html代码。如果是图片或者字体这类无法直接通过文本去表示的二进制类型文件,可以通过将文件的内容进行base64编码,以base64编码过后的结果去表示文件的内容。

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.png$/,
        use: {
          // npm i url-loader -D
          loader: 'url-loader',
          options: {
            // 限制文件的大小,这样设置后url-loader只会处理10K以下的文件,将其转化成Data URLs嵌入到代码中
            // 超过10K的文件会交给file-loader去处理,单独提取存放,此时要保证项目中安装了file-loader,否则会报错
            limit: 10 * 1024 
          }
        }
      }
    ]
  }
}

最佳实践:

  • 小文件使用Data URLs,减少请求次数
  • 大文件单独提取存放,提高加载速度

babel-loader

webpack只是打包工具,加载器可以用来编译转换代码。

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          // npm i babel-loader @babel/core @babel/preset-env -D
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
    ]
  }
}

webpack加载资源的方式

  • 支持 ES Modules 标准的 import 声明
import createHeading from './heading.js'
import better from './better.png'
import './main.css'

const heading = createHeading()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
  • 支持 CommonJS 的 require 函数
const createHeading = require('./heading.js').default
const better = require('./better.png')
require('./main.css')

const heading = createHeading()
const img = new Image()
img.src = better
document.body.append(heading)
document.body.append(img)
  • 支持 AMD 的 define / require 函数
define(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
  const heading = createHeading.default()
  const img = new Image()
  img.src = better
  document.body.append(heading)
  document.body.append(img)
})

require(['./heading.js', './better.png', './main.css'], (createHeading, better) => {
  const heading = createHeading.default()
  const img = new Image()
  img.src = better
  document.body.append(heading)
  document.body.append(img)
})
  • 部分 loader 加载的资源中一些用法也会触发资源模块加载
// webpack.config.js
const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      },
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      },
      {
        test: /.html$/,
        use: {
          // npm i html-loader -D
          loader: 'html-loader',
          options: {
            attrs: ['img:src', 'a:href']
          }
        }
      }
    ]
  }
}

    • 样式代码中的@important指令和url函数
// main.css
@import url(reset.css);
/*css-loader 同样支持 sass/less 风格的 @import 指令*/

body {
  min-height: 100vh;
  background: #f4f8fb;
  background-image: url(background.png);
  background-size: cover;
}

// main.js
import './main.css'
//此时会调用css-loader、file-loader
    • HTML代码中图片标签的src属性
// footer.html
<footer>
  <img src="better.png" alt="better" width="256">
  <a href="better.png">download png</a>
</footer>

// main.js
import footerHtml from './footer.html'
document.write(footerHtml)
// 此时会调用html-loader

webpack插件机制

插件机制是webpack另一个核心特性。它的目的是增强webpack自动化能力。
loader专注实现资源模块加载,plugin解决其他自动化工作。

自动清除输出目录的插件 clean-webpack-plugin

安装:npm i clean-webpack-plugin -D

作用:每次打包前自动清理dist目录

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  // 插件
  plugins: [
    new CleanWebpackPlugin()
  ]
}

自动生成HTML插件 html-webpack-plugin

安装:npm i html-webpack-plugin -D

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/' // 因为html会自动生成到dist目录,所以不需要这个配置了
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // 如果需要生成多个文件就应用多个HtmlWebpackPlugin
    // 每个HtmlWebpackPlugin对应一个html文件
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    })
  ]
}
<!-- index.html模板文件 -->
<!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>

copy-webpack-plugin

copy-webpack-plugin 并非旨在复制从构建过程中生成的文件,而是在构建过程中复制源树中已经存在的文件。如果需要webpack-dev-server在开发过程中将文件写入输出目录,则可以使用writeToDisk选项或强制执行write-file-webpack-plugin。

安装:npm i copy-webpack-plugin -D

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin([
      'public'
    ])
  ]
}

webpack开发一个插件

plugin通过钩子机制实现。webpack在工作的工程中会有很多的环节,为了便于插件的扩展,webpack几乎给每个环节都埋下了一个钩子。这样开发插件的时候就可以通过往不同节点上去挂载不同任务,就可以轻松的扩展webpack的能力。 webpack要求插件必须是一个函数或者是一个包含apply方法的对象。一般都会把插件定义成一个类型,然后在这个类型上定义一个apply方法,使用的时候就是根据这个类型去构建一个实例使用。

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

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 自定义插件 清除打包生成的js中没有必要的注释
class MyPlugin {
  apply (compiler) {
    // emit会在webpack即将往输出目录输出文件时执行
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation => 可以理解为此次打包的上下文
      for (const name in compilation.assets) {
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source()
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '')
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length
          }
        }
      }
    })
  }
}

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Plugin Sample',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // 用于生成 about.html
    new HtmlWebpackPlugin({
      filename: 'about.html'
    }),
    new CopyWebpackPlugin([
      'public'
    ]),
    new MyPlugin()
  ]
}

自动编译及自动刷新浏览器

watch工作模式

使用webpack cli的watch工作模式监听文件变化,自动重新打包。

//webpack.config.js
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  watch: true, // 开启监听模式
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorials',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    new CopyWebpackPlugin([
      'public'
    ])
  ]
}

//package.json
"scripts": {
  "build": "webpack --config webpack.config.js",
  "watch":"webpack --watch webpack.config.js"
}

通过执行npm run watch实现自动编译。

BrowserSync

BrowserSync可以帮助实现自动刷新浏览器的功能。

// 全局安装browser-sync
npm install -g browser-sync

//使用brower-sync启动http服务,同时监听dist目录下的文件变化
brower-sync dist --files '**/*'

这样存在一些弊端:

  • 操作麻烦了,需要使用两个工具
  • 效率降低了,这个过程中webpack会不断把文件写入磁盘,brower-sync再从磁盘中读取出来

webpack-dev-server

webpack-dev-server是webpack官网推出的一个开发工具,提供用于开发的http server,集成自动编译自动刷新浏览器等功能。

webpack-dev-server为了提高工作效率,它并没有把打包结果写入磁盘当中,而是暂时存放在内存中,内部的http server从内存中把这些文件读取出来,发送给浏览器。

webpack-dev-server默认只会serve打包输出文件,只要是webpack输出的文件都可以直接被访问。其他静态资源文件也需要serve,需要额外告诉webpack-dev-server。

安装:npm i webpack-dev-server -D

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  // devServer为webpack-dev-server指定配置选项
  devServer: {
    // contentBase指定额外的静态资源路径,通过字符串或者数组可以指定一个或多个路径
    contentBase: './public',
    // proxy指定代理配置
    proxy: {
      '/api': {
        // target指定代理路径
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com',
        // pathRewrite实现代理路径重写
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: {
          '^/api': ''
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
        changeOrigin: true
      }
    }
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorials',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
    // // CopyWebpackPlugin留在上线前那一次打包使用,开发阶段最好不要使用这个插件
    // // 开发过程中会频繁重复执行打包任务,假设需要拷贝的文件多或大,如果每次都执行这个插件,那么打包过程中的开销大,速度也会降低
    // new CopyWebpackPlugin(['public'])
  ]
}

Source Map

通过构建编译已知类的操作可以将开发阶段的源代码转化成生产环境可以运行的代码,这意味着在生产环境运行的代码与源代码之前存在差异。如果需要调试应用或错误信息定位会无从下手,因为调试和报错都是基于运行代码进行的,Source Map就是解决这类问题最好的方法。Source Map就是映射运行代码和源代码之间的关系。

无标题.png

const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist')
  },
  // devtool配置开发过程中的辅助工具,即source map相关配置
  devtool: 'eval',
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 10 * 1024 // 10 KB
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    // 用于生成 index.html
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorials',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.html'
    }),
  ]
}
  • eval:是否使用eval执行模块代码
  • cheap:是否包含行信息
  • module:是否能否得到Loader处理之前的源代码

如何选择Source Map模式?

开发模式下,选择cheap-module-eval-source-map。

原因:

  • 每行代码不会超过80个字符
  • 实际开发中使用框架的情况较多,代码经过loader转换过后的差异较大
  • 虽然首次打包速度慢,但是重写打包相对较快

生产模式下,选择none,不生成Source Map,因为Source Map会暴露源代码。

webpack自动刷新问题

问题核心是自动刷新导致页面状态丢失。最好办法是页面不刷新的前提下,模块也可以及时更新。 模块热更新可以实现这个办法。

模块热更新就是在应用运行过程中实时替换某个模块,而应用运行状态不受影响。

自动刷新会导致页面状态丢失,热更新只将修改的模块实时替换至应用中。

热更新极大程度的提高了开发者的工作效率。

const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  devtool: 'source-map',
  devServer: {
    hot: true // 开启HMR
    // hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: 'file-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorial',
      template: './src/index.html'
    }),
    // 引入HMR插件
    new webpack.HotModuleReplacementPlugin()
  ]
}

webpack中的HMR并不可以开箱即用,需要手动通过代码处理模块热替换逻辑。
框架下的开发,每种文件都是有规律的。通过脚手架创建的项目内部都集成了HMR方案。

// main.js
import createEditor from './editor'
import background from './better.png'
import './global.css'

const editor = createEditor()
document.body.appendChild(editor)

const img = new Image()
img.src = background
document.body.appendChild(img)

// ============ 以下用于处理 HMR,与业务代码无关 ============

if (module.hot) {
  let lastEditor = editor
  module.hot.accept('./editor', () => {
    // console.log('editor 模块更新了,需要这里手动处理热替换逻辑')

    const value = lastEditor.innerHTML
    document.body.removeChild(lastEditor)
    const newEditor = createEditor()
    newEditor.innerHTML = value
    document.body.appendChild(newEditor)
    lastEditor = newEditor
  })

  module.hot.accept('./better.png', () => {
    img.src = background
    console.log(background)
  })
}

不同环境下的配置

生产环境注重运行效率,开发环境注重开发效率。项目开发过程中需要为不同的工作环境创建不同的配置。

创建不同环境配置的方式:

  • 配置文件根据环境不同导出不同配置。此方式适用于中小型项目。
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = (env, argv) => {
  const config = {
    mode: 'development',// 项目环境
    entry: './src/main.js',
    output: {
      filename: 'js/bundle.js'
    },
    devtool: 'cheap-eval-module-source-map',
    devServer: {
      hot: true,
      contentBase: 'public'
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.(png|jpe?g|gif)$/,
          use: {
            loader: 'file-loader',
            options: {
              outputPath: 'img',
              name: '[name].[ext]'
            }
          }
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title: 'Webpack Tutorial',
        template: './src/index.html'
      }),
      new webpack.HotModuleReplacementPlugin()
    ]
  }

  if (env === 'production') {
    config.mode = 'production'
    config.devtool = false
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    ]
  }

  return config
}
  • 一个环境对应一个配置文件。此方式适用于大型项目。

webpack-merge用于合并webpack配置。

//webpack.common.js 公共配置
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'js/bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: {
          loader: 'file-loader',
          options: {
            outputPath: 'img',
            name: '[name].[ext]'
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Webpack Tutorial',
      template: './src/index.html'
    })
  ]
}
// webpack.dev.js 开发模式配置
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    hot: true,
    contentBase: 'public'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})
// webpack.prod.js 生产模式配置
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin(['public'])
  ]
})

DefinePlugin

DefinePlugin为代码注入全局成员。production环境下会默认启用这个插件,向代码中注入process.env.NODE_ENV常量,很多第三方模块都是通过这个常量判断当前运行环境。

const webpack = require('webpack')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.DefinePlugin({
      // 值要求的是一个代码片段,此时可以在代码中应用API_BASE_URL
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

Tree Shaking

Tree Shaking是摇掉代码中未引用部分。Tree Shaking不是指webpack中某个配置选项,而是一组功能搭配使用的优化效果。生产模式下会自动开启Tree Shaking。

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              // 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // 也可以使用默认配置,也就是 auto,这样 babel-loader 会自动关闭 ESM 转换
              ['@babel/preset-env', { modules: 'auto' }]
            ]
          }
        }
      }
    ]
  },
  // 集中配置webpack优化功能
  optimization: {
    // sideEffects一般用于npm包标记是否有副作用
    sideEffects: true,
    // 模块只导出被使用的成员
    usedExports: true,
    // 尽可能合并每一个模块到一个函数中
    // concatenateModules: true,
    // 压缩输出结果
    // minimize: true
  }
}

代码分包

并不是每个模块在启动时都是必要的,此时就需要进行分包,按需加载模块。

实现代码分包的方式:

  • 多入口打包 多入口打包适用于多页应用程序。一个页面对应一个打包入口,公共部分单独提取。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js' //动态输出文件名
  },
  // 集中配置webpack优化功能
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index'] //指定输出的html文件使用的bundle
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}
  • 按需加载 按需加载就是需要用到某个模块时,再加载这个模块。这种方式极大的节省带宽和流量。

动态导入的模块会被自动分包。

// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    })
  ]
}
//index.js

const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''

  if (hash === '#posts') {
    // 魔法注释格式/* webpackChunkName: 名称 */
    // 通过此方式可以灵活组织动态加载的文件所输出的文件
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)

CSS文件处理

MiniCssExtractPlugin是提取css到单个文件,可以实现css的按需加载

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 提取css到单个文件  npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩输出的css文件  npm i optimize-css-assets-webpack-plugin -D
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
// 内置js压缩插件  npm i terser-webpack-plugin -D
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  // 集中配置webpack优化功能
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入到页面中
          MiniCssExtractPlugin.loader, // 此时通过link标签引入css文件
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin()//按需加载css
  ]
}

输出文件名hash

一般去部署前端的文件时会启用服务器的静态资源缓存,这样的话对于用户的浏览器而言,它就可以缓存住应用中的静态资源,后续就不需要请求服务器得到这些资源文件,整体应用的响应速度就得到大幅度提升。不过开启静态资源的客户端缓存会有一些小小的问题,如果在缓存策略中缓存失效时间设置的过短的话,效果就不是很明显;如果过期时间设置的过长,一旦在这段时间内应用发生了更新,重新部署过后,又没有办法及时更新到客户端。为了解决这个问题,在生产模式下,输出的文件中添加hash,这样一旦资源文件发生改变,输出的文件名称也会跟着变化。对于客户端而言,全新的文件名就是全新的请求,那也就没有缓存的问题。

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    // [hash] 项目级别的,一旦项目中有任何一个地方发生改动,那这一次打包中的文件名都会发生变化
    // [chunkhash] chunk级别的,打包过程中同一路的打包,chunkhash都是相同的
    // [contenthash] 文件级别的,根据输出文件的内容输出的hash,不同的文件有不同的hash。这个方式最好。
    filename: '[name]-[contenthash:8].bundle.js'
  },
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin({
      filename: '[name]-[contenthash:8].bundle.css'
    })
  ]
}