js 模块化:ESM ,CJS, AMD, UMD

一般来说,一个文件就是一个模块,这个文件内的作用域唯一,可以向外暴露变量,函数等。模块化的出现减少了代码的繁琐,利于代码复用和日后维等作用。JavaScript的模块系统有ESM ,CJS, AMD, UMD。

ESM

ECMAScript模块(ESM)是JavaScript的官方标准化模块系统。

基本用法

ECMAScript模块(ESM)是现代JavaScript应用中推荐的模块化标准。以下是ESM的基本用法大纲:

1. 导入模块

使用import关键字从其他模块导入一个或多个绑定(变量、函数、类等)。

  • 导入整个模块:

    import * as myModule from '/modules/my-module.js';
  • 导入特定的绑定:

    import { export1, export2 } from '/modules/my-module.js';
  • 导入默认导出:

    import defaultExport from '/modules/my-module.js';

2. 导出模块

使用export关键字将模块内的绑定提供给其他模块使用。

  • 导出特定的绑定:

    export const export1 = ...; 
    export function export2() { ... };
  • 默认导出(每个模块只能有一个):

    export default function() { ... }
    // 或者
    const myDefault = ...;
    export default myDefault;
  • 导出多个绑定:

    const name1 = ...;
    ...
    const nameN = ...;
    export { name1, /* …, */ nameN };

3. 重命名导入和导出

可以在导入或导出时重命名绑定。

  • 导入时重命名:

    import { export1 as alias1 } from '/modules/my-module.js';
  • 导出时重命名:

    export { myExport as default };
    export { variable1 as name1, variable2 as name2, /* …, */ nameN };

4. 导入和导出组合使用

export * from "module-name";
export * as name1 from "module-name";
export { name1, /* …, */ nameN } from "module-name";
export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name";

5. 动态导入

ESM支持通过import()函数动态导入模块,返回一个Promise对象。

import('/modules/my-module.js') 
    .then((module) => { 
        // 使用模块 
    });

模块解析

  • 静态解析: ESM的导入和导出在编译阶段解析,这有助于性能优化和代码分析。

例如,根据 ES6 规范,import 只能在模块顶层声明,所以下面的写法会直接报语法错误,不会有 log 打印,因为它压根就没有进入 执行阶段:

console.log('hello world');
 
if (true) {
  import { resolve } from 'path';
}
 
// out:
//   import { resolve } from 'path';
//          ^
// SyntaxError: Unexpected token '{'

加载机制

  • 在编译阶段,import 模块中引入的值就指向了 export 中导出的值。这有点像 linux 中的硬链接,指向同一块磁盘数据。或者拿栈和堆来比喻,这就像两个指针指向了同一个栈。

    //b.js
    let b = {
        c:3
    };
    let b1 = 4;
    
    console.log('running b.mjs');
    
    setTimeout(() => {
       console.log('b val in b', b, b1)
    },1000)
    
    const setB = (newB, newB1) => {
        b.c = newB;
        b1 = newB1
    }
    
    export {
        b,
        b1,
        setB
    }
    //a.js
    
    import { b, b1, setB } from './b.js';
    
    console.log('b val in a', b);
    
    console.log('setB to bb');
    
     setB(6,7)
    
    // b.c = 6;
    // b1 =7;
    
    //app.js
    import './a.js';
    import {b, b1} from './b.js';
    console.log(b, b1)

  • 循环依赖处理: ESM可以更好地处理循环依赖问题。

完整可运行:

//test.js
export class AbstractNode {

    constructor(parent) {
        this.parent = parent
    }

    print() {
        console.log("this.parent",this.parent)
    }

    static from(thing, parent) {
        // 是否是对象,对象返回node 不是返回leaf
        if (thing && typeof thing === 'object') return new Node(parent, thing)
        else return new Leaf(parent, thing)
    }
}

export class Node extends AbstractNode {
    constructor(parent, thing) {
        super(parent)
        this.children = thing
    }

    print() {
        console.log("node",this.parent,this.children)
    }
}

export class Leaf extends AbstractNode {
    constructor(parent, value) {
        super(parent)
        this.value = value
    }

    print() {
        console.log("leaf",this.parent,this.children)
        return this.value
    }
}
//app.js
import {AbstractNode} from './components/test.js';

let a=AbstractNode.from(1,2);
a.print()

拆分后:

//app.js
import {AbstractNode} from './components/internal.js';

let a=AbstractNode.from(1,2);
a.print()
//internal.js
export * from './ab.js'
export * from './abc.js'
export * from './abcd.js'

 

//ab.js

import {Node} from './internal.js'
import {Leaf} from './internal.js'
export class AbstractNode {
    constructor(parent) {
        this.parent = parent
    }
    print() {
        console.log("this.parent",this.parent)
    }

    static from(thing, parent) {
        // 是否是对象,对象返回node 不是返回leaf
        if (thing && typeof thing === 'object') return new Node(parent, thing)
        else return new Leaf(parent, thing)
    }
}
//abc.js

import {AbstractNode} from "./internal.js";

export class Node extends AbstractNode {
    constructor(parent, thing) {
        super(parent)
        this.children = thing

    }

    print() {
        console.log("node",this.parent,this.children)
    }
}

 

//abcd.js

import {AbstractNode} from "./internal.js";

export class Leaf extends AbstractNode {
    constructor(parent, value) {
        super(parent)
        this.value = value
    }

    print() {
        console.log("leaf",this.parent,this.children)
        return this.value
    }
}

性能

树摇(Tree Shaking): 静态分析允许工具移除未使用的代码,减小最终包的体积。

代码分割(Code Splitting): 可以将代码拆分成多个片段,按需加载。

兼容性和环境

浏览器支持: 现代浏览器原生支持ESM。

Node.js支持: Node.js通过.mjs扩展名或package.json中的"type": "module"字段支持ESM。

其他特性

ESM支持在模块顶层使用await关键字。

CJS

module.exports = someValue;

CommonJS(CJS)是一种广泛使用的JavaScript模块规范,主要用于服务器端和桌面应用程序,尤其是Node.js环境中。

基本用法

  • require: 用于导入其他模块。

    const moduleA = require('./moduleA');
  • module.exports: 用于导出模块公开的对象或函数。

  • exports: module.exports的简写,用于导出多个属性或方法。

    exports.func1 = function() { /* ... */ };
    exports.value = 123;

模块解析

  • CommonJS的模块解析发生在执行阶段,因为 require 和 module 本质上就是个函数或者对象,只有在执行阶段 运行时,这些函数或者对象才会被实例化。因此被称为运行时加载

加载机制

  • CommonJS 是动态加载,可以在代码的任何地方、任何时间点要求加载模块。

  • 对于简单类型,CommonJS 导入的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值。对于引用类型,CommonJS 导入的是被导出的值的引用的拷贝,因此模块内部的变化会影响到这个引用类型值。

    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    let obj = {
      val: 1
    };
     
    const setVal = (newVal) => {
      obj.val = newVal
    }
     
    module.exports = {
      counter: counter,
      incCounter: incCounter,
        obj,
      setVal
    };
    // main.js
    var counter = require('./lib').counter;
    var incCounter = require('./lib').incCounter;
    const { obj, setVal } = require('./lib')
    
    console.log(counter);  // 3
    incCounter();
    console.log(counter); // 3
    
    console.log(obj);
    setVal(101);
    console.log(obj);
  • 循环依赖处理:由于 commonjs 可以在任何地方加载模块,所以基本不存在循环依赖问题。 还是上面的例子:

    // app.js
    let AbstractNode = require('./components/ab.js');
    let a= AbstractNode.from({a: 1, b: 2},3);
    a.print()
    class AbstractNode {
        constructor(parent) {
            this.parent = parent
        }
        print() {
            console.log("this.parent",this.parent)
        }
    
        static from(thing, parent) {
            let Node=require('./abc.js')
            let Leaf=require('./abcd.js')
            // 是否是对象,对象返回node 不是返回leaf
            if (thing && typeof thing === 'object') return new Node(parent, thing)
            else return new Leaf(parent, thing)
        }
    }
    
    module.exports = AbstractNode;
    //abc.js
    
    let AbstractNode = require("./ab.js");
    class Node extends AbstractNode {
        constructor(parent, thing) {
            super(parent)
            this.children = thing
    
        }
    
        print() {
            console.log("node",this.parent,this.children)
        }
    }
    module.exports = Node;
    //abcd.js
    let AbstractNode = require("./ab.js");
     class Leaf extends AbstractNode {
        constructor(parent, value) {
            super(parent)
            this.value = value
        }
    
        print() {
            console.log("leaf",this.parent,this.children)
            return this.value
        }
    }
    module.exports = Leaf;

性能

动态加载的特性使得难以进行静态优化。

兼容性和环境

浏览器支持: 需要使用打包工具(如Webpack或Browserify)来在浏览器中使用。

Node.js支持: Node.js原生支持CJS。 也可以通过明确指定.cjs扩展名或package.json中的"type": "commonjs"字段支持CJS。

缺点

由于CommonJS的加载方式是同步加载的,这意味着只有前面执行完成才会继续执行。因为同步就会存在一个问题,加载的速度受到影响。Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

AMD

异步模块定义(Asynchronous Module Definition)是一种用于JavaScript模块的规范,它允许模块和它们的依赖可以被异步加载。这在浏览器环境中特别有用,因为它可以提高页面加载性能,并允许按需加载模块。然而,随着ESM(ECMAScript模块)的普及和原生支持,AMD规范的使用频率有所下降。

基本用法

  • AMD使用define函数来定义模块。define接受模块名、依赖列表和一个定义模块的工厂方法。

    define('module', ['dep1', 'dep2'], function(dep1, dep2) {
      // 定义模块的内容
      return module;
    });
  • AMD允许输出的模块兼容CommonJS规范,这时define方法需要写成下面这样:

    define('module', function (require, exports, module){
      var someModule = require("someModule");
      var anotherModule = require("anotherModule");
    
      someModule.doTehAwesome();
      anotherModule.doMoarAwesome();
    
      exports.asplode = function (){
        someModule.doTehAwesome();
        anotherModule.doMoarAwesome();
      };
    });

特点

  • 异步加载:AMD规范的核心是支持异步加载模块,这意味着可以在不阻塞页面渲染的情况下加载JavaScript资源。

  • 按需加载: 可以在程序运行时按需加载模块,而不是在页面加载时一次性加载所有脚本。

  • 依赖前置: 模块的依赖在模块代码执行前被加载和解析。

  • 浏览器兼容: AMD是为了浏览器环境设计的,但他不是原生的规范,所以在浏览器中使用需要加载器。

加载器

  • RequireJS: 最著名的AMD实现。RequireJS提供了一个优化工具,用于将多个模块打包成一个文件,减少HTTP请求。

  • almond: RequireJS的一个精简版本,用于生产环境,在已知所有依赖项的情况下可以减少文件大小。

UMD

UMD(Universal Module Definition)是一种JavaScript模块的封装格式,它允许JavaScript模块在各种环境中运行,包括客户端(浏览器)和服务器端(如Node.js)。UMD实现了一种模式,能够兼容AMD和CommonJS模块定义方式,同时还支持传统的全局变量方式。这使得UMD成为跨平台共享代码的一种方式。然而,随着ESM(ECMAScript模块)的普及和原生支持,UMD规范的使用频率 也有所下降。

基本用法

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD环境
    define(['dependency'], factory);
  } else if (typeof exports === 'object') {
    // CommonJS环境
    module.exports = factory(require('dependency'));
  } else {
    // 浏览器全局变量环境
    root.returnExports = factory(root.dependency);
  }
}(this, function (dependency) {
  // 模块功能定义
  return {};
}));

特点

  • UMD模块可以在AMD和CommonJS环境中无缝工作,同时也支持老式的全局变量方法。

  • UMD模式通常在模块的顶部检测当前环境,根据环境不同采用不同的模块定义方式。

  • 当使用 Rollup/Webpack 之类的打包器时,UMD 通常用作备用模块。

  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值