再学webpack

1 优化 webpack 打包体积的思路

优化 webpack 打包体积的思路包括:

  1. 提取第三方库或通过引用外部文件的方式引入第三方库:将第三方库单独打包,并通过 CDN 引入,减少打包体积。
  2. 使用代码压缩插件:例如 UglifyJsPlugin,可以压缩 JavaScript 代码,减小文件体积。
  3. 启用服务器端的 Gzip 压缩:通过服务器端配置 Gzip 压缩,减少传输体积。
  4. 按需加载资源文件:使用 require.ensure 或动态导入(import())的方式按需加载资源文件,避免一次性加载所有资源,优化加载速度和体积。
  5. 优化 devtool 中的 source-map:选择合适的 devtool 配置,确保在开发阶段能够提供足够的错误追踪信息,但不会增加过多的打包体积。
  6. 剥离 CSS 文件:将 CSS 文件单独打包,通过 <link> 标签引入,利用浏览器的并行加载能力。
  7. 去除不必要的插件:检查 webpack 配置中的插件,移除不必要的插件或根据环境区分开发环境和生产环境的配置,避免将开发环境的调试工具打包到生产环境中。

除了上述优化思路,还可以考虑以下几点:

  • 使用 Tree Shaking:通过配置 webpack,将未使用的代码在打包过程中消除,减少打包体积。
  • 使用模块化引入:合理使用 ES6 模块化语法或其他模块化方案,按需引入模块,避免不必要的全局引入。
  • 按需加载第三方库:对于较大的第三方库,可以考虑按需加载,而不是一次性全部引入。
  • 优化图片资源:压缩图片,使用适当的图片格式,尽量减小图片体积。
  • 优化字体文件:如果使用了大量的字体文件,可以考虑只引入需要的字体文件,避免全部引入。
  • 使用缓存:通过配置合适的缓存策略,利用浏览器缓存机制,减少重复加载资源。

综合以上优化思路,可以有效减小 webpack 打包生成的文件体积,提升应用性能和加载速度。需要根据具体项目情况和需求,选择合适的优化策略和配置。

2 优化 webpack 打包效率的方法

  1. 使用增量构建和热更新:在开发环境下,使用增量构建和热更新功能,只重新构建修改过的模块,减少整体构建时间。
  2. 避免无意义的工作:在开发环境中,避免执行无意义的工作,如提取 CSS、计算文件 hash 等,以减少构建时间。
  3. 配置合适的 devtool:选择适当的 devtool 配置,提供足够的调试信息,但不会对构建性能产生太大影响。
  4. 选择合适的 loader:根据需要加载的资源类型选择高效的 loader,避免不必要的解析和处理过程。
  5. 启用 loader 缓存:对于耗时较长的 loader,如 babel-loader,可以启用缓存功能,避免重复处理同一文件。
  6. 采用引入方式引入第三方库:对于第三方库,可以通过直接引入的方式(如 CDN 引入)来减少打包时间。
  7. 提取公共代码:通过配置 webpack 的 SplitChunks 插件,提取公共代码,避免重复打包相同的代码,提高打包效率。
  8. 优化构建时的搜索路径:指定需要构建的目录和不需要构建的目录,减少搜索范围,加快构建速度。
  9. 模块化引入需要的部分:使用按需引入的方式,只引入需要的模块或组件,避免加载不必要的代码,提高构建效率。

通过以上优化措施,可以有效提升 webpack 的打包效率,减少开发和构建时间,提升开发效率和用户体验。根据具体项目需求和场景,选择适合的优化方法进行配置和调整。

3 编写Loader

编写一个名为 reverse-txt-loader 的 Loader,实现对文本内容进行反转处理的功能。

// reverse-txt-loader.js

module.exports = function (source) {
  // 对源代码进行处理,这里是将字符串反转
  const reversedSource = source.split('').reverse().join('');

  // 返回处理后的 JavaScript 代码作为模块输出
  return `module.exports = '${reversedSource}';`;
};    

上述代码定义了一个函数,该函数接收一个参数 source,即原始的文本内容。在函数内部,我们将源代码进行反转处理,并将处理后的结果拼接成一个字符串,再通过 module.exports 输出为一个 JavaScript 模块。

要使用这个 Loader,需要在 webpack 配置中指定该 Loader 的路径:

// webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /.txt$/,
        use: [
          {
            loader: './path/reverse-txt-loader'
          }
        ]
      }
    ]
  }
  // ...
};  

上述配置将该 Loader 应用于所有以 .txt 结尾的文件。在构建过程中,当遇到需要加载的 .txt 文件时,会调用 reverse-txt-loader 对文件内容进行反转处理,并将处理后的结果作为模块的输出。

请注意,在实际使用中,需要根据实际路径修改 loader 配置的路径,并将该 Loader 安装在项目中。

4 编写plugin

编写一个自定义的 Webpack 插件需要创建一个 JavaScript 类,并在类中实现指定的生命周期方法。下面是一个简单的示例,展示如何编写一个自定义的 Webpack 插件:

class MyPlugin {
  constructor(options) {
    // 在构造函数中可以接收插件的配置参数
    this.options = options;
  }

  // Webpack 在安装插件时会自动调用 apply 方法,并将 compiler 对象传递进来
  apply(compiler) {
    // 在适当的生命周期钩子中挂载插件的功能

    // 示例:在 emit 生命周期钩子中添加自定义的功能
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation 对象包含了当前构建过程的各种信息
      // 可以在这里执行一些自定义的操作

      // 示例:向输出的文件中添加自定义的注释
      const comment = this.options.comment || 'Custom comment';
      for (const asset in compilation.assets) {
        if (compilation.assets.hasOwnProperty(asset)) {
          compilation.assets[asset].source = () => {
            return `/* ${comment} */\n` + compilation.assets[asset].source();
          };
        }
      }
    });
  }
}    

以上是一个简单的插件示例,它在构建过程中的 emit 生命周期钩子中向输出的文件添加了自定义的注释。你可以根据实际需求在其他生命周期钩子中实现不同的功能。

要使用该插件,在 webpack 的配置文件中进行如下配置:

const MyPlugin = require('./path/to/MyPlugin');

module.exports = {
  // ...
  plugins: [
    new MyPlugin({
      comment: 'Custom comment',
    }),
  ],
};

这样,当你运行 webpack 构建时,该插件就会被应用,并执行指定的功能。

需要注意的是,Webpack 的插件机制非常灵活,可以根据实际需求编写各种各样的插件。插件可以监听多个生命周期钩子,并在每个生命周期钩子中实现自定义的功能。详细的插件开发文档可以参考 Webpack 官方文档。

5 说一下webpack的一些plugin,怎么使用webpack对项目进行优化

Webpack 提供了许多插件(Plugins)来帮助优化项目构建和性能。下面列举一些常用的插件以及它们的作用:

构建优化插件:

  • ContextReplacementPlugin:用于限制某些模块的上下文,可以减少编译体积。
  • IgnorePlugin:用于忽略特定的模块,减少打包体积。
  • babel-plugin-import:用于按需加载和使用模块,减少打包体积。
  • babel-plugin-transform-runtime:将代码中的公共部分提取到一个单独的模块中,减少打包体积。
  • happypack、thread-loader:实现并行编译,加快构建速度。
  • uglifyjs-webpack-plugin:通过并行压缩和缓存来加快代码压缩的速度。

性能优化插件:

  • Tree-shaking:通过静态分析代码,去除未使用的代码,减少打包体积。
  • Scope Hoisting:将模块之间的关系进行静态分析,减少打包后的模块数量,提升代码执行速度。
  • webpack-md5-plugin:根据文件内容生成 hash,实现缓存的更新机制。
  • splitChunksPlugin:根据配置将代码拆分成多个块,实现按需加载和并行加载的效果。
  • import()、require.ensure:动态导入模块,实现按需加载,提升页面加载速度。

除了使用这些插件,还可以通过配置 webpack 的其他参数来进一步优化项目,例如:

  • 配置 devtool:选择合适的 Source Map 类型,既满足调试需求又不影响构建速度。
  • 配置 output:使用 chunkhash 或 contenthash 生成文件名,实现长期缓存。
  • 使用 cache-loader、hard-source-webpack-plugin、uglifyjs-webpack-plugin 等插件开启缓存,加速再次构建。
  • 使用 DllWebpackPlugin 和 DllReferencePlugin 预编译公共模块,减少重复构建时间。

综合使用这些插件和优化策略,可以显著提升 webpack 项目的构建效率和性能。但是需要根据具体的项目需求和场景选择合适的插件和优化方法。

6 webpack Plugin 和 Loader 的区别

  • Loader 用于对模块源码进行转换,将非 JavaScript 模块转换为 JavaScript 模块,或对模块进行预处理。它描述了 webpack 如何处理不同类型的文件,比如将 Sass 文件转换为 CSS 文件,或将 ES6 代码转换为 ES5 代码。Loader 是针对单个文件的转换操作,通过配置 rules 来匹配文件并指定相应的 Loader
  • Plugin 用于扩展 webpack 的功能,解决 Loader 无法解决的问题。Plugin 可以监听 webpack 构建过程中的事件,并在特定的时机执行相应的操作。它可以在打包优化、资源管理、环境变量注入等方面提供额外的功能。Plugin 的功能范围更广泛,可以修改 webpack 的内部行为,从而实现更复杂的构建需求。

总的来说,Loader 是用于处理模块源码的转换工具,而 Plugin 则是用于扩展 webpack 的功能,通过监听 webpack 构建过程中的事件来执行相应的操作。它们各自的作用和功能不同,但都可以用于优化和定制 webpack 的构建过程。在配置 webpack 时,我们可以通过配置 Loader 和 Plugin 来满足不同的需求,并实现对模块的转换和构建过程的定制化

7 tree shaking 的原理是什么

Tree shaking 的原理主要是基于静态分析的方式来实现无用代码的消除,从而减小最终打包生成的文件体积。它的工作原理可以简要概括如下:

  1. 采用 ES6 Module 语法:Tree shaking 只对 ES6 Module 语法进行静态分析和优化。ES6 Module 的特点是可以进行静态分析,这意味着在编译阶段就能够确定模块之间的依赖关系。
  2. 静态分析模块依赖:在编译过程中,通过静态分析可以确定每个模块的依赖关系,以及模块中导出的函数、变量等信息。
  3. 标记未被引用的代码:在静态分析的过程中,会标记出那些未被其他模块引用的函数、变量和代码块。
  4. 消除未被引用的代码:在构建过程中,根据静态分析得到的标记信息,可以对未被引用的代码进行消除。这样,在最终生成的打包文件中,未被引用的代码将不会包含在内。

总结来说,Tree shaking 的核心思想是通过静态分析模块依赖关系,并标记和消除未被引用的代码。这样可以大大减小打包后的文件体积,提升应用的性能和加载速度。需要注意的是,Tree shaking 只对 ES6 Module 语法起作用,而对于 CommonJS 等其他模块系统则无法进行静态分析和优化。

8 common.js 和 es6 中模块引入的区别

CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export和 import,会经过 Babel 转换为 CommonJS 规范

CommonJS 和 ES6 Module 在模块引入的方式和特性上有一些区别,主要包括以下几个方面:

  1. 输出方式:CommonJS 输出的是一个值的拷贝,而 ES6 Module 输出的是值的引用。在 CommonJS 中,模块导出的值是被复制的,即使导出模块后修改了模块内部的值,也不会影响导入模块的值。而在 ES6 Module 中,模块导出的值是引用关系,如果导出模块后修改了模块内部的值,会影响到导入模块的值
  2. 加载时机:CommonJS 模块是运行时加载,也就是在代码执行到导入模块的位置时才会加载模块并执行。而 ES6 Module 是编译时输出接口,也就是在代码编译阶段就会确定模块的依赖关系,并在运行前静态地解析模块的导入和导出
  3. 导出方式:CommonJS 采用的是 module.exports 导出,可以导出任意类型的值。ES6 Module 采用的是 export 导出,只能导出具名的变量、函数、类等,而不能直接导出任意值
  4. 导入方式:CommonJS 使用 require() 来导入模块,可以使用动态语法,允许在条件语句中使用。ES6 Module 使用 import 来导入模块,它是静态语法,只能写在模块的顶层,不能写在条件语句中
  5. this 指向:CommonJS 模块中的 this 指向当前模块的 exports 对象,而不是全局对象。ES6 Module 中的 this 默认是 undefined,在模块中直接使用 this 会报错

总的来说,CommonJS 主要用于服务器端的模块化开发,运行时加载,更适合动态加载模块,而 ES6 Module 是在语言层面上实现的模块化方案,静态编译,更适合在构建时进行模块依赖的静态分析和优化。在前端开发中,通常使用打包工具(如 webpack)将 ES6 Module 转换为 CommonJS 或其他模块规范,以实现在浏览器环境中的兼容性。

9 babel原理

Babel 是一个 JavaScript 编译器。他把最新版的 javascript 编译成当下可以执行的版本,简言之,利用 babel 就可以让我们在当前的项目中随意的使用这些新最新的 es6,甚至 es7 的语法

ES6、7代码输入 -> babylon进行解析 -> 得到AST(抽象语法树)-> plugin用babel-traverse对AST树进行遍历转译 ->得到新的AST树->用babel-generator通过AST树生成ES5代码

它的工作流程包括解析(parse)、转换(transform)和生成(generate)三个主要步骤

  1. 解析(parse) :Babel 使用解析器(如 Babylon)将输入的 JavaScript 代码解析成抽象语法树(AST)。解析器将代码分析成语法结构,并生成对应的 AST,表示代码的抽象语法结构。这个阶段包括词法分析和语法分析。词法分析将源代码转换为一个个标记(tokens)的流,而语法分析则将这个标记流转换为 AST 的形式。
  2. 转换(transform) :在转换阶段,Babel 使用插件(plugins)对 AST 进行遍历和转换。插件可以对 AST 进行增删改查的操作,可以根据需求对语法进行转换、代码优化等。Babel 的插件系统非常灵活,可以根据需要自定义插件或使用现有插件来进行代码转换。
  3. 生成(generate) :在生成阶段,Babel 使用生成器(如 babel-generator)将经过转换的 AST 转换回字符串形式的 JavaScript 代码。生成器会深度优先遍历 AST,并根据 AST 的节点类型生成对应的代码字符串,最终将代码字符串输出。

通过以上三个步骤,Babel 实现了将最新版本的 JavaScript 代码转换为向后兼容的代码,使得开发者可以在当前环境中使用较新的 JavaScript 特性和语法。同时,Babel 还提供了一些常用的插件和预设(presets),以便开发者快速配置和使用常见的转换规则,如转换 ES6、ES7 语法、处理模块化、转换 JSX 等。

总的来说,Babel 的原理是通过解析、转换和生成的过程,将新版本的 JavaScript 代码转换为兼容旧环境的代码,使开发者能够在当前环境中使用较新的 JavaScript 特性和语法。

1 介绍一下 webpack 的构建流程

核心概念

  • entry:入口。webpack是基于模块的,使用webpack首先需要指定模块解析入口(entry),webpack从入口开始根据模块间依赖关系递归解析和处理所有资源文件。
  • output:输出。源代码经过webpack处理之后的最终产物。
  • loader:模块转换器。本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
  • plugin:扩展插件。基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
  • module:模块。除了js范畴内的es module、commonJs、AMD等,css @import、url(...)、图片、字体等在webpack中都被视为模块。

解释几个 webpack 中的术语

  • module:指在模块化编程中我们把应用程序分割成的独立功能的代码模块
  • chunk:指模块间按照引用关系组合成的代码块,一个 chunk 中可以包含多个 module
  • chunk group:指通过配置入口点(entry point)区分的块组,一个 chunk group 中可包含一到多个 chunk
  • bundling:webpack 打包的过程
  • asset/bundle:打包产物

webpack 的打包思想可以简化为 3 点:

  • 一切源代码文件均可通过各种 Loader 转换为 JS 模块 (module),模块之间可以互相引用。
  • webpack 通过入口点(entry point)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle) 文件。
  • 每一个入口点都是一个块组(chunk group),在不考虑分包的情况下,一个 chunk group 中只有一个 chunk,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件(asset/bundle)

打包流程

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。
  2. 开始编译:从上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。
  5. 完成模块编译:经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

简版

  • Webpack CLI 启动打包流程;
  • 载入 Webpack 核心模块,创建 Compiler 对象;
  • 使用 Compiler 对象开始编译整个项目;
  • 从入口文件开始,解析模块依赖,形成依赖关系树;
  • 递归依赖树,将每个模块交给对应的 Loader 处理;
  • 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到相关事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

构建流程核心概念:

  • Tapable:一个基于发布订阅的事件流工具类,Compiler 和 Compilation 对象都继承于 Tapable
  • Compiler:compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。在编译初始化阶段被创建的全局单例,包含完整配置信息、loaders、plugins以及各种工具方法
  • Compilation:代表一次 webpack 构建和生成编译资源的的过程,在watch模式下每一次文件变更触发的重新编译都会生成新的 Compilation 对象,包含了当前编译的模块 module, 编译生成的资源,变化的文件, 依赖的状态等
  • 而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

最终Webpack打包出来的bundle文件是一个IIFE的执行函数。

// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
    var __webpack_modules__ = ({
        'file-A-path': ((modules) => { // ... })
        'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
    })
    
    // The module cache
    var __webpack_module_cache__ = {};
    
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        var cachedModule = __webpack_module_cache__[moduleId];
        if (cachedModule !== undefined) {
                return cachedModule.exports;
        }
        // Create a new module (and put it into the cache)
        var module = __webpack_module_cache__[moduleId] = {
                // no module.id needed
                // no module.loaded needed
                exports: {}
        };

        // Execute the module function
        __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

        // Return the exports of the module
        return module.exports;
    }
    
    // startup
    // Load entry module and return exports
    // This entry module can't be inlined because the eval devtool is used.
    var __webpack_exports__ = __webpack_require__("./src/index.js");
})

    

webpack详细工作流程

2 介绍 Loader

常用 Loader:

  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;
  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;
  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;
  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;
  • style-loader: 将 css 代码以<style>标签的形式插入到 html 中;
  • css-loader: 分析@import和url(),引用 css 文件与对应的资源;
  • postcss-loader: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;
  • less-loader / sass-loader: css预处理器,在 css 中新增了许多语法,提高了开发效率;

编写原则:

  • 单一原则: 每个 Loader 只做一件事;
  • 链式调用: Webpack 会按顺序链式调用每个 Loader;
  • 统一原则: 遵循 Webpack制定的设计规则和结构,输入与输出均为字符串,各个 Loader 完全独立,即插即用;

3 介绍 plugin

插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。

一个最简单的 plugin 是这样的:

class Plugin{
  	// 注册插件时,会调用 apply 方法
  	// apply 方法接收 compiler 对象
  	// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
  	apply(compiler){
  		// compilation 是监听每次编译循环
  		// 每次文件变化,都会生成新的 compilation 对象并触发该事件
    	compiler.plugin('compilation',function(compilation) {})
  	}
}

    

注册插件:

// webpack.config.js
module.export = {
	plugins:[
		new Plugin(options),
	]
}

事件流机制:

Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。

  • 通过链式调用,按顺序串起一个个 Loader;
  • 通过事件流机制,让 Plugin 可以插入到整个生产过程中的每个步骤中;

Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:

const { SyncHook } = require("tapable")

const hook = new SyncHook(['arg'])

// 订阅
hook.tap('event', (arg) => {
	// 'event-hook'
	console.log(arg)
})

// 广播
hook.call('event-hook')

Webpack 中两个最重要的类 Compiler 与 Compilation 便是继承于 Tapable,也拥有这样的事件流机制。

  • Compiler: 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;

  • Compilation: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;

  • 区别:

    • Compiler 全局唯一,且从启动生存到结束;
    • Compilation对应每次编译,每轮编译循环均会重新创建;
  • 常用 Plugin:

    • UglifyJsPlugin: 压缩、混淆代码;
    • CommonsChunkPlugin: 代码分割;
    • ProvidePlugin: 自动加载模块;
    • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;
    • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;
    • optimize-css-assets-webpack-plugin: CSS 代码去重;
    • webpack-bundle-analyzer: 代码分析;
    • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;
    • happypack: 使用多进程,加速代码构建;
    • EnvironmentPlugin: 定义环境变量;
  • 调用插件 apply 函数传入 compiler 对象

  • 通过 compiler 对象监听事件

loader和plugin有什么区别?

webapck默认只能打包JS和JOSN模块,要打包其它模块,需要借助loader,loader就可以让模块中的内容转化成webpack或其它laoder可以识别的内容。

  • loader就是模块转换化,或叫加载器。不同的文件,需要不同的loader来处理。
  • plugin是插件,可以参与到整个webpack打包的流程中,不同的插件,在合适的时机,可以做不同的事件。

webpack中都有哪些插件,这些插件有什么作用?

  • html-webpack-plugin 自动创建一个HTML文件,并把打包好的JS插入到HTML文件中
  • clean-webpack-plugin 在每一次打包之前,删除整个输出文件夹下所有的内容
  • mini-css-extrcat-plugin 抽离CSS代码,放到一个单独的文件中
  • optimize-css-assets-plugin 压缩css

4 webpack 热更新实现原理

HMR 的基本流程图

  • 当修改了一个或多个文件;
  • 文件系统接收更改并通知 webpack;
  • webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
  • HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
  • HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新

5 webpack 层面如何做性能优化

优化前的准备工作

  • 准备基于时间的分析工具:我们需要一类插件,来帮助我们统计项目构建过程中在编译阶段的耗时情况。speed-measure-webpack-plugin 分析插件加载的时间
  • 使用 webpack-bundle-analyzer 分析产物内容

代码优化:

无用代码消除,是许多编程语言都具有的优化手段,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码;

例如我们的 UglifyJs,它就会帮我们在生产环境中删除不可能被执行的代码,例如:

var fn = function() {
	return 1;
	// 下面代码便属于 不可能执行的代码;
	// 通过 UglifyJs (Webpack4+ 已内置) 便会进行 DCE;
	var a = 1;
	return a;
}

摇树优化 (Tree-shaking),这是一种形象比喻。我们把打包后的代码比喻成一棵树,这里其实表示的就是,通过工具 “摇” 我们打包后的 js 代码,将没有使用到的无用代码 “摇” 下来 (删除)。即 消除那些被 引用了但未被使用 的模块代码。

  • 原理: 由于是在编译时优化,因此最基本的前提就是语法的静态分析,ES6的模块机制 提供了这种可能性。不需要运行时,便可进行代码字面上的静态分析,确定相应的依赖关系。

  • 问题: 具有 副作用 的函数无法被 tree-shaking

    • 在引用一些第三方库,需要去观察其引入的代码量是不是符合预期;
    • 尽量写纯函数,减少函数的副作用;
    • 可使用 webpack-deep-scope-plugin,可以进行作用域分析,减少此类情况的发生,但仍需要注意;

code-spliting: 代码分割技术,将代码分割成多份进行 懒加载 或 异步加载,避免打包成一份后导致体积过大,影响页面的首屏加载;

  • Webpack 中使用 SplitChunksPlugin 进行拆分;

  • 按 页面 拆分: 不同页面打包成不同的文件;

  • 按 功能 拆分:

    • 将类似于播放器,计算库等大模块进行拆分后再懒加载引入;
    • 提取复用的业务代码,减少冗余代码;
  • 按 文件修改频率 拆分: 将第三方库等不常修改的代码单独打包,而且不改变其文件 hash 值,能最大化运用浏览器的缓存;

scope hoisting: 作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;

编译性能优化:

  • 升级至 最新 版本的 webpack,能有效提升编译性能;

  • 使用 dev-server / 模块热替换 (HMR) 提升开发体验;

    • 监听文件变动 忽略 node_modules 目录能有效提高监听时的编译效率;
  • 缩小编译范围

    • modules: 指定模块路径,减少递归搜索;
    • mainFields: 指定入口文件描述字段,减少搜索;
    • noParse: 避免对非模块化文件的加载;
    • includes/exclude: 指定搜索范围/排除不必要的搜索范围;
    • alias: 缓存目录,避免重复寻址;
  • babel-loader

    • 忽略node_moudles,避免编译第三方库中已经被编译过的代码
    • 使用cacheDirectory,可以缓存编译结果,避免多次重复编译
  • 多进程并发

    • webpack-parallel-uglify-plugin: 可多进程并发压缩 js 文件,提高压缩速度;
    • HappyPack: 多进程并发文件的 Loader 解析;
  • 第三方库模块缓存:

    • DLLPlugin 和 DLLReferencePlugin 可以提前进行打包并缓存,避免每次都重新编译;
  • 使用分析

    • Webpack Analyse / webpack-bundle-analyzer 对打包后的文件进行分析,寻找可优化的地方
    • 配置profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的地方
  • source-map:

    • 开发: cheap-module-eval-source-map
    • 生产: hidden-source-map;

优化webpack打包速度

  • 减少文件搜索范围

    • 比如通过别名
    • loader 的 test,include & exclude
  • Webpack4 默认压缩并行

  • Happypack 并发调用

  • babel 也可以缓存编译

  • Resolve 在构建时指定查找模块文件的规则

  • 使用DllPlugin,不用每次都重新构建

  • externals 和 DllPlugin 解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于

    • 在 Webpack 的配置方面,externals 更简单,而 DllPlugin 需要独立的配置文件。
    • DllPlugin 包含了依赖包的独立构建流程,而 externals 配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包
    • externals 配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等
    • 在引用依赖包的子模块时,DllPlugin 无须更改,而 externals 则会将子模块打入项目包中

优化打包体积

  • 提取第三方库或通过引用外部文件的方式引入第三方库

  • 代码压缩插件UglifyJsPlugin

  • 服务器启用gzip压缩

  • 按需加载资源文件 require.ensure

  • 优化devtool中的source-map

  • 剥离css文件,单独打包

  • 去除不必要插件,通常就是开发环境与生产环境用同一套配置文件导致

  • Tree Shaking 在构建打包过程中,移除那些引入但未被使用的无效代码

  • 开启 scope hosting

    • 体积更小
    • 创建函数作用域更小
    • 代码可读性更好

6 介绍一下 Tree Shaking

对tree-shaking的了解

作用:

它表示在打包的时候会去除一些无用的代码

原理:

  • ES6的模块引入是静态分析的,所以在编译时能正确判断到底加载了哪些模块
  • 分析程序流,判断哪些变量未被使用、引用,进而删除此代码

特点:

  • 在生产模式下它是默认开启的,但是由于经过babel编译全部模块被封装成IIFE,它存在副作用无法被tree-shaking掉
  • 可以在package.json中配置sideEffects来指定哪些文件是有副作用的。它有两种值,一个是布尔类型,如果是false则表示所有文件都没有副作用;如果是一个数组的话,数组里的文件路径表示改文件有副作用
  • rollup和webpack中对tree-shaking的层度不同,例如对babel转译后的class,如果babel的转译是宽松模式下的话(也就是loose为true),webpack依旧会认为它有副作用不会tree-shaking掉,而rollup会。这是因为rollup有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。

原理

  • ES6 Module 引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
  • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

依赖于import/export

通过导入所有的包后再进行条件获取。如下:

import foo from "foo";
import bar from "bar";

if(condition) {
    // foo.xxxx
} else {
    // bar.xxx
}

    

ES6的import语法完美可以使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码

CommonJS的动态特性模块意味着tree shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。这也意味着下面的导入是不可行的:

// 不可行,ES6 的import是完全静态的
if(condition) {
    myDynamicModule = require("foo");
} else {
    myDynamicModule = require("bar");
}

    

7 介绍一下 webpack scope hosting

作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;

8 Webpack Proxy工作原理?为什么能解决跨域

1. 是什么

webpack proxy,即webpack提供的代理服务

基本行为就是接收客户端发送的请求后转发给其他服务器

其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)

想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server

2. webpack-dev-server

webpack-dev-server是 webpack 官方推出的一款开发工具,将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起

目的是为了提高开发者日常的开发效率,「只适用在开发阶段」

关于配置方面,在webpack配置对象属性中通过devServer属性提供,如下:

// ./webpack.config.js
const path = require('path')

module.exports = {
    // ...
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000,
        proxy: {
            '/api': {
                target: 'https://api.github.com'
            }
        }
        // ...
    }
}

devServetr里面proxy则是关于代理的配置,该属性为对象的形式,对象中每一个属性就是一个代理的规则匹配

属性的名称是需要被代理的请求路径前缀,一般为了辨别都会设置前缀为/api,值为对应的代理匹配规则,对应如下:

  • target:表示的是代理到的目标地址
  • pathRewrite:默认情况下,我们的 /api-hy 也会被写入到URL中,如果希望删除,可以使用pathRewrite
  • secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false
  • changeOrigin:它表示是否更新代理后请求的 headers 中host地址

2. 工作原理

proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器

举个例子:

在开发阶段,本地地址为http://localhost:3000,该浏览器发送一个前缀带有/api标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();

app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

    

3. 跨域

在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上

所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题

通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者

当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地

在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据

注意:「服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制」

9 介绍一下 babel原理

babel 的编译过程分为三个阶段:parsing、transforming、generating,以 ES6 编译为 ES5 作为例子:

  1. ES6 代码输入;
  2. babylon 进行解析得到 AST;
  3. plugin 用 babel-traverse 对 AST树进行遍历编译,得到新的 AST树;
  4. 用 babel-generator 通过 AST树生成 ES5 代码。

Babel原理及其使用(opens new window)

10 介绍一下Rollup

Rollup 是一款 ES Modules 打包器。它也可以将项目中散落的细小模块打包为整块代码,从而使得这些划分的模块可以更好地运行在浏览器环境或者 Node.js 环境。

Rollup优势:

  • 输出结果更加扁平,执行效率更高;
  • 自动移除未引用代码;
  • 打包结果依然完全可读。

缺点

  • 加载非 ESM 的第三方模块比较复杂;
  • 因为模块最终都被打包到全局中,所以无法实现 HMR;
  • 浏览器环境中,代码拆分功能必须使用 Require.js 这样的 AMD 库
  • 我们发现如果我们开发的是一个应用程序,需要大量引用第三方模块,同时还需要 HMR 提升开发体验,而且应用过大就必须要分包。那这些需求 Rollup 都无法满足。
  • 如果我们是开发一个 JavaScript 框架或者库,那这些优点就特别有必要,而缺点呢几乎也都可以忽略,所以在很多像 React 或者 Vue 之类的框架中都是使用的 Rollup 作为模块打包器,而并非 Webpack

总结一下:Webpack 大而全,Rollup 小而美。

在对它们的选择上,我的基本原则是:应用开发使用 Webpack,类库或者框架开发使用 Rollup。

不过这并不是绝对的标准,只是经验法则。因为 Rollup 也可用于构建绝大多数应用程序,而 Webpack 同样也可以构建类库或者框架。

hash、chunkhash、contenthash区别

  • 如果是hash的话,是和整个项目有关的,有一处文件发生更改则所有文件的hash值都会发生改变且它们共用一个hash值;
  • 如果是chunkhash的话,只和entry的每个入口文件有关,也就是同一个chunk下的文件有所改动该chunk下的文件的hash值就会发生改变
  • 如果是contenthash的话,和每个生成的文件有关,只有当要构建的文件内容发生改变时才会给该文件生成新的hash值,并不会影响其它文件。

11 webpack常用插件总结

1. 功能类

1.1 html-webpack-plugin

自动生成html,基本用法:

new HtmlWebpackPlugin({
  filename: 'index.html', // 生成文件名
  template: path.join(process.cwd(), './index.html') // 模班文件
})

1.2 copy-webpack-plugin

拷贝资源插件

new CopyWebpackPlugin([
  {
    from: path.join(process.cwd(), './vendor/'),
    to: path.join(process.cwd(), './dist/'),
    ignore: ['*.json']
  }
])

1.3 webpack-manifest-plugin && assets-webpack-plugin

俩个插件效果一致,都是生成编译结果的资源单,只是资源单的数据结构不一致而已

webpack-manifest-plugin 基本用法

module.exports = {
  plugins: [
    new ManifestPlugin()
  ]
}

assets-webpack-plugin 基本用法

module.exports = {
  plugins: [
    new AssetsPlugin()
  ]
}

1.4 clean-webpack-plugin

在编译之前清理指定目录指定内容

// 清理目录
const pathsToClean = [
  'dist',
  'build'
]
 
// 清理参数
const cleanOptions = {
  exclude:  ['shared.js'], // 跳过文件
}
module.exports = {
  // ...
  plugins: [
    new CleanWebpackPlugin(pathsToClean, cleanOptions)
  ]
}

1.5 compression-webpack-plugin

提供带 Content-Encoding 编码的压缩版的资源

module.exports = {
  plugins: [
    new CompressionPlugin()
  ]
}

1.6 progress-bar-webpack-plugin

编译进度条插件

module.exports = {
  //...
  plugins: [
    new ProgressBarPlugin()
  ]
}

2. 代码相关类

2.1 webpack.ProvidePlugin

自动加载模块,如 $ 出现,就会自动加载模块;$ 默认为'jquery'的exports

new webpack.ProvidePlugin({
  $: 'jquery',
})

2.2 webpack.DefinePlugin

定义全局常量

new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: JSON.stringify(process.env.NODE_ENV)
  }
})

2.3 mini-css-extract-plugin && extract-text-webpack-plugin

提取css样式,对比

  • mini-css-extract-plugin 为webpack4及以上提供的plugin,支持css chunk
  • extract-text-webpack-plugin 只能在webpack3 及一下的版本使用,不支持css chunk

基本用法 extract-text-webpack-plugin

const ExtractTextPlugin = require("extract-text-webpack-plugin");
 
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin("styles.css"),
  ]
}

基本用法 mini-css-extract-plugin

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
    module: {
    rules: [
      {
        test: /.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '/'  // chunk publicPath
            }
          },
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css", // 主文件名
      chunkFilename: "[id].css"  // chunk文件名
    })
  ]
}

3. 编译结果优化类

3.1 wbepack.IgnorePlugin

忽略regExp匹配的模块

new webpack.IgnorePlugin(/^./locale$/, /moment$/)

3.2 uglifyjs-webpack-plugin

代码丑化,用于js压缩

module.exports = {
  //...
  optimization: {
    minimizer: [new UglifyJsPlugin({
      cache: true,   // 开启缓存
      parallel: true, // 开启多线程编译
      sourceMap: true,  // 是否sourceMap
      uglifyOptions: {  // 丑化参数
        comments: false,
        warnings: false,
        compress: {
          unused: true,
          dead_code: true,
          collapse_vars: true,
          reduce_vars: true
        },
        output: {
          comments: false
        }
      }
    }]
  }
};

3.3 optimize-css-assets-webpack-plugin

css压缩,主要使用 cssnano 压缩器 https://github.com/cssnano/cssnano

module.exports = {
  //...
  optimization: {
    minimizer: [new OptimizeCssAssetsPlugin({
      cssProcessor: require('cssnano'),   // css 压缩优化器
      cssProcessorOptions: { discardComments: { removeAll: true } } // 去除所有注释
    })]
  }
};

3.4 webpack-md5-hash

使你的chunk根据内容生成md5,用这个md5取代 webpack chunkhash。

var WebpackMd5Hash = require('webpack-md5-hash');
 
module.exports = {
  // ...
  output: {
    //...
    chunkFilename: "[chunkhash].[id].chunk.js"
  },
  plugins: [
    new WebpackMd5Hash()
  ]
};

3.5 SplitChunksPlugin

  • CommonChunkPlugin 的后世,用于chunk切割。

webpack 把 chunk 分为两种类型,一种是初始加载initial chunk,另外一种是异步加载 async chunk,如果不配置SplitChunksPlugin,webpack会在production的模式下自动开启,默认情况下,webpack会将 node_modules 下的所有模块定义为异步加载模块,并分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中,其中默认配置如下

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 异步加载chunk
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~', // 文件名中chunk分隔符
      name: true,
      cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/,  // 
          priority: -10
        },
        default: {
          minChunks: 2,  // 最小的共享chunk数
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
};

4. 编译优化类

4.1 DllPlugin && DllReferencePlugin && autodll-webpack-plugin

  • dllPlugin将模块预先编译,DllReferencePlugin 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块。
  • autodll-webpack-plugin 相当于 dllPlugin 和 DllReferencePlugin 的简化版,其实本质也是使用 dllPlugin && DllReferencePlugin,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块

dllPlugin 基本用法:

const output = {
  filename: '[name].js',
  library: '[name]_library',
  path: './vendor/'
}

module.exports = {
  entry: {
    vendor: ['react', 'react-dom']  // 我们需要事先编译的模块,用entry表示
  },
  output: output,
  plugins: [
    new webpack.DllPlugin({  // 使用dllPlugin
      path: path.join(output.path, `${output.filename}.json`),
      name: output.library // 全局变量名, 也就是 window 下 的 [output.library]
    })
  ]
}

DllReferencePlugin 基本用法:

const manifest = path.resolve(process.cwd(), 'vendor', 'vendor.js.json')

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require(manifest), // 引进dllPlugin编译的json文件
      name: 'vendor_library' // 全局变量名,与dllPlugin声明的一致
    }
  ]
}

autodll-webpack-plugin 基本用法:

module.exports = {
  plugins: [
    new AutoDllPlugin({
      inject: true, // 与 html-webpack-plugin 结合使用,注入html中
      filename: '[name].js',
      entry: {
        vendor: [
          'react',
          'react-dom'
        ]
      }
    })
  ]
}

4.2 happypack && thread-loader

多线程编译,加快编译速度,thread-loader不可以和 mini-css-extract-plugin 结合使用

happypack 基本用法

const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const happyLoaderId = 'happypack-for-react-babel-loader';

module.exports = {
  module: {
    rules: [{
      test: /.jsx?$/,
      loader: 'happypack/loader',
      query: {
        id: happyLoaderId
      },
      include: [path.resolve(process.cwd(), 'src')]
    }]
  },
  plugins: [new HappyPack({
    id: happyLoaderId,
    threadPool: happyThreadPool,
    loaders: ['babel-loader']
  })]
}

thread-loader 基本用法

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // your expensive loader (e.g babel-loader)
          "babel-loader"
        ]
      }
    ]
  }
}

4.3 hard-source-webpack-plugin && cache-loader

使用模块编译缓存,加快编译速度

hard-source-webpack-plugin 基本用法

module.exports = {
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}

cache-loader 基本用法

module.exports = {
  module: {
    rules: [
      {
        test: /.ext$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

5. 编译分析类

5.1 webpack-bundle-analyzer

编译模块分析插件

new BundleAnalyzerPlugin({
  analyzerMode: 'server',
  analyzerHost: '127.0.0.1',
  analyzerPort: 8889,
  reportFilename: 'report.html',
  defaultSizes: 'parsed',
  generateStatsFile: false,
  statsFilename: 'stats.json',
  statsOptions: null,
  logLevel: 'info'
}),

5.2 stats-webpack-plugin && PrefetchPlugin

stats-webpack-plugin 将构建的统计信息写入文件,该文件可在 http://webpack.github.io/analyse中上传进行编译分析,并根据分析结果,可使用 PrefetchPlugin 对部分模块进行预解析编译

stats-webpack-plugin 基本用法:

module.exports = {
  plugins: [
    new StatsPlugin('stats.json', {
      chunkModules: true,
      exclude: [/node_modules[\/]react/]
    })
  ]
};

PrefetchPlugin 基本用法:

module.exports = {
  plugins: [
    new webpack.PrefetchPlugin('/web/', 'app/modules/HeaderNav.jsx'),
    new webpack.PrefetchPlugin('/web/', 'app/pages/FrontPage.jsx')
];
}

5.3 speed-measure-webpack-plugin

统计编译过程中,各loader和plugin使用的时间

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
 
const smp = new SpeedMeasurePlugin();
 
const webpackConfig = {
  plugins: [
    new MyPlugin(),
    new MyOtherPlugin()
  ]
}
module.exports = smp.wrap(webpackConfig);

12 webpack热更新原理

  • 当修改了一个或多个文件;
  • 文件系统接收更改并通知 webpack;
  • webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
  • HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp
  • HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新

13 webpack原理简述

1.1 核心概念

JavaScript 的 模块打包工具 (module bundler)。通过分析模块之间的依赖,最终将所有模块打包成一份或者多份代码包 (bundler),供 HTML 直接引用。实质上,Webpack 仅仅提供了 打包功能 和一套 文件处理机制,然后通过生态中的各种 Loader 和 Plugin 对代码进行预编译和打包。因此 Webpack 具有高度的可拓展性,能更好的发挥社区生态的力量。

  • Entry: 入口文件,Webpack会从该文件开始进行分析与编译;
  • Output: 出口路径,打包后创建 bundler的文件路径以及文件名;
  • Module: 模块,在 Webpack 中任何文件都可以作为一个模块,会根据配置的不同的 Loader 进行加载和打包;
  • Chunk: 代码块,可以根据配置,将所有模块代码合并成一个或多个代码块,以便按需加载,提高性能;
  • Loader: 模块加载器,进行各种文件类型的加载与转换;
  • Plugin: 拓展插件,可以通过 Webpack 相应的事件钩子,介入到打包过程中的任意环节,从而对代码按需修改;

1.2 工作流程 (加载 - 编译 - 输出)

  1. 读取配置文件,按命令 初始化 配置参数,创建 Compiler 对象;
  2. 调用插件的 apply 方法 挂载插件 监听,然后从入口文件开始执行编译;
  3. 按文件类型,调用相应的 Loader 对模块进行 编译,并在合适的时机点触发对应的事件,调用 Plugin 执行,最后再根据模块 依赖查找 到所依赖的模块,递归执行第三步;
  4. 将编译后的所有代码包装成一个个代码块 (Chunk), 并按依赖和配置确定 输出内容。这个步骤,仍然可以通过 Plugin 进行文件的修改;
  5. 最后,根据 Output 把文件内容一一写入到指定的文件夹中,完成整个过程;

1.3 模块包装

(function(modules) {
	// 模拟 require 函数,从内存中加载模块;
	function __webpack_require__(moduleId) {
		// 缓存模块
		if (installedModules[moduleId]) {
			return installedModules[moduleId].exports;
		}
		
		var module = installedModules[moduleId] = {
			i: moduleId,
			l: false,
			exports: {}
		};
		
		// 执行代码;
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		
		// Flag: 标记是否加载完成;
		module.l = true;
		
		return module.exports;
	}
	
	// ...
	
	// 开始执行加载入口文件;
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })({
 	"./src/index.js": function (module, __webpack_exports__, __webpack_require__) {
		// 使用 eval 执行编译后的代码;
		// 继续递归引用模块内部依赖;
		// 实际情况并不是使用模板字符串,这里是为了代码的可读性;
		eval(`
			__webpack_require__.r(__webpack_exports__);
			//
			var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("test", ./src/test.js");
		`);
	},
	"./src/test.js": function (module, __webpack_exports__, __webpack_require__) {
		// ...
	},
 })

总结:

  • 模块机制: webpack自己实现了一套模拟模块的机制,将其包裹于业务代码的外部,从而提供了一套模块机制;
  • 文件编译: webpack规定了一套编译规则,通过 Loader 和 Plugin,以管道的形式对文件字符串进行处理;

1.4 webpack的打包原理

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

1.5 webpack的打包原理详细

相关问题

  • webpack 工作流程是怎样的
  • webpack 在不同阶段做了什么事情

webpack 是一种模块打包工具,可以将各类型的资源,例如图片、CSS、JS 等,转译组合为 JS 格式的 bundle 文件

webpack 构建的核心任务是完成内容转化和资源合并。主要包含以下 3 个阶段:

  1. 初始化阶段
  • 初始化参数:从配置文件、配置对象和 Shell 参数中读取并与默认参数进行合并,组合成最终使用的参数
  • 创建编译对象:用上一步得到的参数创建 Compiler 对象。
  • 初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
  1. 构建阶段
  • 开始编译:执行 Compiler 对象的 run 方法,创建 Compilation 对象。
  • 确认编译入口:进入 entryOption 阶段,读取配置的 Entries,递归遍历所有的入口文件,调用 Compilation.addEntry 将入口文件转换为 Dependency 对象。
  • 编译模块(make) : 调用 normalModule 中的 build 开启构建,从 entry 文件开始,调用 loader 对模块进行转译处理,然后调用 JS 解释器(acorn)将内容转化为 AST 对象,然后递归分析依赖,依次处理全部文件。
  • 完成模块编译:在上一步处理好所有模块后,得到模块编译产物和依赖关系图
  1. 生成阶段
  • 输出资源(seal) :根据入口和模块之间的依赖关系,组装成多个包含多个模块的 Chunk,再把每个 Chunk 转换成一个 Asset 加入到输出列表,这步是可以修改输出内容的最后机会。
  • 写入文件系统(emitAssets) :确定好输出内容后,根据配置的 output 将内容写入文件系统

知识点深入

1. webpack 初始化过程

从 webpack 项目 webpack.config.js 文件 webpack 方法出发,可以看到初始化过程如下:

  • 将命令行参数和用户的配置文件进行合并

  • 调用 getValidateSchema 对配置进行校验

  • 调用 createCompiler 创建 Compiler 对象

    • 将用户配置和默认配置进行合并处理
    • 实例化 Compiler
    • 实例化 NodeEnvironmentPlugin
    • 处理用户配置的 plugins,执行 plugin 的 apply 方法。
    • 触发 environment 和 afterEnvironment 上注册的事件。
    • 注册 webpack 内部插件。
    • 触发 initialize 事件
// lib/webpack.js 122 行 部分代码省略处理
const create = () => {
  if (!webpackOptionsSchemaCheck(options)) {
    // 校验参数
    getValidateSchema()(webpackOptionsSchema, options);
  }
  // 创建 compiler 对象
  compiler = createCompiler(webpackOptions);
};

// lib/webpack.js 57 行
const createCompiler = (rawOptions) => {
  // 统一合并处理参数
  const options = getNormalizedWebpackOptions(rawOptions);
  applyWebpackOptionsBaseDefaults(options);
  // 实例化 compiler
  const compiler = new Compiler(options.context);
  // 把 options 挂载到对象上
  compiler.options = options;
  // NodeEnvironmentPlugin 是对 fs 模块的封装,用来处理文件输入输出等
  new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging,
  }).apply(compiler);
  // 注册用户配置插件
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  applyWebpackOptionsDefaults(options);
  // 触发 environment 和 afterEnvironment 上注册的事件
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  // 注册 webpack 内置插件
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
};

2. webpack 构建阶段做了什么

在 webpack 函数执行完之后,就到主要的构建阶段,首先执行 compiler.run(),然后触发一系列钩子函数,执行 compiler.compile()

  • 在实例化 compiler 之后,执行 compiler.run()

  • 执行 newCompilation 函数,调用 createCompilation 初始化 Compilation 对象

  • 执行 _addEntryItem 将入口文件存入 this.entries(map 对象),遍历 this.entries 对象构建 chunk。

  • 执行 handleModuleCreation,开始创建模块实例。

  • 执行 moduleFactory.create 创建模块

    • 执行 factory.hooks.factorize.call 钩子,然后会调用 ExternalModuleFactoryPlugin 中注册的钩子,用于配置外部文件的模块加载方式
    • 使用 enhanced-resolve 解析模块和 loader 的真实绝对路径
    • 执行 new NormalModule() 创建 module 实例
  • 执行 addModule,存储 module

  • 执行 buildModule,添加模块到模块队列 buildQueue,开始构建模块, 这里会调用 normalModule 中的 build 开启构建

    • 创建 loader 上下文。
    • 执行 runLoaders,通过 enhanced-resolve 解析得到的模块和 loader 的路径获取函数,执行 loader。
    • 生成模块的 hash
  • 所有依赖都解析完毕后,构建阶段结束

  // 构建过程涉及流程比较复杂,代码会做省略

  // lib/webpack.js 1284行
  // 开启编译流程
  compiler.run((err, stats) => {
    compiler.close(err2 => {
      callback(err || err2, stats);
    });
  });

  // lib/compiler.js 1081行
  // 开启编译流程
  compile(callback) {
    const params = this.newCompilationParams();
    // 创建 Compilation 对象
    const Compilation = this.newCompilation(params);
  }

  // lib/Compilation.js 1865行
  // 确认入口文件
  addEntry() {
    this._addEntryItem();
  }

  // lib/Compilation.js 1834行
  // 开始创建模块流程,创建模块实例
  addModuleTree() {
    this.handleModuleCreation()
  }

  // lib/Compilation.js 1548行
  // 开始创建模块流程,创建模块实例
  handleModuleCreation() {
    this.factorizeModule()
  }

  // lib/Compilation.js 1712行
  // 添加到创建模块队列,执行创建模块
  factorizeModule(options, callback) {
    this.factorizeQueue.add(options, callback);
  }

  // lib/Compilation.js 1834行
  // 保存需要构建模块
  _addModule(module, callback) {
    this.modules.add(module);
  }

  // lib/Compilation.js 1284行
  // 添加模块进模块编译队列,开始编译
  buildModule(module, callback) {
    this.buildQueue.add(module, callback);
  }

3. webpack 生成阶段做了什么

构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。经过构建阶段之后,webpack 得到足够的模块内容与模块关系信息,之后通过 Compilation.seal 函数生成最终资源

3.1 生成产物

执行 Compilation.seal 进行产物的封装

  • 构建本次编译的 ChunkGraph 对象,执行 buildChunkGraph,这里会将 import()、require.ensure 等方法生成的动态模块添加到 chunks 中

  • 遍历 Compilation.modules 集合,将 module 按 entry/动态引入 的规则分配给不同的 Chunk 对象。

  • 调用 Compilation.emitAssets 方法将 assets 信息记录到 Compilation.assets 对象中。

  • 执行 hooks.optimizeChunkModules 的钩子,这里开始进行代码生成和封装。

    • 执行一系列钩子函数(reviveModules, moduleId, optimizeChunkIds 等)
    • 执行 createModuleHashes 更新模块 hash
    • 执行 JavascriptGenerator 生成模块代码,这里会遍历 modules,创建构建任务,循环使用 JavascriptGenerator 构建代码,这时会将 import 等模块引入方式替换为 webpack_require 等,并将生成结果存入缓存
    • 执行 processRuntimeRequirements,根据生成的内容所使用到的 webpack_require 的函数,添加对应的代码
    • 执行 createHash 创建 chunk 的 hash
    • 执行 clearAssets 清除 chunk 的 files 和 auxiliary,这里缓存的是生成的 chunk 的文件名,主要是清除上次构建产生的废弃内容

3.2 文件输出

回到 Compiler 的流程中,执行 onCompiled 回调。

  • 触发 shouldEmit 钩子函数,这里是最后能优化产物的钩子。
  • 遍历 module 集合,根据 entry 配置及引入资源的方式,将 module 分配到不同的 chunk。
  • 遍历 chunk 集合,调用 Compilation.emitAsset 方法标记 chunk 的输出规则,即转化为 assets 集合。
  • 写入本地文件,用的是 webpack 函数执行时初始化的文件流工具。
  • 执行 done 钩子函数,这里会执行 compiler.run() 的回调,再执行 compiler.close(),然后执行持久化存储(前提是使用的 filesystem 缓存模式)

1.6 总结

  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。
  2. 开始编译:从上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根scope据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。
  5. 完成模块编译:经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

14 webpack性能优化-构建速度

先分析遇到哪些问题,在配合下面的方法优化,不要上来就回答,让人觉得背面试题

  • 优化babel-loader缓存

  • IgnorePlugin 忽略某些包,避免引入无用模块(直接不引入,需要在代码中引入)

    • import moment from 'moment'
    • 默认会引入所有语言JS代码,代码过大
    import moment from 'moment'
    moment.locale('zh-cn') // 设置语言为中文
    
    // 手动引入中文语言包
    import 'moment/locale/zh-cn'
    
    // webpack.prod.js
    pluins: [
        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/./locale/, /moment/),
    ]
    
  • noParse 避免重复打包(引入但不打包)

  • happyPack多线程打包

    • JS单线程的,开启多进程打包
    • 提高构建速度(特别是多核CPU)
      // webpack.prod.js
      const HappyPack = require('happypack')
    
      {
          module: {
              rules: [
                  // js
                  {
                      test: /.js$/,
                      // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                      use: ['happypack/loader?id=babel'],
                      include: srcPath,
                      // exclude: /node_modules/
                  },
              ]
          },
          plugins: [
              // happyPack 开启多进程打包
              new HappyPack({
                  // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
                  id: 'babel',
                  // 如何处理 .js 文件,用法和 Loader 配置中一样
                  loaders: ['babel-loader?cacheDirectory']
              }),
          ]
      }
    
  • parallelUglifyPlugin多进程压缩JS

    • 关于多进程

      • 项目较大,打包较慢,开启多进程能提高速度
      • 项目较小,打包很快,开启多进程反而会降低速度(进程开销)
      • 按需使用
      // webpack.prod.js
      const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
      
      {
          plugins: [
              // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
              new ParallelUglifyPlugin({
                  // 传递给 UglifyJS 的参数
                  // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
                  uglifyJS: {
                      output: {
                          beautify: false, // 最紧凑的输出
                          comments: false, // 删除所有的注释
                      },
                      compress: {
                          // 删除所有的 `console` 语句,可以兼容ie浏览器
                          drop_console: true,
                          // 内嵌定义了但是只用到一次的变量
                          collapse_vars: true,
                          // 提取出出现多次但是没有定义成变量去引用的静态值
                          reduce_vars: true,
                      }
                  }
              })
          ]
      }
      
          
      
  • 自动刷新(开发环境)使用dev-server即可

  • 热更新(开发环境)

    • 自动刷新:整个网页全部刷新,速度较慢,状态会丢失

    • 热更新:新代码生效,网页不刷新,状态不丢失

      // webpack.dev.js
      const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
      
      entry: {
          // index: path.join(srcPath, 'index.js'),
          index: [
              'webpack-dev-server/client?http://localhost:8080/',
              'webpack/hot/dev-server',
              path.join(srcPath, 'index.js')
          ],
          other: path.join(srcPath, 'other.js')
      },
      devServer: {
          hot: true
      },
      plugins: [
          new HotModuleReplacementPlugin()
      ],
      
       
              
          
      
      // 代码中index.js
      
      // 增加,开启热更新之后的代码逻辑
      if (module.hot) {
          // 注册哪些模块需要热更新
          module.hot.accept(['./math'], () => {
              const sumRes = sum(10, 30)
              console.log('sumRes in hot', sumRes)
          })
      }
      
       
              
          
      
  • DllPlugin 动态链接库(dllPlugin只适用于开发环境,因为生产环境下打包一次就完了,没有必要用于生产环境)

    • 前端框架如react、vue体积大,构建慢

    • 较稳定,不常升级版本,同一个版本只构建一次,不用每次都重新构建

    • webpack已内置DllPlugin,不需要安装

    • DllPlugin打包出dll文件

    • DllReferencePlugin引用dll文件

      // webpack.common.js
      const path = require('path')
      const HtmlWebpackPlugin = require('html-webpack-plugin')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = {
          entry: path.join(srcPath, 'index'),
          module: {
              rules: [
                  {
                      test: /.js$/,
                      use: ['babel-loader'],
                      include: srcPath,
                      exclude: /node_modules/
                  },
              ]
          },
          plugins: [
              new HtmlWebpackPlugin({
                  template: path.join(srcPath, 'index.html'),
                  filename: 'index.html'
              })
          ]
      }
      
          
      
      // webpack.dev.js
      const path = require('path')
      const webpack = require('webpack')
      const { merge } = require('webpack-merge')
      const webpackCommonConf = require('./webpack.common.js')
      const { srcPath, distPath } = require('./paths')
      
      // 第一,引入 DllReferencePlugin
      const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
      
      module.exports = merge(webpackCommonConf, {
          mode: 'development',
          module: {
              rules: [
                  {
                      test: /.js$/,
                      use: ['babel-loader'],
                      include: srcPath,
                      exclude: /node_modules/ // 第二,不要再转换 node_modules 的代码
                  },
              ]
          },
          plugins: [
              new webpack.DefinePlugin({
                  // window.ENV = 'production'
                  ENV: JSON.stringify('development')
              }),
              // 第三,告诉 Webpack 使用了哪些动态链接库
              new DllReferencePlugin({
                  // 描述 react 动态链接库的文件内容
                  manifest: require(path.join(distPath, 'react.manifest.json')),
              }),
          ],
          devServer: {
              port: 8080,
              progress: true,  // 显示打包的进度条
              contentBase: distPath,  // 根目录
              open: true,  // 自动打开浏览器
              compress: true,  // 启动 gzip 压缩
      
              // 设置代理
              proxy: {
                  // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
                  '/api': 'http://localhost:3000',
      
                  // 将本地 /api2/xxx 代理到 localhost:3000/xxx
                  '/api2': {
                      target: 'http://localhost:3000',
                      pathRewrite: {
                          '/api2': ''
                      }
                  }
              }
          }
      })
      
          
      
      // webpack.prod.js
      const path = require('path')
      const webpack = require('webpack')
      const webpackCommonConf = require('./webpack.common.js')
      const { merge } = require('webpack-merge')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = merge(webpackCommonConf, {
          mode: 'production',
          output: {
              filename: 'bundle.[contenthash:8].js',  // 打包代码时,加上 hash 戳
              path: distPath,
              // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
          },
          plugins: [
              new webpack.DefinePlugin({
                  // window.ENV = 'production'
                  ENV: JSON.stringify('production')
              })
          ]
      })
      
       
              
          
      
      // webpack.dll.js
      
      const path = require('path')
      const DllPlugin = require('webpack/lib/DllPlugin')
      const { srcPath, distPath } = require('./paths')
      
      module.exports = {
      mode: 'development',
      // JS 执行入口文件
      entry: {
          // 把 React 相关模块的放到一个单独的动态链接库
          react: ['react', 'react-dom']
      },
      output: {
          // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
          // 也就是 entry 中配置的 react 和 polyfill
          filename: '[name].dll.js',
          // 输出的文件都放到 dist 目录下
          path: distPath,
          // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
          // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
          library: '_dll_[name]',
      },
      plugins: [
          // 接入 DllPlugin
          new DllPlugin({
          // 动态链接库的全局变量名称,需要和 output.library 中保持一致
          // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
          // 例如 react.manifest.json 中就有 "name": "_dll_react"
          name: '_dll_[name]',
          // 描述动态链接库的 manifest.json 文件输出时的文件名称
          path: path.join(distPath, '[name].manifest.json'),
          }),
      ],
      }
      
       
              
          
      
      "scripts": {
          "dev": "webpack serve --config build/webpack.dev.js",
          "dll": "webpack --config build/webpack.dll.js"
      },
      
       
              
          
      

优化打包速度完整代码

// webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            // babel-loader
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
            chunks: ['index', 'vendor', 'common']  // 要考虑代码分割
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other', 'vendor', 'common']  // 考虑代码分割
        })
    ]
}
// webpack.dev.js
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = smart(webpackCommonConf, {
    mode: 'development',
    entry: {
        // index: path.join(srcPath, 'index.js'),
        index: [
            'webpack-dev-server/client?http://localhost:8080/',
            'webpack/hot/dev-server',
            path.join(srcPath, 'index.js')
        ],
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            {
                test: /.js$/,
                loader: ['babel-loader?cacheDirectory'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 直接引入图片 url
            {
                test: /.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            },
            // {
            //     test: /.css$/,
            //     // loader 的执行顺序是:从后往前
            //     loader: ['style-loader', 'css-loader']
            // },
            {
                test: /.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
            },
            {
                test: /.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('development')
        }),
        new HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true,

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },
    // watch: true, // 开启监听,默认为 false
    // watchOptions: {
    //     ignored: /node_modules/, // 忽略哪些
    //     // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    //     // 默认为 300ms
    //     aggregateTimeout: 300,
    //     // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    //     // 默认每隔1000毫秒询问一次
    //     poll: 1000
    // }
})
// webpack.prod.js
const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // js
            {
                test: /.js$/,
                // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                use: ['happypack/loader?id=babel'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 图片 - 考虑 base64 编码的情况
            {
                test: /.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
            // 抽离 css
            {
                test: /.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less
            {
                test: /.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        }),

        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/./locale/, /moment/),

        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),

        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

        // 分割代码块
        splitChunks: {
            chunks: 'all',
            /**
             * initial 入口chunk,对于异步导入的文件不处理
                async 异步chunk,只对异步导入的文件处理
                all 全部chunk
             */

            // 缓存分组
            cacheGroups: {
                // 第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    minSize: 0,  // 大小限制
                    minChunks: 1  // 最少复用过几次
                },

                // 公共的模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0,  // 公共模块的大小限制
                    minChunks: 2  // 公共模块最少复用过几次
                }
            }
        }
    }
})

webpack性能优化-产出代码(线上运行)

前言

  • 体积更小
  • 合理分包,不重复加载
  • 速度更快、内存使用更少

产出代码优化

  • 小图片base64编码,减少http请求
// 图片 - 考虑 base64 编码的情况
module: {
    rules: [
        {
            test: /.(png|jpg|jpeg|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    // 小于 5kb 的图片用 base64 格式产出
                    // 否则,依然延用 file-loader 的形式,产出 url 格式
                    limit: 5 * 1024,

                    // 打包到 img 目录下
                    outputPath: '/img1/',

                    // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                    // publicPath: 'http://cdn.abc.com'
                }
            }
        },
    ]
}
  • bundle加contenthash,有利于浏览器缓存

  • 懒加载import()语法,减少首屏加载时间

  • 提取公共代码(第三方代码Vue、React、loadash等)没有必要多次打包,可以提取到vendor中

  • IgnorePlugin忽略不需要的包(如moment多语言),减少打包的代码

  • 使用CDN加速,减少资源加载时间

    output: {
      filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
      path: path.join(__dirname, '..', 'dist'),
      // 修改所有静态文件 url 的前缀(如 cdn 域名)
      // 这样index.html中引入的js、css、图片等资源都会加上这个前缀
      publicPath: 'http://cdn.abc.com'  
    },
    
  • webpack使用production模式,mode: 'production'

    • 自动压缩代码

    • 启动Tree Shaking

      • ES6模块化,import和export,webpack会自动识别,才会生效

      • Commonjs模块化,require和module.exports,webpack无法识别,不会生效

      • ES6模块和Commonjs模块区别

        • ES6模块是静态引入,编译时引入
        • Commonjs是动态引入,执行时引入
        • 只有ES6 Module才能静态分析,实现Tree Shaking
  • Scope Hoisting:是webpack3引入的一个新特性,它会分析出模块之间的依赖关系,尽可能地把打散的模块合并到一个函数中去,减少代码间的引用,从而减少代码体积

    • 减少代码体积
    • 创建函数作用域更少
    • 代码可读性更好