胖蔡叨叨叨
你听我说

JS 的模块化规范发展历程

JavaScript前期的代码还不是很复杂,我们可以通过代码简单实现,但随着项目越来越大、程序所包含的功能越来越多、开发人员原来越多的时候,我们就需要一个统一的模块化规范来编写代码,以求我们的代码的可读性、可维护性更高,同时也方便其他人来使用我们的实现。

什么是模块化?

模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案,一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。

解决目标

  • 提高代码的可维护性,降低重构成本
  • 解决全局变量污染和变量重名等问题
  • 依赖管理

缺点

  • 系统分层,调用链会很长
  • 模块间通信,模块间发送消息会很耗性能

模块化的演进

随着需求的不断增加,前端的模块化技术也是一直处于不断演进的状态。

全局function模式(1999)

将不同的功能封装成不同的全局函数,这会导致Global被污染了, 很容易引起命名冲突。

 function sum(a,b){
   return parseInt(a) + parseInt(b);
 }


  function reduce(a,b){
   return parseInt(a) - parseInt(b);
 }

命名空间

通过将参数、方法挂载在对象上,实现的简单的模块化隔离。

var MYNAMESPACE = MYNAMESPACE || {};
 
MYNAMESPACE.person = function(name) {
    this.name = name;
};
 
MYNAMESPACE.person.prototype.getName = function() {
    return this.name;
};
 
// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName();        /


IIFE执行函数(闭包模式)

IIFE: Immediately Invoked Function Expression,意为立即调用的函数表达式,也就是说,声明函数的同时立即调用这个函数。我看可以通过自执行函数实现数据的私有化隔离(函数上下文)。匿名函数自身不污染全局环境,同时为内部变量提供作用于环境空间,且提供闭包环境,可以做闭包想做的事情。

var a = 2;
(function IIFE(global){
    var a = 3;
    console.log(a); // 3
    console.log(global.a); // 2
})(window);
 
console.log(a); // 2

CommonJS

2009年发布,Node 应用由模块组成,采用 CommonJS 模块规范。commonJS规范加载是同步的,也就是说,加载完成才执行后面的操作。CommonJS遵循Modules/1.0(http://wiki.commonjs.org/wiki/Modules/1.0)规范,该规范首次提出了JS的模块化实现应该遵循的规则(nodejs)。规范指出:

  1. 模块上下文(require)
  • 在模块中,有一个自由变量“ require”,即一个函数
  • 在模块中,有一个称为“ exports”的自由变量,该变量是模块执行时可以向其添加API的对象。
  • 模块必须使用“导出”对象作为唯一的导出方法。
  1. 模块标识符
  • 模块标识符是由正斜杠分隔的“条款”字符串。
  • 术语必须是驼峰标识符“。”或“ ..”。
  • 模块标识符可能没有文件扩展名,例如“ .js”。
  • 模块标识符可以是“相对”或“顶级”。如果第一项为“”,则模块标识符为“相对”。或者 ”..”。
  • 顶级标识符从概念模块名称空间根目录解析。
  • 相对标识符相对于在其中写入和调用“ require”的模块的标识符进行解析。
  1. 未指定

该规范保留了以下未指明的互操作性要点:

  • 模块是与数据库,文件系统或工厂功能一起存储的,还是与链接库可互换的。
  • 模块加载程序是否支持PATH来解析模块标识符。

遵循commonjs规范的代码:


//math.js
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};


//increment.js
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};


//program.js
var inc = require('increment').increment;
var a = 1;
inc(a); // 2


Modules/1.0规范源于服务端,无法直接用于浏览器端,原因表现为:

  • 外层没有function包裹,变量全暴漏在全局。
  • 资源的加载方式与服务端完全不同。

浏览器模块化

基于Modules/1.0的基础上,commonJs社区讨论关于浏览器端的模块化规范的时候,内部发生了比较大的分歧,分裂出了三个主张,渐渐的形成三个不同的派别:

Modules/Transport

这一波人认为,在现有基础上进行改进即可满足浏览器端的需要,既然浏览器端需要function包装,需要异步加载,那么新增一个方案,能把现有模块转化为适合浏览器端的就行了。基于这个主张,制定了Modules/Transport(http://wiki.commonjs.org/wiki/Modules/Transport)规范,提出了先通过工具把现有模块转化为复合浏览器上使用的模块,然后再使用的方案。

AMD

AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。他们认为浏览器与服务器环境差别太大,不能沿用旧的模块标准。既然浏览器必须异步加载代码,那么模块在定义的时候就必须指明所依赖的模块,然后把本模块的代码写在回调函数里。模块的加载也是通过下载-回调这样的过程来进行,这个思想就是AMD的基础,由于“革新派”与“保皇派”的思想无法达成一致,最终从CommonJs中分裂了出去,独立制定了浏览器端的js模块化规范AMD(Asynchronous Module Definition)(https://github.com/amdjs/amdjs-api/wiki/AMD)

// 规定采用require语句加载模块,但是不同于CommonJS,它要求两个参数
require([module], callback);
// 在定义模块的时候需要使用define函数定义:
define(id?, dependencies?, factory);
  • 动态并行加载js,依赖前置,一个模块的回调函数必须得等到所有依赖都加载完毕之后,才可执行。无需再考虑js加载顺序问题。
  • 规范化输入输出,使用起来方便。
  • 对于不满足AMD规范的文件可以很好地兼容。

Modules/Wrappings

这一波人有点像“中间派”,既不想丢掉旧的规范,也不想像AMD那样推到重来。他们认为,Modules/1.0固然不适合浏览器,但它里面的一些理念还是很好的,(如通过require来声明依赖),新的规范应该兼容这些,AMD规范也有它好的地方(例如模块的预先加载以及通过return可以暴漏任意类型的数据,而不是像commonjs那样exports只能为object),也应采纳。最终他们制定了一个Modules/Wrappings(http://wiki.commonjs.org/wiki/Modules/Wrappings)规范,此规范指出了一个模块应该如何“包装”,包含以下内容:

  • 全局有一个module变量,用来定义模块
  • 通过module.declare方法来定义一个模块
  • module.declare方法只接收一个参数,那就是模块的factory,次factory可以是函数也可以是对象,如果是对象,那么模块输出就是此对象。
  • 模块的factory函数传入三个参数:require,exports,module,用来引入其他依赖和导出本模块API
  • 如果factory函数最后明确写有return数据(js函数中不写return默认返回undefined),那么return的内容即为模块的输出。

UMD

为了支持一个模块同时兼容AMD和CommonJs规范,适用于 同时支持浏览器端和服务端引用的第三方库,提出了 UMD规范。UMD是一个时代的产物,当各个环境最终实现ES harmony的统一的规范后,它也将退出历史舞台。

// 定义一个模块
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'underscore'], factory);
    } else if (typeof exports === 'object') {
        // Node, CommonJS之类的
        module.exports = factory(require('jquery'), require('underscore'));
    } else {
        // 浏览器全局变量(root 即 window)
        root.returnExports = factory(root.jQuery, root._);
    }
}(this, function ($, _) {
    //    方法
    function a(){};    //    私有方法,因为它没被返回 (见下面)
    function b(){};    //    公共方法,因为被返回了
    function c(){};    //    公共方法,因为被返回了

    //    暴露公共方法
    return {
        b: b,
        c: c
    }
}));

CMD

CMD 是 “Common Module Definition”的缩写,意思是通用模块规范。CMD专门用于浏览器端, 模块的加载是异步的 ,模块使用时才会加载执行。在 CMD 规范中,一个模块就是一个文件。规范地址: https://github.com/cmdjs/specification/blob/master/draft/module.md

// 定义一个模块

define(function(require, exports, module) {
   ....
})


//sea.js:
define(function(require, exports, module) {

    var mod_A = require("dep_A");
    var mod_B = require("dep_B");
    var mod_C = require("dep_C");
});

ES2015 Module(es6初版)

ES6是ECMA的为JavaScript制定的第6个版本的标准,标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。ECMAscript 2015 是在2015年6月份发布的ES6的第一个版本。

// file lib/greeting.js 定义一个模块
const helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};
// 对外输出
export const greeting = {
    sayHello: function (lang) {
        return helloInLang[lang];
    }
};

// file hello.js 引入一个模块
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);


webpack(构建工具)

webpack 自己实现了一套模块机制,无论是 CommonJS 模块的 require 语法还是 ES6 模块的 import 语法,都能够被解析并转换成指定环境的可运行代码。随着webpack打包工具的流行,ES6语法广泛手中,后来的开发者对于 AMD CMD的感知越来越少。

赞(0) 打赏
转载请附上原文出处链接:胖蔡叨叨叨 » JS 的模块化规范发展历程
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏

'); })();