前端模块化的前世今生(上)

简介: 前端模块化的前世今生(上)

大家好,我是 CUGGZ。

随着前端项目越来越大,代码复杂性不断增加,对于模块化的需求越来越大。模块化是工程化基础,只有将代码模块化,拆分为合理单元,才具备调度整合的能力。下面就来看看模块化的概念,以及不同模块化方案的使用方式和优缺点。


1. 模块概述


(1)概念

由于代码之间会发生大量交互,如果结构不合理,这些代码就会变得难以维护、难以测试、难以调试。而使用模块化就解决了这些问题,模块化的特点如下:

  • 可重用性: 当应用被组织成模块时,可以方便的在其他地方重用这些模块,避免编写重复代码,从而加快开发流程;
  • 可读性: 当应用变得越来越复杂时,如果在一个文件中编写所有功能,代码会变得难以阅读。如果使用模块设计应用,每个功能都分布在各自的模块中,代码就会更加清晰、易读;
  • 可维护性: 软件的美妙之处在于进化,从长远来看,我们需要不断为应用增加新的功能。当应用被结构化为模块时,可以轻松添加或删除功能。除此之外,修复错误也是软件维护的一部分,使用模块就可以更快速地定位问题。

模块化是一种将系统分离成独立功能部分的方法,可以将系统分割成独立的功能部分,严格定义模块接口,模块间具有透明性。通过将代码进行模块化分隔,每个文件彼此独立,开发者更容易开发和维护代码,模块之间又能够互相调用和通信,这就是现代化开发的基本模式。

(2)模式

JavaScript 模块包含三个部分:

  • 导入: 在使用模块时,需要将所需模块作为依赖项导入。例如,如果想要创建一个 React 组件,就需导入 react 模块。要使用像 Lodash 这样的工具库,就需要安装并导入它作为依赖项;
  • 代码: 模块具体代码;
  • 导出: 模块接口,从模块中导出的内容可供导入模块的任何地方使用。

(3)类型

模块化的贯彻执行离不开相应的约定,即规范。这是能够进行模块化工作的重中之重。实现模块化的规范有很多,比如:AMD、RequireJS、CMD、SeaJS、UMD、CommonJS、ES6 Module。除此之外,IIFE(立即执行函数)也是实现模块化的一种方案。

本文将介绍其中的六个:

  • IIFE: 立即调用函数表达式
  • AMD: 异步模块加载机制
  • CMD: 通用模块定义
  • UMD: 统一模块定义
  • CommonJS: Node.js 采用该规范
  • ES 模块: JavaScript 内置模块系统


2. IIFE


在 ECMAScript 6 之前,模块并没有被内置到 JavaScript 中,因为 JavaScript 最初是为小型浏览器脚本设计的。这种模块化的缺乏,导致在代码的不同部分使用了共享全局变量。

比如,对于以下代码:

var name = 'JavaScript';
var age = 20;

当上面的代码运行时,nameage 变量会被添加到全局对象中。因此,应用中的所有 JavaScript 脚本都可以访问全局变量 nameage,这就很容易导致代码错误,因为在其他不相关的单元中也可以访问和修改这些全局变量。除此之外,向全局对象添加变量会使全局命名空间变得混乱并增加了命名冲突的机会。

所以,我们就需要一种封装变量和函数的方法,并且只对外公开定义的接口。因此,为了实现模块化并避免使用全局变量,可以使用如下方式来创建模块:

javascript

复制代码

(function () {    // 声明私有变量和函数    return {        // 声明公共变量和函数    }})();

上面的代码就是一个返回对象的闭包,这就是我们常说的IIFE(Immediately Invoked Function Expression),即立即调用函数表达式。在该函数中,就创建了一个局部范围。这样就避免了使用全局变量(IIFE 是匿名函数),并且代码单元被封装和隔离。

可以这样来使用 IIFE 作为一个模块:

var module = (function(){
  var age = 20;
  var name = 'JavaScript'
  var fn1 = function(){
    console.log(name, age)
  };
  var fn2 = function(a, b){
    console.log(a + b)
  };
  return {
    age,
    fn1,
    fn2,
  };
})();
module.age;           // 20
module.fn1();         // JavaScript 20
module.fn2(128, 64);  // 192

在这段代码中,module 就是我们定义的一个模块,它里面定义了两个私有变量 agename,同时定义了两个方法 fn1fn2,其中 fn1 中使用 module 中定义的私有变量,fn2 接收外部传入参数。最后,module 向外部暴露了agefn1fn2。这样就形成了一个模块。

当试图在 module 外部直接调用fn1时,就会报错:

javascript

复制代码

fn1(); // Uncaught ReferenceError: fn1 is not defined

当试图在 module 外部打印其内部的私有变量name时,得到的结果是 undefined

javascript

复制代码

module.name; // undefined

上面的 IIFE 的例子是遵循模块模式的,具备其中的三部分,其中 age、name、fn1、fn2 就是模块内部的代码实现,返回的 age、fn1、fn2 就是导出的内容,即接口。调用 module 方法和变量就是导入使用。


3. CommonJS


(1)概念

① 定义

CommonJS 是社区提出的一种 JavaScript 模块化规范,它是为浏览器之外的 JavaScript 运行环境提供的模块规范,Node.js 就采用了这个规范。

注意:

  • 浏览器不支持使用 CommonJS 规范;
  • Node.js 不仅支持使用 CommonJS 来实现模块,还支持最新的  ES 模块。

CommonJS 规范加载模块是同步的,只有加载完成才能继续执行后面的操作。不过由于 Node.js 主要运行在服务端,而所需加载的模块文件一般保存在本地硬盘,所以加载比较快,而无需考虑使用异步的方式。

② 语法

CommonJS 规范规定每个文件就是一个模块,有独立的作用域,对于其他模块不可见,这样就不会污染全局作用域。在 CommonJS 中,可以分别使用 exportrequire 来导出和导入模块。在每个模块内部,都有一个 module 对象,表示当前模块。通过它来导出 API,它有以下属性:

  • exports:模块导出值。
  • filename:模块文件名,使用绝对路径;
  • id:模块识别符,通常是使用绝对路径的模块文件名;
  • loaded:布尔值,表示模块是否已经完成加载;
  • parent:对象,表示调用该模块的模块;
  • children:数组,表示该模块要用到的其他模块;

③ 特点

CommonJS 规范具有以下特点:

  • 文件即模块,文件内所有代码都运行在独立的作用域,因此不会污染全局空间;
  • 模块可以被多次引用、加载。第一次被加载时,会被缓存,之后都从缓存中直接读取结果。
  • 加载某个模块,就是引入该模块的 module.exports 属性,该属性输出的是值拷贝,一旦这个值被输出,模块内再发生变化不会影响到输出的值。
  • 模块加载顺序按照代码引入的顺序。

④ 优缺点

CommonJS 的优点:

  • 使用简单
  • 很多工具系统和包都是使用 CommonJS 构建的;
  • 在 Node.js 中使用,Node.js 是流行的 JavaScript 运行时环境。

CommonJS 的缺点

  • 可以在 JavaScript 文件中包含一个模块;
  • 如果想在 Web 浏览器中使用它,则需要额外的工具;
  • 本质上是同步的,在某些情况下不适合在 Web 浏览器中使用。

(2)使用

在 CommonJS 中,可以通过 require 函数来导入模块,它会读取、执行 JavaScript 文件,并返回该模块的 exports 对象,该对象只有在模块脚本运行完才会生成。

① 模块导出

可以通过以下两种方式来导出模块内容:

module.exports.TestModule = function() {
    console.log('exports');
}
exports.TestModule = function() {
    console.log('exports');
}

则合两种方式的导出结果是一样的,module.exportsexports的区别可以理解为:exportsmodule.exports的引用,如果在exports调用之前调用了exports=...,那么就无法再通过exports来导出模块内容,除非通过exports=module.exports重新设置exports的引用指向。

当然,可以先定义函数,再导出:

function testModule() {
    console.log('exports');
}
module.exports = testModule;

这是仅导出一个函数的情况,使用时就是这样的:

testModule = require('./MyModule');testModule();

如果是导出多个函数,就可以这样:

function testModule1() {
    console.log('exports1');
}
function testModule2() {
    console.log('exports2');
}

导入多个函数并使用:

({testModule1, testModule2} = require('./MyModule'));
testModule1();
testModule2();

② 模块导入

可以通过以下方式来导入模块:

constmodule = require('./MyModule');

注意,如果 require 的路径没有后缀,会自动按照.js.json.node的顺序进行补齐查找。

③ 加载过程

在  CommonJS 中,require 的加载过程如下:

  1. 优先从缓存中加载;
  2. 如果缓存中没有,检查是否是核心模块,如果是直接加载;
  3. 如果不是核心模块,检查是否是文件模块,解析路径,根据解析出的路径定位文件,然后执行并加载;
  4. 如果以上都不是,沿当前路径向上逐级递归,直到根目录的node_modules目录。

(3)示例

下面来看一个购物车的例子,主要功能是将商品添加到购物车,并计算购物车商品总价格:

// cart.js
var items = [];
function addItem (name, price) 
    item.push({
    name: name,
    price: price
  });
}
exports.total = function () {
    return items.reduce(function (a, b) {
      return a + b.price;
    }, 0);
};
exports.addItem = addItem;

这里通过两种方式在 exports 对象上定义了两个方法:addItem 和 total,分别用来添加购物车和计算总价。

下面在控制台测试一下上面定义的模块:

let cart = require('./cart');

这里使用相对路径来导入 cart 模块,打印 cart 模块,结果如下:

cart // { total: [Function], addItem: [Function: addItem] }

向购物车添加一些商品,并计算当前购物车商品的总价格:

cart.addItem('book', 60);
cart.total()  // 60
cart.addItem('pen', 6);
cart.total()  // 66

这就是创建模块的基本方法,我们可以创建一些方法,并且只公开希望其他文件使用的部分代码。该部分成为 API,即应用程序接口。

这里有一个问题,只有一个购物车,即只有一个模块实例。下面来在控制台执行以下代码:

second_cart = require('./cart');

那这时会创建一个新的购物车吗?事实并非如此,打印当前购物车的商品总金额,它仍然是66:

second_cart.total();  // 66

当我们㤇创建多个实例时,就需要再模块内创建一个构造函数,下面来重写 cart.js 文件:

// cart.js
function Cart () {
    this.items = [];
}
Cart.prototype.addItem = function (name, price) {
    this.items.push({
        name: name,
        price: price
    });
}
Cart.prototype.total = function () {
    return this.items.reduce(function(a, b) {
        return a + b.price;
    }, 0);
};
module.export = Cart;

现在,当需要使用此模块时,返回的是 Cart 构造函数,而不是具有 cart 函数作为一个属性的对象。下面来导入这个模块,并创建两个购物车实例:


Cart = require('./second_cart');
cart1 = new Cart();
cart2 = new Cart();
cart1.addItem('book', 50);
cart1.total();   // 50
cart2.total();   // 50


前端模块化的前世今生(下)https://developer.aliyun.com/article/1411382

相关文章
|
1月前
|
资源调度 前端开发 JavaScript
构建高效前端项目:现代包管理器与模块化的深度解析
【2月更文挑战第21天】 在当今快速演变的前端开发领域,高效的项目管理和代码组织已成为成功交付复杂Web应用的关键。本文将深入探讨现代前端包管理器如npm, yarn和pnpm的工作原理,以及它们如何与模块化编程实践(例如CommonJS、ES6模块)协同工作以优化开发流程。我们将剖析这些工具的内部机制,了解它们如何解决依赖冲突,提高安装速度,并保证项目的健壮性。同时,本文还将介绍模块化编程的最佳实践,包括代码拆分、重用和版本控制,帮助开发者构建可维护且性能卓越的前端项目。
|
1月前
|
前端开发 JavaScript jenkins
构建高效前端项目:从模块化到自动化
【2月更文挑战第13天】 随着Web技术的不断进步,前端项目的复杂性日益增加。为了确保可维护性和性能,前端工程师必须采用模块化和自动化的策略来优化开发流程。本文将探讨如何使用现代前端工具和最佳实践来构建一个高效的前端项目架构,包括模块打包、代码分割和持续集成等方面。
|
1天前
|
JavaScript 前端开发
必知的技术知识:esm前端模块化
必知的技术知识:esm前端模块化
|
1月前
|
前端开发 JavaScript 安全
【Web 前端】怎么实现Module模块化?
【5月更文挑战第1天】【Web 前端】怎么实现Module模块化?
|
1月前
|
JavaScript 算法 前端开发
【专栏】前端开发中的slot算法和shadow DOM,两者提供更灵活、高效和模块化的开发方式
【4月更文挑战第29天】本文探讨了前端开发中的slot算法和shadow DOM,两者提供更灵活、高效和模块化的开发方式。slot算法允许在组件中定义插槽位置,实现内容的灵活插入和复用,提高代码可读性和维护性。shadow DOM则通过封装DOM子树,实现样式和事件的隔离,增强组件独立性和安全性。这两种技术常应用于组件开发、页面布局和主题定制,但也面临兼容性、学习曲线和性能优化等挑战。理解并掌握它们能提升开发效率和用户体验。
|
1月前
|
前端开发 JavaScript 安全
前端模块化发展
前端模块化发展
|
1月前
|
JavaScript 前端开发 开发者
【Web 前端】JS模块化有哪些?
【4月更文挑战第22天】【Web 前端】JS模块化有哪些?
|
1月前
|
前端开发 JavaScript 内存技术
Node-前端模块化
Node-前端模块化
29 0
Node-前端模块化
|
1月前
|
前端开发 JavaScript 测试技术
构建高效前端项目:模块化与组件化策略
【2月更文挑战第25天】 在现代网页开发中,随着用户对于网页性能和交互体验的期待不断提升,前端项目的复杂性也随之增加。为了应对这种复杂性并提高开发效率,本文将探讨模块化与组件化在前端项目中的应用策略。通过分析这两种方法的优势与适用场景,我们将揭示如何利用它们来优化项目结构,提升代码复用率,以及加快开发流程。
64 4
|
1月前
|
资源调度 前端开发 JavaScript
构建高效前端项目:模块化与组件化的最佳实践
【2月更文挑战第13天】在现代前端开发的浪潮中,模块化和组件化已经成为提升项目可维护性和开发效率的核心原则。本文深入探讨了如何通过合理的模块划分、组件设计以及工具选择来优化前端项目结构,同时确保代码的复用性和可测试性。我们将从理论出发,结合实例分析,为前端开发者提供一套行之有效的最佳实践指南。
133 0