早期的前端技术标准根本没有预料到前端会有今天这样的规模,所以很多设计会使我们在开发过程中遇到很多模块化的问题,虽然现如今基本上大部分都被我们后来所引用的各种标准所解决了,但是这个过程还是值得我们去梳理了解一下的。
早期不使用工具和规范的情况下对模块化的落地方式:
第一阶段:文件划分
我们去约定每个文件代表一个独立的模块,并最终将其引入到html文件当中,最后我们在后面调用模块当中的成员。
这种方式其实没有从根本上解决模块化想要解决的问题,我们在多人开发过程中会遇到的全局变量污染,命名冲突等问题并没有得到解决,并且我们还是无法很好的去管理模块与模块间的依赖关系。早期引用模块的数量少还可以,将来引用的模块数量一多起来就很麻烦了。
总而言之,这种方式还是需要主要依靠约定。
第二阶段:空间命名划分
我们将每个模块包裹为一个全局对象,相当于我们在每个模块内为每个模块添加了命名空间
通过命名空间的方式可以一定程度上避免命名冲突和全局变量污染的问题,但是问题是我们每个模块的内部成员还是可以在外部直接访问或是修改,这样就会存在安全隐患,并且模块间的依赖关系依然没有得到解决。
第三阶段:立即执行函数
我们将每个模块都放在函数提供的私有作用域当中,对于需要暴露给外部的成员我们可以以挂在到全局对象上的方式去实现
这样我们每个模块的私有成员就都存在了闭包当中,从外部无法直接访问,这样就保证了我们私有变量的安全。
并且我们还可以将依赖模块作为立即执行函数的参数来传入,实现让模块间的依赖关系更加明确。
以上三个阶段的方式都是以原始的模块系统为基础,通过约定化的方式去实现的代码组织,这些方式在不同开发者去实现的过程中会有细微的差别。
为了统一不同开发者和不同项目之间的差异,我们就需要一个标准去规范模块化的实现方式。
并且所有的js文件和依赖都需要在html文件里统一以script标签的方式引入,在代码量越来越大,需要的依赖越来越多的后期维护也是问题。
因此我们需要一个通用的模块化标准 和 使用代码来实现的模块加载器。
模块化规范
CommonJS规范
由Node.js提供的模块化规范,他规定了:
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过module.exports导出成员
- 通过require函数 载入模块成员
现如今的前端开发者应该都非常熟悉这个规范,但是这个规范适用于node端,在浏览器端直接使用是有问题的。
CommonJS约定是以同步的方式去加载模块,node的执行机制是在启动时加载模块,执行过程中实用模块而不需要去加载模块,因此在node端运行是没有问题的。
而在浏览器端,必然导致效率低下,每次页面加载都会导致大量的同步请求出现,因此早期的浏览器规范并没有选择CommonJS规范,而是专门设计了一个AMD规范。
AMD规范(Asynchronous Module Definition)
翻译过来意思就是异步的模块定义规范,并且同期还推出了一个库,Require.js,它实现了AMD规范,它本身又是一个强大的模块加载器。
举个例子,引用require.js使用AMD规范大概的文件结构和过程如下:
main.js:
入口文件
require.js内部提供require函数帮我们引入模块,用法和define类似。每次执行require函数其实内部就是帮我们创建一个script标签来帮我们执行内部的代码。
require.config({
paths: {
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不一定在同级目录下,所以需要指定路径
jquery: './lib/jquery'
}
})
require(['./modules/module1'], function (module1) {
module1.start()
})
/modules/module1.js
定义模块,内部提供define函数,帮助我们定义模块。我们定义模块的时候接受三个参数,分别是:模块名,[依赖1,依赖2],function(依赖1形参,依赖2形参){模块函数私有空间; return 对外暴露成员}
// 因为 jQuery 中定义的是一个名为 jquery 的 AMD 模块
// 所以使用时必须通过 'jquery' 这个名称获取这个模块
// 但是 jQuery.js 并不在同级目录下,所以需要指定路径
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
AMD社区生态可以说相对完善了,但AMD使用过程中可能也存在一定的问题:
- AMD使用起来相对复杂一点
- 模块JS文件请求频繁
总体来看,AMD规范算是前端模块化的一个中间产物,并不能算是一个最终方案。更像是一个这种妥协的产物。除此之外还有同期出现的由taobao推出的Sea.js库,它实现的标准是由淘宝官方提出的CMD规范,全称是Common Module Definition,他的规范类似于CommonJS,在使用上类似于AMD规范,他想实现的目的是让我们在使用CMD的同时写出的代码与CommonJS类似,从而减轻开发者的学习成本,但是最后这种写法后来被require.js也兼容了。
// 兼容 CMD 规范(类似 CommonJS 规范)
define(function (require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery')
// 通过 exports 或者 module.exports 对外暴露成员
module.exports = function () {
console.log('module 2~')
$('body').append('<p>module2</p>')
}
})
目前:ES Modules(浏览器) + CommonJS(node)
在这之前我们大概了解了前端模块化发展的大致过程,尽管之前的标准基本都实现了模块化,但或多或少都会有一些让开发者不太容易接受的问题,随着技术的发展,js的标准也在逐步完善。现如今的模块化可以说已经比较成熟了,目前开发者对于前段模块化的实现方式也已经基本统一。
- 在node环境中使用CommonJS规范
- 在浏览器环境中使用ES Modules规范
当然,也会有部分其他情况出现,但是就主流而言,目前前端基本已经统一成了这两种规范,因此对于目前的前端开发者而言,就模块化规范来讲,着重来掌握这两种规范就好。
CommonJS in Node.js
这里其实没什么好说的,CommonJS是node的内置的模块系统,在node环境下执行不存在任何环境问题,通过require去载入模块,通过module.exports去导出模块。
ES Modules in Browser
ES Modules是ECMAScript2015(ES6)定义的一个新的模块系统,也就是说这是最近几年才被定义的一个标准,他会存在很多兼容性的问题。在这个标准刚出现的时候,几乎所有浏览器都不支持这个新特性。但后来随着webpack等各种打包工具的流行,这个规范才逐渐开始普及,截至目前。ES Modules可以说是目前最主流的前端模块化方案。
相比于AMD这种由社区提出的模块化开发规范,ES Modules相当于是在语言层面实现了模块化,因此更为完善。并且现如今绝大多数浏览器已经开始支持ES Modules特性了:
原生支持,就意味着我们可以在开发中直接使用,这就意味着将来会越来越普及。
原生是如何支持的:
引入时在script标签上添加type="module"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ES Module - 模块的特性</title>
</head>
<body>
<!-- 通过给 script 添加 type = module 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 -->
<script type="module">
console.log('this is es module')
</script>
<!-- 1. ESM 自动采用严格模式,忽略 'use strict' -->
<script type="module">
console.log(this)
</script>
<!-- 2. 每个 ES Module 都是运行在单独的私有作用域中 -->
<script type="module">
var foo = 100
console.log(foo)
</script>
<script type="module">
console.log(foo)
</script>
<!-- 3. ESM 是通过 CORS 的方式请求外部 JS 模块的 -->
<!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->
<!-- 4. ESM 的 script 标签会延迟执行脚本 -->
<script defer src="demo.js"></script>
<p>需要显示的内容</p>
</body>
</html>
通过import导入模块,export导出模块
// import { default as fooName } from './module.js'
// console.log(fooName)
import { name, hello, Person } from './module.js'
console.log(name, hello, Person)
// export var name = 'foo module'
// export function hello () {
// console.log('hello')
// }
// export class Person {}
var name = 'foo module'
function hello () {
console.log('hello')
}
class Person {}
// export { name, hello, Person }
// export {
// // name as default,
// hello as fooHello
// }
// export default name
// var obj = { name, hello, Person }
export { name, hello, Person }
这里注意:
通过import引入的变量其实和引用文件export导出的变量是一个地址,而非复制,即修改了export导出的变量,另一边通过import引入的变量也将法神改变。
-
import引入的变量一般情况下是只读的,我们不可以在引用后进行手动修改,这样也避免了访问和改变其他模块内部变量的安全问题。
ES Modules基本特性:
- 自动采用严格模式,忽略'use strict'
- 每个ESM都是单独的私有作用域
- ESM是通过CORS去请求外部JS模块的
- ESM的script标签都会延迟执行脚本,类似于defer属性。