1.7. js 模块

232 阅读7分钟

概念

js本身只是个轻量级的解释型语言,对code代码进行编译解析的过程;

  • 在node运行,一些操作系统的api都是通过调用底层c++,它的模块化commonjs规范,本质是通过获取内置的对象或者动态读取磁盘上的文件来加载,详见

  • 在浏览器里面,是通过<script></script>标签加载,我们知道html解析时遇到script标签,会把里面的代码交给js引擎处理。浏览器里面的模块一般挂在全局变量上。

  • 浏览器里面模块标准:
    AMD规范:Asynchronous Module Definition “异步模块定义”
    CMD规范:common moudle definition通用模块定义
    ES6规范:es6推出的浏览器端模块管理规范。
    UMD:统一模块定义(UMD:Universal Module Definition )
    就是将 AMD 和 CommonJS 合在一起的一种尝试,常见的做法是将CommonJS 语法包裹在兼容 AMD 的代码中

(function(define) {
    define(function () {
        return {
            sayHello: function () {
                console.log('hello');
            }
        };
    });
}(
    typeof module === 'object' && module.exports && typeof define !== 'function' ?
    function (factory) { module.exports = factory(); } :
    define
));

1、早期的浏览器端模块化

1.1AMD规范

所有的模块将被异步加载,模块加载不影响后面语句运行。所有依赖某些模块的语句均放置在回调函数中
main.js 作为整个应用的入口模块,我们使用 define 关键字声明了该模块以及外部依赖(没有生命模块名称);当我们执行该模块代码时,也就是执行 define 函数的第二个参数中定义的函数功能,其会在框架将所有的其他依赖模块加载完毕后被执行。这种延迟代码执行的技术也就保证了依赖的并发加载

// file lib/sayModule.js
define(function (){
    return {
        sayHello: function () {
            console.log('hello');
        }
    };
});
 
//file main.js
define(['./lib/sayModule'], function (say){
    say.sayHello(); //hello
})

RequireJS 遵循AMD规范

//scripts下的main.js则是指定的主代码脚本文件,所有的依赖模块代码文件都将从该文件开始异步加载进入执行。
<script data-main='scripts/main' src='scripts/require.js'></script>
define([ 'moduleOne', 'moduleTwo' ], function(mOne, mTwo){
    ...
});
//或者
define( function( require ){
    var mOne = require( 'moduleOne' ),
        mTwo = require( 'moduleTwo' );
    ...
});

1.2CMD规范

define'module', ['module1''module2'], function( requireexportsmodule ){

  } );

SeaJS

seajs.use(['./a','./b'],function(a , b){
    a.doSomething();
    b.doSomething();
});

1.3 AMD vs CMD

  • RequireJS在主文件里是将所有的文件同时加载,然而SeaJS强调一个文件一个模块加载;
  • AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块;CMD推崇就近依赖,只有在用到某个模块的时候再去require
  • AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
// CMD
 define(function(requireexportsmodule) {
   var a = require('./a')
   a.doSomething()
   // 此处略去 100 行
   var b = require('./b'// 依赖可以就近书写
   b.doSomething()
    // ...
  })

// AMD 默认推荐的是
define(['./a''./b'], function(a, b) { // 依赖必须一开始就写好
   a.doSomething()
   // 此处略去 100 行
    b.doSomething()
   // ...
})

1.4 require.js 库的原理

  1. 在运行中动态插入sciprt节点,并且 加载 + 运行 其js文件
// 建一个node节点, script标签
var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '3.js'
 
// 将script节点插入dom中
document.body.appendChild(node)
  1. 前面模块加载完并且执行完之后,执行onload加载后面依赖
 
var node = document.createElement('script')
node.type = 'text/javascript'
node.src = '1.js'
 
// 给该节点添加onload事件,标签上onload,这里是load,见事件那里的知识点
// 1.js 加载完后onload的事件
node.addEventListener('load', function(evt) {
    // 开始加载 2.js
    var node2 = document.createElement('script')
    node2.type = 'text/javascript'
    node2.src = '2.js'
    // 插入 2.js script 节点
    document.body.appendChild(node2)
})
// 插入 1.js script 节点
document.body.appendChild(node)
  1. 模块的运行 js文件加载后就会瞬间执行文件,即执行 require() 函数的回调

原理详见

2、commonjs 与 ESM

2.1 commonjs

  • 提出时间:CommonJS 最初由 Kevin Dangoor2009 年提出(当时称为 ServerJS)。

  • 背景

    • JavaScript 最初是为浏览器设计的,没有内建模块系统
    • 前端使用 <script> 标签手动管理依赖,极为不便。
    • CommonJS 的目标是为 JavaScript 在服务端(如 Node.js) 提供一种模块化标准,提升组织与复用能力。

📌 Node.js 是 CommonJS 的主要实现者和推动者,也是最成功的应用场景。

使用方式(语法)

  • 导入模块(require

const fs = require('fs'); // 内置模块
const myModule = require('./myModule'); // 自定义模块(相对路径)

require() 是一个同步函数,立即返回模块的导出内容。 会自动缓存模块,避免重复加载。

  • 导出模块(module.exportsexports
// myModule.js
const name = 'Alice';
function greet() {
  console.log(`Hello, ${name}`);
}

module.exports = {
  name,
  greet
};

或简写:

// exports 是 module.exports 的别名
exports.sayHi = () => {
  console.log('Hi!');
};

⚠️ 注意:不能同时赋值 module.exportsexports,否则会失效。

模块加载机制

Node.js 使用 文件路径定位模块,并遵循以下规则:

1.  `.js`, `.json`, `.node` 后缀按顺序尝试。
1.  若导入文件夹,尝试读取其中的 `index.js`1.  查找顺序遵循 Node 的模块查找规则(例如从 `node_modules` 开始向上查找)。

模块是单例且有缓存

// a.js
console.log('Module loaded');
module.exports = { count: 1 };

// b.js
const a1 = require('./a'); // 执行 a.js
const a2 = require('./a'); // 直接用缓存,不会再执行

2.2 ESM

模块系统是 ES6 的一大亮点,官方标准化模块化机制

用法

  • 导出模块(export)
// math.js
export const PI = 3.14;
export function add(x, y) {
  return x + y;
}

// 或者使用默认导出
export default function () {
  console.log('Default export');
}
  • 导入模块(import)
import { PI, add } from './math.js';
import myFunc from './math.js'; // 默认导入

✅ 模块是 静态加载(编译时就确定依赖关系),更适合现代构建优化(如 Tree Shaking)。

2.3 commonjs vs ESM

CommonJS和ES Modules(ESM)是JavaScript中两种不同的模块系统,它们有以下区别:

  • 运行环境
特性CommonJSES Modules
environmentnode浏览器、node12+
  • 语法

• CommonJS:使用require()函数来引入模块,使用module.exports或exports来导出模块中的内容。例如:const moduleA = require('./moduleA');。

• ES Modules:使用import和export关键字来进行模块的导入和导出。例如:import { functionA } from './moduleA.js';

• 只能在模块顶部导入

• 支持动态导入 const module = await import()

  • 执行时机

• CommonJS:是运行时加载,在代码运行到require语句时才会去加载模块,这意味着可以在代码中根据条件动态地加载不同的模块。

• ES Modules:是编译时加载,在代码编译阶段就确定了模块的依赖关系,import语句必须位于模块的顶层,不能在块级作用域或函数内部使用。

  • 模块加载方式

• CommonJS:模块加载是同步的,在服务器端环境中因为文件系统读取速度快,影响不大,但在浏览器环境中可能会导致阻塞页面渲染。

• ES Modules:
在浏览器环境中,import加载模块是异步的,不会阻塞页面的解析和渲染。
在新版node中,import加载模块也是异步的,不会阻塞主线程。

特性CommonJSES Modules
导出绑定(binding)值拷贝(值是快照)引用绑定(live binding
  • 模块引用

• CommonJS:模块被加载后,require返回的是一个对象,该对象是模块的缓存副本,后续对模块内部变量的修改不会反映到已经加载的模块中。

• ES Modules:import导入的是模块的实时引用,当模块内部的变量发生变化时,引用该变量的地方也会得到最新的值。

特性CommonJSES Modules
导出绑定(binding)值拷贝(值是快照)引用绑定(live binding
  • 适用场景

• CommonJS:在Node.js环境中广泛使用,适合服务器端应用,因为它的同步加载方式和对动态加载的支持很适合服务器端的文件系统操作和路由等场景。

• ES Modules:更适合浏览器环境,其异步加载特性可以提高页面性能,同时在现代的JavaScript项目中,无论是前端还是后端,也越来越多地使用ES Modules来进行模块管理。

  • 在Node.js中,ES Modules的缓存机制与CommonJS有所不同。
特性CommonJSES Modules
缓存时机执行完模块后立即缓存在模块加载过程中就缓存
循环依赖行为模块在执行完成后才导出,如果有循环依赖,可能会导出一个不完整的对象在解析期间就设置好导出变量的引用,所以在循环依赖中更健壮

在 ESM 中,在模块仍在解析依赖时(还未执行完),就已经缓存并暴露其接口了。 这意味着:即使模块还未完全执行完,其 export 就可以被其他模块访问(可能是 undefined,如果尚未赋值)

总结

特性CommonJSES Modules (ESM)
提出时间20092015(ES6)
浏览器支持❌ 需打包工具✅ 原生支持(现代浏览器)
environmentnode浏览器、node12+
导入方式require()import
导出方式module.exportsexport / export default
执行时机运行时加载编译时加载
加载方式同步异步
导出绑定(binding)值拷贝(值是快照)引用绑定(live binding

欢迎关注我的前端自检清单,我和你一起成长