nodejs如何下载模块发布自己的node模块

咱们闲话不多说,直接开始!
由于我从没有使用过MAC,所以我不保证本文中介绍的操作与MAC一致。
文章开始我先假定各位已经在window全局安装了Node.js,下面开始进行详细步骤介绍:
本文本着,以极少的文字说明以及极少的代码书写为原则来给大家演示!
文章中上传的模块不具备任何意义!
一、封装node.js模块时的必需项
1.创建package.json
每一个完整封装的node模块,必须含有一个参数明确的package.json文件!
以下为package.json的最精简配置:
"name": "npmdesc",
"version": "0.0.1",
"main": "npmdesc.js"
package.json详细配置参照表:
2.创建npmdesc.js
exports.desc = function() {
console.log("如何发布一个自定义Node.js模块到NPM");
3.创建README.md文件,文件内容可以留白(该文件的作用是放置你愿意写的任何自述说明)
二、在github上建立包含该模块代码的公共存储库
1.注册github账户(这里不做详细阐述,假定你已经有了github账户)
2.创建项目
3.如果你初次使用git,请自行了解git的使用方法(这里仅做简单描述)
这里推荐查看廖雪峰老师对git的讲解:/wiki/bb000
1.ssh-keygen -t rsa -C "你注册所使用的邮箱"
2.登陆GitHub,打开&Account settings&,&SSH Keys&页面点&Add SSH Key&,填上ssh,在Key文本框里粘贴id_rsa.pub文件的内容。以上步骤是使你的账户与git建立通讯的核心步骤!仅用这些操作远远不够,如果你初次使用git,那么这一步可能将会使你看的云里雾里摸不清头脑,甚至失去继续了解下文的信心,所以我建议初学者先进行git的简单了解。
4.推送项目到刚刚创建的公共存储库(这里假定你已完成以上所有内容,并对git的使用有了简单的认识)
在你的项目根目录中依次运行以下git命令:
1.git init2.git add npmdesc.js
git add package.json
git add README.md3.git remote add origin :BGOnline-CN/npmdesc.git4.git commit -m "如何发布一个自定义Node.js模块到NPM"
5.git pull --rebase origin master
6.git push -u origin master
以上步骤的作用(这里的截图仅做辅助理解,截图内容并不完善,具体执行步骤需参考第4步):
git init (初始化本地git库)
git add (暂存文件)
git remote add origin :XXXX/XXXX.git (使本地与远程库进行连接)
git commit -m "XXXX" (提交暂存到本地库,-m 为你的提交信息,这个是必填的!你需要告诉git你为什么要进行本次提交。)
git pull --rebase origin master (拉取远程库中的内容)
git push -u origin master (将本地项目推送到远程库)
5.修改package.json文件,填入你的项目在git公共存储库中的信息及你希望npm进行搜索时的关键字
"name": "npmdesc",
"version": "0.0.1",
"main": "npmdesc.js",
"repository": {
"type": "git",
"url": "/BGOnline-CN/npmdesc"
"keywords": [
三、生成模块
1.在你的项目根目录打开控制台窗口键入命令
此时npm会将你的项目打包为一个后缀名为.tgz的压缩包,这就是你的Node.js模块
2.别忘了将修改后的package.json及刚刚打包好的.tgz推送到git哦~这里我就不介绍了。
可能在进行git操作时或多或少的会出现一些你暂时无法理解的问题。如果刚好这时你遇到了,请不要害怕,细致的摸索及强大的搜索引擎可以帮助你!任何人都是这样摸爬滚打过来的。
当然不排除我的文章有疏漏 - -
四、发布到npm
1.在https://npmjs.org网站中建立账户(这里不做介绍)
2.在命令行中使用以下命令把创建的账户添加到环境中:
npm adduser
回车之后将会提示你输入用户名和密码以及邮箱,这里密码将不会明文显示。
出现下图说明命令执行成功!
3.发布模块到npm
在项目根目录执行以下命令:
npm publish
如果出现以上错误,使用如下命令即可解决
npm config set registry http://registry.npmjs.org
执行该命令后需要重新进行登录,使用第2步中的命令即可
五、发布成功
如果一切正常,将会出现下图:
到这里你的&Node.js模块就已经发布成功了,你可以试试在npm官网中搜索它,并在项目中使用它了
阅读(...) 评论()OurJS-我们的JS, 我们的技术-IT文摘; 专注JS相关领域;
我们热爱编程, 我们热爱技术;我们是高大上, 有品味的码农;
欢迎您订阅我们的技术周刊
我们会向您分享我们精心收集整理的,最新的行业资讯,技术动态,外文翻译,热点文章;
我们使用第三方邮件列表向您推送,我们不保存您的任何个人资料,注重您的隐私,您可以随时退订,
欢迎分享您的观点,经验,技巧,心得
让我们一起找寻程序员的快乐,探索技术, 发现IT人生的乐趣;
本网站使用缓存技术每次加载仅需很小流量, 可在手机中流畅浏览;
如果您发现任何BUG,请即时告知我们: ourjs(
订阅邮件周刊
一个程序员是如何搞挂NPM和Node社区的
注意 转载须保留原文链接,译文链接,作者译者等信息。&&
注* 一位程序删除了他自己发布在NPM上面的所有模块,导致一系列连锁反应,以下是这位程序员解释他为什么要删除这些模块:几周以前,一位专利律师给我发送了一封邮箱。让我删除NPM上面的一个"kik"模块。我当然不答应。然后他说 “我也不想这样,但是KIK是我们的注册商标,如果你不删除我们的律师将敲你的门,然后将你的帐户冻结”。当我开始与kik模块时,我不知道它跟一某家公司重名了。我不想让一家公司强迫我改名。我还是拒绝了他们。然而他们找到了NPM的技术支持,然后每封邮件都把我抄送在里面。然后 @izs 在未经我授权的情况下接受了他们的请求,让他们接管这个模块。这样的情形,让我认识到NPM是一家私营公司,企业利益高于个人。而我写开源软件的目标是服务个人。总之,NPM不再是我共享开源作品的地方。所以我把我所有的模块都删除了。这不是一时冲动。我热爱开源我相信我还会不断地贡献开源代码。如果你的应用依赖了我的模块,那么他们可能就不能工作了,对此我很抱歉。如果有志愿者要接管我的模块,我很高兴转交给他。很不幸的是很多项目都依赖于其中一个模块: left-pad,它将左边字符自动填充成空格或者0,代码很简单,仅有11行module.exports =function leftpad (str, len, ch) {& str = String(str);& var i = -1;& if (!ch && ch !== 0) ch = ' ';& len = len - str.& while (++i & len) {& & str = ch +& }&}摘自:这导致了Babel、ReactNative、Ember等大量NodeJS中的重要工程构建失败,突显NPM体系的脆弱性。 这场事件引起NodeJS社区的广泛讨论。该律师也已经发函致歉。"The wording we used here was not perfect. We’re sorry for
creating any impression that this was anything more than a polite
request to use the Kik package name on NPM for an open source project we
have been working on that fits the name."相关阅读:
&热门文章 - 分享最多
&相关阅读 - 大话编程
&关键字 - 开源
&欢迎订阅 - 技术周刊
我们热爱编程, 我们热爱技术; 我们是高端, 大气, 上档次, 有品味, 时刻需要和国际接轨的码农; 欢迎您订阅我们的技术周刊; 您只需要在右上角输入您的邮箱即可; 我们注重您的隐私,您可以随时退订.
加入我们吧! 让我们一起找寻码农的快乐,探索技术, 发现IT人生的乐趣;
我们的微信公众号: ourjs-com
打开微信扫一扫即可关注我们:
IT文摘-程序员(码农)技术周刊Node.js 命令行程序开发教程 - 阮一峰的网络日志
Node.js 命令行程序开发教程
一种编程语言是否易用,很大程度上,取决于开发命令行程序的能力。
Node.js 作为目前最热门的开发工具之一,怎样使用它开发命令行程序,是 Web 开发者应该掌握的技能。
最近,Npm的网志有一组,我觉得写得非常好。下面就是我在它的基础上扩展的教程,应该是目前最好的解决方案了。
一、可执行脚本
我们从最简单的讲起。
首先,使用 JavaScript 语言,写一个可执行脚本 hello 。
#!/usr/bin/env node
console.log('hello world');
然后,修改 hello 的权限。
$ chmod 755 hello
现在,hello 就可以执行了。
hello world
如果想把 hello 前面的路径去除,可以将 hello 的路径加入环境变量 PATH。但是,另一种更好的做法,是在当前目录下新建 package.json ,写入下面的内容。
"name": "hello",
"hello": "hello"
然后执行 npm link 命令。
$ npm link
现在再执行 hello ,就不用输入路径了。
hello world
二、命令行参数的原始写法
命令行参数可以用系统变量 process.argv 获取。
下面是一个脚本 hello 。
#!/usr/bin/env node
console.log('hello ', process.argv[2]);
执行时,直接在脚本文件后面,加上参数即可。
$ ./hello tom
上面代码中,实际上执行的是 node ./hello tom ,对应的 process.argv 是 ['node', '/path/to/hello', 'tom'] 。
三、新建进程
脚本可以通过 child_process 模块新建子进程,从而执行 Unix 系统命令。
#!/usr/bin/env node
var name = process.argv[2];
var exec = require('child_process').
var child = exec('echo hello ' + name, function(err, stdout, stderr) {
console.log(stdout);
用法如下。
$ ./hello tom
四、shelljs 模块
模块重新包装了 child_process,调用系统命令更加方便。它需要安装后使用。
npm install --save shelljs
然后,改写脚本。
#!/usr/bin/env node
var name = process.argv[2];
var shell = require("shelljs");
shell.exec("echo hello " + name);
上面代码是 shelljs 的本地模式,即通过 exec 方法执行 shell 命令。此外还有全局模式,允许直接在脚本中写 shell 命令。
require('shelljs/global');
if (!which('git')) {
echo('Sorry, this script requires git');
mkdir('-p', 'out/Release');
cp('-R', 'stuff/*', 'out/Release');
cd('lib');
ls('*.js').forEach(function(file) {
sed('-i', 'BUILD_VERSION', 'v0.1.2', file);
sed('-i', /.*REMOVE_THIS_LINE.*\n/, '', file);
sed('-i', /.*REPLACE_LINE_WITH_MACRO.*\n/, cat('macro.js'), file);
if (exec('git commit -am "Auto-commit"').code !== 0) {
echo('Error: Git commit failed');
五、yargs 模块
shelljs 只解决了如何调用 shell 命令,而 yargs 模块能够解决如何处理命令行参数。它也需要安装。
$ npm install --save yargs
yargs 模块提供 argv 对象,用来读取命令行参数。请看改写后的 hello 。
#!/usr/bin/env node
var argv = require('yargs').
console.log('hello ', argv.name);
使用时,下面两种用法都可以。
$ hello --name=tom
$ hello --name tom
也就是说,process.argv 的原始返回值如下。
$ node hello --name=tom
'/path/to/myscript.js',
'--name=tom' ]
yargs 可以上面的结果改为一个对象,每个参数项就是一个键值对。
var argv = require('yargs').
// $ node hello --name=tom
// argv = {
如果将 argv.name 改成 argv.n,就可以使用一个字母的短参数形式了。
$ hello -n tom
可以使用 alias 方法,指定 name 是 n 的别名。
#!/usr/bin/env node
var argv = require('yargs')
.alias('n', 'name')
console.log('hello ', argv.n);
这样一来,短参数和长参数就都可以使用了。
$ hello -n tom
$ hello --name tom
argv 对象有一个下划线(_)属性,可以获取非连词线开头的参数。
#!/usr/bin/env node
var argv = require('yargs').
console.log('hello ', argv.n);
console.log(argv._);
用法如下。
$ hello A -n tom B C
[ 'A', 'B', 'C' ]
六、命令行参数的配置
yargs 模块还提供3个方法,用来配置命令行参数。
demand:是否必选
default:默认值
describe:提示
#!/usr/bin/env node
var argv = require('yargs')
.demand(['n'])
.default({n: 'tom'})
.describe({n: 'your name'})
console.log('hello ', argv.n);
上面代码指定 n 参数不可省略,默认值为 tom,并给出一行提示。
options 方法允许将所有这些配置写进一个对象。
#!/usr/bin/env node
var argv = require('yargs')
.option('n', {
alias : 'name',
demand: true,
default: 'tom',
describe: 'your name',
type: 'string'
console.log('hello ', argv.n);
有时,某些参数不需要值,只起到一个开关作用,这时可以用 boolean 方法指定这些参数返回布尔值。
#!/usr/bin/env node
var argv = require('yargs')
.boolean(['n'])
console.log('hello ', argv.n);
上面代码中,参数 n 总是返回一个布尔值,用法如下。
$ hello -n
$ hello -n tom
boolean 方法也可以作为属性,写入 option 对象。
#!/usr/bin/env node
var argv = require('yargs')
.option('n', {
boolean: true
console.log('hello ', argv.n);
七、帮助信息
yargs 模块提供以下方法,生成帮助信息。
usage:用法格式
example:提供例子
help:显示帮助信息
epilog:出现在帮助信息的结尾
#!/usr/bin/env node
var argv = require('yargs')
.option('f', {
alias : 'name',
demand: true,
default: 'tom',
describe: 'your name',
type: 'string'
.usage('Usage: hello [options]')
.example('hello -n tom', 'say hello to Tom')
.help('h')
.alias('h', 'help')
.epilog('copyright 2015')
console.log('hello ', argv.n);
执行结果如下。
$ hello -h
Usage: hello [options]
-f, --name
your name [string] [required] [default: "tom"]
-h, --help
Show help [boolean]
hello -n tom
say hello to Tom
copyright 2015
八、子命令
yargs 模块还允许通过 command 方法,设置 Git 风格的子命令。
#!/usr/bin/env node
var argv = require('yargs')
.command("morning", "good morning", function (yargs) {
console.log("Good Morning");
.command("evening", "good evening", function (yargs) {
console.log("Good Evening");
console.log('hello ', argv.n);
用法如下。
$ hello morning -n tom
Good Morning
可以将这个功能与 shellojs 模块结合起来。
#!/usr/bin/env node
require('shelljs/global');
var argv = require('yargs')
.command("morning", "good morning", function (yargs) {
echo("Good Morning");
.command("evening", "good evening", function (yargs) {
echo("Good Evening");
console.log('hello ', argv.n);
每个子命令往往有自己的参数,这时就需要在回调函数中单独指定。回调函数中,要先用 reset 方法重置 yargs 对象。
#!/usr/bin/env node
require('shelljs/global');
var argv = require('yargs')
.command("morning", "good morning", function (yargs) {
echo("Good Morning");
var argv = yargs.reset()
.option("m", {
alias: "message",
description: "provide any sentence"
.help("h")
.alias("h", "help")
echo(argv.m);
用法如下。
$ hello morning -m "Are you hungry?"
Good Morning
Are you hungry?
九、其他事项
(1)返回值
根据 Unix 传统,程序执行成功返回 0,否则返回 1 。
if (err) {
process.exit(1);
process.exit(0);
(2)重定向
Unix 允许程序之间使用管道重定向数据。
$ ps aux | grep 'node'
脚本可以通过监听标准输入的data 事件,获取重定向的数据。
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(data) {
process.stdout.write(data);
下面是用法。
$ echo 'foo' | ./hello
(3)系统信号
操作系统可以向执行中的进程发送信号,process 对象能够监听信号事件。
process.on('SIGINT', function () {
console.log('Got a SIGINT');
process.exit(0);
发送信号的方法如下。
$ kill -s SIGINT [process_id]
一、什么是内存泄漏?
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
学习函数式编程,必须掌握很多术语,否则根本看不懂文档。
本文要回答一个很重要的问题:函数式编程有什么用?
学习函数式编程的过程中,我接触到了 Ramda.js。Node快闪:npm模块版本策略 - 知乎专栏
{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"title":"Node快闪:npm模块版本策略","author":"shaopeng-38","content":"我们继续之前的,以及的教程,来讲解一下npm模块的版本策略。npm采用semver标准(),简单总结如下:采用semver的软件必须声明可以调用的APIversion以x.y.z为格式,x, y, z为非负整数。x称作major,y称作minor,z称作patch。一旦一个version的代码被发布以后,代码的内容绝对不能改变,任何修改必须以新的version发布major为0的的版本用作初始开发用,任何逻辑,功能在这段时间都可以改变,API这时不是稳定的version 1.0.0 为正式API,1.0.0以后version的变动根据API需求而变patch version为bug fix,bug fix应该是内部的逻辑变化,顾名思义修复bugminor version也被称为feature version,指的是API新添加了一个与现有API兼容的功能。major version也被称为breaking version,指的是API做出了不兼容的改变。pre-release版本是通过在x.y.z后面追加“-[0-9A-Za-z]”的字符串,比如1.0.0-alpha, 1.0.0-beta.1等更多细节请参考。之前我们在中运行npm init的时候,细心的读者会发现如果我们接受默认选项,在生成的package.json中的version是1.0.0,而我们在弹出的version项中输入了0.0.1。原因就在于我们一开始开发模块的时候,往往处于初始/试验阶段,等到API成熟以后再发布1.0版本。我们发布的模块version还处于0.0.1,严格来讲其实是不正确的,因为这时候我们的模块已经有了一个feature。所以我们现在来发布第一个minor version。在这里我们可以用npm自带的命令:npm version。这个命令会根据用户输入,自动更新package.json中的version项,添加一个git commit,并且添加一个git tag。你接下来需要做的只不过是用git push将生成的commit和tag同步到git,再用npm publish将更新的版本发布到npm上。写这篇文章的时候,我们已经对代码进行了三次改变:1.
- 添加了最基础的addFn的API - minor version2.
- 添加了现有API的测试,并修复了一个所谓的“bug”:对于非数字的输入,我们返回NaN - patch version3.
- 添加了更多对现有API的测试,并改变了API的行为:对于addFn(1)('a'),输出null,对于addFn('non-number'),抛出Error - 而且我们认为这时候API应该稳定了 - major version注意在以上第3步,理论上,我们的API做出了不兼容的改变,所以应该是一个major version,不过由于我们处于0.y.z的试验阶段,如果我们觉得API还没有稳定,可以进行一个minor version的发布。现在我们分别进行以上minor,patch,major的发布。1. checkout (1)中的代码,运行npm version,并进行同步和发布。$ git checkout npm-module\n$ npm version minor -m \"initial addFn functionality\"\n$ npm publish\n$ git push origin v0.1.0\n现在你可以验证自己发布的npm模块,运行现在你可以验证自己发布的npm模块,运行npm view &your-module-name&@0.1.0\n你也可以查看查看自己的github的repo,查看自己刚同步的tag你也可以查看查看自己的github的repo,查看自己刚同步的tag可以看到,“initial addFn functionality”就是我们之前运行npm version的-m选项。可以看到,“initial addFn functionality”就是我们之前运行npm version的-m选项。2. checkout(2)中的代码,进行上文中的patch version$ git checkout mocha-basics\n$ npm version 0.1.1 -m \"%s add mocha tests, non-number input returns NaN\"\n$ npm publish\n$ git push origin v0.1.1\n注意上面我们直接输入了0.1.1,这是因为我们在追加version,在我们git checkout mocha-basics的时候,package.json里面的version被重置为0.0.1,这时如果我们运行npm version patch,就会被错误的更新成0.0.2。所以这里一个实战经验是,尽量在master branch里面进行所有npm version的升级。3. (3)中的代码其实是我们的master branch的最近的一点,所以,这里我们直接在master上面进行version update。$ git checkout master\n$ npm version 1.0.0 -m \"%s addFn(non-number) throws Error, stable API\"\n$ npm publish\n$ git push origin master v1.0.0\n注意上面在npm version 的 -m 选项中我们放入了一个%s的占位符,这个占位符其实会被最后生成的semver值代替,出现在git tag的message中。最后我们成功地生成了三个git tag。注意所有生成的tag都是semver值前面加了一个v,这是npm version的默认行为,是完全没有问题的。现在你已经掌握了基本的npm 模块版本策略和管理。我们总结一下本篇教程用到的基本知识:# semver以x.y.z为格式,x为major,y为minor,z为patch\n\n# npm version [major|minor|patch|semver] -m \"%s message\"\n\n# npm version会生成计算好的semver,生成相应的commit和tag,\n# 我们需要运行git push和npm publish同步到git和npm\n\n# 在不同的branch上追加version时,会出现version非线性的状况,\n# 可以直接输入计算好的semver,\n# 当然,实战策略是在master branch得到更新后,尽快进行版本升级\n","updated":"T19:09:54.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"collapsedCount":0,"likeCount":0,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"/v2-0f9cd7f0449_r.png","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"Node.js"},{"url":"/topic/","id":"","name":"npm"},{"url":"/topic/","id":"","name":"前端开发"}],"adminClosedComment":false,"titleImageSize":{"width":1366,"height":728},"href":"/api/posts/","excerptTitle":"","tipjarState":"inactivated","annotationAction":[],"sourceUrl":"","pageCommentsCount":0,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T03:09:54+08:00","url":"/p/","lastestLikers":[],"summary":"我们继续之前的,以及的教程,来讲解一下npm模块的版本策略。npm采用semver标准(),简单总结如下:采用semver的软件必须声明可以调用的API version以x.y.z为格式,x, y, …","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"annotationDetail":null,"commentsCount":0,"likesCount":0,"FULLINFO":true}},"User":{"shaopeng-38":{"isFollowed":false,"name":"Shaopeng","headline":"","avatarUrl":"/v2-49c9fbbdafad_s.jpg","isFollowing":false,"type":"people","slug":"shaopeng-38","bio":"小伙子学什么的?计算机。哟,学电脑的!","hash":"fbfa754ae","uid":92,"isOrg":false,"description":"","profileUrl":"/people/shaopeng-38","avatar":{"id":"v2-49c9fbbdafad","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{}}关注社区微信公众号: PMvideo
你需要了解的 Node.js 模块
Node 使用两个核心模块来管理模块依赖:
require&模块,在全局范围可用——无需 require(‘require’)。
module 模块,在全局范围可用——无需 require(‘module’)。
你可以将 require 模块视为命令,将 module 模块视为所有必需模块的组织者。
在 Node 中获取一个模块并不复杂。
const&config&=&require('/path/to/file');
由 require 模块导出的主要对象是一个函数(如上例所用)。 当 Node 使用本地文件路径作为函数的唯一参数调用该 require() 函数时,Node 将执行以下步骤:
解析 :找到文件的绝对路径。
加载 :确定文件内容的类型.
封装 :给文件其私有作用域。 这使得 require 和 module 对象两者都可以下载我们需要的每个文件。
评估 :这是 VM 对加载的代码最后需要做的。
缓存 :当我们再次需要这个文件时,不再重复所有的步骤。
在本文中,我将尝试用示例解释这些不同的阶段,以及它们是如何影响我们在 Node 中编写模块的方式的。
先在终端创建一个目录来保存所有示例:
mkdir&~/learn-node&&&&cd&~/learn-node
本文之后所有命令都在 ~/learn-node 下运行。
解析本地路径
我现在向你介绍 module 对象。你可以在一个的 REPL(译者注:Read-Eval-Print-Loop,就是一般控制台干的事情)会话中很容易地看到它:
~/learn-node&$&node
&&exports:&{},
&&parent:&undefined,
&&filename:&null,
&&loaded:&false,
&&children:&[],
&&paths:&[&...&]&}
每个模块对象都有一个 id 属性作为标识。这个 id 通常是文件的完整路径,不过在 REPL 会话中,它只是 。
Node 模块与文件系统有着一对一的关系。请求模块就是把文件内容加载到内存中。
不过,因为 Node 中有很多方法用于请求文件(比如,使用相对路径,或预定义的路径),在我们把文件内容加载到内存之前,我们需要找到文件的绝对位置。
现在请求 ‘find-me’ 模块,但不指定路径:
require('find-me');
Node 会按顺序在 module.paths 指定的路径中去寻找 find-me.js。
~/learn-node&$&node
&&module.paths
[&'/Users/samer/learn-node/repl/node_modules',
&&'/Users/samer/learn-node/node_modules',
&&'/Users/samer/node_modules',
&&'/Users/node_modules',
&&'/node_modules',
&&'/Users/samer/.node_modules',
&&'/Users/samer/.node_libraries',
&&'/usr/local/Cellar/node/7.7.1/lib/node'&]
路径列表基本上会是从当前目录到根目录下的每一个 node_modules 目录。它也会包含一些不推荐使用的遗留目录。
如果 Node 在这些目录下仍然找不到 find-me.js,它会抛出&“cannot find module error.(不能找到模块)” 这个错误消息。
~/learn-node&$&node
&&require('find-me')
Error:&Cannot&find&module&'find-me'
&&&&at&Function.Module._resolveFilename&(module.js:470:15)
&&&&at&Function.Module._load&(module.js:418:25)
&&&&at&Module.require&(module.js:498:17)
&&&&at&require&(internal/module.js:20:19)
&&&&at&repl:1:1
&&&&at&ContextifyScript.Script.runInThisContext&(vm.js:23:33)
&&&&at&REPLServer.defaultEval&(repl.js:336:29)
&&&&at&bound&(domain.js:280:14)
&&&&at&REPLServer.runBound&[as&eval]&(domain.js:293:12)
&&&&at&REPLServer.onLine&(repl.js:533:10)
现在创建一个局部的 node_modules 目录,放入一个 find-me.js,require(‘find-me’) 就能找到它。
~/learn-node&$&mkdir&node_modules&
~/learn-node&$&echo&"console.log('I&am&not&lost');"&&&node_modules/find-me.js
~/learn-node&$&node
&&require('find-me');
I&am&not&lost
如果别的路径下存在另一个&find-me.js 文件,例如在 home 目录下存在 node_modules 目录,其中有一个不同的 find-me.js:
$&mkdir&~/node_modules
$&echo&"console.log('I&am&the&root&of&all&problems');"&&&~/node_modules/find-me.js
现在 learn-node 目录也包含 node_modules/find-me.js&—— 在这个目录下&require(‘find-me’),那么 home 目录下的 find-me.js 根本不会被加载:
~/learn-node&$&node
&&require('find-me')
I&am&not&lost
如果删除了~/learn-node 目录下的的 node_modules 目录,再次尝试请求 find-me.js,就会使用 home 目录下 node_modules 目录中的 find-me.js 了:
~/learn-node&$&rm&-r&node_modules/
~/learn-node&$&node
&&require('find-me')
I&am&the&root&of&all&problems
请求一个目录
模块不一定是文件。我们也可以在 node_modules 目录下创建一个 find-me 目录,并在其中放一个 index.js 文件。同样的 require(‘find-me’) 会使用这个目录下的 index.js 文件:
~/learn-node&$&mkdir&-p&node_modules/find-me
~/learn-node&$&echo&"console.log('Found&again.');"&&&node_modules/find-me/index.js
~/learn-node&$&node
&&require('find-me');
Found&again.
注意如果存在局部模块,home 下 node_modules 路径中的相应模块仍然会被忽略。
在请求一个目录的时候,默认会使用 index.js,不过我们可以通过 package.json 中的 main 选项来改变起始文件。比如,希望 require(‘find-me’) 在 find-me 目录下去使用另一个文件,只需要在那个目录下添加&&package.json 文件来完成这个事情:
~/learn-node&$&echo&"console.log('I&rule');"&&&node_modules/find-me/start.js
~/learn-node&$&echo&'{&"name":&"find-me-folder",&"main":&"start.js"&}'&&&node_modules/find-me/package.json
~/learn-node&$&node
&&require('find-me');
require.resolve
如果你只是想找到模块,并不想执行它,你可以使用 require.resolve 函数。除了不加载文件,它的行为与主函数 require 完全相同。如果文件不存在它会抛出错误,如果找到了指定的文件,它会返回完整路径。
&&require.resolve('find-me');
'/Users/samer/learn-node/node_modules/find-me/start.js'
&&require.resolve('not-there');
Error:&Cannot&find&module&'not-there'
&&&&at&Function.Module._resolveFilename&(module.js:470:15)
&&&&at&Function.resolve&(internal/module.js:27:19)
&&&&at&repl:1:9
&&&&at&ContextifyScript.Script.runInThisContext&(vm.js:23:33)
&&&&at&REPLServer.defaultEval&(repl.js:336:29)
&&&&at&bound&(domain.js:280:14)
&&&&at&REPLServer.runBound&[as&eval]&(domain.js:293:12)
&&&&at&REPLServer.onLine&(repl.js:533:10)
&&&&at&emitOne&(events.js:101:20)
&&&&at&REPLServer.emit&(events.js:191:7)
这很有用,比如,检查一个可选的包是否安装并在它已安装的情况下使用它。
相对路径和绝对路径
除了在 node_modules 目录中查找模块之外,我们也可以把模块放置于任何位置,然后通过相对路径(./ 和 ../)请求,也可以通过以 / 开始的绝对路径请求。
比如,如果 find-me.js 是放在 lib 目录而不是 node_modules 目录下,可以这样请求:
require('./lib/find-me');
文件中的父子关系
创建 lib/util.js 文件并添加一行 console.log 代码来识别它。console.log 会输出模块自身的 module 对象:
~/learn-node&$&mkdir&lib
~/learn-node&$&echo&"console.log('In&util',&module);"&&&lib/util.js
在 index.js 文件中干同样的事情,稍后我们会通过 node 命令执行这个文件。让 index.js 文件请求 lib/util.js:
~/learn-node&$&echo&"console.log('In&index',&module);&require('./lib/util');"&&&index.js
现在用 node 执行 index.js:
~/learn-node&$&node&index.js
In&index&Module&{
&&id:&'.',
&&exports:&{},
&&parent:&null,
&&filename:&'/Users/samer/learn-node/index.js',
&&loaded:&false,
&&children:&[],
&&paths:&[&...&]&}
In&util&Module&{
&&id:&'/Users/samer/learn-node/lib/util.js',
&&exports:&{},
&&&Module&{
&&&&&id:&'.',
&&&&&exports:&{},
&&&&&parent:&null,
&&&&&filename:&'/Users/samer/learn-node/index.js',
&&&&&loaded:&false,
&&&&&children:&[&[Circular]&],
&&&&&paths:&[...]&},
&&filename:&'/Users/samer/learn-node/lib/util.js',
&&loaded:&false,
&&children:&[],
&&paths:&[...]&}
注意到现在的列表中主模块 index (id: ‘.’) 是 lib/util 模块的父模块。不过 lib/util 模块并未作为 index 的子模块列出来。不过那里有个 [Circular] 值因为那里存在循环引用。如果 Node 打印 lib/util 模块对象,它就会陷入一个无限循环。因此这里用 [Circular] 代替了 lib/util 引用。
现在更重要的问题是,如果 lib/util 模块又请求了 index 模块,会发生什么事情?这就是我们需要了解的循环依赖,Node 允许这种情况存在。
在理解它之前,我们先来搞明白 module 对象中的另外一些概念。
exports、module.exports 以及同步加载模块
exports 是每个模块都有的一个特殊对象。如果你观察仔细,会发现上面示例中每次打印的模块对象中都存在一个 exports 属性,到目前为止它只是个空对象。我们可以给这个特殊的 exports 对象任意添加属性。例如,我们为 index.js 和 lib/util.js 导出 id 属性:
//&Add&the&following&line&at&the&top&of&lib/util.js
exports.id&=&'lib/util';
//&Add&the&following&line&at&the&top&of&index.js
exports.id&=&'index';
现在执行 index.js,我们会看到这些属性受到 module 对象管理:
~/learn-node&$&node&index.js
In&index&Module&{
&&id:&'.',
&&exports:&{&id:&'index'&},
&&loaded:&false,
In&util&Module&{
&&id:&'/Users/samer/learn-node/lib/util.js',
&&exports:&{&id:&'lib/util'&},
&&&Module&{
&&&&&id:&'.',
&&&&&exports:&{&id:&'index'&},
&&&&&loaded:&false,
&&&&&...&},
&&loaded:&false,
上面的输出中我去掉了一些属性,这样看起来比较简洁,不过请注意 exports 对象已经包含了我们在每个模块中定义的属性。你可以在 exports 对象中任意添加属性,也可以直接把 exports 整个替换成另一个对象。比如,可以把 exports 对象变成一个函数,我们会这样做:
//&Add&the&following&line&in&index.js&before&the&console.log&
module.exports&=&function()&{};
现在运行 index.js,你会看到 exports 对象是一个函数:
~/learn-node&$&node&index.js
In&index&Module&{
&&id:&'.',
&&exports:&[Function],
&&loaded:&false,
注意,我没有通过 exports = function() {} 来将 exports 对象改变为函数。这样做是不行的,因为模块中的 exports 变量只是 module.exports 的引用,它用于管理导出属性。如果我们重新给 exports 变量赋值,就会丢失对 module.exports 的引用,实际会产生一个新的变量,而不是改变了 module.exports。
每个模块中的&module.exports 对象就是通过 require 函数请求那个模块返回的。比如,把 index.js 中的 require(‘./lib/util’) 改为:
const&UTIL&=&require('./lib/util');
console.log('UTIL:',&UTIL);
这段代码会输出 lib/util 导出到 UTIL 常量中的属性。现在运行 index.js,输出如下:
UTIL:&{&id:&'lib/util'&}
再来谈谈每个模块的 loaded 属性。到目前为止,每次我们打印一个模块对象的时候,都会看到这个对象的&loaded 属性值为 false。
module 模块使用 loaded 属性来跟踪哪些模块是加载过的(true值),以及哪些模块还在加载中(false 值)。比如我们可以通过调用 setImmediate 来打印 modules 对象,在下一事件循环中看看完成加载的&index.js 模块:
//&In&index.js
setImmediate(()&=&&{
&&console.log('The&index.js&module&object&is&now&loaded!',&module)
输出是这样的:
The&index.js&module&object&is&now&loaded!&Module&{
&&id:&'.',
&&exports:&[Function],
&&parent:&null,
&&filename:&'/Users/samer/learn-node/index.js',
&&loaded:&true,
&&children:
&&&[&Module&{
&&&&&&&id:&'/Users/samer/learn-node/lib/util.js',
&&&&&&&exports:&[Object],
&&&&&&&parent:&[Circular],
&&&&&&&filename:&'/Users/samer/learn-node/lib/util.js',
&&&&&&&loaded:&true,
&&&&&&&children:&[],
&&&&&&&paths:&[Object]&}&],
&&&[&'/Users/samer/learn-node/node_modules',
&&&&&'/Users/samer/node_modules',
&&&&&'/Users/node_modules',
&&&&&'/node_modules'&]&}
注意理解它是如何推迟 console.log,使其在 lib/util.js 和 index.js 加载完成之后再产生输出的。
Node 完成加载模块(并标记)之后&exports 对象就完成了。整个请求/加载某个模块的过程是 同步 的。因此我们可以在一个事件循环周期过后看到模块已经完成加载。
这也就是说,我们不能异步改变 exports 对象。比如在某个模块中干这样的事情:
fs.readFile('/etc/passwd',&(err,&data)&=&&{
&&if&(err)&throw&
&&exports.data&=&&//&Will&not&work.
循环依赖模块
现在来回答关于 Node 循环依赖模块这个重要的问题:如果模块1需要模块2,模块2也需要模块1,会发生什么事情?
为了观察结果,我们在 lib/ 下创建两个文件,module1.js 和 module2.js,它们相互请求对象:
//&lib/module1.js
exports.a&=&1;
require('./module2');
exports.b&=&2;
exports.c&=&3;
//&lib/module2.js
const&Module1&=&require('./module1');
console.log('Module1&is&partially&loaded&here',&Module1);
运行 module1.js 可以看到:
~/learn-node&$&node&lib/module1.js
Module1&is&partially&loaded&here&{&a:&1&}
我们在 module1 完全加载前请求了 module2,而 module2 在未完全加载时又请求了 module1,那么,在那一时刻,能得到的是在循环依赖之前导出的属性。只有 a 属性打印出来了,因为 b 和 c 是在请求了module2 并打印了 module1 之后才导出的。
Node 让这件事变得简单。在加载某个模块的时候,它会创建 exports 对象。你可以在一个模块加载完成之前请求它,但只会得到部分导出的对象,它只包含到目前为止已经定义的项。
JSON 和 C/C++ addon
我们可以利用 require 函数在本地引入 JSON 文件和 C++ addon 文件。这么做不需要指定文件扩展名。
如果没有指定文件扩展名,Node 首先要处理 .js 文件。如果找不到 .js 文件,就会尝试寻找 .json 文件,如果发现为 JSON 文本文件,便将其解析为 .json 文件。 之后,它将尝试找到一个二进制 .node 文件。为了消除歧义,当需要使用 .js 文件以外的其他格式后缀时,你需要制定一个文件扩展名。
引入 JSON 文件在某些情况下是很有用的,例如,当你在该文件中需要管理的所有内容都是些静态配置值时,或者你需要定期从某个外部源读入值时。假设我们有以下 config.json 文件:
&&"host":&"localhost",
&&"port":&8080
我们可以像这样直接请求:
const&{&host,&port&}&=&require('./config');
console.log(`Server&will&run&at&http://${host}:${port}`);
运行上面的代码,输出如下:
Server&will&run&at&http://localhost:8080
如果 Node 不能找到 .js 或 .json 文件,它会寻找 .node 文件,它会被认为是编译好的插件模块。
Node 文档中有一个 插件文件示例 ,它是用 C++ 写的。它只是一个导出了 hello() 函数的简单模块,这个 hello 函数输出 “world”。
你可以使用 node-gyp 包来编译和构建 .cc 文件,生成 .addon 文件。只需要配置一个 binding.gyp 文件来告诉 node-gyp 做什么。
得到 addon.node (或其它在 binding.gyp 中指定的名称)文件后,你可以像请求其它模块一样请求它:
const&addon&=&require('./addon');
console.log(addon.hello());
我们可以在 require.extensions 中看到实际支持的三个扩展名:
看看每个扩展名对应的函数,你就清楚 Node 在怎么使用它们。它使用 module._compile 处理 .js 文件,使用 JSON.parse 处理 .json 文件,以及使用 process.dlopen 处理 .node 文件。
在 Node 编写的所有代码将封装到函数中
有人经常误解 Node 的封装模块的用途。让我们通过 exports/module.exports 之间的关系来了解它。
我们可以使用 exports 对象导出属性,但是我们不能直接替换&exports&对象,因为它仅是对 module.exports 的引用
exports.id&=&42;&//&This&is&ok.
exports&=&{&id:&42&};&//&This&will&not&work.
module.exports&=&{&id:&42&};&//&This&is&ok.
对于每个模块而言这个 exports 对象看似是全局的,这和将其定义为 module 对象的引用,那到底什么是 exports 对象呢?
在解释 Node 的封装过程之前,让我再问一个问题。
在浏览器中,当我们在脚本中如下所示地声明一个变量:
var&answer&=&42;
在定义 answer 变量的脚本之后,该变量将在所有脚本中全局可见。
这在 Node 中根本不是问题。我们在某个模块中定义的变量,其它模块是访问不到的。那么为什么 Node 中变量的作用域这么神奇?
答案很简单。在编译模块之前,Node 会把模块代码封装在一个函数中,我们可以通过 module 模块的 wrapper 属性看出来。
&&require('module').wrapper
[&'(function&(exports,&require,&module,&__filename,&__dirname)&{&',
&&'\n});'&]
Node 不会直接执行你写在文件中的代码。它执行这个包装函数,你写的代码只是它的函数体。因此所有定义在模块中的顶层变量都受限于模块的作用域。
这个包装函数有5个参数:exports, require, module, __filename 和 __dirname。它们看起来像是全局的,但实际它们在每个模块内部。
所有这些参数都会在 Node 执行包装函数的时候获得值。exports 是 module.exports 的引用。require 和 module 都有特定的功能。__filename/__dirname 变量包含了模块文件名及其所有目录的绝对路径。
如果你的脚本在第一行出现错误,你就会看到它是如何包装的:
~/learn-node&$&echo&"euaohseu"&&&bad.js
~/learn-node&$&node&bad.js
~/bad.js:1
(function&(exports,&require,&module,&__filename,&__dirname)&{&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&^
ReferenceError:&euaohseu&is&not&defined
注意上例中的第一行并非是真的错误引用,而是为了在错误报告中输出包装函数。
此外,既然每个模块都封装在函数中,我们可以通过 arguments 关键字来使用函数的参数:
~/learn-node&$&echo&"console.log(arguments)"&&&index.js
~/learn-node&$&node&index.js
{&'0':&{},
&&&{&[Function:&require]
&&&&&resolve:&[Function:&resolve],
&&&&&main:
&&&&&&Module&{
&&&&&&&&id:&'.',
&&&&&&&&exports:&{},
&&&&&&&&parent:&null,
&&&&&&&&filename:&'/Users/samer/index.js',
&&&&&&&&loaded:&false,
&&&&&&&&children:&[],
&&&&&&&&paths:&[Object]&},
&&&&&extensions:&{&...&},
&&&&&cache:&{&'/Users/samer/index.js':&[Object]&}&},
&&&Module&{
&&&&&id:&'.',
&&&&&exports:&{},
&&&&&parent:&null,
&&&&&filename:&'/Users/samer/index.js',
&&&&&loaded:&false,
&&&&&children:&[],
&&&&&paths:&[&...&]&},
&&'3':&'/Users/samer/index.js',
&&'4':&'/Users/samer'&}
第一个参数是 exports 对象,它一开始是空的。然后是 require/module 对象,它们与在执行的&index.js 文件的实例关联,并非全局变量。最后 2 个参数是文件的路径及其所在目录的路径。
包装函数的返回值是 module.exports。在包装函数的内部我们可以通过改变 module.exports 属性来改变 exports 对象,但不能直接对&exports 赋值,因为它只是一个引用。
这个事情大致像这样:
function&(require,&module,&__filename,&__dirname)&{
&&let&exports&=&module.&&//&Your&Code...
&&return&module.
如果我们直接改变 exports 对象,它就不再是 module.exports 的引用。JavaScript 在任何地方都是这样引用对象,并非只是在这个环境中。
require 对象
require 没什么特别,它主要是作为一个函数来使用,接受模块名称或路径作为参数,返回 module.exports 对象。如果我们想改变 require 对象的逻辑,也很容易。
比如,为了进行测试,我们想让每个 require 调用都被模拟为返回一个假对象来代替模块导出的对象。这个简单的调整就像这样:
require&=&function()&{
&&return&{&mocked:&true&};
在上面重新对 require 赋值之后,调用 require(‘something’) 就会返回模拟的对象。
require 对象也有自己的属性。我们已经看到了 resolve 属性,它也是一个函数,是 require 处理过程中解析路径的步骤。上面我们还看到了 require.extensions。
还有一个 require.main 可用于检查代码是通过请求来运行的还是直接运行的。
再来看个例子,定义在 print-in-frame.js 中的 printInFrame 函数:
//&In&print-in-frame.js
const&printInFrame&=&(size,&header)&=&&{
&&console.log('*'.repeat(size));
&&console.log(header);
&&console.log('*'.repeat(size));
这个函数需要一个数值型的参数 size 和一个字符串型的参数 header,它会在打印一个由指定数量的星号生成的框架,并在其中打印 header。
我们希望通过两种方式来使用这个文件:
从命令行直接运行:
~/learn-node&$&node&print-in-frame&8&Hello
在命令行传入 8 和 Hello 作为参数,它会打印出由 8 个星号组成的框架中的 “Hello”。
2. 通过 require 来使用。假设所需要的模块会导出 printInFrame 函数,然后就可以这样做:
const&print&=&require('./print-in-frame');
print(5,&'Hey');
它在由 5 个星号组成的框架中打印&“Hey”。
这是两种不同的使用方式。我们得想办法检测文件是独立运行的还是由其它脚本请求的。
这里用一个简单的 if 语句来解决:
if&(require.main&===&module)&{
&&//&The&file&is&being&executed&directly&(not&with&require)
我们可以使用这个条件,以不同的方式调用 printInFrame 来满足需求:
//&In&print-in-frame.js
const&printInFrame&=&(size,&header)&=&&{
&&console.log('*'.repeat(size));
&&console.log(header);
&&console.log('*'.repeat(size));
if&(require.main&===&module)&{
&&printInFrame(process.argv[2],&process.argv[3]);
&&module.exports&=&printInF
如果文件不是被请求的,我们使用 process.argv 来调用 printInFrame。否则,我们将 module.exports 修改为 printInFrame 引用。
所有模块都会被缓存
理解缓存很重要。我们用一个简单的示例来说明缓存。
假设有一个 ascii-art.js,可以打印炫酷的标头:
我们想每次 请 求 这个文件的时候都能看到这些标头,那么如果我们请求这个文件两次,期望会看到两次标头输出。
require('./ascii-art')&//&会显示标头。
require('./ascii-art')&//&不会显示标头。
因为模块缓存,第二次请求不会显示标头。Node 会在第一次调用的时候缓存文件,所以第二次调用的时候就不会重新加载了。
我们可以在第一次请求之后通过打印 require.cache 来看缓存的内容。缓存注册表只是一个简单的对象,它的每个属性对应着每次请求的模块。那些属性值是每个模块中的 module 对象。只需要从 require.cache 里删除某个属性就可以使对应的缓存失效。如果这样做,Node 会再次加载模块并再加将它加入缓存。
不过在现在这个情况下,这样做并不是一个高效的解决办法。简单的办法是在&ascii-art.js 中把输出语句包装为一个函数,然后导出它。用这个办法,我们请求 ascii-art.js 文件的时候会得到一个函数,然后每次执行这个函数都可以看到输出:
require('./ascii-art')()&//&会显示标头。
require('./ascii-art')()&//&也会显示标头。
以上,就是我这次要说的内容!
来自:/90926/
笔记社区是一个面向中高端IT开发者、程序员的知识共享社区,通过网络抓取与文章分类总结,由专家为用户提供高质量的专题文章系列。
原文链接:http://www./lib/view/open5.html
声明:所有文章资源均从网络抓取,如果侵犯到您的著作权,请联系删除文章。联系方式请关注微信公众号PMvideo【锤子视频-程序员喜欢的短视频】,笔记社区开发者交流群 。
关注微信公众号:PMvideo}

我要回帖

更多关于 nodejs 如何安装模块 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信