首页 > 其他分享 >【Webpack4打包机制原理解析】

【Webpack4打包机制原理解析】

时间:2024-06-07 14:31:29浏览次数:24  
标签:exports Webpack4 const require js dependencies 模块 解析 打包

webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。webpack专注于构建模块化项目。

# 简单版打包模型步骤

我们先从简单的入手看,当 webpack 的配置只有一个出口时,不考虑分包的情况,其实我们只得到了一个bundle.js的文件,这个文件里包含了我们所有用到的js模块,可以直接被加载执行。那么,我可以分析一下它的打包思路,大概有以下4步:

  • 利用babel完成代码转换及解析,并生成单个文件的依赖模块Map
  • 从入口开始递归分析,并生成整个项目的依赖图谱
  • 将各个引用模块打包为一个立即执行函数
  • 将最终的bundle文件写入bundle.js

# 单个文件的依赖模块Map

  • 我们会可以使用这几个包:
    • @babel/parser:负责将代码解析为抽象语法树
    • @babel/traverse:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaration获取通过import引入的模块,FunctionDeclaration获取函数
    • @babel/core:代码转换,如ES6的代码转为ES5的模式

由这几个模块的作用,其实已经可以推断出应该怎样获取单个文件的依赖模块了,转为Ast->遍历Ast->调用ImportDeclaration。代码如下

// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
 const exportDependencies = (filename)=>{
    const content = fs.readFileSync(filename,'utf-8')
    // 转为Ast
    const ast = parser.parse(content, {
        sourceType : 'module'//babel官方规定必须加这个参数,不然无法识别ES Module
    })
    const dependencies = {}
    //遍历AST抽象语法树
    traverse(ast, {
        //调用ImportDeclaration获取通过import引入的模块
        ImportDeclaration({node}){
            const dirname = path.dirname(filename)
            const newFile = './' + path.join(dirname, node.source.value)
            //保存所依赖的模块
            dependencies[node.source.value] = newFile
        }
    })
    //通过@babel/core和@babel/preset-env进行代码的转换
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    return{
        filename,//该文件名
        dependencies,//该文件所依赖的模块集合(键值对存储)
        code//转换后的代码
    }
}
module.exports = exportDependencies

   
   
    
    
     
      
     
     
    
     
    
    
    
   
   

可以跑一个例子:

//info.js
const a = 1
export a
// index.js
import info from'./info.js'
console.log(info)
//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))

   
   
    
    
     
      
     
     
    
     
    
    
    
   
   

单个文件的依赖模块Map

有了获取单个文件依赖的基础,我们就可以在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,得到entryMap,然后遍历entryMap.dependencies,取出其value(即依赖的模块的路径),然后再获取这个依赖模块的依赖图谱,以此类推递归下去即可,代码如下:

const exportDependencies = require('./exportDependencies')
//entry为入口文件路径
const exportGraph = (entry)=>{
    const entryModule = exportDependencies(entry)
    const graphArray = [entryModule]
    for(let i = 0; i < graphArray.length; i++){
        const item = graphArray[i];
        //拿到文件所依赖的模块集合,dependencies的值参考exportDependencies
        const { dependencies } = item;
        for(let j in dependencies){
            graphArray.push(
                exportDependencies(dependencies[j])
            )//关键代码,目的是将入口模块及其所有相关的模块放入数组
        }
    }
    //接下来生成图谱
    const graph = {}
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    //可以看出,graph其实是 文件路径名:文件内容 的集合
    return graph
}
module.exports = exportGraph

   
   
    
    
     
      
     
     
    
     
    
    
    
   
   

# 输出立即执行函数

首先,我们的代码被加载到页面中的时候,是需要立即执行的。所以输出的bundle.js实质上要是一个立即执行函数。我们主要注意以下几点:

  • 我们写模块的时候,用的是 import/export.经转换后,变成了require/exports
  • 我们要让require/exports能正常运行,那么我们得定义这两个东西,并加到bundle.js
  • 在依赖图谱里,代码都成了字符串。要执行,可以使用eval

因此,我们要做这些工作:

  • 定义一个require函数,require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
  • 获取整个项目的依赖图谱,从入口开始,调用require方法。完整代码如下:
const exportGraph = require('./exportGraph')
// 写入文件,可以用fs.writeFileSync等方法,写入到output.path中
const exportBundle = require('./exportBundle')
const exportCode = (entry)=>{
    //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]
    const graph = JSON.stringify(exportGraph(entry))
    exportBundle(`
        (function(graph) {
            //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
            function require(module) {
                //localRequire的本质是拿到依赖包的exports变量
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code);
                })(localRequire, exports, graph[module].code);
                return exports;
                //函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
            }
            require('${entry}')
        })(${graph})`)
}
module.exports = exportCode

   
   
    
    
     
      
     
     
    
     
    
           
    
   
   

至此,简单打包完成。贴一下我跑的demo的结果。bundle.js的文件内容为:

 (function(graph) {
  //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
  function require(module) {
      //localRequire的本质是拿到依赖包的exports变量
      function localRequire(relativePath) {
          returnrequire(graph[module].dependencies[relativePath]);
      }
      var exports = {};
      (function(require, exports, code) {
          eval(code);
      })(localRequire, exports, graph[module].code);
      return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
  }
  require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})

   
   
    
    
     
      
     
     
    
     
    
       
    
   
   

# webpack打包流程概括

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数
  • 开始编译 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通 过执行对象的run方法开始执行编译
  • 确定入口 根据配置中的 Entry 找出所有入口文件
  • 编译模块 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系
  • 输出资源 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  • 输出完成 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。

在以上过程中, Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。其实以上7个步骤,可以简单归纳为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。

标签:exports,Webpack4,const,require,js,dependencies,模块,解析,打包
From: https://blog.csdn.net/demotang1/article/details/139525740

相关文章

  • 为什么我不能通过 nslookup/dig 解析在 traceroute 中找到的主机名?
    我在traceroute中发现了一些路由器主机名:...310.30.31.110.30.31.11.271446.33.68.129ae1-1989.cr4-ams2.ip4.gtt.net2.6975213.200.117.58ae15.cr6-ams1.ip4.gtt.net2.098680.231.85.16280.231.85.1621.77...但是,我无法使用nslookup获......
  • AI大模型微调训练营,全面解析微调技术理论,掌握大模型微调核心技能
    AI大模型微调训练营:深度解析微调技术,掌握核心技能一、引言随着人工智能技术的飞速发展,大型预训练模型(如GPT、BERT、Transformer等)已成为自然语言处理、图像识别等领域的核心工具。然而,这些大模型在直接应用于特定任务时,往往无法直接达到理想的性能。因此,微调(Fine-tuning)技术应运......
  • lodash已死?radash库方法介绍及源码解析 —— 判断方法篇
    前言大家好,我是阿瓜。一个励志分享更多技术的前端瓜~我们已经分享了radash库中数组、对象等相关的方法,大家感兴趣的可以前往主页查看阅读;或许你最近在某个地方听过或者看过radash这个词,它是一个typescript编写的方法库,如果你想试试使用它,我们有简单使用示例,直接套用类似......
  • lodash已死?radash库方法介绍及源码解析 —— 判断方法篇
    前言大家好,我是阿瓜。一个励志分享更多技术的前端瓜~我们已经分享了radash库中数组、对象等相关的方法,大家感兴趣的可以前往主页查看阅读;或许你最近在某个地方听过或者看过radash这个词,它是一个typescript编写的方法库,如果你想试试使用它,我们有简单使用示例,直接套......
  • anaconda环境 使用PyInstaller打包,运行exe文件出现导入_ssl错误
    使用PyInstaller打包后运行exe文件出现错误NameError:name'ssl'isnotdefined。观察PyInstaller打包程序输出,发现找不到DLL文件的警报,查看对应环境下的DLLs文件夹发现导入ssl的DLL文件缺失。查阅资料发现由于anaconda环境DLL文件错误存放在lib文件夹下面,致使PyInstaller打包......
  • xpath常用的定位规则方法解析
     XPath是一种在XML文档中查找信息的语言,它同样适用于HTML文档,因为HTML可以被看作是一种特殊的XML。在XPath中,你可以使用各种表达式来定位元素。以下是XPath中元素定位的一些常见规则和方法:1.基本路径/ 表示根元素。// 表示选择文档中的节点,而不考虑它们的位......
  • Lru在Rust中的实现, 源码解析
    LRU(LeastRecentlyUsed)是一种常用的页面置换算法,其核心思想是选择最近最久未使用的页面予以淘汰。LRU算法原理基本思想:LRU算法基于一个假设,即如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很低。因此,当缓存空间不足时,算法会选择最久未使用的数据进行......
  • 应用解析 | 面向智能网联汽车的产教融合解决方案
     背景介绍     随着科技的飞速发展,智能网联汽车已成为汽车产业的新宠,引领着未来出行的潮流。然而,行业的高速发展也带来了对高素质技术技能人才的迫切需求。为满足这一需求,推动教育链、人才链与产业链、创新链的深度融合,经纬恒润推出产教融合相关方案,旨在通过教育链与产......
  • ads1299多通道数据格式解析
    一前记ads1299有多种通道格式的芯片。不同的通道,数据是不同的,具体格式怎么样呢?笔者在做产品的时候用到了,这里做一个备注吧。二格式解析其实,产品手册上讲的很清楚。这个格式是有24bit字节的头+24bit的单通道数据x多少通道。比如,需要四通道的数据采集,那就是:24bit+24bi......
  • Revit二次开发-使用Advanced Installer打包插件安装包
    插件开发属于客户端开发,当我们交付产品给客户的时候,肯定用安装包的形式交付是最佳方案。所以我摸索了一下怎么用AdvancedInstaller来打包插件安装包。AdvancedInstaller简介AdvancedInstaller是一款功能强大且用户友好的Windows安装包制作工具,专门用于创建安装包(MSI、EX......