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
- .或..开始的相对路径文件模块
- 以/开始的绝对路径文件模块
- 分路径形式的文件模块
第二个步骤:文件定位
我们知道在我们写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 -- 朴灵