JS模块化

106 阅读5分钟

1. 概念

什么是模块化?

将一个复杂的程序依据一定的规范封装成几个块(文件),并进行组合 在一起

块的内部数据/实现 是私有的,只是向外暴露了一些接口(方法)与外部其他模块进行通信

为什么要模块化?

降低复杂度,提高解耦性,

模块化的好处?

避免命名冲突

更好地分离,按需加载

更高的复用性

更好的可维护性

2. 模块化模式进化史

2.1 全局function模式

将不同的功能封装成不同的函数,这样全局变量容易被污染,可能产生命名冲突

// 全局函数模式定义
// module1.js
let msg = 'module1'
function foo(){
    console.log('foo',msg)
}
function bar(){
    console.log('bar',msg)
}

// 全局模式使用
// 另一个引用了module1.js的html文件或者js文件
foo();
bar();
// 下面会造成打印值不一样
msg = 'msg';
foo();

2.2 namespace模式

简单对象封装,只是减少了全局变量的数量,本质上是对象,还是能进行修改,不安全

// namespace 命名空间模式
// module2.js
let obj = {
	msg:'module2',
	foo(){
		console.log('foo',this.msg)
	}
};

// namespace使用
// 另一个引用了module2.js的html文件或者js文件
obj.foo();
// 下面会造成打印值变化
obj.msg = 'NBA';
obj.foo();

2.3 IIFE模式

匿名函数自调用(闭包)

// IIFE模式:匿名函数自调用
// module3.js
(function(window){
    let msg = 'module3'
    function foo(){
        console.log('foo',msg)
    }
    //下方如果不写是不能访问的
    window.module3 = {
        foo
    }
})(window)

// IIFE模式使用
// 另一个引用了module3.js的html文件或者js文件
module3.foo();

2.4 IIFE增强模式:引入依赖

这是现代模块实现的基石

// IIFE模式增强:引入依赖
// module4.js
(function(window,$){ // 这里是形参
    let msg = 'module4'
    function foo(){
        console.log('foo',msg)
    }
    window.module4 = foo;
    $('body').css('background','red')
})(window,jQuery)// 传入window和jQuery

// IIFE模式增强使用
// 另一个引用了module4.js的html文件或者js文件
// 在引用modules4.js之前引入jQuery
module4();

哪怕上述方案是比较好的了,但是也存在一些问题

一个页面需要引入多个js文件,请求过多、依赖模糊、难以维护

通过现代模块化编码和项目构建来解决以上问题

3. 模块化规范

3.1 CommonJs

node是使用CommonJs

特点:

每个js文件都可以当作一个模块

在服务器端,模块的加载是运行时同步加载的(有可能会堵塞)

在浏览器端,模块需要提前编译打包处理(有可能会出现页面空白)

基本语法:

(1)定义暴露模块

/**
 * 使用module.exports = value 定义和暴露
 */
module.exports = {
    m1() {
      console.log('moudle1 m1()')
    }
  }

module.exports = function (){
    console.log('module2 m2()');
}

/**
 * 使用export.xxx = value 定义和暴露
 */

exports.m3 = function (){
    console.log('module3 m3()');
}

(2) 引入模块

/**
 * 使用require(xxx) xxx是路径 引入模块
 */

let module1 = require('./module/module1');
let module2 = require('./module/module2');
let module3 = require('./module/module3');

module1.m1();
module2();
module3.m3()

(3)实现:

在服务器端使用node.js实现

直接使用node main.js 可得到结果

在浏览器端使用Browserify进行编译和打包,最后页面引用的是打包之后的js文件

1.需要下载Browserify

npm install browserify -g

2.打包

browserify main.js -o bundle.js

3.在html中引用打包好的bundle.js文件

3.2 AMD

异步模块定义

专门用于浏览器端,模块的加载是异步的

AMD需要配置和使用requirejs

下载require.js的地址:www.requirejs-cn.cn/docs/downlo…

(1)定义暴露模块

//定义没有依赖的模块:
define(function(){
	let msg = 'module1';
    function getMsg(){
        return msg;
    }
    return {getMsg};
})
// 定义有依赖的模块:
define(['module1','jquery'],function(m1,m2){
    let msg = 'module2';
    function getMsg(){
        m2('body').css('background', 'blue')
        console.log('m1:'+ m1.getMsg());
        console.log('m2:'+ msg);
    }
	return {getMsg}
})

(2)引入使用模块

(function () {
    //配置
    requirejs.config({
      //基本路径
      baseUrl: "./",
      //模块标识名与模块路径映射
      paths: {
        'jquery': 'lib/jquery-1.10.1',
        "module1": "module/module1",
        "module2": "module/module2",
      }
    })
    
    //引入使用模块
    requirejs( ['module2'], function(module2) {
        module2.getMsg()
    })
  })()

(3)实现

在index.html中引用require.js和main.js

<!-- 
        require.js 在加载的时候会检察data-main 属性
        data-main指向的脚本中设置模板加载 选项,然后加载第一个应用模块
     -->
    <script data-main="./main" src="./lib/require.js"></script>

3.3 CMD

通用模块定义

专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行

实现需要使用seajs

下载sea.js地址:seajs.github.io/seajs/docs/…

(1)定义暴露模块

//定义没有依赖的模块:
define(function (require, exports, module) {
    //内部变量数据
    let data = 'module1 data'
    //内部函数
    function show() {
      console.log('module1 show() and  the module1 data is ' + data)
    }
  
    //向外暴露
    exports.show = show
  })

define(function (require, exports, module) {
    module.exports = {
      data: 'module2 data'
    }
  })

// 定义有依赖的模块:
define(function (require, exports, module) {
    //引入依赖模块(同步)
    let module2 = require('./module2')
  
    function show() {
      console.log('module3 show() and  the module2 data is ' + module2.data)
    }
  
    exports.show = show
    //引入依赖模块(异步)
    require.async('./module1', function (m1) {
      console.log('异步引入依赖模块1 and  the module1 show is ' + m1.show)
    })
  })

(2)引入使用模块

// 引入使用模块
define(function (require) {
    let m1 = require('./module/module1');
    let m3 = require('./module/module3');
    m1.show();
    console.log('-----------------');
    m3.show();
  });

(3)实现

在index.html中引入sea.js,使用seajs.use()

    <!--
        使用seajs:
        1. 引入sea.js库
        2. 如何定义导出模块 :
            define()
            exports
            module.exports
        3. 如何依赖模块:
            require()
        4. 如何使用模块:
            seajs.use()
-->
    <script type="text/javascript" src="lib/sea.js"></script>
    <script type="text/javascript">
     seajs.use()
    </script>

3.4 ES6

依赖模块需要编译打包处理

ES6模块化需要使用babel-cli, babel-preset-es2015和browserify

npm install babel-cli browserify -g

npm install babel-preset-es2015 --save-dev

定义.babelrc文件,将ES6转成ES5

{
  "presets": ["es2015"]
}

(1) 导出模块export

// 分别暴露
export function foo() {
    console.log('module1 foo()');
  }
export let bar = function () {
    console.log('module1 bar()');
  }
export const DATA_ARR = [1, 3, 5, 1]

// 统一暴露
let data = 'module2 data'

function fun1() {
  console.log('module2 fun1() ' + data);
}

function fun2() {
  console.log('module2 fun2() ' + data);
}

export {fun1, fun2}

// 默认暴露
export default {
    name: 'Tom',
    setName: function (name) {
      this.name = name
    }
  }

(2) 引入模块import

//  引入模块
import {foo, bar} from './module1'
import {DATA_ARR} from './module1'
import {fun1, fun2} from './module2'
import person from './module3'

// 使用引入模块内变量
foo()
bar()
console.log(DATA_ARR);
fun1()
fun2()

person.setName('JACK')
console.log(person.name);

// 接口改名
// export { foo as myFoo } from 'my_module';

// 整体输出
// export * from 'my_module';

(3) 编译、打包js

使用Babel将ES6编译为ES5代码(但包含CommonJS语法)

babel ./module -d ./build

./module是ES6的js文件目录,./build 是babel转换的ES5的js文件目录

使用Browserify编译js

browserify ./build/main.js -o ./dist/bundle.js

页面引入并测试bundle.js

 <!-- 
        index.html文件引入最后打包好的bundle.js文件
        控制台显示结果:
        module1 foo()
        module1 bar()
        Array(4)0: 11: 32: 53: 1length: 4[[Prototype]]: Array(0)
        module2 fun1() module2 data
        module2 fun2() module2 data
        JACK
     -->
    <script type="text/javascript"  src="./dist/bundle.js"></script>

import()

1)按需加载。

import()可以在需要的时候,再加载某个模块。

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

(3)动态的模块路径

import()允许模块路径动态生成。import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数

import(f())
.then(({export1, export2}) => {
  // ...·
});

上面代码中,根据函数f的返回结果,加载不同的模块。

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段