目录
  1. 1. 深入浅出Node.js学习笔记(二).md
    1. 1.1. 模块机制
    2. 1.2. 1.CommonJS规范
      1. 1.2.1. 1.1CommonJS的出发点
      2. 1.2.2. 1.2CommonJS的模块规范
    3. 1.3. 2.Node的模块实现
      1. 1.3.1. 2.1优先从缓存加载
      2. 1.3.2. 2.2路径分析和文件定位
      3. 1.3.3. 2.3模块编译
    4. 1.4. 3.核心模块
      1. 1.4.1. 3.1JavaScript核心模块的编译过程
      2. 1.4.2. 3.2C/C++核心模块的编译过程
      3. 1.4.3. 3.3核心模块的引入流程
      4. 1.4.4. 3.4编写核心模块
    5. 1.5. 4.C/C++扩展模块
      1. 1.5.1. 4.1前提条件
      2. 1.5.2. 4.2C/C++扩展模块的编写
      3. 1.5.3. 4.3C/C++扩展模块的编译
      4. 1.5.4. 4.4C/C++扩展模块的加载
    6. 1.6. 5.模块调用栈
    7. 1.7. 6.包与NPM
      1. 1.7.1. 6.1包结构
      2. 1.7.2. 6.2包描述文件和NPM
      3. 1.7.3. 6.3NPM常用功能
    8. 1.8. 6.4局域NPM
      1. 1.8.1. 6.5NPM潜在问题
    9. 1.9. 7.前后端共用模块
      1. 1.9.1. 7.1模块的侧重点
      2. 1.9.2. 7.2AMD规范
      3. 1.9.3. 7.3CMD规范
      4. 1.9.4. 7.4兼容多种模块规范
深入浅出Node.js学习笔记(二)

深入浅出Node.js学习笔记(二).md

模块机制

Web1.0时代,JavaScript脚本语言的两个主要功能:

  1. 表单验证;
  2. 网页特效;

Web2.0时代,前端工程师利用JavaScript大大提升了网页的用户体验,经历了工具类库、组件库、前端框架、前端应用的变迁。

JavaScript的先天缺陷:模块

高级语言的模块化机制:

  1. Java-类文件;
  2. Python-import机制;
  3. Ruby-require;
  4. PHP-include和require;

1.CommonJS规范

commonJS的愿景:希望JavaScript能够在任何地方运行。

1.1CommonJS的出发点

JavaScript规范的缺陷:

  1. 没有模块系统;
  2. 标准库较少;
  3. 没有标准接口;
  4. 缺乏包管理系统;

CommonJS规范中,CommonJSAPI可以编写的应用:

  1. 服务端JavaScript应用程序;
  2. 命令行工具;
  3. 桌面图形界面应用程序;
  4. 混合应用;

1.2CommonJS的模块规范

  1. 模块引用

    采用require()方法;

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

    require():用来引入外部模块;

    exports:导出模块的方法或变量,唯一导出的出口;

    module:代表模块自身;

    // math.js
    exports.add = function (){
        var sum = 0,
        i = 0,
        args = arguments,
        l = args.lenght;
        while(i<l){
            sum += args[i++];
        }
        return sum;
    }
    
    //program.js
    var math = require("math");
    exports.increment = function (val){
        return math.add(val,1);
    }
  1. 模块标识

    模块标识:

    就是传递给require()方法的参数,符合小驼峰命名的字符串,或者以...开头的相对路径或绝对路径,可以没有后缀.js。

2.Node的模块实现

Node中引入模块经历的三个步骤:

  1. 路径分析;
  2. 文件定位;
  3. 编译执行;

Node中,模块分为两种:

  1. 核心模块(Node提供的模块);

    核心模块部分在Node源代码的编译过程中,编译了二进制执行文件。在Node进程启动时,部分核心模块被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行两个步骤可以省略掉,并且在路径分析中优先判断,它的加载速度是最快的。

  2. 文件模块(用户编写的模块);

    文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

2.1优先从缓存加载

与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入的开销。不同的是,浏览器仅缓存文件,而Node缓存的是编译和执行后的对象。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。

2.2路径分析和文件定位

  1. 模块标识符分析

    模块标识符在Node中的分类:

    1. 核心模块,如http、fs、path等;
    2. ...开始的相对路径文件模块;
    3. 以/开始的绝对文件模块;
    4. 非路径形式的文件模块,如自定义的connect模块;

    核心模块

    核心模块的优先级仅次于缓存加载,在Node的源代码编译过程中,已经编译为二进制代码了,其加载过程最快。

    路径形式的文件模块

    在分析路径模块时,require()将路径转化为真实路径,以真实路径作为索引,将编译执行后的结果放在缓存中,以使二次加载更快,其加载速度慢于核心模块。

    自定义模块

    这类模块的查找最费时,也是所有方式最慢的一种。

    模块路径:

    Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。

  2. 文件定位

    从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。

    文件定位过程中,需要注意的细节,包括文件扩展名的分析、目录和包的处理。

    文件扩展名分析

    require()在分析标识符的过程中,出现标识符中不包含文件扩展名的情况,Node会按.js、.json、.node的次序补足扩展名,依次尝试。

    在尝试的过程中,需要调用fs模块同步阻塞式低判断文件是否存在。

    目录分析和包

    在分析标识的过程中,require()通过分析文件扩展名之后,可能没有查找到对应的文件,但却得到一个目录,此时Node会将目录当做一个包来处理。

2.3模块编译

在Node中,每个文件模块都是一个对象。

编译和执行时引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入方法也有所不同。

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

3.核心模块

核心模块分为两部分:

  1. C/C++编写的部分;
  2. JavaScript编写的部分;

3.1JavaScript核心模块的编译过程

JavaScript核心模块的编译过程:

  1. 转存为C/C++代码;

    在此过程中,JavaScript代码已字符串的形式存储在Node的命名空间中,是不可直接执行的。在启动Node进程时,JavaScript代码直接加载进内存中。在加载的过程中,JavaScript核心模块经历标识符分析后,直接定位到内存中,比普通的文件模块从磁盘中一处一处查找要快得多。

  2. 编译JavaScript核心模块;

    在引入JavaScript核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出exports对象。与文件模块区别的地方在于:获取源码的方式(核心模块从内存中加载)以及缓存执行结果的位置。

3.2C/C++核心模块的编译过程

C/C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。

  1. 内建模块的组织形式;

    内建模块的优势:

    1. C/C++编写,性能优于脚本语言;
    2. 在进行文件编译时,被编译二进制文件。一旦Node开始执行,直接被加载进内存,无须再次做标识定位,文件定位,编译等过程,直接可执行。
  2. 内建模块的导出;

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

    文件模块可能依赖核心模块,核心模块可能依赖内建模块。

3.3核心模块的引入流程

核心模块的引入流程经历了C/C++层面的内建模块的定义,(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块层面的引入。

3.4编写核心模块

核心模块被编译进二进制文件需要遵循一定规则。作为Node的使用者,几乎没有机会参与核心模块的开发。

4.C/C++扩展模块

JavaScript的一个典型的弱点就是位运算。

在JavaScript应用中,会频繁出现位运算的需求,包括转换、编码等过程,通过JavaScript实现,CPU资源会耗费很多。

4.1前提条件

  1. GYP项目生成工具;
  2. V8引擎C++库;
  3. libux库;
  4. Node内部库;
  5. 其他库;

4.2C/C++扩展模块的编写

普通的扩展模块与内建模块的区别在于无须将源代码编译进Node,而是通过dlopen()方式动态加载。

4.3C/C++扩展模块的编译

通过GYP工具实现。

4.4C/C++扩展模块的加载

require()方法通过解析标识符、路径分析、文件定位,然后加载执行即可。

C/C++扩展模块与JavaScript模块的区别在于加载之后不需要编译,子类执行之后就可以被外部调用了,其加载速度比JavaScript模块速度略快。

使用C/C++扩展模块的一个好处在于可以更加灵活和动态地加载它们,保持Node模块自身简单性的同时,给予Node五=无限的可能性。

5.模块调用栈

C/C++内建模块:属于最底层的模块,属于核心模块,主要通过API给JavaScript核心模块和第三方JavaScript文件模块的调用。

JavaScript核心模块的两个职责:

  1. 作为C/C++内建模块的封装层和桥接层,供文件模块调用;
  2. 纯粹的功能模块,不需要和底层打交道;

文件模块:通常由第三方编写,包括普通JavaScript模块的C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。

image-20191230165445878

6.包与NPM

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

包组织模块示意图:

image-20191230170132889

CommonJS包的定义:

由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者用于描述包的相关信息,以供外部读取分析。

6.1包结构

包实际是一个存档文件,即一个目录直接打包为.zip和tar.gz格式的文件,安装后解压还原为目录。

包目录包含的文件:

  1. package.json:包描述文件;
  2. bin:用于存放可执行二进制文件的目录;
  3. lib:用于存放JavaScript代码的目录;
  4. doc:用于存放文档的目录;
  5. test:用于存放单元测试用例的代码;

6.2包描述文件和NPM

包描述文件用于表达非代码相关的信息,是个JSON格式的文件-package.json,位于包的根目录下,是包的重要组成部分。

6.3NPM常用功能

对于Node而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助Node与第三方模块之间形成了很好的一个生态系统。

  1. 查看帮助;

    查看当前NPM版本:

    $ npm -v

    查看帮助:

    $ npm
  2. 安装依赖包

    安装依赖包是NPM最常见的用法,执行语句:

    $ npm install express

    1.全局模式安装

    $ npm install express -g

    2.从本地安装

    本地安装只需为NPM指明package.json文件的所在的位置即可。它可以是一个包含package.json的存档文件,也可以是个URL地址,也可以是个目录有package.json的目录的位置。

    3.从非官方源安装

    从非官方安装,可以通过镜像源安装。

    $ npm config set underscore --registry=http://registry.url

    镜像源安装指定默认源:

    $ npm config set registry http://registry.url
  3. NPM钩子命令

    C/C++模块实际上是编译后才能使用的。package.json中script字段的提出就是让包在安装或者卸载等过程中提供钩子机制。

  4. 发布包

    1. 编写模块;

    2. 初始化包描述文件;

      $ npm init
    3. 注册包仓库账号

      $ npm adduser
    4. 上传包

      $ npm publish .
    5. 安装包

      $ npm install hello_test_jackson --registy=http://npmjs.org
    6. 管理包权限

      多人进行发布

      $ npm owner ls eventproxy
  5. 分析包

    在使用NPM的过程中,或许你不能确认当前目录下能否通过require()顺利引入想要的包,执行npm ls分析包。

    $ npm ls

6.4局域NPM

为了同时能够享受NPM上众多的包,同时对自己的包进行保密和限制,现有的解决方案就是企业搭建自己的NPM仓库。

企业混合使用官方仓库和局域仓库的示意图:

image-20191230183003521

对于企业内部而言,私有的可重用模块可以打包到局域NPM仓库,这样可以保持更新的中心化,不至于让各个小项目各自维护相同功能的模块,杜绝通过复制粘贴实现代码共享的行为。

6.5NPM潜在问题

NPM的潜在问题:

  1. 每个人都可以分享包平台上,鉴于开发人员水平不一,上面的包的质量也良莠不齐;
  2. Node代码可以运行在服务器端,需要考虑安全问题;

对于包的使用者而言,包质量和安全问题需要作为是否采纳模块的一个判断条件。

如何评判一个包的安全和质量?

  1. 开源社区内在的健康发展机制-口碑效应;
  2. Github中,模块项目的观察者数量和分支数量;
  3. 包中测试用例和文档的状况;

Kwalitee模块的考察点:

  1. 具备良好的测试;
  2. 具备良好的文档(README、API);
  3. 具备良好的测试覆盖率;
  4. 具备良好的编码规范;
  5. 更多条件;

7.前后端共用模块

7.1模块的侧重点

前后端JavaScript分别搁置在HTTP的两端,它们扮演的角色并不同。

浏览器端的JavaScript:

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

浏览器端的JavaScript:

相同的代码需要多次执行,瓶颈在于CPU和内存等资源,从磁盘中加载代码;

两者的加载的速度不在一个数量级别。

CommonJS为后端JavaScript制定的规范;

AMD为前端JavaScript制定的规范;

7.2AMD规范

AMD规范是CommonJS模块规范的一个延伸,定义如下;

define(id?,dependenceies?,factory)

模块的id和依赖是可选的,

与Node模块相似之处:

factory的内容就是实际代码的内容;

与Node模块不同之处:

  1. AMD需要define定义一个模块,Node实现中是隐式包装的;
  2. 内容需要通过返回的方式实现导出;

7.3CMD规范

CMD规范由国内的玉伯提出,与AMD规范的主要区别在于定义模块和依赖引入的部分。

AMD需要在声明模块的时候指定所有的依赖,通过形参传递到模块内容;

define(['dep1','dep2'],function (dep1,dep2){
    return function (){};
})

7.4兼容多种模块规范

为了让同一个模块可以运行在前后端,需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。

文章作者: BleakNight
文章链接: https://bleaknight95.github.io/2020/01/03/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BANode.js%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%BA%8C%EF%BC%89/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 BleakNight's Blog
打赏
  • 微信
  • 支付寶

评论