了解 Javascript 模块化,更好地掌握 Vite 、Webpack、Rollup 等打包工具

JS 模块化 本质是将复杂的 JS 代码按照功能、职责拆分成独立的文件(模块),每个模块只暴露需要对外提供的接口,同时隔离内部实现,解决全局变量污染、代码复用、依赖管理等问题。

CommonJS (Node)

CommonJS 是一套 Node.js 中默认的模块化规范,也是前端模块化发展中重要的一环,主要用于解决代码的模块化组织和依赖管理问题。

CommonJS 的特点

  1. 同步加载require() 是同步执行的,会阻塞后续代码,直到模块加载完成。

    适合 Node.js 环境(模块存于本地磁盘,加载速度快),但不适合浏览器(网络加载慢,会阻塞页面渲染)。

  2. 运行时加载 :模块的导入和导出在代码运行时执行,属于 "动态加载"。

    例如,require() 可以写在条件语句中,根据运行时条件动态加载模块。

  3. 值拷贝:导入的是模块导出值的 "拷贝",若原模块后续修改了导出的基本类型值,导入方不会同步更新(引用类型除外,因拷贝的是引用地址)。

语法 module.exports

module.exports:模块的默认导出对象,本质是一个空对象 {},模块最终导出的内容以 module.exports 为准。

js 复制代码
function add(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;
js 复制代码
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

// 导出 add 和 subtract 函数
module.exports = {
  add,
  subtract
};

导出单个成员 :若模块只需要导出一个核心功能,可直接给 module.exports 赋值(非对象)。

js 复制代码
// utils.js
module.exports = (str) => str.toUpperCase(); // 导出一个函数

语法 exports

exports:是 module.exports 的引用(快捷方式),初始时 exports === module.exports

注意:不能直接给 exports 赋值新对象 (会断开与 module.exports 的引用关系)。

js 复制代码
function add(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

exports.add = add;
exports.sub = sub;
exports.multiply = (a, b) => a * b;

语法 require

js 复制代码
console.log("require", require);

require 是一个函数 ,且是 CommonJS 模块系统中最核心的函数 ------ 它是 Node.js 内置的、挂载在每个模块的 module.require 上的函数,用于同步加载并执行其他模块,返回目标模块的 module.exports 对象。

但要注意:require 不是普通函数,它是「函数对象」(兼具函数和对象的特性),还挂载了多个实用属性(如 require.resolverequire.cache),是 Node.js 为 CommonJS 模块定制的特殊函数。

js 复制代码
console.log('module.require', module.require)

在 Node.js 中直接打印 module 对象时,控制台输出里看不到 require 函数,但却能通过 module.require 访问到这个函数 ------ 核心原因是 requiremodule 对象的不可枚举属性 ,默认不会被 console.log 打印出来,但实际存在且可调用。

require 是 Node.js 为开发者封装的 "易用版" 模块加载函数,而 module.require 是更接近底层的 "核心版" 加载函数 ------ 前者挂载了大量辅助开发的属性,后者仅保留最核心的全局状态

require.resolve

require.resolve 方法用于解析模块标识符对应的绝对路径(只解析路径,不加载模块)。

js 复制代码
console.log(require.resolve('./utils.js')) 
// /Users/xxx/Documents/code/cloudcode/blog/模块化/utils.js
console.log(require.resolve('fs')) // fs
console.log(require.resolve('axios')) 
// /Users/xxx/Documents/code/cloudcode/blog/模块化/node_modules/axios/dist/node/axios.cjs

require.resolve.paths

require.resolve.paths 函数用于返回 Node.js 查找模块时的所有候选路径(即「模块查找路径数组」)。

js 复制代码
console.log(require.resolve.paths('./utils.js')) 
console.log(require.resolve.paths('fs')) 
console.log(require.resolve.paths('axios')) 

require.main

require.main 指向启动当前 Node.js 进程的主模块 (即你执行 node index.js 时的 index.js)。

require.extensions

require.extensions 是 Node.js 用于处理不同后缀模块的加载器映射,键是文件后缀,值是对应的加载函数。

该属性已被 Node.js 废弃(不推荐自定义),但仍保留用于兼容旧代码。

require.cache

require.cache 是 Node.js 模块系统的缓存容器(以模块绝对路径为键,模块对象为值),用于缓存已加载的模块,避免重复加载 / 执行。

模块缓存是 CommonJS 「加载一次,多次复用」的核心(多次 require 同一个模块,只会执行一次模块代码);可通过 delete require.cache[模块路径] 清除指定模块的缓存(常用于热更新场景)。

内置局部对象 module

js 复制代码
console.log("module", module);

module 是每个 CommonJS 模块的「内置局部对象」(非全局),console.log(module) 打印的内容完整展示了当前模块的元信息、运行状态、依赖关系

  • id,模块的唯一标识。主模块(直接运行的模块)id 为.;被导入的模块 id 等于 filename
  • path,模块所在的目录路径(不含文件名)
  • exports,模块的导出对象(核心)。
  • loaded,判断模块是否加载 / 执行完成。
  • filename,模块的完整绝对路径(包含文件名)
  • children,当前模块加载的子模块列表(每个子项都是 Module 实例)
  • paths,模块查找第三方依赖的路径列表,Node.js 会按这个顺序查找 node_modules 目录(从当前目录向上递归)。
js 复制代码
const utils = require("./utils.js");
const  fs = require('fs');
console.log("utils.add", utils.add(1, 2));

fs.readFile('./main.js', 'utf-8', (err, data) => {
  if (err) {
    console.log(err);
    return;
  }
  console.log('main.js content ended');
});
module.exports = {
  add: utils.add,
};
console.log("module", module);
console.log("require", require);
js 复制代码
// utils.js
function add(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;

console.log("utils loading");

ES Module (浏览器、Node)

ES Module(简称 ESM,ES6 模块)是 ECMAScript 2015(ES6)引入的官方官方模块化规范,旨在统一浏览器和 Node.js 的模块化方案。

它通过静态分析(编译时解析依赖)实现更高效的模块管理,支持树摇(Tree-shaking)循环依赖处理等高级特性,现已成为现代前端开发的主流模块化标准。

相比于早期制定的 CommonJS 规范,ES6的模块化设计有 3 点不同。

  • CommonJS 在运行时完成模块的加载,而 ES6 模块是在编译时完成模块的加载,效率要更高。
  • CommonJS 模块是对象,而 ES6 模块可以是任何数据类型,通过 export 命令指定输出的内容,并通过import命令引入即可。
  • CommonJS 模块会在 require 加载时完成执行,而 ES6 的模块是动态引用,只在执行时获取模块中的值。ES6 模块核心的内容在于 export 命令和 import 命令的使用,两者相辅相成,共同为模块化服务。

语法 exports

1、export 的是接口,而不是值

不能直接通过 export 输出变量值,而是需要对外提供接口,必须与模块内部的变量建立一一对应的关系。

js 复制代码
let obj = {};
let a = 1;
function foo() {}

export obj;  // 错误写法
export a; // 错误写法
export foo; // 错误写法
js 复制代码
let obj = {};
function foo() {}

export let a = 1; // 正确写法
export { obj }; // 正确写法
export { foo }; // 正确写法

2、export 值的实时性

export 对外输出的接口,在外部模块引用时,是实时获取的,并不是 import 那个时刻的值。假如在文件中 export 一个变量,然后通过定时器修改这个变量的值,那么在其他文件中不同时刻使用 import 的变量,值也会不同。

js 复制代码
// 导出文件export1.js
const name = 'kingx2';
// 一秒后修改变量name的值
setTimeout(() => name = 'kingx3', 1000);
export { name };

// 导入文件import1.js
import { name } from './export1.js';
console.log(name); // kingx2
setTimeout(() => {
  console.log(name); // 'kingx3'
}, 1000);

3、使用 as 关键字设置别名如果不想对外暴露内部变量的真实名称,可以使用 as 关键字设置别名,同一个属性可以设置多个别名。

js 复制代码
const _name = 'kingx';
export {_name as name};
export {_name as name2};

4、相同变量名只能够 export 一次

在同一个文件中,同一个变量名只能够 export 一次,否则会抛出异常。

js 复制代码
const _name = 'kingx';
const name = 'kingx';

export { _name as name };
export { name }; // 抛出异常,name作为对外输出的变量,只能export一次

5、尽量统一 export

如果文件 export 的内容有很多,建议都放在文件末尾处统一进行export,这样对export的内容能一目了然。

js 复制代码
const name = 'kingx';
const age = 12;
const sayHello = function () {
    console.log('hello');
};

export {
    name, 
    age, 
    sayHello
};

语法 export default

使用 import 引入的变量名需要和 export 导出的变量名一样。在某些情况下,我们希望不设置变量名也能供 import 使用,import 的变量名由使用方自定义,这时就要使用到export default命令了。

注意:一个文件只有一个 export default 语句。import 的内容不需要使用大括号括起来。

js 复制代码
// export.js
const defaultParam = 1;

export default defaultParam;

// import.js
import param from './export.js';
console.log(param); // 1

语法 import

1、与 export 的变量名相同

import 命令引入的变量需要放在一个大括号里,括成对象的形式,而且 import 的变量名必须与 export 的变量名一致。

js 复制代码
// export.js
const _name = 'kingx';
export { _name as name };

// import.js
import { _name } from './export.js'; // 抛出异常
import { name } from './export.js'; // 引入正常

2、相同变量名的值只能 import 一次

相同变量名的值只能 import 一次,否则会抛出异常。假如从多个不同的模块中 import 进相同的变量名,则会抛出异常。

js 复制代码
// export1.js
export const name = 'kingx';

// export2.js
export const name = 'cat';

// 同时从两个模块中引入name变量,会抛出异常。
import {name} from './export1.js';
import {name} from './export2.js'; // 抛出异常

3、import 命令具有提升的效果

import 命令具有提升的效果,会将 import 的内容提升到文件头部。

js 复制代码
// export.js
export const name = 'kingx';

// import.js
console.log(name);  // kingx
import {name} from './export.js';

在上面的代码中,import 语句出现在输出语句的后面,但是仍然能正常输出。本质上是因为 import 是在编译期运行的,在执行输出代码之前已经执行了 import 语句。

4、多次 import 时,只会一次加载

每个模块只加载一次,每个JS文件只执行一次,如果在同一个文件中多次 import 相同的模块,则只会执行一次模块文件,后续直接从内存读取。

js 复制代码
// export.js
console.log('开始执行');
export const name = 'kingx';
export const age = 12;

// import.js
import {name} from './export.js';
import {age} from './export.js';

在上面的代码中,import 两次 export.js 文件,但是最终只输出了一次"开始执行"。

5、import 的值本身是只读的,不可修改

使用 import 命令导入的值,如果是基本数据类型,那么它们的值是不可以修改的,相当于一个 const 常量;如果是引用数据类型的值,那么它们的引用本身是不能修改的,只能修改引用对应的值本身。

js 复制代码
// export.js
const obj = {
    name: 'kingx5'
};
const age = 15;

export {obj, age};

// import.js
import {obj, age} from './export.js';

obj.name = 'kingx6'; // 修改引用指向的值,正常
obj = {}; // 抛出异常,不可修改引用指向
age = 15; // 抛出异常,不可修改值本身

6、设置引入变量的别名

js 复制代码
// export1.js
export const name = 'kingx';

// export2.js
export const name = 'cat';

// 使用as关键字设置两个不同的别名,解决了问题
import {name as personName} from './export1.js';
import {name as animalName} from './export2.js';

7、模块整体加载

当我们需要加载整个模块的内容时,可以使用星号(*)配合 as 关键字指定一个对象,通过对象去访问各个输出值。

js 复制代码
// export.js
const obj = {
    name: 'kingx'
};

export const a = 1;
export { obj };

// import.js
import * as a from './export.js';

语法 Dynamic Import 动态导入

js 复制代码
import(moduleName)
import(moduleName, options)

第一中 ES Module 环境

js 复制代码
// main.js
function add(a, b) {
  return a + b;
}
console.log("main. loading");

const arr = [1, 2, 3];
function getName(arg) {
  arr.push(...arg);
}

export { add, arr, getName };
js 复制代码
import("./main.js")
  .then((module) => {
    console.log("module", module);
  })
  .catch((err) => {
    console.log("error", err);
  });
js 复制代码
// utils.js
function add(a, b) {
  return a + b;
}
console.log("main. loading");

const arr = [1, 2, 3];
function getName(arg) {
  arr.push(...arg);
}
export { add, arr, getName };

export default {
  getFullName: (firstName, lastName) => `${firstName} ${lastName}`,
}

第二种 CommonJS 环境

js 复制代码
// utils.js
function add(a, b) {
  return a + b;
}

function sub(a, b) {
  return a - b;
}

module.exports.add = add;
module.exports.sub = sub;
module.exports.multiply = (a, b) => a * b;

console.log("utils loading");
js 复制代码
import("./utils.js")
  .then((module) => {
    console.log("module", module);
  })
  .catch((err) => {
    console.log("error", err);
  });

语法 import.meta

import.meta 是一个只读的全局对象 ,存在于每个 ES Module 模块的顶层作用域中。仅在 ES Module 环境中可用(CommonJS 中打印 import.meta 会直接报错)。

js 复制代码
console.log('import.meta', import.meta);

import.meta.url 返回当前模块的文件 URL 路径import.meta.resolve异步解析模块路径(替代 require.resolve),返回 Promise。

import attributes 导入属性

import() 导入属性(Import Attributes)是 ES2025(ESNext)的核心新特性,也被称为「Import Assertions 2.0」(导入断言的升级版)。

必须在 ES Module 环境中使用。

Node.js 从 v20.6.0 开始正式支持 ES2025 导入属性(需确保 Node.js 版本 ≥20.6.0)。

导入属性(Import Attributes)功能用于告知运行时应如何加载模块,包括模块解析、获取、解析与执行的行为。它在 import 声明、export...from 声明以及动态导入 import () 中均受支持。

一、静态 import ... with { ... }

静态 import ... with { ... } 中,只能写 type 属性 ,其他属性一律抛错。 type 只支持 type: "json"type: "webassembly"

js 复制代码
import data from "./config.json" with {
  type: "json", // 标准属性:指定模块类型为 JSON
};

二、动态 import

js 复制代码
async function loadJSON() {
  const json = await import("./config.json", { with: { type: "json" } });
  console.log("loadJSON", json.default);
}
loadJSON();

三、export...from 声明

js 复制代码
// ✅ 合法:导出 JSON 模块的默认导出,仅声明 type: "json"
// 核心:JSON 模块必须加 with { type: "json" }
export { default as config } from "./config.json" with { type: "json" };

// 也可导出所有内容(命名空间导出)
export * from "./config.json" with { type: "json" };
相关推荐
Heo2 小时前
深入 React19 Diff 算法
前端·javascript·面试
滕青山2 小时前
个人所得税计算器 在线工具核心JS实现
前端·javascript·vue.js
小怪点点2 小时前
手写promise
前端·promise
国思RDIF框架2 小时前
RDIFramework.NET Web 敏捷开发框架 V6.3 发布 (.NET8+、Framework 双引擎)
前端
Mintopia2 小时前
如何在有限的时间里,活出几倍的人生
前端
炫饭第一名2 小时前
速通Canvas指北🦮——变形、渐变与阴影篇
前端·javascript·程序员
Neptune12 小时前
让我带你迅速吃透React组件通信:从入门到精通(上篇)
前端·javascript
阿懂在掘金2 小时前
Vue 表单避坑(一):为什么 v-model 绑定对象属性会偷偷修改父组件数据?
前端·vue.js
小码哥_常2 小时前
Android与JS交互:解锁混合开发的魔法之门
前端