webpack v4 中的断舍离 - 代码分离 SplitChunksPlugin(一)

2,418 阅读10分钟

  Time went by...  webpack已经从v3版本升级升级到了v4版本,刚刚我在官网看见升级到v4.16.5。最近webpack的升级速度很快,几乎两个星期就会有一个小版本的更新。软件更新速度快是一件好事情,会让一款软件更加稳定和便捷,但对于使用着来说无疑是增加了学习成本。

  案例地址 github.com/z354392349/…

  代码分离主要目的是防止代码重复,减少代码体积,达到加载速度快减少服务器压力和带宽等目的。webpack v3使用CommonsChunkPlugin插件配置代码分离但在webpack v4中被废除了,但新增了SplitChunksPlugin作为替代品,并且配置过程更为便捷高效。今天我就将代码分离的使用方法分享给大家。Remember me my name is 铅笔画不出的黑白

安装方法

npm install webpack-cli webpack -g   // 在全局环境 和 项目内安装  

层级目录

   假设一个学校有两个班级A班级和B班级,有三名老师分别是英语老师、数学老师、语文老师。(这个例子我想了很久,应该比较好理解吧,试想你上学的情景)老师给不同的班级上课,那么此时班级内的学生就相当于公用变量。为防止代码的重复我们需要将学生打包到一个文件内。

project
|- scr
    |- classes  
        |- class-a
        |- class-b
        |- class-c
    |- english
    |- math
    |- chinese

package.json

{
  "name": "school",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },/[p-]
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "import-local": "^1.0.0",
    "lodash": "^4.17.10",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  }
}

minimize

   minimize 属性不算是SplitChunksPlugin下的属性,但事两者之间是兄弟关系,都是Optimization(优化)下的属性, 是负责代码压缩的,在这里介绍minimize的原因是webpackV4版本中默认将压缩打开了,为了更直观的看到本文的效果,我们暂时将minimize设置为false,minimize的具体设置就不在这里展开了。 (看例1)

automaticNameDelimiter

   连接符:此选项允许您指定用于生成名称的分隔符。假设我们生成了一个公用文件名字叫version,class-a,和class-b都依赖他,并且我们设置的连接符是""那么,最终生成的就是 versionclass-a~class-b.js。(看例1)

minSize

   模块被分离前最小的模块大小以字节(b)为单位,例如class-a(~137b)、class-b (~142b) class-c (~142b) 都将会被分离被打包为一个新文件,如果minSize设置为421以上它们就不会被分离,如果设置为421以下(包括421)它们就会被分离合并为新的文件。(看例1)

   注意:这里可能会产生很小误差,我在测试本案例时中文会有5kb的误差,英文没有产生误差,另外编辑器大多是以UTF-8格式,webpack应该是GBk格式计算。如果你想精确的知道webpack计算出文件的大小可以在打包完成后看webpack的输出。

maxSize

   使用maxSize告诉webpack尝试将大于maxSize的块拆分成更小的部分。拆解后的文件最小值为minSize,或接近minSize的值。这样做的目的是避免单个文件过大,增加请求数量,达到减少下载时间的目的。但是这个值设置的时候要掌握好,如果设置的过小产生过多小文件会适得其反。另外HTTP1.0中最大的请求数量为6。在HTTP2.0中则没有限制。

  将案例1中 splitChunks.maxSize 设置为130你就会发现, class-a、class-b、class-c。会被分离为三个独立的文件。注意:这只是一个案例,为了演示效果,在实际开发中不能这么用。

例1

// class-a.js
export default [
    {student: "大红", age: 18},
    {student: "大米", age: 19},
    {student: "大爱", age: 17},
    {student: "大明", age: 20}
]
// class-b.js
export default [
    {student: "小红", age: 18},
    {student: "小米", age: 19},
    {student: "小爱", age: 17},
    {student: "小明", age: 20}
]
// class-c.js
export default [
    {student: "张三", age: 18},
    {student: "李四", age: 19},
    {student: "王五", age: 17},
    {student: "赵六", age: 20}
]
// webpack.config.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
    mode: "production",
    entry: {
        english: "./src/english.js",
        math: "./src/math.js",
        chinese: "./src/chinese.js",
    },
    output: {
        filename: "[name].bundle.js",
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ],
    optimization: {
        minimize: false,
        splitChunks: {
            chunks: "all",  //  async
            minSize: 400,
            automaticNameDelimiter: '~',
        }
    },
};

// english.js
import classA from './classes/class-a';
import classB from './classes/class-b';
import classC from './classes/class-c';

let engligh = {
    teacher: 'english', age: 47
};

classA.push(engligh);
classB.push(engligh);
classC.push(engligh);

// chinese.js
import classA from './classes/class-a';
import classB from './classes/class-b';
import classC from './classes/class-c';

let engligh = {
    teacher: 'english', age: 47
};

classA.push(engligh);
classB.push(engligh);
classC.push(engligh);
// math.js
import classA from './classes/class-a';
import classB from './classes/class-b';
import classC from './classes/class-c';


let math = {
    teacher: 'math', age: 47
};

classA.push(math);
classB.push(math);
classC.push(math);

   运行webpack 你会发现在 dist 下会有三个文件chinese.build.js、english.build.js、math.bundle.js 和公共文件chineseenglishmath.bundle.js。

chunks

   表示对哪些模快进行优化,可选字符串值有 allasyncinitial、和函数。

  • async 表示对动态(异步)导入的模块进行分离。(看例2.1)
  • initial 表示对初始化值进行分离优化。(看例2.2)
  • all 表示对所有模块进行分离优化,一般情况下都用all (看例2.3)
  • 函数 代表对指定的模块快分离优化,相当于定制优化。(看例2.4)

例2.1

更改下列文件,其他文件不变和例1一样。

// chinese.js
import classC from './classes/class-c';

let engligh = {
    teacher: 'english', age: 47
};


import(/* webpackChunkName: "async-class-a" */ './classes/class-a').then(classA =>{
    classA.push(engligh);
});
import(/* webpackChunkName: "async-class-b" */ './classes/class-b').then(classB =>{
    classB.push(engligh);
});
classC.push(engligh);

// english.js
import classB from './classes/class-b';
import classC from './classes/class-c';

let engligh = {
    teacher: 'english', age: 47
};

import( /* webpackChunkName: "async-class-a" */  './classes/class-a').then(classA =>{
    classA.push(engligh);
});

classB.push(engligh);
classC.push(engligh);

// math.js
import classB from './classes/class-b';
import classC from './classes/class-c';


let math = {
    teacher: 'math', age: 47
};
import(/* webpackChunkName: "async-class-a" */  './classes/class-a').then(classA =>{
    classA.push(engligh);
});
classB.push(math);
classC.push(math);

webpack.config.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
    mode: "production",
    entry: {
        english: "./src/english.js",
        math: "./src/math.js",
        chinese: "./src/chinese.js",
    },
    output: {
        filename: "[name].bundle.js",
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ],
    optimization: {
        minimize: false,
        splitChunks: {
            chunks: "async",  // async、 initial 、 all
            minSize: 0,
            automaticNameDelimiter: '~',
        }
    },
};

  运行webpakck dist下会有五个文件,chinese.bundle.js、english.bundle.js、math.bundle.js 以及动态加载的 async-class-a.bundle.js、async-class-b.bundle.js 而非动态导入的 class-c.js则不会被拆分为一个新文件。

 注意为什么会是这个名字,因为我导入时写了 /* webpackChunkName: "async-class-a" */ , /* webpackChunkName: "async-class-b" */ output.chunkFilename 此选项决定了非入口(non-entry) chunk 文件的名称。

  该项操作webpack只关心异步(动态)导入,即使 english.js、 math.js 、chinese.js 都引入了 class-c.js,也不会被打包为一个独立的文件。

例2.2

  将webpack.config.jsoptimization.splitChunks.chunks的值更改为 initial,这个属性的意思是告诉webpack,我希望将动态导入的文件和非动态导入的文件分别打包,如果一个模块被动态引入,也被非动态引入。那么这个模块将会被分离2次。被分别打包到不同的文件中。

   运行webpack,dist下会有7个文件,不要惊慌我将会解答每个文件生成的过程。 english.bundle.js、math.bundle.js、chinese.bundle.js、这三个是入口文件不做过多的解释。

   async-class-a.bundle.js 是因为chinese.js、english.js、math.js。 都异步引入了class-a。

   async-class-b.bundle.js 是因为chinese.js异步引入了class-b。然而english~math.bundle.js 文件的内容和async-class-b.bundle.js几乎完全一样,都包含了class-b模块。这是因为 english.js、math.js都非异步的引入了class-b。

  chineseenglishmath.bundle.js 是因为chinese.js、english.js、math.js。 都非异步引入了class-c.js。

例2.3

  将webpack.config.jsoptimization.splitChunks.chunks的值更改为 all ,此时的webpack就犹如上面的哈士奇一样,根本不会区分动态还是非动态,只是将需要的文件分离出一份。

  运行webpack 你会发现dist下有6个文件,其中三个是入口文件english.bundle.js 、 math.bundle.js、 chinese.bundle.js , 这没什么好说的。class-a.js、class-b.js、class-c.js 文件被分别打包为 async-class-a.bundle.js、async-class-b.bundle.js、chineseenglishmath.bundle.js。

  从打包结果来看,我猜测webpack的内部原理就是只要这个模块被动态加载了一次,就按动态加载处理。然后共享给其他非动态的模块。这里点可以从async-class-b.bundle.js的处理结果中看出。

例2.4

  将webpack.config.js的optimization.splitChunks.chunks的值更改为一个函数,这个函数返回Boolean值。如果值为ture 则分离优化这个模块。为false则不分离优化。
改操作在我测试过程中可选项为入口文件math、english、chinese (注意:这是入口文件名称,不是文件名)

// webpack.config.js  optimization.splitChunks.chunks

chunks: function (chunk) {
            return chunk.name !== ''
        },

  运行webpack你会发现,输出结果和 chunks: 'all' 一样。因为所有返回值都是ture,下面我们在测试入口文件为false的情况。

// webpack.config.js  optimization.splitChunks.chunks

chunks: function (chunk) {
            return chunk.name !== 'math'
        },

  运行webpack,结果还是和chunks: 'all' 几乎一样,但是仔细看chineseenglishmath.bundle.js文件变成了 chinese~english.bundle.js。打开math.bundle.js文件,在文件的下面你会发现class-b.js,class-c.js模块的全部内容,他们并没有被分离出去。但是class-a.js模块却被分离出去。仔细看文件最后的几行代码,class-a.js模块是动态加载的。

  由此我们可以得出结论,当一个模块是非动态加载的那么他将不会被分离出去,如果这个模块是动态加载的,她就会被分离出去,并且还是动态引入关系。

maxInitialRequests

  入口点处的最大并行请求数,换句话来讲就是每一个入口文件打包完成后最多能有多少个模块组成。为了更好的理解本案例(例3),我将会更改下列代码。

例3

// chinese.js 
import classC from './classes/class-c';
import classB from './classes/class-b';

let chinese = {
    teacher: 'chinese', age: 47
};


classB.push(chinese);
classC.push(chinese);
// english.js
import classA from './classes/class-a';
import classC from './classes/class-c';

let engligh = {
    teacher: 'english', age: 47
};


classA.push(engligh);
classC.push(engligh);
// math.js
import classA from './classes/class-a';
import classB from './classes/class-b';


let math = {
    teacher: 'math', age: 47
};
classA.push(math);
classB.push(math);
// webpack.config.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
    mode: "production",
    entry: {
        english: "./src/english.js",
        math: "./src/math.js",
        chinese: "./src/chinese.js",
    },
    output: {
        filename: "[name].bundle.js",
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new CleanWebpackPlugin(['dist'])
    ],
    optimization: {
        minimize: false,
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 1,
            minSize: 0,
            automaticNameDelimiter: '~',
        }
    },
};

例3.1

  将maxInitialRequests设置为了1,运行webpack在dist文件夹内只有三个入口文件,也就是说打包完成后每个入口文件最多之只能由1个文件组成。所以没有分离出来任何独立的模块。

例3.2

   将maxInitialRequests设置为了2,运行webpack相比于例3.1在dist下会多出一个english~math.bundle.js文件。文件内是class-a.js模块,但是class-b.js和class-c.js模块也被引入了两次。为什么偏偏class-a.js被分离出来了呢。我带着这个疑问反复的测试。最终结果是可能和模块名称有关。如果将class-a.js更换为class-d.js那么被分离的就是class-b.js文件。

  一个入口打包完成后最多之能有2个文件组成。english.bundle.js 和 math.bundle.js 都引入english~math.bundle.js 已经达到上限了。chinese.bundle.js 也没有必要自找麻烦把模块分离出来再引入进去,所以 chinese.bundle.js就没有任何模块分离出来。

例3.3

  将maxInitialRequests设置为了3,这就相当于每个入口文件打包完成后最多可由3个模块组成, 运行webpack在dist文件夹下面有6个文件,class-a.js、class-b.js、class-c.js 全部都被分离出来了。但是当你将maxInitialRequests设置为了3以上时在运行webpack,答案和3是一样的。因为webpack 没有必要再分离出更多的模块。

maxAsyncRequests

  按需加载时并行请求的最大数目,但是很抱歉这个属性我理解的可能不是很到位,无法写出案例给各位,如果哪位同学可以写出这个属性的demo请联系我。我会很感谢。engineering.wingify.com/posts/demys… 这个demo 是我在Google上看到的,是关于这个属性的使用方法。这个属性我还会继续研究,如果有研究成果我写出一个demo给大家。

总结

   maxSize的优先级高于maxInitialRequest / maxAsyncRequests。 实际优先级为maxInitialRequest / maxAsyncRequests <maxSize <minSize。

后语

   写这篇文章用了我几乎三个星期的业余时间。如果有哪里写的不对的欢迎指教探讨,也希望大家留言探讨。学技术是一个寻寻渐进的过程,希望各位同学不要松懈,不要被技术的浪潮所淘汰。在下一篇文章SplitChunksPlugin(二中我会重点分析)的分组。

联系方式

   QQ/微信 48988840。