Webpack源碼分析 - 入口Entry

入口Entry

Webpack的執行流程思想很是簡單,從入口文件開始,遞歸地查找文件的依賴,最終將全部依賴輸出到一個文件中。在這過程當中又穿插了文件解析、輸出優化等複雜的操做,咱們就從最簡單的入口開始,逐漸剝繭抽絲,撥開webpack的神祕面紗。webpack

從配置提及

配置是衡量一個系統靈活性的主要標識,咱們在使用某個系統前都會先去查看提供的配置項,能夠快速告訴咱們這個系統能夠用什麼樣的方式運行。在深刻理解源碼前,咱們有必要先熟悉它提供的配置,一是能夠提早預習並大概想象它的實現思路,二是能夠在研究源碼時更好地追蹤代碼的起源。web

因爲Webpack提供了強大的靈活性,它的配置也很是複雜,許多人剛開始就掛在了它的配置上,好在入口的配置還不算複雜,主要有如下兩部分:數組

context

context的中文意思是上下文,上下文在不一樣的環境下有不一樣含義,在webpack中的配置項中有一項context,它的意思是解析入口文件時的基準目錄,會從該目錄下查找入口文件,默認爲執行編譯命令時的絕對路徑。app

一般狀況下咱們不須要設置,可是若是咱們在其餘目錄執行webpack就會找不到路徑,因此咱們最好給他設置一個默認值:異步

context: path.resolve(__dirname, "src")
複製代碼

entry

entry就是咱們配置入口的地方了,支持多種配置類型,一般咱們只須要用到string類型參數。若是使用object配置,則會輸出多份以它們的key爲名稱的文件。另外還能使用動態配置,入口能夠支持異步獲取:函數

// string
entry: "./src/entry"
// array
entry: ["./src/entry1", "./src/entry2"]
// object
entry: {
    a: "./src/entry-a",
    b: ["./src/entry-b1", "./src/entry-b2"]
}
// 動態
entry: () => './demo'
// 動態
entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))
複製代碼

流程梳理

因爲Wepack的插件機制,致使了實現方式略顯複雜,入口流程基本圍繞如下幾點進行:優化

  1. 配置解析: 註冊執行入口配置解析插件EntryOptionPlugin
  2. 入口配置處理: SingleEntryPluginMultiEntryPluginDynamicEntryPlugin
  3. 註冊入口依賴解析器: NormalModuleFactory解析單入口依賴,MultiModuleFactory解析多入口依賴
  4. 建立入口依賴: 生成SingleEntryDependencyMultiEntryDependency
  5. 解析入口依賴: 生成NormalModuleMultiModule

配置解析

Webpack啓動後會先預處理配置文件,預處理後便會交由WebpackOptionsApply來根據配置註冊各類插件,其中就會涉及到入口配置的處理插件EntryOptionPluginEntryOptionPlugin顧名思義就是處理入口配置的插件,在這裏將處理entry的幾種不一樣配置方法:ui

// EntryOptionPlugin.js
function apply(context, entry) {
    if (typeof entry === "string") {
        // 默認輸出文件名爲 main
	    new SingleEntryPlugin(context, entry, 'main')
    } else if(Array.isArray(entry)) {
        new MultiEntryPlugin(context, entry, 'main')
    } else if (typeof entry === "object") {
        for (const name of Object.keys(entry)) {
            if (typeof entry[name] === "string") {
                // 默認輸出文件名爲 key
                new SingleEntryPlugin(context, entry[name], name)
            } else if(Array.isArray(entry[name])) {
                new MultiEntryPlugin(context, entry[name], name)
            }
        }
    } else if (typeof entry === "function") {
        new DynamicEntryPlugin(context, entry)
    }
})
複製代碼

能夠看到代碼仍是很簡潔,得益於Webpack的插件機制,這裏很是方便地將不一樣類型的配置分配給不一樣的插件來處理。固然雖然代碼會稍顯囉嗦,可是帶來的優勢很是明顯,職責分工明確,並且靈活性和擴展性都很是好。this

入口配置處理 - 單入口配置

SingleEntryPlugin用於處理string類型的配置,如entry: "./src/entry"entry: { a: "./src/entry" }。在這裏一共作了兩件事,一是註冊單入口依賴處理器,二是建立單入口依賴並執行解析。spa

依賴是Webpack裏一個重要概念,用於描述模塊間的關係,每一個依賴都有對應的處理器對其解析。這裏咱們只要知道單入口依賴SingleEntryDependency是由NormalModuleFactory進行解析,且他們之間的關係是在這裏進行描述:

// SingleEntryPlugin.js
// 在compiler.hooks.compilation階段註冊,這個鉤子會在建立Compilation後調用
compiler.hooks.compilation.tap(
    (compilation, { normalModuleFactory }) => {
        // dependencyFactories維護了依賴與解析依賴方法的關係
        compilation.dependencyFactories.set(
            SingleEntryDependency,
            normalModuleFactory
        );
    }
);
複製代碼

接着註冊了建立依賴的方法,這裏將是真正將入口文件轉換稱爲依賴對象,接着將其添加到compilation中正式開始解析,從這裏開始就是入口和編譯器的樞紐:

// SingleEntryPlugin.js
// compiler.hooks.make 鉤子在開始執行編譯時調用
compiler.hooks.make.tapAsync(
    (compilation, callback) => {
        const { entry, name, context } = this;
        const dep = new SingleEntryDependency(entry);
        dep.loc = { name };
        compilation.addEntry(context, dep, name, callback);
    }
);
複製代碼

入口配置處理 - 多入口配置

多入口的流程和單入口差很少,不一樣的是這裏要註冊多入口和單入口兩種處理器,能夠從建立依賴對象中看到,一個多入口依賴裏包含了多個單入口依賴:

// MultiEntryPlugin.js
compiler.hooks.compilation.tap(
    (compilation, { normalModuleFactory }) => {
        const multiModuleFactory = new MultiModuleFactory();
        compilation.dependencyFactories.set(
            MultiEntryDependency,
            multiModuleFactory
        );
        compilation.dependencyFactories.set(
            SingleEntryDependency,
            normalModuleFactory
        );
    }
);
compiler.hooks.make.tapAsync(
    (compilation, callback) => {
        const { context, entries, name } = this;
        const dep = new MultiEntryDependency(
			entries.map((e => new SingleEntryDependency(e)),
			name
        ));
        compilation.addEntry(context, dep, name, callback);
    }
);
複製代碼

入口配置處理 - 動態入口配置

動態入口配置也很簡單,就是在執行完配置函數後,根據執行結果轉換成單入口依賴或多入口依賴:

// DynamicEntryPlugin.js
compiler.hooks.make.tapAsync(
    (compilation, callback) => {
        const addEntry = (entry, name) => {
            const dep = Array.isArray(entry) ?
                MultiEntryPlugin.createDependency(entry, name) :
                SingleEntryPlugin.createDependency(entry, name);
            return new Promise((resolve, reject) => {
                compilation.addEntry(this.context, dep, name, err => {
                    if (err) return reject(err);
                    resolve();
                });
            });
        };
        Promise.resolve(this.entry()).then(entry => {
            addEntry(entry, "main").then(() => callback(), callback);
        });
    }
);
複製代碼

解析入口依賴

通過上面處理後,不一樣的入口配置就轉換成爲了依賴,接下來就開始編譯器Compilation的工做。Compilation的做用就是遞歸解析依賴,從而獲取全部須要打包的文件,因此它的工做原理基本上就是添加依賴 -> 解析依賴模塊 -> 獲得該模塊的其餘依賴 -> 添加依賴這麼一個循環。Compilation中添加入口依賴的函數就是addEntry

函數首先將入口依賴添加到_preparedEntrypoints中,這個數組在輸出文件時使用,往數組添加幾個入口依賴,就輸出幾個文件,輸出的代碼咱們在後面文章分析:

  • string: 有一個單入口依賴SingleEntryDependency,輸出一個文件
  • array: 有一個多入口依賴MultiEntryDependency,輸出一個文件
  • object: 有多少個依賴,輸出多少個文件
// Compilation.js
function addEntry(context, entry, name, callback) {
    const slot = {
        name: name,
        request: entry.request,
        module: null
    };
    this._preparedEntrypoints.push(slot);
    this._addModuleChain(context, entry,
        (module) => { this.entries.push(module); },
        (err, module) => {
            return callback(null, module);
        }
    );
}
複製代碼

接着調用真正解析依賴的方法_addModuleChain,這段代碼比較複雜,咱們能夠先忽略其中細節,看其中最重要的解析方法。

首先經過dependencyFactories拿到依賴對應的解析器,前面配置的依賴處理在這裏派上用場了,因此若是依賴是SingleEntryDependency,這裏的moduleFactory拿到的就是前面註冊的NormalModuleFactoryNormalModuleFactory的執行原理咱們後面再講,如今只要知道它的做用是將依賴就轉換爲模塊,最後就是構建模塊而後遞歸處理依賴。

// Compilation.js
function _addModuleChain(context, dependency, onModule, callback) {
    const Dep = /** @type {DepConstructor} */ (dependency.constructor);
    const moduleFactory = this.dependencyFactories.get(Dep);
    moduleFactory.create(
        {
            contextInfo: {
                issuer: "",
                compiler: this.compiler.name
            },
            context: context,
            dependencies: [dependency]
        },
        (err, module) => {
            // ...
            // 構建模塊
            this.buildModule(module, false, null, null, err => {
                // 處理入口模塊的依賴
                if (addModuleResult.dependencies) {
                    this.processModuleDependencies(module, err => {
                        if (err) return callback(err);
                        callback(null, module);
                    });
                } else {
                    return callback(null, module);
                }
            });
        }
    );
}
複製代碼

數組參數僞代碼

因爲Webpack的插件模式使代碼跳躍性比較大,下面咱們使用同步的僞代碼來看數組形式的參數整個的入口運做流程,將上面的內容串起來:

function compile() {
    // webpack.js 解析參數
    const context = path.resolve(__dirname)
    const entry = ['./src/foo', './src/bar']
    const name = 'main'

    // Compiler.js 建立編譯器
    const compilation = new Compilation();
    const normalModuleFactory = new NormalModuleFactory();
    const multiModuleFactory = new MultiModuleFactory();

    // MultiEntryPlugin.js 註冊解析器/建立依賴
    compilation.dependencyFactories.set(MultiEntryDependency, multiModuleFactory)
    compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
    const dep = new MultiEntryDependency(
        entries.map((e => new SingleEntryDependency(e)),
        name
    ));

    // Compilation.js 解析依賴
    const multiModuleFactory = compilation.dependencyFactories.get(dep)
    // 解析出模塊
    const multiModule = multiModuleFactory.create(context, dependency)
    // 構建模塊
    multiModule.buildModule()
    // 處理入口模塊的依賴
    multiModule.dependencies.forEach((singleEntryDependency) => {
        const normalModuleFactory = compilation.dependencyFactories.get(singleEntryDependency)
        // 解析出模塊
        const normalModule = normalModuleFactory.create(context, dependency)
        normalModule.buildModule()
        // 繼續循環解析依賴...
        normalModule..dependencies.forEach(...)
    })
    // 構建完成打包輸出...
}
複製代碼
相關文章
相關標籤/搜索