了不起的Nodejs学习笔记(前五章)
五大部分组成
- Node核心设计理念
- Node核心模块API
- Web开发
- 数据库
- 测试
一、安装与概念
1、执行文件
-
Node.js通过
node
命令来执行Node脚本 -
创建server.js
var http = require('http'); var server = http.createServer(function (req,res){ res.writeHead(200); res.end('Hello World'); }); server.listen(3000);
-
执行命令:
node server.js
-
浏览器输入:https://localhost:3000
2、NPM
2.1、安装模块
-
执行以下命令
mkdir my-project/ cd my-project/ npm install colors
-
验证安装成功可以查看 my-project/node_modules/colors目录
-
创建index.js
require('colors'); console.log('smashing node'.rainbow)
-
执行命令
node index.js # 输出:smashing node (彩色字体)
2.2、自定义模块
-
创建package.json文件
- 好处
- 不需要将整个node_modules目录发给别人
- 方便记录所依赖模块的版本号
- 分享更简单(
npm publish
发布到NPM库)
- 好处
-
创建项目
mkdir my-project/ cd my-project/ touch package.json
-
编辑package.json
{ "name":"my-colors-project", "version":"0.0.1", "dependencies":{ "colors":"1.4.0" } }
文件为json文件,遵循JSON格式。
-
创建index.js
require('colors'); console.log('smashing node'.rainbow)
-
执行命令
npm install node index # 注意:这里文件名不需要加'.js'后缀
-
发布
-
编辑package.json
{ "name":"my-colors-project", "version":"0.0.1", "main":"./index", "dependencies":{ "colors":"1.4.0" } }
- 当别人使用
require('my-project')
时,为了能够让Node知道该载入哪个文件,我们可以使用main属性来指定 - 查看package.json文件所有的属性文档,可以使用命令:
npm help json
- 如果不想发布模块,在package.json文件加入
"private":"true"
。可以避免误发布
- 当别人使用
-
发布模块
npm publish
-
2.3、安装二进制工具包
有的项目分发的Node编写的命令行工具。安装时要增加-g
标志
举例来说,Web框架express就包含一个用于创建项目的可执行工具
npm install -g express
安装后,执行以下命令
# 创建目录
mkdir my-site
# 进入目录
cd my-site
express
如果想要发布此类脚本。发布时,在package.json文件添加"bin":"./path/to/script"
项,并将其值指向可执行的脚本或者二进制文件。
2.4、浏览NPM仓库
-
npm search 模块名
- 该命令会在已发布模块的name、tags以及description字段中搜索此关键字,并返回匹配的模块。
-
npm view 模块名
- 在
search
命令找到感兴趣模块后,通过该命令查看package.json文件及与NPM相关的属性
- 在
-
npm help
- 可以查看某个NPM命令的帮助文档
npm help publish
就会叫你如何发布模块
二、JavaScript概览
1、介绍
- 基于原型、面向对象、弱类型的动态脚本语言
- 根据ECMAScript语言标准来实现。
2、基础
2.1、类型
- 基本类型:访问基本类型,访问的是值
- number
- boolean
- string
- null
- undefined
- 复杂类型:访问复杂类型,访问的是对值的引用
- array
- function
- object
2.2、类型的困惑
要在JavaScript中准确无误的判断变量值的类型并非易事。
-
字符串
// 创建字符串的两种形式 var a = 'woot'; var b = new String('woot'); a + b; // 'woot woot' // 用 typeof 和 instanceof 操作符判断 typeof a; // 'string' typeof b; // 'object' a instanceof String; // false b instanceof String; // true // == 与 === a == b; // true a === b; // false // 事实上,两个变量都是字符串 a.substr == b.substr; // true
考虑到以上差异,建议始终通过直观方式进行定义,避免使用new。
-
条件表达式中的一些特定值会被判定为false
// null、undefined、' '、0 var a = 0; if(a){ // 这里始终不会被执行 } a == false; // true a === false; // false // typeof不会把null识别为类型为null typeof null = 'object'; // true // 数组也不例外,就算是通过[]定义数组也是如此 typeof [] = 'object'; // true // 判断数组 Object.prototype.toString.call([]) == '[object Array]'; // instanceof Array这种方式只适用于与数组初始化在相同上下文中才有效
2.3、函数
在JavaScript中,函数最为重要。
/** 以下属于一级函数:可以作为引用存储在变量中,随后可以像其他对象一样,进行传递 **/
var a = function(){}
console.log(a); // 将函数作为参数传递
/** JavaScript中所有函数都可以进行命名。有一点很重要,就是要能区分出函数名和变量名 **/
var a = function a (){
console.log('function' == typeof a); // true
}
a();
/** THIS、FUNCTION#CALL、FUNCTION#APPLY **/
// 下述代码中函数被调用时,this的值是全局对象。在浏览器中,就是windows对象
function a (){
console.log(window == this); // true
}
a();
// 调用以下函数,使用.call和.apply方法可以改变this的值
function a () {
console.log(this.a == 'b'); // true
}
a.call({a:'b'});
// call和apply的区别在于,call接受参数列表,而apply接受一个参数数组
function a (b,c){
console.log(b == 'first'); // true
console.log(c == 'second'); // true
}
a.call({a:'b'},'first','second');
a.apply({a:'b'},['first','second']);
2.4、函数的参数数量
该属性指明函数声明时可接受的参数数量。在JavaScript中,该属性名为length
var a = function (a,b,c);
a.length == 3; // true
// 尽管这在浏览器端很少用,但,在流行的Node.js框架就是通过此属性来根据不同参数个数提供不同的功能。
2.5、闭包
在JavaScript中,每次函数调用时,新的作用域就会产生。
2.5.1、作用域
// 在某个作用域中定义变量只能在该作用域或其内部作用域(该作用域中定义的作用域)中才能访问到
var a = 5;
function woot () {
console.log(a == 5); // false
var a = 6;
function test(){
console.log(a == 6); // true
}
test();
};
woot();
2.5.2、自执行函数
// 自执行函数是一种机制,通过这种机制声明和调用一个匿名函数,能够达到仅定义一个新作用域的作用
var a = 3;
(function () {
var a = 5;
})();
console.log(a == 3); // true
// 自执行函数对声明私有变量是有用的,这样可以让私有变量不被其他代码访问。
2.6、类
// JavaScript中没有`class`关键字。类只能通过函数定义
function Animal(){}
// 要给所有Animal的实例定义函数,可以通过prototype属性来完成
Animal.prototype.eat = function (food) {
// eat method
}
// 值得一提的是,在prototype的函数内部,this并非像普通函数那样指向global对象
// 而是指向通过该类创建的实例对象
function Animal(name) {
this.name = name;
}
Animal.prototype.getName = function(){
return this.name;
}
var animal = new Animal('tobi');
console.log(animal.getName() == 'tobi'); //true
2.7、继承
JavaScript有基于原型的继承的特点。通常,你可以通过以下方式来模拟类继承。
// 定义Animal
function Animal(name) {
this.name = name;
}
// Animal类方法eat
Animal.prototype.eat = function (food) {
console.log("吃:"+ food)
}
// 定义Ferret类:一个要继承自Animal的构造器
function Ferret () {};
// 要定义继承链,首先创建一个Animal对象,然后将其赋值给Ferret.prototype
Ferret.prototype = new Animal();
// 随后,可以为子类定义属性和方法。
Ferret.prototype.type = 'domestic';
// 通过prototype重写和调用父类函数
Ferret.prototype.eat = function (food) {
// 第一种调用
Animal.prototype.eat(food);
// 第二种调用
Animal.prototype.eat.call(this,food)
// ferret特有的逻辑写在这里
}
// 构建ferret
var ferret = new Ferret();
// 打印type属性值
console.log(ferret.type);
// 调用父类方法
ferret.eat('冰淇凌');
// 这项技术很赞,它是同类方案中最好的(相比其他函数式技巧),而且它不会破坏instanceof操作符的结果
var animal = new Animal();
console.log(animal instanceof Animal); // true
console.log(animal instanceof Ferret); // false
var ferret1 = new Ferret();
console.log(ferret1 instanceof Animal); // true
console.log(ferret1 instanceof Ferret); // true
// 它最大的不足就是声明继承的时候创建的对象总要进行初始化`Ferret.prototype = new Animal()`
// 一种解决该问题的方法就是在构造器中添加判断条件
function Animal (a) {
if (false !== a) return;
// 初始化
}
Ferret.prototype = new Animal(false);
// 另外一种方法就是再定义一个新的空构造器,并重写它的原型
function Animal () {
// constructor struff
}
function f () {};
f.prototype = Animal.prototype;
Ferret.prototype = new f;
// V8提供了更简洁的解决方案,见后文
2.8、异常捕获
/** try/catch允许进行异常捕获。**/
// 以下代码会抛出异常
var a = 5;
a() // TypeError: a is not a function
// 当函数抛出错误时,代码就停止执行了
function () {
throw new Error('hi');
console.log('hi') // 这里永远不会被执行到
}
// 若使用try/catch则可以进行错误处理,并让代码继续执行
function () {
var a = 5;
try{
a();
}catch (e) {
e instanceof Error; // true
}
console.log('you get here')
}
3、V8中的JavaScript
3.1、OBJECT#KEYS
// 想要获取下述对象的键(a和c)
var a = {a:'b',c:'d'}
// 通常会使用如下迭代的方式
for(var i in a){
console.log(i); // a // c
}
// 通过对键进行迭代,可以将它们收集到一个数组中。
// 不过如果采用如下方式对Object.prototype进行过扩展
Object.prototype.c = 'd'
// 为了避免再迭代过程中把c也获取到,就需要使用hasOwnProperty来进行检查
// hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
for (var i in a){
if (a.hasOwnProperty(i)){}
}
// 在V8中,要获取对象上的所有的自有键,还有更简单的方法
var a = {a:'b',c:'d'}
Object.keys(a); // ['a','c']
3.2、ARRAY#ISARRAY
// 对数组使用typeof操作符会返回object
// 下面介绍如何判断数组
console.log(Array.isArray(new Array())); // ture
console.log(Array.isArray([])); // ture
console.log(Array.isArray(null)); // false
console.log(Array.isArray(arguments)); // false
3.3、数组方法
// 1、遍历数组:forEach
[1,2,3].forEach(function (v) {
console.log(v);
})
// 打印:
// 1
// 2
// 3
// 2、过滤数组元素:filter
let filterRes = [1,2,3].filter(function (v) {
return v < 3;
});
console.log(filterRes) // [ 1, 2 ]
// 3、改变数组中每个元素的值:map
let mapRes = [5,10,15].map(function (v) {
return v * 2;
})
console.log(mapRes) // [ 10, 20, 30 ]
// 4、V8还提供了不常用的方法,如reduce、reduceRight以及lastIndexOf
3.4、字符串方法
// 移除字符首末的空格
console.log(' hello '.trim()); // 'hello'
3.5、JSON
// V8提供了JSON.stringify和JSON.parse方法来对JSON数据进行解码和编码
// JSON是一种编码标准和JavaScript对象字面量很相近,它用于大部分的Web服务和API服务:
var obj = JSON.parse('{"a":"b"}')
console.log(obj.a == 'b'); // true
3.6、FUNCTION#BIND
// .bind 允许改变对this的引用
function a () {
console.log(this.hello == 'world'); // true
}
var b = a.bind({hello:'world'})
b()
3.7、FUNCTION#NAME
// V8还支持非标准的函数属性名
var a = function woot () {};
console.log(a.name == 'woot'); // true
// 该属性用于V8内部的堆栈追踪。
// 当有错误抛出时,V8会显示一个堆栈追踪的信息,会告诉你是哪个函数调用导致了错误的发生
var woot = function () {throw new Error();};
woot() // 此时错误信息没有 函数名
var woot = function buggy () {throw new Error();};
woot() // 此时错误信息含有 函数名
// 为函数命名有助于调试,因此,推荐始终对函数进行命名
3.8、继承
// _proto_ 使得定义继承链变得更加容易
function Animal(){}
function Ferret(){}
Ferret.prototype._proto_ = Animal.prototype
- 借助中间构造器
- 借助OOP的工具类库。无须再引入第三方模块来进行基于原型继承的声明
3.9、存取器
你可以通过调用方法来定义属性,访问属性就使用__defineGetter__
、设置属性就使用__defineSetter__
// 以下例子,为所有的Date实例都添加了ago获取器,它会返回以自然语言描述的日期距离现在的时间间隔。
// 简单地访问该属性就会调用事先定义好的函数,无须显式调用
// 双下划线
Date.prototype.__defineGetter__('ago',function(){
var diff = (
( (new Date()).getTime() - this.getTime() ) / 1000
),
day_diff = Math.floor(diff / 86400);
return (
diff < 60 && "just now" ||
diff < 120 && "1 minute ago" ||
diff < 3600 && Math.floor(diff / 60 ) + " minute ago" ||
diff < 7200 && "1 hour ago" ||
diff < 86400 && Math.floor(diff / 3600 ) + " hour ago" ||
day_diff == 1 && "Yesterday" ||
day_diff < 7 && day_diff + " days ago" ||
Math.ceil( day_diff / 7 ) + " weeks ago"
)
});
var a = new Date('12/12/1990')
console.log(a.ago);
三、阻塞与非阻塞IO
1、共享状态并发
-
在Node中,需要对回调函数如何修改当前内存中的变量(状态)特别小心。
-
除此之外,你要需要特别注意对错误的处理是否会潜在地修改这些状态,从而导致整个进程不可用。
nodejs代码
var books = ['笑傲江湖','天龙八部']
function serveBooks () {
// 给客户端的html
var html = '<b>' + books.join('</b><br/><b>') + '</b>';
// 修改状态
books = [];
return html;
}
PHP代码
$books = array('笑傲江湖','天龙八部')
function serveBooks () {
$html = '<b>'.join($book,'</b><br/><b>').</b>;
$books = array();
return html;
}
- 以上两段代码,books是存放图书的数组,假设books就是状态,该数组用来将图书列表以HTML的形式返回给客户端
- 以上两端代码,都在serveBooks函数中将books数组重置
- 分别对两个服务发起各两次请求
- Node会将完整的图书列表返回给第一个请求,而第二个请求则返回一个空的图书列表
- PHP都能将完整的图书列表返回给两个请求
- 两者区别在于架构
- Node采用一个长期运行的进程。serveBooks再次被调用,此时books数组为空
- Apache会产出多个线程(每个请求一个线程),每次都会刷新状态。在PHP中当解释器再次执行时,变量$books会被重新赋值。
2、阻塞
尝试区分下面PHP代码和Node代码有什么不同?
PHP
print('Hello');
sleep(5);
print('World')
Node
console.log('Hello')
setTimeout(function () {
console.log('World')
},5000)
-
区别
-
语义区别(Node.js使用回调函数)
-
阻塞和非阻塞
-
PHP:sleep()阻塞了线程的执行。当程序进入休眠,就什么事情也不做
-
Node.js使用了事件轮询,因此setTimeout时非阻塞的。参看以下代码:
console.log('Hello') setTimeout(function () { console.log('World') },5000) console.log('Bye') // 输出: // Hello // Bye // World
-
-
-
事件轮询意味着什么?
本质上讲,Node会先注册事件,随后不停地询问内核这些事件是否已经分发。
当事件分发时,对应的回调函数就会被触发。然后继续执行下去。
如果没有事件触发,则继续执行其他代码,直到有新事件时,再去执行对应的回调函数。
-
Node并发实现也采用了事件轮询
所有像http、net这样的原生模块中的IO部分也都采用了事件轮询技术
tomeout机制中Node内部会不停地等待,并当超时完成时,触发一个和文件描述符相关的通知
-
文件描述符
文件描述符是抽象的句柄,存有对打开的文件、socket、管道等的引用
本质上,Node接受到从浏览器发来的HTTP请求时,底层的TCP连接会分配一个文件描述符
随后,如果客户端向服务器发送数据,Node就会收到该文件描述符,然后触发JavaScript回调函数
3、单线程的世界
有一点很重要,Node是单线程的。在没有第三方模块的帮助下是无法改变这一事实的。
var start = Date.now();
setTimeout(function () {
console.log(Date.now() - start);
for (var i=0;i<1000000000;i++){}
},1000)
setTimeout(function () {
console.log(Date.now() - start);
},2000)
// 打印
// 1000
// 3738
- 以上程序显示了每个setTimeout执行的时间间隔,其结果和代码中设定的值并不相同
- 原因:
- 事件轮询被JavaScript代码阻塞。
- 当第一个事件分发时,会执行JavaScript回调函数。
- 由于回调函数需要执行很长时间(循环次数很多),所以下一个事件轮询执行时间远超2秒
- 因此,JavaScript的timeout并不能严格遵守时钟设置
Node如何做到高并发?
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
4、错误处理
首先,很重要的一点。Node应用依托在一个拥有大量共享状态的大进程中。
举例来说,如果某个回调函数发生错误,整个进程都会遭殃。
var http = require('http')
http.createServer(function () {
throw new Error('错误不会被捕捉')
}).listen(3000)
因为错误未被捕获,若访问Web服务器,进程就会崩溃。
-
uncatchException处理器
// 添加了uncaughtException处理器,进程不会退出,并且之后的事情你都能掌控 process.on('uncaughtException',function (err) { console.error(err); process.exit(1); // 手动退出 }) // 以上例子中,行为方式和分发error事件的API行为方式一致
-
error事件
var net = require('net') net.createServer(function (connection) { connection.on('error',function (err){ // err是一个错误对象 }) }).listen(400); // Node中许多像http、net这样的原生模块都会分发error事件。如果该事件未处理,就会抛出未捕获的异常
除了uncaughtException和error事件外,绝大部分Node异步API接受的回调函数,第一个参数都是错误对象和NULL。
var fs = require('fs')
fs.readFile('/etc/passwd',function (err,data){
if (err) return console.error(err);
console.log(data);
})
错误处理中,每一步都很重要,因此它能让你书写更安全的程序,并且不丢失出发错误的上下文信息。
5、堆栈追踪
在JavaScript中,当错误发生时,在错误信息中,可以看到一系列的函数调用,这称为堆栈追踪。
function c () {
b();
}
function b () {
a();
}
function a () {
throw new Error('here');
}
c();
// 运行以上代码,可以看到堆栈追踪信息。
// 可以清晰看见导致错误发生的函数调用路径
Error: here
at a (e:demo est est.js:8:11)
at b (e:demo est est.js:5:5)
at c (e:demo est est.js:2:5)
at Object.<anonymous> (e:demo est est.js:10:1)
at Module._compile (internal/modules/cjs/loader.js:959:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
at Module.load (internal/modules/cjs/loader.js:815:32)
at Function.Module._load (internal/modules/cjs/loader.js:727:14)
at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
at internal/main/run_main_module.js:17:11
// 引入事件轮询
function c () {
b();
}
function b () {
a();
}
function a () {
setTimeout(function () {
throw new Error('here');
},10)
}
c();
// 可以发现,有价值的堆栈信息丢失了
// 堆栈信息显示的是从事件轮询开始的
Error: here
at Timeout._onTimeout (e:demo est est.js:10:15)
at listOnTimeout (internal/timers.js:531:17)
at processTimers (internal/timers.js:475:7)
同理,要捕获一个未来才会执行到的函数所抛出的错误是不可能的。
这会直接抛出未捕获的异常,并且catch代码块永远都不会被执行。
try{
setTimeout(function () {
throw new Error('here');
},10)
} catch(e) {}
这就是为什么Nodejs中,每步都要正确进行错误处理的原因。
一旦遗漏,就会发现错误后很难追踪,因为上下文信息都丢失了。
四、Node中的JavaScript
1、global对象
-
在浏览器中,全局对象指window对象。在window对象上定义的任何内容都可以被全局访问到。
- setTimeout:其实就是window.setTimeout
- document:其实就是window.document
-
Node中有两个类似但却各自代表不同含义的对象
- global:和window一样,任何global对象上的属性都可以被全局访问到。
- process:所有全局执行上下文中的内容都在process对象中
- 在浏览器中,只有一个window对象。
- 在Node中,也只有一个process对象。
2、实用的全局对象
- process.nextTick:将一个函数的执行时间规划到下一个事件循环中
- console:最早由Firefox中辅助开发的插件-Firebug实现。
- console.log
- console.error
3、模块系统
- JavaScript语言标准中并未为模块依赖以及模块独立定义专门的API
- Node摒弃了采用定义一堆全局变量的方式,转而引入模块系统.
- 模块系统三个核心的全局对象
- require
- module
- exports
- 模块系统三个核心的全局对象
4、绝对和相对模块
- 绝对模块:指Node通过在其内部node_modules查找到的模块,或者Node内置的如fs这样的模块。
- colors:修改了String.prorotype,因此无须暴露API。
require('colors')
- fs:暴露了一系列函数。
var fs = require('fs');fs.readFile(...)
- colors:修改了String.prorotype,因此无须暴露API。
- 相对模块:将require指向一个相对工作目录中的JavaScript文件。
require(./module)
5、暴露API
要让模块暴露一个API称为require调用的返回值,就要依靠module和exports这两个全局变量
-
默认情况下,每个模块都会暴露出一个空对象
// module_a.js exports.name = 'zhangsan' exports.data = 'data' var pv = 5; exports.getPv = function () { return pv; } // index.js var a = require('./module_a') console.log(a.name) console.log(a.data) console.log(a.getPv())
- 以上例子,exports其实就是对module.exports的引用,其在默认情况下是一个对象。
要是在该对象上逐个添加属性无法满足你都需求,你还可以彻底重写module.exports。
-
常见的将模块中构造器暴露出来的例子
// person.js module.exports = Person; function person (name) { this.name = name; } Person.prototype.talk = function () { console.log('我的名字:',this.name) } // index.js var Person = require('./person') var john = new Person('john') john.talk();
- 以上是一个具备JavaScript OOP风格的Node.js模块的例子
- 在index.js文件中,不再接受一个对象作为返回值,而是函数,归功于module.exports的重写
6、事件
Node.js中的基础API之一就是EventEmitter。
无论是在Node中还是浏览器中,大量代码都依赖于所监听或者分发的事件。
-
浏览器中负责处理事件相关的DOM API包括
- addEventListener
- removeEventListener
- dispatchEvent
- 它们还用在一系列从window到XMLHTTPRequest等的其他对象上
-
Node暴露了Event EmitterAPI
-
on
-
emit
-
removeListener
-
以process.EventEmitter形式暴露
var EventEmitter = require('events').EventEmitter,a = new EventEmitter; a.on('event',function () { console.log('event called') }) a.emit('event')
-
添加到自己的类
var EventEmitter = process.EventEmitter,MyClass = function (){} MyClass.prototype.__proto__ = EventEmitter.prototype; // 这样,所有MyClass的实例都具备了事件功能 var a = new MyClass(); a.on('某一事件',function () { // 做些什么 })
-
事件是Node非阻塞设计的重要体现。Node通常不会直接返回数据(因为这样可能会在等待某个资源的时候发生线程阻塞),而是采用分发事件来传递数据的方式。
以HTTP服务为例。请求到达时,Node会调用一个回调函数,这个时候数据可能不会一下子都到达。
// 当用户提交表单时,通常会监听请求的data和end事件
http.Server(function (req,res) {
var buf = '';
req.on('data',function (data) {
buf += 'data';
});
req.on('end',function () {
console.log('数据接收完毕!')
})
})
// 将请求数据内容进行缓冲(data)事件
// 等到所有数据都接收完毕(end事件)再对数据进行处理
7、buffer
除了模块之外,Node还弥补了语言另外一个不足之处,对二进制数据的处理。
-
buffer
-
是一个表示固定内存分配的全局对象(也就是说,要放到缓冲区中的字节数需要提前定下)
-
它好比一个由八位字节元素组成的数组,可以有效地在JavaScript中表示二进制数据
-
该功能的一部分作用就是可以对数据进行编码转换
// 创建一副用base64表示的图片,将其作为二进制PNG图片的形式写入到文件中 // buffers/index.js var mybuffer = new Buffer('.....','base64') console.log(mybuffer) require('fs').writeFile('logo.png',mybuffer) // 运行 node index open logo.png
-
五、命令行工具(CLI)以及FS API
1、需求
- 需求
- 程序需要在命令行运行。
- 要么通过node命令来执行
- 要么直接执行,然后通过终端提供交互给用户进行输入、输出。
- 程序启动后,需要显示当前目录列表
- 选择某个文件时,程序需要显示该文件内容
- 选择一个目录时,程序需要显示改目录下的信息
- 运行结束后程序退出
- 程序需要在命令行运行。
- 项目步骤
- 创建模块
- 决定采用同步fs还是异步fs
- 理解什么是流(Stream)
- 实现输入输出
- 重构
- 使用fs进行文件交互
- 完成
2、首个Node项目
2.1、创建模块
package.json
{
"name":"file-explorer",
"version":"0.0.1",
"description":"A command-file file explorer!"
}
# 验证package.json有无问题
npm insall
2.2、同步还是异步
- fs模块是唯一一个同时提供同步和异步API的模块
- 为了学习单线程中创建能够处理高并发的高效程序,就得采用异步、事件驱动的程序。
// index.js
var fs = require('fs')
fs.readdir(__dirname,function (err,files){
console.log(files);
})
// 输出:[ 'index.js', 'package-lock.json', 'package.json' ]
2.3、流(stream)
process全局对象包含了三个流对象,分别对应三个UNIX标准流
- stdin:标准输入。可读流。
- stdout:标准输出。可写流。
- stderr:标准错误。可写流。
stream对象和EventEmitter很像,事实上,前者继承自后者。
2.4、输入和输出
var fs = require('fs')
fs.readdir(process.cwd(), function (err, files) {
// 为了输出友好,首先输出一个空行
console.log('');
if (!files.length) {
// 如果files数组为空,告知用户当前目录没有文件
// 33[31m 与 33[39m 为了让文本呈现红色
//
换行
return console.log(' 33[31m No files to show! 33[39m
');
}
console.log(' Select which file or directory you want to see
');
function file(i) {
// 获取文件名
var filename = files[i];
// fs.stat获取文件或目录的元数据
fs.stat(__dirname + '/' + filename, function (err,stat){
// 判断是不是目录,用颜色区分目录和文件
if(stat.isDirectory()){
console.log(' ' +i+ ' 33[36m' + filename + '/ 33[39m');
}else{
console.log(' ' +i+ ' 33[90m' + filename + ' 33[39m');
}
// 计数器不断递增,与此同时,检查是否有未处理的文件
i++;
if(i == files.length){
console.log('');
// 无需换行,让用户可以直接在提示语后进行输入
process.stdout.write(' 33[33mEnter your choice: 33[39m');
// 等待用户输入
process.stdin.resume();
// 设置流编码为utf8,这样就能支持特殊字符了
process.stdin.setEncoding('utf8')
}else{
// 还有未处理的文件,则递归递归调用该函数来进行处理
file(i);
}
});
}
file(0)
})
// 直到列出所有文件,用户输入完毕后,下一步串行处理
2.5、重构
// 要做重构,我们认为从几个常量(如:stdin和stdout)创建快捷变量开始
var fs = require('fs'),stdin = process.stdin,stdout = process.stdout;
fs.readdir(process.cwd(), function (err, files) {
function file(i) {
var filename = files[i];
fs.stat(__dirname + '/' + filename, function (err,stat){
if(stat.isDirectory()){
console.log(' ' +i+ ' 33[36m' + filename + '/ 33[39m');
}else{
console.log(' ' +i+ ' 33[90m' + filename + ' 33[39m');
}
if(++i == files.length){
read()
}else{
file(i);
}
});
}
// 由于代码是异步的,因此,随着含数量的增长(特别是流控制层的增加),过多的函数嵌套会让程序的可读性变差
// 为避免此类问题,应为每一个异步操作预先定义一个函数
// 首先,抽离一个读取stdin函数
function read() {
console.log('');
stdout.write(' 33[33mEnter your choice: 33[39m');
stdin.resume();
stdin.setEncoding('utf8');
// 用户选择需要读取的文件,监听其data事件
stdin.on('data',option);
}
// 检查用户的输入是否匹配files数组中的下标
// files数组是fs.readdir回调函数的一部分
// 将utf-8编码的字符串类型data转化为Number类型,方便做检查
function option (data) {
if(!files[Number(data)]){
stdout.write(' 33[33mEnter your choice: 33[39m')
}else{
// 如果检查通过,确保再次将流暂停(回到默认状态),以便fs操作后,程序能顺利退出
stdin.pause();
}
}
file(0)
})
2.6、fs操作文件
var fs = require('fs'),stdin = process.stdin,stdout = process.stdout;
var stats = [];
fs.readdir(process.cwd(), function (err, files) {
function file(i) {
var filename = files[i];
fs.stat(__dirname + '/' + filename, function (err,stat){
// 保存Stat对象
stats[i] = stat;
if(stat.isDirectory()){
console.log(' ' +i+ ' 33[36m' + filename + '/ 33[39m');
}else{
console.log(' ' +i+ ' 33[90m' + filename + ' 33[39m');
}
if(++i == files.length){
read()
}else{
file(i);
}
});
}
function read() {
console.log('');
stdout.write(' 33[33mEnter your choice: 33[39m');
stdin.resume();
stdin.setEncoding('utf8');
stdin.on('data',option);
}
function option (data) {
var filename = files[Number(data)];
if(!files[Number(data)]){
stdout.write(' 33[33mEnter your choice: 33[39m')
}else{
stdin.pause();
// 如果选择的是目录
if(stats[Number(data)].isDirectory()){
fs.readdir(__dirname + '/' + filename,function(err,files){
console.log('');
// 包含几个文件
console.log(' (' + files.length + ' files)');
// 文件列表
files.forEach(function (file){
console.log(' - '+file);
})
console.log('');
})
}else{ // 如果选择的是文件
// 指定编码,得到的数据就是相应的字符串
fs.readFile(__dirname + '/' + filename,'utf8',function (err,data){
console.log('')
// 正则表达式添加一些辅助缩进后将文件内容进行输出
console.log(' 33[90m' + data.replace(/(.*)/g,' $1') + ' 33[39m');
});
}
}
}
file(0)
})
3、对CLI一探究竟
3.1、argv
process.argv包含了所有Node程序运行时的参数值
console.log(process.argv)
- 第一个元素始终是 node,执行文件的目录
- 第二个元素始终是执行文件的文件路径,紧接着是命令行后紧跟的参数
console.log(process.argv.slice(2))
:获取真正的元素,首先要将数组的前两个元素去掉
3.2、工作目录
- 获取当前工作目录
process.cwd
- 灵活的更改工作目录
process.chdir('/')
3.3、环境变量
process.env
通过process.env.SHELL
变量轻松访问shell环境下的变量
最常见的是process.env.NODE_ENV
,控制是生产环境还是开发环境。
3.4、退出
要让一个应用退出,可以调用process.exit
并提供一个退出代码。
当错误发生时,要退出程序,这个时候最好是使用退出代码1
process.exit(1)
3.5、信号
进程和操作系统进行通信的其中一种方式就是通过信号。
比如,要让进程终止,可以发送SIGKILL信号。
Node程序是通过在process对象上以事件分发的形式来发送信号的
process.on('SIGKILL',funciton(){
// 信号已收到
})
3.6、ANSI转义码
要在文本终端下控制格式、颜色以及其他输出选项,可以使用ANSI转义码
// 以下代码文本周围添加的明显不用于输出的字符,称为非打印字符
console.log(' 33[90m' + data.replace(/(.*)/g,' $1') + ' 33[39m');
// 33表示转义序列的开始
// [ 表示开始颜色设置
// 90表示前景色为亮灰色
// m表示颜色设置结束
4、对fs一探究竟
fs模块允许通过Stream API对数据进行读写操作。
与readFile
及writeFile
方法不同,它对内存的分配不是一次完成的。
- 考虑一个例子:
解析一个大文件,文件内容由上百万行逗号分隔文本组成。
- 解决:
- 方式一:
- 要完整的读取该文件来进行解析,意味着要一次性分配很大的内存。
- 方式二:
- 更好的方式应该是一次只读取一块内容,以行尾结束符(" ")来切分,然后再逐块进行解析。
- Node Stream就是对上述解决方案完美的实现,
- 方式一:
5 、Stream
// 以下例子中
// 回调函数必须等到整个文件读取完毕、载入到RAM、可用的情况下才会触发
fs.readFile('test.txt',function (err,contents){
// 对文件进行处理
})
// 以下例子
// 每次会读取可变大小的内容块,并且每次读取后会触发回调函数
var stream = fs.createReadStream('test.txt')
stream.on('data',function (chunk){
// 处理文件部分内容
})
stream.on('end',function (chunk){
// 文件读取完毕
})
// 视频上传到Web服务
// 网站访问日志
6、监视
Node允许监视文件或目录是否发生变化。
监视意味着当文件系统中的文件(或目录)发生变化时,会分发一个事件,然后触发指定的回调函数。
例如用一种可以编译为CSS的语言书写CSS样式。监视源文件发生改变,则编译为CSS文件
var stream = fs.createReadStream('test.txt')
var fs = require('fs')
// 获取工作目录下所有文件
var files = fs.readdirSync(process.cwd());
files.forEach(function (file){
// 监听".css"后缀的文件
if(/.css/.test(file)){
fs.watchFile(process.cwd() + '/' + file, function(){
console.log(' - ' + file + ' changed!')
})
}
})
// 除了fs.watchFile外,还可以使用fs.watch来监视整个目录