.38-浅析webpack源码之babel-loader转换js文件
Posted QH-Jimmy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了.38-浅析webpack源码之babel-loader转换js文件相关的知识,希望对你有一定的参考价值。
经过非常非常长无聊的流程,只是将获取到的module信息做了一些缓存,然后生成了loaderContext对象。
这里上个图整理一下这节的流程:
这一节来看webpack是如何将babel-loader与js文件结合的,首先总览一下runLoaders函数:
/* options => { resource: \'d:\\\\workspace\\\\doc\\\\input.js\', loaders: [ { loader: \'d:\\\\workspace\\\\node_modules\\\\babel-loader\\\\lib\\\\index.js\' } ], context: loaderContext, readResource: fs.readFile.bind(fs) } */ exports.runLoaders = function runLoaders(options, callback) { // read options var resource = options.resource || ""; var loaders = options.loaders || []; var loaderContext = options.context || {}; var readResource = options.readResource || readFile; // 简单讲就是获取入口文件的绝对路径、参数、目录 var splittedResource = resource && splitQuery(resource); var resourcePath = splittedResource ? splittedResource[0] : undefined; var resourceQuery = splittedResource ? splittedResource[1] : undefined; var contextDirectory = resourcePath ? dirname(resourcePath) : null; // execution state var requestCacheable = true; var fileDependencies = []; var contextDependencies = []; // prepare loader objects loaders = loaders.map(createLoaderObject); // 将属性都挂载到loaderContext上面 loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.async = null; loaderContext.callback = null; loaderContext.cacheable = function cacheable(flag) { if (flag === false) { requestCacheable = false; } }; loaderContext.dependency = loaderContext.addDependency = function addDependency(file) { fileDependencies.push(file); }; loaderContext.addContextDependency = function addContextDependency(context) { contextDependencies.push(context); }; loaderContext.getDependencies = function getDependencies() { return fileDependencies.slice(); }; loaderContext.getContextDependencies = function getContextDependencies() { return contextDependencies.slice(); }; loaderContext.clearDependencies = function clearDependencies() { fileDependencies.length = 0; contextDependencies.length = 0; requestCacheable = true; }; // 定义大量的特殊属性 Object.defineProperty(loaderContext, "resource", { enumerable: true, get: function() { if (loaderContext.resourcePath === undefined) return undefined; return loaderContext.resourcePath + loaderContext.resourceQuery; }, set: function(value) { var splittedResource = value && splitQuery(value); loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined; loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined; } }); // ...大量Object.defineProperty // finish loader context if (Object.preventExtensions) { Object.preventExtensions(loaderContext); } var processOptions = { resourceBuffer: null, readResource: readResource }; iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { if (err) { return callback(err, { cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); } callback(null, { result: result, resourceBuffer: processOptions.resourceBuffer, cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); }); };
传入的4个参数都很直白:
1、待处理文件绝对路径
2、文件后缀对应的loader入口文件绝对路径
3、对应的loaderContext对象
4、fs对象
前面所有的事都是为了生成前3个属性,在这里整合在一起开始做转换处理。
createLoaderObject
这里有一个需要简单看的地方,就是对loaders数组做了一封封装:
// prepare loader objects loaders = loaders.map(createLoaderObject);
简单看一下这个函数:
function createLoaderObject(loader) { var obj = { path: null, query: null, options: null, ident: null, normal: null, pitch: null, raw: null, data: null, pitchExecuted: false, normalExecuted: false }; // 定义request属性的get/set Object.defineProperty(obj, "request", { enumerable: true, get: function() { return obj.path + obj.query; }, set: function(value) { if (typeof value === "string") { var splittedRequest = splitQuery(value); obj.path = splittedRequest[0]; obj.query = splittedRequest[1]; obj.options = undefined; obj.ident = undefined; } else { // value => { loader: \'d:\\\\workspace\\\\node_modules\\\\babel-loader\\\\lib\\\\index.js\' } if (!value.loader) throw new Error("request should be a string or object with loader and object (" + JSON.stringify(value) + ")"); // 这么多行代码其实只有第一行有用 // 即obj.path = \'d:\\\\workspace\\\\node_modules\\\\babel-loader\\\\lib\\\\index.js\' obj.path = value.loader; obj.options = value.options; obj.ident = value.ident; if (obj.options === null) obj.query = ""; else if (obj.options === undefined) obj.query = ""; else if (typeof obj.options === "string") obj.query = "?" + obj.options; else if (obj.ident) obj.query = "??" + obj.ident; else if (typeof obj.options === "object" && obj.options.ident) obj.query = "??" + obj.options.ident; else obj.query = "?" + JSON.stringify(obj.options); } } }); // 这里会触发上面的set obj.request = loader; // 封装 if (Object.preventExtensions) { Object.preventExtensions(obj); } return obj; }
最后做封装,然后返回一个obj。
将属性全部挂载在loaderContext上面,最后也是调用Object.preventExtensions将属性冻结,禁止添加任何新的属性。
完成对象的安装后,最后调用了迭代器方法,这里看一下iteratePitchingLoaders方法内部实现:
function iteratePitchingLoaders(options, loaderContext, callback) { // abort after last loader // loaderIndex初始为0 if (loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); // 取出之前的obj var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 默认是false 代表当前loader未被加载过 if (currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // load loader module loadLoader(currentLoaderObject, function(err) { // ... }); }
取出来loader对象后,调用loadLoader来加载loader,看一眼:
module.exports = function loadLoader(loader, callback) { // 不知道这个System是什么环境下的变量 // node环境是global // 浏览器环境是window if (typeof System === "object" && typeof System.import === "function") { // ... } else { try { // 直接尝试读取路径的文件 var module = require(loader.path); } catch (e) { // it is possible for node to choke on a require if the FD descriptor // limit has been reached. give it a chance to recover. // 因为可能出现阻塞情况 所以这里会进行重试 if (e instanceof Error && e.code === "EMFILE") { var retry = loadLoader.bind(null, loader, callback); if (typeof setImmediate === "function") { // node >= 0.9.0 return setImmediate(retry); } else { // node < 0.9.0 return process.nextTick(retry); } } return callback(e); } if (typeof loader !== "function" && typeof loader !== "object") throw new Error("Module \'" + loader.path + "\' is not a loader (export function or es6 module))"); // babel-loader返回的module是一个function loader.normal = typeof module === "function" ? module : module.default; loader.pitch = module.pitch; loader.raw = module.raw; if (typeof loader.normal !== "function" && typeof loader.pitch !== "function") throw new Error("Module \'" + loader.path + "\' is not a loader (must have normal or pitch function)"); callback(); } };
这里就涉及到loader的返回值,通过直接读取babel-loader的入口文件,最后返回了一个function,后面两个属性babel-loader并没有给,是undefined。
这里把babel-loader返回值挂载到loader上后,就调用了无参回调函数,如下:
loadLoader(currentLoaderObject, function(err) { if (err) return callback(err); // 刚才也说了这个是undefined var fn = currentLoaderObject.pitch; // 这个表明loader已经被调用了 下次再遇到就会直接跳过 currentLoaderObject.pitchExecuted = true; if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if (err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if (args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); });
这里把loader的一个标记置true,然后根据返回函数是否有pitch值来决定流程,很明显这里直接递归调用自身了。
第二次进来时,由于loader已经被加载,所以loaderIndex加1,然后再次递归。
第三次进来时,第一个判断中表明所有的loader都被加载完,会调用processResource方法。
processResource
这里的递归由于都是尾递归,所以在性能上不会有问题,直接看上面的方法:
// options => 包含fs方法的对象 // loaderContext => 包含loader路径、返回值等的对象 function processResource(options, loaderContext, callback) { // 从后往前调用loader loaderContext.loaderIndex = loaderContext.loaders.length - 1; // 获取入口文件路径 var resourcePath = loaderContext.resourcePath; if (resourcePath) { /* loaderContext.dependency = loaderContext.addDependency = function addDependency(file) { fileDependencies.push(file); }; */ loaderContext.addDependency(resourcePath); // readResource => fs.readFile options.readResource(resourcePath, function(err, buffer) { if (err) return callback(err); options.resourceBuffer = buffer; iterateNormalLoaders(options, loaderContext, [buffer], callback); }); } else { iterateNormalLoaders(options, loaderContext, [null], callback); } }
这个获取入口文件路径并调用fs模块进行文件内容读取,返回文件的原始buffer后调用了iterateNormalLoaders方法。
function iterateNormalLoaders(options, loaderContext, args, callback) { // 当所有loader执行完后返回 if (loaderContext.loaderIndex < 0) return callback(null, args); // 取出当前的loader var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 默认为false 跟另外一个标记类似 代表该loader在此方法是否被调用过 if (currentLoaderObject.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); } // 读取返回的module var fn = currentLoaderObject.normal; // 标记置true currentLoaderObject.normalExecuted = true; if (!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); } /* function convertArgs(args, raw) { if (!raw && Buffer.isBuffer(args[0])) args[0] = utf8BufferToString(args[0]); else if (raw && typeof args[0] === "string") args[0] = new Buffer(args[0], "utf-8"); } function utf8BufferToString(buf) { var str = buf.toString("utf-8"); if (str.charCodeAt(0) === 0xFEFF) { return str.substr(1); } else { return str; } } */ // 该方法将原始的buffer转换为utf-8的字符串 convertArgs(args, currentLoaderObject.raw); runSyncOrAsync(fn, loaderContext, args, function(err) { if (err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); iterateNormalLoaders(options, loaderContext, args, callback); }); }
这里的normal就是处理普通的js文件了,在读取入口文件后将其转换为utf-8的格式,然后依次获取loader,调用runSyncOrAsync。
源码如下:
/* fn => 读取babel-loader返回的函数 context => loader的辅助对象 args => 读取入口文件返回的字符串 */ function runSyncOrAsync(fn, context, args, callback) { var isSync = true; var isDone = false; var isError = false; // internal error var reportedError = false; context.async = function async() { if (isDone) { if (reportedError) return; // ignore throw new Error("async(): The callback was already called."); } isSync = false; return innerCallback; }; // 封装成执行一次的回调函数 var innerCallback = context.callback = function() { if (isDone) { if (reportedError) return; // ignore throw new Error("callback(): The callback was already called."); } isDone = true; isSync = false; try { callback.apply(null, arguments); } catch (e) { isError = true; throw e; } }; try { // 可以可以 // 老子看了这么久源码就是等这个方法 // 还装模作样的弄个IIFE var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }()); if (isSync) { isDone = true; if (result === undefined) return callback(); // 根据转换后的类型二次处理 if (result && typeof result === "object" && typeof result.then === "function") { return result.catch(callback).then(function(r) { callback(null, r); }); } return callback(null, result); } } catch (e) { if (isError) throw e; if (isDone) { // loader is already "done", so we cannot use the callback function // for better debugging we print the error on the console if (typeof e === "object" && e.stack) console.error(e.stack); else console.error(e); return; } isDone = true; reportedError = true; callback(e); } }
看了那么多的垃圾代码,终于来到了最关键的方法,可以看出,本质上loader就是将读取到的字符串传入,然后返回对应的字符串或者一个Promise。
这里一路将结果一路返回到了最初的runLoaders方法中:
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { if (err) { return callback(err, { cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); } /* result => babel-loader转换后的字符串 resourceBuffer => JS文件的原始buffer cacheable => [Function] fileDependencies => [\'d:\\\\workspace\\\\doc\\\\input.js\'] contextDependencies => [] */ callback(null, { result: result, resourceBuffer: processOptions.resourceBuffer, cacheable: requestCacheable, fileDependencies: fileDependencies, contextDependencies: contextDependencies }); });
因为案例比较简单,所以返回的东西也比较少,这里继续callback,返回到doBuild:
doBuild(options, compilation, resolver, fs, callback) { this.cacheable = false; const loaderContext = this.createLoaderContext(resolver, options, compilation, fs); runLoaders({ resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // result => 上面的对象 if (result) { this.cacheable = result.cacheable; this.fileDependencies = result.fileDependencies; this.contextDependencies = result.contextDependencies; } if (err) { const error = new ModuleBuildError(this, err); return callback(error); } // 获取对应的原始buffer、转换后的字符串、sourceMap const resourceBuffer = result.resourceBuffer; const source = result.result[0]; // null const sourceMap = result.result[1]; if (!Buffer.isBuffer(source) && typeof source !== "string") { const error = new ModuleBuildError(this, new Error("Final loader didn\'t return a Buffer or String")); return callback(error); } /* function asString(buf) { if (Buffer.isBuffer(buf)) { return buf.toString("utf-8"); } return buf; } */ this._source = this.createSource(asString(source), resourceBuffer, sourceMap); return callback(); }); }
这次获取处理完的对象属性,然后调用另外一个createSource方法:
createSource(source, resourceBuffer, sourceMap) { // if there is no identifier return raw source if (!this.identifier) { return new RawSource(source); } // from here on we assume we have an identifier // 返回下面这个东西 很久之前拼接的 // d:\\workspace\\node_modules\\babel-loader\\lib\\index.js!d:\\workspace\\doc\\input.js const identifier = this.identifier(); // 下面两个属性根本没出现过 if (this.lineToLine && resourceBuffer) { return new LineToLineMappedSource( source, identifier, asString(resourceBuffer)); } if (this.useSourceMap && sourceMap) { return new SourceMapSource(source, identifier, sourceMap); } // 直接进这里 /* class OriginalSource extends Source { constructor(value, name) { super(); this._value = value; this._name = name; } //...原型方法 } */ return new OriginalSource(source, identifier); }
因为都比较简单,所以直接看注释就好了,没啥好解释的。
所有的new都只看看构造函数,方法那么多,又不是全用。
返回的对象赋值给了NormalModule对象的_source属性,然后又是callback,这次回到了build那里:
build(options, compilation, resolver, fs, callback) { this.buildTimestamp = Date.now(); this.built = true; this._source = null; this.error = null; this.errors.length = 0; this.warnings.length = 0; this.meta = {}; return this.doBuild(options, compilation, resolver, fs, (err) => { this.dependencies.length = 0; this.variables.length = 0; this.blocks.length = 0; this._cachedSource = null; // if we have an error mark module as failed and exit if (err) { this.markModuleAsErrored(err); return callback(); } // check if this module should !not! be parsed. // if so, exit here; // undefined跳过 const noParseRule = options.module && options.module.noParse; if (this.shouldPreventParsing(noParseRule, this.request)) { return callback(); } try { this.parser.parse(this._source.source(), { current: this, module: this, compilation: compilation, options: options }); } catch (e) { const source = this._source.source(); const error = new ModuleParseError(this, source, e); this.markModuleAsErrored(error); return callback(); } return callback(); }); }
基本上不知道module.noParser选项哪个人会用,所以这里一般都是直接跳过然后调用那个可怕对象parser对象的parse方法,开始进行解析。
这节的内容就这样吧,总算是把loader跑完了,这个系列的目的也就差不多了。
其实总体来说过程就几步,但是代码的复杂程度真的是不想说了……
以上是关于.38-浅析webpack源码之babel-loader转换js文件的主要内容,如果未能解决你的问题,请参考以下文章
.6-浅析webpack源码之validateSchema模块
.7-浅析webpack源码之WebpackOptionsDefaulter模块