模块化概述
Javascript不是一种模块化编程语言,它不支持“类”(class),更遑论“模块”(module)了,随着前端发展对模块需求越来越大,模块也经历了从最初的简单模块写法到AMD和CMD规范的出现,再到ES6发布,目前已经可以很方便的在Javascript中使用“类”和“模块”了。
模块化规范
浏览器端规范
- AMD(require.js遵循的AMD规范)
- CMD(Sea.js遵循的CMD规范)
- ES6
服务器端规范
- CommonJS(Node.js是CommonJS的实现)
- Node的应用是由模块组成的
CommonJS特点
- 所有代码运行在当前模块作用域,不会污染全局作用域
- 模块同步加载,根据代码执行顺序载入
- 模块可多次加载,只会在第一次加载的时候运行一次,运行结果被缓存
- 以后再次加载模块,会直接从缓存中读取结果
// export.js
module.exports = {
name: 'taki',
getName: function (name) {
console.log(name)
}
}
// require.js
let person = require('./export')
console.log(person)
console.log(person.name)
person.getName('taki_01')
> node require.js
{ name: 'taki', getName: [Function: getName] }
taki
taki_01
module对象详解
module对象属性
- id:当前模块id
- exports:表示当前模块暴露给外部的值
- parent:是一个对象,表示调用当前模块的模块
- children:是一个对象,表示当前模块调用的模块
- filename:模块的绝对路径
- paths: 从当前模块开始查找node_modules目录,然后依次进入到父目录,查找父目录下的node_modules目录;依次迭代,直到根目录下的node_modules目录
- loaded:一个布尔值,表示当前模块是否被完全加载
// export.js
module.exports = {
name: 'taki',
getName: function (name) {
console.log(name)
}
}
console.log(module)
> node export.js
Module {
id: '.',
exports: { name: 'taki', getName: [Function: getName] },
parent: null,
filename:
'E:\\Course\\SeniorFrontEnd\\draft\\nodejs\\1.2.1\\export.js',
loaded: false,
children: [],
paths:
[ 'E:\\Course\\SeniorFrontEnd\\draft\\nodejs\\1.2.1\\node_modules',
'E:\\Course\\SeniorFrontEnd\\draft\\nodejs\\node_modules',
'E:\\Course\\SeniorFrontEnd\\draft\\node_modules',
'E:\\Course\\SeniorFrontEnd\\node_modules',
'E:\\Course\\node_modules',
'E:\\node_modules' ] }
模块分类
核心模块:Node.js提供的模块,例如http模块,fs模块等,称为核心模块。核心模块在node源代码编译过程终就编译成了二进制文件,在node进程启动的时候,部分核心模块就直接加载进内存中,因此这部分模块是不需要手动加载,而且在路径分析中优先判断,加载速度是最快的
文件模块: 用户自己编写的模块,称为文件模块。文件模块是需要按需加载的,速度较慢
module.exports与exports
module.exports和exports都是引用类型变量,指向同一个地址内存,在node中两者一开始都指向一个空对象
exports对象是通过形参的方式传入,直接赋值给形参的引用,但是并不能改变作用域外的值
module.exports是为了解决exports直接赋值的问题而产生的
开始 exports = module.exports
相当于二者都是针对同一个对象的引用,但如果执行了exports = {}
之后,exports指向的地址就会发生改变,所以在使用exports
暴露对象的时候不能直接对其赋值。而module.exports = {}
不会有这个问题。
require方法详解
模块引入规则
通过exports或者module.exports抛出一个模块,通过require方法传入模块标识符,然后node根据一定的规则引入改模块,我们就可以使用模块中定义的方法和属性
加载步骤
路径分析、文件定位、编译执行
路径分析(模块标识符分析)
- 核心模块
- 以’.‘或者’…/'开始的相对路径文件模块
- 以’/'开始的绝对路径模块
- 非路径形式的模块
文件定位
- 扩展名分析
- 目标文件和包分析
模块加载过程
Module._load = function (request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports
}
路径查找过程
Module._findPath = function (request, paths) {
// 列出所有可能的后缀名:.js,.json,.node
var exts = Object.keys(Module._extensions);
// 如果是绝对路径,就不再搜索
if (request.charAt(0) === '/') {
path = [''];
}
// 是否有后缀的目录斜杠
var trailingSlash = (request.slice(-1) === '/');
// 第一步:如果当前路径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({ request: request, paths: paths });
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
// 第三步:是否存在该模块文件
if (!trailingSlash) {
filename = tryFile(basePath);
// 第四步:该模块文件加上后缀名,是否存在
if (!filename && !trailingSlash) {
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目录中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
// 第六步:是否存在目录名 + index + 后缀名
if (!filename) {
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:将找到的文件路径存入返回缓存,然后返回
if(filename){
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:没找到文件,返回false
return false;
}
Extra
var a = require('.a.js')
var b = require('.a.js')
console.log(a === b)
// true
知识技能获取,感谢[网易云课堂 - 微专业 - 前端高级开发工程师]运营团队。