webpack 学习笔记

339 阅读8分钟

基础

配置文件

初次打包体验

// 初始化项目
npm init -y

// 安装 webpack 依赖
npm i webpack webpack-cli -D

// cli 打包
npx webpack

// cli 参数
npx webpack --entry ./src/main.js  --output-path ./build

代码被视为 JavaScript 模块

  • 不支持 CommonJS
  • 兼容性不好,还是需要通过webapck构建。
<script src="..." type="module"></script>

npx 说明

  • 主要有以下特点:
    • 临时安装可执行依赖包,不用全局安装,不用担心长期的污染。
    • 可以执行依赖包中的命令,安装完成自动运行。
    • 自动加载 node_modules 中依赖包,不用指定 $PATH
    • 可以指定 node 版本、命令的版本,解决了不同项目使用不同
  • npx 执行流程如下:
    • 到 node_modules/.bin 路径检查对应的命令是否存在,找到之后执行;
    • 没有找到,就去环境变量 $PATH 里面,检查对应命令是否存在,找到之后执行;
    • 还是没有找到,自动下载一个临时的依赖包最新版本在一个临时目录,然后再运行命令,运行完之后删除,不污染全局环境。

配置文件 webpack.config.js

  • 根目录下创建一个webpack.config.js文件,来作为 webpack 的配置文件
const path = require("path");

module.exports = {
  entry: "./src/main.js",
  output: {
    filename: "js/index.js",
    // 必须是绝对路径
    path: path.resolve(__dirname, "./build"),
  },
};
  • 指定 webpack 配置文件 npx webpack --config wx.config.js

loader 基本使用

使用 loader 的方式

  • 配置方式
  • 内联方式

loader 执行顺序从右往左(从下往上)

打包 css

npm i style-loader css-loader -D

module: {
  rules: [
    {
      // 正则表达式
      test: /\.css$/i, // 匹配资源
      // loader: "css-loader",
      // use: [
      //   { loader: "css-loader" }
      // ],
      use: ["style-loader", "css-loader"],
    },
  ],
},
  • style-loader
    • 插入style标签(处理内联样式)
  • css-loader
    • 解析css文件
      解析css文件中的@import和url语句,处理css-modules,并将结果作为一个js模块返回

原理??

打包 less

npm i less less-loader -D

{
  test: /\.less$/i,
  use: ["style-loader", "css-loader", "less-loader"],
},
  • 可以通过 cli 使用 npx less ./src/css/title.less > title.css

游览器兼容性

查询浏览器市场占有率 caniuse

image.png

browserslist

  • 在不同的前端工具之间,共享目标浏览器Node.js版本的配置 postcss-prest-env、babel、autoprefixer
  • 通常其他包会附带安装,所以不需要安装 npx i browserslist -D
  • 命令行使用 npx browserslist ">1%, last 2 version, not dead"
  • 配置
    • package.json 文件中配置 "browserslist": [ "> 1%", "last 2 versions", "not dead" ]

    • 单独的一个配置文件 .browserslistrc defaults // Browserslist的默认浏览器 // > 0.5%, last 2 versions, Firefox ESR, not dead

      > 1% // 全球市场占有率 >1%
      last 2 versions // 每个浏览器的最后2个版本
      not dead // dead: 24个月内没有官方支持或更新的浏览器
      

PostCSS

什么是PostCSS呢?

  • PostCSS是一个通过JavaScript转换样式的工具;
  • 这个工具可以帮助我们进行一些CSS的转换和适配,比如自动添加浏览器前缀、css样式的重置
  • 但是实现这些工具,我们需要借助于PostCSS对应的插件;

命令行使用

npm i postcss postcss-cli autoprefixer -D

  • postcss
  • postcss-cli
    • 终端使用命令行(实际项目可以不用安装)
  • autoprefixer
    • 添加css前缀插件
    • postcss-preset-env 已经集成,所以使用的会比较少

npx postcss --use autoprefixer -o result.css ./src/css/style.css

webpack 使用

  • npm i postcss postcss-loader -D
  • 修改加载 css 的 loader
module: {
  rules: [
    {
      test: /\.css$/i,
      use: [
        "style-loader",
        "css-loader",
        {
          loader: "postcss-loader",
          options: {
            postcssOptions: {
              plugins: ["autoprefixer"],
            },
          },
        },
      ],
    },
  ],
},

单独的 postcss 配置文件

  • 在根目录下创建 postcss.config.js

  • npm i postcss-preset-env -D

    • 它可以帮助我们将一些现代的CSS特性,转成大多数浏览器认识的CSS,并且会根据目标浏览器或者运行时环境添加所需的polyfill
    • 也包括会自动帮助我们添加autoprefixer(所以相当于已经内置了autoprefixer
  • webpack.config.js

    • css-loader 中的 importLoaders 属性
    • 允许为 @import 样式规则设置在 CSS loader 之前应用的 loader 的数量,例如:@import ./test.css' 等
    module: {
      rules: [
        {
          test: /\.css$/i,
          use: [
            "style-loader",
            {
              loader: "css-loader",
              options: {
                importLoaders: 1,
              },
            },
            "postcss-loader",
          ],
        },
        {
          test: /\.less$/i,
          use: [
            "style-loader",
            {
              loader: "css-loader",
              options: {
                importLoaders: 2,
              },
            },
            "less-loader",
            "postcss-loader",
          ],
        },
      ],
    },
    
  • postcss.config.js

    module.exports = {
      plugins: [
        // require("postcss-preset-env"),
        "postcss-preset-env"
      ],
    };
    

打包图片资源

  • 引入图片的方式
    // import
    import tempImg from "../img/1.png";
    
    // require()
    // file-loader 6.x版本需要添加 .default
    imgEl.src = require("../img/1.png").default;
    
    // css
    background-image: url('../img/nhlt.jpg');
    

file-loader

  • 处理 import/require() 方式引入的一个文件资源
  • npm i file-loader -D
  • 文件的名称规则 placeholders
    {
      test: /\.(png|jpe?g|gif|svg)$/,
      use: [
        {
          loader: "file-loader",
          options: {
            // outputPath: 'img',
            name: "img/[name].[hash:8].[ext]",
          },
        },
      ],
    }
    

url-loader

  • file-loader 相似,但是可以将较小的文件,转成base64的URI
    • 小的图片转换base64之后可以和页面一起被请求,减少不必要的请求过程
    • 大的图片也进行转换,反而会影响页面的请求速度
  • npm i url-loader -D { test: /.(png|jpe?g|gif|svg)$/, use: [ { loader: "url-loader", options: { name: "img/[name].[hash:8].[ext]", limit: 100 * 1024, }, }, ], }

资源模块 asset

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

可以通过在 webpack 配置中设置 output.assetModuleFilename 来修改此模板字符串

{
  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]"
  }
}

认识 plugin

clean-webpack-plugin

清空打包目录的文件夹

  • npm i clean-webpack-plugin -D const { CleanWebpackPlugin } = require('clean-webpack-plugin');

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

html-webpack-plugin

创建 html 文件

  • npm i html-webpack-plugin -D

  • 默认情况下是根据在html-webpack-plugin的源码中,有一个default_index.ejs模块生成的 const HtmlWebpackPlugin = require('html-webpack-plugin');

    new HtmlWebpackPlugin({
      title: 'demo',
      template: './public/index.html',
    })
    

DefinePlugin

允许在 编译时 将你代码中的变量替换为其他值或表达式

  • webpack 内置插件
    const webpack = require("webpack");
    
    new webpack.DefinePlugin({
      BASE_URL: JSON.stringify('./'),
    })
    

copy-webpack-plugin

将单个文件或整个目录复制到生成目录

  • npm i copy-webpack-plugin -D
const CopyPlugin = require("copy-webpack-plugin");

new CopyPlugin({
  patterns: [
    {
      from: 'public',
      globOptions: {
        ignore: [ // 忽略项
          "**/index.html",
          "**/abc.txt",
        ],
      },
    }
  ],
})

进阶

source-map

什么是source-map

  • source-map是从已转换的代码映射到原始的源文件
  • 使浏览器可以重构原始源并在调试器中显示重建的原始源

如何使用

  1. 根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成source-map
  2. 在转换后的代码,最后添加一个注释,它指向sourcemap
    • //# sourceMappingURL=common.bundle.js.map

source-map值含义

下面几个值不会生成source-map

  • false
  • none(不写)
    • production 模式下的默认值
  • eval
    • 会在eval执行的代码中,添加 //# sourceURL=
    • 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码

生成source-map值

  • source-map
    • 生成一个独立的source-map文件,并且在bundle文件中有一个注释
      //# sourceMappingURL=bundle.js.map,指向source-map文件
    • 开发工具会根据这个注释找到source-map文件,并且解析
  • eval-source-map
    • source-map是DataUrl以添加到eval函数的后面
  • inline-source-map
    • source-map是以DataUrl添加到bundle文件的后面
  • hidden-source-map
    • 不会对source-map文件进行引用
    • 相当于删除了打包文件中对sourcemap的引用注释
    • 如果我们手动添加进来,那么sourcemap就会生效了
  • nosources-source-map
    • 只有错误信息的提示,不会生成源代码文件
  • cheap-source-map
    • 更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping)
  • cheap-module-source-map
    • 类似于cheap-source-map,但是对源自loader的sourcemap处理会更好
    • 如果loader对我们的源码进行了特殊的处理,比如babel

cheap-source-map和cheap-module-source-map的区别

  • cheap-module-source-map

  • babel转换后与源代码一致 image.png

  • cheap-source-map

  • babel转换后与源代码不一致

image.png

组合规则

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

最佳的实践

  • 开发、测试阶段
    • cheap-module-source-map
  • 发布阶段
    • false
    • (none)

Babel 深入解析

参考链接

Babel到底是什么

  • 微内核架构 @babel/core
  • Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的 JavaScript
  • 包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等

命令行使用

npm i @babel/core @babel/cli @babel/plugin-transform-arrow-functions @babel/plugin-transform-block-scoping -D

  • @babel/core:babel的核心代码,必须安装
  • @babel/cli:在命令行使用babel
  • @babel/plugin-transform-arrow-functions:转换箭头函数
  • @babel/plugin-transform-block-scoping:转换const为var

npx babel ./src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions

  • --out-dir
  • --plugins

Babel的预设preset

  • npm install @babel/preset-env -D
  • npx babel src --out-dir dist --presets=@babel/preset-env

底层原理

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

Babel也拥有编译器的工作流程:

  • 解析阶段(Parsing)
  • 转换阶段(Transformation)
  • 生成阶段(Code Generation)

image.png

Babel使用

  • npm i @babel/core babel-loader -D
  • 使用插件,安装依赖@babel/plugin-transform-arrow-functions
    @babel/plugin-transform-block-scoping
    {
      test: /\.m?js$/,
      use: {
        loader: "babel-loader",
        options: {
          plugins: [
            '@babel/plugin-transform-arrow-functions',
            '@babel/plugin-transform-block-scoping',
          ]
        }
      }
    }
    
  • 使用 presets,安装依赖@babel/preset-env
  • 根据 browserslist 工具 / tagert 属性使用插件
  • tagert 属性权重高,不建议使用
    use: {
      loader: "babel-loader",
      options: {
        presets: [
          ["@babel/preset-env", {
            targets: "last 2 version",
          }]
        ],
      },
    },
    

Babel的配置文件

  • 文件后缀可选.json .js .cjs .mjs
  • .babelrc.json 早期使用,现在不推荐
  • babel.config.json (babel7)可以直接作用于Monorepos项目的子包,更加推荐
  • Monorepos 多包管理(babel本身、element-plus、umi等)
    module.exports = {
      presets: [
        "@babel/preset-env"
      ],
    };
    

认识polyfill

填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript

{
  test: /\.jsx?$/,
  exclude: /node_modules/,
  use: "babel-loader",
},

@babel/polyfill 已经不推荐使用

单独引入 core-jsregenerator-runtime

  • npm i core-js regenerator-runtime -S
  • 配置 babel.config.js
    module.exports = {
      presets: [
        [
          "@babel/preset-env",
          {
            // false
            // entry
            useBuiltIns: "usage",
            corejs: 3,
          },
        ],
      ],
    };
    
  • useBuiltIns
    • false
      • 打包后的文件不使用polyfill来进行适配
      • 不需要设置corejs属性的
    • usage
      • 自动检测所需要的polyfill
      • 打包的包相对会小一些
      • 设置corejs属性来确定使用的corejs的版本
    • entry
      • 需要在入口文件中添加
        import 'core-js/stable';
        import 'regenerator-runtime/runtime';
      • 会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大
  • corejs

认识Plugin-transform-runtime

  • 可以避免 polyfill 全局污染
  • 编写一个工具库,通过polyfill添加的特性,可能会污染它们的代码
  • 当编写工具时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime 来完成 polyfill 的功能;

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime-corejs3

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      // 配置了 plugin-transform-runtime 插件,这个就要注释(二选一)
      // {
      //   useBuiltIns: "usage",
      //   corejs: 3,
      // },
    ],
  ],
  plugins: [
    [
      "@babel/plugin-transform-runtime",
      {
        corejs: 3,
      },
    ],
  ],
};

React的jsx支持

npm install @babel/preset-react -D

module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        useBuiltIns: "usage",
        corejs: 3,
      },
    ],
    "@babel/preset-react",
  ],
};

加载 Vue

  • npm i vue -S
  • npm i vue-loader vue-template-compiler -D
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module: {
  rules: [
    // ...
    {
      test: /\.vue$/,
      use: "vue-loader",
    },
  ],
},
plugins: [
  // ...
  new VueLoaderPlugin(),
],

TypeScript的编译

使用 ts-loader

缺点:不能添加对应的 polyfill

  • npm install ts-loader -D
    • 安装 ts-loader 会一起安装 typescript 依赖
  • 命令行打包 npx tsc
  • webpack 打包
    • npx tsc --init 初始化 ts 配置信息tsconfig.json文件
    {
      test: /.ts$/,
      exclude: /node_modules/,
      use: 'ts-loader',
    }
    

使用 babel-loader

优点:可以将ts转换成js,并且可以实现polyfill的功能

缺点:在编译的过程中,不会对类型错误进行检测

  • npm install @babel/preset-typescript -D
  • module.rules 修改为 babel-loader
    {
      test: /.ts$/,
      exclude: /node_modules/,
      use: 'babel-loader',
    }
    
  • babel.config.js 添加 @babel/preset-typescript 预设 module.exports = { presets: [ [ "@babel/preset-env", { useBuiltIns: "usage", corejs: 3, }, ], "@babel/preset-react", + "@babel/preset-typescript", ], };

babel vs tsc 最佳实践

使用Babel来完成代码的转换,使用tsc来进行类型的检查

  • package.json 修改运行命令
"scripts": {
  "build": "npm run type-check & webpack",
  "type-check": "tsc --noEmit",
  "type-check-watch": "tsc --noEmit --watch"
},

ESlint

  • npm i eslint -D
  • npx eslint --init 创建配置文件 .eslintrc.js
  • npx eslint ./src/main.js 执行检测命令

ESLint的配置文件解析

  • env:运行的环境,比如是浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法;
  • extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个);
  • parserOptions:这里可以指定ESMAScript的版本、sourceType的类型
    • pparser:默认情况下是espree(也是一个JS Parser,用于ESLint),但是因为我们需要编译TypeScript,所 以需要指定对应的解释器;
  • plugins:指定我们用到的插件;
  • rules:自定义的一些规则;

eslint-loader使用

{
  test: /\.jsx?$/, // /.ts$/
  exclude: /node_modules/,
  use: [
    "babel-loader",
    "eslint-loader"
  ],
},

vscode 可以安装Eslint Prettier插件

  • settings.json配置
"eslint.format.enable": true,
"eslint.alwaysShowStatus": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",

HMR 模块热替换

Webpack 可以监听文件变化,当它们修改后会重新编译

自动编译代码

watch

  • 开启 watch

    • webapck.config.js
    module.exports = {
      //...
      watch: true,
    };
    
    • package.json
    "scripts": {
      "dev": "webpack --watch",
    },
    
  • 缺点:

    • 对所有的源代码都重新编译
    • 编译成功后,都会生成新的文件

webpack-dev-server

  • 提供了实时重新加载功能
    • 如果项目配置了browserslist选项,可能导致页面不能自动刷新,需要配置target: "web"
  • webpack-dev-server 在编译之后不会写入到任何输出文件。而是将文件保留在内存
    • webpack-dev-server使用了一个库叫 memfs
  • 会刷新整个页面

npm i webpack-dev-server -D

  • package.json 修改启动命令
"serve": "webpack serve"

开启HMR

优点:
  • 在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面
    • 保留某些应用程序的状态不丢失
    • 节省开发的时间,立即在浏览器更新
开启HMR
  • 修改webpack的配置 module.exports = { //... devServer: { hot: true, }, };
  • 在入口文件,指定哪些模块发生更新时进行HMR if (module.hot) { module.hot.accept("./math.js", () => { // 更新后的回调函数 console.log("math模块发生了更新~"); }); }

Vue 开启 HMR

  • Vue Loader 支持 vue 组件的 HMR,提供开箱即用体验

React 开启 HMR

  • npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

  • 修改 webpack.config.js const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

    module: {
      rules: [
        {
          test: /\.jsx?$/i,
          exclude: /node_modules/,
          use: "babel-loader",
        },
      ],
    },
    plugins: [
      new ReactRefreshWebpackPlugin()
    ],
    
  • 修改 babel.config.js

    module.exports = {
      presets: [
        "@babel/preset-env",
        "@babel/preset-react"
      ],
      plugins: [
        "react-refresh/babel"
      ],
    };
    

HMR 原理全解析


深入配置

output.publicPath

  • path 输出目录对应一个绝对路径
  • publicPath
    • 默认值是一个空字符串,http协议后面会自动添加 /
    • 路径拼接规则 http://domain:80 + publicPath + 'js/index.js'
    • 开发阶段http://,可以设置为 / image.png
    • 生成阶段(本地直接打开,不开启服务)file:///,可以设置为相对路径 ./

image.png

devServer

publicPath

  • 指定本地服务所在的文件夹,默认值是 /
  • 如果我们将其设置为了/abc,那么我们需要通过http://localhost:8080/abc才能访问到对应的打包后的资源
  • 必须将output.publicPath也设置为/abc
    • 建议 devServer.publicPathoutput.publicPath 相同
module.exports = {
  //...
  output: {
    publicPath: '/abc',
  },
  devServer: {
    publicPath: '/abc'
  },
};

contentBase

告诉服务器从哪里提供静态文件

最好不要修改!!!

  • <script src="./static/test.js"></script>
  • 设置 contentBase: path.resolve(__dirname, './static') 可以省略 static
  • <script src="./test.js"></script>

watchContentBase

  • 默认启用,静态资源文件更改将触发整个页面重新加载

hotOnly

启用热模块替换功能,在构建失败时不刷新页面

  • 默认情况下当代码编译失败修复后,我们会重新刷新整个页面;
  • 旧版本设置 hotOnly: true,新版本设置 hot: 'only'

host

设置主机地址

  • 默认值是 localhost
  • 如果希望其他地方也可以访问,可以设置为 0.0.0.0
  • localhost 和 0.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

指定监听请求的端口号

open

在服务器已经启动后打开浏览器。设置其为 true 以打开你的默认浏览器

compress

启用 gzip 压缩

  • 响应头会携带 Content-Encoding: gzip

proxy

  • 对 /api/users 的请求会将请求代理到 http://localhost:8888/api/users
proxy: {
  // "/api" 替换为 "http://localhost:8888"
  "/api": {
    target: "http://localhost:8888",
    // 不希望传递/api,则需要重写路径
    pathRewrite: {
      // ^/api 是个正则表达式
      // 将 /api 替换为 ""
      // 最终代理到 http://localhost:8888/users
      "^/api": ""
    },
    // 默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器
    // 设置 false 关闭校验
    secure: false,
    // 默认情况下,代理时会保留主机头的来源,可以将 changeOrigin 设置为 true 以覆盖此行为
    // Remote Address: 127.0.0.1:8080 => 127.0.0.1:8888
    changeOrigin: true,
  }
},

historyApiFallback

  • 解决SPA页面在路由跳转之后,进行页面刷新 时,返回404的错误
  • vue-router history 模式 http://localhost:8080/abc
  • 设置为 true,那么在刷新时,返回404错误时,会自动返回 index.html 的内容

Resolve

配置模块如何解析

webpack能解析三种文件路径

  • 绝对路径
    • 不需要再做进一步解析
  • 相对路径
    • 在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录
    • 在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径
  • 模块路径
    • resolve.modules 中指定的所有目录检索模块
    • 可以通过设置 resolve.alias 的方式来替换初识模块路径

确实文件还是文件夹

  • 如果是一个文件:
    • 如果文件具有扩展名,则直接打包文件;
    • 否则,将使用 resolve.extensions 选项作为文件扩展名解析;
  • 如果是一个文件夹:
    • 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找
    • 再根据 resolve.extensions 来解析扩展名

modules

告诉 webpack 解析模块时应该搜索的目录

  • 默认值 ['node_modules']

alias

创建 import 或 require 的别名

const path = require('path');

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
};
  • 引入时可以改为 import 'Utilities/test.js'

extensions

解析到文件时自动添加扩展名

  • ['.js', '.json', '.wasm']
  • 如果有多个文件有相同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀

mainFiles

解析目录时要使用的文件名

  • 默认值 ['index']

优化

如何区分开发环境

环境变量

使用相同的一个入口配置文件,通过设置参数来区分它们

  • 新建 config/webpack.common.js 文件测试
    const path = require("path");
    
    module.exports = function(env) {
      console.log('Goal: ', env.goal); // 'local'
      console.log('Production: ', env.production); // true
    
    
      return {
        // 不配置 context 按照 node.js 进程的当前目录
        entry: "./src/index.js",
        output: {
          path: path.resolve(__dirname, "../dist"),
        },
      };
    };
    
  • npx webpack --config ./config/webpack.common.js --env production goal=local
  • --env 允许你传入任意数量的环境变量;例如,--env production 或 --env goal=local

Tips: 与 mode 配置选项的值无关,只是为 env(参数)追加一个变量

{
  WEBPACK_BUNDLE: true,
  WEBPACK_BUILD: true,
  production: true,
  goal: 'local'
}

配置分离

以下文件都是放在 config 文件夹中

新建 webpack.common.js 文件

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
// webpack 提供的合并文件插件
const { merge } = require("webpack-merge");

// 引入 dev prod 环境配置
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");

// 公共的 webpack 配置项
const commonConfig = {
  // 没有使用 context
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "../dist"),
  },
  module: {
    rules: [
      //...
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new VueLoaderPlugin(),
  ],
};

module.exports = function(env) {
  const isProduction = env.production;
  // 因为插件的生命周期问题,babel 获取不到 DefinePlugin 的值,所以需要手动修改
  // env 属性的值,如果赋值不是字符串,会使用 String() 转为字符串,会导致 undefined 转换为字符串
  process.env.NODE_ENV = isProduction ? "production" : "development";

  // 区分环境合并代码
  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig, config);

  return mergeConfig;
};

新建 webpack.prod.js 文件

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

// 生产环境
module.exports = {
  mode: "production",
  plugins: [
    new CleanWebpackPlugin({}),
  ]
}

新建 webpack.dev.js 文件

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

// 开发环境
module.exports = {
  mode: "development",
  devServer: {
    hot: true,
    hotOnly: true,
    compress: true,
  },
  plugins: [
    new ReactRefreshWebpackPlugin(),
  ]
}

修改 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"]);
}

module.exports = {
  presets,
  plugins
}

入口文件解析

基础目录,绝对路径,用于从配置中解析入口点(entry point)和 加载器(loader)

  • 默认使用 Node.js 进程的当前工作目录(与package.json平级),但是推荐在配置中传入一个值
const path = require("path");

module.exports = function(env) {
  return {
    // 不配置 context 按照 node.js 进程的当前目录
    // entry: "./src/index.js",
    context: path.resolve(__dirname, "./"),
    entry: "../src/index.js",
    output: {
      path: path.resolve(__dirname, "../dist"),
    },
  };
};

代码分离

此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。

代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,优化代码加载性能。

常用的代码分离方法有三种:

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

多入口起点

const path = require('path');

 module.exports = {
   entry: {
     index: './src/index.js',
     another: './src/another-module.js',
   },
   output: {
     filename: '[name].bundle.js',
     path: path.resolve(__dirname, 'dist'),
   },
 };
  • 在 index.html 中,会引入多入口的所有文件

image.png

Q: vue-cli 中的多页面是如何实现的?

Entry dependencies 入口依赖

  • 不推荐使用
  • index.js 和 another.js 都依赖两个库:lodash dayjs
    • 打包后的两个bunlde都有会有一份lodash和dayjs
    • 使用 shared 在多个 chunk 之间共享模块
entry: {
  index: { import: "./src/index.js", dependOn: "shared" },
  math: { import: "./src/math.js", dependOn: "shared" },
  // String | Array<String>
  // shared: "lodash",
  shared: ["lodash", "axios"],
},
output: {
  filename: "[name].bundle.js",
  path: path.resolve(__dirname, "../dist"),
},

SplitChunksPlugin

  • chunks
    • async 默认值,异步导入import()
    • initial 同步导入
    • all 两者都处理
  • minSize
    • 拆分包的大小, 至少为minSize
    • 如果一个包拆分出来达不到minSize,那么这个包就不会拆分
  • maxSize
    • 将大于maxSize的包,拆分为不小于minSize的包
  • minChunks
    • 至少被引入的次数,默认是1
    • 如果我们写一个2,但是引入了一次,那么不会被单独拆分
  • name:设置拆包的名称
    • 可以设置一个名称,也可以设置为false
    • 设置为false后,需要在cacheGroups中设置名称
  • cacheGroups
    • 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包
    • test:匹配符合规则的包;
    • name:拆分包的name属性,固定值;
    • filename:拆分包的名称,可以自己使用placeholder属性;

vue-element-admin 配置信息

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      libs: {
        name: 'chunk-libs',
        test: /[\\/]node_modules[\\/]/,
        priority: 10,
        chunks: 'initial' // only package third parties that are initially dependent
      },
      elementUI: {
        name: 'chunk-elementUI', // split elementUI into a single package
        priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
        test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
      },
      commons: {
        name: 'chunk-commons',
        test: resolve('src/components'), // can customize your rules
        minChunks: 3, //  minimum common number
        priority: 5,
        reuseExistingChunk: true
      }
    }
  }
}

动态导入 import()

// 只要是异步导入的代码, webpack都会进行代码分离
import("./foo").then(res => {
  console.log(res);
});

import("./foo_02").then(res => {
  console.log(res);
});

动态导入的文件命名

  • 修改 chunk 文件的名称

    output: {
      filename: "[name].bundle.js",
      path: path.resolve(__dirname, "../dist"),
      chunkFilename: "[name].[hash:6].chunk.js"
    },
    
  • 获取到的 [name] 是和 id 的名称保持一致的

  • 修改name的值,可以通过magic comments(魔法注释)的方式 // magic comments import(/* webpackChunkName: "foo" */"./foo").then(res => { console.log(res); });

    import(/* webpackChunkName: "foo_02" */"./foo_02").then(res => {
      console.log(res);
    });
    

vue-router 把组件按组分块

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
  • webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
  • 把 output 的 chunkFilename: "[name].[hash:6].chunk.js" 可以添加 hash,这样即使同名的文件,也不会合并到一个模块中

chunkIds

告知 webpack 当选择模块 id 时需要使用哪种算法

  • natural
    • 按使用顺序的数字 id
    • 不推荐使用
  • named
    • 对调试更友好的可读的 id
    • development下的默认值
  • deterministic
    • 在不同的编译中不变的短数字 id。有益于长期缓存。
    • 生产模式中会默认开启。
module.exports = {
  //...
  optimization: {
    chunkIds: 'named',
  },
};

代码懒加载

  • 新建一个导出文件 element.js
const element = document.createElement('div');
element.innerHTML = "Hello Element";
export default element;
  • 按钮点击时,加载这个对象
const button = document.createElement("button");
button.innerHTML = "加载元素";
button.addEventListener("click", () => {
  import(
    /* webpackChunkName: 'element' */
    "./element"
  ).then(({default: element}) => {
    document.body.appendChild(element);
  })
});
document.body.appendChild(button);

Prefetch和Preload

  • 在声明 import 时,使用下面这些内置指令,来告知浏览器:
    • prefetch(预获取):将来某些导航下可能需要的资源
    • preload(预加载):当前导航下可能需要资源
  • 与 prefetch 指令相比,preload 指令有许多不同之处:
    • preload chunk 会在父 chunk 加载时,以并行方式(不会有新的文件请求)开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
    • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
    • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻
// prefetch -> 魔法注释(magic comments)
/* webpackPrefetch: true */
/* webpackPreload: true */
import(
  /* webpackChunkName: 'element' */
  /* webpackPrefetch: true */
  "./element"
).then(({ default: element }) => {
  document.body.appendChild(element);
});

参考文档

Q:vue-router 中的路由如果不设置这两个注释,会立即下载吗?

runtimeChunk

  • 配置runtime相关的代码是否抽取到一个单独的chunk中
    • runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
  • true 或 'multiple' 会为每个入口添加一个只含有 runtime 的额外 chunk
  • "single" 会创建一个在所有生成 chunk 之间共享的运行时文件

CDN

  • 购买CDN服务器
    • 可以直接修改publicPath,在打包时添加上自己的CDN地址
    • publicPath: 'https://xxxx.com/cdn'
  • 第三方库的CDN服务器
    • 通常是生产环境才需要修改
    • 第一步,通过webpack配置,来排除一些库的打包
    externals: {
      // window._
      lodash: "_",
      // window.dayjs
      dayjs: "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>
    <% } %> 
    

Shimming 预置依赖

  • 不推荐使用,可以查阅文档了解

MiniCssExtractPlugin

将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载

  • npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
        filename: "css/[name].[contenthash:6].css"
    })
  ],
  module: {
    rules: [
      {
        test: /.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
};

Hash、ContentHash、ChunkHash

  • 在我们给打包的文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似:
    • hash、chunkhash、contenthash
    • hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);
  • hash:
    • 项目里面的内容变动后,会生成新的 hash
    • 多入口文件,如果修改了一个入口的内容,另一个入口文件也会生成新的 hash
  • chunkhash
    • 不同的入口进行借来解析来生成hash值
    • 多入口文件时,互不影响
  • contenthash
    • 表示生成的文件hash名称,只和内容有关系
    • 自己生成的文件被改动后,才会生成新的 hash
    • css(独立文件) 和 chunkFilename 都建议使用 contenthash

参考文档

Terser

压缩、丑化js,让bundle变得更小

  • 在production模式下,默认就是使用TerserPlugin来处理我们的代码的
  • development 不推荐使用
  • 设置 parallel 使用多进程并发运行以提高构建速度
const TerserPlugin = require("terser-webpack-plugin");

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

关于 source maps 说明

只对 devtool 选项的  source-mapinline-source-maphidden-source-map 和 nosources-source-map 有效。

  • eval 会包裹 modules,通过 eval("string"),而 minimizer 不会处理字符串。
  • cheap 不存在列信息,minimizer 只产生单行,只会留下一个映射。

CSS 压缩

  • npm install css-minimizer-webpack-plugin --save-dev
  • 这将仅在生产环境开启 CSS 优化。
  • 如果还想在开发环境下启用 CSS 优化,请将 optimization.minimize 设置为 true
// 抽离css文件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// 压缩css代码
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

Scope Hoisting

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

  • webpack已经内置了对应的模块
  • 在production模式下,默认这个模块就会启用
  • 在development模式下,我们需要自己来打开该模块
plugins: [
  // ...
  new webpack.optimize.ModuleConcatenationPlugin()
]

Tree Shaking

移除 JavaScript 上下文中的未引用代码(dead-code)

usedExports

  • production 默认开启
  • 需要将 mode 配置设置成development,以确定 bundle 不会被压缩
 mode: 'development',
  optimization: {
    usedExports: true,
  },
  • 应该删除掉未被引用的 export
/* 1 */
/***/ (function (module, __webpack_exports__, __webpack_require__) {
  'use strict';
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  function square(x) {
    return x * x;
  }

  function cube(x) {
    return x * x * x;
  }
});
  • 注意,上面的 unused harmony export square 注释。没有引用 square,但它仍然被包含在 bundle 中。
  • 告知 Terser 在优化时,可以删除掉这段代码
  • minimize 设置 true:
    • usedExports设置为false时,square 函数没有被移除掉;
    • usedExports设置为true时,square 函数有被移除掉;

side-effect-free

告知webpack compiler哪些模块时有副作用

有副作用的文件,例如:文件中有 window.a = 1

  • package.json 的 "sideEffects" 属性
{
  "name": "your-project",
  "sideEffects": false
  // "sideEffects": ["./src/some-side-effectful-file.js", "**/*.css"]
}
  • 还可以在 module.rules 配置选项 中设置 "sideEffects"

CSS实现Tree Shaking

删除未使用的 CSS

  • npm install purgecss-webpack-plugin -D
new PurgeCssPlugin({
  paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
  safelist: function() {
    return {
      standard: ["body", "html"]
    }
  }
})
  • paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
  • 默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;

HTTP压缩

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

  • npm install compression-webpack-plugin -D
new CompressionPlugin({
  test: /\.(css|js)$/i,
  threshold: 0,
  minRatio: 0.8,
  algorithm: "gzip",
  // exclude
  // include
}),

HTML文件中代码的压缩

  • production 默认会压缩
  • 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,
    minifyCSS: true,
    minifyJS: {
      mangle: {
        toplevel: true
      }
    }
  }: false
}),

InlineChunkHtmlPlugin

将一些chunk出来的模块,内联到html中

  • npm install react-dev-utils -D
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/,])

Library

const path = require('path');

module.exports = {
  mode: "production",
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "./build"),
    filename: "coderwhy_utils.js",
    // AMD/CommonJS/浏览器
    libraryTarget: "umd",
    // window.coderwhyUtils
    library: "coderwhyUtils",
    // root 的值
    globalObject: "this"
  }
}