文章目录
- JavaScript模块化全面解析
- 一、什么是JavaScript的模块化?
- 二、立即执行函数
- 二、NodeJS给模块化带来的变化
- 1.CommonJS规范
- 2.AMD规范
- 3.CMD规范
- 4.ES6模块化
- 总结
JavaScript模块化全面解析
定义:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。定义模块化是一种处理复杂系统分解为更好的可管理模块的方式。作用模块化用来分割,组织和打包软件。每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体,完成整个系统所要求的功能。
一、什么是JavaScript的模块化?
随着互联网的发展网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等…开发者不得不使用软件工程的方法,管理网页的业务逻辑。
Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)
Javascript社区做了很多努力,在现有的运行环境中,实现"模块"的效果。本文总结了当前"Javascript模块化编程"的最佳实践,说明如何投入实用。虽然这不是初级教程,但是只要稍稍了解Javascript的基本语法,就能看懂。
在JavaScript最开始进行模块化的时候,大家只是单纯的对javascript代码以文件的形式进行拆分,然后按照HTML文件的需要进行引入。
在这里我们新建几个 Javascript文件:
// module_1.js
var a = [1, 2, 3, 4, 5];
// module_2.js
var b = a.concat([6, 7, 8, 9, 10]);
//module_3.js
var c = b.join('-')
// index.js
console.log(a);
console.log(b);
console.log(c);
然后我们再新建一个index.html文件并引入上面的几个Javascript文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="js/module_1.js"></script>
<script src="js/module_2.js"></script>
<script src="js/module_3.js"></script>
<script src="js/index.js"></script>
</body>
</html>
最后得到运行结果:
这是我们javascript代码原始的一种写法,通过拆分文件的形式来划分功能代码,使代码变得更好维护,但是这种写法会遇到很多的问题,例如把文件的引入顺序变换一下:
<script src="js/index.js"></script>
<script src="js/module_1.js"></script>
<script src="js/module_2.js"></script>
<script src="js/module_3.js"></script>
报错的原因是因为JS引擎在加载js时首先是阻塞,当下载index.js时会首先解析index.js中的内容,等到完成之后才回去进行下一步操作,所以index.js中并没有a变量时就会抛出错误。
所以最开始时我们必须根据代码的逻辑来决定文件的加载顺序。
问题二:
引入到同一个html文件中的js文件会共用一个作用域(全局作用域),我们在任何一个文件中声明的变量都会是一个全局变量,当两个文件具有相同变量名时会造成变量的覆盖,这就是我们常说的“全局污染”。
以上我们可以得出模块化解决了两大问题:
- 加载顺序
- 全局污染
二、立即执行函数
上面我们说模块化解决了两个问题,其中为了解决全局污染的问题,立即执行函数应运而生。
;(function () {
// 作用域 // 执行上下文
})();
立即执行函数区别于普通的函数,普通的函数并不是一个表达式,它需要我们去调用才能够去执行,而立即执行函数可以自行执行。我们知道函数拥有着自己的作用域和执行上下文,我们可以利用这一点来创建一个模块的独立作用域从而避免全局污染。
我们来改造一下上面的代码:
// module_1.js
var module_1 = (function () {
var a = [1, 2, 3, 4, 5]
return {
a: a
}
})()
// module_2.js
var module_2 = (function (module_1) {
var b = module_1.a.concat([6, 7, 8, 9, 10])
return {
b: b
}
})(module_1)
//module_3.js
var module_3 = (function (module_2) {
var c = module_2.b.join('-')
return {
c: c
}
})(module_2)
// index.js
;(function (module_1, module_2, module_3) {
console.log(module_1.a);
console.log(module_2.b);
console.log(module_3.c);
})(module_1, module_2, module_3);
运行结果没有变化,并且我们将模块以参数的形式传入同时也解决了模块依赖的问题。
二、NodeJS给模块化带来的变化
上面的立即执行函数解决了全局污染的问题,但是文件引入顺序的问题并没有得到解决,而NodeJS的出现不仅仅解决了文件引入的问题,同时也解决了js文件只能通过HTML引入的问题。
require(‘...’) // 导入模块
module.exports //导出模块
因为要使用NodeJS的模块化,所以需要安装node(这个自行安装),然后对项目进行一下改造:
在根目录执行npm init -y(生成package.json文件)
安装webpack及一些插件,并在package.json文件的scripts中添加运行和打包命令:
根目录新建webpack.config.js文件:
因为只是演示用所以这里只做了一些简单的配置
// webpack.config.js (commonJS规范)
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
output: {
path: path.join(__dirname, 'dist'),
filename: 'index.js'
},
entry: path.join(__dirname, 'src', 'main.js'),
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.html'),
filename: 'index.html'
})
],
devServer: {
port: '8889',
static: path.join(__dirname, 'dist')
}
}
代码改造:
// module_1.js
var a = (function () {
return [1, 2, 3, 4, 5]
})();
module.exports = {
a
}
// module_2.js
var module_1 = require('./module_1');
var b = (function () {
return module_1.a.concat([6, 7, 8, 9, 10])
})();
module.exports = {
b
}
// module_3.js
var module_2 = require('./module_2');
var c = (function () {
return module_2.b.join('-')
})();
module.exports = {
c
}
// main.js(webpack打包的入口文件)
var module_1 = require('./js/module_1');
var module_2 = require('./js/module_2');
var module_3 = require('./js/module_3');
console.log(module_1.a);
console.log(module_2.b);
console.log(module_3.c);
执行npm run dev:
得到运行结果
1.CommonJS规范
CommonJS是NodeJS提供的模块化规范
- CommonJS只要进行一次引用就会对引用的js文件创建一个模块实例。
- 每进行一次require都会对这个实例进行缓存,当遇到再次加载时直接从缓存中获取。
- 必须依赖NodeJS,无法在客户端运行。
- CommonJS使用同步的方式加载模块。
详见:CommonJS规范 – JavaScript 标准参考教程(alpha)
2.AMD规范
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
- 使用异步的方式加载模块,没有阻塞。
- 基于CommonJS改造的可以在客户端运行的模块化规范。
- 依赖require.js库。
- 依赖前置,依赖模块提前执行。
语法:
定义模块:define(moduleName, [module], factor) // moduleName:模块名,[module]:需要依赖的模块,factor:回调函数
引入模块:require([module], factor) // module:依赖模块,callback:执行函数
接下来我们使用AMD规范将上面的代码进行改造:
因为AMD规范以来require.js,所以我们在index.html文件中引入require.js
amd的cdn地址:https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js
改造代码:
// module_1.js
define('module_1', function () {
var a = [1, 2, 3, 4, 5]
return {
a: a
}
})
// module_2.js
define('module_2', ['module_1'], function (module_1) {
var b = module_1.a.concat([6, 7, 8, 9, 10])
return {
b: b
}
})
//module_3.js
define('module_3', ['module_2'], function (module_2) {
var c = module_2.b.join('-')
return {
c: c
}
})
// index.js
require.config({ // 在最终被引入的入口文件中需要配置所有依赖文件的路径,不然会因为找不到依赖文件报错
paths: {
module_1: 'module_1',
module_2: 'module_2',
module_3: 'module_3'
}
})
require(
['module_1', 'module_2', 'module_3'],
function (module_1, module_2, module_3) {
console.log(module_1.a);
console.log(module_2.b);
console.log(module_3.c);
}
)
3.CMD规范
CMD 即Common Module Definition通用模块定义,CMD规范是国内发展出来的,就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS要解决的问题和requireJS一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同
- 使用异步的方式加载模块。
- 依赖就近,使用的时候才加载。
语法:
定义模块:define(function(require, exports, module){}) // require: 用来获取其他模块提供的接口, exports: 向外提供模块接口, module: 存储与当前模块相关联的一些属性和方法的对象
引入模块:seajs.use([module], factor)
seajs的cdn:https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js
代码改造:
// module_1.js
define(function (require, exports, module) {
var a = [1, 2, 3, 4, 5]
return {
a: a
}
});
// module_2.js
define(function (require, exports, module) {
var module_1 = require('./module_1'),
b = module_1.a.concat([6, 7, 8, 9, 10])
return {
b: b
}
});
//module_3.js
define(function (require, exports, module) {
var module_2 = require('./module_2'),
c = module_2.b.join('-')
return {
c: c
}
});
// index.js
seajs.use(['./module_1.js', './module_2.js', './module_3.js'], function (module_1, module_2, module_3) {
console.log(module_1.a);
console.log(module_2.b);
console.log(module_3.c);
})
4.ES6模块化
在之前的javascript中是没有模块化概念的。如果要进行模块化操作,需要引入第三方的类库。随着技术的发展,前后端分离,前端的业务变的越来越复杂化。直至ES6带来了模块化,才让javascript第一次支持了module。ES6的模块化分为导出(export)与导入(import)两个模块。
语法:
import moduleName from ‘…’ //导入
import moduleName as Name from ‘…’ //导入重命名
import {moduleName,…} from ‘…’ //按需导入
import {moduleName as Name, …} from ‘…’ //按需倒入重命名
export //导出
export default //默认导出
…
总结
无论是AMD(requireJS)还是CMD(seaJS)都是基于CommonJS规范实现的,但ES6的模块化规范与CommonJS规范有两个显著的不同点:
- CommonJS的输出是输出一个值的拷贝,而ES6输出的是一个值的引用。
- CommonJS的模块是在运行时加载,而ES6的模块是在编译时加载。