乐趣区

关于前端:JavaScript-模块化发展历程

ES module(ESM)为 JavaScript 带来了标准化模块零碎,在 Firefox 60 公布之后,所有的支流浏览器就都反对 ESM 了,更加值得开心的是从 Node.js v13.2.0 开始,曾经能够间接在 Node 中应用 ESM。

然而,JavaScript 在这条模块标准化的路线上却破费了很长的工夫,那么它的倒退历程是到底是怎么的?上面就来理解一下。

无模块化

因为晚期前端业务比较简单,JS 承当的业务较少,工程师可能轻易几行代码就搞定了,间接写在一个文件里即可,略微简单些的会分文件引入,而后手动保护加载程序,代码的大略样子如下:

moduleA.js:

function add(a, b) {return a + b;}

moduleB.js:

function average(a, b) {return add(a, b) / 2;
}

main.js:

var result = average(5, 10);
console.log(result);

而后在 HTML 中的引入如下:

<!-- ... 一些业务代码 -->
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script src="./main.js"></script>

能够看到这样写的话,moduleA.js、moduleB.js、main.js 这些文件里的函数变量等都会间接裸露在全局之中,很容易造成命名抵触,并且文件之间的依赖关系也很难确定,毕竟只依据一个办法名很难判断。

命名空间

为了解决命名抵触问题及团队成员间不便单干,这个时候提出了命名空间的思路,此时 moduleA.js, moduleB.js 的写法相似这样:

moduleA.js

var moduleA = {add: function (a, b) {return a + b;},
  foo: function () {},
  bar: 1,
};

moduleA.baz = function () {};

moduleB.js

var moduleB = {average: function (a, b) {return moduleA.add(a, b) / 2;
  },
};

引入命名空间后,命名抵触问题失去了肯定水平的解决,模块间的依赖也比拟容易的看进去,然而 moduleA 和 moduleB 外面的成员变量及办法都裸露了进去,外界能够任意的对其进行更改,无奈创立公有变量及公有办法,模块不够平安。

IIFE(自执行函数)

为了创立公有变量或公有办法,引入了自执行函数及闭包的思路,此时写法相似:

// moduleA.js
var moduleA = (function () {
  var bar = 1; // 公有属性
  var moduleA = window.moduleA || {};

  // 公有办法
  function getBar() {return bar;}

  moduleA.add = function (a, b) {return a + b;};

  moduleA.foo = function () {return getBar();
  };

    // 或者挂到 window 下:  window.moduleA = moduleA
  return moduleA;
})();

这样保障了局部变量及办法的安全性,另外还能够通过传参的形式传递模块的援用:

// moduleB.js
(function (modA) {var moduleB = window.moduleB || {};

  moduleB.average = function (a, b) {return modA.add(a, b) / 2;
  };

  window.moduleB = moduleB;
})(moduleA);

然而,此时问题仍然很多:

  • 各个模块仍然要创立全局变量净化全局
  • 编写模块无奈保障不影响其它模块
  • 模块间的援用及依赖关系还是不够清晰
  • 各个 JS 的加载程序须要手动治理

直到 Node.js 的到来、CommonJS 标准的落地,这成为了所有的转折点…

CommonJS

2009 年 1 月,Mozilla 的工程师 Kevin Dangoor 创立了一个我的项目,过后的名字是 ServerJS。在 2009 年 8 月,这个我的项目被改名为 CommonJS,以显示其 API 的更宽泛实用性,其中,Node.js 采纳的就是这个标准。

CommonJS 约定:

  • 每个文件就是一个模块,有本人的作用域
  • 每个文件中定义的变量、函数、类都是公有的,对其它文件不可见
  • 每个模块外部能够通过 exports 或者 module.exports 对外裸露接口
  • 每个模块通过 require 加载另外的模块

CommonJS 代码示例:

// moduleA.js
var foo = 1;

function add(a, b) {return a + b;}

function foo() {return foo;}

module.exports = {
  add: add,
  foo: foo,
};

// moduleB.js
const moduleA = require('./moduleA');

const result = moduleA.add(5, 10);
console.log(result);

然而,CommonJS 是一套同步的计划,因为 Node.js 次要运行在服务端,模块文件个别都曾经存在于本地硬盘,所以加载起来比拟快,不必思考非同步的加载形式,所以 CommonJS 标准比拟实用。但对于浏览器就无奈实用了,必须要有异步的加载机制。

AMD

即 Asynchronous Module Definition,简直与 CommonJS 同一期间,AMD 标准呈现了,它采纳异步的形式加载 JavaScript 模块,模块的加载并不会影响它前面语句的运行。

AMD 标准规定用 define 定义模块用 require 加载模块,语法如下:

# 模块定义
define(id?, dependencies?, factory);

# 模块加载
require([module], callback);

说起 AMD 标准,不得不提其代表产物 RequireJS,它不仅实现了异步加载,还能够按需加载,一时间成为了泛滥我的项目的抉择,RequireJS + jQuery 简直成为了前端我的项目标配。

上面就来看下基于 RequireJS 的代码是什么样子的。

目录构造如下:

.
├── index.html
└── js
    ├── entry.js
    ├── handleClick.js
    ├── lib
    │   ├── jquery.js
    │   └── require.js
    ├── main.js
    ├── moduleA.js
    ├── moduleB.js
    └── utils
        └── index.js

而后是在 HTML 文件中引入 require.js 并申明 data-main 属性:

<!-- ... -->
<body>
  <button id="btn-click">click me</button>
  <script data-main="./js/main.js" src="./js/lib/require.js"></script>
</body>

data-main 属性的作用是指定我的项目的 JS 主模块,这个模块会被第一个加载,并且能够对模块进行一些门路、别名等配置。

// main.js
require.config({
  baseUrl: 'js/',
  paths: {
    jquery: './lib/jquery',
    utils: './utils/index',
  },
});

require(['./entry'], function (entry) {entry.init();
});

这里对 baseUrl 进行指定,require 模块时就不必每次都写 ./js/ 门路,同理 utils: './utils/index'utils 模块进行了别名申明也能起到同样的成果。

模块定义示例:

// moduleA.js
define(['utils', './moduleA', './moduleB'], function (utils, moduleA, moduleB) {console.log('---- entry.js utils', utils);
  console.log('---- entry.js moduleA', moduleA);
  console.log('---- entry.js moduleB', moduleB);
  return {init: function () {moduleB.init();
      console.log('entry.js', 'init');
    },
  };
});

JS 文件的加载程序会依照模块的依赖申明程序进行加载,例如下面代码中依赖模块的加载程序就是 utils/index.js -> moduleA.js -> moduleB.js。当然 RequireJS 会对加载过的模块进行缓存,如果有屡次依赖,就只加载一次。

另外,RequireJS 也能够实现模块的懒加载,只须要在须要时再 require 模块即可,代码示例如下:

define(['jquery'], function ($) {
  return {init: function () {var $btn = $('#btn-click');
      $btn.click(function () {
          // 事件触发时再加载
        require(['./handleClick'], function (handleClick) {handleClick.init($btn);
        });
      });
    },
  };
});

下面代码在事件触发时才加载须要的 handleClick.js 文件,这样就实现了 JS 文件的懒加载,不必页面刚进入时就加载过多的 JS 文件。然而代码却不够优雅,这种回调看起来太难受了。

CMD

即 Common Module Definition,以玉伯大大的 Sea.js 为代表,SeaJS 要解决的问题和 RequireJS 一样,都是浏览器端的模块加载计划,只不过在模块定义形式和执行机会上有所不同,AMD 推崇 依赖前置、提前执行,CMD 推崇依赖就近、提早执行

上面来看下 Sea.js 的我的项目代码大略是什么样子的。

目录构造如下:

├── index.html
└── js
    ├── entry.js
    ├── handleClick.js
    ├── lib
    │   ├── jquery.js
    │   └── sea.js
    ├── main.js
    ├── moduleA.js
    ├── moduleB.js
    └── utils
        └── index.js

首先在 HTML 文件中引入 sea.js 及 main.js:

<!-- ... -->
<body>
  <button id="btn-click">click me</button>
  <script src="./js/lib/sea.js"></script>
  <script src="./js/main.js"></script>
</body>

这里的 main.js 作为页面入口文件,对 SeaJS 进行了配置 baseUrl 配置及别名配置,并且通过 seajs.use() 加载入口 JS 文件:

seajs.config({
  base: './js/',
  alias: {
    jquery: 'lib/jquery',
    utils: 'utils/index',
  },
});

seajs.use(['jquery', 'entry'], function ($, entry) {entry.init();
});

之后就是模块的定义及援用了,以 moduleA.js 为例:

define(function (require, exports, module) {
  return {a: function () {var utils = require('utils');

      utils.formatDate();
      console.log('moduleA.js');
    },
  };
});

这里就近援用 utils 模块,而后代码执行到此地位时 utils 模块相干的代码才会执行。

UMD

即 Universal Module Definition,UMD 是为了兼容浏览器、Node.js 等多种环境所产生的一种模块标准,经典的代码片段如下:

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? (module.exports = factory())
    : typeof define === 'function' && define.amd
    ? define(factory)
    : (global.mylib = factory());
})(this, function () {
  'use strict';
  var mylib = {};
  mylib.version = '0.0.1';
  mylib.say = function (message) {console.log(message);
  };
  return mylib;
});

合乎 UMD 标准的 JS 模块既能够在 Node.js 中运行,也能够作为 AMD 模块运行,否则就挂载到以后的上下文环境中,如浏览器中的 window。

ES Module

2015 年,ES6 公布,JavaScript 终于在语言规范层面上有了本人的模块零碎,ES6 应用 import 引入模块,应用 export 导出模块。

援用模块:

import moduleA from './moduleA';
import {foo} from './moduleB';

moduleA();
foo();

document.getElementById('btn-click').onclick = () => {import('./handleClick').then(({handleClick}) => {handleClick();
  });
};

导出模块:

// moduleA.js 默认导出
export default function () {console.log('moduleA');
}

// moduleB.js 具名导出
export const NAME = 'moduleB';

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

export function bar() {console.log('bar');
}

ES6 模块的设计思维是尽量的动态化,使得编译时就能确定模块的依赖关系,以及输出和输入的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些货色。比方,CommonJS 模块就是对象,输出时必须查找对象属性。

另外,CommonJS 模块输入的是值的拷贝,而 ES6 模块输入的是值的援用,并且 CommonJS 模块是运行时加载,而 ES6 模块是编译时输入接口,基于这个个性,ES6 模块就很容做动态剖析,比方在 Webpack 打包构建时通过tree shaking 去除无用代码缩小代码体积。

瞻望

当初曾经是 React、Vue 的时代了,ES Module 曾经成为了标配,模块化、组件化在这个时代失去更好的实际,尽管目前在理论我的项目中 ES Module 依然须要通过 Webpack、Babel 等做编译解决,但最新的浏览器和 NodeJS 曾经间接反对 ES Module 了,置信在不久的将来,肯定又是一个新的时代。

本文代码示例:https://github.com/verlime/javascript-modules-history

相干参考

  • 精读 js 模块化倒退
  • 前端模块化开发那点历史
  • CommonJS 标准
  • ES6 入门教程
  • ES modules: A cartoon deep-dive – Mozilla Hacks – the Web developer blog
  • Announcing core Node.js support for ECMAScript modules
退出移动版