系列文章链接:

为什么需要构建工具?

  • 转换 ES6+ 语法;
  • 转换 JSX 语法 / Vue 指令;
  • CSS 私有前缀补全 / 预处理器(lesssass);
  • 压缩混淆 / 图片压缩;

为什么选择 Webpack?

早期的打包工具有 Grount,把打包构建分成一个一个的任务,队列式的处理每一个任务,如解析 html 任务、解析 CSS 任务、解析 JS 任务、图片压缩任务、代码压缩任务等等,每一个任务处理完成之后会将任务结果存放在本地磁盘的 .temp 目录,由于产生了 IO 操作,会导致打包速度比较慢。

后来产生了 Glup,原理与 Grount 类似,管道式的处理打包任务,不同的是 Gulp 有文件流的概念,每一个任务构建后的结果不会存放磁盘,而是存在内存中,在下一个步骤中可以直接使用上一个步骤内存中的结果,提高了打包速度。

目前最火爆的打包工具是 Webpack,在打包性能优于上面工具的基础上,更归功于丰富的生态社区、配置灵活的 loaderplugin,可以通过很灵活的配置完成团队项目个性化的打包需求,并且拥有强大的官方团队进行更新迭代,维护了众多稳定的 loaderplugin,更新速度非常快。

安装 Webpack 及打包命令

安装:

$ npm install webpack webpack-cli -D

使用 Webpack 进行打包执行的其实是 ./node_modules/.bin 目录的 webpack 文件。

# 打包命令
$ ./node_modules/.bin/webpack

为了方便项目中通常将打包命令配置在 package.jsonscripts 中。

/* 打包命令配置 */
{
  "scripts": {
    "build": "webpack"
  }
}

执行配置后的打包命令:

$ npm run build

Webpack 基础配置

零配置

Webpack4 中,在不编写配置文件也可以进行打包,这就是 4.x 版本号称的 “零配置”,其实内部默认对入口文件(entry)和出口文件(output)进行了配置。

/* 零配置默认值 */
module.exports = {
  entry: './src/index.js',
  output: './dist/main.js'
}

mode

modeWebpack4 新提出的概念,用来指定当前构建环境是开发环境(production)、生产环境(development)或 none,默认为 production,设置 mode 可以使用 Webpack 的一些参数值和内置的函数,也可以在打包时针对不同的环境配置不同的打包和优化策略。

mode 配置示例:

module.exports = {
  // ...
  mode: 'development'
  // ...
}

设置为 development 开启的参数如下:

  • 设置 process.env.NODE_ENV 值为 development
  • 开启 NamedChunksPluginNamedModulesPlugin,在代码热更新阶段标识更新的 chunk 和具体模块。

设置为 production 开启的参数如下:

  • 设置 process.env.NODE_ENV 值为 production
  • 开启 FlagDependencyUsagePluginFlagIncludedChunksPluginNoEmitOnErrorsPluginModuleConcatenationPluginOccurrenceOrderPluginSideEffectsFlagPluginTerserPlugin,开启这些插件 Webpack 会对 JS 压缩,识别 package.json 文件中标识代码是否存在副作用的参数等。

mode 设置为 none 不开启任何优化选项。

entry

entry 用于配置打包文件的入口,这个文件中会存在一些依赖关系,依赖的模块又存在依赖关系,最后形成一棵依赖树,Webpack 则将这些模块根据依赖关系,最后打包成多个静态资源,entry 主要有两种应用场景(单页应用和多页应用),配置如下。

/* 单入口(SPA) */
module.exports = {
  entry: './src/index.js'
}
/* 多入口(多页应用) */
module.exports = {
  entry: {
    app: './src/pages/app.js',
    adminApp: './src/pages/adminApp.js'
  }
}

output

entry 配置是用于指定的是源代码,那 output 就是用于指定 Webpack 打包后的结果代码,即用来告诉 Webpack 如何将编译后的文件输出到磁盘。

module.exports = {
  // ...
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  }
}

output 属性值为对象,其中 filename 属性用于指定打包输出后的文件名,path 用于指定打包输出的目录,如果是多页应用,可以使用占位符保证打包后输出多个出口文件名字的唯一性。

/* 多页应用 */
module.exports = {
  entry: {
    app: './src/pages/app.js',
    adminApp: './src/pages/adminApp.js'
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  }
}

上面的 [name] 打包后最后输出的出口文件与入口配置的文件名对应,即 app.jsadminApp.js

loaders

Webpack 默认情况下只支持 jsjson 两种文件类型,loader (加载器)是专门用来支持其他文件类型并把其他文件转换成有效的模块添加到依赖树中,每一个 loader 都默认导出一个函数,接受源文件作为参数,并返回转换的结果,loaders 选项是专门用来配置这些加载器的。

常见 loader 表:

名称描述
babel-loader转化 ES6、ES7 等 JS 新特性
css-loader支持 .css 文件的加载和解析
less-loader将 less 文件转换成 css
ts-loader将 TS 转换成 JS
file-loader对图片、字体等文件的打包
raw-loader将文件以字符串的形式导入
thread-loader多进程打包 JS 和 CSS

配置示例:

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.txt$/, // 指定匹配规则(文件后缀名)
        use: 'raw-loader' // 指定使用的 loader 名称
      }
    ]
  }
}

解析 ES6+ 语法

安装依赖:

$ npm install babel-loader @babel/preset-env -D

loader 配置示例:

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
      // ...
    ]
  }
  // ...
}

在工程中添加 .babelrc 文件来对解析的 ES6+ 语法进行配置。

.babelrc 配置示例:

/* 以 babel7 为例 */
{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    "@babel/proposal-class-properties"
    // ...
  ]
}

babel 配置主要包含两部分,presetspluginsplugins 中配置的每一项都是为了解析某一个语法,而 presets 配置的是这些功能的集合。

解析 React 的 JSX 语法

由于 React 是在 .js.jsx 的文件中使用 JSX 语法,所以解析 JSX 语法也是解析 JS 工作的一部分,同样需要 babel-loader,需要在 Webpack 配置文件的 loader 配置中增加识别 .jsx 文件以及在 .babelrc 配置文件的 presets 中专门增加解析 JSX 功能的集合。

安装依赖:

$ npm install babel-loader @babel/preset-env @babel/preset-react -D

loader 配置示例:

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(jsx?)$/,
        use: 'babel-loader'
      }
      // ...
    ]
  }
  // ...
}

.babelrc 配置示例:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/proposal-class-properties"
    // ...
  ]
}

解析 CSS

解析 CSS 主要靠 css-loaderstyle-loader

  • css-loader:用于加载 .css 文件,并转换成 CommonJS 对象;
  • style-loader:将样式通过 <style></style> 标签插入到 html 文件的 head 中。

安装依赖:

$ npm install css-loader style-loader -D

loader 配置示例:

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

值得注意的是,在处理同一个类型文件使用多个 loader 时,是链式调用的,loader 的执行顺序是从右向左的,所以在编写解析 CSS 加载器配置时应该 style-loader 在前,css-loader 在后,即先通过 css-loader 解析 .css 文件,将解析好的结果传递给 style-loader 处理并插入到页面的 head 中。

解析 Less 和 Sass

LessSass 作为 CSS 的预编译语言,加入了很多编程的特性,功能更强,对样式的组织也更加的灵活,但是浏览器依然不识别,所以也需要 Webpack 进行编译转换。

安装依赖(Less):

$ npm install css-loader style-loader less-loader less -D

安装依赖(Sass):

$ npm install css-loader style-loader sass-loader node-sass -D

loader 配置示例:

/* less 配置 */
module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      }
      // ...
    ]
  }
  // ...
}
/* sass 配置 */
module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader']
      }
      // ...
    ]
  }
  // ...
}

解析图片和字体资源

Webpack 对其他类型的文件进行打包编译需要依赖 file-loader(专门用于处理文件)。

安装依赖:

$ npm install file-loader -D

file-loader 配置示例:

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(png|svg|gif|jpe?g)$/, // 解析图片
        use: 'file-loader'
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/, // 解析字体
        use: 'file-loader'
      }
      // ...
    ]
  }
  // ...
}

也可以使用 url-loader 来实现对图片和字体资源的解析,url-loader 相比 file-loader 而言,支持更颗粒化的解析方式,可以配置解析后出口文件的具体目录,也可以根据资源大小设置将资源转换成 base64

安装依赖:

$ npm install url-loader -D

url-loader 配置示例:

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.(png|svg|gif|jpe?g)$/, // 解析图片
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240, // 资源小于该数值转为 base64
              name: 'img/[name].[ext]' // 打包后的输出路径
            }
          }
        ]
      }
      // 字体资源同理...
    ]
  }
  // ...
}

PostCSS 对 CSS 的增强

在开发 CSS 时,存在着很多让我们头疼的的问题,比如有些 CSS3 的属性由于各浏览器的实现标准不同要加上不同的私有前缀,也比如为了在移动端进行页面适配使用的 remvw 单位与 px 的转换问题等等,其中的一部分问题其实是可以在 Webpack 构建的过程中直接解决的。

安装依赖:

$ npm install postcss-loader autoprefixer postcss-px2rem -D

可以通过 Webpack 配置文件中直接配置,也可以 PostCSS 配置文件中进行配置。

配置示例:

/* 配置在 Webpack 配置文件 */
module.exports = {
  module: {
    rules: [
      // ...
      {
        test: /\.css/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader', // 使用 postcss-loader
            options: {
              plugins: [
                // 添加私有前缀
                require('autoprefixer')({
                  // 兼容浏览器版本(最后两个版本、使用率大于 1%,ios 7 以上)
                  browers: ['last 2 version', '>1%', 'ios 7']
                }),
                // px 自动转换 rem
                require('postcss-px2rem')({
                  remUnit: 75, // 75 px 等于 1 rem
                  remPrecision: 8 // 换算结果小数点后面保留几位小数
                })
                // ...
              ]
            }
          }
        ]
      }
      // ...
    ]
  }
}
/* 在 postcss.config.js 中配置 */
module.exports = {
  plugins: [
    require('autoprefixer')({
      browers: ['last 2 version', '>1%', 'ios 7']
    }),
    require('postcss-px2rem')({
      remUnit: 75,
      remPrecision: 8
    })
    // ...
  ]
}

如果使用其他 PostCSS 的功能也是类似的,需要先下载对应的 PostCSS 插件,然后在配置文件中进行配置。

plugins

plugins 通常用来对 Webpack 打包功能的增强,对打包过程和出口文件大小的优化、资源管理和环境变量的注入,可以作用域整个构建过程。

常见 plugin 表:

名称描述
CommonsChunkPlugin将 chunks 相同的模块代码提取成公共 js
CleanWebpackPlugin清理构建目录
ExtracTextWebpackPlugin将 CSS 从 bundle 文件里提取成一个独立的 .css 文件
CopyWebpackPlugin将文件或者文件夹拷贝到构建的输出目录
HtmlWebpackPlugin创建 html 文件并注入 bundle 文件
UglifyjsWebpackPlugin压缩 .js 文件
ZipWebpackPlugin将打包出的资源生成一个 .zip 包


自动生成 index.html

自动生成 index.html 文件主要通过 HtmlWebpackPlugin 插件实现。

安装依赖:

$ npm install html-webpack-plugin -D

配置示例:

// 引入插件
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ...
  plugins: [
    // 创建插件的实例
    new HtmlWebpackPlugin({
      template: './src/index.html', // 模板文件路径
      filename: 'index.html' // 输出文件名称
    })
    // ...
  ]
}

抽取 CSS 文件

Webpack 在上面对 CSS 的解析中,使用了 css-loaderstyle-loader,通过构建后的结果发现 .css 文件被注入到了 .js 文件中,在生产环境通常会为了减小出口文件的体积对 .css 文件进行抽离,在 Webpack4 中使用 MiniCssExtractPlugin 插件来实现。

安装依赖:

$ npm install mini-css-extract-plugin -D

配置示例:

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

module.exports = {
  // ...
  module: {
    rules: [
      // ...
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader, // 用于将 CSS 抽离的加载器
          'css-loader',
        ]
      }
      // ...
    ]
  },
  plugins: [
    // ...
    new MiniCssExtractPlugin({
      filename: '[name].css' // 抽离的文件名
    })
    // ...
  ]
  // ...
}

解析 .css 文件无论哪种方式需要使用 css-loader,但 MiniCssExtractPlugin 提供的 loaderstyle-loader 的功能是互斥的,style-loader 用于将解析的 CSS 注入,而 MiniCssExtractPlugin.loader 意在单独抽离。

自动清理构建目录

如果输出的文件配置了 hash 且在每次构建时没有及时删除指定的输出目录(如 dist),会导致输出目录中的文件越来越多,不容易区分哪些是新构建出来的文件,所以应该让 Webpack 在每次构建之前清除输出的目录。

当然清除的方式可以多种,比如手动删除,或者在 package.json 中配置的构建命令中增加前置命令如下。

/* 不优雅的方式 */
{
  "scripts": {
    "build": "rm -rf ./dist && webpack"
  }
}

这种方式并不优雅,完全可以通过 Webpack 配置中增加 CleanWebpackPlugin 插件来解决这个问题,这样在每次构建之前就会自动清除输出目录。

安装依赖:

$ npm install clean-webpack-plugin -D

配置示例:

const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  // ...
  output: {
    filename: '[name][chunkhash:8].js',
    path: __dirname + 'dist'
  }
  plugins: [
    new CleanWebpackPlugin()
  ]
  // ...
}

watch

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

Webpack 开启监听模式有两种方式:

  • 启动 Webpack 命令时带上 --watch 参数,可以在 package.json 中进行配置;
  • Webpack 配置文件中进行设置。

添加参数:

/* 配置命令 */
{
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
  }
}

执行命令:

$ npm run watch

Webpack 配置文件:

/* webpack 配置文件 */
module.exports = {
  // ...
  watch: true, // 开启监听
  watchOptions: {
    // 忽略监听的文件目录
    ignored: /node_modules/,
    // 监听到发生变化会等待 300ms 去重新构建(防止多次保存),默认 300ms
    aggregateTimeout: 300,
    // 每秒检查 1000 次
    poll: 1000
  }
  // ...
}

watch 监听原理分析:

Webpack 会轮询的判断文件的最后编辑时间是否发生变化,如果某个文件发生变化不会立即重新构建,而是会将变化缓存起来,等待 aggregateTimeout 配置的时间后重新构建,这样做是为了防止短时间的多次变化或产生了新的变化文件,在该时间到达时将变化的文件列表进行统一构建,以提高性能,这样的监听方式的缺陷是浏览器不会自动刷新,需要手动刷新查看文件修改后的效果。

devServer

webpack-dev-server 是在本地启动服务来监听文件的变化,不是以输出文件的形式更新,而是即时将重新构建的结果放在内存中。

安装依赖:

$ npm install webpack-dev-server -D

配置示例:

module.exports = {
  // ...
  mode: 'development', // 由于 webpack-dev-server 在开发环境中使用
  devServer: {
    host: 'localhost', // 本地服务的域名
    contentBase: './dist', // server 作用的目录
    port: 8080, // 端口号
    compress: true // 是否启动服务器压缩
  }
  // ...
}

配置服务启动命令:

{
  "scripts": {
    "dev": "webpack-dev-server --open"
  }
}

devtool

Webpack 构建后的代码中,经过了压缩、混淆等,会出现一个新的问题,就是代码执行出错后不容定位错误是在源代码中哪一个位置产生的,而 devtool 的配置的作用就是让我们更容易定位错误的位置。

devtool 的关键词:

关键词作用
evel使用 evel 函数包裹代码
source-map产生 .map 文件
cheap不含列信息
inline将 .map 内容作为 DataURI 嵌入,不单独生成 .map 文件
module包含 loader 的 source-map

devtool 属性的值就是由上面的关键词组成的,不同的名字会使用不同的调试策略,而名字中的关键词则包含了上面特性,下面是 Webpcak 官网给出的不同构建策略对应的信息。

devtool首次构建二次构建是否适合生产环境可以定位的代码
none++++++yes最终输出的代码
eval++++++noWebpack 生成的代码(一个个的模块)
cheap-eval-source-map+++no经过 loader 转换后的代码(只能看到行)
cheap-module-eval-source-mapo++no源代码(只能看到行)
eval-source-map--+no源代码
cheap-source-map+ono经过 loader 转换后的代码(只能看到行)
cheap-module-source-mapo-no源代码(只能看到行)
inline-cheap-source-map+ono经过 loader 转换后的代码(只能看到行)
inline-cheap-module-source-mapo-no源代码(只能看到行)
source-map----yes源代码
inline-source-map----no源代码
hidden-source-map----yes源代码
nosources-source-map----yes无源代码

+++ 非常快速,++ 快速,+ 比较快,o 中等,- 比较慢,--

配置示例:

module.exports = {
  // ...
  devtool: 'evel'
  // ...
}

使用不同的 devtool 会带来不同的效果,使用 evel 不安全,使用 inline 注入会增加打包文件的大小、线上环境生成 .map 文件会容易被人反编译进而暴露业务逻辑等等,所以在 devtool 使用时还是根据自己的需要和安全考虑来权衡。

热更新

配置热更新的方式

webpack-dev-server 可以配合自带的插件实现热更新,即文件修改后,不刷新浏览器的情况下自动构建并在浏览器中响应渲染。

/* 使用 webpack-dev-server */
const webpack = require('webpack');

module.exports = {
  // ...
  mode: 'development',
  devServer: {
    host: 'localhost',
    contentBase: './dist',
    port: 8080,
    compress: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
  // ...
}

想要颗粒度更细致的控制 Webpack 的热更新,也可以使用另一种方式,即借助 ExpressKoa 自己创建一个服务,并借助 webpack-dev-middleware 来实现热更新,这种方式更适合灵活的定制化场景。

/* 使用 webpack-dev-middleware */
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');

const app = express();
const compiler = webpack(config);

app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

app.listen(3000, function () {
  console.log('server start 3000');
});

热更新原理简介

热更新的实现分为服务端和浏览器两个部分:

  • Webpack Dev Server
    • Webpack CompileWebpack 的编译器,作用是将 JS 编译成 Bundle
    • HMR Server:将热更新的文件输出给 HMR Runtime
    • Bundle Server:提供文件在浏览器以服务器的方式访问。
  • Browser
    • HMR Runtime:开发阶段打包过程中,会被注入到浏览器,使浏览器的 bundle.js 和服务器建立 websocket 链接,以更新文件的变化;
    • bundle.js:构建输出的文件。

Webpack 在将本地文件显示在浏览器其实有两个阶段:

  • 第一个阶段为启动阶段通过 Webpack Compile 将文件系统中的文件进行构建,然后将文件传递给 Bundle ServerBundle Serverbundle.js 响应给浏览器;
  • 第二个阶段为热更新阶段,依然通过 Webpack Compile 对文件系统中修改的文件进行构建,将构建后的结果传递给 HMR ServerHMR Server 通过 Websocket 协议将文件变化的结果通知浏览器端的 HMR Runtime,执行代码并刷新页面。

未完待续…