zoukankan      html  css  js  c++  java
  • Webpack 常见插件原理分析【转】

    转自:https://www.jianshu.com/p/108d07de0e01

    本章内容主要讲解一下 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("
    ")) return `/*! ${str} */`;
        return `/*!
     * ${str.split("
    ").join("
     * ")}
     */`;
    }
    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, "
    ", 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.
    For 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 插件的分析也可以

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

    就已经足够了



    作者:Dabao123
    链接:https://www.jianshu.com/p/108d07de0e01
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    Maven打包时去掉项目版本号
    maven编译的时候排除junit测试类
    Redis与Zookeeper实现分布式锁的区别
    分布式锁(基于redis和zookeeper)详解
    解读阿里巴巴集团的“大中台、小前台”组织战略
    java高并发系列
    JAVA之Unsafe学习笔记
    测试用例之正交排列法
    测试用例之因果图/判定表
    测试用例之边界值法
  • 原文地址:https://www.cnblogs.com/hz-blog/p/10062158.html
Copyright © 2011-2022 走看看