新年好呀~過個年光打游戲,function都寫不順溜了。 上一節的代碼到這裡了: 經過長長的resolve,最終也只是解析入口文件的合法路徑信息,然後調用回調函數。 接下來分析回調函數是如何處理返回結果的: 返回的結果有兩部分,一個是loader,一個是文件對應路徑。 對於入口文件的當前解析,不存在 ...
新年好呀~過個年光打游戲,function都寫不順溜了。
上一節的代碼到這裡了:
// NormalModuleFactory的resolver事件流 this.plugin("resolver", () => (data, callback) => { // ... asyncLib.parallel( /*...*/ , /* results: [ [], { resourceResolveData: { context: { issuer: '', compiler: undefined }, path: 'd:\\workspace\\doc\\input.js', request: undefined, query: '', module: false, file: false, descriptionFilePath: 'd:\\workspace\\doc\\package.json', descriptionFileData: [Object], descriptionFileRoot: 'd:\\workspace\\doc', relativePath: './input.js', __innerRequest_request: undefined, __innerRequest_relativePath: './input.js', __innerRequest: './input.js' }, resource: 'd:\\workspace\\doc\\input.js' } ] */ (err, results) => { // ...超多內容 }); });
經過長長的resolve,最終也只是解析入口文件的合法路徑信息,然後調用回調函數。
接下來分析回調函數是如何處理返回結果的:
// NormalModuleFactory的resolver事件流 this.plugin("resolver", () => (data, callback) => { // ... asyncLib.parallel( /*...*/ , (err, results) => { if (err) return callback(err); // 暫時不存在loaders let loaders = results[0]; const resourceResolveData = results[1].resourceResolveData; resource = results[1].resource; // 跳過下麵幾部分內容 // translate option idents try { loaders.forEach(item => { if (typeof item.options === "string" && /^\?/.test(item.options)) { item.options = this.ruleSet.findOptionsByIdent(item.options.substr(1)); } }); } catch (e) { return callback(e); } if (resource === false) { // ignored return callback(null, new RawModule( "/* (ignored) */", `ignored ${context} ${request}`, `${request} (ignored)` ) ); } const userRequest = loaders.map(loaderToIdent).concat([resource]).join("!"); // 嘗試獲取路徑參數 let resourcePath = resource; let resourceQuery = ""; const queryIndex = resourcePath.indexOf("?"); if (queryIndex >= 0) { resourceQuery = resourcePath.substr(queryIndex); resourcePath = resourcePath.substr(0, queryIndex); } // 很久之前的東西 const result = this.ruleSet.exec({ resource: resourcePath, resourceQuery, issuer: contextInfo.issuer, compiler: contextInfo.compiler }); // ... }); });
返回的結果有兩部分,一個是loader,一個是文件對應路徑。
對於入口文件的當前解析,不存在loader,所以會直接跳過開始的幾部分內容,直接進入後面的ruleSet方法處理。
ruleSet.exec
這個ruleSet是個很久遠的東西了,在18-19節有講,主要是對配置文件中的modules.rules進行二次處理,包裝在一個對象中。
這裡的exec方法主要是判斷路徑信息是否符合配置文件中定義的rules並解析返回一個result,方法如下(原型方法改成箭頭函數好看一點):
exec = (data) => { const result = []; this._run(data, { rules: this.rules }, result); return result; }
真正的判斷方法是_run,其中data為傳進來的判斷對象,rules為判斷標準,result是返回的結果。
簡答過一下內容:
/* module.rules => [ { test: /\.vue$/, loader: 'vue-loader', }, { test: /\.css$/, loader: 'css!style-loader' }, { test: /\.js$/, loader: 'babel-loader' }, ] data => { resource: 'd:\\workspace\\doc\\input.js', resourceQuery: '', issuer: '', compiler: undefined } rule => { rules: [ { resource: [Function: bound test], use: [Array] }, { resource: [Function: bound test], use: [Array] }, { resource: [Function: bound test], use: [Array] } ] } */ _run = (data, rule, result) => { // 判斷特殊鍵提前返回 // test conditions if (rule.resource && !data.resource) return false; if (rule.resourceQuery && !data.resourceQuery) return false; if (rule.compiler && !data.compiler) return false; if (rule.issuer && !data.issuer) return false; if (rule.resource && !rule.resource(data.resource)) return false; if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false; if (data.resourceQuery && rule.resourceQuery && !rule.resourceQuery(data.resourceQuery)) return false; if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) return false; // ['rules'] => [] // apply const keys = Object.keys(rule).filter((key) => { return ["resource", "resourceQuery", "compiler", "issuer", "rules", "oneOf", "use", "enforce"].indexOf(key) < 0; }); keys.forEach((key) => { result.push({ type: key, value: rule[key] }); }); // 依次進入 if (rule.use) { rule.use.forEach((use) => { result.push({ type: "use", value: typeof use === "function" ? RuleSet.normalizeUseItemFunction(use, data) : use, enforce: rule.enforce }); }); } // 遍歷3個判斷標準 if (rule.rules) { for (let i = 0; i < rule.rules.length; i++) { this._run(data, rule.rules[i], result); } } // 跳過 if (rule.oneOf) { for (let i = 0; i < rule.oneOf.length; i++) { if (this._run(data, rule.oneOf[i], result)) break; } } return true; }
這裡會跳過部分代碼,在配置文件的rules中我寫了3個簡單的loader,分別對應js、css、vue尾碼的文件,入口文件為input.js,所以匹配到了babel-loader。
獲取到了對應的loader,繼續跑流程:
// NormalModuleFactory的resolver事件流 this.plugin("resolver", () => (data, callback) => { // ... asyncLib.parallel( /*...*/ , (err, results) => { // ... /* result => [ { type: 'use', value: { loader: 'babel-loader' }, enforce: undefined } ] */ const result = this.ruleSet.exec({ resource: resourcePath, resourceQuery, issuer: contextInfo.issuer, compiler: contextInfo.compiler }); const settings = {}; const useLoadersPost = []; const useLoaders = []; const useLoadersPre = []; result.forEach(r => { if (r.type === "use") { // enforce代表loader的特殊標記 if (r.enforce === "post" && !noPostAutoLoaders && !noPrePostAutoLoaders) useLoadersPost.push(r.value); else if (r.enforce === "pre" && !noPrePostAutoLoaders) useLoadersPre.push(r.value); // 走這條分支 else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) useLoaders.push(r.value); } else { settings[r.type] = r.value; } }); // 又是parallel asyncLib.parallel([ this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, this.resolvers.loader), this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, this.resolvers.loader), this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, this.resolvers.loader) ], (err, results) => { if (err) return callback(err); loaders = results[0].concat(loaders, results[1], results[2]); process.nextTick(() => { callback(null, { context: context, request: loaders.map(loaderToIdent).concat([resource]).join("!"), dependencies: data.dependencies, userRequest, rawRequest: request, loaders, resource, resourceResolveData, parser: this.getParser(settings.parser) }); }); }); }); });
這裡判斷了loader是否存在特殊標記,然後將結果彈入對應的loader數組中。
最後再次調用了asyncLib的parallel方法,方法在上一個parallel調用過,但是當初沒有loader,這次有了。
看起來就比較複雜,下一節再過。
把resolveRequestArray過完,方法源碼與參數如下:
/* contextInfo => { issuer: '', compiler: undefined } context => D:\workspace\doc array => [ { loader: 'babel-loader' } ] resolver => resolvers.loader callback => undefined */ resolveRequestArray = (contextInfo, context, array, resolver, callback) => { if (array.length === 0) return callback(null, []); asyncLib.map(array, (item, callback) => { // resolver.loader resolver.resolve(contextInfo, context, item.loader, (err, result) => { if (err && /^[^/]*$/.test(item.loader) && !/-loader$/.test(item.loader)) { return resolver.resolve(contextInfo, context, item.loader + "-loader", err2 => { if (!err2) { err.message = err.message + "\n" + "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" + ` You need to specify '${item.loader}-loader' instead of '${item.loader}',\n` + " see https://webpack.js.org/guides/migrating/#automatic-loader-module-name-extension-removed"; } callback(err); }); } if (err) return callback(err); const optionsOnly = item.options ? { options: item.options } : undefined; return callback(null, Object.assign({}, item, identToLoaderRequest(result), optionsOnly)); }); }, callback); }
這裡的array只有一個元素,map方法中調用了resolver的resolve方法,似曾相識,就跟之前那個resolve方法一樣,不過來源是resolvers.loader對象。
再回顧一下定義:
compiler.resolvers.normal = ResolverFactory.createResolver(Object.assign({ fileSystem: compiler.inputFileSystem }, options.resolve)); compiler.resolvers.context = ResolverFactory.createResolver(Object.assign({ fileSystem: compiler.inputFileSystem, resolveToContext: true }, options.resolve)); compiler.resolvers.loader = ResolverFactory.createResolver(Object.assign({ fileSystem: compiler.inputFileSystem }, options.resolveLoader));
可以看出,除去最後面那個options,調用的方法是一模一樣的,而options.resolve與options.resolveLoader在預設情況下如下所示:
{ "resolve": { "unsafeCache": true, "modules": ["node_modules"], "extensions": [".js", ".json"], "mainFiles": ["index"], "aliasFields": ["browser"], "mainFields": ["browser", "module", "main"], "cacheWithContext": false }, "resolveLoader": { "unsafeCache": true, "mainFields": ["loader", "main"], "extensions": [".js", ".json"], "mainFiles": ["index"], "cacheWithContext": false } }
只是少了modules、aliasFileds,其他都是一樣的,這兩個參數並不會影響如前doResolve幾節中所講的流程。
也就是說,這個方法相當於回到了第29節,從頭開始跑一遍所有的事件流,最後解析出對應的路徑。
這裡有一個不一樣的地方,這個babel-loader並不是普通的文件類型,所以在doResolver的事件串流中,會走模塊分支。
又臭又長的過程就先暫時跳過了,下節再講,最後返回babel-loader的入口文件路徑如下所示:
[ { loader: 'D:\\workspace\\node_modules\\babel-loader\\lib\\index.js' } ]
通過神奇的resolve方法找到了對應loader的入口文件,最後的代碼結果如下:
asyncLib.parallel([ this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, this.resolvers.loader), this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, this.resolvers.loader), this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, this.resolvers.loader) ], (err, results) => { if (err) return callback(err); loaders = results[0].concat(loaders, results[1], results[2]); process.nextTick(() => { callback(null, { // 'D:\\workspace\\doc' context: context, // 'D:\\workspace\\node_modules\\babel-loader\\lib\\index.js!D:\\workspace\\doc\\input.js' request: loaders.map(loaderToIdent).concat([resource]).join("!"), // ... dependencies: data.dependencies, // 'D:\\workspace\\doc\\input.js' userRequest, // './input.js' rawRequest: request, // [ { loader: 'D:\\workspace\\node_modules\\babel-loader\\lib\\index.js' } ] loaders, // 'D:\\workspace\\doc\\input.js' resource, // ... resourceResolveData, parser: this.getParser(settings.parser) }); }); });
其中最後的parser一會再說,先講講這個callback。
看webpack源碼最大的痛苦就是函數嵌套太深,每一個callback都是噩夢,所以這個callback我也是找了很久很久。
這個事件流的入口如下:
this.plugin("factory", () => (result, callback) => { let resolver = this.applyPluginsWaterfall0("resolver", null); // Ignored if (!resolver) return callback(); resolver(result, (err, data) => { /*...*/ }) })
這裡調用了tapable的方法返回了一個函數,然後再次調用該函數。
而這個事件流的主心骨就是兩個asyncLib.parallel,根本找不到哪裡返回了東西,直到我看到了事件流的plugin:
this.plugin("resolver", () => (data, callback) => { // ... })
沒錯,這裡有兩個箭頭函數,先返回一個函數,下麵的調用才是真正的執行。
那就很明顯了,process.nextTick是一個node內置的非同步方法,類似於vue的$nextTick,作用就不多說了。
callback對應的就是那個調用時的第二個參數,而最後返回的大對象就是data。
簡要看一下回調函數內容:
resolver(result, (err, data) => { if (err) return callback(err); // Ignored if (!data) return callback(); // direct module if (typeof data.source === "function") return callback(null, data); this.applyPluginsAsyncWaterfall("after-resolve", data, (err, result) => { // ... }); });
對返回的結果做了簡單的判斷,然後觸發了另外一個事件流。
下一節完善webpack是如何根據babel-loader搜索到對應的模塊入口文件的。