.30-浅析webpack源码之doResolve事件流(2)

  这里所有的插件都对应着一个小功能,画个图整理下目前流程:

  上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下:

ParsePlugin.prototype.apply = function(resolver) {
    var target = this.target;
    resolver.plugin(this.source, function(request, callback) {
        // 分析request是否为模块或文件夹
        var parsed = resolver.parse(request.request);
        var obj = Object.assign({}, request, parsed);
        if (request.query && !parsed.query) {
            obj.query = request.query;
        }
        if (parsed && callback.log) {
            if (parsed.module)
                callback.log("Parsed request is a module");
            if (parsed.directory)
                callback.log("Parsed request is a directory");
        }
        // 拼接后的obj如下
        /*
            {
                context: { issuer: '', compiler: undefined },
                path: 'd:\\workspace\\doc',
                request: './input.js',
                query: '',
                module: false,
                directory: false,
                file: false
            }
        */
        // target => parsed-resolve
        resolver.doResolve(target, obj, null, callback);
    });
};

  该插件调用完后,进入下一个事件流,开始跑跑parsed-resolve相关的了。

  回头看了一眼28节的大流程图,发现基本上这些事件流都是串联起来挨个注入的,还好不用自己去找在哪了。

  

createInnerCallback

  这里先讲一下之前跳过的回调函数生成器,在Resolver中调用如下:

// before-callback
createInnerCallback(beforeInnerCallback, {
    log: callback.log,
    missing: callback.missing,
    stack: newStack
}, message && ("before " + message), true);
// normal-callback
createInnerCallback(innerCallback, {
    log: callback.log,
    missing: callback.missing,
    stack: newStack
}, message);
// after-callback
createInnerCallback(afterInnerCallback, {
    log: callback.log,
    missing: callback.missing,
    stack: newStack
}, message && ("after " + message), true);

  方法的第一个参数都大同小异,取第一个为例:

function beforeInnerCallback(err, result) {
    // 根据调用callback时是否有参数决定调用回调函数还是进入下一阶段
    if (arguments.length > 0) {
        if (err) return callback(err);
        if (result) return callback(null, result);
        return callback();
    }
    runNormal();
}

  剩下的两个也只是把runNormal变成了runAfter与callback而已。

  有了参数,接下来看一下生成器的内部实现:

module.exports = function createInnerCallback(callback, options, message, messageOptional) {
    var log = options.log;
    // 无log时
    if (!log) {
        // 基本上也是返回callback
        // 只是把options的两个方法挂载上去了
        if (options.stack !== callback.stack) {
            var callbackWrapper = function callbackWrapper() {
                return callback.apply(this, arguments);
            };
            callbackWrapper.stack = options.stack;
            callbackWrapper.missing = options.missing;
            return callbackWrapper;
        }
        return callback;
    }
    // 这个方法是批量取出本地log数组的内容然后调用options的log方法
    function loggingCallbackWrapper() {
        var i;
        if (message) {
            if (!messageOptional || theLog.length > 0) {
                log(message);
                for (i = 0; i < theLog.length; i++)
                    log("  " + theLog[i]);
            }
        } else {
            for (i = 0; i < theLog.length; i++)
                log(theLog[i]);
        }
        return callback.apply(this, arguments);

    }
    // 有log时
    var theLog = [];
    loggingCallbackWrapper.log = function writeLog(msg) {
        theLog.push(msg);
    };
    loggingCallbackWrapper.stack = options.stack;
    loggingCallbackWrapper.missing = options.missing;
    return loggingCallbackWrapper;
};

  这里的log大部分情况下都是undefined,所以暂时可以认为返回的基本上是第一个参数callback本身。

  有log时也不复杂,等传入的options自带有效log时再看。

 

DescriptionFilePlugin

  继续跑流程,这个插件就是对package.json配置文件进行解析,源码简化如下:

// request => 之前的obj
// callback => createInnerCallback(...)
(request, callback) => {
    const directory = request.path;
    /*
        resolver => 大对象
        directory => 'd:\\workspace\\doc'
        filenames => ['package.json']
    */
    DescriptionFileUtils.loadDescriptionFile(resolver, directory, filenames, ((err, result) => { /**/ }));
};

  这里直接在内部调用了另外一个工具类的实例方法,源码如下:

var forEachBail = require("./forEachBail");

function loadDescriptionFile(resolver, directory, filenames, callback) {
    (function findDescriptionFile() {
        forEachBail(filenames, function(filename, callback) { /**/ }, function(err, result) { /**/ });
    }());
}

 

forEachBail

  内部引用了一个工具方法做迭代,继续看:

// 参数名字说明一切
module.exports = function forEachBail(array, iterator, callback) {
    if (array.length === 0) return callback();
    var currentPos = array.length;
    var currentResult;
    var done = [];
    for (var i = 0; i < array.length; i++) {
        var itCb = createIteratorCallback(i);
        // 传入数组元素与生成的迭代器回调函数
        iterator(array[i], itCb);
        if (currentPos === 0) break;
    }

    function createIteratorCallback(i) {
        return function() {
            if (i >= currentPos) return; // ignore
            var args = Array.prototype.slice.call(arguments);
            done.push(i);
            if (args.length > 0) {
                currentPos = i + 1;
                done = done.filter(function(item) {
                    return item <= i;
                });
                // 将该回调的参数赋值到外部变量
                currentResult = args;
            }
            // 遍历完调用callback
            if (done.length === currentPos) {
                callback.apply(null, currentResult);
                currentPos = 0;
            }
        };
    }
};

  由于本例中array只有一个数组元素,所以这个看似复杂的函数也比较简单了,需要关注的只有一行代码:

iterator(array[i], itCb);

  第一个参数为package.json字符串,第二个为内部生成的一个回调,回到调用方法上,对应的iterator方法如下:

(filename, callback) => {
    // 路径拼接
    var descriptionFilePath = resolver.join(directory, filename);
    // 这个readJson我是翻回去找了老久
    // 来源于CachedInputFileSystem模块的191行
    /* 
        this._readJson = function(path, callback) {
            this.readFile(path, function(err, buffer) {
                if (err) return callback(err);
                try {
                    var data = JSON.parse(buffer.toString("utf-8"));
                } catch (e) {
                    return callback(e);
                }
                callback(null, data);
            });
        }.bind(this);
    */
    // 这两个方法根本没有什么卵区别
    if (resolver.fileSystem.readJson) {
        resolver.fileSystem.readJson(descriptionFilePath, function(err, content) {
            if (err) {
                if (typeof err.code !== "undefined") return callback();
                return onJson(err);
            }
            onJson(null, content);
        });
    } else {
        resolver.fileSystem.readFile(descriptionFilePath, function(err, content) {
            if (err) return callback();
            try {
                var json = JSON.parse(content);
            } catch (e) {
                onJson(e);
            }
            onJson(null, json);
        });
    }
    // 在不出错的情况下传入null与读取到的json字符串
    function onJson(err, content) {
        if (err) {
            if (callback.log)
                callback.log(descriptionFilePath + " (directory description file): " + err);
            else
                err.message = descriptionFilePath + " (directory description file): " + err;
            return callback(err);
        }
        callback(null, {
            content: content,
            directory: directory,
            path: descriptionFilePath
        });
    }
}

  这里首先进行路径拼接,然后调用readFile方法读取对应路径的package.json文件,如果没有出错,将读取到的字符串与路径包装成对象传入callback。

 

Resolver.prototype.join

  简单看一下路径的拼接函数。

var memoryFsJoin = require("memory-fs/lib/join");
var memoizedJoin = new Map();
// path => 目录
// request => 文件名
Resolver.prototype.join = function(path, request) {
    var cacheEntry;
    // 获取缓存目录
    var pathCache = memoizedJoin.get(path);
    if (typeof pathCache === "undefined") {
        memoizedJoin.set(path, pathCache = new Map());
    } else {
        // 获取目录缓存中对应的文件缓存
        cacheEntry = pathCache.get(request);
        if (typeof cacheEntry !== "undefined")
            return cacheEntry;
    }
    // 初次获取文件
    cacheEntry = memoryFsJoin(path, request);
    // 设置缓存
    pathCache.set(request, cacheEntry);
    return cacheEntry;
};

  非常的简单明了,用了一个map缓存一个目录,目录的值也是一个map,缓存该目录下的文件。

  这里看一下是第一次时,memoryFsJoin是如何处理路径的:

var normalize = require("./normalize");
// windows与linux系统绝对路径正则
var absoluteWinRegExp = /^[A-Z]:([\\\/]|$)/i;
var absoluteNixRegExp = /^\//i;

// path => 'd:\\workspace\\doc'
// request => 'package.json'
module.exports = function join(path, request) {
    if (!request) return normalize(path);
    // 检测是否绝对路径
    if (absoluteWinRegExp.test(request)) return normalize(request.replace(/\//g, "\\"));
    if (absoluteNixRegExp.test(request)) return normalize(request);
    // 目录为/时
    if (path == "/") return normalize(path + request);
    // 命中这里 注意正则后面的i
    // 替换拼接后 => d:\\workspace\\doc\\package.json
    if (absoluteWinRegExp.test(path)) return normalize(path.replace(/\//g, "\\") + "\\" + request.replace(/\//g, "\\"));
    if (absoluteNixRegExp.test(path)) return normalize(path + "/" + request);
    return normalize(path + "/" + request);
};

  果然还没完,在进行两个平台路径间的判断后,将两个参数拼接后传入normalize方法,参数已经在注释给出。

  以该字符串为例,看一下normalize方法:

// path => d:\\workspace\\doc\\package.json
module.exports = function normalize(path) {
    // parts => [ 'd:', '\\', 'workspace', '\\', 'doc', '\\', 'package.json' ]
    var parts = path.split(/(\\+|\/+)/);
    if (parts.length === 1)
        return path;
    var result = [];
    var absolutePathStart = 0;
    // sep主要用来标记切割数组中\\这种路径符号
    for (var i = 0, sep = false; i < parts.length; i++, sep = !sep) {
        var part = parts[i];
        //第一次弹入磁盘名 => result = ['d:']
        if (i === 0 && /^([A-Z]:)?$/i.test(part)) {
            result.push(part);
            absolutePathStart = 2;
        } else if (sep) {
            // 如果是路径符号 直接弹入
            // result = ['d:','\\']
            result.push(part[0]);
        }
        // 接下来是对'..'与'.'符号进行处理
        // 看一下注释就懂了 列举了各种情况
        else if (part === "..") {
            switch (result.length) {
                case 0:
                    // i. e. ".." => ".."
                    // i. e. "../a/b/c" => "../a/b/c"
                    result.push(part);
                    break;
                case 2:
                    // i. e. "a/.." => ""
                    // i. e. "/.." => "/"
                    // i. e. "C:\.." => "C:\"
                    // i. e. "a/../b/c" => "b/c"
                    // i. e. "/../b/c" => "/b/c"
                    // i. e. "C:\..\a\b\c" => "C:\a\b\c"
                    i++;
                    sep = !sep;
                    result.length = absolutePathStart;
                    break;
                case 4:
                    // i. e. "a/b/.." => "a"
                    // i. e. "/a/.." => "/"
                    // i. e. "C:\a\.." => "C:\"
                    // i. e. "/a/../b/c" => "/b/c"
                    if (absolutePathStart === 0) {
                        result.length -= 3;
                    } else {
                        i++;
                        sep = !sep;
                        result.length = 2;
                    }
                    break;
                default:
                    // i. e. "/a/b/.." => "/a"
                    // i. e. "/a/b/../c" => "/a/c"
                    result.length -= 3;
                    break;
            }
        } else if (part === ".") {
            switch (result.length) {
                case 0:
                    // i. e. "." => "."
                    // i. e. "./a/b/c" => "./a/b/c"
                    result.push(part);
                    break;
                case 2:
                    // i. e. "a/." => "a"
                    // i. e. "/." => "/"
                    // i. e. "C:\." => "C:\"
                    // i. e. "C:\.\a\b\c" => "C:\a\b\c"
                    if (absolutePathStart === 0) {
                        result.length--;
                    } else {
                        i++;
                        sep = !sep;
                    }
                    break;
                default:
                    // i. e. "a/b/." => "a/b"
                    // i. e. "/a/." => "/"
                    // i. e. "C:\a\." => "C:\"
                    // i. e. "a/./b/c" => "a/b/c"
                    // i. e. "/a/./b/c" => "/a/b/c"
                    result.length--;
                    break;
            }
        }
        // 无意外直接弹入 
        else if (part) {
            result.push(part);
        }
    }
    // 给磁盘名后面拼接上路径符号
    if (result.length === 1 && /^[A-Za-z]:$/.test(result))
        return result[0] + "\\";
    // 这是正常返回
    return result.join("");
};

  讲道理,只有不是乱写路径,这里都会普通的返回传进去的路径(后面会出现特殊情况)。

 

  返回的路径,会被readFile作为参数调用,最终返回读取到的json字符串作为对应的content传入回调函数中。

/*
    callback(null, {
        content: content,
        directory: directory,
        path: descriptionFilePath
    })
*/
function loadDescriptionFile(resolver, directory, filenames, callback) {
    (function findDescriptionFile() {
        forEachBail(filenames, function(filename, callback) { /**/ },
            // 这里的callback为最外部的callback
            // 被这里的回调绕死了
            // result为之前传进来的对象 注释有写
            function(err, result) {
                if (err) return callback(err);
                if (result) {
                    return callback(null, result);
                } else {
                    directory = cdUp(directory);
                    if (!directory) {
                        return callback();
                    } else {
                        return findDescriptionFile();
                    }
                }
            });
    }());
}

  这个callback一层一层的往外执行,最后回到了DescriptionFilePlugin中:

DescriptionFileUtils.loadDescriptionFile(resolver, directory, filenames, ((err, result) => {
    if (err) return callback(err);
    // 找不到package.json文件时
    if (!result) {
        // 第一次也没有这两个属性
        if (callback.missing) {
            filenames.forEach((filename) => {
                callback.missing.push(resolver.join(directory, filename));
            });
        }
        if (callback.log) callback.log("No description file found");
        // 直接调用callback
        return callback();
    }
    // 如果读取到了就会将描述文件的路径、目录、内容拼接到request对象上
    // 路径转换为相对路径
    const relativePath = "." + request.path.substr(result.directory.length).replace(/\\/g, "/");
    const obj = Object.assign({}, request, {
        descriptionFilePath: result.path,
        descriptionFileData: result.content,
        descriptionFileRoot: result.directory,
        relativePath: relativePath
    });
    // 触发下一个事件流
    // 带有message
    resolver.doResolve(target, obj, "using description file: " + result.path + " (relative path: " + relativePath + ")", createInnerCallback((err, result) => {
        if (err) return callback(err);
        if (result) return callback(null, result);

        // Don't allow other description files or none at all
        callback(null, null);
    }, callback));
}));

  如果没有package.json文件,就会直接调用callback并且不传任何参数,知道这个callback是哪个callback吗????

  是这个:

function innerCallback(err, result) {
    if (arguments.length > 0) {
        if (err) return callback(err);
        if (result) return callback(null, result);
        return callback();
    }
    runAfter();
}

  什么是回调地狱?无限ajax内嵌?nonono,太单纯,来看webpack源码吧,一个callback可以传入地心,搞得我现在看到callback就头大。

 

  很明显,这里没有传任何参数,直接进入runAfter,下节讲吧,还好已经跑出来了,不然隔两天回来看根本不知道飞哪去了。

posted @ 2018-01-23 16:26  书生小龙  阅读(616)  评论(0编辑  收藏  举报