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

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

内容简介: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()引入即可。同理它也是享受缓存的便利,二次引入时没有性能影响


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

查看所有标签

猜你喜欢:

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

进化

进化

北大首届互联网CIO-CTO班全体同学 著 / 人民邮电出版社 / 2014-10 / 39

精彩视频:http://v.youku.com/v_show/id_XNzkyNzAyNDA0.html 京东购买链接:http://item.jd.com/11549275.html 互动出版网购买链接:http://product.china-pub.com/4352423 互联网是一个年轻的行业,同时也是一个推陈出新、不断进化的行业。 本书是北京大学首届互联网CIO-......一起来看看 《进化》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

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

HEX HSV 互换工具