资讯专栏INFORMATION COLUMN

JavaScript 是如何工作的:模块的构建以及对应的打包工具

hedzr / 2546人阅读

摘要:挂机科了次使用这个结构,匿名函数就有了自己的执行环境或闭包,然后我们立即执行。注意,匿名函数的圆括号是必需的,因为以关键字开头的语句通常被认为是函数声明请记住,中不能使用未命名的函数声明。

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 20 篇。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

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

JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述!

JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!

JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏!

JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!

JavaScript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!

JavaScript 是如何工作的:与 WebAssembly比较 及其使用场景!

JavaScript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!

JavaScript 是如何工作的:Service Worker 的生命周期及使用场景!

JavaScript 是如何工作的:Web 推送通知的机制!

JavaScript 是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!

JavaScript 是如何工作的:渲染引擎和优化其性能的技巧!

JavaScript 是如何工作的:深入网络层 + 如何优化性能和安全!

JavaScript 是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!

JavaScript 是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!

JavaScript 是如何工作的:深入类和继承内部原理+Babel和 TypeScript 之间转换!

JavaScript 是如何工作的:存储引擎+如何选择合适的存储API!

JavaScript 是如何工作的:Shadow DOM 的内部结构+如何编写独立的组件!

JavaScript 是如何工作的:WebRTC 和对等网络的机制!

JavaScript 是如何工作的:编写自己的 Web 开发框架 + React 及其虚拟 DOM 原理!

如果你是 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."

这可能看起来很多,但它只是模块模式的冰山一角。 以下是我在自己的探索中发现有用的一些资源:

Learning JavaScript Design Patterns:作者是 Addy Osmani,一本简洁又令人印象深刻的书籍,蕴藏着许多宝藏。

Adequately Good by Ben Cherry:包含模块模式的高级用法示例。

Blog of Carl Danley:模块模式概览,也是 JavaScript 许多设计模式的资源库。

CommonJS 和 AMD

所有这些方法都有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为自己创建一个私有名称空间。

虽然每种方法都有效且都有各自特点,但却都有缺点。

首先,作为开发人员,你需要知道加载文件的正确依赖顺序。例如,假设你在项目中使用 Backbone,因此你可以将 Backbone 的源代码 以

下面是几个在浏览器中 构建/转换 ES6 模块的方法,其中第一个是目前最常用的方法:

使用转换器(例如 Babel 或 Traceur)以 CommonJS、AMD 或 UMD 格式将 ES6 代码转换为 ES5 代码,然后再通过 Browserify 或 Webpack 一类的构建工具来进行构建。

使用 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” 来定义模块,如:



更加详细的介绍也可以在 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...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101765.html

相关文章

  • Javascript五十问——从源头细说Webpack与Gulp

    摘要:前言与是目前圈子内比较活跃的前端构建工具。对于初学者来说,对这二者往往容易认识不清,今天,就从事件的源头,说清楚与。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。打包后形成的文件出口。 前言:Webpack 与 gulp是目前圈子内比较活跃的前端构建工具。网上有很多二者比较的文章,面试中也会经常遇到gulp,Webpack的区别这样的问题。对于初学者来说,对这二...

    lwx12525 评论0 收藏0
  • 新一代打包神器parcel简介

    摘要:友好的错误记录体验,语法突出显示的代码帧有助于查明问题。为入口资源创建一个,并为动态导入的资源创建子,这回导致代码拆分的发生。如果一个资源需要多个,它会被打包到最近的共同祖先,因此它不会被包含多次。 官方地址:https://parceljs.org/getting_... Parcel 是 Web 应用打包工具,适用于经验不同的开发者。它利用多核处理提供了极快的速度,并且不需要任何...

    Jenny_Tong 评论0 收藏0
  • 宣布 Parcel:一个快速,零配置 Web 应用打包工具 ?

    摘要:宣布一个快速,零配置的应用打包工具原文译者今天,我非常高兴地宣布,一个快速,零配置的应用程序打包工具,我对于该工具的工作已经持续了几个月。性能我被激发建立一个新的打包工具的第一个原因是性能。 ? 宣布 Parcel:一个快速,零配置的 Web 应用打包工具 ? 原文:? Announcing Parcel: A blazing fast, zero configuration web...

    learn_shifeng 评论0 收藏0
  • webpack 3 零基础入门教程 #1 - 介绍

    摘要:所以它在某些程度上,跟的功能有些相同。严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。更是明显强调模块化开发,而那些文件压缩合并预处理等功能,不过是他附带的功能。 1. webpack 是什么? showImg(https://segmentfault.com/img/remote/1460000012293461); 先来说一下 webpack 是什么。 webpack 的...

    张红新 评论0 收藏0
  • 聊聊webpack

    摘要:但是由于缺乏规范化管理,出现了很多种模块化规范,从针对的规范,到针对浏览器端的,终于在里规范了前端模块化。可以通过两种方式之一终端或。导出多个配置对象运行时,所有的配置对象都会构建。在阶段又会发生很多小事件。 随着前端的迅速发展,web项目复杂度也是越来越高。为了便捷开发和利于优化,将一个复杂项目拆分成一个个小的模块,于是模块化开发出现了。但是由于缺乏规范化管理,出现了很多种模块化规范...

    Ashin 评论0 收藏0

发表评论

0条评论

hedzr

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<