# webpack 优化篇

# 1.项目初始化

# 1.1 安装

cnpm i webpack webpack-cli html-webpack-plugin webpack-dev-server cross-env -D

# 1.2 webpack.config.js

const path = require("path");
module.exports = {
  mode: "development",
  devtool: "source-map",
  context: process.cwd(),
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js",
  },
};

# 1.3 package.json

  "scripts": {
    "build": "webpack",
    "start": "webpack serve"
  },

# 2.数据分析

# 2.1 日志美化

# 2.1.1 安装

cnpm i friendly-errors-webpack-plugin  node-notifier -D

# 2.1.2 webpack.config.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FriendlyErrorsWebpackPlugin = require("friendly-errors-webpack-plugin");
const notifier = require("node-notifier");
const ICON = path.join(__dirname, "icon.jpg");
module.exports = {
  mode: "development",
  devtool: "source-map",
  context: process.cwd(),
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js",
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new FriendlyErrorsWebpackPlugin({
      onErrors: (severity, errors) => {
        const error = errors[0];
        notifier.notify({
          title: "Webpack编译失败",
          message: severity + ": " + error.name,
          subtitle: error.file || "",
          icon: ICON,
        });
      },
    }),
  ],
};

# 2.2 速度分析

# 2.2.1 安装

cnpm i speed-measure-webpack5-plugin -D

# 2.2.2 webpack.config.js

const SpeedMeasureWebpackPlugin = require('speed-measure-webpack5-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports = smw.wrap({
  mode: "development",
  devtool: 'source-map',
  ...
});

# 2.3 文件体积监控

  • webpack-bundle-analyzer是一个 webpack 的插件,需要配合 webpack 和 webpack-cli 一起使用。这个插件的功能是生成代码分析报告,帮助提升代码质量和网站性能
  • 它可以直观分析打包出的文件包含哪些,大小占比如何,模块包含关系,依赖项,文件是否重复,压缩后大小如何,针对这些,我们可以进行文件分割等操作。

# 2.3.1 安装

cnpm i webpack-bundle-analyzer -D

# 2.3.2 编译启动

# 2.3.2.1 webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
  plugins: [new BundleAnalyzerPlugin()],
};
# 2.3.2.2 package.json
  "scripts": {
    "build": "webpack",
    "start": "webpack serve",
    "dev":"webpack  --progress"
  },

# 2.3.3 单独启动

# 2.3.3.1 webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "disabled", // 不启动展示打包报告的http服务器
      generateStatsFile: true, // 是否生成stats.json文件
    }),
  ],
};
# 2.3.3.2 package.json
  "scripts": {
    "build": "webpack",
    "start": "webpack serve",
    "dev":"webpack  --progress",
    "analyzer": "webpack-bundle-analyzer --port 8888 ./dist/stats.json"
  }

# 3.编译时间优化

  • 减少要处理的文件
  • 缩小查找的范围

# 3.1 缩小查找范围

# 3.1.1 extensions

  • 指定extensions之后可以不用在require或是import的时候加文件扩展名
  • 查找的时候会依次尝试添加扩展名进行匹配
  resolve: {
    extensions: [".js",".jsx",".json"]
  },
  module:{

# 3.1.2 alias

  • 配置别名可以加快 webpack 查找模块的速度
  • 每当引入 bootstrap 模块的时候,它会直接引入bootstrap,而不需要从node_modules文件夹中按模块的查找规则查找
cnpm i bootstrap css-loader style-loader -S
const bootstrap =  path.resolve(__dirname,'node_modules/bootstrap/dist/css/bootstrap.css');
module.exports = smw.wrap({
  mode: "development",
  devtool: 'source-map',
  context: process.cwd(),
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js"
  },
  resolve: {
    extensions: [".js",".jsx",".json"],
    alias:{bootstrap}
  },
  module:{
    rules:[
            {
              test: /\.css$/,
              use: ['style-loader', 'css-loader']
            }
    ]
  }
});

# 3.1.3 modules

  • 对于直接声明依赖名的模块 webpack 会使用类似Node.js一样进行路径搜索,搜索node_modules目录
  • 如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的node_modules中的话可以直接指定
  • 默认配置
  resolve: {
    extensions: [".js",".jsx",".json"],
    alias:{bootstrap},
    modules: ['node_modules']
  },
  • 直接指定
  resolve: {
    modules: ['C:/node_modules','node_modules'],
  }

# 3.1.4 mainFields

  • 默认情况下package.json 文件则按照文件中 main 字段的文件名来查找文件
resolve: {
  // 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
  mainFields: ['browser', 'module', 'main'],
  // target 的值为其他时,mainFields 默认值为:
  mainFields: ["module", "main"],
}

# 3.1.5 mainFiles

  • 当目录下没有 package.json 文件时,我们说会默认使用目录下的 index.js这个文件
resolve: {
  mainFiles: ['index']
},

# 3.1.6 oneOf

  • 每个文件对于 rules 中的所有规则都会遍历一遍,如果使用 oneOf 就可以解决该问题,只要能匹配一个即可退出
  • 在 oneOf 中不能两个配置处理同一种类型文件

webpack.config.js

rules: [
  {
    oneOf: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "src"),
        exclude: /node_modules/,
        use: [
          {
            loader: "thread-loader",
            options: {
              workers: 3,
            },
          },
          {
            loader: "babel-loader",
            options: {
              cacheDirectory: true,
            },
          },
        ],
      },
      {
        test: /\.css$/,
        use: ["cache-loader", "logger-loader", "style-loader", "css-loader"],
      },
    ],
  },
];

# 3.1.7 external

  • 如果我们想引用一个库,但是又不想让 webpack 打包,并且又不影响我们在程序中以 CMD、AMD 或者 window/global 全局等方式进行使用,那就可以通过配置externals
# 3.1.7.1 安装
cnpm i jquery html-webpack-externals-plugin -D
# 3.1.7.2 使用 jquery

src/index.js

const jQuery = require("jquery");
import jQuery from "jquery";
# 3.1.7.3 index.html

src/index.html

<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
# 3.1.7.4 webpack.config.js

webpack.config.js

 externals: {
   jquery: 'jQuery',
 },
 module: {

# 3.1.8 resolveLoader

resolve.resolveLoader用于配置解析 loader 时的 resolve 配置,默认的配置

# 3.1.8.1 logger-loader.js

loaders\logger-loader.js

function loader(source) {
  console.log("logger-loader");
  return source;
}
module.exports = loader;
# 3.1.8.2 webpack.config.js

webpack.config.js

module.exports = {
  resolve: {
    extensions: [".js", ".jsx", ".json"],
    alias: { bootstrap },
    modules: ["node_modules"],
  },
  resolveLoader: {
    modules: [path.resolve(__dirname, "loaders"), "node_modules"],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ["logger-loader", "style-loader", "css-loader"],
      },
    ],
  },
};

# 3.2 noParse

  • module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析
  • 不需要解析依赖(即无依赖) 的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度
  • 使用 noParse 进行忽略的模块文件中不能使用 importrequire 等语法

# 3.2.1 src\index.js

src\index.js

let title = require("./title");
console.log(title);

# 3.2.2 src\title.js

src\title.js

let name = require("./name");
module.exports = `hello ${name}`;

# 3.2.3 src\name.js

src\name.js

module.exports = "james";

# 3.2.4 webpack.config.js

module.exports = {
  module: {
       noParse: /title.js/, // 正则表达式
  }
}

# 3.3 IgnorePlugin

  • ignore-plugin用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去
  • requestRegExp 匹配(test)资源请求路径的正则表达式。
  • contextRegExp (可选)匹配(test)资源上下文(目录)的正则表达式。
  • moment 会将所有本地化内容和核心功能一起打包,你可使用 IgnorePlugin 在打包时忽略本地化内容

# 3.3.1 安装

cnpm i moment -S

# 3.3.2 src\index.js

src\index.js

import moment from "moment";
console.log(moment);

# 3.3.3 webpack.config.js

webpack.config.js

 plugins:[
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/
    })
 ]

# 3.4 thread-loader(多进程)

  • thread-loader放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行
  • include 表示哪些目录中的 .js 文件需要进行 babel-loader
  • exclude 表示哪些目录中的 .js 文件不要进行 babel-loader
  • exclude 的优先级高于 include,尽量避免 exclude,更倾向于使用 include

# 3.4.1 安装

cnpm  i thread-loader babel-loader @babel/core @babel/preset-env -D

# 3.4.2 webpack.config.js

webpack.config.js

rules: [
  {
    test: /\.js$/,
    include: path.resolve(__dirname, "src"),
    exclude: /node_modules/
    use: [
      {
      loader: 'thread-loader',
      options: {workers: 3}
      }, 'babel-loader']
  },
  {
    test: /\.css$/,
    use: ['logger-loader', 'style-loader', 'css-loader']
  }
]

# 3.5 利用缓存

  • 利用缓存可以提升重复构建的速度

# 3.5.1 babel-loader

  • Babel 在转义 js 文件过程中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗
  • 默认存放位置是node_modules/.cache/babel-loader
{
test: /\.js$/,
include: path.resolve(__dirname, "src"),
exclude: /node_modules/,
use: [
  {
      loader: 'thread-loader',
      options: {
          workers: 3
      }P
  },
  {
      loader: 'babel-loader',
      options: {
          cacheDirectory: true
      }
  }
]
},

# 3.5.2 cache-loader

  • 在一些性能开销较大的cache-loader之前添加此 loader,可以以将结果缓存中磁盘中
  • 默认保存在 node_modules/.cache/cache-loader 目录下
cnpm i  cache-loader -D
{
test: /\.css$/,
use: [
    'cache-loader',
    'logger-loader',
    'style-loader',
    'css-loader']
}

# 3.5.3 hard-source-webpack-plugin

  • HardSourceWebpackPlugin模块提供了中间缓存,缓存默认的存放路径是 node_modules/.cache/hard-source
  • 配置hard-source-webpack-plugin后,首次构建时间并不会有太大的变化,但是从第二次开始,构建时间大约可以减少80%左右
  • webpack5中已经内置了模块缓存,不需要再使用此插件
# 3.5.3.1 安装
cnpm i  hard-source-webpack-plugin -D
# 3.5.3.2 webpack.config.js
let  HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

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

# 4.编译体积优化

# 4.1 压缩 JS、CSS、HTML 和图片

# 4.1.1 安装

cnpm i terser-webpack-plugin optimize-css-assets-webpack-plugin image-webpack-loader -D

# 4.1.2 webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
+const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  devtool: false,
  entry: './src/index.js',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
    ],
  },
   module:{
     rules:[
         {
          test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
          use: [
            'url-loader',
           {
             loader: 'image-webpack-loader',
             options: {
               mozjpeg: {
                 progressive: true,
                 quality: 65
               },
               optipng: {
                 enabled: false,
               },
               pngquant: {
                 quality: '65-90',
                 speed: 4
               },
               gifsicle: {
                 interlaced: false,
               },
               webp: {
                 quality: 75
               }
             }
           }
          ]
        }
  ]
}
  plugins: [
    new HtmlWebpackPlugin({
    template: './src/index.html',
     minify: {
        collapseWhitespace: true,
        removeComments: true
      }
    })
   new OptimizeCssAssetsWebpackPlugin(),
  ],
};

# 4.2 清除无用的 CSS

# 4.2.1 安装

cnpm i purgecss-webpack-plugin mini-css-extract-plugin  -D

# 4.2.3 src\index.js

import "./index.css";

# 4.2.4 src\index.css

src\index.css

body {
  background-color: red;
}
#root {
  color: red;
}
#other {
  color: red;
}

# 4.2.5 src\index.html

src\index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

# 4.2.6 package.json

package.json

  "sideEffects":["**/*.css"],

# 4.2.7 webpack.config.js

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require("glob");
const PATHS = {
  src: path.join(__dirname, "src"),
};
module.exports = {
  module: {
    rules: [
       {
        test: /\.css$/,
        include: path.resolve(__dirname, "src"),
        exclude: /node_modules/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          "css-loader",
        ],
      }
    ]
  },
  plugins: [
     new MiniCssExtractPlugin({
       filename: "[name].css",
     }),
     new PurgecssPlugin({
       paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
     })
  ]
  devServer: {},
};

# 4.3 Tree Shaking

  • 一个模块可以有多个方法,只要其中某个方法使用到了,则整个文件都会被打到 bundle 里面去,tree shaking 就是只把用到的方法打入 bundle,没用到的方法uglify阶段擦除掉
  • 原理是利用 es6 模块的特点,只能作为模块顶层语句出现,import 的模块名只能是字符串常量

# 4.3.1 开启

  • webpack 默认支持,可在production mode下默认开启
  • 在 package.json 中配置:
    • "sideEffects": false 所有的代码都没有副作用(都可以进行 tree shaking)
    • 可能会把 css和@babel/polyfill文件干掉 可以设置 "sideEffects":["*.css"]

# 4.3.2 没有导入和使用

functions.js

function func1() {
  return "func1";
}
function func2() {
  return "func2";
}
export { func1, func2 };
import { func2 } from "./functions";
const result2 = func2();
console.log(result2);

# 4.3.3 代码不会被执行,不可到达

if (false) {
  console.log("false");
}

# 4.3.4 代码执行的结果不会被用到

import { func2 } from "./functions";
func2();

# 4.3.5 代码中只写不读的变量

var aabbcc = "aabbcc";
aabbcc = "eeffgg";

# 4.4 Scope Hoisting

  • Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 "作用域提升",是在 Webpack3 中新推出的功能。
  • scope hoisting 的原理是将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
  • 这个功能在 mode 为production下默认开启,开发环境要用 webpack.optimizeModuleConcatenationPlugin插件

hello.js

export default "Hello";

index.js

import str from "./hello.js";
console.log(str);

main.js

var hello = ('hello');
console.log(hello);

# 5.运行速度优化

# 5.1 代码分割

  • 对于大的 Web 应用来讲,将所有的代码都放在一个文件中显然是不够有效的,特别是当你的某些代码块是在某些特殊的时候才会被用到。
  • webpack 有一个功能就是将你的代码库分割成 chunks 语块,当代码运行到需要它们的时候再进行加载

# 5.1.1 入口点分割

  • Entry Points:入口文件设置的时候可以配置
  • 这种方法的问题
    • 如果入口 chunks 之间包含重复的模块(lodash),那些重复模块都会被引入到各个 bundle 中
    • 不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
entry: {
  index: "./src/index.js",
  login: "./src/login.js"
}

# 5.1.2 懒加载

  • 用户当前需要用什么功能就只加载这个功能对应的代码,也就是所谓的按需加载,在给单页应用做按需加载优化时
  • 一般采用以下原则:
    • 对网站功能进行划分,每一类功能就对应一个 chunk
    • 对于首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
    • 被分割出去的代码需要一个按需加载的时机
# 5.1.2.1 hello.js

hello.js

module.exports = "hello";
# 5.1.2.2 index.js
document.querySelector("#clickBtn").addEventListener("click", () => {
  import("./hello").then((result) => {
    console.log(result.default);
  });
});
# 5.1.2.3 index.html
<button id="clickBtn">点我</button>

# 5.1.3 prefetch

  • 使用预先拉取,你表示该模块可能以后会用到。浏览器会在空闲时间下载该模块
  • prefetch的作用是告诉浏览器未来可能会使用到的某个资源,浏览器就会在闲时去加载对应的资源,若能预测到用户的行为,比如懒加载,点击到其它页面等则相当于提前预加载了需要的资源
  • 此导入会让<link rel="prefetch" as="script" href="http://localhost:8080/hello.js">被添加至页面的头部。因此浏览器会在空闲时间预先拉取该文件

webpack.config.js

document.querySelector("#clickBtn").addEventListener("click", () => {
  import(
    /* webpackChunkName: 'hello', webpackPrefetch: true */ "./hello"
  ).then((result) => {
    console.log(result.default);
  });
});

# 5.1.6 提取公共代码

  • 怎么配置单页应用?怎么配置多页应用?

    # 5.1.6.1 为什么需要提取公共代码
  • 大网站有多个页面,每个页面由于采用相同技术栈和样式代码,会包含很多公共代码,如果都包含进来会有问题

  • 相同的资源被重复的加载,浪费用户的流量和服务器的成本;

  • 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。

  • 如果能把公共代码抽离成单独文件进行加载能进行优化,可以减少网络传输流量,降低服务器成本

# 5.1.6.2 如何提取
  • 基础类库,方便长期缓存
  • 页面之间的公用代码
  • 各个页面单独生成文件
# 5.1.6.3 splitChunks
  • split-chunks-plugin

    # 5.1.6.3.1 module chunk bundle
  • module:就是 js 的模块化 webpack 支持 commonJS、ES6 等模块化规范,简单来说就是你通过 import 语句引入的代码

  • chunk: chunk 是 webpack 根据功能拆分出来的,包含三种情况

    • 你的项目入口(entry)
    • 通过 import()动态引入的代码
    • 通过 splitChunks 拆分出来的代码
  • bundle:bundle 是 webpack 打包之后的各个文件,一般就是和 chunk 是一对一的关系,bundle 就是对 chunk 进行编译压缩打包等处理之后的产出

# 5.1.6.3.2 默认配置

webpack.config.js

module.exports = {
  output: {
    filename: "[name].js",
    chunkFilename: "[name].js",
  },
  entry: {
    page1: "./src/page1.js",
    page2: "./src/page2.js",
    page3: "./src/page3.js",
  },
  optimization: {
    splitChunks: {
      chunks: "all",
      minSize: 0,
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 10,
      automaticNameDelimiter: "~",
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

src\page1.js

import utils1 from "./module1";
import utils2 from "./module2";
import $ from "jquery";
console.log(utils1, utils2, $);
import(/* webpackChunkName: "asyncModule1" */ "./asyncModule1");

src\page2.js

import utils1 from "./module1";
import utils2 from "./module2";
import $ from "jquery";
console.log(utils1, utils2, $);

src\page3.js

import utils1 from "./module1";
import utils3 from "./module3";
import $ from "jquery";
console.log(utils1, utils3, $);

src\module1.js

console.log("module1");

src\module2.js

console.log("module2");

src\module3.js

console.log("module3");

src\asyncModule1.js

import _ from 'lodash';
console.log(_);
                             Asset       Size                     Chunks             Chunk Names
             asyncModule1.chunk.js  740 bytes               asyncModule1  [emitted]  asyncModule1
                        index.html  498 bytes                             [emitted]
                          page1.js   10.6 KiB                      page1  [emitted]  page1
              page1~page2.chunk.js  302 bytes                page1~page2  [emitted]  page1~page2
        page1~page2~page3.chunk.js  308 bytes          page1~page2~page3  [emitted]  page1~page2~page3
                          page2.js   7.52 KiB                      page2  [emitted]  page2
                          page3.js   7.72 KiB                      page3  [emitted]  page3
     vendors~asyncModule1.chunk.js    532 KiB       vendors~asyncModule1  [emitted]  vendors~asyncModule1
vendors~page1~page2~page3.chunk.js    282 KiB  vendors~page1~page2~page3  [emitted]  vendors~page1~page2~page3
Entrypoint page1 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page1~page2.chunk.js page1.js
Entrypoint page2 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page1~page2.chunk.js page2.js
Entrypoint page3 = vendors~page1~page2~page3.chunk.js page1~page2~page3.chunk.js page3.js

引用下珠峰的图片:
splitChunks

# 5.2 CDN

  • 最影响用户体验的是网页首次打开时的加载等待。 导致这个问题的根本是网络传输过程耗时大,CDN 的作用就是加速网络传输。
  • CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度
  • 用户使用浏览器第一次访问我们的站点时,该页面引入了各式各样的静态资源,如果我们能做到持久化缓存的话,可以在 http 响应头加上 Cache-control 或 Expires 字段来设置缓存,浏览器可以将这些资源一一缓存到本地
  • 用户在后续访问的时候,如果需要再次请求同样的静态资源,且静态资源没有过期,那么浏览器可以直接走本地缓存而不用再通过网络请求资源
  • 缓存配置
    • HTML 文件不缓存,放在自己的服务器上,关闭自己服务器的缓存,静态资源的 URL 变成指向 CDN 服务器的地址
    • 静态的 JavaScript、CSS、图片等文件开启 CDN 和缓存,并且文件名带上 HASH 值
    • 为了并行加载不阻塞,把不同的静态资源分配到不同的 CDN 服务器上
  • 域名限制
    • 同一时刻针对同一个域名的资源并行请求是有限制
    • 可以把这些静态资源分散到不同的 CDN 服务上去
    • 多个域名后会增加域名解析时间
    • 可以通过在 HTML HEAD 标签中 加入去预解析域名,以降低域名解析带来的延迟

webpack.config.js

const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const UploadPlugin = require("./plugins/UploadPlugin");
const glob = require("glob");
const PATHS = {
    src: path.join(__dirname, "src"),
};
module.exports = {
  mode: "development",
  devtool: false,
  context: process.cwd(),
  entry: {
    main: "./src/index.js",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[hash].js",
    chunkFilename: "[name].[hash].chunk.js",
    publicPath: "http://img.golderbrother.cn/",
  },
  optimization: {
    minimize: true,
    minimizer: [
      //压缩JS
      /* new TerserPlugin({
        sourceMap: false,
        extractComments: false,
      }),
      //压缩CSS
      new OptimizeCSSAssetsPlugin({}), */
    ],
    //自动分割第三方模块和公共模块
    splitChunks: {
      chunks: "all", //默认作用于异步chunk,值为all/initial/async
      minSize: 0, //默认值是30kb,代码块的最小尺寸
      minChunks: 1, //被多少模块共享,在分割之前模块的被引用次数
      maxAsyncRequests: 2, //限制异步模块内部的并行最大请求数的,说白了你可以理解为是每个import()它里面的最大并行请求数量
      maxInitialRequests: 4, //限制入口的拆分数量
      name: true, //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
      automaticNameDelimiter: "~", //默认webpack将会使用入口名和代码块的名称生成命名,比如 'vendors~main.js'
      cacheGroups: {
        //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
        vendors: {
          chunks: "all",
          test: /node_modules/, //条件
          priority: -10, ///优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中,为了能够让自定义缓存组有更高的优先级(默认0),默认缓存组的priority属性为负值.
        },
        commons: {
          chunks: "all",
          minSize: 0, //最小提取字节数
          minChunks: 2, //最少被几个chunk引用
          priority: -20,
          reuseExistingChunk: true, //如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
        },
      },
    },
    //为了长期缓存保持运行时代码块是单独的文件
    /*  runtimeChunk: {
      name: (entrypoint) => `runtime-${entrypoint.name}`,
    }, */
  },
  module: {
    rules: [
      {
        test: /\.js/,
        include: path.resolve(__dirname, "src"),
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: [
                ["@babel/preset-env", { modules: false }],
                "@babel/preset-react",
              ],
            },
          },
        ],
      },
      {
        test: /\.css$/,
        include: path.resolve(__dirname, "src"),
        exclude: /node_modules/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          "css-loader",
        ],
      },
      {
        test: /\.(png|svg|jpg|gif|jpeg|ico)$/,
        use: [
          "file-loader",
          {
            loader: "image-webpack-loader",
            // 有各种配置,可以调整你要压缩后图片的质量
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65,
              },
              optipng: {
                enabled: false,
              },
              pngquant: {
                quality: "65-90",
                speed: 4,
              },
              gifsicle: {
                interlaced: false,
              },
              webp: {
                quality: 75,
              },
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      inject: true,
      template: "./src/index.html",
    }),
    new MiniCssExtractPlugin({
      filename: "[name].[hash].css",
    }),
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
    }),
    new UploadPlugin({}),
  ],
  devServer: {},
};

# 6.附录

# 6.1 环境

# 6.1.1 模式(mode)

  • 日常的前端开发工作中,一般都会有两套构建环境
  • 一套开发时使用,构建结果用于本地开发调试,不进行代码压缩,打印 debug 信息,包含 sourcemap 文件
  • 一套构建后的结果是直接应用于线上的,即代码都是压缩后,运行时不打印 debug 信息,静态文件不包括 sourcemap
  • webpack 4.x 版本引入了 mode 的概念
  • 当你指定使用 production mode 时,默认会启用各种性能优化的功能,包括构建结果优化以及 webpack 运行性能优化
  • 而如果是 development mode 的话,则会开启 debug 工具,运行时打印详细的错误信息,以及更加快速的增量编译构建
选项 描述
development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin
production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin

# 6.1.2 环境差异

  • 开发环境
    • 需要生成 sourcemap 文件
    • 需要打印 debug 信息
    • 需要 live reload 或者 hot reload 的功能
  • 生产环境
    • 可能需要分离 CSS 成单独的文件,以便多个页面共享同一个 CSS 文件
    • 需要压缩 HTML/CSS/JS 代码
    • 需要压缩图片
  • 其默认值为 production

# 6.1.3 区分环境

  • --mode用来设置模块内process.env.NODE_ENV
  • --env用来设置 webpack 配置文件的函数参数
  • cross-env用来设置 node 环境的process.env.NODE_ENV
  • dotenv可以按需加载不同的环境变量文件
  • define-plugin用来配置在编译时候用的全局常量
# 6.1.3.1 安装
cnpm i cross-env dotenv terser-webpack-plugin optimize-css-assets-webpack-plugin -D
# 6.1.3.2 mode 默认值
  • webpack 的 mode 默认为production
  • webpack serve的 mode 默认为development
  • 可以在模块内通过process.env.NODE_ENV获取当前的环境变量,无法在webpack配置文件中获取此变量
  "scripts": {
    "build": "webpack",
    "start": "webpack serve"
  },

index.js

console.log(process.env.NODE_ENV); // development | production

webpack.config.js

console.log("NODE_ENV", process.env.NODE_ENV); // undefined
# 6.1.3.3 命令行传 mode
  • 同配置 1

    "scripts": {
      "build": "webpack --mode=production",
      "start": "webpack --mode=development serve"
    },
    
# 6.1.3.4 命令行配置 env
  • 无法在模块内通过process.env.NODE_ENV访问
  • 可以通过webpack 配置文件中中通过函数获取当前环境变量
"scripts": {
   "dev": "webpack serve --env=development",
   "build": "webpack --env=production",
}

index.js

console.log(process.env.NODE_ENV); // undefined

webpack.config.js

console.log('NODE_ENV',process.env.NODE_ENV);// undefined
const TerserWebpackPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');

module.exports = (env) => {
  console.log('env',env);// {development:true} | {production:true}
  return {
+        optimization: {
+          minimize:env&&env.production,
+          minimizer: (env && env.production) ? [
+            new TerserWebpackPlugin({
+              parallel: true//开启多进程并行压缩
+            }),
+            new OptimizeCssAssetsWebpackPlugin({})
+          ] : []
+        },
  }
};
# 6.1.3.5 mode 配置
  • 和命令行配置 2 一样

    module.exports = {
      mode: "development",
    };
    
# 6.1.3.6 DefinePlugin
  • 设置全局变量(不是window),所有模块都能读取到该变量的值
  • 可以在任意模块内通过 process.env.NODE_ENV 获取当前的环境变量
  • 但无法在node环境(webpack 配置文件中)下获取当前的环境变量
# 6.1.3.6.1 webpack.config.js
console.log("process.env.NODE_ENV", process.env.NODE_ENV); // undefined
console.log("NODE_ENV", NODE_ENV); // error !!!
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": JSON.stringify("development"), //注意用双引号引起来,否则就成变量了
      NODE_ENV: JSON.stringify("production"),
    }),
  ],
};
# 6.1.3.6.2 src/index.js
console.log(NODE_ENV); //  production
# 6.1.3.6.3 src/logger.js
export default function logger(...args) {
  if (process.env.NODE_ENV == "development") {
    console.log.apply(console, args);
  }
}
# 6.1.4 cross-env
  • 只能设置node环境下的变量 NODE_ENV

package.json

"scripts": {
  "build": "cross-env NODE_ENV=development webpack"
}

webpack.config.js

console.log("process.env.NODE_ENV", process.env.NODE_ENV); // development

# 6.1.4 env

# 6.1.4.1 .env
NODE_ENV = development;
# 6.1.4.2 webpack.config.js

webpack.config.js

require('dotenv').config({path: path.resolve(__dirname,'.env')})
console.log(process.env.NODE_ENV);

# 6.2 JavaScript 兼容性

# 6.2.1 Babel

  • Babel 是一个 JavaScript 编译器
  • Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
  • Babel 能为你做的事情
    • 语法转换
# 6.2.1.1 安装
cnpm i @babel/core @babel/cli -D
# 6.2.1.2 命令行使用
# 6.2.1.2.1 src\index.js
const sum = (a, b) => a + b;
console.log(sum(1, 2));
# 6.2.1.2.2 package.json
  "scripts": {
    "build": "babel src --out-dir dist --watch"
  },

# 6.2.2 插件

  • Babel 是一个编译器(输入源码 => 输出编译后的代码)。就像其他编译器一样,编译过程分为三个阶段:解析、转换和打印输出
  • 现在,Babel 虽然开箱即用,但是什么动作都不做。它基本上类似于 const babel = code => code; ,将代码解析之后再输出同样的代码。如果想要 Babel 做一些实际的工作,就需要为其添加插件
# 6.2.2.1 安装
cnpm i @babel/plugin-transform-arrow-functions -D
# 6.2.2.2 .babelrc
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

# 6.2.3 预设

  • 不想自己动手组合插件?没问题!preset 可以作为 Babel 插件的组合
  • @babel/preset-env可以让你使用最新的 JavaScript 语法,而不需要去管理语法转换器(并且可选的支持目标浏览器环境的 polyfills)
module.exports = function () {
  return { plugins: ["pluginA", "pluginB", "pluginC"] };
};
# 6.2.3.1 安装
cnpm install --save-dev @babel/preset-env
# 6.2.3.2 .babelrc
{
  "presets": ["@babel/preset-env"]
}
# 6.2.3.3 browsers
  • @babel/preset-env 会根据你配置的目标环境,生成插件列表来编译
  • 可以使用 .browserslistrc 文件来指定目标环境
  • browserslist详细配置
//.browserslistrc
> 0.25%
not dead
last 2 Chrome versions
      {
        test: /\.js?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
                ["@babel/preset-env",
                {
                  useBuiltIns:'usage',
                  corejs:2,
+                 targets: "> 0.25%, not dead",
                }]
           ]
          },
        },
      },

# 6.2.4 polyfill

  • @babel/preset-env默认只转换新的 javascript 语法,而不转换新的 API,比如 Iterator, Generator, Set, Maps, Proxy, Reflect,Symbol,Promise 等全局对象。以及一些在全局对象上的方法(比如 Object.assign)都不会转码
  • 比如说,ES6 在 Array 对象上新增了 Array.from 方法,Babel 就不会转码这个方法,如果想让这个方法运行,必须使用 babel-polyfill 来转换等
  • polyfill的中文意思是垫片,所谓垫片就是垫平不同浏览器或者不同环境下的差异,让新的内置函数、实例方法等在低版本浏览器中也可以使用
  • 官方给出@babel/polyfillbabel-runtime 两种解决方案来解决这种全局对象或全局对象方法不足的问题
  • babel-runtime适合在组件和类库项目中使用,而babel-polyfill适合在业务项目中使用。
# 6.2.4.1 polyfill
  • @babel/polyfill模块可以模拟完整的 ES2015+ 环境
  • 这意味着可以使用诸如 PromiseWeakMap 之类的新的内置对象、 Array.fromObject.assign 之类的静态方法、Array.prototype.includes 之类的实例方法以及生成器函数
  • 它是通过向全局对象和内置对象的prototype上添加方法来实现的。比如运行环境中不支持 Array.prototype.find 方法,引入 polyfill, 我们就可以使用 es6 方法来编写了,但是缺点就是会造成全局空间污染
  • V7.4.0 版本开始,@babel/polyfill 已经被废弃
# 6.2.4.1.1 安装
cnpm install --save @babel/polyfill core-js@3
cnpm install --save-dev  webpack webpack-cli babel-loader
# 6.2.4.1.2 useBuiltIns:false
  • "useBuiltIns": false 此时不对 polyfill 做操作。如果引入 @babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill

src\index.js

import "@babel/polyfill";
console.log(Array.isArray([]));
let p = new Promise();

webpack.config.js

const path = require("path");
module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].js",
  },
  module: {
    rules: [
      {
        test: /\.js?$/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: false,
                },
              ],
            ],
          },
        },
      },
    ],
  },
  plugins: [],
};

package.json

  "scripts": {
    "build": "babel src --out-dir dist --watch",
    "pack":"webpack --mode=development"
  },
# 6.2.4.1.3 useBuiltIns:entry
  • "useBuiltIns": "entry" 根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。需要在入口文件手动添加 import '@babel/polyfill',会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill
  • 这里需要指定 core-js 的版本, 如果 "corejs": 3, 则 import '@babel/polyfill' 需要改成 import 'core-js/stable';import 'regenerator-runtime/runtime'
  • core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3,比如Array.prototype.flat()

webpack.config.js

  module: {
    rules: [
      {
        test: /\.js?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
                ["@babel/preset-env",
                {
+                  useBuiltIns:'entry',
+                  corejs:2,
                  targets: "> 0.25%, not dead",
                }]
           ]
          },
        },
      },
    ]
  },

src/index.js

import "@babel/polyfill";
console.log(Array.isArray([]));
let p = new Promise((resolve) => resolve("ok"));
p.then((result) => console.log(result));

webpack.config.js

  module: {
    rules: [
      {
        test: /\.js?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
                ["@babel/preset-env",
                {
                  useBuiltIns:'entry',
+                  corejs:3,
                  targets: "> 0.25%, not dead",
                }]
           ]
          },
        },
      },
    ]
  },

src/index.js

import 'core-js/stable';
import 'regenerator-runtime/runtime'
console.log(Array.isArray([]));
let p = new Promise(resolve=>resolve('ok'));
p.then(result=>console.log(result));
# 6.2.4.1.4 useBuiltIns:usage
  • "useBuiltIns": "usage" usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加

src\index.js

console.log(Array.isArray([]));
let p = new Promise((resolve) => resolve("ok"));
p.then((result) => console.log(result));
async function fn() {}

webpack.config.js

  module: {
    rules: [
      {
        test: /\.js?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
                ["@babel/preset-env",
                {
+                  useBuiltIns:'usage',
+                  corejs:3,
                  targets: "> 0.25%, not dead",
                }]
           ]
          },
        },
      },
    ]
  },

dist\index.js

"use strict";

require("core-js/stable");

require("regenerator-runtime/runtime");

console.log(Array.isArray([]));
let p = new Promise((resolve) => resolve("ok"));
p.then((result) => console.log(result));
async function d() {}

# 6.2.5 babel-runtime

  • Babel 为了解决全局空间污染的问题,提供了单独的包babel-runtime用以提供编译模块的工具函数
  • 简单说 babel-runtime 更像是一种按需加载的实现,比如你哪里需要使用 Promise,只要在这个文件头部require Promise from 'babel-runtime/core-js/promise'就行了
# 6.2.5.1 安装
cnpm i babel-runtime -D
# 6.2.5.2 src/index.js
import Promise from "babel-runtime/core-js/promise";
const p = new Promise((resolve) => {
  resolve("ok");
});
console.log(p);
# 6.2.5.3 dist/index.js
"use strict";

var _promise = _interopRequireDefault(
  require("babel-runtime/core-js/promise")
);
function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}
const p = new _promise.default((resolve) => {
  resolve("ok");
});
console.log(p);

# 6.2.6 babel-plugin-transform-runtime

  • 启用插件babel-plugin-transform-runtime后,Babel 就会使用babel-runtime下的工具函数。

  • babel-plugin-transform-runtime插件能够将这些工具函数的代码转换成require语句,指向为对babel-runtime的引用

  • babel-plugin-transform-runtime
    

    就是可以在我们使用新 API 时自动

    import babel-runtime
    

    里面的

    polyfill
    
    • 当我们使用 async/await 时,自动引入 babel-runtime/regenerator
    • 当我们使用 ES6 的静态事件或内置对象时,自动引入 babel-runtime/core-js
    • 移除内联babel helpers并替换使用babel-runtime/helpers 来替换
  • babel-plugin-transform-runtime 自带的是 core-js@3,如果配置 corejs 配置为 2 需要单独安装@babel/runtime-corejs2

  • @babel/polyfill 自带的 core-js@2,如果配置 corejs 配置为 3 需要单独安装core-js@3

  • @babel/plugin-transform-runtime可以减少编译后代码的体积外,我们使用它还有一个好处,它可以为代码创建一个沙盒环境,如果使用 @babel/polyfill及其提供的内置程序(例如 Promise ,Set 和 Map ),则它们将污染全局范围。虽然这对于应用程序或命令行工具可能是可以的,但是如果你的代码是要发布供他人使用的库,或者无法完全控制代码运行的环境,则将成为一个问题

# 6.2.6.1 安装
cnpm i @babel/plugin-transform-runtime @babel/runtime-corejs2 -D
# 6.2.6.2 webpack.config.js
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [["@babel/preset-env",{
              targets: "> 0.25%, not dead",
            }], '@babel/preset-react'],
            plugins: [
              [
                "@babel/plugin-transform-runtime",
                {
                  corejs: 2,//当我们使用 ES6 的静态事件或内置对象时自动引入 babel-runtime/core-js
                  helpers: true,//移除内联babel helpers并替换使用babel-runtime/helpers 来替换
                  regenerator: true,//是否开启generator函数转换成使用regenerator runtime来避免污染全局域
                },
              ],
              ['@babel/plugin-proposal-decorators', { legacy: true }],
              ['@babel/plugin-proposal-class-properties', { loose: true }],
            ],
          },
        },
      },
# 6.2.6.3 src/index.js
//corejs
const p = new Promise(() => {});
console.log(p);

//helpers
class A {}
class B extends A {}
console.log(new B());
//regenerator
function* gen() {}
console.log(gen());

# 6.2.7 执行顺序

  • 插件在 Presets 前运行
  • 插件顺序从前往后排列
  • presets 顺序从后往前
let preset1 = { plugins: ["plugin5", "plugin6"] }
let preset2 = { plugins: ["plugin3", "plugin4"] }
{
  "presets": ["preset1","preset"],
  "plugins":['plugin1','plugin2']
}
plugin1 plugin2 plugin3 plugin4 plugin5 plugin6

# 6.3 sourcemap

  • sourcemap是为了解决开发代码与实际运行代码不一致时帮助我们debug到原始开发代码的技术
  • webpack通过配置可以自动给我们source maps文件,map文件是一种对应编译文件和源文件的方法
  • whyeval可以单独缓存 map,重建性能更高
  • source-map

# 6.3.1 配置项

类型 含义
source-map 原始代码 最好的 sourcemap 质量有完整的结果,但是会很慢
eval-source-map 原始代码 同样道理,但是最高的质量和最低的性能
cheap-module-eval-source-map 原始代码(只有行内) 同样道理,但是更高的质量和更低的性能
cheap-eval-source-map 转换代码(行内) 每个模块被 eval 执行,并且 sourcemap 作为 eval 的一个 dataurl
eval 生成代码 每个模块都被 eval 执行,并且存在@sourceURL,带 eval 的构建模式能 cache SourceMap
cheap-source-map 转换代码(行内) 生成的 sourcemap 没有列映射,从 loaders 生成的 sourcemap 没有被使用
cheap-module-source-map 原始代码(只有行内) 与上面一样除了每行特点的从 loader 中进行映射

# 6.3.2 关键字

  • 看似配置项很多, 其实只是五个关键字 eval、source-map、cheap、module 和 inline 的任意组合
  • 关键字可以任意组合,但是有顺序要求
关键字 含义
eval 使用 eval 包裹模块代码
source-map 产生.map 文件
cheap 不包含列信息(关于列信息的解释下面会有详细介绍)也不包含 loader 的 sourcemap
module 包含 loader 的 sourcemap(比如 jsx to js ,babel 的 sourcemap),否则无法定义源文件
inline 将.map 作为 DataURI 嵌入,不单独生成.map 文件

# 6.3.3 webpack.config.js

module.exports = {
  devtool: "source-map",
  devtool: "eval-source-map",
  devtool: "cheap-module-eval-source-map",
  devtool: "cheap-eval-source-map",
  devtool: "eval",
  devtool: "cheap-source-map",
  devtool: "cheap-module-source-map",
};

# 6.3.4 组合规则

  • [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
  • source-map 单独在外部生成完整的 sourcemap 文件,并且在目标文件里建立关联,能提示错误代码的准确原始位置
  • inline-source-map 以 base64 格式内联在打包后的文件中,内联构建速度更快,也能提示错误代码的准确原始位置
  • hidden-source-map 会在外部生成 sourcemap 文件,但是在目标文件里没有建立关联,不能提示错误代码的准确原始位置
  • eval-source-map 会为每一个模块生成一个单独的 sourcemap 文件进行内联,并使用eval执行
  • nosources-source-map 也会在外部生成 sourcemap 文件,能找到源始代码位置,但源代码内容为空
  • cheap-source-map 外部生成 sourcemap 文件,不包含列和 loader 的 map
  • cheap-module-source-map 外部生成 sourcemap 文件,不包含列的信息但包含 loader 的 map

# 6.3.5 最佳实践

# 6.3.5.1 开发环境
  • 我们在开发环境对 sourceMap 的要求是:速度快,调试更友好
  • 要想速度快 推荐 eval-cheap-source-map
  • 如果想调试更友好 cheap-module-source-map
  • 折中的选择就是 eval-source-map
# 6.3.5.2 生产环境
  • 首先排除内联,因为一方面我们为了隐藏源代码,另一方面要减少文件体积
  • 要想调试友好 sourcemap>cheap-source-map/cheap-module-source-map>hidden-source-map/nosources-sourcemap
  • 要想速度快 优先选择cheap
  • 折中的选择就是 hidden-source-map

# 6.3.6 调试代码

# 6.3.6.1 测试环境调试
  • source-map-dev-tool-plugin

    实现了对 source map 生成,进行更细粒度的控制

    • filename (string):定义生成的 source map 的名称(如果没有值将会变成 inlined)。
    • append (string):在原始资源后追加给定值。通常是 #sourceMappingURL 注释。[url] 被替换成 source map 文件的 URL
  • 市面上流行两种形式的文件指定,分别是以 @#符号开头的,@开头的已经被废弃

enablesourcemap

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  mode: 'none',
  devtool: false,
  entry: './src/index.js',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin(),
    ],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js',
    publicPath: '/',
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    compress: true,
    port: 8080,
    open: true,
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'eslint-loader',
        enforce: 'pre',
        options: { fix: true },
        exclude: /node_modules/,
      },
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [[
              '@babel/preset-env',
              {
                useBuiltIns: 'usage', // 按需要加载polyfill
                corejs: {
                  version: 3, // 指定core-js版本
                },
                targets: { // 指定要兼容到哪些版本的浏览器
                  chrome: '60',
                  firefox: '60',
                  ie: '9',
                  safari: '10',
                  edge: '17',
                },
              },
            ], '@babel/preset-react'],
            plugins: [
              ['@babel/plugin-proposal-decorators', { legacy: true }],
              ['@babel/plugin-proposal-class-properties', { loose: true }],
            ],
          },
        },
        include: path.join(__dirname, 'src'),
        exclude: /node_modules/,
      },
      { test: /\.txt$/, use: 'raw-loader' },
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] },
      { test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] },
      { test: /\.scss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },
      {
        test: /\.(jpg|png|bmp|gif|svg)$/,
        use: [{
          loader: 'url-loader',
          options: {
            esModule: false,
            name: '[hash:10].[ext]',
            limit: 8 * 1024,
            outputPath: 'images',
            publicPath: '/images',
          },
        }],
      },
      {
        test: /\.html$/,
        loader: 'html-loader',
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      minify: {
        collapseWhitespace: true,
        removeComments: true,
      },
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css',
    }),
    new OptimizeCssAssetsWebpackPlugin(),
+    new webpack.SourceMapDevToolPlugin({
+      append: '\n//# sourceMappingURL=http://127.0.0.1:8081/[url]',
+      filename: '[file].map',
+    }),
+    new FileManagerPlugin({
+      events: {
+        onEnd: {
+          copy: [{
+            source: './dist/*.map',
+            destination: 'xxx',
+          }],
+          delete: ['./dist/*.map'],
+        },
+      },
+    }),
  ],
};
# 6.3.6.2 生产环境调试
  • webpack 打包仍然生成 sourceMap,但是将 map 文件挑出放到本地服务器,将不含有 map 文件的部署到服务器

addsourcemapfile.png

# 6.4 hash、chunkhash 和 contenthash

  • 文件指纹是指打包后输出的文件名和后缀
  • hash 一般是结合 CDN 缓存来使用,通过 webpack 构建之后,生成对应文件名自动带上对应的 MD5 值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的 HTML 引用的 URL 地址也会改变,触发 CDN 服务器从源服务器上拉取对应数据,进而更新本地缓存

指纹占位符

占位符名称 含义
ext 资源后缀名
name 文件名称
path 文件的相对路径
folder 文件所在的文件夹
hash 每次 webpack 构建时生成一个唯一的 hash 值
chunkhash 根据 chunk 生成 hash 值,来源于同一个 chunk,则 hash 值就一样
contenthash 根据内容生成 hash 值,文件内容相同 hash 值就相同

# 6.4.1 hash 计算

variableHashs.jpg

function createHash() {
  return require("crypto").createHash("md5");
}
let entry = {
  entry1: "entry1",
  entry2: "entry2",
};
let entry1 = "require depModule1"; //模块entry1
let entry2 = "require depModule2"; //模块entry2

let depModule1 = "depModule1"; //模块depModule1
let depModule2 = "depModule2"; //模块depModule2
//如果都使用hash的话,因为这是工程级别的,即每次修改任何一个文件,所有文件名的hash至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效
let hash = createHash()
  .update(entry1)
  .update(entry2)
  .update(depModule1)
  .update(depModule2)
  .digest("hex");
console.log("hash", hash);
//chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。
//在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响
let entry1ChunkHash = createHash()
  .update(entry1)
  .update(depModule1)
  .digest("hex");
console.log("entry1ChunkHash", entry1ChunkHash);

let entry2ChunkHash = createHash()
  .update(entry2)
  .update(depModule2)
  .digest("hex");
console.log("entry2ChunkHash", entry2ChunkHash);

let entry1File = entry1 + depModule1;
let entry1ContentHash = createHash().update(entry1File).digest("hex");
console.log("entry1ContentHash", entry1ContentHash);

let entry2File = entry2 + depModule2;
let entry2ContentHash = createHash().update(entry2File).digest("hex");
console.log("entry2ContentHash", entry2ContentHash);

# 6.4.2 hash

  • Hash 是整个项目的 hash 值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 发生改变
module.exports = {
  entry: {
    main: './src/index.js',
    vender:['lodash']
  },
  output:{
     path:path.resolve(__dirname,'dist'),
    filename:'[name].[hash].js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].[hash].css"
    })
  ]
};

# 6.4.2 chunkhash

  • chunkhash, 采用 hash 计算的话,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。这样子是没办法实现缓存效果,我们需要换另一种哈希值计算方式,即 chunkhash
  • chunkhash 和 hash 不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用 chunkhash 的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响
module.exports = {
  entry: {
    main: './src/index.js',
    vender:['lodash']
  },
  output:{
    path:path.resolve(__dirname,'dist'),
    filename:'[name].[chunkhash].js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name].[chunkhash].css"
    })
  ]
};

# 6.4.3 contenthash

  • 使用 chunkhash 存在一个问题,就是当在一个 JS 文件中引入 CSS 文件,编译后它们的 hash 是相同的,而且只要 js 文件发生改变 ,关联的 css 文件 hash 也会改变,这个时候可以使用mini-css-extract-plugin里的contenthash值,保证即使 css 文件所处的模块里就算其他文件内容改变,只要 css 文件内容不变,那么不会重复构建
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
+      filename: "css/[name].[contenthash].css"
    })
  ],
};

# 最后

文中若有不准确或错误的地方,欢迎指出,有兴趣可以的关注下Github~

Last Updated: 2020/12/5 下午5:50:45