
模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统
Node.js 是commonJS规范的主要实践者,他有4个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块
// 定义math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = {
add: add,
basicNum: basicNum,
};
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math.js');
math.add(2, 4);commonJS用同步的方式加载模块,在服务端,模块文件都存储在本地磁盘,读取非常快;但在浏览器中,限于网络原因,更合理的方式是使用异步加载
在编译的过程中,实际 Commonjs 对 js 的代码块进行了首尾包装, 我们以上述的 home.js 为例子🌰,它被包装之后的样子如下
(function (exports, require, module, __filename, __dirname) {
const sayName = require('./hello.js');
module.exports = function say() {
return {
name: sayName(),
author: '我不是外星人',
};
};
});
// 在 Commonjs 规范下模块中,会形成一个包装函数,我们写的代码将作为包装函数的执行上下文,使用的 require ,exports ,module 本质上是通过形参的方式传递到包装函数中的// 包装函数本质
function wrapper(script) {
return (
'(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
);
}
// 包装函数执行
const modulefunction = wrapper(`
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'author'
}
}
`);
// 如上模拟了一个包装函数功能, script 为我们在 js 模块中写的内容,最后返回的就是如上包装之后的函数。当然这个函数暂且是一个字符串
runInThisContext(modulefunction)(module.exports, require, module, __filename, __dirname)
// 在模块加载的时候,会通过 runInThisContext (可以理解成 eval ) 执行 modulefunction ,传入require ,exports ,module 等参数。最终我们写的 nodejs 文件就这么执行了const fs = require('fs') // 核心模块
const sayName = require('./hello.js') // 文件模块
const crypto = require('crypto-js') // 第三方自定义模块当 require 方法执行的时候,接收的唯一参数作为一个标识符 ,Commonjs 下对不同的标识符,处理流程不同,但是目的相同,都是找到对应的模块。
require加载标识符原则
首先像fs、http、path等标识符,会被作为nodejs的核心模块
./ 和 ../ 作为相对路径的文件模块。 / 作为绝对路径的文件模块
非路径形式也非核心模块,将会当作自定义模块
核心模块的优先级仅次于缓存加载,在Node源码编译中,已被编译成二进制代码,所以加载核心模块速度最快
路径形式的模块处理:已 ./ ,../ 和 / 开始的标识符,会被当作文件模块处理,require() 方法会将路径转化为真实路径,并以真实路径为索引,将编译后的结果缓存起来,第二次加载的时候会更快
自定义模块处理:自定义模块,一般指的是非核心的模块,它可能是一个文件或者一个包,它的查找会遵循以下原则
CommonJS 模块同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖,采用深度优先遍历(depth-first traversal),执行顺序是父 -> 子 -> 父
比如 main.js 和 a.js模块都引用了b.js, 但是b.js 模块只执行了一次;a.js 和 b.js 模块相互引用,但是没有造成循环引用的情况
// id 为路径标识符
function require(id) {
// 查找 Module 上有没有已经加载的 js 对象
// Module 整个系统运行之后,会用 Module 缓存每一个模块加载的信息
const cachedModule = Module._cache[id];
// 如果已经加载了那么直接取走缓存的 exports 对象
if (cachedModule) {
return cachedModule.exports;
}
// 创建当前模块的module
const module = { exports: {}, loaded: false }
// 将module缓存到Module的缓存属性中,路径标识符作为id
Module._cache[id] = module
// 加载文件
runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, __filename, __dirname)
// 加载完成
module.loaded = true
/* 返回值 */
return module.exports
}require流程
require 避免重复加载
require 避免循环引用
// a.js
const getMes = require('./b')
console.log('我是 a 文件')
exports.say = function(){
const message = getMes()
console.log(message)
}
// b.js
const say = require('./a')
const object = {
name:'《React进阶实践指南》',
}
console.log('我是 b 文件')
module.exports = function(){
return object
}
// 主文件 main.js
const a = require('./a')
const b = require('./b')
console.log('node 入口文件')如上第五步的时候,当执行 b.js 模块的时候,因为 a.js 还没有导出 say 方法,所以 b.js 同步上下文中,获取不到 say 解决方法:1. 动态加载a.js方法 2. 异步加载
require 动态加载
// 用 require 动态加载 b.js 模块
console.log('我是 a 文件')
exports.say = function(){
const getMes = require('./b')
const message = getMes()
console.log(message)
}// a.js
exports.name = `《React进阶实践指南》`
exports.say = function (){
console.log(666)
}
// main.js
const a = require('./a')
console.log(a)
// 打印结果
{name: '《React进阶实践指南》', say: [Function]}为什么 exports= 直接赋值一个对象就不可以呢
既然有了 exports,为何又出了 module.exports
与 exports 相比,module.exports 有什么缺陷
AMD规范采用异步方式加载模块,模块的加载不影响他后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块
首先我们需要引入require.js文件和一个入口文件main.js,main.js中配置require.config() 并规定项目中用到的基础模块
<!-- 网页中引入require.js及main.js -->
<script src="js/require.js" data-main="js/main"></script>
<!-- main.js 入口文件/主模块 -->
<!-- 首先用config()指定各模块路径和引用名 -->
<script>
require.config({
baseUrl: 'js/lib',
paths: {
jquery: 'jquery.min', //实际路径为js/lib/jquery.min.js
underscore: 'underscore.min',
},
});
// 执行基本操作
require(['jquery', 'underscore'], function ($, _) {
// some code here
});
</script>引用模块的时候,我们将模块名放在[]中作为reqiure()的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]中作为define()的第一参数。
// 定义math.js
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum: basicNum,
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'], function () {
var classify = function (list) {
_.countBy(list, function (num) {
return num > 30 ? 'old' : 'young';
});
};
return {
classify: classify,
};
});
// 引用模块,将模块放在[]内
require(['jquery', 'math'], function ($, math) {
var sum = math.add(10, 20);
$('#sum').html(sum);
});require.js在申明依赖的模块时会在第一之间加载并执行模块内的代码
define(['a', 'b', 'c', 'd', 'e', 'f'], function (a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.foo();
}
});CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的
/** AMD写法 **/
define(['a', 'b', 'c', 'd', 'e', 'f'], function (a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething();
}
});
/** CMD写法 **/
define(function (require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function (require, exports, module) {
var $ = require('jquery.js');
var add = function (a, b) {
return a + b;
};
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function (math) {
var sum = math.add(1 + 2);
});ES6 在语言标准的层面上,实现了模块功能,而且实现的相当简单,旨在成为浏览器和服务器通用的模块解决方案
其模块功能由两个命令构成:export 和 import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不需要使用大括号。这也更趋近于ADM的引用写法
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}// `export` 正常导出,`import` 导入
const name = '《React进阶实践指南》'
const author = '我不是外星人'
export { name, author }
export const say = function (){
console.log('hello , world')
}// name , author , say 对应 a.js 中的 name , author , say
import { name, author, say } from './a.js'默认导出 export default
const name = '《React进阶实践指南》'
const say = function (){
console.log('hello , world')
}
export default {
name,
say
} import mes from './a.js'
console.log(mes) //{ name: '《React进阶实践指南》', say: Function }混合导入|导出
export const name = '《React进阶实践指南》'
export const author = '2222'
export default function say (){
console.log('hello , world')
}// 第一种:
import theSay, { name, author as bookAuthor } from './a.js'
console.log(
theSay, // ƒ say() {console.log('hello , world') }
name, // "《React进阶实践指南》"
bookAuthor // "2222"
)
// 第二种:
import theSay, * as mes from './a'
console.log(
theSay, // ƒ say() { console.log('hello , world') }
mes // { name:'《React进阶实践指南》' , author: "2222" ,default: ƒ say() { console.log('hello , world') } }
)