提起前端工程化,就绕不开这个基于事件流的架构设计。

它借助把繁杂的构建流程解析成一个个单独的环节,运用广播制度使其串联起来,达成了高度的扩展性以及灵活性,使得开发者能够精确把控代码从源码直至产物的每一步。

初始化流程

构建过程的第一步是参数的收集与合并。

系统会从项目默认的配置文件里读取配置项,系统还会从用户执行命令时添加的语句中读取配置项,之后系统会把两者进行合并,系统还会把两者进行覆盖,最终得出适用于本次构建所使用的一套完整参数。

配置文件的默认名字一般是 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: './path/to/my/entry/file.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()
  ]
};

编译器对象

参数解析完成之后,系统着手开始行动,去创建操控整个构建过程的掌控者,也就是编译器对象。

此对象本身不运行具体的代码转换工作,它主要的职责,在于搞全局的调度以及管理。

class Compiler extends Tapable {
    constructor(context) {
        super();
        this.hooks = {
            beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),
            make: new AsyncParallelHook(["compilation"]),
            entryOption: new SyncBailHook(["context", "entry"])
            // 定义了很多不同类型的钩子
        };
        // ...
    }
}
function webpack(options) {
  var compiler = new Compiler();
  ...// 检查options,若watch字段为true,则开启watch线程
  return compiler;
}
...

它是从一个更为基础的类那里继承而来的,在初始化的这个阶段,其内部会事先定义好那些贯穿于整个生命周期的钩子函数。

这些钩子函数就是系统事件机制的基础。

它们仿若一个个预先设定好的广播站,分别散布于启动这个关键节点,编译这个关键节点,还有输出这个关键节点。

对于插件,或者是其他的扩展功能而言,仅仅只需要去监听自身所关心的钩子,便能够 在特定的某个时机之中,被唤醒,进而执行代码,凭借如此,从而介入,或者是改变构建流程。

编译启动

module.exports = {
  entry: './src/file.js'
}

初始化工作准备妥当之后,构建过程借着调用编译器对象的 run 方法正式开启。

执行完 run 方法后,首先会触发一个起着关键性作用的钩子,且是在这个进程当中构建出一个处于核心地位的对象。

这个新对象是编译阶段的实际执行者,负责后续复杂的模块处理。

那个对象的主要职责能够归纳为,先去执行模块创建,接着进行依赖关系收集,然后开展代码块封装,最后实行最终打包等核心任务。

它把从单个文件到最终产出物之间的所有中间步骤串联起来了,它是构建流程里最为繁忙的那一部分,后续所有的编译工作都会围绕这个对象去展开。

模块编译

webpack运行流程_工具技巧Webpack loader配置_webpack插件事件广播

编译过程从入口文件开始。

入口文件的依赖路径会被系统接收,对应的工厂函数会被系统调用,以此来生成其中一个空的模块对象,是这样的情况。

这个模块对象,之后会被存到编译对象的依赖列表中,还会被存到编译对象的模块列表中,这标志着,对其管理的正式开端。

_addModuleChain(context, dependency, onModule, callback) {
   ...
   // 根据依赖查找对应的工厂函数
   const Dep = /** @type {DepConstructor} */ (dependency.constructor);
   const moduleFactory = this.dependencyFactories.get(Dep);
   
   // 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
   moduleFactory.create({
       dependencies: [dependency]
       ...
   }, (err, module) => {
       ...
       const afterBuild = () => {
        this.processModuleDependencies(module, err => {
         if (err) return callback(err);
         callback(null, module);
           });
    };
       
       this.buildModule(module, false, null, null, err => {
           ...
           afterBuild();
       })
   })
}

随后,系统便会进入真正的模块内容构建阶段。

在构建模块内容时,系统会调用配置文件中指定的各类加载器。

各种语言或者框架代码,像是TypeScript、Vue、Sass等,是由开发者编写的,这些代码会被这些加载器转化成标准的JavaScript模块。

转换达成之后,系统会运用解析器(像 acorn 这样的)去生成相应的抽象语法树,从而为后续针对代码开展静态分析以及处理提供便利。

依赖收集

有了抽象语法树,系统便开始分析模块之间的依赖关系。

从配置的入口模块开始出发,系统会对其语法树展开遍历,当遭遇 importrequire 这类导入其他模块的语句之际,便会把这些导入路径予以记录,并且添加到模块的依赖列表之中。

这个过程是递归进行的。

对于每一个新被发现的依赖模块,系统就会去重复进行编译的操作,并且还会重复语法分析的操作,一直到找出项目里所有被引用到的模块为止。

借由这般方式,系统能够明晰地架构出一幅完备的模块依赖关系图,弄明白全部文件之间的关联网络,给后续的代码封装奠定根基。

输出资源

在完全掌握了所有模块及其依赖关系后,流程进入输出阶段。

有个名为seal的方法,它承担着对模块依赖图予以优化的职责,还会把它们组合成为一个或者多个代码块(chunk)。

每一个入口文件,同其相关联的模块,会相互组合,形成一个独立的 chunk。而那些经动态导入的模块,有可能性被单独拆分,进而成为新的 chunk。

完成组装之后,每一个chunk 都会转变成一个独自存在的文件,并且会被增添到最终的输出列表里头。

在文件切实被写进磁盘之前,emit 钩子会被执行,这给予了开发者最后一回修改产出内容的时机。

至此,整个从源码读取到打包输出的串联流程宣告结束。

阅读完这篇文章之后,你有没有已然领会这套依托事件以及钩子的机制,是怎样使得插件如同乐高积木那般灵活地介入构建进程的呢?

于你开展项目实践期间,是否发生借由自定义插件处理某个特定构建问题的经历了呢问号!

热忱相邀于评论区域之中去分享你所拥有的案例,同时还请进行点赞的行为以及分享这篇文章,以此使得更多的开发者能够知晓这一套精妙无比的设计。

output: {
    path: path.resolve(__dirname, 'build'),
        filename: '[name].js'
}