前言
在学习新一个东西前,最好的办法是了解它能给我们带来什么好处我们为什么学习它,从而激发学习兴趣,哈哈哈~又扯了一堆,下面进入我们前端模块化探索之旅吧。
模块化的历史
在正式进入模块化学习前我们来了解一下前端模块化的前生今世。
这里我就不过多赘述了,可以参考玉伯大佬的前端模块化开发的价值
模块化规范
现有模块化标准:
- CommonJS
- AMD
- CMD
- ES6
浏览器加载脚本
传统方法
1 | <!-- 页面内嵌的脚本 --> |
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
- defer是“渲染完再执行”
- async是“下载完就执行”
CommonJS
Node.js是commonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
模块化
1 | // example.js |
上面代码中,变量x和函数addX,是当前文件example.js私有的,其他文件不可见。
global对象
如果想在多个文件分享变量
1 | global.__ENV__ = dev; |
上面代码的ENV变量,可以被所有文件读取。当然,需要尽量避免定义全局属性
导出模块
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
1 | var exports = module.exports; |
在对外输出模块接口时,可以向exports对象添加方法
1 | exports.area = function (r) { |
注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
1 | exports = function(x) {console.log(x)}; |
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
1 | exports.hello = function() { |
下面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了
引入模块
Node使用CommonJS模块规范,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
特性
commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。
导入模块
1 | // math.js |
加载规则
如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require(‘/home/marco/foo.js’)将加载/home/marco/foo.js。
如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(‘./circle’)将加载当前脚本同一目录的circle.js。
如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require(‘example-module/path/to/file’),则将先找到example-module的位置,然后再以它为参数,找到后续路径。
如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。
如果想得到require命令加载的确切文件名,使用require.resolve()方法。
如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本/home/user/projects/foo.js执行了require(‘bar.js’)命令,Node会依次搜索以下文件。
1 | /usr/local/lib/node/bar.js |
引入
1 | // 引用自定义的模块时,参数包含路径,可省略.js |
目录的加载规则
在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。
1 | // package.json |
module对象
Node内部提供一个Module构建函数。所有模块都是Module的实例。
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
ES6
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
1 | <script type="module"> |
ES6 模块与 CommonJS 模块的差异
它们有两个重大差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
ES6 模块加载 CommonJS 模块
CommonJS 模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx。
1 | // a.js |
一共有三种写法,可以拿到 CommonJS 模块的module.exports。
1 | // 写法一 |
CommonJS 模块加载 ES6 模块
CommonJS 模块加载 ES6 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性。
导出模块
导出方式
1、多次使用export
1 | // profile.js |
export命令对外部输出了三个变量。
2、导出对象
1 | // profile.js |
3、直接导出单个变量、函数
1 | export function multiply(x, y) { |
4、导出默认模块
1 | // 正确 |
导出注意点
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。(简而言之导出的必须是对象的key)
必须建议意义对应关系
1 | export var year = 1958; |
1 | // 报错 |
上面两种写法都会报错,因为没有提供对外的接口。正确写法:
1 | // 写法一 |
同样的,function和class的输出,也必须遵守这样的写法。
1 | // 报错 |
另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
模块别名
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type=”module”属性
浏览器对于带有type=”module”的<script>
,等同于打开了<script>
标签的defer属性。
ES6 模块也允许内嵌在网页中
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明use strict。
- 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
- 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
- 同一个模块如果加载多次,将只执行一次。
示例模块
1 | import utils from 'https://example.com/js/utils.js'; |
导入模块
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
1 | // main.js |
注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。
1 | foo(); |
导入默认模块
1 | // export-default.js |
1 | // import-default.js |
导入别名设置
将surname是lastName的别名
1 | import { lastName as surname } from './profile.js'; |
将多个方法融合成一个
1 | // circle.js |
1 | import * as circle from './circle'; |
按需加载
1 | import('./utils.js') |
CMD
CMD(common module definition), 是sea.js在推广过程中对模块定义的规范化产出,主要用于浏览器端。推崇依赖就近、延迟执行。它的核心思想是:每个文件都是一个模块,在模块中定义的变量、函数、类都是私有的,对外不可见。有一个全局性方法require(),用于加载模块
AMD规范(Asynchronous Module Definition):是 RequireJS 在推广过程中对模块定义的规范化产出,也是主要用于浏览器端。其特点是:依赖前置,需要在定义时就写好需要的依赖,提前执行依赖,应用有require.js