首頁  >  文章  >  web前端  >  一文快速了解Nodejs中的模組系統

一文快速了解Nodejs中的模組系統

青灯夜游
青灯夜游轉載
2021-09-14 10:32:102446瀏覽

這篇文章帶大家了解一下Nodejs中的模組系統,希望對大家有幫助!

一文快速了解Nodejs中的模組系統

模組化的背景

早期JavaScript 是為了實現簡單的頁面互動邏輯, 但隨著時代發展, 瀏覽器不單單僅只能呈現簡單互動, 各種各樣的網站開始大放異彩。隨著網站開始變得複雜化,前端程式碼日漸增多,相對比起其他靜態語言,JavaScript 缺少模組化的弊端開始暴露出來,例如命名衝突。因此為了方便前端程式碼的維護與管理,社群開始了模組化規範的定義。在這個過程中,出現了許多的模組化規範,如CommonJS, AMD, CMD, ES modules,本篇主要講解Node中根據CommonJS實現的模組化。

CommonJS 規範

首先,在Node 世界裡,模組系統是遵守CommonJS規範的,CommonJS規範裡定義,簡單講就是:

  • 每一個檔案就是一個模組
  • 透過module物件來代表一個模組的資訊
  • 透過exports用來導出模組對外暴露的資訊
  • 透過require來引用一個模組

Node 模組分類

  • 核心模組: 如fs,http,path 等模組, 這些模組不需要安裝, 在執行時已經載入在記憶體中。 【推薦學習:《nodejs 教學》】
  • 第三方模組: 透過安裝存放在 node_modules 中。
  • 自訂模組: 主要是指 file 模組,透過絕對路徑或相對路徑進行引入。

Module 物件

我們上面說過,一個檔案就是一個模組,且透過一個module 物件來描述目前模組資訊,一個module 物件對應有以下屬性: - id: 當前模組的id - path: 目前模組對應的路徑 - exports: 當前模組對外暴露的變量 - parent: 也是一個module物件,表示當前模組的父模組,即呼叫當前模組的模組 - filename: 目前模組的檔案名稱(絕對路徑), 可用於在模組引入時將載入的模組加入到全域模組快取中, 後續引入直接從快取裡進行取值 - loaded: 表示目前模組是否載入完畢 - children: 是一個陣列,存放著目前模組呼叫的模組 - paths: 是一個陣列,記錄著從目前模組開始查找node_modules目錄,遞歸向上查找到根目錄下的node_modules目錄下

module.exports 與exports

#說完CommonJS規格,我們先講下module.exportsexports的差別。

首先,我們用個新模組進行一個簡單驗證

console.log(module.exports === exports); // true

可以發現,module.exportsepxorts其實就是指向同一個引用變數。

demo1

// a模块
module.exports.text = 'xxx';
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {text: 'xxx', value: 2}

因此也驗證了上面demo1中,為什麼透過module.exports exports新增屬性,在模組引入時兩者都存在, 因為兩者最終都是往同一個引用變數上面進行屬性的添加.根據該demo, 可以得出結論: module.exportsexports指向同一個引用變數

demo2

// a模块
module.exports = {
  text: 'xxx'
}
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {text: 'xxx'}

上面的demo 範例中,對module.exports進行了重新賦值, exports進行了屬性的新增, 但是在引入模組後最終導出的是module.exports定義的值, 可以得出結論: noed 的模組最終導出的是module.exports, 而exports僅僅是對module.exports 的引用, 類似於以下程式碼:

exports = module.exports = {};
(function (exports, module) {
  // a模块里面的代码
  module.exports = {
    text: 'xxx'
  }
  exports.value = 2;

  console.log(module.exports === exports); // false
})(exports, module)

由於在函數執行中, exports 僅是對原module.exports對應變數的一個引用,當對module.exports進行賦值時,exports對應的變數和最新的module .exports並不是同一個變數

require 方法

#require 介紹模組的過程主要分成以下幾個步驟:

  • 解析文件路径成绝对路径
  • 查看当前需要加载的模块是否已经有缓存, 如果有缓存, 则直接使用缓存的即可
  • 查看是否是 node 自带模块, 如 http,fs 等, 是就直接返回
  • 根据文件路径创建一个模块对象
  • 将该模块加入模块缓存中
  • 通过对应的文件解析方式对文件进行解析编译执行(node 默认仅支持解析.js,.json, .node后缀的文件)
  • 返回加载后的模块 exports 对象

一文快速了解Nodejs中的模組系統

Module.prototype.require = function(id) {
  // ...
  try {
    // 主要通过Module的静态方法_load加载模块 
    return Module._load(id, this, /* isMain */ false);
  } finally {}
  // ...
};
// ...
Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;
  // ...
  // 解析文件路径成绝对路径
  const filename = Module._resolveFilename(request, parent, isMain);
  // 查看当前需要加载的模块是否已经有缓存
  const cachedModule = Module._cache[filename];
  // 如果有缓存, 则直接使用缓存的即可
  if (cachedModule !== undefined) {
    // ...
    return cachedModule.exports;
  }

  // 查看是否是node自带模块, 如http,fs等, 是就直接返回
  const mod = loadNativeModule(filename, request);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // 根据文件路径初始化一个模块
  const module = cachedModule || new Module(filename, parent);

  // ...
  // 将该模块加入模块缓存中
  Module._cache[filename] = module;
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  // ...
  // 进行模块的加载
  module.load(filename);

  return module.exports;
};

至此, node 的模块原理流程基本过完了。目前 node v13.2.0 版本起已经正式支持 ESM 特性。

__filename, __dirname

在接触 node 中,你是否会困惑 __filename, __dirname是从哪里来的, 为什么会有这些变量呢? 仔细阅读该章节,你会对这些有系统性的了解。

  • 顺着上面的 require 源码继续走, 当一个模块加载时, 会对模块内容读取
  • 将内容包裹成函数体
  • 将拼接的函数字符串编译成函数
  • 执行编译后的函数, 传入对应的参数
Module.prototype._compile = function(content, filename) {
  // ...
  const compiledWrapper = wrapSafe(filename, content, this);
  // 
  result = compiledWrapper.call(thisValue, exports, require, module,
                                    filename, dirname);
  
  // ...
  return result;
};
function wrapSafe(filename, content, cjsModuleInstance) {
  // ...
  const wrapper = Module.wrap(content);
  // ...
}
let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

ObjectDefineProperty(Module, 'wrap', {
  get() {
    return wrap;
  },

  set(value) {
    patched = true;
    wrap = value;
  }
});

综上, 也就是之所以模块里面有__dirname,__filename, module, exports, require这些变量, 其实也就是 node 在执行过程传入的, 看完是否解决了多年困惑的问题^_^

NodeJS 中使用 ES Modules

  • package.json增加"type": "module"配置
// test.mjs
export default {
	a: 'xxx'
}
// import.js
import a from './test.mjs';

console.log(a); // {a: 'xxx'}

import 与 require 两种机制的区别

较明显的区别是在于执行时机:

  • ES 模块在执行时会将所有import导入的模块会先进行预解析处理, 先于模块内的其他模块执行
// entry.js
console.log('execute entry');
let a = require('./a.js')

console.log(a);

// a.js
console.log('-----a--------');

module.exports = 'this is a';
// 最终输出顺序为:
// execute entry
// -----a--------
// this is a
// entry.js
console.log('execute entry');
import b from './b.mjs';

console.log(b);

// b.mjs
console.log('-----b--------');

export default 'this is b';
// 最终输出顺序为:
// -----b--------
// execute entry
// this is b
  • import 只能在模块的顶层,不能在代码块之中(比如在if代码块中),如果需要动态引入, 需要使用import()动态加载;

ES 模块对比 CommonJS 模块, 还有以下的区别:

  • 没有 requireexportsmodule.exports

    在大多数情况下,可以使用 ES 模块 import 加载 CommonJS 模块。(CommonJS 模块文件后缀为 cjs) 如果需要引入.js后缀的 CommonJS 模块, 可以使用module.createRequire()在 ES 模块中构造require函数

// test.cjs
export default {
a: 'xxx'
}
// import.js
import a from './test.cjs';

console.log(a); // {a: 'xxx'}
// test.cjs
export default {
a: 'xxx'
}
// import.js
import a from './test.cjs';

console.log(a); // {a: 'xxx'}
// test.cjs
export default {
a: 'xxx'
}
// import.mjs
import { createRequire } from 'module';
const require = createRequire(import.meta.url);

// test.js 是 CommonJS 模块。
const siblingModule = require('./test');
console.log(siblingModule); // {a: 'xxx'}
  • 没有 __filename 或 __dirname

    这些 CommonJS 变量在 ES 模块中不可用。

  • 没有 JSON 模块加载

    JSON 导入仍处于实验阶段,仅通过 --experimental-json-modules 标志支持。

  • 没有 require.resolve

  • 没有 NODE_PATH

  • 没有 require.extensions

  • 没有 require.cache

ES 模块和 CommonJS 的相互引用

在 CommonJS 中引入 ES 模块

由于 ES Modules 的加载、解析和执行都是异步的,而 require() 的过程是同步的、所以不能通过 require() 来引用一个 ES6 模块。

ES6 提议的 import() 函数将会返回一个 Promise,它在 ES Modules 加载后标记完成。借助于此,我们可以在 CommonJS 中使用异步的方式导入 ES Modules:

// b.mjs
export default 'esm b'
// entry.js
(async () => {
	let { default: b } = await import('./b.mjs');
	console.log(b); // esm b
})()

在 ES 模块中引入 CommonJS

在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块,因为在 ES6 模块里异步加载并非是必须的:

// a.cjs
module.exports = 'commonjs a';
// entry.js
import a from './a.cjs';

console.log(a); // commonjs a

至此,提供 2 个 demo 给大家测试下上述知识点是否已经掌握,如果没有掌握可以回头再进行阅读。

demo module.exports&exports

// a模块
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {value: 2}

demo module.exports&exports

// a模块
exports = 2;

// b模块代码
let a = require('./a');

console.log(a); // {}

require&_cache 模块缓存机制

// origin.js
let count = 0;

exports.addCount = function () {
	count++
}

exports.getCount = function () {
	return count;
}

// b.js
let { getCount } = require('./origin');
exports.getCount = getCount;

// a.js
let { addCount, getCount: getValue } = require('./origin');
addCount();
console.log(getValue()); // 1
let { getCount } = require('./b');
console.log(getCount()); // 1

require.cache

根据上述例子, 模块在 require 引入时会加入缓存对象require.cache中。 如果需要删除缓存, 可以考虑将该缓存内容清除,则下次require模块将会重新加载模块。

let count = 0;

exports.addCount = function () {
	count++
}

exports.getCount = function () {
	return count;
}

// b.js
let { getCount } = require('./origin');
exports.getCount = getCount;

// a.js
let { addCount, getCount: getValue } = require('./origin');
addCount();
console.log(getValue()); // 1
delete require.cache[require.resolve('./origin')];
let { getCount } = require('./b');
console.log(getCount()); // 0

结语

至此,本文主要介绍了 Node 中基于CommonJS实现的模块化机制,并且通过源码的方式对模块化的整个流程进行了分析,有关于模块的介绍可查看下面参考资料。有疑问的欢迎评论区留言,谢谢。

参考资料

CommonJS 模块

ES 模組

原文網址:https://juejin.cn/post/7007233910681632781

#作者:慢吃啊

更多程式相關知識,請造訪:程式設計影片! !

以上是一文快速了解Nodejs中的模組系統的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除