读取配置比较简单,webpack是支持两种形式指定配置文件,一个是通过-c参数传入,另一个是默认约定当前工作路径的webpack.config.js。

    1. const path = require('path');
    2. const config = require(path.resolve('webpack.config.js'));

    我们先来考虑简单的情况,即是有默认的文件。

    webpack在加载好配置后,会实例化出Compiler对象,负责webpack整个构建的调度,生命周期,发布订阅等。每一次文件变动产生的构建,又会产生一个Complation对象(继承自Compiler)。

    const Compiler = require('./core/compiler');
    
    const compiler = new Compiler(config);
    
    compiler.run();
    

    第一步很简单,引入Compiler类,然后实例化,并像构造函数传入config配置对象。调用compiler的run方法,来启动构建。实际上run方法还有自己的回调参数,遵守了node.js的回调标准,我们暂时先不细说。

    
    class Compiler {
    
        constructor(config) {
            // 记录入口文件的id
            this.entryId = '';
            // module
            this.modules = {};
            // 储存config文件
            this.config = config;
            // 找到entryFile
            this.entryFile = path.resolve(this.config.entry);
            // 工作目录
            this.workspace = path.resolve();
        }
    
    }
    

    我们再来看下Compiler类,首先,构造函数接收webpack配置,我们创建了modules,即是上一页讲过的sourceMoudules。用来储存文件的源码内容,config缓存在自己的成员属性里,然后解析出entryFile的路径,在真实的webpack配置里,entry还支持对象,数组等结构,我们暂且只关心最简单的字符串类型。

    在webpack执行文件中,我们使用了compiler的run方法。

    class Compiler {
    
        constructor(config) {
            // 记录入口文件的id
            this.entryId = '';
            // module
            this.modules = {};
            // 储存config文件
            this.config = config;
            // 找到entryFile
            this.entryFile = path.resolve(this.config.entry);
            // 工作目录
            this.workspace = path.resolve();
        }
    
        async run() {
            // 构建
            await this.buildMode(this.entryFile , true);
            // 发射文件
            await this.emitFile();
        }
    
    }
    

    run方法里我们按照单一职责原则来拆分下接下来要干的事,分别是构建解析代码还有把解析好的代码再生成文件。

    class Compiler {
    
        constructor(config) {
            // 记录入口文件的id
            this.entryId = '';
            // module
            this.modules = {};
            // 储存config文件
            this.config = config;
            // 找到entryFile
            this.entryFile = path.resolve(this.config.entry);
            // 工作目录
            this.workspace = path.resolve();
        }
    
        async run() {
            // 构建
            await this.buildMode(this.entryFile , true);
            // 发射文件
            await this.emitFile();
        }
    
        async getSource(path) {
            return fs.readFile(path , 'utf8');
        }
    
        async buildMode(modulePath , entry) {
            // 读取文件内容
            const source = await this.getSource(modulePath);
            // 获取到模块基于工作空间中的相对路径差值
            // 这部分差值,即是moduleId
            // 即 /workspace/src - /workspace = ./src
            const moduleName = `./${ path.relative(this.workspace , modulePath) }`;
            // 获取解析模块依赖,且转换后的源码和依赖。
            const { sourceCode , dependencies } = this.parse(source , path.dirname(moduleName));
    
            this.modules[moduleName] = sourceCode;
    
            if (entry) {
                this.entryId = moduleName;
            }
    
            // 将依赖项,进一步的分析构建,进行递归
            await Promise.all(dependencies.map(async dep => {
                await this.buildMode(dep , false);
            }));
    
        }
    
    }
    

    buildMode里做了几件事,解析出基于工作目录的相对路径,调用parse方法获取文件源码和该文件所引用的依赖项。拿到源码后,根据id注册源码。如果是entry,那么就就把entryId记录上。接下来,根据依赖项,继续分析依赖,如果这会不好理解,一会我们回过来看为什么需要递归。暂且把重点放在parse方法是如何解析源码并获取依赖的,parse里是怎么转换成webpack实现的commonjs的。