写点什么

JS/TS 项目里的 Module 都是什么?

  • 2022 年 4 月 15 日
  • 本文字数:2546 字

    阅读完需:约 8 分钟

本文分享自华为云社区《JS/TS项目里的Module都是什么?都有几种形式?loaders和bundlers的区别是什么?》,作者: gentle_zhou 。


在日常进行 JS/TS 项目开发的时候,经常会遇到 require 某个依赖和 module.exports 来定义某个函数的情况。再加上在日常审视代码的时候,发现 tsconfig.json 文件里有一个"compilerOptions",里面关于 module 引入的是"commonjs",就很好奇 Modules 都代表什么和有什么作用呢。

什么是 Module?

一个 Module(模块)顾名思义就是一段可以重复利用的代码(通常是一个特性,或则一些特性的集合;可以是一个文件或则多个文件/文件夹的集合),它封装了内部代码实现的细节并曝露一个公开的 API,让其他代码可以轻易地加载和使用。

为什么我们需要 Modules?

技术上来说,其实完成一个 JS/TS 项目,我们并不需要模块,直接上手写代码也是可以的。但就像在 JAVA、Python 软件项目里,不引入依赖一样,会导致程序员们重复写很多相同的代码。


引入 Module,为的就是可以应对 JS/TS 项目的代码越来越庞大,越来越复杂的情形。我们需要使用软件工程的方法,来管理 JS/TS 项目的业务逻辑。


在 JS/TS 项目中,模块应该允许我们实现以下功能:

  • 抽象代码:将功能委托给专门的库,这样我们就不必了解它们内部实际如何实现的(无论多复杂)

  • 封装代码:如果我们不想再更改代码了,可以将代码隐藏在模块中

  • 重用代码:避免反复编写相同的代码

  • 管理依赖:在不重写代码的情况下,轻松改变依赖关系

几种常见的 Module 形式

在 ES6 Module 出现之前,在 ES5 时期,JS 并没有提供一个官方的定义模块的规则;因此 JavaScript 社区里的天才程序员们尝试了各种形式来定义模块,以达到“在现有的运行环境下,可以实现模块效果”的目的。


一些非常有名的模块形式:

  • CommonJS

  • CommonJS 形式是用在 Node.js 环境里的,我在文章开头提到的 require 和 module.exports 就是 CommonJS 里用来定义依赖和模块的:

  var dep1 = require('./dep1');    module.exports = function(){  // ...}
复制代码
  • Asynchronous Module Definition (AMD)

  • AMD(官方github链接)则是用在浏览器中的,顾名思义这个形式是异步的,其中用 define 函数来定义模块:

  // 一个依赖数组&一个工厂函数以参数的形式调用define函数  define(['dep1', 'dep2'], function (dep1, dep2) {  //通过返回一个值来定义模块值  return function () {};  });
复制代码
  • Universal Module Definition (UMD)

  • UMD 则是可以用在浏览器和 Node.js 中,是通用的:

  (function (root, factory) {    if (typeof define === 'function' && define.amd) {      // AMD. 以同步模块的方式注册.        define(['b'], factory);    } else if (typeof module === 'object' && module.exports) {      // Node节点. 不能和严格意义上的CommonJS一起使用,但是类似CommonJS的环境里是支持使用module.expoerts的,就像node.      module.exports = factory(require('b'));    } else {      // 浏览器 globals (根节点是window)      root.returnExports = factory(root.b);    }  }(this, function (b) {      // 返回一个值来定义module export;这里返回的是一个对象,但是模块其实可以返回一个函数作为exported value.    return {};  }));
复制代码

以及现在出现的官方 ES6 模块形式,一种原生的模块形式。它用 export 来输出模块的公开 API:

// 输出函数export function sayHello(){    console.log('Hello');}
复制代码

我们可以使用 import 和 as 来引入部分代码到模块里:

import { sayHello as say } from './lib';
say(); // 输出Hello
复制代码

或则直接在一开始引入整个模块:

import * as lib from './lib';
lib.sayHello(); // 输出 Hello
复制代码

Module loaders 和 Module bundlers 的区别

两者都是为了让我们编写模块化 JS/TS 应用的时候更方便快捷。

Module loaders

模块加载器用来解析并加载以特定模块格式编写的模块,通常是一些库;可以加载、解释和执行使用特定模块格式/语法定义的 JavaScript 模块,比如 AMD 或 Common JS。


在编写模块化 JS/TS 应用程序时,通常每个模块都有一个文件。因此,当编写由数百个模块组成的应用程序时,要确保所有文件都以正确的顺序包含进去可能会非常痛苦。所以,如果有加载器会为你负责依赖管理,确保所有模块在应用程序执行时被加载,那会轻松容易很多。


模块加载器是在运行时(runtime)运行的:

  • 在浏览器中加载模块加载器

  • 告诉模块加载器加载哪个主应用文件

  • 模块加载器下载并解析主应用文件

  • 模块加载器根据需要去下载文件


如果你试着在浏览器的开发人员控制台中打开 network 选项卡,将看到许多文件是按需由模块加载器加载的:

一些流行的模块加载器的例子如下:

  • Require JS: AMD 格式的模块加载器

  • System JS: AMD, Common JS, UMD 或 System.register 格式的模块加载器

Module bundlers

模块绑定器相当于是模块加载器的替代品;基本上,它们做的事情是一样的(管理和加载相互依赖的模块)。

但模块绑定器和加载器不同的地方是,它并非是在运行时运行的,而是作为应用程序构建的一部分运行(在 build 的时候运行);而且它是在浏览器中加载的。因此,绑定器在执行代码之前会将所有模块合并到一个文件/bundle 中(比如叫 bundle.js),而不是在代码运行时再去加载出现的依赖项。比如现在流行的两个 bundlers:Webpack(AMD,Common JS, es6 模块的 bundler)和 Browserify(Common JS 模块的 bundler)。

什么时候更适合用哪个呢?

这个问题的答案取决于 JS/TS 应用程序的结构与大小。


使用 bundler 的主要优点是,它让浏览器需要下载的文件变少了很多,这可以给我们的应用程序带来性能上的优势(因为减少了加载所需的时间);但是取决于应用程序的模块数量,并不是说用 bundler 就一定是最好的。对于那种大型应用(有很多模块),模块加载器可以提供更好的性能,因为 bundler 在一开始加载一个巨大的单文件会阻碍应用的启动。


如何选取,其实只需要我们进行测试比较一下即可~

参考链接

  1. https://v8.dev/features/modules

  2. https://www.geeksforgeeks.org/node-js-modules/

  3. https://www.jvandemo.com/a-10-minute-primer-to-javascript-modules-module-formats-module-loaders-and-module-bundlers/

  4. https://stackoverflow.com/questions/38864933/what-is-difference-between-module-loader-and-module-bundler-in-javascript


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
JS/TS项目里的Module都是什么?_js_华为云开发者社区_InfoQ写作平台