前端模块化简史

对于模块化这样的话语,同学们都听过,但是你们时候真正了解,什么是 模块化,为什么要模块化,以及如何模块化?相信同学们都有以上的疑问。 对于模块化来说,他并不是一开始就存在的,而是随着 js 的能力浏览器的 能力以及业务的发展,我们也需要对于 js 有一个更好的管理模式,接下来 通过模块化发展来看一下,模块化是如何一步一步的成长到现在的。

模块化的发展历程

当 Brendan Eich 设计 JavaScript 的第一个版本时,他可能不知道他的这个 设计在过去二十年中将如何发展。目前已经有三种种主要版本的语言规范 (ES3 ES5 ES6),其改进工作仍在继续。

说实话,JavaScript 从来就不是一个完美的编程语言。JS 的一个弱点是模块 化的缺失。确实,将所有脚本语言仅用于页面上动画或表单验证,一切都 可以在相同的全局范围内交互,代码的依赖关系又该怎么处理。

随着时间的推移,JavaScript 已经转变为通用语言,因为它开始用于在各种 环境(浏览器,移动设备,服务器,物联网)中构建复杂的应用程序。程 序组件通过全局范围进行交互的旧方法变得不可靠,因为越来越多的代码 往往会使应用程序过于脆弱,能解决以上问题的关键在于模块化。

在早期的 Web 开发中,所有的嵌入到网页内的 JavaScript 对象都会 使用全局的 window 对象来存放未使用 var 定义的变量。大概在上世纪 末,JavaScript 多用于解决简单的任务,这也就意味着我们只需编写少量 的 JavaScript 代码;不过随着代码库的线性增长,我们首先会碰到的就是 所谓命名冲突(Name Collisions)困境。

以下这种情况就存在命名冲突 这里面是两个或者多个 script 标签,代表若干个部分:

<script>
    function showMsg(valuesArr) {
        console.log(valuesArr.join('-'))
    }
</script>
<script>
    function showMsg(msg) {
        console.log(msg)
    }
    showMsg('HELLO MODULE')
</script>

或者:

<script src="tools.js"></script>
<script src="lib.js"></script>

我们在页面内同时引入这两个 JavaScript 脚本文件时,显而易见两个文件 中定义的 showMsg 函数起了冲突,最后调用的函数取决于我们引入的先 后顺序。此外在大型应用中,我们不可能将所有的代码写入到单个 JavaScript 文件中;我们也不可能手动地在 HTML 文件中引入全部的脚本 文件,特别是此时还存在着模块间依赖的问题,相信很多开发者都会遇到 jQuery 尚未定义这样的问题。

命名空间模式(2002)

要解决名称冲突问题,您可以使用特殊代码约定。例如,你可以为所有变量和函数添加特定前缀 myApp_ myApp_address,myApp_validateUser()。 此外,您可以使用 JavaScript 中的函数是一等公民的事实,即您可以将它 们分配给变量,对象的属性并从其他函数返回它们。因此,您可以使用与 对象文档和窗口类似的属性创建对象(document.write(),window.alert ())。

如果命名空间的目的是避免冲突的话。下面这个系统,只要我们知道全局 变量名前缀 myApp_ 是唯一的,可以像其他系统一样避免命名空间冲突。

// add uniquely named global properties
var myApp_sayHello = function() {
    alert('hello');
};
var myApp_sayGoodbye = function() {
    alert('goodbye');
}
// use the namespace properties
myApp_sayHello();

当下,最流行的 JavaScript 命名空间实践是使用一个全局变量来引用一个 对象。这个被引用的对象引用你的『真正的业务』,并且因为你的全局对 象的命名独一无二,你的代码和其他人的代码就可以一起嗨皮地运行。

利用这个模式的第一个重要项目是一个 ui 元素 Bindows 库。Bindows 是由 我们在 2002 年熟悉的 Erik Arvidsson 创建的。他没有在函数和变量的名称 中使用前缀,而是使用了一个全局对象,其属性包含库的数据和逻辑。这 一事实大大减少了全球范围的污染。该代码组织的模式现在称为“命名空 间”(命名空间模式)。 但是这种模式最大的问题在于变量完全暴露,私有变量方法都没有办法进 行设定

闭包模块化模式(2003)

命名空间为代码组织提供了某种顺序。但很明显,这还不够,因为还没有 解决模块和数据的隔离问题。 解决这个问题的先驱是模块模式。它的主要思想是使用闭包封装数据和代 码,并通过外部可访问的方法提供对它们的访问。以下是此类模式的基本 示例:

var greeting = (function () {
    var module = {};
    var helloInLang = {
        en: 'Hello world!',
        es: '¡Hola mundo!',
        ru: 'Привет мир!'
    };
    module.getHello = function (lang) {
        return helloInLang[lang];
    };
    module.writeHello = function (lang) {
        document.write(module.getHello(lang))
    };
    return module;
}());

这里我们看到立即调用的函数,它返回一个模块对象,该模块对象又有一 个通过闭包 getHello 访问对象的方法 helloInLang。因此,helloInLang 从外 部世界变得无法访问,我们得到一个代码片段,可以粘贴到任何其他脚本 而不会发生名称冲突。

模版依赖定义 (2006)

这时候开始流行后端模版语法,通过后端语法聚合 js 文件,从而实现依赖加载,说实话,现在 go 语言等模版语法也很流行这种方式,写后端代 码的时候不觉得,回头看看,还是挂在可维护性上。

注释依赖定义 (2006)

和模版依赖定义同时出现,与 1999 年方案不同的,不仅仅是模块定义方 式,而是终于以文件为单位定义模块了,通过 lazyjs 加载文件,同时读取 文件注释,继续递归加载剩下的文件。

外部依赖定义 (2007)

这种定义方式在 cocos2d-js 开发中普遍使用,其核心思想是将依赖抽出 单独文件定义,这种方式不利于项目管理,毕竟依赖抽到代码之外,我是 不是得两头找呢?所以才有通过 webwpack 打包为一个文件的方式暴力 替换为 commonjs 的方式出现。

依赖注入(2009)

2004 年,Martin Fowler 介绍了“ 依赖注入 ”(DI)的概念,用于描述 Java 中组件的新通信机制。要点是所有依赖关系都来自组件外部,因此组件不 负责初始化它只使用它们的依赖关系。 五年后,MiškoHevery 成为 Sun 和 Adobe 的前雇员(他主要从事 Java 开 发),他开始为他的创业公司设计一个新的 JavaScript 框架,它使用依赖 注入作为组件之间通信的关键机制。商业理念尚未证明其有效性,框架的 源代码在他的创业公司 getangular.com 的领域被打开并引入世界。

我们都 知道接下来发生了什么。谷歌已经采取了它的翼 Miško 和他的项目,现在 Angular 是最着名的 JavaScript 框架之一。 Angular 中的模块通过依赖注入机制实现。顺便说一句,模块化不是 DI 的 主要目的,Miško 在相应问题的答案中也明确表示。

CommonJS(2009)

为什么会出现 CommonJS 规范? 因为 JavaScript 本身并没有模块的概念,不支持封闭的作用域和依赖管理, 传统的文件引入方式又会污染变量,甚至文件引入的先后顺序都会影响整 个项目的运行。同时也没有一个相对标准的文件引入规范和包管理系统, 这个时候 CommonJS 规范就出现了。

CommonJS API 定义很多普通应用程序(主要指非浏览器的应用)使用的 API,从而填补了这个空白。它的终极目标是提供一个类似 Python,Ruby和 Java 标准库。这样的话,开发者可以使用 CommonJS API 编写应用程序, 然后这些应用可以运行在不同的 JavaScript 解释器和不同的主机环境中。

在兼容 CommonJS 的系统中,你可以使用 JavaScript 开发以下程序:

  • 服务器端 JavaScript 应用程序
  • 命令行工具
  • 图形界面应用程序
  • 混合应用程序(如,Titanium 或 Adobe AIR)

2009 年,美国程序员 Ryan Dahl 创造了 node.js 项目,将 javascript 语 言用于服务器端编程。这标志”Javascript 模块化编程”正式诞生。因为老 实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的 复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程 序互动,否则根本没法编程。NodeJS 是 CommonJS 规范的实现,webpack 也是以 CommonJS 的形式来书写。

浏览器不兼容 CommonJS 的根本原因,在于缺少四个 Node.js 环境的变量:

  • module
  • exports
  • require
  • global

只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。下面是一 个简单的示例。

var module = {
    exports: {}
};
(function(module, exports) {
    exports.multiply = function (n) { return n * 1000 };
}(module, module.exports))
var f = module.exports.multiply;
f(5) // 5000 

上面代码向一个立即执行函数提供 module 和 exports 两个外部变 量 , 模 块 就 放 在 这 个 立 即 执 行 函 数 里 面 。 模 块 的 输 出 值 放 在 module.exports 之中,这样就实现了模块的加载。

根据 CommonJS 规范,一个单独的文件就是一个模块。每一个模块都是一 个单独的作用域,也就是说,在一个文件定义的变量(还包括函数和类), 都是私有的,对其他文件是不可见的。

var x = 5;
var addX = function(value) {
    return value + x;
};

上面代码中,变量 x 和函数 addX,是当前文件私有的,其他文件不可见。 如果想在多个文件分享变量,必须定义为 global 对象的属性。\ 上面代码的 waining 变量,可以被所有文件读取。当然,这样写法是不推 荐的。

CommonJS 规定,每个文件的对外接口是 module.exports 对象。这个对象 的所有属性和方法,都可以被其他文件导入。

var x = 5;
var addX = function(value) {
    return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过 module.exports 对象,定义对外接口,输出变量 x 和函数 addX。module.exports 对象是可以被其他文件导入的,它其实就是文件内 部与外部通信的桥梁。

require 方法用于在其他文件加载这个接口,具体用法参见《Require 命令》 的部分。

var example = require('./example.js');
console.log(example.x); // 5
console.log(addX(1)); // 6

CommonJS 模块的特点如下:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就 被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

module 对象

每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性。 module.id 模块的识别符,通常是带有绝对路径的模块文件名。 module.filename 模块的文件名,带有绝对路径。

module.loaded 返回一个布尔值,表示模块是否已经完成加载。

module.parent 返回一个对象,表示调用该模块的模块。

module.children 返回一个数组,表示该模块要用到的其他模块。

下面是一个示例文件,最后一行输出 module 变量:

// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);

执行这个文件,命令行会输出如下信息:

{ 
    id: '.',
    exports: { '$': [Function] },
    parent: null,
    filename: '/path/to/example.js',
    loaded: false,
    children: [ 
        { 
            id: '/path/to/node_modules/jquery/dist/jquery.js',
            exports: [Function],
            parent: [Circular],
            filename: '/path/to/node_modules/jquery/dist/jquery.js',
            loaded: true,
            children: [],
            paths: [Object] 
        } 
    ],
    paths: [ 
        '/home/user/deleted/node_modules',
        '/home/user/node_modules',
        '/home/node_modules',
        '/node_modules' 
    ]
}

如果在命令行下调用某个模块,比如node something.js,那么module.parent 就是 undefined。如果是在脚本之中调用,比如 require('./something.js'), 那么 module.parent 就是调用它的模块。利用这一点,可以判断当前模块 是否为入口脚本。

if (!module.parent) {
 // ran with `node something.js`
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
 // used with `require('/.something.js')`
    module.exports = app;
}

module.exports 属性

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块, 实际上就是读取 module.exports 变量。

var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
    module.exports.emit('ready');
}, 1000);

上面模块会在加载后 1 秒后,发出 ready 事件。其他文件监听该事件,可 以写成下面这样。

var a = require('./a');
a.on('ready', function() {
    console.log('module a is ready');
});

exports 变量

为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports。 这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;

造成的结果是,在对外输出模块接口时,可以向 exports 对象添加方法。

exports.area = function (r) {
    return Math.PI * r * r;
};
exports.circumference = function (r) {
    return 2 * Math.PI * r;
}

注意,不能直接将 exports 变量指向一个值,因为这样等于切断了 exports 与 module.exports 的联系

exports = function(x) {console.log(x)};

上面这样的写法是无效的,因为 exports 不再指向 module.exports 了。

AMD 规范(2009)

介绍了同步方案,我们当然也有异步方案。在浏览器端,我们更常用 AMD 来实现模块化开发。AMD 是 Asynchronous Module Definition 的简称,即 “异步模块定义”。 我们看一下 AMD 模块的使用方式:

在这里,我们使用了 define 函数,并且传入了两个参数。 第一个参数是一个数组,数组中有两个字符串也就是需要依赖的模块名称。 AMD 会以一种非阻塞的方式,通过 appendChild 将这两个模块插入到 DOM 中。在两个模块都加载成功之后,define 会调用第二个参数中的回 调函数,一般是函数主体。

第二个参数也就是回调函数,函数接受了两个参数,正好跟前一个数组里 面的两个模块名一一对应。因为这里只是一种参数注入,所以我们使用自 己喜欢的名称也是完全没问题的。 同时,define 既是一种引用模块的方式,也是定义模块的方式。 所以我们可以看到,AMD 优先照顾浏览器的模块加载场景,使用了异步 加载和回调的方式,这跟 CommonJS 是截然不同的。 接下来说说 AMD 规范的 API:

define()函数

本规范只定义了一个函数"define", 它是全局变量。函数的描述为:

define(id?, dependencies?, factory)id

第一个参数, id 是个字符串。它指的是定义模块的名字, 这个参数是可选 的。如果没有提供该参数, 模块的名字应该默认为模块加载器请求的指定 脚本的名字。如果提供了该参数, 模块名必须是"顶级" 的和绝对的(不允许相对名字)。

第二个参数, dependdencies 是个定义中模块所依赖模块的数组。依赖模块 必须根据模块的工厂方法优先级执行,并且执行的结果应该按照依赖数组 中的位置顺序以参数的形式传入(定义中模块的)工厂方法中。依赖的模块 名如果是相对的,应该解析为相对定义中的模块。换句话来说,相对名解析 为相对于模块的名字,并非相对于寻找该模块的名字的路径。

本规范定义了三种特殊的依赖关键字。如果"require", "exports", 或 "module"出现在依赖列表中, 参数应该按照 CommonJS 模块规范自由变量 去解析。 依赖参数是可选的,如果忽略此参数,它应该默认为["require", "exports", "module"]。然而,如果工厂方法的形参个数小于 3,加载器会选择以函数 指定的参数个数调用工厂方法。

第三个参数, factory, 为模块初始化要执行的函数或对象。如果为函数,它 应该只被执行一次。如果是对象,此对象应该为模块的输出值。 如果工厂方法返回一个值(对象, 函数, 或任意强制类型转换为 true 的值), 应该为设置为模块的输出值。 简单的 CommonJS 转换 如果依赖性参数被忽略,模块加载器可以选择扫描工厂方法中的 require 语 句获得依赖性(字面量形参为 require("module-id"))。

第一个参数必须字面 量为 require 从而使此机制正常工作。 在某些情况下,因为脚本大小的限制或函数不支持 toString 方法(Opera Mobile 是已知的不支持函数的 toString 方法),模块加载器可以选择扫描不 扫描依赖性。 如果有依赖参数, 模块加载器不应该在工厂方法中扫描依赖性。

define.amd 属性

为了清晰的标识全局函数(为浏览器加载script必须的)遵从AMD编程接口, 任何全局函数应该有一个"amd"的属性, 它的值为一个对象。这样可以防 止与现有的定义了 define 函数但不遵从 AMD 编程接口的代码相冲突。

当前,define.amd 对象的属性没有包含在本规范中。实现本规范的作者, 可以用它通知超出本规范编程接口基本实现的额外能力。

define.amd 的存在表明函数遵循本规范。如果有另外一个版本的编程接口, 那么应该定义另外一个属性,如 define.amd2, 表明实现只遵循该版本的编 程接口。

一个如果定义同一个环境中允许多次加载同一个版本的模块的实现:

define.amd = {
    multiversion: true
}

最简单的定义:

define.amd = {}
//一次输出多个模块

在一个脚本中可以使用多次 define 调用。这些 define 调用的顺序不应该是 重要的。早一些的模块定义中所指定的依赖,可以在同一脚本中晚一些定 义。模块加载器负责延迟加载未接解决的依赖,直到全部脚本加载完毕, 防止没必要的请求。

例子:使用 require 和 exports 创建一个名为"alpha"的模块,使用了 require, exports 和名为"beta"的模块:

define("alpha", 
        ["require", "exports", "module"], 
        function (require,
            exports, beta) {
            exports.verb = function () {
                return beta.verb()
                 // Or:
            return requre("beta").verb()
         }
    }
)

一个返回对象的匿名模块:

define(["alpha"], function (alpha) {
    return {
        verb: function () {
            return alpha.verb() + 2
        }
    }
})

一个没有依赖性的模块可以直接定义对象:

define({
    add: function (x, y) {
        return x + y
    }
 })

一个使用了简单 CommonJS 转换的模块定义:

define(function(require, exports, module) {
    var a = require("a")
    b = require("b")
    exports.action = function () {}
})

本规范保留全局变量"define"以用来实现本规范。包额外信息异步定义编 程接口是未将来的 CommonJS API 保留的。模块加载器不应在此函数添加 额外的方法或属性。

本规范保留全局变量"require"被模块加载器使用。模块加载器可以在合适 的情况下自由地使用该全局变量。它可以使用这个变量或添加任何属性以 完成模块加载器的特定功能。它同样也可以选择完全不使用"require"。

UMD(2011)

模块格式的明显对抗甚至在 AMD 与 CommonJS 模块分离之前就开始了。 当时 AMD 阵营已经有很多开发人员喜欢使用模块化代码的最小入门门槛。 由于 Node.JS 的日益普及和 Browserify 的出现,CommonJS 模块的支持者 数量也迅速增长。

所以有两种格式,彼此无法相处。在没有代码修改的情况下,AMD 模块不 能用于实现 CommonJS 模块规范的环境中。CommonJS 模块不能与使用 AMD 作为主要格式(require.js,curl.js)的加载器一起使用。整个 JavaScript 生态系统都是一个糟糕的情况。

已经开发了 UMD 格式来解决该问题。UMD 代表通用模块定义,因此这种 格式允许您使用与 AMD 工具相同的模块以及 CommonJS 环境。

ECMA2015 模块(2015)

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在 成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构 成:export 和 import。export 命令用于规定模块的对外接口,import 命令 用于输入其他模块提供的功能。

/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
    return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
    ele.textContent = add(99 + basicNum);
}

如上例所示,使用 import 命令的时候,用户需要知道所要加载的变量名或 函数名。其实 ES6 还提供了 export default 命令,为模块指定默认输出,对 应的 import 语句不需要使用大括号。这也更趋近于 ADM 的引用写法:

/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
    ele.textContent = math.add(99 + math.basicNum);
}

ES6 的模块不是对象,import 命令会被 JavaScript 引擎静态分析,在编译 时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。 也正因为这个,使得静态分析成为可能。

ES6 模块与 CommonJS 模块的差异:

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内 部的变化就影响不到这个值。 ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时 候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执 行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说, ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加 载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块 里面的变量绑定其所在的模块。

2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块, 生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时 加载”。 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代 码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出 值,而不是加载整个模块,这种加载称为“编译时加载”。 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在 脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静 态定义,在代码静态解析阶段就会生成。

模块化的扩展

从语言层面到文件层面的模块化

从 1999 年开始,模块化探索都是基于语言层面的优化,真正的革命从 2009 年 CommonJS 的引入开始,前端开始大量使用预编译。

模块化历史的方案都是逻辑模块化,从 CommonJS 方案开始前端把服务 端的解决方案搬过来之后,算是看到标准物理与逻辑统一的模块化。但之 后前端工程不得不引入模块化构建这一步。正是这一步给前端开发无疑带 来了诸多的不便,尤其是现在我们开发过程中经常为了优化这个工具带了 很多额外的成本。

为什么模块化方案这么晚才成型,可能早期应用的复杂度都在后端,前端 都是非常简单逻辑。后来 Ajax 火了之后,web app 概念的开始流行,前 端的复杂度也呈指数级上涨,到今天几乎和后端接近一个量级。工程发展 到一定阶段,要出现的必然会出现。

前端三剑客的模块化展望

从 js 模块化发展史,我们还看到了 css html 模块化方面的严重落后,如今依赖编译工具的模块化增强在未来会被标准所替代。

原生支持的模块化,解决 html 与 css 模块化问题正是以后的方向。

再回到 JS 模块化这个主题,开头也说到是为了构建 scope,实则提供了 业务规范标准的输入输出的方式。但文章中的 JS 的模块化还不等于前端 工程的模块化,Web 界面是由 HTML、CSS 和 JS 三种语言实现,不论是 CommonJS 还是 AMD 包括之后的方案都无法解决 CSS 与 HTML 模块 化的问题。

对于 CSS 本身它就是 global scope,因此开发样式可以说是喜忧参半。近几年也涌现把 HTML、CSS 和 JS 合并作模块化的方案,其中 react/cssmodules 和 vue 都 为 人 熟 知 。 当 然 , 这 一 点 还 是 非 常 依 赖 于 webpack/rollup 等构建工具,让我们意识到在 browser 端还有很多本质的 问题需要推进。

对于 css 模块化,目前不依赖预编译的方式是 styled-component,通过 js 动态创建 class。而目前 css 也引入了与 js 通信的机制 与 原生变量支 持。未来 css 模块化也很可能是运行时的,所以目前比较看好 styledcomponent 的方向。

对于 html 模块化,尤大神最近爆出与 chrome 小组调研 html Modules,如 果 html 得到了浏览器,编辑器的模块化支持,未来可能会取代 jsx 成为 最强大的模块化、模板语言。 对于 js 模块化,最近出现的 <script type="module"> 模块化加载方式,虽然还没有得到浏览器原生支持,但也是我比较看好的 未来趋势,这样就连 webpack 的拆包都不需要了,直接把源代码传到服务器,配合 http2.0 完美抛开预编译的枷锁。

上述三中方案都不依赖预编译,分别实现了 html、css、js 模块化,相信 这就是未来。

模块化标准推进速度仍然缓慢

2015 年提出的标准,在 17 年依然没有得到实现,即便在 nodejs 端。

这几年 TC39 对语言终于重视起来了,慢慢有动作了,但针对模块标准制 定的速度,与落实都非常缓慢,与 javascript 越来越流行的趋势逐渐脱节。 nodejs 至今也没有实现 ES2015 模块化规范,所有 jser 都处在构建工具 的阴影下。

Http 2.0 对 js 模块化的推动

js 模块化定义的再美好,浏览器端的支持粒度永远是瓶颈,http 2.0 正是 考虑到了这个因素,大力支持了 ES 2015 模块化规范。

幸运的是,模块化构建将来可能不再需要。随着 HTTP/2 流行起来,请求 和响应可以并行,一次连接允许多个请求,对于前端来说宣告不再需要在 开发和上线时再做编译这个动作。

几年前,模块化几乎是每个流行库必造的轮子(YUI、Dojo、Angular),大 牛们自己爽的同时其实造成了社区的分裂,很难积累。有了 ES2015 Modules 之后,JS 开发者终于可以像 Java 开始者十年前一样使用一致的 方式愉快的互相引用模块。

不过 ES2015 Modules 也只是解决了开发的问题,由于浏览器的特殊性, 还是要经过繁琐打包的过程,等 Import,Export 和 HTTP 2.0 被主流浏览 器支持,那时候才是彻底的模块化。

Http 2.0 后就不需要构建工具了吗?

看到大家基本都提到了 HTTP/2,对这项技术解决前端模块化及资源打包 等工程问题抱有非常大的期待。很多人也认为 HTTP/2 普及后,基本就没 有 Webpack 什么事情了。

不过 Webpack 作者 @sokra 在他的文章 webpack & HTTP/2 里提到了一 个新的 Webpack 插件 AggressiveSplittingPlugin。简单的说,这款插件就是 为了充分利用 HTTP/2 的文件缓存能力,将你的业务代码自动拆分成若干 个数十 KB 的小文件。后续若其中任意一个文件发生变化,可以保证其他 的小 chunck 不需要重新下载。

可见,即使不断的有新技术出现,也依然需要配套的工具来将前端工程问 题解决方案推向极致。

总结

只要遵循了最新模块化规范,就可以使项目具有最好的可维护性吗? Js 模块化的目的是支持前端日益上升的复杂度,但绝不是唯一的解决方案。

分析下 JavaScript 为什么没有模块化,为什么又需要模块化:这个 95 年 被设计出来的时候,语言的开发者根本没有想到它会如此的大放异彩,也 没有将它设计成一种模块化语言。按照文中的说法,99 年也就是 4 年后 开始出现了模块化的需求。如果只有几行代码用模块化是扯,初始的 web 开发业务逻辑都写在 server 端,js 的作用小之又小。而现在 spa 都出现 了,几乎所有的渲染逻辑都在前端,如果还是没有模块化的组织,开发过 程会越来越难,维护也是更痛苦。

文中已经详细说明了模块化的发展和优劣,这里不准备做过多的讨论。我 想说的是,在模块化之后还有一个模块间耦合的问题,如果模块间耦合度 大也会降低代码的可重用性或者说复用性。所以也出现了降低耦合的观察 者模式或者发布/订阅模式。这对于提升代码重用,复用性和避免单点故障 等都很重要。

猜你喜欢

转载自blog.csdn.net/weixin_40851188/article/details/89844706