Node.js on Windows
搭建 Node.js 开发环境
NVM安装
1. Download nvm-noinstall.zip
2. Update the system environment variables:
NVM_HOME, NVM_SYMLINK (C:\Users\Program Files\nodejs This directory should not exist in previously.)
3. Create settings.txt file
root: C:\Users\qinayan\bin\nvm
path: C:\Program Files\nodejs
arch: 64
proxy: none
详情请参考 如何安装nvm-windows
另外别忘了在NVM_HOME目录中运行install命令
node.js安装
1 | 安装特定版本的nodejs |
模块
每个文件就是一个模块,文件的路径名就是模块的名字
require
类似于Java中的import
关键字,导入不同的包。
1 | var express = require('express'); |
exports
导出模块的公有方法和属性。可以理解为Java中的public
方法和属性。
1 | // util.js |
module
包含当前模块的一些信息,常用的做法是替换当前模块的导出对象。
1 | // util.js |
module initialize发生的时机
模块中的代码只会在首次被使用的时候才会执行一次,同时初始化该模块的导出对象,之后导出对象会被缓存到内存当中,供任意使用。
小结
- NVM是Node Version Manager,管理node的版本的工具。使用NVM,可以保证同一个操作系统下,多个不同版本的node得以共存。
- node作为javascript的解析器,可以在终端下进入交互式模式(repl read-eval-print-loop),很方便快速地反馈我们程序的结果。
- nodeJS的模块系统实现了CMD标准,即CommonJS Module Definition标准;而对于运行在浏览器上的javascript的模块化,因为需要异步加载js文件,所以由require.js实现了AMD (Asynchronous Module Definition)标准
1 | CMD |
问题
- 是否可以使用
require('./data.json'
)将json文件引入到我们的程序当中呢? - 有两个js文件同时引入了
data.json
,先执行a.js,后执行b.js。下面的程序会输出什么?1
2
3
4
5
6
7
8
9
10//data.json
{"hello": "world"}
//a.js
var data = require('./data.json');
data = {};
//b.js
var data = require('./data.json');
console.log(data)
代码组织
模块解析路径
node_modules
不想直接require文件路径名,因为这样一旦所依赖的文件路径发生变化,牵扯的文件会很多。所以我们需要一个约定的根目录。这个根目录就是node_modules
以这个文件的路径为例:C:\\Users\\qianyan\\Projects\\lesson2\\util.js
,node搜索的路径如下。
1 | paths: |
NODE_PATH
我们知道java中依赖包的搜索路径是通过classpath这个JVM的参数控制的。其实node也有这样的变量提供支持。这个变量就是NODE_PATH
windows下
1 | cmd |
NODE_PATH中的路径被遍历是发生在从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的node_modules,如果还没有查找到指定模块的话,就会去 NODE_PATH中注册的路径中查找。
内置模块
如fs
, http
等,不做路径解析就直接使用其导出对象require('fs')
, require('http')
Package(包)
包就是封装多个子模块,同时提供入口的大模块。这个大模块的功能是内聚的。举个例子:
1 | C:\USERS\QIANYAN\PROJECTS\LESSON2 |
其中plane目录定义了一个包,其中包含了4个子模块。main.js
作为入口模块,如下:
1 | var engine = require('./engine'); |
其他模块需要使用plane这个包时,得使用require('./plane/main')
才行。不过,这里有两种方法可以省去写文件名main
。
1. index.js
这里有个约定,如果将main.js重命名成index.js,那么就不需要写出文件名字,直接require('./plane')
就可以了。
这样模块显得更内聚,和Clojure中的(use namespace)
的用法类似。以下两条语句等价。
1 | require('./plane/main') |
2. package.json
如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。上例中的模块可以重构如下。
1 | C:\USERS\QIANYAN\PROJECTS\LESSON2\PLANE |
其中package.json内容如下。
1 | { |
如此一来,就同样可以使用require(‘./lib/plane’)的方式加载模块。NodeJS会根据包目录下的package.json找到入口模块所在位置。
命令行程序
node.js的程序是跑在命令行之中的,命令行程序长得类似cmd --name=value
这样的形式。
创建目录
在windows下创建一个greeting
程序的目录,如下:
1 | C:\USERS\QIANYAN\PROJECTS\LESSON2\GREETING |
package.json的内容如下:
1 | { |
在windows下,如果我们想要实现cmd –name=value的效果,就必须使用cmd后缀的文件,如下:
1 | //greeting.cmd |
我们实现一个接受人的名字作为参数的命令行程序lib/index.js,如下:
1 | console.log('hello,', process.argv[2]); |
到这里,可以直接这样运行./bin/greeting.cmd lambeta
,输出hello, lambeta
。不过,还是没有预期的样子。
npm link
我们再运行一条npm的命令
1 | > npm link |
这条命令帮助我们设置两个软链接。第一个链接使得我们可以直接运行greeting lambeta;第二个则在全局范围内,其他的模块得以引入greeting这个包。
此时,我们可以直接使用greeting lambeta来运行程序了
依赖第三方库
为了实现真正的cmd –name=value,我们使用一个第三方库yargs
。
- 安装yargs: npm install yargs –save
- 修改index.js文件如下
1
2var argv = require('yargs').argv;
console.log('hello,', argv.name); - 运行
1
2> greeting --name=lambeta
hello, lambeta
最后再来看看一个完整的node.js的整体结构
1 | C:\USERS\QIANYAN\PROJECTS\LESSON2\GREETING |
小结
- 按照标准目录结构
- 分模块管理项目
- 使用NPM管理第三方模块和命令行程序
- 使用package.json描述项目信息和依赖
问题
- 下载一个第三方命令行程序到本地
npm install es-checker
,不要使用-g
参数,如何运行起来这个程序? - 了解一下npm scripts,在上题的基础上,添加包含下面的内容的package.json,运行
npm test
。思考这样做是否可行?1
2
3
4
5
6//package.json
{
"scripts": {
"test": "es-checker"
}
}
文件操作
前置条件:安装Windows上的离线文档工具
文件操作相关的API
buffer对象(数据块)
Javascript语言本身只支持字符串操作,没有提供针对二进制数据流的操作。NodeJS提供了一个与String
对等的全局对象Buffer
. Buffer和整数的数组很类似,但是它是固定长度,一旦创建就不能修改。
1 | var bin = new Buffer('hello', 'utf8');// <Buffer 68 65 6c 6c 6f> |
stream模块(数据流)
Stream是一个抽象的接口,所有的stream都是EventEmitter的实例。
当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。
1 | var fs = require('fs'); |
fs模块
NodeJS通过fs
内置模块提供对文件的操作。fs
模块提供的API基本可以分为以下三类:
文件属性读写。
其中常用的有fs.stat、fs.chmod、fs.chown等等。文件内容读写。
其中常用的有fs.readFile、fs.readdir、fs.writeFile、fs.mkdir等等。底层文件操作。
其中常用的有fs.open、fs.read、fs.write、fs.close等等。
我们可以通过require('fs')
来引用这个模块,而且该模块下的每个方法都有同步和异步的形式。
1 | // read sync |
一段遍历当前目录的程序
1 | var fs = require('fs'); |
path模块
和java
类似,NodeJS提供path来简化对文件路径的操作。
- path.normalize
1
2var path = require('path');
path.normalize('foo/bar/..'); // 'foo' - path.join & path.sep
1
2path.join('foo', '/bar/', '/baz', 'par/') // 'foo\\bar\\baz\\par\\'
path.sep // '\\' - path.extname
1
path.extname('node.js') //'.js'
小结
Buffer
提供了NodeJS操作二进制的机制;Stream
是一个抽象的接口,每种stream都是EventEmitter的实例。当我们在读取大文件时,可以使用数据流一边读取,一边处理;fs
提供了文件属性读写,内容读写以及底层文件操作。- 不要使用字符串拼接,使用
path
简化操作
问题
- 使用
fs
的API创建一个copy的函数; - NodeJS对文本编码的处理;
- 使用第三方包
findit
重写遍历当前目录的程序。
网络操作
简单的HTTP服务器
使用http
实现一个简单的HTTP服务器。
1 | var http = require('http'); |
http模块
http模块提供两种使用方式:
- 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应;
- 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。
先创建一个HTTP服务器
1 | //http-server.js |
再创建一个HTTP客户端
1 | //http-client.js |
url模块
- parse
使用url解析成URL对象1
2
3
4
5
6
7
8
9
10
11
12
13
14> url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash
Url {
protocol: 'http:',
slashes: true,
auth: 'user:pass',
host: 'host.com:8080',
port: '8080',
hostname: 'host.com',
hash: '#hash',
search: '?query=string',
query: 'query=string',
pathname: '/p/a/t/h',
path: '/p/a/t/h?query=string',
href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' } - format
format方法允许将一个URL对象转换为URL字符串1
2
3
4
5
6
7> url.format({
protocol: 'http:',
host: 'www.example.com',
pathname: '/p/a/t/h',
search: 'query=string'
});
//'http://www.example.com/p/a/t/h?query=string' - resolve
resolve方法拼接两个URL1
2
3
4> url.resolve('http://www.baid.com/path', '../www.google.com')
'http://www.baid.com/www.google.com'
> url.resolve('http://example.com/one', '/two')
'http://example.com/two'
querystring
1 | > querystring.parse('foo=bar&baz=qux&baz=quux&corge'); |
小结
- http模块支持服务端模式和客户端模式两种使用方式;
- request和response对象除了用于读写头数据外,可以当作数据流来操作;
- url.parse方法加上request.url属性是处理HTTP请求时的固定搭配。
问题
- http模块和https模块的区别?
- 如何创建一个https服务器?
进程操作
API一览
Process
process
是一个全局的对象,可以在node环境中随处访问。并且它是EventEmitter的实例。
一个进程对象里头到底包含些什么属性?
1 | pid |
只在POSIX平台支持的函数
1 | getuid |
进程ID、标准输入输出以及错误流、启动进程的参数、运行环境、运行时权限。
应用场景
获取命令行参数
1
2
3
4
5
6
7
8// index.js
console.log(process.argv);
> node index.js hello
[ 'C:\\Program Files\\nodejs\\node.exe', //node的执行路径
'C:\\Users\\qianyan\\Projects\\lesson5\\index.js', //文件路径
'hello' ] //参数一般获取参数的写法
1
process.argv.splice(2)
退出程序
类似Java中的System.exit(1)
,当我们捕获一个异常,同时觉得程序需要立即停止时,就执行process.exit(1)
来表示非正常退出。控制输入和输出
stdin
是只读流,而stdout
和stderr
都是只写流。console.log
等价于1
2
3console.log = (msg) => {
process.stdout.write(`${msg}\n`);
};
Child Process
child_process
是一个内置模块,可以创建和控制子进程。该模块的主要功能都是child_process.spawn()
函数提供的。其余诸如exec
, fork
, execFile
等都是对spawn()
进行的封装。
应用场景
- 创建子进程
1 | //(command[, args][, options]) |
第一参数是可执行文件的路径,第二参数是数组对应可执行文件接收的参数,第三参数用于配置子进程运行的环境和行为。
- 进程间通信
如果父子进程都是Node.js的进程,那么就可以通过IPC通道通信。
1 | //parent.js |
父进程在创建子进程的时候,使用了options.stdio
的ipc
额外开辟了一条通道,之后开始监听子进程的message
事件来接收子进程的消息,同时通过send
方法给子进程发送消息。子进程则通过process
对象监听来自父进程的消息,并通过process.send
方法向父进程发送消息。
Cluster
单个实例的Node.js运行在单独的进程当中。但是我们有时候可能需要利用多核处理器的优势,在每个单独的核上跑一个Node.js的进程。Cluster
就是创造出来简化多进程服务程序开发的,让每一个核上面运行一个工作进程,并统一通过主进程监听端口和分发请求。
应用场景
1 | const cluster = require('cluster'); |
该模块很简单地创建多个共享一个服务端口的子进程,而这些子进程是通过IPC和Master,也即父进程进行信息交互的,可应用于负载均衡。
小结
- 使用
process
对象管理进程 - 使用
child_process
对象管理子进程,其最主要的方法就是spawn
问题
异步编程
NodeJS最大的卖点——事件机制和异步IO,开发者需要按照异步的方式去组织代码。
回调
异步编程的直接体现就是回调函数,但是不是有回调函数,就是异步编程呢?
1 | function sum(arr, callback) { |
显然,这个callback
还是顺序(同步)执行的。我们知道,JS本身是单线程的,所以不具备多线程并发执行的特点,那么异步从何体现呢?
我们再看一段程序:
1 | setTimeout(function() { |
上面的例子先打印出“hello”,然后打印出“world”。看上去好像是setTimeout()
另外启动了一个“平行线程”,等待了1秒钟之后,调用回调函数打印“world”。
JS中提供了两大类异步函数,一种是计时函数,如:setTimeout
和setInterval
。另外一类是I/O异步函数,如:fs.readFile
。
但是JS是单线程的。也就是说如果“主”线程一直处于忙碌状态,即使“平行”线程完成工作,通知“主”线程调用它的回调函数,也会等到“主”线程空闲了才能真正去调用。
1 | var t = new Date(); |
返回值
我们分别使用同步和异步实现一个函数,判断当前目录下的文件是否都是File,最终程序返回一个布尔值的数组,如:[true, false]
当前目录文件结构如下:
1 | |_async.js |
比较中学习
- 同步方式下
1 | const fs = require('fs'); |
- 异步方式下
失败的尝试
1 | const fs = require('fs'); |
成功的尝试
1 | const fs = require('fs'); |
总结
- 同步方法顺序取返回值,而异步方法总是在回调函数的取返回值
- 循环遍历中调用同步方法很容易,但是同样地在异步方法中,需要使用标志位来判断是否所有回调函数都已经调用完毕
- 异步函数的执行回调是无序的
数组的串行处理
我们看到上个例子里的异步的写法,最后的返回结果其实是无序的。使用标志位只能保证数组中的所有数据对应的回调函数都得以执行,但不能保证哪个回调函数先返回。要想顺序执行,那么必须是一个回调函数中包含另一个回调函数。拿上面的例子尝试:
1 | const fs = require('fs'); |
在场景中学习
假如我们有这样一个场景:有一系列的HTTP请求的URL构成的数组和一个初始值。这些HTTP请求是有依赖的,后一个的执行必须依赖前一个HTTP请求的响应。如果只是两个请求,我们可以很轻松地写出这样的代码:
1 | const http = require('http'); |
但如果是十个或者更多,这样的写法就不好使了。
我们知道异步函数必须在回调中才能使用其返回值,这样会很容易写出类似于>
形状的回调套回调的写法。而递归的写法也正好符合这样的形状,所以尝试一下:
1 | const urls = ['localhost', 'www.baidu.com']; //多个urls的数组 |
总结
- 在异步函数想要保证执行的顺序,就必须一个回调套一个回调
- 可以利用递归的写法,在保证执行顺序的同时,处理系列或者不定长度的数据
异常处理
在比较中学习
- 同步方式下
1 | //try ... catch ... |
- 异步方式下
1 | try { |
可以看到,同步方式下异常会沿着代码执行路径一直冒泡,直到遇到第一个try语句时被捕获住。但由于异步函数会打断代码执行路径,异步函数执行过程中以及执行之后产生的异常冒泡到执行路径被打断的位置时,如果一直没有遇到try语句,就作为一个全局异常抛出。
解决方式就是在异常被作为全局异常抛出之前,try-catch住,如下:
1 | setTimeout(() => { |
这样异常又被捕获了。不妨,对setTimetout
做一次封装
1 | function wrapSetTimeout(fn, callback) { |
Node.js的整个异步函数的异常设计都是如此,callback的首个参数都是err。
总结
- try-catch在同步方式下很有效,但在异步方式下做不到
- callback首个参数是err,是因为大多数API都遵循了一致的风格
小结
- 不掌握异步编程就不算学会NodeJS
- 异步编程依托于回调来实现,而使用回调不一定就是异步编程
- 异步编程下的函数间数据传递、数组遍历和异常处理与同步编程有很大差别
参考链接
[1] 七天学会NodeJS
[2] ECMAScript 6入门 - 阮一峰