阅读 580

一文搞懂JS模块、模块格式、模块加载器和模块打包器

目前现代化的JS开发方式大行其道。撸代码时,你可能也会不理解为啥要使用这些繁复的现代化工具。

接下来我们就来一起学习下js模块、模块化解决方案、模块加载器和模块打包器的区别。

本文的主要意图是帮大家快速理解现代前端JS开发的概念,并不会深入去探讨每种工具和模式。

什么是模块?

模块是一块可复用的代码。它封装了实现细节,对外提供公开的API以便其他代码可以轻松加载和使用。

为啥要使用模块?

从技术应用的角度来说,模块并不是必须的。

模块化是一种代码组织方式。开发者们从六七十年代开始在各种编程语言中以多种形式使用了。

在JS中,模块化理想情况下应该允许我们:

  • 抽象代码:将功能实现抽离到特定的库,使用者无需去关注复杂的内部实现逻辑。
  • 封装代码:如果不希望代码被修改,则将代码隐藏在模块中。
  • 复用代码:以避免重复写相同的代码。
  • 管理依赖:轻松改变依赖而无需重写我们的代码。

ES5中的模块模式

ES5及更早之前的版本,在设计时并没有考虑到模块机制。随着时间的推移,开发者想出了多种方式在JS中模拟实现模块机制。

这里我们介绍2个简单的实现方式。

Immediately Invoked Function Expression(IIFE)

(function(){
    // ...dosomething
})()
复制代码

IIFE本质上定义一个立即执行的匿名函数。

要注意的是匿名函数是被括号围起来的,在JS中,如果代码行是以关键字function开始,表示声明一个函数。

// 函数声明
function(){
    console.log('test');
}
复制代码

直接调用函数声明会报错:

// 直接调用函数声明
function(){
    console.log('test');
}()

// => Uncaught SyntaxError: Unexpected token )
复制代码

通过给函数加上圆括号,使其成为一个函数表达式:

// 函数表达式
(function() {
  console.log("test");
});

// => 返回 function(){ console.log('test') }
复制代码

函数表达式返回一个函数,因此我们可以直接调用它:

// IIFE:立即执行函数表达式
(function() {
  console.log("test");
})();

// => console中打印字符串 'test'
复制代码

IIFE 带来的好处:

  • 将代码的复杂性封装到 IIFE 中,我们无需关注其实现。
  • IIFE 中定义变量,避免对全局作用域造成污染。

不过,IIFE 没有提供依赖管理机制。

Revealing Module partten

Revealing Module parttenIIFE 类似,不同之处是它把IIFE的返回值赋给一个变量。

// 将模块导出为一个全局变量
var singleton = (function() {
  // 内部逻辑
  function sayHello() {
    console.log("Hello");
  }

  // 对外暴露的 API
  return {
    sayHello: sayHello,
  };
})();
复制代码

这里我们没有把匿名函数放在括号内,因为function关键字没有在行首。

// 访问模块的方法
singleton.sayHello();
// => Hello
复制代码

除了导出单例对象,模块还可以导出一个构造函数:

// 将模块暴露为全局变量
var Module = function() {
  // 内部逻辑
  function sayHello() {
    console.log("Hello");
  }

  // 导出 API
  return {
    sayHello: sayHello,
  };
};
复制代码

注意,我们没有在声明时立即执行函数。

相反,我们使用模块的构造函数来实例化一个模块:

var module = new Module();
复制代码

访问模块的公共 API:

module.sayHello();
// => Hello
复制代码

这种模式的好处与 IIFE 相同,不过同样它也不能给开发者提供依赖管理的机制。

随着JS的发展,衍生了很多种模块定义语法,每种语法都有各自的优点和缺点。我们称之为模块格式。

模块格式

模块格式是我们用来定义一个模块的语法。

ES6出现之前,JS没有提供官方的模块定义语法。因此,聪明的开发者们提出了多种定义模块的方式。

一些广为使用的模块格式有:

  • Asynchronous Module Definition(AMD)
  • CommonJS
  • Universal Module Definition(UMD)
  • System.register
  • ES6 Module

下面我们快速过一下每种模块定义的方式。

AMD

AMD模块运行在浏览器环境下,它使用define函数来定义模块。

define(['dep1', 'dep2'], function (dep1, dep2) {
    return function () {};
});
复制代码

CommonJS

CommonJS模块运行在nodejs环境下,它使用 require 管理依赖,module.exports 来定义模块。

var dep1 = require('./dep1');  
var dep2 = require('./dep2');

module.exports = function(){  
  // ...
}
复制代码

UMD

UMD模块可以同时运行在浏览器和Nodejs环境下。

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      define(['b'], factory);
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('b'));
  } else {
    root.returnExports = factory(root.b);
  }
}(this, function (b) {
  return {};
}));
复制代码

System.register

System.register 模块的设计意图是在ES5中支持ES6 模块语法:

import { p as q } from './dep';

var s = 'local';

export function func() {  
  return q;
}

export class C {  
}
复制代码

ES6 module

从ES6开始,js提供了原生的模块机制。它使用export向外暴露API:

// lib.js

// 对外导出sayHello
export function sayHello(){  
  console.log('Hello');
}

// 不对外导出
function somePrivateFunction(){  
  // ...
}
复制代码

使用import导入另一个模块中的API:

import { sayHello } from './lib';
sayHello();
复制代码

为导入的API使用别名:

import { sayHello as say } from './lib';

say();  
// => Hello
复制代码

导入整个模块:

import * as lib from './lib';

lib.sayHello();  
// => Hello
复制代码

模块中定义默认导出项:

// lib.js

// 导出默认函数
export default function sayHello(){  
  console.log('Hello');
}

// 导出非默认函数
export function sayGoodbye(){  
  console.log('Goodbye');
}
复制代码

然后使用如下方式导入:

import sayHello, { sayGoodbye } from './lib';

sayHello();  
// => Hello

sayGoodbye();  
// => Goodbye
复制代码

导出一切你想导出的内容:

// lib.js

// 导出默认函数
export default function sayHello(){  
  console.log('Hello');
}

// 导出非默认函数
export function sayGoodbye(){  
  console.log('Goodbye');
}

// 导出简单值
export const apiUrl = '...';

// 导出对象
export const settings = {  
  debug: true
}
复制代码

遗憾的是,不是所有的浏览器都支持了原生的模块语法。不过我们在代码中使用原生的模块语法,然后借助babel等转译工具将ES6模块语法转译成ES5所支持的AMD或CommonJS等模块语法。

模块加载器

模块加载器负责解析和加载以特定模块语法定义的模块。模块加载器在运行时执行:

  • 首先在浏览器中加载模块加载器
  • 告诉模块加载器应用的js入口文件
  • 加载器去下载并解析js入口文件
  • 加载器按需下载所有的js文件

打开浏览器的调试面板,你会看到加载器按需加载的js文件。

以下是两个常见的模块加载器:

  • RequireJS:AMD模块加载器
  • SystemJS:CommonJS、AMD、UMD以及System.register 模块加载器

模块打包器

打包器可以替换加载器。不过与加载器不同,打包器是在构建时运行:

  • 使用打包器生成一个js文件(例如app.js)
  • 在浏览器中加载该文件

如果你在浏览器的调试工具的网络面板中,只会看到浏览器只加载了一个文件。浏览器中不需要模块加载器。所有的代码都被打包在了一个js文件中。

在按需加载的场景下,打包器通常也会提供模块加载器。以按需请求对应的js文件。

列举几个常见打包器:

  • webpack
  • rollup
  • browserify

总结

为了更好地理解现代JS开发环境中的各种工具,理解模块、模块格式、模块加载器和模块打包器之间的区别很重要。

模块是可复用的代码片段,它封装了实现细节,并对外暴露一个公开API,以便其他代码加载使用。

模块格式是用来定义模块的语法。很早之前就已经出现了各种模块格式,如AMD,CommonJS,UMD和System.register。从ES6开始原生提供了模块定义语法。

模块加载器在运行时解释并加载以特定模块格式编写的模块,常见的加载器有RequireJS和SystemJS。

模块打包器可以替代模块加载器。它在构建时生成一个 包含所有代码的JS包。常见的打包器有webpack、rollup和browserify。

好啦,读到这里相信你已经对现代JS开发的知识有了更好地理解了。

文章分类
前端
文章标签