读书笔记 - 《Webpack实战:入门、进阶与调优》

541 阅读9分钟

第一章 Webpack简介

何为Webpack?

  • Webpack是一个开源的JS模块化打包工具,其核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个js文件。

为什么需要Webpack?

  • 模块化有利于开发和迭代,但无法直接实际使用
  • 大多数npm模块是CommonJS的形式,浏览器不支持
  • 个别浏览器/平台的兼容性问题
  • 无法使用code splitting(按需加载) 和 tree shaking(去除无用代码)来提升代码优化速度

为什么 js 需要模块化机制(而不是通过 script 标签来引入我们的分块开发的 js 文件)

需要手动维护 js 的加载顺序。(多文件相互依赖的时候往往很难确认依赖的顺序和加载的顺序)

每一个 script 标签都意味着需要向服务器多发出一次静态资源请求 (性能问题)

全局作用域污染(每个 script 中,顶层作用域即全局作用域)

Webpack优势

  • 支持多种模块标准,可处理好使用多种模块标准的工程中的各种依赖关系

  • 完备的代码分割(code splitting)解决方案

  • 可以通过各种loader处理各种类型的资源

  • 庞大的社区支持

  • 初次使用 

    npx webpack --entry=./index.js --output-filename=bundle.js --mode=development // 根据entry进行项目依赖查找// output-filename是输出资源名// 三种打包模式:development、production、none;

  • npx

    • npx 想要解决的主要问题,就是调用项目内部安装的模块。

      $ node-modules/.bin/mocha --version$ npx mocha --version
      
    • npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

      # 等同于 ls$ npx ls
      
  • webpack配置

    • 默认源码入口src/index.js

    • 参数太多,后期难以维护,可以把这些参数改为对象的形式专门放在一个配置文件里,在webpack每次打包的时候读取该配置文件即可;

    • webpack默认配置文件为webpack.config.js

      module.exports = {    entry: './src/index.js',    output: {        filename: 'bundle.js',    },    mode: 'development',}
      
  • webpack-dev-server

    • webpack进行模块打包,并处理打包结果的资源请求;
    • 作为普通的web servere,处理静态资源文件请求;
    • live-reloading(自动刷新);

注意:直接使用webpack开发和使用webpack-dev-server有一个很大的区别,前者每次都会生成bundle.js,而webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js,在每次webpack-dev-server接收到请求时都只是将内存中的打包结果返回给浏览器。

Webpack 基础配置重点 

Webpack 的默认配置文件为 webpack.config.js

output.path 需要用绝对地址。

webpack-dev-server: 需要关注 HMR 的底层实现

dev-server 工作:浏览器发出资源请求 → url 校验 → 资源服务地址(配置的 publickPath):输出打包结果资源。反之,读取硬盘中的源文件返回。

dev-server 和直接打包的区别:dev-server 的打包结果写在内存中,不会写入实际的 bundle.js,每次接收到请求后返回的是内存中的打包结果。

dev-server 工作内容:

  1. 模块打包。
  2. web server 分发资源。

第二章 模块打包

CommonJs (NodeJs [require, module.export]) 

CommonJs 规定每一个文件是一个模块(模块会形成自已的作用域,这样就不会造成变量污染了。

内部会有一个 module 对象用于存放当前模块的信息。

require 一个模块时会有两种情况:

  1. 第一次被加载的时候会首先执行该模块,然后导出内容。
  2. 如果已经被加载过,不会再次执行,而是直接导出上次执行后得到的结果。
  3. 内部拥有一个 loaded 去记录是否被加载过

注意:在使用exports时不要直接给exports赋值,会导致exports指向了新对象,module.exports仍旧是空对象;并且不要把module.exportsexports混用,可能会发生属性丢失的情况。

ES6 module (export default / import)

import export 属于关键字,而 module 不属于关键字。

ES6 module 会自动采用严格模式。

export 方式:

  1. 命名导出 export const name = 'xxx' / const name = 'xxx', export { name } (可以使用 as 作重命名)
  2. 默认导出 export default xxx (输出入了一个名为 default 的变量)

import 方式:

  1. 使用命名导出的变量: import {name} from 'xxx' (可以使用 as 作重命名),导入的都是只读属性,不能修改。导入多个变量可以用 import * as xxx from 'xxx' 代替
  2. 对于默认导出的变量, import xxx from 'xxx' (xxx 可以自由指定)
  3. 组合写法 import React, { Component } from 'React'

复合写法:导入后立即导出(只适用于命名导出)export name from 'xxx'

CommonJS、ES6 Module区别

  • 模块加载
    • CommonJS:“动态”=>模块依赖关系的建立发生在代码运行阶段,模块被执行前,并没有办法确定明确的依赖关系
    • ES6 Module:“静态”=>模块依赖关系的建立发生在代码编译阶段,导入路径不支持表达式,导入导出必须在顶层作用域
      • 死代码检测和排除,通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积;
      • 模块变量类型检查,import时就能判断出是否正确;
      • 编译器优化。CommonJS等动态模块系统中,本质上导入的是一个对象,ESM支持直接导入变量,减少了引用层级。
  • 导出值
    • CommonJS:值拷贝,允许对导入值进行修改,对其余导入位置无影响
    • ES6 Module:原有值的动态映射,并且只读。对内部变量进行修改其余导入位置也随之变化
  • 循环依赖
    • CommonJS:无法解决,按照顺序执行,产生循环依赖时直接取出默认module.exports,也就是{}
    • ES6 Module:利用导出值是动态映射的特性,保证导入的值被使用时已经设置好正确的导出值,即可支持循环依赖
**CommonJS**
**ES6 Module**
模块加载

动态:

  • 运行阶段明确依赖关系
  • 导入路径支持表达式

静态

  • 编辑阶段明确依赖关系
  • 不支持表达式,必须在顶层作用域

优点:

  • 静态分析排除无用代码
  • 类型检查
  • 支持直接导入变量,减少层级
导出值值拷贝:修改导入值对其他位置无影响动态映射:修改导入值其他位置也随之变化
循环依赖无法解决本身也不支持,利用循环特性可支持循环依赖

加载其他类型模块(使用场景已经不多)

  • 非模块化文件:需特别注意隐式声明的全局变量,Webpack在打包时会为每一个文件包装一层函数作用域来避免全局污染,无法挂在全局
  • AMD:异步模块定义,通过声明回调函数异步加载模块,将其他模块以依赖注入的方式加入进当前模块
    • define('name', [...dependencies], callback)
    • require([...modules], function(...modules) {})
    • 优势:模块加载是非阻塞性的,当执行到require函数时并不会停下去执行被记载的模块,而是继续执行require后面的代码,这使得模块加载操作并不会阻塞浏览器
    • 缺点:导入导出代码冗长,容易造成回调地域 
  • UMD 统一多模块标准管理方案的解决 
    • 目的:让任何标准的模块可以运行在各种环境(AMDCommonJs或非模块环境)下
    • 实现原理:
      • 先判断是否为AMD环境(是否有define函数) ,按 AMD 导出
      • 再判断是否为CommonJs环境,以CommonJs形式导出
  • npm模块

模块打包原理 

  • webpack打包原理:模块缓存—实现require(__webpack_require__函数)—实现入口模块的加载(以key-value的形式存储所有被打包的模块)

    • 最外层立即执行匿名函数—包裹整个bundle,并构成自身的作用域;

    • installModules对象—每个模块只在第一次被加载的时候执行,之后其导出值就被存储到这个对象里面,当再次被加载的时候直接从里面取值,而不会重新执行;

    • __webpack_require__函数:对模块加载的实现,在浏览器中可以通过该函数来完成模块导入;

    • Module对象:以key-value的形式存放工程中所有产生依赖的模块

      • key:表示模块id—数字/hash字符串;

      • value:匿名函数包裹的模块实体,匿名函数的参数赋予了每个模块的导入导出的能力;

/******/ (function(modules) { // webpackBootstrap/******/ // The module cache/******/ var installedModules = {};/******//******/ // The require function/******/ function __webpack_require__(moduleId) {/******//******/ // Check if module is in cache/******/ if(installedModules[moduleId]) {/******/ return installedModules[moduleId].exports;/******/ }/******/ // Create a new module (and put it into the cache)/******/ var module = installedModules[moduleId] = {/******/ i: moduleId,/******/ l: false,/******/ exports: {}/******/ };/******//******/ // Execute the module function/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);/******//******/ // Flag the module as loaded/******/ module.l = true;/******//******/ // Return the exports of the module/******/ return module.exports;/******/ }/******//******/ //....省略一部分.../******/ // Load entry module and return exports/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");/******/ })/************************************************************************//******/ ({/***/ "./node_modules/moment/locale sync recursive ^\\.\\/.*$":/*!**************************************************!*\!*** ./node_modules/moment/locale sync ^\.\/.*$ ***!\**************************************************//*! no static exports found *//***/ (function(module, exports, __webpack_require__) {//....省略一部分.../***/ "./src/add-content.js":/*!****************************!*\!*** ./src/add-content.js ***!\****************************//*! exports provided: default *//***/ (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var moment__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! moment */ \"./node_modules/moment/moment.js\");\n/* harmony import */ var moment__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(moment__WEBPACK_IMPORTED_MODULE_0__);\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (function () {\n document.write('Hello cs! dev-server hahahha<br />')\n document.write(moment__WEBPACK_IMPORTED_MODULE_0___default()(new Date()).format('YYYYMMDD'))\n});\n\n\n//# sourceURL=webpack:///./src/add-content.js?");/***/ }),/***/ "./src/index.js":/*!**********************!*\!*** ./src/index.js ***!\**********************//*! no exports provided *//***/ (function(module, __webpack_exports__, __webpack_require__) {"use strict";eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _add_content__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add-content */ \"./src/add-content.js\");\n\n\ndocument.write('My first Webpack app.<br />');\nObject(_add_content__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\n\n//# sourceURL=webpack:///./src/index.js?");/***/ })/******/ });
  • bundle在浏览器中执行步骤:代码执行顺序和模块加载的顺序完全一致;

    1. 最外层的匿名函数中会初始化浏览器的执行环境;

    2. 加载入口模块—每个bundle有且只有一个入口模块;

    3. 执行模块代码

      1. 执行到module.exports则记录下模块的导出值;

      2. 若遇到require()则会暂时交出执行权,进入__webpack_require__函数体内进行加载其他模块的逻辑;

    4. __webpack_require__中会判断即将加载的模块是否存在于installedModules

      1. 若存在,直接取值;

      2. 若不存在,执行该模块代码获取导出值;

    5. 所有依赖模块都执行完毕,最后执行权又回到入口模块,当入口模块的代码执行到结尾,也意味着整个bundle运行结束;
      要点: 3.4 步为一个递归过程,Webpack 在本质上不会修改代码的执行逻辑,代码的加载顺序和模块的加载顺序完全一致(同步)。

第三章 资源输入输出

资源处理流程

Webpack 从入口文件开始检索,将有依赖关系的模块生成一颗依赖树,最终得到一个个 chunk。(chunk:代码块,呗抽象和包装过后的模块。根据配置不同,一个工程可能会产生一个或多个 chunkchunk 得到的产物就是 bundle。)工程科定义多个入口,每个入口产生一个结果资源,也就是说 entrybundle 有一定的对应关系。

特殊情况下,一个入口也可能产生多个 chunk 并最终生成多个 bundle

配置资源入口

Webpack 通过 contextentry 这两个配置项来共同决定入口文件的路径。

  • 确定入口模块位置;
  • 定义chunk name。一个入口,默认其 chunk name 为“main”;如有多个入口,需定义每个入口的 chunk name,来作为该 chunk 的唯一标识。

- context:资源入口路径前缀,必须是绝对路径的字符串,可省略,默认值为工程根目录。

- entry:多种形式配置

  • 字符串:无法更改chunk name
entry: './src/index.js'
  • 数组:将多个资源预先合并,最后一个元素为实际入口,相当于在入口文件中 import 资源。无法更改chunk name
entry: ['babel-polyfill', './src/index.js']
  • 对象:定义多入口,key => chunk name,value => 入口路径。对象的属性值也可以为字符串或数组。
entry: {    index: ['babel-polyfill', './src/index.js'],    chunk1: './src/chunk1.js',}
  • 函数:返回值为上面任意一种形式。可动态获取入口,支持异步操作
entry: () => './src/index.js'

实例

  • SPA。一般定义单一入口,各个模块都有此入口引用,关系清晰但会导致体积过大,降低渲染速度。Webpackbundle大于250KB时会发出警告

  • 提取 vendor,添加一个新 chunk,放置第三方模块,不会经常更改,加快后续页面渲染速度。

    entry: {    index: './src/index.js',    vendor: ['react', 'redux'],}
    
  • 多页应用,每个页面独立bundle,加载各自必要的逻辑,同时也可以提取vendor

    entry: {    a: './src/a.js',    b: './src/b.js',    vendor: ['react', 'redux'],}
    

配置资源出口

  • filename:字符串,输出文件名或相对路径,目录不存在会创建该目录。
    多入口场景下,Webpack 支持为需要为每个 bundle 指定不同名字。下例中 name 会被替换成chunk name

    entry: {    a: './src/a.js',    b: './src/b.js',    vendor: ['react', 'redux'],}output: {    filename: '[name]@[chunkhash].js'}
    

    模板变量:当有多个 chunk 存在时对不同的 chunk 进行区分;控制客户端缓存。

    模板变量功能其他
    `[hash]``Webpack` 此次打包所有资源生成的 `hash`表中的 [`hash`] 和 [`chunkhash`] 都与 `chunk` 内容直接相关,在 `filename` 中使用了这些变量后,当 `chunk` 的内容改变时,可以同时引起资源文件名的更改,从而使用户在下一次请求资源文件时会立即下载新的版本而不会使用本地缓存。[`query`]也可以起到类似的效果,只不过它与 `chunk` 内容无关,要由开发者手动指定。如果要控制客户端缓存,最好还要加上 [`chunkhash`],因为每个 `chunk` 所产生的[`chunkhash`]只与自身内容有关,单个 `chunk` 内容的改变不会影响其他资源,可以最精确地让客户端缓存得到更新。
    `[chunkhash]`当前 `chunk` 内容的 `hash`
    `[id]`当前 `chunk` 的 `id`
    `[query]``filename` 配置项中的 `query`

更新缓存一般只用在生产环境的配置下,在开发环境中可以不必配置[chunkhash],后续介绍

  • path:指定资源输出的位置,要求值必须为绝对路径。

  • publicPath:指定资源请求的位置。
    输出位置:打包完成后资源产生的目录,一般将其指定为工程中的 dist 目录。
    请求位置:由 JSCSS 所请求的间接资源路径。

    • HTML 相关:可以将 publicPath 指定为 HTML 的相对路径,在请求这些资源时会以当前页面 HTML 所在路径加上相对路径,构成实际请求的 URL

    • Host 相关:若 publicPath 的值以“/”开始,则代表此时 publicPath 是以当前页面的host name为基础路径的。

    • CDN相关:绝对路径,当publicPath以协议头或相对协议的形式开始时,代表当前路径是CDN相关

      // HTML地址:http://localhost:8080/app/index.html// 异步加载资源名为:a.chunk.jspublicPath: "", // http://localhost:8080/app/a.chunk.jspublicPath: "./js", // http://localhost:8080/app/js/a.chunk.jspublicPath: "/", // http://localhost:8080/a.chunk.jspublicPath: "/js", // http://localhost:8080/js/a.chunk.jspublicPath: "https://cdn.com/", // https://cdn.com/a.chunk.jspublicPath: "//cdn.com/assets", // //cdn.com/assets/a.chunk.js
      

注:webpack-dev-serverpublicPath 是指定静态资源服务路径,可以将 webpack-dev-serverpublicPathWebpack 中的 output.path 保持一致,这样在任何环境下资源输出的目录都是相同的。

第四章 预处理器(loader)

一切皆模块

loader - JS 外的其他类型资源都需要 loader 进行转义

每个loader本质上都是一个函数。对接收到的内容进行转换,返回转换后的结果。output = loader(input)

inputoutput 可以是字符串或上一个 loader 转化后的结果,包括字符串、source map、AST 对象。

// babel-loader 转化ES6+ ES5 = babel-loader(ES6+)// 链式 output = loaderA(loaderB(loaderC(input)))// 编译 SCSS Style标签 = style-loader(css-loader(sass-loader(SCSS)))
  • loader 引入:install => module.rules 中配置

    • test:正则表达式 / 正则表达式数组,匹配模块
    • use:该规则使用 loader 的数组 / 字符串(只有一个 loader 时使用)
    • 链式 loaderWebpack打包时按照数组从后往前的顺序将资源交给loader处理,要把最后生效的放在前面。
    • loader options
  • exclude:所有被正则匹配到的模块都排除在该规则之外; include:规则只对正则匹配到的模块生效;
    excludeinclude 同时存在时,exclude 的优先级更高,可以对 include 中的子目录进行排除

  • resourceissuer
    Webpack 中,我们认为被加载模块是 resource,而加载者是 issuer

  • enforce:指定一个 loader 的种类,只接收“pre”或“post”两种字符串类型的值。
    loader执行顺序分为四种:preinline(官方已不推荐)、normalpost
    pre:在所有正常loader之前执行,保证代码未被其他 loader 更改过
    post:与 pre 相反,在所有 loader 之后执行

常用 loader

  • **babel-loader**: 处理ES6+并将其编译为ES5
    • exclude 中要添加 node_modules
    • cacheDirectory 缓存机制,防止未改变模块重复编译,可以是指定的路径,为true时缓存目录指向 node_modules/.cache/babel-loader
    • @babel/preset-envmodules 配置项设置为false会禁用模块语句的转化,防止转化成CommonJStree-shaking 特性失效
    • babel-loader支持从.babelrc文件读取Babel配置,因此可以将presets 和 plugins 从Webpack 配置文件中提取出来,也能达到相同的效果
  • **ts-loader**:Typescript 本身的配置并不在 ts-loader 中,而是必须要放在工程目录下的tsconfig.json 中;
  • **html-loader**:将 HTML 文件转化为字符串并进行格式化;
  • **handlebars-loader**:处理 handlebars 模板,得到一个函数,可以接收一个变量,最终返回字符串。在安装时要额外安装 handlebars
  • file-loader:打包文件类型的资源,并返回其publicPath
    • 不指定output.publicPath,返回文件 hash 值,指定后返回 output.publicPath + hashfile-loader 也支持配置文件名以及 publicPath,会覆盖output.publicPath,通过loaderoptions传入
  • url-loader:用户可以设置一个文件大小的阈值,当大于该阈值时与file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码。

**vue-loader**:处理 vue 组件,可以将组件的模板、JS及样式进行拆分

module.exports = {    module: {    rules: [{      test: /\.css$/,      // use: ['style-loader', {      // loader: 'css-loader',      // options: {        // modules: true,        // camelCase: true      // }      // }],      use: ExtractTextPlugin.extract({        fallback: 'style-loader', // 用于指定当插件无法提取样式时所采用的loader        use: 'css-loader', // 指定在提取样式之前采用哪些loader来预先进行处理      }),      exclude: /node_modules/, //  include: /src/      issuer: {        test: /\.js$/,        include: /src/, // 当前目录的js文件引用css,才会生效      }    },{      test: /\.js|jsx$/,      include: /src/,      use: [{        loader: 'babel-loader',        options: {          cacheDirectory: path.resolve('.cache')        }      }],    },{        test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,        use: {           loader: 'url-loader',           options: {             limit: 8192,             name: 'images/[hash:8].[ext]',           }        },     }]  },  plugins: [    new ExtractTextPlugin("[name].css"),    new MiniCssExtractPlugin({      filename: '[name].css',      chunkFilename: '[id].css'    })  ]}

自定义 loader

第五章 样式处理

plugins

接收一个插件数组,可以在打包的各个环节中添加一些额外任务

分离样式文件

  • extract-text-webpack-plugin,多样式文件使用[name].css来动态生成CSS为文件名
  • mini-css-extract-plugin,支持按需加载,设置publicPath,不需设置fallback

样式预处理

SassSCSS(sass-loader)Less(less-loader),配置sourceMap可在浏览器中查看源码

PostCss

接收样式源代码并交由编译插件处理,最后输出CSS

CSS Modules

CSS模块化

  • 每个CSS文件中的样式都拥有单独的作用域,不会和外界发生命名冲突。
  • CSS进行依赖管理,可以通过相对路径引入CSS文件。
  • 可以通过composes轻松复用其他CSS模块。

使用CSS Modules不需要额外安装模块,只要开启css-loader中的modules配置项即可。localIdentName配置项,它用于指明CSS代码中的类名会如何来编译。

module.exports = {    module: {    rules: [{      test: /\.css$/,      use: ['style-loader', {        loader: 'css-loader',        options: {          modules: true,          localIdentName: '[name]__[local]__[hash:base64:5]'        }      }],    }]  }}
  • [name]指代的是模块名,这里被替换为style。
  • [local]指代的是原本的选择器标识符,这里被替换为title。
  • [hash:base64:5]指代的是一个5位的hash值,这个hash值是根据模块名和标识符计算的,因此不同模块中相同的标识符也不会造成样式冲突。

第六章 代码分片

目的是按需加载,减小首屏资源体积,利用缓存,加快首屏加载速度

通过入口划分

每个入口都生成一个对应的文件,适用多页应用,主要适合于那些将接口绑定在全局对象上的库,业务代码中的模块无法直接引用库中的模块。而且需要手工的方式去配置和提取公共模块,比较麻烦复杂

CommonsChunkPlugin

  • 提取多入口chunk之间的公共模块,单页应用第三方类库及业务中不常更新的模块
    • 配置项:

      • name:用于指定公共chunk的名字;

      • filename:提取后的资源文件名;

      • chunks:设置提取范围;

      • minChunks:提取规则

        • 数字:为n时,只有该模块被n个入口同时引用才会进行提取,数组形式入口传入模块仍会提取
        • Infinity:所有模块都不会被提取。提取特定的几个模块;生成一个没有任何模块而仅仅包含Webpack初始化环境的manifest文件
        • 函数:每个模块都会经过这个函数的处理,当函数的返回值是true时进行提取
    • hash与长效缓存:为Webpack runtime单独提取manifest,写在最后,最先引入

    • 优点

      1. 开发过程中减少了重复模块打包,可以提升开发速度;
      2. 减小整体资源体积;
      3. 合理分片后的代码可以更有效地利用客户端缓存。
    • 缺点

      1. 一个CommonsChunkPlugin只能提取一个vendor
      2. manifest会使浏览器多加载一个资源;
      3. CommonsChunkPlugin在提取公共模块的时候会破坏掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。
    • 需要在页面中添加一个script标签来引入commons.js,并且该JS一定要在其他JS之前引入。

optimization.SplitChunks

只需要设置一些提取条件,chunks的值为allSplitChunks将会对所有的chunks生效,不配置默认只对异步chunks生效

splitChunks: {    chunks: "async", // 匹配模式,async(默认,只提取异步)、initial(只对入口生效)和all    minSize: {      javascript: 30000,      style:        50000,    },    maxSize: 0,    minChunks: 1,    maxAsyncRequests : 5,    maxInitialRequests: 3,    automaticNameDelimiter: '~',    name: true, // 根据cacheGroups和作用范围自动为新生成的chunk命名,并以automaticNameDelimiter分隔    cacheGroups:{      vendors: { // 提取所有node_modules中符合条件的模块        test: /[\\/]node_modules[\\/]/,        priority: -10,      },      default: { // 提取被多次引用的模块        minChunks: 2,        priority: -20,        reuseExistingChunk: true      }    }  }

资源异步加载

  • import() ——通过import函数加载的模块及其依赖会被异步地进行加载,并返回一个Promise对象。可在任何地方调用,而非必须在顶层
  • 异步chunk配置—— 配置chunkFileName[name].js,就可以通过特殊注释/* webpackChunkName: 'chunkName' */ 来获取异步chunk名字

第七章 生产环境配置

环境配置的封装

  • 一个配置文件,根据ENV变量判断使用什么配置
  • 不同环境使用不同配置文件和命令。可提取公共部分或者webpack-merge

环境变量

new webpack.DefinePlugin ({    ENV: JSON.stringify('production'),    IS_PRODUCTION: true,    ENV_ID: 130912098 ,    CONSTANTS: JSON.stringify({        TYPES: ['foo','bar']    })})

许多框架与库都采用process.env.NODE_ENV作为一个区别开发环境和生产环境的变量。

DefinePlugin在替换环境变量时对于字符串类型的值进行的是完全替换,在替换后就会成为变量名,而非字符串值。因此对于字符串环境变量及包含字符串的对象要加上JSON.stringify

source map

将编译、打包、压缩后的代码映射回源代码,有一定的安全隐患,即任何人都可以通过dev tools看到工程源码,越好的源码映射,build速度越慢

  • source-map —— 在外部生成一个文件 在控制台会显示错误代码准确信息和源代码的错误位置

  • eval-source-map —— 不会产生单独的文件内嵌到bundle.js中,但是可以显示行和列

  • cheap-module-source-map —— 会产生单独的文件,不会产生列

  • cheap-module-eval-source-map —— 不会产生文件,集成在文件中,不会产生列

  • hidden-source-map:仍然会产出完整的map文件,只不过不会在bundle文件中添加对于map文件的引用。这样一来,当打开浏览器的开发者工具时,我们是看不到map文件的,浏览器自然也无法对bundle进行解析。如果我们想要追溯源码,则要利用一些第三方服务,将map文件上传到那上面。目前最流行的解决方案是Sentry

  • nosources-source-map:打包部署之后,我们可以在浏览器开发者工具的Sources选项卡中看到源码的目录结构,但是文件的具体内容会被隐藏起来。

在所有这些配置之外还有一种选择,就是我们可以正常打包出source map,然后通过服务器的nginx设置(或其他类似工具)将.map文件只对固定的白名单(比如公司内网)开放,这样我们仍然能看到源码,而在一般用户的浏览器中就无法获取到它们了。

资源压缩

UglifyJS、terser

缓存

  • 资源hash
  • 输出动态HTMLhtml-webpack-plugin
  • 使chunk id更稳定
    • Webpack 3HashedModuleIds-Plugin,它可以为每个模块按照其所在路径生成一个字符串类型的hash id
    • Webpack 3以下webpack-hashed-module-id-plugin

第八章 打包优化

项目初期看不到优化点的时候不要过早优化,复杂且效果不明显

转义代码流程:

  1. 从配置中获取打包入口;
  2. 匹配loader规则,并对入口模块进行转译;
  3. 对转译后的模块进行依赖查找(如a.js中加载了b.jsc.js);
  4. 对新找到的模块重复进行步骤2)和步骤3),直到没有新的依赖模块。

HappyPack - 多线程,适用于转义任务比较重的工程

const HappyPack = require('happypack);module.exports = {    module: {    rules: [{        test: /\.js$/,        include: 'node_modules',        loader: 'happypack/loader', // 多个loader需加id => 'happypack/loader?id=js'    }],    plugins: [        new HappyPack({            // 多个loader需加id => id: 'js'            loaders: [{ // 原有的babel-loader连同它的配置                loader: 'babel-loader',                options: {                    presets: ['react']                }            }]        })    ]  }}

缩小打包作用域

includeexclude:确定loader规则适用的范围

noParse:忽略某些模块,打包时Webpack不会对其进行任何解析,但仍会打包到bundle

IgnorePlugin:排除某些模块,即使被引用也不会打包到bundle

cacheWebpack 5中新增,全局启用文件缓存。⚠️无法自动监测缓存是否过期,现在如果更新需要手动修改cache.version

动态链接库与DLLPlugin

Code Splitting:设置一些特定的规则并在打包的过程中根据这些规则提取模块;

DLLPlugin:将vendor完全拆出来,有自己的一整套Webpack配置并独立打包,在实际工程构建时不用再进行处理,直接取用;

DllReferencePlugin:获取打包好的资源清单

HashedModuleIdsPlugin:解决数字id问题

DLLPlugin的name要与与output.library的值保持一致,因为app.js会通过name字段获取library
// 单独创建Webpack配置文件const path = require('path');const webpack = require('webpack');const dllAssetPath = path.join(__dirname, 'dll');const dllLibraryName = 'dllExample';module.exports = {  entry: ['react'],  output: {    path: dllAssetPath,    filename: 'vendor.js',    library: dllLibraryName  },  plugins: [    new webpack.DllPlugin({      name: dllLibraryName, // 导出的名字      path: path.join(dllAssetPath, 'mainfest.json') // 资源清单的绝对路径    })  ]} /************* manifest.json **************/{    "name": "dllExample",    "content": {        "./node_modules/object-assign/index.js": {            "id": "MgzW",            "buildMeta": {                "providedExports": true            }        }    }}

// Webpack.config.js module.exports = {  plugins: [    new webpack.DllReferencePlugin({      manifest: require(path.join(__dirname, 'dll/mainfest.json'))    })  ]}

tree shakeing

  • 只能对ESM生效
  • 如果使用babel-loader,一定要禁用它的模块依赖解析,modules: false
  • tree shaking只是标记,需要压缩工具去除死代码

第九章 开发环境调优

插件

  • webpack-dashboard
  • webpack-merge
  • speed-measure-webpack-plugin
  • size-plugin

模块热替换HMR

  1. 本地资源发生变化时webpack-dev-server通过websocket向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对
  2. 客户端已经知道新的构建结果和当前的有了差别,向WDS发起一个请求来获取更改文件的列表,即哪些模块有了改动
  3. webpack-dev-server获取main chunk的增量更新

第十章 更多JavaScript打包工具

Rollup

更专注于JavaScript的打包,不会注入多余的代码

可通过output.format配置输出资源的模块形式

Parcel

速度快

  • 利用worker来并行执行任务;
  • 文件系统缓存;
  • 资源编译处理流程优化。

零配置:并非完全没有,把配置进行了切分,交给BabelPostHTMLPostCSS等一些特定的工具进行分别管理