Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[webpack]源码解读:命令行输入webpack的时候都发生了什么? #12

Open
zyf394 opened this issue Jan 5, 2017 · 6 comments

Comments

@zyf394
Copy link

zyf394 commented Jan 5, 2017

我们在使用 webpack 的时候可以通过 webpack 这个命令配合一些参数来执行我们打包编译的任务。我们想探究它的源码,从这个命令入手能够比较容易让我们了解整个代码的运行过程。那么在执行这个命令的时候究竟发生了什么呢?

注:本文中的 webpack 源码版本为1.13.3。本文中的源码分析主要关注的是代码的整体流程,因此一些我认为不是很重要的细节都会省略,以使得读者不要陷入到细节中而 get 不到整体。按照官方文档,webpack.config.js 会通过 module.exports 暴露一个对象,下文中我们统一把这个对象称为 webpack 编译对象(Webpack compiler object)。

Step1:执行脚本 bin/webpack.js

// bin/webpack.js

// 引入 nodejs 的 path 模块
var path = require ("path") ;
// 获取 /bin/webpack.js 的绝对路径
try {
  var localWebpack = require.resolve (path.join (process.cwd (), "node_modules", "webpack", "bin", "webpack.js")) ;
  if (__filename !== localWebpack) {}
} catch (e) {}

// 引入第三方命令行解析库 optimist
// 解析 webpack 指令后面追加的与输出显示相关的参数(Display options)
var optimist = require ("optimist").usage ((("webpack " + require ("../package.json").version) + "\n") + "Usage: https://webpack.github.io/docs/cli.html") ;
require ("./config-optimist") (optimist) ;
optimist
  .boolean ("json").alias ("json", "j").describe ("json")
  .boolean ("colors").alias ("colors", "c")... ;

// 获取解析后的参数并转换格式
var argv = optimist.argv ;
var options = require ("./convert-argv") (optimist, argv) ;

// 判断是否符合 argv 里的参数,并执行该参数的回调
function ifArg (name, fn, init) {...}

// 处理输出相关(output)的配置参数,并执行编译函数
function processOptions (options) {...}
// 执行
processOptions (options) ;

小结1.1:从上面的分析中我们可以比较清晰地看到执行 webpack 命令时会做什么处理,主要就是解析命令行参数以及执行编译。其中 processOptions 这个函数是整个 /bin/webpack.js 里的核心函数。下面我们来仔细看一下这个函数:

function processOptions (options) {
 // 支持 Promise 风格的异步回调
  if ((typeof options.then) === "function") {...}

 // 处理传入一个 webpack 编译对象是数组时的情况
  var firstOptions = (Array.isArray (options)) ? options[0]: options;

 // 设置输出 options
  var outputOptions = Object.create ((options.stats || firstOptions.stats) || ({}));

 // 设置输出的上下文 context
  if ((typeof outputOptions.context) === "undefined") outputOptions.context = firstOptions.context ;

  // 处理各种显示相关的参数,从略
  ifArg ("json", 
    function (bool){...}
  );
  ...

  // 引入主入口模块 lib/webpack.js
  var webpack = require ("../lib/webpack.js") ;

  // 设置错误堆栈追踪上限
  Error.stackTraceLimit = 30 ;
  var lastHash = null ;

 // 执行编译
  var compiler = webpack (options) ;

 // 编译结束后的回调函数
  function compilerCallback (err, stats) {...}

 // 是否在编译完成后继续 watch 文件变更
  if (options.watch) {...}
  else 
 // 执行编译后的回调函数
  compiler.run (compilerCallback) ;
}

小结1.2:从 processOptions 中我们看到,最核心的编译一步,是使用的入口模块 lib/webpack.js 暴露处理的方法,所以我们的数据流接下来要从 bin/webpack.js 来到 lib/webpack.js 了,接下来我们看看 lib/webpack.js 里将会发生什么。

step2:执行 lib/webpack.js 中的方法开始编译

// lib/webpack.js

// 引入 Compiler 模块
var Compiler = require ("./Compiler") ;

// 引入 MultiCompiler 模块,处理多个 webpack 配置文件的情况
var MultiCompiler = require ("./MultiCompiler") ;

// 引入 node 环境插件
var NodeEnvironmentPlugin = require ("./node/NodeEnvironmentPlugin") ;

// 引入 WebpackOptionsApply 模块,应用 webpack 配置文件
var WebpackOptionsApply = require ("./WebpackOptionsApply") ;

// 引入 WebpackOptionsDefaulter 模块,应用 webpack 默认配置
var WebpackOptionsDefaulter = require ("./WebpackOptionsDefaulter") ;

// 核心函数,也是 ./bin/webpack.js 中引用的核心方法
function webpack (options, callback) {...}
exports = module.exports = webpack ;

// 在 webpack 对象上设置一些常用属性
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter ;
webpack.WebpackOptionsApply = WebpackOptionsApply ;
webpack.Compiler = Compiler ;
webpack.MultiCompiler = MultiCompiler ;
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin ;

// 暴露一些插件
function exportPlugins (exports, path, plugins) {...}
exportPlugins (exports, ".", ["DefinePlugin", "NormalModuleReplacementPlugin", ...]) ;

小结2.1lib/webpack.js 文件里的代码比较清晰,核心函数就是我们期待已久的 webpack,我们在 webpack.config.js 里面引入的 webpack 模块就是这个文件,下面我们再来仔细看看这个函数。

function webpack (options, callback) {
  var compiler ;
  if (Array.isArray (options)) {
    // 如果传入了数组类型的 webpack 编译对象,则实例化一个 MultiCompiler 来处理
    compiler = new MultiCompiler (options.map(function (options) {
      return webpack (options) ; // 递归调用 webpack 函数
    })) ;
  } else if ((typeof options) === "object") {
   // 如果传入了一个对象类型的 webpack 编译对象
  
    // 实例化一个 WebpackOptionsDefaulter 来处理默认配置项
    new WebpackOptionsDefaulter ().process (options) ;

    // 实例化一个 Compiler,Compiler 会继承一个 Tapable 插件框架
    // Compiler 实例化后会继承到 apply、plugin 等调用和绑定插件的方法
    compiler = new Compiler () ;

   // 实例化一个 WebpackOptionsApply 来编译处理 webpack 编译对象
    compiler.options = options ; // 疑惑:为何两次赋值 compiler.options?
    compiler.options = new WebpackOptionsApply ().process (options, compiler) ;

  // 应用 node 环境插件
    new NodeEnvironmentPlugin ().apply (compiler) ;
    compiler.applyPlugins ("environment") ;
    compiler.applyPlugins ("after-environment") ;
  } else {
    // 抛出错误
    throw new Error ("Invalid argument: options") ;
  }
}

小结2.2webpack 函数里面有两个地方值得关注一下。

一是 Compiler,实例化它会继承 Tapable ,这个 Tapable 是一个插件框架,通过继承它的一系列方法来实现注册和调用插件,我们可以看到在 webpack 的源码中,存在大量的 compiler.apply、compiler.applyPlugins、compiler.plugin 等Tapable方法的调用。Webpack 的 plugin 注册和调用方式,都是源自 Tapable 。Webpack 通过 plugin 的 apply 方法安装该 plugin,同时传入一个 webpack 编译对象(Webpack compiler object)。

二是 WebpackOptionsApply 的实例方法 process (options, compiler),这个方法将会针对我们传进去的webpack 编译对象进行逐一编译,接下来我们再来仔细看看这个模块。

step3:调用 lib/WebpackOptionsApply.js 模块的 process 方法来逐一编译 webpack 编译对象的各项

// lib/WebpackOptionsApply.js

// ...此处省略一堆依赖引入

// 创建构造器函数 WebpackOptionsApply
function WebpackOptionsApply () {
  OptionsApply.call (this) ;
}

// 将构造器暴露
module.exports = WebpackOptionsApply ;

// 修改构造器的原型属性指向
WebpackOptionsApply.prototype = Object.create (OptionsApply.prototype) ;

// 创建 WebpackOptionsApply 的实例方法 process
WebpackOptionsApply.prototype.process = function (options, compiler) {
 // 处理 context 属性,根目录
  compiler.context = options.context ;
 // 处理 plugins 属性
  if (options.plugins && (Array.isArray (options.plugins))) {...}
// 缓存输入输出的目录地址等
  compiler.outputPath = options.output.path ;
  compiler.recordsInputPath = options.recordsInputPath || options.recordsPath ;
  compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath ;
  compiler.name = options.name ;
// 处理 target 属性,该属性决定包 (bundle) 应该运行的环境
  if ((typeof options.target) === "string") {...}
  else  if (options.target !== false) {...}
  else {...}
 // 处理 output.library 属性,该属性决定导出库 (exported library) 的名称
  if (options.output.library || (options.output.libraryTarget !== "var")) {...}
 // 处理 externals 属性,告诉 webpack 不要遵循/打包这些模块,而是在运行时从环境中请求他们
  if (options.externals) {...}
 // 处理 hot 属性,它决定 webpack 了如何使用热替换
  if (options.hot) {...}
// 处理 devtool 属性,它决定了 webpack 的 sourceMap 模式
  if (options.devtool && (((options.devtool.indexOf ("sourcemap")) >= 0) || ((options.devtool.indexOf ("source-map")) >= 0))) {...}
  else if (options.devtool && ((options.devtool.indexOf ("eval")) >= 0)) {...}

// 以下是安装并调用各种插件 plugin,由于功能众多个人阅历有限,不能面面俱到

  compiler.apply (new EntryOptionPlugin ()) ; // 调用处理入口 entry 的插件
  compiler.applyPluginsBailResult ("entry-option", options.context, options.entry) ;
  if (options.prefetch) {...}
 
  compiler.apply (new CompatibilityPlugin (),
                  new LoaderPlugin (), // 调用 loader 的插件
                  new NodeStuffPlugin (options.node), // 调用 nodejs 环境相关的插件
                  new RequireJsStuffPlugin (), // 调用 RequireJs 的插件
                  new APIPlugin (), // 调用变量名的替换,webpack 编译后的文件里随处可见的 __webpack_require__ 变量名就是在此处理
                  new ConstPlugin (), // 调用一些 if 条件语句、三元运算符等语法相关的插件
                  new RequireIncludePlugin (), // 调用 require.include 函数的插件
                  new RequireEnsurePlugin (), // 调用 require.ensure 函数的插件
                  new RequireContextPlugin(options.resolve.modulesDirectories, options.resolve.extensions),
                  new AMDPlugin (options.module, options.amd || ({})), // 调用处理符合 AMD 规范的插件
                  new CommonJsPlugin (options.module)) ; // 调用处理符合 CommonJs 规范的插件

  compiler.apply (new RemoveParentModulesPlugin (), // 调用移除父 Modules 的插件
                  new RemoveEmptyChunksPlugin (), // 调用移除空 chunk 的插件
                  new MergeDuplicateChunksPlugin (), // 调用合并重复多余 chunk 的插件
                  new FlagIncludedChunksPlugin ()) ; // 

  compiler.apply (new TemplatedPathPlugin ()) ;
  compiler.apply (new RecordIdsPlugin ()) ; // 调用记录 Modules 的 Id 的插件
  compiler.apply (new WarnCaseSensitiveModulesPlugin ()) ; // 调用警告大小写敏感的插件

  // 处理 webpack.optimize 属性下的几个方法
  if (options.optimize && options.optimize.occurenceOrder) {...} // 调用 OccurrenceOrderPlugin 插件
  if (options.optimize && options.optimize.minChunkSize) {...} // 调用 MinChunkSizePlugin 插件
  if (options.optimize && options.optimize.maxChunks) {...} // 调用 LimitChunkCountPlugin 插件
  if (options.optimize.minimize) {...} // 调用 UglifyJsPlugin 插件

  // 处理cache属性(缓存),该属性在watch的模式下默认开启缓存
  if ((options.cache === undefined) ? options.watch: options.cache) {...}
  // 处理 provide 属性,如果有则调用 ProvidePlugin 插件,这个插件可以让一个 module 赋值为一个变量,从而能在每个 module 中以变量名访问它
  if ((typeof options.provide) === "object") {...}
  // 处理define属性,如果有这个属性则调用 DefinePlugin 插件,这个插件可以定义全局的常量
  if (options.define) {...}
  // 处理 defineDebug 属性,调用并开启 DefinePlugin 插件的 debug 模式?
  if (options.defineDebug !== false) compiler.apply (new DefinePlugin ({...})) ; // 处理定义插件的
 
 // 调用一些编译完后的处理插件
  compiler.applyPlugins ("after-plugins", compiler) ;
  compiler.resolvers.normal.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
  compiler.resolvers.context.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
  compiler.resolvers.loader.apply (new UnsafeCachePlugin (options.resolve.unsafeCache)...) ;
  compiler.applyPlugins ("after-resolvers", compiler) ;

 // 最后把处理过的 webpack 编译对象返回
  return options;
};

小结3.1:我们可以在上面的代码中看到 webpack 文档中 Configuration 中介绍的各个属性,同时看到了这些属性对应的处理插件都是谁。我个人看完这里之后,熟悉了好几个平常不怎么用到,但是感觉还是很有用的东西,例如 externals 和 define 属性。

step4:在 step3 中调用的各种插件会按照 webpack 编译对象的配置来构建出文件

由于插件繁多,切每个插件都有不同的细节,我们这里选择一个大家可能比较熟悉的插件 UglifyJsPlugin.js(压缩代码插件)来理解 webpack 的流程。

// lib/optimize/UglifyJsPlugin.js

// 引入一些依赖,主要是与压缩代码、sourceMap 相关
var SourceMapConsumer = require("webpack-core/lib/source-map").SourceMapConsumer;
var SourceMapSource = require("webpack-core/lib/SourceMapSource");
var RawSource = require("webpack-core/lib/RawSource");
var RequestShortener = require("../RequestShortener");
var ModuleFilenameHelpers = require("../ModuleFilenameHelpers");
var uglify = require("uglify-js");

// 定义构造器函数
function UglifyJsPlugin(options) {
	...
}
// 将构造器暴露出去
module.exports = UglifyJsPlugin;

// 按照 Tapable 风格编写插件
UglifyJsPlugin.prototype.apply = function(compiler) {
	...
    // 编译器开始编译
	compiler.plugin("compilation", function(compilation) {
		...
        // 编译器开始调用 "optimize-chunk-assets" 插件编译
		compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
			var files = [];
			...
			files.forEach(function(file) {
				...
				try {
					var asset = compilation.assets[file];
					if(asset.__UglifyJsPlugin) {
						compilation.assets[file] = asset.__UglifyJsPlugin;
						return;
					}
					if(options.sourceMap !== false) {
			        // 需要 sourceMap 时要做的一些操作...
					} else {
						// 获取读取到的源文件
						var input = asset.source(); 
						...
					}
					// base54 编码重置
					uglify.base54.reset(); 
					// 将源文件生成语法树
					var ast = uglify.parse(input, {
						filename: file
					});
					// 语法树转换为压缩后的代码
					if(options.compress !== false) {
						ast.figure_out_scope();
						var compress = uglify.Compressor(options.compress); // eslint-disable-line new-cap
						ast = ast.transform(compress);
					}
					// 处理混淆变量名
					if(options.mangle !== false) {
						ast.figure_out_scope();
						ast.compute_char_frequency(options.mangle || {});
						ast.mangle_names(options.mangle || {});
						if(options.mangle && options.mangle.props) {
							uglify.mangle_properties(ast, options.mangle.props);
						}
					}
					// 定义输出变量名
					var output = {};
					// 处理输出的注释
					output.comments = Object.prototype.hasOwnProperty.call(options, "comments") ? options.comments : /^\**!|@preserve|@license/;
					// 处理输出的美化
					output.beautify = options.beautify;
					for(var k in options.output) {
						output[k] = options.output[k];
					}
					// 处理输出的 sourceMap
					if(options.sourceMap !== false) {
						var map = uglify.SourceMap({ // eslint-disable-line new-cap
							file: file,
							root: ""
						});
						output.source_map = map; // eslint-disable-line camelcase
					}
					// 将压缩后的数据输出
					var stream = uglify.OutputStream(output); // eslint-disable-line new-cap
					ast.print(stream);
					if(map) map = map + "";
					stream = stream + "";
					asset.__UglifyJsPlugin = compilation.assets[file] = (map ?
						new SourceMapSource(stream, file, JSON.parse(map), input, inputSourceMap) :
						new RawSource(stream));
					if(warnings.length > 0) {
						compilation.warnings.push(new Error(file + " from UglifyJs\n" + warnings.join("\n")));
					}
				} catch(err) {
					// 处理异常
					...
				} finally {
					...
				}
			});
			// 回调函数
			callback();
		});
		compilation.plugin("normal-module-loader", function(context) {
			context.minimize = true;
		});
	});
};

小结4.1:从这个插件的源码分析,我们可以基本看到 webpack 编译时的读写过程大致是怎么样的:实例化插件(如 UglifyJsPlugin )--> 读取源文件 --> 编译并输出

总结

现在我们回过头来再看看整体流程,当我们在命令行输入 webpack 命令,按下回车时都发生了什么:

  1. 执行 bin 目录下的 webpack.js 脚本,解析命令行参数以及开始执行编译。
  2. 调用 lib 目录下的 webpack.js 文件的核心函数 webpack ,实例化一个 Compiler,继承 Tapable 插件框架,实现注册和调用一系列插件。
  3. 调用 lib 目录下的 /WebpackOptionsApply.js 模块的 process 方法,使用各种各样的插件来逐一编译 webpack 编译对象的各项。
  4. 在3中调用的各种插件编译并输出新文件。
@bakso
Copy link

bakso commented Jan 12, 2017

webpack 2 实现上区别是啥?

@Thinking80s
Copy link

webpack 2 实现上区别是啥?

@GONDON
Copy link

GONDON commented Aug 9, 2018

emmmm 看完了 感觉能理解一个大概流程了~ 很棒!

@Carrie999
Copy link

请问从webpackgithub下载下来的文件是怎么调试的啊?目录都是分别做什么啊?

@17612271145
Copy link

yyds

@Hansen520
Copy link

Hansen520 commented Jan 5, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants