还不了解webpack工作流?看看这篇文章

109 阅读5分钟

我想webpack工作量,无论是面试还是对了解webpack的内部实现都很重要,我最近也是学习了一下,所以把自己的心得体会记录一下,希望也能帮助到其他有兴趣学习的同学!

首先我们可以将它分成几步来方便记忆

  1. 初始化参数
  2. 初始化Compiler对象
  3. 获取所有plugin并执行
  4. 开始编译,执行Compiler对象的run方法
  5. 根据配置文件获取所有的入口文件
  6. 根据入口文件调用所有的loader对模块进行编译
  7. 找出入口文件所依赖的模块,递归本步骤直到所有的入口依赖都经过本步骤的处理
  8. 根据入口文件和模块的依赖关系,组装成一个个的chunk
  9. 把chunk生成对应的文件添加到输出列表中
  10. 确定输出的内容,根据配置确定输出文件的路径和文件名,把文件写入到文件系统

然后我们接下来先看前三步

webpack.js

  1. 初始化参数
  2. 初始化Compiler对象
  3. 加载所有的插件,把compiler对象传递到apply的形参中
    const Compiler = require('./Compiler);

    function webpack(options) {
      // 1. 初始化参数
      const argv = process.argv.slice(2);
      const shellOptions = argv.reduce((shellOptions, option) => {
        const [key, value] = option.split('=');
        shellOptions[key] = value;
        return shellOptions
      }, {});

      const finalOptions = {
        ...options,
        ...shellOptions
      };

      // 2. 初始化Compiler对象
      const compiler = new Compiler(finalOptions);

      // 3. 加载所有的插件,把compiler对象传递到apply的形参中
      finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
    };

    module.exports = webpack;

Compiler.js

  1. 执行对象的run方法开始编译
  2. 确定输出内容,根据配置确定输出路径和文件名,把文件写入到文件系统
const fs = require('fs');
const { SyncHook } = require('tapable');
const Compilcation = require('./Compilcation');

class Compiler {
    constructor(options) {
        this.options = options;
        this.hooks = {
            run: new SyncHook(),
            done: new SyncHook()
        }
    }
    // 4. 执行对象的run方法
    run(callback) {
        // 在这可以执行插件的钩子函数
        this.hooks.run.call();

        function compiled(err, state, fileDependencies) {
          // 10. 确定输出内容,根据配置确定输出路径和文件名,把文件写入到文件系统
          for (let filename in stats.assets) {
            // 拼接输出路径和文件名
            let filePath = path.join(this.options.output.path, filename);
            // 调用fs把文件内容写到文件系统
            fs.writeFileSync(filePath, stats.assets[filename], "utf8");
          }

          callback(err, {
            toJson: () => stats,
          });

          // 监听文件变化,如果文件发生变化之后会从新调取编译方法
            fileDependencies.forEach(file => {
            fs.watch(file, () => {
                    this.compiler(compiled)
            });
          });
        };

        // 每次编译都会创建一个新的compilcation
        this.compiler(compiled);
        this.hooks.done.call();
      }

      // 每次编译都会创建一个新的compilcation
      compiler(callback) {
        const complication = new Compilcation(this.options);
        complication.build(callback)
      }
}

module.exports = Compiler;

Compilcation.js

  1. 根据配置中的entry找到所有的入口文件
  2. 根据入口文件,调用所有的Loader配置对模块进行编译
  3. 再找出该模块所依赖的模块,递归本步骤直到所有的入口文件依赖的模块都经过了本步骤的处理
  4. 根据入口和模块的依赖关系组装成一个个包含多个模块的chunk
  5. 把每个chunk转换成单独的文件添加到输出列表中
const path = require('path');
const baseDir = process.cwd();
const fs = require('fs')
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generatar = require('@babel/generator').default;

class Compilcation {
	constructor(options) {
  	this.options = options;
    this.fileDependencies = [];
    this.modules = [];
    this.chunks = [];
    this.assets = {};
  }
  
  build(callback) {
    // 5. 根据配置中的entry找到所有的入口文件
  	let 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.posix.join(baseDir, entry[entryName]);
      this.fileDependencies.push(entryPath);
      
      // 6. 根据入口文件调用所有的配置Loader对模块进行编译
      const entryModule = this.buildModule(entryName, entryPath);
      
      // 8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk
      let chunk = {
      	name: entryName,
        entryModule,
        // 包含就表示入口文件依赖了这个模块
        modules: this.modules.filter((item) => item.names.includes(entryName))
      };
      this.chunks.push(chunk);
    }
    
    // 9. 把每个chunk转换成单独的文件添加到输出列表
    this.chunks.forEach(chunk => {
      // 替换output中的输出文件名称
    	let filename = this.options.output.filename.replace("[name]", chunk.name);
      this.assets[filename] = getSource(chunk);
    });
    
    // 调用callback把参数传回
    callback(null, {
    	chunks: this.chunks,
      modules: this.modules,
      assets: this.assets,
    }, this.fileDependencies);
  }
  
  buildModule(name, modulePath) {
    // 获取到源文件代码
    let sourceCode = fs.readFileSync(modulePath, 'utf8');
    
    // 找到webpack.config.js中的module的rules
    const { rules } = this.options.module;
    
    let loaders = [];
    rules.forEach(rule => {
    	if (modulePath.match(rule.test)) {
         loaders.push(...rule.use);
      }
    });
    
    // 执行配置的Loader,先引用loader,然后执行传入源代码
    sourceCode = loaders.reduceRight((sourceCode, loader) => {
    	return require(loader)(sourceCode);
    }, sourceCode);
    
    // 获取当前的模块ID  ./src/entry1
    let moduleId = './' + path.posix.relative(baseDir, modulePath);
    let moudle = {
    	id: moduleId,
      dependencies: [],
      names: [name]
    };
    
    // 7. 再找出该模块所依赖的模块,递归本步骤直到所有的入口依赖都经过了本步骤的处理
    
    // 把源代码传入,通过解析生成ast语法树
    let ast = parser.parse(sourceCode, {sourceType: "module"}) 
    traverse(ast, {
    	CallExpression: ({ node }) => {
      	if (node.callee.name === "require") {
          // 获取依赖模块的相对路径 wepback打包后不管什么模块,模块ID都是相对于根目录的相对路径 ./src ./node_modules
          let depModuleName = node.arguments[0].value; // ./title
          
          // 获取当前模块的所在的目录
          let dirname = path.posix.dirname(modulePath); //src
          
          //C:\aproject\zhufengwebpack202108\4.flow\src\title.js
          let depModulePath = path.posix.join(dirname, depModuleName);
          
          // 匹配文件扩展名找到对应的文件
          let extensions = this.options.resolve.extensions;
          depModulePath = tryExtensions(depModulePath, extensions);
          
          // 把找到的对应的依赖文件添加到依赖数组中去
          this.fileDependencies.push(depModulePath);
          
          //生成此模块的模块ID
          let depModuleId = "./" + path.posix.relative(baseDir, depModulePath);
          node.arguments = [types.stringLiteral(depModuleId)]; // ./title => ./src/title.js
          
          //把此模块依赖的模块ID和模块路径放到此模块的依赖数组中
          module.dependencies.push({ depModuleId, depModulePath });
        }
      }
    });
    
    // 把ast语法树从新生成为源代码
    let { code } = generator(ast);
    
    // 把新生成的源码指向_source属性上
    module._source = code;
    
    // 7. 再找出该模块所依赖的模块,递归本步骤直到所有的入口依赖都经过了本步骤的处理
    module.dependencies.forEach(({depModuleId, depModulePath}) => {
    	let existModule = this.modules.find(module => module.id === depModuleId);
      if (existModule) {
      	existModule.names.push(name);
      } else {
        let depModule = this.buildModule(name, depModulePath);
      	this.modules.push(depModule)
      };
    });
  }
}

// 获取源文件并输出
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}
  })();
   `;
}

module.exports = Compilcation;

webpack.config.js

const RunPlugin = require('./plugins/run-plugin');
const DonePlugin = require('./plugins/done-plugin');

module.exports = {
  entry: {
    entry1: "./src/entry1.js",
    entry2: "./src/entry2.js",
  },
  output: {
    path: path.resolve("dist"),
    filename: "[name].js",
  },
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx", ".json"],
  },
  module: {
  	rules: [
      {
      	test: /.js$/,
        use: [
        	path.resolve(__dirname, 'loaders/loggers1.js'),
          path.resolve(__dirname, 'loaders/loggers2.js')
        ]
      }
    ]
  },
	plugins: [
  	new RunPlugin(),
    new DonePlugin(),
  ]
}

自定义插件 - RunPlugin.js

  • 插件都是一个类
  • 固定都有一个apply的方法,参数是固定的compiler
  • compiler实例可以调用hooks方法,来监听事件
class RunPlugin {
	apply(compiler) {
  	compiler.hooks.run.tap('runPlugin', () => console.log('开始编译 runplugin执行'))
  }
}
module.exports = Runplugin

自定义插件 - DonePlugin.js

class DonePlugin {
	apply(compiler) {
  	compiler.hooks.run.tap('donePlugin', () => console.log('结束编译 doneplugin执行'))
  }
}
module.exports = Runplugin

自定义loader - logger1.js

function loader(source) {
	return source + '//logger1'
};
module.exports = loader;

自定义loader - logger2.js

function loader(source) {
	return source + '//logger2'
};
module.exports = loader;