webpack构造模块的道理 - 基本篇

如今前端用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最先,剖析统统模块的依靠关联,把统统用到的模块都读取进来。
  • 每一个模块的源代码都邑被构造在一个马上实行的函数里。
  • 改写模块代码中和requireexport相干的语法,以及它们对应的援用变量。
  • 在末了天生的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里exportsmodule的由来。当运转完这个函数,模块就被加载完成了,须要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的importexport

以上的例子里都是用传统的CommonJS的写法,如今更通用的ES6作风是用importexport症结词,它们看上去好像只是语法糖,但实际上依据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模块内部变量的援用。要完成这个划定规矩,mAgenerated 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'

状况会轻微庞杂一点,它须要载入模块mexport 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运转后在mAmB上会获得差别的效果:

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部份的源代码,就会发明实在用的是相似的要领。这里有一篇文章能够参考。

    原文作者:hyuan
    原文地址: https://segmentfault.com/a/1190000013610635
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞