【JavaScript高级】模块化规范「一文让你彻底搞懂前端模块化规范 & 区别」

在JS最早出现的时候,是为了实现一些简单的功能,但随着浏览器的不断发展,对于JS的要求也越来越高,需要实现一些较为复杂的功能。这个时候开发者为了维护方便,会把不同功能的模块抽离出来写入单独的 js 文件里,但是当项目更为复杂的时候,html 可能会引入很多个js文件,而这个时候就会出现命名冲突,污染全局作用域,代码库混乱不堪等一系列问题,这个时候模块化的概念及实现方法应运而生。



模块化

什么是模块化规范

模块化规范就是为 JavaScript 提供一种模块编写、模块依赖和模块运行的方案。降低代码复杂度,提高解耦性。

一个模块就是实现某个特定功能的文件,我们可以很方便的使用别人的代码,想要什么模块,就引入那个模块。但是模块开发要遵循一定的规范,后面就出现了我们所熟悉的AMD和CMD规范。

为什么要使用模块化

模块化可以使我们的代码低耦合,功能模块直接不相互影响。模块化主要有以下几点好处:

  1. 可维护性:每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。
  2. 命名空间:在 JavaScript 中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题【这样的问题在我们开发过程中是要极力避免的】。
  3. 可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新项目中。

AMD(Asynchronous Module Definition)

采用 异步方式 加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置

在AMD规范中,我们使用 define 定义模块,使用 require 加载模块。

定义模块(define) 及 规范模块加载(require)

模块必须采用 define() 函数来定义。

define(id?, dependencies?, factory);
/*
 * id是定义的模块名,这个参数是可选的,如果没有定义该参数,模块名字应该默认为模块加载器请求的指定脚本的名字,如果有该参数,模块名必须是顶级的绝对的。
 * dependencies是定义的模块中所依赖的模块数组,依赖模块优先级执行,并且执行结果按照数组中的排序依次以参数的形式传入factory。
 * factory是模块初始化要执行的函数或对象,只被执行依次,如果是对象,则为该模块的输出值。
 */

AMD 采用 require 语句加载模块,它要求两个参数。

require([module], callback); 
/*
 * require 要传入两个参数
 * 第一个是[module],是一个数组,就是要加载的模块
 * 第二个 callback 是加载成功之后的回调函数
 */

使用方法

  1. 若一个模块不依赖其他模块。可以直接定义在 define() 函数中。

    // math.js
    
    define(function (){
     var add = function (x,y){
      return x+y;
     };
     return {
      add: add
     };
    });
    
  2. 若这个模块还依赖其他模块,那么 define() 函数的第一个参数,必须是一个数组,指明该模块的依赖性。当 require() 函数加载 test 模块时,就会先加载 math.js 模块。

    // dataService.js
    
    define(['math'], function (math) {
      function doSomething() {
        let result = math.add(2, 9);
        console.log(result);
      }
      return {
        doSomething
      };
    });
    
  3. 设置一个主模块,统一调度当前项目中所有依赖模块。

    // main.js
    
    (function () {
      require.config ({
        // baseUrl:'',    
        paths:{   
          dataService:'./dataService',
          math:'./math'
        }
      })
      require(['dataService'], function (dataService) {
        dataService.doSomething()
      });
    })();
    
    
  4. 在 index.html 中引入 require.js,并设置 data-main 入口主模块。

    // index.html
    
    <script data-main="./js/main.js" src="./js/require.js"></script>
    
  5. 本案例中所有源码,目录结构及每个模块的作用,如下图所示(源码同上1234步骤)
    在这里插入图片描述

非规范模块加载

理论上 require.js 加载的模块,必须是按照 AMD 规范用 define() 函数定义的模块。但实际上,虽然已经有一部分流行的函数库(比如 jQuery )符合 AMD 规范,但更多的库并不符合。那么require.js 如何能够加载非规范的模块呢?

这样的模块在用 require() 加载之前,要先用 require.config() 方法,定义它们的一些特征。

/*
 * require.config() 接受一个配置对象,这个对象有一个 shim 属性,专门用来配置不兼容的模块。每个模块要定义:
 * exports:输出的变量名,表示这个模块外部调用时的名称;
 * deps:数组,表示该模块的依赖性。
 */
// 如jQuery 的插件还可以这样定义:
require.config({
 shim: {
	'jquery.scroll': {
	 deps: ['jquery'],
	 exports: 'jQuery.fn.scroll'
	}
 }
})

例如,underscorebackbone 这两个库,都没有采用 AMD 规范编写。如果要加载的话,必须先定义它们的特征。

require.config({
 shim: {
  'underscore': {
   exports: '_'
  },
  'backbone': {
   deps: ['underscore', 'jquery'],
   exports: 'Backbone'
   }
 }
});

特点

  1. AMD 允许输出的模块兼容 CommonJS;
  2. 异步并行加载,不阻塞 DOM 渲染;
  3. 推崇依赖前置,也就是提前执行(预执行),在模块使用之前就已经执行完毕。

CMD(Common Module Definition)

CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖

定义模块(define) 及 模块加载(require)

定义模块使用全局函数 define,接收一个 factory 参数,可以是一个函数,也可以是一个对象或字符串;

// factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块。
define(factory);
// 如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数 require、exports 和 module。
define(function(require, exports, module) {
  //...
});
/*
 * factory 是函数时有三个参数,function(require, exports, module):
 * require:函数用来获取其他模块提供的接口require(模块标识ID)
 * exports: 对象用来向外提供模块接口
 * module :对象,存储了与当前模块相关联的属性和方法
 */

使用方法:

  1. factory 是函数时:

    // foo.js
    define(function(require, exports, module) {
      const name = "bajie";
      const age = 18;
      function sum(num1, num2) {
        return num1 + num2
      }
    
      // exports.name = name;
      // exports.age = age
      module.exports = {
        name,
        age
      }
    })
    
    // main.js 
    define(function(require, exports, module) {
      const foo = require("./foo");
      console.log("main:", foo);
    })
    
  2. factory 为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以定义一个 JSON 数据模块:

    // 定义 foo.js
    define({"foo": "bar"});
    
    // 导入使用
    define(function(require, exports, module) {
     var obj = require('foo.js')
     console.log(obj)   // {foo: "bar"}
    });
    

UMD(Universal Module Definition)

UMD 是 AMD 和 CommonJS 的糅合

UMD的实现很简单:

  1. 先判断是否支持 Node.js 模块(exports是否存在),存在则使用 Node.js 模块模式。
  2. 再判断是否支持 AMD(define是否存在),存在则使用 AMD 方式加载模块。
  3. 前两个都不存在,则将模块公开到全局(windowglobal)。

使用方法:

(function (window, factory) {
  if (typeof exports === 'object') {
      module.exports = factory();
   } else if (typeof define === 'function' && define.amd) {
      define([],factory);
   } else {
      window.eventUtil = factory();
   }
})(this, function () {
	return {};
});

CommonJS(同步加载模块)

CommonJS 规范主要应用于Node,每个文件就是一个模块,有自己的作用域,即在一个文件中定义的变量、函数、类都是私有的,对其他文件不可见。

CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测试(unit testing)等部分。

这个模块中包括CommonJS规范的核心变量:exportsmodule.exportsrequire
我们可以使用这些变量来方便的进行模块化开发;

CommonJs规范规定,每个模块内部有两个变量可以使用:requiremodule

require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
module 代表的是当前模块,是一个对象,存储着当前模块的相关联的属性和方法。exportsmodule 上的一个属性。该属性表示当前模块对外输出的接口,其它文件加载该模块,实际上就是读取 module.exports 变量。

导出模块(module.exports)

Node.js 为每个模块提供一个 exports 变量,指向 module.exports。相对于在每个模块头部,有一行这样的命令:var exports = module.exports;

使用方法

function myModule() {
  var name;
  this.setName = function(thyName) {
    name = thyName;
  };
  this.sayName = function() {
    console.log('Hello ' + name);
  };
}
module.exports = myModule;

加载模块(require)

使用 require 函数 加载模块(即被依赖模块的 module.exports 对象)。

  1. 按路径加载模块;
  2. 通过查找 node_modules 目录加载模块;
  3. 加载缓存:Node.js 是根据实际文件名缓存,而不是 require() 提供的参数缓存的,如 require('express')require('./node_modules/express') 加载两次,也不会重复加载,尽管两次参数不同,解析到的文件却是同一个。
  4. 核心模块拥有最高的加载优先级,换言之如果有模块与其命名冲突,Node.js 总是会加载核心模块。

使用方法

var myModule = require('./myModule');

var myModuleInstance = new myModule();
myModuleInstance..setName('BaJie');
myModuleInstance..sayName(); // 'Hello BaJie!'

特点

  1. 同步加载方式,适用于服务端,因为模块都放在服务器端,对于服务端来说模块加载较快,不适合在浏览器环境中使用,因为同步意味着阻塞加载。
  2. 所有代码都运行在模块作用域,不会污染全局作用域。
  3. 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。
  4. 模块加载的顺序,按照其在代码中出现的顺序。

require文件查找规则

一、导入格式:
require(X)
二、查找规则:
  • 情况一:X是一个Node核心模块,比如 path、http

    • 直接返回核心模块,并且停止查找
  • 情况二:X是以 ./…//(根目录)等开头的

    • 第一步:将 X 当做一个文件在对应的目录下查找;

      • 如果有后缀名,按照后缀名的格式查找对应的文件

      • 如果没有后缀名,会按照如下顺序:

          1. 直接查找文件X
          2. 查找X.js文件
          3. 查找X.json文件
          4. 查找X.node文件
        
    • 第二步:没有找到对应的文件,将 X 作为一个目录,查找目录下面的 index 文件

        1. 查找X/index.js文件
        2. 查找X/index.json文件
        3. 查找X/index.node文件
      
    • 第三步:如果没有找到,那么报错:not found

模块的加载过程

  1. 模块在被第一次引入时,模块中的js代码会被运行一次;

  2. 模块被多次引入时,会缓存,最终只加载(运行)一次;

    Q:为什么只会加载运行一次呢?
    A:这是因为每个模块对象 module 都有一个属性:loaded,为 false 表示还没有加载,为 true 表示已经加载;

  3. 如果有循环引入,那么加载顺序是什么?

    在这里插入图片描述

    Q:如果出现上图模块的引用关系,那么加载顺序是什么呢?
    A: 这个其实是一种数据结构:图结构;
    图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
    Node采用的是深度优先算法:所以加载顺序为:main -> aaa -> ccc -> ddd -> eee -> bbb

将 CommonJS 应用于浏览器?

1. CommonJS 加载模块是同步的:
	同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
	这个在服务器不会有什么问题,因为服务器加载的 js 文件都是本地文件,加载速度非常快;

2. 如果将它应用于浏览器呢?
	浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;
	那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;

所以在浏览器中,我们通常不使用CommonJS规范:
	当然在 webpack 中使用 CommonJS 是另外一回事;
	因为它会将我们的代码转成浏览器可以直接执行的代码;

ES Module

在 ES6 没出来之前,模块加载方案主要使用 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。ES6 的模块功能汲取了 CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。

ES6 模块的设计思想,是尽量的静态化,使得 编译时 就能确定模块的依赖关系,以及输入和输出的变量。

ES6 中,import 引用模块,使用 export 导出模块。默认情况下,Node.js默认是不支持 import 语法的,通过 babel 项目将 ES6 模块 编译为 ES5 的 CommonJS。因此 Babel 实际上是将 import/export 翻译成 Node.js 支持的 require/exports

导出模块(export)

一般来说,一个模块对应的就是一个文件,该文件内部的变量外部无法获取,如果你希望外部能够读取到某个变量,就需要使用 export 关键字输出该变量。

  • 方式一:在语句声明的前面直接加上 export 关键字

    export const name = "八戒";
    export const age = 18;
    
  • 方式二:将所有需要导出的标识符,放到 export 后面的 {} 中

    // user.js
    let name = "八戒";
    let age = 18;
    function sayHello() {
      console.log("Hello 八戒!");
    }
    
    export {
        name,
        age,
        sayHello
    }
    
  • 方式三:导出时给标识符起一个别名

    // user.js
    const name = "八戒";
    const age = 18;
    function sayHello() {
      console.log("Hello 八戒!");
    }
    export {
      name as fName,
      age as fAge,
      sayHello as fSay
    }
    
    //此时导入时要用别名导入
    import {fName, fAge, fSay} from "./user.js"
    

加载模块(import)

上面已经使用 export 命令定义了模块对外的接口后,其它的JS文件就可以通过 import 命令加载这个模块。

  • 方式一:普通导入

    import {name, age, sayHello} from "./user.js"
    
  • 方式二:导入时给标识符起别名

    import {name as fName, age as fAge, sayHello as fSay} from "./user.js"
    
  • 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上

    // 将导出的所有内容放到一个标识符中
    import * as user from "./user.js"
    // 在使用时
    console.log(user.name);
    console.log(user.age);
    console.log(user.sayHello());
    
  • 注意:导入是实时只读的,不允许运行时改变

    // ⚠️ 下面的写法是不允许的
    import * as user from "./user.js"
    user.height = 180;
    user.setOld = function () {};
    

export 和 import 结合使用

在当前 js 文件中导入 math.js 和 format.js,然后统一将导入的模块以当前文件的形式导出

import {add, sub} from './math.js'
import {timeFormat, priceFormat} from './format.js'

export {
  add,
  sub,
  timeFormat,
  priceFormat
}

default 用法

在一个模块中,只能有一个默认导出(default export)

  • 默认导出 export 时可以不需要指定名字;
  • 在导入时不需要使用 {},并且可以自己来指定名字;
// user.js
const name = "BaJie";
export default name;

// main.js
import bj from "./user.js"
console.log(bj); // BaJie

总结

模块化规范大总结

在这里插入图片描述

AMD 与 CMD 的区别?

  1. 模块定义时对依赖的处理不同

    • AMD推崇依赖前置,在定义模块时就要声明其依赖的模块;
      difine(['module1', 'module2'],function(m1, m2){
      })
      
    • CMD推崇就近依赖,只有在用到某个模块时再使用 require 导入;
      define(function(require, exports, module){
        const module1 = require('./module1');
      })
      
  2. 对依赖模块的处理机制不同

    • 相同:AMD 和 CMD 对模块的加载方式都是异步的;
    • AMD 当加载了依赖模块之后立即执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致;
    • CMD 加载完依赖模块之后,并不会立即执行,等所有的依赖模块都加载好之后,进入回到函数逻辑,遇到 require 语句的时候,才执行对应的模块,这样模块的执行顺序就和我们书写的时候一致了。

ES Module 与 CommonJS 模块加载的区别?

  1. CommonJS 是 运行时加载

    • ComminJS 加载是先加载整个模块,生成一个对象(这个对象包含了这个模块的所有API),然后再从这个对象上面读取方法。

    • 只能在运行时确定模块的依赖关系,以及输入和输出的变量,一个模块就是一个对象,输入时必须查找对象属性。

      let { stat, exists, readfile } = require('fs');
      
      // 等同于
      let _fs = require('fs');
      let stat = _fs.stat;
      let exists = _fs.exists;
      let readfile = _fs.readfile;
      
  2. ES Module 是 编译时加载

    • ES6 Module 不是对象,它的对外接口只是一种静态定义,在代码静态定义阶段就会生成。

    • 可以在编译时就完成模块加载,引用时只加载需要的方法,其他方法不加载。效率要比 CommonJS 模块的加载方式高。

      import { stat, exists, readFile } from 'fs';
      

require 与 import 的区别?

由于内容较多,这里不展开来讲,欢迎前往 require 与 import 两种引入模块方式到底有什么区别? 进行查看。


本项目部分代码主要参考 https://www.cnblogs.com/echoyya/p/14577243.html、https://blog.csdn.net/weixin_52834435/article/details/123896045,感谢大佬分享,如有侵权请联系删除。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值