命令行输入webpack的时候都发生了什么?

no7281 7年前
   <p>我们在使用 webpack 的时候可以通过 webpack 这个命令配合一些参数来执行我们打包编译的任务。我们想探究它的源码,从这个命令入手能够比较容易让我们了解整个代码的运行过程。那么在执行这个命令的时候究竟发生了什么呢?</p>    <p>注:本文中的 webpack 源码版本为1.13.3。本文中的源码分析主要关注的是代码的整体流程,因此一些我认为不是很重要的细节都会省略,以使得读者不要陷入到细节中而 get 不到整体。按照官方文档,webpack.config.js 会通过 module.exports 暴露一个对象,下文中我们统一把这个对象称为 webpack 编译对象(Webpack compiler object)。</p>    <h2>Step1:执行脚本 bin/webpack.js</h2>    <pre>  <code class="language-javascript">// 引入 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) ;</code></pre>    <p>小结1.1:从上面的分析中我们可以比较清晰地看到执行 webpack 命令时会做什么处理,主要就是解析命令行参数以及执行编译。其中 processOptions 这个函数是整个 /bin/webpack.js 里的核心函数。下面我们来仔细看一下这个函数:</p>    <pre>  <code class="language-javascript">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) ;  }</code></pre>    <p>小结1.2:从 processOptions 中我们看到,最核心的编译一步,是使用的入口模块 lib/webpack.js 暴露处理的方法,所以我们的数据流接下来要从 bin/webpack.js 来到 lib/webpack.js 了,接下来我们看看 lib/webpack.js 里将会发生什么。</p>    <h2>step2:执行 lib/webpack.js 中的方法开始编译</h2>    <pre>  <code class="language-javascript">// 引入 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", ...]) ;</code></pre>    <p>小结2.1:lib/webpack.js 文件里的代码比较清晰,核心函数就是我们期待已久的 webpack,我们在 webpack.config.js 里面引入的 webpack 模块就是这个文件,下面我们再来仔细看看这个函数。</p>    <pre>  <code class="language-javascript">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") ;    }  }</code></pre>    <p>小结2.2:webpack 函数里面有两个地方值得关注一下。</p>    <p>一是 Compiler,实例化它会继承 <a href="/misc/goto?guid=4959733349149865772" rel="nofollow,noindex"> Tapable </a> ,这个 Tapable 是一个插件框架,通过继承它的一系列方法来实现注册和调用插件,我们可以看到在 webpack 的源码中,存在大量的 compiler.apply、compiler.applyPlugins、compiler.plugin 等Tapable方法的调用。Webpack 的 <a href="/misc/goto?guid=4959733349238645702" rel="nofollow,noindex"> plugin </a> 注册和调用方式,都是源自 Tapable 。Webpack 通过 plugin 的 apply 方法安装该 plugin,同时传入一个 webpack 编译对象(Webpack compiler object)。</p>    <p>二是 WebpackOptionsApply 的实例方法 process (options, compiler),这个方法将会针对我们传进去的webpack 编译对象进行逐一编译,接下来我们再来仔细看看这个模块。</p>    <p>step3:调用 lib/WebpackOptionsApply.js 模块的 process 方法来逐一编译 webpack 编译对象的各项。</p>    <pre>  <code class="language-javascript">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;  };</code></pre>    <p>小结3.1:我们可以在上面的代码中看到 webpack 文档中 Configuration 中介绍的各个属性,同时看到了这些属性对应的处理插件都是谁。我个人看完这里之后,熟悉了好几个平常不怎么用到,但是感觉还是很有用的东西,例如 externals 和 define 属性。</p>    <h2>step4:在 step3 中调用的各种插件会按照 webpack 编译对象的配置来构建出文件</h2>    <p>由于插件繁多,切每个插件都有不同的细节,我们这里选择一个大家可能比较熟悉的插件 UglifyJsPlugin.js(压缩代码插件)来理解 webpack 的流程。</p>    <pre>  <code class="language-javascript">// 引入一些依赖,主要是与 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;          });      });  };</code></pre>    <p>小结4.1:从这个插件的源码分析,我们可以基本看到 webpack 编译时的读写过程大致是怎么样的:实例化插件(如 UglifyJsPlugin )--> 读取源文件 --> 编译并输出</p>    <h2>总结</h2>    <p>现在我们回过头来再看看整体流程,当我们在命令行输入 webpack 命令,按下回车时都发生了什么: 1. 执行 bin 目录下的 webpack.js 脚本,解析命令行参数以及开始执行编译。 2. 调用 lib 目录下的 webpack.js 文件的核心函数 webpack ,实例化一个 Compiler,继承 Tapable 插件框架,实现注册和调用一系列插件。 3. 调用 lib 目录下的 /WebpackOptionsApply.js 模块的 process 方法,使用各种各样的插件来逐一编译 webpack 编译对象的各项。 4. 在3中调用的各种插件编译并输出新文件。</p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/24717349</p>    <p> </p>