nodejs学习笔记之包、模块实现

简单了解了node的安装和一些基本的常识之后,今天学习了node中很重要的包和模块的一些知识点。

首先学习一下包的规范,它由包结构和包描述两部分组成。包结构用于组织包的各种文件,包描述用于描述包的信息,供外部读取分析。

完全符合CommonJS规范的包目录包含一下结构:

    package.json: 包的描述文件
    bin: 用于存放可执行的二进制文件的目录 
    lib: 用于存放javascript的目录 
    doc: 用于存放文档的目录 
    test: 用于存放单元测试用例的代码
    node_modules: 第三方模块
    README.md: 关于描述

  

下面以知名框架express项目的package.json文件,讲解一下个参数的含义:

 

{
  "name": "express", //包名由小写字母和数字组成,包含._-,但不允许空格,包名须是唯一的
  "description": "Sinatra inspired web development framework", //包介绍
  "version": "4.4.4", //版本号,用于版本控制,一般是major.minor.revision格式
  "author": { //包作者
    "name": "TJ Holowaychuk",
    "email": "tj@vision-media.ca"
  },
  "contributors": [ //贡献者列表,每个维护者由name、email和web组成
    {
      "name": "Aaron Heckmann",
      "email": "aaron.heckmann+github@gmail.com"
    },
    {
      "name": "Ciaran Jessup",
      "email": "ciaranj@gmail.com"
    },
    {
      "name": "Douglas Christopher Wilson",
      "email": "doug@somethingdoug.com"
    },
    {
      "name": "Guillermo Rauch",
      "email": "rauchg@gmail.com"
    },
    {
      "name": "Jonathan Ong",
      "email": "me@jongleberry.com"
    },
    {
      "name": "Roman Shtylman"
    }
  ],
  "keywords": [ //关键词数组,有利于用户快速查找到
    "express",
    "framework",
    "sinatra",
    "web",
    "rest",
    "restful",
    "router",
    "app",
    "api"
  ],
  "repository": { //托管源代码的位置
    "type": "git",
    "url": "git://github.com/visionmedia/express"
  },
  "license": "MIT", //许可证列表
  "dependencies": { //当前包所依赖的包列表
    "accepts": "~1.0.5",
    "buffer-crc32": "0.2.3",
    "debug": "1.0.2",
    "escape-html": "1.0.1",
    "methods": "1.0.1",
    "parseurl": "1.0.1",
    "proxy-addr": "1.0.1",
    "range-parser": "1.0.0",
    "send": "0.4.3",
    "serve-static": "1.2.3",
    "type-is": "1.2.1",
    "vary": "0.1.0",
    "cookie": "0.1.2",
    "fresh": "0.2.2",
    "cookie-signature": "1.0.3",
    "merge-descriptors": "0.0.2",
    "utils-merge": "1.0.0",
    "qs": "0.6.6",
    "path-to-regexp": "0.1.2"
  },
  "devDependencies": { //一些模块只有在开发的时候需要依赖,用于提示后续开发者
    "after": "0.8.1",
    "istanbul": "0.2.10",
    "mocha": "~1.20.1",
    "should": "~4.0.4",
    "supertest": "~0.13.0",
    "connect-redis": "~2.0.0",
    "ejs": "~1.0.0",
    "jade": "~1.3.1",
    "marked": "0.3.2",
    "multiparty": "~3.2.4",
    "hjs": "~0.0.6",
    "body-parser": "~1.4.3",
    "cookie-parser": "~1.3.1",
    "express-session": "~1.5.0",
    "method-override": "2.0.2",
    "morgan": "1.1.1",
    "vhost": "2.0.0"
  },
  "engines": { //支持的javascript引擎列表
    "node": ">= 0.10.0"
  },
  "scripts": { //脚本说明对象
    "prepublish": "npm prune",
    "test": "mocha --require test/support/env --reporter dot --check-leaks test/ test/acceptance/",
    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/",
    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/"
  },
  "bugs": { //反馈bug的地址
    "url": "https://github.com/visionmedia/express/issues"
  },
  "homepage": "https://github.com/visionmedia/express" //主页地址
}

  

其次学习一下模块的实现,尽管规范中exports、require、module听起来很简单,我们还是需要了解一下这个过程中究竟经历了什么。

node中引入模块需要经历三个步骤:路径分析、文件定位、编译执行。

我们知道在node中模块主要分为两大部分:核心模块由node本身提供,文件模块由用户编写。它们的执行速度明显核心模块优于文件模块,因为核心模块在node编译的过程中,编译进了二进制执行文件,省略掉了文件定位和编译执行,并且在路径分析中优先判断。文件模块需要完整的路径分析、文件定位和编译执行。需要注意的是node中也有缓存机制,相同的模块在第二次加载的时候,优先从缓存加载,并且核心模块的缓存检查优于文件模块。

第一个步骤:路径分析

require接收一个表示符作为参数,标识符在node中分为以下几类:

  • 核心模块:如http、fs
  • .或..开始的相对路径文件模块
  • 以/开始的绝对路径文件模块
  • 分路径形式的文件模块
前三类都很明确根据路径查找,不需要做过多的讲解。文件模块的路径生成规则是:首先查找当前目录下的node_modules目录;其次查找父目录下的node_modules目录;再次查找父目录的父目录下的node_modules;最后不断向上递归查找,直到根目录的node_modules目录。总而言之一句话:一直向上找,直到找到根目录,如果找不到会进入文件文件定位阶段。

第二个步骤:文件定位

我们知道在我们写require的时可以不叫扩展名,这个时候就需要一个规则,来判断到底使用的是什么后缀的文件,这里就会用到文件定位。首先会补全扩展名查找,补全的顺序是:.js、.node、.json。如果补全之后还没有找到的话,会把这个表示符作为一个目录查找,找到这个目录后会,查找当前目录下是否有package.json,提取main的属性值进行定位,如果没有main的话,会查找index,然后一次查找index.js、index.json、index.node,如果还是找不到的话,就会抛出查找失败的异常。

第三个步骤:编译执行

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

  • .js文件,通过fs模块同步读取文件后编译执行
  • .node文件,这是C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成文件文件
  • .json文件,通过fs模块同步读取文件后,用JSON.parse()解析返回结果
  • 其他文件,它们都被当作.js文件载入
以上是学习过程中整理的笔记,方便以后学习。

参考文献:

深入浅出nodejs -- 朴灵