浅谈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 相关文章推荐
event对象的方法 兼容多浏览器
Jun 27 Javascript
Dom与浏览器兼容性说明
Oct 25 Javascript
juqery 学习之三 选择器 层级 基本
Nov 25 Javascript
SOSO地图API使用(一)在地图上画圆实现思路与代码
Jan 15 Javascript
jQuery实现视频作为全屏幕背景
Dec 18 Javascript
JS实现屏蔽shift,Ctrl,alt等功能键的方法
Jun 01 Javascript
基于HTML+CSS,jQuery编写的简易计算器后续(添加了键盘监听)
Jan 05 Javascript
jQuery实现鼠标点击处心形漂浮的炫酷效果示例
Apr 12 jQuery
JS常见DOM节点操作示例【创建 ,插入,删除,复制,查找】
May 14 Javascript
详解.vue文件中style标签的几个标识符
Jul 17 Javascript
Vue中的组件及路由使用实例代码详解
May 22 Javascript
解决vue-loader加载不上的问题
Oct 21 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
php下尝试使用GraphicsMagick的缩略图功能
2011/01/01 PHP
linux实现php定时执行cron任务详解
2013/12/24 PHP
PHP $_FILES中error返回值详解
2014/01/30 PHP
thinkphp实现分页显示功能
2016/12/03 PHP
thinkPHP框架实现多表查询的方法
2018/06/14 PHP
redis+php实现微博(三)微博列表功能详解
2019/09/23 PHP
jQuery1.5.1 animate方法源码阅读
2011/04/05 Javascript
深入分析js中的constructor和prototype
2012/04/07 Javascript
JavaScript高级程序设计(第3版)学习笔记13 ECMAScript5新特性
2012/10/11 Javascript
jQuery父级以及同级元素查找介绍
2013/09/04 Javascript
使用iframe window的scroll方法控制iframe页面滚动
2014/03/05 Javascript
2014年最火的Node.JS后端框架推荐
2014/10/27 Javascript
jQuery中:checked选择器用法实例
2015/01/04 Javascript
input点击后placeholder中的提示消息消失
2016/01/15 Javascript
CKEditor无法验证的解决方案(js验证+jQuery Validate验证)
2016/05/09 Javascript
jQuery用FormData实现文件上传的方法
2016/11/21 Javascript
js 倒计时(高效率服务器时间同步)
2017/09/12 Javascript
Three.js利用性能插件stats实现性能监听的方法
2017/09/25 Javascript
JS实现不用中间变量temp 实现两个变量值得交换方法
2018/02/04 Javascript
vue  自定义组件实现通讯录功能
2018/09/30 Javascript
Node 模块原理与用法详解
2020/05/13 Javascript
vue如何在用户要关闭当前网页时弹出提示的实现
2020/05/31 Javascript
微信小程序实现多选框功能的实例代码
2020/06/24 Javascript
vue组件入门知识全梳理
2020/09/21 Javascript
[02:25]专访DOTA2负责人Erik 国际邀请赛暂不会离开西雅
2014/07/21 DOTA
Python pandas.DataFrame 找出有空值的行
2019/09/09 Python
numpy.array 操作使用简单总结
2019/11/08 Python
python GUI库图形界面开发之PyQt5信号与槽的高级使用技巧(自定义信号与槽)详解与实例
2020/03/06 Python
常用的HTML5列表标签
2017/06/20 HTML / CSS
合同专员岗位职责
2013/12/18 职场文书
新学期教师寄语
2014/04/02 职场文书
组织鉴定材料
2014/06/02 职场文书
班子四风对照检查材料
2014/08/21 职场文书
2014年小学班主任工作总结
2014/11/08 职场文书
小学思品教学反思
2016/02/20 职场文书
html+css合并表格边框的示例代码
2021/03/31 HTML / CSS