前端模块化的学习总结

20 阅读5分钟

为什么会出现模块化

早期的前端,只是简单的页面展示,代码量少,但是随着web技术的发展,ajax的出现,各种交互使得页面的功能越来越丰富, 这同时也让前端的代码越来越多,复杂度越来越高,带来了全局变量的污染依赖关系混乱维护性差复用性差的问题

前端模块化就是为了解决这些问题,提高代码的维护性和可服用性。

什么是模块化

模块化是将一个复杂的系统,分解为独立可复用的软件开发方式。每个模块负责特定的功能,组装起来成为一个整体。

前端模块化的发展

全局Function

将不同功能封装成不同的函数

function a(){...}

function b(){...}

缺点:方法直接挂在window下,污染全局变量,并且容易造成命名冲突。

命名空间模式

解决命名冲突的问题,使用对象封装函数

var module = {
  a:function(){....},
  b:function(){....},
}

缺点:外部可以修改模块内的数据

立即执行函数(IIFE)

解决模块暴露的问题,将数据私有

var MyModule = (function() {
    var privateVar = 'private';
    function privateMethod() {
        console.log(privateVar);
    }
    return {
        publicMethod: function() {
            privateMethod();
        }
    };
})();

//可以传入参数,达到引用其他模块的目的

现代模块化的基石。(commonJS的实现也是这个思路)

缺点:依赖过多传入,代码阅读困难;无特定语法支持,代码简陋。

存在的问题

  • 模块拆分后,通过多个script标签引入,创建了多次请求。
  • 依赖模糊,不能确定模块的先后顺序,加载顺序不同可能会带来错误。
  • 没有明确的关系依赖,维护变得更困难

模块化标准

commonJS

nodeJS的出现,让js编写服务端成为可能。 为了解决服务端的模块化问题,出现了commonJS规范。

这个规范定义了模块的基本结构,模块的导入导出。

特点

  • 每个文件都是一个独立的模块,独立作用域,不会污染全局空间。
  • 同步加载模块,require导入之后,会立即执行这个模块, 将执行结果进行返回。(保证了模块的加载顺序)
  • 当模块加载完成一次之后, 会有缓存,再次加载会直接返回缓存结果。
  • 模块导出的值是值的拷贝, 导出之后,之前模块发生变化不会影响。

导出

可以使用module.export 或者 export 两种方式进行导出。

//导出一个变量
module.export.name = 'a';


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

export.foo = function(){...}

导入

nodejs在导入时,只有执行到require的时候,才会执行。 这样保证了模块的执行顺序。

当模块执行过后,会进行缓存,下次执行时,判断是否有缓存, 有缓存直接拿缓存。

动态加载:可以在代码的任何地方使用require。

var moduleA = require('./moduleA');

原理

实现commonJS,node做了什么事情呢?

两种导出方式有什么不同呢?

//导出
(function(exports, require, module, __filename, __dirname) {
  module.exports = {};
  var export = module.export;
  return module.export
});

可以看出, module.export是真正的导出对象,export只是导出值的一个引用。不能直接对export直接赋值,否则就会断开和module.export的引用。 使用过程中尽量保持一种写法。

局限性

  • 因为node时运行在服务端,所以可以直接读取本地文件,这样不需要考虑同步执行时的效率问题,但是在浏览器端是要从远端获取数据,同步执行会造成卡顿。
  • 模块加载是动态加载, 无法静态分析。

要想实现浏览器模块化,需要解决两个问题: 第一解决浏览器远程加载数据的问题(异步)第二解决模块执行环境的问题(放在函数中执行)

AMD

异步模块定义规范。 适用于浏览器,RequireJS实现。

核心理念:异步加载,提前执行

//定义一个模块
define('module1', ['module2', 'module3'], function(module2, module3) {
  // 模块逻辑
    function foo() {
        return module2.doSomething() + module3.doSomething();
    }

    // 导出模块功能
    return {
        foo: foo
    };
});


// 加载模块 'module1'
require(['module1'], function(module1) {
    module1.foo();
});

require会将第一个参数中的依赖,加载完成,才会执行第二个参数的回调函数。

优点

  • 异步加载:适用于浏览器环境
  • 依赖关系明确

使用的时候,需要依赖requireJS

CMD

解决浏览器模块化,seaJS实现。

核心理念:依赖就近,延迟执行

// 定义一个模块myModule
define(function(require, exports, module) {
    // 就近依赖,不会理解执行。
    var dep1 = require('./dep1');
    var dep2 = require('./dep2');

    function foo() {
      //只有模块被使用时,才去加载依赖的模块
        return dep1.doSomething() + dep2.doSomething();
    }

    // 导出模块功能
    exports.foo = foo;
});


// 加载模块 'myModule'
seajs.use(['myModule'], function(myModule) {
    myModule.foo();
});

优点

  • 减少了无用依赖的加载,避免提前加载造成的资源浪费。
  • 避免过早的加载未使用的模块, 启动更快
  • 延迟执行可以避免不必要的代码执行

UMD

UMD模块化解决方案,旨在兼容多种JS模块加载机制(node使用commonJS,浏览器端使用AMD),他的目的是同一个模块在不同环境中可以使用。

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // 如果环境支持 AMD(如 RequireJS),使用 AMD 定义模块
        define(factory);
    } else if (typeof module === 'object' && module.exports) {
        // 如果环境支持 CommonJS(如 Node.js),使用 CommonJS 定义模块
        module.exports = factory();
    } else {
        // 否则将模块挂载到全局变量(window或者global)
        root.myModule = factory();
    }
}(this, function () {
    // 这里是模块的主体,返回模块暴露的内容
    var myModule = {
        hello: function () {
            console.log('Hello, UMD!');
        }
    };
    return myModule;
}));

ESM模块化标准

ES6引入了ESModule模块化标准。

使用方式:

// moduleA.js
export function foo() {
    console.log('foo');
}

// main.js
// 所有路径必须以./或者../开头
import { foo } from './moduleA.js';
foo();

<!-- 在浏览器中加载ESModule模块 -->
<script type="module" src="./moduleA.js"></script>


//也支持动态导入
// 动态导入模块
import('./math.js').then(math => {
    console.log(math.add(1, 2));  // 3
});

优点

  • 一个文件一个模块,封装了自己模块的作用域,解决了全局变量污染的问题
  • 依赖清晰
  • 静态分析依赖,工具在编译时分析代码,进行一些优化(Tree Shaking)
  • javeScript官方提出, 对浏览器和common都兼容。

对比commonJS

  • commonJS模块输出的是值的拷贝,修改导出后的值不会影响引用的值,ESM是值的引用,修改会影响引用的值
  • commonJS是运行时加载,在运行时才能知道模块的依赖关系。ESM是编译时就可以知道依赖关系。