• .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:..ac" => "C:ac"
                        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:.ac" => "C:ac"
                        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,下节讲吧,还好已经跑出来了,不然隔两天回来看根本不知道飞哪去了。

  • 相关阅读:
    基于C++CJAVA的python入门
    雁栖湖健身计划
    显存的一些知识
    Cuda_bank-conflict
    翻译文章进展
    一些CV界的好资源
    how processor caches work
    LINQ-进阶的扩展方法
    LINQ-基础
    CTFHUB-技能树 基础知识 ctf练习平台
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8334882.html
Copyright © 2020-2023  润新知