在现代JavaScript开发中,模块化系统的演变经历了多次变革,使得前端和后端开发人员在选择模块加载方式时常常感到困惑。尤其是Node.js所采用的CommonJS和ESM(ECMAScript Modules)两种模块体系,以及文件扩展名的多样性(如.cjs
、.mjs
和.js
)带来的复杂性和混乱。Deno的诞生以及其2.0版本带来的模块化系统,正试图解决这些问题,并为开发者提供一种更统一、符合Web标准的模块化方案。
写这篇文章的原因就是,今天使用llamaindex,被其混乱的第三方模块折腾麻了,本来只是简单测试,后面不得不改了模块类型,都是因为cjs和ejs混用导致的,而且 cjs和mjs模块的导入需要加后缀!简直不可忍受
Node.js模块化系统的演变与挑战
Node.js最早采用的模块化方案是CommonJS,这是为了让开发者能够在Node环境中编写模块化代码而提出的方案。通过require()
方法,CommonJS实现了模块的导入与导出。然而,这种模块化方式是基于服务器端环境而设计的,与浏览器中的模块加载机制存在较大差异。
随着ES6(也称为ES2015)的发布,JavaScript正式引入了标准化的模块化机制——ESM(ECMAScript Modules)。ESM通过import
和export
语法,使得模块化变得更加规范化和易于理解。然而,由于Node.js在最初阶段只支持CommonJS,导致这两种模块化方案在Node.js中长期共存。
在Node.js中,当使用ESM模块时,需要使用文件扩展名.mjs
或者在package.json
中指定"type": "module"
,而CommonJS模块通常使用.cjs
扩展名或默认的.js
扩展名。这种双重模块系统的存在,给开发者带来了很多困扰。例如,如果试图在CommonJS模块中使用import
语法,或者在ESM模块中使用require()
,往往会遇到类似ERR_REQUIRE_ESM
的错误。
此外,Node.js对文件扩展名的处理也较为复杂,开发者需要记住何时需要显式指定文件扩展名,何时可以省略,这增加了代码的维护成本和理解难度。这种模块化系统的混乱现状,让很多开发者在使用第三方库、集成旧代码时遇到许多阻碍。
Node.js中的模块化代码示例
以下是Node.js中使用CommonJS和ESM的示例:
CommonJS示例(require()
方式):
// commonjs-module.js
module.exports = function() {console.log("Hello from CommonJS");
};
// main.js (CommonJS)
const greet = require("./commonjs-module");
greet();
CommonJS示例(导入导出对象):
// commonjs-object-module.cjs
const greetings = {greetEnglish: function() {console.log("Hello from CommonJS");},greetSpanish: function() {console.log("Hola desde CommonJS");}
};module.exports = greetings;
// main.cjs (CommonJS)
const greetings = require("./commonjs-object-module.cjs"); // 一定要加.cjs后缀
greetings.greetEnglish();
greetings.greetSpanish();
ESM示例(import
方式):
// esm-module.mjs
export function greet() {console.log("Hello from ESM");
}
// main.mjs
import { greet } from "./esm-module.mjs";
greet();
在Node.js中,必须根据模块类型选择适当的加载方式,否则可能会遇到各种错误。
Deno_74">Deno的统一模块化方案
为了解决这些痛点,Deno引入了一种更加现代化、统一的模块化系统,并且严格遵循Web标准。Deno最初的设计目标之一就是摆脱Node.js的历史包袱,避免其模块化系统中的种种复杂性。
Web标准的模块化方式
Deno采用了原生的ESM作为其模块系统,彻底抛弃了CommonJS,这意味着开发者在Deno中编写模块时,只需使用标准的import
和export
语法,无需担心CommonJS和ESM之间的兼容问题。这样一来,代码的可读性和可维护性得到了极大提升。
此外,Deno要求模块必须通过URL或相对路径来加载,并且所有的模块文件都需要明确指定扩展名(例如.js
、.ts
等)。这种设计与浏览器的模块加载方式保持了一致,使得在Deno中编写的代码可以很方便地移植到浏览器环境中,而无需进行额外的改动。
插一嘴,如果没有写扩展名,默认是js - 这时候可以绕过一些检测机制,例如针对文本的加密等
Deno还内置了对TypeScript的支持,开发者可以直接编写TypeScript代码,而不需要额外的编译步骤。这种内置支持使得TypeScript的使用更加自然,并且与ESM的模块化机制无缝集成。
Deno_88">Deno中的模块化代码示例
以下是Deno中使用ESM的示例:
使用ESM的Deno示例:
// greet.ts
export function greet() {console.log("Hello from Deno");
}
// main.ts
import { greet } from "./greet.ts";
greet();
在Deno中,模块的加载方式与浏览器类似,文件扩展名必须明确指定。这使得代码更加直观,也更符合开发者的预期。
模块管理的现代化
与Node.js依赖npm
来管理模块不同,Deno没有中央的包管理工具,而是采用URL来直接引入第三方模块。这种方式借鉴了Web的资源加载方式,使得模块的获取过程更加透明和简单,开发者可以直接通过URL查看模块的源代码。这种模块管理方式避免了传统包管理器中的“依赖地狱”,并且使得项目的依赖关系更加清晰。
以下是Deno中通过URL加载第三方模块的示例:
import { serve } from "https://deno.land/std@0.113.0/http/server.ts";const handler = (request: Request): Response => {return new Response("Hello from Deno server!", { status: 200 });
};serve(handler);
通过URL加载模块的方式,使得开发者可以清楚地看到模块的来源,并且减少了对包管理器的依赖。
此外,Deno 2.0引入了deno.json
配置文件,使得开发者可以更加灵活地配置项目,类似于Node.js中的package.json
,但更加简洁和统一。通过这种配置文件,开发者可以指定TypeScript编译选项、模块路径别名等,从而进一步提高开发体验。
Deno_129">Deno模块化方案的优势
-
统一的模块化标准:Deno彻底抛弃了CommonJS,只支持ESM模块,这使得模块化体系变得简单一致,开发者不再需要在CommonJS和ESM之间切换,也避免了常见的
ERR_REQUIRE_ESM
错误。 -
与Web兼容:Deno的模块加载方式与浏览器保持一致,使用URL或相对路径加载模块,并且必须显式指定文件扩展名。这种方式使得代码在Deno和浏览器之间的互操作性更强,减少了跨环境迁移的障碍。
-
内置TypeScript支持:Deno直接支持TypeScript,无需额外的配置或编译工具,这使得现代JavaScript开发更加方便,开发者可以充分利用TypeScript的类型系统来提高代码的可靠性。
-
模块管理简化:通过直接使用URL加载模块,Deno减少了对包管理器的依赖,避免了包版本冲突等问题,同时也让模块的来源变得更加透明。
总结
Node.js的模块化系统在历史上经历了从CommonJS到ESM的演变,但这种演变带来的兼容性问题和复杂的文件扩展名规则,给开发者带来了很多困扰。而Deno通过采用统一的ESM模块化方案,严格遵循Web标准,彻底解决了这些问题。Deno的2.0版本更是通过deno.json
等配置进一步提升了开发者的体验。
对于前端和全栈开发者来说,Deno提供了一种现代化、简洁而统一的开发体验,不再需要为模块化的复杂性而烦恼。它让开发者能够更加专注于代码本身,而不是被各种历史遗留的模块化问题所困扰。如果你还在为Node.js中的模块化混乱而烦恼,不妨尝试一下Deno,相信它会为你带来耳目一新的体验。