从前端模块化深入解析node.js的模块加载机制

node模块加载机制.png

框架总览

😁 前言
😁 模块化的理解

  • 🏆 什么是模块
  • 🏆 模块化的进化过程
  • 🏆 模块化的好处
  • 🏆 引入多个<script>后出现出现问题

😁 模块化规范

  • 🏆 CommonJS 模块规范
  • 🏆 AMD 规范
  • 🏆 CMD 规范
  • 🏆 Es6模块化
  • 🏆 总结

😁 Node.js模块分类
😁 nodejs模块使用

  • 🏆 创建 & 导出模块
  • 🏆 引入模块

😁 require的加载机制

  • 🏆 路径分析
  • 🏆 文件定位
  • 🏆 编译执行

😁 模块循环引用问题
😁 站在巨人肩上


1. 前言

随着公司618,双11大促的到来。erp搭建的活动页面逻辑和交互越来越复杂。此时在JS方面就会考虑使用模块化规范去管理。一来便于开发梳理调理。二来也可以多人协作,最后配合bable可以使用很多ES6、ES7的新功能。可以说,公司目前很多复杂的页面非常需要模块化开发。
本文内容主要有理解模块化,为什么要模块化,模块化的优缺点以及模块化规范,介绍下开发中最流行的CommonJS, AMD, ES6、CMD规范。并介绍node中的模块引入机制。本文试图站在小白的角度,用通俗易懂的笔调介绍这些枯燥无味的概念,希望诸君阅读后,对模块化编程有个全新的认识和理解!如有错误请多多包涵。


1. 模块化的理解

1.1.什么是模块?

  • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2.2.模块化的进化过程

  • 全局function模式 : 将不同的功能封装成不同的全局函数

    • 编码: 将不同的功能封装成不同的全局函数
    • 问题: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function m1(){
  //...
}
function m2(){
  //...
}
  • namespace模式 : 简单对象封装

    • 作用: 减少了全局变量,解决命名冲突
    • 问题: 数据不安全(外部可以直接修改模块内部的数据)
let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data

这样的写法会暴露所有模块成员,内部状态可以被外部改写。

  • IIFE模式:匿名函数自调用(闭包)

    • 作用: 数据是私有的, 外部只能通过暴露的方法操作
    • 编码: 将数据和行为封装到一个函数内部, 通过给window添加属性来向外暴露接口
    • 问题: 如果当前这个模块依赖另一个模块怎么办?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能访问模块内部数据
    myModule.data = 'xxxx' //不是修改的模块内部的data
    myModule.foo() //没有改变
</script>
// module.js文件
(function(window) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar } //ES6写法
})(window)

最后得到的结果:

image.png
  • IIFE模式增强 : 引入依赖

这就是现代模块实现的基石

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作数据的函数
  function foo() {
    //用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() {
    //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = { foo, bar }
})(window, jQuery)
 // index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上例子通过jquery方法将页面的背景颜色改成红色,所以必须先引入jQuery库,就把这个库当作参数传入。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。可以参考公司的activity2018.js.

2.3 模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

2.4 引入多个<script>后出现出现问题

  • 请求过多

首先我们要依赖多个模块,那样就会发送多个请求,导致请求过多

  • 依赖模糊

我们不知道他们的具体依赖关系是什么,也就是说很容易因为不了解他们之间的依赖关系导致加载先后顺序出错。

  • 难以维护

以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题。
模块化固然有多个好处,然而一个页面需要引入多个js文件,就会出现以上这些问题。而这些问题可以通过模块化规范来解决,下面介绍开发中最流行的commonjs, AMD, ES6, CMD规范。


3. 模块化规范

3.1 CommonJS规范

image.png
(1)概述

Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。

(2)特点

所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。

(3)基本语法

暴露模块:module.exports = valueexports.xxx = value
引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代码通过module.exports输出变量x和函数addX。

var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的module.exports对象。如果没有发现指定模块,会报错。

Node.js 借鉴了 CommonJS 规范的设计,特别是 CommonJS 的 Modules 规范,实现了一套模块系统,同时 NPM 实现了 CommonJS 的 Packages 规范,模块和包组成了 Node 应用开发的基础。


3.2 AMD 规范

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD 是 RequireJS 在推广过程中对模块定义的规范化的产出.

(1)AMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(){
   return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

引入使用模块:

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

(2)requireJS主要解决两个问题

  • 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器

  • js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长

(3) 未使用AMD规范与使用require.js

通过比较两者的实现方法,来说明使用AMD规范的好处。

  • 未使用AMD规范
// dataService.js文件
(function (window) {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
 // alerter.js文件
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

最后得到如下结果:


image.png
  • 使用require.js

RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载
接下来介绍AMD规范在浏览器实现的步骤:

①下载require.js, 并引入

  • 官网: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后将require.js导入项目: js/libs/require.js

②创建项目结构

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③定义require.js的模块代码

// dataService.js文件 
// 定义没有依赖的模块
define(function() {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return { getMsg } // 暴露模块
})
//alerter.js文件
// 定义有依赖的模块
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //映射: 模块标识名: 路径
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()
// index.html文件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

④页面引入require.js模块:

在index.html引入 <script data-main="js/main" src="js/libs/require.js"></script>

此外在项目中如何引入第三方库?只需在上面代码的基础稍作修改:

// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  $('body').css('background', 'green')
  // 暴露模块
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路径 出发点在根目录下
    paths: {
      //自定义模块
      alerter: './modules/alerter', //此处不能写成alerter.js,会报错
      dataService: './modules/dataService',
      // 第三方库模块
      jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

上例是在alerter.js文件中引入jQuery第三方库,main.js文件也要有相应的路径配置。

小结:require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有依赖的模块都加载成功后,才会运行,解决了依赖性的问题。

3.3 CMD 规范

CMD 即Common Module Definition通用模块定义,CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

(1)CMD规范基本语法

定义暴露模块:

//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})

引入使用模块:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

(2)sea.js简单使用教程

①下载sea.js, 并引入

然后将sea.js导入项目: js/libs/sea.js

②创建项目结构

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③定义sea.js的模块代码

// module1.js文件
define(function (require, exports, module) {
  //内部变量数据
  var data = 'atguigu.com'
  //内部函数
  function show() {
    console.log('module1 show() ' + data)
  }
  //向外暴露
  exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js文件
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
  //引入依赖模块(同步)
  var module2 = require('./module2')
  function show() {
    console.log('module4 show() ' + module2.msg)
  }
  exports.show = show
  //引入依赖模块(异步)
  require.async('./module3', function (m3) {
    console.log('异步引入依赖模块3  ' + m3.API_KEY)
  })
})
// main.js文件
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

④在index.html中引入

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

最后得到结果如下:

module1 show() atguigu.com
module4 show() I Will Back
异步引入依赖模块3 
abc123

Es6模块化

在之前的javascript中是没有模块化概念的。如果要进行模块化操作,需要引入第三方的类库。随着技术的发展,前后端分离,前端的业务变的越来越复杂化。直至ES6带来了模块化,才让javascript第一次支持了module。ES6的模块化分为导出(export)与导入(import)两个模块。

export的用法

在ES6中每一个模块即是一个文件,在文件中定义的变量,函数,对象在外部是无法获取的。如果你希望外部可以读取模块当中的内容,就必须使用export来对其进行暴露(输出)。先来看个例子,来对一个变量进行模块化。我们先来创建一个test.js文件,来对这一个变量进行输出:

export let myName="laowang";

然后可以创建一个index.js文件,以import的形式将这个变量进行引入:

import {myName} from "./test.js";
console.log(myName);//laowang

如果要输出多个变量可以将这些变量包装成对象进行模块化输出:

let myName="laowang";
let myAge=90;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
    myName,
    myAge,
    myfn
}
/******************************接收的代码调整为**********************/
import {myfn,myAge,myName} from "./test.js";
console.log(myfn());//我是laowang!今年90岁了
console.log(myAge);//90
console.log(myName);//laowang

如果你不想暴露模块当中的变量名字,可以通过as来进行操作:

let myName="laowang";
let myAge=90;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
    myName as name,
    myAge as age,
    myfn as fn
}
/******************************接收的代码调整为**********************/
import {fn,age,name} from "./test.js";
console.log(fn());//我是laowang!今年90岁了
console.log(age);//90
console.log(name);//laowang
默认导出(default export)

一个模块只能有一个默认导出,对于默认导出,导入的名称可以和导出的名称不一致。

/******************************导出**********************/
export default function(){
    return "默认导出一个方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意这里默认导出不需要用{}。名称随意起
console.log(myFn());//默认导出一个方法
export和default export的区别

1.exports default 后面跟一个具体的值, export 后面跟变量申明语句。

本质上,export default value就是输出一个叫做default的变量。default是被value赋值的,正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。require引入default的值,并为其起一个名字。接下来,我们可以实践下。

首先看看 export 的执行情况:

export let test1 = 'test1';
import {test1} from "./index";
console.log(test1);
输出// test1 

接下来,我们来看看export default 的执行情况:

export default let test1 = 'test1'
import test1 from "./index";
console.log(test1);
//  报错。
2.使用export, import 需要加大括号(* 除外), export default 则不需要

3.export default 在一个模块里只能有一个,但是export可以有多个

首先看看 export 的执行情况:

let test1 = 'test1'
let test2 = 'test2'
export {
test1,
test2
}
import {test1, test2}  from "./index";
console.log(test1, test2);

上述代码的执行结果如下:

// test1 
// test2

接下来看看export default 的执行情况

let test1 = 'test1'
let test2 = 'test2'
export default test1
export default test2

import test1, test2 from "./index";
console.log(test1, test2);

复制代码上述代码的执行结果如下:

// 报错

4.通过export导出的属性或者方法可以修改,通过export default 导出的基本类型不可修改

首先看看 export 的执行情况:

let test1 = 'test1'
let test2 = {
    a: '1'
}
export  {
    test1,
    test2
}
test1 = 'test1 modify'
test2.a = '1 modify'

import {test1, test2} from "./index";
console.log(test1, test2);

上述代码的执行结果如下:

test1 modify {a: "1 modify"}

接下来看看export default 的执行情况

let test1 = 'test1'
export default test1
test1 = 'test1 modify'
import test1  from "./index";
console.log(test1);

上述代码的执行结果如下:

// test1

上述代码证明export导出的属性或者方法可以修改,无论是基本类型,还是引用类型。

let test1 = {
    a: 'test1'
}
export default test1
test1.a = 'test1 modify'
import test1  from "./index";
console.log(test1);

上述代码的执行结果如下:

{a: "test1 modify"}
  • export default value ,相当于default = value 。 import的时候可以将值赋值给任意一个变量,a/b/c等都行。所以当value是基本数据类型的时候。value修改并不会引起require引入的值的修改。

  • 因为export default也是一种export,所以all 实质上是{a,default}

  • 因为export导出的是一个变量。所以可以修改。

ES6 模块与 CommonJS 模块的差异

它们有两个重大差异:

  • ① CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • ② CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

到此,前端模块化讲完。接下来讲node模块。


image.png

4. Node.js模块分类

前文说, 在 Node.js 中, 每个文件就被视为一个模块. 这个文件可能是 JavaScript 编写的文件、JSON 或者用 C/C++ 编译的二进制文件.

模块可以分成三类:

image.png
  • 核心模块 』: Node.js 自带的原生模块. 比如, http, fs, url. 其中分为 C/C++ 编写的和 JavaScript 编写的两部分. C/C++ 模块存放在 Node.js 源代码目录的 src/ 目录下. JavaScript 模块存放在 lib/ 目录下. 核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,所以不用文件定位和编译执行。
  • 文件模块 』: 开发人员在本地写的模块. 加载时通过相对路径, 绝对路径来定位模块所在位置.在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程
  • 第三方模块 』: 别人编写的模块, 通过包管理工具, 比如 npm, yarn, 可以将其从网络上引入到本地项目, 供己使用.

5.nodejs模块使用

在了解了什么是模块之后, 让我们来看看如何在 Node.js 中实际应用模块机制. 在使用上, 可以很简单的分为三个步骤: 创建, 导出, 引入.。先创建一个模块, 然后导出功能或数据, 模块之间可以互相引入导出的内容.

Node.js 提供了exportsrequire 两个对象,其中exports用于导出模块,require 用于从外部引入另一个模块, 即获取模块的 exports 对象.

5.1创建 & 导出模块

先让我们来看看如何创建并把模块的内容导出. 在Node.js中, 一个文件就是一个模块. 创建模块的方法就是创建一个文件.

通过 exports对象来指定一个模块的导出内容.
示例:

// 文件名: nameModule.js
var name = 'Garrik';

exports.setName = function(newName) {
name = newName;
}

exports.getName = function() {
return name;
}

在以上示例中, nameModule.js 文件通过 exports 对象将 setName 和 getName 作为模块的访问接口. 其他的模块可以引入导出的 exports 对象, 直接访问 exports 对象的成员函数.

5.2引入模块

在 Node.js 中, 通过 require 函数来引入外界模块导出的内容. require 函数接受一个字符串作为路径参数, 函数根据这个字符串参数来进行模块查找. 找到后会返回目标模块导出的 exports 对象.

示例:
// 文件名: showNameModule.js
var nameModule = require('./nameModule.js');
console.log(nameModule.getName()); 
// 显示: Garrik
nameModule.setName('Xiang');
console.log(nameModule.getName());
// 显示: Xiang

上面示例中, 通过require引入了当前目录下nameModule.js导出的 exports对象, 并让一个本地变量指向引入模块的 exports 对象. 之后在 showNameModule.js 文件中就可以使用getNamesetName 这两个方法了.

6. require的加载机制

image.png

上述模块规范看起来十分简单,只有moduleexportsrequire,但 Node 是如何实现的呢?

需要经历路径分析(模块的完整路径)、文件定位(文件扩展名或目录)、编译执行三个步骤。

6.1 路径分析

回顾require()接收 模块标识 作为参数来引入模块,Node 就是基于这个标识符进行路径分析。不同的标识符采用的分析方式是不同的,主要分为以下几类:

  • Node 提供的核心模块,如 http、fs、path
    核心模块在 Node 源码编译时存为二进制执行文件,在 Node 启动时直接加载到内存中,因为不需要路径分析和文件定位,所以加载速度很快,而且也不用后续的文件定位和编译执行。

如果想加载与核心模块同名的自定义模块,如自定义 http 模块,那必须选用不同标志符或改用路径方式。

  • .、..形式的文件模块

...或/开始的标识符都会当成文件模块处理,Node 会将require('./untils.js')中的路径作为参数获取模块可能出现的位置,并以数组的形式返回文件所在的父级,比如[E:/moudles]

  • 自定义文件模块,即非路径形式的文件模块

自定义文件模块是特殊的文件模块,在路径查找时 Node 会从所在的父级开始逐级查找该模块路径中的node_modules路径,直到根目录,生成一个可能路径的数组。并将这个数组返回。
模块路径查找策略示例如下:

// Module._resolveLookupPaths代码相对复杂,这里简单起见只展示一些其执行结果
tt.js文件目录d/wedoctor
node tt.js
console.log(module.constructor._resolveLookupPaths('fs', module, true))
console.log(module.constructor._resolveLookupPaths('/hello', module, true))
console.log(module.constructor._resolveLookupPaths('../../hello', module, true))
console.log(module.constructor._resolveLookupPaths('hello', module, true))

// 1.加载核心模块的时候,返回 null
// 2.加载绝对路径的时候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韩\\.node_modules',
  'C:\\Users\\小韩\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]
// 由于是绝对路径,所以在_findPath方法中会被清空
// 3.加载相对路径的时候,返回
[ 'D:\\wedoctor' ]
// 4.加载自定义模块的时候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韩\\.node_modules',
  'C:\\Users\\小韩\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]

//上面的数组,就是模块所有可能的路径。基本上是,从当前路径开始一级级向上寻找 node_modules 子目录。

路径分析只是获取文件可能出现的位置,将可能出现位置组成的数组返回,其中文件模块返回require引用文件的父级组成的数组:[modu.parent],第三方模块返回沿当前路径向上逐级查找node_modules目录直到根目录组成的数组paths

6.2 文件定位

模块路径分析完成后紧接着的步骤是文件定位。文件定位分为以下几个步骤:

  • 1.从 path数组中取出第一个目录作为查找基准。比如root/src 。Node 会将require()中的路径./untils.js和当前查找基准合并。成为真实路径root/src/untils.js。从目录中查找该文件,如果存在,就结束查找。为索引,然后编译执行。如果不存在,就向上进行下一条查找作。如果省略后缀名,将跳过第一步执行第二步。

  • 2.通过添加.js .json .node后缀查找,如果存在文件就结束查找。如果不存在,则进行下一条。

    1. 将require的参数作为一个包进行查找,读取目录下的package.json文件,取得main(入口文件)指定的文件。如果没有则进行下一步
    1. 如果没有pakage.json或者main属性指定的文件名错误,那 Node 会将 index 当做默认文件名,依次查找 index.js、index.json、index.node
    1. 如果仍没找到,则取出module path数组中的下一个目录作为基准查找,循环1-4步骤,直到module path中的最后一个值。
    1. 如果找到,返回合并的绝对路径。作为下一步要用的索引值。如果仍没找到就会抛出异常。
  • 整个流程如下图:

    image.png

整个查找过程类似原型链的查找和作用域的查找,但node对路径查找实现了缓存机制,所以不会很耗性能。

6.3 编译执行

Node 中每个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,然后根据路径载入并编译。不同的文件扩展名载入方法为:

  • .js 文件: 通过 fs 模块同步读取后编译执行
  • .json 文件: 通过 fs 模块同步读取后,用JSON.parse()解析并返回结果
  • .node 文件: 这是用 C/C++ 写的扩展文件,通过process.dlopen()方法加载最后编译生成的二进制文件执行即可。
  • 其他扩展名: 都被当做 js 文件载入

载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()解析得到对象后直接赋值给模块对象的exports,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()载入执行就可以,下面重点介绍 .js 文件的编译:

1. 包装(Wrapping)

在 Node API 文档中每个模块有 module、exports 、 require __filename、__dirname这些变量,但是在模块中或者全局作用域中没有定义这些变量,那它们是怎么产生的呢?

事实上在编译过程中,通过fs.readFileSync读取js文件,把js内容拼接到一个大大的闭包中。每个文件都是一个模块,有自己的作用域。将exports,require,module,__dirname,__filename五大参数传入。这样模块就可以使用它们了。例如一个 JS 文件会被封装成如下:

(function (exports, require, module, __filename, __dirname) {
    var math = require('math')
    export.add = function(){ //... }
})
  • 2.执行(Evaluation):

传入参数,并执行包装得到的函数。

  • 3.缓存(Caching):

函数执行完毕后,最后将运行函数得到的结果放入module.exports并返回。编译并执行成功的模块会将文件绝对路径作为索引,将module的做为值组成一个对象缓存起来。例如下面代码,会将sayHisayHaHa函数放入module.exports中并作为require()的返回值返回。最后缓存一个key,value的对象。

// untils.js
console.log('untils');
exports.sayHi = function() {
 console.log('Hi');
}
module.exports.sayHaHa = function() {
  console.log('haha');
}
// 缓存的值
{ 'E:\\node test\\index.js':
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: 'E:\\node test\\index.js',
     loaded: false,
     children: [ [Module] ],
     paths: [ 'E:\\node test\\node_modules', 'E:\\node_modules' ] },
  'E:\\node test\\extra\\src\\untils.js':
   Module {
     id: 'E:\\node test\\extra\\src\\untils.js',
     exports: { sayHi: [Function], sayHaHa: [Function] },
     parent:
      Module {
        id: '.',
        exports: {},
        parent: null,
        filename: 'E:\\node test\\index.js',
        loaded: false,
        children: [Array],
        paths: [Array] },
     filename: 'E:\\node test\\extra\\src\\untils.js',
     loaded: true,
     children: [],
     paths:
      [ 'E:\\node test\\extra\\src\\node_modules',
        'E:\\node test\\extra\\node_modules',
        'E:\\node test\\node_modules',
        'E:\\node_modules' ] } }

至此,module、exports 和 require的流程就介绍完了。


注意: 路径分析时优先查找缓存,提高二次引入的性能。所以在第二次使用使用该模块时不会执行模块中的js。只会将函数运行的结果引入。例如:

// untils.js
console.log('untils');
module.exports = function() {
  console.log('haha');
}
//index.js
const untils1 = require(./src/untils.js);
const untils2 = require(./src/untils.js);
untils1();
untils2();

// 打印结果
untils
haha
haha

7.node模块间的循环引用

话不多少,直接上源码吧:

modA.js:

module.exports.test = 'A';
const modB = require('./modB.js');
console.log( 'modA:', modB.test);
module.exports.test = 'AA';</pre>

modB.js:

module.exports.test = 'B';
const modA = require('./modA.js');
console.log( 'modB:', modA.test);
module.exports.test = 'BB';</pre>

main.js

const modA = require('./modA');

运行结果如下:

image

刚开始学习和阅读上述代码,是有点觉得晕晕乎乎,如果A与B存在相互依赖、相互引用关系,不就形成了一个闭环或者说死循环?那程序怎么会继续解析呢?很显然,运行结果告诉我们,nodejs引擎有自己的一套处理循环引用的机制。下面我们根据上述运行结果,来推演了两个module模块的执行顺序,以了解nodejs打破闭环的机制。

image

过程分解:

①执行modA第一行,输出一个test接口

②执行modA第二行,要引入modB此时断点产生了,即开始执行modB里的代码, 程序开始走"breakpoint-out"路线

③执行modB第一行

④执行modB第二行,要因为modA,此步骤为打破闭环的关键,此时将A里断点之前的执行结果输出给modB,如图里的蓝色虚线框标识的部分,此时在modB中打印modA.test,打印'A'

⑤继续执行modB第三行

⑥继续执行modB第四行,对外输出test接口('BB'),此后,modB执行完毕,主程序返回至断点处(modA中在②步骤产生的断点),将modB的执行结果保存在'modB' const变量中。

⑦执行modA的第三行

⑧执行modA的第四行,打印'modB'对象里的test接口,根据中指向结果可知,'modB'返回的test接口为'BB',因此,打印'BB',程序结束。

如果main.js调用的是'modB.js',分析过程完全一致,打印的结果将是'B, AA'。

根据上述分析可知,nodejs中的模块互相引用形成的“闭环”其实是用“断点”这一方式打开的,以断点为出口去执行其他模块,也以断点为入口进行返回,之后继续执行断点之后的代码。

——学无止境,保持好奇。may stars guide your way.


8.站在巨人肩上

详解Node模块加载机制
nodejs模块加载机制
模块机制
node前后端模块规范与模块加载原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349