模块化标准【AMD】【CMD】【CommonJS】【ES6】以及 module.exports、exports、export、export default、require、import 间的理解概论

一、模块化(原因和概念)

先介绍一下模块化是个什么东西,解决了什么。
模块化概念和分类这个部分转自别人转的,也不知道原博在哪,但是写得很不错,所以拿来分析总结一下。

JavaScript发展初期就是为了实现简单的页面交互逻辑,寥寥数语即可;如今CPU、浏览器性能得到了极大的提升,很多页面逻辑迁移到了客户端(表单验证等),随着 web2.0 时代的到来,Ajax技术得到广泛应用,jQuery等前端库层出不穷,前端代码日益膨胀
这时候JavaScript作为嵌入式的脚本语言的定位动摇了,JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了,JavaScript极其简单的代码组织规范不足以驾驭如此庞大规模的代码
既然JavaScript不能处理好如此大规模的代码,我们可以借鉴一下其它语言是怎么处理大规模程序设计的,在Java中有一个重要带概念——package,逻辑上相关的代码组织到同一个包内,包内是一个相对独立的王国,不用担心命名冲突什么的,那么外部如果使用呢?直接import对应的package即可import java.util.ArrayList;
遗憾的是JavaScript在设计时定位原因,没有提供类似的功能,开发者需要模拟出类似的功能,来隔离、组织复杂的JavaScript代码,我们称为模块化。
一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。模块开发需要遵循一定的规范,各行其是就都乱套了

简单来说就是以前前端简单,项目工程也小,现在前端突然发展开来,随之一些需求及项目复杂度也增加,js语言原本的定位不足支持这些工程,就需要向一些成熟的老大哥学习(java等),模块化使代码从一坨变成规整的一块一块。

利于维护(一块出问题了就维护一块)

利于分配工作(每人负责几块)

利于使用(想用哪块的话就拿来哪块)

1、前端模块化的前辈

js模块化有3个最基础的前辈

  1. 函数封装(代码还是一坨)
  2. 向其他语言学习,创建对象(面向对象编程,如果把函数比作能做的事,对象就是做事的人,想要做什么事,就找某个能做这个事的人,而不是去成千上万个事中找。还是一坨。)
  3. 通过<script>标签引入自调用函数包(已经变成块了,不过这个块只能连接一层,块之间的依赖只能手动增添,比如a.js依赖于b.js,然后手动添加script标签,然后b.js又需要依赖c/d/e/f/g/h.js等,也要手动写入,而且要注意写入的先后顺序,而且很多都需要导出一个函数或对象到全局作用域才能使用。)

前面的模块化也适应不了日益增长复杂的前端项目。 这时后端(也就是node.js)迈出了真正模块化的第一步——CommonJS标准

2、CommonJS(同步加载,同步执行)

参考原文中这么写——

因为在网页端没有模块化编程只是页面JavaScript逻辑复杂,但也可以工作下去,在服务器端却一定要有模块,所以虽然JavaScript在web端发展这么多年,第一个流行的模块化规范却由服务器端的JavaScript应用带来,CommonJS规范是由NodeJS发扬光大,这标志着JavaScript模块化编程正式登上舞台。

但是CommonJS的思想并不能用在浏览器上——

模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。
这在服务器端实现很简单,也很自然,然而, 想在浏览器端实现问题却很多。
浏览器端,加载JavaScript最佳、最容易的方式是在document中插入script标签。但脚本标签天生异步,传统CommonJS模块在浏览器环境中无法正常加载。
解决思路之一是,开发一个服务器端组件,对模块代码作静态分析,将模块与它的依赖列表一起返回给浏览器端。 这很好使,但需要服务器安装额外的组件,并因此要调整一系列底层架构。

就是说CommonJS是同步加载的,<script>标签是异步加载的,所以CommonJS的同步加载思想不能用在浏览器端上。

那问题来了,原文有这一句话——但脚本标签天生异步,传统CommonJS模块在浏览器环境中无法正常加载。

<script>标签不是会同步加载和执行并阻塞dom树的构建吗,怎么在他那边就变成异步加载了。
首先我们要先看清楚加载这个词的意思,它在浏览器端指的是下载,在服务器端可以指读取硬盘资源。
同步加载是指在构建dom树时原本就有的一些<script>标签,在前一个标签内容加载和执行完之前,会阻塞dom树的构建,也就是不会开始解析下面<script>标签,也就不会加载此标签的内容了。

但模块化需要的是让每一个模块本身可以自动加载所需依赖。所以肯定得手动用代码去添加<script>标签来加载和执行其中所带的代码段。

但是手动添加的<script>标签它就是异步下载的,也就是我想同时想要下载a、b两个依赖,它们会同时下载,而不是和CommonJS中的那样先下载完a再下载完b,而且如果浏览器端同步加载的话,因为网络下载的延迟性,会阻塞浏览器页面的显示。所以说环境和框架不同使用的标准也要有变化。

于是requireJSseaJS两个模块化工具出现了,随之而来的是requireJSseaJS分别衍生出来的两个标准AMDCMD

3、AMD(异步加载,异步执行)

AMD为前端JS制定了规范,它使用异步的方式去加载模块,并且所有与模块相关的代码都写在回调函数当中,不影响其他代码的执行。
它主张依赖提前,就是在开始便异步加载所有依赖,哪个依赖先加载完就先执行。不过主逻辑还是同步的等所有依赖加载完后再执行。
相对于使用自调用函数封装并用<script>标签导入来说,它解决了:

  1. 异步加载,不会阻塞浏览器构造dom树
  2. 自动加载依赖,不再需要手动引入模块间的依赖关系
    目前requireJScurlJS为这个标准的主要实现

4、CMD(异步加载,同步执行)

CMD是淘宝团队开发的SeaJS在推广过程中的产出,弥补了AMD的一大缺点——异步执行,由于AMD是异步加载,并且加载完直接执行,所以执行的顺序与加载速度有关,并且不可控,这就非常不友好,于是CMD提倡的依赖就近就解决了这个问题。

大家对依赖就近可能会有些误解,依赖就近不是就近加载,而是就近执行。

也就是说,最话时间的加载就和AMD差不多,在开头便开始加载所有依赖,但是CMD是寻找文档中的关键词来判断模块是否有依赖,进而进行加载。

但不影响主逻辑的执行,在主逻辑执行到相应依赖的时候再进行执行,并在回调函数中使用当前依赖。

5、ES6(编译时加载)

官方大佬出动,开始推进模块化了。它被作为是浏览器,服务器双端的标准,但现在浏览器还不支持,服务器也只能靠babel。。。。
在服务端,ES6CommonJS差不多,语法方面下面会有详解,先说一下加载运行方面的差异。
ES6是在编译的时候进行判定需要加载的依赖,编译也就是在加载和运行中间的过程,不能等执行就直接去寻找要加载的依赖。

啥是静态加载呢,也就是importexport在编译 时期通过编译代码的时候看到importexport这两个关键词就去寻找相应要加载的包,没有耐心再等你执行的时候if /else来判定导不导包。所以并不是很多博客所说的按需加载,而是按需导出。加载还是要全部加载的,执行也要全部执行,但导出就按需导出了。

效率要比CommonJS 模块的加载方式高。它可以同步加载同步运行——import关键字,也可以异步加载同步执行——import()方法。
循环加载时,但会因为引用而循环加载并执行。
ConnomJS再循环加载时,会缓存导出的默认值。下一次再加载就直接使用默认值而不是重新加载执行。

二、CommonJS 与 ES6模块化的语法不同

1、module.exports、exports、export、export default、require、import 间的区别

看到这个可能有点头晕,这么多到底哪个是哪个,但还是挺好分的。
其中module.exportsexportsrequire是属于CommonJS的。
exportexport default是属于ES6的。

CommonJS

首先来看看CommonJS中的语法

1、module.exports / exports

它们被用来导出某个模块中的方法、对象或者数据,最简单地来说其实和函数中的return差不多。

比如说模块a中
module.exports = 1
它就等于
return 1

exports 又是啥呢。它就相当于module.exports的引用,因为它身上的地址是指向 module.exports的,所以就能够通过它来操作。
举个例子

module.exports.a = 2
exports.a = 1//最后导出一个对象{a : 1}

也就是exports修改了与module.exports变量指向的相同地址中的 a 变量的值。

但是要注意了,最后导出的是 module.exports 最终的地址指向的内容。

什么意思呢,再举个例子

module.exports.a = 2
exports = { a : 1 }//最后导出{a : 2}

分析一下——比如module.exports指向的地址为0x00000000,因为exports指向刚开始module.exports所指向的地址,所以exports 的地址也为0x00000000,但是上边代码第二行中给exports,赋了个新值,也就是赋了个新的地址0x00000001,但是最后导出的还是module.exports变量所指向0x00000000这个地址的内容,也就是{a : 2}

还有一个注意点,结合我上边所说的注意点,最后导出的是 module.exports 最终的地址指向的内容。而不是 module.exports 最初的地址指向的内容。

再举个例子

module.exports = {a : 2}
exports.a = 1//最后导出{a : 2}

再来分析一下——在代码第一行,module.exports就通过赋值的方式改变了自己的地址,这时候它指向的地址从0x00000000变为0x00000002,但是exports指向的地址还是module.exports最初指向的地址,也就是0x00000000,这时候第二行代码exports改变了0x00000000地址内存中的数据,但是最终导出的,还是 module.exports 最终的地址指向的内容,也就是0x00000002地址中的{a : 2}
总结
其实这个逻辑就是将下面这两行代码隐藏起来了。

var exports = module.exoprts
//中间为自己码的代码段
return module.exoprts

所以挺简单的。

2、require

用法

var a = require('b')

简单来说就是将b模块里面的东西加载并执行了,执行后返回的结果就赋值给变量 a。
在循环加载时,也就是a模块中加载b模块,然后执行b模块,执行到b模块加载a模块时直接返回a模块已经执行的结果,不会陷入死循环。
而且重复加载的时候直接获取缓存。而不会再次执行。

注意,返回出来的变量如果是基本数据类型,则直接返回它的值,而不是它的地址, 这个也和return一样,也和函数传参一样。所以如果想要操作模块里面的基本数据类型的变量的话,只能通过getter()、setter()函数来操作,也就是常说的闭包。和java类中public的getter(),setter()的作用差不多。都是为了在保护模块内一些变量或者说不让模块内的变量去污染全局的情况下能够触摸或者说操作这些变量。

ES6

ES6模块中自动使用严格模式

1、export / export default

也是导出的关键词,但ES6对它们做了一些约束。

  1. 在导出时,export只能导出新声明的变量,或者直接导出个{},而export default都可以但不能声明(因为它和return一样,其后边不能加let、var、const)。
    举个例子
var a = 1
export a //报错
//=====================
export var a = 1//正确
//=====================
export {}//正确
export default var a = 1//错误
//=====================
var a = 1
export default a//正确
//=====================
export default 1//正确
//=====================
export default {}//正确
  1. 在导出引用类型时,export只能导出新声明的引用类型,而export default除了不能加var、let、const,其他都可以
var a = {}
export a //报错
//=====================
export {}//正确
//=====================
export var a = {}//正确
var a = {}
export default a//正确
//=====================
export default {}//正确
//=====================
export default	var a = {}//错误
  1. export 可以多次导出,export default只能导出一次
export var a = 1
export var b = 1//正确
export default a = 1
export default b = 1//报错
  1. 它们可以同时使用
export var a = 1
export default a
  1. export可以实现模块的继承
export * from 'a.js'
//意思是执行一遍a模块并且导出a模块中导出的所有内容
  1. export default只导出在执行时这个变量,而export导出最终的内容。
var a = 1
export default a//最终导出的值为1
a = 2
export var a = 1//最终导出的值为2
a = 2
  1. importexport只能出现在模块的最外层(顶层)结构中,否则报错
  2. 由于ES6模块是静态加载的,因此importexport不能出现在判断等动态语句中
  3. export可以使用as重命名来导出两个相同的变量,可用于新老版本的迭代过渡。
var a = 1
export {
	a as b,
	a as c	
}

总而言之,个人理解,将export说成是导出一个地址或者说导出一个值,而要理解成导出一个变量、接口,比如export var a = 1代表着导出了这个变量,这个变量的内容将一直随这个模块中的上下文执行而变化,因为导出的就是这个变量本身,所以外面也能看到它的变化,而不是像export a这样代表着直接导出模块内的变量a当前和之后的地址或者值。之前这个a的变化就会看不到。CommonJS就没有办法做到这个。
export { a }就可以当作向导出中添加接口a并与变量a相关联,并动态地指向变量a的地址,这种方法比较推荐,可以写在末尾清楚地看到要导出的变量。
export default就把它当作return来看待。与return不同的地方在于,export default之后的代码还是可以执行的。

1、import

比之require也多了点东西,import出来的任何东西都不能直接赋值。只能读取。

简单点就和用const声明的变量一样,只能读不能写。想知道const的用法可以参考一下ES6详解大全

  1. 如果按需导入 export 中的接口就要使用解构赋值来拿取而且命名要和原本导出的变量名一样,也可以使用重命名(最好少重命名)
//a.js
export var a = 1
//b.js
import { a } from './a.js'//正确
import { b } from './a.js'//错误
//b.js
import {a as b} from './a.js'//将变量a重命名为b
  1. 如果为默认导入export default则可以用任何命名来接收并且不需要解构赋值。
//a.js
export default 1
//b.js
import a from './a.js'//正确
import b from './a.js'//错误
  1. 重复导入只会执行一次所导入的模块中的内容。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值