Modules in Node

623 阅读8分钟

1.CommonJS规范

1.1 模块引用

const math = require('math')

require方法接受模块标识,以此引入一个模块的API到当前上下文中。

1.2 模块定义

在模块中,上下文提供require()方法来引入外部模块,提供exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。

1.3 模块标识

模块标识就是传递给require方法的参数,它必须是符合小驼峰命名的字符串,或者以'.'、'..'开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

这套模块导出和引入机制使得用户完全不必考虑变量污染。

CommonJS 的缺点在于这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的 ES5。

CommonJS 还可以细分为 CommonJS1 和 CommonJS2,区别在于 CommonJS1 只能通过 exports.XX = XX 的方式导出,CommonJS2 在 CommonJS1 的基础上加入了 module.exports = XX 的导出方式。 CommonJS 通常指 CommonJS2。

2. Node.js中的Module

2.1 模块作用域(The module scope)

2.1.1 __dirname(string)

  • <string>

当前模块的所在路径地址,结果和path.dirname(__filename)相同。

eg: /Users/mjr/example.js

console.log(__dirname);
// Prints: /Users/mjr
console.log(path.dirname(__filename));
// Prints: /Users/mjr

2.1.2 __filename

  • <string>

当前模块的文件地址,次路径为绝对路径。

eg: /Users/mjr/example.js

console.log(__filename);
// Prints: /Users/mjr/example.js
console.log(__dirname);
// Prints: /Users/mjr

2.1.3 exports

module.exports的引用。

2.1.4 module

当前模块的引用。

module.exports用来定义模块exports的内容,其他模块可以通过require()引入。

2.1.5 require(id)

  • id: 模块名称或者路径
  • 返回模块内容

用来加载模块(包括node_modules路径下的模块)、JSON及本地文件。

2.1.5.1 require.cache

  • Object

当引用一个模块的时候,这个模块会被缓存在require.cache中,从该对象中删除键值对后,下次require会从原文件加载。(不适应扩展插件)。

{ '/Users/guomiao/personal/exercise/node/require.main.js': 
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/guomiao/personal/exercise/node/require.main.js',
     loaded: false,
     children: [],
     paths: 
      [ '/Users/guomiao/personal/exercise/node/node_modules',
        '/Users/guomiao/personal/exercise/node_modules',
        '/Users/guomiao/personal/node_modules',
        '/Users/guomiao/node_modules',
        '/Users/node_modules',
        '/node_modules' ] } }

直接node运行脚本,也会写入require.cache中

2.1.5.2 require.main

  • module

当直接通过Node.js进程启动时,入口脚本的模块内容。

eg:

entry.js

console.log(require.main);

node entry.js

Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/absolute/path/to/entry.js',
  loaded: false,
  children: [],
  paths:
   [ '/absolute/path/to/node_modules',
     '/absolute/path/node_modules',
     '/absolute/node_modules',
     '/node_modules' ] }

还可以通过require.main === module结果判断脚本是否是通过node命令直接运行的。

2.1.5.3 require.resolve(request[,options])

  • request 需要解析的模块路径。

  • options

    paths: <string[]> 解析模块的起点路径。此参数存在时,将使用这些路径而非默认解析路径。注意此数组中的每一个路径都被用作模块解析算法的起点,意味着 node_modules 层级将从这里开始查询。按照数组顺序,找到第一个为止。

  • Returns:

  • 使用内部require()查找模块位置,但是不会加载该模块,仅仅是返回文件的绝对路径。

    相当于fs.readFileSync(path.join(__dirname, './xx/a.txt'));

    require.resolve还会在拼接好路径之后检查该路径是否存在, 如果 resolve 的目标路径不存在, 就会抛出 Cannot find module 'xx' 的异常。省略了一道检查文件是否存在的工序 (fs.exists)。

    2.1.5.3.1 require.resolve.paths(request)
    • request <string> 要查询解析路径的模块路径。
    • Returns: <string>|

    如果是查询核心模块,例如http或fs 返回null,其他情况则返回包含解析 request 过程中被查询的路径

    const b = require.resolve.paths('fs');
    console.log(b); // null
    
    const b = require.resolve.paths('../');
    console.log(b); // [ '/Users/guomiao/personal/exercise/node' ]
    // `为啥呢???`
    const b = require.resolve.paths('a');
    console.log(b); 
    /* [ '/Users/guomiao/personal/exercise/node/node_modules',
      '/Users/guomiao/personal/exercise/node_modules',
      '/Users/guomiao/personal/node_modules',
      '/Users/guomiao/node_modules',
      '/Users/node_modules',
      '/node_modules',
      '/Users/guomiao/.node_modules',
      '/Users/guomiao/.node_libraries',
      '/usr/local/lib/node' ] */
    

    2.2 module对象

    2.2.1 module.children

    • <module[]>

    通过该模块引入的其他模块(只会展示一次)。

    const co = require('./co.js');
    const a = require('./a.js');
    const a1 = require('./a.js');
    console.log(module.children);
    // 
    [ Module {
        id: '/Users/guomiao/personal/exercise/node/co.js',
        exports: {},
        parent:
         Module {
           id: '.',
           exports: {},
           parent: null,
           filename: '/Users/guomiao/personal/exercise/node/require.main.js',
           loaded: false,
           children: [Circular],
           paths: [Array] },
        filename: '/Users/guomiao/personal/exercise/node/co.js',
        loaded: true,
        children: [],
        paths:
         [ '/Users/guomiao/personal/exercise/node/node_modules',
           '/Users/guomiao/personal/exercise/node_modules',
           '/Users/guomiao/personal/node_modules',
           '/Users/guomiao/node_modules',
           '/Users/node_modules',
           '/node_modules' ] },
      Module {
        id: '/Users/guomiao/personal/exercise/node/a.js',
        exports: {},
        parent:
         Module {
           id: '.',
           exports: {},
           parent: null,
           filename: '/Users/guomiao/personal/exercise/node/require.main.js',
           loaded: false,
           children: [Circular],
           paths: [Array] },
        filename: '/Users/guomiao/personal/exercise/node/a.js',
        loaded: true,
        children: [],
        paths:
         [ '/Users/guomiao/personal/exercise/node/node_modules',
           '/Users/guomiao/personal/exercise/node_modules',
           '/Users/guomiao/personal/node_modules',
           '/Users/guomiao/node_modules',
           '/Users/node_modules',
           '/node_modules' ] } ]
    

    2.2.2 module.exports

    • Object

    Module系统创建的module.exports对象。输出模块用于require加载。

    a.js
    const EventEmitter = require('events');
    
    module.exports = new EventEmitter();
    
    // 处理一些工作,并在一段时间后从模块自身触发 'ready' 事件。
    setTimeout(() => {
      module.exports.emit('ready');
    }, 1000);
    
    b.js
    const a = require('./a');
    a.on('ready', () => {
      console.log('模块 a 已准备好');
    });
    
    node b.js
    // 模块 a 已准备好
    

    module.exports 的赋值必须立即完成。 不能在任何回调中完成。 以下是不起作用的:

    x.js
    setTimeout(() => {
      module.exports = { a: 'hello' };
    }, 0);
    
    y.js
    const x = require('./x');
    console.log(x.a);
    

    2.2.2.1 exports shortcut

    exports变量是在模块的文件级作用域内可用,在模块执行前将module.exports赋值给exoports

    module.exports.f = ...可以简写为exports.f = ...如果给exports赋了新值,它不再指向module.exports

    当 module.exports 属性被新对象完全替换时,通常也会重新赋值 exports:

    module.exports = exports = function Constructor() {
      // ... 
    };
    

    require能识别的是module.exports,如果exports指向module.exports,没差别,如果exports指向别的地方,exports导出任何东西,require都识别不到。

    e.js
    const EventEmitter = require('events');
    //module.exports.a = new EventEmitter();
    exports = {};
    exports.a = new EventEmitter();
    //exports.a = new EventEmitter();
    console.log(exports == module.exports);
    setTimeout(() => {
        //module.exports.emit('ready');
        exports.a.emit('ready');
    }, 1000)
    console.log(module);
    
    
    
    b.js
    const e = require('./e');
    e.a.on('ready', () => {
      console.log('module "e" is ready');
    });
    
    
    node b.js
    false
    Module {
      id: '/Users/guomiao/personal/exercise/node/e.js',
      exports: {},
      parent:
       Module {
         id: '.',
         exports: {},
         parent: null,
         filename: '/Users/guomiao/personal/exercise/node/b.js',
         loaded: false,
         children: [ [Circular] ],
         paths:
          [ '/Users/guomiao/personal/exercise/node/node_modules',
            '/Users/guomiao/personal/exercise/node_modules',
            '/Users/guomiao/personal/node_modules',
            '/Users/guomiao/node_modules',
            '/Users/node_modules',
            '/node_modules' ] },
      filename: '/Users/guomiao/personal/exercise/node/e.js',
      loaded: false,
      children: [],
      paths:
       [ '/Users/guomiao/personal/exercise/node/node_modules',
         '/Users/guomiao/personal/exercise/node_modules',
         '/Users/guomiao/personal/node_modules',
         '/Users/guomiao/node_modules',
         '/Users/node_modules',
         '/node_modules' ] }
    /Users/guomiao/personal/exercise/node/b.js:2
    e.a.on('ready', () => {
        ^
    
    TypeError: Cannot read property 'on' of undefined
        at Object.<anonymous> (/Users/guomiao/personal/exercise/node/b.js:2:5)
        at Module._compile (internal/modules/cjs/loader.js:701:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
        at Module.load (internal/modules/cjs/loader.js:600:32)
        at tryModuleLoad (internal/modules/cjs/loader.js:539:12)
        at Function.Module._load (internal/modules/cjs/loader.js:531:3)
        at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)
        at startup (internal/bootstrap/node.js:283:19)
        at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)
    

    2.2.3 module.filename

    • <string>

    模块的绝对路径。

    2.2.4 module.id

    • <string>

    模块的唯一标识,一般是模块的绝对路径。

    2.2.5 module.loaded

    • <boolean>
      模块是否加载完成,或是正在加载。

    2.2.6 module.parent

    • <module>
      第一个加载本模块的模块。

    2.2.7 module.paths

    对本模块进行搜索的路径。

    2.2.8 module.require(id)

    • id <string>\
    • Returns: <any>\ 导出的模块内容

    module.require 方法提供了一种类似 require() 从原始模块被调用的加载模块的方式。

    注意,为了做到这个,需要获得一个 module 对象的引用。 因为 require() 会返回 module.exports,且 module只在一个特定的模块代码中有效,所以为了使用它,必须明确地导出。

    e.js
    const EventEmitter = require('events');
    //module.exports.a = new EventEmitter();
    //exports = {};
    exports.a = new EventEmitter();
    exports.module = module;
    //exports.a = new EventEmitter();
    console.log(exports == module.exports);
    setTimeout(() => {
        //module.exports.emit('ready');
        exports.a.emit('ready');
    }, 1000)
    console.log('e: ' + module.id);
    //console.log(module.parent);
    console.log(module.paths);
    
    
    
    b.js
    const e = require('./e');
    e.a.on('ready', () => {
      console.log('module "e" is ready');
    });
    console.log(module.filename);
    console.log(`id: ${module.id}`);
    // console.log(e.module);
    const dd = module.require(e.module.id);
    console.log('dd: ');
    console.log(dd);
    
    node bb.js
    //
    true
    e: /Users/guomiao/personal/exercise/node/e.js
    [ '/Users/guomiao/personal/exercise/node/node_modules',
      '/Users/guomiao/personal/exercise/node_modules',
      '/Users/guomiao/personal/node_modules',
      '/Users/guomiao/node_modules',
      '/Users/node_modules',
      '/node_modules' ]
    /Users/guomiao/personal/exercise/node/b.js
    id: .
    dd: 
    { a:
       EventEmitter {
         _events: [Object: null prototype] { ready: [Function] },
         _eventsCount: 1,
         _maxListeners: undefined },
      module:
       Module {
         id: '/Users/guomiao/personal/exercise/node/e.js',
         exports: [Circular],
         parent:
          Module {
            id: '.',
            exports: {},
            parent: null,
            filename: '/Users/guomiao/personal/exercise/node/b.js',
            loaded: false,
            children: [Array],
            paths: [Array] },
         filename: '/Users/guomiao/personal/exercise/node/e.js',
         loaded: true,
         children: [],
         paths:
          [ '/Users/guomiao/personal/exercise/node/node_modules',
            '/Users/guomiao/personal/exercise/node_modules',
            '/Users/guomiao/personal/node_modules',
            '/Users/guomiao/node_modules',
            '/Users/node_modules',
            '/node_modules' ] } }
    module "e" is ready
    

    2.3 Module对象

    Module实例提供通用方法。在文件模块中通过require('module')获取实例变量。

    2.3.1 module.builtinModules

    v9.3.0

    • <string[]>

    罗列Node.js提供的所有模块名称。可以用来判断模块是否为第三方所维护。 注意module在此处含义与module wrapper所提供的module是不同的。可以通过引入Module模块访问:

    const builtin = require('module').builtinModules;
    console.log(builtin) 
    // 
    [ 'async_hooks',
      'assert',
      'buffer',
      'child_process',
      'console',
      'constants',
      'crypto',
      'cluster',
      'dgram',
      'dns',
      'domain',
      'events',
      'fs',
      'http',
      'http2',
      '_http_agent',
      '_http_client',
      '_http_common',
      '_http_incoming',
      '_http_outgoing',
      '_http_server',
      'https',
      'inspector',
      'module',
      'net',
      'os',
      'path',
      'perf_hooks',
      'process',
      'punycode',
      'querystring',
      'readline',
      'repl',
      'stream',
      '_stream_readable',
      '_stream_writable',
      '_stream_duplex',
      '_stream_transform',
      '_stream_passthrough',
      '_stream_wrap',
      'string_decoder',
      'sys',
      'timers',
      'tls',
      '_tls_common',
      '_tls_wrap',
      'trace_events',
      'tty',
      'url',
      'util',
      'v8',
      'vm',
      'zlib',
      'v8/tools/splaytree',
      'v8/tools/codemap',
      'v8/tools/consarray',
      'v8/tools/csvparser',
      'v8/tools/profile',
      'v8/tools/profile_view',
      'v8/tools/logreader',
      'v8/tools/arguments',
      'v8/tools/tickprocessor',
      'v8/tools/SourceMap',
      'v8/tools/tickprocessor-driver',
      'node-inspect/lib/_inspect',
      'node-inspect/lib/internal/inspect_client',
      'node-inspect/lib/internal/inspect_repl' ]
    

    2.3.2 module.createRequireFromPath(filename)

    v10.12.0

    • filename <string> 用于构造通过相对路径加载模块的函数的文件名。
    • Returns: <require> 加载模块的函数。
    const { createRequireFromPath } = require('module');
    const requireUtil = createRequireFromPath('../src/utils');
    
    // Require `../src/utils/some-tool`
    requireUtil('./some-tool');
    

    2.4 模块封装器(The module wrapper)

    在一个模块执行之前,Node.js会做如下包装:

    (function(exports, require, module, __filename, __dirname) {
    // Module code actually lives in here
    });
    

    包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。 最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function执行。

    • 通过封装,保证模块内的变量非全局,不同文件中就算是通过var声明的变量也不会相互覆盖

    • 提供模块内可以直接引用的变量,比如__filename__dirname

    2.5 访问主模块

    当 Node.js 直接运行一个文件时, require.main 会被设为它的 module。 这意味着可以通过 require.main === module 来判断一个文件是否被直接运行:

    对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require('./foo') 运行则为 false。

    因为 module 提供了一个 filename 属性(通常等同于 __filename),所以可以通过检查 require.main.filename 来获取当前应用程序的入口点。

    2.6 附录:包管理器的技巧

    此外,为了进一步优化模块查找过程,不要将包直接放在 /usr/lib/node 目录中,而是将它们放在 /usr/lib/node_modules/<name>/<version> 目录中。 这样 Node.js 就不会在 /usr/node_modules/node_modules 目录中查找缺失的依赖。

    为了使模块在 Node.js 的 REPL 中可用,可能需要将 /usr/lib/node_modules 目录添加到 $NODE_PATH 环境变量中。 由于在 node_modules 目录中查找模块使用的是相对路径,而调用 require() 的文件是基于实际路径的,因此包本身可以放在任何地方。

    2.7 总结

    require(X) from module at path Y
    1. 如果X是核心模块,
       a. 返回模块
       b. 停止
    2. 如果X以 '/'开头
       a. Y为系统根路径
    3. 如果X以'./''/''../'开头
       a. LOAD_AS_FILE(Y + X)
       b. LOAD_AS_DIRECTORY(Y + X)
    4. LOAD_NODE_MODULES(X, dirname(Y))
    5. 抛出"not found"错误
    
    LOAD_AS_FILE(X)
    1. 如果X是文件,加载X为JavaScript文本.  停止
    2. 如果X.js是文件,加载 X.js 为 JavaScript文本.  停止
    3. 如果X.json是文件, 解析X.json为JavaScript对象.  停止
    4. 如果X.node是文件, 加载 X.node作为二进制插件.  停止
    
    LOAD_INDEX(X)
    1. 如果 X/index.js是文件, 加载X/index.js as JavaScript文本.  停止
    2. 如果 X/index.json 是文件, 解析 X/index.json为JavaScript 对象. 停止
    3. 如果 X/index.node 是文件, 加载X/index.node为二进制插件.  停止
    
    LOAD_AS_DIRECTORY(X)
    1. 如果 X/package.json是文件,
       a. 解析X/package.json, 查找"main"入口.
       b. let M = X + (json main field)
       c. LOAD_AS_FILE(M)
       d. LOAD_INDEX(M)
    2. LOAD_INDEX(X)
    
    LOAD_NODE_MODULES(X, START)
    1. let DIRS = NODE_MODULES_PATHS(START)
    2. for each DIR in DIRS:
       a. LOAD_AS_FILE(DIR/X)
       b. LOAD_AS_DIRECTORY(DIR/X)
    
    NODE_MODULES_PATHS(START)
    1. let PARTS = path split(START)
    2. let I = count of PARTS - 1
    3. let DIRS = [GLOBAL_FOLDERS]
    4. while I >= 0,
       a. if PARTS[I] = "node_modules" CONTINUE
       b. DIR = path join(PARTS[0 .. I] + "node_modules")
       c. DIRS = DIRS + DIR
       d. let I = I - 1
    5. 返回DIRS
    

    2.8 缓存

    模块在第一次加载后会被缓存。其他模块使用的时候直接从require.cache中获取该模块。多次调用 require(foo) 不会导致模块的代码被执行多次。 这是一个重要的特性。借助它,可以返回“部分完成”的对象,从而允许加载依赖的依赖,即使它们会导致循环依赖。

    如果想要多次执行一个模块,可以导出一个函数,然后调用该函数。

    2.8.1 模块缓存的注意事项

    模块是基于其解析的文件名进行缓存的。 由于调用模块的位置的不同,模块可能被解析成不同的文件名(比如从 node_modules 目录加载),这样就不能保证 require('foo') 总能返回完全相同的对象。

    此外,在不区分大小写的文件系统或操作系统中,被解析成不同的文件名可以指向同一文件,但缓存仍然会将它们视为不同的模块,并多次重新加载。 例如, require('./foo') 和 require('./FOO') 返回两个不同的对象,而不会管 ./foo 和 ./FOO 是否是相同的文件。

    2.9 核心模块

    Node.js有些模块会被编译成二进制。 核心模块定义在 Node.js 源代码的 lib/ 目录下。 require() 总是会优先加载核心模块。 例如, require('http') 始终返回内置的 HTTP 模块,即使有同名文件。

    2.10 循环

    当循环调用 require() 时,一个模块可能在未完成执行时被返回。

    a.js
    console.log('a starting');
    exports.done = false;
    const b = require('./b.js');
    console.log('in a, b.done = %j', b.done);
    exports.done = true;
    console.log('a done');
    
    b.j
    console.log('b starting');
    exports.done = false;
    const a = require('./a.js');
    console.log(a);
    console.log(require.cache[require.resolve('./a.js')].exports);
    console.log('result: ' + (a == require.cache[require.resolve('./a.js')].exports));
    console.log('in b, a.done = %j', a.done);
    exports.done = true;
    console.log('b done');
    
    main.js
    console.log('main starting');
    const a = require('./a.js');
    const b = require('./b.js');
    console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
    
    $ node main.js
    main starting
    a starting
    b starting
    { done: false }
    { done: false }
    result: true
    in b, a.done = false
    b done
    in a, b.done = true
    a done
    in main, a.done = true, b.done = true
    

    当 main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.js 的 exports 对象的 未完成的副本(从require.cache中获取的) 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

    2.11 文件模块

    如果文件本身没有找到,Node.js会加上.js/.json/.node

    .json 文件会被解析为 JSON 文本文件。 .node 文件会被解析为通过 dlopen 加载的编译后的插件模块。

    • '/'开头的地址为绝对路径
    • './'为相对路径
    • 不是以'/'、'./'、'../'而是直接文件名开头的要么是核心模块比如'fs'/'http',或者从node_modules文件夹下加载
    • 其他情况抛出错误'MODULE_NOT_FOUND'

    2.12 目录作为模块

    可以把程序和库放到一个单独的目录,然后提供一个单一的入口来指向它 三种方式将目录作为参数传递给require():

    • 第一种、在目录的根路径下创建package.json,指定main模块:
    {
        "name": "some-library",
        "main": "./lib/some-library.js"
    }
    

    如果目录是./some-library的话,require('./some-library')会加载./some-library/lib/some-library.js

    注意:如果 package.json 中 "main" 入口指定的文件不存在,则无法解析,Node.js 会将模块视为不存在,并抛出默认错误:

    Error: Cannot find module 'some-library'
    

    如果目录里没有package.json文件:

    • 第二种、./some-library/index.js
    • 第三种、./some-library/index.node

    2.13 从node_modules目录加载

    如果加载文件地址为'/home/ry/projects/foo.js'调用require('bar.js'), Node.js的查找顺序如下:

    • /home/ry/projects/node_modules/bar.js
    • /home/ry/node_modules/bar.js
    • /home/node_modules/bar.js
    • /node_modules/bar.js

    这使得程序本地化它们的依赖,避免它们产生冲突。

    通过在模块名后包含一个路径后缀,可以请求特定的文件或分布式的子模块。 例如, require('example-module/path/to/file') 会把 path/to/file 解析成相对于 example-module 的位置。后缀路径同样遵循模块的解析语法。

    2.14 从全局目录加载

    如果NODE_PATH环境变量是以冒号分割的绝对路径列表,当在其他地方搜索不到模块时会查找这些路径。

    在Windows系统中, NODE_PATH是以; 而不是:分割的。

    在当前的模块解析算法运行之前, NODE_PATH 最初是创建来支持从不同路径加载模块的。

    NODE_PATH is still supported, but is less necessary now that the Node.js ecosystem has settled on a convention for locating dependent modules. Sometimes deployments that rely on NODE_PATH show surprising behavior when people are unaware that NODE_PATH must be set. Sometimes a module's dependencies change, causing a different version (or even a different module) to be loaded as the NODE_PATH is searched.

    Node.js还会搜索以下路径:

    • $HOME/.node_modules
    • $HOME/.node_libraries
    • $PREFIX/lib/node

    其中$HOME是用户的home路径,$PREFIX是Node.js里配置的node_prefix

    以上大部分是历史原因。

    强烈建议将依赖模块放在本地node_modules中,这样加载更快更稳。

    参考