webpack原理分析

81017 ワード

webpack原理分析
この文章はウェブパックの原理を勉強しました.これを読んだら分かります.後に書いたのは、この文章の内容によって、自分で手動で一回実現しました.ここでは原作者に感謝し、削除します.文章は絶対オリジナルではありません.ここで声明します.
webpackとは何ですか
本質的に、webpackは現代JavaScriptアプリケーションの静的モジュールパッケージングです.webpack処理アプリケーションは、アプリケーションに必要なモジュールを含み、これらのモジュールを一つまたは複数のbundleにパッケージ化する依存関係図を再帰的に構築します.
webpackは生産ラインのように一連の処理プロセスを経て、ソースファイルを出力結果に変換することができます.この生産ラインの各処理フローの知者は単一で、複数の流れの間に依存関係があります.現在の処理が完了したら、次のプロセスに処理してくれます.プラグインは生産ラインに挿入された機能で、特定の時間に生産ラインのリソースを処理します.
WebpackはTapableを通じてこの複雑な生産ラインを組織します.webpackは運営中にイベントを放送します.プラグインは関心のあるイベントをモニターするだけで、この生産ラインに参加して、生産ラインの操作を変えられます.webpackのイベントフロー機構は、プラグインの秩序化を保証する.全体の拡張性をよくしました.—深入浅出webpack呉浩麟
Tapableは小型のライブラリであり、Javascriptモジュールにプラグインを追加して適用することができます.それは他のモジュールに継承または混入することができます.NodeJSのEventEmitter類と同様に、カスタムイベントのトリガと処理に専念する.この他に、Tapableは、イベントの「トリガ」または「プロデューサ」に対して、コールバック関数のパラメータを介してアクセスすることも可能である.
このコードの中で、以下のようなものが一番分かりにくいです.すべてのimportのコードは一つのrequireでロードされます.入り口をrequireすると、元のrequire方法は在自で匿名関数の作用領域を実行し、書き換えられます.これにより、相対パスリソースへの参照が実現される.
Webpackの核心概念
Entry
入口の始点(enter point)は、webpackがどのモジュールを使用するべきかを示し、その内部依存図の構築の開始とする.木のルートに相当します.
入り口の起点に入ると、webpackはコードをAST文法ツリーに解析します.そして、webpackは文法ツリーによってどのモジュールとライブラリが入り口の起点(直接と間接)依存であるかを探します.
各依存項はすぐに処理されて、最後にbundleのファイルに出力されます.
Output
Output属性は、webpackが作成したbundlesをどこで出力しますか?また、これらのファイルの名前はどうなりますか?特別な指定をしないと、例えばexternalなど、コードは指定された出力ファイルにコンパイルされます.
Module
モジュールは、webpack内の全てのモジュールで、一つのモジュールは一つのファイルに対応しています.例えば、jsはモジュールで、scssも一つのモジュールです.webpackは、設定されたEntryからすべての依存モジュールを再帰的に探します.
Chunk
これはwebpack特定の用語であり、内部でbuildingプロセスを管理するために使用される.bundleはchunkからなり、いくつかのタイプ(例えば、入口chunkと子供chunk)があります.通常chunkは出力されたbundleに直接対応しますが、一対一の関係が生じないように構成されています.
簡単に言えば、書いたコードはmoduleで、コンパイル中のコードはchunkといい、出力のコードはbundleといいます.
Loader
loaderはwebpackにJavaScriptではないファイルを処理させます.
loaderはすべての種類のファイルをwebpack処理の有効なモジュールに変換して、webpackの梱包能力を利用して彼らを処理します.
本質的には、webpack loaderはすべての種類のファイルを、アプリケーションの依存図(および最終的なbundle)に直接参照できるモジュールに変換する.
Plugin
Loaderはいくつかのタイプのモジュールを変換するために使用され、プラグインはより広範なタスクを実行するために使用されることができる.
プラグインの範囲には、パッケージの最適化と圧縮から、環境内の変数を再定義するまでがあります.プラグインインターフェースは非常に強力で、様々なタスクを処理するために使用できます.
Webpack構築プロセス
webpackの運転フローはシリアルのプロセスで、起動から終了までは次のフローが一回実行されます.
  • 初期化パラメータ:設定ファイルとshell漁具からパラメータを読み出し、統合し、最終的なwebpack構成を導出する.
  • からコンパイルが開始されます.前のステップで得られたパラメータ初期化Compilerオブジェクトに従って、設定されたすべてのプラグインをロードし、オブジェクトのrun方法を実行してコンパイルを開始します.
  • は、入口を決定する.プロファイルの中のentryからすべてのエントリファイルを探し出す.
  • コンパイルモジュール:入り口ファイルからトリガして、すべての構成のLoaderを呼び出してモジュールを翻訳し、モジュールに依存するモジュールを見つけて、このステップを再帰する.
  • はモジュールコンパイルを完了しました.第4ステップを経て、すべてのモジュールをloaderで翻訳し終わった後、各モジュールが翻訳された最終コンテンツとその依存関係を得ます.
  • 出力リソース:入口とモジュール間の依存関係に基づいて、複数のモジュールを含むChunkに組み立てられ、各Chunkを個別のファイルに変換し、出力リストに追加することで、出力内容を変更することができる最後の機会となる.
  • の出力が完了しました.出力内容が確定された後、プロファイルから出力のパスとファイル名を決定し、ファイルをファイルシステムに書き込みます.
  • 以上の過程で、Webpackは特定の時点で特定のイベントをブロードキャストし、プラグインは関心のあるイベントを傍受した後、応答の論理を実行し、プラグインはWebpackが提供するAPIを呼び出してWebpackの実行結果を変更することができる.
    実践して理解を深めて、webpackDemoを書きます.
    1.complerクラスを定義する
    このクラスはwebpackのコアフレームに相当します.
    class Compiler {
      constructor(options) {
        // webpack   
        const { entry, output } = options
        //   
        this.entry = entry
        //   
        this.output = output
        //   
        this.modules = []
      }
      //     
      run() {}
      //    require  ,  bundle
      generate() {}
    }
    
    2.入り口ファイルを解析して、AST文法ツリーを取得する
    webpackプロファイルを作成します.
    // webpack.config.js
    
    const path = require('path')
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'main.js'
      }
    }
    
    @babel/parserを使ってコード内部の文法を分析してくれて、AST抽象文法ツリーに戻ります.
    const fs = require('fs')
    const parser = require('@babel/parser')
    const options = require('./webpack.config')
    
    const Parser = {
      getAst: path => {
        //       
        const content = fs.readFileSync(path, 'utf-8')
        //        AST     
        return parser.parse(content, {
          sourceType: 'module'
        })
      }
    }
    
    class Compiler {
      constructor(options) {
        // webpack   
        const { entry, output } = options
        //   
        this.entry = entry
        //   
        this.output = output
        //   
        this.modules = []
      }
      //     
      run() {
        const ast = Parser.getAst(this.entry)
      }
      //    require  ,  bundle
      generate() {}
    }
    
    new Compiler(options).run()
    
    私達の業務コードを作成します.
    // index.js
    import welcome from './utils.js';
    console.log('Hello biubiu!')
    welcome();
    
    // ./utils.js
    export default function ppt() {
        console.log('Welcome to webpack!');
    }
    
    3.すべての依存モジュールを見つける
    Babelは@babel/trverseの方法を提供して、このASTプリリリースツリーの全体的な状態を維持します.ここで依存モジュールを探してみます.
    const fs = require('fs')
    const path = require('path')
    const options = require('./webpack.config')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    
    const Parser = {
      getAst: path => {
        //       
        const content = fs.readFileSync(path, 'utf-8')
        //        AST     
        return parser.parse(content, {
          sourceType: 'module'
        })
      },
      getDependecies: (ast, filename) => {
        console.log('ast', ast);
        const dependecies = {}
        //       import   ,  dependecies
        traverse(ast, {
          //     ImportDeclaration   AST    (  import   )
          /**
         * ImportDeclaration({ node }) {}   ,        
         * ImportDeclaration: function ImportDeclaration({ node }) {
            const dirname = path.dirname(filename)
            //         ,             
            const filepath = './' + path.join(dirname, node.source.value)
            dependecies[node.source.value] = filepath
          }
         */
          ImportDeclaration({ node }) {
            const dirname = path.dirname(filename)
            //         ,             
            const filepath = './' + path.join(dirname, node.source.value)
            dependecies[node.source.value] = filepath
          }
        })
        return dependecies
      }
    }
    
    class Compiler {
      constructor(options) {
        // webpack   
        const { entry, output } = options
        //   
        this.entry = entry
        //   
        this.output = output
        //   
        this.modules = []
      }
      //     
      run() {
        const { getAst, getDependecies } = Parser
        const ast = getAst(this.entry)
        const dependecies = getDependecies(ast, this.entry)
      }
      //    require  ,  bundle
      generate() {}
    }
    
    new Compiler(options).run()
    
    4.ASTをコードに変換する
    ASTをブラウザの実行可能なコードに変換します.私達は主に@babel/coreと@babel/preset-envを使います.
    const fs = require('fs');
    const path = require('path');
    const parser = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const { transformFromAst } = require('@babel/core');
    
    const options = require('./webpack.config.js');
    
    const Parser = {
        getAst: path => {
            const content = fs.readFileSync(path, 'utf-8');
    
            return parser.parse(content, {
                sourceType: 'module'
            })
        },
        getDependecies: (ast, fileName) => {
            const dependecies = {};
            //    importDeclaration AST   
            console.log(ast);
            traverse(ast, {
                ImportDeclaration: function ImportDeclaration({node}) {
                    // console.log(node);
                    const dirname = path.dirname(fileName);
                    //         ,              
                    const filepath = './' + path.join(dirname, node.source.value);
                    // console.log("ImportDeclaration", node, fileName, filepath);
                    dependecies[node.source.value] = filepath;
                }
            })s
            return dependecies;
        },
        getCode: (ast) => {
            // ast   code
            const { code } = transformFromAst(ast, null, {
                presets: ['@babel/preset-env']
            });
            return code;
        }
    };
    
    class Compiler {
        constructor(options) {
            const { entry, output } = options;
    
            this.entry = entry;
    
            this.output = output;
            //   
            this.modules = [];
        }
    
        //     
        run() {
            const ast = Parser.getAst(this.entry);
            // console.log(ast);
            const dependecies = Parser.getDependecies(ast,this.entry);
            // console.log(dependecies);
    
            const code = Parser.getCode(ast);
            // console.log(code);
        }
    }
    
    new Compiler(options).run();
    
    5.再帰的にすべての依存項を解析し、依存関係図を生成する.
    const fs = require('fs');
    const path = require('path');
    const parser = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const { transformFromAst } = require('@babel/core');
    
    const options = require('./webpack.config.js');
    
    const Parser = {
        getAst: path => {
            const content = fs.readFileSync(path, 'utf-8');
    
            return parser.parse(content, {
                sourceType: 'module'
            })
        },
        getDependecies: (ast, fileName) => {
            const dependecies = {};
            //    importDeclaration AST   
            console.log(ast);
            traverse(ast, {
                ImportDeclaration: function ImportDeclaration({node}) {
                    // console.log(node);
                    const dirname = path.dirname(fileName);
                    //         ,              
                    const filepath = './' + path.join(dirname, node.source.value);
                    // console.log("ImportDeclaration", node, fileName, filepath);
                    dependecies[node.source.value] = filepath;
                }
            })
    
            return dependecies;
        },
        getCode: (ast) => {
            // ast   code
            const { code } = transformFromAst(ast, null, {
                presets: ['@babel/preset-env']
            });
            return code;
        }
    };
    
    class Compiler {
        constructor(options) {
            const { entry, output } = options;
    
            this.entry = entry;
    
            this.output = output;
            //   
            this.modules = [];
        }
    
        build(fileName) {
            const { getAst, getDependecies, getCode } = Parser;
            const ast = getAst(fileName);
            
            const dependecies = getDependecies(ast, fileName);
    
            const code = getCode(ast);
    
            return {
                //     ,              
                fileName,
                //     ,         
                dependecies,
                //     
                code
    
            }
    
        }
    
        run() {
            //       
            const info = this.build(this.entry);
    
            this.modules.push(info);
            this.modules.forEach(({dependecies}) => {
                //        ,          
                if(dependecies) {
                    for(const dependecy in dependecies) {
                        this.modules.push(this.build(dependecies[dependecy]));
                    }
                }
            });
            //        
            const dependecyGraph = this.modules.reduce((graph, item) => ({
                ...graph,
                //                 ,                
                [item.fileName]: {
                    dependecies: item.dependecies,
                    code: item.code
                }
            }), {});
    
            // console.log(dependecyGraph);
        }
       
    }
    
    new Compiler(options).run();
    
    6.require関数を書き換え、コードを生成し、bundleを出力する.
    const fs = require('fs');
    const path = require('path');
    const parser = require('@babel/parser');
    const traverse = require('@babel/traverse').default;
    const { transformFromAst } = require('@babel/core');
    
    const options = require('./webpack.config.js');
    
    const Parser = {
        getAst: path => {
            const content = fs.readFileSync(path, 'utf-8');
    
            return parser.parse(content, {
                sourceType: 'module'
            })
        },
        getDependecies: (ast, fileName) => {
            const dependecies = {};
            //    importDeclaration AST   
            console.log(ast);
            traverse(ast, {
                ImportDeclaration: function ImportDeclaration({node}) {
                    // console.log(node);
                    const dirname = path.dirname(fileName);
                    //         ,              
                    const filepath = './' + path.join(dirname, node.source.value);
                    // console.log("ImportDeclaration", node, fileName, filepath);
                    dependecies[node.source.value] = filepath;
                }
            })
    
            return dependecies;
        },
        getCode: (ast) => {
            // ast   code
            const { code } = transformFromAst(ast, null, {
                presets: ['@babel/preset-env']
            });
            return code;
        }
    };
    
    class Compiler {
        constructor(options) {
            const { entry, output } = options;
    
            this.entry = entry;
    
            this.output = output;
            //   
            this.modules = [];
        }
    
        build(fileName) {
            const { getAst, getDependecies, getCode } = Parser;
            const ast = getAst(fileName);
            
            const dependecies = getDependecies(ast, fileName);
    
            const code = getCode(ast);
    
            return {
                //     ,              
                fileName,
                //     ,         
                dependecies,
                //     
                code
            }
        }
    
        run() {
            //       
            const info = this.build(this.entry);
    
            this.modules.push(info);
            this.modules.forEach(({dependecies}) => {
                //        ,          
                if(dependecies) {
                    for(const dependecy in dependecies) {
                        this.modules.push(this.build(dependecies[dependecy]));
                    }
                }
            });
            //        
            const dependecyGraph = this.modules.reduce((graph, item) => ({
                ...graph,
                //                 ,                
                [item.fileName]: {
                    dependecies: item.dependecies,
                    code: item.code
                }
            }), {});
    
            // console.log(dependecyGraph);
    
            this.generate(dependecyGraph);
        }
    
        //   require  (       common.js  ),  bundle
        generate(code) {
            const filepath = path.join(this.output.path, this.output.fileName);
            const bundle = `(function (graph) {
                ;
            
                function require(moduleId) {
                    const exports = {}
                    ;function localRequire(relativePath) {
                        return require(graph[moduleId].dependecies[relativePath]);
                    }
            
                    ;(function(require, exports, code){
                        eval(code);
                    }(localRequire, exports, graph[moduleId].code))
            
                    return exports;
                }
                require('${this.entry}')
            })(${JSON.stringify(code)})`;
            fs.writeFileSync(filepath, bundle, 'utf-8');
        }
    }
    
    new Compiler(options).run();
    
    7.requireのロジックを深く分析する
    ここにはコードをツリーにすると、イベントはルートノードであり、ルートノードが存在するからこそ、requireを書き換える方法でこの論理を実現できるという前提があります.
    7.1まずrequireを定義します.
    まず簡単なのをください.
    generate(code) {
            const filepath = path.join(this.output.path, this.output.fileName);
            const bundle = `(function (graph) {
                ;
            
                function require(moduleId) {
                    console.log("moduleId", moduleId);
                }
                require('${this.entry}')
            })(${JSON.stringify(code)})`;
            fs.writeFileSync(filepath, bundle, 'utf-8');
        }
    
    出力の結果は:
    (function (graph) {
        ;
    
        function require(moduleId) {
            console.log(moduleId);
        }
        require('./src/index.js')
    })({
        "./src/index.js": {
            "dependecies": {
                "./utils.js": "./src/utils.js"
            },
            "code": "\"use strict\";

    var _utils = _interopRequireDefault(require(\"./utils.js\"));

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }

    console.log('Hello biubiu!');
    (0, _utils[\"default\"])();"
    }, "./src/utils.js": { "dependecies": {}, "code": "\"use strict\";

    Object.defineProperty(exports, \"__esModule\", {
    value: true
    });
    exports[\"default\"] = ppt;

    function ppt() {
    console.log('Welcome to webpack!');
    }"
    } })
    7.2実行可能コードを取得する
    graphのコードをmoduleIdで取り出して実行します.
    generate(code) {
            const filepath = path.join(this.output.path, this.output.fileName);
            const bundle = `(function (graph) {
                ;
            
                function require(code) {
                    console.log(code);
                    eval(code);
                }(graph[moduleId].code)
                require('${this.entry}')
            })(${JSON.stringify(code)})`;
            fs.writeFileSync(filepath, bundle, 'utf-8');
        }
    
    出力結果:
    (function (graph) {
        ;
    
        function require(code) {
            console.log(code);
            eval(code);
        }(graph[moduleId].code)
        require('./src/index.js')
    })({
        "./src/index.js": {
            "dependecies": {
                "./utils.js": "./src/utils.js"
            },
            "code": "\"use strict\";

    var _utils = _interopRequireDefault(require(\"./utils.js\"));

    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }

    console.log('Hello biubiu!');
    (0, _utils[\"default\"])();"
    }, "./src/utils.js": { "dependecies": {}, "code": "\"use strict\";

    Object.defineProperty(exports, \"__esModule\", {
    value: true
    });
    exports[\"default\"] = ppt;

    function ppt() {
    console.log('Welcome to webpack!');
    }"
    } })
    実行後にエラーが発生したのは./src/utils.jsモジュールが実行されていないからです.
    7.3オブジェクトアドレスマップに依存して、exportオブジェクトを取得する
    generate(code) {
            const filepath = path.join(this.output.path, this.output.fileName);
            const bundle = `(function (graph) {
                ;
            
                //   require  
                function require(moduleId) {
                  //     moduleId     ,  require  ,eval  ,  exports  
                  function localRequire(relativePath) {
                    return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
                  }
                  //   exports  
                  var exports = {}
                  ;(function(require, exports, code) {
                    // commonjs    module.exports    ,     exports         (hello.js)     (exports.say = say)   
                    eval(code)
                  })(localRequire, exports, graph[moduleId].code)
                  //   exports  ,            
                  return exports
                }
                //          
                require('${this.entry}')
            })(${JSON.stringify(code)})`;
            fs.writeFileSync(filepath, bundle, 'utf-8');
        }
    
    参考文献:
    Webpack概念用語
    webpack包装の原理?これを読んだら分かります.