webpack入门

485 阅读39分钟

写在前面

此blog是在学习coderwhy老师的webpack课程过程中所做的学习笔记。其中可能会有记错的地方,希望大家指出。写出来主要目的是为了方便自己复习,如果能帮助到别人那就更好了。

webpack is a static module bunndler for modern JavaScript applications.----webpack 是一个静态的模块化打包工具,为现代的JavaScript应用程序。

01.webpack初体验

首先电脑必须有Node环境,然后安装webpack,webpack-cli使用 npm install webpack webpack-cli -g命令即可完成全局安装。然后便可以创建一个空项目来上手了呀。项目结构如下:

├─dist 
├─src 
│ ├─js 
│ │ └─util.js
│ └─index.js 
├─index.html
├─package-lock.json
└─package.json

项目创建好之后首先安装依赖npm install webpack webpack-cli -D,然后控制台执行命令webpack便会生成目录dist然后将dist中的代码手动引入index.html就可以开始体验webpack了。需要注意的是如果src下的入口文件名必须叫index.js,webpack默认配置会从文件夹下匹配此文件,如果更改文件名为main.js此时应该使用命令指定入口文件webpack --entry ./src/main.js --output-path ./dist。当然也可以配置package.json文件的scripts部分增加一个新的脚本。

  "scripts": {
    "build": "webpack" // 执行命令npm run build即可
  }

到此便完成了webpack的初次体验。

02.webpack配置文件--CSS

项目结构

├─dist 
├─src 
│ ├─css 
│ │ └─file.(css|less)
│ ├─js 
│ │ └─file.js
│ └─main.js 
├─.browserslistrc //兼容浏览器版本相关(也可以配置在package.json中)
├─postcss.config.js // css兼容相关
├─wk.config.js  // webpack配置
├─index.html
├─package-lock.json
└─package.json

浏览器兼容相关可以去caniuse查看:传送门也可以通过使用命令npx browserslist "兼容信息",eg: npx browserslist ">1%, last 2 version, not dead" 来查看。

less:可以使用less工具将xxx.less文件转换为xxx.css文件: 首先安装less: npm install less然后执行命令:lessc xxx.less xxx.css即可

因为不同的浏览器对css兼容性不同,所以会采用css加前缀的方式来实现此时可以使用命令:npx postcss --use autoprefixer -o output.css source.css

接下来便是编写webpack的配置文件:

const path = require("path");
module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",  // 打包后生成的文件名
    path: path.resolve(__dirname, "./build"), // 生成文件的绝对路径
  },
  module: {
    rules: [
      {
        // 规则使用正则表达式
        test: /\.css$/, // 匹配资源
        use: [
          //  编写顺序(从下往上, 从右往做, 从后往前)
          "style-loader", // {loader: "style-loader"}的简写
          {
            loader: "css-loader",
            options: {
              importLoaders: 1, // 加载到了新的css文件,重新回头使用前面的loader处理一次
            },
          },
          "postcss-loader",
        ]
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          "postcss-loader",
          "less-loader",
        ],
      },
    ],
  },
};

// package.json脚本配置
  "scripts": {
    "build": "webpack --config wk.config.js"
  },

03.webpack处理其他资源--图片&字体

项目结构

├─dist 
├─src 
│ ├─css 
│ │ └─file.(css|less)
│ ├─img 
│ │ └─file.(png|jpg)
│ ├─js 
│ │ └─file.js
│ └─main.js 
├─.browserslistrc //兼容浏览器版本相关(也可以配置在package.json中)
├─postcss.config.js // css兼容相关
├─wk.config.js  // webpack配置
├─index.html
├─package-lock.json
└─package.json

webpack的配置文件:

const path = require("path");
module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    // 必须是一个绝对路径
    path: path.resolve(__dirname, "./build"),
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          "postcss-loader",
        ],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          "postcss-loader",
          "less-loader",
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 1024 * 1024,  // 小于1M的图片直接转换为base64,可以减少http请求
              name: "img/[name].[hash:6].[ext]",
            },
          },
        ],
      },
    ],
  },
};


// package.json脚本配置
  "scripts": {
    "build": "webpack --config wk.config.js"
  }

补充一下文件的名称规则:

  • [ext]: 处理文件的扩展名
  • [name]: 处理文件名
  • [hash]: 处理文件内容的md4值
  • [contentHash]: file-loader中和[hash]结果一致
  • [hash:< length >]: 截取hash值得长度
  • [ext]: 文件相对于webpack配置文件的路径

asset module type的介绍:

  • webpack5之前,加载资源需要使用一些loader eg:raw-loader,url-loader,file-loader

  • webpack5开始之后,可以直接使用资源模块类型(asset module type) 来替代上面的loader

    • asset/resource: 发出一个单独的文件并导出URL(对应file-loader)

    • asset/inline: 导出一个资源的data URL(对应url-loader)

    • asset/source: 导出资源的源代码(对应row-loader)

    • asset: 在导出一个data URL和发送一个单独的文件之间自动选择。之前使用url-loader,并且配置资源体积限制实现。 wk.config.js支持加载字体资源后的目录结构

项目结构

├─dist 
├─src 
│ ├─css 
│ │ └─file.(css|less)
│ ├─font 
│ │ └─file.(css|eot|tff|woff|woff2})
│ ├─img 
│ │ └─file.(png|jpg)
│ ├─js 
│ │ └─file.js
│ └─main.js 
├─.browserslistrc //兼容浏览器版本相关(也可以配置在package.json中)
├─postcss.config.js // css兼容相关
├─wk.config.js  // webpack配置
├─index.html
├─package-lock.json
└─package.json

采用asset module type之后wk.config.js配置更新为:

const path = require("path");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "bundle.js",
    // 必须是一个绝对路径
    path: path.resolve(__dirname, "./build"),
    // assetModuleFilename: "img/[name].[hash:6][ext]"
  },
  module: {
    rules: [
      {
        // 规则使用正则表达式
        test: /\.css$/, // 匹配资源
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          "postcss-loader"
        ]
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          "postcss-loader",
          "less-loader",
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/,
        // type: "asset/resource", file-loader的效果
        // type: "asset/inline", url-loader
        type: "asset",
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 1024 * 1024,
          },
        },
      },
      {
        test: /\.ttf|eot|woff2?$/i, // 加载字体文件
        type: "asset/resource",
        generator: {
          filename: "font/[name].[hash:6][ext]",
        },
      },
    ],
  },
};

04.Plugin的使用

  • Loader是用于特定的模块类型进行转换

  • plugin可以用于执行更广泛的任务,比如打包优化,资源管理,环境变量注入等

  • CleanWebpackPlugin:重新打包时,都需要手动删除dist文件,此插件可以帮助我们完成

  • HtmlWebpackPlugin:帮助生成html文件,将打包后生成的文件引入,当然也可以选择自定义html模板。

  • DefinePlugin:允许在编译时创建配置的全局常量,是webpack的内置插件。 ERROR in Template execution failed: ReferenceError: BASE_URL is not defined这是因为编译template模板时有一个BASE_URL从没有定义过值,所以会报错。此时可以使用DefinePlugin插件。

  • CopyWebpackPlugin:可以帮助我们将文件复制到dist目录下 项目结构:

├─dist 
├─public 
│ ├─index.html 
│ └─favicon.icon
├─src 
│ ├─css 
│ │ └─file.(css|less)
│ ├─font 
│ │ └─file.(css|eot|tff|woff|woff2})
│ ├─img 
│ │ └─file.(png|jpg)
│ ├─js 
│ │ └─file.js
│ └─main.js 
├─.browserslistrc //兼容浏览器版本相关(也可以配置在package.json中)
├─postcss.config.js // css兼容相关
├─wk.config.js  // webpack配置
├─index.html
├─package-lock.json
└─package.json

wk.config.js

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { DefinePlugin } = require("webpack");
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "js/bundle.js",  //生成的dist/js放在js目录下
    path: path.resolve(__dirname, "./build"),
  },
  module: {
    rules: [
      {
        test: /\.css$/, 
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          "postcss-loader",
        ]
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          "postcss-loader",
          "less-loader",
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)$/,
        type: "asset",
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 100 * 1024,
          },
        },
      },
      {
        test: /\.ttf|eot|woff2?$/i,
        type: "asset/resource",
        generator: {
          filename: "font/[name].[hash:6][ext]",
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "hello webpack",
      template: "./public/index.html", //选择使用的模板,未指定则使用默认的ejs模板
    }),
    new DefinePlugin({
      BASE_URL: '"./"', // 定义全局变量BASE_URL
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: "public",
          globOptions: {
            ignore: ["**/index.html", "**/.DS_Store", "**/abc.txt"],
          },
        },
      ],
    }),
  ],
};

05.webpack模块化

Webpack打包的代码,允许使用各种各样的模块化,但是常用的是CommonJS,ES Module

  • webpack中CommonJS的实现 项目结构
├─dist 
├─src 
│ ├─js 
│ │ └─format.js // 使用CommonJS的方式导出对象
│ └─index.js 
├─.browserslistrc、
├─postcss.config.js 
├─wk.config.js 
├─package-lock.json
└─package.json

wk.config.js的配置

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development", // 默认为production
  entry: "./src/index.js",
  devtool: "source-map", // 便于阅读打包后的代码
  output: {
    filename: "js/bundle.js",
    path: path.resolve(__dirname, "./build"),
  },
  plugins: [new CleanWebpackPlugin(), new HtmlWebpackPlugin()],
};

查看打包后生成的js文件

// 立即执行函数
(function () {
  // 定义了一个对象
  // 模块的路径(key): 函数(value)
  var __webpack_modules__ = {
    "./src/js/format.js": function (module) {
      const dateFormat = (date) => {
        return "2020-12-12";
      };
      const priceFormat = (price) => {
        return "100.00";
      };
      console.log(cba);
      module.exports = {
        dateFormat,
        priceFormat,
      };
    },
  };
  // 定义一个对象,用于加载模块缓存
  var __webpack_module_cache__ = {};
  function __webpack_require__(moduleId) {
    if (__webpack_module_cache__[moduleId]) {
      return __webpack_module_cache__[moduleId].exports;
    }
    // 给module变量和__webpack_module_cache__[moduleId]赋值了同一个对象
    var module = (__webpack_module_cache__[moduleId] = {
      exports: {},
    });
    // 加载执行模块
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
    // 导出module.exports {dateFormat: function, priceForamt: function}
    return module.exports;
  }
  // !符号可将函数解析为表达式 => 立即执行函数
  !(function () {
    const { dateFormat, priceFormat } = __webpack_require__("./src/js/format.js");
    console.log(dateFormat("abc"));
    console.log(priceFormat("abc"));
  })();
})();
  • webpack中ES Module的实现
// 1.定义了一个对象, 对象里面放的是我们的模块映射
var __webpack_modules__ = {
  "./src/es_index.js": function (
    __unused_webpack_module,
    __webpack_exports__,
    __webpack_require__
  ) {
    // 调用r的目的是记录时一个__esModule -> true
    __webpack_require__.r(__webpack_exports__);

    // _js_math__WEBPACK_IMPORTED_MODULE_0__ == exports
    var _js_math__WEBPACK_IMPORTED_MODULE_0__ =
      __webpack_require__("./src/js/math.js");

    // 与下面代码等价
    // console.log(_js_math__WEBPACK_IMPORTED_MODULE_0__.mul(20, 30));
    // console.log(_js_math__WEBPACK_IMPORTED_MODULE_0__.sum(20, 30));
    console.log((0, _js_math__WEBPACK_IMPORTED_MODULE_0__.mul)(20, 30));
    console.log((0, _js_math__WEBPACK_IMPORTED_MODULE_0__.sum)(20, 30));
  },
  "./src/js/math.js": function (
    __unused_webpack_module,
    __webpack_exports__,
    __webpack_require__
  ) {
    __webpack_require__.r(__webpack_exports__);

    // 调用了d函数: 给exports设置了一个代理definition
    // exports对象中本身是没有对应的函数
    __webpack_require__.d(__webpack_exports__, {
      sum: function () {
        return sum;
      },
      mul: function () {
        return mul;
      },
    });

    const sum = (num1, num2) => {
      return num1 + num2;
    };
    const mul = (num1, num2) => {
      return num1 * num2;
    };
  },
};

// 2.模块的缓存
var __webpack_module_cache__ = {};

// 3.require函数的实现(加载模块)
function __webpack_require__(moduleId) {
  if (__webpack_module_cache__[moduleId]) {
    return __webpack_module_cache__[moduleId].exports;
  }
  var module = (__webpack_module_cache__[moduleId] = {
    exports: {},
  });
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  return module.exports;
}

!(function () {
  // __webpack_require__这个函数对象添加了一个属性: d -> 值function
  __webpack_require__.d = function (exports, definition) {
    for (var key in definition) {
      if (
        __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key)
      ) {
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };
})();

!(function () {
  // __webpack_require__这个函数对象添加了一个属性: o -> 值function
  __webpack_require__.o = function (obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
  };
})();

!(function () {
  // __webpack_require__这个函数对象添加了一个属性: r -> 值function
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
    }
    Object.defineProperty(exports, "__esModule", { value: true });
  };
})();

__webpack_require__("./src/es_index.js");

06.webpack的source-map

  • 运行在浏览器中的代码一般是经过压缩后的,与我们编写的代码其实是存在差异的。比如ES6的代码可能转换为ES5;对应放入代码行号或列号经过编译后肯定变得不一致;代码丑化时会将编码名称进行修改;会将TS编写的代码转换为JS等。source-map是从已转换的代码,映射到原始的源文件,使浏览器可以重构原始源并且在调试器中显示重建的原始源。
  • 在webpack中提供了26个选项来处理source-map传送门,选择不同的值其生成的source-map会稍微有差异,打包的过程也会有性能差异,可以根据不同的情况进行选择。
  • false: 不使用source-map
  • none:production模式下的默认值(什么都不用写),不生成source-map
  • eval:development模式下的默认值,不生成source-map
    • 会在eval执行的代码中添加//#soourceURL=
    • 他会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便调试代码。
  • source-map: 生成一个独立的source-map文件,并且在bundle文件中有一个注释指向source-map文件
  • eval-source-map: 会生成source-map,但是source-map是以DataUrl添加到eval函数的后面。
  • inline-source-map: 会生成source-map,但是source-map是以DataUrl添加到bundle文件的后面。
  • cheap-source-map: 会生成source-map,但是会更加高效一些(cheap低开销),因为他没有生成列映射(Column Mapping)(因为在开发中,只需要行信息通常就可以定位到错误了)
  • cheap-module-source-map: 会生成source-map,类似于cheap-source-map但是对源自loader的sourcemap处理会更好(需要配置babel-loader)。
  • hidden-module-source-map: 会生成source-map,但是不会对source-map文件进行引用,相当于删除了打包文件中对sourcemap的引用注释
  • nosource-module-source-map: 会生成source-map,但是不会对source-map只有错误信息的提示,不会生成源代码文件
  • 多个值的组合
    • inline | hidden| eval:三个值时三选一
    • nosource: 可选值
    • cheap:可选值且可以跟随module值
    • [inline-|hidden-|eval-][nosources-][cheap-[module-]]souorce-map
  • 开发阶段:推荐source-map/cheap-modules-source-map
  • 测试阶段:推荐source-map/cheap-modules-source-map
  • 发布阶段:false/缺省值(不写) 项目目录:
├─dist 
├─src 
│ ├─js 
│ │ └─format.js 
│ └─index.js 
├─.browserslistrc
├─postcss.config.js 
├─wk.config.js 
├─package-lock.json
└─package.json

wk.config.js配置

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  devtool: "cheap-source-map", // 验证source-map相关可以更改此值
  entry: "./src/index.js",
  output: {
    filename: "js/bundle.js",
    path: path.resolve(__dirname, "./build"),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "hello webpack",
    }),
  ],
};

07.Babel相关

  • 为什么需要babel? Babel是一个工具链,主要用于旧浏览器或者缓解中将ESMAScript2015+代码转换为向后兼容版本的JavaScript。包括语法转换,源代码转换,Polyfill实现目标缓解缺少的功能等。

  • babel是如何将一段代码(ES6, TS,React)转换成另外一段代码(ES5)的呢?

    • 可以将babel看成是一个编译器
    • babel编译器的工作就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码
  • Babel的工作流程

    • 解析阶段(Parsing)
    • 转换阶段(Transformation)
    • 生成阶段(Code Generation) 具体步骤: 源代码 -> 词法分析(Lexical Analysis) -> token数组 -> 语法分析(syntactic analysis也称为Parsing) -> AST抽象语法树 -> 遍历(Traverse) -> 访问(Visitor) -> 应用插件(Plugin) -> 新的AST(新的抽象语法树) -> 目标代码

项目结构

├─dist 
├─src 
│ ├─react_index.js 
│ └─index.js 
├─.browserslistrc
├─babel.config.js
├─postcss.config.js 
├─wk.config.js 
├─package-lock.json
└─package.json

wk.config.js配置

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/react_index.jsx",
  devtool: "source-map",
  output: {
    filename: "js/bundle.js",
    path: path.resolve(__dirname, "./build"),
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          // 下面这部分可以单独抽离出来到babel.config.js文件中去
          // options: {
            // presets: [
              // [
               //  "@babel/preset-env",
                // 该部分在没配置.browserslistrc时候使用
                // {
                  // targets: ["chrome 88"]
                  // esmodules: true
                // },
              // ],
           // ],
            // 使用预设时便不需要挨个配置插件
            // plugins: [
            //   "@babel/plugin-transform-arrow-functions",
            //   "@babel/plugin-transform-block-scoping"
            // ]
          },
        },
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "coderwhy webpack",
      template: "./index.html",
    }),
  ],
};

babel.config.js配置

module.exports = {
  presets: [
    ["@babel/preset-env", {
      // false: 不用任何的polyfill相关的代码
      // usage: 代码中需要哪些polyfill, 就引用相关的api
      // entry: 手动在入口文件中导入 core-js/regenerator-runtime, 根据目标浏览器引入所有对应的polyfill
      useBuiltIns: "entry",
      // 指定core.js版本
      corejs: 3
    }],
    // react相关的预设
    ["@babel/preset-react"]
  ],
  // plugins: [
  //   ["@babel/plugin-transform-runtime", {
  //     corejs: 3
  //   }]
  // ]
}

polyfill: 补丁,帮助我们更好地使用JavaScript,比如我们使用了一些语法特性eg:Promise,Generator,Symbol等某些刘兰兰器不兼容这些特性,必然会报错,此时可以使用polyfill来填充或者说是打一个补丁,那么就会包含该特性了。

08.webpack编译TypeScript相关

项目结构

├─dist 
├─src 
│ ├─react_index.js 
│ └─index.js 
├─.browserslistrc
├─babel.config.js
├─postcss.config.js 
├─wk.config.js 
├─tsconfig.json
├─package-lock.json
└─package.json

使用命令tsc --init生成tsconfig.json

wk.config.js配置如下

const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.ts",
  devtool: "source-map",
  output: {
    filename: "js/bundle.js",
    path: path.resolve(__dirname, "./build"),
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        // 本质上是依赖于typescript(typescript compiler)
        // use: "ts-loader" 
        use: "babel-loader",
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: "hello webpack",
      template: "./index.html",
    }),
  ],
};

babel.config.js配置

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
    ["@babel/preset-typescript"],
  ],
};
  • ts-loader和babel-loader的选择
    • ts-loader

      • 直接编译TypeScript,那么只能将ts转换为js
      • 如果还想在这个过程中添加对应的polyfill,那么ts-loader是无能为力的
      • 还需要babel来完成polyfill的填充功能
    • babel-loader

      • 直接编译TypeScript,那么只能将ts转换为js且可以实现polyfill功能
      • 但是babel-loader在编译的过程中,不会对类型错误校验 最佳实践:配置在package.json的script脚本处增加
  "scripts": {
    "build": "webpack --config wk.config.js",
    "type-check": "tsc --noEmit",
    "type-check-watch": "tsc --noEmit --watch"
  }

09.ESLint和Prettier

什么是ESLint?

  • ESLint是一个静态代码分析工具(static program analysis,在没有任何执行程序的情况下,对代码进行分析)
  • ESLint可以帮助我们在项目中建立统一的代码规范,保持正确,统一的代码风格,提高代码的可读性,可维护性
  • ESLint的规则是可配置的,可以自定义属于自己的规则

ESLint文件解析(默认创建环境)

  • env: 运行的环境比如浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法

  • extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数据(多个)

  • parseOptions:这里可以指定ECMAScript的版本,sourceType类型

    • parse:默认情况下是espree(也是一个JS Parser用于ESLint)但是因为我们需要编译TS,所以需要制定对应的解释器
  • plugins:指定使用到的插件

  • rules:自定义的一些规则

    • 格式是:配置的规则名称对应的值可以是数字,字符串,数组等
    • 字符串对应的三个值: off, warn, error
    • 数字对应的值:0, 1, 2(和上面一一对应)
    • 数组我们可以告知对应的提示以及希望获取到的值比如:['error', 'double'] VSCode的ESLint插件:如果每次校验时都执行一次npm run eslint 的话就有点麻烦了,所以可以使用VSCode的插件 VSCode的Prettier插件:帮助我们自动修复这些问题可以设置自动保存时修复也可以使用 option + shift + f 在编译代码时也想使用es-lint对代码进行检测,这个时候可以使用eslint-loader
  • webpack打包vue文件:首先需要安装vue-loadervue-template-compilerVue Loader查看如何配置。

10.devServer和HMR

为什么要搭建本地服务器? 目前我们开发的代码运行起来需要有两个操作:1.num run build编译打包2.通过live server或者直接通过浏览器打开index.html查看结果,这个过程是非常影响开发体验的我们希望可以做到当文件发生变化时可以完成自动编译和展示。为了完成自动编译webpack提供了几种可选的方式:1. webpack watch mode 2. webpack-dev-serve 3. webpack-dev-middleware

  • watch模式

    • 在此模式下webpack图中依赖的所有文件只要有一个文件发生了更新,那么代码将被重新编译,不需再去手动build
    • 开启方式:在导出配置中添加watch:true 或者在启动webpack的命令中添加 --watch
  • webpack-dev-server

    • 上面的方式可以监听到文件的变化但是事实上它本身是没有自动刷新浏览器功能的,当然我么其实可以在vscode中使用live-server来完成此功能,但是如果希望在不适用live-server的情况下可以具备live reloading(实时重载) 的功能。
    • 安装webpack-dev-server:npm install --save-dev webpack-dev-server
    • 使用webpack-dev-server:在编译之后不会写入到任何输出文件,而是将bundle文件保存到内存中
  • webpack-dev-middleware是一个封装器,他可以将webpack处理过的文件发送到一个server

    • webpack-dev-server在内部使用了它,然而他可以作为一个单独的package来使用,以便根据需求进行更多的自定义设置 认识热模块替换(HMR)
  • 什么是HMR?

    • HMR的全称是Hot Module Replacement(热模块替换)
    • 模块热替换是指在应用程序运行过程中,替换,添加,删除模块而无需重新刷新整个页面
  • HMR通过如下几种方式来提高开发的速度

    • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失
    • 只更新需要变化的内容,节省开发时间
    • 修改了css,js源代码会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式
  • 如何使用HMR

    • 默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可
    • 在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading
    • 开启HMR:修改webpack的配置devServer: { hot: true } 但是使用时会发现当修改了某一处的代码时,依然是刷新的整个页面,这是因为 需要指定哪些模块发生更新时 进行HMR
if (module.hot) {
  module.hot.accept("./file.js", () => {
    // 此函数相当于一个钩子函数
    console.log("file模块发生了更新~");
  });
}
  • 框架的HMR

    • 在开发项目时难道需要经常手动去写入module.hot.accpet相关的API吗?其实在社区针对这些情况已经有很成熟的方案了:比如vue开发中使用vue-loader,此loader支持vue组件的HMR;react有React Hot Loader 实时调用react组件(目前React官方已经弃用了,改用react-refresh
  • Vue的HMR

    • vue的加载需要使用vuve-loader而vue-loader加载的组件默认会进行HMR处理
    • 安装加载vue所需要的依赖 npm install vue-loader vue-template-compiler -D
  • React的HMR

    • 注意在之前React是借助于React Hot Loader来实现的HMR,目前已改用react-refresh来实现了
    • 安装@pmmmwh/react-refresh-webpack-plugin nnpm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

webpack.config.js配置

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  // watch: true,
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "./build"),
  },
  // 专门为webpack-dev-server
  devServer: {
    hot: true,
    port: 9000,
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        use: "babel-loader",
      },
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
      {
        test: /\.css/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new ReactRefreshWebpackPlugin(),
    new VueLoaderPlugin(),
  ],
};

babel.connfig.js配置

module.exports = {
  presets: [
    ["@babel/preset-env"],
    ["@babel/preset-react"],
  ],
  plugins: [
    ["react-refresh/babel"]
  ]
}
  • HMR的原理

    • 那么HMR的原理是什么呢?如何可以做到只更新一个模块中的内容呢?
      • webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket);
      • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
    • HMR Socket Server,是一个socket的长连接:
      • 长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
      • 当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);
      • 通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
      • 浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;
  • 想进一步了解可以查看这篇文章 ---> 传送门

11.webpack中路径的处理

  • output中的path的作用是告知webpack之后的输出目录

    • 比如静态资源的js、css等输出到哪里,常见的会设置为dist、build文件夹等
  • output中还有一个publicPath属性,该属性是指定index.html文件打包引用的一个基本路径

    • 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是 bundle.js
    • 在开发中将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;
    • 如果希望在本地直接打开html文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去查找资源
  • devServer中也有一个publicPath的属性,该属性是指定本地服务所在的文件夹:

    • 它的默认值是 /,也就是我们直接访问端口即可访问其中的资源 http://localhost:8080
    • 如果将其设置为了 /abc,那么我们需要通过 http://localhost:8080/abc 才能访问到对应的打包后的资源
    • 并且这个时候,我们其中的bundle.js通过 http://localhost:8080/bundle.js 也是无法访问的
      • 所以必须将output.publicPath也设置为 /abc;
      • 官方提到,建议 devServer.publicPath 与 output.publicPath相同
  • devServercontentBase对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容

    • 比如在index.html中,我们需要依赖一个 abc.js 文件,这个文件我们存放在public文件中;

    • 比如代码是这样的: <script src="./public/abc.js"></script>;

      • 但是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;

      • 所以代码是这样的:;

      • 但是我们如何让它去查找到这个文件的存在呢? 设置contentBase即可;

    • 当然在devServer中还有一个可以监听contentBase发生变化后重新编译的一个属性: watchContentBase

  • devServerhotOnly是当代码编译失败时,是否刷新整个页面:

    • 默认情况下当代码编译失败修复后,我们会重新刷新整个页面;

    • 如果不希望重新刷新整个页面,可以设置hotOnly为true;

  • devServerhost设置主机地址:

    • 默认是localhost
    • 如果希望其他地方可以访问可以设置为0.0.0.0
  • localhost0.0.0.0的区别

    • localhost:本质上是一个域名,通常情况下会被解析为127.0.0.1

    • 127.0.0.1: 回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被接收

      • 正常的数据库包经过:应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 物理层
      • 而回环地址,是在网络层直接就被获取到了,是不会经过数据链路层和物理层的
      • 比如我们监听127.0.0.1时,在同一个网段的主机中,通过ip地址是不能访问的
    • 0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序

      • 比如我们监听0.0.0.0时,在同一个网段在主机中,通过ip地址是可以访问的
  • port设置监听的端口默认是8080

  • open是否打开浏览器

    • 默认值是false,设置为true会打开浏览器
    • 也可以死设置为类似于Google Chrome等值
  • compress是否为静态文件开启gzip compression: 默认值是false,可以设置为true

image.png

  • Proxy代理
    • proxy是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:比如我们的一个api请求是http://localhost:8888 但是本地启动服务器的域名是: http://localhost:8000 这个时候发送网络请求就会出现跨域的问题,那么我们可以将请求先发送到一个代理服务器,代理服务器和API服务器没有跨域的问题,就可以解决我们的跨域问题了;我们可以进行如下的设置。

    • target:表示的是代理到的目标地址,比如 /api-webpack/moment会被代理到 http://localhost:8888/api-webpack/moment;

    • pathRewrite:默认情况下,我们的 /api-webpack 也会被写入到URL中,如果希望删除,可以使用pathRewrite;

    • secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false;

    • changeOrigin:它表示是否更新代理后请求的headers中host地址; eg:

    proxy: {
      "/api-webpack": {
        target: "http://localhost:8888",
        pathRewrite: {
          "^/api-webpack": "",
        },
        secure: false,
        changeOrigin: true,
      },
    },
    // http://localhost:8888/api-webpack/moment => http://localhost:8888/moment
  • historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误

    • boolean值:默认是false;如果设置为true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容;
    • object类型的值,可以配置rewrites属性: p可以配置from来匹配路径,决定要跳转到哪一个页面;
    // historyApiFallback: true
    historyApiFallback: {
      rewrites: [{ from: /xxx/, to: "/index.html" }],
    },
  • resolve模块解析

    • resolve可以帮助webpack从每个require/import、语句中,找到需要引入到合适模块的代码

    • webpack使用enhanced-resolve来解析文件路径

    • webpack能解析文件的三种路径

      • 绝对路径: 由于已经获得文件的绝对路径,因此不需要再做进一步的解析
      • 相对路径:使用import或者require的资源文件所在的目录,被认为是上下文目录;在import/require中给定的相对路径会拼接此上下文路径来生成模块的绝对路径
      • 模块路径:在resolve.modules中指定的 所有目录检索模块(默认值是['node——modules'],所以默认会从node_modules中查找文件);我们可以通过设置别名的方式来替换初始模块路径
  • 确定是文件还是文件夹

    • 如果是一个文件:

      • 如果文件具有扩展名,则直接打包
      • 否则,将使用resolve.extendsions选项作为文件扩展名解析
    • 如果是文件夹

      • 会在文件夹中根据resolve.mainFiles配置选项中指定的文件顺序查找

        • resolve.mainFiles的默认值是['index']
        • 再根据resolve.extendsions来解析扩展名
  • extendsionsalias配置

    • extendsions是解析到文件的自动添加扩展名

      • 默认值是**['.wasm', '.mjs', '.js', '.json']**
      • 所以代码中想要添加加载.vue或者.jsx,或者ts等文件时,必须手动添加扩展名
    • 另外一个非常好用的功能是配置别名

      • 当我们项目的目录结构比较深的时候,很可能出现'../../../'等路径片段,此时我们可以使用路径别名
  resolve: {
    extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
    alias: {
      "@": path.resolve(__dirname, "./src"),
      pages: path.resolve(__dirname, "./src/pages"),
    },
  },

12.webpack的环境分离

  • 如何区分开发环境: 1.编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可 2.编写相同的一个入口配置文件,通过设置参数来区分他们
"scripts": {
    "build": "webpack --config ./config/webpack.prod.js",
    "serve": "webpack serve --config ./config/webpack.dev.js",
    
    "build2": "webpack --config ./config/webpack.common.js --env production",
    "serve2": "webpack serve --config ./config/webpack.common.js --env development"
  },

项目结构

├─build 
├─config 
│ ├─webpack.env.conf.js
│ ├─webpack.comm.conf.js
│ └─webpack.prod.conf.js
├─src 
│ ├─page
│ ├─react_index.js 
│ └─index.js 
├─index.html
├─babel.config.js
├─package-lock.json
└─package.json
  • 入口文件解析

    • 之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,那么是否应该变成 ../src/index.js呢

      • 如果这样编写,会发现是报错的,依然要写成 ./src/index.js;
      • 这是因为入口文件其实是和另一个属性时有关的 context
    • context的作用是用于解析入口(entry point)和加载器(loader):

      • 官方说法:默认是当前路径(但是经过测试,默认应该是webpack的启动目录)

      • 另外推荐在配置中传入一个值;path.resolve()

// context是配置文件所在的目录
modules.exports = {
    context: path.resolve(__dirname, "./"),
    entry: "../src/index.js"
}
// context是上一个目录
modules.exports = {
    context: path.resolve(__dirname, "./"),
    entry: "./src/index.js"
}

接下来查看每个配置文件对应的内容吧

  • path.js
const path = require('path');
// node中的api -> 用于获取当前命令执行的路径
const appDir = process.cwd();
const resolveApp = (relativePath) => path.resolve(appDir, relativePath);
module.exports = resolveApp;
  • webpack.common.js
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const { merge } = require("webpack-merge");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");

const commonConfig = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: resolveApp("./build"),
  },
  resolve: {
    extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
    alias: {
      "@": resolveApp("./src"),
      pages: resolveApp("./src/pages"),
    },
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        use: "babel-loader",
      },
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
      {
        test: /\.css/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new VueLoaderPlugin(),
  ]
};

module.exports = function(env) {
  const isProduction = env.production;
  process.env.NODE_ENV = isProduction ? "production": "development";

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig, config);

  return mergeConfig;
};
  • webpack.dev.js
const resolveApp = require('./paths');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isProduction = false;

console.log("加载devConfig配置文件");

module.exports = {
  mode: "development",
  devServer: {
    hot: true,
    hotOnly: true,
    compress: true,
    contentBase: resolveApp("./why"),
    watchContentBase: true,
    proxy: {
      "/api-webpack": {
        target: "http://localhost:8888",
        pathRewrite: {
          "^/api-webpack": ""
        },
        secure: false,
        changeOrigin: true
      }
    },
    historyApiFallback: {
      rewrites: [
        {from: /abc/, to: "/index.html"}
      ]
    }
  },
  plugins: [
    // 开发环境
    new ReactRefreshWebpackPlugin(),
  ]
}
  • webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;

module.exports = {
  mode: "production",
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
  ]
}
  • babel.config.js
const presets = [
  ["@babel/preset-env"],
  ["@babel/preset-react"],
];
const plugins = [];
const isProduction = process.env.NODE_ENV === "production";

// React HMR -> 模块的热替换 必然是在开发时才有效果
if (!isProduction) {
  plugins.push(["react-refresh/babel"]);
} else {

}
module.exports = {
  presets,
  plugins
}

13.webpack的代码分离

  • 代码分离(Code Splitting)是webpack一个非常重要的特性

    • 主要目的是将代码分离到不同的bundle当中,之后可以按需加载或者并行加载这些文件
    • 比如在默认的情况下,所有的JavaScript代码(业务代码,第三方依赖,暂时没有用到的别的模块)在首页全部都加载,就会影响首页的加载速度
    • 代码分离可以分出更小的bundle,以及控制资源加载的优先级,提供代码的加载性能
  • webpack中代码分离常用的三种

    • 入口起点:使用entry配置手动分离代码
    • 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和代码分离
    • 动态导入:通过模块的内联函数调用来分离代码
  • 多入口起点:entry配置多个入口

  entry: {
    main: "./src/main.js",
    index: "./src/index.js",
  }
  • Entry Dependencies(入口依赖):假如我们的indx.js和main.js都依赖两个库:lodash,day.js,如果我们单纯的进行入口分离,那么打包后的两个bundle都会有一份lodash和day.js而事实上我们是可以将他们共享的。
  entry: {
    main: { import: "./src/main.js", dependOn: "shared" },
    index: { import: "./src/index.js", dependOn: "shared" },
    shared: ["lodash", "dayjs"]
  }
  • SplitChunks:他是使用SplitChunksPlugin来实现的

    • 该插件webpack已经默认集成,所以不需要再次安装
    • 只需要提供SplitChunksPlugin相关的配置信息即可
  • SplitChunks自定义配置解析 传送门

    • chunks:默认值async仅对异步导入的代码进行处理;initial:仅对同步导入的代码进行处理;all:对同步和异步的代码均进行处理

    • minSize:拆分包的大小至少为minSize,如果一个包拆分出来达不到minSize,那么这个包就不会拆分

    • maxSize:将大于maxSize的包,拆分为不小于minSize的包

    • minChunks:至少被引入的次数,默认是1,如果设置的值为2而引入的次数为1则不会单独拆分

    • name:设置拆包的名称,可以设置为false,设置为false后需要在cacheGroups中设置名称

    • cacheGroups:对拆分的包进行分组,比如一个lodash在拆分后并不会立即打包,而是会等到有没有其他符合规则的包来一起打包

      • test属性:匹配符合规则的包
      • name属性:拆分包的名称
      • filename属性:拆分包的名称,可以使用placeholder属性
      • priority属性:一个模块可以属于多个缓存组。优化将优先考虑具有更高 priority(优先级)的缓存组。默认组的优先级为负,以允许自定义组获得更高的优先级(自定义组的默认值为 0
  • 动态导入(dynamic import)

    • 另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式

      • 使用ECMAScript中的import()语法来完成,也是目前推荐的方式
      • webpack遗留的require.ensure,目前已经不推荐使用
    • 比如我们有一个模块bar.js

      • 该模块我们希望在代码运行中来加载它(比如一个判断条件成立时才加载)
      • 因为我们不确定这个模块中的代码一定会用到,所以最好拆分为一个独立的js文件
      • 这样可以保证不用此处的内容时,浏览器便不需要加载和处理该文件的js代码
      • 此时可以使用动态导入
    • 注意:使用动态导入bar.js

      • 在webpack中,通过动态导入一个对象
      • 真正的导出内容,在该对象的default属性中,所以需要做一个简单的解构
  • 动态导入的文件命名

    • 动态导入通常是一定会打包成为独立的文件的,所以并不会在cacheGroups中进行配置,那么他的命名通常会在output中,通过chunkFilename属性来命名

        output: {
          path: resolveApp("./build"),
          filename: "[name].bundle.js",
          chunkFilename: "[name].[hash:6].chunk.js",
        }
      
    • 但是咋们会发现默认情况下我们获取到的[name]是和id的名称保持一致的,此时如果我们希望修改name的值,可以通过magic comments(魔法注释) 的方式:

      import(/* webpackChunkName: "bar" */ "./bar").then(({default: bar}) => {
          console.log(bar)
      });
      
  • chunkIds配置:告知 webpack模块的id需要使用哪种算法,三种常见的配置如下

    • natural:按照数字的顺序使用id
    • named:development下的默认值,一个可读的名称的id
    • deterministic:确定性的,在不同的编译中不变的短数字id(在webpack中是没有这个值的;那个时候如果使用natual,那么在一些编译发生变化时,就会有问题)
    • 开发过程中推荐使用named;打包过程中推荐使用deterministic
  • runtimeChunk配置:

    • 配置runtime相关的代码是否抽取到一个单独的chunk中

      • runtime相关的代码值指的是在运行环境中,对模块进行解析,加载 ,模块信息相关的代码
      • 咋们的代码通过import函数相关的代码加载,就是通过runtime代码完成的
    • 抽离出来后有利于浏览器的缓存策略

      • 比如我们修改了业务代码(main),那么runtime和component,bar,chunk是不需要重新加载的
      • 比如我们修改了component,bar中的代码那么main中的代码时不需要重新加载的
    • 设置的值

      • true/multiple:针对每个入口打包一个runtime文件
      • single:打包一个runtime文件
      • 对象:name属性决定runtimeChunk的名称

代码展示主要配置模块

  output: {
    path: resolveApp("./build"),
    filename: "[name].bundle.js",
    chunkFilename: "[name].[hash:6].chunk.js",
  },
  // natural: 使用自然数(不推荐),
  // named: 使用包所在目录作为name(在开发环境推荐)
  // deterministic: 生成id, 针对相同文件生成的id是不变
  // chunkIds: "deterministic",
  optimization: {
    // 对代码进行压缩相关的操作
    minimizer: [
      new TerserPlugin({
        extractComments: false,
      }),
    ],
    splitChunks: {
      chunks: "all",
      minSize: 20000,
      maxSize: 20000,
      minChunks: 1,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          filename: "[id]_vendors.js",
          priority: -10,
        },
        default: {
          minChunks: 2,
          filename: "common_[id].js",
          priority: -20,
        },
      },
    },
    runtimeChunk: {
      name: function(entrypoint) {
        return `webpack-${entrypoint.name}`;
      },
    },
  }
  • PrefetchPreload
    • 在声明import时,使用下面的这些内置指令,来告知浏览器

      • prefetch(预获取):将来某些导航下可能需要的资源
      • preload(预加载):当前导航下可能需要的资源
    • 与prefetch指令相比,preload指令有许多不同之处

      • preload chunk会在父chunk加载时,以并行方式开始加载。prefetch chunk会在父chunk加载结束后开始加载
      • preload chunk具有中等优先级,并且立即下载。prefetch chunk在浏览器闲置时下载
      • preload chunk会在父chunk中立刻请求,用于当下时刻。prefetch chunk会用于未来的某个时刻。
    • 用法--魔法注释: /* webpackPrefetch: true */ or /* webpackPreload: true */

14.CDN和shimming

  • 什么是CDN?

    CDN称之为内容分发网络(Content Delivery Network)

    它是指通过相互连接的网络系统,利用最靠近每个用户的服务器

    更快,更可靠的将静态资源发送给用户

    来提供高性能,可扩展性及低成本的网络内容传递给用户

  • 研发中,使用CDN的主要两种方式

    • 打包所有的静态资源,放到CDN服务器,用户所有资源都是通过CDN服务器加载的
    • 一些第三方资源放到CDN服务器上
  • webpack中如何使用CDN

    • 可以直接修改webpack的配置publicPath publicPath: "https://xxx.com/cdn",
  • 第三方库的CDN服务器

    项目中如何引入第三方的CDN? 打包时候,我们不需要对类似于lodash或者dayjs这些库进行打包 在html模块中,我们需要自己加入对应的CDN服务器地址

  <!-- ejs中的if判断 -->
  <% if (process.env.NODE_ENV === 'production') { %> 
  <script src="https://unpkg.com/dayjs@1.8.21/dayjs.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <% } %> 
  
  externals: {
    // window._
    lodash: "_",
    // window.dayjs
    dayjs: "dayjs"
  },
  • shimming:垫片,相当于给我们的代码填充一些垫片来处理一些问题,比如现在的项目中依赖一个第三方的库,这个第三方的库本身依赖lodash,但是默认没有对lodash进行导入(认为全局存在lodash),那么我们就可以通过ProvidePlugin来实现shimming的效果

  • 注意:webpack并不推荐随意的使用shimming

    • webpack背后的整个理念是使前端开发更加的模块化
    • 也就是说,需要编写具有封闭性的,不存在隐含依赖(比如全局变量)彼此隔离的模块
  • 可以使用ProvidePlugin来实现shimming的效果

    • ProvidePlugin能够帮助我们在每个模块中,通过一个变量来获取一个package
    • 如果webpack看到这个模块,它将在最终的bundle中引入这个模块;
    • 另外ProvidePlugin是webpack默认的一个插件,所以不需要专门导入
  // 当在代码中遇到某一个变量找不到时, 我们会通过ProvidePlugin, 自动导入对应的库
   new webpack.ProvidePlugin({
     axios: "axios",
     get: ["axios", "get"]
   })

15.MiniCssExtractPlugin和Hash及DLL

  • MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中去,此插件需要在webpack4+才可以使用 如何配置?
  plugins: [
    // 生产环境
    new CleanWebpackPlugin({}),
    new MiniCssExtractPlugin({
      filename: "css/[name].[hash:8].css"
    })
  ]

// webpack.common.js
module: {
  rules: [
    {
      test: /\.css/i,
      // style-lodader -> development
      use: [
        isProduction ? MiniCssExtractPlugin.loader : "style-loader",
        "css-loader",
      ],
    },
  ],
},
  • Hash、ContentHash、ChunkHash

    在我们给打包的文件进行命名时,会使用placeholder,placeholder中有几个属性比较类似,hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制位)

    • hash值得生成和整个项目有关系

      • 假如现在有两个入口index.js和main.js
      • 他们分别会输出到不同的bundle文件中去,且在文件名中我们有使用hash
      • 这个时候如果更改了index.js文件中的内容,那么hash会发生变化
      • 那就意味着两个文件的名称都会发生变化
    • chunkhash可以有效地解决上面的问题,他会根据不同的入口进行解析来生成hash值

      • 比如我们修改了index.js,那么main.js的chunkhash是不会改变的
    • contenthash表示生成的文件hash名称,只和内容有关系

      • 比如我们的index.js引入了一个style.css,style.css又被抽取到一个独立的css文件中
      • 这个css文件在命名时,如果我们使用的是chunkhash
      • 那么当index.js文件的内容发生变化时,css文件的命名也会发生变化
      • 此时我们可以使用contenthash
  • DLL

    • DLL全称是动态链接库(Dynamic Link Library),是为软件在windows中实现共享函数库的一种实现方式

    • 那么webpack中也有内置DLL的功能,他指的是我们可以将共享,并且不经常改变的代码,抽取成一个共享的库

    • 这个库在之后的编译过程中,会被引入到其他项目的代码中

    • DLL库的使用分为两步

      • 打包一个DLL库
      • 项目中引入DLL库
    • 注意: 在升级webpack4之后,React和Vue脚手架都移除了DLL库(webpack4已经提供了足够的性能,不再需要DLL)

    • 如何打包一个DLL库?

      webpack帮助我们内置了一个DllPlugin可以帮助我们打包一个DLL的库文件 项目结构

     ├─dll 
     │ ├─dll_react.js
     │ └─react.mainifest.json
     ├─webpack.dll.js
     ├─package-lock.json
     └─package.json
    
    • webpack.dll.js的配置
    const path = require('path');
    const webpack = require('webpack');
    const TerserPlugin = require('terser-webpack-plugin');
    module.exports = {
      entry: {
        react: ["react", "react-dom"]
      },
      output: {
        path: path.resolve(__dirname, "./dll"),
        filename: "dll_[name].js",
        library: 'dll_[name]'
      },
      optimization: {
        minimizer: [
          new TerserPlugin({
            extractComments: false
          })
        ]
      },
      plugins: [
        new webpack.DllPlugin({
          name: "dll_[name]",
          path: path.resolve(__dirname, "./dll/[name].manifest.json")
        })
      ]
    }
    
    • 使用打包的DLL库

      如果我们的打码中使用了react,react-dom,我们有配置splitChunks的情况下,他们会进行分包,打包到一个独立的chunk中

      • 但是现在我们有了dll-react,不再需要单独去打包它们,可以直接去引用dll-react即可

      • 第一步:通过DLLReferencePlugin插件告知我们要使用DLL库

      • 第二步:通过AddAssHtmlPlugin插件,将我们打包的DLL库引入到html模块中

      plugins: [
        new HtmlWebpackPlugin({
          template: "./index.html",
        }),
        new webpack.DllReferencePlugin({
          context: resolveApp("./"),
          manifest: resolveApp("./dll/react.manifest.json")
        }),
        new AddAssetHtmlPlugin({
          filepath: resolveApp('./dll/dll_react.js')
        })
      ],
      

16.webpack中terser的使用

  • Terser简介

    • Terser是一个JavaScript的解释器(parse),Mangler(绞肉机)/Compress(压缩机)的工具集
    • 早期使用uglify-js来压缩,丑化我们的JavaScript代码,但是木年前已经不再维护,并且不支持ES6+的语法
    • Terser是从uglify-es fork过来的,并且保留他从原来的大部分Api以及适配uglify-es和uglify-js@3等
    • 也就是说,Terser可以帮助我们压缩,丑化我们的代码,让我们的bundle变得更小
    • 安装:npm install terser -g or npm install terser
  • 命令行使用Terser

    • terser [input files] [options] eg: terser abc.js -o abc.min.js -c -m
  • Compress和Mangle的options

    • compress options:

      • arrows: class或者object中的函数转换成箭头函数
      • arguments:将函数中使用arguments[index]转换为对应的形参名称
      • dead_code:移出不可达代码(tree shaking)
      • 其他传送门
    • mangle options

      • toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换)
      • keep_classnames:默认值false,是否保持依赖的类名称
      • keep_fnames:默认值false,是否保持原来的函数名称 npx terser ./src/abc.js -o abc.min.js -c arrows,arguments=true,dead_code -m toplevel=true,keep_classnames=true,keep_fnames=true
  • 如何在webpack中使用那个Terser

    在实际开发中不需要手动的通过terser来处理代码,可以直接通过使用webpack来处理

    • 在webpack中有一个minimize属性在production模式下默认就是使用TerserPlugin来处理咋们的代码的

    • 如果对默认配置不满意,可以创建TerserPlugin的实例,并且覆盖相关的配置

    • 首先我们得打开minimiize让其对我们的代码进行压缩(默认production模式下已经打开了)

    • 其次我们可以在minimizer创建一个TerserPluginn

    • extractComments:默认值为true,表示将注释抽取到一个单独的文件中去

      • 在开发中我们不希望保留这个注释时可以设置为false
    • parallel:使用多进程并发运行以提高构建速度。 并发运行的默认数量: os.cpus().length - 1,可以设置自己的个数,但是实用默认值即可

    • terserOptions:设置我们的terser相关的配置

      • compress:设置压缩相关的选项
      • mangle: 设置丑化相关的选项,可以直接设置为true
      • toplevel:底层变量是否进行转换
      • keep_classnames:保留类名称
      • keep_fnames:保留函数名称
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
        extractComments: false,
        terserOptions: {
          compress: {
            arguments: true,
            dead_code: true,
          },
          mangle: true,
          toplevel: true,
          keep_classnames: true,
          keep_fnames: true,
        },
      }),
    ],
  },
  • CSS的压缩

    • CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;

    • CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin;

    • css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用); 如何使用?

    第一步:安装css-minimizer-webpack-plugin

    第二步:在optimization.minimizer中配置(或者plugis) new CssMinimizerPlugin(),

  • 什么是Scope Hoisting呢?

    • Scope Hoisting从webpack3开始增加的一个新功能;

    • 功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;

  • 默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:

    • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;

    • Scope Hoisting可以将函数合并到一个模块中来运行;

  • 使用Scope Hoisting非常的简单,webpack已经内置了对应的模块:

    • 在production模式下,默认这个模块就会启用;

    • 在development模式下,我们需要自己来打开该模块; new webpack.optimize.ModuleConcatenationPlugin()

17.Tree Shaking

什么是Tree Shaking呢?

  • Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code);

  • 最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一);

  • 后来Tree Shaking也被应用于其他的语言,比如JavaScript、Dart;

  • JavaScript的Tree Shaking:

    • 对JavaScript进行Tree Shaking是源自打包工具rollup;

    • 这是因为Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);

    • webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;

    • 在webpack4正式扩展了这个能力,并且通过 package.json的 sideEffects属性作为标记,告知webpack在编译时,哪里文件可以安全的删除掉;

    • webpack5中,也提供了对部分CommonJS的tree shaking的支持;

  • 如何在webpack中实现Tree Shaking?事实上webpack实现Tree shaking采用了两种不同方案

    • usedExports: 通过标记某些函数是否被使用,之后通过Terser来进行优化
    • sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用
  • usedExports

    • 将mode设置为development模式:

      • 为了可以看到 usedExports带来的效果,我们需要设置为 development 模式

      • 因为在 production 模式下,webpack默认的一些优化会带来很大额影响。

    • 设置usedExports为true和false对比打包后的代码:

      • 在usedExports设置为true时,会有一段注释:unused harmony export mul;

      • 这段注释的意义是什么呢?告知Terser在优化时,可以删除掉这段代码;

    • 这个时候,我们将 minimize设置true:

      • usedExports设置为false时,mul函数没有被移除掉;

      • usedExports设置为true时,mul函数有被移除掉;

    • 所以,usedExports实现Tree Shaking是结合Terser来完成的。

  • sideEffects

    • sideEffects用于告知webpack compiler哪些模块时有副作用的:

      • 副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
    • 在package.json中设置sideEffects的值:

      • 如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports;

      • 如果有一些我们希望保留,可以设置为数组;

    • 比如我们有一个format.js、style.css文件:

      • 该文件在导入时没有使用任何的变量来接受;

      • 那么打包后的文件,不会保留format.js、style.css相关的任何代码;

      sideEffects: [
          "format.js",
          "*.css"
      ]
      
  • webpack 中配置Tree Shaking(生产环境)

    • 在optimization中配置usedExports为true,来帮助Terser进行优化;

    • 在package.json中配置sideEffects,直接对模块进行优化;

  • CSS中的Tree Shaking

    上面的都是关于JavaScript的Tree Shaking,那么CSS是否也可以进行Tree Shaking呢?

    • CSS的Tree Shaking需要借助于一些其他的插件;

      • 在早期的时候,我们会使用PurifyCss插件来完成CSS的tree shaking,但是目前该库已经不再维护了

      • 目前我们可以使用另外一个库来完成CSS的Tree Shaking:PurgeCSS,也是一个帮助我们删除未使用的CSS的工具;

      • 安装PurgeCss的webpack插件: npm install purgecss-webpack-plugin -D

  • 配置PurgeCss(生产环境)

    • paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;

    • 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;

    • purgecss也可以对less文件进行处理(所以它是对打包后的css进行tree shaking操作);

    new PurgeCssPlugin({
      paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
      safelist: function() {
        return {
          standard: ["body", "html"]
        }
      }
    })
    

18.webpack中代码压缩

  • 什么是HTTP压缩

    • HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式;
  • HTTP压缩的流程什么呢?

    第一步:HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)

    第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;

    第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;

  • 目前的压缩格式

    • compress – UNIX的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzip或deflate);

    • deflate – 基于deflate算法(定义于RFC 1951)的压缩,使用zlib数据格式封装;

    • gzip – GNU zip格式(定义于RFC 1952),是目前使用比较广泛的压缩算法;

    • br – 一种新的开源压缩算法,专为HTTP内容的编码而设计;

  • webpack 对文件进行压缩

    • webpack中相当于是实现了HTTP压缩的第一步操作,我们可以使用CompressionPlugin。

    • 第一步,安装CompressionPlugin:npm install compression-webpack-plugin -D

    • 第二步,使用CompressionPlugin即可

    new CompressionPlugin({
      test: /\.(css|js)$/i, // 匹配哪些文件需要压缩
      threshold: 0,         // 从文件多大时开始压缩
      minRatio: 0.8,        // 至少的压缩比例
      algorithm: "gzip",    // 采用的压缩算法
      // exclude
      // include
    }),
    
  • HTML中对代码进行压缩

    • 之前使用了HtmlWebpackPlugin插件来生成HTML的模板,事实上它还有一些其他的配置:

      • inject:设置打包的资源插入的位置:true、 false 、body、head

      • cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)

      • minify:默认会使用一个插件html-minifier-terser

      new HtmlWebpackPlugin({
        template: "./index.html",
        // inject: "body"
        cache: true, // 当文件没有发生任何改变时, 直接使用之前的缓存
        minify: isProduction ? {
          removeComments: false, // 是否要移除注释
          removeRedundantAttributes: false, // 是否移除多余的属性
          removeEmptyAttributes: true, // 是否移除一些空属性
          collapseWhitespace: false, // 折叠空格
          removeStyleLinkTypeAttributes: true, // 比如link中的 type="text/css"
          minifyCSS: true, // 是否压缩CSS
          minifyJS: {
            mangle: {
              toplevel: true
            }
          }
        } : false
      })
    
  • InlineChunkHtmlPlugin可以辅助将一些chunk出来的模块,内联到html中:

    • 比如runtime的代码,代码量不大,但是是必须加载的;

    • 那么我们可以直接内联到html中;

  • 这个插件是在react-dev-utils中实现的,所以我们可以安装一下它:

    npm install react-dev-utils -D

  • 在production的plugins中进行配置:

    new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/,])

19.自定义Loader

  • 什么是Loader?

    Loader是用于对模块的源代码进行转换(处理),之前我们已经使用过很多的Loader,如css-loader,style-loader等

  • 如何定义自己的Loader?

    Loader本质上是一个导出为函数的JavaScript模块;Loader runner库会调用这个函数,然后将上一个loader产生的结果或者是资源问价传进去

  • 编写一个my-loader模块,这个函数会接收三个参数

    content:资源文件的内容

    map:sourcemap相关的数据

    meta:一些元数据

    module.exports = function(content, map, meta) {
        return content
    }
    
  • 在加载某个模块时引入loader:传入的路径和context是有关系的

    {
        test: /\.js$/i,
        use: ["./loaders/myLoader.js"],
    },
    
  • 如果我们依然希望可以直接去加载自己的loader文件夹则可以配置resolveLoader属性

    resolveLoader: {
        modules: ["node_modules", "./loaders"],
    },
    
  • 为什么loader的执行顺序是相反的?

    • run-loader先优先执行PitchLoader,在执行PitchLoader时进行loaderIndex++;

    • run-loader之后会执行NormalLoader,在执行NormalLoader时进行loaderIndex--;

  • 如何改变它们的执行顺序呢?

    • 我们可以拆分成多个Rule对象,通过enforce来改变它们的顺序;
  • enforce一共有四种方式:

    • 默认所有的loader都是normal;

    • 在行内设置的loader是inline(或者import 'loader1!loader2!./test.js');

    • 也可以通过enforce设置 pre 和 post;

  • PitchingNormal它们的执行顺序分别是:

    • post, inline, normal, pre;

    • pre, normal, inline, post;

  • 同步的Loader

    • 什么是同步的Loader呢?

      • 默认创建的Loader就是同步的Loader;

      • 这个Loader必须通过 return 或者 this.callback 来返回结果,交给下一个loader来处理;

      • 通常在有错误的情况下,我们会使用 this.callback;

    • this.callback的用法如下:

      • 第一个参数必须是 Error 或者 null;

      • 第二个参数是一个 string或者Buffer;

      module.exports = function(content) {
        this.callback(null, content)
      }
      
  • 异步的Loader

    • 什么是异步的Loader呢?

      • 有时候我们使用Loader时会进行一些异步的操作;

      • 我们希望在异步操作完成后,再返回这个loader处理的结果;这个时候我们就要使用异步的Loader了;

    • loader-runner已经在执行loader时给我们提供了方法,让loader变成一个异步的loader

      module.exports = function(content) {
        const callback = this.async()
        setTimeout(() => {
          callback(null, content)
        })
      }
      
  • 传入和获取参数

    • 在使用loader时,传入参数
    • 可以通过使用webpack官方提供的解析库loader-utils npm install loader-utils -D
    {
        test: /\.js$/,
        use: {
          loader: "my_loader",
          options: {
            name: "coder",
            age: 20
          }
        },
    },
    
    const { getOptions } = require("loader-utils");
    module.exports = function (content) {
      // 获取传入的参数:
      const options = getOptions(this);
      // 设置为异步的loader
      const callback = this.async();
      setTimeout(() => {
        console.log("传入的参数是:", options);
        callback(null, content);
      }, 2000);
    };
    
  • 参数校验: webpack官方提供的校验库 schema-utils

参数格式:

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "请输入您的名字"
    },
    "age": {
      "type": "number",
      "description": "请输入您的年龄"
    }
  }, 
  // 允许传入其他参数
  "additionalProperties": true
}
const { getOptions } = require("loader-utils");
const { validate } = require("schema-utils");
const schema = require("../my-schema/my-loader-schema.json");
module.exports = function (content) {
  const options = getOptions(this);
  validate(schema, options, {
    name: "my_loader",
  });
  const callback = this.async();
  setTimeout(() => {
    callback(null, content);
  }, 2000);
};
  • 实现一个自定义的babel-loader
const babel = require("@babel/core");
const { getOptions } = require("loader-utils");
module.exports = function(content) {
  // 0.设置为异步的loader
  const callback = this.async();
  // 1.获取传入的参数
  const options = getOptions(this);
  // 2.对源代码进行转换
  babel.transform(content, options, (err, result) => {
    if (err) {
      callback(err);
    } else {
      callback(null, result.code)
    }
  })
}
  • 实现一个loader,可以将md文件打包进html中

    如果想实现某些单词高亮的效果那么可以将highlight模块中的css样式引入入口函数中即可 import ".js/styles/default.css";

const marked = require('marked');
const hljs = require('highlight.js');

module.exports = function(content) {
  // 样式处理
  marked.setOptions({
    highlight: function(code, lang) {
      return hljs.highlight(lang, code).value;
    }
  })

  // md --> html
  const htmlContent = marked(content);
  const innerContent = "`" + htmlContent + "`";
  const moduleCode = `var code=${innerContent}; export default code;`
  return moduleCode;
} 

20.tapable的使用

  • webpack和tapable

    我们知道webpack有两个非常重要的类:Compiler和Compilation

    • 他们通过注入插件的方式,来监听webopack的所有生命周期

    • 插件的注入离不开各种各样的Hook,而他们的Hook是如何得到的呢?其实是创建了Tapable库中的各种Hook的实例 所以如果想要开发自定义插件,最好先了解一个库:Tapable

    • Table是官方编写维护的一个库

    • Tapable是管理着需要的Hook,这些Hook可以应用到我们的插件中去

  • Tapable中的Hook

image.png

  • Tapable中的Hook分类

    同步和异步的:

    • 以sync开头的,是同步的Hook;

    • 以async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调;

    其他的类别

    • bail:当有返回值时,就不会执行后续的事件触发了;

    • Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;

    • Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数;

    • Parallel:并行,会同时执行次事件处理回调结束,才执行下一次事件处理回调;

    • Series:串行,会等待上一是异步的Hook;

  • SyncHook的使用

const { SyncHook } = require("tapable");

class LearnTapable {
  constructor() {
    this.hooks = {
      syncHook: new SyncHook(["name", "age"]),
    };
    this.hooks.syncHook.tap("event1", (name, age) => {
      console.log("event1", name, age);
      return "event1";
    });
    this.hooks.syncHook.tap("event2", (name, age) => {
      console.log("event2", name, age);
    });
  }
  emit() {
    this.hooks.syncHook.call("coder", 18);
  }
}
const lt = new LearnTapable();
lt.emit();
// 输出:
// event1 coder 18 
// event2 coder 18
  • SyncBailHook的使用
const {  SyncBailHook } = require("tapable");
class LearnTapable {
  constructor() {
    this.hooks = {
      syncHook: new SyncBailHook(["name", "age"])
    };
    this.hooks.syncHook.tap("event1", (name, age) => {
      console.log("event1", name, age);
      return "event1";
    });
    this.hooks.syncHook.tap("event2", (name, age) => {
      console.log("event2", name, age);
    });
  }
  emit() {
    this.hooks.syncHook.call("coder", 18);
  }
}
const lt = new LearnTapable();
lt.emit();
// 输出:event1 coder 18
  • SyncLoopHook的使用:上述案列将不停的打印: event1 coder 18

  • SyncWaterfallHook的使用:上述案列将输出:event1 coder 18; event2 event1 18

  • AsyncSeriesHook的使用

const { AsyncSeriesHook } = require("tapable");
class LearnTapable {
  constructor() {
    this.hooks = {
      asyncHook: new AsyncSeriesHook(["name", "age"])
    };
    this.hooks.asyncHook.tapAsync("event1", (name, age, callback) => {
      setTimeout(() => {
        console.log("event1", name, age);
        callback();
      }, 2000);
    });

    this.hooks.asyncHook.tapAsync("event2", (name, age, callback) => {
      setTimeout(() => {
        console.log("event2", name, age);
        callback();
      }, 2000);
    });
  }
  emit() {
    this.hooks.asyncHook.callAsync("kobe", 30, () => {
      console.log("第一次事件执行完成");
    });
  }
}
const lt = new LearnTapable();
lt.emit();
// 输出:
// event1 kobe 30
// event2 kobe 30(两秒后)
// 第一次事件执行完成
  • AsyncParallelHook的使用:上述例子同时输出:

    event1 kobe 30 event2 kobe 30 第一次事件执行完成

21.自定义Plugin

  • webpack中的Plugin是如何注册到webpack的生命周期中?

    1.在webpack函数的createCompiler方法中注册了所有的插件

    2.在注册插件时,会调用插件函数或者插件对象的apply方法

    3.插件方法会接收compiler对象,我们可以通过compiler对象来注册Hook的事件

    4.某些插件也会传入一个compilation的对象,我们可以监听compilation的Hook事件

  • 开发自己的插件 首先需要了解compiler钩子传送门

22.Gulp工具

什么是Gulp?

A toolkit to automate & enhance your workflow -- 一个工具包,可以帮你自动化和增加你的工作流。

image.png

  • Gulp 和 Webpack

    • gulp的核心理念是task runner

      可以定义自己的一系列任务,等待任务被执行;

      基于文件Stream的构建流;

      我们可以使用gulp的插件体系来完成某些任务;

    • webpack的核心理念是module bundler

      webpack是一个模块化的打包工具;

      可以使用各种各样的loader来加载不同的模块;

      可以使用各种各样的插件在webpack打包的生命周期完成其他的任务;

    • gulp相对于webpack的优缺点:

      gulp相对于webpack思想更加的简单、易用,更适合编写一些自动化的任务;

      但是目前对于大型项目(Vue、React、Angular)并不会使用gulp来构建,比如默认gulp是不支持模块化的;

  • gulp的基本使用

    • 首先需要安装gulp

      全局安装 npm install -g gulp 局部安装npm install gulp

    • 编写gulpfile.js, 在里面编写task

    export.foo = function(cb) {
        console.log('foo task running')
        returnn src('../src/js/*.js')
    }
    
    • 执行 npx gulp foo
  • 创建gulp任务

    • 每个gulp任务都是一个异步的JavaScript函数

      • 此函数可以接受一个callback作为参数,调用callback函数那么任务会结束
      • 或者是返回一个stream,promise,event emitter,child process或observable类型的函数
    • 任务可以是public或者private类型的

      • 公开任务(public tasks): 从gulpfile中被导出(export),可以通过gulp命令直接调用

      • 私有任务(private tasks):被设计为在内部使用,通常作为series()或者parallel()组合的组成部分

      • 默认任务: 执行命令(npx gulp

        module.exports.default = (cb) => {
          console.log("default task");
          cb();
        }
        
      • 补充:gulp4之前,注册任务通过gulp.task的方式进行

        gulp.task("bar", (cb) => {
          console.log("bar");
          cb();
        })
        
  • 任务组合series()parallel()

    • series: 串行任务组合

    • parallel:并行任务组合

    • 他们都可以接受任意数量的任务函数或者已经组合的操作

      // 多个任务的串行执行
      const seriesTask = series(task1, task2, task3);
      // 多个任务的并行执行
      const parallelTask = parallel(task1, task2, task3);
      // 再次组合
      const composeTask = series(parallelTask, seriesTask);
      module.exports = {
        seriesTask,
        parallelTask,
        composeTask,
      };
      

三个task的运行结果: image.png

  • gulp暴露了src() 和 **dest()**方法用于处理计算机上存放的文件

    • src()接受参数,并从文件系统中读取文件然后生成一个Node流(Stream),他将所有的匹配的文件读取到内存中并通过流(Stream)进行处理
    • 由src()产生的流应当从任务(task函数)中返回并发出异步完成的信号
    • dest()接受一个输出目录作为参数,并且他还会产生一个Node流(Stream),通过该流将内容输出到文件中
    const jsTask = () => {
      // 从src中读取文件, 输出到dist文件夹中
      return src("./src/**/*.js")
        .pipe(dest("./dist"));
    };
    
  • 流(stream)所提供的主要的API是 .pipe() 方法,pipe方法的原理是什么呢?

    • pipe方法接收一个转换流(Transform streams)可写流(Writable streams)
    • 那么转换流或者可写流,拿到数据之后可以对数据进行处理,再次传递给下一个转换流或者可写流
  • 对文件进行转换

    • 如果在这个过程中,我们希望对文件进行某些处理,可以使用社区给我们提供的插件。

    • 比如我们希望ES6转换成ES5,那么可以使用babel插件;

    • 如果我们希望对代码进行压缩和丑化,那么可以使用uglify或者terser插件;

      const jsTask = () => {
        // 从src中读取文件, 输出到dist文件夹中
        return src("./src/**/*.js")
          .pipe(babel({ presets: ["@babel/preset-env"] }))
          //.pipe(uglify())
          .pipe(terser({ mangle: { toplevel: true } }))
          .pipe(dest("./dist"));
      };
    
  • glob文件匹配

    • src()方法接收一个glob字符串或由多个glob字符串组成的数组作为参数,用于确定哪些文件需要被操作

      • glob或glob数组必须至少匹配到一个匹配项,否则src()将报错
    • glob的匹配规则如下

      • (一个星号*):在一个字符串中,匹配任意数量的字符,包括0个匹配 *.js

      • (两个星号*):在多个字符串匹配中,匹配任意数量的字符串,通常用在匹配目录下的文件 script/**/*.js

      • (取反!): ['script/**/*.js', '!scripts/vender/']

        • 由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的;

        • 所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面;

        • 第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分

  • gulp的文件监听

    gulp api 中的 watch() 方法利用文件系统的监控程序(file system watcher)将 与进行关联。

    const jsTask = () => {
      // 从src中读取文件, 输出到dist文件夹中
      return src("./src/**/*.js")
        .pipe(babel({ presets: ["@babel/preset-env"] }))
        // .pipe(uglify())
        .pipe(terser({ mangle: { toplevel: true } }))
        .pipe(dest("./dist"));
    };
    
    watch("./src/**/*.js", jsTask);
    

Gulp案列:编写案例,通过启动gulp来开启本地服务和打包

  • 打包html文件
const htmlTask = () => {
  // 增加base字段可以读取文件所在的目录--实现打包后放置在dist/xxx/xxx.html的效果
  return src("./src/*.html", {base: "./src"})
    .pipe(htmlMin({
      collapseWhitespace: true
    }))
    .pipe(dest("./dist"))
}
  • 打包JavaScript文件
const jsTask = () => {
  return src("./src/js/**.js", { base: "./src" })
    .pipe(babel({ presets: ["@babel/preset-env"] }))
    .pipe(terser({ mangle: { toplevel: true } }))
    .pipe(dest("./dist"));
};
  • 打包less文件
const lessTask = () => {
  return src("./src/css/*.less", { base: "./src" })
    .pipe(less())
    .pipe(postcss([postcssPresetEnv()]))
    .pipe(dest("./dist"));
};
  • html文件资源注入
const injectHtml = () => {
  return src("./dist/*.html")
    .pipe(
      inject(src(["./dist/js/*.js", "./dist/css/*.css"]), { relative: true })
    )
    .pipe(dest("./dist"));
};
  • 删除生成的目录
const clean = () => {
  return del(["dist"]);
};
  • 搭建本地服务器
const bs = browserSync.create();
const serve = () => {
  watch("./src/*.html", series(htmlTask, injectHtml));
  watch("./src/js/*.js", series(jsTask, injectHtml));
  watch("./src/css/*.less", series(jsTask, lessTask));

  bs.init({
    port: 8080,
    open: true,
    files: "./dist/*",
    server: {
      baseDir: "./dist",
    },
  });
};
  • 创建打包任务
const buildTask = series(clean, parallel(htmlTask, jsTask, lessTask), injectHtml);
  • 创建开发任务
const serveTask = series(buildTask, serve);

所有代码:

const { src, dest, watch, series, parallel } = require("gulp");

const htmlMin = require("gulp-htmlmin");
const babel = require("gulp-babel");
const terser = require("gulp-terser");
const less = require("gulp-less"); // less
const postcss = require("gulp-postcss"); // postcss
const postcssPresetEnv = require("postcss-preset-env");
const inject = require("gulp-inject");

const browserSync = require("browser-sync");

const del = require("del");

const htmlTask = () => {
  return src("./src/*.html", { base: "./src" })
    .pipe(
      htmlMin({
        collapseWhitespace: true,
      })
    )
    .pipe(dest("./dist"));
};

const jsTask = () => {
  return src("./src/js/**.js", { base: "./src" })
    .pipe(babel({ presets: ["@babel/preset-env"] }))
    .pipe(terser({ mangle: { toplevel: true } }))
    .pipe(dest("./dist"));
};

const lessTask = () => {
  return src("./src/css/*.less", { base: "./src" })
    .pipe(less())
    .pipe(postcss([postcssPresetEnv()]))
    .pipe(dest("./dist"));
};

const injectHtml = () => {
  return src("./dist/*.html")
    .pipe(
      inject(src(["./dist/js/*.js", "./dist/css/*.css"]), { relative: true })
    )
    .pipe(dest("./dist"));
};

// 搭建本地服务器
const bs = browserSync.create();
const serve = () => {
  watch("./src/*.html", series(htmlTask, injectHtml));
  watch("./src/js/*.js", series(jsTask, injectHtml));
  watch("./src/css/*.less", series(jsTask, lessTask));

  bs.init({
    port: 8080,
    open: true,
    files: "./dist/*",
    server: {
      baseDir: "./dist",
    },
  });
};

const clean = () => {
  return del(["dist"]);
};

const buildTask = series(
  clean,
  parallel(htmlTask, jsTask, lessTask),
  injectHtml
);
const serveTask = series(buildTask, serve);

module.exports = {
  serveTask,
  buildTask,
};

package.json

{
  "name": "gulp_demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "serve": "gulp serveTask",
    "build": "gulp buildTask"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.13.10",
    "@babel/preset-env": "^7.13.10",
    "browser-sync": "^2.26.14",
    "del": "^6.0.0",
    "gulp": "^4.0.2",
    "gulp-babel": "^8.0.0",
    "gulp-htmlmin": "^5.0.1",
    "gulp-inject": "^5.0.5",
    "gulp-less": "^4.0.1",
    "gulp-postcss": "^9.0.0",
    "gulp-terser": "^2.0.1",
    "postcss": "^8.2.8",
    "postcss-preset-env": "^6.7.0"
  }
}

23.rollup工具

我们来看一下官方对 rollup 的定义:

  • Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.

    Rollup 是一个 JavaScript 的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序;

  • 我们会发现 Rollup 的定义、定位和 webpack 非常的相似:

    • Rollup 也是一个模块化的打包工具,但是 Rollup 主要是针对 ES Module 进行打包的;
    • 另外 webpack 通常可以通过各种 loader 处理各种各样的文件,以及处理它们的依赖关系;
    • rollup 更多时候是专注于处理 JavaScript 代码的(当然也可以处理 css、font、vue 等文件);
    • 另外 rollup 的配置和理念相对于 webpack 来说,更加的简洁和容易理解;
    • 在早期 webpack 不支持 tree shaking 时,rollup 具备更强的优势;
  • 目前 webpack 和 rollup 分别应用在什么场景呢?

    • 通常在实际项目开发过程中,我们都会使用 webpack(比如 vue、react、angular 项目都是基于 webpack 的);
    • 在对库文件进行打包时,我们通常会使用 rollup(比如 vue、react、dayjs 源码本身都是基于 rollup 的);
  • rollup的基本使用 1.首先安装rollup: 全局安装: npm install rollup -g 局部安装: npm install rollup -D

2.创建main.js打包到bundle.js中去

打包浏览器的库:npx rollup ./src/main.js -f iife -o ./dist/bundle.js

打包AMD的库:npx rollup ./src/main.js -f amd -o ./dist/bundle.js

打包CommonJS的库:npx rollup ./src/main.js -f cjs -o ./dist/bundle.js

打包通用的库(name必填):npx rollup ./src/main.js -f umd --name xxxx -o ./dist/bundle.js

  • rollup配置文件

我们开发时可以将配置文件写在rollup.config.js中去

export default {
  input: "./src/main.js",
  output: {
    format: "cjs",
    file: "dist/util.common.js",
  },
};

我们可以对文件进行分别打包,打包出更多的库文件(用户可以按需引入)

  output: [
    {
      format: "umd",
      name: "myUtils",
      file: "dist/util.umd.js",
    },
    {
      format: "cjs",
      file: "dist/util.commonjs.js",
    },
    {
      format: "amd",
      file: "dist/util.amd.js",
    },
    {
      format: "es",
      file: "dist/util.es.js",
    },
    {
      format: "iife",
      name: "myUtils",
      file: "dist/util.browser.js",
    },
  ],
  • 解决commonjs和第三方库的问题

    • 安装解决common.js的库:npm install @rollup/plugin-commonjs -D

    • 安装解决node_modules的库:npm install @rollup/plugin-node-resolve -D

    • 打包排除lodash

    • babel转换代码:如果我们希望将ES6转成ES5的代码,可以在rollup中使用babel。 npm install @rollup/plugin-babel -D另外还需要配置babel.connfig.js

    • terser代码压缩:npm install rollup-plugin-terser -D

    • 处理css文件:npm install rollup-plugin-postcss postcss -D

    • 处理vue文件:npm install rollup-plugin-vue@4.6.1 vue-template-compiler -D(默认情况下安装的是vue2.X的版本,所以在此指定了rollup-plugin-vue版本)

    • 打包vue后会报错:process is not defined 这是因为在我们打包的vue代码中,用到 process.env.NODE_ENV,所以我们可以使用一个插件 rollup-plugin-replace 设置它对应的值:npm install rollup-plugin-replace -D

    • 搭建本地服务器

      • 第一步: 使用rollup-plugin-serve搭建服务
      • 第二步:当文件发生变化时,自动刷新浏览器
      • 第三步:启动时,开启文件监听命令 npx rollup -c -w
    • 区分开发环境

      "scripts": {
        "build": "rollup -c --environment NODE_ENV:production",
        "serve": "rollup -c --environment NODE_ENV:development -w"
      }

rollup.config.js

    import commonjs from "@rollup/plugin-commonjs";
    import resolve from "@rollup/plugin-node-resolve";
    import babel from "@rollup/plugin-babel";
    import { terser } from "rollup-plugin-terser";
    import postcss from "rollup-plugin-postcss";
    import vue from "rollup-plugin-vue";
    import replace from "rollup-plugin-replace";
    import serve from "rollup-plugin-serve";
    import livereload from "rollup-plugin-livereload";
    const isProduction = process.env.NODE_ENV === "production";
    const plugins = [
      // 解决common.js的问题
      commonjs(),
      // 解决node_modules的问题
      resolve(),
      // 处理process问题
      replace({
        "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
      }),
      babel({
        babelHelpers: "bundled",
      }),
      // 处理css文件
      postcss(),
      vue(),
    ];
    if (isProduction) {
      // terser代码压缩
      plugins.push(terser());
    } else {
      const devPlugins = [
        serve({
          // open: true, // 是否打开浏览器
          port: 8080, // 监听哪一个端口
          contentBase: ".", // 服务哪一个文件夹
        }),
        livereload(),
      ];
      plugins.push(...devPlugins);
    }
    export default {
      input: "./src/main.js",
      output: {
        format: "umd",
        name: "utils",
        file: "dist/util.umd.js",
        globals: {
          lodash: "_",
        },
      },
      // 打包排除lodash
      external: ["lodash"],
      plugins
    };

24.Vite工具

  • 什么是vite呢?

    官方的定位:下一代前端开发与构建工具;

  • 如何定义下一代开发和构建工具呢?

    我们知道在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等;

    所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel;

    但是随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多;

    构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来;所以也有这样的说法:天下苦webpack久矣;

  • Vite (法语意为 "快速的",发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验。

  • vite的构造: 它主要由两部分组成:

    • 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;

    • 一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;

  • 安装vite: npm install vite -g

  • 通过vite来启动项目: npx vite

  • vite对css的支持

    • vite可以直接支持css的处理: 直接导入css即可
    • vite可以直接支持css预处理,比如less:直接导入less,之后安装less编译器
  • vite直接支持postcss的转换,只需要安装postcss,并且配置postcss.config.js的配置文件即可

  • vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:

    只需要直接导入即可;如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:这是因为vite中的服务器Connect会对我们的请求进行转发;获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析;

    • 注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器 传送门
  • vite对vue的支持

    Vue 3 单文件组件支持:@vitejs/plugin-vue

    Vue 3 JSX 支持:@vitejs/plugin-vue-jsx

    Vue 2 支持:underfin/vite-plugin-vue2

  • 安装支持Vue的插件npm install vite-plugin-vue2 -D

    • vue.config.js中配置插件
      import { createVuePlugin } from "vite-plugin-vue2"
      module.exports = {
        plugins: [
          createVuePlugin()
        ]
      }
      
  • Vite对React的支持

    .jsx 和 .tsx 文件同样开箱即用,它们也是通过 ESBuild来完成的编译:

    所以我们只需要直接编写react的代码即可;

    注意:在index.html加载main.js时,我们需要将main.js的后缀,修改为 main.jsx 作为后缀名

  • Vite 打包项目

    可以直接通过npx vite build来完成对当前项目的打包

    可以通过preview的方式,开启一个本地服务来预览打包后的效果: npx vite preview

  • Vite脚手架工具

    在开发中,我们不可能所有的项目都使用vite从零去搭建,比如一个react项目、Vue项目;

    这个时候vite还给我们提供了对应的脚手架工具;

  • 所以Vite实际上是有两个工具的:

    vite:相当于是一个构件工具,类似于webpack、rollup;

    @vitejs/create-app:类似vue-cli、create-react-app;

  • 如果使用脚手架工具呢?:npm init @vitejs/app

该写法相当于省略了安装脚手架的过程: npm install @vitejs/create-app -g create-app

  • ESBuild的特点:

    超快的构建速度,并且不需要缓存;

    支持ES6和CommonJS的模块化;

    支持ES6的Tree Shaking;

    支持Go、JavaScript的API;

    支持TypeScript、JSX等语法编译;

    支持SourceMap;

    支持代码压缩;

    支持扩展其他插件

  • ESBuild为什么这么快呢?

    使用Go语言编写的,可以直接转换成机器代码,而无需经过字节码;

    ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行;

    ESBuild的所有内容都是从零开始编写的,而不是使用第三方所以从一开始就可以考虑各种性能问题