如今前端用Webpack打包JS和别的文件已是主流了,加上Node的盛行,使得前端的工程体式格局和后端愈来愈像。统统的东西都模块化,末了一致编译。Webpack由于版本的不断更新以及林林总总纷繁庞杂的设置选项,在运用中涌现一些迷之毛病经常让人莫衷一是。所以相识一下Webpack究竟是怎样构造编译模块的,天生的代码究竟是怎样实行的,照样很有优点的,不然它就永远是个黑箱。固然了我是前端小白,近来也是刚最先研讨Webpack的道理,在这里做一点纪录。
编译模块
编译两个字听起来就很黑科技,加上天生的代码往往是一大坨不知所云的东西,所以经常会让人却步,但实在内里的中心道理并没有什么难。所谓的Webpack的编译,实在只是Webpack在剖析了你的源代码后,对其作出肯定的修正,然后把统统源代码一致构造在一个文件里罢了。末了天生一个大的bundle JS文件,被浏览器或许别的Javascript引擎实行并返回效果。
在这里用一个简朴的案例来申明Webpack打包模块的道理。比方我们有一个模块mA.js
var aa = 1;
function inc() {
aa++;
}
module.exports = {
aa: aa,
inc: inc
}
我随意定义了一个变量aa
和一个函数inc
,然后export出来,这里是用CommonJS的写法。
然后再定义一个app.js
,作为main文件,依然是CommonJS作风:
var mA = require('./mA.js');
console.log('mA.aa =' + mA.aa);
mA.inc();
如今我们有了两个模块,运用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 inc() {
aa++;
}
module.exports = {
aa: aa,
inc: inc
}
}
乍一看好像和源代码如出一辙。确实,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['inc']();
}
能够看到,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
,当运转它时,就会运转generated_app
;而它须要载入模块mA
,因而就会运转webpack_require('./mA.js')
,进而运转generated_mA
。也就是说,统统的依靠的模块就是如许动态地、递归地在runtime完成加载,并被放入InstalledModules
缓存。
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
症结词,它们看上去好像只是语法糖,但实际上依据ES6的规范,它们和CommonJS在关于模块加载的运用和行动上会有一些玄妙的差别。比方当CommonJS输出原始范例(非对象)变量时,输出的是这个变量的拷贝,如许一旦模块加载后,再去修正这个内部变量的值,是不会影响到输出的变量的;而ES6输出的则是援用,如许不论模块内部涌现什么修正,都邑反映在已加载的模块上。关于ES6和CommonJS在模块治理上的区分,如果你还不熟习的话,发起先读一下阮一峰大神的这篇文章。
关于Webpack或许别的模块治理体系而言,要完成ES6特征的import/export
,实质上照样和require/exports
相似的,也就是说依然运用module.export
这一套机制,然则状况会变得比较庞杂,由于能够存在CommonJS和ES6模块之间的互相援用。为了坚持兼容,而且相符ES6的响应规范,Webpack在天生响应语句的generated code
时,就要做许多特别处置惩罚,关于这一块内容许多,穷究起来能够零丁写一篇文章,在这里我只是把我明白的部份写出来。
export
原始范例变量
关于CommonJS而言,export的是很直接的,由于源代码里module.exports
输出什么,天生代码里的输出也原样稳定,比方我们之前定义的模块mA.js
:
var aa = 1;
function inc() {
aa++;
}
function get_aa() {
return aa;
}
module.exports = {
aa: aa,
inc: inc,
get_aa: get_aa;
}
天生代码里,module.exports
也会像源代码里如许写,注重这里输出的时刻,aa
作为一个原始范例,输出到module.exports
里的是一个拷贝,如许一旦模块mA
加载后,再去挪用inc()
,修正的是模块内部的aa
,而不会影响输出后的aa
:
var mA = require("./mA.js");
console.log("mA.aa = " + mA.aa); // 输出1
mA.inc();
console.log("mA.aa = " + mA.aa); // 依然是1
// 这里会输出2,由于get_aa()拿到的是模块内部的aa原始援用。
console.log("mA.get_aa() = " + mA.get_aa());
但是ES6就完整不是这么一回事儿了,如果上面的模块mA
,我们用ES6输出:
export {aa, inc, get_aa}
然后在别的模块里加载mA
:
import {aa, inc} from "./mA.js"
console.log("aa = " + aa); // 输出1
inc();
console.log("aa = " + aa); // 输出2
这里不论mA
输出的是什么范例的数据,输出的都是它的援用,当别的模块载入mA
时,获得的也是mA
模块内部变量的援用。要完成这个划定规矩,mA
的generated code
就不能简朴地直接给module.exports
设置aa
这个原始变量范例了,而是像上面的get_aa
那样,给它定义getter
。比方当我们export aa
,Webpack会天生相似于如许的代码:
var aa = 1;
defineGetter(module.exports, “aa”, function(){ return aa; });
defineGetter
的定义以下:
function defineGetter(exports, name, getter) {
if (!Object.prototype.hasOwnProperty.call(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter,
});
}
}
如许就完成了我们须要的援用功用,也就是说,在module.exports
上,我们并不是定义aa
这个原始范例,而是定义aa
的getter,使之指向其原模块内部aa
的援用。
不过关于export default
,当输出原始范例时,它又回到了拷贝,而不是getter援用的体式格局,即关于如许的输出:
export default aa;
Webpack会天生如许的代码:
module.exports['default'] = aa;
我还没完整弄清晰如许做是不是相符ES6规范,懂的童鞋能够留下批评。
固然话说回来,模块中直接输出aa
如许的原始范例的变量照样挺少见的,但并不是不能够。源代码一旦有如许的行动,ES6和CommonJS就会表现出完整差别的特征,所以Webpack也必需完成这类区分。
__esModule
:
Webpack对ES6模块输出的另一个特别处置惩罚是__esModule
,比方是我们定义ES6模块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。为何要如许做?由于当他人援用一个模块时,它并不知道这个模块是以CommonJS照样ES6作风输出的,所以__esModule
的意义就在于通知他人,这是一个ES6模块。关于它的详细作用我们继承看下面的import
部份。
import
:
这是一种比较简朴的import体式格局:
import {aa} from './mA.js'
// 基础等价于
var aa = require('./mA.js')['aa']
然则当他人如许援用时:
import m from './m.js'
状况会轻微庞杂一点,它须要载入模块m
的export default
部份,而模块m能够并不是是由ES6的export来写的,也能够基础没有export default。如许在别的模块中,当一个依靠模块以相似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);
这就用到了之前export部份的__esModule
了。我们定义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/export
上,Webpack须要做许多特别处置惩罚的处所。我的剖析还并不完整,发起感兴趣的童鞋本身去敲一下而且浏览编译后的代码,细致比较CommonJS和ES6的差别。不过就完成而言,它们并没有实质区分,而且Webpack末了天生的generated code也照样基于CommonJS的module/exports
这一套机制来完成模块的加载的。
模块治理体系
以上就是Webpack怎样打包构造模块,完成runtime模块加载的解读,实在它的道理并不难,中心的头脑就是竖立模块的治理体系,而如许的做法也是具有普遍性的,如果你读过Node.js的Module部份的源代码,就会发明实在用的是相似的要领。这里有一篇文章能够参考。