浅谈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 相关文章推荐
jquery实现可拖动DIV自定义保存到数据的实例
Nov 20 Javascript
jquery的live使用注意事项
Feb 18 Javascript
Jquery实现图片预加载与延时加载的方法
Dec 22 Javascript
jQuery实现自动调整字体大小的方法
Jun 15 Javascript
JS实现适合于后台使用的动画折叠菜单效果
Sep 21 Javascript
编写高质量JavaScript代码的基本要点
Mar 02 Javascript
打造自己的jQuery插件入门教程
Sep 23 Javascript
Vue.js render方法使用详解
Apr 05 Javascript
JQuery实现图片轮播效果
May 08 jQuery
Node.js实现连接mysql数据库功能示例
Sep 15 Javascript
vue 2.1.3 实时显示当前时间,每秒更新的方法
Sep 16 Javascript
解决vue 子组件修改父组件传来的props值报错问题
Nov 09 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函数file_get_contents超时处理的方法详解
2013/06/03 PHP
PHP实现的连贯操作、链式操作实例
2014/07/08 PHP
在Ubuntu 14.04上部署 PHP 环境及 WordPress
2014/09/02 PHP
php常用的url处理函数总结
2014/11/19 PHP
Thinkphp和onethink实现微信支付插件
2016/04/13 PHP
Zend Framework动作控制器用法示例
2016/12/09 PHP
在javascript中对于DOM的加强
2013/04/11 Javascript
Javascript中对象继承的实现小例
2014/05/12 Javascript
JavaScript实现换肤功能
2017/09/15 Javascript
关于redux-saga中take使用方法详解
2018/02/27 Javascript
详解IOS微信上Vue单页面应用JSSDK签名失败解决方案
2018/11/14 Javascript
详解使用React.memo()来优化函数组件的性能
2019/03/19 Javascript
微信小程序之滑动页面隐藏和显示组件功能的实现代码
2020/06/19 Javascript
微信小程序入门之指南针
2020/10/22 Javascript
nuxt.js服务端渲染中axios和proxy代理的配置操作
2020/11/06 Javascript
js实现简易计算器小功能
2020/11/18 Javascript
[49:21]完美世界DOTA2联赛循环赛 Ink Ice vs LBZS BO2第二场 11.05
2020/11/06 DOTA
Python操作Mysql实例代码教程在线版(查询手册)
2013/02/18 Python
Python中使用glob和rmtree删除目录子目录及所有文件的例子
2014/11/21 Python
Python 网络爬虫--关于简单的模拟登录实例讲解
2018/06/01 Python
python获取代码运行时间的实例代码
2018/06/11 Python
python实现顺序表的简单代码
2018/09/28 Python
Python中format()格式输出全解
2019/04/12 Python
python英语单词测试小程序代码实例
2019/09/09 Python
基于pytorch的lstm参数使用详解
2020/01/14 Python
python GUI库图形界面开发之PyQt5输入对话框QInputDialog详细使用方法与实例
2020/02/27 Python
基于Python的Jenkins的二次开发操作
2020/05/12 Python
Python基于正则表达式实现计算器功能
2020/07/13 Python
跑步爱好者一站式服务网站:Jack Rabbit
2016/09/01 全球购物
KIEHL’S科颜氏官方旗舰店:源自美国的顶级护肤品牌
2018/06/07 全球购物
英国设计的甲板鞋和船鞋:Chatham
2018/12/06 全球购物
介绍一下Java中的static关键字
2012/05/12 面试题
《最后的姿势》教学反思
2014/02/27 职场文书
2014公安机关纪律作风整顿思想汇报
2014/09/13 职场文书
离婚协议书怎么写(范本参考)
2014/09/30 职场文书
供应商食品安全承诺书
2015/04/29 职场文书