【工程化】模块化

相关文章

模块化


一、什么叫前端模块化

前端模块化是指将前端项目中的代码按照一定的规范和机制,拆分为多个独立、可复用的模块,并通过模块化的方式进行管理和调用。这样做的目的是为了提高代码的可维护性、可重用性和可扩展性,使前端开发更加高效和灵活。

在传统的前端开发中,代码通常以一个庞大的文件或多个零散的文件组成,不同功能的代码耦合在一起,难以维护和复用。而通过模块化开发,可以将功能逻辑划分为独立的模块,每个模块只关注特定的功能或职责,使得代码结构更加清晰、可管理。

二、前端模块化的演进过程

全局function模式

将不同的功能封装成不同的函数,缺陷是容易引发全局命名冲突。

1
2
3
4
5
6
7
8
function api(){
return {
data:{
a:1,
b:2
}
}
}

全局namespace模式

约定每个模块暴露一个全局对象,所有的模块成员暴露在对象下,缺点是外部能够修改模块内部数据。

1
2
3
4
5
6
7
8
9
10
var __Module={
api(){
return {
data:{
a:1,
b:2
}
}
}
}

IIFE(立即调用的函数表达式)模式

将模块成员放在函数私有作用域中,缺陷是无法解决模块间相互依赖问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(){
var a =1;
function getA(){
return a;
}
function setA(a){
window.__module.a = a;
}
window.__module = {
a,
getA,
setA
}
})();

IIFE增强模式

支持传入自定义依赖,缺陷是无法支持大规模的模块化开发,无特定语法支持,代码简陋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(global){
var a =1;
function getA(){
return a;
}
function setA(a){
window.__module.a = a;
}
global.__Module_API = {
a,
getA,
setA
}
})(window)

(function(global,moduleAPI){
global.__Module = {
setA:moduleApi.setA
}
})(window,window.__Module_API)

以上就是早期没有工具和规范的情况下对模块化的落地方式。

通过约定的方式去做模块化,不同的开发者和项目会有不同的差异,我们就需要一个标准去规范模块化的实现方式。针对与模块加载的方式,以上方法都是通过script标签手动引入的方式,模块加载不受代码的控制,时间久了维护会非常麻烦。那么就需要一个模块化的标准和自动加载模块的基础库。

三、前端模块化的主要特点

  • 模块化规范:前端模块化需要遵循一定的模块化规范,以确保模块之间的互相调用和依赖关系的正确性。常见的模块化规范包括CommonJS、AMD、UMD、ES6 Modules等。

  • 模块依赖管理:模块化开发中,模块之间存在依赖关系,需要进行有效的依赖管理,确保模块在运行时能够正确加载和执行。依赖管理可以通过工具(如Webpack、Parcel等)来自动处理。

  • 模块复用:模块化开发使得代码更易于复用,可以将通用的功能封装成模块,并在不同的项目中重复使用,提高开发效率和代码质量。

  • 模块化加载:模块化开发可以实现按需加载,只有在需要时才加载相应的模块,减少不必要的网络请求和资源占用,提高页面加载速度和性能。

  • 模块独立性:模块化开发中,每个模块都应该具有独立性,即模块之间相互独立、解耦合,可以独立开发、测试和维护,便于团队协作和项目的持续发展。

四、模块化规范

CommonJS、AMD、UMD、ES6 Modules 的区别

  • CommonJS:

    • 示例:Node.js 中的模块加载机制就是基于 CommonJS 规范的。
    • 特点:同步加载,适用于服务器端和非浏览器环境。
    • 语法:使用 require() 导入模块,使用 module.exports 导出模块。

    math.js文件

    1
    2
    3
    4
    // 导出一个加法函数
    exports.add = function(a, b) {
    return a + b;
    };

    app.js

    1
    2
    3
    4
    5
    6
    // 引入 math 模块
    var math = require('./math.js');

    // 使用 math 模块中的 add 函数
    var sum = math.add(3, 5);
    console.log(sum); // 输出 8

  • AMD(Asynchronous Module Definition):

    • 示例:RequireJS 是一个支持 AMD 规范的模块加载器。
    • 特点:异步加载,适用于浏览器端的异步加载模块。
    • 语法:使用 define() 定义模块,使用 require() 异步加载模块。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 定义一个 AMD 模块
    define('math', ['exports'], function(exports) {
    // 定义模块的内容
    exports.add = function(a, b) {
    return a + b;
    };

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

    // 使用 AMD 模块
    require(['math'], function(math) {
    console.log(math.add(2, 3)); // 输出:5
    console.log(math.subtract(5, 3)); // 输出:2
    });
  • UMD(Universal Module Definition):

    • 示例:UMD 是一种兼容多种模块规范的通用解决方案,可以在浏览器端和 Node.js 中使用。
    • 特点:支持多种模块规范,包括 CommonJS、AMD 和全局变量导出等。
    • 语法:使用通用的导出模式,兼容 CommonJS 和 AMD 规范。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    (function (root, factory) {
    if (typeof define === 'function' && define.amd) {
    // AMD 环境
    define(['exports'], factory);
    } else if (typeof exports === 'object' && typeof module === 'object') {
    // CommonJS 环境
    module.exports = factory(exports);
    } else {
    // 浏览器环境
    root.MyModule = factory({});
    }
    }(typeof self !== 'undefined' ? self : this, function (exports) {
    // 模块代码
    exports.add = function(a, b) {
    return a + b;
    };

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

    // 返回模块接口
    return exports;
    }));
  • ES6 Modules:

    • 示例:现代前端项目中普遍使用的 ES6 模块规范。
    • 特点:原生支持,语法简洁,支持静态分析,可以进行 tree shaking(摇树优化)。
    • 语法:使用 import 导入模块,使用 export 导出模块。

    math.js文件

    1
    2
    3
    4
    5
    6
    export function add(a, b) {
    return a + b;
    }
    export function subtract(a, b) {
    return a - b;
    }

    app.js

    1
    2
    3
    4
    import { add, subtract } from './math.js';

    console.log(add(5, 3)); // 输出 8
    console.log(subtract(5, 3)); // 输出 2

import 和 require 有什么区别

  • 语法差

    • import 是 ES6 中的语法,用于导入 ES6 模块。
    • require 是 CommonJS 中的语法,用于导入 CommonJS 模块。
  • 加载时机

    • import 在模块加载阶段就会执行,也称为静态导入,模块会在解析时加载并执行。
    • require 是动态加载,模块会在运行时加载并执行。
  • 作用域:

    • import 是块级作用域,只能在模块的顶层作用域中使用,不能在循环或条件语句中使用。
    • require 是函数级作用域,可以在任何位置使用。
  • 导出功能:

    • import 支持命名导入和默认导入。
    • require 通常使用 module.exports 或 exports 导出一个对象或值,不支持默认导入,但可以使用 require 函数的属性来获取默认导出。
  • 静态分析:

    • import 语句可以被静态分析,使得工具可以更好地理解和优化代码。
    • require 语句的调用是动态的,使得工具在分析时无法得知导入的模块。

exports 和 module.exports 有什么区别

  • exports

    • exports 实际上是 module.exports 的一个引用。
    • 初始时,exports 是一个空对象 {}。
    • 当你给 exports 添加属性或方法时,实际上是在修改 module.exports 的引用指向,将其指向新的对象,这样就可以导出多个成员。
    • 如果直接对 exports 进行赋值(如 exports = …),这将切断 exports 与 module.exports 的关系,这时应该使用 module.exports。
    1
    2
    3
    // example.js
    exports.foo = function() { ... };
    exports.bar = function() { ... };
  • module.exports

    • module.exports 是模块的实际导出对象。
    • 如果你想导出的是一个单独的值或者其他非对象类型的值,应该直接将其赋给 module.exports。
    • 如果你给 exports 添加了属性或方法,并且也想导出其他类型的值,可以使用 module.exports。
    1
    2
    // example.js
    module.exports = function() { ... };

CommonJS 和 ES6 Modules 修改导出的对象

  • 在 CommonJS 中,直接修改导出的对象会影响到其他模块对该对象的引用,因为它们引用的是同一个对象的引用。

  • 在 ES6 Modules 中,虽然可以直接修改导出的对象,但是修改后不会影响到其他模块对该对象的引用,因为 ES6 Modules 中的导入是值的绑定,不会共享模块的状态。


喜欢这篇文章?打赏一下支持一下作者吧!
【工程化】模块化
https://www.cccccl.com/20220504/工程化/模块化/
作者
Jeffrey
发布于
2022年5月4日
许可协议