JavaScript 模块化前世今生

375 阅读8分钟

你写你的,我写我的,模块化!

为啥需要JS模块化

你想想,如果你和另一个前端两个人共同开发一个项目,你负责实现A功能,他负责实现B功能,两个人加班加点各自完成了各自的功能,你写的文件是a.js,他写的文件是b.js。然后你们两个很开心,觉得大功告成了,然而将你们的代码加载到同一个项目时,发现,WTF,怎么各种报错。然后你们仔细看报错,检查代码,终于发现了问题,原来a.js里面存在了一个名叫render的函数,b.js里面也定义了一个名为render的函数,a.js里面暴露了一个名为slider的全局变量,b.js里也暴露了一个slider全局变量。。

这就是最大的一个问题,当多个人共同开发一个项目的时候,如何保证,别人写的代码不会和你的冲突,这就是JS模块化要解决的最大的问题。

模块化的远古时期

虽然现在ES6已经在语言层面实现了模块化,那在这之前,千千万万的JS开发者又是如何解决这个问题的呢?

函数封装

// 一个函数做一个功能
function method1() {}
function method2() {}

问题: 函数名就是全局变量,容易命名冲突

NameSpace 命名空间

var myJS = {
   var a = 1
   method1: function () {
     xxx
   }
   method2: function () {
    xxx
   }
}

myJS.method1() 
myJS.a = 2 // 外部可以修改内部成员

问题:外部能够随便修改这个对象,不够安全啊。

IIFE 模式

就是搞一个局部作用域,别人访问不到里面的数据。

var module = !function() {
    var a = 1
    var method1 = function () {
        console.log(a)
    }
    return {
        method1:method1
    }
}()

将你的代码全放在一个匿名函数里面,这样外部无法访问到你的代码和数据,不会出现命名冲突。 然后执行这个函数,将这些数据暴露给外部,这样外部就可以使用到这些数据。

JS 模块化近现代史

上面的思想和方法都是前人们在解决模块化的历史过程中留下的,可见,一门语言的进化是多么漫长而充满坎坷的,哈哈哈,我们看完远古时期,再看看近现代历史。 由于JS天生不支持模块化,那么就需要开发者们共同遵守一些开发规范,大家都按这个规范去写代码,这样就能够很好的实现模块化的效果。那么都有哪些规范呢?

  • CommonJS: Node环境下的规范
  • AMD: 浏览器环境下的规范 有 RequireJs
  • CMD: SeaJS

CommonJS

Node环境中,模块的写法是按照CommonJS规范写的,模块的规范核心就是,如何定义一个模块和如何加载一个模块。CommonJS 的规范 如下:

定义一个模块

一个JS文件就是一个模块,有自己的作用域,里面的代码数据是私有的,独立的,对其他文件不可见, 一个文件就是一个模块。

var a = 1
var add = function () {
    console.log(a)
}
console.log(module)  // module 就是当前模块
暴露数据

module表示当前模块,他是一个对象,对象有一些属性,其中,通过module.exports 属性将本模块的数据暴露出去。 一个模块只有一个出口,那就是 module.exports对象,将需要暴露的数据放在这个对象里就可以。

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

或者这样写:
module.exports = {
    x: x,
    addX: addX 
}
加载模块

require 命令加载一个模块

var module1 = require('./module1.js')  // 读取module1.js 并执行这个文件,返回值是 module1.js 的 exports对象

console.log(module1.x)
module1.addX()

CommonJS 是在Node开发中用的规范,CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。

Asynchronous Module Definition (AMD) :异步模块定义

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。

AMD本身是一种规范,而不是具体的实现,具体的实现有 RequireJsRequireJs是一个库,这个库的作用就是让浏览器能够解析你写的模块化的JS。 下面就是RequireJs的基本用法。

具体请参考RequireJs的官方文档:RequireJs

定义模块

语法: define(id,dependencies,factory)
define()函数接受三个参数:
id: 模块的名字,但一般不要,就使用文件名作为模块名
dependencies: 一个数组,成员是该模块的依赖,依赖参数是可选的,如果忽略此参数,它应该默认为["require", "exports", "module"]。
factory: 一个对象或是一个函数,代表该模块的输出,如果是对象,这个对象就是这个模块,如果是函数,这个函数得返回值就是 该模块得内容

示例1:定义一个简单得模块
define({
    name: "小飞"
})

示例2:通过函数返回值模块内容
define(function(){
    var a = 1,
    var getA = function(){
        console.log(a)
    }
    return getA   // 这就是这个模块得输出
})

示例3:定义一个有依赖的模块
define(['./moduleA','./moduleB'],function(a,b){ //函数的参数是 依赖的模块的输出
    return {
        method1: a.method1,
        nethod2: b.method2
    }
})

示例3: 如果一个模块的依赖有很多个,那么你可以这样去写
define(function(require){ // require 默认的一个参数,是一个函数,require()用于加载一个模块,require方法也可以用在define方法内部。
    var m2 = require('./module2')
    var m3 = require('./module3')
    return {
        m2:m2,
        m3:m3
    }
})
//  AMD保留了commonjs中的require、exprots、module这三个功能。可以不把依赖罗列在dependencies数组中。而是在代码中用require来引入
加载模块

使用 require() 函数加载一个模块,在加载模块之前,要搞清楚一个关键:baseUrl

requirejs 在加载模块文件的时候,路径是按照: baseUrl + path寻找加载的,baseUrl 的设置如下:

  1. baseUrl 默认是 data-main 的目录
  2. 没有使用data-main 和 指定 baseUrl ,目录就是引用 requirejsHTML所在目录。

require()函数用于加载模块: 可以接受三个参数:

  • 第一个参数: 是一个数组,成员是加载的模块
  • 第二个参数:是一个回调函数,函数的参数是 加载的模块的是输出,当依赖加载后就会执行 函数体就是你的代码
  • 第三个参数: 也是一个函数,用于处理错误的回调,如果出现错误这个函数就会被执行,接受一个error对象作为参数
require(['./module-JS/module1'],function (m1) {
    console.log(m1)
})

// require()可以在 define()函数内部使用,但注意要确保 依赖了 require 模块,例如
define(function(require){
    var mod = require("./relative/name")
})

还有很多很多用法和配置...,有兴趣的话看文档吧,我没有全部看完并实践,因为我的目的不是学会用RequireJs这个库,而是通过对模块化的演变而对JS模块化有一个完整的了解,我想这是很重要的。

Common Module Definition (CMD)

CMD SeaJS 在推广过程中对模块定义的规范化产出。在 Sea.js 中,所有 JavaScript 模块都遵循 CMDCommon Module Definition) 模块定义规范。该规范明确了模块的基本书写格式和基本交互规则。

JS 模块化的现在进行时

它来了,它来了,它带着Module走来了,这是真正的革命,是新的篇章,是一个全新的时代,哈哈哈,不皮了。 这就是,ES6终于新增语法,在语言层面就实现了模块化。

ES6中 模块化的是由 exportimport 两个语法实现的。

模块导出

输出变量:
export var a = 1; 
或者写成:
var a = 1; 
export {year}; (推荐)

输出函数:
export function f() {};
或者写成:
function f() {}
export {f};

模块导入

import { firstName, lastName, year } from './profile.js';

点击查看详情: ES6 Module语法

总结

JavaScript 的诞生就是为了实现一些简单的脚本验证,JS之父用了十天的时间就发明创造了这门语言,设计的初衷和导致了这门脚本语言必然存在着许许多多的不足,从JS模块化的发展看就能看到一门语言的发展和历史潮流是密不可分的,了解这些思想和演变我觉得比学会用一个库和工具更有价值。这是我在了解整个JS模块化的演变过程后,觉得要留下一点什么东西,于是,就写了这些文字。