前端模块化发展历程

什么是模块化?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起

  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

模块化的理解

模块化是一种处理复杂系统分解为更好的可管理模块的方式。简单来说就是解耦,简化开发,一个模块就是实现特定功能的文件,可以更方便地使用别人的代码,想要什么功能,就加载什么模块。模块开发需要遵循一定的规范

模块化的好处
  • 避免命名冲突(减少命名空间污染)

  • 更好的分离, 按需加载

  • 更高复用性

  • 高可维护性

模块化的作用
  • 减少JS文件的请求次数,通过模块化将JS文件整合为一个入口,然后引入页面可以有效的减少对JS文件的请求;

  • 使各JS文件的依赖关系清晰,在模块化中可以清晰的分析各模块的引用关系,明确JS代码的结构;

  • 降低项目的维护成本,当有某个模块需要添加或减少某个功能使,不需要将整个代码重构,只需要在相应的模块进行修改就可以。

1. 第一阶段——无模块化

早在模块化标准还没有诞生的时候,前端模块化的雏形有三种形式:

  • 文件划分

  • 命名空间

  • IIFE 私有作用域

文件划分

这是最早的模块化实现,简单来说就是把应用的状态和逻辑放到不同的 JS 文件中,HTML 中通过不同的 script 标签一一引入。这样的缺点是:

  • 模块变量相当于在全局声明和定义,会有变量名冲突的问题。

  • 变量都在全局定义,导致难以调试,我们很难知道某个变量到底属于哪些模块。

  • 无法清晰地管理模块之间的依赖关系和加载顺序。假如 a.js 依赖 b.js,那么 HTML 中的 script 执行顺序需要手动调整,不然可能会产生运行时错误。

<script src="jquery.js"></script>
<script src="jquery_scroller.js"></script>
<script src="main.js"></script>
<script src="other.js"></script>
命名空间

命名空间的出现解决了文件划分带来的部分问题:

// module-a.jswindow.moduleA = {
  data: "moduleA",
  method: function () {
    console.log("execute A's method");
  },
};
​
// module-b.jswindow.moduleB = {
  data: "moduleB",
  method: function () {
    console.log("execute B's method");
  },
};

每个变量都有自己专属的命名空间,我们可以清楚地知道某个变量到底属于哪个模块,同时也避免全局变量命名的问题。

IIFE(立即执行函数)

IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。特点: 在一个单独的函数作用域中执行代码,避免变量冲突

(function () {
    statements
})();
  • 这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

  • 第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

(function () { 
    var name = "Barry";
})();
// 无法从外部访问变量 name
name // 抛出错误:"Uncaught ReferenceError: name is not defined"
​
​
var result = (function () { 
    var name = "Barry"; 
    return name; 
})(); 
// IIFE 执行后返回的结果:
result; // "Barry"

2. 第二阶段——CommonJS 规范

CommonJS 是业界最早正式提出的 JavaScript 模块规范,主要用于服务端 Node.js:

var data = "hello world";
functiongetData() {
  return data;
}
module.exports = {
  getData,
};
​
// index.jsconst { getData } = require("./module-a.js");
console.log(getData());

CommonJS 中使用require来导入一个模块,用module.exports来导出一个模块。CommonJS 定义了一套完整的模块化代码规范,不过仍然存在一些问题:

  • 它的模块加载器由 Node.js 提供,依赖了 Node.js 本身的功能实现。如果 CommonJS 模块直接放到浏览器中无法执行。

  • CommonJS 约定以同步的方式进行模块加载,这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。即,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。

CommonJS 的这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。

总之,CommonJS 是一个不太适合在浏览器中运行的模块规范。因此,业界也设计出了全新的规范来作为浏览器端的模块标准,最知名的要数AMD了。

3. 第三阶段——AMD 规范

AMD 即 Asynchronous Module Definition,中文名是“异步模块定义”的意思。在浏览器环境,要从服务器端加载模块,就必须采用非同步模式,因此浏览器端一般采用AMD规范。AMD是一个在浏览器端模块化开发的规范,而AMD规范的实现,就是require.js。特点:依赖必须提前声明好

  • 优点:异步加载,不阻塞页面的加载,能并行加载多个模块

  • 缺点:不能按需加载,必须提前加载所需依赖

// main.js
define(["./print"], function(printModule) {
  printModule.print("main");
});
​
// print.js
define(function() {
  return {
    print: function(msg) {
      console.log("print " + msg);
    },
  };
});

在 AMD 规范当中,我们可以通过 define 去定义或加载一个模块,比如上面的main模块和print模块,如果模块需要导出一些成员需要通过在定义模块的函数中 return 出去(参考print模块),如果当前模块依赖了一些其它的模块则可以通过 define 的第一个参数来声明依赖(参考main模块),这样模块的代码执行之前浏览器会先加载依赖模块

可以使用 require 关键字来加载一个模块,如:

// module-a.js
require(["./print.js"], function(printModule) {
  printModule.print("module-a");
});

不过 require 与 define 的区别在于前者只能加载模块,不能定义一个模块。

由于没有得到浏览器的原生支持,AMD 规范仍然需要由第三方的 loader 来实现。不过 AMD 规范使用起来稍显复杂,代码阅读和书写都比较困难。因此,关于新的模块化规范的探索,业界从仍未停止脚步。

4. CMD规范

CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。特点:支持动态引入依赖文件。

  • 优点:可以按需加载,依赖就近

  • 缺点:依赖SPM打包,模块的加载逻辑偏重

// 定义// 使用 exports 直接向外提供接口。
define(function(require, exports) { 
    // 对外提供 name属性
    exports.name = 'Tom'; 
    // 对外提供 say 方法
    exports.say= function(name) {
        console.log("hello"+name)
    };
});
// 使用 return 直接向外提供接口。
define(function(require) {  
    return {
        name : 'Tom',    
        say: function(name) {
            console.log("hello"+name)
        }
    };
});
// 使用 module.exports 直接向外提供接口。
define(function(require, exports, module) { 
    module.exports = {
        name: 'Tom', 
        say: function(name) {
            console.log("hello"+name)
        }
    };
});
​
​
// 使用
define(function (require) {
    var m1 = require('./module1')
    console.log(m1.name)      // Tom
    m1.say(m1.name)           // Hello Tom
})
​
// require.async 方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback 参数可选。
define(function(require, exports, module) {  
    // 异步加载一个模块
    require.async('./module1', function(a) {
        a.doSomething();
    }); 
​
    // 异步加载多个模块,在加载完成时,执行回调
    require.async(['./module2', './module3'], function(b, c) {
        b.doSomething();
        c.doSomething();
    });
})

5. UMD规范

UMD 叫做通用模块定义规范(Universal Module Definition)。也是随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了。是集结了 CommonJs、CMD、AMD 的规范于一身。

// UMD的实现
((root, factory) => {
    if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } elseif (typeofexports === 'object') {
        //CommonJS
        var $ = requie('jquery');
        module.exports = factory($);
    } else {
        root.testModule = factory(root.jQuery);
    }
})(this, ($) => {
    //todo
});

不难发现,它在定义模块的时候回检测当前使用环境和模块的定义方式,将各种模块化定义方式转化为同样一种写法。它的出现也是前端技术发展的产物,前端在实现跨平台的道路上不断的前进,UMD 规范将浏览器端、服务器端甚至是 APP 端都大统一了

6. webpack(require.ensure)

webpack 2.x 版本中的代码分割。

7. 第四阶段——ES Module

ES6 在语言标准的层面上,实现了模块功能,而且非常简单,ES6到来,完全可以取代 CommonJS 和 AMD规范,成为浏览器和服务器通用的模块解决方案。ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。 注:由于ES6目前在部分版本的浏览器中无法执行,所以,我们需要通过babel将不被支持的import编译为当前受到广泛支持的 require。

// 定义
​
// 变量    es3.jsexportvar m = 1;
//函数exportfunctionfn(x, y) {
  return x * y;
};
//类classclassHello{
  test(){
    console.log("hello")
  }
}
​
​
// 也可以合并为一个出口暴露var m = 1;
functionfn(x, y) {
  return x * y;
};
classHello{
  test(){
    console.log("hello")
  }
}
export {
  m,
  fn,
  Hello
}
​
// 在暴露模块时,可以通过 as 来进行重命名export{
  num as m,
  foo as fn,
  TestasHello
}
​
// 引用
​
//静态加载,只加载es3.js 文件中三个变量,其他不加载import {m, fn, Hello} from'./es3.js';
//import命令要使用as关键字,将输入的变量重命名。import {fn as fn1} from'./es3.js';
//整体加载模块
improt * as all from'./es3.js'console.log(all.m)              // 1console.log(all.fn(3,4))        // 12
all.Hello.test()                // hello

特点:

  • ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。

  • 自动采用严格模式"use strict"。须遵循严格模式的要求

  • ES6 模块的设计思想是尽量的静态化,编译时加载”或者静态加载,编译时输出接口

  • ES6 模块export、import命令可以出现在模块的任何位置,但是必须处于模块顶层。如果处于块级作用域内,就会报错

  • ES6 模块输出的是值的引用

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值