在 node.js 中, 模块使用 CommonJS 规范, 一个文件是一个模块
node.js 中的模块可分为三类
内部模块 - node.js 提供的模块如 fs,http,path 等
自定模块 - 我们自己写的模块
第三方模块 - 通过 npm 安装的模块
node.js 提供了大量的模块供我们使用, 比如 想解析一个文件的路径, 可以使用 path 模块下的相应方法实现:
- const path = require('path');
- // 返回目标文件的绝对路径
- console.log(path.resolve('./1.txt'));
运行结果:
/Users/cuiyue/workspace/test/1.txt
使用 require 引入相应的模块, 即可使用.
__dirname 和__filename
node.js 的每个模块都有这两个参数, 它们都是一个绝对路径的地址, 区别是__filename 存放了从根目录到当前文件名的路径,__dirname 只存放从根目录到模块的所在目录:
- console.log(__dirname);
- console.log(__filename);
运行结果:
- /Users/cuiyue/workspace/test
- /Users/cuiyue/workspace/test/module.js
vm 模块
vm 模块是 node.js 提供在 V8 虚拟机中编译和运行的工具, node.js 中的模块内部实现就是通过此模块完成.
说说 vm 的基本用法.
在 js 环境中有一个 eval 函数, 它可以运行 js 的代码字符串, 比如:
eval('console.log("Hello javascript.")'); // 输出 Hello javascript.
可以看到, eval 函数的参数是一段字符串, 它可以运行字符串形式的 js 代码, 但它可以使用上下文环境中的变量:
- var num=100;
- eval('console.log(num)'); // 输出 100
以上是可以正确访问 num 的值.
vm 模块提供了方法创建一个安全的沙箱, 在指定的上下文环境中运行代码, 不受外界干扰.
- const vm = require('vm');
- var num = 100;
- vm.runInThisContext('console.log(num)');
运行结果:
- console.log(num)
- ^
- ReferenceError: num is not defined
可以看到代码报错了, 说明在 vm 创建了指定的上下文环境中, 拿不到外界的参量.
CommonJS 规范
在以前, 由于 javascript 的历史原因导致它的模块机制很差, 由于这些缺点使得 javascript 不太善于开发大型应用, 于是提出了 CommonJS 规范以弥补 javascript 的不足.
CommonJS 规范主要分为三块内容: 模块导入导出, 模块定义, 模块标识.
模块导入导出
CommonJS 中使用 require() 函数进行模块的引入.
const mymodule = require('mymodule');
使用 exports 导出模块
- module.exports = {
- name: 'Tom'
- };
引用的名称可以不带路径, 若不带路径表示引入的是 node 提供的模块或是 npm 安装的第三方模块 (node_modules)
模块定义
module 对象: 在每一个模块中, module 对象代表该模块自身.
export 属性: module 对象的一个属性, 它向外提供接口.
模块标识
模块标识指的是传递给 require 方法的参数, 必须是符合小驼峰命名的字符串, 或者以 .,.., 开头的相对路径, 或者绝对路径.
node 中模块解析流程
首先接收参数, 把传入的模块名称解析成绝对路径
若没有后缀名称, 依次拼接. js .json .node 尝试加载, 仍到不到模块则报错
取得正确的路径后判断缓存中是否存在此模块, 若有则取出
若缓存中不存在则加载此文件, 在外包裹一层闭包并执行它
以上为大致流程, 下面尝试着写一下模块.
代码的基本结构:
- /**
- * Module 类, 用于处理模块加载
- */
- function Module() {}
- // 模块的缓存
- Module._cacheModule = {};
- // 不同扩展名的加载策略
- Module._extensions = {};
- // 根据 moduleId 解析绝对路径,
- Module._resolveFileName = function(moduleId) {};
- // 入口函数
- function req(moduleId) {}
附上全部代码:
- const path = require('path');
- const fs = require('fs');
- const vm = require('vm');
- /**
- * Module 类, 用于处理模块加载
- */
- function Module(file) {
- this.id = file; // 当前模块的 id, 它使用完整的绝对路径标识, 因此是唯一的
- this.exports = {}; // 导出
- this.loaded = false; // 模块是否已加载完毕
- }
- // 模块的缓存
- Module._cacheModule = {};
- Module._wrapper = ['(function(exports,require,module,__dirname,__filename){', '});'];
- // 不同扩展名的加载策略
- Module._extensions = {
- '.js': function(currentModule) {
- let js = fs.readFileSync(currentModule.id, 'utf8'); // 读取出 js 文件内容
- let fn = Module._wrapper[0] + js + Module._wrapper[1];
- vm.runInThisContext(fn).call(
- currentModule.exports,
- currentModule.exports,
- req,
- currentModule,
- path.dirname(currentModule.id),
- currentModule.id);
- return currentModule.exports;
- },
- '.json': function(currentModule) {
- let json = fs.readFileSync(currentModule.id, 'utf8');
- return JSON.parse(json); // 转换为 JSON 对象返回
- },
- '.node': ''
- };
- // 加载模块 (实例方法)
- Module.prototype.load = function(file) {
- let extname = path.extname(file); // 获取后缀名
- return Module._extensions[extname](this);
- };
- // 根据 moduleId 解析绝对路径,
- Module._resolveFileName = function(moduleId) {
- let p = path.resolve(moduleId);
- if (!path.extname(moduleId)) { // 传入的模块没有后缀
- let arr = Object.keys(Module._extensions);
- // 循环读取不同扩展名的文件
- for (var i = 0; i < arr.length; i++) {
- let file = p + arr[i]; // 拼接上后缀名成为一个完整的路径
- try {
- fs.accessSync(file);
- return file; // 若此文件存在返回它
- } catch (e) {
- console.log(e);
- }
- }
- } else {
- return p;
- }
- };
- function req(moduleId) {
- let file = Module._resolveFileName(moduleId);
- if (Module._cacheModule[file]) { // 若缓存中存在此模块
- return Module._cacheModule[file];
- } else {
- let module = new Module(file);
- module.exports = module.load(file);
- return module.exports;
- }
- }
- console.log(req('./a.js')());
a.js 的文件内容:
- module.exports = function() {
- console.log('This message from a.js');
- console.log(__dirname);
- console.log(__filename);
- }
最终运行结果:
- This message from a.js
- /Users/cuiyue/workspace/test
- /Users/cuiyue/workspace/test/a.js
重要代码说明
_resolveFileName
_resolveFileName 方法的主要作用是把传入的模块解析成绝对路径, 这样才可以进行下一步, 根据完整的路径加载模块.
因此要进行判断, 如果传入的模块不存在, 则要报错; 如果传入的模块已经有扩展名了, 就不要拼接了; 若没有扩展名, 依次以. js .json .node 的顺序拼接成完成的模块进行加载.
_extensions
此对象中封装了加载不同类型模块的处理方法, 其中若是. json 类型则使用 fs 读取文件直接转换成 JSON 对象并返回.
若是. js 文件则读取后, 拼接闭包, 将 exports,require,module,__dirname,__filename 五大参数拼接好, 使用 vm 模块的沙箱机制运行, 得到的结果放入 module.exports 返回.
总结
以上就是 node.js 的模块加载的简单逻辑, 实际上 node.js 的源码远远比上面的代码复杂, 光是处理模块路径, 判断合法等操作就写了 N 行. 而且我这里没有写缓存以及其它的复杂逻辑, 但核心差不多就是这些, 核心的核心就是用 fs.readFileSync 读取 js 文件, 把内容拼接到一个大大的闭包中, 这也解释了为什么我们自己写的所有 node 模块中都会有 require 方法, exports 导出, 以及__dirname 和__filename 参数.
了解了 node.js 的模块加载逻辑, 在以后写 node.js 就更可避免一些误解, 写出精细的代码.
来源: https://juejin.im/post/5b0703d3f265da0dc15d7d7a