淺析webpack原始碼之convert-argv模組(二)

織雪紗奈發表於2019-01-10

開啟webpeck-cli下的convert-argv.js檔案

// 定義options為空陣列
 const options = [];
 // webpack -d 檢查 -d指令
  if (argv.d) { 
      //...
  }
  // webpack -p
  if (argv.p) {
      //...
  }
  if (argv.output) {
      //...
  }
   //...
   /*如果有 --config   --config webpack.config.js   config就是webpack.config.js
    可以這樣理解 
    "dev": "webpack-dev-server --inline --progress --hot --config webpack.config.js",當我們npm run dev的時候執行這段
    package.json的內容 此時有config讀取webpack.config.js的內容 當我們npm run build時 執行 "webpack" 此時沒有config走else分支*/
    if (argv.config) {
        // ... 獲取檔案
    }else{
        /*讀取預設配置 ts co 等字尾類
         defaultConfigFiles是 陣列[{ path:
         `/Users/orion/Desktop/react-beauty-highcharts/webpack.config.js`,
         ext: `.js`
         },{path:`/Users/orion/Desktop/react-beauty-highcharts/webpack.config.ts`, ext: `.ts`},{},...] */
        for (i = 0; i < defaultConfigFiles.length; i++) {
            const webpackConfig = defaultConfigFiles[i].path;
            // 讀取檔案,如果有的話push推進去
            if (fs.existsSync(webpackConfig)) {
                configFiles.push({
                    path: webpackConfig,
                    ext: defaultConfigFiles[i].ext
                });
                //     最終結果configFiles is the Array [ { path:`/Users/orion/Desktop/react-beauty-highcharts/webpack.config.js`,
                //  ext: `.js` } ]
                break;
            }
        
        }
    }

process.cwd() 是node.js裡讀取檔案路徑的一個API

//configFiles長度大於0時
if (configFiles.length > 0) {
    // ...
    const requireConfig = function requireConfig(configPath) {
        // 這是區域性options不要和全域性的options陣列混淆
        let options = (function WEBPACK_OPTIONS() {
            if (argv.configRegister && argv.configRegister.length) {
                module.paths.unshift(
                    path.resolve(process.cwd(), "node_modules"),
                    process.cwd()
                );
                argv.configRegister.forEach(dep => {
                    require(dep);
                });
                return require(configPath);
            } else {
                // 讀取路徑下的檔案內容返回
                return require(configPath);
            }
        })();
        // 預處理options,options若是陣列的話,處理成物件之類的
        options = prepareOptions(options, argv);
        return options;
    };
    configFiles.forEach(function(file) {
            /// interpret.extensions[.js]為null
            // 這裡直接跳出
            registerCompiler(interpret.extensions[file.ext]);
            // options這裡是全域性options空陣列 
            options.push(requireConfig(file.path));
        });
        // configFileLoaded 載入完畢
        configFileLoaded = true;
     }
     // 如果沒有載入完畢,呼叫函式傳遞空陣列
    if (!configFileLoaded) {
        return processConfiguredOptions({});
    } else if (options.length === 1) {
       // 如果只有一個,把僅有的傳進去 
        return processConfiguredOptions(options[0]);
    } else {
        // 傳options
        return processConfiguredOptions(options);
    }

注意了,這裡有一個return 也就是這個convert-argv模組的最終返回結果,函式到這裡就結束了。接下來我看看一下processConfiguredOptions函式

我們先按照npm run build分支走options.length為1,讀取options[0]是webpack.config.js裡的module.exports ={} 物件,饒了這大的一個圈子,那麼接下來一起來看一看對你的輸入配置做了怎麼樣的處理吧?

function processConfiguredOptions(options) {
        // 非法輸出型別檢測
        const webpackConfigurationValidationErrors = validateSchema(
            webpackConfigurationSchema,
            options
        );
        if (webpackConfigurationValidationErrors.length) {
            const error = new WebpackOptionsValidationError(
                webpackConfigurationValidationErrors
            );
            // 報錯處理,具體什麼是非法,不影響主流程先過?
            console.error(
                error.message,
                `
Received: ${typeof options} : ${JSON.stringify(options, null, 2)}`
            );
            process.exit(-1); // eslint-disable-line
        }
        
        // 如果options是Promise?,以後待查,按照我們的npm run build 流程走不是Promise
        if (typeof options.then === "function") { 
            return options.then(processConfiguredOptions);
        }
        
        /* process ES6 default 檢測,雖說我們們的webpack.congfig.js 的內容是用的 ES6 module.exports 寫的,但是options是物件已經被讀出來了,所以不走這個分支,
        可是為什麼要再寫一遍呢?看來還是有這種情況的,繼續往下讀*/
        if (typeof options === "object" && typeof options.default === "object") {
            return processConfiguredOptions(options.default);
        }
        
        // filter multi-config by name,對於多配置的處理,我們先不走這個分支
        if (Array.isArray(options) && argv["config-name"]) {
          //...
        }
        if (Array.isArray(options)) {
            options.forEach(processOptions);
        } else {
            /* 看了這個多判斷終於有沒有找到家的感覺,回家的路程好艱辛,接下來我們看看processOptions函式對options做了什麼
            それは何をしましたか?? */
            processOptions(options);
        }
        // ... 
         // 這個return 正好對應上個return,是最終的模組返回值
        return options;
function processOptions(options) {
        // 基本處理函式
        function ifArg(name, fn, init, finalize) {
            //... 
        }
        function ifArgPair(name, fn, init, finalize){
            //... 
        }
        function ifBooleanArg(name, fn) {
            //... 
        }
}

我們看到processOptions先定義了幾個函式
ifArg,ifArgPair,ifBooleanArg,loadPlugin 根據名字可以翻譯
如果是引數,如果是鍵值對形式引數 如果是布林引數,如果是外掛
提起外掛,是不是就想起了我們如下寫的 plugins,對這個配置的處理,讀原始碼聯想大法和通過名稱翻譯的方法還是很重要的,自己寫的時候,儘量起好懂的名稱,不要用中文拼音?

{
plugins: [
    new HtmlWebpackPlugin({ // 例項化生成html外掛
      title: `title`,
      template: `./src/index.html`, 
      filename: `index.html`, 
      inlineSource:  `.(js|css))$`,  
      minify: {
          removeComments: true,
          collapseWhitespace: true
      },
      chunks: ["index"]
   }),        
    new HtmlWebpackPlugin()
  ],
  }

接下來是真正的函式呼叫

ifArg("mode", function(value) {
    options.mode = value;
});

function ifArg(name, fn, init, finalize) {
    const isArray = Array.isArray(argv[name]);
    const isSet = typeof argv[name] !== "undefined" && argv[name] !== null;
    // isArray為false 直接返回
    if (!isArray && !isSet) return;
    

    init && init();
    if (isArray) argv[name].forEach(fn);
    else if (isSet) fn(argv[name], -1);
    finalize && finalize();
}
  • 說一下argv目前argv是這麼個物件
  • webpakck -p 有- p的時候有p=true
  • 有 -d的時候有d=true
  • `$0`是webpack的目前地址
  • 其他的自己悟,哈哈哈哈哈? 聽說90後喜歡用這個表情
{ 
    _: [],
    cache: null,
    bail: null,
    profile: null,
    color: [Function: getSupportLevel],
    colors: [Function: getSupportLevel],
    d: true,
    p: true,
    `info-verbosity`: `info`,
    infoVerbosity: `info`,
    `$0`:
    `/Users/orion/Desktop/react-beauty-highcharts/node_modules/.bin/webpack`,
    debug: true,
    `output-pathinfo`: true,
    devtool: `eval-cheap-module-source-map`,
    mode: `development`,
    `optimize-minimize`: true,
    define: [ `process.env.NODE_ENV="production"` ] 
}

我們接著讀這個函式

function ifArg(name, fn, init, finalize) {
    //如果argv[name]是陣列 
    const isArray = Array.isArray(argv[name]);
    //如果argv[name]存在
    const isSet = typeof argv[name] !== "undefined" && argv[name] !== null;
    // isArray和isSet同時為false的時候返回
    if (!isArray && !isSet) return;
    // 按照目前的流程走,能過來的name有define,output-pathinfo,debug,devtool,optimize-minimize
    //如果init函式存在就執行init函式,目前流程只有define有init,執行函式defineObject = {};
    init && init();
    //如果是陣列,執行傳入的fn
    if (isArray) argv[name].forEach(fn);
    else if (isSet) fn(argv[name], -1);
    finalize && finalize();
}
這裡只有一個是陣列define,我們追蹤define
   ifArgPair(
            "define",
            function(name, value) {
                if (name === null) {
                    name = value;
                    value = true;
                }
                defineObject[name] = value;
            },
            function() {
                defineObject = {};
            },
            function() {
                const DefinePlugin = require("webpack").DefinePlugin;
                addPlugin(options, new DefinePlugin(defineObject));
            }
        );
    }
    
        function ifArgPair(name, fn, init, finalize) {
            ifArg(
                name,
                function(content, idx) {
                    const i = content.indexOf("=");
                    if (i < 0) {
                        return fn(null, content, idx);
                    } else {
                        // 根據
                        return fn(content.substr(0, i), content.substr(i + 1), idx);
                    }
                },
                init,
                finalize
            );
        }
      // 接著,ifArgPair會呼叫ifArg對fn進行了處理
      從fn到
      function(content, idx) {
                    const i = content.indexOf("=");
                    if (i < 0) {
                        return fn(null, content, idx);
                    } else {
                        // 根據
                        return fn(content.substr(0, i), content.substr(i + 1), idx);
                    }
                }
     
        // 目前define是陣列,define =[ `process.env.NODE_ENV="production"` ]
         if (isArray) argv[name].forEach(fn);
         // 執行fn 的引數把 等號分割,執行
         function(name, value) {
              // name = process.env.NODE_ENV
              // value = production
            
                if (name === null) {
                    name = value;
                    value = true;
                }
                defineObject[name] = value;
                
            }
        // 函式執行結果就是
         defineObject.process.env.NODE_ENV =  production
    
         // 最後返回的是options,此後還有執行第三個引數出入了defineObject最終影響了options配置

            

我們接著往下讀

//如果不為陣列,而且存在執行fn
else if (isSet) fn(argv[name], -1);

我們追蹤output-pathinfo

    ifBooleanArg("output-pathinfo", function() {
        ensureObject(options, "output");
        options.output.pathinfo = true;
    });
    
    function ifBooleanArg(name, fn) {
            ifArg(name, function(bool) {
                if (bool) {
                   // 如果配置是true那麼執行函式
                    fn();
                }
            });
        }
    // 函式執行結果就是
    options.output.pathinfo = true;

再往下讀

finalize && finalize();
對於defin欄位春入了finalize函式,即第三個函式
function() {
    // 從webpack引入DefinePlugin物件
    const DefinePlugin = require("webpack").DefinePlugin;
    // 呼叫addPlugin
    addPlugin(options, new DefinePlugin(defineObject));
}

function addPlugin(options, plugin) {
        ensureArray(options, "plugins");
        options.plugins.unshift(plugin);
}
// 最終結果對options做了處理

其他同理
?這個模組終於寫完了

我們總結一下

這個模組做的就是從webpack配置檔案讀取配置賦值給options,然後根據配置,對options做了附加的物件處理比如options.output.pathinfo = true;然後返回options,國外小哥哥好能繞
我們在bin/cli檔案裡列印一下最後的config

{ entry: `./src/index.js`,
  output:
   { filename: `index.js`,
     path: `/Users/orion/Desktop/react-beauty-highcharts/dist`,
     libraryTarget: `commonjs2`,
     pathinfo: true },
  module: { rules: [ [Object], [Object] ] },
  devServer: { openPage: `./src/index.html`, open: true, hot: true },
  externals: [ [Function] ],
  mode: `development`,
  plugins:
   [ LoaderOptionsPlugin { options: [Object] },
     LoaderOptionsPlugin { options: [Object] },
     DefinePlugin { definitions: [Object] } ],
  devtool: `eval-cheap-module-source-map`,
  context: `/Users/orion/Desktop/react-beauty-highcharts` }

鼓掌?給自己一朵小紅花?

相關文章