经过非常非常长无聊的流程,只是将获取到的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跑完了,这个系列的目的也就差不多了。

  其实总体来说过程就几步,但是代码的复杂程度真的是不想说了……

最新文章

  1. ahjesus使用T4模板自动维护实体
  2. HTML其他基本格式说明
  3. Web开发者的六个代码调试平台
  4. django模型
  5. windows下实现微秒级的延时
  6. hdoj 1950 Bridging signals【二分求最大上升子序列长度】【LIS】
  7. jQuery 表单验证插件——Validation(基础)
  8. JQuery 表单验证--jquery validation
  9. A WebBrowser Toy
  10. STM32F10X -- 模拟IIC程序
  11. SQL修改表字段,加附属属性
  12. 分别用Excel和python进行日期格式转换成时间戳格式
  13. ckeditor文本对齐方式添加,图片上传
  14. 实验五 CC2530平台上ADC组件的TinyOS编程
  15. [转]JAVA实现SFTP实例
  16. .Net转Java.02.数据类型
  17. Expert C Programming 阅读笔记(CH2)
  18. 【题解】SCOI2008配对
  19. cookie、session、localstorage
  20. 实现Windows Server 2003多用户远程登录(转载)

热门文章

  1. iOS开发 关于iBeacon的一些记录
  2. fping常用参数介绍
  3. NODE-WEBKIT教程(5)NATIVE UI API 之FRAMELESS WINDOW
  4. springBoot2 基础语法
  5. C++获取系统信息(IP地址、硬件信息等)
  6. SpringCloud服务注册与服务发现之Eureka
  7. underscore.js源码研究(7)
  8. spring mvc 使用kaptcha配置生成验证码实例
  9. 【小程序开放激励视频】--wepy小程序添加激励视频
  10. 不一样的日期、时间转换(moment.js)