【webpack源码5】- Compiler

webpack | 2020-06-29 19:12:29 406次 0次

Compiler 模块是 webpack 的支柱引擎,它通过 CLI Node API 传递的所有选项,创建出一个 compilation 实例。它扩展自 Tapable 类,以便注册和调用插件。大多数面向用户的插件,会先在 Compiler 上注册。

整体结构如下:

class Compiler extends Tapable {
    ...
    getInfrastructureLogger(name) { }
    //监听初始化
    watch(watchOptions, handler) { }
    // 运行编译, 整个编译过程启动的入口
    run(callback) { }
    // 作为子编译进程运行
    runAsChild(callback) { }
    // 净化输入
    purgeInputFileSystem() { }
    // 发布资源
    emitAssets(compilation, callback) { }
    // 发布记录 ecordsOutputPath: path.join(__dirname, 'newRecords.json')
    emitRecords(callback) { }
    
    /**
     * 一些数据片段,用于储存多次构建过程中的module的标识
     * 可以使用此文件来跟踪在每次构建之间的模块变化
     * recordsInputPath: path.join(__dirname, 'records.json')
     */
    readRecords(callback) { }
    // 创建子编译器
    createChildCompiler(
        compilation,
        compilerName,
        compilerIndex,
        outputOptions,
        plugins
    ) {
       ...
    }
    // 是否子汇编
    isChild() {
        return !!this.parentCompilation;
    }
    // 创建compilation
    createCompilation() { }
    // 一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,
    //代表了一次资源的构建。
    newCompilation(params) {  }
    // 创建普通模块的工厂
    createNormalModuleFactory() { }
    // 创建上下文模块的工厂
    createContextModuleFactory() {  }
    // 获取一个新的汇编参数对象
    newCompilationParams() { }
    // 编译
    compile(callback) {  }
}


一、 compiler.run 

首先 this.hook 上注册了很多事件,这里可以参考下官方的 compiler api,里面有简要说明各个条件下触发机制。

//触发执行前的钩子
this.hooks.beforeRun.callAsync(this, err => {
    if (err) return finalCallback(err); //运行结束回调处理
    //接着执行 run 钩子
    this.hooks.run.callAsync(this, err => {
        if (err) return finalCallback(err);
        //读取之前的records
        this.readRecords(err => {
            if (err) return finalCallback(err);
            //执行编译
            this.compile(onCompiled);
        });
    });
});

这里先调用了 readRecords 方法,它是一些数据片段,用于储存多次构建过程中的 module 的标识,可以使用此文件来跟踪在每次构建之间的模块变化:

readRecords(callback) {
    // 配置的路径 recordsInputPath: path.join(__dirname, 'records.json')
    if (!this.recordsInputPath) {
        this.records = {};
        return callback();
    }
    //inputFileSystem是一个封装过的文件系统,扩展了fs的功能
    this.inputFileSystem.stat(this.recordsInputPath, err => {
        //recordsInputPath的文件是否存在 存在则读取并解析,存到this.records中
        if (err) return callback();
        this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
            if (err) return callback(err);
            try {
                this.records = parseJson(content.toString("utf-8"));
            } catch (e) {
                ...
            }
            return callback();
        });
    });
}

接着执行 compile 方法,开始执行编译,这里最终调用了 run 中的 onCompiled 回调,主要用于输出构建资源,等下面再说:

compile(callback) {
    // 创建了compilation的初始参数
    const params = this.newCompilationParams();
    // 编译前
    this.hooks.beforeCompile.callAsync(params, err => {
        if (err) return callback(err);
        //开始编译钩子
        this.hooks.compile.call(params);
        // 每次编译都会创建一个新的compilation
        const compilation = this.newCompilation(params);

        // WebpackOptionsApply --> EntryOptionPlugin --> itemToPlugin -->
        // SingleEntryPlugin --> compiler.hooks.make.tapAsync这里被触发 --> compilation.addEntry
        this.hooks.make.callAsync(compilation, err => {
            if (err) return callback(err);
            //模块完成构建
            compilation.finish(err => {
                if (err) return callback(err);
                //编译(compilation)停止接收新模块时触发
                compilation.seal(err => {
                    if (err) return callback(err);
                    //seal完成意味着编译完成,钩子触发
                    this.hooks.afterCompile.callAsync(compilation, err => {
                        ...
                        return callback(null, compilation);
                    });
                });
            });
        });
    });
}


二、newCompilationParams

这里面发生两件事件,第一个创建一个普通模块的工厂函数 createNormalModuleFactory,再通过 NormalModuleFactory 实例生成。这里有个 ResolverFactory,使用 Hook 的映射类来创建,可能因为避免多次创建, HookMap 中是 Map 对象存储。

createNormalModuleFactory() {
    const normalModuleFactory = new NormalModuleFactory(
        this.options.context, //当前路径
        this.resolverFactory,
        this.options.module || {}
    );
    this.hooks.normalModuleFactory.call(normalModuleFactory);
    return normalModuleFactory;
}

NormalModuleFactory 这个也是继承自 Tapable,这里面实例化了 RuleSet。

class NormalModuleFactory extends Tapable {
    ...
    //实例化 loader 插件,预置的和传入的配置合并
    this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
    ...
    //先注册这个方法 自身的 create 中会触发
    this.hooks.factory.tap('NormalModuleFactory', () => (result, callback) => {
      //优先触发下面 resolver 回调
      //开始构建模块
      createdModule = new NormalModule(result);
    });
    this.hooks.resolver.tap('NormalModuleFactory', () => (data, callback) => {
      //这里面触发时解析各种loader,然后处理最后生成正确顺序的loader组合
    });
}

ContextModuleFactory 仍然是继承自 Tapable,这里面貌似都是兼容旧版插件写法,做的事情和上面这个方法是一样的。

// 创建上下文模块的工厂
createContextModuleFactory() {
    const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
    this.hooks.contextModuleFactory.call(contextModuleFactory);
    return contextModuleFactory;
}
class ContextModuleFactory extends Tapable { ... }


三、Compilation

compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,代表一次资源的构建。这里面会发现有 thisCompilation compilation,因为在 createChildCompiler 子编译方法中官方注释说会复制所有的钩子,thisCompilation 其实是不会被复制的。

newCompilation(params) {
    const compilation = this.createCompilation();
    compilation.fileTimestamps = this.fileTimestamps;
    this.hooks.thisCompilation.call(compilation, params);
    ...
    return compilation;
}

首先 this.hook 上注册了很多事件,这里可以参考下官方的 compilation api

class Compilation extends Tapable {
    ...
}

接着第一部分中的 this.hooks.make.callAsync 触发,这里先看下它的注册流程以及其他流程:

① new WebpackOptionsApply().process             -- (webpack.js)

② new EntryOptionPlugin().apply(compiler);       -- (WebpackOptionsApply.js

    compiler.hooks.entryOption.call(options.context, options.entry);

③ compiler.hooks.entryOption.tap                      -- (EntryOptionPlugins.js

     new SingleEntryPlugin().apply

④ compiler.hooks.make.tapAsync                        -- (SingleEntryPlugin.js

     compilation.addEntry(context, dep, name, callback);

这里最终调用了 addEntry

Compilation.js

addEntry(context, entry, name, callback) {
    //钩子执行
    this.hooks.addEntry.call(entry, name);
    ...
    this._addModuleChain(
        context,
        entry,
        module => {
            this.entries.push(module);
        },
        (err, module) => {
            ...
        }
     )
}

 _addModuleChain 中继续通过 this.semaphore.acquire 进行一个并发量控制,默认值为 100,这里面进行 create 开始构建模块:

Compilation.js

_addModuleChain(context, dependency, onModule, callback) {
    ...
    //SingleEntryDependency
    const Dep = (dependency.constructor);
    // SingleEntryPlugin中注册了这个normalModuleFactory。dependencyFactories 会在各个插件中存入东西
    const moduleFactory = this.dependencyFactories.get(Dep);
    ...
    this.semaphore.acquire(() => {
        //开始创建 module
        moduleFactory.create(...)
    });
}


四、resolve

NormalModuleFactory 中的 create 中触发 this.hooks.factory.tap,然后这里面在 this.hooks.resolver 事件回调里面执行创建模块 NormalModule:

NormalModuleFactory.js

this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
    let resolver = this.hooks.resolver.call(null);
    ...
    //该函数为解析构建所有 module 所需要的 loaders 组合及这个 module 的相关构建信息
    resolver(result, (err, data) => {
        ...
        this.hooks.afterResolve.callAsync(data, (err, result) => {
            ...
            //创建模块的钩子被触发(源码中没发现哪里注册的)
            let createdModule = this.hooks.createModule.call(result);
            //被执行过
            if (!createdModule) {
                ...
                //构建模块
                createdModule = new NormalModule(result);
            }
            //(源码中没发现哪里注册的)
            createdModule = this.hooks.module.call(createdModule, result);
            ...
        });
    });
});

resolver 时会触发第二小节中提到的 ResolverFactory 方法,里面除了上述说的之外还会使用 enhanced-resolve,能够处理各种文件路径和 loader 处理,并且还提供缓存功能提升效率。

resolver 内部再进行解析各种类型 loader,生成一个 loader 组合,执行在下一小节中会提到。之前写过一篇 loader 深度分析的文章,从使用到插件使用、编写、运行原理可以参考,源码中就不再细入分析了。loader深度分析

上一小节中 NormalModuleFactory 中的 create 中触发的上述系列事件成功后的回调中,拿到 module 实例之后,进入 moduleFactory.create 的回调。 在这之前 dependencyCache 会添加缓存模块。

Compilation.js

//拿到module实例之后 进入这个回调中 在这之前dependencyCache会添加缓存模块
const addModuleResult = this.addModule(module);
module = addModuleResult.module;
//this.entries.push(module) 这里是主入口文件
onModule(module);
dependency.module = module;
module.addReason(null, dependency);
...
if (addModuleResult.build) {
    this.buildModule(module, false, null, null, err => {
        ...
        afterBuild();
    });
} else {
    ...
}

...

buildModule(module, optional, origin, dependencies, thisCallback) {
  ...
    this.hooks.buildModule.call(module);
    module.build(
        this.options,
        this,
        this.resolverFactory.get("normal", module.resolveOptions),
        this.inputFileSystem,
        error => {
            ...
        }
    );
}

接下来开始执行 buildModule 方法,并触发 buildModule 钩子函数,然后执行 module.build(就是 NormalModule 类中的 build 方法)。

NormalModule.js

build(options, compilation, resolver, fs, callback) {
    this.buildTimestamp = Date.now();
    this.built = true;
    this._source = null;
    this._sourceSize = null;
    this._ast = null;
    this._buildHash = "";
    this.error = null;
    this.errors.length = 0;
    this.warnings.length = 0;
    this.buildMeta = {};
    this.buildInfo = {
        cacheable: false,
        fileDependencies: new Set(),
        contextDependencies: new Set(),
        assets: undefined,
        assetsInfo: undefined
    };

    return this.doBuild(options, compilation, resolver, fs, err => {
        ...
    });
}

build 中初始化一些参数,调用自身 doBuild 方法,上面提到 loader 的解析操作,到这里开始执行 runLoaders。


五、回调执行

runLoader 成功后执行 createSource 方法生成 _source,再返回 doBuild 的回调中,执行 this.parser.parse,这个 parser 来自:

NormalModuleFactory.js

process.nextTick(() => {
    const type = settings.type;
    const resolveOptions = settings.resolve;
    callback(null, {
        ...
        parser: this.getParser(type, settings.parser),
        generator: this.getGenerator(type, settings.generator),
        resolveOptions
    });
});
NormalModule.js

const result = this.parser.parse(
    this._ast || this._source.source(),
    {
        current: this,
        module: this,
        compilation: compilation,
        options: options
    },
    (err, result) => {
        if (err) {
            handleParseError(err);
        } else {
            handleParseResult(result);
        }
    }
);

执行了 getParser(将当前模块解析为 ast 语法树)和 getGenerator(用于模版生成时提供方法)。

NormalModuleFactory.js

getParser(type, parserOptions) {
    ...
    return (this.parserCache[ident] = this.createParser(type, parserOptions));
}

createParser(type, parserOptions = {}) {
    const parser = this.hooks.createParser.for(type).call(parserOptions);
    if (!parser) {
        throw new Error(`No parser registered for ${type}`);
    }
    this.hooks.parser.for(type).call(parser, parserOptions);
    return parser;
}

getGenerator(type, generatorOptions) {
    let ident = type;
   ...
    return (this.generatorCache[ident] = this.createGenerator(
        type,
        generatorOptions
    ));
}
createGenerator(type, generatorOptions = {}) {
    const generator = this.hooks.createGenerator
        .for(type)
        .call(generatorOptions);
    if (!generator) {
        throw new Error(`No generator registered for ${type}`);
    }
    this.hooks.generator.for(type).call(generator, generatorOptions);
    return generator;
}

createParser 触发一个事件,首先 WebpackOptionsApply 中实例化一个 JavascriptModulesPlugin 类,这个类中注册了事件 compiler.hooks.compilation.tap,在 Compiler newCompilation 中触发,然后它的回调中又注册了:

JavascriptModulesPlugin.js

normalModuleFactory.hooks.createParser
	.for("javascript/auto")
	.tap("JavascriptModulesPlugin", options => {
		return new Parser(options, "auto");
	});

这里去实例化一个 Parser 对象,所以上面的 createParser 才会直接取到这个实例(这里的山路十八弯~)。

Parse.js

class Parser extends Tapable {
    ...
    parse(source, initialState) {
        let ast;
        let comments;
        if (typeof source === "object" && source !== null) {
            ast = source;
            comments = source.comments;
        } else {
            comments = [];
            //自身的一个静态方法 生成ast
            ast = Parser.parse(source, {
                sourceType: this.sourceType,
                onComment: comments
            });
        }
        ...
        //遍历 ast 收集依赖
        if (this.hooks.program.call(ast, comments) === undefined) {
            //use strict检测
            this.detectMode(ast.body);
            this.prewalkStatements(ast.body);
            this.blockPrewalkStatements(ast.body);
            this.walkStatements(ast.body);
        }
        ...
        return state;
    }
    ...
    static parse(code, options) {
        ...
        let ast;
        let error;
        let threw = false;
        try {
            // acornParser --> acorn.Parser | Babel
            ast = acornParser.parse(code, parserOptions);
        } catch (e) {
            error = e;
            threw = true;
        }
    
        ...
        return ast;
    }
}

this.hooks.program.call 触发,事件定义在 HarmonyDetectionParserPlugin UseStrictPlugin,坦白讲这块的逻辑没有理清楚,引用:上述执行结束后,会根据 import/export 的不同情况即模块间的相互依赖关系,在对应的 module.dependencies 上增加相应的依赖。


六、hash

parse 解析完成的回调中执行 handleParseResult,进行生成 hash

NormalModule.js
const handleParseResult = result => {
    this._lastSuccessfulBuildMeta = this.buildMeta;
    this._initBuildHash(compilation);
    return callback();
};

_initBuildHash(compilation) {
    //依赖 require("crypto") 库实现
    const hash = createHash(compilation.outputOptions.hashFunction);
    if (this._source) {
        hash.update("source");
        this._source.updateHash(hash);
    }
    //更新 hash 内容
    hash.update("meta");
    hash.update(JSON.stringify(this.buildMeta));
    //获取 hash
    this._buildHash = /** @type {string} */ (hash.digest("hex"));
}


七、afterBuild 

再回到 this.buildModule 回调中执行了 afterBuild,某个模块被解析创建后,在 addModule() 中设置 dependencies false 避免该模块重复解析创建依赖。

const afterBuild = () => {
    //首次解析
    if (recursive && addModuleResult.dependencies) {
        this.processModuleDependencies(dependentModule, callback);
    } else {
        return callback();
    }
};

processModuleDependencies 中判断各种模块递归解析

processModuleDependencies(module, callback) {
    const dependencies = new Map();

    const addDependency = dep => {
        ...
    };

    const addDependenciesBlock = block => {
        if (block.dependencies) {
            iterationOfArrayCallback(block.dependencies, addDependency);
        }
        //懒加载 内部 import 木块
        if (block.blocks) {
            iterationOfArrayCallback(block.blocks, addDependenciesBlock);
        }
        //内部变量 __resourceQuery
        if (block.variables) {
            iterationBlockVariable(block.variables, addDependency);
        }
    };
    ...
    this.addModuleDependencies(
        module,
        sortedDependencies,
        this.bail,
        null,
        true,
        callback
    );
}

addModuleDependencies 和 _addModuleChain 很相似,里面调用了 factory.create,重新执行流程,最终生成一个模块对象:

初始化module -> add module -> module build -> afterBuild -> processModuleDependencies


0人赞

分享到: