转载的原文地址https://zhuanlan.zhihu.com/p/158683510

什么是 JavaScript 模块

javaScript 模块指的是一段可复用的,独立的代码。他们通常都有特定的功能,能在整个代码系统里被引进或删除 —— 这个概念多少类似于 Java 或者 Python 中的类。
模块通常是独立的——与其他代码解耦,因此方便修改。这也提高了代码的可读性和可维护性。模块化在使部分代码保持私有,仅暴露公共部分的的同时,还解决了命名空间模糊性的问题。

史前时代

在标准的 JavaScript 模块化方案还没被提出之前,显示模块化模式(Revealing Module Pattern)被用来模拟模块化。

  1. var revealingModule = (function(){
  2. var privateVar = "Ben Thomas"; //私有变量
  3. function setNameFn(strName){
  4. privateVar = strName;
  5. }
  6. return {
  7. setName: setNameFn
  8. }
  9. })()
  10. revealingModule.setName("tom")

利用自执行函数和闭包的特性将部分代码封装私有化,仅暴露部分公用的方法。
使用这个模式的优点是可以在一个js文件中定义多个模块,但缺点是无法异步导入模块,无法动态的导入。

这种模式也一定程度地损害了代码的可读性和可维护性:

  1. 想象有多个js文件被同时使用,后面的 js 代码里要保证模块在前面的 js 里被声明了才能安心使用该模块;
  2. 当后面的 js 文件中使用到某个模块时,IDE也无法帮你快速找到这个模块的定义是什么、在哪个文件;
  3. 一旦模块的命名重复了,后面声明的模块就会覆盖掉前面的,造成bug (命名空间问题)
  4. 代码规模到达一定程度后,这种模块化就会失控,无法维护

    CommonJs规范

    CommonJs的出发点:JS没有完善的模块系统,标准库少,缺少包管理工具。伴随着nodejs的兴起,能让js在任何地方运行,特别是服务端,也达到了具备开发大型项目的能力。
    NodeJs是CommonJs规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
    采用 CommonJS 规范需要使用两个关键字 requireexports ,其中 require 用来声明导入某个 模块,而 exports 声明当前 js 文件要导出的内容

    1. //------ store/customer.js 文件------
    2. exports = function(){
    3. return customers.get('store);
    4. }
    1. //------ payments.js 文件------
    2. var customerStore = require('store/customer'); // 导入模块

    Nodejs模块化的实现基本遵循CommonJs规范,稍有不同的是Nodejs使用了module.exports而不是exports

    1. //store/customer.js 文件
    2. function customerStore(){
    3. return customers.get('store);
    4. }
    5. modules.exports = customerStore;

    特点:

    CommonJs用同步的方式加载模块,在服务端,模块文件都在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络问题,更合理的方案是使用异步加载。
    一个文件就是一个模块,拥有单独的作用域
    普通方式定义的变量、函数、对象都属于改模块内
    nodejs模块化的实现是同步的,因为nodejs通常在服务端,所有js文件都在文件系统上,基本没有异步的问题。require即可以传入具体的模块路径,也可以传入模块名,当require某个模块名时,nodejs就会在node_modules文件夹中查找对应的模块。

    缺点

    一个文件一个模块的形式有点僵硬。
    只有对象能被导出(函数也是特殊的对象),即无法导出变量/常量。
    CommonJS 规范不能直接在浏览器的js环境下使用 ( 必须使用 Webpack 等工具转译处理)

    注意点

  5. 当exports和module.exports同时存在时,module.exports会覆盖exports

  6. 当模块内全是exports时,就等用于module.exports
  7. exports就是module.exports的子集
  8. 所有代码运行在模块作用域,不会污染全局作用域
  9. 模块可以多次加载,但只会在第一次加载时运行,然后运行结果被缓存,以后再加载,就直接读取缓存结果
  10. 模块加载顺序,按照代码出现的顺序同步加载
  11. __dirname代表当前文件所在的文件夹路径
  12. __filename代表当前模块文件所在的文件夹路径+文件名

    Asynchronous Module Definition (AMD)

    AMD 规范的出现就是因为 CommonJS 无法直接在浏览器 js 环境下使用,并且正如它的名字所表达的意思 —— AMD 天然支持异步的模块加载
    AMD 使用 define 函数。define 函数有多种重载版本
    define 函数最多接收3个参数:模块id, 依赖的模块构成的数组,回调函数

    1. define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
    2. exports.verb = function() {
    3. return beta.verb();
    4. //Or:
    5. return require("beta").verb();
    6. }
    7. });

    一个匿名的模块

    1. define(["alpha"], function (alpha) {
    2. return {
    3. verb: function(){
    4. return alpha.verb() + 2;
    5. }
    6. };
    7. });

    直接导出对象字面量

    1. define({
    2. add: function(x, y){
    3. return x + y;
    4. }
    5. });

    还能兼容 CommonJS 的 require 和 exports

    1. define(function (require, exports, module) {
    2. var a = require('a'),
    3. b = require('b');
    4. exports.action = function () {};
    5. });

    特点:

    AMD 的设计初衷就是给浏览器环境使用的——可以加快页面启动时间,而且这些模块导出内容可以是对象、函数、构造器、字符串、JSON等等。支持多模块多文件。

    实现:

    RequireJS 是 AMD API 完整的实现者。使用require.js实现AMD规范的模块化,用require.config()指定引用路径等,用define()定义模块,用require()加载模块。

    1. // 定义模块
    2. define('moduleName', ['a', 'b'], function(ma,mb){
    3. return someExportValue
    4. })
    5. // 引入模块
    6. require(['a', 'b'], function(ma, mb){
    7. // code
    8. })

    requirejs主要解决的问题

  • 文件可能有依赖关系,被依赖的文件需要早于依赖的它的文件加载到浏览器
  • js加载的时候回停止页面渲染,加载文件

ECMAScript 6 模块化

ECMAScript 6 又名 ES6 又名 ES2015 ,终于推出了 JavaScript 原生的模块化方案,ES6在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
主要使用了两个关键字 imprort 和 export (注意和 CommonJS 不同,没有 s 后缀 ),export可以导出的是一个对象中包含的多个属性、方法。export default只能导出一个可以不具名的函数。我们可以通过import进行引用。
ES6的模块不是对象,import命令会被JavaScript引擎静态分析,在编译时就引入模块代码,而不是代码运行时加载,所以无法实现条件加载,因此可以静态分析,可以treeshaking。

  1. // lib/math.js
  2. export function sum (x, y) { return x + y }
  3. export var pi = 3.141593
  4. // someApp.js
  5. import * as math from "lib/math"
  6. console.log("2π = " + math.sum(math.pi, math.pi))
  7. // otherApp.js
  8. import { sum, pi } from "lib/math"
  9. console.log("2π = " + sum(pi, pi))

还可以使用 通配符 和 default 关键字

  1. // lib/mathplusplus.js
  2. export * from "lib/math" // 把 lib/math 里的内容一起导出
  3. export var e = 2.71828182846
  4. export default (x) => Math.exp(x) // 默认导出幂函数
  5. // someApp.js
  6. import exp, { pi, e } from "lib/mathplusplus" // exp 是默认导出的 幂函数; 而 pi 其实是来源于 lib/math
  7. console.log("e^{π} = " + exp(pi))

特点:

不同于 require 和 define , 在 ES6 import 语句是静态的,只能放在 js文件的头部(前几行)—— 在之后的ES版本中,将支持动态/异步的import 参考官方文档
export 语句用来导出模块内容,支持导出对象,函数,变量等等。
由于 import 和 export 都是静态语句,方便代码依赖分析,比如 IDE 就能轻松地帮我们跳转、查询依赖模块。
原生JavaScript的方案,目前已被大多数现代框架使用 如 React,Vue, Angular 等等。

缺点:

目前浏览器环境还没有普遍支持es6的模块化语法,还是需要借助 Babel 等工具转译代码。

ES6模块的具体分析,和其他模块化的区别 https://www.yuque.com/beiniaonanyou/igewkv/gt3hhp

UMD通用模块规范

一种整合CommonJs和AMD规范的方法,希望能做到跨平台模块方案
运行原理

  • UMD先判断是否支持Node.js模块(exports)是否存在,存在则使用node.js模块模式
  • 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块
    1. (function (window, factory){
    2. if(typeof exports === 'object'){
    3. module.exports = factory()
    4. }else if(typeof define === 'function'){
    5. define(factory)
    6. }else{
    7. window.eventUtil = factory()
    8. }
    9. })(this, function(){
    10. // module ...
    11. })