Appearance
工作流程
- 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化 Compiler对象
- 加载所有配置的插件,传入 compiler实例
- 执行 compiler对象的run方法,开始编译
- 根据配置中的 entry 找出入口文件
- 从入口文件出发,调用所有配置的 Loader 对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
- 再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置对象
/**
 * webpack 是一个函数,参数是配置对象
 * @param {*} options
 * @returns 返回compiler实例
 */
const Compiler = require('./Compiler');
function webpack(options) {
  // 1.初始化参数:将配置文件和shell语句参数合并,得到一个最终的配置对象
  // 拿到命令行参数process.argv
  let shellConfig = process.argv.slice(2).reduce((shellConfig, current) => {
    const [key, value] = current.split('=');
    shellConfig[key.slice(2)] = value;
    return shellConfig;
  }, {});
  options = Object.assign({}, options, shellConfig);
  return compiler;
}
module.exports = webpack;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2. 用上一步得到的参数初始化 Compiler 对象 
const compiler = new Compiler(options);
1
3. 加载所有配置的插件,传入 compiler 实例 
const plugins = options.plugins;
plugins.forEach(plugin => {
  plugin.apply.call(compiler, compiler);
});
1
2
3
4
2
3
4
4. 执行 compiler 对象的 run 方法,开始编译 
const config = require('./webpack.config');
const compiler = webpack(config);
compiler.run((err, stats) => {
   console.log(stats.toJson(
     {
       assets: true,
       chunks: true,
       modules: true,
       entries: true,
     }
   ));
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
5. 根据配置中的 entry 找出入口文件
class Compiler {
  constructor(options) {
    this.options = options;
    this.hooks = {
      //钩子
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook()
    };
  }
  run() {
    this.hooks.run.call();
    // 编译阶段
    // 5.从配置文件中找到entry入口文件
    // entry格式化成一个对象,如果配置的是但入口(字符串)就是属性为main的对象
    let entries = {},
      entry = this.options.entry;
    if (typeof entry === 'string') entries['main'] = entry;
    else entries = entry;
    this.hooks.done.call();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
6. 从入口文件出发,调用所有配置的 Loader 对模块进行编译
 buildModule(entryName, modulePath) {
    const moduleSource = fs.readFileSync(modulePath, 'utf-8');
    let targetSource = moduleSource;
    const rules = this.options.module.rules;
    let loaders = []; //loader倒序执行
    rules.forEach(rule => {
      if (rule.test.test(modulePath)) {
        if (Array.isArray(rule.use)) loaders.push(...rule.use);
        else loaders.push(rule.use);
      }
    });
    for (let i = loaders.length - 1; i >= 0; i--) {
      targetSource = require(loaders[i])(targetSource);
    }
    return module;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
let ast = parser.parse(targetSource, { sourceType: 'module' });
traverse(ast, {
  CallExpression: nodePath => {
    const { node } = nodePath;
    if (node.callee.name === 'require') {
      //1.处理文件后缀 ; 2.拿到依赖模块的绝对路径
      let depModuleName = node.arguments[0].value; // ./title
      const dirPath = path.dirname(modulePath);
      let depModulePath = path.posix.join(dirPath, depModuleName);
      depModulePath = tryExtension(
        depModulePath,
        this.options.resolve.extensions,
        depModuleName,
        dirPath
      );
      const depModuleId = getModuleID(depModulePath);
      node.arguments = [types.stringLiteral(depModuleId)];
      module.dependencies.push(depModulePath);
    }
  }
});
module.dependencies.forEach(dependence => {
  const depModule = this.buildModule(entryName, dependence);
  this.modules.push(depModule);
});
const { code } = generator(ast);
module._source = code;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
for (let entryName in entries) {
  //entryName :main / page1 / page2
  // 将入口路径entryPath转成绝对路径
  let entryPath = entries[entryName];
  entryPath = path.posix.join(baseDir, entryPath);
  const entryModule = this.buildModule(entryName, entryPath);
  const chunk = {
    name: entryName,
    entryModule,
    modules: this.modules.filter(module => module.entryName === entryName)
  };
  this.chunks.push(chunk);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
9. 再把每个 Chunk 转换成一个单独的文件加入到输出列表
const { path: outputPath, filename } = this.options.output;
this.chunks.forEach(chunk => {
  const ouputDir = (outputPath + '/' + filename).replace('[name]', chunk.name);
  this.assets[ouputDir] = getSource(chunk);
});
this.files = Object.keys(this.assets);
1
2
3
4
5
6
2
3
4
5
6
10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
//写入文件之前触发emit钩子
this.hooks.emit.call();
this.files.forEach(file => {
  fs.writeFileSync(file, this.assets[file]);
});
1
2
3
4
5
2
3
4
5
 Cmq Webpack
Cmq Webpack