webpack5之原理分析

·  阅读 1399
webpack5之原理分析

往期文章:

整体流程

  • 1.webpack: ./bin/webpack.js 入口
  • 2.webpack-cli/bin/cli.js
  • 3.webpack-cli/lib/bootstrap.js:初始化命令参数比如 development
  • 4.webpack-cli/lib/webpack-cli: run
  • 5.webpack: 回到 webpack,然后找到文件 package.json.main -> webpack/lib/index.js -> webpack/lib/webpack.js -> compiler,绕了一大圈,终于特么到了核心文件
    • hooks,说白了,就是webpack定义了webpack执行过程中插件的事件接口,外部插件通过订阅对应的事件用callback处理

整体流程源码分析

1.我们执行 webpack --mode development/production 时,首先在包里面找 webpack,如果包里没有再到外面找 .bin/webpack -> package.json ("bin": {"webpack": "bin/webpack.js"}) 即 webpack: bin/webpack.js

const runCommand = (command, args) => {
  const cp = require('child_process');
  return new Promise((resolve, reject) => {
    const executedCommand = cp.spawn(command, args, { stdio: 'inherit', shell: true });
    executedCommand.on('error', error => reject(error));
    // process.exit() 如果返回0,表示退出成功
    executedCommand.on('exit', code => (code === 0 ? resolve() : reject()));
  });
};

const isInstalled = packageName => {
  try {
    require.resolve(packageName);
    return true;
  } catch (err) {
    return false;
  }
};

const runCli = cli => {
  const path = require('path');
  const pkgPath = require.resolve(`${cli.package}/package.json`);
  // eslint-disable-next-line node/no-missing-require
  const pkg = require(pkgPath);
  // eslint-disable-next-line node/no-missing-require
  require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};

const cli = {
  name: 'webpack-cli',
  package: 'webpack-cli',
  binName: 'webpack-cli',
  installed: isInstalled('webpack-cli'),
  url: 'https://github.com/webpack/webpack-cli',
};

if (!cli.installed) {
  // ....命令交互,安装 webpack-cli成功后
  // runCommand
  runCli(cli);
} else {
  runCli(cli); // 执行 webpack-cli,找到 webpack-cli/bin/cli.js
}
复制代码

2.webpack-cli/bin/cli.js

require('v8-compile-cache'); // https://www.npmjs.com/package/v8-compile-cache
const runCLI = require('../lib/bootstrap');
process.title = 'webpack';
// 获取 wepback 后面的参数,比如 webpack --mode development, 这里 rawArgs就是 development
const [, , ...rawArgs] = process.argv;

if (packageExists('webpack')) {
    runCLI(rawArgs);
复制代码

v8-compile-cache 这玩意高了,通过使用 V8 的代码缓存来加快实例化时间,

“代码缓存”是在V8 进行的解析和编译工作时设置缓存目录,V8_COMPILE_CACHE_CACHE_DIR 或默认为<os.tmpdir()>/v8-compile-cache-<V8_VERSION>

用法:安装后,直接 require 就行了

  • 基准测试
模组没有缓存带缓存
babel-core218ms185ms
yarn153ms113ms
yarn (捆绑)228ms105ms
  • 核心代码
var wrapper = Module.wrap(content);

var invalidationKey = crypto.createHash('sha1').update(content, 'utf8').digest('hex');

var buffer = this._cacheStore.get(filename, invalidationKey);

var script = new vm.Script(wrapper, {
  filename: filename,
  lineOffset: 0,
  displayErrors: true,
  cachedData: buffer,
  produceCachedData: true,
});

if (script.cachedDataProduced) {
  this._cacheStore.set(filename, invalidationKey, script.cachedData);
} else if (script.cachedDataRejected) {
  this._cacheStore.delete(filename);
}

var compiledWrapper = script.runInThisContext({
  filename: filename,
  lineOffset: 0,
  columnOffset: 0,
  displayErrors: true,
});

return compiledWrapper;
复制代码
    • new vm.script() nodejs 预编译
    • vm 模块可在 V8 引擎上下文中编译和运行代码

3.webpack-cli/lib/bootstrap.js:初始化命令参数比如 development

const WebpackCLI = require('./webpack-cli');
process.title = 'webpack-cli';
const runCLI = async cliArgs => {
  // 初始化参数
  const parsedArgs = argParser(core, cliArgs, true, process.title);
  const cli = new WebpackCLI();
  // core 命令集合
  await cli.run(parsedArgsOpts, core);
};
复制代码

4.webpack-cli/lib/webpack-cli: run

const webpack = packageExists('webpack') ? require('webpack') : undefined;
class WebpackCLI {
  createCompiler(options, callback) {
    compiler = webpack(options, callback);
  }
  async run(args) {
    compiler = this.createCompiler(options, callback);
    return Promise.resolve();
  }
}
复制代码

5.webpack: 回到 webpack,然后找到文件 package.json.main -> webpack/lib/index.js 绑定webpack钩子事件 -> webpack/lib/webpack.js,

const fn = lazyFunction(() => require("./webpack"));
module.exports = mergeExports(fn, {
 ...
 get Cache() {
   return require("./Cache");
 },
 get Chunk() {
   return require("./Chunk");
 },
 get Compilation() {
	return require("./Compilation");
 },
 get Compiler() {
	return require("./Compiler");
 },
 ...
 get DefinePlugin() {
   return require("./DefinePlugin");
 },
 cache: {
   ...
 },
 config: {
   ...
 },
 ids: {
   ...
 },
 javascript: {
   ...
 },
 optimize: {
   ...
 },
 runtime: {
   ...
 },
 ...
 experiments: {
   ...
 }
})
复制代码

6.webpack/lib/webpack.js

// 多入口
const createMultiCompiler = childOptions => {
  const compilers = childOptions.map(options => createCompiler(options));
  const compiler = new MultiCompiler(compilers);
  for (const childCompiler of compilers) {
    // 收集依赖
    if (childCompiler.options.dependencies) {
      compiler.setDependencies(
        childCompiler,
        childCompiler.options.dependencies
      );
    }
  }
  return compiler;
};

// 单入口
const createCompiler = rawOptions => {
  // 根据项目webpack.config 组装 webpack 待执行的配置文件
  const options = getNormalizedWebpackOptions(rawOptions);
  // F(options, "context", () => process.cwd());
  // 设置webpack执行上下文所在工作目录
  applyWebpackOptionsBaseDefaults(options);
  // ** 创建编译实例
  const compiler = new Compiler(options.context);
  compiler.options = options;
  // 将插件挂载在 compiler上
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  // 抹平平台编译环境
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  // 根据 配置,处理 compiler
  new WebpackOptionsApply().process(options, compiler);
  // 初始化 hooks 钩子
  compiler.hooks.initialize.call();
  return compiler;
};

module.exports = (( options, callback ) => {
  const create = () => {
    // 检查你的 webpack.config 是否符合规范
    validateSchema(webpackOptionsSchema, options);
    // 此处先只分析 单入口
    let compiler = createCompiler(options);
    let watch = options.watch;
    let watchOptions = options.watchOptions;
    return { compiler, watch, watchOptions };
  };

  if (callback) {
    const { compiler, watch, watchOptions } = create();
    if (watch) {
	  compiler.watch(watchOptions, callback);
    } else {
      compiler.run((err, stats) => {
      compiler.close(err2 => {
        callback(err || err2, stats);
      });
    });
   }
   return compiler;
 } else {
   const { compiler, watch } = create();
   return compiler;
 } 
});
复制代码

绕了一大圈,终于特么到了核心处理逻辑: compiler

详细分析 compiler

流程图

代码分析

1.webpack/lib/Compiler.js 入口 run (tapable请参考 轻松搞定Tapable)

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require("tapable");
module.exports = class Compiler {
  constructor(context) {
    // 定义webpack 的 钩子
    this.hooks = Object.freeze({
      initialize: new SyncHook([]),
      done: new AsyncSeriesHook(["stats"]),
      afterDone: new SyncHook(["stats"]),
      run: new AsyncSeriesHook(["compiler"]),
      emit: new AsyncSeriesHook(["compilation"]),
      assetEmitted: new AsyncSeriesHook(["file", "info"]),
      afterEmit: new AsyncSeriesHook(["compilation"]),
      thisCompilation: new SyncHook(["compilation", "params"]),
      ...
      compile: new SyncHook(["params"]),
      make: new AsyncParallelHook(["compilation"]),
      ...
    });
    this.webpack = webpack; // webpack.js
    this.running = false;
    this.cache = new Cache();
    ...
  }
  
  // 1.开始入口 run
  run(callback) {
    if (this.running) { return callback(...)}
    // 打包结束时的callback(涉及 ProgressPlugin,watch、IdleFileCachePlugin)
    const finalCallback = (err, stats) => {
      this.running = false;
      // 打包失败(涉及)
      err && this.hooks.failed.call(err);
	  callback && callback(err, stats);
      this.hooks.afterDone.call(stats);
    }
    
    // 记录开始时间
    const startTime = Date.now();
	this.running = true;
    
    const run = () => {
      // 编译前(涉及ProgressPlugin、NodeEnvironmentPlugin)
      this.hooks.beforeRun.callAsync(this, err => {
        if (err) return finalCallback(err);
        // 涉及ProgressPlugin
        this.hooks.run.callAsync(this, err => {
          if (err) return finalCallback(err);
          // 开始编译
          this.compile((err, compilation) => {
            // 如果可以结束
            if (this.hooks.shouldEmit.call(compilation) === false) {
              compilation.startTime = startTime;
              compilation.endTime = Date.now();
              const stats = new Stats(compilation);
              this.hooks.done.callAsync(stats, err => {
              if (err) return finalCallback(err);
                return finalCallback(null, stats);
               });
             }   
             process.nextTick(() => {
                this.emitAssets(compilation, err => {
                  this.hooks.done.callAsync(stats, err => {                  this.hooks.additionalPass.callAsync(err => {
                    this.compile(onCompiled);
                  })
                })
             });
            });
          });
       });
     })
    }
    // run 
    run();
  }
  
  // 2.compile  编译
  compile(callback) {
    ...
    // 2.1. before compile(ProgressPlugin、DllReferencePlugin)
    this.hooks.beforeCompile.callAsync(params, err => {
      // 2.2 compile(ExternalsPlugin -> ExternalModuleFactoryPlugin)
      this.hooks.compile.call(params);
      const compilation = this.newCompilation(params);
      // 3. make (EntryPlugin 收集依赖)
      this.hooks.make.callAsync(compilation, err => {
        this.hooks.finishMake.callAsync(compilation, err => {
          process.nextTick(() => {
            compilation.finish(err => {
              compilation.seal(err => {
                this.hooks.afterCompile.callAsync(compilation, err => {
                  // onCompiled              
                  return callback()
                })
              })
            })
          });
        });
      });
    })
  }
}
复制代码

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B

分类:
前端
标签: