javaScript-模块化的历程
2022-04-16·10min
type
Post
summary
status
Published
category
tags
slug
date
Apr 16, 2022
password
icon
为什么需要模块化
最开始JS只是作为一个脚本语言,来对网页表单做简单的验证,在项目开发中处于非常低的地位。
在IE6之前,甚至没有单独的JS引擎,JS的解释执行是渲染引擎工作的一部分,所以当时一个网页中如果写了过多的JS代码,网页会崩掉。
代码简单的堆在script标签里,只要从上往下执行即可,作为仅有几行的JS代码,自然不需要模块化。
但随着ajax概念的提出,前端浏览器拥有了主动向服务器请求数据并操作返回数据的能力,传统的网页满满向“富客户端”发展,前端的业务逻辑越来越多,代码也越来越多,于是一些问题就暴漏了出来:
- 全局变量的灾难
小明定义了 i=1
小刚在后续的代码里:i=0
小明在接下来的代码里:if(i==1){...} //悲剧
- 依赖关系混乱
b.js依赖a.js,标签的书写顺序必须是
顺序不能错,也不能漏写某个。在多人开发的时候很难协调。
可以说,随着前端的发展,模块化的需要越来越强!
需要模块化解决的问题:
- 安全包装一个模块内的代码,不污染全局
- 唯一标识一个模块
- 优雅的暴露模块的API,不增加全局变量
- 优雅的处理模块间的依赖
模块化的萌芽时代
以下为前端模块化在初期的一些探索:
- 用自执行函数来包装代码
这样function内部的变量就对全局隐藏了,达到是封装的目的。但是这样还是有缺陷的,modA这个变量还是暴漏到全局了,随着模块的增多,全局变量还是会越来越多。
- java风格的命名空间
为了避免全局变量造成的冲突,人们想到或许可以用多级命名空间来进行管理,于是,代码就变成了这个风格:
调用的时候不得不这么写:
这样调用函数,写写都会觉得恶心,所以这种方式并没有被很多人采用。
这些方式都没有解决根本性的问题,js模块化还将走过一段艰苦而曲折的征途。
nodejs的模块化规范CommonJs
2009年,nodejs横空出世,开创了一个新纪元,人们可以用js来编写服务端的代码了。如果说浏览器端的js即便没有模块化也可以忍的话,那服务端是万万不能的。
commonJs与nodeJs的关系
- Nodejs的模块化能一种成熟的姿态出现离不开CommonJs的规范的影响
- 在服务器端CommonJs能以一种寻常的姿态写进各个公司的项目代码中,离不开Node的优异表现
- Node的模块化并非完全按照commonJs规范实现,针对模块规范进行了一定的取舍,同时也增加了少许自身特性
::|以上三点是摘自朴灵的《深入浅出Nodejs》::
commonJs规范的特点
- 运行时加载
- 指在代码运行时才能把模块依赖关系确定下来的外部模块加载形式
- 在代码运行之前不确定会用到外部模块的哪些方法,所以会先把整个外部模块加载完并缓存成一个对象。当运行到指定代码段时,会从缓存的对象中拿外部模块的方法。
- 区别于es6的编译时加载。
- 在 commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module
- 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require
- exports 和 module.exports 可以负责对模块中的内容进行导出
- require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
- CommonJS 模块加载是同步加载,并执行模块文件
commonJs的使用
- 模块引入
通过require函数来引入模块
需要注意的是,require 本质上就是一个函数,那么函数可以在任意上下文中执行,来自由地加载其他模块的属性方法
- 模块定义
通过module.exports和exports两种方式来将模块中定义的成员导出
- 模块标识
模块标识指require函数中的参数,有以下特点:
- 只能是字符串
- 可以是相对路径和绝对路径
- 可以省略文件后缀名
module.exports和exports
exports
和 module.exports
本质上是一个相同对象的引用所以在一个文件中,我们最好选择
exports
和 module.exports
两者之一,如果两者同时存在,会造成覆盖的情况发生。exports
用法:
需要注意的是,不能使用
exports = {}
直接赋值一个对象,这样是无效的module.exports
用法:
module.exports
本质上就是 exports
,用 module.exports
来实现如上的导出。module.exports
也可以单独导出一个函数或者一个类。比如如下:commonJs Q&A
既然有了
exports
,为何又出了 module.exports
?如果不想让一个模块导出对象,而是只导出一个类或者一个函数再或者其他属性的情况,那么
module.exports
就更方便了。exports
会被初始化成一个对象,我们只能在对象上绑定属性,但是我们可以通过 module.exports
自定义导出除对象外的其他类型元素。commonJs总结
Node根据CommonJs规范实现了模块化,但这是针对服务端开发实现的,它的同步加载模块方式不适合于客户端开发。
commonJs加载模块是同步的,引入一个模块时会先加载然后解析该模块,这在服务端没有什么问题,因为模块直接就从硬盘或者内存中读取了,没什么消耗时间。但在前端,模块需要通过http请求从服务器去取,如果网速很慢,而CommonJS又是同步的,所以将阻塞后面代码的执行,从而阻塞浏览器渲染页面,使得页面出现假死状态。
AMD&require.js
AMD——Asynchronous Module Definition,异步模块加载规范 与CommonJS的主要区别就是异步模块加载,就是模块加载过程中即使require的模块还没有获取到,也不会影响后面代码的执行。
RequireJS——AMD规范的实现。其实也可以说AMD是RequireJS在推广过程中对模块定义的规范化产出。
所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
根据AMD规范实现的require.js的使用
AMD规范的模块化:用
require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。首先我们需要引入require.js文件和一个入口文件main.js。main.js中配置
require.config()
并规定项目中用到的基础模块。引用模块的时候,我们将模块名放在
[]
中作为reqiure()
的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]
中作为define()
的第一参数。CMD&sea.js
CMD——Common Module Definition,通用模块规范。
SeaJS——CMD的实现,其实也可以说CMD是SeaJS在推广过程中对模块定义的规范化产出。
与AMD一样是异步加载模块规范,把所有依赖模块的语句定义在一个回调函数中。
与AMD规范的主要区别在于定义模块和依赖引入的部分,AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
CMD示例
与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义,require、exports和module通过形参传递给模块,在需要依赖模块时,随时调用require( )引入即可。
UMD
UMD——Universal Module Definition,通用模块规范。
是一种兼容commonjs、AMD、CMD的思想。
先判断是否支持Nodejs模块(exports是否存在),如果存在就使用Nodehs模块。不支持的话,再判断是否支持AMD/CMD(判断define是否存在)。都不行就挂载在window全局对象上。
ES Module
Nodejs
借鉴了 Commonjs
实现了模块化 ,从 ES6
开始, JavaScript
才真正意义上有自己的模块化规范。相比CommonJs,ES Module有以下特点:
- 在语言规格层面上实现了模块功能,是编译时加载
- 借助静态导入导出的优势,实现了
tree shaking
- 可以
import()
懒加载方式实现代码分割。
ES6模块使用——export
- 导出一个变量
- 导出一个函数
- 常用导出方式(推荐)
- 重命名导出
可以利用as将模块输出多次。
ES6模块使用——import
- 一般用法
- As用法
import命令具有提升效果,会提升到整个模块的头部,首先执行,如下也不会报错:
- 整体模块加载 *
ES6模块使用——export default
使用export default命令时,import是不需要加{}的。而不使用export default时,import是必须加{}。
export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。
我们可以同时使用export 和export default
ES Module特性
- 编译时加载
ES6 module 的引入和导出是静态的,
import
会自动提升到代码的顶层 ,import
, export
不能放在块级作用域或条件语句中。这种静态语法,在编译过程中确定了导入和导出的关系,所以更方便去查找依赖,更方便去
tree shaking
(摇树)- import()动态引入
import()
返回一个 Promise
对象, 返回的 Promise
的 then 成功回调中,可以获取模块的加载成功信息。使用动态引入的方式,可以实现动态加载、懒加载
ES Module和CommonJs区别
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,所以说ES6是编译时加载。
不同于CommonJS的运行时加载(实际加载的是一整个对象),ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式:
以上这种写法与CommonJS的模块加载有什么不同?
- 当require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象包含path这个模块的所有API。以后无论多少次加载这个模块都是取这个缓存的值,也就是第一次运行的结果,除非手动清除。
- ES6会从path模块只加载3个方法,其他不会加载,这就是编译时加载。ES6可以在编译时就完成模块加载,当ES6遇到import时,不会像CommonJS一样去执行模块,而是生成一个动态的只读引用,当真正需要的时候再到模块里去取值,所以ES6模块是动态引用,并且不会缓存值。