webpack 源码

定位 webpack 打包入口

const webpack = require('webpack'); const options = require('./webpack.config.js'); let compiler = webpack(options); compiler.run(function (err, stats) { console.log(err); console.log(stats.toJson()); });

定义 webpack.config.js 文件,执行 npx webpack 和手动引入会产生一样的效果。

执行 npx webpack 会找 node_modules 下 bin 目录下的 webpack 命令。

webpack.cmd

#!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") case `uname` in *CYGWIN*) basedir=`cygpath -w "$basedir"`;; esac if [ -x "$basedir/node" ]; then "$basedir/node" "$basedir/../webpack/bin/webpack.js" "$@" ret=$? else node "$basedir/../webpack/bin/webpack.js" "$@" ret=$? fi exit $ret

=> node webpack/bin/webpack.js

cmd 文件核心作用就是使用 node 命令执行 webpack/bin/webpack.js 文件。

webpack/bin/webpack.js

#!/usr/bin/env node // @ts-ignore process.exitCode = 0; /** * @param {string} command process to run * @param {string[]} args commandline arguments * @returns {Promise<void>} promise */ 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); }); executedCommand.on("exit", code => { if (code === 0) { resolve(); } else { reject(); } }); }); }; /** * @param {string} packageName name of the package * @returns {boolean} is the package installed? */ const isInstalled = packageName => { try { require.resolve(packageName); return true; } catch (err) { return false; } }; /** * @typedef {Object} CliOption * @property {string} name display name * @property {string} package npm package name * @property {string} binName name of the executable file * @property {string} alias shortcut for choice * @property {boolean} installed currently installed? * @property {boolean} recommended is recommended * @property {string} url homepage * @property {string} description description */ /** @type {CliOption[]} */ const CLIs = [ { name: "webpack-cli", package: "webpack-cli", binName: "webpack-cli", alias: "cli", installed: isInstalled("webpack-cli"), recommended: true, url: "https://github.com/webpack/webpack-cli", description: "The original webpack full-featured CLI." }, { name: "webpack-command", package: "webpack-command", binName: "webpack-command", alias: "command", installed: isInstalled("webpack-command"), recommended: false, url: "https://github.com/webpack-contrib/webpack-command", description: "A lightweight, opinionated webpack CLI." } ]; const installedClis = CLIs.filter(cli => cli.installed); if (installedClis.length === 0) { const path = require("path"); const fs = require("fs"); const readLine = require("readline"); let notify = "One CLI for webpack must be installed. These are recommended choices, delivered as separate packages:"; for (const item of CLIs) { if (item.recommended) { notify += `\n - ${item.name} (${item.url})\n ${item.description}`; } } console.error(notify); const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock")); const packageManager = isYarn ? "yarn" : "npm"; const installOptions = [isYarn ? "add" : "install", "-D"]; console.error( `We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join( " " )}".` ); const question = `Do you want to install 'webpack-cli' (yes/no): `; const questionInterface = readLine.createInterface({ input: process.stdin, output: process.stderr }); questionInterface.question(question, answer => { questionInterface.close(); const normalizedAnswer = answer.toLowerCase().startsWith("y"); if (!normalizedAnswer) { console.error( "You need to install 'webpack-cli' to use webpack via CLI.\n" + "You can also install the CLI manually." ); process.exitCode = 1; return; } const packageName = "webpack-cli"; console.log( `Installing '${packageName}' (running '${packageManager} ${installOptions.join( " " )} ${packageName}')...` ); runCommand(packageManager, installOptions.concat(packageName)) .then(() => { require(packageName); //eslint-disable-line }) .catch(error => { console.error(error); process.exitCode = 1; }); }); } else if (installedClis.length === 1) { const path = require("path"); // 取出数据第一项,即 webpack-cli/package.json const pkgPath = require.resolve(`${installedClis[0].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), // webpack-cli pkg.bin[installedClis[0].binName] // bin/cli.js )); } else { console.warn( `You have installed ${installedClis .map(item => item.name) .join( " and " )} together. To work with the "webpack" command you need only one CLI package, please remove one of them or use them directly via their binary.` ); // @ts-ignore process.exitCode = 1; }

webpack.js 核心作用就是 require 了 node_modules/webapck-cli/bin/cli.js。

webpack-cli/bin/cli.js

#!/usr/bin/env node /* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ const { NON_COMPILATION_ARGS } = require("./utils/constants"); (function() { // wrap in IIFE to be able to use return const importLocal = require("import-local"); // Prefer the local installation of webpack-cli if (importLocal(__filename)) { return; } require("v8-compile-cache"); const ErrorHelpers = require("./utils/errorHelpers"); const NON_COMPILATION_CMD = process.argv.find(arg => { if (arg === "serve") { global.process.argv = global.process.argv.filter(a => a !== "serve"); process.argv = global.process.argv; } return NON_COMPILATION_ARGS.find(a => a === arg); }); if (NON_COMPILATION_CMD) { return require("./utils/prompt-command")(NON_COMPILATION_CMD, ...process.argv); } const yargs = require("yargs").usage(`webpack-cli ${require("../package.json").version} Usage: webpack-cli [options] webpack-cli [options] --entry <entry> --output <output> webpack-cli [options] <entries...> --output <output> webpack-cli <command> [options] For more information, see https://webpack.js.org/api/cli/.`); require("./config/config-yargs")(yargs); // yargs will terminate the process early when the user uses help or version. // This causes large help outputs to be cut short (https://github.com/nodejs/node/wiki/API-changes-between-v0.10-and-v4#process). // To prevent this we use the yargs.parse API and exit the process normally yargs.parse(process.argv.slice(2), (err, argv, output) => { Error.stackTraceLimit = 30; // arguments validation failed if (err && output) { console.error(output); process.exitCode = 1; return; } // help or version info if (output) { console.log(output); return; } if (argv.verbose) { argv["display"] = "verbose"; } let options; try { options = require("./utils/convert-argv")(argv); } catch (err) { if (err.code === "MODULE_NOT_FOUND") { const moduleName = err.message.split("'")[1]; let instructions = ""; let errorMessage = ""; if (moduleName === "webpack") { errorMessage = `\n${moduleName} not installed`; instructions = `Install webpack to start bundling: \u001b[32m\n $ npm install --save-dev ${moduleName}\n`; if (process.env.npm_execpath !== undefined && process.env.npm_execpath.includes("yarn")) { instructions = `Install webpack to start bundling: \u001b[32m\n $ yarn add ${moduleName} --dev\n`; } Error.stackTraceLimit = 1; console.error(`${errorMessage}\n\n${instructions}`); process.exitCode = 1; return; } } if (err.name !== "ValidationError") { throw err; } const stack = ErrorHelpers.cleanUpWebpackOptions(err.stack, err.message); const message = err.message + "\n" + stack; if (argv.color) { console.error(`\u001b[1m\u001b[31m${message}\u001b[39m\u001b[22m`); } else { console.error(message); } process.exitCode = 1; return; } /** * When --silent flag is present, an object with a no-op write method is * used in place of process.stout */ const stdout = argv.silent ? { write: () => {} } : process.stdout; function ifArg(name, fn, init) { if (Array.isArray(argv[name])) { if (init) init(); argv[name].forEach(fn); } else if (typeof argv[name] !== "undefined") { if (init) init(); fn(argv[name], -1); } } function processOptions(options) { // process Promise if (typeof options.then === "function") { options.then(processOptions).catch(function(err) { console.error(err.stack || err); // eslint-disable-next-line no-process-exit process.exit(1); }); return; } const firstOptions = [].concat(options)[0]; const statsPresetToOptions = require("webpack").Stats.presetToOptions; let outputOptions = options.stats; if (typeof outputOptions === "boolean" || typeof outputOptions === "string") { outputOptions = statsPresetToOptions(outputOptions); } else if (!outputOptions) { outputOptions = {}; } ifArg("display", function(preset) { outputOptions = statsPresetToOptions(preset); }); outputOptions = Object.create(outputOptions); if (Array.isArray(options) && !outputOptions.children) { outputOptions.children = options.map(o => o.stats); } if (typeof outputOptions.context === "undefined") outputOptions.context = firstOptions.context; // ... // 引入 webpack const webpack = require("webpack"); let lastHash = null; let compiler; try { compiler = webpack(options); } catch (err) { if (err.name === "WebpackOptionsValidationError") { if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`); else console.error(err.message); // eslint-disable-next-line no-process-exit process.exit(1); } throw err; } if (argv.progress) { const ProgressPlugin = require("webpack").ProgressPlugin; new ProgressPlugin({ profile: argv.profile }).apply(compiler); } if (outputOptions.infoVerbosity === "verbose") { if (argv.w) { compiler.hooks.watchRun.tap("WebpackInfo", compilation => { const compilationName = compilation.name ? compilation.name : ""; console.error("\nCompilation " + compilationName + " starting…\n"); }); } else { compiler.hooks.beforeRun.tap("WebpackInfo", compilation => { const compilationName = compilation.name ? compilation.name : ""; console.error("\nCompilation " + compilationName + " starting…\n"); }); } compiler.hooks.done.tap("WebpackInfo", compilation => { const compilationName = compilation.name ? compilation.name : ""; console.error("\nCompilation " + compilationName + " finished\n"); }); } function compilerCallback(err, stats) { if (!options.watch || err) { // Do not keep cache anymore compiler.purgeInputFileSystem(); } if (err) { lastHash = null; console.error(err.stack || err); if (err.details) console.error(err.details); process.exitCode = 1; return; } if (outputOptions.json) { stdout.write(JSON.stringify(stats.toJson(outputOptions), null, 2) + "\n"); } else if (stats.hash !== lastHash) { lastHash = stats.hash; if (stats.compilation && stats.compilation.errors.length !== 0) { const errors = stats.compilation.errors; if (errors[0].name === "EntryModuleNotFoundError") { console.error("\n\u001b[1m\u001b[31mInsufficient number of arguments or no entry found."); console.error( "\u001b[1m\u001b[31mAlternatively, run 'webpack(-cli) --help' for usage info.\u001b[39m\u001b[22m\n" ); } } const statsString = stats.toString(outputOptions); const delimiter = outputOptions.buildDelimiter ? `${outputOptions.buildDelimiter}\n` : ""; if (statsString) stdout.write(`${statsString}\n${delimiter}`); } if (!options.watch && stats.hasErrors()) { process.exitCode = 2; } } if (firstOptions.watch || options.watch) { const watchOptions = firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {}; if (watchOptions.stdin) { process.stdin.on("end", function(_) { process.exit(); // eslint-disable-line }); process.stdin.resume(); } compiler.watch(watchOptions, compilerCallback); if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n"); } else { // compiler.run compiler.run((err, stats) => { if (compiler.close) { compiler.close(err2 => { compilerCallback(err || err2, stats); }); } else { compilerCallback(err, stats); } }); } } processOptions(options); }); })();

cli.js

  • 当前文件一般存在两个操作,处理参数,将参数交给不同的逻辑(业务分发)
  • options 处理 options
  • compiler 加载 webpack 配置
  • compiler.run() 执行

webpack 主流程分析

测试代码

const webpack = require('webpack'); const options = require('./webpack.config.js'); let compiler = webpack(options); compiler.run(function (err, stats) { console.log(err); console.log(stats.toJson()); });

node_modules/webpack/lib/webpack.js

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const Compiler = require("./Compiler"); const MultiCompiler = require("./MultiCompiler"); const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin"); const WebpackOptionsApply = require("./WebpackOptionsApply"); const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter"); const validateSchema = require("./validateSchema"); const WebpackOptionsValidationError = require("./WebpackOptionsValidationError"); const webpackOptionsSchema = require("../schemas/WebpackOptions.json"); const RemovedPluginError = require("./RemovedPluginError"); const version = require("../package.json").version; /** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */ /** * @param {WebpackOptions} options options object * @param {function(Error=, Stats=): void=} callback callback * @returns {Compiler | MultiCompiler} the compiler object */ const webpack = (options, callback) => { const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); if (webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; // 定义 compiler 变量 // 用户传入的 options if (Array.isArray(options)) { compiler = new MultiCompiler( Array.from(options).map(options => webpack(options)) ); } else if (typeof options === "object") { // 通过 process 方法合并默认配置 options = new WebpackOptionsDefaulter().process(options); // 实例化 Compiler compiler = new Compiler(options.context); // 缓存 options compiler.options = options; // 使用插件,调用 apply 方法 // 经过 NodeEnvironmentPlugin 处理后,compiler 具备文件读写能力 new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler); // 获取用户自定义配置的 plugins if (options.plugins && Array.isArray(options.plugins)) { // 循环执行 plugin 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.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if (callback) { if (typeof callback !== "function") { throw new Error("Invalid argument: callback"); } if ( options.watch === true || (Array.isArray(options) && options.some(o => o.watch)) ) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}; return compiler.watch(watchOptions, callback); } compiler.run(callback); } // 返回 compiler 对象 return compiler; }; exports = module.exports = webpack; exports.version = version; webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter; webpack.WebpackOptionsApply = WebpackOptionsApply; webpack.Compiler = Compiler; webpack.MultiCompiler = MultiCompiler; webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin; // @ts-ignore Global @this directive is not supported webpack.validate = validateSchema.bind(this, webpackOptionsSchema); webpack.validateSchema = validateSchema; webpack.WebpackOptionsValidationError = WebpackOptionsValidationError; const exportPlugins = (obj, mappings) => { for (const name of Object.keys(mappings)) { Object.defineProperty(obj, name, { configurable: false, enumerable: true, get: mappings[name] }); } }; exportPlugins(exports, { AutomaticPrefetchPlugin: () => require("./AutomaticPrefetchPlugin"), BannerPlugin: () => require("./BannerPlugin"), CachePlugin: () => require("./CachePlugin"), ContextExclusionPlugin: () => require("./ContextExclusionPlugin"), ContextReplacementPlugin: () => require("./ContextReplacementPlugin"), DefinePlugin: () => require("./DefinePlugin"), Dependency: () => require("./Dependency"), DllPlugin: () => require("./DllPlugin"), DllReferencePlugin: () => require("./DllReferencePlugin"), EnvironmentPlugin: () => require("./EnvironmentPlugin"), EvalDevToolModulePlugin: () => require("./EvalDevToolModulePlugin"), EvalSourceMapDevToolPlugin: () => require("./EvalSourceMapDevToolPlugin"), ExtendedAPIPlugin: () => require("./ExtendedAPIPlugin"), ExternalsPlugin: () => require("./ExternalsPlugin"), HashedModuleIdsPlugin: () => require("./HashedModuleIdsPlugin"), HotModuleReplacementPlugin: () => require("./HotModuleReplacementPlugin"), IgnorePlugin: () => require("./IgnorePlugin"), LibraryTemplatePlugin: () => require("./LibraryTemplatePlugin"), LoaderOptionsPlugin: () => require("./LoaderOptionsPlugin"), LoaderTargetPlugin: () => require("./LoaderTargetPlugin"), MemoryOutputFileSystem: () => require("./MemoryOutputFileSystem"), Module: () => require("./Module"), ModuleFilenameHelpers: () => require("./ModuleFilenameHelpers"), NamedChunksPlugin: () => require("./NamedChunksPlugin"), NamedModulesPlugin: () => require("./NamedModulesPlugin"), NoEmitOnErrorsPlugin: () => require("./NoEmitOnErrorsPlugin"), NormalModuleReplacementPlugin: () => require("./NormalModuleReplacementPlugin"), PrefetchPlugin: () => require("./PrefetchPlugin"), ProgressPlugin: () => require("./ProgressPlugin"), ProvidePlugin: () => require("./ProvidePlugin"), SetVarMainTemplatePlugin: () => require("./SetVarMainTemplatePlugin"), SingleEntryPlugin: () => require("./SingleEntryPlugin"), SourceMapDevToolPlugin: () => require("./SourceMapDevToolPlugin"), Stats: () => require("./Stats"), Template: () => require("./Template"), UmdMainTemplatePlugin: () => require("./UmdMainTemplatePlugin"), WatchIgnorePlugin: () => require("./WatchIgnorePlugin") }); exportPlugins((exports.dependencies = {}), { DependencyReference: () => require("./dependencies/DependencyReference") }); exportPlugins((exports.optimize = {}), { AggressiveMergingPlugin: () => require("./optimize/AggressiveMergingPlugin"), AggressiveSplittingPlugin: () => require("./optimize/AggressiveSplittingPlugin"), ChunkModuleIdRangePlugin: () => require("./optimize/ChunkModuleIdRangePlugin"), LimitChunkCountPlugin: () => require("./optimize/LimitChunkCountPlugin"), MinChunkSizePlugin: () => require("./optimize/MinChunkSizePlugin"), ModuleConcatenationPlugin: () => require("./optimize/ModuleConcatenationPlugin"), OccurrenceOrderPlugin: () => require("./optimize/OccurrenceOrderPlugin"), OccurrenceModuleOrderPlugin: () => require("./optimize/OccurrenceModuleOrderPlugin"), OccurrenceChunkOrderPlugin: () => require("./optimize/OccurrenceChunkOrderPlugin"), RuntimeChunkPlugin: () => require("./optimize/RuntimeChunkPlugin"), SideEffectsFlagPlugin: () => require("./optimize/SideEffectsFlagPlugin"), SplitChunksPlugin: () => require("./optimize/SplitChunksPlugin") }); exportPlugins((exports.web = {}), { FetchCompileWasmTemplatePlugin: () => require("./web/FetchCompileWasmTemplatePlugin"), JsonpTemplatePlugin: () => require("./web/JsonpTemplatePlugin") }); exportPlugins((exports.webworker = {}), { WebWorkerTemplatePlugin: () => require("./webworker/WebWorkerTemplatePlugin") }); exportPlugins((exports.node = {}), { NodeTemplatePlugin: () => require("./node/NodeTemplatePlugin"), ReadFileCompileWasmTemplatePlugin: () => require("./node/ReadFileCompileWasmTemplatePlugin") }); exportPlugins((exports.debug = {}), { ProfilingPlugin: () => require("./debug/ProfilingPlugin") }); exportPlugins((exports.util = {}), { createHash: () => require("./util/createHash") }); const defineMissingPluginError = (namespace, pluginName, errorMessage) => { Object.defineProperty(namespace, pluginName, { configurable: false, enumerable: true, get() { throw new RemovedPluginError(errorMessage); } }); }; // TODO remove in webpack 5 defineMissingPluginError( exports.optimize, "UglifyJsPlugin", "webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead." ); // TODO remove in webpack 5 defineMissingPluginError( exports.optimize, "CommonsChunkPlugin", "webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead." );

node_modules/webpack/lib/Compiler.js

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const parseJson = require("json-parse-better-errors"); const asyncLib = require("neo-async"); const path = require("path"); const { Source } = require("webpack-sources"); const util = require("util"); const { Tapable, SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require("tapable"); const Compilation = require("./Compilation"); const Stats = require("./Stats"); const Watching = require("./Watching"); const NormalModuleFactory = require("./NormalModuleFactory"); const ContextModuleFactory = require("./ContextModuleFactory"); const ResolverFactory = require("./ResolverFactory"); const RequestShortener = require("./RequestShortener"); const { makePathsRelative } = require("./util/identifier"); const ConcurrentCompilationError = require("./ConcurrentCompilationError"); const { Logger } = require("./logging/Logger"); /** @typedef {import("../declarations/WebpackOptions").Entry} Entry */ /** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */ /** * @typedef {Object} CompilationParams * @property {NormalModuleFactory} normalModuleFactory * @property {ContextModuleFactory} contextModuleFactory * @property {Set<string>} compilationDependencies */ class Compiler extends Tapable { constructor(context) { super(); // compiler.hooks. // 默认初始化很多钩子 this.hooks = { /** @type {SyncBailHook<Compilation>} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook<Stats>} */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook<Compiler>} */ beforeRun: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compiler>} */ run: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook<Compilation>} */ emit: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<string, Buffer>} */ assetEmitted: new AsyncSeriesHook(["file", "content"]), /** @type {AsyncSeriesHook<Compilation>} */ afterEmit: new AsyncSeriesHook(["compilation"]), /** @type {SyncHook<Compilation, CompilationParams>} */ thisCompilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<Compilation, CompilationParams>} */ compilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook<NormalModuleFactory>} */ normalModuleFactory: new SyncHook(["normalModuleFactory"]), /** @type {SyncHook<ContextModuleFactory>} */ contextModuleFactory: new SyncHook(["contextModulefactory"]), /** @type {AsyncSeriesHook<CompilationParams>} */ beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook<CompilationParams>} */ compile: new SyncHook(["params"]), /** @type {AsyncParallelHook<Compilation>} */ make: new AsyncParallelHook(["compilation"]), /** @type {AsyncSeriesHook<Compilation>} */ afterCompile: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook<Compiler>} */ watchRun: new AsyncSeriesHook(["compiler"]), /** @type {SyncHook<Error>} */ failed: new SyncHook(["error"]), /** @type {SyncHook<string, string>} */ invalid: new SyncHook(["filename", "changeTime"]), /** @type {SyncHook} */ watchClose: new SyncHook([]), /** @type {SyncBailHook<string, string, any[]>} */ infrastructureLog: new SyncBailHook(["origin", "type", "args"]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /** @type {SyncHook} */ environment: new SyncHook([]), /** @type {SyncHook} */ afterEnvironment: new SyncHook([]), /** @type {SyncHook<Compiler>} */ afterPlugins: new SyncHook(["compiler"]), /** @type {SyncHook<Compiler>} */ afterResolvers: new SyncHook(["compiler"]), /** @type {SyncBailHook<string, Entry>} */ entryOption: new SyncBailHook(["context", "entry"]) }; // TODO webpack 5 remove this this.hooks.infrastructurelog = this.hooks.infrastructureLog; this._pluginCompat.tap("Compiler", options => { switch (options.name) { case "additional-pass": case "before-run": case "run": case "emit": case "after-emit": case "before-compile": case "make": case "after-compile": case "watch-run": options.async = true; break; } }); /** @type {string=} */ this.name = undefined; /** @type {Compilation=} */ this.parentCompilation = undefined; /** @type {string} */ this.outputPath = ""; this.outputFileSystem = null; this.inputFileSystem = null; /** @type {string|null} */ this.recordsInputPath = null; /** @type {string|null} */ this.recordsOutputPath = null; this.records = {}; this.removedFiles = new Set(); /** @type {Map<string, number>} */ this.fileTimestamps = new Map(); /** @type {Map<string, number>} */ this.contextTimestamps = new Map(); /** @type {ResolverFactory} */ this.resolverFactory = new ResolverFactory(); this.infrastructureLogger = undefined; // TODO remove in webpack 5 this.resolvers = { normal: { plugins: util.deprecate((hook, fn) => { this.resolverFactory.plugin("resolver normal", resolver => { resolver.plugin(hook, fn); }); }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), apply: util.deprecate((...args) => { this.resolverFactory.plugin("resolver normal", resolver => { resolver.apply(...args); }); }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.apply(/* … */);\n}); instead.') }, loader: { plugins: util.deprecate((hook, fn) => { this.resolverFactory.plugin("resolver loader", resolver => { resolver.plugin(hook, fn); }); }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), apply: util.deprecate((...args) => { this.resolverFactory.plugin("resolver loader", resolver => { resolver.apply(...args); }); }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.apply(/* … */);\n}); instead.') }, context: { plugins: util.deprecate((hook, fn) => { this.resolverFactory.plugin("resolver context", resolver => { resolver.plugin(hook, fn); }); }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), apply: util.deprecate((...args) => { this.resolverFactory.plugin("resolver context", resolver => { resolver.apply(...args); }); }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.apply(/* … */);\n}); instead.') } }; /** @type {WebpackOptions} */ this.options = /** @type {WebpackOptions} */ ({}); this.context = context; this.requestShortener = new RequestShortener(context); /** @type {boolean} */ this.running = false; /** @type {boolean} */ this.watchMode = false; /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */ this._assetEmittingSourceCache = new WeakMap(); /** @private @type {Map<string, number>} */ this._assetEmittingWrittenFiles = new Map(); } /** * @param {string | (function(): string)} name name of the logger, or function called once to get the logger name * @returns {Logger} a logger with that name */ getInfrastructureLogger(name) { if (!name) { throw new TypeError( "Compiler.getInfrastructureLogger(name) called without a name" ); } return new Logger((type, args) => { if (typeof name === "function") { name = name(); if (!name) { throw new TypeError( "Compiler.getInfrastructureLogger(name) called with a function not returning a name" ); } } if (this.hooks.infrastructureLog.call(name, type, args) === undefined) { if (this.infrastructureLogger !== undefined) { this.infrastructureLogger(name, type, args); } } }); } watch(watchOptions, handler) { if (this.running) return handler(new ConcurrentCompilationError()); this.running = true; this.watchMode = true; this.fileTimestamps = new Map(); this.contextTimestamps = new Map(); this.removedFiles = new Set(); return new Watching(this, watchOptions, handler); } run(callback) { if (this.running) return callback(new ConcurrentCompilationError()); const finalCallback = (err, stats) => { this.running = false; if (err) { this.hooks.failed.call(err); } if (callback !== undefined) return callback(err, stats); }; const startTime = Date.now(); this.running = true; const onCompiled = (err, compilation) => { if (err) return finalCallback(err); if (this.hooks.shouldEmit.call(compilation) === false) { const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; } this.emitAssets(compilation, err => { if (err) return finalCallback(err); if (compilation.hooks.needAdditionalPass.call()) { compilation.needAdditionalPass = true; const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); this.hooks.additionalPass.callAsync(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); return; } this.emitRecords(err => { if (err) return finalCallback(err); const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); }); }); }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); } runAsChild(callback) { this.compile((err, compilation) => { if (err) return callback(err); this.parentCompilation.children.push(compilation); for (const { name, source, info } of compilation.getAssets()) { this.parentCompilation.emitAsset(name, source, info); } const entries = Array.from( compilation.entrypoints.values(), ep => ep.chunks ).reduce((array, chunks) => { return array.concat(chunks); }, []); return callback(null, entries, compilation); }); } purgeInputFileSystem() { if (this.inputFileSystem && this.inputFileSystem.purge) { this.inputFileSystem.purge(); } } emitAssets(compilation, callback) { let outputPath; const emitFiles = err => { if (err) return callback(err); asyncLib.forEachLimit( compilation.getAssets(), 15, ({ name: file, source }, callback) => { let targetFile = file; const queryStringIdx = targetFile.indexOf("?"); if (queryStringIdx >= 0) { targetFile = targetFile.substr(0, queryStringIdx); } const writeOut = err => { if (err) return callback(err); const targetPath = this.outputFileSystem.join( outputPath, targetFile ); // TODO webpack 5 remove futureEmitAssets option and make it on by default if (this.options.output.futureEmitAssets) { // check if the target file has already been written by this Compiler const targetFileGeneration = this._assetEmittingWrittenFiles.get( targetPath ); // create an cache entry for this Source if not already existing let cacheEntry = this._assetEmittingSourceCache.get(source); if (cacheEntry === undefined) { cacheEntry = { sizeOnlySource: undefined, writtenTo: new Map() }; this._assetEmittingSourceCache.set(source, cacheEntry); } // if the target file has already been written if (targetFileGeneration !== undefined) { // check if the Source has been written to this target file const writtenGeneration = cacheEntry.writtenTo.get(targetPath); if (writtenGeneration === targetFileGeneration) { // if yes, we skip writing the file // as it's already there // (we assume one doesn't remove files while the Compiler is running) compilation.updateAsset(file, cacheEntry.sizeOnlySource, { size: cacheEntry.sizeOnlySource.size() }); return callback(); } } // TODO webpack 5: if info.immutable check if file already exists in output // skip emitting if it's already there // get the binary (Buffer) content from the Source /** @type {Buffer} */ let content; if (typeof source.buffer === "function") { content = source.buffer(); } else { const bufferOrString = source.source(); if (Buffer.isBuffer(bufferOrString)) { content = bufferOrString; } else { content = Buffer.from(bufferOrString, "utf8"); } } // Create a replacement resource which only allows to ask for size // This allows to GC all memory allocated by the Source // (expect when the Source is stored in any other cache) cacheEntry.sizeOnlySource = new SizeOnlySource(content.length); compilation.updateAsset(file, cacheEntry.sizeOnlySource, { size: content.length }); // Write the file to output file system this.outputFileSystem.writeFile(targetPath, content, err => { if (err) return callback(err); // information marker that the asset has been emitted compilation.emittedAssets.add(file); // cache the information that the Source has been written to that location const newGeneration = targetFileGeneration === undefined ? 1 : targetFileGeneration + 1; cacheEntry.writtenTo.set(targetPath, newGeneration); this._assetEmittingWrittenFiles.set(targetPath, newGeneration); this.hooks.assetEmitted.callAsync(file, content, callback); }); } else { if (source.existsAt === targetPath) { source.emitted = false; return callback(); } let content = source.source(); if (!Buffer.isBuffer(content)) { content = Buffer.from(content, "utf8"); } source.existsAt = targetPath; source.emitted = true; this.outputFileSystem.writeFile(targetPath, content, err => { if (err) return callback(err); this.hooks.assetEmitted.callAsync(file, content, callback); }); } }; if (targetFile.match(/\/|\\/)) { const dir = path.dirname(targetFile); this.outputFileSystem.mkdirp( this.outputFileSystem.join(outputPath, dir), writeOut ); } else { writeOut(); } }, err => { if (err) return callback(err); this.hooks.afterEmit.callAsync(compilation, err => { if (err) return callback(err); return callback(); }); } ); }; this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); }); } emitRecords(callback) { if (!this.recordsOutputPath) return callback(); const idx1 = this.recordsOutputPath.lastIndexOf("/"); const idx2 = this.recordsOutputPath.lastIndexOf("\\"); let recordsOutputPathDirectory = null; if (idx1 > idx2) { recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx1); } else if (idx1 < idx2) { recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx2); } const writeFile = () => { this.outputFileSystem.writeFile( this.recordsOutputPath, JSON.stringify(this.records, undefined, 2), callback ); }; if (!recordsOutputPathDirectory) { return writeFile(); } this.outputFileSystem.mkdirp(recordsOutputPathDirectory, err => { if (err) return callback(err); writeFile(); }); } readRecords(callback) { if (!this.recordsInputPath) { this.records = {}; return callback(); } this.inputFileSystem.stat(this.recordsInputPath, err => { // It doesn't exist // We can ignore this. if (err) return callback(); this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => { if (err) return callback(err); try { this.records = parseJson(content.toString("utf-8")); } catch (e) { e.message = "Cannot parse records: " + e.message; return callback(e); } return callback(); }); }); } createChildCompiler( compilation, compilerName, compilerIndex, outputOptions, plugins ) { const childCompiler = new Compiler(this.context); if (Array.isArray(plugins)) { for (const plugin of plugins) { plugin.apply(childCompiler); } } for (const name in this.hooks) { if ( ![ "make", "compile", "emit", "afterEmit", "invalid", "done", "thisCompilation" ].includes(name) ) { if (childCompiler.hooks[name]) { childCompiler.hooks[name].taps = this.hooks[name].taps.slice(); } } } childCompiler.name = compilerName; childCompiler.outputPath = this.outputPath; childCompiler.inputFileSystem = this.inputFileSystem; childCompiler.outputFileSystem = null; childCompiler.resolverFactory = this.resolverFactory; childCompiler.fileTimestamps = this.fileTimestamps; childCompiler.contextTimestamps = this.contextTimestamps; const relativeCompilerName = makePathsRelative(this.context, compilerName); if (!this.records[relativeCompilerName]) { this.records[relativeCompilerName] = []; } if (this.records[relativeCompilerName][compilerIndex]) { childCompiler.records = this.records[relativeCompilerName][compilerIndex]; } else { this.records[relativeCompilerName].push((childCompiler.records = {})); } childCompiler.options = Object.create(this.options); childCompiler.options.output = Object.create(childCompiler.options.output); for (const name in outputOptions) { childCompiler.options.output[name] = outputOptions[name]; } childCompiler.parentCompilation = compilation; compilation.hooks.childCompiler.call( childCompiler, compilerName, compilerIndex ); return childCompiler; } isChild() { return !!this.parentCompilation; } createCompilation() { return new Compilation(this); } newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } createNormalModuleFactory() { const normalModuleFactory = new NormalModuleFactory( this.options.context, this.resolverFactory, this.options.module || {} ); this.hooks.normalModuleFactory.call(normalModuleFactory); return normalModuleFactory; } createContextModuleFactory() { const contextModuleFactory = new ContextModuleFactory(this.resolverFactory); this.hooks.contextModuleFactory.call(contextModuleFactory); return contextModuleFactory; } newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; } compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); }); }); } } module.exports = Compiler; // ...

hooks 执行顺序

shouldEmit done additionalPass beforeRun run emit assetEmitted afterEmit thisCompilation compilation normalModuleFactory contextModuleFactory beforeCompile compile make afterCompile watchRun failed invalid watchClose infrastructureLog

webpack 初始化的时候,就已经定义好一系列钩子供我们使用。

beforeRun、run、thisCompilation、compilation、beforeCompile、compile、make、afterCompile 等。

开始 -> 配置合并 -> 实例化 compiler -> 初始化 node 文件读写能力 -> 挂载 plugins -> 处理 wbepack 内部插件(入口文件处理)
开始 -> compiler.beforeRun -> compiler.run -> compiler.beforeComile -> compiler.compile -> compiler.make

webpack.js 主流程实现

主要分为 pack 目录和测试文件 run.js 以及 webpack.config.js。

webpack.config.js

const path = require('path'); module.exports = { devtool: 'none', mode: 'development', entry: './src/index.js', context: process.cwd(), output: { filename: 'index.js', path: path.resolve(__dirname, 'dist') } }

run.js

// const webpack = require('webpack'); // const options = require('./webpack.config.js'); // let compiler = webpack(options); // compiler.run(function (err, stats) { // console.log(err); // console.log(stats.toJson()); // }); const webpack = require('./pack'); const options = require('./webpack.config.js'); let compiler = webpack(options); compiler.run(function (err, stats) { console.log(err); console.log(stats.toJson()); });

pack/package.json

{ "name": "pack", "version": "1.0.0", "description": "", "main": "lib/webpack.js", "directories": { "lib": "lib" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": {}, "devDependencies": { "tapable": "1" } }

pack/lib/webpack.js

const Compiler = require('./Compiler'); const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin'); const webpack = function (options) { // 实例化 compiler 对象 const compiler = new Compiler(options.context); compiler.options = options; // 初始化 NodeEnvironmentPlugin new NodeEnvironmentPlugin().apply(compiler); // 挂载所有的 plugins 插件至 compiler 对象身上 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 挂载所有的 webpack 内置插件 // compiler.options = new WebpackOptionApply().process(options, compiler); // 返回 compiler 对象 return compiler; } module.exports = webpack;

pack/lib/Compiler.js

const { Tapable, AsyncSeriesHook } = require('tapable'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']) } } run (callback) { callback && callback(null, { toJson () { return { entries: [], // 入口信息 chunks: [], // chunk 信息 modules: [], // 模块信息 assets: [], // 最终生成资源 } } }); } } module.exports = Compiler;

pack/lib/node/NodeEnvironmentPlugin.js

简单实现,了解逻辑即可。

const fs = require('fs'); class NodeEnvironmentPlugin { constructor (options) { this.options = options || {}; } apply (compiler) { compiler.inputFileSystem = fs; compiler.outputFileSystem = fs; } } module.exports = NodeEnvironmentPlugin;

EntryOptionPlugin 分析

new WebpackOptionsApply().process(options, compiler),对 webpack 默认插件进行挂载

webpack/lib/WebpackOptionsApply

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; // ... const { cachedCleverMerge } = require("./util/cleverMerge"); /** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */ /** @typedef {import("./Compiler")} Compiler */ class WebpackOptionsApply extends OptionsApply { constructor() { super(); } /** * @param {WebpackOptions} options options object * @param {Compiler} compiler compiler object * @returns {WebpackOptions} options object */ process(options, compiler) { let ExternalsPlugin; compiler.outputPath = options.output.path; compiler.recordsInputPath = options.recordsInputPath || options.recordsPath; compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath; compiler.name = options.name; // TODO webpack 5 refactor this to MultiCompiler.setDependencies() with a WeakMap // @ts-ignore TODO compiler.dependencies = options.dependencies; // ... let noSources; let legacy; let modern; let comment; if ( options.devtool && (options.devtool.includes("sourcemap") || options.devtool.includes("source-map")) ) { const hidden = options.devtool.includes("hidden"); const inline = options.devtool.includes("inline"); const evalWrapped = options.devtool.includes("eval"); const cheap = options.devtool.includes("cheap"); const moduleMaps = options.devtool.includes("module"); noSources = options.devtool.includes("nosources"); legacy = options.devtool.includes("@"); modern = options.devtool.includes("#"); comment = legacy && modern ? "\n/*\n//@ source" + "MappingURL=[url]\n//# source" + "MappingURL=[url]\n*/" : legacy ? "\n/*\n//@ source" + "MappingURL=[url]\n*/" : modern ? "\n//# source" + "MappingURL=[url]" : null; const Plugin = evalWrapped ? EvalSourceMapDevToolPlugin : SourceMapDevToolPlugin; new Plugin({ filename: inline ? null : options.output.sourceMapFilename, moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate, fallbackModuleFilenameTemplate: options.output.devtoolFallbackModuleFilenameTemplate, append: hidden ? false : comment, module: moduleMaps ? true : cheap ? false : true, columns: cheap ? false : true, lineToLine: options.output.devtoolLineToLine, noSources: noSources, namespace: options.output.devtoolNamespace }).apply(compiler); } else if (options.devtool && options.devtool.includes("eval")) { legacy = options.devtool.includes("@"); modern = options.devtool.includes("#"); comment = legacy && modern ? "\n//@ sourceURL=[url]\n//# sourceURL=[url]" : legacy ? "\n//@ sourceURL=[url]" : modern ? "\n//# sourceURL=[url]" : null; new EvalDevToolModulePlugin({ sourceUrlComment: comment, moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate, namespace: options.output.devtoolNamespace }).apply(compiler); } new JavascriptModulesPlugin().apply(compiler); new JsonModulesPlugin().apply(compiler); new WebAssemblyModulesPlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); // new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); // ... } module.exports = WebpackOptionsApply;

webpack/lib/EntryOptionPlugin

/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const SingleEntryPlugin = require("./SingleEntryPlugin"); const MultiEntryPlugin = require("./MultiEntryPlugin"); const DynamicEntryPlugin = require("./DynamicEntryPlugin"); /** @typedef {import("../declarations/WebpackOptions").EntryItem} EntryItem */ /** @typedef {import("./Compiler")} Compiler */ /** * @param {string} context context path * @param {EntryItem} item entry array or single path * @param {string} name entry key name * @returns {SingleEntryPlugin | MultiEntryPlugin} returns either a single or multi entry plugin */ const itemToPlugin = (context, item, name) => { if (Array.isArray(item)) { return new MultiEntryPlugin(context, item, name); } // 返回实例对象 return new SingleEntryPlugin(context, item, name); }; module.exports = class EntryOptionPlugin { /** * @param {Compiler} compiler the compiler instance one is tapping into * @returns {void} */ apply(compiler) { // tap EntryOptionPlugin,注册事件监听 compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => { if (typeof entry === "string" || Array.isArray(entry)) { itemToPlugin(context, entry, "main").apply(compiler); } else if (typeof entry === "object") { for (const name of Object.keys(entry)) { itemToPlugin(context, entry[name], name).apply(compiler); } } else if (typeof entry === "function") { new DynamicEntryPlugin(context, entry).apply(compiler); } return true; }); } };

webpack/lib/SingleEntryPlugin

"use strict"; const SingleEntryDependency = require("./dependencies/SingleEntryDependency"); /** @typedef {import("./Compiler")} Compiler */ class SingleEntryPlugin { /** * An entry plugin which will handle * creation of the SingleEntryDependency * * @param {string} context context path * @param {string} entry entry path * @param {string} name entry key name */ constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } /** * @param {Compiler} compiler the compiler instance * @returns {void} */ apply(compiler) { compiler.hooks.compilation.tap( "SingleEntryPlugin", (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); // 开始执行编译,交由 comilation // compiler 主要是做编译前的准备,比如订阅钩子 compilation.addEntry(context, dep, name, callback); } ); } /** * @param {string} entry entry request * @param {string} name entry name * @returns {SingleEntryDependency} the dependency */ static createDependency(entry, name) { const dep = new SingleEntryDependency(entry); dep.loc = { name }; return dep; } } module.exports = SingleEntryPlugin;

EntryOptionPlugin 实现

pack/lib/Compiler.js

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']) } } run (callback) { callback && callback(null, { toJson () { return { entries: [], // 入口信息 chunks: [], // chunk 信息 modules: [], // 模块信息 assets: [], // 最终生成资源 } } }); } } module.exports = Compiler;

pack/lib/EntryOptionPlugin.js

const SingleEntryPlugin = require('./SingleEntryPlugin'); const itemToPlugin = function (context, item, name) { return new SingleEntryPlugin(context, item, name); } class EntryOptionPlugin { apply (compiler) { compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => { itemToPlugin(context, entry, 'main').apply(compiler); }); } } module.exports = EntryOptionPlugin;

pack/lib/SingleEntryPlugin.js

class SingleEntryPlugin { constructor (context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply (compiler) { compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => { const { context, entry, name } = this; console.log('make tap trigger.'); // compilation.addEntry(context, entry, name, callback); }); } } module.exports = SingleEntryPlugin;

pack/lib/webpack.js

const Compiler = require('./Compiler'); const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin'); const WebpackOptionApply = require('./WebpackOptionApply'); const webpack = function (options) { // 实例化 compiler 对象 const compiler = new Compiler(options.context); compiler.options = options; // 初始化 NodeEnvironmentPlugin new NodeEnvironmentPlugin().apply(compiler); // 挂载所有的 plugins 插件至 compiler 对象身上 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 挂载所有的 webpack 内置插件 compiler.options = new WebpackOptionApply().process(options, compiler); // 返回 compiler 对象 return compiler; } module.exports = webpack;

pack/lib/WebpackOptionApply.js

const EntryOptionPlugin = require('./EntryOptionPlugin'); class WebpackOptionApply { process (options, compiler) { new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); } } module.exports = WebpackOptionApply;

run.js

const webpack = require('./pack'); const options = require('./webpack.config.js'); let compiler = webpack(options); compiler.run(function (err, stats) { console.log(err); console.log(stats.toJson()); });

run 方法分析及实现

源码分析

run(callback) { if (this.running) return callback(new ConcurrentCompilationError()); const finalCallback = (err, stats) => { this.running = false; if (err) { this.hooks.failed.call(err); } if (callback !== undefined) return callback(err, stats); }; const startTime = Date.now(); this.running = true; const onCompiled = (err, compilation) => { if (err) return finalCallback(err); if (this.hooks.shouldEmit.call(compilation) === false) { const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; } this.emitAssets(compilation, err => { if (err) return finalCallback(err); if (compilation.hooks.needAdditionalPass.call()) { compilation.needAdditionalPass = true; const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); this.hooks.additionalPass.callAsync(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); return; } this.emitRecords(err => { if (err) return finalCallback(err); const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); }); }); }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); }

代码实现

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']) } } compile () { console.log('compile'); } run (callback) { const finalCallback = function (err, status) { callback(err, status); } const onCompiled = function (err, compilation) { console.log('onCompiled'); finalCallback(err, { toJson () { return { entries: [], chunks: [], module: [], assets: [] } } }) } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled); }); }); } } module.exports = Compiler;

compier 方法分析及实现

源码分析

Compiler.js

createCompilation() { return new Compilation(this); } newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; } compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); }); }); }

调用 newCompilationParams ,返回 params

调用 beforeCompile 钩子:回调中触发 compile 钩子,调用 newCompilationfan 方法返回 compliation, 触发 make 钩子。

代码实现

lib/Compiler.js

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const Compilation = require('./Compilation'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']) } } newCompilationParams () { const params = { normalModuleFactory: new NormalModuleFactory() } return params; } createCompilation () { return new Compilation(this); } newCompilation (params) { const compilation = this.createCompilation(); } compile (callback) { const params = this.newCompilationParams(); this.hooks.beforeRun.callAsync(params, (err) => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, (err) => { console.log('make trigger', callback) callback && callback(); }); }); } run (callback) { const finalCallback = function (err, status) { callback(err, status); } const onCompiled = function (err, compilation) { console.log('onCompiled'); finalCallback(err, { toJson () { return { entries: [], chunks: [], module: [], assets: [] } } }) } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled); }); }); } } module.exports = Compiler;

make 前流程分析

  • 实例化 compiler 对象(贯穿整个 webpack 工作过程)、由 compiler 调用 run 方法

  • compiler 实例化操作

    • compiler 继承 tapable,因此它具备钩子的操作能力(监听事件、触发事件、webpack 是一个事件流)
    • 实例化 compiler 对象之后向它的身上挂载很多属性,其中 NodeEnvironmentPlugin 这个操作让它具备了文件读写能力
    • 具备文件读写能力之后,然后将 plugins 中的插件挂载到 compiler 对象上
    • 将内部默认的插件与 compiler 建立关系,其中 EntryOptionPlugin 用来处理模块 ID
    • 在实例化 compiler 的时候,只是监听 make 钩子(SingleEntryPlugin)
      • SingleEntryPlugin 模块的 apply 中存在二个钩子的监听
      • 其中 compilation 钩子就是 compilation 具备了利用 normalModuleFactory 工厂创建一个普通模块的能力,因为它就是利用一个自己创建的模块来加载需要被打包的模块
      • 其中 make 钩子在 compiler.run 时会被触发,意味着某个模块打包之前的准备工作就完成了
      • addEntry 方法调用
  • run 方法执行

    • run 方法里就是一堆钩子按照顺序触发(beforeRun、run、compile)

    • compile 方法执行

      • 准备参数(其中 normalModuleFactory 是后续用于创建模块)

      • 触发 beforeCompile

      • 将第一个参数传给一个函数,创建一个 compilation(newCompilation)

      • 在调用 newCompilation 的内部

        • 调用了 createCompilation
        • 触发 this.compilation 钩子和 compilation 的监听
      • 当创建 compilation 对象之后,触发 make 钩子

      • 当触发 make 钩子监听时,将 comilation 对象传递作为参数传递进

总结

  • 实例化 Compiler

  • 调用 compile 方法

  • newCompilation

  • 实例化 Compilation 对象(和 compiler 存在关系)

  • 触发 make 钩子,调用 addEntry 方法(将 context、name、entry 等)进行编译

addEntry 流程分析

  • make 钩子在被触发时,接收 compilation 实例,它由很多属性。

  • 从 compilation 解构三个值

    • entry:当前需要被打包的模块的相对路径(./src/index.js)
    • name:main
    • context:当前项目的根路径
  • dep 是对当前入口模块的依赖关系进行处理

  • 调用 addEntry 方法。

  • 在 compilation 实例身上存在一个 addEntry 方法,然后内部调用 _addModuleChain 方法去处理依赖

  • 在 compilation 中可以通过 NormalModuleFactory 工厂来创建一个普通的模块对象

  • 在 webpack 内部默认开启了一个 100 并发量的打包操作,我们看到的是 normalModule.create()

  • 在 beforeResolve 内部会触发一个 factory 钩子监听(这部分操作用来处理 loader,不会重点分析)

  • 上述操作完成之后,获取到一个函数存在 factory 中,然后对它进行立即调用,在这个函数调用里又触发了一个 resolver 的钩子(处理 loader,拿到 resolver 方法之后意味着所有的 loader 处理完毕)

  • 调用 resolver() 方法之后,就会进入到 afterResolve 这个钩子里,然后就会触发 new NormalModule

  • 完成上述操作之后就将 module 进行保存和一些其他属性参加

  • 调用 buildModule 方法开始编译,内部调用 build 方法,内部返回并调用 doBuild

lib/compilation.js

_addModuleChain(context, dependency, onModule, callback) { const start = this.profile && Date.now(); const currentProfile = this.profile && {}; const errorAndCallback = this.bail ? err => { callback(err); } : err => { err.dependencies = [dependency]; this.errors.push(err); callback(); }; if ( typeof dependency !== "object" || dependency === null || !dependency.constructor ) { throw new Error("Parameter 'dependency' must be a Dependency"); } const Dep = /** @type {DepConstructor} */ (dependency.constructor); const moduleFactory = this.dependencyFactories.get(Dep); if (!moduleFactory) { throw new Error( `No dependency factory available for this dependency type: ${dependency.constructor.name}` ); } this.semaphore.acquire(() => { moduleFactory.create( { contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { if (err) { this.semaphore.release(); return errorAndCallback(new EntryModuleNotFoundError(err)); } let afterFactory; if (currentProfile) { afterFactory = Date.now(); currentProfile.factory = afterFactory - start; } const addModuleResult = this.addModule(module); module = addModuleResult.module; onModule(module); dependency.module = module; module.addReason(null, dependency); const afterBuild = () => { if (addModuleResult.dependencies) { this.processModuleDependencies(module, err => { if (err) return callback(err); callback(null, module); }); } else { return callback(null, module); } }; if (addModuleResult.issuer) { if (currentProfile) { module.profile = currentProfile; } } if (addModuleResult.build) { this.buildModule(module, false, null, null, err => { if (err) { this.semaphore.release(); return errorAndCallback(err); } if (currentProfile) { const afterBuilding = Date.now(); currentProfile.building = afterBuilding - afterFactory; } this.semaphore.release(); afterBuild(); }); } else { this.semaphore.release(); this.waitForBuildingFinished(module, afterBuild); } } ); }); } addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); const slot = { name: name, // TODO webpack 5 remove `request` request: null, module: null }; if (entry instanceof ModuleDependency) { slot.request = entry.request; } // TODO webpack 5: merge modules instead when multiple entry modules are supported const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name); if (idx >= 0) { // Overwrite existing entrypoint this._preparedEntrypoints[idx] = slot; } else { this._preparedEntrypoints.push(slot); } // this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { if (err) { this.hooks.failedEntry.call(entry, name, err); return callback(err); } if (module) { slot.module = module; } else { const idx = this._preparedEntrypoints.indexOf(slot); if (idx >= 0) { this._preparedEntrypoints.splice(idx, 1); } } this.hooks.succeedEntry.call(entry, name, module); return callback(null, module); } ); }

addEntry 实现

lib/NormalModule.js

class NormalModule { constructor (data) { this.name = data.name; this.entry = data.entry; this.rawRequest = data.rawRequest; this.parser = data.parser; this.resource = data.resource; this._source = undefined; // 模块源代码 this._ast = undefined; // 模块源代码对应的 AST } } module.exports = NormalModule;

lib/NormalModuleFactory.js

const NormalModule = require('./NormalModule'); class NormalModuleFactory { create (data) { return new NormalModule(data); } } module.exports = NormalModuleFactory;

lib/Compilation

const path = require('path'); const { Tapable, SyncHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const normalModuleFactory = new NormalModuleFactory(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.hooks = [ successModule: new SyncHook(['module']) ] } _addModuleChain (context, entry, name) { let entryModule = normalModuleFactory.create({ name, context, rawRequest: entry, resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径 // parser }); const afterBuild = function (err) { callback(err, entryModule); } this.buildModule(entryModule, afterBuild); // 完成本次 build 之后,将 Module 进行保存 this.entries.push(entryModule); this.modules.push(entryModule); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } } module.exports = Compilation;

buildModule 实现

lib/Compilation.js

const path = require('path'); const { Tapable, SyncHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.hooks = { successModule: new SyncHook(['module']) } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err); }); } _addModuleChain (context, entry, name, callback) { let entryModule = normalModuleFactory.create({ name, context, rawRequest: entry, resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径 parser }); const afterBuild = function (err) { callback(err, entryModule); } this.buildModule(entryModule, afterBuild); // 完成本次 build 之后,将 Module 进行保存 this.entries.push(entryModule); this.modules.push(entryModule); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } } module.exports = Compilation;

lib/Compiler.js

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); const Stats = require('./Stats'); const NormalModuleFactory = require('./NormalModuleFactory'); const Compilation = require('./Compilation'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']) } } newCompilationParams () { const params = { normalModuleFactory: new NormalModuleFactory() } return params; } createCompilation () { return new Compilation(this); } newCompilation (params) { const compilation = this.createCompilation(); this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } compile (callback) { const params = this.newCompilationParams(); this.hooks.beforeRun.callAsync(params, (err) => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, (err) => { callback(err, compilation); }); }); } run (callback) { const finalCallback = function (err, status) { callback(err, status); } const onCompiled = function (err, compilation) { console.log('onCompiled'); finalCallback(err, new Stats(compilation)); } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled); }); }); } } module.exports = Compiler;

lib/NormalModule

class NormalModule { constructor (data) { this.name = data.name; this.entry = data.entry; this.rawRequest = data.rawRequest; this.parser = data.parser; this.resource = data.resource; this._source = undefined; // 模块源代码 this._ast = undefined; // 模块源代码对应的 AST } getSource (compilation, callback) { compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback); } doBuild (compilation, callback) { this.getSource(compilation, (err, source) => { this._source = source; callback(); }); } build (compilation, callback) { // 从文件中读取需要被加载的 module 内容 // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块 // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树 // 当且 js 模块内部可能又引用很多其他模块,需要递归处理 this.doBuild(compilation, (err) => { this._ast = this.parser.parse(this._source); callback(err); }); } } module.exports = NormalModule;

lib/Parser.js

const babylon = require('babylon'); const { Tapable } = require('tapable'); class Parser extends Tapable { parse (source) { return babylon.parse(source, { sourceType: 'module', plugins: ['dynamicImport'], // 支持 import 动态导入的语法 }); } } module.exports = Parser;

lib/Stats.js

class Stats { constructor (compilation) { this.entries = compilation.entries; this.modules = compilation.modules; } toJson () { return this; } } module.exports = Stats;

依赖模块处理

yarn add @babel/core @babel/generator @babel/traverse @babel/types neo-async -D
Stats { entries: [ NormalModule { name: 'main', rawRequest: './src/index.js', parser: [Parser], resource: 'D:\\workspace\\notes\\webpack\\webpack_write_plus\\webpack_write/src/index.js', _source: "const title = require('./title');\r\n" + '\r\n' + "console.log('index');\r\n" + 'console.log(name);', _ast: [Node] } ], modules: [ NormalModule { name: 'main', rawRequest: './src/index.js', parser: [Parser], resource: 'D:\\workspace\\notes\\webpack\\webpack_write_plus\\webpack_write/src/index.js', _source: "const title = require('./title');\r\n" + '\r\n' + "console.log('index');\r\n" + 'console.log(name);', _ast: [Node] } ] }
  • 需要将 Index.js 中的 require 方法替换成 __webpack_require__
  • 需要将 ./title 替换成 ./src/title.js
  • 实现对模块递归处理

lib/Compilation.js

const path = require('path'); const { Tapable, SyncHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.hooks = { successModule: new SyncHook(['module']) } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err, module); }); } processDependcies (module, callback) { // 当前的函数的功能就是实现一个被依赖模块的递归加载 // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来 // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async) } _addModuleChain (context, entry, name, callback) { let entryModule = normalModuleFactory.create({ name, context, rawRequest: entry, resource: path.posix.join(context, entry), // 返回 entry 入口的绝对路径 parser }); const afterBuild = function (err, module) { // 我们需要判断当前 module 存在依赖 if (module.dependencies.length > 0) { // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现 this.processDependcies(module, (err) => { callback(err, module); }); } else { callback(err, module); } } this.buildModule(entryModule, afterBuild); // 完成本次 build 之后,将 Module 进行保存 this.entries.push(entryModule); this.modules.push(entryModule); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } } module.exports = Compilation;

lib/NormalModule.js

const path = require('path'); const types = require('@babel/types'); const generator = require('@babel/generator').default; const traverse = require('@babel/traverse').default; class NormalModule { constructor (data) { this.name = data.name; this.context = data.context; this.rawRequest = data.rawRequest; this.parser = data.parser; this.resource = data.resource; this._source = undefined; // 模块源代码 this._ast = undefined; // 模块源代码对应的 AST this.dependencies = []; // 定义空数组,用于保存被依赖加载的模块信息 } getSource (compilation, callback) { compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback); } doBuild (compilation, callback) { this.getSource(compilation, (err, source) => { this._source = source; callback(); }); } build (compilation, callback) { // 从文件中读取需要被加载的 module 内容 // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块 // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树 // 当且 js 模块内部可能又引用很多其他模块,需要递归处理 this.doBuild(compilation, (err) => { this._ast = this.parser.parse(this._source); // _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 树转换为 code // https://astexplorer.net traverse(this._ast, { CallExpression: (nodePath) => { const node = nodePath.node; // 定位 require 所在的节点 if (node.callee.name === 'require') { // 获取原始请求路径 const modulePath = node.arguments[0].value; // './title' // 获取当前被加载的模块名称 let moduleName = modulePath.split(path.posix.sep).pop(); // title // 当前只处理 js,只考虑 js 文件处理 const extName = moduleName.indexOf('.') === -1 ? '.js' : ''; // 拼接路径 moduleName += extName; // title.js // 拼接绝对路径 const depResource = path.posix.join(path.posix.dirname(this.resource), moduleName); // 将当前模块的 ID 定义 ok const depModuleId = './' + path.posix.relative(this.context, depResource); // ./src/title.js // 保存当前被依赖模块的信息,方便后续递归加载 this.dependencies.push({ name: this.name, // TODO context: this.context, rawRequest: moduleName, moduleId: depModuleId, resource: depResource }); // 替换内容 node.callee.name = '__webpack_require__'; node.arguments = [types.stringLiteral(depModuleId)]; } } }); // 利用 ast 修改代码后,然后需要将修改后的 ast 树转会可执行 code const { code } = generator(this._ast); this._source = code; callback(err); }); } } module.exports = NormalModule;

抽离 createModule 方法

const path = require('path'); const { Tapable, SyncHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.hooks = { successModule: new SyncHook(['module']) } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err, module); }); } processDependcies (module, callback) { // 当前的函数的功能就是实现一个被依赖模块的递归加载 // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来 // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async) } _addModuleChain (context, entry, name, callback) { this.createModule({ name, context, parser, rawRequest: entry, resource: path.posix.join(context, entry), moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)) }, (entryModule) => { this.entries.push(entryModule); }, callback); } /** * @description 定义一个创建模块的方法,复用 * @param {*} data 创建模块时所需要的一些配置 * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries * @param {*} callback */ createModule (data, doAddEntry, callback) { let module = normalModuleFactory.create(data); const afterBuild = (err, module) => { // 我们需要判断当前 module 存在依赖 if (module.dependencies.length > 0) { // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现 this.processDependcies(module, (err) => { callback(err, module); }); } else { callback(err, module); } } this.buildModule(module, afterBuild); // 完成本次 build 之后,将 Module 进行保存 doAddEntry && doAddEntry(module); this.modules.push(module); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } } module.exports = Compilation;

编译依赖模块

const path = require('path'); const async = require('neo-async'); const { Tapable, SyncHook } = require('tapable'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.hooks = { successModule: new SyncHook(['module']) } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err, module); }); } processDependcies (module, callback) { // 当前的函数的功能就是实现一个被依赖模块的递归加载 // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来 // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async) const dependencies = module.dependencies; async.forEach(dependencies, (dependency, done) => { this.createModule({ parser, name: dependency.name, context: dependency.context, rawRequest: dependency.rawRequest, moduleId: dependency.moduleId, resource: dependency.resource }, null, done); }, callback); } _addModuleChain (context, entry, name, callback) { this.createModule({ name, context, parser, rawRequest: entry, resource: path.posix.join(context, entry), moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)) }, (entryModule) => { this.entries.push(entryModule); }, callback); } /** * @description 定义一个创建模块的方法,复用 * @param {*} data 创建模块时所需要的一些配置 * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries * @param {*} callback */ createModule (data, doAddEntry, callback) { let module = normalModuleFactory.create(data); const afterBuild = (err, module) => { // 我们需要判断当前 module 存在依赖 if (module.dependencies.length > 0) { // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现 this.processDependcies(module, (err) => { callback(err, module); }); } else { callback(err, module); } } this.buildModule(module, afterBuild); // 完成本次 build 之后,将 Module 进行保存 doAddEntry && doAddEntry(module); this.modules.push(module); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } } module.exports = Compilation;

chunk 流程分析及实现

lib/Stats.js

class Stats { constructor (compilation) { this.entries = compilation.entries; this.modules = compilation.modules; this.chunks = compilation.chunks; } toJson () { return this; } } module.exports = Stats;

lib/Chunk.js

class Chunk { constructor (entryModule) { this.entryModule = entryModule; this.name = entryModule.name; this.files = []; // 记录 chunk 的文件信息 this.modules = []; // 记录 chunk 包含的模块 } } module.exports = Chunk;

lib/Compilation.js

const path = require('path'); const async = require('neo-async'); const { Tapable, SyncHook } = require('tapable'); const Chunk = require('./Chunk'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.chunks = []; // 存放打包过程中产出的 chunk this.hooks = { successModule: new SyncHook(['module']), seal: new SyncHook(), beforeChunks: new SyncHook(), afterChunks: new SyncHook() } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err, module); }); } processDependcies (module, callback) { // 当前的函数的功能就是实现一个被依赖模块的递归加载 // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来 // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async) const dependencies = module.dependencies; async.forEach(dependencies, (dependency, done) => { this.createModule({ parser, name: dependency.name, context: dependency.context, rawRequest: dependency.rawRequest, moduleId: dependency.moduleId, resource: dependency.resource }, null, done); }, callback); } _addModuleChain (context, entry, name, callback) { this.createModule({ name, context, parser, rawRequest: entry, resource: path.posix.join(context, entry), moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)) }, (entryModule) => { this.entries.push(entryModule); }, callback); } /** * @description 定义一个创建模块的方法,复用 * @param {*} data 创建模块时所需要的一些配置 * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries * @param {*} callback */ createModule (data, doAddEntry, callback) { let module = normalModuleFactory.create(data); const afterBuild = (err, module) => { // 我们需要判断当前 module 存在依赖 if (module.dependencies.length > 0) { // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现 this.processDependcies(module, (err) => { callback(err, module); }); } else { callback(err, module); } } this.buildModule(module, afterBuild); // 完成本次 build 之后,将 Module 进行保存 doAddEntry && doAddEntry(module); this.modules.push(module); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } // 封装 chunk seal (callback) { this.hooks.seal.call(); this.hooks.beforeChunks.call(); // 所有的入口模块都被存放在 compilation 对象的 entries 数组中 // 封装 chunk 指的就是根据某个入口,找到它的所有依赖,将它们的源代码放到一起,之后再进行合并 for (const entryModule of this.entries) { // 创建模块,加载已有模块内容,同时记录模块信息 const chunk = new Chunk(entryModule); // 保存 chunk 信息 this.chunks.push(chunk); // 给 chunk 属性赋值 chunk.modules = this.modules.filter(module => module.name === chunk.name); } callback(); } } module.exports = Compilation;

lib/Compiler.js

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); const Stats = require('./Stats'); const NormalModuleFactory = require('./NormalModuleFactory'); const Compilation = require('./Compilation'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']) } } newCompilationParams () { const params = { normalModuleFactory: new NormalModuleFactory() } return params; } createCompilation () { return new Compilation(this); } newCompilation (params) { const compilation = this.createCompilation(); this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } compile (callback) { const params = this.newCompilationParams(); this.hooks.beforeRun.callAsync(params, (err) => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, (err) => { // 开始处理 chunk compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, (err) => { callback(err, compilation); }) }); }); }); } run (callback) { const finalCallback = function (err, status) { callback(err, status); } const onCompiled = function (err, compilation) { console.log('onCompiled'); // 将处理好的 chunk 写入到指定的文件,然后输入至 dist 目录 finalCallback(err, new Stats(compilation)); } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled); }); }); } } module.exports = Compiler;

生成 chunk 代码

yarn add ejs -D

lib/temp/main.ejs

(function (modules) { // 定义 webpackJsonpCallback:合并模块定义、改变 Promise 状态,执行后续行为 function webpackJsonpCallback (data) { // 获取需要被加载的模块 ID const chunkIds = data[0]; // 获取需要被动态加载的模块依赖关系对象 const moreModules = data[1]; let chunkId, resolves = []; // 循环判断 chunkIds 里对应的模块内容是否已经完成加载 for (let i = 0; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } // 更新当前 chunk 状态 installedChunks[chunkId] = 0; } for (moduleId in moreModules) { if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } while (resolves.length) { resolves.shift()(); } } // 缓存被加载的模块 const installedModules = {}; // 定义 installedChunks 对于用于标识某个 chunkId 对应 chunk 是否完成加载 // 0 已加载过、promises 正在加载、null/undefiend 未加载 var installedChunks = { main: 0 } // 定义 __webpack_require__ 方法替换 require function __webpack_require__ (moduleId) { // 判断当前缓存中是否存在要被加载的模块内容,如果存在,直接返回 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 如果当前缓存中不存在,定义对象 const module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 调用当前 moduleId 对应的函数,完成内容加载 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 当上述方法调用完成之后,就可以修改 l 的值用于表示当前模块内容已加载完成 module.l = true; // 加载完成之后,将模块内容返回至调用位置 return module.exports; } // 定义 m 属性保存 modules __webpack_require__.m = modules; // 定义 c 属性保存 cache __webpack_require__.c = installedModules; // 定义 o 方法用于对象身上是否存在指定属性 __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); } // 定义 d 方法用于在对象身上添加指定属性及 getter __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } } // 定义 r 方法用于标识当前模块是 ES6 类型 __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }) } // 定义 n 方法用于设置具体 getter __webpack_require__.n = function (module) { let getter = module && module.__esModule ? ( function getDefault () { return module['default'] } ) : ( function getModuleExports () { return module } ); __webpack_require__.d(getter, 'a', getter); return getter; } // 定义 jsonpScriptSrc 实现 src 的处理 function jsonpScriptSrc (chunkId) { return __webpack_require__.p + "" + chunkId + '.build.js'; } // 定义 e 方法用于实现 jsonp 来加载内容,利用 promise 实现异步加载操作 __webpack_require__.e = function (chunkId) { // 定义一个数组用于存放 promise let promises = []; // 获取 chunkId 对应的 chunk 是否已经完成加载 let installedChunkData = installedChunks[chunkId]; // 根据当前是否已完成加载的状态来执行后续逻辑 if (installedChunkData !== 0) { if (installedChunkData) { promises.push(installedChunkData[2]); } else { const promise = new Promise((resolve, reject) => { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // 创建标签 const script = document.createElement('script'); // 设置 src script.src = jsonpScriptSrc(chunkId); // 写入 scrpit 标签 document.head.appendChild(script); } } // 执行 promise return Promise.all(promises); } // 定义 t 方法用于加载指定 value 的模块内容,对内容进行处理并返回 __webpack_require__.t = function (value, mode) { // 加载 value 对应的模块内容(value 通常是模块 ID) if (mode & 1) { value = __webpack_require__(value); } // commonjs if (mode & 8) { return value; } // esmodule if ((mode & 4 && typeof value === 'object' && value && value.__esModule)) { return value; } // 如果 8 和 4 都不成立,则需要自定义 ns,通过 default 属性返回内容 const ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value }); if (mode & 2 && typeof value !== 'string') { for (const k in value) { __webpack_require__.d(ns, k, function (key) { return value[key]; }.bind(null, k)); } } return ns; } // 定义 p 属性用于保存资源访问路径 __webpack_require__.p = ""; // 定义变量存放数组 const jsonpArray = window['webpackJsonp'] = window['webpackJsonp'] || []; // 保存原生的 push 方法 const oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 重写原生的 push 方法 jsonpArray.push = webpackJsonpCallback; // 调用 __webpack_require__ 方法执行模块导入与加载操作 return __webpack_require__(__webpack_require__.s = '<%-entryModuleId%>'); }) ({ <% for (let module of modules) { %> "<%- module.moduleId %>": (function(module, exports, __webpack_require__) { <%- module._source %> }), <% } %> });

lib/Compilation.js

const path = require('path'); const async = require('neo-async'); const ejs = require('ejs'); const { Tapable, SyncHook } = require('tapable'); const Chunk = require('./Chunk'); const NormalModuleFactory = require('./NormalModuleFactory'); const Parser = require('./Parser'); const normalModuleFactory = new NormalModuleFactory(); const parser = new Parser(); class Compilation extends Tapable { constructor (compiler) { super(); this.compiler = compiler; this.context = compiler.context; this.options = compiler.options; this.inputFileSystem = compiler.inputFileSystem; this.outputFileSystem = compiler.outputFileSystem; this.entries = []; // 存放所有入口模块数组 this.modules = []; // 存放所有模块数组 this.chunks = []; // 存放打包过程中产出的 chunk this.assets = []; this.files = []; this.hooks = { successModule: new SyncHook(['module']), seal: new SyncHook(), beforeChunks: new SyncHook(), afterChunks: new SyncHook() } } // 完成具体的 build 行为 buildModule (module, callback) { module.build(this, (err) => { // module 编译完成 this.hooks.successModule.call(module); callback(err, module); }); } processDependcies (module, callback) { // 当前的函数的功能就是实现一个被依赖模块的递归加载 // 加载模块的思路都是创建一个模块,然后将加载到的模块内容拿进来 // 当前并不知道 module 需要依赖几个模块,此时需要想办法让所有被依赖的模块都加载完成之后再执行 callback(neo-async) const dependencies = module.dependencies; async.forEach(dependencies, (dependency, done) => { this.createModule({ parser, name: dependency.name, context: dependency.context, rawRequest: dependency.rawRequest, moduleId: dependency.moduleId, resource: dependency.resource }, null, done); }, callback); } _addModuleChain (context, entry, name, callback) { this.createModule({ name, context, parser, rawRequest: entry, resource: path.posix.join(context, entry), moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)) }, (entryModule) => { this.entries.push(entryModule); }, callback); } /** * @description 定义一个创建模块的方法,复用 * @param {*} data 创建模块时所需要的一些配置 * @param {*} doAddEntry 可选参数,加载入口模块时,将入口模块的 id 写入 this.entries * @param {*} callback */ createModule (data, doAddEntry, callback) { let module = normalModuleFactory.create(data); const afterBuild = (err, module) => { // 我们需要判断当前 module 存在依赖 if (module.dependencies.length > 0) { // 当前逻辑表示存在需要依赖加载的模块,我们可以单独定义一个方法实现 this.processDependcies(module, (err) => { callback(err, module); }); } else { callback(err, module); } } this.buildModule(module, afterBuild); // 完成本次 build 之后,将 Module 进行保存 doAddEntry && doAddEntry(module); this.modules.push(module); } // 完成模块编译操作 addEntry (context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module); }); } // 封装 chunk seal (callback) { this.hooks.seal.call(); this.hooks.beforeChunks.call(); // 所有的入口模块都被存放在 compilation 对象的 entries 数组中 // 封装 chunk 指的就是根据某个入口,找到它的所有依赖,将它们的源代码放到一起,之后再进行合并 for (const entryModule of this.entries) { // 创建模块,加载已有模块内容,同时记录模块信息 const chunk = new Chunk(entryModule); // 保存 chunk 信息 this.chunks.push(chunk); // 给 chunk 属性赋值 chunk.modules = this.modules.filter(module => module.name === chunk.name); } // chunk 代码处理环节(模板文件 + 模块内的源代码 => chunk.js) this.hooks.afterChunks.call(this.chunks); // 生成代码内容 this.createChunkAssets(); callback(); } createChunkAssets () { for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; const fileName = chunk.name + '.js'; chunk.files.push(fileName); // 获取模板文件路径 const tempPath = path.posix.join(__dirname, 'temp/main.ejs'); // 读取模块文件中的内容 const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8'); // 获取渲染函数 const tempRender = ejs.compile(tempCode); // 使用 ejs 语法渲染数据 let source = tempRender({ entryModuleId: chunk.entryModule.moduleId, modules: chunk.modules }); // 输出文件 this.emitAssets(fileName, source); } } emitAssets (fileName, source) { this.assets[fileName] = source; this.files.push(fileName); } } module.exports = Compilation;

lib/NormalModule.js

const path = require('path'); const types = require('@babel/types'); const generator = require('@babel/generator').default; const traverse = require('@babel/traverse').default; class NormalModule { constructor (data) { this.name = data.name; this.context = data.context; this.moduleId = data.moduleId; this.rawRequest = data.rawRequest; this.parser = data.parser; this.resource = data.resource; this._source = undefined; // 模块源代码 this._ast = undefined; // 模块源代码对应的 AST this.dependencies = []; // 定义空数组,用于保存被依赖加载的模块信息 } getSource (compilation, callback) { compilation.inputFileSystem.readFile(this.resource, 'utf-8', callback); } doBuild (compilation, callback) { this.getSource(compilation, (err, source) => { this._source = source; callback(); }); } build (compilation, callback) { // 从文件中读取需要被加载的 module 内容 // 如果当前不是 js 模块,则需要 loader 进行处理,最终也是返回 js 模块 // 上述操作完成之后,就可以将 js 代码转换为 ast 语法树 // 当且 js 模块内部可能又引用很多其他模块,需要递归处理 this.doBuild(compilation, (err) => { this._ast = this.parser.parse(this._source); // _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 树转换为 code // https://astexplorer.net traverse(this._ast, { CallExpression: (nodePath) => { const node = nodePath.node; // 定位 require 所在的节点 if (node.callee.name === 'require') { // 获取原始请求路径 const modulePath = node.arguments[0].value; // './title' // 获取当前被加载的模块名称 let moduleName = modulePath.split(path.posix.sep).pop(); // title // 当前只处理 js,只考虑 js 文件处理 const extName = moduleName.indexOf('.') === -1 ? '.js' : ''; // 拼接路径 moduleName += extName; // title.js // 拼接绝对路径 const depResource = path.posix.join(path.posix.dirname(this.resource), moduleName); // 将当前模块的 ID 定义 ok const depModuleId = './' + path.posix.relative(this.context, depResource); // ./src/title.js // 保存当前被依赖模块的信息,方便后续递归加载 this.dependencies.push({ name: this.name, // TODO context: this.context, rawRequest: moduleName, moduleId: depModuleId, resource: depResource }); // 替换内容 node.callee.name = '__webpack_require__'; node.arguments = [types.stringLiteral(depModuleId)]; } } }); // 利用 ast 修改代码后,然后需要将修改后的 ast 树转会可执行 code const { code } = generator(this._ast); this._source = code; callback(err); }); } } module.exports = NormalModule;

生成打包文件

src/index.js

const { name } = require('./title'); console.log('index'); console.log(name);

lib/Stat.js

class Stats { constructor (compilation) { this.entries = compilation.entries; this.modules = compilation.modules; this.chunks = compilation.chunks; this.files = compilation.files; } toJson () { return this; } } module.exports = Stats;

lib/webpack.js

const Compiler = require('./Compiler'); const NodeEnvironmentPlugin = require('./node/NodeEnvironmentPlugin'); const WebpackOptionApply = require('./WebpackOptionApply'); const webpack = function (options) { // 实例化 compiler 对象 const compiler = new Compiler(options.context); compiler.options = options; // 初始化 NodeEnvironmentPlugin new NodeEnvironmentPlugin().apply(compiler); // 挂载所有的 plugins 插件至 compiler 对象身上 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 挂载所有的 webpack 内置插件 new WebpackOptionApply().process(options, compiler); // 返回 compiler 对象 return compiler; } module.exports = webpack;

lib/Compiler.js

const { Tapable, AsyncSeriesHook, SyncBailHook, SyncHook, AsyncParallelBailHook } = require('tapable'); const path = require('path'); const mkdirp = require('mkdirp'); const Stats = require('./Stats'); const NormalModuleFactory = require('./NormalModuleFactory'); const Compilation = require('./Compilation'); class Compiler extends Tapable { constructor (context) { super(); this.context = context; this.hooks = { done: new AsyncSeriesHook(['stats']), entryOption: new SyncBailHook(['context', 'entry']), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(['params']), compile: new SyncHook(['params']), make: new AsyncParallelBailHook(['compilation']), afterCompile: new AsyncSeriesHook(['compilation']), emit: new AsyncSeriesHook(['compilation']) } } newCompilationParams () { const params = { normalModuleFactory: new NormalModuleFactory() } return params; } createCompilation () { return new Compilation(this); } newCompilation (params) { const compilation = this.createCompilation(); this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } compile (callback) { const params = this.newCompilationParams(); this.hooks.beforeRun.callAsync(params, (err) => { this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, (err) => { // 开始处理 chunk compilation.seal(err => { this.hooks.afterCompile.callAsync(compilation, (err) => { callback(err, compilation); }) }); }); }); } emitAssets (compilation, callback) { // 定义工具方法,用于文件生成操作 const emitFiles = (err) => { const assets = compilation.assets; const outputPath = this.options.output.path; for (let file in assets) { const source = assets[file]; const targetPath = path.posix.join(outputPath, file); this.outputFileSystem.writeFileSync(targetPath, source, 'utf8'); } callback(err); } // 创建目录,准备文件写入 this.hooks.emit.callAsync(compilation, (err) => { mkdirp.sync(this.options.output.path); emitFiles(); }); } run (callback) { const finalCallback = function (err, status) { callback(err, status); } const onCompiled = (err, compilation) => { // 将处理好的 chunk 写入到指定的文件,然后输入至 dist 目录 this.emitAssets(compilation, (err) => { finalCallback(err, new Stats(compilation)); }); } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled); }); }); } } module.exports = Compiler;