模块化规范--入门 CommonJS、AMD、CMD、ES6 模块化

JS模块化

模块化发展

  1. 模块化前
    • 项目壮大时,代码功能不明显,不利于维护
    • 容易污染全局变量
    • html 中引入过多的 js 文件,且不清楚文件依赖
  2. 模块化的优势
    • 避免命名冲突
    • 代码分离,实行按需加载
    • 更好的复用和维护代码
    • 解决引入js库时依赖模糊的情况
  3. 模块化思路
    • 根据代码功能拆分模块

IIFE 实现模块化

  模块化出现之前,实现模块化的一种方法是使用立即执行函数(IIFE),在 window 上添加属性

  • 向IIFE添加依赖,为window添加属性

JS:

// index.js
(function(window) {         // window 是全局变量,可缺省
    let str = 'Hello World!'
    let foo = function() {
        console.log(msg)
    };
  window.module = { foo }
})(window)

HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script type="text/javascript" src="index.js"></script>
    <script type="text/javascript">
        module.foo()
    </script>
</body>
</html>
  • 可以向IIFE中添加更多的依赖

JS:

// index.js
(function(window, $) {
  const str = 'hello world!'
  const foo = function() {
    console.log(str)
  }
  const bar = function() {
    console.log('change background color...')
    $('body').css('background', 'red')
  }
  window.module = {
    foo,
    bar
  }
})(window, jQuery)  // 注入 jQuery

​ HTML:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="index.js"></script>
    <script type="text/javascript">
    module.foo()
    module.bar()
    </script>
</body>
</html>

缺点:

  • 依赖过多时,html 中 <script> 的引入顺序不能改变,否则出错
  • window 上添加过多的属性,看起来臃肿且不好维护

模块化规范

1.CommonJS

  CommonJS 可以在服务器端或浏览器端实现。

  • 服务器端:利用 Node.js,运行时动态加载模块(同步)
  • 浏览器端:利用 Browserify.js ,在转译(编译)时加载打包模块(生成新文件),修改模块文件后需重新编译打包

语法 :

  暴露模块,相当于暴露一个空对象,单独使用 exports.someVar 相当于在该空对象上添加成员,使用 module.exports = { ... } 则用新的对象(也可以是变量或者函数等)代替空对象

// 1.仅能使用一次,重复使用会覆盖
module.exports = { ... };
module.exports = function() { ... };
module.exports = 'someVar';

// 2.能多次使用,默认将所有内容整合成一个对象,相当于 1 中暴露对象
exports.foo = function() { ... };
exports.bar = 'someVar';
// 等同于
module.exports = {
    foo: function() { ... },
    bar: 'someVar'
}

  引入模块,将模块所暴露的对象(也可以是一个单独的变量或者函数等)引入

// 第三方模块,直接输入模块名
let m1 = require('react');
// 自定义模块,相对路径
let m2 = require('./module.js');
// 调用:根据暴露方式调用

1.在服务器端中使用

// module_1.js
exports.str = 'hello world!'
exports.num = 100

// module_2.js
exports.foo = function() {
    console.log('foo() in module_2.js')
}

// module_3.js
module.exports = {
    bar: function() {
        console.log('bar() in module_3.js')
    }
}

// module_4.js
module.exports = {
    bar: function() {
        console.log('bar() in module_3.js')
    }
}

// main.js
const module_1 = require('./module_1');
const module_2 = require('./module_2');
const module_3 = require('./module_3');
const module_4 = require('./module_4');

console.log(module_1.str, module_1.num);
module_2.foo();
module_3.bar();
module_4();

运行 node main.js 的结果:

hello world! 100
foo() in module_2.js
bar() in module_3.js
baz() in module_4.js

2. 在浏览器端中使用

  在浏览器中使用时,要安装依赖包 browserify.js

1)下载安装 browserify.js

// 生成 package.json
npm init
// 全局安装
npm install browserify -g
// 项目目录中安装并添加至 package.json
npm install browserify --save-dev

2)基础目录

|-project
  |-dist    // 打包生成文件目录(注:browserify 不会自动生成文件夹,需要手动创建)
  |-src                 
    |-main.js    // 主文件
    |-module1.js 
    |-module2.js
    |-module3.js
    |-module4.js
  |-index.html   
  |-package.json  // 项目配置文件
  |-node_modules  // 依赖包

  其中各 js 文件代码同上

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script type="text/javascript" src="dist/build.js"></script>
</body>
</html>

使用 browserify 进行模块打包:

$ browserify ./src/main.js -o ./dist/build.js

在浏览器的控制台能看到相同的输出:

hello world! 100
foo() in module_2.js
bar() in module_3.js
baz() in module_4.js

2.AMD

 AMD(Asynchronous Module Definition异步模块定义) 专用于浏览器端,是 异步 加载模块的。依赖于 require.js

语法步骤:

  1. 暴露模块,通常为暴露一个对象,对象包装了一些数据
// 1.定义没有依赖其他模块的模块, module_1.js
define(function() {
  const str = 'hello world'
  const foo = function () {
    return str
  }
  return {
    foo: foo,
    bar: function() {
      console.log('bar() in module_1.js')
    }
  }
})
// 2.定义依赖其他模块的模块, module_2.js
// 第一个参数数组是此模块依赖的所有模块,并要以形参传入后一个参数函数
define(['module_1'], function(m1) {
  const baz = function() {
    console.log(m1.foo(), 'baz() in module_2.js')
  }
  return { baz }
});
  1. 引入模块和模块配置,require 也可以用 requirejs
// main.js
(function() {
    // 模块配置:paths 为各模块路径配置,还可以配置更多的选项,如 baseUrl 等
  require.config({
        // 注意:模块路径不能添加 .js 后缀,引入第三方库时(jQuery, Angular等),名字必须对应而且为小写。
    paths: {
      module_1: './modules/module_1',
      module_2: './modules/module_2',
      jquery: './libs/jquery-3.1.1.min'
    }
    });
    // 引入并使用模块
  require([
    'module_1',
    'module_2',
    'jquery'
  ], function(m1, m2, $) {
    m1.bar();
    m2.baz();
    $('body').css('background', 'red')
  });
})();
  1. 在 HTML 文件的引入
<body>
    <!-- data-main:应用源文件main.js的路径,src:require.js的路径 -->
    <script data-main="./js/main.js" src="./js/libs/require.js"></script>
</body>

完整步骤

1)下载

  在官网下载 RequireJS,保存至文件 require.js

2)基础项目目录

|-project
  |-js
    |-libs    // 库文件夹
      |-require.js
      |-jquery-3.1.1.min.js
    |-modules    // 模块文件夹
      |-module_1.js
      |-module_2.js
    |-main.js    // 主文件
  |-index.html

各 js 文件的内容如语法步骤所示。

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <!-- 
        src:require.js的路径
        data-main:应用源文件main.js的路径
     -->
    <script type="text/javascript" data-main="js/main.js" src="./js/libs/require.js"></script>
</body>
</html>

3)在浏览器控制台的输出:

bar() in module_1.js
3 hello world baz() in module_2.js
// 且页面背景色为红色

3.CMD

  CMD(Common Module Definition通用模块定义),依赖于 sea.js 库。

语法:

  实现语法类似于 browserify.js 和 require.js 的结合。并可以执行异步加载。

暴露模块

// 只要暴露了任何内容的模块,function 都要带这三个形参
define(function(require, exports, module) {
    let data = 'in module-1';
    let foo = function() {
        console.log(data);
    };
    // 暴露内容的语法类似于 browserify
    // 1.暴露一个对象
    module.exports = { foo };
    // 2.暴露一个方法
    module.exports = foo;
    // 3.分开暴露多个属性
    exports.data = data;
    exports.foo = foo;
});

引入模块

// 模块不再暴露内容时,只写一个形参 require
define(function(require) {
    // 同步引入
    let module1 = require('./module1');
    module1.foo();
    let module4 = require('./module4');
    module4.baz();
    // 异步引入
    require.async('./module3', function(module3) {
            module3();
    })
});

完整步骤

1)下载

  官网下载 sea.js。保存至 'js/libs/sea.js'

2)基础项目目录

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
  |-|-main.js
|-index.html

3)文件

module1.js:

define(function(require, exports, module) {
  const str = 'In module_1.'
  exports.fun1 = function() {
    console.log(str)
  }
})

module2.js:

define(function(require, exports, module) {
  const module_1 = require('./module_1')
  module_1.fun1()

  const fun2 = function() {
    console.log('In module_2.')
  }

  module.exports = {
    fun2: fun2
  }
})

module3.js:

define(function(require, exports, module) {
  const fun3 = function() {
    console.log('In module_3.')
  }
  module.exports = fun3
})

module4.js:

define(function(require, exports, module) {
  const module_2 = require('./module_2')
  module_2.fun2()
  // 异步加载
  require.async('./module_3', function(m3) {
    m3()
  })
  
  module.exports = {
    fun4: function() {
      console.log('In module_4.')
    }
  }
})

main.js:

define(function(require) {
  const module_4 = require('./modules/module_4')
  module_4.fun4()
})

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <!-- 先引入 sea.js -->
    <script src="./js/libs/sea.js"></script>
    <script>
        // 加载模块
        seajs.use('./js/modules/main.js');
    </script>
</body>
</html>

4)在控制台的输出:

In module_1.
In module_2.
In module_4.
In module_3.

4.ES6

  ES6 模块化依赖于 babel (转换为ES5语法) 和 browserify.js (编译打包模块),是静态加载的。

语法:

暴露模块:

  • 使用 exportexport default
  • 使用 export 可以逐个暴露变量,但是命令要包含对外的接口;
  • 使用 export default 定义一个默认暴露的对象(其中 default 就相当于对外的接口);
  • 二者可以同时使用,但是 export default 只能使用一次;
// 1.逐个暴露,暴露两个对外接口 arr 和 foo
export let arr = [1, 2, 3];
export function foo() { return 'Hello World!'; };

// 2.一次性暴露,1 的另一种写法
let str = 'Hello World!';
let bar = function() { console.log(str); };
export { str, bar };

// 3.暴露默认模块(只能定义一个默认模块,定义多个相当于覆盖)
export const PI = 3.14  // 暴露一个对外接口 PI
let fun1 = function() { return true; };
let fun2 = function() { return false; };
export default { fun1, fun2 }   // 默认暴露一个对象

引入模块:

  • 使用 import 引入模块(相当于打开与模块的接口通道,从中取出所需模块);
  • export 暴露的变量的引入,要一一对应其对外接口;
  • export default 暴露的变量的引入,可以用别名指定(实质上是对 default 接口的重命名);
  • 还可以使用 * 指定引入模块的所有非默认(default)暴露的对外接口,使用 as 对对外接口重命名;
  • import 具有提升功能,不要求在代码的顶部;
  • import 不能动态引入模块,因为它是静态加载的;
  • importexport 可以一起使用,具有 "转发" 模块的功能,可以用作各模块的整合或者跨模块变量。
  • 引入的变量是只读的,不能修改其值;对象变量的属性可以修改,但是也不要使用,避免产生混乱
// 1.引入自定义模块:非默认暴露
import { foo, bar } from './module1.js';  // .js 后缀可以省略
import * as m1 from './module1.js';  // 引入模块的所有对外接口,可使用 m1.foo, m1.bar

// 2.引入自定义模块:默认暴露
import module3 from './module3.js';  // 不能添加 {}

// 3.引入第三方模块
import $ from 'jquery';   // npm 下载的第三方包,路径自动调至 node_modules

// 4.与 export 一起使用
// index.js
export {exp} from './math_module.js'
export {data} from './data_module.js'
// script.js
import {exp, data} from './index.js'

使用步骤

1)安装

// 初始化 package.json
$ npm init
// 全局安装依赖库
$ npm install babel-cli -g  // babel 命令行接口
$ npm install babel-preset-es2015 -g  // 用于转译 ES6 代码的库
$ npm install browserify -g  // 编译打包 ES6 模块
// 添加项目依赖
$ npm install babel-cli --save-dev
$ npm install babel-preset-es2015 --save-dev
$ npm install browserify --save-dev
// 有必要时,安装第三方依赖包
$ npm install jquery@1 --save-dev

2)基本项目目录

|-js    
  |-build  // 用 babel 转译为 es2015 语法的模块文件时生成,不需要自己创建文件夹
    |-main.js
    |-module1.js
    ...
  |dist  // browserify 打包后的文件,需要自己创建文件夹
    |-bundle.js 
  |-src  // 模块文件
    |-main.js
    |-module1.js
    |-module2.js
    |-module3.js
  |-node_modules
  |-index.html
  |-.babelrc  // babel 配置文件,一个 json 文件,不能添加更多后缀
  |-package.json
  |-package-lock.json

3)文件

.babelrc

{
    "presets": ["es2015"]
}

module1.js

// 分别暴露多个对外接口
export const str = 'hello world!'
export const num = 100
export function fun1() {
  console.log('In module_1.js')
}
// 等同于
/* 
const str = 'hello world!'
const num = 100
function fun1() {
  console.log('In module_1.js')
}
export { str, num, fun1 } 

*/

module2.js

// 暴露一个对外接口
export function fun2() {
  console.log('In module_2.js')
}
// 一个默认暴露
export default function() {
  console.log('export default in module_2.js')
}

module3.js

// 转发来自 module_1 和 module_2 的对外接口
export { str, num, fun1 } from './module_1'
export { fun2 } from './module_2'
import dm2 from './module_2'
// 默认暴露一个自身的对外接口
export default {
  str: 'module_3',
  fun3: function() {
    console.log(`In ${this.str}.js`)
  },
  dm2
}

main.js

// import * as m3 from './module_3'
// import dm3 from './module_3'
// 可以写为
import dm3, * as m3 from './module_3'  // dm3 接收默认暴露;m3 为别名,接受所有非默认暴露的接口
import $ from 'jquery'  // 第三方库

m3.fun1()
m3.fun2()
dm3.fun3()
dm3.dm2()
console.log(m3.str, m3.num)
$('body').css('background', 'red')

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script src="./dist/bundle.js"></script>
</body>
</html>

4)输出

In module_1.js
In module_2.js
In module_3.js
export default in module_2.js
hello world! 100
// body 背景色为红色

对比

CommonJS Module 和 ES6 Module

区别:

  • 暴露模块的语法上类似,exportsexportmodule.exportsexport default;但是含义却不一样,module.exports 会覆盖其他单独暴露的语句,export default 只是一个额外的默认暴露,不影响单独暴露的语句
  • CommonJS Module 是运行时加载的,ES6 Module 是编译时加载接口(静态)
  • CommonJS Module 输出的是一个值的拷贝(带缓存功能),ES6 Module 输出的是值的引用

ES6 Module 加载 CommonJS Module:

  • 三种方法获取 module.exports,使用第三种方法时,要通过 foo.default() 才能获取真正的 module.exports
// a.js
module.exports = function() {
  console.log('hello world!')
}

// 法一
import foo from './a.js'
// foo = function() {}
foo()

// 法二
import { default as foo } from './a.js'
// foo = function() {}
foo()

// 法三
import * as foo from './a.js'
// foo = { default: function() {} }
foo.default()
  • 由于 ES6 Module 是编译时确定输出接口,CommonJS Module 是运行时确定输出接口;这就说明了引入 CommonJS Module 时,要整体引入
// 法一
import * as express from 'express';
const app = express.default();

// 法二
import express from 'express';
const app = express();
上一篇:mysql 大数据量求平均值


下一篇:NodeJS新建服务器以及CommonJS规范