Webpack模塊解析構建流程

原文發佈在個人 GitHub 歡迎 star 收藏。webpack

webpack模塊解析流程
本文源碼來源:webpack-4git

編譯以前

在開始編譯流程以前,webpack 會處理用戶配置,首先進行校驗,經過後與默認配置合併或者是調用相應函數進行處理,輸出最終的 options。以後實例化 Compiler,並傳入 options,併爲註冊的 plugins 注入 compiler 實例。若是配置了 watch 選項,則添加監聽,在資源變化的時候會從新進行編譯流程;不然直接進入編譯流程:github

const webpack = (options, callback) => {
  // 校驗
  validateSchema(webpackOptionsSchema, options);
  /* ... */
  // 處理、合併 options
  options = new WebpackOptionsDefaulter().process(options);
  // 實例化 Compiler
  const compiler = new Compiler(options.context);
  /* ... */
  // 註冊插件
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  // 開始編譯流程
  if (watch) {
    compiler.watch(watchOptions, callback);
  } else {
    compiler.run((err, stats) => {
      compiler.close(err2 => {
        callback(err || err2, stats);
      });
    });
  }
}

module 編譯

compiler.run() 的過程當中,會調用 compile 方法。在 compile 方法中會實例化 Compilation,模塊的編譯構建流程都由 Compilation 控制。實例化 Compilation 後緊接着觸發 compilationmake 事件,從 make 開始主編譯流程。這裏有一個很細節的點,其實也與 webpack 的插件機制有關,webpack 的插件機制當然讓其有很好的擴展性,但對於閱讀源碼來講會把人繞暈,衆多的鉤子,註冊鉤子函數的代碼與觸發的地方很難讓人找到其聯繫之處,只能經過編輯器的全局搜索去找到註冊的鉤子函數。在處理 options 的時候,會調用到 EntryPlugin 插件,其內部會註冊 compilationmake 事件:web

apply(compiler) {
  compiler.hooks.compilation.tap(
    "EntryPlugin",
    (compilation, { normalModuleFactory }) => {
      // 設置 DependencyFactories,在添加 moduleChain 的時候會用上
      compilation.dependencyFactories.set(
        EntryDependency,
        normalModuleFactory
      );
    }
  );

  compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
    const { entry, name, context } = this;

    const dep = EntryPlugin.createDependency(entry, name);
    // 編譯文件模塊的入口
    compilation.addEntry(context, dep, name, err => {
      callback(err);
    });
  });
}

addEntry

make 事件觸發後,會調用 compilation.addEntry 從入口文件開始,根據 dep 類型判斷使用哪種 moduleFactory,而且將入口添加到 _preparedEntrypoints 屬性中,其結構以下:數組

const slot = {
  name: name, // entry 的 name
  request: null, // entry 的 request
  module: null // 對 entry 文件解析成的最終 module,在 afterBuild 中對其賦值
};
// ...
this._preparedEntrypoints.push(slot);

調用 _addModuleChian 將模塊加入編譯鏈條,會傳入一個 dependency 對象,存有 entrynamerequest 等信息。request 中,存儲的是文件的路徑信息:app

addEntry(context, entry, name, callback) {
  this.hooks.addEntry.call(entry, name);
  // ...
  this._addModuleChain(
    context,
    entry,
    module => {
      this.entries.push(module);
    },
    (err, module) => {
      // ...
    }
  );
}

createModule

_addModuleChain 的回調中會調用到 xxxModuleFactorycreate 方法。以 normalModuleFactorycreate 方法爲例,先是觸發 beforeResolve 事件,而後觸發 normalModuleFactoryfactory 事件,並調用返回的 factory 方法:async

create(data, callback) {
  // ...
  this.hooks.beforeResolve.callAsync({ ... },
    (err, result) => {
      // ...
      const factory = this.hooks.factory.call(null);
      // ...
      factory(result, (err, module) => {
        // ...
      });
    }
  );
}

factory 方法中,會觸發 resolver 事件,並返回一個函數,經過調用此函數執行文件路徑及相關 loader 路徑的解析,完成後觸發 afterResolve 事件,並在其回調中生成一個 module 實例,並將 resolve 的結果存入其中:編輯器

// factory 事件註冊
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
  let resolver = this.hooks.resolver.call(null);
  // ...
  resolver(result, (err, data) => {
    // ...
    this.hooks.afterResolve.callAsync(data, (err, result) => {
      // ...
      let createdModule = this.hooks.createModule.call(result);
      if (!createdModule) {
        if (!result.request) {
          return callback(new Error("Empty dependency (no request)"));
        }
        // normalModule 實例
        createdModule = new NormalModule(result);
      }
      createdModule = this.hooks.module.call(createdModule, result);
      return callback(null, createdModule);
    });
  });
});

// resolve 事件註冊
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  // ...
  // loader 和 normal 文件的 resolve 有所區別
  const loaderResolver = this.getResolver("loader");
  const normalResolver = this.getResolver("normal", data.resolveOptions);
  // resolve loader & normal path
});

buildModule

回到 normalModuleFactory.createcallback 中,會依次執行 addModule ---> addReason ---> buildModule ---> afterBuild函數

  • addModule: 將前面建立的 module 實例添加到全局 Compilation.modules 數組中和 _modules 對象中;
  • addReason: 爲該 module 添加 reason,便是哪一個 module 依賴了該 module
  • buildModule: 編譯 module
  • afterBuild: 處理依賴,遞歸編譯 module

調用 buildModule,最終會走到相應 module 實例的 build 方法中,前面咱們的 moduleNormalModule 的實例,因此咱們來看看 NormalModulebuild 方法:ui

// NormalModule 的 build 方法
build(options, compilation, resolver, fs, callback) {
  // ...
  return this.doBuild(options, compilation, resolver, fs, err => {
    // doBuild 回調
    // ...
    try {
      // 進行 AST 的轉換
      const result = this.parser.parse(
        // 若是在 runLoaders 的時候已經解析成 AST 則使用 _ast,不然傳入 JS 源代碼
        this._ast || this._source.source(),
        { current: this, module: this, compilation: compilation, options: options },
        (err, result) => {
          if (err) handleParseError(err);
          else handleParseResult(result);
        }
      );
      if (result !== undefined) handleParseResult(result);
    } catch (e) {
      handleParseError(e);
    }
  });
}
// doBuild 方法
doBuild(options, compilation, resolver, fs, callback) {
  // ...
  runLoaders(
    {
      resource: this.resource,
      loaders: this.loaders,
      context: loaderContext,
      readResource: fs.readFile.bind(fs)
    },
    (err, result) => {
      // ...
      // createSource 將 loaders 處理的結果轉換爲字符串
      this._source = this.createSource(
        this.binary ? asBuffer(source) : asString(source),
        resourceBuffer,
        sourceMap
      );
      this._sourceSize = null;
      // 若是已經轉換爲 AST,則存在 _ast 中
      this._ast =
        typeof extraInfo === "object" &&
        extraInfo !== null &&
        extraInfo.webpackAST !== undefined
          ? extraInfo.webpackAST
          : null;
      return callback();
    }
  );
}

處理依賴

duBuild 方法中,會進行 loaders 的轉換。在 this.parser.parse 方法中,會將轉換後的源碼進行 AST 轉換,並在 import/export 等地方添加相應的 Dependency,也能夠理解爲某種佔位符,在後續的解析中,會根據相應的模板等進行替換生成最終文件。buildModule 結束後,執行其回調,並調用 afterBuild 方法,在 afterBuild 方法中,調用了 processModuleDependencies 方法處理依賴:

processModuleDependencies(module, callback) {
  const dependencies = new Map();
  // 省略對 dependencies 作的一些處理
  const sortedDependencies = [];
  // 將依賴解析成下面的結構
  for (const pair1 of dependencies) {
    for (const pair2 of pair1[1]) {
      sortedDependencies.push({
        factory: pair1[0],
        dependencies: pair2[1]
      });
    }
  }
  // 添加模塊依賴的解析
  this.addModuleDependencies(
    module,
    sortedDependencies,
    this.bail,
    null,
    true,
    callback
  );
}
// addModuleDependencies 遍歷依賴,並從新進行 module 的編譯
addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) {
  const start = this.profile && Date.now();
  const currentProfile = this.profile && {};

  asyncLib.forEach(
    dependencies,
    (item, callback) => {
      const dependencies = item.dependencies;
      // ...
      semaphore.acquire(() => {
        const factory = item.factory;
        // create
        factory.create(
          {
            // ...
          },
          (err, dependentModule) => {
            // ...
            const iterationDependencies = depend => {
              for (let index = 0; index < depend.length; index++) {
                // ...
                // addReason
                dependentModule.addReason(module, dep);
              }
            };
            // addModule
            const addModuleResult = this.addModule(
              dependentModule,
              cacheGroup
            );
            // ...
            if (addModuleResult.build) {
              // buildModule
              this.buildModule(
                dependentModule,
                isOptional(),
                module,
                dependencies,
                err => {
                  // ...
                  // afterBuild
                  afterBuild();
                }
              );
            }
            // ...
          }
        );
      });
    },
    err => {
      // ...
    }
  );
}

能夠看到,在 addModuleDependencies 方法中,對前一個 module 的依賴進行遍歷,又從新執行 factory.create ---> addModule ---> buildModule ---> afterBuild,進行編譯。當全部的依賴都編譯完,便完成了 module 的編譯過程。回到 make 事件的回調中,此時全部須要編譯的 module 都已經通過上面的步驟處理完畢,接下來會調用 compilation.seal 進行 chunk 圖的生成工做:

this.hooks.make.callAsync(compilation, err => {
  if (err) return callback(err);
  compilation.finish(err => {
    if (err) return callback(err);
    // seal 階段,組合 chunk,生成 chunkGroup
    compilation.seal(err => {
      if (err) return callback(err);
      // 在其回調中觸發 afterCompile 事件,編譯過程結束
      this.hooks.afterCompile.callAsync(compilation, err => {
        if (err) return callback(err);
        return callback(null, compilation);
      });
    });
  });
});

生成 chunkGroup

未完待續...

相關文章
相關標籤/搜索