JS模块化进程

一、上古时代

第一步

只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。Global全局污染,容易命名冲突

function foo(){
    //...
}
function bar(){
    //...
}

第二步 Namespace

虽然减少了Global变量的数量,但是这样的写法会暴露所有模块成员,内部状态可以被外部改写。

var name = {
    foo: function(){
        
    },
    bar: function(){
        
    }
}

// 使用
name.foo();

第三步 IIFE模式

闭包模式,通过立即执行函数写法,可以达到不暴露私有变量的目的。且外部无法访问内部的值。

var module = (function(){
    var _private = 'sss';
    var foo = function(){
        
    }   
    return {
        foo: foo
    }
})();

module.foo();
module._private; // undefined

引入依赖

// 改进一些
var Module = (function($){
    var _$body = $("body");     // we can use jQuery now!
    var foo = function(){
        console.log(_$body);    // 特权方法
    }

    // Revelation Pattern
    return {
        foo: foo
    }
})(jQuery)

Module.foo();
// 依赖其他
var module1 = (function (mod){

  mod.m3 = function () {
    //...
  };

  return mod;

})(module1);
// 其他扩展
var MODULE = (function (my) {
  // add capabilities...
  return my;
}(MODULE || {}));
// 可以传入空对象
var MODULE = (function (my) {
  var old_moduleMethod = my.moduleMethod;

  my.moduleMethod = function () {
    // 方法重载
    // 可通过 old_moduleMethod 调用以前的方法...
  };

  return my;
}(MODULE));
// 有时我们要求在扩展时调用以前已被定义的方法,这也有可能被用于覆盖已有的方法。这时,对模块的定义顺序是有要求的。
  • 这就是模块模式,也就是现代模块实现的基石

二、远古时代

Script Loader
通过script标签引入一堆的依赖,按序执行。同样难以维护,依赖过多。

body
    script(src="zepto.js")
    script(src="jhash.js")
    script(src="fastClick.js")
    script(src="iScroll.js")
    script(src="underscore.js")
    script(src="handlebar.js")
    script(src="datacenter.js")
    script(src="deferred.js")
    script(src="util/wxbridge.js")
    script(src="util/login.js")
    script(src="util/base.js")
    script(src="util/city.js")
    script(src="util/date.js")
    script(src="util/cookie.js")
    script(src="app.js")
这样的写法有很大的缺点。首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

三、中古时代

CommonJS 征服世界的第一步是跳出浏览器。

CommonJS规范的提出,主要时为了弥补js没有标准的缺陷,使得通过CommonJS Api编写的应用可以在不同的宿主环境中执行。

因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。因此Node就在Commonjs的基础上应运而生了。

NodeJSCommonJS规范的实现,webpack 也是以CommonJS的形式来书写】

三、近代奇兵

AMD/CMD 浏览器环境模块化方案

AMD(Asynchronous Module Definition:异步模块定义)RequireJS 在推广过程中对模块定义的规范化产出。

CMD(Common Module Definition:公共模块定义)SeaJS 在推广过程中对模块定义的规范化产出。

1. AMD

有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。下面的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?

var math = require('math');
math.add(2, 3);

第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,在浏览器就需要"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js


Require.JS

Require.JS 的基本功能"模块化加载"。

实例1

// math1.js
/*
    语法结构:
    1. define({函数方法})
*/
// 一个没有依赖性的模块可以直接定义对象

define({
  name: '测试',
  add: function(num1, num2) {
    return num1+num2;
  }
})
// test1.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="./lib/require.js" charset="utf-8"></script>
    <script type="text/javascript">
      require(['math1'], function(math) {
        console.log(math.name);
        console.log(math.add(1,3));
      })
    </script>
  </body>
</html>

实例二

// math2.js
/*
    语法结构:
    2. define([引入其他模块地址],回调函数(引入模块别名));
    别名可以在函数里面去调用其他模块提供的方法
*/
// 一个返回对象的匿名模块
define(['math1'], function(math) {
  var sub = function(num1,num2) {
    return num1 - num2;
  }

  return {
    add: math.add,
    sub: sub
  }
})
// test2.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="./lib/require.js" charset="utf-8"></script>
    <script type="text/javascript">
      require(['math2'], function(math) {
        console.log(math.sub(4,2));
      })
    </script>
  </body>
</html>

实例三

// math3.js
// 定义一个命名模块

define('mymath', ['math2'], function(math) {
  var multiplication = function(num1, num2) {
    return num1 * num2;
  }
  return {
    add: math.add,
    sub: math.sub,
    mult: multiplication
  }
})
// test3.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <script src="./lib/require.js" charset="utf-8"></script>
    <script type="text/javascript">
    //需要配置一下引入的模块的地址
      require.config({
        paths:{
          'mymath': 'math3'
        }
      });
      require(['mymath'], function(math){
        console.log(math.mult(3,5));
      })
    </script>
  </body>
</html>

下图有一些注意事项,但是截图不是本实例的截图

实例四

// math4.js
// 一个使用了简单CommonJS转换的模块定义
define(function(require,exports,module){
    // 引入其他模块
    var math = require('js/1_math');
    console.log(math);

    // 导出(暴露方法:2种方式)
    // 第一种
    // exports.a = math.add;
    // 第二种
    module.exports = {
        a : math.add
    }
});
// test4.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    
</body>
<script type="text/javascript" src="js/require.js"></script>
<script type="text/javascript">
require(['js/4_math'],function(mytool){
    console.log(mytool.a(11,22));//33
});
</script>
</html>


一个小demo
  • 目录结构

图片描述

  • 代码
// index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
      <h1>My Sample Project</h1>
      <script src="./script/require.js" data-main="./script/main" charset="utf-8"></script>
  </body>

</html>
// main.js
require.config({
  baseUrl: 'script',
  paths: {
    math: './lib/math1',
    utils: './utils/utils',
    jquery: './lib/jquery.min'
  }
});

require(['math','utils','jquery'], function(math, utils, $) {
  $(function(){
    console.log(math.add(1,2));
    console.log(utils.utils.sub(4,2));
    alert('在加载完math、utils、jquery之后,我执行了');
  })
})
//math.js 
define({
  add:function(num1, num2) {
    return num1+num2;
  }
});
// utils.js
define(function(require, exports, module) {
  var utils = {
    sub:function(num1, num2) {
      return num1-num2;
    }
  };

  module.exports = {
    utils: utils
  }
})

图片描述


2.CMD

SeaJS 是一个适用于 Web 浏览器端的模块加载器。这里就先不写了~

大家可以参考文档学习

SeaJs中文


四、展望未来

面向未来的ES6模块标准

2015年6月,ECMAScript2015也就是ES6发布了,JavaScript终于在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS、AMD之类的需要在运行时才能确定(例如FIS这样的工具只能预处理依赖关系,本质上还是运行时解析),成为浏览器和服务器通用的模块解决方案。

更多关于ES6 Modules的资料,可以看一下ES6阮一峰


感谢您的支持!!

Meils
1.6k 声望157 粉丝

前端开发实践者


引用和评论

0 条评论