webpack工作流程

调试

方法一:

1、node –inspect-brk ./node_modules/webpack-cli/bin/cli.js

2、在Chrome中打开一个新窗口,打开控制台,再刷新,在控制台左上角就会出现绿色六角形,代表node脚本,点这个icon,就会打开新的控制台

3、点左侧的文件,会自动进入断点

方法二:

通过配置文件launch.json,然后再启动调试

流程概况

1.初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象

2.用上一步得到的参数初始化 Compiler 对象

3.加载所有配置的插件

4.执行对象的 run 方法开始执行编译

5.根据配置中的entry找出入口文件

6.从入口文件出发,调用所有配置的Loader对模块进行编译

7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk

9.再把每个 Chunk 转换成一个单独的文件加入到输出列表

10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

\

假设我们有debugger.js,内容如下:

const webpack = require('./webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
//4.执行compiler对象的 run 方法开始执行编译
compiler.run((err, stats) => {
    console.log(err);
    console.log(stats.toJson());
});
复制代码

控制台执行:

node debugger.js title=zhufeng age=10 –mode=development

在执行过程中,进程的任意位置console.log(process.argv),得到的结果为:

[
	'node可执行文件的绝对路径',
  'debugger.js的绝对路径',
  'title=zhufeng',
  'age=10',
  '--mode=development'
]
复制代码

处理参数 初始化Compiler 加载插件

const Compiler = require('./Compiler');
function webpack(options) {
    //1.初始化参数:从配置文件和 Shell 语句中读取并合并参数, 得出最终的配置对象
    const argv = process.argv.slice(2);//[ '--mode=development']
    const shellOptions = argv.reduce((shellOptions, option) => {
        let [key, value] = option.split('=');
        shellOptions[key.slice(2)] = value;
        return shellOptions;
    }, {});
    const finalOptions = { ...options, ...shellOptions };
    //2.用上一步得到的参数初始化 Compiler 对象
    let compiler = new Compiler(finalOptions);
    //3.加载所有配置的插件
    finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
    return compiler;
}

module.exports = webpack;
复制代码

执行对象的 run 方法

这一步在自己写的构建初始化文件中,在本例中就是debugger.js

const webpack = require('./webpack');
const options = require('./webpack.config');
const compiler = webpack(options);
//4.执行compiler对象的 run 方法开始执行编译
compiler.run((err, stats) => {
    console.log(err);
    console.log(stats.toJson());
});
复制代码

根据配置中的entry找出入口文件

Compiler.compile的调用和一个Compilation对象有一一映射关系

这一步在Compilation对象中,从代码中可以看到当entry只给了一个字符串时,会将其转换为一个main入口

class Compilation {
    constructor(options) {
        this.options = options;
    }
    build(onCompiled) {
        //5.根据配置中的entry找出入口文件
        let entry = {};
        //兼容entry的值是对象和字符串的情况
        if (typeof this.options.entry === 'string') {
            entry.main = this.options.entry;
        } else {
            entry = this.options.entry;
        }
复制代码

从入口文件出发,调用所有配置的Loader对模块进行编译

class Compilation {
    constructor(options) {
        this.options = options;
    }
    build(onCompiled) {
        //5.根据配置中的entry找出入口文件
        let entry = {};
        //兼容entry的值是对象和字符串的情况
        if (typeof this.options.entry === 'string') {
            entry.main = this.options.entry;
        } else {
            entry = this.options.entry;
        }
        for (let entryName in entry) {
            //获取到了所有的入口文件的绝对路径
            let entryPath = path.join(baseDir, entry[entryName]);
            this.fileDependencies.push(entryPath);
            //6.从入口文件出发,调用所有配置的Loader对模块进行编译
            let entryModule = this.buildModule(entryName, entryPath);
        }
        onCompiled(null, {}, this.fileDependencies);
    }
    buildModule(name, modulePath) {
      //6.从入口文件出发,调用所有配置的Loader对模块进行编译
      //6.1读取源代码的内容
      let sourceCode = fs.readFileSync(modulePath, 'utf8');
      //6.2匹配此模块需要使用的loader
      let { rules } = this.options.module;
      let loaders = [];
      rules.forEach(rule => {
        //如果正则匹配上了,则把此rule对应的loader添加到loaders数组里
        if (modulePath.match(rule.test)) {
          loaders.push(...rule.use);
        }
      });
      sourceCode = loaders.reduceRight((sourceCode, loader) => {
        return require(loader)(sourceCode);
      }, sourceCode);
复制代码

再寻找依赖模块,递归处理

基本思路:通过遍历ast语法树,拦截到当前文件中的require调用,找到依赖后再进行编译

通常来讲,为了避免耦合,我们不会在import或require里加上文件后缀:

let title = require('./title');
复制代码

所以我们需要在webpack的配置文件中添加resolve参数来告诉webpack通过什么后缀名来识别

module.exports = {
    // ...
    entry: {
        // ...
    },
    output: {
        // ...
    },
    resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
    },
复制代码

webpack遇到每个文件时,会挨个尝试加上这些扩展名去寻找对应文件是否存在(通过方法tryExtensions)

文件和模块一一映射,每个模块都有一个标识:moduleId,是相对于项目根目录的相对路径

每个模块也都有一个对象module来描述其信息

module = {
  id: moduleId,
  dependencies: [],
  // 表示此模块添几个入口依赖了,入口的名称 [entry1,entry2]
  names: [name]
}
复制代码

\

// 需要增加babel依赖,用来对代码进行转换以及分析
const types = require('@babel/types');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;

class Compilation {
    constructor(options) {
    	// ...
    }
    build(onCompiled) {
    	// ...
    }
    buildModule(name, modulePath) {
        // ...
        sourceCode = loaders.reduceRight((sourceCode, loader) => {
          return require(loader)(sourceCode);
        }, sourceCode);
        //7.再找出该模块依赖的模块,再递归本步骤(buildModule)直到所有入口依赖的文件都经过了本步骤的处理
        //"./src/title.js" 每个模块都有一个ID,   id: './src/entry1.js',
        //模块ID就是相对于项目根目录的相对路径
        let moduleId = "./" + path.relative(baseDir, modulePath);
        //创建一个模块对象,moduleId是相对于项目根目录的相对路径 dependencies表示此模块依赖的模块 
        //names表示此模块添几个入口依赖了,入口的名称 [entry1,entry2]
        let module = { id: moduleId, dependencies: [], names: [name] };
        let ast = parser.parse(sourceCode, { sourceType: 'module' });
        traverse(ast, {
            CallExpression: ({ node }) => {
                if (node.callee.name === 'require') {
                    let depModuleName = node.arguments[0].value;//./title
                    //获取当前模块所有的目录 C:\aproject\zhufengwebpack202111\4.flow\src
                    let dirname = path.dirname(modulePath);
                    //获取依赖的模块的绝对路径 C:/aproject/zhufengwebpack202111/4.flow/src/title
                    let depModulePath = path.join(dirname, depModuleName);
                    //获取当前支持扩展名
                    let extensions = this.options.resolve.extensions;
                    //获取依赖的模块的绝对路径
                    depModulePath = tryExtensions(depModulePath, extensions);
                    //把此依赖文件添加到依赖数组里,当文件变化了,会重新启动编译 ,创建一个新的Compilation
                    this.fileDependencies.push(depModulePath);
                    //获取依赖模块的模块iD,也就是相对于根目录的相对路径
                    let depModuleId = './' + path.relative(baseDir, depModulePath);
                    //修改AST语法对,把require方法的参数变成依赖的模块ID
                    node.arguments = [types.stringLiteral(depModuleId)];
                    //把依赖信息添加到依赖数组里
                    module.dependencies.push({ depModuleId, depModulePath });
                }
            }
        });
        let { code } = generator(ast);
        module._source = code;
        module.dependencies.forEach(({ depModuleId, depModulePath }) => {
            let buildedModule = this.modules.find(module => module.id === depModuleId);
            if (buildedModule) {
                //title这个module.names = [entry1,entry2];
                buildedModule.names.push(name);
            } else {
                let depModule = this.buildModule(name, depModulePath);
                this.modules.push(depModule);
            }

        });
        return module;
    }
}
  	
复制代码

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk

这一步是在所有module编译完之后进行的,所以应该在build方法的最后,这里的规则是一个entry对应一个chunk:

class Compilation {
    constructor(options) {
        this.options = options;
        this.modules = [];//存放本次编译的所有的模块
        //当前编译依赖的文件
        this.fileDependencies = [];
        //里面放置所有的代码块
        this.chunks = [];
        this.assets = {};
        this.hooks = {
            chunkAsset: new SyncHook(["chunk", "filename"])
        }
    }
    build(onCompiled) {
        //5.根据配置中的entry找出入口文件
        let entry = {};
        //兼容entry的值是对象和字符串的情况
        if (typeof this.options.entry === 'string') {
            entry.main = this.options.entry;
        } else {
            entry = this.options.entry;
        }
        for (let entryName in entry) {
            //获取到了所有的入口文件的绝对路径
            let entryPath = path.join(baseDir, entry[entryName]);
            this.fileDependencies.push(entryPath);
            //6.从入口文件出发,调用所有配置的Loader对模块进行编译
            let entryModule = this.buildModule(entryName, entryPath);
            //8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
            let chunk = {
                name: entryName,//代码块名称是入口名称
                entryModule,///入口模块
                //这个入口代码块中包含哪些模块
                modules: this.modules.filter(module => module.names.includes(entryName))
            }
            this.chunks.push(chunk);
            //9.再把每个 Chunk 转换成一个单独的文件加入到输出列表
            this.chunks.forEach(chunk => {
                let filename = this.options.output.filename.replace('[name]', chunk.name);
                this.hooks.chunkAsset.call(chunk, filename);
                this.assets[filename] = getSource(chunk);
            });
        }
        onCompiled(null, {
            modules: this.modules,
            chunks: this.chunks,
            assets: this.assets
        }, this.fileDependencies);
    }
  
  
  function getSource(chunk) {
    return `
   (() => {
    var modules = {
      ${chunk.modules.map(
        (module) => `
        "${module.id}": (module) => {
          ${module._source}
        },
      `
    )}  
    };
    var cache = {};
    function require(moduleId) {
      var cachedModule = cache[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = (cache[moduleId] = {
        exports: {},
      });
      modules[moduleId](module, module.exports, require);
      return module.exports;
    }
    var exports ={};
    ${chunk.entryModule._source}
  })();
   `;
}
复制代码

最终生成文件

这一步是所有模块拼接好之后进行的,在最后的onCompiled的回调中:

class Compiler {
    constructor(options) {
        this.options = options;
        //存的是当前的Compiler上面的所有的钩子
        this.hooks = {
            run: new SyncHook(), //开始编译的时候触发
            done: new SyncHook(), //编译结束的时候触发
            compilation: new SyncHook(["compilation", "params"]),
        }
    }
    //4.执行对象的 run 方法开始执行编译
    run(callback) {
        //在执行Compiler的run方法开头触发run这个钩子
        this.hooks.run.call();
        const onCompiled = (err, stats, fileDependencies) => {
            //10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
            for (let filename in stats.assets) {
                let filePath = path.join(this.options.output.path, filename);
                fs.writeFileSync(filePath, stats.assets[filename], 'utf8');
            }
            callback(null, {
                toJson: () => stats
            });
            fileDependencies.forEach(fileDependency => {
                fs.watch(fileDependency, () => this.compile(onCompiled));
            });
        }
        this.compile(onCompiled);
        //编译过程....
        this.hooks.done.call();
    }
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享