webpack 简介

92 阅读7分钟

webpack 是前端项目常用的模块打包器,但不少开发者对 webpack 通常一年只接触两次,剩下的时间就 "只管用"了。接下来作者将带着大家重温下webpack

webpack简介及作用

本质上, webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

webpack的安装与基本配置

安装

npm install webpack webpack-cli --save-dev

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

基本webpack.config.js文件配置

const path = require('path');

module.exports = {
   entry: './src/index.js',
   output: {
      filename: 'main.js',
      path: path.resolve(__dirname, 'dist'),
   },
};

通过配置文件执行构建

npx webpack --config webpack.config.js

常用的webpack配置项

入口(entry)

入口起点(entry point)用来告诉 webpack 使用哪个模块来作为内部依赖图的构建开始。进入入口起点后,webpack 将会找出入口起点(直接和间接)依赖的模块和库。 入口起点可以只有一个也可以有多个

module.exports = {
   // (简写)语法 单个入口
   entry: './path/to/my/entry/file.js', //entry 默认值是./src/index.js 
   // 也可以将一个文件路径数组传递给 entry 属性
   // entry: ['./src/file_1.js', './src/file_2.js'],
   // 也可以采用对象语法。 对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。
   entry: {
      app: './src/app.js',
      adminApp: './src/adminApp.js',
   },
};

用于描述入口的对象。你可以使用如下属性:

  • dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载。(不能是循环引用的)

  • filename: 指定要输出的文件名称。

  • import: 启动时需加载的模块。

  • library: 指定 library 选项,为当前 entry 构建一个 library。

  • runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。(runtime 不能指向已存在的入口名称)

  • publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。

输出(output)

output 指定 webpack 构建后所创建的 bundle 位置,以及命名规则。主要输出文件的默认值是 ./dist/main.js ,其他生成文件默认放置在 ./dist 文件夹中。(只能指定一个 output 配置)

const path = require('path');

module.exports = {
   entry: './path/to/my/entry/file.js',
   output: {
      path: path.resolve(__dirname, 'dist'), // 生成的文件位置
      filename: 'my-first-webpack.bundle.js', // 生成的文件名
      // 如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions) 来确保每个文 唯一  的名称。
      //  filename: '[name].js',
   },
};

加载器(loader)

loader 用于对模块的源代码进行转换。 webpack 只能理解 JavaScript 和 JSON 文件,loader 使 webpack 可以处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。 例如,你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。为此,首先安装相对应的 loader:

npm install --save-dev css-loader ts-loader

然后指示 webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loadermodule.rules 允许你在 webpack 配置中指定多个 loader。 这种方式是展示 loader 的一种简明方式,并且有助于使代码变得简洁和易于维护。

loader 总是从右到左(或从下到上)地取值(evaluate)/执行(execute)。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。

module.exports = {
   module: {
      rules: ['a-loader', 'b-loader', 'c-loader'],
   },
};

实际执行顺序

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loaderpitch 方法返回了一些东西:

module.exports = function (content) {
  return someSyncOperation(content);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return (
      'module.exports = require(' +
      JSON.stringify('-!' + remainingRequest) +
      ');'
    );
  }
};

上面的步骤将被缩短为:

|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution

插件(plugin)

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。

  • 配置方式

    webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
   module: {
      rules: [{ test: /\.txt$/, use: 'raw-loader' }],
   },
   plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
  • NODE API方式

    在使用 Node API 时,还可以通过配置中的 plugins 属性传入插件。

const webpack = require('webpack'); // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');

let compiler = webpack(configuration);

new webpack.ProgressPlugin().apply(compiler);

compiler.run(function (err, stats) {
  // ...
});

模式(mode)

通过选择 development, productionnone 其中的一个,来设置 mode 参数,使之可以在开发和生产环境采用不同的配置。你可以启用 webpack 内置在相应环境下的优化。其默认值为 production

module.exports = {
   mode: 'production',
};

生产环境下将会默认打开一些性能优化配置,如:代码压缩与tree shaking。

解析(Resolve)

配置模块如何解析。例如,当在 ES2015 中调用 import 'lodash'resolve 选项能够对 webpack 查找 'lodash' 的方式去做修改。

module.exports = {
  //...
  resolve: {
    // configuration options
  },
};
  • resolve.alias

    创建 importrequire 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:

const path = require('path');

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
};

现在,替换“在导入时使用相对路径”这种方式,就像这样:

import Utility from '../../utilities/utility';

//也可以这样使用别名:
import Utility from 'Utilities/utility';
  • resolve.fallback

    当正常解析失败时,重定向模块请求。

    module.exports = {
      //...
      resolve: {
        fallback: {
          crypto: require.resolve('crypto-browserify'),
          stream: require.resolve('stream-browserify'),
        },
      },
    };
    

devServer

webpack-dev-server 够实现代码修改后自动打包,自动刷新浏览器,从而提高我们的开发效率。

  • devServer.allowedHosts : 该选项允许将允许访问开发服务器的服务列入白名单。
    module.exports = {
      //...
      devServer: {
        allowedHosts: [
          'host.com',
          'subdomain.host.com',
          'subdomain2.host.com',
          'host2.com',
        ],
      },
    };
    

    . 作为子域通配符。.host.com 会与 host.com,www.host.com 以及 host.com 等其他任何其他子域匹配。当设置为 'all' 时会跳过 host 检查。并不推荐这样做,因为不检查 host 的应用程序容易受到 DNS 重绑定攻击。当设置为 'auto' 时,此配置项总是允许 localhost、 host 和 client.webSocketURL.hostname

  • devServer.client
    • logging 允许在浏览器中设置日志级别,例如在重载之前,在一个错误之前或者 热模块替换 启用时。
    module.exports = {
      //...
      devServer: {
        client: {
          // 'log' | 'info' | 'warn' | 'error' | 'none' | 'verbose'
          logging: 'info',
        },
      },
    };
    
    • overlay boolean = true object: { errors boolean = true, warnings boolean = true } 当出现编译错误或警告时,在浏览器中显示全屏覆盖。
    • progress boolean 在浏览器中以百分比显示编译进度。
  • devServer.compress boolean = true 启用 gzip 压缩
  • devServer.hot 'only' boolean = true 启用 webpack 的 热模块替换 特性。启用热模块替换功能,在构建失败时不刷新页面作为回退,使用 hot: 'only'
  • devServer.open boolean string object [string, object] 告诉 dev-server 在服务器已经启动后打开浏览器。设置其为 true 以打开你的默认浏览器。
  • devServer.port 'auto' string number 指定监听请求的端口号。
  • devServer.proxy object [object, function] 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。可以解决开发环境的跨域问题。
    module.exports = {
      //...
      devServer: {
        proxy: {
          '/api': 'http://localhost:3000',
        },
      },
    };
    
    现在,对 /api/users 的请求会将请求代理到 http://localhost:3000/api/users
    • 如果不希望传递/api,则需要重写路径:
      module.exports = {
        //...
        devServer: {
          proxy: {
            '/api': {
              target: 'http://localhost:3000',
              pathRewrite: { '^/api': '' },
            },
          },
        },
      };
      
    • 默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。 如果需要,可以这样修改配置
      module.exports = {
        //...
        devServer: {
          proxy: {
            '/api': {
              target: 'https://other-server.example.com',
              secure: false,
            },
          },
        },
      };
      
    • 有时不想代理所有内容。 可以基于函数的返回值绕过代理。 在该功能中,可以访问请求,响应和代理选项。
      • 返回 nullundefined 以继续使用代理处理请求。
      • 返回 false 会为请求产生 404 错误。
      • 返回提供服务的路径,而不是继续代理请求。 例如。 对于浏览器请求,想要提供 HTML 页面,但是对于 API 请求,想要代理它。 可以执行以下操作:
      module.exports = {
        //...
        devServer: {
          proxy: {
            '/api': {
              target: 'http://localhost:3000',
              bypass: function (req, res, proxyOptions) {
                if (req.headers.accept.indexOf('html') !== -1) {
                  console.log('Skipping proxy for browser request.');
                  return '/index.html';
                }
              },
            },
          },
        },
      };
      
    • 如果想将多个特定路径代理到同一目标,则可以使用一个或多个带有 context 属性的对象的数组:
      module.exports = {
        //...
        devServer: {
          proxy: [
            {
              context: ['/auth', '/api'],
              target: 'http://localhost:3000',
            },
          ],
        },
      };
      
    • 默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为。
      module.exports = {
        //...
        devServer: {
          proxy: {
            '/api': {
              target: 'http://localhost:3000',
              changeOrigin: true,
            },
          },
        },
      };
      

cache

缓存生成的 webpack 模块和 chunk,来改善构建速度。cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用。 cache: truecache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存:

module.exports = {
  //...
  cache: false,
};

当将 cache.type 设置为 'filesystem' 是会开放更多的可配置项。

  • cache.type

    string: 'memory' | 'filesystem'

    cache 类型设置为内存或者文件系统。memory 选项很简单,它告诉 webpack 在内存中存储缓存,不允许额外的配置。

  • cache.cacheDirectory

    缓存文件存放的路径,默认为 node_modules/.cache/webpack。(当 cache.type 被设置成 'filesystem' 可用)。

    const path = require('path');
    module.exports = {
      //...
      cache: {
        type: 'filesystem',
        cacheDirectory: path.resolve(__dirname, '.temp_cache'),
      },
    };
    
  • cache.buildDependencies

    额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为项目配置文件。 默认是 webpack/lib 来获取 webpack 的所有依赖项。

    module.exports = {
      cache: {
        buildDependencies: {
          // This makes all dependencies of this file - build dependencies
          config: [__filename],
          // 默认情况下 webpack 与 loader 是构建依赖。
        },
      },
    };
    
  • cache.managedPaths

    受控目录,Webpack 构建时会跳过新旧代码哈希值与时间戳的对比,直接使用缓存副本,默认值为 ['./node_modules']

  • cache.profile

    跟踪并记录各个 'filesystem' 缓存项的详细时间信息。默认值为 false

  • cache.maxAge

    缓存失效时间(以毫秒为单位),默认值为一个月(5184000000)

  • cache.allowCollectingMemory

    收集在反序列化期间分配的未使用的内存,仅当 cache.type 设置为 'filesystem' 时生效。这需要将数据复制到更小的缓冲区中,并有性能成本。

    module.exports = {
      cache: {
        type: 'filesystem',
        allowCollectingMemory: true,
      },
    };
    

devtool

此选项控制是否生成,以及如何生成 source map。source map 是用于在浏览器的开发者工具中跳转到源代码的映射文件,可以方便地进行代码调试和性能分析。选择一种 source map 风格来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

你可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,因为它有更多的选项。切勿同时使用 devtool 选项和 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 插件。devtool 选项在内部添加过这些插件,所以你最终将应用两次插件。

一些常见配置

devtoolperformanceproductionquality
(none)build: fastest
rebuild: fastest
yesbundle
evalbuild: fast
rebuild: fastest
nogenerated
eval-cheap-source-mapbuild: ok
rebuild: fast
notransformed
eval-source-mapbuild: slowest
rebuild: ok
nooriginal
eval-cheap-module-source-mapbuild: slow
rebuild: fast
nooriginal lines
source-mapbuild: slowest
rebuild: slowest
yesoriginal
hidden-source-mapbuild: slowest
rebuild: slowest
yesoriginal
nosources-source-mapbuild: slowest
rebuild: slowest
yesoriginal

验证 devtool 名称时, 我们期望使用某种模式, 注意不要混淆 devtool 字符串的顺序, 模式是: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

上述模式中有三类关键词:

  • inlinehiddeneval
    • inline:Source Map内容通过base64放在js文件中引入。 image.png
    • hidden:代码中没有sourceMappingURL,浏览器不自动引入Source Map。 image-1.png
    • eval:生成代码和Source Map内容混淆在一起,通过eval输出。 image-2.png
  • nosources 使用这个关键字的Source Map不包含sourcesContent,调试时只能看到文件信息和行信息,无法看到源码。

品质说明(quality)

quality 决定我们调试时能看到的源码内容。

  • bundled:将所有生成的代码视为一大块代码。你看不到相互分离的模块。
  • generated:每个模块相互分离,并用模块名称进行注释。可以看到 webpack 生成的代码。示例:你会看到类似 var module__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(42); module__WEBPACK_IMPORTED_MODULE_1__.a();,而不是 import {test} from "module"; test();
  • transformed:每个模块相互分离,并用模块名称进行注释。可以看到 webpack 转换前、loader 转译后的代码。示例:你会看到类似 import {test} from "module"; var A = function(_test) { ... }(test);,而不是 import {test} from "module"; class A extends test {};
  • original:每个模块相互分离,并用模块名称进行注释。你会看到转译之前的代码,正如编写它时。这取决于 loader 支持。
  • (lines only):source map 被简化为每行一个映射。这通常意味着每个语句只有一个映射(假设你使用这种方式)。这会妨碍你在语句级别上调试执行,也会妨碍你在每行的一些列上设置断点。与压缩后的代码组合后,映射关系是不可能实现的,因为压缩工具通常只会输出一行。

外部扩展(Externals)

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。 externals 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。 例如,从 CDN 引入 jQuery,而不是把它打包:

<!-- index.html -->
<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>
// webpack.config.js
module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';

$('.my-element').animate(/* ... */);

上面展示了一个使用外部全局变量的示例,但实际上可以以以下任何形式使用外部变量:全局变量、CommonJS、AMD、ES2015 模块。

  • externals的几种写法
module.exports = {
  // 1.字符串
  externals: 'jquery',

  // 2.[string]
  externals: {
    subtract: ['./math', 'subtract'],
  },

  // 3.对象
  externals: {
    react: 'react',
  },
  // 或者
  externals: {
    lodash: {
      commonjs: 'lodash',
      amd: 'lodash',
      root: '_', // 指向全局变量
    },
  },
  // 或者
  externals: {
    subtract: {
      root: ['math', 'subtract'],
    },
  },

  // 4.函数
  /**
   * 1) function ({ context, request, contextInfo, getResolve }, callback) 
   * 2) function ({ context, request, contextInfo, getResolve }) => promise
   * 
   * 函数接收两个入参:
   * - ctx (object):包含文件详情的对象。
         ctx.context (string): 包含引用的文件目录。
         ctc.request (string): 被请求引入的路径。
         ctx.contextInfo (object): 包含 issuer 的信息(如,layer 和 compiler)
         ctx.getResolve 5.15.0+: 获取当前解析器选项的解析函数。
     - callback (function (err, result, type)): 用于指明模块如何被外部化的回调函数

     回调函数接收三个入参:
      - err (Error): 被用于表明在外部外引用的时候是否会产生错误。如果有错误,这将会是唯一被用到的参数。
      - result (string [string] object): 描述外部化的模块。可以接受其它标准化外部化模块格式,(string, [string],或 object)。
      - type (string): 可选的参数,用于指明模块的 external type(如果它没在 result 参数中被指明)。
   **/
  externals: [
    function ({ context, request }, callback) {
      if (/^yourregex$/.test(request)) {
        // 使用 request 路径,将一个 commonjs 模块外部化
        return callback(null, 'commonjs ' + request);
      }

      // 继续下一步且不外部化引用
      callback();
    },
  ],

  // RegExp 匹配给定正则表达式的每个依赖,都将从输出 bundle 中排除。
  externals: /^(jquery|\$)$/i,

  // 5.混用语法
  externals: [
    {
      // 字符串
      react: 'react',
      // 对象
      lodash: {
        commonjs: 'lodash',
        amd: 'lodash',
        root: '_', // indicates global variable
      },
      // 字符串数组
      subtract: ['./math', 'subtract'],
    },
    // 函数
    function ({ context, request }, callback) {
      if (/^yourregex$/.test(request)) {
        return callback(null, 'commonjs ' + request);
      }
      callback();
    },
    // 正则表达式
    /^(jquery|\$)$/i,
  ],

};

补充

import()中的表达式

不能使用完全动态的 import 语句,例如 import(foo)。是因为 foo 可能是系统或项目中任何文件的任何路径。

import() 必须至少包含一些关于模块的路径信息。打包可以限定于一个特定的目录或文件集,以便于在使用动态表达式时 - 包括可能在 import() 调用中请求的每个模块。例如, import(./locale/${language}.json) 会把 .locale 目录中的每个 .json 文件打包到新的 chunk 中。在运行时,计算完变量 language 后,就可以使用像 english.jsongerman.json 的任何文件。

Magic Comments(魔法注释)

内联注释使 'import() 必须至少包含一些关于模块的路径信息' 这一特性得以实现。通过在 import 中添加注释,我们可以进行诸如给 chunk 命名或选择不同模式的操作。

// 单个目标
import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackExports: ["default", "named"] */
  'module'
);

// 多个可能的目标 
import(
  /* webpackInclude: /\.json$/ */
  /* webpackExclude: /\.noimport\.json$/ */
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  `./locale/${language}`
);
// webpackIgnore:设置为 true 时,禁用动态导入解析。 
// 将 webpackIgnore 设置为 true 则不进行代码分割。
import(/* webpackIgnore: true */ 'ignored-module.js');
  • webpackChunkName: 新 chunk 的名称。 从 webpack 2.6.0 开始,占位符 [index][request] 分别支持递增的数字或实际的解析文件名。 添加此注释后,将单独的给我们的 chunk 命名为 [my-chunk-name].js 而不是 [id].js。
  • webpackMode:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入。支持以下选项:
    • 'lazy' (默认值):为每个 import() 导入的模块生成一个可延迟加载(lazy-loadable)的 chunk。
    • 'lazy-once':生成一个可以满足所有 import() 调用的单个可延迟加载(lazy-loadable)的 chunk。此 chunk 将在第一次 import() 时调用时获取,随后的 import() 则使用相同的网络响应。注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径。
    • 'eager':不会生成额外的 chunk。所有的模块都被当前的 chunk 引入,并且没有额外的网络请求。但是仍会返回一个 resolved 状态的 Promise。与静态导入相比,在调用 import() 完成之前,该模块不会被执行。
    • 'weak':尝试加载模块,如果该模块函数已经以其他方式加载,(即另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍会返回 Promise, 但是只有在客户端上已经有该 chunk 时才会成功解析。如果该模块不可用,则返回 rejected 状态的 Promise,且网络请求永远都不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况下触发,这对于通用渲染(SSR)是非常有用的。
  • webpackPrefetch:告诉浏览器将来可能需要该资源来进行某些导航跳转.
  • webpackPreload:预加载,告诉浏览器在当前导航期间可能需要该资源。4.6+才支持,如果是老版本 webpack,可以使用preload-webpack-plugin这种插件来实现预加载。
  • webpackInclude:在导入解析(import resolution)过程中,用于匹配的正则表达式。只有匹配到的模块才会被打包。
  • webpackExclude:在导入解析(import resolution)过程中,用于匹配的正则表达式。所有匹配到的模块都不会被打包。