前端模块化 | 冰岩分享
前端模块化
模块化
为什么要模块化
全局变量 依赖关系管理
不优雅地引入资源
<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
在过去,我们通过从上到下引入<script>
标签的方法来引入想要的资源。然而当项目越做越大时,有两个主要的问题也随之出现。一是每个资源中的变量和方法都会成为全局的变量,污染了全局作用域且不容易维护。二来资源加载之间的依赖关系通过引入资源的顺序决定。一旦项目稍大,资源引入的逻辑就会非常复杂。
模块化解决什么问题
安全的包装一个模块的代码,避免全局污染 唯一标识一个模块 优雅的将模块api暴露出去 方便的使用模块
AMD规范
当我们在服务端加载一个模块时,直接就从硬盘或者内存中读取,消耗时间可以忽略不计
而由于浏览器端需要从服务端下载模块文件,模块不能采用同步加载,因此有了AMD规范
var math = require('math');
math.add(2, 3);
Asynchronous Module Definition 异步模块加载机制
是RequireJS
在推广过程中对模块定义的规范化产出。
AMD制定了定义模块的规则,这样模块和模块的依赖可以被异步加载。这和浏览器的异步加载模块的环境刚好适应(浏览器同步加载模块会导致性能、可用性、调试和跨域访问等问题)。
模块定义 define
api
AMD规范简单到只有一个API,即define函数
define(id?, dependencies?, factory);
id
指定义中模块的名字
可选参数,默认为模块加载器请求的指定脚本的名字
dependencies
定义模块所依赖模块的数组
可选参数,默认为
["require", "exports", "module"]
factory
模块初始化要执行的函数或对象
如果为函数,它应该只被执行一次。 如果是对象,此对象应该为模块的输出值。
独立模块定义
// 独立模块定义
define({
method1: function() {}
method2: function() {}
});
// 或者
define(function(){
return {
method1: function() {},
method2: function() {},
}
});
非独立模块定义
define(['math', 'graph'], function(math, graph){
...
});
模块引入 require
require([module], callback);
AMD采用require()
语句加载模块,但是不同于CommonJS
,它要求两个参数:
[module]
,一个需要加载的模块的数组callback
,加载成功后的回调函数
require(['math'], function (math) {
math.add(2, 3);
});
❝「AMD主要解决了两个问题」
❞
浏览器响应时间
require()
函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应依赖关系
指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
CMD规范
Common Module Definition 通用模块规范
CMD是SeaJS
在推广过程中对模块定义的规范化产出。
SeaJS
是一个适用于 Web 浏览器端的模块加载器。使用SeaJS
,可以更好地组织JavaScript
代码。
推崇「依赖就近」的原则
模块定义 define
define(factory)
define
是一个全局函数,用来定义模块。factory
可以是一个函数,也可以是一个对象或字符串。
定义模块
define(function(require, exports, module){
//依赖模块a
var a = require('./a');
//调用模块a的方法
a.method();
})
模块引入 require
接受模块标识作为唯一参数,用来获取其他模块提供的接口。
require(id)
require.async
require.async(id, callback?)
模块内部异步加载模块,并在加载完成后执行指定回调
define(function(require, exports, module) {
// 异步加载一个模块,在加载完成时,执行回调
require.async('./b', function(b) {
b.doSomething();
});
// 异步加载多个模块,在加载完成时,执行回调
require.async(['./c', './d'], function(c, d) {
c.doSomething();
d.doSomething();
});
});
模块接口 exports
exports
是一个对象,是module.exports
对象的引用,用来向外提供模块接口。
define(function(require, exports) {
// 对外提供 foo 属性
exports.foo = 'bar';
// 对外提供 doSomething 方法
exports.doSomething = function() {};
});
也可以通过return
直接提供接口
define(function(require) {
// 通过 return 直接提供接口
return {
foo: 'bar',
doSomething: function() {}
};
});
module
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id
模块的唯一标识。
module.uri
根据模块系统的路径解析规则得到的模块绝对路径。
module.dependencies
一个数组,表示当前模块的依赖。
module.exports
当前模块对外提供的接口。
比较AMD与CMD
AMD中文版Wiki
CMD规范
相同点
都是为了 JavaScript 的模块化开发 达成「浏览器端模块化开发的目的」。
不同点
「AMD:依赖前置」
AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入
require
的回调函数,执行主逻辑。这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,而是由网络速度决定。哪个模块先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行
「CMD:依赖就近」
CMD加载完某个依赖模块后并不执行,只是下载下来,在所有依赖模块加载完成后进入主逻辑,遇到
require
语句的时候才执行对应的模块。这样模块的执行顺序和书写顺序是完全一致的
CommonJS
❝CommonJS defines a module format.
❞
Node
应用由模块组成,采用CommonJS模块规范
CommonJS定义了一种模块格式,是一种通用模块规范。
在CommonJS中,「每个文件就是一个模块」,拥有自己独立的作用域
module对象
module
是一个变量,代表当前模块。
Node内部提供一个Module
构建函数。所有模块都是Module
的实例。
function Module(id, parent) {
}
module.exports
表示模块对外输出的值。
module.id
模块的识别符,通常是带有「绝对路径」的模块文件名
module.fileName
带有「绝对路径」的模块文件名
module.loaded
返回一个布尔值,表示模块是否已经完成加载
module.parent
如果是在命令行下调用某个模块(即运行 node hello.js
),该属性为null
如果是在脚本中调用某个模块,(如 require('./hello.js');
),则返回一个Module
对象,表示调用该模块的模块。module.children
返回一个数组,表示该模块要用到的其他模块。
Module {
id: 'e:\\Programming\\JavaScript\\Node\\add.js',
path: 'e:\\Programming\\JavaScript\\Node',
exports: { add: [Function: add] },
parent: Module {
...
},
filename: 'e:\\Programming\\JavaScript\\Node\\add.js',
loaded: false,
children: [],
paths: [
'e:\\Programming\\JavaScript\\Node\\node_modules',
'e:\\Programming\\JavaScript\\node_modules',
'e:\\Programming\\node_modules',
'e:\\node_modules'
]
}
module.exports
与exports
❝"
❞exports
is assigned the value ofmodule.exports
before the module is evaluated."
exports
是module.exports
的一个引用,exports
指向的是module.exports
var exports = module.exports
mdoule.exports
初始化为一个空对象require()
返回的是**module.exports
而不是exports
**
知道以上三点后我们可以知道:
当给exports
对象添加属性时,我们同时也操作了module.exports
,是可行的;
exports.a = function() {};
当直接给exports
对象赋值时,断开了exports
与module.exports
的连接,moudle.exports
仍然是空对象,这时就无效了
exports = {
a: a,
b: b
}
因此建议,无论何时都使用module.exports
的形式导出模块
require
读入并执行一个JavaScript文件,然后「返回该模块的module.exports
对象」。
如果没有发现指定模块,会报错。
缓存 Caching
模块在第一次加载后会被缓存。
当之后要再加载该模块时,就直接从缓存中取出该模块的
module.exports
属性
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
如果想要多次执行某一模块,可以让该模块输出一个函数。每次
require
该模块时,重新执行函数。所有缓存的模块保存在
require.cache
之中
核心模块 Core Modules
❝Node.js has several modules compiled into the binary.
核心模块指的是被编译进二进制文件里的模块
❞
位置: lib/
相对于文件模块,核心模块会被 require()
优先加载
文件模块 File Modules
后缀名默认为.js
。
如果按确切的文件名没有找到模块,则Node.js
会尝试带上.js
、.json
或.node
拓展名再加载。
当没有以'/'
、'./'
或'../'
开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules
目录。
目录作为模块 Folders as Modules
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require
方法可以通过这个入口文件,加载整个目录。
在目录中放置一个package.json
文件,并且将入口文件写入main
字段。
// package.json
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
require
发现参数字符串指向一个目录以后,会自动查看该目录的package.json
文件,然后加载main
字段指定的入口文件。如果package.json
文件没有main
字段,或者根本就没有package.json
文件,则会加载该目录下的index.js
文件或index.node
文件。
从node_modules
加载
如果传给require()
的参数字符串既不是核心模块,也不是以'/'
、'./'
或'../'
开头,那么Node
会从当前模块所在的父目录开始加node_modules
,并尝试从node_modules
中加载模块
如果没有找到,那么将移动到父目录,依此类推,直到到达文件系统的根目录。
如:当我们在'e:\\Programming\\JavaScript\\Node\\add.js'
模块中加载require('hello.js')
时
'e:\\Programming\\JavaScript\\Node\\node_modules',
'e:\\Programming\\JavaScript\\node_modules',
'e:\\Programming\\node_modules',
'e:\\node_modules'
从全局文件夹加载
NODE_PATH
如果 NODE_PATH
环境变量被设为一个绝对路径列表,则当在其他地方找不到模块时 Node
会搜索这些路径。
其他全局目录
$HOME/.node_modules
$HOME/.node_libraries
$PREFIX/lib/node
其中$HOME
是用户的主目录,$PREFIX
是Node
里配置的node_prefix
加载顺序
缓存
以
/
,./
,../
开头,如require(./hello)
如果有,则将 main
属性指定的入口文件当作模块;如果没有,则会将 index
作为文件名查找是否存在index.js
,index.json
,index.node
文件先找文件,如果没有后缀自动补充
.js
、.json
、.node
进行查找在这里会查找当前目录下是否存在
hello.js
,hello.json
,hello.node
模块再找目录
在这里会查找当前目录下的
hello
目录,如果存在,会将该目录当作模块先看该目录下是否存在
package.json
不以
/
,./
,../
开头,如在C:/home
目录下require('hello')
先查找是否为核心模块
再到当前目录下的
node_modules
下找文件在这里会先查找
C:/home/node_modules
下是否存在hello.js
,hello.json
,hello.node
再到当前目录下的
node_modules
下找目录同上
不存在则向父级目录进行查找
C:/node_moudles
最后查找全局目录
ES6
在ES6
前,实现模块化使用的是RequireJS
或者SeaJS
ES6
引入了模块化
ES6
的模块自动开启严格模式
export 导出模块
export
命令可以出现在模块的任何位置,但必需处于模块顶层。
导出变量
export const name = 'Shaw';
导出函数
export function hello() {
console.log('hello world');
}
// 或者
function hello() {
console.log('hello world');
}
export hello;
导出多个变量/函数
const name = 'Shaw'
function hello() {
console.log('Hello World');
}
export { name, hello };
默认导出
在一个文件或模块中,export
、import
可以有多个,export default
仅有一个。
function hello() {
console.log('hello');
}
export default hello;
通过export
方式导出,在导入时要加{}
,export default
则不需要。
import 导入模块
引入export
导出的模块
import { name } from './hello';
引入export default
导出的模块
import name from './hello';
同时引入
export const name = 'Shaw'
export default hello() {
console.log('Hello');
}
引入时,默认值必须放在非默认值前
import hello, { name } from './hello.js'
as 重命名
export { hello as hi };
import { hi as hello } from './hello';
结语
如今前端生态不断注入新生命。在前端发展如此迅速的今天,用模块化规范去管理前端代码也就变得格外重要。希望以上内容对正在学习前端的朋友们可以有所帮助。