.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文件的更多相关文章
- .30-浅析webpack源码之doResolve事件流(1)
这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...
- .17-浅析webpack源码之compile流程-入口函数run
本节流程如图: 现在正式进入打包流程,起步方法为run: Compiler.prototype.run = (callback) => { const startTime = Date.now( ...
- .30-浅析webpack源码之doResolve事件流(2)
这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...
- .3-浅析webpack源码之预编译总览
写在前面: 本来一开始想沿用之前vue源码的标题:webpack源码之***,但是这个工具比较巨大,所以为防止有人觉得我装逼跑来喷我(或者随时鸽),加上浅析二字,以示怂. 既然是浅析,那么案例就不必太 ...
- 浅析libuv源码-node事件轮询解析(3)
好像博客有观众,那每一篇都画个图吧! 本节简图如下. 上一篇其实啥也没讲,不过node本身就是这么复杂,走流程就要走全套.就像曾经看webpack源码,读了300行代码最后就为了取package.js ...
- 从Webpack源码探究打包流程,萌新也能看懂~
简介 上一篇讲述了如何理解tapable这个钩子机制,因为这个是webpack程序的灵魂.虽然钩子机制很灵活,而然却变成了我们读懂webpack道路上的阻碍.每当webpack运行起来的时候,我的心态 ...
- .34-浅析webpack源码之事件流make(3)
新年好呀~过个年光打游戏,function都写不顺溜了. 上一节的代码到这里了: // NormalModuleFactory的resolver事件流 this.plugin("resolv ...
- Python的开源人脸识别库:离线识别率高达99.38%(附源码)
Python的开源人脸识别库:离线识别率高达99.38%(附源码) 转https://cloud.tencent.com/developer/article/1359073 11.11 智慧上云 ...
- webpack源码-依赖收集
webpack源码-依赖收集 version:3.12.0 程序主要流程: 触发make钩子 Compilation.js 执行EntryOptionPlugin 中注册的make钩子 执行compi ...
随机推荐
- Python自动化开发 - Django【基础篇】
Python的WEB框架有Django.Tornado.Flask 等多种,Django相较与其他WEB框架其优势为: 大而全,框架本身集成了ORM.模型绑定.模板引擎.缓存.Session等诸多功能 ...
- java的环境变量
Java学习第一步——JDK安装及Java环境变量配置 2014-05-30 9:09 Java SE 阿超 9226 views Java作为当下很主流的编程语言,学习Java的朋友也越来越 ...
- Python 中的深拷贝和浅拷贝
一.浅拷贝python中 对象赋值时 默认是浅拷贝,满足如下规律:1. 对于 不可变对象(字符串,元组 等),赋值 实际上是创建一个新的对象:例如: >>> person=['nam ...
- JS学习笔记6_事件
1.事件冒泡 由内而外的事件传播(从屏幕里飞出来一支箭的感觉) 2.事件捕获 由表及里的事件传播(力透纸背的感觉) 3.DOM事件流(DOM2级) 事件捕获阶段 -> 处于目标阶段 -> ...
- 输出的数据格式是如何决定的-------Asp.net WebAPI学习笔记(二)
在上一篇文章<路由其实也可以很简单>,我们解决了路由问题,这篇文章,我们来研究剩下的另一个问题,为何我们的方法返回的是一个列表,输出到客户端的时候,变成json呢,大家应该还记得我们上一篇 ...
- 利用App漏洞获利2800多万元,企业该如何避免类似事件?
上个月,上海警方抓捕了一个利用网上银行漏洞非法获利的犯罪团伙,该团伙利用银行App漏洞非法获利2800多万元. 据悉,该团伙使用技术软件成倍放大定期存单金额,从而非法获利.理财邦的一篇文章分析了犯罪嫌 ...
- linux03
linux day31.正则表达式 \* ------重复0无数次 \+ ------重复1 无数次 ^ -------开头 $ -------结尾 | ------或 & ----- 与 ( ...
- K8s之spinnaker
一.spinnaker概述 1.spinnaker是netflix开源的一款云发布CI/CD,其前身是Asgard,spinnaker支持各种部署目标,包括OpenStack.DC/OS.Kubern ...
- 安卓Dialog对话框多次显示而闪退的解决办法
事情是这样子的,我在一个活动中自定义了一个AlertDialog,通过一个按钮点击即可弹出,而后来出现的情况是,第一次点击就没问题, 正常跳出,而第二次就直接程序闪退,然后报The specified ...
- [Umbraco] 入门教程(转)
如在页面上显示Helloword. 设计:在umbraco里,最基础的一个概念是文档类型(document type),每个文档其实可以看成一个页面类型.比如我们要创建的两个页面,每个页面都需要显示自 ...