Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于webpack的公共代码提取和运行 #51

Open
Luin-Li opened this issue Jul 6, 2020 · 1 comment
Open

关于webpack的公共代码提取和运行 #51

Luin-Li opened this issue Jul 6, 2020 · 1 comment

Comments

@Luin-Li
Copy link
Contributor

Luin-Li commented Jul 6, 2020

关于webpack的公共代码提取和运行

  • CommonsChunkPlugin
  • 打包后文件分析

CommonsChunkPlugin

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

基本配置

{
  name: string,
  names: string[],
  filename: string,
  minChunks: number|Infinity|function(module, count) => boolean,
  // 在传入  公共chunk(commons chunk) 之前所需要包含的最少数量的 chunks 。
  // 数量必须大于等于2,或者少于等于 chunks的数量
  // 传入 `Infinity` 会马上生成 公共chunk,但里面没有模块。
  // 你可以传入一个 `function` ,以添加定制的逻辑(默认是 chunk 的数量)

  chunks: string[],
  children: boolean,
  deepChildren: boolean,
  // 如果设置为 `true`,所有公共 chunk 的后代模块都会被选择

  async: boolean|string,
  minSize: number,
  // 在 公共chunk 被创建立之前,所有 公共模块 (common module) 的最少大小。
}

name: 名称,

    1. 可以是已经存在的chunk(一般指入口文件)对应的name,那么就会把公共模块代码合并到这个chunk上;否则,会创建名字为name的commons chunk进行合并
module.exports = {
entry: {
  main: './main.js',
  vendor: ['vue']
},
...
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: Infinity,
  }),
]
}
以上会打包出main和vendor两个文件,其中vendor提取了vue代码
module.exports = {
entry: {
  main: './main.js',
},
...
plugins: [
  new webpack.optimize.CommonsChunkPlugin({  
          name: 'vendor',
          minChunks: function(module) {
            return (
              module.resource &&
              /\.js$/.test(module.resource) &&
              module.resource.indexOf(
                path.join(__dirname, '../node_modules')
              ) === 0
            )
          },
      }),
]
}
以上会打包出main和vendor两个文件,其中vendor提取了公共包
    1. 如果一个数组被传入,这相当于插件针对每个 chunk 名被多次调用
plugins: [
      new webpack.optimize.CommonsChunkPlugin({
          name: ['vendor','runtime'],
          filename: '[name].js'
      }),
  ]

等同于

plugins: [
      new webpack.optimize.CommonsChunkPlugin({
          name: 'vendor',
          filename: '[name].js'
      }),
      new webpack.optimize.CommonsChunkPlugin({
          name: 'runtime',
          filename: '[name].js',
          chunks: ['vendor']
      }),
  ]
    1. 如果该选项被忽略,同时 options.async 或者 options.children 被设置,所有的 chunk 都会被使用,否则 options.filename 会用于作为 chunk 名。

    chunks: 通过 chunk name 去选择 chunks 的来源。chunk 必须是公共chunk 的子模块。如果被忽略,所有的入口chunk (entry chunk) 都会被选择。

module.exports = {
entry: {
  main: './main.js',
  vendor: ['vue']
},
...
plugins: [
  new webpack.optimize.CommonsChunkPlugin({
          name: 'vendor',
          minChunks: Infinity,
      }),
  new webpack.optimize.CommonsChunkPlugin({
          name: 'common',
          chunks: ['main'],
          minChunks: function(module) {
            return (
              module.resource &&
              /\.js$/.test(module.resource) &&
              module.resource.indexOf(
                path.join(__dirname, '../node_modules')
              ) === 0
            )
          },
      }),
    new webpack.optimize.CommonsChunkPlugin({
          name: 'manifest',
          chunks: ['vendor', 'common', 'main']
      }),
]
}
以上会打包出main,vendor,common和manifest四个文件,其中`vendor`提取了vue代码,`common`包含了一些公共包(除去vue),`manifest`就可以简单理解为模块映射关系的集合

理解vue-cliwebpack默认配置中对于CommonsChunkPlugin的配置:

// webpack.base.conf.js ???
new webpack.optimize.CommonsChunkPlugin('common.js')

// webpack.prod.conf.js
module.exports = {
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    // 第四步
    // `chunkFilename`: 用来指定异步加载的模块名字,异步加载模块中的共同引用到的模块就会被合并到async中指定名字。
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // 第三步
    new webpack.HashedModuleIdsPlugin(),
    // 第一步
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // 第二步
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // 第四步
    new webpack.optimize.CommonsChunkPlugin({
      async: 'vendor-async',
      children: true
    }),
  ]
}

分步理解:

  1. 第一步:提取公共包
    优化1

  2. 第二步:提取公共包映射关系
    manifest: webpack打包默认模块,通过 manifest,webpack 能够对「你的模块映射到输出 bundle 的过程」保持追踪。

当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

优化2前
优化2后

  1. 第三步:引入HashedModuleIdsPlugin固定模块id

为了便于看出效果,我们可以尝试将vue和其他node包分开打包,使用配置如下:

module.exports = {
   entry: {
       main: './main.js',
       // 新增的
       vue: ['vue']
   },
   output: {
     filename: '[name][chunkhash].js',
     path: path.resolve(__dirname, '../dist'),
     publicPath: './',
   },
    plugins: [
        ...// 省略了一些配置
        // 新增的
        new webpack.optimize.CommonsChunkPlugin({
          name: 'vue',
          minChunks: Infinity,
        }),
        new webpack.HashedModuleIdsPlugin(),

        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function(module) {
              return (
                module.resource &&
                /\.js$/.test(module.resource) &&
                module.resource.indexOf(
                  path.join(__dirname, '../node_modules')
                ) === 0
              )
            },
            // 新增的
            chunks: ['main'],
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest',
            chunks: ['vendor', 'common', 'main', 'vue']
        }),
    ],
    
  }

main.js中对依赖包jquery分别进行移除操作,未添加HashedModuleIdsPlugin
优化3前

添加了HashedModuleIdsPlugin
优化3后

因为未添加HashedModuleIdsPlugin时,模块id是根据webpack的解析顺序增量的,如果变换解析顺序,那模块id也会随之改变,所以需要使用HashedModuleIdsPlugin,它是根据模块相对路径生成模块标识,如果模块没有改变,那模块标识也不会改变。

  1. 第四步:使用async属性处理,异步加载时提取公共代码:

当我们使用异步加载代码时,而且test1test2同时使用了第三方包,如jquery如下:

import Vue from 'vue';
const a = 1 + 1;
new Vue({
    el: '#app',
    data: {
      vue_test: 'vue is loaded!'
    }
})
import('./test1').then(foo => {
});
import('./test2').then(foo => {
});

此时,如果没有使用async属性,打包结果如下:
优化4前

使用async属性,打包结果如下:
优化4后

-------------------------------------------------这是分割线--------------------------------------------------

Webpack4之SplitChunksPlugin

webpack官网:

The CommonsChunkPlugin 已经从 webpack v4 legato 中移除。想要了解在最新版本中如何处理 chunk,请查看 SplitChunksPlugin。

现有CommonsChunkPlugin的问题:CommonsChunkPlugin的思路是基于父子关系的,使得其只能统一抽取到父chunk,造成父chunk过大,不可避免的存在重复引入,引入多余代码。

例如:我们打包多页面时候:

// webpack.js
const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
    entry: {
        main1: './main.js',
        main4: './main4.js',
    },
    output: {
        filename: '[name][chunkhash].js',
        path: path.resolve(__dirname, '../dist'),
        publicPath: './',
    },
    plugins: [
        new CleanWebpackPlugin(['dist'], { root: process.cwd() }),
        new HtmlWebpackPlugin({
            filename: 'pagea.html',
            template: 'index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true
            },
            chunks: ['manifest', 'main1', 'vendor'],
        }),
        new HtmlWebpackPlugin({
            filename: 'pageb.html',
            template: 'index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true
            },
            chunks: ['manifest', 'main4', 'vendor'],
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function(module) {
              return (
                module.resource &&
                /\.js$/.test(module.resource) &&
                module.resource.indexOf(
                  path.join(__dirname, '../node_modules')
                ) === 0
              )
            },
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest',
        }),
    ],
}

其中main.js引入了vuejquery,而main4.js只引入了vue,打包后的结果:

split4

打包后,vender包含了vuejquery,也就是说当我们在加载pageb的时候,多加载不需要的jquery代码。

SplitChunksPlugin的思路
引入chunkGroup的概念,在入口chunk和异步chunk中发现被重复使用的模块,将重复模块以vendor-chunk的形式分离出来,也就是vendor-chunk可能有多个,不再受限于所有chunk中都共同存在的模块。

区别

升级了webpack4之后,production模式下,SplitChunksPlugin插件是默认被启用的,默认配置如下:

splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
    	default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

也就是说,默认情况下,webpack会根据下述条件自动进行代码块分割:

  • 新代码块可以被共享引用,或者这些模块都是来自node_modules文件夹里面
  • 新代码块大于30kb(min+gziped之前的体积)
  • 按需加载的代码块,并行请求最大数量应该小于或者等于5
  • 初始加载的代码块,并行请求最大数量应该小于或等于3

关于SplitChunksPlugin不再做多的介绍,有需要可以查看没有了CommonsChunkPlugin,咱拿什么来分包(译)

打包后文件分析

为了方便查看,我们用最简单的配置去打包,以下为打包配置:

const webpack = require('webpack')
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    entry: {
        main: './main.js',
    },
    output: {
        filename: '[name][chunkhash].js',
        path: path.resolve(__dirname, '../dist'),
        publicPath: './',
    },
    plugins: [
        new CleanWebpackPlugin(['dist'], { root: process.cwd() }),
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: 'index.html',
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeAttributeQuotes: true
            },
        }),
        new BundleAnalyzerPlugin(),
        new webpack.HashedModuleIdsPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
    ],
}

一般情况

从 main.js 开始看代码

<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title></title></head><body><div id=app></div><div id=jq></div><script type=text/javascript src=./manifestce30c729b8270ce6a88d.js></script><script type=text/javascript src=./main189de2d1b19ec3394c54.js></script></body></html>

可以看到,打包后 js 文件的加载顺序是先manifest.js,之后才是main.js,这里先看看 main.js 的内容:

// main.js
webpackJsonp([0],{

/***/ "I6/Z":
/***/ (function(module, exports) {
module.exports = 1;
/***/ }),
/***/ "eitI":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__test2__ = __webpack_require__("I6/Z");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__test2___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__test2__);
const a = 1 + 1;
console.log(__WEBPACK_IMPORTED_MODULE_0__test2___default.a)
/***/ })

},["eitI"]);

删去一些无关代码,首先关注到的是webpackJsonp这个函数,可以看见是不在任何命名空间下的,也就是manifest.js应该定义了一个挂在window下的全局函数main.js往这个函数传入三个参数并调用。

这里先记住两个点:

  1. 第三个参数的值是eitI,与参数2中的某个方法的键是一致的。
  2. 一个exports的函数__webpack_exports__和一个类似require的函数__webpack_require__,这两个应该是模块化的关键。

带着以上疑问,我们去看manifest.js

manifest.js 代码阅读

(function(modules) {
  var parentJsonpFunction = window["webpackJsonp"];
  window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
  	var moduleId, chunkId, i = 0, resolves = [], result;
  	for(moduleId in moreModules) {
  		if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
  			modules[moduleId] = moreModules[moduleId];
  		}
  	}
  	if(executeModules) {
  		for(i=0; i < executeModules.length; i++) {
  			result = __webpack_require__(executeModules[i]);
  		}
  	}
  	return result;
  };

  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {
  	if(installedModules[moduleId]) {
  		return installedModules[moduleId].exports;
  	}
  	// Create a new module (and put it into the cache)
  	var module = installedModules[moduleId] = {
  		i: moduleId,
  		l: false,
  		exports: {}
  	};

  	// Execute the module function
  	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  	// Flag the module as loaded
  	module.l = true;

  	// Return the exports of the module
  	return module.exports;
  }
 })
([]);
  1. manifest.js内部是一个IIFE。这个函数会接受一个空数组作为参数,该数组被命名为 modules。果然在 window 上挂了一个名为webpackJsonp的函数。它接受的三个参数,分别名为chunkIds, moreModules, executeModules。对应了 main.js 中调用webpackJsonp时传入的三个参数。

    看下webpackJsonp:webpackJsonp 先是for遍历了一次moreModules,将 moreModules内的所有方法都存在modules, 也就是自执行函数执行时传入的数组。

    然后判断executeModules, 也就是第三个参数是否存在,如存在即执行 __webpack_require__ 方法。

  2. installedModules 是一个缓存的容器,如果缓存中有对应的 moduleId,那么直接返回它的 exports,不然就定义并赋值一个吧。

  3. __webpack_require__ 最后的返回值是module.exports

    webpack 就是将每一个 js 文件封装成一个函数,每个文件中的 require 方法对应的就是 __webpack_require____webpack_require__ 会根据传入的 moduleId 再去加载对应的代码。

以上面的例子总结,梳理一下打包后代码执行的流程:

  1. 首先执行manifest.js,在里面定义了一个webpackJsonp方法;
  2. 执行main.js:执行webpackJsonp函数,将所有的 moreModules, 也就是每一个依赖的文件存起来。
  3. 因为传入了第三个参数,所以eitI作为参数去执行__webpack_require__,__webpack_require__执行了eitI值定义的方法,这个方法中有以I6/Z作为参数去执行__webpack_require__(可以理解为加载了依赖文件test2.js),将依赖文件导出的值作为__webpack_require_函数返回值后供eitI内使用。
  4. 因为eitI值定义的方法没有导出值,__webpack_require__返回空对象,然后webpackJsonp方法执行完毕。

总结:

  • 首先manifest.js会定义一个webpackJsonp方法,待其他打包后的文件(也可称为 chunk)调用。

  • 当调用 chunk 时,会先将该 chunk 中所有的 moreModules, 也就是每一个依赖的文件也可称为 module存起来。

  • 之后通过executeModules判断这个文件是不是入口文件,决定是否执行第一次 __webpack_require__。而 __webpack_require__ 的作用,就是根据这个 module 所 require 的东西,不断递归调用 __webpack_require____webpack_require_函数返回值后供 require 使用。

  • 当然,模块是不会重复加载的,因为installedModules记录着 module 调用后的 exports 的值,只要命中缓存,就返回对应的值而不会再次调用 module。webpack 打包后的文件,就是通过一个个函数隔离 module 的作用域,以达到不互相污染的目的。

异步加载

这里是demo的代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title></title>
  </head>
  <body>
      <div id="app"></div>
      <div id="jq"></div>
      <button class="btn">load</button>
  </body>
</html>
// main.js
const p = document.querySelector('#jq');
const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
  //懒加载 test3.js
  require.ensure([], function() {
    const data = require('./test4');
    p.innerHTML = data;
  })
})
// test4.js
const data = 'success!';
module.exports = data;

从上面代码看出,test4是懒加载的,只有在点击了按钮才会加载这部分js代码,看一下打包后的运行效果:

异步

可以看出浏览器一开始只加载了main.jsmanifest.js,在点击按钮后,才加载了05d9040b8834b81d1aeb6.js。说明代码是被分割了的,只要当对应的条件触发时,浏览器才会去加载指定的资源。而无论之后我们点击多少次,05d9040b8834b81d1aeb6.js 文件都不会重复加载。所以这里先留下一个问题:如何做到不重复加载?

从 main.js 开始看代码

webpackJsonp([1],{

/***/ "Pmdr":
/***/ (function(module, exports, __webpack_require__) {

const p = document.querySelector('#jq');
const btn = document.querySelector('.btn');
btn.addEventListener('click', function() {
  //懒加载 test3.js
  __webpack_require__.e/* require.ensure */(0).then((function() {
    const data = __webpack_require__("O5aC");
    p.innerHTML = data;
  }).bind(null, __webpack_require__)).catch(__webpack_require__.oe)
})

/***/ })

},["Pmdr"]);

和上文的一般情况相比,问我们注意到__webpack_require__.e这个方法,传入一个数值之后返回一个promise。这方法当promise决议成功后执行切换文本的逻辑,失败则执行__webpack_require__.oe

综上,我们希望在manifest.js找到这三个问题的答案:

  • 如何做到不重复加载?
  • __webpack_require__.e方法的逻辑
  • __webpack_require__.oe方法的逻辑

manifest.js 代码阅读

部分截取__webpack_require__.e代码:

	var installedModules = {};
	// objects to store loaded and loading chunks
	var installedChunks = {
		2: 0
	};
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = function requireEnsure(chunkId) {
		var installedChunkData = installedChunks[chunkId]; // 1.尚未加载对应模块,undefined
    
    // 10.关于如何做到不重复加载?
    // 当再次请求同一文件时,由于对应的 module 已经被加载,因而直接返回一个成功的 promise
		if(installedChunkData === 0) {
			return new Promise(function(resolve) { resolve(); });
		}
		// a Promise means "currently loading".
    // 3.答案:假设网络很差的情况下,我们疯狂点击按钮,为避免浏览器发出若干个请求,通过条件判断都返回同一个 promise,当它决议后,所有挂载在它之上的 then 方法都能得到结果运行下去,相当于构造了一个队列,返回结果后按顺序执行对应方法
		if(installedChunkData) {
			return installedChunkData[2];
		}
		// setup Promise in chunk cache
    // 2. installedChunkData 与 installedChunks[chunkId] 被重新赋值为一个数组,存放着返回值 promise 的 resolve 与 reject
		var promise = new Promise(function(resolve, reject) {
			installedChunkData = installedChunks[chunkId] = [resolve, reject];
		});
    // 问题:为何将数组的第三项赋值为这个 promise呢?
		installedChunkData[2] = promise;
		// start chunk loading
    // 4. 创造一个 script 标签插入头部,加载指定的 js
		var head = document.getElementsByTagName('head')[0];
		var script = document.createElement('script');
		script.type = 'text/javascript';
		script.charset = 'utf-8';
		script.async = true;
		script.timeout = 120000;
		if (__webpack_require__.nc) {
			script.setAttribute("nonce", __webpack_require__.nc);
		}
		script.src = __webpack_require__.p + "" + chunkId + "" + {"0":"5d9040b8834b81d1aeb6","1":"5f81f82fbaa45b7a1fdd"}[chunkId] + ".js";
		var timeout = setTimeout(onScriptComplete, 120000);
    // 5.js 文件下载成功之后,先执行内容,再执行 onload 方法
		script.onerror = script.onload = onScriptComplete;
		function onScriptComplete() {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var chunk = installedChunks[chunkId];
      // 9. 下载失败,未被赋值0,执行reject
			if(chunk !== 0) {
				if(chunk) {
					chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
				}
				installedChunks[chunkId] = undefined;
			}
		};
		head.appendChild(script);
		return promise;
	};

总的来说,就是该方法中接受一个名为 chunkId 的参数,返回一个 promise,解释见注释,按数字顺序阅读。

// 6.执行5f81f82fbaa45b7a1fdd.js内容
webpackJsonp([0],{
/***/ "O5aC":
/***/ (function(module, exports) {
const data = 'success!';
module.exports = data;
/***/ })
});
// 截取部分 webpackJsonp 方法
var resolves = [];

for (; i < chunkIds.length; i++) {
  chunkId = chunkIds[i];
  if (installedChunks[chunkId]) { // 查看步骤2,installedChunks[0]就是[resolve, reject]
    resolves.push(installedChunks[chunkId][0]);
  }
  // 7. installedChunks[0]被重新赋值为0
  installedChunks[chunkId] = 0;
}

while (resolves.length) {
  // 8. 执行resolve
  resolves.shift()();
}

__webpack_require__.oe方法

__webpack_require__.oe = function(err) { console.error(err); throw err; };

流程:当异步请求文件发起时,先判断该 chunk 是否已被加载,是的话直接返回一个成功的 promise,让 then 执行的函数 require 对应的 module 即可。不然则构造一个 script 标签加载对应的 chunk,下载成功后挂载该 chunk 内所有的 module。下载失败则打印错误。

【参考】
CommonsChunkPlugin
知多一点 webpack 的 CommonsChunkPlugin
webpack增量打包
详解CommonsChunkPlugin的配置和用法
简单易懂的 webpack 打包后 JS 的运行过程
简单易懂的 webpack 打包后 JS 的运行过程(二)
webpack CommonsChunkPlugin 和 SplitChunksPlugin 思路
Webpack4之SplitChunksPlugin

@Hibop
Copy link
Contributor

Hibop commented Jul 13, 2020

👍 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants