前端模块化的演进过程
前言:模块化的作用
- 模块能为你提供了一个更加好的方式来组织这些变量和方法
- 模块能会将这些函数和变量放入一个模块作用域当中。模块作用域使得模块中的不同函数能够共享这些变量。
- 模块化能清晰的表达模块与模块间的关系
stage 1 文件划分方式
做法:通过文件来区分模块,然后用script标签去引入
// 目录结构
|__ pageA
|__moduleA.js
|__moduleB.js
|__index.html
// moduleA.js
function func() {
console.log(123)
}
// moduleB.js
var func = 222
// index.html
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script>
console.log(func) // 222
</script>
缺点:
- 健壮性不强;作用于全局作用域,容易造成变量名冲突与覆盖
- 不安全;没有私有空间,所有模块都可以在模块外部被访问和修改
- 不好维护;模块与模块间的依赖关系不清晰,对文件引入顺序有严格的要求
stage 2 命名空间方式
做法:在文件划分方式的基础上,加一个命名空间,一个文件规定只导出一个模块
// moduleA.js
window.moduleA = {
a: 1,
func: function() {
console.log(this.a)
},
}
// moduleB.js
// moduleA.js
window.moduleB = {
a: 2,
func: function() {
console.log(this.a)
},
}
// index.html
<script src="./moduleA.js"></script>
<script src="./moduleB.js"></script>
<script>
moduleA.func() // 1
moduleB.func() // 2
</script>
优点:
- 解决部分命名冲突问题(导出的模块名还是需要保持唯一)
缺点:
- 不安全;没有私有空间,所有模块都可以在模块外部被访问和修改
- 不好维护;模块与模块间的依赖关系不清晰,模块加载顺序问题,对文件引入顺序有严格的要求
state3 IFFE 依赖参数
做法:使用IIFE(立即执行函数) 形成私有作用域,为模块提供私有变量,外部通过闭包访问模块抛出的变量。
// moduleA.js
;(function($){
var a = 1
function func() {
console.log(a)
}
window.moduleA = {
func: func
}
})(jQuery)
优点:
- 解决部分命名冲突问题(导出的模块名还是需要保持唯一)
- 可以声明私有变量,外部无法更改/访问私有变量
- 通过依赖参数可以知道依赖哪些模块
缺点:
- 模块加载顺序问题;对文件引入顺序有严格的要求
state 4 模块化规范的演进
1. CommonJS规范
commonjs是Node.js所遵循的规范,通过module.exports导出模块,require函数导入模块 特点:
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
- 输入的是被输出值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值了。
- 以同步的方式加载模块;启动时加载模块,执行过程中去使用模块;
因为是同步的加载,所以该规范不适用于浏览器,比如说:模块A依赖模块B,当浏览器执行到模块A导入模块B的代码时,才会去请求模块B的代码并等待加载完成,整个应用就会停着一直等待,影响应用性能等。
// moduleB.js
const a = 1
function func() {
console.log(a)
}
module.exports = {
func: func
}
// moduleA.js
const moduleB = require('moduleB')
moduleB.func()
2.AMD规范
AMD(Asynchronous Moudle Definition) 异步模块定义规范
AMD规范主要用于浏览器的模块化加载,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。由于AMD不是浏览器支持的,所以还需引入相关实现库。
AMD规范相关实现库:Require.js、curl.js
-
定义模块
define(['jquery'], function($){ return { func: function() { console.log('moduleA') } } }) -
加载模块
require(['./moduleA', function(moduleA){ moduleA.func() // 'moduleA' }])
优点:可以以模块化的方式组织代码,通过异步的方式去加载模块,以回调的形式执行请求成功后的代码,不会阻塞应用的运行。
缺点:这种引入方式相对复杂,并且模块一多,请求资源的请求也会随之增多。
3.cmd规范
Common Module Definition,即通用模块定义
cmd规范提供了模块定义和按需加载执行模块。它解决的问题和AMD规范是一样的,只不过在模块定义方式和模块加载时机上不同,CMD也需要额外的引入第三方的库文件,SeaJS
-
定义模块:
// cmd1.js define(function(require,exports,module){ // ... module.exports={ // .. } }) // cmd2.js define(function(require,exports,module){ var cmd2 = require('./cmd1') // cmd2.xxx 依赖就近书写 module.exports={ // ... } }) -
加载模块
seajs.use(['cmd2.js','cmd1.js'],function(cmd2,cmd1){ // .... })
CMD与AMD的区别
-
区别:
-
对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
-
CMD 推崇依赖就近,AMD 推崇依赖前置。
// CMD 默认推荐的是 define(function (require, exports, module) { var a = require('./a') a.doSomething() // 此处略去 100 行 var b = require('./b') // 依赖可以就近书写 b.doSomething() // ... }) // AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() }
-
4. ES module(最佳实践)
ES Modules是浏览器原生支持(es6)的模块系统,它通过export(导出)、import(导入)来管理模块。
特性:
-
自动采用严格模式,忽略“use strict”
<script type="javascript"> console.log(this)// => windows </script> <script type="module"> console.log(this)// => undefined </script> -
每个ESM模块都是单独的自有作用域
<script type="javascript"> let a = 10 console.log(a)// => 10 </script> <script type="module"> console.log(a)// => 直接报错未定义 这样就解决了作用域污染的问题 </script> -
ESM是通过CORS去请求外部的JS模块
外部js文件通过cors请求的,如果请求的地址不在同源的话,那么服务端的响应头包含提供有效cors标头, 如果没有的话直接出现跨域问题 cors不支持文件的形式访问,必须是http Sever方式的请求,在实际开发过程中肯定都是http Sever方式的 -
ESM是script标签会延迟执行脚本
-
与COMMONJS导出值的拷贝不同,ESModule导出的是值的引用
问题:
- 由于esmodule是ES6才实现的,低版本的浏览器是没有实现这一规范,使用ESModule会存在兼容性问题,webpack按es module的规范,在内部实现了一套兼容性更好的模块加载系统。
- 模块化的方式划分出来的文件过多,会导致浏览器频繁的发送请求,影响应用的工作效率
5. UMD规范
UMD (Universal Module Definition) 通用模块定义规范
随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了。
写法:
((root, factory) => {
if (typeof define === 'function' && define.amd) {
//AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
//CommonJS
var $ = requie('jquery');
module.exports = factory($);
} else {
root.testModule = factory(root.jQuery);
}
})(this, ($) => {
//todo
});