模块化
什么是模块化
Javascript在最初的时候没有自己的模块化标准和引入外部模块的方法,类似于 c 的 #include
java 的 package
,一个完备的编程语言会考虑一个工程在运行时各个模块的引用和依赖关系。但显然 Javascript 在设计之初只是被当作了一个网页端的脚本语言,既然 HTML 中可以引用js,为什么还要设计 js 中引入 js 的方法呢。可是 Javascript 的发展实在是太快了,我们知道现在 Javascript 不仅在网页中使用,服务器(Nodejs),PC客户端(Electron)甚至移动客户端都有Js的程序。如何更好的定义模块并进行模块间的引用成了Javascript的当务之重。
模块化是一种将系统分离成独立功能部分的方法,可将系统分割成独立的功能部分,严格定义模块接口、模块间具有透明性。
为什么模块化
- 可以解决命名冲突
- 更好的管理依赖
- 提高代码的可读性
- 代码解耦,提高代码的复用性
目前常见的JS模块化规范
CommonJS规范 是在服务器端模块的规范,是同步加载的。应用有 Nodejs。
AMD规范(Asynchronous Module Definition)是 RequireJS 在推广过程中对模块定义的规范化产出,主要用于浏览器端。其特点是:依赖前置,需要在定义时就写好需要的依赖,提前执行依赖,应用有require.js
CMD规范 (Common Module Definition)是 Sea.js 在推广过程中对模块定义的规范化产出,主要用于浏览器端。其主要特点是:对于依赖的模块是延迟执行,依赖可以就近书写,等到需要用这个依赖的时候再引入这个依赖,应用有sea.js
ES6 module 在 ES6 中定义了js语言的模块化规范。从此 AMD 和 CMD 退出了历史舞台。
CommonJS
CommonJS规范参见 http://wiki.commonjs.org/wiki/CommonJS
Nodejs采用了CommonJS的模块化规范,下面列出了CommonJS中模块部分相关的变量,并以 Nodejs 的实现来解释含义和用法。
require
require 方法用于加载模块文件。require 方法的基本功能是,接受一个模块标示符,返回外部模块的 exports 对象或方法。如果没有发现指定模块,会报错。如果有循环调用,require 返回的对象必须至少包含外部模块在调用 require 之前准备好的导出,该调用导致当前模块的执行。
模块标示符是指由正斜杠分隔的特殊字符串。这些字符串需要是驼峰化的,或者 . (代表当前路径)或者 .. (代表上一个路径)。可以省略文件后缀(例如.js)。顶层标识符从模块名称空间根解析。相对标识符是相对于写入和调用 require 的模块的位置来解析。
在 Nodejs 中,require 方法有以下方法 & 属性
resolve 方法
使用内部的 require 机制查询模块的位置,此操作只返回解析后的文件名,不会加载该模块。resolve.paths 方法
返回一个数组,其中包含解析 request 过程中被查询的路径,如果 request 字符串指向核心模块(例如 http 或 fs)则返回 null。main 对象
Module 对象实例,表示当 Node.js 进程启动时加载的入口脚本。extensions 对象
可以加载的文件类型及方法,node中默认支持.js
.json
.node
文件,如果省略后缀,也会按照这三种的次序依次补足扩展名然后尝试加载。可以通过例如以下方法来让node加载不同的文件后缀。1
require.extensions['.sjs'] = require.extensions['.js'];
cache 对象
require缓存,文件的绝对路径和 Module 对象实例的健值对数组。当再次require时,会返回缓存的模块。从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。
和标准稍微有些不同的是:
如果模块标志符是 Nodejs 默认携带的核心模块,会加载响应的核心模块,例如fs
、net
,或者一个位于各级 node_modules
目录的已安装模块。
如果 require
通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,Nodejs 会将目录当做一个 Nodejs 包来处理,首先,Nodejs 在当前目录下查找 package.json (CommonJS包规范定义的包描述文件),通过 JSON.parse()
解析出包描述对象,从中取出 main
属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。而如果 main
属性指定的文件名错误,或者压根没有 package.json 文件,Nodejs 会将 index 当做默认文件名,然后依次查找 index.js、index.json、index.node。
module
module 指加载后的模块。CommonJS 规范中,module 中应包括变量 require 指向 require 方法,变量 exports 指向当外部其它模块导入时可以引用的对,变量 module 对象 包括唯一的可被追溯的 id 属性以及 uri 属性指向模块的资源位置。
在 Node.js 中,稍微有些不同,module对象是一个 Module 类的实例,这个对象包括以下属性 & 方法:
- id
模块的标识符。 通常是完全解析后的文件名。 - exports
将期望导出的对象赋值给 module.exports 可以让其它模块使用 - parent
最先引用该模块的模块。 - filename
模块的完全解析后的文件名。
相当于 require.resolve(\/模块标志符\/) - loaded
模块是否已经加载完成,或正在加载中。 - children
被该模块引用的模块对象。 - paths
模块的搜索路径。
相当于 require.resolve.paths(module.filenname) - require 方法
和require方法类似,也是引入其它模块,但是相对path是当前的module
exports
exports 变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给 module.exports。
它允许使用快捷方式,因此 module.exports.f = ...
可以更简洁地写成 exports.f = ...
。 但是,就像任何变量一样,如果为 exports 赋予了新值,则它将不再绑定到 module.exports。
当 module.exports 属性被新对象完全替换时,通常也会重新赋值 exports。
AMD
AMD 规范采用异步方式加载模块,模块的加载不影响之后语句的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行。
实现了AMD的 RequireJS
RequireJS 实现了 AMD 规范的模块化:用 require.config()
指定引用路径等参数,用 definde()
定义模块,用 require()
加载模块。
1 | /** 网页中引入require.js及main.js **/ |
引用模块的时候,我们将模块名放在 []
中作为 reqiure()
的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在 []
中作为 define()
的第一参数。
1 | // 定义math.js模块 |
CMD
AMD的实现者 require.js
在申明依赖的模块时,会在第一时间加载并执行模块内的代码。
1 | define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { |
遇到如上所述情况时,即使没有使用到某些模块,由于依赖关系仍旧需要等待加载,这在一定程度上浪费了时间。
CMD 是另一种js模块化方案,它与 AMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
实现了CMD的 Sea.js 作者 王保平 玉伯 阿里p10 知乎达人
1 | /** AMD写法 **/ |
Sea.js 的用法:
1 | /** sea.js **/ |
UMD
UMD(Universal Module Definition) 并不是一个规范而是 AMD 规范和 CommonJS 规范的一个兼容性的糅合。AMD 是浏览器优先,异步加载;CommonJS 是服务器优先,同步加载。UMD 通过判断是否支持 Node.js 的模块来决定是按照 COMMONJS 规范来加载还是按照 AMD 规范来加载。
UMD 实现参见 umd
1 | ((root, factory) => { |
ES6 module
ES6 module 为Javascript 引入了模块化的标准。模块功能主要由两个关键字构成: export
和 import
。 export
关键字用于规定模块的对外接口, import
关键字用于输入其他模块提供的功能。
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
可以导出变量、方法和类。可以直接导出,也可以在文件中将待导出的变量、方法或类作为对象的属性导出。
export 关键字可以出现在模块的任何位置,但不能处于块级作用域内。
使用 export default 关键字,为模块指定默认输出。一个模块只能有一个默认输出,因此 export default 命令只能使用一次
1 | export var firstName = 'Michael'; |
1 | var firstName = 'Michael'; |
1 | export default function () { |
import
使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块。
import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同。
import 后面的 from 指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js 路径可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
import 命令是编译阶段执行的,在代码运行之前。由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
import 语句会执行所加载的模块。
如果多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次。
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
当用到 export default 命令,为模块指定默认输出时,其他模块加载该模块时,import 命令可以为该匿名函数指定任意名字(此时不使用{})。
1 | import {firstName, lastName, year} from './profile'; |
1 | export function area(radius) { |
1 | import * as circle from './circle'; |
1 | import customName from './export-default'; |
as
可以使用as关键字重命名导出或导入的变量。
1 | import { lastName as surname } from './profile'; |
require 和 import 引入的区别
- require 是 Commonjs 规范的模块化引入语法, import 是 ES6 的模块化引入语法
- require 是运行时加载,import 是编译时加载,执行时效率更高
- require 可以在任意位置引入包括语句块里,import 会提升到整个模块的头部
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJs 是单个值导出,ES6 Module可以导出多个
- CommonJs 的 this 是当前模块,ES6 Module 的 this 是 undefined