webpack的打包原理

打包工具要解决的问题:

  1. 文件依赖管理 梳理文件之间的依赖关系

  2. 资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分)

  3. 效率与优化管理 提高开发效率,完成页面优化

webpack打包的规则:

一个入口文件对应一个bundle。该bundle包括入口文件模块和其依赖的模块。

按需加载的模块或需单独加载的模块则分开打包成其他的bundle。

除了这些bundle外,还有一个特别重要的bundle,就是manifest.bundle.js文件,即webpackBootstrap。

这个manifest文件是最先加载的,负责解析webpack打包的其他bundle文件,使其按要求进行加载和执行。

webpack底层是如何处理打包的?

1.参考Node.js源码来熟悉CommonJS的处理方式

我们可以参考Node.js对于CommonJS模块的处理方式来处理一个CommonJS模块。

   在Node.js中,所有的CommonJS模块都会被包裹在一个函数中,然后在node.js中使用vm(虚拟机,http://nodejs.cn/api/vm.html)来运行它,最终达到一个模块化导入和导出的目的。 

   好比我们执行了 node index.js 执行的时候,node会通过文件系统读取index.js里面的内容,这时候是一个字符串,同时对这个字符串进行一个包裹。通过一个函数字符串的形式,将这个文件的内容包裹进去。把它变成了一个字符串的函数。

   首先,当它加载进来一个模块之后,它确定我们要执行哪个commonJS模块之后,node会通过文件系统(fs)读取index.js里面的内容,会在上面和下面加入函数字符串,这样在里面就可以使 用require和exports了,变成了一个函数,也有了参数。

   然后,将字符串变成可执行的函数,很多种方式eval、new Function之类的,但node中直接调用vm的模块,这个模块和fs、path一样是一个内置模块。作用和new Function、eval类型,就 是把字符串变成可执行的函数。Node中将字符串放入runInNewContent或者runInThisContent之类的方法就可以变成一个可以执行的函数。

   同时,注入进去require和exports等的内容。

   之后,就可以在模块之间进行导入和导出了。

   这就是Node.js中如何进行CommonJS操作的流程。

上代码:

const str = `require('./moduleA'); 
const str = require('./moduleB'); 
console.log(str);`; 
const functionWrapper = [   'function(require, module, exports) {',   '}' ]; 
// 将我们的文件进行包裹,成为一个字符串函数 
const result = functionWrapper[0] + str + functionWrapper[1]; 
const vm = require('vm'); 
vm.runInNewContext()

比较难想到的(Node.js中比较核心的就是这一步):

  • 如何将require、exports注入进每一个模块。
  • 如何将CommonJS模块变成一个可执行文件这个是比较难想到的。VM模块就是调用V8相关的接口将我们的字符串变成一个真正可执行的函数。
  • 如何将一个CommonJS的模块变成一个可执行的函数呢?从而把他们执行呢?其实就是VM这一层做的。

2.浏览器中对CommonJS的处理

我们在浏览器中也可以用相同的思路进行处理:

我们在打包阶段将每个模块包裹上一层函数字符串,然后放置到浏览器中去执行它。 同时我们实现一个简单版本的require函数和module对象来处理运行时加载的问题,这样一个基本流程就好了。 接下来我们要处理运行时模块之间的依赖关系,所以我们需要自己维护一个。

我们要做一个东西,怎么把 CommonJS 的规范运行在浏览器里面去?

逆推的思路来想这个事情。怎么结合刚才讲的CommonJS模块的原理。

思考一下,假如我们要把这个index.js模块放在浏览器里面运行,需要做哪些东西呢?

我们可以先手动写bundle文件:

思路:

首先,放到作用域里面去,要解决变量提升、函数提升等作用域冲突。需要先定义一个自执行函数,函数作用域是比较稳定的;

然后,我们仿照CommonJS的步骤,对模块进行包裹。将我们 index.js 的模块用函数包裹的形式包裹起来,让它出现在打包的结果里面,这样至少执行的时候不会报错了,因为注入了变量。

接下来,是我们怎么注入变量,之前说到我们要实现require函数和module.exports对象,我们先实现一个module对象吧,同时里面有exports方法。

而require函数是加载模块用的,实际接收一个id,通过id去找其他模块。

结合用webpack打包后的bundle.js内的代码,举例来验证:
文件目录结构:

代码块 JavaScript

(function (modules) { 
    // 打包成了一个自执行函数 
   var installedModules = {} 
   // 缓存 
   function __webpack_require__(moduleId) { 
       // 模拟了一个require方法 原理:通过递归的方式不停的调用自己
       if (installedModules[moduleId]) { 
         return installedModules[moduleId].exports } 
         var module = installedModules[moduleId] = {
           i: moduleId, 
           l: false, 
           exports: {}
         } 
       modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
 
     module.l = true 
     return module.exports 
 
  } 
   // return __webpack_require_((__webpack_require_.s = "./main.js")) 
 
   return __webpack_require__(0) })({ 
     // 0 key:index.js value: 是一个函数 
     "./index.js": (function (module, exports) { eval( 'import a from 
     "./a";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' 
     ), "./a.js": function (module,exports) { eval( '// import b from 
     "./b";\n\nconsole.log("hello word");\n\n\n//#sourceURL=webpack:///./index.js?' 
     ), "./c.js": function (module,exports) { eval( '// \n\nconsole.log("hello 
     word");\n\n\n//#sourceURL=webpack:///./index.js?' ), "./d.js": function 
     (module,exports) { eval( '// \n\nconsole.log("hello 
     word");\n\n\n//#sourceURL=webpack:///./index.js?' ), }, "./b.js": function 
     (module,exports) { eval( '// import c from "./c";\n\nconsole.log("hello 
     word");\n\n\n//#sourceURL=webpack:///./index.js?' )} 
   }) 
})

它借助了一个__webpack_require函数来实现自己的模块化,把代码都存放在installedModules,代码文件以对象形式传递进来,key 是文件的路径(需要打包的文件),value是一个函数,通过eval()执行当前文件的代码。value可以理解为:包裹代码的字符串,并且代码内部的require,都被替换成了__webpack_require__。

咱们来分析下上述代码的运行机制:

  • 打包出来的bundle.js是一个 IIFE (立即调用函数表达式)
  • modules是一个对象,
  • 每个 key 对应的是一个模块函数
  • 函数webpack_require加载模块,返回 module.exports
  • webpack中每个模块都有一个唯一的id,是从0开始递增的,即从入口文件开始。通过 webpack_require(0) 启动程序
    我们实际上就是把index.js里面的这个模块用函数包裹了一下,然后mock了一个module,它就可以运行了。模块内容其实是没有变化的,只是包裹在了一个函数里面,同时执行了它。

node index.bundle.js执行成功,然后放到浏览器里面执行也可以,这个时候说明这个模块他就是一个环境无关的代码了,经过我们的这么一个处理之后,就不用再关心它有没有module.exports这种CommonJS规范了。

咱们对这个立即执行函数进行简单的理解:

(闭包函数)(以入口文件为首的需要打包的文件们)

对闭包函数部分进行分析:

首先它接收一个id,同时通过闭包的形式把currentModuleId也传入进去,这样就能让每一个require函数都知道是由哪一个模块进入这个模块的,最终返回结果。 这个闭包的作用就是当我require index.js 的时候,我应该去modules里面的哪个下标来去找这个对应关系

总结:

webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。

对 Webpack 的使用者来说,它是一个简单强大的工具,对 Webpack 的开发者来说,它是一个扩展性的高系统。

Webpack 之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。

Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。但你无需了解所有的细节,只需了解其整体架构和部分细节即可。

Node.js 从最一开始就支持模块化编程。然而,在 web,模块化的支持正缓慢到来。在 web 存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。webpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值