zoukankan      html  css  js  c++  java
  • .34-浅析webpack源码之事件流make(3)

      新年好呀~过个年光打游戏,function都写不顺溜了。

      上一节的代码到这里了:

    // NormalModuleFactory的resolver事件流
    this.plugin("resolver", () => (data, callback) => {
        // ...
    
        asyncLib.parallel( /*...*/ ,
            /* 
                results:
                [ 
                    [],
                    { 
                        resourceResolveData:
                        { 
                            context: { issuer: '', compiler: undefined },
                            path: 'd:\workspace\doc\input.js',
                            request: undefined,
                            query: '',
                            module: false,
                            file: false,
                            descriptionFilePath: 'd:\workspace\doc\package.json',
                            descriptionFileData: [Object],
                            descriptionFileRoot: 'd:\workspace\doc',
                            relativePath: './input.js',
                            __innerRequest_request: undefined,
                            __innerRequest_relativePath: './input.js',
                            __innerRequest: './input.js' 
                        },
                        resource: 'd:\workspace\doc\input.js' 
                    } 
                ]
            */
            (err, results) => {
                // ...超多内容
            });
    });

      经过长长的resolve,最终也只是解析入口文件的合法路径信息,然后调用回调函数。

      接下来分析回调函数是如何处理返回结果的:

    // NormalModuleFactory的resolver事件流
    this.plugin("resolver", () => (data, callback) => {
        // ...
    
        asyncLib.parallel( /*...*/ ,
            (err, results) => {
                if (err) return callback(err);
                // 暂时不存在loaders
                let loaders = results[0];
                const resourceResolveData = results[1].resourceResolveData;
                resource = results[1].resource;
    
                // 跳过下面几部分内容
                // translate option idents
                try {
                    loaders.forEach(item => {
                        if (typeof item.options === "string" && /^?/.test(item.options)) {
                            item.options = this.ruleSet.findOptionsByIdent(item.options.substr(1));
                        }
                    });
                } catch (e) {
                    return callback(e);
                }
    
                if (resource === false) {
                    // ignored
                    return callback(null,
                        new RawModule(
                            "/* (ignored) */",
                            `ignored ${context} ${request}`,
                            `${request} (ignored)`
                        )
                    );
                }
    
                const userRequest = loaders.map(loaderToIdent).concat([resource]).join("!");
    
                // 尝试获取路径参数
                let resourcePath = resource;
                let resourceQuery = "";
                const queryIndex = resourcePath.indexOf("?");
                if (queryIndex >= 0) {
                    resourceQuery = resourcePath.substr(queryIndex);
                    resourcePath = resourcePath.substr(0, queryIndex);
                }
    
                // 很久之前的东西
                const result = this.ruleSet.exec({
                    resource: resourcePath,
                    resourceQuery,
                    issuer: contextInfo.issuer,
                    compiler: contextInfo.compiler
                });
    
                // ...
            });
    });

      返回的结果有两部分,一个是loader,一个是文件对应路径。

      对于入口文件的当前解析,不存在loader,所以会直接跳过开始的几部分内容,直接进入后面的ruleSet方法处理。

    ruleSet.exec

      这个ruleSet是个很久远的东西了,在18-19节有讲,主要是对配置文件中的modules.rules进行二次处理,包装在一个对象中。

      这里的exec方法主要是判断路径信息是否符合配置文件中定义的rules并解析返回一个result,方法如下(原型方法改成箭头函数好看一点):

    exec = (data) => {
        const result = [];
        this._run(data, {
            rules: this.rules
        }, result);
        return result;
    }

      真正的判断方法是_run,其中data为传进来的判断对象,rules为判断标准,result是返回的结果。

      简答过一下内容:

    /*
    module.rules => 
        [
            {
                test: /.vue$/,
                loader: 'vue-loader',
            },
            {
                test: /.css$/,
                loader: 'css!style-loader'
            },
            {
                test: /.js$/,
                loader: 'babel-loader'
            },
        ]
    data =>
        {
            resource: 'd:\workspace\doc\input.js',
            resourceQuery: '',
            issuer: '',
            compiler: undefined 
        }
    rule =>
        { 
            rules:
            [   
                { resource: [Function: bound test], use: [Array] },
                { resource: [Function: bound test], use: [Array] },
                { resource: [Function: bound test], use: [Array] } 
            ] 
        }
    */
    _run = (data, rule, result) => {
        // 判断特殊键提前返回
        // test conditions
        if (rule.resource && !data.resource)
            return false;
        if (rule.resourceQuery && !data.resourceQuery)
            return false;
        if (rule.compiler && !data.compiler)
            return false;
        if (rule.issuer && !data.issuer)
            return false;
        if (rule.resource && !rule.resource(data.resource))
            return false;
        if (data.issuer && rule.issuer && !rule.issuer(data.issuer))
            return false;
        if (data.resourceQuery && rule.resourceQuery && !rule.resourceQuery(data.resourceQuery))
            return false;
        if (data.compiler && rule.compiler && !rule.compiler(data.compiler))
            return false;
    
        // ['rules'] => []
        // apply
        const keys = Object.keys(rule).filter((key) => {
            return ["resource", "resourceQuery", "compiler", "issuer", "rules", "oneOf", "use", "enforce"].indexOf(key) < 0;
        });
        keys.forEach((key) => {
            result.push({
                type: key,
                value: rule[key]
            });
        });
        // 依次进入
        if (rule.use) {
            rule.use.forEach((use) => {
                result.push({
                    type: "use",
                    value: typeof use === "function" ? RuleSet.normalizeUseItemFunction(use, data) : use,
                    enforce: rule.enforce
                });
            });
        }
        // 遍历3个判断标准
        if (rule.rules) {
            for (let i = 0; i < rule.rules.length; i++) {
                this._run(data, rule.rules[i], result);
            }
        }
        // 跳过
        if (rule.oneOf) {
            for (let i = 0; i < rule.oneOf.length; i++) {
                if (this._run(data, rule.oneOf[i], result))
                    break;
            }
        }
    
        return true;
    }

      这里会跳过部分代码,在配置文件的rules中我写了3个简单的loader,分别对应js、css、vue后缀的文件,入口文件为input.js,所以匹配到了babel-loader。

      获取到了对应的loader,继续跑流程:

    // NormalModuleFactory的resolver事件流
    this.plugin("resolver", () => (data, callback) => {
        // ...
    
        asyncLib.parallel( /*...*/ , (err, results) => {
            // ...
    
            /*  
            result => 
                [ 
                    { 
                        type: 'use',
                        value: { loader: 'babel-loader' },
                        enforce: undefined 
                    } 
                ]
            */
            const result = this.ruleSet.exec({
                resource: resourcePath,
                resourceQuery,
                issuer: contextInfo.issuer,
                compiler: contextInfo.compiler
            });
    
            const settings = {};
            const useLoadersPost = [];
            const useLoaders = [];
            const useLoadersPre = [];
            result.forEach(r => {
                if (r.type === "use") {
                    // enforce代表loader的特殊标记
                    if (r.enforce === "post" && !noPostAutoLoaders && !noPrePostAutoLoaders)
                        useLoadersPost.push(r.value);
                    else if (r.enforce === "pre" && !noPrePostAutoLoaders)
                        useLoadersPre.push(r.value);
                    // 走这条分支
                    else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders)
                        useLoaders.push(r.value);
                } else {
                    settings[r.type] = r.value;
                }
            });
            // 又是parallel
            asyncLib.parallel([
                this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, this.resolvers.loader),
                this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, this.resolvers.loader),
                this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, this.resolvers.loader)
            ], (err, results) => {
                if (err) return callback(err);
                loaders = results[0].concat(loaders, results[1], results[2]);
                process.nextTick(() => {
                    callback(null, {
                        context: context,
                        request: loaders.map(loaderToIdent).concat([resource]).join("!"),
                        dependencies: data.dependencies,
                        userRequest,
                        rawRequest: request,
                        loaders,
                        resource,
                        resourceResolveData,
                        parser: this.getParser(settings.parser)
                    });
                });
            });
        });
    });

      这里判断了loader是否存在特殊标记,然后将结果弹入对应的loader数组中。

      最后再次调用了asyncLib的parallel方法,方法在上一个parallel调用过,但是当初没有loader,这次有了。

      看起来就比较复杂,下一节再过。

      把resolveRequestArray过完,方法源码与参数如下:

    /*
        contextInfo => { issuer: '', compiler: undefined }
        context => D:workspacedoc
        array => [ { loader: 'babel-loader' } ]
        resolver => resolvers.loader
        callback => undefined
    */
    resolveRequestArray = (contextInfo, context, array, resolver, callback) => {
        if (array.length === 0) return callback(null, []);
        asyncLib.map(array, (item, callback) => {
            // resolver.loader
            resolver.resolve(contextInfo, context, item.loader, (err, result) => {
                if (err && /^[^/]*$/.test(item.loader) && !/-loader$/.test(item.loader)) {
                    return resolver.resolve(contextInfo, context, item.loader + "-loader", err2 => {
                        if (!err2) {
                            err.message = err.message + "
    " +
                                "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.
    " +
                                `                 You need to specify '${item.loader}-loader' instead of '${item.loader}',
    ` +
                                "                 see https://webpack.js.org/guides/migrating/#automatic-loader-module-name-extension-removed";
                        }
                        callback(err);
                    });
                }
                if (err) return callback(err);
    
                const optionsOnly = item.options ? {
                    options: item.options
                } : undefined;
                return callback(null, Object.assign({}, item, identToLoaderRequest(result), optionsOnly));
            });
        }, callback);
    }

      这里的array只有一个元素,map方法中调用了resolver的resolve方法,似曾相识,就跟之前那个resolve方法一样,不过来源是resolvers.loader对象。

      再回顾一下定义:

    compiler.resolvers.normal = ResolverFactory.createResolver(Object.assign({
        fileSystem: compiler.inputFileSystem
    }, options.resolve));
    compiler.resolvers.context = ResolverFactory.createResolver(Object.assign({
        fileSystem: compiler.inputFileSystem,
        resolveToContext: true
    }, options.resolve));
    compiler.resolvers.loader = ResolverFactory.createResolver(Object.assign({
        fileSystem: compiler.inputFileSystem
    }, options.resolveLoader));

      可以看出,除去最后面那个options,调用的方法是一模一样的,而options.resolve与options.resolveLoader在默认情况下如下所示:

    {
        "resolve": {
            "unsafeCache": true,
            "modules": ["node_modules"],
            "extensions": [".js", ".json"],
            "mainFiles": ["index"],
            "aliasFields": ["browser"],
            "mainFields": ["browser", "module", "main"],
            "cacheWithContext": false
        },
        "resolveLoader": {
            "unsafeCache": true,
            "mainFields": ["loader", "main"],
            "extensions": [".js", ".json"],
            "mainFiles": ["index"],
            "cacheWithContext": false
        }
    }

      只是少了modules、aliasFileds,其他都是一样的,这两个参数并不会影响如前doResolve几节中所讲的流程。

      也就是说,这个方法相当于回到了第29节,从头开始跑一遍所有的事件流,最后解析出对应的路径。

      这里有一个不一样的地方,这个babel-loader并不是普通的文件类型,所以在doResolver的事件串流中,会走模块分支。

      又臭又长的过程就先暂时跳过了,下节再讲,最后返回babel-loader的入口文件路径如下所示:

    [ { loader: 'D:\workspace\node_modules\babel-loader\lib\index.js' } ]

      通过神奇的resolve方法找到了对应loader的入口文件,最后的代码结果如下:

    asyncLib.parallel([
        this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPost, this.resolvers.loader),
        this.resolveRequestArray.bind(this, contextInfo, this.context, useLoaders, this.resolvers.loader),
        this.resolveRequestArray.bind(this, contextInfo, this.context, useLoadersPre, this.resolvers.loader)
    ], (err, results) => {
        if (err) return callback(err);
        loaders = results[0].concat(loaders, results[1], results[2]);
        process.nextTick(() => {
            callback(null, {
                // 'D:\workspace\doc'
                context: context,
                // 'D:\workspace\node_modules\babel-loader\lib\index.js!D:\workspace\doc\input.js'
                request: loaders.map(loaderToIdent).concat([resource]).join("!"),
                // ...
                dependencies: data.dependencies,
                // 'D:\workspace\doc\input.js'
                userRequest,
                // './input.js'
                rawRequest: request,
                // [ { loader: 'D:\workspace\node_modules\babel-loader\lib\index.js' } ]
                loaders,
                // 'D:\workspace\doc\input.js'
                resource,
                // ...
                resourceResolveData,
                parser: this.getParser(settings.parser)
            });
        });
    });

      其中最后的parser一会再说,先讲讲这个callback。

      看webpack源码最大的痛苦就是函数嵌套太深,每一个callback都是噩梦,所以这个callback我也是找了很久很久。

      这个事件流的入口如下:

    this.plugin("factory", () => (result, callback) => {
        let resolver = this.applyPluginsWaterfall0("resolver", null);
    
        // Ignored
        if (!resolver) return callback();
    
        resolver(result, (err, data) => { /*...*/ })
    })

      这里调用了tapable的方法返回了一个函数,然后再次调用该函数。

      而这个事件流的主心骨就是两个asyncLib.parallel,根本找不到哪里返回了东西,直到我看到了事件流的plugin:

    this.plugin("resolver", () => (data, callback) => {
        // ...
    })

      没错,这里有两个箭头函数,先返回一个函数,下面的调用才是真正的执行。

      那就很明显了,process.nextTick是一个node内置的异步方法,类似于vue的$nextTick,作用就不多说了。

      callback对应的就是那个调用时的第二个参数,而最后返回的大对象就是data。

      简要看一下回调函数内容:

    resolver(result, (err, data) => {
        if (err) return callback(err);
    
        // Ignored
        if (!data) return callback();
    
        // direct module
        if (typeof data.source === "function")
            return callback(null, data);
    
        this.applyPluginsAsyncWaterfall("after-resolve", data, (err, result) => {
            // ...
        });
    });

      对返回的结果做了简单的判断,然后触发了另外一个事件流。

      

      下一节完善webpack是如何根据babel-loader搜索到对应的模块入口文件的。

  • 相关阅读:
    磁盘挂载基本概念
    判断cache Line的作用
    map的用法
    java中关键字volatile的作用
    Java获取用户输入
    小白说编译原理lex和yacc环境配置-多图
    C++中对sprintf()函数的说明(转)
    解决VS编译DevExpress后Debug默认产生几个多余的语言包"de" "en" "es" "ja" "ru"的问题
    SqlServer发现不是默认端口1433该如何进行连接
    量化交易之接口篇 — 恒生UFX交易接口基本介绍说明
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/8461176.html
Copyright © 2011-2022 走看看