Javascript模块化编程总结

在上一篇文章 模块化与Webpack属性 library,libraryTarget的关联中提到很多关于模块化的规范。 如什么有些模块需要暴露在module.export上,而有些需要要暴露为define('XXX', [], function() =>{})等等。这些都是因为使用不同的规范导致的。可能会有点混乱,这里就好好来总结模块化的到底有哪些规范。

目前的模块化的规范:**CommonJsAMDCMD** 和 **ES6模块**。

首先先来看一张图片,说明这种模式的整体区别:

那么接下来具体讲讲!

CommonJS

CommonJS是以在浏览器之外构建 Javascript 生态系统为目标而产生的项目,比如在服务器里面。CommonJS的代表:Node应用中的模块,通俗的来说就是 npm 安装的模块。

基本概念

Node 应用是由模块组成,Node的模块采用CommonJS模块规范;那么它的规范有哪些呢?

CommonJS模块规范:

  • 每一个文件就是一个模块,都有自己的作用域。在一个文件定义的变量,函数,类都是私有的,对其他的文件不可见。

    1
    2
    3
    4
    5
    // test.js
    var name = "shuliqi";
    var getName = function() {
    return name;
    }

    上面这个代码表示:变量 x 和函数 getName 是当前 test.js私有的,其他文件是不可见的。如果想在多个文件中分享一个变量的话, 必须定义为 global对象的属性;当然,这种写法是不推荐的。

  • 每个模块内部,module 变量代表当前的模块。这个变量是一个对象,它的 exports 属性(module.exports)是对对外的接口

    1
    2
    3
    4
    5
    6
    7
    // test.js
    var name = "shuliqi";
    var getName = function() {
    return name;
    }
    module.exports.name = name;
    module.exports.getName = getName;

    上面的代码通过module.exports将变量name,getName暴露出来。

  • Node的每个模块提供一个 exports 变量, 指向 module.exports;这等同于在每个模块头部, 都有这样的代码。

    1
    var exports= module.exports;

    这个造成的结果是:在对外输出模块接口时,可以向 exports对象添加方法

    注意,不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系。

    注意:不能直接将exports变量指向一个值, 因为这样就等用于切断了 exportsmodule.exports之间的联系;如果 一个模块的对外接口是一个一个单一的值,那么也是不能用 exports 的、建议还是直接使用 module.exports。不用区分那么多

  • 在服务端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理(这个我们下面的例子会具体来讲)

CommonJS模范定义分为三个部分:

  • 模块定义

    模块定义其实就是暴露模块,可以把模块暴露在module.exports或者exports

    1
    2
    3
    4
    module.exports.name = "shuliqi";
    exports.getName = function() {
    return ...
    }

    把对外开放的接口都暴露在 module.exports对象上。

  • 模块标识

    模块标识就是require()函数的参数,规范是这样的

    • 必须是字符串

    • 可以是以./, ../开头的相对路径

    • 可以是绝对路径

    • 可以省略后缀名

    require命令用于加载模块文件,读入并且执行一个Javascript 文件,然后返回该模块的的 exports 的对象。如果没有指定模块, 会报错。

  • 引用模块

    使用 require 加载模块

    1
    2
    3
    4
    5
    6
    7
    8
    // 绝对路径
    const name = require('shuliqi/study/name.js');

    // 省略了后缀名, 相对路径
    const getName = require('./getName');

    // 模块式第三方模块,直接引用模块名
    const http = require('http');

服务端使用

首先肯定得下载NodeJS。到这个页面进行下载哦。

项目结构

然后初始化我们的项目结构:

1
2
3
$ mkdir module-example
$ cd module-example
$ npm init

添加一些文件夹以及文件;最后的基本结构为:

1
2
3
4
5
6
|--src
|--CommonJS
|--module1.js
|--module2.js
|--index.js
|--package.json

定义模块

module1.js

1
2
3
4
5
6
7
// 暴露(定义)模块:可以直接在写 module.exports 对象上
module.exports = {
name: "模块1",
getName: function() {
console.log(this.name)
}
}

module2.js

1
2
3
4
//  暴露(定义)模块:也可以直接在写 exports 对象上
exports.getName = () => {
console.log("shuliqi")
}

加载模块

index.js

1
2
3
4
5
6
7
8
9
 // 引用模块: 通过模块表示 require 来引用;
// 模块标识 可以省略后缀名, 可以使相对地址
const module1 = require('./module1');
console.log(module1.name);
module1.getName();

const module2 = require('./module2.js')
module2.getName();

执行模块

最后我们执行 index.js 文件

1
2
// 根目录执行
$ node src/commonJS/index.js

执行结果

1
2
3
模块1
模块1
shuliqi

浏览器端使用

添加 index.html 文件

src/commonJS/index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CommonJS</title>
<script src='./index.js'></script>
</head>
<body>
CommonJS 浏览器端使用
</body>
</html>

我们浏览器打开 HTML 文件, 发现报错: Uncaught ReferenceError: require is not defined

为什么呢? 这是因为浏览器缺少Node.Js环境的变量:module,exports,require,global。所以浏览器是无法加载CommonJs模块(npm模块);

所以: npm 模块的都是 JavaScript 语言写的,但是浏览器用不了, 当然是因为不支持 CommonJS格式;所以想要浏览器能使用上这些模块, 就要得必须转换格式。

目前最常用的 CommonJS格式转换的工具是:Browserify; 那我们接下来的例子(基于上面的例子)就使用Browserify 来转换的 CommonJS

Browserify的作用: 将在HTML 引用的js文件打包编译,是的其能够在浏览器上运行

安装 Browserify

1
2
3
4
5
// 全局安装(本例子采用)
npm install browserify -g

// 局部安装
npm install browserify --save-dev

打包编辑

在跟目录执行:

1
browserify src/commonJS/index.js  -o src/commonJS/bundle.js

引用编译文件

src/commonJS/index2.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CommonJS</title>
<script src='./bundle.js'></script>
</head>
<body>
CommonJS 浏览器端使用
</body>
</html>

打开 HTML。控制台能成功能成功打印:

1
2
3
模块1
模块1
shuliqi

现在最终的文件结构为:

1
2
3
4
5
6
7
8
9
10
11
|--src
|--commonJS
|--module1.js
|--module2.js
|--index.js
|--index.html
|--index2.html
|--bundle.js
|--require.js
|--package.json
|--package-lock.json

AMD

在 上一节CommonJS 中,我们可以看出 CommonJS的加载是同步的,如:

1
2
const module1 = require('./module1');
console.log('1111')

上面的第二行代码必须在 require('./module1')之后运行,因此是必须要等 module1.js加载完,也就是说,如果加载的时间很长的, 整个应用是会卡在那里的。

但是在服务器端,所有的模块都是放在本地磁盘,可以同步加载完成,等待的时间就是硬盘的读取时间。但是对于浏览器来说,等待的时间取决于网速的快慢,可能要等待很长的时间,浏览器处于”假死”状态。

所以浏览器端的模块不能采用“同步加载”, 只能采用“异步加载”。这就是AMD规范产生的背景

基本概念

AMD-异步模块加载定义,采用异步方式加载模块, 模块的加载不影响后面语句的运行,所有依赖这个模块的语句都定义在一个回调函数中,等到模块完成之后, 这个回调函数才会运行。

定义暴露模块

1
2
3
4
5
// 定义一个没有依赖的模块
define(function() {
// xxx: 模块
return xxx
})
1
2
3
4
5
// 定义一个没有依赖的模块
define(['module1', 'module2'], function() {
// xxx: 模块
return xxx
})

引用模块

如何引用AMD模块呢?这里先讲讲 RequireJSAMD规范的关系!

我们知道的大名鼎鼎的 ReuireJS,实际上AMD 就是ReuireJS 在推广的过程中对模块定义的规范化产出;

RequireJS 是一个工具库,主要是用于客户端的模块管理,它可以让客户端的代码分成一个个模块,实现异步加载和动态加载。从而提高代码的性能和可维护性。重要的是它的模块管理遵守 AMD规范。 如果我们是可以使用RequireJS来加载 AMD模块

先引入require.js

1
<script src="js/require.js" defer async="true" ></script>

引入require.js 之后加载我们自己的代码:

1
<script src="js/require.js" data-main="js/main"></script>

data-main属性的作用是:指定网页的主模块。

AMD模块的加载(主模块)

1
2
3
require(['module1', 'module2'], function(module1, module2){
// 在回调函数中使用module1/module2
})

更详细的引用方式, 我们看下面的例子

例子🌰

在基于上一节CommonJS的结构目录, 我们添加新的目录和文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|--src
|--commonJS
|--module1.js
|--module2.js
|--index.js
|--index.html
|--index2.html
|--bundle.js
|--require.js
|--AMD
|--index.html
|--main.js
|--module1.js
|--module2.js
|--package.json
|--package-lock.json

定义(暴露)模块

src/AMD/module1.js

1
2
3
4
5
6
7
8
9
// 定义一个没有依赖的模块
define(function() {
let name = "shuliqi";
function getName() {
return name;
}
// 暴露模块
return { getName, name };
});

src/AMD/module2.js

1
2
3
4
5
6
7
8
9
10
// 加载有依赖的模块
// 依赖module1.js
define(['./module1.js'], function(module1) {
let age = 12;
function getMsg() {
console.log('name:',module1.getName(), 'age:', age);
}
// 暴露模块
return { getMsg };
});

引用模块

可以下载 require.js,到本地直接引用, 也可以网上的 [require.js链接]https://requirejs.org/docs/release/2.3.6/minified/require.js)

1
<script  src="https://requirejs.org/docs/release/2.3.6/minified/require.js"  data-main="./main.js"></script>

src/AMD/index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AMD</title>
<script src="https://requirejs.org/docs/release/2.3.6/minified/require.js" data-main="./main.js"></script>
</head>
<body>
AMD 浏览器端使用
</body>
</html>

主模块main.js 引入我们的module1 module2

src/AMD/main.js

1
2
3
4
require(['./module1', './mosule2'], function(module1, mosule2) {
console.log(module1.name);
mosule2.getMsg();
})

结果

打来我们的index,html文件,结果如下:

结果说明:浏览器能使用我们的AMD模块了

CMD

ReuireJS在声明依赖的模块时会在第一时间加载并且执行模块内部的代码:

1
2
3
4
5
6
require(['./module1', './mosule2'], function(module1, mosule2) {
// 等于在最前面声明并初始化了要用到的所有模块

// 如:即使没有用到模块 module1,但是 module1 还是提前执行了
mosule2.getMsg();
})

CMD则是另外一种 js 优化方案, 它与 AMD 很类似,规范也是专门用于浏览器,模块的加载也是异步的, 但是模块使用的时候才会加载执行。AMD 是推崇依赖前置,提前执行,CMD 推崇依赖就近延迟执行。

CMDSea.js推广过程中的产生的;

基本概念

定义暴露模块

CMD中规范中, 一个模块就是一个文件, 定义暴露模块的格式如下:

1
define(factory)

factory为函数时,表示是模块的构造方法,执行这个构造方法就可以得到模块向外提供的接口。

factory方法在执行的时候,默认会传入三个参数:requireexportsmodule

  • require 是一个方法,用来获取其他模块提供的接口

    require.async 方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback 参数可选

    注意require 是同步往下执行,require.async 则是异步回调执行。require.async 一般用来加载可延迟异步加载的模块。

  • exports 是一个对象, 用来向外部提供模块接口

  • module 是一个对象,存储了与当前模块相关联的一些属性和方法

1
2
3
4
5
6
// 定义没有依赖的模块
define(function(require, exports, module) {
// 暴露模块
exports.xxx = value
module.exports = value
})
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义有依赖的模块
define(function(require, exports, module) {
// 引入依赖模块(同步)
const module1 = require('./module1')

// 引入依赖模块(异步)
require.async('./module2', function(module2) {

})

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

引用使用模块

使用 require 来引用模块。上面代码也写过。

1
2
3
4
5
6
define(function (require) {
// 引入 CMD模块 module1
var module1 = require('./module1')
// 引入 CMD模块 module2
var module2 = require('./module2')
})

举例子🌰

CMDSea.js推广过程中的产生的;所有结合 Sea.js来使用。

在原先的项目结构目录上我们加载CMD目录, 添加文件

1
2
3
4
5
6
7
|--CMD
|--index.html
|--module1.js
|--module2.js
|--module3.js
|--main.js

引入Sea.js

在这里下载 然后将dist目录下面的sea.js导入项目

1
2
3
4
5
6
7
|--CMD
|--index.html
|--module1.js
|--module2.js
|--module3.js
|--main.js
|--sea.js

定义暴露模块

module1.js

1
2
3
4
5
6
7
8
9
10
11
12
// 定义没有依赖的模块
define(function(require, exports, module) {
// 模块内部变量
const name = "shuliqi";
// 模块内部函数
function getName() {
return name;
}

// 向外暴露
exports.getName = getName;
})

module2.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义有依赖的模块
define( function(require, exports, module ) {
// 模块内部变量
const age = 18;
// 引入模块(同步)
const module1 = require('./module1');
function getMsg() {
console.log('name:', module1.getName(), 'age: ', age)
}
// 向外暴露模块
module.exports = { getMsg };

})

module3.js

1
2
3
4
// 定义没有依赖的模块
define( function (require, exports, module) {
exports.sex = "女"
})

main.js

1
2
3
4
5
6
7
8
9
10
11
// 定义有依赖的模块
define( function (require, exports, module) {
// 引入模块(同步)
const module2 = require('./module2');
module2.getMsg();

// 引入模块(异步)
require.async('./module3', function(module3) {
console.log('我是异步加载的:', module3.sex)
})
})

浏览器使用模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>CMD</title>
<script src="./sea.js"></script>
</head>
<body>
CMD模块化 浏览器端使用
<script type="text/javascript">
seajs.use('./main.js')
</script>
</body>
</html>

结果

控制台能成功打印:

1
2
name: shuliqi age:  18
我是异步加载的: 女

ES6 模块

ES6之前,最只要是的模块规范是 CommonJSAMD,CommonJS主要用于服务端,AMD用于浏览器端;ES6实现了模块功能,完全可以取代 CommonJSAMD规范。

ES6 模块的设计思想是尽量静态化,使得编译时就能确定模块的依赖关系,输入和输出的变量。而CommonJSAMD都是只能在运行的时候才能确定这些:

1
2
3
4
5
6
7
8
// CommonJS
let { start, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

这代码实质是加载fs模块(即加载fs的所有模块)生成一个对象_fs, 然后再从这个对象上读取 3 个方法。 这种加载就称为”运行时加载“, 因为只有运行了才能得到这个对象,导致完全没办法在编译时做”静态化“

ES6模块不是对象,而是通过export命令显示的指定输出的代码,再通过import命令输入:

1
import { stat, exists, readFile } from 'fs';

这代码的实质是从fs模块加载3 个方法,其他方法不加载,这种加载称为”编译时加载“或者”静态加载“,即 ES6可以在编译时就完成模块加载, 效率要比 CommonJS模块的加载方式高。

基本概念

定义暴露模块

使用 export命令, export default 命令来暴露对外的接口。其中export default 命令是为模块指定默认输出

使用 export暴露对外的接口, import引用的时候需要知道所要加载的变量名/函数名,否则无法加载,为了给用户提供方便,让他们不需要阅读文档就能加载模块,就可以使用export default 命令来为模块指定默认输出。

1
2
3
4
5
// 暴露变量 (export 命令)
const name = 'shuliqi';
const age = 18;
export { name, age }

1
2
3
4
// 为模块指定默认输出
export default function () {
console.log('shuliqi')
}
1
2
3
4
// 暴露函数或者类
function getName() {
return "shuliqi";
}

引用模块

使用 import命令来加载模块。但是暴露模块的方式,import 的使用会有所不同

  • 如果是使用export命令定义了模块的对外接口

    1
    2
    3
    4
    5
    // export.js
    // 暴露变量 (export 命令)
    const name = 'shuliqi';
    const age = 18;
    export { name, age }
    1
    2
    // 引用
    const { name, age } from './export.js';
  • 如果使用 export default来加载模块

    1
    2
    3
    4
    5
    // export-default.js
    // 为模块指定默认输出
    export default function () {
    console.log('shuliqi')
    }
    1
    2
    import customName from './export-default.js';
    customName()

例子🌰

在原来的项目结构上,加载ES6文件夹及其一些文件

1
2
3
4
5
6
7
8
|--ES6
|--module1.js
|--module2.js
|--main.js
|--index.js
|--lib
|--bundle.js

定义暴露模块

module1.js

1
2
3
4
5
6
// 使用 export 暴露模块
const name = "shuliqi";
function getName() {
console.log(18);
}
export { name, getName };

module2.js

1
2
3
4
5
6
// 使用 export default 为模块指定默认输出
const sex = '女';

export default function() {
console.log(sex)
}

引用模块

main.js

1
2
3
4
5
import { name, getName } from './module1.js';
import getSex from './module2.js';
console.log(name);
getName();
getSex();

安装转换器和编译器

由于目前各大浏览器对ES6的支持大不相同, 所以通常需要把ES6代码转换成ES5的代码,这就需要转换器,如现在广泛使用的 Babel 。转换完之后代码需要再编译一下, 就可以在浏览器使用了。

1
2
3
// 安装babel-cli, babel-preset-es2015和browserify
npm install babel-cli browserify -g
npm install babel-preset-es2015 --save-dev

在跟目录添加 .babelrc文件

1
2
3
4
5
6
{
"presets":[
"es2015"
],
"plugins":[]
}

转码我们写的ES6代码和编译我们的转码之后的代码

1
2
3
4
// 转码
babel src/ES6 -d src/ES6/lib
// 编译
browserify src/ES6/lib/main.js -o src/ES6/bundle.js

完成之后我们的ES6文件目录为:

1
2
3
4
5
6
7
8
9
10
|--ES6
|--module1.js
|--module2.js
|--main.js
|--index.js
|--lib
|--module1.js
|--module2.js
|--main.js
|--bundle.js

html 文件使用

最后在我们的html文件中加载我们转码和编译之后的代码 bundle.js

index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ES6</title>
<script src="./bundle.js"></script>
</head>
<body>
ES6 浏览器端使用
</body>
</html>

打开浏览器,结果: 控制台能正确打印出:

1
2
3
shuliqi
18

最后

最后文章所有的示例可以在module-example下载


参考文章:

文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2021/03/06/webpack学习-优化(optimization)/前端模块化/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog