• 《深入浅出Node.js》第2章 模块机制


    @by Ruth92(转载请注明出处)

    第2章 模块机制

    JavaScript 的变迁

    JavaScript 先天缺乏的功能:模块。

    一、CommonJS 规范:

    JavaScript 规范的缺陷:1)没有模块系统;2)标准库较少;3)没有标准接口;4)缺乏包管理系统。

    CommonJS 规范的提出,主要是为了弥补当前 JavaScript 没有标准的缺陷,使其具备开发大型应用的基础能力。

    Node与浏览器以及W3C组织、CommonJS组织、ECMAScript之间的关系

    Node 借鉴 CommonJS 的 Modules 规范实现了一套非常易用的模块系统,NPM 对 Packages 规范的完好支持使得 Node 应用在开发过程中事半功倍。

    ☛ 【CommonJS 对模块的定义】:

    1. 模块引用:

      require() 方法:引入一个模块的 API 到当前上下文中。

       var math = require('math');
      
    2. 模块定义:

      module 对象:代表模块自身。

      exports 对象:module 的属性,用于导出当前模块的方法或者变量,且它是唯一导出的出口。

      在 Node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性即可定义导出的方式:

       // math.js
       exports.add = function() {};
      
    3. 模块标识:传递给 require() 方法的参数

      模块定义

    ♫ 【优点】:CommonJS 构建的这套模块导出和引入机制使得用户完全不必考虑变量污染,命名空间等方案与之相比相形见绌。

    二、Node 的模块实现

    在 Node 中引入模块,需要经历:(1)路径分析;(2)文件定位;(3)编译执行。

    Node 中的模块分为两类:

    (1)核心模块:在 Node 源代码编译过程中,编译进了二进制执行文件,所以在引入时,可以省略文件定位和编译执行,且在路径分析中优先判断,加载速度最快。

    (2)文件模块:在运行时动态加载,需要完整的3步过程,速度较慢。

    ☛ 【详细的模块加载过程】:

    1. 优先从缓存加载(第一优先级)

      ♫ 提高性能的方式比较:

      • 前端浏览器:缓存静态脚本文件,仅缓存文件。
      • Node:对引入过的模块进行缓存,缓存的是编译和执行之后的对象。

      无论是核心模块还是文件模块,require() 方法对相同模块的二次加载都一律采用缓存优先的方式。

      核心模块的缓存检查先于文件模块。

    2. 路径分析与文件定位

      ① 模块标识符分析:require() 方法的参数

      • 核心模块:如 http、fs、path 等。(最快)
      • 路径形式的文件模块:... 开始的相对路径文件模块 和 以 / 开始的绝对路径文件模块。
      • 自定义模块:特殊的文件模块,可能是一个文件或者包的形式。(最慢)
        • 模块路径:Node 在定位文件模块的具体文件时指定的查找策略,具体表现为一个路径组成的数组
        • 当前文件的路径越深,模块查找越耗时,因此自定义模块加载速度最慢。

      ② 文件定位

      • 文件扩展名分析
        • require() 在分析标识符的过程中,会出现标识符不包含扩展名的情况;
        • Node 会按 .js.json.node 的次序依次补足扩展名,调用 fs 模块同步阻塞式地依次尝试。
        • 缓解 Node 单线程中阻塞式调用的诀窍:(1) 带上扩展名;(2) 同步配合缓存。
      • 目录分析和包
        • 在分析标识符时,require() 通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录(经常出现在引入自定义模块和逐个模块路径进行查找时),此时 Node 会将目录当做一个包来处理。
    3. 模块编译

      在 Node 中,每个文件模块都是一个对象,其定义如下:

       function Module(id, parent) {
           this.id = id;
           this.exports = {};
           this.parent = parent;
           if (parent && parent.children) {
               parent.children.push(this);
           }
       
           this.filename = null;
           this.loaded = false;
           this.children = [];
       }
      
      • 定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。

      • 对于不同的文件扩展名,有不同的载入方法:

        • .js 文件:通过 fs 模块同步读取文件后编译执行;
        • .node 文件:这是用 c/C++ 编写的扩展文件,通过 dlopen() 方法加在最后编译生成的文件;
        • .json 文件:通过 fs 模块同步读取文件后,用 JSON.parse() 解析返回结果;
        • 其余扩展名文件:它们都被当做 .js 文件载入。
      • 每一次编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上,以提高二次引入的性能。

      • exports 只能改变形参的引用,但不能改变作用域外的值;要达到 require 引入一个类的效果,请赋值给 module.exports 对象。

    三、核心模块

    核心模块分为两部分: c/C++ 编写的和 JavaScript 编写的。

    1. JavaScript 核心模块的编译过程

      ① 转存为 C/C++ 代码

      ② 编译 JavaScript 核心模块

      与文件模块的区别:

      • 获取源代码的方式(核心模块是从内存中加载的);
      • 缓存执行结果的位置:编译成功的模块缓存到 NativeModule._cache 对象上,文件模块则缓存到 Module._cache 对象上。
    2. C/C++ 核心模块的编译过程

      内建模块:由纯 C/C++ 编写的部分,Node 的 buffercryptoevalsfsos 等。

      内建模块的优势:(1)性能上优于脚本语言;(2)在进行文件编译时,被编译进二进制文件,一旦 Node 开始执行,它们被直接加载进内存中,无须再做标识符定位、文件定位、编译等过程,直接就可执行。

      ➹ 【内建模块的导出】:

      • 在 Node 的所有模块类型中,存在着一种依赖层级关系:

        依赖层级关系

      通常,不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块即可,因为核心模块中基本都封装了内建模块。

      • Node 在启动时,会生成一个全局变量 process(),并提供 Binding() 方法来协助加载内建模块。

    3. 核心模块的引入流程

      os原生模块的引入流程

    4. 编写核心模块

      • 核心模块中的 JavaScript 部分几乎与文件模块的开发相同,遵循 CommonJS 规范,上下文中除了拥有 requiremoduleexports 外,还可以调用 Node 中的一些全局变量;
      • 内建模块的编写通常分成两步:编写头文件和编写 C/C++ 文件。

    四、C/C++ 扩展模块

    扩展模块不同平台上的编译和加载过程

    • C/C++ 扩展模块与 JavaScript 模块的区别在于加载之后不需要编译,直接执行之后就可以被外部调用了,其加载速度比 JavaScript 模块略快。
    • 使用 C/C++ 扩展模块的好处:可更灵活和动态地加载它们,保持 Node 模块自身简单性的同时,给予 Node 无限的可扩展性。

    五、模块调用栈

    模块之间的调用关系

    • C/C++ 内建模块属于最底层的模块,它属于核心模块,主要提供 API 给 JavaScript 核心模块和第三方JavaScript 文件模块调用。
    • javaScript 核心模块属于的两类职责:1)作为C/C++内建模块的封装层和桥接层,供文件模块调用;2)纯粹的功能模块,它不需要跟底层打交道,但是又十分重要。

    六、包与 NPM

    在模块之外,包和 NPM 是将模块联系起来的一种机制。

    包组织模块示意图

    • CommonJS 的包规范由两部分组成:包结构、包描述文件。
    • CommonJS 包规范是理论,NPM 是其中的一种实践。

    NPM 常用功能

    七、前后端共用模块

    1. 模块的侧重点

      • 浏览器端的 JavaScript:需要经历从同一个服务器端分发到多个客户端执行,瓶颈在于带宽,需要通过网络加载代码;

      • 服务器端的 javaScript:相同的代码需要多次执行,瓶颈在于 CPU 和内存等资源,从磁盘加载。

    2. AMD 规范

      • 产生原因:鉴于网络的原因,CommonJS 为后端 JavaScript 制定的规范并不完全适合前端的应用场景。

      • AMD:Asynchronous Module Definition,异步模块定义,是 CommonJS 规范的一个延伸,适用于前端应用场景。

      • AMD 模块定义(id 和依赖是可选的):

          // 定义
          define(id?, dependencies?, factory);
        
          // 实现
          define(function() {
              var exports = {};
              exports.sayHello = function() {
                  alert('Hello from module: ' + module.id);
              };
              return exports;
          })
        
      • 与 Node 的区别:1)AMD模块需要用 define 来明确定义模块,而在 Node 实现中是隐式包装的,目的都是进行作用域隔离;2)内容需要通过返回的方式实现导出。

    3. CMD 规范

      • 由国内的玉伯提出。

      • 与 AMD 规范的主要区别:在于定义模块和依赖引入的部分。

          /**
           * AMD 模块定义:
           * 在声明模块的时候需要制定所有的依赖,通过形参传递到模块内容中
           */
          define(['dep1', 'dep2'], function(dep1, dep2) {
              return function () {};
          });
          
          /**
           * CMD 模块定义:
           * 更加接近于 Node 对 CommonJS 规范的定义
           */
          define(factory);
          
          // 在依赖部分,CMD 支持动态引入
          // require、exports 和 module 通过形参传递给模块
          // 在需要依赖模块时,随时调用 require() 引入即可
          define(function(require, exports, module) {
              // The module code goes here
          });
        
    4. 兼容多种模块规范

      • 为了让同一个模块运行在前后端提出的解决方案;
      • 将类库代码包装在一个闭包内,兼容 Node、AMD、CMD 以及常见的浏览器环境。

    Scoop It and Enjoy the Ride!
  • 相关阅读:
    sqlserver2008 查看数据库自带的索引建议
    DataSnap服务器生成的ID自动更新到客户端
    outlook 插件:导出rss的link地址
    eclipse 升级note
    合并百度影音的离线数据 with python 2.2 bdv格式的更新
    windows ubuntu bcdeditor
    合并百度影音的离线数据 with python 2.1 bdv格式的更新
    合并百度影音的离线数据 with python 第二版 基于yield
    php密码加密(密码散列)
    PHP (超文本预处理器)
  • 原文地址:https://www.cnblogs.com/Ruth92/p/5827651.html
Copyright © 2020-2023  润新知