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概念的提出,前端浏览器拥有了主动向服务器请求数据并操作返回数据的能力,传统的网页满满向“富客户端”发展,前端的业务逻辑越来越多,代码也越来越多,于是一些问题就暴漏了出来:
  1. 全局变量的灾难
小明定义了 i=1
小刚在后续的代码里:i=0
小明在接下来的代码里:if(i==1){...} //悲剧
  1. 依赖关系混乱
b.js依赖a.js,标签的书写顺序必须是
顺序不能错,也不能漏写某个。在多人开发的时候很难协调。
可以说,随着前端的发展,模块化的需要越来越强!
需要模块化解决的问题:
  1. 安全包装一个模块内的代码,不污染全局
  1. 唯一标识一个模块
  1. 优雅的暴露模块的API,不增加全局变量
  1. 优雅的处理模块间的依赖

模块化的萌芽时代

以下为前端模块化在初期的一些探索:
  1. 用自执行函数来包装代码
这样function内部的变量就对全局隐藏了,达到是封装的目的。但是这样还是有缺陷的,modA这个变量还是暴漏到全局了,随着模块的增多,全局变量还是会越来越多。
  1. 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的使用

  1. 模块引入
通过require函数来引入模块
需要注意的是,require 本质上就是一个函数,那么函数可以在任意上下文中执行,来自由地加载其他模块的属性方法
  1. 模块定义
通过module.exports和exports两种方式来将模块中定义的成员导出
  1. 模块标识
模块标识指require函数中的参数,有以下特点:
  • 只能是字符串
  • 可以是相对路径和绝对路径
  • 可以省略文件后缀名

module.exports和exports

exportsmodule.exports 本质上是一个相同对象的引用
所以在一个文件中,我们最好选择 exportsmodule.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

  1. 导出一个变量
  1. 导出一个函数
  1. 常用导出方式(推荐)
  1. 重命名导出
可以利用as将模块输出多次。

ES6模块使用——import

  1. 一般用法
  1. As用法
import命令具有提升效果,会提升到整个模块的头部,首先执行,如下也不会报错:
  1. 整体模块加载 *

ES6模块使用——export default

使用export default命令时,import是不需要加{}的。而不使用export default时,import是必须加{}。
export default其实是导出一个叫做default的变量,所以其后面不能跟变量声明语句。
我们可以同时使用export 和export default

ES Module特性

  1. 编译时加载
ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 ,import , export 不能放在块级作用域或条件语句中。
这种静态语法,在编译过程中确定了导入和导出的关系,所以更方便去查找依赖,更方便去 tree shaking (摇树)
  1. 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模块是动态引用,并且不会缓存值。
> cd ..