Webpack 常見插件原理分析

本章內(nèi)容主要講解一下 Webpack 幾個稍微簡單的插件原理,通過本章節(jié)的學(xué)習(xí),對前面的知識應(yīng)該會有一個更加深入的理解。
prepack-webpack-plugin 的說明今年 Facebook 開源了一個 prepack,當(dāng)時就很好奇,它到底和 Webpack 之間的關(guān)系是什么?于是各種搜索,最后還是去官網(wǎng)上看了下各種例子。例子都很好理解,但是對于其和 Webpack 的關(guān)系還是有點(diǎn)迷糊。最后找到了一個好用的插件,即 prepack-webpack-plugin,這才恍然大悟~

解析 prepack-webpack-plugin 源碼

下面直接給出這個插件的 apply 源碼,因?yàn)?Webpack 的 plugin 的所有邏輯都是在 apply 方法中處理的。內(nèi)容如下:

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 產(chǎn)生的所有的輸出文件,記住是輸出文件
          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();
            //獲取文件的代碼內(nèi)容
            const prepackedCode = prepack(code, {
              ...configuration.prepack,
              filename: file
            });
            //所以,這里是在 Webpack 打包后對 ES5 代碼的處理
            compilation.assets[file] = new RawSource(prepackedCode.code);
          }
        }
        callback();
      });
    });
  }
}

首先對于 Webpack 各種鉤子函數(shù)時機(jī)不了解的可以 點(diǎn)擊這里。如果對于 Webpack 中各個對象的屬性不了解的可以點(diǎn)擊這里。接下來對上面的代碼進(jìn)行簡單的剖析:
(1)首先看 for 循環(huán)的前面那幾句:

const files = chunk.files;
  //chunk.files 獲取該 chunk 產(chǎn)生的所有的輸出文件,記住是輸出文件
  for (const file of files) {
   //這里只會對該 chunk 包含的文件中符合 test 規(guī)則的文件進(jìn)行后續(xù)處理
    const matchObjectConfiguration = {
      test: configuration.test
    };
    if (!ModuleFilenameHelpers.matchObject(matchObjectConfiguration, file)) {
      // eslint-disable-next-line no-continue
      continue;
    }
}

這里給出 ModuleFilenameHelpers.matchObject 的代碼:

/將字符串轉(zhuǎn)化為 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 規(guī)則返回 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;
};

這幾句代碼是一目了然的,如果這個產(chǎn)生的文件名稱符合 test 規(guī)則返回 true,否則為 false。
(2)繼續(xù)看后面對于符合規(guī)則的文件的處理

 //如果滿足規(guī)則繼續(xù)處理~
 const asset = compilation.assets[file];
//獲取編譯產(chǎn)生的資源
const code = asset.source();
//獲取文件的代碼內(nèi)容
const prepackedCode = prepack(code, {
  ...configuration.prepack,
  filename: file
});
//所以,這里是在 Webpack 打包后對 ES5 代碼的處理
compilation.assets[file] = new RawSource(prepackedCode.code);

其中 asset.source 表示的是模塊的內(nèi)容,可以
點(diǎn)擊這里查看。假如模塊是一個 html,內(nèi)容如下:

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

最后打包的結(jié)果為:

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

這也是為什么會有下面的代碼:

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

前面兩句代碼都分析過了,繼續(xù)看下面的內(nèi)容:

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

此時才真正的對 Webpack 打包后的代碼進(jìn)行處理,prepack的nodejs 用法可以 查看這里。最后一句代碼其實(shí)就是操作我們的輸出資源,在輸出資源中添加一個文件,文件的內(nèi)容就是 prepack 打包后的代碼。其中 webpack-source 的內(nèi)容可以 點(diǎn)擊這里。按照官方的說明,該對象可以獲取源代碼、hash、內(nèi)容大小、sourceMap 等所有信息。我們給出對 RowSourceMap 的說明:

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

很顯然,就是顯示源代碼而不包含 sourceMap。

prepack-webpack-plugin 總結(jié)

所以,prepack 作用于 Webpack 的時機(jī)在于:將源代碼轉(zhuǎn)化為 ES5 以后。從上面的 html 的編譯結(jié)果就可以知道了,至于它到底做了什么,以及如何做的,還請查看 官網(wǎng)。

BannerPlugin 插件分析

我們現(xiàn)在講述一下 BannerPlugin 內(nèi)部的原理。它的主要用法如下:

{
  banner: string, 
    // the banner as string, it will be wrapped in a comment
  raw: boolean, 
    //如果配置了 raw,那么 banner 會被包裹到注釋當(dāng)中
  entryOnly: boolean, 
    //如果設(shè)置為 true,那么 banner 僅僅會被添加到入口文件產(chǎn)生的 chunk 中
  test: string | RegExp | Array,
  include: string | RegExp | Array,
  exclude: string | RegExp | Array,
}

我們看看它的內(nèi)部代碼:

"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 || {};
        //配置參數(shù)
        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) => {
                    //入口文件都是默認(rèn)首次加載的,即 isInitial為true 和 require.ensure 按需加載是完全不一樣的
                    if(options.entryOnly && !chunk.isInitial()) return;
                    chunk.files
                        .filter(ModuleFilenameHelpers.matchObject.bind(undefined, options))
                        //只要滿足 test 正則表達(dá)式的文件才會被處理
                        .forEach((file) =>
                            compilation.assets[file] = new ConcatSource(
                                banner, "\n", compilation.assets[file]
                                //在原來的輸出文件頭部添加我們的 banner 信息
                            )
                        );
                });
                callback();
            });
        });
    }
}
module.exports = BannerPlugin;

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

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

此時相當(dāng)于以以下方式使用 DefinePlugin 插件:

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

當(dāng)然,該插件也可以傳入一個對象:

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');
}

如果執(zhí)行 NODE_ENV=production webpack 命令,那么會發(fā)現(xiàn)輸出文件為如下內(nèi)容:

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

上面講述了這個插件如何使用,來看看它的內(nèi)部原理是什么?

"use strict";
const DefinePlugin = require("./DefinePlugin");
//1.EnvironmentPlugin 內(nèi)部直接調(diào)用 DefinePlugin
class EnvironmentPlugin {
    constructor(keys) {
        this.keys = Array.isArray(keys) ? keys : Object.keys(arguments);
    }
    apply(compiler) {
        //2.這里直接使用 compiler.apply 方法來執(zhí)行 DefinePlugin 插件
        compiler.apply(new DefinePlugin(this.keys.reduce((definitions, key) => {
            const value = process.env[key];
            //獲取 process.env 中的參數(shù)
            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 插件分析

這個插件的作用在于,如果產(chǎn)生的某個 Chunk 的大小小于閾值,那么直接和其他的 Chunk 合并,其主要使用方法如下:

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

來看下它的內(nèi)部原理是如何實(shí)現(xiàn)的:

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)
                    //入口文件乘以的權(quán)重,所以如果含有入口文件,那么更加不容易小于 minChunkSize,所以入口文件過小不容易被集成到別的 chunk 中
                };
                combinations = combinations.filter((pair) => {
                    return pair[0].size(equalOptions) < minChunkSize || pair[1].size(equalOptions) < minChunkSize;
                });
        //對數(shù)組中元素進(jìn)行刪選,至少有一個 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]等這樣的數(shù)組元素,前面加上兩個元素:集成后總體積的變化量;集成后的體積
                });
                //此時 combinations 的元素至少有一個的大小是小于 minChunkSize 的
                combinations = combinations.filter((pair) => {
                    return pair[1] !== false;
                });
                if(combinations.length === 0) return;
                //如果沒有需要優(yōu)化的,直接返回
                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 是組合形式,把自己和前面比自己小的元素組合成為一個元素。之所以是選擇比自己的小的情況是為了減少重復(fù)的個數(shù),如 [0,2] 和 [2,0] 必須只有一個。

本章小結(jié)

在本章節(jié)中主要講了幾個稍微簡單一點(diǎn)的 Webpack 的 Plugin,如果對于 Plugin 的原理比較感興趣,在前面介紹的那些基礎(chǔ)知識已經(jīng)夠用了。至于很多復(fù)雜的 Plugin 就需要在平時開發(fā)的時候多關(guān)注和學(xué)習(xí)了。更多 Webpack 插件的分析也可以

點(diǎn)擊這里,而至于插件本身的用法,官網(wǎng)

就已經(jīng)足夠了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • GitChat技術(shù)雜談 前言 本文較長,為了節(jié)省你的閱讀時間,在文前列寫作思路如下: 什么是 webpack,它要...
    蕭玄辭閱讀 12,892評論 7 110
  • 寫在開頭 先說說為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,445評論 4 31
  • 無意中看到zhangwnag大佬分享的webpack教程感覺受益匪淺,特此分享以備自己日后查看,也希望更多的人看到...
    小小字符閱讀 8,369評論 7 35
  • 今天早上,被秋風(fēng)凍醒。起床讀書,腦子里突然閃現(xiàn)出來這句話——做一個好(hǎo)玩兒的人,所以敲動鍵盤與大家交流分享...
    像話讀書爻閱讀 519評論 2 3
  • ——龍·茶館 清平宋詞好伴茶, 瑰麗唐詩入酒香; 吟詩侃侃更醉茶, 聞詞不語比酒香。 (2016.9.20)
    龍茶館閱讀 366評論 0 1

友情鏈接更多精彩內(nèi)容