You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
让我们把时间追溯到 2009 年 1 月 29 日,在这个普通的冬天里,万物开始生长, JavaScript 社区中, Mozilla 的工程师 Kevin Dangoor 发布了一篇文章 《What Server Side JavaScript needs》,并在 Google Groups 中创建了一个 ServerJS 小组,旨在构建更好的 JavaScript 生态系统,包括服务器端、浏览器端,而后更名为 CommonJS 小组,发起了 CommonJS 的提案。
本系列的主题是 JavaScript 深入系列,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末。
如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。
前端模块化发展史
十年之前,模块化还只是使用「闭包」简单的实现一个命名空间。使用这种解决方式可以简单粗暴的处理全局变量和依赖关系等问题。
转眼间模块化已经发展了有十余年了,不同的工具和轮子层出不穷,下面是最各大工具或框架的诞生时间:
1. 时间线
不知不觉,模块化的发展已有十年之余了。
2. 模块化的目的
前端模块化,默认聊的就是 JavaScript 模块化,从一开始定位为简单的网页脚本语言,到如今可以开发复杂交互的前端,模块化的发展自然而然,目的无非是为了代码的可组织重用性、隔离性、可维护性、版本管理、依赖管理等。
前端模块跑在浏览器端,异步的加载 JavaScript 脚本,使模块化的考虑需要比后端从直接可以快速的本地加载模块的实现需要考虑的更多。
我们来看看都有哪些作品深刻的影响了前端模块化发展:
3. CommonJS 横空出世
让我们把时间追溯到 2009 年 1 月 29 日,在这个普通的冬天里,万物开始生长,
JavaScript
社区中,Mozilla
的工程师Kevin Dangoor
发布了一篇文章 《What Server Side JavaScript needs》,并在 Google Groups 中创建了一个 ServerJS 小组,旨在构建更好的 JavaScript 生态系统,包括服务器端、浏览器端,而后更名为 CommonJS 小组,发起了CommonJS
的提案。再到后来横空出世的
Node.js
采用CommonJS
模块化规范,同时还带来了npm
(全球最大的模块仓库) 。「nodejs 中模块的实现并非完全按照 CommonJS 的规范来的,而是进行了取舍,并增加了少许特性。」
Node.js
的发布是 2009 年末,基于CommonJS
社区最初的主流规范实现模块化,但是之后赢得了Server-side JavaScript
战争的Node.js
更加看重实际开发者的声音而不是CommonJS
社区许多腐朽化的规范,虽然大体上的使用方法未变,但之后 Node.jsmodules
发展其实于CommonJS
已分道扬镳。CommonJS
规范在服务端表现出色,使得JavaScript
在服务器端大放异彩,与传统服务器语言(PHP、Python)等产生抗衡甚至压倒之势。程序员们便萌发出了将它移植到浏览器端的想法。然而由于
CommonJS
的模块加载是同步的。我们知道,服务器端加载的模块从内存或磁盘中加载,耗时基本可忽略。但是在浏览器端却会造成阻塞,白屏时间过长,用户体验不够友好。因此,从
CommonJS
中逐渐产生了一些分支,也就是业内熟知的AMD
、CMD
等。讲到这里那就一定要谈一下 RequireJS 和 SeaJS:
随着 2015 年 6 月,ECMAScript 对 ES6 Modules 的正式发布,浏览器厂商和 Node.js 随之纷纷跟进实现,市面上的模块化加载库随之暗淡失色,间接给 CommonJS 社区判了死刑。在浏览器端取而代之流行的做法的是大家都使用 ES6 Modules 写法,然后使用 Babel 等的 transpiler 来应对不同浏览器版本的支持程度和在浏览器端异步特性产生的一些待解决的问题。Node.js 的模块还是大量的采用 CommonJS 模式,随着对 ES6 Modules 的支持力度的提高和可以兼容之前 CommonJS 模块,CommonJS 写法过渡到 ES6 Modules 只是时间的问题。
4. AMD 与 CommonJS 的兼容性
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD规范使用
define
方法定义模块,下面就是一个例子:AMD规范允许输出的模块兼容CommonJS规范,这时
define
方法需要写成下面这样:5. AMD 和 CMD 的区别
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
上述两种规范是服务于JavaScript的模块化开发,目前两种都能实现浏览器端的模块化开发的目的,不同之处是CMD是懒执行,AMD是预执行。
区别:
看代码:
虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。
● AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
6. RequirJS 和 SeaJS 的区别
两者的主要区别如下:
总之,如果说 RequireJS 是 Prototype 类库的话,则 Sea.js 致力于成为 jQuery 类库。
7. ES6 Module 原生支持
Javascript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 Javascript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。
因此,近年来,有必要开始考虑提供一种 将 JavaScript 程序拆分为可按需导入的单独模块 的机制。Node.js 已经提供这个能力很长时间了,还有很多的 Javascript 库和框架 已经开始了模块的使用(例如, CommonJS 和基于 AMD 的其他模块系统 如 RequireJS, 以及最新的 Webpack 和 Babel)。
好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 — 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
上面代码的实质是整体加载
fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。上面代码的实质是从
fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。下面我们详细讲解各个模块化方案的具体使用方法和细节:
CommonJS
CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。
Node 应用由模块组成,采用 CommonJS 模块规范。
CommonJS规范规定,每个模块内部,
module
变量代表当前模块。这个变量是一个对象,它的exports
属性(即module.exports
)是对外的接口。加载某个模块,其实是加载该模块的module.exports
属性。上面代码通过
module.exports
输出变量x
和函数addX
。require
方法用于加载模块。也可以为
module.exports
属性分配对象。在
square.js
中定义了square
模块:下面,
bar.js
使用了导出 Square 类的square
模块:CommonJS模块的特点如下:
1. exports 快捷导出
前面已经讲到了,通过
module.exports
可以导出模块,但这样未免有些繁琐。为了方便,Node为每个模块提供一个
exports
变量,指向module.exports
。这等同在每个模块头部,有一行这样的命令。它允许一个快捷方式,即
module.exports.f = ...
可以更简洁地写成exports.f = ...
。举个例子,以下是
circle.js
的内容:模块
circle.js
已导出函数area()
和circumference()
。 通过在特殊的exports
对象上指定额外的属性,将函数和对象添加到模块的根部。一个名为
foo.js
的文件:在第一行,
foo.js
加载了与foo.js
位于同一目录中的模块circle.js
。但是,请注意,与任何变量一样,如果将新值分配给
exports
,则它就不再绑定到当
module.exports
属性被新对象完全替换时,通常也会重新分配exports
:2. require 加载模块
Node使用CommonJS模块规范,内置的
require
命令用于加载模块文件。require
命令用于导入模块、JSON 和本地文件。运行下面的命令,可以输出exports对象。
如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在
module.exports
变量上面。上面代码中,require命令调用自身
require('./example2.js')()
,等于是执行module.exports
,因此会输出 hello world。3. 加载规则
require
命令用于加载文件,后缀名默认为.js
。根据参数的不同格式,
require
命令去不同路径寻找模块文件。(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,
require('/home/marco/foo.js')
将加载/home/marco/foo.js
。(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,
require('./circle')
将加载当前脚本同一目录的circle.js
。(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本
/home/user/projects/foo.js
执行了require('bar.js')
命令,Node会依次搜索以下文件。这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如
require('example-module/path/to/file')
,则将先找到example-module
的位置,然后再以它为参数,找到后续路径。(5)如果指定的模块文件没有发现,Node会尝试为文件名添加
.js
、.json
、.node
后,再去搜索。.js
件会以文本格式的JavaScript脚本文件解析,.json
文件会以JSON格式的文本文件解析,.node
文件会以编译后的二进制文件解析。(6)如果想得到
require
命令加载的确切文件名,使用require.resolve()
方法。● 目录的加载规则
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让
require
方法可以通过这个入口文件,加载整个目录。在目录中放置一个
package.json
文件,并且将入口文件写入main
字段。下面是一个例子。require
发现参数字符串指向一个目录以后,会自动查看该目录的package.json
文件,然后加载main
字段指定的入口文件。如果package.json
文件没有main
字段,或者根本就没有package.json
文件,则会加载该目录下的index.js
文件或index.node
文件。● 环境变量 NODE_PATH
Node执行一个脚本时,会先查看环境变量
NODE_PATH
。它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node会去这些路径查找。可以将NODE_PATH添加到
.bashrc
。所以,如果遇到复杂的相对路径,比如下面这样。
有两种解决方法,一是将该文件加入
node_modules
目录,二是修改NODE_PATH
环境变量,package.json
文件可以采用下面的写法。NODE_PATH
是历史遗留下来的一个路径解决方案,通常不应该使用,而应该使用node_modules
目录机制。4. 模块的缓存
模块在第一次加载后被缓存。 这意味着(类似其他缓存)每次调用 require('foo') 都会返回完全相同的对象(如果解析为相同的文件)。
如果 require.cache 没有被修改,则多次调用 require('foo') 不会导致模块代码被多次执行。 这是重要的特征。 有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。
要让模块多次执行代码,则导出函数,然后调用该函数。
5. 模块的循环加载
如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。
循环
require()
调用时,模块在返回时可能尚未完成执行。考虑这种情况:
a.js
:b.js
:main.js
:当
main.js
加载a.js
时,a.js
依次加载b.js
。 此时,b.js
尝试加载a.js
。 为了防止无限循环,将a.js
导出对象的未完成副本返回给b.js
模块。 然后b.js
完成加载,并将其exports
对象提供给a.js
模块。到
main.js
加载这两个模块时,它们都已完成。 因此,该程序的输出将是:需要仔细规划以允许循环模块依赖项在应用程序中正常工作。
6. 模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。
下面是一个模块文件
lib.js
。上面代码输出内部变量
counter
和改写这个变量的内部方法incCounter
。然后,加载上面的模块。
上面代码说明,
counter
输出以后,lib.js
模块内部的变化就影响不到counter
了。AMD和RequirJS
AMD 简介:
AMD
全称为Asynchromous Module Definition
(异步模块定义)AMD
是RequireJS
在推广过程中对模块定义的规范化产出,它是一个在浏览器端模块化开发的规范。AMD
模式可以用于浏览器环境并且允许非同步加载模块,同时又能保证正确的顺序,也可以按需动态加载模块。RequireJS 概述:
RequireJS
是一个JavaScript
文件和模块加载器。它针对浏览器内使用进行了优化,非常适合在浏览器中使用,但也可以在其他 JavaScript 环境中使用,例如Rhino和Node。使用像RequireJS
这样的模块化脚本加载器将提高代码的速度和质量。1. AMD 模块规范
AMD
通过异步加载模块。模块加载不影响后面语句的运行。所有依赖某些模块的语句均放置在回调函数中。AMD
规范只定义了一个函数define
,通过define
方法定义模块。该函数的描述如下:
AMD
规范允许输出模块兼容CommonJS
规范,这时define
方法如下:这与我们前文讲到的 「AMD 与 CommonJS 的兼容性」想呼应。
2. RequireJS 模块系统
RequireJS的基本思想是,通过
define
方法,将代码定义为模块;通过require
方法,实现代码的模块加载。首先,将
require.js
嵌入网页,然后就能在网页中进行模块化编程了。上面代码的
data-main
属性不可省略,用于指定主代码所在的脚本文件,在上例中为scripts
子目录下的main.js
文件。用户自定义的代码就放在这个main.js
文件中。3. define 定义模块
define
方法用于定义模块,RequireJS
要求每个模块放在一个单独的文件里。按照是否依赖其他模块,可以分成两种情况讨论。第一种情况是定义独立模块,即所定义的模块不依赖其他模块;第二种情况是定义非独立模块,即所定义的模块依赖于其他模块。
(1)独立模块
如果被定义的模块是一个独立模块,不需要依赖任何其他模块,可以直接用define方法生成。
上面代码生成了一个拥有method1、method2两个方法的模块。
另一种等价的写法是,把对象写成一个函数,该函数的返回值就是输出的模块。
后一种写法的自由度更高一点,可以在函数体内写一些模块初始化代码。
值得指出的是,define定义的模块可以返回任何值,不限于对象。
(2)非独立模块
如果被定义的模块需要依赖其他模块,则define方法必须采用下面的格式。
define
方法的第一个参数是一个数组,它的成员是当前模块所依赖的模块。比如,[‘module1’, ‘module2’]
表示我们定义的这个新模块依赖于module1
模块和module2
模块,只有先加载这两个模块,新模块才能正常运行。一般情况下,module1
模块和module2
模块指的是,当前目录下的module1.js
文件和module2.js
文件,等同于写成[’./module1’, ‘./module2’]
。define
方法的第二个参数是一个函数,当前面数组的所有成员加载成功后,它将被调用。它的参数与数组的成员一一对应,比如function(m1, m2)
就表示,这个函数的第一个参数m1
对应module1
模块,第二个参数m2
对应module2
模块。这个函数必须返回一个对象,供其他模块调用。上面代码表示新模块返回一个对象,该对象的
method
方法就是外部调用的接口,menthod
方法内部调用了m1
模块的methodA
方法和m2
模块的methodB
方法。需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。
如果依赖的模块很多,参数与模块一一对应的写法非常麻烦。
为了避免像上面代码那样繁琐的写法,
RequireJS
提供一种更简单的写法。下面是一个
define
实际运用的例子。上面代码定义的模块依赖
math
和graph
两个库,然后返回一个具有plot
接口的对象。另一个实际的例子是,通过判断浏览器是否为
IE
,而选择加载zepto
或jQuery
。上面代码定义了一个中间模块,该模块先判断浏览器是否支持
__proto__
属性(除了IE,其他浏览器都支持),如果返回true
,就加载zepto
库,否则加载jQuery
库。4. require 调用模块
require
方法用于调用模块。它的参数与define
方法类似。上面方法表示加载
foo
和bar
两个模块,当这两个模块都加载成功后,执行一个回调函数。该回调函数就用来完成具体的任务。require
方法的第一个参数,是一个表示依赖关系的数组。这个数组可以写得很灵活,请看下面的例子。上面代码加载JSON模块时,首先判断浏览器是否原生支持JSON对象。如果是的,则将
undefined
传入回调函数,否则加载util
目录下的json2
模块。require
方法也可以用在define
方法内部。下面的例子显示了如何动态加载模块。
上面代码所定义的模块,内部加载了
foo
和bar
两个模块,在没有加载完成前,isReady
属性值为false
,加载完成后就变成了true
。因此,可以根据isReady
属性的值,决定下一步的动作。下面的例子是模块的输出结果是一个promise对象。
上面代码的
define
方法返回一个promise
对象,可以在该对象的then
方法,指定下一步的动作。如果服务器端采用JSONP模式,则可以直接在
require
中调用,方法是指定JSONP的callback
参数为define
。require
方法允许添加第三个参数,即错误处理的回调函数。require
方法的第三个参数,即处理错误的回调函数,接受一个error
对象作为参数。require
对象还允许指定一个全局性的Error
事件的监听函数。所有没有被上面的方法捕获的错误,都会被触发这个监听函数。小结:
define
和require
这两个定义模块、调用模块的方法,合称为AMD模式。它的模块定义的方法非常清晰,不会污染全局环境,能够清楚地显示依赖关系。AMD模式可以用于浏览器环境,并且允许非同步加载模块,也可以根据需要动态加载模块。
CMD和SeaJS
SeaJS 的作者是「玉伯」。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD(Common Module Definition) 模块定义规范。该规范明确了模块的基本书写格式和基本交互规则。
CMD 表示通用模块定义,该规范是国内发展出来的,由阿里的玉伯提出。就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS和requireJS一样,都是javascript的模块化解决方案。
SeaJS本身遵循KISS(Keep It Simple, Stupid)理念进行开发,其本身仅有个位数的API,因此学习起来毫无压力。在学习SeaJS的过程中,处处能感受到KISS原则的精髓——仅做一件事,做好一件事。
1. SeaJS 模块系统
Sea.js 是一个适用于 Web 浏览器端的模块加载器。在 Sea.js 里,一切皆是模块,所有模块协同构建成模块系统。Sea.js 首要要解决的是模块系统的基本问题:
在前端开发领域,一个模块,可以是JS 模块,也可以是 CSS 模块,或是 Template 等模块。在 Sea.js 里,我们专注于 JS 模块(其他类型的模块可以转换为 JS 模块):
把上面两点中提及的基本书写格式和基本交互规则描述清楚,就能构建出一个模块系统。对书写格式和交互规则的详细描述,就是模块定义规范(Module Definition Specification)。比如 CommonJS 社区的 Modules 1.1.1 规范,以及 NodeJS 的 Modules规范,还有 RequireJS 提出的 AMD 规范等等。
2. CMD 模块规范
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD
(Common Module Definition)
模块定义规范。该规范明确了模块的基本书写格式和基本交互规则。在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:
3. define 定义模块
define
是一个全局函数,用来定义模块。●
define(factory)
define
接受factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。factory
为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:也可以通过字符串定义模板模块:
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory
方法在执行时,默认会传入三个参数:require
、exports
和module
:●
define(id?, deps?, factory)
define
也可以接受两个以上参数。字符串id
表示模块标识,数组deps
是模块依赖。比如:id
和deps
参数可以省略。省略时,可以通过构建工具自动生成。注意:带
id
和deps
参数的define
用法不属于 CMD 规范,而属于 Modules/Transport 规范。● define.cmd
Object
一个空对象,可用来判定当前页面是否有 CMD 模块加载器:
4. require 加载模块
require
是factory
函数的第一个参数。●
require(id)
require
是一个方法,接受模块标识
作为唯一参数,用来获取其他模块提供的接口。注意:在开发时,
require
的书写需要遵循一些 简单约定。●
require.async(id, callback?)
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。注意:
require
是同步往下执行,require.async
则是异步回调执行。require.async
一般用来加载可延迟异步加载的模块。●
require.resolve(id)
使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
这可以用来获取模块路径,一般用在插件环境或需动态拼接模块路径的场景下。
5. exports 导出模块
exports
是一个对象,用来向外提供模块接口。除了给
exports
对象增加成员,还可以使用return
直接向外提供接口。如果
return
语句是模块中的唯一代码,还可简化为:上面这种格式特别适合定义 JSONP 模块。
特别注意:下面这种写法是错误的!
正确的写法是用
return
或者给module.exports
赋值:提示:
exports
仅仅是module.exports
的一个引用。在factory
内部给exports
重新赋值时,并不会改变module.exports
的值。因此给exports
赋值是无效的,不能用来更改模块接口。6. module 对象
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。● module.id
String
模块的唯一标识。
上面代码中,
define
的第一个参数就是模块标识。● module.uri
String
根据模块系统的路径解析规则得到的模块绝对路径。
一般情况下(没有在
define
中手写id
参数时),module.id
的值就是module.uri
,两者完全相同。● module.dependencies
Array
dependencies
是一个数组,表示当前模块的依赖。● module.exports
Object
当前模块对外提供的接口。
传给
factory
构造方法的exports
参数是module.exports
对象的一个引用。只通过exports
参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过module.exports
来实现:这就是 CMD 模块定义规范的所有内容。经常使用的 API 只有
define
,require
,require.async
,exports
,module.exports
这五个。其他 API 有个印象就好,在需要时再来查文档,不用刻意去记。与 RequireJS 的 AMD 规范相比,CMD 规范尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。通过 CMD 规范书写的模块,可以很容易在 Node.js 中运行.
ES6 Module
Javascript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 Javascript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。
因此,近年来,有必要开始考虑提供一种 将 JavaScript 程序拆分为可按需导入的单独模块 的机制。Node.js 已经提供这个能力很长时间了,还有很多的 Javascript 库和框架 已经开始了模块的使用(例如, CommonJS 和基于 AMD 的其他模块系统 如 RequireJS, 以及最新的 Webpack 和 Babel)。
好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 — 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
上面代码的实质是整体加载
fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。上面代码的实质是从
fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。◾️ 使用JavaScript 模块依赖于 import 和 export
1. 导出 export
为了获得模块的功能要做的第一件事是把它们导出来。使用
export
语句来完成。最简单的方法是把它(指上面的
export
语句)放到你想要导出的项前面,比如:你能够导出函数,var,let,const, 和等会会看到的类。export要放在最外层;比如你不能够在函数内使用export。
● 有一个更方便的方法想可以导出所有你想要导出的模块,即在模块文件的末尾使用一个
export
语句, 语句是用花括号括起来的用逗号分割的列表。比如:2. 导入 import
你想在模块外面使用一些功能,那你就需要导入他们才能使用。最简单的就像下面这样的:
使用
import
语句,然后你被花括号包围的用逗号分隔的你想导入的功能列表,然后是关键字from
,然后是模块文件的路径。模块文件的路径是相对于站点根目录的相对路径。3. 默认导出 default export
● 默认导出
(default export )
VS 命名导出(named exports)
到目前为止我们导出的功能都是由 命名导出
(named exports)
组成 —— 每个项目(无论是函数,常量等)在导出时都由其名称引用,并且该名称也用于在导入时引用它。还有一种导出类型叫做 默认导出
(default export )
—— 这样可以很容易地使模块提供默认功能,并且还可以帮助JavaScript模块与现有的CommonJS和AMD模块系统进行互操作。使用
import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。● 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到
export default
命令,为模块指定默认输出。上面代码是一个模块文件
export-default.js
,它的默认输出是一个函数。其他模块加载该模块时,
import
命令可以为该匿名函数指定任意名字。上面代码的
import
命令,可以用任意名称指向export-default.js
输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import
命令后面,不使用大括号。●
export default
命令用在非匿名函数前,也是可以的。上面代码中,
foo
函数的函数名foo
,在模块外部是无效的。加载的时候,视同匿名函数加载。下面比较一下默认输出和正常输出。
上面代码的两组写法:
export default
时,对应的import
语句不需要使用大括号;export default
时,对应的import
语句需要使用大括号。export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
命令后面才不用加大括号,因为只可能唯一对应export default
命令。本质上,export default就是输出一个叫做
default
的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。正是因为
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。上面代码中,
export default a
的含义是将变量a
的值赋给变量default
。所以,最后一种写法会报错。同样地,因为
export default
命令的本质是将后面的值,赋给default
变量,所以可以直接将一个值写在export default
之后。上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为
default
。有了
export default
命令,输入模块时就非常直观了,以输入 lodash 模块为例。如果想在一条
import
语句中,同时输入默认方法和其他接口,可以写成下面这样。对应上面代码的
export
语句如下。如果想在一条
import
语句中,同时输入默认方法和其他接口,可以写成下面这样。对应上面代码的
export
语句如下。上面代码的最后一行的意思是,暴露出
forEach
接口,默认指向each
接口,即forEach
和each
指向同一个方法。export default
也可以用来输出类。4. 重命名
在你的
import
和export
语句的大括号中,可以使用as
关键字跟一个新的名字,来改变你在顶级模块中将要使用的功能的标识名字。因此,例如,以下两者都会做同样的工作,尽管方式略有不同:5. 模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(
*
)指定一个对象,所有输出值都加载在这个对象上面。下面是一个
circle.js
文件,它输出两个方法area
和circumference
。现在,加载这个模块。
上面写法是逐一指定要加载的方法,整体加载的写法如下。
注意,模块整体加载所在的那个对象(上例是
circle
),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。参考
查看原文
查看全部文章
博文系列目录
交流
各系列文章汇总:https://github.com/yuanyuanbyte/Blog
我是圆圆,一名深耕于前端开发的攻城狮。
The text was updated successfully, but these errors were encountered: