深入浅出node.js总结-模块机制(1)

栏目: Node.js · 发布时间: 7年前

内容简介:javasciprt 通过script标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力。人们不得不用命名空间等方式人为地约束代码,以求达到安全和易用的目的。javascript有以下缺点:CommonJS就是来弥补这些缺陷的
  • javascript先天就缺乏一项功能:模块

javasciprt 通过script标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力。人们不得不用命名空间等方式人为地约束代码,以求达到安全和易用的目的。

  • 为了让javascript能在服务端有市场,社区为javascript制定了相应的规范——CommonJS

CommonJS规范

CommonJs的出发点

javascript有以下缺点:

  • ==没有模块系统==

  • ==标准库较少==-比如没有文件系统、I/O流等标准的API

  • ==没有标准接口==-几乎没有定义过web服务器或者数据库之类的标准统一接口

  • ==缺乏包管理系统==-javascript没有自动加载和安装依赖的能力

CommonJS就是来弥补这些缺陷的

CommonJS大部分规范依旧是草案,但是为javascript开发大型应用程序指明了一条道路

  • Node受到CommonJS的影响,CommonJS因Node表现优异而走入各个公司项目里,相互影响和促进

CommonJS的模块规范

分为模块引用、模块定义、模块标识

  1. ==模块引用== var math = require('math');

es6已更新一套支持模块引用的语法:import。在调用require时,可以把它放在某个判断条件下,但import不行;在打包编译时,如果require里的文件模块不存在,即便逻辑上不会进入其所在的判断条件,依旧会报错。可以使用try{}catch(e){}来处理

  1. ==模块定义==
  • 提供了exports对象用于导出当前模块的方法或变量

export 是es6的规范,与import一同用,不要弄混了

  • 一个模块里还有module对象,代表模块自身;exports是module的属性

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

// math.js

exports.add = function(a,b){
    return a+b;
}

exports.sum = function(a,b){
    return a+b;
}
复制代码
// index.js

var math = reqire('math');
exports.add = function(val){
    return math.add(val,1);
}

复制代码
  1. ==模块标识==
  • 指的是传给require()方法的参数,必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径

每个模块具有独立的空间,互不干扰,用户不必考虑变量污染

Node的模块实现

Node没有完全按照CommonJS规范实现,做了一定的取舍并加入自身需要的特性。

  • Node引入模块需经历如下3个步骤
  1. ==路径分析==
  2. ==文件定位==
  3. ==编译执行==
  • Node中模块分为两块:一类是Node提供的模块,称为==核心模块==;另一类是用户编写的模块,称为==文件模块==
  1. 核心模块在编译过程中,成为二进制执行文件;Node进程启动时,部分核心模块就直接加载进内存中

开发者引入这部分核心模块时,省略了文件定位、编译执行,且在路径分析中优先判断;故,加载速度最快

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

先路径分析文件定位、最后再运行,所以前面require一个不存在的文件时,即便有判断条件,依然会报错

优先从缓存加载

  • Node对引入过的模块都会进行缓存,以减少二次引入时的开销。==Node缓存的是编译和执行之后的对象==

所以,所有模块exports/export出的的东西,在内存中有且仅有一份,不受exports/export后的表达式/new之类的语句所影响

  • require()方法/import语句对于相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。核心模块的缓存检查优于文件模块的缓存检查

路径分析和文件定位

因标识符的不同形式,模块的查找和定位有不同程度的差异

  1. 模块标识符分析
  • 标识符分为以下几类:

    • ==核心模块==,如http、fs、path
    • .或..开始的==相对路径文件模块==
    • 以/开始的==绝对路径文件模块==
    • ==非路径形式的文件模块==

    核心模块

    • 优先级仅次于缓存加载,加载速度最快
    • 不能将自定义模块的标识符与核心模块标识符定义得一致,除非你换用路径的方式

    路径形式的文件模块

    • . .. /开始的标识符,均视为文件模块
    • require将路径转为真实路径,以此做索引,编译执行后的结果放入缓存
    • 加载速度比核心模块慢

    自定义模块

    • 特殊的文件模块,查找费时,加载速度最慢

    • “模块路径”——Node定位文件模块时定制的查找策略。模块路径是一个路径组成的数组:

      [

       当前文件目录下的xx目录,

       父目录下的xx目录,

       父目录下的父目录下的xx目录,

       沿路径向上逐级递归,直到根目录下的xx目录

      ]

      类似于js的原型链或作用域链的查找方式。也正因如此,一旦路径越深,查找耗时就越多,所以加载速度最慢

  1. 文件定位

有缓存的存在和前面的路径分析,文件定位相对比较简单。这里注意一些细节:

  • 文件扩展名分析

    • 有时标识符没有扩展名,CommonJS规范不允许不包含文件扩展名,但Node会按.js、.json、.node的顺序依次尝试
    • 尝试时,会调用fs模块同步阻塞时判断文件是否存在。由于Node单线程,所以这里会引起性能问题——建议.node/.json文件加上扩展名
  • 目录分析和包

    • 有时没查找到文件,而是一个目录

    • Node会先找当前目录下的package.json,解析出main属性指定的文件名进行定位;如果文件名没有扩展名,则会进入扩展名分析的步骤

      解析方式就是JSON.parse()

    • 如果main提供的文件名有误,或者没有package.json,则查index(.js、.json、.json)

    • 还没定位成功,则按照自定义模块的 模块路径 策略,去父目录查询;如果模块路径数组遍历完毕都没找到,则抛出查找失败的异常

模块编译

(这里指的都是文件模块,非核心模块) 当定位到具体文件后,Node会新建一个模块对象,将文件载入并编译。载入方法根据不同文件扩展名而区分

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

编译成功的模块会缓存在Module._cache上,以文件路径作为索引

  1. javascript模块的编译

    • 编译过程中,Node对获取的javascript的文件内容进行了头尾包装,头部添加(function(==exports, require, module, __filename, __dirname==){\n,尾部添加\n})

    __filename 完整的文件路径;__dirname 文件目录

    • 包装后的代码vm原生模块的runInThisContext()执行,返回一个具体的function对象

    • 最后,将当前模块对象的exports、require()、module、文件路径和目录作为参数传入给function对象

    执行后exports返回给调用方,其上的任何方法与属性均可被外部调用,但是模块中的其余变量或属性不可。==以此达到模块间的作用域隔离==

    这就是Node对CommonJS模块规范的实现

    exports的误用 编写代码时,理论上只要这样写就行:

    exports = function(){//My Class};
    复制代码

    但是这样写是有问题的,我们来看看编译过程:

    1. 头尾包装:
    (function(exports, require, module, __filename, __dirname){
        exports = function(){//My Class};
    })
    复制代码

    看,你把形参改了。。。但是exports最后是要返回给调用方然后被外部调用的,==改形参根本不能真正改变exports对象的内容==

    所以,要不老老实实地写:exports.add = ...;或者写module.exports = ...

  2. c/c++模块的编译

    • 事实上,.node模块不需要编译,因为它是c/c++模块之后编译生成。它只需要加载和执行
    • 执行中exports对象与.node模块产生脸型,返回给调用者
    • 优势:执行效率高;劣势:编写门槛高
  3. JSON文件的编译

    • fs模块同步读取JSON文件
    • JSON.parse()方法得到对象
    • 赋给exports

    如果.json文件作为配置文件(如package.json),则不必调用fs模块去读取和解析,直接reqire()引入即可。同理它也是享受缓存的便利,二次引入时没有性能影响


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learn Python the Hard Way

Learn Python the Hard Way

Zed A. Shaw / Addison-Wesley Professional / 2013-10-11 / USD 39.99

Master Python and become a programmer-even if you never thought you could! This breakthrough book and CD can help practically anyone get started in programming. It's called "The Hard Way," but it's re......一起来看看 《Learn Python the Hard Way》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具