概要

介绍 webpack 的基础用法。

为什么选择 webpack

  • 社区生态丰富
  • 配置灵活、插件化扩展
  • 官方更新迭代速度快

    功能介绍

代码转换

把浏览器不支持的东西转换成浏览器支持的东西。例如 ES6 有很多语法糖,值得我们使用。

ES6 -> ES5、SCSS -> CSS 等。

文件优化

浏览器处理资源时,需要耗费时间,所以需要压缩文件,这样,http 请求更快,浏览器处理更快。

压缩 JS、CSS、HTML、图片等。

代码检查

可以配置 ESLint 等工具,约束团队。前端单元测试。

代码规范、单元测试等。

模块合并与分割

将组件化、模块化的代码公共代码进行抽取,优化,合并。有些文件可以设置异步加载。

刷新与发布

webpack 提供了工程化、自动化的构建方案,方案即系统化、流程化、规范化的自动构建。

安装与使用

安装

  1. npm i webpack@4.16.5 webpack-cli -D

不推荐使用全局安装。多个项目可能会使用不同的 webpack 版本与配置,全局安装很可能会导致诸多问题。

webpack-cli

webpack command line interface:webpack 命令行接口

  1. webpack3 中,webpack-cli 和 webpack 在一个包中
  2. webpack4 与 wenpack-cli 分来管理,也需要单独安装
  3. webpack 是核心功能库,webpack-cli 是提供 webpack 进行构建的命令行接口

使用

直接运行命令

  1. npx webpack

编写 script 脚本

  1. {
  2. "name": "webpack-demo",
  3. "version": "1.0.0",
  4. "description": "",
  5. "private": true,
  6. "scripts": {
  7. "build": "webpack"
  8. },
  9. "keywords": [],
  10. "author": "heora",
  11. "license": "ISC",
  12. "devDependencies": {
  13. "webpack": "^5.17.0",
  14. "webpack-cli": "^4.4.0"
  15. }
  16. }
  1. npm run build

基础概念

entry

Entry 用来指定 webpack 的打包入口。

webpack中,所有都是模块,JS/CSS/HTML都是模块,如需打包,必须引入到入口文件里,

当然也可以借助 plugin 对模块进行单独处理,但常规情况下,都是通过入口文件。

理解依赖图的含义

webpack 是一个模块打包工具(module bundler)。

它会把一切的资源,不管是代码资源还是非代码资源(图片、字体依赖等),都会不断加入到依赖图中。

webpack.png

entry 用法

单入口

entry 是一个字符串。

  1. module.exports = {
  2. entry: './src/index.js'
  3. }

一般比较适用于一个项目只有一个入口文件或者项目是单页面应用。

多入口

entry 是一个对象。

  1. module.exports = {
  2. entry: {
  3. index: './src/index.js',
  4. detail: './src/detail.js'
  5. }
  6. }

适合多页面的场景,多页应用。

output

Output 用来告诉 webpack 如何将编译后的文件输出到磁盘。

单入口配置

  1. module.exports = {
  2. entry: './src/index.js',
  3. output: {
  4. filename: 'bundle.js',
  5. path: path.resolve(__dirname, 'dist')
  6. }
  7. }

多入口配置

可以通过占位符确保文件名称的唯一。

  1. module.exports = {
  2. entry: {
  3. index: './src/index.js',
  4. detail: './src/detail.js'
  5. }
  6. output: {
  7. filename: '[name].js',
  8. path: path.resolve(__dirname, 'dist')
  9. }
  10. }

loader

webpack 开箱即用只支持 JS 和 JSON 两种文件类型,可以通过 Loader 去支持其他文件类型并且把它们转换成有效的模块,并且可以添加到依赖图中。

loader 本身是一个函数,接收源文件作为参数,返回转换的结果。

常见的 loader

名称 描述
babel-loader 转换 ES6、ES7 等 JS 新特性语法
css-loader 支持 .css 文件的加载和解析
style-loader 将 css-loader 处理后的内容挂载到页面的 head 部分
less-loader 将 less 文件转换成 css
ts-loader 将 ts 转换为 JS
file-loader 进行图片、字体的打包
url-loader 功能类似于 file-loader,可以指定限制,返回一个 DataURL
raw-loader 将文件以字符串的形式导入
thread-loader 多进程打包 JS 和 CSS

loader 的用法

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.(jpg|png|gif)$/,
  6. use: {
  7. loader: 'url-loader',
  8. options: {
  9. name: '[name]_[hash].[ext]',
  10. outputPath: 'images/',
  11. limit: 2048
  12. }
  13. }
  14. },
  15. {
  16. test: /\.scss$/,
  17. use: [
  18. 'style-loader',
  19. 'css-loader',
  20. 'sass-loader',
  21. 'postcss-loader'
  22. ]
  23. }
  24. ]
  25. }
  26. }

test 指定匹配规则。use 执行使用的 loader 名称。

  1. 多个loader的执行顺序是从后向前
  2. 第一个loader将模块代码作为参数,然后处理参数并返回
  3. 其后的loader接收前一个loader的返回值作为参数,继续处理参数并返回
  4. 最后一个loader返回处理后的JS模块源码

Plugins

插件用于 bundle 文件的优化,资源管理和环境变量注入,作用于整个构建过程。

任何不能通过 loader 完成的事情,都可以用 plugins 完成,比如构建之前需要手动删除目录。

每一个 plugin 实际上是一个类(构造函数)。

常见的 Plugin

名称 描述
CommonsChunkPlugin 将 chunks 相同的模块代码提取成公共 js(通常用于多个页面打包的情况)
CleanWebpackPlugin 清理构建目录
ExtractTextWebpackPlugin 将 CSS 从 bundle 文件里提取成一个独立的 CSS 文件
MiniCssExtaractPlugin 将 CSS 源码提取,创建为独立的文件引入相应的HTML中
HtmlWebpackPlugin 创建 html 文件去承载输出的 bundle
CopyWebpackPlugin 将文件或者文件夹拷贝到构建的输出目录
UglifyjsWebpackPlugin 压缩 JS
ZipWebpackPlugin 将打包出的资源生成一个 zip 包

Plugin 的用法

  1. module.exports = {
  2. plugins: [
  3. new htmlWebpackPlugin({
  4. template: path.resolve(__dirname, 'src/index.html'),
  5. filename: 'index.html',
  6. chunks: ['index']
  7. }),
  8. new htmlWebpackPlugin({
  9. template: path.resolve(__dirname, 'src/list.html'),
  10. filename: 'list.html',
  11. chunks: ['list']
  12. }),
  13. new MiniCssExtractPlugin({
  14. // 独立打包 css 文件的相对路径与名称
  15. filename: 'css/[name].css',
  16. // 通过import导入的样式文件模块的文件名
  17. // 生成一个 JS 的 chunk
  18. chunkFilename: 'css/[id].[hash:16].css'
  19. }),
  20. // 优化与压缩 CSS 资源
  21. new OptimizeCssAssetsPlugin({
  22. // https://www.cssnano.cn/guides/optimisations/
  23. // https://www.cssnano.cn/guides/presets/
  24. // CSS 优化插件设置
  25. cssProcessor: require('cssnano'),
  26. // CSS 优化插件配置项
  27. cssProcessorOptions: {
  28. preset: [
  29. 'default',
  30. {
  31. discardCommonents: {
  32. removeAll: true
  33. }
  34. }
  35. ]
  36. }
  37. })
  38. ]
  39. }

将定义好的插件实例化并放入 plugins 数组中。

  1. webpack在其生命周期中会向外界广播一些事件钩子
  2. 每一个plugin都可以监听其中的任何一个事件钩子
  3. 当被监听的事件响应,就执行对应的处理函数改变原本webpack正常输出的结果
  4. 插件的执行顺序是从上到下

mode

Mode 用来指定当前的构建环境是:production、development 还是 none。

设置 mode 可以使用 webpack 的内置函数,默认为 production。

Mode 的内置函数功能

选项 描述
development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin.
none 不开启任何优化选项

基础用法

解析 ES6 和 React JSX

解析 ES6

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

webpack 原生支持 js 解析,但是对于 ES6 语法,原生是不支持的。

可以使用 babel-loader,babel 的配置文件是:.babelrc。

  1. const path = require('path');
  2. module.exports = {
  3. mode: "development",
  4. entry: './src/index.js',
  5. output: {
  6. filename: 'bundle.js',
  7. path: path.resolve(__dirname, 'dist')
  8. },
  9. module: {
  10. rules: [
  11. {
  12. test: /.js$/,
  13. use: 'babel-loader'
  14. }
  15. ]
  16. }
  17. }

增加 ES6 的 babel preset 配置。

  1. {
  2. "presets": [
  3. "@babel/preset-env"
  4. ]
  5. }

解析 React JSX

  1. npm i react react-dom -S
  1. npm i @babel/preset-react -D

package.json 中 dependencies 和 devDependencies 的区别是:devDependencies 用于本地环境开发时候,dependencies 用户发布环境,也就是开发阶段的依赖最后是不对被打入包内。 通常框架、组件和 utils 等业务逻辑相关的包依赖放在 dependencies s里面,对于构建、ESlint、单元测试等相关依赖放在 devDependencies 中。

在前面的基础之上增加 React 的 babel preset 配置,同理,vue 也可以这样配置。

  1. {
  2. "presets": [
  3. "@babel/preset-env",
  4. "@babel/preset-react"
  5. ]
  6. }

babel-loader 解析 ES6 的语法也是需要知道哪些语法需要解析,需要通过 .babelrc 进行配置,这个 @babel/preset-env 就是告诉 babel-loader 要解析ES6的语法,其它的 react 语法可以通过其它的 babel preset 进行处理。

编写个测试代码

  1. 'use strict';
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. class Search extends React.Component {
  5. render () {
  6. return <div>Hello React</div>;
  7. }
  8. }
  9. ReactDOM.render(
  10. <Search />,
  11. document.getElementById('app')
  12. );

解析 CSS、Less 和 Sass

解析 CSS

css-loader 用于加载 .css 文件,并且转换成 commonjs 对象。

style-loader 将样式通过 <style> 标签插入到 head 中。

  1. npm i style-loader css-loader -D

index.js

  1. 'use strict';
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import './css/index.css';
  5. class Search extends React.Component {
  6. render () {
  7. return (
  8. <div className="text">Hello React</div>
  9. );
  10. }
  11. }
  12. ReactDOM.render(
  13. <Search />,
  14. document.getElementById('app')
  15. );

index.css

  1. .text {
  2. font-size: 20px;
  3. color: red;
  4. }

webpack.config.js

const path = require('path');

module.exports = {
  mode: "development",
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
}

解析 Less、Sass

less-loader 用于将 less 转换成 css。

npm i less less-loader -D

index.js

import './css/index.less';

webpack.config.js

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

sass-loader 和 less-loader 配置基本一致。

npm i node-sass sass-loader -D

解析图片和字体

file-loader 用于处理文件。

npm i file-loader -D

解析图片

index.js

import './css/index.less';
import avator from './imgs/avator.png';

class Search extends React.Component {
  render () {
    return (
      <div className="text">
        Hello React <img width={ 200 } height={ 200 } src={ avator } />
      </div>
    );
  }
}

webpack.config.js

module: {
  rules: [
    {
      test: /\.(png|jpg|gif|jpeg)$/,
      use: 'file-loader'
    }
  ]
}

解析字体

file-loader 可以用来处理图片,也可以用来处理字体。

index.less

@font-face {
  font-family: 'SourceHanSerifSC-Heavy';
  src: url('../fonts/SourceHanSerifSC-Heavy.otf');
}

.text {
  font-size: 20px;
  color: red;
  font-family: 'SourceHanSerifSC-Heavy';
}

webpack.config.js

module: {
  rules: [
    {
      test: /\.(woff|woff2|eot|ttf|otf)$/,
      use: 'file-loader'
    }
  ]
}

资源解析

url-loader 也可以处理图片和字体。还可以设置较小资源自动转换 base64。

url-loader 内部也是使用的 file-loader。对于图片资源,可以使用 url-loader 进行处理。

npm i url-loader -D

webpack.config.js

module: {
    // {
    //   test: /.(png|jpg|gif|jpeg)$/,
    //   use: 'file-loader'
    // },
    {
      test: /\.(png|jpg|gif|jpeg)$/,
      use: [
        {
          loader: 'url-loader',
          options: {
            limit: 10240
          }
        }
      ]
    }
  ]
}

webpack 文件监听

文件监听是在发现源码发生变化时,自动重新构建出新的输出文件。

开启方式

webpack 开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 —watch 参数。

    "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
    },
    
  • webpack.config.js 中设置 watch: true。

缺陷

每次需要手动刷新浏览器。

原理分析

轮询判断文件的最后编辑时间是否变化。

某个文件发生变化,并不会立即告诉监听者,而是先缓存起来,等 aggregateTimeout。

webpack.config.js

// 默认 false,也就是不开启
watch: true,
// 只有开启监听模式,watchOptions 才生效
watchOptions: {
  // 默认为空,不监听的文件或者文件夹,支持正则匹配
  ignored: /node_modules/,
  // 监听到变化发生后会等 300ms 再去执行,默认 300ms
  aggregateTimeout: 300,
  // 判断文件是否变化是通过不停询问系统指定文件有没有变化实现的,默认每秒询问 1 次
  poll: 1000
}

webpack 热更新

使用 webpack-dev-server。

npm i webpack-dev-server -D

特性

WDS 不刷新浏览器。

WDS 不输出文件,而是放在内存中。

配置 hot:true后,自动引入 HotModuleReplacementPlugin 插件。

webpack-dev-server(WDS)的功能提供 bundle server的能力,就是生成的 bundle.js 文件可以通过 localhost://xxx 的方式去访问,另外 WDS 也提供 livereload(浏览器的自动刷新)。 hot-module-replacement-plugin 的作用是提供 HMR 的 runtime,并且将 runtime 注入到 bundle.js 代码里面去。一旦磁盘里面的文件修改,那么 HMR server 会将有修改的 js module 信息发送给 HMR runtime,然后 HMR runtime 去局部更新页面的代码。因此这种方式可以不用刷新浏览器。 单独写两个包也是出于功能的解耦来考虑的。简单来说就是:hot-module-replacement-plugin 包给 webpack-dev-server 提供了热更新的能力。

配置

package.json

"scripts": {
  "build": "webpack",
  "watch": "webpack --watch",
  "dev": "webpack serve --open"
},

webpack.config.js

webpack-dev-server 开发环境使用,生产环境不需要使用。

module.exports = {
  // ...
  mode: "development",
  // ...
  devServer: {
    contentBase: './dist',
    hot: true
  }
}

其他方式

使用 webpack-dev-middleware。

WDM 将 webpack 输出的文件传输给服务器(通常使用 express,koa2)。

适用于灵活的定制场景。

原理分析

Webpack Compile 将 JS 编译成 Bundle。

Bundle server:提供文件在浏览器的访问。

HMR Server:建热更新的文件输出给 HMR Runtime。

HMR Runtime:会被注入到浏览器。

webpack-dev-server.png

正常流程 1-2-A-B ,更新流程 1-2- 3-4。

文件指纹策略

文件指纹指打包后的文件名的后缀。

通常文件指纹用来做版本管理,只需要发布修改的页面。

常见指纹

文件指纹策略:chunkhash、contenthash、hash。

Hash

和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改。

Chunkhash

和 webpack 打包出的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值。

Contenthash

根据文件内容来定义 hash,文件内容不变,则 contenthash 不变。

JS 文件指纹设置

设置 output 的 filename,使用 [chunkhash]。

CSS 文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 [contenthash]。

单纯使用 css-loader 和 style-loader 会把 css 插入到 head 头部,不存在文件的概念。

图片的文件指纹设置

设置 file-loader 的 name,使用 [hash]。

这里的 hash 也是指文件内容 hash,和上面提的 Hash 的含义不同。

占位符名称 含义
[ext] 资源后缀名
[name] 文件名称
[path] 文件的相对路径
[folder] 文件所在的文件夹
[contenthash] 文件的内容 hash,默认是 md5 生成
[hash] 文件内容的 hash,默认是 md5 生成
[emoji] 一个随机的指代文件内容的 emoj

配置

webpack 的 chunkhash 不能和热更新一起使用(测试环境),需要在生产环境使用。

css 使用文件指纹,需要安装 plugin。

npm i mini-css-extract-plugin -D

package.json

"scripts": {
  "watch": "webpack --watch",
  "build": "webpack --config webpack.prod.js",
  "dev": "webpack serve --config webpack.dev.js --open"
}

webpack.prod.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    publicPath: '/',
    filename: '[name][chunkhash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: "production",
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              name: '[name]_[hash:8].[ext]',
              limit: 10240
            },
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'
            },
          }
        ]
      },
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    })
  ]
}

HTML、CSS、JavaScript 代码压缩

代码压缩分为三块。

HTML 压缩、CSS 压缩、JS 压缩。

JS 文件压缩

内置 uglifyjs-webpack-plugin。

生产环境默认开启此插件。

CSS 文件压缩

使用 optimize-css-assets-webpack-plugin,同时使用 cssnano。

npm i optimize-css-assets-webpack-plugin -D
npm i cssnano -D

HTML 文件压缩

修改 html-wbepack-plugin,设置压缩参数

npm i html-webpack-plugin -D

webpack5 使用 html-webpack-plugin 报错,建议降级 webpack4。

删除再安装发现可以,但是未复现解决策略,目前看是还是存在兼容问题的。

配置

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js'
  },
  // ...
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin({
      assetNameRegExp: /\.css$/g,
      cssProcessor: require('cssnano')
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src/index.html'),
      filename: 'index.html',
      chunks: ['index'],
      excludeChunks: ['node_modules'],
      inject: true,
      minify: {
        html5: true,
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true,
        removeComments: false
      }
    })
  ]
}