JavaScript 是怎样事情的:模块的构建以及对应的打包东西

这是特地探究 JavaScript 及其所构建的组件的系列文章的第 20 篇。

想浏览更多优良文章请猛戳GitHub博客,一年百来篇优良文章等着你!

假如你错过了前面的章节,可以在这里找到它们:

  1. JavaScript 是如何事变的:引擎,运转时和挪用客栈的概述!
  2. JavaScript 是如何事变的:深切V8引擎&编写优化代码的5个技能!
  3. JavaScript 是如何事变的:内存治理+如何处置惩罚4个罕见的内存走漏!
  4. JavaScript 是如何事变的:事宜轮回和异步编程的兴起+ 5种运用 async/await 更好地编码体式格局!
  5. JavaScript 是如何事变的:深切探究 websocket 和HTTP/2与SSE +如何挑选准确的途径!
  6. JavaScript 是如何事变的:与 WebAssembly比较 及其运用场景!
  7. JavaScript 是如何事变的:Web Workers的构建块+ 5个运用他们的场景!
  8. JavaScript 是如何事变的:Service Worker 的生命周期及运用场景!
  9. JavaScript 是如何事变的:Web 推送关照的机制!
  10. JavaScript 是如何事变的:运用 MutationObserver 跟踪 DOM 的变化!
  11. JavaScript 是如何事变的:衬着引擎和优化其机能的技能!
  12. JavaScript 是如何事变的:深切收集层 + 如何优化机能和平安!
  13. JavaScript 是如何事变的:CSS 和 JS 动画底层道理及如何优化它们的机能!
  14. JavaScript 是如何事变的:剖析、笼统语法树(AST)+ 提拔编译速率5个技能!
  15. JavaScript 是如何事变的:深切类和继续内部道理+Babel和 TypeScript 之间转换!
  16. JavaScript 是如何事变的:存储引擎+如何挑选适宜的存储API!
  17. JavaScript 是如何事变的:Shadow DOM 的内部构造+如何编写自力的组件!
  18. JavaScript 是如何事变的:WebRTC 和对等收集的机制!
  19. JavaScript 是如何事变的:编写自身的 Web 开辟框架 + React 及其假造 DOM 道理!

《JavaScript 是怎样事情的:模块的构建以及对应的打包东西》

假如你是 JavaScript 的新手,一些像 “module bundlers vs module loaders”、“Webpack vs Browserify” 和 “AMD vs.CommonJS” 如许的术语,很快让你不堪重负。

JavaScript 模块体系可以使人生畏,但明白它对 Web 开辟人员至关重要。

在这篇文章中,我将以简朴的言语(以及一些代码示例)为你诠释这些术语。 愿望这对你有会有协助!

什么是模块?

好作者能将他们的书分红章节,优异的递次员将他们的递次划分为模块。

就像书中的章节一样,模块只是笔墨片断(或代码,视情况而定)的集群。然则,好的模块是高内聚低松耦的,具有差别的功用,许可在必要时对它们举行替代、删除或增添,而不会骚动扰攘侵犯团体功用。

为何运用模块?

运用模块有利于扩大、互相依靠的代码库,这有很多长处。在我看来,最重要的是:

1)可维护性: 依据定义,模块是高内聚的。一个设想优越的模块旨在尽量削减对代码库部份的依靠,如许它就可以自力地加强和革新,当模块与其他代码片断解耦时,更新单个模块要轻易很多。

回到我们的书的例子,假如你想要更新你书中的一个章节,假如对一个章节的小修改须要你调解每一个章节,那将是一场恶梦。相反,你愿望以如许一种体式格局编写每一章,即可以在不影响其他章节的情况下举行革新。

2)定名空间: 在 JavaScript 中,顶级函数局限以外的变量是全局的(这意味着每一个人都可以接见它们)。因而,“称号空间污染”很罕见,完整不相干的代码同享全局变量。

在不相干的代码之间同享全局变量在开辟中是一个大忌讳。正如我们将在本文背面看到的,经由历程为变量建立私有空间,模块许可我们防止称号空间污染。

3)可重用性:坦白地说:我们将前写过的代码复制到新项目中。 比方,假定你从之前项目编写的一些有用递次要领复制到当前项目中。

这一切都很好,但假如你找到一个更好的要领来编写代码的某些部份,那末你必须记得归去在曾运用过的其他项目更新它。

这显然是在浪费时刻。假如有一个我们可以一遍又一各处重复运用的模块,不是更轻易吗?

如何建立模块?

有多种要领来建立模块,来看几个:

模块情势

模块情势用于模仿类的看法(因为 JavaScript 自身不支撑类),因而我们可以在单个对象中存储大众和私有要领和变量——类似于在 Java 或 Python 等其他编程言语中运用类的体式格局。这许可我们为想要公然的要领建立一个面向大众的 API,同时依然将私有变量和要领封装在闭包局限中。

有几种要领可以完成模块情势。在第一个示例中,将运用匿名闭包,将一切代码放在匿名函数中来协助我们完成目标。(记着:在 JavaScript 中,函数是建立新作用域的唯一要领。)

例一:匿名闭包

(function () {
  // 将这些变量放在闭包局限内完成私有化
  
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item}, 0);
    
      return '平均分 ' + total / myGrades.length + '.';
  }

  var failing = function(){
    var failingGrades = myGrades.filter(function(item) {
      return item < 70;});
      
    return '挂机科了 ' + failingGrades.length + ' 次。';
  }

  console.log(failing()); // 挂机科了次

}());


运用这个构造,匿名函数就有了自身的实行环境或“闭包”,然后我们马上实行。这让我们可以从父(全局)定名空间隐蔽变量。

这类要领的长处是,你可以在这个函数中运用局部变量,而不会意外埠掩盖现有的全局变量,但依然可以接见全局变量,就像如许:

    var global = '你好,我是一个全局变量。)';
    
   (function () {
      // 将这些变量放在闭包局限内完成私有化
      
      var myGrades = [93, 95, 88, 0, 55, 91];
      
      var average = function() {
        var total = myGrades.reduce(function(accumulator, item) {
          return accumulator + item}, 0);
        
          return '平均分 ' + total / myGrades.length + '.';
      }
    
      var failing = function(){
        var failingGrades = myGrades.filter(function(item) {
          return item < 70;});
          
        return '挂机科了 ' + failingGrades.length + ' 次。';
      }
    
      console.log(failing()); // 挂机科了次
      onsole.log(global); // 你好,我是一个全局变量。
    
    }());

注重,匿名函数的圆括号是必须的,因为以关键字 function 开首的语句一般被以为是函数声明(请记着,JavaScript 中不能运用未定名的函数声明)。因而,四周的括号将建立一个函数表达式,并马上实行这个函数,这另有另一种叫法 马上实行函数(IIFE)。假如你对这感兴趣,可以在这里相识到更多。

例二:全局导入

jQuery 等库运用的另一种盛行要领是全局导入。它类似于我们适才看到的匿名闭包,只是如今我们作为参数传入全局变量:

(function (globalVariable) {

  // 在这个闭包局限内对峙变量的私有化
  var privateFunction = function() {
    console.log('Shhhh, this is private!');
  }

  // 经由历程 globalVariable 接口公然下面的要领
 // 同时将要领的完成隐蔽在 function() 块中

  globalVariable.each = function(collection, iterator) {
    if (Array.isArray(collection)) {
      for (var i = 0; i < collection.length; i++) {
        iterator(collection[i], i, collection);
      }
    } else {
      for (var key in collection) {
        iterator(collection[key], key, collection);
      }
    }
  };

  globalVariable.filter = function(collection, test) {
    var filtered = [];
    globalVariable.each(collection, function(item) {
      if (test(item)) {
        filtered.push(item);
      }
    });
    return filtered;
  };

  globalVariable.map = function(collection, iterator) {
    var mapped = [];
    globalUtils.each(collection, function(value, key, collection) {
      mapped.push(iterator(value));
    });
    return mapped;
  };

  globalVariable.reduce = function(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    globalVariable.each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;

  };

 }(globalVariable));

在这个例子中,globalVariable 是唯一的全局变量。与匿名闭包比拟,这类要领的长处是可以预先声明全局变量,使得他人更轻易浏览代码。

例三:对象接口

另一种要领是运用马上实行函数接口对象建立模块,以下所示:

var myGradesCalculate = (function () {
    
  // 将这些变量放在闭包局限内完成私有化
  var myGrades = [93, 95, 88, 0, 55, 91];

  // 经由历程接口公然这些函数,同时将模块的完成隐蔽在function()块中

  return {
    average: function() {
      var total = myGrades.reduce(function(accumulator, item) {
        return accumulator + item;
        }, 0);
        
      return'平均分 ' + total / myGrades.length + '.';
    },

    failing: function() {
      var failingGrades = myGrades.filter(function(item) {
          return item < 70;
        });

      return '挂科了' + failingGrades.length + ' 次.';
    }
  }
})();

myGradesCalculate.failing(); // '挂科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

正如您所看到的,这类要领许可我们经由历程将它们放在 return 语句中(比方算平均分和挂科数要领)来决议我们想要保存的变量/要领(比方 myGrades)以及我们想要公然的变量/要领。

例四:显式模块情势

这与上面的要领异常类似,只是它确保一切要领和变量在显式公然之前都是私有的:

var myGradesCalculate = (function () {
    
  // 将这些变量放在闭包局限内完成私有化
  var myGrades = [93, 95, 88, 0, 55, 91];
  
  var average = function() {
    var total = myGrades.reduce(function(accumulator, item) {
      return accumulator + item;
      }, 0);
      
    return'平均分 ' + total / myGrades.length + '.';
  };

  var failing = function() {
    var failingGrades = myGrades.filter(function(item) {
        return item < 70;
      });

    return '挂科了' + failingGrades.length + ' 次.';
  };

  // Explicitly reveal public pointers to the private functions 
  // that we want to reveal publicly

  return {
    average: average,
    failing: failing
  }
})();

myGradesCalculate.failing(); // '挂科了 2 次.' 
myGradesCalculate.average(); // '平均分 70.33333333333333.'

这可以看起来很多,但它只是模块情势的冰山一角。 以下是我在自身的探究中发明有用的一些资本:

CommonJS 和 AMD

一切这些要领都有一个共同点:运用单个全局变量将其代码包装在函数中,从而运用闭包作用域为自身建立一个私有称号空间。

虽然每种要领都有用且都有各自特征,但却都有瑕玷。

起首,作为开辟人员,你须要晓得加载文件的准确依靠递次。比方,假定你在项目中运用 Backbone,因而你可以将 Backbone 的源代码 以<script> 剧本标签的情势引入到文件中。

然则,因为 Backbone 对 Underscore.js 有很强的依靠性,因而 Backbone 文件的剧本标记不能放在Underscore.js 文件之前。

作为一位开辟人员,治理依靠关联并准确处置惩罚这些事变偶然会使人头痛。

另一个瑕玷是它们依然会致使称号空间争执。比方,假如两个模块具有雷同的称号怎么办?或许,假如有一个模块的两个版本,而且二者都须要,该怎么办?

荣幸的是,答案是一定的。

有两种盛行且有用的要领:CommonJSAMD

CommonJS

CommonJS 是一个志愿者事变组,担任设想和完成用于声明模块的 JavaScript API。

CommonJS 模块本质上是一个可重用的 JavaScript,它导出特定的对象,使其可供其递次中须要的其他模块运用。 假如你已运用 Node.js 编程,那末你应当异常熟习这类花样。

运用 CommonJS,每一个 JavaScript 文件都将模块存储在自身自力的模块高低文中(就像将其封装在闭包中一样)。 在此局限内,我们运用 module.exports 导出模块,或运用 require 来导入模块。

在定义 CommonJS 模块时,它多是如许的:

function myModule() {
  this.hello = function() {
    return 'hello!';
  }
   
  this.goodbye = function() {
    return 'goodbye!';
  }
}

module.exports = myModule;

我们运用特别的对象模块,并将函数的援用放入 module.exports 中。这让 CommonJS 模块体系晓得我们想要公然什么,以便其他文件可以运用它。

假如想运用 myModule,只须要运用 require 要领就可以,以下:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

与前面议论的模块情势比拟,这类要领有两个显著的长处:

  1. 防止全局定名空间污染
  2. 依靠关联越发明白

别的须要注重的是,CommonJS 采纳服务器优先要领并同步加载模块。 这很重要,因为假如我们须要三个其他模块,它将逐一加载它们。

如今,它在服务器上运转优越,但遗憾的是,在为浏览器编写 JavaScript 时运用起来越发难题。 可以这么说,从网上读取模块比从磁盘读取须要更长的时刻。 只需加载模块的剧本正在运转,它就会阻挠浏览器运转其他任何内容,直到完成加载,这是因为 JavaScript 是单线程且 CommonJS 是同步加载的。

AMD

CommonJS一切都很好,然则假如我们想要异步加载模块呢? 答案是 异步模块定义,简称 AMD

运用 AMD 的加载模块以下:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

define 函数的第一个参数是一个数组,数组中是依靠的种种模块。这些依靠模块在背景(以非壅塞的体式格局)加载进来,一旦加载终了,define 函数就会挪用第二个参数,即回调函数实行操纵。

接下来,回调函数吸收参数,即依靠模块 – 示例中就是 myModulemyOtherModule – 许可函数运用这些依靠项, 末了,所依靠的模块自身也必须运用 define 关键字来定义。比方,myModule以下所示:

define([], function() {

  return {
    hello: function() {
      console.log('hello');
    },
    goodbye: function() {
      console.log('goodbye');
    }
  };
});

因而,与 CommonJS 差别,AMD 采纳浏览器优先的要领和异步行动来完成事变。 (注重,有很多人深信在最先运转代码时动态加载文件是不利的,我们将在下一节关于模块构建的内容中讨论更多内容)。

除了异步性,AMD 的另一个长处是模块可以是对象,函数,构造函数,字符串,JSON 和很多其他范例,而CommonJS 只支撑对象作为模块。

也就是说,和CommonJS比拟,AMD不兼容io、文件体系或许其他服务器端的功用特征,而且函数包装语法与简朴的require 语句比拟有点冗杂。

UMD

关于同时支撑 AMD 和 CommonJS 特征的项目,另有另一种花样:通用模块定义(Universal Module Definition, UMD)。

UMD 本质上制造了一种运用二者之一的要领,同时也支撑全局变量定义。因而,UMD 模块可以同时在客户端和服务端同时事变。

简朴看一下 UMD 是如何事变的:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Browser globals (Note: root is window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Methods
  function notHelloOrGoodbye(){}; // A private method
  function hello(){}; // A public method because it's returned (see below)
  function goodbye(){}; // A public method because it's returned (see below)

  // Exposed public methods
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

Github 上 enlightening repo 里有更多关于 UMD 的例子。

Native JS

你可以已注重到,上面的模块都不是 JavaScript 原生的。相反,我们已建立了经由历程运用模块情势、CommonJS 或 AMD 来模仿模块体系的要领。

荣幸的是,TC39(定义 ECMAScript 的语法和语义的范例构造)一帮智慧的人已引入了ECMAScript 6(ES6)的内置模块。

ES6 为导入导出模块供应了很多差别的可以性,已有很多其他人花时刻诠释这些,下面是一些有用的资本:

与 CommonJS 或 AMD 比拟,ES6 模块最大的长处在于它可以同时供应两方面的上风:简明的声明式语法和异步加载,以及对轮回依靠项的更好支撑。

或许我个人最喜欢的 ES6 模块功用是它的导入模块是导出时模块的及时只读视图。(比拟起 CommonJS,导入的是导出模块的拷贝副本,因而也不是及时的)。

下面是一个例子:

// lib/counter.js

var counter = 1;

function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};


// src/main.js

var counter = require('../../lib/counter');

counter.increment();
console.log(counter.counter); // 1

在这个例子中,我们基本上建立了两个模块的对象:一个用于导出它,一个在我们须要的时刻引入。

另外,在 main.js 中的对象现在是与原始模块是互相自力的,这就是为何纵然我们实行 increment 要领,它依然返回 1,因为引入的变量和最初导入的变量是毫无关联的。须要转变你引入的对象唯一的体式格局是手动实行增添:

counter.counter++;
console.log(counter.counter); // 2

另一方面,ES6建立了我们导入的模块的及时只读视图:

// lib/counter.js
export let counter = 1;

export function increment() {
  counter++;
}

export function decrement() {
  counter--;
}


// src/main.js
import * as counter from '../../counter';

console.log(counter.counter); // 1
counter.increment();

console.log(counter.counter); // 2

超酷?我发明这一点是因为ES6许可你可以把你定义的模块拆分红更小的模块而不必删减功用,然后你还能反过来把它们合成到一同, 完整没题目。

什么是模块打包?

总体上看,模块打包只是将一组模块(及其依靠项)以准确的递次拼接到一个文件(或一组文件)中的历程。正如 Web开辟的别的各个方面,辣手的题目老是潜藏在细致的细节里。

为何须要打包?

将递次划分为模块时,一般会将这些模块构造到差别的文件和文件夹中。 有可以,你另有一组用于正在运用的库的模块,如 Underscore 或 React。

因而,每一个文件都必须以一个 <script> 标签引入到主 HTML 文件中,然后当用户接见你的主页时由浏览器加载进来。 每一个文件运用 <script> 标签引入,意味着浏览器不能不离别逐一的加载它们。

这关于页面加载时刻来讲简直是恶梦。

为相识决这个题目,我们将一切文件打包或“拼接”到一个大文件(或视情况而定的几个文件),以削减要求的数目。 当你听到开辟人员议论“构建步骤”或“构建历程”时,这就是他们所议论的内容。

另一种加快构建操纵的常常使用要领是“缩减”打包代码。 缩减是从源代码中移除不必要的字符(比方,空格,解释,换行符等)的历程,以便在不转变代码功用的情况下削减内容的团体大小。

较少的数据意味着浏览器处置惩罚时刻会更快,从而削减了下载文件所需的时刻。 假如你见过具有 “min” 扩大名的文件,如 “underscore-min.js” ,可以会注重到与完整版比拟,减少版本异常小(不过很难浏览)。

除了绑缚和/或加载模块以外,模块绑缚器还供应了很多其他功用,比方在举行更改时天生自动从新编译代码或天生用于调试的源映照。

构建东西(如 Gulp 和 Grunt)能为开辟者直接举行拼接和缩减,确保为开辟人员供应可读代码,同时有利于浏览器实行的代码。

打包模块有哪些差别的要领?

当你运用一种范例模块情势(上部份议论过)来定义模块时,拼接和缩减文件异常有用。 你真正在做的就是将一堆一般的 JavaScript 代码绑缚在一同。

然则,假如你对峙运用浏览器没法剖析的非原生模块体系(如 CommonJS 或 AMD(以至是原生 ES6模块花样)),则须要运用特地东西将模块转换为分列准确、浏览器可剖析的代码。 这就是 Browserify,RequireJS,Webpack 和其他“模块打包东西”或“模块加载东西”的用武之地。

除了打包和/或加载模块以外,模块打包器还供应了很多其他功用,比方在举行更改时天生自动从新编译代码或天生用于调试的源映照。

下面是一些罕见的模块打包要领:

打包 CommonJS

正如前面所晓得的,CommonJS以同步体式格局加载模块,这没有什么题目,只是它对浏览器不有用。我提到过有一个处理方案——个中一个是一个名为 Browserify 的模块打包东西。Browserify 是一个为浏览器编译 CommonJS模块的东西。

比方,有个 main.js 文件,它导入一个模块来盘算一组数字的平均值:

var myDependency = require(‘myDependency’);

var myGrades = [93, 95, 88, 0, 91];

var myAverageGrade = myDependency.average(myGrades);

在这类情况下,我们有一个依靠项(myDependency),运用下面的敕令,Browserify 以 main.js 为进口把一切依靠的模块递归打包成一个文件:

browserify main.js -o bundle.js

Browserify 经由历程跳入文件剖析每一个依靠的 笼统语法树(AST),以便遍历项目的全部依靠关联图。一旦肯定了依靠项的构造,就把它们按准确的递次打包到一个文件中。然后,在 html 里插进去一个用于引入 “bundle.js”<script> 标签,从而确保你的源代码在一个 HTTP 要求中完成下载。

类似地,假如有多个文件且有多个依靠时,只需通知 Browserify 的进口文件途径即可。末了打包后的文件可以经由历程 Minify-JS 之类的东西紧缩打包后的代码。

打包 AMD

假如你正在运用 AMD,你须要运用像 RequireJS 或许 Curl 如许的 AMD 加载器。模块加载器(与模块打包东西差别)会动态加载递次须要运转的模块。

提示一下,AMD 与 CommonJS 的重要区分之一是它以异步体式格局加载模块。 从这个意义上说,关于 AMD,从手艺上讲,实际上并不须要构建步骤,因为异步加载模块意味着在运转历程当中逐渐下载那些递次所须要的文件,而不是用户刚进入页面就一下把一切文件都下载下来。

但实际上,关于每一个用户操纵而言,跟着时刻的推移,大容量要求的开支在生产中没有多大意义。 大多数 Web 开辟人员依然运用构建东西打包和紧缩 AMD 模块以取得最好机能,比方运用 RequireJS 优化器,r.js 等东西。

总的来讲,AMD 和 CommonJS 在打包方面的区分在于:在开辟时期,AMD 可以省去任何构建历程。固然,在代码上线前,要运用优化东西(如 r.js)举行优化。

Webpack

就打包东西而言,Webpack 是一个新事物。它被设想成与你运用的模块体系无关,许可开辟人员在恰当的情况下运用 CommonJS、AMD 或 ES6。

你可以想晓得,为何我们须要 Webpack,而我们已有了其他打包东西了,比方 Browserify 和 RequireJS,它们可以完成事变,而且做得异常好。起首,Webpack 供应了一些有用的特征,比方 “代码支解”(code
splitting) —— 一种将代码库支解为“块(chunks)”的体式格局,从而能完成按需加载。

比方,假如你的 Web 应用递次,个中只须要某些代码,那末将全部代码库都打包进一个大文件就不是很高效。 在这类情况下,可以运用代码支解,将须要的部份代码抽离在”打包块”,在实行按需加载,从而防止在最最先就碰到大批负载的贫苦。

代码支解只是 Webpack 供应的浩瀚有目共睹的特征之一,网上有很多关于 “Webpack 与 Browserify 谁更好” 的猛烈议论。以下是一些客观岑寂的议论,协助我轻微理清了眉目:

ES6 模块

当前 JS 模块范例(CommonJS, AMD) 与 ES6 模块之间最重要的区分是 ES6 模块的设想斟酌到了静态剖析。这意味着当你导入模块时,导入的模块在编译阶段也就是代码最先运转之前就被剖析了。这许可我们在运转递次之前移,移除那些在导出模块中不被别的模块运用的部份。移除不被运用的模块能节约空间,且有用地削减浏览器的压力。

一个罕见的题目,运用一些东西,如 Uglify.js ,缩减代码时,有一个死码删除的处置惩罚,它和 ES6 移除没用的模块又有什么差别呢?只能说 “视情况而定”。

死码消弭(Dead codeelimination)是一种编译器道理中编译最优化手艺,它的用处是移除对递次运转效果没有任何影响的代码。移除这类的代码有两种长处,不只可以削减递次的大小,还可以防止递次在运转中举行不相干的运算行动,削减它运转的时刻。不会被运转到的代码(unreachable code)以及只会影响到无关递次运转效果的变量(Dead Variables),都是死码(Dead code)的领域。

偶然,在 UglifyJS 和 ES6 模块之间死码消弭的事变体式格局完整雷同,偶然则不然。假如你想考证一下, Rollup’s wiki 里有个很好的示例。

ES6 模块的差别之处在于死码消弭的差别要领,称为“tree shaking”。“tree shaking” 本质上是死码消弭反历程。它只包含包须要运转的代码,而非消除不须要的代码。来看个例子:

假定有一个带有多个函数的 utils.js 文件,每一个函数都用 ES6 的语法导出:

export function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 }

export function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
}

export function map(collection, iterator) {
  var mapped = [];
  each(collection, function(value, key, collection) {
    mapped.push(iterator(value));
  });
  return mapped;
}

export function reduce(collection, iterator, accumulator) {
    var startingValueMissing = accumulator === undefined;

    each(collection, function(item) {
      if(startingValueMissing) {
        accumulator = item;
        startingValueMissing = false;
      } else {
        accumulator = iterator(accumulator, item);
      }
    });

    return accumulator;
}

接着,假定我们不晓得要在递次中运用什么 utils.js 中的哪一个函数,所以我们将上述的一切模块导入main.js中,以下所示:

import * as Utils from ‘./utils.js’;

终究,我们只用到的 each 要领:

import * as Utils from ‘./utils.js’;

Utils.each([1, 2, 3], function(x) { console.log(x) });

“tree shaken” 版本的 main.js 看起来以下(一旦模块被加载后):

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

each([1, 2, 3], function(x) { console.log(x) });

注重:只导出我们运用的 each 函数。

同时,假如决议运用 filte r函数而不是每一个函数,终究会看到以下的效果:

import * as Utils from ‘./utils.js’;

Utils.filter([1, 2, 3], function(x) { return x === 2 });

tree shaken 版本以下:

function each(collection, iterator) {
  if (Array.isArray(collection)) {
    for (var i = 0; i < collection.length; i++) {
      iterator(collection[i], i, collection);
    }
  } else {
    for (var key in collection) {
      iterator(collection[key], key, collection);
    }
  }
 };

function filter(collection, test) {
  var filtered = [];
  each(collection, function(item) {
    if (test(item)) {
      filtered.push(item);
    }
  });
  return filtered;
};

filter([1, 2, 3], function(x) { return x === 2 });

此时,each 和 filter 函数都被包含进来。这是因为 filter 在定义时运用了 each。因而也须要导出该函数模块以保证递次一般运转。

构建 ES6 模块

我们晓得 ES6 模块的加载体式格局与其他模块花样差别,但我们依然没有议论运用 ES6 模块时的构建步骤。

遗憾的是,因为浏览器对 ES6模 块的原生支撑还不够完美,所以现阶段还须要我们做一些补充事变。

《JavaScript 是怎样事情的:模块的构建以及对应的打包东西》

下面是几个在浏览器中 构建/转换 ES6 模块的要领,个中第一个是现在最常常使用的要领:

  1. 运用转换器(比方 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 花样将 ES6 代码转换为 ES5 代码,然后再经由历程 Browserify 或 Webpack 一类的构建东西来举行构建。
  2. 运用 Rollup.js,这实在和上面差不多,只是 Rollup 捎带 ES6 模块的功用,在打包之前静态剖析ES6 代码和依靠项。 它应用 “tree shaking” 手艺来优化你的代码。 总言,当您运用ES6模块时,Rollup.js 相干于 Browserify 或 Webpack 的重要长处是 tree shaking 能让打包文件更小。 须要注重的是,Rollup提 供了几种花样来的打包代码,包含 ES6,CommonJS,AMD,UMD 或 IIFE。 IIFE 和 UMD 绑缚包可以直接在浏览器中事变,但假如你挑选打包 AMD,CommonJS 或 ES6,需须要寻觅能将代码转成浏览器能明白运转的代码的要领(比方,运用 Browserify, Webpack,RequireJS等)。

警惕踩坑

作为 web 开辟人员,我们必须阅历很多难题。转换语法文雅的ES6代码以便在浏览器里运转并不老是轻易的。

题目是,什么时刻 ES6 模块可以在浏览器中运转而不须要这些开支?

答案是:“尽快”。

ECMAScript 现在有一个处理方案的范例,称为 ECMAScript 6 module loader API。简而言之,这是一个纲领性的、基于 Promise 的 API,它支撑动态加载模块并缓存它们,以便后续导入不会从新加载模块的新版本。

它看起来以下:

// myModule.js

export class myModule {
  constructor() {
    console.log('Hello, I am a module');
  }

  hello() {
    console.log('hello!');
  }

  goodbye() {
    console.log('goodbye!');
  }
}

 // main.js
System.import(‘myModule’).then(function(myModule) {
  new myModule.hello();
});

// ‘hello!’

你亦可直接对 script 标签指定 “type=module” 来定义模块,如:

<script type="module">
  // loads the 'myModule' export from 'mymodule.js'
  import { hello } from 'mymodule';

  new Hello(); // 'Hello, I am a module!'
</script>

越发细致的引见也可以在 Github 上检察:es-module-loader

另外,假如您想测试这类要领,请检察 SystemJS,它建立在 ES6 Module Loader polyfill 之上。 SystemJS 在浏览器和 Node 中动态加载任何模块花样(ES6模块,AMD,CommonJS 或 全局剧本)。

它跟踪“模块注册表”中一切已加载的模块,以防止从新加载先前已加载过的模块。 更不必说它还会自动转换ES6模块(假如只是设置一个选项)而且可以从任何其他范例加载任何模块范例!

有了原生的 ES6 模块后,还须要模块打包吗?

关于日趋提高的 ES6 模块,下面有一些风趣的看法:

HTTP/2 会让模块打包过期吗?

关于 HTTP/1,每一个TCP衔接只许可一个要求。这就是为何加载多个资本须要多个要求。有了 HTTP/2,一切都变了。HTTP/2 是完整多路复用的,这意味着多个要乞降相应可以并行发作。因而,我们可以在一个衔接上同时处置惩罚多个要求。

因为每一个 HTTP 要求的本钱显著低于HTTP/1,因而从长远来看,加载一组模块不会形成很大的机能题目。一些人以为这意味着模块打包不再是必要的,这固然有可以,但这要细致情况细致剖析了。

比方,模块打包另有 HTTP/2 没有长处,比方移除冗余的导出模块以节约空间。 假如你正在构建一个机能至关重要的网站,那末从长远来看,打包可以会为你带来增量上风。 也就是说,假如你的机能需求不是那末极度,那末经由历程完整跳过构建步骤,可以以最小的本钱节约时刻。

总的来讲,绝大多数网站都用上 HTTP/2 的那个时刻离我们如今还很远。我展望构建历程将会保存,至少在近期内。

CommonJS、AMD 与 UMD 会被镌汰吗?

一旦 ES6 成为模块范例,我们还须要其他非原生模块范例吗?

我以为另有。

Web 开辟恪守一个范例要领举行导入和导出模块,而不须要中心构建步骤——网页开辟历久受益于此。但 ES6 成为模块范例须要多长时刻呢?

机会是有,但得等一段时刻 。

再者,众口难调,所以“一个范例的要领”可以永久不会成为实际。

总结

愿望这篇文章能帮你理清一些开辟者口中的模块和模块打包的相干看法,共提高。

原文:

https://medium.freecodecamp.o…

https://medium.freecodecamp.o…

你的点赞是我延续分享好东西的动力,迎接点赞!

交换

干货系列文章汇总以下,以为不错点个Star,迎接 加群 互相进修。

https://github.com/qq44924588…

我是小智,民众号「大迁天下」作者,对前端手艺对峙进修爱好者。我会常常分享自身所学所看的干货,在进阶的路上,共勉!

关注民众号,背景复兴福利,即可看到福利,你懂的。

《JavaScript 是怎样事情的:模块的构建以及对应的打包东西》

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