• .29-浅析webpack源码之doResolve事件流(1)


      在上一节中,最后返回了一个resolver,本质上就是一个Resolver对象:

    resolver = new Resolver(fileSystem);

      这个对象的构造函数非常简单,只是简单的继承了Tapable,并接收了fileSystem参数:

    function Resolver(fileSystem) {
        Tapable.call(this);
        this.fileSystem = fileSystem;
    }
    module.exports = Resolver;

    resolve

      而在make事件流中,调用的正是该类的原型方法resolve,现在可以进行看一眼了:

    /* 
        context => { issuer: '', compiler: undefined }
        path => 'd:\workspace\doc'
        request => './input.js'
        callback => [Function]
    */
    Resolver.prototype.resolve = function resolve(context, path, request, callback) {
        if (arguments.length === 3) {
            throw new Error("Signature changed: context parameter added");
        }
        var resolver = this;
        // 包装参数
        var obj = {
            context: context,
            path: path,
            request: request
        };
    
        var localMissing;
        var log;
        // message => resolve './input.js' in 'd:\workspace\doc'
        var message = "resolve '" + request + "' in '" + path + "'";
    
        function writeLog(msg) {
            log.push(msg);
        }
    
        function logAsString() {
            return log.join("
    ");
        }
    
        function onError(err, result) { /**/ }
    
        function onResolve(err, result) { /**/ }
        // 这两个并不存在
        onResolve.missing = callback.missing;
        onResolve.stack = callback.stack;
        // 调用另一个原型方法
        return this.doResolve("resolve", obj, message, onResolve);
    };

      需要注意的是,该方法会在webpack编译期间被调用多次,这里的参数仅仅是第一次被调用时的。

    doResolve

      简单的说,resolve方法将参数进行二次包装后,调用了另外一个原型方法doResolve,源码整理如下:

    /*
        type => 'resolve'
        request => 
            { 
                context: { issuer: '', compiler: undefined }, 
                path: 'd:\workspace\doc', 
                request: './input.js' 
            }
        message => resolve './input.js' in 'd:\workspace\doc'
        callback => doResolve()
    */
    Resolver.prototype.doResolve = function doResolve(type, request, message, callback) {
        var resolver = this;
        // stackLine => resolve: (d:workspacedoc) ./input.js
        var stackLine = type + ": (" + request.path + ") " +
            (request.request || "") + (request.query || "") +
            (request.directory ? " directory" : "") +
            (request.module ? " module" : "");
        var newStack = [stackLine];
        // 暂无
        if (callback.stack) { /**/ }
        // 没这个事件流
        resolver.applyPlugins("resolve-step", type, request);
        // before-resolve
        var beforePluginName = "before-" + type;
        // 检测是否存在对应的before事件流
        if (resolver.hasPlugins(beforePluginName)) { /**/ }
        // 走正常流程
        else {
            runNormal();
        }
    }

      由于callback的missing、stack属性均为undefined,所以会直接跳过那个if判断。

      而事件流resolve-step、before-resolve也不存在,所以会直接走最后的else,进入runNormal方法。

      这里全面描述一下doResolve,方法内部有5个函数,分别名为beforeInnerCallback、runNormal、innerCallback、runAfter、afterInnerCallback,所有的callback函数都负责包装对应事件流的回调函数。

      源码如下:

    // 先判断是否存在before-type事件流
    if (resolver.hasPlugins(beforePluginName)) {
        // 触发完调用回调
        resolver.applyPluginsAsyncSeriesBailResult1(beforePluginName, request, createInnerCallback(beforeInnerCallback, {
            log: callback.log,
            missing: callback.missing,
            stack: newStack
        }, message && ("before " + message), true));
    }
    // 不存在跳过直接触发type事件流 
    else {
        runNormal();
    }
    
    function beforeInnerCallback(err, result) {
        if (arguments.length > 0) {
            if (err) return callback(err);
            if (result) return callback(null, result);
            return callback();
        }
        // 这里进入下一阶段
        runNormal();
    }
    
    // 触发type事件流
    function runNormal() {
        if (resolver.hasPlugins(type)) { /**/ } else {
            runAfter();
        }
    }
    
    function innerCallback(err, result) { /**/ }
    // 触发after-type
    function runAfter() {
        var afterPluginName = "after-" + type;
        // 这里就是直接调用callback了
        if (resolver.hasPlugins(afterPluginName)) { /**/ } else {
            callback();
        }
    }
    
    function afterInnerCallback(err, result) { /**/ }

      可以看到逻辑很简单,每一个事件流type存在3个类型:before-type、type、after-type,doResolve会尝试依次触发每一个阶段的事件流。

      在上面的例子中,因为不存在before-resolve事件流,所以会调用runNormal方法去触发resolve的事件流。

      如果存在,触发对应的事件流,并在回调函数中触发下一阶段的事件流。

      所以这里的调用就可以用一句话概括:尝试触发before-resolve、resolve、after-resolve事件流后,调用callback。

    unsafeCache

      resolve事件流均来源于上一节第三部分注入的开头,如下:

    // resolve
    if (unsafeCache) {
        plugins.push(new UnsafeCachePlugin("resolve", cachePredicate, unsafeCache, cacheWithContext, "new-resolve"));
        plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
    } else {
        plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
    }

    UnsafeCachePlugin

      这个unsafeCache虽然不知道是啥,但是一般不会去设置,默认情况下是true,因此进入UnsafeCachePlugin插件,构造函数如下:

    /*
        source => resolve
        filterPredicate => function(){return true}
        cache => {}
        withContext => false
        target => new-resolve
     */
    function UnsafeCachePlugin(source, filterPredicate, cache, withContext, target) {
        this.source = source;
        this.filterPredicate = filterPredicate;
        this.withContext = withContext;
        this.cache = cache || {};
        this.target = target;
    }

      基本上只是对传入参数的获取,直接看事件流的内容:

    function getCacheId(request, withContext) {
        // 直接用配置对象的字符串形式作为缓存对象key
        // 貌似vue源码的compile也是这样的
        return JSON.stringify({
            context: withContext ? request.context : "",
            path: request.path,
            query: request.query,
            request: request.request
        });
    }
    UnsafeCachePlugin.prototype.apply = function(resolver) {
        var filterPredicate = this.filterPredicate;
        var cache = this.cache;
        var target = this.target;
        var withContext = this.withContext;
        // 这里注入resolve事件流
        /* 
            request => 
            { 
                context: { issuer: '', compiler: undefined }, 
                path: 'd:\workspace\doc', 
                request: './input.js' 
            }
            callback => createInnerCallback(innerCallback,{...})
        */
        resolver.plugin(this.source, function(request, callback) {
            // 这里永远是true
            if (!filterPredicate(request)) return callback();
            // 尝试获取缓存
            var cacheId = getCacheId(request, withContext);
            var cacheEntry = cache[cacheId];
            if (cacheEntry) {
                return callback(null, cacheEntry);
            }
            // 这里再次调用了doResolve函数
            // target => new-resolve
            resolver.doResolve(target, request, null, createInnerCallback(function(err, result) {
                if (err) return callback(err);
                if (result) return callback(null, cache[cacheId] = result);
                callback();
            }, callback));
        });
    };

      这样就很明显了,resolve事件只是为了获取缓存,如果不存在缓存,就再次调用doResolve方法,这一次传入的type为new-resolve。

    ParsePlugin

      new-resolve事件流并不存在before-xxx或者after-xxx的情况,所以直接看事件流本身。注入地点在UnsafeCachePlugin插件的后面。

      从上面的if/else可以看出,无论如何都会调用该插件,只是会根据unsafeCache的值来决定是否取缓存。

      这个插件内容比较简单暴力,简答过一下:

    // source => new-resolve
    // target => parsed-resolve
    function ParsePlugin(source, target) {
        this.source = source;
        this.target = target;
    }
    module.exports = ParsePlugin;
    
    ParsePlugin.prototype.apply = function(resolver) {
        var target = this.target;
        resolver.plugin(this.source, function(request, callback) {
            // 解析
            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");
            }
            // 触发target的doResolve
            resolver.doResolve(target, obj, null, callback);
        });
    };

      基本上都是一个套路了,触发事件流,做点什么,然后最后调用doResolve触发下一轮。

      这里的核心就是parse方法,估计跟vue源码的parse差不多,比较麻烦,下一节再讲。

    Resolver.prototype.parse

      这个parse方法超级简单,如下:

    Resolver.prototype.parse = function parse(identifier) {
        if (identifier === "") return null;
        var part = {
            request: "",
            query: "",
            module: false,
            directory: false,
            file: false
        };
        // 根据问号切割参数
        var idxQuery = identifier.indexOf("?");
        if (idxQuery === 0) {
            part.query = identifier;
        } else if (idxQuery > 0) {
            part.request = identifier.slice(0, idxQuery);
            part.query = identifier.slice(idxQuery);
        } else {
            part.request = identifier;
        }
        if (part.request) {
            // 判断是文件还是文件夹
            part.module = this.isModule(part.request);
            part.directory = this.isDirectory(part.request);
            // 去掉文件夹最后的斜杠
            if (part.directory) {
                part.request = part.request.substr(0, part.request.length - 1);
            }
        }
        return part;
    };
    /* 
        匹配以下内容开头的字符串
        1 => .
        2 => ./ or .
        3 => ..
        4 => ../ or ..
        5 => /
        6 => A-Z:/ or A-Z:
    */
    var notModuleRegExp = /^.$|^.[\/]|^..$|^..[/\]|^/|^[A-Z]:[\/]/i;
    Resolver.prototype.isModule = function isModule(path) {
        return !notModuleRegExp.test(path);
    };
    /*
        匹配以 or /结尾的字符串
    */
    var directoryRegExp = /[/\]$/i;
    Resolver.prototype.isDirectory = function isDirectory(path) {
        return directoryRegExp.test(path);
    };

      内容很简单,就做了2件事:

    1、根据问号切割参数

    2.、判断是文件还是文件夹

      最后返回了信息组成的对象。

  • 相关阅读:
    【1801視聴説2宿題】中国のリサイクル事情やごみの分別事情に対する意見
    【1701日本語新聞編集】第2回3月6日
    【1701新聞編集宿題】興味のあるネットニュース
    【1801日語写作】第2回:3月5日
    【1801日語听解4】第2回:3月3日
    【1801日本語新聞選読】第2回:3月3日
    不解压查看tar.gz包内文件
    设计模式——适配器模式
    ubuntu安装jre
    设计模式——抽象工厂模式
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8287527.html
Copyright © 2020-2023  润新知