什么是前端模块化?它需要满足什么标准呢?
- 将复杂程序根据规范拆分成若干模块,一个模块包括输入和输出;
- 模块的内部是私有的,对外暴露接口与其他模块通信;
- 一个 HTML 可以引用的script包括: 脚本和模块;
脚本 vs 模块
脚本和模块在最大区别是通过模块化拆分以后,我们能更清楚看到整个业务的逻辑,即各个模块是做什么的。如下图所示:
- getdata.js 是向服务端发送请求获取数据
- handle.js 处理数据
- sum.js 处理数据求和的逻辑。
但是如果只有一个 index.js 脚本文件,其实不知道这个文件里面大致的逻辑的。
注意:模块化是一个标准,不是一个实现,实现是由具体一个具体的语言实现的。比如commonjs是模块化的一个标准,nodejs 实现了 commonjs。
前端模块化的进化过程
全局 function 模式
这种模式是将不同的功能封装成不同的函数。比如,在 index.js 文件中定义三个不同功能的方法。
function api() {
return {}
}
function handle(data, key) {
return data.data[key]
}
function sum(a, b) {
return a + b
}
这个三个方法都是挂载在window的下面,如下图:
假如其他的同学或者第三方库也在全局下定义了上面的那三个方法,那么就会形成冲突,这样就会导致你的业务不能用,或者别人的业务不能用。
所以,这种模式最大的缺陷就是容易引发全局命名空间冲突。
全局 namespace 模式
既然全局 function 模式容易导致命名冲突,那我们把方法不要定义在window下面,而是定义到一个对象下,这样不就好了吗?同时,我们可以把这个对象的名称变得稀有,比如,window.__ moduleApi__, 这样命名冲突的概率就大大下降了。
window.__module = {
name: 'ming',
api() {
return {
data: {
a: 1,
b: 2
}
},
handle(data, key) {
return data.data[key]
},
sum(a, b) {
return a + b
}
}
但是在这种模式下,外部能够修改内部的数据,比如我们修改了模块内部的变量 name
const m = window.__module
console.log(m.name) // ming
m.name = 'xiao'
console.log(m.name) // xiao
这与模块化的定义相冲突,模块化的定义要求是模块内部的变量私有化,不能被外部修改,如果能随便被外面修改就丧失了封装性。
IIFE 模式
那如何让模块内的变量私有化,不能被外部修改呢?可以通过函数作用域 + 闭包的方式解决。
function test() {
let a = 1
}
console.log({ test })
执行上面的代码,打印结果只有 test
函数,是没有 a
这个变量的。这就是 javascript 这门语言天生的一个特性,就是在函数作用域下,它的变量是天生具有私有化性质,外部是拿不到的,这就符合模块内变量私有化的特点。
那如何才能拿到a
呢?并能对a
的值进行修改呢?这就可以通过闭包来实现。
function test() {
let a = 1
return {
set(v) {
a = v
},
get() {
return a
}
}
}
const handle = test()
console.log(handle.get()) // 1
handle.set(2)
console.log(handle.get()) // 2
根据闭包的特点,当test
函数执行完的时候,里面的变量a
是不会被释放的。于是,模块化就进入到第三阶段:IIFE模式,通过自执行函数创建闭包来实现。
;(function () {
let x = 1
function api() {
return {
data: {
a: 1,
b: 2
}
}
}
function handle(data, key) {
return data.data[key]
}
function sum(a, b) {
return a + b
}
function setX(v) {
x = v
}
function getX() {
return x
}
window.__module = {
x,
setX,
getX,
api,
handle,
sum
}
})()
const m = window.__module
console.log(m.x) // 1
m.x = 2
console.log(m.x) // 2
为什么 m.x = 2
能赋值成功呢?
我们刚才不是说函数作用域内部的变量是无法被访问和修改的吗?这里就要区分什么是函数作用域内的变量和对象属性的区别。
我们先把m.getX()
输出的值打印下:
console.log(m.getX()) // 1
发现m.getX() = 1
。m
是 window 下面挂的__module
对象,这个对象下面有一个属性是x
,而这个对象m
调用getX()
获取的x
是原函数作用域里面的x
,这是两个不同的x
。
所以,一般不要把函数作用域里面的变量x
暴露出来,因为你暴露出来的x
仅仅是一个值的拷贝。如果你要更新函数作用域的值,就可以通过m.setX()
来更新。
m.setX(10)
console.log(m.getX()) // 10
但是,这种模式无法解决模块间相互依赖的问题,因为模块写出来肯定要相互引用的。
IIFE模式增强
IIFE模式增强,就是要支持传入自定义的模块,完成模块间的引用。首先定义一个api
模块,并把模块挂载到 window 下:
;(function (global) {
function api() {
return {
data: {
a: 1,
b: 2
}
}
}
global.__module_Api = { api}
})(window)
接着定义个sum
模块,并且引入api
模块:
;(function (global, apiModule) {
function sum(a, b) {
return a + b
}
global.__module = {
api: apiModule,
sum
}
})(window, window.__module_Api)
const m = window.__module
这种模式虽然解决了模块间的依赖问题,但是有很多缺陷:
- 多依赖传入时, 代码阅读困难;
- 无法支持大规模模块开发,比如某个模块依赖了50个依赖,50个依赖全部通过入参的形式传递进去,这显示是不现实的;
- 无特点语法支持,代码简陋;
总结
本文从全局 function 模式容易产生命名冲突问题开始,引出了全局 namespace 模式,把模块封装到一个特定的对象里面,这样就避免了命名的冲突,但是这会带来对象里面的变量容易被外部修改的缺陷。
紧接着利用函数作用域和闭包的特性,采用IIFE函数解决了模块内变量私有化的问题,但是这种模式还没有解决模块间的依赖问题,尽管通过把自定义模块作为参数传入来达到模块间引用,但是这种方式无法支持大规模模块的开发。
针对以上问题,nodejs 采用了 commonjs 模块规范,而 es6 则采用了 esModule 模块规范。下一节将学习 commonjs 。