前端工程化二-webpack原理

webpack原理

tree shaking原理

Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)

Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对javascript来说更有意义。

Tree-shaking 和传统的 DCE的方法又不太一样,传统的DCE 消灭不可能执行的代码,而Tree-shaking 更关注宇消除没有用到的代码。

Dead Code 一般具有以下几个特征

•代码不会被执行,不可到达

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

•代码只会影响死变量(只写不读)

JavaScript代码的tree shaking不是rollup,webpack,cc做的,而是著名的代码压缩优化工具uglify,uglify完成了javascript的DCE。

模块必须采用ES6 Module语法,因为treeShaking依赖ES6的静态语法:import 和 export。如果项目中使用了babel的话, @babel/preset-env默认将模块转换成CommonJs语法,因此需要设置module:false,webpack2后已经支持ESModule。

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

原理总结:

  • 只能作为模块顶层的语句出现
  • import的模块名只能是字符串常量
  • import binding 是 immutable的,引入的模块不能再进行修改

代码删除:

  • uglify:判断程序流,判断变量是否被使用和引用,进而删除代码

tree shaking具体能做的事情:

1.Webpack Tree shaking从ES6顶层模块开始分析,可以清除未使用的模块

2.Webpack Tree shaking会对多层调用的模块进行重构,提取其中的代码,简化函数的调用结构

3.Webpack Tree shaking不会清除IIFE(立即调用函数表达式):因为IIFE比较特殊,它在被翻译时(JS并非编译型的语言)就会被执行,Webpack不做程序流分析,它不知道IIFE会做什么特别的事情,所以不会删除这部分代码

4.Webpack Tree shaking对于IIFE的返回函数,如果未使用会被清除

5.Webpack Tree shaking结合第三方包使用,引入第三方包时不同的方式会造成不同的优化

//全部引入,webpack不能清理
import _ from 'lodash'
import {last} from 'lodash'
//第三种打包体积减少
import last from 'lodash/last'; 

链接:https://juejin.cn/post/6844903544756109319

side effects是指那些当import的时候会执行一些动作,但是不一定会有任何export。比如ployfill,ployfills不对外暴露方法给主程序使用。

tree shaking 不能自动的识别哪些代码属于side effects,因此手动指定这些代码显得非常重要,如果不指定可能会出现一些意想不到的问题。

webapck中,是通过package.jsonsideEffects属性来实现的。

{
  "name": "tree-shaking",
  "sideEffects": false
}

如果所有代码都不包含副作用,我们就可以简单地将该属性标记为false,来告知 webpack,它可以安全地删除未用到的export导出。

如果你的代码确实有一些副作用,那么可以改为提供一个数组:

{
  "name": "tree-shaking",
  "sideEffects": [
    "./src/common/polyfill.js"
  ]
}

HMR原理

https://juejin.cn/post/7049608872553611301#heading-7

Hot Module Replace是webpack一个重要的特性,当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。

HMR的工作原理

1.webpack --watch启动监听模式之后,webpack第一次编译项目,并将结果存储在内存文件系统,相比较磁盘文件读写方式内存文件管理速度更快,内存webpack服务器通知浏览器加载资源,浏览器获取的静态资源除了JS code内容之外,还有一部分通过webpack-dev-server注入的的 HMR runtime代码,作为浏览器和webpack服务器通信的客户端( webpack-hot-middleware提供类似的功能)。

2.文件系统中一个文件(或者模块)发生变化,webpack监听到文件变化对文件重新编译打包,每次编译生成唯一的hash值,根据变化的内容生成两个补丁文件:说明变化内容的manifest(文件格式是hash.hot-update.json,包含了hash和chunkId用来说明变化的内容)和chunk js(hash.hot-update.js)模块。

3.hrm-server通过websocket将manifest推送给浏览器

浏览器接受到最新的hotCurrentHash,触发 hotDownloadManifest 函数,获取manifest json 文件。

4.浏览器端hmr runtime根据manifest的hash和chunkId使用ajax拉取最新的更新模块chunk

5.触发render流程实现局部热重载

HMR runtime 调用window["webpackHotUpdate"] 方法,调用hotAddUpdateChunk

HMR相关的中间件

webpack-dev-server 提供了实时重加载的功能,但是不能局部刷新。必须配合后两步的配置才能实现局部刷新,这两步的背后其实是借助了HotModuleReplacementPlugin

  1. webpack-dev-middleware

本质上是一个容器,将webpack处理后的文件传递个服务器。

webpack-dev-middleware 是一个 express 中间件,核心实现两个功能:第一通过file-loader内部集成了node的 monery-fs/memfs 内部文件系统,,直接将资源存储在内存;第二是通过watch监听文件的变化,动态编译。

2.webpack-hot-middleware

核心是给webpack提高服务端和客户端之间的通信机制,内部使用windoe.EventSocurce实现。

在webpack第一次打包的时候,除了代码本身之外,还包含一部分HMRruntime订阅服务代码,HMRruntime 订阅服务端的更新变化,触发HMR runtime API拉取最新的资源模块。

webpack-hot-middleware实现页面的热重载。

webpack-dev-server内置了webpack-dev-middleware和express服务器,利用webpack-dev-middleware提供文件的监听和编译,利用express提供http服务,底层利用websocket代替EventSource实现了webpack-hot-middleware提供的客户端和服务器之间的通信机制。

webpack生命周期/构建原理

从启动构建到输出结果一系列过程:

(1)初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果。

(2)开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的 run 方法开始执行编译。

(3)确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去。

(4)编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。

(5)完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry配置生成代码块chunk。

(6)输出完成:输出所有的chunk到文件系统。

注意:在构建生命周期中有一系列插件在做合适的时机做合适事情,比如UglifyPlugin会在loader转换递归完对结果使用UglifyJs压缩覆盖之前的结果。

Tapable

webpack本质是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是tapable。Webpack最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的子类,并且实例内部的声明周期也是通过Tapable库提供的钩子类实现的。

Tapable的功能类似于Node的Event Emitter,但是更强大,它包含了多种不同的监听和触发事件的方式

tapable的钩子函数有

const {
  SyncHook, //普通同步串行,不关心监听函数的返回值
  SyncBailHook,  //同步串行,只要监听函数的有一个函数的返回值不为null则跳出该循环
  SyncWaterfallHook, //同步串行,上一个监听函数的返回值作为参数传递给下一个监听函数
  SyncLoopHook, //同步串行,如果监听函数返回值则循环执行,直至返回undefined
  AsyncParallelHook, //异步并行,不关心函数返回值
  AsyncParallelBailHook, //异步并行,只要监听函数的有一个函数的返回值不为null则跳出该循环
  AsyncSeriesHook, //异步串行,不关心函数返回值
  AsyncSeriesBailHook, //异步串行,不关心函数返回值
  AsyncSeriesWaterfallHook //异步串行,不关心函数返回值
} = require('tapable')

共有9种钩子类型,普通同步的、异步的、瀑布流、并行、串行、循环类型等

注册事件回调的方法有三个,tap、tapAsync和tapPromise。其中tapAsync和tapPromise不能用于Sync开头的钩子类

webpack与gulp、grunt区别

webpack 是 module bundle

gulp 是 tast runner

Rollup 是在 Webpack 流行后出现的替代品。Rollup 在用于打包 JavaScript 库时比 Webpack 更加有优势,因为其打包出来的代码更小更快。 但功能不够完善,很多场景都找不到现成的解决方案。

构建Chunk图

构建 Chunk 图的逻辑其实很简单,

  1. 遍历当前模块的导入语句
  2. 遇到静态 import,就将引入的 module 放到当前 Chunk 中,然后继续遍历该 module 的所有导入
  3. 遇到动态 import,就新创建一个 Chunk,将引入的 module 放到新 Chunk 中,新 Chunk 变成当前 Chunk,继续遍历 module 即可

concatenateModules

也有人称作 scope hoisting,直译过来是连接 module,我们知道很多时候大家觉得 webpack 不够好的原因一般有3点

  1. 产物太难看了,第一次看 webpack 产物会发现充满了意义不明的注释作为缩进,以及一堆 webpack 特有的 runtime 函数,并且每个模块包在一个函数中,调用起来像是跑 cjs 一样,总感觉性能也不会好
  2. 构建性能太差了,大项目打包时间10分钟往上的也不少
  3. 配置项太细节太多(作为底层的构建工具不好说这就是缺点

类似 Rollup esbuild 等轻量 runtime 的打包工具,chunk 就是 module 拼接起来而已,因此看着干净,实际上 webpack 在生产环境也是这样的,对于纯 esm 模块,webpack 也会简单的将 module 拼接在一起,产物和 Rollup 等其实是一样的。而对于 cjs 模块,Rollup 需要 commonjs 插件提供少量 cjs runtime(将 cjs module 用函数包一层,require 的时候就相当于调用该函数,获取函数返回值,以及一些 cjs 和 esm 交互的 runtime),esbuild 则自带 cjs runtime

enhanced-resolve

webpack 使用 enhanced-resolve 进行路径解析。它的作用类似于一个异步的 require.resolve 方法,将 require / import 语句中引入的字符串,解析为引入文件的绝对路径

enhanced-resolve 的原理可以简单理解成是一个管道pipeline进行解析,从最初的地方传入要解析的路径,经过一个个插件解析,最终返回文件路径或报错。

解析器是使用 enhanced-resolve 库创建的。Resolver 类 拓展了 tapable 类,并使用 tapable 来提供了一些钩子。 enhanced-resolve 可以直接用于创建新的解析器, 但是,任何 compiler 实例 都有一些解析器实例,可以 被 tap 进去

在 Resolver.js 中,enhance-resolve 默认只有 4 个 hook

使用

const resolve = require("enhanced-resolve");

resolve("/some/path/to/folder", "module/dir", (err, result) => {
	result; // === "/some/path/node_modules/module/dir/index.js"
});

resolve.sync("/some/path/to/folder", "../../dir");
// === "/some/path/dir/index.js"

const myResolve = resolve.create({
	// or resolve.create.sync
	extensions: [".ts", ".js"]
	// see more options below
});

myResolve("/some/path/to/folder", "ts-module", (err, result) => {
	result; // === "/some/node_modules/ts-module/index.ts"
});

创建一个resolver类

const fs = require("fs");
const { CachedInputFileSystem, ResolverFactory } = require("enhanced-resolve");

// create a resolver
const myResolver = ResolverFactory.createResolver({
	// Typical usage will consume the `fs` + `CachedInputFileSystem`, which wraps Node.js `fs` to add caching.
	fileSystem: new CachedInputFileSystem(fs, 4000),
	extensions: [".js", ".json"]
	/* any other resolver options here. Options/defaults can be seen below */
});

// resolve a file with the new resolver
const context = {};
const lookupStartPath = "/Users/webpack/some/root/dir";
const request = "./path/to-look-up.js";
const resolveContext = {};
myResolver.resolve(context, lookupStartPath, request, resolveContext, (
	err /*Error*/,
	filepath /*string*/
) => {
	// Do something with the path
});

源码解析: https://juejin.cn/post/6844904056633327630#heading-12

编译原理

https://zhuanlan.zhihu.com/p/419438023

分离/合并配置

安装webpack-merge

npm i webpack-merge -D

在webpackjs中引入别的webpack配置

const {merge} = require('webpack-merge');
const commonConfig = require('./webpack.common');

const prodConfig = {
    mode:'production',
    //可以找出报错代码的源代码路径
    //cheap 只报告行号(不加会报告列)  module 会报告loader及第三方依赖的错误位置
    //加了inline就不产生映射文件了
    //生产环境不需要eval
    devtool:'cheap-module-source-map',
    plugins: [
    ]
}

module.exports = merge(commonConfig,prodConfig);

webpack-chain

使用链式的方式调用webpack的api

安装

npm install --save-dev webpack-chain

使用

const Config = require('webpack-chain');

const config = new Config();

config
  // Interact with entry points
  .entry('index')
    .add('src/index.js')
    .end()
  // Modify output settings
  .output
    .path('dist')
    .filename('[name].bundle.js');

// Create named rules which can be modified later
config.module
  .rule('lint')
    .test(/\.js$/)
    .pre()
    .include
      .add('src')
      .end()
    // Even create named uses (loaders)
    .use('eslint')
      .loader('eslint-loader')
      .options({
        rules: {
          semi: 'off'
        }
      });

config.module
  .rule('compile')
    .test(/\.js$/)
    .include
      .add('src')
      .add('test')
      .end()
    .use('babel')
      .loader('babel-loader')
      .options({
        presets: [
          ['@babel/preset-env', { modules: false }]
        ]
      });

// Create named plugins too!
config
  .plugin('clean')
    .use(CleanPlugin, [['dist'], { root: '/dir' }]);

// Export the completed configuration object to be consumed by webpack
module.exports = config.toConfig();

Webpack插件汇总

功能类

html-webpack-plugin

自动生成html,基本用法:

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

copy-webpack-plugin

拷贝资源插件,copy-webpack-plugin不是为了复制构建过程中生成的文件,相反,它是复制源树中已经存在的文件,作为构建过程的一部分

基本用法:

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

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

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

webpack-manifest-plugin 基本用法:

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

assets-webpack-plugin 基本用法:

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

clean-webpack-plugin

在编译之前清理指定目录指定内容,基本用法

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

compression-webpack-plugin

提供带 Content-Encoding 编码的压缩版的资源,基本用法

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

progress-bar-webpack-plugin

编译进度条插件,基本用法

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

speed-measure-webpack-plugin

进行构建速度分析,可以看到各个 loader、plugin 的构建时长,后续可针对耗时 loader、plugin 进行优化。

npm i -D speed-measure-webpack-plugin

使用

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
  // ...webpack config...
})

lodash-webpack-plugin

替换lodash中的一些函数

var LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
var webpack = require('webpack');

module.exports = {
  'module': {
    'rules': [{
      'use': 'babel-loader',
      'test': /\.js$/,
      'exclude': /node_modules/,
      'options': {
        'plugins': ['lodash'],
        'presets': [['env', { 'modules': false, 'targets': { 'node': 4 } }]]
      }
    }]
  },
  'plugins': [
    new LodashModuleReplacementPlugin,
    new webpack.optimize.UglifyJsPlugin
  ]
};

代码相关类

webpack.ProvidePlugin

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

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

webpack.DefinePlugin

定义全局常量,用法:

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

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

提取css样式,对比:

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

extract-text-webpack-plugin基本用法 :

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

mini-css-extract-plugin 基本用法:

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

编译结果优化类

wbepack.IgnorePlugin

忽略regExp匹配的模块,用法

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

uglifyjs-webpack-plugin

代码丑化,用于js压缩,

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

optimize-css-assets-webpack-plugin

css压缩,主要使用 cssnano 压缩器,用法

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

webpack-md5-hash

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

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

SplitChunksPlugin

CommonChunkPlugin 的后世,用于chunk切割。

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

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

webpack-obfuscator

混淆js代码,为了不完全泄露源码,需要对给出的代码进行加密混淆,前端代码虽然无法做到完全加密混淆,但是通过使用 obfuscator 通过增加随机废代码段、字符编码转义等方法可以使构建代码完全混淆,达到无法恢复源码甚至无法阅读的目的。

npm install --save-dev webpack-obfuscator 

配置

// webpack.config.js
const JavaScriptObfuscator = require('webpack-obfuscator');
module.exports = {
  entry: {
    'abc': './test/input/index.js',
    'cde': './test/input/index1.js'
  },
  output: {
    path: 'dist',
    filename: '[name].js'
  },
  plugins: [
    new JavaScriptObfuscator({
      rotateUnicodeArray: true
      // 数组内是需要排除的文件
    }, ['abc.js'])
  ]
};

webpack-deadcode-plugin

检测无效代码

const DeadCodePlugin = require('webpack-deadcode-plugin');

const webpackConfig = {
  ...
  optimization: {
    usedExports: true,
  },
  plugins: [
    new DeadCodePlugin({
      patterns: [
        'src/**/*.(js|jsx|css)',
      ],
      exclude: [
        '**/*.(stories|spec).(js|jsx)',
      ],
    })
  ]
}

js-minifycaition-benchmarks

https://github.com/privatenumber/minification-benchmarks

比较各个js压缩插件的运行速度和压测,选出最适合你的js打包压缩工具

编译优化类

DllPlugin && DllReferencePlugin && autodll-webpack-plugin

dllPlugin 将模块预先编译,DllReferencePlugin 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块。

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

dllPlugin 基本用法:

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

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

DllReferencePlugin 基本用法:

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

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

autodll-webpack-plugin 基本用法:

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

happypack && thread-loader

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

happypack 基本用法:

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

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

thread-loader 基本用法:

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

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

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

hard-source-webpack-plugin ,基本用法:

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

cache-loader 基本用法:

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

编译分析类

webpack-bundle-analyzer

编译模块分析插件,基本用法

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

stats-webpack-plugin && PrefetchPlugin

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

stats-webpack-plugin 基本用法:

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

PrefetchPlugin 基本用法:

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

speed-measure-webpack-plugin

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

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

react相关

react-refresh-webpack-plugin

热更新 react 组件。

React Fast Refresh 是 React 官方为 React Native 开发的模块热替换(HMR)方案,由于其核心实现与平台无关,所以官方将它作为纯用户解决方案,web 也能使用。同时 react-hot-loader 也随之被取代。

React Fast Refresh 具有更低的侵入性,它不需要在代码中加入hot(App),同时官方支持的光环之外,还带来性能与稳定性保障。使用过程中,除了提供编辑后的及时反馈。还有个好处就是,对 hook 更完善的支持,在刷新的时候,组件状态可以得以保留。

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

使用

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

module.exports = {
    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new ReactRefreshWebpackPlugin(),
    ]
}

然后通过webpack-dev-server启动,hot选项是必须的

https://zhuanlan.zhihu.com/p/374318806

按需导入

antd按需导入

babel-plugin-import

yarn add babel-plugin-import

在webpack里配置

plugins: [
  [
    "import",
    {
      "libraryName": "antd",
      "libraryDirectory": "es",
      "style": true //设置为true即是less
    }
  ]
]

lodash按需导入

可以借助于lodash-webpack-plugin,去除未引入的模块,需要和babel-plugin-lodash插件配合使用。类似于webpacktree-shaking

npm i -S lodash-webpack-plugin babel-plugin-lodash

在webpack中配置

var LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
plugins: [ new LodashModuleReplacementPlugin()]

{
  test: /\.(js|jsx)$/,
  loader: 'babel-loader',
  exclude: /node_modules/,
  include: [resolve('src'), resolve('test')]
  options: {plugins: ['lodash']}
}

无需导入

unplugin-auto-import

在文件中自动导入包,无需引入

npm i -D unplugin-auto-import

使用

// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  plugins: [
    AutoImport({ /* options */ }),
  ],
})

// webpack.config.js
module.exports = {
  /* ... */
  plugins: [
    require('unplugin-auto-import/webpack').default({ /* options */ }),
  ],
}

service-worker

workbox-webpack-plugin

https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin?hl=zh-cn

ts相关

fork-ts-checker-webpack-plugin

fork-ts-checker-webpack-plugin 的工作原理是,在 Webpack 构建过程中,它会 fork 出一个子进程来执行 TypeScript 类型检查。这个子进程与主进程并行运行,互不影响。这意味着类型检查任务可以在不阻塞其他构建任务的情况下进行,从而充分利用多核 CPU 的计算能力。

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  context: __dirname, // to automatically find tsconfig.json
  entry: './src/index.ts',
  resolve: {
    extensions: [".ts", ".tsx", ".js"],
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        // add transpileOnly option if you use ts-loader < 9.3.0 
        // options: {
        //   transpileOnly: true
        // }
      }
    ]
  },
  plugins: [new ForkTsCheckerWebpackPlugin()]
};

tsconfig-paths-webpack-plugin

配置tsconfig的路径

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = {
  ...
  resolve: {
    plugins: [new TsconfigPathsPlugin({/* options: see below */})]
  }
  ...
}

上传oss

如果希望webpack可以自动上传静态资源(如js,css等)到阿里云oss上,可以使用webpack-aliyun-oss这款插件

安装

npm i webpack-aliyun-oss -D

配置

const WebpackAliyunOss = require('webpack-aliyun-oss');
const webpackConfig = {
  // ... 省略其他
  plugins: [new WebpackAliyunOss({
    from: ['./build/**', '!./build/**/*.html'],//排除html文件
    dist: 'path/in/alioss',
    region: 'your region',
    accessKeyId: 'your key',
    accessKeySecret: 'your secret',
    bucket: 'your bucket',
    
    // 如果希望重新组织上传路径,可以传这个函数
    // 否则按构建目录的结构上传
    setOssPath(filePath) {
      // filePath为当前文件路径,函数应该返回路径+文件名,如/new/path/to/file.js,则最终上传路径为 path/in/alioss/new/path/to/file.js
      
      return '/new/path/to/file.js';
    },
    
    // 如果想定义header就传
    setHeaders(filePath) {
      // 定义当前文件header,可选
      return {
        'Cache-Control': 'max-age=31536000'
      }
    }
  })]
}
  • from: 上传哪些文件,支持类似gulp.src的glob方法,如'./build/**', 可以为glob字符串或者数组。

    • 作为插件使用时:可选,默认为output.path下所有的文件。
    • 独立使用时:必须,否则不知道从哪里取图片:)
  • dist: 上传到oss哪个目录下,默认为oss根目录。可作为路径前缀使用。
  • region: 阿里云上传区域
  • accessKeyId: 阿里云的授权accessKeyId
  • accessKeySecret: 阿里云的授权accessKeySecret
  • bucket: 上传到哪个bucket
  • timeout: oss超时设置,默认为30秒(30000)
  • verbose: 是否显示上传日志,默认为true
  • deletOrigin: 上传完成是否删除原文件,默认false
  • deleteEmptyDir: 如果某个目录下的文件都上传到cdn了,是否删除此目录。deleteOrigin为true时候生效。默认false。
  • setOssPath: 自定义上传路径的函数。接收参数为当前文件路径。不传,或者所传函数返回false则按默认路径上传。(默认为output.path下文件路径)
  • setHeaders: 配置headers的函数。接收参数为当前文件路径。不传,或者所传函数返回false则不设置header。
  • test: 测试,仅显示要上传的文件,但是不执行上传操作。默认false

esbuild-loader

esbuild-loader允许在webpack中使用esbuild,可以替代babel-loader进行polyfill,也可以压缩最终打包后的js,替代uglifyjs

替代ts-loader和babel-loader

const { ESBuildMinifyPlugin } = require('esbuild-loader') 
module.exports = {
    module: {
      rules: [
-       {
-         test: /\.js$/,
-         use: 'babel-loader',
-       },
+       {
+         test: /\.js$/,
+         loader: 'esbuild-loader',
+         options: {
+           loader: 'jsx',  // Remove this if you're not using JSX
+           target: 'es2015'  // Syntax to compile to (see options below for possible values)
+.          tsconfigRaw: require('./tsconfig.json')
+         }
+       },
-       {
-         test: /\.tsx?$/,
-         use: 'ts-loader'
-       },
+       {
+         test: /\.tsx?$/,
+         loader: 'esbuild-loader',
+         options: {
+           loader: 'tsx',  // Or 'ts' if you don't need tsx
+           target: 'es2015'
+         }
+       },
        ...
      ],
    },
+   optimization: {
+     minimizer: [
+       new ESBuildMinifyPlugin({
+         target: 'es2015'  // Syntax to compile to (see options below for possible values)
+       })
+     ]
+   },
  }

webpackbar

https://github.com/unjs/webpackbar

webpack-spritesmith

webpack-spritesmith生成精灵图

https://juejin.cn/post/6844903918539898894

利用source-map还原代码

webpack打包时如果有配置sourcemap,可能在生成js时生成对应的map文件,而利用这个文件甚至可以还原js和组件

reverse-sourcemap

npm install --global reverse-sourcemap

找到webpack生成的js对应的map文件,一般为js.map文件,进行还原

webpack学习资源

wepack:https://www.kancloud.cn/sllyli/webpack/1242354

深入浅出webpack:https://webpack.wuhaolin.cn/

wepack指南: https://Tsejx.github.io/webpack-guidebook/

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github