首页 > webfront > ECMAS > nodejs > > 正文

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

点击:

模块的加载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了