likes
comments
collection
share

一文梳理前端模块化方案

作者站长头像
站长
· 阅读数 22

模块化方案汇总

写在前面

由于官方ECMAScript规范化卷时间比较早,他所规范的范畴较小,随着JavaScript的不断壮大,他的规范相对于其他语言来说就显得比较薄弱,ES6之前他有以下的一些缺陷:

  • 没有模块系统
  • 标准库较少
  • 没有标准的接口
  • 缺乏包管理系统

在ES6之前,JavaScript并没有统一的模块规范,但是JavaScript社区对其进行了探索,提出了AMD、UMD等模块规范 合格的模块化应该拥有以下特性:

  • 独立性:独立完成功能,与外界环境隔离
  • 完整性:能完成某个特定功能
  • 可依赖:能够依赖其他模块
  • 可被依赖:能被其他模块依赖

好在这么多年的发展,官方也逐渐在解决这些问题,但是在实际学习中,发现各种各样的模块规范让人容易搞混,我们来梳理下到目前为止JavaScript常见的模块规范吧,这也为后面学习规范化开发开源库打下基础。

原始模块

从定义上来说,一个函数即为一个模块,下面为JavaScript社区中原始模块代码

(function (mod,$){
    function clone(){
       //...
    }
    mod.clone = clone
})((window.clone = window.clone || {}), jquery)

这段代码是一个立即执行函数表达式(Immediately Invoked Function Expression,IIFE),用于定义一个模块(module)并将其中的 clone 函数添加到全局对象 window 中。

  1. (function (mod, $) { ... }): 这是一个匿名函数,接受两个参数 mod$。这个函数尚未被调用,只是定义了它。

  2. window.clone = window.clone || {}: 这是一个表达式,用于确保 window.clone 对象已存在,如果不存在则创建一个空对象 {}。这个表达式的结果将作为第一个参数 mod 传递给上述匿名函数。

  3. jquery:这是第二个参数,它用于传递 jQuery 库或对象给匿名函数,以便在函数内使用 $ 符号来表示 jQuery。

  4. 在匿名函数内部,定义了一个名为 clone 的函数。

  5. 最后,mod.clone = cloneclone 函数添加到 mod 对象中,通常是一个模块或命名空间对象。在这种情况下,mod 被设置为 window.clone,所以 window.clone 现在拥有一个名为 clone 的函数。

这个代码的目的是将一个 clone 函数添加到全局命名空间中,使得其他部分的代码可以访问并使用这个函数。此外,确保在使用之前 jQuery 库已正确引入。

很明显这种模式下的致命缺点:

  • jquery必须先于代码被引用,不然会报错,对于大型项目来说不可维护

一般库都会提供对这种模块的支持,因为这种模块可以直接通过script标签引用。

AMD(Asynchronous Module Definition)模块

该模块的中文名称是“异步模块定义”,他是一种异步模块加载规范,专门为浏览器端设计。AMD 规范由 Common JS 规范演进而来,前文介绍 Common JS 规范时说过,浏览器端无法直接使用 Common JS,需要使用 browserify 编译后才能运行。而 AMD 规范规范则专注于浏览器端。

AMD定义模块的方式如下:

define(id?, dependencies?, factory);

浏览器 并不支持 AMD模块,需要借助Require.js才能加载AMD模块,Require.js是AMD最常用的加载器,但是目前的新系统基本不再使用Require.js,因为大部分库都会提供对AMD的支持。 下文为AMD模块支持的代码

// clone.js
define(function(){
    function clone(){
        //...
    }
    return clone
})

引入该模块

require(['clone'],function(clone){
    clone()
})

看下完整示例:

  1. 目录结构

一文梳理前端模块化方案

2.index.html文件

首先在index.html中引入了RequireJS的cdn,data-main属性为requirejs要找的入口文件路径,也就是说所有逻辑和配置都写在data-main属性对应的文件中

<!DOCTYPE html>  
<html lang="en">  
    <head>  
        <meta charset="UTF-8">  
        <title>Title</title>  
    </head>  
<body>  
    <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="index.js" async ></script>  
</body>  
</html>
  1. index.js文件

index.js文件中主要导入依赖的模块和文件路径了,关于导入其他模块,require对象提供了config方法,其中的path属性指定了所有模块的名字和文件目录(requirejs要求一个模块为一个文件),这样requirejs就知道其他模块叫什么在哪里了,define方法是requirejs提供的定义模块的方法,第一个参数为该模块依赖的相关模块名,第二个参数为模块内部的执行逻辑函数,函数的参数为依赖的模块的传入对象。这样就可以在该模块中使用导入其他模块了。

require.config({  
    path:{  
    clone:'./clone',  // 导入模块名clone,位置在./clone.js中
    util:'./util'  // 导入模块名util,位置在./util.js中
    }  
})  
require(['clone'],function (clone) {  // 该模块依赖clone模块,将其作为参数传入函数中,就可以在函数中调用了
    clone()  
})
  1. clone.js文件
define(['util'],function(util){  //clone模块依赖util模块,在其作用域中可调用util模块,define第一个参数为依赖的模块列表,若无则可不写第一个参数
    function clone(source){  
        //...  
        console.log("clone函数执行")  
        util()  
    }  
    return clone  
})
  1. util.js
define(function () {  // 该模块并未依赖任何其他模块,第一个参数省略
    function util() {  
        console.log("utils被执行")  
    }  
    return util  
})

就这样 AMD规范就基本实现了,由于浏览器端有更好的ES模块方案替代,所以此方案基本不再使用,这里只做简单了解,感兴趣的可看阮一峰关于此模块方案的博客

CommonJS规范

CommonJS是运用在NodeJS环境中的一种同步模块加载规范,CommonJS的提出是为了弥补JavaScript当时没有模块标准的缺陷,开发者希望JavaScript拥有类似python和Java那样开发大型项目的基础能力,而不是让JavaScript仅仅停留在小脚本的阶段,甚至期望CommonJS API写出的应用能够具备跨宿主环境执行的能力,让其不仅能开发客户端,还能开发服务端,命令行工具,混合应用等,如今CommonJS的大部分规范依旧是草案,但是不可否认的是他是JavaScript最为重要的里程碑。

CommonJS模块规范如下:

define(function, require, exports, module){
    //模块代码
}

在NodeJS中,此define函数为系统自动生成,无需开发者书写

  1. 定义模块

通过module.exports导出

function clone(){
    //...
}

module.exports = clone
  1. 导入模块
  2. 通过require导入
 const clone = require('./clone.js')
 clone()

你需要注意的是

  • require 是赋值过程并且是运行时才执行,也就是同步加载
  • require 可以理解为一个全局方法,因为它是一个方法所以意味着可以在任何地方执行。
  • require 导入的是值的引用,也就是说可以多文件内共享对象

UMD (Universal Module Definition)规范

UMD是一种通用模块加载规范,他并不是一种新规范,而是对前面三种规范的整合,支持UMD规范的库可以在任意模块中工作

他的规范如下:

(function (root, factory) {
    var clone = factory(root);
    if(typeof define === 'function' && define.amd){
        //AMD规范
        define('clone',function () {
            return clone
        })
    }else if(typeof exports === 'object'){
        // CommonJS
        module.exports = clone
    }else {
        // 原始模块
        var _clone = root.clone;
        
        clone.onConflict = function () {
            if(root.clone === clone){
                root.clone = _clone
            }
            return clone
        };
        root.clone = clone;
    }
})(this,function (root) {
    function clone() {
        //...
    }
    return clone
})

关于此段代码的详细解释如下:

  1. (function (root, factory) { ... })(this, function (root) { ... }): 这是一个自执行函数,接受两个参数。第一个参数 root 是当前环境的全局对象,通常是 window 对象在浏览器中,或 global 对象在 Node.js 中。第二个参数 factory 是一个函数,用于创建一个名为 "clone" 的模块或变量。

  2. var clone = factory(root);: 在自执行函数内部,它调用 factory 函数并传入 root 作为参数,然后将返回的结果赋值给 clone 变量。这个结果应该是一个包含 "clone" 功能的函数或对象。

  3. if(typeof define === 'function' && define.amd){ ... }: 这是一个条件语句,用于检查是否支持 AMD (Asynchronous Module Definition) 规范。如果当前环境支持 AMD 规范,它会执行下面的代码块。 define('clone', function () { ... }): 这里使用 AMD 规范定义一个名为 "clone" 的模块,其内容由匿名函数返回,这个匿名函数应该返回之前定义的 clone 变量或模块。

  4. else if(typeof exports === 'object'){ ... }: 如果不支持 AMD 规范,它继续检查是否是 CommonJS 模块系统。如果是 CommonJS,它会执行下面的代码块。 module.exports = clone: 这里将之前定义的 clone 变量或模块导出,使其可以被其他 CommonJS 模块引用。

  5. else { ... }: 如果既不支持 AMD 也不是 CommonJS,它会执行下面的代码块。 var _clone = root.clone;: 这里保存了原始的 root.clone_clone 变量中。 clone.onConflict = function () { ... }: 定义了一个名为 onConflict 的方法,这个方法的作用是在命名冲突时解决问题。如果 root.clone 与当前的 clone 相同,它会将 root.clone 恢复为之前保存的 _clone,然后返回当前的 cloneroot.clone = clone;: 最后,它将当前的 clone 赋值给全局的 root.clone,以确保可以在全局范围内访问到这个功能。

这段代码的作用是在不同的模块系统中定义一个名为 "clone" 的模块或变量,并根据模块系统的不同来适配定义方式。在 AMD 中定义模块,在 CommonJS 中导出模块,而在原始模块环境中则将功能赋值给全局变量。同时,它还提供了一个解决命名冲突的机制,以防止与其他模块或全局变量发生冲突。 =》 此段为chatgpt解释

ES Module模块

ES Module是ECMAScript2015带来的原生模块系统,主流浏览器都已支持,不兼容ESM的可以通过构建工具处理后使用

  1. 导出
export function clone(){}

// 或者

export default clone
  1. 导入
import { clone } from "./clone"

// 或者

import clone form "./clone"

关于ESM你需要知道的是

  1. 使用 import 语句导入模块时,模块会被静态加载,他是在编译时就已经确定了导入的模块;

  2. importimport() 语句支持模块的默认导出和命名导出。

  3. export导出的也是引用。 => 似乎很多人说require是浅拷贝,但是实操后并未发现其是浅拷贝而同样是引用

若在script标签中使用 需要加上type="module"属性

ESM和CommonJS的区别

  1. 静态 vs. 动态导入:

    • ES6模块系统是静态的,模块的依赖关系在编译时确定。这意味着所有导入语句必须在模块的顶层,并且不能放在条件语句或函数内部。
    • CommonJS模块系统是动态的,模块的依赖关系可以在运行时确定。这允许你在条件下或函数内部导入模块。
  2. 导出方式:

    • 在ES6模块系统中,你可以使用 exportexport default 导出模块的功能或值。
    • 在CommonJS中,你可以通过 module.exports 导出一个值或对象。
  3. 导入方式:

    • 在ES6模块系统中,你可以使用 import 关键字导入模块的特定功能或值,也可以重命名导入的项。
    • 在CommonJS中,你可以使用 require 函数导入整个模块,并且你不能像ES6一样轻松重命名导入的项。
  4. 默认导出:

    • ES6模块系统支持单一的默认导出,一个模块只能有一个默认导出。
    • CommonJS模块系统没有内置的默认导出机制,但可以手动约定一个默认导出属性。
  5. 循环依赖:

    • ES6模块系统能够更好地处理循环依赖情况,因为它在编译时解析依赖关系。
    • CommonJS模块系统在处理循环依赖时可能会导致奇怪的行为,因为它在运行时解析依赖。
  6. 生态系统:

    • ES6模块系统是ECMAScript的一部分,主要用于浏览器端和现代JavaScript开发。
    • CommonJS模块系统主要用于Node.js和服务器端JavaScript开发。
转载自:https://juejin.cn/post/7281159309969915961
评论
请登录