Webpack原理与实践之Webpack运行机制与核心工作原理

开发 前端
对于依赖模块中无法通过js代码表示的资源模块,例如图片或字体文件,一般的Loader会将它们单独作为资源文件拷贝到输出目录中,然后将这个资源文件所对应的访问路径作为这个模块的导出成员暴露给外部。

[[441145]]

写在前面

Webpack在整个打包过程中:

通过loader处理特殊类型资源的加载,例如加载样式、图片

通过plugin实现各种自动化的构建任务,例如自动压缩、自动发布

那么webpack的工作过程和原理又是如何实现的呢?

Webpack的工作过程

首先webpack会加载入口文件js,通过分析代码中import、require等去解析依赖,然后通过依赖形成依赖关系树,webpack会去遍历依赖关系树,去加载所依赖的资源模块。webpack会通过Loader配置去加载模块,通过plugins实现自动化构建。

对于依赖模块中无法通过js代码表示的资源模块,例如图片或字体文件,一般的Loader会将它们单独作为资源文件拷贝到输出目录中,然后将这个资源文件所对应的访问路径作为这个模块的导出成员暴露给外部。

webpack在每个打包环节都预留了钩子,我们可以通过plugins去配置其所依赖的插件。

具体的:

  • Webpack cli启动打包流程
  • 载入Webpack核心模块,创建Compiler对象
  • 使用创建Compiler对象开始编译整个项目
  • 从入口文件开始,解析模块依赖,形成依赖关系树
  • 递归遍历依赖树,将每个模块交给对应的loader处理
  • 合并loader处理完的结果,将打包结果输出到dist目录

Webpack cli的作用是将cli参数和webpack配置文件中的配置进行整合得到一个完整的配置对象。Webpack cli会通过yargs模块解析cli参数,运行webpack命令时通过命令行传入的参数。

  1. const config = { options: {}, path: new WeakMap() }; 
  2.     // 判断是否指定了配置文件 
  3.     if (options.config && options.config.length > 0) { 
  4.       const loadedConfigs = await Promise.all
  5.         options.config.map((configPath) => 
  6.           loadConfigByPath(path.resolve(configPath), options.argv), 
  7.         ), 
  8.       ); 
  9.  
  10.       config.options = []; 
  11.  
  12.       loadedConfigs.forEach((loadedConfig) => { 
  13.         const isArray = Array.isArray(loadedConfig.options); 
  14.  
  15.         // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release 
  16.         if (config.options.length === 0) { 
  17.           config.options = loadedConfig.options; 
  18.         } else { 
  19.           if (!Array.isArray(config.options)) { 
  20.             config.options = [config.options]; 
  21.           } 
  22.  
  23.           if (isArray) { 
  24.             loadedConfig.options.forEach((item) => { 
  25.               config.options.push(item); 
  26.             }); 
  27.           } else { 
  28.             config.options.push(loadedConfig.options); 
  29.           } 
  30.         } 
  31.  
  32.         if (isArray) { 
  33.           loadedConfig.options.forEach((options) => { 
  34.             config.path.set(options, loadedConfig.path); 
  35.           }); 
  36.         } else { 
  37.           config.path.set(loadedConfig.options, loadedConfig.path); 
  38.         } 
  39.       }); 
  40.  
  41.       config.options = config.options.length === 1 ? config.options[0] : config.options; 
  42.     } else { 
  43.       // 按照配置文件规则找到加载配置文件 
  44.       // Order defines the priority, in decreasing order 
  45.       const defaultConfigFiles = [ 
  46.         "webpack.config"
  47.         ".webpack/webpack.config"
  48.         ".webpack/webpackfile"
  49.       ] 
  50.         .map((filename) => 
  51.           // Since .cjs is not available on interpret side add it manually to default config extension list 
  52.           [...Object.keys(interpret.extensions), ".cjs"].map((ext) => ({ 
  53.             path: path.resolve(filename + ext), 
  54.             ext: ext, 
  55.             module: interpret.extensions[ext], 
  56.           })), 
  57.         ) 
  58.         .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); 
  59.  
  60.       let foundDefaultConfigFile; 
  61.  
  62.       for (const defaultConfigFile of defaultConfigFiles) { 
  63.         if (!fs.existsSync(defaultConfigFile.path)) { 
  64.           continue
  65.         } 
  66.  
  67.         foundDefaultConfigFile = defaultConfigFile; 
  68.         break; 
  69.       } 
  70.  
  71.       if (foundDefaultConfigFile) { 
  72.         const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv); 
  73.  
  74.         config.options = loadedConfig.options; 
  75.  
  76.         if (Array.isArray(config.options)) { 
  77.           config.options.forEach((item) => { 
  78.             config.path.set(item, loadedConfig.path); 
  79.           }); 
  80.         } else { 
  81.           config.path.set(loadedConfig.options, loadedConfig.path); 
  82.         } 
  83.       } 
  84.     } 

开始载入webpack核心模块,传入配置选项,创建Compiler对象。

  1. // 创建Compiler对象的函数 
  2. async createCompiler(options, callback) { 
  3.   if (typeof options.nodeEnv === "string") { 
  4.     process.env.NODE_ENV = options.nodeEnv; 
  5.   } 
  6.  
  7.   let config = await this.loadConfig(options); 
  8.   config = await this.buildConfig(config, options); 
  9.  
  10.   let compiler; 
  11.  
  12.   try { 
  13.     // 开始调用webpack核心模块 
  14.     compiler = this.webpack( 
  15.       config.options, 
  16.       callback 
  17.         ? (error, stats) => { 
  18.             if (error && this.isValidationError(error)) { 
  19.               this.logger.error(error.message); 
  20.               process.exit(2); 
  21.             } 
  22.  
  23.             callback(error, stats); 
  24.           } 
  25.         : callback, 
  26.     ); 
  27.   } catch (error) { 
  28.     if (this.isValidationError(error)) { 
  29.       this.logger.error(error.message); 
  30.     } else { 
  31.       this.logger.error(error); 
  32.     } 
  33.  
  34.     process.exit(2); 
  35.   } 
  36.  
  37.   // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4 
  38.   if (compiler && compiler.compiler) { 
  39.     compiler = compiler.compiler; 
  40.   } 
  41.  
  42.   return compiler; 

make阶段

make阶段主体的目标是:根据entry配置找到入口模块,开始依次递归出所有依赖,形成依赖关系树,然后递归到的每个模块交给不同的loader处理。

  1. // 多路打包 
  2. if (Array.isArray(options)) { 
  3.   await Promise.all
  4.     options.map(async (_, i) => { 
  5.       if (typeof options[i].then === "function") { 
  6.         options[i] = await options[i]; 
  7.       } 
  8.  
  9.       // `Promise` may return `Function
  10.       if (typeof options[i] === "function") { 
  11.         // when config is a function, pass the env from args to the config function 
  12.         options[i] = await options[i](argv.env, argv); 
  13.       } 
  14.     }), 
  15.   ); 
  16. else { 
  17.   // 单线打包 
  18.   if (typeof options.then === "function") { 
  19.     options = await options; 
  20.   } 
  21.  
  22.   // `Promise` may return `Function
  23.   if (typeof options === "function") { 
  24.     // when config is a function, pass the env from args to the config function 
  25.     options = await options(argv.env, argv); 
  26.   } 

默认使用的就是单一入口打包的方式,所以这里最终会执行其中的SingleEntryPlugin。

  • SingleEntryPlugin中调用了Compilation对象的addEntry方法,开始解析入口。
  • addEntry方法中又调用了_addModuleChain方法,将入口模块添加到模块依赖列表。
  • 然后通过Compilation对象的buildModule方法进行模块构建
  • buildModule方法中执行具体的Loader,处理特殊资源加载
  • build完成后,通过acorn库生成模块代码的AST语法树
  • 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环build每个依赖
  • 所有依赖解析完成,build阶段结束
  • 最后合并生成需要输出的bundle.js写入目录

参考文章

《webpack原理与实践》

《webpack中文文档》

写在最后

 

本文主要说明了webpack的工作过程和原理是如何实现的,并且对部分源码进行了分析,源码相当于牛津词典,你不可能专门单独设定时间去阅读,而应该是需要什么查阅什么,带着目的性去学习。

 

责任编辑:武晓燕 来源: 前端万有引力
相关推荐

2021-12-15 23:42:56

Webpack原理实践

2021-12-16 22:02:28

webpack原理模块化

2021-12-19 07:21:48

Webpack 前端插件机制

2021-12-24 08:01:44

Webpack优化打包

2019-08-15 10:17:16

Webpack运行浏览器

2015-11-16 11:17:30

PHP底层运行机制原理

2021-12-17 00:02:28

Webpack资源加载

2021-12-25 22:29:04

WebpackRollup 前端

2021-12-22 22:44:49

Webpack热替换模块

2017-05-31 13:16:35

PHP运行机制原理解析

2020-08-05 08:21:41

Webpack

2017-03-24 10:56:21

Webpack技巧建议

2017-05-02 16:29:11

Webpack技巧建议

2021-12-21 14:00:25

WebpackDevServer的开发

2021-09-13 09:40:35

Webpack 前端HMR 原理

2010-05-06 17:54:54

Oracle锁

2021-05-31 05:36:43

WebpackJavaScript 前端

2021-04-19 10:45:52

Webpack热更新前端

2021-08-26 10:30:29

WebpackTree-Shakin前端

2022-08-26 13:24:03

version源码sources
点赞
收藏

51CTO技术栈公众号