.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模块

.11-浅析webpack源码之Storage模块

.12-浅析webpack源码之NodeWatchFileSystem模块总览

.32-浅析webpack源码之doResolve事件流

.5-浅析webpack源码之入口函数