模块化理解
在JavaScript发展初期就是为了实现简单的页面交互逻辑,而如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着web2.0时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀,此时在JS方面就会考虑使用模块化规范去管理。模块化已经发展了有十余年了,不同的工具和轮子层出不穷,但总结起来,它们解决的问题主要有三个:
- 外部模块的管理;
- 内部模块的组织;
- 模块源码到目标代码的编译和转换;
以下为各个工具或者框架的诞生时间,先了解下时间节奏,方便对内容有所了解:
生态 诞生时间
Node.js 2009年
NPM 2010年
requireJS(AMD) 2010年
seaJS(CMD) 2011年
broswerify 2011年
webpack 2012年
grunt 2012年
gulp 2013年
react 2013年
vue 2014年
angular 2016年
redux 2015年
vite 2020年
snowpack 2020年
什么是模块
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起;
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;
模块化的进化过程
全局function模式
将不同的功能封装成不同的全局函数
- 编码:将不同的功能封装成不同的全局函数
- 问题:污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function m1(){
//...
}
function m2(){
//...
}
namespace模式
简单对象封装
- 作用:减少了全局变量,解决命名冲突;
- 问题:数据不安全(外部可以直接修改模块内部的数据);
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${
this.data}`)
},
bar() {
console.log(`bar() ${
this.data}`)
}
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data
这样的写法会暴露所有模块成员,内部状态可以被外部改写。
IIFE自调用模式
匿名函数自调用(闭包)
- 作用:数据是私有的, 外部只能通过暴露的方法操作
- 编码:将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
- 问题:如果当前这个模块依赖另一个模块怎么办?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能访问模块内部数据
myModule.data = 'xxxx' //不是修改的模块内部的data
myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
let data = 'www.chenghuai.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${
data}`)
}
function bar() {
//用于暴露有函数
console.log(`bar() ${
data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = {
foo, bar } //ES6写法
})(window)
最后得到的结果:
foo() www.chenghuai.com
bar() www.chenghuai.com
otehrFun()
undefined
foo() www.chenghuai.com
IIFE模式增强
这就是现代模块实现的基石
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操作数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${
data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函数
console.log(`bar() ${
data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = {
foo, bar }
})(window, jQuery)
// index.html文件
<!-- 引入的js必须有一定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
上例子通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
模块化的好处
- 避免命名冲突(减少命名空间污染);
- 更好的分离, 按需加载;
- 更高复用性;
- 高可维护性;
引入多个script标签产生的问题
- 引入顺序
一旦引入顺序出现问题,就可能报错 - 请求过多
首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多; - 依赖模糊
我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错; - 难以维护
以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,因此才有了后续的commonjs, AMD, ESM, CMD规范。
模块化规范
CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
特点
- 所有代码都运行在模块作用域,不会污染全局作用域;
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存;
- 模块加载的顺序,按照其在代码中出现的顺序;
基本语法
- 暴露模块&