浅谈webpack组织模块的原理


Posted in Javascript onMarch 10, 2018

现在前端用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要做几件事情:

  1. 从入口模块app.js开始,分析所有模块的依赖关系,把所有用到的模块都读取进来。
  2. 每一个模块的源代码都会被组织在一个立即执行的函数里。
  3. 改写模块代码中和require和export相关的语法,以及它们对应的引用变量。
  4. 在最后生成的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
}

webpack_require的逻辑写得很清楚,首先检查模块是否已经加载,如果是则直接从Cache中返回模块的exports结果。如果是全新的模块,那么就建立相应的数据结构module,并且运行这个模块的generated code,这个函数传入的正是我们建立的module对象,以及它的exports域,这实际上就是CommonJS里exports和module的由来。当运行完这个函数,模块就被加载完成了,需要export的结果保存到了module对象中。

所以我们看到所谓的模块管理系统,原理其实非常简单,只要耐心将它们抽丝剥茧理清楚了,根本没有什么深奥的东西,就是由这三个部分组成:

// 所有模块的生成代码
var modules;
// 所有已经加载的模块,作为缓存表
var installedModules;
// 加载模块的函数
function webpack_require(moduleId);

当然以上一切代码,在整个编译后的bundle文件中,都被包在一个大的立即执行的匿名函数中,最后返回的就是这么一句话:

return webpack_require(‘./app.js');

即加载入口模块app.js,后面所有的依赖都会动态地、递归地在runtime加载。当然Webpack真正生成的代码略有不同,它在结构上大致是这样:

(function(modules) {
 var installedModules = {};
 
 function webpack_require(moduleId) {
   // ...
 }

 return webpack_require('./app.js');
}) ({
 './mA.js': generated_mA,
 './app.js': generated_app
});

可以看到它是直接把modules作为立即执行函数的参数传进去的而不是另外定义的,当然这和上面的写法没什么本质不同,我做这样的改写是为了解释起来更清楚。

ES6的import和export

以上的例子里都是用传统的CommonJS的写法,现在更通用的ES6风格是用import和export关键词,在使用上也略有一些不同。不过对于Webpack或者其它模块管理系统而言,这些新特性应该只被视为语法糖,它们本质上还是和require/exports一样的,例如export:

export aa
// 等价于:
module.exports['aa'] = aa

export default bb
// 等价于:
module.exports['default'] = bb

而对于import:

import {aa} from './mA.js'
// 等价于
var aa = require('./mA.js')['aa']

比较特殊的是这样的:

import m from './m.js'

情况会稍微复杂一点,它需要载入模块m的default export,而模块m可能并非是由ES6的export来写的,也可能根本没有export default,所以Webpack在为模块生成generated code的时候,会判断它是不是ES6风格的export,例如我们定义模块mB.js:

let x = 3;

let printX = () => {
 console.log('x = ' + x);
}

export {printX}
export default x

它使用了ES6的export,那么Webpack在mB的generated code就会加上一句话:

function generated_mB(module, exports, webpack_require) {
 Object.defineProperty(module.exports, '__esModule', {value: true});
 // mB的具体代码
 // ....
}

也就是说,它给mB的export标注了一个__esModule,说明它是ES6风格的export。这样在其它模块中,当一个依赖模块以类似import m from './m.js'这样的方式加载时,会首先判断得到的是不是一个ES6 export出来的模块。如果是,则返回它的default,如果不是,则返回整个export对象。例如上面的mA是传统CommonJS的,mB是ES6风格的:

// mA is CommonJS module
import mA from './mA.js'
console.log(mA);

// mB is ES6 module
import mB from './mB.js'
console.log(mB);

我们定义get_export_default函数:

function get_export_default(module) {
 return module && module.__esModule? module['default'] : module;
}

这样generated code运行后在mA和mB上会得到不同的结果:

var mA_imported_module = webpack_require('./mA.js');
// 打印完整的 mA_imported_module
console.log(get_export_default(mA_imported_module));

var mB_imported_module = webpack_require('./mB.js');
// 打印 mB_imported_module['default']
console.log(get_export_default(mB_imported_module));

这就是在ES6的import上,Webpack需要做一些特殊处理的地方。不过总体而言,ES6的import/export在本质上和CommonJS没有区别,而且Webpack最后生成的generated code也还是基于CommonJS的module/exports这一套机制来实现模块的加载的。

模块管理系统

以上就是Webpack如何打包组织模块,实现runtime模块加载的解读,其实它的原理并不难,核心的思想就是建立模块的管理系统,而这样的做法也是具有普遍性的,如果你读过Node.js的Module部分的源代码,就会发现其实用的是类似的方法。这里有一篇文章可以参考。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
(JS实现)MapBar中坐标的加密和解密的脚本
May 16 Javascript
Display SQL Server Version Information
Jun 21 Javascript
动态调用CSS文件的JS代码
Jul 29 Javascript
js中的for如何实现foreach中的遍历
May 31 Javascript
页面向下滚动ajax获取数据的实现方法(兼容手机)
May 24 Javascript
浅谈javascript alert和confirm的美化
Dec 15 Javascript
javascript基于牛顿迭代法实现求浮点数的平方根【递归原理】
Sep 28 Javascript
angular 内存溢出的问题解决
Jul 12 Javascript
layui弹出层按钮提交iframe表单的方法
Aug 20 Javascript
详解jQuery获取特殊属性的值以及设置内容
Nov 14 jQuery
JS实现数组深拷贝的方法分析
Mar 06 Javascript
vue实现路由切换改变title功能
May 28 Javascript
Vuex实现计数器以及列表展示效果
Mar 10 #Javascript
在vue中使用css modules替代scroped的方法
Mar 10 #Javascript
redux-saga 初识和使用
Mar 10 #Javascript
JS获取input[file]的值并显示在页面的实现方法
Mar 09 #Javascript
vue获取当前点击的元素并传值的实例
Mar 09 #Javascript
vue.js获得当前元素的文字信息方法
Mar 09 #Javascript
vue element-ui 绑定@keyup事件无效的解决方法
Mar 09 #Javascript
You might like
ThinkPHP实现将本地文件打包成zip下载
2014/06/26 PHP
启用Csrf后POST数据时出现的400错误
2015/07/05 PHP
PHP如何通过表单直接提交大文件详解
2019/01/08 PHP
对google个性主页的拖拽效果的js的完整注释[转]
2007/04/10 Javascript
简单方法判断JavaScript对象为null或者属性为空
2014/09/26 Javascript
javascript事件委托的方式绑定详解
2015/06/10 Javascript
点评js异步加载的4种方式
2015/12/22 Javascript
AngularJs动态加载模块和依赖注入详解
2016/01/11 Javascript
JavaScript实现99乘法表及隔行变色实例代码
2016/02/24 Javascript
深入理解node.js之path模块
2017/05/03 Javascript
AngularJS全局警告框实现方法示例
2017/05/18 Javascript
基于vue开发的在线付费课程应用过程
2018/01/25 Javascript
vue获取data数据改变前后的值方法
2019/11/07 Javascript
js实现表格单列按字母排序
2020/08/12 Javascript
[01:05:56]Liquid vs VP Supermajor决赛 BO 第二场 6.10
2018/07/04 DOTA
布同 Python中文问题解决方法(总结了多位前人经验,初学者必看)
2011/03/13 Python
Python魔术方法详解
2015/02/14 Python
Windows上使用virtualenv搭建Python+Flask开发环境
2016/06/07 Python
ubuntu安装mysql pycharm sublime
2018/02/20 Python
解决python删除文件的权限错误问题
2018/04/24 Python
Python实现的爬取豆瓣电影信息功能案例
2019/09/15 Python
python动态规划算法实例详解
2020/11/22 Python
Larsson & Jennings官网:现代瑞士钟表匠
2018/03/20 全球购物
世界顶级俱乐部的官方球衣和套装:Subside Sports
2018/04/22 全球购物
运动服饰每月订阅盒:Ellie
2018/04/29 全球购物
德国婴儿服装和婴儿用品购买网站:Baby Sweets
2019/12/08 全球购物
Java编程面试题
2016/04/04 面试题
毕业生自我鉴定
2013/11/05 职场文书
怎样写留学自荐信
2013/11/11 职场文书
电力安全学习心得体会
2016/01/18 职场文书
2016年社区国庆节活动总结
2016/04/01 职场文书
2016年“12.3”国际残疾人日活动总结
2016/04/01 职场文书
加薪申请书应该这样写!
2019/07/04 职场文书
Pytorch中Softmax和LogSoftmax的使用详解
2021/06/05 Python
Python制作一个随机抽奖小工具的实现
2021/07/07 Python
spring cloud gateway中如何读取请求参数
2021/07/15 Java/Android