Webpack 常见插件原理分析

本章内容主要讲解一下 Webpack 几个稍微简单的插件原理,通过本章节的学习,对前面的知识应该会有一个更加深入的理解。
prepack-webpack-plugin 的说明今年 Facebook 开源了一个 prepack,当时就很好奇,它到底和 Webpack 之间的关系是什么?于是各种搜索,最后还是去官网上看了下各种例子。例子都很好理解,但是对于其和 Webpack 的关系还是有点迷糊。最后找到了一个好用的插件,即 prepack-webpack-plugin,这才恍然大悟~

解析 prepack-webpack-plugin 源码

下面直接给出这个插件的 apply 源码,因为 Webpack 的 plugin 的所有逻辑都是在 apply 方法中处理的。内容如下:

import ModuleFilenameHelpers from 'webpack/lib/ModuleFilenameHelpers';
import {
  RawSource
} from 'webpack-sources';
import {
  prepack
} from 'prepack';
import type {
  PluginConfigurationType,
  UserPluginConfigurationType
} from './types';
const defaultConfiguration = {
  prepack: {},
  test: /\.js($|\?)/i
};
export default class PrepackPlugin {
  configuration: PluginConfigurationType;
  constructor (userConfiguration?: UserPluginConfigurationType) {
    this.configuration = {
      ...defaultConfiguration,
      ...userConfiguration
    };
  }
  apply (compiler: Object) {
    const configuration = this.configuration;
    compiler.plugin('compilation', (compilation) => {
      compilation.plugin('optimize-chunk-assets', (chunks, callback) => {
        for (const chunk of chunks) {
          const files = chunk.files;
          //chunk.files 获取该 chunk 产生的所有的输出文件,记住是输出文件
          for (const file of files) {
            const matchObjectConfiguration = {
              test: configuration.test
            };
            if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) {
              // eslint-disable-next-line no-continue
              continue;
            }
            const asset = compilation.assets[file];
            //获取文件本身
            const code = asset.source();
            //获取文件的代码内容
            const prepackedCode = prepack(code, {
              ...configuration.prepack,
              filename: file
            });
            //所以,这里是在 Webpack 打包后对 ES5 代码的处理
            compilation.assets[file] = new RawSource(prepackedCode.code);
          }
        }
        callback();
      });
    });
  }
}

首先对于 Webpack 各种钩子函数时机不了解的可以 点击这里。如果对于 Webpack 中各个对象的属性不了解的可以点击这里。接下来对上面的代码进行简单的剖析:
(1)首先看 for 循环的前面那几句:

const files = chunk.files;
  //chunk.files 获取该 chunk 产生的所有的输出文件,记住是输出文件
  for (const file of files) {
   //这里只会对该 chunk 包含的文件中符合 test 规则的文件进行后续处理
    const matchObjectConfiguration = {
      test: configuration.test
    };
    if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) {
      // eslint-disable-next-line no-continue
      continue;
    }
}

这里给出 ModuleFilenameHelpers.matchObject 的代码:

/将字符串转化为 regex
function asRegExp(test) {
    if(typeof test === "string") test = new RegExp("^" + test.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"));
    return test;
}
ModuleFilenameHelpers.matchPart = function matchPart(str, test) {
    if(!test) return true;
    test = asRegExp(test);
    if(Array.isArray(test)) {
        return test.map(asRegExp).filter(function(regExp) {
            return regExp.test(str);
        }).length > 0;
    } else {
        return test.test(str);
    }
};
ModuleFilenameHelpers.matchObject = function matchObject(obj, str) {
    if(obj.test)
        if(!ModuleFilenameHelpers.matchPart(str, obj.test)) 
        return false;
    //获取 test,如果这个文件名称符合 test 规则返回 true,否则为 false
    if(obj.include)
        if(!ModuleFilenameHelpers.matchPart(str, obj.include)) return false;
    if(obj.exclude)
        if(ModuleFilenameHelpers.matchPart(str, obj.exclude)) return false;
     return true;
};

这几句代码是一目了然的,如果这个产生的文件名称符合 test 规则返回 true,否则为 false。
(2)继续看后面对于符合规则的文件的处理

 //如果满足规则继续处理~
 const asset = compilation.assets[file];
//获取编译产生的资源
const code = asset.source();
//获取文件的代码内容
const prepackedCode = prepack(code, {
  ...configuration.prepack,
  filename: file
});
//所以,这里是在 Webpack 打包后对 ES5 代码的处理
compilation.assets[file] = new RawSource(prepackedCode.code);

其中 asset.source 表示的是模块的内容,可以
点击这里查看。假如模块是一个 html,内容如下:

<header class="header">{{text}}</header>

最后打包的结果为:

module.exports = "<header class=\\"header\\">{{text}}</header>";' }

这也是为什么会有下面的代码:

compilation.assets[basename] = {
      source: function () {
        return results.source;
      },
      //source 是文件的内容,通过 fs.readFileAsync 完成
      size: function () {
        return results.size.size;
        //size 通过 fs.statAsync(filename) 完成
      }
    };
    return basename;
  });

前面两句代码都分析过了,继续看下面的内容:

const prepackedCode = prepack(code, {
  ...configuration.prepack,
  filename: file
});
//所以,这里是在 Webpack 打包后对 ES5 代码的处理
compilation.assets[file] = new RawSource(prepackedCode.code);

此时才真正的对 Webpack 打包后的代码进行处理,prepack的nodejs 用法可以 查看这里。最后一句代码其实就是操作我们的输出资源,在输出资源中添加一个文件,文件的内容就是 prepack 打包后的代码。其中 webpack-source 的内容可以 点击这里。按照官方的说明,该对象可以获取源代码、hash、内容大小、sourceMap 等所有信息。我们给出对 RowSourceMap 的说明:

RawSource
Represents source code without SourceMap.
new RawSource(sourceCode: String)

很显然,就是显示源代码而不包含 sourceMap。

prepack-webpack-plugin 总结

所以,prepack 作用于 Webpack 的时机在于:将源代码转化为 ES5 以后。从上面的 html 的编译结果就可以知道了,至于它到底做了什么,以及如何做的,还请查看 官网

BannerPlugin 插件分析

我们现在讲述一下 BannerPlugin 内部的原理。它的主要用法如下:

{
  banner: string, 
    // the banner as string, it will be wrapped in a comment
  raw: boolean, 
    //如果配置了 raw,那么 banner 会被包裹到注释当中
  entryOnly: boolean, 
    //如果设置为 true,那么 banner 仅仅会被添加到入口文件产生的 chunk 中
  test: string | RegExp | Array,
  include: string | RegExp | Array,
  exclude: string | RegExp | Array,
}

我们看看它的内部代码:

"use strict";
const ConcatSource = require("webpack-sources").ConcatSource;
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
//'This file is created by liangklfangl' =>/*! This file is created by liangklfangl */
function wrapComment(str) {
    if(!str.includes("\n")) return `/*! ${str} */`;
    return `/*!\n * ${str.split("\n").join("\n * ")}\n */`;
}
class BannerPlugin {
    constructor(options) {
        if(arguments.length > 1)
            throw new Error("BannerPlugin only takes one argument (pass an options object)");
        if(typeof options === "string")
            options = {
                banner: options
            };
        this.options = options || {};
        //配置参数
        this.banner = this.options.raw ? options.banner : wrapComment(options.banner);
    }
    apply(compiler) {
        let options = this.options;
        let banner = this.banner;
        compiler.plugin("compilation", (compilation) => {
            compilation.plugin("optimize-chunk-assets", (chunks, callback) => {
                chunks.forEach((chunk) => {
                    //入口文件都是默认首次加载的,即 isInitial为true 和 require.ensure 按需加载是完全不一样的
                    if(options.entryOnly && !chunk.isInitial()) return;
                    chunk.files
                        .filter(ModuleFilenameHelpers.matchObject.bind(undefined, options))
                        //只要满足 test 正则表达式的文件才会被处理
                        .forEach((file) =>
                            compilation.assets[file] = new ConcatSource(
                                banner, "\n", compilation.assets[file]
                                //在原来的输出文件头部添加我们的 banner 信息
                            )
                        );
                });
                callback();
            });
        });
    }
}
module.exports = BannerPlugin;

EnvironmentPlugin 插件分析
该插件的使用方法如下:

new webpack.EnvironmentPlugin(['NODE_ENV', 'DEBUG'])

此时相当于以以下方式使用 DefinePlugin 插件:

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

当然,该插件也可以传入一个对象:

new webpack.EnvironmentPlugin({
  NODE_ENV: 'development', 
    // use 'development' unless process.env.NODE_ENV is defined
  DEBUG: false
})

假如有如下的 entry 文件:

if (process.env.NODE_ENV === 'production') {
  console.log('Welcome to production');
}
if (process.env.DEBUG) {
  console.log('Debugging output');
}

如果执行 NODE_ENV=production webpack 命令,那么会发现输出文件为如下内容:

if ('production' === 'production') { // <-- 'production' from NODE_ENV is taken
  console.log('Welcome to production');
}
if (false) { // <-- default value is taken
  console.log('Debugging output');
}

上面讲述了这个插件如何使用,来看看它的内部原理是什么?

"use strict";
const DefinePlugin = require("./DefinePlugin");
//1.EnvironmentPlugin 内部直接调用 DefinePlugin
class EnvironmentPlugin {
    constructor(keys) {
        this.keys = Array.isArray(keys) ? keys : Object.keys(arguments);
    }
    apply(compiler) {
        //2.这里直接使用 compiler.apply 方法来执行 DefinePlugin 插件
        compiler.apply(new DefinePlugin(this.keys.reduce((definitions, key) => {
            const value = process.env[key];
            //获取 process.env 中的参数
            if(value === undefined) {
                compiler.plugin("this-compilation", (compilation) => {
                    const error = new Error(key + " environment variable is undefined.");
                    error.name = "EnvVariableNotDefinedError";
                    //3.可以往 compilation.warning 里面填充编译 warning 信息
                    compilation.warnings.push(error);
                });
            }
            definitions["process.env." + key] = value ? JSON.stringify(value) : "undefined";
            //4.将所有的 key 都封装到 process.env 上面了并返回(注意这里是向 process.env 上赋值)
            return definitions;
        }, {})));
    }
}
module.exports = EnvironmentPlugin;
MinChunkSizePlugin 插件分析

这个插件的作用在于,如果产生的某个 Chunk 的大小小于阈值,那么直接和其他的 Chunk 合并,其主要使用方法如下:

new webpack.optimize.MinChunkSizePlugin({
  minChunkSize: 10000 
})

来看下它的内部原理是如何实现的:

class MinChunkSizePlugin {
    constructor(options) {
        if(typeof options !== "object" || Array.isArray(options)) {
            throw new Error("Argument should be an options object.\nFor more info on options, see https://webpack.github.io/docs/list-of-plugins.html");
        }
        this.options = options;
    }
    apply(compiler) {
        const options = this.options;
        const minChunkSize = options.minChunkSize;
        compiler.plugin("compilation", (compilation) => {
            compilation.plugin("optimize-chunks-advanced", (chunks) => {
                let combinations = [];
                chunks.forEach((a, idx) => {
                    for(let i = 0; i < idx; i++) {
                        const b = chunks[i];
                        combinations.push([b, a]);
                    }
                });
                const equalOptions = {
                    chunkOverhead: 1,
                    // an additional overhead for each chunk in bytes (default 10000, to reflect request delay)
                    entryChunkMultiplicator: 1
                    //a multiplicator for entry chunks (default 10, entry chunks are merged 10 times less likely)
                    //入口文件乘以的权重,所以如果含有入口文件,那么更加不容易小于 minChunkSize,所以入口文件过小不容易被集成到别的 chunk 中
                };
                combinations = combinations.filter((pair) => {
                    return pair[0].size(equalOptions) < minChunkSize || pair[1].size(equalOptions) < minChunkSize;
                });
        //对数组中元素进行删选,至少有一个 chunk 的值是小于 minChunkSize 的
                combinations.forEach((pair) => {
                    const a = pair[0].size(options);
                    const b = pair[1].size(options);
                    const ab = pair[0].integratedSize(pair[1], options);
                    //得到第一个 chunk 集成了第二个 chunk 后的文件大小
                    pair.unshift(a + b - ab, ab);
                    //这里的 pair 是如[0,1]、[0,2]等这样的数组元素,前面加上两个元素:集成后总体积的变化量;集成后的体积
                });
                //此时 combinations 的元素至少有一个的大小是小于 minChunkSize 的
                combinations = combinations.filter((pair) => {
                    return pair[1] !== false;
                });
                if(combinations.length === 0) return;
                //如果没有需要优化的,直接返回
                combinations.sort((a, b) => {
                    const diff = b[0] - a[0];
                    if(diff !== 0) return diff;
                    return a[1] - b[1];
                });
                //按照集成后变化的体积来比较,从大到小排序
                const pair = combinations[0];
                //得到第一个元素
                pair[2].integrate(pair[3], "min-size");
                //pair[2] 是 chunk,pair[3] 也是 chunk
                chunks.splice(chunks.indexOf(pair[3]), 1);
                //从 chunks 集合中删除集成后的 chunk
                return true;
            });
        });
    }
}
module.exports = MinChunkSizePlugin;

下面给出主要的代码:

var combinations = [];
var chunks=[0,1,2,3]
chunks.forEach((a, idx) => {
    for(let i = 0; i < idx; i++) {
        const b = chunks[i];
        combinations.push([b, a]);
    }
});

变量 combinations 是组合形式,把自己和前面比自己小的元素组合成为一个元素。之所以是选择比自己的小的情况是为了减少重复的个数,如 [0,2] 和 [2,0] 必须只有一个。

本章小结

在本章节中主要讲了几个稍微简单一点的 Webpack 的 Plugin,如果对于 Plugin 的原理比较感兴趣,在前面介绍的那些基础知识已经够用了。至于很多复杂的 Plugin 就需要在平时开发的时候多关注和学习了。更多 Webpack 插件的分析也可以

点击这里,而至于插件本身的用法,官网

就已经足够了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,619评论 7 110
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,236评论 4 31
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,089评论 7 35
  • 今天早上,被秋风冻醒。起床读书,脑子里突然闪现出来这句话——做一个好(hǎo)玩儿的人,所以敲动键盘与大家交流分享...
    像话读书爻阅读 438评论 2 3
  • ——龍·茶館 清平宋词好伴茶, 瑰丽唐诗入酒香; 吟诗侃侃更醉茶, 闻词不语比酒香。 (2016.9.20)
    龍茶館阅读 330评论 0 1