Webpack源碼分析 - 目錄解析及優化

Webpack中目錄解析及優化

在Webpack中咱們能夠任意使用多種模塊化方式組織代碼:node

  • ES6: import xxx from 'xxx'
  • CommonJS: require('./xxx')
  • AMD...

咱們可使用它導入各類第三方庫及項目文件,也可使用別名來簡化路徑。咱們知道,雖然CommonJS是node原生支持的依賴管理方法,可是咱們打包項目時不是node來加載要打包的文件,並且查找規則也和node的不同,因此Webpack實現了本身的一套文件查找方法。webpack

依賴這套加載器,Webpack能夠實現各類模塊化規則解析,同時支持別名,目錄查找等定製化的需求。下面咱們來看看它的實現方式:git

初始化及使用Resolver

Webpack中使用enhanced-resolve包裝的ResolverFactory提供實例化方法,並根據參數添加特殊功能的插件,共建立了三種類型的resolver:github

  • normalResolver:提供文件路徑解析功能,用於普通文件導入
  • contextResolver:提供目錄路徑解析功能,用於動態文件導入
  • loaderResolver:提供文件路徑解析功能,用於loader文件導入

在建立模塊時,就會利用Resolver預先判斷路徑是否存在,並獲取路徑的完整地址供後續加載文件使用。web

ResolverFactory

ResolverFactory繼承了Tapable,共提供兩類鉤子,兩個都是個是在建立resolver時調用:json

class ResolverFactory extends Tapable {
	constructor() {
		super();
		this.hooks = {
			resolveOptions: new HookMap(() => new SyncWaterfallHook(["resolveOptions"])),
			resolver: new HookMap(() => new SyncHook(["resolver", "resolveOptions"]))
		};
    }
    get(type, resolveOptions) {
        // 調用配置鉤子
		resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);
		const resolver = Factory.createResolver(resolveOptions);
        // 調用新建鉤子
		this.hooks.resolver.for(type).call(resolver, resolveOptions);
		return resolver;
    }
複製代碼

默認建立參數

在未修改配置狀況下,Webpack給Resolver設定的默認值以下:瀏覽器

  • normalResolver
aliasFields: ["browser"]
cacheWithContext: false
extensions: [".wasm", ".mjs", ".js", ".json"]
fileSystem: CachedInputFileSystem 
mainFields: ["browser", "module", "main"]
mainFiles: ["index"]
modules: ["node_modules"]
unsafeCache:true
複製代碼
  • contextResolver
aliasFields: ["browser"]
cacheWithContext: false
extensions: [".wasm", ".mjs", ".js", ".json"]
fileSystem: CachedInputFileSystem 
mainFields: ["browser", "module", "main"]
mainFiles: ["index"]
modules: ["node_modules"]
unsafeCache:true
resolveToContext:true
複製代碼
  • loaderResolver
cacheWithContext: false
extensions: [".js", ".json"]
fileSystem: CachedInputFileSystem
mainFields: ["loader", "main"]
mainFiles: ["index"]
unsafeCache: true
複製代碼

WebpackOptionsApply

WebpackOptionsApply中初始化配置時,會在hook.resolveOptions上添加額外配置緩存

// normalResolver, loaderResolver
{ fileSystem: compiler.inputFileSystem }
// contextResolver
{ fileSystem: compiler.inputFileSystem, resolveToContext: true }
複製代碼

NodeSourcePlugin

用於對Node.js核心庫polyfill,當Webpack配置targetweb,webworker時啓用。例如os是node的核心模塊,在瀏覽器裏是沒有的,可是用webpack打包後會加上polyfill,打包後能在瀏覽器中運行:服務器

const os = require('os')
console.log(os)
複製代碼

在初始化時,會向hooks.resolver添加AliasPlugin插件,將須要polyfill的庫名添加到別名中。babel

AMDPlugin

用於處理AMD模塊化相關代碼,其中會向hooks.resolver添加AliasPlugin插件。在啓用AMD模塊化時,define做爲AMD關鍵字不容許被間接使用,例以下面代碼在打包過程當中,define會轉換爲webpack amd define當成一個模塊被引入,插件在遇到這個模塊時會返回一個異常的函數:

const df = define
console.log(df)
複製代碼

打包後生成的代碼:

"./buildin/amd-define.js": (function(module, exports) {
    module.exports = function() {
        throw new Error("define cannot be used indirect");
    };
}),
"./src/example.js": (function(module, exports, __webpack_require__) {
    const df = __webpack_require__("./buildin/amd-define.js")
    console.warn(__webpack_require__("./buildin/amd-define.js"))
})
複製代碼

調用位置

NormalModuleFactory用於建立普通模塊對象,在create方法中會調用建立好的normalResolver檢查將要建立的模塊是否存在,另外會使用loaderResolver檢查使用的loader是否存在。

ContextModuleFactory用於建立普通模塊對象,在create方法中會調用建立好的contextResolver檢查將要建立的目錄是否存在。

優化

若是對enhanced-resolve原理不瞭解,能夠先看這篇文章

首先準備兩個文件,並添加babel-loader:

// example.js
const inc = require('./increment');

// increment.js
console.log('1+1');

// webpack.config.js
module.exports = {
    mode: 'development',
    entry: "./src/example.js",
    module: {
      rules: [{
        test: /\.js$/,
        exclude: /(node_modules)/,
        use: {
          loader: 'babel-loader',
          options: { presets: ['@babel/preset-env'] }
        }
      }]
    }
}
複製代碼

運行打包後,能夠看到resolver相關的輸出日誌:

// resolve './src/example.js' in '/Users/kukiiu/Desktop/study/webpack'
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: .)
// Field 'browser' doesn't contain a valid alias configuration
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src/example.js)
// no extension
// Field 'browser' doesn't contain a valid alias configuration
// existing file: /Users/kukiiu/Desktop/study/webpack/src/example.js
// reporting result /Users/kukiiu/Desktop/study/webpack/src/example.js
// resolve 'babel-loader' in '/Users/kukiiu/Desktop/study/webpack'
// Parsed request is a module
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: .)
// resolve as module
// /Users/kukiiu/Desktop/study/node_modules doesn't exist or is not a directory
// /Users/kukiiu/Desktop/node_modules doesn't exist or is not a directory
// /Users/node_modules doesn't exist or is not a directory
// /node_modules doesn't exist or is not a directory
// looking for modules in /Users/kukiiu/node_modules
// using description file: /Users/kukiiu/package.json (relative path: ./node_modules)
// using description file: /Users/kukiiu/package.json (relative path: ./node_modules/babel-loader)
// no extension
// /Users/kukiiu/node_modules/babel-loader doesn't exist
// looking for modules in /Users/kukiiu/Desktop/study/webpack/node_modules
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./node_modules)
// using description file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/package.json (relative path: .)
// no extension
// /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader is not a file
// .js
// /Users/kukiiu/node_modules/babel-loader.js doesn't exist
// .js
// /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader.js doesn't exist
// .json
// /Users/kukiiu/node_modules/babel-loader.json doesn't exist
// as directory
// /Users/kukiiu/node_modules/babel-loader doesn't exist
// .json
// /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader.json doesn't exist
// as directory
// existing directory
// use ./lib/index.js from main in package.json
// using description file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/package.json (relative path: .)
// using description file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/package.json (relative path: ./lib/index.js)
// no extension
// existing file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/lib/index.js
// reporting result /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/lib/index.js
// resolve './increment' in '/Users/kukiiu/Desktop/study/webpack/src'
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src)
// Field 'browser' doesn't contain a valid alias configuration
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src/increment)
// no extension
// Field 'browser' doesn't contain a valid alias configuration
// /Users/kukiiu/Desktop/study/webpack/src/increment doesn't exist
// .wasm
// Field 'browser' doesn't contain a valid alias configuration
// /Users/kukiiu/Desktop/study/webpack/src/increment.wasm doesn't exist
// .mjs
// Field 'browser' doesn't contain a valid alias configuration
// /Users/kukiiu/Desktop/study/webpack/src/increment.mjs doesn't exist
// .js
// Field 'browser' doesn't contain a valid alias configuration
// existing file: /Users/kukiiu/Desktop/study/webpack/src/increment.js
// reporting result /Users/kukiiu/Desktop/study/webpack/src/increment.js
複製代碼

能夠看到,resovler作了不少重複或沒用的查詢,主要分爲3個文件的查詢example.js, babel-loader, ./increment,下面咱們看看經常使用的幾個優化方法是怎麼提高查找速度的

使用別名

由於有了緩存機制,別名能夠在不一樣文件使引入路徑不變,發揮緩存做用。可是單獨使用時由於多了個別名查找,會多一層替換步驟反而慢一點。

resolve: {
    alias: {
        ROOT: path.resolve('src/'),
    },
},
複製代碼

使用擴展名

引入文件時使用擴展名能夠最快速度找到文件,不須要經過遍歷擴展名來判段文件是否存在。

減小擴展名範圍

resolve.extensions能夠控制默認須要搜索的擴展,默認是[".wasm", ".mjs", ".js", ".json"],能夠根據項目狀況來設置,最經常使用的放第一位,一般只設置['.js']就好。

減小描述文件查找範圍

resolve.modulesresolveLoader.modules的默認值爲[node_modules],指在查找第三方模塊目錄或項目根目錄,而模塊是否找到是以目錄下是否有描述文件爲斷定,若是在當前文件目錄下查找不到,就往上一級查找。通常來講咱們都會將node_modules放在項目根目錄下,因此只要將其指定爲絕對路徑就能減小查找次數。

resolve: {
    modules: [path.resolve("node_modules")],
},
resolveLoader: {
    modules: [path.resolve("node_modules")],
},
複製代碼

意義不大的優化

在使用第三方庫時,將庫名設置別名,能夠省去Webpack查找的過程,由於緩存的存在使其意義不大:

resolveLoader: {
    alias: {
    'babel-loader': path.resolve('node_modules/babel-loader/lib/index.js'),
    },
},
複製代碼

較爲危險的優化

有些庫爲了同時輸出web端代碼和服務器代碼,會在package.json描述哪一個文件是用於web端,哪一個用於服務端。一般來講,導出的文件通常以main字段來描述,可是若是main描述的文件被服務端文件佔用,就會使用browser來描述。在使用下面優化時要考慮第三方庫的兼容,不然打包出的代碼有可能不能在web端運行。

mainFields是在查找第三方模塊的入口文件使用,咱們可使用[main]來縮小搜索範圍。

aliasFields配置是爲了替換模塊裏的某些文件使用,默認配置了[browser],咱們能夠配置爲[]不進行查找

resolve: {
    aliasFields: [],
    mainFields: ['main'],
}
複製代碼

優化結果

通過一頓操做後,再次打包,輸出日誌以下,明顯少了不少步驟。掌握了原理咱們就能夠根據項目狀況來選擇優化方案。

// resolve './src/example.js' in '/Users/kukiiu/Desktop/study/webpack'
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: .)
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src/example.js)
// no extension
// existing file: /Users/kukiiu/Desktop/study/webpack/src/example.js
// reporting result /Users/kukiiu/Desktop/study/webpack/src/example.js
// resolve 'babel-loader' in '/Users/kukiiu/Desktop/study/webpack'
// Parsed request is a module
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: .)
// aliased with mapping 'babel-loader': '/Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/lib/index.js' to '/Users/kukiiu/Desktop/study/webpack/
// node_modules/babel-loader/lib/index.js'
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: .)
// using description file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/package.json (relative path: ./lib/index.js)
// no extension
// existing file: /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/lib/index.js
// reporting result /Users/kukiiu/Desktop/study/webpack/node_modules/babel-loader/lib/index.js
// resolve './increment.js' in '/Users/kukiiu/Desktop/study/webpack/src'
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src)
// using description file: /Users/kukiiu/Desktop/study/webpack/package.json (relative path: ./src/increment.js)
// no extension
// existing file: /Users/kukiiu/Desktop/study/webpack/src/increment.js
// reporting result /Users/kukiiu/Desktop/study/webpack/src/increment.js
複製代碼

參考資料

enhanced-resolve

node-libs-browser

相關文章
相關標籤/搜索