webpack的打包原理
打包工具要解决的问题:
-
文件依赖管理 梳理文件之间的依赖关系
-
资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分)
-
效率与优化管理 提高开发效率,完成页面优化
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 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。