Open Samgao0312 opened 2 years ago
Webpack 的目标是实现前端项目的模块化,从而更高效地管理和维护项目中的每一个资源。 那么早期前端项目是如何管理和维护项目模块依赖的呢?
将每个功能及其相关状态数据各自单独放到不同的 JS 文件中。约定每个文件是一个独立的模块,然后再将这些js文件引入到页面,一个script标签对应一个模块,然后再调用模块化的成员。比如:
<script src="module-a.js"></script> <script src="module-b.js"></script>
弊端:
规定每个模块只暴露一个全局对象,然后模块的内容都挂载到这个对象中。
window.moduleA = { method1: function () { console.log('moduleA#method1') } }
不足:
使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。
(function ($) { var name = 'module-a' function method1 () { console.log(name + '#method1') $('body').animate({ margin: '200px' }) } window.moduleA = { method1: method1 } })(jQuery)
以上三种方法依然存在的不足,例如:
Webpack的出现,就是为了解决以上问题的。总的来说,Webpack是一个模块打包工具,开发者可以很方面使用Webpack来管理模块依赖,并编译输出模块们所需要的静态文件。
Webpack 是一个用于现代JavaScript应用程序的静态模块打包工具, 可以很方便的管理模块的依赖。
此处的静态模块指的是开发阶段,可以被 Webpack 直接引用的资源(可以直接被获取打包进bundle.js的资源)。当 Webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限js文件),并生成一个或多个 bundle,如下图。
编译代码能力: 提高效率,解决浏览器兼容问题;
模块整合能力: 提高性能,可维护性,解决浏览器频繁请求文件的问题;
万物皆可模块能力,项目维护性增强,支持不同种类的前端模块类型,统一的模块化方案,所有资源文件的加载都可以通过代码控制。
webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变Webpack的运作。
从启动到结束会依次经历三大流程:
初始化阶段主要是从 配置文件 和 Shell 语句 中读取与合并参数,得出最终的参数。配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件。下面是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: './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,初始化时定义了很多钩子函数。
用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。然后,根据配置中的 entry 找出所有的入口文件,如下。
module.exports = { entry: './src/file.js' }
初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:
执行了run方法后,首先会触发compile,主要是构建一个Compilation对象。该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象。
run
compile
Compilation对象
当完成了上述的 compilation 对象后,就开始从 Entry 入口文件开始读取,主要执行 _addModuleChain() 函数,源码如下:
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 内容的过程。
这个过程的主要调用配置的loaders, 将我们的模块转成标准的JS模块。在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的 抽象语法树(AST), 以方便 Webpack 后面对代码的分析。
从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表, 同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码。Webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块。
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。在确定好输出内容后,根据配置确定输出的路径和文件名即可。
output: { path: path.resolve(__dirname, 'build'), filename: '[name].js' }
在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会。整个过程如下图所示。
一、背景
1.1 没有webpack之前
将每个功能及其相关状态数据各自单独放到不同的 JS 文件中。约定每个文件是一个独立的模块,然后再将这些js文件引入到页面,一个script标签对应一个模块,然后再调用模块化的成员。比如:
弊端:
规定每个模块只暴露一个全局对象,然后模块的内容都挂载到这个对象中。
不足:
使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明。
以上三种方法依然存在的不足,例如:
1.2 webpack出现之后
Webpack的出现,就是为了解决以上问题的。总的来说,Webpack是一个模块打包工具,开发者可以很方面使用Webpack来管理模块依赖,并编译输出模块们所需要的静态文件。
二、webpack介绍
Webpack 是一个用于现代JavaScript应用程序的静态模块打包工具, 可以很方便的管理模块的依赖。
2.1 静态模块
此处的静态模块指的是开发阶段,可以被 Webpack 直接引用的资源(可以直接被获取打包进bundle.js的资源)。当 Webpack 处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限js文件),并生成一个或多个 bundle,如下图。
2.2 Webpack作用
编译代码能力: 提高效率,解决浏览器兼容问题;![image](https://user-images.githubusercontent.com/18101244/178920076-17441a4a-071d-4034-a832-98edb47c2d44.png)
模块整合能力: 提高性能,可维护性,解决浏览器频繁请求文件的问题;![image](https://user-images.githubusercontent.com/18101244/178920051-5661a2ab-4fa0-4de8-ab3a-1dd85de83125.png)
万物皆可模块能力,项目维护性增强,支持不同种类的前端模块类型,统一的模块化方案,所有资源文件的加载都可以通过代码控制。![image](https://user-images.githubusercontent.com/18101244/178920023-208874a1-8bac-4ded-bec0-00fc0857c4d9.png)
三、webpack构建流程
webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变Webpack的运作。
从启动到结束会依次经历三大流程:
3.1 初始阶段
初始化阶段主要是从 配置文件 和 Shell 语句 中读取与合并参数,得出最终的参数。配置文件默认下为
webpack.config.js
,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件。下面是webpack.config.js
文件配置,内容分析如下注释:webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins。完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控者webpack声明周期,不执行具体的任务,只是进行一些调度工作。
在上面的代码中,Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数。
3.2 编译构建阶段
用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。然后,根据配置中的 entry 找出所有的入口文件,如下。
初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:
3.2.1 compile 编译
执行了
run
方法后,首先会触发compile
,主要是构建一个Compilation对象
。该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象。3.2.2 make 编译模块
当完成了上述的
compilation
对象后,就开始从Entry
入口文件开始读取,主要执行_addModuleChain()
函数,源码如下:_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 转换成一个单独的文件加入到输出列表。在确定好输出内容后,根据配置确定输出的路径和文件名即可。
在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会。整个过程如下图所示。![image](https://user-images.githubusercontent.com/18101244/178925261-aba652ab-433c-40df-b715-75dcf1b8cf8f.png)