1. 初始化参数

1. 初始化参数

  • 从配置文件和shell语句中读取与合并参数,得到最终参数
    • process.argv是shell语句上的参数,npm run build —mode=development,最终获取的是是[‘—mode=development’],前两项被截取,没有用
    • 通过split字符串分割得到key和value添加到一个空对象中
    • 把webpack.config.js和shell语句的脚本解构到一个新的对象中进行合并,得到最终的参数结果

2. 开始编译

2.1 初始化Compiler对象

  • 初始化Compiler对象,把options参数传入

    2.2 加载所有plugin

    2.3 执行对象(Compiler)的run方法开始编译

  • Compiler是全局的单例,只有一个

  • Compilcation每一次编译都会创建一个实例

    2.4 根据配置文件中的entry找到所有的入口文件

3. 编译模块

3.1 根据所有的入口文件,调用所有的配置的Loader对模块进行编译

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

4. 完成编译

4.1 根据入口文件和模块的依赖关系组装成一个个包含多个模块的chunk

5. 输出资源

5.1 在把每一个chunk转换成单独的文件添加到输出列表

6. 写入文件

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

webpack.js

1. 初始化参数

2. 初始化compiler对象

3. 加载所有插件

  1. const Compiler = require('./Compiler);
  2. function webpack(options) {
  3. // 1. 初始化参数
  4. // 从shell语句中截取参数
  5. const argv = process.argv.slice(2);
  6. // 把shell语句从a=b 组装成一个对象形势
  7. const shellOptions = argv.reduce((shellOptions, option) => {
  8. const [key, value] = option.split('=');
  9. shellOptions[key] = value;
  10. return shellOptions
  11. }, {});
  12. // 把webpage配置文件和shell语句参数合并成一个对象
  13. const finalOptions = {
  14. ...options,
  15. ...shellOptions
  16. };
  17. // 2. 初始化Compiler对象
  18. const compiler = new Compiler(finalOptions);
  19. // 3. 加载所有的插件,把compiler对象传递到apply的形参中
  20. finalOptions.plugins.forEach(plugin => plugin.apply(compiler));
  21. };
  22. module.exports = webpack;

Compiler.js

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

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

  • 执行run方法开始编译,可以执行tapable的call方法来执行插件
  • 没执行一次新的编译,都会创建一个Compilation类的实例 ```javascript 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();

  1. function compiled(err, state, fileDependencies) {
  2. // 10. 确定输出内容,根据配置确定输出路径和文件名,把文件写入到文件系统
  3. for (let filename in stats.assets) {
  4. // 拼接输出路径和文件名
  5. let filePath = path.join(this.options.output.path, filename);
  6. // 调用fs把文件内容写到文件系统
  7. fs.writeFileSync(filePath, stats.assets[filename], "utf8");
  8. }
  9. callback(err, {
  10. toJson: () => stats,
  11. });
  12. // 监听文件变化,如果文件发生变化之后会从新调取编译方法
  13. fileDependencies.forEach(file => {
  14. fs.watch(file, () => {
  15. this.compiler(compiled)
  16. });
  17. });
  18. };
  19. this.compiler(compiled);
  20. this.hooks.done.call();

}

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

module.exports = Compiler;


<a name="F3Lxe"></a>
### Compilcation.js
<a name="oiNnO"></a>
#### 5. 根据配置中的entry找到所有的入口文件
<a name="IS6nn"></a>
#### 6. 根据入口文件,调用所有的Loader配置对模块进行编译
<a name="kT3Ic"></a>
#### 7. 再找出该模块所依赖的模块,递归本步骤直到所有的入口文件依赖的模块都经过了本步骤的处理
<a name="s8zsr"></a>
#### 8. 根据入口和模块的依赖关系组装成一个个包含多个模块的chunk
<a name="UMl03"></a>
#### 9. 把每个chunk转换成单独的文件添加到输出列表中

- **path.posix**是为了统一路径上的斜杠,在window环境和mac环境下路径的斜杠不一样,所以这个字段可以帮忙统一格式
- **process.cwd()**是为了获取当前目录
- **@babel/parser**是把源代码解析成ast语法树
- **@babel/traverse**可以用来遍历parser生成的ast语法树
- **@babel/generator**是把ast生成源代码
```javascript
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;