模块机制

JavaScript自诞生以来,曾经没有人拿它当作一门真正的编程语言,认为它不过是一种网页小脚本而已。直到Web 2.0时代,前端工程师利用它大大提高了网页上的用户体验。在这个过程当中,B/S应用展示出比C/S应用优越的地方。至此,JavaScript才被普遍重视起来。javascript

经历了长长的后天努力过程,JavaScript不断被类聚和抽象,以更好地组织业务逻辑。从另外一个角度而言,它也道出了JavaScript先天就缺少的一项功能:模块。前端

在其余高级语言中,Java有类文件,Python有import机制,Ruby有require,PHP有include和require。而JavaScript经过<script>标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力,直到出现CommonJS规范。java

CommonJS规范

CommonJS规范为JavaScript制定了一个美好的愿景——但愿JavaScript可以在任何地方运行。node

CommonJS的出发点

CommonJS规范的提出,主要是为了弥补当前JavaScript没有标准的缺陷,以达到像Python、Ruby和Java具有开发大型应用的基础能力,而不是停留在小脚本程序的阶段。他们指望那些用CommonJS API写出的应用能够具有跨宿主环境执行的能力,这样不只能够利用JavaScript开发富客户端应用,并且还能够编写如下应用。webpack

  • 服务器端JavaScript应用程序。
  • 命令行工具。
  • 桌面图形界面应用程序。
  • 混合应用(Titanium和Adobe AIR等形式的应用)。

现在,CommonJS中的大部分规范虽然依旧是草案,可是已经初显成效,为JavaScript开发大型应用程序指明了一条很是棒的道路。目前,它依旧在成长中,这些规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、Web服务器网关接口、包管理等。c++

CommonJS的模块规范

CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。web

  1. 模块引用(require)
var fs = require('fs');
  1. 模块定义(exports)
// math.js
exports.add = function () {
  var sum = 0,
    i = 0,
    args = arguments,
    l = args.length;
  while (i < l) {
    sum += args[i++];
  }
  return sum;
};
  1. 模块标识
    模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它能够没有文件名后缀.js。

模块机制

CommonJS构建的这套模块导出和引入机制使得用户彻底没必要考虑变量污染,命名空间等方案与之相比相形见绌。express

Node的模块实现

Node在实现中并不是彻底按照规范实现,而是对模块规范进行了必定的取舍,同时也增长了少量自身须要的特性。在Node中引入模块,须要经历以下3个步骤。npm

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

在Node中,模块分为两类:一类是Node提供的模块,称为核心模块;另外一类是用户编写的模块,称为文件模块。编程

  • 核心模块部分在Node源代码的编译过程当中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,因此这部分核心模块引入时,文件定位和编译执行这两个步骤能够省略掉,而且在路径分析中优先判断,因此它的加载速度是最 快的。
  • 文件模块则是在运行时动态加载,须要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

Node对引入过的模块都会进行缓存,以减小二次引入时的开销。Node缓存的是编译和执行以后的对象。不管是核心模块仍是文件模块,require()方法对相同模块的二次加载都一概采用缓存优先的方式,这是第一优先级的。不一样之处在于核心模块的缓存检查先于文件模块的缓存检查。

CommonJS包规范

CommonJS的包规范的定义其实也十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各类文件,后者则用于描述包的相关信息,以供外部读取分析。

包结构

包其实是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。彻底符合CommonJS规范的包目录应该包含以下这些文件。

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

能够看到,CommonJS包规范从文档、测试等方面都作过考虑。当一个包完成后向外公布时,用户看到单元测试和文档的时候,会给他们一种踏实可靠的感受。

注意: 最好不要用CommonJS规范的文件名,存储与其功能不对应的文件。

包描述文件与NPM

包描述文件用于表达非代码相关的信息,它是一个JSON格式的文件——package.json,位于包的根目录下,是包的重要组成部分。而NPM的全部行为都与包描述文件的字段息息相关。因为CommonJS包规范尚处于草案阶段,NPM在实践中作了必定的取舍,这里就只介绍实践相关主要字段了。

{
    "name": "包名。规范定义它须要由小写的字母和数字组成,能够包含.、_和-,但不容许出现空格。",
    "version": "版本号",
    "description": "包简介",
    "keywords": "关键词数组,NPM中主要用来作分类搜索。一个好的关键词数组有利于用户快速找到你编写的包。",
    "repositories": "托管源代码的位置列表,代表能够经过哪些方式和地址访问包的源代码。",
    "author": "做者",
    "bin": "一些包做者但愿包能够做为命令行工具使用。配置好bin字段后,经过npm install package_name -g命令能够将脚本添加到执行路径中,以后能够在命令行中直接执行。前面的node-gyp便是这样安装的。经过-g命令安装的模块包称为全局模式。",
    "main": "入口文件",
    "scripts": "脚本说明对象。它主要被包管理器用来安装、编译、测试和卸载包。",
    "engines": "支持的JavaScript引擎列表,有效的引擎取值包括ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node和v8。",
    "dependencies": "使用当前包所须要依赖的包列表。",
    "devDependencies": "一些模块只在开发时须要依赖。配置这个属性,能够提示包的后续开发者安装依赖包。",
    "licenses": "当前包所使用的许可证列表,表示这个包能够在哪些许可证下使用。",
    "contributors": "贡献者列表。在开源社区中,为开源项目提供代码是常常出现的事情,若是名字能出如今知名项目的contributors列表中,是一件比较有荣誉感的事。列表中的第一个贡献应当是包的做者本人。它的格式与维护者列表相同。",
    "maintainers": "包维护者列表"
}

NPM经常使用功能

CommonJS包规范是理论,NPM是其中的一种实践。NPM之于Node,至关于gem之于Ruby,pear之于PHP。对于Node而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助NPM,Node与第三方模块之间造成了很好的一个生态系统。

借助NPM,能够帮助用户快速安装和管理依赖包。除此以外,NPM还有一些巧妙的用法,下面咱们详细介绍一下。

1.查看帮助

在安装Node以后,执行npm –v命令能够查看当前NPM的版本:

$ npm -v
3.10.9

在不熟悉NPM的命令以前,能够直接执行NPM查看到帮助引导说明:

$ npm

Usage: npm <command>

where <command> is one of:
    access, adduser, bin, bugs, c, cache, completion, config,
    ddp, dedupe, deprecate, dist-tag, docs, edit, explore, get,
    help, help-search, i, init, install, install-test, it, link,
    list, ln, login, logout, ls, outdated, owner, pack, ping,
    prefix, prune, publish, rb, rebuild, repo, restart, root,
    run, run-script, s, se, search, set, shrinkwrap, star,
    stars, start, stop, t, tag, team, test, tst, un, uninstall,
    unpublish, unstar, up, update, v, version, view, whoami

npm <cmd> -h     quick help on <cmd>
npm -l           display full usage info
npm help <term>  search for help on <term>
npm help npm     involved overview

Specify configs in the ini-formatted file:
    /Users/yangzhinian/.npmrc
or on the command line via: npm <command> --key value
Config info can be viewed via: npm help config

npm@3.10.9 /Users/yangzhinian/.nvm/versions/node/v6.9.2/lib/node_modules/npm

能够看到,帮助中列出了全部的命令,其中npm help <command &gt能够查看具体的命令说明。

2.安装依赖包

安装依赖包是NPM最多见的用法,它的执行语句是npm install express。执行该命令后,NPM会在当前目录下建立node_modules目录,而后在node_modules目录下建立express目录,接着将包解压到这个目录下。

安装好依赖包后,直接在代码中调用require('express');便可引入该包。require()方法在作路径分析的时候会经过模块路径查找到express所在的位置。模块引入和包的安装这两个步骤是相辅相承的。

全局模式安装

若是包中含有命令行工具,那么须要执行npm install express -g命令进行全局模式安装。须要注意的是,全局模式并非将一个模块包安装为一个全局包的意思,它并不意味着能够从任何地方经过require()来引用到它。

全局模式这个称谓其实并不精确,存在诸多误导。实际上,-g是将一个包安装为全局可用的可执行命令。它根据包描述文件中的bin字段配置,将实际脚本连接到与Node可执行文件相同的路径下

"bin": {
  "express": "./bin/express"
}

事实上,经过全局模式安装的全部模块包都被安装进了一个统一的目录下,这个目录能够经过以下方式推算出来:

path.resolve(process.execPath, '..', '..', 'lib', 'node_modules');

若是Node可执行文件的位置是/usr/local/bin/node,那么模块目录就是/usr/local/lib/node_modules。最后,经过软连接的方式将bin字段配置的可执行文件连接到Node的可执行目录下。

本地安装

对于一些没有发布到NPM上的包,或是由于网络缘由致使没法直接安装的包,能够经过将包下载到本地,而后以本地安装。本地安装只需为NPM指明package.json文件所在的位置便可:它能够是一个包含package.json的存档文件,也能够是一个URL地址,也能够是一个目录下有package.json文件的目录位置。具体参数以下:

npm install <tarball file>
npm install <tarball url>
npm install <folder>
从非官方源安装

若是不能经过官方源安装,能够经过镜像源安装。在执行命令时,添加 --registry=http://registry.url便可,示例以下:

npm install underscore --registry=http://registry.url

若是使用过程当中几乎都采用镜像源安装,能够执行如下命令指定默认源:

npm config set registry http://registry.url

3.NPM钩子命令

"scripts": {
    "test": "make test",
    "start": "./node_modules/.bin/nodemon -L  index.js",
    "dev": "webpack-dev-server --config ./bin/build/webpack.dev.conf.js"
}

当在一个具体的包目录下执行npm run test时,将会执行"make test"命令。

4.发布包

为了将整个NPM的流程串联起来,这里将演示如何编写一个包,将其发布到NPM仓库中,并经过NPM安装回本地。

编写模块

模块的内容咱们尽可能保持简单,这里仍是以sayHello做为例子,相关代码以下:

exports.sayHello = function () {
  return 'Hello, world.';
};

将这段代码保存为hello.js便可。

初始化包描述文件

package.json文件的内容尽管相对较多,可是实际发布一个包时并不须要一行一行编写。NPM提供的npm init命令会帮助你生成package.json文件,具体以下所示:

$ npm init
注册包仓库帐号

为了维护包,NPM必需要使用仓库帐号才容许将包发布到仓库中。注册帐号的命令是npm adduser。这也是一个提问式的交互过程,按顺序进行便可:

$ npm adduser
Username: (jacksontian) 
Email: (shyvo1987@gmail.com)
上传包

上传包的命令是npm publish 。在刚刚建立的package.json文件所在的目录下,执行npm publish .开始上传包,相关代码以下:

$ npm publish .

在这个过程当中,NPM会将目录打包为一个存档文件,而后上传到官方源仓库中。

安装包
$ npm install package_name
管理包权限

一般,一个包只有一我的拥有权限进行发布。若是须要多人进行发布,可使用npm owner命令帮助你管理包的全部者:

$ npm owner ls eventproxy
npm http GET https://registry.npmjs.org/eventproxy
npm http 200 https://registry.npmjs.org/eventproxy
jacksontian <shyvo1987@gmail.com>

使用这个命令,也能够添加包的拥有者,删除一个包的拥有者:

npm owner ls <package name>
npm owner add <user> <package name>
npm owner rm <user> <package name>

5.分析包

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

这个命令能够为你分析出当前路径下可以经过模块路径找到的全部包,并生成依赖树,以下:

$ npm ls
/Users/jacksontian
├─┬ connect@2.0.3 
│ ├── crc@0.1.0 
│ ├── debug@0.6.0 
│ ├── formidable@1.0.9 
│ ├── mime@1.2.4 
│ └── qs@0.4.2 
├── hello_test_jackson@0.0.1 
└── urllib@0.2.3

局域NPM

NPM自身是开源的,不管是它的服务器端和客户端。经过源代码搭建本身的仓库并非什么秘密。

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

NPM潜在问题

潜在的问题在于,在NPM平台上,每一个人均可以分享包到平台上,鉴于开发人员水平不一,上面的包的质量也参差不齐。另外一个问题则是,Node代码能够运行在服务器端,须要考虑安全问题。

对于包的使用者而言,包质量和安全问题须要做为是否采纳模块的一个判断条件。好的包大体具有如下几种特征:

  • 具有良好的测试。
  • 具有良好的文档(README、API)。
  • 具有良好的测试覆盖率。
  • 具有良好的编码规范。
  • 更多条件。

先后端共用模块

JavaScript在Node出现以后,比别的编程语言多了一项优点,那就是一些模块能够在先后端实现共用,这是由于不少API在各个宿主环境下都提供。可是在实际状况中,先后端的环境是略有差异的。

模块的侧重点

纵观Node的模块引入过程,几乎全都是同步的。尽管与Node强调异步的行为有些相反,但它是合理的。可是若是前端模块也采用同步的方式来引入,那将会在用户体验上形成很大的问题。UI在初始化过程当中须要花费不少时间来等待脚本加载完成。

鉴于网络的缘由,CommonJS为后端JavaScript制定的规范并不彻底适合前端的应用场景。通过一段争执以后,AMD规范最终在前端应用场景中胜出。它的全称是Asynchronous Module Definition,便是"异步模块定义",除此以外,还有玉伯定义的CMD规范。

AMD与CMD的区别

CMD与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD须要在声明模块的时候指定全部的依赖,经过形参传递依赖到模块内容中:

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

与AMD模块规范相比,CMD模块更接近于Node对CommonJS规范的定义:

define(factory);

在依赖部分,CMD支持动态引入,示例以下:

define(function(require, exports, module) {
  // The module code goes here
});

require、exports和module经过形参传递给模块,在须要依赖模块时,随时调用require()引入便可。

兼容多种模块规范

为了让同一个模块能够运行在先后端,在写做过程当中须要考虑兼容前端也实现了模块规范的环境。为了保持先后端的一致性,类库开发者须要将类库代码包装在一个闭包内。如下代码演示如何将hello()方法定义到不一样的运行环境中,它可以兼容Node、AMD、CMD以及常见的浏览器环境中

;(function (name, definition) {
  // 检测上下文环境是否为AMD或CMD
  var hasDefine = typeof define === 'function',
    // 检查上下文环境是否为Node
    hasExports = typeof module !== 'undefined' && module.exports;

  if (hasDefine) {
    // AMD环境或CMD环境
    define(definition);
  } else if (hasExports) {
    // 定义为普通Node模块
    module.exports = definition();
  } else {
    // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
    this[name] = definition();
  }
})('hello', function () {
  var hello = function () {};
  return hello;
});
相关文章
相关标签/搜索