细说 webpack 之流程篇

0.1. 引言

目前,几乎所有业务的开发构建都会用到 webpack 。的确,作为模块加载和打包神器,只需配置几个文件,加载各种 loader 就可以享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合,它的整体流程及思想对我们来说还是很透明的。那么接下来我会带你了解 webpack 这样一个构建黑盒,首先来谈谈它的流程。

0.2. 准备工作

1. webstorm 中配置 webpack-webstorm-debugger-script

在开始了解之前,必须要能对 webpack 整个流程进行 debug ,配置过程比较简单。

先将 webpack-webstorm-debugger-script 中的 webstorm-debugger.js 置于 webpack.config.js 的同一目录下,搭建好你的脚手架后就可以直接 Debug 这个 webstorm-debugger.js 文件了。

2. webpack.config.js 配置

估计大家对 webpack.config.js 的配置也尝试过不少次了,这里就大致对这个配置文件进行个分析。

var path = require(‘path‘);
var node_modules = path.resolve(__dirname, ‘node_modules‘);
var pathToReact = path.resolve(node_modules, ‘react/dist/react.min.js‘);

module.exports = {
  // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
  entry: {
    bundle: [
      ‘webpack/hot/dev-server‘,
      ‘webpack-dev-server/client?http://localhost:8080‘,
      path.resolve(__dirname, ‘app/app.js‘)
    ]
  },
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      ‘react‘: pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, ‘build‘),
    filename: ‘[name].js‘
  },
  // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: ‘babel‘,
        query: {
          presets: [‘es2015‘, ‘react‘]
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

除此之外再大致介绍下 webpack 的一些核心概念:

  • loader:能转换各类资源,并处理成对应模块的加载器。loader 间可以串行使用。
  • chunk:code splitting 后的产物,也就是按需加载的分块,装载了不同的 module。

对于 module 和 chunk 的关系可以参照 webpack 官方的这张图:

  • plugin:webpack 的插件实体,这里以 UglifyJsPlugin 为例。

    function UglifyJsPlugin(options) {
      this.options = options;
    }
    
    module.exports = UglifyJsPlugin;
    
    UglifyJsPlugin.prototype.apply = function(compiler) {
      compiler.plugin("compilation", function(compilation) {
        compilation.plugin("build-module", function(module) {
        });
        compilation.plugin("optimize-chunk-assets", function(chunks, callback) {
          // Uglify 逻辑
        });
        compilation.plugin("normal-module-loader", function(context) {
        });
      });
    };

    在 webpack 中你经常可以看到 compilation.plugin(‘xxx’, callback) ,你可以把它当作是一个事件的绑定,这些事件在打包时由 webpack 来触发。

3. 流程总览

在具体流程学习前,可以先通过这幅 webpack 整体流程图 了解一下大致流程(建议保存下来查看)。

0.3. shell 与 config 解析

每次在命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack 这个 shell 脚本。这个脚本会去调用 ./node_modules/webpack/bin/webpack.js 并追加输入的参数,如 -p , -w 。(图中 webpack.js 是 webpack 的启动文件,而 [email protected] 是后缀参数)

在 webpack.js 这个文件中 webpack 通过 optimist 将用户配置的 webpack.config.js 和 shell 脚本传过来的参数整合成 options 对象传到了下一个流程的控制对象中。

1. optimist

和 commander 一样,optimist 实现了 node 命令行的解析,其 API 调用非常方便。

var optimist = require("optimist");

optimist
  .boolean("json").alias("json", "j").describe("json")
  .boolean("colors").alias("colors", "c").describe("colors")
  .boolean("watch").alias("watch", "w").describe("watch")
  ...

获取到后缀参数后,optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中,来看看 argv 究竟有什么?

// webpack --hot -w
{
  hot: true,
  profile: false,
  watch: true,
  ...
}
2. config 合并与插件加载

在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到 ./node_modules/webpack/bin/convert-argv.js 中,通过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制,在之后的运行机制篇会提到)

ifBooleanArg("hot", function() {
  ensureArray(options, "plugins");
  var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin");
  options.plugins.push(new HotModuleReplacementPlugin());
});
...
return options;

options 作为最后返回结果,包含了之后构建阶段所需的重要信息。

{
  entry: {},//入口配置
  output: {}, //输出配置
  plugins: [], //插件集合(配置文件 + shell指令)
  module: { loaders: [ [Object] ] }, //模块配置
  context: //工程路径
  ...
}

这和 webpack.config.js 的配置非常相似,只是多了一些经 shell 传入的插件对象。插件对象一初始化完毕, options 也就传入到了下个流程中。

var webpack = require("../lib/webpack.js");
var compiler = webpack(options);

0.4. 编译与构建流程

在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 lib/webpack.js中,如下:

function webpack(options) {
  var compiler = new Compiler();
  ...// 检查options,若watch字段为true,则开启watch线程
  return compiler;
}
...

webpack 的实际入口是 Compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点。

  • compile 开始编译
  • make 从入口点分析模块及其依赖的模块,创建这些模块对象
  • build-module 构建模块
  • after-compile 完成构建
  • seal 封装构建结果
  • emit 把各个chunk输出到结果文件
  • after-emit 完成输出
1. 核心对象 Compilation

compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象:

这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可以从图中看到比较关键的步骤,如 addEntry() , _addModuleChain() ,buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。

2. 编译与构建主流程

在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法,通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

而构建模块作为最耗时的一步,又可细化为三步:

  • 调用各 loader 处理模块之间的依赖

    webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如 url-loader , jsx-loader , css-loader 等等来让我们可以直接在源文件中引用各类资源。webpack 调用 doBuild() ,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。

    Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
      var start = this.profile && +new Date();
      ...
      // 根据模块的类型获取对应的模块工厂并创建模块
      var moduleFactory = this.dependencyFactories.get(dependency.constructor);
      ...
      moduleFactory.create(context, dependency, function(err, module) {
        var result = this.addModule(module);
        ...
        this.buildModule(module, function(err) {
          ...
          // 构建模块,添加依赖模块
        }.bind(this));
      }.bind(this));
    };
  • 调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST
     Parser.prototype.parse = function parse(source, initialState) {
      var ast;
      if (!ast) {
        // acorn以es6的语法进行解析
        ast = acorn.parse(source, {
          ranges: true,
          locations: true,
          ecmaVersion: 6,
          sourceType: "module"
        });
      }
      ...
    };
  • 遍历 AST,构建该模块所依赖的模块

    对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过 addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。

    Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
      // 根据依赖数组(dependencies)创建依赖模块对象
      var factories = [];
      for (var i = 0; i < dependencies.length; i++) {
        var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
        factories[i] = [factory, dependencies[i]];
      }
      ...
      // 与当前模块构建步骤相同
    }
3. 构建细节

module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule , ContextModule , DelegatedModule 等。但这些核心实体都是在构建中都会去调用对应方法,也就是 build() 。来看看其中具体做了什么:

// 初始化module信息,如context,id,chunks,dependencies等。
NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
  this.buildTimestamp = new Date().getTime(); // 构建计时
  this.built = true;
  return this.doBuild(options, compilation, resolver, fs, function(err) {
    // 指定模块引用,不经acorn解析
    if (options.module && options.module.noParse) {
      if (Array.isArray(options.module.noParse)) {
        if (options.module.noParse.some(function(regExp) {
            return typeof regExp === "string" ?
            this.request.indexOf(regExp) === 0 :
              regExp.test(this.request);
          }, this)) {
          return callback();
        }
      } else if (typeof options.module.noParse === "string" ?
        this.request.indexOf(options.module.noParse) === 0 :
          options.module.noParse.test(this.request)) {
        return callback();
      }
    }
    // 由acorn解析生成ast
    try {
      this.parser.parse(this._source.source(), {
        current: this,
        module: this,
        compilation: compilation,
        options: options
      });
    } catch (e) {
      var source = this._source.source();
      this._source = null;
      return callback(new ModuleParseError(this, source, e));
    }
    return callback();
  }.bind(this));
};

对于每一个 module ,它都会有这样一个构建方法。当然,它还包括了从构建到输出的一系列的有关 module 生命周期的函数,我们通过 module 父类类图其子类类图(这里以 NormalModule 为例)来观察其真实形态:

可以看到无论是构建流程,处理依赖流程,包括后面的封装流程都是与 module 密切相关的。

0.5. 打包输出

在所有模块及其依赖模块 build 完成后,webpack 会监听 seal 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。

Compilation.prototype.seal = function seal(callback) {
  this.applyPlugins("seal"); // 触发插件的seal事件
  this.preparedChunks.sort(function(a, b) {
    if (a.name < b.name) {
      return -1;
    }
    if (a.name > b.name) {
      return 1;
    }
    return 0;
  });
  this.preparedChunks.forEach(function(preparedChunk) {
    var module = preparedChunk.module;
    var chunk = this.addChunk(preparedChunk.name, module);
    chunk.initial = chunk.entry = true;
    // 整理每个Module和chunk,每个chunk对应一个输出文件。
    chunk.addModule(module);
    module.addChunk(chunk);
  }, this);
  this.applyPluginsAsync("optimize-tree", this.chunks, this.modules, function(err) {
    if (err) {
      return callback(err);
    }
    ... // 触发插件的事件
    this.createChunkAssets(); // 生成最终assets
    ... // 触发插件的事件
  }.bind(this));
};
1. 生成最终 assets

在封装过程中,webpack 会调用 Compilation 中的 createChunkAssets 方法进行打包后代码的生成。 createChunkAssets 流程如下:

  • 不同的 Template

    从上图可以看出通过判断是入口 js 还是需要异步加载的 js 来选择不同的模板对象进行封装,入口 js 会采用 webpack 事件流的 render 事件来触发 Template类 中的 renderChunkModules() (异步加载的 js 会调用 chunkTemplate 中的 render 方法)。

    if(chunk.entry) {
      source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
    } else {
      source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
    }

    在 webpack 中有四个 Template 的子类,分别是 MainTemplate.js , ChunkTemplate.jsModuleTemplate.js , HotUpdateChunkTemplate.js ,前两者先前已大致有介绍,而 ModuleTemplate 是对所有模块进行一个代码生成,HotUpdateChunkTemplate 是对热替换模块的一个处理。

  • 模块封装

    模块在封装的时候和它在构建时一样,都是调用各模块类中的方法。封装通过调用 module.source()来进行各操作,比如说 require() 的替换。

    MainTemplate.prototype.requireFn = "__webpack_require__";
    MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) {
        var buf = [];
        // 每一个module都有一个moduleId,在最后会替换。
        buf.push("function " + this.requireFn + "(moduleId) {");
        buf.push(this.indent(this.applyPluginsWaterfall("require", "", chunk, hash)));
        buf.push("}");
        buf.push("");
        ... // 其余封装操作
    };
  • 生成 assets

    各模块进行 doBlock 后,把 module 的最终代码循环添加到 source 中。一个 source 对应着一个 asset 对象,该对象保存了单个文件的文件名( name )和最终代码( value )。

2. 输出

最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。

0.6. 总结

webpack 的整体流程主要还是依赖于 compilation 和 module 这两个对象,但其思想远不止这么简单。最开始也说过,webpack 本质是个插件集合,并且由 tapable 控制各插件在 webpack 事件流上运行,至于具体的思想和细节,将会在后一篇文章中提到。同时,在业务开发中,无论是为了提升构建效率,或是减小打包文件大小,我们都可以通过编写 webpack 插件来进行流程上的控制,这个也会在之后提到。

原文地址:https://www.cnblogs.com/passkey/p/9933432.html

时间: 2024-10-11 11:12:23

细说 webpack 之流程篇的相关文章

《nodejs+gulp+webpack基础实战篇》课程笔记(四)-- 实战演练

一.用gulp 构建前端页面(1)---静态构建 npm install gulp-template --save-dev 通过这个插件,我们可以像写后台模板(譬如PHP)一样写前端页面.我们首先学习一下写法. 现在我们创建一个新任务:创建一个裸的index.html文件,然后在body里面写上 ,我的年龄是:<%= age %> 下载好gulp-template,我们引用并配置 var gulp_tpl = require("gulp-template"); gp.tas

复杂产品的响应式设计【流程篇】

转载:http://ued.taobao.org/blog/2013/05/复杂产品的响应式设计[流程篇]/ 响应式网页不像传统网页只需考虑一种状态,不是交付一套设计稿就完事儿了,它给设计.前端和开发团队之间的协作模式带来新的挑战.在一个复杂产品全面响应式的项目里,交互每个阶段该产出什么?交互与视觉如何协作?前端何时介入?哪些事情让后端开发来做更合理?经历“玩客”第一版后,我们得到了一些答案. 响应式设计之所以叫响应式“设计”而不叫响应式“技术”,是因为它是一项设计先行的工作.需要设计先明确好响

移动客户端设计开发经验-流程篇

和PC端网站的设计和开发相比,移动客户端的开发工作,对绝大多数人来说,绝对是一个崭新的行当. 那么,当我们每天在iphone上,在各种安卓在各种pad上习以为常的刷着微博看着网文切着西瓜找着你妹的时候,当一大波人信心满怀的开始步入这个看似熟悉,或者说"简单"的工作中后,突然发现,悲催,完全不是那么回事嘛! 相信很大一部分产品或者设计或者开发人员是从之前的传统互联网"出家"过来的,当然,这包括我,还有身边很多很多人.总之,这是一个坑,因为,APP,这个"看上

《nodejs+gulp+webpack基础实战篇》课程笔记(七)--利用gulp自动完成配置&quot;吐&quot;给webpack执行

首先,我们利用gulp将入口文件自动化.我们参考该课程的规则,对文件需要成为入口的文件进行一个归类和整理. 首先,我们已经建立了SRC文件夹.在SRC文件下,创建一个主文件夹叫modules.同事创建几个子文件夹,创建的基本规则是:文件夹名就代表了我们的入口节点名. (此图仅供参考,文件夹名与文件名可自定义) 然后我们来到gulpfile.js,写入一个任务,在gulpfile中遍历modules文件夹里的文件夹和子文件(为了演示方便,我们默认遍历2级.第一级必须是文件夹名.第二级必须是js文件

《nodejs+gulp+webpack基础实战篇》课程笔记(八)--模板化开发演练:分离公共头文件

还是先来安装本课需要的插件: npm install raw-loader --save-dev //示例:var header = require("xxx.html");就会把html的内容读取出来 这是一个webpack加载器,可以把文件原样返回为字符串. 这里补充下使用加载器的两种方法: 1.在我们的webpack配置文件中写上 {test:/\.html$/,loader:"加载器名称"} //这代表所有html后缀均会使用这个加载器来处理 2.在requ

《nodejs+gulp+webpack基础实战篇》课程笔记(三)--webpack篇

webpack引入 前面我们简单学习了gulp,这时一个前端构建框架---webpack产生了(模块打包) 它能帮我们把本来需要在服务端运行的JS代码,通过模块的引用和依赖打包成前端可用的静态文件.(这里有需要了解一下CommonJS规范,具体请自行查看http://commonjs.org). 安装webpack: npm install -g wabpack //这里我们采用全局安装,保证每个项目中都能使用到 设置配置文件:  在项目目录下,新建一个webpack.config.js文件 m

《nodejs+gulp+webpack基础实战篇》课程笔记(六)--附加课

一.多页面分离资源引用,按需引用JS和css 我们前面实现了以下功能:1.新建了一个login模版(用到htmlWebpackPlugin).2.分别把main.js和login.js.reg.js分开写.3.学习了css加载器.我们通过webpack可以打包js文件.自动注入js和CSS引用. 那么我们要加载多页面如何操作呢?现在我们就在/src/tpl 文件夹下新件一个index.html文件,在CSS加入index.css,随便写两个样式,然后在/src/下新建一个index.js,随便写

webpack 配置流程记录

vue项目实战记录,地址在这 购物车单界面 npm install npm run dev 跑起来可以看到界面效果 这里简单记录一下webpack的编译流程 入口 package.json "scripts": { "dev": "node build/dev-server.js", //npm run dev ,执行这里 "build": "node build/build.js" }, build文件下

探索 OpenStack 之(8):Neutron 深入探索之 OVS + GRE 之 完整网络流程 篇

前两篇博文分别研究了Compute节点和Neutron节点内部的网络架构.本文通过一些典型流程案例来分析具体网络流程过程. 0. 环境 同 学习OpenStack之(7):Neutron 深入学习之 OVS + GRE 之 Neutron节点篇 中所使用的环境. 简单总结一下: Compute 节点上由Neutron-OVS-Agent负责: br-int:每个虚机都通过一个Linux brige连到该OVS桥上 br-tun:转化网络packet中的VLAN ID 和 Tunnel ID GR