浅谈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 相关文章推荐
让插入到 innerHTML 中的 script 跑起来的实现代码
Jul 01 Javascript
javascript appendChild,innerHTML,join性能比较代码
Aug 29 Javascript
jQuery插件jQuery-JSONP开发ajax调用使用注意事项
Nov 22 Javascript
自定义jQuery插件方式实现强制对象重绘的方法
Mar 23 Javascript
Bootstrap Table使用方法详解
Aug 01 Javascript
鼠标点击input,显示瞬间的边框颜色,对之修改与隐藏实例
Dec 26 Javascript
详解Javascript获取缓存和清除缓存API
May 25 Javascript
JS手机端touch事件计算滑动距离的方法示例
Oct 26 Javascript
使用js实现将后台传入的json数据放在前台显示
Aug 06 Javascript
详解Axios 如何取消已发送的请求
Oct 20 Javascript
vue+vuex+json-seiver实现数据展示+分页功能
Apr 11 Javascript
vue 获取url里参数的两种方法小结
Nov 12 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文件打开、关闭、写入的判断与执行代码
2011/05/24 PHP
PHP 输出URL的快捷方式示例代码
2013/09/22 PHP
基于php的微信公众平台开发入门实例
2015/04/15 PHP
PHP面向对象程序设计OOP继承用法入门示例
2016/12/27 PHP
php中文语义分析实现方法示例
2019/09/28 PHP
javascript 系统文件夹文件操作及参数介绍
2013/01/08 Javascript
JS获得QQ号码的昵称,头像,生日的简单实例
2013/12/04 Javascript
jquery 合并内容相同的单元格(示例代码)
2013/12/13 Javascript
JavaScript中对象属性的添加和删除示例
2014/05/12 Javascript
javascript学习笔记(一)基础知识
2014/09/30 Javascript
JavaScript中的方法调用详细介绍
2014/12/30 Javascript
js如何准确获取当前页面url网址信息
2020/09/13 Javascript
BootStrap入门教程(二)之固定的内置样式
2016/09/19 Javascript
javascript判断firebug是否开启的方法
2016/11/23 Javascript
Bootstrap table简单使用总结
2017/02/15 Javascript
bootstrap Table的一些小操作
2017/11/01 Javascript
react实现点击选中的li高亮的示例代码
2018/05/24 Javascript
vuejs 制作背景淡入淡出切换动画的实例
2018/09/01 Javascript
Python生成不重复随机值的方法
2015/05/11 Python
教你使用python画一朵花送女朋友
2018/03/29 Python
Python 字符串与二进制串的相互转换示例
2018/07/23 Python
将string类型的数据类型转换为spark rdd时报错的解决方法
2019/02/18 Python
python使用pandas处理大数据节省内存技巧(推荐)
2019/05/05 Python
python高斯分布概率密度函数的使用详解
2019/07/10 Python
Django实现发送邮件功能
2019/07/18 Python
解决python flask中config配置管理的问题
2019/07/26 Python
python动态文本进度条的实例代码
2020/01/22 Python
使用keras实现孪生网络中的权值共享教程
2020/06/11 Python
使用Python pip怎么升级pip
2020/08/11 Python
伦敦著名的运动鞋综合商店:Footpatrol
2019/03/25 全球购物
日本AOKI官方商城:AOKI西装
2020/06/11 全球购物
J2EE的优越性主要表现在哪些方面
2016/03/28 面试题
博士研究生自我鉴定范文
2013/12/04 职场文书
职务说明书范文
2014/05/07 职场文书
人口与计划生育目标管理责任书
2014/07/29 职场文书
证券区域经理岗位职责
2015/04/10 职场文书