Javascript模块化的演进历程

ES2015 在2015年6月正式发布,官方终于引入了对于模块的原生支持,现在 JS 的模块化开发很是的方便、天然,但这个新规范仅仅持续了3年。就在7年前,JS 的模块化还停留在运行时的支持;13年前,经过后端模版定义、注释定义模块依赖。对于经历过的人来讲,历史的模块化方式还停留在脑海中,久久不能忘怀。javascript

为何须要模块化

模块,是为了完成某一功能所需的程序或者子程序。模块是系统中“职责单一”且“可替换”的部分。所谓的模块化就是指把系统代码分为一系列职责单一且可替换的模块。模块化开发是指如何开发新的模块和复用已有的模块来实现应用的功能。前端

那么,为何须要模块化呢?主要是如下几个方面的缘由:java

  • 传统的网页开发正在转变成 Web Apps 开发
  • 代码复杂度在逐步增高,随着 Web 能力的加强,愈来愈多的业务逻辑和交互都放在 Web 层实现
  • 开发者想要分离的JS文件/模块,便于后续代码的维护性
  • 部署时但愿把代码优化成几个 HTTP 请求

模块化的演进历程

直接定义依赖(1999)

最早尝试将模块化引入到 JS 中是在1999年,称做“直接定义依赖”。这种方式实现模块化简单粗暴 —— 经过全局方法定义、引用模块。Dojo 就是使用这种方式进行模块组织的,使用dojo.provide 定义一个模块,dojo.require 来调用在其它地方定义的模块。git

咱们在dojo 1.6中修改下示例:程序员

// greeting.js 文件
dojo.provide("app.greeting");

app.greeting.helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

app.greeting.sayHello = function (lang) {
    return app.greeting.helloInLang[lang];
};

// hello.js 文件
dojo.provide("app.hello");

dojo.require('app.greeting');

app.hello = function(x) {
    document.write(app.greeting.sayHello('es'));
};
复制代码

直接定义依赖的方式和commonjs很是相似,区别是它能够在任何文件中定义模块,模块不和文件进行关联。而在commonjs中,每个文件便是一个模块。github

namespace模式(2002)

最先,咱们这么写代码:typescript

function foo() {

}

function bar() {

}
复制代码

不少变量和函数会直接在 global 全局做用域下面声明,很容易产生命名冲突。因而,命名空间模式被提出。好比:npm

var app = {};

app.foo = function() {

}

app.bar = function() {

}

app.foo();
复制代码

匿名闭包 IIFE 模式(2003)

尽管 namespace 模式必定程度上解决了 global 命名空间上的变量污染问题,可是它没办法解决代码和数据隔离的问题。后端

解决这个问题的先驱是:匿名闭包 IIFE 模式。它最核心的思想是对数据和代码进行封装,并经过提供外部方法来对它们进行访问。一个基本的例子以下:浏览器

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;
}());

复制代码

模板依赖定义(2006)

模板依赖定义是接下来的一个用于解决模块化问题的方案,它须要配合后端的模板语法一块儿使用,经过后端语法聚合 js 文件,从而实现依赖加载。

它最开始被应用到 Prototype 1.4 库中。05年的时候,Sam Stephenson 开始开发 Prototype 库,Prototype 当时是做为 Ruby on rails 的一个组成部分。因为 Sam 平时使用 Ruby 不少,这也难怪他会选择使用 Ruby 里的 erb 模板做为 JS 模块的依赖管理了。

模板依赖定义的具体作法是:对于某个 JS 文件而言,若是它依赖其它 JS 文件,则在这个文件的头部经过特殊的标签语法去指定以来。这些标签语法能够经过后端模板(erb, jinjia, smarty)去解析,和特殊的构建工具如 borshik 去识别。只得一提的是,这种模式只能做用于预编译阶段。

下面是一个使用了 borshik 的例子:

// app.tmp.js 文件

/*borschik:include:../lib/main.js*/

/*borschik:include:../lib/helloInLang.js*/

/*borschik:include:../lib/writeHello.js*/

// main.js 文件
var app = {};

// helloInLang.js 文件
app.helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

// writeHello.js 文件
app.writeHello = function (lang) {
    document.write(app.helloInLang[lang]);
};

复制代码

注释定义依赖(2006)

注释定义依赖和1999年的直接定义依赖的作法很是相似,不一样的是,注释定义依赖是以文件为单位定义模块了。模块之间的依赖关系经过注释语法进行定义。

一个应用若是想用这种方式,能够经过预编译的方式(Mootools),或者在运行期动态的解析下载下来的代码(LazyJS)。

// helloInLang.js 文件
var helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

// sayHello.js 文件

/*! lazy require scripts/app/helloInLang.js */

function sayHello(lang) {
    return helloInLang[lang];
}

// hello.js 文件

/*! lazy require scripts/app/sayHello.js */

document.write(sayHello('en'));
复制代码

依赖注入(2009)

2004年,Martin Fowler为了描述Java里的组件之间的通讯问题,提出了依赖注入概念。

五年以后,前Sun和Adobe员工Miško Hevery(JAVA程序员)开始为他的创业公司设计一个 JS 框架,这个框架主要使用依赖注入的思想却解决组件之间的通讯问题。后来的结局你们都知道,他的项目被 Google 收购,Angular 成为最流行的 JS 框架之一。

// greeting.js 文件
angular.module('greeter', [])
    .value('greeting', {
        helloInLang: {
            en: 'Hello world!',
            es: '¡Hola mundo!',
            ru: 'Привет мир!'
        },

        sayHello: function(lang) {
            return this.helloInLang[lang];
        }
    });

// app.js 文件
angular.module('app', ['greeter'])
    .controller('GreetingController', ['$scope', 'greeting', function($scope, greeting) {
        $scope.phrase = greeting.sayHello('en');
    }]);
复制代码

CommonJS模式 (2009)

09年 CommonJS(或者称做 CJS)规范推出,而且最后在 Node.js 中被实现。

// greeting.js 文件
var helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

var sayHello = function (lang) {
    return helloInLang[lang];
}

module.exports.sayHello = sayHello;

// hello.js 文件
var sayHello = require('./lib/greeting').sayHello;
var phrase = sayHello('en');

console.log(phrase);
复制代码

这里咱们发现,为了实现CommonJS规范,咱们须要两个新的入口 -- requiremodule,它们提供加载模块和对外界暴露接口的能力。

值得注意的是,不管是 require 仍是 module,它们都是语言的关键字。在 Node.js 中,因为是辅助功能,咱们可使用它。在将Node.js中的模块发送给 V8 以前,会经过下面的函数进行包裹:

(function (exports, require, module, __filename, __dirname) {
    // ...
    // 模块的代码在这里
    // ...
});
复制代码

就目前来看,CommonJS规范是最多见的模块格式规范。你不只能够在 Node.js 中使用它,并且能够借助 Browserfiy 或 Webpack 在浏览器中使用。

AMD 模式(2009)

CommonJS的工做在全力的推动,同时,关于模块的异步加载的讨论也愈来愈多。主要是但愿解决 Web 应用的动态加载依赖,相比于 CommonJS,体积更小。

// lib/greeting.js 文件
define(function() {
    var helloInLang = {
        en: 'Hello world!',
        es: '¡Hola mundo!',
        ru: 'Привет мир!'
    };

    return {
        sayHello: function (lang) {
            return helloInLang[lang];
        }
    };
});

// hello.js 文件
define(['./lib/greeting'], function(greeting) {
    var phrase = greeting.sayHello('en');
    document.write(phrase);
});
复制代码

虽然 AMD 的模式很适合浏览器端的开发,可是随着 npm 包管理的机制愈来愈流行,这种方式可能会逐步的被淘汰掉。

ES2015 Modules(2015)

ES2015 modules(简称 ES modules)就是咱们目前所使用的模块化方案,它已经在Node.js 9里原生支持,能够经过启动加上flag --experimental-modules使用,不须要依赖 babel 等工具。目前尚未被浏览器实现,前端的项目可使用 babel 或 typescript 提早体验。

// lib/greeting.js 文件
const helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

export const greeting = {
    sayHello: function (lang) {
        return helloInLang[lang];
    }
};

// hello.js 文件
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
复制代码

总结

JS 模块化的演进历程让人感慨,感谢 TC 39 对 ES modules 的支持,让 JS 的模块化进程终于能够告一段落了,但愿几年后全部的主流浏览器能够原生支持 ES modules。

JS 模块化的演进另外一方面也说明了 Web 的能力在不断加强,Web 应用日趋复杂。相信将来 JS 的能力进一步提高,咱们的开发效率也会更加高效。


《IVWEB 技术周刊》 震撼上线了,关注公众号:IVWEB社区,每周定时推送优质文章。

相关文章
相关标签/搜索