带你彻底理解JS模块化规范及运用

200 阅读11分钟

前言:大型项目的后期维护想要轻松就需要考虑使用模块化规范取管理

早期js文件的页面加载情况:

<body>
<script src=""></script>
<script src=""></script>
<script src=""></script>
<script src=""></script>
<script src=""></script>
</body>

问题:
请求过多
依赖模糊---需要考虑顺序
难以维护

💖💖所以一定是要做出改变的!!!

什么是模块/模块化?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起。块的内部数据/实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信

模块化的进化史

一:最早的JS函数封装逻辑--Global被污染,很容易命名冲突

/*一:最早的JS函数封装逻辑--Global被污染,很容易命名冲突

---直接写在全局中
 function feature1() {  }

 function feature2() {  }
*/

举例说明:
   let globalExposure='opp';

   function feature1(){    
        console.log(globalExposure)
    } 

    feature1() //opp

    function feature2(){
        console.log(globalExposure)
    } 

   //如果我意外修改了global
   globalExposure='my bad'

   feature1()  // my bad
   feature2()  // my bad

二:简单封装的Namespace模式逻辑--减少Global上的变量数目--本质是对象,不安全会被修改

/*二:简单封装的Namespace模式逻辑--减少Global上的变量数目--本质是对象,不安全会被修改

---定义一个api对象接口
    let api ={
        feature1:function(){},   
        feature2:function(){}
    }
    api.feature1();
*/

举例说明:
    let obj = {
        localExposure:'module2',  
        feature1(){        
            console.log('我feature1能访问localExposure吗?',this.localExposure)    
        }
    }
    obj.feature1()  //我feature1能访问localExposure吗? module2
    obj.localExposure='my bad'
    obj.feature1(); //我feature1能访问localExposure吗? my bad

三:匿名闭包:IIFE模式--函数是Javascript唯一的Local Scope

/*三:匿名闭包:IIFE模式--函数是Javascript唯一的Local Scope

var api =(    
        //匿名函数
        function(){        
            var _private='safe variable'        
            var feature1=function(){            
                console.log(_private)        
            }        

            return { feature1: feature1}    

        }
    )()
    api.feature1();  //safe variable
    console.log(api._private) //undefined  ---闭包内的变量是私有的(作用域的问题)*/

四:再增强一点:在IIFE的基础上引入依赖--模块模式的基石

/* 四:再增强一点:在IIFE的基础上引入依赖--模块模式的基石*/
举例说明:

    var api =(    
        function($){//成功引入并使用上了Jquery        
            var _$body=$('body')          
            var feature1=function(){            
                console.log(_$body)        
            }        
            return {            feature1: feature1        }    
        })(Jquery)
    api.feature1()  

举例说明一:

举例说明一:
//nodejs下没有window属性----基于nodejs环境下测试
    (    
        function(global){ 
            //在该作用域下是私有的       
            let _private="module3";        
            function feature1(){            
                console.log(_private)        
            }        
            // console.log('IIFE',_private) //IIFE module3        
            //暴露在全局        
            global.module3={feature1:feature1}    
        }
    )(global)
    module3.feature1()  //IIFE module3
    feature1()  //调用失败
    let _private='my bad'  //添加到了global上
    console.log('global',_private) //global my bad  

举例说明二:

举例说明二:
    (    
        function (global) {        
            let _private="module4"        
            function feature2(){            
                console.log('module4暴露',_private)        
            }        
            global.module4=feature2;    
        })(global)
        module4();  //module4暴露 module4  ---module4是一个函数

举例说明三:

举例说明三:
    var api =(    
        function(){        
            let _private="module5"        
            var feature1=function(){            
                console.log( _private)        
            }        
        return {            feature1: feature1        }    
        })()
    console.log(api) //{ feature1: [Function: feature1] }
    api.feature1() //module5

为什么要模块化

  • 降低复杂度 Code complexity(复杂) grows as the site gets bigger

  • 降低耦合度Code complexity(复杂) grows as the site gets bigger

  • 部署准确性Deployment(部署) wants optimizedd code in few HTTP calls

模块化的好处

  • 避免命名冲突(减少命名空间污染)

  • 更好的分离(按需加载)

  • 更高的复用性

  • 高可维护性

模块化规范

  • CommonJS---Nodejs是基于Commonjs模块化规范编写的
  • AMD
  • CMD
  • ES6

1.模块化详解--CommonJs

  • 1.1 规范说明:

    说明: 每个文件(js文件)都可以当作一个模块

    在服务器端: 模块的加载是运行时同步加载的 ---基于服务器端实现:这里以借助Nodejs环境为例说明

    在浏览器端: 模块需要提前编译打包处理 ---基于浏览器端实现:这里以借助browserify浏览器打包工具为例说明

  • 1.2 基本语法

    💖暴露模块 第一种方式---->module.exports=value(任意的数据类型)

    分析---exports本身就是一个对象, 分析---value这个任意数据类型覆盖了原本的默认空对象(exports)的数据类型

    第二种方式---->exports.xxx=value(任意的数据类型)

    问题:暴露的模块到底是什么? 暴露的本质是exports这个对象

    💖引入模块 ---->require(xxx) ---引入第三方模块时:xxx为模块(包)名 ---引入自定义模块时:xxx为模块文件路径

  • 1.3 Commonjs实现在服务器端的构建目录为:

1.3.1这里是module1.js的页面代码

//这里是module1.js的页面代码
//暴露模块  module.exports=value
//value这个任意数据类型覆盖了原本的默认空对象(exports)的数据类型//module.exports={}本来是一个空对象

    //这里以暴露一个对象为例
    module.exports={    
        msg:'是我module1',    
        feature1(){        console.log(this.msg)    }
    }

1.3.2.这里是module2.js的页面代码

//这里是module2.js的页面代码

//暴露模块  module.exports=value

    //这里以暴露一个函数为例
    module.exports=function(){    
        console.log('是我module2')
    }

    //如果继续这样写下面的exports又被一个空对象覆盖了
    // module.exports={}

1.3.3.这里是module3.js的页面代码

//这里是module3.js的页面代码
//exports.xxx=value可以暴露多个不会相互覆盖
    //exports.xxx=value
    exports.feature2=function(){    
        console.log('是我feature2','module3')
    }

    exports.feature3=function(){    
        console.log('是我feature3','module3')
    }

    exports.arr=[2,4,5,3,1,5,6,5]

1.3.4.这里是app.js的页面代码

//这里是app.js的页面代码

/** * 终极目标--基于服务器端实现(nodejs环境下) 
* ---将(其他的模块)modules下的所有模块汇集到这个主模块  
* ---使用CommonJs规范 * */

    //引入第三方模块--基于node_modules文件夹下查找
    let uniq=require('uniq')

    //引入自定义模块
    let module1=require('./modules/module1')
    let module2=require('./modules/module2')
    let module3=require('./modules/module3')

    // 通过module.exports={}语法暴露的---暴露出一个具有feature1属性的对象
    module1.feature1(); //是我module1

    //通过module.exports=function(){}语法暴露的--暴露出一个函数
    module2(); //是我module2

    //通过exports.xxx=value暴露的---现在的module3是一个对象(对象上一直可以增加属性和方法)
    module3.feature3() //是我feature3 module3

    module3.feature2() //是我feature2 module3

    //利用引入的第三方模块uniq实现数组的去重
    let res=uniq(module3.arr);
    console.log(res) //[ 1, 2, 3, 4, 5, 6 ]
  • 1.4 Commonjs实现在浏览器端的构建目录为:

1.4.1 这里是module1.js的页面代码

//这里是module1.js的页面代码

    //暴露模块  module.exports=value
    //这里以暴露一个对象为例
    module.exports={    
        msg:'是我module1',    
        feature1(){        
            console.log(this.msg)    
        }
    }

1.4.2. 这里是module3.js的页面代码

//这里是module3.js的页面代码

    //这里用exports.xxx=value的语法暴露
    exports.feature2=function(){    
        console.log('是我feature2','module3')
    }

    exports.feature3=function(){    
        console.log('是我feature3','module3')
    }

    exports.arr=[2,4,5,3,1,5,6,5]

1.4.3. 这里是module2.js的页面代码

//这里是module2.js的页面代码
//暴露模块  module.exports=value

    //这里以暴露一个函数为例
    module.exports=function(){    
        console.log('是我module2')
    }
    //如果继续这样写下面的exports又被空对象覆盖了
    // module.exports={}

1.4.4. 这里是app.js的页面代码

//这里是app.js的页面代码

/**应用主模块文件 
    * 终极目标--基于浏览器端实现 
    * ---将(其他的模块)modules下的所有模块汇集到这个主模块  
    * ---使用CommonJs规范 * 
**/

/* 
如果直接在index.html引入<script type='text/javascript' src='./js/src/app.js'></script> 
并在浏览器中打开的话会默认报错:app.js:8 Uncaught ReferenceError: require is not defined*/

    let module1=require('./module1')
    let module2=require('./module2')
    let module3=require('./module3')
    module1.feature1();
    module2();
    module3.feature2();
    module3.feature3();

    //重大改变:两步曲
    //1.先通过browserify打包编译处理
    //browserify js/src/app.js -o js/dist/bundle.js
    //2.再改变index.html引入的路径就实现了CommonJs在浏览器端成功运行
    /* <script type='text/javascript' src='./js/dist/bundle.js'></script> */

1.4.5.  注意事项

注意事项:
cnpm init 时给packagename取名时不能有中文和大写字母
--save 局部安装把依赖写入进去   运行依赖--线上(生产)时需要用的依赖
--save-dev 开发依赖--开发的时候需要用到依赖

所需下载第三方模块:
1.cnpm install uniq  --save
2.cnpm install browserify -g
3.cnpm install browserify --save-dev

2.模块化详解--AMD规范

规范:

一:
说明:
Asynchronous Module Definition(异步模块定义)
专门用于浏览器端,模块的加载是异步的

二:
基本语法:
💖定义暴露模块

---->定义没有依赖的模块
define(function(){
	return 模块(通过return去暴露模块,否则括号内的都是私有的)
})

---->定义有依赖的模块
define(['module1','module2'],function(m1,m2){
	return 模块
})

💖引入使用模块---放在主模块汇集
require(['module1','module2'],function(m1,m2){
	使用m1/m2
})

实现:利用require.js在浏览器端实现

----

情景重现一:在还没有AMD规范时是怎么模拟实现AMD规范的

情景重现一:noamd模拟实现的构建目录为:

这里是aleter.js的页面代码

//这里的代码中dataService依赖于dataservice.js
//所以在引用的时候要先去引用dataservice.js

//定义一个有依赖的模块    
(function(window,dataService){        
    let msg='alerter.js';        
    function showMsg(){            
        console.log(msg,dataService.getName())        
    }        
    window.alerter={showMsg}    //alerter被挂载在了window上
})(window,dataService)  //dataServeice被挂载在window上

这里是dataservice.js的页面代码

//定义一个没有依赖的模块    
(function(window){        
    let name='dataService.js';        
    function getName(){            
        return name;        
    }
    //在window对象上挂载着dataServeice对象        
    window.dataService={getName}    
})(window)

这里是app.js的页面代码

(function(){        
    alerter.showMsg()    
})(alerter)  

//因为在aleter.js中把alerter通过window暴露了(暴露在了window)

这里是test.html的页面代码

//特别注意script的引入顺序

<!DOCTYPE html>
<html lang="en">
<head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head>
<body>    
<!-- 依赖关系顺序不能变  请求增加 -->    
<script type="text/javascript" src="./js/dataservice.js"></script>    
<script type="text/javascript" src="./js/aleter.js"></script>    
<script type="text/javascript" src="./app.js"></script>
</body>
</html>

情景重现二:AMD模块化规范出现后的使用方式

情景重现二:AMD模块化规范的使用方式的构建目录为:

这里是dataService.js的页面代码

//定义没有依赖的模块    
define(function(){        
    let name='dataService';        
    function getName(){            
        return name;        
    }        

    //暴露模块---暴露出去一个对象        
    return {getName}    
})

这里是alerter.js的页面代码

//定义有依赖的模块    
//jquery支持amd规范----jquery规定好了如果监测到requirejs默认暴露出名字叫'jquery'
//jquery-1.10.1.js有关于amd规范的设置在第9802行

define(['dataService','jquery'],function(dataService,$){        
    let msg='alerter.js';        
    function showMsg(){            
        console.log(msg,dataService.getName())        
    }        
    $('body').css('background','pink')   

    //暴露模块---暴露出去一个对象        
    return {showMsg};    
})

这里是main.js的页面代码

(function(){        
/**         
* angular如果想要使用amd规范需改增加配置shim         
* requirejs.config({            
    // baseUrl:'',  //基本的路径 ---以它的角度去找路径  js/-->./js 出发点在根目录下            
    paths:{  //配置路径 ---这里是站在main.js的角度去找路径  ---映射文件路径                
        angular:'./libs/angular'            
    },            
    shim:{                
        angular:{                    
            //暴露的是这个库的这个对象                    
            exports:'angular'                
        }            
    }
    //requirejs在拼接路径的时候会自动把.js后缀名拼接上去        
})     
*      
*/      

requirejs.config({            
    // baseUrl:'',  //基本的路径 ---以它的角度去找路径  js/-->./js 出发点在根目录下            
    paths:{  //配置路径 ---这里是站在main.js的角度去找路径  ---映射文件路径                
    dataService:'./modules/dataService',  //不用加.js文件后缀名                
    alerter:'./modules/alerter',                
    jquery:'./libs/jquery-1.10.1'            
    }
    //requirejs在拼接路径的时候会自动把.js后缀名拼接上去        
})    

requirejs(['alerter'],function(alerter){//无需引入不需要的依赖               
    alerter.showMsg();        })    
})()

这里是test2.html的页面代码

<!DOCTYPE html>
<html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head>
<body>    
<!-- 引入require.js并指定js主文件的入口 -->    
<script data-main="js/main.js" src="js/libs/require.js"></script>
</body>
</html>

3.模块化详解--CMD(了解即可)

规范:

一:
说明:
Common Module Definition(通用模块定义)
专门用于浏览器端,模块的加载是异步的
模块使用时才会加载执行

二:
基本语法:
💖定义暴露模块
---->定义没有依赖的模块
define(function(require,exports,module){
	exports.xxx=value
	module.exports=value
})

---->定义有依赖的模块
define(function(require,exports,module){
	//引入依赖模块(同步)
	var module2=require('./module2')

	//引入依赖模块(异步)
	//引入模块的相对路径 回调函数
	require.async('./module3',function(m3){

	})

	//暴露模块
	exports.xxx=value
})

💖引入使用模块---不再需要向外去暴露只需require一个参数
define(function(require){
	var m1=require('./module1')
	var m4=require('./module4')
	m1.show()
	m4.show()
})

可以基于浏览器端实现---依赖 Sea.js

CMD模块化规范的构建目录为:

这里是module1.js的页面代码

//定义没有依赖的模块    
define(        
    function(require,exports,module){            
        let msg='module1';            
        function foo(){                
            return msg;            
        }          
     //暴露模块            
    module.exports={foo}
    }
)

这里是module2.js的页面代码

define(function(require,exports,module){        
    let msg="module2";        
    function bar(){            
        console.log(msg)        
    }        
    module.exports=bar;    
})

这里是module3.js的页面代码

define(function(require,exports,module){        
    let data='module3';        
    function fun(){            
        console.log(data)        
    }        
    exports.module3={fun}    
})

这里是module4.js的页面代码

define(function(require,exports,module){        
    let msg='module4';        
    //同步引入        
    let module2=require('./module2')        
    module2();//同步会阻塞        
    //异步引入        
    require.async('./module3',function(module3){            
        module3.module3.fun()        
    }) //异步会等待        
    function fun2(){                
        console.log(msg)        
    }        
    exports.fun2=fun2;
})

这里是main.js的页面代码

define(function(require){        
    let module1=require('./module1')        
    console.log(module1.foo());        
    let module4=require('./module4')        
    module4.fun2();    
})

这里是index.html的页面代码

<!DOCTYPE html>
<html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head>
<body>  
<!--引入的是sea.js依赖-->    
<script type="text/javascript" src="./js/libs/sea.js"></script>    
<script type="text/javascript">        
    seajs.use('./js/modules/main.js')    
</script>
</body>
</html>

4.模块化详解--ES6规范

规范:

一:
说明:
依赖模块需要编译打包处理

二:
基本语法:
💖定义暴露模块---导出模块
----> export

💖引入使用模块
----> import

可以基于浏览器端实现
---->先使用Babel将ES6编译为ES5代码
---->再使用Browserify编译打包js

实现ES6模块化规范开始的工作之一:

1.安装 babel-cli babel-preset-es2015 browserify 三个第三方包
//cli  ---command line interface 命令行接口

cnpm install babel-cli browserify -g
cnpm install  babel-preset-es2015 --save-dev
cnpm install babel-prest-env -D
preset 预设(将es6转换成es5的所有插件打包)

---------

2.定义 .babelrc文件
//run control 运行时控制文件
{
	"presets":["es2015"]
}

可能遇到的问题一:
Uncaught SyntaxError: Cannot use import statement outside a module
问题一解决方法:使用Babel将ES6编译为ES5代码(但包含CommonJs语法)
终端输入指令 ---> babel js/src -d js/lib

可能遇到的问题二:
Uncaught ReferenceError: require is not defined
问题二解决方法:使用Browserify编译js
终端输入指令 ---> browserify js/lib/main.js -o js/lib/bundle.js

注意事项:

-------

注意事项:

在通过npm下在第三方包的时候可以通过指令指定版本下载

cnpm  install jquery@1
cnpm install jquery@1.11

import $ from 'jquery'  //在通过import语法就可以使用了

如果不借助nodejs环境可以使用<script type='module'></script>

ES6模块化规范的构建目录为:

这里是module1.js的页面代码

//这里是module1.js的页面代码

//暴露模块 分别暴露--不会被覆盖    
export function foo(){        
    console.log('foo() module1')    
}    

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

export let arr=[1,2,3,4,5]

----------

这里是module2.js的页面代码

//这里是module2.js的页面代码

//统一暴露

function fun(){    
    console.log('fun() module2')
}

function fun2(){    
    console.log('fun2() module2')
}

export {    fun,    fun2}

-----------

这里是module3.js的页面代码

//这里是module3.js的页面代码

//默认暴露 可以暴露任意数据类型 
//暴露出的是那么数据类型接收到的就是什么数据类型
// export default ()=>{//     console.log('我是默认暴露的')// }    
//暴露一个函数,可以这样不划算,还不如暴露一个对象

export default {    
    msg:"我又是默认暴露的",    
    foo(){        
        console.log('会覆盖掉之前用export default 暴露的',this.msg)    
    }
}

---------

这里是main.js的页面代码

//这里是main.js的页面代码

//引入其他模块//语法---->import xxx from '路径'

import {foo,bar} from './module1';
import {fun,fun2} from './module2';
console.log(foo,fun)
import xx from './module3'
// xx(); //我是默认暴露的  console.log(xx)

这里是index.html的页面代码----引用的script是编译构建后的bundle.js

//这里是index.html的页面代码----引用的script是编译构建后的bundle.js

<!DOCTYPE html>
<html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Document</title></head>
<body>    
<script src='./js/lib/bundle.js'></script>
</body>
</html>