Webpack源碼分析 - 模塊Module

模塊 Module

在Webpack中,一切資源都被看做模塊,也就是說不論是jscss仍是圖片文件,Webpack都將他們都抽象成一個個模塊,模塊記錄了資源的位置和內容,編譯過程就是從入口模塊開始遞歸找到全部須要用的模塊,最終將全部模塊打包輸出。javascript

模塊說白了就是文件在Webpack中的表示對象,道理很簡單,但不知你們心中有沒有許多問號,資源是怎樣轉換成爲模塊?Webpack怎樣識別不一樣的資源?怎樣從收集到全部的模塊?模塊又是怎樣輸出結果?css

前面的入口Entry章節咱們已經知道,一個單入口配置通過一頓操做後會生成NormalModule來表示入口文件模塊,從第一個起始模塊開始,就能夠將其餘requireimport導入的資源通通轉成模塊供後續輸出流程使用。這節開始,咱們就來揭曉Webpack中模塊是怎樣構建起來:java

開局一個簡單的js文件

js文件是咱們打包的主要文件類型,因此固然先從一個js文件開始,咱們這裏選擇打包一個沒有requireimport其餘文件的js,由於若是有依賴其餘文件分析起來太複雜,咱們先從簡單的js開始分析:webpack

  • 準備一個js文件:
// .src/empty.js
console.log('hello webpack')
複製代碼
  • 接着修改配置文件中的entry:
// webpack.config.js
{ entry: './src/empty.js' }
複製代碼

配置解析

Webpack默認只能解析jsjsonwasm類型的文件,因此會有專門處理這三種類型的解析器Parser,其餘類型的文件須要經過loader轉換成js來兼容。在配置解析過程當中,會註冊js模塊處理插件JavascriptModulesPlugin,它的做用就是註冊js解析器Parser及js輸出模版生成器JavascriptGeneratores6

解析js的Parser就叫Parser,解析json的Parser叫JsonParserweb

// JavascriptModulesPlugin.js
(compilation, { normalModuleFactory }) => {
    // 對 javascript/auto 類型的文件使用 Parser 解析
	normalModuleFactory.hooks.createParser.for("javascript/auto")
		.tap("JavascriptModulesPlugin", options => {
			return new Parser(options, "auto");
        });
    // ...
}
複製代碼

建立模塊

生成入口依賴的過程咱們在前面的入口Entry章節有分析過,因爲咱們這裏使用了單入口配置./src/empty.js,因此會生成單入口依賴SingleEntryDependency。這個地方比較有意思的是Webpack並無直接生成一個模塊,而是先生成了一個依賴,接着再使用NormalModuleFactory解析依賴,並生成模塊NormalModule:json

// NormalModuleFactory.js
class NormalModuleFactory {
    create(context, dependencie) {
        // 資源路徑,若是是入口依賴則這個就是入口文件路徑
        const request = dependencie.request
        // 資源類型
        const type = this.getType(request)
        return new NormalModule({
            // 建立解析該模塊須要的loader
            loaders: this.createLoaders(request)
            // 獲取模塊解析器
            parser: this.getParser(type)
            // 獲取模版生成器
            generator: this.getGenerator(type)
        });
    }
}
複製代碼

這裏有個比較重要的變量type,它用來標識這個模塊用什麼樣的方式解析,上面說了Webpack默認只能解析jsjsonwasm類型的文件,因此type也只有這三種,分別對應三種不一樣的Parser,默認值爲javascript/auto即默認使用處理js的Parser。Webpack內部添加了默認規則,這個規則會和外部配置的rules合併。ide

// WebpackOptionsDefaulter.js
class WebpackOptionsDefaulter {
	constructor() {
		this.set("module.defaultRules", "make", options => [
            // 默認文件使用類型javascript/auto"
            { type: "javascript/auto", resolve: {} },
            // 若是遇到.json文件,用json類型
			{ test: /\.json$/i, type: "json" },
		]);
    }
}
複製代碼

那難道解析圖片文件也用javascript/auto類型解析器嗎?沒錯,待會下面咱們會說到。這裏解析器Parserloader可不是同一個東西,你們須要區分開,解析器是用來分析文件依賴,如分析require等;loader是用來作內容轉換,如es6es5等。通過這一步後,入口文件就轉換成一個模塊了。函數

構建模塊

模塊建立完後,此時只是獲取了模塊的基本信息,如相對路徑,文件類型,須要用哪些loader處理這個文件等。接下來就要調用模塊的構建函數,用於真正加載並處理文件。post

// Compilation.js
buildModule(module) {
    module.build()
}
複製代碼

首先會使用loader加載文件並轉換內容,這裏會經過使用不一樣loader來轉換資源文件,js文件內容能夠直接傳給下一步,非js文件通過loader後通常會輸出一段js字符串內容,後面會舉例演示。本例通過loader處理後獲得的結果內容就是原文件內容console.log('hello webpack')

接着使用parser解析上面的結果,解析js的parser作的事情主要是將內容轉換成ast,並解析出文件全部的依賴,因爲咱們解析的js很是簡單,在構建時並不會有什麼依賴輸出,因此咱們先暫且忽略這裏。

// NormalLoader.js
class NormalLoader {
    build() {
        // 執行loader
        const result = runLoaders(this.resource, this.loaders)
        this.source = result
        // 解析輸出內容
        this.parser.parse(this.source)
    }
}
複製代碼

模塊依賴處理

通過模塊構建階段後,NormalModule就已經獲取到完整的內容,而且獲取到了它的全部依賴。接下來就是要處理這些依賴。在這裏會過濾掉不須要遞歸解析的依賴,因爲Webpack中依賴的表明的內容比較多,一般來講只有使用requireimport模塊依賴纔會須要被遞歸解析,即遞歸生成相應的資源模塊,其餘依賴不須要生成新的模塊就不會在這裏處理。因爲本例子沒有生成依賴,因此這裏也不會執行:

// Compilation.js
// 用於獲取須要被遞歸解析的依賴
processModuleDependencies(module) {
    const dependencies = []
	module.dependencies.map(dep => {
        // 有resourceIdent的纔是模塊依賴,須要生成新的模塊
        const resourceIdent = dep.getResourceIdentifier();
        if (resourceIdent) {
            dependencies.push(dep);
        }
    })
    this.addModuleDependencies(module, dependencies);
}
// 遞歸解析模塊依賴
addModuleDependencies(module, dependencies) {
    dependencies.map(dep => {
        // 建立模塊 -> 構建模塊 ...
        const factory = dep.factory
        const dependentModule = factory.create()
        this.buildModule(dependentModule)
    })
}
複製代碼

輸出文件

在全部模塊解析完成後,模塊構建階段make就完成了,接下來進入seal階段,即把全部模塊構建出輸出內容,主要負責生成輸出內容的函數是Compilation.createChunkAssets,可是不是這裏的重點,咱們在輸出文件章節中再詳細分析。最終咱們會看到Webpack給咱們生成了下面的js:

(function(modules){
    // 啓動函數...
})({  "./src/empty.js":
    /*!******************************!*\ !*** (webpack)/src/empty.js ***! \******************************/
    /*! no static exports found */
    (function(module, exports) {
        console.log('hello webpack')
    })
})
複製代碼

打包一個json文件

Webpack除了解析js文件外,同時內置支持解析json文件,咱們一樣能夠解析一個json看看它的工做流程。

準備工做

  • 咱們準備一個簡單的json文件,將用Webpack來打包這個json文件:
// a.json
{
    "say hello": "webpack",
    "foo": ["bar"]
}
複製代碼
  • 接着修改配置文件中的entry:
// webpack.config.js
entry: './src/a.json'
複製代碼

流程分析

  • JsonModulesPlugin註冊json解析器JsonParser及json輸出模版生成器JsonGenerator,webpack能直接解析json文件就是這個解析器的功勞:
// JsonModulesPlugin.js
(compilation, { normalModuleFactory }) => {
	normalModuleFactory.hooks.createParser.for("json")
		.tap("JsonModulesPlugin", () => new JsonParser() });
	normalModuleFactory.hooks.createGenerator.for("json")
		.tap("JsonModulesPlugin", () => new JsonGenerator() });
}
複製代碼
  • 建立單入口依賴SingleEntryDependency,經過NormalModuleFactory解析後,生成NormalModule,這一步跟解析js文件同樣。
  • 構建模塊module.build,經過loader獲取到json文件內容。
  • 因爲類型是json,因此使用JsonParser解析內容,將json文件轉爲對象,給module添加依賴JsonExportsDependency依賴,用於在輸出文件時標記導出內容,做用不大能夠先忽略。
class JsonParser {
	parse(source, state) {
        // 解析json爲對象
		const data = parseJson(source);
		state.module.buildInfo.jsonData = data;
        state.module.buildMeta.exportsType = "named";
        // 給json添加依賴,可是這個不是模塊依賴,只是給輸出作個標記,註釋掉也沒問題
		if (typeof data === "object" && data) {
			state.module.addDependency(new JsonExportsDependency(Object.keys(data)));
		}
		state.module.addDependency(new JsonExportsDependency(["default"]));
		return state;
	}
}
複製代碼
  • 處理模塊依賴processModuleDependencies,因爲JsonExportsDependency繼承於NullDependency,不屬於模塊依賴,因此這裏不須要繼續處理。
  • 渲染輸出內容,其中會調用JsonGenerator生成json輸出數據。

輸出

執行Webpack事後,咱們能夠看到輸出了下面的js文件:

(function(modules){
    // 啓動函數...
})({
    "./src/a.json": 
    /*!****************************!*\ !*** (webpack)/src/a.json ***! \****************************/
    /*! exports provided: say hello, foo, default */
    (function(module) {
        module.exports = JSON.parse("{\"say hello\":\"webpack\",\"foo\":[\"bar\"]}");
    })
})
複製代碼

能夠看到Webpack對json的解析和對js的解析過程大同小異。因爲解析json所用的JsonParse很是簡單,能夠說是js的簡化版本,之因此這麼簡單是由於通常json只是做爲描述內容,並不會依賴其餘文件,並且咱們代碼中引用json文件也只是獲取它的值,若是看js的Parser那就複雜多了。

打包一個圖片文件

以上在解析jsjson都是Webpack默認支持的,如今咱們看看默認不支持的文件類型是怎麼打包的。因爲webpack默認不識別圖片類型文件,因此咱們要加個能處理圖片的loader來處理。

準備工做

  • 安裝file-loader
  • 準備一個圖片a.png
  • 修改配置文件中的entry和rules:
// webpack.config.js
{
    entry: './src/a.png',
    module: {
        rules: [{
          test: /\.png$/,
          use: [{ loader: 'file-loader', options: { outputPath: '/', } }]
        }]
    }
}
複製代碼

流程分析

  • 入口文件生成NormalModule過程和上面同樣;
  • 構建模塊這一步和上面比較不一樣,原來的圖片會在loader執行中經過emitFile將文件輸出,而通過loader處理後返回的結果則是一段js字符串,因爲是js字符串因此固然能夠給解析js的Parser處理結果:
export default __webpack_public_path__ + "/2abc8e354a818e3316cc7c9dc97f881e.png";
複製代碼
  • 使用Parser解析上面的結果,因爲這段js還比較複雜,通過處理後一共添加了下面幾個依賴HarmonyCompatibilityDependencyHarmonyInitDependencyHarmonyExportHeaderDependencyHarmonyExportExpressionDependencyConstDependency 可是好在這些都不是模塊依賴,不須要遞歸進行處理,他們主要在輸出內容時用到,咱們能夠暫時忽略;
  • 渲染輸出內容

輸出

執行Webpack事後,會輸出一張圖片和下面的js文件:

(function(modules){
    // 啓動函數...
})({ "./src/a.png":
    /*!***************************!*\ !*** (webpack)/src/a.png ***! \***************************/
    /*! exports provided: default */
    (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony default export */ __webpack_exports__["default"] = (__webpack_require__.p + "/2abc8e354a818e3316cc7c9dc97f881e.png");
    })
})
複製代碼

總結

以上咱們分析的三個文件都是很是簡單的內容,基本流程就是如下幾點:

  • 解析配置
  • 建立入口模塊
  • 解析入口模塊
  • 輸出文件

固然Webpack對模塊的處理確定沒那麼簡單,文章反覆提到的模塊依賴還沒正式出場,解析js文件用的Parser也只是簡單提起。這裏咱們只須要對解析模塊流程有個大概印象,瞭解模塊在Webpack充當的角色就行,後面咱們將詳細分析其中的具體內容。

參考資料

入口entry

loader及優化

相關文章
相關標籤/搜索