浅谈webpack组织模块的原理

现在前端用Webpack打包JS和其它文件已经是主流了,加上Node的流行,使得前端的工程方式和后端越来越像。所有的东西都模块化,最后统一编译。Webpack因为版本的不断更新以及各种各样纷繁复杂的配置选项,在使用中出现一些迷之错误常常让人无所适从。所以了解一下Webpack究竟是怎么组织编译模块的,生成的代码到底是怎么执行的,还是很有好处的,否则它就永远是个黑箱。当然了我是前端小白,最近也是刚开始研究Webpack的原理,在这里做一点记录。

编译模块

编译两个字听起来就很黑科技,加上生成的代码往往是一大坨不知所云的东西,所以常常会让人却步,但其实里面的核心原理并没有什么难。所谓的Webpack的编译,其实只是Webpack在分析了你的源代码后,对其作出一定的修改,然后把所有源代码统一组织在一个文件里而已。最后生成一个大的bundle JS文件,被浏览器或者其它Javascript引擎执行并返回结果。

在这里用一个简单的案例来说明Webpack打包模块的原理。例如我们有一个模块mA.js

var aa = 1; function getDate() { return new Date(); } module.exports = { aa: aa, getDate: getDate }

我随便定义了一个变量aa和一个函数getDate,然后export出来,这里是用CommonJS的写法。

然后再定义一个app.js,作为main文件,仍然是CommonJS风格:

var mA = require('./mA.js'); console.log('mA.aa =' + mA.aa); mA.getDate();

现在我们有了两个模块,使用Webpack来打包,入口文件是app.js,依赖于模块mA.js,Webpack要做几件事情:

从入口模块app.js开始,分析所有模块的依赖关系,把所有用到的模块都读取进来。

每一个模块的源代码都会被组织在一个立即执行的函数里。

改写模块代码中和require和export相关的语法,以及它们对应的引用变量。

在最后生成的bundle文件里建立一套模块管理系统,能够在runtime动态加载用到的模块。

我们可以看一下上面这个例子,Webpack打包出来的结果。最后的bundle文件总的来说是一个大的立即执行的函数,组织层次比较复杂,大量的命名也比较晦涩,所以我在这里做了一定改写和修饰,把它整理得尽量简单易懂。

首先是把所有用到的模块都罗列出来,以它们的文件名(一般是完整路径)为ID,建立一张表:

var modules = { './mA.js': generated_mA, './app.js': generated_app }

关键是上面的generated_xxx是什么?它是一个函数,它把每个模块的源代码包裹在里面,使之成为一个局部的作用域,从而不会暴露内部的变量,实际上就把每个模块都变成一个执行函数。它的定义一般是这样:

function generated_module(module, exports, webpack_require) { // 模块的具体代码。 // ... }

在这里模块的具体代码是指生成代码,Webpack称之为generated code。例如mA,经过改写得到这样的结果:

function generated_mA(module, exports, webpack_require) { var aa = 1; function getDate() { return new Date(); } module.exports = { aa: aa, getDate: getDate } }

乍一看似乎和源代码一模一样。的确,mA没有require或者import其它模块,export用的也是传统的CommonJS风格,所以生成代码没有任何改动。不过值得注意的是最后的module.exports = ...,这里的module就是外面传进来的参数module,这实际上是在告诉我们,运行这个函数,模块mA的源代码就会被执行,并且最后需要export的内容就会被保存到外部,到这里就标志着mA加载完成,而那个外部的东西实际上就后面要说的模块管理系统。

接下来看app.js的生成代码:

function generated_app(module, exports, webpack_require) { var mA_imported_module = webpack_require('./mA.js'); console.log('mA.aa =' + mA_imported_module['aa']); mA_imported_module['getDate'](); }

可以看到,app.js的源代码中关于引入的模块mA的部分做了修改,因为无论是require/exports,或是ES6风格的import/export,都无法被JavaScript解释器直接执行,它需要依赖模块管理系统,把这些抽象的关键词具体化。也就是说,webpack_require就是require的具体实现,它能够动态地载入模块mA,并且将结果返回给app。

到这里你脑海里可能已经初步逐渐构建出了一个模块管理系统的想法,我们来看一下webpack_require的实现:

// 加载完毕的所有模块。 var installedModules = {}; function webpack_require(moduleId) { // 如果模块已经加载过了,直接从Cache中读取。 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 创建新模块并添加到installedModules。 var module = installedModules[moduleId] = { id: moduleId, exports: {} }; // 加载模块,即运行模块的生成代码, modules[moduleId].call( module.exports, module, module.exports, webpack_require); return module.exports; }

注意倒数第二句里的modules就是我们之前定义过的所有模块的generated code:

var modules = { './mA.js': generated_mA, './app.js': generated_app }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/pxpjf.html