JS模块化以及相关规范
1.模块化概念
随着前端应用日趋复杂,项目代码也大量膨胀,模块化就是一种最主流的代码组织方式,一个模块就是一个实现特定功能的文件,它通过把我们的复杂代码按照功能的不同,划分为不同的模块单独维护的这种方式,去提高我们的开发效率,降低维护成本。要用什么功能就加载什么模块,模块化开发是当下最重要的前端开发范式之一,其只是思想,不包含具体实现。
2.模块化开发的优点
- 避免变量污染、命名冲突等问题
- 提高代码复用率
- 提高可维护性
- 能够进行依赖关系的管理
3.模块化演变过程
1.文件划分方式
将每个功能和相关的一些状态数据单独存放在不同的文件当中,此时一个文件就是一个独立的模块。
然后将这个模块引入页面当中,直接调用模块中的成员(变量/函数),一个script标签就对应一个模块,所有模块都在全局范围内工作。
例:
html文件:
<script type='text/javascript' src='module1.js'></script> <!-- 模块1 -->
<script type='text/javascript' src='module2.js'></script> <!-- 模块2 -->
<script type='text/javascript' src='module3.js'></script> <!-- 模块3 -->
<script type='text/javascript'>
foo();
bar();
msg='NBA'; //会污染全局变量
foo();
</script>
js文件:
/**
* 全局函数模式: 将不同的功能封装成不同的全局函数
* 问题: Global被污染了, 很容易引起命名冲突
*/
let msg = 'modulel'
function foo() {
console.log('foo()', msg);
}
function bar() {
console.log('bar()', msg);
}
这种方式的缺点很明显,即:
- 各模块内部的成员都处在全局作用域中,即任意位置均可进行访问和修改,这样就会污染全局作用域。
- 容易出现命名冲突。
- 无法很好地管理各模块之间的依赖关系。
2.命名空间(namespace)方式
命名空间方式是指:在文件划分方式的基础上,约定每个模块只暴露一个对象,并将该模块中的所有成员封装在该对象中。当需要使用的时候,就调用这个对象的属性即可。
例如:
module_a.js
let moduleA = {
name: '一碗周',
handle() {
console.log(this.name)
},
}
module_b.js
let moduleB = {
name: '一碗粥',
handle() {
console.log(this.name)
},
}
html文件:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
console.log(moduleA.name);
console.log(moduleB.name); //仍然可以访问到模块中的所有属性
moduleA.handle()
moduleB.handle()
</script>
</body>
所以,这种方法实际上就是简单的对象封装。
这种方式减少了命名冲突的可能,但是各模块中仍然没有私有空间,而且也没有解决管理模块依赖关系的问题。
3.IIFE(立即执行函数)
所谓的IIFE模式就是使用立即执行函数去创建闭包,这种方式为模块提供了私有空间。
具体的做法就是:
将模块中每一个成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以通过挂载到全局对象上的方式去实现。
这种方式实现了私有成员的概念,就是说模块的私有成员只能在模块内部通过闭包的方式去访问。而在外部,是没有办法去使用的。这样就确保了私有成员的安全。
例:
module_a.js
(function () {
let name = '一碗周'
function handle() {
console.log(name)
}
window.moduleA = { handle } //向window暴露handle对象,从而形成闭包
})()
module_b.js
(function () {
let name = '一碗粥'
function handle() {
console.log(name)
}
window.moduleB = { handle }
})()
html文件如下:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
console.log(moduleA.name) // undefined
console.log(moduleB.name) // undefined,说明无法访问到这个属性,即实现了私有变量的效果
moduleA.handle()
moduleB.handle()
</script>
</body>
在这一阶段,实现了私有成员的概念,但仍未解决模块间的依赖关系问题。
4.IIFE模式增强
这一阶段,在 IIFE 模式的基础上,通过为立即执行函数添加参数的形式,实现模块间的依赖。
例如:
module_a.js
(function () {
function printName(name) {
console.log(name)
}
// 暴露一个打印的方法
window.moduleA = { printName }
})()
module_b.js
(function (m) /* 形参 */ {
let name = '一碗周'
function sayName() {
// 使用其他模块的成员
m.printName(name)
}
window.moduleB = { sayName }
})(moduleA) // 实参
html文件:
<body>
<script src="./component/module_a.js"></script>
<script src="./component/module_b.js"></script>
<script>
moduleB.sayName() // 一碗周,即实现了模块之间的依赖
</script>
</body>
但这种方式仍然存在问题:
- 引入了过多
<script>
标签,就需要发送多个请求,请求数量太多 - 依赖关系模糊
- 难以维护
下面来介绍两种现在开发过程中常使用的模块化规范:
4.常用模块化规范 — CommonJS
CommonJS在Node.js中广泛应用,Node.js是CommonJS的实践者。
CommonJS规范指出一个单独的文件就是一个模块,它采用的是同步加载模块,也就是说模块加载的顺序就是代码中编写的顺序是一致的,而加载的文件资源大多数都存储在服务器中,所以说加载速度没有什么问题。
但是这种方案不适用与浏览器端,由于网络原因,更合理的方案是采用异步加载(CMD、AMD和ESmodule)。
4.1 CommonJS的基本语法
暴露模块使用module.exports
,或者直接使用exports
,引入模块直接使用require()
方法,示例代码如下:
module_c.js
let name = '一碗周'
module.exports = {
name,
getName() {
return name
},
setName(n) {
name = n
},
}
index.js
// 引入自定义的模块
const person = require('./module_c')
// 引入 Node.js 提供的模块
const fs = require('fs')
console.log(person.getName()) // 一碗周
person.setName('一碗粥')
console.log(person.name) // 一碗周
console.log(person.getName()) // 一碗粥
4.2 CommonJS的模块加载机制
在上面的代码中,首先通过 module.exports
导出一个对象,其中包含一个属性两个方法。然后在index.js
中引入该模块,通过require()
方法引入模块并定义一个变量来接收这个模块。
但需要注意的是,CommonJS的模块加载机制是被输出值的拷贝 ,也就是说一旦输出了某个值,即使模块内的数据变化,也不会影响这个值了!
上面的代码中通过setName()
重新为name
进行赋值,在赋值后拿到的结果还是初始值,这是因为name
是一个原始类型的值,它的值会被缓存。
当我们通过getName()方法
来方法name
的值才可以获取到没有缓存的那个结果。
5.AMD和CMD
AMD是"Asynchronous Module Definition "的缩写。AMD规范的最佳实践者是require.js。
CMD规范是在sea.js推广中形成的,与AMD类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
这里不对这两者做介绍…
6.ES Module
6.1 ES Module 的语法特性
如果想要在HTML中使用使用ES Module的话,需要为<script>
标签添加一个type="module"
的属性,然后就可以执行其中的JS代码。
ES Module有主要以下几个特性:
- 自动全部采用严格模式,自动忽略
'use strict'
- 每个ES Module都会运行在单独的私有作用域中
- ES Module是通过CORS的方式请求外部JavaScript模块的
-
ES Module的
<script>
标签会自动延迟执行脚本,相当于加了defer
属性,网页对默认的<script>
标签采用的是立即执行的机制,页面的渲染会等待这个脚本执行完成才会往下渲染
6.2 ES Module 的导入和导出
导出成员可以通过export
导出具体成员,也可以通过export default
导出默认成员,示例代码如下:
module_e.js
// 导出单个成员
export let name = '一碗周'
// 导出默认成员
export default function sayMe() {
console.log('一碗周')
}
// 批量导出成员
// export { name, sayMe }
值得注意的是,批量导出成员的写法并不是导出为一个对象,而是固定的语法,导出得到的是多个成员,导出多个成员必须使用花括号包裹!
想要导出对象,可以使用默认语法,示例代码如下:
export default { name, sayMe }
这样获得的就是一个对象,其中有两个属性。
要注意的是:使用ES Module导出成员,导出的是值的引用 ,也就是说如果模块内部的成员发生改变,所有引用该模块的地方都会发生改变。
导入成员使用import
关键字导入,如下代码展示了如何导入一个ES Module模块,示例代码如下:
// 导入默认成员
// import sayMe from './module_e.js'
// 或者通过 as 关键字对导入的默认成员进行重命名
// import { default as sayMe } from './module_e.js'
// 导入指定成员
// import { name } from './module_e.js'
// 也可以将上面两行合并为1行,示例代码如下:
// import { default as sayMe, name } from './module_e.js'
// 或者简写如下:
import sayMe, { name } from './module_e.js'
sayMe()
console.log(name)
但是,我们无法修改导入的成员的值,如果修改则会抛出异常!
import sayMe, { name } from './module_e.js'
name = '1'
异常信息为Uncaught TypeError: Assignment to constant variable.
如果我们只想要执行某个模块,并不需要模块内部的成员,可以直接通过import
关键字引入即可。
如果我们想要动态的引入某个成员,可以将import()
当做一个函数来使用,示例代码如下:
import('./module.js').then(res=>{
// res 表示模块的默认导出成员
})
我们可以将导入的模块直接导出,示例代码如下:文章来源:https://www.toymoban.com/news/detail-790809.html
export { name } from './module.js'
总结
总的来说,如今模块化已经成为了前端开发者的必备技能了。文章来源地址https://www.toymoban.com/news/detail-790809.html
到了这里,关于【前端模块化】JS模块化思想以及相关规范(CommonJS、ES module)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!