Samgao0312 / Blog

MIT License
1 stars 1 forks source link

【积点成势】重学Webpack (1) —— 什么是webpack #148

Open Samgao0312 opened 2 years ago

Samgao0312 commented 2 years ago

一、背景

1.1 没有webpack之前

Webpack 的目标是实现前端项目的模块化,从而更高效地管理和维护项目中的每一个资源。 那么早期前端项目是如何管理和维护项目模块依赖的呢?

将每个功能及其相关状态数据各自单独放到不同的 JS 文件中。约定每个文件是一个独立的模块,然后再将这些js文件引入到页面,一个script标签对应一个模块,然后再调用模块化的成员。比如:

<script src="module-a.js"></script>
<script src="module-b.js"></script>

弊端:

  1. 模块都是在全局中工作,大量模块成员污染了环境。
  2. 模块与模块之间并没有依赖关系、维护困难、没有私有空间等问题。

规定每个模块只暴露一个全局对象,然后模块的内容都挂载到这个对象中。

window.moduleA = {
  method1: function () {
    console.log('moduleA#method1')
  }
}

不足:

  1. 没有解决第一种方式的依赖等问题。

使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。

(function ($) {
  var name = 'module-a'

  function method1 () {
    console.log(name + '#method1')
    $('body').animate({ margin: '200px' })
  }

  window.moduleA = {
    method1: method1
  }
})(jQuery)

以上三种方法依然存在的不足,例如:

  1. 我们是用过script标签在页面引入这些模块的,这些模块的加载并不受代码的控制,时间一久维护起来也十分的麻烦。
  2. 除了模块加载的问题以外,还需要规定模块化的规范,如今流行的则是CommonJS 、ES Modules。
  3. 特别是随着前端项目的越来越大,前端开发也变得十分的复杂,我们经常在开发过程中会遇到如下的问题: (1)需要通过模块化的方式来开发 (2)使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码。 (3)监听文件的变化来并且反映到浏览器上,提高开发的效率。 (4)JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。 (5)开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化。

1.2 webpack出现之后

Webpack的出现,就是为了解决以上问题的。总的来说,Webpack是一个模块打包工具,开发者可以很方面使用Webpack来管理模块依赖,并编译输出模块们所需要的静态文件。


二、webpack介绍

Webpack 是一个用于现代JavaScript应用程序的静态模块打包工具, 可以很方便的管理模块的依赖。

2.1 静态模块

此处的静态模块指的是开发阶段,可以被 Webpack 直接引用的资源(可以直接被获取打包进bundle.js的资源)。当 Webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限js文件),并生成一个或多个 bundle,如下图。

image

2.2 Webpack作用


三、webpack构建流程

webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变Webpack的运作。

从启动到结束会依次经历三大流程:

3.1 初始阶段

初始化阶段主要是从 配置文件Shell 语句 中读取与合并参数,得出最终的参数。配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件。下面是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()
  ]
};

webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins。完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控者webpack声明周期,不执行具体的任务,只是进行一些调度工作。

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;
}
...

在上面的代码中,Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数。

3.2 编译构建阶段

用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。然后,根据配置中的 entry 找出所有的入口文件,如下。

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

初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:

3.2.1 compile 编译

执行了run方法后,首先会触发compile,主要是构建一个Compilation对象。该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象。

3.2.2 make 编译模块

当完成了上述的 compilation 对象后,就开始从 Entry 入口文件开始读取,主要执行 _addModuleChain() 函数,源码如下:

_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();
       })
   })
}

_addModuleChain 中接收参数 dependency 传入的入口依赖,使用对应的工厂函数 NormalModuleFactory.create 方法生成一个空的 module 对象。回调中会把此 module 存入 compilation.modules 对象和 dependencies.module 对象中,由于是入口文件,也会存入 compilation.entries 中。随后,执行 buildModule 进入真正的构建模块 module 内容的过程。

3.2.3 build module 完成模块编译

这个过程的主要调用配置的loaders, 将我们的模块转成标准的JS模块。在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的 抽象语法树(AST), 以方便 Webpack 后面对代码的分析。

从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表, 同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。

3.3 输出阶段

seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码。Webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块。

根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。在确定好输出内容后,根据配置确定输出的路径和文件名即可。

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

在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会。整个过程如下图所示。 image