JavaScript 之模块化篇

1,477 阅读9分钟

什么是模块化?

模块化就是把系统分离成独立功能的方法,这样我们需要什么功能,就加载什么功能。

优点:
可维护性:根据定义,每个模块都是独立的,良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。
可复用性:可以重复利用,而不用经常复制自己之前写过的代码

原始JS开发问题

1、污染全局变量//a.js 文件:

var test1='aaaaaa';
//b.js 文件
var test1='bbbbbb';
 <script>
    console.log('test1='+test1);//bbbbbb;
 
</script>
console test1 输出'bbbbbb';悲剧啊

2、命名冲突

//a.js 文件:
function fun(){
    console.log('this is b');
}
 //b.js 文件
 
function fun(){
    console.log('this is b');
}
//main.js 文件
<script src="a.js"></script>
<script src="b.js"></script>
<script>
    fun();//this is b;
</script>
小张在a.js定义了fun(),小李在b.js又定义了fun(),a,b被小王引入到main.js,执行fun(),输出this is b; 

3、依赖关系 b.js依赖a.js,标签的书写顺序必须是:

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>

这样在多人开发的时候很难协调啊,令人头疼的问题。

解决冲突的方式

1、使用java式的命名空间
2、变量前加“_”
3、对象写法

var module1={
    test1:'aaaaaa',
    fun:function(){
        console.log(this.test1);
    }
}
变量和函数封装在对象里面,使用时,调用对象的属性即可:
module1.fun();//aaaaaa
但是这样的写法会暴露所有模块成员,内部状态可以被外部改写,
module1.test1='cccccc';

4、匿名闭包函数

var  module1=(function(){
    var test1='aaaaaa';
    var fun=function(){
        console.log('this is a');
    }
    return{
        fun:fun
    }
}());

匿名函数有自己的作用域,这样外部代码无法读取 module1 function 里面的变量了,从而也不会修改变量或者是覆盖同名变量了,但是还是有缺陷的,module1这个的变量还是暴露到全局了,而去随着模块的增多,全局变量会越来越多。
5、全局引入
像jquery库使用的全局引入。和匿名闭包函数相似,只是传入全局变量的方法不同
(function(window){

var test1='aaaaaa';
window.testFun=function(){//通过给window添加属性而暴漏到全局
    console.log(test1);
}

}(window));

通过匿名函数包装代码,所依赖的外部变量传给这个函数,在函数内部可以使用这些依赖,然后在函数的最后把模块自身暴漏给window。

3,4,5解决方法都是通过定一个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。

本文着重介绍几种广受欢迎的解决方案:CommonJS,AMD,CMD,ES模块化。

CommonJs

根据CommonJs规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

commonJS中模块可以加载多次,但是只会在第一次加载的时候运行一次,然后运行结构被缓存,再次加载就是读取缓存的结果。

CommonJS规范加载模块是同步的,也就是说,加载完成才可以执行后面的操作,Node.js主要用于服务器编程,模块一般都是存在本地硬盘中,加载比较快,所以Node.js采用CommonJS规范。

CommonJS规范分为三部分:module(模块标识),require(模块引用), exports(模块定义),
module变量在每个模块内部,就代表当前模块;
exports属性是对外的接口,用于导出当前模块的方法或变量;
require()用来加载外部模块,读取并执行js文件,返回该模块的exports对象;

1、commonJs模块定义

module.exports定义模块:

//math.js
let add=(x,y)=>{
    return x+y;
}
let sub=(x,y)=>{
    return x-y;
}

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

exports 定义模块:

let add=(x,y)=>{
    return x+y;
}
let sub=(x,y)=>{
    return x-y;
}
exports.add=add;
exports.sub=sub;

注意:不可以直接对exports赋值,exports=add;

exports和module.exports有什么区别呢?
在每个模块中Node都提供了一个Module 对象,代表当前模块。

//console.log(Module);
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Users/zss/node-Demo/my-app/testNOde/b.js',
  loaded: false,
  children: [],
  paths: 
   [ '/Users/zss/node-Demo/my-app/testNOde/node_modules',
     '/Users/zss/node-Demo/my-app/node_modules',
     '/Users/zss/node-Demo/node_modules',
     '/Users/zss/node_modules',
     '/Users/node_modules',
     '/node_modules' 
     ] 
   }

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。为了方便,Node为每个模块提供一个exports变量,指向module.exports。我们把它们都打印出来看看究竟,

//test.js
console.log(module.exports);
console.log(exports);
console.log(module.exports===exports);

exports.test = ()=>{
    console.log('exports 1');
};
module.exports.test1 = ()=>{
    console.log('module.exports 1');
};
console.log(module.exports);
console.log(exports);

//输出:
{}
{}
true
{ test: [Function], test1: [Function] }
{ test: [Function], test1: [Function] }

从上例可以看出:
1.每个模块文件一创建,有个var exports = module.exports = {};使exports和module.exports都指向一个空对象。
**2.module是全局内置对象,exports是被var创建的局部对象,module.exports和exports所指向的内存地址相同
所有的exports收集到的属性和方法,都赋值给了Module.exports,最终返回给模块调用的是module.exports而不是exports。**

再举个例子:

//test.js
exports.test = ()=>{
    console.log('exports 1');
};
module.exports={
    test:function(){
        console.log('module.exports 1');
    },
    testmodule:()=>{
        console.log('module.exports 2')
    }
}
console.log(module.exports);
console.log(exports);

 
 //输出
{ test: [Function: test], testmodule: [Function: testmodule] }
{ test: [Function] }

//在index.js文件中调用test2.js
let a=require('./test2');
a.test();
a.testmodule();
//输出:
module.exports 1
module.exports 2

所有的exports收集到的属性和方法,都赋值给了Module.exports,当直接把函数和属性传给module.exports时,module.exports与exports不想等了,在调用时候,exports的属性和方法会被忽略,所以最终返回给模块调用的是module.exports而不是exports。

2、模块分类

NodeJs的模块分为两类:
一类是原生模块,例如http,fs,path 等等。node在加载原生模块的时候,不需要传入路径,NodeJs将原生模块的代码编译到了二进制执行文件中,加载速度快。
一类是文件模块,动态加载模块,
但是NodeJs对原生模块和文件模块都进行了缓存,第二次require时,就是执行的内存中的文件。

3、commonJs模块加载规则

index.js调用math模块:

let math=require('./math');
let test=math.add(3,3);
console.log(test);

执行index.js 输出:6;

当我们执行node index.js的时候,第一语句就是“require('./math');” 加载 math文件。加载math文件这个动作是由原生模块module的runMain()实现的。

有没有注意到上面写的是加载math文件,并没有明确指出是js文件。
NodeJS加载文件模块基本流程:
1、根据名称按照‘.js’,‘.node‘,’.json‘的顺讯依次查找,如果是.node或者.json的文件最好加上扩展名,加载速度快。
2、查找到math.js,读取js内容,将使用function进行包装,这样可以避免污染全局环境,该函数的参数包括require、module、exports等等参数,以mathi.js为例:

(function(exports,require,module,__filename,__dirname){
        let add=(x,y)=>{
            return x+y;
        }
        let sub=(x,y)=>{
            return x-y;
        }

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

 })

require 方法中的文件查找规则很复杂底,在网上copy了一个图:

更详细的加载规则可以参考:www.infoq.com/cn/artic...

4、commonJs模块的加载机制:

//lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
//index.js
var mod=require('./lib');
consoe.log(mod.counter);
mod.incCounter();
consoe.log(mod.counter);

输出:3
     3

commonJS中模块加载以后,它的内部变化不会影响其内部变量,因为它们会被缓存,所以它输出的是值的拷贝。

CommonJS规范比较适用服务器端,如果是浏览器就需要异步加载模块了,所以就有了AMD,CMD解决方案。

AMD(requireJS)

 AMD是"Asynchronous Module Definition"的简写,也就是异步模块定义。它采用异步方式加载模块。通过define方法去定义模块,require方法去加载模块。

AMD模块定义:

define(function(){
    let add=(x,y)=>{
        return x+y;
    }
    let sub=(x,y)=>{
        return x-y;
    }
    
    return {
        add:add,
        sub:sub
    };
});

如果这个模块还需要依赖其他模块,那么define函数的第一个参数,必须是一个数组,指明该模块的依赖。

define([tools],function(){
    //…………………………
})

AMD模块的加载:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。例如加载math.js。

require([math],function(){
    //……………………
})

require()异步加载math,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

CMD(SeaJS)

玉伯提出的CMD规范,并开发了前端模块化开发框架SeaJS,不过在2015年后SeaJS停止了在github上维护,CMD与AMD用法很相似,但是我个人更喜欢使用SeaJS,虽然在2016年后也被我抛弃啦。

SeaJs使用:

// 所有模块都通过 define 来定义
define(function(require, exports, module) {

   // 通过 require 引入依赖
   var $ = require('jquery');
   var Spinning = require('./spinning');

   // 通过 exports 对外提供接口
   exports.doSomething = ...

   // 或者通过 module.exports 提供整个接口
   module.exports = ...

});

有关于SeaJS与 RequireJS 的异同,可以参考:
github.com/seajs/seaj.…
www.douban.com/note/2...

ES6 模块化

在es6 之前没有模块化的,为了解决问题,提出了commonJS,AMD,CMD,现在ES6模块化汲取了CommonJS 和 AMD 的优点,简洁的语法,异步加载 它完全可以成为浏览器和服务器通用的模块化解决方案。

ES6中模块的定义

ES6 新增了两个关键字 export 和 import,export 用于把 模块里的内容 暴露 出来, import 用于引入模块提供的功能。

export命令输出变量:

//lib.js
let bar=function(){
    console.log('this is bar funciton');
};

let foo=function(){
    console.log('this is foo function');
};

export {bar,foo}

上面的代码还有另一种写法:

export let bar=function(){
    console.log('this is bar funciton');
};

export let foo=function(){
    console.log('this is foo function');
};

export 不止可以导出函数,还可以导出对象,类,字符串等等

const test='aaa';
const obj={
    str:'hello!'
}
export {test,obj};

注:使用export在尾部输出变量时,一定要加大括号,

ES6中模块的加载

import 加载模块:

 //加载 lib.js文件
 import {bar,foo,test,obj} from './lib'
 
 foo();//this is foo function

注:import 命令具有提升效果,会提升到整个模块的头部,首先执行

上面的是逐一指定要加载的方法,我们还可以使用 * 可以整体加载模块:

import * as lib from './lib'
lib.foo();

上面的加载模块的方式需要知道变量名和函数名,否则是无法加载的,我们可以使用export default 命令,为模块指定默认输出。

//lib.js
let foo=function(){
    console.log('this is foo');
} 
export default foo; 

其他文件加载时,可以为该匿名函数指定任意名字。

import  lib from 'lib';

注:export default 命令适用于指定默认模块的输出,一个模块只能有一个默认输出,所以export default 只能使用一次。

ES6 模块运行机制

ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),变量不会被缓存,而是成为一个指向被加载模块的引用。等脚本执行时,根据只读引用,到被加载的那个模块中去取值。举一个NodeJS模块化的例子:

//lib.js
export let counter = 3;
exoprt function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
//index.js
import {counter,incCounter} from './lib';
consoe.log(mod.counter);
mod.incCounter();
consoe.log(mod.counter);

输出:3
     4
调用 incCounter()方法后,lib 模块里的counter变量值改变了。

参考:
www.cnblogs.com/TomXu/...
blog.csdn.net/tyro_jav...
javascript.ruanyifeng....
www.ruanyifeng.com/blo...
zhuanlan.zhihu.com/p/...
segmentfault.com/a/11...
web.jobbole.com/83761/
es6.ruanyifeng.com/#do...
www.cnblogs.com/lishux...