• home > webfront > ECMAS > npm-node >

    再谈Node.js的模块加载方式+机制与运行原理

    Author:zhizunbao Date:

    模块的加载require( __ )||import __ 这里应该怎么填?为什么require( express )可以直接加载?他的加载顺序是什么?加载原理是什么?

    此篇主要由以下几篇神文凝练而成

    Node.js的模块载入方式与机制

    模块以及加载机制,主要讨论找不到模块的问题

    结合源码分析 Node.js 模块加载与运行原理

    npm 模块加载机制详解


    相对路径指定模块,一般用于加载自己的模块。

        必须用到的符号:

             ./ 表示当前目录,相对路径所相对的就是当前的目录

             ../ 表示上一级模块,可以无限使用直到跳转到根目录

    nodejs的模块分类

    • 核心模块:包含在 Node.js 源码中,被编译进 Node.js 可执行二进制文件 JavaScript 模块,也叫 native 模块,比如常用的 http, fs 等等

    • C/C++ 模块,也叫 built-in 模块,一般我们不直接调用,而是在 native module 中调用,然后我们再 require

    • native原生模块:http  fs path等,这些模块都在源码包的lib目录下面,nodejs安装好之后是找不到这些模块的,都作为node.exe的一部分了,require这些模块永远没问题的,如果哪天出现问题了,直接重启电脑或者重装node。有什么疑问可以通过下载源码对这些原生模块的功能进行查看。地址:https://nodejs.org/download/。

    • 第三方模块-文件模块:非 Node.js 源码自带的模块都可以统称第三方模块,比如 express,webpack 等等。

      JavaScript 模块,这是最常见的,我们开发的时候一般都写的是 JavaScript 模块

      JSON 模块,这个很简单,就是一个 JSON 文件

      C/C++ 扩展模块,使用 C/C++ 编写,编译之后后缀名为 .node

    • 自定义模块:我们自己写的模块,之所以独立出来是因为其加载和另两种模块有区别。

    怎样定义模块

    nodejs声明一个模块有2中做法

        exports.module_name

        module.exports

    关于这两个的区别也很简单,不过要讲明白很费劲,关键点在于知道有 module 这个全局变量的存在,打印出来并做几次尝试,就完全明白了,这里有一篇非常精彩  的关于这两者异同的文章:《nodejs中exports与module.exports的实践

    第三方模块安装在哪(NPM)

    几条命令

    npm config get/set prefix //查看设置全局安装目录,全局安装的模块就安装该目录下面的node_modules目录下
    npm install [-g]  // -g 全局安装,模块将会安装到全局目录下。不带 -g 则直接安装在当前所在目录下,即为本地安装

     模块的存在形式

      1、文件包含,这个比较直观,直接指定到文件名(去掉 .js 后缀),就可以得到文件里面所有导出的模块。

      2、文件夹包含,通过npm安装的第三方模块都是这种方式,指定到模块所在的文件夹,该文件夹就是模块名,以express为例:


    express文件结构

    模块加载机制:

    首先搜索当前目录下的 package.json 文件,查找里面的mian属性,如果存在,则加载该属性所指定的的文件。如果不存在 package.json 或者该文件里面没有main字段,nodejs将试图加载 index.js 

    都不存在那么就只有说一声Cannot find module了。


    node模块的载入及缓存机制

    1. 载入内置模块(A Core Module)

    2. 载入文件模块(A File Module)

    3. 载入文件目录模块(A Folder Module)

    4. 载入node_modules里的模块

    5. 自动缓存已载入模块

     

    一、载入内置模块

    Node的内置模块被编译为二进制形式,引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时,内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http模块

        var http = require('http')

    返回的http即是实现了HTTP功能Node的内置模块。

    二、载入文件模块

    • 绝对路径的:var myMod = require('/home/base/my_mod')

    • 相对路径的:var myMod = require('./my_mod')

    注意,这里忽略了扩展名“.js”,以下是对等的

        var myMod = require('./my_mod')

        var myMod = require('./my_mod.js')


    三、载入文件目录模块

    可以直接require一个目录,假设有一个目录名为folder,如

        var myMod = require('./folder')

    此时,Node将搜索整个folder目录,Node会假设folder为一个包并试图找到包定义文件package.json。如果folder目录里没有包含package.json文件,Node会假设默认主文件为index.js,即会加载index.js。如果index.js也不存在,那么加载将失败。

    +folder

        index.js

        modA.js

        package.json

    +init.js

    package.json定义如下

    {
        "name": "pack",
        "main": "modA.js"
    }

    此时 require('./folder') 将返回模块modA.js。如果package.json不存在,那么将返回模块index.js。如果index.js也不存在,那么将发生载入异常。

    四、载入node_modules里的模块

    如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。

    不必担心,npm命令可让我们很方便的去安装,卸载,更新node_modules目录。

    五、自动缓存已载入模块

    对于已加载的模块Node会缓存下来,而不必每次都重新搜索。下面是一个示例

    modA.js

    console.log('模块modA开始加载...')
    exports = function() {
        console.log('Hi')
    }
    console.log('模块modA加载完毕')

    init.js

    var mod1 = require('./modA')
    var mod2 = require('./modA')
    console.log(mod1 === mod2)

    虽然require了两次,但modA.js仍然只执行了一次。mod1和mod2是相同的,即两个引用都指向了同一个模块对象。

    模块在每一次加载之后都会被缓存起来。这也就意味着在每一次使用require(‘foo’)时,返回的都是同一个对象,即使文件随后被修改过。


    多次执行require('./modA')并不会导致模块代码被执行多次,这是一个很重要的功能。

    而如果你确实是想让一个模块的代码被执行多次,那么就导出一个函数,然后多次调用那个函数。

    模块缓存注意事项

    模块缓存基于它们的resolved filename。由于被调用的模块的位置[从node_modules文件夹中加载]不同,所以模块可能会被解析为不同的文件名。

    如果解析的结果是不同的文件,但却总是返回相同的对象,这并不是我们所希望的结果。

    此外,在大小写不敏感的文件系统或操作系统上,不同的文件名可能会指向相同的文件,但缓存系统依旧会将它们视为不同的模块,并多次加载文件。例如,require(‘./foo’) 和 require(‘./FOO’)不管./foo或./FOO是否为同一个文件,都将会返回两个不同的对象。

    require('__')||import '__' 这里应该怎么填

    • 相对路径指定模块,一般用于加载自己的模块。必须用到的符号:

          ./ 表示当前目录,相对路径所相对的就是当前的目录

          ../ 表示上一级模块,可以无限使用直到跳转到根目录

    • 绝对路径指定模块地址,除了原生模块之外,任何文件模块都可以加载到,除非路径出错了。

      比如我们可以这样子加载express模块 '/www/node_module/express/'

    • 直接使用 require('xxx') 那么所加载的模块要么是原生模块,要么该模块在某个node_modules目录下面

    混合模式

    为了确定通过require()函数来加载时,调用的明确的文件名,我们可以使用

    require.resolve()函数。

    能将上述的方式搭配利用,需要的就是require.resolve方法体现出来的高水平算法。

    文件的路径为Y,require(x):

    1. 如果X是核心模块
      a. 返回核心模块
      b. 结束代码

    2. 如果 X 以 ‘./‘ 或 ‘/‘ 或 ‘../‘开始
      a. 以文件的形式加载(Y + X)
      b. 以目录的形式加载(Y + X)

    3. 以NODE_MODULES的形式来加载(X, dirname(Y))

    4. 抛出异常 “not found”

    以文件的形式加载的具体说明

    1.如果存在文件x,那么就把x以javascript文本的形式来加载
    1.如果存在文件x.js,那么就把x.js以javascript文本的形式来加载
    3.如果存在文件x.json,  那么就解析 x.json 为一个 JavaScript 对象
    4.如果存在文件x.node, 那么就把 x.node 作为一个二进制文件来加载

    以目录的形式加载的具体说明

    1. 如果存在文件 X/package.json,
      a. 解析 X/package.json, 然后寻在main字段.
      b. let M = X + (json 的main字段)
      c. 以文件的形式加载(M)

    2. 如果存在文件X/index.js, 把 X/index.js 以javascript文本的形式来加载

    3. 如果存在文件X/index.json, 解析 X/index.json 为一个 JavaScript 对象

    4. 如果存在文件X/index.node, 把 X/index.node 作为一个二进制文件来加载

    以NODE_MODULES的形式来加载(X, START)的具体说明

    1. let DIRS=NODE_MODULES_PATHS(START)

    2. for each DIR in DIRS:
      a. 以文件的形式加载(DIR/X)
      b. 以目录的形式加载(DIR/X)

    NODE_MODULES_PATHS(START)函数的具体说明

    1. let PARTS = path split(START)

    2. let I = count of PARTS - 1

    3. let DIRS = []

    4. while I >= 0,
      a. if PARTS[I] = “node_modules” CONTINUE
      c. DIR = path join(PARTS[0 .. I] + “node_modules”)
      b.    DIRS = DIRS + DIR
      c. let I = I - 1

    5. return DIRS

    nodejs模块运行分析

    假设有一个 index.js 文件,里面只有一行很简单的 console.log('hello world') 代码。当输入 node index.js 的时候,Node.js 是如何编译、运行这个文件的呢?

    从《结合源码分析 Node.js 模块加载与运行原理》,

    可以看到,主要是调用 Module._load 来加载执行 process.argv[1]。

    下文我们在分析模块的 require 的时候,也会来到 lib/module.js 中,也会分析到 Module._load。因此我们可以看出,Node.js 启动一个文件的过程,其实到最后,也是 require 一个文件的过程,可以理解为是立即 require 一个文件

    分析 require 的原理

    var http = require('http');

    那么当执行这一句代码的时候,会发生什么呢?

    • 先生成 cacheKey,判断相应 cache 是否存在,若存在直接返回

    • 如果 path 的最后一个字符不是 /

      • 判断路径如果存在,直接返回

      • 尝试在路径后面加上 .js, .json, .node 三种后缀名,判断是否存在,存在则返回

      • 尝试在路径后面依次加上 index.js, index.json, index.node,判断是否存在,存在则返回

      • 如果路径是一个文件并且存在,那么直接返回文件的路径

      • 如果路径是一个目录,调用 tryPackage 函数去解析目录下的 package.json,然后取出其中的 main 字段所写入的文件路径

      • 如果还不成功,直接对当前路径加上 .js, .json, .node 后缀名进行尝试

    • 如果 path 的最后一个字符是 /

      • 调用 tryPackage ,解析流程和上面的情况类似

      • 如果不成功,尝试在路径后面依次加上 index.js, index.json, index.node,判断是否存在,存在则返回

    process1

    这里目前还没有如何跟直白简练的文字来概括此文内容,所以,及不copy了


    转载本站文章《再谈Node.js的模块加载方式+机制与运行原理》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/nodejs/121.html